Kivy App开发之SQLite:用TaoToken统一Key打通本地数据层调试链路
1. Kivy 里 SQLite 数据层为什么总在真机翻车Kivy App 开发里SQLite 是最省心的本地存储方案一个.db文件Python 标准库sqlite3直接读写不用装服务、不用配账号。它适合谁适合做离线优先的移动端应用——记账、打卡、笔记、设备配置缓存这类场景数据量不大但要求随时可读可写。核心检索词就一句话Kivy App 开发之 SQLite本质是把 Python 的 sqlite3 嵌进 Kivy 的跨平台运行环境里。但真正上手你会发现桌面端跑得好好的代码打包到 Android 就报unable to open database file。原因不复杂桌面端你的工作目录是工程根目录db/hello.db找得到打包成 APK 后应用被解压到私有目录当前工作目录变了相对路径直接失效。更麻烦的是Android 上应用资源目录是只读的你不能往 APK 内部写数据库必须写到App.user_data_dir这种可写路径。我试过在三个平台反复调试数据层最耗时间的不是 SQL 本身而是路径和连接管理。每次改完表结构要手动删库重建事务写错一半数据脏了还得重装 App。所以这篇不讲空泛概念直接给你能复制的 KV Python 片段、迁移脚本以及三条验证动作建表回读、事务回滚、打包后路径打印。同时把调试期要调用的模型接口用 TaoToken 统一 Key 管起来避免在多个工具之间来回切配置。先说清楚 SQLite 在 Kivy 里的定位。它不是网络数据库没有并发连接池的概念一个连接就是一个文件句柄。Python 内置 sqlite3 驱动connect()得到 connectioncursor()得到游标execute()执行 SQLfetchall()拿结果。这套 API 在任何平台都一样差异只在文件路径和权限。所以数据层调试的核心就是把这四步封装成可复用函数再把路径问题一次性解决掉。下面按真实开发顺序走先解决路径再写增删改查再做迁移最后验证。每一步都给完整代码你可以直接贴进自己的 Kivy 工程。2. TaoToken 前置统一 Key 管理调试期接口调用Kivy App 的数据层调试往往不只是本地 SQLite。你可能在同步阶段调用模型接口做文本处理或者在 Agent 场景里让模型帮忙生成 SQL、解析用户输入。这时候就会遇到一个现实问题Claude Code、Cline、Codex 各自要配一套 Base URL 和 Key切来切去容易配错调试日志也乱。TaoToken 在这里的作用是提供一个统一的 API 入口把调试期用到的模型调用收敛到一套 Key 上。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 。注意 API 地址不带 UTM 参数配置时直接用这个。为什么数据层调试需要它举个具体场景你在 Kivy 里写了一个sync_notes()函数要把本地 SQLite 里的笔记批量送去模型做摘要再写回数据库。调试这个函数时你需要反复触发接口调用如果每次都要改不同工具的配置效率极低。用 TaoToken 统一 Key 后你的 Python 代码里只需要维护一个base_url和一个api_key所有调试请求走同一个入口。配置方式很简单在工程里建一个config.py把调试期配置集中管理# config.py import os # TaoToken 统一入口调试期所有模型调用走这里 TAOTOKEN_BASE_URL https://taotoken.net/api TAOTOKEN_API_KEY os.environ.get(TAOTOKEN_API_KEY, sk-your-debug-key) # 模型 ID按你实际使用的填 DEBUG_MODEL_ID claude-sonnet-4-20250514 # SQLite 数据库文件名 DB_NAME hello.db然后在数据层代码里引用它。这样做的另一个好处是打包发布时你只需要把TAOTOKEN_API_KEY换成正式 Key代码逻辑一行不用动。如果你用的是 Claude Code 做辅助开发可以在它的配置里把 Base URL 指向 TaoToken 的 API 地址Key 用同一个模型 ID 填你需要的那个。三件套就是 Base URL Key Model ID缺一不可。需要拿 Key 的话去控制台创建https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。API Keys 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。如果你只是想在调试时快速验证模型返回可以用模型对话页面https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。长期做编码和 Agent 开发建议看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。这里要强调一点TaoToken 是正常的 API 服务入口不是让你绕过什么限制。它的价值在于把调试期的接口调用统一管理减少配置切换成本。数据层调试本身还是靠 SQLite 和 Kivy 的路径处理两者不要混为一谈。3. 可复制配置KV Python 数据层完整片段这一节给你可以直接复制的代码。先解决路径再封装数据库操作最后给 KV 布局。所有片段都经过桌面端和 Android 打包验证。3.1 路径处理桌面端与 Android 统一关键是用App.user_data_dir作为可写目录桌面端它会落到用户目录下Android 上落到应用私有目录。数据库文件放这里打包后不会丢。# db_helper.py import os import sqlite3 import logging from kivy.app import App logging.basicConfig(levellogging.INFO) def get_db_path(): 返回可写数据库路径桌面端和 Android 通用 app App.get_running_app() if app is None: # 非 Kivy 环境比如单独跑脚本测试 base_dir os.path.dirname(os.path.abspath(__file__)) data_dir os.path.join(base_dir, data) else: data_dir app.user_data_dir os.makedirs(data_dir, exist_okTrue) return os.path.join(data_dir, hello.db)注意App.get_running_app()在 Kivy 启动前返回 None所以单独跑测试脚本时走__file__分支。这样你可以在不启动 Kivy 的情况下先验证 SQL 逻辑。3.2 连接与增删改查封装把 connection 和 cursor 的生命周期管好避免连接泄漏。下面这个类封装了插入、查询、更新、删除以及事务回滚。# db_helper.py 续 class DBHelper: def __init__(self, db_pathNone): self.db_path db_path or get_db_path() def _connect(self): conn sqlite3.connect(self.db_path) conn.row_factory sqlite3.Row # 让查询结果支持按列名访问 return conn def execute(self, sql, params()): 执行写操作返回 lastrowid conn self._connect() try: cursor conn.cursor() cursor.execute(sql, params) conn.commit() return cursor.lastrowid except Exception as e: conn.rollback() logging.warning(execute failed: %s, e) raise finally: conn.close() def query(self, sql, params()): 执行读操作返回 dict 列表 conn self._connect() try: cursor conn.cursor() cursor.execute(sql, params) rows cursor.fetchall() return [dict(row) for row in rows] except Exception as e: logging.warning(query failed: %s, e) return [] finally: conn.close() def execute_many(self, sql, seq_of_params): 批量写用于迁移或导入 conn self._connect() try: cursor conn.cursor() cursor.executemany(sql, seq_of_params) conn.commit() return cursor.rowcount except Exception as e: conn.rollback() logging.warning(execute_many failed: %s, e) raise finally: conn.close()3.3 建表与迁移脚本迁移的核心是版本号。用PRAGMA user_version记录当前 schema 版本启动时对比目标版本逐级升级。这样你改表结构不用删库。# migrations.py from db_helper import DBHelper SCHEMA_VERSION 2 MIGRATIONS { 1: [ CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ], 2: [ ALTER TABLE notes ADD COLUMN tag TEXT DEFAULT , CREATE INDEX IF NOT EXISTS idx_notes_tag ON notes(tag) ] } def get_user_version(db): rows db.query(PRAGMA user_version) return rows[0][user_version] if rows else 0 def set_user_version(db, version): db.execute(fPRAGMA user_version {version}) def migrate(dbNone): db db or DBHelper() current get_user_version(db) if current SCHEMA_VERSION: return current for version in range(current 1, SCHEMA_VERSION 1): for sql in MIGRATIONS.get(version, []): db.execute(sql) set_user_version(db, version) return SCHEMA_VERSION注意PRAGMA user_version不能用参数占位符只能字符串拼接但版本号是代码里写死的整数不存在注入风险。3.4 KV 布局片段KV 文件负责界面Python 负责数据。下面是一个最小可用的笔记列表界面。# notes.kv NoteItemBoxLayout: orientation: vertical size_hint_y: None height: 80dp padding: 8dp Label: text: root.title font_size: 16sp halign: left text_size: self.width, None Label: text: root.tag font_size: 12sp color: 0.5, 0.5, 0.5, 1 NotesScreen: BoxLayout: orientation: vertical BoxLayout: size_hint_y: None height: 48dp TextInput: id: title_input hint_text: 标题 Button: text: 添加 size_hint_x: None width: 80dp on_release: root.add_note(title_input.text) RecycleView: id: rv viewclass: NoteItem RecycleBoxLayout: default_size: None, dp(80) default_size_hint: 1, None size_hint_y: None height: self.minimum_height orientation: vertical对应的 Python 里add_note调用DBHelper.execute插入然后刷新 RecycleView 的 data。这样界面和数据层解耦调试时你可以单独测数据层不用启动界面。4. 验证请求三条动作确认数据层正常写完代码不算完必须验证。下面三条动作覆盖了建表、事务、打包路径三个最容易出问题的点。4.1 建表回读启动时先迁移再插入一条测试数据读回来对比。# verify.py from db_helper import DBHelper from migrations import migrate def verify_create_and_read(): db DBHelper() migrate(db) note_id db.execute( INSERT INTO notes (title, content, tag) VALUES (?, ?, ?), (测试标题, 测试内容, debug) ) rows db.query(SELECT * FROM notes WHERE id ?, (note_id,)) assert len(rows) 1, 回读失败 assert rows[0][title] 测试标题, 字段不匹配 print(建表回读通过id , note_id) return note_id if __name__ __main__: verify_create_and_read()单独跑这个脚本不启动 Kivy确认 SQL 逻辑没问题。输出建表回读通过就说明建表、插入、查询链路通了。4.2 事务回滚故意在事务中间抛异常确认数据没写进去。def verify_rollback(): db DBHelper() before db.query(SELECT COUNT(*) AS c FROM notes)[0][c] try: conn db._connect() cursor conn.cursor() cursor.execute(INSERT INTO notes (title) VALUES (?), (回滚测试,)) raise RuntimeError(模拟失败) conn.commit() except RuntimeError: conn.rollback() finally: conn.close() after db.query(SELECT COUNT(*) AS c FROM notes)[0][c] assert before after, f回滚失败: {before} - {after} print(事务回滚通过记录数保持, after)这条验证很关键。Kivy 里数据写入往往在按钮回调里如果回调中途出错没有 rollback 就会留下半截数据。跑通这条说明你的异常处理是有效的。4.3 打包后路径打印打包成 APK 后第一件事是打印数据库实际路径确认它落在可写目录。def verify_path(): from db_helper import get_db_path path get_db_path() print(数据库路径:, path) import os print(目录可写:, os.access(os.path.dirname(path), os.W_OK)) print(文件存在:, os.path.exists(path))在 Kivy 的on_start里调用它用adb logcat看输出。Android 上路径应该类似/data/user/0/org.example.notes/files/hello.db。如果打印出来是 APK 内部路径或者报权限错误说明你还在用相对路径回去改get_db_path。三条动作跑完数据层基本可信。接下来是排错。5. 本篇常见错排查401、路径、事务、OAuth调试期报错集中在几类逐个对照。401 Unauthorized如果你在数据层调试时调用了模型接口返回 401 通常是 Key 没配或配错。检查config.py里的TAOTOKEN_API_KEY是否从环境变量正确读取以及 Base URL 是否写成了https://taotoken.net/api。注意 API 地址不带 UTM 参数带参数的地址是给浏览器访问的。用 Claude Code 时如果报 OAuth 相关错误检查它的配置文件里 Base URL 和 Key 是否成对出现Model ID 是否填了有效值。三件套缺一个都会失败。local proxy failed / connection refused这类错误说明请求根本没发出去。先确认网络能通再确认 Base URL 没有多余斜杠或路径。如果你在 Kivy 里用requests调接口Android 上需要网络权限检查buildozer.spec里android.permissions是否包含INTERNET。reading choices 报错这通常出现在解析模型返回时。返回结构和你预期的不一致比如你按 OpenAI 格式解析但实际返回是 Anthropic 格式。解决办法是先打印原始 response确认结构再写解析。调试期建议把原始返回存到 SQLite 里方便回溯。unable to open database file路径问题。桌面端能跑、打包后报错就是没用App.user_data_dir。另外确认os.makedirs(data_dir, exist_okTrue)在连接前执行了。Android 上应用私有目录不需要额外权限但你不能往 APK 资源目录写。database is lockedSQLite 同一时间只允许一个写连接。如果你在多个线程里同时写会锁。Kivy 的主线程和回调线程要注意。简单做法是所有写操作走同一个队列或者用timeout参数让连接等待sqlite3.connect(path, timeout10)。no such table迁移没跑或者跑迁移时用的数据库路径和查询时不一致。打印两次的db_path对比。常见于你在测试脚本里用了__file__分支在 App 里用了user_data_dir分支两个路径不同。事务没生效检查是否调用了conn.commit()。Python sqlite3 默认不会自动提交execute后必须 commit。另外with conn:上下文管理器会自动提交但很多人忘了用。打包后数据丢失每次重装 APKuser_data_dir会被清空。这是正常行为。如果你需要保留数据得做导出功能或者用外部存储。调试期不用管正式版要考虑。Claude Code 配置不生效检查配置文件路径是否正确以及是否重启了工具。有些工具读配置只在启动时。Base URL、Key、Model ID 三件套写全缺一个都会回退到默认配置。对照这些报错基本能覆盖 90% 的数据层调试问题。剩下的靠日志把logging.warning换成logging.exception能看到完整堆栈。6. 语义一致 CTA把调试链路收拢到一处数据层调通之后你会发现调试期真正的时间消耗在配置切换上。SQLite 本身很稳路径和事务处理一次就够但模型接口的 Key、Base URL、Model ID 如果散落在多个工具里每次改都要重新对齐。我的做法是把调试期所有接口调用收拢到 TaoToken 一套 Key 上。Python 代码里只维护config.py一个配置源Claude Code 和 Cline 也指向同一个 Base URL。这样你在 Kivy 里调sync_notes()时不用关心底层走的是哪个工具日志也集中。具体入口按用途分排障和接入看 API Keys 和文档地址分别是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 和 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。验证模型返回用模型对话https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。长期做编码和 Agent 开发看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。控制台在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。最后给一个实用技巧把verify.py里的三条验证动作做成一个debug_menu在 Kivy 界面里加一个隐藏按钮长按触发。这样真机调试时不用连电脑看日志直接在手机上跑验证结果弹窗显示。路径打印、回滚测试、建表回读一键完成比翻 logcat 快得多。数据层稳了上层功能才敢往上堆。

相关新闻

Ubuntu 18.04上vcpkg安装报错:CMake版本过旧与工具链升级全记录

Ubuntu 18.04上vcpkg安装报错:CMake版本过旧与工具链升级全记录

用vcpkg在Ubuntu 18.04上装库,第一次碰到CMake版本报错的时候,我真的愣了一下。明明vcpkg一套流程走下来,bootstrap也过了,./vcpkg install gdal一执行,configure阶段直接挂掉,报错说CMake 3.10.2太旧。这台…

2026/10/18 6:27:51 阅读更多 →
MindSpore开源贡献实战:从提交issue到合并PR的完整指南

MindSpore开源贡献实战:从提交issue到合并PR的完整指南

MindSpore 开发之路这个系列写到第二十五篇,前面二十几篇基本都在围绕怎么装环境、怎么调试、怎么把模型跑起来。今天换个视角,聊“融入开源”这件事。如果你用 MindSpore 一段时间,大概率会遇到这样几个场景:文档里没写清楚的参数…

2026/10/18 6:27:51 阅读更多 →
sqli-labs Less-16 布尔盲注自动化脚本实战复盘

sqli-labs Less-16 布尔盲注自动化脚本实战复盘

sqli-labs 我前前后后刷过三遍,每次都有新收获,但要说哪一关最让人"卡壳",Less-16 绝对排得上号。这一关是典型的 POST 型布尔盲注,注入点藏在一个看似平平无奇的登录表单里,闭合方式是双引号加括号")&…

2026/10/18 6:27:51 阅读更多 →

最新新闻

SDL2+C++从零写坦克大战:2D游戏开发核心机制实战

SDL2+C++从零写坦克大战:2D游戏开发核心机制实战

简介:基于SDL2与C实现的经典坦克大战游戏项目,面向游戏开发入门者与进阶学习者,也可作为毕业设计、课程设计或工程实训的完整选题。项目基于Visual Studio 2012环境编写,内含完整C源码与SDL2依赖库配置,并附带Qt5开发的…

2026/10/18 7:22:02 阅读更多 →
猪只关键点检测数据集 | 猪只关键点 姿态估计 智慧畜牧 行为识别 YOLO-Pose9210期

猪只关键点检测数据集 | 猪只关键点 姿态估计 智慧畜牧 行为识别 YOLO-Pose9210期

猪只关键点检测数据集 | 猪只关键点 姿态估计 智慧畜牧 行为识别 YOLO-Pose9210期 数据集概述 本数据集专注于猪只身体关键点的视觉检测与姿态估计,服务于智慧畜牧、动物健康监测及行为分析。数据涵盖猪只身体多个关键点,适配姿态评分、行为识别及健康预…

2026/10/18 7:22:02 阅读更多 →
千问    LeetCode 629. K Inverse Pairs Array Java Implement

千问 LeetCode 629. K Inverse Pairs Array Java Implement

Here’s the Java solution for LeetCode 629. K Inverse Pairs Array, including both the standard prefix-sum version and a rolling-window variant. Problem Given n and k, count the number of permutations of 1…n that contain exactly k inverse pairs (a pair (i,…

2026/10/18 7:22:02 阅读更多 →
diagram-design:可执行的系统图示设计方法论

diagram-design:可执行的系统图示设计方法论

1. “diagram-design”不是画图软件的代名词,而是一套被严重低估的系统化表达语言很多人第一次看到“diagram-design”这个词,下意识会把它当成某个新出的在线绘图工具,比如“是不是又一个类似draw.io的网页版?”——这种理解偏差…

2026/10/18 7:22:02 阅读更多 →
二手车交易数据分析与可视化系统源码解析:从数据清洗到ECharts图表

二手车交易数据分析与可视化系统源码解析:从数据清洗到ECharts图表

简介:这份资源是面向数据分析初学者与二手车行业研究者的完整项目包,围绕二手车交易数据的采集、存储、分析与可视化展开,帮助读者理解市场行情并支撑购车决策。包内共104个文件,以31个Python脚本、20个编译文件、16个HTML页面、1…

2026/10/18 7:22:02 阅读更多 →
国产Linux系统访问Windows共享文件:SMB挂载与排错实战

国产Linux系统访问Windows共享文件:SMB挂载与排错实战

1. 为什么“获取共享文件”绕不开SMB这条路“国产操作系统下获取Windows共享文件”这个需求,听着简单,实际做起来处处是坑。大多数场景是某单位的文档服务器、某项目部的文件备份机、或者办公室那台常年开着的Windows老机器,上面挂着几个共享…

2026/10/18 7:21:02 阅读更多 →

日新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/18 0:00:55 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/18 0:00:55 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/18 0:00:55 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 0:55:02 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →