前往抽奖：https://vintagestory.top/t/topic/117🎉 抽奖活动：抽一个复古物语激活活动奖品：一个Key（价值166￥）获奖人数：1开奖时间：2026-7-10 20:00最低积分：150简单说明：每人仅限一次，仅限回复参与（一句话）欢迎参与，祝大家好运！积分更加严格，意味着并非新成员就可以加入，你必须活跃社区！仅有一个名额，注册/登录账号然后评论即可参与。查看积分：https://vintagestory.top/leaderboard/1系统自动判定中奖楼层发奖方法：中奖后站内私信我，24小时内未联系视为放弃中奖，收到站内短信后24小时内发奖。
WeChat Official Account（微信公众号），为了更好分享文章内容和复古物语等文章，我于是重新拾起了{公众号:以前就开了}。
重要提醒：此页面已过时（最后验证版本为 1.15）。每次游戏更新验证如此长列表非常耗时！如果本页语法未产生预期效果，请立即在游戏内输入 .chb 打开命令手册——它与最新版本同步，且信息通常比Wiki更完整、更准确。服务器命令全部以 / 开头，客户端命令全部以 . 开头，在聊天框输入。不要输入 [ ]（仅用于表示可选参数）。更改世界/服务器设置后必须重启世界才能生效。坐标请参考Wiki的“coordinates”页面。来源：https://wiki.vintagestory.at/List_of_server_commandshttps://wiki.vintagestory.at/List_of_client_commands一、服务器命令（以 / 开头）这些命令在服务器端执行，会影响所有玩家、世界或多人设置。1. 玩家专用多人联机指令/land用于领地系统（Land Claiming）。认领区域后只有你能建造/交互。/land [claim|free|info|list]认领世界一块区域，只有你能建造或使用方块。/land list显示你已认领的领地列表，可用显示的索引号进行修改。/land info显示当前位置的领地持有者信息（如果有）。/land free [index]删除你自己的某个领地。/land adminfree管理员删除其他玩家的领地。/land claim [load|new|grant|revoke|grantgroup|revokegroup|grow|shrink|start|end|add|allowuseeveryone|plevel|fullheight|save|cancel|download]创建或修改现有领地。/land claim load [0..999]：加载现有领地进行修改（用 /land list 显示的索引号）。/land claim new：创建新领地，同时标记起点。/land claim grant [playername] [use|all]：授予玩家访问权限（use=仅交互，all=建造+破坏）。/land claim revoke [playername]：撤销玩家权限。/land claim grantgroup [groupname] [use|all]：授予玩家组权限。/land claim revokegroup [groupname]：撤销玩家组权限。/land claim allowuseeveryone [true|false]：允许/禁止所有人交互。/land claim start：标记立方体起点。/land claim end：标记立方体终点。/land claim grow [north|east|south|west|up|down] [-99..99]：向指定方向扩展（默认1格）。/land claim shrink [north|east|south|west|up|down] [-99..99]：向指定方向缩小（默认1格）。/land claim add：将当前选区添加到正在编辑的领地（必须相邻）。/land claim fullheight：垂直扩展至世界全高（默认设置下玩家无法认领这么多区块）。/land claim save [description]：保存修改并停止编辑。/land claim cancel：放弃所有修改。/land claim download [0..999]：下载你的领地JSON文件到电脑（保存至 AppData/Roaming/VintageStoryData/WorldEdit，即使非管理员也可使用）。/group创建/管理玩家组（提供独立聊天频道 + 领地权限）。/group [create|disband|rename|invite|acceptinvite|leave|list|info|kick|op|deop]/group create [groupname]：创建新组（自动获得聊天频道）。/group invite [group] [playername]：邀请玩家。/group acceptinvite [groupname]：接受邀请。/group leave [group]：退出组。/group list：列出你所在的所有组。/group info [groupname]：列出组成员。/group rename [oldgroupname] [newgroupname]：重命名组。/group kick [group] [playername]：踢出玩家。/group op [group] [playername]：授予邀请权限（op身份）。/group deop [group] [playername]：撤销op身份。/group disband [group]：解散组（仅拥有者）。/waypoint管理世界地图标记点。/waypoint [add|addat|addati|list|remove]/waypoint add [color] [title]：在当前位置添加标记（颜色可用.NET颜色名或十六进制，如 red 或 #FF0000）。示例：/waypoint add red copper。/waypoint addat [coords] [pinned] [color] [title]：在指定坐标添加。/waypoint addati [icon] [x] [y] [z] [pinned] [color] [title]：带图标添加。/waypoint list：按ID显示所有标记。/waypoint remove [id]：删除指定ID标记。/kill自杀（立即死亡）。/emote [wave|cheer|shrug|cry|nod|facepalm|bow|laugh|rage]角色执行表情动画。/pm [playername] [message]私聊（自动创建临时组）。2. 管理员 / Moderator 命令/help列出所有服务器命令。/giveblock [blockcode][{attributes}] [quantity] [toplayername]给予指定方块。数量、目标玩家、属性（JSON格式）可选，默认数量1。示例：/giveblock clutter{type: "barrel-metal1"}。/giveitem [itemcode] [quantity] [toplayername] [attributes]同上，但给予物品。/gamemode [0..4|guest|survival|creative|spectator]（可缩写）设置自身游戏模式。0/guest：仅交互，不能放置/破坏。1/survival：生存模式。2/creative：创造模式（无限物品、飞行、无死亡）。3/spectator：旁观者。/gamemode [playername] [0..4]：设置他人模式。/gm 为其简写。/tp [coordinates] 或 /tp [playername] [coordinates] 或 /tp [playername]传送自己/他人。支持相对坐标（~）、绝对坐标（=）。/tpwp [starts with name]：传送到标记点。/ban [playername] [reason] / /unban [playername]封禁/解封玩家。/kick [playername] [reason]踢出玩家。/clearinv清空自己背包。/op [playername]别名：/player [playername] role admin。/role [rolename] [roleproperty]显示/修改角色属性。spawnpoint、privilege、landclaimallowance、landclaimmaxareas、landclaimminsize 等子命令（详见Wiki原文）。/player [playername] [子命令]修改玩家数据（movespeed、clearinv、wipedata、whitelist、privilege、role、entity、stats、gamemode、landclaimallowance 等）。子命令包括设置体温、健康、饱食度、氧气、醉酒值等。/worldconfig（详见单独的 World Configuration 页面）永久修改世界规则（如 colorAccurateWorldmap 等）。/entity [subcommand]生成、移除、计数实体。包括 cmd、debug、spawndebug、count、remove 等子命令。/time [set|get|speed|stop|resume]控制时间（set day/night、speed、hoursperday、calendarspeedmul 等）。/weather控制天气（setprecip、stoprain、setev、setw 等）。/serverconfig [config] [value]修改服务器配置（maxchunkradius、maxclients、password、allowpvp、tickrate 等）。/stats / /announce [message] / /stop / /autosavenow / /list [clients|banned|role|privileges] / /allowlan [on|off] / /moddb [install|remove|list|search...]其他管理员工具（统计、公告、停止服务器、强制保存、列出、允许局域网、模组数据库等）。二、客户端命令（以 . 开头）仅影响本地客户端（界面、调试、录制等）。.blockitempngexport [inv|all] [imageSize] [domain]导出所有物品/方块为PNG图像。.bsedit（别名 .dev bsedit）打开方块选择编辑器。.cam [p|rp|cp|up|goto|start|loop|stop|clear|load|save|alpha|tp|rec|gui]电影摄影机工具（路径录制、动画、录视频等）。详细子命令见Wiki。.c[louds] 系列（.cdensity / .cdensitylarge / .cdensitysmall / .clight / .crand / .cviewdist）云层渲染调整。.charsel打开角色选择菜单。.chatsize [width]/default [height]/default自定义聊天窗口尺寸。.chb打开游戏内命令手册（最推荐！实时更新）。.clearchat清空聊天记录。.clientconfig [parameterName] [parameterValue]实时修改客户端设置（gammaLevel、floatyGuis、ssaa 等）。.clientconfigcreate [name] [datatype] [value]新增自定义配置键。.clients（别名 .online） / .clients ping显示在线玩家/带延迟。.cp [posi|aposi|apos|chat]复制坐标或聊天历史到剪贴板。.crash强制崩溃游戏（调试）。.ctblocks [freezeCtBlocks]切换客户端 ticking 方块更新（录制瀑布等用）。.debug [normalview|wireframe|talk|perceptioneffect|...]高级调试（线框、视觉效果、语音等）。.dev 系列（.dev bsedit、.dev reload、.dev tfedit 等）开发工具（资产重载、变换编辑器等）。.edi（别名 .debug edi）显示调试屏扩展信息（如方块光照）。.errorreporter重新打开错误报告对话框。.exponepng导出单个物品/方块为PNG。.fbdeb切换帧缓冲/WOIT调试模式。.fountain [quantity] [quad|cube]粒子喷泉测试。.firstlaunchinfo显示首次启动信息对话框。.freemove [on|off]切换自由移动。.gencraftjson复制当前手持物品的合成配方JSON片段。.gui [on|off]隐藏/显示所有GUI。.help [commandname]显示客户端命令列表及子命令说明。.lockfly [1|2|3]锁定飞行/游泳时的某个轴。.logticks切换Tick性能分析。.map [purgedb|redraw|worldmapsize]地图数据库/重绘/尺寸调整。.moon显示当前月相。.movespeed [speed]设置移动速度。.music stop停止当前音乐。.netbenchmark切换网络基准测试。.notminecraft显示“这不是Minecraft”横幅（再次输入关闭）。.noclip [on|off]切换穿墙模式。.pastemode [single|multi]设置聊天粘贴模式。.recomposechat重新组合聊天对话框。.reconnect重新连接服务器。.recordingmode录制模式（自动调亮画面）。.reload [shapes|textures|lang|handbook]重载资产（模型、纹理、语言、 handbook）。.resolution [width] [height]设置屏幕分辨率。.serverinfo显示服务器信息。.spawndecal在位置生成贴花。.tfedit（别名 .dev tfedit）打开变换编辑器。.time读取本地游戏时间或速度。.timelapse [interval-in-days] [duration-in-months]拍摄延时摄影（图片保存至 Pictures/Vintagestory/timelapse）。.tutorial [hud|restart|skip]教程系统交互。.viewdistance设置视距（无上限）。.vrec视频录制工具。.weWorldEdit工具栏（详见Wiki“How to use WorldEdit”）。.weather显示当前天气信息。.zfar [value]设置远裁剪平面距离（默认1000，高值可能导致Z-fighting）。
当一个游戏没有社区：我如何从0搭建Vintage Story中文论坛
S-背景2023年我搭建部署了Minecraft（一款自由度高创意游戏）联机论坛，在一周内上涨70名用户，帖子数涨了50篇，1k\~2k单日流量，不过迫于高考，我最终关闭了社区；2024年我发现了一个特别有潜力的游戏——复古物语（Vintage Story），这款游戏吸引了不少国内玩家（玩法硬核、克苏鲁风格），然而在中国区域，这款游戏的知名度仍然很低，主要原因包括：缺少玩家社区Wiki 资料中文翻译不完整下载渠道不明确正版购买流程较为困难这些问题导致很多玩家即使听说过这款游戏，也很难真正深入了解。因此，我决定搭建一个面向中文玩家的社区，并购买了域名（vintagestory.top）。这个社区的目标其实很简单——为中国玩家提供一个可以交流、分享和获取资源的平台。T-目标为了解决最初面临的巨大问题：抉择论坛系统、搭建论坛、传播社区、提升社区的活跃度与知名度，我必须寻找更多渠道和方法，其中困难如下：国内玩家很少，很隐蔽，各个平台仅有部分UP主分享视频，流量并不是很大，更多流量是来自UP主自带的流量；国内游戏开发者并不了解，其中有哪些正在维护游戏项目？哪些正在为游戏维基百科贡献内容？哪些人的视频有稳定流量？是否有小社区在不为人知的角落？搭建论坛需要的材料，怎么选择论坛系统？需要解决玩家哪些需求？是否自研系统？服务器怎么选择？域名怎么选择？A-行动2026年3月15日，我开始整理论坛方案，做了多选单选：选择PHPBB、Discuz！、MyBB、NodeBB、Flarum、Discourse系统，还是自研系统；自研语言选择PHP、NodeJS、Python、JAVA还是其他？域名选择vsbbs、vintagestory、fuguwuyu？一级域名选择com还是cn等？功能有哪些？作品系统？服务器列表检测？2026年3月26日，一夜1h，我决定下来了，选择Discourse系统论坛，我在搭建论坛的同时，我写了一篇文章：关于中文社区的开发和运营 - 网站反馈 - Vintage Story 中文社区 其中包含我对社区的前期的思考；随后我将文章分享到了各个平台：BiliBli、抖音、百度贴吧、微信公众号；在分享的途中，我发现一个很大的群聊玩家集结点，里面有UP主、游戏启动器作者、模组开发者、维基维护者，一开始大家对此并不感冒，觉得很快就会结束，上一个社区也是如此，毕竟游戏在国区是不温不火的；30号晚上，我参与了MVL（游戏启动器作者的项目），我参与了项目部分代码，我构建了国内加速下载游戏的渠道，这一个举动，开发者作者决定将我的社区（非官方论坛）贴在项目介绍里面，这也带来了不少来自国外用户的瞩目。R-结果不出我所料：游戏不乏爱分享的玩家，从26日建站到4月3日，一共涨用户27名，下面是Grok模型对数据的分享：过去7天的健康状况评价：流量稳定且较高：每天页面浏览量维持在2000-2900左右（峰值接近3000），今天还有1.3k（昨天946），属于小社区里比较活跃的水平。登录用户（蓝色）是流量主力，说明有一定黏性。用户参与度提升明显：DAU/MAU从几乎0飙升到高峰95，后续稳定在40-60；每日参与用户也从0涨到7-8左右。内容产出活跃：帖子数量在爆发期明显增加（单日最高接近15，后续也有持续产出）；新贡献者出现（单日最高4人），说明有新用户愿意发帖。互动良好：今天点赞18（昨天11），过去7天累计43，说明内容被认可。用户对用户互动（今天2）也开始出现。注册和话题有脉冲：虽然不稳定，但爆发期单日注册最高5、话题最高7，证明有用户愿意注册和讨论。
简单介绍MarkdownPlus是让Codex通过本站主题HansJack文章的渲染功能来剥离而出的Typecho插件，不确保你的主题能不能完美适配，不过添加了主题文章内容选中器。https://github.com/TGU-HansJack/MarkdownPlus主要功能提示块（alert）和引用块增强[!NOTE]111[!TIP]111[!IMPORTANT]111[!WARNING]111[!CAUTION]111并且支持折叠。行内语法增强!!剧透!!、++插入++、==高亮==、{字:注音}嵌入短代码{% youtube %}、{% bilibili %}、{% video %}、{% iframe/embed %}、{% comment %}Onebox 卡片段落里单独 URL 自动卡片化；GitHub repo/issue/PR 会调用 GitHub API 补全信息https://github.com/VintageStory-Community/vintagestory-forum-cnhttps://github.com/scgm0/MVL/pull/4https://github.com/VintageStory-Community/vintagestory-forum-cn/blob/main/README.md图片增强独立图片段落转 figure + figcaption，并自动 loading="lazy"代码块增强复制按钮、超过 10 行可折叠class Plugin implements PluginInterface { private const VERSION = '1.0.2'; private static $frontRendered = false; public static function activate() { \Typecho\Plugin::factory('Widget_Archive')->footer = [__CLASS__, 'renderFooter']; return _t('MarkdownPlus 已启用'); } public static function deactivate() { } public static function config(Form $form) { $selector = new Text( 'contentSelector', null, '.post-content, .post_content', _t('文章内容容器选择器'), _t('支持多个 CSS 选择器，使用英文逗号分隔。留空时默认匹配 default / OneBlog 的正文容器。') ); $form->addInput($selector); } public static function personalConfig(Form $form) { } private static function cut($text, $len) { if (function_exists('mb_substr')) { return mb_substr((string) $text, 0, (int) $len); } return substr((string) $text, 0, (int) $len); } private static function getConfig() { $defaults = [ 'contentSelector' => '.post-content, .post_content', ]; try { $raw = \Helper::options()->plugin('MarkdownPlus')->toArray(); if (!is_array($raw)) { $raw = []; } } catch (\Throwable $e) { $raw = []; } $cfg = array_merge($defaults, $raw); $selector = trim((string) ($cfg['contentSelector'] ?? '')); $selector = str_replace(["\r\n", "\r", "\n"], ' ', $selector); $selector = preg_replace('/\s{2,}/u', ' ', $selector); $selector = trim((string) $selector); if ($selector === '') { $selector = $defaults['contentSelector']; } $cfg['contentSelector'] = self::cut($selector, 600); return $cfg; } public static function renderFooter($archive) { if (defined('__TYPECHO_ADMIN__') && __TYPECHO_ADMIN__) { return; } if (self::$frontRendered) { return; } if (!is_object($archive) || !method_exists($archive, 'is')) { return; } $isSingle = false; try { $isSingle = $archive->is('single') || $archive->is('post') || $archive->is('page'); } catch (\Throwable $e) { $isSingle = false; } if (!$isSingle) { return; } $cfg = self::getConfig(); $pluginUrl = ''; try { $pluginUrl = rtrim((string) (\Helper::options()->pluginUrl ?? ''), '/') . '/MarkdownPlus'; } catch (\Throwable $e) { $pluginUrl = ''; } if ($pluginUrl === '') { return; } $bootstrap = [ 'version' => self::VERSION, 'contentSelector' => (string) ($cfg['contentSelector'] ?? ''), 'assetBase' => $pluginUrl . '/assets', ]; $json = json_encode( $bootstrap, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); if ($json === false) { return; } self::$frontRendered = true; echo ' '; echo ' '; echo ' '; } }图表/白板渲染支持 vega-lite 和 excalidraw 代码块移动端交互优化tooltip/spoiler 点击展开逻辑
🎉 抽奖活动：抽一个复古物语激活活动奖品：一个赠送游戏账户（价值166￥）获奖人数：1开奖时间：2026-04-04 12:00最低积分：5简单说明：每人仅限一次，仅限回复参与（一句话）欢迎参与，祝大家好运！仅有一个名额，注册/登录账号然后评论即可参与。积分限制说明： 5积分可以通过别人点赞+1/访问网站+1/发回复+1/创建话题+5/…等方式获取，所以不属于门槛，仅验证是否真人回复。系统自动判定中奖楼层发奖方法： 中奖后站内私信我，24小时内未联系视为放弃中奖，收到站内短信后24小时内发奖。抽奖：https://vintagestory.top/t/topic/12
1. 示范渲染Vega-lite 是一个基于 VEGA 的高级可视化库，旨在简化数据可视化的过程。它通过简单的 JSON 规范来描述数据与视觉元素之间的映射关系，支持多种图表类型，如条形图、散点图和折线图等。Vega-lite 的设计使得用户无需编写复杂的 JavaScript 代码即可创建交互式和美观的可视化效果，特别适合数据科学家、记者和研究人员使用。它还支持多种编程语言，方便集成到各种环境中，如 Web 页面和 Jupyter Notebook。下面就是我最近实验课DSC结果，通过csv文件生成的图形。{ "$schema": "https://vega.github.io/schema/vega-lite/v6.json", "description": "Temperature vs DTG/DSC/TG (curves)", "width": 820, "height": 380, "data": { "url": "/usr/uploads/data/ExpDat_1_clean.csv", "format": { "type": "csv", "parse": { "Temp_C": "number", "DTG": "number", "DSC": "number", "TG": "number" } } }, "encoding": { "x": { "field": "Temp_C", "type": "quantitative", "title": "Temperature (°C)" } }, "layer": [ { "mark": { "type": "line", "interpolate": "monotone", "color": "#e11d48", "strokeWidth": 2 }, "encoding": { "y": { "field": "DTG", "type": "quantitative", "title": "DTG (%/min)", "axis": { "orient": "left", "titleColor": "#e11d48", "labelColor": "#e11d48" } } } }, { "mark": { "type": "line", "interpolate": "monotone", "color": "#2563eb", "strokeWidth": 2 }, "encoding": { "y": { "field": "DSC", "type": "quantitative", "title": "DSC (mW/mg)", "axis": { "orient": "right", "offset": 0, "titleColor": "#2563eb", "labelColor": "#2563eb" } } } }, { "mark": { "type": "line", "interpolate": "monotone", "color": "#16a34a", "strokeWidth": 2 }, "encoding": { "y": { "field": "TG", "type": "quantitative", "title": "TG (%)", "axis": { "orient": "right", "offset": 58, "titleColor": "#16a34a", "labelColor": "#16a34a" } } } } ], "resolve": { "scale": { "y": "independent" } } }可以对比机器结果：下面是实际的Markdown代码：{ "$schema": "https://vega.github.io/schema/vega-lite/v6.json", "description": "Temperature vs DTG/DSC/TG (curves)", "width": 820, "height": 380, "data": { "url": "/usr/uploads/data/ExpDat_1_clean.csv", "format": { "type": "csv", "parse": { "Temp_C": "number", "DTG": "number", "DSC": "number", "TG": "number" } } }, "encoding": { "x": { "field": "Temp_C", "type": "quantitative", "title": "Temperature (°C)" } }, "layer": [ { "mark": { "type": "line", "interpolate": "monotone", "color": "#e11d48", "strokeWidth": 2 }, "encoding": { "y": { "field": "DTG", "type": "quantitative", "title": "DTG (%/min)", "axis": { "orient": "left", "titleColor": "#e11d48", "labelColor": "#e11d48" } } } }, { "mark": { "type": "line", "interpolate": "monotone", "color": "#2563eb", "strokeWidth": 2 }, "encoding": { "y": { "field": "DSC", "type": "quantitative", "title": "DSC (mW/mg)", "axis": { "orient": "right", "offset": 0, "titleColor": "#2563eb", "labelColor": "#2563eb" } } } }, { "mark": { "type": "line", "interpolate": "monotone", "color": "#16a34a", "strokeWidth": 2 }, "encoding": { "y": { "field": "TG", "type": "quantitative", "title": "TG (%)", "axis": { "orient": "right", "offset": 58, "titleColor": "#16a34a", "labelColor": "#16a34a" } } } } ], "resolve": { "scale": { "y": "independent" } } }2. 如何实现短代码写法 embed{% vega src="/usr/uploads/vega/sales.json" height=360 renderer=svg actions=off %}代码块写法{ "$schema": "https://vega.github.io/schema/vega-lite/v6.json", "data": {"values":[{"x":"A","y":28},{"x":"B","y":55}]}, "mark": "bar", "encoding": { "x": {"field":"x","type":"nominal"}, "y": {"field":"y","type":"quantitative"} } }服务端实现识别 embed 语法：{% ... %}。解析参数：支持 key=value、单双引号值。vega 短代码渲染成占位节点： 安全src 必须是站内绝对路径禁止 //、..、反斜杠、控制字符、空白等比如：{% vega src="/uploads/vega/sales.json" height=360 renderer=svg actions=off %}{ "data": { "url": "https://evil.com/data.json" }, "mark": "bar" }
字段内容扩展中文名称数据库目录ext/hansjack/db扩展英文名称DataBase作者HansJack版本号0.0.1用途游戏论坛开发者展示自己的作品项目，支持项目展示、项目筛选、主题帖子绑定项目用于讨论、项目提交、项目审核、项目分类、版本更新涉及页面：ACP（后台管理） UCP（用户中心） 前台（app.php/db）ACP（管理员面板）路径： ACP -> 扩展 -> DataBase分为：项目管理、分类管理、标签管理、版本管理、扩展设置1. 项目管理功能：列表显示所有项目的条目搜索 / 筛选状态控制：pending（待审核）approved（已发布）rejected（拒绝）操作：审核通过 / 拒绝编辑内容删除开启评分2. 分类管理字段：分类名（模组/工具/其他）标识(URL路径)（mod / tool / other）{categorize}是否启用功能：新增分类编辑分类删除分类3. 标签管理功能：创建标签合并标签（防止重复）删除标签查看使用次数4. 版本管理用途：项目添加支持的游戏版本号、支持端（Win/Linux/Mac）链接导入版本号：https://api.vintagestory.at/stable-unstable.json5. 扩展设置是否开启审核标签最大数量允许上传文件大小、类型是否开启全局评分UCP（用户面板）路径： UCP -> 数据库1. 我的贡献用户可以看到：我提交的 Mod等 项目状态（审核中 / 已发布 / 被拒）项目编辑/更新查看页面2. 项目编辑/更新项目字段：名称描述项目唯一{id}（不可与已有项目重复，大小写不敏感，仅支持中文、英文、-、_）分类{categorize}（支持多选）支持端（支持多选）标签（支持创建和输入时下拉选择）预览图（支持多图片上传）绑定主题（仅自己发布的主题，下拉选择）版本添加：添加版本编辑版本删除版本版本字段：版本号支持端支持版本号上传项目文件更新日志前台页面路径：/app.php/db首页（数据库列表）顶部支持：搜索分类筛选标签筛选作者筛选版本筛选下方网格布局形式显示项目：展示：名称短描述标签作者分类封面图（第一张预览图）详情页面路径：/app.php/db/{categorize}/{id}展示：名称短描述标签作者分类预览图最新版本和下载按钮支持版本文件页面（位于详情页面，tabs切换两个内容）展示：历史版本下载按钮隐藏显示更新日志支持版本号更新时间
如果让你去选择接下来的路程，你是选择考研、选择找工作、选择创业、选择还是其他？我无法去创业，我没有资金，我见识少，每个人都心怀挣大钱，又有多少人能够直接去创业；我无法去直接就业，我深知本科的知识根本没有用，本科的阶梯不过多了一双可以护脚的鞋，我无法茁壮自身，正如我体验到的：大学正在高中化，考研高考化。我无法孤注一掷，因为家庭原因，我既不是饥寒交迫的边缘人，也不是前路平坦的高层人，在金字塔的经济社会，我不过处于负贷+负荷打工下家庭的成员。考研是否真的可以让我跨越”阶级“？是的，但也不是。多了关系网，多了一条路，多了一个行程。在去年开始，我已思考考研方向，但我无法剥离互联网，因为这里带给我不一样的感受：努力真的可以获取知识，大家互不认识，可以讨论，可以水，可以吐槽，可以批评；关系不会因为一件事而破灭，大家都带面具，没有利益关系。{% comment https://www.hansjack.com/memos#comment-11 %}今年我把考研想法告知父母，他们很赞同：族族代代没有考研的，考研多一条出路。考研还是选择材料吗？必须的，而且一直走下去。我深知选择材料是从高考完成就已经决定的，不仅仅是朋友对我的肯定：别人走不了的，你一定可以。胆怯吗？会失败吗？胆怯是真的，担心没有考上而无脸面对“父老乡亲”；会失败是可能的，谁能保证次次运气呢？越来越很少看QQ，平常还有课程，若回复慢一天甚至几天，请见谅。
{% comment https://www.hansjack.com/memos#comment-14 %}欢迎赞助我的开发！
搭建起因我曾经搭建过一个 Minecraft 论坛 —— 创梦联机论坛。不过由于当时正处于高中阶段，学业压力较大，最终无法继续维护项目，论坛只运行了大约一个月时间。后来接触到了 Vintage Story 这款游戏。这是一款非常适合喜欢 像素风、生存探索、自由玩法以及克苏鲁风格 的玩家的游戏。然而在中国区域，这款游戏的知名度仍然很低，主要原因包括：缺少玩家社区Wiki 资料中文翻译不完整下载渠道不明确正版购买流程较为困难这些问题导致很多玩家即使听说过这款游戏，也很难真正深入了解。因此，我决定搭建一个面向中文玩家的社区，并购买了域名：https://vintagestory.top开始部署论坛社区。这个社区的目标其实很简单 ——为中国玩家提供一个可以交流、分享和获取资源的平台。为什么不注册 .com 或其他域名？主要原因是个人经济能力有限。对我来说，域名只是网站的“入场券”。.com 域名已经被炒到很高的价格，而官方社区使用的是 .at 域名。.cn 域名又存在实名信息暴露的问题，因此最终选择了价格较为便宜的 .top 域名。社区内容社区目前计划提供以下内容：模组下载渠道整理和提供可用的游戏模组资源。游戏下载与购买指引提供学习版渠道说明，以及正版购买教程。玩家交流社区为玩家提供讨论游戏、分享经验和发布内容的平台。预计开发功能论坛目前基于 phpBB 搭建，未来计划逐步扩展功能。1. Markdown 渲染支持论坛目前主要使用 BBCode。由于现有 Markdown 插件兼容问题较多，未来可能会开发 专用 Markdown 渲染插件，以便更好地支持技术类和长文内容。2. 模组中文数据库（MDZ）计划开发 Mod 中文数据库：mod_db_zh（简称 MDZ）目标是为中文社区提供一个模组数据库系统，包括：模组分类与索引模组搜索功能模组讨论区（开发中 / 已发布）模组信息面板同时也欢迎中文社区开发原创模组。3. 服务器列表与 API未来将提供：服务器列表服务器信息展示服务器状态 API服务器数据面板方便玩家快速找到服务器。社区主旨与建议社区主旨开放自由，友善交流拒绝戾气、谩骂和恶意攻击希望这里能成为一个氛围友好、理性讨论的社区。社区建议赞助说明论坛未来仅接受 爱发电渠道赞助。除该渠道以外的任何收费或赞助方式均不属于本站行为，请注意辨别。赞助预计 在今年 11 月之后开放，在社区基本稳定前暂不接受赞助。隐私提示论坛不会主动收集用户隐私信息，但：网站 无法完全保证所有数据的绝对安全唯一重点保护的是 用户邮箱隐私因此建议：请不要在个人资料或帖子中填写真实隐私信息！
1. 起因我舍友放假回来宿舍一直玩Minecarft，正好我也打算看看AI对这游戏影响：上周末玩了一下Minecraft，又因为我最近几个月都在玩AI Coding，于是想到能不能使用GPT 5.4去做一个模组：AIPlayers (AIP){% comment https://www.hansjack.com/memos#comment-33 %}2. 思路采用Forge模组，借鉴Britone的寻路逻辑、自研的任务型脚本。多AI玩家之间通过同一个知识库互相获取世界信息，数据来自Minecarft Wiki。使用阿里模型QWEN-ASR实现语音发送命令，Openai中转API实现对话。3. 转折点昨天晚上我才发现已经有项目做的很强，成熟了。其中Mindcraft实现LLM在我的世界服务器中进行命令深层思考然后调用项目中做好的动作逻辑。相比较我的AIP优点是完全可以AI驱动运动，而不是脚本写死的任务。不过，运行一晚上后发现Mindcraft项目很耗费延迟，很容易卡死循环，任务复杂很难依靠简单API去完成，这需要更好的机器（基础设施）。已经把Mindcraft项目加入我的AIPlayers项目中，意味着我的项目跃升至AI智能化，旧AIP项目做了寻路A*算法、动作逻辑、GOAP，但是依旧不智能，自从加入Mindcraft就已经更上一层楼了！🤗{% comment https://www.hansjack.com/memos#comment-36 %}
前言{蟒蛇:Python}是面向对象的高级语言，我想应该比C语言简单吧？我最近的文章《用Typecho备份文件构建博客专属字库 - 寒士杰克》里面就是使用Python库去抽取、子集化字体文件，原本20MB的字体文件，分包后可能加载需要1~2MB大小，我通过Python库的字体子集化：python -m fontTools.subset将字体文件裁剪指定字符的字体文件，输出字体：文件内容core-cjk.woff2高频汉字core-misc.woff2标点 + ASCIIfallback.woff2低频汉字结果不足300KB的文件，大大加快网站速度！如果你也想体验，可以下载Python依赖库：pip install fonttools brotli这篇文章我写下我学习过程的笔记，可详可略，不建议初学者，适合学后回顾，欢迎评论告诉我你的困惑，或者AI辅助自己，人工智能时代希望大家变成真正的AI工程师，而不是AI替代品！参考教程：Python3 教程 | 菜鸟教程Python历史 - Python教程 - 廖雪峰的官方网站1. Python简介Python是著名的“龟叔”Guido van Rossum在1989年圣诞节期间，为了打发无聊的圣诞节而编写的一个编程语言。1特点：非常完善的代码库，覆盖全（网络、文件、GUI、数据库、文本等1），”即用食品“，适合快速开发场景。适合开发：网络应用，包括网站、后台服务等1运行速度慢、代码不能加密（PHP可以加密，C语言直接用打包就行）2. 安装过程网站：Python Releases for Windows | Python.org启动：终端输入python，说明Python安装成功：Microsoft Windows [Version 10.0.26100.7840] (c) Microsoft Corporation. All rights reserved. C:\Users\29596>python Python 3.8.6 (tags/v3.8.6:db45529, Sep 23 2020, 15:52:53) [MSC v.1927 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> _IDLE界面(交互式编程)：测试（print() 在括号中添加字符串就可以输出指定文字）：print ("Hello, Python!")输出：Hello, Python!print() 函数也可以输出多个字符串，使用英文逗号 , 隔开，其中英文逗号会输出成一个空格。也可以计算和输出结果，比如print('100 + 200 =', 100 + 200) 。其中 '100 + 200 =' 是字符串而非数学公式。3. 基础语法2编码：以 UTF-8 编码，所有字符串都是 unicode 字符串。如果你需要使用不同的编码，例如ISO-8859-1或ASCII，你需要明确指定3。Python 标识符：在 Python 里，标识符由字母、数字、下划线组成。 Python历史 - Python教程 - 廖雪峰的官方网站 ↩ Python3 教程 | 菜鸟教程 ↩ 这可以通过在源文件的第一行或第二行添加一个编码声明来实现。具体格式为# -*- coding: <encoding name> -*-，比如# -*- coding: utf-8 -*- 或者 # coding=utf-8 就行了。这是为了确保Python解释器能够正确解析源文件中的所有字符。 ↩
相比较高中紧张气氛，我“佛系”了，走路慢下来，心静下来，学多少是多少，不必急于填鸭学习，不必熬夜（报复性）。这些力量可能来自老班主任的指导：不要苦恼做不出来的，没有做对就当命，不要影响到自己的接下来的路程。
人成熟的过程，是承认孤独的过程。小时候，我的安全感来自于家人、朋友。我希望被看见、被理解、被陪伴。我会害怕孤独。长大后，我开始从他人那里得到自我认同。我试图融入群体，证明自己的价值，被喜欢、被接受。但也正是在这种追求中，我慢慢发现：无论我多么努力，别人终究无法完全理解我。我开始学会与孤独共处，不再逃避它，而是将它视为生命的一部分。这时的孤独，不再是空虚的，而是自由的。正如村上春树说过的一句话：“孤独一旦被接受，就会变成一种力量”。
思政作业PPT已经做完了。由于我是一个人一组的，我的演讲稿使用Deepseek完成。我使用 GPT-SoVITS 项目进行声音克隆；模仿我的声音，然后生成全部演讲稿；使用 达芬奇 进行剪辑完成！https://github.com/RVC-Boss/GPT-SoVITS版本20250606v2proGPT-SoVITS 下载地址：huggingface中国镜像直链解压文件，打开点击 go-webui.bat （注意：这个终端窗口请勿删掉，一段时间后自动进入浏览器）人声分离等操作：训练模型：进行1、2步骤，1是一键三连、2默认训练即可，先SoVITS再GPT：开启推理：放入音频和文本： 输入文本，点击合成：
在优化博客性能时，字体往往是一个被忽略却体积巨大的资源。以思源宋体为例，完整字体动辄十几 MB，即使分包后依然不小。对于以文字为主的博客来说，这是完全可以进一步优化的。结果：文件名类型大小core-misc.woff2woff239.68 kBcore-cjk.woff2woff2141.33 kBfallback.woff2woff296.26 kB思路：从网站备份中提取高频字符（或者使用3500常用字1）基于提取结果裁剪字体，生成子集[!NOTE]不套用“3500的常用字”，而是统计真正用过的，这样生成的字体子集，既保证文章正常显示，又最大程度缩小体积。1. 从备份中提取常用字通过脚本解析网站备份文件：统计字符频率提取前 800 个高频字同时收集全部出现过的 CJK 字符生成两个字符集文件： charset-core.txt、charset-all-cjk.txt2. 生成字体子集利用 fonttools 对原始思源宋体进行裁剪输出：core-cjk.woff2（高频汉字）core-misc.woff2（标点、符号、ASCII）fallback.woff2（其余 CJK 字符）subset-font.css（自动生成的 font-face 配置）3. 脚本参考提取param( [Parameter(Mandatory = $true)] [string]$InputFile, [int]$TopChars = 4000, [string]$OutputDir = "" ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" function Get-TextByBestEncoding { param( [byte[]]$Bytes ) $utf8 = [System.Text.Encoding]::UTF8.GetString($Bytes) $gbk = [System.Text.Encoding]::GetEncoding(936).GetString($Bytes) $cjkPattern = "[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]" $commonHan = "的是了我不在人有这中大来上个们到说和你地出道时年得就那要下以会可也后能子里所然文于着起看学" $utf8CjkCount = [regex]::Matches($utf8, $cjkPattern).Count $gbkCjkCount = [regex]::Matches($gbk, $cjkPattern).Count $utf8CommonHit = 0 $gbkCommonHit = 0 foreach ($ch in $commonHan.ToCharArray()) { $p = [regex]::Escape([string]$ch) $utf8CommonHit += [regex]::Matches($utf8, $p).Count $gbkCommonHit += [regex]::Matches($gbk, $p).Count } # Typical UTF-8 mojibake markers when decoded as GBK. $mojibakeMarkers = @("锛", "涓", "鐨", "銆", "鍙", "鎴", "浠", "璇", "鏄", "鍚") $utf8Mojibake = 0 $gbkMojibake = 0 foreach ($m in $mojibakeMarkers) { $p = [regex]::Escape($m) $utf8Mojibake += [regex]::Matches($utf8, $p).Count $gbkMojibake += [regex]::Matches($gbk, $p).Count } $utf8Replacement = [regex]::Matches($utf8, "�").Count $gbkReplacement = [regex]::Matches($gbk, "�").Count $utf8Score = ($utf8CommonHit * 1000) + $utf8CjkCount - ($utf8Mojibake * 200) - ($utf8Replacement * 10) $gbkScore = ($gbkCommonHit * 1000) + $gbkCjkCount - ($gbkMojibake * 200) - ($gbkReplacement * 10) if ($gbkScore -gt $utf8Score) { return @{ Text = $gbk Encoding = "GBK(936)" CjkCount = $gbkCjkCount } } return @{ Text = $utf8 Encoding = "UTF-8" CjkCount = $utf8CjkCount } } function Add-CharsToSet { param( [System.Collections.Generic.HashSet[string]]$Set, [System.Collections.Generic.List[string]]$Ordered, [string]$Text ) foreach ($ch in $Text.ToCharArray()) { $s = [string]$ch if ($Set.Add($s)) { $Ordered.Add($s) } } } if (-not (Test-Path -LiteralPath $InputFile)) { throw "Input file not found: $InputFile" } $inputFullPath = (Resolve-Path -LiteralPath $InputFile).Path $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path if ([string]::IsNullOrWhiteSpace($OutputDir)) { $OutputDir = Join-Path $scriptDir "..\assets\fonts\SourceHanSerifCN\subset" } $OutputDir = [System.IO.Path]::GetFullPath($OutputDir) New-Item -Path $OutputDir -ItemType Directory -Force | Out-Null $bytes = [System.IO.File]::ReadAllBytes($inputFullPath) $decoded = Get-TextByBestEncoding -Bytes $bytes $text = [string]$decoded.Text $cjkPattern = "[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]" $matches = [regex]::Matches($text, $cjkPattern) $freq = @{} foreach ($m in $matches) { $ch = $m.Value if ($freq.ContainsKey($ch)) { $freq[$ch] += 1 } else { $freq[$ch] = 1 } } $sorted = $freq.GetEnumerator() | Sort-Object -Property @{ Expression = "Value"; Descending = $true }, @{ Expression = "Name"; Descending = $false } $uniqueCjk = $sorted.Count if ($TopChars -lt 1) { $TopChars = 1 } if ($TopChars -gt $uniqueCjk -and $uniqueCjk -gt 0) { $TopChars = $uniqueCjk } $top = @() if ($uniqueCjk -gt 0) { $top = $sorted | Select-Object -First $TopChars } # Base ASCII set + commonly used Chinese punctuation. $ascii = " !`"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_abcdefghijklmnopqrstuvwxyz{|}~" $hanPunctCodePoints = @( 0x3001, 0x3002, 0xFF0C, 0xFF1A, 0xFF1B, 0xFF01, 0xFF1F, 0x300A, 0x300B, 0x300C, 0x300D, 0x300E, 0x300F, 0x201C, 0x201D, 0x2018, 0x2019, 0xFF08, 0xFF09, 0x3010, 0x3011, 0x2014, 0x2026, 0x00B7, 0x3008, 0x3009 ) $hanPunct = "" foreach ($cp in $hanPunctCodePoints) { $hanPunct += [char]$cp } $set = New-Object "System.Collections.Generic.HashSet[string]" $ordered = New-Object "System.Collections.Generic.List[string]" Add-CharsToSet -Set $set -Ordered $ordered -Text $ascii Add-CharsToSet -Set $set -Ordered $ordered -Text $hanPunct foreach ($item in $top) { $ch = [string]$item.Name if ($set.Add($ch)) { $ordered.Add($ch) } } $charsetCore = ($ordered -join "") $charsetCorePath = Join-Path $OutputDir "charset-core.txt" [System.IO.File]::WriteAllText($charsetCorePath, $charsetCore, [System.Text.Encoding]::UTF8) $allCjkChars = "" if ($uniqueCjk -gt 0) { $allCjkChars = (($sorted | ForEach-Object { $_.Name }) -join "") } $charsetAllPath = Join-Path $OutputDir "charset-all-cjk.txt" [System.IO.File]::WriteAllText($charsetAllPath, $allCjkChars, [System.Text.Encoding]::UTF8) $totalCjkCount = 0 foreach ($item in $sorted) { $totalCjkCount += [int]$item.Value } $csvPath = Join-Path $OutputDir "char-frequency.csv" "rank,char,count,ratio,cumulative_ratio" | Out-File -FilePath $csvPath -Encoding UTF8 $acc = 0 $rank = 0 foreach ($item in $sorted) { $rank += 1 $count = [int]$item.Value $acc += $count $ratio = if ($totalCjkCount -gt 0) { [math]::Round(($count / $totalCjkCount), 8) } else { 0 } $cumRatio = if ($totalCjkCount -gt 0) { [math]::Round(($acc / $totalCjkCount), 8) } else { 0 } "$rank,$($item.Name),$count,$ratio,$cumRatio" | Out-File -FilePath $csvPath -Encoding UTF8 -Append } $topCoverage = 0 if ($totalCjkCount -gt 0 -and $TopChars -gt 0) { $topCount = ($sorted | Select-Object -First $TopChars | Measure-Object -Property Value -Sum).Sum $topCoverage = [math]::Round(($topCount / $totalCjkCount) * 100, 4) } $reportPath = Join-Path $OutputDir "charset-report.txt" $report = @( "InputFile=$inputFullPath", "DetectedEncoding=$($decoded.Encoding)", "FileBytes=$($bytes.Length)", "TotalCjkChars=$totalCjkCount", "UniqueCjkChars=$uniqueCjk", "TopChars=$TopChars", "TopCoveragePercent=$topCoverage", "OutputCharsetCore=$charsetCorePath", "OutputCharsetAll=$charsetAllPath", "OutputFrequencyCsv=$csvPath" ) $report | Out-File -FilePath $reportPath -Encoding UTF8 Write-Output ("OK") Write-Output ("Detected encoding: {0}" -f $decoded.Encoding) Write-Output ("Total CJK chars: {0}" -f $totalCjkCount) Write-Output ("Unique CJK chars: {0}" -f $uniqueCjk) Write-Output ("Top {0} coverage: {1}%" -f $TopChars, $topCoverage) Write-Output ("charset-core: {0}" -f $charsetCorePath) Write-Output ("report: {0}" -f $reportPath) .\extract-common-chars.ps1 ` -InputFile "...dat" ` -TopChars 800 ` -OutputDir "...subset-backup-800"生成param( [Parameter(Mandatory = $true)] [string]$SourceFont, [Parameter(Mandatory = $true)] [string]$CoreCharsetFile, [Parameter(Mandatory = $true)] [string]$AllCharsetFile, [string]$OutputDir = "", [string]$PythonCommand = "python", [int]$InstanceWeight = 400 ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" function Resolve-ExistingPath { param( [Parameter(Mandatory = $true)] [string]$Path ) if (-not (Test-Path -LiteralPath $Path)) { throw "Path not found: $Path" } return (Resolve-Path -LiteralPath $Path).Path } function Is-CjkCodePoint { param( [int]$CodePoint ) return ( ($CodePoint -ge 0x3400 -and $CodePoint -le 0x4DBF) -or ($CodePoint -ge 0x4E00 -and $CodePoint -le 0x9FFF) -or ($CodePoint -ge 0xF900 -and $CodePoint -le 0xFAFF) ) } function Get-UniqueChars { param( [string]$Text ) $set = New-Object "System.Collections.Generic.HashSet[string]" $ordered = New-Object "System.Collections.Generic.List[string]" foreach ($ch in $Text.ToCharArray()) { $s = [string]$ch if ($set.Add($s)) { $ordered.Add($s) } } return ,$ordered.ToArray() } function Convert-CharsToUnicodeRange { param( [string[]]$Chars ) if (-not $Chars -or $Chars.Count -eq 0) { return "" } $codeSet = New-Object "System.Collections.Generic.HashSet[int]" foreach ($ch in $Chars) { if ([string]::IsNullOrEmpty($ch)) { continue } [void]$codeSet.Add([int][char]$ch) } if ($codeSet.Count -eq 0) { return "" } $codes = @($codeSet) | Sort-Object $ranges = New-Object "System.Collections.Generic.List[string]" $start = $codes[0] $prev = $codes[0] for ($i = 1; $i -lt $codes.Count; $i++) { $curr = [int]$codes[$i] if ($curr -eq ($prev + 1)) { $prev = $curr continue } if ($start -eq $prev) { $ranges.Add(("U+{0}" -f $start.ToString("X"))) } else { $ranges.Add(("U+{0}-{1}" -f @($start.ToString("X"), $prev.ToString("X")))) } $start = $curr $prev = $curr } if ($start -eq $prev) { $ranges.Add(("U+{0}" -f $start.ToString("X"))) } else { $ranges.Add(("U+{0}-{1}" -f @($start.ToString("X"), $prev.ToString("X")))) } return ($ranges -join ",") } function Invoke-FontSubset { param( [string]$PythonCommand, [string]$SourceFontPath, [string]$OutputFilePath, [string]$TextFilePath, [string[]]$CommonArgs ) $args = @( "-m", "fontTools.subset", $SourceFontPath, "--output-file=$OutputFilePath", "--text-file=$TextFilePath" ) + $CommonArgs & $PythonCommand @args if ($LASTEXITCODE -ne 0) { throw "Subset build failed ($OutputFilePath) with exit code $LASTEXITCODE" } } $sourceFontPath = Resolve-ExistingPath -Path $SourceFont $coreCharsetPath = Resolve-ExistingPath -Path $CoreCharsetFile $allCharsetPath = Resolve-ExistingPath -Path $AllCharsetFile if ([string]::IsNullOrWhiteSpace($OutputDir)) { $OutputDir = Split-Path -Parent $coreCharsetPath } $outputDirFull = [System.IO.Path]::GetFullPath($OutputDir) New-Item -ItemType Directory -Path $outputDirFull -Force | Out-Null $utf8 = [System.Text.Encoding]::UTF8 $utf8NoBom = New-Object System.Text.UTF8Encoding($false) $coreText = [System.IO.File]::ReadAllText($coreCharsetPath, $utf8) $allText = [System.IO.File]::ReadAllText($allCharsetPath, $utf8) $subsetSourceFontPath = $sourceFontPath $fontWeightDescriptor = "250 900" $instanceFontPath = "" if ($InstanceWeight -gt 0) { $instanceFontPath = Join-Path $outputDirFull ("source-wght-{0}.ttf" -f $InstanceWeight) $instanceArgs = @( "-m", "fontTools.varLib.instancer", $sourceFontPath, ("wght={0}" -f $InstanceWeight), "--output", $instanceFontPath ) & $PythonCommand @instanceArgs if ($LASTEXITCODE -ne 0) { throw "Font instancer failed with exit code $LASTEXITCODE" } if (-not (Test-Path -LiteralPath $instanceFontPath)) { throw "Instanced font not found: $instanceFontPath" } $subsetSourceFontPath = $instanceFontPath $fontWeightDescriptor = [string]$InstanceWeight } $coreUniqueChars = Get-UniqueChars -Text $coreText $coreCjkChars = New-Object "System.Collections.Generic.List[string]" $coreMiscChars = New-Object "System.Collections.Generic.List[string]" foreach ($ch in $coreUniqueChars) { if ([string]::IsNullOrEmpty($ch)) { continue } $cp = [int][char]$ch if (Is-CjkCodePoint -CodePoint $cp) { $coreCjkChars.Add($ch) } else { $coreMiscChars.Add($ch) } } $coreCjkText = $coreCjkChars -join "" $coreMiscText = $coreMiscChars -join "" $coreCjkCharsetPath = Join-Path $outputDirFull "charset-core-cjk.txt" $coreMiscCharsetPath = Join-Path $outputDirFull "charset-core-misc.txt" [System.IO.File]::WriteAllText($coreCjkCharsetPath, $coreCjkText, $utf8NoBom) [System.IO.File]::WriteAllText($coreMiscCharsetPath, $coreMiscText, $utf8NoBom) $coreSet = New-Object "System.Collections.Generic.HashSet[string]" foreach ($ch in $coreText.ToCharArray()) { [void]$coreSet.Add([string]$ch) } $fallbackChars = New-Object "System.Collections.Generic.List[string]" foreach ($ch in $allText.ToCharArray()) { $s = [string]$ch if (-not $coreSet.Contains($s)) { $fallbackChars.Add($s) } } $fallbackText = $fallbackChars -join "" $fallbackCharsetPath = Join-Path $outputDirFull "charset-fallback.txt" [System.IO.File]::WriteAllText($fallbackCharsetPath, $fallbackText, $utf8NoBom) $commonSubsetArgs = @( "--flavor=woff2", "--layout-features=*", "--name-IDs=*", "--name-legacy", "--name-languages=*", "--notdef-glyph", "--notdef-outline", "--recommended-glyphs", "--symbol-cmap", "--legacy-cmap", "--no-hinting" ) $coreCjkWoff2Path = Join-Path $outputDirFull "core-cjk.woff2" $coreMiscWoff2Path = Join-Path $outputDirFull "core-misc.woff2" $fallbackWoff2Path = Join-Path $outputDirFull "fallback.woff2" if ($coreCjkChars.Count -gt 0) { Invoke-FontSubset ` -PythonCommand $PythonCommand ` -SourceFontPath $subsetSourceFontPath ` -OutputFilePath $coreCjkWoff2Path ` -TextFilePath $coreCjkCharsetPath ` -CommonArgs $commonSubsetArgs } if ($coreMiscChars.Count -gt 0) { Invoke-FontSubset ` -PythonCommand $PythonCommand ` -SourceFontPath $subsetSourceFontPath ` -OutputFilePath $coreMiscWoff2Path ` -TextFilePath $coreMiscCharsetPath ` -CommonArgs $commonSubsetArgs } if ($fallbackText.Length -gt 0) { Invoke-FontSubset ` -PythonCommand $PythonCommand ` -SourceFontPath $subsetSourceFontPath ` -OutputFilePath $fallbackWoff2Path ` -TextFilePath $fallbackCharsetPath ` -CommonArgs $commonSubsetArgs } $subsetCssPath = Join-Path $outputDirFull "subset-font.css" [System.Collections.Generic.List[string]]$cssLines = @() if ($coreCjkChars.Count -gt 0) { $cssLines.Add("@font-face {") $cssLines.Add(" font-family: `"HJ Source Han Serif Core`";") $cssLines.Add(" src: url(`"./core-cjk.woff2`") format(`"woff2`");") $cssLines.Add(" font-style: normal;") $cssLines.Add(" font-weight: $fontWeightDescriptor;") $cssLines.Add(" font-display: swap;") $cssLines.Add(" unicode-range: $(Convert-CharsToUnicodeRange -Chars $coreCjkChars.ToArray());") $cssLines.Add("}") $cssLines.Add("") } if ($coreMiscChars.Count -gt 0) { $cssLines.Add("@font-face {") $cssLines.Add(" font-family: `"HJ Source Han Serif Core`";") $cssLines.Add(" src: url(`"./core-misc.woff2`") format(`"woff2`");") $cssLines.Add(" font-style: normal;") $cssLines.Add(" font-weight: $fontWeightDescriptor;") $cssLines.Add(" font-display: swap;") $cssLines.Add(" unicode-range: $(Convert-CharsToUnicodeRange -Chars $coreMiscChars.ToArray());") $cssLines.Add("}") $cssLines.Add("") } $fallbackUniqueChars = Get-UniqueChars -Text $fallbackText if ($fallbackUniqueChars.Count -gt 0) { $cssLines.Add("@font-face {") $cssLines.Add(" font-family: `"HJ Source Han Serif Fallback`";") $cssLines.Add(" src: url(`"./fallback.woff2`") format(`"woff2`");") $cssLines.Add(" font-style: normal;") $cssLines.Add(" font-weight: $fontWeightDescriptor;") $cssLines.Add(" font-display: swap;") $cssLines.Add(" unicode-range: $(Convert-CharsToUnicodeRange -Chars $fallbackUniqueChars);") $cssLines.Add("}") } $subsetCss = ($cssLines -join [Environment]::NewLine) [System.IO.File]::WriteAllText($subsetCssPath, $subsetCss, $utf8NoBom) $coreCjkBytes = if (Test-Path -LiteralPath $coreCjkWoff2Path) { (Get-Item -LiteralPath $coreCjkWoff2Path).Length } else { 0 } $coreMiscBytes = if (Test-Path -LiteralPath $coreMiscWoff2Path) { (Get-Item -LiteralPath $coreMiscWoff2Path).Length } else { 0 } $fallbackBytes = if (Test-Path -LiteralPath $fallbackWoff2Path) { (Get-Item -LiteralPath $fallbackWoff2Path).Length } else { 0 } $reportPath = Join-Path $outputDirFull "subset-build-report.txt" $reportLines = @( "SourceFont=$sourceFontPath", "SubsetSourceFont=$subsetSourceFontPath", "InstanceWeight=$InstanceWeight", "CoreCharset=$coreCharsetPath", "CoreCjkCharset=$coreCjkCharsetPath", "CoreMiscCharset=$coreMiscCharsetPath", "AllCharset=$allCharsetPath", "FallbackCharset=$fallbackCharsetPath", "CoreChars=$($coreText.Length)", "CoreCjkChars=$($coreCjkText.Length)", "CoreMiscChars=$($coreMiscText.Length)", "FallbackChars=$($fallbackText.Length)", "CoreCjkWoff2=$coreCjkWoff2Path", "CoreCjkWoff2Bytes=$coreCjkBytes", "CoreMiscWoff2=$coreMiscWoff2Path", "CoreMiscWoff2Bytes=$coreMiscBytes", "FallbackWoff2=$fallbackWoff2Path", "FallbackWoff2Bytes=$fallbackBytes", "TotalSubsetBytes=$($coreCjkBytes + $coreMiscBytes + $fallbackBytes)", "SubsetCss=$subsetCssPath" ) [System.IO.File]::WriteAllLines($reportPath, $reportLines, $utf8NoBom) if (-not [string]::IsNullOrWhiteSpace($instanceFontPath) -and (Test-Path -LiteralPath $instanceFontPath)) { try { Remove-Item -LiteralPath $instanceFontPath -Force -ErrorAction Stop } catch { # Keep build successful even if temp instance cleanup fails. } } Write-Output "OK" Write-Output "Core CJK woff2: $coreCjkWoff2Path ($coreCjkBytes bytes)" Write-Output "Core Misc woff2: $coreMiscWoff2Path ($coreMiscBytes bytes)" Write-Output "Fallback woff2: $fallbackWoff2Path ($fallbackBytes bytes)" Write-Output "Subset css: $subsetCssPath" Write-Output "Report: $reportPath" .\build-font-subsets.ps1 ` -SourceFont "SourceHanSerifCN.ttf" ` -CoreCharsetFile "...charset-core.txt" ` -AllCharsetFile "...charset-all-cjk.txt" ` -OutputDir "...subset-backup-800" ` -InstanceWeight 400结果subset-backup-800/ ├── core-cjk.woff2 ├── core-misc.woff2 ├── fallback.woff2 ├── subset-font.css ├── charset-core.txt └── charset-all-cjk.txt调用<link rel="stylesheet" href="<?php $this->options->themeUrl('assets/fonts/SourceHanSerifCN/subset-backup-800/subset-font.css'); ?>">body { font-family: "HJ Source Han Serif Core", "HJ Source Han Serif Fallback", serif; } 博客站换字体添新意，域名场售珍宝获千金 - 索玛(Suo.Ma) ↩
我在 Typecho 里支持下面Excalidraw Markdown 语法，并在前台自动渲染为 Excalidraw 预览图：{"type":"excalidraw/clipboard","elements":[{"id":"6GjbnNpb0v7uZHOxT6s7-","type":"rectangle","x":661.5999755859375,"y":333.6000061035156,"width":584.800048828125,"height":419.9999694824219,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"transparent","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"index":"a0","roundness":{"type":3},"seed":1761550740,"version":78,"versionNonce":1747656724,"isDeleted":false,"boundElements":null,"updated":1772295344156,"link":null,"locked":false},{"id":"WvhWp8UfkYdSsNK8OTPDJ","type":"rectangle","x":618.4000244140625,"y":205.60000610351562,"width":400,"height":370.3999938964844,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"transparent","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"index":"a2","roundness":{"type":3},"seed":1405776428,"version":30,"versionNonce":285044652,"isDeleted":false,"boundElements":null,"updated":1772295348046,"link":null,"locked":false}],"files":{}}前台只做预览，不开放编辑工具栏本地化资源文章内支持 excalidraw 代码块Ⅰ. 构建本地化资源把 Excalidraw 运行时资源放到当前主题目录，例如：/usr/themes/你的主题/assets/vendor/excalidraw/excalidraw-runtime.mod.js/usr/themes/你的主题/assets/vendor/excalidraw/excalidraw-runtime.css/usr/themes/你的主题/assets/vendor/excalidraw/chunks/*/usr/themes/你的主题/assets/vendor/excalidraw/assets/*注意：推荐 ESM 分包版本，入口小、按需加载 chunks11. 创建文件夹在.tmp-excalidraw-build文件夹添加文件package.json、src/runtime.jspackage.json如下：{ "name": "excalidraw-build", "private": true, "version": "1.0.0", "type": "module", "scripts": { "build": "esbuild src/runtime.js --bundle --format=iife --target=es2020 --conditions=production --define:process.env.NODE_ENV=\\\"production\\\" --minify --global-name=ExcalidrawMarkdownRuntime --outfile=dist/excalidraw-runtime.js --loader:.woff2=file --loader:.ttf=file --loader:.png=file --loader:.svg=file --loader:.css=css --asset-names=assets/[name]-[hash] --public-path=./", "build:split": "esbuild src/runtime.js --bundle --format=esm --splitting --target=es2020 --conditions=production --define:process.env.NODE_ENV=\\\"production\\\" --minify --outdir=dist-split --entry-names=excalidraw-runtime --chunk-names=chunks/[name]-[hash] --loader:.woff2=file --loader:.ttf=file --loader:.png=file --loader:.svg=file --loader:.css=css --asset-names=assets/[name]-[hash] --public-path=./", "build:split:absolute": "esbuild src/runtime.js --bundle --format=esm --splitting --target=es2020 --conditions=production --define:process.env.NODE_ENV=\\\"production\\\" --minify --outdir=dist-split-abs --entry-names=excalidraw-runtime --chunk-names=chunks/[name]-[hash] --loader:.woff2=file --loader:.ttf=file --loader:.png=file --loader:.svg=file --loader:.css=css --asset-names=assets/[name]-[hash] --public-path=/usr/themes/YOUR_THEME/assets/vendor/excalidraw/" }, "dependencies": { "@excalidraw/excalidraw": "0.18.0", "react": "19.0.0", "react-dom": "19.0.0" }, "devDependencies": { "esbuild": "0.25.10" } }Excalidraw 渲染适配层 把 Excalidraw 包装成你主题里可直接调用的统一入口 src/runtime.js如下：import React from "react"; import { createRoot } from "react-dom/client"; import { Excalidraw } from "@excalidraw/excalidraw"; import "@excalidraw/excalidraw/index.css"; function safeTheme(theme) { return theme === "dark" ? "dark" : "light"; } function normalizeScene(scene) { const raw = scene && typeof scene === "object" ? scene : {}; const appState = raw.appState && typeof raw.appState === "object" ? raw.appState : {}; return { scrollToContent: true, elements: Array.isArray(raw.elements) ? raw.elements : [], files: raw.files && typeof raw.files === "object" ? raw.files : {}, appState: { ...appState, viewModeEnabled: true, zenModeEnabled: false, viewBackgroundColor: "transparent" } }; } function mount(stage, scene, options) { if (!stage) { throw new Error("excalidraw mount stage missing"); } const opts = options && typeof options === "object" ? options : {}; const normalizedScene = normalizeScene(scene); let apiRef = null; const root = createRoot(stage); root.render( React.createElement(Excalidraw, { initialData: normalizedScene, excalidrawAPI: (api) => { apiRef = api; }, theme: safeTheme(opts.theme), detectScroll: false, viewModeEnabled: true, zenModeEnabled: false }) ); requestAnimationFrame(() => { requestAnimationFrame(() => { try { if (apiRef && typeof apiRef.scrollToContent === "function") { apiRef.scrollToContent(undefined, { fitToContent: true, animate: false }); } } catch (e) {} }); }); return { unmount() { try { root.unmount(); } catch (e) {} } }; } window.ExcalidrawMarkdownRuntime = { mount };2. 初始化cd .tmp-excalidraw-build npm install安装完成后，官方 Excalidraw prod 资源在：node_modules/@excalidraw/excalidraw/dist/prod这里包含 index.js、index.css、locales、fonts、data 等生产资源。3. 构建推荐 ESM 分包npm run build:split产物目录：dist-splitexcalidraw-runtime.js（入口）excalidraw-runtime.csschunks/*assets/*可选：绝对路径分包npm run build:split:absolute说明：package.json 中 public-path 默认是占位值：/usr/themes/YOUR_THEME/assets/vendor/excalidraw/使用前请改成你的主题路径。4. 本地化到主题目录.tmp-excalidraw-build/node_modules/@excalidraw/excalidraw/dist/prod目标文件夹 /assets/vendor/excalidraw/prod.tmp-excalidraw-build/dist-split/chunks目标文件夹 /assets/vendor/excalidraw/chunks.tmp-excalidraw-build/dist-split/assets目标文件夹 /assets/vendor/excalidraw/assets.tmp-excalidraw-build/dist-split/excalidraw-runtime.js目标文件 /assets/vendor/excalidraw/excalidraw-runtime.js.tmp-excalidraw-build/dist-split/excalidraw-runtime.css目标文件 /assets/vendor/excalidraw/excalidraw-runtime.cssⅡ. 前端渲染把下面脚本放进主题 footer.php中： Ⅲ. 样式.tc-excalidraw-block { display: block; margin: 1rem 0; width: 100%; max-width: 100%; box-sizing: border-box; border: 1px solid #000; border-radius: 4px; background: transparent; overflow: hidden; } .tc-excalidraw-stage { position: relative; width: 100%; min-height: 520px; max-height: min(82vh, 760px); overflow: hidden; background: transparent; } .tc-excalidraw-editor { position: absolute; inset: 0; width: 100%; height: 100%; } /* 预览模式下隐藏编辑工具区（防缓存差异） */ .tc-excalidraw-editor .excalidraw .shapes-section, .tc-excalidraw-editor .excalidraw .App-toolbar-container { display: none !important; } ESM分包 - 周记杂货铺-Leo Zhou's Grocery Store ↩
我总结了 Typecho 主题里常见的 Markdown 资源按需加载方案，重点是 KaTeX highlight.js主题会在所有页面加载：katex.min.css / katex.min.jshighlight.min.js这样会导致：首页、分类页、非渲染的文章也发起请求首屏重CDN 请求数增加于是我采用按需加载：没有公式就不加载 KaTeX没有代码块就不加载 highlight检测时排除 pre/code，避免误判保证主题切换（明/暗）下高亮样式正确首页结果：状态域名文件耗时200www.hansjack.com/59 ms200www.hansjack.comresult.css66 ms200www.hansjack.comstyle.css53 msxwww.blogsclub.orgwww.hansjack.com109 msKaTeX 预判（含 pre/code 排除）<?php use Widget\Archive; function hjArchiveContentContainsKatexSyntax(string $content): bool { if ($content === '') { return false; } // 关键：先排除 pre/code，避免代码示例里的 $...$ 触发误判 $scanContent = $content; if (strpos($scanContent, '<') !== false) { $withoutPre = preg_replace('/<pre\b[^>]*>[\s\S]*?<\/pre>/iu', ' ', $scanContent); if (is_string($withoutPre)) { $scanContent = $withoutPre; } $withoutCode = preg_replace('/<code\b[^>]*>[\s\S]*?<\/code>/iu', ' ', $scanContent); if (is_string($withoutCode)) { $scanContent = $withoutCode; } } if ($scanContent === '') { return false; } if (strpos($scanContent, '$') !== false) { if (preg_match('/\$\$[\s\S]+?\$\$/u', $scanContent) === 1) { return true; } if (preg_match('/(?<!\\\\)\$(?!\s)(?:[^$\\\\\r\n]|\\\\.)+?(?<!\s)(?<!\\\\)\$/u', $scanContent) === 1) { return true; } } if (strpos($scanContent, '\\') !== false) { if (preg_match('/\\\\\([\s\S]+?\\\\\)|\\\\\[[\s\S]+?\\\\\]/u', $scanContent) === 1) { return true; } if (preg_match('/\\\\begin\{(?:equation|align|alignat|gather|CD)\}/u', $scanContent) === 1) { return true; } } return false; } function hjShouldLoadKatexAssets(Archive $archive): bool { $isRenderable = false; try { $isRenderable = $archive->is('post') || $archive->is('page'); } catch (\Throwable $e) { $isRenderable = false; } if (!$isRenderable) { return false; } try { $content = trim((string) ($archive->content ?? '')); } catch (\Throwable $e) { $content = ''; } return hjArchiveContentContainsKatexSyntax($content); }highlight 预判<?php use Widget\Archive; function hjShouldLoadHighlightAssets(Archive $archive): bool { $isRenderable = false; try { $isRenderable = $archive->is('post') || $archive->is('page'); } catch (\Throwable $e) { $isRenderable = false; } if (!$isRenderable) { return false; } try { $content = (string) ($archive->content ?? ''); } catch (\Throwable $e) { $content = ''; } if ($content === '') { return false; } // Markdown 渲染后一般为 ... return preg_match('/<pre\b[^>]*>\s*<code\b/i', $content) === 1; }前端按需加载function loadScriptOnce(src, key, done) { if (!src) return done(); var selector = 'script[data-res-key="' + key + '"]'; var existing = document.querySelector(selector); if (existing) { if (existing.getAttribute("data-res-loaded") === "1") return done(); existing.addEventListener("load", done); existing.addEventListener("error", done); return; } var s = document.createElement("script"); s.src = src; s.async = false; s.setAttribute("data-res-key", key); s.onload = function () { s.setAttribute("data-res-loaded", "1"); done(); }; s.onerror = done; document.head.appendChild(s); } function appendCssOnce(href, key) { if (!href) return; if (document.querySelector('link[data-css-key="' + key + '"]')) return; var link = document.createElement("link"); link.rel = "stylesheet"; link.href = href; link.setAttribute("data-css-key", key); document.head.appendChild(link); }(function () { var containers = Array.prototype.slice.call( document.querySelectorAll(".hj-article-content, .hj-comment-content") ); if (!containers.length) return; var codeBlocks = []; containers.forEach(function (content) { if (!content || !content.querySelectorAll) return; var blocks = content.querySelectorAll("pre code"); for (var i = 0; i < blocks.length; i++) codeBlocks.push(blocks[i]); }); if (!codeBlocks.length) return; function applyHighlightTheme() { var root = document.documentElement; var isDark = root.classList.contains("hj-theme-dark") && !root.classList.contains("hj-theme-light"); var links = document.querySelectorAll("link[data-hljs-theme]"); for (var i = 0; i < links.length; i++) { var theme = links[i].getAttribute("data-hljs-theme") || ""; links[i].disabled = isDark ? theme !== "dark" : theme !== "light"; } } function appendThemeLink(theme, href) { if (!href) return; if (document.querySelector('link[data-hljs-theme="' + theme + '"]')) return; var link = document.createElement("link"); link.rel = "stylesheet"; link.href = href; link.disabled = true; link.setAttribute("data-hljs-theme", theme); document.head.appendChild(link); } appendThemeLink("light", "/usr/themes/YourTheme/assets/vendor/highlight/github.min.css"); appendThemeLink("dark", "/usr/themes/YourTheme/assets/vendor/highlight/github-dark.min.css"); applyHighlightTheme(); loadScriptOnce("/usr/themes/YourTheme/assets/vendor/highlight/highlight.min.js", "hljs", function () { if (!window.hljs) return; try { window.hljs.configure({ ignoreUnescapedHTML: true }); } catch (e) {} codeBlocks.forEach(function (code) { if (!code) return; if (code.dataset && code.dataset.highlighted) return; try { window.hljs.highlightElement(code); } catch (e) {} }); }); })();KaTeX 前端检测（排除 pre/code）function hasKatexSyntax(text) { if (!text) return false; if (text.indexOf("$$") !== -1 && /\$\$[\s\S]+?\$\$/.test(text)) return true; if (text.indexOf("\\(") !== -1 && /\\\([\s\S]+?\\\)/.test(text)) return true; if (text.indexOf("\\[") !== -1 && /\\\[[\s\S]+?\\\]/.test(text)) return true; if (text.indexOf("\\begin{") !== -1 && /\\begin\{(?:equation|align|alignat|gather|CD)\}/.test(text)) return true; if (text.indexOf("$") !== -1 && /(^|[^\\])\$(?![\s$])(?:[^$\\\r\n]|\\.)+?\$(?!\$)/.test(text)) return true; return false; } function readTextWithoutCode(node) { if (!node) return ""; var sourceNode = node.cloneNode ? node.cloneNode(true) : node; if (sourceNode.querySelectorAll) { var ignored = sourceNode.querySelectorAll("pre, code"); for (var i = 0; i < ignored.length; i++) { if (ignored[i] && ignored[i].parentNode) ignored[i].parentNode.removeChild(ignored[i]); } } return String((sourceNode && sourceNode.textContent) || ""); }注意事项检测误判最常见来源是代码块中的数学符号，务必排除 pre/code。正则不要过宽，特别是 $...$，容易误伤金额、Shell 变量等文本。动态加载脚本必须防重复，否则会出现多次初始化。highlight 明暗主题要跟随 html class 切换。若站点启用严格 CSP，需要给动态注入资源放行域名。压缩和缓存策略要和按需加载配合，避免重复下载。评论区若支持 Markdown，同样应纳入检测范围。
Ⅰ. 感观介绍作为大学生宅男，这三年真的很费身体，睡觉越来越晚，突发性的通宵以及不规律的饮食，发现精力也慢慢变少。总结一下三年的作息和饮食：++平均++2-3点开始睡觉（{QQ群里的假睡小子:每晚发：睡了，结果得玩到2点甚至通宵}），早上早八或者睡到10点，可能会熬夜通宵（最近一次熬夜感觉到离死神很近，通宵当天作地铁结果头晕，{半路下地铁后又打车:结果还是晕}，站着不行，必须蹲着，猜测是气血不足，脑子缺血，加上熬夜精神紧绷）；饮食方面是最挑食的，合口的饭菜会吃很多，但是不合口可能就吃几口就不吃了，导致一顿饭没有吃多少；喜欢和饮料，{糖水摄入应该每天都超标:导致检查前都怀疑会不会检查出糖尿病}；Ⅱ. 上周体检分别检查：==肾功能/肝功能/血糖/血脂==：查出一处异常如下项目数值正常值肾小球滤过率131↑90-120[!NOTE]我第一个感觉是鹿多了，天天飞机那种，需要过几个月再复查吧！乙肝还有很强抵抗力所以就没有打疫苗。==彩超（甲状腺/肝胆胰脾）==：[!NOTE]胆囊壁粗糙、3.4mm的胆囊息肉很危险的预兆，因为饮食不规律，又偏食，又熬夜，很可能会胆囊炎等。Ⅲ. 祝愿自己新的一年开始，不要再 鹿、熬夜和不规律饮食吧！