diff --git a/.gitignore b/.gitignore index 4617537..4f716b8 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,7 @@ go.work.sum # otherfile ./workbuddy ./scripts -./outputs \ No newline at end of file +# 注意:gitignore 不认 "./" 前缀,/outputs 才是「仓库根下的 outputs」。 +# outputs 里全是 TestGenPreview / 主题生成器 / 截图这类一次性产物, +# 每次跑都会变,跟版本库无关。 +/outputs/ diff --git a/.workbuddy/memory/2026-10-20.md b/.workbuddy/memory/2026-10-20.md index 9ee5e79..417eefd 100644 --- a/.workbuddy/memory/2026-10-20.md +++ b/.workbuddy/memory/2026-10-20.md @@ -115,3 +115,12 @@ **顺手发现**:`.gitignore` 写的是 `./outputs`(无效写法),导致 247 个 outputs 文件被跟踪, 每次跑 `TestGenPreview` 都会产生一堆 M。没擅自改(会让人以为要清理历史产物),留待用户决定。 + +## 12:20 outputs 不再纳入版本控制 + +- 根因:`.gitignore` 里写的是 `./outputs`,**gitignore 不认 `./` 前缀**,整条规则等于失效, + 247 个产物(预览快照、截图、历史 review 报告)一直被跟踪,每次跑 `TestGenPreview` 都产生一堆 M。 +- 改成 `/outputs/` + `git rm -r --cached outputs`:索引删除 247 个,**磁盘文件一个没动**。 + `git check-ignore -v outputs/x` 确认命中。 +- 另两条同样无效的规则没动:`./workbuddy`(目录根本不存在,笔误)、`./scripts`(有 1 个文件被跟踪)。 + 用户只要求 outputs;要清理得单独确认。 diff --git a/outputs/TamaBox-v4-code-review-2026-08-27.md b/outputs/TamaBox-v4-code-review-2026-08-27.md deleted file mode 100644 index 3e6d000..0000000 --- a/outputs/TamaBox-v4-code-review-2026-08-27.md +++ /dev/null @@ -1,113 +0,0 @@ -# TamaBox-v4 完整代码审阅报告 - -> 审阅时间:2026-08-27 -> 审阅对象:`TamaBox-v4-new-toupiao`(基于 NekoBox 的 Go 匿名问答 + 匿名投票 Web 应用,Flamego + GORM + MySQL/PostgreSQL + Redis) -> 审阅范围:全项目 Go 源码(约 20.8k 行,86 个 internal 文件 + 21 个 route 文件 + cmd)、关键模板、路由/中间件、配置 -> 审阅方式:针对性精读(认证/会话、投票、私聊、提问、个人资料、Markdown 清洗、缓存、路由/中间件、限流)+ 全代码库自动化广度扫描 + 逐项复检历史报告(08-13 / 08-18 / 08-24 / 08-26)+ 核对 Flamego 框架源码确认中间件语义 -> 结论口径:仅就当前代码可直接确认的问题定性,未做全量编译/运行(本环境缺 CGO/libwebp 与测试数据库,与 08-26 报告一致) - ---- - -## 一、总体结论 - -**当前代码质量高、安全水位扎实,历史报告中的 P0/P1 缺陷已全部修复,未发现可阻断上线的 P0/P1 漏洞。** 剩余问题集中在 **健壮性 / 可扩展性(P2)** 与 **防御纵深缺口 / 易用性(P3)**,无直接可利用的高危漏洞。 - -本项目在安全意识上明显优于一般同类项目:CSRF 全覆盖、会话密码版本失效、图片像素/解压炸弹防护、代理 SSRF 三处防护、内容审核 fail-closed、匿名投票复合唯一索引 + HMAC 签名指纹、SafeGo 防崩溃、私聊原子计数 + 回滚、CSV 公式注入防护等均已落地。 - ---- - -## 二、历史问题复检(相对 08-13 / 08-18 / 08-24 / 08-26) - -逐条核对后,**此前报告中列为 P0/P1/P2 的问题在当前代码中均已处理**: - -| 历史问题 | 状态 | 当前证据 | -|---|---|---| -| 真实云服务/数据库密钥入库 | 已知未处理 | 按用户要求保留(测试环境凭据),非本次新增 | -| 图片解压炸弹(OOM) | ✅ 已修复 | `internal/imagecheck` 像素校验 + 三处解码入口接入 | -| 6 位 token 可爆破 | ✅ 已缓解 | 投票 `LinkToken` 用 `randstr.String(12)`;私聊 `Conversation.Token` 6 位(旧约定,低风险) | -| PostgreSQL schema 硬编码 | ✅ 已修复 | `fixPostgresSequences` 改用 `conf.Database.Schema` | -| 密码修改后旧会话仍有效 | ✅ 已修复 | `PasswordVersion` + `authenticatedUser` 版本校验 | -| 找回密码邮箱枚举 | ✅ 已修复 | 邮箱不存在也渲染统一「已发送」页 | -| 内容审核 fail-open(08-24 P1-1 两处) | ✅ 已修复 | `question.go:136-149`、`page.go:317-329`、`page.go:444-458`、`route/vote/censorCheck` 均在 `err != nil` 时拒绝发布 | -| 匿名 `vote_fp` Cookie 可伪造(08-26 P1-01) | ✅ 已修复 | `route/vote/vote.go:83-93` `validVoterToken` 做 HMAC 签名校验;`newVoterToken` 签名 | -| Markdown 实体编码 XSS(08-26 P1-02) | ✅ 已修复 | `internal/template/markdown.go:147` `sanitizeHTML` 对 `TextToken` 调用 `html.EscapeString`;`markdown_test.go:35` 含 XSS 回归测试 | -| 多选重复选项重复计票(08-26 P2-01) | ✅ 已修复 | `parseMultipleOptionIDs`(`vote.go:753-780`)用 `seen` 集合去重 | -| `max_votes_per_fingerprint` 配置矛盾(08-26 P2-02) | 🟡 已缓解 | 业务规则固定为一人一票(`parseVoteMeta` 硬编码 `maxVotes=1`,UI 无控制);但 DB 列与模板默认值仍残留(见 P3-5) | - ---- - -## 三、新发现的问题(本次审阅) - -### P2(中危 / 健壮性·可扩展性) - -**P2-1 投票结果/CSV 全量加载所有提交记录(O(N) 内存+CPU)** -`LoadResults`(`route/vote/vote.go:209`)对 multiple/rank/rating 类型,以及 `aggregateFormFieldStats` / `aggregateCustomStats` / `VoteResults` / `VoteExportCSV` 均使用 `db.VoteSubmissions.ListByVoteID(ctx, voteID, nil)`(无游标、无上限)把**全部提交记录载入内存**再逐条 `json.Unmarshal` 聚合。结果虽有 30s 进程内 TTL 缓存,但每个缓存窗口的首次计算对热门投票(数千~数万条)是一次性内存/CPU 尖峰。配合 P2-2 的灌票风险,可能放大为资源耗尽。 -**建议**:聚合逻辑下推 SQL(`COUNT ... GROUP BY option_id` 已有;rank/rating/form 应补 `SUM/AVG` 或分页流式聚合);或限定单次加载行数上限并降级展示。 - -**P2-2 匿名投票防刷阈值偏高 + 清 Cookie 可绕过 token 维度限流** -`voteSubmitRateLimitMiddleware`(`internal/route/ratelimit.go:151`)对匿名用户: -- IP 维度兜底 = `limit*10` = **200 次/分钟**; -- token 维度 = `limit` = 20 次/分钟,但**仅当请求携带有效签名 `vote_fp` 时才计数**。 - -若箱主关闭验证码(`RequireCaptcha=false`,DB 默认 `true` 但可由表单关闭),攻击者在每次提交时发送无效/清空 `vote_fp` Cookie,`ensureVoterToken` 会签发一个**全新的有效签名 token**,token 维度限流永不触发,仅 IP 维度 200/min 兜底生效 → 单 IP 每分钟可灌入最多 200 张真实票,污染结果统计。 -**建议**:匿名投票始终要求验证码(或行为验证码);降低 IP 兜底阈值;或改为服务端持久化指纹(绑定 IP+UA 哈希)替代纯客户端 Cookie。 - -### P3(低危 / 防御纵深·易用性) - -**P3-1 autoban 的 404 探测自动封禁失效**(经 Flamego 源码核实) -`internal/route/autoban.go:46-58` 在 `defer` 中读取 `rw.Status()` 判断 404 探测并 `b.Record(KindNotFound)`。但 Flamego 的调度(`flamego@v1.9.4/context.go:190-229`)在**每个 handler 返回后、下游执行前**就运行 `defer`,且此时 `Written()==false`、`Status()==0`,因此 `KindNotFound` 分支**永远不触发**——404 探测型自动封禁形同虚设。敏感路径封禁(`KindSensitive`,在 defer 前同步设置)仍正常。 -**建议**:在 allow 分支显式调用 `ctx.Next()`,使 `defer` 在下游 handler 执行后才运行;或在 `NotFound` 钩子里直接统计 404 信号。 - -**P3-2 进程内结果缓存跨实例不一致 + 新提交延迟最多 30s** -`resultsCache` / `voteTextRepliesCache` 为单进程 TTL 缓存(`internal/cache/cache.go` 已确认 `sync.RWMutex` 并发安全)。多实例横向扩容时各实例独立缓存,结果会发散;且提交成功后未主动失效,参与者「投票后查看结果」最多延迟 30s 才反映自己的票。 -**建议**:多实例改用 Redis 共享缓存(项目已用 flamego/cache-redis);或在 `VoteSubmissions.Create` 成功后删除对应 `vote.ID` 的结果缓存 key。 - -**P3-3 死配置字段 `max_votes_per_fingerprint`** -业务逻辑已固定为一人一票(`parseVoteMeta` 硬编码 `maxVotes=1`,UI 无控制项),但 `db.Votes.MaxVotesPerFingerprint` 列仍存在并被写入,`VoteNew` 仍设默认值 1。无功能影响,但易误导维护者以为可配置多票。 -**建议**:移除该列与模板默认值,或补 UI/文档明确「一人一票」。 - -**P3-4 草稿/归档投票 POST `/vote/{token}/submit` 提示误导** -`route/vote/vote.go:647-655`:`isVoteActive` 为 false 且非 ended 时统一提示「该投票尚未开始」。对 `draft`/`archived` 状态的投票(其链接本不该被提交),该提示不准确(实际应提示「不存在或已失效」)。 -**建议**:按 `vote.Status` 区分提示文案。 - -**P3-5(信息项,非缺陷)已确认无误的点** -- 复合唯一索引 `(vote_id,user_id)` / `(vote_id,voter_token)` 利用 MySQL/PG 的 NULL 不冲突语义正确实现一人一票;`VoteSubmissions.Create` 用 `isDuplicateKeyError` 将唯一冲突转为 `ErrVoteAlreadyVoted` 兜底 TOCTOU。 -- `db.User` 的 `Password`/`PasswordVersion`/`PagePassword`/`AskPassword` 均为 `json:"-"`,`ProfileAPI`(`ctx.JSON(ctx.User)`)与 `ExportProfileJSON` 均**不泄漏口令哈希**。 -- `resultsCache` 返回共享 `*Results` 指针,构造后未被变异,当前安全;属脆弱点(未来若在某 handler 内修改该对象会影响所有读者)。 - ---- - -## 四、改进方向(非缺陷,建议项) - -1. **聚合下推 SQL**:投票结果统计(rank/rating/form)改为数据库层聚合或分页流式处理,消除热门投票的内存/CPU 尖峰(对应 P2-1)。 -2. **匿名指纹防刷增强**:持久化指纹 + 始终验证码 + 更严格的 IP 阈值,替代纯客户端 Cookie(对应 P2-2)。 -3. **缓存失效与一致性**:提交成功后主动失效结果缓存;多实例用 Redis 共享(对应 P3-2)。 -4. **自动化测试补齐**:增加集成/E2E 用例覆盖——内容审核 fail-closed、并发一人一票(唯一索引兜底)、`vote_fp` 签名防伪造、SSRF 逐跳校验、CSV 公式注入、autoban 敏感路径封禁。 -5. **CI 质量门禁**:引入 `golangci-lint` + `govulncheck`;当前 `go test ./...` 因 CGO/libwebp 与未配置测试 DB 无法全绿,建议在带 C 工具链与测试库的预发布环境跑全量 `go test ./...`、`go vet ./...`、`govulncheck ./...`。 -6. **清理与一致性**:移除死配置 `max_votes_per_fingerprint`、修正草稿/归档投票提交提示、统一错误日志与监控指标(若干 `_ =`/忽略错误可改为显式 metric)。 - ---- - -## 五、审阅覆盖清单 - -| 模块 | 审阅结论 | -|---|---| -| 认证/会话(`route/auth`、`internal/context/auth`、`internal/db/users`) | 密码版本失效、会话重生成到位;口令哈希 `json:"-"` 不泄漏 | -| 投票参与端(`route/vote/vote.go`) | 一人一票(唯一索引+签名 token)、fail-closed 审核、CSRF、去重、限流到位;P2-1/P2-2 待优化 | -| 投票管理端(`route/user/vote.go`) | `getOwnedVote` 全链路归属校验;草稿/发布分级正确;P3-4 提示待修 | -| 私聊(`internal/route/chat`) | 原子计数+回滚、归属校验、fail-closed 审核、邮件节流,均正确 | -| 提问(`route/question`) | 审核 fail-closed、token 访问控制、追问等待逻辑正确 | -| Markdown 清洗(`internal/template/markdown.go`) | 实体编码 XSS 已修复(EscapeString) | -| 缓存(`internal/cache`) | 并发安全(RWMutex);P3-2 跨实例/失效待优化 | -| 路由/中间件/限流(`internal/route`) | 限流/IP 封禁/自动封禁正确拦截(WriteHeader 即停链);P3-1 autoban 404 探测失效 | -| 个人资料/导出/停用(`route/user/profile.go`) | 仅操作 `ctx.User.ID`,无 IDOR;导出不泄漏口令 | -| 模板 XSS 输出 | `QuestionFormat`/`AnswerFormat` 已 EscapeString;仅 CSRF token 用 `Safe`(服务端生成)| - ---- - -## 六、优先级建议 - -1. (可选,按需)P2-1 / P2-2:若投票功能面向公网且预计有热门投票,优先加固匿名防刷与结果聚合性能。 -2. P3-1:修正 autoban 404 探测(一行 `ctx.Next()` 改动,恢复防御纵深)。 -3. P3-2 ~ P3-4:随迭代清理,不阻塞上线。 -4. 上线前在完整 CI 环境跑全量测试与漏洞扫描,确认无回归。 diff --git a/outputs/TamaBox-v4-code-review-followup-2026-08-26.md b/outputs/TamaBox-v4-code-review-followup-2026-08-26.md deleted file mode 100644 index aea9dbe..0000000 --- a/outputs/TamaBox-v4-code-review-followup-2026-08-26.md +++ /dev/null @@ -1,140 +0,0 @@ -# TamaBox-v4 修复后代码复审报告 - -- **复审时间**:2026-08-26 -- **审阅对象**:TamaBox-v4 -- **复审基线**:`outputs/TamaBox-v4-code-review-report-2026-08-26.md` -- **复审范围**:上一轮 4 项确认问题的修复调用链、相关数据库模型、模板与回归测试;并对修复影响面做定向静态复查。 -- **结论口径**:本报告仅将能由当前代码直接确认的问题列为确认问题;环境限制与建议性事项单独列出。 - -## 一、复审结论 - -上一轮报告中的 4 项确认问题均已完成代码级修复,未发现原问题仍可沿原路径复现: - -| 原问题 | 复审结论 | 关键位置 | -|---|---|---| -| P1-01 匿名 `vote_fp` Cookie 可伪造 | **已修复** | `route/vote/vote.go:62-120, 691-700` | -| P1-02 Markdown 实体编码导致 XSS | **已修复** | `internal/template/markdown.go:143-147` | -| P2-01 多选重复选项可重复计票 | **已修复** | `route/vote/vote.go:232-247, 749-775, 802-809` | -| P2-02 投票次数配置与一人一票约束矛盾 | **已修复** | `route/user/vote.go:199-202, 563-566`、`templates/user/vote-form.html:85-88` | - -本轮复审另发现并修复了一个由 P1-01 修复引入的兼容性问题:签名 token 长度为 108 字符,原数据库字段 `size:64` 不足,已调整为 `size:128`(`internal/db/vote_submissions.go:54-55`)。 - -**当前代码级结论:未发现 P0/P1/P2 确认问题。** - -## 二、逐项复审证据 - -### 1. P1-01:匿名指纹 Cookie 伪造与防刷绕过 - -**状态:已修复。** - -- `newVoterToken` 生成 32 字节随机 nonce,格式为 `64 位十六进制 nonce + "." + HMAC-SHA256 签名`。 -- `signVoterToken` 使用 `conf.Server.XSRFKey` 作为服务端密钥;配置初始化已强制要求该密钥非空。 -- `validVoterToken` 检查分段数量、nonce 长度、十六进制格式,并使用 `hmac.Equal` 比较签名。 -- `ensureVoterToken` 对无效/伪造 Cookie 重新签发;`currentVoterToken` 对无效 Cookie 返回空值,不再将客户端任意字符串作为身份依据。 -- 匿名提交无论是否带有效 Cookie,均按投票维度执行 24 小时 IP 计数;当前一人一票策略下,同一投票同一 IP 窗口内最多接受一次匿名提交。 -- 数据库 `UNIQUE(vote_id, voter_token)` 仍作为有效 token 的最终并发防重兜底。 - -**残余设计取舍**:IP 兜底会对 NAT、校园网、公司网络等共享出口产生误伤;这是保守防刷策略,不是身份伪造漏洞。若业务必须支持共享出口多人投票,应改用验证码、设备/行为风控或更细粒度限流,而不能取消服务端限流。 - -### 2. P1-02:Markdown 实体编码 XSS - -**状态:已修复。** - -- `sanitizeHTML` 的 `html.TextToken` 分支现在对 tokenizer 已解码的文本调用 `html.EscapeString` 后再写入 `template.HTML`。 -- 因此 `<script>`、双重编码输入不会在重建 HTML 时重新变成可执行标签。 -- 标签、属性、URL 白名单逻辑保持有效;事件属性和危险协议仍被过滤。 -- 回归测试已覆盖实体编码、双重编码、script、事件属性、危险 URL、iframe、SVG、style 以及 font color 属性注入。 - -### 3. P2-01:多选重复 option ID 重复计票 - -**状态:已修复。** - -- `parseMultipleOptionIDs` 统一负责多选参数解析。 -- 解析时使用 `seen` 集合;同一 `option_id` 出现两次直接返回“选项不能重复选择”。 -- 同时校验空提交、最大选项数、选项归属和 `uint` 溢出,避免异常数值绕过校验。 -- `LoadResults` 的多选聚合层也对历史数据中的重复 option ID 做防御性去重,因此旧异常数据不会在每次结果计算中再次放大统计。 -- 新增测试覆盖正常多选、重复选项、超出上限、无效选项和整数溢出。 - -### 4. P2-02:MaxVotesPerFingerprint 与数据库约束矛盾 - -**状态:已修复。** - -- 当前数据模型明确采用一人一票:登录用户由 `UNIQUE(vote_id, user_id)` 约束,匿名用户由 `UNIQUE(vote_id, voter_token)` 与 IP 兜底共同保护。 -- 管理端解析不再信任旧页面或手工请求提交的 `max_votes_per_fingerprint`,统一按 `1` 保存。 -- 新建/编辑失败回填也固定显示 `1`;表单输入设置为 `readonly`、`max="1"`,避免继续传达可配置多票语义。 -- 四种语言的字段文案已改为明确的一人一票含义。 - -## 三、本轮新增兼容性问题及修复 - -签名 token 的组成长度为:64 字符 nonce + 1 个分隔符 + 43 字符 Raw Base64URL HMAC = **108 字符**。复审过程中确认原字段: - -```go -VoterToken *string `gorm:"size:64;..."` -``` - -不足以存储完整 token,可能导致 MySQL 严格模式下匿名提交失败或 token 截断。现已改为: - -```go -VoterToken *string `gorm:"size:128;uniqueIndex:idx_submission_token"` -``` - -并加入 token 长度回归断言。部署时依赖启动阶段的 GORM AutoMigrate 将字段扩容;上线前应在预生产数据库确认迁移结果和唯一索引状态。 - -## 四、验证结果 - -### 通过 - -```text -gofmt -d route/vote/vote.go route/vote/vote_stats_test.go route/user/vote.go \ - internal/db/vote_submissions.go -# 无输出,格式通过 - -go test -count=1 ./route/vote ./internal/template ./templates ./internal/i18n \ - ./internal/security/autoban ./internal/security/censor ./internal/imagecheck \ - ./internal/shared ./internal/cache -# 全部 PASS - -go vet ./route/vote ./internal/template ./templates ./internal/i18n -# 无输出,检查通过 -``` - -### 全量验证限制 - -```text -go test ./... -go vet ./... -``` - -未能全绿,原因是当前环境已有的基础设施限制,不是本轮修复代码引入: - -1. `github.com/chai2010/webp@v1.4.0` 在当前 Windows 环境缺少 CGO/WebP 编译符号(如 `webpGetInfo`、`webpDecodeRGB`),导致 `internal/storage`、`route/question`、`route/user`、主程序等相关包无法完整编译。 -2. `internal/db` 测试未配置数据库类型,测试初始化报 `Unknown database type: ""`。 - -因此仍需在生产同构的 Linux/CGO 构建环境,并配置测试数据库后执行: - -```text -go test ./... -go vet ./... -govulncheck ./... -``` - -## 五、上线前建议 - -1. 在预生产数据库执行/确认 AutoMigrate,确认 `vote_submissions.voter_token` 已扩容到至少 128 字符,且唯一索引仍存在。 -2. 使用两个不同匿名客户端验证:同一 IP 同一投票第二次提交被拦截;伪造 `vote_fp` 会被覆盖且不能绕过 IP 限制;正常匿名首次投票成功。 -3. 用真实浏览器验证实体编码文本仅以文字显示,不执行脚本;覆盖提问、回答、投票描述和结果页等 Markdown 输出点。 -4. 若站点存在大量共享出口,结合验证码或行为风控评估 IP 一票策略的误伤率。 - -## 六、修改文件清单 - -- `internal/template/markdown.go` -- `internal/template/markdown_test.go` -- `route/vote/vote.go` -- `route/vote/vote_stats_test.go` -- `route/user/vote.go` -- `templates/user/vote-form.html` -- `internal/db/vote_submissions.go` -- `internal/i18n/dict_zh_cn.go` -- `internal/i18n/dict_zh_tw.go` -- `internal/i18n/dict_en.go` -- `internal/i18n/dict_ja.go` diff --git a/outputs/TamaBox-v4-code-review-report-2026-08-26.md b/outputs/TamaBox-v4-code-review-report-2026-08-26.md deleted file mode 100644 index 23eff3b..0000000 --- a/outputs/TamaBox-v4-code-review-report-2026-08-26.md +++ /dev/null @@ -1,105 +0,0 @@ -# TamaBox-v4 代码逻辑与安全审阅报告 - -- **审阅时间**:2026-08-26 -- **审阅对象**:`C:\Users\Tamakyi Gekka\Downloads\TamaBox-v4-backup-20260813-025725-release\TamaBox-v4-new-toupiao` -- **审阅范围**:Go 后端、路由/中间件、投票与私聊业务、模板渲染与 Markdown 清洗、配置与测试 -- **审阅方式**:静态代码审阅、调用链回溯、关键路径输入校验检查、现有测试执行 -- **结论口径**:本报告只把能由当前代码直接确认的问题列为“确认问题”;无法在当前环境完整运行的部分单列为验证限制。 - -## 一、总体结论 - -当前版本已有较多安全加固(CSRF、会话密码版本、图片像素限制、代理 SSRF 防护、内容审核、投票提交唯一索引等),但仍发现 **4 项需要处理的确认问题**: - -- **P1:2 项**:匿名投票指纹 Cookie 可伪造绕过防刷;Markdown 清洗器存在实体编码导致的存储型 XSS。 -- **P2:2 项**:多选重复选项可被重复计数;“同一指纹可投次数”配置与数据库唯一索引/应用逻辑不一致,配置大于 1 时实际仍只能投 1 次。 - -因此,当前不建议直接以“P0/P1 已全部清零”作为上线判断。至少应先修复两项 P1,并对投票防刷策略做明确取舍。 - -## 二、确认问题清单 - -### P1-01 🔴 匿名投票指纹 Cookie 未签名,可无限伪造绕过防刷 - -| 项目 | 详情 | -|---|---| -| 位置 | `route/vote/vote.go:52-90`、`route/vote/vote.go:619-669` | -| 触发条件 | 匿名投票,攻击者可自行发送任意 `vote_fp` Cookie 值 | -| 证据 | `currentVoterToken` 直接返回 Cookie 值;`ensureVoterToken` 也只检查“是否存在”,未校验服务端签名或格式。匿名提交只有在 `currentVoterToken(ctx) == ""` 时才执行 IP 计数(`route/vote/vote.go:656-665`)。数据库唯一索引只约束同一 `vote_id + voter_token`,攻击者每次换一个伪造值即可创建一条新记录。 | -| 影响 | 可绕过匿名投票的 IP 兜底与 Cookie 防重复机制,批量提交大量票;结果统计可被污染,验证码未开启时尤其明显。 | -| 修复建议 | 不要把客户端 Cookie 当作可信身份。使用带 HMAC 的服务端签名 token(签名内容包含随机值、版本与过期时间),服务端拒绝无效/过期/格式异常 token;对匿名提交始终保留 IP/设备/行为限流,不应因 Cookie 存在而跳过 IP 保护。若需要强防刷,使用服务端持久化指纹或按投票维度的限速。 | - -### P1-02 🔴 Markdown 清洗器对实体编码文本未再次转义,存在存储型 XSS 路径 - -| 项目 | 详情 | -|---|---| -| 位置 | `internal/template/markdown.go:95-117`,重点为 `internal/template/markdown.go:103-106` | -| 触发条件 | 用户输入经过 Markdown 渲染后,文本中包含实体编码形式的 HTML,例如 `<img src=x onerror=alert(1)>`;该结果通过 `MarkdownToHTML` 以 `template.HTML` 输出。 | -| 证据 | `html.Tokenizer.Text()` 返回“已解码”的文本(`golang.org/x/net/html` 文档/实现明确为 unescaped text);当前 `extractPlainText` 在 `html.TextToken` 分支直接 `out.WriteString(string(tokenizer.Text()))`,未调用 `html.EscapeString`。因此实体编码可能重新变成原始 `<...>` 字符并被当作 HTML 输出。`MarkdownToHTML` 最终在第 73 行以 `template.HTML(cleaned)` 返回,形成危险输出路径。现有 `internal/template/markdown_test.go:35-54` 没有覆盖实体编码绕过。 | -| 影响 | 提问、回答、投票描述等使用 Markdown 渲染的内容可能形成存储型 XSS;受影响页面上的访问者/登录用户可能执行攻击者脚本。 | -| 修复建议 | 在 `sanitizeHTML` 的 `html.TextToken` 分支对 tokenizer 文本再次使用 `html.EscapeString` 后写入;或改用成熟且经过长期验证的 HTML sanitizer(如 bluemonday),并补充 `<script>...`、双重编码、属性上下文等回归测试。修复后检查所有 `template.HTML` 输出点。 | - -### P2-01 🟠 多选提交未拒绝重复选项 ID,可造成单次提交重复计票 - -| 项目 | 详情 | -|---|---| -| 位置 | `route/vote/vote.go:741-758` | -| 触发条件 | 对多选投票手工构造 POST,使 `option_ids` 重复,例如 `option_ids=12&option_ids=12`。 | -| 证据 | 后端只检查 `len(ids) > v.MaxChoices`、选项 ID 是否存在,然后直接将每个 ID追加到 `chosen`;未使用 `seen` 集合去重。结果聚合 `route/vote/vote.go:203-223` 遍历 `data.Options`,每次出现都增加该选项计数。 | -| 影响 | 一次提交可让同一选项被重复计数;若 `MaxChoices` 大于 1,可用重复值放大某选项票数,破坏统计可信度。 | -| 修复建议 | 解析时维护 `seenOptionIDs`,重复 ID 直接拒绝或去重;同时在统计层对历史异常数据做防御性去重,并补充重复参数测试。 | - -### P2-02 🟠 “同一指纹可投次数”配置与实际实现矛盾 - -| 项目 | 详情 | -|---|---| -| 位置 | `route/user/vote.go:199-205`、`route/vote/vote.go:619-669`、`internal/db/vote_submissions.go:49-55` | -| 触发条件 | 箱主将 `max_votes_per_fingerprint` 设置为 2-100。 | -| 证据 | 管理端允许并保存 1-100(`route/user/vote.go:199-205`);但 `VoteSubmission` 定义了唯一索引 `UNIQUE(vote_id, voter_token)`,且提交前 `HasTokenVoted` 一旦已有记录就直接拒绝(`route/vote/vote.go:627-635`)。因此同一匿名指纹对同一投票始终只能提交一次。该配置只在“无 Cookie 时”的 IP 计数分支使用,无法实现其名称暗示的多次投票。 | -| 影响 | 管理员配置与实际行为不一致;若业务预期允许同一指纹投多次,功能失效;若业务只允许一票,配置项会误导并使防刷策略难以理解。 | -| 修复建议 | 二选一:①业务规则是一人一票,则把字段固定为 1、移除 2-100 UI 和相关逻辑;②业务确实需要多投,则去掉 `UNIQUE(vote_id, voter_token)`,改为服务端计数/窗口约束,并使预检、数据库约束、IP 兜底和结果策略统一。不要只改 UI。 | - -## 三、已检查且未发现确认问题的重点区域 - -- **认证/会话**:登录成功后调用会话 ID 重生成;密码版本变更会使旧会话失效(`route/auth/login.go:64-74`、`internal/context/auth.go:26-30`)。 -- **CSRF**:Web POST 统一进入 CSRF 校验;注销已改为 POST;当前未发现可直接确认的高风险 CSRF 缺口。 -- **投票归属**:管理端通过 `getOwnedVote` 校验 `vote.UserID == ctx.User.ID`,结果查看、编辑、发布、关闭、删除、导出均复用该校验。 -- **SQL 注入**:业务查询主要使用参数绑定;本次检索未发现由用户输入直接拼接 SQL 的确认路径。 -- **远程图片代理**:域名白名单、私网 IP 拦截、重定向逐跳校验、固定解析 IP 拨号和响应体上限均已存在;仍建议后续补充集成测试。 -- **图片上传**:已在解码前检查像素上限,降低解压炸弹风险。 -- **Flash/模板普通文本**:`templates/base/alert.html` 使用普通模板转义;当前未发现历史报告所述的 FlashTip 原样 HTML 输出问题。 - -## 四、验证结果与环境限制 - -### 已通过 - -```text -go test ./internal/template ./route/vote ./templates ./internal/security/autoban \ - ./internal/security/censor ./internal/imagecheck ./internal/shared ./internal/i18n -PASS -``` - -### 完整测试未能作为全绿结论 - -`go test ./...` 当前失败,主要原因是: - -1. 本机 CGO/WebP 依赖导致 `github.com/chai2010/webp` 编译错误(`webpGetInfo` 等符号缺失),进而使 `internal/storage`、`route/question`、`route/user`、主程序等包无法完整编译。 -2. `internal/db` 测试未配置数据库类型,多个测试报 `Unknown database type: ""`。 - -上述失败不等同于本次发现的 P1/P2 已被测试覆盖;建议在带 C 工具链与测试数据库的 CI/预发布环境重新执行: - -```text -go test ./... -go vet ./... -govulncheck ./... -``` - -## 五、修复优先级建议 - -1. **P1-02**:先修 Markdown 清洗器实体编码绕过,并新增 XSS 回归测试。 -2. **P1-01**:改为服务端签名匿名 token;同时恢复匿名提交的稳定限流,不因客户端 Cookie 存在而完全跳过 IP/行为防刷。 -3. **P2-01**:多选参数去重 + 统计层防御性去重。 -4. **P2-02**:明确“一人一票”还是“同指纹多票”,统一数据库约束、应用预检、配置和 UI 文案。 -5. 在修复后使用真实测试数据库、启用 CGO 的构建环境重新跑全量测试与漏洞扫描。 - -## 六、与历史报告的差异说明 - -项目内已有的 `code-review-report-final.md` 将上一轮结论描述为“P0/P1 全部清零”。本次复审发现的 2 项 P1 与投票相关 P2 属于当前代码路径中仍存在的独立问题,因此不能直接沿用上一轮“全部清零”的结论;应以本报告的当前代码证据为准。 diff --git a/outputs/TamaBox-v4-feature-control-fix-review-2026-08-31.md b/outputs/TamaBox-v4-feature-control-fix-review-2026-08-31.md deleted file mode 100644 index 7c522e9..0000000 --- a/outputs/TamaBox-v4-feature-control-fix-review-2026-08-31.md +++ /dev/null @@ -1,144 +0,0 @@ -# TamaBox-v4 功能管理修复 + 代码审阅报告 - -- **日期**:2026-08-31(更新于同日 16:00,追加 §三 遗留项修复) -- **范围**:4 个已报告问题的修复 + 全链路回归审阅 + 审阅发现项二次修复 -- **状态**:✅ 4 个问题已修复;✅ 审阅发现的 P1-2 / P2-1 已一并修复;仅剩已知边界 P2-2(时区约定,非 bug) - ---- - -## 一、修复总览 - -| # | 用户报告的问题 | 根因 | 修复 | 验证 | -|---|---------------|------|------|------| -| 1 | 🔴「开启该功能」取消勾选保存后自动恢复勾选 | `Enabled bool` 带 `gorm:"default:true"`,GORM 在 Create 时把零值 `false` 替换为默认值 `true` 写入(INSERT 值与 ON CONFLICT 的 `excluded.enabled` 都是 true) | 移除该标签,GORM 原样写入显式值 | 生成的 SQL 已确认写入 `false`;单元测试通过 | -| 2 | 🟠 勾选「仅允许注册用户向我提问」后,二维码下仍显示「谁都可以以匿名的形式提问」 | `templates/question/list.html:165` 与 `question-not-found.html:86` 硬编码匿名文案,与 `HarassmentSetting` 无关 | 文案随设置联动:新增 `ask.registerOnly` 词条,`Pager` 注入 `RegisterOnlyAsk`,模板二选一 | 新增 3 个模板渲染测试 | -| 3 | 🟠 功能关闭时只显示「提问功能尚未开放」,缺少上次关闭/下次开放时间 | 关闭提示仅有 `FeatureClosedMessage` 一行 | `FeatureState` 新增 `NextOpenAt` / `LastCloseAt`,关闭提示下追加两行时间信息 | 新增 2 个渲染测试 + 逻辑单元测试 | -| 4 | 🟡 希望支持每周几 / 每天几点到几点循环开放 | 原模型只有一次性 `OpenAt/CloseAt` | 新增 `ScheduleDays`(周几列表)+ `ScheduleStart/End`(每日时段),`Evaluate` 综合判定 | 新增 20+ 个单元测试用例 | - -**验证结果**:`internal/db`、`internal/i18n`、`internal/template`、`templates`、`internal/shared`、`internal/security/censor` 全部测试通过;`go vet ./...` 无新增告警(仅 Windows 沙箱 webp CGO 缺库的已知噪音);模板解析/跨引用/渲染测试全绿。 - ---- - -## 二、修复细节 - -### 1. 问题 1 根因(P0,已修复) - -`internal/db/feature_control.go`: - -```go -// 修复前(BUG) -Enabled bool `gorm:"default:true"` - -// 修复后 -Enabled bool // 不要加 default:true 标签(注释已标注原因) -``` - -**机制**:GORM 在 `Create` 时对带 `default` 标签的字段,若值为零值(`false`)会替换为 `DefaultValueInterface`(`true`)再写库。实测生成 SQL: - -```sql --- 修复前:Enabled=false 传入 → VALUES 里写入 true -INSERT INTO feature_controls (..., enabled, ...) VALUES (..., true, ...) - ON CONFLICT (...) DO UPDATE SET enabled = excluded.enabled -- 也是 true - --- 修复后:Enabled=false 传入 → VALUES 里写入 false ✓ -INSERT INTO feature_controls (..., enabled, ...) VALUES (..., false, ...) -``` - -**连带修复**:`backfillFeatureControls` 为私聊(默认关闭)补行时同样会把 `false` 写成 `true`——即历史上所有私聊默认关闭的用户可能被静默开启。同一处修复同时解决。 - -### 2. 问题 2(P1,已修复) - -- `route/question/page.go`:`Pager` 注入 `ctx.Data["RegisterOnlyAsk"] = pageUser.HarassmentSetting == register_only`。 -- `templates/question/list.html` / `question-not-found.html`:二维码下文案改为 - `{{ if .RegisterOnlyAsk }}仅允许注册用户向TA提问{{ else }}谁都可以以匿名的形式提问{{ end }}`。 -- 新增 i18n 词条 `ask.registerOnly`(4 种语言)。 - -### 3. 问题 3(P1,已修复) - -- `FeatureState` 新增 `NextOpenAt` / `LastCloseAt`(由 `Evaluate` 计算)。 -- `Pager` 格式化为 `AskNextOpenAtStr` / `AskLastCloseAtStr`。 -- `new-question-template.html` 在关闭提示下方追加: - - `下次开放时间:2026-09-02 10:00`(未开始 / 循环定时未来再开时) - - `上次关闭时间:2026-08-31 18:00`(曾经开放过时) -- 手动关闭 / 定量上限两类与时间无关的状态不展示时间行。 - -### 4. 问题 4(P1,新功能) - -**模型**(`feature_controls` 新增 3 列,`AutoMigrate` 自动加列): - -| 列 | 语义 | -|----|------| -| `schedule_days` | 逗号分隔周几,`0=周日…6=周六`(与 `time.Weekday` 一致);空 = 每天都开放 | -| `schedule_start` / `schedule_end` | 每日时段 `HH:MM`;都为空 = 开放日全天 | - -**语义**: -- 只填开放日 → 该天全天开放(每周循环);只填时间 → 每天该时段开放;都填 → 按周循环的每日时段开放。 -- 与一次性 `OpenAt/CloseAt` **叠加(AND)**:一次性窗口是硬性外边界,循环定时仅在其内生效。 -- `Evaluate` 判定优先级:手动关闭 > 未到一次性开放 > 已过一次性关闭 > 不在循环窗口 > 定量上限。 -- 「下次开放时间」按日向前扫描 8 天(覆盖全部周几组合),与一次性窗口取交集。 - -**UI**(`feature-control-form.html`):新增「循环定时(每周循环)」区块——7 个开放日胶囊复选框 + 每日开始/结束 `time` 输入;「只填开放日=全天 / 只填时间=每天」提示写入 hint。 - -**校验**(`route/user/features.go`):开始/结束必须成对填写;`HH:MM` 合法;结束须晚于开始;开放日仅接受 0-6 并去重排序存储。 - ---- - -## 三、审阅发现的其他潜在问题 - -### 🟠 P1-1 一次性时间解析的时区偏移(已修复) - -- 位置:`route/user/vote.go` `parseTimeInput`。 -- 问题:`time.Parse("2006-01-02T15:04", s)` 解析结果落在 **UTC**,而 `Evaluate` 用 `time.Now()`(服务器本地时区)比较——服务器非 UTC 时,「定时开放/关闭」与「投票起止」会整体偏移(如东八区偏移 8 小时)。 -- 修复:改用 `time.ParseInLocation(..., time.Local)`,与 `time.Now()` 比较基准一致。 -- 注意:修复前已保存的时间值按 UTC 解释,部署后若服务器本地时区非 UTC,历史设定会有一致性位移(UTC 服务器无感知)。 - -### 🟠 P1-2 历史数据:旧版 backfill 可能已把私聊错误开启(✅ 已修复) - -- 位置:`internal/db/feature_control.go` 新增 `reconcileLegacyChatEnabled`(`db.Init` 中在 backfill 之后调用)。 -- 问题:旧版本(`Enabled gorm:"default:true"`)下,为 `allow_chat=false` 的用户补私聊行时,`false` 被写成 `true`,**私聊被被动开启**;且 `UpdateChatSettings` 会反向把 `users.allow_chat` 同步为 `true`,形成持久化。 -- 修复:启动期**幂等纠偏**,同时命中以下条件才复位(绝不误伤箱主配置过的行): - - `feature='chat'` 且 `enabled=true`; - - 对应 `users.allow_chat=false`(箱主从未在面板开启私聊——开启会同步 allow_chat=true); - - 该行无任何自定义配置(一次性时间为空、quota_limit=0、循环定时为空); - - `updated_at <= created_at`(从未被面板保存过)。 - - 命中即复位 `feature_controls.enabled=false` 并同步 `users.allow_chat=false`;每次启动只处理未修复行,直到清零,失败仅告警不阻断启动。 -- 说明:对于「allow_chat 已被旧同步逻辑改写为 true」的更深度历史污染,无法与箱主主动开启区分,不在自动纠偏范围内;如需处理请人工核对后按下方 SQL 定向调整: - ```sql - -- PostgreSQL(仅当确认某账号私聊被错误开启时,替换 ) - UPDATE feature_controls SET enabled = false WHERE feature = 'chat' AND user_id = ; - UPDATE users SET allow_chat = false WHERE id = ; - ``` - -### 🟡 P2-1 `Vote.RequireCaptcha` 同类 GORM 陷阱(✅ 已修复) - -- 位置:`internal/db/votes.go` `RequireCaptcha bool`(原带 `gorm:"default:true"`)。 -- 问题:与问题 1 同源——若底层 `Votes.Create` 未来允许 `RequireCaptcha=false`,GORM 会把 `false` 替换为默认 `true`。 -- 修复:去掉 `default:true` 标签。业务层 `route/user/vote.go` 仍硬编码 `requireCaptcha := true`(决策 #3),**线上行为不变**;已同步更新 `votes_test.go` 的 `testVotesCreate` 断言(Create 未显式传参时按 `false` 落库,注释说明业务层强制 true)。实测生成的 SQL:显式 `true` 写入 `true`、未传写入 `false`。 - -### 🟡 P2-2 循环定时的时区与跨日限制(已知边界,建议知晓) - -- 时段按**服务器本地时区**解释(与一次性时间一致)。若服务器与箱主时区不同,每日时段会整体偏移;如需支持,可后续增加箱主时区字段。 -- 不支持跨午夜的时段(如 22:00-02:00,结束必须晚于开始),校验会拦截并给出中文提示。 -- 「下次开放时间」扫描窗口为 8 天,覆盖任意周几组合;一次性窗口 >8 天的场景由一次性分支单独处理,不受影响。 - ---- - -## 四、回归检查(原问题是否复现 / 影响面) - -| 检查项 | 结果 | -|--------|------| -| 问题 1(开关复位):`Save` 生成的 SQL 正确写入 `false`,ON CONFLICT 更新列含 `enabled` | ✅ | -| 问题 2(文案):匿名/注册专属两种状态的渲染测试 | ✅ 3 个用例通过 | -| 问题 3(关闭信息):未开始 / 已结束(永久)/ 循环定时三种关闭形态的模板渲染 | ✅ 2 个用例通过 | -| 问题 4(循环定时):14 组 Evaluate 判定 + 8 组 NextOpenAt/LastCloseAt 边界 | ✅ 全部通过 | -| P1-2 纠偏:SELECT(带 `fc.` 别名)/ UPDATE(无前缀)生成的 PostgreSQL SQL 均正确;软删除行已过滤 | ✅ SQL 生成验证 | -| P2-1 Vote:`Create(RequireCaptcha=true)` 写入 `true`、未传写入 `false`(不再被默认替换) | ✅ SQL 生成验证 + 断言更新 | -| 模板:新增 `hasDay` 函数与 `printf` 组合无解析错误,全部模板解析/跨引用测试通过 | ✅ | -| i18n:新增 12 个词条 × 4 语言齐全,无缺失(缺失会回退 zh-CN,但已全部补齐) | ✅ | -| 代码质量:`gofmt` 干净、`go vet ./...` 无新增告警 | ✅ | -| 存量行为:未改 `CanAsk` 计算、未改 `FeatureOpen` 语义、投票业务流(硬编码 requireCaptcha=true)不变 | ✅ | - -**部署提醒**: -1. `feature_controls` 新增 3 列由 `AutoMigrate` 自动加列,无手工迁移;上线后在功能管理页重新保存一次各功能即可看到循环定时区块。 -2. 私聊纠偏为**启动期自动执行**:首次启动时若命中旧数据会输出 `Warn "私聊开关纠偏:旧版数据把未开启私聊的账号置为开启,已复位为关闭"`(附数量),属预期行为;未命中则无日志。 -3. `TestVotes` 等 DB 集成测试需在配置了 `DB_TYPE` 的 PostgreSQL/MySQL 环境运行(本地沙箱无凭据,未执行);逻辑层测试已全部通过。 diff --git a/outputs/TamaBox-v4-keyset-pagination-timezone-incident-2026-09-05.md b/outputs/TamaBox-v4-keyset-pagination-timezone-incident-2026-09-05.md deleted file mode 100644 index 9d1e75b..0000000 --- a/outputs/TamaBox-v4-keyset-pagination-timezone-incident-2026-09-05.md +++ /dev/null @@ -1,146 +0,0 @@ -# TamaBox 提问箱「加载更多」翻页重复事故复盘(时区偏移根因 + timestamptz 迁移修正) - -- **日期**:2026-09-05(排查 01:57–04:35 左右,约 2.5 小时闭环) -- **影响范围**:线上 `box.tama.guru` 全部「提问箱历史」列表(单题页 `/_/{domain}/{id}` 与用户主页底部内嵌同一列表) -- **最终结论**:两层缺陷叠加——① 服务端游标解析的时区偏移导致 keyset 分页失效(直接根因);② 为根治而做的 timestamptz 迁移解释基准选错,导致存量时间整体 +8h(次生事故,已二次修正) -- **状态**:✅ 全部修复并线上验证通过 - ---- - -## 一、现象与影响 - -| 现象 | 影响 | -|---|---| -| 提问列表首屏 20 条加载完后「加载更多」按钮仍存在 | 点击后**追加与首页完全相同的内容** | -| 按钮永不消失,可无限点击重复 | 用户困惑;若服务端异常则无限膨胀 DOM | - -线上 API 实测复现:携带首页返回的 `next_cursor` 再次请求,返回**完全相同的第 1 页 20 条**,`has_more=true`、游标不变。 - ---- - -## 二、关键环境事实(排查中确认) - -| 项 | 值 | 备注 | -|---|---|---| -| 线上 schema | `nekobox`(非 public,默认 search_path 找不到) | 首版诊断脚本"关系不存在"根因 | -| `questions` 表 | `id numeric`(seq)、`user_id bigint`、`answer text`、`is_private/is_pinned/is_read boolean` | 与源码 GORM 模型基本一致 | -| **时间列** | `created_at/updated_at/deleted_at = timestamp WITHOUT time zone`(6) | **与源码 AutoMigrate 默认 timestamptz 不一致**——本次两大事故的共同土壤 | -| 实际含无时区时间列的表 | 仅 4 张:`questions / users / censor_logs / upload_images`(12 列) | 其余表(messages/conversations/votes 等)本为 timestamptz | -| 服务端进程/会话时区 | Asia/Shanghai(+08:00) | 与无时区列叠加构成偏移 | -| aoi 箱 | user_id=329,已答公开 38 条 | 与线上 API 总数一致 | - -分页链路:前端 `templates/question/history-template.html`(Alpine fetch)→ `route/question/page.go ListAPI` → `internal/db/questions.go GetByUserID`,PinFirst 复合键集游标 `..`,排序 `is_pinned DESC, created_at DESC, id DESC`,`page_size=20` 多取 1 条探测 `has_more`。 - ---- - -## 三、排查时间线 - -| 时间(约) | 事件 | 结果/判断 | -|---|---|---| -| 01:57–02:30 | 线上复现 + 全链路代码走查 | 前端模板与 0904 源码逐字符一致;排除 CDN 缓存(回源 MISS);**服务端游标被无视** | -| ~02:30 | 对照实验 | 任意格式游标均返回首页/个别怪异结果(空集、固定 6 条、38 条全量)——源码逻辑无法解释,疑似版本不一致 | -| 02:39 | 产出 v1 PG 诊断脚本 | 用户执行报"关系不存在"→ 定位 schema=nekobox | -| ~03:00 | 产出 v2 诊断脚本(search_path + 无时区列 + 时区诊断) | **DB 层洗清**:38 条 keyset 翻页不重不漏、无时区列读写文本对称、nano 指纹精确命中 | -| ~03:10 | 首次部署 0904(含防重兜底)后复测 | **仍复现**——推翻"部署版本旧"假设 | -| ~03:15 | 读 `parsePinCursor` 源码 + 本地 tz 实验 | 🔴 **真根因**:`time.Unix(0, nano)` 按服务器本地时区还原,+8h 墙钟发给无时区列比较恒真 → keyset 失效 | -| ~03:25 | 修复 `.UTC()` + 编译测试 | `go build`/`go vet` PASS | -| ~03:30 | 用户重新部署 | 线上翻页**38 条唯一收敛** ✓;非法/旧格式游标返回空+`has_more=false` ✓ | -| ~03:40 | 克隆库 timestamptz 迁移全套验证 | epoch 前后 0 差异、指纹一致、翻页同款、回滚路径无损 | -| 04:02 | 线上执行迁移 | 4 表 12 列转 timestamptz,验收 3.1–3.4 全绿(epoch 一致、残余 0) | -| ~04:20 | 端到端验收 | 38 条唯一收敛 ✓,但**发现 created_at 文本从 Z 变 +08:00** | -| 04:27 | 用户反馈「真实本地 00:01 的提问页面显示 08:01」 | 🟠 **次生事故确认**:迁移解释基准用错(见第五节) | -| 04:30–04:35 | 克隆库验证反向修正 → 线上执行 → API 验收 | created_at 回到本地墙钟、`next_cursor` nano 变化、38 条收敛 ✓ | - ---- - -## 四、直接根因(第一层):游标解析时区偏移 - -```go -// 修改前(internal/db/questions.go parsePinCursor) -return isPinned, time.Unix(0, nano), uint(idVal), true -// ↑ 返回「服务器本地时区」的时间 -``` - -**机制**: -1. 服务端把某行 `created_at`(无时区列读出后带 UTC 标记)转 `UnixNano` 下发(nano = UTC 绝对时刻)。 -2. 解析时 `time.Unix(0, nano)` 返回 **Local(Asia/Shanghai)** 时间 → 墙钟数字 +8h(00:01 → 08:01)。 -3. 驱动把该参数发往 **无时区列** 比较时按墙钟直发 → `created_at < '2026-09-05 08:01:16'` 对库内全部 `00:xx` 行**恒为真** → keyset 条件退化为"全部行" → **永远返回第一页 + has_more=true → 无限重复**。 -4. 之前线上"空集 / 固定 6 条 / 38 条全量"等怪异现象,均为偏移后游标落在数据窗口不同位置的表现。 - -**修复**:`time.Unix(0, nano)` → `time.Unix(0, nano).UTC()`(绝对时刻语义,与列值墙钟对齐)。同时补充两层防重兜底: - -| 文件 | 改动 | -|---|---| -| `internal/db/questions.go` `parsePinCursor` | `time.Unix(0, nano).UTC()` ← **根治一行**(附时区约定注释) | -| `internal/db/questions.go` `GetByUserID` | 游标解析失败**不再静默回退首页**,返回空列表 → `has_more=false` 收敛 | -| `internal/db/questions.go` `getBy`(旧路径) | 同步改为解析失败返回空,保持一致 | -| `templates/question/history-template.html` | SSR/JS 条目都带 `data-qid`;点击时按已渲染 id 集合**过滤重复**,无新增即 `more=false` 隐藏按钮 | - -> 注:其余 `UnixNano` 用途(随机种子/文件名)无 keyset 风险,未受影响。 - ---- - -## 五、次生事故(第二层):timestamptz 迁移基准选错,存量 +8h - -### 5.1 数据真实语义的判定 - -存量无时区列存的是**上海本地墙钟**(如 `00:01:16` = 用户凌晨 00:01 提问)。证据:用户以现实锚点反馈"真实本地 00:01 的提问页面显示 08:01"。 - -**此前 v2 诊断的认知盲区**:`DB 墙钟 == 线上 JSON Z 文本` 只能证明**读路径文本一致**——驱动把无时区列读出值标记为 UTC 输出,文本恰好等于库内墙钟数字,形成"读写对称"假象;它**不能证明**墙钟的世界语义是 UTC。而驱动写入无时区列时按进程本地墙钟落库 → 真实语义其实是**上海墙钟**。 - -### 5.2 错误迁移及其后果 - -首版迁移用 `USING created_at AT TIME ZONE 'UTC'`(把墙钟当 UTC 解释)→ 存量绝对时刻整体 **+8h** → 页面显示比真实晚 8 小时。迁移前页面显示"00:01"(无时区列按 UTC 直显,文本恰巧等于上海墙钟)反而是对的。 - -### 5.3 反向修正 - -```sql -ALTER TABLE nekobox.questions - ALTER COLUMN created_at TYPE timestamptz - USING (created_at AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Shanghai'); --- 净效果:绝对时刻 -8h;语义 = 原墙钟按 Asia/Shanghai 解释;与会话时区无关 -``` - -克隆库整段演练通过后线上执行,API 验证: - -| 验证点 | 修正前(错误) | 修正后(正确) | -|---|---|---| -| 3089 会话显示 | `2026-09-05 08:01:16.678885+08` | `2026-09-05 00:01:16.678885+08` | -| 3089 epoch | `1788566476678885000` | `1788537676678885000` | -| 3109 | `08:49:38+08` | `00:49:38.648502+08` | -| 2411 | `06-18 06:30:19+08` | `2026-06-17 22:30:19.615754+08` | -| API `next_cursor` nano 段 | `0.1788566476678885000` | `0.1788537676678885000` | -| 翻页收敛 | — | 38 条唯一 ✓ | - ---- - -## 六、经验教训(预防清单) - -| 级别 | 教训 | 落地动作 | -|---|---|---| -| 🔴 P0 | **无时区时间列与带时区应用代码混用是事故土壤** | 代码约定:所有时间列应统一 timestamptz;任何 `time.Unix`/`time.Parse` 进 keyset/SQL 比较路径必须显式 `.UTC()` 并在注释写明约定 | -| 🔴 P0 | **迁移解释基准必须先锚定数据真实语义**,不能只看"转换前后 epoch 一致" | 迁移前用 1–2 条**用户可确认真实时刻**的记录做锚点(如"这条我实际几点发的"),或写一条测试数据核对显示 | -| 🟠 P1 | "DB 墙钟 == JSON 文本"不能证明世界语义 | 读路径文本一致 ≠ 写路径语义正确;需结合写入方(驱动对无时区列的落库行为)判断 | -| 🟠 P1 | 线上行为无法用源码解释时,先怀疑**时区/列类型**再怀疑部署版本 | 本次"部署后仍复现"正是锁定 `.UTC()` 的关键转折 | -| 🟡 P2 | psql `\i` 长输出会进分页器截断 | 运维长脚本建议开头 `\pset pager off`,或分段执行 | - ---- - -## 七、脚本/文档产物清单(`outputs/`) - -| 文件 | 用途 | 状态 | -|---|---|---| -| `pg-cursor-pagination-diagnose.sql` | v1 诊断(默认 public schema,被 schema 问题阻住) | 已被 v2 取代 | -| `pg-cursor-diagnose-v2-nekobox.sql` | v2 诊断(search_path=nekobox,洗清 DB 层) | 完成使命 | -| `timestamptz-migration-online.sql` | 首版迁移(UTC 基准)——**含错误基准,勿复用** | ⚠️ 已被 fix-8h 修正 | -| `timestamptz-migration-fix-8h.sql` | 反向修正(Asia/Shanghai 基准) | ✅ 线上已执行;**不可重跑**(会再 -8h) | -| 本文档 | 事故复盘留档 | ✅ | - -## 八、最终状态与遗留建议 - -- **线上已闭环**:翻页 38 条唯一收敛;存量时间回到真实本地墙钟;timestamptz 类型层根治,keyset 不再依赖进程时区。 -- **遗留建议(按需排期)**: - 1. 页面强刷后人工抽查时间显示(SSR 与「加载更多」追加项应一致)。 - 2. 可选:应用层 JSON 序列化统一 `.UTC()` 输出 Z(若存在外部消费者依赖 UTC 文本)。 - 3. 本地克隆测试实例(127.0.0.1:5433)与 dump 副本已无用途,可清理。 - 4. 生产库 `DROP SCHEMA diag_test CASCADE;` 清理诊断快照表。 diff --git a/outputs/TamaBox-v4-remediation-2026-08-27.md b/outputs/TamaBox-v4-remediation-2026-08-27.md deleted file mode 100644 index 90ae9d4..0000000 --- a/outputs/TamaBox-v4-remediation-2026-08-27.md +++ /dev/null @@ -1,65 +0,0 @@ -# TamaBox-v4 评审修复落地报告(2026-08-27) - -基于 `outputs/TamaBox-v4-code-review-2026-08-27.md` 的 P2/P3 findings,落实 4 项修复。 -改动遵循「最小、可读、可回滚」原则;每项均给出根因、改法、验证状态。 - ---- - -## 已落地(4 项) - -### P3-1 autoban 404 探测失效 ✅ -**文件**:`internal/route/autoban.go` -**根因**:`autobanMiddleware` 内 `defer` 在该 handler **返回时**触发,而 Flamego `run()` 会在 handler 返回后自动推进链路,因此 `defer` 执行时下游路由尚未运行,`rw.Status()` 恒为 `0` → `KindNotFound` 分支永远进不去。敏感路径封禁因 `kind` 在 body 内同步置位,不受影响。 -**修复**:在 `defer` 之后、函数返回前加 `ctx.Next()`。经核对 `flamego@v1.9.4`:`Next()` 即 `c.index++; c.run()`,会把下游全部跑完并让 `c.index` 越过末尾,外层循环随后只「超调」一次即退出,**不会重复执行下游**。 -**验证**:`internal/security/autoban` 单测通过(安全核心逻辑);`gofmt` 语法校验通过。 - -### P3-4 草稿/归档提交提示误导 ✅ -**文件**:`route/vote/vote.go`(`Submit`,约 647 行) -**根因**:`!isVoteActive` 仅二分「已结束 / 尚未开始」。草稿(`draft`)与归档(`archived`)状态均 `Status != published` 且通常无 `EndAt`,双双落入「尚未开始」,而重定向目标 `/vote/{LinkToken}` 本身会拦截草稿/归档 → 死循环式跳转。 -**修复**:按 `vote.Status` 精确分支: -- `draft` → 「该投票尚未发布」 -- `archived` → 「该投票已归档,无法参与」 -- 其余:`isVoteEnded` → 「该投票已结束」/ 否则「该投票尚未开始」 -**验证**:`gofmt` 语法校验通过;状态常量 `db.VoteStatusDraft/Archived` 已核实存在。 - -### P2-2 匿名投票强制验证码 ✅ -**方向**(用户拍板):匿名投票始终要求验证码(或行为验证码)。 -**文件**:`route/vote/vote.go`(`Submit`)、`route/user/vote.go`(`parseVoteMeta` / `renderVoteFormWithInput`)、`templates/user/vote-form.html` -**根因**:原逻辑 `if vote.RequireCaptcha` 仅在创建者开启验证码时才校验匿名提交;关闭后匿名用户清除 `vote_fp` Cookie 即可换签名 token 重复投票,仅剩 `limit*10` 的 IP 兜底。 -**修复**: -1. `Submit` 匿名分支门槛由 `if vote.RequireCaptcha` 改为 `if conf.Recaptcha.CaptchaType != ""`——**只要全局配置了验证码,匿名提交一律校验**;未配置(无密钥部署)时不阻断,避免整崩。 -2. `parseVoteMeta`(草稿创建/编辑、发布中编辑三路共用的唯一 `require_captcha` 读取点)与 `renderVoteFormWithInput` 强制 `requireCaptcha := true`,使存储值与策略一致。 -3. 模板复选框置为 `checked disabled`,对 owner 明确「不可关闭」。 -**对齐既有决策**:`db.Votes.RequireCaptcha` gorm 默认 `true`,`votes_test.go:63` 断言「匿名验证码默认开启(决策 #3)」,i18n 即「匿名投票需通过验证码」。本修复使该决策不可被 UI 关闭。 -**验证**:`gofmt` 语法校验通过;`conf.Recaptcha.CaptchaType` 字段在 `internal/context/context.go:269` 已使用,类型无误。 - -### P2-1 结果缓存写穿失效 ✅(务实版) -**文件**:`internal/cache/cache.go`、`route/vote/vote.go`(`Submit`) -**根因**:`LoadResults` 对 `multiple/rating/rank/form` 类型需 `ListByVoteID` 全量加载并在 Go 层逐条 `json.Unmarshal` 聚合(O(N));`resultsCache`(30s TTL)仅在过期时重算,新提交后结果页最多陈旧 30s,且每次窗口到期都要重算一次全量。 -**修复**: -1. 给 `TTLCache` 新增 `Delete(key)` 方法。 -2. `Submit` 成功 `Create` 提交后调用 `resultsCache.Delete(strconv.FormatUint(vote.ID,10))`——**写穿式失效**:新投票后立即清除该投票结果缓存,结果页随即反映,且把 O(N) 重算成本收敛到「每次新投票一次」。 -**未做(需独立迁移)**:真正的 O(1) SQL 下推受限于选项选择结果存于 JSON `FormData` 大字段(跨 MySQL/PG 做 JSON 聚合既脆弱又依赖具体版本)。彻底消除 O(N) 需把选择项正态化到独立表(如 `vote_submission_selections(vote_id, submission_id, option_id)`),以 `GROUP BY option_id` 聚合。建议作为后续独立 schema 迁移评估,**不在本次改动内**。 -**验证**:`internal/cache` 单测通过(含新 `Delete`);`gofmt` 语法校验通过。 - ---- - -## 本次未改(留作独立决策) - -- **P3-3 死配置 `MaxVotesPerFingerprint`**:全链路写死为 1、提交侧从不读取,属历史遗留。鉴于设计本意即「一人一票」,建议整体移除该字段与表单接线;涉及 DB schema 迁移,需你确认后再做。 -- **P3-2 进程内缓存跨实例陈旧**:`TTLCache` 为进程本地,多实例下清缓存不跨实例。可接受最终一致 / 缩短 TTL / 迁 Redis——架构决策,不急。 - ---- - -## 构建与验证状态 - -- ✅ 语法:`gofmt -e -l` 对 4 个改动的 .go 文件均无报错。 -- ✅ 单元:`go test ./internal/security/autoban/... ./internal/cache/...` 全绿。 -- ⚠️ 全量构建受阻(**既有环境问题,非本次改动**):`github.com/chai2010/webp` 依赖 libwebp 本机 C 库(CGO),当前沙箱缺该库,报 `undefined: webpGetInfo` 等。该依赖经传递链被 `route/vote`、`route/user`、`internal/route` 引入,故这几个包在本环境无法完整 `go build`。在带 libwebp 的 CI/Docker 环境中可正常构建。建议你在本地/CI 跑一次全量 `go build ./...` 与 `go test ./...` 做最终确认。 - -## 改动文件清单 -- `internal/route/autoban.go` -- `internal/cache/cache.go` -- `route/vote/vote.go` -- `route/user/vote.go` -- `templates/user/vote-form.html` diff --git a/outputs/admin6-cdn-check.png b/outputs/admin6-cdn-check.png deleted file mode 100644 index ce33c2d..0000000 Binary files a/outputs/admin6-cdn-check.png and /dev/null differ diff --git a/outputs/admin6-snrat-check.png b/outputs/admin6-snrat-check.png deleted file mode 100644 index 48850d4..0000000 Binary files a/outputs/admin6-snrat-check.png and /dev/null differ diff --git a/outputs/aliyun-censor-empty-data-diagnosis-2026-09-10.md b/outputs/aliyun-censor-empty-data-diagnosis-2026-09-10.md deleted file mode 100644 index fff1cf8..0000000 --- a/outputs/aliyun-censor-empty-data-diagnosis-2026-09-10.md +++ /dev/null @@ -1,235 +0,0 @@ -# 阿里云审核通道「response data is empty」诊断报告 - -日期:2026-09-10 -场景:`@scene#15:"日常开发"` — 内容审核 / `[security] text_censor_source = aliyun` - ---- - -## 最终落地(2026-09-10 16:40)— 已迁移至内容安全 2.0 - -三条路径中**已选定并落地「方案 A:迁移到内容安全增强版 2.0」**,同时采纳第五节提出的折中防呆 -(首次出现账号级拒绝时打一条一次性告警)。 - -### 为什么不是方案 B / C - -- **B(升级企业实名认证)**:需要企业资质,当前账号不具备;且 1.0 的标签体系本身也已过时。 -- **C(放弃阿里云只用七牛)**:会丢掉 auto 模式后半段「七牛误报时由阿里云二次复核」的能力。 - -### 关键事实(官方文档核实) - -| 项目 | 内容安全 1.0(旧) | 文本审核增强版 2.0(新) | -| --- | --- | --- | -| 接口 | `green.cn-shanghai` + `/green/text/scan` | `green-cip.cn-shanghai.aliyuncs.com` + `TextModeration` | -| SDK | `alibaba-cloud-sdk-go`(已从依赖中移除) | `alibabacloud-go/green-20220302/v3 v3.5.4` | -| 开通前提 | **企业实名认证**(个人账号永远 596) | **实名认证即可**(个人可开通) | -| 计费 | — | 文本审核标准 7.5 元/万次;开通免费、按量出账 | -| 单次上限 | — | `content` ≤ **600 字符**(超长必须分段) | - -⚠️ **一处必须记录的事实修正**:`green-20220302` SDK 中 -`TextModerationResponseBodyData.Labels` 的内联注释列出的标签(10 个,且不含涉政) -**是旧的、不完整的**。以官方《文本审核增强版标签处理建议参考》为准,2.0 通用服务 -**包含** `political_content`(涉政)、`violence`(暴恐)、`sexual_content`(色情)、 -`C_customized`(自定义词库)—— 即 `policy.go` 的底线四件套可完整覆盖, -**不存在此前担心的「迁移后失去涉政拦截」缺口**。 - -### 落地改动 - -| 文件 | 改动 | -| --- | --- | -| `internal/security/censor/aliyun.go` | 整文件重写为 2.0:`TextModeration` + 归一化响应 + 标签映射 + 分段审核 + 一次性账号告警 | -| 同上 | 审核场景固定 `comment_detection`(公聊评论:官方定位覆盖涉政/涉暴/涉黄底线 + 广告导流) | -| 同上 | 超长文本按 550 字分段、段间重叠 50 字;合并口径为标签取并集、风险级别取最高 | -| 同上 | `riskLevel` → 置信度折算 high=1 / medium=0.6 / low=0.3,供 `policy.go` 的阈值复用 | -| 同上 | 归一化响应带 `api: "TextModeration"` 格式标记 | -| `internal/security/censor/censor.go` | 阿里云缓存回放解析失败时**退回实时调用**,不再直接抛错 | -| `internal/security/censor/{text,policy}.go` | 同步注释(置信度来源由 rate 改为 riskLevel 折算) | -| `internal/security/censor/aliyun_test.go` | 新增 2.0 测试套件(分段 / 标签映射 / 多片合并 / 兼容性 / 告警判定) | -| `internal/security/censor/policy_test.go` | 移除 1.0 格式的旧测试 | -| `conf/app.sample.ini`、`internal/conf/static.go` | 注释改为 2.0 开通指引 | -| `go.mod` / `go.sum` | 移除已无引用的 `alibaba-cloud-sdk-go v1.62.26` | - -### 两个刻意的设计取舍 - -1. **格式标记 + 缓存回退**:解析器拒绝一切非 `api: "TextModeration"` 的载荷,`censor.go` - 在解析失败时退回一次实时调用。作用:迁移前 1.0 落库的缓存(若有)既不会被误判成 - 「通过」(那会变成绕过审核的后门),也不会因 fail-closed 让这些历史文本在 31 天缓存 - 过期前一直发不出去。 -2. **一次性账号级告警**:首次出现 596 / Forbidden / InvalidAccessKeyId 等「账号侧未就绪」 - 失败时,打印一条带开通入口与授权说明的 WARN,之后不再重复 —— 零成本、无外部依赖, - 把「静默全站拒签」变成日志里一眼可见的可操作信号。 - -### 校验结果 - -- `go build -overlay=... ./...`:通过 -- `go vet ./internal/security/...`:通过 -- `go test ./...`:`internal/security/censor` 全绿;仅 `internal/db` 失败, - 且失败信息均为 `Unknown database type: ""`(需真实 PostgreSQL/MySQL),与本改动无关 -- `gofmt -l`:仅 `censor.go`(改动前既有的空格缩进,按约定未动) - -### 仍需你处理 - -1. 在阿里云**内容安全 2.0 控制台**开通「内容审核增强版」按量付费;RAM 子账号授予 - `AliyunYundunGreenWebFullAccess`。开通前:`auto` 模式只走七牛;`aliyun` 模式仍会 fail-closed。 -2. CHANGELOG 尚未记入(见文末第五节)。 - ---- - -## 〇、结论速览(诊断阶段,2026-09-10 16:05) - -> ✅ **根因(两层)** -> 1. 直接原因:**阿里云账号未开通「内容安全 1.0」** → `code=596`。 -> 2. 深层原因:**「内容安全 1.0」的开通前提是账号已完成企业实名认证**(官方文档明文)。 -> 若该账号是**个人实名认证**,则 1.0 **永远无法开通**,无论在产品页怎么点。 - -诊断改动落地后,下一条日志立即给出了答案: - -``` -error="text scan (request body={\"scenes\":[\"antispam\"],\"tasks\":[{\"content\":\"测试!\"}]}): - aliyun rejected the request: code=596 - msg=\"You have not opened Yundun Content Moderation Service, please go to the page - (https://www.aliyun.com/product/lvwang) to open our service after login Ali cloud console\" - request_id=01A08A4B-CCE2-5AAF-9329-A0C51C0F5060 - raw={\"code\":596,\"httpStatusCode\":200,\"msg\":\"...\",\"success\":false}" -``` - -三点关键信息: - -1. **`httpStatusCode: 200` + `success: false` + 无 `data`** —— 精确印证了第二节的推断: - 阿里云在**请求被拒时依然返回 HTTP 200**,所以旧代码只会报出「`data` 为空」这一句废话。 -2. `code=596` 是**账号级「服务未开通」**拒绝,与请求体、签名、Content-Type、RAM 权限均无关 - (RAM 权限不足会返回 403/Forbidden,不是 596)。 -3. 用户按提示点开后仍报 596,原因见下:**开通入口与认证门槛**。 - -### 开通门槛对照(官方文档原文) - -| 服务 | 开通前提条件 | 个人实名认证可否 | -| --- | --- | --- | -| **内容安全 1.0**(`green`, 2018-05-09 — 本项目在用) | **需要账号完成企业认证**(`document_detail/69806.html`,2026-07-23 更新) | ❌ 不可 | -| 内容安全增强版 2.0(`green-cip`, 2022-03-02) | 需要账号完成**实名认证**(`document_detail/477720.html`) | ✅ 可 | - -另附两点排查结论: - -- 用户点开的 `https://www.aliyun.com/product/lvwang` **只是产品介绍页**(现为「AI 安全护栏」聚合页), - 点不出「一键开通 1.0」。1.0 的正确开通入口是: - `https://common-buy.aliyun.com/?commodityCode=cdi#/open`。 -- 但即使换到正确入口,个人认证账号依然会被「企业认证」前提卡住 → 596 不会消失。 - -### 三条可选路径 - -| 方案 | 代价 | 说明 | -| --- | --- | --- | -| **A. 改用「内容安全增强版 2.0」** | 🟠 需改代码(中等) | 个人认证即可开通。需换成 `green-cip..aliyuncs.com` + `TextModeration` API + 新 SDK(`green-20220302`),并**重做风险标签映射**(2.0 标签体系与 1.0 完全不同:`pornographic_adult` / `inappropriate_profanity` / `pt_to_contact` …),策略层 `policy.go` 的维度清单需同步重写。 | -| **B. 把该账号升级为企业实名认证** | 需企业资质 | **代码零改动**,升级后到正确入口开通 1.0 即可。 | -| **C. 放弃阿里云通道,只用七牛** | 功能损失 | 置 `text_censor_source = auto` / `qiniu`。阿里云通道的原始用途是「七牛误报时的二次复核」,放弃后该能力消失。 | - -📌 **副产品结论**:第三节第 2 项(`SetContentType(requests.Json)`)**已被证伪**——它不是本次根因。 -改动保留,但代码注释已改成「按接口契约对齐」而非「修 bug」,避免后人误解。 - -📌 **副产品结论**:第三节第 2 项(`SetContentType(requests.Json)`)**已被证伪**——它不是本次根因。 -改动保留,但代码注释已改成「按接口契约对齐」而非「修 bug」,避免后人误解。 - -## 一、报障现象 - -``` -ERRO Failed to censor text censor_source=aliyun error="response data is empty" -ERRO Failed to censor text error="no censor source available" strictness=lax -``` - -两条同时出现 = 链上**所有**通道都没给出结论 → `censor.Text` 返回错误 → `moderation.Check` fail-closed 拒签。 - -> 🔴 **严重性说明**:fail-closed 是本项目既有设计,但推论是 —— **只要显式选定的审核通道坏掉(账号未开通 / 欠费停服 / RAM 权限不足),整站的「发布提问/回答 + 私信」会全部被拒**,且现象只在日志可见,前端只显示「审核服务不可用」。这次报障正是这种形态。 - -## 二、根因取证 - -### 2.1 请求构造:不是本项目引入的问题(已验证) - -| 比对对象 | 结论 | -| --- | --- | -| 上游 `NekoWheel/NekoBox` 的 `internal/security/censor/aliyun.go` | 与本仓 **逐字节一致** | -| 阿里云官方 Go 示例(`help.aliyun.com/document_detail/330839.htm`) | `green.CreateTextScanRequest()` + `SetContent(json)`,body 形状 `{"scenes":["antispam"],"tasks":[{"content":...}]}` **一致** | - -→ 请求的 URL、body 形状、签名流程都与官方参考实现相同,**不是本次改造引入的偏差**。 - -### 2.2 能产出该错误消息的唯一组合(已验证,走通 SDK 源码) - -逐段读过 `alibaba-cloud-sdk-go@v1.62.26` 后,可以确定报出 `response data is empty` 必须同时满足: - -| 条件 | 依据 | -| --- | --- | -| HTTP 状态是 2xx | `sdk/responses/response.go:44` 非 2xx 会被 `Unmarshal` 直接转成 `ServerError` → 走 `errors.Wrap(err,"text scan")`,不会是该消息 | -| 响应体是**合法 JSON** | `AliyunTextCensorParser` 的 `json.Unmarshal` 失败会报 `unmarshal response` | -| 顶层 `data` 为空数组 / 缺省 / null | `len(responseJSON.Data) == 0` 才命中该分支 | - -→ 即:**阿里云回了「HTTP 200 + 合法 JSON + 无 data」**。 - -顺带排除一个可疑点:`green.EndpointMap` **不含 `cn-shanghai`**,但端点解析会一路回落到 -`GetEndpointRules` → `green.cn-shanghai.aliyuncs.com`,**地址是正确的**。 - -### 2.3 顺带发现的确定缺陷(🟠 P1,非本次根因,但让根因不可见) - -旧 parser 在 `len(Data)==0` 时只报 `response data is empty`,**把顶层 `code` / `msg` / `requestId` 全部丢掉**—— -而这正是唯一能区分「签名错 / 权限不足 / 欠费 / 服务未开通 / 请求体不合契约」的线索。 -这不是「阿里云用不了」的原因,但它是「为什么查不下去」的原因。 - -## 三、已落地改动 - -`internal/security/censor/aliyun.go`、`internal/security/censor/policy_test.go` - -| # | 改动 | 性质 | -| --- | --- | --- | -| 1 | 抽出 `newTextScanRequest(text)`,便于测试断言请求形态 | 重构 | -| 2 | **显式 `req.SetContentType(requests.Json)`** | 🟡 **假设性修复**,见下方说明 | -| 3 | 顶层 `code != 0 && != 200` → 新分支 `aliyun rejected the request: code=.. msg=.. request_id=.. raw=..` | ✅ 确定改进(与旧「data 为空」同为错误返回,上层控制流不变) | -| 4 | `data` 为空 / JSON 解析失败时,错误里带上 `code/msg/requestId` + 原始响应摘要(≤512 字符、按字符截断) | ✅ 确定改进 | -| 5 | `Censor()` 中 SDK 返回 err 时补 `status` + `body`;`!IsSuccess()` 分支同样带 status/body | ✅ 确定改进 | -| 6 | `data[].code != 200`(任务级失败)**语义不变**(仍 fail-closed 置 block),但在真正决定 fail-closed 处补一条 Warn(code/msg/taskId/requestId) | ✅ 确定改进 | -| 7 | 修掉 `Censor()` 上错误的文档链接(原指向七牛 7260 → 阿里云 70439) | 文档 | -| 8 | 新增 5 个测试:请求形态+Content-Type、顶层 code 拒绝、空 data、非法 JSON、摘要截断 | 测试 | - -**关于第 2 项(Content-Type)**:SDK 对 ROA 请求的**默认回退值是 `application/octet-stream`** -(`sdk/auth/roa_signature_composer.go:88`),而 `/green/text/scan` 的接口契约是 JSON 请求体。 -该改动属**按接口契约对齐**(对已正常工作的环境无副作用)。 -> ⚠️ 曾假设它是本例根因,**已于 2026-09-10 15:50 证伪**(真实根因是 code=596 未开通服务)。 -> 保留理由仅为「显式声明正确类型优于依赖 SDK 兜底值」,代码注释已同步更正。 - -注意它必须在 `client.TextScan` 之前设置(`Content-Type` 参与 ROA 签名串)。 - -### 校验结果 - -- `gofmt -l internal/security/censor/`:仅列出 `censor.go`、`text_test.go`(升级前既有的空格缩进,按约定未动) -- `go build -overlay=... ./...`:通过 -- `go vet ./internal/security/...`:通过 -- `go test ./internal/security/... ./internal/template/...`:全绿 - -## 四、处置(已确认后) - -1. ⛔ **(已作废,见文首「最终落地」)** 原计划:到阿里云开通「内容安全 1.0」按量付费。 - 实际已确认个人实名认证账号**无法**开通 1.0(企业认证前提),故改为迁移到 2.0。 -2. ⚠️ **先恢复线上可用性**:开通动作需要时间。在开通完成前,把 `[security] text_censor_source` - 改回 `auto`(七牛优先)或 `qiniu`,否则整站发布/私信会持续被 fail-closed 拒绝。 -3. 开通后切回 `aliyun` 再验证一次;届时日志若仍报错,`code` / `msg` / `requestId` 会直接给出下一步。 - -### 为同类问题补的防呆(已落地) - -`conf/app.sample.ini` 的 `text_censor_source` 注释里补上了这段经验: - -``` -; ⚠️ 配好凭据 ≠ 可用:阿里云通道还必须在阿里云控制台开通 -; 「内容安全 1.0」按量付费(https://www.aliyun.com/product/lvwang,需勾选协议点「立即开通」), -; 否则接口返回 code=596 "You have not opened Yundun Content Moderation Service", -; 审核必然失败 → fail-closed 拒绝全部提交(整站发不出提问/回答/私信)。 -; 用 RAM 子账号调用时还需授予内容安全(Green)权限;欠费停服同理。 -; 开通完成前建议先用 auto / qiniu,避免线上不可用。 -; 审核失败时日志会打印阿里云返回的 code / msg / requestId 便于定位。 -``` - -## 五、待你决策 - -1. **是否加「启动预检」**(🟠 P2,现在证据更充分了):`text_censor_source` 显式选定某个通道时, - 启动阶段发一次探活请求,失败则告警或直接拒启。 - - 收益:把「静默全站拒签」提前到启动时暴露——本次故障正是这个形态(用户是先发现发不出内容,才去看日志); - - 成本:按条计费产生一次调用 + 启动强依赖外部网络; - - 折中方案:不做网络预检,改为在**首次**出现账号级拒绝(如 596 / 403)时打一条一次性、带处置指引的显式告警 - (复用 `warnNoCensorCredentials` 的 `sync.Once` 模式)——零成本、无外部依赖。 -2. **CHANGELOG 仍未记入**(此前遗留):按项目惯例应在 `dev/changelog-source.md` 顶部补条目并跑 - `python dev/gen_changelog.py` 重新生成 `static/tamabox.changelogs.json`。档位口径已定(strict + auto),可以写了。 diff --git a/outputs/autoban-false-positive-2026-09-05.md b/outputs/autoban-false-positive-2026-09-05.md deleted file mode 100644 index ba4db39..0000000 --- a/outputs/autoban-false-positive-2026-09-05.md +++ /dev/null @@ -1,93 +0,0 @@ -# autoban 自动封禁误伤诊断报告 - -日期:2026-09-05 · 分析对象:`internal/security/autoban` + `internal/route/autoban.go` + `internal/shared/ip.go` - -## 一、当前配置与生效机制 - -| 配置 | 你的值 | sample 默认 | 语义 | -|---|---|---|---| -| trust_proxy | true | true | 信任 CDN 权威头解析客户端 IP | -| enable_auto_ban | true | true | 启用自动封禁 | -| ban_threshold_sensitive | **10** | 10 | 窗口内敏感路径命中 ≥10 触发封禁 | -| ban_threshold_404 | **20** | 20 | 窗口内 404 ≥20 触发封禁 | -| ban_window_seconds | **10** | **60** | 统计窗口(滚动) | -| ban_duration_minutes | 15 | 15 | 封禁时长 | - -**判定逻辑**(autoban.go Record / IsBanned): -- 每个请求按「归一化客户端 IP」计数(IPv4 原样;IPv6 聚合到 /64 → `LimitKey`) -- 两类事件独立计数,`敏感≥10 OR 404≥20`(10s 滚动窗口内)任一达标 → 封禁该键 15 分钟 -- 已封禁键上所有后续请求直接 403 - -**两个计数入口**(全站仅此两处): -1. 中间件 `autoban.go:39`:请求路径命中敏感词表(`/admin`、`/.env`、`/wp-admin`、`/config`、`/db`…共 39 条前缀)→ defer 计数 -2. `f.NotFound`(route.go:403):一切未匹配路由 → 404 计数后 Redirect("/") - -## 二、排除项(已静态验证,非误伤来源) - -- ✅ **业务路由零碰撞**:全站注册路由 + 模板 77 个站内 URL 与 39 条敏感前缀**无一匹配**——正常页面打开不会误计敏感 -- ✅ favicon.ico / robots.txt / sitemap / llms 均有 embed handler,不产生 404 -- ✅ 404 处理器与业务性 302(语言切换/登出/投票不存在)已分离,不误计 - -## 三、误伤根因(按可能性排序) - -### 🔴 P0-A:客户端 IP「键折叠」→ 多用户共享同一计数键(最可能) - -正常用户自己 10s 内绝不可能凑出 10 次敏感或 20 次 404(每秒 2 个 404 的强度),所以能误伤正常用户,**唯一机制是"别人的探测被记到了你的键上"**。trust_proxy=true 的解析链(ip.go ResolveClientIP): - -1. 先看 `CF-Connecting-IP` / `EO-Client-IP`(权威头) -2. 都没有 → 回退 `RemoteAddr` - -**折叠场景**: -- 若 EO 回源**未开启/未注入** `EO-Client-IP`(需在 EO 控制台确认回源透传配置),国内请求全部回退到 **EO 边缘 IP(43.174.247.x 新加坡)** → 全体大陆用户共享一个键。扫描器也走同一边缘 → 任一扫描触发 → **全国用户 403 15 分钟** -- 海外 CF tunnel:cloudflared 必注 `CF-Connecting-IP`,正常不折叠;但若异常缺失则回退 `127.0.0.1` → 所有海外用户共享同一键 -- sample 注释原话即为该坑:"否则解析到的将是 Cloudflare 边缘 IP,自动封禁会误伤所有访客" - -**自检方法**:配置 `security_alert_email`,看封禁告警里的 IP —— 若出现 43.174.247.43 / 43.174.246.43 / 127.0.0.1 这类**非用户 IP**,即实锤折叠。 - -### 🔴 P0-B:运营商 CGNAT / 共享出口连坐(国内常态) - -国内移动/联通/电信家宽普遍 NAT444,**成百上千真实用户共用一个公网 IPv4**。该池内任一被黑设备/扫描器探测你的站点(攻击者扫全网,你的 EO 域名是公网目标)→ 触发阈值 → 整个出口池 403 15 分钟。你本人被误伤大概率由此而来(你的出口 IP 里别人在扫站)。 - -### 🟠 P1-C:IPv6 /64 聚合连坐 - -`LimitKey` 将 IPv6 聚合到 /64。若运营商分配粒度粗(多户共 /64)或会话前缀抖动,同段邻居的探测会连坐你。相对少见,但配置了 IPv6 解析时存在。 - -### 🟠 P1-D:双 CDN 权威头互信被伪造(定向误伤) - -若未按 `outputs/edgeone-cloudflare-dual-cdn-guide.md` 在两家 CDN 规则引擎删除"对方 CDN"的权威头,攻击者可伪造 `CF-Connecting-IP: <受害者IP>` 直连源站/借道 EO → 探测全部记到受害者头上 → **把任意人"定向打成 403"**。属攻击向量而非随机误伤,但同样表现为"我没干什么就 403"。 - -## 四、修复建议(分级) - -### P0-① 先止血(纯配置,立即可做,无需改代码) -```ini -ban_window_seconds = 60 ; 10s 窗口对正常浏览太苛刻,回到默认 -ban_threshold_sensitive = 30 ; 敏感命中门槛抬高(真扫描 1s 内远超此值) -ban_threshold_404 = 100 ; 404 门槛抬高(正常用户+偶发爬虫不会到) -security_alert_email = <你的邮箱> ; 关键:先打开告警看封禁 IP,定位是否"键折叠" -``` -> 窗口越短反而越难误伤,你已比默认更保守仍误伤 → 证明误伤不是参数敏感,而是**键共享**,调参只能缓解不能根治。 - -### P0-② 确认并修复 IP 解析(治本,需查服务器/控制台) -1. **源站侧**:抓一条真实请求,确认带没带 `EO-Client-IP` / `CF-Connecting-IP` 头(nginx access log 或临时 echo 头调试) - - EO 域名确认开启「回源客户端 IP 透传」;无则开 -2. **源站锁定**:防火墙仅放行两家 CDN 回源网段(sample 已警告;未锁定时任何人可直连伪造头) -3. 双 CDN 按指南删对端权威头 - -### P1-① 代码增强(需你批准后实施) -| 项 | 改动 | 效果 | -|---|---|---| -| 登录用户豁免 | 有有效 session 的请求不计数/不封禁 | 管理员/正常登录用户永不误伤(扫描器无 cookie) | -| 封禁日志 | 封禁时 logrus 打印 ip+路径样本+UA | 事后可归因,不再"黑盒" | -| UA 特征二次确认 | 敏感命中要求 UA 缺浏览器特征才加速计数 | 降低误伤 | - -### P2 观察项 -- 若确认 P0-A 折叠实锤:可考虑对"CDN 边缘类"键整体排除(仅当 RemoteAddr 属于 CDN 且权威头缺失时才回退 RemoteAddr——需小改 ip.go) -- 监控告警邮件中的封禁频率与 IP 类型分布 - -## 五、证据清单(本次分析依据) -- internal/security/autoban/autoban.go:阈值判定、窗口、清理 -- internal/route/autoban.go:中间件挂载、403 直接返回 -- internal/route/route.go:78-84、146、403:实例化、全局挂载、404 计数 -- internal/shared/ip.go:ResolveClientIP 信任链 + LimitKey /64 聚合 + 双 CDN 警告注释 -- conf/app.sample.ini:默认值与设计约束说明 -- 静态碰撞分析:业务路由/模板 URL × 敏感词表 = 零命中 diff --git a/outputs/avatar-qrcode-slow-2026-09-05.md b/outputs/avatar-qrcode-slow-2026-09-05.md deleted file mode 100644 index 42e3334..0000000 --- a/outputs/avatar-qrcode-slow-2026-09-05.md +++ /dev/null @@ -1,149 +0,0 @@ -# 个人主页"头像 + 二维码加载慢 / 页面一直加载中"诊断报告 - -> 日期:2026-09-05 | 对象:`https://box.tama.guru/_/tamakyi`(个人主页 list.html) -> 测速视角:南宁电信(真实大陆出口,与本机直连一致) - ---- - -## 一、结论速览 - -| 严重度 | 问题 | 一句话 | -|---|---|---| -| 🔴 P0 | **R2 图片全部无 `Cache-Control`** | 头像 Last-Modified 是当天 → 启发式缓存寿命 ≈ 0 → **每次**页面加载都重新走国际链路下载头像,这是"每次都很久"的直接原因 | -| 🔴 P0 | R2 是纯 CF 国际链路 | 南宁电信实测每个 R2 资源 **0.78–1.4s**,是 snrat 大陆镜像(0.3s)的 **3–5 倍**;304 条件请求也要 **1.05s**(省不掉往返 RTT) | -| 🟠 P1 | 二维码绘制依赖头像 `onload` | easy.qrcode 加载 logo 头像失败仅 `console.error`、**无任何降级** → 头像加载失败/超时 = 二维码永久空白 | -| 🟠 P1 | 头像缓存键分裂 | 同一头像被拆成 `?og-image=1`(header icon/meta)与 `?v=2`(页面头像 + QR logo)两个缓存条目 → 一页下载 2 份 | -| 🟠 P1 | 背景图 303KB 未压缩 PNG | `box_background.png` = 297KB,换设备/清缓存时首访要拉 297KB 的国际链路下载 | -| 🟡 P2 | defer 统计脚本拖慢 DOMContentLoaded | vercount 实测中位 1.24s(波动至 2.7s)、mami 波动至 3.7s → 二维码初始化挂在 DOMContentLoaded,被一起拖慢 | - ---- - -## 二、加载链路拆解(页面发生了什么) - -个人主页资源依赖(实测 61KB HTML): - -``` -EO 动态页 HTML ........................ ~1.2s (源站新加坡,已知架构上限) -head 渲染阻塞 CSS:仅 uikit.min.css (snrat) . ~0.5s - (waline CSS 已用 media=print 非阻塞,✅ 无问题) -defer 脚本 ×10(下载完才触发 DOMContentLoaded): - snrat: uikit/icons/dayjs/alpine/waline/jQuery 0.2–0.5s/个 - R2: time.box.js(1.6KB) + easy.qrcode(52KB) ~0.9–1.0s/个 - 第三方: cn.vercount.one ~1.2s(波动 2.7s)、mami.tama.guru ~0.3s(波动 3.7s) -头像 (body 解析即并发) ...... ~1s(R2) -背景 uk-img(UIkit 可见后加载) ............... ~1.4s(R2,297KB) -favicon = 头像 URL ............................ ~1s(R2) -二维码 boxqrimg: - DOMContentLoaded 后 easy.qrcode 初始化 - → 先加载 logo=头像?v=2(onload 后才绘制)→ +0~1s -``` - -**首访体感:头像 ~2.2s、二维码 ~2.5–3.5s(R2 链路抖动时 5s+)** -**二次访问:由于无 Cache-Control,头像/favicon 每次仍要 ~1s 重新下载 → 这就是"每次都慢、一直加载中"的体感来源。** - -浏览器"加载中"迟迟不退的构成:`load` 事件要等非懒加载图片(头像)完成;`DOMContentLoaded` 要等全部 10 个 defer 完成(含 R2 与第三方统计的最慢者)。两者都被 R2 + 统计脚本拖着。 - ---- - -## 三、证据链(均为 2026-09-05 实测) - -### 3.1 各资源耗时(南宁电信,3 次中位) - -| 资源 | Host | 中位耗时 | 备注 | -|---|---|---|---| -| jQuery / UIkit | cdnjs.snrat.com | **0.29s / 0.52s** | 大陆镜像,基准线 | -| 头像 webp 27KB | r2.337845818.best | **0.91–1.05s** | | -| easy.qrcode.min.js 52KB | r2.337845818.best | 0.96s | | -| time.box.js 1.6KB | r2.337845818.best | 0.90s | 小文件也 0.9s = 纯 RTT 主导 | -| 背景 box_background.png 297KB | r2.337845818.best | 1.39s | | -| waline CSS | r2.337845818.best | 0.99s | | -| vercount 统计 | cn.vercount.one | 1.24s(波动至 2.69s) | | -| mami 统计 | mami.tama.guru | 0.31s(波动至 3.7s) | | - -### 3.2 决定性验证:R2 缓存头 - -``` -头像/背景/图标/CSS 响应头: - Content-Type / ETag / Last-Modified 有 - Cache-Control ❌ 无 - Expires ❌ 无 - cf-cache-status: DYNAMIC(CF 边缘不缓存,每次都回 R2 源) -``` - -- 头像 `Last-Modified: Sat, 05 Sep 2026 06:49:29 GMT` = **当天上传** -- 按 RFC 7234 启发式缓存:freshness ≈ (当前时间 − Last-Modified) × 10% ≈ **0** → 浏览器每次**重新验证/下载** -- 实测:带 `If-Modified-Since` 的 304 请求仍需 **1.05s**(RTT 无法通过 304 消除) -- 对比:easy.qrcode.js / time.box.js 有 `max-age=2678400`(31 天强缓存)→ 它们二次访问是秒出的,唯独**图片类对象从未设置缓存头** - -> 上传代码 `internal/storage/s3.go` `PutObjectInput` 仅设置 `ContentType`,**从未设置 `CacheControl`** —— 已确认。 - -### 3.3 easy.qrcode 头像 logo 逻辑(反编译 52KB 源) - -```js -if (d.logo) { - var r = new Image; - r.onload = function(){ 绘制(带头像logo) }, // ✅ 头像到了才画 - r.onerror = function(a){ console.error(a) }, // ❌ 仅打日志,什么都不画 - r.crossOrigin = d.crossOrigin; - r.src = d.logo; // logo = 头像 URL -} -``` - -**结论:二维码 = 同步等待 R2 头像图下载;头像失败/超时 → 二维码区域永久空白,页面没有任何降级提示。** - -### 3.4 头像 URL 在同一页面出现 3 个变体(缓存键分裂) - -| 出处 | URL | 浏览器行为 | -|---|---|---| -| header.html 646–648(icon/favicon) | `头像?og-image=1` | Chrome 请求 favicon | -| header.html 642/644/649(og/twitter meta) | `头像?og-image=1` | 爬虫专用,浏览器不下载 | -| list.html 4–6 + 页面头像 + QR logo | `头像?v=2` | 页面必下载 | - -`?og-image=1` 与 `?v=2` 是两个缓存键 → favicon 与页面头像各下载一次(同一天上传的头像,各自都 miss)。 - ---- - -## 四、优化建议 - -### P0(改小、见效最大:解决"每次都慢") - -**① R2 上传图片设置强缓存头(根因修复)** -- 改 `internal/storage/s3.go` 上传处加 `CacheControl: aws.String("public, max-age=31536000, immutable")` -- 图片 key = 日期 + 15 位随机串,**天然不可变** → `immutable` 完全安全 -- 存量对象(历史头像/背景)用 `wrangler r2 object` 或 S3 API 重打 metadata;或重传一次 -- **预期收益**:二次访问头像/favicon/QR logo 从 ~1s → **0ms(本地缓存直读)**,这是本次优化中体感变化最大的一项 - -**② easy.qrcode 失败降级 + 两阶段绘制(防二维码空白)** -- 改 remake 库 logo 分支:`onerror` 时改为**先画无 logo 二维码**(立即可扫),并 `onload` 时自动重绘带 logo 版 -- 或加超时(如 3s)兜底,避免头像悬挂时二维码永久空白 -- 参考实现方向:库内 `r.onerror = function(){ 降级绘制() }`,其中降级绘制即"无 logo 路径 `b()` + makeImage" - -### P1(进一步缩短首访) - -**③ 统一头像缓存键** -- 确认 `?og-image=1` 无服务端语义(Go 代码中无处理,疑似历史遗留)后:header 的 icon/favicon 与 list.html 统一用同一 URL 形态(②落地强缓存后,`?v=2` 击穿历史污染缓存的目的已由 immutable+随机 key 取代,可逐步去掉 query) -- 收益:每页少 1 次头像下载(favicon 与页面头像共享缓存条目) - -**④ 背景图压缩** -- `box_background.png`(297KB)→ WebP(预计 ≤60KB),首访下载量降 ~5 倍;该图 2024-08 上传未再变,压缩一次长期受益 - -**⑤ 统计脚本 async 化** -- `cn.vercount.one/js`、`mami.tama.guru/tamami.js` 均非页面功能依赖 → 改 `async`(或 DOMContentLoaded 后再注入),不再拖慢 DOMContentLoaded → 二维码初始化提前 - -### P2(结构性,可选) - -**⑥ 二维码区域骨架占位**:`#boxqrimg` 给 min-height + 轻提示(如"二维码生成中…"),避免空白感 -**⑦ waline 评论懒加载**:滚动到评论区再初始化(若评论非首屏核心) -**⑧ 图片域加速(治本路线)**:R2 域名无法直接挂 EO,但可在 R2 域名前接 EO/大陆 CDN 回源 `r2.337845818.best`,把头像等图片链路从"CF 国际 ~1s"降到"EO 近大陆 ~0.2s"——与主站 EO 方案同理,属另一工程 - ---- - -## 五、建议落地批次 - -| 批次 | 内容 | 风险 | -|---|---|---| -| 批 1(P0) | ① + ② | 低:加缓存头仅影响新上传;库改动影响 3 个模板的二维码渲染,需测试 profile/list/not-found | -| 批 2(P1) | ③④⑤ | 低–中:③需先确认 og-image 语义 | -| 批 3(P2) | ⑥⑦⑧ | 视需要 | - -> 参考实测:①落地后二次访问头像 0ms;②落地后即使头像失败,二维码 0.2s 内出现可扫 → 用户体感"加载中"消失。 diff --git a/outputs/base-after-explore-light.png b/outputs/base-after-explore-light.png deleted file mode 100644 index b628fe1..0000000 Binary files a/outputs/base-after-explore-light.png and /dev/null differ diff --git a/outputs/base-after-home-dark.png b/outputs/base-after-home-dark.png deleted file mode 100644 index 4d54d1b..0000000 Binary files a/outputs/base-after-home-dark.png and /dev/null differ diff --git a/outputs/base-after-home-light.png b/outputs/base-after-home-light.png deleted file mode 100644 index ffca959..0000000 Binary files a/outputs/base-after-home-light.png and /dev/null differ diff --git a/outputs/base-before-explore-light.png b/outputs/base-before-explore-light.png deleted file mode 100644 index bc50b85..0000000 Binary files a/outputs/base-before-explore-light.png and /dev/null differ diff --git a/outputs/base-before-home-dark.png b/outputs/base-before-home-dark.png deleted file mode 100644 index 9720485..0000000 Binary files a/outputs/base-before-home-dark.png and /dev/null differ diff --git a/outputs/base-before-home-light.png b/outputs/base-before-home-light.png deleted file mode 100644 index 88fec6b..0000000 Binary files a/outputs/base-before-home-light.png and /dev/null differ diff --git a/outputs/cloudflare-origin-lockdown-guide.md b/outputs/cloudflare-origin-lockdown-guide.md deleted file mode 100644 index 2b31d4d..0000000 --- a/outputs/cloudflare-origin-lockdown-guide.md +++ /dev/null @@ -1,167 +0,0 @@ -# Cloudflare 锁定源站 实操指南(配合 P0 修复) - -> 目标:让 `trust_proxy = true` 安全生效,同时**不维护 Cloudflare 庞大的 CIDR 清单**。 -> 代码侧(`internal/shared/ip.go`)已改为:优先信任 `CF-Connecting-IP`,仅在 TCP 对端落在 -> `trusted_proxies`(你自己的基础设施)时才解析 `X-Forwarded-For`/`X-Real-IP`。 -> 关键前提:**源站必须只能从 Cloudflare 抵达**,这样攻击者无法直连并伪造 `CF-Connecting-IP`。 - ---- - -## 0. 为什么必须锁源站 - -上一轮的 P0 风险是:开启 `trust_proxy = true` 后,只要攻击者能**直连到源站 IP**, -他就可以在请求里塞一个 `CF-Connecting-IP: 他想封的任意 IP`,应用会信以为真。 -后果:自动封禁、限流、IP 黑名单全部按**攻击者指定的 IP** 计数 → 可把正常访客(甚至你自己的 IP)打进黑名单。 - -两种解法都不需要 Cloudflare CIDR 白名单,只是「让攻击者根本连不到源站 / 连到也过不了 TLS」: - -- **方案 A:Cloudflare Tunnel** —— 源站不开放任何公网入站端口,`cloudflared` 主动出站连 Cloudflare, - 外网流量只走 CF→隧道→本地。攻击者不知道、也连不到源站 IP。**最省事,推荐。** -- **方案 B:Authenticated Origin Pull(mTLS)** —— 源站要求 Cloudflare 回源时出示客户端证书, - 没证书的直连在 TLS 握手阶段就被拒,连 HTTP 头都发不出来。保留你现有公网 IP/端口时选它。 - ---- - -## 1. 方案 A:Cloudflare Tunnel(推荐) - -### 1.1 原理 -源站防火墙只放行运维端口(如 22),**不对外开 80/443**。Cloudflare 边缘与源站上的 -`cloudflared` 之间建立一条持久出站隧道。访客的请求在 CF 边缘进入,经隧道到达本地 -`cloudflared`,再转发给监听在 `127.0.0.1` 的 TamaBox。CF 在边缘做 TLS,所以回源用 HTTP 即可。 - -### 1.2 步骤 -1. **前提**:域名已在 Cloudflare(橙色云代理),源站能主动出网(TCP 443 出站)。 -2. **安装 cloudflared**(二选一): - - Debian/Ubuntu:`sudo apt install cloudflared`(或官方 deb:`curl -fsSL https://pkg.cloudflare.com/cloudflared.deb | sudo dpkg -i -`) - - Docker:`docker pull cloudflare/cloudflared:latest` -3. **登录授权**(浏览器选域):`cloudflared tunnel login` -4. **建隧道**:`cloudflared tunnel create tamabox` - 记下输出的 tunnel ID 与 credentials 文件路径(默认 `~/.cloudflared/.json`)。 -5. **写配置** `~/.cloudflared/config.yml`: - ```yaml - tunnel: <你的 tunnel id> - credentials-file: /root/.cloudflared/.json - ingress: - - hostname: yourdomain.com # 改成你的域名 - service: http://127.0.0.1:8080 # 改成 TamaBox 实际监听地址(见 1.4) - - service: http_status:404 - ``` -6. **绑定 DNS**(自动加 CNAME 到 Cloudflare):`cloudflared tunnel route dns tamabox yourdomain.com` -7. **常驻运行**: - - systemd:`sudo cloudflared service install` 然后 `sudo systemctl enable --now cloudflared` - - Docker:`docker run -d --name cf-tunnel --restart unless-stopped -v ~/.cloudflared:/root/.cloudflared cloudflare/cloudflared tunnel run` -8. **收紧源站防火墙**:云安全组 / `iptables` 把 **80、443 的 0.0.0.0/0 入站全部删掉**, - 只保留你的运维 IP 的 22。这样即便有人探测到源站 IP 也连不上。 - -### 1.3 app.ini 配置 glue -```ini -[security] -trust_proxy = true -trusted_proxies = 127.0.0.1 -``` -说明:`cloudflared` 连的是本地,`ResolveClientIP` 看到的 TCP 对端就是 `127.0.0.1`; -`CF-Connecting-IP` 由 Cloudflare 注入并经隧道透传,应用**优先采用它**,无需任何 CIDR。 - -### 1.4 TamaBox 监听地址建议 -`conf/app.ini` 的 `[server] port = 80`。务必让程序**只绑本地**: -- 若用 `tamabox.go` 里 `flamego.Classic()` 默认 `0.0.0.0`,请改成 `127.0.0.1:8080`(或你 tunnel 里写的端口), - 双保险——即使防火墙漏配也不会对外暴露。 -- 改完重启 TamaBox。 - ---- - -## 2. 方案 B:Authenticated Origin Pull(mTLS) - -### 2.1 原理 -Cloudflare 回源时出示一张由 Cloudflare 私有 CA 签发的**客户端证书**;源站配置「要求并校验客户端证书」。 - attackers 直接连源站 443 时,因为没有这张证书,TLS 握手直接失败,根本到不了 HTTP 层, - 自然无法发送 `CF-Connecting-IP`。**这步本身已足以挡住伪造,不需要 CIDR。** - -### 2.2 步骤 -1. **开启 AOP**:Cloudflare 控制台 → **SSL/TLS → Origin Server → Authenticated Origin Pulls** → 开启。 -2. **下载 Cloudflare Origin CA**(用于校验 CF 客户端证):同一页底部 - **Origin CA certificates → Create Certificate**(或下载根 CA 证书 `cloudflare_origin_ca.pem`), - 保存到源站 `/etc/ssl/cloudflare/cf-origin-ca.pem`,权限 `600`。 -3. **落地方式二选一**: - - **B1(nginx 在前,最常见)** —— nginx 终止 TLS 并要求客户端证书: - ```nginx - server { - listen 443 ssl; - server_name yourdomain.com; - - ssl_certificate /etc/ssl/yourdom/fullchain.pem; # 源站证书(可用 CF Origin CA 签发,或自有) - ssl_certificate_key /etc/ssl/yourdom/privkey.pem; - ssl_client_certificate /etc/ssl/cloudflare/cf-origin-ca.pem; # Cloudflare 根 CA - ssl_verify_client on; # 必须校验 Cloudflare 客户端证书 - ssl_verify_depth 2; - - location / { - proxy_pass http://127.0.0.1:8080; # TamaBox 本地端口 - proxy_set_header Host $host; - proxy_set_header CF-Connecting-IP $http_cf_connecting_ip; # 透传给应用(优先采用) - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } - } - ``` - `sudo nginx -t && sudo systemctl reload nginx` - - **B2(Go 直接对外 TLS,不用 nginx)** —— 让 TamaBox 监听 TLS 并强制客户端证书 - (需改启动代码,示意): - ```go - srv := &http.Server{ - Addr: ":443", - TLSConfig: &tls.Config{ - ClientAuth: tls.RequireAndVerifyClientCert, - ClientCAs: cfCACertPool, // 载入 cf-origin-ca.pem - }, - } - srv.ServeTLS(ln, "/etc/ssl/yourdom/fullchain.pem", "/etc/ssl/yourdom/privkey.pem") - ``` -4. **防火墙(可选加固)**:源站 443 只放行 Cloudflare 回源 IP。**但 AOP 的 mTLS 已挡住伪造, - CIDR 只是减少无效握手,可按需省略**——正好契合你「不想维护大 CIDR」的诉求。 - -### 2.3 app.ini 配置 glue -- 用 nginx(B1):`trust_proxy = true`,`trusted_proxies = 127.0.0.1` - (nginx 在本地,TCP 对端 `127.0.0.1`)。`CF-Connecting-IP` 经 nginx 透传被应用优先采用; - 即便没透传,XFF 由受信的 `127.0.0.1` 解析也能拿到真实 IP。 -- 用 Go 直连 TLS(B2):`trust_proxy = true`,`trusted_proxies` 可留空 - (TCP 对端就是 Cloudflare,`CF-Connecting-IP` 直接采用)。 - ---- - -## 3. 怎么选 -| 场景 | 选 | -|------|----| -| 单实例、怕配错、不想管证书/防火墙 | **方案 A(Tunnel)** | -| 已有 nginx、必须保留公网 IP、希望源站也强制 TLS | **方案 B(AOP)** | - ---- - -## 4. 验证(上线前务必做) -1. **直连源站公网 IP 应失败**: - - A:`curl -I https://<源站IP>` 应超时 / 连接拒绝。 - - B:`curl -I https://<源站IP>` 应 TLS 握手失败(`sslv3 alert handshake failure` 之类)。 -2. **经域名访问正常**:`curl -I https://yourdomain.com` 返回 200。 -3. **确认记录的是真实客户端 IP**:用不同网络访问,查看限流/统计/日志, - 不应再是 Cloudflare 边缘 IP。 -4. **伪造头测试(仅限你自己的 IP)**: - `curl -H "CF-Connecting-IP: 1.2.3.4" https://yourdomain.com` —— 应用应忽略该头,记录你的真实 IP。 -5. **自动封禁仍生效**:连续触发 404 阈值,确认被封的是真实客户端 IP,不是 CF 边缘。 - ---- - -## 5. 注意事项 -- `trust_proxy = true` 一旦开启,**源站就必须锁定**,否则比 `false` 更危险 - (应用会更信任客户端头)。代码注释已写明此约束。 -- Tunnel 下建议 TamaBox 监听 `127.0.0.1`(不要 `0.0.0.0`),双保险。 -- 隧道 credentials 与证书文件权限 `600`,**不要进 Git 仓库**。 -- 多实例 / 容器:把 `cloudflared` 跑在与 TamaBox 能直连的网络(同主机或同 docker 网络)。 -- 证书 / 隧道有有效期,纳入监控或自动续期(Cloudflare 证书默认较长,Tunnel 凭证基本免维护)。 - ---- - -## 6. 与代码的对应关系(便于回溯) -- `internal/shared/ip.go`:`ResolveClientIP` 优先 `CF-Connecting-IP`,仅 `isTrustedProxy(remote)` 才解析 XFF。 -- `internal/conf/static.go`:`Security.TrustedProxies`(`trusted_proxies`)新字段。 -- `internal/cmd/web.go`:`conf.Init()` 后调用 `shared.InitTrustedProxies()` 解析清单。 -- `conf/app.sample.ini`:`trust_proxy` / `trusted_proxies` 注释已更新。 diff --git a/outputs/cmp-bg-with.png b/outputs/cmp-bg-with.png deleted file mode 100644 index ef9eaf0..0000000 Binary files a/outputs/cmp-bg-with.png and /dev/null differ diff --git a/outputs/cmp-bg-without.png b/outputs/cmp-bg-without.png deleted file mode 100644 index 3298c0c..0000000 Binary files a/outputs/cmp-bg-without.png and /dev/null differ diff --git a/outputs/cmp-default.png b/outputs/cmp-default.png deleted file mode 100644 index 0f7c092..0000000 Binary files a/outputs/cmp-default.png and /dev/null differ diff --git a/outputs/cmp-new-with.png b/outputs/cmp-new-with.png deleted file mode 100644 index 633d9e3..0000000 Binary files a/outputs/cmp-new-with.png and /dev/null differ diff --git a/outputs/cmp-new-without.png b/outputs/cmp-new-without.png deleted file mode 100644 index 46e8b71..0000000 Binary files a/outputs/cmp-new-without.png and /dev/null differ diff --git a/outputs/cmp-sakura-mobile.png b/outputs/cmp-sakura-mobile.png deleted file mode 100644 index d7a431e..0000000 Binary files a/outputs/cmp-sakura-mobile.png and /dev/null differ diff --git a/outputs/cmp-sakura.png b/outputs/cmp-sakura.png deleted file mode 100644 index 0e04d9e..0000000 Binary files a/outputs/cmp-sakura.png and /dev/null differ diff --git a/outputs/code-review-2026-08-28.md b/outputs/code-review-2026-08-28.md deleted file mode 100644 index 5780f59..0000000 --- a/outputs/code-review-2026-08-28.md +++ /dev/null @@ -1,178 +0,0 @@ -# TamaBox-v4 整体代码审阅报告 - -- **审阅日期**:2026-08-28 -- **审阅范围**:全仓(87 个 Go 文件 + 38 个 HTML 模板 + 配置/脚本) -- **技术栈**:Flamego + GORM + PostgreSQL + Redis(1C1G 部署) -- **方法**:4 个并行只读探索代理(架构 / 安全 / 性能 / 前端·代码质量)+ 关键高危项人工复核 -- **说明**:本报告为纯审阅,**未改动任何代码**。按你一贯的分阶段工作流,请选定优先级后我分批落地。 - ---- - -## 一、执行摘要 - -整体健康度**良好**——安全工程成熟度高于同类项目:IDOR 越权、XSS 转义、SQL 注入三大高风险面均有系统化纵深防御;会话/CSRF/限流/SSRF/内容审核(fail-closed)均已落地。未发现会让线上立即崩溃或数据泄露的 P0 级事故。 - -主要短板集中在:**配置防呆不足**(Salt 未校验)、**部署样例与结构体错位**(PostgreSQL 直接连库失败)、**少量数据层查询未分页/缺索引**、**一个暴露面偏宽的内部反代**、以及**架构债**(上帝文件、缺 service 层)。这些均为可规划修复的中等问题,无紧急止血项。 - -| 维度 | 评分 | 一句话 | -|------|------|--------| -| 安全 | 🟢 良好 | 高风险面有纵深防御;残余项为中低危加固 | -| 架构 | 🟡 中等 | 分层清晰但存在上帝文件、缺 service 层、配置防呆不足 | -| 性能 | 🟢 良好 | 连接池/缓存/分页已调优;少量 N+1 与缺索引 | -| 代码质量 | 🟢 良好 | 日志统一、测试有部分覆盖;魔法字符串与构建依赖待理 | -| 前端/UX | 🟢 良好 | 模板写法规范、交互健全;少量体验增强空间 | - ---- - -## 二、问题清单(按优先级) - -### 🔴 P0 — 部署前必须修复(防呆 / 阻断) - -**P0-1 密码 Salt 未做非空校验(高危防呆)** -- 位置:`internal/db/password.go:25`、`password.go:55` 使用 `HmacSha1(password, conf.Server.Salt)`;`conf/conf.go` 仅校验 `external_url` 与 `xsrf_key`,**未校验 Salt**。 -- 影响:若部署沿用样例空盐(`app.sample.ini` 中 `salt = ""`),全站密码散列实际以空盐计算——相同密码产生相同哈希,且针对本站的离线字典/彩虹表攻击可行。 -- 建议:在 `conf.Init` 仿照 `xsrf_key` 增加 `if Server.Salt == "" { return error }`;并提供强随机默认值或启动即失败。 - -**P0-2 配置样例与结构体字段不一致(部署即失败)** -- 位置:`conf/app.sample.ini:36-42` 的 `[database]` 用 `address = "tcp(127.0.0.1:3306)"`;而结构体为 `type/host/port/name`(`internal/conf/static.go:58-63`),**缺 host/port/type**。 -- 影响:按样例部署时 `Database.Type` 为空 → 走 mysql 分支 → 生成非法 DSN `:@tcp(:0)/`,启动连库直接失败;PostgreSQL 用户也无法从样例获得正确字段。 -- 建议:样例补全 `type = postgres`、`host`、`port`、`name`、`user`、`password`、`ssl_mode`,使其与当前结构体一一对应。 - -### 🟠 P1 — 应当修复(重要) - -**P1-1 `/api/v1/service` 反代可转发至内部后端(中危)** -- 位置:`route/service/proxy.go:35-48` -- 影响:登录用户可控 `**` 路径转发到配置后端,`httputil.ReverseProxy` 透传原始 Cookie/Header;若后端前缀泄露或配置为内网地址,可成为横向探测入口;且 `TLSInsecureSkipVerify` 可关闭证书校验。 -- 建议:白名单限定可转发子路径与方法;禁止转发敏感 Header;内网后端强制校验;补审计日志。 - -**P1-2 导出接口 N+1 查询** -- 位置:`route/user/profile.go:381-401`(循环内 `FollowUps.GetByQuestionID`/`UploadImages.GetByQuestionID`,每题 2 次)、`profile.go:426-449`(循环 `Messages.GetByConversationID`,每会话 1 次)。 -- 影响:大账号导出慢、占用 DB 连接。 -- 建议:改为 `IN (?)` 批量拉取后内存分组(与已有的 `GetFirstMessagesByConversationIDs` 窗口函数做法一致)。 - -**P1-3 私信列表无分页(全量返回)** -- 位置:`internal/db/conversation.go:145-161` `ListByOwner`/`ListByAsker` 无 LIMIT;调用处 `profile.go:419-422`、`chat` 列表。 -- 影响:私信多的用户一次拉全部会话与消息,内存/DB 压力大。 -- 建议:加 `LIMIT/OFFSET` 或按 `created_at DESC` 游标分页。 - -**P1-4 高频查询缺复合索引** -- `questions.asker_user_id`:`CREATE INDEX idx_questions_asker ON questions(asker_user_id);`(`questions.go:243`) -- 箱主公开页:`CREATE INDEX idx_q_user_private_created ON questions(user_id, is_private, created_at);`(`questions.go:171-228`) -- 探索页:`CREATE INDEX idx_users_showinhome_created ON users(show_in_home, created_at);`(`users.go:487-540`) -- 投票热门:`CREATE INDEX idx_votes_status_vis_created ON votes(status, visibility, created_at);`(`votes.go:206-270`) -- 消息每日计数:`CREATE INDEX idx_messages_sender_created ON messages(sender_id, created_at);`(`users.go:805`) -- 会话消息排序:将单列 `conversation_id` 索引改为复合 `(conversation_id, created_at)`(`message.go:56`) - -**P1-5 changelog 仍走外部 fetch(引入外部依赖)** -- 位置:`templates/change-logs.html:10` 从外部域名取 `tamabox.changelogs.json`,而本地已生成并提交 `tamabox.changelogs.json` 未被使用。 -- 影响:跨域依赖、外部不可达时 changelog 空白、可被篡改。 -- 建议:将 json 纳入 `static` embed,新增同源路由 `/changelogs.json`(或放入 static 目录直接 fetch 同源路径),去掉外部依赖。 - -**P1-6 私密提问访问令牌熵值偏低(隐私/安全)** -- 位置:`internal/db/questions.go:93`(`Token: randstr.String(6)`) -- 影响:私密/未答提问与追问权仅靠 6 位随机串(约 62^6),结合 questionID 可枚举猜测,进而读取私密提问或冒用提问者身份发追问。 -- 建议:Token 提升至 ≥16 位随机串或直接用 UUIDv4。 - -**P1-7 架构债:上帝文件 + 缺 service 层(可维护性,分阶段重构)** -- 位置:`route/user/vote.go`(1289 行)、`route/vote/vote.go`(1036)、`route/question/share.go`(964)、`internal/db/users.go`(881)。 -- 影响:单文件同时承载 HTML 渲染、图片生成、DB 事务与业务判断,违反单一职责,难测试难维护;`route/*` 直接调 `db.*`,业务逻辑散布。 -- 建议:引入 `internal/service` 承载用例编排,handler 仅做请求解析/渲染;图片/分享卡生成抽到 `internal/render`。分阶段进行,不一次性大改。 - -### 🟡 P2 — 可优化(锦上添花 / 技术债) - -- **webp CGO 硬依赖**(已知约束):`internal/storage/s3.go:23` 与 `route/question/page.go:595` 直接 `import "github.com/chai2010/webp"`,链接阶段即需 libwebp,阻断 Windows/macOS 构建。你已在 Linux 解决,**不作为当前阻塞项**。建议用 `//go:build linux,cgo` 隔离为可选实现,未启用时走原图直传,消除跨平台构建门槛。 -- **魔法字符串**:提问过滤参数 `"all"/"public"/"private_question"` 散落 `route/user/question.go:58,121,171,174`、`form/question.go:17`,无编译期保护。建议抽 `QuestionFilter*` 常量(投票可见性已有先例 `internal/db/votes.go:43`)。 -- **会话 Cookie 未硬化**:`internal/route/route.go:92-95` 未设 `Secure/SameSite`。建议 `session.Options{Cookie:{Secure:true, SameSite:Lax}}`。 -- **PostgreSQL 无 TLS**:`internal/conf/db.go:16` 硬编码 `sslmode=disable`。建议加 `ssl_mode` 配置项,生产默认 `require`。 -- **限流为单进程内存**:`internal/route/ratelimit.go:17-100`,多实例可被分摊绕过;登录无失败锁定。建议改用 Redis 共享限流 + 失败阈值临时锁定。 -- **匿名身份服务端留存**:`internal/db/questions.go:64-72` 记录 `FromIP`;若承诺强匿名,应定期脱敏并明示隐私策略。 -- **邮件模板外链字体**:`templates/mail/*.html` 引用 `//fonts.loli.net`,离线/被墙时回退不可控。建议改用系统字体栈或自托管。 -- **遗留 .bak**:`templates/question/new-question-template.html.bak` 含硬编码中文,易与 i18n 正式版混淆。建议删除,由 git 管历史。 -- **无优雅退出**:`internal/cmd/web.go:82-94` 未注册 `signal.Notify`,重启会断连。建议加 SIGTERM + `server.Shutdown(ctx)`。 -- **路由包命名混乱**:顶层 `route` 与 `internal/route` 同名(无 import 循环,但易误读)。建议收口到 `internal/route/*`。 -- **首页统计缓存细节**:`internal/stats/stats.go:41-78` 进程内 30 分钟缓存合理,但过期瞬间并发重算、写后未 `Invalidate()`。建议 TTL 加抖动 + 写后失效。 -- **GetHotTags 全表 Pluck**:`internal/db/users.go:661-667`(已 60s 缓存)。大数据量建议 SQL 内聚合或限样本。 -- **搜索无分页**:`internal/db/questions.go:374` `SearchByUserID` 返回全部 LIKE 匹配。建议加 `LIMIT`。 -- **CSRF 对 `/api/v1/pixel/` 关闭**:`internal/context/context.go:170`。影响有限,建议仅限 GET 或补回校验。 -- **handler 级测试薄弱**:`route/question`、`route/user` 多依赖真实 DB/Redis,缺单测。建议补基于 `dbutil/testing.go` 的集成测试或 httptest mock。 - ---- - -## 三、已确认的良好实践(正面,保持) - -- **IDOR 防护到位**:提问删除/私密/回答经 `canDelete = 登录且为箱主`(`route/question/question.go:69`)及 `question.UserID==pageUser.ID` 双重校验;投票经 `getOwnedVote`(`route/user/vote.go:392`);私聊经 `OwnerID/AskerID`(`chat_handler.go:52`)。 -- **XSS 转义到位**:`QuestionFormat/AnswerFormat` 走 `html.EscapeString`(`internal/template/template.go:46-55`);Markdown 经白名单 sanitize;`toJSON` 依赖 `json.Marshal` 默认转义。 -- **SQL 注入为零**:全量 GORM 参数化;Raw 均用 `?` 占位(`message.go:91` 的 `IN ?`)。 -- **SSRF 防护**:`/proxy-image` 含域名白名单 + 私网 IP 拦截 + DNS rebinding 防护(`route/proxy.go:24-127`)。 -- **其他**:注册/登录/提问强制验证码;内容审核 fail-closed;登录会话再生防固定(`route/auth/login.go:66`);开放跳转前缀校验防开放重定向。 -- **性能基础好**:模板启动解析缓存(`templates/embed.go`)、Gzip 全量接入(`internal/cmd/web.go:84`)、连接池按 1C1G 调优(MaxOpen=10)、批量反 N+1 已落地。 -- **Go 模板无运算符误用**:全仓 grep `!=`/`==`/`&&`/`||` 零命中,无启动 panic 风险;删除交互均有二次确认。 - ---- - -## 四、新功能与 UX 提升建议 - -> 均考虑 1C1G 约束(不引入常驻重服务);按"用户价值 / 实现成本"简评。 - -| # | 建议 | 用户价值 | 成本 | 说明 | -|---|------|---------|------|------| -| F1 | **通知中心 + 邮件摘要聚合** | 高 | 中 | 提问箱核心体验是"收到提问"。站内通知列表 + 邮件可设每日/每周摘要开关,避免逐条邮件轰炸;与现有邮件体系复用。 | -| F2 | **箱主治理面板** | 高 | 中 | 自定义提问箱简介/主题、开关"允许匿名/图片/投票"、屏蔽关键词、隐藏不当内容。提升箱主掌控感。 | -| F3 | **前台举报入口** | 高 | 低-中 | 后台已有 fail-closed 审核,但面向用户的"举报不当提问/回答"按钮缺失;补前台入口 + 箱主一键隐藏。 | -| F4 | **私密提问体验增强** | 中 | 中 | 追问流、已读状态、链接过期机制(配合 P1-6 令牌熵提升),让匿名双向对话更顺畅。 | -| F5 | **探索/发现页增强** | 中 | 中 | 标签筛选、热门排序、按话题聚合(`GetHotTags` 已有数据),提升内容分发与留存。 | -| F6 | **防刷与内容质量** | 中 | 中 | per-box 提问限流、垃圾链接/敏感词前端预检、重复内容检测(配合 P2 限流改造)。 | -| F7 | **数据导出优化** | 中 | 低 | 支持 Markdown/PDF 导出,导出走批量查询(顺带修 P1-2 N+1),导出包隐私脱敏。 | -| F8 | **移动端 PWA + 暗色模式** | 中 | 中 | 纯静态资源即可,1C1G 友好;显著提升移动端阅读与夜间体验。 | -| F9 | **草稿与定时发布** | 低-中 | 中 | 私密回答存草稿、公开回答定时发布,降低发布压力。 | -| F10 | **无障碍 a11y** | 低 | 低 | 键盘可达、aria 标签、对比度,提升专业度与合规。 | - -**优先推荐**:F1(通知中心)+ F2(箱主治理)+ F3(前台举报)组合——直接提升"收提问→治理→互动"的主链路体验,且不引入重型依赖。 - ---- - -## 五、建议的分阶段实施路径 - -1. **阶段 0(本次部署前)**:P0-1、P0-2(防呆 + 样例对齐),半小时内可合。 -2. **阶段 1(安全/数据层)**:P1-1 反代收敛、P1-6 令牌熵、P1-2/P1-3 分页与 N+1、P1-4 建索引、P1-5 changelog 同源。 -3. **阶段 2(架构债)**:P1-7 引入 service 层,按模块分批抽离上帝文件(先 `route/question/share.go` 的图片/分享卡)。 -4. **阶段 3(体验增强)**:P2 各项 + 从 F1/F2/F3 启动新功能。 - ---- - -## 附:探索索引 -- 架构:`tamabox.go` → `cmd.Web` → `conf.Init` → `db.Init` → `route.New()` -- 安全:IDOR/ XSS / SQLi / SSRF / 限流 / 会话 已逐项核验 -- 性能:连接池、缓存、分页、索引、changelog、统计 -- 前端:模板转义、partial、交互确认、webp 构建依赖、魔法字符串 - ---- - -## 六、实施进展 - -### P0 已修复(2026-08-28) -- **P0-1 Salt 校验**:`internal/conf/conf.go` 的 `Init` 在 `server` 段 MapTo 后新增 `if Server.Salt == "" { return error }`,空盐启动即失败,防止密码以空盐哈希(离线碰撞风险)。 -- **P0-2 配置样例对齐**:`conf/app.sample.ini` 的 `[database]` 段按 `Database` 结构体字段对齐(`type`/`host`/`port`/`name`/`schema`),去掉旧的不存在字段 `address`,默认 `type=postgres`、`port=5432`、`schema=public`。 -- **验证**:`go build ./internal/conf/...` 通过;临时测试(已清理)确认 ① 样例 database 段 MapTo 出 `type=postgres`/`port=5432`/`schema=public`;② 空 salt 时 `conf.Init` 返回 salt 错误;③ 非空 salt 时正常加载。 -- **重要陷阱**:`conf.Init` 使用 `IgnoreInlineComment: true`,行内 `;` 注释会被并入配置值——初次加的行内注释曾污染 `database.type` 与 `server.salt`(并使 Salt 校验失效),已全部改回整行注释。原 sample 大量行内注释存在同类系统性污染,建议后续统一改为整行注释(见 MEMORY.md 配置解析约定)。 - -### P1 已修复(2026-08-28) -- **P1-1 服务反代加固**:`route/service/proxy.go` 剥离转发的敏感 Header(Cookie / Authorization / X-* / Proxy-* / Host 等),并在转发前补审计日志(操作者、目标、路径、大小);`internal/route/route.go` 将 `/service/{**}` 路由由 `f.Any`(任意方法)收敛为 `f.Get`(仅 GET),消除写方法经反代的安全面。`go build ./route/service/...` 通过。 -- **P1-2 导出接口 N+1 批量化**:`internal/db/follow_ups.go`、`upload_images.go`、`message.go` 新增 `GetByQuestionIDs` / `GetAllByConversationIDs` 批量方法(返回 `map[uint][]*T` 分组);`route/user/profile.go` 的导出逻辑由"循环单查"改为"IN 查询 + 内存分组",追问题/图片由每题 2 次、消息由每会话 1 次降为 3 次批量查询。`go build ./internal/db/...` 通过。 -- **P1-3 私信列表分页**:`internal/db/conversation.go` 的 `ListByOwner`/`ListByAsker` 增加 `limit, offset` 参数(导出仍传 `0,0` 取全量,行为不变),并新增 `CountByOwner`/`CountByAsker`;`route/user/question.go` 的「我收到的 / 我发起的」私聊列表接入游标分页(每页 50,复用 `buildQListPageURL` 保留筛选参数),模板 `templates/user/question-list.html` 增加真实总数头与「加载更多」链接。模板解析/渲染测试全部通过。**注意**:`route/user` 在 Windows 因 `github.com/chai2010/webp` 的 CGO 硬依赖无法编译(已知约束,最终在 Linux 完整构建);逻辑已逐处类型核对无误。 -- **P1-4 高频查询补复合索引**:`internal/db/db.go` 在 `AutoMigrate` 之后调用 `ensureFrequentlyUsedIndexes`(仅 PostgreSQL),以 6 条 `CREATE INDEX IF NOT EXISTS` 幂等建索引:`idx_questions_asker_user_id`、`idx_questions_user_private_created`、`idx_users_show_in_home_created`、`idx_votes_status_visibility_created`、`idx_messages_sender_created`、`idx_messages_conversation_created`。不影响共享 Model,适合既有库。`go build ./internal/db/...` 通过。 -- **P1-5 changelog 同源化**:`dev/gen_changelog.py` 输出路径由根目录 `tamabox.changelogs.json` 改为 `static/tamabox.changelogs.json`;`static/fs.go` 的 `//go:embed` 纳入该文件;`templates/change-logs.html` 的 fetch 由外部域名 `api-direct.337845818.best/...` 改为同源 `/static/tamabox.changelogs.json`,并删除根目录旧副本。`go build ./static/...` + 模板解析测试通过。 -- **验证总览**:可本地构建的包(`internal/conf`、`internal/db`、`static`、`templates`)全部 `go build`/`go test` 通过;`gofmt -l` 对全部改动 Go 文件零输出(已 `gofmt -w` 归一化);`route/service` 与 `route/user` 因 webp CGO 在 Windows 不可编,依项目约定留待 Linux/CI 完整构建。P1-6(令牌熵)、P1-7(service 层)未在本轮处理,列为后续阶段。 - -### 回归核查(2026-08-28 16:2x) -逐项复核 P0 + P1-1~P1-5 是否复现或引发新问题。**结论:7 处修复均完好、无复现、无回归。** -- 验证手段:逐文件 grep 关键标记 + 读 proxy.go/route.go/question.go/profile.go/db.go 改动段 + `gofmt -l`/`go build`/`go vet ./internal/db`/模板解析渲染测试 + 核对 i18n 三语言键(zh_cn/ja/en)。 -- 构建结果:可本地编译包 `build` 退出码 0、`go vet` 0、模板测试 ok、`gofmt -l` 对 11 个改动文件零输出。 -- 各点确认: - - P0-1 Salt 非空校验在 conf.go:57;P0-2 样例 `[database]` 字段对齐且**无行内注释污染**(IgnoreInlineComment 陷阱未复发)。 - - P1-1 反代剥离敏感头(proxy.go:66-80)+ 审计日志(92-99)+ route.go:292 收敛 `f.Get`。 - - P1-2 三个 store 批量方法签名与实现齐全,profile.go 导出改用批量/分组。 - - P1-3 私信分页:`ChatStats.received/initiated` 来自 `CountByOwner/CountByAsker` **真实总数**(非分页切片长度);模板「加载更多」键 `qlist.loadMore` 三语言齐全;`strconv.FormatUint(uint64(...))` 类型错误已修。 - - P1-4 6 条 `CREATE INDEX IF NOT EXISTS` 幂等、internal/db 编译通过。 - - P1-5 静态前缀 `/static`(route.go:99)+ embed 含 json + 模板同源 fetch;根副本已删、无残留引用。 -- 一处措辞纠正(非缺陷):P1-4 `ensureFrequentlyUsedIndexes` 实际**未做 dialect 门控**(无条件执行),但所用 DDL 为 PG 与 MySQL 8.0+ 均支持的 `CREATE INDEX IF NOT EXISTS` 可移植语法,对当前 PG 部署无影响;若将来接 MySQL 仍可用。 diff --git a/outputs/code-review-2026-08-31.md b/outputs/code-review-2026-08-31.md deleted file mode 100644 index d4b74f7..0000000 --- a/outputs/code-review-2026-08-31.md +++ /dev/null @@ -1,132 +0,0 @@ -# TamaBox-v4 整体功能与逻辑审阅报告 - -- **审阅日期**:2026-08-31 -- **范围**:全仓库(route / internal / templates / conf) -- **方法**:静态走查 + 编译验证(`go build` / `go vet` / 单元测试)+ 对每项高危结论做源码核对(trust but verify) -- **结论概览**:核心架构扎实(路由/中间件/会话固定/回收令牌/配额原子递增/功能开关 Evaluate 逻辑均正确)。发现 **1 个 P0 级安全缺陷(IP 伪造绕过)、若干 P1 逻辑/安全/可用性问题、一批 P2 健壮性项**。无导致编译崩溃的致命代码错误。 - ---- - -## 0. 测试与构建验证结果 - -| 项目 | 结果 | -|---|---| -| `go version` | go1.25.5 windows/amd64 | -| `go build ./...` | ❌ 仅因 `internal/storage/s3.go` 依赖 `github.com/chai2010/webp`(CGO 缺 libwebp 本机库,沙箱限制,与既有约定一致)。**其余包均可独立编译** | -| 核心包编译 | ✅ `internal/form internal/db internal/cache internal/i18n internal/captcha internal/security/* internal/context internal/conf internal/dbutil` 全部 OK | -| `go vet`(上述包) | ✅ 无告警 | -| 单元测试 | ✅ `internal/form` `internal/i18n` `internal/cache` `internal/security/autoban` 全部通过 | -| DB 集成测试 | ⚠️ 需外部 PostgreSQL/MySQL(`DB_TYPE` 环境变量,沙箱无),未执行;`feature_control_test` 等需落库测试建议在有 DB 环境补跑 | - -> 说明:`gofmt -l` 发现 11 个文件未格式化(如 `internal/security/censor/text.go`、`internal/captcha/math.go`、`route/user/harassment.go` 等),建议 `gofmt -w` 统一。 - ---- - -## 🔴 P0 — 安全/可用阻断级 - -### P0-1 反向代理 IP 信任可被伪造,限流/黑名单/自动封禁全线失效 -- **位置**:`internal/shared/ip.go:42-69`(`ResolveClientIP`)、被 `internal/route/ratelimit.go`、`autoban.go`、`ipblacklist.go` 共用 -- **问题**:当 `conf.Security.TrustProxy=true` 时,直接信任客户端发来的 `CF-Connecting-IP` / `X-Real-IP` / `X-Forwarded-For`,**不校验 `RemoteAddr` 是否属于可信代理 CIDR**。任何人都可伪造这些头。 -- **影响**: - 1. 限速、IP 黑名单被完全绕过(每次请求换一个伪造 IP); - 2. 自动封禁(fail2ban)被反射利用——攻击者用伪造头把**任意受害者 IP** 打进黑名单,造成误封(含 CDN 出口 IP,可能误伤全站访客)。 -- **现实性**:`conf/app.sample.ini` 明确建议"站点在 Cloudflare 后且需自动封禁时必须设 `trust_proxy = true`"——这正是绝大多数生产部署,故该缺陷在生产可达。 -- **修复**:配置 `trusted_proxies` CIDR 列表;仅当 `RemoteAddr` 命中可信网段才读取代理头;`X-Forwarded-For` 自右向左剥离已确认的可信跳数后再取最左原始 IP。 - ---- - -## 🟠 P1 — 逻辑错误 / 漏洞 / 可用性 - -### P1-1 会话 Cookie 缺 `Secure` / `SameSite` -- **位置**:`internal/route/route.go:92-95`(`session.Options` 仅设 `Initer/Config`,无 `Cookie` 字段) -- **影响**:生产经 TLS 反代时,会话 Cookie 不会标 `Secure`(明文链路可泄露);未显式 `SameSite`。`conf.App.Production` 只切 Flamego 环境,不联动 Cookie 标志。 -- **修复**:按 `conf.App.ExternalURL` 是否 `https` 设 `Secure:true`;`SameSite:http.SameSiteLaxMode`(与既有 CSRF Token 协同);`HttpOnly:true`。 - -### P1-2 配置解析:行内 `;` 注释污染值,最坏导致启动崩溃 -- **位置**:`internal/conf/conf.go:25-27`(`IgnoreInlineComment:true`)+ `conf/app.sample.ini:5,11,12,16-28,54` -- **问题**:该选项下 `key = val ; 注释` 的注释会被并入字段值。示例里 `trust_proxy = false ; 是否信任…`、`ban_threshold_sensitive = 10 ; …` 等带行内注释。若把 sample 直接当 `app.ini` 用,`File.Section("security").MapTo(&Security)` 解析 bool/int 会因尾部中文注释失败 → **进程启动即崩溃**;`share_background` 这类字符串字段则会得到带注释的非法 URL。 -- **修复**:将 `IgnoreInlineComment` 改为 `false`(安全默认,`;` 重新开始注释),或全量清除 sample 中的行内注释(当前 sample 与其自身约定"禁用行内注释"矛盾)。 - -### P1-3 本地算数验证码可被脚本自动解出 + 端点无节流 -- **位置**:`internal/captcha/math.go:195-212`(`GenerateHandler` 无限流)、`:148-159`(`VerifyOnly` 不消费、不计数)、`:49-55`(忽略 `rand.Int` 错误) -- **问题**:验证码题目由服务端返回给客户端(如 `"23 × 7 ="`),答案对客户端完全可见,**任何脚本都能读题即算**——对机器人无实质阻挡;`/api/captcha/verify` 无节流且 `VerifyOnly` 不消费,可被无限轮询;`GenerateHandler` 无限流会导致会话条目无界膨胀(DoS)。 -- **影响**:匿名投票/提问的"唯一门槛"被自动化绕过;叠加 **P0-1** 的 IP 伪造,匿名刷票/刷提问几乎无成本。 -- **修复**:匿名提交改用语服务端校验的 Turnstile/recaptcha;或补 `/api/captcha/*` 限流 + 同 id 尝试 3 次即销毁;对 math 答案做常量时间比较。 - -### P1-4 追问"等主人回复"仅客户端校验,服务端可绕过 -- **位置**:`route/question/follow_up.go:31-63`(`PublishFollowUp`) -- **问题**:仅检查 `question.Answer != ""` 与 `count < 3`,**不校验上一条追问是否已被回答**(`waitingFollowUpReply` 只在模板隐藏表单,POST 不强制)。攻击者可连发满 3 条追问而不等主人回任一条。 -- **修复**:取该问题最新一条 follow-up,若 `Answer == ""` 直接拒绝(并复用 `CreateIfBelowLimit` 原子兜底)。 - -### P1-5 内容审核"可信域名剥离"被绕过,钓鱼链接漏审 -- **位置**:`internal/security/censor/text.go:89-96`(`RemoveTrustedURL`) -- **问题**:朴素 `strings.ReplaceAll("https://github.com", "")`。`https://github.com.evil.tld/phish` 会被剥成 `.evil.tld/phish`,恶意域名逃过 URL 审核。 -- **修复**:解析 URL 后按"精确 host 或受信 host 的子域"精确匹配,再决定是否跳过审核。 - -### P1-6 密码校验:遗留 HMAC 比较非常量时间 + bcrypt 失败静默降级 -- **位置**:`internal/db/password.go:56`(`legacyHash == storedHash`)、`:24`(`GenerateFromPassword` 出错回退弱 HMAC-SHA1) -- **问题**:遗留账户走 `==` 比较存在时序侧信道;bcrypt 出错时不返回错误,反而存一个弱 HMAC-SHA1 哈希(下次登录按 legacy 处理), silently 削弱了密码存储强度。 -- **修复**:遗留分支改用 `subtle.ConstantTimeCompare`;bcrypt 失败直接 `return err`,不回退。建议 bcrypt cost 提到 12。 - -### P1-7 删除内容不回退功能配额计数 -- **位置**:`internal/db/questions.go`(删除)、`internal/db/conversation.go:204`(`MarkDeletedByOwner`) -- **问题**:达定量上限后,即便删除该提问/私聊,对应 `feature_controls.quota_count` 不 `-1`,功能仍显示 `quota_reached` 关闭,须箱主手动"重置计数"。 -- **修复**:删除时按功能类型调用 `FeatureControls.IncrementQuota(-1)`(注意软删除与并发)。 - -### P1-8 自动封禁把合法 302 误判为探测 -- **位置**:`internal/route/autoban.go:51-53` -- **问题**:把"302 → /"一律当扫描信号;但 `/i18n/{lang}` 切换、logout、黑名单跳转、投票不存在等都是 302→/。60s 内 20 次即 403 封 15 分钟,易误伤正常用户。 -- **修复**:仅在 `f.NotFound`(真正的 404)路径内计数,排除业务性 302。 - -### P1-9 服务反代路径穿越 + 丢失 query + 无超时 -- **位置**:`route/service/proxy.go:37,59` -- **问题**:`forwardPath` 未经 `path.Clean`,`/service//../../x` 可越过白名单前缀(白名单本身有效,无任意 SSRF/元数据风险);丢失 `RawQuery`;每请求新建 `Transport` 且无超时。 -- **修复**:`Clean` 后再次校验仍以 `forwardURL.Path` 为前缀;透传 query;复用带超时的 `Transport`。 - -### P1-10 `/api/v1/pixel/**` 豁免 CSRF + 放行写方法 + 无节流 + 响应头透传 -- **位置**:`internal/context/context.go:170`(CSRF 排除)、`internal/route/route.go:296`(`f.Any` + `reqUserSignIn`) -- **问题**:该前缀跳过 CSRF 校验且 `f.Any` 放行 POST/PUT/DELETE;`path.Join("/api/", uri)` 可经 `..` 跳出 `/api`;上游 `Set-Cookie` 等响应头原样回传;无速率限制。 -- **修复**:强制 CSRF 或仅放行 GET;拒绝含 `..` 的路径;白名单放行响应头;补限流。 - ---- - -## 🟡 P2 — 健壮性与改进(建议项) - -| # | 位置 | 问题 | 建议 | -|---|---|---|---| -| P2-1 | `route/vote/vote.go:310` | 排序题统计 `json.Unmarshal(&data.Ranking,…)` 应为 `&data`,解析必失败且错误被忽略 | 改为 `&data` 并补单测验证 | -| P2-2 | `route/vote/vote.go:48,343,776` | 结果缓存共享指针,且仅新提交失效(删提交/改选项不失效),多实例不一致 | 失效键扩展;或缓存按 vote 维度加版本号 | -| P2-3 | `internal/db/vote_submissions.go:51-55` | 唯一索引不含 `deleted_at`:软删提交后统计归零却无法重投 | 唯一约束纳入 `deleted_at` 或改用业务去重 | -| P2-4 | `internal/db/questions.go` | `Question.Token` 仅 6 位且无唯一索引约束说明,碰撞可越权看他人私密/未答提问 | 确认是否 `uniqueIndex`;不足则加长 | -| P2-5 | `route/question/question.go` | 普通 `Delete` 不级联 `follow_ups`/`UploadImages`,留孤儿记录 | 软删时同步标记子表 | -| P2-6 | `internal/db/users.go` | `CountTodayMessagesFromUser` 用 `CURRENT_DATE`(DB 时区),与 App 本地时区不符,每日私信上限窗口偏移 | 用 App 本地时区日期边界 | -| P2-7 | `route/user/profile.go:337` | 导出 XLSX/CSV 时用户串直写单元格,含 `= + - @` 开头可在 Excel 执行公式(公式注入) | 单元格前加 `\t`/单引号或置文本格式 | -| P2-8 | `internal/db/users.go:454` | 注册重复 email/domain 校验存在 TOCTOU,并发同值双过 `validate` | 包事务或友好去重(唯一索引兜底) | -| P2-9 | `internal/route/gzip.go:75-80` | `ct` 求值后未用 → 未显式设 `Content-Type` 的响应永不压缩;204/304 仍加 `Content-Encoding` | 显式压缩判断 + 跳过无实体状态码 | -| P2-10 | `internal/route/route.go:233` | `/i18n/{lang}` 仅拦 `//`,未拦 `/\` 反斜杠等价变体 | 归一化 `to` 后再校验 | -| P2-11 | `internal/route/route.go` | 缺 HSTS;`lang` cookie 无 `Secure` | 加 `Strict-Transport-Security`;cookie `Secure` | -| P2-12 | `internal/route/chat/chat_handler.go:122` | `SendMessage` 未重新校验 `FeatureChat.Open`(创建时查过,发消息漏查) | 发送前 `FeatureStateOf(FeatureChat).Open` 校验,或明确设计为既有会话豁免 | -| P2-13 | `internal/db/questions.go` | 提问列表 `PinFirst` 与 `id 注:outputs/ 下已有 2026-08-26/08-28 的历史审阅文档,本次为独立复检,新增/重申了 IP 伪造、审核 fail-closed、追问绕过、审核域名绕过、配置崩溃等项;其中 chat 消息计数竞态经核实**不存在**,已在历史结论中纠正。 diff --git a/outputs/code-review-2026-09-02-late-batch.md b/outputs/code-review-2026-09-02-late-batch.md deleted file mode 100644 index b4e70e2..0000000 --- a/outputs/code-review-2026-09-02-late-batch.md +++ /dev/null @@ -1,138 +0,0 @@ -# 代码审阅报告 —— 9-01 深夜加固批次(凌晨 02:11–02:23) - -- 审阅范围:`internal/shared/ip.go`、`internal/route/ratelimit.go`、`internal/route/autoban.go`、`internal/template/markdown.go`、`internal/form/form.go`,及 3 个测试(`ip_test.go`、`markdown_test.go`、`csrf_binding_test.go`) -- 审阅方式:逐文件源码走查 + 交叉核对调用方契约(route.go 挂载点、login.go uid 键、ipblacklist 精确匹配、模板 FuncMap)+ **第三方库源码级语义核验**(flamego@v1.9.4 `run()`、flamego/csrf@v1.3.0 `Validate`、net/http 表单惰性解析) -- 审阅日期:2026-09-02 -- 结论:**单个文件实现均正确、自洽,且有测试覆盖;但 form.go 的「请求体 12MiB 上限」因中间件执行顺序在真实链路上失效,属上一轮安全评审 P1-3「磁盘耗尽」未真正闭环;另有一处反序列化失败不中断业务链的通用缺陷。** 建议修复后再上线(本次仅审阅,未改码)。 - ---- - -## 一、🔴 P1-1 请求体 12MiB 上限实际失效:超大 multipart 仍可无节流落盘(磁盘耗尽 DoS 未闭环) - -**位置**:`internal/form/form.go:59-79`(上限安装点)、`internal/context/context.go:180-182`(CSRF 校验点)、`internal/route/route.go:372-388`(中间件注册顺序) - -### 问题 -`maxRequestBodySize` 的 `http.MaxBytesReader` 被安装在 **form.Bind 内(路由级 handler)**,但真实链路上 CSRF 校验(`Contexter` → `x.Validate(ctx)`)是 **组级中间件、先于一切路由级 handler 执行**: - -``` -安全头 → IP黑名单 → autoban → cache/recaptcha/session → csrf.Csrfer → templater - → Contexter(★ 此处对每个 POST 执行 csrf.Validate) - → form.Bind(★ 12MiB 上限到这一步才装上)→ rateLimit → 业务动作 -``` - -而 flamego/csrf 的 `Validate` 取值顺序是**先读 Header,无 Header 时 `FormValue("_csrf")`**——后者会触发 net/http 的**整包惰性解析**: - -- multipart:`ParseMultipartForm(默认 32MiB 内存)`,超出部分**无上限落盘临时文件**; -- 解析**发生在 token 校验之前**:即使不带/带错 CSRF token(随后被 400 拒绝),超大 body 也已被完整读入并落盘; -- 该 400 由 `Contexter` 直接写出 → **响应已写、链路中断** → 路由级的 `rateLimitMiddleware` / autoban 计数**根本没机会执行**。 - -即:对任意 POST 路由(公开提问框 `/_/{domain}`、`/vote/{token}/submit`、`/login` 等),攻击者**无需有效 CSRF token、不受限流约束**,持续 POST >32MiB 的 multipart body 即可无限写临时目录,1C1G 机器可在数分钟内被打满磁盘。form.go 注释中想防御的场景(匿名可达接口的磁盘耗尽)依然存在。 - -> 补充:urlencoded 大 body 同理在 CSRF 阶段整体载入内存(无上限、无限流),为内存压力向量。 - -### 修复建议(单点修复,可同时覆盖下文 P2-2) -在**全局 `f.Use` 层**(`route.go:146` autobanMiddleware 之后即可)安装统一请求体上限,早于一切组级中间件与 body 解析: - -```go -// internal/route/(建议独立文件 bodylimit.go) -const maxRequestBodySize = 12 << 20 // 与 form.go 语义一致;如需多图余量可统一上调 - -func requestBodyLimitMiddleware() flamego.Handler { - return func(ctx flamego.Context) { - req := ctx.Request().Request - req.Body = http.MaxBytesReader(ctx.ResponseWriter(), req.Body, maxRequestBodySize) - } -} -// route.go -f.Use(autobanMiddleware(ab)) -f.Use(requestBodyLimitMiddleware()) // 必须在 Contexter 之前 -``` - -- **取值论证**:合法单请求最大 body = 单张图片 ≤8MiB(`storage.MaxUploadBytes`,`internal/storage/s3.go:36`,且提问/回答只取 `f.Images[0]`)+ 表单字段/边界开销,远小于 12MiB;头像/背景 2MiB、私聊图 5MiB 均安全。12MiB 不会误伤合法提交。 -- 该中间件同时封住:form.Bind 路由(含 HTML 表单路径)、chat `/send`、vote `/submit`,以及 CSRF 自身触发的解析。form.go 内层 wrap 可保留(幂等,误差同源)或删除。 -- **回归测试建议**:仿 `route/csrf_binding_test.go` 增加路由级测试:向任一 POST 路由发送 Content-Length 超过上限的 multipart body,断言返回 4xx 且不落入业务(可借 `f.Post("/protected", ...)` 桩验证 body 未被读)。 - ---- - -## 二、🟠 P2-1 form.Bind 反序列化失败不阻断业务链:零值结构体进入业务动作 - -**位置**:`internal/form/form.go:70-78`(失败分支仅 `c.Map(Error{...})` 后 `return`) - -### 问题 -flamego@v1.9.4 `run()`(`context.go:190-228`)的链语义是:**handler 返回后只要 `ResponseWriter().Written() == false`,就继续执行下一 handler**(没有「不调用 Next 即停链」的语义)。因此 Bind 解析失败分支只 `Map(Error)`、**不 `SetError` 也不写响应** → 后续业务动作**以零值结构体照常执行**,`ctx.HasError()` 恒为 false,govalid 校验被整体跳过。 - -**触发面**:body 在 Bind 阶段解析失败(>12MiB、畸形编码)。HTML 表单提交的 body 在 CSRF 阶段已解析成功(Bind 幂等重解析不会失败),**实际只影响 Header 携带 CSRF token 的 JS/fetch 提交**(token 在头里时 CSRF 不解析 body,解析首次发生在 Bind)。匿名路由攻击者可自持「uid=0」的匿名 token 构造该请求。 - -**各动作影响(按危害排序)**: - -| 动作 | 零值后果 | 二次防线 | -|---|---|---| -| `user.UpdateProfile`(`route/user/profile.go:66`) | `db.Users.Update` 用 **map 全字段无条件写**(`internal/db/users.go:336-357`)→ 昵称/简介/配色/社交链接被清空、`Notify` 置 `None`(**静默退订邮件通知**);无任何错误提示 | 无验证码;仅限账户本人/持其 CSRF 的请求(自伤型数据丢失) | -| `user.UpdatePagePassword` / `UpdateAskPassword`(`route/user/password.go:17,32`) | 这两个 handler **连 `HasError` 都不查**,直接 `f.Password`(空=清除密码)→ 反序列化失败等于「关闭页面/提问密码」,**防护被静默解除** | 仅本人(自伤型) | -| 匿名 `question.New` / `PublishFollowUp`(`route/question/page.go:315`) | 零值内容绕过 govalid 与屏蔽词/长度校验进入业务;`New` 有验证码闸门兜底,`PublishFollowUp` **无验证码且该路由无限流** | 验证码开启时安全;站点关闭/未配验证码时可产生空内容垃圾数据 | -| `auth.Register/Login/Forgot/Recover` | 零值走 DB 校验失败路径 | 验证码 + DB 唯一性 | - -### 修复建议(与既有 govalid 分支同构,改动最小) -`internal/form/form.go:71,76` 两处失败分支,在 `c.Map(...)` 前补 `c.SetError(err)`(或本地化包装,如「请求内容不合法,请重试」)。`SetError` 会置 `HasError=true` 并写入 `Data["Error"]`(`internal/context/context.go:71-86`),所有动作的 `if ctx.HasError()` 分支随即生效(重渲染当前页并提示),与校验失败体验一致。 - -```go -if err := r.ParseForm(); err != nil { - c.SetError(err) - c.Map(Error{Category: ErrorCategoryDeserialization, Error: err}) - return -} -``` - -> 注意:`UpdatePagePassword`/`UpdateAskPassword` 本身不查 `HasError`(空密码=关防护是合法操作),若采用 SetError 方案需给这两个 handler 补一个 `if ctx.HasError() { ctx.Redirect(...); return }` 守卫,否则 SetError 后仍会往下写空密码。 - -### 回归测试建议 -给 `internal/form`(或路由级)补用例:模拟 >12MiB 的 multipart body 经 Bind 后,断言 `HasError()==true`;若约束链上动作,断言动作未执行。 - ---- - -## 三、🟠 P2-2 chat `/send` 与 vote `/submit` 无任何请求体上限 - -**位置**:`internal/route/chat/chat_handler.go:127-141`、`route/vote/vote.go:678+`;两路由均不走 `form.Bind`(`route.go:245,257` 直接挂 handler)。 - -**问题**:`SendMessage` 用 `FormValue("content")`/`FormFile("image")`、`Submit` 用多个 `FormValue(...)` 惰性触发解析——在 Header token 路径下(CSRF 不预解析),首次解析发生在 **handler 内部、无 MaxBytesReader**:multipart 超 32MiB 落盘无上限。私聊图 5MiB 业务上限在**解析完成后**才检查(`chat_handler.go:233`)。 - -**缓解因素**:chat `/send` 前置 `reqUserSignIn`(`route.go:248`)+ 20/min 限流;vote `/submit` 前置三层限流。故列为 P2。**采用 P1-1 的全局 body 上限方案后本项自动闭环**,无需单独改动。 - ---- - -## 四、✅ 确认无问题项(本批次核心实现 + 测试) - -| 文件 | 判定 | 依据 | -|---|---|---| -| `internal/shared/ip.go` | ✅ | 信任模型正确:`TrustProxy=off` 时仅取 `RemoteAddr`;on 时 CF-Connecting-IP → 可信代理 XFF 剥取。`LimitKey` IPv6 聚合 /64 防轮换绕过;注释明确「不得用于黑名单」——与 `ipblacklist.go` 的 exact/CIDR 匹配一致 | -| `internal/route/ratelimit.go` | ✅ | 滑动窗口 + 全局注册表 `sync.Once` 单清理协程(修复旧版每实例泄漏);visitors>10000 快速清理守卫;匿名/登录 3 倍分级独立计数;vote 三层限流键无冲突。登录判定 `s.Get("uid").(uint)` 与 login.go 写入类型一致 | -| `internal/route/autoban.go` | ✅ | `LimitKey` 键与 `f.NotFound` 记录(`route.go:395`)一致,封禁可命中;403 直断(写响应即停链);敏感路径 `defer Record` 于响应后计数;404 独立计数,避免业务 302→/ 误判 | -| `internal/template/markdown.go` | ✅ | 白名单消毒 + `safeURL`(拒 `//`、`\`、`javascript:` 等)+ `safeColorValue`;`MarkdownToHTML`/`PlainText` 与模板 FuncMap 注册一致;测试覆盖 XSS/协议相对 URL/字体色注入 | -| `csrf_binding_test.go` | ✅ | 守卫 `CSRFUserSessionKey="uid"` 与 login 绑定一致(误改回 "userID" 即失败);会话 B 复用 A 的 token → 400 | -| `ip_test.go` / `markdown_test.go` | ✅ | 覆盖充分(IPv4 透传、v4-mapped 折叠、/64 聚合、端口、非法/空;markdown 六类用例) | - -**次要观察(不改)**:`rateLimiter.allow()` 的 >10000 清理仅删「窗口内已无活动」键,活动大流量下 map 仍可增长——受 `LimitKey` 聚合 + 短窗口约束,实际可达量级(数万键 ≈ 数 MB)可接受,非回归。 - ---- - -## 五、✅ 修复实施记录(2026-09-02 夜,已落地并验证) - -> 上节为原评审建议(针对 02:11–02:23 深夜加固批次)。用户批准后已全部实施,P2-2 随 P1-1 自动闭环。**复核中发现原 P2-1 影响面比初判更大**:`UpdateHarassment` / `UpdateBlockWords` / `UpdateTags` 与两个 password handler 一样**不检查 `HasError`**,零值结构体会静默关闭防骚扰 / 清空屏蔽词 / 清空标签——一并纳入守卫。 - -| 编号 | 改动文件 | 内容 | 验证 | -|---|---|---|---| -| P1-1 | `internal/route/bodylimit.go`(新增) | 全局 `requestBodyLimitMiddleware`:POST/PUT/PATCH 且 `ContentLength>12MiB` → 413 快速失败;其余用 `http.MaxBytesReader` 惰性封顶。挂载于 `f.Use` 链(autoban 之后、所有 Group 之前),先于 Contexter 内 CSRF 的整包惰性解析 | `go build -overlay` ✅ | -| P1-1 | `internal/route/route.go` | `f.Use(requestBodyLimitMiddleware())`(autoban 之后) | 全模块编译 ✅ | -| P1-1 | `internal/form/form.go` | `maxRequestBodySize` 导出为 `MaxRequestBodySize`(单一事实来源,供全局中间件引用) | `go vet` ✅ | -| P1-1 | `internal/route/bodylimit_test.go`(新增) | 回归测试:合法 8MiB → 200;Content-Length 超限 → 413;chunked 超限(绕过预检)→ MaxBytesReader 拒绝 | `go test ./internal/route/` ✅ | -| P2-1 | `internal/form/form.go` | 两个反序列化失败分支(ParseForm / ParseMultipartForm)补 `c.SetError(err)`——置位 HasError,使检查 HasError 的 action(含匿名提问/回答/追问)在零值结构体进入业务前早退 | 同上 | -| P2-1 | `route/user/harassment.go` | 新增 `abortOnFormError` helper(HasError → 错误经 flash 转交 → redirect 中止);`UpdateHarassment`、`UpdateBlockWords` 开头加守卫 | 同上 | -| P2-1 | `route/user/tags.go` | `UpdateTags` 开头加守卫(防清空标签 + 关闭首页展示) | 同上 | -| P2-1 | `route/user/password.go` | `UpdatePagePassword` / `UpdateAskPassword` 开头加守卫(防空密码静默解除页面/提问防护;govalid 超长密码此前也会绕过前端限制直接入库,一并拦截) | 同上 | -| P2-2 | — | chat `/send`、vote `/submit` 已被全局中间件覆盖(超限时 CSRF/解析失败即拒),无需单独改动 | 见 P1-1 | - -**测试结果**(`go test -overlay=dev/overlay.json`):`templates`、`internal/template`、`internal/form`、`internal/shared`、`internal/security/autoban`、`internal/security/censor`、`internal/route`(含新增 bodylimit 回归 + csrf_binding)全部 PASS;`go build ./...` 与 `go vet ./internal/... ./route/...` exit 0。 - ---- - -*附:本报告评审结论基于源码走查与依赖库源码语义核验;修复部分已于 2026-09-02 夜落地并通过 overlay 全模块编译 / vet / 相关包测试。* diff --git a/outputs/crop-profile-bottom.png b/outputs/crop-profile-bottom.png deleted file mode 100644 index 7552be4..0000000 Binary files a/outputs/crop-profile-bottom.png and /dev/null differ diff --git a/outputs/crop-q-default-pills.png b/outputs/crop-q-default-pills.png deleted file mode 100644 index c08b5e4..0000000 Binary files a/outputs/crop-q-default-pills.png and /dev/null differ diff --git a/outputs/crop-q-pills.png b/outputs/crop-q-pills.png deleted file mode 100644 index 45eeaa3..0000000 Binary files a/outputs/crop-q-pills.png and /dev/null differ diff --git a/outputs/crop-sakura-footer.png b/outputs/crop-sakura-footer.png deleted file mode 100644 index cf61f6f..0000000 Binary files a/outputs/crop-sakura-footer.png and /dev/null differ diff --git a/outputs/crop-sakura-form.png b/outputs/crop-sakura-form.png deleted file mode 100644 index c171050..0000000 Binary files a/outputs/crop-sakura-form.png and /dev/null differ diff --git a/outputs/crop-sakura-pattern.png b/outputs/crop-sakura-pattern.png deleted file mode 100644 index bddfab1..0000000 Binary files a/outputs/crop-sakura-pattern.png and /dev/null differ diff --git a/outputs/default-bg.png b/outputs/default-bg.png deleted file mode 100644 index f3656e6..0000000 Binary files a/outputs/default-bg.png and /dev/null differ diff --git a/outputs/deploy-res5.png b/outputs/deploy-res5.png deleted file mode 100644 index 89c3cf2..0000000 Binary files a/outputs/deploy-res5.png and /dev/null differ diff --git a/outputs/deploy-vote.png b/outputs/deploy-vote.png deleted file mode 100644 index ee24259..0000000 Binary files a/outputs/deploy-vote.png and /dev/null differ diff --git a/outputs/domain-migration-box-shiroko-2026-09-06.md b/outputs/domain-migration-box-shiroko-2026-09-06.md deleted file mode 100644 index cb5d144..0000000 --- a/outputs/domain-migration-box-shiroko-2026-09-06.md +++ /dev/null @@ -1,279 +0,0 @@ -# box.tama.guru → box.shiroko.one 域名迁移方案(保 Bing 排名) - -> 日期:2026-09-06 | 状态:方案待批准 -> 目标:在 tama.guru **不续费**(2026-12-24 到期)的约束下,把 box.tama.guru 迁移到 box.shiroko.one,最小化 Bing(及 Google)排名损失。 - ---- - -## 0. 决策记录(用户已拍板) - -| 决策点 | 选择 | 影响 | -|---|---|---| -| 过渡策略 | **不续费硬过渡** | 301 窗口 ≈ 现在→2026-12-24(约 3.5 个月) | -| 新域形态 | **box.shiroko.one**(子域) | 根域 shiroko.one 留给导航页/blog 等未来用途 | -| 引擎范围 | **Bing + Google 全套** | Site Move + GSC Change of Address + 双 sitemap + IndexNow | -| umami | **一并迁走** → mami.shiroko.one | 统计不中断 | -| 站群其余子域 | **一并迁移**(blog/chat/share/status → 同名 `.shiroko.one`) | 用户 2026-09-06 追加确认;footer 链接群与 home 文案内链全部换域 | -| `/_/tamakyi` | **不保** | 从 sitemap 移除;robots 维持 `Disallow: /_/`(与 sitemap 自相矛盾的问题一并消除)。页脚/首页指向站长箱的**功能性链接保留**(用户仍可点击),只是不再提交给搜索引擎 | - -> ✅ **批 1(本地代码改动)已于 2026-09-06 完成**,详见 §12。 - ---- - -## 1. 现状与硬约束(侦查取证结果) - -### 1.1 资产地图 -- `tama.guru` 根域 = 「狼的导航页」(Hugo 0.121.2 / CF Pages)——用户声明不保 -- `box.tama.guru` = **本次迁移对象**(提问箱,生产,DNSPod 三线路 DNS:境内→EdgeOne 新加坡、境外/默认→Cloudflare SaaS+tunnel) -- `mami.tama.guru` = umami 统计(Vercel)——随迁 -- `blog/status/share/chat.tama.guru` = 站群其余子域——用户未声明保,列入开放项(§11) -- `r2.337845818.best`、`337845818.best` zone = 独立域,**不受影响**(仅需在 R2 CORS 白名单追加新域) -- 注册信息:tama.guru 注册商 **DNSPod, Inc.**,**expiration 2026-12-24**,当前 active - -### 1.2 box.tama.guru 的 SEO 现状(好消息) -- Bing 收录管道 = `static/sitemap.xml` 仅 **8 条 URL**: - `/`、`/explore`、`/register`、`/login`、`/_/tamakyi`、`/change-logs`、`/explore`(重复)、裸域 -- `/explore` 是唯一「内容页」,排名大概率集中于此 → **索引面小、301 映射表短、信号集中,迁移成本极低** -- ⚠️ robots.txt `Disallow: /_/` 与 sitemap 提交 `/_/tamakyi` **自相矛盾**——搜索引擎以 robots 为准,该条实为无效提交;若 `/_/tamakyi` 有真实流量价值需决策(§11 开放项) -- ⚠️ 页面**无 ``**(仅 og:url)——迁移是新站补 canonical 的好时机,对 Bing 有实际帮助 - -### 1.3 关键机制(Bing/Google 官方语义) -1. **301 全量逐 URL 一一对应**是唯一可靠路径:不 302、不跳首页、无链式跳转 -2. Bing Site Move / GSC Change of Address 只是「加速器」;工具不可用时 301+sitemap 兜底 -3. 提交 Site Move 后 **新域 6 个月内不得再次迁移** -4. 通行要求:301 保持 6–12 个月;权重转移 60–90 天;迁移期掉 10–30% 属正常 -5. **约束风险**:本方案窗口 ~3.5 个月 < 6 个月通行线。缓解:① 索引面仅 3–5 个有效 URL,Bing 对小型站处理快;② 尽早切换拉满窗口;③ IndexNow 秒级通知加速新站收录 - ---- - -## 2. 目标架构 - -``` - ┌─ 境内线路 ──> EdgeOne(box.shiroko.one 加速域) ─┐ -用户 → DNSPod(shiroko.one) ┤ ├→ 源站 TamaBox - (分线路) └─ 境外/默认 ─> Cloudflare SaaS + tunnel ──────┘ - │ -旧域 box.tama.guru(DNS 在切换日改指静态 301 载体,见 §6) - 任意线路 ─> CF Pages 跳转项目 ─ 301 ─> https://box.shiroko.one/<同路径><同query> -``` - -- 新域 **双 CDN 架构完全照搬** box.tama.guru 现状(境内 EO / 境外 CF) -- 旧域在 12-24 前**只做 301**,不再回源 → 源站与旧域解耦,安全退役 -- umami:Vercel 加 `mami.shiroko.one`,同一实例,数据不丢 - ---- - -## 3. 时间线(倒排,越早切换 301 窗口越长) - -| 阶段 | 窗口 | 内容 | 责任 | -|---|---|---|---| -| **P0 准备** | 现在 – 9/20 | 代码/配置改动(§5)、域名/控制台准备(§7)、Bing/GSC 双属性验证(§8.1) | 我 + 你 | -| **P1 新站上线** | 9/20 – 9/27 | box.shiroko.one 双线打通、全功能验收、旧站并行不动 | 我 + 你 | -| **P2 切换** | 9 月底某低峰日凌晨 | 改 external_url → 源站重启 → 旧域 DNS 切 301 载体 → Site Move/CoA/IndexNow(§8) | 我 + 你 | -| **P3 过渡监控** | 10 月 – 12/24 | 每周看两站索引与流量;反链外联更新;旧域保持 301 | 我 | -| **P4 到期善后** | 12/24 后 | 旧域自然失效属计划内;确认新站独立站稳、无旧缓存依赖 | 我 | - -**切换建议 ≤ 2026-10-01**,301 窗口 ≈ 85–90 天,覆盖 Bing 权重转移期(60–90 天)。 - ---- - -## 4. 保排名逻辑(为什么这样做能最小化损失) - -``` -旧 URL(Bing 索引中) ──301──> 新 URL ──sitemap/IndexNow──> Bing 立即重爬新址 - │ │ - 排名信号(点击/外链/信任) 由 301 传递 ≈90% 权重 60–90 天内转移 - │ │ - 旧域 12/24 消亡时:Bing 已把信号合并到新域 ← 目标状态 -``` - -不这样做的代价:旧 URL 到期后全部变死链 → Bing 逐个降权删除 → 新域从零积累,等于「重新开始」。 - ---- - -## 5. 代码与配置文件改动清单(本地副本,批准后分批实施) - -### P0 必改(切域前) - -| # | 文件 | 改动 | 说明 | -|---|---|---|---| -| C1 | 部署侧 `conf/app.ini` | `external_url = https://box.shiroko.one` | **必填配置**;og:url 与全部邮件链接(找回密码/私信通知)一次修复。app.ini 在部署环境持有,本地副本无此文件 | -| C2 | `static/robots.txt` | `Sitemap:` 行改 `https://box.shiroko.one/sitemap.xml` | go:embed 静态文件,当前硬编码旧域 | -| C3 | `static/sitemap.xml` | 8 条 URL 域替换为 box.shiroko.one | 同上;建议顺手去掉与 robots 冲突的 `/_/tamakyi`(§11 决策) | -| C4 | `route/auth/register.go:64` | `Bloglink: "box.shiroko.one"` | 注册引导链接硬编码 | -| C5 | `internal/security/censor/text.go` | TrustedHosts:`tama.guru` → 增加/替换为 `shiroko.one` | 否则用户在新站发 shiroko.one 链接被当广告拦。实施时确认 RemoveTrustedURL 对子域是否覆盖(needle=裸域 + 边界判断) | -| C6 | `templates/base/header.html` | og:url 保留(自动跟随 ExternalURL);**新增** `` | 新站补 canonical,防重复收录,Bing 友好 | -| C7 | `templates/base/header.html` | umami 注入块:`s.src = "https://mami.shiroko.one/tamami.js"` | 与 umami 迁移同批(§5 D 组) | -| C8 | `templates/base/footer.html` | 链接群(blog/chat/share/status/tama.guru)按 §11 决策处理 | 到期后这些全是死链,必须在切换前定 | -| C9 | `templates/home.html` | 正文内链 `box.tama.guru` → `box.shiroko.one`;指向 blog 的文章链接按 §11 决策 | 首页是 Bing 重点页面,内链必须指向新站而非继续依赖 301 | - -### 伴随迁移(D 组,与 box 同批) - -| # | 位置 | 改动 | -|---|---|---| -| D1 | Vercel 控制台 | umami 项目添加自定义域 `mami.shiroko.one`(CNAME → cname.vercel-dns.com) | -| D2 | DNS(新 zone) | `mami` 记录 CNAME → Vercel 目标 | -| D3 | C7 的 header 埋点 | 指向 mami.shiroko.one(同一 data-website-id,数据连续) | - -> mami.tama.guru 在 12-24 前保持在线,与新域并行无冲突(浏览器缓存自然淘汰)。 - ---- - -## 6. 301 实现方案(旧域唯一使命) - -**推荐:CF Pages 静态跳转项目**(免费、一行配置、境内境外通吃) - -1. Cloudflare 新建一个 Pages 项目(如 `tama-redirect`),根目录放一个 `_redirects`: - ``` - /* https://box.shiroko.one/:splat 301! - ``` -2. 把 `box.tama.guru` 添加为该 Pages 项目的自定义域(Pages 面板 → Custom domains → 按提示在 DNSPod 加 CNAME) -3. **切换日**:DNSPod 上 box.tama.guru 的三条线路(境内 EO CNAME / 境外 CF / 默认 CF)统一改 CNAME → 该 Pages 域名(或直接 CNAME 到 pages.dev 域名) -4. 原 EO 加速站点 / CF SaaS 主机名(box.tama.guru)**可停可留**——DNS 切走后自然不再产生流量,12/24 随旧域注销即可 - -- ✅ 保留完整路径与 query(`:splat`),无链式跳转,天然 301 -- ✅ 与源站/旧 CDN 完全解耦,源站只需服务新域 -- ✅ 301 响应体积极小,境内经 CF Pages 慢 ~1s 无感 -- 备选 A:EO 重定向规则(境内)+ CF Worker route(境外)——需在两边控制台分别配,操作更碎 -- 备选 B:源站 nginx 按 Host 301——**不推荐**:CDN 回源 Host=box.tama.guru 时会产生回源环,需同时改回源配置,风险高 - ---- - -## 7. 控制台操作清单(需你在各平台操作,我提供每一步的精确值) - -### 7.1 DNS 托管决策(先行) -**推荐**:`shiroko.one` 的 NS 从 Spaceship 改到 **DNSPod**(你已在 DNSPod 管理 tama.guru,分线路 DNS 是现有双 CDN 架构的前提;Spaceship DNS 无境内/境外分线路)。 -步骤:DNSPod 添加 `shiroko.one` → 获得 DNSPod 分配的 NS → Spaceship 域名面板改 NS → 等待生效(1–24h,可用 `nslookup -type=NS shiroko.one 8.8.8.8` 验证)。 - -### 7.2 DNSPod(shiroko.one zone) -| 主机 | 类型 | 值 | 线路 | -|---|---|---|---| -| box | CNAME | EO 分配的 CNAME 目标(控制台给,形如 `xxx.eo.dnse4.com`) | 中国境内 | -| box | CNAME | CF SaaS CNAME 目标(337845818.best zone → Custom hostnames → 形如 `.cdn.cloudflare.net`)或既有优选 CNAME | 境外 / 默认 | -| mami | CNAME | `cname.vercel-dns.com` | 默认 | - -> 以上记录结构完全照搬 box.tama.guru / mami.tama.guru 现有三条线;切换日另加 box.tama.guru 的 301 线路(§6)。 - -### 7.3 EdgeOne(腾讯) -- 新增加速域名 **box.shiroko.one**:归属验证(TXT/CNAME,验证记录保留勿删)→ HTTPS 证书 → 回源=源站 IP:高位端口、回源 HOST=box.shiroko.one -- 规则/缓存配置照抄 box.tama.guru 现有站点 - -### 7.4 Cloudflare -- `337845818.best` zone → SSL/TLS → Custom hostnames(for SaaS):添加 **box.shiroko.one**(回源走既有 tunnel;cloudflared ingress 增加 box.shiroko.one 路由) -- R2 bucket CORS 白名单:追加 `https://box.shiroko.one`(**必须在 301 前完成**,否则新站图片/二维码导出被 CORS 拦) - -### 7.5 Vercel -- umami 项目添加 `mami.shiroko.one`(§5 D1) - ---- - -## 8. SEO 迁移执行序列 - -### 8.1 预备(P0,先于新站上线,可立即做) -1. **Bing Webmaster Tools**:添加并验证 `box.shiroko.one`(用 DNS TXT 验证,DNSPod 加记录即可,无需等站点上线);确认旧站 `box.tama.guru` 属性已存在并验证 -2. **Google Search Console**:同上双属性(URL-prefix:`https://box.tama.guru/`、`https://box.shiroko.one/`) - -### 8.2 切换日顺序(P2,低峰凌晨,301 是前提) -1. 确认 box.shiroko.one 在 EO(境内视角)与 CF(境外视角)全功能验收通过(§9) -2. 改 `external_url` + 重启 → 新站所有 og/canonical/sitemap/邮件全指新域 -3. 旧域 DNS 切 301 载体(§6)→ `curl -I` 验证 301 与 Location 正确 -4. **Bing Site Move**(旧属性 → Diagnostics & Tools → Site Move → 目标 box.shiroko.one,Scope=Domain)——若工具不可用,301+sitemap 兜底 -5. **GSC Change of Address**(旧属性 → 设置 → Change of Address → 新属性) -6. 双引擎分别提交**新站 sitemap**;旧站 sitemap 保留 60–90 天(引导爬虫吃 301) -7. **IndexNow**:用 API key 提交新站全部 URL(Bing 收录最快路径) -8. 逐条验证 §9.2 的 301 映射 - ---- - -## 9. 验证与验收 - -### 9.1 切换前(新站验收,全部通过才允许 301) -- [ ] `https://box.shiroko.one/` 境内 EO / 境外 CF 均 200(用不同线路 DNS 查询验证) -- [ ] 登录 / 提问 / 私信 / 上传 / 投票 / 二维码导出 主链路通过(R2 CORS 已含新域) -- [ ] `curl -s https://box.shiroko.one/robots.txt` → Sitemap 行 = 新域 -- [ ] `curl -s https://box.shiroko.one/sitemap.xml` → 全部 URL = 新域 -- [ ] 页面含 canonical 指向自身、og:url = 新域 -- [ ] 邮件模板链接 = 新域(发一封找回密码实测) - -### 9.2 切换后(301 映射抽查) -``` -curl -sI https://box.tama.guru/explore → 301 Location: https://box.shiroko.one/explore -curl -sI https://box.tama.guru/_/tamakyi → 301 → https://box.shiroko.one/_/tamakyi -curl -sI "https://box.tama.guru/vote/xxx?lang=en" → 301 且保留 query -curl -sI https://box.tama.guru/no-such-page → 301(不允许 404/302) -``` -- [ ] 抽样 20+ URL,确认无 302、无 404、无链式跳转(一条 301 直达) - -### 9.3 过渡期每周监控(P3) -- Bing/GSC:旧域 indexed 数量应**逐周下降**;新域**逐周上升**,60–90 天追平旧域基线 -- 综合 Search Performance:前 2–4 周掉 10–30% 属正常;**持续 >30% 且无回升 = 异常**,立即排查 -- 新域 site:box.shiroko.one 查询确认收录增长 - ---- - -## 10. 风险清单 - -| # | 风险 | 等级 | 缓解 | -|---|---|---|---| -| R1 | 301 窗口 3.5 个月 < 6 个月通行线,权重转移可能不完整 | 🟠 中 | 9 月底前切换拉满窗口;索引面小 + IndexNow 加速;Bing 对小型站处理快 | -| R2 | 切换瞬间旧书签/API 用户 301 到新域 → cookie 一次性失效、跨域 | 🟡 低 | 一次性,切换公告;API 调用方(若有)需跟随新域 | -| R3 | umami 迁移延误 → 12/24 后统计断 | 🟡 低 | D 组随 P1 完成,两域并行过渡 | -| R4 | DNSPod 到期政策未知:到期日是否即停解析 | 🟡 低 | 按 12-24 停摆做计划;若到期后保留解析则 301 窗口自动延长,纯赚 | -| R5 | `/_/tamakyi` robots/sitemap 矛盾未决 → 若该页真有 Bing 流量,迁移映射需特殊处理 | 🟡 低 | §11 决策后补映射 | -| R6 | footer/home 指向 blog 等将死子域 → 新站页面出现死链 | 🟡 低 | §11 决策 | - ---- - -## 11. 开放项(2026-09-06 更新:1/2 已由用户答复) - -1. ~~站群其余子域(blog/status/share/chat)是否迁~~ → **已答:一并迁**。已按 `*.shiroko.one` 同名映射改完(footer 6 条链接 + home 文案内链 + llms 三处博客链)。 - ⚠️ 仍需你操作:这些子域的**实际服务**也要搬(blog 的 Hugo/CF Pages、chat/share/status 各自托管),否则新域链接会是死链。若某个子域打算放弃(如 share/status),告诉我,我把 footer 对应条目删掉。 -2. ~~`/_/tamakyi` 是否保~~ → **已答:不保**。已从 sitemap 移除,robots 维持 Disallow。 -3. **源站回源端口/隧道细节**:EO 与 CF tunnel 的 ingress 路由是否按 hostname 区分——决定 cloudflared 配置加一条路由即可(实施时我出精确 diff) -4. 迁移公告是否要做(首页横幅提示用户更新收藏/提问链接) -5. **搜索引擎验证 token 需替换**(本次代码审查新发现):`templates/base/header.html` 的 `google-site-verification`(636 行)与 `msvalidate.01`(7 行 + 637 行重复)是**绑定域名**的,新域验证后必须替换 content 值,否则 GSC/Bing 无法验证新站 → Site Move / Change of Address 无法提交。已加注释标记。 - ---- - -## 12. 批 1 实施记录(本地改动,2026-09-06 已完成) - -**备份**:`.workbuddy/backup/domain-shiroko-20260906/`(9 个文件原样) -**替换规则**:`*.tama.guru` → `*.shiroko.one`;apex `tama.guru` → `shiroko.one` - -| 文件 | 改动 | -|---|---| -| `static/robots.txt` | `Sitemap:` 行 → `box.shiroko.one` | -| `static/sitemap.xml` | 5 条 URL 全部换域;**移除 `/_/tamakyi` 条目**;**去重**(原 `https://box.tama.guru` 与 `.../` 重复);`lastmod` 全量刷新为 2026-09-06 以触发重抓 | -| `static/llms.txt` | 4 条站点链接 + 2 条资源链接换域 | -| `static/llms-full.txt` | 7 处换域(主域推荐、FAQ、站点地图、blog 更新日志链) | -| `templates/base/footer.html` | 6 条链接全换域(admin/notice/chat/drive/status/nav) | -| `templates/base/header.html` | umami 注释同步为 `mami.shiroko.one`(脚本 src 已先行改为新域);验证 token 处加迁移提醒注释 | -| `templates/home.html` | 3 处换域:站长箱链接、投票/Markdown 示例链、4 条 blog 文章链、探索页链 | -| `route/auth/register.go:64` | `Bloglink` 默认值 → `box.shiroko.one` | -| `internal/security/censor/text.go` | `TrustedHosts` **新增** `"shiroko.one"`;旧域 `tama.guru` **保留**(过渡期用户分享旧链接仍需判为可信,待旧域到期后再删) | -| `conf/app.sample.ini` | 补齐**缺失的必填项** `external_url`(conf.go:36 强制校验,原模板缺失会导致照抄即启动失败),示例指向新域 | -| `dev/eo-probe-ips.py`、`eo-probe-v2.py`、`eo-scan-all.py` | EO 回源探测脚本 `--host` 默认值 → `box.shiroko.one` | -| `internal/route/route.go` | **新增 `/index-now/{key}` 路由**(IndexNow 密钥文件,见 §12.1)。文件名用 `^[0-9a-f]{32}\.txt$` 白名单校验防路径穿越;配套回归测试 `internal/route/indexnow_route_test.go` | - -**验证**:sitemap XML 解析通过(5 条、无重复);`go test ./templates/`、`./internal/security/censor/`、`./internal/route/` 通过;`route/auth`、`static`、`internal/conf` 带 overlay 编译通过。 - -### 12.1 IndexNow 密钥文件(迁移后加速新站收录,已实现) - -用户在 `static/index-now/66e5e0911b0941f2ba7dbfbbd961e9be.txt` 放了 IndexNow key,但访问被 302 到首页。 - -**根因(重要,以后加任何根目录静态文件都会踩)**:`internal/route/route.go:138` 的 `flamego.Static` 中间件配置了 `Prefix: "/static"`,**只接管 `/static/...`**;而 `robots.txt`、`sitemap.xml`、`llms.txt`、`llms-full.txt`、`favicon.ico` 这些根目录文件**每一个都是单独注册的 `f.Get` handler**。新加的文件没有对应路由 → 落到 `f.NotFound` → `ctx.Redirect("/")` 返回 302。embed 本身没问题。 - -**修复**:新增 `f.Get("/index-now/{key}")`,文件名经 `^[0-9a-f]{32}\.txt$` 白名单校验(防路径穿越)后从 embed FS 读取,返回 `text/plain` + `max-age=86400`;校验失败或文件不存在返回 404(不再是 302)。 - -**提交 IndexNow 时的 keyLocation 填**: -``` -https://box.shiroko.one/index-now/66e5e0911b0941f2ba7dbfbbd961e9be.txt -``` -⚠️ 旧域 301 上线后,旧域的该路径会被 301 重定向,而搜索引擎校验密钥文件要求 **200 直出**,因此切换后请用新域的 keyLocation 重新提交。 - ---- - -## 附:本地代码改动的执行批次建议(批准后按批落地) -- ~~**批 1(P0 代码)**:C2/C3/C4/C5/C6/C8/C9 —— 纯本地仓库改动,可先行~~ → ✅ **已完成 2026-09-06**(见 §12) -- **批 2(配置)**:C1 + D 组 —— 需部署环境 app.ini + Vercel 控制台配合 -- **批 3(切换)**:§6 301 载体 + §7 DNS/CDN 控制台操作 + §8 SEO 提交 diff --git a/outputs/edgeone-cloudflare-dual-cdn-guide.md b/outputs/edgeone-cloudflare-dual-cdn-guide.md deleted file mode 100644 index 8f19470..0000000 --- a/outputs/edgeone-cloudflare-dual-cdn-guide.md +++ /dev/null @@ -1,155 +0,0 @@ -# TamaBox 双 CDN 接入指南:境内腾讯 EdgeOne + 境外 Cloudflare - -> 目标:中国大陆访客走腾讯 EdgeOne 节点加速,境外访客走 Cloudflare 边缘网络加速,共用同一域名与源站。 -> 代码前置:`internal/shared/ip.go` 自 **2026-09-03** 起在 `trust_proxy = true` 时信任 EdgeOne 权威客户端头 `EO-Client-IP`(等效 `CF-Connecting-IP`)。单 EdgeOne 或双 CDN 部署均可直接用本文件配套。 - ---- - -## 0. 总体架构 - -``` - ┌──────────────── DNS(DNSPod 智能解析)─────────────────┐ - │ 线路「境内」 → CNAME → EdgeOne 分配的 CNAME │ - │ 线路「境外」 → CNAME → Cloudflare for SaaS CNAME │ - │ 线路「默认」 → CNAME → Cloudflare(未匹配线路兜底) │ - └───────────────┬───────────────────┬────────────────────┘ - ▼ ▼ - EdgeOne(腾讯,大陆节点) Cloudflare(全球边缘) - │ │ - └─────► 源站(仅放行两家回源网段)◄──────┘ - TamaBox (Flamego) + PG + Redis -``` - -核心思路:**CDN 各自接入,DNS 按线路分发**。两家 CDN 都不需要托管你的主域名 NS,主域名 NS 全程留在 DNSPod(腾讯云解析),靠「境内 / 境外 / 默认」三条线路把用户分给不同的 CNAME 目标。 - -## 1. 前置条件(逐项确认) - -| # | 条件 | 说明 | -|---|------|------| -| 1 | 域名已 **ICP 备案** | EdgeOne 加速区域含「中国大陆 / 全球可用区」时的硬性要求,不可绕开 | -| 2 | 腾讯云账号已实名 | 开通 EdgeOne 需要 | -| 3 | Cloudflare 账号 | Free 套餐即可(见步骤 3 的 SaaS 说明) | -| 4 | **一个闲置域名(或其子域)用于 Cloudflare 承载 zone** | Cloudflare for SaaS 需要一个已托管在 CF(NS 迁过去)的域名作为“承载 zone”,主域名不需要迁 NS。没有就考虑末尾的备选方案 C | -| 5 | 源站公网可达 | 回源协议 / 端口按你的部署调整 | - -## 2. 步骤 1:DNS 迁到 DNSPod 并建立分线路骨架 - -1. 在 DNSPod(腾讯云解析)添加你的域名,按提示把注册商处的 NS 改成 DNSPod 提供的 `xxx.dnspod.net` 两条。 -2. 等解析生效后,先**建立保底记录**,避免切换期中断: - - 线路「默认」:`www` → CNAME → EdgeOne(或先保持源站 A 记录,逐步切); - - 建议把各记录 **TTL 临时调小(如 60 秒)**,切换完成后再调回。 -3. ⚠️ 若根域(`@`)承载邮箱(存在 MX 记录),**不能**给 `@` 配 CNAME(与 MX 冲突)。此时只对 `www` / 业务子域做 CDN 加速,`@` 保持 A 记录直连或做 301 跳转。 - -## 3. 步骤 2:EdgeOne 接入(CNAME 模式) - -1. 控制台 → 添加站点 → 输入主域名 → 选购套餐 → **加速区域:中国大陆可用区**(境外交给 Cloudflare)。 -2. **接入模式选 CNAME 接入**(主域名 NS 在 DNSPod,不能用 NS 接入)。 -3. 归属权验证:按控制台提示在 DNSPod 添加 TXT 或做文件验证。 -4. 添加加速域名:在「域名服务 → 域名管理 → 添加域名」里填业务主机名(`www` 或 `@`)+ 源站 IP/域名。 -5. 平台分配一个 CNAME 地址(形如 `xxx.edgeone.net`),**先记下来**,步骤 5 统一填到 DNSPod。 -6. 证书:开启「站点加速 → HTTPS」自动证书(免费)。 - -## 4. 步骤 3:Cloudflare for SaaS(主域名不迁 NS 的关键) - -Cloudflare Free/Pro/Business 套餐的普通接入要求把整个 zone 迁到 CF 的 NS,与分线路解析冲突。**Cloudflare for SaaS**(Free 套餐含 100 个自定义主机名免费)允许主域名不迁 NS、仅用 CNAME 挂到 CF: - -1. 用闲置域名(如 `cf-host.example`)在 Cloudflare 添加 zone,并把该域名的 NS 迁到 Cloudflare(承载 zone,仅此域名迁)。 -2. 承载 zone 内:`SSL/TLS → Custom Hostnames(SSL for SaaS)→ Add Custom Hostname`,填入你的**主域名**(如 `www.example.com`)。 -3. 按提示完成归属验证:CF 会给出一条验证 TXT(加到 DNSPod)+ 一个 **CNAME 目标值**(形如 `xxx.1234.cdn.cloudflare.net`),记下来用于步骤 5 的境外线路。 -4. 等待证书签发至状态 `Active`(通常数分钟到数小时)。之后 CF 自动管理该域名的证书与续期。 - -> 备选:若不想多养一个域名做承载 zone,可退回**方案 C**——EdgeOne 加速区域直接选「全球可用区」,一家覆盖全球,不做 Cloudflare(代价:境外节点质量与覆盖面不如 CF)。或评估 Cloudflare Business(约 $200/月)的官方 CNAME 接入。 - -## 5. 步骤 4:DNSPod 分线路记录(最终形态) - -同一主机名、不同线路分别建行(全部 CNAME,避免 A/CNAME 冲突提示): - -| 主机记录 | 线路 | 类型 | 记录值 | 说明 | -|---|---|---|---|---| -| `www` | 境内 | CNAME | `xxx.edgeone.net`(EO 分配) | 大陆走 EdgeOne | -| `www` | 境外 | CNAME | `xxx.…cdn.cloudflare.net`(SaaS 分配) | 境外走 Cloudflare | -| `www` | 默认 | CNAME | `xxx.…cdn.cloudflare.net` | 未匹配线路(如部分境外 DNS)兜底 | - -> 验证:境内 `dig www.example.com`(或 `nslookup`)应解析出 EdgeOne CNAME;用境外 DNS(如 `dig @8.8.8.8`)应解析出 Cloudflare CNAME。不同递归节点可能有缓存,等待 TTL 过期后复测。 - -## 6. 步骤 5:源站锁定(安全前提,不可跳过) - -`trust_proxy = true` 意味着应用信任 CDN 写来的客户端头,**前提是请求只能来自这两家 CDN**: - -1. 从官方渠道取回源网段并配置防火墙 / 安全组,仅放行: - - Cloudflare 官方 IP 段:https://www.cloudflare.com/ips/ (`cloudflare.com/ips-v4`、`ips-v6`) - - EdgeOne 回源 IP 段:腾讯云 EdgeOne 文档「回源 IP 网段」页(会动态更新,建议做成定期同步或至少配置时核对) -2. 放行你的运维入口(SSH 管理 IP)与回源端口(80/443),其余一律拒绝直连。 -3. 回归验证:跑一次仓库自带的 `dev/security-selfcheck.sh`,确认带伪造 `CF-Connecting-IP` 的直连请求已被防火墙拦下(应无法到达应用)。 - -## 7. 步骤 6:真实 IP 传递(EdgeOne 侧关键配置!) - -EdgeOne **默认不携带** `EO-Client-IP`,必须在控制台开启,否则经 EdgeOne 的请求到源站后拿不到客户端真实 IP(自动封禁会误伤所有大陆访客): - -- 站点级:`站点加速(全局)→ 网络优化 → 客户端 IP 头部配置 → 开启`,**头部名称务必填 `EO-Client-IP`**(代码固定读这个名字,大小写敏感)。 -- 或域名级:`规则引擎 → 创建规则(Host 匹配该域名)→ 操作:客户端 IP 头部 → 开启 → 名称填 `EO-Client-IP``。 - -Cloudflare 侧无需额外配置:`CF-Connecting-IP` 默认就有且恒被 CF 覆盖为真实客户端 IP。 - -## 8. 步骤 7:防“借道伪造”剥离规则(双 CDN 特有安全约束) - -两家并存时出现新的伪造面:**攻击者借道 CDN-A 时携带 CDN-B 的权威头**。例如客户端经 Cloudflare 访问时自行塞一个 `EO-Client-IP: 任意IP`,若 CF 透传该头到源站,代码会信任它 → 可绕过限流 / 自动封禁 / 单 IP 投票限制,或嫁祸他人。因此必须在两家 CDN 的**回源请求**上剥离对方权威头: - -| CDN | 剥离动作 | 位置 | -|---|---|---| -| Cloudflare | 回源前删除请求头 `EO-Client-IP` | `Rules → Transform Rules → Modify Request Header`(Free 套餐可用,规则数有限但只建一条即可) | -| EdgeOne | 回源前删除请求头 `CF-Connecting-IP` | `规则引擎 → 操作:修改回源请求头 → 删除 CF-Connecting-IP`(或站点全局回源请求头设置) | - -> `X-Forwarded-For` / `X-Real-IP` 不需要在此剥离:代码只在 TCP 对端落入 `trusted_proxies` 时才解析它们,两家 CDN 边缘 IP 不在清单内,天然不可信。 -> -> 验证方法:分别经两家 CDN 发起带伪造头的请求(如 `curl -H 'EO-Client-IP: 203.0.113.66' https://www.example.com/`),在源站访问日志里确认最终到达的请求**不包含**伪造的对方头,且应用记录的客户端 IP 为你的真实出口 IP。 - -## 9. 步骤 8:应用配置(conf/app.ini) - -```ini -[security] -trust_proxy = true -; trusted_proxies 仅用于“你自己的”反代/LB(如本机 nginx 127.0.0.1), -; 双 CDN 场景不需要也不应把两家 CDN 的全量网段填进来(代码直接信任 CF-Connecting-IP / EO-Client-IP)。 -trusted_proxies = - -[server] -; 若用防火墙按网段放行回源,可继续监听 :443;若希望更严,可只监听本机 + 本机回源代理。 -listen = -``` - -行为(2026-09-03 起):`trust_proxy = true` 时 `ResolveClientIP` 依次信任 `CF-Connecting-IP` → `EO-Client-IP` → (仅当 TCP 对端在 `trusted_proxies` 内)解析 XFF/X-Real-IP → 最后回退 RemoteAddr。 - -## 10. 步骤 9:缓存与动态内容 - -TamaBox 是动态应用: - -- 两侧都**不要缓存 HTML / 页面 / API 响应**;只缓存静态资源。 - - EdgeOne:缓存规则命中 `/static/*` 与带版本号资源即可; - - Cloudflare:`Caching → Cache Rules` 对 `/static/*` 开缓存,其余 `Bypass`。 -- 若将来启用 WebSocket 长连接:Cloudflare Free 支持;EdgeOne 需在站点配置里显式开启 WebSocket。 -- 注意两侧的 TLS 版本 / HTTP/3 行为差异属正常现象,与应用无关。 - -## 11. 上线切换与回滚 - -1. 先切「默认」线路 → 观察 15~30 分钟(错误率、日志中的真实 IP 是否正确)。 -2. 再切「境内」线路(EdgeOne CNAME 生效)→ 用大陆网络(或大陆 DNS 递归查询)验证。 -3. 最后切「境外」线路到 Cloudflare SaaS CNAME → 境外访问验证 + 证书状态 `Active`。 -4. 回滚:把对应线路的 CNAME 改回原记录即可,秒级生效(DNS TTL 决定传播速度)。 - -## 12. 故障排查速查 - -| 现象 | 检查 | -|---|---| -| 大陆访问慢 / 未走 EO | DNSPod「境内」线路是否已配 EO CNAME;`nslookup` 从大陆递归查询结果 | -| 境外访问失败 / 证书错误 | SaaS Custom Hostname 状态是否 `Active`;境外线路 CNAME 是否拼错 | -| 自动封禁误封 / 限流全走同一个 IP | EO「客户端 IP 头部」是否开启且名称是否为 `EO-Client-IP`;`trust_proxy` 是否 true | -| 应用日志里出现伪造 IP | 两家剥离规则是否生效;防火墙是否真的挡住了直连 | -| 根域无法配 CNAME | 根域有 MX → 只加速 `www`/业务子域,`@` 保持 A 记录 | - -## 13. 相关代码与文档 - -- 代码改动:`internal/shared/ip.go`(`EO-Client-IP` 信任分支)、`internal/shared/ip_test.go`(新增 4 条用例)、`internal/conf/static.go` 与 `conf/app.sample.ini`(注释同步)。 -- 单 CDN Cloudflare 加固参考:`outputs/cloudflare-origin-lockdown-guide.md`。 -- EdgeOne 客户端 IP 头部(腾讯官方文档,会更新):「携带客户端 IP 头部回源」。 -- Cloudflare for SaaS 官方文档:developers.cloudflare.com → SSL/TLS → SSL for SaaS。 diff --git a/outputs/edgeone-noicp-cf-tunnel-guide.md b/outputs/edgeone-noicp-cf-tunnel-guide.md deleted file mode 100644 index c460b64..0000000 --- a/outputs/edgeone-noicp-cf-tunnel-guide.md +++ /dev/null @@ -1,252 +0,0 @@ -# TamaBox 免备案双线接入指南(EdgeOne 国际站 + Cloudflare Tunnel) - -> **适用场景(2026-09-03 定稿)**:不备案。国内用户走腾讯 EdgeOne 国际站(免备案加速区域「全球可用区(不含中国大陆)」,大陆用户落到香港等最近海外节点,跨域骨干质量优于 CF);海外用户继续走现网 Cloudflare(`box.tama.guru` 已是 `337845818.best` zone 的 SaaS 自定义主机名 + Cloudflare Tunnel 回源,已正常生效)。 -> -> **配套代码**:`internal/shared/ip.go` 已支持 EO-Client-IP(境内分支)与 CF-Connecting-IP(海外分支),`trust_proxy=true` 时双头同权信任,无需维护 CDN 大 CIDR 清单。 - -> ## 🔴 重要勘误(2026-09-03 三次更正定稿)——权威在 DNSPod;分线路已确认生效 -> -> ⚠️ **历次结论演进**: -> 1. 初版「权威在 Cloudflare → 需子域委派」**作废**(用户 CF 没有 tama.guru zone,正常——它本来就不该有); -> 2. 二次更正(RDAP 直连 `.guru` 注册局 Identity Digital 铁证):`tama.guru` 注册局 NS 委托 = **`byron.dnspod.net` + `sunshine.dnspod.net`(DNSPod)**。此前误判因 apex `tama.guru` 自身 CNAME → `navi-ew2.pages.dev`(CF Pages),nslookup 跟随 CNAME 链误读; -> 3. **三次更正(12:10 最终确认)**:用户 DNSPod 面板已配好三线路(境内→EO / 境外→CF 优选 `cloudflare.182682.xyz` / 默认→CF 优选),NS=byron/sunshine——**分线路确认生效**: -> - **境外视角**(两家境外服务器 API 实测:hackertarget / networkcalc)→ `CNAME cloudflare.182682.xyz` → CF IP(104.x)✅ -> - **境内视角**(223.5.5.5 / 119.29.29.29 / alidns DoH 稳定查询)→ `CNAME eo.dnse4.com` → EO IP(43.174.x)✅ -> - 此前「全球四视角全部 EO」的实测是**假象**:大陆本机发往境外公共 DNS(8.8.8.8 / 9.9.9.9 / OpenDNS / 1.0.0.1,UDP 与 TCP 均)的查询被网络劫持/污染,应答按境内线路给出 EO。⚠️ **大陆本机 nslookup 境外 DNS 的结果不可信**,验证分线路必须用境外服务器 API 或境外网络(方法见 2.3 第 4 步)。 -> 4. EO 归属验证若依赖 CNAME 自动校验,仍建议改 TXT 验证并把 TXT 放 DNSPod(防止分线路后续期失败)——此条仍适用。 - ---- - -## 0. 已核实事实(官方口径) - -| 项 | 结论 | -|---|---| -| EdgeOne 加速区域与备案 | 「中国大陆可用区」「全球可用区」**必须备案**;「全球可用区(不含中国大陆)」**无需备案**(国际站同规则) | -| 不备案时大陆用户走哪 | 无大陆节点,就近落香港/日韩/新加坡等海外节点;香港节点走腾讯跨境骨干,大陆延迟通常显著优于 Cloudflare 免费版(仍建议实测) | -| Cloudflare for SaaS | 免费额度 100 个自定义主机名;`box.tama.guru` 已配置并正常打开,海外分支**零改动** | -| EdgeOne EO-Client-IP | **默认关闭**,需手动开启;自定义头部名必须填 `EO-Client-IP`(代码固定读此名) | -| 应用真实 IP 信任 | 已支持:境内→EO-Client-IP;海外→CF-Connecting-IP | -| **回源 IP 段官方入口** | 控制台「**安全防护 → 源站防护**」启用后展示回源 IP 列表(IPv4/IPv6 段);段变更前 14/7/3/1 天站内信+短信+邮件通知;可用 `DescribeOriginACL` API 每 3 天轮询自动同步(intl 文档 1145/48535) | -| **⚠️ 免费套餐限制** | **免费套餐不支持启用「源站防护」**(2026-03 官方答疑)→ 拿不到官方回源段。出路见 2.2「回源网段三选一」 | -| **🔴 DNS 分线路实测(2026-09-03 三次更正定稿)** | `tama.guru` 权威 NS = **DNSPod**(注册局 RDAP:`byron.dnspod.net` + `sunshine.dnspod.net`);CF 无此 zone。三线路(境内→EO / 境外→CF / 默认→CF)**已确认生效**:境外服务器 API → CF(104.x),境内通道 → EO(43.174.x)。⚠️ 大陆本机 nslookup 境外 DNS 会被劫持假返回 EO,不可用作境外视角 | - ---- - -## 1. 目标拓扑 - -``` - ┌────────────────────────────────────────────┐ - │ DNSPod 托管 tama.guru 整域 zone(权威) │ - │ box 记录按线路分流: │ - │ 中国境内 ──CNAME──▶ EdgeOne CNAME 目标 │ - │ 默认 / 国外 ──CNAME──▶ CF SaaS CNAME 目标 │ - └───────┬─────────────────────────────┬──────┘ - 大陆用户▶▶────────┘ │ 海外用户▶▶ - (大陆 ISP 递归命中境内线路) │ - ▼ ▼ - EdgeOne 国际站 Cloudflare 边缘 - 区域: 全球(不含中国大陆) SaaS 自定义主机名 box.tama.guru - HTTPS 免费证书自动签发 (挂在 337845818.best zone) - │ EO 回源(直连源站公网端口) │ Tunnel(源站主动出站) - ▼ ▼ - ┌────────────────────────────┐ cloudflared(本机/内网) - │ 源站防火墙:仅放行 EO 回源网段│◀──────────────(无入站端口,不受影响) - └───────────────┬────────────┘ - ▼ - TamaBox 应用(监听公网回源端口) -``` - -**安全前提(缺一不可,否则头信任模型可被绕过):** -1. 源站防火墙只对 **EdgeOne 官方回源网段** 开放回源端口(默认拒绝一切); -2. EO 侧开启「客户端 IP 头」且名称 = `EO-Client-IP`; -3. EO 规则引擎**删除**客户端可伪造的 `CF-Connecting-IP` / `X-Real-IP`(否则大陆攻击者经 EO 带假 CF 头会被应用优先信任); -4. `conf/app.ini` 的 `[security]` 设 `trust_proxy = true`。 - -> ⚠️ **前提 1 与套餐强相关**:官方回源段只有启用「源站防护」才能拿到,而**免费套餐不支持该功能**。免费版请先读 2.2 的「回源网段三选一」选定替代路径,再继续配置。 - -> 海外分支天然安全:攻击者只能经 Cloudflare 边缘进来,CF 会用自己的观测值重写 `CF-Connecting-IP`,客户端伪造无效;Tunnel 到源站为出站连接,源站无对应入站口。 - ---- - -## 2. 分步实施 - -### 2.1 EdgeOne 接入(国际站 edgeone.ai) - -1. 登录 EdgeOne 国际站控制台 → **添加站点**: - - 站点域名:`box.tama.guru`(直接用子域做站点,避免影响 `tama.guru` 其他记录); - - 加速区域:**「全球可用区(不含中国大陆)」**(勿选含中国大陆的区域——未备案无法添加,选错会要求备案); - - 接入方式:**CNAME 接入**(保留 DNSPod 分线路能力)。 -2. **归属验证选「DNS / TXT 验证」**(不要选要求整体 CNAME 的方式):按控制台提示在 DNSPod 为 `box.tama.guru` 添加指定 TXT(或指定子域名的 CNAME),验证通过后**该验证记录保留勿删**(续期/校验会用到)。 -3. HTTPS:开启自动免费证书(默认开启)。 -4. 状态变「已生效」后,记下控制台分配的 **CNAME 目标**(形如 `..edgeone…`,以控制台显示为准),供 2.3 使用。 -5. **开启客户端 IP 头**:站点 → 网络优化 / 规则引擎(套餐不同路径略有差异)→「增加客户端 IP 请求头」类功能 → 开启,并把头部名**务必填 `EO-Client-IP`**。 -6. **配置回源**:源站地址 = 服务器公网 IP;端口 = 2.2 选择的回源端口(优先高位非标端口,若控制台仅允许 80/443 则用 443 + 源站 TLS,见 2.2);回源 HOST = `box.tama.guru`;协议建议与现状一致(HTTP,由 EO 边缘终结 TLS)。 -7. **(可选加固)回源鉴权**:若套餐支持「回源鉴权 / 自定义回源头」,加一个仅源站知道的头(如 `X-EO-Auth: <随机串>`),源站入口校验,作为防火墙白名单之外的纵深兜底。 - -### 2.2 源站监听公网口(不需要 nginx)与回源锁定(关键安全步骤) - -**不需要 nginx/Caddy**:TamaBox 自带 HTTP 服务器,直接让应用监听公网端口即可——nginx 在整套方案里只是可选的「TLS/反代」环节,不是安全环节,低配服务器完全不必装。系统防火墙(ufw/iptables/firewalld)是内核 netfilter 规则,无常驻高内存进程,也不占资源。 - -1. **改监听地址**:`conf/app.ini` 的 `[server]` 段: - ```ini - [server] - port = 18080 ; 换高位非标端口,别用 80/443 - listen = 0.0.0.0:18080 ; 关键:从「仅本机」改为「全网卡」,EO 才能远程连入 - ``` - > 说明:此前 Cloudflare Tunnel 单线部署建议 `listen = 127.0.0.1:port`(只对本机 cloudflared 可见);接 EO 直连后必须改为 `0.0.0.0:port`(或删掉 `listen` 留空 = 默认 `:port` 全网卡)。**Tunnel 分支不受影响**——cloudflared 连的是 `127.0.0.1:port`,`0.0.0.0` 监听兼容回环连入。改完重启应用。 - > 端口限制:EO 回源端口是否允许高位非标以控制台为准;若只允许标准端口集合,再考虑在 `443` 前置 TLS(届时才需要 Caddy/nginx 或改用 HTTPS 回源)。 -2. **EO 回源协议选 HTTP**(TLS 由 EO 边缘终结,与现在 Tunnel 到本机 HTTP 的形态一致)。注意 EO 边缘→源站这段是公网明文,可接受则用,介意再改 HTTPS 回源。 - -#### 回源网段三选一(决定安全模型) - -**背景**:官方回源 IP 段只在「源站防护」功能里给,**免费套餐不支持启用**——这正是你在控制台找不到列表的原因。三条出路: - -| 方案 | 成本 | 做法 | 风险 / 说明 | -|---|---|---|---| -| **A. 官方源站防护(推荐)** | EO 基础版以上(以控制台计费为准) | 控制台 → 站点 → **安全防护 → 源站防护** → 启用 → 复制回源 IP 列表(IPv4/IPv6)配防火墙;变更前 14/7/3/1 天会站内信/短信/邮件通知;也可用 `DescribeOriginACL` API 每 3 天轮询自动同步 | 最标准,段变更有官方通知兜底 | -| **B. 观察法自建 ACL(免费)** | 免费 | ① 先把回源端口临时放开(或安全组先放行);② 源站抓 3–7 天回源连接来源:`tcpdump -nn port 18080` 或看应用日志里的对端 IP;③ 把来源聚类成 CIDR;④ 防火墙收紧为只放行这些段;⑤ **每月自查一次**来源是否有新增 | ⚠️ EO 回源段平均 3–6 个月会变,免费档收不到变更通知 → 段变化会导致回源失败;出故障先临时全放再排查。低流量个人站可接受 | -| **C. 不设白名单(不推荐)** | 免费 | 仅靠随机高位端口 + 应用层 Host 校验 | 🔴 源站 IP 一旦泄露即被绕过 CDN 直连,且可伪造 `EO-Client-IP`/`CF-Connecting-IP` 绕过限流/封禁。若仍要走,需先做代码加固:让 ip.go 只信任来自 `trusted_proxies`/回环对端的 CDN 头(另行评审) | - -**防火墙(ufw 示例,云安全组同理;按 A 或 B 得到的段列表执行):** -```bash -ufw default deny incoming -ufw allow from to any port 18080 proto tcp -ufw allow from to any port 18080 proto tcp -# …全部 EO 段放行完毕后,确认没有 0.0.0.0/0 放行 18080 -# ssh 等管理端口勿对公网裸开(建议仅内网/跳板) -``` - -**验证「直连被拒」**:用一台与 EO 无关的外网 VPS 执行 `nc -zv <服务器IP> 18080`,应 `refused / timeout`;随后通过 EO 加速 URL 访问,确认回源返回 200。 - -**说明**:cloudflared(Tunnel)不受本次开端口影响——它始终是源站**主动出站**连 Cloudflare,不依赖任何入站规则。 - -### 2.3 DNS 分线路改造(权威在 DNSPod,直接配置;**无需任何委派**) - -> **2026-09-03 三次更正后的最终认知(12:10 实测确认)**:注册局 RDAP 铁证 `tama.guru` 权威 NS = `byron.dnspod.net` + `sunshine.dnspod.net`(DNSPod),Cloudflare 从未托管此 zone。DNSPod 面板的**三线路配置(境内→EO / 境外→CF / 默认→CF)已确认生效**(境外服务器 API 实测 → CF;境内通道实测 → EO)。本节仅需**核对面板与下表一致**;不一致时按下表改。 - -#### 第 1 步:核对 DNSPod 域名列表(关键,避免改错地方) - -登录腾讯云解析 DNSPod → 域名列表,确认: -- ✅ 存在 **`tama.guru`**(整域 zone,权威 NS = byron/sunshine.dnspod.net 所在); -- ⚠️ **若还存在一个独立的 `box.tama.guru` 子域 zone**(之前按旧方案误建的)——它不生效且会造成混淆:在 DNSPod 删除它(或忽略它,把配置改到 tama.guru 整域下); -- ⚠️ 确认不是登录错账号/域名在另一个 DNSPod 账号下。 - -> 判别方法:DNSPod 面板上 tama.guru 域名详情页底部会显示「NS 记录:byron.dnspod.net / sunshine.dnspod.net」;若显示的不是这组,说明域名不在当前账号。 - -#### 第 2 步:在 tama.guru 整域 zone 里配置 box 记录(主机记录填 `box`) - -进入 `tama.guru` → 记录管理,找到/新建主机记录 `box`,**分线路三行**: - -| 线路 | 类型 | 记录值 | -|---|---|---| -| **中国境内** | CNAME | `box.tama.guru.eo.dnse4.com`(EdgeOne CNAME 目标,2.1 控制台核对) | -| **境外** | CNAME | **CF SaaS CNAME 目标**(见下) | -| **默认** | CNAME | 同境外(兜底,覆盖未识别线路) | - -- ✅ **用户面板实际状态(2026-09-03 12:07 核对)已符合上表,无需改动**:默认 → `cloudflare.182682.xyz`(2025-11-20 建)、境外 → `cloudflare.182682.xyz`(10:54 建)、境内 → `eo.dnse4.com`(11:29 建),NS = byron/sunshine.dnspod.net。若你的面板只有单条「默认 CNAME → EO」,则把它**编辑为线路=中国境内**,再新增境外/默认两条指向 CF。 -- CF SaaS CNAME 目标获取:CF 控制台 → zone `337845818.best` → **SSL/TLS → Custom hostnames**(for SaaS)→ `box.tama.guru` 详情里的 **CNAME target**(形如 `.cdn.cloudflare.net`)。若用第三方「优选 IP」域名,填你的优选 CNAME 亦可(保证解析到 CF 边缘)。 -- TTL:`120`~`600`(便于回滚)。 -- ⚠️ DNSPod 免费版**支持**境内/境外/默认分线路(基础分线路免费;运营商级细分才收费)。 - -#### 第 3 步:⚠️ EO 归属验证方式的适配(重要,防止证书续期失败) - -- EO 站点若靠「主记录 CNAME → EO 目标」自动验证归属,分线路后**境外/默认线路不再返回该 CNAME**,EO 周期性校验会失败。 -- 对策:EO 控制台把验证方式改为 **TXT 验证**(EO 会给一条形如 `_.box.tama.guru → TXT <值>` 的验证记录),把这条 TXT **加到 DNSPod 的 tama.guru zone**(主机记录 `_`,线路=默认,全球可见);box 的境内线路 CNAME 保持指向 EO 不动。 - -#### 第 4 步:验证分线路(⚠️ 大陆本机 nslookup 境外 DNS 不可信) - -大陆本机向境外公共 DNS(8.8.8.8 / 9.9.9.9 / OpenDNS 等)的查询会被网络劫持/污染,**必然假返回境内线路的 EO**——勿用它判断境外线路。正确方法: - -```bash -# 境内视角(本机即可,通道可信) -nslookup box.tama.guru 223.5.5.5 # 期望 → eo.dnse4.com / 43.174.x(EO) -curl -s "https://dns.alidns.com/resolve?name=box.tama.guru&type=A" # 阿里 DoH(HTTPS),同上 -``` - -```text -# 境外视角:用境外服务器 API(浏览器打开即可,2026-09-03 已实测) -# https://api.hackertarget.com/dnslookup/?q=box.tama.guru -# https://networkcalc.com/api/dns/lookup/box.tama.guru -# 期望:CNAME cloudflare.182682.xyz → 104.x / 172.x(CF 边缘 IP) -``` - -境内 EO + 境外 CF = 分线路成功(2026-09-03 12:10 已按此确认)。若在境外 VPS / 境外网络上,直接 `nslookup box.tama.guru 8.8.8.8` 亦可。 - -#### 回滚 -把 box 的境外/默认线路 CNAME 改回(或删除,只留境内→EO)即回到全量 EO;或把三条都指向 CF 即全量 CF。分钟级生效,见 §5。 - -### 2.4 Cloudflare 侧(零改动——CF 没有 tama.guru zone,也无需有) - -- **无需在 CF 做任何 DNS 操作**:`tama.guru` 的解析权在 DNSPod,CF 控制台不会、也不应出现该 zone。之前「父 zone 委派」方案基于错误前提,**作废**。 -- **保持不动**:`337845818.best` zone 的 SaaS 自定义主机名 `box.tama.guru`(证书 + Tunnel 路由)、Tunnel 本身、承载域记录。它们只依赖「有流量带着 `Host: box.tama.guru` 到 CF 边缘」,与 DNS 记录归属无关——DNSPod 分线路把境外流量指到 CF 边缘 IP/CNAME 后,CF 按 Host 自动匹配到该自定义主机名并走 Tunnel 回源。 -- **可选纵深**:在 `337845818.best` zone 加一条 Transform Rule「删除请求头 `EO-Client-IP`」——防海外用户经 CF 时把伪造 EO 头带入源站。当前代码因「CF-Connecting-IP 优先」已天然免疫,此规则属可选项。 -- **无环路风险**:EO 回源目标是你的源站 IP,不会指回 CF SaaS 域名;CF 分支回源走 Tunnel(出站),互不干扰。 - -### 2.5 应用配置 - -`conf/app.ini`: -```ini -[server] -port = 18080 -listen = 0.0.0.0:18080 ; EO 直连需要公网可达(纯 Tunnel 时期是 127.0.0.1:port,接 EO 后必须改) - -[security] -trust_proxy = true ; 信任 EO-Client-IP / CF-Connecting-IP -``` -无需维护 CDN 大 CIDR 清单(安全闭环依赖 2.1~2.2 的防火墙锁定 + 头剥离前提;免费版走观察法方案 B 时同理,前提是端口只对观测到的 EO 来源开放)。 - ---- - -## 3. 头信任矩阵(安全模型核对) - -| 流量路径 | 到达源站的相关头 | 应用采用 | 伪造风险与对策 | -|---|---|---|---| -| 大陆:用户 → EO → 源站 | `EO-Client-IP`(EO 开启后写入真实客户端 IP);客户端自带的假 `CF-Connecting-IP` 被 EO 规则删除 | `EO-Client-IP` | 若 EO 未开启该功能或未删 CF 头 → 应用会拿到空/伪造 IP,**此为前提 2、3,必须配置** | -| 海外:用户 → CF 边缘 → Tunnel → 源站 | `CF-Connecting-IP`(CF 边缘以观测值重写);客户端伪造的 `EO-Client-IP` 也被透传但被忽略 | `CF-Connecting-IP` | CF 重写不可伪造;代码 CF 优先故天然免疫 | -| 攻击者假设绕过防火墙直连 | 可自带任意头 | — | 防火墙拒绝,到不了应用;纵深:回源 HOST 校验 + 可选 Token | - ---- - -## 4. 上线验证清单 - -| # | 检查项 | 通过标准 | -|---|---|---| -| 0 | DNS 分线路 | 境内视角(本机):`nslookup box.tama.guru 223.5.5.5` → EO(eo.dnse4.com / 43.174.x);境外视角:境外服务器 API(hackertarget / networkcalc,方法见 2.3 第 4 步)→ CF(104.x)。⚠️ 大陆本机 `nslookup 8.8.8.8` 会被劫持假返回 EO,不可用 | -| 1 | 境内访问 | 大陆节点访问 `https://box.tama.guru` 正常,浏览器证书为 EO 签发;源站/EO 访问日志中客户端 IP = 你的大陆公网出口 IP(说明 `EO-Client-IP` 生效) | -| 2 | 海外访问 | 海外访问正常,日志客户端 IP = 海外出口 IP(CF-Connecting-IP 生效) | -| 3 | 直连封锁 | 外网 VPS `nc -zv <回源端口>` 拒绝/超时 | -| 4 | 伪造头 | 境内客户端手动带 `CF-Connecting-IP: 1.2.3.4` 请求,日志仍记录真实 IP(EO 删除规则生效) | -| 5 | 限流/封禁联动 | 用 EO 路径触发限流,确认按真实 IP 计数而非 EO 节点 IP | -| 6 | WebSocket/上传 | 若用到 WS:EO 站点需显式开启 WebSocket(CF 侧 Tunnel 原生支持);大体积上传实测通过 | -| 7 | 证书 | 大陆/海外浏览器均无证书告警 | - ---- - -## 5. 回滚顺序 - -1. **快速回滚(恢复访问)**:DNSPod 里把 box 的「境外/默认」线路临时指向 EO 目标(或全线路都指向 EO)→ 全球走 EO,分钟级生效; -2. **彻底回滚(回到纯 CF 时代)**:DNSPod 里把 box 三条线路统一改为 `CNAME → <原 CF SaaS 目标 / 承载记录>`(全球走 CF); -3. 确认 CF 路径正常后,再逐步收尾:关 EO 客户端 IP 头、撤防火墙放行、删除 EO 站点。 -4. ⚠️ 回滚后 EO 归属验证记录(若已按 2.3 第 3 步迁为 TXT)保持在 DNSPod 即可随线路变化继续有效;若 EO 站点还保留,按 EO 控制台提示重新验证。 - ---- - -## 6. 合规与风险提示 - -- 未备案域名面向大陆提供服务:无大陆节点(合规红线决定了这一点),且存在被运营商/监管按未备案域名处置(如阻断)的风险。本方案仅为技术可达路径,业务风险需自行评估。 -- 若日后完成备案:EO 加速区域可改「全球可用区」(含大陆节点),大陆延迟进一步下降,DNSPod 分线路结构无需变动,只改 EO 区域并复验。 -- `.guru` 等新顶级域在大陆解析/合规环境的稳定性低于 `.com`,如遇解析异常优先查 DNS 污染与线路命中。 - ---- - -## 7. 常见问题 - -- **`EO-Client-IP` 一直没出现?** 功能默认关闭且可自定义头名,需在 EO 侧开启并确认名称一字不差;另外只有走 EO 的流量才会带此头。 -- **从大陆本机 `nslookup box.tama.guru 8.8.8.8` 返回 EO,是不是境外线路没生效?** ⚠️ **不是,别被误导**——大陆本机发往境外公共 DNS 的查询会被网络劫持/污染,应答按境内线路给出(EO),UDP 与 TCP 均如此。境外线路是否生效要用**境外服务器视角**验证:浏览器打开 `https://api.hackertarget.com/dnslookup/?q=box.tama.guru` 或 `https://networkcalc.com/api/dns/lookup/box.tama.guru`,返回 `cloudflare.182682.xyz` → 104.x 即境外→CF 生效(2026-09-03 已实测确认)。若境外 API 真返回 EO,才需要回 DNSPod 面板核对 box 记录三行是否齐全、线路字段是否正确(境内→EO / 境外→CF / 默认→CF)、是否误建了未委派的 `box.tama.guru` 子域 zone。 -- **DNSPod 面板该核对哪些字段?** ① `box` 记录应有三行且线路分别是:境内(值 `box.tama.guru.eo.dnse4.com`)、境外(值 CF 优选 `cloudflare.182682.xyz` 或 CF SaaS CNAME target)、默认(同境外);② 域名列表只有 `tama.guru` 整域 zone(权威 = byron/sunshine.dnspod.net,域名详情页可见 NS),不应有独立的 `box.tama.guru` 子域 zone;③ 权威归属可用注册局 RDAP 自查(`https://rdap.org/domain/tama.guru` 应返回 byron/sunshine.dnspod.net)。 -- **HSTS 报错「无法访问…使用了 HSTS」?** 应用在 ExternalURL 以 https 开头时对响应下发 `Strict-Transport-Security`(1 年,includeSubDomains),浏览器在 HTTPS 时代已记录。切 CDN 后若边缘证书与域名不匹配(如 EO 返回兜底证书 `*.cdn.myqcloud.com`)即触发此页。根因是**新 CDN 证书尚未签发/部署**,等证书正确即自动恢复(域名不在 preload 列表时无需清浏览器);排查命令:`echo | openssl s_client -connect box.tama.guru:443 -servername box.tama.guru \| openssl x509 -noout -subject`,subject 应含 `box.tama.guru`。 -- **只放行 EO 段后业务断了?** 大概率回源 IP 段列表不全或已更新:先临时放行全段定位,再按最新清单收紧(付费档以官方源站防护通知为准;免费观察法每月自查来源 IP)。 -- **找不到回源 IP 段列表?** 那是「源站防护」功能里的展示项,而**免费套餐不支持启用**——见 2.2「回源网段三选一」。 -- **服务器太差不想跑 nginx,直接让服务监听公网端口行不行?** 行,而且推荐这么做:TamaBox 自带 HTTP 服务器,nginx 本就不是必需。只需把 `[server] listen` 改为 `0.0.0.0:port` 并让 EO 回源直连该端口;安全由防火墙网段白名单承担,与是否装 nginx 无关。 -- **应用出现 http 跳转 / 非 Secure Cookie?** EO 回源为 HTTP 时应用看到的是 http scheme,与 Tunnel 现状一致。若应用按 scheme 生成链接或下发 Secure Cookie 且行为异常,改为 HTTPS 回源或让应用信任 `X-Forwarded-Proto`。 -- **两家 CDN 缓存冲突?** TamaBox 为动态应用,两侧都保持默认不缓存 HTML/API,仅缓存 `static/` 资源。 diff --git a/outputs/edgeone-origin-whitelist-guide.md b/outputs/edgeone-origin-whitelist-guide.md deleted file mode 100644 index 87cb633..0000000 --- a/outputs/edgeone-origin-whitelist-guide.md +++ /dev/null @@ -1,299 +0,0 @@ -# EdgeOne 回源端口 iptables 白名单自动维护指南 - -> **交付物**:`scripts/edgeone-origin-whitelist.sh`(Linux / iptables + ipset) -> **适用**:TamaBox 源站对公网开放了 EO 回源端口(免备案双线架构的 EO 分支),想把「开放 IP + 端口」收敛为「仅 EdgeOne 可访问」。 -> **更新日期**:2026-09-03 - ---- - -## 0. 先泼一盆冷水:官方清单是付费功能 - -你要的「定期获取 EdgeOne IP 列表」——**免费套餐下官方不提供可程序化获取的渠道**: - -| 事实 | 说明 | -|---|---| -| 官方唯一清单来源 = 「源站防护 / Origin ACL」 | EO 控制台 → 安全防护 → 源站防护,启用后展示回源 IPv4/IPv6 网段,变更前 14/7/3/1 天通知 | -| **仅付费套餐支持** | 官方文档明确「origin protection 仅支持绑定付费套餐的站点,free edition 不支持」(腾讯文档 1552/76086)——这就是你在控制台找不到入口的原因 | -| 自动化 API 存在但同样付费 | `DescribeOriginACL`(teo.intl.tencentcloudapi.com,国际站),每 3 天低频轮询,`NextOriginACL` 非空即同步并调确认接口(文档 71118) | - -**所以本脚本的现实定位**:不依赖官方 API,以「本地/远程 CIDR 清单」为骨架,免费档立刻可用;日后升级付费套餐,只需把 API 拉取结果落到清单文件,脚本零改动继续工作(见 §7)。 - -> ### 🔴 0.1 2026-09-03 实测定稿:免费档请直接用「段级放行」,IP 级采种已证明不可行 -> -> 曾设计 learn/auto 从连接表逐个收集 EO 出口 IP 作白名单,**服务器实测(tcpdump 取证)否决了该路线**: -> -> - EO 回源出口是**节点池,规模数百上千**,且**同一时刻在多段轮换**——一次 23 秒采样就看到 13 个不同出口(分布在 `43.146/43.152/43.168/43.174/43.175` 五段),其中 9 个因不在白名单被 REJECT → 表现为境内**间歇性 521**(EO 边缘回源失败)。 -> - 结论:**IP 级清单 = 打地鼠**,永远追不上。免费档唯一现实可行方案是**按腾讯云大段放行**(脚本原生支持 CIDR,无需任何改动): -> -> ```bash -> # /etc/edgeone-origin/cidr.txt(2026-09-03 实测生效配置,521 消失) -> 43.146.0.0/16 -> 43.152.0.0/16 -> 43.168.0.0/16 -> 43.174.0.0/16 -> 43.175.0.0/16 -> ``` -> -> - learn/auto 在段级模式下**降级为「新段预警」辅助**:auto 采到清单外的 IP → `whois` 反查归属 → 若属腾讯云 EO 新段,把整段 `/16` 补进清单。首次采种不再需要 learn(§3.1 的逐 IP 采种流程整体退役)。 -> - 代价说明:`/16` 段内任意腾讯云主机可直连该回源端口(比全开安全、比精确 ACL 松),对 TamaBox 这类自身带鉴权的应用风险中等、可接受(§8)。 - ---- - -## 1. 脚本做什么 - -把回源端口从「全网开放」收敛为白名单: - -``` -INPUT 链(默认仅 2 条规则,其余不动) - ├─ -p tcp --dport 18080 -i lo -> ACCEPT ← cloudflared Tunnel 本机回连(回环放行) - └─ -p tcp --dport 18080 -i eth0 -> EO_IN ← 仅公网网卡 eth0 入站才判定 - EO_IN 自建链(一次性建立,内容恒定) - ├─ EO_ORIGIN 集合命中 -> ACCEPT ← EdgeOne 回源(白名单) - └─ 其余 -> REJECT tcp-reset ← 直连者秒断 -(网卡名不是 eth0 请改脚本头 EO_IFACE;留空=不限网卡,回环由 EO_IN 内 127.0.0.0/8 兜底) -``` - -**两个关键设计,保证不会把自己锁死、不会误伤 CF 分支:** - -1. **更新零窗口**:白名单存于 ipset 集合 `EO_ORIGIN`,更新走「临时集合填充 → `ipset swap` 原子替换 → 销毁临时集」;iptables 规则永远引用同名集合、从不重建。同步过程中不存在「规则被清空」的瞬间。 -2. **cloudflared 不受影响**:Tunnel 是 cloudflared 从本机 `127.0.0.1` 连回源端口,INPUT 里有独立的 `-i lo` ACCEPT 放行回环(与 eth0 网卡限定解耦),所以**即使防火墙收紧,CF 分支依然正常工作**——这正是裸 iptables 白名单最容易踩的坑(有人把 18080 一刀切 REJECT,Tunnel 分支跟着全挂)。 - -其余保障:清单为空/拉取失败/全非法 → **拒绝更新、保留现网规则**(fail-safe);支持 `--dry-run` 演练;全程日志 `/var/log/eo-origin-whitelist.log`。 - ---- - -## 2. 部署前确认两件事 - -1. **确认你的实际回源端口**(脚本默认 `18080`,务必改成实际值): - ```bash - ss -tlnp | grep -iE 'tamabox|LISTEN' # 找 TamaBox 监听的端口 - ``` - 或看 `conf/app.ini` 的 `[server] port`。改脚本文件头 `EO_PORT=18080` 那一行,或运行时 `sudo EO_PORT=xxxx ./edgeone-origin-whitelist.sh sync`。 - -2. **确认系统有 ipset**(白名单原子替换依赖它): - ```bash - command -v ipset || sudo apt-get install -y ipset # Debian/Ubuntu - command -v ipset || sudo dnf install -y ipset # RHEL/CentOS/Fedora - ``` - -3. **确认公网网卡名**(脚本默认只对 `eth0` 生效): - ```bash - ip route get 1.1.1.1 | awk '{print $5; exit}' # 输出如 eth0 / ens3 / enp1s0 - ``` - 输出不是 `eth0` 时,改脚本头 `EO_IFACE=` 或运行时覆盖:`sudo EO_IFACE=ens3 ./edgeone-origin-whitelist.sh sync`。⚠️ 网卡名不对 → 跳转规则永不命中 → 端口等于裸奔。 - ---- - -## 3. 初始化:生成种子清单(免费档必经步骤) - -> 🔴 **先读 §0.1(实测定稿)**:免费档**不要**再做逐 IP 采种(EO 出口是海量轮换节点池,IP 级白名单实测导致间歇 521)。直接按 §0.1 的五个 `/16` 段建清单即可;learn/auto 只留作「新段预警」,见 §3.1 末注。 - -### 3.1 首选:腾讯云大段放行(实测定稿) - -按 §0.1 直接放行 EO 所属的腾讯云大段(免费档没有官方回源段 API,而逐 IP 采种已被实测否决)。清单内容(2026-09-03 服务器实测生效): - -```bash -sudo mkdir -p /etc/edgeone-origin -# 每行一个 CIDR,# 开头为注释。以下为实测生效配置: -sudo tee /etc/edgeone-origin/cidr.txt >/dev/null <<'EOF' -# EdgeOne 回源段(腾讯云,2026-09-03 实测;境内 EO 分支访问验证正常) -43.146.0.0/16 -43.152.0.0/16 -43.168.0.0/16 -43.174.0.0/16 -43.175.0.0/16 -EOF -``` - -> 段怎么来的:用 `auto`(或 tcpdump 抓 SYN)持续观察回源出口,把出现过的 IP 归并到所属腾讯云段(whois 查 ASN/归属确认),整段放行。上面五个 `/16` 来自多次实测采样(新加坡/腾讯云 EO 出口遍布这些段)。 -> -> 🔴 段不是永恒不变的:EO 可能启用清单外的新段(罕见、以月计)。每月跑一次 `auto` 看采样里有没有清单外的 IP,`whois ` 确认属腾讯云 EO 后把整段补进 cidr.txt 再 sync(详见 §4.4)。 - -### 3.2 备选:向 EO 索取回源段 - -免费档控制台无源站防护页面,但可尝试**提交工单索取当前回源 IP 段**(客服可能以"尽力提供"形式给出);拿到后把 CIDR 逐行放进清单文件即可,效果等同。日后升级付费套餐启用源站防护,官方段可直接用(§7)。 - -### 3.3 首次启用(务必先 dry-run) - -```bash -# 1. 建清单文件 -sudo mkdir -p /etc/edgeone-origin -sudo cp 你整理好的清单 /etc/edgeone-origin/cidr.txt # 每行一个 IP/CIDR,# 注释 - -# 2. 演练:只看将要执行的动作,不触碰系统 -sudo ./edgeone-origin-whitelist.sh sync --dry-run - -# 3. 正式启用 -sudo ./edgeone-origin-whitelist.sh sync -``` - -启用后**立刻验证**(另开一个 SSH 或终端,别把自己关外面): - -```bash -# 服务器本机验证(必须能通:Tunnel 回环放行) -curl -sI --max-time 5 http://127.0.0.1:18080/ | head -3 - -# 从一台"与 EO 无关"的外网机器验证(必须被拒) -nc -zv <你的服务器IP> 18080 # 期望 refused 或超时 - -# 经 EO 访问验证(必须能通) -# ⚠️ 必须用【境内网络】访问——境外访问会被 DNSPod 解析到 CF 分支(走 Tunnel 回环), -# 验证到的是 CF 而不是 EO!境内手机 4G 访问最方便 -curl -sI --max-time 10 https://box.tama.guru/ | head -3 -``` - -### 3.4 撤掉原来的全局放行 - -若你之前是用 ufw 放行的(`ufw allow 18080`),现在它已冗余(脚本的 REJECT 会先于它拦截非白名单),建议删掉避免困惑: - -```bash -sudo ufw delete allow 18080 # 脚本 sync 时也会检测并提示 -``` - -> ⚠️ 云厂商安全组若也是「全放行 18080」:本机 iptables 白名单已是最终防线,直连者到本机 INPUT 即被拒,**不强制改安全组**;但建议安全组层面至少把 18080 的源收敛为 EO 大段(升级付费拿到官方段后可做),纵深更稳。 - ---- - -## 4. 定时维护(核心诉求) - -### 4.1 清单会变吗? - -EO 官方口径:**回源网段平均 3–6 个月更新一次**,变更前 14/7/3/1 天通知。付费档启用源站防护后段变更有 API 兜底(§7);免费档请按 §7 的「工单/客服」或定期(建议每月一次)核对。 - -### 4.2 用 cron(最简单) - -段级模式(§0.1)下清单是 5 个 `/16` 段、几乎不变 → 定时只需**周期 sync 兜底 + 月度人工归段核对**,auto 不再作日常主线(改为按需「新段预警」观测,见 4.4): - -```bash -sudo crontab -e -# ★ 推荐:每 3 天一次 sync(全量重建:清单里没有的旧条目自动剔除) -17 4 */3 * * /usr/local/bin/edgeone-origin-whitelist.sh sync >> /var/log/eo-origin-whitelist.log 2>&1 -# 想更保险可改每天:把 */3 改成 *(即 17 4 * * * …)——sync 开销秒级,零负担 -# 新段预警观测(不设定时,需要时手动跑): -# sudo /usr/local/bin/edgeone-origin-whitelist.sh auto -# 输出「新增」→ whois 确认属腾讯云新段 → 整段写进 cidr.txt → 再 sync -``` - -把脚本放到固定路径:`sudo cp scripts/edgeone-origin-whitelist.sh /usr/local/bin/ && sudo chmod +x /usr/local/bin/edgeone-origin-whitelist.sh` - -### 4.3 用 systemd timer(更规范,推荐) - -`/etc/systemd/system/eo-origin-whitelist.service`: - -```ini -[Unit] -Description=Sync EdgeOne origin whitelist -After=network-online.target - -[Service] -Type=oneshot -ExecStart=/usr/local/bin/edgeone-origin-whitelist.sh sync -``` - -推荐**每 3 天一次**(段级清单近乎静态,sync 纯兜底): - -```ini -# A. 日历式(推荐):每月 1/4/7/10/13/16/19/22/25/28/31 号 04:17 -# /etc/systemd/system/eo-origin-whitelist.timer -[Unit] -Description=EdgeOne origin whitelist sync (3-day) - -[Timer] -OnCalendar=*-*-1/3 04:17:00 -Persistent=true - -[Install] -WantedBy=timers.target -``` - -> 想每天跑:`OnCalendar=*-*-* 04:17:00`;想严格 72h 间隔:`OnBootSec=5min` + `OnUnitActiveSec=3d`。 - -### 4.4 失效的条目会自动剔除吗?(段级模式) - -**会——且已内建,不需要任何额外参数。** 关键是理解 sync 的语义: - -| 你可能以为 | 实际机制 | -|---|---| -| sync 是把清单「增量添加」,旧条目越积越多 | ❌ 错。sync 是**全量重建**:每次按当前清单文件从零填充临时集合 → `ipset swap` 原子替换 → 旧的 `EO_ORIGIN` 整个被新集合顶掉 | - -所以**只要某段/某 IP 不在「当前清单文件」里,下一次 sync 它就自动消失**——剔除是 sync 的默认行为,零残留。反证见 §5:「临时 `ipset add` 放行的 IP,下次 sync 就没了」正是同一机制。 - -段级模式下清单近乎静态(5 个 `/16`),日常维护极简,只剩一件事:**防 EO 启用清单外新段**。 - -```bash -# ① 月度新段预警:跑一次 auto(或 tcpdump 抓 SYN),看采样里有没有清单外的 IP -sudo ./edgeone-origin-whitelist.sh auto -# auto 会把「清单外的新 IP」增量合并进 cidr.txt(只增不删),并提示新增了哪些 -# ② 归段:whois <新IP>(/ ipinfo.io/),确认属腾讯云 EO(Tencent/EdgeOne 字样) -# ③ 把新 IP 归并成所属整段后写入清单(若 auto 已把裸 IP 加进 cidr.txt,可顺手整理成段): -sudo nano /etc/edgeone-origin/cidr.txt # 示例:43.175.x.x → 加一行 43.175.0.0/16 -sudo ./edgeone-origin-whitelist.sh sync # 立即生效;不手动跑则下次定时 sync 也会生效 -``` - -三个要点: - -1. **别删到清单为空**:fail-safe 会让 sync 拒绝执行(宁可保留旧白名单,不可裸奔)。 -2. **旧段留在清单里没有安全风险**:那些地址属于腾讯云,不会"变成攻击者",顶多白名单略宽(/16 级本就宽);真正要紧的只是**新段漏加 → 偶发 521**(见 §8 免费档清单质量)。 -3. **想彻底全自动**(连月度核对都不想做):升级付费套餐启用「源站防护」,把 `DescribeOriginACL` 输出定时写入 cidr.txt(§7)——清单每次都是官方最新全集,sync 自动带上新段、自动剔除已退出的段,脚本零改动。 - ---- - -## 5. 常用操作速查 - -```bash -sudo ./edgeone-origin-whitelist.sh sync # 周期入口(定时任务用这个,段级清单全量重建) -sudo ./edgeone-origin-whitelist.sh auto # 新段预警:采集当前回源 IP → 合并进清单 → 有新增即 sync -sudo ./edgeone-origin-whitelist.sh learn # 看当前谁在回源(即时观测,不必用于日常维护) -sudo ./edgeone-origin-whitelist.sh show # 当前白名单条目 + iptables 规则 -sudo ./edgeone-origin-whitelist.sh sync --dry-run # 演练 -sudo ./edgeone-origin-whitelist.sh flush # 回滚:移除本脚本全部规则/集合 -tail -20 /var/log/eo-origin-whitelist.log # 看更新历史 -``` - -**临时放行某个 IP**(不用改清单,立即生效;下次 sync 会随清单重建而消失——想持久就写进清单文件): -```bash -sudo ipset add EO_ORIGIN x.x.x.x -``` - -**手动移除某个 IP**: -```bash -sudo ipset del EO_ORIGIN x.x.x.x -``` - -**修改回源端口**(低概率操作):改 `EO_PORT` 后先 `flush` 再 `sync`(避免 INPUT 残留旧端口跳转)。 - -**修改生效网卡**:改 `EO_IFACE`(或 `sudo EO_IFACE=ens3 ./edgeone-origin-whitelist.sh sync`)后直接 sync 即可——从「不限网卡」升级为「限定网卡」时会自动删除旧的无限定跳转;改完用 `show` 确认 INPUT 规则带 `-i <网卡>`。 - ---- - -## 6. 排错 - -| 现象 | 原因与处理 | -|---|---| -| sync 报「清单为空…保持现网白名单不变(fail-safe)」 | 正常保护:空清单绝不上线。成因=清单文件不存在 / 内容全非法 / 无有效条目。先用 `learn` 采种并人工审阅后写入 `/etc/edgeone-origin/cidr.txt`(裸 IP 每行即可,已支持直接入清单);脚本报错会自带下一步指引。若真配了 `EO_CIDR_URL` 且拉取失败 + 本地无条目,会单独报「来源全部失败」 | -| 收紧后 CF 分支(海外)访问挂 | 几乎肯定是 cloudflared 连的不是 `127.0.0.1`。确认 Tunnel 配置里 service 指向 `http://127.0.0.1:PORT`(脚本对 127.0.0.0/8 放行;若 cloudflared 在**另一台机器**则需把那台机器出口 IP 加进白名单——但那属于另一架构,需单独设计) | -| 经 EO 访问 502/回源失败(境内间歇性 521) | ① 端口填错(脚本拦的不是 EO 回源那个端口);② 🔴 **清单太细**——EO 回源出口是海量轮换节点池(实测 23 秒 13 个出口、分布多个 /16),逐 IP 白名单必然间歇漏放 → EO 报 521。解法=按 §0.1 放行腾讯云大段(43.146/43.152/43.168/43.174/43.175 的 /16,2026-09-03 实测 521 消失);③ 服务器重启后 ipset/iptables 规则丢失 → 见下 | -| sync 报 `Set EO_ORIGIN doesn't exist` | 🔴 iptables-nft 要求「被引用的 ipset 集合必须先于规则存在」。旧版脚本先建链(内含引用集合的规则)后建集合,**首次运行必失败**。已修复(集合创建挪到建链之前);旧版升级后重跑 `sync` 即可自愈 | -| sync 报 `RULE_APPEND failed (Invalid argument)` 且 EO_IN 链**缺 REJECT 兜底行**(只剩 127/集合两条 ACCEPT) | 🔴 iptables-nft 下 `--reject-with tcp-reset` **必须带 `-p tcp`**(nft 的 reject 语句需确认匹配 TCP 才能构造 RST,无 -p 直接 EINVAL)。旧版 REJECT 行无 `-p tcp` 必然失败 → 链尾无兜底 → **非白名单直连仍通,白名单形同虚设**(集合/127 两条 ACCEPT 无此限制,能正常加入)。已修复(REJECT 加 `-p tcp`);部署新版后重跑 `sync`,验证 `iptables -S EO_IN` 三条齐全:`-s 127.0.0.0/8 ACCEPT` / `-m set --match-set EO_ORIGIN src ACCEPT` / `-p tcp ... REJECT --reject-with tcp-reset` | -| 服务器重启后白名单没了 | ipset 与 iptables 规则是内存态。用 `iptables-persistent` + `ipset-persistent`(Debian/Ubuntu),或把 sync 加进开机自启(systemd timer `Persistent=true` 只补跑错过的任务,不保证开机即跑——建议加一条 `@reboot` cron 或 oneshot service 开机执行一次 sync) | -| 直连还是能通 | 先 `iptables -S INPUT` 看是否还有更靠前的全局 ACCEPT 匹配 18080;`ufw status` 若显示 allow 18080 先删;云安全组全放行但本机已 REJECT 的情况下直连应被拒——若仍通说明规则没插在 INPUT 最前或被其它链绕过(如 nftables 后端混用,`iptables -S` 与实际生效不一致时改用 `nft list ruleset` 排查) | -| auto 立即结束、没等 `EO_SAMPLE_SECONDS` 窗口(日志同秒出现「采样到 0 个」) | 🔴 旧版 tcpdump filter 误写为 `"... and 'tcp[tcpflags] & tcp-syn != 0'"`——双引号内的单引号是**字面字符**,会原样传给 tcpdump,pcap 编译器不认识引号 → syntax error **秒退**(手动执行时单引号由 shell 吃掉,脚本里没这一步);且旧版 `awk '{print $2}'` 取到的是协议名 `IP` 而非源地址(源在 `>` 前一字段)。两个缺陷叠加 = 抓了也白抓、还秒退。已修复(filter 去引号 + 按 `>` 前字段取源 IP,窗口会真实阻塞)。部署新版后:`sudo edgeone-origin-whitelist.sh auto` 应看到「tcpdump 采样 Ns … 请稍候」并等待 N 秒;`EO_SAMPLE_SECONDS` 非数字会打印提示并跳过 | -| learn 输出为空 | ① **没有活跃回源连接**——EO 回源是短连接、错过即无,且**必须境内网络访问才走 EO 路径**(境外访问走 CF Tunnel,对端 127.0.0.1 会被脚本过滤,永远为空);② 端口核对:`ss -tlnp \| grep LISTEN` 确认 TamaBox 监听端口 = 脚本 `EO_PORT`;③ 访问后 60 秒内 `ss -Htan \| grep :<端口>` 看 TIME-WAIT 痕迹;④ 最稳:`tcpdump -ni <网卡> tcp port <端口> and 'tcp[tcpflags] & tcp-syn != 0'` 抓 SYN——触发一次境内访问,出现的源 IP 即 EO 回源 IP。新版脚本空结果时会自带以上提示;`auto` 的采样含 conntrack(窗口比 learn 宽),仍采不到时排查思路同上 | - ---- - -## 7. 升级付费套餐后:接官方 API 全自动(零脚本改动) - -启用「源站防护」拿到 ZoneId 后,可用腾讯云 Python SDK / tccli 周期调用 `DescribeOriginACL`,把返回的 `CurrentOriginACL.EntireAddresses.IPv4[]` 写入 `/etc/edgeone-origin/cidr.txt`,本脚本照常 sync 即可——**脚本无需任何改动**(它只认"每行一个 CIDR 的文件")。 - -> 也可让脚本直接拉取:把 API 结果发布到一个私有 HTTP URL,配置 `EO_CIDR_URL=...`(文件 + URL 会取并集)。注意变更时先核对 `NextOriginACL` 计划生效时间,建议同步时机放在 `PlannedActiveTime` 前完成,避免 EO 强制切换后回源失败。 - ---- - -## 8. 边界与已知限制 - -- **默认仅公网网卡 eth0**:白名单只判定 `EO_IFACE` 指定网卡的入站流量(默认 `eth0`);回环由独立的 `-i lo` ACCEPT 放行(供 Tunnel 本机回连),与网卡限定解耦。网卡名不是 eth0 时务必配置,否则跳转永不命中、端口裸奔。 -- **IPv6 天然无风险、无需任何 ip6tables 规则**:脚本只管理 IPv4;TamaBox 监听 `0.0.0.0`(仅 IPv4、无 AAAA),IPv6 上不存在该回源端口,v6 用户访问由 CDN 边缘走 IPv4 回源,一切正常。只有日后改为监听 `[::]:PORT` 并向 EO 开放 v6 回源,才需扩展 inet6 集合 + ip6tables 镜像(脚本头注释 5 有说明)。 -- **免费档清单质量(段级视角)**:清单是 5 个腾讯云 `/16` 段(§0.1),EO 出口怎么轮换都在段内 → 正常无感。唯一风险是 EO 启用清单外的**新段**(罕见、以月计)→ 表现为个别请求偶发 521;对策=月度跑一次 `auto` 看有无清单外新 IP,`whois` 确认归段后补段(§4.4)。这是免费档没有官方清单的固有代价,可接受则用,不可接受就升级付费档走 §7。 -- **脚本不触碰 ufw/云安全组**,只管理自己创建的链与集合;`flush` 也只删自己的东西,可放心回滚。 -- 本机为 1C1G 低配:脚本每次运行仅秒级、无常驻进程,ipset 查表为 O(1) 哈希,不影响转发性能。 diff --git a/outputs/eo-free-probe-2026-09-05.md b/outputs/eo-free-probe-2026-09-05.md deleted file mode 100644 index 1057b02..0000000 --- a/outputs/eo-free-probe-2026-09-05.md +++ /dev/null @@ -1,114 +0,0 @@ -# EO 优选方案 B 实测报告(免费替代 wetest.vip) - -日期:2026-09-05 | 结论状态:**候选已找到,待大陆多节点 itdog 验证后决定是否落地 DNS** -前提:方案 A(wetest.vip 会员活 IP,付费)用户已排除 → 本次全程免费实测 - ---- - -## 一、背景与目标 - -- 现状:box.tama.guru 国内走 EdgeOne(CNAME 恒定解析新加坡 43.174.246.43/247.43),大陆 itdog 3-11s -- 方案 B:不付费,自扫/公开 CNAME 找 **EO 网络内能服务 box.tama.guru 的亚洲近大陆边缘 IP**,改 dnspod 境内 A 记录直连 -- 区域不限香港,周边(日/韩/东南亚)比新加坡好即可 - -## 二、免费公开数据源现状(全部实测,均死) - -| 数据源 | 类型 | 实测结果 | -|---|---|---| -| wetest.vip/page/edgeone/address_v4 | 会员活 IP | 🔴 需登录/付费,公开页 0 IP(页面仅"登录可购买") | -| *.v1/v4.eo.fastlydns.cn(小周优选) | 免费 CNAME | 🔴 全 NXD(域名 NS 在但子域已删) | -| eo.072103.xyz | 免费 CNAME | 🔴 NXD | -| eo.088086.xyz | 免费 CNAME | 🔴 NXD | -| 蓝点网 43.174.150.199 等单 IP | 免费 IP | 🔴 证书已退回 CDN 通用池(*.cdn.myqcloud.com) | -| 雾创岛 43.174.150.150 等香港 IP | 免费 IP | 🔴 同上,全部失效 | - -> 印证:EO 优选 IP 时效性极强(月度级),2025 年社区免费源到 2026-09 已全部死亡。 -> 博客佐证(2026-02):"EdgeOne 现在不能使用优选节点,对套餐不适用的节点直接返回 418"。 - -## 三、自扫实测方法与结果 - -### 3.1 判定标准(关键) - -社区 2025 教程用"TLS 握手成功"判优选是**错误判据**(腾讯 CDN 泛播池也握手成功但证书是 *.cdn.myqcloud.com)。 -正确判据 = schannel 证书链校验通过(浏览器视角可访问)+ HTTP 200 + 响应头 Server: TencentEdgeOne。 - -### 3.2 扫描执行 - -- 段级抽样:43.174.0.0/16 + 43.175.0.0/16 全部 512 个 /24 × 6 IP = 3072 抽样 -- 全段细扫:官方段 + 社区段 21 个 /24 ≈ 5300 IP -- HK/JP 段细扫:56 个 /24 ≈ 14000 IP -- 工具:dev/eo-probe-v2.py(schannel 判定)、dev/eo-probe-ips.py、dev/eo-scan-all.py - -### 3.3 关键发现 - -**能服务 box.tama.guru 的 EO 边缘池(证书 CN=box.tama.guru)集中在 9 个 /24,全部新加坡:** -43.174.192-195、43.174.218、43.174.240-241、43.174.246-247(AS139341 ACE) - -**但 EO 泛播确实同步到全球**——非官方分配段也有零星节点可服务: -- 香港:43.175.115.x / 119.x / 43.174.126.x(8 个 IP) -- 东京:43.175.16/17/18/113.x(6 个 IP) -- 大阪:43.175.107.217 -- 阿姆斯特丹 43.175.185.x(5 个)、迪拜 43.174.152.63(远,无意义) - -> 说明:域名配置经 EO 泛播池同步到亚洲边缘,只是官方 CNAME 只调度到新加坡。 -> 这些 IP 均可作为境内 A 记录候选(证书/SNI/回源已验证 OK)。 - -## 四、大陆视角实测对比(南宁电信,真实大陆出口) - -### 4.1 动态页 /_/tamakyi TTFB(用户核心痛点指标) - -| 节点 | TCP connect | TTFB (3 次) | 评价 | -|---|---|---|---| -| **官方新加坡 43.174.247.43** | 180-186ms | 0.81 / 0.91 / 1.03s | 当前最优 | -| 香港 43.174.126.220 | 354-371ms | 1.29 / 1.47 / 1.80s | 更慢 | -| 东京 43.175.18.50 | 287-304ms | 1.07 / 1.19 / **4.02s** | 更慢+不稳 | -| 东京 43.175.113.139 | 292-294ms | 1.08 / 1.50 / 2.10s | 更慢 | -| 正常 DNS(现状) | 208-1060ms | 0.92 / 0.97 / 1.70s | 与官方一致 | - -### 4.2 TCP RTT 排名(南宁电信 443 端口 3-5 次中位) - -``` -180ms 43.174.247.43 官方新加坡 ← 最优 -276-299ms 东京 43.175.17/18/113/16.x -351-377ms 香港 43.175.115/119/43.174.126.x -``` - -### 4.3 结论:单点(南宁电信)实测官方新加坡反而最优 - -- ipinfo 标"香港/东京"的 EO 边缘,大陆电信实际路由质量**不如官方新加坡** -- 原因:官方 CNAME 的 43.174.246.43/247.43 是 EO 对大陆的优化调度;自扫边缘的跨境路由绕行 -- ⚠️ 注意:南宁电信 ≠ 全国。itdog 曾显示多地区 3-11s 慢,说明不同地区/运营商差异大。 - 香港候选在广州/深圳/福建等华南地区可能优于新加坡,需 itdog 多节点验证才能定论。 - -## 五、候选 IP 清单(供 itdog 大陆多节点验证) - -```bash -# 官方对照(当前 DNS) -43.174.246.43 43.174.247.43 -# 香港候选(8) -43.174.126.152 43.174.126.220 43.175.115.41 43.175.115.123 -43.175.115.149 43.175.115.202 43.175.119.174 43.175.119.214 -# 东京候选(6) -43.175.16.233 43.175.17.109 43.175.17.185 43.175.18.50 43.175.113.139 -# 大阪 -43.175.107.217(返回 400,慎用) -``` - -itdog 建议:URL 填 `https://box.tama.guru/robots.txt`(静态,可缓存对比), -对上述每个 IP 用"自定义 IP"模式分别测;若某香港/东京 IP 全国均值明显 < 新加坡,再改 DNS。 - -## 六、结论与建议 - -1. **免费公开源已全部死亡**(wetest 会员除外)——免费优选的时代窗口(2025 年中-2026 初)已关闭 -2. **自扫能力已具备**(脚本在 dev/),能稳定找出"当前仍服务 box.tama.guru 的 EO 边缘" -3. **南宁电信单点**:官方新加坡最优,候选不占优 → 但需 itdog 全国多点验证(华南可能反转) -4. 若 itdog 确认无候选优于新加坡 → 方案 B 免费路线到此为止,剩: - - 备案 + 大陆 CDN(治本,免费套餐获取难度降低但需实名+银行卡) - - 维持现状(静态已迁 snrat,动态页 ~1s TTFB 已是免备案架构上限附近) -5. 日常维护建议:若坚持 EO 优选,成本最低的是**订阅一个 ITDOG 会员自己做月度优选**(比 wetest 便宜且通用),而非固定买活 IP 列表 - -## 七、遗留 - -- dev/ 下新增 3 个扫描脚本可复用(改 --host 即可用于其他 EO 域名) -- 43.175.115.x 等香港候选在 check-host HK 节点延迟 2-60ms(真香港),仅大陆跨境路由绕 -- 建议优先 itdog 验证 43.175.115.202 / 43.174.126.220(check-host 证实真香港 PoP) diff --git a/outputs/feature-mode-2026-09-01.md b/outputs/feature-mode-2026-09-01.md deleted file mode 100644 index 4178144..0000000 --- a/outputs/feature-mode-2026-09-01.md +++ /dev/null @@ -1,47 +0,0 @@ -# 功能管理面板改进:长期开放模式 + 私聊上限下线 + 按钮同行(2026-09-01) - -## 需求与方案 - -| # | 需求 | 方案 | -|---|------|------| -| 1 | 缺少「长期开放」开关:设了时间/数量后服务到期即关,想要一直开放的服务没有直接入口 | 每个功能面板新增「收集模式」三选一(radio):**长期开放 / 定时收集 / 循环定时**;选「长期开放」保存后即不限时间,保持开放直到手动关闭(仍可叠加收集上限) | -| 2 | 私聊设置收集上限没有意义 | 私聊面板隐藏「收集上限」与「重置计数」;后端保存私聊时强制 `quota_limit=0`;启动期幂等清零历史残留的私聊上限/计数(`normalizeChatQuota`) | -| 3 | 「保存该功能设置」应与「重置计数」同一行 | 两按钮合并到同一 flex 行;重置按钮通过 HTML5 `form` 属性指向独立的 reset 表单(HTML 不允许表单嵌套),CSRF 各自保留 | - -## 改动明细 - -### 1. 收集模式三选一(`templates/user/feature-control-form.html` + `route/user/features.go`) - -- 模式取值:`always`(长期开放)/ `window`(定时收集,一次性开放/关闭窗口)/ `recurring`(循环定时,每周循环)。 -- 渲染时从已保存配置推断模式(`inferFeatureMode`):有循环定时 → recurring;有一性次时间 → window;否则 → always。 -- **模式互斥**:保存时只保留所选模式的字段,其余清空。这顺带修掉一个旧语义坑—— - 此前「一次性窗口 AND 循环定时」叠加,一次性关闭时间过后功能**永久结束**(硬边界), - 用户想恢复一直开放必须手动清空时间字段;现在切换到「长期开放」保存一次即可。 -- JS 按所选模式显隐「定时收集」/「循环定时」字段区(未选模式的字段仍提交,由后端按模式清理)。 -- 未携带 `mode` 的旧表单提交按字段推断兜底,保持兼容;`Evaluate` 判定逻辑零改动。 - -### 2. 私聊收集上限下线 - -- UI:`{{ if ne .Key "chat" }}` 条件隐藏配额输入、已收集行、重置按钮。 -- 后端:`FeaturesUpdate` 对 `feature=chat` 强制 `quota = 0`(防历史残留上限导致私聊"莫名关闭")。 -- 数据:新增 `internal/db/feature_control.go::normalizeChatQuota`,启动时把 - `feature='chat'` 且 `quota_limit>0 或 quota_count>0` 的行幂等清零(`db.go` 调用,失败仅告警)。 -- 文案:四语言字典更新 `admin.featureHint.chat`(去掉"达到上限后停止新建会话")。 - -### 3. 按钮同行 - -- `.feat-actions` 行内先「重置计数」(default 样式,仅非私聊面板)后「保存」(primary)。 -- 重置按钮 `form="feat-reset-{key}"` 指向表单外的独立 reset 表单,功能与原来完全一致。 - -## 其他改动 - -- i18n 四语言(zh-CN/zh-TW/en/ja)新增 `admin.featureMode`、`admin.featureModeAlways/Window/Recurring`、`admin.featureModeHint`。 -- `templates/render_test.go`:mock 结构体补 `Mode` 字段(question=always、chat=always、vote=recurring)。 -- `templates/user/profile.html`:新增 `.feat-modes` / `.feat-field-label` / `.is-hidden` 样式。 - -## 验证 - -- `gofmt -l -e` 全部改动文件 ✅ -- 全模块 overlay `go build` / `go vet` ✅(webp 屏蔽包用 `dev/webpstub` 桩 + `-overlay` 类型检查) -- 全部可运行测试通过(含 `templates` 面板渲染回归、`internal/i18n`)✅ -- 待办:Linux 环境最终 `go build ./...`(webp CGO 真实编译) diff --git a/outputs/icon-ailicili.png b/outputs/icon-ailicili.png deleted file mode 100644 index 4585eba..0000000 Binary files a/outputs/icon-ailicili.png and /dev/null differ diff --git a/outputs/icon-bili.png b/outputs/icon-bili.png deleted file mode 100644 index 9177056..0000000 Binary files a/outputs/icon-bili.png and /dev/null differ diff --git a/outputs/icon-blog.png b/outputs/icon-blog.png deleted file mode 100644 index 2ffb8e0..0000000 Binary files a/outputs/icon-blog.png and /dev/null differ diff --git a/outputs/icon-weibo.png b/outputs/icon-weibo.png deleted file mode 100644 index db9adeb..0000000 Binary files a/outputs/icon-weibo.png and /dev/null differ diff --git a/outputs/icon-xiaohongshu.png b/outputs/icon-xiaohongshu.png deleted file mode 100644 index 277e3a0..0000000 Binary files a/outputs/icon-xiaohongshu.png and /dev/null differ diff --git a/outputs/live-admin6.html b/outputs/live-admin6.html deleted file mode 100644 index 67f3377..0000000 --- a/outputs/live-admin6.html +++ /dev/null @@ -1,1064 +0,0 @@ - - - - - - admin6 - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - -
-
-
- -

admin6

-

问你想问的

-
- - - - - - - bloglink - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@admin6提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/live-default.html b/outputs/live-default.html deleted file mode 100644 index ac7c4f2..0000000 --- a/outputs/live-default.html +++ /dev/null @@ -1,1064 +0,0 @@ - - - - - - admin6 - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - -
-
-
- -

admin6

-

问你想问的

-
- - - - - - - bloglink - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@admin6提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/live-mobile.png b/outputs/live-mobile.png deleted file mode 100644 index 5e3c1b2..0000000 Binary files a/outputs/live-mobile.png and /dev/null differ diff --git a/outputs/live-sakura-1280.png b/outputs/live-sakura-1280.png deleted file mode 100644 index 1e4ceb3..0000000 Binary files a/outputs/live-sakura-1280.png and /dev/null differ diff --git a/outputs/live-sakura-dark.html b/outputs/live-sakura-dark.html deleted file mode 100644 index 5c86a60..0000000 --- a/outputs/live-sakura-dark.html +++ /dev/null @@ -1,1064 +0,0 @@ - - - - - - admin6 - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - -
-
-
- -

admin6

-

问你想问的

-
- - - - - - - bloglink - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@admin6提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/live-sakura-dark.png b/outputs/live-sakura-dark.png deleted file mode 100644 index 652fb1c..0000000 Binary files a/outputs/live-sakura-dark.png and /dev/null differ diff --git a/outputs/live-sakura.html b/outputs/live-sakura.html deleted file mode 100644 index 265710d..0000000 --- a/outputs/live-sakura.html +++ /dev/null @@ -1,1064 +0,0 @@ - - - - - - admin6 - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - -
-
-
- -

admin6

-

问你想问的

-
- - - - - - - bloglink - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@admin6提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/live-user-profile.html b/outputs/live-user-profile.html deleted file mode 100644 index 49cef07..0000000 --- a/outputs/live-user-profile.html +++ /dev/null @@ -1,2667 +0,0 @@ - - - - - - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
-

裁剪图片

-
-
-
- -
-

-
- -
-
- - -
- - - - - -
- - -
-
- - - -
-

基础资料

-
- - -
-
- - -
-
- -
- -
-
- - -
-
- -
- -
-
- - - 选择访客访问你的提问箱时显示的语言(后台页面跟随此设置) -
-
- - -
-
- -
-
- - -
-

头像与背景

-
-
- - - - 1:1 -
- - -
- 裁剪框已锁定为 1:1 正方形 -
-
- - - - 16:9 -
- - -
- 裁剪框已锁定为 16:9 宽屏 -
-
- -
- -
-
- - -
-
    -
  • - 二维码样式 -
    -
    -
    -
    - -
    - -
    -
    - -
    - -
    -
    - -
    - -
    -
    - - 仅在启用时生效,否则颜色由系统自动生成 -
    -
    - -
    - - 0.45 - -
    -
    -
    - -
    - - 0.11 - -
    -
    -
    - -
    - - -
    - 选择图片后预览将即时更新 -
    -
    -
    -
    -

    实时预览

    -
    -
    -
    - 预设主题 -
    - - - - -
    -
    -
    - -
    -

    调整仅用于预览,最终效果点击「保存修改」后生效

    -
    -
    -
    -
    -
  • -
-
- -
-
- - -
-
    -
  • - 社交信息设置 -
    - 勾选后填写主页地址(无需 https://),未勾选的项将不会在前台显示。 - - - - - -
    -
  • -
-
- -
-
- - -
-

修改密码

-
- - -
-
- - -
-

保存后当前会话立即失效,需使用新密码重新登录

-
-
- -
-
-
- - -
-

标签设置

-
- -
- - -
-
- -
-
- -
-
-
- - -
-

账号设置

- -
- - -
-

主题外观

-
- -

对提问箱页面和你的后台页面生效,首页/探索等公开页保持默认外观。

-
- - - - - - -
-
- -
-
-
- - -
- - - - - - - - - - - - - - - - - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/live-user-questions-default.html b/outputs/live-user-questions-default.html deleted file mode 100644 index c67a545..0000000 --- a/outputs/live-user-questions-default.html +++ /dev/null @@ -1,1661 +0,0 @@ - - - - - - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - -
-
-

我的提问

- 设置 -
- - -
- - - -
- - -
- - - - - - - - - - - - - - -
-
6总提问数
-
5已回答
-
1未回答
-
5已读
-
1未读
-
5私密提问
-
0置顶
-
- -
- -
-
-
- -
-
- -
- -
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- - -

我收到的提问

- - -
-
- -
- -
- 2026-10-19 00:58:03 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
等宽按钮自检B 005800 -
-
-
- -
-
- -
- -
- 2026-10-19 00:40:58 - - - - 待答未回答 - - - - 未读未读 - -
- -
- - -
- - -
- - -
- - -
-
-
- -
等宽按钮自检 004055 -
-
-
- -
-
- -
- -
- 2026-10-18 21:53:28 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
自检用提问 L3:今天更新吗(restrict self-check L3) -
-
-
- -
-
- -
- -
- 2026-10-18 21:48:21 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
自检用提问:今天更新吗(restrict self-check) -
-
-
- -
-
- -
- -
- 2026-10-18 21:39:41 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
啊吧啊吧 -
-
-
- -
-
- -
- -
- 2026-08-20 16:01:14 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
测试! -
-
-
- - - -
- - - - - - -
- - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/live-user-questions-local.html b/outputs/live-user-questions-local.html deleted file mode 100644 index 01d6dab..0000000 --- a/outputs/live-user-questions-local.html +++ /dev/null @@ -1,1661 +0,0 @@ - - - - - - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - -
-
-

我的提问

- 设置 -
- - -
- - - -
- - -
- - - - - - - - - - - - - - -
-
6总提问数
-
5已回答
-
1未回答
-
5已读
-
1未读
-
5私密提问
-
0置顶
-
- -
- -
-
-
- -
-
- -
- -
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- - -

我收到的提问

- - -
-
- -
- -
- 2026-10-19 00:58:03 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
等宽按钮自检B 005800 -
-
-
- -
-
- -
- -
- 2026-10-19 00:40:58 - - - - 待答未回答 - - - - 未读未读 - -
- -
- - -
- - -
- - -
- - -
-
-
- -
等宽按钮自检 004055 -
-
-
- -
-
- -
- -
- 2026-10-18 21:53:28 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
自检用提问 L3:今天更新吗(restrict self-check L3) -
-
-
- -
-
- -
- -
- 2026-10-18 21:48:21 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
自检用提问:今天更新吗(restrict self-check) -
-
-
- -
-
- -
- -
- 2026-10-18 21:39:41 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
啊吧啊吧 -
-
-
- -
-
- -
- -
- 2026-08-20 16:01:14 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
测试! -
-
-
- - - -
- - - - - - -
- - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/live-user-questions.html b/outputs/live-user-questions.html deleted file mode 100644 index 1c04faa..0000000 --- a/outputs/live-user-questions.html +++ /dev/null @@ -1,1661 +0,0 @@ - - - - - - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - -
-
-

我的提问

- 设置 -
- - -
- - - -
- - -
- - - - - - - - - - - - - - -
-
6总提问数
-
5已回答
-
1未回答
-
5已读
-
1未读
-
5私密提问
-
0置顶
-
- -
- -
-
-
- -
-
- -
- -
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- - -

我收到的提问

- - -
-
- -
- -
- 2026-10-19 00:58:03 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
等宽按钮自检B 005800 -
-
-
- -
-
- -
- -
- 2026-10-19 00:40:58 - - - - 待答未回答 - - - - 未读未读 - -
- -
- - -
- - -
- - -
- - -
-
-
- -
等宽按钮自检 004055 -
-
-
- -
-
- -
- -
- 2026-10-18 21:53:28 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
自检用提问 L3:今天更新吗(restrict self-check L3) -
-
-
- -
-
- -
- -
- 2026-10-18 21:48:21 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
自检用提问:今天更新吗(restrict self-check) -
-
-
- -
-
- -
- -
- 2026-10-18 21:39:41 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
啊吧啊吧 -
-
-
- -
-
- -
- -
- 2026-08-20 16:01:14 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
测试! -
-
-
- - - -
- - - - - - -
- - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/live-vote-before.html b/outputs/live-vote-before.html deleted file mode 100644 index eb548db..0000000 --- a/outputs/live-vote-before.html +++ /dev/null @@ -1,886 +0,0 @@ - - - - - - 测试 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - -
- admin6 -
の投票
-
- -
- - - 進行中 - -
-
- -

测试

- -

测试

- -
- - - - - - - - - -
匿名での参加が可能で、あなたの身元は記録されません(同一端末で最大1回)。
- - -
- - - -
- - -
-
- 1 - 一行テキスト -
测试 *
-
- - - -
- - - - -
- -
- - - -
-
- - - - - - - - - -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/llm-moderation-feasibility-2026-09-10.md b/outputs/llm-moderation-feasibility-2026-09-10.md deleted file mode 100644 index 327dc05..0000000 --- a/outputs/llm-moderation-feasibility-2026-09-10.md +++ /dev/null @@ -1,310 +0,0 @@ -# 提问箱 LLM 内容审核方案 · 可行性评审 - -- 日期:2026-09-10 -- 范围:可行性评审,**未改动任何代码** -- 背景更正:项目文本/图片审核服务为**七牛云**(阿里云通道虽仍在代码中,但 `conf/app.sample.ini` 的 `aliyun_access_key` 为空,实际不生效) - ---- - -## 0. 七牛云能力核对(评审基准) - -已验证(官方文档 [文本审核 API](https://developer.qiniu.com/censor/api/7260/api-text-censor)): - -| 项 | 事实 | -|---|---| -| 端点 | `POST https://ai.qiniuapi.com/v3/text/censor`(代码 `qiniu.go:85` 一致) | -| 审核场景 | 仅 `antispam` 一种 | -| 返回 label | `normal` / `spam` / `ad` / `politics` / `terrorism` / **`abuse`(辱骂)** / `porn` / `flood` / `contraband` / `meaningless` | -| 管控建议 | `result.suggestion`:`block` / `review` / `pass`,并带 `score` | -| 限制 | 文本长度 ≤ 2000 字;**不支持白名单**,自定义拉黑词库需提交工单 | - -已验证([七牛 AI 大模型推理](https://developer.qiniu.com/aitokenapi/13379/real-time-ai-interface-api)): - -- 提供 MaaS 推理端点 `https://api.qiniuapi.com/v1` → 实为 **`https://api.qnaigc.com/v1`** -- 兼容 OpenAI `/v1/chat/completions`、Anthropic `/v1/messages`、支持流式 -- 50+ 模型(DeepSeek / Qwen / Kimi / GLM 等) -- 使用**独立的 API Key**,与七牛对象存储 AK/SK 不是同一套凭证 - -代码内已验证的现状(`/internal/security/censor/qiniu.go`): - -```go -func (r *QiniuTextCensorResponse) IsPass() bool { - for _, detail := range r.Result.Scenes.Antispam.Details { - blockedLabels := []string{"politics", "terrorism", "porn"} // ← 只有 3 类 - if lo.Contains(blockedLabels, detail.Label) { return false } - } - return r.Result.Scenes.Antispam.Suggestion != "block" // ← review 放行 -} -``` - -**关键发现:`abuse`(辱骂)不在硬拦名单内,且 `review` 一律放行** —— 辱骂类内容现在基本是放行的。 - -> ⚠️ 推测待实测:`abuse` 在七牛返回的是 `review` 而非 `block`。建议直接抽样 `censor_logs.raw_response` 里 `label=abuse` 的记录验证(零成本,不需要额外调接口)。 - -**另一条已验证的好消息**:`censor.go:50-53` 命中缓存后会用 `QiniuTextCensorParser(raw)` **重新计算** `Pass`(缓存只省 API 调用费,不锁死判定结果)。所以后续若收紧 `IsPass()` 逻辑,**不需要清理 31 天缓存**。 - ---- - -## 1. 总体结论 - -**方案可行,但按现在的描述直接落地会有 4 个 P0 问题(SSRF、Key 明文、成本被刷、邮件轰炸)。** - -更重要的一点判断:**方案里"系统内置的若干种屏蔽判定类型",大部分根本不需要 LLM。** - -七牛已经按 `antispam` 场景返回 9 类 label + score,且代码里已经拿到这些数据只是没用上。合理分层是: - -| 层级 | 手段 | 成本 | 覆盖 | -|---|---|---|---| -| L1(应先做) | 收紧七牛 `IsPass()`:`abuse` + score 阈值 | **0 元,0 延迟** | 辱骂、垃圾、广告、灌水、违禁 | -| L2(补充) | 本地负面情绪词库打分 | 0 元 | 七牛不覆盖的语气/情绪词 | -| L3(本方案) | LLM 语义判定 | 钱 + 延迟 | 阴阳怪气、暗示、语境依赖的恶意 | - -**建议:L3 只承担 L1/L2 覆盖不到的部分**,否则你在用最贵、最慢、最不确定的手段去解决已经有确定性解的问题。 - ---- - -## 2. 数据流与调用时序 - -### 2.1 现状(已验证) - -`route/question/page.go` 的 `New()` 是**全同步**链路,任一环节返回即终止: - -``` -长度校验 → 验证码 → 同IP重复拦截 → 屏蔽词 → 七牛审核(10s超时) → 入库 → 邮件通知(goroutine) - ↓ 失败 - fail-closed 拒绝发布 -``` - -现状里唯一的异步是 `shared.SafeGo`(`internal/shared/goutil.go`)——**裸 goroutine,无重试、无持久化、进程重启即丢失**,项目里没有队列/Worker,Redis 只用于 session 与 cache。 - -### 2.2 拟议方案插在哪里 - -按方案 3「命中时在前端提示提问者」,LLM 判定**必须同步阻塞**在入库之前: - -``` -... → 屏蔽词 → 七牛审核 → 【LLM 审核】→ 入库 - ├─ 命中 → 前端提示 / 拦截 - ├─ 未命中 → 正常入库 - └─ 超限/连不上 → 跳过 + 前端异常提示 + 邮件告警 + 正常入库 -``` - -### 2.3 时序上的问题 - -| # | 问题 | 级别 | 说明 | -|---|---|---|---| -| T1 | **同步阻塞被放大** | 🟠 P1 | 七牛审核约 100–500ms;LLM 判定通常 1–10s(推理模型可达 30s+)。提问提交从"几乎无感"变成明显卡顿 | -| T2 | **超时预算未定义** | 🟠 P1 | 方案未给超时值。必须独立设 deadline(建议 3–5s),且不能复用请求 ctx(客户端断连会误判为调用失败 → 触发邮件告警) | -| T3 | **429 与连接失败未区分** | 🟠 P1 | 429(配额耗尽)应当立即熔断,不要再每次都等满超时;连接失败才走告警路径 | -| T4 | **无结果缓存** | 🟡 P2 | 相同内容重复提交会重复计费。可直接复用 `censor_logs` 已有的 31 天缓存范式 | -| T5 | **临时性错误无重试** | 🟡 P2 | 一次网络抖动就降级放行,命中率会明显低于预期 | - -**改进建议(时序)** - -1. 硬超时 3–5s,用独立 `context.WithTimeout`,**不复用请求 ctx**; -2. 内置熔断器:连续 N 次失败或收到 429 → 熔断 5–10 分钟,期间直接 fail-open 且**不发邮件**; -3. 结果按 `hash(userID + 内容)` 缓存,复用 `censor_logs` 的写法; -4. 若坚持不阻塞体验,可改为**先入库 → 异步判定 → 命中则转"待审/折叠"**,但这就不是方案 3 描述的"前端提示"了,需要你二选一(见 §7)。 - ---- - -## 3. 逐条评审 - -### 3.1 用户自填 API 地址与 Key - -**可行,但这是整个方案风险最集中的一条。** - -🔴 **P0-1 · SSRF(服务端请求伪造)** -用户填的地址由**你的服务器**发起请求。攻击者注册账号后填入 `http://127.0.0.1:6379`、`http://169.254.169.254/latest/meta-data/`、内网段地址,即可: -- 探测内网拓扑与端口存活 -- 打云厂商 metadata 拿临时凭证 -- 1C1G 机器上 Redis(6379) / Postgres(5432) 通常与 Web 同机 - -而且**方案 4 的"保存前连通性测试"本身就是一个开箱即用的内网探测入口**——不需要任何绕过技巧,填个地址点保存就能看到响应差异。 - -必须的防护(缺一不可): -- 仅允许 `https`(例外:显式允许 `http://localhost` 供自建模型调试,且需独立开关) -- 解析 DNS 后**逐个 IP 校验**,拒绝 loopback / link-local(169.254/16) / private(10/8, 172.16/12, 192.168/16) / IPv6 ULA -- **禁用 HTTP 重定向跟随**(否则 302 到内网即可绕过上面的校验) -- 建议在 `internal/shared/multidial.go` 已有自定义 Dialer 的基础上实现,统一在 dial 层拦截 - -🔴 **P0-2 · Key 存储安全** -- 已验证:全项目**没有任何 at-rest 加密**(grep `aes` / `Encrypt(` / `secretbox` 无结果) -- `users` 表单表存储,新增列即明文落库;DB/备份/导出 CSV 泄露 = Key 全泄露 -- 建议:派生密钥加密存储(不要直接用 `conf.Server.Salt`,用 HKDF 派生独立子密钥);后台**只回显掩码**(`sk-...abcd`);**永不写日志**(现有代码有把完整响应写日志的习惯,需确认 LLM 请求体不会被 logrus 打出来) - -🟠 **P1 · 协议兼容性未定义** -"API 地址"是任意 OpenAI 兼容地址,还是固定七牛 `https://api.qnaigc.com/v1`?若允许任意地址,则: -- 需约定必须 OpenAI 兼容(否则无法解析) -- 模型名由用户填还是系统固定?不同模型对 JSON 输出的遵循度差异极大 -- 建议:**内置预设 + 允许自定义**,预设直接给七牛官方端点,用户只填 Key 和选模型(顺带消掉 SSRF 面) - -### 3.2 内置判定类型,用户只能勾选 - -**方向正确。** 但强烈建议: - -- **类型优先映射到七牛 label**(`abuse` / `spam` / `ad` / `flood` / `contraband` …),这部分走 L1,零成本、确定性、无延迟 -- LLM 类型只保留七牛覆盖不到的少数几种(如「阴阳怪气 / 恶意暗示」「情绪宣泄 / 人身攻击」) -- 类型定义要写死在 Go 常量 + i18n key 里,与现有 `ForbiddenType`(`internal/security/censor/text.go:16`)保持同一种风格 - -🟡 注意:判定类型与界面语言是 4 种(zh-CN / zh-TW / en / ja),提示词里的类型描述要用提问箱主人设置的语言,但**被审核的提问文本可能是任意语言**,prompt 需显式要求"按原文语言判断"。 - -### 3.3 命中时在前端提示提问者 - -🟠 **P1 · 提示会泄露规则,可被枚举探测** -若提示写成"你的提问包含辱骂内容",攻击者就能用二分法逐词试探出完整判定规则,进而绕过。 -现有屏蔽词的提示(`question.errBlockWords`)只说"包含屏蔽词"、不说是哪个 —— **LLM 提示应对齐这个粒度**,不暴露具体类型。 - -🟠 **P1 · 误判兜底(最重要的一条)** -LLM 判定是非确定性的:同一段文本两次调用可能给出不同结果。如果"命中 = 拒绝提交",那么**误判导致的提问丢失是不可逆的**——提问者写了一大段,被吞了,且不知道为什么。 - -建议三选一(按推荐度排序): -1. **命中 → 提示 + 仍允许提交**,但给箱主打 ⚠️ 标记并默认折叠(推荐:零丢失,箱主有最终处置权) -2. 命中 → 提示 + 要求二次确认后提交(保留"冷静期"效果,又不丢内容) -3. 命中 → 硬拦(不推荐,除非你接受误判成本) - -另需注意:**提示文案走 i18n**,4 种语言各一份(现有 `admin.*` / `question.*` key 的写法)。 - -### 3.4 保存前连通性与鉴权测试 - -**可行,但是 SSRF 的头号入口(见 P0-1)。** 补充: - -- 测试请求本身也要有超时(≤5s)与 SSRF 校验 -- 建议用**最小 chat 请求**(`max_tokens=1`)而不是 `/v1/models`:部分 Key 只有推理权限没有列举模型权限,用 `/v1/models` 会误判为鉴权失败 -- 测试通过 ≠ 长期可用:Key 可能随时被吊销/欠费。**保存时通过只是准入条件,运行期的熔断与告警才是主力** -- 保存动作必须防重放:测试是服务端发起的外网请求,需加频率限制(否则可被用来打第三方 / 放大攻击) - -### 3.5 失败降级:前端提示 + 邮件告警 + 跳过审核正常提交 - -🟠 **P1 · fail-open 之后没有补审** -"跳过 LLM 审核并正常提交"意味着降级窗口内恶意内容**照常入库且无人复查**。而攻击者完全可以**主动制造失败**(刷到 Key 超限 / 打满你的连接),把系统稳定地压在降级态。 - -建议:降级期间入库的提问标记为 `llm_review = 'skipped'`,后台提供"补审"入口(复用 `cmd/censor.go` 已有的批量补审范式),否则降级态就是永久免审。 - -🔴 **P0-3 · 成本被刷(用户 Key 直接经济损失)** -Key 是**用户自己的钱**。提问入口匿名开放,攻击者可对某个启用了 LLM 的箱主持续提问,一夜烧光配额: -- 必须有**每箱主每日调用上限**(超上限即 fail-open,不再调用) -- 复用 `internal/route/ratelimit.go` 已有的内存滑动窗口限流器做**单 IP 限流** -- 超限后应**自动停用**该箱主的 LLM 审核并通知箱主,而不是静默继续烧钱 - -🔴 **P0-4 · 邮件轰炸 → SMTP 账号被封** -"调用失败就发邮件"在攻击场景下 = **每条提问一封邮件**。攻击者刷 1000 条提问,你的发信账号会被标记为垃圾邮件发送者甚至直接封号,**影响的是全站所有邮件(含找回密码)**。 - -必须的节流: -- 按 **箱主 + 时间窗聚合**(建议 10 分钟最多 1 封),窗口内汇总次数 -- 收敛粒度建议按**错误类别**(连接失败 / 超时 / 429 / 鉴权失败)分别计数,而不是每次都发 -- 触发熔断后**停止发信**,只在恢复时发一封汇总 -- 告警收件人建议复用 `conf.Security.SecurityAlertEmail` 的模式,并允许箱主选择关闭告警 - ---- - -## 4. 失败降级策略 · 建议矩阵 - -| 失败类型 | 是否跳过审核 | 是否提示提问者 | 是否发邮件 | 是否熔断 | -|---|---|---|---|---| -| 超时 | 跳过 | 否(静默) | 窗口内聚合 | 连续 N 次 → 熔断 | -| 连接失败 | 跳过 | 否(静默) | 窗口内聚合 | 是 | -| 429 / 配额耗尽 | 跳过 | 否 | 是(1 次) | 是(较长) | -| 401 / 403 鉴权失败 | 跳过 | 否 | 是(1 次) | **自动停用该功能** | -| 响应解析失败 | 跳过(视为未命中) | 否 | 窗口内聚合 | 连续 N 次 → 熔断 | -| 达到日调用上限 | 跳过 | 否 | 是(1 次) | 是(到次日) | - -> 原则:**提问者侧永远不要暴露"审核系统坏了"**——否则等于告诉对方"现在可以随便发"。 - ---- - -## 5. 风险清单 - -| ID | 级别 | 问题 | 处置 | -|---|---|---|---| -| P0-1 | 🔴 | 用户自填 API 地址导致 SSRF,连通性测试即探测入口 | 协议白名单 + DNS 解析后逐 IP 校验 + 禁用重定向 | -| P0-2 | 🔴 | Key 明文落库、无 at-rest 加密、可能回显/入日志 | 派生密钥加密 + 后台掩码 + 禁用请求体日志 | -| P0-3 | 🔴 | 用户 Key 被刷造成直接经济损失 | 每日调用上限 + 单 IP 限流 + 超限自动停用 | -| P0-4 | 🔴 | 失败即发信 → 邮件轰炸 → SMTP 封号 | 箱主+时间窗聚合 + 熔断后停发 + 允许关闭告警 | -| P1-1 | 🟠 | 同步阻塞放大(1–10s+),超时预算未定义 | 独立 3–5s deadline,不复用请求 ctx | -| P1-2 | 🟠 | 误判导致提问不可逆丢失 | 改"提示 + 标记折叠"或"二次确认",不要硬拦 | -| P1-3 | 🟠 | 提示文案泄露判定规则,可被枚举绕过 | 提示不暴露具体类型,对齐现有屏蔽词语气 | -| P1-4 | 🟠 | fail-open 后无补审,攻击者可主动维持降级态 | 标记 `skipped` + 后台补审入口 | -| P1-5 | 🟠 | LLM 输出解析不可靠(中文自由文本) | 强制 JSON 输出(`response_format`),严格 schema 校验,解析失败视为未命中 | -| P2-1 | 🟡 | 无结果缓存,重复内容重复计费 | 复用 `censor_logs` 31 天缓存范式 | -| P2-2 | 🟡 | 临时错误无重试 | 1 次快速重试(仅对连接类错误) | -| P2-3 | 🟡 | 提问内容出网给第三方 LLM(匿名提问的隐私) | 设置页明确告知 + 默认关闭 | -| P2-4 | 🟡 | 七牛文本上限 2000 字;`AnswerContentLen` 默认即 2000(边界) | 站长若调高超 2000,七牛会拒 → fail-closed 拒绝发布。需加前置截断或上限校验 | -| P2-5 | 🟡 | 无调用审计(谁、何时、花多少) | 建议记录调用量/耗时/结果,供箱主自查 | - ---- - -## 6. 成本与限额 - -| 维度 | 建议 | -|---|---| -| 调用时机 | 只对**提问**调用,不要覆盖回答 / 追问 / 私聊(成本与延迟都会失控) | -| 单箱主日上限 | 建议默认 100–300 次/天,可在 `[limits]` 配置,超限 fail-open + 通知箱主 | -| 单 IP 限流 | 复用 `internal/route/ratelimit.go`,建议 5–10 次/分钟 | -| 输入长度 | 截断到提问上限(`QuestionContentLen`,默认 1000 字符),避免超长文本爆 token | -| 模型 | 审核是分类任务,用小模型即可(成本差一个数量级);不要上推理模型 | -| 输出 | `max_tokens` 压到最小(如 64),只回 JSON | -| 缓存 | 相同内容 31 天内不重复调用 | -| 免费额度 | 七牛 AI 推理对新用户有免费 token,可作为内置预设的冷启动优势 | - ---- - -## 6.5 修订:撤回 L1 建议(用户实测反馈后追加) - -> 2026-09-10 追加。用户反馈:**七牛对「辱骂」的判定过于严格,正常对话经常被误伤。** - -这条反馈直接推翻了本文档 §1 的 L1 建议("收紧 `IsPass()`,把 `abuse` 纳入硬拦")——**该建议予以撤回**,照做只会放大误伤。 - -配合代码复核,误伤的真实成因有两个,且都是**硬拦语义**造成的: - -| 成因 | 代码位置 | 现状 | -|---|---|---| -| 七牛把正常内容判为 `abuse` 且 `suggestion=block` | `qiniu.go:55-68` → `!Pass` → 拒绝 | 提问/私聊**直接发不出去**,用户只看到"内容安全检查不通过 [辱骂]" | -| 审核服务不可用/超时 | `page.go:574`、`chat_handler.go:258` → `err != nil` → **fail-closed 拒绝** | 七牛抖动期间所有提问与私聊一律失败 | - -第二个成因尤其容易被忽略:**这不是"判得准不准"的问题,是审核服务一抖全站就发不出内容。** - -### 关键约束(七牛官方已确认) - -文本审核**不支持白名单**,自定义拉黑词库需**提交工单**处理 —— 也就是说: - -**我们既不能让七牛判得更准,也不能让它判得更松,唯一能控制的是"命中之后怎么办"。** - -### 修订后的设计原则 - -**显式规则硬拦,算法推断只标记。** - -| 判定来源 | 处置 | 理由 | -|---|---|---| -| 箱主自定义屏蔽词 | **硬拦** | 用户显式指定,知道自己拦的是什么,误伤自负 | -| 算法判定 `politics` / `terrorism` / `porn` | **硬拦** | 合规底线,不能软 | -| 算法判定 `abuse` / `spam` / `ad` / `flood` / `contraband` / `meaningless` | **只打 ⚠️ 标记,照常送达** | 误报率高,误伤不可逆,把决定权交回箱主 | -| 审核服务不可用 | **fail-open + 标记待审** | 避免服务抖动导致全站发不出内容 | - -### 落地形态 - -- `questions` / `messages` 各加一个标记列(GORM `AutoMigrate` 自动加列,无需手写迁移) -- 箱主后台:⚠️ 角标 + 默认折叠 + 可筛选 + 一键"忽略/删除" -- 设置页新增「审核严格度」三档,把权衡交还给用户: - - **严格**:维持现状(不过即拒) - - **宽松(建议默认)**:仅高危三类硬拦,其余标记 - - **仅标记**:全部不拒,只标记 -- 阈值自建:不采信七牛 `suggestion`,改用已有的 `detail.Score` + 可配阈值(默认偏高,如 0.9) -- 容错:短文本可跳过判定;箱主"这不是辱骂"反馈计数,用于后续调阈值 - -### 这条路线顺带消掉的风险 - -零延迟、零外部依赖 → **SSRF、Key 明文、Key 被刷、邮件轰炸四个 P0 全部不再存在**,也无需引入队列或加密主密钥。 - ---- - -## 7. 需要你确认的点 - -1. **命中后的语义**:是「①仅提示 + 仍提交(打标记折叠)」/「②提示 + 二次确认后提交」/「③硬拦拒绝」? - —— 我的建议是 ①,理由见 §3.3。 -2. **是否接受同步阻塞**?若不接受,方案 3「前端提示提问者」就要改成异步 + 待审队列(需要新增队列能力,项目现在没有)。 -3. **API 地址范围**:固定七牛 `https://api.qnaigc.com/v1`(消灭 SSRF 面)/ 还是允许任意 OpenAI 兼容地址? -4. **模型名**:系统固定一个小模型,还是用户自填? -5. **Key 加密**:是否接受引入 at-rest 加密(需要一个主密钥,放 `[server]` 段)?还是先明文 + `json:"-"` + 掩码回显,后续再补? -6. **告警收件人**:复用 `conf.Security.SecurityAlertEmail`,还是发给箱主本人(`users.email`),还是两者都发? -7. **调用范围**:只审提问,还是也要审回答 / 追问 / 私聊? -8. **是否先做 L1/L2**:我建议先收紧七牛 `IsPass()`(把 `abuse` 纳入 + score 阈值),这条零成本、零延迟,可能就解决了你 80% 的诉求,之后再决定 LLM 要不要上。要不要我先出这部分的变更清单? diff --git a/outputs/m-bottom-strip.png b/outputs/m-bottom-strip.png deleted file mode 100644 index b5f99df..0000000 Binary files a/outputs/m-bottom-strip.png and /dev/null differ diff --git a/outputs/m-box.png b/outputs/m-box.png deleted file mode 100644 index a3ca9ed..0000000 Binary files a/outputs/m-box.png and /dev/null differ diff --git a/outputs/m-explore.png b/outputs/m-explore.png deleted file mode 100644 index 6c14706..0000000 Binary files a/outputs/m-explore.png and /dev/null differ diff --git a/outputs/m-profile.png b/outputs/m-profile.png deleted file mode 100644 index 7ddf061..0000000 Binary files a/outputs/m-profile.png and /dev/null differ diff --git a/outputs/m-rules.png b/outputs/m-rules.png deleted file mode 100644 index 3b65d58..0000000 Binary files a/outputs/m-rules.png and /dev/null differ diff --git a/outputs/new-default.html b/outputs/new-default.html deleted file mode 100644 index ec62a58..0000000 --- a/outputs/new-default.html +++ /dev/null @@ -1,1064 +0,0 @@ - - - - - - admin6 - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - -
-
-
- -

admin6

-

问你想问的

-
- - - - - - - bloglink - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@admin6提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/new-sakura-dark.html b/outputs/new-sakura-dark.html deleted file mode 100644 index 956f090..0000000 --- a/outputs/new-sakura-dark.html +++ /dev/null @@ -1,1114 +0,0 @@ - - - - - - admin6 - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - -
-
-
- -

admin6

-

问你想问的

-
- - - - - - - bloglink - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@admin6提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/new-sakura-light.html b/outputs/new-sakura-light.html deleted file mode 100644 index 9e54807..0000000 --- a/outputs/new-sakura-light.html +++ /dev/null @@ -1,1114 +0,0 @@ - - - - - - admin6 - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - -
-
-
- -

admin6

-

问你想问的

-
- - - - - - - bloglink - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@admin6提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/p0-p1-fixes-2026-09-01.md b/outputs/p0-p1-fixes-2026-09-01.md deleted file mode 100644 index 094b933..0000000 --- a/outputs/p0-p1-fixes-2026-09-01.md +++ /dev/null @@ -1,77 +0,0 @@ -# TamaBox-v4 P0/P1 修复报告(2026-09-01) - -第三轮审计的全部 P0×2 + P1×6 已修复,并在验证过程中额外发现并修复了一处 -被 webp 屏障遮蔽的**隐藏编译回归**(见「额外发现」)。全模块构建/vet/测试全绿。 - -## 🔴 P0 修复 - -### P0-1 profile.go 导出函数编译错误 -- `route/user/profile.go`:`personalData` 是 `[][]interface{}`,原代码把 `interface{}` 直接传给 - `sanitizeCell(string)` 导致编译失败。 -- 改为逐单元格类型断言:字符串净化防公式注入,非字符串(`time.Time` 等)原样写入。 - -### P0-2 history-template 复合游标未同步(翻页损坏) -- 采用更彻底的方案:`route/question/page.go` 的 `ListAPI` 响应改为 - `{data: [...], next_cursor: "..."}`,由**服务端直接下发** `EncodePinCursor` 编码的复合游标。 -- 前端 JS 不再用 `new Date().getTime()*1e6` 自行拼游标——JS 双精度浮点无法精确表示纳秒级 - UnixNano(毫秒以下精度丢失),时间戳相同的提问会在翻页边界被跳过。 -- JS 层已适配 `ctx.JSON` 的统一包装 `{code, data, message}`(内层才是分页负载)。 - -## 🟠 P1 修复 - -### P1-1 upload_images.go 级联删除查询不存在的列 -- `DeleteByQuestionID` 重写:先查 `upload_image_questions` 关联行拿 imageID, - 再事务内软删 `UploadImage` 主体并删除关联行(原代码直接在错误表上 `WHERE question_id`)。 - -### P1-2 conversation.go 重复删除二次回退配额 -- `MarkDeletedByOwner` 增加幂等守卫:`conv.IsDeletedByOwner == true` 直接返回, - 不再重复 `IncrementQuota(-1)`。 - -### P1-3 questions.go 非 PinFirst 游标 PG 报错 + ASC 方向反 -- 游标改用 `strconv.ParseUint` 校验(原 `id < ?` 传 string,PG 下 `integer < text` 无操作符)。 -- 排序方向规范化提前,ASC 用 `id > ?`、DESC 用 `id < ?`;解析失败忽略游标(与 PinFirst 一致)。 - -### P1-4 home.html 供应链 XSS -- 远程公告改 `x-text` 纯文本渲染,移除 `x-html`;meta 行与正文拆成两个元素分别渲染。 - -### P1-5 tracing span 泄露 query string -- span 名从 `RequestURI` 改为 `URL.Path`,`?t=`、`?code=` 等不再落入 trace。 - -### P1-6 password.go(第一轮遗留项) -- legacy HMAC-SHA1 比较改用 `crypto/subtle.ConstantTimeCompare`(常量时间,防时序侧信道)。 -- `encodePasswordBcrypt` 失败不再静默降级为 HMAC-SHA1 弱哈希:密码超 72 字节按 bcrypt - 原生语义截断;bcrypt 生成失败记日志并返回空串(账号校验失败比弱哈希安全,且 - `users.go:390` 的 `newHash != ""` 守卫保证空串不会被写库)。 - -## ⚠️ 额外发现:internal/route 隐藏编译回归(P0 级) - -P1-8 批次(autoban 重构)引入、一直被 webp 屏障遮蔽的编译错误,本次通过 -`go build -overlay` 全模块类型检查暴露: - -- `internal/route/route.go`:使用 `autoban` / `mail` / `shared` 三个标识符但缺少 import; -- `internal/route/autoban.go`:`autoban.Autoban` 类型不存在,应为 `autoban.Banner`。 - -**若不修复,Linux 上的完整构建同样会失败。** 两处已修复。 - -## 验证方式(重要) - -Windows 沙箱缺 libwebp,`internal/storage` 及其依赖包无法正常编译,导致其中的类型错误 -(P0-1、internal/route 回归)在常规 `go build` 下被遮蔽。本次引入 **overlay 桩验证法**: - -- `dev/webpstub/`:chai2010/webp 的签名兼容桩(仅 `Encode`/`Options`),保留在仓库供复用; -- `go build -overlay=<临时 overlay.json> ./route/... ./internal/... .` 将 - `internal/storage/s3.go` 的 import 临时替换为桩包,实现全模块完整类型检查。 - -验证结果: -- 全模块 `go build`(overlay)✅ -- 全模块 `go vet`(overlay)✅ -- 全部可运行测试通过(templates/form/i18n/cache/autoban/censor 等,`go test` sweep)✅ - - 含 `TestTemplatesParse` / `TestRenderPublicQuestionList` / `TestRenderPublicQuestionItem` / - `TestRenderUserQuestionList` / `TestTemplateCrossReferences` -- `internal/db` 测试需 `DB_TYPE` 环境变量 + 真实数据库,属既有环境限制,非回归。 - -## 待办提醒 - -1. **Linux/CI 环境跑一次 `go build ./...` + `go test ./...` 最终确认**(overlay 桩只覆盖类型检查, - webp CGO 真实编译仍需 libwebp 环境)。 -2. P2 9 项(mail header、上传后缀白名单、孤儿文件、dbutil pageSize 上限等)尚未处理。 diff --git a/outputs/p1-fixes-2026-08-31.md b/outputs/p1-fixes-2026-08-31.md deleted file mode 100644 index 61160a3..0000000 --- a/outputs/p1-fixes-2026-08-31.md +++ /dev/null @@ -1,51 +0,0 @@ -# P1 修复落地记录(P1-3 / 4 / 5 / 7 / 8 / 9 / 10) - -- **日期**:2026-08-31 -- **范围**:按用户要求顺序实施 7 个 P1 项(P1-6 密码比较不在本次范围;P1-3 按要求**不使用 Turnstile/recaptcha**)。 -- **P0-1(IP 伪造)暂缓**,按用户指示留待后续。 - -## 1. P1-3 验证码限流 + 常量时间比较 -**文件**:`internal/captcha/math.go`、`internal/route/route.go` -- 答案比对改为 `crypto/subtle.ConstantTimeCompare`,消除时序侧信道。 -- `VerifyOnly`(模态框预校验)累加尝试次数,达到 `maxCaptchaAttempts=5` 即销毁该验证码,防止无限轮询爆破。 -- `GenerateMath` 处理 `rand.Int` 错误(不再忽略)。 -- `/api/captcha/generate`、`/api/captcha/verify` 加上 `rateLimitMiddleware(20, 1m)`。 -- ⚠️ 这是**缓解**而非根治:数学答案仍返回给客户端,脚本理论上可读题。根治依赖 P0-1(IP 信任校验)落地后整体提权。 - -## 2. P1-4 追问"等主人回复"服务端校验 -**文件**:`internal/db/follow_ups.go`、`route/question/follow_up.go` -- 新增 `FollowUps.GetLatestByQuestionID`(`Order(created_at DESC) Limit 1`)。 -- `PublishFollowUp` 在计数校验后,取该问题最新一条追问;若其 `Answer == ""` 直接拒绝(不再仅依赖前端 `waitingFollowUpReply` 隐藏表单)。 - -## 3. P1-5 审核可信域名精确匹配 -**文件**:`internal/security/censor/text.go` -- `RemoveTrustedURL` 改为边界感知:仅当可信域名后紧跟 `/`、`?`、`#` 或位于字符串结尾时才剥离,避免 `https://github.com.evil.tld` 被误判为 `github.com` 子域而漏审。 - -## 4. P1-7 删除回退功能配额计数 -**文件**:`internal/db/feature_control.go`、`internal/db/questions.go`、`internal/db/conversation.go` -- `IncrementQuota` 负 delta 增加 `WHERE quota_count > 0` 守卫,防止计数减到负数。 -- `questions.DeleteByID` 删除提问后回退 `FeatureQuestion` 配额 -1。 -- `conversations.MarkDeletedByOwner` 删除私聊会话后回退 `FeatureChat` 配额 -1(并先取 OwnerID,已删除/不存在时幂等返回)。 - -## 5. P1-8 自动封禁仅计真正 404 -**文件**:`internal/route/autoban.go`、`internal/route/route.go` -- `autobanMiddleware` 改为接收共享 `*autoban.Autoban` 实例(由 `New()` 创建一次)。 -- 真正的 404 探测计数移入 `f.NotFound` 处理器单独记录;不再把业务性 `302 → /`(语言切换 / 登出 / 投票不存在 / 黑名单跳转)误判为路径扫描导致误封。 - -## 6. P1-9 服务反代路径穿越 / query / 超时 -**文件**:`route/service/proxy.go` -- `forwardPath` 经 `path.Clean` 归一化,`singleJoiningSlash` 安全拼接,杜绝 `../` 越出白名单前缀。 -- 透传原始 `RawQuery`(此前静默丢弃)。 -- 复用带超时的共享 `Transport`(`ResponseHeaderTimeout=15s`、`IdleConnTimeout=30s`),不再每请求新建。 - -## 7. P1-10 像素追踪路由收紧 -**文件**:`internal/route/route.go`、`internal/context/context.go`、`route/pixel/pixel.go` -- `/api/v1/pixel/{**}` 由 `f.Any` 改为 `f.Get` + `rateLimitMiddleware(30, 1m)`,仅放行只读 GET,并补限流。 -- `context.go` 去掉对 `/api/v1/pixel/` 的 CSRF 豁免(GET 本不需 CSRF,写方法已被路由层禁止)。 -- `pixel.go`:路径 `path.Clean` 后必须仍在 `/api/` 之下,否则拒绝;响应头仅透传白名单(Content-Type/Length/Encoding/Disposition/Cache-Control/ETag/Expires/Last-Modified/Pragma/Vary),**不透传 Set-Cookie 等**。 - -## 验证状态 -- ✅ `go build` / `go vet` 全绿(不依赖 storage 的 5 个包):`internal/captcha`、`internal/security/censor`、`internal/db`、`route/pixel`、`route/service`。 -- ✅ 单元测试通过:`internal/security/censor`、`internal/security/autoban`。 -- ✅ 12 个改动文件 `gofmt -w` 已格式化。 -- ⚠️ `internal/route/route.go`、`internal/route/autoban.go`、`internal/context/context.go`、`route/question/follow_up.go` 因传递依赖 `internal/storage`(`github.com/chai2010/webp` 缺 libwebp,Windows 沙箱 CGO 限制)**无法在此环境完整编译**;已用 `gofmt -e` 确认无语法错误,请在 Linux / 带 libwebp 环境做完整 `go build ./...` 验证。 diff --git a/outputs/p2-explore-light.png b/outputs/p2-explore-light.png deleted file mode 100644 index 8dc05ed..0000000 Binary files a/outputs/p2-explore-light.png and /dev/null differ diff --git a/outputs/p2-fixes-and-p0-alt-2026-09-01.md b/outputs/p2-fixes-and-p0-alt-2026-09-01.md deleted file mode 100644 index 4abc82b..0000000 --- a/outputs/p2-fixes-and-p0-alt-2026-09-01.md +++ /dev/null @@ -1,76 +0,0 @@ -# TamaBox-v4 — P0 替代方案 + P2 整改(除 P2-4)与复审 - -- **日期**:2026-09-01 -- **范围**:P0 IP 伪造的「免 CIDR」替代方案落地;P2-1~P2-3、P2-5~P2-14(除 P2-4)全部改进;改动后复审。 -- **方法**:静态走查 + `go build` / `go vet` / `go test`(受 webp CGO 沙箱限制,含 `internal/storage` 的包仅做 `gofmt -e` 语法校验)。 - ---- - -## 一、P0 的「免 CIDR」替代方案(你最关心的点) - -原报告建议「维护 Cloudflare 全量 CIDR 白名单 + 自右向左剥离跳数」。但正如你指出的:CIDR 列表太大、且 Cloudflare 边缘节点会变动,维护成本高且易遗漏。本次采用 **不依赖任何 CDN CIDR 清单** 的方案: - -### 设计核心(`internal/shared/ip.go`) -1. **优先采用 `CF-Connecting-IP`**(Cloudflare 权威头)。只要源站被 **锁定只接受 Cloudflare 连接**,该头即等于真实客户端 IP,无需任何 IP 白名单。 -2. **`trusted_proxies` 仅用于你自己的基础设施**(如本机 nginx `127.0.0.1`、私有 `10.0.0.0/8` LB),默认空。仅当 TCP 对端命中该小清单时才解析 `X-Forwarded-For` / `X-Real-IP`,且从右向左剥离已知可信跳数后取原始客户端。 -3. **其余情况一律回退到 `RemoteAddr`**,绝不信任客户端自行提供的头。 - -### 为什么这能根治,且不用 CIDR -- **关键在部署侧锁定源站**,推荐二选一(都**不需要** CIDR 清单): - - **Cloudflare Tunnel(`cloudflared`)**:源站无任何公网 IP,只与 Cloudflare 建隧道 → 攻击者无法直连源站伪造 `CF-Connecting-IP`。 - - **Cloudflare Authenticated Origin Pull(mTLS)**:源站 TLS 要求 Cloudflare 客户端证书 → 非 Cloudflare 连接被拒。 -- 二者都让「伪造 `CF-Connecting-IP`」在物理/密码学层面不可行,从而免去了维护庞大 CIDR 的负担。 - -### 配置变更 -- `conf/static.go`:`Security` 新增 `TrustedProxies string \`ini:"trusted_proxies"\``。 -- `conf/app.sample.ini`:`[security]` 下新增 `trusted_proxies =`(默认空),并改写 `trust_proxy` 注释,明确「开启 trust_proxy 必须配合 Tunnel/mTLS 锁定源站」。 -- `internal/cmd/web.go`:`conf.Init()` 后调用 `shared.InitTrustedProxies()` 解析清单(解析失败则启动报错)。 - -> 残留风险(已文档化):若 `trust_proxy=true` 但**源站仍可被直连**(未锁源站),攻击者可自行发 `CF-Connecting-IP` 被信任。生产必须配合上述锁定手段。这是「免 CIDR」方案的必要前提。 - ---- - -## 二、P2 整改清单(除 P2-4 外全部完成) - -| # | 位置 | 改动 | -|---|---|---| -| P2-1 | `route/vote/vote.go:310` | 排序题统计 `json.Unmarshal(s.FormData, &data.Ranking)` → `&data`,修复解析必失败、排名恒为 0 的 bug | -| P2-2 | `route/vote/vote.go` + `route/user/vote.go` | 抽离 `InvalidateResultsCache(voteID)`;除新提交外,草稿「选项/字段重建」「投票删除」也主动失效结果缓存 | -| P2-3 | `internal/db/vote_submissions.go` | `DeleteByVoteID` 改为 `Unscoped()` 物理删除;避免软删行残留占据唯一索引导致「统计归零却无法重投」 | -| P2-5 | `internal/db/questions.go` + `follow_ups.go` + `upload_images.go` | 删除提问时级联软删其 `follow_ups` 与 `upload_images`,消除孤儿记录 | -| P2-6 | `internal/db/users.go:814` | `CountTodayMessagesFromUser` 改用应用本地时区当日 00:00 边界,替代 DB 的 `CURRENT_DATE`,修正每日私信上限窗口偏移 | -| P2-7 | `route/user/profile.go` | 导出 XLSX 单元格前置单引号防御公式注入(以 `= + - @ \t \r \n` 开头的内容按纯文本处理) | -| P2-8 | `internal/db/users.go:244` | 注册 `Create` 遇重复键错误时重新执行 `validate` 返回友好错误(`ErrDuplicateEmail`/`ErrDuplicateDomain`),消除 TOCTOU 暴露底层 DB 错误 | -| P2-9 | `internal/route/gzip.go` | `Write` 将嗅探到的 `Content-Type` 写回响应头使压缩判定生效;`204/304` 不再附加 `Content-Encoding` | -| P2-10 | `internal/route/route.go:244` | `/i18n/{lang}` 重定向拦截反斜杠 `\\` 与控制字符,防开放重定向变体 | -| P2-11 | `internal/route/route.go` | 加 `Strict-Transport-Security`(仅 https 时下发);`lang` cookie 在 https 下设 `Secure` | -| P2-12 | `internal/route/chat/chat_handler.go` | `SendMessage` 发送前重新校验 `FeatureStateOf(FeatureChat).Open`,箱主中途关闭私聊即阻止继续发消息 | -| P2-13 | `internal/db/questions.go` + `route/question/page.go` | 置顶优先分页改用复合键集游标 `(is_pinned, created_at, id)`,消除置顶项与非置顶项 id 交错导致的「加载更多」漏项 | -| P2-14 | `internal/captcha/math.go` | 已由前次 P1-3 覆盖:`rand.Int` 错误已校验(无新增改动) | - -> P2-4(Question.Token 仅 6 位/唯一索引)按你要求**跳过**。 - ---- - -## 三、复审验证结果 - -| 项目 | 结果 | -|---|---| -| `go build` 不依赖 storage 的包(`shared/db/captcha/security/censor`) | ✅ 全绿 | -| `go vet` 同上包 | ✅ 无告警 | -| `go test ./route/vote/...` | ✅ 通过(覆盖 P2-1 排名聚合、缓存接口一致) | -| `gofmt -e` 受 webp 影响的编辑文件(`route/user/*`、`route/question/page.go`、`internal/route/*`、`chat_handler.go`) | ✅ 均无语法错误 | -| `route/vote` 整包 `go build` | ✅ 通过(P2-1/P2-2 改动类型校验通过) | - -⚠️ 含 `internal/storage`(webp CGO)的包(`internal/route`、`route/user`、`route/question`、`internal/route/chat`、`cmd/web`)**本沙箱无法完整编译**(既定限制,与改动无关)。这些文件已用 `gofmt -e` 确认无语法错误,需在 **Linux / 带 libwebp** 环境跑一次 `go build ./...` 做最终确认。 - -### 复审新发现 / 残留项 -- **P2-2 多实例一致性**:本方案让各实例在「新提交 / 改选项 / 删投票」时各自失效自身进程内缓存,但**跨实例**仍依赖 30s TTL 自然过期(进程内 TTL 缓存的固有限制)。若未来做多实例横向扩容,建议将 `resultsCache` 迁到 Redis 共享缓存,方能做到即时全局一致。当前单实例/低并发场景已足够。 -- **P2-5 级联为软删除**:与提问本身的软删除保持一致;对象存储中的实际图片文件未删除(避免误删复用资源),仅清 DB 记录。若需彻底清理文件,应另起异步任务。 -- 其余改动逻辑自洽,未发现引入新的越权 / 竞态 / 注入点。 - ---- - -## 四、待你确认/执行的部署动作 -1. **P0 生效前提**:生产环境用 **Cloudflare Tunnel 或 Authenticated Origin Pull** 锁定源站;并在 `app.ini` 设 `trust_proxy = true`、`trusted_proxies` 填入你自己的反代 IP(如 `127.0.0.1`)。 -2. **最终构建**:在带 libwebp 的环境 `go build ./...` 验证全部包。 diff --git a/outputs/p2-home-dark.png b/outputs/p2-home-dark.png deleted file mode 100644 index 14708d8..0000000 Binary files a/outputs/p2-home-dark.png and /dev/null differ diff --git a/outputs/p2-home-light.png b/outputs/p2-home-light.png deleted file mode 100644 index 0aad8e0..0000000 Binary files a/outputs/p2-home-light.png and /dev/null differ diff --git a/outputs/p2-sheet.html b/outputs/p2-sheet.html deleted file mode 100644 index c07be36..0000000 --- a/outputs/p2-sheet.html +++ /dev/null @@ -1,697 +0,0 @@ - - -
-
-

导航图标:emoji → 内联单色 SVG(跟随文字色/主题)

-
- - -
-
-
-

标签:实心彩色 uk-label → 轻量胶囊

-
- 单选多选评分 - 排序表单 - #杂谈#音乐#日常 -
-
-
-

占比条:div.uk-progressbar → 原生 <progress>

-
66% -
-
33% -
-
0% -
-
-
- \ No newline at end of file diff --git a/outputs/p2-sheet.png b/outputs/p2-sheet.png deleted file mode 100644 index 56f2636..0000000 Binary files a/outputs/p2-sheet.png and /dev/null differ diff --git a/outputs/p2fix-results-light.png b/outputs/p2fix-results-light.png deleted file mode 100644 index a1591e6..0000000 Binary files a/outputs/p2fix-results-light.png and /dev/null differ diff --git a/outputs/p2fix-vote-page.png b/outputs/p2fix-vote-page.png deleted file mode 100644 index 226abfe..0000000 Binary files a/outputs/p2fix-vote-page.png and /dev/null differ diff --git a/outputs/p2live-home-dark.png b/outputs/p2live-home-dark.png deleted file mode 100644 index ccf8d6d..0000000 Binary files a/outputs/p2live-home-dark.png and /dev/null differ diff --git a/outputs/p2live-home-light.png b/outputs/p2live-home-light.png deleted file mode 100644 index 7a124aa..0000000 Binary files a/outputs/p2live-home-light.png and /dev/null differ diff --git a/outputs/p2live-p2-home.png b/outputs/p2live-p2-home.png deleted file mode 100644 index fe56e7a..0000000 Binary files a/outputs/p2live-p2-home.png and /dev/null differ diff --git a/outputs/p2live-probe-explore.png b/outputs/p2live-probe-explore.png deleted file mode 100644 index 34c2d04..0000000 Binary files a/outputs/p2live-probe-explore.png and /dev/null differ diff --git a/outputs/p2live-probe-res5.png b/outputs/p2live-probe-res5.png deleted file mode 100644 index a876764..0000000 Binary files a/outputs/p2live-probe-res5.png and /dev/null differ diff --git a/outputs/p2live-results-light.png b/outputs/p2live-results-light.png deleted file mode 100644 index 8c17651..0000000 Binary files a/outputs/p2live-results-light.png and /dev/null differ diff --git a/outputs/pg-cursor-diagnose-v2-nekobox.sql b/outputs/pg-cursor-diagnose-v2-nekobox.sql deleted file mode 100644 index afdff1e..0000000 --- a/outputs/pg-cursor-diagnose-v2-nekobox.sql +++ /dev/null @@ -1,234 +0,0 @@ --- ============================================================================ --- TamaBox「加载更多重复加载」根因诊断 v2(按生产真实结构定制) --- 适用库: schema = nekobox,表 = nekobox.questions / nekobox.users --- questions: id numeric, 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 --- users: id numeric, domain text, ... --- 脚本用途: 用纯 SQL 复刻 ListAPI(FilterAnswered+PrivateStatus=1+PinFirst, 每页20)的 --- 复合键集翻页,判定「DB 层数据自洽性」+「时区解释是否与线上游标一致」。 --- 覆盖范围: 只读查询 + 会话级临时表,无任何持久写。可放心在生产执行。 --- 使用: psql/pgAdmin/DBeaver 整段执行;若当前库 schema 不是 nekobox,改开头 SET 一行。 --- 判读: 见文件末尾说明;跑完把「查询结果 + NOTICE」整体贴回。 --- ============================================================================ - --- ============ 0) 切换 schema(关键:默认 search_path 只有 public,所以之前找不到表) ============ -SET search_path TO nekobox, public; -SELECT '连接确认' AS step, - current_database() AS db, - current_schema() AS effective_schema, -- 应显示 nekobox - current_setting('TimeZone') AS session_tz; - --- ============================================================================ --- PART A 环境 / 结构 / 目标集合核对 --- ============================================================================ -SELECT 'PART A: 环境与结构核对' AS step; - --- 1) questions 关键列定义(确认 id 为 numeric、created_at 无时区精度6) -SELECT column_name, data_type, datetime_precision, is_nullable, column_default -FROM information_schema.columns -WHERE table_schema = 'nekobox' AND table_name = 'questions' - AND column_name IN ('id','created_at','updated_at','deleted_at','user_id', - 'answer','is_private','is_pinned','is_read') -ORDER BY ordinal_position; - --- 2) user_id 相关索引(确认是否命中 idx_questions_user_private_created 复合索引) -SELECT indexname, indexdef -FROM pg_indexes -WHERE schemaname = 'nekobox' AND tablename = 'questions' - AND indexdef ILIKE '%user_id%'; - --- 3) aoi 箱主定位 + 集合核对(answered_public_all 应=线上 API 总数,曾为 38) -SELECT u.id AS aoi_user_id, - u.domain, - count(*) FILTER (WHERE q.answer IS NOT NULL AND q.answer <> '' AND q.is_private = false) - AS answered_public_all, - count(*) FILTER (WHERE q.answer IS NOT NULL AND q.answer <> '' AND q.is_private = false AND q.is_pinned) - AS answered_public_pinned, - count(*) AS all_questions_rows -FROM users u -LEFT JOIN questions q ON q.user_id = u.id AND q.deleted_at IS NULL -WHERE u.domain = 'aoi' -GROUP BY u.id, u.domain; - --- 4) 该集合最新 6 条(含被删/私密剔除样本),关键看 created_at 的「墙钟」写法 --- -> 与线上 API JSON 的 created_at(如 2026-09-05T00:49:38.648502Z) 逐位比对: --- 若 DB 墙钟==线上 Z 时间文本 => 读写按 UTC 对称(无时区偏移) --- 若 DB 墙钟比线上 Z 大 8 小时 => 时区偏移实锤(上海墙钟被当 UTC / 反之) -SELECT q.id, - q.is_pinned, - q.created_at AS db_wall_clock, -- 无时区列原值 - to_char(q.created_at, 'YYYY-MM-DD"T"HH24:MI:SS.US') AS db_wall_text, - (extract(epoch FROM q.created_at) * 1000000000)::numeric(30,0) AS nano_if_wall_is_utc, - q.answer <> '' AS has_answer, - q.is_private AS is_private, - q.deleted_at IS NOT NULL AS is_deleted -FROM questions q -WHERE q.user_id = (SELECT id FROM users WHERE domain = 'aoi') -ORDER BY q.is_pinned DESC, q.created_at DESC, q.id DESC -LIMIT 6; - --- 5) 是否存在 created_at 完全相同的行(同刻多行若缺 id tiebreaker 会导致翻页跳行/重复) -SELECT q.created_at, count(*) AS n -FROM questions q -WHERE q.user_id = (SELECT id FROM users WHERE domain = 'aoi') - AND q.deleted_at IS NULL AND q.answer <> '' AND q.is_private = false -GROUP BY q.created_at -HAVING count(*) > 1 -ORDER BY n DESC -LIMIT 10; - - --- ============================================================================ --- PART B 纯 SQL 复刻 ListAPI 复合键集翻页(核心复现,DB 层数据自洽性判定) --- 游标锚点直接用 DB 行自身的 created_at(墙钟) 值推进——不经过任何 Go/时区转换, --- 专门回答: 「库里的数据 + 墙钟 keyset 排序」本身能否正确翻页、不重不漏。 --- ============================================================================ -SELECT 'PART B: keyset 翻页复现' AS step; - -DROP TABLE IF EXISTS pg_temp.diag_pages; - -DO $$ -DECLARE - v_uid numeric; -- users.id 也是 numeric - v_page int := 0; - v_rows int; - v_ids text; - v_prev_ids text := ''; - v_pin boolean; -- 上一页末条锚点 - v_ts timestamp; -- 上一页末条锚点(用 timestamp 变量,避免任何时区换算) - v_id numeric; - v_last_pin boolean; - v_last_ts timestamp; - v_last_id numeric; - r record; -BEGIN - SELECT id INTO v_uid FROM users WHERE domain = 'aoi' LIMIT 1; - IF v_uid IS NULL THEN - RAISE EXCEPTION 'users 中不存在 domain=aoi,请把脚本中的 aoi 换成你的箱 domain'; - END IF; - - CREATE TEMP TABLE IF NOT EXISTS diag_pages (page_no int, n_rows int, ids text); - DELETE FROM diag_pages; - - LOOP - v_page := v_page + 1; - v_ids := ''; - v_rows := 0; - v_last_pin := NULL; v_last_ts := NULL; v_last_id := NULL; - - FOR r IN - SELECT q.id, q.is_pinned, q.created_at - FROM questions q - WHERE q.user_id = v_uid - AND q.deleted_at IS NULL - AND q.answer IS NOT NULL AND q.answer <> '' - AND q.is_private = false - AND ( - CASE - WHEN v_page = 1 THEN TRUE -- 首页无游标 - WHEN v_pin THEN -- 锚点在置顶段: 置顶段内更小 + 全部非置顶 - (q.is_pinned = true AND (q.created_at < v_ts OR (q.created_at = v_ts AND q.id < v_id))) - OR q.is_pinned = false - ELSE -- 锚点在非置顶段: 非置顶段内更小 - q.is_pinned = false AND (q.created_at < v_ts OR (q.created_at = v_ts AND q.id < v_id)) - END - ) - ORDER BY q.is_pinned DESC, q.created_at DESC, q.id DESC - LIMIT 20 - LOOP - v_rows := v_rows + 1; - v_ids := v_ids || r.id::text || ','; - v_last_pin := r.is_pinned; - v_last_ts := r.created_at; - v_last_id := r.id; - END LOOP; - - IF v_rows = 0 THEN - RAISE NOTICE '第 % 页: 0 行 -> 翻页正常结束(空页,等价 has_more=false)', v_page; - EXIT; - END IF; - - v_ids := left(v_ids, -1); - INSERT INTO diag_pages VALUES (v_page, v_rows, v_ids); - RAISE NOTICE '第 % 页: % 行 ids=[%]', v_page, v_rows, v_ids; - - IF v_ids = v_prev_ids THEN - RAISE NOTICE '!!! 异常: 第 % 页与第 % 页完全重复,游标未前进(正是线上 bug 症状)', v_page, v_page - 1; - EXIT; - END IF; - v_prev_ids := v_ids; - - IF v_rows < 20 THEN - RAISE NOTICE '本页不足 20 行 -> 已到末页(等价 has_more=false),翻页完成'; - EXIT; - END IF; - IF v_page >= 30 THEN - RAISE NOTICE '!!! 异常: 已达 30 页上限仍未收敛,疑似死循环'; - EXIT; - END IF; - - v_pin := v_last_pin; - v_ts := v_last_ts; - v_id := v_last_id; - END LOOP; - - RAISE NOTICE '--- 汇总: 共 % 页 ---', (SELECT count(*) FROM diag_pages); -END $$; - --- 表格形式再输出一次(NOTICE 显示不全时看这里) -SELECT page_no, n_rows, ids FROM diag_pages ORDER BY page_no; - --- 跨页重复检测: 无重复时只返回上面提示 -SELECT '跨页重复检测: 若下方无第二张表结果则无重复' AS check_msg; -SELECT unnest(string_to_array(ids, ','))::numeric AS duplicated_question_id -FROM diag_pages -GROUP BY 1 -HAVING count(*) > 1; - - --- ============================================================================ --- PART C 时区/游标指纹核对 —— 区分「部署版本旧」vs「无时区列+时区解释错位」 --- 线上游标形如 0.1788566476678885000.3089(= pin=0, nano=1788566476678885000, id=3089) --- 判读: 下面第 1 条若能精确命中 DB 行(3089) => 服务端按「DB墙钟==UTC」生成游标, --- 与库值一致 => 无时区错位,问题指向部署版本/API 层; --- 若命中不了 => nano 与实际库值对不上,时区解释有偏差。 --- 用法: 把 nano 数字换成你线上抓到的 next_cursor 中间段;id 换成对应行。 --- ============================================================================ -SELECT 'PART C: 时区/游标指纹核对' AS step; - --- 1) nano 反向推导: 假定「墙钟==UTC」得到的 timestamp,是否正好等于 DB 中 3089 行的 created_at? -WITH c(nano) AS (VALUES (1788566476678885000::numeric)) -- <-- 替换为线上 next_cursor 中间段 -SELECT c.nano, - (to_timestamp(c.nano / 1000000000.0) AT TIME ZONE 'UTC')::timestamp(6) AS derived_ts_if_wall_is_utc, - (SELECT q.created_at FROM questions q WHERE q.id = 3089) AS actual_db_ts_of_id_3089, - ((to_timestamp(c.nano / 1000000000.0) AT TIME ZONE 'UTC')::timestamp(6) - = (SELECT q.created_at FROM questions q WHERE q.id = 3089)) AS match_3089 -FROM c; - --- 2) 命中检测(通用版): nano 推导出的墙钟时刻在库中对应哪些行(正常应含线上游标那行) -WITH c(nano) AS (VALUES (1788566476678885000::numeric)) -- <-- 同上 -SELECT q.id, q.created_at -FROM questions q, c -WHERE q.user_id = (SELECT id FROM users WHERE domain = 'aoi') - AND q.deleted_at IS NULL AND q.answer <> '' AND q.is_private = false - AND q.created_at = (to_timestamp(c.nano / 1000000000.0) AT TIME ZONE 'UTC')::timestamp(6) -ORDER BY q.id; - --- 3) 对照: 库中是否存在「把线上 JSON 的 Z 时刻原样解释为墙钟」对应的行 --- 即验证: 应用写库时是否把 UTC 时刻的墙钟数字直接存入无时区列(读写对称、无偏移) -SELECT q.id, q.created_at -FROM questions q -WHERE q.user_id = (SELECT id FROM users WHERE domain = 'aoi') - AND q.deleted_at IS NULL AND q.answer <> '' AND q.is_private = false - AND q.created_at IN ('2026-09-05 00:49:38.648502', '2026-09-05 00:01:16.678885') -- <-- 替换为线上首页首尾条的 created_at 文本 -ORDER BY q.created_at DESC, q.id DESC; - - --- ============================================================================ --- 判读指引 --- 1) PART B 第1页 id 应=线上首页20条;第2页应翻出剩余且无重复(DB 层自洽) --- 2) PART B 若出现「第N页与第N-1页完全重复」=> 直接用 SQL 在 DB 层复现了 bug -> 数据/排序问题 --- 3) PART C.1 match_3089=true 且 C.3 能命中 => 读写按 UTC 对称,无时区错位, --- DB keyset 正常时问题锁定在【部署版本/API层】;反之 => 无时区列+时区解释错位 --- 4) PART A.4 直接目视: DB 墙钟 vs 线上 JSON 的 Z 文本,差 8 小时即时区偏移实锤 --- ============================================================================ diff --git a/outputs/pg-cursor-pagination-diagnose.sql b/outputs/pg-cursor-pagination-diagnose.sql deleted file mode 100644 index 47812ea..0000000 --- a/outputs/pg-cursor-pagination-diagnose.sql +++ /dev/null @@ -1,224 +0,0 @@ --- ============================================================================ --- TamaBox「加载更多重复加载」根因排查 SQL(PostgreSQL) --- 目标箱: aoi(/api/v1/user/aoi/questions,/_/aoi/3105 底部历史列表同款数据) --- 依据: internal/db/questions.go GetByUserID (PinFirst) + route/question/page.go ListAPI --- ListAPI 等价条件: user_id=aoi箱 AND answer<>'' AND is_private=false AND deleted_at IS NULL --- 排序: is_pinned DESC, created_at DESC, id DESC;每页 20 条 --- 游标: ".."(末条编码,下一页取严格更小的行) --- 兼容: psql / pgAdmin / DBeaver(避免 psql 专属元命令;NOTICE 各客户端均可看到) --- 若提示语法错误 -> 说明当前连接不是 PostgreSQL,请改连 PG 实例 --- 使用: 按 PART 顺序整段复制执行,把「结果 + NOTICE 消息」原样粘贴回来 --- ============================================================================ - --- ============================================================================ --- PART A 环境 / 表结构 / 集合核对 --- 目的: 核对列类型与精度、索引,确认下方 SQL 筛选后条数与线上 API 总数一致 --- (线上 page_size=99 曾返回 38 条 -> 这里 answered_public_all 应=38, --- 若不一致说明还有隐藏过滤或连接的不是同一库) --- ============================================================================ - -SELECT 'PART A: 环境与结构核对' AS step; - -SELECT current_setting('server_version') AS server_version, - current_setting('TimeZone') AS session_tz, - version() AS full_version; - --- 1) questions 关键列定义(看 created_at 是 timestamptz 还是 timestamp、精度几位) -SELECT column_name, data_type, datetime_precision, is_nullable, column_default -FROM information_schema.columns -WHERE table_name = 'questions' - AND column_name IN ('id','created_at','updated_at','deleted_at', - 'user_id','answer','is_private','is_pinned','is_read') -ORDER BY ordinal_position; - --- 2) user_id 相关索引(若只有单列 user_id 索引即可;看是否缺复合索引影响性能,不影响正确性) -SELECT indexname, indexdef -FROM pg_indexes -WHERE tablename = 'questions' AND indexdef ILIKE '%user_id%'; - --- 3) 目标集合核对:应=线上 API 总数(曾为 38) -SELECT u.id AS aoi_user_id, - u.domain AS domain, - count(*) FILTER (WHERE q.answer <> '' AND q.is_private = false) - AS answered_public_all, - count(*) FILTER (WHERE q.answer <> '' AND q.is_private = false AND q.is_pinned) - AS answered_public_pinned, - count(*) AS all_rows_for_user -FROM users u -LEFT JOIN questions q ON q.user_id = u.id AND q.deleted_at IS NULL -WHERE u.domain = 'aoi' -GROUP BY u.id, u.domain; - --- 4) 该集合最新 5 条:把 created_at 按 UTC 与 UnixNano 打出来, --- 便于和线上 API JSON / next_cursor(形如 0.1788566476678885000.3089)逐位目视比对 -SELECT q.id, - q.is_pinned, - q.created_at, - to_char(q.created_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US') AS created_at_utc_text, - (extract(epoch FROM q.created_at) * 1000000000)::numeric(30,0) AS created_at_unix_nano -FROM questions q -JOIN users u ON u.id = q.user_id AND u.domain = 'aoi' -WHERE q.answer <> '' AND q.is_private = false AND q.deleted_at IS NULL -ORDER BY q.is_pinned DESC, q.created_at DESC, q.id DESC -LIMIT 5; - --- 5) 是否存在 created_at 完全相同的行(同一时间多行时若旧版漏 id tiebreaker 会翻页跳行/重复) -SELECT q.created_at, count(*) AS n -FROM questions q -JOIN users u ON u.id = q.user_id AND u.domain = 'aoi' -WHERE q.answer <> '' AND q.is_private = false AND q.deleted_at IS NULL -GROUP BY q.created_at -HAVING count(*) > 1 -ORDER BY n DESC -LIMIT 10; - - --- ============================================================================ --- PART B 用纯 SQL 完整复刻 ListAPI 的复合键集翻页(核心复现) --- 目的: 逐页打印每页的 id 序列。若 DB 层正常: 第1页 20 条、第2页 18 条后结束, --- 且无任何一行与上一页重复(总 38 条不重不漏)。 --- 若 DB 层有 bug: 会出现重复页 / 跳页 / 死循环(脚本会检测并提前退出)。 --- 判读: --- [DB 层正常] 第1页id集合与线上首页一致,第2页正常翻出剩余 -> 问题在应用层 --- (部署版本旧 / GORM 参数转换),与 DB 无关,见 PART C 指纹进一步定位。 --- [DB 层异常] 某页与上一页完全重复 / 提前空页 / 顺序错乱 -> 问题在数据或库配置, --- 把输出贴回即可看到错在哪一页。 --- ============================================================================ - -SELECT 'PART B: keyset 翻页复现' AS step; - -DROP TABLE IF EXISTS pg_temp.diag_pages; - -DO $$ -DECLARE - v_uid bigint; - v_page int := 0; - v_rows int; - v_ids text; - v_prev_ids text := ''; - v_pin boolean; -- 上一页末条 is_pinned(游标锚点) - v_ts timestamptz; -- 上一页末条 created_at(游标锚点) - v_id bigint; -- 上一页末条 id(游标锚点) - v_last_pin boolean; - v_last_ts timestamptz; - v_last_id bigint; - v_note text; - r record; -BEGIN - SELECT id INTO v_uid FROM users WHERE domain = 'aoi' LIMIT 1; - IF v_uid IS NULL THEN - RAISE EXCEPTION 'users 表中不存在 domain=aoi 的用户,请把脚本里的 aoi 换成你的箱 domain'; - END IF; - - CREATE TEMP TABLE diag_pages (page_no int, n_rows int, ids text); - - LOOP - v_page := v_page + 1; - v_ids := ''; - v_rows := 0; - v_last_pin := NULL; v_last_ts := NULL; v_last_id := NULL; - - FOR r IN - SELECT q.id, q.is_pinned, q.created_at - FROM questions q - WHERE q.user_id = v_uid - AND q.answer <> '' - AND q.is_private = false - AND q.deleted_at IS NULL - AND ( - CASE - WHEN v_page = 1 THEN TRUE -- 首页无游标 - WHEN v_pin THEN -- 游标位于置顶段: 置顶段更小 + 全部非置顶 - (q.is_pinned = true AND (q.created_at < v_ts OR (q.created_at = v_ts AND q.id < v_id))) - OR q.is_pinned = false - ELSE -- 游标位于非置顶段: 非置顶段内更小 - q.is_pinned = false AND (q.created_at < v_ts OR (q.created_at = v_ts AND q.id < v_id)) - END - ) - ORDER BY q.is_pinned DESC, q.created_at DESC, q.id DESC - LIMIT 20 - LOOP - v_rows := v_rows + 1; - v_ids := v_ids || r.id::text || ','; - v_last_pin := r.is_pinned; - v_last_ts := r.created_at; - v_last_id := r.id; - END LOOP; - - IF v_rows = 0 THEN - RAISE NOTICE '第 % 页: 0 行 -> 翻页正常结束(空页,等价 has_more=false)', v_page; - EXIT; - END IF; - - v_ids := left(v_ids, -1); - INSERT INTO diag_pages VALUES (v_page, v_rows, v_ids); - RAISE NOTICE '第 % 页: % 行 ids=[%]', v_page, v_rows, v_ids; - - -- 死循环防护: 本页与上一页逐字相同(key set 未前进)则提前退出并标记异常 - IF v_ids = v_prev_ids THEN - RAISE NOTICE '!!! 异常: 第 % 页与第 % 页内容完全重复,游标未前进(复现线上 bug 的症状)', v_page, v_page - 1; - EXIT; - END IF; - v_prev_ids := v_ids; - - -- 末页判定: 本页不足 20 行 -> 已无下一页(等价 ListAPI 的 has_more=false) - IF v_rows < 20 THEN - RAISE NOTICE '本页不足 20 行 -> 已到末页(等价 has_more=false),翻页完成'; - EXIT; - END IF; - - IF v_page >= 30 THEN - RAISE NOTICE '!!! 异常: 已达 30 页上限仍未收敛,疑似死循环'; - EXIT; - END IF; - - -- 前进游标锚点 = 本页末条 - v_pin := v_last_pin; - v_ts := v_last_ts; - v_id := v_last_id; - END LOOP; - - RAISE NOTICE '--- 汇总: 共 % 页 ---', (SELECT count(*) FROM diag_pages); -END $$; - --- 同一会话内再以表格形式输出一次(若客户端 NOTICE 显示不全,看这个即可) -SELECT page_no, n_rows, ids -FROM pg_temp.diag_pages -ORDER BY page_no; - --- 重复检测: 跨页是否存在重复 id(正常应为 0 行,无重复时只显示上一条提示) -SELECT '跨页重复检测: 若下方无第二张表结果则无重复' AS check_msg; -SELECT unnest(string_to_array(ids, ','))::bigint AS duplicated_question_id -FROM pg_temp.diag_pages -GROUP BY 1 -HAVING count(*) > 1; - - --- ============================================================================ --- PART C created_at 精度指纹核对(可选,用于区分「部署版本旧」vs「DB 环境问题」) --- 目的: 若线上 next_cursor 里的 UnixNano(如 0.1788566476678885000.3089 中的 --- 1788566476678885000)能在下方查询里精确命中对应 id 的 created_at_unix_nano, --- 说明 API 下发的游标与 DB 行完全对应 -> 问题几乎锁定在部署版本/API 层。 --- 若命中不了 / 差着末几位 -> 说明 DB 存值与线上 JSON 时间对不上,是库环境问题。 --- 用法: 把下方 id 列表替换成你想核对的线上行(默认最新几条 + 3089/3105)。 --- ============================================================================ - -SELECT 'PART C: created_at 精度指纹' AS step; - -SELECT q.id, - q.is_pinned, - to_char(q.created_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US') AS created_at_utc_text, - (extract(epoch FROM q.created_at) * 1000000000)::numeric(30,0) AS created_at_unix_nano, - (extract(epoch FROM q.created_at) * 1000000000)::numeric(30,0) % 1000 AS nano_tail3 -- 应为0: 微秒精度*1000 -FROM questions q -WHERE q.id IN (3109, 3105, 3089, 3088) -- 替换成线上首页前几条 id -ORDER BY q.id DESC; - --- ============================================================================ --- 判读指引(复制上面的 PART A/B 即可,C 按需): --- 1) PART A.3 的 answered_public_all 应与线上 API 总数一致(曾为 38) --- 2) PART B 第1页应=线上首页20条;第2页应翻出剩余且与第1页无重复 -> --- DB 层 keyset 自洽 = 问题在应用层(部署版本/参数转换) --- 3) 若 PART B 出现「某页与上一页完全相同」= 直接在 DB 层复现了 bug --- 4) PART C 的 unix_nano 与线上 next_cursor 中间段逐位一致 = 版本/API 层问题 --- ============================================================================ diff --git a/outputs/preview-assets/base.css b/outputs/preview-assets/base.css deleted file mode 100644 index a7b8253..0000000 --- a/outputs/preview-assets/base.css +++ /dev/null @@ -1,779 +0,0 @@ -/* TamaBox 全站基础样式:设计令牌 / 主题覆盖 / 组件质感 / 响应式。 - 由 base/header.html 内联块抽出,集中一份便于缓存与维护。 - ⚠️ 改完务必把 header 里的 ?v= 版本号 +1,否则老访客仍用缓存。 */ - -/* 设计令牌:亮色为默认(:root),深色在 html[data-theme="dark"] 覆盖。 - 页面与组件样式一律引用 var(--tb-*),不再写死色值/圆角/阴影; - var() 的第二个参数仅作兜底(旧缓存页、邮件模板等不引入本文件的场景)。 - 改配色只动这一块,亮暗两套自动保持一致。 */ - :root { - --tb-bg: #f7f8fa; - --tb-surface: #ffffff; - --tb-surface-2: #f4f6f8; - --tb-surface-hover: #f3f7ff; - /* 顶栏底色:默认跟随卡片面;主题想消除「纯白顶栏压在彩色底上」的割裂时单独覆盖 */ - --tb-navbar: var(--tb-surface); - --tb-border: #e8eaee; - --tb-text: #1f2329; - --tb-text-secondary: #5c6370; - --tb-text-muted: #8a919c; - --tb-primary: #1e87f0; - --tb-primary-weak: rgba(30, 135, 240, 0.10); - --tb-radius: 12px; - --tb-radius-sm: 8px; - --tb-radius-xs: 6px; - --tb-shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 12px 28px -18px rgba(16, 24, 40, 0.20); - --tb-shadow-hover: 0 1px 2px rgba(16, 24, 40, 0.05), 0 18px 40px -20px rgba(16, 24, 40, 0.28); - --tb-font: system-ui, -apple-system, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif; - /* 整页背景:主题可覆盖。图案层叠在背景图之上,两者都不用时保持纯色底。 */ - --tb-page-image: none; - --tb-page-pattern: none; - --tb-page-pattern-size: auto; - /* 装饰层:固定像素尺寸 + 四角定位,不受 cover 裁切影响(整页背景图做不到这一点, - 视口比例一变,贴在图片边缘的插画就会被裁掉)。 - ⚠️ 固定 6 个槽位:background-* 各列表的层数必须恒定(1 图案 + 6 装饰 + 1 背景 = 8), - 缺省用 none 占位;值的个数一变,多背景的分层就会整体错位。 */ - --tb-deco: none, none, none, none, none, none; - --tb-deco-size: auto, auto, auto, auto, auto, auto; - --tb-deco-position: left top, left top, left top, left top, left top, left top; - } - html[data-theme="dark"] { - --tb-bg: #1a1d23; - --tb-surface: #242830; - --tb-surface-2: #2d323c; - --tb-surface-hover: rgba(92, 141, 255, 0.12); - --tb-border: #3a404c; - --tb-text: #d8dce4; - --tb-text-secondary: #a8b0bd; - --tb-text-muted: #8b93a1; - --tb-primary: #5c8dff; - --tb-primary-weak: rgba(92, 141, 255, 0.14); - --tb-shadow: 0 1px 2px rgba(0, 0, 0, 0.30), 0 12px 28px -18px rgba(0, 0, 0, 0.55); - --tb-shadow-hover: 0 1px 2px rgba(0, 0, 0, 0.35), 0 18px 40px -20px rgba(0, 0, 0, 0.65); - } - -/* UIkit 补丁:复刻原自建版 "modify-black-font"(官方版与自建版仅 3 处颜色差异——把有色容器内默认白字/白标题改为黑字)。 - 模板当前只用到 .uk-light 容器(用户背景封面,question/list.html、question-not-found.html; - 其中 h3/p 已由 #usernamecolor/#introcolor 内联 style 覆盖),故只需补该容器的 3 处投影即可保持渲染一致。 - 若日后新增 .uk-card-primary/.uk-card-secondary/.uk-section-primary/.uk-tile-*/.uk-overlay-primary 等容器且需黑字,须同步扩展。 */ - .uk-light { color: rgba(0,0,0,.7); } - .uk-light .uk-h1, .uk-light .uk-h2, .uk-light .uk-h3, .uk-light .uk-h4, .uk-light .uk-h5, .uk-light .uk-h6, - .uk-light .uk-heading-2xlarge, .uk-light .uk-heading-large, .uk-light .uk-heading-medium, .uk-light .uk-heading-small, .uk-light .uk-heading-xlarge, - .uk-light h1, .uk-light h2, .uk-light h3, .uk-light h4, .uk-light h5, .uk-light h6 { color: #000; } - .uk-light .uk-table th { color: rgba(0,0,0,.7); } - - .grecaptcha-badge {visibility: hidden;} - .icon-container { - display: flex; - justify-content: center; - width: 100%; - } - .icon { - margin-left: 10px; - width: 20px; /* 设置图标的宽度 */ - height: auto; /* 保持图标的高度自动调整 */ - } - .emoji-placeholder { - display: inline-block; - animation: jump 0.5s ease-in-out; - } - - @keyframes jump { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-5px); } - } - /* 防止 UIkit Dropbar 初始闪烁 */ - .uk-dropbar { - opacity: 0; - visibility: hidden; - transition: opacity 0.2s ease, visibility 0.2s ease; - } - - .uk-dropbar.uk-open { - opacity: 1; - visibility: visible; - } - - a.uk-button.uk-button-link { - text-transform: none; - } - - /* 移动端优化:缩小字体和间距,避免右侧空白 */ - @media (max-width: 640px) { - .uk-logo { - font-size: 1rem; - } - .uk-navbar-nav a { - font-size: 0.78rem; - padding: 0 4px; - } - .emoji-placeholder { - font-size: 1rem; - } - /* 移动端用户名限宽省略,避免长英文用户名撑爆顶栏 */ - .tb-nav-username { - max-width: 64px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - display: inline-block; - vertical-align: middle; - } - } - -/* 深色模式:html[data-theme="dark"] 时覆盖 UIkit 3.3.3 编译主题的关键颜色 */ - /* 变量(--tb-*)已上移到 head 顶部的令牌块,亮暗两套同名常量在那里定义。 */ - /* 根元素与 body 一起覆盖,确保整个视口(含内容不足一屏时的空白区)都是深色背景 */ - html[data-theme="dark"], - html[data-theme="dark"] body { - background-color: var(--tb-bg) !important; - color: var(--tb-text) !important; - } - /* 通用表面容器 */ - html[data-theme="dark"] .uk-card, - html[data-theme="dark"] .uk-card-default, - html[data-theme="dark"] .uk-card-secondary, - html[data-theme="dark"] .uk-dropbar, - html[data-theme="dark"] .uk-navbar-dropdown, - html[data-theme="dark"] .uk-modal-dialog, - html[data-theme="dark"] .uk-dropdown, - html[data-theme="dark"] .uk-offcanvas-bar, - html[data-theme="dark"] .uk-section, - html[data-theme="dark"] .uk-tile, - html[data-theme="dark"] .uk-panel, - html[data-theme="dark"] .uk-background-default, - html[data-theme="dark"] .uk-search-default .uk-search-input { - background-color: var(--tb-surface) !important; - color: var(--tb-text) !important; - } - html[data-theme="dark"] .uk-navbar-container { - background-color: var(--tb-navbar, var(--tb-surface)) !important; - color: var(--tb-text) !important; - } - html[data-theme="dark"] .uk-navbar-nav > li > a, - html[data-theme="dark"] .uk-card a, - html[data-theme="dark"] .uk-link, - html[data-theme="dark"] a.uk-button-link, - html[data-theme="dark"] .uk-navbar-dropdown-nav > li > a, - html[data-theme="dark"] .uk-dropdown-nav > li > a, - html[data-theme="dark"] .uk-list a { - color: var(--tb-text) !important; - } - html[data-theme="dark"] .uk-navbar-nav > li > a:hover, - html[data-theme="dark"] .uk-card a:hover, - html[data-theme="dark"] .uk-navbar-dropdown-nav > li > a:hover, - html[data-theme="dark"] .uk-dropdown-nav > li > a:hover { - color: var(--tb-primary) !important; - } - html[data-theme="dark"] .uk-input, - html[data-theme="dark"] .uk-textarea, - html[data-theme="dark"] .uk-select { - background-color: var(--tb-surface-2) !important; - color: var(--tb-text) !important; - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] .uk-input:focus, - html[data-theme="dark"] .uk-textarea:focus, - html[data-theme="dark"] .uk-select:focus { - border-color: var(--tb-primary) !important; - } - html[data-theme="dark"] .uk-text-muted, - html[data-theme="dark"] .uk-text-meta, - html[data-theme="dark"] .uk-text-secondary, - html[data-theme="dark"] .uk-text-muted a, - html[data-theme="dark"] .uk-text-small.uk-text-muted { - color: var(--tb-text-muted) !important; - } - html[data-theme="dark"] .uk-divider-icon::after, - html[data-theme="dark"] hr, - html[data-theme="dark"] .uk-table th, - html[data-theme="dark"] .uk-table td, - html[data-theme="dark"] .uk-list-divider > li:nth-child(n+2) { - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] .uk-button-default { - background-color: var(--tb-surface-2) !important; - color: var(--tb-text) !important; - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] .uk-modal-title, - html[data-theme="dark"] h1, html[data-theme="dark"] h2, html[data-theme="dark"] h3, - html[data-theme="dark"] h4, html[data-theme="dark"] h5, html[data-theme="dark"] h6, - html[data-theme="dark"] .uk-card-title { - color: var(--tb-text) !important; - } - html[data-theme="dark"] .uk-alert-warning, - html[data-theme="dark"] .uk-alert-danger, - html[data-theme="dark"] .uk-alert-success, - html[data-theme="dark"] .uk-alert-primary { - background-color: var(--tb-surface-2) !important; - color: var(--tb-text) !important; - } - html[data-theme="dark"] #math-captcha-modal .uk-modal-dialog { - background-color: var(--tb-surface) !important; - color: var(--tb-text) !important; - } - html[data-theme="dark"] #math-captcha-modal .captcha-equation-box { - background-color: var(--tb-surface-2) !important; - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] #math-captcha-modal .math-captcha-modal-question { - color: var(--tb-text) !important; - } - html[data-theme="dark"] #math-captcha-modal #captcha-modal-answer { - background-color: var(--tb-surface) !important; - color: var(--tb-text) !important; - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] #math-captcha-modal #captcha-modal-answer::placeholder { - color: var(--tb-text-muted) !important; - } - html[data-theme="dark"] #math-captcha-modal .uk-text-meta, - html[data-theme="dark"] #math-captcha-modal #captcha-modal-refresh { - color: var(--tb-text-muted) !important; - } - html[data-theme="dark"] #math-captcha-modal #captcha-modal-refresh:hover { - color: var(--tb-primary) !important; - } - html[data-theme="dark"] .uk-label { - color: #fff !important; - } - /* 自定义组件(探索页推荐卡片等)深色覆盖 */ - html[data-theme="dark"] .recommend-card, - html[data-theme="dark"] .recommend-card:hover { - background-color: var(--tb-surface-2) !important; - } - html[data-theme="dark"] .explore-name a { - color: var(--tb-text) !important; - } - html[data-theme="dark"] .explore-name a:hover { - color: var(--tb-primary) !important; - } - html[data-theme="dark"] .explore-intro { - color: var(--tb-text-muted) !important; - } - /* 切换按钮图标切换(内联 SVG,不依赖 UIkit 图标库) */ - #theme-toggle .tb-icon-light, #theme-toggle-mobile .tb-icon-light, - #theme-toggle .tb-icon-dark, #theme-toggle-mobile .tb-icon-dark { - display: inline-flex; - align-items: center; - } - #theme-toggle .tb-icon-light svg, #theme-toggle-mobile .tb-icon-light svg, - #theme-toggle .tb-icon-dark svg, #theme-toggle-mobile .tb-icon-dark svg { display: block; } - #theme-toggle .tb-icon-dark, #theme-toggle-mobile .tb-icon-dark { display: none; } - html[data-theme="dark"] #theme-toggle .tb-icon-light, - html[data-theme="dark"] #theme-toggle-mobile .tb-icon-light { display: none; } - html[data-theme="dark"] #theme-toggle .tb-icon-dark, - html[data-theme="dark"] #theme-toggle-mobile .tb-icon-dark { display: inline-flex; } - -/* 基础质感层:只调整外观(圆角/投影/字体/数字对齐),不改任何结构与布局。 - 颜色一律取自头部令牌,亮暗两套自动生效,因此这里不需要再写 html[data-theme="dark"] 分支。 */ - /* 根元素与 body 同时上色:内容不足一屏时,body 之外的视口区域也要是同一底色 */ - html, - body { - font-family: var(--tb-font); - background-color: var(--tb-bg); - /* 主题的整页背景:四角装饰 + 平铺图案 + cover 背景图(共 6 层,顺序即绘制顺序) */ - background-image: var(--tb-page-pattern), var(--tb-deco), var(--tb-page-image); - background-size: var(--tb-page-pattern-size), var(--tb-deco-size), cover; - background-repeat: repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat, no-repeat, - no-repeat; - background-position: left top, var(--tb-deco-position), center center; - background-attachment: scroll, fixed, fixed, fixed, fixed, fixed, fixed, fixed; - color: var(--tb-text); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - /* 卡片:12px 圆角 + 一层极淡接触影 + 一层大扩散影(UIkit 默认的 0 5px 15px 偏硬) */ - .uk-card { - border-radius: var(--tb-radius); - } - .uk-card-default { - background-color: var(--tb-surface); - box-shadow: var(--tb-shadow); - } - .uk-card-default.uk-card-hover:hover { - box-shadow: var(--tb-shadow-hover); - } - .uk-card-header { - border-radius: var(--tb-radius) var(--tb-radius) 0 0; - } - .uk-card-footer { - border-radius: 0 0 var(--tb-radius) var(--tb-radius); - } - .uk-card, - .uk-button, - .uk-alert, - .uk-label { - transition: box-shadow 0.18s ease, background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease; - } - /* 交互元素:统一 8px 圆角,标签 6px */ - .uk-button { - border-radius: var(--tb-radius-sm); - } - .uk-input, - .uk-select, - .uk-textarea, - .uk-form-small, - .uk-search-input { - border-radius: var(--tb-radius-sm); - } - .uk-label { - border-radius: var(--tb-radius-xs); - } - .uk-alert { - border-radius: 10px; - } - .uk-modal-dialog { - border-radius: 14px; - } - .uk-dropdown, - .uk-navbar-dropdown { - border-radius: 10px; - box-shadow: var(--tb-shadow); - } - .uk-navbar-container:not(.uk-navbar-transparent) { - background-color: var(--tb-navbar, var(--tb-surface)); - } - /* 键盘可达性:仅在键盘聚焦时出现描边,鼠标点击不出现 */ - .uk-button:focus-visible, - .uk-navbar-nav > li > a:focus-visible { - outline: 2px solid var(--tb-primary); - outline-offset: 2px; - } - /* 次要文字统一走令牌,避免 #999 / #8a919c 混用 */ - .uk-text-muted, - .uk-text-meta { - color: var(--tb-text-muted); - } - -/* 主题选择器(设置页):色块 + 名称。纯展示,不参与主题令牌,所以放在基础层。 */ -.tb-theme-hint { - margin: 0 0 6px; - font-size: 0.8rem; - color: var(--tb-text-muted); -} -.tb-theme-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); - gap: 8px; - margin: 8px 0 2px; -} -.tb-theme-opt { - display: flex; - align-items: center; - gap: 8px; - padding: 7px 9px; - border: 1px solid var(--tb-border); - border-radius: var(--tb-radius-sm); - cursor: pointer; -} -.tb-theme-opt:hover { - background-color: var(--tb-surface-hover); -} -.tb-theme-opt input { - margin: 0; -} -.tb-theme-swatch { - flex: 0 0 auto; - width: 26px; - height: 26px; - border-radius: var(--tb-radius-xs); - border: 1px solid var(--tb-border); - background-color: var(--tb-surface-2); -} -/* 选中态用描边而不是 :has(),兼容老 WebView */ -.tb-theme-opt input:checked + .tb-theme-swatch { - outline: 2px solid var(--tb-primary); - outline-offset: 1px; -} -.tb-theme-name { - font-size: 0.84rem; - line-height: 1.3; -} -.tb-theme-opt input:checked ~ .tb-theme-name { - color: var(--tb-primary); -} - -/* UIkit 组件跟随主题色:UIkit 3.3.3 的主色是编译期常量(#1e87f0), - CSS 变量注入不进去,只能按组件覆盖。基础主题的 --tb-primary 恰好就是 #1e87f0, - 所以这段对默认外观零改动,只是让箱主主题能贯穿到按钮/链接/勾选。 */ -a, .uk-link { - color: var(--tb-primary); -} -a:hover, .uk-link:hover { - color: var(--tb-primary); -} -.uk-button-primary { - background-color: var(--tb-primary); -} -.uk-button-primary:hover, -.uk-button-primary:focus { - background-color: var(--tb-primary); - filter: brightness(1.07); -} -.uk-checkbox:checked, -.uk-checkbox:indeterminate, -.uk-radio:checked { - background-color: var(--tb-primary); - border-color: transparent; -} -/* 胶囊按钮/状态标签(提问管理里的「置顶」「未回答」,投票的「已参与」)也走主色。 - danger / warning / success / secondary 是语义色,必须用 :not() 排除—— - 同特异性下 base.css 在 UIkit 之后加载,不排除会把它们一并刷成主色。 */ -.uk-label:not(.uk-label-danger):not(.uk-label-warning):not(.uk-label-success):not(.uk-label-secondary) { - background-color: var(--tb-primary); -} -.uk-input:focus, -.uk-select:focus, -.uk-textarea:focus { - border-color: var(--tb-primary); -} -.uk-tab > .uk-active > a { - border-color: var(--tb-primary); -} -.uk-text-primary { - color: var(--tb-primary) !important; -} - -/* 轻量胶囊:用于投票类型、热门标签等「只是分类信息」的位置。 - 实心 uk-label 一屏多个会变成一排色块,这里统一降级为浅底 + 次要色文字。 */ -.tb-chip { - display: inline-block; - padding: 1px 8px; - border-radius: var(--tb-radius-xs); - background-color: var(--tb-surface-2); - color: var(--tb-text-secondary); - font-size: 0.72rem; - line-height: 1.7; - white-space: nowrap; -} -a.tb-chip:hover { - background-color: var(--tb-primary-weak); - color: var(--tb-primary); - text-decoration: none; -} - -/* 统计/日期等数字列等宽对齐,避免刷新时左右跳动 */ - .uk-table, - .tb-num { - font-variant-numeric: tabular-nums; - } - /* 图片上传条:原为 item/new-question 两处各写一份内联 style(写死 #f8f8f8,深色下露白), - 收敛到一处并走令牌,亮暗自动适配 */ - .tb-img-uploader { - width: 100%; - background-color: var(--tb-surface-2); - display: flex; - padding-top: 5px; - padding-bottom: 5px; - align-items: center; - justify-content: flex-start; - gap: 5px; - } - /* 背景图封面(提问箱主页):渐变遮罩,保证用户自选文字色叠加后仍可读。 - 默认浅色遮罩(页面 .uk-light 补丁把文字兜底成深色);若箱主选了浅色文字, - question/list.html 会按亮度加上 .tb-cover-dark 换成深色遮罩。 */ - .tb-cover { - position: relative; - border-radius: var(--tb-radius) var(--tb-radius) 0 0; - overflow: hidden; - } - /* ⚠️ 必须带 .uk-flex 复合选择器:UIkit 3.3.3 里有 - .uk-flex::after,.uk-flex::before{display:none}(优先级 0,2,0), - 会把这个遮罩伪元素整个干掉——不报错、只是什么都不显示(踩过一次)。 - 单写 .tb-cover::after 优先级不够,会被它压回 display:none。 */ - .tb-cover::after, - .tb-cover.uk-flex::after { - content: ""; - display: block; - position: absolute; - inset: 0; - z-index: 0; - background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.58) 55%, rgba(255, 255, 255, 0.84) 100%); - pointer-events: none; - } - .tb-cover.tb-cover-dark::after { - background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.16) 45%, rgba(0, 0, 0, 0.46) 100%); - } - .tb-cover > * { - position: relative; - z-index: 1; - } - .tb-cover h3, - .tb-cover p { - text-shadow: 0 1px 10px rgba(255, 255, 255, 0.65); - } - .tb-cover.tb-cover-dark h3, - .tb-cover.tb-cover-dark p { - text-shadow: 0 1px 12px rgba(0, 0, 0, 0.45); - } - /* 尊重系统的减少动效设置 */ - @media (prefers-reduced-motion: reduce) { - .uk-card, - .uk-button, - .emoji-placeholder { - transition: none; - animation: none; - } - } - -/* 顶栏自适应:导航全部直接显示,窄屏时分级缩小字号/间距,保证英文/日文等长文字不溢出 */ - .uk-navbar-nav a { - font-size: 0.95rem; - padding: 0 10px; - } - /* 未登录移动端占位:桌面端隐藏,仅 ≤640px 显示(见下方 media 查询)。 - 桌面端布局与改动前完全一致,不受影响。 */ - .tb-mobile-balancer { display: none; } - /* 桌面端保持 UIkit 原生居中三列布局(无需额外 CSS) */ - .tb-navbar .uk-logo { - white-space: nowrap; - } - @media (max-width: 1200px) { - .uk-navbar-nav a { - font-size: 0.88rem; - padding: 0 8px; - } - } - @media (max-width: 1000px) { - .uk-navbar-nav a { - font-size: 0.82rem; - padding: 0 6px; - } - .uk-logo { - font-size: 1.1rem; - } - /* 登录用户名限宽,过长省略号截断 */ - .tb-nav-username { - max-width: 110px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - display: inline-block; - vertical-align: middle; - } - } - @media (max-width: 800px) { - .uk-navbar-nav a { - font-size: 0.78rem; - padding: 0 4px; - } - .uk-logo { - font-size: 1rem; - } - .tb-nav-username { - max-width: 80px; - } - } - /* 手机端(≤640px):沿用 UIkit 桌面端 navbar 的原生三列定位。 - 不再重写 .uk-navbar-center / center-left / center-right 的定位, - 避免 logo 被挤到下一行;底部语言与深色模式按钮仍由下方独立栏承载。 */ - @media (max-width: 640px) { - .tb-navbar { - position: relative; - min-height: 56px; - overflow: visible; - } - .tb-navbar > .uk-navbar-center { - position: relative !important; - top: auto !important; - left: auto !important; - right: auto !important; - bottom: auto !important; - transform: none !important; - display: flex !important; - align-items: center; - justify-content: center; - width: 100%; - min-height: 56px; - white-space: nowrap; - flex-wrap: nowrap; - } - .tb-navbar .uk-navbar-center-left, - .tb-navbar .uk-navbar-center-right { - position: static !important; - top: auto !important; - left: auto !important; - right: auto !important; - bottom: auto !important; - transform: none !important; - display: flex !important; - align-items: center; - flex: 0 0 auto; - white-space: nowrap; - } - .tb-navbar .uk-navbar-center-left > div, - .tb-navbar .uk-navbar-center-right > div { - display: flex; - align-items: center; - white-space: nowrap; - } - .tb-navbar .uk-navbar-nav { - display: flex; - flex-wrap: nowrap; - flex: 0 0 auto; - } - .tb-navbar .uk-navbar-nav > li > a, - .tb-navbar .uk-logo { - white-space: nowrap; - } - /* 未登录:移动端显示与「我的」等宽的隐藏占位,使左栏「探索+占位」与 - 右栏「注册/登录」两项等宽,logo 真正居中,左右视觉平衡 */ - .tb-mobile-balancer { display: block; } - } - -/* 标签样式的按钮(管理页置顶等): - 尺寸完全继承 .uk-label(font-size/padding/line-height/text-transform 等), - 仅覆盖 button 元素特有的默认样式(边框/字体族/光标等),保证与相邻 span 标签完全一致。 */ - .tb-label-btn { - border: none !important; - cursor: pointer; - font-family: inherit; - margin: 0; - vertical-align: middle; - outline: none; - background-image: none; - box-sizing: border-box; - -webkit-appearance: none; - appearance: none; - } - .tb-label-btn:hover { - opacity: 0.85; - } - /* 公开页置顶小图标:仅一个图钉,不占行高、不影响原布局 */ - .tb-pin-flag { - font-size: 0.9em; - margin-right: 4px; - vertical-align: -1px; - cursor: default; - } - -/* Markdown 渲染排版(问题/回答/追问正文,左对齐以保留排版格式) */ - /* 纯文本列表:保留换行与连续空格(pre-wrap),不渲染 Markdown 大字号/彩色 */ - .tb-preline { - white-space: pre-wrap; - } - /* 纯文本列表:超过 5 行截断并显示省略号(不足 5 行则完整显示) */ - .tb-clamp-5 { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 5; - overflow: hidden; - } - .md-body { - line-height: 1.7; - text-align: left; - word-break: break-word; - } - .md-body p { margin: 0.5em 0; } - .md-body h1, .md-body h2, .md-body h3, - .md-body h4, .md-body h5, .md-body h6 { - margin: 0.8em 0 0.4em; - font-weight: 600; - line-height: 1.4; - } - .md-body ul, .md-body ol { padding-left: 1.6em; margin: 0.5em 0; } - .md-body li { margin: 0.2em 0; } - .md-body blockquote { - border-left: 3px solid #d3d1c7; - margin: 0.6em 0; - padding: 0.2em 1em; - color: #5f5e5a; - } - .md-body pre { - background: #f5f5f5; - padding: 0.8em 1em; - border-radius: 6px; - overflow-x: auto; - margin: 0.6em 0; - } - .md-body code { - background: #f0f0f0; - padding: 0.1em 0.35em; - border-radius: 4px; - font-size: 0.9em; - } - .md-body pre code { background: none; padding: 0; } - .md-body img { max-width: 100%; height: auto; } - .md-body table { border-collapse: collapse; margin: 0.6em 0; } - .md-body th, .md-body td { - border: 1px solid #ddd; - padding: 0.4em 0.8em; - } - .md-body hr { margin: 1em 0; } - html[data-theme="dark"] .md-body blockquote { - border-color: #3a404c; - color: #8b93a1; - } - html[data-theme="dark"] .md-body pre, - html[data-theme="dark"] .md-body code { background: #2d323c; } - html[data-theme="dark"] .md-body th, - html[data-theme="dark"] .md-body td { border-color: #3a404c; } - -/* 投票结果占比条:模板已改用 UIkit 3 原生 , - 这里只把默认的 15px 高 / 500px 圆角 / 20px 外边距收紧到与页面一致的规格。 - (原先为 UIkit 2 的 .uk-progressbar 结构打的补丁已删除) */ -.uk-progress { - height: 10px; - margin: 0; - border-radius: var(--tb-radius-xs); - background-color: var(--tb-surface-2); -} -.uk-progress::-webkit-progress-bar { - background-color: var(--tb-surface-2); - border-radius: var(--tb-radius-xs); -} -.uk-progress::-webkit-progress-value { - /* 两端都圆:值 100% 时若只圆左边,右端方角会顶出轨道圆角 */ - background-color: var(--tb-primary); - border-radius: var(--tb-radius-xs); -} -.uk-progress::-moz-progress-bar { - background-color: var(--tb-primary); - border-radius: var(--tb-radius-xs); -} - -/* 移动端底部操作栏:固定底栏承载语言切换与深色模式,桌面端隐藏 */ - #tb-mobile-utils { - display: none; - } - @media (max-width: 640px) { - /* 顶栏不再容纳语言/深色按钮,恢复原有导航空间 */ - .tb-nav-utils { - display: none !important; - } - /* 手机端隐藏登录用户名(左边仅保留"探索"),保证 logo 居中、 - 两侧各不超过 2 个短按钮,避免用户名宽度挤压布局 */ - .tb-nav-username { - display: none !important; - } - /* 固定底部操作栏 */ - #tb-mobile-utils { - display: flex; - position: fixed; - left: 0; - right: 0; - bottom: 0; - z-index: 1000; - justify-content: center; - align-items: center; - gap: 10px; - padding: 8px 12px calc(8px + env(safe-area-inset-bottom)); - background: rgba(255, 255, 255, 0.92); - -webkit-backdrop-filter: blur(8px); - backdrop-filter: blur(8px); - box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08); - } - #tb-mobile-utils .uk-select { - width: 110px; - } - html[data-theme="dark"] #tb-mobile-utils { - background: rgba(36, 40, 48, 0.92); - box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.4); - } - /* 给页面底部预留空间,避免页脚内容被固定底栏遮挡 */ - body { - padding-bottom: 56px; - } - } diff --git a/outputs/preview-assets/uikit-icons.min.js b/outputs/preview-assets/uikit-icons.min.js deleted file mode 100644 index dd7d59e..0000000 --- a/outputs/preview-assets/uikit-icons.min.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! UIkit 3.3.3 | http://www.getuikit.com | (c) 2014 - 2019 YOOtheme | MIT License */ - -!function(t,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define("uikiticons",i):(t=t||self).UIkitIcons=i()}(this,function(){"use strict";function i(t){i.installed||t.icon.add({"500px":'',album:'',"arrow-down":'',"arrow-left":'',"arrow-right":'',"arrow-up":'',ban:'',behance:'',bell:'',bold:'',bolt:'',bookmark:'',calendar:'',camera:'',cart:'',check:'',"chevron-double-left":'',"chevron-double-right":'',"chevron-down":'',"chevron-left":'',"chevron-right":'',"chevron-up":'',clock:'',close:'',"cloud-download":'',"cloud-upload":'',code:'',cog:'',comment:'',commenting:'',comments:'',copy:'',"credit-card":'',database:'',desktop:'',download:'',dribbble:'',etsy:'',expand:'',facebook:'',"file-edit":'',"file-pdf":'',"file-text":'',file:'',flickr:'',folder:'',forward:'',foursquare:'',future:'',"git-branch":'',"git-fork":'',"github-alt":'',github:'',gitter:'',"google-plus":'',google:'',grid:'',happy:'',hashtag:'',heart:'',history:'',home:'',image:'',info:'',instagram:'',italic:'',joomla:'',laptop:'',lifesaver:'',link:'',linkedin:'',list:'',location:'',lock:'',mail:'',menu:'',microphone:'',"minus-circle":'',minus:'',"more-vertical":'',more:'',move:'',nut:'',pagekit:'',"paint-bucket":'',pencil:'',"phone-landscape":'',phone:'',pinterest:'',"play-circle":'',play:'',"plus-circle":'',plus:'',print:'',pull:'',push:'',question:'',"quote-right":'',receiver:'',reddit:'',refresh:'',reply:'',rss:'',search:'',server:'',settings:'',shrink:'',"sign-in":'',"sign-out":'',social:'',soundcloud:'',star:'',strikethrough:'',table:'',"tablet-landscape":'',tablet:'',tag:'',thumbnails:'',trash:'',"triangle-down":'',"triangle-left":'',"triangle-right":'',"triangle-up":'',tripadvisor:'',tumblr:'',tv:'',twitter:'',uikit:'',unlock:'',upload:'',user:'',users:'',"video-camera":'',vimeo:'',warning:'',whatsapp:'',wordpress:'',world:'',xing:'',yelp:'',youtube:''})}return"undefined"!=typeof window&&window.UIkit&&window.UIkit.use(i),i}); \ No newline at end of file diff --git a/outputs/preview-assets/uikit.min.css b/outputs/preview-assets/uikit.min.css deleted file mode 100644 index 0a64c67..0000000 --- a/outputs/preview-assets/uikit.min.css +++ /dev/null @@ -1 +0,0 @@ -/*! UIkit 3.3.3 | http://www.getuikit.com | (c) 2014 - 2019 YOOtheme | MIT License */html{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:16px;font-weight:400;line-height:1.5;-webkit-text-size-adjust:100%;background:#fff;color:#666}body{margin:0}a:active,a:hover{outline:0}.uk-link,a{color:#1e87f0;text-decoration:none;cursor:pointer}.uk-link-toggle:focus .uk-link,.uk-link-toggle:hover .uk-link,.uk-link:hover,a:hover{color:#0f6ecd;text-decoration:underline}abbr[title]{text-decoration:underline dotted;-webkit-text-decoration-style:dotted}b,strong{font-weight:bolder}:not(pre)>code,:not(pre)>kbd,:not(pre)>samp{font-family:Consolas,monaco,monospace;font-size:.875rem;color:#f0506e;white-space:nowrap;padding:2px 6px;background:#f8f8f8}em{color:#f0506e}ins{background:#ffd;color:#666;text-decoration:none}mark{background:#ffd;color:#666}q{font-style:italic}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}audio,canvas,iframe,img,svg,video{vertical-align:middle}canvas,img,video{max-width:100%;height:auto;box-sizing:border-box}@supports (display:block){svg{max-width:100%;height:auto;box-sizing:border-box}}svg:not(:root){overflow:hidden}img:not([src]){min-width:1px;visibility:hidden}iframe{border:0}address,dl,fieldset,figure,ol,p,pre,ul{margin:0 0 20px 0}*+address,*+dl,*+fieldset,*+figure,*+ol,*+p,*+pre,*+ul{margin-top:20px}.uk-h1,.uk-h2,.uk-h3,.uk-h4,.uk-h5,.uk-h6,.uk-heading-2xlarge,.uk-heading-large,.uk-heading-medium,.uk-heading-small,.uk-heading-xlarge,h1,h2,h3,h4,h5,h6{margin:0 0 20px 0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-weight:400;color:#333;text-transform:none}*+.uk-h1,*+.uk-h2,*+.uk-h3,*+.uk-h4,*+.uk-h5,*+.uk-h6,*+.uk-heading-2xlarge,*+.uk-heading-large,*+.uk-heading-medium,*+.uk-heading-small,*+.uk-heading-xlarge,*+h1,*+h2,*+h3,*+h4,*+h5,*+h6{margin-top:40px}.uk-h1,h1{font-size:2.23125rem;line-height:1.2}.uk-h2,h2{font-size:1.7rem;line-height:1.3}.uk-h3,h3{font-size:1.5rem;line-height:1.4}.uk-h4,h4{font-size:1.25rem;line-height:1.4}.uk-h5,h5{font-size:16px;line-height:1.4}.uk-h6,h6{font-size:.875rem;line-height:1.4}@media (min-width:960px){.uk-h1,h1{font-size:2.625rem}.uk-h2,h2{font-size:2rem}}ol,ul{padding-left:30px}ol>li>ol,ol>li>ul,ul>li>ol,ul>li>ul{margin:0}dt{font-weight:700}dd{margin-left:0}.uk-hr,hr{overflow:visible;text-align:inherit;margin:0 0 20px 0;border:0;border-top:1px solid #e5e5e5}*+.uk-hr,*+hr{margin-top:20px}address{font-style:normal}blockquote{margin:0 0 20px 0;font-size:1.25rem;line-height:1.5;font-style:italic;color:#333}*+blockquote{margin-top:20px}blockquote p:last-of-type{margin-bottom:0}blockquote footer{margin-top:10px;font-size:.875rem;line-height:1.5;color:#666}blockquote footer::before{content:"— "}pre{font:.875rem/1.5 Consolas,monaco,monospace;color:#666;-moz-tab-size:4;tab-size:4;overflow:auto;padding:10px;border:1px solid #e5e5e5;border-radius:3px;background:#fff}pre code{font-family:Consolas,monaco,monospace}::selection{background:#39f;color:#fff;text-shadow:none}details,main{display:block}summary{display:list-item}template{display:none}.uk-breakpoint-s::before{content:'640px'}.uk-breakpoint-m::before{content:'960px'}.uk-breakpoint-l::before{content:'1200px'}.uk-breakpoint-xl::before{content:'1600px'}:root{--uk-breakpoint-s:640px;--uk-breakpoint-m:960px;--uk-breakpoint-l:1200px;--uk-breakpoint-xl:1600px}.uk-link-muted a,a.uk-link-muted{color:#999}.uk-link-muted a:hover,.uk-link-toggle:focus .uk-link-muted,.uk-link-toggle:hover .uk-link-muted,a.uk-link-muted:hover{color:#666}.uk-link-text a,a.uk-link-text{color:inherit}.uk-link-text a:hover,.uk-link-toggle:focus .uk-link-text,.uk-link-toggle:hover .uk-link-text,a.uk-link-text:hover{color:#999}.uk-link-heading a,a.uk-link-heading{color:inherit}.uk-link-heading a:hover,.uk-link-toggle:focus .uk-link-heading,.uk-link-toggle:hover .uk-link-heading,a.uk-link-heading:hover{color:#1e87f0;text-decoration:none}.uk-link-reset a,a.uk-link-reset{color:inherit!important;text-decoration:none!important}.uk-link-toggle{color:inherit!important;text-decoration:none!important}.uk-link-toggle:focus{outline:0}.uk-heading-small{font-size:2.6rem;line-height:1.2}.uk-heading-medium{font-size:2.8875rem;line-height:1.1}.uk-heading-large{font-size:3.4rem;line-height:1.1}.uk-heading-xlarge{font-size:4rem;line-height:1}.uk-heading-2xlarge{font-size:6rem;line-height:1}@media (min-width:960px){.uk-heading-small{font-size:3.25rem}.uk-heading-medium{font-size:3.5rem}.uk-heading-large{font-size:4rem}.uk-heading-xlarge{font-size:6rem}.uk-heading-2xlarge{font-size:8rem}}@media (min-width:1200px){.uk-heading-medium{font-size:4rem}.uk-heading-large{font-size:6rem}.uk-heading-xlarge{font-size:8rem}.uk-heading-2xlarge{font-size:11rem}}.uk-heading-divider{padding-bottom:calc(5px + .1em);border-bottom:calc(.2px + .05em) solid #e5e5e5}.uk-heading-bullet{position:relative}.uk-heading-bullet::before{content:"";display:inline-block;position:relative;top:calc(-.1 * 1em);vertical-align:middle;height:calc(4px + .7em);margin-right:calc(5px + .2em);border-left:calc(5px + .1em) solid #e5e5e5}.uk-heading-line{overflow:hidden}.uk-heading-line>*{display:inline-block;position:relative}.uk-heading-line>::after,.uk-heading-line>::before{content:"";position:absolute;top:calc(50% - ((.2px + .05em)/ 2));width:2000px;border-bottom:calc(.2px + .05em) solid #e5e5e5}.uk-heading-line>::before{right:100%;margin-right:calc(5px + .3em)}.uk-heading-line>::after{left:100%;margin-left:calc(5px + .3em)}[class*=uk-divider]{border:none;margin-bottom:20px}*+[class*=uk-divider]{margin-top:20px}.uk-divider-icon{position:relative;height:20px;background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20viewBox%3D%220%200%2020%2020%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Ccircle%20fill%3D%22none%22%20stroke%3D%22%23e5e5e5%22%20stroke-width%3D%222%22%20cx%3D%2210%22%20cy%3D%2210%22%20r%3D%227%22%20%2F%3E%0A%3C%2Fsvg%3E%0A");background-repeat:no-repeat;background-position:50% 50%}.uk-divider-icon::after,.uk-divider-icon::before{content:"";position:absolute;top:50%;max-width:calc(50% - (50px / 2));border-bottom:1px solid #e5e5e5}.uk-divider-icon::before{right:calc(50% + (50px / 2));width:100%}.uk-divider-icon::after{left:calc(50% + (50px / 2));width:100%}.uk-divider-small{line-height:0}.uk-divider-small::after{content:"";display:inline-block;width:100px;max-width:100%;border-top:1px solid #e5e5e5;vertical-align:top}.uk-divider-vertical{width:1px;height:100px;margin-left:auto;margin-right:auto;border-left:1px solid #e5e5e5}.uk-list{padding:0;list-style:none}.uk-list>li::after,.uk-list>li::before{content:"";display:table}.uk-list>li::after{clear:both}.uk-list>li>:last-child{margin-bottom:0}.uk-list ul{margin:0;padding-left:30px;list-style:none}.uk-list>li:nth-child(n+2),.uk-list>li>ul{margin-top:10px}.uk-list-divider>li:nth-child(n+2){margin-top:10px;padding-top:10px;border-top:1px solid #e5e5e5}.uk-list-striped>li{padding:10px 10px}.uk-list-striped>li:nth-of-type(odd){border-top:1px solid #e5e5e5;border-bottom:1px solid #e5e5e5}.uk-list-striped>li:nth-of-type(odd){background:#f8f8f8}.uk-list-striped>li:nth-child(n+2){margin-top:0}.uk-list-bullet>li{position:relative;padding-left:calc(1.5em + 10px)}.uk-list-bullet>li::before{content:"";position:absolute;top:0;left:0;width:1.5em;height:1.5em;background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%226%22%20height%3D%226%22%20viewBox%3D%220%200%206%206%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Ccircle%20fill%3D%22%23666%22%20cx%3D%223%22%20cy%3D%223%22%20r%3D%223%22%20%2F%3E%0A%3C%2Fsvg%3E");background-repeat:no-repeat;background-position:50% 50%;display:block}.uk-list-large>li:nth-child(n+2),.uk-list-large>li>ul{margin-top:20px}.uk-list-large.uk-list-divider>li:nth-child(n+2){margin-top:20px;padding-top:20px}.uk-list-large.uk-list-striped>li{padding:20px 10px}.uk-list-large.uk-list-striped>li:nth-of-type(odd){border-top:1px solid #e5e5e5;border-bottom:1px solid #e5e5e5}.uk-list-large.uk-list-striped>li:nth-child(n+2){margin-top:0}.uk-description-list>dt{color:#333;font-size:.875rem;font-weight:400;text-transform:uppercase}.uk-description-list>dt:nth-child(n+2){margin-top:20px}.uk-description-list-divider>dt:nth-child(n+2){margin-top:20px;padding-top:20px;border-top:1px solid #e5e5e5}.uk-table{border-collapse:collapse;border-spacing:0;width:100%;margin-bottom:20px}*+.uk-table{margin-top:20px}.uk-table th{padding:16px 12px;text-align:left;vertical-align:bottom;font-size:.875rem;font-weight:400;color:#999;text-transform:uppercase}.uk-table td{padding:16px 12px;vertical-align:top}.uk-table td>:last-child{margin-bottom:0}.uk-table tfoot{font-size:.875rem}.uk-table caption{font-size:.875rem;text-align:left;color:#999}.uk-table-middle,.uk-table-middle td{vertical-align:middle!important}.uk-table-divider>:first-child>tr:not(:first-child),.uk-table-divider>:not(:first-child)>tr,.uk-table-divider>tr:not(:first-child){border-top:1px solid #e5e5e5}.uk-table-striped tbody tr:nth-of-type(odd),.uk-table-striped>tr:nth-of-type(odd){background:#f8f8f8;border-top:1px solid #e5e5e5;border-bottom:1px solid #e5e5e5}.uk-table-hover tbody tr:hover,.uk-table-hover>tr:hover{background:#ffd}.uk-table tbody tr.uk-active,.uk-table>tr.uk-active{background:#ffd}.uk-table-small td,.uk-table-small th{padding:10px 12px}.uk-table-large td,.uk-table-large th{padding:22px 12px}.uk-table-justify td:first-child,.uk-table-justify th:first-child{padding-left:0}.uk-table-justify td:last-child,.uk-table-justify th:last-child{padding-right:0}.uk-table-shrink{width:1px}.uk-table-expand{min-width:150px}.uk-table-link{padding:0!important}.uk-table-link>a{display:block;padding:16px 12px}.uk-table-small .uk-table-link>a{padding:10px 12px}@media (max-width:959px){.uk-table-responsive,.uk-table-responsive tbody,.uk-table-responsive td,.uk-table-responsive th,.uk-table-responsive tr{display:block}.uk-table-responsive thead{display:none}.uk-table-responsive td,.uk-table-responsive th{width:auto!important;max-width:none!important;min-width:0!important;overflow:visible!important;white-space:normal!important}.uk-table-responsive .uk-table-link:not(:first-child)>a,.uk-table-responsive td:not(:first-child):not(.uk-table-link),.uk-table-responsive th:not(:first-child):not(.uk-table-link){padding-top:5px!important}.uk-table-responsive .uk-table-link:not(:last-child)>a,.uk-table-responsive td:not(:last-child):not(.uk-table-link),.uk-table-responsive th:not(:last-child):not(.uk-table-link){padding-bottom:5px!important}.uk-table-justify.uk-table-responsive td,.uk-table-justify.uk-table-responsive th{padding-left:0;padding-right:0}}.uk-table tbody tr{transition:background-color .1s linear}.uk-icon{margin:0;border:none;border-radius:0;overflow:visible;font:inherit;color:inherit;text-transform:none;padding:0;background-color:transparent;display:inline-block;fill:currentcolor;line-height:0}button.uk-icon:not(:disabled){cursor:pointer}.uk-icon::-moz-focus-inner{border:0;padding:0}.uk-icon:not(.uk-preserve) [fill*='#']:not(.uk-preserve){fill:currentcolor}.uk-icon:not(.uk-preserve) [stroke*='#']:not(.uk-preserve){stroke:currentcolor}.uk-icon>*{transform:translate(0,0)}.uk-icon-image{width:20px;height:20px;background-position:50% 50%;background-repeat:no-repeat;background-size:contain;vertical-align:middle}.uk-icon-link{color:#999}.uk-icon-link:focus,.uk-icon-link:hover{color:#666;outline:0}.uk-active>.uk-icon-link,.uk-icon-link:active{color:#595959}.uk-icon-button{box-sizing:border-box;width:36px;height:36px;border-radius:500px;background:#f8f8f8;color:#999;vertical-align:middle;display:inline-flex;justify-content:center;align-items:center;transition:.1s ease-in-out;transition-property:color,background-color}.uk-icon-button:focus,.uk-icon-button:hover{background-color:#ebebeb;color:#666;outline:0}.uk-active>.uk-icon-button,.uk-icon-button:active{background-color:#dfdfdf;color:#666}.uk-range{box-sizing:border-box;margin:0;vertical-align:middle;max-width:100%;width:100%;-webkit-appearance:none;background:0 0;padding:0}.uk-range:focus{outline:0}.uk-range::-moz-focus-outer{border:none}.uk-range::-ms-track{height:15px;background:0 0;border-color:transparent;color:transparent}.uk-range:not(:disabled)::-webkit-slider-thumb{cursor:pointer}.uk-range:not(:disabled)::-moz-range-thumb{cursor:pointer}.uk-range:not(:disabled)::-ms-thumb{cursor:pointer}.uk-range::-webkit-slider-thumb{-webkit-appearance:none;margin-top:-7px;height:15px;width:15px;border-radius:500px;background:#fff;border:1px solid #ccc}.uk-range::-moz-range-thumb{border:none;height:15px;width:15px;border-radius:500px;background:#fff;border:1px solid #ccc}.uk-range::-ms-thumb{margin-top:0}.uk-range::-ms-thumb{border:none;height:15px;width:15px;border-radius:500px;background:#fff;border:1px solid #ccc}.uk-range::-ms-tooltip{display:none}.uk-range::-webkit-slider-runnable-track{height:3px;background:#ebebeb;border-radius:500px}.uk-range:active::-webkit-slider-runnable-track,.uk-range:focus::-webkit-slider-runnable-track{background:#d2d2d2}.uk-range::-moz-range-track{height:3px;background:#ebebeb;border-radius:500px}.uk-range:focus::-moz-range-track{background:#d2d2d2}.uk-range::-ms-fill-lower,.uk-range::-ms-fill-upper{height:3px;background:#ebebeb;border-radius:500px}.uk-range:focus::-ms-fill-lower,.uk-range:focus::-ms-fill-upper{background:#d2d2d2}.uk-checkbox,.uk-input,.uk-radio,.uk-select,.uk-textarea{box-sizing:border-box;margin:0;border-radius:0;font:inherit}.uk-input{overflow:visible}.uk-select{text-transform:none}.uk-select optgroup{font:inherit;font-weight:700}.uk-textarea{overflow:auto}.uk-input[type=search]::-webkit-search-cancel-button,.uk-input[type=search]::-webkit-search-decoration{-webkit-appearance:none}.uk-input[type=number]::-webkit-inner-spin-button,.uk-input[type=number]::-webkit-outer-spin-button{height:auto}.uk-input::-moz-placeholder,.uk-textarea::-moz-placeholder{opacity:1}.uk-checkbox:not(:disabled),.uk-radio:not(:disabled){cursor:pointer}.uk-fieldset{border:none;margin:0;padding:0}.uk-input,.uk-textarea{-webkit-appearance:none}.uk-input,.uk-select,.uk-textarea{max-width:100%;width:100%;border:0 none;padding:0 10px;background:#fff;color:#666;border:1px solid #e5e5e5;transition:.2s ease-in-out;transition-property:color,background-color,border}.uk-input,.uk-select:not([multiple]):not([size]){height:40px;vertical-align:middle;display:inline-block}.uk-input:not(input),.uk-select:not(select){line-height:38px}.uk-select[multiple],.uk-select[size],.uk-textarea{padding-top:4px;padding-bottom:4px;vertical-align:top}.uk-input:focus,.uk-select:focus,.uk-textarea:focus{outline:0;background-color:#fff;color:#666;border-color:#1e87f0}.uk-input:disabled,.uk-select:disabled,.uk-textarea:disabled{background-color:#f8f8f8;color:#999;border-color:#e5e5e5}.uk-input::-ms-input-placeholder{color:#999!important}.uk-input::placeholder{color:#999}.uk-textarea::-ms-input-placeholder{color:#999!important}.uk-textarea::placeholder{color:#999}.uk-form-small{font-size:.875rem}.uk-form-small:not(textarea):not([multiple]):not([size]){height:30px;padding-left:8px;padding-right:8px}.uk-form-small:not(select):not(input):not(textarea){line-height:28px}.uk-form-large{font-size:1.25rem}.uk-form-large:not(textarea):not([multiple]):not([size]){height:55px;padding-left:12px;padding-right:12px}.uk-form-large:not(select):not(input):not(textarea){line-height:53px}.uk-form-danger,.uk-form-danger:focus{color:#f0506e;border-color:#f0506e}.uk-form-success,.uk-form-success:focus{color:#32d296;border-color:#32d296}.uk-form-blank{background:0 0;border-color:transparent}.uk-form-blank:focus{border-color:#e5e5e5;border-style:dashed}input.uk-form-width-xsmall{width:50px}select.uk-form-width-xsmall{width:75px}.uk-form-width-small{width:130px}.uk-form-width-medium{width:200px}.uk-form-width-large{width:500px}.uk-select:not([multiple]):not([size]){-webkit-appearance:none;-moz-appearance:none;padding-right:20px;background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2224%22%20height%3D%2216%22%20viewBox%3D%220%200%2024%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22%23666%22%20points%3D%2212%201%209%206%2015%206%22%20%2F%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22%23666%22%20points%3D%2212%2013%209%208%2015%208%22%20%2F%3E%0A%3C%2Fsvg%3E%0A");background-repeat:no-repeat;background-position:100% 50%}.uk-select:not([multiple]):not([size])::-ms-expand{display:none}.uk-select:not([multiple]):not([size]) option{color:#444}.uk-select:not([multiple]):not([size]):disabled{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2224%22%20height%3D%2216%22%20viewBox%3D%220%200%2024%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22%23999%22%20points%3D%2212%201%209%206%2015%206%22%20%2F%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22%23999%22%20points%3D%2212%2013%209%208%2015%208%22%20%2F%3E%0A%3C%2Fsvg%3E%0A")}.uk-input[list]{padding-right:20px;background-repeat:no-repeat;background-position:100% 50%}.uk-input[list]:focus,.uk-input[list]:hover{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2224%22%20height%3D%2216%22%20viewBox%3D%220%200%2024%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22%23666%22%20points%3D%2212%2012%208%206%2016%206%22%20%2F%3E%0A%3C%2Fsvg%3E%0A")}.uk-input[list]::-webkit-calendar-picker-indicator{display:none}.uk-checkbox,.uk-radio{display:inline-block;height:16px;width:16px;overflow:hidden;margin-top:-4px;vertical-align:middle;-webkit-appearance:none;-moz-appearance:none;background-color:transparent;background-repeat:no-repeat;background-position:50% 50%;border:1px solid #ccc;transition:.2s ease-in-out;transition-property:background-color,border}.uk-radio{border-radius:50%}.uk-checkbox:focus,.uk-radio:focus{outline:0;border-color:#1e87f0}.uk-checkbox:checked,.uk-checkbox:indeterminate,.uk-radio:checked{background-color:#1e87f0;border-color:transparent}.uk-checkbox:checked:focus,.uk-checkbox:indeterminate:focus,.uk-radio:checked:focus{background-color:#0e6dcd}.uk-radio:checked{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Ccircle%20fill%3D%22%23fff%22%20cx%3D%228%22%20cy%3D%228%22%20r%3D%222%22%20%2F%3E%0A%3C%2Fsvg%3E")}.uk-checkbox:checked{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2211%22%20viewBox%3D%220%200%2014%2011%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22%23fff%22%20points%3D%2212%201%205%207.5%202%205%201%205.5%205%2010%2013%201.5%22%20%2F%3E%0A%3C%2Fsvg%3E%0A")}.uk-checkbox:indeterminate{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22%23fff%22%20x%3D%223%22%20y%3D%228%22%20width%3D%2210%22%20height%3D%221%22%20%2F%3E%0A%3C%2Fsvg%3E")}.uk-checkbox:disabled,.uk-radio:disabled{background-color:#f8f8f8;border-color:#e5e5e5}.uk-radio:disabled:checked{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Ccircle%20fill%3D%22%23999%22%20cx%3D%228%22%20cy%3D%228%22%20r%3D%222%22%20%2F%3E%0A%3C%2Fsvg%3E")}.uk-checkbox:disabled:checked{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2211%22%20viewBox%3D%220%200%2014%2011%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22%23999%22%20points%3D%2212%201%205%207.5%202%205%201%205.5%205%2010%2013%201.5%22%20%2F%3E%0A%3C%2Fsvg%3E%0A")}.uk-checkbox:disabled:indeterminate{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22%23999%22%20x%3D%223%22%20y%3D%228%22%20width%3D%2210%22%20height%3D%221%22%20%2F%3E%0A%3C%2Fsvg%3E")}.uk-legend{width:100%;color:inherit;padding:0;font-size:1.5rem;line-height:1.4}.uk-form-custom{display:inline-block;position:relative;max-width:100%;vertical-align:middle}.uk-form-custom input[type=file],.uk-form-custom select{position:absolute;top:0;z-index:1;width:100%;height:100%;left:0;-webkit-appearance:none;opacity:0;cursor:pointer}.uk-form-custom input[type=file]{font-size:500px;overflow:hidden}.uk-form-label{color:#333;font-size:.875rem}.uk-form-stacked .uk-form-label{display:block;margin-bottom:5px}@media (max-width:959px){.uk-form-horizontal .uk-form-label{display:block;margin-bottom:5px}}@media (min-width:960px){.uk-form-horizontal .uk-form-label{width:200px;margin-top:7px;float:left}.uk-form-horizontal .uk-form-controls{margin-left:215px}.uk-form-horizontal .uk-form-controls-text{padding-top:7px}}.uk-form-icon{position:absolute;top:0;bottom:0;left:0;width:40px;display:inline-flex;justify-content:center;align-items:center;color:#999}.uk-form-icon:hover{color:#666}.uk-form-icon:not(a):not(button):not(input){pointer-events:none}.uk-form-icon:not(.uk-form-icon-flip)~.uk-input{padding-left:40px!important}.uk-form-icon-flip{right:0;left:auto}.uk-form-icon-flip~.uk-input{padding-right:40px!important}.uk-button{margin:0;border:none;overflow:visible;font:inherit;color:inherit;text-transform:none;-webkit-appearance:none;border-radius:0;display:inline-block;box-sizing:border-box;padding:0 30px;vertical-align:middle;font-size:.875rem;line-height:38px;text-align:center;text-decoration:none;text-transform:uppercase;transition:.1s ease-in-out;transition-property:color,background-color,border-color}.uk-button:not(:disabled){cursor:pointer}.uk-button::-moz-focus-inner{border:0;padding:0}.uk-button:hover{text-decoration:none}.uk-button:focus{outline:0}.uk-button-default{background-color:transparent;color:#333;border:1px solid #e5e5e5}.uk-button-default:focus,.uk-button-default:hover{background-color:transparent;color:#333;border-color:#b2b2b2}.uk-button-default.uk-active,.uk-button-default:active{background-color:transparent;color:#333;border-color:#999}.uk-button-primary{background-color:#1e87f0;color:#fff;border:1px solid transparent}.uk-button-primary:focus,.uk-button-primary:hover{background-color:#0f7ae5;color:#fff}.uk-button-primary.uk-active,.uk-button-primary:active{background-color:#0e6dcd;color:#fff}.uk-button-secondary{background-color:#222;color:#fff;border:1px solid transparent}.uk-button-secondary:focus,.uk-button-secondary:hover{background-color:#151515;color:#fff}.uk-button-secondary.uk-active,.uk-button-secondary:active{background-color:#080808;color:#fff}.uk-button-danger{background-color:#f0506e;color:#fff;border:1px solid transparent}.uk-button-danger:focus,.uk-button-danger:hover{background-color:#ee395b;color:#fff}.uk-button-danger.uk-active,.uk-button-danger:active{background-color:#ec2147;color:#fff}.uk-button-danger:disabled,.uk-button-default:disabled,.uk-button-primary:disabled,.uk-button-secondary:disabled{background-color:transparent;color:#999;border-color:#e5e5e5}.uk-button-small{padding:0 15px;line-height:28px;font-size:.875rem}.uk-button-large{padding:0 40px;line-height:53px;font-size:.875rem}.uk-button-text{padding:0;line-height:1.5;background:0 0;color:#333;position:relative}.uk-button-text::before{content:"";position:absolute;bottom:0;left:0;right:100%;border-bottom:1px solid #333;transition:right .3s ease-out}.uk-button-text:focus,.uk-button-text:hover{color:#333}.uk-button-text:focus::before,.uk-button-text:hover::before{right:0}.uk-button-text:disabled{color:#999}.uk-button-text:disabled::before{display:none}.uk-button-link{padding:0;line-height:1.5;background:0 0;color:#1e87f0}.uk-button-link:focus,.uk-button-link:hover{color:#0f6ecd;text-decoration:underline}.uk-button-link:disabled{color:#999;text-decoration:none}.uk-button-group{display:inline-flex;vertical-align:middle;position:relative}.uk-button-group>.uk-button:nth-child(n+2),.uk-button-group>div:nth-child(n+2) .uk-button{margin-left:-1px}.uk-button-group .uk-button.uk-active,.uk-button-group .uk-button:active,.uk-button-group .uk-button:focus,.uk-button-group .uk-button:hover{position:relative;z-index:1}.uk-section{box-sizing:border-box;padding-top:40px;padding-bottom:40px}@media (min-width:960px){.uk-section{padding-top:70px;padding-bottom:70px}}.uk-section::after,.uk-section::before{content:"";display:table}.uk-section::after{clear:both}.uk-section>:last-child{margin-bottom:0}.uk-section-xsmall{padding-top:20px;padding-bottom:20px}.uk-section-small{padding-top:40px;padding-bottom:40px}.uk-section-large{padding-top:70px;padding-bottom:70px}@media (min-width:960px){.uk-section-large{padding-top:140px;padding-bottom:140px}}.uk-section-xlarge{padding-top:140px;padding-bottom:140px}@media (min-width:960px){.uk-section-xlarge{padding-top:210px;padding-bottom:210px}}.uk-section-default{background:#fff}.uk-section-muted{background:#f8f8f8}.uk-section-primary{background:#1e87f0}.uk-section-secondary{background:#222}.uk-container{box-sizing:content-box;max-width:1200px;margin-left:auto;margin-right:auto;padding-left:15px;padding-right:15px}@media (min-width:640px){.uk-container{padding-left:30px;padding-right:30px}}@media (min-width:960px){.uk-container{padding-left:40px;padding-right:40px}}.uk-container::after,.uk-container::before{content:"";display:table}.uk-container::after{clear:both}.uk-container>:last-child{margin-bottom:0}.uk-container .uk-container{padding-left:0;padding-right:0}.uk-container-xsmall{max-width:750px}.uk-container-small{max-width:900px}.uk-container-large{max-width:1400px}.uk-container-xlarge{max-width:1600px}.uk-container-expand{max-width:none}.uk-container-expand-left{margin-left:0}.uk-container-expand-right{margin-right:0}@media (min-width:640px){.uk-container-expand-left.uk-container-xsmall,.uk-container-expand-right.uk-container-xsmall{max-width:calc(50% + (750px / 2) - 30px)}.uk-container-expand-left.uk-container-small,.uk-container-expand-right.uk-container-small{max-width:calc(50% + (900px / 2) - 30px)}}@media (min-width:960px){.uk-container-expand-left,.uk-container-expand-right{max-width:calc(50% + (1200px / 2) - 40px)}.uk-container-expand-left.uk-container-xsmall,.uk-container-expand-right.uk-container-xsmall{max-width:calc(50% + (750px / 2) - 40px)}.uk-container-expand-left.uk-container-small,.uk-container-expand-right.uk-container-small{max-width:calc(50% + (900px / 2) - 40px)}.uk-container-expand-left.uk-container-large,.uk-container-expand-right.uk-container-large{max-width:calc(50% + (1400px / 2) - 40px)}.uk-container-expand-left.uk-container-xlarge,.uk-container-expand-right.uk-container-xlarge{max-width:calc(50% + (1600px / 2) - 40px)}}.uk-container-item-padding-remove-left,.uk-container-item-padding-remove-right{width:calc(100% + 15px)}.uk-container-item-padding-remove-left{margin-left:-15px}.uk-container-item-padding-remove-right{margin-right:-15px}@media (min-width:640px){.uk-container-item-padding-remove-left,.uk-container-item-padding-remove-right{width:calc(100% + 30px)}.uk-container-item-padding-remove-left{margin-left:-30px}.uk-container-item-padding-remove-right{margin-right:-30px}}@media (min-width:960px){.uk-container-item-padding-remove-left,.uk-container-item-padding-remove-right{width:calc(100% + 40px)}.uk-container-item-padding-remove-left{margin-left:-40px}.uk-container-item-padding-remove-right{margin-right:-40px}}.uk-grid{display:flex;flex-wrap:wrap;margin:0;padding:0;list-style:none}.uk-grid>*{margin:0}.uk-grid>*>:last-child{margin-bottom:0}.uk-grid{margin-left:-30px}.uk-grid>*{padding-left:30px}*+.uk-grid-margin,.uk-grid+.uk-grid,.uk-grid>.uk-grid-margin{margin-top:30px}@media (min-width:1200px){.uk-grid{margin-left:-40px}.uk-grid>*{padding-left:40px}*+.uk-grid-margin,.uk-grid+.uk-grid,.uk-grid>.uk-grid-margin{margin-top:40px}}.uk-grid-column-small,.uk-grid-small{margin-left:-15px}.uk-grid-column-small>*,.uk-grid-small>*{padding-left:15px}*+.uk-grid-margin-small,.uk-grid+.uk-grid-row-small,.uk-grid+.uk-grid-small,.uk-grid-row-small>.uk-grid-margin,.uk-grid-small>.uk-grid-margin{margin-top:15px}.uk-grid-column-medium,.uk-grid-medium{margin-left:-30px}.uk-grid-column-medium>*,.uk-grid-medium>*{padding-left:30px}*+.uk-grid-margin-medium,.uk-grid+.uk-grid-medium,.uk-grid+.uk-grid-row-medium,.uk-grid-medium>.uk-grid-margin,.uk-grid-row-medium>.uk-grid-margin{margin-top:30px}.uk-grid-column-large,.uk-grid-large{margin-left:-40px}.uk-grid-column-large>*,.uk-grid-large>*{padding-left:40px}*+.uk-grid-margin-large,.uk-grid+.uk-grid-large,.uk-grid+.uk-grid-row-large,.uk-grid-large>.uk-grid-margin,.uk-grid-row-large>.uk-grid-margin{margin-top:40px}@media (min-width:1200px){.uk-grid-column-large,.uk-grid-large{margin-left:-70px}.uk-grid-column-large>*,.uk-grid-large>*{padding-left:70px}*+.uk-grid-margin-large,.uk-grid+.uk-grid-large,.uk-grid+.uk-grid-row-large,.uk-grid-large>.uk-grid-margin,.uk-grid-row-large>.uk-grid-margin{margin-top:70px}}.uk-grid-collapse,.uk-grid-column-collapse{margin-left:0}.uk-grid-collapse>*,.uk-grid-column-collapse>*{padding-left:0}.uk-grid+.uk-grid-collapse,.uk-grid+.uk-grid-row-collapse,.uk-grid-collapse>.uk-grid-margin,.uk-grid-row-collapse>.uk-grid-margin{margin-top:0}.uk-grid-divider>*{position:relative}.uk-grid-divider>:not(.uk-first-column)::before{content:"";position:absolute;top:0;bottom:0;border-left:1px solid #e5e5e5}.uk-grid-divider.uk-grid-stack>.uk-grid-margin::before{content:"";position:absolute;left:0;right:0;border-top:1px solid #e5e5e5}.uk-grid-divider{margin-left:-60px}.uk-grid-divider>*{padding-left:60px}.uk-grid-divider>:not(.uk-first-column)::before{left:30px}.uk-grid-divider.uk-grid-stack>.uk-grid-margin{margin-top:60px}.uk-grid-divider.uk-grid-stack>.uk-grid-margin::before{top:-30px;left:60px}@media (min-width:1200px){.uk-grid-divider{margin-left:-80px}.uk-grid-divider>*{padding-left:80px}.uk-grid-divider>:not(.uk-first-column)::before{left:40px}.uk-grid-divider.uk-grid-stack>.uk-grid-margin{margin-top:80px}.uk-grid-divider.uk-grid-stack>.uk-grid-margin::before{top:-40px;left:80px}}.uk-grid-divider.uk-grid-column-small,.uk-grid-divider.uk-grid-small{margin-left:-30px}.uk-grid-divider.uk-grid-column-small>*,.uk-grid-divider.uk-grid-small>*{padding-left:30px}.uk-grid-divider.uk-grid-column-small>:not(.uk-first-column)::before,.uk-grid-divider.uk-grid-small>:not(.uk-first-column)::before{left:15px}.uk-grid-divider.uk-grid-row-small.uk-grid-stack>.uk-grid-margin,.uk-grid-divider.uk-grid-small.uk-grid-stack>.uk-grid-margin{margin-top:30px}.uk-grid-divider.uk-grid-small.uk-grid-stack>.uk-grid-margin::before{top:-15px;left:30px}.uk-grid-divider.uk-grid-row-small.uk-grid-stack>.uk-grid-margin::before{top:-15px}.uk-grid-divider.uk-grid-column-small.uk-grid-stack>.uk-grid-margin::before{left:30px}.uk-grid-divider.uk-grid-column-medium,.uk-grid-divider.uk-grid-medium{margin-left:-60px}.uk-grid-divider.uk-grid-column-medium>*,.uk-grid-divider.uk-grid-medium>*{padding-left:60px}.uk-grid-divider.uk-grid-column-medium>:not(.uk-first-column)::before,.uk-grid-divider.uk-grid-medium>:not(.uk-first-column)::before{left:30px}.uk-grid-divider.uk-grid-medium.uk-grid-stack>.uk-grid-margin,.uk-grid-divider.uk-grid-row-medium.uk-grid-stack>.uk-grid-margin{margin-top:60px}.uk-grid-divider.uk-grid-medium.uk-grid-stack>.uk-grid-margin::before{top:-30px;left:60px}.uk-grid-divider.uk-grid-row-medium.uk-grid-stack>.uk-grid-margin::before{top:-30px}.uk-grid-divider.uk-grid-column-medium.uk-grid-stack>.uk-grid-margin::before{left:60px}.uk-grid-divider.uk-grid-column-large,.uk-grid-divider.uk-grid-large{margin-left:-80px}.uk-grid-divider.uk-grid-column-large>*,.uk-grid-divider.uk-grid-large>*{padding-left:80px}.uk-grid-divider.uk-grid-column-large>:not(.uk-first-column)::before,.uk-grid-divider.uk-grid-large>:not(.uk-first-column)::before{left:40px}.uk-grid-divider.uk-grid-large.uk-grid-stack>.uk-grid-margin,.uk-grid-divider.uk-grid-row-large.uk-grid-stack>.uk-grid-margin{margin-top:80px}.uk-grid-divider.uk-grid-large.uk-grid-stack>.uk-grid-margin::before{top:-40px;left:80px}.uk-grid-divider.uk-grid-row-large.uk-grid-stack>.uk-grid-margin::before{top:-40px}.uk-grid-divider.uk-grid-column-large.uk-grid-stack>.uk-grid-margin::before{left:80px}@media (min-width:1200px){.uk-grid-divider.uk-grid-column-large,.uk-grid-divider.uk-grid-large{margin-left:-140px}.uk-grid-divider.uk-grid-column-large>*,.uk-grid-divider.uk-grid-large>*{padding-left:140px}.uk-grid-divider.uk-grid-column-large>:not(.uk-first-column)::before,.uk-grid-divider.uk-grid-large>:not(.uk-first-column)::before{left:70px}.uk-grid-divider.uk-grid-large.uk-grid-stack>.uk-grid-margin,.uk-grid-divider.uk-grid-row-large.uk-grid-stack>.uk-grid-margin{margin-top:140px}.uk-grid-divider.uk-grid-large.uk-grid-stack>.uk-grid-margin::before{top:-70px;left:140px}.uk-grid-divider.uk-grid-row-large.uk-grid-stack>.uk-grid-margin::before{top:-70px}.uk-grid-divider.uk-grid-column-large.uk-grid-stack>.uk-grid-margin::before{left:140px}}.uk-grid-item-match,.uk-grid-match>*{display:flex;flex-wrap:wrap}.uk-grid-item-match>:not([class*=uk-width]),.uk-grid-match>*>:not([class*=uk-width]){box-sizing:border-box;width:100%;flex:auto}.uk-tile{position:relative;box-sizing:border-box;padding-left:15px;padding-right:15px;padding-top:40px;padding-bottom:40px}@media (min-width:640px){.uk-tile{padding-left:30px;padding-right:30px}}@media (min-width:960px){.uk-tile{padding-left:40px;padding-right:40px;padding-top:70px;padding-bottom:70px}}.uk-tile::after,.uk-tile::before{content:"";display:table}.uk-tile::after{clear:both}.uk-tile>:last-child{margin-bottom:0}.uk-tile-xsmall{padding-top:20px;padding-bottom:20px}.uk-tile-small{padding-top:40px;padding-bottom:40px}.uk-tile-large{padding-top:70px;padding-bottom:70px}@media (min-width:960px){.uk-tile-large{padding-top:140px;padding-bottom:140px}}.uk-tile-xlarge{padding-top:140px;padding-bottom:140px}@media (min-width:960px){.uk-tile-xlarge{padding-top:210px;padding-bottom:210px}}.uk-tile-default{background:#fff}.uk-tile-muted{background:#f8f8f8}.uk-tile-primary{background:#1e87f0}.uk-tile-secondary{background:#222}.uk-card{position:relative;box-sizing:border-box;transition:box-shadow .1s ease-in-out}.uk-card-body{padding:30px 30px}.uk-card-header{padding:15px 30px}.uk-card-footer{padding:15px 30px}@media (min-width:1200px){.uk-card-body{padding:40px 40px}.uk-card-header{padding:20px 40px}.uk-card-footer{padding:20px 40px}}.uk-card-body::after,.uk-card-body::before,.uk-card-footer::after,.uk-card-footer::before,.uk-card-header::after,.uk-card-header::before{content:"";display:table}.uk-card-body::after,.uk-card-footer::after,.uk-card-header::after{clear:both}.uk-card-body>:last-child,.uk-card-footer>:last-child,.uk-card-header>:last-child{margin-bottom:0}.uk-card-title{font-size:1.5rem;line-height:1.4}.uk-card-badge{position:absolute;top:30px;right:30px;z-index:1}.uk-card-badge:first-child+*{margin-top:0}.uk-card-hover:not(.uk-card-default):not(.uk-card-primary):not(.uk-card-secondary):hover{background:#fff;box-shadow:0 14px 25px rgba(0,0,0,.16)}.uk-card-default{background:#fff;color:#666;box-shadow:0 5px 15px rgba(0,0,0,.08)}.uk-card-default .uk-card-title{color:#333}.uk-card-default.uk-card-hover:hover{background-color:#fff;box-shadow:0 14px 25px rgba(0,0,0,.16)}.uk-card-default .uk-card-header{border-bottom:1px solid #e5e5e5}.uk-card-default .uk-card-footer{border-top:1px solid #e5e5e5}.uk-card-primary{background:#1e87f0;color:#fff;box-shadow:0 5px 15px rgba(0,0,0,.08)}.uk-card-primary .uk-card-title{color:#fff}.uk-card-primary.uk-card-hover:hover{background-color:#1e87f0;box-shadow:0 14px 25px rgba(0,0,0,.16)}.uk-card-secondary{background:#222;color:#fff;box-shadow:0 5px 15px rgba(0,0,0,.08)}.uk-card-secondary .uk-card-title{color:#fff}.uk-card-secondary.uk-card-hover:hover{background-color:#222;box-shadow:0 14px 25px rgba(0,0,0,.16)}.uk-card-small .uk-card-body,.uk-card-small.uk-card-body{padding:20px 20px}.uk-card-small .uk-card-header{padding:13px 20px}.uk-card-small .uk-card-footer{padding:13px 20px}@media (min-width:1200px){.uk-card-large .uk-card-body,.uk-card-large.uk-card-body{padding:70px 70px}.uk-card-large .uk-card-header{padding:35px 70px}.uk-card-large .uk-card-footer{padding:35px 70px}}.uk-card-body>.uk-nav-default{margin-left:-30px;margin-right:-30px}.uk-card-body>.uk-nav-default:only-child{margin-top:-15px;margin-bottom:-15px}.uk-card-body .uk-nav-default .uk-nav-divider,.uk-card-body .uk-nav-default .uk-nav-header,.uk-card-body .uk-nav-default>li>a{padding-left:30px;padding-right:30px}.uk-card-body .uk-nav-default .uk-nav-sub{padding-left:45px}@media (min-width:1200px){.uk-card-body>.uk-nav-default{margin-left:-40px;margin-right:-40px}.uk-card-body>.uk-nav-default:only-child{margin-top:-25px;margin-bottom:-25px}.uk-card-body .uk-nav-default .uk-nav-divider,.uk-card-body .uk-nav-default .uk-nav-header,.uk-card-body .uk-nav-default>li>a{padding-left:40px;padding-right:40px}.uk-card-body .uk-nav-default .uk-nav-sub{padding-left:55px}}.uk-card-small>.uk-nav-default{margin-left:-20px;margin-right:-20px}.uk-card-small>.uk-nav-default:only-child{margin-top:-5px;margin-bottom:-5px}.uk-card-small .uk-nav-default .uk-nav-divider,.uk-card-small .uk-nav-default .uk-nav-header,.uk-card-small .uk-nav-default>li>a{padding-left:20px;padding-right:20px}.uk-card-small .uk-nav-default .uk-nav-sub{padding-left:35px}@media (min-width:1200px){.uk-card-large>.uk-nav-default{margin:0}.uk-card-large>.uk-nav-default:only-child{margin:0}.uk-card-large .uk-nav-default .uk-nav-divider,.uk-card-large .uk-nav-default .uk-nav-header,.uk-card-large .uk-nav-default>li>a{padding-left:0;padding-right:0}.uk-card-large .uk-nav-default .uk-nav-sub{padding-left:15px}}.uk-close{color:#999;transition:.1s ease-in-out;transition-property:color,opacity}.uk-close:focus,.uk-close:hover{color:#666;outline:0}.uk-spinner>*{animation:uk-spinner-rotate 1.4s linear infinite}@keyframes uk-spinner-rotate{0%{transform:rotate(0)}100%{transform:rotate(270deg)}}.uk-spinner>*>*{stroke-dasharray:88px;stroke-dashoffset:0;transform-origin:center;animation:uk-spinner-dash 1.4s ease-in-out infinite;stroke-width:1;stroke-linecap:round}@keyframes uk-spinner-dash{0%{stroke-dashoffset:88px}50%{stroke-dashoffset:22px;transform:rotate(135deg)}100%{stroke-dashoffset:88px;transform:rotate(450deg)}}.uk-totop{padding:5px;color:#999;transition:color .1s ease-in-out}.uk-totop:focus,.uk-totop:hover{color:#666;outline:0}.uk-totop:active{color:#333}.uk-marker{padding:5px;background:#222;color:#fff;border-radius:500px}.uk-marker:focus,.uk-marker:hover{color:#fff;outline:0}.uk-alert{position:relative;margin-bottom:20px;padding:15px 29px 15px 15px;background:#f8f8f8;color:#666}*+.uk-alert{margin-top:20px}.uk-alert>:last-child{margin-bottom:0}.uk-alert-close{position:absolute;top:20px;right:15px;color:inherit;opacity:.4}.uk-alert-close:first-child+*{margin-top:0}.uk-alert-close:focus,.uk-alert-close:hover{color:inherit;opacity:.8}.uk-alert-primary{background:#d8eafc;color:#1e87f0}.uk-alert-success{background:#edfbf6;color:#32d296}.uk-alert-warning{background:#fff6ee;color:#faa05a}.uk-alert-danger{background:#fef4f6;color:#f0506e}.uk-alert h1,.uk-alert h2,.uk-alert h3,.uk-alert h4,.uk-alert h5,.uk-alert h6{color:inherit}.uk-alert a:not([class]){color:inherit;text-decoration:underline}.uk-alert a:not([class]):hover{color:inherit;text-decoration:underline}.uk-badge{box-sizing:border-box;min-width:22px;height:22px;padding:0 5px;border-radius:500px;vertical-align:middle;background:#1e87f0;color:#fff;font-size:.875rem;display:inline-flex;justify-content:center;align-items:center}.uk-badge:focus,.uk-badge:hover{color:#fff;text-decoration:none;outline:0}.uk-label{display:inline-block;padding:0 10px;background:#1e87f0;line-height:1.5;font-size:.875rem;color:#fff;vertical-align:middle;white-space:nowrap;border-radius:2px;text-transform:uppercase}.uk-label-success{background-color:#32d296;color:#fff}.uk-label-warning{background-color:#faa05a;color:#fff}.uk-label-danger{background-color:#f0506e;color:#fff}.uk-overlay{padding:30px 30px}.uk-overlay>:last-child{margin-bottom:0}.uk-overlay-default{background:rgba(255,255,255,.8)}.uk-overlay-primary{background:rgba(34,34,34,.8)}.uk-article::after,.uk-article::before{content:"";display:table}.uk-article::after{clear:both}.uk-article>:last-child{margin-bottom:0}.uk-article+.uk-article{margin-top:70px}.uk-article-title{font-size:2.23125rem;line-height:1.2}@media (min-width:960px){.uk-article-title{font-size:2.625rem}}.uk-article-meta{font-size:.875rem;line-height:1.4;color:#999}.uk-article-meta a{color:#999}.uk-article-meta a:hover{color:#666;text-decoration:none}.uk-comment-body{overflow-wrap:break-word;word-wrap:break-word}.uk-comment-header{margin-bottom:20px}.uk-comment-body::after,.uk-comment-body::before,.uk-comment-header::after,.uk-comment-header::before{content:"";display:table}.uk-comment-body::after,.uk-comment-header::after{clear:both}.uk-comment-body>:last-child,.uk-comment-header>:last-child{margin-bottom:0}.uk-comment-title{font-size:1.25rem;line-height:1.4}.uk-comment-meta{font-size:.875rem;line-height:1.4;color:#999}.uk-comment-list{padding:0;list-style:none}.uk-comment-list>:nth-child(n+2){margin-top:70px}.uk-comment-list .uk-comment~ul{margin:70px 0 0 0;padding-left:30px;list-style:none}@media (min-width:960px){.uk-comment-list .uk-comment~ul{padding-left:100px}}.uk-comment-list .uk-comment~ul>:nth-child(n+2){margin-top:70px}.uk-comment-primary{padding:30px;background-color:#f8f8f8}.uk-search{display:inline-block;position:relative;max-width:100%;margin:0}.uk-search-input::-webkit-search-cancel-button,.uk-search-input::-webkit-search-decoration{-webkit-appearance:none}.uk-search-input::-moz-placeholder{opacity:1}.uk-search-input{box-sizing:border-box;margin:0;border-radius:0;font:inherit;overflow:visible;-webkit-appearance:none;vertical-align:middle;width:100%;border:none;color:#666}.uk-search-input:focus{outline:0}.uk-search-input:-ms-input-placeholder{color:#999!important}.uk-search-input::placeholder{color:#999}.uk-search-icon:focus{outline:0}.uk-search .uk-search-icon{position:absolute;top:0;bottom:0;left:0;display:inline-flex;justify-content:center;align-items:center;color:#999}.uk-search .uk-search-icon:hover{color:#999}.uk-search .uk-search-icon:not(a):not(button):not(input){pointer-events:none}.uk-search .uk-search-icon-flip{right:0;left:auto}.uk-search-default{width:180px}.uk-search-default .uk-search-input{height:40px;padding-left:6px;padding-right:6px;background:0 0;border:1px solid #e5e5e5}.uk-search-default .uk-search-input:focus{background-color:transparent}.uk-search-default .uk-search-icon{width:40px}.uk-search-default .uk-search-icon:not(.uk-search-icon-flip)~.uk-search-input{padding-left:40px}.uk-search-default .uk-search-icon-flip~.uk-search-input{padding-right:40px}.uk-search-navbar{width:400px}.uk-search-navbar .uk-search-input{height:40px;background:0 0;font-size:1.5rem}.uk-search-navbar .uk-search-icon{width:40px}.uk-search-navbar .uk-search-icon:not(.uk-search-icon-flip)~.uk-search-input{padding-left:40px}.uk-search-navbar .uk-search-icon-flip~.uk-search-input{padding-right:40px}.uk-search-large{width:500px}.uk-search-large .uk-search-input{height:80px;background:0 0;font-size:2.625rem}.uk-search-large .uk-search-icon{width:80px}.uk-search-large .uk-search-icon:not(.uk-search-icon-flip)~.uk-search-input{padding-left:80px}.uk-search-large .uk-search-icon-flip~.uk-search-input{padding-right:80px}.uk-search-toggle{color:#999}.uk-search-toggle:focus,.uk-search-toggle:hover{color:#666}.uk-nav,.uk-nav ul{margin:0;padding:0;list-style:none}.uk-nav li>a{display:block;text-decoration:none}.uk-nav li>a:focus{outline:0}.uk-nav>li>a{padding:5px 0}ul.uk-nav-sub{padding:5px 0 5px 15px}.uk-nav-sub ul{padding-left:15px}.uk-nav-sub a{padding:2px 0}.uk-nav-parent-icon>.uk-parent>a::after{content:"";width:1.5em;height:1.5em;float:right;background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolyline%20fill%3D%22none%22%20stroke%3D%22%23666%22%20stroke-width%3D%221.1%22%20points%3D%2210%201%204%207%2010%2013%22%20%2F%3E%0A%3C%2Fsvg%3E");background-repeat:no-repeat;background-position:50% 50%}.uk-nav-parent-icon>.uk-parent.uk-open>a::after{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolyline%20fill%3D%22none%22%20stroke%3D%22%23666%22%20stroke-width%3D%221.1%22%20points%3D%221%204%207%2010%2013%204%22%20%2F%3E%0A%3C%2Fsvg%3E")}.uk-nav-header{padding:5px 0;text-transform:uppercase;font-size:.875rem}.uk-nav-header:not(:first-child){margin-top:20px}.uk-nav-divider{margin:5px 0}.uk-nav-default{font-size:.875rem}.uk-nav-default>li>a{color:#999}.uk-nav-default>li>a:focus,.uk-nav-default>li>a:hover{color:#666}.uk-nav-default>li.uk-active>a{color:#333}.uk-nav-default .uk-nav-header{color:#333}.uk-nav-default .uk-nav-divider{border-top:1px solid #e5e5e5}.uk-nav-default .uk-nav-sub a{color:#999}.uk-nav-default .uk-nav-sub a:focus,.uk-nav-default .uk-nav-sub a:hover{color:#666}.uk-nav-default .uk-nav-sub li.uk-active>a{color:#333}.uk-nav-primary>li>a{font-size:1.5rem;line-height:1.5;color:#999}.uk-nav-primary>li>a:focus,.uk-nav-primary>li>a:hover{color:#666}.uk-nav-primary>li.uk-active>a{color:#333}.uk-nav-primary .uk-nav-header{color:#333}.uk-nav-primary .uk-nav-divider{border-top:1px solid #e5e5e5}.uk-nav-primary .uk-nav-sub a{color:#999}.uk-nav-primary .uk-nav-sub a:focus,.uk-nav-primary .uk-nav-sub a:hover{color:#666}.uk-nav-primary .uk-nav-sub li.uk-active>a{color:#333}.uk-nav-center{text-align:center}.uk-nav-center .uk-nav-sub,.uk-nav-center .uk-nav-sub ul{padding-left:0}.uk-nav-center.uk-nav-parent-icon>.uk-parent>a::after{position:absolute}.uk-navbar{display:flex;position:relative}.uk-navbar-container:not(.uk-navbar-transparent){background:#f8f8f8}.uk-navbar-container>::after,.uk-navbar-container>::before{display:none!important}.uk-navbar-center,.uk-navbar-center-left>*,.uk-navbar-center-right>*,.uk-navbar-left,.uk-navbar-right{display:flex;align-items:center}.uk-navbar-right{margin-left:auto}.uk-navbar-center:only-child{margin-left:auto;margin-right:auto;position:relative}.uk-navbar-center:not(:only-child){position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:max-content;box-sizing:border-box;z-index:990}.uk-navbar-center-left,.uk-navbar-center-right{position:absolute;top:0}.uk-navbar-center-left{right:100%}.uk-navbar-center-right{left:100%}[class*=uk-navbar-center-]{width:max-content;box-sizing:border-box}.uk-navbar-nav{display:flex;margin:0;padding:0;list-style:none}.uk-navbar-center:only-child,.uk-navbar-left,.uk-navbar-right{flex-wrap:wrap}.uk-navbar-item,.uk-navbar-nav>li>a,.uk-navbar-toggle{display:flex;justify-content:center;align-items:center;box-sizing:border-box;min-height:80px;padding:0 15px;font-size:.875rem;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";text-decoration:none}.uk-navbar-nav>li>a{color:#999;text-transform:uppercase;transition:.1s ease-in-out;transition-property:color,background-color}.uk-navbar-nav>li:hover>a,.uk-navbar-nav>li>a.uk-open,.uk-navbar-nav>li>a:focus{color:#666;outline:0}.uk-navbar-nav>li>a:active{color:#333}.uk-navbar-nav>li.uk-active>a{color:#333}.uk-navbar-item{color:#666}.uk-navbar-toggle{color:#999}.uk-navbar-toggle.uk-open,.uk-navbar-toggle:focus,.uk-navbar-toggle:hover{color:#666;outline:0;text-decoration:none}.uk-navbar-subtitle{font-size:.875rem}.uk-navbar-dropdown{display:none;position:absolute;z-index:1020;box-sizing:border-box;width:200px;padding:25px;background:#fff;color:#666;box-shadow:0 5px 12px rgba(0,0,0,.15)}.uk-navbar-dropdown.uk-open{display:block}[class*=uk-navbar-dropdown-top]{margin-top:-15px}[class*=uk-navbar-dropdown-bottom]{margin-top:15px}[class*=uk-navbar-dropdown-left]{margin-left:-15px}[class*=uk-navbar-dropdown-right]{margin-left:15px}.uk-navbar-dropdown-grid{margin-left:-50px}.uk-navbar-dropdown-grid>*{padding-left:50px}.uk-navbar-dropdown-grid>.uk-grid-margin{margin-top:50px}.uk-navbar-dropdown-stack .uk-navbar-dropdown-grid>*{width:100%!important}.uk-navbar-dropdown-width-2:not(.uk-navbar-dropdown-stack){width:400px}.uk-navbar-dropdown-width-3:not(.uk-navbar-dropdown-stack){width:600px}.uk-navbar-dropdown-width-4:not(.uk-navbar-dropdown-stack){width:800px}.uk-navbar-dropdown-width-5:not(.uk-navbar-dropdown-stack){width:1000px}.uk-navbar-dropdown-dropbar{margin-top:0;margin-bottom:0;box-shadow:none}.uk-navbar-dropdown-nav{font-size:.875rem}.uk-navbar-dropdown-nav>li>a{color:#999}.uk-navbar-dropdown-nav>li>a:focus,.uk-navbar-dropdown-nav>li>a:hover{color:#666}.uk-navbar-dropdown-nav>li.uk-active>a{color:#333}.uk-navbar-dropdown-nav .uk-nav-header{color:#333}.uk-navbar-dropdown-nav .uk-nav-divider{border-top:1px solid #e5e5e5}.uk-navbar-dropdown-nav .uk-nav-sub a{color:#999}.uk-navbar-dropdown-nav .uk-nav-sub a:focus,.uk-navbar-dropdown-nav .uk-nav-sub a:hover{color:#666}.uk-navbar-dropdown-nav .uk-nav-sub li.uk-active>a{color:#333}.uk-navbar-dropbar{background:#fff}.uk-navbar-dropbar-slide{position:absolute;z-index:980;left:0;right:0;box-shadow:0 5px 7px rgba(0,0,0,.05)}.uk-navbar-container>.uk-container .uk-navbar-left{margin-left:-15px;margin-right:-15px}.uk-navbar-container>.uk-container .uk-navbar-right{margin-right:-15px}.uk-navbar-dropdown-grid>*{position:relative}.uk-navbar-dropdown-grid>:not(.uk-first-column)::before{content:"";position:absolute;top:0;bottom:0;left:25px;border-left:1px solid #e5e5e5}.uk-navbar-dropdown-grid.uk-grid-stack>.uk-grid-margin::before{content:"";position:absolute;top:-25px;left:50px;right:0;border-top:1px solid #e5e5e5}.uk-subnav{display:flex;flex-wrap:wrap;margin-left:-20px;padding:0;list-style:none}.uk-subnav>*{flex:none;padding-left:20px;position:relative}.uk-subnav>*>:first-child{display:block;color:#999;font-size:.875rem;text-transform:uppercase;transition:.1s ease-in-out;transition-property:color,background-color}.uk-subnav>*>a:focus,.uk-subnav>*>a:hover{color:#666;text-decoration:none;outline:0}.uk-subnav>.uk-active>a{color:#333}.uk-subnav-divider>*{display:flex;align-items:center}.uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before{content:"";height:1.5em;margin-left:0;margin-right:20px;border-left:1px solid #e5e5e5}.uk-subnav-pill>*>:first-child{padding:5px 10px;background:0 0;color:#999}.uk-subnav-pill>*>a:focus,.uk-subnav-pill>*>a:hover{background-color:#f8f8f8;color:#666}.uk-subnav-pill>*>a:active{background-color:#f8f8f8;color:#666}.uk-subnav-pill>.uk-active>a{background-color:#1e87f0;color:#fff}.uk-subnav>.uk-disabled>a{color:#999}.uk-breadcrumb{display:flex;flex-wrap:wrap;padding:0;list-style:none}.uk-breadcrumb>*{flex:none}.uk-breadcrumb>*>*{display:inline-block;font-size:.875rem;color:#999}.uk-breadcrumb>*>:focus,.uk-breadcrumb>*>:hover{color:#666;text-decoration:none}.uk-breadcrumb>:last-child>a:not([href]),.uk-breadcrumb>:last-child>span{color:#666}.uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before{content:"/";display:inline-block;margin:0 20px;font-size:.875rem;color:#999}.uk-pagination{display:flex;flex-wrap:wrap;margin-left:-20px;padding:0;list-style:none}.uk-pagination>*{flex:none;padding-left:20px;position:relative}.uk-pagination>*>*{display:block;color:#999;transition:color .1s ease-in-out}.uk-pagination>*>:focus,.uk-pagination>*>:hover{color:#666;text-decoration:none}.uk-pagination>.uk-active>*{color:#666}.uk-pagination>.uk-disabled>*{color:#999}.uk-tab{display:flex;flex-wrap:wrap;margin-left:-20px;padding:0;list-style:none;position:relative}.uk-tab::before{content:"";position:absolute;bottom:0;left:20px;right:0;border-bottom:1px solid #e5e5e5}.uk-tab>*{flex:none;padding-left:20px;position:relative}.uk-tab>*>a{display:block;text-align:center;padding:5px 10px;color:#999;border-bottom:1px solid transparent;font-size:.875rem;text-transform:uppercase;transition:color .1s ease-in-out}.uk-tab>*>a:focus,.uk-tab>*>a:hover{color:#666;text-decoration:none}.uk-tab>.uk-active>a{color:#333;border-color:#1e87f0}.uk-tab>.uk-disabled>a{color:#999}.uk-tab-bottom::before{top:0;bottom:auto}.uk-tab-bottom>*>a{border-top:1px solid transparent;border-bottom:none}.uk-tab-left,.uk-tab-right{flex-direction:column;margin-left:0}.uk-tab-left>*,.uk-tab-right>*{padding-left:0}.uk-tab-left::before{top:0;bottom:0;left:auto;right:0;border-left:1px solid #e5e5e5;border-bottom:none}.uk-tab-right::before{top:0;bottom:0;left:0;right:auto;border-left:1px solid #e5e5e5;border-bottom:none}.uk-tab-left>*>a{text-align:left;border-right:1px solid transparent;border-bottom:none}.uk-tab-right>*>a{text-align:left;border-left:1px solid transparent;border-bottom:none}.uk-tab .uk-dropdown{margin-left:30px}.uk-slidenav{padding:5px 10px;color:rgba(102,102,102,.5);transition:color .1s ease-in-out}.uk-slidenav:focus,.uk-slidenav:hover{color:rgba(102,102,102,.9);outline:0}.uk-slidenav:active{color:rgba(102,102,102,.5)}.uk-slidenav-large{padding:10px 10px}.uk-slidenav-container{display:flex}.uk-dotnav{display:flex;flex-wrap:wrap;margin:0;padding:0;list-style:none;margin-left:-12px}.uk-dotnav>*{flex:none;padding-left:12px}.uk-dotnav>*>*{display:block;box-sizing:border-box;width:10px;height:10px;border-radius:50%;background:0 0;text-indent:100%;overflow:hidden;white-space:nowrap;border:1px solid rgba(102,102,102,.4);transition:.2s ease-in-out;transition-property:background-color,border-color}.uk-dotnav>*>:focus,.uk-dotnav>*>:hover{background-color:rgba(102,102,102,.6);outline:0;border-color:transparent}.uk-dotnav>*>:active{background-color:rgba(102,102,102,.2);border-color:transparent}.uk-dotnav>.uk-active>*{background-color:rgba(102,102,102,.6);border-color:transparent}.uk-dotnav-vertical{flex-direction:column;margin-left:0;margin-top:-12px}.uk-dotnav-vertical>*{padding-left:0;padding-top:12px}.uk-thumbnav{display:flex;flex-wrap:wrap;margin:0;padding:0;list-style:none;margin-left:-15px}.uk-thumbnav>*{padding-left:15px}.uk-thumbnav>*>*{display:inline-block;position:relative}.uk-thumbnav>*>::after{content:"";position:absolute;top:0;bottom:0;left:0;right:0;background:rgba(255,255,255,.4);transition:background-color .1s ease-in-out}.uk-thumbnav>*>:focus,.uk-thumbnav>*>:hover{outline:0}.uk-thumbnav>*>:focus::after,.uk-thumbnav>*>:hover::after{background-color:transparent}.uk-thumbnav>.uk-active>::after{background-color:transparent}.uk-thumbnav-vertical{flex-direction:column;margin-left:0;margin-top:-15px}.uk-thumbnav-vertical>*{padding-left:0;padding-top:15px}.uk-accordion{padding:0;list-style:none}.uk-accordion>:nth-child(n+2){margin-top:20px}.uk-accordion-title{display:block;font-size:1.25rem;line-height:1.4;color:#333;overflow:hidden}.uk-accordion-title::before{content:"";width:1.4em;height:1.4em;margin-left:10px;float:right;background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2213%22%20height%3D%2213%22%20viewBox%3D%220%200%2013%2013%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22%23666%22%20width%3D%2213%22%20height%3D%221%22%20x%3D%220%22%20y%3D%226%22%20%2F%3E%0A%20%20%20%20%3Crect%20fill%3D%22%23666%22%20width%3D%221%22%20height%3D%2213%22%20x%3D%226%22%20y%3D%220%22%20%2F%3E%0A%3C%2Fsvg%3E");background-repeat:no-repeat;background-position:50% 50%}.uk-open>.uk-accordion-title::before{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2213%22%20height%3D%2213%22%20viewBox%3D%220%200%2013%2013%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22%23666%22%20width%3D%2213%22%20height%3D%221%22%20x%3D%220%22%20y%3D%226%22%20%2F%3E%0A%3C%2Fsvg%3E")}.uk-accordion-title:focus,.uk-accordion-title:hover{color:#666;text-decoration:none;outline:0}.uk-accordion-content{margin-top:20px}.uk-accordion-content::after,.uk-accordion-content::before{content:"";display:table}.uk-accordion-content::after{clear:both}.uk-accordion-content>:last-child{margin-bottom:0}.uk-drop{display:none;position:absolute;z-index:1020;box-sizing:border-box;width:300px}.uk-drop.uk-open{display:block}[class*=uk-drop-top]{margin-top:-20px}[class*=uk-drop-bottom]{margin-top:20px}[class*=uk-drop-left]{margin-left:-20px}[class*=uk-drop-right]{margin-left:20px}.uk-drop-stack .uk-drop-grid>*{width:100%!important}.uk-dropdown{display:none;position:absolute;z-index:1020;box-sizing:border-box;min-width:200px;padding:25px;background:#fff;color:#666;box-shadow:0 5px 12px rgba(0,0,0,.15)}.uk-dropdown.uk-open{display:block}.uk-dropdown-nav{white-space:nowrap;font-size:.875rem}.uk-dropdown-nav>li>a{color:#999}.uk-dropdown-nav>li.uk-active>a,.uk-dropdown-nav>li>a:focus,.uk-dropdown-nav>li>a:hover{color:#666}.uk-dropdown-nav .uk-nav-header{color:#333}.uk-dropdown-nav .uk-nav-divider{border-top:1px solid #e5e5e5}.uk-dropdown-nav .uk-nav-sub a{color:#999}.uk-dropdown-nav .uk-nav-sub a:focus,.uk-dropdown-nav .uk-nav-sub a:hover,.uk-dropdown-nav .uk-nav-sub li.uk-active>a{color:#666}[class*=uk-dropdown-top]{margin-top:-10px}[class*=uk-dropdown-bottom]{margin-top:10px}[class*=uk-dropdown-left]{margin-left:-10px}[class*=uk-dropdown-right]{margin-left:10px}.uk-dropdown-stack .uk-dropdown-grid>*{width:100%!important}.uk-modal{display:none;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1010;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:15px 15px;background:rgba(0,0,0,.6);opacity:0;transition:opacity .15s linear}@media (min-width:640px){.uk-modal{padding:50px 30px}}@media (min-width:960px){.uk-modal{padding-left:40px;padding-right:40px}}.uk-modal.uk-open{opacity:1}.uk-modal-page{overflow:hidden}.uk-modal-dialog{position:relative;box-sizing:border-box;margin:0 auto;width:600px;max-width:calc(100% - .01px)!important;background:#fff;opacity:0;transform:translateY(-100px);transition:.3s linear;transition-property:opacity,transform}.uk-open>.uk-modal-dialog{opacity:1;transform:translateY(0)}.uk-modal-container .uk-modal-dialog{width:1200px}.uk-modal-full{padding:0;background:0 0}.uk-modal-full .uk-modal-dialog{margin:0;width:100%;max-width:100%;transform:translateY(0)}.uk-modal-body{padding:30px 30px}.uk-modal-header{padding:15px 30px;background:#fff;border-bottom:1px solid #e5e5e5}.uk-modal-footer{padding:15px 30px;background:#fff;border-top:1px solid #e5e5e5}.uk-modal-body::after,.uk-modal-body::before,.uk-modal-footer::after,.uk-modal-footer::before,.uk-modal-header::after,.uk-modal-header::before{content:"";display:table}.uk-modal-body::after,.uk-modal-footer::after,.uk-modal-header::after{clear:both}.uk-modal-body>:last-child,.uk-modal-footer>:last-child,.uk-modal-header>:last-child{margin-bottom:0}.uk-modal-title{font-size:2rem;line-height:1.3}[class*=uk-modal-close-]{position:absolute;z-index:1010;top:10px;right:10px;padding:5px}[class*=uk-modal-close-]:first-child+*{margin-top:0}.uk-modal-close-outside{top:0;right:-5px;transform:translate(0,-100%);color:#fff}.uk-modal-close-outside:hover{color:#fff}@media (min-width:960px){.uk-modal-close-outside{right:0;transform:translate(100%,-100%)}}.uk-modal-close-full{top:0;right:0;padding:20px;background:#fff}.uk-lightbox{display:none;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1010;background:#000;opacity:0;transition:opacity .15s linear}.uk-lightbox.uk-open{display:block;opacity:1}.uk-lightbox-page{overflow:hidden}.uk-lightbox-items>*{position:absolute;top:0;right:0;bottom:0;left:0;display:none;justify-content:center;align-items:center;color:rgba(255,255,255,.7);will-change:transform,opacity}.uk-lightbox-items>*>*{max-width:100vw;max-height:100vh}.uk-lightbox-items>:focus{outline:0}.uk-lightbox-items>*>:not(iframe){width:auto;height:auto}.uk-lightbox-items>.uk-active{display:flex}.uk-lightbox-toolbar{padding:10px 10px;background:rgba(0,0,0,.3);color:rgba(255,255,255,.7)}.uk-lightbox-toolbar>*{color:rgba(255,255,255,.7)}.uk-lightbox-toolbar-icon{padding:5px;color:rgba(255,255,255,.7)}.uk-lightbox-toolbar-icon:hover{color:#fff}.uk-lightbox-button{box-sizing:border-box;width:50px;height:50px;background:rgba(0,0,0,.3);color:rgba(255,255,255,.7);display:inline-flex;justify-content:center;align-items:center}.uk-lightbox-button:hover{color:#fff}.uk-lightbox-iframe{width:80%;height:80%}.uk-slideshow{-webkit-tap-highlight-color:transparent}.uk-slideshow-items{position:relative;z-index:0;margin:0;padding:0;list-style:none;overflow:hidden;-webkit-touch-callout:none}.uk-slideshow-items>*{position:absolute;top:0;left:0;right:0;bottom:0;overflow:hidden;will-change:transform,opacity;touch-action:pan-y}.uk-slideshow-items>:focus{outline:0}.uk-slideshow-items>:not(.uk-active){display:none}.uk-slider{-webkit-tap-highlight-color:transparent}.uk-slider-container{overflow:hidden}.uk-slider-items{will-change:transform;position:relative}.uk-slider-items:not(.uk-grid){display:flex;margin:0;padding:0;list-style:none;-webkit-touch-callout:none}.uk-slider-items.uk-grid{flex-wrap:nowrap}.uk-slider-items>*{flex:none;position:relative;touch-action:pan-y}.uk-slider-items>:focus{outline:0}.uk-sticky-fixed{z-index:980;box-sizing:border-box;margin:0!important;-webkit-backface-visibility:hidden;backface-visibility:hidden}.uk-sticky[class*=uk-animation-]{animation-duration:.2s}.uk-sticky.uk-animation-reverse{animation-duration:.2s}.uk-offcanvas{display:none;position:fixed;top:0;bottom:0;left:0;z-index:1000}.uk-offcanvas-flip .uk-offcanvas{right:0;left:auto}.uk-offcanvas-bar{position:absolute;top:0;bottom:0;left:-270px;box-sizing:border-box;width:270px;padding:20px 20px;background:#222;overflow-y:auto;-webkit-overflow-scrolling:touch}@media (min-width:960px){.uk-offcanvas-bar{left:-350px;width:350px;padding:40px 40px}}.uk-offcanvas-flip .uk-offcanvas-bar{left:auto;right:-270px}@media (min-width:960px){.uk-offcanvas-flip .uk-offcanvas-bar{right:-350px}}.uk-open>.uk-offcanvas-bar{left:0}.uk-offcanvas-flip .uk-open>.uk-offcanvas-bar{left:auto;right:0}.uk-offcanvas-bar-animation{transition:left .3s ease-out}.uk-offcanvas-flip .uk-offcanvas-bar-animation{transition-property:right}.uk-offcanvas-reveal{position:absolute;top:0;bottom:0;left:0;width:0;overflow:hidden;transition:width .3s ease-out}.uk-offcanvas-reveal .uk-offcanvas-bar{left:0}.uk-offcanvas-flip .uk-offcanvas-reveal .uk-offcanvas-bar{left:auto;right:0}.uk-open>.uk-offcanvas-reveal{width:270px}@media (min-width:960px){.uk-open>.uk-offcanvas-reveal{width:350px}}.uk-offcanvas-flip .uk-offcanvas-reveal{right:0;left:auto}.uk-offcanvas-close{position:absolute;z-index:1000;top:20px;right:20px;padding:5px}.uk-offcanvas-overlay{width:100vw;touch-action:none}.uk-offcanvas-overlay::before{content:"";position:absolute;top:0;bottom:0;left:0;right:0;background:rgba(0,0,0,.1);opacity:0;transition:opacity .15s linear}.uk-offcanvas-overlay.uk-open::before{opacity:1}.uk-offcanvas-container,.uk-offcanvas-page{overflow-x:hidden}.uk-offcanvas-container{position:relative;left:0;transition:left .3s ease-out;box-sizing:border-box;width:100%}:not(.uk-offcanvas-flip).uk-offcanvas-container-animation{left:270px}.uk-offcanvas-flip.uk-offcanvas-container-animation{left:-270px}@media (min-width:960px){:not(.uk-offcanvas-flip).uk-offcanvas-container-animation{left:350px}.uk-offcanvas-flip.uk-offcanvas-container-animation{left:-350px}}.uk-switcher{margin:0;padding:0;list-style:none}.uk-switcher>:not(.uk-active){display:none}.uk-switcher>*>:last-child{margin-bottom:0}.uk-leader{overflow:hidden}.uk-leader-fill::after{display:inline-block;margin-left:15px;width:0;content:attr(data-fill);white-space:nowrap}.uk-leader-fill.uk-leader-hide::after{display:none}.uk-leader-fill-content::before{content:'.'}:root{--uk-leader-fill-content:.}.uk-iconnav{display:flex;flex-wrap:wrap;margin:0;padding:0;list-style:none;margin-left:-10px}.uk-iconnav>*{padding-left:10px}.uk-iconnav>*>a{display:block;color:#999}.uk-iconnav>*>a:focus,.uk-iconnav>*>a:hover{color:#666;outline:0}.uk-iconnav>.uk-active>a{color:#666}.uk-iconnav-vertical{flex-direction:column;margin-left:0;margin-top:-10px}.uk-iconnav-vertical>*{padding-left:0;padding-top:10px}.uk-notification{position:fixed;top:10px;left:10px;z-index:1040;box-sizing:border-box;width:350px}.uk-notification-bottom-right,.uk-notification-top-right{left:auto;right:10px}.uk-notification-bottom-center,.uk-notification-top-center{left:50%;margin-left:-175px}.uk-notification-bottom-center,.uk-notification-bottom-left,.uk-notification-bottom-right{top:auto;bottom:10px}@media (max-width:639px){.uk-notification{left:10px;right:10px;width:auto;margin:0}}.uk-notification-message{position:relative;padding:15px;background:#f8f8f8;color:#666;font-size:1.25rem;line-height:1.4;cursor:pointer}*+.uk-notification-message{margin-top:10px}.uk-notification-close{display:none;position:absolute;top:20px;right:15px}.uk-notification-message:hover .uk-notification-close{display:block}.uk-notification-message-primary{color:#1e87f0}.uk-notification-message-success{color:#32d296}.uk-notification-message-warning{color:#faa05a}.uk-notification-message-danger{color:#f0506e}.uk-tooltip{display:none;position:absolute;z-index:1030;top:0;box-sizing:border-box;max-width:200px;padding:3px 6px;background:#666;border-radius:2px;color:#fff;font-size:12px}.uk-tooltip.uk-active{display:block}[class*=uk-tooltip-top]{margin-top:-10px}[class*=uk-tooltip-bottom]{margin-top:10px}[class*=uk-tooltip-left]{margin-left:-10px}[class*=uk-tooltip-right]{margin-left:10px}.uk-placeholder{margin-bottom:20px;padding:30px 30px;background:0 0;border:1px dashed #e5e5e5}*+.uk-placeholder{margin-top:20px}.uk-placeholder>:last-child{margin-bottom:0}.uk-progress{vertical-align:baseline;-webkit-appearance:none;-moz-appearance:none;display:block;width:100%;border:0;background-color:#f8f8f8;margin-bottom:20px;height:15px;border-radius:500px;overflow:hidden}*+.uk-progress{margin-top:20px}.uk-progress:indeterminate{color:transparent}.uk-progress::-webkit-progress-bar{background-color:#f8f8f8;border-radius:500px;overflow:hidden}.uk-progress:indeterminate::-moz-progress-bar{width:0}.uk-progress::-webkit-progress-value{background-color:#1e87f0;transition:width .6s ease}.uk-progress::-moz-progress-bar{background-color:#1e87f0}.uk-progress::-ms-fill{background-color:#1e87f0;transition:width .6s ease;border:0}.uk-sortable{position:relative}.uk-sortable svg{pointer-events:none}.uk-sortable>:last-child{margin-bottom:0}.uk-sortable-drag{position:absolute!important;z-index:1050!important;pointer-events:none}.uk-sortable-placeholder{opacity:0;pointer-events:none}.uk-sortable-empty{min-height:50px}.uk-sortable-handle:hover{cursor:move}.uk-countdown-number{font-variant-numeric:tabular-nums;font-size:2rem;line-height:.8}@media (min-width:640px){.uk-countdown-number{font-size:4rem}}@media (min-width:960px){.uk-countdown-number{font-size:6rem}}.uk-countdown-separator{font-size:1rem;line-height:1.6}@media (min-width:640px){.uk-countdown-separator{font-size:2rem}}@media (min-width:960px){.uk-countdown-separator{font-size:3rem}}[class*=uk-animation-]{animation-duration:.5s;animation-timing-function:ease-out;animation-fill-mode:both}.uk-animation-fade{animation-name:uk-fade;animation-duration:.8s;animation-timing-function:linear}.uk-animation-scale-up{animation-name:uk-fade-scale-02}.uk-animation-scale-down{animation-name:uk-fade-scale-18}.uk-animation-slide-top{animation-name:uk-fade-top}.uk-animation-slide-bottom{animation-name:uk-fade-bottom}.uk-animation-slide-left{animation-name:uk-fade-left}.uk-animation-slide-right{animation-name:uk-fade-right}.uk-animation-slide-top-small{animation-name:uk-fade-top-small}.uk-animation-slide-bottom-small{animation-name:uk-fade-bottom-small}.uk-animation-slide-left-small{animation-name:uk-fade-left-small}.uk-animation-slide-right-small{animation-name:uk-fade-right-small}.uk-animation-slide-top-medium{animation-name:uk-fade-top-medium}.uk-animation-slide-bottom-medium{animation-name:uk-fade-bottom-medium}.uk-animation-slide-left-medium{animation-name:uk-fade-left-medium}.uk-animation-slide-right-medium{animation-name:uk-fade-right-medium}.uk-animation-kenburns{animation-name:uk-scale-kenburns;animation-duration:15s}.uk-animation-shake{animation-name:uk-shake}.uk-animation-stroke{animation-name:uk-stroke;stroke-dasharray:var(--uk-animation-stroke);animation-duration:2s}.uk-animation-reverse{animation-direction:reverse;animation-timing-function:ease-in}.uk-animation-fast{animation-duration:.1s}.uk-animation-toggle:not(:hover):not(:focus) [class*=uk-animation-]{animation-name:none}.uk-animation-toggle{-webkit-tap-highlight-color:transparent}.uk-animation-toggle:focus{outline:0}@keyframes uk-fade{0%{opacity:0}100%{opacity:1}}@keyframes uk-fade-top{0%{opacity:0;transform:translateY(-100%)}100%{opacity:1;transform:translateY(0)}}@keyframes uk-fade-bottom{0%{opacity:0;transform:translateY(100%)}100%{opacity:1;transform:translateY(0)}}@keyframes uk-fade-left{0%{opacity:0;transform:translateX(-100%)}100%{opacity:1;transform:translateX(0)}}@keyframes uk-fade-right{0%{opacity:0;transform:translateX(100%)}100%{opacity:1;transform:translateX(0)}}@keyframes uk-fade-top-small{0%{opacity:0;transform:translateY(-10px)}100%{opacity:1;transform:translateY(0)}}@keyframes uk-fade-bottom-small{0%{opacity:0;transform:translateY(10px)}100%{opacity:1;transform:translateY(0)}}@keyframes uk-fade-left-small{0%{opacity:0;transform:translateX(-10px)}100%{opacity:1;transform:translateX(0)}}@keyframes uk-fade-right-small{0%{opacity:0;transform:translateX(10px)}100%{opacity:1;transform:translateX(0)}}@keyframes uk-fade-top-medium{0%{opacity:0;transform:translateY(-50px)}100%{opacity:1;transform:translateY(0)}}@keyframes uk-fade-bottom-medium{0%{opacity:0;transform:translateY(50px)}100%{opacity:1;transform:translateY(0)}}@keyframes uk-fade-left-medium{0%{opacity:0;transform:translateX(-50px)}100%{opacity:1;transform:translateX(0)}}@keyframes uk-fade-right-medium{0%{opacity:0;transform:translateX(50px)}100%{opacity:1;transform:translateX(0)}}@keyframes uk-fade-scale-02{0%{opacity:0;transform:scale(.2)}100%{opacity:1;transform:scale(1)}}@keyframes uk-fade-scale-18{0%{opacity:0;transform:scale(1.8)}100%{opacity:1;transform:scale(1)}}@keyframes uk-scale-kenburns{0%{transform:scale(1)}100%{transform:scale(1.2)}}@keyframes uk-shake{0%,100%{transform:translateX(0)}10%{transform:translateX(-9px)}20%{transform:translateX(8px)}30%{transform:translateX(-7px)}40%{transform:translateX(6px)}50%{transform:translateX(-5px)}60%{transform:translateX(4px)}70%{transform:translateX(-3px)}80%{transform:translateX(2px)}90%{transform:translateX(-1px)}}@keyframes uk-stroke{0%{stroke-dashoffset:var(--uk-animation-stroke)}100%{stroke-dashoffset:0}}[class*=uk-child-width]>*{box-sizing:border-box;width:100%}.uk-child-width-1-2>*{width:50%}.uk-child-width-1-3>*{width:calc(100% * 1 / 3.001)}.uk-child-width-1-4>*{width:25%}.uk-child-width-1-5>*{width:20%}.uk-child-width-1-6>*{width:calc(100% * 1 / 6.001)}.uk-child-width-auto>*{width:auto}.uk-child-width-expand>:not([class*=uk-width]){flex:1;min-width:1px}@media (min-width:640px){.uk-child-width-1-1\@s>*{width:100%}.uk-child-width-1-2\@s>*{width:50%}.uk-child-width-1-3\@s>*{width:calc(100% * 1 / 3.001)}.uk-child-width-1-4\@s>*{width:25%}.uk-child-width-1-5\@s>*{width:20%}.uk-child-width-1-6\@s>*{width:calc(100% * 1 / 6.001)}.uk-child-width-auto\@s>*{width:auto}.uk-child-width-expand\@s>:not([class*=uk-width]){flex:1;min-width:1px}}@media (min-width:960px){.uk-child-width-1-1\@m>*{width:100%}.uk-child-width-1-2\@m>*{width:50%}.uk-child-width-1-3\@m>*{width:calc(100% * 1 / 3.001)}.uk-child-width-1-4\@m>*{width:25%}.uk-child-width-1-5\@m>*{width:20%}.uk-child-width-1-6\@m>*{width:calc(100% * 1 / 6.001)}.uk-child-width-auto\@m>*{width:auto}.uk-child-width-expand\@m>:not([class*=uk-width]){flex:1;min-width:1px}}@media (min-width:1200px){.uk-child-width-1-1\@l>*{width:100%}.uk-child-width-1-2\@l>*{width:50%}.uk-child-width-1-3\@l>*{width:calc(100% * 1 / 3.001)}.uk-child-width-1-4\@l>*{width:25%}.uk-child-width-1-5\@l>*{width:20%}.uk-child-width-1-6\@l>*{width:calc(100% * 1 / 6.001)}.uk-child-width-auto\@l>*{width:auto}.uk-child-width-expand\@l>:not([class*=uk-width]){flex:1;min-width:1px}}@media (min-width:1600px){.uk-child-width-1-1\@xl>*{width:100%}.uk-child-width-1-2\@xl>*{width:50%}.uk-child-width-1-3\@xl>*{width:calc(100% * 1 / 3.001)}.uk-child-width-1-4\@xl>*{width:25%}.uk-child-width-1-5\@xl>*{width:20%}.uk-child-width-1-6\@xl>*{width:calc(100% * 1 / 6.001)}.uk-child-width-auto\@xl>*{width:auto}.uk-child-width-expand\@xl>:not([class*=uk-width]){flex:1;min-width:1px}}[class*=uk-width]{box-sizing:border-box;width:100%;max-width:100%}.uk-width-1-2{width:50%}.uk-width-1-3{width:calc(100% * 1 / 3.001)}.uk-width-2-3{width:calc(100% * 2 / 3.001)}.uk-width-1-4{width:25%}.uk-width-3-4{width:75%}.uk-width-1-5{width:20%}.uk-width-2-5{width:40%}.uk-width-3-5{width:60%}.uk-width-4-5{width:80%}.uk-width-1-6{width:calc(100% * 1 / 6.001)}.uk-width-5-6{width:calc(100% * 5 / 6.001)}.uk-width-small{width:150px}.uk-width-medium{width:300px}.uk-width-large{width:450px}.uk-width-xlarge{width:600px}.uk-width-xxlarge{width:750px}.uk-width-auto{width:auto}.uk-width-expand{flex:1;min-width:1px}@media (min-width:640px){.uk-width-1-1\@s{width:100%}.uk-width-1-2\@s{width:50%}.uk-width-1-3\@s{width:calc(100% * 1 / 3.001)}.uk-width-2-3\@s{width:calc(100% * 2 / 3.001)}.uk-width-1-4\@s{width:25%}.uk-width-3-4\@s{width:75%}.uk-width-1-5\@s{width:20%}.uk-width-2-5\@s{width:40%}.uk-width-3-5\@s{width:60%}.uk-width-4-5\@s{width:80%}.uk-width-1-6\@s{width:calc(100% * 1 / 6.001)}.uk-width-5-6\@s{width:calc(100% * 5 / 6.001)}.uk-width-small\@s{width:150px}.uk-width-medium\@s{width:300px}.uk-width-large\@s{width:450px}.uk-width-xlarge\@s{width:600px}.uk-width-xxlarge\@s{width:750px}.uk-width-auto\@s{width:auto}.uk-width-expand\@s{flex:1;min-width:1px}}@media (min-width:960px){.uk-width-1-1\@m{width:100%}.uk-width-1-2\@m{width:50%}.uk-width-1-3\@m{width:calc(100% * 1 / 3.001)}.uk-width-2-3\@m{width:calc(100% * 2 / 3.001)}.uk-width-1-4\@m{width:25%}.uk-width-3-4\@m{width:75%}.uk-width-1-5\@m{width:20%}.uk-width-2-5\@m{width:40%}.uk-width-3-5\@m{width:60%}.uk-width-4-5\@m{width:80%}.uk-width-1-6\@m{width:calc(100% * 1 / 6.001)}.uk-width-5-6\@m{width:calc(100% * 5 / 6.001)}.uk-width-small\@m{width:150px}.uk-width-medium\@m{width:300px}.uk-width-large\@m{width:450px}.uk-width-xlarge\@m{width:600px}.uk-width-xxlarge\@m{width:750px}.uk-width-auto\@m{width:auto}.uk-width-expand\@m{flex:1;min-width:1px}}@media (min-width:1200px){.uk-width-1-1\@l{width:100%}.uk-width-1-2\@l{width:50%}.uk-width-1-3\@l{width:calc(100% * 1 / 3.001)}.uk-width-2-3\@l{width:calc(100% * 2 / 3.001)}.uk-width-1-4\@l{width:25%}.uk-width-3-4\@l{width:75%}.uk-width-1-5\@l{width:20%}.uk-width-2-5\@l{width:40%}.uk-width-3-5\@l{width:60%}.uk-width-4-5\@l{width:80%}.uk-width-1-6\@l{width:calc(100% * 1 / 6.001)}.uk-width-5-6\@l{width:calc(100% * 5 / 6.001)}.uk-width-small\@l{width:150px}.uk-width-medium\@l{width:300px}.uk-width-large\@l{width:450px}.uk-width-xlarge\@l{width:600px}.uk-width-xxlarge\@l{width:750px}.uk-width-auto\@l{width:auto}.uk-width-expand\@l{flex:1;min-width:1px}}@media (min-width:1600px){.uk-width-1-1\@xl{width:100%}.uk-width-1-2\@xl{width:50%}.uk-width-1-3\@xl{width:calc(100% * 1 / 3.001)}.uk-width-2-3\@xl{width:calc(100% * 2 / 3.001)}.uk-width-1-4\@xl{width:25%}.uk-width-3-4\@xl{width:75%}.uk-width-1-5\@xl{width:20%}.uk-width-2-5\@xl{width:40%}.uk-width-3-5\@xl{width:60%}.uk-width-4-5\@xl{width:80%}.uk-width-1-6\@xl{width:calc(100% * 1 / 6.001)}.uk-width-5-6\@xl{width:calc(100% * 5 / 6.001)}.uk-width-small\@xl{width:150px}.uk-width-medium\@xl{width:300px}.uk-width-large\@xl{width:450px}.uk-width-xlarge\@xl{width:600px}.uk-width-xxlarge\@xl{width:750px}.uk-width-auto\@xl{width:auto}.uk-width-expand\@xl{flex:1;min-width:1px}}[class*=uk-height]{box-sizing:border-box}.uk-height-1-1{height:100%}.uk-height-viewport{min-height:100vh}.uk-height-small{height:150px}.uk-height-medium{height:300px}.uk-height-large{height:450px}.uk-height-max-small{max-height:150px}.uk-height-max-medium{max-height:300px}.uk-height-max-large{max-height:450px}.uk-text-lead{font-size:1.5rem;line-height:1.5;color:#333}.uk-text-meta{font-size:.875rem;line-height:1.4;color:#999}.uk-text-meta a{color:#999}.uk-text-meta a:hover{color:#666;text-decoration:none}.uk-text-small{font-size:.875rem;line-height:1.5}.uk-text-large{font-size:1.5rem;line-height:1.5}.uk-text-light{font-weight:300}.uk-text-normal{font-weight:400}.uk-text-bold{font-weight:700}.uk-text-lighter{font-weight:lighter}.uk-text-bolder{font-weight:bolder}.uk-text-italic{font-style:italic}.uk-text-capitalize{text-transform:capitalize!important}.uk-text-uppercase{text-transform:uppercase!important}.uk-text-lowercase{text-transform:lowercase!important}.uk-text-muted{color:#999!important}.uk-text-emphasis{color:#333!important}.uk-text-primary{color:#1e87f0!important}.uk-text-secondary{color:#222!important}.uk-text-success{color:#32d296!important}.uk-text-warning{color:#faa05a!important}.uk-text-danger{color:#f0506e!important}.uk-text-background{-webkit-background-clip:text;-webkit-text-fill-color:transparent;display:inline-block;color:#1e87f0!important}@supports (-webkit-background-clip:text){.uk-text-background{background-color:#1e87f0}}.uk-text-left{text-align:left!important}.uk-text-right{text-align:right!important}.uk-text-center{text-align:center!important}.uk-text-justify{text-align:justify!important}@media (min-width:640px){.uk-text-left\@s{text-align:left!important}.uk-text-right\@s{text-align:right!important}.uk-text-center\@s{text-align:center!important}}@media (min-width:960px){.uk-text-left\@m{text-align:left!important}.uk-text-right\@m{text-align:right!important}.uk-text-center\@m{text-align:center!important}}@media (min-width:1200px){.uk-text-left\@l{text-align:left!important}.uk-text-right\@l{text-align:right!important}.uk-text-center\@l{text-align:center!important}}@media (min-width:1600px){.uk-text-left\@xl{text-align:left!important}.uk-text-right\@xl{text-align:right!important}.uk-text-center\@xl{text-align:center!important}}.uk-text-top{vertical-align:top!important}.uk-text-middle{vertical-align:middle!important}.uk-text-bottom{vertical-align:bottom!important}.uk-text-baseline{vertical-align:baseline!important}.uk-text-nowrap{white-space:nowrap}.uk-text-truncate{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}td.uk-text-truncate,th.uk-text-truncate{max-width:0}.uk-text-break{overflow-wrap:break-word;word-wrap:break-word}td.uk-text-break,th.uk-text-break{word-break:break-all}[class*=uk-column-]{column-gap:30px}@media (min-width:1200px){[class*=uk-column-]{column-gap:40px}}[class*=uk-column-] img{transform:translate3d(0,0,0)}.uk-column-divider{column-rule:1px solid #e5e5e5;column-gap:60px}@media (min-width:1200px){.uk-column-divider{column-gap:80px}}.uk-column-1-2{column-count:2}.uk-column-1-3{column-count:3}.uk-column-1-4{column-count:4}.uk-column-1-5{column-count:5}.uk-column-1-6{column-count:6}@media (min-width:640px){.uk-column-1-2\@s{column-count:2}.uk-column-1-3\@s{column-count:3}.uk-column-1-4\@s{column-count:4}.uk-column-1-5\@s{column-count:5}.uk-column-1-6\@s{column-count:6}}@media (min-width:960px){.uk-column-1-2\@m{column-count:2}.uk-column-1-3\@m{column-count:3}.uk-column-1-4\@m{column-count:4}.uk-column-1-5\@m{column-count:5}.uk-column-1-6\@m{column-count:6}}@media (min-width:1200px){.uk-column-1-2\@l{column-count:2}.uk-column-1-3\@l{column-count:3}.uk-column-1-4\@l{column-count:4}.uk-column-1-5\@l{column-count:5}.uk-column-1-6\@l{column-count:6}}@media (min-width:1600px){.uk-column-1-2\@xl{column-count:2}.uk-column-1-3\@xl{column-count:3}.uk-column-1-4\@xl{column-count:4}.uk-column-1-5\@xl{column-count:5}.uk-column-1-6\@xl{column-count:6}}.uk-column-span{column-span:all}.uk-cover{max-width:none;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%)}iframe.uk-cover{pointer-events:none}.uk-cover-container{overflow:hidden;position:relative}.uk-background-default{background-color:#fff}.uk-background-muted{background-color:#f8f8f8}.uk-background-primary{background-color:#1e87f0}.uk-background-secondary{background-color:#222}.uk-background-contain,.uk-background-cover{background-position:50% 50%;background-repeat:no-repeat}.uk-background-cover{background-size:cover}.uk-background-contain{background-size:contain}.uk-background-top-left{background-position:0 0}.uk-background-top-center{background-position:50% 0}.uk-background-top-right{background-position:100% 0}.uk-background-center-left{background-position:0 50%}.uk-background-center-center{background-position:50% 50%}.uk-background-center-right{background-position:100% 50%}.uk-background-bottom-left{background-position:0 100%}.uk-background-bottom-center{background-position:50% 100%}.uk-background-bottom-right{background-position:100% 100%}.uk-background-norepeat{background-repeat:no-repeat}.uk-background-fixed{background-attachment:fixed;backface-visibility:hidden}@media (pointer:coarse){.uk-background-fixed{background-attachment:scroll}}@media (max-width:639px){.uk-background-image\@s{background-image:none!important}}@media (max-width:959px){.uk-background-image\@m{background-image:none!important}}@media (max-width:1199px){.uk-background-image\@l{background-image:none!important}}@media (max-width:1599px){.uk-background-image\@xl{background-image:none!important}}.uk-background-blend-multiply{background-blend-mode:multiply}.uk-background-blend-screen{background-blend-mode:screen}.uk-background-blend-overlay{background-blend-mode:overlay}.uk-background-blend-darken{background-blend-mode:darken}.uk-background-blend-lighten{background-blend-mode:lighten}.uk-background-blend-color-dodge{background-blend-mode:color-dodge}.uk-background-blend-color-burn{background-blend-mode:color-burn}.uk-background-blend-hard-light{background-blend-mode:hard-light}.uk-background-blend-soft-light{background-blend-mode:soft-light}.uk-background-blend-difference{background-blend-mode:difference}.uk-background-blend-exclusion{background-blend-mode:exclusion}.uk-background-blend-hue{background-blend-mode:hue}.uk-background-blend-saturation{background-blend-mode:saturation}.uk-background-blend-color{background-blend-mode:color}.uk-background-blend-luminosity{background-blend-mode:luminosity}[class*=uk-align]{display:block;margin-bottom:30px}*+[class*=uk-align]{margin-top:30px}.uk-align-center{margin-left:auto;margin-right:auto}.uk-align-left{margin-top:0;margin-right:30px;float:left}.uk-align-right{margin-top:0;margin-left:30px;float:right}@media (min-width:640px){.uk-align-left\@s{margin-top:0;margin-right:30px;float:left}.uk-align-right\@s{margin-top:0;margin-left:30px;float:right}}@media (min-width:960px){.uk-align-left\@m{margin-top:0;margin-right:30px;float:left}.uk-align-right\@m{margin-top:0;margin-left:30px;float:right}}@media (min-width:1200px){.uk-align-left\@l{margin-top:0;float:left}.uk-align-right\@l{margin-top:0;float:right}.uk-align-left,.uk-align-left\@l,.uk-align-left\@m,.uk-align-left\@s{margin-right:40px}.uk-align-right,.uk-align-right\@l,.uk-align-right\@m,.uk-align-right\@s{margin-left:40px}}@media (min-width:1600px){.uk-align-left\@xl{margin-top:0;margin-right:40px;float:left}.uk-align-right\@xl{margin-top:0;margin-left:40px;float:right}}.uk-svg,.uk-svg:not(.uk-preserve) [fill*='#']:not(.uk-preserve){fill:currentcolor}.uk-svg:not(.uk-preserve) [stroke*='#']:not(.uk-preserve){stroke:currentcolor}.uk-svg{transform:translate(0,0)}.uk-panel{position:relative;box-sizing:border-box}.uk-panel::after,.uk-panel::before{content:"";display:table}.uk-panel::after{clear:both}.uk-panel>:last-child{margin-bottom:0}.uk-panel-scrollable{height:170px;padding:10px;border:1px solid #e5e5e5;overflow:auto;-webkit-overflow-scrolling:touch;resize:both}.uk-clearfix::before{content:"";display:table-cell}.uk-clearfix::after{content:"";display:table;clear:both}.uk-float-left{float:left}.uk-float-right{float:right}[class*=uk-float-]{max-width:100%}.uk-overflow-hidden{overflow:hidden}.uk-overflow-auto{overflow:auto;-webkit-overflow-scrolling:touch}.uk-overflow-auto>:last-child{margin-bottom:0}.uk-resize{resize:both}.uk-resize-vertical{resize:vertical}.uk-display-block{display:block!important}.uk-display-inline{display:inline!important}.uk-display-inline-block{display:inline-block!important}[class*=uk-inline]{display:inline-block;position:relative;max-width:100%;vertical-align:middle;-webkit-backface-visibility:hidden}.uk-inline-clip{overflow:hidden}.uk-preserve-width,.uk-preserve-width canvas,.uk-preserve-width img,.uk-preserve-width svg,.uk-preserve-width video{max-width:none}.uk-responsive-height,.uk-responsive-width{box-sizing:border-box}.uk-responsive-width{max-width:100%!important;height:auto}.uk-responsive-height{max-height:100%;width:auto;max-width:none}.uk-border-circle{border-radius:50%}.uk-border-pill{border-radius:500px}.uk-border-rounded{border-radius:5px}.uk-inline-clip[class*=uk-border-]{-webkit-transform:translateZ(0)}.uk-box-shadow-small{box-shadow:0 2px 8px rgba(0,0,0,.08)}.uk-box-shadow-medium{box-shadow:0 5px 15px rgba(0,0,0,.08)}.uk-box-shadow-large{box-shadow:0 14px 25px rgba(0,0,0,.16)}.uk-box-shadow-xlarge{box-shadow:0 28px 50px rgba(0,0,0,.16)}[class*=uk-box-shadow-hover]{transition:box-shadow .1s ease-in-out}.uk-box-shadow-hover-small:hover{box-shadow:0 2px 8px rgba(0,0,0,.08)}.uk-box-shadow-hover-medium:hover{box-shadow:0 5px 15px rgba(0,0,0,.08)}.uk-box-shadow-hover-large:hover{box-shadow:0 14px 25px rgba(0,0,0,.16)}.uk-box-shadow-hover-xlarge:hover{box-shadow:0 28px 50px rgba(0,0,0,.16)}@supports (filter:blur(0)){.uk-box-shadow-bottom{display:inline-block;position:relative;max-width:100%;vertical-align:middle}.uk-box-shadow-bottom::before{content:'';position:absolute;bottom:-30px;left:0;right:0;height:30px;border-radius:100%;background:#444;filter:blur(20px)}.uk-box-shadow-bottom>*{position:relative}}.uk-dropcap::first-letter,.uk-dropcap>p:first-of-type::first-letter{display:block;margin-right:10px;float:left;font-size:4.5em;line-height:1;margin-bottom:-2px}@-moz-document url-prefix(){.uk-dropcap::first-letter,.uk-dropcap>p:first-of-type::first-letter{margin-top:1.1%}}@supports (-ms-ime-align:auto){.uk-dropcap>p:first-of-type::first-letter{font-size:1em}}.uk-logo{font-size:1.5rem;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";color:#666;text-decoration:none}.uk-logo:focus,.uk-logo:hover{color:#666;outline:0;text-decoration:none}.uk-logo-inverse{display:none}.uk-disabled{pointer-events:none}.uk-drag,.uk-drag *{cursor:move}.uk-drag iframe{pointer-events:none}.uk-dragover{box-shadow:0 0 20px rgba(100,100,100,.3)}.uk-blend-multiply{mix-blend-mode:multiply}.uk-blend-screen{mix-blend-mode:screen}.uk-blend-overlay{mix-blend-mode:overlay}.uk-blend-darken{mix-blend-mode:darken}.uk-blend-lighten{mix-blend-mode:lighten}.uk-blend-color-dodge{mix-blend-mode:color-dodge}.uk-blend-color-burn{mix-blend-mode:color-burn}.uk-blend-hard-light{mix-blend-mode:hard-light}.uk-blend-soft-light{mix-blend-mode:soft-light}.uk-blend-difference{mix-blend-mode:difference}.uk-blend-exclusion{mix-blend-mode:exclusion}.uk-blend-hue{mix-blend-mode:hue}.uk-blend-saturation{mix-blend-mode:saturation}.uk-blend-color{mix-blend-mode:color}.uk-blend-luminosity{mix-blend-mode:luminosity}.uk-transform-center{transform:translate(-50%,-50%)}.uk-transform-origin-top-left{transform-origin:0 0}.uk-transform-origin-top-center{transform-origin:50% 0}.uk-transform-origin-top-right{transform-origin:100% 0}.uk-transform-origin-center-left{transform-origin:0 50%}.uk-transform-origin-center-right{transform-origin:100% 50%}.uk-transform-origin-bottom-left{transform-origin:0 100%}.uk-transform-origin-bottom-center{transform-origin:50% 100%}.uk-transform-origin-bottom-right{transform-origin:100% 100%}.uk-flex{display:flex}.uk-flex-inline{display:inline-flex}.uk-flex-inline::after,.uk-flex-inline::before,.uk-flex::after,.uk-flex::before{display:none}.uk-flex-left{justify-content:flex-start}.uk-flex-center{justify-content:center}.uk-flex-right{justify-content:flex-end}.uk-flex-between{justify-content:space-between}.uk-flex-around{justify-content:space-around}@media (min-width:640px){.uk-flex-left\@s{justify-content:flex-start}.uk-flex-center\@s{justify-content:center}.uk-flex-right\@s{justify-content:flex-end}.uk-flex-between\@s{justify-content:space-between}.uk-flex-around\@s{justify-content:space-around}}@media (min-width:960px){.uk-flex-left\@m{justify-content:flex-start}.uk-flex-center\@m{justify-content:center}.uk-flex-right\@m{justify-content:flex-end}.uk-flex-between\@m{justify-content:space-between}.uk-flex-around\@m{justify-content:space-around}}@media (min-width:1200px){.uk-flex-left\@l{justify-content:flex-start}.uk-flex-center\@l{justify-content:center}.uk-flex-right\@l{justify-content:flex-end}.uk-flex-between\@l{justify-content:space-between}.uk-flex-around\@l{justify-content:space-around}}@media (min-width:1600px){.uk-flex-left\@xl{justify-content:flex-start}.uk-flex-center\@xl{justify-content:center}.uk-flex-right\@xl{justify-content:flex-end}.uk-flex-between\@xl{justify-content:space-between}.uk-flex-around\@xl{justify-content:space-around}}.uk-flex-stretch{align-items:stretch}.uk-flex-top{align-items:flex-start}.uk-flex-middle{align-items:center}.uk-flex-bottom{align-items:flex-end}.uk-flex-row{flex-direction:row}.uk-flex-row-reverse{flex-direction:row-reverse}.uk-flex-column{flex-direction:column}.uk-flex-column-reverse{flex-direction:column-reverse}.uk-flex-nowrap{flex-wrap:nowrap}.uk-flex-wrap{flex-wrap:wrap}.uk-flex-wrap-reverse{flex-wrap:wrap-reverse}.uk-flex-wrap-stretch{align-content:stretch}.uk-flex-wrap-top{align-content:flex-start}.uk-flex-wrap-middle{align-content:center}.uk-flex-wrap-bottom{align-content:flex-end}.uk-flex-wrap-between{align-content:space-between}.uk-flex-wrap-around{align-content:space-around}.uk-flex-first{order:-1}.uk-flex-last{order:99}@media (min-width:640px){.uk-flex-first\@s{order:-1}.uk-flex-last\@s{order:99}}@media (min-width:960px){.uk-flex-first\@m{order:-1}.uk-flex-last\@m{order:99}}@media (min-width:1200px){.uk-flex-first\@l{order:-1}.uk-flex-last\@l{order:99}}@media (min-width:1600px){.uk-flex-first\@xl{order:-1}.uk-flex-last\@xl{order:99}}.uk-flex-none{flex:none}.uk-flex-auto{flex:auto}.uk-flex-1{flex:1}.uk-margin{margin-bottom:20px}*+.uk-margin{margin-top:20px!important}.uk-margin-top{margin-top:20px!important}.uk-margin-bottom{margin-bottom:20px!important}.uk-margin-left{margin-left:20px!important}.uk-margin-right{margin-right:20px!important}.uk-margin-small{margin-bottom:10px}*+.uk-margin-small{margin-top:10px!important}.uk-margin-small-top{margin-top:10px!important}.uk-margin-small-bottom{margin-bottom:10px!important}.uk-margin-small-left{margin-left:10px!important}.uk-margin-small-right{margin-right:10px!important}.uk-margin-medium{margin-bottom:40px}*+.uk-margin-medium{margin-top:40px!important}.uk-margin-medium-top{margin-top:40px!important}.uk-margin-medium-bottom{margin-bottom:40px!important}.uk-margin-medium-left{margin-left:40px!important}.uk-margin-medium-right{margin-right:40px!important}.uk-margin-large{margin-bottom:40px}*+.uk-margin-large{margin-top:40px!important}.uk-margin-large-top{margin-top:40px!important}.uk-margin-large-bottom{margin-bottom:40px!important}.uk-margin-large-left{margin-left:40px!important}.uk-margin-large-right{margin-right:40px!important}@media (min-width:1200px){.uk-margin-large{margin-bottom:70px}*+.uk-margin-large{margin-top:70px!important}.uk-margin-large-top{margin-top:70px!important}.uk-margin-large-bottom{margin-bottom:70px!important}.uk-margin-large-left{margin-left:70px!important}.uk-margin-large-right{margin-right:70px!important}}.uk-margin-xlarge{margin-bottom:70px}*+.uk-margin-xlarge{margin-top:70px!important}.uk-margin-xlarge-top{margin-top:70px!important}.uk-margin-xlarge-bottom{margin-bottom:70px!important}.uk-margin-xlarge-left{margin-left:70px!important}.uk-margin-xlarge-right{margin-right:70px!important}@media (min-width:1200px){.uk-margin-xlarge{margin-bottom:140px}*+.uk-margin-xlarge{margin-top:140px!important}.uk-margin-xlarge-top{margin-top:140px!important}.uk-margin-xlarge-bottom{margin-bottom:140px!important}.uk-margin-xlarge-left{margin-left:140px!important}.uk-margin-xlarge-right{margin-right:140px!important}}.uk-margin-auto{margin-left:auto!important;margin-right:auto!important}.uk-margin-auto-top{margin-top:auto!important}.uk-margin-auto-bottom{margin-bottom:auto!important}.uk-margin-auto-left{margin-left:auto!important}.uk-margin-auto-right{margin-right:auto!important}.uk-margin-auto-vertical{margin-top:auto!important;margin-bottom:auto!important}@media (min-width:640px){.uk-margin-auto\@s{margin-left:auto!important;margin-right:auto!important}.uk-margin-auto-left\@s{margin-left:auto!important}.uk-margin-auto-right\@s{margin-right:auto!important}}@media (min-width:960px){.uk-margin-auto\@m{margin-left:auto!important;margin-right:auto!important}.uk-margin-auto-left\@m{margin-left:auto!important}.uk-margin-auto-right\@m{margin-right:auto!important}}@media (min-width:1200px){.uk-margin-auto\@l{margin-left:auto!important;margin-right:auto!important}.uk-margin-auto-left\@l{margin-left:auto!important}.uk-margin-auto-right\@l{margin-right:auto!important}}@media (min-width:1600px){.uk-margin-auto\@xl{margin-left:auto!important;margin-right:auto!important}.uk-margin-auto-left\@xl{margin-left:auto!important}.uk-margin-auto-right\@xl{margin-right:auto!important}}.uk-margin-remove{margin:0!important}.uk-margin-remove-top{margin-top:0!important}.uk-margin-remove-bottom{margin-bottom:0!important}.uk-margin-remove-left{margin-left:0!important}.uk-margin-remove-right{margin-right:0!important}.uk-margin-remove-vertical{margin-top:0!important;margin-bottom:0!important}.uk-margin-remove-adjacent+*,.uk-margin-remove-first-child>:first-child{margin-top:0!important}.uk-margin-remove-last-child>:last-child{margin-bottom:0!important}@media (min-width:640px){.uk-margin-remove-left\@s{margin-left:0!important}.uk-margin-remove-right\@s{margin-right:0!important}}@media (min-width:960px){.uk-margin-remove-left\@m{margin-left:0!important}.uk-margin-remove-right\@m{margin-right:0!important}}@media (min-width:1200px){.uk-margin-remove-left\@l{margin-left:0!important}.uk-margin-remove-right\@l{margin-right:0!important}}@media (min-width:1600px){.uk-margin-remove-left\@xl{margin-left:0!important}.uk-margin-remove-right\@xl{margin-right:0!important}}.uk-padding{padding:30px}@media (min-width:1200px){.uk-padding{padding:40px}}.uk-padding-small{padding:15px}.uk-padding-large{padding:30px}@media (min-width:1200px){.uk-padding-large{padding:70px}}.uk-padding-remove{padding:0!important}.uk-padding-remove-top{padding-top:0!important}.uk-padding-remove-bottom{padding-bottom:0!important}.uk-padding-remove-left{padding-left:0!important}.uk-padding-remove-right{padding-right:0!important}.uk-padding-remove-vertical{padding-top:0!important;padding-bottom:0!important}.uk-padding-remove-horizontal{padding-left:0!important;padding-right:0!important}[class*=uk-position-bottom],[class*=uk-position-center],[class*=uk-position-left],[class*=uk-position-right],[class*=uk-position-top]{position:absolute!important;max-width:100%}.uk-position-top{top:0;left:0;right:0}.uk-position-bottom{bottom:0;left:0;right:0}.uk-position-left{top:0;bottom:0;left:0}.uk-position-right{top:0;bottom:0;right:0}.uk-position-top-left{top:0;left:0}.uk-position-top-right{top:0;right:0}.uk-position-bottom-left{bottom:0;left:0}.uk-position-bottom-right{bottom:0;right:0}.uk-position-center{top:50%;left:50%;transform:translate(-50%,-50%);width:max-content;max-width:100%;box-sizing:border-box}[class*=uk-position-center-left],[class*=uk-position-center-right]{top:50%;transform:translateY(-50%)}.uk-position-center-left{left:0}.uk-position-center-right{right:0}.uk-position-center-left-out{right:100%;width:max-content}.uk-position-center-right-out{left:100%;width:max-content}.uk-position-bottom-center,.uk-position-top-center{left:50%;transform:translateX(-50%);width:max-content;max-width:100%;box-sizing:border-box}.uk-position-top-center{top:0}.uk-position-bottom-center{bottom:0}.uk-position-cover{position:absolute;top:0;bottom:0;left:0;right:0}.uk-position-relative{position:relative!important}.uk-position-absolute{position:absolute!important}.uk-position-fixed{position:fixed!important}.uk-position-z-index{z-index:1}.uk-position-small{max-width:calc(100% - (15px * 2));margin:15px}.uk-position-small.uk-position-center{transform:translate(-50%,-50%) translate(-15px,-15px)}.uk-position-small[class*=uk-position-center-left],.uk-position-small[class*=uk-position-center-right]{transform:translateY(-50%) translateY(-15px)}.uk-position-small.uk-position-bottom-center,.uk-position-small.uk-position-top-center{transform:translateX(-50%) translateX(-15px)}.uk-position-medium{max-width:calc(100% - (30px * 2));margin:30px}.uk-position-medium.uk-position-center{transform:translate(-50%,-50%) translate(-30px,-30px)}.uk-position-medium[class*=uk-position-center-left],.uk-position-medium[class*=uk-position-center-right]{transform:translateY(-50%) translateY(-30px)}.uk-position-medium.uk-position-bottom-center,.uk-position-medium.uk-position-top-center{transform:translateX(-50%) translateX(-30px)}.uk-position-large{max-width:calc(100% - (30px * 2));margin:30px}.uk-position-large.uk-position-center{transform:translate(-50%,-50%) translate(-30px,-30px)}.uk-position-large[class*=uk-position-center-left],.uk-position-large[class*=uk-position-center-right]{transform:translateY(-50%) translateY(-30px)}.uk-position-large.uk-position-bottom-center,.uk-position-large.uk-position-top-center{transform:translateX(-50%) translateX(-30px)}@media (min-width:1200px){.uk-position-large{max-width:calc(100% - (50px * 2));margin:50px}.uk-position-large.uk-position-center{transform:translate(-50%,-50%) translate(-50px,-50px)}.uk-position-large[class*=uk-position-center-left],.uk-position-large[class*=uk-position-center-right]{transform:translateY(-50%) translateY(-50px)}.uk-position-large.uk-position-bottom-center,.uk-position-large.uk-position-top-center{transform:translateX(-50%) translateX(-50px)}}.uk-transition-toggle{-webkit-tap-highlight-color:transparent}.uk-transition-toggle:focus{outline:0}.uk-transition-fade,[class*=uk-transition-scale],[class*=uk-transition-slide]{transition:.3s ease-out;transition-property:opacity,transform,filter;opacity:0}.uk-transition-active.uk-active .uk-transition-fade,.uk-transition-toggle:focus .uk-transition-fade,.uk-transition-toggle:hover .uk-transition-fade{opacity:1}.uk-transition-scale-up{transform:scale(1,1)}.uk-transition-scale-down{transform:scale(1.1,1.1)}.uk-transition-active.uk-active .uk-transition-scale-up,.uk-transition-toggle:focus .uk-transition-scale-up,.uk-transition-toggle:hover .uk-transition-scale-up{opacity:1;transform:scale(1.1,1.1)}.uk-transition-active.uk-active .uk-transition-scale-down,.uk-transition-toggle:focus .uk-transition-scale-down,.uk-transition-toggle:hover .uk-transition-scale-down{opacity:1;transform:scale(1,1)}.uk-transition-slide-top{transform:translateY(-100%)}.uk-transition-slide-bottom{transform:translateY(100%)}.uk-transition-slide-left{transform:translateX(-100%)}.uk-transition-slide-right{transform:translateX(100%)}.uk-transition-slide-top-small{transform:translateY(-10px)}.uk-transition-slide-bottom-small{transform:translateY(10px)}.uk-transition-slide-left-small{transform:translateX(-10px)}.uk-transition-slide-right-small{transform:translateX(10px)}.uk-transition-slide-top-medium{transform:translateY(-50px)}.uk-transition-slide-bottom-medium{transform:translateY(50px)}.uk-transition-slide-left-medium{transform:translateX(-50px)}.uk-transition-slide-right-medium{transform:translateX(50px)}.uk-transition-active.uk-active [class*=uk-transition-slide],.uk-transition-toggle:focus [class*=uk-transition-slide],.uk-transition-toggle:hover [class*=uk-transition-slide]{opacity:1;transform:translate(0,0)}.uk-transition-opaque{opacity:1}.uk-transition-slow{transition-duration:.7s}.uk-hidden,[hidden]{display:none!important}@media (min-width:640px){.uk-hidden\@s{display:none!important}}@media (min-width:960px){.uk-hidden\@m{display:none!important}}@media (min-width:1200px){.uk-hidden\@l{display:none!important}}@media (min-width:1600px){.uk-hidden\@xl{display:none!important}}@media (max-width:639px){.uk-visible\@s{display:none!important}}@media (max-width:959px){.uk-visible\@m{display:none!important}}@media (max-width:1199px){.uk-visible\@l{display:none!important}}@media (max-width:1599px){.uk-visible\@xl{display:none!important}}.uk-invisible{visibility:hidden!important}.uk-visible-toggle:not(:hover):not(:focus) .uk-hidden-hover:not(:focus-within){position:absolute!important;width:0!important;height:0!important;padding:0!important;margin:0!important;overflow:hidden!important}.uk-visible-toggle:not(:hover):not(:focus) .uk-invisible-hover:not(:focus-within){opacity:0!important}.uk-visible-toggle{-webkit-tap-highlight-color:transparent}.uk-visible-toggle:focus{outline:0}@media (pointer:coarse){.uk-hidden-touch{display:none!important}}.uk-hidden-notouch{display:none!important}@media (pointer:coarse){.uk-hidden-notouch{display:block!important}}.uk-card-primary.uk-card-body,.uk-card-primary>:not([class*=uk-card-media]),.uk-card-secondary.uk-card-body,.uk-card-secondary>:not([class*=uk-card-media]),.uk-light,.uk-offcanvas-bar,.uk-overlay-primary,.uk-section-primary:not(.uk-preserve-color),.uk-section-secondary:not(.uk-preserve-color),.uk-tile-primary:not(.uk-preserve-color),.uk-tile-secondary:not(.uk-preserve-color){color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-link,.uk-card-primary.uk-card-body a,.uk-card-primary>:not([class*=uk-card-media]) .uk-link,.uk-card-primary>:not([class*=uk-card-media]) a,.uk-card-secondary.uk-card-body .uk-link,.uk-card-secondary.uk-card-body a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-link,.uk-card-secondary>:not([class*=uk-card-media]) a,.uk-light .uk-link,.uk-light a,.uk-offcanvas-bar .uk-link,.uk-offcanvas-bar a,.uk-overlay-primary .uk-link,.uk-overlay-primary a,.uk-section-primary:not(.uk-preserve-color) .uk-link,.uk-section-primary:not(.uk-preserve-color) a,.uk-section-secondary:not(.uk-preserve-color) .uk-link,.uk-section-secondary:not(.uk-preserve-color) a,.uk-tile-primary:not(.uk-preserve-color) .uk-link,.uk-tile-primary:not(.uk-preserve-color) a,.uk-tile-secondary:not(.uk-preserve-color) .uk-link,.uk-tile-secondary:not(.uk-preserve-color) a{color:#fff}.uk-card-primary.uk-card-body .uk-link:hover,.uk-card-primary.uk-card-body a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-link:hover,.uk-card-primary>:not([class*=uk-card-media]) a:hover,.uk-card-secondary.uk-card-body .uk-link:hover,.uk-card-secondary.uk-card-body a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-link:hover,.uk-card-secondary>:not([class*=uk-card-media]) a:hover,.uk-light .uk-link:hover,.uk-light a:hover,.uk-offcanvas-bar .uk-link:hover,.uk-offcanvas-bar a:hover,.uk-overlay-primary .uk-link:hover,.uk-overlay-primary a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-link:hover,.uk-section-primary:not(.uk-preserve-color) a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-link:hover,.uk-section-secondary:not(.uk-preserve-color) a:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-link:hover,.uk-tile-primary:not(.uk-preserve-color) a:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-link:hover,.uk-tile-secondary:not(.uk-preserve-color) a:hover{color:#fff}.uk-card-primary.uk-card-body :not(pre)>code,.uk-card-primary.uk-card-body :not(pre)>kbd,.uk-card-primary.uk-card-body :not(pre)>samp,.uk-card-primary>:not([class*=uk-card-media]) :not(pre)>code,.uk-card-primary>:not([class*=uk-card-media]) :not(pre)>kbd,.uk-card-primary>:not([class*=uk-card-media]) :not(pre)>samp,.uk-card-secondary.uk-card-body :not(pre)>code,.uk-card-secondary.uk-card-body :not(pre)>kbd,.uk-card-secondary.uk-card-body :not(pre)>samp,.uk-card-secondary>:not([class*=uk-card-media]) :not(pre)>code,.uk-card-secondary>:not([class*=uk-card-media]) :not(pre)>kbd,.uk-card-secondary>:not([class*=uk-card-media]) :not(pre)>samp,.uk-light :not(pre)>code,.uk-light :not(pre)>kbd,.uk-light :not(pre)>samp,.uk-offcanvas-bar :not(pre)>code,.uk-offcanvas-bar :not(pre)>kbd,.uk-offcanvas-bar :not(pre)>samp,.uk-overlay-primary :not(pre)>code,.uk-overlay-primary :not(pre)>kbd,.uk-overlay-primary :not(pre)>samp,.uk-section-primary:not(.uk-preserve-color) :not(pre)>code,.uk-section-primary:not(.uk-preserve-color) :not(pre)>kbd,.uk-section-primary:not(.uk-preserve-color) :not(pre)>samp,.uk-section-secondary:not(.uk-preserve-color) :not(pre)>code,.uk-section-secondary:not(.uk-preserve-color) :not(pre)>kbd,.uk-section-secondary:not(.uk-preserve-color) :not(pre)>samp,.uk-tile-primary:not(.uk-preserve-color) :not(pre)>code,.uk-tile-primary:not(.uk-preserve-color) :not(pre)>kbd,.uk-tile-primary:not(.uk-preserve-color) :not(pre)>samp,.uk-tile-secondary:not(.uk-preserve-color) :not(pre)>code,.uk-tile-secondary:not(.uk-preserve-color) :not(pre)>kbd,.uk-tile-secondary:not(.uk-preserve-color) :not(pre)>samp{color:rgba(255,255,255,.7);background:rgba(255,255,255,.1)}.uk-card-primary.uk-card-body em,.uk-card-primary>:not([class*=uk-card-media]) em,.uk-card-secondary.uk-card-body em,.uk-card-secondary>:not([class*=uk-card-media]) em,.uk-light em,.uk-offcanvas-bar em,.uk-overlay-primary em,.uk-section-primary:not(.uk-preserve-color) em,.uk-section-secondary:not(.uk-preserve-color) em,.uk-tile-primary:not(.uk-preserve-color) em,.uk-tile-secondary:not(.uk-preserve-color) em{color:#fff}.uk-card-primary.uk-card-body .uk-h1,.uk-card-primary.uk-card-body .uk-h2,.uk-card-primary.uk-card-body .uk-h3,.uk-card-primary.uk-card-body .uk-h4,.uk-card-primary.uk-card-body .uk-h5,.uk-card-primary.uk-card-body .uk-h6,.uk-card-primary.uk-card-body .uk-heading-2xlarge,.uk-card-primary.uk-card-body .uk-heading-large,.uk-card-primary.uk-card-body .uk-heading-medium,.uk-card-primary.uk-card-body .uk-heading-small,.uk-card-primary.uk-card-body .uk-heading-xlarge,.uk-card-primary.uk-card-body h1,.uk-card-primary.uk-card-body h2,.uk-card-primary.uk-card-body h3,.uk-card-primary.uk-card-body h4,.uk-card-primary.uk-card-body h5,.uk-card-primary.uk-card-body h6,.uk-card-primary>:not([class*=uk-card-media]) .uk-h1,.uk-card-primary>:not([class*=uk-card-media]) .uk-h2,.uk-card-primary>:not([class*=uk-card-media]) .uk-h3,.uk-card-primary>:not([class*=uk-card-media]) .uk-h4,.uk-card-primary>:not([class*=uk-card-media]) .uk-h5,.uk-card-primary>:not([class*=uk-card-media]) .uk-h6,.uk-card-primary>:not([class*=uk-card-media]) .uk-heading-2xlarge,.uk-card-primary>:not([class*=uk-card-media]) .uk-heading-large,.uk-card-primary>:not([class*=uk-card-media]) .uk-heading-medium,.uk-card-primary>:not([class*=uk-card-media]) .uk-heading-small,.uk-card-primary>:not([class*=uk-card-media]) .uk-heading-xlarge,.uk-card-primary>:not([class*=uk-card-media]) h1,.uk-card-primary>:not([class*=uk-card-media]) h2,.uk-card-primary>:not([class*=uk-card-media]) h3,.uk-card-primary>:not([class*=uk-card-media]) h4,.uk-card-primary>:not([class*=uk-card-media]) h5,.uk-card-primary>:not([class*=uk-card-media]) h6,.uk-card-secondary.uk-card-body .uk-h1,.uk-card-secondary.uk-card-body .uk-h2,.uk-card-secondary.uk-card-body .uk-h3,.uk-card-secondary.uk-card-body .uk-h4,.uk-card-secondary.uk-card-body .uk-h5,.uk-card-secondary.uk-card-body .uk-h6,.uk-card-secondary.uk-card-body .uk-heading-2xlarge,.uk-card-secondary.uk-card-body .uk-heading-large,.uk-card-secondary.uk-card-body .uk-heading-medium,.uk-card-secondary.uk-card-body .uk-heading-small,.uk-card-secondary.uk-card-body .uk-heading-xlarge,.uk-card-secondary.uk-card-body h1,.uk-card-secondary.uk-card-body h2,.uk-card-secondary.uk-card-body h3,.uk-card-secondary.uk-card-body h4,.uk-card-secondary.uk-card-body h5,.uk-card-secondary.uk-card-body h6,.uk-card-secondary>:not([class*=uk-card-media]) .uk-h1,.uk-card-secondary>:not([class*=uk-card-media]) .uk-h2,.uk-card-secondary>:not([class*=uk-card-media]) .uk-h3,.uk-card-secondary>:not([class*=uk-card-media]) .uk-h4,.uk-card-secondary>:not([class*=uk-card-media]) .uk-h5,.uk-card-secondary>:not([class*=uk-card-media]) .uk-h6,.uk-card-secondary>:not([class*=uk-card-media]) .uk-heading-2xlarge,.uk-card-secondary>:not([class*=uk-card-media]) .uk-heading-large,.uk-card-secondary>:not([class*=uk-card-media]) .uk-heading-medium,.uk-card-secondary>:not([class*=uk-card-media]) .uk-heading-small,.uk-card-secondary>:not([class*=uk-card-media]) .uk-heading-xlarge,.uk-card-secondary>:not([class*=uk-card-media]) h1,.uk-card-secondary>:not([class*=uk-card-media]) h2,.uk-card-secondary>:not([class*=uk-card-media]) h3,.uk-card-secondary>:not([class*=uk-card-media]) h4,.uk-card-secondary>:not([class*=uk-card-media]) h5,.uk-card-secondary>:not([class*=uk-card-media]) h6,.uk-light .uk-h1,.uk-light .uk-h2,.uk-light .uk-h3,.uk-light .uk-h4,.uk-light .uk-h5,.uk-light .uk-h6,.uk-light .uk-heading-2xlarge,.uk-light .uk-heading-large,.uk-light .uk-heading-medium,.uk-light .uk-heading-small,.uk-light .uk-heading-xlarge,.uk-light h1,.uk-light h2,.uk-light h3,.uk-light h4,.uk-light h5,.uk-light h6,.uk-offcanvas-bar .uk-h1,.uk-offcanvas-bar .uk-h2,.uk-offcanvas-bar .uk-h3,.uk-offcanvas-bar .uk-h4,.uk-offcanvas-bar .uk-h5,.uk-offcanvas-bar .uk-h6,.uk-offcanvas-bar .uk-heading-2xlarge,.uk-offcanvas-bar .uk-heading-large,.uk-offcanvas-bar .uk-heading-medium,.uk-offcanvas-bar .uk-heading-small,.uk-offcanvas-bar .uk-heading-xlarge,.uk-offcanvas-bar h1,.uk-offcanvas-bar h2,.uk-offcanvas-bar h3,.uk-offcanvas-bar h4,.uk-offcanvas-bar h5,.uk-offcanvas-bar h6,.uk-overlay-primary .uk-h1,.uk-overlay-primary .uk-h2,.uk-overlay-primary .uk-h3,.uk-overlay-primary .uk-h4,.uk-overlay-primary .uk-h5,.uk-overlay-primary .uk-h6,.uk-overlay-primary .uk-heading-2xlarge,.uk-overlay-primary .uk-heading-large,.uk-overlay-primary .uk-heading-medium,.uk-overlay-primary .uk-heading-small,.uk-overlay-primary .uk-heading-xlarge,.uk-overlay-primary h1,.uk-overlay-primary h2,.uk-overlay-primary h3,.uk-overlay-primary h4,.uk-overlay-primary h5,.uk-overlay-primary h6,.uk-section-primary:not(.uk-preserve-color) .uk-h1,.uk-section-primary:not(.uk-preserve-color) .uk-h2,.uk-section-primary:not(.uk-preserve-color) .uk-h3,.uk-section-primary:not(.uk-preserve-color) .uk-h4,.uk-section-primary:not(.uk-preserve-color) .uk-h5,.uk-section-primary:not(.uk-preserve-color) .uk-h6,.uk-section-primary:not(.uk-preserve-color) .uk-heading-2xlarge,.uk-section-primary:not(.uk-preserve-color) .uk-heading-large,.uk-section-primary:not(.uk-preserve-color) .uk-heading-medium,.uk-section-primary:not(.uk-preserve-color) .uk-heading-small,.uk-section-primary:not(.uk-preserve-color) .uk-heading-xlarge,.uk-section-primary:not(.uk-preserve-color) h1,.uk-section-primary:not(.uk-preserve-color) h2,.uk-section-primary:not(.uk-preserve-color) h3,.uk-section-primary:not(.uk-preserve-color) h4,.uk-section-primary:not(.uk-preserve-color) h5,.uk-section-primary:not(.uk-preserve-color) h6,.uk-section-secondary:not(.uk-preserve-color) .uk-h1,.uk-section-secondary:not(.uk-preserve-color) .uk-h2,.uk-section-secondary:not(.uk-preserve-color) .uk-h3,.uk-section-secondary:not(.uk-preserve-color) .uk-h4,.uk-section-secondary:not(.uk-preserve-color) .uk-h5,.uk-section-secondary:not(.uk-preserve-color) .uk-h6,.uk-section-secondary:not(.uk-preserve-color) .uk-heading-2xlarge,.uk-section-secondary:not(.uk-preserve-color) .uk-heading-large,.uk-section-secondary:not(.uk-preserve-color) .uk-heading-medium,.uk-section-secondary:not(.uk-preserve-color) .uk-heading-small,.uk-section-secondary:not(.uk-preserve-color) .uk-heading-xlarge,.uk-section-secondary:not(.uk-preserve-color) h1,.uk-section-secondary:not(.uk-preserve-color) h2,.uk-section-secondary:not(.uk-preserve-color) h3,.uk-section-secondary:not(.uk-preserve-color) h4,.uk-section-secondary:not(.uk-preserve-color) h5,.uk-section-secondary:not(.uk-preserve-color) h6,.uk-tile-primary:not(.uk-preserve-color) .uk-h1,.uk-tile-primary:not(.uk-preserve-color) .uk-h2,.uk-tile-primary:not(.uk-preserve-color) .uk-h3,.uk-tile-primary:not(.uk-preserve-color) .uk-h4,.uk-tile-primary:not(.uk-preserve-color) .uk-h5,.uk-tile-primary:not(.uk-preserve-color) .uk-h6,.uk-tile-primary:not(.uk-preserve-color) .uk-heading-2xlarge,.uk-tile-primary:not(.uk-preserve-color) .uk-heading-large,.uk-tile-primary:not(.uk-preserve-color) .uk-heading-medium,.uk-tile-primary:not(.uk-preserve-color) .uk-heading-small,.uk-tile-primary:not(.uk-preserve-color) .uk-heading-xlarge,.uk-tile-primary:not(.uk-preserve-color) h1,.uk-tile-primary:not(.uk-preserve-color) h2,.uk-tile-primary:not(.uk-preserve-color) h3,.uk-tile-primary:not(.uk-preserve-color) h4,.uk-tile-primary:not(.uk-preserve-color) h5,.uk-tile-primary:not(.uk-preserve-color) h6,.uk-tile-secondary:not(.uk-preserve-color) .uk-h1,.uk-tile-secondary:not(.uk-preserve-color) .uk-h2,.uk-tile-secondary:not(.uk-preserve-color) .uk-h3,.uk-tile-secondary:not(.uk-preserve-color) .uk-h4,.uk-tile-secondary:not(.uk-preserve-color) .uk-h5,.uk-tile-secondary:not(.uk-preserve-color) .uk-h6,.uk-tile-secondary:not(.uk-preserve-color) .uk-heading-2xlarge,.uk-tile-secondary:not(.uk-preserve-color) .uk-heading-large,.uk-tile-secondary:not(.uk-preserve-color) .uk-heading-medium,.uk-tile-secondary:not(.uk-preserve-color) .uk-heading-small,.uk-tile-secondary:not(.uk-preserve-color) .uk-heading-xlarge,.uk-tile-secondary:not(.uk-preserve-color) h1,.uk-tile-secondary:not(.uk-preserve-color) h2,.uk-tile-secondary:not(.uk-preserve-color) h3,.uk-tile-secondary:not(.uk-preserve-color) h4,.uk-tile-secondary:not(.uk-preserve-color) h5,.uk-tile-secondary:not(.uk-preserve-color) h6{color:#fff}.uk-card-primary.uk-card-body blockquote,.uk-card-primary>:not([class*=uk-card-media]) blockquote,.uk-card-secondary.uk-card-body blockquote,.uk-card-secondary>:not([class*=uk-card-media]) blockquote,.uk-light blockquote,.uk-offcanvas-bar blockquote,.uk-overlay-primary blockquote,.uk-section-primary:not(.uk-preserve-color) blockquote,.uk-section-secondary:not(.uk-preserve-color) blockquote,.uk-tile-primary:not(.uk-preserve-color) blockquote,.uk-tile-secondary:not(.uk-preserve-color) blockquote{color:#fff}.uk-card-primary.uk-card-body blockquote footer,.uk-card-primary>:not([class*=uk-card-media]) blockquote footer,.uk-card-secondary.uk-card-body blockquote footer,.uk-card-secondary>:not([class*=uk-card-media]) blockquote footer,.uk-light blockquote footer,.uk-offcanvas-bar blockquote footer,.uk-overlay-primary blockquote footer,.uk-section-primary:not(.uk-preserve-color) blockquote footer,.uk-section-secondary:not(.uk-preserve-color) blockquote footer,.uk-tile-primary:not(.uk-preserve-color) blockquote footer,.uk-tile-secondary:not(.uk-preserve-color) blockquote footer{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-hr,.uk-card-primary.uk-card-body hr,.uk-card-primary>:not([class*=uk-card-media]) .uk-hr,.uk-card-primary>:not([class*=uk-card-media]) hr,.uk-card-secondary.uk-card-body .uk-hr,.uk-card-secondary.uk-card-body hr,.uk-card-secondary>:not([class*=uk-card-media]) .uk-hr,.uk-card-secondary>:not([class*=uk-card-media]) hr,.uk-light .uk-hr,.uk-light hr,.uk-offcanvas-bar .uk-hr,.uk-offcanvas-bar hr,.uk-overlay-primary .uk-hr,.uk-overlay-primary hr,.uk-section-primary:not(.uk-preserve-color) .uk-hr,.uk-section-primary:not(.uk-preserve-color) hr,.uk-section-secondary:not(.uk-preserve-color) .uk-hr,.uk-section-secondary:not(.uk-preserve-color) hr,.uk-tile-primary:not(.uk-preserve-color) .uk-hr,.uk-tile-primary:not(.uk-preserve-color) hr,.uk-tile-secondary:not(.uk-preserve-color) .uk-hr,.uk-tile-secondary:not(.uk-preserve-color) hr{border-top-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-link-muted,.uk-card-primary.uk-card-body .uk-link-muted a,.uk-card-primary>:not([class*=uk-card-media]) .uk-link-muted,.uk-card-primary>:not([class*=uk-card-media]) .uk-link-muted a,.uk-card-secondary.uk-card-body .uk-link-muted,.uk-card-secondary.uk-card-body .uk-link-muted a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-link-muted,.uk-card-secondary>:not([class*=uk-card-media]) .uk-link-muted a,.uk-light .uk-link-muted,.uk-light .uk-link-muted a,.uk-offcanvas-bar .uk-link-muted,.uk-offcanvas-bar .uk-link-muted a,.uk-overlay-primary .uk-link-muted,.uk-overlay-primary .uk-link-muted a,.uk-section-primary:not(.uk-preserve-color) .uk-link-muted,.uk-section-primary:not(.uk-preserve-color) .uk-link-muted a,.uk-section-secondary:not(.uk-preserve-color) .uk-link-muted,.uk-section-secondary:not(.uk-preserve-color) .uk-link-muted a,.uk-tile-primary:not(.uk-preserve-color) .uk-link-muted,.uk-tile-primary:not(.uk-preserve-color) .uk-link-muted a,.uk-tile-secondary:not(.uk-preserve-color) .uk-link-muted,.uk-tile-secondary:not(.uk-preserve-color) .uk-link-muted a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-link-muted a:hover,.uk-card-primary.uk-card-body .uk-link-muted:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-link-muted a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-link-muted:hover,.uk-card-secondary.uk-card-body .uk-link-muted a:hover,.uk-card-secondary.uk-card-body .uk-link-muted:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-link-muted a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-link-muted:hover,.uk-light .uk-link-muted a:hover,.uk-light .uk-link-muted:hover,.uk-offcanvas-bar .uk-link-muted a:hover,.uk-offcanvas-bar .uk-link-muted:hover,.uk-overlay-primary .uk-link-muted a:hover,.uk-overlay-primary .uk-link-muted:hover,.uk-section-primary:not(.uk-preserve-color) .uk-link-muted a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-link-muted:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-link-muted a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-link-muted:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-link-muted a:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-link-muted:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-link-muted a:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-link-muted:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-link-text a:hover,.uk-card-primary.uk-card-body .uk-link-text:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-link-text a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-link-text:hover,.uk-card-secondary.uk-card-body .uk-link-text a:hover,.uk-card-secondary.uk-card-body .uk-link-text:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-link-text a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-link-text:hover,.uk-light .uk-link-text a:hover,.uk-light .uk-link-text:hover,.uk-offcanvas-bar .uk-link-text a:hover,.uk-offcanvas-bar .uk-link-text:hover,.uk-overlay-primary .uk-link-text a:hover,.uk-overlay-primary .uk-link-text:hover,.uk-section-primary:not(.uk-preserve-color) .uk-link-text a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-link-text:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-link-text a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-link-text:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-link-text a:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-link-text:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-link-text a:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-link-text:hover{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-link-heading a:hover,.uk-card-primary.uk-card-body .uk-link-heading:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-link-heading a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-link-heading:hover,.uk-card-secondary.uk-card-body .uk-link-heading a:hover,.uk-card-secondary.uk-card-body .uk-link-heading:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-link-heading a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-link-heading:hover,.uk-light .uk-link-heading a:hover,.uk-light .uk-link-heading:hover,.uk-offcanvas-bar .uk-link-heading a:hover,.uk-offcanvas-bar .uk-link-heading:hover,.uk-overlay-primary .uk-link-heading a:hover,.uk-overlay-primary .uk-link-heading:hover,.uk-section-primary:not(.uk-preserve-color) .uk-link-heading a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-link-heading:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-link-heading a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-link-heading:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-link-heading a:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-link-heading:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-link-heading a:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-link-heading:hover{color:#fff}.uk-card-primary.uk-card-body .uk-heading-divider,.uk-card-primary>:not([class*=uk-card-media]) .uk-heading-divider,.uk-card-secondary.uk-card-body .uk-heading-divider,.uk-card-secondary>:not([class*=uk-card-media]) .uk-heading-divider,.uk-light .uk-heading-divider,.uk-offcanvas-bar .uk-heading-divider,.uk-overlay-primary .uk-heading-divider,.uk-section-primary:not(.uk-preserve-color) .uk-heading-divider,.uk-section-secondary:not(.uk-preserve-color) .uk-heading-divider,.uk-tile-primary:not(.uk-preserve-color) .uk-heading-divider,.uk-tile-secondary:not(.uk-preserve-color) .uk-heading-divider{border-bottom-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-heading-bullet::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-heading-bullet::before,.uk-card-secondary.uk-card-body .uk-heading-bullet::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-heading-bullet::before,.uk-light .uk-heading-bullet::before,.uk-offcanvas-bar .uk-heading-bullet::before,.uk-overlay-primary .uk-heading-bullet::before,.uk-section-primary:not(.uk-preserve-color) .uk-heading-bullet::before,.uk-section-secondary:not(.uk-preserve-color) .uk-heading-bullet::before,.uk-tile-primary:not(.uk-preserve-color) .uk-heading-bullet::before,.uk-tile-secondary:not(.uk-preserve-color) .uk-heading-bullet::before{border-left-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-heading-line>::after,.uk-card-primary.uk-card-body .uk-heading-line>::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-heading-line>::after,.uk-card-primary>:not([class*=uk-card-media]) .uk-heading-line>::before,.uk-card-secondary.uk-card-body .uk-heading-line>::after,.uk-card-secondary.uk-card-body .uk-heading-line>::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-heading-line>::after,.uk-card-secondary>:not([class*=uk-card-media]) .uk-heading-line>::before,.uk-light .uk-heading-line>::after,.uk-light .uk-heading-line>::before,.uk-offcanvas-bar .uk-heading-line>::after,.uk-offcanvas-bar .uk-heading-line>::before,.uk-overlay-primary .uk-heading-line>::after,.uk-overlay-primary .uk-heading-line>::before,.uk-section-primary:not(.uk-preserve-color) .uk-heading-line>::after,.uk-section-primary:not(.uk-preserve-color) .uk-heading-line>::before,.uk-section-secondary:not(.uk-preserve-color) .uk-heading-line>::after,.uk-section-secondary:not(.uk-preserve-color) .uk-heading-line>::before,.uk-tile-primary:not(.uk-preserve-color) .uk-heading-line>::after,.uk-tile-primary:not(.uk-preserve-color) .uk-heading-line>::before,.uk-tile-secondary:not(.uk-preserve-color) .uk-heading-line>::after,.uk-tile-secondary:not(.uk-preserve-color) .uk-heading-line>::before{border-bottom-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-divider-icon,.uk-card-primary>:not([class*=uk-card-media]) .uk-divider-icon,.uk-card-secondary.uk-card-body .uk-divider-icon,.uk-card-secondary>:not([class*=uk-card-media]) .uk-divider-icon,.uk-light .uk-divider-icon,.uk-offcanvas-bar .uk-divider-icon,.uk-overlay-primary .uk-divider-icon,.uk-section-primary:not(.uk-preserve-color) .uk-divider-icon,.uk-section-secondary:not(.uk-preserve-color) .uk-divider-icon,.uk-tile-primary:not(.uk-preserve-color) .uk-divider-icon,.uk-tile-secondary:not(.uk-preserve-color) .uk-divider-icon{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20viewBox%3D%220%200%2020%2020%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Ccircle%20fill%3D%22none%22%20stroke%3D%22rgba%28255,%20255,%20255,%200.2%29%22%20stroke-width%3D%222%22%20cx%3D%2210%22%20cy%3D%2210%22%20r%3D%227%22%20%2F%3E%0A%3C%2Fsvg%3E%0A")}.uk-card-primary.uk-card-body .uk-divider-icon::after,.uk-card-primary.uk-card-body .uk-divider-icon::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-divider-icon::after,.uk-card-primary>:not([class*=uk-card-media]) .uk-divider-icon::before,.uk-card-secondary.uk-card-body .uk-divider-icon::after,.uk-card-secondary.uk-card-body .uk-divider-icon::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-divider-icon::after,.uk-card-secondary>:not([class*=uk-card-media]) .uk-divider-icon::before,.uk-light .uk-divider-icon::after,.uk-light .uk-divider-icon::before,.uk-offcanvas-bar .uk-divider-icon::after,.uk-offcanvas-bar .uk-divider-icon::before,.uk-overlay-primary .uk-divider-icon::after,.uk-overlay-primary .uk-divider-icon::before,.uk-section-primary:not(.uk-preserve-color) .uk-divider-icon::after,.uk-section-primary:not(.uk-preserve-color) .uk-divider-icon::before,.uk-section-secondary:not(.uk-preserve-color) .uk-divider-icon::after,.uk-section-secondary:not(.uk-preserve-color) .uk-divider-icon::before,.uk-tile-primary:not(.uk-preserve-color) .uk-divider-icon::after,.uk-tile-primary:not(.uk-preserve-color) .uk-divider-icon::before,.uk-tile-secondary:not(.uk-preserve-color) .uk-divider-icon::after,.uk-tile-secondary:not(.uk-preserve-color) .uk-divider-icon::before{border-bottom-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-divider-small::after,.uk-card-primary>:not([class*=uk-card-media]) .uk-divider-small::after,.uk-card-secondary.uk-card-body .uk-divider-small::after,.uk-card-secondary>:not([class*=uk-card-media]) .uk-divider-small::after,.uk-light .uk-divider-small::after,.uk-offcanvas-bar .uk-divider-small::after,.uk-overlay-primary .uk-divider-small::after,.uk-section-primary:not(.uk-preserve-color) .uk-divider-small::after,.uk-section-secondary:not(.uk-preserve-color) .uk-divider-small::after,.uk-tile-primary:not(.uk-preserve-color) .uk-divider-small::after,.uk-tile-secondary:not(.uk-preserve-color) .uk-divider-small::after{border-top-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-divider-vertical,.uk-card-primary>:not([class*=uk-card-media]) .uk-divider-vertical,.uk-card-secondary.uk-card-body .uk-divider-vertical,.uk-card-secondary>:not([class*=uk-card-media]) .uk-divider-vertical,.uk-light .uk-divider-vertical,.uk-offcanvas-bar .uk-divider-vertical,.uk-overlay-primary .uk-divider-vertical,.uk-section-primary:not(.uk-preserve-color) .uk-divider-vertical,.uk-section-secondary:not(.uk-preserve-color) .uk-divider-vertical,.uk-tile-primary:not(.uk-preserve-color) .uk-divider-vertical,.uk-tile-secondary:not(.uk-preserve-color) .uk-divider-vertical{border-left-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-list-divider>li:nth-child(n+2),.uk-card-primary>:not([class*=uk-card-media]) .uk-list-divider>li:nth-child(n+2),.uk-card-secondary.uk-card-body .uk-list-divider>li:nth-child(n+2),.uk-card-secondary>:not([class*=uk-card-media]) .uk-list-divider>li:nth-child(n+2),.uk-light .uk-list-divider>li:nth-child(n+2),.uk-offcanvas-bar .uk-list-divider>li:nth-child(n+2),.uk-overlay-primary .uk-list-divider>li:nth-child(n+2),.uk-section-primary:not(.uk-preserve-color) .uk-list-divider>li:nth-child(n+2),.uk-section-secondary:not(.uk-preserve-color) .uk-list-divider>li:nth-child(n+2),.uk-tile-primary:not(.uk-preserve-color) .uk-list-divider>li:nth-child(n+2),.uk-tile-secondary:not(.uk-preserve-color) .uk-list-divider>li:nth-child(n+2){border-top-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-list-striped>li:nth-of-type(odd),.uk-card-primary>:not([class*=uk-card-media]) .uk-list-striped>li:nth-of-type(odd),.uk-card-secondary.uk-card-body .uk-list-striped>li:nth-of-type(odd),.uk-card-secondary>:not([class*=uk-card-media]) .uk-list-striped>li:nth-of-type(odd),.uk-light .uk-list-striped>li:nth-of-type(odd),.uk-offcanvas-bar .uk-list-striped>li:nth-of-type(odd),.uk-overlay-primary .uk-list-striped>li:nth-of-type(odd),.uk-section-primary:not(.uk-preserve-color) .uk-list-striped>li:nth-of-type(odd),.uk-section-secondary:not(.uk-preserve-color) .uk-list-striped>li:nth-of-type(odd),.uk-tile-primary:not(.uk-preserve-color) .uk-list-striped>li:nth-of-type(odd),.uk-tile-secondary:not(.uk-preserve-color) .uk-list-striped>li:nth-of-type(odd){border-top-color:rgba(255,255,255,.2);border-bottom-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-list-striped>li:nth-of-type(odd),.uk-card-primary>:not([class*=uk-card-media]) .uk-list-striped>li:nth-of-type(odd),.uk-card-secondary.uk-card-body .uk-list-striped>li:nth-of-type(odd),.uk-card-secondary>:not([class*=uk-card-media]) .uk-list-striped>li:nth-of-type(odd),.uk-light .uk-list-striped>li:nth-of-type(odd),.uk-offcanvas-bar .uk-list-striped>li:nth-of-type(odd),.uk-overlay-primary .uk-list-striped>li:nth-of-type(odd),.uk-section-primary:not(.uk-preserve-color) .uk-list-striped>li:nth-of-type(odd),.uk-section-secondary:not(.uk-preserve-color) .uk-list-striped>li:nth-of-type(odd),.uk-tile-primary:not(.uk-preserve-color) .uk-list-striped>li:nth-of-type(odd),.uk-tile-secondary:not(.uk-preserve-color) .uk-list-striped>li:nth-of-type(odd){background-color:rgba(255,255,255,.1)}.uk-card-primary.uk-card-body .uk-list-bullet>li::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-list-bullet>li::before,.uk-card-secondary.uk-card-body .uk-list-bullet>li::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-list-bullet>li::before,.uk-light .uk-list-bullet>li::before,.uk-offcanvas-bar .uk-list-bullet>li::before,.uk-overlay-primary .uk-list-bullet>li::before,.uk-section-primary:not(.uk-preserve-color) .uk-list-bullet>li::before,.uk-section-secondary:not(.uk-preserve-color) .uk-list-bullet>li::before,.uk-tile-primary:not(.uk-preserve-color) .uk-list-bullet>li::before,.uk-tile-secondary:not(.uk-preserve-color) .uk-list-bullet>li::before{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%226%22%20height%3D%226%22%20viewBox%3D%220%200%206%206%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Ccircle%20fill%3D%22rgba%28255,%20255,%20255,%200.7%29%22%20cx%3D%223%22%20cy%3D%223%22%20r%3D%223%22%20%2F%3E%0A%3C%2Fsvg%3E")}.uk-card-primary.uk-card-body .uk-table th,.uk-card-primary>:not([class*=uk-card-media]) .uk-table th,.uk-card-secondary.uk-card-body .uk-table th,.uk-card-secondary>:not([class*=uk-card-media]) .uk-table th,.uk-light .uk-table th,.uk-offcanvas-bar .uk-table th,.uk-overlay-primary .uk-table th,.uk-section-primary:not(.uk-preserve-color) .uk-table th,.uk-section-secondary:not(.uk-preserve-color) .uk-table th,.uk-tile-primary:not(.uk-preserve-color) .uk-table th,.uk-tile-secondary:not(.uk-preserve-color) .uk-table th{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-table caption,.uk-card-primary>:not([class*=uk-card-media]) .uk-table caption,.uk-card-secondary.uk-card-body .uk-table caption,.uk-card-secondary>:not([class*=uk-card-media]) .uk-table caption,.uk-light .uk-table caption,.uk-offcanvas-bar .uk-table caption,.uk-overlay-primary .uk-table caption,.uk-section-primary:not(.uk-preserve-color) .uk-table caption,.uk-section-secondary:not(.uk-preserve-color) .uk-table caption,.uk-tile-primary:not(.uk-preserve-color) .uk-table caption,.uk-tile-secondary:not(.uk-preserve-color) .uk-table caption{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-table tbody tr.uk-active,.uk-card-primary.uk-card-body .uk-table>tr.uk-active,.uk-card-primary>:not([class*=uk-card-media]) .uk-table tbody tr.uk-active,.uk-card-primary>:not([class*=uk-card-media]) .uk-table>tr.uk-active,.uk-card-secondary.uk-card-body .uk-table tbody tr.uk-active,.uk-card-secondary.uk-card-body .uk-table>tr.uk-active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-table tbody tr.uk-active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-table>tr.uk-active,.uk-light .uk-table tbody tr.uk-active,.uk-light .uk-table>tr.uk-active,.uk-offcanvas-bar .uk-table tbody tr.uk-active,.uk-offcanvas-bar .uk-table>tr.uk-active,.uk-overlay-primary .uk-table tbody tr.uk-active,.uk-overlay-primary .uk-table>tr.uk-active,.uk-section-primary:not(.uk-preserve-color) .uk-table tbody tr.uk-active,.uk-section-primary:not(.uk-preserve-color) .uk-table>tr.uk-active,.uk-section-secondary:not(.uk-preserve-color) .uk-table tbody tr.uk-active,.uk-section-secondary:not(.uk-preserve-color) .uk-table>tr.uk-active,.uk-tile-primary:not(.uk-preserve-color) .uk-table tbody tr.uk-active,.uk-tile-primary:not(.uk-preserve-color) .uk-table>tr.uk-active,.uk-tile-secondary:not(.uk-preserve-color) .uk-table tbody tr.uk-active,.uk-tile-secondary:not(.uk-preserve-color) .uk-table>tr.uk-active{background:rgba(255,255,255,.08)}.uk-card-primary.uk-card-body .uk-table-divider>:first-child>tr:not(:first-child),.uk-card-primary.uk-card-body .uk-table-divider>:not(:first-child)>tr,.uk-card-primary.uk-card-body .uk-table-divider>tr:not(:first-child),.uk-card-primary>:not([class*=uk-card-media]) .uk-table-divider>:first-child>tr:not(:first-child),.uk-card-primary>:not([class*=uk-card-media]) .uk-table-divider>:not(:first-child)>tr,.uk-card-primary>:not([class*=uk-card-media]) .uk-table-divider>tr:not(:first-child),.uk-card-secondary.uk-card-body .uk-table-divider>:first-child>tr:not(:first-child),.uk-card-secondary.uk-card-body .uk-table-divider>:not(:first-child)>tr,.uk-card-secondary.uk-card-body .uk-table-divider>tr:not(:first-child),.uk-card-secondary>:not([class*=uk-card-media]) .uk-table-divider>:first-child>tr:not(:first-child),.uk-card-secondary>:not([class*=uk-card-media]) .uk-table-divider>:not(:first-child)>tr,.uk-card-secondary>:not([class*=uk-card-media]) .uk-table-divider>tr:not(:first-child),.uk-light .uk-table-divider>:first-child>tr:not(:first-child),.uk-light .uk-table-divider>:not(:first-child)>tr,.uk-light .uk-table-divider>tr:not(:first-child),.uk-offcanvas-bar .uk-table-divider>:first-child>tr:not(:first-child),.uk-offcanvas-bar .uk-table-divider>:not(:first-child)>tr,.uk-offcanvas-bar .uk-table-divider>tr:not(:first-child),.uk-overlay-primary .uk-table-divider>:first-child>tr:not(:first-child),.uk-overlay-primary .uk-table-divider>:not(:first-child)>tr,.uk-overlay-primary .uk-table-divider>tr:not(:first-child),.uk-section-primary:not(.uk-preserve-color) .uk-table-divider>:first-child>tr:not(:first-child),.uk-section-primary:not(.uk-preserve-color) .uk-table-divider>:not(:first-child)>tr,.uk-section-primary:not(.uk-preserve-color) .uk-table-divider>tr:not(:first-child),.uk-section-secondary:not(.uk-preserve-color) .uk-table-divider>:first-child>tr:not(:first-child),.uk-section-secondary:not(.uk-preserve-color) .uk-table-divider>:not(:first-child)>tr,.uk-section-secondary:not(.uk-preserve-color) .uk-table-divider>tr:not(:first-child),.uk-tile-primary:not(.uk-preserve-color) .uk-table-divider>:first-child>tr:not(:first-child),.uk-tile-primary:not(.uk-preserve-color) .uk-table-divider>:not(:first-child)>tr,.uk-tile-primary:not(.uk-preserve-color) .uk-table-divider>tr:not(:first-child),.uk-tile-secondary:not(.uk-preserve-color) .uk-table-divider>:first-child>tr:not(:first-child),.uk-tile-secondary:not(.uk-preserve-color) .uk-table-divider>:not(:first-child)>tr,.uk-tile-secondary:not(.uk-preserve-color) .uk-table-divider>tr:not(:first-child){border-top-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-table-striped tbody tr:nth-of-type(odd),.uk-card-primary.uk-card-body .uk-table-striped>tr:nth-of-type(odd),.uk-card-primary>:not([class*=uk-card-media]) .uk-table-striped tbody tr:nth-of-type(odd),.uk-card-primary>:not([class*=uk-card-media]) .uk-table-striped>tr:nth-of-type(odd),.uk-card-secondary.uk-card-body .uk-table-striped tbody tr:nth-of-type(odd),.uk-card-secondary.uk-card-body .uk-table-striped>tr:nth-of-type(odd),.uk-card-secondary>:not([class*=uk-card-media]) .uk-table-striped tbody tr:nth-of-type(odd),.uk-card-secondary>:not([class*=uk-card-media]) .uk-table-striped>tr:nth-of-type(odd),.uk-light .uk-table-striped tbody tr:nth-of-type(odd),.uk-light .uk-table-striped>tr:nth-of-type(odd),.uk-offcanvas-bar .uk-table-striped tbody tr:nth-of-type(odd),.uk-offcanvas-bar .uk-table-striped>tr:nth-of-type(odd),.uk-overlay-primary .uk-table-striped tbody tr:nth-of-type(odd),.uk-overlay-primary .uk-table-striped>tr:nth-of-type(odd),.uk-section-primary:not(.uk-preserve-color) .uk-table-striped tbody tr:nth-of-type(odd),.uk-section-primary:not(.uk-preserve-color) .uk-table-striped>tr:nth-of-type(odd),.uk-section-secondary:not(.uk-preserve-color) .uk-table-striped tbody tr:nth-of-type(odd),.uk-section-secondary:not(.uk-preserve-color) .uk-table-striped>tr:nth-of-type(odd),.uk-tile-primary:not(.uk-preserve-color) .uk-table-striped tbody tr:nth-of-type(odd),.uk-tile-primary:not(.uk-preserve-color) .uk-table-striped>tr:nth-of-type(odd),.uk-tile-secondary:not(.uk-preserve-color) .uk-table-striped tbody tr:nth-of-type(odd),.uk-tile-secondary:not(.uk-preserve-color) .uk-table-striped>tr:nth-of-type(odd){background:rgba(255,255,255,.1);border-top-color:rgba(255,255,255,.2);border-bottom-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-table-hover tbody tr:hover,.uk-card-primary.uk-card-body .uk-table-hover>tr:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-table-hover tbody tr:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-table-hover>tr:hover,.uk-card-secondary.uk-card-body .uk-table-hover tbody tr:hover,.uk-card-secondary.uk-card-body .uk-table-hover>tr:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-table-hover tbody tr:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-table-hover>tr:hover,.uk-light .uk-table-hover tbody tr:hover,.uk-light .uk-table-hover>tr:hover,.uk-offcanvas-bar .uk-table-hover tbody tr:hover,.uk-offcanvas-bar .uk-table-hover>tr:hover,.uk-overlay-primary .uk-table-hover tbody tr:hover,.uk-overlay-primary .uk-table-hover>tr:hover,.uk-section-primary:not(.uk-preserve-color) .uk-table-hover tbody tr:hover,.uk-section-primary:not(.uk-preserve-color) .uk-table-hover>tr:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-table-hover tbody tr:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-table-hover>tr:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-table-hover tbody tr:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-table-hover>tr:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-table-hover tbody tr:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-table-hover>tr:hover{background:rgba(255,255,255,.08)}.uk-card-primary.uk-card-body .uk-icon-link,.uk-card-primary>:not([class*=uk-card-media]) .uk-icon-link,.uk-card-secondary.uk-card-body .uk-icon-link,.uk-card-secondary>:not([class*=uk-card-media]) .uk-icon-link,.uk-light .uk-icon-link,.uk-offcanvas-bar .uk-icon-link,.uk-overlay-primary .uk-icon-link,.uk-section-primary:not(.uk-preserve-color) .uk-icon-link,.uk-section-secondary:not(.uk-preserve-color) .uk-icon-link,.uk-tile-primary:not(.uk-preserve-color) .uk-icon-link,.uk-tile-secondary:not(.uk-preserve-color) .uk-icon-link{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-icon-link:focus,.uk-card-primary.uk-card-body .uk-icon-link:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-icon-link:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-icon-link:hover,.uk-card-secondary.uk-card-body .uk-icon-link:focus,.uk-card-secondary.uk-card-body .uk-icon-link:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-icon-link:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-icon-link:hover,.uk-light .uk-icon-link:focus,.uk-light .uk-icon-link:hover,.uk-offcanvas-bar .uk-icon-link:focus,.uk-offcanvas-bar .uk-icon-link:hover,.uk-overlay-primary .uk-icon-link:focus,.uk-overlay-primary .uk-icon-link:hover,.uk-section-primary:not(.uk-preserve-color) .uk-icon-link:focus,.uk-section-primary:not(.uk-preserve-color) .uk-icon-link:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-icon-link:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-icon-link:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-icon-link:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-icon-link:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-icon-link:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-icon-link:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-active>.uk-icon-link,.uk-card-primary.uk-card-body .uk-icon-link:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-active>.uk-icon-link,.uk-card-primary>:not([class*=uk-card-media]) .uk-icon-link:active,.uk-card-secondary.uk-card-body .uk-active>.uk-icon-link,.uk-card-secondary.uk-card-body .uk-icon-link:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-active>.uk-icon-link,.uk-card-secondary>:not([class*=uk-card-media]) .uk-icon-link:active,.uk-light .uk-active>.uk-icon-link,.uk-light .uk-icon-link:active,.uk-offcanvas-bar .uk-active>.uk-icon-link,.uk-offcanvas-bar .uk-icon-link:active,.uk-overlay-primary .uk-active>.uk-icon-link,.uk-overlay-primary .uk-icon-link:active,.uk-section-primary:not(.uk-preserve-color) .uk-active>.uk-icon-link,.uk-section-primary:not(.uk-preserve-color) .uk-icon-link:active,.uk-section-secondary:not(.uk-preserve-color) .uk-active>.uk-icon-link,.uk-section-secondary:not(.uk-preserve-color) .uk-icon-link:active,.uk-tile-primary:not(.uk-preserve-color) .uk-active>.uk-icon-link,.uk-tile-primary:not(.uk-preserve-color) .uk-icon-link:active,.uk-tile-secondary:not(.uk-preserve-color) .uk-active>.uk-icon-link,.uk-tile-secondary:not(.uk-preserve-color) .uk-icon-link:active{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-icon-button,.uk-card-primary>:not([class*=uk-card-media]) .uk-icon-button,.uk-card-secondary.uk-card-body .uk-icon-button,.uk-card-secondary>:not([class*=uk-card-media]) .uk-icon-button,.uk-light .uk-icon-button,.uk-offcanvas-bar .uk-icon-button,.uk-overlay-primary .uk-icon-button,.uk-section-primary:not(.uk-preserve-color) .uk-icon-button,.uk-section-secondary:not(.uk-preserve-color) .uk-icon-button,.uk-tile-primary:not(.uk-preserve-color) .uk-icon-button,.uk-tile-secondary:not(.uk-preserve-color) .uk-icon-button{background-color:rgba(255,255,255,.1);color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-icon-button:focus,.uk-card-primary.uk-card-body .uk-icon-button:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-icon-button:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-icon-button:hover,.uk-card-secondary.uk-card-body .uk-icon-button:focus,.uk-card-secondary.uk-card-body .uk-icon-button:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-icon-button:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-icon-button:hover,.uk-light .uk-icon-button:focus,.uk-light .uk-icon-button:hover,.uk-offcanvas-bar .uk-icon-button:focus,.uk-offcanvas-bar .uk-icon-button:hover,.uk-overlay-primary .uk-icon-button:focus,.uk-overlay-primary .uk-icon-button:hover,.uk-section-primary:not(.uk-preserve-color) .uk-icon-button:focus,.uk-section-primary:not(.uk-preserve-color) .uk-icon-button:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-icon-button:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-icon-button:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-icon-button:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-icon-button:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-icon-button:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-icon-button:hover{background-color:rgba(242,242,242,.1);color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-icon-button:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-icon-button:active,.uk-card-secondary.uk-card-body .uk-icon-button:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-icon-button:active,.uk-light .uk-icon-button:active,.uk-offcanvas-bar .uk-icon-button:active,.uk-overlay-primary .uk-icon-button:active,.uk-section-primary:not(.uk-preserve-color) .uk-icon-button:active,.uk-section-secondary:not(.uk-preserve-color) .uk-icon-button:active,.uk-tile-primary:not(.uk-preserve-color) .uk-icon-button:active,.uk-tile-secondary:not(.uk-preserve-color) .uk-icon-button:active{background-color:rgba(230,230,230,.1);color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-input,.uk-card-primary.uk-card-body .uk-select,.uk-card-primary.uk-card-body .uk-textarea,.uk-card-primary>:not([class*=uk-card-media]) .uk-input,.uk-card-primary>:not([class*=uk-card-media]) .uk-select,.uk-card-primary>:not([class*=uk-card-media]) .uk-textarea,.uk-card-secondary.uk-card-body .uk-input,.uk-card-secondary.uk-card-body .uk-select,.uk-card-secondary.uk-card-body .uk-textarea,.uk-card-secondary>:not([class*=uk-card-media]) .uk-input,.uk-card-secondary>:not([class*=uk-card-media]) .uk-select,.uk-card-secondary>:not([class*=uk-card-media]) .uk-textarea,.uk-light .uk-input,.uk-light .uk-select,.uk-light .uk-textarea,.uk-offcanvas-bar .uk-input,.uk-offcanvas-bar .uk-select,.uk-offcanvas-bar .uk-textarea,.uk-overlay-primary .uk-input,.uk-overlay-primary .uk-select,.uk-overlay-primary .uk-textarea,.uk-section-primary:not(.uk-preserve-color) .uk-input,.uk-section-primary:not(.uk-preserve-color) .uk-select,.uk-section-primary:not(.uk-preserve-color) .uk-textarea,.uk-section-secondary:not(.uk-preserve-color) .uk-input,.uk-section-secondary:not(.uk-preserve-color) .uk-select,.uk-section-secondary:not(.uk-preserve-color) .uk-textarea,.uk-tile-primary:not(.uk-preserve-color) .uk-input,.uk-tile-primary:not(.uk-preserve-color) .uk-select,.uk-tile-primary:not(.uk-preserve-color) .uk-textarea,.uk-tile-secondary:not(.uk-preserve-color) .uk-input,.uk-tile-secondary:not(.uk-preserve-color) .uk-select,.uk-tile-secondary:not(.uk-preserve-color) .uk-textarea{background-color:rgba(255,255,255,.1);color:rgba(255,255,255,.7);background-clip:padding-box;border-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-input:focus,.uk-card-primary.uk-card-body .uk-select:focus,.uk-card-primary.uk-card-body .uk-textarea:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-input:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-select:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-textarea:focus,.uk-card-secondary.uk-card-body .uk-input:focus,.uk-card-secondary.uk-card-body .uk-select:focus,.uk-card-secondary.uk-card-body .uk-textarea:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-input:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-select:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-textarea:focus,.uk-light .uk-input:focus,.uk-light .uk-select:focus,.uk-light .uk-textarea:focus,.uk-offcanvas-bar .uk-input:focus,.uk-offcanvas-bar .uk-select:focus,.uk-offcanvas-bar .uk-textarea:focus,.uk-overlay-primary .uk-input:focus,.uk-overlay-primary .uk-select:focus,.uk-overlay-primary .uk-textarea:focus,.uk-section-primary:not(.uk-preserve-color) .uk-input:focus,.uk-section-primary:not(.uk-preserve-color) .uk-select:focus,.uk-section-primary:not(.uk-preserve-color) .uk-textarea:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-input:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-select:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-textarea:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-input:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-select:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-textarea:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-input:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-select:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-textarea:focus{background-color:rgba(255,255,255,.1);color:rgba(255,255,255,.7);border-color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-input::-ms-input-placeholder,.uk-card-primary>:not([class*=uk-card-media]) .uk-input::-ms-input-placeholder,.uk-card-secondary.uk-card-body .uk-input::-ms-input-placeholder,.uk-card-secondary>:not([class*=uk-card-media]) .uk-input::-ms-input-placeholder,.uk-light .uk-input::-ms-input-placeholder,.uk-offcanvas-bar .uk-input::-ms-input-placeholder,.uk-overlay-primary .uk-input::-ms-input-placeholder,.uk-section-primary:not(.uk-preserve-color) .uk-input::-ms-input-placeholder,.uk-section-secondary:not(.uk-preserve-color) .uk-input::-ms-input-placeholder,.uk-tile-primary:not(.uk-preserve-color) .uk-input::-ms-input-placeholder,.uk-tile-secondary:not(.uk-preserve-color) .uk-input::-ms-input-placeholder{color:rgba(255,255,255,.5)!important}.uk-card-primary.uk-card-body .uk-input::placeholder,.uk-card-primary>:not([class*=uk-card-media]) .uk-input::placeholder,.uk-card-secondary.uk-card-body .uk-input::placeholder,.uk-card-secondary>:not([class*=uk-card-media]) .uk-input::placeholder,.uk-light .uk-input::placeholder,.uk-offcanvas-bar .uk-input::placeholder,.uk-overlay-primary .uk-input::placeholder,.uk-section-primary:not(.uk-preserve-color) .uk-input::placeholder,.uk-section-secondary:not(.uk-preserve-color) .uk-input::placeholder,.uk-tile-primary:not(.uk-preserve-color) .uk-input::placeholder,.uk-tile-secondary:not(.uk-preserve-color) .uk-input::placeholder{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-textarea::-ms-input-placeholder,.uk-card-primary>:not([class*=uk-card-media]) .uk-textarea::-ms-input-placeholder,.uk-card-secondary.uk-card-body .uk-textarea::-ms-input-placeholder,.uk-card-secondary>:not([class*=uk-card-media]) .uk-textarea::-ms-input-placeholder,.uk-light .uk-textarea::-ms-input-placeholder,.uk-offcanvas-bar .uk-textarea::-ms-input-placeholder,.uk-overlay-primary .uk-textarea::-ms-input-placeholder,.uk-section-primary:not(.uk-preserve-color) .uk-textarea::-ms-input-placeholder,.uk-section-secondary:not(.uk-preserve-color) .uk-textarea::-ms-input-placeholder,.uk-tile-primary:not(.uk-preserve-color) .uk-textarea::-ms-input-placeholder,.uk-tile-secondary:not(.uk-preserve-color) .uk-textarea::-ms-input-placeholder{color:rgba(255,255,255,.5)!important}.uk-card-primary.uk-card-body .uk-textarea::placeholder,.uk-card-primary>:not([class*=uk-card-media]) .uk-textarea::placeholder,.uk-card-secondary.uk-card-body .uk-textarea::placeholder,.uk-card-secondary>:not([class*=uk-card-media]) .uk-textarea::placeholder,.uk-light .uk-textarea::placeholder,.uk-offcanvas-bar .uk-textarea::placeholder,.uk-overlay-primary .uk-textarea::placeholder,.uk-section-primary:not(.uk-preserve-color) .uk-textarea::placeholder,.uk-section-secondary:not(.uk-preserve-color) .uk-textarea::placeholder,.uk-tile-primary:not(.uk-preserve-color) .uk-textarea::placeholder,.uk-tile-secondary:not(.uk-preserve-color) .uk-textarea::placeholder{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-select:not([multiple]):not([size]),.uk-card-primary>:not([class*=uk-card-media]) .uk-select:not([multiple]):not([size]),.uk-card-secondary.uk-card-body .uk-select:not([multiple]):not([size]),.uk-card-secondary>:not([class*=uk-card-media]) .uk-select:not([multiple]):not([size]),.uk-light .uk-select:not([multiple]):not([size]),.uk-offcanvas-bar .uk-select:not([multiple]):not([size]),.uk-overlay-primary .uk-select:not([multiple]):not([size]),.uk-section-primary:not(.uk-preserve-color) .uk-select:not([multiple]):not([size]),.uk-section-secondary:not(.uk-preserve-color) .uk-select:not([multiple]):not([size]),.uk-tile-primary:not(.uk-preserve-color) .uk-select:not([multiple]):not([size]),.uk-tile-secondary:not(.uk-preserve-color) .uk-select:not([multiple]):not([size]){background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2224%22%20height%3D%2216%22%20viewBox%3D%220%200%2024%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22rgba%28255,%20255,%20255,%200.7%29%22%20points%3D%2212%201%209%206%2015%206%22%20%2F%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22rgba%28255,%20255,%20255,%200.7%29%22%20points%3D%2212%2013%209%208%2015%208%22%20%2F%3E%0A%3C%2Fsvg%3E%0A")}.uk-card-primary.uk-card-body .uk-input[list]:focus,.uk-card-primary.uk-card-body .uk-input[list]:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-input[list]:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-input[list]:hover,.uk-card-secondary.uk-card-body .uk-input[list]:focus,.uk-card-secondary.uk-card-body .uk-input[list]:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-input[list]:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-input[list]:hover,.uk-light .uk-input[list]:focus,.uk-light .uk-input[list]:hover,.uk-offcanvas-bar .uk-input[list]:focus,.uk-offcanvas-bar .uk-input[list]:hover,.uk-overlay-primary .uk-input[list]:focus,.uk-overlay-primary .uk-input[list]:hover,.uk-section-primary:not(.uk-preserve-color) .uk-input[list]:focus,.uk-section-primary:not(.uk-preserve-color) .uk-input[list]:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-input[list]:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-input[list]:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-input[list]:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-input[list]:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-input[list]:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-input[list]:hover{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2224%22%20height%3D%2216%22%20viewBox%3D%220%200%2024%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22rgba%28255,%20255,%20255,%200.7%29%22%20points%3D%2212%2012%208%206%2016%206%22%20%2F%3E%0A%3C%2Fsvg%3E%0A")}.uk-card-primary.uk-card-body .uk-checkbox,.uk-card-primary.uk-card-body .uk-radio,.uk-card-primary>:not([class*=uk-card-media]) .uk-checkbox,.uk-card-primary>:not([class*=uk-card-media]) .uk-radio,.uk-card-secondary.uk-card-body .uk-checkbox,.uk-card-secondary.uk-card-body .uk-radio,.uk-card-secondary>:not([class*=uk-card-media]) .uk-checkbox,.uk-card-secondary>:not([class*=uk-card-media]) .uk-radio,.uk-light .uk-checkbox,.uk-light .uk-radio,.uk-offcanvas-bar .uk-checkbox,.uk-offcanvas-bar .uk-radio,.uk-overlay-primary .uk-checkbox,.uk-overlay-primary .uk-radio,.uk-section-primary:not(.uk-preserve-color) .uk-checkbox,.uk-section-primary:not(.uk-preserve-color) .uk-radio,.uk-section-secondary:not(.uk-preserve-color) .uk-checkbox,.uk-section-secondary:not(.uk-preserve-color) .uk-radio,.uk-tile-primary:not(.uk-preserve-color) .uk-checkbox,.uk-tile-primary:not(.uk-preserve-color) .uk-radio,.uk-tile-secondary:not(.uk-preserve-color) .uk-checkbox,.uk-tile-secondary:not(.uk-preserve-color) .uk-radio{background-color:rgba(242,242,242,.1);border-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-checkbox:focus,.uk-card-primary.uk-card-body .uk-radio:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-checkbox:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-radio:focus,.uk-card-secondary.uk-card-body .uk-checkbox:focus,.uk-card-secondary.uk-card-body .uk-radio:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-checkbox:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-radio:focus,.uk-light .uk-checkbox:focus,.uk-light .uk-radio:focus,.uk-offcanvas-bar .uk-checkbox:focus,.uk-offcanvas-bar .uk-radio:focus,.uk-overlay-primary .uk-checkbox:focus,.uk-overlay-primary .uk-radio:focus,.uk-section-primary:not(.uk-preserve-color) .uk-checkbox:focus,.uk-section-primary:not(.uk-preserve-color) .uk-radio:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-checkbox:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-radio:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-checkbox:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-radio:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-checkbox:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-radio:focus{border-color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-checkbox:checked,.uk-card-primary.uk-card-body .uk-checkbox:indeterminate,.uk-card-primary.uk-card-body .uk-radio:checked,.uk-card-primary>:not([class*=uk-card-media]) .uk-checkbox:checked,.uk-card-primary>:not([class*=uk-card-media]) .uk-checkbox:indeterminate,.uk-card-primary>:not([class*=uk-card-media]) .uk-radio:checked,.uk-card-secondary.uk-card-body .uk-checkbox:checked,.uk-card-secondary.uk-card-body .uk-checkbox:indeterminate,.uk-card-secondary.uk-card-body .uk-radio:checked,.uk-card-secondary>:not([class*=uk-card-media]) .uk-checkbox:checked,.uk-card-secondary>:not([class*=uk-card-media]) .uk-checkbox:indeterminate,.uk-card-secondary>:not([class*=uk-card-media]) .uk-radio:checked,.uk-light .uk-checkbox:checked,.uk-light .uk-checkbox:indeterminate,.uk-light .uk-radio:checked,.uk-offcanvas-bar .uk-checkbox:checked,.uk-offcanvas-bar .uk-checkbox:indeterminate,.uk-offcanvas-bar .uk-radio:checked,.uk-overlay-primary .uk-checkbox:checked,.uk-overlay-primary .uk-checkbox:indeterminate,.uk-overlay-primary .uk-radio:checked,.uk-section-primary:not(.uk-preserve-color) .uk-checkbox:checked,.uk-section-primary:not(.uk-preserve-color) .uk-checkbox:indeterminate,.uk-section-primary:not(.uk-preserve-color) .uk-radio:checked,.uk-section-secondary:not(.uk-preserve-color) .uk-checkbox:checked,.uk-section-secondary:not(.uk-preserve-color) .uk-checkbox:indeterminate,.uk-section-secondary:not(.uk-preserve-color) .uk-radio:checked,.uk-tile-primary:not(.uk-preserve-color) .uk-checkbox:checked,.uk-tile-primary:not(.uk-preserve-color) .uk-checkbox:indeterminate,.uk-tile-primary:not(.uk-preserve-color) .uk-radio:checked,.uk-tile-secondary:not(.uk-preserve-color) .uk-checkbox:checked,.uk-tile-secondary:not(.uk-preserve-color) .uk-checkbox:indeterminate,.uk-tile-secondary:not(.uk-preserve-color) .uk-radio:checked{background-color:#fff;border-color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-checkbox:checked:focus,.uk-card-primary.uk-card-body .uk-checkbox:indeterminate:focus,.uk-card-primary.uk-card-body .uk-radio:checked:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-checkbox:checked:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-checkbox:indeterminate:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-radio:checked:focus,.uk-card-secondary.uk-card-body .uk-checkbox:checked:focus,.uk-card-secondary.uk-card-body .uk-checkbox:indeterminate:focus,.uk-card-secondary.uk-card-body .uk-radio:checked:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-checkbox:checked:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-checkbox:indeterminate:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-radio:checked:focus,.uk-light .uk-checkbox:checked:focus,.uk-light .uk-checkbox:indeterminate:focus,.uk-light .uk-radio:checked:focus,.uk-offcanvas-bar .uk-checkbox:checked:focus,.uk-offcanvas-bar .uk-checkbox:indeterminate:focus,.uk-offcanvas-bar .uk-radio:checked:focus,.uk-overlay-primary .uk-checkbox:checked:focus,.uk-overlay-primary .uk-checkbox:indeterminate:focus,.uk-overlay-primary .uk-radio:checked:focus,.uk-section-primary:not(.uk-preserve-color) .uk-checkbox:checked:focus,.uk-section-primary:not(.uk-preserve-color) .uk-checkbox:indeterminate:focus,.uk-section-primary:not(.uk-preserve-color) .uk-radio:checked:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-checkbox:checked:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-checkbox:indeterminate:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-radio:checked:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-checkbox:checked:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-checkbox:indeterminate:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-radio:checked:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-checkbox:checked:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-checkbox:indeterminate:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-radio:checked:focus{background-color:#e6e6e6}.uk-card-primary.uk-card-body .uk-radio:checked,.uk-card-primary>:not([class*=uk-card-media]) .uk-radio:checked,.uk-card-secondary.uk-card-body .uk-radio:checked,.uk-card-secondary>:not([class*=uk-card-media]) .uk-radio:checked,.uk-light .uk-radio:checked,.uk-offcanvas-bar .uk-radio:checked,.uk-overlay-primary .uk-radio:checked,.uk-section-primary:not(.uk-preserve-color) .uk-radio:checked,.uk-section-secondary:not(.uk-preserve-color) .uk-radio:checked,.uk-tile-primary:not(.uk-preserve-color) .uk-radio:checked,.uk-tile-secondary:not(.uk-preserve-color) .uk-radio:checked{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Ccircle%20fill%3D%22%23666%22%20cx%3D%228%22%20cy%3D%228%22%20r%3D%222%22%20%2F%3E%0A%3C%2Fsvg%3E")}.uk-card-primary.uk-card-body .uk-checkbox:checked,.uk-card-primary>:not([class*=uk-card-media]) .uk-checkbox:checked,.uk-card-secondary.uk-card-body .uk-checkbox:checked,.uk-card-secondary>:not([class*=uk-card-media]) .uk-checkbox:checked,.uk-light .uk-checkbox:checked,.uk-offcanvas-bar .uk-checkbox:checked,.uk-overlay-primary .uk-checkbox:checked,.uk-section-primary:not(.uk-preserve-color) .uk-checkbox:checked,.uk-section-secondary:not(.uk-preserve-color) .uk-checkbox:checked,.uk-tile-primary:not(.uk-preserve-color) .uk-checkbox:checked,.uk-tile-secondary:not(.uk-preserve-color) .uk-checkbox:checked{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2211%22%20viewBox%3D%220%200%2014%2011%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolygon%20fill%3D%22%23666%22%20points%3D%2212%201%205%207.5%202%205%201%205.5%205%2010%2013%201.5%22%20%2F%3E%0A%3C%2Fsvg%3E%0A")}.uk-card-primary.uk-card-body .uk-checkbox:indeterminate,.uk-card-primary>:not([class*=uk-card-media]) .uk-checkbox:indeterminate,.uk-card-secondary.uk-card-body .uk-checkbox:indeterminate,.uk-card-secondary>:not([class*=uk-card-media]) .uk-checkbox:indeterminate,.uk-light .uk-checkbox:indeterminate,.uk-offcanvas-bar .uk-checkbox:indeterminate,.uk-overlay-primary .uk-checkbox:indeterminate,.uk-section-primary:not(.uk-preserve-color) .uk-checkbox:indeterminate,.uk-section-secondary:not(.uk-preserve-color) .uk-checkbox:indeterminate,.uk-tile-primary:not(.uk-preserve-color) .uk-checkbox:indeterminate,.uk-tile-secondary:not(.uk-preserve-color) .uk-checkbox:indeterminate{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2016%2016%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22%23666%22%20x%3D%223%22%20y%3D%228%22%20width%3D%2210%22%20height%3D%221%22%20%2F%3E%0A%3C%2Fsvg%3E")}.uk-card-primary.uk-card-body .uk-form-label,.uk-card-primary>:not([class*=uk-card-media]) .uk-form-label,.uk-card-secondary.uk-card-body .uk-form-label,.uk-card-secondary>:not([class*=uk-card-media]) .uk-form-label,.uk-light .uk-form-label,.uk-offcanvas-bar .uk-form-label,.uk-overlay-primary .uk-form-label,.uk-section-primary:not(.uk-preserve-color) .uk-form-label,.uk-section-secondary:not(.uk-preserve-color) .uk-form-label,.uk-tile-primary:not(.uk-preserve-color) .uk-form-label,.uk-tile-secondary:not(.uk-preserve-color) .uk-form-label{color:#fff}.uk-card-primary.uk-card-body .uk-form-icon,.uk-card-primary>:not([class*=uk-card-media]) .uk-form-icon,.uk-card-secondary.uk-card-body .uk-form-icon,.uk-card-secondary>:not([class*=uk-card-media]) .uk-form-icon,.uk-light .uk-form-icon,.uk-offcanvas-bar .uk-form-icon,.uk-overlay-primary .uk-form-icon,.uk-section-primary:not(.uk-preserve-color) .uk-form-icon,.uk-section-secondary:not(.uk-preserve-color) .uk-form-icon,.uk-tile-primary:not(.uk-preserve-color) .uk-form-icon,.uk-tile-secondary:not(.uk-preserve-color) .uk-form-icon{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-form-icon:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-form-icon:hover,.uk-card-secondary.uk-card-body .uk-form-icon:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-form-icon:hover,.uk-light .uk-form-icon:hover,.uk-offcanvas-bar .uk-form-icon:hover,.uk-overlay-primary .uk-form-icon:hover,.uk-section-primary:not(.uk-preserve-color) .uk-form-icon:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-form-icon:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-form-icon:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-form-icon:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-button-default,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-default,.uk-card-secondary.uk-card-body .uk-button-default,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-default,.uk-light .uk-button-default,.uk-offcanvas-bar .uk-button-default,.uk-overlay-primary .uk-button-default,.uk-section-primary:not(.uk-preserve-color) .uk-button-default,.uk-section-secondary:not(.uk-preserve-color) .uk-button-default,.uk-tile-primary:not(.uk-preserve-color) .uk-button-default,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-default{background-color:transparent;color:#fff;border-color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-button-default:focus,.uk-card-primary.uk-card-body .uk-button-default:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-default:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-default:hover,.uk-card-secondary.uk-card-body .uk-button-default:focus,.uk-card-secondary.uk-card-body .uk-button-default:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-default:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-default:hover,.uk-light .uk-button-default:focus,.uk-light .uk-button-default:hover,.uk-offcanvas-bar .uk-button-default:focus,.uk-offcanvas-bar .uk-button-default:hover,.uk-overlay-primary .uk-button-default:focus,.uk-overlay-primary .uk-button-default:hover,.uk-section-primary:not(.uk-preserve-color) .uk-button-default:focus,.uk-section-primary:not(.uk-preserve-color) .uk-button-default:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-button-default:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-button-default:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-button-default:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-button-default:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-default:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-default:hover{background-color:transparent;color:#fff;border-color:#fff}.uk-card-primary.uk-card-body .uk-button-default.uk-active,.uk-card-primary.uk-card-body .uk-button-default:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-default.uk-active,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-default:active,.uk-card-secondary.uk-card-body .uk-button-default.uk-active,.uk-card-secondary.uk-card-body .uk-button-default:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-default.uk-active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-default:active,.uk-light .uk-button-default.uk-active,.uk-light .uk-button-default:active,.uk-offcanvas-bar .uk-button-default.uk-active,.uk-offcanvas-bar .uk-button-default:active,.uk-overlay-primary .uk-button-default.uk-active,.uk-overlay-primary .uk-button-default:active,.uk-section-primary:not(.uk-preserve-color) .uk-button-default.uk-active,.uk-section-primary:not(.uk-preserve-color) .uk-button-default:active,.uk-section-secondary:not(.uk-preserve-color) .uk-button-default.uk-active,.uk-section-secondary:not(.uk-preserve-color) .uk-button-default:active,.uk-tile-primary:not(.uk-preserve-color) .uk-button-default.uk-active,.uk-tile-primary:not(.uk-preserve-color) .uk-button-default:active,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-default.uk-active,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-default:active{background-color:transparent;color:#fff;border-color:#fff}.uk-card-primary.uk-card-body .uk-button-primary,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-primary,.uk-card-secondary.uk-card-body .uk-button-primary,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-primary,.uk-light .uk-button-primary,.uk-offcanvas-bar .uk-button-primary,.uk-overlay-primary .uk-button-primary,.uk-section-primary:not(.uk-preserve-color) .uk-button-primary,.uk-section-secondary:not(.uk-preserve-color) .uk-button-primary,.uk-tile-primary:not(.uk-preserve-color) .uk-button-primary,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-primary{background-color:#fff;color:#666}.uk-card-primary.uk-card-body .uk-button-primary:focus,.uk-card-primary.uk-card-body .uk-button-primary:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-primary:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-primary:hover,.uk-card-secondary.uk-card-body .uk-button-primary:focus,.uk-card-secondary.uk-card-body .uk-button-primary:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-primary:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-primary:hover,.uk-light .uk-button-primary:focus,.uk-light .uk-button-primary:hover,.uk-offcanvas-bar .uk-button-primary:focus,.uk-offcanvas-bar .uk-button-primary:hover,.uk-overlay-primary .uk-button-primary:focus,.uk-overlay-primary .uk-button-primary:hover,.uk-section-primary:not(.uk-preserve-color) .uk-button-primary:focus,.uk-section-primary:not(.uk-preserve-color) .uk-button-primary:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-button-primary:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-button-primary:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-button-primary:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-button-primary:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-primary:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-primary:hover{background-color:#f2f2f2;color:#666}.uk-card-primary.uk-card-body .uk-button-primary.uk-active,.uk-card-primary.uk-card-body .uk-button-primary:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-primary.uk-active,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-primary:active,.uk-card-secondary.uk-card-body .uk-button-primary.uk-active,.uk-card-secondary.uk-card-body .uk-button-primary:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-primary.uk-active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-primary:active,.uk-light .uk-button-primary.uk-active,.uk-light .uk-button-primary:active,.uk-offcanvas-bar .uk-button-primary.uk-active,.uk-offcanvas-bar .uk-button-primary:active,.uk-overlay-primary .uk-button-primary.uk-active,.uk-overlay-primary .uk-button-primary:active,.uk-section-primary:not(.uk-preserve-color) .uk-button-primary.uk-active,.uk-section-primary:not(.uk-preserve-color) .uk-button-primary:active,.uk-section-secondary:not(.uk-preserve-color) .uk-button-primary.uk-active,.uk-section-secondary:not(.uk-preserve-color) .uk-button-primary:active,.uk-tile-primary:not(.uk-preserve-color) .uk-button-primary.uk-active,.uk-tile-primary:not(.uk-preserve-color) .uk-button-primary:active,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-primary.uk-active,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-primary:active{background-color:#e6e6e6;color:#666}.uk-card-primary.uk-card-body .uk-button-secondary,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-secondary,.uk-card-secondary.uk-card-body .uk-button-secondary,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-secondary,.uk-light .uk-button-secondary,.uk-offcanvas-bar .uk-button-secondary,.uk-overlay-primary .uk-button-secondary,.uk-section-primary:not(.uk-preserve-color) .uk-button-secondary,.uk-section-secondary:not(.uk-preserve-color) .uk-button-secondary,.uk-tile-primary:not(.uk-preserve-color) .uk-button-secondary,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-secondary{background-color:#fff;color:#666}.uk-card-primary.uk-card-body .uk-button-secondary:focus,.uk-card-primary.uk-card-body .uk-button-secondary:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-secondary:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-secondary:hover,.uk-card-secondary.uk-card-body .uk-button-secondary:focus,.uk-card-secondary.uk-card-body .uk-button-secondary:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-secondary:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-secondary:hover,.uk-light .uk-button-secondary:focus,.uk-light .uk-button-secondary:hover,.uk-offcanvas-bar .uk-button-secondary:focus,.uk-offcanvas-bar .uk-button-secondary:hover,.uk-overlay-primary .uk-button-secondary:focus,.uk-overlay-primary .uk-button-secondary:hover,.uk-section-primary:not(.uk-preserve-color) .uk-button-secondary:focus,.uk-section-primary:not(.uk-preserve-color) .uk-button-secondary:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-button-secondary:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-button-secondary:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-button-secondary:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-button-secondary:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-secondary:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-secondary:hover{background-color:#f2f2f2;color:#666}.uk-card-primary.uk-card-body .uk-button-secondary.uk-active,.uk-card-primary.uk-card-body .uk-button-secondary:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-secondary.uk-active,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-secondary:active,.uk-card-secondary.uk-card-body .uk-button-secondary.uk-active,.uk-card-secondary.uk-card-body .uk-button-secondary:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-secondary.uk-active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-secondary:active,.uk-light .uk-button-secondary.uk-active,.uk-light .uk-button-secondary:active,.uk-offcanvas-bar .uk-button-secondary.uk-active,.uk-offcanvas-bar .uk-button-secondary:active,.uk-overlay-primary .uk-button-secondary.uk-active,.uk-overlay-primary .uk-button-secondary:active,.uk-section-primary:not(.uk-preserve-color) .uk-button-secondary.uk-active,.uk-section-primary:not(.uk-preserve-color) .uk-button-secondary:active,.uk-section-secondary:not(.uk-preserve-color) .uk-button-secondary.uk-active,.uk-section-secondary:not(.uk-preserve-color) .uk-button-secondary:active,.uk-tile-primary:not(.uk-preserve-color) .uk-button-secondary.uk-active,.uk-tile-primary:not(.uk-preserve-color) .uk-button-secondary:active,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-secondary.uk-active,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-secondary:active{background-color:#e6e6e6;color:#666}.uk-card-primary.uk-card-body .uk-button-text,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-text,.uk-card-secondary.uk-card-body .uk-button-text,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-text,.uk-light .uk-button-text,.uk-offcanvas-bar .uk-button-text,.uk-overlay-primary .uk-button-text,.uk-section-primary:not(.uk-preserve-color) .uk-button-text,.uk-section-secondary:not(.uk-preserve-color) .uk-button-text,.uk-tile-primary:not(.uk-preserve-color) .uk-button-text,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-text{color:#fff}.uk-card-primary.uk-card-body .uk-button-text::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-text::before,.uk-card-secondary.uk-card-body .uk-button-text::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-text::before,.uk-light .uk-button-text::before,.uk-offcanvas-bar .uk-button-text::before,.uk-overlay-primary .uk-button-text::before,.uk-section-primary:not(.uk-preserve-color) .uk-button-text::before,.uk-section-secondary:not(.uk-preserve-color) .uk-button-text::before,.uk-tile-primary:not(.uk-preserve-color) .uk-button-text::before,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-text::before{border-bottom-color:#fff}.uk-card-primary.uk-card-body .uk-button-text:focus,.uk-card-primary.uk-card-body .uk-button-text:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-text:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-text:hover,.uk-card-secondary.uk-card-body .uk-button-text:focus,.uk-card-secondary.uk-card-body .uk-button-text:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-text:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-text:hover,.uk-light .uk-button-text:focus,.uk-light .uk-button-text:hover,.uk-offcanvas-bar .uk-button-text:focus,.uk-offcanvas-bar .uk-button-text:hover,.uk-overlay-primary .uk-button-text:focus,.uk-overlay-primary .uk-button-text:hover,.uk-section-primary:not(.uk-preserve-color) .uk-button-text:focus,.uk-section-primary:not(.uk-preserve-color) .uk-button-text:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-button-text:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-button-text:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-button-text:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-button-text:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-text:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-text:hover{color:#fff}.uk-card-primary.uk-card-body .uk-button-text:disabled,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-text:disabled,.uk-card-secondary.uk-card-body .uk-button-text:disabled,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-text:disabled,.uk-light .uk-button-text:disabled,.uk-offcanvas-bar .uk-button-text:disabled,.uk-overlay-primary .uk-button-text:disabled,.uk-section-primary:not(.uk-preserve-color) .uk-button-text:disabled,.uk-section-secondary:not(.uk-preserve-color) .uk-button-text:disabled,.uk-tile-primary:not(.uk-preserve-color) .uk-button-text:disabled,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-text:disabled{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-button-link,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-link,.uk-card-secondary.uk-card-body .uk-button-link,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-link,.uk-light .uk-button-link,.uk-offcanvas-bar .uk-button-link,.uk-overlay-primary .uk-button-link,.uk-section-primary:not(.uk-preserve-color) .uk-button-link,.uk-section-secondary:not(.uk-preserve-color) .uk-button-link,.uk-tile-primary:not(.uk-preserve-color) .uk-button-link,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-link{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-button-link:focus,.uk-card-primary.uk-card-body .uk-button-link:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-link:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-button-link:hover,.uk-card-secondary.uk-card-body .uk-button-link:focus,.uk-card-secondary.uk-card-body .uk-button-link:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-link:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-button-link:hover,.uk-light .uk-button-link:focus,.uk-light .uk-button-link:hover,.uk-offcanvas-bar .uk-button-link:focus,.uk-offcanvas-bar .uk-button-link:hover,.uk-overlay-primary .uk-button-link:focus,.uk-overlay-primary .uk-button-link:hover,.uk-section-primary:not(.uk-preserve-color) .uk-button-link:focus,.uk-section-primary:not(.uk-preserve-color) .uk-button-link:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-button-link:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-button-link:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-button-link:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-button-link:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-link:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-button-link:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-grid-divider>:not(.uk-first-column)::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-grid-divider>:not(.uk-first-column)::before,.uk-card-secondary.uk-card-body .uk-grid-divider>:not(.uk-first-column)::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-grid-divider>:not(.uk-first-column)::before,.uk-light .uk-grid-divider>:not(.uk-first-column)::before,.uk-offcanvas-bar .uk-grid-divider>:not(.uk-first-column)::before,.uk-overlay-primary .uk-grid-divider>:not(.uk-first-column)::before,.uk-section-primary:not(.uk-preserve-color) .uk-grid-divider>:not(.uk-first-column)::before,.uk-section-secondary:not(.uk-preserve-color) .uk-grid-divider>:not(.uk-first-column)::before,.uk-tile-primary:not(.uk-preserve-color) .uk-grid-divider>:not(.uk-first-column)::before,.uk-tile-secondary:not(.uk-preserve-color) .uk-grid-divider>:not(.uk-first-column)::before{border-left-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-card-secondary.uk-card-body .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-light .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-offcanvas-bar .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-overlay-primary .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-section-primary:not(.uk-preserve-color) .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-section-secondary:not(.uk-preserve-color) .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-tile-primary:not(.uk-preserve-color) .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before,.uk-tile-secondary:not(.uk-preserve-color) .uk-grid-divider.uk-grid-stack>.uk-grid-margin::before{border-top-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-close,.uk-card-primary>:not([class*=uk-card-media]) .uk-close,.uk-card-secondary.uk-card-body .uk-close,.uk-card-secondary>:not([class*=uk-card-media]) .uk-close,.uk-light .uk-close,.uk-offcanvas-bar .uk-close,.uk-overlay-primary .uk-close,.uk-section-primary:not(.uk-preserve-color) .uk-close,.uk-section-secondary:not(.uk-preserve-color) .uk-close,.uk-tile-primary:not(.uk-preserve-color) .uk-close,.uk-tile-secondary:not(.uk-preserve-color) .uk-close{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-close:focus,.uk-card-primary.uk-card-body .uk-close:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-close:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-close:hover,.uk-card-secondary.uk-card-body .uk-close:focus,.uk-card-secondary.uk-card-body .uk-close:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-close:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-close:hover,.uk-light .uk-close:focus,.uk-light .uk-close:hover,.uk-offcanvas-bar .uk-close:focus,.uk-offcanvas-bar .uk-close:hover,.uk-overlay-primary .uk-close:focus,.uk-overlay-primary .uk-close:hover,.uk-section-primary:not(.uk-preserve-color) .uk-close:focus,.uk-section-primary:not(.uk-preserve-color) .uk-close:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-close:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-close:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-close:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-close:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-close:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-close:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-totop,.uk-card-primary>:not([class*=uk-card-media]) .uk-totop,.uk-card-secondary.uk-card-body .uk-totop,.uk-card-secondary>:not([class*=uk-card-media]) .uk-totop,.uk-light .uk-totop,.uk-offcanvas-bar .uk-totop,.uk-overlay-primary .uk-totop,.uk-section-primary:not(.uk-preserve-color) .uk-totop,.uk-section-secondary:not(.uk-preserve-color) .uk-totop,.uk-tile-primary:not(.uk-preserve-color) .uk-totop,.uk-tile-secondary:not(.uk-preserve-color) .uk-totop{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-totop:focus,.uk-card-primary.uk-card-body .uk-totop:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-totop:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-totop:hover,.uk-card-secondary.uk-card-body .uk-totop:focus,.uk-card-secondary.uk-card-body .uk-totop:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-totop:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-totop:hover,.uk-light .uk-totop:focus,.uk-light .uk-totop:hover,.uk-offcanvas-bar .uk-totop:focus,.uk-offcanvas-bar .uk-totop:hover,.uk-overlay-primary .uk-totop:focus,.uk-overlay-primary .uk-totop:hover,.uk-section-primary:not(.uk-preserve-color) .uk-totop:focus,.uk-section-primary:not(.uk-preserve-color) .uk-totop:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-totop:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-totop:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-totop:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-totop:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-totop:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-totop:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-totop:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-totop:active,.uk-card-secondary.uk-card-body .uk-totop:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-totop:active,.uk-light .uk-totop:active,.uk-offcanvas-bar .uk-totop:active,.uk-overlay-primary .uk-totop:active,.uk-section-primary:not(.uk-preserve-color) .uk-totop:active,.uk-section-secondary:not(.uk-preserve-color) .uk-totop:active,.uk-tile-primary:not(.uk-preserve-color) .uk-totop:active,.uk-tile-secondary:not(.uk-preserve-color) .uk-totop:active{color:#fff}.uk-card-primary.uk-card-body .uk-marker,.uk-card-primary>:not([class*=uk-card-media]) .uk-marker,.uk-card-secondary.uk-card-body .uk-marker,.uk-card-secondary>:not([class*=uk-card-media]) .uk-marker,.uk-light .uk-marker,.uk-offcanvas-bar .uk-marker,.uk-overlay-primary .uk-marker,.uk-section-primary:not(.uk-preserve-color) .uk-marker,.uk-section-secondary:not(.uk-preserve-color) .uk-marker,.uk-tile-primary:not(.uk-preserve-color) .uk-marker,.uk-tile-secondary:not(.uk-preserve-color) .uk-marker{background:#f8f8f8;color:#666}.uk-card-primary.uk-card-body .uk-marker:focus,.uk-card-primary.uk-card-body .uk-marker:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-marker:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-marker:hover,.uk-card-secondary.uk-card-body .uk-marker:focus,.uk-card-secondary.uk-card-body .uk-marker:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-marker:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-marker:hover,.uk-light .uk-marker:focus,.uk-light .uk-marker:hover,.uk-offcanvas-bar .uk-marker:focus,.uk-offcanvas-bar .uk-marker:hover,.uk-overlay-primary .uk-marker:focus,.uk-overlay-primary .uk-marker:hover,.uk-section-primary:not(.uk-preserve-color) .uk-marker:focus,.uk-section-primary:not(.uk-preserve-color) .uk-marker:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-marker:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-marker:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-marker:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-marker:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-marker:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-marker:hover{color:#666}.uk-card-primary.uk-card-body .uk-badge,.uk-card-primary>:not([class*=uk-card-media]) .uk-badge,.uk-card-secondary.uk-card-body .uk-badge,.uk-card-secondary>:not([class*=uk-card-media]) .uk-badge,.uk-light .uk-badge,.uk-offcanvas-bar .uk-badge,.uk-overlay-primary .uk-badge,.uk-section-primary:not(.uk-preserve-color) .uk-badge,.uk-section-secondary:not(.uk-preserve-color) .uk-badge,.uk-tile-primary:not(.uk-preserve-color) .uk-badge,.uk-tile-secondary:not(.uk-preserve-color) .uk-badge{background-color:#fff;color:#666}.uk-card-primary.uk-card-body .uk-badge:focus,.uk-card-primary.uk-card-body .uk-badge:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-badge:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-badge:hover,.uk-card-secondary.uk-card-body .uk-badge:focus,.uk-card-secondary.uk-card-body .uk-badge:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-badge:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-badge:hover,.uk-light .uk-badge:focus,.uk-light .uk-badge:hover,.uk-offcanvas-bar .uk-badge:focus,.uk-offcanvas-bar .uk-badge:hover,.uk-overlay-primary .uk-badge:focus,.uk-overlay-primary .uk-badge:hover,.uk-section-primary:not(.uk-preserve-color) .uk-badge:focus,.uk-section-primary:not(.uk-preserve-color) .uk-badge:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-badge:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-badge:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-badge:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-badge:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-badge:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-badge:hover{color:#666}.uk-card-primary.uk-card-body .uk-label,.uk-card-primary>:not([class*=uk-card-media]) .uk-label,.uk-card-secondary.uk-card-body .uk-label,.uk-card-secondary>:not([class*=uk-card-media]) .uk-label,.uk-light .uk-label,.uk-offcanvas-bar .uk-label,.uk-overlay-primary .uk-label,.uk-section-primary:not(.uk-preserve-color) .uk-label,.uk-section-secondary:not(.uk-preserve-color) .uk-label,.uk-tile-primary:not(.uk-preserve-color) .uk-label,.uk-tile-secondary:not(.uk-preserve-color) .uk-label{background-color:#fff;color:#666}.uk-card-primary.uk-card-body .uk-article-meta,.uk-card-primary>:not([class*=uk-card-media]) .uk-article-meta,.uk-card-secondary.uk-card-body .uk-article-meta,.uk-card-secondary>:not([class*=uk-card-media]) .uk-article-meta,.uk-light .uk-article-meta,.uk-offcanvas-bar .uk-article-meta,.uk-overlay-primary .uk-article-meta,.uk-section-primary:not(.uk-preserve-color) .uk-article-meta,.uk-section-secondary:not(.uk-preserve-color) .uk-article-meta,.uk-tile-primary:not(.uk-preserve-color) .uk-article-meta,.uk-tile-secondary:not(.uk-preserve-color) .uk-article-meta{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-search-input,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-input,.uk-card-secondary.uk-card-body .uk-search-input,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-input,.uk-light .uk-search-input,.uk-offcanvas-bar .uk-search-input,.uk-overlay-primary .uk-search-input,.uk-section-primary:not(.uk-preserve-color) .uk-search-input,.uk-section-secondary:not(.uk-preserve-color) .uk-search-input,.uk-tile-primary:not(.uk-preserve-color) .uk-search-input,.uk-tile-secondary:not(.uk-preserve-color) .uk-search-input{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-search-input:-ms-input-placeholder,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-input:-ms-input-placeholder,.uk-card-secondary.uk-card-body .uk-search-input:-ms-input-placeholder,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-input:-ms-input-placeholder,.uk-light .uk-search-input:-ms-input-placeholder,.uk-offcanvas-bar .uk-search-input:-ms-input-placeholder,.uk-overlay-primary .uk-search-input:-ms-input-placeholder,.uk-section-primary:not(.uk-preserve-color) .uk-search-input:-ms-input-placeholder,.uk-section-secondary:not(.uk-preserve-color) .uk-search-input:-ms-input-placeholder,.uk-tile-primary:not(.uk-preserve-color) .uk-search-input:-ms-input-placeholder,.uk-tile-secondary:not(.uk-preserve-color) .uk-search-input:-ms-input-placeholder{color:rgba(255,255,255,.5)!important}.uk-card-primary.uk-card-body .uk-search-input::placeholder,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-input::placeholder,.uk-card-secondary.uk-card-body .uk-search-input::placeholder,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-input::placeholder,.uk-light .uk-search-input::placeholder,.uk-offcanvas-bar .uk-search-input::placeholder,.uk-overlay-primary .uk-search-input::placeholder,.uk-section-primary:not(.uk-preserve-color) .uk-search-input::placeholder,.uk-section-secondary:not(.uk-preserve-color) .uk-search-input::placeholder,.uk-tile-primary:not(.uk-preserve-color) .uk-search-input::placeholder,.uk-tile-secondary:not(.uk-preserve-color) .uk-search-input::placeholder{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-search .uk-search-icon,.uk-card-primary>:not([class*=uk-card-media]) .uk-search .uk-search-icon,.uk-card-secondary.uk-card-body .uk-search .uk-search-icon,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search .uk-search-icon,.uk-light .uk-search .uk-search-icon,.uk-offcanvas-bar .uk-search .uk-search-icon,.uk-overlay-primary .uk-search .uk-search-icon,.uk-section-primary:not(.uk-preserve-color) .uk-search .uk-search-icon,.uk-section-secondary:not(.uk-preserve-color) .uk-search .uk-search-icon,.uk-tile-primary:not(.uk-preserve-color) .uk-search .uk-search-icon,.uk-tile-secondary:not(.uk-preserve-color) .uk-search .uk-search-icon{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-search .uk-search-icon:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-search .uk-search-icon:hover,.uk-card-secondary.uk-card-body .uk-search .uk-search-icon:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search .uk-search-icon:hover,.uk-light .uk-search .uk-search-icon:hover,.uk-offcanvas-bar .uk-search .uk-search-icon:hover,.uk-overlay-primary .uk-search .uk-search-icon:hover,.uk-section-primary:not(.uk-preserve-color) .uk-search .uk-search-icon:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-search .uk-search-icon:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-search .uk-search-icon:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-search .uk-search-icon:hover{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-search-default .uk-search-input,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-default .uk-search-input,.uk-card-secondary.uk-card-body .uk-search-default .uk-search-input,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-default .uk-search-input,.uk-light .uk-search-default .uk-search-input,.uk-offcanvas-bar .uk-search-default .uk-search-input,.uk-overlay-primary .uk-search-default .uk-search-input,.uk-section-primary:not(.uk-preserve-color) .uk-search-default .uk-search-input,.uk-section-secondary:not(.uk-preserve-color) .uk-search-default .uk-search-input,.uk-tile-primary:not(.uk-preserve-color) .uk-search-default .uk-search-input,.uk-tile-secondary:not(.uk-preserve-color) .uk-search-default .uk-search-input{background-color:transparent;border-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-search-default .uk-search-input:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-default .uk-search-input:focus,.uk-card-secondary.uk-card-body .uk-search-default .uk-search-input:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-default .uk-search-input:focus,.uk-light .uk-search-default .uk-search-input:focus,.uk-offcanvas-bar .uk-search-default .uk-search-input:focus,.uk-overlay-primary .uk-search-default .uk-search-input:focus,.uk-section-primary:not(.uk-preserve-color) .uk-search-default .uk-search-input:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-search-default .uk-search-input:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-search-default .uk-search-input:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-search-default .uk-search-input:focus{background-color:transparent}.uk-card-primary.uk-card-body .uk-search-navbar .uk-search-input,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-navbar .uk-search-input,.uk-card-secondary.uk-card-body .uk-search-navbar .uk-search-input,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-navbar .uk-search-input,.uk-light .uk-search-navbar .uk-search-input,.uk-offcanvas-bar .uk-search-navbar .uk-search-input,.uk-overlay-primary .uk-search-navbar .uk-search-input,.uk-section-primary:not(.uk-preserve-color) .uk-search-navbar .uk-search-input,.uk-section-secondary:not(.uk-preserve-color) .uk-search-navbar .uk-search-input,.uk-tile-primary:not(.uk-preserve-color) .uk-search-navbar .uk-search-input,.uk-tile-secondary:not(.uk-preserve-color) .uk-search-navbar .uk-search-input{background-color:transparent}.uk-card-primary.uk-card-body .uk-search-large .uk-search-input,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-large .uk-search-input,.uk-card-secondary.uk-card-body .uk-search-large .uk-search-input,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-large .uk-search-input,.uk-light .uk-search-large .uk-search-input,.uk-offcanvas-bar .uk-search-large .uk-search-input,.uk-overlay-primary .uk-search-large .uk-search-input,.uk-section-primary:not(.uk-preserve-color) .uk-search-large .uk-search-input,.uk-section-secondary:not(.uk-preserve-color) .uk-search-large .uk-search-input,.uk-tile-primary:not(.uk-preserve-color) .uk-search-large .uk-search-input,.uk-tile-secondary:not(.uk-preserve-color) .uk-search-large .uk-search-input{background-color:transparent}.uk-card-primary.uk-card-body .uk-search-toggle,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-toggle,.uk-card-secondary.uk-card-body .uk-search-toggle,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-toggle,.uk-light .uk-search-toggle,.uk-offcanvas-bar .uk-search-toggle,.uk-overlay-primary .uk-search-toggle,.uk-section-primary:not(.uk-preserve-color) .uk-search-toggle,.uk-section-secondary:not(.uk-preserve-color) .uk-search-toggle,.uk-tile-primary:not(.uk-preserve-color) .uk-search-toggle,.uk-tile-secondary:not(.uk-preserve-color) .uk-search-toggle{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-search-toggle:focus,.uk-card-primary.uk-card-body .uk-search-toggle:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-toggle:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-search-toggle:hover,.uk-card-secondary.uk-card-body .uk-search-toggle:focus,.uk-card-secondary.uk-card-body .uk-search-toggle:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-toggle:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-search-toggle:hover,.uk-light .uk-search-toggle:focus,.uk-light .uk-search-toggle:hover,.uk-offcanvas-bar .uk-search-toggle:focus,.uk-offcanvas-bar .uk-search-toggle:hover,.uk-overlay-primary .uk-search-toggle:focus,.uk-overlay-primary .uk-search-toggle:hover,.uk-section-primary:not(.uk-preserve-color) .uk-search-toggle:focus,.uk-section-primary:not(.uk-preserve-color) .uk-search-toggle:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-search-toggle:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-search-toggle:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-search-toggle:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-search-toggle:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-search-toggle:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-search-toggle:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-nav-parent-icon>.uk-parent>a::after,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-parent-icon>.uk-parent>a::after,.uk-card-secondary.uk-card-body .uk-nav-parent-icon>.uk-parent>a::after,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-parent-icon>.uk-parent>a::after,.uk-light .uk-nav-parent-icon>.uk-parent>a::after,.uk-offcanvas-bar .uk-nav-parent-icon>.uk-parent>a::after,.uk-overlay-primary .uk-nav-parent-icon>.uk-parent>a::after,.uk-section-primary:not(.uk-preserve-color) .uk-nav-parent-icon>.uk-parent>a::after,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-parent-icon>.uk-parent>a::after,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-parent-icon>.uk-parent>a::after,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-parent-icon>.uk-parent>a::after{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolyline%20fill%3D%22none%22%20stroke%3D%22rgba%28255,%20255,%20255,%200.7%29%22%20stroke-width%3D%221.1%22%20points%3D%2210%201%204%207%2010%2013%22%20%2F%3E%0A%3C%2Fsvg%3E")}.uk-card-primary.uk-card-body .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-card-secondary.uk-card-body .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-light .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-offcanvas-bar .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-overlay-primary .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-section-primary:not(.uk-preserve-color) .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-parent-icon>.uk-parent.uk-open>a::after,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-parent-icon>.uk-parent.uk-open>a::after{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2214%22%20height%3D%2214%22%20viewBox%3D%220%200%2014%2014%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Cpolyline%20fill%3D%22none%22%20stroke%3D%22rgba%28255,%20255,%20255,%200.7%29%22%20stroke-width%3D%221.1%22%20points%3D%221%204%207%2010%2013%204%22%20%2F%3E%0A%3C%2Fsvg%3E")}.uk-card-primary.uk-card-body .uk-nav-default>li>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default>li>a,.uk-card-secondary.uk-card-body .uk-nav-default>li>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default>li>a,.uk-light .uk-nav-default>li>a,.uk-offcanvas-bar .uk-nav-default>li>a,.uk-overlay-primary .uk-nav-default>li>a,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default>li>a,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default>li>a,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-default>li>a,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-default>li>a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-nav-default>li>a:focus,.uk-card-primary.uk-card-body .uk-nav-default>li>a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default>li>a:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default>li>a:hover,.uk-card-secondary.uk-card-body .uk-nav-default>li>a:focus,.uk-card-secondary.uk-card-body .uk-nav-default>li>a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default>li>a:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default>li>a:hover,.uk-light .uk-nav-default>li>a:focus,.uk-light .uk-nav-default>li>a:hover,.uk-offcanvas-bar .uk-nav-default>li>a:focus,.uk-offcanvas-bar .uk-nav-default>li>a:hover,.uk-overlay-primary .uk-nav-default>li>a:focus,.uk-overlay-primary .uk-nav-default>li>a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default>li>a:focus,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default>li>a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default>li>a:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default>li>a:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-default>li>a:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-default>li>a:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-default>li>a:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-default>li>a:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-nav-default>li.uk-active>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default>li.uk-active>a,.uk-card-secondary.uk-card-body .uk-nav-default>li.uk-active>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default>li.uk-active>a,.uk-light .uk-nav-default>li.uk-active>a,.uk-offcanvas-bar .uk-nav-default>li.uk-active>a,.uk-overlay-primary .uk-nav-default>li.uk-active>a,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default>li.uk-active>a,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default>li.uk-active>a,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-default>li.uk-active>a,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-default>li.uk-active>a{color:#fff}.uk-card-primary.uk-card-body .uk-nav-default .uk-nav-header,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-header,.uk-card-secondary.uk-card-body .uk-nav-default .uk-nav-header,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-header,.uk-light .uk-nav-default .uk-nav-header,.uk-offcanvas-bar .uk-nav-default .uk-nav-header,.uk-overlay-primary .uk-nav-default .uk-nav-header,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default .uk-nav-header,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default .uk-nav-header,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-default .uk-nav-header,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-default .uk-nav-header{color:#fff}.uk-card-primary.uk-card-body .uk-nav-default .uk-nav-divider,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-divider,.uk-card-secondary.uk-card-body .uk-nav-default .uk-nav-divider,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-divider,.uk-light .uk-nav-default .uk-nav-divider,.uk-offcanvas-bar .uk-nav-default .uk-nav-divider,.uk-overlay-primary .uk-nav-default .uk-nav-divider,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default .uk-nav-divider,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default .uk-nav-divider,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-default .uk-nav-divider,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-default .uk-nav-divider{border-top-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-nav-default .uk-nav-sub a,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-sub a,.uk-card-secondary.uk-card-body .uk-nav-default .uk-nav-sub a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-sub a,.uk-light .uk-nav-default .uk-nav-sub a,.uk-offcanvas-bar .uk-nav-default .uk-nav-sub a,.uk-overlay-primary .uk-nav-default .uk-nav-sub a,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-nav-default .uk-nav-sub a:focus,.uk-card-primary.uk-card-body .uk-nav-default .uk-nav-sub a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-sub a:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-sub a:hover,.uk-card-secondary.uk-card-body .uk-nav-default .uk-nav-sub a:focus,.uk-card-secondary.uk-card-body .uk-nav-default .uk-nav-sub a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-sub a:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-sub a:hover,.uk-light .uk-nav-default .uk-nav-sub a:focus,.uk-light .uk-nav-default .uk-nav-sub a:hover,.uk-offcanvas-bar .uk-nav-default .uk-nav-sub a:focus,.uk-offcanvas-bar .uk-nav-default .uk-nav-sub a:hover,.uk-overlay-primary .uk-nav-default .uk-nav-sub a:focus,.uk-overlay-primary .uk-nav-default .uk-nav-sub a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a:focus,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub a:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-nav-default .uk-nav-sub li.uk-active>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-sub li.uk-active>a,.uk-card-secondary.uk-card-body .uk-nav-default .uk-nav-sub li.uk-active>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-default .uk-nav-sub li.uk-active>a,.uk-light .uk-nav-default .uk-nav-sub li.uk-active>a,.uk-offcanvas-bar .uk-nav-default .uk-nav-sub li.uk-active>a,.uk-overlay-primary .uk-nav-default .uk-nav-sub li.uk-active>a,.uk-section-primary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub li.uk-active>a,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub li.uk-active>a,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub li.uk-active>a,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-default .uk-nav-sub li.uk-active>a{color:#fff}.uk-card-primary.uk-card-body .uk-nav-primary>li>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary>li>a,.uk-card-secondary.uk-card-body .uk-nav-primary>li>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary>li>a,.uk-light .uk-nav-primary>li>a,.uk-offcanvas-bar .uk-nav-primary>li>a,.uk-overlay-primary .uk-nav-primary>li>a,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary>li>a,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary>li>a,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-primary>li>a,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-primary>li>a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-nav-primary>li>a:focus,.uk-card-primary.uk-card-body .uk-nav-primary>li>a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary>li>a:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary>li>a:hover,.uk-card-secondary.uk-card-body .uk-nav-primary>li>a:focus,.uk-card-secondary.uk-card-body .uk-nav-primary>li>a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary>li>a:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary>li>a:hover,.uk-light .uk-nav-primary>li>a:focus,.uk-light .uk-nav-primary>li>a:hover,.uk-offcanvas-bar .uk-nav-primary>li>a:focus,.uk-offcanvas-bar .uk-nav-primary>li>a:hover,.uk-overlay-primary .uk-nav-primary>li>a:focus,.uk-overlay-primary .uk-nav-primary>li>a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary>li>a:focus,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary>li>a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary>li>a:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary>li>a:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-primary>li>a:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-primary>li>a:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-primary>li>a:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-primary>li>a:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-nav-primary>li.uk-active>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary>li.uk-active>a,.uk-card-secondary.uk-card-body .uk-nav-primary>li.uk-active>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary>li.uk-active>a,.uk-light .uk-nav-primary>li.uk-active>a,.uk-offcanvas-bar .uk-nav-primary>li.uk-active>a,.uk-overlay-primary .uk-nav-primary>li.uk-active>a,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary>li.uk-active>a,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary>li.uk-active>a,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-primary>li.uk-active>a,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-primary>li.uk-active>a{color:#fff}.uk-card-primary.uk-card-body .uk-nav-primary .uk-nav-header,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-header,.uk-card-secondary.uk-card-body .uk-nav-primary .uk-nav-header,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-header,.uk-light .uk-nav-primary .uk-nav-header,.uk-offcanvas-bar .uk-nav-primary .uk-nav-header,.uk-overlay-primary .uk-nav-primary .uk-nav-header,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-header,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-header,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-header,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-header{color:#fff}.uk-card-primary.uk-card-body .uk-nav-primary .uk-nav-divider,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-divider,.uk-card-secondary.uk-card-body .uk-nav-primary .uk-nav-divider,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-divider,.uk-light .uk-nav-primary .uk-nav-divider,.uk-offcanvas-bar .uk-nav-primary .uk-nav-divider,.uk-overlay-primary .uk-nav-primary .uk-nav-divider,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-divider,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-divider,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-divider,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-divider{border-top-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-nav-primary .uk-nav-sub a,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-sub a,.uk-card-secondary.uk-card-body .uk-nav-primary .uk-nav-sub a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-sub a,.uk-light .uk-nav-primary .uk-nav-sub a,.uk-offcanvas-bar .uk-nav-primary .uk-nav-sub a,.uk-overlay-primary .uk-nav-primary .uk-nav-sub a,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-nav-primary .uk-nav-sub a:focus,.uk-card-primary.uk-card-body .uk-nav-primary .uk-nav-sub a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-sub a:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-sub a:hover,.uk-card-secondary.uk-card-body .uk-nav-primary .uk-nav-sub a:focus,.uk-card-secondary.uk-card-body .uk-nav-primary .uk-nav-sub a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-sub a:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-sub a:hover,.uk-light .uk-nav-primary .uk-nav-sub a:focus,.uk-light .uk-nav-primary .uk-nav-sub a:hover,.uk-offcanvas-bar .uk-nav-primary .uk-nav-sub a:focus,.uk-offcanvas-bar .uk-nav-primary .uk-nav-sub a:hover,.uk-overlay-primary .uk-nav-primary .uk-nav-sub a:focus,.uk-overlay-primary .uk-nav-primary .uk-nav-sub a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a:focus,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub a:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-nav-primary .uk-nav-sub li.uk-active>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-sub li.uk-active>a,.uk-card-secondary.uk-card-body .uk-nav-primary .uk-nav-sub li.uk-active>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-nav-primary .uk-nav-sub li.uk-active>a,.uk-light .uk-nav-primary .uk-nav-sub li.uk-active>a,.uk-offcanvas-bar .uk-nav-primary .uk-nav-sub li.uk-active>a,.uk-overlay-primary .uk-nav-primary .uk-nav-sub li.uk-active>a,.uk-section-primary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub li.uk-active>a,.uk-section-secondary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub li.uk-active>a,.uk-tile-primary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub li.uk-active>a,.uk-tile-secondary:not(.uk-preserve-color) .uk-nav-primary .uk-nav-sub li.uk-active>a{color:#fff}.uk-card-primary.uk-card-body .uk-navbar-nav>li>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-nav>li>a,.uk-card-secondary.uk-card-body .uk-navbar-nav>li>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-nav>li>a,.uk-light .uk-navbar-nav>li>a,.uk-offcanvas-bar .uk-navbar-nav>li>a,.uk-overlay-primary .uk-navbar-nav>li>a,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-nav>li>a,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-nav>li>a,.uk-tile-primary:not(.uk-preserve-color) .uk-navbar-nav>li>a,.uk-tile-secondary:not(.uk-preserve-color) .uk-navbar-nav>li>a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-navbar-nav>li:hover>a,.uk-card-primary.uk-card-body .uk-navbar-nav>li>a.uk-open,.uk-card-primary.uk-card-body .uk-navbar-nav>li>a:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-nav>li:hover>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-nav>li>a.uk-open,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-nav>li>a:focus,.uk-card-secondary.uk-card-body .uk-navbar-nav>li:hover>a,.uk-card-secondary.uk-card-body .uk-navbar-nav>li>a.uk-open,.uk-card-secondary.uk-card-body .uk-navbar-nav>li>a:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-nav>li:hover>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-nav>li>a.uk-open,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-nav>li>a:focus,.uk-light .uk-navbar-nav>li:hover>a,.uk-light .uk-navbar-nav>li>a.uk-open,.uk-light .uk-navbar-nav>li>a:focus,.uk-offcanvas-bar .uk-navbar-nav>li:hover>a,.uk-offcanvas-bar .uk-navbar-nav>li>a.uk-open,.uk-offcanvas-bar .uk-navbar-nav>li>a:focus,.uk-overlay-primary .uk-navbar-nav>li:hover>a,.uk-overlay-primary .uk-navbar-nav>li>a.uk-open,.uk-overlay-primary .uk-navbar-nav>li>a:focus,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-nav>li:hover>a,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-nav>li>a.uk-open,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-nav>li>a:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-nav>li:hover>a,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-nav>li>a.uk-open,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-nav>li>a:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-navbar-nav>li:hover>a,.uk-tile-primary:not(.uk-preserve-color) .uk-navbar-nav>li>a.uk-open,.uk-tile-primary:not(.uk-preserve-color) .uk-navbar-nav>li>a:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-navbar-nav>li:hover>a,.uk-tile-secondary:not(.uk-preserve-color) .uk-navbar-nav>li>a.uk-open,.uk-tile-secondary:not(.uk-preserve-color) .uk-navbar-nav>li>a:focus{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-navbar-nav>li>a:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-nav>li>a:active,.uk-card-secondary.uk-card-body .uk-navbar-nav>li>a:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-nav>li>a:active,.uk-light .uk-navbar-nav>li>a:active,.uk-offcanvas-bar .uk-navbar-nav>li>a:active,.uk-overlay-primary .uk-navbar-nav>li>a:active,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-nav>li>a:active,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-nav>li>a:active,.uk-tile-primary:not(.uk-preserve-color) .uk-navbar-nav>li>a:active,.uk-tile-secondary:not(.uk-preserve-color) .uk-navbar-nav>li>a:active{color:#fff}.uk-card-primary.uk-card-body .uk-navbar-nav>li.uk-active>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-nav>li.uk-active>a,.uk-card-secondary.uk-card-body .uk-navbar-nav>li.uk-active>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-nav>li.uk-active>a,.uk-light .uk-navbar-nav>li.uk-active>a,.uk-offcanvas-bar .uk-navbar-nav>li.uk-active>a,.uk-overlay-primary .uk-navbar-nav>li.uk-active>a,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-nav>li.uk-active>a,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-nav>li.uk-active>a,.uk-tile-primary:not(.uk-preserve-color) .uk-navbar-nav>li.uk-active>a,.uk-tile-secondary:not(.uk-preserve-color) .uk-navbar-nav>li.uk-active>a{color:#fff}.uk-card-primary.uk-card-body .uk-navbar-item,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-item,.uk-card-secondary.uk-card-body .uk-navbar-item,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-item,.uk-light .uk-navbar-item,.uk-offcanvas-bar .uk-navbar-item,.uk-overlay-primary .uk-navbar-item,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-item,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-item,.uk-tile-primary:not(.uk-preserve-color) .uk-navbar-item,.uk-tile-secondary:not(.uk-preserve-color) .uk-navbar-item{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-navbar-toggle,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-toggle,.uk-card-secondary.uk-card-body .uk-navbar-toggle,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-toggle,.uk-light .uk-navbar-toggle,.uk-offcanvas-bar .uk-navbar-toggle,.uk-overlay-primary .uk-navbar-toggle,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-toggle,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-toggle,.uk-tile-primary:not(.uk-preserve-color) .uk-navbar-toggle,.uk-tile-secondary:not(.uk-preserve-color) .uk-navbar-toggle{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-navbar-toggle.uk-open,.uk-card-primary.uk-card-body .uk-navbar-toggle:focus,.uk-card-primary.uk-card-body .uk-navbar-toggle:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-toggle.uk-open,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-toggle:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-navbar-toggle:hover,.uk-card-secondary.uk-card-body .uk-navbar-toggle.uk-open,.uk-card-secondary.uk-card-body .uk-navbar-toggle:focus,.uk-card-secondary.uk-card-body .uk-navbar-toggle:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-toggle.uk-open,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-toggle:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-navbar-toggle:hover,.uk-light .uk-navbar-toggle.uk-open,.uk-light .uk-navbar-toggle:focus,.uk-light .uk-navbar-toggle:hover,.uk-offcanvas-bar .uk-navbar-toggle.uk-open,.uk-offcanvas-bar .uk-navbar-toggle:focus,.uk-offcanvas-bar .uk-navbar-toggle:hover,.uk-overlay-primary .uk-navbar-toggle.uk-open,.uk-overlay-primary .uk-navbar-toggle:focus,.uk-overlay-primary .uk-navbar-toggle:hover,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-toggle.uk-open,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-toggle:focus,.uk-section-primary:not(.uk-preserve-color) .uk-navbar-toggle:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-toggle.uk-open,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-toggle:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-navbar-toggle:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-navbar-toggle.uk-open,.uk-tile-primary:not(.uk-preserve-color) .uk-navbar-toggle:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-navbar-toggle:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-navbar-toggle.uk-open,.uk-tile-secondary:not(.uk-preserve-color) .uk-navbar-toggle:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-navbar-toggle:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-subnav>*>:first-child,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav>*>:first-child,.uk-card-secondary.uk-card-body .uk-subnav>*>:first-child,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav>*>:first-child,.uk-light .uk-subnav>*>:first-child,.uk-offcanvas-bar .uk-subnav>*>:first-child,.uk-overlay-primary .uk-subnav>*>:first-child,.uk-section-primary:not(.uk-preserve-color) .uk-subnav>*>:first-child,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav>*>:first-child,.uk-tile-primary:not(.uk-preserve-color) .uk-subnav>*>:first-child,.uk-tile-secondary:not(.uk-preserve-color) .uk-subnav>*>:first-child{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-subnav>*>a:focus,.uk-card-primary.uk-card-body .uk-subnav>*>a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav>*>a:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav>*>a:hover,.uk-card-secondary.uk-card-body .uk-subnav>*>a:focus,.uk-card-secondary.uk-card-body .uk-subnav>*>a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav>*>a:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav>*>a:hover,.uk-light .uk-subnav>*>a:focus,.uk-light .uk-subnav>*>a:hover,.uk-offcanvas-bar .uk-subnav>*>a:focus,.uk-offcanvas-bar .uk-subnav>*>a:hover,.uk-overlay-primary .uk-subnav>*>a:focus,.uk-overlay-primary .uk-subnav>*>a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-subnav>*>a:focus,.uk-section-primary:not(.uk-preserve-color) .uk-subnav>*>a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav>*>a:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav>*>a:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-subnav>*>a:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-subnav>*>a:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-subnav>*>a:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-subnav>*>a:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-subnav>.uk-active>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav>.uk-active>a,.uk-card-secondary.uk-card-body .uk-subnav>.uk-active>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav>.uk-active>a,.uk-light .uk-subnav>.uk-active>a,.uk-offcanvas-bar .uk-subnav>.uk-active>a,.uk-overlay-primary .uk-subnav>.uk-active>a,.uk-section-primary:not(.uk-preserve-color) .uk-subnav>.uk-active>a,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav>.uk-active>a,.uk-tile-primary:not(.uk-preserve-color) .uk-subnav>.uk-active>a,.uk-tile-secondary:not(.uk-preserve-color) .uk-subnav>.uk-active>a{color:#fff}.uk-card-primary.uk-card-body .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-card-secondary.uk-card-body .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-light .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-offcanvas-bar .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-overlay-primary .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-section-primary:not(.uk-preserve-color) .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-tile-primary:not(.uk-preserve-color) .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before,.uk-tile-secondary:not(.uk-preserve-color) .uk-subnav-divider>:nth-child(n+2):not(.uk-first-column)::before{border-left-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-subnav-pill>*>:first-child,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav-pill>*>:first-child,.uk-card-secondary.uk-card-body .uk-subnav-pill>*>:first-child,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav-pill>*>:first-child,.uk-light .uk-subnav-pill>*>:first-child,.uk-offcanvas-bar .uk-subnav-pill>*>:first-child,.uk-overlay-primary .uk-subnav-pill>*>:first-child,.uk-section-primary:not(.uk-preserve-color) .uk-subnav-pill>*>:first-child,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav-pill>*>:first-child,.uk-tile-primary:not(.uk-preserve-color) .uk-subnav-pill>*>:first-child,.uk-tile-secondary:not(.uk-preserve-color) .uk-subnav-pill>*>:first-child{background-color:transparent;color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-subnav-pill>*>a:focus,.uk-card-primary.uk-card-body .uk-subnav-pill>*>a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav-pill>*>a:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav-pill>*>a:hover,.uk-card-secondary.uk-card-body .uk-subnav-pill>*>a:focus,.uk-card-secondary.uk-card-body .uk-subnav-pill>*>a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav-pill>*>a:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav-pill>*>a:hover,.uk-light .uk-subnav-pill>*>a:focus,.uk-light .uk-subnav-pill>*>a:hover,.uk-offcanvas-bar .uk-subnav-pill>*>a:focus,.uk-offcanvas-bar .uk-subnav-pill>*>a:hover,.uk-overlay-primary .uk-subnav-pill>*>a:focus,.uk-overlay-primary .uk-subnav-pill>*>a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-subnav-pill>*>a:focus,.uk-section-primary:not(.uk-preserve-color) .uk-subnav-pill>*>a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav-pill>*>a:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav-pill>*>a:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-subnav-pill>*>a:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-subnav-pill>*>a:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-subnav-pill>*>a:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-subnav-pill>*>a:hover{background-color:rgba(255,255,255,.1);color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-subnav-pill>*>a:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav-pill>*>a:active,.uk-card-secondary.uk-card-body .uk-subnav-pill>*>a:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav-pill>*>a:active,.uk-light .uk-subnav-pill>*>a:active,.uk-offcanvas-bar .uk-subnav-pill>*>a:active,.uk-overlay-primary .uk-subnav-pill>*>a:active,.uk-section-primary:not(.uk-preserve-color) .uk-subnav-pill>*>a:active,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav-pill>*>a:active,.uk-tile-primary:not(.uk-preserve-color) .uk-subnav-pill>*>a:active,.uk-tile-secondary:not(.uk-preserve-color) .uk-subnav-pill>*>a:active{background-color:rgba(255,255,255,.1);color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-subnav-pill>.uk-active>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav-pill>.uk-active>a,.uk-card-secondary.uk-card-body .uk-subnav-pill>.uk-active>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav-pill>.uk-active>a,.uk-light .uk-subnav-pill>.uk-active>a,.uk-offcanvas-bar .uk-subnav-pill>.uk-active>a,.uk-overlay-primary .uk-subnav-pill>.uk-active>a,.uk-section-primary:not(.uk-preserve-color) .uk-subnav-pill>.uk-active>a,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav-pill>.uk-active>a,.uk-tile-primary:not(.uk-preserve-color) .uk-subnav-pill>.uk-active>a,.uk-tile-secondary:not(.uk-preserve-color) .uk-subnav-pill>.uk-active>a{background-color:#fff;color:#666}.uk-card-primary.uk-card-body .uk-subnav>.uk-disabled>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-subnav>.uk-disabled>a,.uk-card-secondary.uk-card-body .uk-subnav>.uk-disabled>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-subnav>.uk-disabled>a,.uk-light .uk-subnav>.uk-disabled>a,.uk-offcanvas-bar .uk-subnav>.uk-disabled>a,.uk-overlay-primary .uk-subnav>.uk-disabled>a,.uk-section-primary:not(.uk-preserve-color) .uk-subnav>.uk-disabled>a,.uk-section-secondary:not(.uk-preserve-color) .uk-subnav>.uk-disabled>a,.uk-tile-primary:not(.uk-preserve-color) .uk-subnav>.uk-disabled>a,.uk-tile-secondary:not(.uk-preserve-color) .uk-subnav>.uk-disabled>a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-breadcrumb>*>*,.uk-card-primary>:not([class*=uk-card-media]) .uk-breadcrumb>*>*,.uk-card-secondary.uk-card-body .uk-breadcrumb>*>*,.uk-card-secondary>:not([class*=uk-card-media]) .uk-breadcrumb>*>*,.uk-light .uk-breadcrumb>*>*,.uk-offcanvas-bar .uk-breadcrumb>*>*,.uk-overlay-primary .uk-breadcrumb>*>*,.uk-section-primary:not(.uk-preserve-color) .uk-breadcrumb>*>*,.uk-section-secondary:not(.uk-preserve-color) .uk-breadcrumb>*>*,.uk-tile-primary:not(.uk-preserve-color) .uk-breadcrumb>*>*,.uk-tile-secondary:not(.uk-preserve-color) .uk-breadcrumb>*>*{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-breadcrumb>*>:focus,.uk-card-primary.uk-card-body .uk-breadcrumb>*>:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-breadcrumb>*>:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-breadcrumb>*>:hover,.uk-card-secondary.uk-card-body .uk-breadcrumb>*>:focus,.uk-card-secondary.uk-card-body .uk-breadcrumb>*>:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-breadcrumb>*>:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-breadcrumb>*>:hover,.uk-light .uk-breadcrumb>*>:focus,.uk-light .uk-breadcrumb>*>:hover,.uk-offcanvas-bar .uk-breadcrumb>*>:focus,.uk-offcanvas-bar .uk-breadcrumb>*>:hover,.uk-overlay-primary .uk-breadcrumb>*>:focus,.uk-overlay-primary .uk-breadcrumb>*>:hover,.uk-section-primary:not(.uk-preserve-color) .uk-breadcrumb>*>:focus,.uk-section-primary:not(.uk-preserve-color) .uk-breadcrumb>*>:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-breadcrumb>*>:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-breadcrumb>*>:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-breadcrumb>*>:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-breadcrumb>*>:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-breadcrumb>*>:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-breadcrumb>*>:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-breadcrumb>:last-child>*,.uk-card-primary>:not([class*=uk-card-media]) .uk-breadcrumb>:last-child>*,.uk-card-secondary.uk-card-body .uk-breadcrumb>:last-child>*,.uk-card-secondary>:not([class*=uk-card-media]) .uk-breadcrumb>:last-child>*,.uk-light .uk-breadcrumb>:last-child>*,.uk-offcanvas-bar .uk-breadcrumb>:last-child>*,.uk-overlay-primary .uk-breadcrumb>:last-child>*,.uk-section-primary:not(.uk-preserve-color) .uk-breadcrumb>:last-child>*,.uk-section-secondary:not(.uk-preserve-color) .uk-breadcrumb>:last-child>*,.uk-tile-primary:not(.uk-preserve-color) .uk-breadcrumb>:last-child>*,.uk-tile-secondary:not(.uk-preserve-color) .uk-breadcrumb>:last-child>*{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-card-secondary.uk-card-body .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-light .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-offcanvas-bar .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-overlay-primary .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-section-primary:not(.uk-preserve-color) .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-section-secondary:not(.uk-preserve-color) .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-tile-primary:not(.uk-preserve-color) .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before,.uk-tile-secondary:not(.uk-preserve-color) .uk-breadcrumb>:nth-child(n+2):not(.uk-first-column)::before{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-pagination>*>*,.uk-card-primary>:not([class*=uk-card-media]) .uk-pagination>*>*,.uk-card-secondary.uk-card-body .uk-pagination>*>*,.uk-card-secondary>:not([class*=uk-card-media]) .uk-pagination>*>*,.uk-light .uk-pagination>*>*,.uk-offcanvas-bar .uk-pagination>*>*,.uk-overlay-primary .uk-pagination>*>*,.uk-section-primary:not(.uk-preserve-color) .uk-pagination>*>*,.uk-section-secondary:not(.uk-preserve-color) .uk-pagination>*>*,.uk-tile-primary:not(.uk-preserve-color) .uk-pagination>*>*,.uk-tile-secondary:not(.uk-preserve-color) .uk-pagination>*>*{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-pagination>*>:focus,.uk-card-primary.uk-card-body .uk-pagination>*>:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-pagination>*>:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-pagination>*>:hover,.uk-card-secondary.uk-card-body .uk-pagination>*>:focus,.uk-card-secondary.uk-card-body .uk-pagination>*>:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-pagination>*>:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-pagination>*>:hover,.uk-light .uk-pagination>*>:focus,.uk-light .uk-pagination>*>:hover,.uk-offcanvas-bar .uk-pagination>*>:focus,.uk-offcanvas-bar .uk-pagination>*>:hover,.uk-overlay-primary .uk-pagination>*>:focus,.uk-overlay-primary .uk-pagination>*>:hover,.uk-section-primary:not(.uk-preserve-color) .uk-pagination>*>:focus,.uk-section-primary:not(.uk-preserve-color) .uk-pagination>*>:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-pagination>*>:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-pagination>*>:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-pagination>*>:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-pagination>*>:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-pagination>*>:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-pagination>*>:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-pagination>.uk-active>*,.uk-card-primary>:not([class*=uk-card-media]) .uk-pagination>.uk-active>*,.uk-card-secondary.uk-card-body .uk-pagination>.uk-active>*,.uk-card-secondary>:not([class*=uk-card-media]) .uk-pagination>.uk-active>*,.uk-light .uk-pagination>.uk-active>*,.uk-offcanvas-bar .uk-pagination>.uk-active>*,.uk-overlay-primary .uk-pagination>.uk-active>*,.uk-section-primary:not(.uk-preserve-color) .uk-pagination>.uk-active>*,.uk-section-secondary:not(.uk-preserve-color) .uk-pagination>.uk-active>*,.uk-tile-primary:not(.uk-preserve-color) .uk-pagination>.uk-active>*,.uk-tile-secondary:not(.uk-preserve-color) .uk-pagination>.uk-active>*{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-pagination>.uk-disabled>*,.uk-card-primary>:not([class*=uk-card-media]) .uk-pagination>.uk-disabled>*,.uk-card-secondary.uk-card-body .uk-pagination>.uk-disabled>*,.uk-card-secondary>:not([class*=uk-card-media]) .uk-pagination>.uk-disabled>*,.uk-light .uk-pagination>.uk-disabled>*,.uk-offcanvas-bar .uk-pagination>.uk-disabled>*,.uk-overlay-primary .uk-pagination>.uk-disabled>*,.uk-section-primary:not(.uk-preserve-color) .uk-pagination>.uk-disabled>*,.uk-section-secondary:not(.uk-preserve-color) .uk-pagination>.uk-disabled>*,.uk-tile-primary:not(.uk-preserve-color) .uk-pagination>.uk-disabled>*,.uk-tile-secondary:not(.uk-preserve-color) .uk-pagination>.uk-disabled>*{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-tab::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-tab::before,.uk-card-secondary.uk-card-body .uk-tab::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-tab::before,.uk-light .uk-tab::before,.uk-offcanvas-bar .uk-tab::before,.uk-overlay-primary .uk-tab::before,.uk-section-primary:not(.uk-preserve-color) .uk-tab::before,.uk-section-secondary:not(.uk-preserve-color) .uk-tab::before,.uk-tile-primary:not(.uk-preserve-color) .uk-tab::before,.uk-tile-secondary:not(.uk-preserve-color) .uk-tab::before{border-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-tab>*>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-tab>*>a,.uk-card-secondary.uk-card-body .uk-tab>*>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-tab>*>a,.uk-light .uk-tab>*>a,.uk-offcanvas-bar .uk-tab>*>a,.uk-overlay-primary .uk-tab>*>a,.uk-section-primary:not(.uk-preserve-color) .uk-tab>*>a,.uk-section-secondary:not(.uk-preserve-color) .uk-tab>*>a,.uk-tile-primary:not(.uk-preserve-color) .uk-tab>*>a,.uk-tile-secondary:not(.uk-preserve-color) .uk-tab>*>a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-tab>*>a:focus,.uk-card-primary.uk-card-body .uk-tab>*>a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-tab>*>a:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-tab>*>a:hover,.uk-card-secondary.uk-card-body .uk-tab>*>a:focus,.uk-card-secondary.uk-card-body .uk-tab>*>a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-tab>*>a:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-tab>*>a:hover,.uk-light .uk-tab>*>a:focus,.uk-light .uk-tab>*>a:hover,.uk-offcanvas-bar .uk-tab>*>a:focus,.uk-offcanvas-bar .uk-tab>*>a:hover,.uk-overlay-primary .uk-tab>*>a:focus,.uk-overlay-primary .uk-tab>*>a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-tab>*>a:focus,.uk-section-primary:not(.uk-preserve-color) .uk-tab>*>a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-tab>*>a:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-tab>*>a:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-tab>*>a:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-tab>*>a:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-tab>*>a:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-tab>*>a:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-tab>.uk-active>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-tab>.uk-active>a,.uk-card-secondary.uk-card-body .uk-tab>.uk-active>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-tab>.uk-active>a,.uk-light .uk-tab>.uk-active>a,.uk-offcanvas-bar .uk-tab>.uk-active>a,.uk-overlay-primary .uk-tab>.uk-active>a,.uk-section-primary:not(.uk-preserve-color) .uk-tab>.uk-active>a,.uk-section-secondary:not(.uk-preserve-color) .uk-tab>.uk-active>a,.uk-tile-primary:not(.uk-preserve-color) .uk-tab>.uk-active>a,.uk-tile-secondary:not(.uk-preserve-color) .uk-tab>.uk-active>a{color:#fff;border-color:#fff}.uk-card-primary.uk-card-body .uk-tab>.uk-disabled>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-tab>.uk-disabled>a,.uk-card-secondary.uk-card-body .uk-tab>.uk-disabled>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-tab>.uk-disabled>a,.uk-light .uk-tab>.uk-disabled>a,.uk-offcanvas-bar .uk-tab>.uk-disabled>a,.uk-overlay-primary .uk-tab>.uk-disabled>a,.uk-section-primary:not(.uk-preserve-color) .uk-tab>.uk-disabled>a,.uk-section-secondary:not(.uk-preserve-color) .uk-tab>.uk-disabled>a,.uk-tile-primary:not(.uk-preserve-color) .uk-tab>.uk-disabled>a,.uk-tile-secondary:not(.uk-preserve-color) .uk-tab>.uk-disabled>a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-slidenav,.uk-card-primary>:not([class*=uk-card-media]) .uk-slidenav,.uk-card-secondary.uk-card-body .uk-slidenav,.uk-card-secondary>:not([class*=uk-card-media]) .uk-slidenav,.uk-light .uk-slidenav,.uk-offcanvas-bar .uk-slidenav,.uk-overlay-primary .uk-slidenav,.uk-section-primary:not(.uk-preserve-color) .uk-slidenav,.uk-section-secondary:not(.uk-preserve-color) .uk-slidenav,.uk-tile-primary:not(.uk-preserve-color) .uk-slidenav,.uk-tile-secondary:not(.uk-preserve-color) .uk-slidenav{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-slidenav:focus,.uk-card-primary.uk-card-body .uk-slidenav:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-slidenav:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-slidenav:hover,.uk-card-secondary.uk-card-body .uk-slidenav:focus,.uk-card-secondary.uk-card-body .uk-slidenav:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-slidenav:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-slidenav:hover,.uk-light .uk-slidenav:focus,.uk-light .uk-slidenav:hover,.uk-offcanvas-bar .uk-slidenav:focus,.uk-offcanvas-bar .uk-slidenav:hover,.uk-overlay-primary .uk-slidenav:focus,.uk-overlay-primary .uk-slidenav:hover,.uk-section-primary:not(.uk-preserve-color) .uk-slidenav:focus,.uk-section-primary:not(.uk-preserve-color) .uk-slidenav:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-slidenav:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-slidenav:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-slidenav:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-slidenav:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-slidenav:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-slidenav:hover{color:rgba(255,255,255,.95)}.uk-card-primary.uk-card-body .uk-slidenav:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-slidenav:active,.uk-card-secondary.uk-card-body .uk-slidenav:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-slidenav:active,.uk-light .uk-slidenav:active,.uk-offcanvas-bar .uk-slidenav:active,.uk-overlay-primary .uk-slidenav:active,.uk-section-primary:not(.uk-preserve-color) .uk-slidenav:active,.uk-section-secondary:not(.uk-preserve-color) .uk-slidenav:active,.uk-tile-primary:not(.uk-preserve-color) .uk-slidenav:active,.uk-tile-secondary:not(.uk-preserve-color) .uk-slidenav:active{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-dotnav>*>*,.uk-card-primary>:not([class*=uk-card-media]) .uk-dotnav>*>*,.uk-card-secondary.uk-card-body .uk-dotnav>*>*,.uk-card-secondary>:not([class*=uk-card-media]) .uk-dotnav>*>*,.uk-light .uk-dotnav>*>*,.uk-offcanvas-bar .uk-dotnav>*>*,.uk-overlay-primary .uk-dotnav>*>*,.uk-section-primary:not(.uk-preserve-color) .uk-dotnav>*>*,.uk-section-secondary:not(.uk-preserve-color) .uk-dotnav>*>*,.uk-tile-primary:not(.uk-preserve-color) .uk-dotnav>*>*,.uk-tile-secondary:not(.uk-preserve-color) .uk-dotnav>*>*{background-color:transparent;border-color:rgba(255,255,255,.9)}.uk-card-primary.uk-card-body .uk-dotnav>*>:focus,.uk-card-primary.uk-card-body .uk-dotnav>*>:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-dotnav>*>:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-dotnav>*>:hover,.uk-card-secondary.uk-card-body .uk-dotnav>*>:focus,.uk-card-secondary.uk-card-body .uk-dotnav>*>:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-dotnav>*>:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-dotnav>*>:hover,.uk-light .uk-dotnav>*>:focus,.uk-light .uk-dotnav>*>:hover,.uk-offcanvas-bar .uk-dotnav>*>:focus,.uk-offcanvas-bar .uk-dotnav>*>:hover,.uk-overlay-primary .uk-dotnav>*>:focus,.uk-overlay-primary .uk-dotnav>*>:hover,.uk-section-primary:not(.uk-preserve-color) .uk-dotnav>*>:focus,.uk-section-primary:not(.uk-preserve-color) .uk-dotnav>*>:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-dotnav>*>:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-dotnav>*>:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-dotnav>*>:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-dotnav>*>:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-dotnav>*>:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-dotnav>*>:hover{background-color:rgba(255,255,255,.9);border-color:transparent}.uk-card-primary.uk-card-body .uk-dotnav>*>:active,.uk-card-primary>:not([class*=uk-card-media]) .uk-dotnav>*>:active,.uk-card-secondary.uk-card-body .uk-dotnav>*>:active,.uk-card-secondary>:not([class*=uk-card-media]) .uk-dotnav>*>:active,.uk-light .uk-dotnav>*>:active,.uk-offcanvas-bar .uk-dotnav>*>:active,.uk-overlay-primary .uk-dotnav>*>:active,.uk-section-primary:not(.uk-preserve-color) .uk-dotnav>*>:active,.uk-section-secondary:not(.uk-preserve-color) .uk-dotnav>*>:active,.uk-tile-primary:not(.uk-preserve-color) .uk-dotnav>*>:active,.uk-tile-secondary:not(.uk-preserve-color) .uk-dotnav>*>:active{background-color:rgba(255,255,255,.5);border-color:transparent}.uk-card-primary.uk-card-body .uk-dotnav>.uk-active>*,.uk-card-primary>:not([class*=uk-card-media]) .uk-dotnav>.uk-active>*,.uk-card-secondary.uk-card-body .uk-dotnav>.uk-active>*,.uk-card-secondary>:not([class*=uk-card-media]) .uk-dotnav>.uk-active>*,.uk-light .uk-dotnav>.uk-active>*,.uk-offcanvas-bar .uk-dotnav>.uk-active>*,.uk-overlay-primary .uk-dotnav>.uk-active>*,.uk-section-primary:not(.uk-preserve-color) .uk-dotnav>.uk-active>*,.uk-section-secondary:not(.uk-preserve-color) .uk-dotnav>.uk-active>*,.uk-tile-primary:not(.uk-preserve-color) .uk-dotnav>.uk-active>*,.uk-tile-secondary:not(.uk-preserve-color) .uk-dotnav>.uk-active>*{background-color:rgba(255,255,255,.9);border-color:transparent}.uk-card-primary.uk-card-body .uk-accordion-title,.uk-card-primary>:not([class*=uk-card-media]) .uk-accordion-title,.uk-card-secondary.uk-card-body .uk-accordion-title,.uk-card-secondary>:not([class*=uk-card-media]) .uk-accordion-title,.uk-light .uk-accordion-title,.uk-offcanvas-bar .uk-accordion-title,.uk-overlay-primary .uk-accordion-title,.uk-section-primary:not(.uk-preserve-color) .uk-accordion-title,.uk-section-secondary:not(.uk-preserve-color) .uk-accordion-title,.uk-tile-primary:not(.uk-preserve-color) .uk-accordion-title,.uk-tile-secondary:not(.uk-preserve-color) .uk-accordion-title{color:#fff}.uk-card-primary.uk-card-body .uk-accordion-title:focus,.uk-card-primary.uk-card-body .uk-accordion-title:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-accordion-title:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-accordion-title:hover,.uk-card-secondary.uk-card-body .uk-accordion-title:focus,.uk-card-secondary.uk-card-body .uk-accordion-title:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-accordion-title:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-accordion-title:hover,.uk-light .uk-accordion-title:focus,.uk-light .uk-accordion-title:hover,.uk-offcanvas-bar .uk-accordion-title:focus,.uk-offcanvas-bar .uk-accordion-title:hover,.uk-overlay-primary .uk-accordion-title:focus,.uk-overlay-primary .uk-accordion-title:hover,.uk-section-primary:not(.uk-preserve-color) .uk-accordion-title:focus,.uk-section-primary:not(.uk-preserve-color) .uk-accordion-title:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-accordion-title:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-accordion-title:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-accordion-title:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-accordion-title:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-accordion-title:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-accordion-title:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-iconnav>*>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-iconnav>*>a,.uk-card-secondary.uk-card-body .uk-iconnav>*>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-iconnav>*>a,.uk-light .uk-iconnav>*>a,.uk-offcanvas-bar .uk-iconnav>*>a,.uk-overlay-primary .uk-iconnav>*>a,.uk-section-primary:not(.uk-preserve-color) .uk-iconnav>*>a,.uk-section-secondary:not(.uk-preserve-color) .uk-iconnav>*>a,.uk-tile-primary:not(.uk-preserve-color) .uk-iconnav>*>a,.uk-tile-secondary:not(.uk-preserve-color) .uk-iconnav>*>a{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-iconnav>*>a:focus,.uk-card-primary.uk-card-body .uk-iconnav>*>a:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-iconnav>*>a:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-iconnav>*>a:hover,.uk-card-secondary.uk-card-body .uk-iconnav>*>a:focus,.uk-card-secondary.uk-card-body .uk-iconnav>*>a:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-iconnav>*>a:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-iconnav>*>a:hover,.uk-light .uk-iconnav>*>a:focus,.uk-light .uk-iconnav>*>a:hover,.uk-offcanvas-bar .uk-iconnav>*>a:focus,.uk-offcanvas-bar .uk-iconnav>*>a:hover,.uk-overlay-primary .uk-iconnav>*>a:focus,.uk-overlay-primary .uk-iconnav>*>a:hover,.uk-section-primary:not(.uk-preserve-color) .uk-iconnav>*>a:focus,.uk-section-primary:not(.uk-preserve-color) .uk-iconnav>*>a:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-iconnav>*>a:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-iconnav>*>a:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-iconnav>*>a:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-iconnav>*>a:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-iconnav>*>a:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-iconnav>*>a:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-iconnav>.uk-active>a,.uk-card-primary>:not([class*=uk-card-media]) .uk-iconnav>.uk-active>a,.uk-card-secondary.uk-card-body .uk-iconnav>.uk-active>a,.uk-card-secondary>:not([class*=uk-card-media]) .uk-iconnav>.uk-active>a,.uk-light .uk-iconnav>.uk-active>a,.uk-offcanvas-bar .uk-iconnav>.uk-active>a,.uk-overlay-primary .uk-iconnav>.uk-active>a,.uk-section-primary:not(.uk-preserve-color) .uk-iconnav>.uk-active>a,.uk-section-secondary:not(.uk-preserve-color) .uk-iconnav>.uk-active>a,.uk-tile-primary:not(.uk-preserve-color) .uk-iconnav>.uk-active>a,.uk-tile-secondary:not(.uk-preserve-color) .uk-iconnav>.uk-active>a{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-text-lead,.uk-card-primary>:not([class*=uk-card-media]) .uk-text-lead,.uk-card-secondary.uk-card-body .uk-text-lead,.uk-card-secondary>:not([class*=uk-card-media]) .uk-text-lead,.uk-light .uk-text-lead,.uk-offcanvas-bar .uk-text-lead,.uk-overlay-primary .uk-text-lead,.uk-section-primary:not(.uk-preserve-color) .uk-text-lead,.uk-section-secondary:not(.uk-preserve-color) .uk-text-lead,.uk-tile-primary:not(.uk-preserve-color) .uk-text-lead,.uk-tile-secondary:not(.uk-preserve-color) .uk-text-lead{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-text-meta,.uk-card-primary>:not([class*=uk-card-media]) .uk-text-meta,.uk-card-secondary.uk-card-body .uk-text-meta,.uk-card-secondary>:not([class*=uk-card-media]) .uk-text-meta,.uk-light .uk-text-meta,.uk-offcanvas-bar .uk-text-meta,.uk-overlay-primary .uk-text-meta,.uk-section-primary:not(.uk-preserve-color) .uk-text-meta,.uk-section-secondary:not(.uk-preserve-color) .uk-text-meta,.uk-tile-primary:not(.uk-preserve-color) .uk-text-meta,.uk-tile-secondary:not(.uk-preserve-color) .uk-text-meta{color:rgba(255,255,255,.5)}.uk-card-primary.uk-card-body .uk-text-muted,.uk-card-primary>:not([class*=uk-card-media]) .uk-text-muted,.uk-card-secondary.uk-card-body .uk-text-muted,.uk-card-secondary>:not([class*=uk-card-media]) .uk-text-muted,.uk-light .uk-text-muted,.uk-offcanvas-bar .uk-text-muted,.uk-overlay-primary .uk-text-muted,.uk-section-primary:not(.uk-preserve-color) .uk-text-muted,.uk-section-secondary:not(.uk-preserve-color) .uk-text-muted,.uk-tile-primary:not(.uk-preserve-color) .uk-text-muted,.uk-tile-secondary:not(.uk-preserve-color) .uk-text-muted{color:rgba(255,255,255,.5)!important}.uk-card-primary.uk-card-body .uk-text-emphasis,.uk-card-primary>:not([class*=uk-card-media]) .uk-text-emphasis,.uk-card-secondary.uk-card-body .uk-text-emphasis,.uk-card-secondary>:not([class*=uk-card-media]) .uk-text-emphasis,.uk-light .uk-text-emphasis,.uk-offcanvas-bar .uk-text-emphasis,.uk-overlay-primary .uk-text-emphasis,.uk-section-primary:not(.uk-preserve-color) .uk-text-emphasis,.uk-section-secondary:not(.uk-preserve-color) .uk-text-emphasis,.uk-tile-primary:not(.uk-preserve-color) .uk-text-emphasis,.uk-tile-secondary:not(.uk-preserve-color) .uk-text-emphasis{color:#fff!important}.uk-card-primary.uk-card-body .uk-text-primary,.uk-card-primary>:not([class*=uk-card-media]) .uk-text-primary,.uk-card-secondary.uk-card-body .uk-text-primary,.uk-card-secondary>:not([class*=uk-card-media]) .uk-text-primary,.uk-light .uk-text-primary,.uk-offcanvas-bar .uk-text-primary,.uk-overlay-primary .uk-text-primary,.uk-section-primary:not(.uk-preserve-color) .uk-text-primary,.uk-section-secondary:not(.uk-preserve-color) .uk-text-primary,.uk-tile-primary:not(.uk-preserve-color) .uk-text-primary,.uk-tile-secondary:not(.uk-preserve-color) .uk-text-primary{color:#fff!important}.uk-card-primary.uk-card-body .uk-text-secondary,.uk-card-primary>:not([class*=uk-card-media]) .uk-text-secondary,.uk-card-secondary.uk-card-body .uk-text-secondary,.uk-card-secondary>:not([class*=uk-card-media]) .uk-text-secondary,.uk-light .uk-text-secondary,.uk-offcanvas-bar .uk-text-secondary,.uk-overlay-primary .uk-text-secondary,.uk-section-primary:not(.uk-preserve-color) .uk-text-secondary,.uk-section-secondary:not(.uk-preserve-color) .uk-text-secondary,.uk-tile-primary:not(.uk-preserve-color) .uk-text-secondary,.uk-tile-secondary:not(.uk-preserve-color) .uk-text-secondary{color:#fff!important}.uk-card-primary.uk-card-body .uk-column-divider,.uk-card-primary>:not([class*=uk-card-media]) .uk-column-divider,.uk-card-secondary.uk-card-body .uk-column-divider,.uk-card-secondary>:not([class*=uk-card-media]) .uk-column-divider,.uk-light .uk-column-divider,.uk-offcanvas-bar .uk-column-divider,.uk-overlay-primary .uk-column-divider,.uk-section-primary:not(.uk-preserve-color) .uk-column-divider,.uk-section-secondary:not(.uk-preserve-color) .uk-column-divider,.uk-tile-primary:not(.uk-preserve-color) .uk-column-divider,.uk-tile-secondary:not(.uk-preserve-color) .uk-column-divider{column-rule-color:rgba(255,255,255,.2)}.uk-card-primary.uk-card-body .uk-logo,.uk-card-primary>:not([class*=uk-card-media]) .uk-logo,.uk-card-secondary.uk-card-body .uk-logo,.uk-card-secondary>:not([class*=uk-card-media]) .uk-logo,.uk-light .uk-logo,.uk-offcanvas-bar .uk-logo,.uk-overlay-primary .uk-logo,.uk-section-primary:not(.uk-preserve-color) .uk-logo,.uk-section-secondary:not(.uk-preserve-color) .uk-logo,.uk-tile-primary:not(.uk-preserve-color) .uk-logo,.uk-tile-secondary:not(.uk-preserve-color) .uk-logo{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-logo:focus,.uk-card-primary.uk-card-body .uk-logo:hover,.uk-card-primary>:not([class*=uk-card-media]) .uk-logo:focus,.uk-card-primary>:not([class*=uk-card-media]) .uk-logo:hover,.uk-card-secondary.uk-card-body .uk-logo:focus,.uk-card-secondary.uk-card-body .uk-logo:hover,.uk-card-secondary>:not([class*=uk-card-media]) .uk-logo:focus,.uk-card-secondary>:not([class*=uk-card-media]) .uk-logo:hover,.uk-light .uk-logo:focus,.uk-light .uk-logo:hover,.uk-offcanvas-bar .uk-logo:focus,.uk-offcanvas-bar .uk-logo:hover,.uk-overlay-primary .uk-logo:focus,.uk-overlay-primary .uk-logo:hover,.uk-section-primary:not(.uk-preserve-color) .uk-logo:focus,.uk-section-primary:not(.uk-preserve-color) .uk-logo:hover,.uk-section-secondary:not(.uk-preserve-color) .uk-logo:focus,.uk-section-secondary:not(.uk-preserve-color) .uk-logo:hover,.uk-tile-primary:not(.uk-preserve-color) .uk-logo:focus,.uk-tile-primary:not(.uk-preserve-color) .uk-logo:hover,.uk-tile-secondary:not(.uk-preserve-color) .uk-logo:focus,.uk-tile-secondary:not(.uk-preserve-color) .uk-logo:hover{color:rgba(255,255,255,.7)}.uk-card-primary.uk-card-body .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-card-primary>:not([class*=uk-card-media]) .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-card-secondary.uk-card-body .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-card-secondary>:not([class*=uk-card-media]) .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-light .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-offcanvas-bar .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-overlay-primary .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-section-primary:not(.uk-preserve-color) .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-section-secondary:not(.uk-preserve-color) .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-tile-primary:not(.uk-preserve-color) .uk-logo>:not(.uk-logo-inverse):not(:only-of-type),.uk-tile-secondary:not(.uk-preserve-color) .uk-logo>:not(.uk-logo-inverse):not(:only-of-type){display:none}.uk-card-primary.uk-card-body .uk-logo-inverse,.uk-card-primary>:not([class*=uk-card-media]) .uk-logo-inverse,.uk-card-secondary.uk-card-body .uk-logo-inverse,.uk-card-secondary>:not([class*=uk-card-media]) .uk-logo-inverse,.uk-light .uk-logo-inverse,.uk-offcanvas-bar .uk-logo-inverse,.uk-overlay-primary .uk-logo-inverse,.uk-section-primary:not(.uk-preserve-color) .uk-logo-inverse,.uk-section-secondary:not(.uk-preserve-color) .uk-logo-inverse,.uk-tile-primary:not(.uk-preserve-color) .uk-logo-inverse,.uk-tile-secondary:not(.uk-preserve-color) .uk-logo-inverse{display:inline}.uk-card-primary.uk-card-body .uk-accordion-title::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-accordion-title::before,.uk-card-secondary.uk-card-body .uk-accordion-title::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-accordion-title::before,.uk-light .uk-accordion-title::before,.uk-offcanvas-bar .uk-accordion-title::before,.uk-overlay-primary .uk-accordion-title::before,.uk-section-primary:not(.uk-preserve-color) .uk-accordion-title::before,.uk-section-secondary:not(.uk-preserve-color) .uk-accordion-title::before,.uk-tile-primary:not(.uk-preserve-color) .uk-accordion-title::before,.uk-tile-secondary:not(.uk-preserve-color) .uk-accordion-title::before{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2213%22%20height%3D%2213%22%20viewBox%3D%220%200%2013%2013%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22rgba%28255,%20255,%20255,%200.7%29%22%20width%3D%2213%22%20height%3D%221%22%20x%3D%220%22%20y%3D%226%22%20%2F%3E%0A%20%20%20%20%3Crect%20fill%3D%22rgba%28255,%20255,%20255,%200.7%29%22%20width%3D%221%22%20height%3D%2213%22%20x%3D%226%22%20y%3D%220%22%20%2F%3E%0A%3C%2Fsvg%3E")}.uk-card-primary.uk-card-body .uk-open>.uk-accordion-title::before,.uk-card-primary>:not([class*=uk-card-media]) .uk-open>.uk-accordion-title::before,.uk-card-secondary.uk-card-body .uk-open>.uk-accordion-title::before,.uk-card-secondary>:not([class*=uk-card-media]) .uk-open>.uk-accordion-title::before,.uk-light .uk-open>.uk-accordion-title::before,.uk-offcanvas-bar .uk-open>.uk-accordion-title::before,.uk-overlay-primary .uk-open>.uk-accordion-title::before,.uk-section-primary:not(.uk-preserve-color) .uk-open>.uk-accordion-title::before,.uk-section-secondary:not(.uk-preserve-color) .uk-open>.uk-accordion-title::before,.uk-tile-primary:not(.uk-preserve-color) .uk-open>.uk-accordion-title::before,.uk-tile-secondary:not(.uk-preserve-color) .uk-open>.uk-accordion-title::before{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%2213%22%20height%3D%2213%22%20viewBox%3D%220%200%2013%2013%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%20%20%3Crect%20fill%3D%22rgba%28255,%20255,%20255,%200.7%29%22%20width%3D%2213%22%20height%3D%221%22%20x%3D%220%22%20y%3D%226%22%20%2F%3E%0A%3C%2Fsvg%3E")}@media print{*,::after,::before{background:0 0!important;color:#000!important;box-shadow:none!important;text-shadow:none!important}a,a:visited{text-decoration:underline}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}} \ No newline at end of file diff --git a/outputs/preview-assets/uikit.min.js b/outputs/preview-assets/uikit.min.js deleted file mode 100644 index 91e2441..0000000 --- a/outputs/preview-assets/uikit.min.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! UIkit 3.3.3 | http://www.getuikit.com | (c) 2014 - 2019 YOOtheme | MIT License */ - -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define("uikit",e):(t=t||self).UIkit=e()}(this,function(){"use strict";var t=Object.prototype,i=t.hasOwnProperty;function u(t,e){return i.call(t,e)}var e={},n=/([a-z\d])([A-Z])/g;function d(t){return t in e||(e[t]=t.replace(n,"$1-$2").toLowerCase()),e[t]}var r=/-(\w)/g;function f(t){return t.replace(r,o)}function o(t,e){return e?e.toUpperCase():""}function p(t){return t.length?o(0,t.charAt(0))+t.slice(1):""}var s=String.prototype,a=s.startsWith||function(t){return 0===this.lastIndexOf(t,0)};function w(t,e){return a.call(t,e)}var h=s.endsWith||function(t){return this.substr(-t.length)===t};function c(t,e){return h.call(t,e)}function l(t,e){return~this.indexOf(t,e)}var g=Array.prototype,m=s.includes||l,v=g.includes||l;function b(t,e){return t&&(D(t)?m:v).call(t,e)}var x=g.findIndex||function(t){for(var e=arguments,i=0;ie.left&&t.tope.top}function nt(t,e){return t.x<=e.right&&t.x>=e.left&&t.y<=e.bottom&&t.y>=e.top}var rt={ratio:function(t,e,i){var n,r="width"===e?"height":"width";return(n={})[r]=t[e]?Math.round(i*t[r]/t[e]):t[r],n[e]=i,n},contain:function(i,n){var r=this;return J(i=G({},i),function(t,e){return i=i[e]>n[e]?r.ratio(i,e,n[e]):i}),i},cover:function(i,n){var r=this;return J(i=this.contain(i,n),function(t,e){return i=i[e]+~-]/,Et=/([!>+~-])(?=\s+[!>+~-]|\s*$)/g;function At(t){return D(t)&&t.match(Tt)}var _t=/.*?[^\\](?:,|$)/g;var Ct=Element.prototype,Nt=Ct.matches||Ct.webkitMatchesSelector||Ct.msMatchesSelector;function Mt(t,e){return V(t).some(function(t){return Nt.call(t,e)})}var zt=Ct.closest||function(t){var e=this;do{if(Mt(e,t))return e}while(e=Pt(e))};function Dt(t,e){return w(e,">")&&(e=e.slice(1)),N(t)?zt.call(t,e):V(t).map(function(t){return Dt(t,e)}).filter(Boolean)}function Pt(t){return(t=W(t))&&N(t.parentNode)&&t.parentNode}function Bt(t,e){for(var i=[];t=Pt(t);)e&&!Mt(t,e)||i.push(t);return i}function Ot(t,e){var i=(t=W(t))?V(t.children):[];return e?i.filter(function(t){return Mt(t,e)}):i}var Ht=window.CSS&&CSS.escape||function(t){return t.replace(/([^\x7f-\uFFFF\w-])/g,function(t){return"\\"+t})};function Lt(t){return D(t)?Ht.call(null,t):""}var Ft={area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,menuitem:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0};function jt(t){return V(t).some(function(t){return Ft[t.tagName.toLowerCase()]})}function Wt(t){return V(t).some(function(t){return t.offsetWidth||t.offsetHeight||t.getClientRects().length})}var Vt="input,select,textarea,button";function Rt(t){return V(t).some(function(t){return Mt(t,Vt)})}function qt(t,e){return V(t).filter(function(t){return Mt(t,e)})}function Yt(t,e){return D(e)?Mt(t,e)||Dt(t,e):t===e||(A(e)?e.documentElement:W(e)).contains(W(t))}function Ut(){for(var t=[],e=arguments.length;e--;)t[e]=arguments[e];var i,n,r=Zt(t),o=r[0],s=r[1],a=r[2],h=r[3],u=r[4];return o=ie(o),1]*>/,Ee=/^<(\w+)\s*\/?>(?:<\/\1>)?$/;function Ae(t){var e=Ee.exec(t);if(e)return document.createElement(e[1]);var i=document.createElement("div");return Te.test(t)?i.insertAdjacentHTML("beforeend",t.trim()):i.textContent=t,1n[u]){var i=p[s]/2,r="center"===l[a]?-g[s]/2:0;return"center"===c[a]&&(o(i,r)||o(-i,-r))||o(t,e)}function o(e,t){var i=m[h]+e+t-2*d[a];if(i>=n[h]&&i+p[s]<=n[u])return m[h]=i,["element","target"].forEach(function(t){f[t][a]=e?f[t][a]===ri[s][1]?ri[s][2]:ri[s][1]:f[t][a]}),!0}})})}return si(t,m),f}function si(i,n){if(!n)return ai(i);var r=si(i),o=Ve(i,"position");["left","top"].forEach(function(t){if(t in n){var e=Ve(i,t);Ve(i,t,n[t]-r[t]+j("absolute"===o&&"auto"===e?hi(i)[t]:e))}})}function ai(t){if(!t)return{};var e,i,n=R(t),r=n.pageYOffset,o=n.pageXOffset;if(E(t)){var s=t.innerHeight,a=t.innerWidth;return{top:r,left:o,height:s,width:a,bottom:r+s,right:o+a}}Wt(t)||"none"!==Ve(t,"display")||(e=ot(t,"style"),i=ot(t,"hidden"),ot(t,{style:(e||"")+";display:block !important;",hidden:null}));var h=(t=W(t)).getBoundingClientRect();return H(e)||ot(t,{style:e,hidden:i}),{height:h.height,width:h.width,top:h.top+r,left:h.left+o,bottom:h.bottom+r,right:h.right+o}}function hi(t,e){e=e||W(t).offsetParent||R(t).document.documentElement;var i=si(t),n=si(e);return{top:i.top-n.top-j(Ve(e,"borderTopWidth")),left:i.left-n.left-j(Ve(e,"borderLeftWidth"))}}function ui(t){var e=[0,0];t=W(t);do{if(e[0]+=t.offsetTop,e[1]+=t.offsetLeft,"fixed"===Ve(t,"position")){var i=R(t);return e[0]+=i.pageYOffset,e[1]+=i.pageXOffset,e}}while(t=t.offsetParent);return e}var ci=di("height"),li=di("width");function di(n){var r=p(n);return function(t,e){if(H(e)){if(E(t))return t["inner"+r];if(A(t)){var i=t.documentElement;return Math.max(i["offset"+r],i["scroll"+r])}return(e="auto"===(e=Ve(t=W(t),n))?t["offset"+r]:j(e)||0)-fi(t,n)}Ve(t,n,e||0===e?+e+fi(t,n)+"px":"")}}function fi(i,t,e){return void 0===e&&(e="border-box"),Ve(i,"boxSizing")===e?ri[t].slice(1).map(p).reduce(function(t,e){return t+j(Ve(i,"padding"+e))+j(Ve(i,"border"+e+"Width"))},0):0}function pi(o,s,a,h){J(ri,function(t,e){var i=t[0],n=t[1],r=t[2];s[i]===r?o[n]+=a[e]*h:"center"===s[i]&&(o[n]+=a[e]*h/2)})}function gi(t){var e=/left|center|right/,i=/top|center|bottom/;return 1===(t=(t||"").split(" ")).length&&(t=e.test(t[0])?t.concat("center"):i.test(t[0])?["center"].concat(t):["center","center"]),{x:e.test(t[0])?t[0]:"center",y:i.test(t[1])?t[1]:"center"}}function mi(t,e,i){var n=(t||"").split(" "),r=n[0],o=n[1];return{x:r?j(r)*(c(r,"%")?e/100:1):0,y:o?j(o)*(c(o,"%")?i/100:1):0}}function vi(t){switch(t){case"left":return"right";case"right":return"left";case"top":return"bottom";case"bottom":return"top";default:return t}}function wi(t,e,i){return void 0===e&&(e="width"),void 0===i&&(i=window),B(t)?+t:c(t,"vh")?bi(ci(R(i)),t):c(t,"vw")?bi(li(R(i)),t):c(t,"%")?bi(ai(i)[e],t):j(t)}function bi(t,e){return t*j(e)/100}var xi={reads:[],writes:[],read:function(t){return this.reads.push(t),$i(),t},write:function(t){return this.writes.push(t),$i(),t},clear:function(t){return Si(this.reads,t)||Si(this.writes,t)},flush:yi};function yi(t){void 0===t&&(t=1),Ii(xi.reads),Ii(xi.writes.splice(0,xi.writes.length)),xi.scheduled=!1,(xi.reads.length||xi.writes.length)&&$i(t+1)}var ki=5;function $i(t){if(!xi.scheduled){if(xi.scheduled=!0,kiMath.round(si(t).height)}).reverse();return n.length?n:[i]}function Fi(t){return t===Wi(t)?window:t}function ji(t){return Li(t,/auto|scroll|hidden/)}function Wi(t){var e=R(t).document;return e.scrollingElement||e.documentElement}Mi.prototype.isVideo=function(){return this.isYoutube()||this.isVimeo()||this.isHTML5()},Mi.prototype.isHTML5=function(){return"VIDEO"===this.el.tagName},Mi.prototype.isIFrame=function(){return"IFRAME"===this.el.tagName},Mi.prototype.isYoutube=function(){return this.isIFrame()&&!!this.el.src.match(/\/\/.*?youtube(-nocookie)?\.[a-z]+\/(watch\?v=[^&\s]+|embed)|youtu\.be\/.*/)},Mi.prototype.isVimeo=function(){return this.isIFrame()&&!!this.el.src.match(/vimeo\.com\/video\/.*/)},Mi.prototype.enableApi=function(){var e=this;if(this.ready)return this.ready;var i,r=this.isYoutube(),o=this.isVimeo();return r||o?this.ready=new se(function(t){var n;Gt(e.el,"load",function(){if(r){var t=function(){return Di(e.el,{event:"listening",id:e.id})};i=setInterval(t,100),t()}}),(n=function(t){return r&&t.id===e.id&&"onReady"===t.event||o&&Number(t.player_id)===e.id},new se(function(i){Gt(window,"message",function(t,e){return i(e)},!1,function(t){var e=t.data;if(e&&D(e)){try{e=JSON.parse(e)}catch(t){return}return e&&n(e)}})})).then(function(){t(),i&&clearInterval(i)}),ot(e.el,"src",e.el.src+(b(e.el.src,"?")?"&":"?")+(r?"enablejsapi=1":"api=1&player_id="+e.id))}):se.resolve()},Mi.prototype.play=function(){var t=this;if(this.isVideo())if(this.isIFrame())this.enableApi().then(function(){return Di(t.el,{func:"playVideo",method:"play"})});else if(this.isHTML5())try{var e=this.el.play();e&&e.catch(et)}catch(t){}},Mi.prototype.pause=function(){var t=this;this.isVideo()&&(this.isIFrame()?this.enableApi().then(function(){return Di(t.el,{func:"pauseVideo",method:"pause"})}):this.isHTML5()&&this.el.pause())},Mi.prototype.mute=function(){var t=this;this.isVideo()&&(this.isIFrame()?this.enableApi().then(function(){return Di(t.el,{func:"mute",method:"setVolume",value:0})}):this.isHTML5()&&(this.el.muted=!0,ot(this.el,"muted","")))};var Vi="IntersectionObserver"in window?window.IntersectionObserver:function(){function t(e,t){var i=this;void 0===t&&(t={});var n=t.rootMargin;void 0===n&&(n="0 0"),this.targets=[];var r,o=(n||"0 0").split(" ").map(j),s=o[0],a=o[1];this.offsetTop=s,this.offsetLeft=a,this.apply=function(){r=r||requestAnimationFrame(function(){return setTimeout(function(){var t=i.takeRecords();t.length&&e(t,i),r=!1})})},this.off=Ut(window,"scroll resize load",this.apply,{passive:!0,capture:!0})}return t.prototype.takeRecords=function(){var i=this;return this.targets.filter(function(t){var e=Pi(t.target,i.offsetTop,i.offsetLeft);if(null===t.isIntersecting||e^t.isIntersecting)return t.isIntersecting=e,!0})},t.prototype.observe=function(t){this.targets.push({target:t,isIntersecting:null}),this.apply()},t.prototype.disconnect=function(){this.targets=[],this.off()},t}();function Ri(t){return!(!w(t,"uk-")&&!w(t,"data-uk-"))&&f(t.replace("data-uk-","").replace("uk-",""))}function qi(t){this._init(t)}var Yi,Ui,Xi,Gi,Ki,Ji,Zi,Qi,tn;function en(t,e){if(t)for(var i in t)t[i]._connected&&t[i]._callUpdate(e)}function nn(t,e){var i={},n=t.args;void 0===n&&(n=[]);var r=t.props;void 0===r&&(r={});var o=t.el;if(!r)return i;for(var s in r){var a=d(s),h=ht(o,a);if(!H(h)){if(h=r[s]===Boolean&&""===h||an(r[s],h),"target"===a&&(!h||w(h,"_")))continue;i[s]=h}}var u=Ni(ht(o,e),n);for(var c in u){var l=f(c);void 0!==r[l]&&(i[l]=an(r[l],u[c]))}return i}function rn(n,r,o){Object.defineProperty(n,r,{enumerable:!0,get:function(){var t=n._computeds,e=n.$props,i=n.$el;return u(t,r)||(t[r]=(o.get||o).call(n,e,i)),t[r]},set:function(t){var e=n._computeds;e[r]=o.set?o.set.call(n,t):t,H(e[r])&&delete e[r]}})}function on(e,i,n){T(i)||(i={name:n,handler:i});var t=i.name,r=i.el,o=i.handler,s=i.capture,a=i.passive,h=i.delegate,u=i.filter,c=i.self;r=$(r)?r.call(e):r||e.$el,k(r)?r.forEach(function(t){return on(e,G({},i,{el:t}),n)}):!r||u&&!u.call(e)||e._events.push(Ut(r,t,h?D(h)?h:h.call(e):null,D(o)?e[o]:o.bind(e),{passive:a,capture:s,self:c}))}function sn(t,e){return t.every(function(t){return!t||!u(t,e)})}function an(t,e){return t===Boolean?L(e):t===Number?F(e):"list"===t?q(e):t?t(e):e}qi.util=Object.freeze({__proto__:null,ajax:le,getImage:de,transition:Je,Transition:Ze,animate:ei,Animation:ni,attr:ot,hasAttr:st,removeAttr:at,data:ht,addClass:ze,removeClass:De,removeClasses:Pe,replaceClass:Be,hasClass:Oe,toggleClass:He,positionAt:oi,offset:si,position:hi,offsetPosition:ui,height:ci,width:li,boxModelAdjust:fi,flipPosition:vi,toPx:wi,ready:fe,index:pe,getIndex:ge,empty:me,html:ve,prepend:function(e,t){return(e=Ce(e)).hasChildNodes()?ye(t,function(t){return e.insertBefore(t,e.firstChild)}):we(e,t)},append:we,before:be,after:xe,remove:ke,wrapAll:$e,wrapInner:Ie,unwrap:Se,fragment:Ae,apply:_e,$:Ce,$$:Ne,isIE:ut,isRtl:ct,hasTouch:ft,pointerDown:pt,pointerMove:gt,pointerUp:mt,pointerEnter:vt,pointerLeave:wt,pointerCancel:bt,on:Ut,off:Xt,once:Gt,trigger:Kt,createEvent:Jt,toEventTargets:ie,isTouch:ne,getEventPos:re,fastdom:xi,isVoidElement:jt,isVisible:Wt,selInput:Vt,isInput:Rt,filter:qt,within:Yt,hasOwn:u,hyphenate:d,camelize:f,ucfirst:p,startsWith:w,endsWith:c,includes:b,findIndex:y,isArray:k,isFunction:$,isObject:I,isPlainObject:T,isWindow:E,isDocument:A,isJQuery:_,isNode:C,isElement:N,isNodeCollection:M,isBoolean:z,isString:D,isNumber:P,isNumeric:B,isEmpty:O,isUndefined:H,toBoolean:L,toNumber:F,toFloat:j,toNode:W,toNodes:V,toWindow:R,toList:q,toMs:Y,isEqual:U,swap:X,assign:G,last:K,each:J,sortBy:Z,uniqueBy:Q,clamp:tt,noop:et,intersectRect:it,pointInRect:nt,Dimensions:rt,MouseTracker:Ti,mergeOptions:Ci,parseOptions:Ni,Player:Mi,Promise:se,Deferred:oe,IntersectionObserver:Vi,query:xt,queryAll:yt,find:$t,findAll:It,matches:Mt,closest:Dt,parent:Pt,parents:Bt,children:Ot,escape:Lt,css:Ve,getStyles:Re,getStyle:qe,getCssVar:Ue,propName:Ge,isInView:Pi,scrollTop:Bi,scrollIntoView:Oi,scrolledOver:Hi,scrollParents:Li,getViewport:Fi}),qi.data="__uikit__",qi.prefix="uk-",qi.options={},qi.version="3.3.3",Xi=(Yi=qi).data,Yi.use=function(t){if(!t.installed)return t.call(null,this),t.installed=!0,this},Yi.mixin=function(t,e){(e=(D(e)?Yi.component(e):e)||this).options=Ci(e.options,t)},Yi.extend=function(t){t=t||{};function e(t){this._init(t)}return((e.prototype=Object.create(this.prototype)).constructor=e).options=Ci(this.options,t),e.super=this,e.extend=this.extend,e},Yi.update=function(t,e){Bt(t=t?W(t):document.body).reverse().forEach(function(t){return en(t[Xi],e)}),_e(t,function(t){return en(t[Xi],e)})},Object.defineProperty(Yi,"container",{get:function(){return Ui||document.body},set:function(t){Ui=Ce(t)}}),(Gi=qi).prototype._callHook=function(t){var e=this,i=this.$options[t];i&&i.forEach(function(t){return t.call(e)})},Gi.prototype._callConnected=function(){this._connected||(this._data={},this._computeds={},this._initProps(),this._callHook("beforeConnect"),this._connected=!0,this._initWatches(),this._initEvents(),this._initObserver(),this._callHook("connected"),this._callUpdate())},Gi.prototype._callDisconnected=function(){this._connected&&(this._callHook("beforeDisconnect"),this._observer&&(this._observer.disconnect(),this._observer=null),this._unbindEvents(),this._callHook("disconnected"),this._connected=!1)},Gi.prototype._callUpdate=function(t){var o=this;void 0===t&&(t="update");var s=t.type||t;b(["update","resize"],s)&&this._callWatches();var e=this.$options.update,i=this._frames,a=i.reads,h=i.writes;e&&e.forEach(function(t,e){var i=t.read,n=t.write,r=t.events;"update"!==s&&!b(r,s)||(i&&!b(xi.reads,a[e])&&(a[e]=xi.read(function(){var t=o._connected&&i.call(o,o._data,s);!1===t&&n?xi.clear(h[e]):T(t)&&G(o._data,t)})),n&&!b(xi.writes,h[e])&&(h[e]=xi.write(function(){return o._connected&&n.call(o,o._data,s)})))})},Ji=0,(Ki=qi).prototype._init=function(t){(t=t||{}).data=function(t,e){var i=t.data,n=(t.el,e.args),r=e.props;void 0===r&&(r={});if(i=k(i)?O(n)?void 0:i.slice(0,n.length).reduce(function(t,e,i){return T(e)?G(t,e):t[n[i]]=e,t},{}):i)for(var o in i)H(i[o])?delete i[o]:i[o]=r[o]?an(r[o],i[o]):i[o];return i}(t,this.constructor.options),this.$options=Ci(this.constructor.options,t,this),this.$el=null,this.$props={},this._frames={reads:{},writes:{}},this._events=[],this._uid=Ji++,this._initData(),this._initMethods(),this._initComputeds(),this._callHook("created"),t.el&&this.$mount(t.el)},Ki.prototype._initData=function(){var t=this.$options.data;for(var e in void 0===t&&(t={}),t)this.$props[e]=this[e]=t[e]},Ki.prototype._initMethods=function(){var t=this.$options.methods;if(t)for(var e in t)this[e]=t[e].bind(this)},Ki.prototype._initComputeds=function(){var t=this.$options.computed;if(this._computeds={},t)for(var e in t)rn(this,e,t[e])},Ki.prototype._initWatches=function(){var t=this.$options.computed;if(t)for(var e in t){var i=t[e],n=i.watch,r=i.immediate;n&&r&&n.call(this,this[e])}},Ki.prototype._callWatches=function(){var t=this.$options.computed,e=this._computeds;for(var i in e){var n=e[i];delete e[i],t[i].watch&&!U(n,this[i])&&t[i].watch.call(this,this[i],n)}},Ki.prototype._initProps=function(t){var e;for(e in t=t||nn(this.$options,this.$name))H(t[e])||(this.$props[e]=t[e]);var i=[this.$options.computed,this.$options.methods];for(e in this.$props)e in t&&sn(i,e)&&(this[e]=this.$props[e])},Ki.prototype._initEvents=function(){var i=this,t=this.$options.events;t&&t.forEach(function(t){if(u(t,"handler"))on(i,t);else for(var e in t)on(i,t[e],e)})},Ki.prototype._unbindEvents=function(){this._events.forEach(function(t){return t()}),this._events=[]},Ki.prototype._initObserver=function(){var i=this,t=this.$options,n=t.attrs,e=t.props,r=t.el;if(!this._observer&&e&&!1!==n){n=k(n)?n:Object.keys(e),this._observer=new MutationObserver(function(){var e=nn(i.$options,i.$name);n.some(function(t){return!H(e[t])&&e[t]!==i.$props[t]})&&i.$reset()});var o=n.map(function(t){return d(t)}).concat(this.$name);this._observer.observe(r,{attributes:!0,attributeFilter:o.concat(o.map(function(t){return"data-"+t}))})}},Qi=(Zi=qi).data,tn={},Zi.component=function(s,t){var e=d(s);if(s=f(e),!t)return T(tn[s])&&(tn[s]=Zi.extend(tn[s])),tn[s];Zi[s]=function(t,i){for(var e=arguments.length,n=Array(e);e--;)n[e]=arguments[e];var r=Zi.component(s);return r.options.functional?new r({data:T(t)?t:[].concat(n)}):t?Ne(t).map(o)[0]:o(t);function o(t){var e=Zi.getComponent(t,s);if(e){if(!i)return e;e.$destroy()}return new r({el:t,data:i})}};var i=T(t)?G({},t):t.options;return i.name=s,i.install&&i.install(Zi,i,s),Zi._initialized&&!i.functional&&xi.read(function(){return Zi[s]("[uk-"+e+"],[data-uk-"+e+"]")}),tn[s]=T(t)?i:t},Zi.getComponents=function(t){return t&&t[Qi]||{}},Zi.getComponent=function(t,e){return Zi.getComponents(t)[e]},Zi.connect=function(t){if(t[Qi])for(var e in t[Qi])t[Qi][e]._callConnected();for(var i=0;i *",active:!1,animation:[!0],collapsible:!0,multiple:!1,clsOpen:"uk-open",toggle:"> .uk-accordion-title",content:"> .uk-accordion-content",transition:"ease"},computed:{items:{get:function(t,e){return Ne(t.targets,e)},watch:function(t,e){var i=this;if(t.forEach(function(t){return i._toggle(Ce(i.content,t),Oe(t,i.clsOpen))}),!Oe(t,this.clsOpen)){var n=!e&&!1!==this.active&&t[Number(this.active)]||!this.collapsible&&t[0];n&&this.toggle(n,!1)}},immediate:!0}},events:[{name:"click",delegate:function(){return this.targets+" "+this.$props.toggle},handler:function(t){t.preventDefault(),this.toggle(pe(Ne(this.targets+" "+this.$props.toggle,this.$el),t.current))}}],methods:{toggle:function(r,o){var s=this,t=ge(r,this.items),a=qt(this.items,"."+this.clsOpen);(r=this.items[t])&&[r].concat(!this.multiple&&!b(a,r)&&a||[]).forEach(function(e){var t=e===r,i=t&&!Oe(e,s.clsOpen);if(i||!t||s.collapsible||!(a.length<2)){He(e,s.clsOpen,i);var n=e._wrapper?e._wrapper.firstElementChild:Ce(s.content,e);e._wrapper||(e._wrapper=$e(n,"
"),ot(e._wrapper,"hidden",i?"":null)),s._toggle(n,!0),s.toggleElement(e._wrapper,i,o).then(function(){if(Oe(e,s.clsOpen)===i){if(i){var t=Ce(s.$props.toggle,e);!1===o||Pi(t)||Oi(t)}else s._toggle(n,!1);e._wrapper=null,Se(n)}})}})}}},ln={mixins:[hn,un],args:"animation",props:{close:String},data:{animation:[!0],selClose:".uk-alert-close",duration:150,hideProps:G({opacity:0},un.data.hideProps)},events:[{name:"click",delegate:function(){return this.selClose},handler:function(t){t.preventDefault(),this.close()}}],methods:{close:function(){var t=this;this.toggleElement(this.$el).then(function(){return t.$destroy(!0)})}}},dn={args:"autoplay",props:{automute:Boolean,autoplay:Boolean},data:{automute:!1,autoplay:!0},computed:{inView:function(t){return"inview"===t.autoplay}},connected:function(){this.inView&&!st(this.$el,"preload")&&(this.$el.preload="none"),this.player=new Mi(this.$el),this.automute&&this.player.mute()},update:{read:function(){return!!this.player&&{visible:Wt(this.$el)&&"hidden"!==Ve(this.$el,"visibility"),inView:this.inView&&Pi(this.$el)}},write:function(t){var e=t.visible,i=t.inView;!e||this.inView&&!i?this.player.pause():(!0===this.autoplay||this.inView&&i)&&this.player.play()},events:["resize","scroll"]}},fn={mixins:[hn,dn],props:{width:Number,height:Number},data:{automute:!0},update:{read:function(){var t=this.$el,e=function(t){for(;t=Pt(t);)if("static"!==Ve(t,"position"))return t}(t)||t.parentNode,i=e.offsetHeight,n=e.offsetWidth,r=rt.cover({width:this.width||t.naturalWidth||t.videoWidth||t.clientWidth,height:this.height||t.naturalHeight||t.videoHeight||t.clientHeight},{width:n+(n%2?1:0),height:i+(i%2?1:0)});return!(!r.width||!r.height)&&r},write:function(t){var e=t.height,i=t.width;Ve(this.$el,{height:e,width:i})},events:["resize"]}};var pn,gn={props:{pos:String,offset:null,flip:Boolean,clsPos:String},data:{pos:"bottom-"+(ct?"right":"left"),flip:!0,offset:!1,clsPos:""},computed:{pos:function(t){var e=t.pos;return(e+(b(e,"-")?"":"-center")).split("-")},dir:function(){return this.pos[0]},align:function(){return this.pos[1]}},methods:{positionAt:function(t,e,i){var n;Pe(t,this.clsPos+"-(top|bottom|left|right)(-[a-z]+)?"),Ve(t,{top:"",left:""});var r=this.offset,o=this.getAxis();B(r)||(r=(n=Ce(r))?si(n)["x"===o?"left":"top"]-si(e)["x"===o?"right":"bottom"]:0);var s=oi(t,e,"x"===o?vi(this.dir)+" "+this.align:this.align+" "+vi(this.dir),"x"===o?this.dir+" "+this.align:this.align+" "+this.dir,"x"===o?""+("left"===this.dir?-r:r):" "+("top"===this.dir?-r:r),null,this.flip,i).target,a=s.x,h=s.y;this.dir="x"===o?a:h,this.align="x"===o?h:a,He(t,this.clsPos+"-"+this.dir+"-"+this.align,!1===this.offset)},getAxis:function(){return"top"===this.dir||"bottom"===this.dir?"y":"x"}}},mn={mixins:[gn,un],args:"pos",props:{mode:"list",toggle:Boolean,boundary:Boolean,boundaryAlign:Boolean,delayShow:Number,delayHide:Number,clsDrop:String},data:{mode:["click","hover"],toggle:"- *",boundary:window,boundaryAlign:!1,delayShow:0,delayHide:800,clsDrop:!1,animation:["uk-animation-fade"],cls:"uk-open"},computed:{boundary:function(t,e){return xt(t.boundary,e)},clsDrop:function(t){return t.clsDrop||"uk-"+this.$options.name},clsPos:function(){return this.clsDrop}},created:function(){this.tracker=new Ti},connected:function(){ze(this.$el,this.clsDrop);var t=this.$props.toggle;this.toggle=t&&this.$create("toggle",xt(t,this.$el),{target:this.$el,mode:this.mode}),this.toggle||Kt(this.$el,"updatearia")},disconnected:function(){this.isActive()&&(pn=null)},events:[{name:"click",delegate:function(){return"."+this.clsDrop+"-close"},handler:function(t){t.preventDefault(),this.hide(!1)}},{name:"click",delegate:function(){return'a[href^="#"]'},handler:function(t){var e=t.defaultPrevented,i=t.current.hash;e||!i||Yt(i,this.$el)||this.hide(!1)}},{name:"beforescroll",handler:function(){this.hide(!1)}},{name:"toggle",self:!0,handler:function(t,e){t.preventDefault(),this.isToggled()?this.hide(!1):this.show(e,!1)}},{name:"toggleshow",self:!0,handler:function(t,e){t.preventDefault(),this.show(e)}},{name:"togglehide",self:!0,handler:function(t){t.preventDefault(),this.hide()}},{name:vt,filter:function(){return b(this.mode,"hover")},handler:function(t){ne(t)||this.clearTimers()}},{name:wt,filter:function(){return b(this.mode,"hover")},handler:function(t){ne(t)||Mt(this.$el,":hover")||this.hide()}},{name:"beforeshow",self:!0,handler:function(){this.clearTimers(),ni.cancel(this.$el),this.position()}},{name:"show",self:!0,handler:function(){var n=this;(pn=this).tracker.init(),Kt(this.$el,"updatearia");var t=vn(document,"click",function(t){var e=t.defaultPrevented,i=t.target;e||Yt(i,n.$el)||n.toggle&&Yt(i,n.toggle.$el)||n.hide(!1)});Gt(this.$el,"hide",t,{self:!0})}},{name:"beforehide",self:!0,handler:function(){this.clearTimers()}},{name:"hide",handler:function(t){var e=t.target;this.$el===e?(pn=this.isActive()?null:pn,Kt(this.$el,"updatearia"),this.tracker.cancel()):pn=null===pn&&Yt(e,this.$el)&&this.isToggled()?this:pn}},{name:"updatearia",self:!0,handler:function(t,e){t.preventDefault(),this.updateAria(this.$el),(e||this.toggle)&&(ot((e||this.toggle).$el,"aria-expanded",this.isToggled()),He(this.toggle.$el,this.cls,this.isToggled()))}}],update:{write:function(){this.isToggled()&&!ni.inProgress(this.$el)&&this.position()},events:["resize"]},methods:{show:function(t,e){var i=this;if(void 0===t&&(t=this.toggle),void 0===e&&(e=!0),this.isToggled()&&t&&this.toggle&&t.$el!==this.toggle.$el&&this.hide(!1),this.toggle=t,this.clearTimers(),!this.isActive()){if(pn){if(e&&pn.isDelaying)return void(this.showTimer=setTimeout(this.show,10));for(;pn&&!Yt(this.$el,pn.$el);)pn.hide(!1)}this.showTimer=setTimeout(function(){return!i.isToggled()&&i.toggleElement(i.$el,!0)},e&&this.delayShow||0)}},hide:function(t){var e=this;void 0===t&&(t=!0);function i(){return e.toggleNow(e.$el,!1)}var n,r;this.clearTimers(),this.isDelaying=(n=this.$el,r=[],_e(n,function(t){return"static"!==Ve(t,"position")&&r.push(t)}),r).some(function(t){return e.tracker.movesTo(t)}),t&&this.isDelaying?this.hideTimer=setTimeout(this.hide,50):t&&this.delayHide?this.hideTimer=setTimeout(i,this.delayHide):i()},clearTimers:function(){clearTimeout(this.showTimer),clearTimeout(this.hideTimer),this.showTimer=null,this.hideTimer=null,this.isDelaying=!1},isActive:function(){return pn===this},position:function(){Pe(this.$el,this.clsDrop+"-(stack|boundary)"),Ve(this.$el,{top:"",left:"",display:"block"}),He(this.$el,this.clsDrop+"-boundary",this.boundaryAlign);var t=si(this.boundary),e=this.boundaryAlign?t:si(this.toggle.$el);if("justify"===this.align){var i="y"===this.getAxis()?"width":"height";Ve(this.$el,i,e[i])}else this.$el.offsetWidth>Math.max(t.right-e.left,e.right-t.left)&&ze(this.$el,this.clsDrop+"-stack");this.positionAt(this.$el,this.boundaryAlign?this.boundary:this.toggle.$el,this.boundary),Ve(this.$el,"display","")}}};function vn(t,e,i){var n=Gt(t,e,function(){return n=Ut(t,e,i)},!0);return function(){return n()}}var wn={extends:mn},bn={mixins:[hn],args:"target",props:{target:Boolean},data:{target:!1},computed:{input:function(t,e){return Ce(Vt,e)},state:function(){return this.input.nextElementSibling},target:function(t,e){var i=t.target;return i&&(!0===i&&this.input.parentNode===e&&this.input.nextElementSibling||xt(i,e))}},update:function(){var t=this.target,e=this.input;if(t){var i,n=Rt(t)?"value":"textContent",r=t[n],o=e.files&&e.files[0]?e.files[0].name:Mt(e,"select")&&(i=Ne("option",e).filter(function(t){return t.selected})[0])?i.textContent:e.value;r!==o&&(t[n]=o)}},events:[{name:"change",handler:function(){this.$update()}},{name:"reset",el:function(){return Dt(this.$el,"form")},handler:function(){this.$update()}}]},xn={update:{read:function(t){var e=Pi(this.$el);if(!e||t.isInView===e)return!1;t.isInView=e},write:function(){this.$el.src=this.$el.src},events:["scroll","resize"]}},yn={props:{margin:String,firstColumn:Boolean},data:{margin:"uk-margin-small-top",firstColumn:"uk-first-column"},update:{read:function(t){var e=this.$el.children;if(!e.length||!Wt(this.$el))return t.rows=[[]];t.rows=kn(e),t.stacks=!t.rows.some(function(t){return 1=a.bottom-1&&r.top!==a.top){e.push([n]);break}if(r.bottom>a.top){if(r.left=t.offsetHeight)&&Ve(t,"height",e)})},order:5,events:["resize"]}]}:{},Tn={mixins:[Sn],args:"target",props:{target:String,row:Boolean},data:{target:"> *",row:!0,forceHeight:!0},computed:{elements:function(t,e){return Ne(t.target,e)}},update:{read:function(){return{rows:(this.row?kn(this.elements):[this.elements]).map(En)}},write:function(t){t.rows.forEach(function(t){var i=t.heights;return t.elements.forEach(function(t,e){return Ve(t,"minHeight",i[e])})})},events:["resize"]}};function En(t){var e;if(t.length<2)return{heights:[""],elements:t};var i=An(t),n=i.heights,r=i.max,o=t.some(function(t){return t.style.minHeight}),s=t.some(function(t,e){return!t.style.minHeight&&n[e]";zn.lastIndex=0}return Dn[t][e]}(t,e)||t);return(t=Ce(t.substr(t.indexOf("/g,Dn={};function Pn(t){return Math.ceil(Math.max.apply(Math,[0].concat(Ne("[stroke]",t).map(function(t){try{return t.getTotalLength()}catch(t){return 0}}))))}function Bn(t,e){return ot(t,"data-svg")===ot(e,"data-svg")}var On={},Hn={spinner:'',totop:'',marker:'',"close-icon":'',"close-large":'',"navbar-toggle-icon":'',"overlay-icon":'',"pagination-next":'',"pagination-previous":'',"search-icon":'',"search-large":'',"search-navbar":'',"slidenav-next":'',"slidenav-next-large":'',"slidenav-previous":'',"slidenav-previous-large":''},Ln={install:function(r){r.icon.add=function(t,e){var i,n=D(t)?((i={})[t]=e,i):t;J(n,function(t,e){Hn[e]=t,delete On[e]}),r._initialized&&_e(document.body,function(t){return J(r.getComponents(t),function(t){t.$options.isIcon&&t.icon in n&&t.$reset()})})}},extends:Nn,args:"icon",props:["icon"],data:{include:["focusable"]},isIcon:!0,beforeConnect:function(){ze(this.$el,"uk-icon")},methods:{getSvg:function(){var t,e=function(t){if(!Hn[t])return null;On[t]||(On[t]=Ce(Hn[t].trim()));return On[t].cloneNode(!0)}((t=this.icon,ct?X(X(t,"left","right"),"previous","next"):t));return e?se.resolve(e):se.reject("Icon not found.")}}},Fn={args:!1,extends:Ln,data:function(t){return{icon:d(t.constructor.options.name)}},beforeConnect:function(){ze(this.$el,this.$name)}},jn={extends:Fn,beforeConnect:function(){ze(this.$el,"uk-slidenav")},computed:{icon:function(t,e){var i=t.icon;return Oe(e,"uk-slidenav-large")?i+"-large":i}}},Wn={extends:Fn,computed:{icon:function(t,e){var i=t.icon;return Oe(e,"uk-search-icon")&&Bt(e,".uk-search-large").length?"search-large":Bt(e,".uk-search-navbar").length?"search-navbar":i}}},Vn={extends:Fn,computed:{icon:function(){return"close-"+(Oe(this.$el,"uk-close-large")?"large":"icon")}}},Rn={extends:Fn,connected:function(){var e=this;this.svg.then(function(t){return 1!==e.ratio&&Ve(Ce("circle",t),"strokeWidth",1/e.ratio)},et)}};var qn={args:"dataSrc",props:{dataSrc:String,dataSrcset:Boolean,sizes:String,width:Number,height:Number,offsetTop:String,offsetLeft:String,target:String},data:{dataSrc:"",dataSrcset:!1,sizes:!1,width:!1,height:!1,offsetTop:"50vh",offsetLeft:0,target:!1},computed:{cacheKey:function(t){var e=t.dataSrc;return this.$name+"."+e},width:function(t){var e=t.width,i=t.dataWidth;return e||i},height:function(t){var e=t.height,i=t.dataHeight;return e||i},sizes:function(t){var e=t.sizes,i=t.dataSizes;return e||i},isImg:function(t,e){return Zn(e)},target:{get:function(t){var e=t.target;return[this.$el].concat(yt(e,this.$el))},watch:function(){this.observe()}},offsetTop:function(t){return wi(t.offsetTop,"height")},offsetLeft:function(t){return wi(t.offsetLeft,"width")}},connected:function(){tr[this.cacheKey]?Yn(this.$el,tr[this.cacheKey]||this.dataSrc,this.dataSrcset,this.sizes):this.isImg&&this.width&&this.height&&Yn(this.$el,function(t,e,i){var n;i&&(n=rt.ratio({width:t,height:e},"width",wi(Xn(i))),t=n.width,e=n.height);return'data:image/svg+xml;utf8,'}(this.width,this.height,this.sizes)),this.observer=new Vi(this.load,{rootMargin:this.offsetTop+"px "+this.offsetLeft+"px"}),requestAnimationFrame(this.observe)},disconnected:function(){this.observer.disconnect()},update:{read:function(t){var e=this,i=t.image;if(i||"complete"!==document.readyState||this.load(this.observer.takeRecords()),this.isImg)return!1;i&&i.then(function(t){return t&&""!==t.currentSrc&&Yn(e.$el,Qn(t))})},write:function(t){if(this.dataSrcset&&1!==window.devicePixelRatio){var e=Ve(this.$el,"backgroundSize");!e.match(/^(auto\s?)+$/)&&j(e)!==t.bgSize||(t.bgSize=(i=this.dataSrcset,n=this.sizes,r=wi(Xn(n)),(o=(i.match(Jn)||[]).map(j).sort(function(t,e){return t-e})).filter(function(t){return r<=t})[0]||o.pop()||""),Ve(this.$el,"backgroundSize",t.bgSize+"px"))}var i,n,r,o},events:["resize"]},methods:{load:function(t){var e=this;t.some(function(t){return H(t.isIntersecting)||t.isIntersecting})&&(this._data.image=de(this.dataSrc,this.dataSrcset,this.sizes).then(function(t){return Yn(e.$el,Qn(t),t.srcset,t.sizes),tr[e.cacheKey]=Qn(t),t},et),this.observer.disconnect())},observe:function(){var e=this;this._connected&&!this._data.image&&this.target.forEach(function(t){return e.observer.observe(t)})}}};function Yn(t,e,i,n){if(Zn(t))n&&(t.sizes=n),i&&(t.srcset=i),e&&(t.src=e);else if(e){!b(t.style.backgroundImage,e)&&(Ve(t,"backgroundImage","url("+Lt(e)+")"),Kt(t,Jt("load",!1)))}}var Un=/\s*(.*?)\s*(\w+|calc\(.*?\))\s*(?:,|$)/g;function Xn(t){var e,i;for(Un.lastIndex=0;e=Un.exec(t);)if(!e[1]||window.matchMedia(e[1]).matches){e=w(i=e[2],"calc")?i.substring(5,i.length-1).replace(Gn,function(t){return wi(t)}).replace(/ /g,"").match(Kn).reduce(function(t,e){return t+ +e},0):i;break}return e||"100vw"}var Gn=/\d+(?:\w+|%)/g,Kn=/[+-]?(\d+)/g;var Jn=/\s+\d+w\s*(?:,|$)/g;function Zn(t){return"IMG"===t.tagName}function Qn(t){return t.currentSrc||t.src}var tr,er="__test__";try{(tr=window.sessionStorage||{})[er]=1,delete tr[er]}catch(t){tr={}}var ir={props:{media:Boolean},data:{media:!1},computed:{matchMedia:function(){var t=function(t){if(D(t)){if("@"===t[0])t=j(Ue("breakpoint-"+t.substr(1)));else if(isNaN(t))return t}return!(!t||isNaN(t))&&"(min-width: "+t+"px)"}(this.media);return!t||window.matchMedia(t).matches}}};var nr={mixins:[hn,ir],props:{fill:String},data:{fill:"",clsWrapper:"uk-leader-fill",clsHide:"uk-leader-hide",attrFill:"data-fill"},computed:{fill:function(t){return t.fill||Ue("leader-fill-content")}},connected:function(){var t;t=Ie(this.$el,''),this.wrapper=t[0]},disconnected:function(){Se(this.wrapper.childNodes)},update:{read:function(t){var e=t.changed,i=t.width,n=i;return{width:i=Math.floor(this.$el.offsetWidth/2),fill:this.fill,changed:e||n!==i,hide:!this.matchMedia}},write:function(t){He(this.wrapper,this.clsHide,t.hide),t.changed&&(t.changed=!1,ot(this.wrapper,this.attrFill,new Array(t.width).join(t.fill)))},events:["resize"]}},rr={props:{container:Boolean},data:{container:!0},computed:{container:function(t){var e=t.container;return!0===e&&this.$container||e&&Ce(e)}}},or=[],sr={mixins:[hn,rr,un],props:{selPanel:String,selClose:String,escClose:Boolean,bgClose:Boolean,stack:Boolean},data:{cls:"uk-open",escClose:!0,bgClose:!0,overlay:!0,stack:!1},computed:{panel:function(t,e){return Ce(t.selPanel,e)},transitionElement:function(){return this.panel},bgClose:function(t){return t.bgClose&&this.panel}},beforeDisconnect:function(){this.isToggled()&&this.toggleNow(this.$el,!1)},events:[{name:"click",delegate:function(){return this.selClose},handler:function(t){t.preventDefault(),this.hide()}},{name:"toggle",self:!0,handler:function(t){t.defaultPrevented||(t.preventDefault(),this.toggle())}},{name:"beforeshow",self:!0,handler:function(t){if(b(or,this))return!1;!this.stack&&or.length?(se.all(or.map(function(t){return t.hide()})).then(this.show),t.preventDefault()):or.push(this)}},{name:"show",self:!0,handler:function(){var r=this;li(window)-li(document)&&this.overlay&&Ve(document.body,"overflowY","scroll"),ze(document.documentElement,this.clsPage),this.bgClose&&Gt(this.$el,"hide",vn(document,"click",function(t){var e=t.defaultPrevented,i=t.target,n=K(or);e||n!==r||n.overlay&&!Yt(i,n.$el)||Yt(i,n.panel)||n.hide()}),{self:!0}),this.escClose&&Gt(this.$el,"hide",Ut(document,"keydown",function(t){var e=K(or);27===t.keyCode&&e===r&&(t.preventDefault(),e.hide())}),{self:!0})}},{name:"hidden",self:!0,handler:function(){var e=this;or.splice(or.indexOf(this),1),or.length||Ve(document.body,"overflowY",""),or.some(function(t){return t.clsPage===e.clsPage})||De(document.documentElement,this.clsPage)}}],methods:{toggle:function(){return this.isToggled()?this.hide():this.show()},show:function(){var e=this;return this.container&&this.$el.parentNode!==this.container?(we(this.container,this.$el),new se(function(t){return requestAnimationFrame(function(){return e.show().then(t)})})):this.toggleElement(this.$el,!0,ar(this))},hide:function(){return this.toggleElement(this.$el,!1,ar(this))}}};function ar(t){var s=t.transitionElement,a=t._toggle;return function(r,o){return new se(function(i,n){return Gt(r,"show hide",function(){r._reject&&r._reject(),r._reject=n,a(r,o);var t=Gt(s,"transitionstart",function(){Gt(s,"transitionend transitioncancel",i,{self:!0}),clearTimeout(e)},{self:!0}),e=setTimeout(function(){t(),i()},Y(Ve(s,"transitionDuration")))})})}}var hr={install:function(t){var a=t.modal;function e(t,e,i,n){e=G({bgClose:!1,escClose:!0,labels:a.labels},e);var r=a.dialog(t(e),e),o=new oe,s=!1;return Ut(r.$el,"submit","form",function(t){t.preventDefault(),o.resolve(n&&n(r)),s=!0,r.hide()}),Ut(r.$el,"hide",function(){return!s&&i(o)}),o.promise.dialog=r,o.promise}a.dialog=function(t,e){var i=a('
'+t+"
",e);return i.show(),Ut(i.$el,"hidden",function(){return se.resolve().then(function(){return i.$destroy(!0)})},{self:!0}),i},a.alert=function(i,t){return e(function(t){var e=t.labels;return'
'+(D(i)?i:ve(i))+'
"},t,function(t){return t.resolve()})},a.confirm=function(i,t){return e(function(t){var e=t.labels;return'
'+(D(i)?i:ve(i))+'
"},t,function(t){return t.reject()})},a.prompt=function(i,n,t){return e(function(t){var e=t.labels;return'
"},t,function(t){return t.resolve(null)},function(t){return Ce("input",t.$el).value})},a.labels={ok:"Ok",cancel:"Cancel"}},mixins:[sr],data:{clsPage:"uk-modal-page",selPanel:".uk-modal-dialog",selClose:".uk-modal-close, .uk-modal-close-default, .uk-modal-close-outside, .uk-modal-close-full"},events:[{name:"show",self:!0,handler:function(){Oe(this.panel,"uk-margin-auto-vertical")?ze(this.$el,"uk-flex"):Ve(this.$el,"display","block"),ci(this.$el)}},{name:"hidden",self:!0,handler:function(){Ve(this.$el,"display",""),De(this.$el,"uk-flex")}}]};var ur={extends:cn,data:{targets:"> .uk-parent",toggle:"> a",content:"> ul"}},cr={mixins:[hn,Sn],props:{dropdown:String,mode:"list",align:String,offset:Number,boundary:Boolean,boundaryAlign:Boolean,clsDrop:String,delayShow:Number,delayHide:Number,dropbar:Boolean,dropbarMode:String,dropbarAnchor:Boolean,duration:Number},data:{dropdown:".uk-navbar-nav > li",align:ct?"right":"left",clsDrop:"uk-navbar-dropdown",mode:void 0,offset:void 0,delayShow:void 0,delayHide:void 0,boundaryAlign:void 0,flip:"x",boundary:!0,dropbar:!1,dropbarMode:"slide",dropbarAnchor:!1,duration:200,forceHeight:!0,selMinHeight:".uk-navbar-nav > li > a, .uk-navbar-item, .uk-navbar-toggle"},computed:{boundary:function(t,e){var i=t.boundary,n=t.boundaryAlign;return!0===i||n?e:i},dropbarAnchor:function(t,e){return xt(t.dropbarAnchor,e)},pos:function(t){return"bottom-"+t.align},dropbar:{get:function(t){var e=t.dropbar;return e?(e=this._dropbar||xt(e,this.$el)||Ce("+ .uk-navbar-dropbar",this.$el))||(this._dropbar=Ce("
")):null},watch:function(t){ze(t,"uk-navbar-dropbar"),He(t,"uk-navbar-dropbar-slide","slide"===this.dropbarMode)},immediate:!0},dropdowns:{get:function(t,e){return Ne(t.dropdown+" ."+t.clsDrop,e)},watch:function(t){var e=this;this.$create("drop",t.filter(function(t){return!e.getDropdown(t)}),G({},this.$props,{boundary:this.boundary,pos:this.pos,offset:this.dropbar||this.offset}))},immediate:!0}},disconnected:function(){this.dropbar&&ke(this.dropbar),delete this._dropbar},events:[{name:"mouseover",delegate:function(){return this.dropdown},handler:function(t){var e=t.current,i=this.getActive();i&&i.toggle&&!Yt(i.toggle.$el,e)&&!i.tracker.movesTo(i.$el)&&i.hide(!1)}},{name:"mouseleave",el:function(){return this.dropbar},handler:function(){var t=this.getActive();t&&!this.dropdowns.some(function(t){return Mt(t,":hover")})&&t.hide()}},{name:"beforeshow",capture:!0,filter:function(){return this.dropbar},handler:function(){this.dropbar.parentNode||xe(this.dropbarAnchor||this.$el,this.dropbar)}},{name:"show",capture:!0,filter:function(){return this.dropbar},handler:function(t,e){var i=e.$el,n=e.dir;this.clsDrop&&ze(i,this.clsDrop+"-dropbar"),"bottom"===n&&this.transitionTo(i.offsetHeight+j(Ve(i,"marginTop"))+j(Ve(i,"marginBottom")),i)}},{name:"beforehide",filter:function(){return this.dropbar},handler:function(t,e){var i=e.$el,n=this.getActive();Mt(this.dropbar,":hover")&&n&&n.$el===i&&t.preventDefault()}},{name:"hide",filter:function(){return this.dropbar},handler:function(t,e){var i=e.$el,n=this.getActive();(!n||n&&n.$el===i)&&this.transitionTo(0)}}],methods:{getActive:function(){var t=this.dropdowns.map(this.getDropdown).filter(function(t){return t&&t.isActive()})[0];return t&&b(t.mode,"hover")&&Yt(t.toggle.$el,this.$el)&&t},transitionTo:function(t,e){var i=this,n=this.dropbar,r=Wt(n)?ci(n):0;return Ve(e=r"),ze(this.panel.parentNode,this.clsMode)),Ve(document.documentElement,"overflowY",this.overlay?"hidden":""),ze(document.body,this.clsContainer,this.clsFlip),Ve(document.body,"touch-action","pan-y pinch-zoom"),Ve(this.$el,"display","block"),ze(this.$el,this.clsOverlay),ze(this.panel,this.clsSidebarAnimation,"reveal"!==this.mode?this.clsMode:""),ci(document.body),ze(document.body,this.clsContainerAnimation),this.clsContainerAnimation&&(dr().content+=",user-scalable=0")}},{name:"hide",self:!0,handler:function(){De(document.body,this.clsContainerAnimation),Ve(document.body,"touch-action","")}},{name:"hidden",self:!0,handler:function(){var t;this.clsContainerAnimation&&((t=dr()).content=t.content.replace(/,user-scalable=0$/,"")),"reveal"===this.mode&&Se(this.panel),De(this.panel,this.clsSidebarAnimation,this.clsMode),De(this.$el,this.clsOverlay),Ve(this.$el,"display",""),De(document.body,this.clsContainer,this.clsFlip),Ve(document.documentElement,"overflowY","")}},{name:"swipeLeft swipeRight",handler:function(t){this.isToggled()&&c(t.type,"Left")^this.flip&&this.hide()}}]};function dr(){return Ce('meta[name="viewport"]',document.head)||we(document.head,'')}var fr={mixins:[hn],props:{selContainer:String,selContent:String},data:{selContainer:".uk-modal",selContent:".uk-modal-dialog"},computed:{container:function(t,e){return Dt(e,t.selContainer)},content:function(t,e){return Dt(e,t.selContent)}},connected:function(){Ve(this.$el,"minHeight",150)},update:{read:function(){return!(!this.content||!this.container)&&{current:j(Ve(this.$el,"maxHeight")),max:Math.max(150,ci(this.container)-(si(this.content).height-ci(this.$el)))}},write:function(t){var e=t.current,i=t.max;Ve(this.$el,"maxHeight",i),Math.round(e)!==Math.round(i)&&Kt(this.$el,"resize")},events:["resize"]}},pr={props:["width","height"],connected:function(){ze(this.$el,"uk-responsive-width")},update:{read:function(){return!!(Wt(this.$el)&&this.width&&this.height)&&{width:li(this.$el.parentNode),height:this.height}},write:function(t){ci(this.$el,rt.contain({height:this.height,width:this.width},t).height)},events:["resize"]}},gr={props:{duration:Number,offset:Number},data:{duration:1e3,offset:0},methods:{scrollTo:function(t){var e=this;t=t&&Ce(t)||document.body,Kt(this.$el,"beforescroll",[this,t])&&Oi(t,this.$props).then(function(){return Kt(e.$el,"scrolled",[e,t])})}},events:{click:function(t){t.defaultPrevented||(t.preventDefault(),this.scrollTo(Lt(decodeURIComponent(this.$el.hash)).substr(1)))}}},mr={args:"cls",props:{cls:String,target:String,hidden:Boolean,offsetTop:Number,offsetLeft:Number,repeat:Boolean,delay:Number},data:function(){return{cls:!1,target:!1,hidden:!0,offsetTop:0,offsetLeft:0,repeat:!1,delay:0,inViewClass:"uk-scrollspy-inview"}},computed:{elements:{get:function(t,e){var i=t.target;return i?Ne(i,e):[e]},watch:function(t){this.hidden&&Ve(qt(t,":not(."+this.inViewClass+")"),"visibility","hidden")},immediate:!0}},update:[{read:function(t){var i=this;t.update&&this.elements.forEach(function(t){var e=t._ukScrollspyState;(e=e||{cls:ht(t,"uk-scrollspy-class")||i.cls}).show=Pi(t,i.offsetTop,i.offsetLeft),t._ukScrollspyState=e})},write:function(n){var r=this;if(!n.update)return this.$update(),n.update=!0;this.elements.forEach(function(e){function t(t){Ve(e,"visibility",!t&&r.hidden?"hidden":""),He(e,r.inViewClass,t),He(e,i.cls),Kt(e,t?"inview":"outview"),i.inview=t,r.$update(e)}var i=e._ukScrollspyState;!i.show||i.inview||i.queued?!i.show&&i.inview&&!i.queued&&r.repeat&&t(!1):(i.queued=!0,n.promise=(n.promise||se.resolve()).then(function(){return new se(function(t){return setTimeout(t,r.delay)})}).then(function(){t(!0),setTimeout(function(){return i.queued=!1},300)}))})},events:["scroll","resize"]}]},vr={props:{cls:String,closest:String,scroll:Boolean,overflow:Boolean,offset:Number},data:{cls:"uk-active",closest:!1,scroll:!1,overflow:!0,offset:0},computed:{links:{get:function(t,e){return Ne('a[href^="#"]',e).filter(function(t){return t.hash})},watch:function(t){this.scroll&&this.$create("scroll",t,{offset:this.offset||0})},immediate:!0},targets:function(){return Ne(this.links.map(function(t){return Lt(t.hash).substr(1)}).join(","))},elements:function(t){var e=t.closest;return Dt(Ne(this.targets.map(function(t){return'[href="#'+t.id+'"]'}).join(",")),e||"*")}},update:[{read:function(){var i=this,t=this.targets.length;if(!t||!Wt(this.$el))return!1;var e=K(Li(this.targets[0])),n=e.scrollTop,r=e.scrollHeight,o=Fi(e),s=n,a=r-si(o).height,h=!1;return s===a?h=t-1:(this.targets.every(function(t,e){if(hi(t,o).top-i.offset<=0)return h=e,!0}),!1===h&&this.overflow&&(h=0)),{active:h}},write:function(t){var e=t.active;this.links.forEach(function(t){return t.blur()}),De(this.elements,this.cls),!1!==e&&Kt(this.$el,"active",[e,ze(this.elements[e],this.cls)])},events:["scroll","resize"]}]},wr={mixins:[hn,ir],props:{top:null,bottom:Boolean,offset:String,animation:String,clsActive:String,clsInactive:String,clsFixed:String,clsBelow:String,selTarget:String,widthElement:Boolean,showOnUp:Boolean,targetOffset:Number},data:{top:0,bottom:!1,offset:0,animation:"",clsActive:"uk-active",clsInactive:"",clsFixed:"uk-sticky-fixed",clsBelow:"uk-sticky-below",selTarget:"",widthElement:!1,showOnUp:!1,targetOffset:!1},computed:{offset:function(t){return wi(t.offset)},selTarget:function(t,e){var i=t.selTarget;return i&&Ce(i,e)||e},widthElement:function(t,e){return xt(t.widthElement,e)||this.placeholder},isActive:{get:function(){return Oe(this.selTarget,this.clsActive)},set:function(t){t&&!this.isActive?(Be(this.selTarget,this.clsInactive,this.clsActive),Kt(this.$el,"active")):t||Oe(this.selTarget,this.clsInactive)||(Be(this.selTarget,this.clsActive,this.clsInactive),Kt(this.$el,"inactive"))}}},connected:function(){this.placeholder=Ce("+ .uk-sticky-placeholder",this.$el)||Ce('
'),this.isFixed=!1,this.isActive=!1},disconnected:function(){this.isFixed&&(this.hide(),De(this.selTarget,this.clsInactive)),ke(this.placeholder),this.placeholder=null,this.widthElement=null},events:[{name:"load hashchange popstate",el:window,handler:function(){var n=this;if(!1!==this.targetOffset&&location.hash&&0this.topOffset?(ni.cancel(this.$el),ni.out(this.$el,this.animation).then(function(){return i.hide()},et)):this.hide()}else this.isFixed?this.update():this.animation?(ni.cancel(this.$el),this.show(),ni.in(this.$el,this.animation).catch(et)):this.show()},events:["resize","scroll"]}],methods:{show:function(){this.isFixed=!0,this.update(),ot(this.placeholder,"hidden",null)},hide:function(){this.isActive=!1,De(this.$el,this.clsFixed,this.clsBelow),Ve(this.$el,{position:"",top:"",width:""}),ot(this.placeholder,"hidden","")},update:function(){var t=0!==this.top||this.scroll>this.top,e=Math.max(0,this.offset);this.bottom&&this.scroll>this.bottom-this.offset&&(e=this.bottom-this.scroll),Ve(this.$el,{position:"fixed",top:e+"px",width:this.width}),this.isActive=t,He(this.$el,this.clsBelow,this.scroll>this.bottomOffset),ze(this.$el,this.clsFixed)}}};function br(t,e){var i=e.$props,n=e.$el,r=e[t+"Offset"],o=i[t];if(o)return B(o)&&D(o)&&o.match(/^-?\d/)?r+wi(o):si(!0===o?n.parentNode:xt(o,n)).bottom}var xr={mixins:[un],args:"connect",props:{connect:String,toggle:String,active:Number,swiping:Boolean},data:{connect:"~.uk-switcher",toggle:"> * > :first-child",active:0,swiping:!0,cls:"uk-active",clsContainer:"uk-switcher",attrItem:"uk-switcher-item",queued:!0},computed:{connects:{get:function(t,e){return yt(t.connect,e)},watch:function(t){var e=this;t.forEach(function(t){return e.updateAria(t.children)}),this.swiping&&Ve(t,"touch-action","pan-y pinch-zoom")},immediate:!0},children:{get:function(){return V(this.$el.children)},watch:function(t){this.show(qt(t,"."+this.cls)[0]||t[this.active]||t[0])},immediate:!0},toggles:function(t,e){return Ne(t.toggle,e)}},events:[{name:"click",delegate:function(){return this.toggle+":not(.uk-disabled)"},handler:function(e){e.preventDefault(),this.show(Ot(this.$el).filter(function(t){return Yt(e.current,t)})[0])}},{name:"click",el:function(){return this.connects},delegate:function(){return"["+this.attrItem+"],[data-"+this.attrItem+"]"},handler:function(t){t.preventDefault(),this.show(ht(t.current,this.attrItem))}},{name:"swipeRight swipeLeft",filter:function(){return this.swiping},el:function(){return this.connects},handler:function(t){var e=t.type;this.show(c(e,"Left")?"next":"previous")}}],methods:{index:function(){return O(this.connects)?-1:pe(qt(this.connects[0].children,"."+this.cls)[0])},show:function(t){for(var e,i,n=this,r=this.children,o=r.length,s=this.index(),a=0<=s,h="previous"===t?-1:1,u=ge(t,r,s),c=0;c
"}).join("")),e.forEach(function(t,e){return i.children[e].textContent=t}))})}},methods:{start:function(){this.stop(),this.date&&this.units.length&&(this.$update(),this.timer=setInterval(this.$update,1e3))},stop:function(){this.timer&&(clearInterval(this.timer),this.timer=null)}}};var Sr,Tr="uk-animation-target",Er={props:{animation:Number},data:{animation:150},computed:{target:function(){return this.$el}},methods:{animate:function(t){var n=this;!function(){if(Sr)return;(Sr=we(document.head," - - - - - - - -
- - -
- - - - -
- - - - - - - - -
-
-
- -

测试箱主

-

欢迎提问

-
- - - - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@测试箱主提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -

@测试箱主 以前回答过的问题 (2)

- -
- -
-
- 查看回答 -
- ⭐ - 2026-10-19 16:08:02 -
-

置顶的问题 -

-
- -
-
- 查看回答 -
- - 2026-10-19 15:08:02 -
-

普通问题 -

-
- -
- - -
- -
-
- 无更多留言 -

-
-
- - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/preview-box-light.html b/outputs/preview-box-light.html deleted file mode 100644 index 315813e..0000000 --- a/outputs/preview-box-light.html +++ /dev/null @@ -1,865 +0,0 @@ - - - - - - 预览 16:08 · - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - - - - - - -
-
-
- -

测试箱主

-

欢迎提问

-
- - - - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@测试箱主提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -

@测试箱主 以前回答过的问题 (2)

- -
- -
-
- 查看回答 -
- ⭐ - 2026-10-19 16:08:02 -
-

置顶的问题 -

-
- -
-
- 查看回答 -
- - 2026-10-19 15:08:02 -
-

普通问题 -

-
- -
- - -
- -
-
- 无更多留言 -

-
-
- - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/preview-defaultbg-with.html b/outputs/preview-defaultbg-with.html deleted file mode 100644 index ae8c5bb..0000000 --- a/outputs/preview-defaultbg-with.html +++ /dev/null @@ -1,1505 +0,0 @@ - - - - - - 预览 10:58 · - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - - - - - - -
-
-
- -

测试箱主

-

欢迎提问

-
- - - - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@测试箱主提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -

@测试箱主 以前回答过的问题 (2)

- -
- -
-
- 查看回答 -
- ⭐ - 2026-10-19 10:58:34 -
-

置顶的问题 -

-
- -
-
- 查看回答 -
- - 2026-10-19 09:58:34 -
-

普通问题 -

-
- -
- - -
- -
-
- 无更多留言 -

-
-
- - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/preview-defaultbg-without.html b/outputs/preview-defaultbg-without.html deleted file mode 100644 index ae8c5bb..0000000 --- a/outputs/preview-defaultbg-without.html +++ /dev/null @@ -1,1505 +0,0 @@ - - - - - - 预览 10:58 · - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - - - - - - -
-
-
- -

测试箱主

-

欢迎提问

-
- - - - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@测试箱主提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -

@测试箱主 以前回答过的问题 (2)

- -
- -
-
- 查看回答 -
- ⭐ - 2026-10-19 10:58:34 -
-

置顶的问题 -

-
- -
-
- 查看回答 -
- - 2026-10-19 09:58:34 -
-

普通问题 -

-
- -
- - -
- -
-
- 无更多留言 -

-
-
- - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/preview-explore-dark.html b/outputs/preview-explore-dark.html deleted file mode 100644 index ac452ff..0000000 --- a/outputs/preview-explore-dark.html +++ /dev/null @@ -1,1000 +0,0 @@ - - - - - - 预览 16:08 · 投票 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - - - -
-

探索用户标签

- -
-

此处展示已开启“在首页展示我的标签”的用户。该功能默认关闭,若希望自己的提问箱在这里被发现,请前往 个人设置 手动开启。

-
- -
- - -
- -
-
-
-

随机推荐

- -
-
- -
暂无推荐用户
- -
-
- - - -
-

热门标签

-
- - #杂谈 - - #学习 - - #音乐 - -
-
- - -
-
-
-
- - -
-
-
- -
-
-
- - -
- - -
-
-
-
- avatar -
-
-
- 小狼 -
-
- 随便问,友善一点,看到都会回。 -
-
- - 无标签 - -
-
-
-
-
- - -
-
-
-
- avatar -
-
-
- Kitsune -
-
- 摄影 / 音乐 / 一点点代码 -
-
- - 无标签 - -
-
-
-
-
- - -
-
-
-
- avatar -
-
-
- 残响 -
-
- 长期潜水,偶尔冒泡。 -
-
- - 无标签 - -
-
-
-
-
- - -
-
-
-
- avatar -
-
-
- Mika -
-
- 想认识更多有趣的人。 -
-
- - 无标签 - -
-
-
-
-
- -
- -
    - -
  • 上一页
  • - -
  • 1 / 1
  • - -
  • 下一页
  • - -
- -
- - -
- - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/preview-explore-light.html b/outputs/preview-explore-light.html deleted file mode 100644 index 79b40bc..0000000 --- a/outputs/preview-explore-light.html +++ /dev/null @@ -1,1000 +0,0 @@ - - - - - - 预览 16:08 · 投票 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - - - -
-

探索用户标签

- -
-

此处展示已开启“在首页展示我的标签”的用户。该功能默认关闭,若希望自己的提问箱在这里被发现,请前往 个人设置 手动开启。

-
- -
- - -
- -
-
-
-

随机推荐

- -
-
- -
暂无推荐用户
- -
-
- - - -
-

热门标签

-
- - #杂谈 - - #学习 - - #音乐 - -
-
- - -
-
-
-
- - -
-
-
- -
-
-
- - -
- - -
-
-
-
- avatar -
-
-
- 小狼 -
-
- 随便问,友善一点,看到都会回。 -
-
- - 无标签 - -
-
-
-
-
- - -
-
-
-
- avatar -
-
-
- Kitsune -
-
- 摄影 / 音乐 / 一点点代码 -
-
- - 无标签 - -
-
-
-
-
- - -
-
-
-
- avatar -
-
-
- 残响 -
-
- 长期潜水,偶尔冒泡。 -
-
- - 无标签 - -
-
-
-
-
- - -
-
-
-
- avatar -
-
-
- Mika -
-
- 想认识更多有趣的人。 -
-
- - 无标签 - -
-
-
-
-
- -
- -
    - -
  • 上一页
  • - -
  • 1 / 1
  • - -
  • 下一页
  • - -
- -
- - -
- - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/preview-home-dark.html b/outputs/preview-home-dark.html deleted file mode 100644 index 3307990..0000000 --- a/outputs/preview-home-dark.html +++ /dev/null @@ -1,932 +0,0 @@ - - - - - - 预览 16:08 · TamaBox - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - -
-

狼的提问箱 | 匿名提问箱

-

- 一个无需注册即可提问的匿名提问箱。不用下载 App,不用绑定社交账号,连邮箱都不必是真的。 -

- -
- -
-
-

匿名也能问

-

不需要账号、不需要真实邮箱,打开就能写。想收到回复再留邮箱即可。

-
-
-

不止一问一答

-

问题二维码、私聊、投票、追加提问、Markdown 排版,原版没有的这里都有。

-
-
-

箱子归你

-

头像、背景、二维码配色都能自己调,还能设置审核严格度和收集时段。

-
-
- -
-

域名迁移

-

- 本站已切换到 box.shiroko.one,原域名将在 2026 年 12 月 24 日到期, - 期间通过 301 转发指向新域名,到期后旧域名将彻底不再使用。 -

-

- 审核服务已切换到阿里云,审核速度提升且误伤更少;同时允许用户调整审核严格度,在不碰底线的前提下 - 可以设置不接受辱骂、广告等信息(即「不听坏话模式」)。 -

-
- -
-

一点提醒

-
    -
  • 本站唯一指定认证站长:小狼,注意不要被骗。
  • -
  • 本站提倡友好、友善的发言,请遵守社区规则。无论匿名与否,都要为自己的发言负责。
  • -
  • 不允许垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义、灌水提问,涉政或不良言论将直接删除。
  • -
-

- 想先试试功能,可以到站长的提问箱 - 小狼的提问箱 提问或提意见。 -

-
- -
- 近期更新 -
    -
  • 2026.9 新增审核严格度选项,增加提问、投票、私聊的总开关,可设置定时收集、定量收集或关闭功能,前后端显示也更易用了。
  • -
  • 2026.8 新增投票功能、追加提问、多语言、Markdown 显示、置顶显示等,详情见这里。
  • -
  • 2026.8 放弃使用 recaptcha 等对国内用户不友好的验证方式,更新密码加密方式,修复图片上传等一系列问题。
  • -
  • 2026.5 增加私聊功能,可连续对话,详见介绍;另有提问箱卡片分享。
  • -
  • 2026.2 新增用户展示和标签功能,可给自己贴 5 个标签并选择是否在探索页展示,详情见狼的公告板。
  • -
-
- -
-

提问箱信息统计

-
-
- 1284 - 注册用户 -
-
- 56321 - 累计提问 -
-
- 31208 - 已获回答 -
-
-

- 最后提问:2026-10-19 10:12:44
- 最后回答:2026-10-19 09:58:02
- 数据更新于:2026-10-19 10:00:00 -

-
- -
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/preview-home-light.html b/outputs/preview-home-light.html deleted file mode 100644 index 55232ff..0000000 --- a/outputs/preview-home-light.html +++ /dev/null @@ -1,932 +0,0 @@ - - - - - - 预览 16:08 · TamaBox - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - -
-

狼的提问箱 | 匿名提问箱

-

- 一个无需注册即可提问的匿名提问箱。不用下载 App,不用绑定社交账号,连邮箱都不必是真的。 -

- -
- -
-
-

匿名也能问

-

不需要账号、不需要真实邮箱,打开就能写。想收到回复再留邮箱即可。

-
-
-

不止一问一答

-

问题二维码、私聊、投票、追加提问、Markdown 排版,原版没有的这里都有。

-
-
-

箱子归你

-

头像、背景、二维码配色都能自己调,还能设置审核严格度和收集时段。

-
-
- -
-

域名迁移

-

- 本站已切换到 box.shiroko.one,原域名将在 2026 年 12 月 24 日到期, - 期间通过 301 转发指向新域名,到期后旧域名将彻底不再使用。 -

-

- 审核服务已切换到阿里云,审核速度提升且误伤更少;同时允许用户调整审核严格度,在不碰底线的前提下 - 可以设置不接受辱骂、广告等信息(即「不听坏话模式」)。 -

-
- -
-

一点提醒

-
    -
  • 本站唯一指定认证站长:小狼,注意不要被骗。
  • -
  • 本站提倡友好、友善的发言,请遵守社区规则。无论匿名与否,都要为自己的发言负责。
  • -
  • 不允许垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义、灌水提问,涉政或不良言论将直接删除。
  • -
-

- 想先试试功能,可以到站长的提问箱 - 小狼的提问箱 提问或提意见。 -

-
- -
- 近期更新 -
    -
  • 2026.9 新增审核严格度选项,增加提问、投票、私聊的总开关,可设置定时收集、定量收集或关闭功能,前后端显示也更易用了。
  • -
  • 2026.8 新增投票功能、追加提问、多语言、Markdown 显示、置顶显示等,详情见这里。
  • -
  • 2026.8 放弃使用 recaptcha 等对国内用户不友好的验证方式,更新密码加密方式,修复图片上传等一系列问题。
  • -
  • 2026.5 增加私聊功能,可连续对话,详见介绍;另有提问箱卡片分享。
  • -
  • 2026.2 新增用户展示和标签功能,可给自己贴 5 个标签并选择是否在探索页展示,详情见狼的公告板。
  • -
-
- -
-

提问箱信息统计

-
-
- 1284 - 注册用户 -
-
- 56321 - 累计提问 -
-
- 31208 - 已获回答 -
-
-

- 最后提问:2026-10-19 10:12:44
- 最后回答:2026-10-19 09:58:02
- 数据更新于:2026-10-19 10:00:00 -

-
- -
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/preview-profile-account-light.html b/outputs/preview-profile-account-light.html deleted file mode 100644 index 9efbf03..0000000 --- a/outputs/preview-profile-account-light.html +++ /dev/null @@ -1,2022 +0,0 @@ - - - - - - 预览 16:08 · 设置 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
-

裁剪图片

-
-
-
- -
-

-
- -
-
- - -
- - - - - -
- - -
-
- - - -
-

基础资料

-
- - -
-
- - -
-
- -
- -
-
- - -
-
- -
- -
-
- - - 选择访客访问你的提问箱时显示的语言(后台页面跟随此设置) -
-
- - -
-
- -
-
- - -
-

头像与背景

-
-
- - - - 1:1 -
- - -
- 裁剪框已锁定为 1:1 正方形 -
-
- - - - 16:9 -
- - -
- 裁剪框已锁定为 16:9 宽屏 -
-
- -
- -
-
- - -
-
    -
  • - 二维码样式 -
    -
    -
    -
    - -
    - -
    -
    - -
    - -
    -
    - -
    - -
    -
    - - 仅在启用时生效,否则颜色由系统自动生成 -
    -
    - -
    - - 0.35 - -
    -
    -
    - -
    - - 0.8 - -
    -
    -
    - -
    - - -
    - 选择图片后预览将即时更新 -
    -
    -
    -
    -

    实时预览

    -
    -
    -
    - 预设主题 -
    - - - - -
    -
    -
    - -
    -

    调整仅用于预览,最终效果点击「保存修改」后生效

    -
    -
    -
    -
    -
  • -
-
- -
-
- - -
-
    -
  • - 社交信息设置 -
    - 勾选后填写主页地址(无需 https://),未勾选的项将不会在前台显示。 - - - - - -
    -
  • -
-
- -
-
- - -
-

修改密码

-
- - -
-
- - -
-

保存后当前会话立即失效,需使用新密码重新登录

-
-
- -
-
-
- - -
-

标签设置

-
- -
- - -
-
- -
-
- -
-
-
- - -
-

账号设置

- -
- - -
-

主题外观

-
- -

仅对提问箱页面生效,站内其它页面保持默认外观。

-
- - - - - - -
-
- -
-
-
- - -
- - - - - - - - - - - - - - - - - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/preview-profile-dark.html b/outputs/preview-profile-dark.html deleted file mode 100644 index fcba1de..0000000 --- a/outputs/preview-profile-dark.html +++ /dev/null @@ -1,2022 +0,0 @@ - - - - - - 预览 16:08 · 设置 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
-

裁剪图片

-
-
-
- -
-

-
- -
-
- - -
- - - - - -
- - - - - - - - - - - - - - - -
-
-
-
- - 防骚扰设置 -
-
- -
-
- -
-
-
- -
- - - -
-
- - 全局屏蔽词 -
-

屏蔽词对提问、私聊(对方发给你的消息)与投票提交内容生效;三个设置页同步显示同一份设置,任意一处修改即时生效。

- - -
-
- -
-
-
- - - - - -
-
- - 审核严格度 - -

对提问、投票、私聊三类内容同时生效。这里调节的是「审核命中之后是否拦截」,不影响屏蔽词——屏蔽词始终直接拒绝。

-

底线拦截(所有档位强制生效,不可关闭):涉政、暴恐、色情,以及审核服务自定义词库的命中。

- - - - - - - - - - - - - - - - - - - - - - - - -
档位拦截范围
畅所欲言仅拦截底线内容,其余一律放行(误伤最少)。
正常水平底线内容,以及置信度不低于 90% 的辱骂、违禁内容。
不听坏话底线内容,以及辱骂、违禁、垃圾信息、广告、灌水、无意义、不良场景内容(不限置信度);同时采信审核服务给出的整体拦截建议。最严格档,拦截面不小于升级前。
- -
- - - -
- -
-
- 未设置时跟随站点默认档位。 -
-
- -
- -
-
-
- - -
-
-
- - 页面访问密码 -
- - -
-
- -
-
-
- -
-
-
- - 提问密码 -
- - -
-
- -
-
-
-
-
- - - - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/preview-profile-light.html b/outputs/preview-profile-light.html deleted file mode 100644 index 2e561dd..0000000 --- a/outputs/preview-profile-light.html +++ /dev/null @@ -1,2022 +0,0 @@ - - - - - - 预览 16:08 · 设置 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
-

裁剪图片

-
-
-
- -
-

-
- -
-
- - -
- - - - - -
- - - - - - - - - - - - - - - -
-
-
-
- - 防骚扰设置 -
-
- -
-
- -
-
-
- -
- - - -
-
- - 全局屏蔽词 -
-

屏蔽词对提问、私聊(对方发给你的消息)与投票提交内容生效;三个设置页同步显示同一份设置,任意一处修改即时生效。

- - -
-
- -
-
-
- - - - - -
-
- - 审核严格度 - -

对提问、投票、私聊三类内容同时生效。这里调节的是「审核命中之后是否拦截」,不影响屏蔽词——屏蔽词始终直接拒绝。

-

底线拦截(所有档位强制生效,不可关闭):涉政、暴恐、色情,以及审核服务自定义词库的命中。

- - - - - - - - - - - - - - - - - - - - - - - - -
档位拦截范围
畅所欲言仅拦截底线内容,其余一律放行(误伤最少)。
正常水平底线内容,以及置信度不低于 90% 的辱骂、违禁内容。
不听坏话底线内容,以及辱骂、违禁、垃圾信息、广告、灌水、无意义、不良场景内容(不限置信度);同时采信审核服务给出的整体拦截建议。最严格档,拦截面不小于升级前。
- -
- - - -
- -
-
- 未设置时跟随站点默认档位。 -
-
- -
- -
-
-
- - -
-
-
- - 页面访问密码 -
- - -
-
- -
-
-
- -
-
-
- - 提问密码 -
- - -
-
- -
-
-
-
-
- - - - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/preview-profile-sakura-dark.html b/outputs/preview-profile-sakura-dark.html deleted file mode 100644 index b633fb0..0000000 --- a/outputs/preview-profile-sakura-dark.html +++ /dev/null @@ -1,2073 +0,0 @@ - - - - - - 预览 16:08 · 设置 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
-

裁剪图片

-
-
-
- -
-

-
- -
-
- - -
- - - - - -
- - -
-
- - - -
-

基础资料

-
- - -
-
- - -
-
- -
- -
-
- - -
-
- -
- -
-
- - - 选择访客访问你的提问箱时显示的语言(后台页面跟随此设置) -
-
- - -
-
- -
-
- - -
-

头像与背景

-
-
- - - - 1:1 -
- - -
- 裁剪框已锁定为 1:1 正方形 -
-
- - - - 16:9 -
- - -
- 裁剪框已锁定为 16:9 宽屏 -
-
- -
- -
-
- - -
-
    -
  • - 二维码样式 -
    -
    -
    -
    - -
    - -
    -
    - -
    - -
    -
    - -
    - -
    -
    - - 仅在启用时生效,否则颜色由系统自动生成 -
    -
    - -
    - - 0.35 - -
    -
    -
    - -
    - - 0.8 - -
    -
    -
    - -
    - - -
    - 选择图片后预览将即时更新 -
    -
    -
    -
    -

    实时预览

    -
    -
    -
    - 预设主题 -
    - - - - -
    -
    -
    - -
    -

    调整仅用于预览,最终效果点击「保存修改」后生效

    -
    -
    -
    -
    -
  • -
-
- -
-
- - -
-
    -
  • - 社交信息设置 -
    - 勾选后填写主页地址(无需 https://),未勾选的项将不会在前台显示。 - - - - - -
    -
  • -
-
- -
-
- - -
-

修改密码

-
- - -
-
- - -
-

保存后当前会话立即失效,需使用新密码重新登录

-
-
- -
-
-
- - -
-

标签设置

-
- -
- - -
-
- -
-
- -
-
-
- - -
-

账号设置

- -
- - -
-

主题外观

-
- -

仅对提问箱页面生效,站内其它页面保持默认外观。

-
- - - - - - -
-
- -
-
-
- - -
- - - - - - - - - - - - - - - - - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/preview-profile-sakura-light.html b/outputs/preview-profile-sakura-light.html deleted file mode 100644 index 30d24e9..0000000 --- a/outputs/preview-profile-sakura-light.html +++ /dev/null @@ -1,2073 +0,0 @@ - - - - - - 预览 16:08 · 设置 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
-

裁剪图片

-
-
-
- -
-

-
- -
-
- - -
- - - - - -
- - -
-
- - - -
-

基础资料

-
- - -
-
- - -
-
- -
- -
-
- - -
-
- -
- -
-
- - - 选择访客访问你的提问箱时显示的语言(后台页面跟随此设置) -
-
- - -
-
- -
-
- - -
-

头像与背景

-
-
- - - - 1:1 -
- - -
- 裁剪框已锁定为 1:1 正方形 -
-
- - - - 16:9 -
- - -
- 裁剪框已锁定为 16:9 宽屏 -
-
- -
- -
-
- - -
-
    -
  • - 二维码样式 -
    -
    -
    -
    - -
    - -
    -
    - -
    - -
    -
    - -
    - -
    -
    - - 仅在启用时生效,否则颜色由系统自动生成 -
    -
    - -
    - - 0.35 - -
    -
    -
    - -
    - - 0.8 - -
    -
    -
    - -
    - - -
    - 选择图片后预览将即时更新 -
    -
    -
    -
    -

    实时预览

    -
    -
    -
    - 预设主题 -
    - - - - -
    -
    -
    - -
    -

    调整仅用于预览,最终效果点击「保存修改」后生效

    -
    -
    -
    -
    -
  • -
-
- -
-
- - -
-
    -
  • - 社交信息设置 -
    - 勾选后填写主页地址(无需 https://),未勾选的项将不会在前台显示。 - - - - - -
    -
  • -
-
- -
-
- - -
-

修改密码

-
- - -
-
- - -
-

保存后当前会话立即失效,需使用新密码重新登录

-
-
- -
-
-
- - -
-

标签设置

-
- -
- - -
-
- -
-
- -
-
-
- - -
-

账号设置

- -
- - -
-

主题外观

-
- -

仅对提问箱页面生效,站内其它页面保持默认外观。

-
- - - - - - -
-
- -
-
-
- - -
- - - - - - - - - - - - - - - - - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/preview-theme-sakura-dark.html b/outputs/preview-theme-sakura-dark.html deleted file mode 100644 index ef7fdeb..0000000 --- a/outputs/preview-theme-sakura-dark.html +++ /dev/null @@ -1,916 +0,0 @@ - - - - - - 预览 16:08 · - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - - - - - - -
-
-
- -

测试箱主

-

欢迎提问

-
- - - - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@测试箱主提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -

@测试箱主 以前回答过的问题 (2)

- -
- -
-
- 查看回答 -
- ⭐ - 2026-10-19 16:08:02 -
-

置顶的问题 -

-
- -
-
- 查看回答 -
- - 2026-10-19 15:08:02 -
-

普通问题 -

-
- -
- - -
- -
-
- 无更多留言 -

-
-
- - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/preview-theme-sakura-light.html b/outputs/preview-theme-sakura-light.html deleted file mode 100644 index 6acd9ac..0000000 --- a/outputs/preview-theme-sakura-light.html +++ /dev/null @@ -1,916 +0,0 @@ - - - - - - 预览 16:08 · - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - - - - - - -
-
-
- -

测试箱主

-

欢迎提问

-
- - - - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@测试箱主提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -

@测试箱主 以前回答过的问题 (2)

- -
- -
-
- 查看回答 -
- ⭐ - 2026-10-19 16:08:02 -
-

置顶的问题 -

-
- -
-
- 查看回答 -
- - 2026-10-19 15:08:02 -
-

普通问题 -

-
- -
- - -
- -
-
- 无更多留言 -

-
-
- - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/prod-bottom.png b/outputs/prod-bottom.png deleted file mode 100644 index b0fb426..0000000 Binary files a/outputs/prod-bottom.png and /dev/null differ diff --git a/outputs/prod-mobile.png b/outputs/prod-mobile.png deleted file mode 100644 index 64264da..0000000 Binary files a/outputs/prod-mobile.png and /dev/null differ diff --git a/outputs/prod-tamakyi-snrat-check.png b/outputs/prod-tamakyi-snrat-check.png deleted file mode 100644 index aa2c759..0000000 Binary files a/outputs/prod-tamakyi-snrat-check.png and /dev/null differ diff --git a/outputs/profile-account.png b/outputs/profile-account.png deleted file mode 100644 index 8d79c18..0000000 Binary files a/outputs/profile-account.png and /dev/null differ diff --git a/outputs/profile-full.png b/outputs/profile-full.png deleted file mode 100644 index acd5ccb..0000000 Binary files a/outputs/profile-full.png and /dev/null differ diff --git a/outputs/profile-theme-card.png b/outputs/profile-theme-card.png deleted file mode 100644 index 5a86e9a..0000000 Binary files a/outputs/profile-theme-card.png and /dev/null differ diff --git a/outputs/proxy-image-timeout-2026-09-02.md b/outputs/proxy-image-timeout-2026-09-02.md deleted file mode 100644 index 4dfe488..0000000 --- a/outputs/proxy-image-timeout-2026-09-02.md +++ /dev/null @@ -1,90 +0,0 @@ -# proxy-image 回源超时事故处置报告 - -- **日期**:2026-09-02 -- **影响**:`/proxy-image` 拉取 R2 图片稳定超时 502(后台 `ProxyImage fetch failed`);分享卡生成存在同根因隐患 -- **状态**:代码侧已修复并全量验证;待服务器部署验证 - ---- - -## 一、症状 - -``` -ERRO[0013] ProxyImage fetch failed error="Get \"https://r2.337845818.best/tamabox/picture/...\": context deadline exceeded (Client.Timeout exceeded while awaiting headers)" -2026-09-02 23:51:00 Logger: Completed method=GET path="/proxy-image?url=..." status=502 duration=10.57s -``` - -- 同一 URL 每次请求稳定 10.5s 超时(旧 client `Timeout: 10s` + 开销) -- 用户在浏览器直接访问该 R2 链接正常 - -## 二、证据链(根因判定过程) - -| 证据 | 结论 | -|---|---| -| 报错文本 "awaiting headers" | TCP+TLS 已完成、HTTP 已发出,**等不到响应头**(非 DNS/dial/TLS 失败) | -| `r2.337845818.best` 解析结果 | Cloudflare anycast:104.21.47.52 / 172.67.144.141 + 2 IPv6 → **多边缘 IP** | -| 本地对照:走 https_proxy 代理 | ✅ 正常响应 | -| 本地对照:强制直连两个 CF IPv4 | ❌ code=000,TLS 未建立即断 → 裸连 CF 存在劣化边缘 | -| 服务器 curl 裸连 | ✅ **200 但耗时 6.46s** → 链路"慢而通",非完全不通 | -| 服务器 `env \| grep -i proxy` | 无代理 → 排除"程序绕过代理"假设 | - -**代码侧根因**(与 curl 的行为差异): -1. `route/proxy.go` 旧 `proxyImageDialContext` 解析后**固定拨第一个 IP**——覆盖了标准库的多 IP 容错。anycast 恰好抽中被劣化的边缘 → 稳定等满超时;curl 默认多 IP 尝试所以能成功。 -2. 超时预算过紧:`Timeout: 10s`(curl 成功都要 6.46s)、TLS 5s、读头阶段无独立兜底。 - -## 三、修复内容 - -### 1. 共享多 IP 并发拨号(新增) -`internal/shared/multidial.go` → `DialFirstReachable(ctx, dialer, network, ips, port, maxCandidates)` -- 对候选 IP **并发拨号取最先成功**(happy-eyeballs 风格) -- IP 字面量直连、无二次解析 → **DNS rebinding 免疫**(SSRF 防护不降级) -- 成功路径取消其余拨号、关闭落选连接(防泄漏);空候选/ctx 取消/截断均有防御 - -### 2. `/proxy-image`(route/proxy.go) -| 项 | 旧 | 新 | -|---|---|---| -| DialContext | 固定单 IP | `shared.DialFirstReachable`(上限 6 并发) | -| `Client.Timeout` | 10s | 25s | -| `TLSHandshakeTimeout` | 5s | 8s | -| `ResponseHeaderTimeout` | 无 | 8s(兜底"响应头迟到"黑洞) | -| 超时类错误 | 不重试 | 自动重试一次(`fetchProxyImageWithRetry`) | - -### 3. 分享卡生成(route/question/share.go)——排查发现的同根因复发点 -- `shareDialContext` 同款固定单 IP → 改调共享 helper -- 超时放宽:`Timeout` 10→20s、TLS 5→8s、读头 10→8s(图源同为 R2,宁等勿缺) -- 全仓 `DialContext` 扫查:**仅上述两处,无第三处同类** - -### 4. 测试 -- 新增 `internal/shared/multidial_test.go`(5 项):坏 IP 在前仍选通 / 全败报错 / ctx 取消即返回 / 空候选 / maxCandidates 截断 -- 原 `route/proxy_test.go` 随实现迁移删除 - -## 四、验证矩阵 - -| 项 | 结果 | -|---|---| -| gofmt | 无差异 | -| overlay `go build ./...`(全模块) | exit 0 | -| `go vet`(route + internal/shared) | 干净 | -| 全量安全相关测试(12 包:templates/cache/conf/form/i18n/imagecheck/internal-route/autoban/censor/shared/internal-template/route-vote) | 全 PASS | -| 新增回归测试 | 5/5 PASS | - -## 五、部署验证清单(服务器侧) - -1. 重新编译部署(Linux 补跑 `go build ./...`,webp/CGO 惯例确认) -2. 观察日志:`ProxyImage fetch failed` 应消失 -3. 刷新提问箱页:头像/背景图正常出图 -4. 触发一次分享卡生成(任意提问箱分享链接),确认头像/背景/社交图标完整 -5. **判断标准**:链路本身 ~6s 属正常(curl 实测),重点是**不再 10s 稳定超时** - -## 六、补充诊断(可选) - -服务器 curl 成功也要 6.46s,建议部署后定位慢在哪个阶段: - -```bash -curl -sI --max-time 15 -o /dev/null -w \ -"dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} total=%{time_total}\n" \ -"https://r2.337845818.best/tamabox/picture/2026/05/13/f95ba0476f90331e190baa65052a03" -``` - -- 若 `dns` 占大头 → 服务器 DNS 解析慢(可换公共 DNS / 加 hosts) -- 若 `tls` 占大头 → Cloudflare 边缘 TLS 劣化(多 IP 并发已缓解,可考虑图源域名走非代理直连评估) -- 若 `connect` 占大头 → 链路 TCP 劣化 diff --git a/outputs/r2-assets-cdn-audit-2026-09-05.md b/outputs/r2-assets-cdn-audit-2026-09-05.md deleted file mode 100644 index 772bb68..0000000 --- a/outputs/r2-assets-cdn-audit-2026-09-05.md +++ /dev/null @@ -1,115 +0,0 @@ -# R2 静态资源盘点与国内加速建议(2026-09-05) - -> 背景:uikit 已迁移 cdnjs.snrat.com(国内 4-5× 提速)。本次全量盘点仍挂在 -> `r2.337845818.best`(Cloudflare 链路、未接 EdgeOne)的静态资源,逐文件做 -> **官方源 hash 校验 + 国内实测**,给出分类迁移建议。 - -## 0. 关键结论速览 - -| # | 结论 | 级别 | -|---|---|---| -| 1 | 截图 1100ms 来自 **PageSpeed(海外探测)**,是 CF 链路 + Rocket Loader 的组合;**国内 EdgeOne 路径干净无 rocket-loader**,国内体验不受其影响 | 🔴 认知修正 | -| 2 | 国内真实瓶颈 = `r2.337845818.best` 本身(CF 国际链路),实测**每个资源 0.72–1.10s**,与"哪个库"无关,是源的问题 | 🔴 | -| 3 | **公共 CDN ≠ 快**:cdnjs.cloudflare/jsdelivr 国内实测 0.6–1.8s,与 R2 同级;真正快的是 **snrat(cdnjs 国内镜像)0.15–0.42s** | 🟠 关键认知 | -| 4 | 6 个 JS 库经 sha256 校验 = **纯官方原版**,可安全迁 snrat(2–7.4× 提速) | 🟢 | -| 5 | waline-no-commits.css / easy.qrcode(remake) / time.box.js / neko-pixel / 图片图标 = **定制或专属文件,无公共源**,只能自托 | 🟡 | -| 6 | 根治定制文件的唯一途径:**把 R2 静态域名接入 EdgeOne 国内加速**(方案 A),与库迁移可叠加 | 🟡 推荐 | - ---- - -## 1. 全量清单(去重后 17 项,含已注释项) - -### A 组:官方库,sha256 与官方完全一致 → 可迁 cdnjs.snrat.com ✅ - -| 文件 | 大小 | 加载位置 | 加载方式 | 国内实测 R2→snrat | 提速 | -|---|---|---|---|---|---| -| `cdn/lib/alpinejs/3.9.0/cdn.min.js` | 38KB | header 全站 | defer | 1.10s → 0.15s | **7.4×** | -| `cdn/lib/jquery/3.7.1/jquery.min.js` | 88KB | 二维码页(list/not-found) | 按需 | 0.83s → 0.17s | **4.9×** | -| `cdn/lib/vconsole/3.12.1/vconsole.min.js` | 214KB | header(VConsole 条件) | 同步 | 0.72s → 0.20s | **3.5×** | -| `cdn/lib/cropperjs/1.6.2/cropper.min.js` | 37KB | 头像裁剪页 | 按需 | 0.81s → 0.22s | **3.6×** | -| `cdn/lib/dayjs/1.10.8/dayjs.min.js` | 6.5KB | header 全站 | defer | 0.85s → 0.42s | **2.0×** | -| uikit css/js/icons | — | header 全站 | 已迁 ✅ | — | 4.3–4.9× | - -> 校验方式:R2 文件与 snrat/cdnjs/jsdelivr 官方逐一 sha256 比对,全部一致 -> (jquery/dayjs/alpine/cropperjs/vConsole 同 cdnjs 官方发行版;waline.js 同 -> `@waline/client@2.15.8` 官方发行版)。 - -### B 组:官方原版但公共源国内不优 → 暂留 R2 / 随方案 A - -| 文件 | 大小 | 说明 | 判断 | -|---|---|---|---| -| `cdn/lib/waline/2.15.8/waline.js` | 164KB | sha256 = 官方原版,但 @waline/client 只发 jsdelivr/unpkg(**无 cdnjs 收录**),jsdelivr 国内实测 ~0.99s > R2 0.79s | 🟡 迁移无收益,暂留 | -| `cdn/waline/.../waline-no-commits.css` | 19KB | **定制**:193 规则与官方一致,39 条差异全部是把 `.wl-card*`(评论卡片/管理操作/徽章/状态/内容)设 `display:none` —— 即"只留评论区容器、不显示评论列表"的刻意定制,**不可换官方 css** | 🟡 media=print 非阻塞,暂留 | - -### C 组:专属/定制文件,无公共源 → 留 R2,等方案 A 🔴 - -| 文件 | 用途 | 说明 | -|---|---|---| -| `cdn/time.box.js` | 全站 defer | 站点专属脚本 | -| `cdn/js/remake/easy.qrcode.min.js` | 二维码页 | **remake 改造版**,公共 qrcodejs 功能不等价 | -| `cdn/lib/neko-pixel/0.0.21/{style.css, neko-pixel-app.umd.js}` | IsPixel 页 | npm registry **查无此包**,无公共源 | -| `tamabox/tamabox_low.png` | logo/favicon/og | 站点资源 | -| `tamabox/icon/social/*.png` ×5 | 社交图标 | 站点资源 | - -### D 组:已注释、未启用(清理候选) - -- `cdn/busuanzi.pure.mini.js`(header 注释)— UV/PV 已由 vercount 承担 -- `oss-to-r2/nekobox/.../waline.js` + `waline-no-commits.css`(home.html 注释) - ---- - -## 2. 实测数据 - -### 国内本机(2026-09-05,3 次全量下载取中位) - -| 资源 | R2(CF 链路) | snrat(国内节点) | -|---|---|---| -| jquery | 0.83s | **0.17s** | -| dayjs | 0.85s | **0.42s** | -| alpinejs | 1.10s | **0.15s** | -| cropperjs | 0.81s | **0.22s** | -| vconsole | 0.72s | **0.20s** | -| waline.js(仅 jsdelivr 可比) | 0.79s | jsdelivr 0.99s ❌ | -| 专属文件(png/time.box.js) | 0.72–1.10s | 无公共对应 | - -> R2 所有资源稳定落在 0.72–1.10s(连接的是 Cloudflare AMS 国际链路)。 -> 对照参考:snrat 上 uikit 全量 263KB 约 0.25s —— 差的不是文件,是源。 - -### 修正认知 - -- **Rocket Loader 只影响走 CF 的海外路径**(PageSpeed/GTmetrix/测试域名), - 国内 EdgeOne 路径的 HTML 实测无 rocket-loader。**国内用户感受到的"慢" - 主要是 R2 的 CF 国际链路,不是 Rocket Loader。** -- PageSpeed 分数低的部分原因(海外 1100ms 资源)仍可靠关 Rocket Loader - 改善海外体验,但对国内数字无影响。 - ---- - -## 3. 迁移方案对比 - -| 方案 | 做法 | 覆盖范围 | 工作量 | 模板改动 | 风险 | -|---|---|---|---|---|---| -| **A. R2 接入 EdgeOne**(根治) | r2.337845818.best 走 EdgeOne 国内加速(源站=CF R2 公共端点),沿用 dnspod 境内/境外分流经验 | **全部文件**:A+B+C 组一次解决 | EdgeOne 控制台 + DNS 记录 | 无(域名不变) | 🟢 低,但涉及账号/计费与回源配置 | -| **B. 库迁 snrat**(对症) | A 组 5 个库 URL 换 cdnjs.snrat.com | 仅官方库 | 每库改 1 处模板 URL | 5 处 | 🟢 极低(sha256 已验证) | -| A+B 组合(推荐) | 先 B 快速见效,再 A 收尾定制文件 | 全部 | — | 5 处 + 控制台 | 🟢 | - -> 方案 A 的核心价值:B/C 组那些"无公共源"的定制文件(no-commits.css、 -> easy.qrcode remake、time.box.js、neko-pixel、图片)在 R2 上每请求 ~0.8s, -> 只有接国内加速才能根治;逐个找公共源既找不到也不值得。 -> 注:EdgeOne 加速的对象是**静态资源分发**,R2 仍保留在 Cloudflare 作为 -> 源站与海外出口,国内节点回源缓存后 CDN 分发。 - ---- - -## 4. 建议落地批次(待批准) - -- 🟢 **P1(已做)**:uikit 三件套 → snrat ✅ 已上线测试环境 -- 🟢 **P1-批量 B**:alpine/dayjs(header 全站)→ snrat;jquery/cropperjs/vconsole(按需页)→ snrat,共 5 处 URL -- 🟠 **P2-方案 A**:r2.337845818.best 接入 EdgeOne 国内加速(需你在 EdgeOne 控制台操作/确认计费;我可以先帮你查 EdgeOne 对 R2 自定义域作源站的官方支持方式) -- 🟡 **P3**:waline.js/no-commits.css 随方案 A 覆盖;清理 D 组注释残留;PageSpeed 若仍需优化海外分可关 CF Rocket Loader(per-zone) - -## 5. 风险与回滚 - -- 全部 A 组迁移经 sha256 验证 = 官方原版,无内容差异风险;回滚=改回 R2 URL -- waline-no-commits.css **不可**替换为官方 waline.css(会显示被隐藏的评论区);若误换,回滚即可 -- 方案 A 若回源/计费踩坑,DNS 一条记录切回原解析即完全回滚 diff --git a/outputs/re-audit-2026-09-01.md b/outputs/re-audit-2026-09-01.md deleted file mode 100644 index 8a3f024..0000000 --- a/outputs/re-audit-2026-09-01.md +++ /dev/null @@ -1,73 +0,0 @@ -# TamaBox-v4 第三轮审计报告(2026-09-01) - -本轮聚焦前两轮未覆盖的区域(上传/存储/邮件/基础设施、模板层)+ 对 8/31~9/1 修复批次做回归复审。 -所有 P0/P1 结论均已逐一核对源码确认,并纠正了子代理的一处误报(见 P1-3 说明)。 - -## 验证基线 -- `go build`:不依赖 storage 的 14 个包全部通过;`go test`(form/i18n/cache/autoban/censor)全绿。 -- ⚠️ 注意:本轮两个 P0 都藏在 webp 屏障遮住的包里(route/user、route/db),Windows 下编译不到——**务必在 Linux/带 libwebp 环境跑一次 `go build ./...`**,否则下面 P0-1 会直接让部署失败。 - ---- - -## 🔴 P0(2 项,均为上一批修复引入的回归) - -### P0-1 profile.go:335 — 导出函数无法编译(构建失败) -`personalData` 是 `[][]interface{}`,`sanitizeCell(row[0])` 把 `interface{}` 传给 `string` 参数 → **编译错误**。webp CGO 屏蔽了本机报错,Linux/Docker 构建同样会失败。 -**修复**:遍历单元格时先做 `s, ok := v.(string)` 类型断言,非字符串(如 `user.CreatedAt` 的 time.Time)跳过净化原样写入。 - -### P0-2 history-template.html:29 — 复合游标未同步,「加载更多」翻页损坏 -P2-13 把游标改成 `..` 复合格式,但前端 JS 仍回传纯 `data[data.length-1].id` → 服务端 `parsePinCursor` 解析失败 → 游标被忽略 → 每次都返回第一页(内容重复、无法翻页)。 -**修复**:JS 按 `${q.is_pinned?1:0}.${new Date(q.created_at).getTime()*1e6}.${q.id}` 组装游标;或服务端兼容裸 id 格式(解析失败时回退为 id 游标)。 - ---- - -## 🟠 P1(6 项) - -### P1-1 upload_images.go:167 — 级联删除查询了不存在的列 -`DeleteByQuestionID` 在 `upload_images` 表上执行 `WHERE question_id=?`,但该列实际在关联表 `upload_image_questions`(:54)。PG 报列不存在 → 级联失败(目前仅记日志不阻断删除),**关联记录永久残留**。 -**修复**:改为先 `UploadImageQuestion` 按 question_id 查出 imageID 列表,再软删对应 `UploadImage`(并顺带删 link 行)。 -(纠正误报:子代理称"提问无法删除",实际 `questions.DeleteByID` 只记日志不返回错误,删除本身仍成功。) - -### P1-2 conversation.go:205 — 重复删除导致配额二次回退 -会话删除是标志位软删(`is_deleted_by_owner`),重复 POST 同一删除请求,`First()` 仍命中 → 每次都 `IncrementQuota(-1)`。守卫保证了不为负,但多条会话反复删可把计数错误清零。 -**修复**:`First()` 后若 `conv.IsDeletedByOwner == true` 直接幂等返回。 - -### P1-3 questions.go:222 — 非 PinFirst 游标路径在 PG 下报错 + ASC 方向错误 -`q.Where("id < ?", cursorStr)` 传 string,PG 中 `integer < text` 无操作符 → 管理后台 `/api/.../questions` 带游标请求 500。且 ASC(sort=oldest)时仍用 `id < ?`,方向相反 → 翻页停在第一页。 -**修复**:`strconv.ParseUint` 校验游标;按 `opts.SortOrder` 选择 `id < ?`(DESC)或 `id > ?`(ASC)。 - -### P1-4 home.html:8-12 — 远程内容经 `x-html` 无消毒渲染(供应链 XSS) -首页从外部 API(`api-direct.337845818.best/...importantinfomation.json`)取 `log.content` 后 `x-html` 直出。该域被劫持或 JSON 中混入用户上报内容时,首页即任意 JS 执行。 -**修复**:改 `x-text`(内容纯文本时),或引入 DOMPurify 白名单过滤后再 `x-html`;顺带补上 `class=uk-article-meta` 缺失的引号。 - -### P1-5 tracing/flamego.go:42 — span 名含 query string,私密 token 落日志 -span 取 `RequestURI`,`?t=<问题token>`、找回密码 `?code=` 等会原样写入 trace;配合 `AlwaysSample` + stdout 全量输出,1C1G 上还是 IO 负担。 -**修复**:span 名只用 `URL.Path`;生产关闭 stdout exporter 或降采样。 - -### P1-6(遗留提醒)password.go — legacy 密码 `==` 非常量时间比较 -第一轮报告的 P1-6 至今未修(你当时未勾选)。bcrypt 失败静默降级 HMAC 路径同样在。建议尽快处理。 - ---- - -## 🟡 P2(择要 9 项) - -1. **mail.go:123**:`SetHeader("To", to)` 建议 `SetAddressHeader`(防 header 注入的纵深防御)。 -2. **page.go:664/673**:上传 key 保留用户可控后缀(可为 .svg/.html),建议后缀白名单。 -3. **孤儿文件**:图片上传 S3 成功但 DB 失败仅记日志;删除从不清理 S3 对象 → 存储只增不减。 -4. **conf/db.go:16**:PostgresDsn 密码未转义 + `sslmode=disable` 写死。 -5. **dbutil/pagination.go:19**:`LimitOffset` 无 pageSize 上限,可被超大值打 DB。 -6. **storage/oss.go**:全项目已无调用方(遗留代码),建议删除。 -7. **autoban 双计数**:敏感路径未命中路由时被中间件和 f.NotFound 各计一次 → 封禁提前触发;NotFound 应跳过 IsSensitivePath 命中的路径。 -8. **captcha VerifyOnly**:Attempts 读改写非原子,并发可绕 5 次上限(单会话低风险)。 -9. **questions 删除链路非事务**:级联失败仅记日志可留孤儿,建议包 `Transaction`;另 `sanitizeCSVCell`(vote.go:1194)缺 `\t\r\n` 分支,与 profile.go 净化规则不一致。 - ---- - -## ✅ 本轮确认无问题的部分 -- 模板层:41 处 POST 表单 CSRF token 全覆盖;`toJSON`/内联 JS 均经 html/template script-context 转义,无逃逸;Markdown 白名单消毒;探索页 AJAX 分支 escapeHTML 正确。 -- storage/s3.go:LimitReader 8MB + 魔数 + 像素上限 + 随机 key + 重试,凭证不入日志。 -- cache:RWMutex + maxItems + TTL,无泄漏。 -- 回归确认正确:XFF 从右向左剥离、autoban 单实例单次 Next、配额归属(提问→箱主、私聊→OwnerID 一致)、追问服务端校验、proxy 编码绕过防护、pixel GET-only、chat 重校验覆盖全部写路径。 - -## 建议修复顺序 -P0-1/P0-2(我引入的回归,必修)→ P1-1/P1-2/P1-3(数据一致性)→ P1-4/P1-5 → P1-6 遗留项 → P2 择要。 diff --git a/outputs/sakura-before-after.png b/outputs/sakura-before-after.png deleted file mode 100644 index cf6f1e5..0000000 Binary files a/outputs/sakura-before-after.png and /dev/null differ diff --git a/outputs/security-review-2026-09-02.md b/outputs/security-review-2026-09-02.md deleted file mode 100644 index 89308c2..0000000 --- a/outputs/security-review-2026-09-02.md +++ /dev/null @@ -1,253 +0,0 @@ -# TamaBox v4 全量代码安全审阅报告 - -- **审阅日期**:2026-09-02 -- **代码规模**:123 个 Go 文件 / 约 40 个 HTML 模板 -- **技术栈**:Flamego + GORM + PostgreSQL/MySQL + Redis -- **审阅方式**:静态代码审计(6 个领域并行)+ `go build` / `go vet` / `go test -race` + **真实启动服务做动态渗透验证** -- **凭据处理**:全程未读取 `conf/app.ini`;服务日志经正则脱敏后才进入分析,配置内容未外传 - ---- - -## 一、总体结论 - -| 项目 | 结果 | -|---|---| -| `go build ./...` | ✅ 通过,无错误 | -| `go vet ./...` | ✅ 完全干净,零告警 | -| `go test -race ./...` | ✅ **无任何 DATA RACE**(`internal/db` 因缺数据库环境变量失败,非代码缺陷) | -| 实际运行 | ✅ 成功启动并连接数据库,监听 5877 端口,功能正常 | -| SQL 注入 | ✅ 未发现 | -| IDOR / 越权 | ✅ 未发现,归属校验完备 | -| SSRF | ✅ 防护完备(已实测拦截) | -| XSS | ✅ 主链路 sanitize 扎实 | - -**这是一个安全基线明显高于同类项目的代码库**:bcrypt 密码哈希、登录后重建 session ID、CSRF 全局校验、SSRF fail-closed 白名单 + DNS rebinding 防护、开放重定向多变体防护、限流 + 自动封禁、CSV 公式注入转义、敏感字段 `json:"-"`,这些都做对了。 - -**未发现可被匿名攻击者直接利用的 P0 漏洞。** 以下为 5 个 P1 与 12 个 P2 项。 - ---- - -## 二、P1 问题(重要,建议优先修复) - -### 🔴 P1-1 CSRF token 未与用户会话绑定(已动态验证可跨会话复用) - -**位置**:`internal/route/route.go:349` 与 `route/auth/login.go:74` - -```go -// route.go:349 —— 未指定 SessionKey -csrf.Csrfer(csrf.Options{ - Secret: conf.Server.XSRFKey, - Header: "X-CSRF-Token", -}) - -// login.go:74 —— 实际写入的 key 是 "uid" -ctx.Session.Set("uid", user.ID) -``` - -**根因**:`flamego/csrf@v1.3.0` 的 `defaultSessionKey = "userID"`(`csrf.go:150`)。应用写入的却是 `"uid"`,导致 `s.Get("userID")` 恒为 `nil`,进而 `x.id` 恒为字符串 `"0"`(`csrf.go:198-202`)。而 token 校验是**无状态 HMAC 重算**,并不与 session 中存储的 token 比对: - -```go -// csrf.go:62 —— 校验只依赖 secret + c.id("0") + "POST" + 时间戳 -func (c *csrf) ValidToken(t string) bool { - return ValidToken(t, c.secret, c.id, http.MethodPost) -} -``` - -**动态验证结果**(本地实测,非推测): - -| 测试用例 | 结果 | -|---|---| -| 会话 B 的 cookie + **会话 A 的 token** | `HTTP 200` — **通过校验**,进入业务逻辑 | -| 会话 B 的 cookie + 伪造 token | `HTTP 400 Bad Request: invalid CSRF token` | -| 同一个 token 连续复用 26 次 | 全部通过(token 有效期 24 小时) | - -即:**任意匿名访客从公开的 `/login` 页面拿到的 token,对任何已登录用户都有效**,CSRF 从「每会话密钥」退化为「全站公开常量」。 - -**缓解因素**:session cookie 实测带 `SameSite=Lax`,可阻断经典跨站表单 POST。因此未定为 P0。但一旦出现子域接管、站内 HTML 注入,或将来因兼容性调整为 `SameSite=None`,防线即完全失效。 - -**修复**(一行): -```go -csrf.Csrfer(csrf.Options{ - Secret: conf.Server.XSRFKey, - Header: "X-CSRF-Token", - SessionKey: "uid", // 与 login.go 写入的 key 对齐 -}) -``` -修复后匿名 token(id=`"0"`)将无法通过登录用户(id=`uid`)的校验。**建议同时增加一条回归测试**锁定该行为,避免日后 key 改名再次错配。 - ---- - -### 🔴 P1-2 Session cookie 缺少 `Secure` 标志 - -**位置**:`internal/route/route.go:111-114` - -```go -sessioner := session.Sessioner(session.Options{ - Initer: initer, - Config: sessionStorage, - // 缺少 Cookie 配置,Secure 默认为 false -}) -``` - -**实测响应头**: -``` -Set-Cookie: flamego_session=pqc9w5j3kq9thjxu; Path=/; HttpOnly; SameSite=Lax -``` -`HttpOnly` 与 `SameSite=Lax` 有(库默认),**但 `Secure` 缺失**。虽然站点已下发 HSTS,但用户首次访问(HSTS 尚未落地、且未加入 preload 名单)时,主动 MITM 可诱导明文 HTTP 请求窃取会话 cookie。 - -**修复**: -```go -sessioner := session.Sessioner(session.Options{ - Initer: initer, - Config: sessionStorage, - Cookie: session.CookieOptions{ - HTTPOnly: true, - SameSite: http.SameSiteLaxMode, - Secure: strings.HasPrefix(conf.App.ExternalURL, "https"), - }, -}) -``` -建议与 `securityHeaders()` 中已有的 HSTS 判断复用同一条件,保持 http 本地开发可用。 - ---- - -### 🟠 P1-3 无请求体总大小上限,可磁盘耗尽 DoS - -**位置**:`internal/form/form.go:52` - -```go -if err := r.ParseMultipartForm(10 * 1 << 20); err != nil { // 10 MiB -``` - -**关键误区澄清**:`10 MiB` 参数**只限制内存缓冲**,超出部分由 `mime/multipart` 自动落盘到临时文件,**总量无任何上限**;且全项目未使用 `http.MaxBytesReader`(已全局搜索确认)。 - -匿名可达的提问接口即接受 multipart,攻击者可持续 POST 超大 body 撑满磁盘。`ReadTimeout: 30s` 仅提供有限约束,高带宽下 30 秒足以写入数 GB。 - -> **附带澄清一个误报**:初步审计曾怀疑 `imgFile.Size`(`route/question/page.go:632`)取自客户端声明的 `Content-Length` 因而可伪造绕过 5MB 校验。**我编写测试实测否定了这一点** —— 构造声明 `Content-Length: 10` 而实际写入 100000 字节的 multipart part,`FileHeader.Size` 如实返回 `100000`。Go 的该字段反映真实字节数,**5MB 校验有效,此处无漏洞**。 - -**修复**(在 `form.Bind` 或最外层中间件统一加固): -```go -const maxBody = 12 << 20 // 略高于业务上限 10MiB,留 multipart 边界开销 -r.Body = http.MaxBytesReader(w, r.Body, maxBody) -if err := r.ParseMultipartForm(10 << 20); err != nil { - // 超限时返回 413 Request Entity Too Large -} -``` - ---- - -### 🟠 P1-4 IPv6 未按 `/64` 聚合,限流与自动封禁可被绕过 - -**位置**:`internal/shared/ip.go:105`、`internal/route/ratelimit.go:140`、`internal/security/autoban/autoban.go` - -`ResolveClientIP` 返回完整 IPv6 地址,限流与 autoban 直接以其作 map key。单个住宅 IPv6 `/64` 前缀通常可用 2^64 个地址,攻击者每请求换一个源地址,则每个 key 仅命中 1 次,**阈值永远无法触发**;同时海量 key 会持续膨胀内存。 - -**实测补充**:IPv4 场景下限流工作正常 —— 连续 26 次请求精确地在第 21 次开始返回 `429`;伪造 `X-Forwarded-For` / `CF-Connecting-IP` / `X-Real-IP` **均未能绕过**(`trust_proxy` 关闭时确实忽略这些头,实现正确)。本条仅针对 IPv6 键粒度。 - -**修复**: -```go -// internal/shared/ip.go -func LimitKey(ip string) string { - p := net.ParseIP(ip) - if p == nil { return ip } - if v4 := p.To4(); v4 != nil { return v4.String() } - return p.Mask(net.CIDRMask(64, 128)).String() + "/64" -} -``` -在限流与 autoban 取 key 处改用 `shared.LimitKey(ip)`;**IP 黑名单仍保持精确匹配**,不要套用聚合。 - ---- - -### 🟠 P1-5 限流 / 封禁状态仅存进程内存,多实例部署失效 - -**位置**:`internal/route/ratelimit.go:17`(`visitors` 为进程内 map) - -限流与 autoban 状态均在单进程内存中,未落 Redis(尽管 Redis 已作为 session/cache 后端接入)。横向扩容到 N 个副本后,攻击者轮询各副本即可获得 **N 倍配额**;某副本封禁的 IP 打到其他副本仍被放行。 - -**修复**:迁移到 Redis 滑动窗口(`INCR` + `EXPIRE`,或 `SET NX EX`);若坚持单副本部署,请在 README 与部署文档中明确标注「本服务不支持多副本水平扩展」,并配合宿主机 fail2ban 兜底。 - ---- - -## 三、P2 问题(改进建议) - -| # | 问题 | 位置 | 说明与修复 | -|---|---|---|---| -| P2-1 | `safeURL` 允许协议相对 URL | `internal/template/markdown.go:246-255` | `strings.HasPrefix(lower, "/")` 会让 `//evil.com` 通过。**已写测试实测确认 `safeURL("//evil.com") == true`**。markdown 中 `[x](//evil.com)` 渲染为外链,可用于钓鱼。注意 `javascript:` 变体仍被正确拦截。修复:在白名单判断前先 `if strings.HasPrefix(lower, "//") { return false }` | -| P2-2 | 缺少 Content-Security-Policy | `internal/route/route.go:47-59` | 实测响应头确认无 CSP。其余安全头(X-Frame-Options / nosniff / Referrer-Policy / HSTS)齐备。因站内大量内联脚本,短期需保留 `'unsafe-inline'`,但限定 `script-src` 域名仍能显著收窄面 | -| P2-3 | `ProfileAPI` 返回完整 User 含 email | `route/user/profile.go:39-41` | `return ctx.JSON(ctx.User)`,而 `Email` 标签为 `json:"email"`(`users.go:82`)。虽为本人数据,仍违反最小暴露原则。建议返回裁剪后的 DTO | -| P2-4 | 提问删除级联未包事务 | `internal/db/questions.go:354-383` | 软删提问 → 删追问 → 删图片 → 回退配额,四步非原子,中途失败会留下孤儿记录与计数漂移。建议包入 `Transaction` | -| P2-5 | autoban 告警 goroutine 无 recover | `internal/security/autoban/autoban.go:172` | `go func(){ _ = b.sendAlert(...) }()` 一旦 panic 将终止整个进程。改用带 `recover` 的 `shared.SafeGo` | -| P2-6 | 登出未销毁会话 | `route/auth/logout.go:12` | 仅 `Session.Flush()` 清空数据,会话 ID 与 cookie 仍有效。建议改用 `Destroy()` 或登出后 `RegenerateID()` | -| P2-7 | 登录时序差异可枚举邮箱 | `internal/db/users.go:378` | 邮箱不存在时立即返回;存在但密码错时执行 bcrypt(约 50–100ms)。建议不存在时也比对一个固定 dummy hash 以对齐耗时(错误文案已正确统一) | -| P2-8 | 颜色字段仅校验长度未校验格式 | `internal/form/user.go:12-13` | `Usernamecolor` / `Introcolor` 仅 `minlen:7;maxlen:7`,可提交任意 7 字符。建议加 `Match(^#[0-9a-fA-F]{6}$)`。**注意:这不构成 XSS**,见第四节 | -| P2-9 | 会话有效期偏长 | `internal/route/route.go:97,108` | `Lifetime: 7 * 24h` 且无空闲超时,建议缩短并引入 idle timeout | -| P2-10 | `.bak` 文件被打包进二进制 | `templates/question/new-question-template.html.bak` | `//go:embed ... question ...` 会包含该文件(虽因 `.html` 后缀过滤不会被注册为模板)。建议删除或移出 embed 目录 | -| P2-11 | `app.sample.ini` 存在重复配置块 | `conf/app.sample.ini:82-86` 与 `89-93` | `image_endpoint` / `image_access_id` / `image_bucket` 等整组重复定义,会被静默覆盖为后者,易误导部署者。注释虽区分了「头像/背景」与「用户上传图片」,但键名完全相同 | -| P2-12 | `production` 默认 `false` | `internal/conf/static.go` + `route.go:63` | 缺省运行于 dev 模式可能暴露错误栈。建议默认按生产处理,或启动时强制校验该项 | - -**附加工程建议**:`internal/db` 的 8 个测试因需要 `DB_TYPE`/`DB_USER` 等环境变量而全部失败(`internal/dbutil/testing.go:40`)。建议提供 `docker-compose.test.yml` 或 `Makefile` 目标一键拉起测试库,否则这部分测试在 CI 外基本不会被执行。 - ---- - -## 四、经验证排除的「疑似问题」(误报澄清) - -审阅过程中有 3 项初步怀疑,我通过编写测试或实际请求**逐一验证后排除**,特此说明以免造成不必要的返工: - -1. **模板 JS 上下文缺引号 ≠ 注入** - `templates/question/question-not-found.html:46-47` 写作 `style.color = {{.PageUser.Introcolor}};`(无引号),与 `list.html:132` 的 `"{{...}}"` 不一致,初判为 JS 表达式上下文注入。 - **实测结论:安全。** Go `html/template` 在 JS 表达式上下文会自动 JSON 编码并补引号:输入 `0;alert(1` 输出 `"0;alert(1"`,输入 `` 输出 `"\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/serverless-migration-feasibility-2026-09-27.md b/outputs/serverless-migration-feasibility-2026-09-27.md deleted file mode 100644 index 56b0378..0000000 --- a/outputs/serverless-migration-feasibility-2026-09-27.md +++ /dev/null @@ -1,159 +0,0 @@ -# TamaBox-v4 迁移 Cloudflare Workers + D1 可行性评估 - -- 日期:2026-09-27 -- 评估对象:将 TamaBox-v4(Go + Flamego + GORM + PG/Redis)改造为 **TypeScript + Cloudflare Workers + D1** 的 serverless 形态 -- 参照项目:[OpenListTeam/OpenList-Worker](https://github.com/OpenListTeam/OpenList-Worker) -- 范围:**只读评估,未改动任何源码** - ---- - -## 0. 结论速览 - -| 项 | 结论 | -|---|---| -| 技术可行性 | 可行 —— 没有理论上的不可能 | -| 工程性价比 | 🔴 **低** —— 是「整体重写」,不是「改造」 | -| 主要阻碍 | ① 图像管线(cgo WebP / cwebp 子进程 / freetype 中文渲染)② 进程内有状态子系统 ③ PG 专有 SQL | -| 建议 | **方案 A(不迁移)**;若必须上边缘,走**方案 B(静态+只读下沉)** | - -一句话:**OpenList 是「无状态代理」,TamaBox 是「有状态 Web 应用」,两者的边缘适配成本差一个数量级。** - ---- - -## 1. 先看清参照物:OpenList-Worker 为什么能做成 - -| 维度 | OpenList(Go 原版) | OpenList-Worker(TS) | -|---|---|---| -| 仓库体积 | 9.8 MB | 21.7 MB(**比原版还大**) | -| 语言 | Go | TypeScript | -| 代码量 | — | 416 个 `.ts`,约 2.6 MB 源码 | -| Star | 24,800 | 372 | -| 立项 | — | 2025-08-23(约一年) | -| 维护方 | 官方团队 | 官方团队 | - -即使是**官方团队 + 24.8k star** 的项目,重写一年后 Worker 版代码量反而超过原版。而更关键的是**应用形态**: - -**OpenList 的本质是无状态的文件列表 + 反代:** - -- 没有多用户会话状态(只有一个管理员) -- 没有服务端可变状态(限流 / 封禁 / 去重全都不需要) -- 存储是「可插拔后端」,官方甚至支持降级到 `map` / KV,并明说 *serverless 下禁止回退到内存* -- 没有图像处理、没有服务端模板渲染 —— 前端是现成 SPA,Worker 只负责注入 CDN 地址 - -所以它干的其实是「**把无状态代理逻辑搬到边缘**」—— 这正是边缘计算最擅长的形态。 - -**TamaBox 恰恰相反,它是一个有状态的多用户 Web 应用。** - ---- - -## 2. TamaBox 的形态画像 - -代码规模:**34,664 行 Go** - -| 模块 | LOC | 说明 | -|---|---|---| -| `route/` | 7,645 | 业务逻辑(vote 1297 / share 970 / profile 606 …) | -| `internal/db` | 6,950 | GORM 模型 + 仓储,15 张表 | -| `internal/i18n` | 3,689 | 四语言字典(以数据为主) | -| `internal/security` | 2,675 | 审核、自动封禁、IP 情报 | -| `internal/route` | 1,653 | 中间件与路由装配 | -| `internal/template` | 825 | 模板函数 / Markdown / minify | -| `internal/shared` | 758 | IP、去重、并发工具 | -| `internal/conf` | 587 | 配置(ini + 单环境变量) | -| `internal/cmd` | 482 | CLI 子命令(web/censor/uid/ban/restore) | -| `internal/storage` | 440 | R2 / OSS 上传 + WebP 转换 | -| 模板 | 42 个 html | 服务端 `html/template` 渲染 | -| 中间件 | — | Session / CSRF / Redis cache / reCAPTCHA | - ---- - -## 3. 分层可行性矩阵 - -### P0 —— 硬阻断(必须重构,无法直搬) - -| # | 阻断点 | 位置 | 为什么在 Workers 上不行 | -|---|---|---|---| -| 1 | **WebP 编码是 cgo** | `internal/storage/s3.go:16,23,192` | `chai2010/webp` 依赖 libwebp 的 C 代码(`//go:build cgo`)。Workers 是 V8 isolate,跑不了 cgo → 只能换 `@jsquash/webp`(WASM) | -| 2 | **GIF→WebP 走 `cwebp` 子进程** | `internal/storage/s3.go:200-233` | `exec.LookPath("cwebp")` + `os.CreateTemp`。Workers 无子进程、无可写磁盘 → **完全不可实现** | -| 3 | **分享图 freetype 渲染中文** | `route/question/share.go:121-131, 387-720` | `os.ReadFile(font_path)` 读磁盘 OTF + freetype 光栅化 + jpeg 编码。必须整体重写;且大图中文字形光栅化是 CPU 密集,易撞 Workers CPU 上限 | -| 4 | **进程内状态**(Session / 缓存 / 限流 / 封禁 / 去重 / 统计) | `internal/route/route.go:386-395`、`internal/cache`、`internal/security/autoban`、`internal/shared/duplicate.go`、`internal/route/ratelimit.go` | Workers 请求间无共享可变状态 → 全部要迁 Durable Objects / KV / D1,成本明显上升 | -| 5 | **cache 中间件硬编码 Redis** | `internal/route/route.go:386-395` | 无条件走 `cacheRedis.Initer()`,不判断配置是否为空。Workers 无法用原生 TCP Redis | -| 6 | **启动期副作用** | `internal/cmd/web.go:57-105`、`internal/db/db.go:58-127` | `ListenAndServe` 常驻监听 + `AutoMigrate` + `fixPostgresSequences` + backfill。serverless 无常驻启动流程 | -| 7 | **常驻 goroutine 定时任务** | `ratelimit.go:41`、`autoban.go:250`、`duplicate.go:34`、`share.go:207` | `for { sleep; cleanup }` 模式 → 需改 Cron Triggers + DO alarm | -| 8 | **PG 专有 SQL** | `db.go:211-234`(序列 `nextval/setval`)、`follow_ups.go:68`(`FOR UPDATE`)、各处 `datatypes.JSON`(JSONB) | D1 是 SQLite:**无序列、无 `FOR UPDATE`、无 JSONB** | -| 9 | **SMTP 发信** | `internal/mail/mail.go:129-147` | Workers 禁止裸 TCP 25/465/587 → 须换 Resend / SendGrid 等 HTTP 邮件 API | -| 10 | **服务端 Go 模板** | `templates/` 42 个 + `internal/template/template.go:29-229` | `html/template` 的**上下文敏感转义**是安全边界,迁到 JS 模板引擎需谨慎等价实现 | -| 11 | **OTel/Uptrace gRPC 上报** | `internal/tracing/tracing.go:15-27`、`internal/cmd/web.go:41-48` | Workers 无 gRPC、无后台 batch 上报协程 | - -### P1 —— 需换库 / 改写(有明确对应物) - -- **S3/R2 上传** → `@aws-sdk/client-s3`(R2 兼容 S3 协议,可行) -- **七牛 / 阿里云文本审核** → `fetch` 直调或 JS SDK(阿里云 `tea`/`darabonba` 运行时需重做) -- **Excel 导出 `excelize`** → `exceljs` -- **CSV 导出** `encoding/csv` → 手写(简单) -- **QR `skip2/go-qrcode`** → `qrcode` JS 库 -- **Markdown `blackfriday` + 自研 XSS 白名单** → `marked` + `DOMPurify`(**白名单规则需逐条复刻**,属安全关键) -- **i18n 的 `fmt` 风格 `%s` 占位** → 需实现等价格式化 -- **软删除 + 唯一索引语义** → SQLite 下槽位行为需重新设计 - -### P2 —— 基本可直接移植 - -- `internal/imagecheck`(纯标准库像素校验) -- `internal/captcha/math.go`(**JSON 算数题,非图片、无字体**) -- `internal/i18n` 字典(纯数据) -- CSRF(无状态 HMAC,本身可移植) -- `internal/security/ipinfo`(离线签名,零网络、零 DNS) -- `internal/shared/ip.go`(IP 解析) -- `bodylimit` / `ipblacklist` -- `embed.FS`(转 Assets / 模块) - -> 补充:`internal/template/minify.go` 当前**未被路由装配实际使用**,迁移时可直接丢弃。 - ---- - -## 4. 成本量化 - -- 源码 **34.6k 行 Go** → 等价重写预计 **40k+ 行 TS** -- `templates/` **42 个模板 + 25 个模板函数**全部重写 -- 全部测试重写(含 `route/vote/vote_stats_test.go` 这类带 DB 的门控测试) -- 参考标定:OpenList 官方团队 + 24.8k star,重写约 **一年**,产出 416 个 TS 文件 / 2.6 MB -- TamaBox 的形态比 OpenList **更「重」**(有状态 + 关系型 DB + 图像 + 模板),且为**单人维护** - ---- - -## 5. 容易被忽略的坑 - -1. **中国大陆可用性**:Workers 是海外节点。文本审核走七牛/阿里云(国内 API),海外边缘回源国内延迟高;面向大陆用户还有备案/合规问题(cf. 项目已有 `edgeone-noicp-cf-tunnel-guide`、`edgeone-origin-whitelist-guide`) -2. **D1 限额**:SQLite 单库上限(付费 10 GB)、写并发受限;`questions` / `vote_submissions` 属写密集 -3. **Workers CPU 限额**:免费 10 ms、付费 30 s/请求 且**按 CPU 计费**。分享图渲染、Excel 导出都吃 CPU -4. **Durable Objects 是付费项**,按请求 + 时长计费 → 把 Session / 限流 / 封禁搬过去后,成本模型从「一台 1C1G」变成「按量」 -5. **当前生产是 1C1G 小服务器**:迁移的收益(免运维 / 弹性)对这个体量并不显著 - ---- - -## 6. 建议方案 - -### 方案 A(推荐):不迁移 - -形态不匹配 + 单人维护 + 当前 1C1G 成本已很低。收益(免运维、弹性)不足以覆盖重写成本与风险。 - -### 方案 B(若确要上边缘,渐进式) - -只把**无状态**的部分下沉边缘: - -- **下沉**:静态资源(`static/`、模板产物)+ 只读接口(公开提问页、投票公开页、`/explore`)→ EdgeOne Pages / Workers -- **保留源站**:写路径、图像处理、审核、邮件、导出 → 继续跑 Go - -项目已有 EdgeOne 回源白名单脚本,这套「边缘 + 源站」形态其实已在用。**风险最低、可随时回退。** - -### 方案 C(完全重写) - -仅当能接受**功能裁剪**(砍分享图 / Excel 导出 / 聊天 / 投票表单)且投入 2–3 个月专职。且建议优先 **EdgeOne(腾讯云,有大陆节点)** 而非纯 Cloudflare Workers。 - ---- - -## 附:评估依据 - -- 全部源码只读探查:`internal/**`(110 个 Go 文件)、`route/**`(22 个)、`templates/**`(42 个模板) -- 依赖清单:`go.mod`(含 `chai2010/webp`、`disintegration/imaging`、`golang/freetype`、`xuri/excelize`、`uptrace/uptrace-go` 等) -- 对照数据来自 GitHub API 与 OpenList-Worker `README` / 提交历史 diff --git a/outputs/shared-egress-vote-antispam-review-2026-08-26.md b/outputs/shared-egress-vote-antispam-review-2026-08-26.md deleted file mode 100644 index 678ef33..0000000 --- a/outputs/shared-egress-vote-antispam-review-2026-08-26.md +++ /dev/null @@ -1,76 +0,0 @@ -# 共享出口匿名投票防刷修改与测试报告 - -- **时间**: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/shot-box-dark.png b/outputs/shot-box-dark.png deleted file mode 100644 index 4fadc28..0000000 Binary files a/outputs/shot-box-dark.png and /dev/null differ diff --git a/outputs/shot-box-light.png b/outputs/shot-box-light.png deleted file mode 100644 index 6e4dc96..0000000 Binary files a/outputs/shot-box-light.png and /dev/null differ diff --git a/outputs/shot-box6-preview.png b/outputs/shot-box6-preview.png deleted file mode 100644 index aa01181..0000000 Binary files a/outputs/shot-box6-preview.png and /dev/null differ diff --git a/outputs/shot-default-check.html b/outputs/shot-default-check.html deleted file mode 100644 index 6d7039b..0000000 --- a/outputs/shot-default-check.html +++ /dev/null @@ -1,1640 +0,0 @@ - - - - - - 预览 15:09 · - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - - - - - - -
-
-
- -

测试箱主

-

欢迎提问

-
- - - - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@测试箱主提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -

@测试箱主 以前回答过的问题 (2)

- -
- -
-
- 查看回答 -
- ⭐ - 2026-10-19 15:09:46 -
-

置顶的问题 -

-
- -
-
- 查看回答 -
- - 2026-10-19 14:09:46 -
-

普通问题 -

-
- -
- - -
- -
-
- 无更多留言 -

-
-
- - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - -
- diff --git a/outputs/shot-explore-light.png b/outputs/shot-explore-light.png deleted file mode 100644 index 3737e12..0000000 Binary files a/outputs/shot-explore-light.png and /dev/null differ diff --git a/outputs/shot-home-dark.png b/outputs/shot-home-dark.png deleted file mode 100644 index 9feebdf..0000000 Binary files a/outputs/shot-home-dark.png and /dev/null differ diff --git a/outputs/shot-home-light.png b/outputs/shot-home-light.png deleted file mode 100644 index c2ef606..0000000 Binary files a/outputs/shot-home-light.png and /dev/null differ diff --git a/outputs/shot-live-box-deployed.png b/outputs/shot-live-box-deployed.png deleted file mode 100644 index 0b15aa9..0000000 Binary files a/outputs/shot-live-box-deployed.png and /dev/null differ diff --git a/outputs/shot-live-user-profile.png b/outputs/shot-live-user-profile.png deleted file mode 100644 index 75f4c62..0000000 Binary files a/outputs/shot-live-user-profile.png and /dev/null differ diff --git a/outputs/shot-live-user-questions.png b/outputs/shot-live-user-questions.png deleted file mode 100644 index c6b97d5..0000000 Binary files a/outputs/shot-live-user-questions.png and /dev/null differ diff --git a/outputs/shot-new-default.png b/outputs/shot-new-default.png deleted file mode 100644 index 94a34a2..0000000 Binary files a/outputs/shot-new-default.png and /dev/null differ diff --git a/outputs/shot-new-sakura-dark.png b/outputs/shot-new-sakura-dark.png deleted file mode 100644 index ea733e1..0000000 Binary files a/outputs/shot-new-sakura-dark.png and /dev/null differ diff --git a/outputs/shot-new-sakura-light.png b/outputs/shot-new-sakura-light.png deleted file mode 100644 index 1806dcd..0000000 Binary files a/outputs/shot-new-sakura-light.png and /dev/null differ diff --git a/outputs/shot-preview-profile-sakura-dark.png b/outputs/shot-preview-profile-sakura-dark.png deleted file mode 100644 index df956ed..0000000 Binary files a/outputs/shot-preview-profile-sakura-dark.png and /dev/null differ diff --git a/outputs/shot-preview-profile-sakura-light.png b/outputs/shot-preview-profile-sakura-light.png deleted file mode 100644 index a988470..0000000 Binary files a/outputs/shot-preview-profile-sakura-light.png and /dev/null differ diff --git a/outputs/shot-preview-theme-sakura-dark.html b/outputs/shot-preview-theme-sakura-dark.html deleted file mode 100644 index dca600c..0000000 --- a/outputs/shot-preview-theme-sakura-dark.html +++ /dev/null @@ -1,1685 +0,0 @@ - - - - - - 预览 15:09 · - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - - - - - - -
-
-
- -

测试箱主

-

欢迎提问

-
- - - - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@测试箱主提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -

@测试箱主 以前回答过的问题 (2)

- -
- -
-
- 查看回答 -
- ⭐ - 2026-10-19 15:09:46 -
-

置顶的问题 -

-
- -
-
- 查看回答 -
- - 2026-10-19 14:09:46 -
-

普通问题 -

-
- -
- - -
- -
-
- 无更多留言 -

-
-
- - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - -
- diff --git a/outputs/shot-preview-theme-sakura-light.html b/outputs/shot-preview-theme-sakura-light.html deleted file mode 100644 index ba83397..0000000 --- a/outputs/shot-preview-theme-sakura-light.html +++ /dev/null @@ -1,1685 +0,0 @@ - - - - - - 预览 15:09 · - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - - - - - - -
-
-
- -

测试箱主

-

欢迎提问

-
- - - - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@测试箱主提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -

@测试箱主 以前回答过的问题 (2)

- -
- -
-
- 查看回答 -
- ⭐ - 2026-10-19 15:09:46 -
-

置顶的问题 -

-
- -
-
- 查看回答 -
- - 2026-10-19 14:09:46 -
-

普通问题 -

-
- -
- - -
- -
-
- 无更多留言 -

-
-
- - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - -
- diff --git a/outputs/shot-profile-account-probe.html b/outputs/shot-profile-account-probe.html deleted file mode 100644 index f0a572b..0000000 --- a/outputs/shot-profile-account-probe.html +++ /dev/null @@ -1,2797 +0,0 @@ - - - - - - 预览 15:13 · 设置 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
-

裁剪图片

-
-
-
- -
-

-
- -
-
- - -
- - - - - -
- - -
-
- - - -
-

基础资料

-
- - -
-
- - -
-
- -
- -
-
- - -
-
- -
- -
-
- - - 选择访客访问你的提问箱时显示的语言(后台页面跟随此设置) -
-
- - -
-
- -
-
- - -
-

头像与背景

-
-
- - - - 1:1 -
- - -
- 裁剪框已锁定为 1:1 正方形 -
-
- - - - 16:9 -
- - -
- 裁剪框已锁定为 16:9 宽屏 -
-
- -
- -
-
- - -
-
    -
  • - 二维码样式 -
    -
    -
    -
    - -
    - -
    -
    - -
    - -
    -
    - -
    - -
    -
    - - 仅在启用时生效,否则颜色由系统自动生成 -
    -
    - -
    - - 0.35 - -
    -
    -
    - -
    - - 0.8 - -
    -
    -
    - -
    - - -
    - 选择图片后预览将即时更新 -
    -
    -
    -
    -

    实时预览

    -
    -
    -
    - 预设主题 -
    - - - - -
    -
    -
    - -
    -

    调整仅用于预览,最终效果点击「保存修改」后生效

    -
    -
    -
    -
    -
  • -
-
- -
-
- - -
-
    -
  • - 社交信息设置 -
    - 勾选后填写主页地址(无需 https://),未勾选的项将不会在前台显示。 - - - - - -
    -
  • -
-
- -
-
- - -
-

修改密码

-
- - -
-
- - -
-

保存后当前会话立即失效,需使用新密码重新登录

-
-
- -
-
-
- - -
-

标签设置

-
- -
- - -
-
- -
-
- -
-
-
- - -
-

账号设置

- -
- - -
-

主题外观

-
- -

仅对提问箱页面生效,站内其它页面保持默认外观。

-
- - - - - - -
-
- -
-
-
- - -
- - - - - - - - - - - - - - - - - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - -
- - diff --git a/outputs/shot-profile-account.html b/outputs/shot-profile-account.html deleted file mode 100644 index afa164f..0000000 --- a/outputs/shot-profile-account.html +++ /dev/null @@ -1,2789 +0,0 @@ - - - - - - 预览 15:15 · 设置 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
-

裁剪图片

-
-
-
- -
-

-
- -
-
- - -
- - - - - -
- - -
-
- - - -
-

基础资料

-
- - -
-
- - -
-
- -
- -
-
- - -
-
- -
- -
-
- - - 选择访客访问你的提问箱时显示的语言(后台页面跟随此设置) -
-
- - -
-
- -
-
- - -
-

头像与背景

-
-
- - - - 1:1 -
- - -
- 裁剪框已锁定为 1:1 正方形 -
-
- - - - 16:9 -
- - -
- 裁剪框已锁定为 16:9 宽屏 -
-
- -
- -
-
- - -
-
    -
  • - 二维码样式 -
    -
    -
    -
    - -
    - -
    -
    - -
    - -
    -
    - -
    - -
    -
    - - 仅在启用时生效,否则颜色由系统自动生成 -
    -
    - -
    - - 0.35 - -
    -
    -
    - -
    - - 0.8 - -
    -
    -
    - -
    - - -
    - 选择图片后预览将即时更新 -
    -
    -
    -
    -

    实时预览

    -
    -
    -
    - 预设主题 -
    - - - - -
    -
    -
    - -
    -

    调整仅用于预览,最终效果点击「保存修改」后生效

    -
    -
    -
    -
    -
  • -
-
- -
-
- - -
-
    -
  • - 社交信息设置 -
    - 勾选后填写主页地址(无需 https://),未勾选的项将不会在前台显示。 - - - - - -
    -
  • -
-
- -
-
- - -
-

修改密码

-
- - -
-
- - -
-

保存后当前会话立即失效,需使用新密码重新登录

-
-
- -
-
-
- - -
-

标签设置

-
- -
- - -
-
- -
-
- -
-
-
- - -
-

账号设置

- -
- - -
-

主题外观

-
- -

仅对提问箱页面生效,站内其它页面保持默认外观。

-
- - - - - - -
-
- -
-
-
- - -
- - - - - - - - - - - - - - - - - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/shot-profile-full.png b/outputs/shot-profile-full.png deleted file mode 100644 index c17b885..0000000 Binary files a/outputs/shot-profile-full.png and /dev/null differ diff --git a/outputs/shot-profile-preview.png b/outputs/shot-profile-preview.png deleted file mode 100644 index ec904f5..0000000 Binary files a/outputs/shot-profile-preview.png and /dev/null differ diff --git a/outputs/shot-profile-security.png b/outputs/shot-profile-security.png deleted file mode 100644 index beb7579..0000000 Binary files a/outputs/shot-profile-security.png and /dev/null differ diff --git a/outputs/shot-q-default.png b/outputs/shot-q-default.png deleted file mode 100644 index 9d4b5c7..0000000 Binary files a/outputs/shot-q-default.png and /dev/null differ diff --git a/outputs/shot-q-local.png b/outputs/shot-q-local.png deleted file mode 100644 index ffa1940..0000000 Binary files a/outputs/shot-q-local.png and /dev/null differ diff --git a/outputs/shot-sakura-dark.png b/outputs/shot-sakura-dark.png deleted file mode 100644 index f7a8318..0000000 Binary files a/outputs/shot-sakura-dark.png and /dev/null differ diff --git a/outputs/shot-sakura-light.png b/outputs/shot-sakura-light.png deleted file mode 100644 index 0573763..0000000 Binary files a/outputs/shot-sakura-light.png and /dev/null differ diff --git a/outputs/shot-vote-before.png b/outputs/shot-vote-before.png deleted file mode 100644 index 060d56e..0000000 Binary files a/outputs/shot-vote-before.png and /dev/null differ diff --git a/outputs/t-700.png b/outputs/t-700.png deleted file mode 100644 index 126ce99..0000000 Binary files a/outputs/t-700.png and /dev/null differ diff --git a/outputs/t-768.png b/outputs/t-768.png deleted file mode 100644 index 295b5f4..0000000 Binary files a/outputs/t-768.png and /dev/null differ diff --git a/outputs/t-980.png b/outputs/t-980.png deleted file mode 100644 index 4a2bcd3..0000000 Binary files a/outputs/t-980.png and /dev/null differ diff --git a/outputs/t-nav-right.png b/outputs/t-nav-right.png deleted file mode 100644 index d258a6f..0000000 Binary files a/outputs/t-nav-right.png and /dev/null differ diff --git a/outputs/tb-dev-test-qr-crop.png b/outputs/tb-dev-test-qr-crop.png deleted file mode 100644 index 67e48eb..0000000 Binary files a/outputs/tb-dev-test-qr-crop.png and /dev/null differ diff --git a/outputs/tb-dev-test-qr.png b/outputs/tb-dev-test-qr.png deleted file mode 100644 index 3b7484f..0000000 Binary files a/outputs/tb-dev-test-qr.png and /dev/null differ diff --git a/outputs/tb-page-snapshot.html b/outputs/tb-page-snapshot.html deleted file mode 100644 index e87f7ed..0000000 --- a/outputs/tb-page-snapshot.html +++ /dev/null @@ -1,1712 +0,0 @@ - - - - - - 小狼 - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - -
-
-
- -

小狼

-

提问密码是12123。欢迎你,我的朋友,不管你是因为什么原因发现这个网站,都希望它能帮助到你。有疑问和建议可以在下方提交问题,或者加群聊聊。

-
- - - acfunlink - - - - - - weibolink - - - - - xiaohongshulink - - - - - bloglink - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@小狼提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - -
- -
- -
- - - - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -

@小狼 以前回答过的问题 (32)

- -
- -
-
- 查看回答 -
- ⭐ - 2026-08-27 13:42:36 -
-

礼貌请教小狼提问箱如何避免被恶意利用 -

-
- -
-
- 查看回答 -
- ⭐ - 2026-05-12 21:22:46 -
-

小狼小狼,想问问可不可以实现提问后继续回复被提问者的功能,就像在同一层聊天一样;还有就是好像我发出提问之后,在提问者回答之前我好像都看不到自己发出的提问,这样的话感觉有点不好找(?) -

-
- -
-
- 查看回答 -
- - 2026-08-18 14:59:06 -
-

以下是markdown语法展示,大部分功能基本正常实现,空格和换行调整为一般语法,html代码暂不开放。 -标题展示: -Heading level 1 -Heading level 2 -Heading level 3 -Heading level 4 -Heading level 5 -Heading level 6 -粗体: -I just love bold text. -斜体: -Italicized text is the cat’s meow. -又粗又斜: -This text is really important. -引用: -> Dorothy followed her through many of the beautiful rooms in her castle. -列表: -- First item -- Second item -- Third item - - Indented item - - Indented item -- Fourth item -代码块: -At the command prompt, type nano. -链接: -这是一个链接 提问箱。 -

-
- -
-
- 查看回答 -
- - 2026-08-13 02:04:36 -
-

这是个md测试: -粗体 -斜体 - 代码 -链接 -

-
- -
-
- 查看回答 -
- - 2026-07-15 15:36:26 -
-

小狼有没有想过为什么人总是先学会告别再学会爱吗 -

-
- -
-
- 查看回答 -
- - 2026-06-15 13:39:45 -
-

啊不是 是聊天室总是显示 应用程序错误:发生了客户端异常(更多信息请参见浏览器控制台)。(上一条打错字了orz -

-
- -
-
- 查看回答 -
- - 2026-06-15 13:28:43 -
-

小狼是什么mbti( -

-
- -
-
- 查看回答 -
- - 2026-06-15 13:27:29 -
-

页面底部的 聊天室 如何创建房间哇 -

-
- -
-
- 查看回答 -
- - 2026-06-15 05:24:30 -
-

这个网站的页面和box.n3ko.cc这个好像(是同一个开发者嘛 -

-
- -
-
- 查看回答 -
- - 2026-05-06 11:23:26 -
-

好厉害啊,自己做一个网站也太棒了吧👍🏻老师回复的好及时,感觉好温暖啊。原来这个提问箱背后是这样的初心,希望它能一直被好好运营下去,也谢谢你愿意分享这些想法✨ -

-
- -
-
- 查看回答 -
- - 2026-05-06 10:46:32 -
-

小狼,你做这个网站是为了什么呀 -

-
- -
-
- 查看回答 -
- - 2026-04-23 01:07:41 -
-

小狼小狼,请问可以有拉黑功能吗? -

-
- -
-
- 查看回答 -
- - 2026-03-20 18:26:42 -
-

谢谢小狼ruarua小狼(鞠躬 -

-
- -
-
- 查看回答 -
- - 2026-03-16 23:02:34 -
-

小狼,你有哪个瞬间感觉到“界限”消失,又或者猛然意识到命运这回事吗🤔 -

-
- -
-
- 查看回答 -
- - 2025-12-30 16:15:04 -
-

测试图片上传机能 -

-
- -
-
- 查看回答 -
- - 2025-11-16 23:03:26 -
-

找了几个例子,这些”?“的位置以前都是emoji,看起来好像确实是比较新的emoji会显示成符号,老版常规的像❤️这样的就可以显示出来。回答页面也是类似的情况 -

-
- -
-
- 查看回答 -
- - 2025-11-15 13:58:28 -
-

小狼,之前提问里的emoji是可以正常显示的,但是从昨天开始变成了”?“符号🤔不过也有个别emoji现在还能显示 -

-
- -
-
- 查看回答 -
- - 2025-08-31 14:02:26 -
-

小狼小狼今天吃了什么! -

-
- -
-
- 查看回答 -
- - 2025-07-25 12:28:46 -
-

awa -

-
- -
-
- 查看回答 -
- - 2024-12-31 15:04:20 -
-

小狼小狼,如果提问者勾选了私密不公开提问,但又没有勾选接收通知,就算被提问者回答了,提问者是不是也没有办法看到回复呀? -

-
- -
- - -
- -
-
- 无更多留言 -

-
-
- - -
- - -
-
- -

- 您是本站第 位访客 | 本页面已被访问 次 -
Powered by NekoBox | 狼ICP备 20230101
提问箱已成功存活: -
- - 找站长 - | 公告板 - | 聊天室 - | 盘 - | 监控板 - | 导航页 -

-
-
- - - - diff --git a/outputs/tb-prod-test-qr-crop.png b/outputs/tb-prod-test-qr-crop.png deleted file mode 100644 index 5c68d2e..0000000 Binary files a/outputs/tb-prod-test-qr-crop.png and /dev/null differ diff --git a/outputs/tb-prod-test-qr.png b/outputs/tb-prod-test-qr.png deleted file mode 100644 index 510f972..0000000 Binary files a/outputs/tb-prod-test-qr.png and /dev/null differ diff --git a/outputs/theme-dark.png b/outputs/theme-dark.png deleted file mode 100644 index 1f024f7..0000000 Binary files a/outputs/theme-dark.png and /dev/null differ diff --git a/outputs/theme-light.png b/outputs/theme-light.png deleted file mode 100644 index 03e2a14..0000000 Binary files a/outputs/theme-light.png and /dev/null differ diff --git a/outputs/timestamptz-migration-fix-8h.sql b/outputs/timestamptz-migration-fix-8h.sql deleted file mode 100644 index 54550e3..0000000 --- a/outputs/timestamptz-migration-fix-8h.sql +++ /dev/null @@ -1,124 +0,0 @@ --- ============================================================================ --- TamaBox 存量时间 +8h 漂移反向修正脚本(线上执行版) --- 背景: 先前 timestamptz 迁移用 `AT TIME ZONE 'UTC'` 把存量"上海墙钟"误标为 --- UTC,导致全部存量时间绝对时刻 +8h(页面显示比真实本地时间晚 8 小时)。 --- 本脚本: 把当前值按 UTC 还原出原墙钟,再按 Asia/Shanghai 重新解释(净效果 --- -8h 绝对时刻,会话显示回到真实本地墙钟)。与会话时区无关。 --- 影响范围: nekobox.questions / users / censor_logs / upload_images 的 --- created_at / updated_at / deleted_at(12 列,全为 timestamptz)。 --- 安全: 全程无 DROP 业务对象;仅 CREATE SCHEMA/临时快照表 + ALTER TYPE。 --- 回滚方式见 PART 4(注释)。 --- 预期: PART 3 各项判读通过即成功;异常则执行 PART 4 回滚后贴回输出。 --- ============================================================================ - --- PART 0: 核对当前状态 ------------------------------------------------------- --- 确认处于"错误状态"(时间列全为 timestamptz 且显示比真实晚 8h)。 --- 应输出 12 列 timestamptz; 3089 应显示 08:01:16(错误态) 而非 00:01:16。 -SET TIME ZONE 'Asia/Shanghai'; -SELECT 'PART 0: 当前状态核对(仅本次修正涉及的4表12列)' AS step; -SELECT table_name, column_name, data_type -FROM information_schema.columns -WHERE table_schema = 'nekobox' - AND table_name IN ('questions','users','censor_logs','upload_images') - AND column_name IN ('created_at','updated_at','deleted_at') -ORDER BY table_name, column_name; -SELECT id, created_at AS now_display, (extract(epoch FROM created_at)*1000000000)::numeric(30,0) AS epoch_ns -FROM nekobox.questions WHERE id = 3089; - --- PART 1: 迁移前快照 ---------------------------------------------------------- -CREATE SCHEMA IF NOT EXISTS diag_test; -DROP TABLE IF EXISTS diag_test.fix_before; -CREATE TABLE diag_test.fix_before(tbl text, col text, n bigint, s numeric); -DO $$ -DECLARE r record; -BEGIN - FOR r IN - SELECT table_name tbl, column_name col - FROM information_schema.columns - WHERE table_schema='nekobox' AND data_type='timestamp with time zone' - AND column_name IN ('created_at','updated_at','deleted_at') - AND table_name IN ('questions','users','censor_logs','upload_images') - ORDER BY table_name, column_name - LOOP - EXECUTE format( - 'INSERT INTO diag_test.fix_before SELECT %L, %L, count(%I), sum(extract(epoch FROM %I)*1000000000) FROM nekobox.%I', - r.tbl, r.col, r.col, r.col, r.tbl); - END LOOP; -END $$; -SELECT 'PART 1: 快照完成' AS step, count(*) AS cols FROM diag_test.fix_before; - --- PART 2: 反向修正 ------------------------------------------------------------ --- 核心: 原墙钟按 Asia/Shanghai 解释。净效果 = 绝对时刻 -8h, 会话显示回本地墙钟。 -SELECT 'PART 2: 反向修正执行' AS step; -DO $$ -DECLARE r record; -BEGIN - FOR r IN - SELECT table_name tbl, column_name col - FROM information_schema.columns - WHERE table_schema='nekobox' AND data_type='timestamp with time zone' - AND column_name IN ('created_at','updated_at','deleted_at') - AND table_name IN ('questions','users','censor_logs','upload_images') - ORDER BY table_name, column_name - LOOP - EXECUTE format( - 'ALTER TABLE nekobox.%I ALTER COLUMN %I TYPE timestamptz USING (%I AT TIME ZONE ''UTC'' AT TIME ZONE ''Asia/Shanghai'')', - r.tbl, r.col, r.col); - END LOOP; -END $$; -SELECT '修正完成' AS step; - --- PART 3: 验收 ----------------------------------------------------------------- --- 3.1 关键行: 会话上海显示应回到真实本地墙钟(3089=00:01:16.678885+08, epoch 1788537676678885000) -DROP TABLE IF EXISTS diag_test.fix_after; -CREATE TABLE diag_test.fix_after(tbl text, col text, n bigint, s numeric); -DO $$ -DECLARE r record; -BEGIN - FOR r IN - SELECT table_name tbl, column_name col - FROM information_schema.columns - WHERE table_schema='nekobox' AND data_type='timestamp with time zone' - AND column_name IN ('created_at','updated_at','deleted_at') - AND table_name IN ('questions','users','censor_logs','upload_images') - ORDER BY table_name, column_name - LOOP - EXECUTE format( - 'INSERT INTO diag_test.fix_after SELECT %L, %L, count(%I), sum(extract(epoch FROM %I)*1000000000) FROM nekobox.%I', - r.tbl, r.col, r.col, r.col, r.tbl); - END LOOP; -END $$; -SELECT '3.1 修正后关键行(期望 3089=2026-09-05 00:01:16.678885+08 / epoch 1788537676678885000)' AS check_name; -SELECT id, created_at AS val_after, (extract(epoch FROM created_at)*1000000000)::numeric(30,0) AS epoch_ns -FROM nekobox.questions WHERE id IN (3089,3109,2411) ORDER BY id; - --- 3.2 keyset 翻页抽查(期望第2页 3088,2883..2411 共18条) -SELECT '3.2 keyset 第2页抽查' AS check_name; -SELECT q.id -FROM nekobox.questions q -WHERE q.user_id = 329 AND q.deleted_at IS NULL AND q.answer <> '' AND q.is_private = false - AND q.is_pinned = false - AND (q.created_at < TIMESTAMPTZ '2026-09-05 00:01:16.678885+08' - OR (q.created_at = TIMESTAMPTZ '2026-09-05 00:01:16.678885+08' AND q.id < 3089)) -ORDER BY q.is_pinned DESC, q.created_at DESC, q.id DESC -LIMIT 20; - --- 3.3 全表差异留档(修正前后仅整体偏移, n 与结构不变; 供留档) -SELECT '3.3 修正前后列计数(应逐列一致)' AS check_name; -SELECT b.tbl, b.col, b.n AS n_before, a.n AS n_after -FROM diag_test.fix_before b JOIN diag_test.fix_after a USING (tbl,col) -WHERE b.n IS DISTINCT FROM a.n; - --- 3.4 列类型(仍应为 timestamptz, 残余 without=0) -SELECT '3.4 残余 timestamp without time zone(应0)' AS check_name; -SELECT count(*) AS remain_without -FROM information_schema.columns -WHERE table_schema='nekobox' AND data_type='timestamp without time zone' - AND column_name IN ('created_at','updated_at','deleted_at'); - --- PART 4: 回滚(需要时才执行; 当前被注释) --------------------------------------- --- 若修正后发现问题需回到"错误态"(UTC 基准): --- 把 PART 2 的 USING 子句换成 (%I AT TIME ZONE 'Asia/Shanghai' AT TIME ZONE 'UTC') --- 即: ALTER TABLE nekobox.xxx ALTER COLUMN c TYPE timestamptz --- USING (c AT TIME ZONE 'Asia/Shanghai' AT TIME ZONE 'UTC'); --- 可先对比 diag_test.fix_before 快照确认还原。 diff --git a/outputs/timestamptz-migration-online.sql b/outputs/timestamptz-migration-online.sql deleted file mode 100644 index 85a17a9..0000000 --- a/outputs/timestamptz-migration-online.sql +++ /dev/null @@ -1,158 +0,0 @@ --- ============================================================================ --- TamaBox 时间列 timestamptz 迁移脚本(线上执行版) --- 前置: 本脚本已在你提供的生产克隆库(nekobox_clone)完整验证通过: --- * 迁移后 epoch 与迁移前逐项一致(差异 0) --- * 回滚路径同样验证无损(可安全回退) --- * 会话时区无关(验证时使用 Asia/Shanghai 会话) --- 实际受影响表(含数据): questions / users / censor_logs / upload_images --- 其余表(conversations/messages/votes/...)时间列本来就是 timestamptz, 脚本会自动跳过 --- 覆盖: 只读 + ALTER(DDL), 建议低峰窗口执行; 克隆实测全库 1.7s 完成 --- 使用: psql/pgAdmin 整段按 PART 顺序执行, 把输出贴回核对 --- 注意: 迁移期间避免应用滚动发布; 执行前确认无长时间运行事务持锁 --- ============================================================================ - --- ============================================================================ --- PART 0 前置核对: 列出将被迁移的列(线上应为 4 表 x 3 列 = 12 列) --- ============================================================================ -SELECT 'PART 0: 将被迁移的无时区时间列' AS step; -SELECT table_name, column_name -FROM information_schema.columns -WHERE table_schema = 'nekobox' - AND data_type = 'timestamp without time zone' - AND column_name IN ('created_at','updated_at','deleted_at') -ORDER BY table_name, column_name; - --- ============================================================================ --- PART 1 迁移前 epoch 快照(存入 diag_test schema, 供 PART 3 自动比对) --- ============================================================================ -SELECT 'PART 1: 迁移前快照' AS step; -CREATE SCHEMA IF NOT EXISTS diag_test; -DROP TABLE IF EXISTS diag_test.snap_before; -CREATE TABLE diag_test.snap_before(tbl text, col text, n bigint, minv numeric, maxv numeric, s numeric); -DO $$ -DECLARE r record; -BEGIN - FOR r IN - SELECT table_name tbl, column_name col - FROM information_schema.columns - WHERE table_schema='nekobox' AND data_type='timestamp without time zone' - AND column_name IN ('created_at','updated_at','deleted_at') - ORDER BY table_name, column_name - LOOP - EXECUTE format( - 'INSERT INTO diag_test.snap_before SELECT %L, %L, count(%I), min(extract(epoch FROM %I)*1000000000), max(extract(epoch FROM %I)*1000000000), sum(extract(epoch FROM %I)*1000000000) FROM nekobox.%I', - r.tbl, r.col, r.col, r.col, r.col, r.col, r.tbl); - END LOOP; -END $$; -SELECT '快照完成, 共 ' || count(*) || ' 列' AS info FROM diag_test.snap_before; - --- ============================================================================ --- PART 2 执行迁移 --- 核心语义: 库存值是 UTC 墙钟, 用 AT TIME ZONE 'UTC' 显式解释为 timestamptz, --- 与会话时区无关。绝不可用裸 ALTER(会按会话时区解释 -> 全部 -8h 偏移)。 --- ============================================================================ -SELECT 'PART 2: 执行迁移' AS step; -DO $$ -DECLARE r record; -BEGIN - FOR r IN - SELECT table_name tbl, column_name col - FROM information_schema.columns - WHERE table_schema='nekobox' AND data_type='timestamp without time zone' - AND column_name IN ('created_at','updated_at','deleted_at') - ORDER BY table_name, column_name - LOOP - EXECUTE format( - 'ALTER TABLE nekobox.%I ALTER COLUMN %I TYPE timestamptz USING %I AT TIME ZONE ''UTC''', - r.tbl, r.col, r.col); - END LOOP; -END $$; -SELECT '迁移完成' AS step; - --- ============================================================================ --- PART 3 验收(4 项全绿 = 迁移成功; 任一项不满足立即执行 PART 4 回滚) --- ============================================================================ -SELECT 'PART 3: 验收' AS step; - --- 3.1 迁移后快照并自动比对: 差异应为 0 行 -DROP TABLE IF EXISTS diag_test.snap_after; -CREATE TABLE diag_test.snap_after(tbl text, col text, n bigint, minv numeric, maxv numeric, s numeric); -DO $$ -DECLARE r record; -BEGIN - FOR r IN - SELECT table_name tbl, column_name col - FROM information_schema.columns - WHERE table_schema='nekobox' AND data_type='timestamp with time zone' - AND column_name IN ('created_at','updated_at','deleted_at') - ORDER BY table_name, column_name - LOOP - EXECUTE format( - 'INSERT INTO diag_test.snap_after SELECT %L, %L, count(%I), min(extract(epoch FROM %I)*1000000000), max(extract(epoch FROM %I)*1000000000), sum(extract(epoch FROM %I)*1000000000) FROM nekobox.%I', - r.tbl, r.col, r.col, r.col, r.col, r.col, r.tbl); - END LOOP; -END $$; -SELECT '3.1 迁移前后 epoch 差异(应为 0 行)' AS check_name; -SELECT b.tbl, b.col, b.n AS n_before, a.n AS n_after, - b.minv AS min_before, a.minv AS min_after, - b.maxv AS max_before, a.maxv AS max_after, - b.s AS sum_before, a.s AS sum_after -FROM diag_test.snap_before b -JOIN diag_test.snap_after a USING (tbl, col) -WHERE b.n IS DISTINCT FROM a.n OR b.minv IS DISTINCT FROM a.minv - OR b.maxv IS DISTINCT FROM a.maxv OR b.s IS DISTINCT FROM a.s; - --- 3.2 残余无时区时间列(应为 0) -SELECT '3.2 残余 timestamp without time zone(应 0)' AS check_name; -SELECT count(*) AS remain_without -FROM information_schema.columns -WHERE table_schema='nekobox' AND data_type='timestamp without time zone' - AND column_name IN ('created_at','updated_at','deleted_at'); - --- 3.3 关键行 epoch 指纹(应与迁移前/线上游标一致) -SELECT '3.3 关键行 epoch 指纹' AS check_name; -SELECT id, (extract(epoch FROM created_at)*1000000000)::numeric(30,0) AS epoch_ns -FROM nekobox.questions WHERE id IN (3109, 3089, 2411) ORDER BY id; - --- 3.4 keyset 翻页抽查: 应精确返回 3088,2883,...,2411 共 18 条(与修复后线上第2页一致) -SELECT '3.4 keyset 翻页抽查(第2页)' AS check_name; -SELECT q.id -FROM nekobox.questions q -WHERE q.user_id = (SELECT id FROM nekobox.users WHERE domain='aoi') - AND q.deleted_at IS NULL AND q.answer <> '' AND q.is_private = false - AND q.is_pinned = false - AND (q.created_at < TIMESTAMPTZ '2026-09-05 00:01:16.678885+00' - OR (q.created_at = TIMESTAMPTZ '2026-09-05 00:01:16.678885+00' AND q.id < 3089)) -ORDER BY q.is_pinned DESC, q.created_at DESC, q.id DESC; - --- ============================================================================ --- PART 4 回滚(仅在 PART 3 验收不通过或线上出现异常时执行) --- 还原为 timestamp, 与迁移前快照一致(已在克隆库验证无损) --- ============================================================================ --- SELECT 'PART 4: 回滚(按需执行)' AS step; --- DO $$ --- DECLARE r record; --- BEGIN --- FOR r IN --- SELECT table_name tbl, column_name col --- FROM information_schema.columns --- WHERE table_schema='nekobox' AND data_type='timestamp with time zone' --- AND column_name IN ('created_at','updated_at','deleted_at') --- AND table_name IN ('questions','users','censor_logs','upload_images') --- ORDER BY table_name, column_name --- LOOP --- EXECUTE format( --- 'ALTER TABLE nekobox.%I ALTER COLUMN %I TYPE timestamp USING %I AT TIME ZONE ''UTC''', --- r.tbl, r.col, r.col); --- END LOOP; --- END $$; --- SELECT '回滚完成(可再次跑 PART 3 核验)' AS step; --- ============================================================================ --- 验收说明: --- 3.1 = 0 行且 3.2 = 0、3.3 指纹与迁移前一致、3.4 翻出 18 条 -> 迁移成功 --- 任何一项不符 -> 执行 PART 4 回滚后停止排查, 联系开发 --- 迁移后建议: --- 1) 重新部署应用(含 parsePinCursor 的 .UTC() 修复版本) --- 2) 复跑今日翻页验收脚本, 预期 38 条唯一收敛 --- 3) 确认无误后可清理 diag_test schema: DROP SCHEMA diag_test; --- ============================================================================ diff --git a/outputs/tutorial-dual-cdn-split-routing.md b/outputs/tutorial-dual-cdn-split-routing.md deleted file mode 100644 index 87d9a57..0000000 --- a/outputs/tutorial-dual-cdn-split-routing.md +++ /dev/null @@ -1,314 +0,0 @@ -# 以狼的提问箱为例-免备案双 CDN 分流实战教程:境内走 EdgeOne、境外走 Cloudflare - -> **教程性质**: -> 本文以 狼的提问箱 项目为例,但所有原理与步骤对任何「不备案、有海外用户、想让境内/境外各走最优线路」的站点通用。 -> -> 场景速览:域名权威 DNS 托管在 **DNSPod**(免费版支持按「境内 / 境外 / 默认」线路返回不同解析结果);境内线路 CNAME 到**腾讯 EdgeOne 国际站**(免备案区域,大陆用户落到香港等最近节点,走腾讯跨境骨干,比 Cloudflare 免费版大陆绕行明显更快);境外/默认线路 CNAME 到 **Cloudflare 优选 IP**(经 Cloudflare for SaaS 自定义主机名识别流量,由既有 Cloudflare Tunnel 回源)。 - - ---- - -## 1. 为什么能这么搭 —— 三个关键认知 - -整套方案成立的前提,是先把三个「概念解耦」想清楚: - -### 1.1 Cloudflare for SaaS:让「流量识别」与「DNS 解析权」解耦 - -Cloudflare 普通接入(Free 套餐)要求把整域 NS 迁到 Cloudflare,这与「DNS 要放在 DNSPod 做分线路」**天然冲突**——NS 在谁手里,谁才说了算。 - -Cloudflare for SaaS(免费额度 100 个自定义主机名)绕开这个限制: - -- 你有一个**承载 zone**(例如 `cf-zone.example.net`,托管在 Cloudflare); -- 在承载 zone 里把目标域名(`box.example.com`)加为 **Custom Hostname**,并验证归属; -- Cloudflare 边缘从此「认识」这个域名:**任何带着 `Host: box.example.com` 到达 CF 边缘的请求**,都会被匹配证书、按配置路由(本项目为 Tunnel 回源)。 - -关键结论:**CF 侧只负责"来了流量怎么认、怎么转",不负责"流量怎么来"**。流量能不能到 CF 边缘,取决于你自己的 DNS(DNSPod)把境外用户解析到哪。所以「境外走 CF」必须在 DNSPod 配一条境外线路指向 CF 的 IP/CNAME——CF 自己不会拉客。 - -### 1.2 DNSPod 分线路:免费就能「同一域名,不同答案」 - -腾讯云解析(DNSPod)免费版就支持按查询来源返回不同记录:线路可选「中国境内 / 境外 / 默认」,境内细分到省、运营商。**默认线路是兜底**,任何未命中的查询都走它。 - -要理解一个易错点:**分线路的判定发生在 DNSPod 的权威服务器上**,依据是「递归 DNS 服务器的出口 IP 归属地」——所以「境外用户走境外线路」成立的前提是境外用户的递归 DNS(8.8.8.8 等)能真的问到 DNSPod 权威,且没被中间网络劫持/污染(第 6 节有实战案例)。 - -### 1.3 EdgeOne 国际站:选对「加速区域」就不用备案 - -EdgeOne 分「中国大陆可用区 / 全球可用区(不含中国大陆)/ 海外可用区」等区域配置。官方口径: - -- 选 **全球可用区(不含中国大陆)** 或海外区域 → **不要求 ICP 备案**; -- 选中国大陆区域 → 必须备案。 - -不备案的代价:大陆用户**没有本地节点**,请求落到最近的海外节点(大陆一般落香港,也有日韩/新加坡等),由腾讯跨境骨干回源。实测体验通常仍明显优于 Cloudflare 免费版大陆线路(后者常绕美/日且抖动大)——所以「不备案也选 EO 当境内线路」是有实测前提的,切换前建议先测速对比。 - -> 补充:Cloudflare for SaaS 启用通常需要账户先绑定支付方式,额度内不扣费。 - ---- - -## 2. 目标架构 - -``` - DNS 层 DNSPod(example.com 整域权威,免费版支持按线路返回) - NS = byron.dnspod.net / sunshine.dnspod.net(官方 NS) - box CNAME 记录 - ├─ 境内 ──▶ box.example.com.edgeone.example (EdgeOne 接入目标) - ├─ 境外 ──▶ cf-pref.example.net (CF 优选 IP 域名) - └─ 默认 ──▶ cf-pref.example.net (兜底,语义同境外) - ┌──────────────┴──────────────────────────────┐ -大陆用户 ▶▶(命中境内线路) ◀◀ 海外用户(命中境外/默认线路) - ▼ ▼ -EdgeOne 国际站 Cloudflare 边缘 -区域: 全球(不含中国大陆) 优选 IP: 198.51.100.x(示例段) -证书: TrustAsia, CN=box.example.com 识别: for SaaS 自定义主机名 -回源: EO 直连源站公网端口 回源: Cloudflare Tunnel(纯出站) - │ │ - └──────────────────┬──────────────────────────┘ - ▼ - 源站防火墙 iptables+ipset(仅放行 EO 回源网段;cloudflared 走 127.0.0.1 不受影响) - ▼ - HTTP 应用直接监听公网高位端口(本项目 TamaBox,无 nginx) -``` - -两条回源路径互不干扰:EO 是 **入站直连**(需开放端口 + 白名单锁定),CF 是 **Tunnel 出站**(无需任何入站规则)。 - ---- - -## 3. 前置条件清单 - -| # | 条件 | 说明 | -|---|---|---| -| 1 | 一个域名(`example.com`),权威 DNS 托管在 DNSPod | 免费版即可;注册局委派的 NS = byron/sunshine.dnspod.net | -| 2 | 一个 Cloudflare 账户 + 承载 zone(`cf-zone.example.net`) | 用于 for SaaS 与 Tunnel;**域名本身的 DNS 不用迁给 CF** | -| 3 | cloudflared Tunnel 已在跑 | 服务器 → CF 纯出站连接,绑定承载域 `box.cf-zone.example.net` | -| 4 | 服务器有公网 IP,且能开一个高位入站端口 | 给 EO 回源用;**不需要 nginx**,应用直接监听即可 | -| 5 | EdgeOne 账户(本例国际站 edgeone.ai) | 选免备案区域 | -| 6 | CF 账户已绑支付方式 | 启用 for SaaS 的前提(额度内不扣费) | -| 7 | 服务器 1C1G 级别即可 | iptables/ipset 是内核规则,无常驻进程 | - ---- - -## 4. 分步实施 - -> 建议顺序:**先 CF(步骤 A)→ 再 EO(步骤 B)→ 再 DNS(步骤 C)→ 最后服务器(步骤 D)**。 -> 前两步做完,流量仍在旧路径上,随时可暂停;步骤 C/D 才真正切换与收敛。 - -### Step A —— Cloudflare 侧:SaaS 自定义主机名 + Tunnel(本次零改动,讲解原理) - -本项目 `box.example.com` 的 SaaS 自定义主机名此前已配好(放在承载 zone `cf-zone.example.net` 下),Tunnel 已正常回源。若从零配置,动作是: - -1. 承载 zone `cf-zone.example.net` → **SSL/TLS → Custom hostnames** → Add custom hostname,填 `box.example.com`; -2. 按提示完成归属验证(加一条 TXT 或 CNAME 验证记录); -3. 在 Tunnel 的路由里把 `box.example.com` 指到本地 HTTP 服务(`http://127.0.0.1:端口`); -4. 记录页里能拿到该主机名的 **CNAME target**(形如 `.cdn.cloudflare.net`)——它是「把流量引到 CF 边缘」的官方入口。 - -> ⚠️ 本项目实际没用 SaaS CNAME target,而是用**优选 IP CNAME(`cf-pref.example.net`)**:一个第三方维护的、解析到 CF 低延迟边缘 IP 的域名。原理相同——解析结果落在 CF 边缘,CF 靠 `Host: box.example.com` 识别出这是 SaaS 自定义主机名流量再按 Tunnel 路由。**优选域名的可用性依赖第三方维护,失效时换回官方 CNAME target 即可**(第 7 节维护清单)。 -> -> 本项目 DNSPod 上境外/默认线路指向的是 `cf-pref.example.net`。 - -### Step B —— EdgeOne 侧:添加站点(CNAME 接入,免备案区域) - -1. EO 控制台 → 添加站点 → 填 `box.example.com`; -2. **接入方式选 CNAME 接入**(⚠️ 不要选 NS 接入——NS 接入会要求把域名托管给 EO,与 DNSPod 分线路冲突,和「DNS 在 CF 手里没法分线路」是同一个问题); -3. **加速区域选「全球可用区(不含中国大陆)」**(免备案的关键选择); -4. 归属验证:按 EO 提示添加验证记录(本项目用 TXT:`edgeonereclaim.example.com TXT ...`),**记录加在 DNSPod 的默认线路**(CA/验证服务器多在境外,放默认线路全球可见最稳); -5. 配置回源:源站地址 = 你的公网 IP + 高位端口,Host 头 = `box.example.com`;回源协议按你的源站监听协议选(应用直监听 HTTP 就选 HTTP,TLS 由 EO 边缘终结); -6. 免费证书:CNAME 验证通过 + 回源配置就绪后,EO 自动签发(本项目 TrustAsia,`CN=box.example.com`)。签发需要几分钟到几小时——**在证书签发完成前,443 会拿 `*.cdn.myqcloud.com` 兜底证书应答**,浏览器会出现证书/HSTS 报错(第 6 节排障 6.1),属正常过程,等签发即可; -7. **开启「客户端 IP 头」**(关键,默认关闭!):站点 → 网络优化 / 规则引擎 → 客户端 IP 头部 → 开启,**自定义头部名必须填 `EO-Client-IP`**——应用侧固定读取这个名字; -8. **删除/改写伪造的 CF 头**:在 EO 加一条规则引擎规则,把客户端可能携带的 `CF-Connecting-IP` / `X-Real-IP` 等头删掉——否则大陆攻击者经 EO 带假 CF 头,会被应用当成 CF 流量优先信任(头信任模型见 Step D)。 - -### Step C —— DNSPod 分线路:三条记录 - -权威 DNS 在 DNSPod(整域 zone `example.com`),所以**直接在整域 zone 里配,不需要任何子域委派**。 - -| 主机记录 | 类型 | 线路 | 记录值 | TTL | -|---|---|---|---|---| -| `box` | CNAME | **中国境内** | `box.example.com.edgeone.example`(EO 分配的目标) | 120~600 | -| `box` | CNAME | **境外** | `cf-pref.example.net`(CF 优选 / 或 SaaS CNAME target) | 600 | -| `box` | CNAME | **默认** | `cf-pref.example.net`(兜底,语义同境外) | 600 | - -要点: - -- **EO 归属验证 TXT 保持「默认」线路**(全球可见),别只放境内; -- 若之前误把 EO 的 CNAME 建在「默认」线路,会导致全球都走 EO——把它改成「中国境内」,境外/默认补 CF 两条; -- TTL 别太大,方便回滚与换优选域时快速生效。 - -### Step D —— 服务器侧:应用监听 + 信任头 + 端口锁定 - -#### D1 应用直接监听公网端口(免 nginx) - -本项目 TamaBox 自带 HTTP 服务器,改配置即可: - -```ini -[server] -listen = 0.0.0.0:18080 ; 从仅本机 127.0.0.1 改为全网卡,EO 才能远程连入(18080 仅为示例端口) -``` - -- cloudflared 连的是 `127.0.0.1:端口`,`0.0.0.0` 监听兼容回环连入,**Tunnel 分支不受影响**; -- nginx 在本方案中从头到尾不是必需——防火墙(iptables/ufw)是内核规则,零常驻进程,比 nginx 轻得多,适合 1C1G 小机器。 - -#### D2 应用信任两家 CDN 的权威头 - -`conf/app.ini`: - -```ini -[security] -trust_proxy = true -``` - -配合代码层支持(本项目 `internal/shared/ip.go` 的 `ResolveClientIP`),信任顺序: - -1. `CF-Connecting-IP`(Cloudflare 权威头,海外分支); -2. `EO-Client-IP`(EdgeOne 权威头,境内分支); -3. 兜底:TCP 对端 RemoteAddr。 - -> ⚠️ **头信任的安全前提(缺一不可)**: -> 1. 源站防火墙**只放行两家 CDN 的回源来源**(EO 直连需要;CF 分支天然安全——Tunnel 是出站,外人到不了源站); -> 2. EO 侧已删除伪造的 CF 头(Step B-8); -> 3. 若还有 CF 侧直连,同样在 CF 规则里删除伪造的 EO 头。 -> -> 任一缺失,「信任头」就会被攻击者直连源站 + 自带假头绕过——限流/封禁会拿到伪造 IP。 - -#### D3 端口白名单:iptables + ipset 自动维护(EO 免费档没有官方回源段) - -EO 官方回源清单由「源站防护 / Origin ACL」提供(配套 `DescribeOriginACL` API),**但该功能只支持付费套餐**——免费档在控制台找不到入口,也没有可拉取的清单 URL。 - -> 🔴 **免费档方案定稿(2026-09-03 实测)**:**不要**逐个收集 EO 出口 IP 做白名单——实测发现 EO 回源出口是**海量轮换节点池**(一次 23 秒抓包就出现 13 个不同出口、散布多个腾讯云 /16 段),逐 IP 白名单必然间歇漏放 → 境内访问偶发 **521**(EO 边缘回源失败)。正解 = 直接放行 EO 所属的**腾讯云大段**(CIDR 整段入清单,脚本原生支持)。实测生效清单见下。 - -交付脚本 `scripts/edgeone-origin-whitelist.sh`(bash,需 root,架构要点): - -- 白名单放 **ipset 集合** `EO_ORIGIN`,iptables 一条规则引用它:`自建链 EO_IN` 内「命中集合放行 → 其余 REJECT tcp-reset」。INPUT 层规则默认**只对公网网卡 eth0 生效**(`EO_IFACE` 可配,网卡名不同必改否则端口裸奔);cloudflared 本机回连走独立的 `-i lo` ACCEPT 放行,与网卡限定解耦; -- 更新走「临时集填充 → `ipset swap` 原子替换」:**同步全程无规则清空窗口**; -- **fail-safe**:拉取失败 / 清单为空 / 全非法 → 拒绝更新、保留现网,绝不误删导致端口裸奔; -- 命令:`sync`(周期入口)/ `auto`(采集当前回源 IP → 新段预警)/ `learn`(即时观测)/ `show` / `flush`(回滚)/ `--dry-run`; -- 清单来源:本地文件(`/etc/edgeone-origin/cidr.txt`)+ 可选 URL,取并集。脚本支持每行一个 IP 或 CIDR。 - -落地四步: - -```bash -# 1. 改端口(默认 18080 → 你的实际端口) -# 2. 安装 ipset -apt-get install -y ipset -# 3. 建清单:放行腾讯云 EO 大段(每行一个 CIDR,# 注释;以下为实测生效配置) -mkdir -p /etc/edgeone-origin -cat > /etc/edgeone-origin/cidr.txt <<'EOF' -43.146.0.0/16 -43.152.0.0/16 -43.168.0.0/16 -43.174.0.0/16 -43.175.0.0/16 -EOF -# 段哪来的?用脚本 auto / tcpdump 持续观察回源出口,把出现的 IP whois 归并到所属腾讯云段 -# 注意:种子/采样是 EO 的"出口 IP",不是应用日志里的客户端 IP(日志里是 EO-Client-IP, -# 客户端真实 IP,不能当白名单)!段级方案下无需逐个采种,新段靠 auto 月度预警 -bash edgeone-origin-whitelist.sh sync --dry-run -bash edgeone-origin-whitelist.sh sync -# 4. 验证:外网机器 nc -zv 18080 应 refused;境内访问 https://box.example.com 应正常(无 521) -# 5. 挂定时:cron「17 4 */3 * *」或 systemd timer 每 3 天一次 sync(EO 段以月计才变) -``` - -> 日后升级付费套餐:把 `DescribeOriginACL` API 的输出写入 `cidr.txt` 即可,脚本零改动。 - ---- - -## 5. 验证方法(含大陆 DNS 劫持陷阱) - -### 5.1 分线路是否生效(🔴 重点:别在大陆本机 nslookup 境外 DNS) - -大陆网络对发往境外公共 DNS(8.8.8.8 / 1.1.1.1 / 9.9.9.9 / OpenDNS,**UDP 53 和 TCP 53 都会被劫持**)的查询会返回**按境内线路伪造的答案**——在本机 `nslookup box.example.com 8.8.8.8` 永远显示 EO,即使境外线路早已配好。正确验证通道: - -```bash -# 境内视角(本机直接查即可,通道可信) -nslookup box.example.com 223.5.5.5 # 期望 → CNAME edgeone.example → 203.0.113.x(EO 示例段) -nslookup box.example.com 119.29.29.29 # 同上(DNSPod 自家 DNS) - -# 境外视角:用「境外服务器代为查询」,浏览器直接打开下面两个 API 之一 -# https://api.hackertarget.com/dnslookup/?q=box.example.com -# https://networkcalc.com/api/dns/lookup/box.example.com -# 期望 → CNAME cf-pref.example.net → 198.51.100.x(CF 边缘 IP 示例段) -``` - -两条结果**不一样 = 分线路生效**。 - -### 5.2 HTTPS 证书是否就绪 - -```bash -echo | openssl s_client -connect box.example.com:443 -servername box.example.com \ - | openssl x509 -noout -subject -issuer -dates -# 期望 subject = CN=box.example.com(而不是 *.cdn.myqcloud.com 兜底证书) -``` - -### 5.3 真实 IP 是否被正确识别 - -境内访问后看源站/EO 访问日志中的客户端 IP = 你的大陆公网出口 IP(说明 `EO-Client-IP` 生效);海外访问后同理应为你的海外出口 IP(`CF-Connecting-IP`)。 - ---- - -## 6. 今日排障实录(每个坑都值得记) - -### 6.1 「网站使用 HSTS」报错 = EO 证书还没签发(不是配置错) - -现象:DNS 切到 EO 后浏览器报 HSTS 错误页。 -原因链:之前走 CF HTTPS 时应用下发了 `Strict-Transport-Security: max-age=31536000; includeSubDomains`(按 ExternalURL 以 https 开头就对每个响应下发)→ 浏览器记录「一年内必须 HTTPS」→ 切到 EO 后证书尚未签发、443 拿 `*.cdn.myqcloud.com` 兜底证书 → 证书与域名不匹配 → HSTS 错误页。 -处理:等 EO 证书签发(几分钟~几小时)即可,**域名不在 HSTS preload 列表,浏览器自动恢复,无需清缓存**。紧急回滚 = DNS 改回 CF。 -(若在 preload 列表里才是真麻烦——需先修好证书再等 preload 移除,故平时别主动提交 preload。) - -### 6.2 误判「权威 DNS 在 Cloudflare」(方向性错误) - -现象:一度以为 `example.com` 权威在 Cloudflare,推导出「需要子域 NS 委派」的复杂方案,差点让用户在 CF 做一堆不存在 zone 的操作。 -根因:apex `example.com` 本身是一条 CNAME → `your-project.pages.dev`(Cloudflare Pages 项目)——nslookup 查 NS 时**一路跟随 CNAME 链**走到 pages.dev 的 NS(CF),被误读为「example.com zone 在 CF」。 -正确做法:**查域名权威必须问注册局(RDAP)或直查 SOA**,不能信递归 CNAME 链: - -```bash -# RDAP(注册局铁证):https://rdap.org/domain/example.com → 应返回 byron/sunshine.dnspod.net -# 或直查 SOA:nslookup -type=SOA example.com byron.dnspod.net -``` - -用户一句「我的 Cloudflare 根本没有 example.com zone」就是最有效的纠偏信号——**控制台里没有的 zone,DNS 上不可能归它管**。 - -### 6.3 「境外也返回 EO」的假象 = 大陆 DNS 劫持(连 TCP 53 都劫) - -现象:从大陆本机用 nslookup 测 8.8.8.8 / 9.9.9.9 / OpenDNS(UDP 和 TCP 都试),境外视角全部返回 EO,一度以为分线路没生效、甚至怀疑 DNSPod 配置错了。 -真相:大陆网络劫持出境 DNS 查询,返回按境内线路伪造的 EO 答案,请求根本没到境外服务器。 -破案手段:用**境外服务器**(hackertarget / networkcalc API)代为查询,立刻看到 CF 答案——DNSPod 三线路配置完全正确,从未出过错。教训见 5.1。 - -### 6.4 残留的阿里云验证记录 - -实测发现 DNSPod 上有历史残留 `_dnsauth.box.example.com CNAME → xxxxxxxx...aliyunauth.certificate-validations.com`(以前阿里云申请证书留下的)。`_dnsauth` / `_acme-challenge` 下的旧验证记录会干扰证书自动续期验证——**证书验证记录的坑**,建议确认证书正常后删除。 - -### 6.5 免费档没有「源站防护」,回源 IP 段拿不到 - -见 Step D3:免费档无官方清单,用 learn 观察法兜底;付费后切 `DescribeOriginACL`,脚本零改动。 - ---- - -## 7. 日常维护清单 - -| 周期 | 事项 | -|---|---| -| 证书到期前 | EO 免费证书自动续期并**重新做域名验证**——⚠️ CA 验证服务器多在境外,**所有验证记录务必放默认线路**;到期前留意 EO 控制台证书状态,失败会提示重新验证 | -| 每 3 天(定时任务) | 白名单 sync(EO 回源段 3~6 个月才变,每 3 天足够);sync 是全量重建,清单里删掉的旧 IP 会自动剔除 | -| 每月 | 抽查清单有无漏网的新回源 IP(auto 已每日自动补种,此处仅兜底核对);确认优选域名 `cf-pref.example.net` 仍解析到 CF 边缘(失效则换回官方 SaaS CNAME target,只改 DNSPod 一条记录) | -| EO 回源突然失败 | 先临时放行全段定位,再按 learn 结果收紧——EO 免费档收不到段变更通知,要有这个预案 | -| 长期 | 若升级 EO 付费套餐:启用源站防护拿官方段 → 接入 DescribeOriginACL 输出到 cidr.txt | - ---- - -## 8. 回滚 - -1. **快速回滚(境外也要回 CF 之外的兜底)**:DNSPod 把「境内」线路改指向 CF(或删掉境内线路,让默认兜底接管)→ 全球走 CF,分钟级生效; -2. **彻底回滚(回到纯 CF 时代)**:删除 EO 站点/记录、撤防火墙放行、DNSPod 三线路合并为默认→CF; -3. EO 证书到期前回滚,记得去 EO 控制台删除站点避免残留验证记录干扰。 - ---- - -## 附:配置快照(示例,2026-09-03 实测定稿,域名已脱敏) - -``` -域名: box.example.com(权威 DNS: DNSPod,NS=byron/sunshine.dnspod.net) -境内: CNAME box.example.com.edgeone.example → 203.0.113.x (EdgeOne 国际站,区域不含大陆,示例段) -境外/默认: CNAME cf-pref.example.net → 198.51.100.x (CF 优选 IP,示例段) -CF 侧: SaaS 自定义主机名(承载 zone cf-zone.example.net)+ Tunnel 出站回源 -EO 证书: TrustAsia, CN=box.example.com(签发后约 90 天到期,自动续期) -EO 归属验证: edgeonereclaim.example.com TXT(DNSPod 默认线路 ✅) -EO 客户端头: EO-Client-IP(手动开启,头名固定 EO-Client-IP) -源站: TamaBox 直监听 0.0.0.0:高位端口(无 nginx);iptables+ipset 白名单 -应用信任: trust_proxy=true;ResolveClientIP 信任 CF-Connecting-IP → EO-Client-IP → RemoteAddr -``` diff --git a/outputs/user-updates-2026-09-01-02.md b/outputs/user-updates-2026-09-01-02.md deleted file mode 100644 index 3f88ab2..0000000 --- a/outputs/user-updates-2026-09-01-02.md +++ /dev/null @@ -1,37 +0,0 @@ -# 近期更新速览(9 月 1 日 — 9 月 2 日) - -> 面向站点使用者的简要更新说明,已省略技术细节。 -> 提示:以下内容在服务端部署最新版本后才会全部生效。 - ---- - -## 2026-09-01 - -### 新增 / 改进 -- 管理后台的「功能设置」为提问、私聊、投票新增三种开放方式:**一直开放**、**指定时间段开放**、**每周固定时段循环开放**。以前定时开放一旦结束,功能就会一直关闭;现在可以设定每周循环时间,到期自动开启与关闭。 -- 未登录访客查看问题详情页时,「分享」按钮自动居中,页面更整齐。 - -### 修复 -- 修复未登录用户切换界面语言不生效的问题。 -- 全站提示文字(错误提示、操作结果、表单校验等)统一跟随所选界面语言显示,简体中文 / 繁體中文 / English / 日本語 不再混用。 - ---- - -## 2026-09-02 - -### 新增 / 改进 -- 投票的自定义回答框加入**实时字数统计**,超过字数上限会标红并阻止提交,不再被悄悄截断。 -- 站长创建投票时,每个选项文本的长度上限放宽到 300 字。 -- 标签与屏蔽词改为「标签块」式输入:回车或逗号即可生成一个词,点 × 删除,无需手动敲英文逗号分隔,并实时显示数量。 -- 提问、回答、追问、私聊消息的字数上限已统一,超出时会有明确提示。 -- 私聊的会话消息条数上限可在 1~200 之间自行设置(新用户默认 200 条)。 - -### 修复 -- 修复已发布投票在编辑保存后出现「未知的投票类型」、选项看似丢失的问题,现在保存后投票与选项均正常显示。 -- 修复「加载更多」按钮失效:公开主页与后台投票列表可以正常翻页;列表到底后按钮不再出现,连点也不会重复加载。 -- 修复头像、背景、分享卡片等图片偶尔加载超时失败的问题。 - -### 调整 -- 屏蔽词现在对提问、私聊(对方发来的消息)与投票的自定义内容**同时生效**;提问 / 私聊 / 投票三个设置面板展示同一份屏蔽词,改一处即全局同步。 -- 精简后台投票设置页,移除重复的「投票内容管理」入口(投票列表仍可从顶栏进入)。 -- 多项安全与稳定性加固(细节略)。 diff --git a/outputs/verify/b_${escapeHTML(user.domain)}.html b/outputs/verify/b_${escapeHTML(user.domain)}.html deleted file mode 100644 index 0a1ccbb..0000000 --- a/outputs/verify/b_${escapeHTML(user.domain)}.html +++ /dev/null @@ -1,2 +0,0 @@ -Found. - diff --git a/outputs/verify/b_admin3.html b/outputs/verify/b_admin3.html deleted file mode 100644 index 9a34a4e..0000000 --- a/outputs/verify/b_admin3.html +++ /dev/null @@ -1,1275 +0,0 @@ - - - - - - 狼の質問箱 | 匿名質問箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - -
-
-
- -
-

admin3x的提问箱

-

此提问箱已设置访问密码,请输入密码查看内容

- - - - - - - - - - - -
- - -
-
- - -
-
-
- -
-
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/verify/base.css b/outputs/verify/base.css deleted file mode 100644 index 955d41b..0000000 --- a/outputs/verify/base.css +++ /dev/null @@ -1,650 +0,0 @@ -/* TamaBox 全站基础样式:设计令牌 / 主题覆盖 / 组件质感 / 响应式。 - 由 base/header.html 内联块抽出,集中一份便于缓存与维护。 - ⚠️ 改完务必把 header 里的 ?v= 版本号 +1,否则老访客仍用缓存。 */ - -/* 设计令牌:亮色为默认(:root),深色在 html[data-theme="dark"] 覆盖。 - 页面与组件样式一律引用 var(--tb-*),不再写死色值/圆角/阴影; - var() 的第二个参数仅作兜底(旧缓存页、邮件模板等不引入本文件的场景)。 - 改配色只动这一块,亮暗两套自动保持一致。 */ - :root { - --tb-bg: #f7f8fa; - --tb-surface: #ffffff; - --tb-surface-2: #f4f6f8; - --tb-surface-hover: #f3f7ff; - --tb-border: #e8eaee; - --tb-text: #1f2329; - --tb-text-secondary: #5c6370; - --tb-text-muted: #8a919c; - --tb-primary: #1e87f0; - --tb-primary-weak: rgba(30, 135, 240, 0.10); - --tb-radius: 12px; - --tb-radius-sm: 8px; - --tb-radius-xs: 6px; - --tb-shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 12px 28px -18px rgba(16, 24, 40, 0.20); - --tb-shadow-hover: 0 1px 2px rgba(16, 24, 40, 0.05), 0 18px 40px -20px rgba(16, 24, 40, 0.28); - --tb-font: system-ui, -apple-system, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif; - } - html[data-theme="dark"] { - --tb-bg: #1a1d23; - --tb-surface: #242830; - --tb-surface-2: #2d323c; - --tb-surface-hover: rgba(92, 141, 255, 0.12); - --tb-border: #3a404c; - --tb-text: #d8dce4; - --tb-text-secondary: #a8b0bd; - --tb-text-muted: #8b93a1; - --tb-primary: #5c8dff; - --tb-primary-weak: rgba(92, 141, 255, 0.14); - --tb-shadow: 0 1px 2px rgba(0, 0, 0, 0.30), 0 12px 28px -18px rgba(0, 0, 0, 0.55); - --tb-shadow-hover: 0 1px 2px rgba(0, 0, 0, 0.35), 0 18px 40px -20px rgba(0, 0, 0, 0.65); - } - -/* UIkit 补丁:复刻原自建版 "modify-black-font"(官方版与自建版仅 3 处颜色差异——把有色容器内默认白字/白标题改为黑字)。 - 模板当前只用到 .uk-light 容器(用户背景封面,question/list.html、question-not-found.html; - 其中 h3/p 已由 #usernamecolor/#introcolor 内联 style 覆盖),故只需补该容器的 3 处投影即可保持渲染一致。 - 若日后新增 .uk-card-primary/.uk-card-secondary/.uk-section-primary/.uk-tile-*/.uk-overlay-primary 等容器且需黑字,须同步扩展。 */ - .uk-light { color: rgba(0,0,0,.7); } - .uk-light .uk-h1, .uk-light .uk-h2, .uk-light .uk-h3, .uk-light .uk-h4, .uk-light .uk-h5, .uk-light .uk-h6, - .uk-light .uk-heading-2xlarge, .uk-light .uk-heading-large, .uk-light .uk-heading-medium, .uk-light .uk-heading-small, .uk-light .uk-heading-xlarge, - .uk-light h1, .uk-light h2, .uk-light h3, .uk-light h4, .uk-light h5, .uk-light h6 { color: #000; } - .uk-light .uk-table th { color: rgba(0,0,0,.7); } - - .grecaptcha-badge {visibility: hidden;} - .icon-container { - display: flex; - justify-content: center; - width: 100%; - } - .icon { - margin-left: 10px; - width: 20px; /* 设置图标的宽度 */ - height: auto; /* 保持图标的高度自动调整 */ - } - .emoji-placeholder { - display: inline-block; - animation: jump 0.5s ease-in-out; - } - - @keyframes jump { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-5px); } - } - /* 防止 UIkit Dropbar 初始闪烁 */ - .uk-dropbar { - opacity: 0; - visibility: hidden; - transition: opacity 0.2s ease, visibility 0.2s ease; - } - - .uk-dropbar.uk-open { - opacity: 1; - visibility: visible; - } - - a.uk-button.uk-button-link { - text-transform: none; - } - - /* 移动端优化:缩小字体和间距,避免右侧空白 */ - @media (max-width: 640px) { - .uk-logo { - font-size: 1rem; - } - .uk-navbar-nav a { - font-size: 0.78rem; - padding: 0 4px; - } - .emoji-placeholder { - font-size: 1rem; - } - /* 移动端用户名限宽省略,避免长英文用户名撑爆顶栏 */ - .tb-nav-username { - max-width: 64px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - display: inline-block; - vertical-align: middle; - } - } - -/* 深色模式:html[data-theme="dark"] 时覆盖 UIkit 3.3.3 编译主题的关键颜色 */ - /* 变量(--tb-*)已上移到 head 顶部的令牌块,亮暗两套同名常量在那里定义。 */ - /* 根元素与 body 一起覆盖,确保整个视口(含内容不足一屏时的空白区)都是深色背景 */ - html[data-theme="dark"], - html[data-theme="dark"] body { - background-color: var(--tb-bg) !important; - color: var(--tb-text) !important; - } - /* 通用表面容器 */ - html[data-theme="dark"] .uk-card, - html[data-theme="dark"] .uk-card-default, - html[data-theme="dark"] .uk-card-secondary, - html[data-theme="dark"] .uk-dropbar, - html[data-theme="dark"] .uk-navbar-dropdown, - html[data-theme="dark"] .uk-modal-dialog, - html[data-theme="dark"] .uk-dropdown, - html[data-theme="dark"] .uk-offcanvas-bar, - html[data-theme="dark"] .uk-section, - html[data-theme="dark"] .uk-tile, - html[data-theme="dark"] .uk-panel, - html[data-theme="dark"] .uk-background-default, - html[data-theme="dark"] .uk-search-default .uk-search-input { - background-color: var(--tb-surface) !important; - color: var(--tb-text) !important; - } - html[data-theme="dark"] .uk-navbar-container { - background-color: var(--tb-surface) !important; - color: var(--tb-text) !important; - } - html[data-theme="dark"] .uk-navbar-nav > li > a, - html[data-theme="dark"] .uk-card a, - html[data-theme="dark"] .uk-link, - html[data-theme="dark"] a.uk-button-link, - html[data-theme="dark"] .uk-navbar-dropdown-nav > li > a, - html[data-theme="dark"] .uk-dropdown-nav > li > a, - html[data-theme="dark"] .uk-list a { - color: var(--tb-text) !important; - } - html[data-theme="dark"] .uk-navbar-nav > li > a:hover, - html[data-theme="dark"] .uk-card a:hover, - html[data-theme="dark"] .uk-navbar-dropdown-nav > li > a:hover, - html[data-theme="dark"] .uk-dropdown-nav > li > a:hover { - color: var(--tb-primary) !important; - } - html[data-theme="dark"] .uk-input, - html[data-theme="dark"] .uk-textarea, - html[data-theme="dark"] .uk-select { - background-color: var(--tb-surface-2) !important; - color: var(--tb-text) !important; - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] .uk-input:focus, - html[data-theme="dark"] .uk-textarea:focus, - html[data-theme="dark"] .uk-select:focus { - border-color: var(--tb-primary) !important; - } - html[data-theme="dark"] .uk-text-muted, - html[data-theme="dark"] .uk-text-meta, - html[data-theme="dark"] .uk-text-secondary, - html[data-theme="dark"] .uk-text-muted a, - html[data-theme="dark"] .uk-text-small.uk-text-muted { - color: var(--tb-text-muted) !important; - } - html[data-theme="dark"] .uk-divider-icon::after, - html[data-theme="dark"] hr, - html[data-theme="dark"] .uk-table th, - html[data-theme="dark"] .uk-table td, - html[data-theme="dark"] .uk-list-divider > li:nth-child(n+2) { - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] .uk-button-default { - background-color: var(--tb-surface-2) !important; - color: var(--tb-text) !important; - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] .uk-modal-title, - html[data-theme="dark"] h1, html[data-theme="dark"] h2, html[data-theme="dark"] h3, - html[data-theme="dark"] h4, html[data-theme="dark"] h5, html[data-theme="dark"] h6, - html[data-theme="dark"] .uk-card-title { - color: var(--tb-text) !important; - } - html[data-theme="dark"] .uk-alert-warning, - html[data-theme="dark"] .uk-alert-danger, - html[data-theme="dark"] .uk-alert-success, - html[data-theme="dark"] .uk-alert-primary { - background-color: var(--tb-surface-2) !important; - color: var(--tb-text) !important; - } - html[data-theme="dark"] #math-captcha-modal .uk-modal-dialog { - background-color: var(--tb-surface) !important; - color: var(--tb-text) !important; - } - html[data-theme="dark"] #math-captcha-modal .captcha-equation-box { - background-color: var(--tb-surface-2) !important; - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] #math-captcha-modal .math-captcha-modal-question { - color: var(--tb-text) !important; - } - html[data-theme="dark"] #math-captcha-modal #captcha-modal-answer { - background-color: var(--tb-surface) !important; - color: var(--tb-text) !important; - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] #math-captcha-modal #captcha-modal-answer::placeholder { - color: var(--tb-text-muted) !important; - } - html[data-theme="dark"] #math-captcha-modal .uk-text-meta, - html[data-theme="dark"] #math-captcha-modal #captcha-modal-refresh { - color: var(--tb-text-muted) !important; - } - html[data-theme="dark"] #math-captcha-modal #captcha-modal-refresh:hover { - color: var(--tb-primary) !important; - } - html[data-theme="dark"] .uk-label { - color: #fff !important; - } - /* 自定义组件(探索页推荐卡片等)深色覆盖 */ - html[data-theme="dark"] .recommend-card, - html[data-theme="dark"] .recommend-card:hover { - background-color: var(--tb-surface-2) !important; - } - html[data-theme="dark"] .explore-name a { - color: var(--tb-text) !important; - } - html[data-theme="dark"] .explore-name a:hover { - color: var(--tb-primary) !important; - } - html[data-theme="dark"] .explore-intro { - color: var(--tb-text-muted) !important; - } - /* 切换按钮图标切换 */ - #theme-toggle .tb-icon-dark, #theme-toggle-mobile .tb-icon-dark { display: none; } - html[data-theme="dark"] #theme-toggle .tb-icon-light, - html[data-theme="dark"] #theme-toggle-mobile .tb-icon-light { display: none; } - html[data-theme="dark"] #theme-toggle .tb-icon-dark, - html[data-theme="dark"] #theme-toggle-mobile .tb-icon-dark { display: inline; } - -/* 基础质感层:只调整外观(圆角/投影/字体/数字对齐),不改任何结构与布局。 - 颜色一律取自头部令牌,亮暗两套自动生效,因此这里不需要再写 html[data-theme="dark"] 分支。 */ - /* 根元素与 body 同时上色:内容不足一屏时,body 之外的视口区域也要是同一底色 */ - html, - body { - font-family: var(--tb-font); - background-color: var(--tb-bg); - color: var(--tb-text); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - /* 卡片:12px 圆角 + 一层极淡接触影 + 一层大扩散影(UIkit 默认的 0 5px 15px 偏硬) */ - .uk-card { - border-radius: var(--tb-radius); - } - .uk-card-default { - background-color: var(--tb-surface); - box-shadow: var(--tb-shadow); - } - .uk-card-default.uk-card-hover:hover { - box-shadow: var(--tb-shadow-hover); - } - .uk-card-header { - border-radius: var(--tb-radius) var(--tb-radius) 0 0; - } - .uk-card-footer { - border-radius: 0 0 var(--tb-radius) var(--tb-radius); - } - .uk-card, - .uk-button, - .uk-alert, - .uk-label { - transition: box-shadow 0.18s ease, background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease; - } - /* 交互元素:统一 8px 圆角,标签 6px */ - .uk-button { - border-radius: var(--tb-radius-sm); - } - .uk-input, - .uk-select, - .uk-textarea, - .uk-form-small, - .uk-search-input { - border-radius: var(--tb-radius-sm); - } - .uk-label { - border-radius: var(--tb-radius-xs); - } - .uk-alert { - border-radius: 10px; - } - .uk-modal-dialog { - border-radius: 14px; - } - .uk-dropdown, - .uk-navbar-dropdown { - border-radius: 10px; - box-shadow: var(--tb-shadow); - } - .uk-navbar-container:not(.uk-navbar-transparent) { - background-color: var(--tb-surface); - } - /* 键盘可达性:仅在键盘聚焦时出现描边,鼠标点击不出现 */ - .uk-button:focus-visible, - .uk-navbar-nav > li > a:focus-visible { - outline: 2px solid var(--tb-primary); - outline-offset: 2px; - } - /* 次要文字统一走令牌,避免 #999 / #8a919c 混用 */ - .uk-text-muted, - .uk-text-meta { - color: var(--tb-text-muted); - } - /* 统计/日期等数字列等宽对齐,避免刷新时左右跳动 */ - .uk-table, - .tb-num { - font-variant-numeric: tabular-nums; - } - /* 图片上传条:原为 item/new-question 两处各写一份内联 style(写死 #f8f8f8,深色下露白), - 收敛到一处并走令牌,亮暗自动适配 */ - .tb-img-uploader { - width: 100%; - background-color: var(--tb-surface-2); - display: flex; - padding-top: 5px; - padding-bottom: 5px; - align-items: center; - justify-content: flex-start; - gap: 5px; - } - /* 背景图封面(提问箱主页):渐变遮罩,保证用户自选文字色叠加后仍可读。 - 默认浅色遮罩(页面 .uk-light 补丁把文字兜底成深色);若箱主选了浅色文字, - question/list.html 会按亮度加上 .tb-cover-dark 换成深色遮罩。 */ - .tb-cover { - position: relative; - border-radius: var(--tb-radius) var(--tb-radius) 0 0; - overflow: hidden; - } - /* ⚠️ 必须带 .uk-flex 复合选择器:UIkit 3.3.3 里有 - .uk-flex::after,.uk-flex::before{display:none}(优先级 0,2,0), - 会把这个遮罩伪元素整个干掉——不报错、只是什么都不显示(踩过一次)。 - 单写 .tb-cover::after 优先级不够,会被它压回 display:none。 */ - .tb-cover::after, - .tb-cover.uk-flex::after { - content: ""; - display: block; - position: absolute; - inset: 0; - z-index: 0; - background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.58) 55%, rgba(255, 255, 255, 0.84) 100%); - pointer-events: none; - } - .tb-cover.tb-cover-dark::after { - background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.16) 45%, rgba(0, 0, 0, 0.46) 100%); - } - .tb-cover > * { - position: relative; - z-index: 1; - } - .tb-cover h3, - .tb-cover p { - text-shadow: 0 1px 10px rgba(255, 255, 255, 0.65); - } - .tb-cover.tb-cover-dark h3, - .tb-cover.tb-cover-dark p { - text-shadow: 0 1px 12px rgba(0, 0, 0, 0.45); - } - /* 尊重系统的减少动效设置 */ - @media (prefers-reduced-motion: reduce) { - .uk-card, - .uk-button, - .emoji-placeholder { - transition: none; - animation: none; - } - } - -/* 顶栏自适应:导航全部直接显示,窄屏时分级缩小字号/间距,保证英文/日文等长文字不溢出 */ - .uk-navbar-nav a { - font-size: 0.95rem; - padding: 0 10px; - } - /* 未登录移动端占位:桌面端隐藏,仅 ≤640px 显示(见下方 media 查询)。 - 桌面端布局与改动前完全一致,不受影响。 */ - .tb-mobile-balancer { display: none; } - /* 桌面端保持 UIkit 原生居中三列布局(无需额外 CSS) */ - .tb-navbar .uk-logo { - white-space: nowrap; - } - @media (max-width: 1200px) { - .uk-navbar-nav a { - font-size: 0.88rem; - padding: 0 8px; - } - } - @media (max-width: 1000px) { - .uk-navbar-nav a { - font-size: 0.82rem; - padding: 0 6px; - } - .uk-logo { - font-size: 1.1rem; - } - /* 登录用户名限宽,过长省略号截断 */ - .tb-nav-username { - max-width: 110px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - display: inline-block; - vertical-align: middle; - } - } - @media (max-width: 800px) { - .uk-navbar-nav a { - font-size: 0.78rem; - padding: 0 4px; - } - .uk-logo { - font-size: 1rem; - } - .tb-nav-username { - max-width: 80px; - } - } - /* 手机端(≤640px):沿用 UIkit 桌面端 navbar 的原生三列定位。 - 不再重写 .uk-navbar-center / center-left / center-right 的定位, - 避免 logo 被挤到下一行;底部语言与深色模式按钮仍由下方独立栏承载。 */ - @media (max-width: 640px) { - .tb-navbar { - position: relative; - min-height: 56px; - overflow: visible; - } - .tb-navbar > .uk-navbar-center { - position: relative !important; - top: auto !important; - left: auto !important; - right: auto !important; - bottom: auto !important; - transform: none !important; - display: flex !important; - align-items: center; - justify-content: center; - width: 100%; - min-height: 56px; - white-space: nowrap; - flex-wrap: nowrap; - } - .tb-navbar .uk-navbar-center-left, - .tb-navbar .uk-navbar-center-right { - position: static !important; - top: auto !important; - left: auto !important; - right: auto !important; - bottom: auto !important; - transform: none !important; - display: flex !important; - align-items: center; - flex: 0 0 auto; - white-space: nowrap; - } - .tb-navbar .uk-navbar-center-left > div, - .tb-navbar .uk-navbar-center-right > div { - display: flex; - align-items: center; - white-space: nowrap; - } - .tb-navbar .uk-navbar-nav { - display: flex; - flex-wrap: nowrap; - flex: 0 0 auto; - } - .tb-navbar .uk-navbar-nav > li > a, - .tb-navbar .uk-logo { - white-space: nowrap; - } - /* 未登录:移动端显示与「我的」等宽的隐藏占位,使左栏「探索+占位」与 - 右栏「注册/登录」两项等宽,logo 真正居中,左右视觉平衡 */ - .tb-mobile-balancer { display: block; } - } - -/* 标签样式的按钮(管理页置顶等): - 尺寸完全继承 .uk-label(font-size/padding/line-height/text-transform 等), - 仅覆盖 button 元素特有的默认样式(边框/字体族/光标等),保证与相邻 span 标签完全一致。 */ - .tb-label-btn { - border: none !important; - cursor: pointer; - font-family: inherit; - margin: 0; - vertical-align: middle; - outline: none; - background-image: none; - box-sizing: border-box; - -webkit-appearance: none; - appearance: none; - } - .tb-label-btn:hover { - opacity: 0.85; - } - /* 公开页置顶小图标:仅一个图钉,不占行高、不影响原布局 */ - .tb-pin-flag { - font-size: 0.9em; - margin-right: 4px; - vertical-align: -1px; - cursor: default; - } - -/* Markdown 渲染排版(问题/回答/追问正文,左对齐以保留排版格式) */ - /* 纯文本列表:保留换行与连续空格(pre-wrap),不渲染 Markdown 大字号/彩色 */ - .tb-preline { - white-space: pre-wrap; - } - /* 纯文本列表:超过 5 行截断并显示省略号(不足 5 行则完整显示) */ - .tb-clamp-5 { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 5; - overflow: hidden; - } - .md-body { - line-height: 1.7; - text-align: left; - word-break: break-word; - } - .md-body p { margin: 0.5em 0; } - .md-body h1, .md-body h2, .md-body h3, - .md-body h4, .md-body h5, .md-body h6 { - margin: 0.8em 0 0.4em; - font-weight: 600; - line-height: 1.4; - } - .md-body ul, .md-body ol { padding-left: 1.6em; margin: 0.5em 0; } - .md-body li { margin: 0.2em 0; } - .md-body blockquote { - border-left: 3px solid #d3d1c7; - margin: 0.6em 0; - padding: 0.2em 1em; - color: #5f5e5a; - } - .md-body pre { - background: #f5f5f5; - padding: 0.8em 1em; - border-radius: 6px; - overflow-x: auto; - margin: 0.6em 0; - } - .md-body code { - background: #f0f0f0; - padding: 0.1em 0.35em; - border-radius: 4px; - font-size: 0.9em; - } - .md-body pre code { background: none; padding: 0; } - .md-body img { max-width: 100%; height: auto; } - .md-body table { border-collapse: collapse; margin: 0.6em 0; } - .md-body th, .md-body td { - border: 1px solid #ddd; - padding: 0.4em 0.8em; - } - .md-body hr { margin: 1em 0; } - html[data-theme="dark"] .md-body blockquote { - border-color: #3a404c; - color: #8b93a1; - } - html[data-theme="dark"] .md-body pre, - html[data-theme="dark"] .md-body code { background: #2d323c; } - html[data-theme="dark"] .md-body th, - html[data-theme="dark"] .md-body td { border-color: #3a404c; } - -/* 投票结果占比条:UIkit 3 已无 .uk-progressbar 类(那是 UIkit 2 的), - 模板用
结构,需补一套填充样式, - 否则内层填充无高度/背景,所有占比条都只剩外层灰色轨道,看起来一模一样。 */ - .uk-progress { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - height: 10px; - margin: 0; - border: none; - border-radius: 6px; - background-color: #e5e5e5; - overflow: hidden; - box-shadow: none; - } - .uk-progressbar { - display: block; - height: 100%; - background-color: #1e87f0; - border-radius: 6px 0 0 6px; - transition: width 0.3s ease; - } - html[data-theme="dark"] .uk-progress { - background-color: var(--tb-border); - } - html[data-theme="dark"] .uk-progressbar { - background-color: var(--tb-primary); - } - -/* 移动端底部操作栏:固定底栏承载语言切换与深色模式,桌面端隐藏 */ - #tb-mobile-utils { - display: none; - } - @media (max-width: 640px) { - /* 顶栏不再容纳语言/深色按钮,恢复原有导航空间 */ - .tb-nav-utils { - display: none !important; - } - /* 手机端隐藏登录用户名(左边仅保留"探索"),保证 logo 居中、 - 两侧各不超过 2 个短按钮,避免用户名宽度挤压布局 */ - .tb-nav-username { - display: none !important; - } - /* 固定底部操作栏 */ - #tb-mobile-utils { - display: flex; - position: fixed; - left: 0; - right: 0; - bottom: 0; - z-index: 1000; - justify-content: center; - align-items: center; - gap: 10px; - padding: 8px 12px calc(8px + env(safe-area-inset-bottom)); - background: rgba(255, 255, 255, 0.92); - -webkit-backdrop-filter: blur(8px); - backdrop-filter: blur(8px); - box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08); - } - #tb-mobile-utils .uk-select { - width: 110px; - } - html[data-theme="dark"] #tb-mobile-utils { - background: rgba(36, 40, 48, 0.92); - box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.4); - } - /* 给页面底部预留空间,避免页脚内容被固定底栏遮挡 */ - body { - padding-bottom: 56px; - } - } diff --git a/outputs/verify/box.html b/outputs/verify/box.html deleted file mode 100644 index 6f51475..0000000 --- a/outputs/verify/box.html +++ /dev/null @@ -1,1275 +0,0 @@ - - - - - - 狼の質問箱 | 匿名質問箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - -
-
-
- -
-

admin3x的提问箱

-

此提问箱已设置访问密码,请输入密码查看内容

- - - - - - - - - - - -
- - -
-
- - -
-
-
- -
-
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/verify/box6-preview.html b/outputs/verify/box6-preview.html deleted file mode 100644 index e7f9537..0000000 --- a/outputs/verify/box6-preview.html +++ /dev/null @@ -1,1729 +0,0 @@ - - - - - - admin6 - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - -
-
-
- -

admin6

-

问你想问的

-
- - - - - - - bloglink - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@admin6提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - -
- - - diff --git a/outputs/verify/box6.html b/outputs/verify/box6.html deleted file mode 100644 index 84c4ab7..0000000 --- a/outputs/verify/box6.html +++ /dev/null @@ -1,1063 +0,0 @@ - - - - - - admin6 - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - -
-
-
- -

admin6

-

问你想问的

-
- - - - - - - bloglink - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@admin6提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/verify/c3.json b/outputs/verify/c3.json deleted file mode 100644 index 9fc30f4..0000000 --- a/outputs/verify/c3.json +++ /dev/null @@ -1 +0,0 @@ -{"code":0,"data":{"id":"af87f847522ec487","question":"10 × 5 ="}} diff --git a/outputs/verify/cap.json b/outputs/verify/cap.json deleted file mode 100644 index 656fdc1..0000000 --- a/outputs/verify/cap.json +++ /dev/null @@ -1 +0,0 @@ -{"code":0,"data":{"id":"da49922027925285","question":"6 × 2 ="}} diff --git a/outputs/verify/cap2.json b/outputs/verify/cap2.json deleted file mode 100644 index 6041960..0000000 --- a/outputs/verify/cap2.json +++ /dev/null @@ -1 +0,0 @@ -{"code":0,"data":{"id":"1ca74ac2fda2ab07","question":"72 + 75 ="}} diff --git a/outputs/verify/cj.txt b/outputs/verify/cj.txt deleted file mode 100644 index 2317ec0..0000000 --- a/outputs/verify/cj.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Netscape HTTP Cookie File -# https://curl.se/docs/http-cookies.html -# This file was generated by libcurl! Edit at your own risk. - -#HttpOnly_box-dev-api.337845818.best FALSE / TRUE 0 flamego_session 19alv4sneiwlykv6 diff --git a/outputs/verify/cj2.txt b/outputs/verify/cj2.txt deleted file mode 100644 index ff5cd79..0000000 --- a/outputs/verify/cj2.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Netscape HTTP Cookie File -# https://curl.se/docs/http-cookies.html -# This file was generated by libcurl! Edit at your own risk. - -#HttpOnly_box-dev-api.337845818.best FALSE / TRUE 0 flamego_session hprz8iav3vfy6tzb diff --git a/outputs/verify/cj3.txt b/outputs/verify/cj3.txt deleted file mode 100644 index 9fb38cf..0000000 --- a/outputs/verify/cj3.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Netscape HTTP Cookie File -# https://curl.se/docs/http-cookies.html -# This file was generated by libcurl! Edit at your own risk. - -#HttpOnly_box-dev-api.337845818.best FALSE / TRUE 0 flamego_session xzuo76g1wkt93yde diff --git a/outputs/verify/d-base.css b/outputs/verify/d-base.css deleted file mode 100644 index c5d2a97..0000000 --- a/outputs/verify/d-base.css +++ /dev/null @@ -1,670 +0,0 @@ -/* TamaBox 全站基础样式:设计令牌 / 主题覆盖 / 组件质感 / 响应式。 - 由 base/header.html 内联块抽出,集中一份便于缓存与维护。 - ⚠️ 改完务必把 header 里的 ?v= 版本号 +1,否则老访客仍用缓存。 */ - -/* 设计令牌:亮色为默认(:root),深色在 html[data-theme="dark"] 覆盖。 - 页面与组件样式一律引用 var(--tb-*),不再写死色值/圆角/阴影; - var() 的第二个参数仅作兜底(旧缓存页、邮件模板等不引入本文件的场景)。 - 改配色只动这一块,亮暗两套自动保持一致。 */ - :root { - --tb-bg: #f7f8fa; - --tb-surface: #ffffff; - --tb-surface-2: #f4f6f8; - --tb-surface-hover: #f3f7ff; - --tb-border: #e8eaee; - --tb-text: #1f2329; - --tb-text-secondary: #5c6370; - --tb-text-muted: #8a919c; - --tb-primary: #1e87f0; - --tb-primary-weak: rgba(30, 135, 240, 0.10); - --tb-radius: 12px; - --tb-radius-sm: 8px; - --tb-radius-xs: 6px; - --tb-shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 12px 28px -18px rgba(16, 24, 40, 0.20); - --tb-shadow-hover: 0 1px 2px rgba(16, 24, 40, 0.05), 0 18px 40px -20px rgba(16, 24, 40, 0.28); - --tb-font: system-ui, -apple-system, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif; - } - html[data-theme="dark"] { - --tb-bg: #1a1d23; - --tb-surface: #242830; - --tb-surface-2: #2d323c; - --tb-surface-hover: rgba(92, 141, 255, 0.12); - --tb-border: #3a404c; - --tb-text: #d8dce4; - --tb-text-secondary: #a8b0bd; - --tb-text-muted: #8b93a1; - --tb-primary: #5c8dff; - --tb-primary-weak: rgba(92, 141, 255, 0.14); - --tb-shadow: 0 1px 2px rgba(0, 0, 0, 0.30), 0 12px 28px -18px rgba(0, 0, 0, 0.55); - --tb-shadow-hover: 0 1px 2px rgba(0, 0, 0, 0.35), 0 18px 40px -20px rgba(0, 0, 0, 0.65); - } - -/* UIkit 补丁:复刻原自建版 "modify-black-font"(官方版与自建版仅 3 处颜色差异——把有色容器内默认白字/白标题改为黑字)。 - 模板当前只用到 .uk-light 容器(用户背景封面,question/list.html、question-not-found.html; - 其中 h3/p 已由 #usernamecolor/#introcolor 内联 style 覆盖),故只需补该容器的 3 处投影即可保持渲染一致。 - 若日后新增 .uk-card-primary/.uk-card-secondary/.uk-section-primary/.uk-tile-*/.uk-overlay-primary 等容器且需黑字,须同步扩展。 */ - .uk-light { color: rgba(0,0,0,.7); } - .uk-light .uk-h1, .uk-light .uk-h2, .uk-light .uk-h3, .uk-light .uk-h4, .uk-light .uk-h5, .uk-light .uk-h6, - .uk-light .uk-heading-2xlarge, .uk-light .uk-heading-large, .uk-light .uk-heading-medium, .uk-light .uk-heading-small, .uk-light .uk-heading-xlarge, - .uk-light h1, .uk-light h2, .uk-light h3, .uk-light h4, .uk-light h5, .uk-light h6 { color: #000; } - .uk-light .uk-table th { color: rgba(0,0,0,.7); } - - .grecaptcha-badge {visibility: hidden;} - .icon-container { - display: flex; - justify-content: center; - width: 100%; - } - .icon { - margin-left: 10px; - width: 20px; /* 设置图标的宽度 */ - height: auto; /* 保持图标的高度自动调整 */ - } - .emoji-placeholder { - display: inline-block; - animation: jump 0.5s ease-in-out; - } - - @keyframes jump { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-5px); } - } - /* 防止 UIkit Dropbar 初始闪烁 */ - .uk-dropbar { - opacity: 0; - visibility: hidden; - transition: opacity 0.2s ease, visibility 0.2s ease; - } - - .uk-dropbar.uk-open { - opacity: 1; - visibility: visible; - } - - a.uk-button.uk-button-link { - text-transform: none; - } - - /* 移动端优化:缩小字体和间距,避免右侧空白 */ - @media (max-width: 640px) { - .uk-logo { - font-size: 1rem; - } - .uk-navbar-nav a { - font-size: 0.78rem; - padding: 0 4px; - } - .emoji-placeholder { - font-size: 1rem; - } - /* 移动端用户名限宽省略,避免长英文用户名撑爆顶栏 */ - .tb-nav-username { - max-width: 64px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - display: inline-block; - vertical-align: middle; - } - } - -/* 深色模式:html[data-theme="dark"] 时覆盖 UIkit 3.3.3 编译主题的关键颜色 */ - /* 变量(--tb-*)已上移到 head 顶部的令牌块,亮暗两套同名常量在那里定义。 */ - /* 根元素与 body 一起覆盖,确保整个视口(含内容不足一屏时的空白区)都是深色背景 */ - html[data-theme="dark"], - html[data-theme="dark"] body { - background-color: var(--tb-bg) !important; - color: var(--tb-text) !important; - } - /* 通用表面容器 */ - html[data-theme="dark"] .uk-card, - html[data-theme="dark"] .uk-card-default, - html[data-theme="dark"] .uk-card-secondary, - html[data-theme="dark"] .uk-dropbar, - html[data-theme="dark"] .uk-navbar-dropdown, - html[data-theme="dark"] .uk-modal-dialog, - html[data-theme="dark"] .uk-dropdown, - html[data-theme="dark"] .uk-offcanvas-bar, - html[data-theme="dark"] .uk-section, - html[data-theme="dark"] .uk-tile, - html[data-theme="dark"] .uk-panel, - html[data-theme="dark"] .uk-background-default, - html[data-theme="dark"] .uk-search-default .uk-search-input { - background-color: var(--tb-surface) !important; - color: var(--tb-text) !important; - } - html[data-theme="dark"] .uk-navbar-container { - background-color: var(--tb-surface) !important; - color: var(--tb-text) !important; - } - html[data-theme="dark"] .uk-navbar-nav > li > a, - html[data-theme="dark"] .uk-card a, - html[data-theme="dark"] .uk-link, - html[data-theme="dark"] a.uk-button-link, - html[data-theme="dark"] .uk-navbar-dropdown-nav > li > a, - html[data-theme="dark"] .uk-dropdown-nav > li > a, - html[data-theme="dark"] .uk-list a { - color: var(--tb-text) !important; - } - html[data-theme="dark"] .uk-navbar-nav > li > a:hover, - html[data-theme="dark"] .uk-card a:hover, - html[data-theme="dark"] .uk-navbar-dropdown-nav > li > a:hover, - html[data-theme="dark"] .uk-dropdown-nav > li > a:hover { - color: var(--tb-primary) !important; - } - html[data-theme="dark"] .uk-input, - html[data-theme="dark"] .uk-textarea, - html[data-theme="dark"] .uk-select { - background-color: var(--tb-surface-2) !important; - color: var(--tb-text) !important; - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] .uk-input:focus, - html[data-theme="dark"] .uk-textarea:focus, - html[data-theme="dark"] .uk-select:focus { - border-color: var(--tb-primary) !important; - } - html[data-theme="dark"] .uk-text-muted, - html[data-theme="dark"] .uk-text-meta, - html[data-theme="dark"] .uk-text-secondary, - html[data-theme="dark"] .uk-text-muted a, - html[data-theme="dark"] .uk-text-small.uk-text-muted { - color: var(--tb-text-muted) !important; - } - html[data-theme="dark"] .uk-divider-icon::after, - html[data-theme="dark"] hr, - html[data-theme="dark"] .uk-table th, - html[data-theme="dark"] .uk-table td, - html[data-theme="dark"] .uk-list-divider > li:nth-child(n+2) { - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] .uk-button-default { - background-color: var(--tb-surface-2) !important; - color: var(--tb-text) !important; - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] .uk-modal-title, - html[data-theme="dark"] h1, html[data-theme="dark"] h2, html[data-theme="dark"] h3, - html[data-theme="dark"] h4, html[data-theme="dark"] h5, html[data-theme="dark"] h6, - html[data-theme="dark"] .uk-card-title { - color: var(--tb-text) !important; - } - html[data-theme="dark"] .uk-alert-warning, - html[data-theme="dark"] .uk-alert-danger, - html[data-theme="dark"] .uk-alert-success, - html[data-theme="dark"] .uk-alert-primary { - background-color: var(--tb-surface-2) !important; - color: var(--tb-text) !important; - } - html[data-theme="dark"] #math-captcha-modal .uk-modal-dialog { - background-color: var(--tb-surface) !important; - color: var(--tb-text) !important; - } - html[data-theme="dark"] #math-captcha-modal .captcha-equation-box { - background-color: var(--tb-surface-2) !important; - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] #math-captcha-modal .math-captcha-modal-question { - color: var(--tb-text) !important; - } - html[data-theme="dark"] #math-captcha-modal #captcha-modal-answer { - background-color: var(--tb-surface) !important; - color: var(--tb-text) !important; - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] #math-captcha-modal #captcha-modal-answer::placeholder { - color: var(--tb-text-muted) !important; - } - html[data-theme="dark"] #math-captcha-modal .uk-text-meta, - html[data-theme="dark"] #math-captcha-modal #captcha-modal-refresh { - color: var(--tb-text-muted) !important; - } - html[data-theme="dark"] #math-captcha-modal #captcha-modal-refresh:hover { - color: var(--tb-primary) !important; - } - html[data-theme="dark"] .uk-label { - color: #fff !important; - } - /* 自定义组件(探索页推荐卡片等)深色覆盖 */ - html[data-theme="dark"] .recommend-card, - html[data-theme="dark"] .recommend-card:hover { - background-color: var(--tb-surface-2) !important; - } - html[data-theme="dark"] .explore-name a { - color: var(--tb-text) !important; - } - html[data-theme="dark"] .explore-name a:hover { - color: var(--tb-primary) !important; - } - html[data-theme="dark"] .explore-intro { - color: var(--tb-text-muted) !important; - } - /* 切换按钮图标切换(内联 SVG,不依赖 UIkit 图标库) */ - #theme-toggle .tb-icon-light, #theme-toggle-mobile .tb-icon-light, - #theme-toggle .tb-icon-dark, #theme-toggle-mobile .tb-icon-dark { - display: inline-flex; - align-items: center; - } - #theme-toggle .tb-icon-light svg, #theme-toggle-mobile .tb-icon-light svg, - #theme-toggle .tb-icon-dark svg, #theme-toggle-mobile .tb-icon-dark svg { display: block; } - #theme-toggle .tb-icon-dark, #theme-toggle-mobile .tb-icon-dark { display: none; } - html[data-theme="dark"] #theme-toggle .tb-icon-light, - html[data-theme="dark"] #theme-toggle-mobile .tb-icon-light { display: none; } - html[data-theme="dark"] #theme-toggle .tb-icon-dark, - html[data-theme="dark"] #theme-toggle-mobile .tb-icon-dark { display: inline-flex; } - -/* 基础质感层:只调整外观(圆角/投影/字体/数字对齐),不改任何结构与布局。 - 颜色一律取自头部令牌,亮暗两套自动生效,因此这里不需要再写 html[data-theme="dark"] 分支。 */ - /* 根元素与 body 同时上色:内容不足一屏时,body 之外的视口区域也要是同一底色 */ - html, - body { - font-family: var(--tb-font); - background-color: var(--tb-bg); - color: var(--tb-text); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - /* 卡片:12px 圆角 + 一层极淡接触影 + 一层大扩散影(UIkit 默认的 0 5px 15px 偏硬) */ - .uk-card { - border-radius: var(--tb-radius); - } - .uk-card-default { - background-color: var(--tb-surface); - box-shadow: var(--tb-shadow); - } - .uk-card-default.uk-card-hover:hover { - box-shadow: var(--tb-shadow-hover); - } - .uk-card-header { - border-radius: var(--tb-radius) var(--tb-radius) 0 0; - } - .uk-card-footer { - border-radius: 0 0 var(--tb-radius) var(--tb-radius); - } - .uk-card, - .uk-button, - .uk-alert, - .uk-label { - transition: box-shadow 0.18s ease, background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease; - } - /* 交互元素:统一 8px 圆角,标签 6px */ - .uk-button { - border-radius: var(--tb-radius-sm); - } - .uk-input, - .uk-select, - .uk-textarea, - .uk-form-small, - .uk-search-input { - border-radius: var(--tb-radius-sm); - } - .uk-label { - border-radius: var(--tb-radius-xs); - } - .uk-alert { - border-radius: 10px; - } - .uk-modal-dialog { - border-radius: 14px; - } - .uk-dropdown, - .uk-navbar-dropdown { - border-radius: 10px; - box-shadow: var(--tb-shadow); - } - .uk-navbar-container:not(.uk-navbar-transparent) { - background-color: var(--tb-surface); - } - /* 键盘可达性:仅在键盘聚焦时出现描边,鼠标点击不出现 */ - .uk-button:focus-visible, - .uk-navbar-nav > li > a:focus-visible { - outline: 2px solid var(--tb-primary); - outline-offset: 2px; - } - /* 次要文字统一走令牌,避免 #999 / #8a919c 混用 */ - .uk-text-muted, - .uk-text-meta { - color: var(--tb-text-muted); - } - -/* 轻量胶囊:用于投票类型、热门标签等「只是分类信息」的位置。 - 实心 uk-label 一屏多个会变成一排色块,这里统一降级为浅底 + 次要色文字。 */ -.tb-chip { - display: inline-block; - padding: 1px 8px; - border-radius: var(--tb-radius-xs); - background-color: var(--tb-surface-2); - color: var(--tb-text-secondary); - font-size: 0.72rem; - line-height: 1.7; - white-space: nowrap; -} -a.tb-chip:hover { - background-color: var(--tb-primary-weak); - color: var(--tb-primary); - text-decoration: none; -} - -/* 统计/日期等数字列等宽对齐,避免刷新时左右跳动 */ - .uk-table, - .tb-num { - font-variant-numeric: tabular-nums; - } - /* 图片上传条:原为 item/new-question 两处各写一份内联 style(写死 #f8f8f8,深色下露白), - 收敛到一处并走令牌,亮暗自动适配 */ - .tb-img-uploader { - width: 100%; - background-color: var(--tb-surface-2); - display: flex; - padding-top: 5px; - padding-bottom: 5px; - align-items: center; - justify-content: flex-start; - gap: 5px; - } - /* 背景图封面(提问箱主页):渐变遮罩,保证用户自选文字色叠加后仍可读。 - 默认浅色遮罩(页面 .uk-light 补丁把文字兜底成深色);若箱主选了浅色文字, - question/list.html 会按亮度加上 .tb-cover-dark 换成深色遮罩。 */ - .tb-cover { - position: relative; - border-radius: var(--tb-radius) var(--tb-radius) 0 0; - overflow: hidden; - } - /* ⚠️ 必须带 .uk-flex 复合选择器:UIkit 3.3.3 里有 - .uk-flex::after,.uk-flex::before{display:none}(优先级 0,2,0), - 会把这个遮罩伪元素整个干掉——不报错、只是什么都不显示(踩过一次)。 - 单写 .tb-cover::after 优先级不够,会被它压回 display:none。 */ - .tb-cover::after, - .tb-cover.uk-flex::after { - content: ""; - display: block; - position: absolute; - inset: 0; - z-index: 0; - background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.58) 55%, rgba(255, 255, 255, 0.84) 100%); - pointer-events: none; - } - .tb-cover.tb-cover-dark::after { - background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.16) 45%, rgba(0, 0, 0, 0.46) 100%); - } - .tb-cover > * { - position: relative; - z-index: 1; - } - .tb-cover h3, - .tb-cover p { - text-shadow: 0 1px 10px rgba(255, 255, 255, 0.65); - } - .tb-cover.tb-cover-dark h3, - .tb-cover.tb-cover-dark p { - text-shadow: 0 1px 12px rgba(0, 0, 0, 0.45); - } - /* 尊重系统的减少动效设置 */ - @media (prefers-reduced-motion: reduce) { - .uk-card, - .uk-button, - .emoji-placeholder { - transition: none; - animation: none; - } - } - -/* 顶栏自适应:导航全部直接显示,窄屏时分级缩小字号/间距,保证英文/日文等长文字不溢出 */ - .uk-navbar-nav a { - font-size: 0.95rem; - padding: 0 10px; - } - /* 未登录移动端占位:桌面端隐藏,仅 ≤640px 显示(见下方 media 查询)。 - 桌面端布局与改动前完全一致,不受影响。 */ - .tb-mobile-balancer { display: none; } - /* 桌面端保持 UIkit 原生居中三列布局(无需额外 CSS) */ - .tb-navbar .uk-logo { - white-space: nowrap; - } - @media (max-width: 1200px) { - .uk-navbar-nav a { - font-size: 0.88rem; - padding: 0 8px; - } - } - @media (max-width: 1000px) { - .uk-navbar-nav a { - font-size: 0.82rem; - padding: 0 6px; - } - .uk-logo { - font-size: 1.1rem; - } - /* 登录用户名限宽,过长省略号截断 */ - .tb-nav-username { - max-width: 110px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - display: inline-block; - vertical-align: middle; - } - } - @media (max-width: 800px) { - .uk-navbar-nav a { - font-size: 0.78rem; - padding: 0 4px; - } - .uk-logo { - font-size: 1rem; - } - .tb-nav-username { - max-width: 80px; - } - } - /* 手机端(≤640px):沿用 UIkit 桌面端 navbar 的原生三列定位。 - 不再重写 .uk-navbar-center / center-left / center-right 的定位, - 避免 logo 被挤到下一行;底部语言与深色模式按钮仍由下方独立栏承载。 */ - @media (max-width: 640px) { - .tb-navbar { - position: relative; - min-height: 56px; - overflow: visible; - } - .tb-navbar > .uk-navbar-center { - position: relative !important; - top: auto !important; - left: auto !important; - right: auto !important; - bottom: auto !important; - transform: none !important; - display: flex !important; - align-items: center; - justify-content: center; - width: 100%; - min-height: 56px; - white-space: nowrap; - flex-wrap: nowrap; - } - .tb-navbar .uk-navbar-center-left, - .tb-navbar .uk-navbar-center-right { - position: static !important; - top: auto !important; - left: auto !important; - right: auto !important; - bottom: auto !important; - transform: none !important; - display: flex !important; - align-items: center; - flex: 0 0 auto; - white-space: nowrap; - } - .tb-navbar .uk-navbar-center-left > div, - .tb-navbar .uk-navbar-center-right > div { - display: flex; - align-items: center; - white-space: nowrap; - } - .tb-navbar .uk-navbar-nav { - display: flex; - flex-wrap: nowrap; - flex: 0 0 auto; - } - .tb-navbar .uk-navbar-nav > li > a, - .tb-navbar .uk-logo { - white-space: nowrap; - } - /* 未登录:移动端显示与「我的」等宽的隐藏占位,使左栏「探索+占位」与 - 右栏「注册/登录」两项等宽,logo 真正居中,左右视觉平衡 */ - .tb-mobile-balancer { display: block; } - } - -/* 标签样式的按钮(管理页置顶等): - 尺寸完全继承 .uk-label(font-size/padding/line-height/text-transform 等), - 仅覆盖 button 元素特有的默认样式(边框/字体族/光标等),保证与相邻 span 标签完全一致。 */ - .tb-label-btn { - border: none !important; - cursor: pointer; - font-family: inherit; - margin: 0; - vertical-align: middle; - outline: none; - background-image: none; - box-sizing: border-box; - -webkit-appearance: none; - appearance: none; - } - .tb-label-btn:hover { - opacity: 0.85; - } - /* 公开页置顶小图标:仅一个图钉,不占行高、不影响原布局 */ - .tb-pin-flag { - font-size: 0.9em; - margin-right: 4px; - vertical-align: -1px; - cursor: default; - } - -/* Markdown 渲染排版(问题/回答/追问正文,左对齐以保留排版格式) */ - /* 纯文本列表:保留换行与连续空格(pre-wrap),不渲染 Markdown 大字号/彩色 */ - .tb-preline { - white-space: pre-wrap; - } - /* 纯文本列表:超过 5 行截断并显示省略号(不足 5 行则完整显示) */ - .tb-clamp-5 { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 5; - overflow: hidden; - } - .md-body { - line-height: 1.7; - text-align: left; - word-break: break-word; - } - .md-body p { margin: 0.5em 0; } - .md-body h1, .md-body h2, .md-body h3, - .md-body h4, .md-body h5, .md-body h6 { - margin: 0.8em 0 0.4em; - font-weight: 600; - line-height: 1.4; - } - .md-body ul, .md-body ol { padding-left: 1.6em; margin: 0.5em 0; } - .md-body li { margin: 0.2em 0; } - .md-body blockquote { - border-left: 3px solid #d3d1c7; - margin: 0.6em 0; - padding: 0.2em 1em; - color: #5f5e5a; - } - .md-body pre { - background: #f5f5f5; - padding: 0.8em 1em; - border-radius: 6px; - overflow-x: auto; - margin: 0.6em 0; - } - .md-body code { - background: #f0f0f0; - padding: 0.1em 0.35em; - border-radius: 4px; - font-size: 0.9em; - } - .md-body pre code { background: none; padding: 0; } - .md-body img { max-width: 100%; height: auto; } - .md-body table { border-collapse: collapse; margin: 0.6em 0; } - .md-body th, .md-body td { - border: 1px solid #ddd; - padding: 0.4em 0.8em; - } - .md-body hr { margin: 1em 0; } - html[data-theme="dark"] .md-body blockquote { - border-color: #3a404c; - color: #8b93a1; - } - html[data-theme="dark"] .md-body pre, - html[data-theme="dark"] .md-body code { background: #2d323c; } - html[data-theme="dark"] .md-body th, - html[data-theme="dark"] .md-body td { border-color: #3a404c; } - -/* 投票结果占比条:模板已改用 UIkit 3 原生 , - 这里只把默认的 15px 高 / 500px 圆角 / 20px 外边距收紧到与页面一致的规格。 - (原先为 UIkit 2 的 .uk-progressbar 结构打的补丁已删除) */ -.uk-progress { - height: 10px; - margin: 0; - border-radius: var(--tb-radius-xs); - background-color: var(--tb-surface-2); -} -.uk-progress::-webkit-progress-bar { - background-color: var(--tb-surface-2); - border-radius: var(--tb-radius-xs); -} -.uk-progress::-webkit-progress-value { - /* 两端都圆:值 100% 时若只圆左边,右端方角会顶出轨道圆角 */ - background-color: var(--tb-primary); - border-radius: var(--tb-radius-xs); -} -.uk-progress::-moz-progress-bar { - background-color: var(--tb-primary); - border-radius: var(--tb-radius-xs); -} - -/* 移动端底部操作栏:固定底栏承载语言切换与深色模式,桌面端隐藏 */ - #tb-mobile-utils { - display: none; - } - @media (max-width: 640px) { - /* 顶栏不再容纳语言/深色按钮,恢复原有导航空间 */ - .tb-nav-utils { - display: none !important; - } - /* 手机端隐藏登录用户名(左边仅保留"探索"),保证 logo 居中、 - 两侧各不超过 2 个短按钮,避免用户名宽度挤压布局 */ - .tb-nav-username { - display: none !important; - } - /* 固定底部操作栏 */ - #tb-mobile-utils { - display: flex; - position: fixed; - left: 0; - right: 0; - bottom: 0; - z-index: 1000; - justify-content: center; - align-items: center; - gap: 10px; - padding: 8px 12px calc(8px + env(safe-area-inset-bottom)); - background: rgba(255, 255, 255, 0.92); - -webkit-backdrop-filter: blur(8px); - backdrop-filter: blur(8px); - box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08); - } - #tb-mobile-utils .uk-select { - width: 110px; - } - html[data-theme="dark"] #tb-mobile-utils { - background: rgba(36, 40, 48, 0.92); - box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.4); - } - /* 给页面底部预留空间,避免页脚内容被固定底栏遮挡 */ - body { - padding-bottom: 56px; - } - } diff --git a/outputs/verify/d-home.html b/outputs/verify/d-home.html deleted file mode 100644 index dd52593..0000000 --- a/outputs/verify/d-home.html +++ /dev/null @@ -1,937 +0,0 @@ - - - - - - 狼の質問箱 | 匿名質問箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - -
-

狼の質問箱 | 匿名質問箱

-

- 一个无需注册即可提问的匿名提问箱。不用下载 App,不用绑定社交账号,连邮箱都不必是真的。 -

- -
- -
-
-

匿名也能问

-

不需要账号、不需要真实邮箱,打开就能写。想收到回复再留邮箱即可。

-
-
-

不止一问一答

-

问题二维码、私聊、投票、追加提问、Markdown 排版,原版没有的这里都有。

-
-
-

箱子归你

-

头像、背景、二维码配色都能自己调,还能设置审核严格度和收集时段。

-
-
- -
-

域名迁移

-

- 本站已切换到 box.shiroko.one,原域名将在 2026 年 12 月 24 日到期, - 期间通过 301 转发指向新域名,到期后旧域名将彻底不再使用。 -

-

- 审核服务已切换到阿里云,审核速度提升且误伤更少;同时允许用户调整审核严格度,在不碰底线的前提下 - 可以设置不接受辱骂、广告等信息(即「不听坏话模式」)。 -

-
- -
-

一点提醒

-
    -
  • 本站唯一指定认证站长:小狼,注意不要被骗。
  • -
  • 本站提倡友好、友善的发言,请遵守社区规则。无论匿名与否,都要为自己的发言负责。
  • -
  • 不允许垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义、灌水提问,涉政或不良言论将直接删除。
  • -
-

- 想先试试功能,可以到站长的提问箱 - 小狼的提问箱 提问或提意见。 -

-
- -
- 近期更新 -
    -
  • 2026.9 新增审核严格度选项,增加提问、投票、私聊的总开关,可设置定时收集、定量收集或关闭功能,前后端显示也更易用了。
  • -
  • 2026.8 新增投票功能、追加提问、多语言、Markdown 显示、置顶显示等,详情见这里。
  • -
  • 2026.8 放弃使用 recaptcha 等对国内用户不友好的验证方式,更新密码加密方式,修复图片上传等一系列问题。
  • -
  • 2026.5 增加私聊功能,可连续对话,详见介绍;另有提问箱卡片分享。
  • -
  • 2026.2 新增用户展示和标签功能,可给自己贴 5 个标签并选择是否在探索页展示,详情见狼的公告板。
  • -
-
- -
-

質問箱の統計

-
-
- 28 - 注册用户 -
-
- 94 - 累计提问 -
-
- 21 - 已获回答 -
-
-

- 最后提问:2026-10-19 00:58:03
- 最后回答:2026-10-19 02:44:47
- 数据更新于:2026-10-19 12:48:26 -

-
- -
- -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - diff --git a/outputs/verify/d-prev-10.html b/outputs/verify/d-prev-10.html deleted file mode 100644 index c50437a..0000000 --- a/outputs/verify/d-prev-10.html +++ /dev/null @@ -1,854 +0,0 @@ - - - - - - 预览:测试 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - -
- admin6 -
发起的投票
-
- -
- - - 进行中 - -
-
- -

测试

- -

测试

- -
- - -
预览模式:这是发布前的页面样例,提交已禁用
- - - - - - - - -
该投票允许匿名参与,你的身份不会被记录(同一设备最多可参与 1 次)。
- - -
- - - -
- - -
-
- 1 - 多行文本 -
测是
-
- - - -
- - - - -
- -
- - - 预览模式,发布后即可提交 - -
-
- - - - - - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/d-prev-11.html b/outputs/verify/d-prev-11.html deleted file mode 100644 index f6f4a9d..0000000 --- a/outputs/verify/d-prev-11.html +++ /dev/null @@ -1,902 +0,0 @@ - - - - - - 预览:今天吃什么 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - -
- admin6 -
发起的投票
-
- -
- - - 进行中 - -
-
- -

今天吃什么

- -

是啊,吃什么?

- -
- - -
预览模式:这是发布前的页面样例,提交已禁用
- - - - - - - - -
该投票允许匿名参与,你的身份不会被记录(同一设备最多可参与 1 次)。
- - -
- - - -
- - -
-
- 1 - 单选 -
水果
-
- - - - - - - - - - -
- -
-
- 2 - 多选 -
主食 *
-
- - - - - - - - - - -
- -
-
- 3 - 数字 -
吃多少 *
-
- - - -
- - - - -
- -
- - - 预览模式,发布后即可提交 - -
-
- - - - - - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/d-res-12.html b/outputs/verify/d-res-12.html deleted file mode 100644 index f1d67c2..0000000 --- a/outputs/verify/d-res-12.html +++ /dev/null @@ -1,758 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:测试功能

- -
- -
-
3
-
总参与人数
-
- - - - -

投票结果

-
- -
-
- 1 - 单选 - 必填 - 回答数: 3 -
- - - - - -
-
- A - 3 票(100%) -
- -
- -
-
- B - 0 票(0%) -
- -
- -
-
- C - 0 票(0%) -
- -
- - - - -
- -
-
- 2 - 多选 - 非必填 - 回答数: 1 -
- - - - - -
-
- A - 1 票(100%) -
- -
- -
-
- B - 0 票(0%) -
- -
- -
-
- C - 0 票(0%) -
- -
- -
-
- D - 0 票(0%) -
- -
- - - - -
- -
-
- 3 - 单行文本 - 文本1 - 回答数: 0 -
- - -
已填写: 0
- - - -
- -
-
- 4 - 多行文本 - 文本2 - 回答数: 3 -
- - -
已填写: 3
- - -
-
- ▾ - 查看文本提交(3) -
- -
- - -
- -
- - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/d-res-13.html b/outputs/verify/d-res-13.html deleted file mode 100644 index fde00cd..0000000 --- a/outputs/verify/d-res-13.html +++ /dev/null @@ -1,626 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:吃什么

- -
- -
-
0
-
总参与人数
-
- - - - -

投票结果

-
- -
-
- 1 - 单行文本 - 是啊,吃什么? - 回答数: 0 -
- - -
已填写: 0
- - - -
- -
- - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/d-res-3.html b/outputs/verify/d-res-3.html deleted file mode 100644 index 47d1e65..0000000 --- a/outputs/verify/d-res-3.html +++ /dev/null @@ -1,693 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:明天吃什么

- -
- -
-
1
-
总参与人数
-
- - - -

投票结果

-
- -
平均分: 8.0
- -
-
- 1 分 - 0 票 -
- -
- -
-
- 2 分 - 0 票 -
- -
- -
-
- 3 分 - 0 票 -
- -
- -
-
- 4 分 - 0 票 -
- -
- -
-
- 5 分 - 0 票 -
- -
- -
-
- 6 分 - 0 票 -
- -
- -
-
- 7 分 - 0 票 -
- -
- -
-
- 8 分 - 1 票 -
- -
- -
-
- 9 分 - 0 票 -
- -
- -
-
- 10 分 - 0 票 -
- -
- - - - - -
- - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/d-res-5.html b/outputs/verify/d-res-5.html deleted file mode 100644 index 9ee0e82..0000000 --- a/outputs/verify/d-res-5.html +++ /dev/null @@ -1,929 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:11

- -
- -
-
2
-
总参与人数
-
- - - - -

投票结果

-
- -
-
- 1 - 单选 - 11 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(100%) -
- -
- -
-
- 22 - 0 票(0%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 2 - 多选 - 22 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(66%) -
- -
- -
-
- 22 - 1 票(33%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 3 - 单行文本 - 33 - 回答数: 2 -
- - -
已填写: 2
- - -
-
- ▾ - 查看文本提交(2) -
- -
- - -
- -
-
- 4 - 多行文本 - 44 - 回答数: 2 -
- - -
已填写: 2
- - -
-
- ▾ - 查看文本提交(2) -
- -
- - -
- -
-
- 5 - 下拉选择 - 55 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(100%) -
- -
- -
-
- 22 - 0 票(0%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 6 - 数字 - 66 - 回答数: 2 -
- - -
平均: 4.0(2 票)
- - -
- -
-
- 7 - 评分 - 77 - 回答数: 2 -
- - -
平均分: 9.0(2 票)
- -
- 1 分 - 0 票(0%) -
- - -
- 2 分 - 0 票(0%) -
- - -
- 3 分 - 0 票(0%) -
- - -
- 4 分 - 0 票(0%) -
- - -
- 5 分 - 0 票(0%) -
- - -
- 6 分 - 0 票(0%) -
- - -
- 7 分 - 0 票(0%) -
- - -
- 8 分 - 1 票(50%) -
- - -
- 9 分 - 0 票(0%) -
- - -
- 10 分 - 1 票(50%) -
- - - - -
- -
-
- 8 - 排序 - 88 - 回答数: 2 -
- - -
平均排名: 1.48(2 票)
- -
- 11 - 平均排名: 1.10 · 被排次数: 2 -
- -
- 22 - 平均排名: 1.25 · 被排次数: 2 -
- -
- 33 - 平均排名: 2.10 · 被排次数: 2 -
- - - -
- -
- - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/d-shot-res5.html b/outputs/verify/d-shot-res5.html deleted file mode 100644 index 57bd940..0000000 --- a/outputs/verify/d-shot-res5.html +++ /dev/null @@ -1,1603 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:11

- -
- -
-
2
-
总参与人数
-
- - - - -

投票结果

-
- -
-
- 1 - 单选 - 11 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(100%) -
- -
- -
-
- 22 - 0 票(0%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 2 - 多选 - 22 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(66%) -
- -
- -
-
- 22 - 1 票(33%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 3 - 单行文本 - 33 - 回答数: 2 -
- - -
已填写: 2
- - -
-
- ▾ - 查看文本提交(2) -
- -
- - -
- -
-
- 4 - 多行文本 - 44 - 回答数: 2 -
- - -
已填写: 2
- - -
-
- ▾ - 查看文本提交(2) -
- -
- - -
- -
-
- 5 - 下拉选择 - 55 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(100%) -
- -
- -
-
- 22 - 0 票(0%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 6 - 数字 - 66 - 回答数: 2 -
- - -
平均: 4.0(2 票)
- - -
- -
-
- 7 - 评分 - 77 - 回答数: 2 -
- - -
平均分: 9.0(2 票)
- -
- 1 分 - 0 票(0%) -
- - -
- 2 分 - 0 票(0%) -
- - -
- 3 分 - 0 票(0%) -
- - -
- 4 分 - 0 票(0%) -
- - -
- 5 分 - 0 票(0%) -
- - -
- 6 分 - 0 票(0%) -
- - -
- 7 分 - 0 票(0%) -
- - -
- 8 分 - 1 票(50%) -
- - -
- 9 分 - 0 票(0%) -
- - -
- 10 分 - 1 票(50%) -
- - - - -
- -
-
- 8 - 排序 - 88 - 回答数: 2 -
- - -
平均排名: 1.48(2 票)
- -
- 11 - 平均排名: 1.10 · 被排次数: 2 -
- -
- 22 - 平均排名: 1.25 · 被排次数: 2 -
- -
- 33 - 平均排名: 2.10 · 被排次数: 2 -
- - - -
- -
- - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/d-shot-vote.html b/outputs/verify/d-shot-vote.html deleted file mode 100644 index 5c6350a..0000000 --- a/outputs/verify/d-shot-vote.html +++ /dev/null @@ -1,1620 +0,0 @@ - - - - - - 测试功能 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - -
- admin6 -
の投票
-
- -
- - - 進行中 - -
-
- -

测试功能

- -
- - - - - - - - - -
匿名での参加が可能で、あなたの身元は記録されません(同一端末で最大1回)。
- - -
- - - -
- - -
-
- 1 - 単一選択 -
必填 *
-
- - - - - - - - - - -
- -
-
- 2 - 複数選択 -
非必填
-
- - - - - - - - - - - - -
- -
-
- 3 - 一行テキスト -
文本1
-
- - - -
- -
-
- 4 - 複数行テキスト -
文本2 *
-
- - - -
- - - - -
- -
- - - -
-
- - - - - - - - - -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/d-v-9wfpVjXi73mc.html b/outputs/verify/d-v-9wfpVjXi73mc.html deleted file mode 100644 index bbe548e..0000000 --- a/outputs/verify/d-v-9wfpVjXi73mc.html +++ /dev/null @@ -1,946 +0,0 @@ - - - - - - 测试功能 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - -
- admin6 -
の投票
-
- -
- - - 進行中 - -
-
- -

测试功能

- -
- - - - - - - - - -
匿名での参加が可能で、あなたの身元は記録されません(同一端末で最大1回)。
- - -
- - - -
- - -
-
- 1 - 単一選択 -
必填 *
-
- - - - - - - - - - -
- -
-
- 2 - 複数選択 -
非必填
-
- - - - - - - - - - - - -
- -
-
- 3 - 一行テキスト -
文本1
-
- - - -
- -
-
- 4 - 複数行テキスト -
文本2 *
-
- - - -
- - - - -
- -
- - - -
-
- - - - - - - - - -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/d-v-urSsfenvRomb.html b/outputs/verify/d-v-urSsfenvRomb.html deleted file mode 100644 index 376dcbe..0000000 --- a/outputs/verify/d-v-urSsfenvRomb.html +++ /dev/null @@ -1,1039 +0,0 @@ - - - - - - 11 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - -
- admin6 -
の投票
-
- -
- - - 進行中 - -
-
- -

11

- -
- - - - - - - - - -
匿名での参加が可能で、あなたの身元は記録されません(同一端末で最大1回)。
- - -
- - - -
- - -
-
- 1 - 単一選択 -
11
-
- - - - - - - - - - -
- -
-
- 2 - 複数選択 -
22 *
-
- - - - - - - - - - -
- -
-
- 3 - 一行テキスト -
33 *
-
- - - -
- -
-
- 4 - 複数行テキスト -
44 *
-
- - - -
- -
-
- 5 - ドロップダウン選択 -
55 *
-
- - - - -
- -
-
- 6 - 数値 -
66 *
-
- - - -
- -
-
- 7 - 評価 -
77 *
-
- - - -
- -
-
- 8 - 順位 -
88 *
-
- -
- 順位 1-N(必須) -
- - -
- 11 - -
- -
- 22 - -
- -
- 33 - -
- - -
- - - - -
- -
- - - -
-
- - - - - - - - - -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/d-v-z9F8mIqPtDOA.html b/outputs/verify/d-v-z9F8mIqPtDOA.html deleted file mode 100644 index e2135ec..0000000 --- a/outputs/verify/d-v-z9F8mIqPtDOA.html +++ /dev/null @@ -1,924 +0,0 @@ - - - - - - 测试4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - -
- admin6 -
の投票
-
- -
- - - 進行中 - -
-
- -

测试4

- -
- - - - - - - - - -
匿名での参加が可能で、あなたの身元は記録されません(同一端末で最大1回)。
- - -
- - - -
- - -
-
- 1 - 単一選択 -
这里显示第一题
-
- - - - - - - - - - - - - - -
- -
-
- 2 - 単一選択 -
11
-
- - - - - - - - -
- - - - -
- -
- - - -
-
- - - - - - - - - -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/explore-probe.html b/outputs/verify/explore-probe.html deleted file mode 100644 index 2198da0..0000000 --- a/outputs/verify/explore-probe.html +++ /dev/null @@ -1,1663 +0,0 @@ - - - - - - 预览 11:39 · 投票 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - - - -
-

探索用户标签

- -
-

此处展示已开启“在首页展示我的标签”的用户。该功能默认关闭,若希望自己的提问箱在这里被发现,请前往 个人设置 手动开启。

-
- -
- - -
- -
-
-
-

随机推荐

- -
-
- -
暂无推荐用户
- -
-
- - - -
-

热门标签

-
- - #杂谈 - - #学习 - - #音乐 - -
-
- - -
-
-
-
- - -
-
-
- -
-
-
- - -
- - -
-
-
-
- avatar -
-
-
- 小狼 -
-
- 随便问,友善一点,看到都会回。 -
-
- - 无标签 - -
-
-
-
-
- - -
-
-
-
- avatar -
-
-
- Kitsune -
-
- 摄影 / 音乐 / 一点点代码 -
-
- - 无标签 - -
-
-
-
-
- - -
-
-
-
- avatar -
-
-
- 残响 -
-
- 长期潜水,偶尔冒泡。 -
-
- - 无标签 - -
-
-
-
-
- - -
-
-
-
- avatar -
-
-
- Mika -
-
- 想认识更多有趣的人。 -
-
- - 无标签 - -
-
-
-
-
- -
- -
    - -
  • 上一页
  • - -
  • 1 / 1
  • - -
  • 下一页
  • - -
- -
- - -
- - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - -
- - - - diff --git a/outputs/verify/explore.html b/outputs/verify/explore.html deleted file mode 100644 index 4275a4b..0000000 --- a/outputs/verify/explore.html +++ /dev/null @@ -1,2169 +0,0 @@ - - - - - - 探索 - 狼の質問箱 | 匿名質問箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - -
-

ユーザータグを探索

- -
-

ここでは「ホームでタグを表示」を有効にしたユーザーを紹介しています。この機能は初期状態でオフです。自分の質問箱を見つけてもらいたい場合は 個人設定 から有効にしてください。

-
- -
- - -
- -
-
-
-

ランダムおすすめ

- -
- -
- - - -
-

人気タグ

-
- - #测试 - - #胡言乱语 - -
-
- - -
-
-
-
- - -
-
-
- -
-
-
- - -
- - -
-
-
-
- avatar -
-
-
- admin3x -
-
- 这里的内容可能会很长,希望能够自动分段,不然就很难看了,万一要是更长的话会怎么样呢???? -
-
- - - 测试 - - 胡言乱语 - - -
-
-
-
-
- -
- -
    - -
  • 前へ
  • - -
  • 1 / 1
  • - -
  • 次へ
  • - -
- -
- - -
- - -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/fix-res5-light.html b/outputs/verify/fix-res5-light.html deleted file mode 100644 index 57bd940..0000000 --- a/outputs/verify/fix-res5-light.html +++ /dev/null @@ -1,1603 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:11

- -
- -
-
2
-
总参与人数
-
- - - - -

投票结果

-
- -
-
- 1 - 单选 - 11 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(100%) -
- -
- -
-
- 22 - 0 票(0%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 2 - 多选 - 22 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(66%) -
- -
- -
-
- 22 - 1 票(33%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 3 - 单行文本 - 33 - 回答数: 2 -
- - -
已填写: 2
- - -
-
- ▾ - 查看文本提交(2) -
- -
- - -
- -
-
- 4 - 多行文本 - 44 - 回答数: 2 -
- - -
已填写: 2
- - -
-
- ▾ - 查看文本提交(2) -
- -
- - -
- -
-
- 5 - 下拉选择 - 55 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(100%) -
- -
- -
-
- 22 - 0 票(0%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 6 - 数字 - 66 - 回答数: 2 -
- - -
平均: 4.0(2 票)
- - -
- -
-
- 7 - 评分 - 77 - 回答数: 2 -
- - -
平均分: 9.0(2 票)
- -
- 1 分 - 0 票(0%) -
- - -
- 2 分 - 0 票(0%) -
- - -
- 3 分 - 0 票(0%) -
- - -
- 4 分 - 0 票(0%) -
- - -
- 5 分 - 0 票(0%) -
- - -
- 6 分 - 0 票(0%) -
- - -
- 7 分 - 0 票(0%) -
- - -
- 8 分 - 1 票(50%) -
- - -
- 9 分 - 0 票(0%) -
- - -
- 10 分 - 1 票(50%) -
- - - - -
- -
-
- 8 - 排序 - 88 - 回答数: 2 -
- - -
平均排名: 1.48(2 票)
- -
- 11 - 平均排名: 1.10 · 被排次数: 2 -
- -
- 22 - 平均排名: 1.25 · 被排次数: 2 -
- -
- 33 - 平均排名: 2.10 · 被排次数: 2 -
- - - -
- -
- - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/fix-vote-page-light.html b/outputs/verify/fix-vote-page-light.html deleted file mode 100644 index 18a77e8..0000000 --- a/outputs/verify/fix-vote-page-light.html +++ /dev/null @@ -1,1620 +0,0 @@ - - - - - - 测试功能 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - -
- admin6 -
の投票
-
- -
- - - 進行中 - -
-
- -

测试功能

- -
- - - - - - - - - -
匿名での参加が可能で、あなたの身元は記録されません(同一端末で最大1回)。
- - -
- - - -
- - -
-
- 1 - 単一選択 -
必填 *
-
- - - - - - - - - - -
- -
-
- 2 - 複数選択 -
非必填
-
- - - - - - - - - - - - -
- -
-
- 3 - 一行テキスト -
文本1
-
- - - -
- -
-
- 4 - 複数行テキスト -
文本2 *
-
- - - -
- - - - -
- -
- - - -
-
- - - - - - - - - -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/h-prev-10.txt b/outputs/verify/h-prev-10.txt deleted file mode 100644 index aabbdcb..0000000 --- a/outputs/verify/h-prev-10.txt +++ /dev/null @@ -1,22 +0,0 @@ -HTTP/1.1 200 Connection Established - -HTTP/1.1 200 OK -Date: Mon, 19 Oct 2026 04:50:43 GMT -Content-Type: text/html; charset=utf-8 -Transfer-Encoding: chunked -Connection: keep-alive -Speculation-Rules: "/cdn-cgi/speculation" -referrer-policy: strict-origin-when-cross-origin -strict-transport-security: max-age=31536000; includeSubDomains -trace-id: 00000000000000000000000000000000 -vary: Accept-Encoding -x-content-type-options: nosniff -x-frame-options: DENY -x-xss-protection: 1; mode=block -cf-cache-status: DYNAMIC -Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800} -Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=LNNyS3fb%2BRQ4lDSreatYe3z7vYXJ5DBvFEY39WJc%2B8HZ1NjXYKxVWdDWaiWMQ%2B%2FvlEA0T2DYuliVVbPWsERm5o1d9QfSG0wMX8hF5b0XBHoxociB8pPyH1LZDRlllEINsKtHcLfseGtFGQr58g%3D%3D"}]} -Server: cloudflare -CF-RAY: a4cd29d93d591ca5-AMS -alt-svc: h3=":443"; ma=86400 - diff --git a/outputs/verify/h-prev-11.txt b/outputs/verify/h-prev-11.txt deleted file mode 100644 index ba16ee7..0000000 --- a/outputs/verify/h-prev-11.txt +++ /dev/null @@ -1,22 +0,0 @@ -HTTP/1.1 200 Connection Established - -HTTP/1.1 200 OK -Date: Mon, 19 Oct 2026 04:50:47 GMT -Content-Type: text/html; charset=utf-8 -Transfer-Encoding: chunked -Connection: keep-alive -Speculation-Rules: "/cdn-cgi/speculation" -referrer-policy: strict-origin-when-cross-origin -strict-transport-security: max-age=31536000; includeSubDomains -trace-id: 00000000000000000000000000000000 -vary: Accept-Encoding -x-content-type-options: nosniff -x-frame-options: DENY -x-xss-protection: 1; mode=block -cf-cache-status: DYNAMIC -Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800} -Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=s9ceWO0ESedGa5u0nddaXW07CHQrtIc9OIdjAPsRo7ngWVNAPIIdFbgiTZR5%2Bl3u2H9LTwdWSKOYpb1U90XF09z4x88W0xbDzjFZio9EwdShj1rTkK3E6WboqRk75JfBct3otKwp8vRlSed8vg%3D%3D"}]} -Server: cloudflare -CF-RAY: a4cd29f74dae1af9-AMS -alt-svc: h3=":443"; ma=86400 - diff --git a/outputs/verify/h2.txt b/outputs/verify/h2.txt deleted file mode 100644 index e66be16..0000000 --- a/outputs/verify/h2.txt +++ /dev/null @@ -1,21 +0,0 @@ -HTTP/1.1 200 Connection Established - -HTTP/1.1 302 Found -Date: Mon, 19 Oct 2026 04:24:11 GMT -Content-Length: 0 -Connection: keep-alive -location: / -referrer-policy: strict-origin-when-cross-origin -Set-Cookie: flamego_session=hprz8iav3vfy6tzb; Path=/; HttpOnly; Secure; SameSite=Lax -strict-transport-security: max-age=31536000; includeSubDomains -trace-id: 00000000000000000000000000000000 -x-content-type-options: nosniff -x-frame-options: DENY -x-xss-protection: 1; mode=block -cf-cache-status: DYNAMIC -Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800} -Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=xCuQqn2mEttANrv5sLh%2BGTlX5Y72aBFAAsUVPIWcvKvdv3w32AaiR%2FpBwbfkeIwSznnctdkAKyXjUtksAPe64DdTILC%2F1ZkMC8tx5l8BH3AatTgML4IDtsg%2BKz0u8npwzjh0oL6oa0mKWwIEFg%3D%3D"}]} -Server: cloudflare -CF-RAY: a4cd02ff3c26a11d-AMS -alt-svc: h3=":443"; ma=86400 - diff --git a/outputs/verify/h3.txt b/outputs/verify/h3.txt deleted file mode 100644 index 7a93462..0000000 --- a/outputs/verify/h3.txt +++ /dev/null @@ -1,21 +0,0 @@ -HTTP/1.1 200 Connection Established - -HTTP/1.1 302 Found -Date: Mon, 19 Oct 2026 04:48:53 GMT -Content-Length: 0 -Connection: keep-alive -location: / -referrer-policy: strict-origin-when-cross-origin -Set-Cookie: flamego_session=xzuo76g1wkt93yde; Path=/; HttpOnly; Secure; SameSite=Lax -strict-transport-security: max-age=31536000; includeSubDomains -trace-id: 00000000000000000000000000000000 -x-content-type-options: nosniff -x-frame-options: DENY -x-xss-protection: 1; mode=block -cf-cache-status: DYNAMIC -Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800} -Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=4lN%2FjpiiXhqAFEa04LgpyUx1T6gq6As3ND39p0U%2B%2B%2BnV18I8OPf%2B3C4P1hF2ZbvemgZD3yKBN0KGjMXH3MKidzjjBRg0UN0glf5LWn9QPaATLorzaAcF3cY3eMFsZ%2BFA3GJVVI%2FAv4v2dquZoA%3D%3D"}]} -Server: cloudflare -CF-RAY: a4cd27291b28a007-AMS -alt-svc: h3=":443"; ma=86400 - diff --git a/outputs/verify/headers.txt b/outputs/verify/headers.txt deleted file mode 100644 index 6da1e05..0000000 --- a/outputs/verify/headers.txt +++ /dev/null @@ -1,21 +0,0 @@ -HTTP/1.1 200 Connection Established - -HTTP/1.1 302 Found -Date: Mon, 19 Oct 2026 03:48:09 GMT -Content-Length: 0 -Connection: keep-alive -location: / -referrer-policy: strict-origin-when-cross-origin -Set-Cookie: flamego_session=19alv4sneiwlykv6; Path=/; HttpOnly; Secure; SameSite=Lax -strict-transport-security: max-age=31536000; includeSubDomains -trace-id: 00000000000000000000000000000000 -x-content-type-options: nosniff -x-frame-options: DENY -x-xss-protection: 1; mode=block -cf-cache-status: DYNAMIC -Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800} -Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=mt51BDC553zfd84HQmzmbFBIUCtm8uyVduSXv3wGYmT60XhJFCI6frcvrMawEhFX6f3tbKwf9rdM%2BEbHnmt%2Bn2BL6jm3gzXD5L7QqxUFzwOovEMimGkltJAjd689eDDVeacR0cmLD%2FZLWisIGQ%3D%3D"}]} -Server: cloudflare -CF-RAY: a4ccce358f121c0a-AMS -alt-svc: h3=":443"; ma=86400 - diff --git a/outputs/verify/home.html b/outputs/verify/home.html deleted file mode 100644 index 327dc6e..0000000 --- a/outputs/verify/home.html +++ /dev/null @@ -1,1577 +0,0 @@ - - - - - - 狼の質問箱 | 匿名質問箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - -
-

狼の質問箱 | 匿名質問箱

-

- 一个无需注册即可提问的匿名提问箱。不用下载 App,不用绑定社交账号,连邮箱都不必是真的。 -

- -
- -
-
-

匿名也能问

-

不需要账号、不需要真实邮箱,打开就能写。想收到回复再留邮箱即可。

-
-
-

不止一问一答

-

问题二维码、私聊、投票、追加提问、Markdown 排版,原版没有的这里都有。

-
-
-

箱子归你

-

头像、背景、二维码配色都能自己调,还能设置审核严格度和收集时段。

-
-
- -
-

域名迁移

-

- 本站已切换到 box.shiroko.one,原域名将在 2026 年 12 月 24 日到期, - 期间通过 301 转发指向新域名,到期后旧域名将彻底不再使用。 -

-

- 审核服务已切换到阿里云,审核速度提升且误伤更少;同时允许用户调整审核严格度,在不碰底线的前提下 - 可以设置不接受辱骂、广告等信息(即「不听坏话模式」)。 -

-
- -
-

一点提醒

-
    -
  • 本站唯一指定认证站长:小狼,注意不要被骗。
  • -
  • 本站提倡友好、友善的发言,请遵守社区规则。无论匿名与否,都要为自己的发言负责。
  • -
  • 不允许垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义、灌水提问,涉政或不良言论将直接删除。
  • -
-

- 想先试试功能,可以到站长的提问箱 - 小狼的提问箱 提问或提意见。 -

-
- -
- 近期更新 -
    -
  • 2026.9 新增审核严格度选项,增加提问、投票、私聊的总开关,可设置定时收集、定量收集或关闭功能,前后端显示也更易用了。
  • -
  • 2026.8 新增投票功能、追加提问、多语言、Markdown 显示、置顶显示等,详情见这里。
  • -
  • 2026.8 放弃使用 recaptcha 等对国内用户不友好的验证方式,更新密码加密方式,修复图片上传等一系列问题。
  • -
  • 2026.5 增加私聊功能,可连续对话,详见介绍;另有提问箱卡片分享。
  • -
  • 2026.2 新增用户展示和标签功能,可给自己贴 5 个标签并选择是否在探索页展示,详情见狼的公告板。
  • -
-
- -
-

質問箱の統計

-
-
- 28 - 注册用户 -
-
- 94 - 累计提问 -
-
- 21 - 已获回答 -
-
-

- 最后提问:2026-10-19 00:58:03
- 最后回答:2026-10-19 02:44:47
- 数据更新于:2026-10-19 11:23:15 -

-
- -
- -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - diff --git a/outputs/verify/home2.html b/outputs/verify/home2.html deleted file mode 100644 index 761ee8f..0000000 --- a/outputs/verify/home2.html +++ /dev/null @@ -1,938 +0,0 @@ - - - - - - 狼の質問箱 | 匿名質問箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - -
-

狼の質問箱 | 匿名質問箱

-

- 一个无需注册即可提问的匿名提问箱。不用下载 App,不用绑定社交账号,连邮箱都不必是真的。 -

- -
- -
-
-

匿名也能问

-

不需要账号、不需要真实邮箱,打开就能写。想收到回复再留邮箱即可。

-
-
-

不止一问一答

-

问题二维码、私聊、投票、追加提问、Markdown 排版,原版没有的这里都有。

-
-
-

箱子归你

-

头像、背景、二维码配色都能自己调,还能设置审核严格度和收集时段。

-
-
- -
-

域名迁移

-

- 本站已切换到 box.shiroko.one,原域名将在 2026 年 12 月 24 日到期, - 期间通过 301 转发指向新域名,到期后旧域名将彻底不再使用。 -

-

- 审核服务已切换到阿里云,审核速度提升且误伤更少;同时允许用户调整审核严格度,在不碰底线的前提下 - 可以设置不接受辱骂、广告等信息(即「不听坏话模式」)。 -

-
- -
-

一点提醒

-
    -
  • 本站唯一指定认证站长:小狼,注意不要被骗。
  • -
  • 本站提倡友好、友善的发言,请遵守社区规则。无论匿名与否,都要为自己的发言负责。
  • -
  • 不允许垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义、灌水提问,涉政或不良言论将直接删除。
  • -
-

- 想先试试功能,可以到站长的提问箱 - 小狼的提问箱 提问或提意见。 -

-
- -
- 近期更新 -
    -
  • 2026.9 新增审核严格度选项,增加提问、投票、私聊的总开关,可设置定时收集、定量收集或关闭功能,前后端显示也更易用了。
  • -
  • 2026.8 新增投票功能、追加提问、多语言、Markdown 显示、置顶显示等,详情见这里。
  • -
  • 2026.8 放弃使用 recaptcha 等对国内用户不友好的验证方式,更新密码加密方式,修复图片上传等一系列问题。
  • -
  • 2026.5 增加私聊功能,可连续对话,详见介绍;另有提问箱卡片分享。
  • -
  • 2026.2 新增用户展示和标签功能,可给自己贴 5 个标签并选择是否在探索页展示,详情见狼的公告板。
  • -
-
- -
-

質問箱の統計

-
-
- 28 - 注册用户 -
-
- 94 - 累计提问 -
-
- 21 - 已获回答 -
-
-

- 最后提问:2026-10-19 00:58:03
- 最后回答:2026-10-19 02:44:47
- 数据更新于:2026-10-19 11:47:10 -

-
- -
- -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - diff --git a/outputs/verify/icon-probe.html b/outputs/verify/icon-probe.html deleted file mode 100644 index 9da6172..0000000 --- a/outputs/verify/icon-probe.html +++ /dev/null @@ -1,1613 +0,0 @@ - - - - - - 预览 12:01 · TamaBox - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - -
-

狼的提问箱 | 匿名提问箱

-

- 一个无需注册即可提问的匿名提问箱。不用下载 App,不用绑定社交账号,连邮箱都不必是真的。 -

- -
- -
-
-

匿名也能问

-

不需要账号、不需要真实邮箱,打开就能写。想收到回复再留邮箱即可。

-
-
-

不止一问一答

-

问题二维码、私聊、投票、追加提问、Markdown 排版,原版没有的这里都有。

-
-
-

箱子归你

-

头像、背景、二维码配色都能自己调,还能设置审核严格度和收集时段。

-
-
- -
-

域名迁移

-

- 本站已切换到 box.shiroko.one,原域名将在 2026 年 12 月 24 日到期, - 期间通过 301 转发指向新域名,到期后旧域名将彻底不再使用。 -

-

- 审核服务已切换到阿里云,审核速度提升且误伤更少;同时允许用户调整审核严格度,在不碰底线的前提下 - 可以设置不接受辱骂、广告等信息(即「不听坏话模式」)。 -

-
- -
-

一点提醒

-
    -
  • 本站唯一指定认证站长:小狼,注意不要被骗。
  • -
  • 本站提倡友好、友善的发言,请遵守社区规则。无论匿名与否,都要为自己的发言负责。
  • -
  • 不允许垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义、灌水提问,涉政或不良言论将直接删除。
  • -
-

- 想先试试功能,可以到站长的提问箱 - 小狼的提问箱 提问或提意见。 -

-
- -
- 近期更新 -
    -
  • 2026.9 新增审核严格度选项,增加提问、投票、私聊的总开关,可设置定时收集、定量收集或关闭功能,前后端显示也更易用了。
  • -
  • 2026.8 新增投票功能、追加提问、多语言、Markdown 显示、置顶显示等,详情见这里。
  • -
  • 2026.8 放弃使用 recaptcha 等对国内用户不友好的验证方式,更新密码加密方式,修复图片上传等一系列问题。
  • -
  • 2026.5 增加私聊功能,可连续对话,详见介绍;另有提问箱卡片分享。
  • -
  • 2026.2 新增用户展示和标签功能,可给自己贴 5 个标签并选择是否在探索页展示,详情见狼的公告板。
  • -
-
- -
-

提问箱信息统计

-
-
- 1284 - 注册用户 -
-
- 56321 - 累计提问 -
-
- 31208 - 已获回答 -
-
-

- 最后提问:2026-10-19 10:12:44
- 最后回答:2026-10-19 09:58:02
- 数据更新于:2026-10-19 10:00:00 -

-
- -
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | -
提问箱已成功存活: -

- - -
-
- - - -
- - - diff --git a/outputs/verify/l3.html b/outputs/verify/l3.html deleted file mode 100644 index 912ffa8..0000000 --- a/outputs/verify/l3.html +++ /dev/null @@ -1,638 +0,0 @@ - - - - - - 狼の質問箱 | 匿名質問箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - -
-
- - ログイン - - - - - - - - - -
- - -
-
- - -
-
- - - - - パスワードを忘れた - -
-
-
-

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/live-mobile.html b/outputs/verify/live-mobile.html deleted file mode 100644 index 938573b..0000000 --- a/outputs/verify/live-mobile.html +++ /dev/null @@ -1,936 +0,0 @@ - - - - - 狼の質問箱 | 匿名質問箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - -
-

狼の質問箱 | 匿名質問箱

-

- 一个无需注册即可提问的匿名提问箱。不用下载 App,不用绑定社交账号,连邮箱都不必是真的。 -

- -
- -
-
-

匿名也能问

-

不需要账号、不需要真实邮箱,打开就能写。想收到回复再留邮箱即可。

-
-
-

不止一问一答

-

问题二维码、私聊、投票、追加提问、Markdown 排版,原版没有的这里都有。

-
-
-

箱子归你

-

头像、背景、二维码配色都能自己调,还能设置审核严格度和收集时段。

-
-
- -
-

域名迁移

-

- 本站已切换到 box.shiroko.one,原域名将在 2026 年 12 月 24 日到期, - 期间通过 301 转发指向新域名,到期后旧域名将彻底不再使用。 -

-

- 审核服务已切换到阿里云,审核速度提升且误伤更少;同时允许用户调整审核严格度,在不碰底线的前提下 - 可以设置不接受辱骂、广告等信息(即「不听坏话模式」)。 -

-
- -
-

一点提醒

-
    -
  • 本站唯一指定认证站长:小狼,注意不要被骗。
  • -
  • 本站提倡友好、友善的发言,请遵守社区规则。无论匿名与否,都要为自己的发言负责。
  • -
  • 不允许垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义、灌水提问,涉政或不良言论将直接删除。
  • -
-

- 想先试试功能,可以到站长的提问箱 - 小狼的提问箱 提问或提意见。 -

-
- -
- 近期更新 -
    -
  • 2026.9 新增审核严格度选项,增加提问、投票、私聊的总开关,可设置定时收集、定量收集或关闭功能,前后端显示也更易用了。
  • -
  • 2026.8 新增投票功能、追加提问、多语言、Markdown 显示、置顶显示等,详情见这里。
  • -
  • 2026.8 放弃使用 recaptcha 等对国内用户不友好的验证方式,更新密码加密方式,修复图片上传等一系列问题。
  • -
  • 2026.5 增加私聊功能,可连续对话,详见介绍;另有提问箱卡片分享。
  • -
  • 2026.2 新增用户展示和标签功能,可给自己贴 5 个标签并选择是否在探索页展示,详情见狼的公告板。
  • -
-
- -
-

質問箱の統計

-
-
- 28 - 注册用户 -
-
- 94 - 累计提问 -
-
- 21 - 已获回答 -
-
-

- 最后提问:2026-10-19 00:58:03
- 最后回答:2026-10-19 02:44:47
- 数据更新于:2026-10-19 12:48:26 -

-
- -
  • 哇哦
    0
    哇哦
  • prpr
    0
    prpr
  • 流汗
    0
    流汗
  • 生气
    0
    生气
  • 流鼻血
    0
    流鼻血
  • あべ
    0
    あべ
评论
  • 按正序
  • 按倒序
  • 按热度
来发评论吧~
- -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中:3年 110天 18时 44分 45秒 -

- - -
-
- - - - - diff --git a/outputs/verify/login.html b/outputs/verify/login.html deleted file mode 100644 index 3c7d3f4..0000000 --- a/outputs/verify/login.html +++ /dev/null @@ -1,639 +0,0 @@ - - - - - - 狼の質問箱 | 匿名質問箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - -
-
- - ログイン - - - - - - - - - -
- - -
-
- - -
-
- - - - - パスワードを忘れた - -
-
-
-

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/login2.html b/outputs/verify/login2.html deleted file mode 100644 index 46c454d..0000000 --- a/outputs/verify/login2.html +++ /dev/null @@ -1,638 +0,0 @@ - - - - - - 狼の質問箱 | 匿名質問箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - -
-
- - ログイン - - - - - - - - - -
- - -
-
- - -
-
- - - - - パスワードを忘れた - -
-
-
-

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/login2_post.html b/outputs/verify/login2_post.html deleted file mode 100644 index e69de29..0000000 diff --git a/outputs/verify/login_post.html b/outputs/verify/login_post.html deleted file mode 100644 index e69de29..0000000 diff --git a/outputs/verify/p2-___admin6.html b/outputs/verify/p2-___admin6.html deleted file mode 100644 index fdebc07..0000000 --- a/outputs/verify/p2-___admin6.html +++ /dev/null @@ -1,1062 +0,0 @@ - - - - - - admin6 - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - -
-
-
- -

admin6

-

问你想问的

-
- - - - - - - bloglink - - -
-
-
-
- -
-
- - - - -
- - -

扫码向@admin6提问
谁都可以以匿名的形式提问

- - - - - - - - - - - - - - - - - - -
- - - - -
- -
- -
- - - - - - - -
- -
-
- - - -
- -
-
- -
- - - -
- -
-
-
- - - -
- -
- -
- -
-
-
- - - - - - -
- - -
-
- -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - diff --git a/outputs/verify/p2-_explore.html b/outputs/verify/p2-_explore.html deleted file mode 100644 index 54a5745..0000000 --- a/outputs/verify/p2-_explore.html +++ /dev/null @@ -1,1527 +0,0 @@ - - - - - - 探索 - 狼の質問箱 | 匿名質問箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - -
-

ユーザータグを探索

- -
-

ここでは「ホームでタグを表示」を有効にしたユーザーを紹介しています。この機能は初期状態でオフです。自分の質問箱を見つけてもらいたい場合は 個人設定 から有効にしてください。

-
- -
- - -
- -
-
-
-

ランダムおすすめ

- -
- -
- - - -
-

人気タグ

-
- - #测试 - - #胡言乱语 - -
-
- - -
-
-
-
- - -
-
-
- -
-
-
- - -
- - -
-
-
-
- avatar -
-
-
- admin3x -
-
- 这里的内容可能会很长,希望能够自动分段,不然就很难看了,万一要是更长的话会怎么样呢???? -
-
- - - 测试 - - 胡言乱语 - - -
-
-
-
-
- -
- -
    - -
  • 前へ
  • - -
  • 1 / 1
  • - -
  • 次へ
  • - -
- -
- - -
- - -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/p2-base.css b/outputs/verify/p2-base.css deleted file mode 100644 index 45a1b3a..0000000 --- a/outputs/verify/p2-base.css +++ /dev/null @@ -1,668 +0,0 @@ -/* TamaBox 全站基础样式:设计令牌 / 主题覆盖 / 组件质感 / 响应式。 - 由 base/header.html 内联块抽出,集中一份便于缓存与维护。 - ⚠️ 改完务必把 header 里的 ?v= 版本号 +1,否则老访客仍用缓存。 */ - -/* 设计令牌:亮色为默认(:root),深色在 html[data-theme="dark"] 覆盖。 - 页面与组件样式一律引用 var(--tb-*),不再写死色值/圆角/阴影; - var() 的第二个参数仅作兜底(旧缓存页、邮件模板等不引入本文件的场景)。 - 改配色只动这一块,亮暗两套自动保持一致。 */ - :root { - --tb-bg: #f7f8fa; - --tb-surface: #ffffff; - --tb-surface-2: #f4f6f8; - --tb-surface-hover: #f3f7ff; - --tb-border: #e8eaee; - --tb-text: #1f2329; - --tb-text-secondary: #5c6370; - --tb-text-muted: #8a919c; - --tb-primary: #1e87f0; - --tb-primary-weak: rgba(30, 135, 240, 0.10); - --tb-radius: 12px; - --tb-radius-sm: 8px; - --tb-radius-xs: 6px; - --tb-shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 12px 28px -18px rgba(16, 24, 40, 0.20); - --tb-shadow-hover: 0 1px 2px rgba(16, 24, 40, 0.05), 0 18px 40px -20px rgba(16, 24, 40, 0.28); - --tb-font: system-ui, -apple-system, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", "Microsoft YaHei", sans-serif; - } - html[data-theme="dark"] { - --tb-bg: #1a1d23; - --tb-surface: #242830; - --tb-surface-2: #2d323c; - --tb-surface-hover: rgba(92, 141, 255, 0.12); - --tb-border: #3a404c; - --tb-text: #d8dce4; - --tb-text-secondary: #a8b0bd; - --tb-text-muted: #8b93a1; - --tb-primary: #5c8dff; - --tb-primary-weak: rgba(92, 141, 255, 0.14); - --tb-shadow: 0 1px 2px rgba(0, 0, 0, 0.30), 0 12px 28px -18px rgba(0, 0, 0, 0.55); - --tb-shadow-hover: 0 1px 2px rgba(0, 0, 0, 0.35), 0 18px 40px -20px rgba(0, 0, 0, 0.65); - } - -/* UIkit 补丁:复刻原自建版 "modify-black-font"(官方版与自建版仅 3 处颜色差异——把有色容器内默认白字/白标题改为黑字)。 - 模板当前只用到 .uk-light 容器(用户背景封面,question/list.html、question-not-found.html; - 其中 h3/p 已由 #usernamecolor/#introcolor 内联 style 覆盖),故只需补该容器的 3 处投影即可保持渲染一致。 - 若日后新增 .uk-card-primary/.uk-card-secondary/.uk-section-primary/.uk-tile-*/.uk-overlay-primary 等容器且需黑字,须同步扩展。 */ - .uk-light { color: rgba(0,0,0,.7); } - .uk-light .uk-h1, .uk-light .uk-h2, .uk-light .uk-h3, .uk-light .uk-h4, .uk-light .uk-h5, .uk-light .uk-h6, - .uk-light .uk-heading-2xlarge, .uk-light .uk-heading-large, .uk-light .uk-heading-medium, .uk-light .uk-heading-small, .uk-light .uk-heading-xlarge, - .uk-light h1, .uk-light h2, .uk-light h3, .uk-light h4, .uk-light h5, .uk-light h6 { color: #000; } - .uk-light .uk-table th { color: rgba(0,0,0,.7); } - - .grecaptcha-badge {visibility: hidden;} - .icon-container { - display: flex; - justify-content: center; - width: 100%; - } - .icon { - margin-left: 10px; - width: 20px; /* 设置图标的宽度 */ - height: auto; /* 保持图标的高度自动调整 */ - } - .emoji-placeholder { - display: inline-block; - animation: jump 0.5s ease-in-out; - } - - @keyframes jump { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-5px); } - } - /* 防止 UIkit Dropbar 初始闪烁 */ - .uk-dropbar { - opacity: 0; - visibility: hidden; - transition: opacity 0.2s ease, visibility 0.2s ease; - } - - .uk-dropbar.uk-open { - opacity: 1; - visibility: visible; - } - - a.uk-button.uk-button-link { - text-transform: none; - } - - /* 移动端优化:缩小字体和间距,避免右侧空白 */ - @media (max-width: 640px) { - .uk-logo { - font-size: 1rem; - } - .uk-navbar-nav a { - font-size: 0.78rem; - padding: 0 4px; - } - .emoji-placeholder { - font-size: 1rem; - } - /* 移动端用户名限宽省略,避免长英文用户名撑爆顶栏 */ - .tb-nav-username { - max-width: 64px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - display: inline-block; - vertical-align: middle; - } - } - -/* 深色模式:html[data-theme="dark"] 时覆盖 UIkit 3.3.3 编译主题的关键颜色 */ - /* 变量(--tb-*)已上移到 head 顶部的令牌块,亮暗两套同名常量在那里定义。 */ - /* 根元素与 body 一起覆盖,确保整个视口(含内容不足一屏时的空白区)都是深色背景 */ - html[data-theme="dark"], - html[data-theme="dark"] body { - background-color: var(--tb-bg) !important; - color: var(--tb-text) !important; - } - /* 通用表面容器 */ - html[data-theme="dark"] .uk-card, - html[data-theme="dark"] .uk-card-default, - html[data-theme="dark"] .uk-card-secondary, - html[data-theme="dark"] .uk-dropbar, - html[data-theme="dark"] .uk-navbar-dropdown, - html[data-theme="dark"] .uk-modal-dialog, - html[data-theme="dark"] .uk-dropdown, - html[data-theme="dark"] .uk-offcanvas-bar, - html[data-theme="dark"] .uk-section, - html[data-theme="dark"] .uk-tile, - html[data-theme="dark"] .uk-panel, - html[data-theme="dark"] .uk-background-default, - html[data-theme="dark"] .uk-search-default .uk-search-input { - background-color: var(--tb-surface) !important; - color: var(--tb-text) !important; - } - html[data-theme="dark"] .uk-navbar-container { - background-color: var(--tb-surface) !important; - color: var(--tb-text) !important; - } - html[data-theme="dark"] .uk-navbar-nav > li > a, - html[data-theme="dark"] .uk-card a, - html[data-theme="dark"] .uk-link, - html[data-theme="dark"] a.uk-button-link, - html[data-theme="dark"] .uk-navbar-dropdown-nav > li > a, - html[data-theme="dark"] .uk-dropdown-nav > li > a, - html[data-theme="dark"] .uk-list a { - color: var(--tb-text) !important; - } - html[data-theme="dark"] .uk-navbar-nav > li > a:hover, - html[data-theme="dark"] .uk-card a:hover, - html[data-theme="dark"] .uk-navbar-dropdown-nav > li > a:hover, - html[data-theme="dark"] .uk-dropdown-nav > li > a:hover { - color: var(--tb-primary) !important; - } - html[data-theme="dark"] .uk-input, - html[data-theme="dark"] .uk-textarea, - html[data-theme="dark"] .uk-select { - background-color: var(--tb-surface-2) !important; - color: var(--tb-text) !important; - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] .uk-input:focus, - html[data-theme="dark"] .uk-textarea:focus, - html[data-theme="dark"] .uk-select:focus { - border-color: var(--tb-primary) !important; - } - html[data-theme="dark"] .uk-text-muted, - html[data-theme="dark"] .uk-text-meta, - html[data-theme="dark"] .uk-text-secondary, - html[data-theme="dark"] .uk-text-muted a, - html[data-theme="dark"] .uk-text-small.uk-text-muted { - color: var(--tb-text-muted) !important; - } - html[data-theme="dark"] .uk-divider-icon::after, - html[data-theme="dark"] hr, - html[data-theme="dark"] .uk-table th, - html[data-theme="dark"] .uk-table td, - html[data-theme="dark"] .uk-list-divider > li:nth-child(n+2) { - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] .uk-button-default { - background-color: var(--tb-surface-2) !important; - color: var(--tb-text) !important; - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] .uk-modal-title, - html[data-theme="dark"] h1, html[data-theme="dark"] h2, html[data-theme="dark"] h3, - html[data-theme="dark"] h4, html[data-theme="dark"] h5, html[data-theme="dark"] h6, - html[data-theme="dark"] .uk-card-title { - color: var(--tb-text) !important; - } - html[data-theme="dark"] .uk-alert-warning, - html[data-theme="dark"] .uk-alert-danger, - html[data-theme="dark"] .uk-alert-success, - html[data-theme="dark"] .uk-alert-primary { - background-color: var(--tb-surface-2) !important; - color: var(--tb-text) !important; - } - html[data-theme="dark"] #math-captcha-modal .uk-modal-dialog { - background-color: var(--tb-surface) !important; - color: var(--tb-text) !important; - } - html[data-theme="dark"] #math-captcha-modal .captcha-equation-box { - background-color: var(--tb-surface-2) !important; - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] #math-captcha-modal .math-captcha-modal-question { - color: var(--tb-text) !important; - } - html[data-theme="dark"] #math-captcha-modal #captcha-modal-answer { - background-color: var(--tb-surface) !important; - color: var(--tb-text) !important; - border-color: var(--tb-border) !important; - } - html[data-theme="dark"] #math-captcha-modal #captcha-modal-answer::placeholder { - color: var(--tb-text-muted) !important; - } - html[data-theme="dark"] #math-captcha-modal .uk-text-meta, - html[data-theme="dark"] #math-captcha-modal #captcha-modal-refresh { - color: var(--tb-text-muted) !important; - } - html[data-theme="dark"] #math-captcha-modal #captcha-modal-refresh:hover { - color: var(--tb-primary) !important; - } - html[data-theme="dark"] .uk-label { - color: #fff !important; - } - /* 自定义组件(探索页推荐卡片等)深色覆盖 */ - html[data-theme="dark"] .recommend-card, - html[data-theme="dark"] .recommend-card:hover { - background-color: var(--tb-surface-2) !important; - } - html[data-theme="dark"] .explore-name a { - color: var(--tb-text) !important; - } - html[data-theme="dark"] .explore-name a:hover { - color: var(--tb-primary) !important; - } - html[data-theme="dark"] .explore-intro { - color: var(--tb-text-muted) !important; - } - /* 切换按钮图标切换(内联 SVG,不依赖 UIkit 图标库) */ - #theme-toggle .tb-icon-light, #theme-toggle-mobile .tb-icon-light, - #theme-toggle .tb-icon-dark, #theme-toggle-mobile .tb-icon-dark { - display: inline-flex; - align-items: center; - } - #theme-toggle .tb-icon-light svg, #theme-toggle-mobile .tb-icon-light svg, - #theme-toggle .tb-icon-dark svg, #theme-toggle-mobile .tb-icon-dark svg { display: block; } - #theme-toggle .tb-icon-dark, #theme-toggle-mobile .tb-icon-dark { display: none; } - html[data-theme="dark"] #theme-toggle .tb-icon-light, - html[data-theme="dark"] #theme-toggle-mobile .tb-icon-light { display: none; } - html[data-theme="dark"] #theme-toggle .tb-icon-dark, - html[data-theme="dark"] #theme-toggle-mobile .tb-icon-dark { display: inline-flex; } - -/* 基础质感层:只调整外观(圆角/投影/字体/数字对齐),不改任何结构与布局。 - 颜色一律取自头部令牌,亮暗两套自动生效,因此这里不需要再写 html[data-theme="dark"] 分支。 */ - /* 根元素与 body 同时上色:内容不足一屏时,body 之外的视口区域也要是同一底色 */ - html, - body { - font-family: var(--tb-font); - background-color: var(--tb-bg); - color: var(--tb-text); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - } - /* 卡片:12px 圆角 + 一层极淡接触影 + 一层大扩散影(UIkit 默认的 0 5px 15px 偏硬) */ - .uk-card { - border-radius: var(--tb-radius); - } - .uk-card-default { - background-color: var(--tb-surface); - box-shadow: var(--tb-shadow); - } - .uk-card-default.uk-card-hover:hover { - box-shadow: var(--tb-shadow-hover); - } - .uk-card-header { - border-radius: var(--tb-radius) var(--tb-radius) 0 0; - } - .uk-card-footer { - border-radius: 0 0 var(--tb-radius) var(--tb-radius); - } - .uk-card, - .uk-button, - .uk-alert, - .uk-label { - transition: box-shadow 0.18s ease, background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease; - } - /* 交互元素:统一 8px 圆角,标签 6px */ - .uk-button { - border-radius: var(--tb-radius-sm); - } - .uk-input, - .uk-select, - .uk-textarea, - .uk-form-small, - .uk-search-input { - border-radius: var(--tb-radius-sm); - } - .uk-label { - border-radius: var(--tb-radius-xs); - } - .uk-alert { - border-radius: 10px; - } - .uk-modal-dialog { - border-radius: 14px; - } - .uk-dropdown, - .uk-navbar-dropdown { - border-radius: 10px; - box-shadow: var(--tb-shadow); - } - .uk-navbar-container:not(.uk-navbar-transparent) { - background-color: var(--tb-surface); - } - /* 键盘可达性:仅在键盘聚焦时出现描边,鼠标点击不出现 */ - .uk-button:focus-visible, - .uk-navbar-nav > li > a:focus-visible { - outline: 2px solid var(--tb-primary); - outline-offset: 2px; - } - /* 次要文字统一走令牌,避免 #999 / #8a919c 混用 */ - .uk-text-muted, - .uk-text-meta { - color: var(--tb-text-muted); - } - -/* 轻量胶囊:用于投票类型、热门标签等「只是分类信息」的位置。 - 实心 uk-label 一屏多个会变成一排色块,这里统一降级为浅底 + 次要色文字。 */ -.tb-chip { - display: inline-block; - padding: 1px 8px; - border-radius: var(--tb-radius-xs); - background-color: var(--tb-surface-2); - color: var(--tb-text-secondary); - font-size: 0.72rem; - line-height: 1.7; - white-space: nowrap; -} -a.tb-chip:hover { - background-color: var(--tb-primary-weak); - color: var(--tb-primary); - text-decoration: none; -} - -/* 统计/日期等数字列等宽对齐,避免刷新时左右跳动 */ - .uk-table, - .tb-num { - font-variant-numeric: tabular-nums; - } - /* 图片上传条:原为 item/new-question 两处各写一份内联 style(写死 #f8f8f8,深色下露白), - 收敛到一处并走令牌,亮暗自动适配 */ - .tb-img-uploader { - width: 100%; - background-color: var(--tb-surface-2); - display: flex; - padding-top: 5px; - padding-bottom: 5px; - align-items: center; - justify-content: flex-start; - gap: 5px; - } - /* 背景图封面(提问箱主页):渐变遮罩,保证用户自选文字色叠加后仍可读。 - 默认浅色遮罩(页面 .uk-light 补丁把文字兜底成深色);若箱主选了浅色文字, - question/list.html 会按亮度加上 .tb-cover-dark 换成深色遮罩。 */ - .tb-cover { - position: relative; - border-radius: var(--tb-radius) var(--tb-radius) 0 0; - overflow: hidden; - } - /* ⚠️ 必须带 .uk-flex 复合选择器:UIkit 3.3.3 里有 - .uk-flex::after,.uk-flex::before{display:none}(优先级 0,2,0), - 会把这个遮罩伪元素整个干掉——不报错、只是什么都不显示(踩过一次)。 - 单写 .tb-cover::after 优先级不够,会被它压回 display:none。 */ - .tb-cover::after, - .tb-cover.uk-flex::after { - content: ""; - display: block; - position: absolute; - inset: 0; - z-index: 0; - background: linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.58) 55%, rgba(255, 255, 255, 0.84) 100%); - pointer-events: none; - } - .tb-cover.tb-cover-dark::after { - background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.16) 45%, rgba(0, 0, 0, 0.46) 100%); - } - .tb-cover > * { - position: relative; - z-index: 1; - } - .tb-cover h3, - .tb-cover p { - text-shadow: 0 1px 10px rgba(255, 255, 255, 0.65); - } - .tb-cover.tb-cover-dark h3, - .tb-cover.tb-cover-dark p { - text-shadow: 0 1px 12px rgba(0, 0, 0, 0.45); - } - /* 尊重系统的减少动效设置 */ - @media (prefers-reduced-motion: reduce) { - .uk-card, - .uk-button, - .emoji-placeholder { - transition: none; - animation: none; - } - } - -/* 顶栏自适应:导航全部直接显示,窄屏时分级缩小字号/间距,保证英文/日文等长文字不溢出 */ - .uk-navbar-nav a { - font-size: 0.95rem; - padding: 0 10px; - } - /* 未登录移动端占位:桌面端隐藏,仅 ≤640px 显示(见下方 media 查询)。 - 桌面端布局与改动前完全一致,不受影响。 */ - .tb-mobile-balancer { display: none; } - /* 桌面端保持 UIkit 原生居中三列布局(无需额外 CSS) */ - .tb-navbar .uk-logo { - white-space: nowrap; - } - @media (max-width: 1200px) { - .uk-navbar-nav a { - font-size: 0.88rem; - padding: 0 8px; - } - } - @media (max-width: 1000px) { - .uk-navbar-nav a { - font-size: 0.82rem; - padding: 0 6px; - } - .uk-logo { - font-size: 1.1rem; - } - /* 登录用户名限宽,过长省略号截断 */ - .tb-nav-username { - max-width: 110px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - display: inline-block; - vertical-align: middle; - } - } - @media (max-width: 800px) { - .uk-navbar-nav a { - font-size: 0.78rem; - padding: 0 4px; - } - .uk-logo { - font-size: 1rem; - } - .tb-nav-username { - max-width: 80px; - } - } - /* 手机端(≤640px):沿用 UIkit 桌面端 navbar 的原生三列定位。 - 不再重写 .uk-navbar-center / center-left / center-right 的定位, - 避免 logo 被挤到下一行;底部语言与深色模式按钮仍由下方独立栏承载。 */ - @media (max-width: 640px) { - .tb-navbar { - position: relative; - min-height: 56px; - overflow: visible; - } - .tb-navbar > .uk-navbar-center { - position: relative !important; - top: auto !important; - left: auto !important; - right: auto !important; - bottom: auto !important; - transform: none !important; - display: flex !important; - align-items: center; - justify-content: center; - width: 100%; - min-height: 56px; - white-space: nowrap; - flex-wrap: nowrap; - } - .tb-navbar .uk-navbar-center-left, - .tb-navbar .uk-navbar-center-right { - position: static !important; - top: auto !important; - left: auto !important; - right: auto !important; - bottom: auto !important; - transform: none !important; - display: flex !important; - align-items: center; - flex: 0 0 auto; - white-space: nowrap; - } - .tb-navbar .uk-navbar-center-left > div, - .tb-navbar .uk-navbar-center-right > div { - display: flex; - align-items: center; - white-space: nowrap; - } - .tb-navbar .uk-navbar-nav { - display: flex; - flex-wrap: nowrap; - flex: 0 0 auto; - } - .tb-navbar .uk-navbar-nav > li > a, - .tb-navbar .uk-logo { - white-space: nowrap; - } - /* 未登录:移动端显示与「我的」等宽的隐藏占位,使左栏「探索+占位」与 - 右栏「注册/登录」两项等宽,logo 真正居中,左右视觉平衡 */ - .tb-mobile-balancer { display: block; } - } - -/* 标签样式的按钮(管理页置顶等): - 尺寸完全继承 .uk-label(font-size/padding/line-height/text-transform 等), - 仅覆盖 button 元素特有的默认样式(边框/字体族/光标等),保证与相邻 span 标签完全一致。 */ - .tb-label-btn { - border: none !important; - cursor: pointer; - font-family: inherit; - margin: 0; - vertical-align: middle; - outline: none; - background-image: none; - box-sizing: border-box; - -webkit-appearance: none; - appearance: none; - } - .tb-label-btn:hover { - opacity: 0.85; - } - /* 公开页置顶小图标:仅一个图钉,不占行高、不影响原布局 */ - .tb-pin-flag { - font-size: 0.9em; - margin-right: 4px; - vertical-align: -1px; - cursor: default; - } - -/* Markdown 渲染排版(问题/回答/追问正文,左对齐以保留排版格式) */ - /* 纯文本列表:保留换行与连续空格(pre-wrap),不渲染 Markdown 大字号/彩色 */ - .tb-preline { - white-space: pre-wrap; - } - /* 纯文本列表:超过 5 行截断并显示省略号(不足 5 行则完整显示) */ - .tb-clamp-5 { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 5; - overflow: hidden; - } - .md-body { - line-height: 1.7; - text-align: left; - word-break: break-word; - } - .md-body p { margin: 0.5em 0; } - .md-body h1, .md-body h2, .md-body h3, - .md-body h4, .md-body h5, .md-body h6 { - margin: 0.8em 0 0.4em; - font-weight: 600; - line-height: 1.4; - } - .md-body ul, .md-body ol { padding-left: 1.6em; margin: 0.5em 0; } - .md-body li { margin: 0.2em 0; } - .md-body blockquote { - border-left: 3px solid #d3d1c7; - margin: 0.6em 0; - padding: 0.2em 1em; - color: #5f5e5a; - } - .md-body pre { - background: #f5f5f5; - padding: 0.8em 1em; - border-radius: 6px; - overflow-x: auto; - margin: 0.6em 0; - } - .md-body code { - background: #f0f0f0; - padding: 0.1em 0.35em; - border-radius: 4px; - font-size: 0.9em; - } - .md-body pre code { background: none; padding: 0; } - .md-body img { max-width: 100%; height: auto; } - .md-body table { border-collapse: collapse; margin: 0.6em 0; } - .md-body th, .md-body td { - border: 1px solid #ddd; - padding: 0.4em 0.8em; - } - .md-body hr { margin: 1em 0; } - html[data-theme="dark"] .md-body blockquote { - border-color: #3a404c; - color: #8b93a1; - } - html[data-theme="dark"] .md-body pre, - html[data-theme="dark"] .md-body code { background: #2d323c; } - html[data-theme="dark"] .md-body th, - html[data-theme="dark"] .md-body td { border-color: #3a404c; } - -/* 投票结果占比条:模板已改用 UIkit 3 原生 , - 这里只把默认的 15px 高 / 500px 圆角 / 20px 外边距收紧到与页面一致的规格。 - (原先为 UIkit 2 的 .uk-progressbar 结构打的补丁已删除) */ -.uk-progress { - height: 10px; - margin: 0; - border-radius: var(--tb-radius-xs); - background-color: var(--tb-surface-2); -} -.uk-progress::-webkit-progress-bar { - background-color: var(--tb-surface-2); - border-radius: var(--tb-radius-xs); -} -.uk-progress::-webkit-progress-value { - background-color: var(--tb-primary); - border-radius: var(--tb-radius-xs) 0 0 var(--tb-radius-xs); -} -.uk-progress::-moz-progress-bar { - background-color: var(--tb-primary); -} - -/* 移动端底部操作栏:固定底栏承载语言切换与深色模式,桌面端隐藏 */ - #tb-mobile-utils { - display: none; - } - @media (max-width: 640px) { - /* 顶栏不再容纳语言/深色按钮,恢复原有导航空间 */ - .tb-nav-utils { - display: none !important; - } - /* 手机端隐藏登录用户名(左边仅保留"探索"),保证 logo 居中、 - 两侧各不超过 2 个短按钮,避免用户名宽度挤压布局 */ - .tb-nav-username { - display: none !important; - } - /* 固定底部操作栏 */ - #tb-mobile-utils { - display: flex; - position: fixed; - left: 0; - right: 0; - bottom: 0; - z-index: 1000; - justify-content: center; - align-items: center; - gap: 10px; - padding: 8px 12px calc(8px + env(safe-area-inset-bottom)); - background: rgba(255, 255, 255, 0.92); - -webkit-backdrop-filter: blur(8px); - backdrop-filter: blur(8px); - box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.08); - } - #tb-mobile-utils .uk-select { - width: 110px; - } - html[data-theme="dark"] #tb-mobile-utils { - background: rgba(36, 40, 48, 0.92); - box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.4); - } - /* 给页面底部预留空间,避免页脚内容被固定底栏遮挡 */ - body { - padding-bottom: 56px; - } - } diff --git a/outputs/verify/p2-home.hdr b/outputs/verify/p2-home.hdr deleted file mode 100644 index 70b035e..0000000 --- a/outputs/verify/p2-home.hdr +++ /dev/null @@ -1,23 +0,0 @@ -HTTP/1.1 200 Connection Established - -HTTP/1.1 200 OK -Date: Mon, 19 Oct 2026 04:22:53 GMT -Content-Type: text/html; charset=utf-8 -Transfer-Encoding: chunked -Connection: keep-alive -Speculation-Rules: "/cdn-cgi/speculation" -referrer-policy: strict-origin-when-cross-origin -Set-Cookie: flamego_session=sa1s4htg9ll3f6qw; Path=/; HttpOnly; Secure; SameSite=Lax -strict-transport-security: max-age=31536000; includeSubDomains -trace-id: 00000000000000000000000000000000 -vary: Accept-Encoding -x-content-type-options: nosniff -x-frame-options: DENY -x-xss-protection: 1; mode=block -cf-cache-status: DYNAMIC -Nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800} -Report-To: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=s4%2FRWTCedXYJQnbw46UeaM1OcjLEpBS32BiqnIVkSxWffJBUOeQX52rMEhSWxJQBW%2B7BHcmVxrdiqzpfMfj1EjifJX3tFmiFo%2FZC7xzrK48mAdNhMA2%2BDDrJWMMkG7hEZAk5QZ5%2FDH%2B8INO%2BnA%3D%3D"}]} -Server: cloudflare -CF-RAY: a4cd01154a414a25-AMS -alt-svc: h3=":443"; ma=86400 - diff --git a/outputs/verify/p2-home.html b/outputs/verify/p2-home.html deleted file mode 100644 index 61b5619..0000000 --- a/outputs/verify/p2-home.html +++ /dev/null @@ -1,937 +0,0 @@ - - - - - - 狼の質問箱 | 匿名質問箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - -
-

狼の質問箱 | 匿名質問箱

-

- 一个无需注册即可提问的匿名提问箱。不用下载 App,不用绑定社交账号,连邮箱都不必是真的。 -

- -
- -
-
-

匿名也能问

-

不需要账号、不需要真实邮箱,打开就能写。想收到回复再留邮箱即可。

-
-
-

不止一问一答

-

问题二维码、私聊、投票、追加提问、Markdown 排版,原版没有的这里都有。

-
-
-

箱子归你

-

头像、背景、二维码配色都能自己调,还能设置审核严格度和收集时段。

-
-
- -
-

域名迁移

-

- 本站已切换到 box.shiroko.one,原域名将在 2026 年 12 月 24 日到期, - 期间通过 301 转发指向新域名,到期后旧域名将彻底不再使用。 -

-

- 审核服务已切换到阿里云,审核速度提升且误伤更少;同时允许用户调整审核严格度,在不碰底线的前提下 - 可以设置不接受辱骂、广告等信息(即「不听坏话模式」)。 -

-
- -
-

一点提醒

-
    -
  • 本站唯一指定认证站长:小狼,注意不要被骗。
  • -
  • 本站提倡友好、友善的发言,请遵守社区规则。无论匿名与否,都要为自己的发言负责。
  • -
  • 不允许垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义、灌水提问,涉政或不良言论将直接删除。
  • -
-

- 想先试试功能,可以到站长的提问箱 - 小狼的提问箱 提问或提意见。 -

-
- -
- 近期更新 -
    -
  • 2026.9 新增审核严格度选项,增加提问、投票、私聊的总开关,可设置定时收集、定量收集或关闭功能,前后端显示也更易用了。
  • -
  • 2026.8 新增投票功能、追加提问、多语言、Markdown 显示、置顶显示等,详情见这里。
  • -
  • 2026.8 放弃使用 recaptcha 等对国内用户不友好的验证方式,更新密码加密方式,修复图片上传等一系列问题。
  • -
  • 2026.5 增加私聊功能,可连续对话,详见介绍;另有提问箱卡片分享。
  • -
  • 2026.2 新增用户展示和标签功能,可给自己贴 5 个标签并选择是否在探索页展示,详情见狼的公告板。
  • -
-
- -
-

質問箱の統計

-
-
- 28 - 注册用户 -
-
- 94 - 累计提问 -
-
- 21 - 已获回答 -
-
-

- 最后提问:2026-10-19 00:58:03
- 最后回答:2026-10-19 02:44:47
- 数据更新于:2026-10-19 12:22:53 -

-
- -
- -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - diff --git a/outputs/verify/p2-http.html b/outputs/verify/p2-http.html deleted file mode 100644 index 428c5fb..0000000 --- a/outputs/verify/p2-http.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - -
-
-

导航图标(原 emoji 🌐🌙☀️)

-
- - - - -
-
-
-

标签(原实心彩色 uk-label)

-
- 单选多选 - 评分排序 - 表单 - #杂谈#音乐 -
-
-
-

占比条(原生 progress)

- - - -
-
-
- - \ No newline at end of file diff --git a/outputs/verify/p2-probe.html b/outputs/verify/p2-probe.html deleted file mode 100644 index da59563..0000000 --- a/outputs/verify/p2-probe.html +++ /dev/null @@ -1,720 +0,0 @@ - - - - - -
-
-

导航图标(原 emoji)

-
- - - - -
-
-
-

标签(原实心彩色 uk-label)

-
- 单选多选 - 评分排序 - 表单 - #杂谈#音乐 -
-
-
-

占比条(原生 progress)

- - - -
-
-
- - -
- - \ No newline at end of file diff --git a/outputs/verify/p2-probe3.html b/outputs/verify/p2-probe3.html deleted file mode 100644 index 0f5532a..0000000 --- a/outputs/verify/p2-probe3.html +++ /dev/null @@ -1,714 +0,0 @@ - - - - - -
-
-

导航图标(原 emoji)

-
- - - - -
-
-
-

标签(原实心彩色 uk-label)

-
- 单选多选 - 评分排序 - 表单 - #杂谈#音乐 -
-
-
-

占比条(原生 progress)

- - - -
-
-
- - -
- - \ No newline at end of file diff --git a/outputs/verify/p2-results.html b/outputs/verify/p2-results.html deleted file mode 100644 index 6b2a731..0000000 --- a/outputs/verify/p2-results.html +++ /dev/null @@ -1,626 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:吃什么

- -
- -
-
0
-
总参与人数
-
- - - - -

投票结果

-
- -
-
- 1 - 单行文本 - 是啊,吃什么? - 回答数: 0 -
- - -
已填写: 0
- - - -
- -
- - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/p2-sheet.html b/outputs/verify/p2-sheet.html deleted file mode 100644 index de1af51..0000000 --- a/outputs/verify/p2-sheet.html +++ /dev/null @@ -1,709 +0,0 @@ - - - - - -
-
-

导航图标(原 emoji)

-
- - - - -
-
-
-

标签(原实心彩色 uk-label)

-
- 单选多选 - 评分排序 - 表单 - #杂谈#音乐 -
-
-
-

占比条(原生 progress)

- - - -
-
-
- - \ No newline at end of file diff --git a/outputs/verify/p2-vote.html b/outputs/verify/p2-vote.html deleted file mode 100644 index d154720..0000000 --- a/outputs/verify/p2-vote.html +++ /dev/null @@ -1,885 +0,0 @@ - - - - - - 测试 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - -
- admin6 -
の投票
-
- -
- - - 進行中 - -
-
- -

测试

- -

测试

- -
- - - - - - - - - -
匿名での参加が可能で、あなたの身元は記録されません(同一端末で最大1回)。
- - -
- - - -
- - -
-
- 1 - 一行テキスト -
测试 *
-
- - - -
- - - - -
- -
- - - -
-
- - - - - - - - - -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/pm-home.html b/outputs/verify/pm-home.html deleted file mode 100644 index 9a8c3d7..0000000 --- a/outputs/verify/pm-home.html +++ /dev/null @@ -1,1629 +0,0 @@ - - - - - - 狼の質問箱 | 匿名質問箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - -
-

狼の質問箱 | 匿名質問箱

-

- 一个无需注册即可提问的匿名提问箱。不用下载 App,不用绑定社交账号,连邮箱都不必是真的。 -

- -
- -
-
-

匿名也能问

-

不需要账号、不需要真实邮箱,打开就能写。想收到回复再留邮箱即可。

-
-
-

不止一问一答

-

问题二维码、私聊、投票、追加提问、Markdown 排版,原版没有的这里都有。

-
-
-

箱子归你

-

头像、背景、二维码配色都能自己调,还能设置审核严格度和收集时段。

-
-
- -
-

域名迁移

-

- 本站已切换到 box.shiroko.one,原域名将在 2026 年 12 月 24 日到期, - 期间通过 301 转发指向新域名,到期后旧域名将彻底不再使用。 -

-

- 审核服务已切换到阿里云,审核速度提升且误伤更少;同时允许用户调整审核严格度,在不碰底线的前提下 - 可以设置不接受辱骂、广告等信息(即「不听坏话模式」)。 -

-
- -
-

一点提醒

-
    -
  • 本站唯一指定认证站长:小狼,注意不要被骗。
  • -
  • 本站提倡友好、友善的发言,请遵守社区规则。无论匿名与否,都要为自己的发言负责。
  • -
  • 不允许垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义、灌水提问,涉政或不良言论将直接删除。
  • -
-

- 想先试试功能,可以到站长的提问箱 - 小狼的提问箱 提问或提意见。 -

-
- -
- 近期更新 -
    -
  • 2026.9 新增审核严格度选项,增加提问、投票、私聊的总开关,可设置定时收集、定量收集或关闭功能,前后端显示也更易用了。
  • -
  • 2026.8 新增投票功能、追加提问、多语言、Markdown 显示、置顶显示等,详情见这里。
  • -
  • 2026.8 放弃使用 recaptcha 等对国内用户不友好的验证方式,更新密码加密方式,修复图片上传等一系列问题。
  • -
  • 2026.5 增加私聊功能,可连续对话,详见介绍;另有提问箱卡片分享。
  • -
  • 2026.2 新增用户展示和标签功能,可给自己贴 5 个标签并选择是否在探索页展示,详情见狼的公告板。
  • -
-
- -
-

質問箱の統計

-
-
- 28 - 注册用户 -
-
- 94 - 累计提问 -
-
- 21 - 已获回答 -
-
-

- 最后提问:2026-10-19 00:58:03
- 最后回答:2026-10-19 02:44:47
- 数据更新于:2026-10-19 12:48:26 -

-
- -
- -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - -
- - - diff --git a/outputs/verify/pm-results.html b/outputs/verify/pm-results.html deleted file mode 100644 index e5c7755..0000000 --- a/outputs/verify/pm-results.html +++ /dev/null @@ -1,1621 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:11

- -
- -
-
2
-
总参与人数
-
- - - - -

投票结果

-
- -
-
- 1 - 单选 - 11 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(100%) -
- -
- -
-
- 22 - 0 票(0%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 2 - 多选 - 22 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(66%) -
- -
- -
-
- 22 - 1 票(33%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 3 - 单行文本 - 33 - 回答数: 2 -
- - -
已填写: 2
- - -
-
- ▾ - 查看文本提交(2) -
- -
- - -
- -
-
- 4 - 多行文本 - 44 - 回答数: 2 -
- - -
已填写: 2
- - -
-
- ▾ - 查看文本提交(2) -
- -
- - -
- -
-
- 5 - 下拉选择 - 55 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(100%) -
- -
- -
-
- 22 - 0 票(0%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 6 - 数字 - 66 - 回答数: 2 -
- - -
平均: 4.0(2 票)
- - -
- -
-
- 7 - 评分 - 77 - 回答数: 2 -
- - -
平均分: 9.0(2 票)
- -
- 1 分 - 0 票(0%) -
- - -
- 2 分 - 0 票(0%) -
- - -
- 3 分 - 0 票(0%) -
- - -
- 4 分 - 0 票(0%) -
- - -
- 5 分 - 0 票(0%) -
- - -
- 6 分 - 0 票(0%) -
- - -
- 7 分 - 0 票(0%) -
- - -
- 8 分 - 1 票(50%) -
- - -
- 9 分 - 0 票(0%) -
- - -
- 10 分 - 1 票(50%) -
- - - - -
- -
-
- 8 - 排序 - 88 - 回答数: 2 -
- - -
平均排名: 1.48(2 票)
- -
- 11 - 平均排名: 1.10 · 被排次数: 2 -
- -
- 22 - 平均排名: 1.25 · 被排次数: 2 -
- -
- 33 - 平均排名: 2.10 · 被排次数: 2 -
- - - -
- -
- - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - -
- - - - diff --git a/outputs/verify/probe-explore.html b/outputs/verify/probe-explore.html deleted file mode 100644 index 2078805..0000000 --- a/outputs/verify/probe-explore.html +++ /dev/null @@ -1,2216 +0,0 @@ - - - - - - 探索 - 狼の質問箱 | 匿名質問箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - -
-

ユーザータグを探索

- -
-

ここでは「ホームでタグを表示」を有効にしたユーザーを紹介しています。この機能は初期状態でオフです。自分の質問箱を見つけてもらいたい場合は 個人設定 から有効にしてください。

-
- -
- - -
- -
-
-
-

ランダムおすすめ

- -
- -
- - - -
-

人気タグ

-
- - #测试 - - #胡言乱语 - -
-
- - -
-
-
-
- - -
-
-
- -
-
-
- - -
- - -
-
-
-
- avatar -
-
-
- admin3x -
-
- 这里的内容可能会很长,希望能够自动分段,不然就很难看了,万一要是更长的话会怎么样呢???? -
-
- - - 测试 - - 胡言乱语 - - -
-
-
-
-
- -
- -
    - -
  • 前へ
  • - -
  • 1 / 1
  • - -
  • 次へ
  • - -
- -
- - -
- - -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - -
- - - - diff --git a/outputs/verify/probe-mobile.html b/outputs/verify/probe-mobile.html deleted file mode 100644 index 3ff28a8..0000000 --- a/outputs/verify/probe-mobile.html +++ /dev/null @@ -1,1635 +0,0 @@ - - - - - - 狼の質問箱 | 匿名質問箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - -
-

狼の質問箱 | 匿名質問箱

-

- 一个无需注册即可提问的匿名提问箱。不用下载 App,不用绑定社交账号,连邮箱都不必是真的。 -

- -
- -
-
-

匿名也能问

-

不需要账号、不需要真实邮箱,打开就能写。想收到回复再留邮箱即可。

-
-
-

不止一问一答

-

问题二维码、私聊、投票、追加提问、Markdown 排版,原版没有的这里都有。

-
-
-

箱子归你

-

头像、背景、二维码配色都能自己调,还能设置审核严格度和收集时段。

-
-
- -
-

域名迁移

-

- 本站已切换到 box.shiroko.one,原域名将在 2026 年 12 月 24 日到期, - 期间通过 301 转发指向新域名,到期后旧域名将彻底不再使用。 -

-

- 审核服务已切换到阿里云,审核速度提升且误伤更少;同时允许用户调整审核严格度,在不碰底线的前提下 - 可以设置不接受辱骂、广告等信息(即「不听坏话模式」)。 -

-
- -
-

一点提醒

-
    -
  • 本站唯一指定认证站长:小狼,注意不要被骗。
  • -
  • 本站提倡友好、友善的发言,请遵守社区规则。无论匿名与否,都要为自己的发言负责。
  • -
  • 不允许垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义、灌水提问,涉政或不良言论将直接删除。
  • -
-

- 想先试试功能,可以到站长的提问箱 - 小狼的提问箱 提问或提意见。 -

-
- -
- 近期更新 -
    -
  • 2026.9 新增审核严格度选项,增加提问、投票、私聊的总开关,可设置定时收集、定量收集或关闭功能,前后端显示也更易用了。
  • -
  • 2026.8 新增投票功能、追加提问、多语言、Markdown 显示、置顶显示等,详情见这里。
  • -
  • 2026.8 放弃使用 recaptcha 等对国内用户不友好的验证方式,更新密码加密方式,修复图片上传等一系列问题。
  • -
  • 2026.5 增加私聊功能,可连续对话,详见介绍;另有提问箱卡片分享。
  • -
  • 2026.2 新增用户展示和标签功能,可给自己贴 5 个标签并选择是否在探索页展示,详情见狼的公告板。
  • -
-
- -
-

質問箱の統計

-
-
- 28 - 注册用户 -
-
- 94 - 累计提问 -
-
- 21 - 已获回答 -
-
-

- 最后提问:2026-10-19 00:58:03
- 最后回答:2026-10-19 02:44:47
- 数据更新于:2026-10-19 12:48:26 -

-
- -
- -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - -
- - - diff --git a/outputs/verify/probe-res5.html b/outputs/verify/probe-res5.html deleted file mode 100644 index c43e32a..0000000 --- a/outputs/verify/probe-res5.html +++ /dev/null @@ -1,1629 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:11

- -
- -
-
2
-
总参与人数
-
- - - - -

投票结果

-
- -
-
- 1 - 单选 - 11 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(100%) -
- -
- -
-
- 22 - 0 票(0%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 2 - 多选 - 22 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(66%) -
- -
- -
-
- 22 - 1 票(33%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 3 - 单行文本 - 33 - 回答数: 2 -
- - -
已填写: 2
- - -
-
- ▾ - 查看文本提交(2) -
- -
- - -
- -
-
- 4 - 多行文本 - 44 - 回答数: 2 -
- - -
已填写: 2
- - -
-
- ▾ - 查看文本提交(2) -
- -
- - -
- -
-
- 5 - 下拉选择 - 55 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(100%) -
- -
- -
-
- 22 - 0 票(0%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 6 - 数字 - 66 - 回答数: 2 -
- - -
平均: 4.0(2 票)
- - -
- -
-
- 7 - 评分 - 77 - 回答数: 2 -
- - -
平均分: 9.0(2 票)
- -
- 1 分 - 0 票(0%) -
- - -
- 2 分 - 0 票(0%) -
- - -
- 3 分 - 0 票(0%) -
- - -
- 4 分 - 0 票(0%) -
- - -
- 5 分 - 0 票(0%) -
- - -
- 6 分 - 0 票(0%) -
- - -
- 7 分 - 0 票(0%) -
- - -
- 8 分 - 1 票(50%) -
- - -
- 9 分 - 0 票(0%) -
- - -
- 10 分 - 1 票(50%) -
- - - - -
- -
-
- 8 - 排序 - 88 - 回答数: 2 -
- - -
平均排名: 1.48(2 票)
- -
- 11 - 平均排名: 1.10 · 被排次数: 2 -
- -
- 22 - 平均排名: 1.25 · 被排次数: 2 -
- -
- 33 - 平均排名: 2.10 · 被排次数: 2 -
- - - -
- -
- - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - -
- - - - diff --git a/outputs/verify/profile-preview.html b/outputs/verify/profile-preview.html deleted file mode 100644 index 3ef7f54..0000000 --- a/outputs/verify/profile-preview.html +++ /dev/null @@ -1,3299 +0,0 @@ - - - - - - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
-

裁剪图片

-
-
-
- -
-

-
- -
-
- - -
- - - - - -
- - - - - - - - - - - - - - - -
-
-
-
- - 防骚扰设置 -
-
- -
-
- -
-
-
- -
- - - -
-
- - 全局屏蔽词 -
-

屏蔽词对提问、私聊(对方发给你的消息)与投票提交内容生效;三个设置页同步显示同一份设置,任意一处修改即时生效。

- - -
-
- -
-
-
- - - - - -
-
- - 审核严格度 - -

对提问、投票、私聊三类内容同时生效。这里调节的是「审核命中之后是否拦截」,不影响屏蔽词——屏蔽词始终直接拒绝。

-

底线拦截(所有档位强制生效,不可关闭):涉政、暴恐、色情,以及审核服务自定义词库的命中。

- - - - - - - - - - - - - - - - - - - - - - - - -
档位拦截范围
畅所欲言仅拦截底线内容,其余一律放行(误伤最少)。
正常水平底线内容,以及置信度不低于 90% 的辱骂、违禁内容。
不听坏话底线内容,以及辱骂、违禁、垃圾信息、广告、灌水、无意义、不良场景内容(不限置信度);同时采信审核服务给出的整体拦截建议。最严格档,拦截面不小于升级前。
- -
- - - -
- -
-
- 未设置时跟随站点默认档位。 -
-
- -
- -
-
-
- - -
-
-
- - 页面访问密码 -
- - -
-
- -
-
-
- -
-
-
- - 提问密码 -
- - -
-
- -
-
-
-
-
- - - - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - -
- - - - diff --git a/outputs/verify/profile.html b/outputs/verify/profile.html deleted file mode 100644 index 434f677..0000000 --- a/outputs/verify/profile.html +++ /dev/null @@ -1,2632 +0,0 @@ - - - - - - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
-

裁剪图片

-
-
-
- -
-

-
- -
-
- - -
- - - - - -
- - - - - - - - - - - - - - - -
-
-
-
- - 防骚扰设置 -
-
- -
-
- -
-
-
- -
- - - -
-
- - 全局屏蔽词 -
-

屏蔽词对提问、私聊(对方发给你的消息)与投票提交内容生效;三个设置页同步显示同一份设置,任意一处修改即时生效。

- - -
-
- -
-
-
- - - - - -
-
- - 审核严格度 - -

对提问、投票、私聊三类内容同时生效。这里调节的是「审核命中之后是否拦截」,不影响屏蔽词——屏蔽词始终直接拒绝。

-

底线拦截(所有档位强制生效,不可关闭):涉政、暴恐、色情,以及审核服务自定义词库的命中。

- - - - - - - - - - - - - - - - - - - - - - - - -
档位拦截范围
畅所欲言仅拦截底线内容,其余一律放行(误伤最少)。
正常水平底线内容,以及置信度不低于 90% 的辱骂、违禁内容。
不听坏话底线内容,以及辱骂、违禁、垃圾信息、广告、灌水、无意义、不良场景内容(不限置信度);同时采信审核服务给出的整体拦截建议。最严格档,拦截面不小于升级前。
- -
- - - -
- -
-
- 未设置时跟随站点默认档位。 -
-
- -
- -
-
-
- - -
-
-
- - 页面访问密码 -
- - -
-
- -
-
-
- -
-
-
- - 提问密码 -
- - -
-
- -
-
-
-
-
- - - - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/res-12.html b/outputs/verify/res-12.html deleted file mode 100644 index 7b4ec3d..0000000 --- a/outputs/verify/res-12.html +++ /dev/null @@ -1,758 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:测试功能

- -
- -
-
3
-
总参与人数
-
- - - - -

投票结果

-
- -
-
- 1 - 单选 - 必填 - 回答数: 3 -
- - - - - -
-
- A - 3 票(100%) -
- -
- -
-
- B - 0 票(0%) -
- -
- -
-
- C - 0 票(0%) -
- -
- - - - -
- -
-
- 2 - 多选 - 非必填 - 回答数: 1 -
- - - - - -
-
- A - 1 票(100%) -
- -
- -
-
- B - 0 票(0%) -
- -
- -
-
- C - 0 票(0%) -
- -
- -
-
- D - 0 票(0%) -
- -
- - - - -
- -
-
- 3 - 单行文本 - 文本1 - 回答数: 0 -
- - -
已填写: 0
- - - -
- -
-
- 4 - 多行文本 - 文本2 - 回答数: 3 -
- - -
已填写: 3
- - -
-
- ▾ - 查看文本提交(3) -
- -
- - -
- -
- - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/res-13.html b/outputs/verify/res-13.html deleted file mode 100644 index 6b2a731..0000000 --- a/outputs/verify/res-13.html +++ /dev/null @@ -1,626 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:吃什么

- -
- -
-
0
-
总参与人数
-
- - - - -

投票结果

-
- -
-
- 1 - 单行文本 - 是啊,吃什么? - 回答数: 0 -
- - -
已填写: 0
- - - -
- -
- - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/res-2.html b/outputs/verify/res-2.html deleted file mode 100644 index e1688e1..0000000 --- a/outputs/verify/res-2.html +++ /dev/null @@ -1,609 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:今天吃什么

- -
- -
-
2
-
总参与人数
-
- - - -

投票结果

-
- - - -
- - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/res-3.html b/outputs/verify/res-3.html deleted file mode 100644 index aa7fae9..0000000 --- a/outputs/verify/res-3.html +++ /dev/null @@ -1,693 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:明天吃什么

- -
- -
-
1
-
总参与人数
-
- - - -

投票结果

-
- -
平均分: 8.0
- -
-
- 1 分 - 0 票 -
- -
- -
-
- 2 分 - 0 票 -
- -
- -
-
- 3 分 - 0 票 -
- -
- -
-
- 4 分 - 0 票 -
- -
- -
-
- 5 分 - 0 票 -
- -
- -
-
- 6 分 - 0 票 -
- -
- -
-
- 7 分 - 0 票 -
- -
- -
-
- 8 分 - 1 票 -
- -
- -
-
- 9 分 - 0 票 -
- -
- -
-
- 10 分 - 0 票 -
- -
- - - - - -
- - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/res-4.html b/outputs/verify/res-4.html deleted file mode 100644 index d27fe4b..0000000 --- a/outputs/verify/res-4.html +++ /dev/null @@ -1,636 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:测试

- -
- -
-
1
-
总参与人数
-
- - - -

投票结果

-
- - -
-
- 测试 - 1 票(100%) -
- -
- -
-
- 测试2 - 0 票(0%) -
- -
- -
-
- 测试3 - 0 票(0%) -
- -
- - - - - -
- - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/res-5.html b/outputs/verify/res-5.html deleted file mode 100644 index e443e2f..0000000 --- a/outputs/verify/res-5.html +++ /dev/null @@ -1,929 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:11

- -
- -
-
2
-
总参与人数
-
- - - - -

投票结果

-
- -
-
- 1 - 单选 - 11 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(100%) -
- -
- -
-
- 22 - 0 票(0%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 2 - 多选 - 22 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(66%) -
- -
- -
-
- 22 - 1 票(33%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 3 - 单行文本 - 33 - 回答数: 2 -
- - -
已填写: 2
- - -
-
- ▾ - 查看文本提交(2) -
- -
- - -
- -
-
- 4 - 多行文本 - 44 - 回答数: 2 -
- - -
已填写: 2
- - -
-
- ▾ - 查看文本提交(2) -
- -
- - -
- -
-
- 5 - 下拉选择 - 55 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(100%) -
- -
- -
-
- 22 - 0 票(0%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 6 - 数字 - 66 - 回答数: 2 -
- - -
平均: 4.0(2 票)
- - -
- -
-
- 7 - 评分 - 77 - 回答数: 2 -
- - -
平均分: 9.0(2 票)
- -
- 1 分 - 0 票(0%) -
- - -
- 2 分 - 0 票(0%) -
- - -
- 3 分 - 0 票(0%) -
- - -
- 4 分 - 0 票(0%) -
- - -
- 5 分 - 0 票(0%) -
- - -
- 6 分 - 0 票(0%) -
- - -
- 7 分 - 0 票(0%) -
- - -
- 8 分 - 1 票(50%) -
- - -
- 9 分 - 0 票(0%) -
- - -
- 10 分 - 1 票(50%) -
- - - - -
- -
-
- 8 - 排序 - 88 - 回答数: 2 -
- - -
平均排名: 1.48(2 票)
- -
- 11 - 平均排名: 1.10 · 被排次数: 2 -
- -
- 22 - 平均排名: 1.25 · 被排次数: 2 -
- -
- 33 - 平均排名: 2.10 · 被排次数: 2 -
- - - -
- -
- - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/res-6.html b/outputs/verify/res-6.html deleted file mode 100644 index 608fffc..0000000 --- a/outputs/verify/res-6.html +++ /dev/null @@ -1,701 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:测试4

- -
- -
-
0
-
总参与人数
-
- - - - -

投票结果

-
- -
-
- 1 - 单选 - 这里显示第一题 - 回答数: 0 -
- - - - - -
-
- A - 0 票(0%) -
- -
- -
-
- B - 0 票(0%) -
- -
- -
-
- C - 0 票(0%) -
- -
- -
-
- D - 0 票(0%) -
- -
- -
-
- E - 0 票(0%) -
- -
- - - - -
- -
-
- 2 - 单选 - 11 - 回答数: 0 -
- - - - - -
-
- 1 - 0 票(0%) -
- -
- -
-
- 2 - 0 票(0%) -
- -
- - - - -
- -
- - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/res-7.html b/outputs/verify/res-7.html deleted file mode 100644 index 999a48a..0000000 --- a/outputs/verify/res-7.html +++ /dev/null @@ -1,628 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:测试5

- -
- -
-
0
-
总参与人数
-
- - - -

投票结果

-
- - -
-
- 1 - 0 票(0%) -
- -
- -
-
- 2 - 0 票(0%) -
- -
- - - - - -
- - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/res-8.html b/outputs/verify/res-8.html deleted file mode 100644 index ef01cde..0000000 --- a/outputs/verify/res-8.html +++ /dev/null @@ -1,628 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:测试7

- -
- -
-
0
-
总参与人数
-
- - - -

投票结果

-
- - -
-
- 11 - 0 票(0%) -
- -
- -
-
- 22 - 0 票(0%) -
- -
- - - - - -
- - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/res-9.html b/outputs/verify/res-9.html deleted file mode 100644 index 3424996..0000000 --- a/outputs/verify/res-9.html +++ /dev/null @@ -1,626 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:测试

- -
- -
-
0
-
总参与人数
-
- - - - -

投票结果

-
- -
-
- 1 - 单行文本 - 测试 - 回答数: 0 -
- - -
已填写: 0
- - - -
- -
- - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/shot-home-dark.html b/outputs/verify/shot-home-dark.html deleted file mode 100644 index b915538..0000000 --- a/outputs/verify/shot-home-dark.html +++ /dev/null @@ -1,1609 +0,0 @@ - - - - - - 狼の質問箱 | 匿名質問箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - -
-

狼の質問箱 | 匿名質問箱

-

- 一个无需注册即可提问的匿名提问箱。不用下载 App,不用绑定社交账号,连邮箱都不必是真的。 -

- -
- -
-
-

匿名也能问

-

不需要账号、不需要真实邮箱,打开就能写。想收到回复再留邮箱即可。

-
-
-

不止一问一答

-

问题二维码、私聊、投票、追加提问、Markdown 排版,原版没有的这里都有。

-
-
-

箱子归你

-

头像、背景、二维码配色都能自己调,还能设置审核严格度和收集时段。

-
-
- -
-

域名迁移

-

- 本站已切换到 box.shiroko.one,原域名将在 2026 年 12 月 24 日到期, - 期间通过 301 转发指向新域名,到期后旧域名将彻底不再使用。 -

-

- 审核服务已切换到阿里云,审核速度提升且误伤更少;同时允许用户调整审核严格度,在不碰底线的前提下 - 可以设置不接受辱骂、广告等信息(即「不听坏话模式」)。 -

-
- -
-

一点提醒

-
    -
  • 本站唯一指定认证站长:小狼,注意不要被骗。
  • -
  • 本站提倡友好、友善的发言,请遵守社区规则。无论匿名与否,都要为自己的发言负责。
  • -
  • 不允许垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义、灌水提问,涉政或不良言论将直接删除。
  • -
-

- 想先试试功能,可以到站长的提问箱 - 小狼的提问箱 提问或提意见。 -

-
- -
- 近期更新 -
    -
  • 2026.9 新增审核严格度选项,增加提问、投票、私聊的总开关,可设置定时收集、定量收集或关闭功能,前后端显示也更易用了。
  • -
  • 2026.8 新增投票功能、追加提问、多语言、Markdown 显示、置顶显示等,详情见这里。
  • -
  • 2026.8 放弃使用 recaptcha 等对国内用户不友好的验证方式,更新密码加密方式,修复图片上传等一系列问题。
  • -
  • 2026.5 增加私聊功能,可连续对话,详见介绍;另有提问箱卡片分享。
  • -
  • 2026.2 新增用户展示和标签功能,可给自己贴 5 个标签并选择是否在探索页展示,详情见狼的公告板。
  • -
-
- -
-

質問箱の統計

-
-
- 28 - 注册用户 -
-
- 94 - 累计提问 -
-
- 21 - 已获回答 -
-
-

- 最后提问:2026-10-19 00:58:03
- 最后回答:2026-10-19 02:44:47
- 数据更新于:2026-10-19 12:22:53 -

-
- -
- -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - diff --git a/outputs/verify/shot-home-light.html b/outputs/verify/shot-home-light.html deleted file mode 100644 index 98b9730..0000000 --- a/outputs/verify/shot-home-light.html +++ /dev/null @@ -1,1609 +0,0 @@ - - - - - - 狼の質問箱 | 匿名質問箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - -
-

狼の質問箱 | 匿名質問箱

-

- 一个无需注册即可提问的匿名提问箱。不用下载 App,不用绑定社交账号,连邮箱都不必是真的。 -

- -
- -
-
-

匿名也能问

-

不需要账号、不需要真实邮箱,打开就能写。想收到回复再留邮箱即可。

-
-
-

不止一问一答

-

问题二维码、私聊、投票、追加提问、Markdown 排版,原版没有的这里都有。

-
-
-

箱子归你

-

头像、背景、二维码配色都能自己调,还能设置审核严格度和收集时段。

-
-
- -
-

域名迁移

-

- 本站已切换到 box.shiroko.one,原域名将在 2026 年 12 月 24 日到期, - 期间通过 301 转发指向新域名,到期后旧域名将彻底不再使用。 -

-

- 审核服务已切换到阿里云,审核速度提升且误伤更少;同时允许用户调整审核严格度,在不碰底线的前提下 - 可以设置不接受辱骂、广告等信息(即「不听坏话模式」)。 -

-
- -
-

一点提醒

-
    -
  • 本站唯一指定认证站长:小狼,注意不要被骗。
  • -
  • 本站提倡友好、友善的发言,请遵守社区规则。无论匿名与否,都要为自己的发言负责。
  • -
  • 不允许垃圾信息、广告、辱骂、涉政、暴恐、色情、违禁、无意义、灌水提问,涉政或不良言论将直接删除。
  • -
-

- 想先试试功能,可以到站长的提问箱 - 小狼的提问箱 提问或提意见。 -

-
- -
- 近期更新 -
    -
  • 2026.9 新增审核严格度选项,增加提问、投票、私聊的总开关,可设置定时收集、定量收集或关闭功能,前后端显示也更易用了。
  • -
  • 2026.8 新增投票功能、追加提问、多语言、Markdown 显示、置顶显示等,详情见这里。
  • -
  • 2026.8 放弃使用 recaptcha 等对国内用户不友好的验证方式,更新密码加密方式,修复图片上传等一系列问题。
  • -
  • 2026.5 增加私聊功能,可连续对话,详见介绍;另有提问箱卡片分享。
  • -
  • 2026.2 新增用户展示和标签功能,可给自己贴 5 个标签并选择是否在探索页展示,详情见狼的公告板。
  • -
-
- -
-

質問箱の統計

-
-
- 28 - 注册用户 -
-
- 94 - 累计提问 -
-
- 21 - 已获回答 -
-
-

- 最后提问:2026-10-19 00:58:03
- 最后回答:2026-10-19 02:44:47
- 数据更新于:2026-10-19 12:22:53 -

-
- -
- -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - diff --git a/outputs/verify/shot-res5-light.html b/outputs/verify/shot-res5-light.html deleted file mode 100644 index 1499002..0000000 --- a/outputs/verify/shot-res5-light.html +++ /dev/null @@ -1,1601 +0,0 @@ - - - - - - 投票结果 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - -
-

投票结果:11

- -
- -
-
2
-
总参与人数
-
- - - - -

投票结果

-
- -
-
- 1 - 单选 - 11 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(100%) -
- -
- -
-
- 22 - 0 票(0%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 2 - 多选 - 22 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(66%) -
- -
- -
-
- 22 - 1 票(33%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 3 - 单行文本 - 33 - 回答数: 2 -
- - -
已填写: 2
- - -
-
- ▾ - 查看文本提交(2) -
- -
- - -
- -
-
- 4 - 多行文本 - 44 - 回答数: 2 -
- - -
已填写: 2
- - -
-
- ▾ - 查看文本提交(2) -
- -
- - -
- -
-
- 5 - 下拉选择 - 55 - 回答数: 2 -
- - - - - -
-
- 11 - 2 票(100%) -
- -
- -
-
- 22 - 0 票(0%) -
- -
- -
-
- 33 - 0 票(0%) -
- -
- - - - -
- -
-
- 6 - 数字 - 66 - 回答数: 2 -
- - -
平均: 4.0(2 票)
- - -
- -
-
- 7 - 评分 - 77 - 回答数: 2 -
- - -
平均分: 9.0(2 票)
- -
- 1 分 - 0 票(0%) -
- - -
- 2 分 - 0 票(0%) -
- - -
- 3 分 - 0 票(0%) -
- - -
- 4 分 - 0 票(0%) -
- - -
- 5 分 - 0 票(0%) -
- - -
- 6 分 - 0 票(0%) -
- - -
- 7 分 - 0 票(0%) -
- - -
- 8 分 - 1 票(50%) -
- - -
- 9 分 - 0 票(0%) -
- - -
- 10 分 - 1 票(50%) -
- - - - -
- -
-
- 8 - 排序 - 88 - 回答数: 2 -
- - -
平均排名: 1.48(2 票)
- -
- 11 - 平均排名: 1.10 · 被排次数: 2 -
- -
- 22 - 平均排名: 1.25 · 被排次数: 2 -
- -
- 33 - 平均排名: 2.10 · 被排次数: 2 -
- - - -
- -
- - - - - - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/uq.html b/outputs/verify/uq.html deleted file mode 100644 index 46b97a8..0000000 --- a/outputs/verify/uq.html +++ /dev/null @@ -1,1659 +0,0 @@ - - - - - - 狼的提问箱 | 匿名提问箱 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - - - -
- - - -
-
-

人机验证

-

请计算下方的算术题以验证你不是机器人

-
- 加载中... - -
-
- -
- -
- - -
-
-
- - - - - - - - -
-
-

我的提问

- 设置 -
- - -
- - - -
- - -
- - - - - - - - - - - - - - -
-
6总提问数
-
5已回答
-
1未回答
-
5已读
-
1未读
-
5私密提问
-
0置顶
-
- -
- -
-
-
- -
-
- -
- -
-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- - -

我收到的提问

- - -
-
- -
- -
- 2026-10-19 00:58:03 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
等宽按钮自检B 005800 -
-
-
- -
-
- -
- -
- 2026-10-19 00:40:58 - - - - 待答未回答 - - - - 未读未读 - -
- -
- - -
- - -
- - -
- - -
-
-
- -
等宽按钮自检 004055 -
-
-
- -
-
- -
- -
- 2026-10-18 21:53:28 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
自检用提问 L3:今天更新吗(restrict self-check L3) -
-
-
- -
-
- -
- -
- 2026-10-18 21:48:21 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
自检用提问:今天更新吗(restrict self-check) -
-
-
- -
-
- -
- -
- 2026-10-18 21:39:41 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
啊吧啊吧 -
-
-
- -
-
- -
- -
- 2026-08-20 16:01:14 - - - 私密私密 - - - - - 已读已读 - -
- -
- - -
- - -
-
-
- -
测试! -
-
-
- - - -
- - - - - - -
- - - -

-
- 社区规则 | - - 找站长 -
- 盘 - | 公告板 - | 聊天室 - | 监控板 - | 导航页 -
Powered by NekoBox | 狼ICP备 20230101 -
提问箱已成功存活: -

- - -
-
- - - - - diff --git a/outputs/verify/v-7kcP5kR8YnJC.html b/outputs/verify/v-7kcP5kR8YnJC.html deleted file mode 100644 index 4e337fa..0000000 --- a/outputs/verify/v-7kcP5kR8YnJC.html +++ /dev/null @@ -1,885 +0,0 @@ - - - - - - 测试 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - -
- admin6 -
の投票
-
- -
- - - 進行中 - -
-
- -

测试

- -

测试

- -
- - - - - - - - - -
匿名での参加が可能で、あなたの身元は記録されません(同一端末で最大1回)。
- - -
- - - -
- - -
-
- 1 - 一行テキスト -
测试 *
-
- - - -
- - - - -
- -
- - - -
-
- - - - - - - - - -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/v-9wfpVjXi73mc.html b/outputs/verify/v-9wfpVjXi73mc.html deleted file mode 100644 index fc216b3..0000000 --- a/outputs/verify/v-9wfpVjXi73mc.html +++ /dev/null @@ -1,946 +0,0 @@ - - - - - - 测试功能 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - -
- admin6 -
の投票
-
- -
- - - 進行中 - -
-
- -

测试功能

- -
- - - - - - - - - -
匿名での参加が可能で、あなたの身元は記録されません(同一端末で最大1回)。
- - -
- - - -
- - -
-
- 1 - 単一選択 -
必填 *
-
- - - - - - - - - - -
- -
-
- 2 - 複数選択 -
非必填
-
- - - - - - - - - - - - -
- -
-
- 3 - 一行テキスト -
文本1
-
- - - -
- -
-
- 4 - 複数行テキスト -
文本2 *
-
- - - -
- - - - -
- -
- - - -
-
- - - - - - - - - -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/v-C84m7PoENWsG.html b/outputs/verify/v-C84m7PoENWsG.html deleted file mode 100644 index 7c15a15..0000000 --- a/outputs/verify/v-C84m7PoENWsG.html +++ /dev/null @@ -1,883 +0,0 @@ - - - - - - 测试5 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - -
- admin6 -
の投票
-
- -
- - - 進行中 - -
-
- -

测试5

- -

测试一下flash

- -
- - - - - - - - - -
匿名での参加が可能で、あなたの身元は記録されません(同一端末で最大1回)。
- - -
- - - -
- - - - - - - - - - -
- -
- - - -
-
- - - - - - - - - -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/v-GJkdls4fH78y.html b/outputs/verify/v-GJkdls4fH78y.html deleted file mode 100644 index bbef153..0000000 --- a/outputs/verify/v-GJkdls4fH78y.html +++ /dev/null @@ -1,901 +0,0 @@ - - - - - - 明天吃什么 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - -
- admin6 -
の投票
-
- -
- - - 進行中 - -
-
- -

明天吃什么

- -
- - - - - - - - - - -
- この投票は登録ユーザーのみ参加できます。ログインしてください。 ログイン -
- - - -
- - - -
- - - - - - - -
- -
- - - -
-
- - - - - - - - - -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/v-t86urUZo26xE.html b/outputs/verify/v-t86urUZo26xE.html deleted file mode 100644 index 45c2465..0000000 --- a/outputs/verify/v-t86urUZo26xE.html +++ /dev/null @@ -1,887 +0,0 @@ - - - - - - 吃什么 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - -
- admin6 -
の投票
-
- -
- - - 進行中 - -
-
- -

吃什么

- -
- - - - - - - - - - -
- この投票は登録ユーザーのみ参加できます。ログインしてください。 ログイン -
- - - -
- - - -
- - -
-
- 1 - 一行テキスト -
是啊,吃什么?
-
- - - -
- - - - -
- -
- - - -
-
- - - - - - - - - -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/v-urSsfenvRomb.html b/outputs/verify/v-urSsfenvRomb.html deleted file mode 100644 index 449bab3..0000000 --- a/outputs/verify/v-urSsfenvRomb.html +++ /dev/null @@ -1,1039 +0,0 @@ - - - - - - 11 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - -
- admin6 -
の投票
-
- -
- - - 進行中 - -
-
- -

11

- -
- - - - - - - - - -
匿名での参加が可能で、あなたの身元は記録されません(同一端末で最大1回)。
- - -
- - - -
- - -
-
- 1 - 単一選択 -
11
-
- - - - - - - - - - -
- -
-
- 2 - 複数選択 -
22 *
-
- - - - - - - - - - -
- -
-
- 3 - 一行テキスト -
33 *
-
- - - -
- -
-
- 4 - 複数行テキスト -
44 *
-
- - - -
- -
-
- 5 - ドロップダウン選択 -
55 *
-
- - - - -
- -
-
- 6 - 数値 -
66 *
-
- - - -
- -
-
- 7 - 評価 -
77 *
-
- - - -
- -
-
- 8 - 順位 -
88 *
-
- -
- 順位 1-N(必須) -
- - -
- 11 - -
- -
- 22 - -
- -
- 33 - -
- - -
- - - - -
- -
- - - -
-
- - - - - - - - - -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/v-z9F8mIqPtDOA.html b/outputs/verify/v-z9F8mIqPtDOA.html deleted file mode 100644 index a7bc1b9..0000000 --- a/outputs/verify/v-z9F8mIqPtDOA.html +++ /dev/null @@ -1,924 +0,0 @@ - - - - - - 测试4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - -
- - - -
-
-

人機認証

-

以下の計算問題を解いて、ロボットでないことを証明してください

-
- 読み込み中... - -
-
- -
- -
- - -
-
-
- - - - - - - - - - - - - - - - - - -
-
- - -
- admin6 -
の投票
-
- -
- - - 進行中 - -
-
- -

测试4

- -
- - - - - - - - - -
匿名での参加が可能で、あなたの身元は記録されません(同一端末で最大1回)。
- - -
- - - -
- - -
-
- 1 - 単一選択 -
这里显示第一题
-
- - - - - - - - - - - - - - -
- -
-
- 2 - 単一選択 -
11
-
- - - - - - - - -
- - - - -
- -
- - - -
-
- - - - - - - - - -

-
- ルール | - - 管理者へ -
- ドライブ - | お知らせ - | チャット - | ステータス - | リンク -
Powered by NekoBox | 狼ICP备 20230101 -
質問箱は稼働中: -

- - -
-
- - - - - diff --git a/outputs/verify/ver.json b/outputs/verify/ver.json deleted file mode 100644 index f1acdca..0000000 --- a/outputs/verify/ver.json +++ /dev/null @@ -1 +0,0 @@ -{"code":0,"correct":true} diff --git a/outputs/verify/vl.html b/outputs/verify/vl.html deleted file mode 100644 index 4ed799b..0000000 --- a/outputs/verify/vl.html +++ /dev/null @@ -1,2 +0,0 @@ -Found. - diff --git a/outputs/vote-i18n-audit-2026-09-01.md b/outputs/vote-i18n-audit-2026-09-01.md deleted file mode 100644 index 76c9a4d..0000000 --- a/outputs/vote-i18n-audit-2026-09-01.md +++ /dev/null @@ -1,53 +0,0 @@ -# 投票页中文提示 Bug 排查 + 同类 i18n 缺口审计(2026-09-01) - -## 一、结论先行 -- **你报告的 `投票成功!感谢参与` 确属 Bug**:`route/vote/vote.go` 的 `Submit` 之前把成功提示硬编码成了中文,未走 i18n,所以无论访客界面语言设成什么,弹出的都是中文。 -- **已修复**:投票页全部用户提示 + 验证码包(共享根因,影响 6 个页面)。 -- **还有大量同类问题**:route 层另有 40+ 处硬编码中文 flash(含登录页直接把 db 中文错误弹给用户),以及 db 层的中文错误哨兵,属于系统性 i18n 缺口,建议作为下一批次修复(见第三节)。 - -## 二、本次已修复(可直接验收) -### 1. `route/vote/vote.go`(投票参与页,访客可见) -- `SetSuccessFlash("投票成功!感谢参与")` → `i18n.T(lang, "vote.flash.success")`。 -- `Page` / `Submit` 的全部 `SetErrorFlash("中文")` 改为对应 i18n 键(`vote.flash.notFound/notPublished/archived/ended/notStarted`、`vote.page.loginRequired/alreadyVoted`)。 -- 表单解析 `parseSubmission` 内约 25 条校验错误(单选/多选/评分/排名/表单字段必填、超长、自定义选项、序列化失败等)全部改为 `i18n.T(lang, ...)`,动态字段名以 `%s` 占位注入(标签本身是箱主自定的内容,不翻译)。 -- `censorCheck` 签名改为 `(ctx, lang, text)`,失败文案走 `censor.unavailable` 键。 -- 新增 i18n 键:`vote.err.fieldRequired/fieldCustomRequired/fieldCustomTooLong/fieldInvalidOption/fieldConfigError/fieldRankIncomplete/fieldRankRange/fieldRankDuplicate/fieldTooLong/fieldNotNumber/fieldRating/customTooLong/serialize/unknownType/invalidOption/optionDuplicate`(四语言已补齐)。 - -### 2. `internal/captcha/math.go`(验证码,共享根因) -- `Verify` / `VerifyMath` 增加 `lang` 参数,6 处硬编码中文错误(不能为空/格式错误/答案格式/已过期/服务异常/答案错误/验证码错误)改为 i18n 键。 -- 新增 i18n 键:`captcha.errEmpty/errFormat/errAnswerFormat/errExpired/errService/errAnswerWrong`(四语言已补齐)。 -- 副作用:登录、注册、找回密码、提问箱页的验证码错误现在也跟随界面语言(这 6 个调用点已同步传 `ctx.Lang()` 并把 `if !ok` 分支的硬编码 `验证码错误` 改为 `captcha.error` 键)。 -- 配套:`internal/context/context.go` 新增 `(*Context).Lang()` 方法(返回中间件注入的界面语言,未注入回退 `i18n.Default`),供后端 flash/错误本地化复用。 - -**验证**:gofmt 通过;`go build -overlay`(含 webp 屏障包)全绿;`go vet` 全绿;全量可运行测试(templates / i18n / vote / auth / question 等)通过。 - -## 三、尚未修复的同类问题(建议下一批次) -判定标准:用户可见的 flash / 错误信息硬编码中文、不跟随界面语言。按「访客可见 / 箱主后台」分级。 - -### P0 — 访客/公开页(与投票页同类,匿名用户可见,优先级最高) -| 文件:行 | 当前硬编码 | 建议键 | -|---|---|---| -| route/auth/login.go:49 | `errors.Cause(err).Error()` → `ErrBadCredential` "邮箱或密码错误" | `auth.errBadCredential` | -| route/auth/recover.go:124 | `用户不存在` | `auth.errUserNotFound` | -| route/auth/recover.go:70 / 86 / 105 / 169 | 邮件频繁/失败/验证码过期/密码修改成功 | `auth.*` | -| route/question/page.go:232 | `密码错误` | `question.errWrongPassword` | -| route/question/page.go:313 | `仅注册用户才能提问…` | `question.errLoginRequired` | -| route/question/page.go:322 / 345 | `不能给自己发起私聊` / `你已被对方拉黑…` | `question.*` | -| route/question/page.go:384 | `检测到重复提交,请稍后再试` | `question.errDuplicate` | -| route/question/follow_up.go:38/45/75/90/149/161/180 | 无权操作/未回答不能追问/等上一条/审核不可用/追问不存在/回答成功 等 | `question.*` | -| route/question/question.go:215/281/299/313/325 | 回答发布成功/操作成功/已读/未读 等 | `question.*` | -| internal/db/users.go:206-208 | `ErrBadCredential`/`ErrDuplicateEmail`/`ErrDuplicateDomain` 中文哨兵 | 改为键或 handler 层映射 | - -### P1 — 箱主后台(登录态可见,影响较小) -route/user/vote.go、profile.go、password.go、tags.go、harassment.go 等约 25 处硬编码中文 flash -(如 `投票已发布!分享链接:…`、`更新标签成功`、`页面访问密码已更新`、`私聊设置更新成功` 等)。 - -### P2 — 内部 API(JS 消费的 JSON,非 flash) -internal/captcha/math.go `VerifyHandler` 返回的 `请求格式错误` / `验证码 ID 不能为空`(JSON message,前端弹通用提示,影响低)。 - -## 四、建议的下一步 -1. **先修 P0(公开页)**:与投票页同类,直接决定匿名访客的语言体验,工作量约 8 个文件 + 约 30 个 i18n 键。 -2. 再修 P1(箱主后台)+ P2(可选)。 -3. db 层中文错误哨兵建议统一改为「哨兵错误 + handler 层按 `errors.Is` 映射 i18n」,避免翻译逻辑下沉到 db 包。 - -请确认是否按上述批次继续修复(特别是 P0 公开页),我可立即开工。 diff --git a/outputs/webp-compression-review-2026-10-15.md b/outputs/webp-compression-review-2026-10-15.md deleted file mode 100644 index a7941be..0000000 --- a/outputs/webp-compression-review-2026-10-15.md +++ /dev/null @@ -1,186 +0,0 @@ -# WebP 压缩链路评审(2026-10-15) - -范围:`internal/storage/s3.go`(上传管线)、`route/question/page.go`(提问/回答图片)、 -依赖 `github.com/chai2010/webp v1.4.0` + `disintegration/imaging v1.6.2`。 - -结论:**提升空间很大,且主要不在「调 quality」上,而在「按用途分档 + 修方向 + 兜底回退 + 编码器能力上限」四件事。** -最肥的一块:头像服务端不缩到实际展示尺寸,实测多传 ~7 倍字节。 - ---- - -## 一、现状事实(已核对代码) - -| 位置 | 现状 | -|---|---| -| `s3.go:186-195` | `maxWidth=1920` 对**所有**用途统一生效;`webp.Options{Quality: 80}`,无 `Lossless`、无 `Exact` | -| `s3.go:113-133` | GIF 走 `cwebp -q 80 -mixed -mt`;静态走 `ConvertStaticImageToWebP`;失败回退原图 | -| `s3.go:200-203` | `cwebp` 不在 PATH 时静默回退原始 GIF(无日志) | -| `route/question/page.go:713-734` | GIF **完全不转换**,直接原样上传 —— 与 `s3.go` 行为不一致 | -| `route/question/page.go:740-748` | 该分支 PutObject **未设 CacheControl**(`s3.go` 有 immutable) | -| `route/question/page.go:721` | 提问图片与头像共用同一函数,同样 1920/80 | - -`chai2010/webp` 的 `Options` 只有三个字段: - -```go -type Options struct { Lossless bool; Quality float32; Exact bool } -``` - -即 **method(0-6)、sns_strength、filter_strength、alpha_quality、near_lossless 全部不可控**。 -C 层是裸 `WebPEncodeRGB` / `WebPEncodeRGBA`(`internal/src/webp.c:194-215`),等价于 cwebp 默认档 -(method=4)。 - ---- - -## 二、体积实测(本机 Pillow 绑定同一份libwebp 1.4.0,样本取自 outputs/ 真实截图) - -### 2.1 method 的影响(q80 固定) - -| 样本 | 原 PNG | m4 q80(=现状) | m6 q80 | m6 q75 | m6 q85 | -|---|---|---|---|---|---| -| tb-prod-test-qr.png | 343,674 | 31,856 | **29,916** (-6.1%) | 24,274 (-23.8%) | 37,448 | -| prod-tamakyi-snrat-check.png | 635,502 | 151,888 | **147,498** (-2.9%) | 126,766 (-16.5%) | 172,434 | -| admin6-cdn-check.png | 375,873 | 51,202 | **48,664** (-5.0%) | 41,122 (-19.7%) | 59,222 | - -→ **method=6 平均再省 3~6%**,纯 CPU 换体积,无画质损失。 - -### 2.2 按用途分档的收益(当前完全没分档) - -模拟一张典型手机照片: - -| 用途 | 现状(1920/q80/m4) | 建议档位 | 建议档实测 | 节省 | -|---|---|---|---|---| -| **头像**(前端 cropper 输出512) | 83,366 | 512 / q78 / m6 | 11,390 | **-86%** | -| 头像(列表缩略) | — | 320 / q78 | 4,966 | -94% | -| 背景16:9 | 26,222 | 1920 / q80 | 26,222 | — | -| 正文配图 | — | 1600 / q80 | — | -20~25% | - -注意:前端 `profile.html:1390` 头像 `maxWidth:512`,但**服务端把 1920 传给编码器**, -意味着直传(绕过前端 cropper)的大图会被以 1920 全尺寸存下,而头像展示最大也就 256px。 - -### 2.3 有损 vs 无损(透明图) - -| 类型 | 体积 | -|---|---| -| 带 alpha 图 lossy q80 | 49,636 | -| 带 alpha 图 lossless | 253,128 | - -→ 无脑lossless 是灾难(5x),**不要**走这条路。但存在反向问题: - -### 2.4 「编码后不一定更小」 - -对已经压得很狠的小 PNG/图标,WebP 有损可能**比原图还大**(WebP 对纯色/线框图有损效率差)。 -**当前代码没有任何 `len(webpData) > len(fileData)` 的兜底判断** —— 转换失败会回退,但转换「成功却更大」不会回退。 - -### 2.5 重复编码(代际损失) - -已是 WebP 的图再走一次管线 → 二次有损。实测 160,896 → 139,866(体积反而小了 13%, -因为 q82→q80),**画质白白损失一代,体积收益不确定**。 - ---- - -## 三、问题清单 - -### P0 🔴 EXIF 方向未处理 —— 手机竖拍 JPEG 会横躺 - -`image.Decode`(`s3.go:99`、`page.go:707`)**不读 EXIF Orientation**。 -前端 cropper 走 canvas 会正确应用方向,但 `templates/question/item.html:206,238` 的 -`accept="image/*"` 是**原始文件直传**,没有 canvas 归一化 → 手机竖拍照片转 WebP 后躺倒。 - -这不是压缩问题,但和转换管线同源,且用户可见。修EXIF 时顺带可在转码前 -`image.NewRGBA` 重绘归一,比让 libwebp 处理更可控。 - -### P1 🟠 按用途分档缺失(最大体积收益) - -`s3.go:186` 的 `maxWidth=1920` + `Quality:80` 一刀切。已有 `aspect` 参数做比例分档的先例 -(`UploadAvatarToS3` / `UploadBackgroundToS3`),扩展成 `{maxW, maxH, quality}` 成本极低。 -预期头像场景存储与流量降 **~85%**。 - -### P1 🟠 method 不可控,白丢 3~6% - -`chai2010/webp` 不暴露 method。三条路: -- (a) `cwebp -m 6`(GIF 分支已经在用 `cwebp`,静态图也可统一走 CLI,还能顺带拿到 - `-alpha_q`/`-sns`/`-near_lossless`/`-pass`)—— 需保证 Dockerfile 装 webp 包(**待核**); -- (b) 换 `gen2brain/go-webp`,API 直接暴露完整 `WebPConfig`; -- (c) 维持现状。 - -⚠️ method=6 编码耗时约为 method=4 的 **5~10 倍**。1C1G 生产机上批量上传场景 -需实测编码耗时再定,不能只看体积。 - -### P1 🟠 无「转换后更大」的兜底 - -`s3.go:125-132`、`page.go:721-733` 都缺`if len(webp) >= len(orig) { 用原图 }`。 -小图标/线框图/已优化 JPEG 场景会越转越大。 - -### P1 🟠 已是 WebP 还再编一代 - -`format == "webp"` 走 default 分支二次有损。建议:已是 WebP 且宽度合规 → 直接透传, -只在超宽时才 resize 后重编。 - -### P2 🟠 GIF 转换在生产上从未生效(已实证) - -`Dockerfile:16` 是 Alpine `apk add tzdata`,**未安装 `webp` 包** → 生产容器内没有 `cwebp`。 -`s3.go:201` 的 `exec.LookPath("cwebp")` 必然失败 → GIF 全部走 `uploadData = fileData` 原图上传, -且**无任何日志**。即`s3.go:115` 的动画 WebP 分支是死代码,线上动画图一直是原 GIF。 - -(另外 `page.go:714-718` GIF 不转,与 `s3.go` 不一致;但因为上述原因,两条路实际殊途同归。) - -若要启用:Dockerfile 加 `RUN apk add --no-cache webp`(Alpine 包名即 `webp`,含 `cwebp`/`img2webp`/`gif2webp`), -并在 `LookPath` 失败时补 Warn。 - -### P2 🟠 GIF 分支其余问题 - -`s3.go:223` `cwebp -q 80 -mixed -mt` 缺 `-m 6`、`-lossless`、`-metadata none`, -也缺 GIF 帧数/总像素上限(`imagecheck.CheckPixelLimit` 只看逻辑屏幕宽高, -500 帧 2000×2000 的 GIF 不会被拦)。 - -### P2 走 cwebp CLI 需先改 Dockerfile - -若采纳批次 B 的 method=6 方案 (a)(静态图统一走 CLI),必须先在 Dockerfile 装 webp 包, -否则会把「能用但慢」变成「不能用的静默回退」。 - -### P2 🟠 峰值内存 - -`s3.go:95-99` 解码一次拿format,`s3.go:181` 又完整解码一次,**同一张图解码两遍**; -`imaging.Resize` 再全量分配。4096×4096 上限下 RGBA 峰值约 64MB/张, -并发上传时 1C1G 有 OOM 风险。可合并为「一次解码 → 校验 → resize → 编码」。 - -### P2 提问图片分支缺 CacheControl - -`page.go:740-748` 未设 `CacheControl`,与 `s3.go:158` 不一致(后者 immutable 一年)。 - -### P2 maxWidth 只限宽不限高 - -`s3.go:187` 只判 `Dx() > 1920`。竖图 1080×10000(像素总量在4096² 内?1080*10000 -= 10.8M < 16.7M,通过)→ 不缩放,长边 10000px 下发,首屏极慢。 - ---- - -## 四、建议的落地方案(待批准,未动代码) - -### 批次 A(低风险,先做) -1. `ConvertStaticImageToWebP` 增加参数:`maxWidth int, quality float32`; - `uploadPictureToS3` 按 `aspect` 传入不同档位(头像 512/q78、背景 1920/q80、通用 1600/q80)。 -2. 加 `len(webp) >= len(fileData) && format != "gif"` 回退原图。 -3. `format == "webp"` 且尺寸合规 → 直接透传,不重编。 -4. maxWidth 改为同时约束长边(`maxSide`)。 -5. `page.go` PutObject 补 `CacheControl`。 -6. GIF 分支加`cwebp` 缺失时的 Warn 日志。 - -### 批次 B(需实测/依赖改动) -7. method=6:先在 dev 环境用真实图测编码耗时,确认 1C1G 可接受再上; - 路线倾向 (a) 统一走 `cwebp`(Dockerfile 需确认 webp 包,见下)。 -8. EXIF 方向归一(转码前重绘)。 -9. 统一 GIF 双实现。 -10. 合并重复解码降内存。 - -### 待核实(已全部核实完毕) -- ✅ `Dockerfile` 用 Alpine apk,**未装 `webp`** → `cwebp` 线上不存在,GIF 转换是死代码。 - ---- - -## 五、当前无法验证的部分 - -- 本机无 libwebp/GCC,**未编译运行 Go 侧实测**。体积数据来自 Pillow 绑定同一份 - libwebp 1.4.0、同样 quality/method 组合,作为体积基线可信;但 - **`imaging.Resize` 的 Lanczos 与 Pillow LANCZOS 的输出差异未纳入实测**,实际体积会有小幅出入。 -- method=6 的**编码耗时**未实测(这是决定是否采用的关键约束)。 \ No newline at end of file