Files
tamabox/conf/app.sample.ini
T
tamakyi d690355cdc feat(honeypot): 蜜罐安全状况周期汇总(周报/月报 + 地图 + 邮件)
以周/月为单位汇总蜜罐命中,把来源 IP 通过公共服务查地理位置,画到地图上,
生成 HTML 报告并发邮件。同时补齐 ipinfo 无法回答「这个 IP 在哪儿」的问题。

新增 IP 地理定位层 internal/security/geoip:
- 与 internal/security/ipinfo 正交。ipinfo 走 cdncheck 离线指纹,只认云厂商/CDN/WAF,
  答的是「是不是代理」;肉鸡(家宽、企业宽带、移动网络)必然被判为
  「未识别(普通网络)」,所以「在哪儿」必须另建一套。
- ip-api.com 批量打底(100 个/请求、1.4s 节流)+ 腾讯位置服务精修国内
  (一 IP 一请求,只精修命中数最高的前 N 个,受 ip_geo_tencent_key 控制)。
- Result{Geo, Unresolved, Requests, Failures} 三态。服务明确回答「没有这个地址」
  才允许写失败缓存;请求超时/限流/5xx 一律不写 —— 否则一次抖动会把整批 IP
  钉成「查不到」三天,此后每份报告的定位覆盖率都莫名变成 0。
- addrs 按调用方给的顺序构建(不是 map 遍历):腾讯精修只取前 N 个,乱序会让
  每次运行精修到不同 IP,配额花在长尾上。

报告生成 internal/security/report:
- period:PreviousWeek / PreviousMonth,窗口一律 [start, end) 左闭右开,
  相邻两期不重叠也不漏边界整点;period_key(week:2026-W44)做幂等。
- aggregate:按 shared.LimitKey 合并(IPv4 精确 / IPv6 折 /64);国家、城市、
  路径、每日趋势;显式统计未定位命中数(覆盖率 60% 的图看起来和 100% 一样)。
- mapdraw:自绘等距圆柱投影气泡图。零海岸线零国界 —— 国界/台湾/南海诸岛绘制
  必须符合国家标准,自绘极易画错,真实底图交给腾讯。经纬网 + 度数刻度 +
  标签防重叠(同一都市圈两城市坐标差 0.01°,曾把「AU 55」和「AU 30」叠成
  看起来像 AU 305 的假数字)。
- html:自包含报告页,内嵌 SVG + 腾讯交互地图(无 key 时给申请指引,
  代码里不预置任何 key)。
- countryname:港澳台统一显示「中国香港/中国澳门/中国台湾」,常见国家用中文名。

存储与调度:
- ip_geo_cache(主键 = LimitKey,成功 TTL 90 天 / 明确无数据 TTL 3 天)
- security_report_logs(period_key 唯一索引 → 重启、错过时间窗都能正确补发,
  失败重试上限 5 次后停止)
- honeypot report --period week|month [--to] [--dir] [--no-email]
- web 启动时按配置挂 30 分钟一轮的调度器(与保留期清理共用停止信号,
  defer 移出 if 外,避免「清理关掉、报告开着」时 goroutine 泄漏)

配置([security]):ip_geo_provider=auto|ipapi|tencent|none、ip_geo_tencent_key、
honeypot_report_enabled / _weekly / _monthly / _email / _dir。

顺带修复:
- route/honeypot_test.go 的 captureStore 缺 AggregateSources/AggregatePaths/
  TimestampsBetween,整个 route 包编译不过。
- templates/render_test.go 的 mockLoggedUser 缺 TelegramChatID,3 个渲染测试失败。
2026-10-30 16:40:44 +08:00

414 lines
28 KiB
INI
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
; ── 关于配置键名 ──────────────────────────────────────────────────────────
; ⚠️ 键名写错时,程序启动会 WARN 提示「存在无法识别的键」并给出最接近的合法键名。
; 对于少量已知的常见误拼(如 honeypot_retention_days → honeypot_retain_days),
; 程序会自动纠正并在日志里说明纠正了什么。
; 但请优先写对键名 —— 自动纠正只覆盖已知的误拼,不认识的键一律不猜。
[app]
production = true
; 【必填】站点对外访问地址。用于生成 og:url 与邮件中的全部链接
external_url = "https://box.shiroko.one"
; (找回密码、私信通知等),必须以 http(s):// 开头、结尾不带斜杠
; (程序会自动 TrimRight "/")。域名迁移时只改这一处即可全局生效。
icp = ""
; 站点默认语言:zh-CN / zh-TW / en / ja,留空默认简体中文(访客未设置语言时的兜底)
default_lang = ""
uptrace_dsn = ""
qiniu_access_key = ""
qiniu_access_secret = ""
aliyun_access_key = ""
aliyun_access_key_secret = ""
; 默认分享图背景图链接(1280x720)
share_background = "https://your-default-background.jpg"
; 分享卡绘图用的中文字体(相对项目根目录)。字体文件不在仓库里,必须自备一份,
; 否则分享卡接口会返回 500「字体文件未配置或无法加载」。可用 Noto Sans SC / 思源黑体,
; 支持 .otf/.ttf/.ttc;放好后无需重启生效。
font_path = "static/fonts/NotoSansSC-Regular.otf"
[security]
enable_text_censor = true
; 文本审核服务通道:qiniu(七牛)/ aliyun(阿里云)/ auto(七牛优先、阿里云兜底)。
text_censor_source = auto
; 留空等同于 auto,即引入该配置前的历史行为。
; 显式选 qiniu / aliyun 时必须在 [app] 配好对应凭据,否则启动即报错
; —— 避免运行期审核必然失败(审核失败是 fail-closed,拒绝提交,
; 会让发布/私信整站不可用,且现象隐蔽、只在日志可见)。
; 凭据位置:[app] 的 qiniu_access_key / qiniu_access_secret、
; aliyun_access_key / aliyun_access_key_secret。
; ⚠️ 配好凭据 ≠ 可用:阿里云通道走的是「内容安全 2.0 / 文本审核增强版」
; (接口 TextModeration,接入点 green-cip.cn-shanghai.aliyuncs.com),
; 开通前提只要求【实名认证,个人账号即可】,不需要企业认证。
; 开通入口:内容安全 2.0 控制台 → 开通「内容审核增强版」按量付费
; (开通本身不收费,按实际调用量出账;文本审核标准 7.5 元/万次)。
; 用 RAM 子账号调用需授予 AliyunYundunGreenWebFullAccess;欠费停服同理。
; 审核场景固定为 comment_detection(公聊评论检测,覆盖涉政/涉暴/涉黄等底线维度)。
; 单次 content 上限 600 字符,超长文本由代码分段审核(段间带重叠,避免跨段漏判)。
; ⚠️ 不要再去开通「内容安全 1.0」:它要求企业实名认证,个人账号无法开通,
; 调用会永久返回 code=596 "You have not opened Yundun Content Moderation Service"。
; 审核失败时日志会打印阿里云返回的 code / msg / requestId 便于定位;
; 首次出现账号级失败(未开通 / 未授权 / 密钥错误)还会额外打印一条一次性告警。
; 逗号分隔的IP黑名单,支持精确IP(如1.2.3.4)和CIDR(如1.2.3.0/24),留空不启用
ip_blacklist =
; 黑名单IP访问时强制跳转到该URL
ip_blacklist_redirect_url =
; 是否信任反向代理设置的 CF-Connecting-IP / X-Real-IP / X-Forwarded-For 头。
trust_proxy = false
; 仅当站点部署在可信反向代理(如 nginx/Cloudflare)之后且代理已覆盖这些头时设为 true;
; 否则保持 false,防止客户端伪造 header 绕过速率限制与 IP 黑名单。
; ⚠️ 若站点在 Cloudflare 后且需使用下面的自动封禁,必须设为 true,
; 否则解析到的将是 Cloudflare 边缘 IP,自动封禁会误伤所有访客。
; ⚠️ 蜜罐取证同理:trust_proxy = false 时所有请求的来源都被记成反代地址
; (常见 127.0.0.1),取证数据失去意义;而且一次蜜罐封禁会连带封掉整站。
; 蜜罐命中回环地址时启动日志会打一次告警提醒检查这里。
; ⚠️ 开启 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 时同时支持这两个权威头。
; ⚠️ 权威头只在「TCP 对端可信」时才被采信(回环地址,或命中 trusted_proxies)。
; 这条是代码里的硬约束,不再依赖运维纪律 —— EdgeOne 不剥离客户端自带的 CF-Connecting-IP,
; 旧实现无条件优先读它,导致经 EdgeOne 进来的请求客户端 IP 完全可控:
; 限流 / IP 黑名单失效,且 autoban 按假 IP 记账可被用来栽赃任意正常用户。
; 双 CDN 并存时:① 两家回源网段都登记到 trusted_proxies;② 防火墙仅放行两家回源网段;
; ③ 仍建议在各家规则引擎里删掉「对方 CDN」的权威头(纵深防御)。
; 仅用 cloudflared Tunnel 回本机时对端是 127.0.0.1,无需任何额外配置。
; 重新启用 EdgeOne 前请先按本节核对,步骤见 outputs/edgeone-cloudflare-dual-cdn-guide.md。
trusted_proxies =
; 逗号分隔的「你自己的」反向代理/负载均衡 IP 或 CIDR(如 10.0.0.0/8),
; 或「CDN 直连回源」形态下该 CDN 的回源网段。
; 回环地址(127.0.0.1 / ::1)无需登记即视为可信对端:cloudflared / 本机 nginx
; 回源的连接必然由本机进程发起,外部无法伪造。
; 除回环以外,CDN 权威头也必须对端命中本清单才会被采信 —— 公网对端带来的
; CF-Connecting-IP / EO-Client-IP 一律忽略,改用 TCP 对端地址。
; 仅当 TCP 对端命中本清单时才解析 X-Forwarded-For / X-Real-IP。
; 注意:此清单用于你自己的基础设施(含你选定 CDN 的回源网段),
; 不是 CDN 的全量 CIDR 清单。
; 内容安全策略(CSP)模式:off / report / enforce,留空等同 report。
csp = enforce
; enforce(默认):真正拦截。脚本白名单由代码从 [upload] 的图片 CDN 域名与
; [recaptcha] 的域名自动派生,另内置模板实际引用的 cdnjs.snrat.com
; (UIkit/Alpine/dayjs/vConsole)、challenges.cloudflare.com 与 umami 统计域;
; 配了 sentry_dsn 时自动放行 browser.sentry-cdn.com。
; ⚠️ 日后新增第三方脚本 / 字体 / 接口域名,必须同步加进 internal/route/csp.go,
; 否则控制台会刷 "Refused to load ..." 且对应功能静默失效。
; report:只下发 Content-Security-Policy-Report-Only,页面照常加载,违规只进浏览器
; 控制台(配了 csp_report_uri 才上报)。不给上报地址时它等于没装,
; 所以只在「怀疑误伤、想先看证据」时临时用。
; off:完全不下发(排障逃生口)。
; 默认值的前提是白名单已用真浏览器逐页巡检过:
; outputs/sec-audit-2026-10-25/csp_sweep.py(20 个页面零违规)。
; 上线后发现误伤:把这里改成 report 或 off 再重启,不用改代码。
; 图片 / 样式 / 字体 / 连接放宽到 https:(箱主上传图与主题背景可能落在任意 CDN),
; 真正收紧的是 script-src、object-src、base-uri、form-action、frame-ancestors。
; report:只下发 Content-Security-Policy-Report-Only,页面照常加载,违规只进浏览器
; 控制台(配了 csp_report_uri 才上报)—— 用于上线前观察一周再切 enforce。
; enforce:真正拦截。脚本白名单由代码从 [upload] 的图片 CDN 域名与 [recaptcha] 的域名
; 自动派生,另内置模板实际引用的 cdnjs.snrat.com(UIkit/Alpine/dayjs/vConsole)
; 与 challenges.cloudflare.com。
; ⚠️ 日后新增第三方脚本 / 字体 / 接口域名,必须同步加进 internal/route/csp.go,
; 否则控制台会刷 "Refused to load ..." 且对应功能静默失效。
; off:完全不下发(排障逃生口)。
; 图片 / 样式 / 字体 / 连接放宽到 https:(箱主上传图与主题背景可能落在任意 CDN),
; 真正收紧的是 script-src、object-src、base-uri、form-action、frame-ancestors。
; CSP 违规上报地址(可选)。留空时违规只进浏览器控制台,不上报。
csp_report_uri =
; ⚠️ 限流与自动封禁的计数器都在进程内存里(internal/route/ratelimit.go、
; internal/security/autoban)。单副本部署(当前形态:cloudflared Tunnel 回一台机器)
; 下工作正常;一旦改成多副本 / 多机,每个副本各有一份计数(实际额度 × 副本数),
; 进程重启即清零。多副本前必须先把计数迁到 Redis。
; 封禁「事实」已落库(ban_records 表:键 / 原因 / 时长 / 到期时间),
; 重启丢的只是生效状态,账本还在。
enable_auto_ban = true
; 是否把「未注册路径的 404」计入封禁统计(默认 false)。正常业务
ban_track_notfound = false
; (域名/题目不存在等均走 302 业务跳转,不产生 404 计数)一律放行,
; 仅命中后台扫描特征路径(/admin、/.env 等)才参与统计封禁,避免
; 共享出口(CDN 边缘 / CGNAT)下正常用户被 404 探测连坐误伤。
; 如需恢复对纯 404 字典扫描的封禁再置 true。
; 统计窗口内命中敏感路径(/admin、/.env、/wp-admin 等)的次数阈值
ban_threshold_sensitive = 10
; 统计窗口内返回 404 的次数阈值(仅 ban_track_notfound = true 时生效)
ban_threshold_404 = 20
; 统计窗口时长(秒)
ban_window_seconds = 60
; 临时封禁时长(分钟),到期自动解除,重启即清零
ban_duration_minutes = 15
; 触发封禁时发送告警邮件的收件人(复用下方 SMTP 发信配置),留空不发送
security_alert_email =
; ── 蜜罐(虚假后台登录界面)────────────────────────────────────────────
; ⚠️ 本段一律「注释独占一行、值单独一行」。
; 配置加载器开启了 IgnoreInlineComment,行尾 `; 注释` 会**并入值**:
; honeypot_max_rows = 200000 ; 说明 → 值变成 "200000 ; 说明" → int 解析失败
; honeypot_paths = ; 说明 → 值变成 " ; 说明" → 被切成一堆假路径
; 后果是该项静默回退零值,而日志里只会说「配置值不对」,很难联想到是注释造成的。
; 本文件其余段落已全部改成这个写法,新增配置项时请保持一致。
; 开启后,扫描器探测后台路径(/wp-admin、/.ssh、/.env 等)时不再返回 302,
; 而是返回一个仿 WordPress 的假登录页,并把其提交的用户名/密码写入取证文件。
; 只在「没有任何路由匹配」的路径上生效,真实业务路由零交集。
; 假页面只含 canary 标记与假表单,绝不含任何真实信息。
enable_honeypot = false
; 追加自定义诱饵路径(逗号分隔),与内置清单取并集。留空 = 只用内置清单。
; 用 `TamaBox honeypot paths` 可查看最终生效的完整清单。
honeypot_paths =
; 蜜罐命中记录的保留天数(取证用途),到期自动清理。
; ⚠️ 键名是 honeypot_retain_days(没有 tion)。写错会被静默忽略,
; 程序会尝试自动纠正并在日志里提示,但别指望它每次都猜对。
; 缺省 30 天;写 0 或负数会回退 30(不表示「永不清理」)。
; 要关闭自动清理请用 honeypot_auto_clean = false。
honeypot_retain_days = 30
; 是否启用蜜罐记录的后台自动保留期清理(每小时一轮)。
; honeypot_hits 是只追加的审计表,写入完全由外部扫描流量驱动,
; 不清理的话表体积只增不减。关闭后仍可手动 `TamaBox honeypot prune`。
honeypot_auto_clean = true
; 蜜罐命中记录的行数上限(第二道闸)。显式写 0 = 不限(会告警提醒表只增不减)。
; 保留期是时间维度上界,但攻击者可用海量源 IP 在保留期内把表撑爆。
; 实测单行约 281 字节(含索引,常规 UA),20 万行约 53 MB。
honeypot_max_rows = 200000
; 「提交凭据」多少次即封禁。默认 1(一次即封):往假后台里填账号密码是证据最强
; 的行为,抓到一次就足够。⚠️ 不作用于纯 GET 探测 —— 那类请求走 ban_threshold_sensitive,
; 否则扫描器看一眼假登录页就被封,永远走不到提交凭据那一步。
honeypot_threshold = 1
; 蜜罐封禁时长(分钟)。缺省 1440(24 小时),刻意远长于 ban_duration_minutes:
; 命中蜜罐意味着对方已在提交账号密码,证据强度远高于单纯路径探测。
; 显式写 0 = 回退 ban_duration_minutes。
; ⚠️ 蜜罐封禁与 enable_auto_ban 相互独立:即使关掉自动封禁,
; 只要 enable_honeypot = true,蜜罐命中仍会封禁并记录。
honeypot_ban_minutes = 1440
; 凭据取证文件的存放目录。
; ⚠️ 捕获到的用户名/密码**不入库**,只追加到这个目录下的按天文件
; (creds-YYYY-MM-DD.jsonl,一行一条 JSON)。原因:明文账号密码一旦入库,
; 就会跟着进备份、主从复制和任何能连库的报表工具,事后无法彻底清除。
; 目录会被强制为 0700、文件 0600;相对路径以进程工作目录为基准。
; 目录不可写时只降级为「不保存凭据」,不影响站点启动。
; 建议单独挂一个卷,并让它不参与备份。
honeypot_cred_dir = data/honeypot-creds
; ── 安全状况汇总报告(周报 / 月报)──────────────────────────────
;
; 把周期内的蜜罐命中按来源 IP 聚合,再通过公共 IP 库定位到城市,
; 生成「世界地图 + 国家/城市排行 + 来源明细」的 HTML 报告并邮件送达。
;
; ⚠️ 与 cdncheck 离线指纹的区别:那个只认云厂商 / CDN / WAF 的 ASN,
; 肉鸡(家宽、企业宽带、移动网络)一律显示「未识别(普通网络)」。
; 下面的 IP 库回答的是「这个 IP 在哪儿」,是另一个维度。
;
; 数据源(ip_geo_provider):
; auto(缺省) = ip-api.com 批量打底(全球、无需 key)+ 腾讯精修国内
; ipapi = 只用 ip-api.com
; tencent = 只用腾讯位置服务(必须配 key,否则降级 ipapi)
; none = 不查询任何外部服务,攻击者 IP 一个字节都不外发
; 隐私边界:auto / ipapi 会把攻击者 IP 发给 ip-api.com,tencent 发给腾讯。
ip_geo_provider = auto
; 腾讯位置服务 key(lbs.qq.com 申请,个人免费额度足够)。
; 两个用途:① 精修国内 IP 的省市与坐标;② 报告里可交互地图的底图。
; 留空 → 跳过国内精修,报告里只保留离线地图(经纬网 + 气泡)。
ip_geo_tencent_key =
; 周期性安全汇总邮件。缺省全开。
; 发送时机:周报在每周一、月报在每月 1 日之后的第一轮检查发出,覆盖
; 「上一个完整周期」;服务停机错过时间窗会在重新上线后补发;
; 幂等由 security_report_logs 的 period_key 唯一索引保证,不会重复发。
; 失败最多重试 5 次后停下(SMTP 配错时无限重试只会刷满日志),
; 之后可用 TamaBox honeypot report 手动补发。
honeypot_report_enabled = true
honeypot_report_weekly = true
honeypot_report_monthly = true
; 报告收件人。留空回退 security_alert_email。
; 单独一个键是为了能把「随时发生的封禁告警」与「定期汇总」分投到不同邮箱
; (例如告警进手机推送邮箱、汇总进归档邮箱)。
honeypot_report_email =
; 报告文件(HTML + PNG)的存放目录,按 0700 创建、文件 0600。
; 里面有攻击者 IP 明细,不要放进 Web 可访问的目录。
; 想先看内容不发送:TamaBox honeypot report --period week --no-email
honeypot_report_dir = data/reports
; 被封禁(自动封禁 / 蜜罐封禁)时返回的图片路径,相对路径以进程工作目录为基准。
; 留空、文件读不到、或内容不是图片 → 一律回退为纯文字 "403 Forbidden"。
; 只接受 image/* 的内容(png / jpeg / gif / webp):指向 app.ini 之类会被拒绝,
; 否则等于把配置文件当响应体吐给攻击者。
; 建议用 256×256 左右的图并压到 20 KB 以内 —— 被封的来源会反复重试,
; 每张图都会照常占用出网带宽。
ban_image_path =
; 站点默认「内容审核严格度」:lax(宽松)/ standard(标准)/ strict(严格)。
default_moderation_strictness =
; 仅对未单独设置档位的账号生效(账号在设置页选择后以账号设置为准)。
; 留空回退 strict(最严档):保证升级不会放宽任何拦截。
; 严格档是三个档位中最严的(拦截面不小于升级前),因此部署后默认拦截面可能略有扩大;
; 若希望默认更少误伤,改为 standard 即可,无需改代码。
; 三档的拦截范围见 internal/security/censor/policy.go,
; 适用于提问、投票、私聊三类内容。
[server]
port = 80
; 绑定监听地址(含端口)。Cloudflare Tunnel 部署时强烈建议设为 127.0.0.1:<port>,
; 使源站端口仅对本地 cloudflared 可见,即使防火墙误配也无法被外部直连伪造 CF-Connecting-IP。
; 留空(默认)则监听所有网卡 ":port"。
listen =
salt = ""
# ⚠️ 必填:密码哈希盐,留空会导致启动失败(已在 conf.Init 强制校验)
xsrf_key = ""
[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]
; recaptcha(默认), turnstile, math(本地算数验证码,国内访问友好)
captcha_type = recaptcha
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 = ""
[notify]
; ── 站级广播式通知通道(可选,均留空则只走 [mail] 点对点邮件)──────────────
; webhook_url:通用 Webhook(Discord / Slack / 企业微信群机器人等),站级事件
; (新提问 / 新回答 / 新追问 / 新私聊 / 新投票回复)统一 POST JSON 推送。
; 留空 = 不推送站级 Webhook。
; 注意:不同平台的 webhook 格式不同,这里发送统一 JSON(含 content/event/title/
; body/link/time 字段),Discord 可直接用 content 字段展示;其他平台建议在
; webhook 前端加一层转发做格式适配。
; webhook_url = ""
; telegram_bot_token:Telegram Bot token,配合每个箱主自填的 Chat ID 实现
; 「新提问」点对点通知。留空 = 禁用 Telegram 通知。
; telegram_bot_token = ""
[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
[announcement]
; ── 站内公告:全站每页导航栏下方展示,改完重启生效 ──────────────────────
; content :默认文案,留空 = 关闭公告(任一语言文案非空也视为开启)。
; 用 \n 表示换行;http(s) 地址会自动变成链接;
; 内容按纯文本渲染,不支持 HTML 标签(写 <b> 会原样显示)。
; content_zh_cn / content_zh_tw / content_en / content_ja:
; 分语言文案,留空则回退到 content;content 也留空时该语言不显示公告。
; timezone:start / end 里不带偏移的「裸时间」按哪个时区解释,默认 Asia/Shanghai。
; 可填任意 IANA 时区名(UTC / America/New_York …);二进制已内嵌 tzdata,
; 容器不需要另装时区包。带偏移的 RFC3339 以自带偏移为准。
; start / end:展示时间窗,左闭右开 —— 到 end 那一刻下线。留空表示该侧不限:
; 只写 start = 定时上线;只写 end = 现在起展示到某时刻;都留空 = 一直展示。
; 支持 2026-10-25 02:00 / 2026-10-25 02:00:59 / 2026-10-25 / RFC3339
; (2026-10-25T02:00:00+08:00)。
; 时间或时区写错不会导致启动失败:日志 WARN 后关闭公告。
; 例:content = "10 月 25 日 02:00–04:00 机房维护,期间可能短暂无法访问。详情见 https://status.shiroko.one"
; content_en = "Maintenance 02:00-04:00 on Oct 25 (UTC+8). Details: https://status.shiroko.one"
; start = "2026-10-24 00:00"
; end = "2026-10-25 04:00"
content = ""
content_zh_cn = ""
content_zh_tw = ""
content_en = ""
content_ja = ""
start = ""
end = ""
timezone = Asia/Shanghai
[theme]
; 主题系统:只改配色 / 图案 / 整页背景,不涉及布局。
; 作用范围:提问箱页面(主页 / 单题 / 密码 / 分享卡)、箱主本人的投票页与 /user 后台;
; 首页、探索等站方页面保持站点默认外观。
; default :站点默认主题(箱主未选择时使用)。填 default 或留空 = 现有外观。
; available :启用哪些主题,逗号分隔,同时是箱主设置页的白名单;
; 列了但 static/css/themes/<slug>.css 不存在的会被忽略并告警。
; 新增主题:把 static/css/themes/_scaffold.css 复制成 <slug>.css,
; 改选择器与令牌值,再把 <slug> 加进 available —— 不需要改模板。
; 需要图片素材的,放 static/images/themes/<slug>/(static/fs.go 已嵌 images/themes 目录)。
default = default
available = default,sakura,shiroko,new-sakura
; ⚠️ 没登记在 available 里的主题,即使 css 已随二进制发布也不会出现在设置页;
; 启动日志会打出「[theme] available 未登记以下主题」提醒。