From 2ed27dd55e8c6ae7a60d3ced86b2f560d8ac1c8c Mon Sep 17 00:00:00 2001 From: tamakyi Date: Tue, 8 Sep 2026 10:10:44 +0800 Subject: [PATCH] 0904 --- .../app.sample.ini | 154 + .../autoban.go | 266 ++ .../autoban_test.go | 123 + .../autoban-track-notfound-20260905/route.go | 409 ++ .../autoban-track-notfound-20260905/static.go | 128 + .../backup/cdn-migration-20260905/header.html | 977 +++++ .../backup/cdn-migration-20260905/list.html | 309 ++ .../cdn-migration-20260905/profile.html | 1527 +++++++ .../question-not-found.html | 105 + .../domain-shiroko-20260906/footer.html | 102 + .../domain-shiroko-20260906/header.html | 1029 +++++ .../backup/domain-shiroko-20260906/home.html | 122 + .../domain-shiroko-20260906/llms-full.txt | 121 + .../backup/domain-shiroko-20260906/llms.txt | 29 + .../domain-shiroko-20260906/register.go | 92 + .../backup/domain-shiroko-20260906/robots.txt | 7 + .../domain-shiroko-20260906/route.go.bak | 414 ++ .../domain-shiroko-20260906/sitemap.xml | 58 + .../backup/domain-shiroko-20260906/text.go | 118 + .../internal/storage/s3.go | 279 ++ .../templates/base/header.html | 977 +++++ .../templates/question/list.html | 309 ++ .../question/question-not-found.html | 105 + .../templates/user/profile.html | 1527 +++++++ .../remove-vercount-20260906/dict_en.go | 842 ++++ .../remove-vercount-20260906/dict_ja.go | 842 ++++ .../remove-vercount-20260906/dict_zh_cn.go | 842 ++++ .../remove-vercount-20260906/dict_zh_tw.go | 842 ++++ .../remove-vercount-20260906/footer.html | 79 + .../remove-vercount-20260906/header.html | 1044 +++++ .../remove-vercount-20260906/i18n_test.go | 76 + .../tamabox.changelogs.json | 35 + .../tamami-load-inject-20260906/header.html | 1020 +++++ .../timebox-inline-20260906/footer.html | 78 + .../timebox-inline-20260906/header.html | 1029 +++++ .../backup/wp-fragments-20260906/autoban.go | 277 ++ .../wp-fragments-20260906/autoban_test.go | 143 + .workbuddy/memory/2026-09-04.md | 99 + .workbuddy/memory/2026-09-05.md | 167 + .workbuddy/memory/2026-09-06.md | 136 + .workbuddy/memory/2026-09-07.md | 83 + CHANGELOG.md | 90 + Dockerfile | 26 + LICENSE | 21 + README.md | 89 + README.zh-CN.md | 122 + cmd/tamabox.go | 29 + conf/app.sample.ini | 157 + .../notify_domain_change.cpython-313.pyc | Bin 0 -> 70851 bytes dev/add_form_keys.py | 191 + dev/bench-cdn.sh | 96 + dev/bg-test/box_background.png | Bin 0 -> 303865 bytes dev/changelog-source.md | 11 + dev/checktpl/main.go | 45 + dev/easy.qrcode.min.js | 21 + dev/eo-hits-150.json | 219 + dev/eo-hits-hkjp.json | 114 + dev/eo-hits-v2.json | 3579 +++++++++++++++++ dev/eo-probe-ips.py | 114 + dev/eo-probe-v2.py | 101 + dev/eo-scan-all.py | 127 + dev/gen_changelog.py | 129 + dev/notify_domain_change.py | 1269 ++++++ dev/overlay.json | 1 + dev/security-selfcheck.sh | 156 + dev/time.box.js | 44 + dev/webpstub/webp.go | 20 + go.mod | 146 + go.sum | 1268 ++++++ internal/cache/cache.go | 87 + internal/cache/cache_test.go | 61 + internal/captcha/math.go | 273 ++ internal/cmd/ban.go | 105 + internal/cmd/censor.go | 86 + internal/cmd/uid.go | 61 + internal/cmd/web.go | 107 + internal/conf/conf.go | 105 + internal/conf/db.go | 27 + internal/conf/limits.go | 119 + internal/conf/limits_test.go | 127 + internal/conf/static.go | 133 + internal/context/auth.go | 57 + internal/context/context.go | 327 ++ internal/context/flash.go | 52 + internal/db/censor_log.go | 81 + internal/db/censor_log_test.go | 85 + internal/db/conversation.go | 231 ++ internal/db/db.go | 234 ++ internal/db/feature_control.go | 723 ++++ internal/db/feature_control_test.go | 341 ++ internal/db/follow_ups.go | 221 + internal/db/main_test.go | 18 + internal/db/message.go | 132 + internal/db/message_image.go | 63 + internal/db/password.go | 85 + internal/db/questions.go | 546 +++ internal/db/questions_test.go | 467 +++ internal/db/upload_images.go | 190 + internal/db/upload_images_test.go | 133 + internal/db/user_block.go | 21 + internal/db/users.go | 921 +++++ internal/db/users_test.go | 428 ++ internal/db/vote_form_fields.go | 184 + internal/db/vote_form_fields_test.go | 109 + internal/db/vote_options.go | 163 + internal/db/vote_options_test.go | 116 + internal/db/vote_submissions.go | 256 ++ internal/db/vote_submissions_test.go | 308 ++ internal/db/votes.go | 377 ++ internal/db/votes_test.go | 252 ++ internal/dbutil/clock.go | 11 + internal/dbutil/model.go | 24 + internal/dbutil/pagination.go | 46 + internal/dbutil/testing.go | 145 + internal/form/auth.go | 31 + internal/form/form.go | 172 + internal/form/form_test.go | 94 + internal/form/question.go | 43 + internal/form/user.go | 53 + internal/i18n/dict_en.go | 840 ++++ internal/i18n/dict_ja.go | 840 ++++ internal/i18n/dict_zh_cn.go | 840 ++++ internal/i18n/dict_zh_tw.go | 840 ++++ internal/i18n/i18n.go | 133 + internal/i18n/i18n_test.go | 76 + internal/imagecheck/imagecheck.go | 81 + internal/imagecheck/imagecheck_test.go | 126 + internal/mail/mail.go | 138 + internal/route/autoban.go | 45 + internal/route/bodylimit.go | 54 + internal/route/bodylimit_test.go | 81 + internal/route/chat/chat_handler.go | 412 ++ internal/route/csrf_binding_test.go | 120 + internal/route/gzip.go | 150 + internal/route/indexnow_route_test.go | 63 + internal/route/ipblacklist.go | 87 + internal/route/ratelimit.go | 183 + internal/route/ratelimit_test.go | 27 + internal/route/route.go | 441 ++ internal/security/autoban/autoban.go | 298 ++ internal/security/autoban/autoban_test.go | 157 + internal/security/censor/aliyun.go | 194 + internal/security/censor/censor.go | 96 + internal/security/censor/qiniu.go | 178 + internal/security/censor/text.go | 121 + internal/security/censor/text_test.go | 29 + internal/shared/blockword.go | 26 + internal/shared/blockword_test.go | 32 + internal/shared/duplicate.go | 94 + internal/shared/goutil.go | 20 + internal/shared/ip.go | 176 + internal/shared/ip_test.go | 127 + internal/shared/multidial.go | 61 + internal/shared/multidial_test.go | 127 + internal/shared/state.go | 44 + internal/shared/state_test.go | 77 + internal/stats/stats.go | 100 + internal/storage/client.go | 81 + internal/storage/oss.go | 66 + internal/storage/picture.go | 13 + internal/storage/s3.go | 282 ++ internal/template/date_pattern.go | 50 + internal/template/markdown.go | 265 ++ internal/template/markdown_test.go | 171 + internal/template/minify.go | 119 + internal/template/template.go | 220 + internal/tracing/config.go | 62 + internal/tracing/flamego.go | 62 + internal/tracing/tracing.go | 27 + outputs/TamaBox-v4-code-review-2026-08-27.md | 113 + ...aBox-v4-code-review-followup-2026-08-26.md | 140 + ...amaBox-v4-code-review-report-2026-08-26.md | 105 + ...4-feature-control-fix-review-2026-08-31.md | 144 + ...pagination-timezone-incident-2026-09-05.md | 146 + outputs/TamaBox-v4-remediation-2026-08-27.md | 65 + outputs/admin6-cdn-check.png | Bin 0 -> 375873 bytes outputs/admin6-snrat-check.png | Bin 0 -> 375853 bytes outputs/autoban-false-positive-2026-09-05.md | 93 + outputs/avatar-qrcode-slow-2026-09-05.md | 149 + outputs/cloudflare-origin-lockdown-guide.md | 167 + outputs/code-review-2026-08-28.md | 178 + outputs/code-review-2026-08-31.md | 132 + outputs/code-review-2026-09-02-late-batch.md | 138 + ...domain-migration-box-shiroko-2026-09-06.md | 279 ++ outputs/edgeone-cloudflare-dual-cdn-guide.md | 155 + outputs/edgeone-noicp-cf-tunnel-guide.md | 252 ++ outputs/edgeone-origin-whitelist-guide.md | 299 ++ outputs/eo-free-probe-2026-09-05.md | 114 + outputs/feature-mode-2026-09-01.md | 47 + outputs/p0-p1-fixes-2026-09-01.md | 77 + outputs/p1-fixes-2026-08-31.md | 51 + outputs/p2-fixes-and-p0-alt-2026-09-01.md | 76 + outputs/pg-cursor-diagnose-v2-nekobox.sql | 234 ++ outputs/pg-cursor-pagination-diagnose.sql | 224 ++ outputs/prod-tamakyi-snrat-check.png | Bin 0 -> 635502 bytes outputs/proxy-image-timeout-2026-09-02.md | 90 + outputs/r2-assets-cdn-audit-2026-09-05.md | 115 + outputs/re-audit-2026-09-01.md | 73 + outputs/security-review-2026-09-02.md | 253 ++ ...-egress-vote-antispam-review-2026-08-26.md | 76 + outputs/tb-dev-test-qr-crop.png | Bin 0 -> 53132 bytes outputs/tb-dev-test-qr.png | Bin 0 -> 312078 bytes outputs/tb-page-snapshot.html | 1712 ++++++++ outputs/tb-prod-test-qr-crop.png | Bin 0 -> 134424 bytes outputs/tb-prod-test-qr.png | Bin 0 -> 343674 bytes outputs/timestamptz-migration-fix-8h.sql | 124 + outputs/timestamptz-migration-online.sql | 158 + outputs/tutorial-dual-cdn-split-routing.md | 314 ++ outputs/user-updates-2026-09-01-02.md | 37 + outputs/vote-i18n-audit-2026-09-01.md | 53 + route/auth/login.go | 77 + route/auth/logout.go | 14 + route/auth/recover.go | 172 + route/auth/register.go | 92 + route/home.go | 44 + route/pixel/pixel.go | 87 + route/question/follow_up.go | 208 + route/question/page.go | 787 ++++ route/question/question.go | 338 ++ route/question/share.go | 970 +++++ route/service/proxy.go | 148 + route/user/explore.go | 299 ++ route/user/features.go | 351 ++ route/user/harassment.go | 139 + route/user/password.go | 56 + route/user/profile.go | 599 +++ route/user/question.go | 480 +++ route/user/tags.go | 73 + route/user/vote.go | 1318 ++++++ route/vote/vote.go | 1128 ++++++ route/vote/vote_stats_test.go | 262 ++ scripts/edgeone-origin-whitelist.sh | 522 +++ static/favicon.ico | Bin 0 -> 4286 bytes static/fs.go | 12 + .../66e5e0911b0941f2ba7dbfbbd961e9be.txt | 1 + static/llms-full.txt | 121 + static/llms.txt | 29 + static/robots.txt | 7 + static/sitemap.xml | 47 + static/tamabox.changelogs.json | 35 + tamabox.go | 30 + templates/about-me.html | 11 + templates/auth/forgot-password-sent.html | 6 + templates/auth/forgot-password.html | 23 + templates/auth/login.html | 29 + templates/auth/password-recovery.html | 23 + templates/auth/register.html | 61 + templates/base/alert.html | 32 + templates/base/footer.html | 102 + templates/base/header.html | 1031 +++++ templates/change-logs.html | 15 + templates/chat/conversation.html | 195 + templates/embed.go | 12 + templates/home.html | 123 + templates/mail/new-answer.html | 120 + templates/mail/new-conversation.html | 15 + templates/mail/new-follow-up.html | 112 + templates/mail/new-message.html | 15 + templates/mail/new-question-to-user.html | 109 + templates/mail/new-question.html | 109 + templates/mail/new-vote.html | 75 + templates/mail/password-recovery.html | 109 + templates/mail/security-alert.html | 66 + templates/maintenance-mode.html | 8 + templates/percent_check_test.go | 148 + templates/pixel.html | 19 + templates/question/history-template.html | 83 + templates/question/item.html | 425 ++ templates/question/list.html | 310 ++ templates/question/new-question-template.html | 232 ++ .../question/new-question-template.html.bak | 145 + templates/question/password.html | 62 + templates/question/question-not-found.html | 106 + templates/question/success.html | 54 + templates/render_public_test.go | 271 ++ templates/render_test.go | 313 ++ templates/sponsor.html | 108 + templates/templates_test.go | 85 + templates/user/deactivate.html | 17 + templates/user/explore.html | 621 +++ templates/user/feature-control-form.html | 135 + templates/user/global-blockwords.html | 29 + templates/user/profile.html | 1528 +++++++ templates/user/question-list.html | 486 +++ templates/user/vote-form.html | 392 ++ templates/user/vote-results.html | 182 + templates/vote/page.html | 526 +++ templates/vote_templates_test.go | 465 +++ 288 files changed, 67061 insertions(+) create mode 100644 .workbuddy/backup/autoban-track-notfound-20260905/app.sample.ini create mode 100644 .workbuddy/backup/autoban-track-notfound-20260905/autoban.go create mode 100644 .workbuddy/backup/autoban-track-notfound-20260905/autoban_test.go create mode 100644 .workbuddy/backup/autoban-track-notfound-20260905/route.go create mode 100644 .workbuddy/backup/autoban-track-notfound-20260905/static.go create mode 100644 .workbuddy/backup/cdn-migration-20260905/header.html create mode 100644 .workbuddy/backup/cdn-migration-20260905/list.html create mode 100644 .workbuddy/backup/cdn-migration-20260905/profile.html create mode 100644 .workbuddy/backup/cdn-migration-20260905/question-not-found.html create mode 100644 .workbuddy/backup/domain-shiroko-20260906/footer.html create mode 100644 .workbuddy/backup/domain-shiroko-20260906/header.html create mode 100644 .workbuddy/backup/domain-shiroko-20260906/home.html create mode 100644 .workbuddy/backup/domain-shiroko-20260906/llms-full.txt create mode 100644 .workbuddy/backup/domain-shiroko-20260906/llms.txt create mode 100644 .workbuddy/backup/domain-shiroko-20260906/register.go create mode 100644 .workbuddy/backup/domain-shiroko-20260906/robots.txt create mode 100644 .workbuddy/backup/domain-shiroko-20260906/route.go.bak create mode 100644 .workbuddy/backup/domain-shiroko-20260906/sitemap.xml create mode 100644 .workbuddy/backup/domain-shiroko-20260906/text.go create mode 100644 .workbuddy/backup/p0-avatar-qrcode-20260905/internal/storage/s3.go create mode 100644 .workbuddy/backup/p0-avatar-qrcode-20260905/templates/base/header.html create mode 100644 .workbuddy/backup/p0-avatar-qrcode-20260905/templates/question/list.html create mode 100644 .workbuddy/backup/p0-avatar-qrcode-20260905/templates/question/question-not-found.html create mode 100644 .workbuddy/backup/p0-avatar-qrcode-20260905/templates/user/profile.html create mode 100644 .workbuddy/backup/remove-vercount-20260906/dict_en.go create mode 100644 .workbuddy/backup/remove-vercount-20260906/dict_ja.go create mode 100644 .workbuddy/backup/remove-vercount-20260906/dict_zh_cn.go create mode 100644 .workbuddy/backup/remove-vercount-20260906/dict_zh_tw.go create mode 100644 .workbuddy/backup/remove-vercount-20260906/footer.html create mode 100644 .workbuddy/backup/remove-vercount-20260906/header.html create mode 100644 .workbuddy/backup/remove-vercount-20260906/i18n_test.go create mode 100644 .workbuddy/backup/remove-vercount-20260906/tamabox.changelogs.json create mode 100644 .workbuddy/backup/tamami-load-inject-20260906/header.html create mode 100644 .workbuddy/backup/timebox-inline-20260906/footer.html create mode 100644 .workbuddy/backup/timebox-inline-20260906/header.html create mode 100644 .workbuddy/backup/wp-fragments-20260906/autoban.go create mode 100644 .workbuddy/backup/wp-fragments-20260906/autoban_test.go create mode 100644 .workbuddy/memory/2026-09-04.md create mode 100644 .workbuddy/memory/2026-09-05.md create mode 100644 .workbuddy/memory/2026-09-06.md create mode 100644 .workbuddy/memory/2026-09-07.md create mode 100644 CHANGELOG.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 README.zh-CN.md create mode 100644 cmd/tamabox.go create mode 100644 conf/app.sample.ini create mode 100644 dev/__pycache__/notify_domain_change.cpython-313.pyc create mode 100644 dev/add_form_keys.py create mode 100644 dev/bench-cdn.sh create mode 100644 dev/bg-test/box_background.png create mode 100644 dev/changelog-source.md create mode 100644 dev/checktpl/main.go create mode 100644 dev/easy.qrcode.min.js create mode 100644 dev/eo-hits-150.json create mode 100644 dev/eo-hits-hkjp.json create mode 100644 dev/eo-hits-v2.json create mode 100644 dev/eo-probe-ips.py create mode 100644 dev/eo-probe-v2.py create mode 100644 dev/eo-scan-all.py create mode 100644 dev/gen_changelog.py create mode 100644 dev/notify_domain_change.py create mode 100644 dev/overlay.json create mode 100644 dev/security-selfcheck.sh create mode 100644 dev/time.box.js create mode 100644 dev/webpstub/webp.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/cache/cache.go create mode 100644 internal/cache/cache_test.go create mode 100644 internal/captcha/math.go create mode 100644 internal/cmd/ban.go create mode 100644 internal/cmd/censor.go create mode 100644 internal/cmd/uid.go create mode 100644 internal/cmd/web.go create mode 100644 internal/conf/conf.go create mode 100644 internal/conf/db.go create mode 100644 internal/conf/limits.go create mode 100644 internal/conf/limits_test.go create mode 100644 internal/conf/static.go create mode 100644 internal/context/auth.go create mode 100644 internal/context/context.go create mode 100644 internal/context/flash.go create mode 100644 internal/db/censor_log.go create mode 100644 internal/db/censor_log_test.go create mode 100644 internal/db/conversation.go create mode 100644 internal/db/db.go create mode 100644 internal/db/feature_control.go create mode 100644 internal/db/feature_control_test.go create mode 100644 internal/db/follow_ups.go create mode 100644 internal/db/main_test.go create mode 100644 internal/db/message.go create mode 100644 internal/db/message_image.go create mode 100644 internal/db/password.go create mode 100644 internal/db/questions.go create mode 100644 internal/db/questions_test.go create mode 100644 internal/db/upload_images.go create mode 100644 internal/db/upload_images_test.go create mode 100644 internal/db/user_block.go create mode 100644 internal/db/users.go create mode 100644 internal/db/users_test.go create mode 100644 internal/db/vote_form_fields.go create mode 100644 internal/db/vote_form_fields_test.go create mode 100644 internal/db/vote_options.go create mode 100644 internal/db/vote_options_test.go create mode 100644 internal/db/vote_submissions.go create mode 100644 internal/db/vote_submissions_test.go create mode 100644 internal/db/votes.go create mode 100644 internal/db/votes_test.go create mode 100644 internal/dbutil/clock.go create mode 100644 internal/dbutil/model.go create mode 100644 internal/dbutil/pagination.go create mode 100644 internal/dbutil/testing.go create mode 100644 internal/form/auth.go create mode 100644 internal/form/form.go create mode 100644 internal/form/form_test.go create mode 100644 internal/form/question.go create mode 100644 internal/form/user.go create mode 100644 internal/i18n/dict_en.go create mode 100644 internal/i18n/dict_ja.go create mode 100644 internal/i18n/dict_zh_cn.go create mode 100644 internal/i18n/dict_zh_tw.go create mode 100644 internal/i18n/i18n.go create mode 100644 internal/i18n/i18n_test.go create mode 100644 internal/imagecheck/imagecheck.go create mode 100644 internal/imagecheck/imagecheck_test.go create mode 100644 internal/mail/mail.go create mode 100644 internal/route/autoban.go create mode 100644 internal/route/bodylimit.go create mode 100644 internal/route/bodylimit_test.go create mode 100644 internal/route/chat/chat_handler.go create mode 100644 internal/route/csrf_binding_test.go create mode 100644 internal/route/gzip.go create mode 100644 internal/route/indexnow_route_test.go create mode 100644 internal/route/ipblacklist.go create mode 100644 internal/route/ratelimit.go create mode 100644 internal/route/ratelimit_test.go create mode 100644 internal/route/route.go create mode 100644 internal/security/autoban/autoban.go create mode 100644 internal/security/autoban/autoban_test.go create mode 100644 internal/security/censor/aliyun.go create mode 100644 internal/security/censor/censor.go create mode 100644 internal/security/censor/qiniu.go create mode 100644 internal/security/censor/text.go create mode 100644 internal/security/censor/text_test.go create mode 100644 internal/shared/blockword.go create mode 100644 internal/shared/blockword_test.go create mode 100644 internal/shared/duplicate.go create mode 100644 internal/shared/goutil.go create mode 100644 internal/shared/ip.go create mode 100644 internal/shared/ip_test.go create mode 100644 internal/shared/multidial.go create mode 100644 internal/shared/multidial_test.go create mode 100644 internal/shared/state.go create mode 100644 internal/shared/state_test.go create mode 100644 internal/stats/stats.go create mode 100644 internal/storage/client.go create mode 100644 internal/storage/oss.go create mode 100644 internal/storage/picture.go create mode 100644 internal/storage/s3.go create mode 100644 internal/template/date_pattern.go create mode 100644 internal/template/markdown.go create mode 100644 internal/template/markdown_test.go create mode 100644 internal/template/minify.go create mode 100644 internal/template/template.go create mode 100644 internal/tracing/config.go create mode 100644 internal/tracing/flamego.go create mode 100644 internal/tracing/tracing.go create mode 100644 outputs/TamaBox-v4-code-review-2026-08-27.md create mode 100644 outputs/TamaBox-v4-code-review-followup-2026-08-26.md create mode 100644 outputs/TamaBox-v4-code-review-report-2026-08-26.md create mode 100644 outputs/TamaBox-v4-feature-control-fix-review-2026-08-31.md create mode 100644 outputs/TamaBox-v4-keyset-pagination-timezone-incident-2026-09-05.md create mode 100644 outputs/TamaBox-v4-remediation-2026-08-27.md create mode 100644 outputs/admin6-cdn-check.png create mode 100644 outputs/admin6-snrat-check.png create mode 100644 outputs/autoban-false-positive-2026-09-05.md create mode 100644 outputs/avatar-qrcode-slow-2026-09-05.md create mode 100644 outputs/cloudflare-origin-lockdown-guide.md create mode 100644 outputs/code-review-2026-08-28.md create mode 100644 outputs/code-review-2026-08-31.md create mode 100644 outputs/code-review-2026-09-02-late-batch.md create mode 100644 outputs/domain-migration-box-shiroko-2026-09-06.md create mode 100644 outputs/edgeone-cloudflare-dual-cdn-guide.md create mode 100644 outputs/edgeone-noicp-cf-tunnel-guide.md create mode 100644 outputs/edgeone-origin-whitelist-guide.md create mode 100644 outputs/eo-free-probe-2026-09-05.md create mode 100644 outputs/feature-mode-2026-09-01.md create mode 100644 outputs/p0-p1-fixes-2026-09-01.md create mode 100644 outputs/p1-fixes-2026-08-31.md create mode 100644 outputs/p2-fixes-and-p0-alt-2026-09-01.md create mode 100644 outputs/pg-cursor-diagnose-v2-nekobox.sql create mode 100644 outputs/pg-cursor-pagination-diagnose.sql create mode 100644 outputs/prod-tamakyi-snrat-check.png create mode 100644 outputs/proxy-image-timeout-2026-09-02.md create mode 100644 outputs/r2-assets-cdn-audit-2026-09-05.md create mode 100644 outputs/re-audit-2026-09-01.md create mode 100644 outputs/security-review-2026-09-02.md create mode 100644 outputs/shared-egress-vote-antispam-review-2026-08-26.md create mode 100644 outputs/tb-dev-test-qr-crop.png create mode 100644 outputs/tb-dev-test-qr.png create mode 100644 outputs/tb-page-snapshot.html create mode 100644 outputs/tb-prod-test-qr-crop.png create mode 100644 outputs/tb-prod-test-qr.png create mode 100644 outputs/timestamptz-migration-fix-8h.sql create mode 100644 outputs/timestamptz-migration-online.sql create mode 100644 outputs/tutorial-dual-cdn-split-routing.md create mode 100644 outputs/user-updates-2026-09-01-02.md create mode 100644 outputs/vote-i18n-audit-2026-09-01.md create mode 100644 route/auth/login.go create mode 100644 route/auth/logout.go create mode 100644 route/auth/recover.go create mode 100644 route/auth/register.go create mode 100644 route/home.go create mode 100644 route/pixel/pixel.go create mode 100644 route/question/follow_up.go create mode 100644 route/question/page.go create mode 100644 route/question/question.go create mode 100644 route/question/share.go create mode 100644 route/service/proxy.go create mode 100644 route/user/explore.go create mode 100644 route/user/features.go create mode 100644 route/user/harassment.go create mode 100644 route/user/password.go create mode 100644 route/user/profile.go create mode 100644 route/user/question.go create mode 100644 route/user/tags.go create mode 100644 route/user/vote.go create mode 100644 route/vote/vote.go create mode 100644 route/vote/vote_stats_test.go create mode 100644 scripts/edgeone-origin-whitelist.sh create mode 100644 static/favicon.ico create mode 100644 static/fs.go create mode 100644 static/index-now/66e5e0911b0941f2ba7dbfbbd961e9be.txt create mode 100644 static/llms-full.txt create mode 100644 static/llms.txt create mode 100644 static/robots.txt create mode 100644 static/sitemap.xml create mode 100644 static/tamabox.changelogs.json create mode 100644 tamabox.go create mode 100644 templates/about-me.html create mode 100644 templates/auth/forgot-password-sent.html create mode 100644 templates/auth/forgot-password.html create mode 100644 templates/auth/login.html create mode 100644 templates/auth/password-recovery.html create mode 100644 templates/auth/register.html create mode 100644 templates/base/alert.html create mode 100644 templates/base/footer.html create mode 100644 templates/base/header.html create mode 100644 templates/change-logs.html create mode 100644 templates/chat/conversation.html create mode 100644 templates/embed.go create mode 100644 templates/home.html create mode 100644 templates/mail/new-answer.html create mode 100644 templates/mail/new-conversation.html create mode 100644 templates/mail/new-follow-up.html create mode 100644 templates/mail/new-message.html create mode 100644 templates/mail/new-question-to-user.html create mode 100644 templates/mail/new-question.html create mode 100644 templates/mail/new-vote.html create mode 100644 templates/mail/password-recovery.html create mode 100644 templates/mail/security-alert.html create mode 100644 templates/maintenance-mode.html create mode 100644 templates/percent_check_test.go create mode 100644 templates/pixel.html create mode 100644 templates/question/history-template.html create mode 100644 templates/question/item.html create mode 100644 templates/question/list.html create mode 100644 templates/question/new-question-template.html create mode 100644 templates/question/new-question-template.html.bak create mode 100644 templates/question/password.html create mode 100644 templates/question/question-not-found.html create mode 100644 templates/question/success.html create mode 100644 templates/render_public_test.go create mode 100644 templates/render_test.go create mode 100644 templates/sponsor.html create mode 100644 templates/templates_test.go create mode 100644 templates/user/deactivate.html create mode 100644 templates/user/explore.html create mode 100644 templates/user/feature-control-form.html create mode 100644 templates/user/global-blockwords.html create mode 100644 templates/user/profile.html create mode 100644 templates/user/question-list.html create mode 100644 templates/user/vote-form.html create mode 100644 templates/user/vote-results.html create mode 100644 templates/vote/page.html create mode 100644 templates/vote_templates_test.go diff --git a/.workbuddy/backup/autoban-track-notfound-20260905/app.sample.ini b/.workbuddy/backup/autoban-track-notfound-20260905/app.sample.ini new file mode 100644 index 0000000..10c1d99 --- /dev/null +++ b/.workbuddy/backup/autoban-track-notfound-20260905/app.sample.ini @@ -0,0 +1,154 @@ +[app] +production = true +title = "NekoBox" +icp = "" +default_lang = "" ; 站点默认语言:zh-CN / zh-TW / en / ja,留空默认简体中文(访客未设置语言时的兜底) +uptrace_dsn = "" +qiniu_access_key = "" +qiniu_access_secret = "" +aliyun_access_key = "" +aliyun_access_key_secret = "" +share_background = "https://your-default-background.jpg" ; 默认分享图背景图链接(1280x720) +font_path = "static/fonts/NotoSansSC-Regular.otf" ; 中文字体文件路径(相对项目根目录) + +[security] +enable_text_censor = true +ip_blacklist = ; 逗号分隔的IP黑名单,支持精确IP(如1.2.3.4)和CIDR(如1.2.3.0/24),留空不启用 +ip_blacklist_redirect_url = ; 黑名单IP访问时强制跳转到该URL +trust_proxy = false ; 是否信任反向代理设置的 CF-Connecting-IP / X-Real-IP / X-Forwarded-For 头。 + ; 仅当站点部署在可信反向代理(如 nginx/Cloudflare)之后且代理已覆盖这些头时设为 true; + ; 否则保持 false,防止客户端伪造 header 绕过速率限制与 IP 黑名单。 + ; ⚠️ 若站点在 Cloudflare 后且需使用下面的自动封禁,必须设为 true, + ; 否则解析到的将是 Cloudflare 边缘 IP,自动封禁会误伤所有访客。 + ; ⚠️ 开启 trust_proxy 时,必须配合「Cloudflare Tunnel / Authenticated Origin Pull / + ; 防火墙仅放行 Cloudflare / EdgeOne 回源网段」锁定源站,否则攻击者可直连源站并伪造 + ; CF-Connecting-IP / EO-Client-IP 头。无需维护 CDN 庞大 CIDR 清单。 + ; + ; —— 双 CDN(境内腾讯 EdgeOne + 境外 Cloudflare)场景 —— + ; EdgeOne 回源会用 EO-Client-IP 携带客户端真实 IP(等效 CF-Connecting-IP), + ; 本程序在 trust_proxy=true 时已同时信任这两个权威头。 + ; 两家 CDN 并存时必须:① 防火墙仅放行两家回源网段;② 在各家 CDN 的规则引擎中 + ; 删除回源请求里「对方 CDN」的权威头(经 EdgeOne 回源删 CF-Connecting-IP、 + ; 经 Cloudflare 回源删 EO-Client-IP),否则攻击者可借道另一家伪造客户端身份, + ; 绕过限流 / 自动封禁 / 单 IP 投票限制。详细步骤见 outputs/edgeone-cloudflare-dual-cdn-guide.md。 + +trusted_proxies = + ; 逗号分隔的「你自己的」反向代理/负载均衡 IP 或 CIDR(如 127.0.0.1、 + ; 10.0.0.0/8)。仅当 TCP 对端落在该清单内时才解析 X-Forwarded-For / X-Real-IP。 + ; 留空(默认)则这两个头永不被信任,仅采用 CDN 权威头 CF-Connecting-IP / + ; EO-Client-IP(需源站已锁定)。 + ; 注意:此清单用于你自己的基础设施,不是 CDN 的全量 CIDR 清单。 +enable_auto_ban = true ; 自动封禁(fail2ban 风格):检测到批量扫描/探测后临时封禁来源 IP +ban_track_notfound = false ; 是否把「未注册路径的 404」计入封禁统计(默认 false)。正常业务 + ; (域名/题目不存在等均走 302 业务跳转,不产生 404 计数)一律放行, + ; 仅命中后台扫描特征路径(/admin、/.env 等)才参与统计封禁,避免 + ; 共享出口(CDN 边缘 / CGNAT)下正常用户被 404 探测连坐误伤。 + ; 如需恢复对纯 404 字典扫描的封禁再置 true。 +ban_threshold_sensitive = 10 ; 统计窗口内命中敏感路径(/admin、/.env、/wp-admin 等)的次数阈值 +ban_threshold_404 = 20 ; 统计窗口内返回 404 的次数阈值(仅 ban_track_notfound = true 时生效) +ban_window_seconds = 60 ; 统计窗口时长(秒) +ban_duration_minutes = 15 ; 临时封禁时长(分钟),到期自动解除,重启即清零 +security_alert_email = ; 触发封禁时发送告警邮件的收件人(复用下方 SMTP 发信配置),留空不发送 + +[server] +port = 80 +; 绑定监听地址(含端口)。Cloudflare Tunnel 部署时强烈建议设为 127.0.0.1:, +; 使源站端口仅对本地 cloudflared 可见,即使防火墙误配也无法被外部直连伪造 CF-Connecting-IP。 +; 留空(默认)则监听所有网卡 ":port"。 +listen = +salt = "" +# ⚠️ 必填:密码哈希盐,留空会导致启动失败(已在 conf.Init 强制校验) +xsrf_key = "" +# ⚠️ 必填:CSRF token 密钥,留空会导致启动失败(已强制校验) +xsrf_expire = 3600 + +[database] +type = "postgres" +user = "nekobox" +password = "" +host = "127.0.0.1" +port = 5432 +name = "nekobox" +schema = "public" +# 仅在已确认需要自动清理重复 email/domain 时开启;默认关闭,避免启动时改写账号数据。 +dedupe_unique_fields = false + +[redis] +addr = "127.0.0.1:6379" +password = "" + +[recaptcha] +captcha_type = recaptcha ; recaptcha(默认), turnstile, math(本地算数验证码,国内访问友好) +domain = "https://www.recaptcha.net" +site_key = "" +server_key = "" +turnstile_style = false + +[upload] +default_avatar = "" +default_background = "" + +# User avatar and background. +aliyun_endpoint = "" +aliyun_access_id = "" +aliyun_access_secret = "" +aliyun_bucket = "" +aliyun_bucket_cdn_host = "" +image_endpoint = "" +image_access_id = "" +image_access_secret ="" +image_bucket = "" +image_bucket_cdn_host = "" + +# User uploaded images. +image_endpoint = "" +image_access_id = "" +image_access_secret = "" +image_bucket = "" +image_bucket_cdn_host = "" + +[mail] +account = "" +password = "" +port = 465 +smtp = "" + +[limits] +; ── 全站内容 / 数量上限(运行时配置,修改后重启生效)────────────────────── +; 缺失本段或个别键时按内置默认值运行。长度统一按字符(rune)计数。 +; 键留空或填 0/负数时自动回退内置默认值,不会导致启动失败,但也不会按你的意图生效, +; 请按需填写正整数。本文件启用 IgnoreInlineComment,行内分号注释会被并入配置值, +; 因此下面注释一律独占整行、勿写行内注释。 + +; ── 标签与屏蔽词(账号设置页)────────────────────────────────────────────── +; 最多标签个数 +max_tags = 5 +; 单个标签最大字符数 +max_tag_len = 5 +; 最多屏蔽词个数 +max_block_words = 10 +; 单个屏蔽词最大字符数 +max_block_word_len = 10 + +; ── 投票 ──────────────────────────────────────────────────────────────────── +; 参与者填写的「自定义补充选项」(其他/自定义)最大字符数 +custom_answer_len = 200 +; 站长创建投票时单个「选项」文本最大字符数(预设选项) +vote_option_len = 300 + +; ── 提问 / 回答 / 追问 ───────────────────────────────────────────────────── +; 提问正文最大字符数(访客提交的问题内容) +question_content_len = 1000 +; 回答正文最大字符数(箱主对提问的回答,及对追问的回复,共用) +answer_content_len = 2000 +; 追问正文最大字符数(提问者被回答后继续追问的内容) +followup_content_len = 2000 + +; ── 私聊 ──────────────────────────────────────────────────────────────────── +; 单条消息(发送/回复)最大字符数 +chat_message_len = 2000 +; 每个私聊会话的消息条数上限(系统硬顶)。用户只能在 1..该值 之间设置自己的 +; 会话条数上限;新注册用户的默认值即该硬顶。 +chat_max_messages = 200 +; 单用户每日私信条数上限(用户可设范围 0..该值;0 表示不限制每日条数) +daily_message_limit_max = 999 diff --git a/.workbuddy/backup/autoban-track-notfound-20260905/autoban.go b/.workbuddy/backup/autoban-track-notfound-20260905/autoban.go new file mode 100644 index 0000000..9f2004b --- /dev/null +++ b/.workbuddy/backup/autoban-track-notfound-20260905/autoban.go @@ -0,0 +1,266 @@ +// Package autoban 提供 fail2ban 风格的自动临时封禁:检测批量扫描/探测特征后 +// 临时封禁来源 IP,并可回调发送告警。纯标准库实现,便于独立测试与复用。 +package autoban + +import ( + "strings" + "sync" + "time" +) + +// 探测事件类型 +const ( + KindNone = 0 // 无探测特征 + KindSensitive = 1 // 命中敏感路径 + KindNotFound = 2 // 404 响应(未命中敏感路径) +) + +// 默认阈值/窗口(Options 未配置或为 0 时兜底)。 +const ( + DefaultWindowSeconds = 60 + DefaultDurationMinutes = 15 + DefaultThresholdSensitive = 10 + DefaultThreshold404 = 20 + pathSampleMax = 6 +) + +// Options 自动封禁配置。 +type Options struct { + Enabled bool + ThresholdSensitive int // 窗口内敏感路径命中次数 + Threshold404 int // 窗口内 404 次数 + Window time.Duration // 统计窗口 + Duration time.Duration // 封禁时长 + AlertEmail string // 告警收件人(留空不发送) + SendAlert func(ip string, paths []string, durationMinutes int) error // 告警发送函数(nil 则不发送) +} + +// sensitivePaths 常见后台/配置/漏洞探测路径特征(忽略大小写,前缀匹配)。 +var sensitivePaths = []string{ + "/admin", "/wp-admin", "/wp-login.php", "/wp-content", "/wordpress", + "/administrator", "/manager", "/cpanel", "/plesk", + "/.env", "/.git", "/.aws", "/.ssh", "/.svn", "/.hg", "/.htaccess", + "/config", "/configuration", "/backup", "/database", "/db", + "/actuator", "/swagger", "/api-docs", "/openapi", "/v2/api-docs", + "/phpmyadmin", "/pma", "/mysql", "/server-status", "/server-info", + "/shell", "/webshell", "/cmd", "/eval", "/hack", "/shell.php", + "/login.php", "/admin.php", "/index.php", "/console", "/vendor", +} + +type probeEvent struct { + at time.Time + kind int +} + +type probeStat struct { + events []probeEvent + sensitive int + notFound int + paths []string // 命中路径样本(告警用) +} + +// Banner 自动封禁器。方法并发安全。 +type Banner struct { + mu sync.Mutex + + probes map[string]*probeStat + banned map[string]time.Time + alerted map[string]bool + + thresholdSensitive int + threshold404 int + window time.Duration + duration time.Duration + alertEmail string + sendAlert func(ip string, paths []string, durationMinutes int) error + enabled bool +} + +// New 按配置创建封禁器并注册全局清理。 +func New(opts Options) *Banner { + window := opts.Window + if window <= 0 { + window = DefaultWindowSeconds * time.Second + } + duration := opts.Duration + if duration <= 0 { + duration = DefaultDurationMinutes * time.Minute + } + sensitive := opts.ThresholdSensitive + if sensitive <= 0 { + sensitive = DefaultThresholdSensitive + } + notFound := opts.Threshold404 + if notFound <= 0 { + notFound = DefaultThreshold404 + } + b := &Banner{ + probes: make(map[string]*probeStat), + banned: make(map[string]time.Time), + alerted: make(map[string]bool), + thresholdSensitive: sensitive, + threshold404: notFound, + window: window, + duration: duration, + alertEmail: strings.TrimSpace(opts.AlertEmail), + sendAlert: opts.SendAlert, + enabled: opts.Enabled, + } + register(b) + return b +} + +// Record 记录一次探测事件,达到阈值时封禁 IP 并触发一次告警。 +func (b *Banner) Record(ip, path string, kind int) { + if !b.enabled || ip == "" || kind == KindNone { + return + } + b.mu.Lock() + defer b.mu.Unlock() + + now := time.Now() + cutoff := now.Add(-b.window) + + st := b.probes[ip] + if st == nil { + st = &probeStat{} + b.probes[ip] = st + } + + // 滚动清理过期事件并重算计数 + valid := st.events[:0] + sensitive, notFound := 0, 0 + for _, e := range st.events { + if e.at.After(cutoff) { + valid = append(valid, e) + if e.kind == KindSensitive { + sensitive++ + } else if e.kind == KindNotFound { + notFound++ + } + } + } + valid = append(valid, probeEvent{at: now, kind: kind}) + if kind == KindSensitive { + sensitive++ + } else { + notFound++ + } + st.events = valid + st.sensitive = sensitive + st.notFound = notFound + if len(st.paths) < pathSampleMax && path != "" { + st.paths = append(st.paths, path) + } + + if sensitive < b.thresholdSensitive && notFound < b.threshold404 { + return + } + + // 已达阈值:封禁(已在封禁中则跳过) + if until, ok := b.banned[ip]; ok && until.After(now) { + return + } + until := now.Add(b.duration) + b.banned[ip] = until + + // 告警(每 IP 仅一次) + if b.alertEmail != "" && b.sendAlert != nil && !b.alerted[ip] { + b.alerted[ip] = true + sample := append([]string{}, st.paths...) + dur := int(b.duration.Minutes()) + go func() { + _ = b.sendAlert(ip, sample, dur) + }() + } +} + +// IsBanned 判断 IP 是否处于封禁期;到期自动解除。 +func (b *Banner) IsBanned(ip string) bool { + if !b.enabled || ip == "" { + return false + } + b.mu.Lock() + defer b.mu.Unlock() + if until, ok := b.banned[ip]; ok { + if until.After(time.Now()) { + return true + } + delete(b.banned, ip) + } + return false +} + +// IsSensitivePath 判断路径是否命中敏感探测特征(忽略大小写,完整段前缀匹配)。 +func IsSensitivePath(p string) bool { + lower := strings.ToLower(p) + for _, s := range sensitivePaths { + if lower == s || strings.HasPrefix(lower, s+"/") { + return true + } + } + return false +} + +// ---------- 全局注册与清理 ---------- + +var ( + bannersMu sync.Mutex + banners = make(map[*Banner]struct{}) + bannersOnce sync.Once +) + +func register(b *Banner) { + bannersMu.Lock() + banners[b] = struct{}{} + bannersMu.Unlock() + + bannersOnce.Do(func() { + go func() { + for { + time.Sleep(2 * time.Minute) + bannersMu.Lock() + for b := range banners { + b.cleanup() + } + bannersMu.Unlock() + } + }() + }) +} + +// cleanup 定期清理过期探测记录与已到期封禁(防止内存膨胀)。 +func (b *Banner) cleanup() { + b.mu.Lock() + defer b.mu.Unlock() + + now := time.Now() + cutoff := now.Add(-b.window) + + for ip, st := range b.probes { + if len(st.events) == 0 || st.events[len(st.events)-1].at.Before(cutoff) { + delete(b.probes, ip) + continue + } + valid := st.events[:0] + sensitive, notFound := 0, 0 + for _, e := range st.events { + if e.at.After(cutoff) { + valid = append(valid, e) + if e.kind == KindSensitive { + sensitive++ + } else if e.kind == KindNotFound { + notFound++ + } + } + } + st.events = valid + st.sensitive = sensitive + st.notFound = notFound + } + for ip, until := range b.banned { + if !until.After(now) { + delete(b.banned, ip) + } + } +} diff --git a/.workbuddy/backup/autoban-track-notfound-20260905/autoban_test.go b/.workbuddy/backup/autoban-track-notfound-20260905/autoban_test.go new file mode 100644 index 0000000..bd2fd2e --- /dev/null +++ b/.workbuddy/backup/autoban-track-notfound-20260905/autoban_test.go @@ -0,0 +1,123 @@ +package autoban + +import ( + "sync/atomic" + "testing" + "time" +) + +func newTestBanner(thresholdSensitive, threshold404 int, window, duration time.Duration, alertEmail string, alertCalls *int64) *Banner { + return New(Options{ + Enabled: true, + ThresholdSensitive: thresholdSensitive, + Threshold404: threshold404, + Window: window, + Duration: duration, + AlertEmail: alertEmail, + SendAlert: func(ip string, paths []string, dur int) error { + if alertCalls != nil { + atomic.AddInt64(alertCalls, 1) + } + return nil + }, + }) +} + +func TestSensitiveThreshold(t *testing.T) { + b := newTestBanner(2, 5, time.Minute, time.Minute, "", nil) + b.Record("1.2.3.4", "/admin", KindSensitive) + if b.IsBanned("1.2.3.4") { + t.Fatal("should not be banned before reaching threshold") + } + b.Record("1.2.3.4", "/.env", KindSensitive) + if !b.IsBanned("1.2.3.4") { + t.Fatal("expected ban after reaching sensitive threshold") + } +} + +func TestNotFoundThreshold(t *testing.T) { + b := newTestBanner(10, 3, time.Minute, time.Minute, "", nil) + for i := 0; i < 3; i++ { + b.Record("5.6.7.8", "/random-"+string(rune('a'+i)), KindNotFound) + } + if !b.IsBanned("5.6.7.8") { + t.Fatal("expected ban after reaching 404 threshold") + } +} + +func TestWindowExpiry(t *testing.T) { + b := newTestBanner(2, 5, 50*time.Millisecond, time.Minute, "", nil) + b.Record("1.1.1.1", "/admin", KindSensitive) + time.Sleep(80 * time.Millisecond) // 事件滑出窗口 + b.Record("1.1.1.1", "/.env", KindSensitive) + if b.IsBanned("1.1.1.1") { + t.Fatal("events outside the window must not accumulate") + } +} + +func TestDurationExpiry(t *testing.T) { + b := newTestBanner(1, 5, time.Minute, 60*time.Millisecond, "", nil) + b.Record("2.2.2.2", "/admin", KindSensitive) + if !b.IsBanned("2.2.2.2") { + t.Fatal("expected ban") + } + time.Sleep(100 * time.Millisecond) // 封禁到期 + if b.IsBanned("2.2.2.2") { + t.Fatal("ban should expire after duration") + } +} + +func TestAlertOncePerIP(t *testing.T) { + var calls int64 + b := newTestBanner(1, 5, time.Minute, time.Minute, "admin@example.com", &calls) + b.Record("9.9.9.9", "/wp-admin", KindSensitive) + b.Record("9.9.9.9", "/.git", KindSensitive) // 已在封禁中,不应重复告警 + b.Record("9.9.9.9", "/config", KindSensitive) + + // sendAlert 异步调用,轮询等待最多 500ms + deadline := time.Now().Add(500 * time.Millisecond) + for atomic.LoadInt64(&calls) == 0 && time.Now().Before(deadline) { + time.Sleep(10 * time.Millisecond) + } + if got := atomic.LoadInt64(&calls); got != 1 { + t.Fatalf("expected exactly 1 alert, got %d", got) + } +} + +func TestDisabled(t *testing.T) { + b := New(Options{Enabled: false}) + b.Record("3.3.3.3", "/admin", KindSensitive) + if b.IsBanned("3.3.3.3") { + t.Fatal("disabled auto-ban must not ban") + } +} + +func TestDefaults(t *testing.T) { + b := New(Options{Enabled: true}) // 全部走默认值 + if b.thresholdSensitive != DefaultThresholdSensitive || b.threshold404 != DefaultThreshold404 { + t.Fatalf("default thresholds wrong: %d/%d", b.thresholdSensitive, b.threshold404) + } + if b.window != DefaultWindowSeconds*time.Second || b.duration != DefaultDurationMinutes*time.Minute { + t.Fatalf("default window/duration wrong: %v/%v", b.window, b.duration) + } +} + +func TestIsSensitivePath(t *testing.T) { + cases := []struct { + path string + want bool + }{ + {"/admin", true}, + {"/admin/panel", true}, + {"/WP-ADMIN", true}, + {"/.env", true}, + {"/user/questions", false}, + {"/vote/abc123", false}, + {"/administratorx", false}, // 前缀必须完整段匹配,避免误伤 + } + for _, c := range cases { + if got := IsSensitivePath(c.path); got != c.want { + t.Errorf("IsSensitivePath(%q) = %v, want %v", c.path, got, c.want) + } + } +} diff --git a/.workbuddy/backup/autoban-track-notfound-20260905/route.go b/.workbuddy/backup/autoban-track-notfound-20260905/route.go new file mode 100644 index 0000000..dc32c09 --- /dev/null +++ b/.workbuddy/backup/autoban-track-notfound-20260905/route.go @@ -0,0 +1,409 @@ +// Copyright 2022 E99p1ant. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package route + +import ( + "encoding/gob" + "io" + "net/http" + "strings" + "time" + + "github.com/flamego/cache" + cacheRedis "github.com/flamego/cache/redis" + "github.com/flamego/csrf" + "github.com/flamego/flamego" + "github.com/flamego/recaptcha" + "github.com/flamego/session" + "github.com/flamego/session/mysql" + sessionRedis "github.com/flamego/session/redis" + "github.com/flamego/template" + "github.com/sirupsen/logrus" + + "github.com/tamakyi/TamaBox/internal/captcha" + "github.com/tamakyi/TamaBox/internal/conf" + "github.com/tamakyi/TamaBox/internal/context" + "github.com/tamakyi/TamaBox/internal/form" + "github.com/tamakyi/TamaBox/internal/i18n" + "github.com/tamakyi/TamaBox/internal/mail" + "github.com/tamakyi/TamaBox/internal/route/chat" // 私聊路由 + "github.com/tamakyi/TamaBox/internal/security/autoban" + "github.com/tamakyi/TamaBox/internal/shared" + templatepkg "github.com/tamakyi/TamaBox/internal/template" + "github.com/tamakyi/TamaBox/route" + "github.com/tamakyi/TamaBox/route/auth" + "github.com/tamakyi/TamaBox/route/pixel" + "github.com/tamakyi/TamaBox/route/question" + "github.com/tamakyi/TamaBox/route/service" + "github.com/tamakyi/TamaBox/route/user" + "github.com/tamakyi/TamaBox/route/vote" + "github.com/tamakyi/TamaBox/static" + "github.com/tamakyi/TamaBox/templates" +) + +// CSRFUserSessionKey 是 CSRF token 绑定的会话键,必须与登录时写入的会话键一致 +// (见 route/auth/login.go 中 ctx.Session.Set("uid", ...))。flamego/csrf 默认使用 +// "userID",若不与之对齐,token 的 id 恒为 "0",任意匿名访客从公开页面取到的 token +// 对全部已登录用户均有效,CSRF 退化为全站公开常量。 +// +// 本常量同时被 csrf_binding_test.go 引用,作为单一事实来源:一旦被误改,回归测试会在 +// CI 失败,避免修复悄悄倒退。 +const CSRFUserSessionKey = "uid" + +// securityHeaders adds security-related HTTP headers to every response. +func securityHeaders() flamego.Handler { + return func(ctx flamego.Context) { + w := ctx.ResponseWriter() + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("X-XSS-Protection", "1; mode=block") + w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + // HSTS:仅当站点以 https 对外提供服务时下发,避免 http 部署下把用户锁死在 https。 + if strings.HasPrefix(conf.App.ExternalURL, "https") { + w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") + } + } +} + +func New() *flamego.Flame { + f := flamego.Classic() + if conf.App.Production { + flamego.SetEnv(flamego.EnvTypeProd) + } + + // 自动封禁实例:整个请求链路共享同一个,供中间件与 f.NotFound 复用, + // 避免对业务性 302→/(语言切换、登出、投票不存在等)误判为路径扫描。 + ab := autoban.New(autoban.Options{ + Enabled: conf.Security.EnableAutoBan, + ThresholdSensitive: conf.Security.BanThresholdSensitive, + Threshold404: conf.Security.BanThreshold404, + Window: time.Duration(conf.Security.BanWindowSeconds) * time.Second, + Duration: time.Duration(conf.Security.BanDurationMinutes) * time.Minute, + AlertEmail: conf.Security.SecurityAlertEmail, + SendAlert: mail.SendSecurityAlertMail, + }) + // 需要输出详细日志时配置如下 + // if conf.App.Production { + // flamego.SetEnv(flamego.EnvTypeProd) + // } + + templateFS, err := template.EmbedFS(templates.FS, ".", []string{".html"}) + if err != nil { + logrus.WithError(err).Fatal("Failed to embed templates file system") + } + + gob.Register(time.Time{}) + gob.Register(context.Flash{}) + gob.Register(captcha.StoredAnswer{}) + var sessionStorage interface{} + initer := session.MemoryIniter() + if conf.Database.DSN != "" { + initer = mysql.Initer() + sessionStorage = mysql.Config{ + DSN: conf.Database.DSN, + Lifetime: 7 * 24 * time.Hour, + } + } + if conf.Redis.Addr != "" { + initer = sessionRedis.Initer() + sessionStorage = sessionRedis.Config{ + Options: &sessionRedis.Options{ + Addr: conf.Redis.Addr, + Password: conf.Redis.Password, + DB: 1, + }, + Lifetime: 7 * 24 * time.Hour, + } + } + sessioner := session.Sessioner(session.Options{ + Initer: initer, + Config: sessionStorage, + // 显式声明 cookie 安全属性,不依赖库默认值: + // - HTTPOnly:禁止 JS 读取,降低 XSS 窃取会话的风险 + // - SameSite=Lax:阻断经典跨站表单 POST(CSRF 的主要防线之一) + // - Secure:仅在站点以 https 对外服务时下发,避免 http 本地开发下 cookie 不生效。 + // 缺失 Secure 时,HSTS 尚未落地的首次访问可被主动 MITM 诱导明文请求窃取会话。 + Cookie: session.CookieOptions{ + HTTPOnly: true, + SameSite: http.SameSiteLaxMode, + Secure: strings.HasPrefix(conf.App.ExternalURL, "https"), + }, + }) + + f.Use(flamego.Static(flamego.StaticOptions{ + FileSystem: http.FS(static.FS), + Prefix: "/static", + // 静态资源随二进制发布,内容变更需重新部署;1 天缓存是安全折中 + CacheControl: func() string { + return "public, max-age=86400" + }, + })) + + f.Use(securityHeaders()) + f.Use(ipBlacklistMiddleware()) + f.Use(autobanMiddleware(ab)) + // 全局请求体上限:必须在任何会读取 body 的中间件(尤其 Contexter 内的 CSRF 校验) + // 之前执行,否则超大 multipart 会在 Bind 的路由级限制生效前落盘临时文件耗尽磁盘。 + f.Use(requestBodyLimitMiddleware()) + + reqUserSignOut := context.Toggle(&context.ToggleOptions{UserSignOutRequired: true}) + reqUserSignIn := context.Toggle(&context.ToggleOptions{UserSignInRequired: true}) + + f.Group("", func() { + f.Get("/", route.Home) + f.Get("/pixel", reqUserSignIn, pixel.Index) + f.Get("/sponsor", route.Sponsor) + f.Get("/change-logs", route.ChangeLogs) + f.Get("/about-me", route.AboutMe) + f.Get("/robots.txt", func(c context.Context) { + fs, err := static.FS.Open("robots.txt") + if err != nil { + c.ResponseWriter().WriteHeader(404) + return + } + defer func() { _ = fs.Close() }() + c.ResponseWriter().Header().Set("Content-Type", "text/plain; charset=utf-8") + c.ResponseWriter().Header().Set("Cache-Control", "public, max-age=86400") + _, _ = io.Copy(c.ResponseWriter(), fs) + }) + f.Get("/favicon.ico", func(c context.Context) { + fs, _ := static.FS.Open("favicon.ico") + defer func() { _ = fs.Close() }() + c.ResponseWriter().Header().Set("Content-Type", "image/x-icon") + c.ResponseWriter().Header().Set("Cache-Control", "public, max-age=86400") + _, _ = io.Copy(c.ResponseWriter(), fs) + }) + + f.Get("/sitemap.xml", func(c context.Context) { + fs, err := static.FS.Open("sitemap.xml") + if err != nil { + c.ResponseWriter().WriteHeader(404) + return + } + defer func() { _ = fs.Close() }() + c.ResponseWriter().Header().Set("Content-Type", "application/xml") + c.ResponseWriter().Header().Set("Content-Type", "text/xml; charset=utf-8") + c.ResponseWriter().Header().Set("Cache-Control", "public, max-age=86400") + _, _ = io.Copy(c.ResponseWriter(), fs) + }) + + f.Get("/llms.txt", func(c context.Context) { + fs, err := static.FS.Open("llms.txt") + if err != nil { + c.ResponseWriter().WriteHeader(404) + return + } + defer func() { _ = fs.Close() }() + c.ResponseWriter().Header().Set("Content-Type", "text/plain; charset=utf-8") + c.ResponseWriter().Header().Set("Cache-Control", "public, max-age=86400") + _, _ = io.Copy(c.ResponseWriter(), fs) + }) + + f.Get("/llms-full.txt", func(c context.Context) { + fs, err := static.FS.Open("llms-full.txt") + if err != nil { + c.ResponseWriter().WriteHeader(404) + return + } + defer func() { _ = fs.Close() }() + c.ResponseWriter().Header().Set("Content-Type", "text/plain; charset=utf-8") + c.ResponseWriter().Header().Set("Cache-Control", "public, max-age=86400") + _, _ = io.Copy(c.ResponseWriter(), fs) + }) + + f.Group("", func() { + f.Combo("/register").Get(auth.Register).Post(form.Bind(form.Register{}), rateLimitMiddleware(10, 1*time.Minute), auth.RegisterAction) + f.Combo("/login").Get(auth.Login).Post(form.Bind(form.Login{}), rateLimitMiddleware(20, 1*time.Minute), auth.LoginAction) + f.Combo("/forgot-password").Get(auth.ForgotPassword).Post(form.Bind(form.ForgotPassword{}), rateLimitMiddleware(5, 5*time.Minute), auth.ForgotPasswordAction) + f.Combo("/recover-password").Get(auth.RecoverPassword).Post(form.Bind(form.RecoverPassword{}), auth.RecoverPasswordAction) + }, reqUserSignOut) + + // 页面密码验证路由(独立于密码保护中间件);限流防爆破 + f.Combo("/_/{domain}/password").Get(question.PasswordPage).Post(rateLimitMiddleware(10, 1*time.Minute), question.VerifyPagePassword) + + // 独立的分享图路由(不经过 Pager 中间件,直接返回图片) + // 注意:仍挂载 question.Pager,页面设置了密码时必须先通过密码验证, + // 否则分享图会绕过页面密码保护(与单题分享图行为一致)。 + f.Group("/_/{domain}", func() { + f.Get("/share-card", question.ShareCard) + }, question.Pager) + + f.Group("/_/{domain}", func() { + f.Combo("").Get(question.List).Post(form.Bind(form.NewQuestion{}), rateLimitMiddleware(30, 1*time.Minute), question.New) + f.Group("/{questionID}", func() { + f.Get("", question.Item) + f.Get("/share-card", question.QuestionShareCard) + f.Post("/delete", question.Delete) + f.Post("/set-private", question.SetPrivate) + f.Post("/set-public", question.SetPublic) + f.Post("/pin", question.TogglePin) + f.Post("/read", question.SetRead) + f.Post("/unread", question.SetUnread) + f.Post("/answer", reqUserSignIn, form.Bind(form.PublishAnswerQuestion{}), question.PublishAnswer) + f.Post("/follow-up", form.Bind(form.PublishFollowUpQuestion{}), question.PublishFollowUp) + f.Post("/follow-up/{followUpID}/answer", reqUserSignIn, form.Bind(form.AnswerFollowUpQuestion{}), question.AnswerFollowUp) + }, question.Questioner) + + // 新增:私聊路由(需要登录,使用 Token 标识对话) + f.Group("/chat/{token}", func() { + f.Get("", chat.ConversationPage) + f.Post("/send", rateLimitMiddleware(20, 1*time.Minute), chat.SendMessage) + f.Post("/delete", chat.DeleteConversation) + f.Post("/block", chat.ToggleBlockUser) + }, reqUserSignIn) + }, question.Pager) // Pager 中间件已内置页面密码检查 + + f.Get("/explore", user.Explore) + + // 匿名投票参与端(公开):token 链接入口 + f.Group("/vote/{token}", func() { + f.Get("", vote.Page) + // 提交限流优先按登录用户/签名 vote_fp 计数,IP 仅作高阈值兜底,避免共享出口误伤。 + f.Post("/submit", voteSubmitRateLimitMiddleware(20, 1*time.Minute), vote.Submit) + }) + + // 公开页语言切换:把语言偏好写入 session(由 session 中间件随其 cookie 一并持久化) + // 后跳回原页面。此前写独立 lang cookie 会被 session 中间件的 Set-Cookie 整体覆盖而失效, + // 故改为托管在 session 中。仅允许站内相对路径,防开放重定向。 + f.Get("/i18n/{lang}", func(c context.Context) { + lang := i18n.Resolve(c.Param("lang")) + // 写入 session,避免被 session 中间件的 Set-Cookie 覆盖(匿名用户也走 session 中间件)。 + c.Session.Set("lang", lang) + to := c.Query("to") + // 仅允许站内相对路径,防开放重定向: + // 拒绝空、非 "/" 开头、以 "//" 开头的协议相对路径,以及含反斜杠/控制字符的变体。 + if to == "" || !strings.HasPrefix(to, "/") || + strings.HasPrefix(to, "//") || strings.ContainsAny(to, "\\\x00\r\n\t") { + to = "/" + } + c.Redirect(to) + }) + + // 算数验证码 API(仅在 captcha_type = "math" 时生效,但路由本身不依赖中间件) + // 加限流:防止匿名脚本无限生成/轮询爆破(答案本身对客户端可见,限流是主要节流手段)。 + f.Get("/api/captcha/generate", rateLimitMiddleware(20, 1*time.Minute), captcha.GenerateHandler) + f.Post("/api/captcha/verify", rateLimitMiddleware(20, 1*time.Minute), captcha.VerifyHandler) + + f.Group("/user", func() { + f.Get("/questions", user.QuestionList) + + // 匿名投票管理(箱主侧) + f.Group("/votes", func() { + f.Get("", user.VoteList) + f.Get("/new", user.VoteNew) + f.Post("", rateLimitMiddleware(10, 1*time.Minute), user.VoteCreate) + f.Get("/{voteID}/edit", user.VoteEdit) + f.Post("/{voteID}", rateLimitMiddleware(10, 1*time.Minute), user.VoteUpdate) + f.Post("/{voteID}/publish", user.VotePublish) + f.Post("/{voteID}/close", user.VoteClose) + f.Post("/{voteID}/delete", user.VoteDelete) + f.Get("/{voteID}/results", user.VoteResults) + f.Get("/{voteID}/preview", user.VotePreview) + // 导出为只读操作(幂等、无副作用),用 GET 链接下载,避免表单提交 + f.Get("/{voteID}/export", user.VoteExportCSV) + }) + + f.Group("/profile", func() { + f.Get("", user.Profile) + f.Post("/update", form.Bind(form.UpdateProfile{}), rateLimitMiddleware(10, 1*time.Minute), user.UpdateProfile) + f.Post("/export", user.ExportProfile) + f.Post("/export-json", user.ExportProfileJSON) + f.Combo("/deactivate").Get(user.DeactivateProfile).Post(user.DeactivateProfileAction) + + f.Post("/update-page-password", form.Bind(form.UpdatePagePassword{}), rateLimitMiddleware(10, 1*time.Minute), user.UpdatePagePassword) + f.Post("/update-ask-password", form.Bind(form.UpdateAskPassword{}), rateLimitMiddleware(10, 1*time.Minute), user.UpdateAskPassword) + + f.Post("/update-chat-settings", user.UpdateChatSettings) + + // 功能管理(提问 / 私聊 / 投票 的开关、定时开关与定量收集): + // 不单独开页面,作为账号设置页的三个面板,保存后回跳到对应面板。 + f.Group("/features", func() { + f.Post("/update", rateLimitMiddleware(20, 1*time.Minute), user.FeaturesUpdate) + f.Post("/reset-quota", rateLimitMiddleware(20, 1*time.Minute), user.FeatureResetQuota) + }) + }) + // 防骚扰设置:注册限制(harassment/update)与全局屏蔽词(blockwords/update) + // 拆为两个独立入口。屏蔽词在提问/私聊/投票三个设置面板共用一份,保存后经 + // ?tab= 参数回跳到来源面板。 + f.Post("/harassment/update", form.Bind(form.UpdateHarassment{}), user.UpdateHarassment) + f.Post("/blockwords/update", form.Bind(form.UpdateBlockWords{}), user.UpdateBlockWords) + f.Post("/tags/update", form.Bind(form.UpdateTags{}), user.UpdateTags) + // Logout 必须用 POST:GET 会留下 CSRF 风险(第三方图片/链接可触发退出登录) + f.Post("/logout", auth.Logout) + }, reqUserSignIn) + + f.Group("/api/v1", func() { + f.Group("/user", func() { + f.Get("", reqUserSignIn, user.ProfileAPI) + f.Group("/{domain}", func() { + f.Group("/questions", func() { + f.Get("", rateLimitMiddleware(60, 1*time.Minute), question.ListAPI) + }) + }) + }) + // 像素追踪代理:仅暴露只读 GET(f.Any 会放行写方法,曾有 CSRF 豁免 + 写方法放行的风险), + // 并加限流防止被用作高频打点通道。 + f.Get("/pixel/{**}", reqUserSignIn, rateLimitMiddleware(30, 1*time.Minute), pixel.Proxy) + // 服务反代仅暴露只读 GET:本网关是把已登录用户的请求转发到配置好的内部服务, + // 不应允许任意写方法,避免被用作对内网后端的越权操作入口。 + f.Get("/service/{**}", reqUserSignIn, service.Proxy) + f.Get("/explore/random", rateLimitMiddleware(60, 1*time.Minute), user.RandomRecommend) + // 首页"提问箱信息统计"公开接口(服务端 30 分钟缓存),供前端定时刷新 + f.Get("/site-stats", rateLimitMiddleware(60, 1*time.Minute), route.SiteStatsAPI) + }, context.APIEndpoint) + }, + cache.Cacher(cache.Options{ + Initer: cacheRedis.Initer(), + Config: cacheRedis.Config{ + Options: &cacheRedis.Options{ + Addr: conf.Redis.Addr, + Password: conf.Redis.Password, + DB: 0, + }, + }, + }), + recaptcha.V3( + recaptcha.Options{ + Secret: conf.Recaptcha.ServerKey, + VerifyURL: func() recaptcha.VerifyURL { + if conf.Recaptcha.TurnstileStyle { + return "https://challenges.cloudflare.com/turnstile/v0/siteverify" + } + return recaptcha.VerifyURLGlobal + }(), + }, + ), + sessioner, + csrf.Csrfer(csrf.Options{ + Secret: conf.Server.XSRFKey, + Header: "X-CSRF-Token", + // ⚠️ 必须与 route/auth/login.go 中 ctx.Session.Set("uid", ...) 的 key 一致。 + // flamego/csrf 默认 SessionKey 为 "userID",若不显式指定,s.Get("userID") + // 恒为 nil,token 内绑定的 id 恒为 "0" —— 由于校验是无状态 HMAC 重算 + // (不与 session 中存储的 token 比对),这会导致任意匿名访客从公开的 + // /login 页面取到的 token 对所有已登录用户均有效,CSRF 退化为全站公开常量。 + // 回归测试见 route/csrf_binding_test.go(引用 CSRFUserSessionKey)。 + SessionKey: CSRFUserSessionKey, + }), + template.Templater(template.Options{ + FileSystem: templateFS, + FuncMaps: templatepkg.FuncMap(), + }), + context.Contexter(), + ) + f.NotFound(func(ctx flamego.Context) { + // 真正的 404 探测在此计数(仅此处,不把业务 302→/ 误判为扫描)。 + // 注意:flamego 的 NotFound 会先写入 404 状态再 Redirect 到 "/", + // 因此这里在 Redirect 之前记录,确保状态为 404。 + // 键必须与 autobanMiddleware 中的 IsBanned/Record 保持一致(同样经 LimitKey 聚合), + // 否则 404 计数会记在另一个键上,导致封禁永不生效。 + if ip := shared.LimitKey(shared.ResolveClientIP(ctx.Request().Request)); ip != "" { + ab.Record(ip, ctx.Request().URL.Path, autoban.KindNotFound) + } + ctx.Redirect("/") + }) + + return f +} diff --git a/.workbuddy/backup/autoban-track-notfound-20260905/static.go b/.workbuddy/backup/autoban-track-notfound-20260905/static.go new file mode 100644 index 0000000..e32d120 --- /dev/null +++ b/.workbuddy/backup/autoban-track-notfound-20260905/static.go @@ -0,0 +1,128 @@ +// Copyright 2022 E99p1ant. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package conf + +// Build time and commit information. +// +// ⚠️ WARNING: should only be set by "-ldflags". +var ( + BuildTime string + BuildCommit = "dev" +) + +var ( + App struct { + Production bool `ini:"production"` + ExternalURL string `ini:"external_url"` + ICP string `ini:"icp"` + DefaultLang string `ini:"default_lang"` // 站点默认语言(zh-CN/zh-TW/en/ja),留空回退简体中文 + UptraceDSN string `ini:"uptrace_dsn"` + QiniuAccessKey string `ini:"qiniu_access_key"` + QiniuAccessSecret string `ini:"qiniu_access_secret"` + AliyunAccessKey string `ini:"aliyun_access_key"` + AliyunAccessKeySecret string `ini:"aliyun_access_key_secret"` + SentryDSN string `ini:"sentry_dsn"` + MaintenanceMode bool `ini:"maintenance_mode"` + ShareBackground string `ini:"share_background"` + FontPath string `ini:"font_path"` + } + + Security struct { + EnableTextCensor bool `ini:"enable_text_censor"` + IPBlacklist string `ini:"ip_blacklist"` + IPBlacklistRedirectURL string `ini:"ip_blacklist_redirect_url"` + // TrustProxy 为 true 时信任 CDN 权威客户端头 CF-Connecting-IP / EO-Client-IP + // (以及受信代理设置的 X-Real-IP / X-Forwarded-For);否则一律使用 RemoteAddr, + // 防止客户端伪造 header 绕过速率限制与 IP 黑名单。 + TrustProxy bool `ini:"trust_proxy"` + // TrustedProxies 逗号分隔的 IP / CIDR 列表,仅限“你自己的”反向代理/负载均衡基础设施 + // (例如本机 nginx 的 127.0.0.1、私有网络 LB)。仅当 TCP 对端落在该清单内时才会解析 + // X-Forwarded-For / X-Real-IP。留空则这两个头永不被信任(CDN 权威头 CF-Connecting-IP / + // EO-Client-IP 仍会被采用,前提是源站已被对应 CDN 锁定)。 + // 目的:避免维护 Cloudflare / EdgeOne 等上游 CDN 的庞大 CIDR 清单——源站应配合 + // Cloudflare Tunnel / Authenticated Origin Pull / 防火墙仅放行 CDN 回源网段锁定, + // 使 CF-Connecting-IP / EO-Client-IP 成为权威客户端 IP。 + TrustedProxies string `ini:"trusted_proxies"` + // 自动封禁(fail2ban 风格):检测批量扫描/探测特征后临时封禁来源 IP。 + EnableAutoBan bool `ini:"enable_auto_ban"` + BanThresholdSensitive int `ini:"ban_threshold_sensitive"` // 窗口内敏感路径命中次数 + BanThreshold404 int `ini:"ban_threshold_404"` // 窗口内 404 次数 + BanWindowSeconds int `ini:"ban_window_seconds"` // 统计窗口(秒) + BanDurationMinutes int `ini:"ban_duration_minutes"` // 封禁时长(分钟) + SecurityAlertEmail string `ini:"security_alert_email"` // 触发封禁时告警收件人(留空不发送) + } + + Server struct { + Port int `ini:"port"` + Listen string `ini:"listen"` // 绑定监听地址(含端口)。留空则默认 ":port"(监听所有网卡)。 + Salt string `ini:"salt"` + XSRFKey string `ini:"xsrf_key"` + } + + Database struct { + DSN string + + Type string `ini:"type"` + User string `ini:"user"` + Password string `ini:"password"` + Host string `ini:"host"` + Port uint `ini:"port"` + Name string `ini:"name"` + Schema string `ini:"schema"` + DedupeUniqueFields bool `ini:"dedupe_unique_fields"` + } + + Redis struct { + Addr string `ini:"addr"` + Password string `ini:"password"` + } + + Recaptcha struct { + CaptchaType string `ini:"captcha_type"` + Domain string `ini:"domain"` + SiteKey string `ini:"site_key"` + ServerKey string `ini:"server_key"` + TurnstileStyle bool `ini:"turnstile_style"` + } + + Pixel struct { + Host string `ini:"host"` + } + + Upload struct { + DefaultAvatarURL string `ini:"default_avatar"` + DefaultBackground string `ini:"default_background"` + DefaultBackgroundImageURL string `ini:"default_qrbackgroundimage"` + AliyunEndpoint string `ini:"aliyun_endpoint"` + AliyunAccessID string `ini:"aliyun_access_id"` + AliyunAccessSecret string `ini:"aliyun_access_secret"` + AliyunBucket string `ini:"aliyun_bucket"` + AliyunBucketCDNHost string `ini:"aliyun_bucket_cdn_host"` + ImageEndpoint string `ini:"image_endpoint"` + ImageAccessID string `ini:"image_access_id"` + ImageAccessSecret string `ini:"image_access_secret"` + ImageBucket string `ini:"image_bucket"` + ImageBucketCDNHost string `ini:"image_bucket_cdn_host"` + } + + Mail struct { + Account string `ini:"account"` + Password string `ini:"password"` + Port int `ini:"port"` + SMTP string `ini:"smtp"` + // SkipTLSVerify 跳过 SMTP TLS 证书校验(仅自签名/内网 SMTP 需要,默认 false 校验证书) + SkipTLSVerify bool `ini:"skip_tls_verify"` + } + + Service struct { + Backends []struct { + Prefix string `ini:"prefix"` + ForwardURL string `ini:"forward_url"` + } + // TLSInsecureSkipVerify 跳过反向代理到后端服务的 TLS 证书校验 + // (仅内网后端使用自签名证书时需要,默认 false 校验证书) + TLSInsecureSkipVerify bool `ini:"tls_insecure_skip_verify"` + } +) diff --git a/.workbuddy/backup/cdn-migration-20260905/header.html b/.workbuddy/backup/cdn-migration-20260905/header.html new file mode 100644 index 0000000..8a6eb93 --- /dev/null +++ b/.workbuddy/backup/cdn-migration-20260905/header.html @@ -0,0 +1,977 @@ + + + + + + {{ .Title }} + + + + + + + + + + + + + + + + + {{ if .VConsole }} + + + {{ end }} + + + + + + {{ if eq .CaptchaType "math" }} + + + {{ else if .RecaptchaTurnstileStyle }} + + + {{ else }} + + + {{ end }} + {{ if ne SentryDSN "" }} + + + {{ end }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ if .NoIndex }}{{ end }} + {{ if .IsUserPage }} + + + + + + + + + {{ else }} + + + + + + + {{ end }} + + + + + + + + + + + + +
+ {{ if not .IsUserPage }} + + {{ end }} + +
+ + + +{{ if .IsPixel }} +
+{{ else }} +
+{{ end }} + +{{ if eq .CaptchaType "math" }} +
+
+

{{t .LangCode "captcha.title"}}

+

{{t .LangCode "captcha.hint"}}

+
+ {{t .LangCode "captcha.loading"}} + +
+
+ +
+ +
+ + +
+
+
+ + + + +{{ end }} \ No newline at end of file diff --git a/.workbuddy/backup/cdn-migration-20260905/list.html b/.workbuddy/backup/cdn-migration-20260905/list.html new file mode 100644 index 0000000..4215219 --- /dev/null +++ b/.workbuddy/backup/cdn-migration-20260905/list.html @@ -0,0 +1,309 @@ +{{template "base/header" .}} + + + + + +
+
+
+ +

{{ .PageUser.Name }}

+

{{ .PageUser.Intro }}

+
+ {{ if and .PageUser.Acfunlink (ne .PageUser.Acfunlink "none") }} + + acfunlink + + {{ end }} + {{ if and .PageUser.Bililink (ne .PageUser.Bililink "none") }} + + bililink + + {{ end }} + {{ if and .PageUser.Weibolink (ne .PageUser.Weibolink "none") }} + + weibolink + + {{ end }} + {{ if and .PageUser.Xiaohongshulink (ne .PageUser.Xiaohongshulink "none") }} + + xiaohongshulink + + {{ end }} + {{ if and .PageUser.Bloglink (ne .PageUser.Bloglink "none") }} + + bloglink + + {{ end }} +
+
+
+
+ +
+
+ {{ if .CanAsk }} + + + +
+ + +

{{t .LangCode "ask.scan" .PageUser.Name}}
{{ if .RegisterOnlyAsk }}{{t .LangCode "ask.registerOnly"}}{{ else }}{{t .LangCode "ask.anonymous"}}{{ end }}

+ + + + + + + {{ end }} + {{template "base/alert" .}} + + +
+ + {{ if .VoteEnabled }} + + {{ else }} + + + {{ end }} +
+ +
+ {{template "question/new-question-template" .}} + + {{ if ne (len .PageQuestions) 0 }} +

{{t .LangCode "ask.history" .PageUser.Name}} ({{ .AnsweredCount }})

+ {{ template "question/history-template" . }} + {{ end }} +
+ + +
+
+ +{{template "base/footer" .}} \ No newline at end of file diff --git a/.workbuddy/backup/cdn-migration-20260905/profile.html b/.workbuddy/backup/cdn-migration-20260905/profile.html new file mode 100644 index 0000000..13ef910 --- /dev/null +++ b/.workbuddy/backup/cdn-migration-20260905/profile.html @@ -0,0 +1,1527 @@ +{{template "base/header" .}} + + + + + + + + + + + + + + + + +{{template "base/alert" .}} + + +
+
+ +
+

{{t .LangCode "profile.cropTitle"}}

+
+
+
+ +
+

+
+ +
+
+ + +
+ + + + +
+ + +
+
+ {{ .CSRFTokenHTML }} + + +
+

{{t .LangCode "profile.card.basic"}}

+
+ + +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + + {{t .LangCode "admin.langHint"}} +
+
+ + +
+
+ +
+
+ + +
+

{{t .LangCode "profile.card.avatarBg"}}

+
+
+ + + + 1:1 +
+ + +
+ {{t .LangCode "profile.avatarCropHint"}} +
+
+ + + + 16:9 +
+ + +
+ {{t .LangCode "profile.backgroundCropHint"}} +
+
+ +
+ +
+
+ + +
+
    +
  • + {{t .LangCode "profile.card.qr"}} +
    +
    +
    +
    + +
    + +
    +
    + +
    + +
    +
    + +
    + +
    +
    + + {{t .LangCode "admin.customFgHint"}} +
    +
    + +
    + + {{.LoggedUser.Dotscale}} + +
    +
    +
    + +
    + + {{.LoggedUser.Backgroundimagealpha}} + +
    +
    +
    + +
    + + +
    + {{t .LangCode "admin.qrPreviewHint"}} +
    +
    +
    +
    +

    {{t .LangCode "admin.preview"}}

    +
    +
    +
    + {{t .LangCode "admin.themePreset"}} +
    + + + + +
    +
    +
    + +
    +

    {{t .LangCode "admin.previewHint"}}

    +
    +
    +
    +
    +
  • +
+
+ +
+
+ + +
+
    +
  • + {{t .LangCode "admin.social"}} +
    + {{t .LangCode "admin.socialHint"}} + + + + + +
    +
  • +
+
+ +
+
+ + +
+

{{t .LangCode "profile.card.password"}}

+
+ + +
+
+ + +
+

{{t .LangCode "profile.pwdNote"}}

+
+
+ +
+
+
+ + +
+

{{t .LangCode "admin.tags"}}

+
+ {{ .CSRFTokenHTML }} +
+ + +
+
+ +
+
+ +
+
+
+ + +
+

{{t .LangCode "admin.pagePassword"}}

+
+ {{ .CSRFTokenHTML }} +
+ + +
+
+ +
+
+
+ + +
+

{{t .LangCode "admin.account"}}

+ +
+
+ + + +
+ {{ with .FeatureQuestion }}{{template "user/feature-control-form" .}}{{ end }} + +
+
+
+ {{ $.CSRFTokenHTML }} + {{t $.LangCode "admin.harassment"}} +
+
+ +
+
+ +
+
+
+ + + {{ template "user/global-blockwords" (dict "Root" $ "Action" "/user/blockwords/update?tab=question") }} + +
+
+
+ {{ $.CSRFTokenHTML }} + {{t $.LangCode "admin.askPassword"}} +
+ + +
+
+ +
+
+
+
+ + +
+ +
+ {{ with .FeatureChat }}{{template "user/feature-control-form" .}}{{ end }} + +
+
+ {{ $.CSRFTokenHTML }} + {{t $.LangCode "admin.chat"}} +
+ + {{ with $.ChatLimits }} + + {{ end }} +
+
+ + {{ with $.ChatLimits }} + + {{ end }} + {{t $.LangCode "admin.dailyLimitHint"}} +
+
+ +
+
+
+ +
+ + {{ template "user/global-blockwords" (dict "Root" $ "Action" "/user/blockwords/update?tab=chat") }} +
+ + +
+ {{ with .FeatureVote }}{{template "user/feature-control-form" .}}{{ end }} + +
+ + {{ template "user/global-blockwords" (dict "Root" $ "Action" "/user/blockwords/update?tab=vote") }} +
+ + + + + + + + + +{{template "base/footer" .}} diff --git a/.workbuddy/backup/cdn-migration-20260905/question-not-found.html b/.workbuddy/backup/cdn-migration-20260905/question-not-found.html new file mode 100644 index 0000000..a81806f --- /dev/null +++ b/.workbuddy/backup/cdn-migration-20260905/question-not-found.html @@ -0,0 +1,105 @@ +{{template "base/header" .}} + + + + + +
+
+
+ +

{{ .PageUser.Name }}

+

{{ .PageUser.Intro }}

+
+ {{ if and .PageUser.Acfunlink (ne .PageUser.Acfunlink "none") }} + + acfunlink + + {{ end }} + {{ if and .PageUser.Bililink (ne .PageUser.Bililink "none") }} + + bililink + + {{ end }} + {{ if and .PageUser.Weibolink (ne .PageUser.Weibolink "none") }} + + weibolink + + {{ end }} + {{ if and .PageUser.Xiaohongshulink (ne .PageUser.Xiaohongshulink "none") }} + + xiaohongshulink + + {{ end }} + {{ if and .PageUser.Bloglink (ne .PageUser.Bloglink "none") }} + + bloglink + + {{ end }} +
+
+
+
+ +
+
+ {{ if .CanAsk }} + + + +
+ + +

扫码向@{{ .PageUser.Name }}提问
{{ if .RegisterOnlyAsk }}{{t .LangCode "ask.registerOnly"}}{{ else }}{{t .LangCode "ask.anonymous"}}{{ end }}

+ {{ end }} + {{template "base/alert" .}} + {{template "question/new-question-template" .}} + +
+ {{ if ne (len .PageQuestions) 0 }} +

@{{ .PageUser.Name }} 以前回答过的问题 ({{ .AnsweredCount }})

+ {{ template "question/history-template" . }} + {{ end }} +
+
+{{template "base/footer" .}} \ No newline at end of file diff --git a/.workbuddy/backup/domain-shiroko-20260906/footer.html b/.workbuddy/backup/domain-shiroko-20260906/footer.html new file mode 100644 index 0000000..e6e0b60 --- /dev/null +++ b/.workbuddy/backup/domain-shiroko-20260906/footer.html @@ -0,0 +1,102 @@ +

+
Powered by NekoBox | {{ ICP }}
{{t .LangCode "site.footer.runtime"}}: +
+ + {{t .LangCode "site.footer.admin"}} + | {{t .LangCode "site.footer.notice"}} + | {{t .LangCode "site.footer.chat"}} + | {{t .LangCode "site.footer.drive"}} + | {{t .LangCode "site.footer.status"}} + | {{t .LangCode "site.footer.nav"}} +

+ + +
+
+ + + + diff --git a/.workbuddy/backup/domain-shiroko-20260906/header.html b/.workbuddy/backup/domain-shiroko-20260906/header.html new file mode 100644 index 0000000..eb6c166 --- /dev/null +++ b/.workbuddy/backup/domain-shiroko-20260906/header.html @@ -0,0 +1,1029 @@ + + + + + + {{ .Title }} + + + + + + + + + + + + + + + + + + {{ if .VConsole }} + + + {{ end }} + + + {{ if eq .CaptchaType "math" }} + + + {{ else if .RecaptchaTurnstileStyle }} + + + {{ else }} + + + {{ end }} + {{ if ne SentryDSN "" }} + + + {{ end }} + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ if .NoIndex }}{{ end }} + {{ if .IsUserPage }} + + + + + + + + + {{ else }} + + + + + + + {{ end }} + + + + + + + + + + + + + + +
+ {{ if not .IsUserPage }} + + {{ end }} + +
+ + + +{{ if .IsPixel }} +
+{{ else }} +
+{{ end }} + +{{ if eq .CaptchaType "math" }} +
+
+

{{t .LangCode "captcha.title"}}

+

{{t .LangCode "captcha.hint"}}

+
+ {{t .LangCode "captcha.loading"}} + +
+
+ +
+ +
+ + +
+
+
+ + + + +{{ end }} \ No newline at end of file diff --git a/.workbuddy/backup/domain-shiroko-20260906/home.html b/.workbuddy/backup/domain-shiroko-20260906/home.html new file mode 100644 index 0000000..63c52a8 --- /dev/null +++ b/.workbuddy/backup/domain-shiroko-20260906/home.html @@ -0,0 +1,122 @@ +{{template "base/header" .}} +
+

{{t .LangCode "site.title"}}

+


⭐一个无需注册即可提问的匿名提问箱。你不需要下载App,不需要绑定社交账号,甚至连邮箱都不需要是真的。
⭐本站在原版的基础上新增了问题页面的二维码、私聊、投票、追加提问等功能以及更多自定义内容。各位用户可自行修改二维码背景图、颜色等个性化配置。
⭐可以到站长唯一提问提问箱链接小狼的提问箱尝试相关功能或给我提意见。
⭐可以加群:小狼的群一起水群。

+

近期将国内段访问交由EdgeOne接管,境外访问照旧由CloudFlare提供,经测试国内访问速度有所提升。但因EdgeOne Free套餐不提供CIDR列表,IP放行采取自动抓取手动维护模式,如果出现521错误请刷新一下页面,过几天应该就好了。

+

近期更新:
⭐2026.9 增加了提问、投票、私聊的总开关,可以设置定时收集、定量收集或关闭功能,顺便调整了前后端显示,看起来更易用了一些。
⭐2026.8 新增了投票功能、追加提问、多语言、Markdown显示、置顶显示等多个功能,详情见这里。
⭐2026.8 放弃使用recaptcha等对国内用户非常不友好的验证方式,更新了密码加密方式,修复了图片上传等一系列问题。
⭐2026.5 增加了私聊功能,可进行连续性对话,详见介绍;另有增加提问箱的卡片分享,更直观和美观。
⭐2026.2 新增了用户展示和标签功能,如果你希望陌生人给你提问,可以在设置页面给自己贴上5个标签并选择是否展示在探索页面:点我访问,详情见狼的公告板

+

一点提醒:
⭐本站唯一指定认证站长:小狼,注意不要被骗。 +
⭐本站不允许垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义、灌水提问的出现,如有涉证或不良言论将直接删除。

+
+

{{t .LangCode "site.stats"}}:

{{if .SiteStats}}{{.SiteStats.Text}}{{end}}

+ + +
+ + {{template "base/footer" .}} \ No newline at end of file diff --git a/.workbuddy/backup/domain-shiroko-20260906/llms-full.txt b/.workbuddy/backup/domain-shiroko-20260906/llms-full.txt new file mode 100644 index 0000000..8685afd --- /dev/null +++ b/.workbuddy/backup/domain-shiroko-20260906/llms-full.txt @@ -0,0 +1,121 @@ +# 狼的提问箱 / Wolf's Question Box — 完整说明 + +狼的提问箱(Wolf's Question Box)是一个免费、无需注册的匿名提问箱服务,是 NekoBox 的社区分支(community fork)。访客无需下载 App、绑定社交账号或提供邮箱,即可向箱主匿名提问;箱主可公开或私密回答,并与提问者进行追问式的多轮对话。 + +> 隐私声明:单个提问箱内的问答内容属于用户隐私,不对外公开索引,也不写入 sitemap。本文件仅使用站点的公开功能信息,不包含任何具体提问箱或提问内容。 + +## 1. 服务定位 + +- 面向个人创作者、UP 主、博主等,提供一个让粉丝/陌生人匿名提问的入口。 +- 匿名是核心:提问者不需要任何账号、邮箱或社交登录。 +- 多语言界面:简体中文、繁体中文、英文、日文。 +- 内置浅色 / 深色主题切换。 + +## 2. 核心功能详解 + +### 2.1 匿名提问 +访客打开任意箱主的提问箱页面即可直接提问,全程无需登录。提问内容以纯文本或 Markdown 提交。 + +### 2.2 回答与追问(多轮对话) +- 箱主可对问题进行公开回答。 +- 提问者可就同一问题「继续追问」,箱主可再次回复,形成来回多轮对话,适合连续交流、澄清或访谈场景。 +- 已回答的追问,箱主可随时「更新回答」。 + +### 2.3 Markdown 渲染 +问题与回答支持 Markdown 显示(标题、列表、引用、代码块、表格、图片等),保留排版格式。 + +### 2.4 置顶 +箱主可将重要或精彩的问题「置顶」,固定展示在提问箱顶部,便于新访客一眼看到。 + +### 2.5 访问密码 / 提问密码 +- **查看密码**:访问提问箱页面时需要输入密码,适合仅对特定人群开放。 +- **提问密码**:向某提问箱提问时需要输入密码,防止陌生人随意刷屏。 +- 密码采用加密方式存储,不保存明文。 + +### 2.6 二维码分享 +每个问题页均可生成分享二维码,箱主可自定义二维码的背景图与颜色,方便线下或社媒直接扫码访问。 + +### 2.7 标签与探索页 +- 箱主最多可设置 5 个标签,并选择是否展示在「探索」页面。 +- 「探索」页(`/explore`)按用户与标签发现其他公开提问箱,是站内唯一的公开内容发现入口。 + +### 2.8 私聊 +箱主与访客之间可进行一对一的连续私密对话(私聊),区别于公开问答,内容不对外展示。 + +### 2.9 多语言 +界面支持简体中文、繁体中文、英文、日文四种语言,跟随用户选择或浏览器/站点偏好。 + +### 2.10 深色模式 +全站内置浅色 / 深色主题切换,偏好记忆在本地 cookie。 + +### 2.11 验证码与图片上传 +- 为兼顾国内外用户,已弃用对国内不友好的 reCAPTCHA,改用更友好的验证方式(如算数验证码 / Turnstile)。 +- 提问与回答支持图片上传,上传前会做像素总量校验以防解压炸弹。 + +## 3. 使用流程 + +### 创建提问箱(箱主) +1. 访问 `/register` 注册一个免费账号,获得专属提问箱与二级域名(如 `/_/你的用户名`)。 +2. 在 `/user/profile` 设置用户名、头像、简介与标签。 +3. 按需开启「查看密码」或「提问密码」,并设置是否参与探索页。 + +### 匿名提问(访客) +1. 打开某箱主的提问箱页面(如 `/_/用户名`)。 +2. 若箱主设置了提问密码,先输入密码。 +3. 填写问题并提交(可勾选邮件通知,但邮箱非必填)。 +4. 等待箱主公开回答;如需补充,可使用「追问」。 + +### 分享 +- 复制提问箱链接,或使用问题页二维码分享。 +- 推荐国内用户使用主域名 `https://box.tama.guru`。 + +## 4. 常见问题(FAQ) + +### 4.1 给提问者(访客) + +- **提问需要注册吗?** 不需要。任何人都能匿名提问;只有创建与管理提问箱(注册账号)才需要登录。 +- **我的匿名性能保证吗?** 提问者无需提供账号或邮箱即可提问;是否填写通知邮箱完全可选,不填也能正常使用。 +- **我可以追问吗?** 可以。对箱主已公开回答的问题,你可「继续追问」,箱主再次回复后形成来回多轮对话,适合澄清、访谈或连续交流。 +- **我能收到回答通知吗?** 可以。提问时若留下邮箱(可选),箱主首次公开回答后你会收到邮件通知;邮件内含一条私密链接,可凭其查看该问题、接收回复并继续追问。 +- **提问有数量限制吗?** 有基础频率限制(约 30 条/分钟/IP)以防刷屏滥用,正常使用基本无感。 +- **能上传图片吗?** 能。提问与回答均支持图片上传,上传前会做像素总量等安全校验以防解压炸弹。 +- **需要验证码吗?** 需要。提问前需通过验证码(算数验证码 / Turnstile),已弃用对国内不友好的 reCAPTCHA,兼顾国内外用户。 +- **提问内容会被搜索引擎收录吗?** 不会。单个提问箱及其问答内容属于用户隐私,已排除在索引与 sitemap 之外;仅首页、注册、登录、探索页等公开页可被收录。 + +### 4.2 给箱主(创作者) + +- **如何创建提问箱?** 注册一个免费账号,即可获得专属路径(如 `/_/你的用户名`),作为你的匿名提问箱。 +- **如何设置个人资料?** 在 `/user/profile` 设置用户名、头像、简介与标签。 +- **如何限制谁能访问或提问?** 可开启「查看密码」(访问页面需密码)与「提问密码」(提问需密码),均以加密形式存储,服务端不保存明文。 +- **我能删除某条提问吗?** 可以。箱主可删除提问箱内的任意提问。 +- **我能隐藏某条提问/回答吗?** 可以。单条问题可设为「私密」不对外公开展示,也可随时改回公开。 +- **如何把精彩问题置顶?** 使用「置顶」功能,将重要或精彩的问题固定在提问箱顶部,便于新访客一眼看到。 +- **如何出现在探索页?** 设置最多 5 个标签,并选择是否展示在「探索」页(`/explore`),这是站内唯一的公开内容发现入口。 +- **我能和访客私聊吗?** 可以。支持一对一连续私密对话(区别于公开问答);可拉黑对方、或删除整个对话,被拉黑方将无法继续发送。 +- **有新提问/新消息会通知我吗?** 会。若你已设置邮箱,有新提问、新私聊消息时会收到邮件通知。 +- **分享卡片 / 二维码能自定义吗?** 可以。问题页与提问箱页面均可生成分享卡片与二维码,并自定义二维码背景图与颜色,方便线下或社媒直接扫码访问。 +- **内容审核如何运作?** 采用自动与人工结合的审核,对垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁及无意义灌水内容进行处置与删除。 + +### 4.3 通用 + +- **支持哪些语言?** 简体中文、繁体中文、英文、日文;可在页面切换,偏好记忆于本地 cookie。 +- **支持深色模式吗?** 支持。全站内置浅色 / 深色主题切换,偏好同样记忆于本地 cookie。 +- **这是 NekoBox 吗?** 不是。TamaBox(狼的提问箱)是 NekoBox 的社区分支(community fork),在功能与多语言方面做了扩展。 +- **收费吗?** 免费。 +- **数据安全吗?** 密码加密存储,图片上传前后均做安全校验,单个提问箱内容不对外索引。 +- **如何分享我的提问箱?** 复制提问箱链接,或使用页面二维码、分享卡片;国内用户推荐使用主域名 `https://box.tama.guru`。 +- **忘记密码怎么办?** 登录页提供找回密码流程(通过注册邮箱重置),具体以登录页提示为准。 + +## 5. 隐私与使用政策 + +- **隐私**:单个提问箱内容属于用户隐私,不对外索引、不写入 sitemap。 +- **内容规范**:禁止垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义或灌水内容;违规内容将被删除。 +- **数据安全**:图片上传前后均做安全校验,密码加密存储。 + +## 6. 资源 + +- 更新日志(功能更新记录):https://blog.tama.guru/record/tamabox_changelog_20260813 +- 站点首页:https://box.tama.guru/ +- 探索页:https://box.tama.guru/explore +- 注册:https://box.tama.guru/register +- 登录:https://box.tama.guru/login diff --git a/.workbuddy/backup/domain-shiroko-20260906/llms.txt b/.workbuddy/backup/domain-shiroko-20260906/llms.txt new file mode 100644 index 0000000..843add9 --- /dev/null +++ b/.workbuddy/backup/domain-shiroko-20260906/llms.txt @@ -0,0 +1,29 @@ +# 狼的提问箱 / Wolf's Question Box + +> 狼的提问箱(Wolf's Question Box)是一个免费、无需注册的匿名提问箱服务。访客无需下载 App、绑定社交账号或提供邮箱,即可向箱主匿名提问;箱主可公开或私密回答,并与提问者进行追问式的多轮对话。本站为 NekoBox 的社区分支(community fork)。单个提问箱内的问答内容属于用户隐私,不对外公开索引。 + +## 概述 +- [首页](https://box.tama.guru/): 站点介绍与多语言落地页。 +- [探索](https://box.tama.guru/explore): 按用户与标签发现公开的提问箱。 +- [注册](https://box.tama.guru/register): 免费创建自己的提问箱。 +- [登录](https://box.tama.guru/login): 登录以管理你的提问箱。 + +## 核心功能 +- 匿名提问:无需账号、邮箱或社交登录即可提问。 +- 回答与追问:箱主可公开回答;提问者可继续追问,箱主可回复,形成多轮来回对话。 +- Markdown 渲染:问题与回答支持 Markdown 显示。 +- 访问控制:可分别设置「查看提问箱密码」与「提问密码」,密码加密存储。 +- 置顶:将重要问题置顶展示。 +- 二维码分享:每个问题页生成可分享二维码,背景图与颜色可自定义。 +- 标签与发现:最多添加 5 个标签,展示于探索页。 +- 私聊:与箱主一对一的连续私密对话。 +- 多语言界面:简体中文、繁体中文、英文、日文。 +- 深色模式:内置浅色 / 深色主题切换。 + +## 资源 +- [完整说明 (llms-full.txt)](https://box.tama.guru/llms-full.txt): 站点完整功能说明、使用流程、FAQ 与隐私政策。 +- [更新日志](https://blog.tama.guru/record/tamabox_changelog_20260813): 近期功能更新(追问、多语言、Markdown、置顶等)。 + +## 使用政策 +- 隐私:单个提问箱内容属于隐私,已排除在搜索引擎索引与 sitemap 之外。 +- 内容规范:禁止垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义或灌水内容,违规内容将被删除。 diff --git a/.workbuddy/backup/domain-shiroko-20260906/register.go b/.workbuddy/backup/domain-shiroko-20260906/register.go new file mode 100644 index 0000000..1eab76d --- /dev/null +++ b/.workbuddy/backup/domain-shiroko-20260906/register.go @@ -0,0 +1,92 @@ +// Copyright 2022 E99p1ant. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package auth + +import ( + "github.com/flamego/recaptcha" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/tamakyi/TamaBox/internal/captcha" + "github.com/tamakyi/TamaBox/internal/conf" + "github.com/tamakyi/TamaBox/internal/context" + "github.com/tamakyi/TamaBox/internal/db" + "github.com/tamakyi/TamaBox/internal/form" + "github.com/tamakyi/TamaBox/internal/i18n" + "github.com/tamakyi/TamaBox/internal/shared" +) + +func Register(ctx context.Context) { + ctx.Success("auth/register") +} + +func RegisterAction(ctx context.Context, f form.Register, recaptcha recaptcha.RecaptchaV3) { + if ctx.HasError() { + ctx.Success("auth/register") + return + } + + // Check captcha code (unified: recaptcha, turnstile, or math). + ok, err := captcha.Verify(f.Recaptcha, shared.ResolveClientIP(ctx.Request().Request), ctx.Session, recaptcha, ctx.Lang()) + if err != nil { + logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to check captcha") + ctx.SetErrorFlash(err.Error()) + ctx.Redirect("/register") + return + } + if !ok { + ctx.SetErrorFlash(i18n.T(ctx.Lang(), "captcha.error")) + ctx.Redirect("/register") + return + } + + if err := db.Users.Create(ctx.Request().Context(), db.CreateUserOptions{ + Name: f.Name, + Password: f.Password, + Email: f.Email, + Avatar: conf.Upload.DefaultAvatarURL, + Domain: f.Domain, + Background: conf.Upload.DefaultBackground, + Intro: "问你想问的", + Qrcodebackcolor: "#FDF8F0", + Qrcodecolor: "#C45500", + Usernamecolor: "#000000", + Introcolor: "#000000", + Dotscale: "0.35", + BackgroundImage: conf.Upload.DefaultBackgroundImageURL, + Backgroundimagealpha: "0.8", + Qrcodepdpcolor: "#C45500", + Acfunlink: "none", + Bililink: "none", + Weibolink: "none", + Bloglink: "box.tama.guru", + Xiaohongshulink: "none", + Language: i18n.Resolve(f.Language), // 注册时选择的界面语言;留空回退站点默认 + }); err != nil { + switch { + case errors.Is(err, db.ErrDuplicateEmail): + ctx.SetError(errors.New(i18n.T(ctx.Lang(), "auth.errDuplicateEmail"))) + case errors.Is(err, db.ErrDuplicateDomain): + ctx.SetError(errors.New(i18n.T(ctx.Lang(), "auth.errDuplicateDomain"))) + case errors.Is(err, db.ErrBadCredential): + ctx.SetError(errors.New(i18n.T(ctx.Lang(), "auth.errBadCredential"))) + + default: + logrus.WithContext(ctx.Request().Context()).WithError(err).Error("Failed to create new user") + ctx.SetInternalError() + } + + ctx.Success("auth/register") + return + } + + // 成功提示跟随用户注册时选择的语言 + lang := "zh-CN" + if l, ok := ctx.Data["Lang"].(string); ok && l != "" { + lang = l + } + ctx.SetSuccessFlash(i18n.T(lang, "auth.register.success")) + ctx.Redirect("/login") +} diff --git a/.workbuddy/backup/domain-shiroko-20260906/robots.txt b/.workbuddy/backup/domain-shiroko-20260906/robots.txt new file mode 100644 index 0000000..4977c33 --- /dev/null +++ b/.workbuddy/backup/domain-shiroko-20260906/robots.txt @@ -0,0 +1,7 @@ +User-agent: * +Disallow: /_/ +Disallow: /user/ +Allow: /explore +Allow: /llms.txt +Allow: /llms-full.txt +Sitemap: https://box.tama.guru/sitemap.xml diff --git a/.workbuddy/backup/domain-shiroko-20260906/route.go.bak b/.workbuddy/backup/domain-shiroko-20260906/route.go.bak new file mode 100644 index 0000000..872608e --- /dev/null +++ b/.workbuddy/backup/domain-shiroko-20260906/route.go.bak @@ -0,0 +1,414 @@ +// Copyright 2022 E99p1ant. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package route + +import ( + "encoding/gob" + "io" + "net/http" + "strings" + "time" + + "github.com/flamego/cache" + cacheRedis "github.com/flamego/cache/redis" + "github.com/flamego/csrf" + "github.com/flamego/flamego" + "github.com/flamego/recaptcha" + "github.com/flamego/session" + "github.com/flamego/session/mysql" + sessionRedis "github.com/flamego/session/redis" + "github.com/flamego/template" + "github.com/sirupsen/logrus" + + "github.com/tamakyi/TamaBox/internal/captcha" + "github.com/tamakyi/TamaBox/internal/conf" + "github.com/tamakyi/TamaBox/internal/context" + "github.com/tamakyi/TamaBox/internal/form" + "github.com/tamakyi/TamaBox/internal/i18n" + "github.com/tamakyi/TamaBox/internal/mail" + "github.com/tamakyi/TamaBox/internal/route/chat" // 私聊路由 + "github.com/tamakyi/TamaBox/internal/security/autoban" + "github.com/tamakyi/TamaBox/internal/shared" + templatepkg "github.com/tamakyi/TamaBox/internal/template" + "github.com/tamakyi/TamaBox/route" + "github.com/tamakyi/TamaBox/route/auth" + "github.com/tamakyi/TamaBox/route/pixel" + "github.com/tamakyi/TamaBox/route/question" + "github.com/tamakyi/TamaBox/route/service" + "github.com/tamakyi/TamaBox/route/user" + "github.com/tamakyi/TamaBox/route/vote" + "github.com/tamakyi/TamaBox/static" + "github.com/tamakyi/TamaBox/templates" +) + +// CSRFUserSessionKey 是 CSRF token 绑定的会话键,必须与登录时写入的会话键一致 +// (见 route/auth/login.go 中 ctx.Session.Set("uid", ...))。flamego/csrf 默认使用 +// "userID",若不与之对齐,token 的 id 恒为 "0",任意匿名访客从公开页面取到的 token +// 对全部已登录用户均有效,CSRF 退化为全站公开常量。 +// +// 本常量同时被 csrf_binding_test.go 引用,作为单一事实来源:一旦被误改,回归测试会在 +// CI 失败,避免修复悄悄倒退。 +const CSRFUserSessionKey = "uid" + +// securityHeaders adds security-related HTTP headers to every response. +func securityHeaders() flamego.Handler { + return func(ctx flamego.Context) { + w := ctx.ResponseWriter() + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") + w.Header().Set("X-XSS-Protection", "1; mode=block") + w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") + // HSTS:仅当站点以 https 对外提供服务时下发,避免 http 部署下把用户锁死在 https。 + if strings.HasPrefix(conf.App.ExternalURL, "https") { + w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains") + } + } +} + +func New() *flamego.Flame { + f := flamego.Classic() + if conf.App.Production { + flamego.SetEnv(flamego.EnvTypeProd) + } + + // 自动封禁实例:整个请求链路共享同一个,供中间件与 f.NotFound 复用, + // 避免对业务性 302→/(语言切换、登出、投票不存在等)误判为路径扫描。 + // 404 是否参与统计由 ban_track_notfound 控制(默认 false:正常业务一律放行, + // 仅敏感扫描路径计数封禁,见 conf.Security.BanTrackNotFound)。 + ab := autoban.New(autoban.Options{ + Enabled: conf.Security.EnableAutoBan, + TrackNotFound: conf.Security.BanTrackNotFound, + ThresholdSensitive: conf.Security.BanThresholdSensitive, + Threshold404: conf.Security.BanThreshold404, + Window: time.Duration(conf.Security.BanWindowSeconds) * time.Second, + Duration: time.Duration(conf.Security.BanDurationMinutes) * time.Minute, + AlertEmail: conf.Security.SecurityAlertEmail, + SendAlert: mail.SendSecurityAlertMail, + }) + // 需要输出详细日志时配置如下 + // if conf.App.Production { + // flamego.SetEnv(flamego.EnvTypeProd) + // } + + templateFS, err := template.EmbedFS(templates.FS, ".", []string{".html"}) + if err != nil { + logrus.WithError(err).Fatal("Failed to embed templates file system") + } + + gob.Register(time.Time{}) + gob.Register(context.Flash{}) + gob.Register(captcha.StoredAnswer{}) + var sessionStorage interface{} + initer := session.MemoryIniter() + if conf.Database.DSN != "" { + initer = mysql.Initer() + sessionStorage = mysql.Config{ + DSN: conf.Database.DSN, + Lifetime: 7 * 24 * time.Hour, + } + } + if conf.Redis.Addr != "" { + initer = sessionRedis.Initer() + sessionStorage = sessionRedis.Config{ + Options: &sessionRedis.Options{ + Addr: conf.Redis.Addr, + Password: conf.Redis.Password, + DB: 1, + }, + Lifetime: 7 * 24 * time.Hour, + } + } + sessioner := session.Sessioner(session.Options{ + Initer: initer, + Config: sessionStorage, + // 显式声明 cookie 安全属性,不依赖库默认值: + // - HTTPOnly:禁止 JS 读取,降低 XSS 窃取会话的风险 + // - SameSite=Lax:阻断经典跨站表单 POST(CSRF 的主要防线之一) + // - Secure:仅在站点以 https 对外服务时下发,避免 http 本地开发下 cookie 不生效。 + // 缺失 Secure 时,HSTS 尚未落地的首次访问可被主动 MITM 诱导明文请求窃取会话。 + Cookie: session.CookieOptions{ + HTTPOnly: true, + SameSite: http.SameSiteLaxMode, + Secure: strings.HasPrefix(conf.App.ExternalURL, "https"), + }, + }) + + f.Use(flamego.Static(flamego.StaticOptions{ + FileSystem: http.FS(static.FS), + Prefix: "/static", + // 静态资源随二进制发布,内容变更需重新部署;1 天缓存是安全折中 + CacheControl: func() string { + return "public, max-age=86400" + }, + })) + + f.Use(securityHeaders()) + f.Use(ipBlacklistMiddleware()) + f.Use(autobanMiddleware(ab)) + // 全局请求体上限:必须在任何会读取 body 的中间件(尤其 Contexter 内的 CSRF 校验) + // 之前执行,否则超大 multipart 会在 Bind 的路由级限制生效前落盘临时文件耗尽磁盘。 + f.Use(requestBodyLimitMiddleware()) + + reqUserSignOut := context.Toggle(&context.ToggleOptions{UserSignOutRequired: true}) + reqUserSignIn := context.Toggle(&context.ToggleOptions{UserSignInRequired: true}) + + f.Group("", func() { + f.Get("/", route.Home) + f.Get("/pixel", reqUserSignIn, pixel.Index) + f.Get("/sponsor", route.Sponsor) + f.Get("/change-logs", route.ChangeLogs) + f.Get("/about-me", route.AboutMe) + f.Get("/robots.txt", func(c context.Context) { + fs, err := static.FS.Open("robots.txt") + if err != nil { + c.ResponseWriter().WriteHeader(404) + return + } + defer func() { _ = fs.Close() }() + c.ResponseWriter().Header().Set("Content-Type", "text/plain; charset=utf-8") + c.ResponseWriter().Header().Set("Cache-Control", "public, max-age=86400") + _, _ = io.Copy(c.ResponseWriter(), fs) + }) + f.Get("/favicon.ico", func(c context.Context) { + fs, _ := static.FS.Open("favicon.ico") + defer func() { _ = fs.Close() }() + c.ResponseWriter().Header().Set("Content-Type", "image/x-icon") + c.ResponseWriter().Header().Set("Cache-Control", "public, max-age=86400") + _, _ = io.Copy(c.ResponseWriter(), fs) + }) + + f.Get("/sitemap.xml", func(c context.Context) { + fs, err := static.FS.Open("sitemap.xml") + if err != nil { + c.ResponseWriter().WriteHeader(404) + return + } + defer func() { _ = fs.Close() }() + c.ResponseWriter().Header().Set("Content-Type", "application/xml") + c.ResponseWriter().Header().Set("Content-Type", "text/xml; charset=utf-8") + c.ResponseWriter().Header().Set("Cache-Control", "public, max-age=86400") + _, _ = io.Copy(c.ResponseWriter(), fs) + }) + + f.Get("/llms.txt", func(c context.Context) { + fs, err := static.FS.Open("llms.txt") + if err != nil { + c.ResponseWriter().WriteHeader(404) + return + } + defer func() { _ = fs.Close() }() + c.ResponseWriter().Header().Set("Content-Type", "text/plain; charset=utf-8") + c.ResponseWriter().Header().Set("Cache-Control", "public, max-age=86400") + _, _ = io.Copy(c.ResponseWriter(), fs) + }) + + f.Get("/llms-full.txt", func(c context.Context) { + fs, err := static.FS.Open("llms-full.txt") + if err != nil { + c.ResponseWriter().WriteHeader(404) + return + } + defer func() { _ = fs.Close() }() + c.ResponseWriter().Header().Set("Content-Type", "text/plain; charset=utf-8") + c.ResponseWriter().Header().Set("Cache-Control", "public, max-age=86400") + _, _ = io.Copy(c.ResponseWriter(), fs) + }) + + f.Group("", func() { + f.Combo("/register").Get(auth.Register).Post(form.Bind(form.Register{}), rateLimitMiddleware(10, 1*time.Minute), auth.RegisterAction) + f.Combo("/login").Get(auth.Login).Post(form.Bind(form.Login{}), rateLimitMiddleware(20, 1*time.Minute), auth.LoginAction) + f.Combo("/forgot-password").Get(auth.ForgotPassword).Post(form.Bind(form.ForgotPassword{}), rateLimitMiddleware(5, 5*time.Minute), auth.ForgotPasswordAction) + f.Combo("/recover-password").Get(auth.RecoverPassword).Post(form.Bind(form.RecoverPassword{}), auth.RecoverPasswordAction) + }, reqUserSignOut) + + // 页面密码验证路由(独立于密码保护中间件);限流防爆破 + f.Combo("/_/{domain}/password").Get(question.PasswordPage).Post(rateLimitMiddleware(10, 1*time.Minute), question.VerifyPagePassword) + + // 独立的分享图路由(不经过 Pager 中间件,直接返回图片) + // 注意:仍挂载 question.Pager,页面设置了密码时必须先通过密码验证, + // 否则分享图会绕过页面密码保护(与单题分享图行为一致)。 + f.Group("/_/{domain}", func() { + f.Get("/share-card", question.ShareCard) + }, question.Pager) + + f.Group("/_/{domain}", func() { + f.Combo("").Get(question.List).Post(form.Bind(form.NewQuestion{}), rateLimitMiddleware(30, 1*time.Minute), question.New) + f.Group("/{questionID}", func() { + f.Get("", question.Item) + f.Get("/share-card", question.QuestionShareCard) + f.Post("/delete", question.Delete) + f.Post("/set-private", question.SetPrivate) + f.Post("/set-public", question.SetPublic) + f.Post("/pin", question.TogglePin) + f.Post("/read", question.SetRead) + f.Post("/unread", question.SetUnread) + f.Post("/answer", reqUserSignIn, form.Bind(form.PublishAnswerQuestion{}), question.PublishAnswer) + f.Post("/follow-up", form.Bind(form.PublishFollowUpQuestion{}), question.PublishFollowUp) + f.Post("/follow-up/{followUpID}/answer", reqUserSignIn, form.Bind(form.AnswerFollowUpQuestion{}), question.AnswerFollowUp) + }, question.Questioner) + + // 新增:私聊路由(需要登录,使用 Token 标识对话) + f.Group("/chat/{token}", func() { + f.Get("", chat.ConversationPage) + f.Post("/send", rateLimitMiddleware(20, 1*time.Minute), chat.SendMessage) + f.Post("/delete", chat.DeleteConversation) + f.Post("/block", chat.ToggleBlockUser) + }, reqUserSignIn) + }, question.Pager) // Pager 中间件已内置页面密码检查 + + f.Get("/explore", user.Explore) + + // 匿名投票参与端(公开):token 链接入口 + f.Group("/vote/{token}", func() { + f.Get("", vote.Page) + // 提交限流优先按登录用户/签名 vote_fp 计数,IP 仅作高阈值兜底,避免共享出口误伤。 + f.Post("/submit", voteSubmitRateLimitMiddleware(20, 1*time.Minute), vote.Submit) + }) + + // 公开页语言切换:把语言偏好写入 session(由 session 中间件随其 cookie 一并持久化) + // 后跳回原页面。此前写独立 lang cookie 会被 session 中间件的 Set-Cookie 整体覆盖而失效, + // 故改为托管在 session 中。仅允许站内相对路径,防开放重定向。 + f.Get("/i18n/{lang}", func(c context.Context) { + lang := i18n.Resolve(c.Param("lang")) + // 写入 session,避免被 session 中间件的 Set-Cookie 覆盖(匿名用户也走 session 中间件)。 + c.Session.Set("lang", lang) + to := c.Query("to") + // 仅允许站内相对路径,防开放重定向: + // 拒绝空、非 "/" 开头、以 "//" 开头的协议相对路径,以及含反斜杠/控制字符的变体。 + if to == "" || !strings.HasPrefix(to, "/") || + strings.HasPrefix(to, "//") || strings.ContainsAny(to, "\\\x00\r\n\t") { + to = "/" + } + c.Redirect(to) + }) + + // 算数验证码 API(仅在 captcha_type = "math" 时生效,但路由本身不依赖中间件) + // 加限流:防止匿名脚本无限生成/轮询爆破(答案本身对客户端可见,限流是主要节流手段)。 + f.Get("/api/captcha/generate", rateLimitMiddleware(20, 1*time.Minute), captcha.GenerateHandler) + f.Post("/api/captcha/verify", rateLimitMiddleware(20, 1*time.Minute), captcha.VerifyHandler) + + f.Group("/user", func() { + f.Get("/questions", user.QuestionList) + + // 匿名投票管理(箱主侧) + f.Group("/votes", func() { + f.Get("", user.VoteList) + f.Get("/new", user.VoteNew) + f.Post("", rateLimitMiddleware(10, 1*time.Minute), user.VoteCreate) + f.Get("/{voteID}/edit", user.VoteEdit) + f.Post("/{voteID}", rateLimitMiddleware(10, 1*time.Minute), user.VoteUpdate) + f.Post("/{voteID}/publish", user.VotePublish) + f.Post("/{voteID}/close", user.VoteClose) + f.Post("/{voteID}/delete", user.VoteDelete) + f.Get("/{voteID}/results", user.VoteResults) + f.Get("/{voteID}/preview", user.VotePreview) + // 导出为只读操作(幂等、无副作用),用 GET 链接下载,避免表单提交 + f.Get("/{voteID}/export", user.VoteExportCSV) + }) + + f.Group("/profile", func() { + f.Get("", user.Profile) + f.Post("/update", form.Bind(form.UpdateProfile{}), rateLimitMiddleware(10, 1*time.Minute), user.UpdateProfile) + f.Post("/export", user.ExportProfile) + f.Post("/export-json", user.ExportProfileJSON) + f.Combo("/deactivate").Get(user.DeactivateProfile).Post(user.DeactivateProfileAction) + + f.Post("/update-page-password", form.Bind(form.UpdatePagePassword{}), rateLimitMiddleware(10, 1*time.Minute), user.UpdatePagePassword) + f.Post("/update-ask-password", form.Bind(form.UpdateAskPassword{}), rateLimitMiddleware(10, 1*time.Minute), user.UpdateAskPassword) + + f.Post("/update-chat-settings", user.UpdateChatSettings) + + // 功能管理(提问 / 私聊 / 投票 的开关、定时开关与定量收集): + // 不单独开页面,作为账号设置页的三个面板,保存后回跳到对应面板。 + f.Group("/features", func() { + f.Post("/update", rateLimitMiddleware(20, 1*time.Minute), user.FeaturesUpdate) + f.Post("/reset-quota", rateLimitMiddleware(20, 1*time.Minute), user.FeatureResetQuota) + }) + }) + // 防骚扰设置:注册限制(harassment/update)与全局屏蔽词(blockwords/update) + // 拆为两个独立入口。屏蔽词在提问/私聊/投票三个设置面板共用一份,保存后经 + // ?tab= 参数回跳到来源面板。 + f.Post("/harassment/update", form.Bind(form.UpdateHarassment{}), user.UpdateHarassment) + f.Post("/blockwords/update", form.Bind(form.UpdateBlockWords{}), user.UpdateBlockWords) + f.Post("/tags/update", form.Bind(form.UpdateTags{}), user.UpdateTags) + // Logout 必须用 POST:GET 会留下 CSRF 风险(第三方图片/链接可触发退出登录) + f.Post("/logout", auth.Logout) + }, reqUserSignIn) + + f.Group("/api/v1", func() { + f.Group("/user", func() { + f.Get("", reqUserSignIn, user.ProfileAPI) + f.Group("/{domain}", func() { + f.Group("/questions", func() { + f.Get("", rateLimitMiddleware(60, 1*time.Minute), question.ListAPI) + }) + }) + }) + // 像素追踪代理:仅暴露只读 GET(f.Any 会放行写方法,曾有 CSRF 豁免 + 写方法放行的风险), + // 并加限流防止被用作高频打点通道。 + f.Get("/pixel/{**}", reqUserSignIn, rateLimitMiddleware(30, 1*time.Minute), pixel.Proxy) + // 服务反代仅暴露只读 GET:本网关是把已登录用户的请求转发到配置好的内部服务, + // 不应允许任意写方法,避免被用作对内网后端的越权操作入口。 + f.Get("/service/{**}", reqUserSignIn, service.Proxy) + f.Get("/explore/random", rateLimitMiddleware(60, 1*time.Minute), user.RandomRecommend) + // 首页"提问箱信息统计"公开接口(服务端 30 分钟缓存),供前端定时刷新 + f.Get("/site-stats", rateLimitMiddleware(60, 1*time.Minute), route.SiteStatsAPI) + }, context.APIEndpoint) + }, + cache.Cacher(cache.Options{ + Initer: cacheRedis.Initer(), + Config: cacheRedis.Config{ + Options: &cacheRedis.Options{ + Addr: conf.Redis.Addr, + Password: conf.Redis.Password, + DB: 0, + }, + }, + }), + recaptcha.V3( + recaptcha.Options{ + Secret: conf.Recaptcha.ServerKey, + VerifyURL: func() recaptcha.VerifyURL { + if conf.Recaptcha.TurnstileStyle { + return "https://challenges.cloudflare.com/turnstile/v0/siteverify" + } + return recaptcha.VerifyURLGlobal + }(), + }, + ), + sessioner, + csrf.Csrfer(csrf.Options{ + Secret: conf.Server.XSRFKey, + Header: "X-CSRF-Token", + // ⚠️ 必须与 route/auth/login.go 中 ctx.Session.Set("uid", ...) 的 key 一致。 + // flamego/csrf 默认 SessionKey 为 "userID",若不显式指定,s.Get("userID") + // 恒为 nil,token 内绑定的 id 恒为 "0" —— 由于校验是无状态 HMAC 重算 + // (不与 session 中存储的 token 比对),这会导致任意匿名访客从公开的 + // /login 页面取到的 token 对所有已登录用户均有效,CSRF 退化为全站公开常量。 + // 回归测试见 route/csrf_binding_test.go(引用 CSRFUserSessionKey)。 + SessionKey: CSRFUserSessionKey, + }), + template.Templater(template.Options{ + FileSystem: templateFS, + FuncMaps: templatepkg.FuncMap(), + }), + context.Contexter(), + ) + f.NotFound(func(ctx flamego.Context) { + // 真正的 404 探测在此记录(仅此处,不把业务 302→/ 误判为扫描)。 + // 注意:flamego 的 NotFound 会先写入 404 状态再 Redirect 到 "/", + // 因此这里在 Redirect 之前记录,确保状态为 404。 + // 键必须与 autobanMiddleware 中的 IsBanned/Record 保持一致(同样经 LimitKey 聚合), + // 否则 404 计数会记在另一个键上,导致封禁永不生效。 + // ⚠️ 该 404 计数受 ban_track_notfound 配置控制,默认关闭(正常业务一律放行, + // 仅敏感扫描路径计数封禁);Record 内部对未开启的 KindNotFound 直接忽略。 + if ip := shared.LimitKey(shared.ResolveClientIP(ctx.Request().Request)); ip != "" { + ab.Record(ip, ctx.Request().URL.Path, autoban.KindNotFound) + } + ctx.Redirect("/") + }) + + return f +} diff --git a/.workbuddy/backup/domain-shiroko-20260906/sitemap.xml b/.workbuddy/backup/domain-shiroko-20260906/sitemap.xml new file mode 100644 index 0000000..d5e91c3 --- /dev/null +++ b/.workbuddy/backup/domain-shiroko-20260906/sitemap.xml @@ -0,0 +1,58 @@ + + + + + https://box.tama.guru/explore + 0.3 + 2026-02-27 + weekly + + + + https://box.tama.guru/ + 0.3 + 2026-02-27 + weekly + + + + https://box.tama.guru/register + 0.3 + 2026-02-27 + weekly + + + + https://box.tama.guru/login + 0.3 + 2026-02-27 + weekly + + + + https://box.tama.guru/_/tamakyi + 0.3 + 2026-02-27 + weekly + + + + https://box.tama.guru + 0.3 + 2026-02-27 + weekly + + + + https://box.tama.guru/change-logs + 0.3 + 2026-02-27 + weekly + + + \ No newline at end of file diff --git a/.workbuddy/backup/domain-shiroko-20260906/text.go b/.workbuddy/backup/domain-shiroko-20260906/text.go new file mode 100644 index 0000000..60cc7f4 --- /dev/null +++ b/.workbuddy/backup/domain-shiroko-20260906/text.go @@ -0,0 +1,118 @@ +// Copyright 2022 E99p1ant. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package censor + +import ( + "context" + "encoding/json" + "fmt" + "strings" +) + +type ForbiddenType string + +const ( + ForbiddenTypeSpam ForbiddenType = "spam" + ForbiddenTypeAd ForbiddenType = "ad" + ForbiddenTypePolitics ForbiddenType = "politics" + ForbiddenTypeTerrorism ForbiddenType = "terrorism" + ForbiddenTypeAbuse ForbiddenType = "abuse" + ForbiddenTypePorn ForbiddenType = "porn" + ForbiddenTypeFlood ForbiddenType = "flood" + ForbiddenTypeContraband ForbiddenType = "contraband" + ForbiddenTypeMeaningless ForbiddenType = "meaningless" + ForbiddenTypeHarmful ForbiddenType = "harmful" +) + +func (f ForbiddenType) String() string { + return map[ForbiddenType]string{ + ForbiddenTypeSpam: "含垃圾信息", + ForbiddenTypeAd: "广告", + ForbiddenTypePolitics: "涉政", + ForbiddenTypeTerrorism: "暴恐", + ForbiddenTypeAbuse: "辱骂", + ForbiddenTypePorn: "色情", + ForbiddenTypeFlood: "灌水", + ForbiddenTypeContraband: "违禁", + ForbiddenTypeMeaningless: "无意义", + ForbiddenTypeHarmful: "不良场景", + }[f] +} + +type TextCensorResponse struct { + SourceName string `json:"source_name"` + Pass bool `json:"pass"` + ForbiddenType ForbiddenType `json:"forbidden_type"` + Hint string `json:"hint"` + Confidence float64 `json:"confidence"` + RawResponse json.RawMessage `json:"raw_response"` +} + +func (r *TextCensorResponse) ToJSON() []byte { + jsonBytes, _ := json.Marshal(r) + return jsonBytes +} + +func (r *TextCensorResponse) ErrorMessage() string { + errorMessage := "内容安全检查不通过" + if r.ForbiddenType != "" { + errorMessage += fmt.Sprintf(" [%s]", r.ForbiddenType.String()) + } + if r.Hint != "" { + errorMessage += fmt.Sprintf(",相关标签:%q", r.Hint) + } + return errorMessage +} + +type TextCensor interface { + Censor(ctx context.Context, text string) (*TextCensorResponse, error) + String() string +} + +var TrustedHosts = []string{ + "xhslink.com", // 小红书 + "weibo.com", // 微博 + "b23.tv", // Bilibili + "acfun.cn", + "aixifan.com", + "github.com", + "tama.guru", + "337845818.best", + "x.com", + "bilibili.com", +} + +// RemoveTrustedURL removes the trusted URL from the text, +// in order to bypass the URL censoring. +// 仅当可信域名后紧跟路径分隔符(/ ? #)或位于字符串结尾时才视为可信, +// 避免 github.com.evil.tld 被朴素替换误判为 github.com 之子域而漏审。 +func RemoveTrustedURL(str string) string { + for _, host := range TrustedHosts { + for _, protocol := range []string{"http://", "https://"} { + needle := protocol + host + var b strings.Builder + rest := str + for { + idx := strings.Index(rest, needle) + if idx < 0 { + b.WriteString(rest) + break + } + b.WriteString(rest[:idx]) + after := rest[idx+len(needle):] + if len(after) == 0 || after[0] == '/' || after[0] == '?' || after[0] == '#' { + // 可信域名边界:跳过该前缀,从域名之后继续(域名本身被移除,不进入审核)。 + rest = after + } else { + // 非可信域名边界(如 github.com.evil):保留前缀,从其后继续扫描。 + b.WriteString(needle) + rest = after + } + } + str = b.String() + } + } + return str +} diff --git a/.workbuddy/backup/p0-avatar-qrcode-20260905/internal/storage/s3.go b/.workbuddy/backup/p0-avatar-qrcode-20260905/internal/storage/s3.go new file mode 100644 index 0000000..7f93ca5 --- /dev/null +++ b/.workbuddy/backup/p0-avatar-qrcode-20260905/internal/storage/s3.go @@ -0,0 +1,279 @@ +// Copyright 2024 E99p1ant. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package storage + +import ( + "bytes" + "context" + "fmt" + "image" + _ "image/gif" + _ "image/jpeg" + _ "image/png" + "io" + "mime/multipart" + "os" + "os/exec" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/chai2010/webp" + "github.com/disintegration/imaging" + "github.com/pkg/errors" + "github.com/thanhpk/randstr" + "github.com/wuhan005/gadget" + + "github.com/tamakyi/TamaBox/internal/conf" + "github.com/tamakyi/TamaBox/internal/imagecheck" +) + +// MaxUploadBytes 是单次图片上传的内存读取上限(8MB)。 +// 调用方虽在 HTTP 层对 fileHeader.Size 做了校验,但该值可被客户端伪造, +// 因此在读取前用 io.LimitReader 兜底,避免超大文件将内存撑爆(DoS)。 +const MaxUploadBytes = 8 * 1024 * 1024 + +// aspect 约束一次上传的宽高比(wantW:wantH,容差 DefaultAspectTolerance)。 +// 头像固定 1:1、页面背景固定 16:9:客户端裁剪后即为精确比例, +// 服务端复校验用于拦截绕过前端直接 POST 的越界图片。 +type aspect struct { + W, H float64 +} + +func (a *aspect) check(data []byte) error { + if a == nil { + return nil + } + return imagecheck.CheckAspectRatio(bytes.NewReader(data), a.W, a.H, imagecheck.DefaultAspectTolerance) +} + +// UploadAvatarToS3 上传头像:除通用校验/转换外强制 1:1 宽高比。 +func UploadAvatarToS3(file multipart.File, fileHeader *multipart.FileHeader) (string, error) { + return uploadPictureToS3(file, fileHeader, &aspect{W: 1, H: 1}) +} + +// UploadBackgroundToS3 上传页面背景:除通用校验/转换外强制 16:9 宽高比。 +func UploadBackgroundToS3(file multipart.File, fileHeader *multipart.FileHeader) (string, error) { + return uploadPictureToS3(file, fileHeader, &aspect{W: 16, H: 9}) +} + +// UploadPictureToS3 上传通用图片到 Cloudflare R2,自动转换为 WebP(动画 GIF 可选转换) +func UploadPictureToS3(file multipart.File, fileHeader *multipart.FileHeader) (string, error) { + return uploadPictureToS3(file, fileHeader, nil) +} + +// uploadPictureToS3 是上传管线实现:读入 → 魔数 → (可选)比例校验 → 像素总量校验 +// → 解码 → WebP 转换 → 落盘 R2。avatar/background 的固定比例校验复用同一管线, +// 仅通过 aspect 参数区分,避免为一种用途复制一套上传代码。 +func uploadPictureToS3(file multipart.File, fileHeader *multipart.FileHeader, asp *aspect) (string, error) { + // 1. 限制读取大小,防止超大上传撑爆内存(DoS)。 + // 先用 LimitReader 截断到 MaxUploadBytes+1 字节;若实际读到的字节数超过上限则直接拒绝。 + limited := io.LimitReader(file, MaxUploadBytes+1) + fileData, err := io.ReadAll(limited) + if err != nil { + return "", errors.Wrap(err, "read file data") + } + if int64(len(fileData)) > MaxUploadBytes { + return "", errors.New("uploaded file is too large") + } + + // 2. 校验文件魔数,仅允许 JPEG / PNG / GIF / WebP + if !isSupportedImage(fileData) { + return "", errors.New("unsupported file type: not a valid image") + } + + // 2.5 用途宽高比校验(头像 1:1 / 背景 16:9),绕过前端裁剪的直传在此被拦截 + if err := asp.check(fileData); err != nil { + return "", err + } + + fileReader := bytes.NewReader(fileData) + + // 3. 解码前先校验像素总量(防解压炸弹),再解码图片获取格式;解码失败则直接返回错误。 + if err := imagecheck.CheckPixelLimit(bytes.NewReader(fileData)); err != nil { + return "", err + } + + _, format, err := image.Decode(fileReader) + if err != nil { + return "", errors.Wrap(err, "image decode failed, file may be corrupted") + } + + var uploadData []byte + var contentType string + + // 重置 reader,以便后续可能的 resize 操作 + if _, err := fileReader.Seek(0, io.SeekStart); err != nil { + return "", errors.Wrap(err, "reset reader") + } + + // 4. 根据格式进行转换或直接使用原图 + switch format { + case "gif": + webpData, err := convertGifToAnimatedWebP(fileData) + if err != nil { + // 转换失败则回退到原始 GIF + uploadData = fileData + contentType = "image/gif" + } else { + uploadData = webpData + contentType = "image/webp" + } + default: + // 静态图片(JPEG / PNG / WebP 等)转换为静态 WebP + webpData, err := ConvertStaticImageToWebP(fileReader) + if err != nil { + // 转换失败时回退到原始图片 + uploadData = fileData + contentType = mimeTypeFromFormat(format) + } else { + uploadData = webpData + contentType = "image/webp" + } + } + + // 5. 生成存储路径(无后缀名) + now := time.Now() + year := now.Year() + month := int(now.Month()) + day := now.Day() + key := fmt.Sprintf("%s%d/%02d/%02d/%s", PictureKeyPrefix, year, month, day, randstr.Hex(15)) + + // 6. 获取共享 S3 客户端 + client, err := GetS3Client() + if err != nil { + return "", errors.Wrap(err, "get S3 client") + } + + // 7. 上传到 R2 + if err := gadget.Retry(5, func() error { + _, err := client.PutObject(context.Background(), &s3.PutObjectInput{ + Bucket: aws.String(conf.Upload.ImageBucket), + Key: aws.String(key), + Body: bytes.NewReader(uploadData), + ContentLength: aws.Int64(int64(len(uploadData))), + ContentType: aws.String(contentType), + }) + return err + }); err != nil { + return "", errors.Wrap(err, "put object after retry") + } + + // 8. 返回 CDN URL + return fmt.Sprintf("https://%s/%s", conf.Upload.ImageBucketCDNHost, key), nil +} + +// ConvertStaticImageToWebP 将静态图片(JPEG/PNG等)转换为 WebP 格式 +// 限制最大宽度为 1920px,保持比例 +func ConvertStaticImageToWebP(r io.Reader) ([]byte, error) { + // 先整体读入并校验像素总量(防解压炸弹),再完整解码。 + data, err := io.ReadAll(r) + if err != nil { + return nil, errors.Wrap(err, "read image") + } + if err := imagecheck.CheckPixelLimit(bytes.NewReader(data)); err != nil { + return nil, errors.Wrap(err, "check image pixel limit") + } + + img, _, err := image.Decode(bytes.NewReader(data)) + if err != nil { + return nil, errors.Wrap(err, "decode image") + } + + maxWidth := 1920 + if img.Bounds().Dx() > maxWidth { + img = imaging.Resize(img, maxWidth, 0, imaging.Lanczos) + } + + buf := new(bytes.Buffer) + if err := webp.Encode(buf, img, &webp.Options{Quality: 80}); err != nil { + return nil, errors.Wrap(err, "webp encode") + } + return buf.Bytes(), nil +} + +// convertGifToAnimatedWebP 将 GIF 文件数据转换为动画 WebP(需要系统安装 cwebp 工具) +// 如果 cwebp 不可用,返回错误,调用方应回退到原始 GIF +func convertGifToAnimatedWebP(gifData []byte) ([]byte, error) { + _, err := exec.LookPath("cwebp") + if err != nil { + return nil, errors.New("cwebp not found in PATH") + } + + tmpGif, err := os.CreateTemp("", "input_*.gif") + if err != nil { + return nil, errors.Wrap(err, "create temp gif") + } + defer os.Remove(tmpGif.Name()) + if _, err := tmpGif.Write(gifData); err != nil { + return nil, errors.Wrap(err, "write temp gif") + } + tmpGif.Close() + + tmpWebp, err := os.CreateTemp("", "output_*.webp") + if err != nil { + return nil, errors.Wrap(err, "create temp webp") + } + defer os.Remove(tmpWebp.Name()) + tmpWebp.Close() + + cmd := exec.Command("cwebp", "-q", "80", "-mixed", "-mt", "-quiet", tmpGif.Name(), "-o", tmpWebp.Name()) + if err := cmd.Run(); err != nil { + return nil, errors.Wrap(err, "run cwebp") + } + + webpData, err := os.ReadFile(tmpWebp.Name()) + if err != nil { + return nil, errors.Wrap(err, "read webp output") + } + return webpData, nil +} + +// mimeTypeFromFormat 根据图片格式返回 MIME 类型 +func mimeTypeFromFormat(format string) string { + switch format { + case "jpeg": + return "image/jpeg" + case "png": + return "image/png" + case "gif": + return "image/gif" + case "webp": + return "image/webp" + default: + return "application/octet-stream" + } +} + +// isSupportedImage 通过文件头魔数判断是否为支持的图片格式 +func isSupportedImage(data []byte) bool { + if len(data) < 4 { + return false + } + + // JPEG: FF D8 FF + if data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF { + return true + } + + // PNG: 89 50 4E 47 + if bytes.Equal(data[:4], []byte{0x89, 0x50, 0x4E, 0x47}) { + return true + } + + // GIF: GIF87a / GIF89a + if len(data) >= 6 { + if bytes.Equal(data[:6], []byte("GIF87a")) || bytes.Equal(data[:6], []byte("GIF89a")) { + return true + } + } + + // WebP: RIFF ... WEBP + if len(data) >= 12 && bytes.Equal(data[:4], []byte("RIFF")) && bytes.Equal(data[8:12], []byte("WEBP")) { + return true + } + + return false +} diff --git a/.workbuddy/backup/p0-avatar-qrcode-20260905/templates/base/header.html b/.workbuddy/backup/p0-avatar-qrcode-20260905/templates/base/header.html new file mode 100644 index 0000000..09c4e2e --- /dev/null +++ b/.workbuddy/backup/p0-avatar-qrcode-20260905/templates/base/header.html @@ -0,0 +1,977 @@ + + + + + + {{ .Title }} + + + + + + + + + + + + + + + + + + {{ if .VConsole }} + + + {{ end }} + + + + + + {{ if eq .CaptchaType "math" }} + + + {{ else if .RecaptchaTurnstileStyle }} + + + {{ else }} + + + {{ end }} + {{ if ne SentryDSN "" }} + + + {{ end }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ if .NoIndex }}{{ end }} + {{ if .IsUserPage }} + + + + + + + + + {{ else }} + + + + + + + {{ end }} + + + + + + + + + + + +
+ {{ if not .IsUserPage }} + + {{ end }} + +
+ + + +{{ if .IsPixel }} +
+{{ else }} +
+{{ end }} + +{{ if eq .CaptchaType "math" }} +
+
+

{{t .LangCode "captcha.title"}}

+

{{t .LangCode "captcha.hint"}}

+
+ {{t .LangCode "captcha.loading"}} + +
+
+ +
+ +
+ + +
+
+
+ + + + +{{ end }} \ No newline at end of file diff --git a/.workbuddy/backup/p0-avatar-qrcode-20260905/templates/question/list.html b/.workbuddy/backup/p0-avatar-qrcode-20260905/templates/question/list.html new file mode 100644 index 0000000..35f243c --- /dev/null +++ b/.workbuddy/backup/p0-avatar-qrcode-20260905/templates/question/list.html @@ -0,0 +1,309 @@ +{{template "base/header" .}} + + + + + +
+
+
+ +

{{ .PageUser.Name }}

+

{{ .PageUser.Intro }}

+
+ {{ if and .PageUser.Acfunlink (ne .PageUser.Acfunlink "none") }} + + acfunlink + + {{ end }} + {{ if and .PageUser.Bililink (ne .PageUser.Bililink "none") }} + + bililink + + {{ end }} + {{ if and .PageUser.Weibolink (ne .PageUser.Weibolink "none") }} + + weibolink + + {{ end }} + {{ if and .PageUser.Xiaohongshulink (ne .PageUser.Xiaohongshulink "none") }} + + xiaohongshulink + + {{ end }} + {{ if and .PageUser.Bloglink (ne .PageUser.Bloglink "none") }} + + bloglink + + {{ end }} +
+
+
+
+ +
+
+ {{ if .CanAsk }} + + + +
+ + +

{{t .LangCode "ask.scan" .PageUser.Name}}
{{ if .RegisterOnlyAsk }}{{t .LangCode "ask.registerOnly"}}{{ else }}{{t .LangCode "ask.anonymous"}}{{ end }}

+ + + + + + + {{ end }} + {{template "base/alert" .}} + + +
+ + {{ if .VoteEnabled }} + + {{ else }} + + + {{ end }} +
+ +
+ {{template "question/new-question-template" .}} + + {{ if ne (len .PageQuestions) 0 }} +

{{t .LangCode "ask.history" .PageUser.Name}} ({{ .AnsweredCount }})

+ {{ template "question/history-template" . }} + {{ end }} +
+ + +
+
+ +{{template "base/footer" .}} \ No newline at end of file diff --git a/.workbuddy/backup/p0-avatar-qrcode-20260905/templates/question/question-not-found.html b/.workbuddy/backup/p0-avatar-qrcode-20260905/templates/question/question-not-found.html new file mode 100644 index 0000000..eeb6bde --- /dev/null +++ b/.workbuddy/backup/p0-avatar-qrcode-20260905/templates/question/question-not-found.html @@ -0,0 +1,105 @@ +{{template "base/header" .}} + + + + + +
+
+
+ +

{{ .PageUser.Name }}

+

{{ .PageUser.Intro }}

+
+ {{ if and .PageUser.Acfunlink (ne .PageUser.Acfunlink "none") }} + + acfunlink + + {{ end }} + {{ if and .PageUser.Bililink (ne .PageUser.Bililink "none") }} + + bililink + + {{ end }} + {{ if and .PageUser.Weibolink (ne .PageUser.Weibolink "none") }} + + weibolink + + {{ end }} + {{ if and .PageUser.Xiaohongshulink (ne .PageUser.Xiaohongshulink "none") }} + + xiaohongshulink + + {{ end }} + {{ if and .PageUser.Bloglink (ne .PageUser.Bloglink "none") }} + + bloglink + + {{ end }} +
+
+
+
+ +
+
+ {{ if .CanAsk }} + + + +
+ + +

扫码向@{{ .PageUser.Name }}提问
{{ if .RegisterOnlyAsk }}{{t .LangCode "ask.registerOnly"}}{{ else }}{{t .LangCode "ask.anonymous"}}{{ end }}

+ {{ end }} + {{template "base/alert" .}} + {{template "question/new-question-template" .}} + +
+ {{ if ne (len .PageQuestions) 0 }} +

@{{ .PageUser.Name }} 以前回答过的问题 ({{ .AnsweredCount }})

+ {{ template "question/history-template" . }} + {{ end }} +
+
+{{template "base/footer" .}} \ No newline at end of file diff --git a/.workbuddy/backup/p0-avatar-qrcode-20260905/templates/user/profile.html b/.workbuddy/backup/p0-avatar-qrcode-20260905/templates/user/profile.html new file mode 100644 index 0000000..5e337d6 --- /dev/null +++ b/.workbuddy/backup/p0-avatar-qrcode-20260905/templates/user/profile.html @@ -0,0 +1,1527 @@ +{{template "base/header" .}} + + + + + + + + + + + + + + + + +{{template "base/alert" .}} + + +
+
+ +
+

{{t .LangCode "profile.cropTitle"}}

+
+
+
+ +
+

+
+ +
+
+ + +
+ + + + +
+ + +
+
+ {{ .CSRFTokenHTML }} + + +
+

{{t .LangCode "profile.card.basic"}}

+
+ + +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ +
+ +
+
+ + + {{t .LangCode "admin.langHint"}} +
+
+ + +
+
+ +
+
+ + +
+

{{t .LangCode "profile.card.avatarBg"}}

+
+
+ + + + 1:1 +
+ + +
+ {{t .LangCode "profile.avatarCropHint"}} +
+
+ + + + 16:9 +
+ + +
+ {{t .LangCode "profile.backgroundCropHint"}} +
+
+ +
+ +
+
+ + +
+
    +
  • + {{t .LangCode "profile.card.qr"}} +
    +
    +
    +
    + +
    + +
    +
    + +
    + +
    +
    + +
    + +
    +
    + + {{t .LangCode "admin.customFgHint"}} +
    +
    + +
    + + {{.LoggedUser.Dotscale}} + +
    +
    +
    + +
    + + {{.LoggedUser.Backgroundimagealpha}} + +
    +
    +
    + +
    + + +
    + {{t .LangCode "admin.qrPreviewHint"}} +
    +
    +
    +
    +

    {{t .LangCode "admin.preview"}}

    +
    +
    +
    + {{t .LangCode "admin.themePreset"}} +
    + + + + +
    +
    +
    + +
    +

    {{t .LangCode "admin.previewHint"}}

    +
    +
    +
    +
    +
  • +
+
+ +
+
+ + +
+
    +
  • + {{t .LangCode "admin.social"}} +
    + {{t .LangCode "admin.socialHint"}} + + + + + +
    +
  • +
+
+ +
+
+ + +
+

{{t .LangCode "profile.card.password"}}

+
+ + +
+
+ + +
+

{{t .LangCode "profile.pwdNote"}}

+
+
+ +
+
+
+ + +
+

{{t .LangCode "admin.tags"}}

+
+ {{ .CSRFTokenHTML }} +
+ + +
+
+ +
+
+ +
+
+
+ + +
+

{{t .LangCode "admin.pagePassword"}}

+
+ {{ .CSRFTokenHTML }} +
+ + +
+
+ +
+
+
+ + +
+

{{t .LangCode "admin.account"}}

+ +
+
+ + + +
+ {{ with .FeatureQuestion }}{{template "user/feature-control-form" .}}{{ end }} + +
+
+
+ {{ $.CSRFTokenHTML }} + {{t $.LangCode "admin.harassment"}} +
+
+ +
+
+ +
+
+
+ + + {{ template "user/global-blockwords" (dict "Root" $ "Action" "/user/blockwords/update?tab=question") }} + +
+
+
+ {{ $.CSRFTokenHTML }} + {{t $.LangCode "admin.askPassword"}} +
+ + +
+
+ +
+
+
+
+ + +
+ +
+ {{ with .FeatureChat }}{{template "user/feature-control-form" .}}{{ end }} + +
+
+ {{ $.CSRFTokenHTML }} + {{t $.LangCode "admin.chat"}} +
+ + {{ with $.ChatLimits }} + + {{ end }} +
+
+ + {{ with $.ChatLimits }} + + {{ end }} + {{t $.LangCode "admin.dailyLimitHint"}} +
+
+ +
+
+
+ +
+ + {{ template "user/global-blockwords" (dict "Root" $ "Action" "/user/blockwords/update?tab=chat") }} +
+ + +
+ {{ with .FeatureVote }}{{template "user/feature-control-form" .}}{{ end }} + +
+ + {{ template "user/global-blockwords" (dict "Root" $ "Action" "/user/blockwords/update?tab=vote") }} +
+ + + + + + + + + +{{template "base/footer" .}} diff --git a/.workbuddy/backup/remove-vercount-20260906/dict_en.go b/.workbuddy/backup/remove-vercount-20260906/dict_en.go new file mode 100644 index 0000000..c3c7f1c --- /dev/null +++ b/.workbuddy/backup/remove-vercount-20260906/dict_en.go @@ -0,0 +1,842 @@ +// Copyright 2022 E99p1ant. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package i18n + +// enDict English copy. +var enDict = map[string]string{ + // Navigation + "nav.explore": "Explore", + "nav.mine": "Mine", + "nav.register": "Sign up", + "nav.login": "Log in", + "nav.ask": "Ask", + "nav.settings": "Settings", + "nav.lang": "Language", + + // Common + "common.login": "Log in", + "common.forgot": "Forgot password", + "common.submit": "Submit", + "common.cancel": "Cancel", + "common.save": "Save", + "common.close": "Close", + "common.delete": "Delete", + "common.moveUp": "Move up", + "common.moveDown": "Move down", + "common.share": "Share", + "common.verifying": "Verifying...", + "common.submitting": "Submitting...", + "common.uploading": "Uploading...", + "common.lang": "Language", + "common.menu": "Menu", + + "common.pleaseWait": "Please wait...", + "common.backHome": "Back to Home", + "common.theme": "Toggle dark/light mode", + + // Site + "site.title": "Wolf's Question Box | Anonymous Question Box", + "site.description": "Wolf's Question Box is an anonymous question box — ask me anything without signing up, a safe space to share and respond.", + "site.keywords": "Anonymous Question Box,question box,anonymous ask,ask anonymously,Wolf's Question Box,anonymous Q&A", + "site.footer.visitor": "You are visitor #%s", + "site.footer.pageview": "This page has been viewed %s times", + "site.footer.runtime": "The box has been alive for", + "site.footer.admin": "Contact admin", + "site.footer.notice": "Announcements", + "site.footer.chat": "Chat room", + "site.footer.drive": "Drive", + "site.footer.status": "Status", + "site.footer.nav": "Links", + "site.maintenance": "The site is under maintenance. Please come back later ~", + + // Auth + "auth.login.title": "User Log in", + "auth.login.email": "Email", + "auth.login.password": "Password", + "auth.login.submit": "Log in", + "auth.login.forgot": "Forgot password", + "auth.register.title": "User Sign up", + "auth.register.name": "Nickname", + "auth.register.email": "Email", + "auth.register.password": "Password", + "auth.register.submit": "Sign up", + "auth.forgot.title": "Forgot password", + "auth.forgot.email": "Email", + "auth.forgot.submit": "Send reset email", + "auth.forgot.submitBtn": "Recover password", + "auth.forgot.sent": "An email has been sent to %s, please check your inbox.", + "auth.recover.title": "Reset password", + "auth.recover.greeting": "%s, you are resetting your password:", + "auth.recover.password": "New password", + "auth.recover.confirm": "Confirm password", + "auth.recover.submit": "Reset password", + + // Explore + "explore.title": "Explore User Tags", + "explore.notice": "Here you can find users who enabled \"Show my tags on the homepage\". This feature is off by default. To get discovered here, go to your profile settings and enable it.", + "explore.shuffle": "Shuffle", + "explore.empty": "No users to show yet. Enable it and be the first!", + "explore.hotVotes": "Hot polls", + "explore.questionsTab": "Questions", + "explore.votesTab": "Polls", + "explore.votesTitle": "Public polls", + "explore.votesDesc": "Browse active public polls and join the topics that interest you.", + "explore.randomVote": "Random pick", + "explore.allVotes": "All polls", + "explore.noVotes": "There are no public polls available right now.", + + // Ask page + "ask.scan": "Scan to ask @%s", + "ask.anonymous": "Anyone can ask anonymously", + "ask.registerOnly": "Only registered users can ask", + "ask.nextOpenAt": "Next opening: %s", + "ask.lastCloseAt": "Last closed: %s", + "weekday.0": "Sun", + "weekday.1": "Mon", + "weekday.2": "Tue", + "weekday.3": "Wed", + "weekday.4": "Thu", + "weekday.5": "Fri", + "weekday.6": "Sat", + "ask.pinned": "Pinned", + "ask.share": "Share this box", + "ask.placeholder": "Write your question here...", + "ask.chatPlaceholder": "Write your chat message here...", + "ask.addImage": "Add image", + "ask.private": "Don't publish after reply", + "ask.notify": "Notify me", + "ask.emailPlaceholder": "Enter your email address", + "ask.submit": "Send question", + "ask.chat": "Start a chat", + "ask.chatDisabled": "Chat is disabled on this box", + "ask.chatLimit": "Message limit: %d", + "ask.history": "Previously answered by @%s", + "ask.loginRequired": "Log in required", + "ask.loginRequiredChat": "You need to log in to start a chat", + "ask.goLogin": "Go to login", + "ask.needPassword": "This box requires a password", + "ask.needLogin": "The owner only allows registered users to ask questions. Please log in first.", + "ask.pleaseEmail": "Please enter your email to receive notifications", + "ask.generating": "Generating share card…", + "ask.genFailed": "Failed to generate share card. Please try again later", + "ask.networkError": "Network error", + "ask.shareFilename": "share.png", + + // Question item + "qitem.answer": "Answer", + "qitem.delete": "Delete", + "qitem.private": "Make private", + "qitem.public": "Make public", + "qitem.asked": "Asked", + "qitem.pending": "Pending reply", + "qitem.answeredBy": "Answer by @%s on %s", + "qitem.danger": "Danger!", + "qitem.deleteConfirm": "Are you sure you want to delete this question? This cannot be undone.", + "qitem.deleteConfirmBtn": "Yes, delete", + "qitem.backToBox": "Back to %s's question box", + "qitem.answerTitle": "Answer the question", + "qitem.answerPlaceholder": "Write your answer here...", + "qitem.emailNotifyHint": "The asker left their email. Once you answer this question for the first time, they will receive an email notification.", + "qitem.submitAnswer": "Answer", + "qitem.updateAnswer": "Update answer", + "qitem.askMore": "Ask something else?", + "qitem.markRead": "Mark as read", + "qitem.markUnread": "Mark as unread", + "qitem.readLocked": "Answered, locked as read", + + // Admin / profile page + "admin.profile": "Profile", + "admin.lang": "Language", + "admin.langHint": "Choose the language visitors see when they open your question box (admin pages follow this too)", + "admin.email": "Email", + "admin.name": "Nickname", + "admin.nameColor": "Nickname color", + "admin.oldPassword": "Old password", + "admin.newPassword": "New password", + "admin.notify": "New question notifications", + "admin.personalize": "Customization", + "admin.intro": "Box introduction", + "admin.introColor": "Introduction color", + "admin.qrBgColor": "QR background color (light part)", + "admin.qrPdColor": "QR finder pattern color (corner squares)", + "admin.qrFgColor": "QR foreground color", + "admin.qrDotSize": "QR dot size (←small large→)", + "admin.qrBgAlpha": "QR background opacity (←opaque transparent→)", + "admin.avatar": "Avatar", + "admin.background": "Box background", + "admin.qrBackground": "QR background image", + "admin.social": "Social links", + "admin.harassment": "Anti-harassment", + "admin.askLimit": "Asking restriction", + "admin.blockWords": "Blocked words (press Enter or comma to add, up to 10 words, 10 characters each)", + "admin.chat": "Chat settings", + "admin.chatMaxMessages": "Max messages per chat", + "admin.chatDailyLimit": "Daily message limit per user", + "admin.tags": "Tags", + "admin.tagsHint": "Tags (up to 5, 5 characters each, press Enter or comma to add)", + "admin.tagsPlaceholder": "e.g. anime, music, games", + "admin.tagInputHint": "Type and press Enter or comma to add", + "admin.tagInputDuplicate": "Already exists, ignored", + "admin.pagePassword": "Page access password", + "admin.pagePasswordHint": "Set a page password (leave empty to disable)", + "admin.askPassword": "Ask password", + "admin.askPasswordHint": "Set a password for asking (leave empty to disable)", + "admin.saveProfile": "Save changes", + "admin.updated": "Profile updated successfully", + "admin.logout": "Log out", + "admin.export": "Export data", + + // Admin settings (additional) + "admin.oldPasswordHint": "Only needed when changing password; leave empty to keep it unchanged", + "admin.newPasswordHint": "6-64 characters; leave empty to keep it unchanged", + "admin.notifyEmail": "Email", + "admin.useCustomFg": "Use custom foreground color (otherwise auto-matched)", + "admin.customFgHint": "Only takes effect when enabled; otherwise the color is auto-generated", + "admin.avatarPlaceholder": "Upload avatar (non-1:1 images will be squished)", + "admin.backgroundPlaceholder": "Upload box background", + "admin.qrBackgroundPlaceholder": "Upload QR background (a background that is too dark may make the QR code unreadable)", + "admin.qrPreviewHint": "The preview updates instantly after choosing an image", + "admin.preview": "Live preview", + "admin.themePreset": "Preset themes", + "admin.themeDefault": "Default", + "admin.themeDark": "Dark", + "admin.themeOcean": "Ocean", + "admin.themeSunset": "Sunset", + "admin.resetDefaults": "🔄 Reset to defaults", + "admin.previewHint": "Adjustments are for preview only; click \"Save changes\" to apply", + "admin.socialHint": "Check the box and fill in the profile URL (no https:// needed). Unchecked items won't be shown on your page.", + "admin.enableAcfun": "Enable AcFun", + "admin.enableBili": "Enable Bilibili", + "admin.enableWeibo": "Enable Weibo", + "admin.enableXiaohongshu": "Enable Xiaohongshu", + "admin.enableBlog": "Enable Personal Blog", + "admin.registerOnly": "Only allow registered users to ask me questions", + "admin.updateHarassment": "Update anti-harassment settings", + "admin.globalBlockWords": "Global blocked words", + "admin.globalBlockWordsHint": "Blocked words apply to questions, chat messages sent to you, and vote submissions. This single setting is shared across the question / chat / vote panels and takes effect immediately from any of them.", + "admin.updateBlockWords": "Save blocked words", + "admin.allowChat": "Allow registered users to start a chat with me", + "admin.dailyLimitHint": "0 means unlimited", + "admin.updateChat": "Update chat settings", + "admin.showInHome": "Show my tags on the home page (let other users find me by tags)", + "admin.updateTags": "Update tags", + "admin.updatePagePassword": "Update page password", + "admin.pagePasswordPlaceholder": "Enter page access password", + "admin.updateAskPassword": "Update ask password", + "admin.askPasswordPlaceholder": "Enter ask password", + "admin.account": "Account settings", + "admin.exportHint": "You can export all your personal data from %s, including your basic information, received questions and answers.", + "admin.exportJSON": "Export all data (JSON)", + "admin.exportJSONHint": "Includes questions, answers, follow-ups and chats for full backup or migration.", + "admin.deactivate": "Deactivate my account", + "admin.deactivateHint": "You can deactivate your account at any time. After deactivation, your account can no longer log in, your question box page and questions will be inaccessible, and others won't be able to send you new questions.This action cannot be undone. Please be careful!", + "admin.socialRequired": "Please fill in the link or uncheck the box", + + // Question management + "qlist.title": "My Questions", + "qlist.answered": "Answered", + "qlist.pending": "Pending", + "qlist.private": "Private", + "qlist.empty": "No questions yet. Share your box and get some!", + "qlist.action": "Actions", + "qlist.answerNow": "Answer", + "qlist.sort": "Sort", + "qlist.status": "Status", + "qlist.type": "Type", + "qlist.latest": "Latest", + "qlist.oldest": "Oldest", + "qlist.all": "All", + "qlist.public": "Normal question", + "qlist.privateQuestion": "Private question", + "qlist.received": "Questions I received", + "qlist.myChats": "Chats I started", + "qlist.statsTotal": "Total questions", + "qlist.statsAnswered": "Answered", + "qlist.statsUnanswered": "Unanswered", + "qlist.statsPrivate": "Private", + "qlist.statsPinned": "Pinned", + "qlist.statsChats": "Chats", + "qlist.searchPlaceholder": "Search questions or answers...", + "qlist.searchBtn": "Search", + "qlist.searchClear": "Clear", + "qlist.questionsTab": "Questions", + "qlist.votesTab": "Votes", + "qlist.chatsTab": "Chats", + "qlist.receivedChats": "Chats I received", + "qlist.chatEmpty": "No chats yet. Share your question box to start chatting!", + "qlist.pinnedShort": "Pin", + "qlist.privateShort": "Pvt", + "qlist.chatStatsTotal": "Total chats", + "qlist.chatStatsReceived": "Received", + "qlist.chatStatsInitiated": "Initiated", + "qlist.chatStatsClosed": "Closed", + "qlist.unansweredShort": "Open", + "qlist.readShort": "Read", + "qlist.unreadShort": "Unread", + "qitem.markReadShort": "Mark", + "qlist.pinShort": "Pin", + "qlist.unpinShort": "Unpin", + "qlist.myVotes": "My votes", + "qlist.loadMore": "Load more", + "qlist.newVote": "New vote", + "qlist.edit": "Edit", + "qlist.untitledVote": "Untitled vote", + "qlist.blocked": "Blocked", + "qlist.unanswered": "Unanswered", + + // Site logo suffix (random animal emoji + suffix) + "site.logoSuffix": "'s Question Box", + "site.stats": "Question Box Statistics", + + // Captcha modal + "captcha.title": "Verification", + "captcha.hint": "Solve the math problem below to prove you are not a robot", + "captcha.refresh": "New problem", + "captcha.submit": "Verify & submit", + "captcha.loading": "Loading...", + "captcha.loadFailed": "Load failed", + "captcha.loadRetry": "Load failed, please retry", + "captcha.inputAnswer": "Please enter the answer", + "captcha.wrongAnswer": "Wrong answer, please recalculate", + "captcha.verifyFailed": "Verification failed, please retry", + + // Register page extras + "auth.register.domain.prefix": "Custom domain (your box URL will be: %s + ", + "auth.register.domain.suffix": " + what you fill in below)", + "auth.register.confirm": "Confirm password", + "auth.register.language": "Language", + "auth.register.languageDefault": "Use site default", + "auth.register.success": "Registration successful, welcome to TamaBox!", + + // Explore page extras + "explore.recommend": "Random picks", + "explore.search": "Search tags...", + "explore.sortLatest": "Newest", + "explore.sortOldest": "Oldest", + "explore.noTags": "No tags", + "explore.noRecommend": "No recommendations yet", + "explore.hotTags": "Hot Tags", + "explore.hotQuestions": "Hot Questions", + "explore.noResult": "No users match your criteria.", + "explore.lazy": "This user is too lazy to write anything~", + "explore.prevPage": "Previous", + "explore.nextPage": "Next", + "explore.loading": "Loading...", + "explore.loadFailed": "Load failed", + + // Question box password page + "password.title": "This question box is password protected", + "password.desc": "This question box is password protected. Enter the password to view its contents.", + "password.input": "Enter the access password", + "password.enter": "Enter", + "password.submit": "Confirm", + + // Question not found + "qitem.notFound": "Question not found or has been deleted", + + // Chat page + "chat.title": "Chat", + "chat.input": "Type a message...", + "chat.send": "Send", + "chat.delete": "Delete conversation", + "chat.block": "Block user", + "chat.unblock": "Unblock user", + "chat.empty": "No messages yet. Say something~", + "chat.withAsker": "Chat with the asker", + "chat.withUser": "Chat with %s", + "chat.closed": "Conversation closed", + "chat.closedNoSend": "The conversation is closed. You cannot send new messages.", + "chat.remaining": "Messages left: %d (of %d total)", + "chat.me": "Me", + "chat.other": "Other", + "chat.userBlocked": "This user has been blocked", + "chat.addImage": "Add image (optional, max 5MB)", + "chat.confirm": "Confirm", + "chat.confirmBlock": "Block this user? They won't be able to send you messages anymore.", + "chat.confirmUnblock": "Unblock this user? They will be able to send you messages again.", + "chat.deleteConfirm": "Delete this conversation? The other party will no longer be able to see it, and this cannot be undone.", + "maintenance.title": "NekoBox is under maintenance...", + "maintenance.desc": "NekoBox is currently under maintenance and temporarily unavailable. Please come back later~", + + // Single-question share (v4) + "qitem.shareCard": "Share this question", + + // Follow-ups (v4) + "followup.title": "Follow-ups", + "followup.content": "Follow-up: ", + "followup.answerPlaceholder": "Answer this follow-up...", + "followup.answerBtn": "Answer", + "followup.waiting": "Waiting for answer...", + "followup.askMore": "Add a follow-up", + "followup.remaining": "%d / %d follow-ups remaining", + "followup.contentPlaceholder": "Add more details...", + "followup.waitingReply": "Your follow-up has been sent. Waiting for the box owner to reply...", + "followup.submit": "Submit", + + // Pinning (v4) + "qlist.pinned": "Pinned", + "qlist.pin": "Pin", + "qlist.unpin": "Unpin", + "qlist.readStatus": "Read status", + "qlist.read": "Read", + "qlist.unread": "Unread", + "qlist.statsRead": "Read", + "qlist.statsUnread": "Unread", + + // Question submitted page (v4) + "success.title": "Question sent successfully!", + "success.yourQuestion": "Your question:", + "success.emailSent": "A private link to this question was sent to your email. You can also use the link below to view the question, receive replies, and follow up.", + "success.saveLink": "Please save the private link below! Since you didn't leave an email, this link (including the token) is your only credential to view the question, receive replies, and follow up. If you lose it, it cannot be recovered.", + "success.privateLink": "Your private link (click to copy)", + "success.copy": "Copy", + "success.copied": "Copied ✓", + "success.copyFailed": "Copy failed", + "success.token": "Token: %s (the asker uses this token to access the private link)", + "success.viewQuestion": "View my question", + "success.backToBox": "Back to %s's box", + "success.pageTitle": "Question Sent - %s", + + // Deactivate page + "deactivate.title": "Deactivate account", + "deactivate.desc": "After deactivation your question box will be inaccessible and all data will be deleted. This cannot be undone!", + "deactivate.confirm": "I confirm I want to deactivate my account", + "deactivate.submit": "Deactivate account", + + // Anonymous voting + "nav.votes": "Polls", + + "common.back": "Back", + + "vote.listTitle": "Poll Management", + "vote.newTitle": "New Poll", + "vote.editTitle": "Edit Poll", + "vote.resultsTitle": "Poll Results", + "vote.statsTotal": "Total Polls", + "vote.statsPublished": "Active", + "vote.createNew": "New Poll", + "vote.typeSingle": "Single choice", + "vote.typeMultiple": "Multiple choice", + "vote.typeRating": "Rating", + "vote.typeRank": "Ranking", + "vote.typeForm": "Form", + "vote.statusNotStarted": "Not started", + "vote.statusDraft": "Draft", + "vote.statusPublished": "Active", + "vote.statusEnded": "Ended", + "vote.statusArchived": "Archived", + "vote.participants": "Participants", + "vote.remainLabel": "Remaining", + "vote.perpetual": "No deadline", + "vote.durationDay": "d", + "vote.durationHour": "h", + "vote.durationMinute": "m", + "vote.createdAt": "Created", + "vote.edit": "Edit", + "vote.results": "Results", + "vote.preview": "Preview", + "vote.page.mustComplete": "Please complete", + "vote.page.rankHintRequired": "Rank 1-N (required)", + "vote.page.rankHintOptional": "Rank 1-N (optional, partial OK)", + "vote.page.previewHint": "Preview mode: this is a sample of the live page; submitting is disabled", + "vote.page.previewSubmitHint": "Preview mode; submitting is enabled after publish", + "vote.publish": "Publish", + "vote.close": "End", + "vote.share": "Copy link", + "vote.copied": "Copied", + "vote.shareURL": "Poll share link", + "vote.confirmPublish": "Publish this poll? A shareable link will be generated.", + "vote.confirmClose": "End this poll? Participants will no longer be able to vote.", + "vote.confirmDelete": "Delete this poll? All vote data will be removed and cannot be recovered!", + "vote.manageEntry": "Manage polls", + + "vote.manageTitle": "Manage polls", + "vote.manageDesc": "Create and manage your anonymous polls", "vote.empty": "No polls yet. Click \"New Poll\" in the top right to get started.", + "vote.formLockedHint": "This poll is live. Only basic info can be edited; type and options are locked to protect collected votes.", + "vote.formBasic": "Basic Settings", + "vote.formTitle": "Poll Title", + "vote.formTitlePlaceholder": "e.g. What should we eat today?", + "vote.formDescription": "Description", + "vote.formDescriptionPlaceholder": "Optional details", + "vote.formType": "Poll Type", + "vote.formVisibility": "Visibility", + "vote.formVisibilityPublic": "Public (Explore + question box)", + "vote.formVisibilityLinkOnly": "Link only", + "vote.formVoterScope": "Voting Scope", + "vote.formVoterScopeAnyone": "Anyone (including anonymous)", + "vote.formVoterScopeRegistered": "Registered users only", + "vote.formShowResult": "Result Visibility", + "vote.formShowResultAfterVote": "Visible right after voting", + "vote.formShowResultAfterEnd": "Visible after the poll ends", + "vote.formShowResultOwnerOnly": "Owner only", + "vote.formStartAt": "Start time (empty = now)", + "vote.formEndAt": "End time (empty = no limit)", + "vote.formMaxChoices": "Max choices", + "vote.formMaxVotesPerFingerprint": "One vote per device per poll; repeated submissions from the same browser are blocked.", + "vote.formMaxVotesPerAccount": "One vote per registered account per poll; repeated submissions from the same account are blocked.", + "vote.formAllowCustom": "Allow participants to add custom options", + "vote.formRequireCaptcha": "Require captcha for anonymous voting", + "vote.formOptions": "Poll Options", + "vote.formRatingHint": "Rating polls need no options (1-10).", + "vote.addOption": "Add option", + "vote.optionPlaceholder": "Option text", + "vote.formFields": "Form Fields", + "vote.addField": "Add field", + "vote.field": "Field", + "vote.formFieldType": "Field Type", + "vote.fieldTypeText": "Single-line text", + "vote.fieldTypeTextarea": "Multi-line text", + "vote.fieldTypeSelect": "Dropdown select", + "vote.fieldTypeRadio": "Single choice", + "vote.fieldTypeCheckbox": "Multiple choice", + "vote.fieldTypeNumber": "Number", + "vote.fieldTypeRank": "Ranking", + "vote.page.selectPlaceholder": "Select...", + "vote.page.avgNumber": "Average", + "vote.page.fieldFilled": "Filled", + "vote.fieldTypeRating": "Rating", + "vote.formFieldLabel": "Field Label", + "vote.formFieldRequired": "Required", + "vote.formFieldMaxLength": "Max length (0 = unlimited)", + "vote.formFieldOptions": "Options", + "vote.formFieldOptionsHint": "Click \"Add option\" to add each option; no comma needed", + "vote.fieldAllowCustom": "Allow participants to add a custom option", + "vote.formFieldOptionsPlaceholder": "e.g. Great,Okay,Poor", + "vote.createDraft": "Save draft", + "vote.builderHintOptions": "At least 2 options are required", + "vote.builderHintFields": "Add at least 1 field", + "vote.builderHintFieldOptions": "Choice/rank fields need options (rank needs at least 2)", + "vote.resultsAnswered": "Answered", + "vote.page.rankedTimes": "Ranked", + "vote.resultsTotal": "Total participants", + "vote.resultsSubmissions": "Form responses", + "vote.resultsViewTexts": "View text responses", + "vote.resultsToggleHint": "Click to expand or collapse text responses", + "vote.resultsOptions": "Poll results", + "vote.resultsVotes": "votes", + "vote.export": "Export CSV", + "vote.resultsEmpty": "No participation yet. Share the link!", + + // Poll participation page + "vote.inlineTitle": "Polls", + "vote.page.by": "poll", + "vote.page.goVote": "Vote", + "vote.page.notStarted": "Not started", + "vote.page.notStartedHint": "This poll has not started yet.", + "vote.page.ended": "Ended", + "vote.page.endedHint": "This poll has ended.", + "vote.page.active": "Active", + "vote.page.participated": "Participated", + "vote.page.loginRequired": "This poll is limited to registered users. Please log in first.", + "vote.page.anonymousHint": "Anonymous participation is allowed and your identity is not recorded (max 1 per device).", + "vote.page.submit": "Submit vote", + "vote.page.alreadyVoted": "You have already participated in this poll. Thanks!", + "vote.page.resultsTitle": "Poll Results", + "vote.page.totalVotes": "Total", + "vote.page.avgRating": "Average", + "vote.page.avgRank": "Avg rank", + "vote.page.optionColumn": "Option", + "vote.page.ratingLabel": "Rate", + "vote.page.ratingUnit": "pts", + "vote.page.rankHint": "Rank each option 1-N (smaller = higher, no duplicates)", + "vote.page.rankPlaceholder": "Rank", + "vote.page.multipleHint": "Multiple choice", + "vote.page.customOption": "Custom option (optional)", + "vote.page.customPlaceholder": "Type your own option", + "vote.page.customOther": "Other", + "vote.page.customFieldPlaceholder": "Type a custom option...", + "vote.page.customStats": "Custom options", + "vote.page.formCount": "Responses received", + "vote.page.ownerManage": "Manage this poll (results visible only to you)", + + // Per-poll quota collection + "vote.formQuotaLimit": "Collection quota (responses)", + "vote.formQuotaHint": "0 means unlimited; the poll ends automatically once the quota is reached.", + "vote.flash.notFound": "This poll does not exist or is no longer available", + "vote.flash.notPublished": "This poll has not been published yet", + "vote.flash.archived": "This poll has been archived and is closed", + "vote.flash.ended": "This poll has ended", + "vote.flash.notStarted": "This poll has not started yet", + "vote.flash.success": "Vote submitted. Thanks for participating!", + "vote.err.noChoice": "Please select at least one option", + "vote.err.tooManyChoices": "You can select at most %d options", + "vote.err.pickOne": "Please select an option", + "vote.err.invalidOption": "Invalid option", + "vote.err.optionDuplicate": "Options cannot be duplicated", + "vote.err.ratingRequired": "Please provide a rating from 1 to 10", + "vote.err.rankRequired": "Please rank every option from 1 to %d", + "vote.err.rankDuplicate": "Rankings must not repeat", + "vote.err.fieldRequired": "Please fill in \"%s\"", + "vote.err.fieldCustomRequired": "Please fill in the custom option for \"%s\"", + "vote.err.fieldCustomTooLong": "The custom option for \"%s\" must be at most %d characters", + "vote.err.fieldInvalidOption": "Invalid option for \"%s\"", + "vote.err.fieldConfigError": "Misconfigured options for \"%s\"", + "vote.err.fieldRankIncomplete": "Please provide a complete ranking for \"%s\"", + "vote.err.fieldRankRange": "The ranking for \"%s\" must be a number between 1 and %d", + "vote.err.fieldRankDuplicate": "Rankings for \"%s\" must not repeat", + "vote.err.fieldTooLong": "\"%s\" exceeds the length limit (max %d characters)", + "vote.err.fieldNotNumber": "\"%s\" must be a number", + "vote.err.fieldRating": "\"%s\" requires a rating from 1 to 10", + "vote.err.unknownType": "Unknown poll type", + "vote.err.customTooLong": "The custom option must be at most %d characters", + "vote.err.blockWords": "Your submission contains a blocked word set by the poll creator and could not be submitted", + "vote.err.serialize": "Failed to serialize the submission", + "captcha.error": "Captcha verification failed", + "captcha.errEmpty": "Captcha cannot be empty", + "captcha.errFormat": "Invalid captcha format, please refresh and try again", + "captcha.errAnswerFormat": "Invalid captcha answer format", + "captcha.errExpired": "Captcha expired, please refresh and try again", + "captcha.errService": "Captcha service error, please try again later", + "captcha.errAnswerWrong": "Incorrect captcha answer", + "censor.unavailable": "The content moderation service is temporarily unavailable. Please try again later.", + + // Feature management: switch / schedule / quota for questions, chats and polls + "admin.tabAccount": "Settings", + "admin.featuresHint": "Manage the switch, opening hours and collection quota of each feature on the Questions / Chats / Polls panels. A feature closes automatically once its quota is reached.", + "admin.featuresEntry": "Settings", + "admin.featureHint.question": "Visitors cannot submit new questions while closed; collection stops automatically once the quota is reached (private questions included).", + "admin.featureHint.chat": "Visitors cannot start new chats while closed; existing conversations are unaffected.", + "admin.featureHint.vote": "Your polls are hidden from your box page and cannot be answered while closed. Each poll can also define its own quota on the edit page.", + "admin.featureEnabled": "Enable this feature", + "admin.featureOpenAt": "Open at", + "admin.featureCloseAt": "Close at", + "admin.featureTimeHint": "Leave blank for no limit; the feature is unavailable before the open time and after the close time.", + "admin.featureSchedule": "Recurring schedule (weekly)", + "admin.featureScheduleStart": "Daily start", + "admin.featureScheduleEnd": "Daily end", + "admin.featureScheduleHint": "Select the weekdays and a daily time window to open every week. Leave all empty to disable. Days without a time window open all day; a time window with no weekday selected opens every day.", + "admin.featureMode": "Schedule mode", + "admin.featureModeAlways": "Always open", + "admin.featureModeWindow": "One-time window", + "admin.featureModeRecurring": "Weekly schedule", + "admin.featureModeHint": "Always open: no time limit until manually disabled. One-time window: a single open/close time window. Weekly schedule: recurring weekly open days and hours. Modes are mutually exclusive; settings of unselected modes are cleared on save.", + "admin.featureQuotaLimit": "Collection quota (items)", + "admin.featureQuotaHint": "0 means unlimited; the feature closes automatically once the quota is reached. Reset the counter to start a new round.", + "admin.featureQuotaCount": "Collected", + "admin.featureQuotaUnlimited": "Unlimited", + "admin.featureQuotaResetAt": "Last reset: ", + "admin.featureResetQuota": "Reset counter", + "admin.featureSave": "Save this feature", + + // Feature management: states and messages + "feature.name.question": "Questions", + "feature.name.chat": "Chats", + "feature.name.vote": "Polls", + "feature.statusOpen": "Open", + "feature.status.disabled": "Closed", + "feature.status.not_started": "Not started", + "feature.status.ended": "Ended", + "feature.status.quota_reached": "Quota reached", + "feature.saved": "Feature settings saved", + "feature.errTime": "Invalid date for %s, please use the date-time picker", + "feature.errTimeRange": "The close time of %s must be later than the open time", + "feature.errQuota": "The collection quota of %s must be an integer between 0 and 1000000", + "feature.errInvalid": "Unknown feature", + "feature.quotaReset": "The collection counter of %s has been reset", + "feature.errScheduleHalf": "The recurring start and end times of %s must be filled in together", + "feature.errScheduleTime": "Invalid recurring time for %s, please use the time picker", + "feature.errScheduleRange": "The recurring end time of %s must be later than the start time", + + // Messages shown to visitors when a feature is closed + "feature.disabled.question": "The box owner has closed questions", + "feature.disabled.chat": "This box does not accept chats", + "feature.disabled.vote": "This box has closed polls", + "feature.not_started.question": "Questions are not open yet", + "feature.not_started.chat": "Chats are not open yet", + "feature.not_started.vote": "Polls are not open yet", + "feature.ended.question": "Question collection has ended", + "feature.ended.chat": "Chats have ended", + "feature.ended.vote": "Polls have ended", + "feature.quota_reached.question": "The question quota has been reached", + "feature.quota_reached.chat": "The chat quota has been reached", + "feature.quota_reached.vote": "The poll quota has been reached", + + // Auth: visitor-visible messages (P0) + "auth.errBadCredential": "Incorrect email or password", + "auth.errUserNotFound": "User does not exist", + "auth.errEmailFrequent": "Emails are sent too frequently, please try again later", + "auth.errEmailFailed": "Failed to send email, please try again later", + "auth.errCodeExpired": "The verification code has expired", + "auth.errPasswordChanged": "Password changed successfully", + "auth.errLoginRequired": "Please log in first", + "auth.errDuplicateEmail": "This email is already registered", + "auth.errDuplicateDomain": "This custom domain is already taken", + + // Question box page: visitor-visible (P0) + "question.errWrongPassword": "Wrong password", + "question.errAskPasswordWrong": "Wrong ask password", + "question.errLoginRequired": "The owner only allows registered users to ask questions. Please log in first.", + "question.errSelfChat": "You cannot start a chat with yourself", + "question.errBlockedChat": "You have been blocked by the other party and cannot start a chat", + "question.errChatDailyLimit": "You have reached today's message limit (%d). Please try again tomorrow", + "question.errDuplicateSubmit": "Duplicate submission detected, please try again later", + "question.errImageTooLarge": "Image file size cannot exceed 5Mb", + "question.errUploadImageFailed": "Failed to upload image", + "question.errBlockWords": "The question contains a blocked word set by the box owner, and could not be sent", + "question.errAnsweredLocked": "An answered question cannot be marked as unread", + "question.flash.chatSent": "Chat request sent! You can view the conversation via this link: %s", + "question.flash.answered": "Answer published successfully!", + "question.flash.operated": "Operation successful", + "question.flash.markedRead": "Marked as read", + "question.flash.markedUnread": "Marked as unread", + + // Follow-ups: visitor/owner visible (P0) + "followup.errNoPermission": "Permission denied", + "followup.errNotAnswered": "This question has not been answered yet, so you cannot follow up", + "followup.errLimit": "Each question allows at most %d follow-ups, and the limit has been reached", + "followup.errWaitReply": "Please wait for the owner to reply to your previous follow-up before continuing", + "followup.errNotFound": "The follow-up does not exist", + "followup.flash.sent": "Follow-up sent successfully!", + "followup.flash.answered": "Answer posted successfully!", + + // Admin settings: owner visible (P1) + "admin.flash.profileUpdated": "Profile updated successfully", + "admin.flash.chatUpdated": "Chat settings updated successfully", + "admin.flash.pagePwdOff": "Page password access disabled", + "admin.flash.pagePwdUpdated": "Page access password updated", + "admin.flash.askPwdOff": "Ask password disabled", + "admin.flash.askPwdUpdated": "Ask password updated", + "admin.flash.tagsTooMany": "You can have at most 5 tags", + "admin.flash.tagsUpdated": "Tags updated successfully", + "admin.flash.tagTooLong": "Each tag cannot exceed 5 characters: %s", + "admin.flash.blockWordsTooMany": "You can have at most 10 blocked words", + "admin.flash.blockWordTooLong": "A blocked word cannot exceed 10 characters: %s", + "admin.flash.harassmentUpdated": "Anti-harassment settings updated successfully", + "admin.flash.blockWordsUpdated": "Blocked words updated", + "admin.flash.deactivated": "Your account has been deactivated. Thank you for using Wolf's Question Box. We hope to see you again 👋🏻", + "admin.export.errUser": "Export failed: could not get user information", + "admin.export.errQuestion": "Export failed: could not get question information", + + // Vote management: owner visible (P1) + "vote.err.invalidID": "Invalid vote ID", + "vote.err.notExist": "The vote does not exist", + "vote.err.noPermission": "You are not allowed to operate this vote", + "vote.err.type": "Unknown poll type", + "vote.err.titleRequired": "Please fill in the poll title", + "vote.err.titleTooLong": "The poll title cannot exceed 100 characters", + "vote.err.visibility": "Unknown visibility setting", + "vote.err.voterScope": "Unknown voting scope setting", + "vote.err.showResult": "Unknown result visibility setting", + "vote.err.timeRange": "The end time must be later than the start time", + "vote.err.quota": "The collection quota must be an integer between 0 and 1000000", + "vote.err.optionsTooFew": "Please add at least 2 options", + "vote.err.optionsFormat": "Invalid options data format", + "vote.err.optionTooLong": "A single option cannot exceed %d characters", + "vote.err.maxChoicesExceed": "The max choices cannot exceed the total number of options", + "vote.err.fieldsTooFew": "Please add at least 1 form field", + "vote.err.fieldsFormat": "Invalid form fields data format", + "vote.err.fieldsTooMany": "You can have at most 50 form fields", + "vote.err.fieldType": "Unknown form field type", + "vote.err.fieldLabelMissing": "Field #%d is missing a label", + "vote.err.fieldLabelTooLong": "Field #%d label cannot exceed 100 characters", + "vote.err.fieldLabelDuplicate": "The field label \"%s\" is duplicated, please use a different label", + "vote.err.fieldMaxLength": "Field #%d max length must be between 0 and 2000", + "vote.err.fieldReservedOption": "Field #%d (%s) contains a reserved option value, please change it", + "vote.err.fieldOptionsTooFew": "Field #%d (%s) needs at least 1 option", + "vote.err.fieldRankTooFew": "Field #%d (%s) ranking questions need at least 2 options", + "vote.err.fieldSerialize": "Failed to serialize field options", + "vote.err.editClosed": "Votes that are ended or archived cannot be edited", + "vote.err.publishDraftOnly": "Only draft polls can be published", + "vote.err.closePublishedOnly": "Only active polls can be closed", + "vote.err.publishTitleRequired": "Please fill in the poll title before publishing", + "vote.err.publishFormFieldsRequired": "Form polls need at least 1 field", + "vote.err.publishOptionsRequired": "Choice polls need at least 2 options", + "vote.err.optionUpdateFailed": "Failed to update options, please retry", + "vote.err.fieldUpdateFailed": "Failed to update form fields, please retry", + "vote.flash.published": "Poll published! Share link: %s", + "vote.flash.updated": "Poll info updated (active polls cannot change type or options)", + "vote.flash.draftSaved": "Saved to draft", + "vote.flash.closed": "Poll ended", + "vote.flash.deleted": "Poll deleted", + "vote.err.timeFormat": "Invalid time format, please use the datetime picker", + "vote.err.maxChoicesLimit": "Maximum number of choices cannot exceed 20", + "vote.err.descriptionTooLong": "Vote description cannot exceed 5000 characters", + "profile.err.avatarTooLarge": "Avatar file is too large, maximum 2MB", + "profile.err.backgroundTooLarge": "Background image is too large, maximum 2MB", + "profile.err.newPasswordTooShort": "New password must be at least 8 characters", + "profile.err.badOldPassword": "Incorrect old password", + "admin.export.errCreateExcel": "Export failed: could not create Excel file", + "admin.export.errWriteExcel": "Export failed: could not write Excel file", + + // Profile page UI rework: card sections / avatar & background cropping / force re-login after password change + "profile.card.basic": "Basic info", + "profile.card.avatarBg": "Avatar & background", + "profile.card.qr": "QR code style", + "profile.card.password": "Change password", + "profile.avatarCropHint": "Crop frame is locked to 1:1 (square)", + "profile.backgroundCropHint": "Crop frame is locked to 16:9", + "profile.cropTitle": "Crop image", + "profile.cropConfirm": "Apply crop", + "profile.err.avatarRatio": "Avatar image must be a 1:1 square. Please crop it and upload again.", + "profile.err.backgroundRatio": "Background image must have a 16:9 aspect ratio. Please crop it and upload again.", + "profile.flash.passwordChanged": "Password changed. You have been signed out; please sign in again with your new password.", + "profile.pwdNote": "After saving, your current session will end immediately and you will need to sign in again with the new password.", + "profile.pickAvatar": "Change avatar", + "profile.pickBackground": "Change background", + "profile.saveBasic": "Save basic info", + "profile.saveMedia": "Save avatar & background", + "profile.savePassword": "Update password", + "profile.saveQR": "Save QR code style", + "profile.saveSocial": "Save social links", + "profile.err.cropLibUnavailable": "The image cropper failed to load. Please refresh the page and try again.", + + // Captcha JSON endpoint: consumed by frontend (P2) + "captcha.json.badRequest": "Invalid request format", + "captcha.json.idRequired": "Captcha ID cannot be empty", + "captcha.json.genFailed": "Failed to generate captcha", + "chat.errDeletedByOwner": "This private conversation has been deleted by the other party.", + "chat.errBlockedView": "You have been blocked by the other party and cannot view this chat", + "chat.errEmpty": "Message cannot be empty", + "chat.errTooLong": "Message is too long, please keep it within %d characters", + "chat.errConvDeletedSend": "This conversation has been deleted and messages cannot be sent", + "chat.errBlockedSend": "You have been blocked by the other party and cannot send messages", + "chat.errBlockWords": "Your message contains a blocked word set by the box owner and could not be sent", + "chat.errDailyLimit": "You have reached today's message limit (%d), please try again tomorrow", + "chat.errLimitReached": "The conversation has reached its limit and no new messages can be sent", + "chat.errChatClosed": "The box owner has disabled private chat and messages cannot be sent", + "chat.errImageTooLarge": "Image size cannot exceed %dMB", + "chat.errUploadFailed": "Image upload failed, please try again later", + "chat.errNoPermission": "You are not authorized to perform this action", + "chat.flash.sent": "Message sent successfully", + "chat.flash.deleted": "Private conversation deleted", + "chat.flash.operated": "Operation successful", + "image.errTooLarge": "Image is too large, maximum supported size is %d×%d pixels, please compress and re-upload", + "internal.errFlash": "Internal server error, please try again later.", + "internal.errServer": "Internal server error", + "home.errStats": "Failed to retrieve statistics", + "pixel.errPath": "Invalid request path", + "proxy.errNotFound": "Page not found", + "proxy.errInternal": "Service gateway internal error", + "explore.errRandomUser": "Failed to retrieve random users", + // 表单校验 + "form.answer.content": "Answer content is required", + "form.answer.contentTooLong": "Answer content must be within %d characters", + "form.email.required": "Please enter a valid email address", + "form.followup.answer": "Follow-up answer is required", + "form.followup.answerTooLong": "Follow-up answer must be within %d characters", + "form.followup.content": "Follow-up content is required", + "form.followup.contentTooLong": "Follow-up content must be within %d characters", + "form.login.password": "Please enter your password", + "form.profile.backgroundimagealpha": "Please enter the QR code background opacity", + "form.profile.colorHex": "Please enter a 7-character hex color code (including #)", + "form.profile.dotscale": "Please enter the QR code dot size", + "form.profile.intro": "Bio is required and must be at most 100 characters", + "form.profile.linkMax": "Profile link must be at most 30 characters", + "form.profile.name": "Nickname is required and must be at most 20 characters", + "form.profile.newPassword": "New password must be at most 30 characters", + "form.profile.passwordMax": "Password must be at most 30 characters", + "form.question.content": "Question content is required", + "form.question.contentTooLong": "Question content must be within %d characters", + "form.question.receiveReplyEmail": "Please enter a valid email address to receive replies", + "form.recaptcha": "Please complete the CAPTCHA verification", + "form.recover.newPassword": "Please set a new password of 8–30 characters", + "form.recover.repeatPassword": "The two new passwords do not match", + "form.register.domain": "The custom domain may only contain letters, digits and underscores, and must be 3–20 characters long", + "form.register.email": "Please enter a valid email address (max 100 characters)", + "form.register.name": "Nickname is required and must be at most 20 characters", + "form.register.password": "Please set a password of 8–30 characters", + "form.register.recaptcha": "Unable to load the CAPTCHA. Please refresh the page or switch your network and try again", + "form.register.repeatPassword": "The two passwords do not match", +} diff --git a/.workbuddy/backup/remove-vercount-20260906/dict_ja.go b/.workbuddy/backup/remove-vercount-20260906/dict_ja.go new file mode 100644 index 0000000..bc96681 --- /dev/null +++ b/.workbuddy/backup/remove-vercount-20260906/dict_ja.go @@ -0,0 +1,842 @@ +// Copyright 2022 E99p1ant. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package i18n + +// jaDict 日本語文案。 +var jaDict = map[string]string{ + // ナビゲーション + "nav.explore": "探索", + "nav.mine": "マイ", + "nav.register": "登録", + "nav.login": "ログイン", + "nav.ask": "質問", + "nav.settings": "設定", + "nav.lang": "言語", + + // 共通 + "common.login": "ログイン", + "common.forgot": "パスワードを忘れた", + "common.submit": "送信", + "common.cancel": "キャンセル", + "common.save": "保存", + "common.close": "閉じる", + "common.delete": "削除", + "common.moveUp": "上へ移動", + "common.moveDown": "下へ移動", + "common.share": "共有", + "common.verifying": "認証中...", + "common.submitting": "送信中...", + "common.uploading": "アップロード中...", + "common.lang": "言語", + "common.menu": "メニュー", + + "common.pleaseWait": "お待ちください...", + "common.backHome": "ホームに戻る", + "common.theme": "ダーク/ライトモード切替", + + // サイト + "site.title": "狼の質問箱 | 匿名質問箱", + "site.description": "狼の質問箱は登録不要で使える匿名質問箱。誰でも匿名で質問でき、安心してやり取りできる場所です。", + "site.keywords": "匿名質問箱,質問箱,匿名質問,狼の質問箱,匿名で質問,匿名Q&A", + "site.footer.visitor": "あなたは %s 人目の訪問者です", + "site.footer.pageview": "このページは %s 回閲覧されました", + "site.footer.runtime": "質問箱は稼働中", + "site.footer.admin": "管理者へ", + "site.footer.notice": "お知らせ", + "site.footer.chat": "チャット", + "site.footer.drive": "ドライブ", + "site.footer.status": "ステータス", + "site.footer.nav": "リンク", + "site.maintenance": "メンテナンス中です。しばらくお待ちください~", + + // 認証 + "auth.login.title": "ログイン", + "auth.login.email": "メールアドレス", + "auth.login.password": "パスワード", + "auth.login.submit": "ログイン", + "auth.login.forgot": "パスワードを忘れた", + "auth.register.title": "ユーザー登録", + "auth.register.name": "ニックネーム", + "auth.register.email": "メールアドレス", + "auth.register.password": "パスワード", + "auth.register.submit": "登録", + "auth.forgot.title": "パスワードを忘れた", + "auth.forgot.email": "メールアドレス", + "auth.forgot.submit": "再設定メールを送信", + "auth.forgot.submitBtn": "パスワードを再取得", + "auth.forgot.sent": "%s にメールを送信しました。ご確認ください。", + "auth.recover.title": "パスワード再設定", + "auth.recover.greeting": "%s さん、パスワードを再設定しています:", + "auth.recover.password": "新しいパスワード", + "auth.recover.confirm": "パスワード(確認)", + "auth.recover.submit": "パスワードを再設定", + + // 探索ページ + "explore.title": "ユーザータグを探索", + "explore.notice": "ここでは「ホームでタグを表示」を有効にしたユーザーを紹介しています。この機能は初期状態でオフです。自分の質問箱を見つけてもらいたい場合は 個人設定 から有効にしてください。", + "explore.shuffle": "シャッフル", + "explore.empty": "表示できるユーザーがいません。有効にしてみましょう~", + "explore.hotVotes": "人気の投票", + "explore.questionsTab": "質問", + "explore.votesTab": "投票", + "explore.votesTitle": "公開投票", + "explore.votesDesc": "開催中の公開投票を見て、興味のあるテーマに参加できます。", + "explore.randomVote": "ランダムおすすめ", + "explore.allVotes": "すべての投票", + "explore.noVotes": "現在、参加できる公開投票はありません。", + + // 質問ページ + "ask.scan": "QRコードで@%sに質問", + "ask.anonymous": "誰でも匿名で質問できます", + "ask.registerOnly": "登録ユーザーのみ質問できます", + "ask.nextOpenAt": "次回開放:%s", + "ask.lastCloseAt": "前回終了:%s", + "weekday.0": "日", + "weekday.1": "月", + "weekday.2": "火", + "weekday.3": "水", + "weekday.4": "木", + "weekday.5": "金", + "weekday.6": "土", + "ask.pinned": "ピン留め済み", + "ask.share": "質問箱を共有", + "ask.placeholder": "ここに質問を書いてください...", + "ask.chatPlaceholder": "ここにチャットメッセージを書いてください...", + "ask.addImage": "画像を追加", + "ask.private": "返信後に公開しない", + "ask.notify": "通知を受け取る", + "ask.emailPlaceholder": "メールアドレスを入力してください", + "ask.submit": "質問を送信", + "ask.chat": "チャットを開始", + "ask.chatDisabled": "この質問箱ではチャットが無効です", + "ask.chatLimit": "メッセージ上限:%d件", + "ask.history": "@%s が以前回答した質問", + "ask.loginRequired": "ログインが必要", + "ask.loginRequiredChat": "チャットを開始するにはログインが必要です", + "ask.goLogin": "ログインへ", + "ask.needPassword": "この質問箱にはパスワードが必要です", + "ask.needLogin": "質問箱の所有者は登録ユーザーのみ質問できるように設定しています。先にログインしてください。", + "ask.pleaseEmail": "通知を受け取るメールアドレスを入力してください", + "ask.generating": "共有カードを生成中…", + "ask.genFailed": "共有カードの生成に失敗しました。しばらくしてからお試しください", + "ask.networkError": "ネットワークエラー", + "ask.shareFilename": "share.png", + + // 質問アイテム + "qitem.answer": "回答", + "qitem.delete": "削除", + "qitem.private": "非公開にする", + "qitem.public": "公開にする", + "qitem.asked": "質問", + "qitem.pending": "回答待ち", + "qitem.answeredBy": "@%s による %s の回答", + "qitem.danger": "危険!", + "qitem.deleteConfirm": "この質問を削除してもよろしいですか?この操作は取り消せません。", + "qitem.deleteConfirmBtn": "削除する", + "qitem.backToBox": "%sの質問箱に戻る", + "qitem.answerTitle": "質問に回答する", + "qitem.answerPlaceholder": "ここに回答を入力...", + "qitem.emailNotifyHint": "質問者はメールアドレスを残しています。最初に回答すると、質問者にメール通知が送信されます。", + "qitem.submitAnswer": "回答", + "qitem.updateAnswer": "回答を更新", + "qitem.askMore": "ほかの質問は?", + "qitem.markRead": "既読にする", + "qitem.markUnread": "未読にする", + "qitem.readLocked": "回答済みのため既読に固定", + + // 個人設定ページ + "admin.profile": "プロフィール", + "admin.lang": "表示言語", + "admin.langHint": "訪問者があなたの質問箱を開いたときに表示する言語を選択します(管理画面もこの設定に従います)", + "admin.email": "メールアドレス", + "admin.name": "ニックネーム", + "admin.nameColor": "ニックネームの色", + "admin.oldPassword": "現在のパスワード", + "admin.newPassword": "新しいパスワード", + "admin.notify": "新着質問の通知", + "admin.personalize": "カスタマイズ", + "admin.intro": "質問箱の紹介文", + "admin.introColor": "紹介文の色", + "admin.qrBgColor": "QRコードの背景色(明るい部分)", + "admin.qrPdColor": "QRコードのファインダーパターン色(角の四角)", + "admin.qrFgColor": "QRコードの前景色", + "admin.qrDotSize": "QRコードのドットサイズ (←小 大→)", + "admin.qrBgAlpha": "QRコードの背景透明度 (←不透明 透明→)", + "admin.avatar": "アバター", + "admin.background": "質問箱の背景", + "admin.qrBackground": "QRコードの背景画像", + "admin.social": "ソーシャルリンク", + "admin.harassment": "迷惑行為対策", + "admin.askLimit": "質問制限", + "admin.blockWords": "ブロックワード設定(Enter またはカンマ区切り、最大10個、各10文字まで)", + "admin.chat": "チャット設定", + "admin.chatMaxMessages": "チャットのメッセージ上限", + "admin.chatDailyLimit": "1ユーザーあたりの1日のメッセージ上限", + "admin.tags": "タグ設定", + "admin.tagsHint": "タグ(最大5個、各5文字まで、Enter またはカンマ区切り)", + "admin.tagsPlaceholder": "例:アニメ,音楽,ゲーム", + "admin.tagInputHint": "入力して Enter またはカンマで追加", + "admin.tagInputDuplicate": "既に存在するため無視しました", + "admin.pagePassword": "ページアクセスパスワード", + "admin.pagePasswordHint": "アクセスパスワードを設定(空欄で無効)", + "admin.askPassword": "質問パスワード", + "admin.askPasswordHint": "質問用パスワードを設定(空欄で無効)", + "admin.saveProfile": "変更を保存", + "admin.updated": "プロフィールを更新しました", + "admin.logout": "ログアウト", + "admin.export": "データをエクスポート", + + // 管理画面設定(追加) + "admin.oldPasswordHint": "パスワード変更時のみ使用。空欄のままなら変更されません", + "admin.newPasswordHint": "6〜64文字。空欄のままなら変更されません", + "admin.notifyEmail": "メール", + "admin.useCustomFg": "カスタム前景色を使用(それ以外は自動マッチ)", + "admin.customFgHint": "有効時のみ適用されます。それ以外は自動生成されます", + "admin.avatarPlaceholder": "アバターをアップロード(1:1 以外は縦横比が崩れます)", + "admin.backgroundPlaceholder": "質問箱の背景をアップロード", + "admin.qrBackgroundPlaceholder": "QRコードの背景画像をアップロード(暗すぎる背景はQRコードが読み取れなくなることがあります)", + "admin.qrPreviewHint": "画像を選択するとプレビューが即時更新されます", + "admin.preview": "ライブプレビュー", + "admin.themePreset": "プリセットテーマ", + "admin.themeDefault": "デフォルト", + "admin.themeDark": "ダーク", + "admin.themeOcean": "オーシャン", + "admin.themeSunset": "サンセット", + "admin.resetDefaults": "🔄 デフォルトに戻す", + "admin.previewHint": "調整はプレビューのみです。「変更を保存」をクリックすると反映されます", + "admin.socialHint": "チェックを入れてプロフィールURLを入力してください(https:// は不要)。チェックのない項目は表示されません。", + "admin.enableAcfun": "AcFun を有効にする", + "admin.enableBili": "Bilibili を有効にする", + "admin.enableWeibo": "Weibo を有効にする", + "admin.enableXiaohongshu": "小紅書 を有効にする", + "admin.enableBlog": "個人ブログを有効にする", + "admin.registerOnly": "登録ユーザーのみ質問を許可", + "admin.updateHarassment": "迷惑行為対策を更新", + "admin.globalBlockWords": "共通ブロックワード", + "admin.globalBlockWordsHint": "ブロックワードは質問・チャット(相手からあなたへのメッセージ)・投票の投稿内容に適用されます。この設定は質問/チャット/投票の3つのタブで共有され、どこで変更してもすぐに反映されます。", + "admin.updateBlockWords": "ブロックワードを保存", + "admin.allowChat": "登録ユーザーからのチャットを許可", + "admin.dailyLimitHint": "0 は無制限", + "admin.updateChat": "チャット設定を更新", + "admin.showInHome": "ホームページでタグを表示(タグで検索されることを許可)", + "admin.updateTags": "タグを更新", + "admin.updatePagePassword": "ページパスワードを更新", + "admin.pagePasswordPlaceholder": "ページアクセスパスワードを入力", + "admin.updateAskPassword": "質問パスワードを更新", + "admin.askPasswordPlaceholder": "質問パスワードを入力", + "admin.account": "アカウント設定", + "admin.exportHint": "%s のすべての個人データ(基本情報、受け取った質問と回答を含む)をエクスポートできます。", + "admin.exportJSON": "全データをエクスポート(JSON)", + "admin.exportJSONHint": "質問・回答・追加質問・チャットを含む完全バックアップ。", + "admin.deactivate": "アカウントを停止", + "admin.deactivateHint": "いつでもアカウントを停止できます。停止後はログインできなくなり、質問箱のページや質問にもアクセスできなくなります。新しい質問も受け付けられません。この操作は取り消せません。ご注意ください!", + "admin.socialRequired": "リンクを入力するか、チェックを外してください", + + // 質問管理ページ + "qlist.title": "マイ質問", + "qlist.answered": "回答済み", + "qlist.pending": "回答待ち", + "qlist.private": "非公開", + "qlist.empty": "質問はまだありません。質問箱を共有しましょう~", + "qlist.action": "操作", + "qlist.answerNow": "回答する", + "qlist.sort": "並び替え", + "qlist.status": "状態", + "qlist.type": "種類", + "qlist.latest": "最新", + "qlist.oldest": "最古", + "qlist.all": "すべて", + "qlist.public": "通常の質問", + "qlist.privateQuestion": "非公開の質問", + "qlist.received": "受け取った質問", + "qlist.myChats": "開始したチャット", + "qlist.statsTotal": "質問総数", + "qlist.statsAnswered": "回答済み", + "qlist.statsUnanswered": "未回答", + "qlist.statsPrivate": "非公開質問", + "qlist.statsPinned": "ピン留め", + "qlist.statsChats": "チャット", + "qlist.searchPlaceholder": "質問や回答を検索...", + "qlist.searchBtn": "検索", + "qlist.searchClear": "クリア", + "qlist.questionsTab": "質問", + "qlist.votesTab": "投票", + "qlist.chatsTab": "チャット", + "qlist.receivedChats": "受け取ったチャット", + "qlist.chatEmpty": "まだチャットがありません。質問箱を共有してチャットを始めましょう!", + "qlist.pinnedShort": "ピン", + "qlist.privateShort": "非公開", + "qlist.chatStatsTotal": "チャット総数", + "qlist.chatStatsReceived": "受け取った", + "qlist.chatStatsInitiated": "開始した", + "qlist.chatStatsClosed": "終了", + "qlist.unansweredShort": "未答", + "qlist.readShort": "既読", + "qlist.unreadShort": "未読", + "qitem.markReadShort": "既読に", + "qlist.pinShort": "ピン", + "qlist.unpinShort": "解除", + "qlist.myVotes": "作成した投票", + "qlist.loadMore": "もっと読み込む", + "qlist.newVote": "投票を作成", + "qlist.edit": "編集", + "qlist.untitledVote": "無題の投票", + "qlist.blocked": "ブロック済み", + "qlist.unanswered": "未回答", + + // サイトロゴの接尾辞 + "site.logoSuffix": "の質問箱", + "site.stats": "質問箱の統計", + + // キャプチャモーダル + "captcha.title": "人機認証", + "captcha.hint": "以下の計算問題を解いて、ロボットでないことを証明してください", + "captcha.refresh": "別の問題", + "captcha.submit": "認証して送信", + "captcha.loading": "読み込み中...", + "captcha.loadFailed": "読み込み失敗", + "captcha.loadRetry": "読み込み失敗、再試行してください", + "captcha.inputAnswer": "答えを入力してください", + "captcha.wrongAnswer": "答えが違います。もう一度計算してください", + "captcha.verifyFailed": "認証失敗、再試行してください", + + // 登録ページ補足 + "auth.register.domain.prefix": "カスタムドメイン(あなたの質問箱URLは %s + ", + "auth.register.domain.suffix": " + 下記に入力した内容になります)", + "auth.register.confirm": "パスワード(確認)", + "auth.register.language": "表示言語", + "auth.register.languageDefault": "サイト既定に従う", + "auth.register.success": "登録が完了しました。狼の質問箱へようこそ!", + + // 探索ページ補足 + "explore.recommend": "ランダムおすすめ", + "explore.search": "タグを検索...", + "explore.sortLatest": "新着順", + "explore.sortOldest": "古い順", + "explore.noTags": "タグなし", + "explore.noRecommend": "おすすめユーザーはいません", + "explore.hotTags": "人気タグ", + "explore.hotQuestions": "人気質問", + "explore.noResult": "条件に一致するユーザーはいません。", + "explore.lazy": "このユーザーは何も書いていません~", + "explore.prevPage": "前へ", + "explore.nextPage": "次へ", + "explore.loading": "読み込み中...", + "explore.loadFailed": "読み込み失敗", + + // 質問箱パスワードページ + "password.title": "この質問箱はパスワード保護されています", + "password.desc": "この質問箱はパスワード保護されています。パスワードを入力して内容を表示してください", + "password.input": "アクセスパスワードを入力してください", + "password.enter": "認証して入る", + "password.submit": "確認", + + // 質問が見つかりません + "qitem.notFound": "質問が見つからないか、削除されました", + + // チャットページ + "chat.title": "チャット", + "chat.input": "メッセージを入力...", + "chat.send": "送信", + "chat.delete": "会話を削除", + "chat.block": "相手をブロック", + "chat.unblock": "ブロック解除", + "chat.empty": "メッセージはまだありません。何か書いてみましょう~", + "chat.withAsker": "質問者とのチャット", + "chat.withUser": "%s とのチャット", + "chat.closed": "会話は終了しました", + "chat.closedNoSend": "会話は終了したため、新しいメッセージを送信できません", + "chat.remaining": "残りメッセージ:%d 件(全 %d 件)", + "chat.me": "自分", + "chat.other": "相手", + "chat.userBlocked": "このユーザーはブロックされています", + "chat.addImage": "画像を追加(任意、最大5MB)", + "chat.confirm": "確認", + "chat.confirmBlock": "このユーザーをブロックしますか?ブロック後はメッセージを送信できなくなります。", + "chat.confirmUnblock": "このユーザーのブロックを解除しますか?再度メッセージを送信できるようになります。", + "chat.deleteConfirm": "このチャットを削除しますか?相手はこの会話を見られなくなり、この操作は取り消せません。", + "maintenance.title": "NekoBox メンテナンス中...", + "maintenance.desc": "NekoBox はメンテナンス中のため、一時的にご利用いただけません。また後でお越しください~", + + // 質問の個別シェア(v4) + "qitem.shareCard": "この質問をシェア", + + // 追加質問(v4) + "followup.title": "追加質問", + "followup.content": "追加質問:", + "followup.answerPlaceholder": "この追加質問に回答...", + "followup.answerBtn": "回答する", + "followup.waiting": "回答待ち...", + "followup.askMore": "追加で質問する", + "followup.remaining": "追加質問可能: %d / %d 回", + "followup.contentPlaceholder": "質問を補足...", + "followup.submit": "送信", + "followup.waitingReply": "追記を送信しました。ボックス所有者の返信をお待ちください...", + + // ピン留め(v4) + "qlist.pinned": "ピン留め中", + "qlist.pin": "ピン留め", + "qlist.unpin": "ピン留め解除", + "qlist.readStatus": "既読ステータス", + "qlist.read": "既読", + "qlist.unread": "未読", + "qlist.statsRead": "既読", + "qlist.statsUnread": "未読", + + // 質問送信完了ページ(v4) + "success.title": "質問を送信しました!", + "success.yourQuestion": "あなたの質問:", + "success.emailSent": "この質問のプライベートリンクをメールで送信しました。以下のリンクからいつでも質問の確認、回答の受信、追加質問ができます。", + "success.saveLink": "以下のプライベートリンクを必ず保存してください!メールアドレスを入力しなかったため、このリンク(トークンを含む)が質問の確認・回答の受信・追加質問のための唯一の証拠です。失くすと復元できません。", + "success.privateLink": "あなたのプライベートリンク(クリックしてコピー)", + "success.copy": "コピー", + "success.copied": "コピーしました ✓", + "success.copyFailed": "コピーに失敗", + "success.token": "トークン: %s(質問者はこのトークンでプライベートリンクにアクセス)", + "success.viewQuestion": "質問を確認する", + "success.backToBox": "%sの質問箱に戻る", + "success.pageTitle": "質問送信完了 - %s", + + // アカウント停止ページ + "deactivate.title": "アカウント停止", + "deactivate.desc": "停止後、あなたの質問箱にはアクセスできなくなり、すべてのデータが削除されます。この操作は取り消せません!", + "deactivate.confirm": "アカウントを停止することに同意します", + "deactivate.submit": "アカウントを停止", + + // 匿名投票 + "nav.votes": "投票", + + "common.back": "戻る", + + "vote.listTitle": "投票管理", + "vote.newTitle": "投票を作成", + "vote.editTitle": "投票を編集", + "vote.resultsTitle": "投票結果", + "vote.statsTotal": "投票総数", + "vote.statsPublished": "進行中", + "vote.createNew": "投票を作成", + "vote.typeSingle": "単一選択", + "vote.typeMultiple": "複数選択", + "vote.typeRating": "評価", + "vote.typeRank": "順位付け", + "vote.typeForm": "フォーム", + "vote.statusNotStarted": "未開始", + "vote.statusDraft": "下書き", + "vote.statusPublished": "進行中", + "vote.statusEnded": "終了", + "vote.statusArchived": "アーカイブ", + "vote.participants": "参加者", + "vote.remainLabel": "残り", + "vote.perpetual": "期限なし", + "vote.durationDay": "日", + "vote.durationHour": "時間", + "vote.durationMinute": "分", + "vote.createdAt": "作成日時", + "vote.edit": "編集", + "vote.results": "結果", + "vote.preview": "プレビュー", + "vote.page.mustComplete": "入力してください", + "vote.page.rankHintRequired": "順位 1-N(必須)", + "vote.page.rankHintOptional": "順位 1-N(任意、一部のみ可)", + "vote.page.previewHint": "プレビューモード:公開前のサンプルです。送信は無効です", + "vote.page.previewSubmitHint": "プレビューモード。公開後に送信できます", + "vote.publish": "公開", + "vote.close": "終了", + "vote.share": "リンクをコピー", + "vote.copied": "コピーしました", + "vote.shareURL": "投票の共有リンク", + "vote.confirmPublish": "この投票を公開しますか?公開後、共有リンクが生成されます。", + "vote.confirmClose": "この投票を終了しますか?終了後、参加者は投票できなくなります。", + "vote.confirmDelete": "この投票を削除しますか?投票データもすべて削除され、元に戻せません!", + "vote.manageEntry": "投票を管理", + + "vote.manageTitle": "投票管理", + "vote.manageDesc": "匿名投票の作成と管理", "vote.empty": "投票はまだありません。右上の\"投票を作成\"から始めましょう。", + "vote.formLockedHint": "この投票は公開中です。基本情報のみ編集可能です(収集済みデータを守るため、タイプと選択肢はロックされています)", + "vote.formBasic": "基本設定", + "vote.formTitle": "投票タイトル", + "vote.formTitlePlaceholder": "例:今日の昼ごはんは?", + "vote.formDescription": "説明", + "vote.formDescriptionPlaceholder": "補足説明(任意)", + "vote.formType": "投票タイプ", + "vote.formVisibility": "表示方法", + "vote.formVisibilityPublic": "公開(探索ページ + 質問箱ページ)", + "vote.formVisibilityLinkOnly": "リンクのみ", + "vote.formVoterScope": "投票範囲", + "vote.formVoterScopeAnyone": "誰でも(匿名含む)", + "vote.formVoterScopeRegistered": "登録ユーザーのみ", + "vote.formShowResult": "結果の表示タイミング", + "vote.formShowResultAfterVote": "投票後すぐに表示", + "vote.formShowResultAfterEnd": "終了後に表示", + "vote.formShowResultOwnerOnly": "本人のみ", + "vote.formStartAt": "開始時間(空欄=すぐ開始)", + "vote.formEndAt": "終了時間(空欄=制限なし)", + "vote.formMaxChoices": "最大選択数", + "vote.formMaxVotesPerFingerprint": "投票ごとに1台の端末につき1回まで。同じブラウザからの再送信は拒否されます。", + "vote.formMaxVotesPerAccount": "投票ごとに1つの登録アカウントにつき1回まで。同じアカウントからの再送信は拒否されます。", + "vote.formAllowCustom": "参加者がカスタム選択肢を追加できる", + "vote.formRequireCaptcha": "匿名投票にキャプチャを要求", + "vote.formOptions": "投票の選択肢", + "vote.formRatingHint": "評価投票は選択肢不要(1-10点)", + "vote.addOption": "選択肢を追加", + "vote.optionPlaceholder": "選択肢の内容", + "vote.formFields": "フォーム項目", + "vote.addField": "項目を追加", + "vote.field": "項目", + "vote.formFieldType": "項目タイプ", + "vote.fieldTypeText": "一行テキスト", + "vote.fieldTypeTextarea": "複数行テキスト", + "vote.fieldTypeSelect": "ドロップダウン選択", + "vote.fieldTypeRadio": "単一選択", + "vote.fieldTypeCheckbox": "複数選択", + "vote.fieldTypeNumber": "数値", + "vote.fieldTypeRank": "順位", + "vote.page.selectPlaceholder": "選択してください", + "vote.page.avgNumber": "平均", + "vote.page.fieldFilled": "回答数", + "vote.fieldTypeRating": "評価", + "vote.formFieldLabel": "項目ラベル", + "vote.formFieldRequired": "必須", + "vote.formFieldMaxLength": "文字数上限(0=制限なし)", + "vote.formFieldOptions": "選択肢", + "vote.formFieldOptionsHint": "下の「選択肢を追加」で一つずつ追加してください(カンマ不要)", + "vote.fieldAllowCustom": "参加者がカスタム選択肢を追加できる", + "vote.formFieldOptionsPlaceholder": "例:良い,普通,悪い", + "vote.createDraft": "下書きを保存", + "vote.builderHintOptions": "選択肢は最低 2 つ必要です", + "vote.builderHintFields": "フィールドを最低 1 つ追加してください", + "vote.builderHintFieldOptions": "選択・順位題には選択肢が必要です(順位は最低 2 つ)", + "vote.resultsAnswered": "回答数", + "vote.page.rankedTimes": "順位付け数", + "vote.resultsTotal": "総参加人数", + "vote.resultsSubmissions": "フォームの回答", + "vote.resultsViewTexts": "テキスト回答を表示", + "vote.resultsToggleHint": "クリックでテキスト回答の表示・非表示を切り替え", + "vote.resultsOptions": "投票結果", + "vote.resultsVotes": "票", + "vote.export": "CSV エクスポート", + "vote.resultsEmpty": "まだ参加者がいません。リンクを共有しましょう!", + + // 投票参加ページ + "vote.inlineTitle": "投票", + "vote.page.by": "の投票", + "vote.page.goVote": "投票する", + "vote.page.notStarted": "未開始", + "vote.page.notStartedHint": "この投票はまだ始まっていません。", + "vote.page.ended": "終了", + "vote.page.endedHint": "この投票は終了しました。", + "vote.page.active": "進行中", + "vote.page.participated": "参加済み", + "vote.page.loginRequired": "この投票は登録ユーザーのみ参加できます。ログインしてください。", + "vote.page.anonymousHint": "匿名での参加が可能で、あなたの身元は記録されません(同一端末で最大1回)。", + "vote.page.submit": "投票する", + "vote.page.alreadyVoted": "この投票にはすでに参加済みです。ありがとうございます!", + "vote.page.resultsTitle": "投票結果", + "vote.page.totalVotes": "総参加", + "vote.page.avgRating": "平均点", + "vote.page.avgRank": "平均順位", + "vote.page.optionColumn": "選択肢", + "vote.page.ratingLabel": "評価", + "vote.page.ratingUnit": "点", + "vote.page.rankHint": "各選択肢に 1-N の順位を入力してください(小さいほど上位、重複不可)", + "vote.page.rankPlaceholder": "順位", + "vote.page.multipleHint": "複数選択可", + "vote.page.customOption": "カスタム選択肢(任意)", + "vote.page.customPlaceholder": "あなたの選択肢を入力", + "vote.page.customOther": "その他", + "vote.page.customFieldPlaceholder": "カスタム選択肢を入力…", + "vote.page.customStats": "カスタム選択肢", + "vote.page.formCount": "回答受付数", + "vote.page.ownerManage": "この投票を管理(結果は本人のみ閲覧可能)", + + // 投票ごとの定量収集 + "vote.formQuotaLimit": "収集上限(件)", + "vote.formQuotaHint": "0 は無制限。指定件数に達すると自動的に投票を終了します。", + "vote.flash.notFound": "この投票は存在しないか、既に無効です", + "vote.flash.notPublished": "この投票はまだ公開されていません", + "vote.flash.archived": "この投票はアーカイブ済みのため参加できません", + "vote.flash.ended": "この投票は終了しました", + "vote.flash.notStarted": "この投票はまだ開始していません", + "vote.flash.success": "投票が完了しました。ご参加ありがとうございます!", + "vote.err.noChoice": "少なくとも 1 つの選択肢を選んでください", + "vote.err.tooManyChoices": "選択できるのは最大 %d 個までです", + "vote.err.pickOne": "選択肢を 1 つ選んでください", + "vote.err.invalidOption": "無効な選択肢です", + "vote.err.optionDuplicate": "選択肢は重複できません", + "vote.err.ratingRequired": "1〜10 点の評価を入力してください", + "vote.err.rankRequired": "すべての選択肢に 1〜%d の順位を入力してください", + "vote.err.rankDuplicate": "順位が重複しています", + "vote.err.fieldRequired": "「%s」を入力してください", + "vote.err.fieldCustomRequired": "「%s」の自由記入欄を入力してください", + "vote.err.fieldCustomTooLong": "「%s」の自由記入は %d 字以内にしてください", + "vote.err.fieldInvalidOption": "「%s」の選択肢が無効です", + "vote.err.fieldConfigError": "「%s」の選択肢の設定に誤りがあります", + "vote.err.fieldRankIncomplete": "「%s」の順位をすべて入力してください", + "vote.err.fieldRankRange": "「%s」の順位は 1〜%d の数字で入力してください", + "vote.err.fieldRankDuplicate": "「%s」の順位が重複しています", + "vote.err.fieldTooLong": "「%s」が文字数上限(最大 %d 字)を超えています", + "vote.err.fieldNotNumber": "「%s」は数値で入力してください", + "vote.err.fieldRating": "「%s」は 1〜10 点で評価してください", + "vote.err.unknownType": "不明な投票タイプです", + "vote.err.customTooLong": "自由記入オプションは %d 字以内にしてください", + "vote.err.blockWords": "投稿内容に投票作成者が設定したブロックワードが含まれているため、送信できませんでした", + "vote.err.serialize": "送信内容の保存に失敗しました", + "captcha.error": "認証コードが正しくありません", + "captcha.errEmpty": "認証コードを入力してください", + "captcha.errFormat": "認証コードの形式が正しくありません。再読み込みしてお試しください", + "captcha.errAnswerFormat": "認証コードの回答形式が正しくありません", + "captcha.errExpired": "認証コードの有効期限が切れました。再読み込みしてお試しください", + "captcha.errService": "認証コードサービスでエラーが発生しました。後ほどお試しください", + "captcha.errAnswerWrong": "認証コードの回答が正しくありません", + "censor.unavailable": "コンテンツ審査サービスが一時的に利用できません。後ほどお試しください", + + // 機能管理:質問 / チャット / 投票 の ON・OFF、期間指定、定量収集 + "admin.tabAccount": "ユーザー設定", + "admin.featuresHint": "「質問 / チャット / 投票」パネルで各機能の ON/OFF、公開期間、収集上限を管理します。上限に達すると対象機能は自動的に閉じられます。", + "admin.featuresEntry": "設定", + "admin.featureHint.question": "OFF にすると新しい質問を投稿できません。上限に達すると自動的に受付を停止します(非公開質問を含む)。", + "admin.featureHint.chat": "OFF にすると新しいチャットを開始できません。既存の会話には影響しません。", + "admin.featureHint.vote": "OFF にすると質問箱ページに投票が表示されず、回答もできなくなります。投票ごとの上限は編集ページで設定できます。", + "admin.featureEnabled": "この機能を有効にする", + "admin.featureOpenAt": "公開開始", + "admin.featureCloseAt": "公開終了", + "admin.featureTimeHint": "空欄は制限なし。開始前と終了後はその機能を利用できません。", + "admin.featureSchedule": "定期公開(毎週)", + "admin.featureScheduleStart": "毎日の開始", + "admin.featureScheduleEnd": "毎日の終了", + "admin.featureScheduleHint": "毎週の公開曜日と毎日の時間帯を設定すると、毎週繰り返し公開されます。すべて空欄で無効。曜日のみ(時間なし)は終日公開、時間のみ(曜日なし)は毎日その時間帯に公開。", + "admin.featureMode": "収集モード", + "admin.featureModeAlways": "常時開放", + "admin.featureModeWindow": "期間限定", + "admin.featureModeRecurring": "曜日スケジュール", + "admin.featureModeHint": "常時開放:手動でオフにするまで開放し続けます。期間限定:一回限りの開始/終了時刻で開放します。曜日スケジュール:毎週の開放日と時間帯を繰り返し適用します。モードは保存時に排他となり、未選択モードの設定はクリアされます。", + "admin.featureQuotaLimit": "収集上限(件)", + "admin.featureQuotaHint": "0 は無制限。上限に達すると自動的に閉じられます。カウントをリセットすると次の回を開始できます。", + "admin.featureQuotaCount": "収集済み", + "admin.featureQuotaUnlimited": "無制限", + "admin.featureQuotaResetAt": "最終リセット:", + "admin.featureResetQuota": "カウントをリセット", + "admin.featureSave": "この機能を保存", + + // 機能管理:状態とメッセージ + "feature.name.question": "質問", + "feature.name.chat": "チャット", + "feature.name.vote": "投票", + "feature.statusOpen": "受付中", + "feature.status.disabled": "停止中", + "feature.status.not_started": "未開始", + "feature.status.ended": "終了", + "feature.status.quota_reached": "上限到達", + "feature.saved": "機能設定を保存しました", + "feature.errTime": "%s の日時形式が正しくありません。日時選択ツールを使用してください", + "feature.errTimeRange": "%s の公開終了時刻は公開開始時刻より後にしてください", + "feature.errQuota": "%s の収集上限は 0〜1000000 の整数で入力してください", + "feature.errInvalid": "不明な機能です", + "feature.quotaReset": "%s の収集カウントをリセットしました", + "feature.errScheduleHalf": "%s の定期公開の開始時間と終了時間は両方入力してください", + "feature.errScheduleTime": "%s の定期公開の時刻形式が正しくありません。時刻選択ツールを使用してください", + "feature.errScheduleRange": "%s の定期公開の終了時刻は開始時刻より後にしてください", + + // 機能が無効なときに訪問者へ表示するメッセージ + "feature.disabled.question": "この質問箱の管理者は質問の受付を停止しています", + "feature.disabled.chat": "この質問箱ではチャットを受け付けていません", + "feature.disabled.vote": "この質問箱では投票を受け付けていません", + "feature.not_started.question": "質問の受付はまだ開始していません", + "feature.not_started.chat": "チャットの受付はまだ開始していません", + "feature.not_started.vote": "投票の受付はまだ開始していません", + "feature.ended.question": "質問の受付は終了しました", + "feature.ended.chat": "チャットの受付は終了しました", + "feature.ended.vote": "投票の受付は終了しました", + "feature.quota_reached.question": "質問の収集上限に達しました", + "feature.quota_reached.chat": "チャットの収集上限に達しました", + "feature.quota_reached.vote": "投票の収集上限に達しました", + + // 認証:訪問者に表示されるメッセージ(P0) + "auth.errBadCredential": "メールアドレスまたはパスワードが正しくありません", + "auth.errUserNotFound": "ユーザーが存在しません", + "auth.errEmailFrequent": "メールの送信頻度が高すぎます。しばらくしてからお試しください", + "auth.errEmailFailed": "メールの送信に失敗しました。しばらくしてからお試しください", + "auth.errCodeExpired": "認証コードの有効期限が切れました", + "auth.errPasswordChanged": "パスワードを変更しました", + "auth.errLoginRequired": "ログインしてください", + "auth.errDuplicateEmail": "このメールアドレスは既に登録されています", + "auth.errDuplicateDomain": "このカスタムドメインは既に使用されています", + + // 質問箱ページ:訪問者に表示(P0) + "question.errWrongPassword": "パスワードが正しくありません", + "question.errAskPasswordWrong": "質問用パスワードが正しくありません", + "question.errLoginRequired": "質問箱の所有者は登録ユーザーのみ質問できるように設定しています。先にログインしてください。", + "question.errSelfChat": "自分自身とのチャットは開始できません", + "question.errBlockedChat": "相手にブロックされているため、チャットを開始できません", + "question.errChatDailyLimit": "本日の送信上限(%d件)に達しました。明日お試しください", + "question.errDuplicateSubmit": "重複送信が検出されました。しばらくしてからお試しください", + "question.errImageTooLarge": "画像ファイルのサイズは 5Mb 以下である必要があります", + "question.errUploadImageFailed": "画像のアップロードに失敗しました", + "question.errBlockWords": "質問に所有者が設定したブロックワードが含まれているため、送信できませんでした", + "question.errAnsweredLocked": "回答済みの質問は未読にできません", + "question.flash.chatSent": "チャットリクエストを送信しました!以下のリンクから会話を確認できます:%s", + "question.flash.answered": "回答を公開しました!", + "question.flash.operated": "操作が完了しました", + "question.flash.markedRead": "既読にしました", + "question.flash.markedUnread": "未読にしました", + + // 追加質問:訪問者/所有者に表示(P0) + "followup.errNoPermission": "権限がありません", + "followup.errNotAnswered": "この質問にはまだ回答がありません。追加質問はできません", + "followup.errLimit": "各質問の追加質問は最大 %d 回までで、上限に達しました", + "followup.errWaitReply": "前の追加質問への返信を待ってから続けてください", + "followup.errNotFound": "追加質問が存在しません", + "followup.flash.sent": "追加質問を送信しました!", + "followup.flash.answered": "回答を投稿しました!", + + // 管理設定:所有者に表示(P1) + "admin.flash.profileUpdated": "プロフィールを更新しました", + "admin.flash.chatUpdated": "チャット設定を更新しました", + "admin.flash.pagePwdOff": "ページパスワード認証を無効にしました", + "admin.flash.pagePwdUpdated": "ページアクセスパスワードを更新しました", + "admin.flash.askPwdOff": "質問パスワードを無効にしました", + "admin.flash.askPwdUpdated": "質問パスワードを更新しました", + "admin.flash.tagsTooMany": "タグは最大 5 個までです", + "admin.flash.tagsUpdated": "タグを更新しました", + "admin.flash.tagTooLong": "各タグは 5 文字以内である必要があります:%s", + "admin.flash.blockWordsTooMany": "ブロックワードは最大 10 個までです", + "admin.flash.blockWordTooLong": "ブロックワードの長さは 10 文字以内である必要があります:%s", + "admin.flash.harassmentUpdated": "迷惑行為対策を更新しました", + "admin.flash.blockWordsUpdated": "ブロックワードを更新しました", + "admin.flash.deactivated": "アカウントを停止しました。狼の質問箱をご利用いただきありがとうございます。またのお越しをお待ちしています 👋🏻", + "admin.export.errUser": "エクスポート失敗:ユーザー情報の取得に失敗しました", + "admin.export.errQuestion": "エクスポート失敗:質問情報の取得に失敗しました", + + // 投票管理:所有者に表示(P1) + "vote.err.invalidID": "無効な投票 ID", + "vote.err.notExist": "投票が存在しません", + "vote.err.noPermission": "この投票を操作する権限がありません", + "vote.err.type": "不明な投票タイプです", + "vote.err.titleRequired": "投票のタイトルを入力してください", + "vote.err.titleTooLong": "投票のタイトルは 100 文字以内である必要があります", + "vote.err.visibility": "不明な表示設定です", + "vote.err.voterScope": "不明な投票範囲設定です", + "vote.err.showResult": "不明な結果表示設定です", + "vote.err.timeRange": "終了時刻は開始時刻より後にしてください", + "vote.err.quota": "収集上限は 0〜1000000 の整数で入力してください", + "vote.err.optionsTooFew": "選択肢を最低 2 つ追加してください", + "vote.err.optionsFormat": "選択肢データの形式が正しくありません", + "vote.err.optionTooLong": "1 つの選択肢は %d 文字以内である必要があります", + "vote.err.maxChoicesExceed": "最大選択数は選択肢の総数を超えられません", + "vote.err.fieldsTooFew": "フォーム項目を最低 1 つ追加してください", + "vote.err.fieldsFormat": "フォーム項目データの形式が正しくありません", + "vote.err.fieldsTooMany": "フォーム項目は最大 50 個までです", + "vote.err.fieldType": "不明なフォーム項目タイプです", + "vote.err.fieldLabelMissing": "%d 番目の項目にラベルがありません", + "vote.err.fieldLabelTooLong": "%d 番目の項目のラベルは 100 文字以内である必要があります", + "vote.err.fieldLabelDuplicate": "項目ラベル「%s」が重複しています。別のラベルを使用してください", + "vote.err.fieldMaxLength": "%d 番目の項目の文字数上限は 0〜2000 の間である必要があります", + "vote.err.fieldReservedOption": "%d 番目の項目(%s)の選択肢に予約値が含まれています。変更してください", + "vote.err.fieldOptionsTooFew": "%d 番目の項目(%s)は選択肢を最低 1 つ入力してください", + "vote.err.fieldRankTooFew": "%d 番目の項目(%s)の順位付けには最低 2 つの選択肢が必要です", + "vote.err.fieldSerialize": "項目の選択肢のシリアライズに失敗しました", + "vote.err.editClosed": "終了またはアーカイブされた投票は編集できません", + "vote.err.publishDraftOnly": "草稿状態の投票のみ公開できます", + "vote.err.closePublishedOnly": "進行中の投票のみ終了できます", + "vote.err.publishTitleRequired": "公開前に投票のタイトルを入力してください", + "vote.err.publishFormFieldsRequired": "フォーム型投票には最低 1 つの項目が必要です", + "vote.err.publishOptionsRequired": "選択型投票には最低 2 つの選択肢が必要です", + "vote.err.optionUpdateFailed": "選択肢の更新に失敗しました。再試行してください", + "vote.err.fieldUpdateFailed": "フォーム項目の更新に失敗しました。再試行してください", + "vote.flash.published": "投票を公開しました!共有リンク:%s", + "vote.flash.updated": "投票情報を更新しました(進行中の投票はタイプと選択肢を変更できません)", + "vote.flash.draftSaved": "下書きに保存しました", + "vote.flash.closed": "投票を終了しました", + "vote.flash.deleted": "投票を削除しました", + "vote.err.timeFormat": "日時の形式が正しくありません。日時選択器を使用してください", + "vote.err.maxChoicesLimit": "選択可能数は20を超えられません", + "vote.err.descriptionTooLong": "投票の説明は5000文字を超えられません", + "profile.err.avatarTooLarge": "アイコンファイルが大きすぎます。最大2MB", + "profile.err.backgroundTooLarge": "背景画像が大きすぎます。最大2MB", + "profile.err.newPasswordTooShort": "新しいパスワードは8文字以上である必要があります", + "profile.err.badOldPassword": "古いパスワードが間違っています", + "admin.export.errCreateExcel": "エクスポート失敗:Excelファイルの作成に失敗しました", + "admin.export.errWriteExcel": "エクスポート失敗:Excelファイルの書き込みに失敗しました", + + // プロフィール設定ページ UI リニューアル:カード区切り/アイコン・背景の切り取り/パスワード変更後の強制ログアウト + "profile.card.basic": "基本情報", + "profile.card.avatarBg": "アイコンと背景", + "profile.card.qr": "QRコードのスタイル", + "profile.card.password": "パスワード変更", + "profile.avatarCropHint": "切り取り枠は 1:1(正方形)に固定されています", + "profile.backgroundCropHint": "切り取り枠は 16:9 に固定されています", + "profile.cropTitle": "画像を切り取る", + "profile.cropConfirm": "切り取りを確定", + "profile.err.avatarRatio": "アイコン画像は 1:1 の正方形である必要があります。切り取ってから再アップロードしてください", + "profile.err.backgroundRatio": "背景画像は 16:9 の縦横比である必要があります。切り取ってから再アップロードしてください", + "profile.flash.passwordChanged": "パスワードを変更しました。ログアウトしましたので、新しいパスワードで再度ログインしてください", + "profile.pwdNote": "保存後、現在のセッションはすぐに無効になり、新しいパスワードで再ログインする必要があります", + "profile.pickAvatar": "アイコンを変更", + "profile.pickBackground": "背景を変更", + "profile.saveBasic": "基本情報を保存", + "profile.saveMedia": "アイコンと背景を保存", + "profile.savePassword": "パスワードを更新", + "profile.saveQR": "QRコードのスタイルを保存", + "profile.saveSocial": "SNSリンクを保存", + "profile.err.cropLibUnavailable": "画像切り取り機能の読み込みに失敗しました。ページを更新してもう一度お試しください", + + // 認証コード JSON インターフェース:フロントエンドが消費(P2) + "captcha.json.badRequest": "リクエスト形式が正しくありません", + "captcha.json.idRequired": "認証コード ID を入力してください", + "captcha.json.genFailed": "認証コードの生成に失敗しました", + "chat.errDeletedByOwner": "このプライベート会話は相手によって削除されました。", + "chat.errBlockedView": "あなたは相手にブロックされ、このチャットを表示できません", + "chat.errEmpty": "メッセージを入力してください", + "chat.errTooLong": "メッセージが長すぎます。%d文字以内でお願いします", + "chat.errConvDeletedSend": "この会話は削除されたため、メッセージを送信できません", + "chat.errBlockedSend": "あなたは相手にブロックされ、メッセージを送信できません", + "chat.errBlockWords": "メッセージにボックス所有者が設定したブロックワードが含まれているため、送信できませんでした", + "chat.errDailyLimit": "本日の送信上限(%d件)に達しました。明日再度お試しください", + "chat.errLimitReached": "会話が上限に達したため、新しいメッセージを送信できません", + "chat.errChatClosed": "箱の所有者がプライベートチャットを無効にしたため、メッセージを送信できません", + "chat.errImageTooLarge": "画像サイズは%dMBを超えることはできません", + "chat.errUploadFailed": "画像のアップロードに失敗しました。後でもう一度お試しください", + "chat.errNoPermission": "この操作を実行する権限がありません", + "chat.flash.sent": "メッセージを送信しました", + "chat.flash.deleted": "プライベート会話を削除しました", + "chat.flash.operated": "操作が完了しました", + "image.errTooLarge": "画像が大きすぎます。最大%d×%dピクセルまで対応しています。圧縮して再アップロードしてください", + "internal.errFlash": "サーバー内部エラーが発生しました。後でもう一度お試しください。", + "internal.errServer": "サーバー内部エラー", + "home.errStats": "統計情報の取得に失敗しました", + "pixel.errPath": "不正なリクエストパス", + "proxy.errNotFound": "ページが見つかりません", + "proxy.errInternal": "サービスゲートウェイ内部エラー", + "explore.errRandomUser": "ランダムユーザーの取得に失敗しました", + // 表单校验 + "form.answer.content": "回答内容を入力してください", + "form.answer.contentTooLong": "回答内容は%d文字以内にしてください", + "form.email.required": "有効なメールアドレスを入力してください", + "form.followup.answer": "フォローアップの回答を入力してください", + "form.followup.answerTooLong": "フォローアップの回答は%d文字以内にしてください", + "form.followup.content": "フォローアップ内容を入力してください", + "form.followup.contentTooLong": "フォローアップ内容は%d文字以内にしてください", + "form.login.password": "パスワードを入力してください", + "form.profile.backgroundimagealpha": "QRコードの背景の不透明度を入力してください", + "form.profile.colorHex": "7文字の16進数カラーコード(#を含む)を入力してください", + "form.profile.dotscale": "QRコードの点の大きさを入力してください", + "form.profile.intro": "自己紹介は必須で、100文字以内である必要があります", + "form.profile.linkMax": "プロフィールリンクは30文字以内である必要があります", + "form.profile.name": "ニックネームは必須で、20文字以内である必要があります", + "form.profile.newPassword": "新しいパスワードは30文字以内である必要があります", + "form.profile.passwordMax": "パスワードは30文字以内である必要があります", + "form.question.content": "質問内容を入力してください", + "form.question.contentTooLong": "質問内容は%d文字以内にしてください", + "form.question.receiveReplyEmail": "返信を受け取るための有効なメールアドレスを入力してください", + "form.recaptcha": "CAPTCHA認証を完了してください", + "form.recover.newPassword": "8〜30文字の新しいパスワードを設定してください", + "form.recover.repeatPassword": "入力した2つの新しいパスワードが一致しません", + "form.register.domain": "カスタムドメインは半角英数字とアンダースコアのみ使用でき、3〜20文字である必要があります", + "form.register.email": "有効なメールアドレスを入力してください(100文字以内)", + "form.register.name": "ニックネームは必須で、20文字以内である必要があります", + "form.register.password": "8〜30文字のパスワードを設定してください", + "form.register.recaptcha": "CAPTCHAを読み込めません。ページを再読み込みするか、ネットワークを変更してお試しください", + "form.register.repeatPassword": "入力した2つのパスワードが一致しません", +} diff --git a/.workbuddy/backup/remove-vercount-20260906/dict_zh_cn.go b/.workbuddy/backup/remove-vercount-20260906/dict_zh_cn.go new file mode 100644 index 0000000..41e82d0 --- /dev/null +++ b/.workbuddy/backup/remove-vercount-20260906/dict_zh_cn.go @@ -0,0 +1,842 @@ +// Copyright 2022 E99p1ant. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package i18n + +// zhCNDict 简体中文文案。 +var zhCNDict = map[string]string{ + // 导航 + "nav.explore": "探索", + "nav.mine": "我的", + "nav.register": "注册", + "nav.login": "登录", + "nav.ask": "提问", + "nav.settings": "设置", + "nav.lang": "语言", + + // 通用 + "common.login": "登录", + "common.forgot": "忘记密码", + "common.submit": "提交", + "common.cancel": "取消", + "common.save": "保存", + "common.close": "关闭", + "common.delete": "删除", + "common.moveUp": "上移", + "common.moveDown": "下移", + "common.share": "分享", + "common.verifying": "验证中...", + "common.submitting": "提交中...", + "common.uploading": "上传中...", + "common.lang": "语言", + "common.menu": "菜单", + + "common.pleaseWait": "请稍候...", + "common.backHome": "返回首页", + "common.theme": "切换深色/浅色模式", + + // 站点 + "site.title": "狼的提问箱 | 匿名提问箱", + "site.description": "狼的提问箱——一个无需注册、可匿名提问的匿名提问箱,安全地倾诉与回应。", + "site.keywords": "匿名提问箱,提问箱,匿名提问,狼的提问箱,匿名问答,匿名提问网站", + "site.footer.visitor": "您是本站第 %s 位访客", + "site.footer.pageview": "本页面已被访问 %s 次", + "site.footer.runtime": "提问箱已成功存活", + "site.footer.admin": "找站长", + "site.footer.notice": "公告板", + "site.footer.chat": "聊天室", + "site.footer.drive": "盘", + "site.footer.status": "监控板", + "site.footer.nav": "导航页", + "site.maintenance": "本站正在维护中,请稍后再来~", + + // 认证 + "auth.login.title": "用户登录", + "auth.login.email": "电子邮箱", + "auth.login.password": "密码", + "auth.login.submit": "登录", + "auth.login.forgot": "忘记密码", + "auth.register.title": "用户注册", + "auth.register.name": "昵称", + "auth.register.email": "电子邮箱", + "auth.register.password": "密码", + "auth.register.submit": "注册", + "auth.forgot.title": "忘记密码", + "auth.forgot.email": "电子邮箱", + "auth.forgot.submit": "发送重置邮件", + "auth.forgot.submitBtn": "找回密码", + "auth.forgot.sent": "邮件已发送至 %s,请查收。", + "auth.recover.title": "重置密码", + "auth.recover.greeting": "%s,您正在重置您的密码:", + "auth.recover.password": "新密码", + "auth.recover.confirm": "确认密码", + "auth.recover.submit": "重置密码", + + // 探索页 + "explore.title": "探索用户标签", + "explore.notice": "此处展示已开启“在首页展示我的标签”的用户。该功能默认关闭,若希望自己的提问箱在这里被发现,请前往 个人设置 手动开启。", + "explore.shuffle": "换一批", + "explore.empty": "暂时没有发现可展示的用户,快去开启吧~", + "explore.hotVotes": "热门投票", + "explore.questionsTab": "提问", + "explore.votesTab": "投票", + "explore.votesTitle": "公开投票", + "explore.votesDesc": "浏览正在进行的公开投票,选择你感兴趣的主题参与。", + "explore.randomVote": "随机推荐", + "explore.allVotes": "全部投票", + "explore.noVotes": "暂时没有可参与的公开投票。", + + // 提问页 + "ask.scan": "扫码向@%s提问", + "ask.anonymous": "谁都可以以匿名的形式提问", + "ask.registerOnly": "仅允许注册用户向TA提问", + "ask.nextOpenAt": "下次开放时间:%s", + "ask.lastCloseAt": "上次关闭时间:%s", + "weekday.0": "周日", + "weekday.1": "周一", + "weekday.2": "周二", + "weekday.3": "周三", + "weekday.4": "周四", + "weekday.5": "周五", + "weekday.6": "周六", + "ask.pinned": "已置顶", + "ask.share": "分享提问箱", + "ask.placeholder": "在此处撰写你的问题...", + "ask.chatPlaceholder": "在此处撰写你的私聊消息...", + "ask.addImage": "添加图片", + "ask.private": "回复后不公开", + "ask.notify": "接收通知", + "ask.emailPlaceholder": "请输入你的邮箱地址", + "ask.submit": "发送提问", + "ask.chat": "发起私聊", + "ask.chatDisabled": "该提问箱未开启私聊", + "ask.chatLimit": "消息上限:%d条", + "ask.history": "@%s 以前回答过的问题", + "ask.loginRequired": "需要登录", + "ask.loginRequiredChat": "发起私聊需要登录账号", + "ask.goLogin": "去登录", + "ask.needPassword": "此提问箱需要密码", + "ask.needLogin": "提问箱的主人设置了仅注册用户才能提问,你需要先登录才能向他提问。", + "ask.pleaseEmail": "请填写接收通知的邮箱地址", + "ask.generating": "正在生成分享图…", + "ask.genFailed": "生成分享图失败,请稍后重试", + "ask.networkError": "网络错误", + "ask.shareFilename": "share.png", + + // 问题条目 + "qitem.answer": "回答", + "qitem.delete": "删除", + "qitem.private": "设为私密", + "qitem.public": "设为公开", + "qitem.asked": "提问", + "qitem.pending": "等待回答", + "qitem.answeredBy": "@%s 于 %s 的回答", + "qitem.danger": "危险!", + "qitem.deleteConfirm": "你确定要删除这个提问吗?该操作不可恢复,请谨慎操作。", + "qitem.deleteConfirmBtn": "确认删除", + "qitem.backToBox": "返回%s的提问箱", + "qitem.answerTitle": "回答问题", + "qitem.answerPlaceholder": "在此处撰写你的回答...", + "qitem.emailNotifyHint": "提问人留下了自己的电子邮箱,在你第一次回复该问题后,提问人将会收到一封邮件通知。", + "qitem.submitAnswer": "回答", + "qitem.updateAnswer": "更新回答", + "qitem.askMore": "再问点别的问题?", + "qitem.markRead": "标记为已读", + "qitem.markUnread": "标记为未读", + "qitem.readLocked": "已回答,已锁定为已读", + + // 个人设置页 + "admin.profile": "个人信息", + "admin.lang": "界面语言", + "admin.langHint": "选择访客访问你的提问箱时显示的语言(后台页面跟随此设置)", + "admin.email": "电子邮箱", + "admin.name": "昵称", + "admin.nameColor": "昵称颜色", + "admin.oldPassword": "旧密码", + "admin.newPassword": "新密码", + "admin.notify": "新提问通知", + "admin.personalize": "个性化设置", + "admin.intro": "提问箱介绍", + "admin.introColor": "提问箱介绍颜色", + "admin.qrBgColor": "二维码背景色(亮色部分)", + "admin.qrPdColor": "二维码探测图形颜色(边角方形)", + "admin.qrFgColor": "二维码前景色", + "admin.qrDotSize": "二维码点大小 (←小 大→)", + "admin.qrBgAlpha": "二维码背景透明度 (←不透明 很透明→)", + "admin.avatar": "个人头像", + "admin.background": "提问箱背景", + "admin.qrBackground": "二维码背景图", + "admin.social": "社交信息设置", + "admin.harassment": "防骚扰设置", + "admin.askLimit": "提问限制", + "admin.blockWords": "屏蔽词设置(回车或逗号分隔,最多支持 10 个屏蔽词,每个屏蔽词最大长度为 10)", + "admin.chat": "私聊设置", + "admin.chatMaxMessages": "私聊消息条数上限", + "admin.chatDailyLimit": "单用户每日私信条数上限", + "admin.tags": "标签设置", + "admin.tagsHint": "标签(最多5个,每个最多5字,回车或逗号分隔)", + "admin.tagsPlaceholder": "例如:动漫,音乐,游戏", + "admin.tagInputHint": "输入后按回车或逗号添加", + "admin.tagInputDuplicate": "已存在,已忽略", + "admin.pagePassword": "页面访问密码", + "admin.pagePasswordHint": "设置访问密码(留空则关闭)", + "admin.askPassword": "提问密码", + "admin.askPasswordHint": "设置提问密码(留空则关闭)", + "admin.saveProfile": "保存修改", + "admin.updated": "更新个人信息成功", + "admin.logout": "退出登录", + "admin.export": "导出数据", + + // 后台设置(补充) + "admin.oldPasswordHint": "修改密码使用,留空则不修改", + "admin.newPasswordHint": "6-64 位,留空则不修改", + "admin.notifyEmail": "邮件", + "admin.useCustomFg": "启用自定义前景色(否则自动匹配)", + "admin.customFgHint": "仅在启用时生效,否则颜色由系统自动生成", + "admin.avatarPlaceholder": "上传个人头像(头像比例不为1:1就扁了你哦)", + "admin.backgroundPlaceholder": "上传提问箱背景", + "admin.qrBackgroundPlaceholder": "上传二维码背景(颜色过深的背景会导致二维码无法识别)", + "admin.qrPreviewHint": "选择图片后预览将即时更新", + "admin.preview": "实时预览", + "admin.themePreset": "预设主题", + "admin.themeDefault": "默认", + "admin.themeDark": "暗夜", + "admin.themeOcean": "海洋", + "admin.themeSunset": "日落", + "admin.resetDefaults": "🔄 恢复默认值", + "admin.previewHint": "调整仅用于预览,最终效果点击「保存修改」后生效", + "admin.socialHint": "勾选后填写主页地址(无需 https://),未勾选的项将不会在前台显示。", + "admin.enableAcfun": "启用 AcFun", + "admin.enableBili": "启用 Bilibili", + "admin.enableWeibo": "启用 微博", + "admin.enableXiaohongshu": "启用 小红书", + "admin.enableBlog": "启用 个人博客", + "admin.registerOnly": "仅允许注册用户向我提问", + "admin.updateHarassment": "更新防骚扰设置", + "admin.globalBlockWords": "全局屏蔽词", + "admin.globalBlockWordsHint": "屏蔽词对提问、私聊(对方发给你的消息)与投票提交内容生效;三个设置页同步显示同一份设置,任意一处修改即时生效。", + "admin.updateBlockWords": "保存屏蔽词", + "admin.allowChat": "允许注册用户向我发起私聊", + "admin.dailyLimitHint": "0 表示不限制", + "admin.updateChat": "更新私聊设置", + "admin.showInHome": "在首页展示我的标签(允许其他用户通过标签搜索到我)", + "admin.updateTags": "更新标签", + "admin.updatePagePassword": "更新页面密码", + "admin.pagePasswordPlaceholder": "输入页面访问密码", + "admin.updateAskPassword": "更新提问密码", + "admin.askPasswordPlaceholder": "输入提问密码", + "admin.account": "账号设置", + "admin.exportHint": "您可以导出您在 %s 中的所有个人数据,包括你的基本信息、收到的问题以及回答。", + "admin.exportJSON": "导出全部数据(JSON)", + "admin.exportJSONHint": "包含提问、回答、追问与私聊记录,可完整备份或迁移。", + "admin.deactivate": "停用我的账号", + "admin.deactivateHint": "您随时可以选择停用您的账号。停用后,您的账号将无法登录,您的提问箱页面以及提问将无法访问,其他人也无法再给您发送新的提问。该操作无法撤销!请谨慎操作!", + "admin.socialRequired": "请填写链接或取消勾选", + + // 问题管理页 + "qlist.title": "我的提问", + "qlist.answered": "已回答", + "qlist.pending": "待回答", + "qlist.private": "私密", + "qlist.empty": "还没有提问,快把提问箱分享出去吧~", + "qlist.action": "操作", + "qlist.answerNow": "去回答", + "qlist.sort": "排序", + "qlist.status": "状态", + "qlist.type": "类型", + "qlist.latest": "最新", + "qlist.oldest": "最早", + "qlist.all": "全部", + "qlist.public": "一般提问", + "qlist.privateQuestion": "私密提问", + "qlist.received": "我收到的提问", + "qlist.myChats": "我发起的私聊", + "qlist.statsTotal": "总提问数", + "qlist.statsAnswered": "已回答", + "qlist.statsUnanswered": "未回答", + "qlist.statsPrivate": "私密提问", + "qlist.statsPinned": "置顶", + "qlist.statsChats": "私聊会话", + "qlist.searchPlaceholder": "搜索问题或回答内容...", + "qlist.searchBtn": "搜索", + "qlist.searchClear": "清除搜索", + "qlist.questionsTab": "提问", + "qlist.votesTab": "投票", + "qlist.chatsTab": "私聊", + "qlist.receivedChats": "我收到的私聊", + "qlist.chatEmpty": "还没有私聊,把提问箱分享出去让大家来私聊吧~", + "qlist.pinnedShort": "置顶", + "qlist.privateShort": "私密", + "qlist.chatStatsTotal": "总私聊", + "qlist.chatStatsReceived": "我收到的", + "qlist.chatStatsInitiated": "我发起的", + "qlist.chatStatsClosed": "已关闭", + "qlist.unansweredShort": "待答", + "qlist.readShort": "已读", + "qlist.unreadShort": "未读", + "qitem.markReadShort": "标记", + "qlist.pinShort": "置顶", + "qlist.unpinShort": "取消置顶", + "qlist.myVotes": "我创建的投票", + "qlist.loadMore": "加载更多", + "qlist.newVote": "新建投票", + "qlist.edit": "编辑", + "qlist.untitledVote": "未命名投票", + "qlist.blocked": "已拉黑", + "qlist.unanswered": "未回答", + + // 站点 Logo 后缀(emoji 随机动物 + 后缀组成,如 🐺 的提问箱) + "site.logoSuffix": "的提问箱", + "site.stats": "提问箱信息统计", + + // 验证码弹窗 + "captcha.title": "人机验证", + "captcha.hint": "请计算下方的算术题以验证你不是机器人", + "captcha.refresh": "换一题", + "captcha.submit": "验证并提交", + "captcha.loading": "加载中...", + "captcha.loadFailed": "加载失败", + "captcha.loadRetry": "加载失败,请重试", + "captcha.inputAnswer": "请输入答案", + "captcha.wrongAnswer": "答案错误,请重新计算", + "captcha.verifyFailed": "验证失败,请重试", + + // 注册页补充 + "auth.register.domain.prefix": "个性域名(你的问答箱网址将会是:%s + ", + "auth.register.domain.suffix": " + 你在下面文本框中填写的内容)", + "auth.register.confirm": "确认密码", + "auth.register.language": "界面语言", + "auth.register.languageDefault": "跟随站点默认", + "auth.register.success": "注册成功,欢迎来到 狼的提问箱!", + + // 探索页补充 + "explore.recommend": "随机推荐", + "explore.search": "搜索标签...", + "explore.sortLatest": "最新加入", + "explore.sortOldest": "最早加入", + "explore.noTags": "无标签", + "explore.noRecommend": "暂无推荐用户", + "explore.hotTags": "热门标签", + "explore.hotQuestions": "热门问题", + "explore.noResult": "暂无符合条件的用户。", + "explore.lazy": "这个人很懒,什么都没写~", + "explore.prevPage": "上一页", + "explore.nextPage": "下一页", + "explore.loading": "加载中...", + "explore.loadFailed": "加载失败", + + // 提问箱密码页 + "password.title": "此提问箱需要密码", + "password.desc": "此提问箱已设置访问密码,请输入密码查看内容", + "password.input": "请输入访问密码", + "password.enter": "验证进入", + "password.submit": "确认", + + // 问题不存在 + "qitem.notFound": "问题不存在或已被删除", + + // 聊天页 + "chat.title": "私聊", + "chat.input": "输入消息...", + "chat.send": "发送", + "chat.delete": "删除对话", + "chat.block": "拉黑对方", + "chat.unblock": "取消拉黑", + "chat.empty": "还没有消息,说点什么吧~", + "chat.withAsker": "与提问者的私聊", + "chat.withUser": "与 %s 的私聊", + "chat.closed": "对话已关闭", + "chat.closedNoSend": "对话已关闭,无法发送新消息", + "chat.remaining": "消息剩余:%d 条(共 %d 条)", + "chat.me": "我", + "chat.other": "对方", + "chat.userBlocked": "该用户已被拉黑", + "chat.addImage": "添加图片(可选,最大5MB)", + "chat.confirm": "确认", + "chat.confirmBlock": "确定要拉黑该用户吗?他将无法再发送私信。", + "chat.confirmUnblock": "确定要取消屏蔽该用户吗?他将可以再次发送私信。", + "chat.deleteConfirm": "确定要删除这条私聊对话吗?对方将无法再看到此对话,且操作不可恢复。", + "maintenance.title": "NekoBox 维护中...", + "maintenance.desc": "NekoBox 正在进行维护,服务暂时不可用。过一会再来吧~", + + // 单题分享(v4) + "qitem.shareCard": "分享此提问", + + // 追问(v4) + "followup.title": "追问记录", + "followup.content": "追问:", + "followup.answerPlaceholder": "回答这条追问...", + "followup.answerBtn": "回答追问", + "followup.waiting": "等待回答中...", + "followup.askMore": "继续追问", + "followup.remaining": "还可追问 %d / %d 次", + "followup.contentPlaceholder": "补充你的问题...", + "followup.waitingReply": "你的追加提问已发出,等待提问箱主人回复中...", + "followup.submit": "提交追问", + + // 置顶(v4) + "qlist.pinned": "置顶中", + "qlist.pin": "置顶", + "qlist.unpin": "取消置顶", + "qlist.readStatus": "阅读状态", + "qlist.read": "已读", + "qlist.unread": "未读", + "qlist.statsRead": "已读", + "qlist.statsUnread": "未读", + + // 提问成功页(v4) + "success.title": "提问发送成功!", + "success.yourQuestion": "你的提问:", + "success.emailSent": "已向你的邮箱发送了该提问的私密链接,也可以使用下方链接随时查看提问、接收回答与继续追问。", + "success.saveLink": "请务必保存下面的私密链接!由于你没有留下邮箱,此链接(含口令 Token)是你查看提问、接收回答与继续追问的唯一凭证,丢失后将无法找回。", + "success.privateLink": "你的提问私密链接(点击复制保存)", + "success.copy": "复制", + "success.copied": "已复制 ✓", + "success.copyFailed": "复制失败", + "success.token": "口令 Token:%s(提问人凭 Token 访问私密链接)", + "success.viewQuestion": "查看我的提问", + "success.backToBox": "返回%s的提问箱", + "success.pageTitle": "提问成功 - %s", + + // 停用页 + "deactivate.title": "账号停用", + "deactivate.desc": "停用后你的提问箱将无法访问,且所有数据将被删除,此操作不可恢复!", + "deactivate.confirm": "我确认要停用账号", + "deactivate.submit": "停用账号", + + // 匿名投票 + "nav.votes": "投票", + + "common.back": "返回", + + "vote.listTitle": "投票管理", + "vote.newTitle": "新建投票", + "vote.editTitle": "编辑投票", + "vote.resultsTitle": "投票结果", + "vote.statsTotal": "投票总数", + "vote.statsPublished": "进行中", + "vote.createNew": "新建投票", + "vote.typeSingle": "单选", + "vote.typeMultiple": "多选", + "vote.typeRating": "评分", + "vote.typeRank": "排序", + "vote.typeForm": "表单收集", + "vote.statusNotStarted": "未开始", + "vote.statusDraft": "草稿", + "vote.statusPublished": "进行中", + "vote.statusEnded": "已结束", + "vote.statusArchived": "已归档", + "vote.participants": "参与", + "vote.remainLabel": "剩余", + "vote.perpetual": "无限期", + "vote.durationDay": "天", + "vote.durationHour": "小时", + "vote.durationMinute": "分钟", + "vote.createdAt": "创建于", + "vote.edit": "编辑", + "vote.results": "结果", + "vote.preview": "预览", + "vote.page.mustComplete": "请完成", + "vote.page.rankHintRequired": "排名 1-N(必填)", + "vote.page.rankHintOptional": "排名 1-N(可选,可只排部分)", + "vote.page.previewHint": "预览模式:这是发布前的页面样例,提交已禁用", + "vote.page.previewSubmitHint": "预览模式,发布后即可提交", + "vote.publish": "发布", + "vote.close": "结束", + "vote.share": "复制链接", + "vote.copied": "已复制", + "vote.shareURL": "投票分享链接", + "vote.confirmPublish": "确定发布该投票吗?发布后即生成可分享链接。", + "vote.confirmClose": "确定结束该投票吗?结束后参与者将无法再投票。", + "vote.confirmDelete": "确定删除该投票吗?投票数据将一并删除,不可恢复!", + "vote.manageEntry": "管理投票", + + "vote.manageTitle": "管理投票", + "vote.manageDesc": "创建与管理你的匿名投票", "vote.empty": "还没有投票,点击右上角\"新建投票\"开始吧~", + "vote.formLockedHint": "该投票已发布,仅可修改基本信息;类型与选项已锁定(避免破坏已收集的投票数据)", + "vote.formBasic": "基本设置", + "vote.formTitle": "投票标题", + "vote.formTitlePlaceholder": "例如:今天吃什么?", + "vote.formDescription": "描述 / 说明", + "vote.formDescriptionPlaceholder": "补充说明(可选)", + "vote.formType": "投票类型", + "vote.formVisibility": "展示方式", + "vote.formVisibilityPublic": "公开展示(探索页 + 提问箱页)", + "vote.formVisibilityLinkOnly": "仅链接分享", + "vote.formVoterScope": "投票范围", + "vote.formVoterScopeAnyone": "任何人(含匿名)", + "vote.formVoterScopeRegistered": "仅注册用户", + "vote.formShowResult": "结果可见时机", + "vote.formShowResultAfterVote": "投票后立即可见", + "vote.formShowResultAfterEnd": "结束后可见", + "vote.formShowResultOwnerOnly": "仅我可见", + "vote.formStartAt": "开始时间(留空立即开始)", + "vote.formEndAt": "结束时间(留空不限制)", + "vote.formMaxChoices": "最多可选数量", + "vote.formMaxVotesPerFingerprint": "每个投票每个设备限 1 次,使用同一浏览器重复提交将被拦截。", + "vote.formMaxVotesPerAccount": "每个投票每个注册账号限 1 次,使用同一账号重复提交将被拦截。", + "vote.formAllowCustom": "允许参与者补充自定义选项", + "vote.formRequireCaptcha": "匿名投票需通过验证码", + "vote.formOptions": "投票选项", + "vote.formRatingHint": "评分题无需配置选项(1-10 分)", + "vote.addOption": "添加选项", + "vote.optionPlaceholder": "选项内容", + "vote.formFields": "表单字段", + "vote.addField": "添加字段", + "vote.field": "字段", + "vote.formFieldType": "字段类型", + "vote.fieldTypeText": "单行文本", + "vote.fieldTypeTextarea": "多行文本", + "vote.fieldTypeSelect": "下拉选择", + "vote.fieldTypeRadio": "单选", + "vote.fieldTypeCheckbox": "多选", + "vote.fieldTypeNumber": "数字", + "vote.fieldTypeRank": "排序", + "vote.page.selectPlaceholder": "请选择", + "vote.page.avgNumber": "平均", + "vote.page.fieldFilled": "已填写", + "vote.fieldTypeRating": "评分", + "vote.formFieldLabel": "字段标签", + "vote.formFieldRequired": "必填", + "vote.formFieldMaxLength": "字数上限(0 不限制)", + "vote.formFieldOptions": "选项", + "vote.formFieldOptionsHint": "点击下方「添加选项」逐条添加,无需用逗号分隔", + "vote.fieldAllowCustom": "允许补充自定义选项", + "vote.formFieldOptionsPlaceholder": "如:满意,一般,不满意", + "vote.createDraft": "保存草稿", + "vote.builderHintOptions": "至少需要 2 个选项", + "vote.builderHintFields": "请至少添加 1 个字段", + "vote.builderHintFieldOptions": "选择题/排序题请填写选项(排序题至少 2 个)", + "vote.resultsAnswered": "回答数", + "vote.page.rankedTimes": "被排次数", + "vote.resultsTotal": "总参与人数", + "vote.resultsSubmissions": "表单回复", + "vote.resultsViewTexts": "查看文本提交", + "vote.resultsToggleHint": "点击展开或收起文本回复明细", + "vote.resultsOptions": "投票结果", + "vote.resultsVotes": "票", + "vote.export": "导出 CSV", + "vote.resultsEmpty": "还没有人参与,快来分享吧~", + + // 匿名投票参与页 + "vote.inlineTitle": "投票", + "vote.page.by": "发起的投票", + "vote.page.goVote": "去投票", + "vote.page.notStarted": "未开始", + "vote.page.notStartedHint": "该投票尚未开始,敬请期待~", + "vote.page.ended": "已结束", + "vote.page.endedHint": "该投票已结束", + "vote.page.active": "进行中", + "vote.page.participated": "已参与", + "vote.page.loginRequired": "该投票仅限注册用户参与,请先登录。", + "vote.page.anonymousHint": "该投票允许匿名参与,你的身份不会被记录(同一设备最多可参与 1 次)。", + "vote.page.submit": "提交投票", + "vote.page.alreadyVoted": "你已参与过该投票,感谢参与!", + "vote.page.resultsTitle": "投票结果", + "vote.page.totalVotes": "总参与", + "vote.page.avgRating": "平均分", + "vote.page.avgRank": "平均排名", + "vote.page.optionColumn": "选项", + "vote.page.ratingLabel": "请评分", + "vote.page.ratingUnit": "分", + "vote.page.rankHint": "请为每个选项填写 1-N 的排名(数字越小越靠前,不可重复)", + "vote.page.rankPlaceholder": "排名", + "vote.page.multipleHint": "可多选", + "vote.page.customOption": "补充选项(可选)", + "vote.page.customPlaceholder": "填写你心中的其他选项", + "vote.page.customOther": "其他", + "vote.page.customFieldPlaceholder": "填写自定义选项…", + "vote.page.customStats": "自定义补充选项", + "vote.page.formCount": "已收到回复", + "vote.page.ownerManage": "管理此投票(仅你可见结果)", + + // 单份投票的定量收集 + "vote.formQuotaLimit": "收集上限(份)", + "vote.formQuotaHint": "0 表示不限制;收集到指定份数后自动结束该投票。", + "vote.flash.notFound": "投票不存在或已失效", + "vote.flash.notPublished": "该投票尚未发布", + "vote.flash.archived": "该投票已归档,无法参与", + "vote.flash.ended": "该投票已结束", + "vote.flash.notStarted": "该投票尚未开始", + "vote.flash.success": "投票成功!感谢参与", + "vote.err.noChoice": "请至少选择一个选项", + "vote.err.tooManyChoices": "最多只能选择 %d 个选项", + "vote.err.pickOne": "请选择一个选项", + "vote.err.invalidOption": "存在无效的选项", + "vote.err.optionDuplicate": "选项不能重复选择", + "vote.err.ratingRequired": "请给出 1-10 分的评分", + "vote.err.rankRequired": "请为每个选项填写 1-%d 的排名", + "vote.err.rankDuplicate": "排名不能重复", + "vote.err.fieldRequired": "请填写「%s」", + "vote.err.fieldCustomRequired": "请填写「%s」的自定义选项", + "vote.err.fieldCustomTooLong": "「%s」的自定义选项不能超过 %d 字", + "vote.err.fieldInvalidOption": "「%s」的选项无效", + "vote.err.fieldConfigError": "「%s」选项配置错误", + "vote.err.fieldRankIncomplete": "请填写「%s」的完整排名", + "vote.err.fieldRankRange": "「%s」排名必须是 1-%d 之间的数字", + "vote.err.fieldRankDuplicate": "「%s」排名不能重复", + "vote.err.fieldTooLong": "「%s」超出字数限制(最多 %d 字)", + "vote.err.fieldNotNumber": "「%s」必须是数字", + "vote.err.fieldRating": "「%s」请给出 1-10 分的评分", + "vote.err.unknownType": "未知的投票类型", + "vote.err.customTooLong": "自定义选项不能超过 %d 字", + "vote.err.blockWords": "提交内容中包含了投票发起人设置的屏蔽词,无法提交", + "vote.err.serialize": "提交内容序列化失败", + "captcha.error": "验证码错误", + "captcha.errEmpty": "验证码不能为空", + "captcha.errFormat": "验证码格式错误,请刷新后重试", + "captcha.errAnswerFormat": "验证码答案格式错误", + "captcha.errExpired": "验证码已过期,请刷新后重试", + "captcha.errService": "验证码服务异常,请稍后重试", + "captcha.errAnswerWrong": "验证码答案错误", + "censor.unavailable": "内容审核服务暂时不可用,请稍后再试", + + // 功能管理:提问 / 私聊 / 投票 的开关、定时开关与定量收集 + "admin.tabAccount": "用户设置", + "admin.featuresHint": "在「提问 / 私聊 / 投票」面板中管理各功能的开关、开放时段与收集上限;达到上限后对应功能自动关闭。", + "admin.featuresEntry": "设置", + "admin.featureHint.question": "关闭后访客无法提交新提问;达到收集上限后自动停止接收(含私密提问)。", + "admin.featureHint.chat": "关闭后访客无法发起新的私聊;已有会话不受影响。", + "admin.featureHint.vote": "关闭后提问箱页不再展示投票板块,访客也无法参与你的投票。单份投票可在编辑页单独设置收集上限。", + "admin.featureEnabled": "开启该功能", + "admin.featureOpenAt": "定时开放", + "admin.featureCloseAt": "定时关闭", + "admin.featureTimeHint": "留空表示不限制;开放时间之前、关闭时间之后该功能均不可用。", + "admin.featureSchedule": "循环定时(每周循环)", + "admin.featureScheduleStart": "每日开始", + "admin.featureScheduleEnd": "每日结束", + "admin.featureScheduleHint": "勾选每周固定开放日并设置每日时间段,按周循环生效;全部留空表示不启用。只填开放日(不填时间)表示该天全天开放;只填时间(不勾选任何日)表示每天该时段开放。", + "admin.featureMode": "收集模式", + "admin.featureModeAlways": "长期开放", + "admin.featureModeWindow": "定时收集", + "admin.featureModeRecurring": "循环定时", + "admin.featureModeHint": "长期开放:不限时间,保持开放直到手动关闭;定时收集:一次性开放 / 关闭时间窗口;循环定时:每周固定开放日循环生效。三种模式互斥,保存时未选模式的设置将被清空。", + "admin.featureQuotaLimit": "收集上限(条 / 份)", + "admin.featureQuotaHint": "0 表示不限制;累计收集达到上限后自动关闭,重置计数后可开启下一轮。", + "admin.featureQuotaCount": "已收集", + "admin.featureQuotaUnlimited": "不限制", + "admin.featureQuotaResetAt": "上次重置:", + "admin.featureResetQuota": "重置计数", + "admin.featureSave": "保存该功能设置", + + // 功能管理:状态与提示 + "feature.name.question": "提问", + "feature.name.chat": "私聊", + "feature.name.vote": "投票", + "feature.statusOpen": "进行中", + "feature.status.disabled": "已关闭", + "feature.status.not_started": "未开始", + "feature.status.ended": "已结束", + "feature.status.quota_reached": "已达上限", + "feature.saved": "功能设置已保存", + "feature.errTime": "%s 的时间格式不正确,请使用日期时间选择器", + "feature.errTimeRange": "%s 的定时关闭时间必须晚于定时开放时间", + "feature.errQuota": "%s 的收集上限需为 0 到 1000000 之间的整数", + "feature.errInvalid": "未知的功能类型", + "feature.quotaReset": "%s 的收集计数已重置", + "feature.errScheduleHalf": "%s 的循环定时开始与结束时间必须同时填写", + "feature.errScheduleTime": "%s 的循环定时时间格式不正确,请使用时间选择器", + "feature.errScheduleRange": "%s 的循环定时结束时间必须晚于开始时间", + + // 功能关闭时对访客的提示 + "feature.disabled.question": "提问箱主人已关闭提问功能", + "feature.disabled.chat": "该提问箱未开启私聊功能", + "feature.disabled.vote": "该提问箱已关闭投票功能", + "feature.not_started.question": "提问功能尚未开放", + "feature.not_started.chat": "私聊功能尚未开放", + "feature.not_started.vote": "投票功能尚未开放", + "feature.ended.question": "提问功能已结束收集", + "feature.ended.chat": "私聊功能已结束", + "feature.ended.vote": "投票功能已结束", + "feature.quota_reached.question": "提问功能已达到收集上限", + "feature.quota_reached.chat": "私聊功能已达到收集上限", + "feature.quota_reached.vote": "投票功能已达到收集上限", + + // 认证:访客可见的提示(P0) + "auth.errBadCredential": "邮箱或密码错误", + "auth.errUserNotFound": "用户不存在", + "auth.errEmailFrequent": "邮件发送太频繁,请稍后再试", + "auth.errEmailFailed": "邮件发送失败,请稍后再试", + "auth.errCodeExpired": "验证码已过期", + "auth.errPasswordChanged": "密码修改成功", + "auth.errLoginRequired": "请先登录", + "auth.errDuplicateEmail": "该邮箱已被注册", + "auth.errDuplicateDomain": "该个性域名已被使用", + + // 提问箱页:访客可见(P0) + "question.errWrongPassword": "密码错误", + "question.errAskPasswordWrong": "提问密码错误", + "question.errLoginRequired": "提问箱的主人设置了仅注册用户才能提问,请先登录。", + "question.errSelfChat": "不能给自己发起私聊", + "question.errBlockedChat": "你已被对方拉黑,无法发起私聊", + "question.errChatDailyLimit": "你今日已达发送上限(%d条),请明天再试", + "question.errDuplicateSubmit": "检测到重复提交,请稍后再试", + "question.errImageTooLarge": "图片文件大小不能大于 5Mb", + "question.errUploadImageFailed": "上传图片失败", + "question.errBlockWords": "提问内容中包含了提问箱主人设置的屏蔽词,发送失败", + "question.errAnsweredLocked": "已回答的问题不能标记为未读", + "question.flash.chatSent": "私聊请求已发送!你可以通过以下链接查看对话:%s", + "question.flash.answered": "回答发布成功!", + "question.flash.operated": "操作成功", + "question.flash.markedRead": "已标记为已读", + "question.flash.markedUnread": "已标记为未读", + + // 追问:访客/箱主可见(P0) + "followup.errNoPermission": "无权操作", + "followup.errNotAnswered": "问题尚未被回答,暂时不能追问", + "followup.errLimit": "每条提问最多追问 %d 次,已达上限", + "followup.errWaitReply": "请等待主人回复上一条追问后再继续追问", + "followup.errNotFound": "追问不存在", + "followup.flash.sent": "追问发送成功!", + "followup.flash.answered": "回答成功!", + + // 后台设置:箱主可见(P1) + "admin.flash.profileUpdated": "更新个人信息成功", + "admin.flash.chatUpdated": "私聊设置更新成功", + "admin.flash.pagePwdOff": "已关闭页面密码访问", + "admin.flash.pagePwdUpdated": "页面访问密码已更新", + "admin.flash.askPwdOff": "已关闭提问密码", + "admin.flash.askPwdUpdated": "提问密码已更新", + "admin.flash.tagsTooMany": "标签不能超过 5 个", + "admin.flash.tagsUpdated": "更新标签成功", + "admin.flash.tagTooLong": "每个标签不能超过 5 个字符:%s", + "admin.flash.blockWordsTooMany": "屏蔽词不能超过 10 个", + "admin.flash.blockWordTooLong": "屏蔽词长度不能超过 10 个字符:%s", + "admin.flash.harassmentUpdated": "更新防骚扰设置成功", + "admin.flash.blockWordsUpdated": "屏蔽词已更新", + "admin.flash.deactivated": "您的账号已停用,感谢您使用 狼的提问箱。期待未来还能再见 👋🏻", + "admin.export.errUser": "导出失败:获取用户信息失败", + "admin.export.errQuestion": "导出失败:获取问题信息失败", + + // 投票管理:箱主可见(P1) + "vote.err.invalidID": "无效的投票 ID", + "vote.err.notExist": "投票不存在", + "vote.err.noPermission": "无权操作该投票", + "vote.err.type": "未知的投票类型", + "vote.err.titleRequired": "请填写投票标题", + "vote.err.titleTooLong": "投票标题不能超过 100 字", + "vote.err.visibility": "未知的可见性设置", + "vote.err.voterScope": "未知的投票范围设置", + "vote.err.showResult": "未知的结果展示设置", + "vote.err.timeRange": "结束时间必须晚于开始时间", + "vote.err.quota": "收集上限需为 0 到 1000000 之间的整数", + "vote.err.optionsTooFew": "请至少添加 2 个选项", + "vote.err.optionsFormat": "选项数据格式错误", + "vote.err.optionTooLong": "单个选项内容不能超过 %d 字", + "vote.err.maxChoicesExceed": "最多可选数量不能超过选项总数", + "vote.err.fieldsTooFew": "请至少添加 1 个表单字段", + "vote.err.fieldsFormat": "表单字段数据格式错误", + "vote.err.fieldsTooMany": "表单字段不能超过 50 个", + "vote.err.fieldType": "存在未知的表单字段类型", + "vote.err.fieldLabelMissing": "第 %d 个字段缺少标签", + "vote.err.fieldLabelTooLong": "第 %d 个字段的标签不能超过 100 字", + "vote.err.fieldLabelDuplicate": "字段标签「%s」重复,请使用不同的标签", + "vote.err.fieldMaxLength": "第 %d 个字段的字数上限需在 0-2000 之间", + "vote.err.fieldReservedOption": "第 %d 个字段(%s)的选项内容包含保留值,请更换", + "vote.err.fieldOptionsTooFew": "第 %d 个字段(%s)请至少填写 1 个选项", + "vote.err.fieldRankTooFew": "第 %d 个字段(%s)排序题至少需要 2 个选项", + "vote.err.fieldSerialize": "字段选项序列化失败", + "vote.err.editClosed": "已结束或归档的投票不允许编辑", + "vote.err.publishDraftOnly": "只有草稿状态的投票可以发布", + "vote.err.closePublishedOnly": "只有进行中的投票可以结束", + "vote.err.publishTitleRequired": "请先填写投票标题再发布", + "vote.err.publishFormFieldsRequired": "表单类型投票至少需要 1 个字段", + "vote.err.publishOptionsRequired": "选项投票至少需要 2 个选项", + "vote.err.optionUpdateFailed": "选项更新失败,请重试", + "vote.err.fieldUpdateFailed": "表单字段更新失败,请重试", + "vote.flash.published": "投票已发布!分享链接:%s", + "vote.flash.updated": "投票信息已更新(进行中的投票不可修改类型与选项)", + "vote.flash.draftSaved": "已保存到草稿", + "vote.flash.closed": "投票已结束", + "vote.flash.deleted": "投票已删除", + "vote.err.timeFormat": "时间格式不正确,请使用日期时间选择器", + "vote.err.maxChoicesLimit": "最多可选数量不能超过 20", + "vote.err.descriptionTooLong": "投票描述不能超过 5000 字", + "profile.err.avatarTooLarge": "头像文件太大,最大支持 2MB", + "profile.err.backgroundTooLarge": "背景图文件太大,最大支持 2MB", + "profile.err.newPasswordTooShort": "新密码长度不能少于 8 位", + "profile.err.badOldPassword": "旧密码输入错误", + "admin.export.errCreateExcel": "导出失败:创建Excel文件失败", + "admin.export.errWriteExcel": "导出失败:写入Excel文件失败", + + // profile 设置页 UI 重构:卡片分区 / 头像背景裁剪 / 改密强制登出 + "profile.card.basic": "基础资料", + "profile.card.avatarBg": "头像与背景", + "profile.card.qr": "二维码样式", + "profile.card.password": "修改密码", + "profile.avatarCropHint": "裁剪框已锁定为 1:1 正方形", + "profile.backgroundCropHint": "裁剪框已锁定为 16:9 宽屏", + "profile.cropTitle": "裁剪图片", + "profile.cropConfirm": "确认裁剪", + "profile.err.avatarRatio": "头像图片必须为 1:1 正方形,请裁剪后再上传", + "profile.err.backgroundRatio": "背景图片必须为 16:9 宽高比,请裁剪后再上传", + "profile.flash.passwordChanged": "密码修改成功,已退出登录,请使用新密码重新登录", + "profile.pwdNote": "保存后当前会话立即失效,需使用新密码重新登录", + "profile.pickAvatar": "更换头像", + "profile.pickBackground": "更换背景", + "profile.saveBasic": "保存基础资料", + "profile.saveMedia": "保存头像与背景", + "profile.savePassword": "确认修改密码", + "profile.saveQR": "保存二维码样式", + "profile.saveSocial": "保存社交链接", + "profile.err.cropLibUnavailable": "图片裁剪组件加载失败,请刷新页面后重试", + + // 验证码 JSON 接口:前端消费(P2) + "captcha.json.badRequest": "请求格式错误", + "captcha.json.idRequired": "验证码 ID 不能为空", + "captcha.json.genFailed": "生成验证码失败", + "chat.errDeletedByOwner": "该私聊对话已被对方删除。", + "chat.errBlockedView": "你已被对方拉黑,无法查看该私聊", + "chat.errEmpty": "消息不能为空", + "chat.errTooLong": "消息内容过长,请控制在 %d 字以内", + "chat.errConvDeletedSend": "该对话已被删除,无法发送消息", + "chat.errBlockedSend": "你已被对方拉黑,无法发送私信", + "chat.errBlockWords": "消息中包含了提问箱主人设置的屏蔽词,无法发送", + "chat.errDailyLimit": "你今日已达发送上限(%d条),请明天再试", + "chat.errLimitReached": "对话已达到上限,无法发送新消息", + "chat.errChatClosed": "该箱主已关闭私聊功能,无法发送消息", + "chat.errImageTooLarge": "图片大小不能超过 %dMB", + "chat.errUploadFailed": "图片上传失败,请稍后重试", + "chat.errNoPermission": "无权执行此操作", + "chat.flash.sent": "消息发送成功", + "chat.flash.deleted": "私聊对话已删除", + "chat.flash.operated": "操作成功", + "image.errTooLarge": "图片尺寸过大,最大支持 %d×%d 像素,请压缩后重新上传", + "internal.errFlash": "服务内部错误,请稍后重试。", + "internal.errServer": "服务器内部错误", + "home.errStats": "获取统计信息失败", + "pixel.errPath": "非法的请求路径", + "proxy.errNotFound": "页面不存在", + "proxy.errInternal": "服务网关内部错误", + "explore.errRandomUser": "获取随机用户失败", + // 表单校验 + "form.answer.content": "回答内容不能为空", + "form.answer.contentTooLong": "回答内容不能超过 %d 个字符", + "form.email.required": "请填写有效的电子邮箱地址", + "form.followup.answer": "追问回答不能为空", + "form.followup.answerTooLong": "追问回答不能超过 %d 个字符", + "form.followup.content": "追问内容不能为空", + "form.followup.contentTooLong": "追问内容不能超过 %d 个字符", + "form.login.password": "请输入密码", + "form.profile.backgroundimagealpha": "请填写二维码背景透明度", + "form.profile.colorHex": "请填写 7 位十六进制颜色代码(含 #)", + "form.profile.dotscale": "请填写二维码点大小", + "form.profile.intro": "个人介绍不能为空,且不超过 100 个字符", + "form.profile.linkMax": "主页链接长度不能超过 30 个字符", + "form.profile.name": "昵称不能为空,且不超过 20 个字符", + "form.profile.newPassword": "新密码长度不能超过 30 个字符", + "form.profile.passwordMax": "密码长度不能超过 30 个字符", + "form.question.content": "问题内容不能为空", + "form.question.contentTooLong": "问题内容不能超过 %d 个字符", + "form.question.receiveReplyEmail": "请填写有效的电子邮箱地址以接收回复", + "form.recaptcha": "请完成人机验证", + "form.recover.newPassword": "请设置 8 至 30 位的新密码", + "form.recover.repeatPassword": "两次输入的新密码不一致", + "form.register.domain": "个性域名只能包含字母、数字和下划线,长度为 3 至 20 位", + "form.register.email": "请填写有效的电子邮箱地址(不超过 100 个字符)", + "form.register.name": "昵称不能为空,且不超过 20 个字符", + "form.register.password": "请设置 8 至 30 位的密码", + "form.register.recaptcha": "无法加载人机验证,请刷新页面或切换网络后重试", + "form.register.repeatPassword": "两次输入的密码不一致", +} diff --git a/.workbuddy/backup/remove-vercount-20260906/dict_zh_tw.go b/.workbuddy/backup/remove-vercount-20260906/dict_zh_tw.go new file mode 100644 index 0000000..6f3acf4 --- /dev/null +++ b/.workbuddy/backup/remove-vercount-20260906/dict_zh_tw.go @@ -0,0 +1,842 @@ +// Copyright 2022 E99p1ant. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package i18n + +// zhTWDict 繁體中文文案。 +var zhTWDict = map[string]string{ + // 導覽 + "nav.explore": "探索", + "nav.mine": "我的", + "nav.register": "註冊", + "nav.login": "登入", + "nav.ask": "提問", + "nav.settings": "設定", + "nav.lang": "語言", + + // 通用 + "common.login": "登入", + "common.forgot": "忘記密碼", + "common.submit": "送出", + "common.cancel": "取消", + "common.save": "儲存", + "common.close": "關閉", + "common.delete": "刪除", + "common.moveUp": "上移", + "common.moveDown": "下移", + "common.share": "分享", + "common.verifying": "驗證中...", + "common.submitting": "提交中...", + "common.uploading": "上傳中...", + "common.lang": "語言", + "common.menu": "選單", + + "common.pleaseWait": "請稍候...", + "common.backHome": "返回首頁", + "common.theme": "切換深色/淺色模式", + + // 站點 + "site.title": "狼的提問箱 | 匿名提問箱", + "site.description": "狼的提問箱——一個無需註冊、可匿名提問的匿名提問箱,安全地傾訴與回應。", + "site.keywords": "匿名提問箱,提問箱,匿名提問,狼的提問箱,匿名問答,匿名提問網站", + "site.footer.visitor": "您是本站第 %s 位訪客", + "site.footer.pageview": "本頁面已被訪問 %s 次", + "site.footer.runtime": "提問箱已成功存活", + "site.footer.admin": "找站長", + "site.footer.notice": "公告板", + "site.footer.chat": "聊天室", + "site.footer.drive": "盤", + "site.footer.status": "監控板", + "site.footer.nav": "導航頁", + "site.maintenance": "本站正在維護中,請稍後再來~", + + // 認證 + "auth.login.title": "使用者登入", + "auth.login.email": "電子郵件", + "auth.login.password": "密碼", + "auth.login.submit": "登入", + "auth.login.forgot": "忘記密碼", + "auth.register.title": "使用者註冊", + "auth.register.name": "暱稱", + "auth.register.email": "電子郵件", + "auth.register.password": "密碼", + "auth.register.submit": "註冊", + "auth.forgot.title": "忘記密碼", + "auth.forgot.email": "電子郵件", + "auth.forgot.submit": "傳送重設郵件", + "auth.forgot.submitBtn": "找回密碼", + "auth.forgot.sent": "郵件已傳送至 %s,請查收。", + "auth.recover.title": "重設密碼", + "auth.recover.greeting": "%s,您正在重設您的密碼:", + "auth.recover.password": "新密碼", + "auth.recover.confirm": "確認密碼", + "auth.recover.submit": "重設密碼", + + // 探索頁 + "explore.title": "探索使用者標籤", + "explore.notice": "此處展示已開啟「在首頁展示我的標籤」的使用者。該功能預設關閉,若希望自己的提問箱在這裡被發現,請前往 個人設定 手動開啟。", + "explore.shuffle": "換一批", + "explore.empty": "暫時沒有可展示的使用者,快去開啟吧~", + "explore.hotVotes": "熱門投票", + "explore.questionsTab": "提問", + "explore.votesTab": "投票", + "explore.votesTitle": "公開投票", + "explore.votesDesc": "瀏覽進行中的公開投票,選擇你感興趣的主題參與。", + "explore.randomVote": "隨機推薦", + "explore.allVotes": "全部投票", + "explore.noVotes": "暫時沒有可參與的公開投票。", + + // 提問頁 + "ask.scan": "掃碼向@%s提問", + "ask.anonymous": "誰都可以以匿名的方式提問", + "ask.registerOnly": "僅允許註冊使用者向TA提問", + "ask.nextOpenAt": "下次開放時間:%s", + "ask.lastCloseAt": "上次關閉時間:%s", + "weekday.0": "週日", + "weekday.1": "週一", + "weekday.2": "週二", + "weekday.3": "週三", + "weekday.4": "週四", + "weekday.5": "週五", + "weekday.6": "週六", + "ask.pinned": "已置頂", + "ask.share": "分享提問箱", + "ask.placeholder": "在此處撰寫你的問題...", + "ask.chatPlaceholder": "在此處撰寫你的私聊訊息...", + "ask.addImage": "新增圖片", + "ask.private": "回覆後不公開", + "ask.notify": "接收通知", + "ask.emailPlaceholder": "請輸入你的信箱地址", + "ask.submit": "傳送提問", + "ask.chat": "發起私聊", + "ask.chatDisabled": "該提問箱未開啟私聊", + "ask.chatLimit": "訊息上限:%d則", + "ask.history": "@%s 以前回答過的問題", + "ask.loginRequired": "需要登入", + "ask.loginRequiredChat": "發起私聊需要登入帳號", + "ask.goLogin": "去登入", + "ask.needPassword": "此提問箱需要密碼", + "ask.needLogin": "提問箱的主人設定了僅註冊使用者才能提問,你需要先登入才能向他提問。", + "ask.pleaseEmail": "請填寫接收通知的信箱地址", + "ask.generating": "正在產生分享圖…", + "ask.genFailed": "產生分享圖失敗,請稍後重試", + "ask.networkError": "網路錯誤", + "ask.shareFilename": "share.png", + + // 問題條目 + "qitem.answer": "回答", + "qitem.delete": "刪除", + "qitem.private": "設為私密", + "qitem.public": "設為公開", + "qitem.asked": "提問", + "qitem.pending": "等待回答", + "qitem.answeredBy": "@%s 於 %s 的回答", + "qitem.danger": "危險!", + "qitem.deleteConfirm": "你確定要刪除這個提問嗎?該操作不可恢復,請謹慎操作。", + "qitem.deleteConfirmBtn": "確認刪除", + "qitem.backToBox": "返回%s的提問箱", + "qitem.answerTitle": "回答問題", + "qitem.answerPlaceholder": "在此處撰寫你的回答...", + "qitem.emailNotifyHint": "提問人留下了自己的電子郵箱,在你第一次回覆該問題後,提問人將會收到一封郵件通知。", + "qitem.submitAnswer": "回答", + "qitem.updateAnswer": "更新回答", + "qitem.askMore": "再問點別的問題?", + "qitem.markRead": "標記為已讀", + "qitem.markUnread": "標記為未讀", + "qitem.readLocked": "已回答,已鎖定為已讀", + + // 個人設定頁 + "admin.profile": "個人資訊", + "admin.lang": "介面語言", + "admin.langHint": "選擇訪客訪問你的提問箱時顯示的語言(後台頁面跟隨此設定)", + "admin.email": "電子郵件", + "admin.name": "暱稱", + "admin.nameColor": "暱稱顏色", + "admin.oldPassword": "舊密碼", + "admin.newPassword": "新密碼", + "admin.notify": "新提問通知", + "admin.personalize": "個人化設定", + "admin.intro": "提問箱介紹", + "admin.introColor": "提問箱介紹顏色", + "admin.qrBgColor": "QR Code 背景色(亮色部分)", + "admin.qrPdColor": "QR Code 探測圖形顏色(邊角方形)", + "admin.qrFgColor": "QR Code 前景色", + "admin.qrDotSize": "QR Code 點大小 (←小 大→)", + "admin.qrBgAlpha": "QR Code 背景透明度 (←不透明 很透明→)", + "admin.avatar": "個人頭像", + "admin.background": "提問箱背景", + "admin.qrBackground": "QR Code 背景圖", + "admin.social": "社交資訊設定", + "admin.harassment": "防騷擾設定", + "admin.askLimit": "提問限制", + "admin.blockWords": "屏蔽詞設定(Enter 或逗號分隔,最多支援 10 個屏蔽詞,每個屏蔽詞最大長度為 10)", + "admin.chat": "私聊設定", + "admin.chatMaxMessages": "私聊訊息條數上限", + "admin.chatDailyLimit": "單一使用者每日私訊條數上限", + "admin.tags": "標籤設定", + "admin.tagsHint": "標籤(最多5個,每個最多5字,Enter 或逗號分隔)", + "admin.tagsPlaceholder": "例如:動漫,音樂,遊戲", + "admin.tagInputHint": "輸入後按 Enter 或逗號新增", + "admin.tagInputDuplicate": "已存在,已忽略", + "admin.pagePassword": "頁面訪問密碼", + "admin.pagePasswordHint": "設定訪問密碼(留空則關閉)", + "admin.askPassword": "提問密碼", + "admin.askPasswordHint": "設定提問密碼(留空則關閉)", + "admin.saveProfile": "儲存修改", + "admin.updated": "更新個人資訊成功", + "admin.logout": "登出", + "admin.export": "匯出資料", + + // 後台設定(補充) + "admin.oldPasswordHint": "修改密碼使用,留空則不修改", + "admin.newPasswordHint": "6-64 位,留空則不修改", + "admin.notifyEmail": "郵件", + "admin.useCustomFg": "啟用自訂前景色(否則自動比對)", + "admin.customFgHint": "僅在啟用時生效,否則顏色由系統自動產生", + "admin.avatarPlaceholder": "上傳個人頭像(頭像比例不為1:1就扁了你哦)", + "admin.backgroundPlaceholder": "上傳提問箱背景", + "admin.qrBackgroundPlaceholder": "上傳QR Code背景(顏色過深的背景會導致QR Code無法辨識)", + "admin.qrPreviewHint": "選擇圖片後預覽將即時更新", + "admin.preview": "即時預覽", + "admin.themePreset": "預設主題", + "admin.themeDefault": "預設", + "admin.themeDark": "暗夜", + "admin.themeOcean": "海洋", + "admin.themeSunset": "日落", + "admin.resetDefaults": "🔄 恢復預設值", + "admin.previewHint": "調整僅用於預覽,最終效果點擊「儲存修改」後生效", + "admin.socialHint": "勾選後填寫主頁地址(無需 https://),未勾選的項目將不會在前台顯示。", + "admin.enableAcfun": "啟用 AcFun", + "admin.enableBili": "啟用 Bilibili", + "admin.enableWeibo": "啟用 微博", + "admin.enableXiaohongshu": "啟用 小紅書", + "admin.enableBlog": "啟用 個人部落格", + "admin.registerOnly": "僅允許註冊使用者向我提問", + "admin.updateHarassment": "更新防騷擾設定", + "admin.globalBlockWords": "全局屏蔽詞", + "admin.globalBlockWordsHint": "屏蔽詞對提問、私聊(對方傳送給你的訊息)與投票提交內容生效;三個設定頁同步顯示同一份設定,任意一處修改即時生效。", + "admin.updateBlockWords": "儲存屏蔽詞", + "admin.allowChat": "允許註冊使用者向我發起私聊", + "admin.dailyLimitHint": "0 表示不限制", + "admin.updateChat": "更新私聊設定", + "admin.showInHome": "在首頁展示我的標籤(允許其他使用者透過標籤搜尋到我)", + "admin.updateTags": "更新標籤", + "admin.updatePagePassword": "更新頁面密碼", + "admin.pagePasswordPlaceholder": "輸入頁面訪問密碼", + "admin.updateAskPassword": "更新提問密碼", + "admin.askPasswordPlaceholder": "輸入提問密碼", + "admin.account": "帳號設定", + "admin.exportHint": "您可以匯出您在 %s 中的所有個人資料,包括你的基本資訊、收到的問題以及回答。", + "admin.exportJSON": "導出全部資料(JSON)", + "admin.exportJSONHint": "包含提問、回答、追問與私聊記錄,可完整備份或遷移。", + "admin.deactivate": "停用我的帳號", + "admin.deactivateHint": "您隨時可以選擇停用您的帳號。停用後,您的帳號將無法登入,您的提問箱頁面以及提問將無法訪問,其他人也無法再給您發送新的提問。該操作無法撤銷!請謹慎操作!", + "admin.socialRequired": "請填寫連結或取消勾選", + + // 問題管理頁 + "qlist.title": "我的提問", + "qlist.answered": "已回答", + "qlist.pending": "待回答", + "qlist.private": "私密", + "qlist.empty": "還沒有提問,快把提問箱分享出去吧~", + "qlist.action": "操作", + "qlist.answerNow": "去回答", + "qlist.sort": "排序", + "qlist.status": "狀態", + "qlist.type": "類型", + "qlist.latest": "最新", + "qlist.oldest": "最早", + "qlist.all": "全部", + "qlist.public": "一般提問", + "qlist.privateQuestion": "私密提問", + "qlist.received": "我收到的提問", + "qlist.myChats": "我發起的私聊", + "qlist.statsTotal": "總提問數", + "qlist.statsAnswered": "已回答", + "qlist.statsUnanswered": "未回答", + "qlist.statsPrivate": "私密提問", + "qlist.statsPinned": "置頂", + "qlist.statsChats": "私聊會話", + "qlist.searchPlaceholder": "搜尋問題或回答內容...", + "qlist.searchBtn": "搜尋", + "qlist.searchClear": "清除搜尋", + "qlist.questionsTab": "提問", + "qlist.votesTab": "投票", + "qlist.chatsTab": "私聊", + "qlist.receivedChats": "我收到的私聊", + "qlist.chatEmpty": "還沒有私聊,把提問箱分享出去讓大家來私聊吧~", + "qlist.pinnedShort": "置頂", + "qlist.privateShort": "私密", + "qlist.chatStatsTotal": "總私聊", + "qlist.chatStatsReceived": "我收到的", + "qlist.chatStatsInitiated": "我發起的", + "qlist.chatStatsClosed": "已關閉", + "qlist.unansweredShort": "待答", + "qlist.readShort": "已讀", + "qlist.unreadShort": "未讀", + "qitem.markReadShort": "標記", + "qlist.pinShort": "置頂", + "qlist.unpinShort": "取消置頂", + "qlist.myVotes": "我建立的投票", + "qlist.loadMore": "載入更多", + "qlist.newVote": "新建投票", + "qlist.edit": "編輯", + "qlist.untitledVote": "未命名投票", + "qlist.blocked": "已拉黑", + "qlist.unanswered": "未回答", + + // 站點 Logo 後綴 + "site.logoSuffix": "的提問箱", + "site.stats": "提問箱資訊統計", + + // 驗證碼彈窗 + "captcha.title": "人機驗證", + "captcha.hint": "請計算下方的算術題以驗證你不是機器人", + "captcha.refresh": "換一題", + "captcha.submit": "驗證並送出", + "captcha.loading": "載入中...", + "captcha.loadFailed": "載入失敗", + "captcha.loadRetry": "載入失敗,請重試", + "captcha.inputAnswer": "請輸入答案", + "captcha.wrongAnswer": "答案錯誤,請重新計算", + "captcha.verifyFailed": "驗證失敗,請重試", + + // 註冊頁補充 + "auth.register.domain.prefix": "個性網域(你的問答箱網址將會是:%s + ", + "auth.register.domain.suffix": " + 你在下面文字框中填寫的內容)", + "auth.register.confirm": "確認密碼", + "auth.register.language": "介面語言", + "auth.register.languageDefault": "跟隨站點預設", + "auth.register.success": "註冊成功,歡迎來到 狼的提問箱!", + + // 探索頁補充 + "explore.recommend": "隨機推薦", + "explore.search": "搜尋標籤...", + "explore.sortLatest": "最新加入", + "explore.sortOldest": "最早加入", + "explore.noTags": "無標籤", + "explore.noRecommend": "暫無推薦用戶", + "explore.hotTags": "熱門標籤", + "explore.hotQuestions": "熱門問題", + "explore.noResult": "暫無符合條件的用戶。", + "explore.lazy": "這個人很懶,什麼都沒寫~", + "explore.prevPage": "上一頁", + "explore.nextPage": "下一頁", + "explore.loading": "載入中...", + "explore.loadFailed": "載入失敗", + + // 提問箱密碼頁 + "password.title": "此提問箱需要密碼", + "password.desc": "此提問箱已設定存取密碼,請輸入密碼查看內容", + "password.input": "請輸入存取密碼", + "password.enter": "驗證進入", + "password.submit": "確認", + + // 問題不存在 + "qitem.notFound": "問題不存在或已被刪除", + + // 聊天頁 + "chat.title": "私聊", + "chat.input": "輸入訊息...", + "chat.send": "傳送", + "chat.delete": "刪除對話", + "chat.block": "封鎖對方", + "chat.unblock": "取消封鎖", + "chat.empty": "還沒有訊息,說點什麼吧~", + "chat.withAsker": "與提問者的私聊", + "chat.withUser": "與 %s 的私聊", + "chat.closed": "對話已關閉", + "chat.closedNoSend": "對話已關閉,無法發送新訊息", + "chat.remaining": "訊息剩餘:%d 條(共 %d 條)", + "chat.me": "我", + "chat.other": "對方", + "chat.userBlocked": "該使用者已被拉黑", + "chat.addImage": "新增圖片(可選,最大5MB)", + "chat.confirm": "確認", + "chat.confirmBlock": "確定要拉黑該使用者嗎?他將無法再發送私訊。", + "chat.confirmUnblock": "確定要取消封鎖該使用者嗎?他將可以再次發送私訊。", + "chat.deleteConfirm": "確定要刪除這條私聊對話嗎?對方將無法再看到此對話,且操作不可恢復。", + "maintenance.title": "NekoBox 維護中...", + "maintenance.desc": "NekoBox 正在進行維護,服務暫時不可用。過一會再來吧~", + + // 單題分享(v4) + "qitem.shareCard": "分享此提問", + + // 追問(v4) + "followup.title": "追問記錄", + "followup.content": "追問:", + "followup.answerPlaceholder": "回答這條追問...", + "followup.answerBtn": "回答追問", + "followup.waiting": "等待回答中...", + "followup.askMore": "繼續追問", + "followup.remaining": "還可追問 %d / %d 次", + "followup.contentPlaceholder": "補充你的問題...", + "followup.waitingReply": "你的追加提問已發出,等待提問箱主人回覆中...", + "followup.submit": "提交追問", + + // 置頂(v4) + "qlist.pinned": "置頂中", + "qlist.pin": "置頂", + "qlist.unpin": "取消置頂", + "qlist.readStatus": "閱讀狀態", + "qlist.read": "已讀", + "qlist.unread": "未讀", + "qlist.statsRead": "已讀", + "qlist.statsUnread": "未讀", + + // 提問成功頁(v4) + "success.title": "提問發送成功!", + "success.yourQuestion": "你的提問:", + "success.emailSent": "已向你的信箱發送了該提問的私密連結,也可以使用下方連結隨時查看提問、接收回答與繼續追問。", + "success.saveLink": "請務必保存下面的私密連結!由於你沒有留下信箱,此連結(含口令 Token)是你查看提問、接收回答與繼續追問的唯一憑證,遺失後將無法找回。", + "success.privateLink": "你的提問私密連結(點擊複製保存)", + "success.copy": "複製", + "success.copied": "已複製 ✓", + "success.copyFailed": "複製失敗", + "success.token": "口令 Token:%s(提問人憑 Token 存取私密連結)", + "success.viewQuestion": "查看我的提問", + "success.backToBox": "返回%s的提問箱", + "success.pageTitle": "提問成功 - %s", + + // 停用頁 + "deactivate.title": "帳號停用", + "deactivate.desc": "停用後你的提問箱將無法存取,且所有資料將被刪除,此操作不可恢復!", + "deactivate.confirm": "我確認要停用帳號", + "deactivate.submit": "停用帳號", + + // 匿名投票 + "nav.votes": "投票", + + "common.back": "返回", + + "vote.listTitle": "投票管理", + "vote.newTitle": "新建投票", + "vote.editTitle": "編輯投票", + "vote.resultsTitle": "投票結果", + "vote.statsTotal": "投票總數", + "vote.statsPublished": "進行中", + "vote.createNew": "新建投票", + "vote.typeSingle": "單選", + "vote.typeMultiple": "多選", + "vote.typeRating": "評分", + "vote.typeRank": "排序", + "vote.typeForm": "表單收集", + "vote.statusNotStarted": "未開始", + "vote.statusDraft": "草稿", + "vote.statusPublished": "進行中", + "vote.statusEnded": "已結束", + "vote.statusArchived": "已歸檔", + "vote.participants": "參與", + "vote.remainLabel": "剩餘", + "vote.perpetual": "無限期", + "vote.durationDay": "天", + "vote.durationHour": "小時", + "vote.durationMinute": "分鐘", + "vote.createdAt": "建立於", + "vote.edit": "編輯", + "vote.results": "結果", + "vote.preview": "預覽", + "vote.page.mustComplete": "請完成", + "vote.page.rankHintRequired": "排名 1-N(必填)", + "vote.page.rankHintOptional": "排名 1-N(可選,可只排部分)", + "vote.page.previewHint": "預覽模式:這是發布前的頁面範例,提交已停用", + "vote.page.previewSubmitHint": "預覽模式,發布後即可提交", + "vote.publish": "發布", + "vote.close": "結束", + "vote.share": "複製連結", + "vote.copied": "已複製", + "vote.shareURL": "投票分享連結", + "vote.confirmPublish": "確定發布該投票嗎?發布後即產生可分享連結。", + "vote.confirmClose": "確定結束該投票嗎?結束後參與者將無法再投票。", + "vote.confirmDelete": "確定刪除該投票嗎?投票資料將一併刪除,不可復原!", + "vote.manageEntry": "管理投票", + + "vote.manageTitle": "管理投票", + "vote.manageDesc": "建立與管理你的匿名投票", "vote.empty": "還沒有投票,點擊右上角\"新建投票\"開始吧~", + "vote.formLockedHint": "該投票已發布,僅可修改基本資訊;類型與選項已鎖定(避免破壞已收集的投票資料)", + "vote.formBasic": "基本設定", + "vote.formTitle": "投票標題", + "vote.formTitlePlaceholder": "例如:今天吃什麼?", + "vote.formDescription": "描述 / 說明", + "vote.formDescriptionPlaceholder": "補充說明(可選)", + "vote.formType": "投票類型", + "vote.formVisibility": "展示方式", + "vote.formVisibilityPublic": "公開展示(探索頁 + 提問箱頁)", + "vote.formVisibilityLinkOnly": "僅連結分享", + "vote.formVoterScope": "投票範圍", + "vote.formVoterScopeAnyone": "任何人(含匿名)", + "vote.formVoterScopeRegistered": "僅註冊用戶", + "vote.formShowResult": "結果可見時機", + "vote.formShowResultAfterVote": "投票後立即可見", + "vote.formShowResultAfterEnd": "結束後可見", + "vote.formShowResultOwnerOnly": "僅我可見", + "vote.formStartAt": "開始時間(留空立即開始)", + "vote.formEndAt": "結束時間(留空不限制)", + "vote.formMaxChoices": "最多可選數量", + "vote.formMaxVotesPerFingerprint": "每個投票每個裝置限 1 次,使用同一瀏覽器重複提交將被攔截。", + "vote.formMaxVotesPerAccount": "每個投票每個註冊帳號限 1 次,使用同一帳號重複提交將被攔截。", + "vote.formAllowCustom": "允許參與者補充自訂選項", + "vote.formRequireCaptcha": "匿名投票需通過驗證碼", + "vote.formOptions": "投票選項", + "vote.formRatingHint": "評分題無需設定選項(1-10 分)", + "vote.addOption": "新增選項", + "vote.optionPlaceholder": "選項內容", + "vote.formFields": "表單欄位", + "vote.addField": "新增欄位", + "vote.field": "欄位", + "vote.formFieldType": "欄位類型", + "vote.fieldTypeText": "單行文字", + "vote.fieldTypeTextarea": "多行文字", + "vote.fieldTypeSelect": "下拉選擇", + "vote.fieldTypeRadio": "單選", + "vote.fieldTypeCheckbox": "多選", + "vote.fieldTypeNumber": "數字", + "vote.fieldTypeRank": "排序", + "vote.page.selectPlaceholder": "請選擇", + "vote.page.avgNumber": "平均", + "vote.page.fieldFilled": "已填寫", + "vote.fieldTypeRating": "評分", + "vote.formFieldLabel": "欄位標籤", + "vote.formFieldRequired": "必填", + "vote.formFieldMaxLength": "字數上限(0 不限制)", + "vote.formFieldOptions": "選項", + "vote.formFieldOptionsHint": "點擊下方「新增選項」逐條新增,不需用逗號分隔", + "vote.fieldAllowCustom": "允許補充自訂選項", + "vote.formFieldOptionsPlaceholder": "如:滿意,一般,不滿意", + "vote.createDraft": "儲存草稿", + "vote.builderHintOptions": "至少需要 2 個選項", + "vote.builderHintFields": "請至少新增 1 個欄位", + "vote.builderHintFieldOptions": "選擇題/排序題請填寫選項(排序題至少 2 個)", + "vote.resultsAnswered": "回答數", + "vote.page.rankedTimes": "被排次數", + "vote.resultsTotal": "總參與人數", + "vote.resultsSubmissions": "表單回覆", + "vote.resultsViewTexts": "查看文字提交", + "vote.resultsToggleHint": "點擊展開或收起文字回覆明細", + "vote.resultsOptions": "投票結果", + "vote.resultsVotes": "票", + "vote.export": "匯出 CSV", + "vote.resultsEmpty": "還沒有人參與,快來分享吧~", + + // 匿名投票參與頁 + "vote.inlineTitle": "投票", + "vote.page.by": "發起的投票", + "vote.page.goVote": "去投票", + "vote.page.notStarted": "未開始", + "vote.page.notStartedHint": "該投票尚未開始,敬請期待~", + "vote.page.ended": "已結束", + "vote.page.endedHint": "該投票已結束", + "vote.page.active": "進行中", + "vote.page.participated": "已參與", + "vote.page.loginRequired": "該投票僅限註冊用戶參與,請先登入。", + "vote.page.anonymousHint": "該投票允許匿名參與,你的身分不會被記錄(同一裝置最多可參與 1 次)。", + "vote.page.submit": "提交投票", + "vote.page.alreadyVoted": "你已參與過該投票,感謝參與!", + "vote.page.resultsTitle": "投票結果", + "vote.page.totalVotes": "總參與", + "vote.page.avgRating": "平均分", + "vote.page.avgRank": "平均排名", + "vote.page.optionColumn": "選項", + "vote.page.ratingLabel": "請評分", + "vote.page.ratingUnit": "分", + "vote.page.rankHint": "請為每個選項填寫 1-N 的排名(數字越小越靠前,不可重複)", + "vote.page.rankPlaceholder": "排名", + "vote.page.multipleHint": "可多選", + "vote.page.customOption": "補充選項(可選)", + "vote.page.customPlaceholder": "填寫你心中的其他選項", + "vote.page.customOther": "其他", + "vote.page.customFieldPlaceholder": "填寫自訂選項…", + "vote.page.customStats": "自訂補充選項", + "vote.page.formCount": "已收到回覆", + "vote.page.ownerManage": "管理此投票(僅你可見結果)", + + // 單份投票的定量收集 + "vote.formQuotaLimit": "收集上限(份)", + "vote.formQuotaHint": "0 表示不限制;收集到指定份數後自動結束該投票。", + "vote.flash.notFound": "該投票不存在或已失效", + "vote.flash.notPublished": "該投票尚未發布", + "vote.flash.archived": "該投票已歸檔,無法參與", + "vote.flash.ended": "該投票已結束", + "vote.flash.notStarted": "該投票尚未開始", + "vote.flash.success": "投票成功!感謝參與", + "vote.err.noChoice": "請至少選擇一個選項", + "vote.err.tooManyChoices": "最多只能選擇 %d 個選項", + "vote.err.pickOne": "請選擇一個選項", + "vote.err.invalidOption": "存在無效的選項", + "vote.err.optionDuplicate": "選項不能重複選擇", + "vote.err.ratingRequired": "請給出 1-10 分的評分", + "vote.err.rankRequired": "請為每個選項填寫 1-%d 的排名", + "vote.err.rankDuplicate": "排名不能重複", + "vote.err.fieldRequired": "請填寫「%s」", + "vote.err.fieldCustomRequired": "請填寫「%s」的自訂選項", + "vote.err.fieldCustomTooLong": "「%s」的自訂選項不能超過 %d 字", + "vote.err.fieldInvalidOption": "「%s」的選項無效", + "vote.err.fieldConfigError": "「%s」選項配置錯誤", + "vote.err.fieldRankIncomplete": "請填寫「%s」的完整排名", + "vote.err.fieldRankRange": "「%s」排名必須是 1-%d 之間的數字", + "vote.err.fieldRankDuplicate": "「%s」排名不能重複", + "vote.err.fieldTooLong": "「%s」超出字數限制(最多 %d 字)", + "vote.err.fieldNotNumber": "「%s」必須是數字", + "vote.err.fieldRating": "「%s」請給出 1-10 分的評分", + "vote.err.unknownType": "未知的投票類型", + "vote.err.customTooLong": "自訂選項不能超過 %d 字", + "vote.err.blockWords": "提交內容中包含了投票發起人設定的屏蔽詞,無法提交", + "vote.err.serialize": "提交內容序列化失敗", + "captcha.error": "驗證碼錯誤", + "captcha.errEmpty": "驗證碼不能為空", + "captcha.errFormat": "驗證碼格式錯誤,請重新整理後重試", + "captcha.errAnswerFormat": "驗證碼答案格式錯誤", + "captcha.errExpired": "驗證碼已過期,請重新整理後重試", + "captcha.errService": "驗證碼服務異常,請稍後再試", + "captcha.errAnswerWrong": "驗證碼答案錯誤", + "censor.unavailable": "內容審核服務暫時不可用,請稍後再試", + + // 功能管理:提問 / 私聊 / 投票 的開關、定時開關與定量收集 + "admin.tabAccount": "用戶設定", + "admin.featuresHint": "在「提問 / 私聊 / 投票」面板中管理各功能的開關、開放時段與收集上限;達到上限後對應功能自動關閉。", + "admin.featuresEntry": "設定", + "admin.featureHint.question": "關閉後訪客無法提交新提問;達到收集上限後自動停止接收(含私密提問)。", + "admin.featureHint.chat": "關閉後訪客無法發起新的私聊;已有會話不受影響。", + "admin.featureHint.vote": "關閉後提問箱頁不再展示投票板塊,訪客也無法參與你的投票。單份投票可在編輯頁單獨設定收集上限。", + "admin.featureEnabled": "啟用該功能", + "admin.featureOpenAt": "定時開放", + "admin.featureCloseAt": "定時關閉", + "admin.featureTimeHint": "留空表示不限制;開放時間之前、關閉時間之後該功能均不可用。", + "admin.featureSchedule": "循環定時(每週循環)", + "admin.featureScheduleStart": "每日開始", + "admin.featureScheduleEnd": "每日結束", + "admin.featureScheduleHint": "勾選每週固定開放日並設定每日時間段,按週循環生效;全部留空表示不啟用。只填開放日(不填時間)表示該天全天開放;只填時間(不勾選任何日)表示每天該時段開放。", + "admin.featureMode": "收集模式", + "admin.featureModeAlways": "長期開放", + "admin.featureModeWindow": "定時收集", + "admin.featureModeRecurring": "循環定時", + "admin.featureModeHint": "長期開放:不限時間,保持開放直到手動關閉;定時收集:一次性開放 / 關閉時間視窗;循環定時:每週固定開放日循環生效。三種模式互斥,儲存時未選模式的設定將被清空。", + "admin.featureQuotaLimit": "收集上限(條 / 份)", + "admin.featureQuotaHint": "0 表示不限制;累計收集達到上限後自動關閉,重設計數後可開啟下一輪。", + "admin.featureQuotaCount": "已收集", + "admin.featureQuotaUnlimited": "不限制", + "admin.featureQuotaResetAt": "上次重設:", + "admin.featureResetQuota": "重設計數", + "admin.featureSave": "儲存該功能設定", + + // 功能管理:狀態與提示 + "feature.name.question": "提問", + "feature.name.chat": "私聊", + "feature.name.vote": "投票", + "feature.statusOpen": "進行中", + "feature.status.disabled": "已關閉", + "feature.status.not_started": "未開始", + "feature.status.ended": "已結束", + "feature.status.quota_reached": "已達上限", + "feature.saved": "功能設定已儲存", + "feature.errTime": "%s 的時間格式不正確,請使用日期時間選擇器", + "feature.errTimeRange": "%s 的定時關閉時間必須晚於定時開放時間", + "feature.errQuota": "%s 的收集上限需為 0 到 1000000 之間的整數", + "feature.errInvalid": "未知的功能類型", + "feature.quotaReset": "%s 的收集計數已重設", + "feature.errScheduleHalf": "%s 的循環定時開始與結束時間必須同時填寫", + "feature.errScheduleTime": "%s 的循環定時時間格式不正確,請使用時間選擇器", + "feature.errScheduleRange": "%s 的循環定時結束時間必須晚於開始時間", + + // 功能關閉時對訪客的提示 + "feature.disabled.question": "提問箱主人已關閉提問功能", + "feature.disabled.chat": "該提問箱未開啟私聊功能", + "feature.disabled.vote": "該提問箱已關閉投票功能", + "feature.not_started.question": "提問功能尚未開放", + "feature.not_started.chat": "私聊功能尚未開放", + "feature.not_started.vote": "投票功能尚未開放", + "feature.ended.question": "提問功能已結束收集", + "feature.ended.chat": "私聊功能已結束", + "feature.ended.vote": "投票功能已結束", + "feature.quota_reached.question": "提問功能已達到收集上限", + "feature.quota_reached.chat": "私聊功能已達到收集上限", + "feature.quota_reached.vote": "投票功能已達到收集上限", + + // 認證:訪客可見的提示(P0) + "auth.errBadCredential": "郵箱或密碼錯誤", + "auth.errUserNotFound": "使用者不存在", + "auth.errEmailFrequent": "郵件發送太頻繁,請稍後再試", + "auth.errEmailFailed": "郵件發送失敗,請稍後再試", + "auth.errCodeExpired": "驗證碼已過期", + "auth.errPasswordChanged": "密碼修改成功", + "auth.errLoginRequired": "請先登入", + "auth.errDuplicateEmail": "該郵箱已被註冊", + "auth.errDuplicateDomain": "該個性網域已被使用", + + // 提問箱頁:訪客可見(P0) + "question.errWrongPassword": "密碼錯誤", + "question.errAskPasswordWrong": "提問密碼錯誤", + "question.errLoginRequired": "提問箱的主人設定了僅註冊使用者才能提問,請先登入。", + "question.errSelfChat": "不能給自己發起私聊", + "question.errBlockedChat": "你已被對方封鎖,無法發起私聊", + "question.errChatDailyLimit": "你今日已達發送上限(%d則),請明天再試", + "question.errDuplicateSubmit": "偵測到重複提交,請稍後再試", + "question.errImageTooLarge": "圖片檔案大小不能大於 5Mb", + "question.errUploadImageFailed": "上傳圖片失敗", + "question.errBlockWords": "提問內容包含了提問箱主人設定的屏蔽詞,發送失敗", + "question.errAnsweredLocked": "已回答的問題不能標記為未讀", + "question.flash.chatSent": "私聊請求已發送!你可以透過以下連結查看對話:%s", + "question.flash.answered": "回答發布成功!", + "question.flash.operated": "操作成功", + "question.flash.markedRead": "已標記為已讀", + "question.flash.markedUnread": "已標記為未讀", + + // 追問:訪客/箱主可見(P0) + "followup.errNoPermission": "無權操作", + "followup.errNotAnswered": "問題尚未被回答,暫時不能追問", + "followup.errLimit": "每條提問最多追問 %d 次,已達上限", + "followup.errWaitReply": "請等待主人回覆上一條追問後再繼續追問", + "followup.errNotFound": "追問不存在", + "followup.flash.sent": "追問發送成功!", + "followup.flash.answered": "回答成功!", + + // 後台設定:箱主可見(P1) + "admin.flash.profileUpdated": "更新個人資訊成功", + "admin.flash.chatUpdated": "私聊設定更新成功", + "admin.flash.pagePwdOff": "已關閉頁面密碼存取", + "admin.flash.pagePwdUpdated": "頁面存取密碼已更新", + "admin.flash.askPwdOff": "已關閉提問密碼", + "admin.flash.askPwdUpdated": "提問密碼已更新", + "admin.flash.tagsTooMany": "標籤不能超過 5 個", + "admin.flash.tagsUpdated": "更新標籤成功", + "admin.flash.tagTooLong": "每個標籤不能超過 5 個字元:%s", + "admin.flash.blockWordsTooMany": "屏蔽詞不能超過 10 個", + "admin.flash.blockWordTooLong": "屏蔽詞長度不能超過 10 個字元:%s", + "admin.flash.harassmentUpdated": "更新防騷擾設定成功", + "admin.flash.blockWordsUpdated": "屏蔽詞已更新", + "admin.flash.deactivated": "您的帳號已停用,感謝您使用 狼的提問箱。期待未來還能再見 👋🏻", + "admin.export.errUser": "匯出失敗:獲取使用者資訊失敗", + "admin.export.errQuestion": "匯出失敗:獲取問題資訊失敗", + + // 投票管理:箱主可見(P1) + "vote.err.invalidID": "無效的投票 ID", + "vote.err.notExist": "投票不存在", + "vote.err.noPermission": "無權操作該投票", + "vote.err.type": "未知的投票類型", + "vote.err.titleRequired": "請填寫投票標題", + "vote.err.titleTooLong": "投票標題不能超過 100 字", + "vote.err.visibility": "未知的可見性設定", + "vote.err.voterScope": "未知的投票範圍設定", + "vote.err.showResult": "未知的結果展示設定", + "vote.err.timeRange": "結束時間必須晚於開始時間", + "vote.err.quota": "收集上限需為 0 到 1000000 之間的整數", + "vote.err.optionsTooFew": "請至少新增 2 個選項", + "vote.err.optionsFormat": "選項資料格式錯誤", + "vote.err.optionTooLong": "單個選項內容不能超過 %d 字", + "vote.err.maxChoicesExceed": "最多可選數量不能超過選項總數", + "vote.err.fieldsTooFew": "請至少新增 1 個表單欄位", + "vote.err.fieldsFormat": "表單欄位資料格式錯誤", + "vote.err.fieldsTooMany": "表單欄位不能超過 50 個", + "vote.err.fieldType": "存在未知的表單欄位類型", + "vote.err.fieldLabelMissing": "第 %d 個欄位缺少標籤", + "vote.err.fieldLabelTooLong": "第 %d 個欄位的標籤不能超過 100 字", + "vote.err.fieldLabelDuplicate": "欄位標籤「%s」重複,請使用不同的標籤", + "vote.err.fieldMaxLength": "第 %d 個欄位的字數上限需在 0-2000 之間", + "vote.err.fieldReservedOption": "第 %d 個欄位(%s)的選項內容包含保留值,請更換", + "vote.err.fieldOptionsTooFew": "第 %d 個欄位(%s)請至少填寫 1 個選項", + "vote.err.fieldRankTooFew": "第 %d 個欄位(%s)排序題至少需要 2 個選項", + "vote.err.fieldSerialize": "欄位選項序列化失敗", + "vote.err.editClosed": "已結束或歸檔的投票不允許編輯", + "vote.err.publishDraftOnly": "只有草稿狀態的投票可以發布", + "vote.err.closePublishedOnly": "只有進行中的投票可以結束", + "vote.err.publishTitleRequired": "請先填寫投票標題再發布", + "vote.err.publishFormFieldsRequired": "表單類型投票至少需要 1 個欄位", + "vote.err.publishOptionsRequired": "選項投票至少需要 2 個選項", + "vote.err.optionUpdateFailed": "選項更新失敗,請重試", + "vote.err.fieldUpdateFailed": "表單欄位更新失敗,請重試", + "vote.flash.published": "投票已發布!分享連結:%s", + "vote.flash.updated": "投票資訊已更新(進行中的投票不可修改類型與選項)", + "vote.flash.draftSaved": "已儲存到草稿", + "vote.flash.closed": "投票已結束", + "vote.flash.deleted": "投票已刪除", + "vote.err.timeFormat": "時間格式不正確,請使用日期時間選擇器", + "vote.err.maxChoicesLimit": "最多可選數量不能超過 20", + "vote.err.descriptionTooLong": "投票描述不能超過 5000 字", + "profile.err.avatarTooLarge": "頭像檔案太大,最大支援 2MB", + "profile.err.backgroundTooLarge": "背景圖檔案太大,最大支援 2MB", + "profile.err.newPasswordTooShort": "新密碼長度不能少於 8 位", + "profile.err.badOldPassword": "舊密碼輸入錯誤", + "admin.export.errCreateExcel": "匯出失敗:建立Excel檔案失敗", + "admin.export.errWriteExcel": "匯出失敗:寫入Excel檔案失敗", + + // profile 設定頁 UI 重構:卡片分區 / 頭像背景裁切 / 改密強制登出 + "profile.card.basic": "基本資料", + "profile.card.avatarBg": "頭像與背景", + "profile.card.qr": "二維碼樣式", + "profile.card.password": "修改密碼", + "profile.avatarCropHint": "裁切框已鎖定為 1:1 正方形", + "profile.backgroundCropHint": "裁切框已鎖定為 16:9 寬螢幕", + "profile.cropTitle": "裁切圖片", + "profile.cropConfirm": "確認裁切", + "profile.err.avatarRatio": "頭像圖片必須為 1:1 正方形,請裁切後再上傳", + "profile.err.backgroundRatio": "背景圖片必須為 16:9 寬高比,請裁切後再上傳", + "profile.flash.passwordChanged": "密碼修改成功,已登出,請使用新密碼重新登入", + "profile.pwdNote": "儲存後目前工作階段立即失效,需使用新密碼重新登入", + "profile.pickAvatar": "更換頭像", + "profile.pickBackground": "更換背景", + "profile.saveBasic": "儲存基本資料", + "profile.saveMedia": "儲存頭像與背景", + "profile.savePassword": "確認修改密碼", + "profile.saveQR": "儲存 QR Code 樣式", + "profile.saveSocial": "儲存社交連結", + "profile.err.cropLibUnavailable": "圖片裁切元件載入失敗,請重新整理頁面後重試", + + // 驗證碼 JSON 介面:前端消費(P2) + "captcha.json.badRequest": "請求格式錯誤", + "captcha.json.idRequired": "驗證碼 ID 不能為空", + "captcha.json.genFailed": "生成驗證碼失敗", + "chat.errDeletedByOwner": "該私聊對話已被對方刪除。", + "chat.errBlockedView": "你已被對方拉黑,無法查看該私聊", + "chat.errEmpty": "消息不能為空", + "chat.errTooLong": "消息內容過長,請控制在 %d 字以內", + "chat.errConvDeletedSend": "該對話已被刪除,無法發送消息", + "chat.errBlockedSend": "你已被對方拉黑,無法發送私信", + "chat.errBlockWords": "訊息中包含了提問箱主人設定的屏蔽詞,無法傳送", + "chat.errDailyLimit": "你今日已達發送上限(%d條),請明天再試", + "chat.errLimitReached": "對話已達到上限,無法發送新消息", + "chat.errChatClosed": "該箱主已關閉私聊功能,無法發送消息", + "chat.errImageTooLarge": "圖片大小不能超過 %dMB", + "chat.errUploadFailed": "圖片上傳失敗,請稍後重試", + "chat.errNoPermission": "無權執行此操作", + "chat.flash.sent": "消息發送成功", + "chat.flash.deleted": "私聊對話已刪除", + "chat.flash.operated": "操作成功", + "image.errTooLarge": "圖片尺寸過大,最大支援 %d×%d 像素,請壓縮後重新上傳", + "internal.errFlash": "服務內部錯誤,請稍後重試。", + "internal.errServer": "伺服器內部錯誤", + "home.errStats": "獲取統計資訊失敗", + "pixel.errPath": "非法的請求路徑", + "proxy.errNotFound": "頁面不存在", + "proxy.errInternal": "服務網關內部錯誤", + "explore.errRandomUser": "獲取隨機用戶失敗", + // 表单校验 + "form.answer.content": "回答內容不能為空", + "form.answer.contentTooLong": "回答內容不能超過 %d 個字元", + "form.email.required": "請填寫有效的電子郵件地址", + "form.followup.answer": "追問回答不能為空", + "form.followup.answerTooLong": "追問回答不能超過 %d 個字元", + "form.followup.content": "追問內容不能為空", + "form.followup.contentTooLong": "追問內容不能超過 %d 個字元", + "form.login.password": "請輸入密碼", + "form.profile.backgroundimagealpha": "請填寫 QR Code 背景透明度", + "form.profile.colorHex": "請填寫 7 位十六進制顏色代碼(含 #)", + "form.profile.dotscale": "請填寫 QR Code 點大小", + "form.profile.intro": "個人介紹不能為空,且不超過 100 個字元", + "form.profile.linkMax": "主頁連結長度不能超過 30 個字元", + "form.profile.name": "暱稱不能為空,且不超過 20 個字元", + "form.profile.newPassword": "新密碼長度不能超過 30 個字元", + "form.profile.passwordMax": "密碼長度不能超過 30 個字元", + "form.question.content": "問題內容不能為空", + "form.question.contentTooLong": "問題內容不能超過 %d 個字元", + "form.question.receiveReplyEmail": "請填寫有效的電子郵件地址以接收回覆", + "form.recaptcha": "請完成人機驗證", + "form.recover.newPassword": "請設定 8 至 30 位的新密碼", + "form.recover.repeatPassword": "兩次輸入的新密碼不一致", + "form.register.domain": "個性網域只能包含字母、數字與底線,長度為 3 至 20 位", + "form.register.email": "請填寫有效的電子郵件地址(不超過 100 個字元)", + "form.register.name": "暱稱不能為空,且不超過 20 個字元", + "form.register.password": "請設定 8 至 30 位的密碼", + "form.register.recaptcha": "無法人機驗證,請重新整理頁面或更換網路後重試", + "form.register.repeatPassword": "兩次輸入的密碼不一致", +} diff --git a/.workbuddy/backup/remove-vercount-20260906/footer.html b/.workbuddy/backup/remove-vercount-20260906/footer.html new file mode 100644 index 0000000..0536a82 --- /dev/null +++ b/.workbuddy/backup/remove-vercount-20260906/footer.html @@ -0,0 +1,79 @@ +

+ {{t .LangCode "site.footer.visitor" `` | safe}} | {{t .LangCode "site.footer.pageview" `` | safe}} +
Powered by NekoBox | {{ ICP }}
{{t .LangCode "site.footer.runtime"}}: +
+ + {{t .LangCode "site.footer.admin"}} + | {{t .LangCode "site.footer.notice"}} + | {{t .LangCode "site.footer.chat"}} + | {{t .LangCode "site.footer.drive"}} + | {{t .LangCode "site.footer.status"}} + | {{t .LangCode "site.footer.nav"}} +

+
+
+ + + + diff --git a/.workbuddy/backup/remove-vercount-20260906/header.html b/.workbuddy/backup/remove-vercount-20260906/header.html new file mode 100644 index 0000000..6518b98 --- /dev/null +++ b/.workbuddy/backup/remove-vercount-20260906/header.html @@ -0,0 +1,1044 @@ + + + + + + {{ .Title }} + + + + + + + + + + + + + + + + + + {{ if .VConsole }} + + + {{ end }} + + + + + + {{ if eq .CaptchaType "math" }} + + + {{ else if .RecaptchaTurnstileStyle }} + + + {{ else }} + + + {{ end }} + {{ if ne SentryDSN "" }} + + + {{ end }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ if .NoIndex }}{{ end }} + {{ if .IsUserPage }} + + + + + + + + + {{ else }} + + + + + + + {{ end }} + + + + + + + + + + + + + +
+ {{ if not .IsUserPage }} + + {{ end }} + +
+ + + +{{ if .IsPixel }} +
+{{ else }} +
+{{ end }} + +{{ if eq .CaptchaType "math" }} +
+
+

{{t .LangCode "captcha.title"}}

+

{{t .LangCode "captcha.hint"}}

+
+ {{t .LangCode "captcha.loading"}} + +
+
+ +
+ +
+ + +
+
+
+ + + + +{{ end }} \ No newline at end of file diff --git a/.workbuddy/backup/remove-vercount-20260906/i18n_test.go b/.workbuddy/backup/remove-vercount-20260906/i18n_test.go new file mode 100644 index 0000000..e16347b --- /dev/null +++ b/.workbuddy/backup/remove-vercount-20260906/i18n_test.go @@ -0,0 +1,76 @@ +// Copyright 2022 E99p1ant. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package i18n + +import ( + "testing" +) + +// TestDictionaryComplete 校验四种语言的字典 key 完全一致,防止漏翻译。 +func TestDictionaryComplete(t *testing.T) { + ref := dictionary[LangZhCN] + langs := []string{LangZhTW, LangEn, LangJa} + + for _, lang := range langs { + dict := dictionary[lang] + if len(dict) != len(ref) { + t.Errorf("%s: 词条数量 %d != 简体中文 %d", lang, len(dict), len(ref)) + } + for key := range ref { + if _, ok := dict[key]; !ok { + t.Errorf("%s: 缺少词条 %q", lang, key) + } + } + for key := range dict { + if _, ok := ref[key]; !ok { + t.Errorf("%s: 多出简体中文没有的词条 %q", lang, key) + } + } + } +} + +func TestResolve(t *testing.T) { + cases := map[string]string{ + "": LangZhCN, + "zh-cn": LangZhCN, + "zh": LangZhCN, + "zh-TW": LangZhTW, + "zh-Hant": LangZhTW, + "en": LangEn, + "en-US": LangEn, + "ja": LangJa, + "ja-JP": LangJa, + "fr": LangZhCN, // 未知回退 + " En ": LangEn, // 自动去除首尾空白 + "zh_cn": LangZhCN, + "zh_tw": LangZhTW, + "zh_hk": LangZhTW, // 香港繁体(下划线变体) + } + for in, want := range cases { + if got := Resolve(in); got != want { + t.Errorf("Resolve(%q) = %q, want %q", in, got, want) + } + } +} + +func TestT(t *testing.T) { + if got := T(LangZhCN, "nav.explore"); got != "探索" { + t.Errorf("zh-CN nav.explore = %q", got) + } + if got := T(LangEn, "nav.explore"); got != "Explore" { + t.Errorf("en nav.explore = %q", got) + } + if got := T(LangJa, "nav.explore"); got != "探索" { + t.Errorf("ja nav.explore = %q", got) + } + // 参数填充 + if got := T(LangEn, "site.footer.visitor", "42"); got != "You are visitor #42" { + t.Errorf("en site.footer.visitor with arg = %q", got) + } + // 未知 key 回退返回 key 本身 + if got := T(LangEn, "no.such.key"); got != "no.such.key" { + t.Errorf("unknown key = %q", got) + } +} diff --git a/.workbuddy/backup/remove-vercount-20260906/tamabox.changelogs.json b/.workbuddy/backup/remove-vercount-20260906/tamabox.changelogs.json new file mode 100644 index 0000000..e089503 --- /dev/null +++ b/.workbuddy/backup/remove-vercount-20260906/tamabox.changelogs.json @@ -0,0 +1,35 @@ +{ + "page": 1, + "perPage": 3, + "totalItems": 3, + "totalPages": 1, + "items": [ + { + "collectionId": "tamabox", + "collectionName": "tamabox-changelogs", + "content": "新增:用户私信功能上线,可在「我的」页查看匿名来信。
\n修复:移动端问题列表间距过窄的问题。
\n优化:首页加载速度提升约 30%。", + "created": "2024-03-04 21:45:55.000Z", + "id": "none", + "published_at": "2024-03-04 21:45:55.000Z", + "updated": "2024-03-04 21:45:55.000Z" + }, + { + "collectionId": "tamabox", + "collectionName": "tamabox-changelogs", + "content": "修复:部分旧浏览器下时间显示异常。
\n新增:修改记录页(本页),将完整更新历史从首页挪出,保持首页简洁。", + "created": "2024-02-29 00:00:55.000Z", + "id": "none", + "published_at": "2024-02-29 00:00:55.000Z", + "updated": "2024-02-29 00:00:55.000Z" + }, + { + "collectionId": "tamabox", + "collectionName": "tamabox-changelogs", + "content": "项目初始化:匿名提问 / 回答的基础流程跑通。", + "created": "2023-06-03 13:11:55.000Z", + "id": "none", + "published_at": "2023-06-03 13:11:55.000Z", + "updated": "2023-06-03 13:11:55.000Z" + } + ] +} \ No newline at end of file diff --git a/.workbuddy/backup/tamami-load-inject-20260906/header.html b/.workbuddy/backup/tamami-load-inject-20260906/header.html new file mode 100644 index 0000000..7659d79 --- /dev/null +++ b/.workbuddy/backup/tamami-load-inject-20260906/header.html @@ -0,0 +1,1020 @@ + + + + + + {{ .Title }} + + + + + + + + + + + + + + + + + {{ if .VConsole }} + + + {{ end }} + + + + {{ if eq .CaptchaType "math" }} + + + {{ else if .RecaptchaTurnstileStyle }} + + + {{ else }} + + + {{ end }} + {{ if ne SentryDSN "" }} + + + {{ end }} + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ if .NoIndex }}{{ end }} + {{ if .IsUserPage }} + + + + + + + + + {{ else }} + + + + + + + {{ end }} + + + + + + + + + + + + + +
+ {{ if not .IsUserPage }} + + {{ end }} + +
+ + + +{{ if .IsPixel }} +
+{{ else }} +
+{{ end }} + +{{ if eq .CaptchaType "math" }} +
+
+

{{t .LangCode "captcha.title"}}

+

{{t .LangCode "captcha.hint"}}

+
+ {{t .LangCode "captcha.loading"}} + +
+
+ +
+ +
+ + +
+
+
+ + + + +{{ end }} \ No newline at end of file diff --git a/.workbuddy/backup/timebox-inline-20260906/footer.html b/.workbuddy/backup/timebox-inline-20260906/footer.html new file mode 100644 index 0000000..0564317 --- /dev/null +++ b/.workbuddy/backup/timebox-inline-20260906/footer.html @@ -0,0 +1,78 @@ +

+
Powered by NekoBox | {{ ICP }}
{{t .LangCode "site.footer.runtime"}}: +
+ + {{t .LangCode "site.footer.admin"}} + | {{t .LangCode "site.footer.notice"}} + | {{t .LangCode "site.footer.chat"}} + | {{t .LangCode "site.footer.drive"}} + | {{t .LangCode "site.footer.status"}} + | {{t .LangCode "site.footer.nav"}} +

+
+
+ + + + diff --git a/.workbuddy/backup/timebox-inline-20260906/header.html b/.workbuddy/backup/timebox-inline-20260906/header.html new file mode 100644 index 0000000..ad7f165 --- /dev/null +++ b/.workbuddy/backup/timebox-inline-20260906/header.html @@ -0,0 +1,1029 @@ + + + + + + {{ .Title }} + + + + + + + + + + + + + + + + + {{ if .VConsole }} + + + {{ end }} + + + + {{ if eq .CaptchaType "math" }} + + + {{ else if .RecaptchaTurnstileStyle }} + + + {{ else }} + + + {{ end }} + {{ if ne SentryDSN "" }} + + + {{ end }} + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ if .NoIndex }}{{ end }} + {{ if .IsUserPage }} + + + + + + + + + {{ else }} + + + + + + + {{ end }} + + + + + + + + + + + + + + +
+ {{ if not .IsUserPage }} + + {{ end }} + +
+ + + +{{ if .IsPixel }} +
+{{ else }} +
+{{ end }} + +{{ if eq .CaptchaType "math" }} +
+
+

{{t .LangCode "captcha.title"}}

+

{{t .LangCode "captcha.hint"}}

+
+ {{t .LangCode "captcha.loading"}} + +
+
+ +
+ +
+ + +
+
+
+ + + + +{{ end }} \ No newline at end of file diff --git a/.workbuddy/backup/wp-fragments-20260906/autoban.go b/.workbuddy/backup/wp-fragments-20260906/autoban.go new file mode 100644 index 0000000..f2186aa --- /dev/null +++ b/.workbuddy/backup/wp-fragments-20260906/autoban.go @@ -0,0 +1,277 @@ +// Package autoban 提供 fail2ban 风格的自动临时封禁:检测批量扫描/探测特征后 +// 临时封禁来源 IP,并可回调发送告警。纯标准库实现,便于独立测试与复用。 +package autoban + +import ( + "strings" + "sync" + "time" +) + +// 探测事件类型 +const ( + KindNone = 0 // 无探测特征 + KindSensitive = 1 // 命中敏感路径 + KindNotFound = 2 // 404 响应(未命中敏感路径) +) + +// 默认阈值/窗口(Options 未配置或为 0 时兜底)。 +const ( + DefaultWindowSeconds = 60 + DefaultDurationMinutes = 15 + DefaultThresholdSensitive = 10 + DefaultThreshold404 = 20 + pathSampleMax = 6 +) + +// Options 自动封禁配置。 +type Options struct { + Enabled bool + // TrackNotFound 是否把 KindNotFound(未注册路径的 404)计入封禁统计。 + // 默认 false:正常业务路径一律放行,仅敏感扫描特征路径参与计数封禁, + // 避免共享出口下正常访问被 404 探测连坐误伤(详见 conf.Security.BanTrackNotFound)。 + TrackNotFound bool + ThresholdSensitive int // 窗口内敏感路径命中次数 + Threshold404 int // 窗口内 404 次数 + Window time.Duration // 统计窗口 + Duration time.Duration // 封禁时长 + AlertEmail string // 告警收件人(留空不发送) + SendAlert func(ip string, paths []string, durationMinutes int) error // 告警发送函数(nil 则不发送) +} + +// sensitivePaths 常见后台/配置/漏洞探测路径特征(忽略大小写,前缀匹配)。 +var sensitivePaths = []string{ + "/admin", "/wp-admin", "/wp-login.php", "/wp-content", "/wordpress", + "/administrator", "/manager", "/cpanel", "/plesk", + "/.env", "/.git", "/.aws", "/.ssh", "/.svn", "/.hg", "/.htaccess", + "/config", "/configuration", "/backup", "/database", "/db", + "/actuator", "/swagger", "/api-docs", "/openapi", "/v2/api-docs", + "/phpmyadmin", "/pma", "/mysql", "/server-status", "/server-info", + "/shell", "/webshell", "/cmd", "/eval", "/hack", "/shell.php", + "/login.php", "/admin.php", "/index.php", "/console", "/vendor", +} + +type probeEvent struct { + at time.Time + kind int +} + +type probeStat struct { + events []probeEvent + sensitive int + notFound int + paths []string // 命中路径样本(告警用) +} + +// Banner 自动封禁器。方法并发安全。 +type Banner struct { + mu sync.Mutex + + probes map[string]*probeStat + banned map[string]time.Time + alerted map[string]bool + + thresholdSensitive int + threshold404 int + trackNotFound bool + window time.Duration + duration time.Duration + alertEmail string + sendAlert func(ip string, paths []string, durationMinutes int) error + enabled bool +} + +// New 按配置创建封禁器并注册全局清理。 +func New(opts Options) *Banner { + window := opts.Window + if window <= 0 { + window = DefaultWindowSeconds * time.Second + } + duration := opts.Duration + if duration <= 0 { + duration = DefaultDurationMinutes * time.Minute + } + sensitive := opts.ThresholdSensitive + if sensitive <= 0 { + sensitive = DefaultThresholdSensitive + } + notFound := opts.Threshold404 + if notFound <= 0 { + notFound = DefaultThreshold404 + } + b := &Banner{ + probes: make(map[string]*probeStat), + banned: make(map[string]time.Time), + alerted: make(map[string]bool), + thresholdSensitive: sensitive, + threshold404: notFound, + trackNotFound: opts.TrackNotFound, + window: window, + duration: duration, + alertEmail: strings.TrimSpace(opts.AlertEmail), + sendAlert: opts.SendAlert, + enabled: opts.Enabled, + } + register(b) + return b +} + +// Record 记录一次探测事件,达到阈值时封禁 IP 并触发一次告警。 +func (b *Banner) Record(ip, path string, kind int) { + if !b.enabled || ip == "" || kind == KindNone { + return + } + // 404 计数默认关闭(正常业务路径一律放行,仅敏感扫描特征参与封禁): + // 只有显式开启 TrackNotFound 时才把未注册路径的 404 计入统计。 + if kind == KindNotFound && !b.trackNotFound { + return + } + b.mu.Lock() + defer b.mu.Unlock() + + now := time.Now() + cutoff := now.Add(-b.window) + + st := b.probes[ip] + if st == nil { + st = &probeStat{} + b.probes[ip] = st + } + + // 滚动清理过期事件并重算计数 + valid := st.events[:0] + sensitive, notFound := 0, 0 + for _, e := range st.events { + if e.at.After(cutoff) { + valid = append(valid, e) + if e.kind == KindSensitive { + sensitive++ + } else if e.kind == KindNotFound { + notFound++ + } + } + } + valid = append(valid, probeEvent{at: now, kind: kind}) + if kind == KindSensitive { + sensitive++ + } else { + notFound++ + } + st.events = valid + st.sensitive = sensitive + st.notFound = notFound + if len(st.paths) < pathSampleMax && path != "" { + st.paths = append(st.paths, path) + } + + if sensitive < b.thresholdSensitive && notFound < b.threshold404 { + return + } + + // 已达阈值:封禁(已在封禁中则跳过) + if until, ok := b.banned[ip]; ok && until.After(now) { + return + } + until := now.Add(b.duration) + b.banned[ip] = until + + // 告警(每 IP 仅一次) + if b.alertEmail != "" && b.sendAlert != nil && !b.alerted[ip] { + b.alerted[ip] = true + sample := append([]string{}, st.paths...) + dur := int(b.duration.Minutes()) + go func() { + _ = b.sendAlert(ip, sample, dur) + }() + } +} + +// IsBanned 判断 IP 是否处于封禁期;到期自动解除。 +func (b *Banner) IsBanned(ip string) bool { + if !b.enabled || ip == "" { + return false + } + b.mu.Lock() + defer b.mu.Unlock() + if until, ok := b.banned[ip]; ok { + if until.After(time.Now()) { + return true + } + delete(b.banned, ip) + } + return false +} + +// IsSensitivePath 判断路径是否命中敏感探测特征(忽略大小写,完整段前缀匹配)。 +func IsSensitivePath(p string) bool { + lower := strings.ToLower(p) + for _, s := range sensitivePaths { + if lower == s || strings.HasPrefix(lower, s+"/") { + return true + } + } + return false +} + +// ---------- 全局注册与清理 ---------- + +var ( + bannersMu sync.Mutex + banners = make(map[*Banner]struct{}) + bannersOnce sync.Once +) + +func register(b *Banner) { + bannersMu.Lock() + banners[b] = struct{}{} + bannersMu.Unlock() + + bannersOnce.Do(func() { + go func() { + for { + time.Sleep(2 * time.Minute) + bannersMu.Lock() + for b := range banners { + b.cleanup() + } + bannersMu.Unlock() + } + }() + }) +} + +// cleanup 定期清理过期探测记录与已到期封禁(防止内存膨胀)。 +func (b *Banner) cleanup() { + b.mu.Lock() + defer b.mu.Unlock() + + now := time.Now() + cutoff := now.Add(-b.window) + + for ip, st := range b.probes { + if len(st.events) == 0 || st.events[len(st.events)-1].at.Before(cutoff) { + delete(b.probes, ip) + continue + } + valid := st.events[:0] + sensitive, notFound := 0, 0 + for _, e := range st.events { + if e.at.After(cutoff) { + valid = append(valid, e) + if e.kind == KindSensitive { + sensitive++ + } else if e.kind == KindNotFound { + notFound++ + } + } + } + st.events = valid + st.sensitive = sensitive + st.notFound = notFound + } + for ip, until := range b.banned { + if !until.After(now) { + delete(b.banned, ip) + } + } +} diff --git a/.workbuddy/backup/wp-fragments-20260906/autoban_test.go b/.workbuddy/backup/wp-fragments-20260906/autoban_test.go new file mode 100644 index 0000000..57ee69b --- /dev/null +++ b/.workbuddy/backup/wp-fragments-20260906/autoban_test.go @@ -0,0 +1,143 @@ +package autoban + +import ( + "sync/atomic" + "testing" + "time" +) + +func newTestBanner(thresholdSensitive, threshold404 int, window, duration time.Duration, alertEmail string, alertCalls *int64) *Banner { + return New(Options{ + Enabled: true, + TrackNotFound: true, // 测试 helper 显式开启 404 跟踪,覆盖两种计数的既有用例 + ThresholdSensitive: thresholdSensitive, + Threshold404: threshold404, + Window: window, + Duration: duration, + AlertEmail: alertEmail, + SendAlert: func(ip string, paths []string, dur int) error { + if alertCalls != nil { + atomic.AddInt64(alertCalls, 1) + } + return nil + }, + }) +} + +func TestSensitiveThreshold(t *testing.T) { + b := newTestBanner(2, 5, time.Minute, time.Minute, "", nil) + b.Record("1.2.3.4", "/admin", KindSensitive) + if b.IsBanned("1.2.3.4") { + t.Fatal("should not be banned before reaching threshold") + } + b.Record("1.2.3.4", "/.env", KindSensitive) + if !b.IsBanned("1.2.3.4") { + t.Fatal("expected ban after reaching sensitive threshold") + } +} + +func TestNotFoundThreshold(t *testing.T) { + b := newTestBanner(10, 3, time.Minute, time.Minute, "", nil) + for i := 0; i < 3; i++ { + b.Record("5.6.7.8", "/random-"+string(rune('a'+i)), KindNotFound) + } + if !b.IsBanned("5.6.7.8") { + t.Fatal("expected ban after reaching 404 threshold") + } +} + +func TestNotFoundTrackDisabledByDefault(t *testing.T) { + // 默认(TrackNotFound=false)下 404 不参与统计:正常业务路径一律放行, + // 仅敏感扫描路径仍计数封禁——防止共享出口下正常访问被 404 探测连坐误伤。 + b := New(Options{Enabled: true, ThresholdSensitive: 1, Threshold404: 1, Window: time.Minute, Duration: time.Minute}) + for i := 0; i < 5; i++ { + b.Record("7.7.7.7", "/random-"+string(rune('a'+i)), KindNotFound) + } + if b.IsBanned("7.7.7.7") { + t.Fatal("404 events must not accumulate when TrackNotFound is false") + } + b.Record("7.7.7.7", "/.env", KindSensitive) + if !b.IsBanned("7.7.7.7") { + t.Fatal("sensitive events must still ban even when 404 tracking is off") + } +} + +func TestWindowExpiry(t *testing.T) { + b := newTestBanner(2, 5, 50*time.Millisecond, time.Minute, "", nil) + b.Record("1.1.1.1", "/admin", KindSensitive) + time.Sleep(80 * time.Millisecond) // 事件滑出窗口 + b.Record("1.1.1.1", "/.env", KindSensitive) + if b.IsBanned("1.1.1.1") { + t.Fatal("events outside the window must not accumulate") + } +} + +func TestDurationExpiry(t *testing.T) { + b := newTestBanner(1, 5, time.Minute, 60*time.Millisecond, "", nil) + b.Record("2.2.2.2", "/admin", KindSensitive) + if !b.IsBanned("2.2.2.2") { + t.Fatal("expected ban") + } + time.Sleep(100 * time.Millisecond) // 封禁到期 + if b.IsBanned("2.2.2.2") { + t.Fatal("ban should expire after duration") + } +} + +func TestAlertOncePerIP(t *testing.T) { + var calls int64 + b := newTestBanner(1, 5, time.Minute, time.Minute, "admin@example.com", &calls) + b.Record("9.9.9.9", "/wp-admin", KindSensitive) + b.Record("9.9.9.9", "/.git", KindSensitive) // 已在封禁中,不应重复告警 + b.Record("9.9.9.9", "/config", KindSensitive) + + // sendAlert 异步调用,轮询等待最多 500ms + deadline := time.Now().Add(500 * time.Millisecond) + for atomic.LoadInt64(&calls) == 0 && time.Now().Before(deadline) { + time.Sleep(10 * time.Millisecond) + } + if got := atomic.LoadInt64(&calls); got != 1 { + t.Fatalf("expected exactly 1 alert, got %d", got) + } +} + +func TestDisabled(t *testing.T) { + b := New(Options{Enabled: false}) + b.Record("3.3.3.3", "/admin", KindSensitive) + if b.IsBanned("3.3.3.3") { + t.Fatal("disabled auto-ban must not ban") + } +} + +func TestDefaults(t *testing.T) { + b := New(Options{Enabled: true}) // 全部走默认值 + if b.thresholdSensitive != DefaultThresholdSensitive || b.threshold404 != DefaultThreshold404 { + t.Fatalf("default thresholds wrong: %d/%d", b.thresholdSensitive, b.threshold404) + } + if b.window != DefaultWindowSeconds*time.Second || b.duration != DefaultDurationMinutes*time.Minute { + t.Fatalf("default window/duration wrong: %v/%v", b.window, b.duration) + } + if b.trackNotFound { + t.Fatal("404 tracking must be disabled by default") + } +} + +func TestIsSensitivePath(t *testing.T) { + cases := []struct { + path string + want bool + }{ + {"/admin", true}, + {"/admin/panel", true}, + {"/WP-ADMIN", true}, + {"/.env", true}, + {"/user/questions", false}, + {"/vote/abc123", false}, + {"/administratorx", false}, // 前缀必须完整段匹配,避免误伤 + } + for _, c := range cases { + if got := IsSensitivePath(c.path); got != c.want { + t.Errorf("IsSensitivePath(%q) = %v, want %v", c.path, got, c.want) + } + } +} diff --git a/.workbuddy/memory/2026-09-04.md b/.workbuddy/memory/2026-09-04.md new file mode 100644 index 0000000..e975282 --- /dev/null +++ b/.workbuddy/memory/2026-09-04.md @@ -0,0 +1,99 @@ +# 2026-09-04 + +- 调研并输出 /user/profile 页面(account 面板)UI 优化评审与改造方案(等待用户批准后实施)。 +- 关键结论: + - 密码强制登出:`users.password_version` + 会话 `pwd_ver` 机制已存在(internal/context/auth.go authenticatedUser),改密后旧会话下次请求即失效;但 UpdateProfile 改密后仍重定向 /user/profile,被 Toggle 以「请先登录」错误文案弹回 /login,成功 flash 被吞。改造方向:改密成功 → Session.Flush() + success flash(复用 auth.errPasswordChanged)→ 直接跳 /login。 + - 头像/背景裁剪:项目无任何 crop 库;第三方库全部自托管于 R2(r2.337845818.best/tamabox/cdn)。候选方案:Cropper.js 内嵌 static/(需扩 static/fs.go embed 规则)。当前 avatarPlaceholder 文案自嘲"非1:1会扁"但无强制。 + - 复用点:profile-tabs 面板、Pickr 颜色选择、easy.qrcode 预览、UIkit modal/accordion、imagecheck 像素校验、storage.UploadPictureToS3(自动转 WebP、宽>1920 缩放)。 + - i18n 四语言字典 key 全等校验(i18n_test.go TestDictionaryComplete),新增词条须同步 4 个 dict 文件。 + +## 下午:profile account 面板改造已实施(用户已批准方案) + +- 决策:Cropper.js 走自有 R2 CDN(`lib/cropperjs/1.6.2/`,用户自行上传);改密立即登出跳登录页;卡片分区+折叠(中等重构);前后端双比例校验。 +- 改动清单: + - `route/user/profile.go`:avatar→storage.UploadAvatarToS3、background→storage.UploadBackgroundToS3(新增),改密成功(passwordChanged 标志)→ Session.Flush + flash(profile.flash.passwordChanged) → /login;比例不符 flash 提示(profile.err.avatarRatio / backgroundRatio)。 + - `internal/imagecheck/imagecheck.go`:新增 ErrBadAspectRatio + CheckAspectRatio + DefaultAspectTolerance(0.01);`imagecheck_test.go` 新增 4 个用例。 + - `internal/storage/s3.go`:抽 uploadPictureToS3 内部分管线(aspect 可选),UploadAvatarToS3=1:1、UploadBackgroundToS3=16:9,魔数后、像素总量前校验;原有 UploadPictureToS3 兼容签名(其他调用方不变)。 + - `internal/i18n/dict_*.go`(4 语):新增 15 词条(card.* 4、cropHint 2、cropTitle/cropConfirm、pwdNote、pickAvatar/pickBackground、err.avatarRatio/backgroundRatio/cropLibUnavailable、flash.passwordChanged)。 + - `templates/user/profile.html`:account 面板重构为 8 张卡片(基础资料/头像与背景[预览+裁剪]/二维码样式折叠/社交折叠/修改密码[随主表单]/标签/页面密码/账号操作);头像 96px 圆、背景 16:9 预览;Cropper 弹窗+写回 file input 逻辑;accordion 展开时重绘二维码预览;新增 .tb-pcard 等样式与暗色适配、移动端单列。 +- 验证:go build ./...(用 dev/webpstub overlay 方式,Windows 无 libwebp)通过;go test internal/i18n + internal/imagecheck 通过;dev/checktpl 全模板语法 OK;HTML 标签开闭平衡校验通过。 +- 待办:R2 上传 cropper.min.{js,css} 到 r2.337845818.best/tamabox/cdn/lib/cropperjs/1.6.2/ 后即可端到端生效;未做浏览器实测(需本地 DB/Redis)。 + +## 晚间:修复部署环境 `UIkit is not defined`(Rocket Loader 时序) + +- 现象:VM234:42 Uncaught ReferenceError: UIkit is not defined;更换头像/背景按钮点击无效果。 +- 根因:裁剪脚本(templates/user/profile.html 末尾)IIFE 解析期即执行 `var modalObj = UIkit.modal(modalEl)`;UIkit 在 base/header.html 是 defer 加载,Cloudflare Rocket Loader 把该内联脚本激活在 UIkit 执行前 → 抛错中断整个脚本 → `[data-pick-image]` 监听(其后)从未绑定。 +- 修复(templates/user/profile.html,4 处): + 1. `modalObj` 改为 null 懒创建(startCrop 内、依赖就绪后 `UIkit.modal(modalEl)`); + 2. 新增 `notify()`:UIkit 可用→UIkit.modal.alert,否则回退原生 window.alert; + 3. 新增 `whenReady(cb)`:轮询等待 window.UIkit && window.Cropper,超时 3s → notify(MSG_NO_CROPPER);文件大小校验前置(不依赖库); + 4. 脚本标签加 `data-cfasync="false"`,按钮监听尽早同步绑定。 +- 经验:模板内联脚本**禁止在解析期触碰 defer/外部库全局对象**(UIkit/Pickr 等),一律在用户事件回调内或依赖就绪后使用——Rocket Loader 会打乱执行序;全站内联脚本仅裁剪脚本踩中此坑。 +- 验证:node --check 通过(模板占位符替换后);R2 上 cropper 文件此前 curl 仍不可达(HTTP 000),用户需先上传,否则选图后 3s 提示「裁剪组件暂不可用」——需提醒用户。 + +## 晚间(二):图像保存 UX + 同行居中 + console 噪音排查 + +- 用户反馈:图像上传/裁剪已可用;但「保存修改」按钮在「修改密码」卡片里,改图后要去那里点击才生效,不直观;且头像修改显示在左上角别扭。 +- 改动(templates/user/profile.html): + 1. 头像与背景卡片内新增本地保存按钮(type=submit,label 新 i18n key `profile.saveMedia`,随主表单提交,裁剪写回的文件域一并上传);CSS `.tb-media-save`(居中条)。 + 2. `.tb-media-row` 改 `justify-content:center`、子项 `flex:0 1 auto` + column 居中、`tb-frame-bg` 固定 320px(max-width:100%);移动端 ≤640 子项 `flex-basis:100%` 上下堆叠居中、背景图全宽、保存按钮全宽。 + 3. 4 语字典新增 `profile.saveMedia`(zh-CN 保存头像与背景 / zh-TW 儲存頭像與背景 / en Save avatar & background / ja アイコンと背景を保存)。 +- console 噪音排查结论:`inspector.js` AbortError/Request timeout/responseType arraybuffer 报错**非项目代码**(全仓库 grep 无 inspector/fetch 包装);是浏览器 XHR 监控类扩展包装 fetch/XHR + Rocket Loader 激进异步调度(arraybuffer 脚本请求被中止)造成的误报。建议无痕窗口验证 + 或 Cloudflare 关闭 Rocket Loader。 +- 验证:gofmt -w 4 个 dict;go test ./internal/i18n/ 通过;go run ./dev/checktpl 全模板语法 OK。 + +## 晚间(三):二维码/社交默认展开 + 头像背景组件等宽 + 每卡片独立保存按钮 + +- 用户要求:①二维码、社交两块默认展开;②更换头像/更换背景组件大小一致(原头像 96 圆 vs 背景 320 宽预览悬殊);③为 account 面板每块加独立保存按钮(仿头像背景卡样式)。 +- 改动(templates/user/profile.html): + 1. 卡片3/4 的 uk-accordion `
  • ` 加 `uk-open` 默认展开(仍可折叠,multiple:true)。 + 2. `.tb-media-item` 固定等宽列 320px(flex:0 0 320 + max-width:100%),头像预览 96→120px、背景帧 width:100%(列内满宽 16:9);`.tb-media-actions .uk-button` min-width:150px 令两个更换按钮同宽。移动端:头像 80→96px、item 全宽堆叠、按钮全宽。 + 3. 保存按钮:基础资料(profile.saveBasic)/头像背景(已有 saveMedia)/二维码(profile.saveQR)/社交(profile.saveSocial)/改密(原 admin.saveProfile 改 profile.savePassword「确认修改密码」)——均为同表单 type=submit(提交整表),复用居中类 `.tb-card-save`(与 .tb-media-save 同规则)。 + 4. 二维码预览:默认展开后从其它 tab(?tab=question 等)切回 account 面板时不会再触发 accordion 'shown' → showPanel 里 account 时调 `window.__tbRefreshAccountPreview`(IIFE 内暴露的 debouncedUpdatePreview)重绘。 +- i18n 4 语新增 saveBasic/savePassword/saveQR/saveSocial;admin.saveProfile key 保留未删(四语仍在,仅模板不再引用)。 +- 验证:go test ./internal/i18n/ 通过;go run ./dev/checktpl OK。 + +## 晚间(四):头像/背景框等高 + 按钮统一居中 + 页面纵向压缩 + +- 用户反馈:①头像框偏小,希望与背景框显示高度一致;②按钮有的居中有的居左,要求统一并压缩空间。 +- 改动(templates/user/profile.html,纯 CSS/结构): + 1. 预览框等高:头像 120→150px 圆;背景框 267×150(16:9,与头像等高);媒体列 320→280 等宽;行间隙 20/40→14/32;移动端头像 96→120px。 + 2. 按钮统一居中:账号操作卡片(登出/导出/停用)`text-align:center` + 行距 12→9px;卡片6 标签、卡片7 页面密码按钮 `div.uk-margin`→`div.tb-card-save`;提问面板 updateHarassment/updateAskPassword、私聊面板 updateChat 三个按钮同改居中。 + 3. 压缩:`.tb-pcard` padding 16/16/10→13/14/8、margin-bottom 16→12、标题 margin 12→10;新增 `.profile-panel .uk-margin { margin-bottom: 12px }`(全面板字段间距 20→12)。 +- 范围说明:global-blockwords / feature-control-form 两个 partial 模板(三面板共用)内按钮未改(避免影响其它页面),如需统一再单独处理。 +- 验证:go run ./dev/checktpl OK。 + +## 晚间(五):账号操作按钮分组同行 + partial 内按钮居中 + +- 用户要求:①导出数据/导出全部数据按钮同一行;②退出登录/停用我的账号同一行;③屏蔽词设置、计数设置也居中。 +- 改动: + 1. `templates/user/profile.html`:账号操作卡重构为 `.tb-account-pair`(flex 居中,两按钮同行):导出组(默认按钮×2,target=_blank 表单)+ 说明行 `.tb-account-note`;登出组(danger 按钮+停用链接)+ 说明行。旧的 `.tb-account-action` 行式堆叠 CSS 删除,暗色规则改 `.tb-account-note`。 + 2. `.feat-quota-line` 加 text-align:center;`.feat-actions` 加 justify-content:center(保存/重置计数按钮居中)——覆盖 feature-control-form(提问/私聊/投票共用)。 + 3. `templates/user/global-blockwords.html`:提交按钮行 `.uk-margin` → `.tb-card-save`(居中);该 partial 仅 profile.html 引用,无外溢。 + 4. 说明文字合并保留:exportHint+exportJSONHint 合并一行小字,deactivateHint(含 )保留在其按钮组下方(deactivate 确认页另有完整警告文案)。 +- 验证:go run ./dev/checktpl OK。 + +## 晚间(六):修复导出按钮点击后永久「提交中」转圈 + +- 现象:导出数据/导出全部数据(target=_blank 下载表单)文件能下,但按钮永久转圈禁用。 +- 根因:`templates/base/footer.html` 全局 submit 反馈(P0 防重复提交)把触发按钮置「转圈+提交中」并禁用;复位仅靠整页导航/bfcache pageshow。`target="_blank"` 下载表单当前页不导航 → 无复位机会 → 永久卡死。 +- 修复(双保险): + 1. `templates/user/profile.html`:两个导出 form 加 `data-tb-skip-submit-feedback="1"`(footer 既有跳过通道,每次 submit 读到即跳并移除)。 + 2. `templates/base/footer.html`:submit 监听里对 `form.getAttribute('target')==='_blank'` 一律 return 跳过反馈——防御未来任何下载/新窗口表单踩同一坑(全站现存 target=_blank 表单仅 profile 这两处,行为从“卡死 bug”变“无反馈”=纯修复)。 +- 经验:全站提交反馈的复位依赖导航,凡不触发本页导航的提交(target=_blank / 下载 / iframe)都必须显式跳过。 +- 验证:go run ./dev/checktpl OK。 + +## 晚间(七):私聊面板 chat 专属功能聚合单卡 + +- 用户要求:`?tab=chat` 面板里「私聊块」(功能开关/收集窗口 feature-control-form)与「私聊设置」(update-chat-settings 消息上限)聚合为一张卡;全局屏蔽词为三面板共享、保持独立。 +- 改动(templates/user/profile.html): + 1. chat 面板改两段结构:私聊专属 = `
    ` 内含 FeatureChat partial + `
    ` + update-chat-settings 表单(原外层 `div.uk-margin` 移除);全局屏蔽词维持原 `hr` + partial 独立区。 + 2. CSS `.chat-zone`:内部 `.feat-card` 去边框透明(padding:0; border:0; background:transparent),避免卡中卡;`hr` margin 14px;内部 `legend`(admin.chat)从 uk-legend 大标题降级为 1.05rem 小节标题。 +- 提问/投票面板未动(结构同前)。 +- 验证:go run ./dev/checktpl OK。 + +## 晚间(八):功能控制卡按钮尺寸统一为常规按钮 + +- 用户要求:「保存该功能设置」按钮样式/大小改成与「更新私聊设置」一致;「重置计数」同调。 +- 改动(templates/user/feature-control-form.html,三面板共享 partial):feat-actions 内两按钮去掉 `uk-button-small` —— 保存改 `uk-button uk-button-primary`、重置改 `uk-button uk-button-default`(常规尺寸,与全站其它表单主按钮一致)。提问/私聊/投票同步生效。 +- 验证:go run ./dev/checktpl OK。 + diff --git a/.workbuddy/memory/2026-09-05.md b/.workbuddy/memory/2026-09-05.md new file mode 100644 index 0000000..d2ad474 --- /dev/null +++ b/.workbuddy/memory/2026-09-05.md @@ -0,0 +1,167 @@ +# 2026-09-05 工作日志 + +## 提问箱公开主页「加载更多」翻页失效(内容重复)排查 + +- 用户报告:提问列表加载完后「加载更多」按钮仍在,点击后加载相同内容。示例链接 `https://box.tama.guru/_/aoi/3105?t=Kmd0mu`(item 页底部嵌有该箱「历史提问」列表 + history-template 加载更多)。 +- 代码链路:`templates/question/history-template.html`(Alpine fetch) → `route/question/page.go ListAPI`(`/api/v1/user/{domain}/questions`) → `internal/db/questions.go GetByUserID`(PinFirst 复合键集游标 `isPinned.CreatedAtUnixNano.ID`)。 +- 线上实测(box.tama.guru, aoi 箱共 38 条已答公开问题): + - 首页 20 条 id 3109→3089;`next_cursor=0.1788566476678885000.3089`。 + - 携带任意真实 next_cursor 再次请求 → 仍返回首页同样 20 条(has_more 恒 true)→ 内容重复、无法翻页(已稳定复现)。 + - 部分异常游标返回空集/6 条等与源码 keyset 预期不符的怪异结果 → 指向线上二进制或 DB 环境与工作区源码假设不一致(时区/列精度/部署版本),单凭源码无法闭合解释。 +- 前端模板与工作区 0904 版逐字符一致(x-on:click.debounce、payload.next_cursor、注释),说明线上模板=新版;但服务端行为异常。 +- 历史:outputs/re-audit-2026-09-01.md P0-2 曾记录同款症状(前端回传旧格式游标→parsePinCursor 失败→永远第一页),当时修复为服务端下发 next_cursor;但线上当前仍复现,说明修复未生效或有残余缺陷。 +- 后续建议:生产库执行 keyset SQL 直查对照 + SHOW CREATE TABLE 确认列精度;代码侧做健壮化(游标未前进/解析失败 → 返回空 + has_more=false;前端按已见 id 集合防重)。 + +## 落地修复(用户选定:防重复兜底) + +1. `internal/db/questions.go`: + - `GetByUserID`:携带游标但 PinFirst 复合游标/纯 id 游标解析失败时,由「忽略游标静默回退首页」改为直接 `return []*Question{}, nil`(空列表 → 上层 has_more=false → 按钮收敛)。 + - `getBy`(GetByAskUserID 旧路径):同步改为解析失败返回空。 +2. `templates/question/history-template.html`: + - SSR range 项与 JS append 项均加 `data-qid`;点击时先扫描 `#question-list [data-qid]` 收集已见 id,过滤重复项;无新增条目时 `more=false`(按钮隐藏/显示无更多)。 + - 注释说明重复加载 bug 的历史形态。 +3. 验证:`go build ./internal/db/...`、`go vet ./internal/db/` 通过;模板测试 `TestTemplatesParse/TestTemplateCrossReferences/TestRenderPublicQuestionList/TestRenderPublicQuestionItem` 全 PASS。全量 `go build ./...` 因第三方 `chai2010/webp`(CGO,本地缺 C 库)预存失败,与本次改动无关。 +4. 待办(未做):生产侧核对部署二进制是否含 9/1 P0-2 修复、DB 列精度/时区一致性诊断;线上复现的 keyset 怪异行为(同 nano 不同 id 返回不同页等)仍未完全由源码闭合解释。 +- 为用户产出 PG 根因诊断脚本 outputs/pg-cursor-pagination-diagnose.sql:PART A 环境/表结构/集合核对(含 created_at 列精度、索引、answered_public_all 应=38 与线上 API 对齐);PART B 用 DO+临时表纯 SQL 复刻 ListAPI 复合键集翻页循环,逐页打印 ids、带死循环/重复页检测;PART C UnixNano 指纹核对(1788566476678885000 类)用于区分部署版本旧 vs DB 环境问题。ListAPI 等价条件已核对: FilterAnswered(True)+PrivateStatus=1+PinFirst+page_size=20+1 探测。待用户执行回贴结果判读。 +- 生产库真实结构确认(用户探查): schema=nekobox(search_path 默认 public 导致首版脚本找不到表);nekobox.questions: id numeric(seq nekobox.questions_id_seq), created_at/updated_at/deleted_at = timestamp(6) WITHOUT TIME ZONE, user_id bigint, answer text, is_private/is_pinned/is_read boolean NOT NULL;nekobox.users 含 domain。线上库与源码(GORM timestamptz 假设)不一致 -> 无时区列+Go UTC 解释=线上 keyset 错位头号嫌疑。产出 v2 脚本 outputs/pg-cursor-diagnose-v2-nekobox.sql:SET search_path=nekobox; PART A 结构/集合核对(应38)+DB墙钟vs线上Z文本目视(差8h即偏移实锤); PART B DO+临时表纯墙钟 keyset 复现(锚点 timestamp 变量避免时区换算, 重复页/死循环检测); PART C nano 指纹反推命中检测(match_3089)+Z文本墙钟命中。待用户执行回贴。 +- 【根因闭合】v2 脚本线上结果: aoi_user_id=329, answered_public_all=38(与线上API一致), 无置顶行(pinned=0)。DB 墙钟==线上 JSON Z 文本(无时区偏移,会话 Asia/Shanghai 不影响无时区列直比)。PART B 纯墙钟 keyset 翻页: 第1页20条(3109..3089)=线上首页, 第2页18条(3088,2883..2411), 无重复无跳行 -> **DB 层完全自洽**。PART C: nano=1788566476678885000 反推墙钟 00:01:16.678885 精确命中 3089 (match_3089=t), Z 文本命中 3109/3089 -> 服务端 encode 正确、无时区/精度错位。结论: DB/SQL 层彻底排除,线上"游标被无视返回首页"只能用【线上部署二进制与 0904 源码不一致(解析路径差异)】解释。修复=部署含防重兜底的最新源码; 部署后需 curl 复测翻页收敛。 +- 【真根因确认+已修复】部署 0904 后线上仍复现 -> 读 parsePinCursor 源码定位: `time.Unix(0, nano)` 返回 Local 时区,进程 TZ=Asia/Shanghai 时墙钟 +8h(00:01->08:01)。生产 questions.created_at 是 timestamp WITHOUT time zone,驱动按 time.Location 墙钟直发无时区列 -> `created_at < '08:01'` 对库内全部 00:xx 行恒真 -> keyset 退化为全量 -> 永远返回首页 has_more=true -> 无限循环。本地实验 tz_proof.go 证明 08:01 vs 00:01 偏移。已修复 internal/db/questions.go parsePinCursor: time.Unix(0,nano) -> time.Unix(0,nano).UTC()(含时区约定注释)。build/vet PASS。其余 UnixNano 用途(随机/文件名)无 keyset 风险。待用户重新部署后 curl 验收翻页收敛(38条唯一)。 +- 【修复验收通过】用户重新部署后线上验证:第1页20条(新增20) has_more=true -> 第2页18条(新增18) has_more=false -> 总计38条唯一,完全收敛 ✓。非法游标(abc)/旧纯id游标(3089)均返回 {"data":[],"has_more":false} -> 解析失败收敛兜底同时生效。根因(parsePinCursor 时区偏移)与今日两处防重兜底均已上线。问题闭环。 +- 用户拟将生产库克隆到本地实验环境做 timestamptz 迁移测试。本地已建独立 PG 测试实例: 127.0.0.1:5433, user=postgres, trust 认证, data 目录 $HOME/AppData/Local/Temp/tamabox-pg-clone-test/data (PostgreSQL 18 at C:/OtherFiles/PostgreSQL/18)。等待用户 pg_dump 生产库(Fc, --schema=nekobox) 后导入跑迁移验证(epoch 快照/转换/验收/翻页复现)。注意本地另有用户自己的 5432 实例(scram 认证, 无密码勿扰)。 +- 【timestamptz 迁移已在克隆库验证通过】用户 dump(nekobox_clone_20260905.dump, 资料库 drive 节点 6oAmSRkxq7DVFAnqRt8QEv) 下载后导入本地 5433 测试实例 nekobox_clone。结果: 16 表, aoi=38 与线上一致。实际无时区时间列仅 4 表 12 列: questions/users/censor_logs/upload_images(其余表本为 timestamptz)。Asia/Shanghai 会话下迁移 ALTER TYPE timestamptz USING AT TIME ZONE 'UTC': 1.7s 完成, epoch 前后差异 0 行, 残余 without=0; 关键行指纹 3109=1788569378648502000/3089=1788566476678885000/2411=1781735419615754000 与线上一致; keyset 翻页带 +00 参数精确翻出线上第2页同款 18 条; 索引完整; 回滚路径双向验证无损。产出 outputs/timestamptz-migration-online.sql(PART0 核对/1 快照/2 迁移/3 验收4项/4 注释回滚)。克隆测试实例与 dump 暂存本地 Temp, 待用户线上执行确认后清理。 +- 【线上 timestamptz 迁移执行成功】用户线上跑 timestamptz-migration-online.sql: PART0=4表12列, PART1快照12列, PART2迁移DO完成, PART3验收全绿: 3.1 epoch差异0行, 3.2 remain_without=0, 3.3 指纹 2411=1781735419615754000/3089=1788566476678885000/3109=1788569378648502000 与克隆库/线上游标一致, 3.4 keyset第2页18条(3088,2883..2411)同款。但端到端验收受阻: box.tama.guru 持续 HTTP 521(源站不可达, 页面+API均打不开, 观察约2.5min) -> 疑似用户正在重启/部署应用或服务宕机, 待服务恢复后重跑翻页验收脚本(38条唯一收敛)确认整体闭环; 本地5433克隆实例与dump暂留待最终验收后清理。 +- 【端到端验收通过·最终闭环】服务启动后重跑线上翻页: 第1页20条 has_more=true -> 第2页18条 has_more=false -> 38条唯一收敛 ✓。注意: 迁移后 JSON created_at 文本从 "...Z"(UTC) 变为 "...+08:00"(session TZ=Asia/Shanghai) —— 绝对时刻一致。前端消费核对: history-template SSR {{Date .CreatedAt}} 迁移前显示 UTC 墙钟(少8h) 而 JS dayjs() 追加项本地化 -> 首屏与追加时间曾不一致(隐藏显示bug), 迁移后 SSR 读 timestamptz 转会话时区 -> 两者一致且显示上海本地时间(对用户更正确); item.html/list.html 同理顺带修正。无需改代码, 属显示语义修正观察项。遗留: 用户清理 diag_test 报依赖错误 -> 解答用 DROP SCHEMA diag_test CASCADE (schema 内仅 snap_before/snap_after 两张自建快照表)。本地 5433 克隆实例+dump 待清理。 +- 【发现迁移基准错误+已产修复脚本】用户反馈: 真实本地时间 00:01 的提问页面显示 08:01 -> 存量墙钟实为"上海本地时间"(驱动按进程本地墙钟写无时区列, 读出按UTC标记, 文本一致造成 v2 诊断"对称"假象), 先前迁移用 AT TIME ZONE 'UTC' 把上海墙钟误标 UTC -> 存量绝对时刻整体 +8h。克隆库验证反向修正(USING c AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Shanghai', 净-8h): 3089 08:01->00:01:16+08 (epoch 1788566476678885000->1788537676678885000), 3109->00:49:38+08, 2411->06-17 22:30+08; keyset 第2页仍18条。产出 outputs/timestamptz-migration-fix-8h.sql (PART0 聚焦4表12列/1快照/2修正/3验收含3.1显示+epoch/3.2翻页/3.3计数/3.4残余/4注释回滚=反向AT TIME ZONE 'Asia/Shanghai' AT TIME ZONE 'UTC')。克隆库全流程演练通过。待用户线上执行贴回 + 我重跑 API 验收(next_cursor nano 应从 0.1788566476678885000 变 0.1788537676678885000)。 +- 【线上反向修正验收通过】用户线上执行 timestamptz-migration-fix-8h.sql(PART2 修正完成, 后续输出被 psql 分页截断不影响), 我直接 API 验证: next_cursor nano 1788566476678885000->1788537676678885000 ✓, created_at 文本回到本地墙钟 00:49:38.648502+08:00 / 末条 00:01:16.678885+08:00 ✓, 翻页 38 唯一收敛 ✓。存量时间语义修正为"上海墙钟"。注意: 该脚本不可重跑(会再-8h)。遗留: 用户如需补看验收可跑只读 SQL; diag_test 清理可用 DROP SCHEMA diag_test CASCADE; 本地 5433 克隆实例+dump 待清理(询问用户后执行)。 +- 【留档完成】事故复盘文档 outputs/TamaBox-v4-keyset-pagination-timezone-incident-2026-09-05.md:完整记录两层缺陷(①parsePinCursor time.Unix Local +8h 致 keyset 失效②timestamptz 迁移基准误用 UTC 致存量+8h 已反向修正)、时间线、4文件代码改动、SQL 脚本产物清单(标注 v1/v2 已淘汰、online.sql 基准错误勿复用、fix-8h.sql 不可重跑)、经验教训预防清单(无时区列+带时区代码混用是土壤/迁移前须用现实锚点确认数据语义/文本一致≠语义正确)、遗留建议(页面抽查/可选 UTC 输出/清理克隆实例与dump/生产 DROP SCHEMA diag_test CASCADE)。 + +## UIkit CSS 迁移公共 CDN(速度优化落地) + +- 背景:生产速度测试发现 R2 自托管 `uikit/3.3.3/css/modify-black-font/uikit.min.css`(263432B)加载 ~1100ms 是大头。 +- Diff 结论:自建版与官方 uikit@3.3.3(263444B,sha256 6630e078...)**仅 3 处颜色差异**——把有色容器(.uk-light/.uk-card-primary/.uk-card-secondary/.uk-overlay-primary/.uk-section-*/.uk-tile-*/.uk-offcanvas-bar)内默认白字/白标题改黑:容器文字与 table th `rgba(255,255,255,.7)→rgba(0,0,0,.7)`,h1-h6/.uk-h*/.uk-heading-* `#fff→#000`。 +- 模板实际仅用 `.uk-light`(用户背景封面,question/list.html、question-not-found.html;内部 h3/p 由 #usernamecolor/#introcolor 内联 JS 覆盖,优先级最高),其余有色容器全仓无 class 使用 → 补丁可只投影 .uk-light(477B 代替 9KB)。 +- 测速(本地国内网络,全量 263KB 3 次):cdnjs.snrat.com ~0.25s(热)最快 > jsdelivr ~0.47s > unpkg ~1.3s > cdnjs.cloudflare ~1.0-1.4s > R2 自建 1.4-2.0s。check-host.net 全球 12 节点:snrat 与 jsdelivr 均 12/12 = 200 OK(snrat 根路径 301 不影响带资源路径请求)。snrat 缓存头 max-age=31104000(360 天)。 +- 落地修改(仅 templates/base/header.html): + 1. css link → `https://cdnjs.snrat.com/ajax/libs/uikit/3.3.3/css/uikit.min.css` + 2. js(uikit.min.js/uikit-icons.min.js)→ `https://cdnjs.snrat.com/ajax/libs/uikit/3.3.3/js/...`(CDNJS 路径与 cdnjs 官方同构,已验证文件存在 Content-Type 正确) + 3. link 后新增补丁 ``, + } + for _, in := range cases { + got := string(MarkdownToHTML(in)) + if strings.Contains(got, "x`, "x"}, + {"quote-inject", `x`, "x"}, + {"url-inject", `x`, "x"}, + {"onattr-inject", `x`, "x"}, + {"long-value", `x`, "x"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := string(MarkdownToHTML(tc.in)) + if !strings.Contains(got, tc.want) { + t.Errorf("输出 %q 应包含 %q", got, tc.want) + } + // 注入的 color 值不得出现 + for _, evil := range []string{";", "position:", "onclick", "javascript:", "红字`, "红字"}, + {"link", "[百度](https://baidu.com)", "百度"}, + {"code", "`code`", "code"}, + {"list", "- 第一项\n- 第二项", "第一项"}, + {"block", "# 标题\n内容", "标题"}, + {"mixed", "问题:**重要** 详情 蓝", "重要"}, + {"empty", "", ""}, + // 危险标签被剥离:script 的内容是纯文本(绘制无风险),img 自闭合无文本则输出空 + {"xss-script", "", "alert(1)"}, + {"xss-img", ``, ""}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := strings.Join(strings.Fields(MarkdownToPlainText(tc.in)), " ") + if tc.want == "" { + if got != "" { + t.Errorf("MarkdownToPlainText(%q) = %q, 期望空", tc.in, got) + } + return + } + if !strings.Contains(got, tc.want) { + t.Errorf("MarkdownToPlainText(%q) = %q, 期望包含 %q", tc.in, got, tc.want) + } + // 输出不得残留任何标签(< 必须被剥离) + if strings.Contains(got, "<") { + t.Errorf("MarkdownToPlainText(%q) = %q, 不应包含标签字符", tc.in, got) + } + }) + } +} + +// TestSafeURL 是 P2-1 的回归测试:markdown 链接白名单必须拒绝协议相对 URL +// (//evil.com 等),否则会被误判为站内相对路径而渲染成指向外站的钓鱼 / 外链埋点链接。 +// 同时验证 javascript:/data:/ftp: 等危险协议与大小写变体均被拒绝,http/https/mailto +// 与站内相对路径被放行。 +func TestSafeURL(t *testing.T) { + allow := []string{ + "https://example.com", + "http://example.com", + "HTTPS://EXAMPLE.COM", // 大小写不敏感 + "mailto:foo@bar.com", + "/path/to/page", + "./relative", + "../up", + "#fragment", + } + for _, u := range allow { + if !safeURL(u) { + t.Errorf("safeURL(%q) = false, 期望放行", u) + } + } + + deny := []string{ + "//evil.com", // 协议相对 URL(核心回归点) + `\\evil.com`, // 反斜杠变体 + `/\evil.com`, // 反斜杠变体 + "javascript:alert(1)", // 危险协议 + "JavaScript:alert(1)", // 大小写变体 + "data:text/html,` 输出 `"\u003c/script\u003e"`。两种写法均安全,无需修改(仅风格不统一)。 + +2. **`multipart.FileHeader.Size` 不可伪造** —— 详见 P1-3 中的澄清说明。 + +3. **`service.Proxy` / `pixel.Proxy` 无 SSRF** —— 目标 Host 取自配置,用户仅控制路径且经 `path.Clean` 归一化,非用户可控目标。 + +--- + +## 五、经确认实现正确的部分 + +- **密码安全**:bcrypt(cost=10) + 每用户 salt,超 72 字节截断;旧 HMAC-SHA1 仅作兼容且登录时自动升级,旧路径用 `subtle.ConstantTimeCompare` +- **会话固定防护**:`login.go:67` 认证成功后先 `RegenerateID()` 再写入身份 +- **密码重置**:`randstr.String(64)` 加密级随机,24h 过期,一次性消费,按 code 直查无枚举面 +- **页面密码 / 提问密码**:均 bcrypt 存储与比较 +- **验证码**:`crypto/rand` 生成,答案仅存服务端 session,5 分钟 TTL,一次性,尝试上限 5 次,`ConstantTimeCompare` 比对 +- **SQL 注入**:`Raw`/`Exec` 仅用于固定 DDL;排序字段已归一化为 `ASC`/`DESC` 白名单;`LIKE` 有 `escapeLike` 转义 `% _ \`;`db.go` 的 `fmt.Sprintf` 仅拼接配置 schema 与硬编码表名 +- **越权防护**:`getOwnedVote` 校验 `vote.UserID`;`Questioner` 中间件校验归属并注入 `canDelete`;chat 处理器校验 `OwnerID`/`AskerID` +- **SSRF**:**实测全部拦截** —— `127.0.0.1:6379`、`169.254.169.254`(云元数据)、`localhost`、`10.0.0.1`、`[::1]`、`example.com` 均返回 `403 domain not allowed`,`file://` 返回 `400 invalid url`。白名单默认为空即 fail-closed;`proxyImageDialContext` 在拨号时重新解析并固定公网 IP,彻底防 DNS rebinding;`CheckRedirect` 对每跳重复校验 +- **开放重定向**:**实测全部拦截** —— `//evil.com`、`https://evil.com`、`\\evil.com`、`/\evil.com`、`////evil.com`、`/%09/evil.com`、`%2F%2Fevil.com` 一律回落到 `/` +- **XSS 主链路**:blackfriday 未开启 `HTML` 扩展(原始标签被转义),渲染后经白名单 sanitize + `on*` 属性剥离 + `safeURL`/`safeColorValue` 校验 + 文本节点二次转义;`toJSON` 依赖 `json.Marshal` 默认 HTML 转义 +- **HTTP 方法约束**:`site-stats` 等只读端点对 `POST`/`PUT`/`DELETE` 均不执行;登出已正确改为 POST +- **中间件顺序**:CSRF 校验先于限流,无效 token 不消耗限流配额 +- **并发安全**:`go test -race` 零 race;`rateLimiter.visitors`、`autoban.probes/banned`、`TTLCache.items`、`stats.cached`、`shared.App` 全部有锁保护;`captcha` 类型断言使用双返回值 +- **数据一致性**:追问创建用 `clause.Locking` 行锁;会话消息计数为原子 `UPDATE ... WHERE count < ?` 且失败回滚;投票提交有复合唯一索引兜底;配额用 `gorm.Expr` 并防负数 +- **敏感字段**:`Password`/`PagePassword`/`AskPassword`/`UID`/`BlockWords`/`FromIP`/`VoterIP` 均标 `json:"-"`;CSV/XLSX 导出有公式注入转义 +- **超时配置**:`http.Server` 四类超时齐备;所有外部 HTTP 调用均设 `Timeout` + +--- + +## 六、建议修复顺序 + +1. **P1-1 CSRF `SessionKey: "uid"`** —— 一行改动,收益最高,**建议补回归测试** +2. **P1-2 session cookie `Secure`** —— 一行改动 +3. **P1-3 `http.MaxBytesReader`** —— 防匿名磁盘耗尽 +4. **P1-4 IPv6 `/64` 聚合** —— 同时缓解内存膨胀 +5. **P2-1 `safeURL` 拒绝 `//`** —— 一行改动 +6. P1-5 与其余 P2 项按迭代节奏推进 + +--- + +## 七、修复状态(批次 A+B 已落地) + +| 项 | 修复 | 回归测试 | +|----|------|----------| +| P1-1 CSRF `SessionKey: "uid"` | `route.go` 改为包级常量 `CSRFUserSessionKey = "uid"` | `internal/route/csrf_binding_test.go`:`TestCSRFTokenBoundToUserSession`(内存会话重放跨会话 token 复用 → 期望 400;SessionKey 误改回 `"userID"` 即失败) | +| P1-2 session cookie `Secure` | `route.go`:`Secure = strings.HasPrefix(ExternalURL,"https")` | (一行常量改动,行为随部署 `ExternalURL` 而定;HTTPS 部署下自动 `Secure`) | +| P1-3 请求体上限 | `form/form.go`:`http.MaxBytesReader` 封顶 12 MiB | (`internal/form` 既有测试保持通过) | +| P1-4 IPv6 `/64` 聚合 | `shared/ip.go` 新增 `LimitKey`;`ratelimit.go`/`autoban.go`/NotFound 统一套用 | `internal/shared/ip_test.go`:`TestLimitKey` + `TestLimitKeyIPv6SamePrefixCollapses` | +| P2-1 `safeURL` 拒绝 `//` | `markdown.go`:先拒协议相对 URL(`//`、`\\`、`/\`) | `internal/template/markdown_test.go`:`TestSafeURL` | + +**验证**:`go build ./...` 与 `go vet ./...` 均干净;`internal/route`、`internal/shared`、`internal/template`、`internal/form` 测试全过(含上述 3 个新测试)。 +**说明**:CSRF 跨会话拒绝(最关键动态发现)已由 DB 无关集成测试覆盖;本沙箱无 SQL DB(`db.Init` 启动连库),故未在此重启动服务做完整动态复验,需在用户可达 SQL DB 的测试环境下复跑 SSRF / 开放重定向 / 限流等探针。 + +--- + +*报告完毕。全程未读取 `conf/app.ini`;测试服务已停止,临时文件已清理;审阅期间在 `internal/template/` 临时创建的验证测试文件已删除,工作区无残留改动。* diff --git a/outputs/shared-egress-vote-antispam-review-2026-08-26.md b/outputs/shared-egress-vote-antispam-review-2026-08-26.md new file mode 100644 index 0000000..678ef33 --- /dev/null +++ b/outputs/shared-egress-vote-antispam-review-2026-08-26.md @@ -0,0 +1,76 @@ +# 共享出口匿名投票防刷修改与测试报告 + +- **时间**:2026-08-26 +- **目标**:解决 Cloudflare + `trust_proxy=true` 场景下,局域网用户共享公网 IP 导致同一投票只能一人提交的问题。 +- **策略**:签名 `vote_fp` 负责设备级一人一票;IP 仅用于高阈值突发风控,不再承担同一投票的一票封顶。 + +## 修改内容 + +### 1. 删除匿名投票的 IP 一票封顶 + +`route/vote/vote.go`: + +- 移除提交处理器内 `CountByIP(... 24h)` 且 `count >= 1` 即拒绝的逻辑。 +- 匿名用户继续通过有效 HMAC `vote_fp` 与数据库唯一索引防止同一浏览器/设备重复投票。 +- 无效或缺失 Cookie 仍会被重新签发,不会成为可信身份。 + +### 2. 新增投票提交专用的身份优先限流 + +`internal/route/ratelimit.go`: + +新增 `voteSubmitRateLimitMiddleware`,投票提交同时经过两层限制: + +- 登录用户:按 `user:` 独立限流,额度为常规参数的 3 倍。 +- 匿名用户:按有效 `token:` 独立限流。 +- 所有匿名请求:同时经过 `ip:` 高阈值兜底限流,默认是传入基础额度的 10 倍。 +- 无效/缺失 Cookie 不会获得独立 token 限流身份,只能使用 IP 兜底。 +- 触发限制时返回 HTTP 429,不进入实际投票提交处理器。 + +`internal/route/route.go` 已将 `/vote/{token}/submit` 从普通 IP 限流切换到该专用中间件。 + +### 3. 校准 Cloudflare 客户端 IP 解析 + +`internal/shared/ip.go`: + +- `TrustProxy=true` 时优先读取 Cloudflare 官方 `CF-Connecting-IP`。 +- 若该头不存在或非法,再回退到 `X-Real-IP`、`X-Forwarded-For`,最后使用 `RemoteAddr`。 +- `TrustProxy=false` 时仍完全忽略这些请求头,只使用 `RemoteAddr`。 +- 新增 Cloudflare 头优先级和不信任代理时忽略头部的测试。 + +### 4. 更新设计文档 + +`anonymous-vote-design.md` 已同步新的防刷矩阵、token 流程和共享出口说明。 + +## 验证结果 + +通过: + +```text +gofmt -d internal/route/ratelimit.go internal/route/route.go \ + internal/shared/ip.go route/vote/vote.go +# 无输出 + +go test -count=1 ./internal/shared ./route/vote ./internal/template \ + ./templates ./internal/i18n ./internal/cache ./internal/security/autoban +# 全部 PASS + +go vet ./route/vote ./internal/shared ./internal/template ./templates ./internal/i18n +# 无输出 +``` + +补充测试覆盖: + +- 同一共享 IP 下多个不同 token 彼此独立; +- 同一 token 仍受独立限流; +- IP 兜底允许多次正常请求,但能阻断异常突发; +- Cloudflare `CF-Connecting-IP` 优先于其他代理头; +- 未启用可信代理时伪造的代理头不会生效。 + +`go test ./internal/route` 当前仍无法编译完成,原因是项目既有的 `github.com/chai2010/webp@v1.4.0` 在本机缺少 CGO/WebP 编译符号,并非本次限流逻辑测试失败。 + +## Cloudflare 部署注意事项 + +1. 源站防火墙应只允许 Cloudflare 官方 IP 段访问源站端口,否则攻击者可能直接访问源站并伪造 `CF-Connecting-IP`。 +2. 保持 `trust_proxy=true` 的前提是源站确实位于可信反向代理之后。 +3. 重要投票建议开启 Turnstile 或要求登录;匿名签名 Cookie 只能保证设备级防重,无法阻止清 Cookie、换浏览器或自动化客户端。 +4. 当前共享 IP 兜底阈值为 200 次/分钟(投票路由传入基础值 20),可根据真实并发量调整。 diff --git a/outputs/tb-dev-test-qr-crop.png b/outputs/tb-dev-test-qr-crop.png new file mode 100644 index 0000000000000000000000000000000000000000..67e48eb9a959fd6e4226dd18b213bcf923e8add3 GIT binary patch literal 53132 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

    KU)~$m)zL$Qr8@ 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 c7`tn*aJxSs>^V$-{V5i{-u{<=0YdM+L+Bks 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

    $kFlJjEpp=M@o+;*{KY7w?P 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

  • + + + + diff --git a/templates/base/header.html b/templates/base/header.html new file mode 100644 index 0000000..b50b75f --- /dev/null +++ b/templates/base/header.html @@ -0,0 +1,1031 @@ + + + + + + {{ .Title }} + + + + + + + + + + + + + + + + + + {{ if .VConsole }} + + + {{ end }} + + + {{ if eq .CaptchaType "math" }} + + + {{ else if .RecaptchaTurnstileStyle }} + + + {{ else }} + + + {{ end }} + {{ if ne SentryDSN "" }} + + + {{ end }} + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ if .NoIndex }}{{ end }} + {{ if .IsUserPage }} + + + + + + + + + {{ else }} + + + + + + + {{ end }} + + + + + + + + + + + + + + +
    + {{ if not .IsUserPage }} + + {{ end }} + +
    + + + +{{ if .IsPixel }} +
    +{{ else }} +
    +{{ end }} + +{{ if eq .CaptchaType "math" }} +
    +
    +

    {{t .LangCode "captcha.title"}}

    +

    {{t .LangCode "captcha.hint"}}

    +
    + {{t .LangCode "captcha.loading"}} + +
    +
    + +
    + +
    + + +
    +
    +
    + + + + +{{ end }} \ No newline at end of file diff --git a/templates/change-logs.html b/templates/change-logs.html new file mode 100644 index 0000000..57c3bac --- /dev/null +++ b/templates/change-logs.html @@ -0,0 +1,15 @@ +{{template "base/header" .}} +
    +

    修改记录

    +
    +

    为使首页更为简洁,今后修改的相关记录存档在此处(主要信息仍然会在首页提示)。

    +
    +
    +
    +
    +
    +
    +{{template "base/footer" .}} diff --git a/templates/chat/conversation.html b/templates/chat/conversation.html new file mode 100644 index 0000000..1d220e7 --- /dev/null +++ b/templates/chat/conversation.html @@ -0,0 +1,195 @@ +{{template "base/header" .}} + + +
    +

    + {{ if .IsOwner }}{{t .LangCode "chat.withAsker"}}{{ else }}{{t .LangCode "chat.withUser" .PageUser.Name}}{{ end }} +

    +

    + {{ if .Conversation.IsClosed }} + {{t .LangCode "chat.closed"}} + {{ else }} + {{t .LangCode "chat.remaining" (sub .Conversation.MaxMessages .Conversation.MessageCount) .Conversation.MaxMessages}} + {{ end }} +

    + +
    +
      + {{ range .Messages }} + {{ $isMyMessage := eq .SenderID $.LoggedUser.ID }} +
    • +
      +
      +
      + + + {{ if $isMyMessage }}{{t $.LangCode "chat.me"}}{{ else }}{{t $.LangCode "chat.other"}}{{ end }} + + + {{ .CreatedAt.Format "01-02 15:04" }} + +
      + {{ if and $.IsOwner $.IsBlocked (eq .SenderID $.Conversation.AskerID) }} +

      {{t $.LangCode "chat.userBlocked"}}

      + {{ else }} + {{ if .Content }} +

      {{ .Content }}

      + {{ end }} + {{ range (index $.MessageImages .ID) }} +
      + {{ .FileName }} +
      + {{ end }} + {{ end }} +
      +
      +
    • + {{ end }} +
    +
    + + {{ if .CanSend }} +
    +
    + {{ .CSRFTokenHTML }} + + +
    +
    + + {{t .LangCode "chat.addImage"}} + +
    + +
    + +
    + +
    +
    +
    + {{ else }} +
    +

    {{t .LangCode "chat.closedNoSend"}}

    + {{t .LangCode "common.backHome"}} +
    + {{ end }} + + {{ if .IsOwner }} +
    + + {{ if .IsBlocked }}{{t .LangCode "chat.unblock"}}{{ else }}{{t .LangCode "chat.block"}}{{ end }} + +
    +
    +

    {{ if .IsBlocked }}{{t .LangCode "chat.unblock"}}{{ else }}{{t .LangCode "chat.block"}}{{ end }}?

    +

    {{ if .IsBlocked }}{{t .LangCode "chat.confirmUnblock"}}{{ else }}{{t .LangCode "chat.confirmBlock"}}{{ end }}

    +
    + {{ .CSRFTokenHTML }} + +
    +
    +
    + + {{t .LangCode "chat.delete"}} +
    +
    +

    {{t .LangCode "qitem.danger"}}

    +

    {{t .LangCode "chat.deleteConfirm"}}

    +
    + {{ .CSRFTokenHTML }} + +
    +
    +
    +
    + {{ end }} + +
    + + +
    + × + +
    +
    + + + +{{template "base/footer" .}} \ No newline at end of file diff --git a/templates/embed.go b/templates/embed.go new file mode 100644 index 0000000..284f679 --- /dev/null +++ b/templates/embed.go @@ -0,0 +1,12 @@ +// Copyright 2022 E99p1ant. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package templates + +import ( + "embed" +) + +//go:embed auth base mail question user chat vote home.html about-me.html sponsor.html change-logs.html pixel.html maintenance-mode.html +var FS embed.FS diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..a43f14f --- /dev/null +++ b/templates/home.html @@ -0,0 +1,123 @@ +{{template "base/header" .}} +
    +

    {{t .LangCode "site.title"}}

    +

    重大消息!本站已切换到box.shiroko.one,原域名将在2026年12月24日到期,期间将通过301转发指向新域名,到期后旧域名将彻底不在使用。
    问:为什么?答:.guru域名连续涨价,现在一年三百出头了遭不住。
    问:我该做什么?答:记住新域名,如果之前有通过二维码分享出去的,记得替换成新的二维码。


    +


    ⭐一个无需注册即可提问的匿名提问箱。你不需要下载App,不需要绑定社交账号,甚至连邮箱都不需要是真的。
    ⭐本站在原版的基础上新增了问题页面的二维码、私聊、投票、追加提问等功能以及更多自定义内容。各位用户可自行修改二维码背景图、颜色等个性化配置。
    ⭐可以到站长唯一提问提问箱链接小狼的提问箱尝试相关功能或给我提意见。
    ⭐可以加群:小狼的群一起水群。

    +

    近期将国内段访问交由EdgeOne接管,境外访问照旧由CloudFlare提供,经测试国内访问速度有所提升。但因EdgeOne Free套餐不提供CIDR列表,IP放行采取自动抓取手动维护模式,如果出现521错误请刷新一下页面,过几天应该就好了。

    +

    近期更新:
    ⭐2026.9 增加了提问、投票、私聊的总开关,可以设置定时收集、定量收集或关闭功能,顺便调整了前后端显示,看起来更易用了一些。
    ⭐2026.8 新增了投票功能、追加提问、多语言、Markdown显示、置顶显示等多个功能,详情见这里。
    ⭐2026.8 放弃使用recaptcha等对国内用户非常不友好的验证方式,更新了密码加密方式,修复了图片上传等一系列问题。
    ⭐2026.5 增加了私聊功能,可进行连续性对话,详见介绍;另有增加提问箱的卡片分享,更直观和美观。
    ⭐2026.2 新增了用户展示和标签功能,如果你希望陌生人给你提问,可以在设置页面给自己贴上5个标签并选择是否展示在探索页面:点我访问,详情见狼的公告板

    +

    一点提醒:
    ⭐本站唯一指定认证站长:小狼,注意不要被骗。 +
    ⭐本站不允许垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义、灌水提问的出现,如有涉证或不良言论将直接删除。

    +
    +

    {{t .LangCode "site.stats"}}:

    {{if .SiteStats}}{{.SiteStats.Text}}{{end}}

    + + +
    + + {{template "base/footer" .}} \ No newline at end of file diff --git a/templates/mail/new-answer.html b/templates/mail/new-answer.html new file mode 100644 index 0000000..be78e5c --- /dev/null +++ b/templates/mail/new-answer.html @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + +
    +
    +
    +
    + 您在 狼的提问箱 的提问有了回复 +
    + + + + + + +
    +

    + {{.question}}

    +
    + + + + + + +
    +

    + {{.answer}}

    +
    +
    + +
    +
    +
    +
    + 我们向您发送这封邮件来告诉您在 狼的提问箱 的活动状态,若您未曾使用过 狼的提问箱,请忽略本邮件。 +
    +
    + {{ .year }} 狼的提问箱 +
    +
    +
    +
    +
    + + diff --git a/templates/mail/new-conversation.html b/templates/mail/new-conversation.html new file mode 100644 index 0000000..6e5449e --- /dev/null +++ b/templates/mail/new-conversation.html @@ -0,0 +1,15 @@ + + + + +

    您好!

    +

    您收到一个新的私聊对话,内容如下:

    +
    + {{.content}} +
    +

    点击下方链接查看并回复:

    +

    {{.link}}

    +
    +

    此邮件由系统自动发送,请勿回复。
    © {{.year}} 狼的提问箱

    + + \ No newline at end of file diff --git a/templates/mail/new-follow-up.html b/templates/mail/new-follow-up.html new file mode 100644 index 0000000..978d070 --- /dev/null +++ b/templates/mail/new-follow-up.html @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + +
    +
    +
    +
    + 您的提问有了一条追问 +
    + + + + + + +
    +

    + 原提问:{{.question}}

    +

    + 追问:{{.followup}}

    +
    +
    + +
    +
    +
    +
    + 我们向您发送这封邮件来告诉您账号的状态,若您未曾在 狼的提问箱 注册过账号,请忽略本邮件。 +
    +
    + {{ .year }} 狼的提问箱 +
    +
    +
    +
    +
    + + diff --git a/templates/mail/new-message.html b/templates/mail/new-message.html new file mode 100644 index 0000000..eaa8e2c --- /dev/null +++ b/templates/mail/new-message.html @@ -0,0 +1,15 @@ + + + + +

    您好!

    +

    您收到一条私聊新消息:

    +
    + {{.content}} +
    +

    点击下方链接查看对话:

    +

    {{.link}}

    +
    +

    此邮件由系统自动发送,请勿回复。
    © {{.year}} 狼的提问箱

    + + \ No newline at end of file diff --git a/templates/mail/new-question-to-user.html b/templates/mail/new-question-to-user.html new file mode 100644 index 0000000..66a7c88 --- /dev/null +++ b/templates/mail/new-question-to-user.html @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + +
    +
    +
    +
    + 您在 狼的提问箱 发送了一个提问 +
    + + + + + + +
    +

    {{.question}}

    +
    +
    + +
    +
    +
    +
    + 我们向您发送这封邮件来告诉您账号的状态,若您未曾在 狼的提问箱 进行过提问,请忽略本邮件。 +
    +
    + {{ .year }} 狼的提问箱 +
    +
    +
    +
    +
    + + diff --git a/templates/mail/new-question.html b/templates/mail/new-question.html new file mode 100644 index 0000000..8bef529 --- /dev/null +++ b/templates/mail/new-question.html @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + +
    +
    +
    +
    + 您的 狼的提问箱 收到一个新的提问 +
    + + + + + + +
    +

    {{.question}}

    +
    +
    + +
    +
    +
    +
    + 我们向您发送这封邮件来告诉您账号的状态,若您未曾在 狼的提问箱 注册过账号,请忽略本邮件。 +
    +
    + {{ .year }} 狼的提问箱 +
    +
    +
    +
    +
    + + diff --git a/templates/mail/new-vote.html b/templates/mail/new-vote.html new file mode 100644 index 0000000..10c3d62 --- /dev/null +++ b/templates/mail/new-vote.html @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + +
    +
    +
    +
    + 您的投票收到了新回复 +
    + + + + + + +
    +

    投票《{{.title}}》收到了一条新的表单回复,点击下方按钮查看。

    +
    +
    +
    + +
    + 提示:直接回复此邮件不能提交回答,请点击上方“查看回复”按钮完成操作。 +
    +
    +
    +
    +
    +
    +
    + 此邮件由系统自动发送,请勿直接回复。直接回复邮件不能实现回答,请通过邮件中的按钮访问页面。
    + 若您未曾在 狼的提问箱 创建过投票,请忽略本邮件。 +
    +
    + {{ .year }} 狼的提问箱 +
    +
    +
    +
    +
    + + diff --git a/templates/mail/password-recovery.html b/templates/mail/password-recovery.html new file mode 100644 index 0000000..ebb5235 --- /dev/null +++ b/templates/mail/password-recovery.html @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + +
    +
    +
    +
    + 您正在找回 狼的提问箱 注册账号的密码 +
    + + + + + + +
    + {{.email}} +
    +
    + +
    +
    +
    +
    + 我们向您发送这封邮件来告诉您账号的状态,若您未曾在 狼的提问箱 注册过账号,请忽略本邮件。 +
    +
    + {{ .year }} 狼的提问箱 +
    +
    +
    +
    +
    + + diff --git a/templates/mail/security-alert.html b/templates/mail/security-alert.html new file mode 100644 index 0000000..8e18d67 --- /dev/null +++ b/templates/mail/security-alert.html @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + +
    +
    +
    +
    + 检测到疑似批量扫描 +
    +
    + {{.time}} +
    +
    +
    +
    +
    来源 IP:{{.ip}}
    +
    处置:已自动临时封禁 {{.duration}} 分钟,到期自动解除
    +
    +
    命中路径样本:
    +
    + {{.paths}} +
    +
    + 该 IP 在统计窗口内频繁访问后台/敏感路径或产生大量 404,已自动加入临时封禁名单。如属误报,请检查服务器访问日志确认。 +
    +
    +
    +
    +
    +
    + 此邮件由系统自动发送,请勿直接回复。
    + {{ .year }} 狼的提问箱 +
    +
    +
    +
    +
    + + diff --git a/templates/maintenance-mode.html b/templates/maintenance-mode.html new file mode 100644 index 0000000..0de4842 --- /dev/null +++ b/templates/maintenance-mode.html @@ -0,0 +1,8 @@ +{{template "base/header" .}} +
    +

    {{t .LangCode "maintenance.title"}}

    +
    +

    {{t .LangCode "maintenance.desc"}}

    +
    +
    +{{template "base/footer" .}} \ No newline at end of file diff --git a/templates/percent_check_test.go b/templates/percent_check_test.go new file mode 100644 index 0000000..ec42fff --- /dev/null +++ b/templates/percent_check_test.go @@ -0,0 +1,148 @@ +package templates + +import ( + "bytes" + gotemplate "html/template" + "testing" + "time" + + "github.com/flamego/template" + + "github.com/tamakyi/TamaBox/internal/db" + "github.com/tamakyi/TamaBox/internal/dbutil" + templatepkg "github.com/tamakyi/TamaBox/internal/template" +) + +// TestPercentRender 渲染投票结果页并检查百分比与进度条是否真实输出。 +func TestPercentRender(t *testing.T) { + fs, err := template.EmbedFS(FS, ".", []string{".html"}) + if err != nil { + t.Fatalf("EmbedFS: %v", err) + } + tpl := gotemplate.New("Flamego.Template") + for _, f := range fs.Files() { + tt := tpl.New(f.Name()) + for _, funcMap := range templatepkg.FuncMap() { + tt.Funcs(funcMap) + } + dataBytes, err := f.Data() + if err != nil { + t.Fatalf("get data of %q: %v", f.Name(), err) + } + if _, err := tt.Parse(string(dataBytes)); err != nil { + t.Fatalf("parse %q: %v", f.Name(), err) + } + } + + data := map[string]interface{}{ + "LangCode": "zh-CN", + "IsUserPage": true, + "Vote": &db.Vote{Model: dbutil.Model{ID: 1}, Title: "表单结果", Type: db.VoteTypeForm}, + "Total": int64(1), + "HasTextFields": true, + "FormFields": []*db.VoteFormField{ + {Label: "满意度", FieldType: db.VoteFormFieldTypeRadio, Options: `["满意","一般"]`}, + }, + "Results": map[string]interface{}{ + "Type": "form", "FormCount": int64(1), "FormFieldStats": []interface{}{ + map[string]interface{}{ + "Field": map[string]interface{}{"Label": "满意度", "FieldType": "radio"}, + "Total": int64(3), + "OptionStats": []interface{}{ + map[string]interface{}{"Option": map[string]interface{}{"Content": "满意"}, "Count": int64(2)}, + map[string]interface{}{"Option": map[string]interface{}{"Content": "一般"}, "Count": int64(1)}, + }, + }, + }, + }, + "Submissions": []interface{}{ + map[string]interface{}{"CreatedAt": time.Now(), "Fields": map[string]interface{}{"满意度": "满意"}}, + }, + "CSRFTokenHTML": gotemplate.HTML(""), + } + + var buf bytes.Buffer + if err := tpl.ExecuteTemplate(&buf, "user/vote-results", data); err != nil { + t.Fatalf("execute: %v", err) + } + html := buf.String() + + // 2/3 取整 = 66%,1/3 取整 = 33%;进度条宽度同步渲染 + for _, want := range []string{"(66%)", "(33%)", "width: 66%", "width: 33%", "uk-progressbar"} { + if !bytes.Contains([]byte(html), []byte(want)) { + t.Errorf("HTML missing %q", want) + } + } +} + +// TestInlineTextRepliesRender 验证表单结果页:文本类字段的逐条提交内容内联在题目 +// 下方、默认收起(含 hidden 属性),点击"查看文本提交(N)"展开。 +func TestInlineTextRepliesRender(t *testing.T) { + fs, err := template.EmbedFS(FS, ".", []string{".html"}) + if err != nil { + t.Fatalf("EmbedFS: %v", err) + } + tpl := gotemplate.New("Flamego.Template") + for _, f := range fs.Files() { + tt := tpl.New(f.Name()) + for _, funcMap := range templatepkg.FuncMap() { + tt.Funcs(funcMap) + } + dataBytes, err := f.Data() + if err != nil { + t.Fatalf("get data of %q: %v", f.Name(), err) + } + if _, err := tt.Parse(string(dataBytes)); err != nil { + t.Fatalf("parse %q: %v", f.Name(), err) + } + } + + data := map[string]interface{}{ + "LangCode": "zh-CN", + "IsUserPage": true, + "Vote": &db.Vote{Model: dbutil.Model{ID: 1}, Title: "表单结果", Type: db.VoteTypeForm}, + "Total": int64(1), + "FormFields": []*db.VoteFormField{ + {Model: dbutil.Model{ID: 10}, Label: "建议", FieldType: db.VoteFormFieldTypeTextarea}, + }, + "Results": map[string]interface{}{ + "Type": "form", "FormCount": int64(1), "FormFieldStats": []interface{}{ + map[string]interface{}{ + "Field": map[string]interface{}{"ID": int64(10), "Label": "建议", "FieldType": "textarea"}, + "Total": int64(2), + "TextCount": int64(2), + }, + }, + }, + "TextReplies": map[string]interface{}{ + "建议": []interface{}{ + map[string]interface{}{"CreatedAt": time.Now(), "Text": "希望增加夜间模式"}, + map[string]interface{}{"CreatedAt": time.Now(), "Text": "页面很清爽"}, + }, + }, + "CSRFTokenHTML": gotemplate.HTML(""), + } + + var buf bytes.Buffer + if err := tpl.ExecuteTemplate(&buf, "user/vote-results", data); err != nil { + t.Fatalf("execute: %v", err) + } + html := buf.String() + + // 内联折叠入口 + 两则文本均存在 + 默认收起(hidden) + for _, want := range []string{ + "查看文本提交(2)", + "希望增加夜间模式", + "页面很清爽", + `id="replies-10"`, + `id="caret-10"`, + } { + if !bytes.Contains([]byte(html), []byte(want)) { + t.Errorf("HTML missing %q", want) + } + } + // 折叠内容默认 hidden,不随页面始终铺在底部 + if !bytes.Contains([]byte(html), []byte(`id="replies-10" hidden`)) { + t.Errorf("inline text replies should be hidden by default") + } +} diff --git a/templates/pixel.html b/templates/pixel.html new file mode 100644 index 0000000..03575c9 --- /dev/null +++ b/templates/pixel.html @@ -0,0 +1,19 @@ +{{template "base/header" .}} + +
    +

    + 请各位用户知悉并确认在使用画板功能时,不会发起及/或参与以下内容: + 1. 涉及地域、种族、性别、宗教、政治等方面的歧视性内容; + 2. 侮辱、诽谤他人,侵害他人合法权益的内容; + 3. 含有淫秽色情、血腥暴力、赌博的内容; + 4. 其他在法律法规中禁止的或不适宜的内容。 + 如果您实施了违反上述规范的行为,我们将视情况采取相关措施,并保留移交司法机关或者行政机关的权利。 +

    +
    + +
    + + +{{template "base/footer" .}} \ No newline at end of file diff --git a/templates/question/history-template.html b/templates/question/history-template.html new file mode 100644 index 0000000..ee27070 --- /dev/null +++ b/templates/question/history-template.html @@ -0,0 +1,83 @@ +{{$pinTitle := t .LangCode "ask.pinned"}} +
    + {{range $index, $elem := .PageQuestions}} +
    +
    + 查看回答 +
    + {{if $elem.IsPinned}}⭐{{end}} + {{Date $elem.CreatedAt "Y-m-d H:i:s"}} +
    +

    {{MarkdownPlainText $elem.Content}}

    +
    + {{end}} +
    + + +
    + +
    +
    + 无更多留言 +

    +
    +
    diff --git a/templates/question/item.html b/templates/question/item.html new file mode 100644 index 0000000..54d5591 --- /dev/null +++ b/templates/question/item.html @@ -0,0 +1,425 @@ +{{template "base/header" .}} + + + + +
    +
    +
    +
    {{Date .Question.CreatedAt "Y-m-d H:i:s"}}
    +
    {{MarkdownFormat .Question.Content}}
    + {{ if .AskUploadImages }} +
      + {{ range $index, $elem := .AskUploadImages }} +
      + + + +
      + {{ end }} +
    + {{ end }} +
    + + {{if ne .Question.Answer ""}} +
    + +
    +
    {{MarkdownFormat .Question.Answer}}
    + {{ if .AnswerUploadImages }} +
      + {{ range $index, $elem := .AnswerUploadImages }} +
      + + + +
      + {{ end }} +
    + {{ end }} +
    +

    {{t .LangCode "qitem.answeredBy" .PageUser.Name (Date .Question.UpdatedAt "Y-m-d H:i:s")}}

    + +
    + {{ if and (not .Question.IsPrivate) (ne .Question.Answer "") }} + 📤 {{t .LangCode "qitem.shareCard"}} + {{ end }} + {{ if .CanDelete }} +
    + +
    + {{ end }} +
    + {{ if not .CanDelete }} +
    + + {{ end }} + {{ if .CanDelete }} + + {{ end }} +
    + {{ else if .CanDelete }} +
    +
    +
    {{t .LangCode "qitem.answerTitle"}}
    +
    + {{ .CSRFTokenHTML }} +
    + +
    +
    + + + +
    + +
    +
    + {{ if ne .Question.ReceiveReplyEmail "" }} +
    +

    {{t .LangCode "qitem.emailNotifyHint"}}

    +
    + {{ end }} +
    + +
    + +
    + {{ if .Question.IsRead }} + ✓ {{t .LangCode "qlist.read"}} +
    + {{ .CSRFTokenHTML }} + +
    + {{ else }} +
    + {{ .CSRFTokenHTML }} + +
    + {{ end }} +
    +
    +
    + {{ end }} + + + {{ if and .CanDelete (ne .Question.Answer "") }} +
    +
    + ✓ {{t .LangCode "qlist.read"}} + {{t .LangCode "qitem.readLocked"}} +
    +
    + {{ end }} + + {{ if .FollowUps }} +
    + {{ range $index, $elem := .FollowUps }} +
    +

    {{t $.LangCode "followup.content"}}{{ $elem.Content }}

    + {{ if ne $elem.Answer "" }} + {{ if $.CanDelete }} +
    @{{ $.PageUser.Name }}:{{ MarkdownFormat $elem.Answer }}
    +
    + +
    + + {{ else }} +
    @{{ $.PageUser.Name }}:{{ MarkdownFormat $elem.Answer }}
    + {{ end }} + {{ else if $.CanDelete }} +
    + {{ $.CSRFTokenHTML }} + +
    + +
    +
    + {{ else }} +

    {{t $.LangCode "followup.waiting"}}

    + {{ end }} +
    + {{ end }} +
    + {{ end }} + + {{ if and .CanFollowUp (not .CanDelete) }} +
    +
    +
    {{t .LangCode "followup.askMore"}}
    +

    {{t .LangCode "followup.remaining" .FollowUpRemaining .FollowUpLimit}}

    +
    + {{ .CSRFTokenHTML }} + + +
    + +
    +
    +
    +
    + {{ end }} + + {{ if and .WaitingFollowUpReply (not .CanDelete) }} +
    +
    {{t .LangCode "followup.waitingReply"}}
    +
    + {{ end }} + + +
    +
    + +{{template "base/footer" .}} diff --git a/templates/question/list.html b/templates/question/list.html new file mode 100644 index 0000000..2934952 --- /dev/null +++ b/templates/question/list.html @@ -0,0 +1,310 @@ +{{template "base/header" .}} + + + + + +
    +
    +
    + +

    {{ .PageUser.Name }}

    +

    {{ .PageUser.Intro }}

    +
    + {{ if and .PageUser.Acfunlink (ne .PageUser.Acfunlink "none") }} + + acfunlink + + {{ end }} + {{ if and .PageUser.Bililink (ne .PageUser.Bililink "none") }} + + bililink + + {{ end }} + {{ if and .PageUser.Weibolink (ne .PageUser.Weibolink "none") }} + + weibolink + + {{ end }} + {{ if and .PageUser.Xiaohongshulink (ne .PageUser.Xiaohongshulink "none") }} + + xiaohongshulink + + {{ end }} + {{ if and .PageUser.Bloglink (ne .PageUser.Bloglink "none") }} + + bloglink + + {{ end }} +
    +
    +
    +
    + +
    +
    + {{ if .CanAsk }} + + + +
    + + +

    {{t .LangCode "ask.scan" .PageUser.Name}}
    {{ if .RegisterOnlyAsk }}{{t .LangCode "ask.registerOnly"}}{{ else }}{{t .LangCode "ask.anonymous"}}{{ end }}

    + + + + + + + {{ end }} + {{template "base/alert" .}} + + +
    + + {{ if .VoteEnabled }} + + {{ else }} + + + {{ end }} +
    + +
    + {{template "question/new-question-template" .}} + + {{ if ne (len .PageQuestions) 0 }} +

    {{t .LangCode "ask.history" .PageUser.Name}} ({{ .AnsweredCount }})

    + {{ template "question/history-template" . }} + {{ end }} +
    + + +
    +
    + +{{template "base/footer" .}} \ No newline at end of file diff --git a/templates/question/new-question-template.html b/templates/question/new-question-template.html new file mode 100644 index 0000000..ab70a6f --- /dev/null +++ b/templates/question/new-question-template.html @@ -0,0 +1,232 @@ +{{ if .AskClosedMessage }} +
    +
    +

    {{ .AskClosedMessage }}

    + {{ if .AskNextOpenAtStr }} +

    {{t .LangCode "ask.nextOpenAt" .AskNextOpenAtStr}}

    + {{ end }} + {{ if .AskLastCloseAtStr }} +

    {{t .LangCode "ask.lastCloseAt" .AskLastCloseAtStr}}

    + {{ end }} +
    + + + {{ if .AllowChat }} +
    + {{ if .LoggedUser }} +
    + {{ .CSRFTokenHTML }} + {{ if .PageUser.AskPassword }} + + {{ end }} + + +
    + +
    +
    + {{ if eq .CaptchaType "math" }} + + {{ else }} + + {{ end }} +
    {{t .LangCode "ask.chatLimit" .PageUser.ChatMaxMessages}}
    +
    +
    + {{ else }} + +
    +

    {{t .LangCode "ask.loginRequiredChat"}}

    + {{t .LangCode "ask.goLogin"}} +
    + {{ end }} +
    + {{ end }} + + {{ if and .AllowChat .LoggedUser }} + {{ if ne .CaptchaType "math" }} + + + {{ end }} + {{ end }} +
    +{{ else if not .CanAsk }} +
    +
    +

    {{t .LangCode "ask.needLogin"}}

    +
    + +
    +{{ else }} +
    + {{ .CSRFTokenHTML }} + + {{if .PageUser.AskPassword}} + + {{end}} + + + + +
    + +
    +
    + + + +
    + +
    +
    + +
    + + + +
    + +
    +
    +
    + {{ if eq .CaptchaType "math" }} + + {{ else }} + + {{ end }} +
    + {{ if not .AllowChat }} +
    + +
    + {{ else if .LoggedUser }} +
    + +
    {{t .LangCode "ask.chatLimit" .PageUser.ChatMaxMessages}}
    +
    + {{ else }} +
    + +
    + {{ end }} +
    +
    +
    + + +{{ end }} diff --git a/templates/question/new-question-template.html.bak b/templates/question/new-question-template.html.bak new file mode 100644 index 0000000..f1ad848 --- /dev/null +++ b/templates/question/new-question-template.html.bak @@ -0,0 +1,145 @@ +{{ if not .CanAsk }} +
    +
    +

    提问箱的主人设置了仅注册用户才能提问,你需要先登录才能向他提问。

    +
    + +
    +{{ else }} +
    + {{ .CSRFTokenHTML }} + + {{if .PageUser.AskPassword}} + + {{end}} + + + +
    + +
    +
    + + + +
    + +
    +
    + +
    + + + +
    + +
    +
    +
    + +
    + {{ if and .LoggedUser .PageUser.AllowChat }} +
    + +
    消息上限:{{.PageUser.ChatMaxMessages}}条
    +
    + {{ else if not .LoggedUser }} +
    + +
    + {{ else }} +
    + +
    + {{ end }} +
    +
    +
    + + +{{ end }} \ No newline at end of file diff --git a/templates/question/password.html b/templates/question/password.html new file mode 100644 index 0000000..92f8f2b --- /dev/null +++ b/templates/question/password.html @@ -0,0 +1,62 @@ +{{template "base/header" .}} + + + + + + + +
    +
    +
    + +
    +

    {{ .PageUser.Name }}{{t .LangCode "site.logoSuffix"}}

    +

    {{t .LangCode "password.desc"}}

    + + {{template "base/alert" .}} + +
    + {{ .CSRFTokenHTML }} + +
    +
    + + +
    +
    +
    + +
    +
    + + +
    +
    + +{{template "base/footer" .}} \ No newline at end of file diff --git a/templates/question/question-not-found.html b/templates/question/question-not-found.html new file mode 100644 index 0000000..beec2fc --- /dev/null +++ b/templates/question/question-not-found.html @@ -0,0 +1,106 @@ +{{template "base/header" .}} + + + + + +
    +
    +
    + +

    {{ .PageUser.Name }}

    +

    {{ .PageUser.Intro }}

    +
    + {{ if and .PageUser.Acfunlink (ne .PageUser.Acfunlink "none") }} + + acfunlink + + {{ end }} + {{ if and .PageUser.Bililink (ne .PageUser.Bililink "none") }} + + bililink + + {{ end }} + {{ if and .PageUser.Weibolink (ne .PageUser.Weibolink "none") }} + + weibolink + + {{ end }} + {{ if and .PageUser.Xiaohongshulink (ne .PageUser.Xiaohongshulink "none") }} + + xiaohongshulink + + {{ end }} + {{ if and .PageUser.Bloglink (ne .PageUser.Bloglink "none") }} + + bloglink + + {{ end }} +
    +
    +
    +
    + +
    +
    + {{ if .CanAsk }} + + + +
    + + +

    扫码向@{{ .PageUser.Name }}提问
    {{ if .RegisterOnlyAsk }}{{t .LangCode "ask.registerOnly"}}{{ else }}{{t .LangCode "ask.anonymous"}}{{ end }}

    + {{ end }} + {{template "base/alert" .}} + {{template "question/new-question-template" .}} + +
    + {{ if ne (len .PageQuestions) 0 }} +

    @{{ .PageUser.Name }} 以前回答过的问题 ({{ .AnsweredCount }})

    + {{ template "question/history-template" . }} + {{ end }} +
    +
    +{{template "base/footer" .}} \ No newline at end of file diff --git a/templates/question/success.html b/templates/question/success.html new file mode 100644 index 0000000..ae3a840 --- /dev/null +++ b/templates/question/success.html @@ -0,0 +1,54 @@ +{{template "base/header" .}} +
    +
    + +

    {{t .LangCode "success.title"}}

    +
    + +

    {{t .LangCode "success.yourQuestion"}}

    +
    {{MarkdownFormat .Question.Content}}
    + + {{ if .HasReceiveEmail }} +
    +

    {{t .LangCode "success.emailSent"}}

    +
    + {{ else }} +
    +

    {{t .LangCode "success.saveLink" | safe}}

    +
    + {{ end }} + +
    + +
    + + +
    +

    {{t .LangCode "success.token" .Question.Token}}

    +
    + + +
    + + +{{template "base/footer" .}} diff --git a/templates/render_public_test.go b/templates/render_public_test.go new file mode 100644 index 0000000..12eb2b8 --- /dev/null +++ b/templates/render_public_test.go @@ -0,0 +1,271 @@ +package templates + +import ( + gotemplate "html/template" + "strings" + "testing" + "time" + + "github.com/tamakyi/TamaBox/internal/conf" +) + +type mockPageQuestion struct { + ID uint + Content string + CreatedAt time.Time + IsPinned bool + Answer string +} + +func publicBaseData() map[string]interface{} { + pageUser := map[string]interface{}{ + "Domain": "tester", "Name": "测试箱主", "Avatar": "", "Background": "", + "Intro": "欢迎提问", "Usernamecolor": "#000", "Introcolor": "#666", + "Acfunlink": "none", "Bililink": "none", "Weibolink": "none", + "Xiaohongshulink": "none", "Bloglink": "none", + "Qrcodebackcolor": "#fff", "Qrcodecolor": "#000", "Dotscale": "0.35", + "BackgroundImage": "", "Backgroundimagealpha": "0.8", "Qrcodepdpcolor": "#000", + "HarassmentSetting": "disabled", "AllowChat": true, "ChatMaxMessages": 10, + } + return map[string]interface{}{ + "LangCode": "zh-CN", + "SupportedLangs": []map[string]string{{"Code": "zh-CN", "Name": "简体中文"}}, + "IsLogged": false, + "IsUserPage": true, + "IsOwnPage": false, + "IsPixel": false, + "VConsole": false, + "CSRFTokenHTML": gotemplate.HTML(``), + "RecaptchaSiteKey": "", + "CaptchaType": "recaptcha", + "PageUser": pageUser, + "PageUserRaw": pageUser, + "PageQuestions": []mockPageQuestion{ + {ID: 1, Content: "置顶的问题", CreatedAt: time.Now(), IsPinned: true, Answer: "答1"}, + {ID: 2, Content: "普通问题", CreatedAt: time.Now().Add(-time.Hour), IsPinned: false, Answer: "答2"}, + }, + "PageQuestionCursor": "2", + "CanAsk": true, + "AnsweredCount": 2, + "ICP": "", + // [limits] 全站上限(生产由 Contexter 全局注入 conf.Limits),供提问/回答/追问 + // 输入框 maxlength 与私聊发起框同源 + "Limits": conf.Limits, + } +} + +// TestRenderPublicQuestionList 渲染提问箱主页(含置顶条目),复现公开页 500。 +func TestRenderPublicQuestionList(t *testing.T) { + if err := renderWithData(t, "question/list", publicBaseData()); err != nil { + t.Fatalf("render question/list failed: %v", err) + } +} + +// TestRenderQuestionListClosed 提问功能关闭时:展示关闭原因 + 下次开放/上次关闭时间(问题 3 的模板路径)。 +func TestRenderQuestionListClosed(t *testing.T) { + base := publicBaseData() + base["CanAsk"] = false + base["AskState"] = map[string]interface{}{"Open": false, "Reason": "not_started"} + base["AskClosedMessage"] = "提问功能尚未开放" + base["AskNextOpenAtStr"] = "2026-09-02 10:00" + base["AskLastCloseAtStr"] = "2026-09-01 18:00" + + out, err := renderToString(t, "question/list", base) + if err != nil { + t.Fatalf("render question/list (closed) failed: %v", err) + } + if !strings.Contains(out, "提问功能尚未开放") { + t.Fatal("closed reason not rendered") + } + if !strings.Contains(out, "下次开放时间:2026-09-02 10:00") { + t.Fatal("next open time not rendered") + } + if !strings.Contains(out, "上次关闭时间:2026-09-01 18:00") { + t.Fatal("last close time not rendered") + } +} + +// TestRenderQuestionListClosedPermanent 一次性窗口结束后(无循环定时):只展示上次关闭时间,不展示下次开放。 +func TestRenderQuestionListClosedPermanent(t *testing.T) { + base := publicBaseData() + base["CanAsk"] = false + base["AskState"] = map[string]interface{}{"Open": false, "Reason": "ended"} + base["AskClosedMessage"] = "提问功能已结束收集" + base["AskNextOpenAtStr"] = "" + base["AskLastCloseAtStr"] = "2026-08-31 18:00" + + out, err := renderToString(t, "question/list", base) + if err != nil { + t.Fatalf("render question/list (ended) failed: %v", err) + } + if strings.Contains(out, "下次开放时间") { + t.Fatal("next open time should NOT be rendered when permanently closed") + } + if !strings.Contains(out, "上次关闭时间:2026-08-31 18:00") { + t.Fatal("last close time not rendered") + } +} + +// TestRenderQuestionListRegisterOnlyCaption 未开启「仅允许注册用户」时,二维码文案保持原样。 +func TestRenderQuestionListRegisterOnlyCaption(t *testing.T) { + base := publicBaseData() + base["RegisterOnlyAsk"] = false + out, err := renderToString(t, "question/list", base) + if err != nil { + t.Fatalf("render question/list failed: %v", err) + } + if !strings.Contains(out, "谁都可以以匿名的形式提问") { + t.Fatal("anonymous caption should be rendered when not register-only") + } + if strings.Contains(out, "仅允许注册用户向TA提问") { + t.Fatal("register-only caption should NOT be rendered when not register-only") + } +} + +// TestRenderQuestionListRegisterOnlyOn 开启「仅允许注册用户」(箱主登录态可提问)时,二维码文案联动(问题 2)。 +func TestRenderQuestionListRegisterOnlyOn(t *testing.T) { + base := publicBaseData() + base["CanAsk"] = true + base["RegisterOnlyAsk"] = true + out, err := renderToString(t, "question/list", base) + if err != nil { + t.Fatalf("render question/list failed: %v", err) + } + if !strings.Contains(out, "仅允许注册用户向TA提问") { + t.Fatal("register-only caption should be rendered when register-only enabled") + } + if strings.Contains(out, "谁都可以以匿名的形式提问") { + t.Fatal("anonymous caption should NOT be rendered when register-only enabled") + } +} + +// TestRenderQuestionListVoteDisabled 投票功能关闭时:投票 Tab 保留显示但置灰禁用,并带原因 tooltip。 +func TestRenderQuestionListVoteDisabled(t *testing.T) { + base := publicBaseData() + base["VoteEnabled"] = false + base["VoteClosedMessage"] = "该提问箱已关闭投票功能" + out, err := renderToString(t, "question/list", base) + if err != nil { + t.Fatalf("render question/list (vote disabled) failed: %v", err) + } + if !strings.Contains(out, "profile-tab is-disabled") { + t.Fatal("vote tab should stay visible but disabled") + } + if !strings.Contains(out, "disabled") { + t.Fatal("vote tab should carry disabled attribute") + } + if !strings.Contains(out, "该提问箱已关闭投票功能") { + t.Fatal("vote tab should carry the close reason tooltip") + } + // 注意:JS 守卫代码里也有 [data-profile-tab="votes"] 选择器,须按按钮元素精确断言 + if strings.Contains(out, `data-profile-tab="votes" role="tab"`) { + t.Fatal("disabled vote tab must NOT be switchable") + } +} + +// TestRenderQuestionListVoteEnabled 投票功能正常时:投票 Tab 可点击切换。 +func TestRenderQuestionListVoteEnabled(t *testing.T) { + base := publicBaseData() + base["VoteEnabled"] = true + out, err := renderToString(t, "question/list", base) + if err != nil { + t.Fatalf("render question/list (vote enabled) failed: %v", err) + } + if !strings.Contains(out, `data-profile-tab="votes"`) { + t.Fatal("vote tab should be switchable when enabled") + } + if strings.Contains(out, "profile-tab is-disabled") { + t.Fatal("vote tab must not be disabled when enabled") + } +} + +// TestRenderQuestionListChatOnly 提问功能关闭但私聊开启时:保留独立私聊入口(含已登录/未登录两种形态)。 +func TestRenderQuestionListChatOnly(t *testing.T) { + base := publicBaseData() + base["CanAsk"] = false + base["AskState"] = map[string]interface{}{"Open": false, "Reason": "disabled"} + base["AskClosedMessage"] = "提问箱主人已关闭提问功能" + base["AskNextOpenAtStr"] = "" + base["AskLastCloseAtStr"] = "" + base["AllowChat"] = true + base["ChatMaxMessages"] = 20 + base["LoggedUser"] = map[string]interface{}{"Domain": "tester", "Name": "测试箱主"} + + // 已登录:渲染独立私聊表单 + out, err := renderToString(t, "question/list", base) + if err != nil { + t.Fatalf("render question/list (chat only, logged in) failed: %v", err) + } + if !strings.Contains(out, "提问箱主人已关闭提问功能") { + t.Fatal("ask closed reason should still be shown") + } + if !strings.Contains(out, `id="chat-only-form"`) { + t.Fatal("chat-only form should be rendered when chat enabled and logged in") + } + if !strings.Contains(out, `id="chat-only-form" enctype="multipart/form-data"`) { + t.Fatal("chat-only form must be multipart, otherwise form.Bind panics on the Images field") + } + if !strings.Contains(out, `name="start_chat_val" value="true"`) { + t.Fatal("chat-only form must submit as a chat request") + } + if !strings.Contains(out, "在此处撰写你的私聊消息...") { + t.Fatal("chat placeholder should be rendered") + } + if !strings.Contains(out, "onChatOnlySubmit") { + t.Fatal("chat-only captcha callback should be defined for non-math captcha") + } + + // 未登录:渲染登录引导而非表单 + base["LoggedUser"] = nil + out, err = renderToString(t, "question/list", base) + if err != nil { + t.Fatalf("render question/list (chat only, anonymous) failed: %v", err) + } + if strings.Contains(out, `id="chat-only-form"`) { + t.Fatal("chat-only form must not be rendered for anonymous visitors") + } + if !strings.Contains(out, "/login?to=") { + t.Fatal("anonymous visitors should get a login link to start chats") + } +} + +// TestRenderQuestionListChatOnlyNoChat 提问关闭且私聊也关闭:不渲染私聊入口。 +func TestRenderQuestionListChatOnlyNoChat(t *testing.T) { + base := publicBaseData() + base["CanAsk"] = false + base["AskClosedMessage"] = "提问箱主人已关闭提问功能" + base["AllowChat"] = false + base["LoggedUser"] = map[string]interface{}{"Domain": "tester", "Name": "测试箱主"} + out, err := renderToString(t, "question/list", base) + if err != nil { + t.Fatalf("render question/list (all closed) failed: %v", err) + } + if strings.Contains(out, `id="chat-only-form"`) { + t.Fatal("chat-only form must not be rendered when chat is disabled") + } +} + +// TestRenderPublicQuestionItem 渲染单题页(含历史列表),复现公开页 500。 +func TestRenderPublicQuestionItem(t *testing.T) { + data := publicBaseData() + data["Question"] = map[string]interface{}{ + "ID": 1, "Content": "置顶的问题", "Answer": "答1", "CreatedAt": time.Now(), + "UpdatedAt": time.Now(), "IsPrivate": false, "IsPinned": true, + "Token": "abc123", "ReceiveReplyEmail": "", + } + data["QuestionToken"] = "abc123" + data["AskUploadImages"] = []interface{}{} + data["AnswerUploadImages"] = []interface{}{} + data["FollowUps"] = []interface{}{} + data["CanFollowUp"] = false + data["FollowUpRemaining"] = 0 + data["FollowUpLimit"] = 3 + data["IsOwnPage"] = true + data["IsQuestioner"] = false + data["IsBlocked"] = false + data["AllowChat"] = true + + if err := renderWithData(t, "question/item", data); err != nil { + t.Fatalf("render question/item failed: %v", err) + } +} diff --git a/templates/render_test.go b/templates/render_test.go new file mode 100644 index 0000000..a7ddb8f --- /dev/null +++ b/templates/render_test.go @@ -0,0 +1,313 @@ +package templates + +import ( + "bytes" + "fmt" + gotemplate "html/template" + "strings" + "testing" + "time" + + "github.com/flamego/template" + + "github.com/tamakyi/TamaBox/internal/conf" + "github.com/tamakyi/TamaBox/internal/db" + templatepkg "github.com/tamakyi/TamaBox/internal/template" +) + +// renderWithData 用给定数据渲染模板(复刻生产环境 Templater + FuncMap)。 +func renderWithData(t *testing.T, name string, data map[string]interface{}) error { + t.Helper() + _, err := renderToString(t, name, data) + return err +} + +// renderToString 渲染模板并返回输出,供需要断言渲染结果的用例使用。 +func renderToString(t *testing.T, name string, data map[string]interface{}) (string, error) { + t.Helper() + fs, err := template.EmbedFS(FS, ".", []string{".html"}) + if err != nil { + t.Fatalf("EmbedFS: %v", err) + } + + tpl := gotemplate.New("Flamego.Template") + for _, f := range fs.Files() { + tt := tpl.New(f.Name()) + for _, funcMap := range templatepkg.FuncMap() { + tt.Funcs(funcMap) + } + dataBytes, err := f.Data() + if err != nil { + t.Fatalf("get data of %q: %v", f.Name(), err) + } + if _, err := tt.Parse(string(dataBytes)); err != nil { + t.Fatalf("parse %q: %v", f.Name(), err) + } + } + + var buf bytes.Buffer + if err := tpl.ExecuteTemplate(&buf, name, data); err != nil { + return "", err + } + return buf.String(), nil +} + +// mockLoggedUser 覆盖 templates/user/profile.html 引用到的 LoggedUser 字段。 +type mockLoggedUser struct { + Domain string + Name string + Email string + Avatar string + Background string + BackgroundImage string + Backgroundimagealpha string + Intro string + Introcolor string + Usernamecolor string + Qrcodebackcolor string + Qrcodecolor string + Qrcodepdpcolor string + Dotscale string + UseCustomQRColor bool + Notify string + Language string + HarassmentSetting string + BlockWords string + Tags string + ShowInHome bool + Acfunlink string + Bililink string + Weibolink string + Xiaohongshulink string + Bloglink string + ChatMaxMessages int + DailyMessageLimit int +} + +// profileBaseData 构造 /user/profile 渲染所需的基础数据。 +func profileBaseData() map[string]interface{} { + return map[string]interface{}{ + "Title": "设置", + "LangCode": "zh-CN", + "SupportedLangs": []map[string]string{{"Code": "zh-CN", "Name": "简体中文"}}, + "IsLogged": true, + "LoggedUser": &mockLoggedUser{ + Domain: "tester", Name: "测试箱主", Email: "t@example.com", + Usernamecolor: "#000", Introcolor: "#666", Qrcodecolor: "#000", + Qrcodepdpcolor: "#000", Qrcodebackcolor: "#fff", Dotscale: "0.35", + Backgroundimagealpha: "0.8", Notify: "email", Language: "zh-CN", + HarassmentSetting: "none", BlockWords: "广告,spam", Tags: "杂谈", + ChatMaxMessages: 20, DailyMessageLimit: 0, + }, + "CSRFTokenHTML": gotemplate.HTML(``), + "IsUserPage": false, + "IsPixel": false, + "VConsole": false, + "HasPagePassword": false, + "HasAskPassword": true, + "ActiveProfileTab": "account", + "ICP": "", + // 标签 / 屏蔽词上限,供标签块输入组件做实时提示(生产由 route/user.Profile 注入, + // 值来自 conf.Limits) + "TagLimits": map[string]int{ + "MaxTags": conf.Limits.MaxTags, + "MaxTagLen": conf.Limits.MaxTagLen, + "MaxBlockWords": conf.Limits.MaxBlockWords, + "MaxBlockWordLen": conf.Limits.MaxBlockWordLen, + }, + // 私聊面板数值上下界(同样来自 conf.Limits) + "ChatLimits": map[string]int{ + "MaxChatMessages": conf.Limits.ChatMaxMessages, + "DailyMessageLimitMax": conf.Limits.DailyMessageLimitMax, + }, + } +} + +// TestRenderUserProfileTabs 渲染整合后的账号设置页:用户设置 + 提问/私聊/投票 三个功能面板, +// 覆盖"开启 / 手动关闭 / 已达定量上限"三种功能状态。 +func TestRenderUserProfileTabs(t *testing.T) { + type mockControl struct { + Enabled bool + OpenAt *time.Time + CloseAt *time.Time + QuotaLimit int64 + QuotaCount int64 + QuotaResetAt *time.Time + } + type mockState struct { + Enabled bool + Open bool + Reason string + QuotaLimit int64 + QuotaCount int64 + } + type mockFeature struct { + Key string + LangCode string + NameText string + StatusText string + CSRFTokenHTML interface{} + Control *mockControl + State *mockState + OpenAtStr string + CloseAtStr string + QuotaResetAtStr string + ScheduleDays string + ScheduleStart string + ScheduleEnd string + Mode string + } + + resetAt := time.Now().Add(-24 * time.Hour) + csrf := gotemplate.HTML(``) + + base := profileBaseData() + base["QuotaLimitMax"] = db.QuotaLimitMax + base["CSRFTokenHTML"] = csrf + base["FeatureQuestion"] = &mockFeature{ + Key: "question", LangCode: "zh-CN", NameText: "提问", StatusText: "进行中", CSRFTokenHTML: csrf, + Control: &mockControl{Enabled: true}, + State: &mockState{Enabled: true, Open: true}, + Mode: "always", + } + // 手动关闭 + base["FeatureChat"] = &mockFeature{ + Key: "chat", LangCode: "zh-CN", NameText: "私聊", StatusText: "已关闭", CSRFTokenHTML: csrf, + Control: &mockControl{Enabled: false}, + State: &mockState{Enabled: false, Reason: "disabled"}, + Mode: "always", + } + // 定时 + 定量:已达上限(历史数据存在循环定时,模式推断为 recurring) + base["FeatureVote"] = &mockFeature{ + Key: "vote", LangCode: "zh-CN", NameText: "投票", StatusText: "已达上限", CSRFTokenHTML: csrf, + Control: &mockControl{Enabled: true, QuotaLimit: 100, QuotaCount: 100, QuotaResetAt: &resetAt}, + State: &mockState{Enabled: true, Reason: "quota_reached", QuotaLimit: 100, QuotaCount: 100}, + OpenAtStr: "2026-09-01T09:00", CloseAtStr: "2026-09-30T18:00", + QuotaResetAtStr: resetAt.Format("2006-01-02 15:04"), + ScheduleDays: "1,3,5", ScheduleStart: "09:00", ScheduleEnd: "18:00", + Mode: "recurring", + } + + // 逐个面板渲染:模板能正常执行,且只有目标面板可见、其余面板带 hidden + all := []string{"account", "question", "chat", "vote"} + for _, tab := range all { + base["ActiveProfileTab"] = tab + out, err := renderToString(t, "user/profile", base) + if err != nil { + t.Fatalf("render user/profile (tab=%s) failed: %v", tab, err) + } + if tab == "account" { + // 标签块组件的上限常量已注入(缺注入时 with 会静默跳过,页面失去计数/锁定能力) + wantMax := fmt.Sprintf(`data-tb-max="%d" data-tb-max-len="%d"`, conf.Limits.MaxTags, conf.Limits.MaxTagLen) + if !strings.Contains(out, wantMax) { + t.Fatalf("account: 标签输入缺 data-tb-max 注入,want %q", wantMax) + } + wantBlock := fmt.Sprintf(`data-tb-max="%d" data-tb-max-len="%d"`, conf.Limits.MaxBlockWords, conf.Limits.MaxBlockWordLen) + if !strings.Contains(out, wantBlock) { + t.Fatalf("account: 屏蔽词输入缺 data-tb-max 注入,want %q", wantBlock) + } + } + if tab == "chat" { + // 私聊面板的 number 输入框 min/max 与 conf.Limits 同源 + wantChat := fmt.Sprintf(`name="chat_max_messages" class="uk-input uk-width-small" type="number" min="1" max="%d"`, conf.Limits.ChatMaxMessages) + if !strings.Contains(out, wantChat) { + t.Fatalf("chat: 会话条数输入缺 conf 上限,want %q", wantChat) + } + wantDaily := fmt.Sprintf(`name="chat_daily_limit" class="uk-input uk-width-small" type="number" min="0" max="%d"`, conf.Limits.DailyMessageLimitMax) + if !strings.Contains(out, wantDaily) { + t.Fatalf("chat: 每日条数输入缺 conf 上限,want %q", wantDaily) + } + } + for _, other := range all { + want := fmt.Sprintf(`data-profile-panel="%s"`, other) + visible := strings.Contains(out, want+">") + hidden := strings.Contains(out, want+" hidden>") + if other == tab && !visible { + t.Fatalf("tab=%s: 面板 %s 未渲染为可见", tab, other) + } + if other != tab && !hidden { + t.Fatalf("tab=%s: 面板 %s 未隐藏", tab, other) + } + } + } +} + +// TestRenderUserQuestionList 模拟 /user/questions 的数据渲染,复现 500 错误。 +func TestRenderUserQuestionList(t *testing.T) { + type mockItem struct { + Type string + CreatedAt time.Time + Content string + Link string + IsAnswered bool + IsRead bool + IsPrivate bool + IsBlocked bool + QuestionID uint + IsPinned bool + PinLink string + } + type mockConv struct { + Token string + CreatedAt time.Time + } + type mockConvDetail struct { + Conversation *mockConv + OwnerDomain string + OwnerName string + FirstMessage string + IsBlocked bool + } + + base := map[string]interface{}{ + "Title": "我的提问", + "LangCode": "zh-CN", + "SupportedLangs": []map[string]string{{"Code": "zh-CN", "Name": "简体中文"}}, + "IsLogged": true, + "LoggedUser": map[string]interface{}{"Domain": "test", "Name": "tester"}, + "LoggedUserName": "tester", + "CurrentURI": "/user/questions", + "IsUserPage": true, + "IsPixel": false, + "VConsole": false, + "CSRFTokenHTML": gotemplate.HTML(``), + "RecaptchaSiteKey": "", + "CaptchaType": "recaptcha", + "PageUser": map[string]interface{}{"Domain": "test", "Name": "tester", "Avatar": ""}, + "ICP": "", + "Stats": map[string]int64{ + "total": 3, "answered": 2, "unanswered": 1, "read": 2, "unread": 1, "private": 1, "pinned": 1, + }, + "ChatStats": map[string]int64{ + "total": 2, "received": 1, "initiated": 1, "closed": 0, + }, + "Keyword": "", + "SelectedSort": "latest", + "SelectedStatus": "all", + "SelectedRead": "all", + "SelectedType": "all", + "ReceivedItems": []interface{}{ + mockItem{Type: "question_public", CreatedAt: time.Now(), Content: "你好吗?", Link: "/_/test/1", + IsAnswered: true, IsRead: true, QuestionID: 1, IsPinned: true, PinLink: "/_/test/1/pin"}, + mockItem{Type: "question_private", CreatedAt: time.Now(), Content: "悄悄话", Link: "/_/test/2", + IsAnswered: false, IsPrivate: true, QuestionID: 2, PinLink: "/_/test/2/pin"}, + }, + "ReceivedChats": []interface{}{ + mockItem{Type: "chat", CreatedAt: time.Now(), Content: "你好", Link: "/_/test/chat/abc", IsBlocked: false}, + }, + "ConversationDetails": []interface{}{ + mockConvDetail{Conversation: &mockConv{Token: "abc", CreatedAt: time.Now()}, + OwnerDomain: "other", OwnerName: "别人", FirstMessage: "hi", IsBlocked: false}, + }, + } + + if err := renderWithData(t, "user/question-list", base); err != nil { + t.Fatalf("render user/question-list failed: %v", err) + } + + // 带搜索关键词的场景 + base["Keyword"] = "hello" + if err := renderWithData(t, "user/question-list", base); err != nil { + t.Fatalf("render user/question-list (keyword) failed: %v", err) + } +} diff --git a/templates/sponsor.html b/templates/sponsor.html new file mode 100644 index 0000000..ae8c551 --- /dev/null +++ b/templates/sponsor.html @@ -0,0 +1,108 @@ +{{template "base/header" .}} + +
    +
    +
    +

    给 NekoBox 打钱

    +

    NekoBox 创立于 2020 年疫情初期,一开始是我疫情在家花了三天时间随手写得一个匿名留言箱。

    +

    目前 NekoBox 的服务器、CDN 开销还是我一个人承担,如果你也想为我分担一些,欢迎给 NekoBox 打钱!👏🏻

    +
    +
    + +
    +
    +
    感谢以下小伙伴对 NekoBox 的支持!
    + + + + + + + + + + +
    日期附言金额
    +
    +{{template "base/footer" .}} diff --git a/templates/templates_test.go b/templates/templates_test.go new file mode 100644 index 0000000..5e83e39 --- /dev/null +++ b/templates/templates_test.go @@ -0,0 +1,85 @@ +// Copyright 2022 E99p1ant. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package templates + +import ( + gotemplate "html/template" + "regexp" + "testing" + + "github.com/flamego/template" + + templatepkg "github.com/tamakyi/TamaBox/internal/template" +) + +// TestTemplatesParse 复刻生产环境(internal/route/route.go 中 +// template.Templater + templatepkg.FuncMap())的模板加载逻辑: +// 逐个模板注册 FuncMap 并解析,任一文件解析失败即报错。 +func TestTemplatesParse(t *testing.T) { + fs, err := template.EmbedFS(FS, ".", []string{".html"}) + if err != nil { + t.Fatalf("EmbedFS: %v", err) + } + + tpl := gotemplate.New("Flamego.Template") + for _, f := range fs.Files() { + tt := tpl.New(f.Name()) + for _, funcMap := range templatepkg.FuncMap() { + tt.Funcs(funcMap) + } + + data, err := f.Data() + if err != nil { + t.Fatalf("get data of %q: %v", f.Name(), err) + } + + if _, err := tt.Parse(string(data)); err != nil { + t.Errorf("parse %q: %v", f.Name(), err) + } + } +} + +var ( + // definePattern 匹配 {{define "x"}} / {{block "x" ...}}:定义模板。 + definePattern = regexp.MustCompile(`\{\{\s*(?:define|block)\s+"([^"]+)"`) + // refPattern 匹配 {{template "x" ...}}:引用模板。 + refPattern = regexp.MustCompile(`\{\{\s*template\s+"([^"]+)"`) +) + +// TestTemplateCrossReferences 检查所有 {{template "x"}} 引用的目标模板 +// 是否真实存在(文件名或 define/block 定义),避免运行时 "no such template"。 +func TestTemplateCrossReferences(t *testing.T) { + fs, err := template.EmbedFS(FS, ".", []string{".html"}) + if err != nil { + t.Fatalf("EmbedFS: %v", err) + } + + // 集合 = 所有模板文件名 + 文件内 define/block 定义的名称 + names := make(map[string]bool) + fileData := make(map[string][]byte) + for _, f := range fs.Files() { + names[f.Name()] = true + data, err := f.Data() + if err != nil { + t.Fatalf("get data of %q: %v", f.Name(), err) + } + fileData[f.Name()] = data + } + + for _, data := range fileData { + for _, m := range definePattern.FindAllStringSubmatch(string(data), -1) { + names[m[1]] = true + } + } + + // 检查所有引用是否落在集合内 + for name, data := range fileData { + for _, m := range refPattern.FindAllStringSubmatch(string(data), -1) { + if !names[m[1]] { + t.Errorf("%s: 引用了未定义的模板 %q", name, m[1]) + } + } + } +} diff --git a/templates/user/deactivate.html b/templates/user/deactivate.html new file mode 100644 index 0000000..3e71042 --- /dev/null +++ b/templates/user/deactivate.html @@ -0,0 +1,17 @@ +{{template "base/header" .}} +
    +
    + {{ .CSRFTokenHTML }} + {{t .LangCode "deactivate.title"}} + {{template "base/alert" .}} +
    + {{t .LangCode "deactivate.desc"}} +
    +
    + +
    +
    +
    +{{template "base/footer" .}} diff --git a/templates/user/explore.html b/templates/user/explore.html new file mode 100644 index 0000000..f50de58 --- /dev/null +++ b/templates/user/explore.html @@ -0,0 +1,621 @@ +{{template "base/header" .}} + + +
    +

    {{t .LangCode "explore.title"}}

    + +
    +

    {{t .LangCode "explore.notice" | safe}}

    +
    + +
    + + +
    + +
    +
    +
    +

    {{t .LangCode "explore.recommend"}}

    + +
    +
    + {{if .RecommendUsers}} + {{range $user := .RecommendUsers}} + + {{end}} + {{else}} +
    {{t .LangCode "explore.noRecommend"}}
    + {{end}} +
    +
    + + + {{if .HotTags}} +
    +

    {{t .LangCode "explore.hotTags"}}

    +
    + {{range $i, $tag := .HotTags}} + #{{$tag}} + {{end}} +
    +
    + {{end}} + +
    +
    +
    +
    + + +
    +
    +
    + +
    +
    +
    + + {{if .Users}} +
    + {{range .Users}} + {{$user := .}} +
    +
    +
    +
    + avatar +
    +
    + +
    + {{if $user.Intro}}{{$user.Intro}}{{else}}{{t $.LangCode "explore.lazy"}}{{end}} +
    +
    + {{if $user.TagList}} + {{range $index, $tag := $user.TagList}} + {{$tag}} + {{end}} + {{else}} + {{t $.LangCode "explore.noTags"}} + {{end}} +
    +
    +
    +
    +
    + {{end}} +
    + + + {{else}} +
    {{t .LangCode "explore.noResult"}}
    + {{end}} +
    + + +
    + + +{{template "base/footer" .}} diff --git a/templates/user/feature-control-form.html b/templates/user/feature-control-form.html new file mode 100644 index 0000000..0345d3a --- /dev/null +++ b/templates/user/feature-control-form.html @@ -0,0 +1,135 @@ + +
    +
    +

    {{ .NameText }}

    + {{ .StatusText }} +
    + {{t .LangCode (print "admin.featureHint." .Key)}} + +
    + {{ .CSRFTokenHTML }} + +
    + +
    +
    + {{t .LangCode "admin.featureMode"}} +
    + + + +
    + {{t .LangCode "admin.featureModeHint"}} +
    +
    + + + {{t .LangCode "admin.featureTimeHint"}} +
    +
    + +
    +
    + {{ range $i := loop 7 }} + + {{ end }} +
    +
    + + +
    + {{t .LangCode "admin.featureScheduleHint"}} +
    +
    + {{ if ne .Key "chat" }} +
    + + {{t .LangCode "admin.featureQuotaHint"}} +
    +
    + {{t .LangCode "admin.featureQuotaCount"}}:{{ .State.QuotaCount }} + {{ if .Control.QuotaLimit }}/ {{ .Control.QuotaLimit }}{{ else }}({{t .LangCode "admin.featureQuotaUnlimited"}}){{ end }} + {{ if .Control.QuotaResetAt }}
    {{t .LangCode "admin.featureQuotaResetAt"}} {{ .QuotaResetAtStr }}{{ end }} +
    + {{ end }} +
    + {{ if ne .Key "chat" }} + + {{ end }} + +
    +
    + + {{ if ne .Key "chat" }} +
    + {{ .CSRFTokenHTML }} + +
    + {{ end }} + + +
    diff --git a/templates/user/global-blockwords.html b/templates/user/global-blockwords.html new file mode 100644 index 0000000..a59668e --- /dev/null +++ b/templates/user/global-blockwords.html @@ -0,0 +1,29 @@ + +{{ $root := .Root }} +
    +
    + {{ $root.CSRFTokenHTML }} + {{t $root.LangCode "admin.globalBlockWords"}} +
    +

    {{t $root.LangCode "admin.globalBlockWordsHint"}}

    + + +
    +
    + +
    +
    +
    diff --git a/templates/user/profile.html b/templates/user/profile.html new file mode 100644 index 0000000..739e561 --- /dev/null +++ b/templates/user/profile.html @@ -0,0 +1,1528 @@ +{{template "base/header" .}} + + + + + + + + + + + + + + + + +{{template "base/alert" .}} + + +
    +
    + +
    +

    {{t .LangCode "profile.cropTitle"}}

    +
    +
    +
    + +
    +

    +
    + +
    +
    + + +
    + + + + +
    + + +
    +
    + {{ .CSRFTokenHTML }} + + +
    +

    {{t .LangCode "profile.card.basic"}}

    +
    + + +
    +
    + + +
    +
    + +
    + +
    +
    + + +
    +
    + +
    + +
    +
    + + + {{t .LangCode "admin.langHint"}} +
    +
    + + +
    +
    + +
    +
    + + +
    +

    {{t .LangCode "profile.card.avatarBg"}}

    +
    +
    + + + + 1:1 +
    + + +
    + {{t .LangCode "profile.avatarCropHint"}} +
    +
    + + + + 16:9 +
    + + +
    + {{t .LangCode "profile.backgroundCropHint"}} +
    +
    + +
    + +
    +
    + + +
    +
      +
    • + {{t .LangCode "profile.card.qr"}} +
      +
      +
      +
      + +
      + +
      +
      + +
      + +
      +
      + +
      + +
      +
      + + {{t .LangCode "admin.customFgHint"}} +
      +
      + +
      + + {{.LoggedUser.Dotscale}} + +
      +
      +
      + +
      + + {{.LoggedUser.Backgroundimagealpha}} + +
      +
      +
      + +
      + + +
      + {{t .LangCode "admin.qrPreviewHint"}} +
      +
      +
      +
      +

      {{t .LangCode "admin.preview"}}

      +
      +
      +
      + {{t .LangCode "admin.themePreset"}} +
      + + + + +
      +
      +
      + +
      +

      {{t .LangCode "admin.previewHint"}}

      +
      +
      +
      +
      +
    • +
    +
    + +
    +
    + + +
    +
      +
    • + {{t .LangCode "admin.social"}} +
      + {{t .LangCode "admin.socialHint"}} + + + + + +
      +
    • +
    +
    + +
    +
    + + +
    +

    {{t .LangCode "profile.card.password"}}

    +
    + + +
    +
    + + +
    +

    {{t .LangCode "profile.pwdNote"}}

    +
    +
    + +
    +
    +
    + + +
    +

    {{t .LangCode "admin.tags"}}

    +
    + {{ .CSRFTokenHTML }} +
    + + +
    +
    + +
    +
    + +
    +
    +
    + + +
    +

    {{t .LangCode "admin.pagePassword"}}

    +
    + {{ .CSRFTokenHTML }} +
    + + +
    +
    + +
    +
    +
    + + +
    +

    {{t .LangCode "admin.account"}}

    + +
    +
    + + + +
    + {{ with .FeatureQuestion }}{{template "user/feature-control-form" .}}{{ end }} + +
    +
    +
    + {{ $.CSRFTokenHTML }} + {{t $.LangCode "admin.harassment"}} +
    +
    + +
    +
    + +
    +
    +
    + + + {{ template "user/global-blockwords" (dict "Root" $ "Action" "/user/blockwords/update?tab=question") }} + +
    +
    +
    + {{ $.CSRFTokenHTML }} + {{t $.LangCode "admin.askPassword"}} +
    + + +
    +
    + +
    +
    +
    +
    + + +
    + +
    + {{ with .FeatureChat }}{{template "user/feature-control-form" .}}{{ end }} + +
    +
    + {{ $.CSRFTokenHTML }} + {{t $.LangCode "admin.chat"}} +
    + + {{ with $.ChatLimits }} + + {{ end }} +
    +
    + + {{ with $.ChatLimits }} + + {{ end }} + {{t $.LangCode "admin.dailyLimitHint"}} +
    +
    + +
    +
    +
    + +
    + + {{ template "user/global-blockwords" (dict "Root" $ "Action" "/user/blockwords/update?tab=chat") }} +
    + + +
    + {{ with .FeatureVote }}{{template "user/feature-control-form" .}}{{ end }} + +
    + + {{ template "user/global-blockwords" (dict "Root" $ "Action" "/user/blockwords/update?tab=vote") }} +
    + + + + + + + + + +{{template "base/footer" .}} diff --git a/templates/user/question-list.html b/templates/user/question-list.html new file mode 100644 index 0000000..3c8c53a --- /dev/null +++ b/templates/user/question-list.html @@ -0,0 +1,486 @@ +{{template "base/header" .}} + + + +
    +
    +

    {{t .LangCode "qlist.title"}}

    + {{t .LangCode "admin.featuresEntry"}} +
    + + +
    + + + +
    + + +
    + + {{template "base/alert" .}} + + +
    +
    {{ .Stats.total }}{{t .LangCode "qlist.statsTotal"}}
    +
    {{ .Stats.answered }}{{t .LangCode "qlist.statsAnswered"}}
    +
    {{ .Stats.unanswered }}{{t .LangCode "qlist.statsUnanswered"}}
    +
    {{ .Stats.read }}{{t .LangCode "qlist.statsRead"}}
    +
    {{ .Stats.unread }}{{t .LangCode "qlist.statsUnread"}}
    +
    {{ .Stats.private }}{{t .LangCode "qlist.statsPrivate"}}
    +
    {{ .Stats.pinned }}{{t .LangCode "qlist.statsPinned"}}
    +
    + +
    + +
    +
    +
    + +
    +
    + +
    + {{ if ne .Keyword "" }} + + {{ end }} +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    + + +

    {{t .LangCode "qlist.received"}}

    + {{ if .ReceivedItems }} + {{ range .ReceivedItems }} +
    +
    + +
    + +
    + {{Date .CreatedAt "Y-m-d H:i:s"}} + {{ if .IsPinned }} + {{t $.LangCode "qlist.pinnedShort"}}{{t $.LangCode "qlist.pinned"}} + {{ end }} + {{ if eq .Type "question_private" }} + {{t $.LangCode "qlist.privateShort"}}{{t $.LangCode "qlist.private"}} + {{ end }} + {{ if not .IsAnswered }} + {{t $.LangCode "qlist.unansweredShort"}}{{t $.LangCode "qlist.unanswered"}} + {{ end }} + {{ if .IsRead }} + {{t $.LangCode "qlist.readShort"}}{{t $.LangCode "qlist.read"}} + {{ else }} + {{t $.LangCode "qlist.unreadShort"}}{{t $.LangCode "qlist.unread"}} + {{ end }} +
    + +
    + {{ if not .IsRead }} + +
    + {{ $.CSRFTokenHTML }} + +
    + {{ end }} + +
    + {{ $.CSRFTokenHTML }} + +
    +
    +
    + +
    {{MarkdownPlainText .Content}}
    +
    +
    + {{ end }} + {{ if .HasMoreQuestions }} + + {{ end }} + {{ else }} +

    {{t .LangCode "qlist.empty"}}

    + {{ end }} +
    + + + + + + +
    + + + +{{template "base/footer" .}} diff --git a/templates/user/vote-form.html b/templates/user/vote-form.html new file mode 100644 index 0000000..19aefff --- /dev/null +++ b/templates/user/vote-form.html @@ -0,0 +1,392 @@ +{{template "base/header" .}} + +
    +

    + {{ if .IsEdit }}{{t .LangCode "vote.editTitle"}}{{ else }}{{t .LangCode "vote.newTitle"}}{{ end }} +

    + {{t .LangCode "common.back"}} +
    + +
    + {{ .CSRFTokenHTML }} + + {{template "base/alert" .}} + + +
    +

    {{t .LangCode "vote.formBasic"}}

    + + {{ if .LockedContent }} +
    {{t .LangCode "vote.formLockedHint"}}
    + {{ end }} + +
    + + +
    + +
    + + +
    + +
    +
    + + {{ if .LockedContent }} + + {{ else }} + + {{ end }} +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + + {{t .LangCode "vote.formQuotaHint"}} +
    + {{ if not .LockedContent }} +
    + + +
    + {{ end }} +
    + + + +
    +
    + {{t .LangCode "vote.formMaxVotesPerFingerprint"}}{{t .LangCode "vote.formMaxVotesPerAccount"}} +
    +
    + +
    + + + +
    +
    + + {{ if not .LockedContent }} + +
    +

    {{t .LangCode "vote.formOptions"}}

    + +
    + + +
    + + + + {{ end }} + +
    + + {{ if and .IsEdit (not .LockedContent) }}{{ end }} + + {{ if not .LockedContent }}{{ end }} + {{t .LangCode "common.cancel"}} +
    + +
    + + + +{{if .DraftSaved}} + +{{end}} + +{{template "base/footer" .}} diff --git a/templates/user/vote-results.html b/templates/user/vote-results.html new file mode 100644 index 0000000..7300b1b --- /dev/null +++ b/templates/user/vote-results.html @@ -0,0 +1,182 @@ +{{template "base/header" .}} + +
    +

    {{t .LangCode "vote.resultsTitle"}}:{{ .Vote.Title }}

    + +
    + +
    +
    {{ .Total }}
    +
    {{t .LangCode "vote.resultsTotal"}}
    +
    + +{{ if eq .Vote.Type "form" }} + + {{ if .Results.FormFieldStats }} +

    {{t .LangCode "vote.resultsOptions"}}

    +
    + {{ range $si, $stat := .Results.FormFieldStats }} +
    +
    + {{ add $si 1 }} + {{ fieldTypeLabel $.LangCode $stat.Field.FieldType }} + {{ $stat.Field.Label }} + {{t $.LangCode "vote.resultsAnswered"}}: {{ $stat.Total }} +
    + + {{ if or (eq $stat.Field.FieldType "radio") (eq $stat.Field.FieldType "select") (eq $stat.Field.FieldType "checkbox") }} + {{ $denom := $stat.Total }} + {{ if eq $stat.Field.FieldType "checkbox" }}{{ $denom = $stat.TotalSelections }}{{ end }} + {{ range $o := $stat.OptionStats }} +
    +
    + {{ $o.Option.Content }} + {{ $o.Count }} {{t $.LangCode "vote.resultsVotes"}}({{ if eq $denom 0 }}0{{ else }}{{ percentOf $o.Count $denom }}{{ end }}%) +
    +
    +
    +
    +
    + {{ end }} + {{ if $stat.CustomStats }} +
    +
    {{t $.LangCode "vote.page.customStats"}}:
    + {{ range $c := $stat.CustomStats }} +
    + {{ $c.Text }} + {{ $c.Count }} {{t $.LangCode "vote.resultsVotes"}} +
    + {{ end }} +
    + {{ end }} + + {{ else if eq $stat.Field.FieldType "rating" }} +
    {{t $.LangCode "vote.page.avgRating"}}: {{ printf "%.1f" $stat.AvgRating }}({{ $stat.Total }} {{t $.LangCode "vote.resultsVotes"}})
    + {{ range $r := $stat.RatingCounts }} +
    + {{ $r.Score }} {{t $.LangCode "vote.page.ratingUnit"}} + {{ $r.Count }} {{t $.LangCode "vote.resultsVotes"}}({{ if eq $stat.Total 0 }}0{{ else }}{{ percentOf $r.Count $stat.Total }}{{ end }}%) +
    +
    +
    +
    + {{ end }} + + {{ else if eq $stat.Field.FieldType "rank" }} +
    {{t $.LangCode "vote.page.avgRank"}}: {{ printf "%.2f" $stat.AvgRank }}({{ $stat.Total }} {{t $.LangCode "vote.resultsVotes"}})
    + {{ range $o := $stat.OptionStats }} +
    + {{ $o.Option.Content }} + {{t $.LangCode "vote.page.avgRank"}}: {{ if gt $o.Count 0 }}{{ printf "%.2f" $o.AvgRank }}{{ else }}-{{ end }} · {{t $.LangCode "vote.page.rankedTimes"}}: {{ $o.Count }} +
    + {{ end }} + + {{ else if eq $stat.Field.FieldType "number" }} +
    {{t $.LangCode "vote.page.avgNumber"}}: {{ printf "%.1f" $stat.AvgNumber }}({{ $stat.Total }} {{t $.LangCode "vote.resultsVotes"}})
    + + {{ else }} +
    {{t $.LangCode "vote.page.fieldFilled"}}: {{ $stat.TextCount }}
    + {{ $replies := index $.TextReplies $stat.Field.Label }} + {{ if $replies }} +
    +
    + ▾ + {{t $.LangCode "vote.resultsViewTexts"}}({{ len $replies }}) +
    + +
    + {{ end }} + {{ end }} +
    + {{ end }} +
    + {{ end }} + + +{{ else if .Results }} + +

    {{t .LangCode "vote.resultsOptions"}}

    +
    + {{ if or (eq .Results.Type "single") (eq .Results.Type "multiple") }} + {{ range .Results.Options }} +
    +
    + {{ .Option.Content }} + {{ .Count }} {{t $.LangCode "vote.resultsVotes"}}({{ .Percent }}%) +
    +
    +
    +
    +
    + {{ end }} + + {{ else if eq .Results.Type "rating" }} +
    {{t .LangCode "vote.page.avgRating"}}: {{ printf "%.1f" .Results.AvgRating }}
    + {{ range .Results.Ratings }} +
    +
    + {{ .Score }} {{t $.LangCode "vote.page.ratingUnit"}} + {{ .Count }} {{t $.LangCode "vote.resultsVotes"}} +
    +
    +
    +
    +
    + {{ end }} + + {{ else if eq .Results.Type "rank" }} + + + + {{ range .Results.Rankings }} + + {{ end }} + +
    {{t .LangCode "vote.page.optionColumn"}}{{t .LangCode "vote.page.avgRank"}}
    {{ .Option.Content }}{{ if gt .AvgRank 0.0 }}{{ printf "%.2f" .AvgRank }}{{ else }}-{{ end }}
    + {{ end }} + + {{ if .Results.CustomStats }} +
    +
    {{t .LangCode "vote.page.customStats"}}
    + {{ range .Results.CustomStats }} +
    + {{ .Text }} + {{ .Count }} {{t $.LangCode "vote.resultsVotes"}} +
    + {{ end }} +
    + {{ end }} +
    +{{ else }} +
    {{t .LangCode "vote.resultsEmpty"}}
    +{{ end }} + + + +{{template "base/footer" .}} diff --git a/templates/vote/page.html b/templates/vote/page.html new file mode 100644 index 0000000..33e895f --- /dev/null +++ b/templates/vote/page.html @@ -0,0 +1,526 @@ +{{template "base/header" .}} + + + +{{template "base/alert" .}} + +
    +
    + {{ if .Owner }} + +
    + {{ .Owner.Name }} +
    {{t .LangCode "vote.page.by"}}
    +
    + {{ end }} +
    + {{ if .NotStarted }}{{t .LangCode "vote.page.notStarted"}}{{ end }} + {{ if .Ended }}{{t .LangCode "vote.page.ended"}}{{ end }} + {{ if and .CanVote (not .Ended) }}{{t .LangCode "vote.page.active"}}{{ end }} + {{ if and .HasVoted (not .Ended) }}{{t .LangCode "vote.page.participated"}}{{ end }} +
    +
    + +

    {{ .Vote.Title }}

    + {{ if .Vote.Description }} +

    {{ .Vote.Description }}

    + {{ end }} +
    + +{{ if .PreviewMode }} +
    {{t .LangCode "vote.page.previewHint"}}
    +{{ end }} + +{{ if .FeatureClosedMessage }} +
    {{ .FeatureClosedMessage }}
    +{{ end }} + +{{ if and .NotStarted (not .Error) }} +
    {{t .LangCode "vote.page.notStartedHint"}}
    +{{ else if and .Ended (not .ShowResults) (not .Error) }} +
    {{t .LangCode "vote.page.endedHint"}}
    +{{ end }} + +{{ if .CanVote }} + {{ if .VoteScopeRegisteredOnly }} + {{ if not .IsLogged }} +
    + {{t .LangCode "vote.page.loginRequired"}} {{t .LangCode "common.login"}} +
    + {{ end }} + {{ else }} +
    {{t .LangCode "vote.page.anonymousHint"}}
    + {{ end }} + +
    + {{ .CSRFTokenHTML }} + + +
    + {{ if eq .Vote.Type "single" }} + {{ range .Options }} + + {{ end }} + + {{ else if eq .Vote.Type "multiple" }} +
    {{t .LangCode "vote.page.multipleHint"}}(最多 {{ .Vote.MaxChoices }} 项)
    +
    + {{ range .Options }} + + {{ end }} +
    + + {{ else if eq .Vote.Type "rating" }} + + + + {{ else if eq .Vote.Type "rank" }} +
    {{t .LangCode "vote.page.rankHint"}}
    + {{ range .Options }} +
    + {{ .Content }} + +
    + {{ end }} + + {{ else if eq .Vote.Type "form" }} + {{ range $fi, $f := .FormFields }} +
    +
    + {{ add $fi 1 }} + {{ fieldTypeLabel $.LangCode $f.FieldType }} +
    {{ $f.Label }}{{ if $f.Required }} *{{ end }}
    +
    + {{ if eq $f.FieldType "text" }} + + {{ else if eq $f.FieldType "textarea" }} + + {{ else if eq $f.FieldType "select" }} + + {{ if $f.AllowCustom }} + + {{ end }} + {{ else if eq $f.FieldType "radio" }} + {{ range parseOptions $f.Options }} + + {{ end }} + {{ if $f.AllowCustom }} + + + {{ end }} + {{ else if eq $f.FieldType "checkbox" }} + {{ range parseOptions $f.Options }} + + {{ end }} + {{ if $f.AllowCustom }} + + + {{ end }} + {{ else if eq $f.FieldType "number" }} + + {{ else if eq $f.FieldType "rating" }} + + {{ else if eq $f.FieldType "rank" }} +
    + {{ if $f.Required }}{{t $.LangCode "vote.page.rankHintRequired"}}{{ else }}{{t $.LangCode "vote.page.rankHintOptional"}}{{ end }} +
    + {{ $opts := parseOptions $f.Options }} + {{ range $oi, $opt := $opts }} +
    + {{ $opt }} + +
    + {{ end }} + {{ end }} +
    + {{ end }} + {{ end }} + + {{ if and .Vote.AllowCustom (or (eq .Vote.Type "single") (eq .Vote.Type "multiple") (eq .Vote.Type "rank")) }} +
    + + +
    + {{ end }} +
    + +
    + {{ if .PreviewMode }} + + {{t .LangCode "vote.page.previewSubmitHint"}} + {{ else if eq .CaptchaType "math" }} + + {{ else }} + + {{ end }} +
    +
    + +{{ else if .HasVoted }} + {{ if and (not .Success) (not .Error) }} +
    {{t .LangCode "vote.page.alreadyVoted"}}
    + {{ end }} +{{ end }} + +{{ if .ShowResults }} +
    +

    {{t .LangCode "vote.page.resultsTitle"}}

    + + {{ if .Results }} + {{ if or (eq .Results.Type "single") (eq .Results.Type "multiple") }} +
    {{t .LangCode "vote.page.totalVotes"}}: {{ .Results.Total }}
    + {{ range .Results.Options }} +
    +
    + {{ .Option.Content }} + {{ .Count }} {{t $.LangCode "vote.resultsVotes"}}({{ .Percent }}%) +
    +
    +
    +
    +
    + {{ end }} + + {{ else if eq .Results.Type "rating" }} +
    {{t .LangCode "vote.page.totalVotes"}}: {{ .Results.Total }} · {{t .LangCode "vote.page.avgRating"}}: {{ printf "%.1f" .Results.AvgRating }}
    + {{ range .Results.Ratings }} +
    +
    + {{ .Score }} {{t $.LangCode "vote.page.ratingUnit"}} + {{ .Count }} {{t $.LangCode "vote.resultsVotes"}} +
    +
    +
    +
    +
    + {{ end }} + + {{ else if eq .Results.Type "rank" }} +
    {{t .LangCode "vote.page.totalVotes"}}: {{ .Results.Total }}
    + + + + {{ range .Results.Rankings }} + + {{ end }} + +
    {{t .LangCode "vote.page.optionColumn"}}{{t .LangCode "vote.page.avgRank"}}
    {{ .Option.Content }}{{ if gt .AvgRank 0.0 }}{{ printf "%.2f" .AvgRank }}{{ else }}-{{ end }}
    + + {{ else if eq .Results.Type "form" }} +
    {{t .LangCode "vote.page.formCount"}}: {{ .Results.FormCount }}
    + {{ range $si, $stat := .Results.FormFieldStats }} +
    +
    + {{ add $si 1 }} + {{ fieldTypeLabel $.LangCode $stat.Field.FieldType }} + {{ $stat.Field.Label }} + {{t $.LangCode "vote.resultsAnswered"}}: {{ $stat.Total }} +
    + + {{ if or (eq $stat.Field.FieldType "radio") (eq $stat.Field.FieldType "select") (eq $stat.Field.FieldType "checkbox") }} + {{ $denom := $stat.Total }} + {{ if eq $stat.Field.FieldType "checkbox" }}{{ $denom = $stat.TotalSelections }}{{ end }} + {{ range $o := $stat.OptionStats }} +
    +
    + {{ $o.Option.Content }} + {{ $o.Count }} {{t $.LangCode "vote.resultsVotes"}}({{ if eq $denom 0 }}0{{ else }}{{ percentOf $o.Count $denom }}{{ end }}%) +
    +
    +
    +
    +
    + {{ end }} + {{ if $stat.CustomStats }} +
    +
    {{t $.LangCode "vote.page.customStats"}}:
    + {{ range $c := $stat.CustomStats }} +
    + {{ $c.Text }} + {{ $c.Count }} {{t $.LangCode "vote.resultsVotes"}} +
    + {{ end }} +
    + {{ end }} + + {{ else if eq $stat.Field.FieldType "rating" }} +
    {{t $.LangCode "vote.page.avgRating"}}: {{ printf "%.1f" $stat.AvgRating }}({{ $stat.Total }} {{t $.LangCode "vote.resultsVotes"}})
    + {{ range $r := $stat.RatingCounts }} +
    + {{ $r.Score }} {{t $.LangCode "vote.page.ratingUnit"}} + {{ $r.Count }} {{t $.LangCode "vote.resultsVotes"}}({{ if eq $stat.Total 0 }}0{{ else }}{{ percentOf $r.Count $stat.Total }}{{ end }}%) +
    +
    +
    +
    + {{ end }} + + {{ else if eq $stat.Field.FieldType "rank" }} +
    {{t $.LangCode "vote.page.avgRank"}}: {{ printf "%.2f" $stat.AvgRank }}({{ $stat.Total }} {{t $.LangCode "vote.resultsVotes"}})
    + {{ range $o := $stat.OptionStats }} +
    + {{ $o.Option.Content }} + {{t $.LangCode "vote.page.avgRank"}}: {{ if gt $o.Count 0 }}{{ printf "%.2f" $o.AvgRank }}{{ else }}-{{ end }} · {{t $.LangCode "vote.page.rankedTimes"}}: {{ $o.Count }} +
    + {{ end }} + + {{ else if eq $stat.Field.FieldType "number" }} +
    {{t $.LangCode "vote.page.avgNumber"}}: {{ printf "%.1f" $stat.AvgNumber }}({{ $stat.Total }} {{t $.LangCode "vote.resultsVotes"}})
    + + {{ else }} +
    {{t $.LangCode "vote.page.fieldFilled"}}: {{ $stat.TextCount }}
    + {{ end }} +
    + {{ end }} + {{ end }} + + {{ if .Results.CustomStats }} +
    +
    {{t .LangCode "vote.page.customStats"}}
    + {{ range .Results.CustomStats }} +
    + {{ .Text }} + {{ .Count }} {{t $.LangCode "vote.resultsVotes"}} +
    + {{ end }} +
    + {{ end }} + {{ else }} +
    {{t .LangCode "vote.resultsEmpty"}}
    + {{ end }} +
    +{{ end }} + +{{ if and .IsOwner (not .ShowResults) }} + +{{ end }} + + + +{{template "base/footer" .}} diff --git a/templates/vote_templates_test.go b/templates/vote_templates_test.go new file mode 100644 index 0000000..9469b6a --- /dev/null +++ b/templates/vote_templates_test.go @@ -0,0 +1,465 @@ +package templates + +import ( + gotemplate "html/template" + "regexp" + "strconv" + "strings" + "testing" + "time" + + "github.com/flamego/template" + + "github.com/tamakyi/TamaBox/internal/conf" + "github.com/tamakyi/TamaBox/internal/db" + "github.com/tamakyi/TamaBox/internal/dbutil" +) + +// voteBaseData 构造投票模板渲染的基础数据(仿 render_test.go 的 base)。 +func voteBaseData() map[string]interface{} { + return map[string]interface{}{ + "Title": "投票", + "LangCode": "zh-CN", + "SupportedLangs": []map[string]string{{"Code": "zh-CN", "Name": "简体中文"}}, + "IsLogged": true, + "LoggedUser": map[string]interface{}{"Domain": "test", "Name": "tester"}, + "LoggedUserName": "tester", + "CurrentURI": "/vote/abc123", + "IsUserPage": false, + "IsPixel": false, + "VConsole": false, + "CSRFTokenHTML": gotemplate.HTML(``), + "RecaptchaSiteKey": "", + "CaptchaType": "recaptcha", + "PageUser": map[string]interface{}{"Domain": "test", "Name": "tester", "Avatar": ""}, + "ICP": "", + // 参与端自定义补充选项的字数上限(生产由 route/vote 注入 conf.Limits.CustomAnswerLen) + "MaxCustomTextLen": conf.Limits.CustomAnswerLen, + "CustomFieldMarker": db.CustomFieldMarker, + // 站长侧单个选项文本上限(生产由 route/user 注入 conf.Limits.VoteOptionLen) + "MaxVoteOptionLen": conf.Limits.VoteOptionLen, + // [limits] 全站上限(生产由 Contexter 全局注入 conf.Limits), + // 供 question/list 嵌套的提问框 maxlength 同源 + "Limits": conf.Limits, + } +} + +func TestVoteFormUsesFixedDeviceLimitNotice(t *testing.T) { + fs, err := template.EmbedFS(FS, ".", []string{".html"}) + if err != nil { + t.Fatalf("EmbedFS: %v", err) + } + for _, f := range fs.Files() { + if f.Name() != "user/vote-form" && f.Name() != "user/vote-form.html" { + continue + } + data, err := f.Data() + if err != nil { + t.Fatalf("get template data: %v", err) + } + source := string(data) + if strings.Contains(source, `name="max_votes_per_fingerprint"`) { + t.Fatal("fixed fingerprint limit must not be rendered as a form input") + } + if !strings.Contains(source, `vote.formMaxVotesPerFingerprint`) { + t.Fatal("fixed fingerprint limit notice is missing") + } + return + } + t.Fatal("user/vote-form.html not found") +} + +func TestRenderVotePage(t *testing.T) { + opt1 := &db.VoteOption{Content: "选项 A"} + opt2 := &db.VoteOption{Content: "选项 B"} + + t.Run("single can vote", func(t *testing.T) { + base := voteBaseData() + base["Vote"] = &db.Vote{Title: "测试单选", Description: "描述", Type: db.VoteTypeSingle, LinkToken: "abc123", MaxChoices: 1} + base["Owner"] = map[string]interface{}{"Name": "tester", "Domain": "test", "Avatar": ""} + base["Options"] = []*db.VoteOption{opt1, opt2} + base["HasVoted"] = false + base["CanVote"] = true + base["NotStarted"] = false + base["Ended"] = false + base["VoteScopeRegisteredOnly"] = false + base["ShowResults"] = false + base["IsOwner"] = false + if err := renderWithData(t, "vote/page", base); err != nil { + t.Fatalf("render vote/page (single) failed: %v", err) + } + }) + + t.Run("multiple can vote", func(t *testing.T) { + base := voteBaseData() + base["Vote"] = &db.Vote{Title: "多选", Type: db.VoteTypeMultiple, LinkToken: "abc123", MaxChoices: 2} + base["Owner"] = map[string]interface{}{"Name": "tester", "Domain": "test", "Avatar": ""} + base["Options"] = []*db.VoteOption{opt1, opt2, {Content: "选项 C"}} + base["HasVoted"] = false + base["CanVote"] = true + base["NotStarted"] = false + base["Ended"] = false + base["VoteScopeRegisteredOnly"] = false + base["ShowResults"] = false + base["IsOwner"] = false + if err := renderWithData(t, "vote/page", base); err != nil { + t.Fatalf("render vote/page (multiple) failed: %v", err) + } + }) + + t.Run("rating can vote", func(t *testing.T) { + base := voteBaseData() + base["Vote"] = &db.Vote{Title: "评分", Type: db.VoteTypeRating, LinkToken: "abc123"} + base["Owner"] = map[string]interface{}{"Name": "tester", "Domain": "test", "Avatar": ""} + base["HasVoted"] = false + base["CanVote"] = true + base["NotStarted"] = false + base["Ended"] = false + base["VoteScopeRegisteredOnly"] = false + base["ShowResults"] = false + base["IsOwner"] = false + if err := renderWithData(t, "vote/page", base); err != nil { + t.Fatalf("render vote/page (rating) failed: %v", err) + } + }) + + t.Run("rank can vote", func(t *testing.T) { + base := voteBaseData() + base["Vote"] = &db.Vote{Title: "排序", Type: db.VoteTypeRank, LinkToken: "abc123"} + base["Owner"] = map[string]interface{}{"Name": "tester", "Domain": "test", "Avatar": ""} + base["Options"] = []*db.VoteOption{opt1, opt2} + base["HasVoted"] = false + base["CanVote"] = true + base["NotStarted"] = false + base["Ended"] = false + base["VoteScopeRegisteredOnly"] = false + base["ShowResults"] = false + base["IsOwner"] = false + if err := renderWithData(t, "vote/page", base); err != nil { + t.Fatalf("render vote/page (rank) failed: %v", err) + } + }) + + t.Run("preview draft form", func(t *testing.T) { + base := voteBaseData() + base["Vote"] = &db.Vote{Title: "草稿表单", Type: db.VoteTypeForm, LinkToken: "abc123"} + base["Owner"] = map[string]interface{}{"Name": "tester", "Domain": "test", "Avatar": ""} + base["FormFields"] = []*db.VoteFormField{ + {Model: dbutil.Model{ID: 1}, FieldType: db.VoteFormFieldTypeText, Label: "昵称", Required: true, MaxLength: 20}, + {Model: dbutil.Model{ID: 2}, FieldType: db.VoteFormFieldTypeRadio, Label: "满意度", Options: `["满意","一般"]`}, + } + base["HasVoted"] = false + base["CanVote"] = true + base["NotStarted"] = false + base["Ended"] = false + base["VoteScopeRegisteredOnly"] = false + base["ShowResults"] = false + base["IsOwner"] = true + base["IsLogged"] = true + base["PreviewMode"] = true + if err := renderWithData(t, "vote/page", base); err != nil { + t.Fatalf("render vote/page (preview) failed: %v", err) + } + }) + + t.Run("custom text limit injected", func(t *testing.T) { + base := voteBaseData() + base["Vote"] = &db.Vote{Title: "自定义选项", Type: db.VoteTypeSingle, LinkToken: "abc123", AllowCustom: true} + base["Owner"] = map[string]interface{}{"Name": "tester", "Domain": "test", "Avatar": ""} + base["FormFields"] = []*db.VoteFormField{ + {Model: dbutil.Model{ID: 1}, FieldType: db.VoteFormFieldTypeRadio, Label: "满意度", Options: `["满意","一般"]`, AllowCustom: true}, + } + base["HasVoted"] = false + base["CanVote"] = true + base["NotStarted"] = false + base["Ended"] = false + base["VoteScopeRegisteredOnly"] = false + base["ShowResults"] = false + base["IsOwner"] = false + out, err := renderToString(t, "vote/page", base) + if err != nil { + t.Fatalf("render vote/page (custom) failed: %v", err) + } + // 缺注入时模板不报错,只会渲染出 让整段脚本失效,必须显式断言 + if strings.Contains(out, "") { + t.Fatal("rendered page contains : MaxCustomTextLen was not injected") + } + // 注意:html/template 在