├── .gitignore ├── Linux ├── README.md └── Shell │ ├── README.md │ └── wanxiang-update ├── Mac ├── README.md └── Shell │ ├── README.md │ └── wanxiang-update.sh ├── Android ├── README.MD ├── trime │ └── README.md └── Fcitx5-For-Android │ ├── README.md │ └── 小企鹅导入包构建脚本.py ├── Windows ├── README.md └── PowerShell │ ├── README.md │ └── 按需下载万象方案-词库-模型-utf-8.ps1 ├── iOS ├── Shortcuts │ └── README.md └── README.md ├── Python-全平台版本 ├── README.md └── Python │ └── 万象下载更新.py ├── LICENSE ├── README.md └── .github └── workflows └── release.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /Python-全平台版本/Python/settings.ini -------------------------------------------------------------------------------- /Linux/README.md: -------------------------------------------------------------------------------- 1 | # Rime 万象更新工具 - Linux 版本 2 | 3 | ## 前端支持 4 | 5 | - [x] Fcitx5 6 | - [x] iBus 7 | 8 | ## 项目简介 9 | 10 | 本工具用于在 Linux 系统上自动更新 Rime 输入法的万象方案、词库和模型。 11 | 12 | ## 版本列表 13 | 14 | - [Shell 版本](./Shell/README.md) 15 | 16 | ## 使用说明 17 | 18 | 1. 选择对应的版本 19 | 2. 按照说明文档进行操作 20 | 3. 工具将自动完成更新 21 | 22 | ## 许可证 23 | 24 | MIT License 25 | -------------------------------------------------------------------------------- /Mac/README.md: -------------------------------------------------------------------------------- 1 | # Rime 万象更新工具 - Mac端 Shell 版本 2 | 3 | **移植自[Linux](../Linux/Shell/wanxiang-update)脚本** 4 | 5 | ## 前端支持 6 | 7 | - [x] Fcitx5 8 | - [x] Squirrel 9 | 10 | ## 项目简介 11 | 12 | 本工具用于在 MacOS 系统上自动更新 Rime 输入法的万象方案、词库和模型,方便没有安装Python环境的人使用。 13 | 14 | ## 版本列表 15 | 16 | - [Shell 版本](./Shell/README.md) 17 | 18 | ## 使用说明 19 | 20 | 1. 选择对应的版本 21 | 2. 按照说明文档进行操作 22 | 3. 工具将自动完成更新 23 | 24 | ## 许可证 25 | 26 | MIT License 27 | -------------------------------------------------------------------------------- /Android/README.MD: -------------------------------------------------------------------------------- 1 | # 安卓更新 2 | 3 | ## 依赖说明 4 | 5 | - **需要 Termux , 且启用了存储权限** 6 | - Termux 中需要安装 Python 7 | - 安装 requests 库 8 | 9 | ```bash 10 | pkg install python 11 | pip install requests 12 | ``` 13 | 14 | ## 逻辑说明 15 | 16 | 安卓检测脚本同级目录下的 Rime/rime 子文件夹,没有就创建 Rime 子文件夹. 17 | 18 | ## 前端适配 19 | 20 | - [小企鹅输入法导入包构建脚本](Fcitx5-For-Android/README.md) 21 | - 使用更新工具更新完之后,使用小企鹅打包工具将 Rime 文件夹打包成小企鹅备份文件,直接导入即可。 22 | - [同文输入法更新说明](trime/README.md) 23 | - 同文输入法设置对应的用户文件夹即可直接部署使用。 24 | -------------------------------------------------------------------------------- /Android/trime/README.md: -------------------------------------------------------------------------------- 1 | # trime 同文输入法 2 | 3 | ## 更新方式 4 | 5 | 1. 将更新 python 全平台更新脚本放到你想放置的文件夹中 `例如 document/Github/trime`(手机文件夹管理视角) 6 | 2. 这个时候更新脚本的路径为 `Document/Github/trime/rime-wanxiang-update-win-mac-ios-android.py`(手机文件夹管理视角) 7 | 3. 运行脚本 8 | 1. 打开 Termux 9 | 2. cd 到对应的脚本存放路径 ~/storage/document/Github/trime (Termux 视角) 10 | 3. 运行脚本 11 | 12 | ```python 13 | python rime-wanxiang-update-win-mac-ios-android.py 14 | ``` 15 | 16 | ## 逻辑说明 17 | 18 | 安卓检测脚本同级目录下的 Rime/rime 子文件夹,没有就创建 Rime 子文件夹. 19 | 20 | ## 设置同文输入法 21 | 22 | 设置同文输入法中的 "配置->用户文件夹" 为 `document/Github/trime/Rime` **即与更新脚本同级目录下的 Rime 子目录** 23 | -------------------------------------------------------------------------------- /Windows/README.md: -------------------------------------------------------------------------------- 1 | # Rime 万象更新工具 - Windows 版本 - 只支持小狼毫前端 2 | 3 | ## 项目简介 4 | 5 | 本工具用于在Windows系统上自动更新Rime输入法的万象方案、词库和模型。 6 | 7 | ## 版本列表 8 | 9 | - [PowerShell版本](./PowerShell/README.md) 10 | 11 | ## 使用说明 12 | 13 | 1. 选择对应的版本 14 | 2. 按照说明文档进行操作 15 | 3. 工具将自动完成更新 16 | 17 | ## 常见问题解决方案 18 | 19 | 当你在使用最新的版本的脚本时(默认使用 cnb 源),可能会在更新方案文件的时候出现 `解压失败: 使用“1”个参数调用“.ctor”时发生异常:“路径中具有非法字符。”` 问题,这个是因为你使用的 PowerShell 5 版本,可以通过升级到 PowerShell 7 解决,同时升级到 PowerShell 7 后,如果出现输入提示乱码,请下载 utf-8 后缀版本的更新脚本。使用 GitHub 源无此影响。 20 | 21 | 升级链接:https://learn.microsoft.com/zh-cn/powershell/scripting/whats-new/migrating-from-windows-powershell-51-to-powershell-7?view=powershell-7.5#installing-powershell-7 22 | 23 | ## 许可证 24 | 25 | MIT License 26 | -------------------------------------------------------------------------------- /Android/Fcitx5-For-Android/README.md: -------------------------------------------------------------------------------- 1 | # Fcitx5 for Android 方案导入包生成工具 2 | 3 | ## 使用场景 4 | 5 | - 适用场景:想把已经配置好的 Rime 方案迁移到到 f5a 中使用,但是普通方法操作过于繁琐 6 | - 功能说明:根据指示指定需要打包的 Rime 方案路径(可以是你已经使用小狼毫等配置好的方案),以及对应的输出名称,自动生成可以导入 Fcitx5 for Android 的压缩包,在 f5a 的导入备份文件中将对应的压缩包进行导入即可。 7 | 8 | 9 | ## 使用方法 10 | 11 | ```shell 12 | python 小企鹅导入包构建脚本.py 13 | 用法: 14 | 基本用法: python package_rime.py -s <源目录> -o <输出ZIP路径> 15 | 添加模型: python package_rime.py -s <源目录> -m <模型目录> -o <输出ZIP路径> 16 | 示例: 17 | python package_rime.py -s ./rime-data -o ./dist/rime-package.zip 18 | python package_rime.py -s ./rime-data -m ./models -o ./dist/rime-with-models.zip 19 | 20 | ``` 21 | ## Fcitx5 for Android导入步骤 22 | 1. 打开输入法的设置 23 | 2. 打开高级 24 | 3. 导入用户数据, 点确定 25 | 4. 选择本脚本打包好的 zip 文件等待完成后即可使用 26 | 27 | ## 脚本文件 28 | 29 | [小企鹅导入包构建脚本.py](小企鹅导入包构建脚本.py) 30 | -------------------------------------------------------------------------------- /iOS/Shortcuts/README.md: -------------------------------------------------------------------------------- 1 | # Rime 万象更新工具 - Shortcuts 版本 2 | 3 | ## 简介 4 | 5 | 本工具是为iOS上的Rime输入法(Hamster输入法)用户设计的自动更新工具,需要在Shortcuts中运行。 6 | 7 | ## 功能特性 8 | 9 | - 配置自己使用的辅助码方案 10 | - 配置输入法路径 11 | - 获取所有方案版本,选择安装 12 | 13 | ## 使用要求 14 | 15 | - 快捷指令app:[Shortcuts](https://apps.apple.com/us/app/shortcuts/id1462947752) 16 | 17 | ## 安装步骤 18 | 19 | 1. 确保已安装上面的app 20 | 2. 获取两个快捷指令([万象方案下载(支持CNB和GitHub)](https://www.icloud.com/shortcuts/d905901c56a34188a6a6a67cd7fa6136)和[日常自动更新万象中文词库](https://www.icloud.com/shortcuts/bd6eee4c48ee4f669bf24f83157f4d4e) 21 | 3. 打开并根据提示配置 22 | 4. 执行一次获取权限 23 | 5. `日常自动更新万象中文词库`可以添加到自动化设置时间进行执行,`万象Pro版本下载`可以根据需要手动执行 24 | 25 | 26 | 27 | ## 注意事项 28 | 29 | - 请在运行快捷指令前先配置好相应路径和方案选择 30 | - 更新过程中请勿操作键盘 31 | - 确保网络连接稳定,需要使用科学上网工具请求GitHub 32 | - 建议定期运行本工具以保持输入法最新 33 | 34 | ## 贡献 35 | 36 | 欢迎提交issue或pull request 37 | 38 | ## 许可证 39 | 40 | MIT License 41 | -------------------------------------------------------------------------------- /Python-全平台版本/README.md: -------------------------------------------------------------------------------- 1 | # Win-Mac-iOS融合版 2 | 3 | **合并Windows、Mac、iOS端的Python脚本,支持万象基础版和Pro版** 4 | 5 | - 下载链接:[rime-wanxiang-update-win-mac-ios-android.py](https://github.com/expoli/rime-wanxiang-update-tools/releases/latest/download/rime-wanxiang-update-win-mac-ios-android.py) 6 | 7 | ## 使用须知 8 | 9 | - iOS仓输入法:请在Pythonista中添加仓输入法的整个文件夹,并将脚本放在该路径下 10 | 11 | ## 注意事项 12 | 13 | - 如果脚本有更新,请更新脚本后,重新打开脚本确保成功覆盖以后再运行(如:iOS端需退出Pythonista 3重新打开) 14 | - 在Windows下运行,如遇到乱码问题(如下图),可在微软商店下载Windows Terminal Preview,或是通过命令行启用虚拟终端 15 | ![屏幕截图 2025-06-23 202131](https://github.com/user-attachments/assets/ee4a9c86-e76e-4433-9114-9b31088fb677) 16 | ![屏幕截图 2025-06-23 202028](https://github.com/user-attachments/assets/6b582f17-7819-44bb-aefb-bd239b876cc7) 17 | - **启用虚拟终端**: 18 | 19 | ```cmd 20 | reg add HKCU\Console /v VirtualTerminalLevel /t REG_DWORD /d 1 /f 21 | ``` 22 | 23 | - **禁用虚拟终端**: 24 | 25 | ```cmd 26 | reg add HKCU\Console /v VirtualTerminalLevel /t REG_DWORD /d 0 /f 27 | ``` 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 expoli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Mac/Shell/README.md: -------------------------------------------------------------------------------- 1 | # Rime 万象方案方案更新 - Mac端 Shell 脚本 2 | 3 | ## 简介 4 | 5 | 这是一个用于更新 Rime 输入法方案的 Mac Shell 脚本。该脚本可以帮助用户自动从 GitHub 上下载最新的方案文件,并将其部署到指定的目录中。 6 | 7 | ## 环境要求 8 | 9 | - `curl` 命令 10 | - `unzip` 命令 11 | 12 | ## 使用教程 13 | ### 为脚本添加可执行权限 14 | 15 | ```bash 16 | chmod +x rime-wanxiang-update-macos.sh 17 | ``` 18 | ### 设置输入法引擎 19 | 20 | 使用任意编辑器打开脚本文件,修改 `ENGINE=""` 为你需要的内容(小企鹅`fcitx5`或鼠须管`squirrel`) 21 | 比如 `DEPLOY_DIR="fcitx5"` 22 | 23 | 24 | ### 创建排除列表文件 25 | 26 | 在部署目录下创建名为 `user_exclude_file.txt` 的文件,以下是一个示例 27 | 28 | - 注释内容以 "#" 开头 29 | 30 | ```txt 31 | # 文件本身 32 | user_exclude_file.txt 33 | # 用户数据库 34 | lua/sequence.userdb 35 | user_flypyzc.userdb 36 | # custom 文件 37 | default.custom.yaml 38 | wanxiang_pro.custom.yaml 39 | wanxiang_reverse.custom.yaml 40 | wanxiang_mixedcode.custom.yaml 41 | # 萌娘百科词库 42 | dicts/moegirl.pro.dict.yaml 43 | wanxiang_pro.dict.yaml 44 | # 自定义 lua 45 | lua/shijian.lua 46 | lua/super_comment.lua 47 | ``` 48 | 49 | ### 使用适当的参数运行脚本 50 | 51 | 以下内容使用专业版、自然码辅助码进行示例,请按需修改 52 | 你可以组合多个参数运行 53 | 54 | #### 使用 CNB 镜像 55 | 56 | ```bash 57 | ./rime-wanxiang-update-macos.sh --mirror cnb 58 | ``` 59 | 60 | #### 更新全部内容 61 | 62 | ```bash 63 | ./rime-wanxiang-update-macos.sh --schema pro --fuzhu zrm --dict --gram 64 | ``` 65 | 66 | #### 只更新方案文件 67 | 68 | ```bash 69 | ./rime-wanxiang-update-macos.sh --schema pro --fuzhu zrm 70 | ``` 71 | 72 | #### 只更新词典文件 73 | 74 | ```bash 75 | ./rime-wanxiang-update-macos.sh --dict --fuzhu zrm 76 | ``` 77 | 78 | #### 只更新语法模型 79 | 80 | ```bash 81 | ./rime-wanxiang-update-macos.sh --gram 82 | ``` 83 | 84 | ### 高级用法 85 | 86 | #### 传入 engine 87 | 88 | 脚本还支持直接传入输入法引擎,这样可以避免修改脚本,方便更新脚本自身 89 | 以下是一个示例 90 | 91 | 92 | ```bash 93 | ./rime-wanxiang-update-macos.sh --engine fcitx5 94 | ``` 95 | -------------------------------------------------------------------------------- /iOS/README.md: -------------------------------------------------------------------------------- 1 | # Rime 万象更新工具 - iOS 版本 - 需下载Hamster输入法和Pythonista 3或a-shell或pyto或Code App 2 | 3 | [Hamster](https://apps.apple.com/us/app/%E4%BB%93%E8%BE%93%E5%85%A5%E6%B3%95/id6446617683) 4 | 5 | [Pythonista 3](https://apps.apple.com/us/app/pythonista-3/id1085978097) / [a-shell](https://apps.apple.com/us/app/a-shell/id1473805438) / [pyto](https://apps.apple.com/us/app/pyto-ide/id1436650069) / [Code App](https://apps.apple.com/us/app/code-app/id1512938504) 6 | 7 | **推荐使用[a-shell](https://apps.apple.com/us/app/a-shell/id1473805438),免费** 8 | 9 | ## 项目简介 10 | 11 | 本工具用于在iOS系统上自动更新Rime输入法(Hamster)的万象方案、词库和模型。 12 | 13 | ## 版本列表 14 | 15 | - [Python版本](../Python-全平台版本/README.md) 16 | - [Shortcuts版本](./Shortcuts/README.md) 17 | 18 | ## 使用说明 19 | 20 | ### Python版本 21 | 22 | 1. 将脚本放在Hamster输入法路径下 23 | 2. 运行脚本,选择对应的版本 24 | 3. 按照说明文档进行操作 25 | 4. 工具将自动完成更新 26 | 5. 更新完成后需手动打开Hamster输入法重新部署 27 | 28 | **注意:** 29 | 若使用[a-shell](https://apps.apple.com/us/app/a-shell/id1473805438),则打开a-shell后,先下载所需包 30 | 31 | ```shell 32 | pip install requests tqdm 33 | ``` 34 | 35 | 然后输入`pickFolder`,选择Hamster输入法的文件夹(也是脚本所在的文件夹),运行: 36 | 37 | ```shell 38 | python rime-wanxiang-update-win-mac-ios-android.py 39 | ``` 40 | 41 | 为方便使用,也可以进行如下操作: 42 | 43 | 1. 打开`.bashrc`文件: 44 | 45 | ```shell 46 | cd ~ 47 | vim .bashrc 48 | ``` 49 | 50 | 2. 插入如下内容(按`i`进入插入模式) 51 | 52 | ```shell 53 | alias rime='python rime-wanxiang-update-win-mac-ios-android.py' 54 | ``` 55 | 56 | 3. 按`esc`,然后输入`:wq`,回车保存,执行`source .bashrc` 57 | 4. 使用`pickFolder`重新打开Hamster文件夹 58 | 5. 执行`rime`命令即可运行脚本 59 | 60 | ### Shortcuts版本 61 | 62 | 1. 获取两个快捷指令 [万象方案下载(支持CNB和GitHub)](https://www.icloud.com/shortcuts/d905901c56a34188a6a6a67cd7fa6136) 和 [日常自动更新万象中文词库](https://www.icloud.com/shortcuts/bd6eee4c48ee4f669bf24f83157f4d4e) 63 | 2. 打开并根据提示配置 64 | 3. 执行一次获取权限 65 | 4. `日常自动更新万象中文词库`可以添加到自动化设置时间进行执行,`万象Pro版本下载`可以根据需要手动执行 66 | 67 | ## 许可证 68 | 69 | MIT License 70 | -------------------------------------------------------------------------------- /Windows/PowerShell/README.md: -------------------------------------------------------------------------------- 1 | # Rime 万象更新工具-Windows 版本(只适用于小狼毫前端) 2 | 3 | ## 简介 4 | 5 | 本工具用于自动下载和更新 Rime 输入法的[万象方案、词库和语言模型](https://github.com/amzxyz/rime_wanxiang_pro)。支持多种输入方案,包括仓颉、小鹤、汉心、简单鹤、墨奇、虎码、五笔和自然码。 6 | 7 | ## 功能 8 | 9 | - 自动检测最新版本 10 | - 按需下载方案、词库和模型 11 | - 自动解压和安装 12 | - 记录更新时间,避免重复下载 13 | - 支持通过GitHub API获取最新版本信息 14 | - 自动重启Rime输入法服务 15 | 16 | ## 使用说明 17 | 18 | 1. 确保已安装 Rime 输入法(小狼毫) 19 | - **本工具仅支持小狼毫前端**,其他Rime前端可能无法正常使用 20 | 2. 运行脚本 `按需下载万象方案-词库-模型.ps1` 21 | 3. 根据提示选择要下载的内容: 22 | - 输入方案类型编号(0-7) 23 | - 选择是否更新所有内容[0/1] 24 | 0. 表示更新方案、词库、模型 25 | 1. 手动选择是否下载方案、词库和模型 26 | 4. 等待下载和安装完成 27 | 28 | ## 脚本运行效果展示 29 | 30 | ```powershell 31 | E:\Github\rime-wanxiang-update-tools\按需下载万象方案-词库-模型.ps1 32 | Weasel用户目录路径为: E:\Github\wanxiang-zrm-fuzhu 33 | 解析出最新的词库链接为:https://github.com/amzxyz/rime_wanxiang_pro/releases/tag/dict-nightly 34 | 解析出最新的版本链接为:https://github.com/amzxyz/rime_wanxiang_pro/releases/tag/v6.7.9 35 | 解析出最新的模型链接为:https://github.com/amzxyz/RIME-LMDG/releases/tag/LTS 36 | 最新的版本为:v6.7.9 37 | 请选择你要下载的辅助码方案类型的编号: 38 | [0]-仓颉; [1]-小鹤; [2]-汉心; [3]-简单鹤; [4]-墨奇; [5]-虎码; [6]-五笔; [7]-自然码: 7 39 | 是否更新所有内容(方案、词库、模型): 40 | [0]-更新所有; [1]-不更新所有: 0 41 | 下载方案 42 | 下载词库 43 | 下载模型 44 | 正在更新词库,请不要操作键盘,直到更新完成 45 | 更新完成后会自动拉起小狼毫 46 | 正在检查方案是否需要更新... 47 | 本地时间: 05/15/2025 21:16:24 48 | 远程时间: 05/15/2025 21:16:24 49 | 当前已是最新版本 50 | 正在检查词库是否需要更新... 51 | 本地时间: 05/16/2025 14:54:23 52 | 远程时间: 05/16/2025 14:54:23 53 | 当前已是最新版本 54 | 正在检查模型是否需要更新... 55 | 本地时间: 05/12/2025 14:03:36 56 | 远程时间: 05/12/2025 14:03:36 57 | 当前已是最新版本 58 | 操作已完成!文件已部署到 Weasel 配置目录:E:\Github\wanxiang-zrm-fuzhu 59 | ``` 60 | 61 | ## 注意事项 62 | 63 | - 更新过程中请勿操作键盘 64 | - 更新完成后会自动重启 Rime 输入法 65 | - 建议定期运行本工具以保持输入法最新 66 | - 需要Windows PowerShell 5.1或更高版本 67 | - 需要稳定的网络连接 68 | 69 | ## 依赖 70 | 71 | - PowerShell 5.1 或更高版本 72 | - 网络连接 73 | - GitHub API访问权限 74 | - [Rime wanxiang pro](https://github.com/amzxyz/rime_wanxiang_pro) 75 | 76 | ## 许可证 77 | 78 | MIT License 79 | -------------------------------------------------------------------------------- /Linux/Shell/README.md: -------------------------------------------------------------------------------- 1 | # Rime 万象方案方案更新 - Linux Shell 脚本 2 | 3 | ## 简介 4 | 5 | 这是一个用于更新 Rime 输入法方案的 Linux Shell 脚本。该脚本可以帮助用户自动从 GitHub 上下载最新的方案文件,并将其部署到指定的目录中。 6 | 7 | ## 环境要求 8 | 9 | - `curl` 程序 10 | - `unzip` 程序 11 | - `jq` 程序 12 | 13 | ## 使用教程 14 | 15 | ### 为脚本添加可执行权限 16 | 17 | ```bash 18 | chmod +x rime-wanxiang-update-linux 19 | ``` 20 | 21 | ### 设置部署目录 22 | 23 | 使用任意编辑器打开脚本文件,修改 `DEPLOY_DIR=""` 为你需要的内容 24 | 比如 `DEPLOY_DIR="$HOME/.local/share/fcitx5/rime"` 25 | 26 | ### 创建排除列表文件 27 | 28 | 在部署目录下创建名为 `user_exclude_file.txt` 的文件,以下是一个示例 29 | 30 | - 注释内容以 "#" 开头 31 | 32 | ```txt 33 | # 文件本身 34 | user_exclude_file.txt 35 | # 用户数据库 36 | lua/sequence.userdb 37 | user_flypyzc.userdb 38 | # custom 文件 39 | default.custom.yaml 40 | wanxiang_pro.custom.yaml 41 | wanxiang_reverse.custom.yaml 42 | wanxiang_mixedcode.custom.yaml 43 | # 萌娘百科词库 44 | dicts/moegirl.pro.dict.yaml 45 | wanxiang_pro.dict.yaml 46 | # 自定义 lua 47 | lua/shijian.lua 48 | lua/super_comment.lua 49 | ``` 50 | 51 | ### 使用适当的参数运行脚本 52 | 53 | 以下内容使用专业版、自然码辅助码进行示例,请按需修改 54 | 你可以组合多个参数运行 55 | 56 | #### 使用 CNB 镜像 57 | 58 | ```bash 59 | rime-wanxiang-update-linux --mirror cnb 60 | ``` 61 | 62 | #### 更新全部内容 63 | 64 | ```bash 65 | rime-wanxiang-update-linux --schema pro --fuzhu zrm --dict --gram 66 | ``` 67 | 68 | #### 只更新方案文件 69 | 70 | ```bash 71 | rime-wanxiang-update-linux --schema pro --fuzhu zrm 72 | ``` 73 | 74 | #### 只更新词典文件 75 | 76 | ```bash 77 | rime-wanxiang-update-linux --dict --fuzhu zrm 78 | ``` 79 | 80 | #### 只更新语法模型 81 | 82 | ```bash 83 | rime-wanxiang-update-linux --gram 84 | ``` 85 | 86 | ### 高级用法 87 | 88 | #### 传入 DEPLOY_DIR 与 inputime 89 | 90 | 脚本还支持直接传入部署目录,这样可以避免修改脚本,方便更新脚本自身 91 | 以下是一个示例 92 | 93 | ```bash 94 | rime-wanxiang-update-linux --depdir "$HOME/.local/share/fcitx5/rime" 95 | ``` 96 | 97 | 脚本也可以传入输入引擎,这可以实现 Rime 的自动部署 98 | 以下是一个示例 99 | 100 | ```bash 101 | rime-wanxiang-update-linux --inputime fcitx5 102 | ``` 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rime 万象更新工具 2 | 3 | ## 项目简介 4 | 5 | 本工具用于自动更新Rime输入法的万象方案、词库和模型,支持Windows、macOS和Linux系统。 6 | 7 | ## 工具版本计划 8 | 9 | - [x] [Windows版本](./Windows/README.md) 10 | - [x] [macOS版本](./Mac/README.md) 11 | - [x] [Linux版本](./Linux/README.md) 12 | - [x] [iOS版本](./iOS/README.md) 13 | - [x] [Android版本(同文、小企鹅导入包通用构建脚本)](./Android/README.MD) 14 | 15 | ### Python-全平台版本 16 | 17 | - [x] [Python版本](./Python-全平台版本/README.md) 18 | - 下载链接:[rime-wanxiang-update-win-mac-ios-android.py](https://github.com/expoli/rime-wanxiang-update-tools/releases/latest/download/rime-wanxiang-update-win-mac-ios-android.py) 19 | 20 | ### Windows版本 21 | 22 | - **版本区别** 23 | - **执行环境**: PowerShell 运行环境 Windows 10 自带,Python 需要自己安装 Python 环境 24 | - **运行方式**: PowerShell版本直接双击运行;Python版本需要命令行执行 25 | - **功能实现**: 两个版本功能相同,但Python版本更易于跨平台移植 26 | - Python 版本支持 GitHub 镜像加速 27 | 28 | - [x] [PowerShell版本](./Windows/PowerShell/README.md) 29 | - 下载链接:[按需下载万象方案-词库-模型(GBK版本:文件名:**rime-wanxiang-update-windows.ps1**)](https://github.com/expoli/rime-wanxiang-update-tools/releases/latest/download/rime-wanxiang-update-windows.ps1) 30 | - 下载链接:[按需下载万象方案-词库-模型(UTF-8版本:文件名:**rime-wanxiang-update-windows-utf-8.ps1**](https://github.com/expoli/rime-wanxiang-update-tools/releases/latest/download/rime-wanxiang-update-windows-utf-8.ps1) 31 | - [x] [Python版本](./Python-全平台版本/README.md) 32 | - 下载链接:[rime-wanxiang-update-win-mac-ios-android.py](https://github.com/expoli/rime-wanxiang-update-tools/releases/latest/download/rime-wanxiang-update-win-mac-ios-android.py) 33 | 34 | ### Linux版本 35 | 36 | - [x] [Shell版本](./Linux/Shell/README.md) 37 | - 下载链接:[Linux/Shell/wanxiang-update](https://github.com/expoli/rime-wanxiang-update-tools/releases/latest/download/linux-wanxiang-update) 38 | 39 | ### Mac版本 40 | 41 | - [x] [Python版本](./Python-全平台版本/README.md) 42 | - [x] [Shell版本](./Mac/Shell/README.md) 43 | 44 | ### iOS版本 45 | 46 | - [x] [Python版本](./Python-全平台版本/README.md) 47 | - [x] [Shortcuts版本](./iOS/Shortcuts/README.md) 48 | 49 | ## 使用说明 50 | 51 | 1. 选择对应的系统版本 52 | 2. 按照说明文档进行操作 53 | 3. 工具将自动完成更新 54 | 4. Windows 默认不支持无签名的 ps 脚本运行,如果右键运行失败,请在终端中运行,如果提示如下错误: 55 | 56 | ```PowerShell 57 | C:\Users\12418\Desktop\按需下载万象方案-词库-模型.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅 https:/go.microsoft.com/fwlink/?LinkID=135170 中的 about_Executio 58 | n_Policies。 59 | 所在位置 行:1 字符: 1 60 | + C:\Users\12418\Desktop\按需下载万象方案-词库-模型.ps1 61 | + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 62 | + CategoryInfo : SecurityError: (:) [],PSSecurityException 63 | + FullyQualifiedErrorId : UnauthorizedAccess 64 | ``` 65 | 66 | 请在终端中运行以下命令,然后再运行脚本即可。 67 | 68 | ```PowerShell 69 | Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope CurrentUser 70 | ``` 71 | 72 | ## 许可证 73 | 74 | MIT License 75 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | create-release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Get commits between tags 18 | id: commits 19 | run: | 20 | PREV_TAG=$(git tag --list "v[0-9]*" --sort=-v:refname | grep -Ev "rc|beta|alpha" | head -n 1) 21 | CURRENT_TAG=${{ github.ref_name }} 22 | if [ "$PREV_TAG" = "$CURRENT_TAG" ]; then 23 | PREV_TAG=$(git tag --list "v[0-9]*" --sort=-v:refname | grep -Ev "rc|beta|alpha" | head -n 2 | tail -n 1) 24 | fi 25 | echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT 26 | 27 | # Correctly handle multi-line output 28 | commits=$(git log --pretty=format:'- %s' $PREV_TAG..HEAD | sed 's/[^[:print:]]//g') 29 | echo "commits<> $GITHUB_OUTPUT 30 | echo "$commits" >> $GITHUB_OUTPUT 31 | echo "EOF" >> $GITHUB_OUTPUT 32 | 33 | - name: Replace version tag 34 | env: 35 | CURRENT_TAG: ${{ github.ref_name }} 36 | run: | 37 | # 使用井号(#)作为分隔符避免路径冲突 38 | find . -type f -name '*.ps1' -exec sed -i "s#DEFAULT_UPDATE_TOOLS_VERSION_TAG#$CURRENT_TAG#g" {} + 39 | find . -type f -name '*.py' -exec sed -i "s#DEFAULT_UPDATE_TOOLS_VERSION_TAG#$CURRENT_TAG#g" {} + 40 | find . -type f -name '*.sh' -exec sed -i "s#DEFAULT_UPDATE_TOOLS_VERSION_TAG#$CURRENT_TAG#g" {} + 41 | find Linux/ -type f -name 'wanxiang-update' -exec sed -i "s#DEFAULT_UPDATE_TOOLS_VERSION_TAG#$CURRENT_TAG#g" {} + 42 | 43 | - name: Rename files 44 | run: | 45 | find Windows/ -type f -name '*.ps1' -exec bash -c 'mv "$0" "$(dirname "$0")/$(basename "$0" | sed "s/按需下载万象方案-词库-模型/rime-wanxiang-update-windows/")"' {} \; 46 | find Windows/ -type f -name '*.py' -exec bash -c 'mv "$0" "$(dirname "$0")/$(basename "$0" | sed "s/按需下载万象方案-词库-模型/rime-wanxiang-update-windows/")"' {} \; 47 | iconv -f utf-8 -t gbk Windows/PowerShell/rime-wanxiang-update-windows-utf-8.ps1 > Windows/PowerShell/rime-wanxiang-update-windows.ps1 48 | mv Linux/Shell/wanxiang-update Linux/Shell/rime-wanxiang-update-linux 49 | mv Mac/Shell/wanxiang-update.sh Mac/Shell/rime-wanxiang-update-macos.sh 50 | mv Python-全平台版本/Python/万象下载更新.py Python-全平台版本/Python/rime-wanxiang-update-win-mac-ios-android.py 51 | 52 | - name: Create Release 53 | uses: softprops/action-gh-release@v1 54 | with: 55 | tag_name: ${{ github.ref_name }} 56 | name: ${{ github.ref_name }} 57 | body: | 58 | Changes since ${{ steps.commits.outputs.prev_tag }}: 59 | ${{ steps.commits.outputs.commits }} 60 | prerelease: ${{ contains(github.ref_name, '-rc') }} 61 | files: | 62 | Windows/PowerShell/rime-wanxiang-update-windows.ps1 63 | Windows/PowerShell/rime-wanxiang-update-windows-utf-8.ps1 64 | Linux/Shell/rime-wanxiang-update-linux 65 | Mac/Shell/rime-wanxiang-update-macos.sh 66 | Python-全平台版本/Python/rime-wanxiang-update-win-mac-ios-android.py 67 | -------------------------------------------------------------------------------- /Android/Fcitx5-For-Android/小企鹅导入包构建脚本.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import json 4 | import subprocess 5 | import sys 6 | import tempfile 7 | import time 8 | import argparse 9 | from pathlib import Path 10 | import winreg 11 | 12 | def create_zip_package(source_dir, output_zip, model_path=None): 13 | """ 14 | 创建符合要求的ZIP包,跳过.git/.github/build目录和.gitignore/.gitattributes文件 15 | 可选添加模型目录内容到ZIP包中 16 | """ 17 | # 验证源目录是否存在 18 | if not os.path.isdir(source_dir): 19 | print(f"错误: 源目录不存在 - {source_dir}") 20 | sys.exit(1) 21 | 22 | # 如果提供了模型目录,验证其是否存在 23 | if model_path and not os.path.exists(model_path): 24 | print(f"错误: 模型文件不存在 - {model_path}") 25 | sys.exit(1) 26 | 27 | # 创建临时工作目录 28 | with tempfile.TemporaryDirectory() as temp_dir: 29 | temp_dir_path = Path(temp_dir) 30 | 31 | # 创建目标目录结构 32 | dest_rime = temp_dir_path / "external" / "data" / "rime" 33 | os.makedirs(dest_rime, exist_ok=True) 34 | 35 | # 定义要跳过的目录和文件 36 | skip_dirs = {'.git', '.github', 'build'} # 使用集合提高查找效率 37 | skip_files = {'.gitignore', '.gitattributes'} 38 | 39 | # ========== 步骤1: 复制源目录内容 ========== 40 | print(f"正在复制源目录文件: {source_dir} -> {dest_rime}") 41 | 42 | for root, dirs, files in os.walk(source_dir): 43 | # 从当前遍历中移除要跳过的目录 44 | dirs[:] = [d for d in dirs if d not in skip_dirs] 45 | 46 | # 计算目标路径 47 | rel_path = os.path.relpath(root, source_dir) 48 | dest_path = dest_rime / rel_path 49 | 50 | # 创建目标子目录 51 | os.makedirs(dest_path, exist_ok=True) 52 | 53 | # 复制文件(跳过指定文件) 54 | print(f" 正在复制目录: {root} 到 {dest_path}") # 简化复制输出 55 | for file in files: 56 | if file in skip_files: 57 | print(f" 跳过文件: {os.path.join(root, file)}") 58 | continue 59 | 60 | src_file = os.path.join(root, file) 61 | dst_file = dest_path / file 62 | shutil.copy2(src_file, dst_file) 63 | # print(f" 已复制: {src_file} -> {dst_file}") # 简化复制输出 64 | 65 | # ========== 步骤2: 可选添加模型目录内容 ========== 66 | if model_path: 67 | # 计算目标路径 68 | model_file_name = os.path.basename(model_path) 69 | src_file = model_path 70 | dst_file = dest_rime / model_file_name 71 | print(f"\n正在添加模型文件: {src_file} -> {dst_file}") 72 | # 如果文件已存在,覆盖它 73 | if os.path.exists(dst_file): 74 | print(f" 覆盖: {dst_file}") 75 | shutil.copy2(src_file, dst_file) 76 | print(f" 已添加: {src_file} -> {dst_file}") 77 | 78 | # ========== 步骤3: 创建元数据文件 ========== 79 | current_time_ms = int(time.time() * 1000) 80 | metadata = { 81 | "packageName": "org.fcitx.fcitx5.android", 82 | "versionCode": 92, 83 | "versionName": "0.1.1-14-gdf4e1349-release", 84 | "exportTime": current_time_ms 85 | } 86 | metadata_path = temp_dir_path / "metadata.json" 87 | with open(metadata_path, 'w', encoding='utf-8') as f: 88 | json.dump(metadata, f, indent=2) 89 | print(f"\n已创建元数据文件: {metadata_path}") 90 | print(f" 导出时间戳: {current_time_ms} ({time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(current_time_ms/1000))})") 91 | 92 | # ========== 步骤4: 创建ZIP包 ========== 93 | # 确保输出目录存在 94 | output_dir = os.path.dirname(output_zip) 95 | if output_dir and not os.path.exists(output_dir): 96 | os.makedirs(output_dir) 97 | 98 | print(f"\n正在创建ZIP包: {output_zip}") 99 | base_dir = temp_dir_path # ZIP包的根目录 100 | 101 | # 创建临时ZIP文件 102 | temp_zip = shutil.make_archive( 103 | base_name=os.path.splitext(output_zip)[0], 104 | format='zip', 105 | root_dir=base_dir, 106 | base_dir='.', # 包含整个目录内容 107 | verbose=True 108 | ) 109 | 110 | # 移动到目标位置 111 | shutil.move(temp_zip, output_zip) 112 | print(f"\nZIP包创建成功: {output_zip}") 113 | print(f"源目录中的.git/.github/build文件夹和.gitignore/.gitattributes文件保持原样未修改") 114 | if model_path: 115 | print(f"模型文件内容已添加到ZIP包中") 116 | 117 | # 照着win-mac-ios融合版抄来的代码 118 | if sys.platform == 'win32': 119 | def terminate_processes(): 120 | """组合式进程终止策略""" 121 | if not graceful_stop(): # 先尝试优雅停止 122 | hard_stop() # 失败则强制终止 123 | 124 | def graceful_stop(): 125 | """优雅停止服务""" 126 | 127 | try: 128 | with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Rime\Weasel") as key: 129 | exe, _ = winreg.QueryValueEx(key, "ServerExecutable") 130 | root, _ = winreg.QueryValueEx(key, "WeaselRoot") 131 | value = os.path.join(root, exe) 132 | subprocess.run( 133 | [value, "/q"], 134 | check=True, 135 | stdout=subprocess.DEVNULL, 136 | stderr=subprocess.DEVNULL, 137 | creationflags=subprocess.CREATE_NO_WINDOW 138 | ) 139 | print(f"{exe} 服务已优雅退出") 140 | return True 141 | except subprocess.CalledProcessError as e: 142 | print(f"优雅退出失败: {e}") 143 | return False 144 | except (FileNotFoundError, PermissionError, OSError) as e: 145 | print(f"优雅退出失败: {e}") 146 | return False 147 | except Exception as e: 148 | print(f"未知错误: {str(e)}") 149 | return False 150 | 151 | def hard_stop(): 152 | """强制终止保障""" 153 | print("强制终止残留进程") 154 | for _ in range(3): 155 | subprocess.run(["taskkill", "/IM", "WeaselServer.exe", "/F"], 156 | shell=True, stderr=subprocess.DEVNULL) 157 | subprocess.run(["taskkill", "/IM", "WeaselDeployer.exe", "/F"], 158 | shell=True, stderr=subprocess.DEVNULL) 159 | time.sleep(0.5) 160 | print("进程清理完成") 161 | 162 | def main(): 163 | parser = argparse.ArgumentParser(description="打包 Rime 文件目录为 zip 包") 164 | 165 | parser.add_argument("--source", "-s", required=True, help="源目录") 166 | parser.add_argument("--output", "-o", required=True, help="输出 zip 路径") 167 | parser.add_argument("--model", "-m", help="模型目录(可选)", default=None) 168 | 169 | args = parser.parse_args() 170 | terminate_processes() # 在复制前终止相关进程 171 | create_zip_package(args.source, args.output, args.model) 172 | 173 | 174 | 175 | 176 | if __name__ == "__main__": 177 | main() 178 | -------------------------------------------------------------------------------- /Linux/Shell/wanxiang-update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | #### 配置 Rime 部署目录 #### 6 | # 支持相对路径、绝对路径、拓展变量 7 | # 例如 "/home/user/.local/share/fcitx5/rime" 8 | # 例如 "$HOME/.local/share/fcitx5/rime" 9 | # 例如 "${XDG_DATA_HOME:-$HOME/.local/share}/fcitx5/rime" 10 | 11 | DEPLOY_DIR="" 12 | 13 | ######### 配置结束 ######### 14 | 15 | # 全局变量 16 | CNB_API="https://cnb.cool/amzxyz/rime-wanxiang/-/releases" 17 | SCHEMA_API="https://api.github.com/repos/amzxyz/rime_wanxiang/releases" 18 | GRAM_API="https://api.github.com/repos/amzxyz/RIME-LMDG/releases" 19 | TOOLS_API="https://api.github.com/repos/expoli/rime-wanxiang-update-tools/releases" 20 | FUZHU_LIST=("base" "flypy" "hanxin" "moqi" "tiger" "wubi" "zrm") 21 | TEMP_DIR=$(mktemp -d /tmp/wanxiang-update-XXXXXX) 22 | UPDATE_TOOLS_VERSION="DEFAULT_UPDATE_TOOLS_VERSION_TAG" 23 | 24 | # 日志与错误处理 25 | log() { 26 | local red="\033[0;31m" green="\033[0;32m" yellow="\033[0;33m" nc="\033[0m" 27 | local level="$1" color="$nc" 28 | case "$level" in 29 | INFO) color="$green" ;; 30 | WARN) color="$yellow" ;; 31 | ERROR) color="$red" ;; 32 | esac 33 | shift 34 | printf "${color}[%s] %s${nc}\n" "$level" "$*" 35 | } 36 | error_exit() { 37 | log ERROR "$*" 38 | cleanup 39 | exit 1 40 | } 41 | cleanup() { 42 | if [[ -d "$TEMP_DIR" ]]; then 43 | rm -rf "$TEMP_DIR" || log WARN "清理缓存文件失败" 44 | fi 45 | } 46 | deps_check() { 47 | for _cmd in curl jq unzip; do 48 | command -v "$_cmd" >/dev/null || error_exit "缺少必要依赖:$_cmd" 49 | done 50 | } 51 | fuzhu_check() { 52 | local fuzhu_check="$1" 53 | for _fuzhu in "${FUZHU_LIST[@]}"; do 54 | if [[ "$fuzhu_check" == "$_fuzhu" ]]; then 55 | return 0 56 | fi 57 | done 58 | return 1 59 | } 60 | script_check() { 61 | local mirror="$1" 62 | if [[ "$UPDATE_TOOLS_VERSION" =~ ^"DEFAULT" ]]; then 63 | log WARN "您似乎正在使用源文件!" 64 | log WARN "请从 Release 页面下载正式版!" 65 | error_exit "终止操作" 66 | fi 67 | log INFO "工具当前版本 $UPDATE_TOOLS_VERSION" 68 | if [[ "$mirror" == "github" ]]; then 69 | # 检查 GitHub 连接状态 70 | log INFO "正在检查 GitHub 连接状态" 71 | if ! curl -sL --connect-timeout 5 "https://api.github.com" >/dev/null; then 72 | error_exit "您似乎无法连接到 GitHub API, 请检查您的网络" 73 | elif ! curl -sL --connect-timeout 5 "https://github.com" >/dev/null; then 74 | error_exit "您似乎无法连接到 GitHub, 请检查您的网络" 75 | fi 76 | log INFO "正在检查本工具是否存在更新" 77 | local local_version remote_version 78 | local_version="$UPDATE_TOOLS_VERSION" 79 | remote_version=$( 80 | curl -sL --connect-timeout 10 $TOOLS_API | 81 | jq -r '.[].tag_name' | grep -vE "rc" | sort -rV | head -n 1 82 | ) 83 | if [[ "$remote_version" > "$local_version" ]]; then 84 | log WARN "检测到工具最新版本为: $remote_version, 建议更新后继续" 85 | log WARN "https://github.com/expoli/rime-wanxiang-update-tools/releases/download/$remote_version/rime-wanxiang-update-linux" 86 | else 87 | log INFO "工具已是最新版本" 88 | fi 89 | elif [[ "$mirror" == "cnb" ]]; then 90 | log WARN "由于您正在使用镜像,无法检查本工具是否存在更新" 91 | fi 92 | } 93 | 94 | get_info() { 95 | local mirror="$1" version="$2" name="$3" info 96 | if [[ "$mirror" == "github" ]]; then 97 | info=$( 98 | jq -r --arg version "$version" --arg name "$name" '.[] | 99 | select( .tag_name == $version ) | .assets.[] | 100 | select( .name | test( $name ) )' "$TEMP_DIR/github_$name.json" 101 | ) 102 | echo "$info" 103 | elif [[ "$mirror" == "cnb" ]]; then 104 | info=$( 105 | jq -r --arg version "refs/tags/$version" --arg name "$name" '.releases.[] | 106 | select( .tag_ref == $version ) | .assets[] | 107 | select( .name | test( $name ) )' "$TEMP_DIR/cnb.json" 108 | ) 109 | echo "$info" 110 | fi 111 | } 112 | 113 | update_schema() { 114 | local mirror="$1" fuzhu="$2" gram="$3" 115 | # 缓存 API 响应 116 | if [[ "$mirror" == "github" ]]; then 117 | if [[ ! -f "$TEMP_DIR/github_$fuzhu.json" ]]; then 118 | if ! curl -sL -H "Accept: application/vnd.github.v3+json" \ 119 | --connect-timeout 10 "$SCHEMA_API" >"$TEMP_DIR/github_$fuzhu.json"; then 120 | error_exit "连接到 GitHub API 失败,您可能需要检查网络" 121 | fi 122 | fi 123 | elif [[ "$mirror" == "cnb" ]]; then 124 | if [[ ! -f "$TEMP_DIR/cnb.json" ]]; then 125 | if ! curl -sL -H "accept: application/vnd.cnb.web+json" \ 126 | --connect-timeout 10 "$CNB_API" >"$TEMP_DIR/cnb.json"; then 127 | error_exit "连接到 CNB 失败,您可能需要检查网络" 128 | fi 129 | fi 130 | fi 131 | # 获取本地版本号 132 | local local_version remote_version 133 | if [[ -f "$DEPLOY_DIR/lua/wanxiang.lua" ]]; then 134 | local_version=$(grep "wanxiang.version" "$DEPLOY_DIR/lua/wanxiang.lua" | awk -F '"' '{print $2}') 135 | [[ "$local_version" == v* ]] || local_version="v$local_version" 136 | else 137 | local_version="v0" 138 | fi 139 | # 获取远程版本号 140 | if [[ "$mirror" == "github" ]]; then 141 | remote_version=$( 142 | jq -r '.[].tag_name' "$TEMP_DIR/github_$fuzhu.json" | 143 | grep -vE "dict-nightly" | sort -rV | head -n 1 144 | ) 145 | elif [[ "$mirror" == "cnb" ]]; then 146 | remote_version=$( 147 | jq -r '.releases.[].tag_ref' \ 148 | "$TEMP_DIR/cnb.json" | grep -vE "model" | sort -rV | head -n 1 149 | ) 150 | remote_version="${remote_version#"refs/tags/"}" 151 | fi 152 | [[ "$remote_version" == v* ]] || remote_version="v$remote_version" 153 | if [[ "$remote_version" > "$local_version" ]]; then 154 | log INFO "远程方案文件版本号为 $remote_version, 以下内容为更新日志" 155 | local changelog 156 | if [[ "$mirror" == "github" ]]; then 157 | changelog=$( 158 | jq -r --arg version "$remote_version" '.[] | 159 | select( .tag_name == $version ) | .body' "$TEMP_DIR/github_$fuzhu.json" 160 | ) 161 | elif [[ "$mirror" == "cnb" ]]; then 162 | changelog=$( 163 | jq -r --arg version "refs/tags/$remote_version" '.releases.[] | 164 | select( .tag_ref == $version ) | .body' "$TEMP_DIR/cnb.json" 165 | ) 166 | fi 167 | echo -e "$changelog" | sed -n '/## 📝 更新日志/,/## 🚀 下载引导/p' | head -n -1 168 | sleep 3 169 | log INFO "开始更新方案文件,正在下载文件" 170 | local schemaurl schemaname local_size remote_size 171 | if [[ "$mirror" == "github" ]]; then 172 | schemaurl=$(get_info "$mirror" "$remote_version" "$fuzhu" | jq -r '.browser_download_url') 173 | elif [[ "$mirror" == "cnb" ]]; then 174 | schemaurl=$(get_info "$mirror" "$remote_version" "$fuzhu" | jq -r '.path') 175 | schemaurl="https://cnb.cool$schemaurl" 176 | fi 177 | schemaname=$(get_info "$mirror" "$remote_version" "$fuzhu" | jq -r '.name') 178 | curl -L --connect-timeout 10 -o "$TEMP_DIR/$schemaname" "$schemaurl" 179 | log INFO "正在验证文件完整性" 180 | local_size=$(stat -c %s "$TEMP_DIR/$schemaname") 181 | if [[ "$mirror" == "github" ]]; then 182 | remote_size=$(get_info "$mirror" "$remote_version" "$fuzhu" | jq -r '.size') 183 | elif [[ "$mirror" == "cnb" ]]; then 184 | remote_size=$(get_info "$mirror" "$remote_version" "$fuzhu" | jq -r '.size_in_byte') 185 | fi 186 | if [[ "$local_size" != "$remote_size" ]]; then 187 | log ERROR "期望文件大小: $remote_size, 实际文件大小: $local_size" 188 | error_exit "方案文件下载出错,请重试!" 189 | fi 190 | log INFO "验证成功,开始更新方案文件" 191 | unzip -q "$TEMP_DIR/$schemaname" -d "$TEMP_DIR/${schemaname%.zip}" 192 | for _file in "简纯+.trime.yaml" "custom_phrase.txt" "squirrel.yaml" "weasel.yaml"; do 193 | if [[ -f "$TEMP_DIR/${schemaname%.zip}/$_file" ]]; then 194 | rm -r "$TEMP_DIR/${schemaname%.zip}/${_file:?}" 195 | fi 196 | done 197 | local exclude_file 198 | while IFS= read -r _line; do 199 | if [[ "$_line" != \#* ]]; then 200 | exclude_file="$_line" 201 | if [[ ! -e "$DEPLOY_DIR/$exclude_file" ]]; then 202 | log WARN "项目 $DEPLOY_DIR/$exclude_file 不存在,跳过备份!" 203 | else 204 | cp -rf "$DEPLOY_DIR/$exclude_file" "$TEMP_DIR/${schemaname%.zip}/$exclude_file" 205 | fi 206 | fi 207 | done <"$DEPLOY_DIR/custom/user_exclude_file.txt" 208 | # 单独处理语法模型 209 | [[ "$gram" == "true" ]] || cp -rf "$DEPLOY_DIR/wanxiang-lts-zh-hans.gram" \ 210 | "$TEMP_DIR/${schemaname%.zip}/wanxiang-lts-zh-hans.gram" 211 | rm -rf "${DEPLOY_DIR:?}" 212 | cp -rf "$TEMP_DIR/${schemaname%.zip}" "$DEPLOY_DIR" 213 | log INFO "方案文件更新成功" 214 | else 215 | log INFO "远程方案文件版本号为 $remote_version" 216 | log INFO "本地方案文件版本号为 $local_version, 您目前无需更新它" 217 | fi 218 | } 219 | update_dict() { 220 | local mirror="$1" fuzhu="$2" 221 | # 缓存 API 响应 222 | if [[ "$mirror" == "github" ]]; then 223 | if [[ ! -f "$TEMP_DIR/github_$fuzhu.json" ]]; then 224 | if ! curl -sL -H "Accept: application/vnd.github.v3+json" \ 225 | --connect-timeout 10 "$SCHEMA_API" >"$TEMP_DIR/github_$fuzhu.json"; then 226 | error_exit "连接到 GitHub API 失败,您可能需要检查网络" 227 | fi 228 | fi 229 | elif [[ "$mirror" == "cnb" ]]; then 230 | if [[ ! -f "$TEMP_DIR/cnb.json" ]]; then 231 | if ! curl -sL -H "accept: application/vnd.cnb.web+json" \ 232 | --connect-timeout 10 "$CNB_API" >"$TEMP_DIR/cnb.json"; then 233 | error_exit "连接到 CNB 失败,您可能需要检查网络" 234 | fi 235 | fi 236 | fi 237 | local local_date remote_date 238 | if [[ -f "$DEPLOY_DIR/dicts/chengyu.txt" ]]; then 239 | local_date=$(stat -c %Z "$DEPLOY_DIR/dicts/chengyu.txt") 240 | else 241 | local_date=0 242 | fi 243 | if [[ "$mirror" == "github" ]]; then 244 | remote_date=$(get_info "$mirror" "dict-nightly" "$fuzhu" | jq -r '.updated_at') 245 | elif [[ "$mirror" == "cnb" ]]; then 246 | remote_date=$(get_info "$mirror" "v1.0.0" "$fuzhu" | jq -r '.updated_at') 247 | fi 248 | remote_date=$(date -d "$remote_date" +%s) 249 | if [[ $remote_date -gt $local_date ]]; then 250 | log INFO "正在下载最新词典文件" 251 | local dicturl dictname local_size remote_size 252 | if [[ "$mirror" == "github" ]]; then 253 | dicturl=$(get_info "$mirror" "dict-nightly" "$fuzhu" | jq -r '.browser_download_url') 254 | dictname=$(get_info "$mirror" "dict-nightly" "$fuzhu" | jq -r '.name') 255 | elif [[ "$mirror" == "cnb" ]]; then 256 | dicturl=$(get_info "$mirror" "v1.0.0" "$fuzhu" | jq -r '.path') 257 | dicturl="https://cnb.cool$dicturl" 258 | dictname=$(get_info "$mirror" "v1.0.0" "$fuzhu" | jq -r '.name') 259 | fi 260 | curl -L --connect-timeout 10 -o "$TEMP_DIR/$dictname" "$dicturl" 261 | log INFO "正在验证文件完整性" 262 | local_size=$(stat -c %s "$TEMP_DIR/$dictname") 263 | if [[ "$mirror" == "github" ]]; then 264 | remote_size=$(get_info "$mirror" "dict-nightly" "$fuzhu" | jq -r '.size') 265 | elif [[ "$mirror" == "cnb" ]]; then 266 | remote_size=$(get_info "$mirror" "v1.0.0" "$fuzhu" | jq -r '.size_in_byte') 267 | fi 268 | if [[ "$local_size" != "$remote_size" ]]; then 269 | log ERROR "期望文件大小: $remote_size, 实际文件大小: $local_size" 270 | error_exit "词典文件下载出错,请重试!" 271 | fi 272 | log INFO "验证成功,开始更新词典文件" 273 | unzip -q "$TEMP_DIR/$dictname" -d "$TEMP_DIR" 274 | dictname="${dictname:2}" && dictname="${dictname%.zip}" 275 | cp -rf "$TEMP_DIR/$dictname"/* "$DEPLOY_DIR/dicts" 276 | log INFO "词典文件更新成功" 277 | else 278 | remote_date=$(date -d "@$remote_date" +"%Y-%m-%d %H:%M:%S") 279 | log INFO "远程词典文件最后更新于 $remote_date" 280 | local_date=$(date -d "@$local_date" +"%Y-%m-%d %H:%M:%S") 281 | log INFO "本地词典文件最后更新于 $local_date, 您目前无需更新它" 282 | fi 283 | } 284 | update_gram() { 285 | local mirror="$1" 286 | # 缓存 API 响应 287 | if [[ "$mirror" == "github" ]]; then 288 | if [[ ! -f "$TEMP_DIR/github_gram.json" ]]; then 289 | if ! curl -sL -H "Accept: application/vnd.github.v3+json" \ 290 | --connect-timeout 10 "$GRAM_API" >"$TEMP_DIR/github_gram.json"; then 291 | error_exit "连接到 GitHub API 失败,您可能需要检查网络" 292 | fi 293 | fi 294 | elif [[ "$mirror" == "cnb" ]]; then 295 | if [[ ! -f "$TEMP_DIR/cnb.json" ]]; then 296 | if ! curl -sL -H "accept: application/vnd.cnb.web+json" \ 297 | --connect-timeout 10 "$CNB_API" >"$TEMP_DIR/cnb.json"; then 298 | error_exit "连接到 CNB 失败,您可能需要检查网络" 299 | fi 300 | fi 301 | fi 302 | local local_date remote_date gramname="wanxiang-lts-zh-hans.gram" 303 | if [[ -f "$DEPLOY_DIR/$gramname" ]]; then 304 | local_date=$(stat -c %Z "$DEPLOY_DIR/$gramname") 305 | else 306 | local_date=0 307 | fi 308 | if [[ "$mirror" == "github" ]]; then 309 | remote_date=$(get_info "$mirror" "LTS" "gram" | jq -r '.updated_at') 310 | elif [[ "$mirror" == "cnb" ]]; then 311 | remote_date=$(get_info "$mirror" "model" "gram" | jq -r '.updated_at') 312 | fi 313 | remote_date=$(date -d "$remote_date" +%s) 314 | if [[ $remote_date -gt $local_date ]]; then 315 | log INFO "正在下载最新语法模型" 316 | local gramurl local_size remote_size 317 | if [[ "$mirror" == "github" ]]; then 318 | gramurl=$(get_info "$mirror" "LTS" "gram" | jq -r '.browser_download_url') 319 | elif [[ "$mirror" == "cnb" ]]; then 320 | gramurl=$(get_info "$mirror" "model" "gram" | jq -r '.path') 321 | gramurl="https://cnb.cool$gramurl" 322 | fi 323 | curl -L --connect-timeout 10 -o "$TEMP_DIR/$gramname" "$gramurl" 324 | log INFO "正在验证文件完整性" 325 | local_size=$(stat -c %s "$TEMP_DIR/$gramname") 326 | if [[ "$mirror" == "github" ]]; then 327 | remote_size=$(get_info "$mirror" "LTS" "gram" | jq -r '.size') 328 | elif [[ "$mirror" == "cnb" ]]; then 329 | remote_size=$(get_info "$mirror" "model" "gram" | jq -r '.size_in_byte') 330 | fi 331 | if [[ "$local_size" != "$remote_size" ]]; then 332 | log ERROR "期望文件大小: $remote_size, 实际文件大小: $local_size" 333 | error_exit "语法模型下载出错,请重试!" 334 | fi 335 | log INFO "验证成功,开始更新语法模型" 336 | cp -rf "$TEMP_DIR/$gramname" "${DEPLOY_DIR}/$gramname" 337 | log INFO "语法模型更新成功" 338 | else 339 | remote_date=$(date -d "@$remote_date" +"%Y-%m-%d %H:%M:%S") 340 | log INFO "远程语法模型最后更新于 $remote_date" 341 | local_date=$(date -d "@$local_date" +"%Y-%m-%d %H:%M:%S") 342 | log INFO "本地语法模型最后更新于 $local_date, 您目前无需更新它" 343 | fi 344 | } 345 | 346 | main() { 347 | # 脚本退出清理临时目录 348 | trap cleanup EXIT 349 | # 欢迎语 350 | log INFO "欢迎使用万象方案更新助手" 351 | # 检查是否为root用户 352 | if [[ "$EUID" -eq 0 ]]; then 353 | error_exit "请不要使用 root 身份运行该脚本!" 354 | fi 355 | # 检查必要的依赖 356 | deps_check 357 | # 处理用户输入 358 | local mirror="" depdir="" inputime="" schema="" fuzhu="" dict="false" gram="false" 359 | # 解析命令行参数 360 | while [[ "$#" -gt 0 ]]; do 361 | case $1 in 362 | --mirror) 363 | if [[ -n "$mirror" ]]; then 364 | error_exit "选项 mirror 需要参数!" 365 | else 366 | shift 367 | fi 368 | if [[ "$1" != "cnb" ]]; then 369 | error_exit "选项 mirror 的参数目前只能为 cnb" 370 | else 371 | mirror="$1" 372 | fi 373 | ;; 374 | --depdir) 375 | if [[ -n "$depdir" ]]; then 376 | error_exit "选项 depdir 需要参数!" 377 | else 378 | shift 379 | fi 380 | DEPLOY_DIR="$1" 381 | ;; 382 | --inputime) 383 | if [[ -n "$inputime" ]]; then 384 | error_exit "选项 inputime 需要参数!" 385 | else 386 | shift 387 | fi 388 | if [[ "$1" != "fcitx5" && "$1" != "ibus" ]]; then 389 | error_exit "选项 inputime 的参数只能为 fcitx5 或 ibus" 390 | else 391 | inputime="$1" 392 | fi 393 | ;; 394 | --schema) 395 | if [[ -n "$schema" ]]; then 396 | error_exit "选项 schema 需要参数!" 397 | else 398 | shift 399 | fi 400 | if [[ "$1" != "base" && "$1" != "pro" ]]; then 401 | error_exit "选项 schema 的参数只能为 base 或 pro" 402 | else 403 | schema="$1" 404 | fi 405 | ;; 406 | --fuzhu) 407 | if [[ -n "$fuzhu" ]]; then 408 | error_exit "选项 fuzhu 需要参数!" 409 | else 410 | shift 411 | fi 412 | if fuzhu_check "$1"; then 413 | fuzhu="$1" 414 | else 415 | error_exit "选项 fuzhu 的参数只能为 ${FUZHU_LIST[*]} 其中之一" 416 | fi 417 | ;; 418 | --dict) 419 | dict="true" 420 | ;; 421 | --gram) 422 | gram="true" 423 | ;; 424 | *) 425 | log WARN "您可能错误的使用了该脚本" 426 | log WARN "请前往 GitHub 页面阅读 Readme" 427 | log WARN "https://github.com/expoli/rime-wanxiang-update-tools/blob/main/Linux/Shell/README.md" 428 | error_exit "参数输入错误: $1" 429 | ;; 430 | esac 431 | shift 432 | done 433 | # 判断是否设置了部署目录 434 | if [[ -n "$DEPLOY_DIR" ]]; then 435 | if [[ ! -d "$DEPLOY_DIR" ]]; then 436 | log WARN "部署目录 $DEPLOY_DIR 不存在,您要创建它吗?" 437 | read -rp "请输入 YES 或 NO (区分大小写) " _check 438 | if [[ "$_check" == "YES" ]]; then 439 | log WARN "您真的要创建该目录吗?您确定您的设置正确吗?" 440 | read -rp "请输入 YES 或 NO (区分大小写) " _check_again 441 | [[ "$_check_again" == "YES" ]] || error_exit "用户终止操作" 442 | mkdir -p "$DEPLOY_DIR" 443 | else 444 | error_exit "用户终止操作" 445 | fi 446 | fi 447 | else 448 | error_exit "请设置部署目录!" 449 | fi 450 | # 排除项目列表文件是否存在 451 | if [[ -f "$DEPLOY_DIR/user_exclude_file.txt" ]]; then 452 | mv "$DEPLOY_DIR/user_exclude_file.txt" "$DEPLOY_DIR/custom/user_exclude_file.txt" 453 | sed -i 's/user_exclude_file\.txt/custom\/user_exclude_file\.txt/g' \ 454 | "$DEPLOY_DIR/custom/user_exclude_file.txt" 455 | fi 456 | if [[ ! -f "$DEPLOY_DIR/custom/user_exclude_file.txt" ]]; then 457 | log WARN "您没有设置排除项目列表!" 458 | log WARN "您需要创建的文件为 $DEPLOY_DIR/custom/user_exclude_file.txt" 459 | log WARN "请在该文件中写入您需要排除的项目,每行一个" 460 | error_exit "$DEPLOY_DIR/custom/user_exclude_file.txt 文件不存在" 461 | fi 462 | # 检查 schema 和 fuzhu 是否同时存在 463 | if [[ -n "$schema" && -z "$fuzhu" ]]; then 464 | error_exit "选项 schema 与选项 fuzhu 必须同时使用" 465 | fi 466 | # 检查 dict 和 fuzhu 是否同时存在 467 | if [[ "$dict" == "true" && -z "$fuzhu" ]]; then 468 | error_exit "选项 dict 与选项 fuzhu 必须同时使用" 469 | fi 470 | # 检查当 schema 为 base 时,fuzhu 是否也为 base 471 | if [[ "$schema" == "base" && "$fuzhu" != "base" ]]; then 472 | error_exit "当选项 schema 为 base 时,选项 fuzhu 必须为 base" 473 | fi 474 | [[ -n "$mirror" ]] || mirror="github" 475 | # 脚本自检 476 | script_check "$mirror" 477 | # 开始更新 478 | local needed_deploy="false" 479 | [[ -z "$schema" ]] || update_schema "$mirror" "$fuzhu" "$gram" && needed_deploy="true" 480 | [[ "$dict" == "false" ]] || update_dict "$mirror" "$fuzhu" && needed_deploy="true" 481 | [[ "$gram" == "false" ]] || update_gram "$mirror" && needed_deploy="true" 482 | # 自动部署 483 | if [[ "$needed_deploy" == "true" ]]; then 484 | if [[ "$inputime" == "fcitx5" ]]; then 485 | if command -v qdbus6 >/dev/null; then 486 | log INFO "更新完成。已检测到 Fcitx5 守护进程,正在自动部署" 487 | qdbus6 org.fcitx.Fcitx5 /controller org.fcitx.Fcitx.Controller1.SetConfig "fcitx://config/addon/rime/deploy" "" 488 | fi 489 | elif [[ "$inputime" == "ibus" ]]; then 490 | if command -v ibus-daemon >/dev/null; then 491 | log INFO "更新完成。已检测到 iBus 守护进程,正在自动部署" 492 | ibus-daemon -drx 493 | fi 494 | else 495 | # 提示用户重新进行部署 496 | log INFO "更新完成。请手动重新部署 rime" 497 | fi 498 | else 499 | log INFO "您目前无需更新" 500 | fi 501 | } 502 | 503 | main "$@" 504 | -------------------------------------------------------------------------------- /Mac/Shell/wanxiang-update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | #### 配置 Rime 输入法引擎 #### 6 | # 支持鼠须管或小企鹅 7 | # 例如 "fcitx5" 8 | # 例如 "squirrel" 9 | ENGINE="" 10 | 11 | ######### 配置结束 ######### 12 | 13 | # 全局变量 14 | CNB_API="https://cnb.cool/amzxyz/rime-wanxiang/-/releases" 15 | SCHEMA_API="https://api.github.com/repos/amzxyz/rime_wanxiang/releases" 16 | GRAM_API="https://api.github.com/repos/amzxyz/RIME-LMDG/releases" 17 | TOOLS_API="https://api.github.com/repos/expoli/rime-wanxiang-update-tools/releases" 18 | FUZHU_LIST=("base" "flypy" "hanxin" "moqi" "tiger" "wubi" "zrm") 19 | TEMP_DIR=$(mktemp -d /tmp/wanxiang-update-XXXXXX) 20 | UPDATE_TOOLS_VERSION="DEFAULT_UPDATE_TOOLS_VERSION_TAG" 21 | 22 | # 日志与错误处理 23 | log() { 24 | local red="\033[0;31m" green="\033[0;32m" yellow="\033[0;33m" nc="\033[0m" 25 | local level="$1" color="$nc" 26 | case "$level" in 27 | INFO) color="$green" ;; 28 | WARN) color="$yellow" ;; 29 | ERROR) color="$red" ;; 30 | esac 31 | shift 32 | printf "${color}[%s] %s${nc}\n" "$level" "$*" 33 | } 34 | 35 | # 获取当前脚本名称 36 | script_name=$(basename $0) 37 | 38 | engine_check() { 39 | # 输入法引擎检测 40 | if [ -z "$ENGINE" ]; then 41 | log ERROR "当前未配置输入法引擎" 42 | log WARN "如果使用Fcitx5(小企鹅)输入法,请复制以下语句并按回车执行,结束后请重新运行脚本:" 43 | echo "sed -i '' 's/ENGINE=\"\"/ENGINE=\"fcitx5\"/g' $script_name" 44 | log WARN "如果使用Squirrel(鼠须管)输入法,请复制以下语句并按回车执行,结束后请重新运行脚本:" 45 | echo "sed -i '' 's/ENGINE=\"\"/ENGINE=\"squirrel\"/g' $script_name" 46 | exit 47 | elif [ "$ENGINE" == "fcitx5" ]; then 48 | log INFO "当前使用Fcitx5(小企鹅)输入法" 49 | read -rp "按回车继续,M 键更改: " if_modify 50 | if [ "$if_modify" == "M" ]; then 51 | log WARN "请复制以下语句并按回车执行,结束后请重新运行脚本:" 52 | echo "sed -i '' 's/ENGINE=\"fcitx5\"/ENGINE=\"squirrel\"/g' $script_name" 53 | exit 54 | fi 55 | elif [ "$ENGINE" == "squirrel" ]; then 56 | log INFO "当前使用squirrel(鼠须管)输入法" 57 | read -rp "按回车继续,M 键更改: " if_modify 58 | if [ "$if_modify" == "M" ]; then 59 | log WARN "请复制以下语句并按回车执行,结束后请重新运行脚本:" 60 | echo "sed -i '' 's/ENGINE=\"squirrel\"/ENGINE=\"fcitx5\"/g' $script_name" 61 | exit 62 | fi 63 | fi 64 | } 65 | 66 | error_exit() { 67 | log ERROR "$*" 68 | cleanup 69 | exit 1 70 | } 71 | cleanup() { 72 | if [[ -d "$TEMP_DIR" ]]; then 73 | rm -rf "$TEMP_DIR" || log WARN "清理缓存文件失败" 74 | fi 75 | } 76 | deps_check() { 77 | for _cmd in curl jq unzip; do 78 | command -v "$_cmd" >/dev/null || error_exit "缺少必要依赖:$_cmd" 79 | done 80 | } 81 | fuzhu_check() { 82 | local fuzhu_check="$1" 83 | for _fuzhu in "${FUZHU_LIST[@]}"; do 84 | if [[ "$fuzhu_check" == "$_fuzhu" ]]; then 85 | return 0 86 | fi 87 | done 88 | return 1 89 | } 90 | script_check() { 91 | local mirror="$1" 92 | if [[ "$UPDATE_TOOLS_VERSION" =~ ^"DEFAULT" ]]; then 93 | log WARN "您似乎正在使用源文件!" 94 | log WARN "请从 Release 页面下载正式版!" 95 | error_exit "终止操作" 96 | fi 97 | log INFO "工具当前版本 $UPDATE_TOOLS_VERSION" 98 | if [[ "$mirror" == "github" ]]; then 99 | # 检查 GitHub 连接状态 100 | log INFO "正在检查 GitHub 连接状态" 101 | if ! curl -sL --connect-timeout 5 "https://api.github.com" >/dev/null; then 102 | error_exit "您似乎无法连接到 GitHub API, 请检查您的网络" 103 | elif ! curl -sL --connect-timeout 5 "https://github.com" >/dev/null; then 104 | error_exit "您似乎无法连接到 GitHub, 请检查您的网络" 105 | fi 106 | log INFO "正在检查本工具是否存在更新" 107 | local local_version remote_version 108 | local_version="$UPDATE_TOOLS_VERSION" 109 | remote_version=$( 110 | curl -sL --connect-timeout 10 $TOOLS_API | 111 | jq -r '.[].tag_name' | grep -vE "rc" | sort -rV | head -n 1 112 | ) 113 | if [[ "$remote_version" > "$local_version" ]]; then 114 | log WARN "检测到工具最新版本为: $remote_version, 建议更新后继续" 115 | log WARN "https://github.com/expoli/rime-wanxiang-update-tools/releases/download/$remote_version/rime-wanxiang-update-macos.sh" 116 | else 117 | log INFO "工具已是最新版本" 118 | fi 119 | elif [[ "$mirror" == "cnb" ]]; then 120 | log WARN "由于您正在使用镜像,无法检查本工具是否存在更新" 121 | fi 122 | } 123 | 124 | get_info() { 125 | local mirror="$1" version="$2" name="$3" info 126 | if [[ "$mirror" == "github" ]]; then 127 | info=$( 128 | jq -r --arg version "$version" --arg name "$name" '.[] | 129 | select( .tag_name == $version ) | .assets.[] | 130 | select( .name | test( $name ) )' "$TEMP_DIR/github_$name.json" 131 | ) 132 | echo "$info" 133 | elif [[ "$mirror" == "cnb" ]]; then 134 | info=$( 135 | jq -r --arg version "refs/tags/$version" --arg name "$name" '.releases.[] | 136 | select( .tag_ref == $version ) | .assets[] | 137 | select( .name | test( $name ) )' "$TEMP_DIR/cnb.json" 138 | ) 139 | echo "$info" 140 | fi 141 | } 142 | 143 | update_schema() { 144 | local mirror="$1" fuzhu="$2" gram="$3" 145 | # 缓存 API 响应 146 | if [[ "$mirror" == "github" ]]; then 147 | if [[ ! -f "$TEMP_DIR/github_$fuzhu.json" ]]; then 148 | if ! curl -sL -H "Accept: application/vnd.github.v3+json" \ 149 | --connect-timeout 10 "$SCHEMA_API" >"$TEMP_DIR/github_$fuzhu.json"; then 150 | error_exit "连接到 GitHub API 失败,您可能需要检查网络" 151 | fi 152 | fi 153 | elif [[ "$mirror" == "cnb" ]]; then 154 | if [[ ! -f "$TEMP_DIR/cnb.json" ]]; then 155 | if ! curl -sL -H "accept: application/vnd.cnb.web+json" \ 156 | --connect-timeout 10 "$CNB_API" >"$TEMP_DIR/cnb.json"; then 157 | error_exit "连接到 CNB 失败,您可能需要检查网络" 158 | fi 159 | fi 160 | fi 161 | # 获取本地版本号 162 | local local_version remote_version 163 | if [[ -f "$DEPLOY_DIR/lua/wanxiang.lua" ]]; then 164 | local_version=$(grep "wanxiang.version" "$DEPLOY_DIR/lua/wanxiang.lua" | awk -F '"' '{print $2}') 165 | [[ "$local_version" == v* ]] || local_version="v$local_version" 166 | else 167 | local_version="v0" 168 | fi 169 | # 获取远程版本号 170 | if [[ "$mirror" == "github" ]]; then 171 | remote_version=$( 172 | jq -r '.[].tag_name' "$TEMP_DIR/github_$fuzhu.json" | 173 | grep -vE "dict-nightly" | sort -rV | head -n 1 174 | ) 175 | elif [[ "$mirror" == "cnb" ]]; then 176 | remote_version=$( 177 | jq -r '.releases.[].tag_ref' \ 178 | "$TEMP_DIR/cnb.json" | grep -vE "model" | sort -rV | head -n 1 179 | ) 180 | remote_version="${remote_version#"refs/tags/"}" 181 | fi 182 | [[ "$remote_version" == v* ]] || remote_version="v$remote_version" 183 | if [[ "$remote_version" > "$local_version" ]]; then 184 | log INFO "远程方案文件版本号为 $remote_version, 以下内容为更新日志" 185 | local changelog 186 | if [[ "$mirror" == "github" ]]; then 187 | changelog=$( 188 | jq -r --arg version "$remote_version" '.[] | 189 | select( .tag_name == $version ) | .body' "$TEMP_DIR/github_$fuzhu.json" 190 | ) 191 | elif [[ "$mirror" == "cnb" ]]; then 192 | changelog=$( 193 | jq -r --arg version "refs/tags/$remote_version" '.releases.[] | 194 | select( .tag_ref == $version ) | .body' "$TEMP_DIR/cnb.json" 195 | ) 196 | fi 197 | echo -e "$changelog" | sed -n '/## 📝 更新日志/,/## 🚀 下载引导/p' | head -n -1 198 | sleep 3 199 | log INFO "开始更新方案文件,正在下载文件" 200 | local schemaurl schemaname local_size remote_size 201 | if [[ "$mirror" == "github" ]]; then 202 | schemaurl=$(get_info "$mirror" "$remote_version" "$fuzhu" | jq -r '.browser_download_url') 203 | elif [[ "$mirror" == "cnb" ]]; then 204 | schemaurl=$(get_info "$mirror" "$remote_version" "$fuzhu" | jq -r '.path') 205 | schemaurl="https://cnb.cool$schemaurl" 206 | fi 207 | schemaname=$(get_info "$mirror" "$remote_version" "$fuzhu" | jq -r '.name') 208 | curl -L --connect-timeout 10 -o "$TEMP_DIR/$schemaname" "$schemaurl" 209 | log INFO "正在验证文件完整性" 210 | local_size=$(stat -f %z "$TEMP_DIR/$schemaname") 211 | if [[ "$mirror" == "github" ]]; then 212 | remote_size=$(get_info "$mirror" "$remote_version" "$fuzhu" | jq -r '.size') 213 | elif [[ "$mirror" == "cnb" ]]; then 214 | remote_size=$(get_info "$mirror" "$remote_version" "$fuzhu" | jq -r '.size_in_byte') 215 | fi 216 | if [[ "$local_size" != "$remote_size" ]]; then 217 | log ERROR "期望文件大小: $remote_size, 实际文件大小: $local_size" 218 | error_exit "方案文件下载出错,请重试!" 219 | fi 220 | log INFO "验证成功,开始更新方案文件" 221 | unzip -q "$TEMP_DIR/$schemaname" -d "$TEMP_DIR/${schemaname%.zip}" 222 | for _file in "简纯+.trime.yaml" "custom_phrase.txt" "squirrel.yaml" "weasel.yaml"; do 223 | if [[ -f "$TEMP_DIR/${schemaname%.zip}/$_file" ]]; then 224 | rm -r "$TEMP_DIR/${schemaname%.zip}/${_file:?}" 225 | fi 226 | done 227 | local exclude_file 228 | while IFS= read -r _line; do 229 | if [[ "$_line" != \#* ]]; then 230 | exclude_file="$_line" 231 | if [[ ! -e "$DEPLOY_DIR/$exclude_file" ]]; then 232 | log WARN "项目 $DEPLOY_DIR/$exclude_file 不存在,跳过备份!" 233 | else 234 | cp -rf "$DEPLOY_DIR/$exclude_file" "$TEMP_DIR/${schemaname%.zip}/$exclude_file" 235 | fi 236 | fi 237 | done <"$DEPLOY_DIR/custom/user_exclude_file.txt" 238 | # 单独处理语法模型 239 | [[ "$gram" == "true" ]] || cp -rf "$DEPLOY_DIR/wanxiang-lts-zh-hans.gram" \ 240 | "$TEMP_DIR/${schemaname%.zip}/wanxiang-lts-zh-hans.gram" 241 | rm -rf "${DEPLOY_DIR:?}" 242 | cp -rf "$TEMP_DIR/${schemaname%.zip}" "$DEPLOY_DIR" 243 | log INFO "方案文件更新成功" 244 | return 0 245 | else 246 | log INFO "远程方案文件版本号为 $remote_version" 247 | log INFO "本地方案文件版本号为 $local_version, 您目前无需更新它" 248 | return 1 249 | fi 250 | } 251 | update_dict() { 252 | local mirror="$1" fuzhu="$2" 253 | # 缓存 API 响应 254 | if [[ "$mirror" == "github" ]]; then 255 | if [[ ! -f "$TEMP_DIR/github_$fuzhu.json" ]]; then 256 | if ! curl -sL -H "Accept: application/vnd.github.v3+json" \ 257 | --connect-timeout 10 "$SCHEMA_API" >"$TEMP_DIR/github_$fuzhu.json"; then 258 | error_exit "连接到 GitHub API 失败,您可能需要检查网络" 259 | fi 260 | fi 261 | elif [[ "$mirror" == "cnb" ]]; then 262 | if [[ ! -f "$TEMP_DIR/cnb.json" ]]; then 263 | if ! curl -sL -H "accept: application/vnd.cnb.web+json" \ 264 | --connect-timeout 10 "$CNB_API" >"$TEMP_DIR/cnb.json"; then 265 | error_exit "连接到 CNB 失败,您可能需要检查网络" 266 | fi 267 | fi 268 | fi 269 | local local_date remote_date 270 | if [[ -f "$DEPLOY_DIR/dicts/chengyu.txt" ]]; then 271 | local_date=$(stat -f %c "$DEPLOY_DIR/dicts/chengyu.txt") 272 | else 273 | local_date=0 274 | fi 275 | if [[ "$mirror" == "github" ]]; then 276 | remote_date=$(get_info "$mirror" "dict-nightly" "$fuzhu" | jq -r '.updated_at') 277 | elif [[ "$mirror" == "cnb" ]]; then 278 | remote_date=$(get_info "$mirror" "v1.0.0" "$fuzhu" | jq -r '.updated_at') 279 | fi 280 | remote_date=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$remote_date" +%s) 281 | if [[ $remote_date -gt $local_date ]]; then 282 | log INFO "正在下载最新词典文件" 283 | local dicturl dictname local_size remote_size 284 | if [[ "$mirror" == "github" ]]; then 285 | dicturl=$(get_info "$mirror" "dict-nightly" "$fuzhu" | jq -r '.browser_download_url') 286 | dictname=$(get_info "$mirror" "dict-nightly" "$fuzhu" | jq -r '.name') 287 | elif [[ "$mirror" == "cnb" ]]; then 288 | dicturl=$(get_info "$mirror" "v1.0.0" "$fuzhu" | jq -r '.path') 289 | dicturl="https://cnb.cool$dicturl" 290 | dictname=$(get_info "$mirror" "v1.0.0" "$fuzhu" | jq -r '.name') 291 | fi 292 | curl -L --connect-timeout 10 -o "$TEMP_DIR/$dictname" "$dicturl" 293 | log INFO "正在验证文件完整性" 294 | local_size=$(stat -f %z "$TEMP_DIR/$dictname") 295 | if [[ "$mirror" == "github" ]]; then 296 | remote_size=$(get_info "$mirror" "dict-nightly" "$fuzhu" | jq -r '.size') 297 | elif [[ "$mirror" == "cnb" ]]; then 298 | remote_size=$(get_info "$mirror" "v1.0.0" "$fuzhu" | jq -r '.size_in_byte') 299 | fi 300 | if [[ "$local_size" != "$remote_size" ]]; then 301 | log ERROR "期望文件大小: $remote_size, 实际文件大小: $local_size" 302 | error_exit "词典文件下载出错,请重试!" 303 | fi 304 | log INFO "验证成功,开始更新词典文件" 305 | unzip -q "$TEMP_DIR/$dictname" -d "$TEMP_DIR" 306 | dictname="${dictname:2}" && dictname="${dictname%.zip}" 307 | cp -rf "$TEMP_DIR/$dictname"/* "$DEPLOY_DIR/dicts" 308 | log INFO "词典文件更新成功" 309 | return 0 310 | else 311 | remote_date=$(date -r "$remote_date" +"%Y-%m-%d %H:%M:%S") 312 | log INFO "远程词典文件最后更新于 $remote_date" 313 | local_date=$(date -r "$local_date" +"%Y-%m-%d %H:%M:%S") 314 | log INFO "本地词典文件最后更新于 $local_date, 您目前无需更新它" 315 | return 1 316 | fi 317 | } 318 | update_gram() { 319 | local mirror="$1" 320 | # 缓存 API 响应 321 | if [[ "$mirror" == "github" ]]; then 322 | if [[ ! -f "$TEMP_DIR/github_gram.json" ]]; then 323 | if ! curl -sL -H "Accept: application/vnd.github.v3+json" \ 324 | --connect-timeout 10 "$GRAM_API" >"$TEMP_DIR/github_gram.json"; then 325 | error_exit "连接到 GitHub API 失败,您可能需要检查网络" 326 | fi 327 | fi 328 | elif [[ "$mirror" == "cnb" ]]; then 329 | if [[ ! -f "$TEMP_DIR/cnb.json" ]]; then 330 | if ! curl -sL -H "accept: application/vnd.cnb.web+json" \ 331 | --connect-timeout 10 "$CNB_API" >"$TEMP_DIR/cnb.json"; then 332 | error_exit "连接到 CNB 失败,您可能需要检查网络" 333 | fi 334 | fi 335 | fi 336 | local local_date remote_date gramname="wanxiang-lts-zh-hans.gram" 337 | if [[ -f "$DEPLOY_DIR/$gramname" ]]; then 338 | local_date=$(stat -f %c "$DEPLOY_DIR/$gramname") 339 | else 340 | local_date=0 341 | fi 342 | if [[ "$mirror" == "github" ]]; then 343 | remote_date=$(get_info "$mirror" "LTS" "gram" | jq -r '.updated_at') 344 | elif [[ "$mirror" == "cnb" ]]; then 345 | remote_date=$(get_info "$mirror" "model" "gram" | jq -r '.updated_at') 346 | fi 347 | remote_date=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$remote_date" +%s) 348 | if [[ $remote_date -gt $local_date ]]; then 349 | log INFO "正在下载最新语法模型" 350 | local gramurl local_size remote_size 351 | if [[ "$mirror" == "github" ]]; then 352 | gramurl=$(get_info "$mirror" "LTS" "gram" | jq -r '.browser_download_url') 353 | elif [[ "$mirror" == "cnb" ]]; then 354 | gramurl=$(get_info "$mirror" "model" "gram" | jq -r '.path') 355 | gramurl="https://cnb.cool$gramurl" 356 | fi 357 | curl -L --connect-timeout 10 -o "$TEMP_DIR/$gramname" "$gramurl" 358 | log INFO "正在验证文件完整性" 359 | local_size=$(stat -f %z "$TEMP_DIR/$gramname") 360 | if [[ "$mirror" == "github" ]]; then 361 | remote_size=$(get_info "$mirror" "LTS" "gram" | jq -r '.size') 362 | elif [[ "$mirror" == "cnb" ]]; then 363 | remote_size=$(get_info "$mirror" "model" "gram" | jq -r '.size_in_byte') 364 | fi 365 | if [[ "$local_size" != "$remote_size" ]]; then 366 | log ERROR "期望文件大小: $remote_size, 实际文件大小: $local_size" 367 | error_exit "语法模型下载出错,请重试!" 368 | fi 369 | log INFO "验证成功,开始更新语法模型" 370 | cp -rf "$TEMP_DIR/$gramname" "${DEPLOY_DIR}/$gramname" 371 | log INFO "语法模型更新成功" 372 | return 0 373 | else 374 | remote_date=$(date -r "$remote_date" +"%Y-%m-%d %H:%M:%S") 375 | log INFO "远程语法模型最后更新于 $remote_date" 376 | local_date=$(date -r "$local_date" +"%Y-%m-%d %H:%M:%S") 377 | log INFO "本地语法模型最后更新于 $local_date, 您目前无需更新它" 378 | return 1 379 | fi 380 | } 381 | # 部属函数 382 | deploy() { 383 | local deploy_executable="$1" 384 | shift # 移除第一个参数,后续所有参数都是要传给可执行文件的 385 | if [ -x "$deploy_executable" ]; then 386 | echo "正在触发重新部署配置" 387 | if output_and_error=$("$deploy_executable" "$@" 2>&1); then 388 | [[ -n "$output_and_error" ]] && echo "输出: $output_and_error" 389 | echo "重新部署成功" 390 | else 391 | echo "重新部署失败" 392 | [[ -n "$output_and_error" ]] && echo "错误信息: $output_and_error" 393 | fi 394 | else 395 | echo "找不到可执行文件: $deploy_executable" 396 | echo "请手动部署" 397 | fi 398 | } 399 | main() { 400 | # 脚本退出清理临时目录 401 | trap cleanup EXIT 402 | # 欢迎语 403 | log INFO "欢迎使用万象方案更新助手" 404 | # 检查是否为root用户 405 | if [[ "$EUID" -eq 0 ]]; then 406 | error_exit "请不要使用 root 身份运行该脚本!" 407 | fi 408 | # 检查必要的依赖 409 | deps_check 410 | # 处理用户输入 411 | local mirror="" schema="" fuzhu="" dict="false" gram="false" 412 | # 解析命令行参数 413 | while [[ "$#" -gt 0 ]]; do 414 | case $1 in 415 | --mirror) 416 | if [[ -n "$mirror" ]]; then 417 | error_exit "选项 mirror 需要参数!" 418 | else 419 | shift 420 | fi 421 | if [[ "$1" != "cnb" ]]; then 422 | error_exit "选项 mirror 的参数目前只能为 cnb" 423 | else 424 | mirror="$1" 425 | fi 426 | ;; 427 | --engine) 428 | if [[ -n "$ENGINE" ]]; then 429 | error_exit "选项 engine 已指定!" 430 | fi 431 | shift 432 | if [[ -z "$1" || "$1" == --* ]]; then 433 | error_exit "选项 engine 需要参数!" 434 | fi 435 | if [[ "$1" != "fcitx5" && "$1" != "squirrel" ]]; then 436 | error_exit "选项 engine 的参数只能为 fcitx5 或 squirrel" 437 | fi 438 | ENGINE="$1" 439 | ;; 440 | --schema) 441 | if [[ -n "$schema" ]]; then 442 | error_exit "选项 schema 需要参数!" 443 | else 444 | shift 445 | fi 446 | if [[ "$1" != "base" && "$1" != "pro" ]]; then 447 | error_exit "选项 schema 的参数只能为 base 或 pro" 448 | else 449 | schema="$1" 450 | fi 451 | ;; 452 | --fuzhu) 453 | if [[ -n "$fuzhu" ]]; then 454 | error_exit "选项 fuzhu 需要参数!" 455 | else 456 | shift 457 | fi 458 | if fuzhu_check "$1"; then 459 | fuzhu="$1" 460 | else 461 | error_exit "选项 fuzhu 的参数只能为 ${FUZHU_LIST[*]} 其中之一" 462 | fi 463 | ;; 464 | --dict) 465 | dict="true" 466 | ;; 467 | --gram) 468 | gram="true" 469 | ;; 470 | *) 471 | log WARN "您可能错误的使用了该脚本" 472 | log WARN "请前往 GitHub 页面阅读 Readme" 473 | log WARN "https://github.com/expoli/rime-wanxiang-update-tools/blob/main/Mac/Shell/README.md" 474 | error_exit "参数输入错误: $1" 475 | ;; 476 | esac 477 | shift 478 | done 479 | 480 | engine_check 481 | # 获取输入法配置路径 482 | if [ "$ENGINE" = "fcitx5" ]; then 483 | DEPLOY_DIR="$HOME/.local/share/fcitx5/rime" 484 | else 485 | DEPLOY_DIR="$HOME/Library/Rime" 486 | fi 487 | 488 | # 判断是否设置了部署目录 489 | if [[ -n "$DEPLOY_DIR" ]]; then 490 | if [[ ! -d "$DEPLOY_DIR" ]]; then 491 | log WARN "部署目录 $DEPLOY_DIR 不存在,您要创建它吗?" 492 | read -rp "请输入 YES 或 NO (区分大小写) " _check 493 | if [[ "$_check" == "YES" ]]; then 494 | log WARN "您真的要创建该目录吗?您确定您的设置正确吗?" 495 | read -rp "请输入 YES 或 NO (区分大小写) " _check_again 496 | [[ "$_check_again" == "YES" ]] || error_exit "用户终止操作" 497 | mkdir -p "$DEPLOY_DIR" 498 | else 499 | error_exit "用户终止操作" 500 | fi 501 | fi 502 | else 503 | error_exit "请设置部署目录!" 504 | fi 505 | # 排除项目列表文件是否存在 506 | if [[ -f "$DEPLOY_DIR/user_exclude_file.txt" ]]; then 507 | mv "$DEPLOY_DIR/user_exclude_file.txt" "$DEPLOY_DIR/custom/user_exclude_file.txt" 508 | sed -i 's/user_exclude_file\.txt/custom\/user_exclude_file\.txt/g' \ 509 | "$DEPLOY_DIR/custom/user_exclude_file.txt" 510 | fi 511 | if [[ ! -f "$DEPLOY_DIR/custom/user_exclude_file.txt" ]]; then 512 | log WARN "您没有设置排除项目列表!" 513 | log WARN "您需要创建的文件为 $DEPLOY_DIR/custom/user_exclude_file.txt" 514 | log WARN "请在该文件中写入您需要排除的项目,每行一个" 515 | error_exit "$DEPLOY_DIR/custom/user_exclude_file.txt 文件不存在" 516 | fi 517 | # 检查 schema 和 fuzhu 是否同时存在 518 | if [[ -n "$schema" && -z "$fuzhu" ]]; then 519 | error_exit "选项 schema 与选项 fuzhu 必须同时使用" 520 | fi 521 | # 检查 dict 和 fuzhu 是否同时存在 522 | if [[ "$dict" == "true" && -z "$fuzhu" ]]; then 523 | error_exit "选项 dict 与选项 fuzhu 必须同时使用" 524 | fi 525 | # 检查当 schema 为 base 时,fuzhu 是否也为 base 526 | if [[ "$schema" == "base" && "$fuzhu" != "base" ]]; then 527 | error_exit "当选项 schema 为 base 时,选项 fuzhu 必须为 base" 528 | fi 529 | [[ -n "$mirror" ]] || mirror="github" 530 | # 脚本自检 531 | script_check "$mirror" 532 | # 开始更新 533 | updated=false 534 | [[ -z "$schema" ]] || { 535 | update_schema "$mirror" "$fuzhu" "$gram" && updated=true 536 | } 537 | [[ "$dict" == "false" ]] || { 538 | update_dict "$mirror" "$fuzhu" && updated=true 539 | } 540 | [[ "$gram" == "false" ]] || { 541 | update_gram "$mirror" && updated=true 542 | } 543 | # 自动部署 544 | if [ "$updated" = true ]; then 545 | if [ "$ENGINE" = "squirrel" ]; then 546 | DEPLOY_EXECUTABLE="/Library/Input Methods/Squirrel.app/Contents/MacOS/Squirrel" 547 | deploy "$DEPLOY_EXECUTABLE" --reload 548 | else 549 | DEPLOY_EXECUTABLE="/Library/Input Methods/Fcitx5.app/Contents/bin/fcitx5-curl" 550 | deploy "$DEPLOY_EXECUTABLE" /config/addon/rime/deploy -X POST -d '{}' 551 | fi 552 | fi 553 | } 554 | 555 | main "$@" 556 | -------------------------------------------------------------------------------- /Windows/PowerShell/按需下载万象方案-词库-模型-utf-8.ps1: -------------------------------------------------------------------------------- 1 | ############# 自动更新配置项,配置好后将 AutoUpdate 设置为 true 即可 ############# 2 | $AutoUpdate = $false; 3 | 4 | # 是否使用 CNB 镜像源,如果设置为 $true,则从 CNB 获取资源;否则从 GitHub 获取。 5 | $UseCnbMirrorSource = $true 6 | 7 | # 设置自动更新时,是否更新方案、词库、模型,不想更新某项就改成false 8 | $IsUpdateSchemaDown = $true 9 | $IsUpdateDictDown = $true 10 | $IsUpdateModel = $true 11 | 12 | # 设置自动更新时选择的方案,注意必须包含双引号,例如:$InputSchemaType = "0"; 13 | # [0]-基础版; [1]-小鹤; [2]-汉心; [3]-墨奇; [4]-虎码; [5]-五笔; [6]-自然码" 14 | $InputSchemaType = "6"; 15 | 16 | # 设置自动更新时要跳过的文件列表,配置好后删除注释符号 17 | #$SkipFiles = @( 18 | # "wanxiang_en.dict.yaml", 19 | # "tone_fallback.lua", 20 | # "custom_phrase.txt" 21 | #); 22 | 23 | # 设置代理地址和端口,配置好后删除注释符号 24 | # $proxyAddress = "http://127.0.0.1:7897" 25 | # [System.Net.WebRequest]::DefaultWebProxy = New-Object System.Net.WebProxy($proxyAddress) 26 | # [System.Net.WebRequest]::DefaultWebProxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials 27 | 28 | # 设置GitHub Token请求头,防止api请求失败403错误,配置好后删除注释符号 29 | # $env:GITHUB_TOKEN = "填入这里你的token字符串" #打开链接https://github.com/settings/tokens,注册一个token# (Public repositories) 30 | 31 | ############# 自动更新配置项,配置好后将 AutoUpdate 设置为 true 即可 ############# 32 | 33 | $Debug = $false; 34 | 35 | $UpdateToolsVersion = "DEFAULT_UPDATE_TOOLS_VERSION_TAG"; 36 | if ($UpdateToolsVersion.StartsWith("DEFAULT")) { 37 | Write-Host "您下载的是非发行版脚本,请勿直接使用,请去 releases 页面下载最新版本:https://github.com/expoli/rime-wanxiang-update-tools/releases" -ForegroundColor Yellow; 38 | } else { 39 | Write-Host "当前更新工具版本:$UpdateToolsVersion" -ForegroundColor Yellow; 40 | } 41 | 42 | # 设置仓库所有者和名称 43 | $UpdateToolsOwner = "expoli" 44 | $UpdateToolsRepo = "rime-wanxiang-update-tools" 45 | # 定义临时文件路径 46 | $tempSchemaZip = Join-Path $env:TEMP "wanxiang_schema_temp.zip" 47 | $tempDictZip = Join-Path $env:TEMP "wanxiang_dict_temp.zip" 48 | $tempGram = Join-Path $env:TEMP "wanxiang-lts-zh-hans.gram" 49 | $SchemaExtractPath = Join-Path $env:TEMP "wanxiang_schema_extract" 50 | $DictExtractPath = Join-Path $env:TEMP "wanxiang_dict_extract" 51 | 52 | $GramModelFileName = "wanxiang-lts-zh-hans.gram" 53 | $ReleaseTimeRecordFile = "release_time_record.json" 54 | 55 | if ($UseCnbMirrorSource) { 56 | $SchemaOwner = "amzxyz" 57 | $SchemaRepo = "rime-wanxiang" 58 | $GramRepo = "RIME-LMDG" 59 | $GramReleaseTag = "model" 60 | $DictReleaseTag = "v1.0.0" 61 | } else { 62 | $SchemaOwner = "amzxyz" 63 | $SchemaRepo = "rime_wanxiang" 64 | $GramRepo = "RIME-LMDG" 65 | $GramReleaseTag = "LTS" 66 | $DictReleaseTag = "dict-nightly" 67 | } 68 | 69 | $KeyTable = @{ 70 | "0" = "base"; 71 | "1" = "flypy"; 72 | "2" = "hanxin"; 73 | "3" = "moqi"; 74 | "4" = "tiger"; 75 | "5" = "wubi"; 76 | "6" = "zrm"; 77 | } 78 | 79 | $UriHeader = @{ 80 | "accept"="application/vnd.cnb.web+json" 81 | "cache-control"="no-cache" 82 | 'Accept-Charset' = 'utf-8' 83 | } 84 | 85 | $SchemaDownloadTip = "[0]-基础版; [1]-小鹤; [2]-汉心; [3]-墨奇; [4]-虎码; [5]-五笔; [6]-自然码"; 86 | 87 | $GramKeyTable = @{ 88 | "0" = "zh-hans.gram"; 89 | } 90 | 91 | $GramFileTableIndex = 0; 92 | 93 | $DictFileSaveDirTable = @{ 94 | "base" = "dicts"; 95 | "pro" = "dicts"; 96 | } 97 | 98 | $DictFileSaveDirTableIndex = "base"; 99 | 100 | # 设置安全协议为TLS 1.2 101 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 102 | 103 | function Exit-Tip { 104 | param( 105 | [string]$exitCode = 0 106 | ) 107 | Write-Host '按任意键退出...' 108 | $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') 109 | exit $exitCode 110 | } 111 | 112 | # 获取 Weasel 用户目录路径 113 | function Get-RegistryValue { 114 | param( 115 | [string]$regPath, 116 | [string]$regValue 117 | ) 118 | 119 | try { 120 | # 获取注册表值 121 | $value = (Get-ItemProperty -Path $regPath -Name $regValue).$regValue 122 | # 返回结果 123 | return $value 124 | } 125 | catch { 126 | Write-Host "警告:注册表路径 $regPath 不存在,请检查输入法是否正确安装" -ForegroundColor Yellow 127 | return $null 128 | } 129 | } 130 | 131 | function Get-FileNameWithoutExtension { 132 | param( 133 | [string]$filePath 134 | ) 135 | $fileName = Split-Path $filePath -Leaf 136 | return $fileName -replace '\.[^.]+$', '' 137 | } 138 | 139 | function Get-DictExtractedFolderPath { 140 | param( 141 | [string]$extractPath, 142 | [string]$assetName 143 | ) 144 | $folders = Get-ChildItem -Path $extractPath -Directory 145 | if ($folders.Count -eq 0) { 146 | Write-Host "错误:解压后的目录中没有找到任何文件夹" -ForegroundColor Red 147 | Exit-Tip 1 148 | } elseif ($folders.Count -gt 1) { 149 | Write-Host "警告:解压后的目录中有多个文件夹,将使用第一个文件夹" -ForegroundColor Yellow 150 | Write-Host "文件夹名称: $($folders[0].Name)" -ForegroundColor Green 151 | return $folders[0].FullName 152 | } else { 153 | Write-Host "解压后的目录中只有一个文件夹,将使用该文件夹" -ForegroundColor Green 154 | Write-Host "文件夹名称: $($folders[0].Name)" -ForegroundColor Green 155 | return $folders[0].FullName 156 | } 157 | } 158 | 159 | function Get-WeaselUserDir { 160 | try { 161 | $userDir = Get-RegistryValue -regPath "HKCU:\Software\Rime\Weasel" -regValue "RimeUserDir" 162 | if (-not $userDir) { 163 | # appdata 目录下的 Rime 目录 164 | $userDir = Join-Path $env:APPDATA "Rime" 165 | } 166 | return $userDir 167 | } 168 | catch { 169 | Write-Host "警告:未找到Weasel用户目录,请确保已正确安装小狼毫输入法" -ForegroundColor Yellow 170 | } 171 | } 172 | 173 | function Get-WeaselInstallDir { 174 | try { 175 | return Get-RegistryValue -regPath "HKLM:\SOFTWARE\WOW6432Node\Rime\Weasel" -regValue "WeaselRoot" 176 | } 177 | catch { 178 | Write-Host "警告:未找到Weasel安装目录,请确保已正确安装小狼毫输入法" -ForegroundColor Yellow 179 | return $null 180 | } 181 | } 182 | 183 | function Get-WeaselServerExecutable { 184 | try { 185 | return Get-RegistryValue -regPath "HKLM:\SOFTWARE\WOW6432Node\Rime\Weasel" -regValue "ServerExecutable" 186 | } 187 | catch { 188 | Write-Host "警告:未找到Weasel服务端可执行程序,请确保已正确安装小狼毫输入法" -ForegroundColor Yellow 189 | return $null 190 | } 191 | } 192 | 193 | function Test-SkipFile { 194 | param( 195 | [string]$filePath 196 | ) 197 | return $SkipFiles -contains $filePath 198 | } 199 | 200 | # 调用函数并赋值给变量 201 | $rimeUserDir = Get-WeaselUserDir 202 | $rimeInstallDir = Get-WeaselInstallDir 203 | $rimeServerExecutable = Get-WeaselServerExecutable 204 | 205 | function Stop-WeaselServer { 206 | if (-not $rimeServerExecutable) { 207 | Write-Host "警告:未找到Weasel服务端可执行程序,请确保已正确安装小狼毫输入法" -ForegroundColor Yellow 208 | Exit-Tip 1 209 | } 210 | Start-Process -FilePath (Join-Path $rimeInstallDir $rimeServerExecutable) -ArgumentList '/q' 211 | } 212 | 213 | function Start-WeaselServer { 214 | if (-not $rimeServerExecutable) { 215 | Write-Host "警告:未找到Weasel服务端可执行程序,请确保已正确安装小狼毫输入法" -ForegroundColor Yellow 216 | Exit-Tip 1 217 | } 218 | Start-Process -FilePath (Join-Path $rimeInstallDir $rimeServerExecutable) 219 | } 220 | 221 | function Start-WeaselReDeploy{ 222 | $defaultShortcutPath = "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\小狼毫输入法\【小狼毫】重新部署.lnk" 223 | $backupEnglishShortcutPath = "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Weasel\Weasel Deploy.lnk" 224 | if (Test-Path -Path $defaultShortcutPath) { 225 | Write-Host "找到默认【小狼毫】重新部署快捷方式,将执行" -ForegroundColor Green 226 | Invoke-Item -Path $defaultShortcutPath 227 | } elseif (Test-Path -Path $backupEnglishShortcutPath) { 228 | Write-Host "找到默认【小狼毫】重新部署快捷方式,将执行" -ForegroundColor Green 229 | Invoke-Item -Path $backupEnglishShortcutPath 230 | } else { 231 | Write-Host "未找到默认的【小狼毫】重新部署快捷方式,将尝试执行默认的重新部署命令" -ForegroundColor Yellow 232 | Write-Host "跳过触发重新部署" -ForegroundColor Yellow 233 | } 234 | } 235 | 236 | # 检查必要路径是否为空 237 | if (-not $rimeUserDir -or -not $rimeInstallDir -or -not $rimeServerExecutable) { 238 | Write-Host "错误:无法获取Weasel必要路径,请检查输入法是否正确安装" -ForegroundColor Red 239 | Exit-Tip 1 240 | } 241 | Write-Host "Weasel用户目录路径为: $rimeUserDir" 242 | $targetDir = $rimeUserDir 243 | $TimeRecordFile = Join-Path $targetDir $ReleaseTimeRecordFile 244 | 245 | function Test-VersionSuffix { 246 | param( 247 | [string]$url 248 | ) 249 | # tag_name = v1.0.0 or v1.0 250 | $pattern = 'v(\d+)(\.\d+)+' 251 | if ($UseCnbMirrorSource) { 252 | return $url -match $pattern | Where-Object { $_ -notmatch $DictReleaseTag -and $_ -notmatch $GramReleaseTag } 253 | } 254 | else { 255 | return $url -match $pattern 256 | } 257 | } 258 | 259 | function Test-DictSuffix { 260 | param( 261 | [string]$url 262 | ) 263 | 264 | return $url -match $DictReleaseTag 265 | } 266 | 267 | function Test-CnbGramSuffix { 268 | param( 269 | [string]$url 270 | ) 271 | # tag_name = model 272 | return $url -match $GramReleaseTag 273 | } 274 | 275 | function Invoke-FileUtf8 { 276 | param( 277 | [Parameter(Mandatory=$true)][string]$Uri, 278 | [hashtable]$Headers 279 | ) 280 | $tmp = [System.IO.Path]::GetTempFileName() 281 | try { 282 | Invoke-WebRequest -Uri $Uri -Headers $Headers -OutFile $tmp 283 | $bytes = [System.IO.File]::ReadAllBytes($tmp) 284 | $text = [System.Text.Encoding]::UTF8.GetString($bytes) 285 | return $text 286 | } catch { 287 | Write-Error "错误:下载或解析文件失败: $Uri" 288 | Write-Error $_.Exception.Message 289 | Exit-Tip 1 290 | } finally { 291 | Remove-Item $tmp -ErrorAction SilentlyContinue 292 | } 293 | } 294 | 295 | function Get-CnbReleaseInfo { 296 | param( 297 | [string]$owner, 298 | [string]$repo 299 | ) 300 | 301 | # https://cnb.cool/amzxyz/rime-wanxiang/-/releases?page=1&page_size=20&query= 302 | $apiUrl = "https://cnb.cool/$owner/$repo/-/releases?page=1&page_size=100&query=" 303 | 304 | try { 305 | Write-Host "正在从 CNB 页面获取信息: $apiUrl" -ForegroundColor Cyan 306 | $jsonDataTmp = Invoke-FileUtf8 -Uri $apiUrl -Headers $UriHeader 307 | $jsonDataFormat = $jsonDataTmp | ConvertFrom-Json 308 | 309 | if ($jsonDataFormat.releases){ 310 | $releaseData = $jsonDataFormat.releases 311 | Write-Host "成功获取 CNB release 版本信息" -ForegroundColor Green 312 | if ($jsonDataFormat.release_count -eq 0) { 313 | Write-Error "CNB release 版本没有可下载资源" 314 | Exit-Tip 1 315 | } 316 | return $releaseData 317 | } else { 318 | Write-Error "错误:在页面中未找到 'release' 数据。" 319 | Exit-Tip 1 320 | } 321 | } 322 | catch { 323 | Write-Error "错误:下载或解析CNB页面失败: $pageUrl" 324 | Write-Error $_.Exception.Message 325 | Exit-Tip 1 326 | } 327 | } 328 | 329 | function Get-GithubReleaseInfo { 330 | param( 331 | [string]$owner, 332 | [string]$repo 333 | ) 334 | # 构建API请求URL 335 | $apiUrl = "https://api.github.com/repos/$owner/$repo/releases" 336 | 337 | # 构建API请求头 338 | $GitHubHeaders = @{ 339 | "User-Agent" = "PowerShell Release Downloader" 340 | "Accept" = "application/vnd.github.v3+json" 341 | } 342 | if ($env:GITHUB_TOKEN) { 343 | $GitHubHeaders["Authorization"] = "token $($env:GITHUB_TOKEN)" 344 | } 345 | 346 | try { 347 | # 发送API请求 348 | $response = Invoke-RestMethod -Uri $apiUrl -Headers $GitHubHeaders 349 | } 350 | catch { 351 | $statusCode = $_.Exception.Response.StatusCode.Value__ 352 | if ($statusCode -eq 404) { 353 | Write-Error "错误:仓库 '$owner/$repo' 不存在或没有发布版本" 354 | } 355 | else { 356 | Write-Error "API请求失败 [$statusCode]:$_" 357 | } 358 | Exit-Tip 1 359 | } 360 | 361 | # 检查是否有可下载资源 362 | if ($response.assets.Count -eq 0) { 363 | Write-Error "该版本没有可下载资源" 364 | Exit-Tip 1 365 | } 366 | return $response 367 | } 368 | 369 | function Get-ReleaseInfo { 370 | param( 371 | [string]$owner, 372 | [string]$repo, 373 | [bool]$updateToolFlag = $false 374 | ) 375 | # 构建API请求URL 376 | if ($updateToolFlag) { 377 | return Get-GithubReleaseInfo -owner $owner -repo $repo 378 | } 379 | if ($UseCnbMirrorSource){ 380 | return Get-CnbReleaseInfo -owner $owner -repo $repo 381 | } else { 382 | return Get-GithubReleaseInfo -owner $owner -repo $repo 383 | } 384 | } 385 | 386 | $UpdateTollsResponse = Get-ReleaseInfo -owner $UpdateToolsOwner -repo $UpdateToolsRepo -updateToolFlag $true 387 | 388 | # 检查是否有新版本,如果获取的版本信息比现在的版本信息(UpdateToolsVersion)新,则提示用户更新 389 | # 版本格式:v3.4.0,v3.4.1,v3.4.1-rc1,不比较 rc 版本, 390 | # UpdateToolsVersion 391 | if ($UpdateTollsResponse.Count -eq 0) { 392 | Write-Host "没有找到更新工具的版本信息,请检查网络连接或仓库是否存在" -ForegroundColor Red 393 | Exit-Tip 1 394 | } 395 | 396 | $StableUpdateToolsReleases = $UpdateTollsResponse 397 | if ($StableUpdateToolsReleases.Count -eq 0) { 398 | Write-Host "没有找到稳定版的更新工具版本信息" -ForegroundColor Yellow 399 | } else { 400 | $LatestUpdateToolsRelease = $StableUpdateToolsReleases | Select-Object -First 1 401 | if ($LatestUpdateToolsRelease.tag_name -ne $UpdateToolsVersion) { 402 | Write-Host "发现新版本的更新工具: $($LatestUpdateToolsRelease.tag_name)" -ForegroundColor Yellow 403 | Write-Host "如需更新,请访问 https://github.com/expoli/rime-wanxiang-update-tools/releases 下载最新版本" -ForegroundColor Yellow 404 | Write-Host "当前版本: $UpdateToolsVersion" -ForegroundColor Yellow 405 | Write-Host "更新日志: $($LatestUpdateToolsRelease.body)" -ForegroundColor Yellow 406 | } 407 | } 408 | 409 | # 获取最新的版本信息 410 | $SchemaResponse = Get-ReleaseInfo -owner $SchemaOwner -repo $SchemaRepo 411 | if ($UseCnbMirrorSource) { 412 | $GramResponse = $SchemaResponse 413 | } else { 414 | $GramResponse = Get-ReleaseInfo -owner $SchemaOwner -repo $GramRepo 415 | } 416 | 417 | $SelectedDictRelease = $null 418 | $SelectedSchemaRelease = $null 419 | $SelectedGramRelease = $null 420 | 421 | function Get-ReleaseTagName { 422 | param( 423 | [object]$release 424 | ) 425 | if ($UseCnbMirrorSource) { 426 | $tag_name = $release.tag_ref 427 | } else { 428 | $tag_name = $release.tag_name 429 | } 430 | return $tag_name 431 | } 432 | 433 | foreach ($release in $SchemaResponse) { 434 | $tag_name = Get-ReleaseTagName -release $release 435 | 436 | if (($null -eq $SelectedDictRelease) -and (Test-DictSuffix -url $tag_name)) { 437 | $SelectedDictRelease = $release 438 | continue 439 | } 440 | if ((-not $SelectedSchemaRelease) -and (Test-VersionSuffix -url $tag_name)) { 441 | $SelectedSchemaRelease = $release 442 | } 443 | if ($SelectedDictRelease -and $SelectedSchemaRelease) { 444 | break 445 | } 446 | } 447 | 448 | foreach ($release in $GramResponse) { 449 | $tag_name = Get-ReleaseTagName -release $release 450 | if ($Debug) { 451 | Write-Host "release.tag_name: $tag_name" -ForegroundColor Green 452 | Write-Host "GramReleaseTag: $GramReleaseTag" -ForegroundColor Green 453 | } 454 | if ($tag_name -match $GramReleaseTag) { 455 | $SelectedGramRelease = $release 456 | } 457 | } 458 | 459 | if ($SelectedDictRelease -and $SelectedSchemaRelease -and $SelectedGramRelease) { 460 | if (-not $UseCnbMirrorSource) { 461 | Write-Host "解析出最新的词库链接为:$($SelectedDictRelease.html_url)" -ForegroundColor Green 462 | Write-Host "解析出最新的版本链接为:$($SelectedSchemaRelease.html_url)" -ForegroundColor Green 463 | Write-Host "解析出最新的模型链接为:$($SelectedGramRelease.html_url)" -ForegroundColor Green 464 | } 465 | } else { 466 | Write-Error "未找到符合条件的版本或词库链接" 467 | Exit-Tip 1 468 | } 469 | 470 | # 获取最新的版本的tag_name 471 | if (-not $UseCnbMirrorSource) { 472 | Write-Host "方案最新的版本为:$($SelectedSchemaRelease.tag_name)" 473 | Write-Host "方案更新日志: " -ForegroundColor Yellow 474 | Write-Host $SelectedSchemaRelease.body -ForegroundColor Yellow 475 | } else { 476 | Write-Host "方案最新的版本为:$($SelectedSchemaRelease.tag_ref)" 477 | Write-Host "方案更新日志: " -ForegroundColor Yellow 478 | Write-Host $SelectedSchemaRelease.body -ForegroundColor Yellow 479 | } 480 | 481 | 482 | $promptSchemaType = "请选择你要下载的方案类型的编号: `n$SchemaDownloadTip" 483 | $promptAllUpdate = "是否更新所有内容(方案、词库、模型):`n[0]-更新所有; [1]-不更新所有" 484 | $promptSchemaDown = "是否下载方案:`n[0]-下载; [1]-不下载" 485 | $promptGramModel = "是否下载模型:`n[0]-下载; [1]-不下载" 486 | $promptDictDown = "是否下载词库:`n[0]-下载; [1]-不下载" 487 | 488 | if (-not $Debug) { 489 | if ($AutoUpdate) { 490 | Write-Host "自动更新模式,将自动下载最新的版本" -ForegroundColor Green 491 | Write-Host "你配置的方案号为:$InputSchemaType" -ForegroundColor Green 492 | # 方案号只支持0-7 493 | if ($InputSchemaType -lt 0 -or $InputSchemaType -gt 7) { 494 | Write-Error "错误:方案号只能是0-7" 495 | Exit-Tip 1 496 | } 497 | $InputAllUpdate = "0" 498 | $InputSchemaDown = if ($IsUpdateSchemaDown) { "0" } else { "1" } 499 | $InputGramModel = if ($IsUpdateModel) { "0" } else { "1" } 500 | $InputDictDown = if ($IsUpdateDictDown) { "0" } else { "1" } 501 | } else { 502 | $InputSchemaType = Read-Host $promptSchemaType 503 | $InputAllUpdate = Read-Host $promptAllUpdate 504 | if ($InputAllUpdate -eq "0") { 505 | $InputSchemaDown = "0" 506 | $InputGramModel = "0" 507 | $InputDictDown = "0" 508 | } else { 509 | $InputSchemaDown = Read-Host $promptSchemaDown 510 | $InputGramModel = Read-Host $promptGramModel 511 | $InputDictDown = Read-Host $promptDictDown 512 | } 513 | } 514 | } else { 515 | $InputSchemaType = "7" 516 | $InputSchemaDown = "0" 517 | $InputGramModel = "0" 518 | $InputDictDown = "0" 519 | } 520 | 521 | if ($InputSchemaType -eq "0") { 522 | $DictFileSaveDirTableIndex = "base" 523 | } else { 524 | $DictFileSaveDirTableIndex = "pro" 525 | } 526 | 527 | # 根据用户输入的方案号获取下载链接 528 | function Get-ExpectedAssetTypeInfo { 529 | param( 530 | [string]$index, 531 | [hashtable]$keyTable, 532 | [Object]$releaseObject 533 | ) 534 | 535 | $info = $null 536 | 537 | foreach ($asset in $releaseObject.assets) { 538 | if ($Debug) { 539 | Write-Host "asset.name: $($asset.name)" -ForegroundColor Green 540 | Write-Host "keyTable[$index]: $($keyTable[$index])" -ForegroundColor Green 541 | } 542 | 543 | if ($asset.name -match $keyTable[$index]) { 544 | $info = $asset 545 | # 打印 546 | if ($Debug) { 547 | Write-Host "匹配成功,asset.name: $($asset.name)" -ForegroundColor Green 548 | Write-Host "目标信息为:$($info)" 549 | } 550 | break 551 | } 552 | } 553 | 554 | return $info 555 | } 556 | 557 | $ExpectedSchemaTypeInfo = Get-ExpectedAssetTypeInfo -index $InputSchemaType -keyTable $KeyTable -releaseObject $SelectedSchemaRelease 558 | $ExpectedDictTypeInfo = Get-ExpectedAssetTypeInfo -index $InputSchemaType -keyTable $KeyTable -releaseObject $SelectedDictRelease 559 | $ExpectedGramTypeInfo = Get-ExpectedAssetTypeInfo -index $GramFileTableIndex -keyTable $GramKeyTable -releaseObject $SelectedGramRelease 560 | 561 | if (-not $ExpectedSchemaTypeInfo -or -not $ExpectedDictTypeInfo -or -not $ExpectedGramTypeInfo) { 562 | if (($InputSchemaDown -eq 0) -and (-not $ExpectedSchemaTypeInfo)) { 563 | Write-Error "未找到符合条件的方案下载链接" 564 | Exit-Tip 1 565 | } 566 | if (($InputDictDown -eq 0) -and (-not $ExpectedDictTypeInfo)) { 567 | Write-Error "未找到符合条件的词库下载链接" 568 | Exit-Tip 1 569 | } 570 | if (($InputGramModel -eq 0) -and (-not $ExpectedGramTypeInfo)) { 571 | Write-Error "未找到符合条件的模型下载链接" 572 | Exit-Tip 1 573 | } 574 | } 575 | 576 | # 打印 577 | if ($InputSchemaDown -eq "0") { 578 | Write-Host "下载方案" -ForegroundColor Green 579 | if ($Debug) { 580 | Write-Host "最新的辅助码方案下载信息为:$($ExpectedSchemaTypeInfo)" -ForegroundColor Green 581 | } 582 | } 583 | 584 | if ($InputDictDown -eq "0") { 585 | Write-Host "下载词库" -ForegroundColor Green 586 | if ($Debug) { 587 | Write-Host "最新的辅助码词库下载信息为:$($ExpectedDictTypeInfo)" -ForegroundColor Green 588 | } 589 | } 590 | 591 | if ($InputGramModel -eq "0") { 592 | Write-Host "下载模型" -ForegroundColor Green 593 | if ($Debug) { 594 | Write-Host "最新的辅助码模型下载信息为:$($ExpectedGramTypeInfo)" -ForegroundColor Green 595 | } 596 | } 597 | 598 | function Save-TimeRecord { 599 | param( 600 | [string]$filePath, 601 | [string]$key, 602 | [string]$value 603 | ) 604 | 605 | $timeData = @{} 606 | if (Test-Path $filePath) { 607 | try { 608 | if ($PSVersionTable.PSVersion.Major -ge 7) { 609 | $timeData = Get-Content $filePath | ConvertFrom-Json -AsHashtable 610 | } else { 611 | $timeData = Get-Content $filePath | ConvertFrom-Json | ForEach-Object { 612 | $ht = @{} 613 | $_.PSObject.Properties | ForEach-Object { $ht[$_.Name] = $_.Value } 614 | $ht 615 | } 616 | } 617 | } 618 | catch { 619 | Write-Host "警告:无法读取时间记录文件,将创建新的记录" -ForegroundColor Yellow 620 | } 621 | } 622 | 623 | $timeData[$key] = $value 624 | 625 | try { 626 | if ($PSVersionTable.PSVersion.Major -ge 7) { 627 | $timeData | ConvertTo-Json | Set-Content $filePath 628 | } else { 629 | $timeData | ConvertTo-Json -Depth 100 | Set-Content $filePath 630 | } 631 | } 632 | catch { 633 | Write-Host "错误:无法保存时间记录" -ForegroundColor Red 634 | } 635 | } 636 | 637 | function Get-TimeRecord { 638 | param( 639 | [string]$filePath, 640 | [string]$key 641 | ) 642 | 643 | if (Test-Path $filePath) { 644 | try { 645 | if ($PSVersionTable.PSVersion.Major -ge 7) { 646 | $timeData = Get-Content $filePath | ConvertFrom-Json -AsHashtable 647 | } else { 648 | $json = Get-Content $filePath | ConvertFrom-Json 649 | $timeData = @{} 650 | $json.PSObject.Properties | ForEach-Object { $timeData[$_.Name] = $_.Value } 651 | } 652 | return $timeData[$key] 653 | } 654 | catch { 655 | Write-Host "警告:无法读取时间记录文件" -ForegroundColor Yellow 656 | } 657 | } 658 | return $null 659 | } 660 | 661 | # 比较本地和远程更新时间 662 | function Compare-UpdateTime { 663 | param( 664 | [Object]$localTime, 665 | [datetime]$remoteTime 666 | ) 667 | 668 | if ($null -eq $localTime) { 669 | Write-Host "本地时间记录不存在,将创建新的时间记录" -ForegroundColor Yellow 670 | return $true 671 | } 672 | 673 | $localTime = [datetime]::Parse($localTime) 674 | 675 | if ($null -eq $remoteTime) { 676 | Write-Host "远程时间记录不存在,无法比较" -ForegroundColor Red 677 | return $false 678 | } 679 | 680 | if ($remoteTime -gt $localTime) { 681 | Write-Host "发现新版本,准备更新" -ForegroundColor Yellow 682 | return $true 683 | } 684 | Write-Host "当前已是最新版本" -ForegroundColor Yellow 685 | return $false 686 | } 687 | 688 | # 从JSON文件加载并解析UpdateTimeKey 689 | function Read-UpdateTimeKey { 690 | param( 691 | [string]$filePath 692 | ) 693 | 694 | if (-not (Test-Path $filePath)) { 695 | Write-Host "警告:时间记录文件不存在" -ForegroundColor Yellow 696 | return $null 697 | } 698 | 699 | try { 700 | if ($PSVersionTable.PSVersion.Major -ge 7) { 701 | $timeData = Get-Content $filePath | ConvertFrom-Json -AsHashtable 702 | } else { 703 | $json = Get-Content $filePath | ConvertFrom-Json 704 | $timeData = @{} 705 | $json.PSObject.Properties | ForEach-Object { $timeData[$_.Name] = $_.Value } 706 | } 707 | return $timeData 708 | } 709 | catch { 710 | Write-Host "错误:无法解析JSON文件" -ForegroundColor Red 711 | return $null 712 | } 713 | } 714 | 715 | # 检查时间记录文件 716 | $hasTimeRecord = Read-UpdateTimeKey -filePath $TimeRecordFile 717 | 718 | if (-not $hasTimeRecord) { 719 | Write-Host "时间记录文件不存在,将创建新的时间记录" -ForegroundColor Yellow 720 | } 721 | 722 | # 创建目标目录(如果不存在) 723 | if (-not (Test-Path $targetDir)) { 724 | Write-Host "创建目标目录: $targetDir" -ForegroundColor Green 725 | New-Item -Path $targetDir -ItemType Directory -Force | Out-Null 726 | } 727 | 728 | function Test-FileSHA256 { 729 | param ( 730 | [Parameter(Mandatory=$true)] 731 | [string]$FilePath, 732 | [Parameter(Mandatory=$true)] 733 | [string]$CompareSHA256 734 | ) 735 | 736 | if (-not (Test-Path $FilePath)) { 737 | Write-Host "文件不存在:$FilePath" -ForegroundColor Red 738 | return $false 739 | } 740 | 741 | $hash = Get-FileHash -Path $FilePath -Algorithm SHA256 742 | if ($hash.Hash.ToLower() -eq $CompareSHA256.ToLower()) { 743 | Write-Host "SHA256 匹配。" -ForegroundColor Green 744 | return $true 745 | } else { 746 | Write-Host "SHA256 不匹配。" -ForegroundColor Red 747 | Write-Host "文件 SHA256: $($hash.Hash)" 748 | Write-Host "期望 SHA256: $CompareSHA256" 749 | return $false 750 | } 751 | } 752 | 753 | # 下载函数 754 | function Download-Files { 755 | param( 756 | [Object]$assetInfo, 757 | [string]$outFilePath 758 | ) 759 | 760 | try { 761 | if ($UseCnbMirrorSource) { 762 | $downloadUrl = "https://cnb.cool" + $assetInfo.path 763 | } else { 764 | $downloadUrl = $assetInfo.browser_download_url 765 | } 766 | 767 | Write-Host "正在下载文件:$($assetInfo.name)..." -ForegroundColor Green 768 | Invoke-WebRequest -Uri $downloadUrl -OutFile $outFilePath -UseBasicParsing 769 | Write-Host "下载完成" -ForegroundColor Green 770 | if ($UseCnbMirrorSource) { 771 | # 校验文件大小 772 | $expectedSize = [int64]$assetInfo.size_in_byte 773 | $actualSize = (Get-Item $outFilePath).Length 774 | if ($expectedSize -ne $actualSize) { 775 | Write-Host "文件大小校验失败,删除文件" -ForegroundColor Red 776 | Write-Host "期望大小: $expectedSize 字节,实际大小: $actualSize 字节" -ForegroundColor Red 777 | Remove-Item -Path $outFilePath -Force 778 | Exit-Tip 1 779 | } 780 | } else { 781 | $SHA256 = $assetInfo.digest.Split(":")[1] 782 | if (-not (Test-FileSHA256 -FilePath $outFilePath -CompareSHA256 $SHA256)) { 783 | Write-Host "SHA256 校验失败,删除文件" -ForegroundColor Red 784 | Remove-Item -Path $outFilePath -Force 785 | Exit-Tip 1 786 | } 787 | } 788 | } 789 | catch { 790 | Write-Host "下载失败: $_" -ForegroundColor Red 791 | Exit-Tip 1 792 | } 793 | } 794 | 795 | # 解压 zip 文件 796 | function Expand-ZipFile { 797 | param( 798 | [string]$zipFilePath, 799 | [string]$destinationPath 800 | ) 801 | 802 | try { 803 | Write-Host "正在解压文件: $zipFilePath" -ForegroundColor Green 804 | Write-Host "解压到: $destinationPath" -ForegroundColor Green 805 | 806 | # --- 获取 7z.exe 路径 --- 807 | $weaselRootDir = Get-WeaselInstallDir 808 | if (-not $weaselRootDir) { 809 | Throw "无法获取小狼毫输入法安装目录,因此无法定位 7z.exe 进行解压。" 810 | } 811 | $sevenZipPath = Join-Path $weaselRootDir "7z.exe" 812 | 813 | # 检查 7z.exe 是否存在 814 | if (-not (Test-Path $sevenZipPath -PathType Leaf)) { 815 | Throw "找不到 7z.exe。预期路径: '$sevenZipPath'。请确认小狼毫输入法安装正常且包含 7z.exe" 816 | } 817 | Write-Host "已找到 7z.exe:$sevenZipPath" -ForegroundColor DarkCyan 818 | 819 | # --- 确保目标目录存在 --- 820 | if (-not (Test-Path $destinationPath)) { 821 | try { 822 | New-Item -Path $destinationPath -ItemType Directory -Force | Out-Null 823 | Write-Host "已创建目标目录: $destinationPath" -ForegroundColor Yellow 824 | } 825 | catch { 826 | Throw "创建目标目录 '$destinationPath' 失败: $($_.Exception.Message)。" 827 | } 828 | } 829 | 830 | # --- 调用 7z.exe 进行解压 --- 831 | $arguments = "x `"$zipFilePath`" -o`"$destinationPath`" -y" 832 | Write-Host "正在调用 7-Zip 进行解压..." -ForegroundColor DarkGreen 833 | 834 | $process = Start-Process -FilePath $sevenZipPath -ArgumentList $arguments -Wait -PassThru -NoNewWindow 835 | 836 | if ($process.ExitCode -ne 0) { 837 | Throw "7-Zip 解压失败,退出代码: $($process.ExitCode)。" 838 | } 839 | 840 | Write-Host "解压完成" -ForegroundColor Green 841 | } 842 | catch { 843 | Write-Host "解压失败: $($_.Exception.Message)" -ForegroundColor Red 844 | Remove-Item -Path $zipFilePath -Force -ErrorAction SilentlyContinue 845 | Exit-Tip 1 846 | } 847 | } 848 | 849 | if ($InputSchemaDown -eq "0" -or $InputDictDown -eq "0" -or $InputGramModel -eq "0") { 850 | # 开始更新词库,从现在开始不要操作键盘,直到更新完成,否则会触发小狼毫重启,文件更新告警,导致更新失败,请放心更新完成后会自动拉起小狼毫 851 | Write-Host "正在更新词库,请不要操作键盘,直到更新完成" -ForegroundColor Red 852 | Write-Host "更新完成后会自动拉起小狼毫" -ForegroundColor Red 853 | } else { 854 | Write-Host "没有指定要更新的内容,将退出" -ForegroundColor Red 855 | Exit-Tip 0 856 | } 857 | 858 | function Get-UpdateAtObj { 859 | param ( 860 | [object]$assetInfo 861 | ) 862 | return $assetInfo.updated_at 863 | } 864 | 865 | $UpdateFlag = $false 866 | 867 | if ($InputSchemaDown -eq "0") { 868 | # 下载方案 869 | $SchemaUpdateTimeKey = $KeyTable[$InputSchemaType] + "_schema_update_time" 870 | $SchemaUpdateTime = Get-TimeRecord -filePath $TimeRecordFile -key $SchemaUpdateTimeKey 871 | $SchemaRemoteTime = [datetime]::Parse($(Get-UpdateAtObj -assetInfo $ExpectedSchemaTypeInfo)) 872 | Write-Host "正在检查方案是否需要更新..." -ForegroundColor Yellow 873 | Write-Host "本地时间: $SchemaUpdateTime" -ForegroundColor Green 874 | Write-Host "远程时间: $SchemaRemoteTime" -ForegroundColor Green 875 | if (Compare-UpdateTime -localTime $SchemaUpdateTime -remoteTime $SchemaRemoteTime) { 876 | $UpdateFlag = $true 877 | Write-Host "正在下载方案..." -ForegroundColor Green 878 | Download-Files -assetInfo $ExpectedSchemaTypeInfo -outFilePath $tempSchemaZip 879 | Write-Host "正在解压方案..." -ForegroundColor Green 880 | Expand-ZipFile -zipFilePath $tempSchemaZip -destinationPath $SchemaExtractPath 881 | Write-Host "正在复制文件..." -ForegroundColor Green 882 | # 方案里面没有子文件夹,直接复制到目标目录 883 | $sourceDir = $SchemaExtractPath 884 | if (-not (Test-Path $sourceDir)) { 885 | Write-Host "错误:压缩包中未找到 $sourceDir 目录" -ForegroundColor Red 886 | Remove-Item -Path $tempSchemaZip -Force 887 | Remove-Item -Path $SchemaExtractPath -Recurse -Force 888 | Exit-Tip 1 889 | } 890 | Stop-WeaselServer 891 | # 等待1秒 892 | Start-Sleep -Seconds 1 893 | Get-ChildItem -Path $sourceDir -Recurse | ForEach-Object { 894 | if ($_.Name -notin $SkipFiles) { 895 | $relativePath = $_.FullName.Substring($sourceDir.Length) 896 | $destinationPath = Join-Path $targetDir $relativePath 897 | $destinationDir = [System.IO.Path]::GetDirectoryName($destinationPath) 898 | if (-not (Test-Path $destinationDir)) { 899 | New-Item -ItemType Directory -Path $destinationDir | Out-Null 900 | } 901 | if (Test-Path $_.FullName -PathType Container) { 902 | if ($Debug) { 903 | Write-Host "跳过目录: $($_.Name)" -ForegroundColor Yellow 904 | } 905 | } elseif (Test-Path $_.FullName -PathType Leaf) { 906 | Copy-Item -Path $_.FullName -Destination $destinationPath -Force 907 | } 908 | 909 | if ($Debug) { 910 | Write-Host "正在复制文件: $($_.Name)" -ForegroundColor Green 911 | Write-Host "相对路径: $relativePath" -ForegroundColor Green 912 | Write-Host "目标路径: $destinationPath" -ForegroundColor Green 913 | } 914 | } else { 915 | Write-Host "跳过文件: $($_.Name)" -ForegroundColor Yellow 916 | } 917 | } 918 | 919 | # 将现在的本地时间记录到JSON文件 920 | Save-TimeRecord -filePath $TimeRecordFile -key $SchemaUpdateTimeKey -value $SchemaRemoteTime 921 | # 清理临时文件 922 | Remove-Item -Path $tempSchemaZip -Force 923 | Remove-Item -Path $SchemaExtractPath -Recurse -Force 924 | } 925 | } 926 | 927 | if ($InputDictDown -eq "0") { 928 | # 下载词库 929 | $DictUpdateTimeKey = $KeyTable[$InputSchemaType] + "_dict_update_time" 930 | $DictUpdateTime = Get-TimeRecord -filePath $TimeRecordFile -key $DictUpdateTimeKey 931 | $DictRemoteTime = [datetime]::Parse($(Get-UpdateAtObj -assetInfo $ExpectedDictTypeInfo)) 932 | Write-Host "正在检查词库是否需要更新..." -ForegroundColor Yellow 933 | Write-Host "本地时间: $DictUpdateTime" -ForegroundColor Green 934 | Write-Host "远程时间: $DictRemoteTime" -ForegroundColor Green 935 | if (Compare-UpdateTime -localTime $DictUpdateTime -remoteTime $DictRemoteTime) { 936 | $UpdateFlag = $true 937 | Write-Host "正在下载词库..." -ForegroundColor Green 938 | Download-Files -assetInfo $ExpectedDictTypeInfo -outFilePath $tempDictZip 939 | Write-Host "正在解压词库..." -ForegroundColor Green 940 | Expand-ZipFile -zipFilePath $tempDictZip -destinationPath $DictExtractPath 941 | Write-Host "正在复制文件..." -ForegroundColor Green 942 | $sourceDir = Get-DictExtractedFolderPath -extractPath $DictExtractPath -assetName $KeyTable[$InputSchemaType] 943 | if (-not (Test-Path $sourceDir)) { 944 | Write-Host "错误:压缩包中未找到 $sourceDir 目录" -ForegroundColor Red 945 | Remove-Item -Path $DictExtractPath -Force -Recurse 946 | Exit-Tip 1 947 | } 948 | Stop-WeaselServer 949 | # 等待1秒 950 | Start-Sleep -Seconds 1 951 | if (-not (Test-Path -Path $(Join-Path $targetDir $DictFileSaveDirTable[$DictFileSaveDirTableIndex]))){ 952 | New-Item -ItemType Directory -Path $(Join-Path $targetDir $DictFileSaveDirTable[$DictFileSaveDirTableIndex]) | Out-Null 953 | } 954 | Get-ChildItem -Path $sourceDir | ForEach-Object { 955 | if ($Debug) { 956 | Write-Host "正在复制文件: $($_.Name)" -ForegroundColor Green 957 | } 958 | if (Test-SkipFile -filePath $_.Name) { 959 | Write-Host "跳过文件: $($_.Name)" -ForegroundColor Yellow 960 | } else { 961 | Copy-Item -Path $_.FullName -Destination $(Join-Path $targetDir $DictFileSaveDirTable[$DictFileSaveDirTableIndex]) -Recurse -Force 962 | } 963 | } 964 | 965 | # 将现在的本地时间记录到JSON文件 966 | Save-TimeRecord -filePath $TimeRecordFile -key $DictUpdateTimeKey -value $DictRemoteTime -isDict $true 967 | # 清理临时文件 968 | Remove-Item -Path $DictExtractPath -Recurse -Force 969 | } 970 | } 971 | 972 | function Update-GramModel { 973 | Write-Host "正在下载模型..." -ForegroundColor Green 974 | Download-Files -assetInfo $ExpectedGramTypeInfo -outFilePath $tempGram 975 | Write-Host "正在复制文件..." -ForegroundColor Green 976 | 977 | Stop-WeaselServer 978 | # 等待1秒 979 | Start-Sleep -Seconds 1 980 | Copy-Item -Path $tempGram -Destination $targetDir -Force 981 | # 将现在的本地时间记录到JSON文件 982 | Save-TimeRecord -filePath $TimeRecordFile -key $GramUpdateTimeKey -value $GramRemoteTime 983 | # 清理临时文件 984 | Remove-Item -Path $tempGram -Force 985 | } 986 | 987 | if ($InputGramModel -eq "0") { 988 | # 下载模型 989 | $GramUpdateTimeKey = $GramReleaseTag + "_gram_update_time" 990 | $GramUpdateTime = Get-TimeRecord -filePath $TimeRecordFile -key $GramUpdateTimeKey 991 | $GramRemoteTime = [datetime]::Parse($(Get-UpdateAtObj -assetInfo $ExpectedGramTypeInfo)) 992 | Write-Host "正在检查模型是否需要更新..." -ForegroundColor Yellow 993 | # 检查目标文件 $targetDir/$tempGram 是否存在 994 | $filePath = Join-Path $targetDir $GramModelFileName 995 | if ($Debug) { 996 | Write-Host "模型文件路径: $filePath" -ForegroundColor Green 997 | } 998 | Write-Host "本地时间: $GramUpdateTime" -ForegroundColor Green 999 | Write-Host "远程时间: $GramRemoteTime" -ForegroundColor Green 1000 | if (Compare-UpdateTime -localTime $GramUpdateTime -remoteTime $GramRemoteTime) { 1001 | Update-GramModel 1002 | $UpdateFlag = $true 1003 | } elseif (Test-Path -Path $filePath) { 1004 | if ($UseCnbMirrorSource) { 1005 | # 校验文件大小 1006 | $expectedSize = [int64]$ExpectedGramTypeInfo.size_in_byte 1007 | $actualSize = (Get-Item $filePath).Length 1008 | if ($expectedSize -ne $actualSize) { 1009 | Write-Host "文件大小校验失败,需要更新" -ForegroundColor Red 1010 | Write-Host "期望大小: $expectedSize 字节,实际大小: $actualSize 字节" -ForegroundColor Red 1011 | Remove-Item -Path $filePath -Force 1012 | Update-GramModel 1013 | $UpdateFlag = $true 1014 | } 1015 | } else { 1016 | # 计算目标文件的SHA256 1017 | $localSHA256 = (Get-FileHash $filePath -Algorithm SHA256).Hash.ToLower() 1018 | # 计算远程文件的SHA256 1019 | $remoteSHA256 = $ExpectedGramTypeInfo.digest.Split(":")[1].ToLower() 1020 | # 比较SHA256 1021 | if ($localSHA256 -ne $remoteSHA256) { 1022 | Write-Host "模型SHA256不匹配,需要更新" -ForegroundColor Red 1023 | Update-GramModel 1024 | $UpdateFlag = $true 1025 | } 1026 | } 1027 | } else { 1028 | Write-Host "模型不存在,需要更新" -ForegroundColor Red 1029 | Update-GramModel 1030 | } 1031 | } 1032 | 1033 | Write-Host "操作已完成!文件已部署到 Weasel 配置目录:$($targetDir)" -ForegroundColor Green 1034 | 1035 | if ($UpdateFlag) { 1036 | Start-WeaselServer 1037 | # 等待1秒 1038 | Start-Sleep -Seconds 1 1039 | Write-Host "内容更新,触发小狼毫重新部署..." -ForegroundColor Green 1040 | Start-WeaselReDeploy 1041 | } 1042 | 1043 | Exit-Tip 0 1044 | -------------------------------------------------------------------------------- /Python-全平台版本/Python/万象下载更新.py: -------------------------------------------------------------------------------- 1 | import time 2 | import subprocess 3 | import configparser 4 | import requests 5 | import os 6 | import hashlib 7 | import json 8 | from datetime import datetime, timezone, timedelta 9 | import sys 10 | import zipfile 11 | import shutil 12 | import fnmatch 13 | import re 14 | from typing import Tuple, Optional, List, Dict 15 | from tqdm import tqdm 16 | 17 | UPDATE_TOOLS_VERSION = "DEFAULT_UPDATE_TOOLS_VERSION_TAG" 18 | # ====================== 全局配置 ====================== 19 | # 仓库信息 20 | OWNER = "amzxyz" 21 | REPO = "rime_wanxiang" 22 | # cnb信息 23 | CNB_REPO = "rime-wanxiang" 24 | DICT_TAG = "dict-nightly" 25 | # 模型相关配置 26 | MODEL_REPO = "RIME-LMDG" 27 | MODEL_TAG = "LTS" 28 | MODEL_FILE = "wanxiang-lts-zh-hans.gram" 29 | 30 | CNB_HEADERS = { 31 | "User-Agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Mobile Safari/537.36", 32 | "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", 33 | "Accept": "application/vnd.cnb.web+json" # 确保返回JSON 34 | } 35 | # Zh词库目录 36 | ZH_DICTS = ZH_DICTS_PRO = "dicts" 37 | SCHEME_MAP = { 38 | '1': 'moqi', 39 | '2': 'flypy', 40 | '3': 'zrm', 41 | '4': 'tiger', 42 | '5': 'wubi', 43 | '6': 'hanxin' 44 | } 45 | 46 | # ====================== 系统检测函数 =========================== 47 | def system_check(): 48 | """检查系统类型""" 49 | if sys.platform == 'win32': 50 | return 'windows' 51 | # iOS上a-shell、code app的Python环境sys.pltform也为'darwin',因此取当前解释器路径进行判断 52 | elif sys.platform == 'darwin' and sys.executable.find('Code.app') >= 0: 53 | return 'ios' 54 | elif sys.platform == 'darwin' and sys.executable == 'python3': 55 | return 'ios' 56 | elif sys.platform == 'darwin': 57 | return 'macos' 58 | elif sys.platform == 'ios': 59 | return 'ios' 60 | else: 61 | return 'android/linux' 62 | 63 | SYSTEM_TYPE = system_check() 64 | 65 | # ====================== 界面函数 ====================== 66 | BORDER = "=" * 35 if SYSTEM_TYPE == 'ios' else "-" * 60 67 | SUB_BORDER = "-" * 30 if SYSTEM_TYPE == 'ios' else "-" * 55 68 | INDENT = " " * 2 69 | COLOR = { 70 | "HEADER": "\033[95m", 71 | "OKBLUE": "\033[94m", 72 | "OKCYAN": "\033[96m", 73 | "OKGREEN": "\033[92m", 74 | "WARNING": "\033[93m", 75 | "FAIL": "\033[91m", 76 | "BLACK": "\033[30m", 77 | "RED": "\033[31m", 78 | "GREEN": "\033[32m", 79 | "YELLOW": "\033[33m", 80 | "BLUE": "\033[34m", 81 | "MAGENTA": "\033[35m", 82 | "CYAN": "\033[36m", 83 | "WHITE": "\033[37m", 84 | "BOLD": "\033[1m", 85 | "UNDERLINE": "\033[4m", 86 | "REVERSE": "\033[7m", 87 | "ENDC": "\033[0m" 88 | } 89 | 90 | def print_header(text): 91 | print(f"\n{BORDER}") 92 | print(f"{INDENT}{text.upper()}") 93 | print(f"{BORDER}") 94 | 95 | def print_subheader(text): 96 | print(f"\n{SUB_BORDER}") 97 | print(f"{INDENT}* {text}") 98 | print(f"{SUB_BORDER}") 99 | 100 | def print_success(text): 101 | print(f"{COLOR['OKGREEN']}[√]{COLOR['ENDC']} {text}") 102 | 103 | def print_warning(text): 104 | print(f"{COLOR['OKCYAN']}[!]{COLOR['ENDC']} {text}") 105 | 106 | def print_error(text): 107 | print(f"[×] 错误: {text}") 108 | 109 | 110 | # ====================== win注册表路径配置 ====================== 111 | if SYSTEM_TYPE == 'windows': 112 | import winreg 113 | 114 | REG_PATHS = { 115 | 'rime_user_dir': ( 116 | r"Software\Rime\Weasel", 117 | "RimeUserDir", 118 | winreg.HKEY_CURRENT_USER 119 | ), 120 | 'weasel_root': ( 121 | r"SOFTWARE\WOW6432Node\Rime\Weasel", 122 | "WeaselRoot", 123 | winreg.HKEY_LOCAL_MACHINE 124 | ), 125 | 'server_exe': ( 126 | r"SOFTWARE\WOW6432Node\Rime\Weasel", 127 | "ServerExecutable", 128 | winreg.HKEY_LOCAL_MACHINE 129 | ) 130 | } 131 | 132 | # ====================== 工具函数 ====================== 133 | def get_registry_value(key_path, value_name, hive): 134 | """安全读取注册表值""" 135 | try: 136 | with winreg.OpenKey(hive, key_path) as key: 137 | value, _ = winreg.QueryValueEx(key, value_name) 138 | return value 139 | except (FileNotFoundError, PermissionError, OSError): 140 | return None 141 | 142 | 143 | # ====================== 配置管理器 ====================== 144 | class ConfigManager: 145 | """配置管理类""" 146 | def __init__(self): 147 | self.config_path = self._get_config_path() 148 | self.config = configparser.ConfigParser() 149 | self.rime_engine = '' 150 | self.rime_dir = '' 151 | self.scheme_type = '' 152 | self.zh_dicts_dir = '' 153 | self.reload_flag = False 154 | self.auto_update = False 155 | self._ensure_config_exists() 156 | 157 | def detect_installation_paths(self, show=False): 158 | """自动检测安装路径""" 159 | detected = {} 160 | if SYSTEM_TYPE == 'windows': 161 | for key in REG_PATHS: 162 | path, name, hive = REG_PATHS[key] 163 | detected[key] = get_registry_value(path, name, hive) 164 | 165 | # 智能路径处理 166 | if detected['weasel_root'] and detected['server_exe']: 167 | detected['server_exe'] = os.path.join(detected['weasel_root'], detected['server_exe']) 168 | else: 169 | print_error("无法自动检测到 Weasel 根目录或 WeaselServer.exe。") 170 | print_error("你的小狼毫可能没有安装或配置正确。") 171 | print_error("正在退出程序...") 172 | sys.exit(1) 173 | 174 | defaults = { 175 | 'rime_user_dir': os.path.join(os.environ['APPDATA'], 'Rime') 176 | } 177 | 178 | if not detected["rime_user_dir"] or not os.path.exists(detected['rime_user_dir']): 179 | detected["rime_user_dir"] = defaults["rime_user_dir"] 180 | if not self.reload_flag and show: 181 | print_warning("未检测到小狼毫自定义 RimeUserDir,使用默认路径:" + detected["rime_user_dir"]) 182 | else: 183 | if not self.reload_flag and show: 184 | print_success("检测到小狼毫自定义 RimeUserDir:" + detected["rime_user_dir"]) 185 | elif SYSTEM_TYPE == 'macos': 186 | # 处理macOS 187 | if self.config.get('Settings', 'engine') == '鼠须管': 188 | detected['rime_user_dir'] = os.path.expanduser('~/Library/Rime') 189 | elif self.config.get('Settings', 'engine') == '小企鹅': 190 | detected['rime_user_dir'] = os.path.expanduser('~/.local/share/fcitx5/rime') 191 | else: 192 | detected['rime_user_dir'] = os.path.expanduser('~/Library/Rime') 193 | elif SYSTEM_TYPE == 'ios': 194 | detected['rime_user_dir'] = self.rime_dir 195 | else: 196 | current_file_dir = os.path.dirname(os.path.abspath(__file__)) 197 | if os.path.exists(os.path.join(current_file_dir, 'Rime')): 198 | detected['rime_user_dir'] = os.path.join(current_file_dir, 'Rime') 199 | elif os.path.exists(os.path.join(current_file_dir, 'rime')): 200 | detected['rime_user_dir'] = os.path.join(current_file_dir, 'rime') 201 | else: 202 | os.makedirs(os.path.join(current_file_dir, 'Rime'), exist_ok=True) 203 | detected['rime_user_dir'] = os.path.join(current_file_dir, 'Rime') 204 | 205 | return detected 206 | 207 | 208 | def _check_hamster_path(self) -> bool: 209 | """检查脚本是否放置在正确的Hamster目录下""" 210 | file_dir = os.path.dirname(os.path.abspath(__file__)) 211 | hamster_path_names = os.listdir(file_dir) 212 | if "RIME" in hamster_path_names: 213 | self.rime_dir = os.path.join(file_dir, 'RIME', 'Rime') 214 | return True 215 | elif "Rime" in hamster_path_names: 216 | self.rime_dir = os.path.join(file_dir, 'Rime') 217 | return True 218 | else: 219 | print_error('请将脚本放置到正确的位置(Hamster目录下)') 220 | return False 221 | 222 | def _select_rime_engine(self) -> None: 223 | """选择输入法引擎:鼠须管/小企鹅""" 224 | print(f"\n{BORDER}") 225 | print(f"{INDENT}首次运行引擎选择向导") 226 | print(f"{BORDER}") 227 | print("[1]-鼠须管Squirrel [2]-小企鹅Fcitx5") 228 | 229 | while True: 230 | choice = input(f"{INDENT}请选择输入法引擎:").strip() 231 | if choice == '1': 232 | self.rime_dir = os.path.expanduser('~/Library/Rime') 233 | self.rime_engine = '鼠须管' 234 | # 更新配置文件 235 | self.config.set('Settings', 'engine', self.rime_engine) 236 | return 237 | elif choice == '2': 238 | self.rime_dir = os.path.expanduser('~/.local/share/fcitx5/rime') 239 | self.rime_engine = '小企鹅' 240 | # 更新配置文件 241 | self.config.set('Settings', 'engine', self.rime_engine) 242 | return 243 | else: 244 | print(f"{INDENT}无效的选择,请重新选择。") 245 | 246 | def _get_config_path(self) -> str: 247 | """获取配置文件路径""" 248 | # 检查程序是否是打包后的可执行文件。如果是,sys.frozen 属性会被设置为 True 249 | if getattr(sys, 'frozen', False): 250 | # 如果是打包后的可执行文件,获取可执行文件所在的目录 251 | base_dir = os.path.dirname(sys.executable) 252 | else: 253 | # 如果是普通的 Python 脚本,获取当前脚本文件的绝对路径所在的目录 254 | base_dir = os.path.dirname(os.path.abspath(__file__)) 255 | # 将基础目录和配置文件名 'settings.ini' 拼接成完整的配置文件路径并返回 256 | return os.path.join(base_dir, 'settings.ini') 257 | 258 | def _ensure_config_exists(self) -> None: 259 | """确保配置文件存在,如果不存在则创建一个新的配置文件""" 260 | if SYSTEM_TYPE == 'ios': 261 | if not self._check_hamster_path(): 262 | return 263 | if not os.path.exists(self.config_path): 264 | print_warning("正在创建一个新的配置文件。") 265 | self._init_empty_config() 266 | if SYSTEM_TYPE == 'macos': 267 | self._select_rime_engine() # mac首次运行选择引擎 268 | # self._guide_scheme_type_selection() # 首次运行引导选择方案名称 269 | # self._guide_scheme_selection() # 首次运行引导选择方案 270 | if self._guide_scheme_type_selection() and self._guide_scheme_selection(): 271 | self._write_config() # 写入配置文件 272 | print_success("配置文件创建成功。") 273 | else: 274 | print_error("配置向导失败,请手动配置。") 275 | exit(1) # 终止程序执行 276 | self._show_config_guide() # 配置引导 277 | else: 278 | print_warning(COLOR['YELLOW'] + "配置文件已存在,将加载配置。" + COLOR['ENDC']) 279 | new_config_items = { 280 | 'auto_update': 'false', 281 | } 282 | self._add_new_config_items(new_config_items) 283 | self._try_load_config() 284 | self._print_config_info() # 打印配置信息 285 | self._confirm_config() # 确认配置是否符合预期 286 | 287 | def _add_new_config_items(self, new_config_items: Dict[str, str]) -> None: 288 | """添加或更新配置项""" 289 | changed = False 290 | self.config.read(self.config_path, encoding='utf-8') 291 | for key, value in new_config_items.items(): 292 | if not self.config.has_option('Settings', key): 293 | print_warning(f"添加缺失的配置项: {key} = {value}") 294 | self.config.set('Settings', key, value) 295 | changed = True 296 | if changed: 297 | self._write_config() 298 | 299 | def _print_config_info(self) -> None: 300 | """打印配置信息""" 301 | print(f"\n{BORDER}") 302 | print(f"{INDENT}当前配置信息") 303 | print(f"{BORDER}") 304 | print(f"{INDENT}▪ 方案版本:{self.config['Settings']['scheme_type']}") 305 | print(f"{INDENT}▪ 方案文件:{self.config['Settings']['scheme_file']}") 306 | print(f"{INDENT}▪ 词库文件:{self.config['Settings']['dict_file']}") 307 | if SYSTEM_TYPE == 'macos': 308 | print(f"{INDENT}▪ 输入法引擎:{self.config['Settings']['engine']}") 309 | print(f"{INDENT}▪ 跳过文件目录:{self.config['Settings']['exclude_files']}") 310 | print(f"{BORDER}") 311 | 312 | def _confirm_config(self) -> None: 313 | """确认配置是否符合预期""" 314 | # 如果启用了自动更新,跳过确认步骤 315 | if self.config.getboolean('Settings', 'auto_update', fallback=False): 316 | self.auto_update = True 317 | print_warning("已启用自动更新,跳过配置确认") 318 | return 319 | while True: 320 | choice = input(f"{INDENT}配置是否正确?【Y(y)或回车确认/N(n)重新生成/M(m)修改】: ").strip().lower() 321 | if choice == 'y' or not choice: 322 | print_success("配置正确。") 323 | break 324 | elif choice == 'n': 325 | print_warning("请重新配置生成新的配置文件。") 326 | os.remove(self.config_path) # 删除配置文件 327 | self.reload_flag = True 328 | self._ensure_config_exists() # 重新创建配置文件 329 | break 330 | elif choice == 'm': 331 | if SYSTEM_TYPE == 'ios': 332 | print_warning("iOS平台不支持修改配置文件,请手动编辑 settings.ini 文件。") 333 | else: 334 | if os.name == 'nt': 335 | subprocess.run(['notepad.exe', self.config_path], shell=True) 336 | else: 337 | subprocess.run(['open', self.config_path]) 338 | print_warning("请在打开的配置文件中手动修改,保存后继续执行。") 339 | input("按任意键继续...") 340 | self._try_load_config() # 再次尝试加载配置 341 | self._print_config_info() 342 | break 343 | else: 344 | print_error("无效的输入,请重新输入。") 345 | 346 | def _try_load_config(self) -> None: 347 | """尝试加载配置文件""" 348 | # 加载并验证配置 349 | try: 350 | settings = self.load_config(show=True) 351 | print(f"\n{COLOR['GREEN']}[√] 配置加载成功{COLOR['ENDC']}") 352 | except Exception as e: 353 | print(f"\n{COLOR['FAIL']}❌ 配置加载失败:{str(e)}{COLOR['ENDC']}") 354 | sys.exit(1) 355 | 356 | def _init_empty_config(self) -> None: 357 | """创建空配置""" 358 | self.config['Settings'] = { 359 | 'engine': '', 360 | 'scheme_type': '', 361 | 'scheme_file': '', 362 | 'dict_file': '', 363 | 'use_mirror': 'true', 364 | 'github_token': '', 365 | 'exclude_files': '', 366 | 'auto_update': 'false', 367 | 368 | } 369 | 370 | def _write_config(self) -> None: 371 | """写入配置文件""" 372 | with open(self.config_path, 'w', encoding='utf-8') as f: 373 | self.config.write(f) 374 | 375 | def _guide_scheme_type_selection(self) -> bool: 376 | """首次运行引导选择万象版本""" 377 | print(f"\n{BORDER}") 378 | print(f"{INDENT}首次运行方案版本选择向导") 379 | print(f"{BORDER}") 380 | print("[1]-万象基础版 [2]-万象增强版(支持各种辅助码)") 381 | 382 | while True: 383 | choice = input(f"{INDENT}请选择方案版本(1-2): ").strip() 384 | if choice == '1': 385 | self.scheme_type = 'base' 386 | self.zh_dicts_dir = ZH_DICTS 387 | scheme_file, dict_file = self.get_actual_filenames('base') 388 | self.config.set('Settings', 'scheme_type', self.scheme_type) 389 | self.config.set('Settings', 'scheme_file', scheme_file) 390 | self.config.set('Settings', 'dict_file', dict_file) 391 | print_success(f"已选择方案:万象基础版,方案文件: {scheme_file},词库文件: {dict_file}") 392 | return True 393 | elif choice == '2': 394 | self.scheme_type = 'pro' 395 | self.zh_dicts_dir = ZH_DICTS_PRO 396 | self.config.set('Settings', 'scheme_type', self.scheme_type) 397 | print_success("已选择方案:万象增强版") 398 | return True 399 | else: 400 | print_error("无效的选项,请重新输入") 401 | 402 | def _guide_scheme_selection(self) -> bool: 403 | """首次运行引导选择方案""" 404 | if self.scheme_type == 'pro': 405 | print(f"\n{BORDER}") 406 | print(f"{INDENT}万象Pro首次运行辅助码选择配置向导") 407 | print("[1]-墨奇 [2]-小鹤 [3]-自然码") 408 | print("[4]-虎码 [5]-五笔 [6]-汉心") 409 | 410 | while True: 411 | choice = input("请选择你的辅助码方案(1-7): ").strip() 412 | if choice in SCHEME_MAP: 413 | scheme_key = SCHEME_MAP[choice] 414 | 415 | # 立即获取实际文件名 416 | scheme_file, dict_file = self.get_actual_filenames(scheme_key) 417 | 418 | self.config.set('Settings', 'scheme_file', scheme_file) 419 | self.config.set('Settings', 'dict_file', dict_file) 420 | 421 | print_success(f"已选择方案:{scheme_key.upper()}") 422 | print(f"方案文件: {scheme_file}") 423 | print(f"词库文件: {dict_file}") 424 | return True 425 | print_error("无效的选项,请重新输入") 426 | else: 427 | print_success(f"基础版使用方案文件: {self.config.get('Settings', 'scheme_file')} 和词库文件: {self.config.get('Settings', 'dict_file')}") 428 | return True 429 | 430 | 431 | def get_actual_filenames(self, scheme_key) -> Tuple[str, str]: 432 | """ 433 | 获取实际文件名(带网络请求) 434 | Args: 435 | scheme_key (str): 方案关键字 436 | Returns: 437 | Tuple[str, str]: 方案文件名,词库文件名 438 | """ 439 | try: 440 | if self.scheme_type == 'base': 441 | scheme_pattern = f"*base.zip" 442 | dict_pattern = f"*base*.zip" 443 | else: 444 | scheme_pattern = f"*{scheme_key}*fuzhu.zip" 445 | dict_pattern = f"*{scheme_key}*dicts.zip" 446 | 447 | 448 | scheme_checker = FileChecker( 449 | owner=OWNER, 450 | repo=CNB_REPO if self.config.getboolean('Settings', 'use_mirror') else REPO, 451 | pattern=scheme_pattern, 452 | use_mirror=self.config.getboolean('Settings', 'use_mirror') 453 | ) 454 | dict_checker = FileChecker( 455 | owner=OWNER, 456 | repo=CNB_REPO if self.config.getboolean('Settings', 'use_mirror') else REPO, 457 | pattern=dict_pattern, 458 | use_mirror=self.config.getboolean('Settings', 'use_mirror'), 459 | tag=DICT_TAG 460 | ) 461 | 462 | # 获取文件名 463 | scheme_file = scheme_checker.get_latest_file() 464 | dict_file = dict_checker.get_latest_file() 465 | print(scheme_file, dict_file) 466 | 467 | # 验证文件名是否有效 468 | if not scheme_file or not dict_file: 469 | raise ValueError(f"未找到匹配的文件: {scheme_pattern} 或 {dict_pattern}") 470 | 471 | return scheme_file, dict_file 472 | 473 | except Exception as e: 474 | print_error(f"无法获取最新文件名: {str(e)}") 475 | print_error("请检查网络连接,或关闭代理后重试...") 476 | sys.exit(-1) 477 | 478 | def _show_config_guide(self) -> None: 479 | """配置引导界面""" 480 | # 显示第一个路径检测界面 481 | print(f"\n{BORDER}") 482 | print(f"{INDENT}自动检测路径结果") 483 | print(f"{BORDER}") 484 | 485 | self.config.read(self.config_path, encoding='utf-8') 486 | detected = self.detect_installation_paths() 487 | status_emoji = {True: "✅", False: "❌"} 488 | for key in detected: 489 | exists = os.path.exists(detected[key]) 490 | print(f"{INDENT}{key.ljust(15)}: {status_emoji[exists]} {detected[key]}") 491 | 492 | print(f"\n{INDENT}生成的配置文件路径: {self.config_path}") 493 | 494 | self.display_config_instructions() 495 | 496 | if os.name == 'nt': 497 | os.startfile(self.config_path) 498 | elif os.name == 'posix' and SYSTEM_TYPE == 'macos': 499 | subprocess.Popen(['open', self.config_path]) 500 | else: 501 | None 502 | input("\n请按需修改上述路径,保存后按回车键继续...") 503 | 504 | def display_config_instructions(self) -> None: 505 | """静默显示配置说明""" 506 | print_header("请检查配置文件路径,需用户修改") 507 | print("\n▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂") 508 | print("使用说明:\n") 509 | 510 | path_display = [ 511 | ("[engine]", "Mac端选择的输入法引擎", 'engine'), 512 | ("[scheme_type]", "选择的方案版本", 'scheme_type'), 513 | ("[scheme_file]", "选择的方案文件名称", 'scheme_file'), 514 | ("[dict_file]", "关联的词库文件名称", 'dict_file'), 515 | ("[use_mirror]", "是否使用国内仓库CNB(网址:cnb.cool,默认true)", 'use_mirror'), 516 | ("[github_token]", "GitHub令牌(可选)", 'github_token'), 517 | ("[exclude_files]", "更新时需保留的免覆盖文件(默认为空,逗号分隔...格式如下tips_show.txt", 'exclude_files'), 518 | ("[auto_update]", "是否跳过确认并自动更新(默认false)", 'auto_update'), 519 | ] 520 | 521 | for item in path_display: 522 | print(f" {item[0].ljust(25)}{item[1]}") 523 | print(f" {self.config['Settings'][item[2]]}\n") 524 | 525 | print("▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂") 526 | 527 | 528 | def load_config(self, 529 | system=SYSTEM_TYPE, 530 | show=False, 531 | first_download=False 532 | ) -> Tuple[str, str, str, str, bool, str, list]: 533 | """ 534 | 加载配置文件 535 | Args: 536 | system (str): 系统类型 537 | show (bool): 是否显示小狼毫路径说明 538 | first_download (bool): 是否是第一次下载 539 | Returns: 540 | Tuple[str, str, str, str, bool, str, list]: 配置信息 541 | """ 542 | self.config.read(self.config_path, encoding='utf-8') 543 | config = {k: v.strip('"') for k, v in self.config['Settings'].items()} 544 | github_token = config.get('github_token', '') 545 | 546 | # 读取排除文件配置 547 | exclude_files = [ 548 | pattern.strip() 549 | for pattern in re.split(r',|,', self.config.get('Settings', 'exclude_files', fallback='')) # 同时分割中英文逗号 550 | if pattern.strip() 551 | ] 552 | 553 | self.scheme_type = config.get('scheme_type', 'pro') 554 | if self.scheme_type == 'base': 555 | self.zh_dicts_dir = ZH_DICTS 556 | else: 557 | self.zh_dicts_dir = ZH_DICTS_PRO 558 | 559 | # 验证关键路径 560 | if system == 'windows': 561 | paths = self.detect_installation_paths(show=show) 562 | required_paths = { 563 | '小狼毫服务程序': paths['server_exe'], 564 | '方案解压目录': paths['rime_user_dir'], 565 | '词库解压目录': os.path.join(paths['rime_user_dir'], self.zh_dicts_dir) 566 | } 567 | elif system == 'macos': 568 | paths = self.detect_installation_paths() 569 | required_paths = { 570 | '方案解压目录': paths['rime_user_dir'], 571 | '词库解压目录': os.path.join(paths['rime_user_dir'], self.zh_dicts_dir), 572 | } 573 | elif system == 'ios': 574 | required_paths = { 575 | '方案解压目录': self.rime_dir, 576 | '词库解压目录': os.path.join(self.rime_dir, self.zh_dicts_dir) 577 | } 578 | else: 579 | paths = self.detect_installation_paths() 580 | required_paths = { 581 | '方案解压目录': paths['rime_user_dir'], 582 | '词库解压目录': os.path.join(paths['rime_user_dir'], self.zh_dicts_dir) 583 | } 584 | 585 | if first_download: 586 | missing = [] if os.path.exists(required_paths['方案解压目录']) else [required_paths['方案解压目录']] 587 | else: 588 | missing = [path for name, path in required_paths.items() if not os.path.exists(path)] 589 | if not os.path.exists(required_paths['方案解压目录']): 590 | print(f"\n{COLOR['FAIL']}关键路径配置错误:{COLOR['ENDC']}") 591 | for name in missing: 592 | print(f"{INDENT}{name}: {required_paths[name]}") 593 | print(f"\n{INDENT}可能原因:") 594 | if system == 'windows': 595 | print(f"{INDENT}1. 小狼毫输入法未正确安装") 596 | print(f"{INDENT}2. 注册表信息被修改") 597 | print(f"{INDENT}3. 自定义路径配置错误") 598 | elif system == 'macos': 599 | print(f"{INDENT}1. 鼠须管或小企鹅输入法未正确安装") 600 | print(f"{INDENT}2. 自定义路径配置错误") 601 | elif system == 'ios': 602 | print(f"{INDENT}1. 该路径不存在") 603 | print(f"{INDENT}2. 没有将该脚本放置在Hamster路径下") 604 | else: 605 | print(f"{INDENT}1. 该路径不存在") 606 | print(f"{INDENT}2. 没有将该脚本放置在正确路径下") 607 | sys.exit(1) 608 | 609 | if missing: 610 | self.ensure_directories(missing) 611 | 612 | 613 | return ( 614 | config['engine'], 615 | config['scheme_type'], 616 | config['scheme_file'], 617 | config['dict_file'], 618 | self.config.getboolean('Settings', 'use_mirror'), 619 | github_token, 620 | exclude_files, 621 | ) 622 | 623 | def ensure_directories(self, dirs: List) -> None: 624 | """目录保障系统""" 625 | for dir in dirs: 626 | os.makedirs(dir, exist_ok=True) 627 | 628 | 629 | class FileChecker: 630 | def __init__(self, owner, repo, pattern, use_mirror, tag=None): 631 | self.owner = owner 632 | self.repo = repo 633 | self.pattern_regex = re.compile(pattern.replace('*', '.*')) 634 | self.tag = tag 635 | self.use_mirror = use_mirror 636 | 637 | def get_latest_file(self) -> Optional[str]: 638 | """获取匹配模式的最新文件""" 639 | if self.use_mirror: 640 | releases = self._get_cnb_releases() 641 | for asset in releases.get("assets", []): 642 | if self.pattern_regex.match(asset['name']): 643 | return asset['name'] 644 | else: 645 | releases = self._get_releases() 646 | for release in releases: 647 | for asset in release.get("assets", []): 648 | if self.pattern_regex.match(asset['name']): 649 | return asset['name'] 650 | return None 651 | 652 | def _get_releases(self) -> List: 653 | """根据标签获取对应的Release""" 654 | if self.tag: 655 | # 获取指定标签的Release 656 | url = f"https://api.github.com/repos/{self.owner}/{self.repo}/releases/tags/{self.tag}" 657 | else: 658 | # 获取所有Release(按时间排序) 659 | url = f"https://api.github.com/repos/{self.owner}/{self.repo}/releases" 660 | 661 | response = requests.get(url) 662 | response.raise_for_status() 663 | # 返回结果处理:指定标签时为单个Release,否则为列表 664 | return [response.json()] if self.tag else response.json() 665 | 666 | def _get_cnb_releases(self) -> Dict: 667 | headers = CNB_HEADERS 668 | url = f'https://cnb.cool/{self.owner}/{self.repo}/-/releases' 669 | response = requests.get(url=url, headers=headers) 670 | if response.status_code == 200: 671 | releases_all = response.json() 672 | releases_list = releases_all['releases'] 673 | for release in releases_list: 674 | if self.tag: 675 | if "词库" in release.get("title"): 676 | return release # 词库 677 | if "万象拼音输入方案" in release.get("title"): 678 | return release # 方案 679 | return {} 680 | 681 | 682 | # ====================== 更新基类 ====================== 683 | class UpdateHandler: 684 | """更新系统核心基类""" 685 | def __init__(self, config_manager): 686 | """ 687 | 初始化更新处理器 688 | Args: 689 | config_manager (ConfigManager): 配置管理器 690 | first_download (bool): 是否是第一次下载,用于传递给load_config方法,默认False,需手动设置为True 691 | """ 692 | self.config_manager = config_manager 693 | ( 694 | self.engine, 695 | self.scheme_type, 696 | self.scheme_file, 697 | self.dict_file, 698 | self.use_mirror, 699 | self.github_token, 700 | self.exclude_files 701 | ) = config_manager.load_config(show=False) 702 | ( 703 | self.custom_dir, 704 | self.extract_path, 705 | self.dict_extract_path, 706 | self.weasel_server 707 | ) = self.get_all_dir() 708 | os.makedirs(self.custom_dir, exist_ok=True) 709 | self.update_info = None 710 | 711 | def has_update(self) -> bool: 712 | """检查是否有更新可用""" 713 | # 如果没有更新信息或本地没有获取本地时间 714 | if not self.update_info: 715 | return False 716 | 717 | remote_time = datetime.strptime(self.update_info["update_time"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) 718 | local_time = self.get_local_time() 719 | 720 | # 如果本地没有时间记录或有新更新 721 | return not local_time or remote_time > local_time 722 | 723 | def get_local_time(self) -> Optional[datetime]: 724 | """获取本地记录的更新时间""" 725 | return None 726 | 727 | def get_all_dir(self) -> Tuple[str, str, str, str]: 728 | """获取所有目录""" 729 | rime_user_dir = self.config_manager.detect_installation_paths().get('rime_user_dir', '') 730 | server = self.config_manager.detect_installation_paths().get('server_exe', '') 731 | zh_dicts_dir = self.config_manager.zh_dicts_dir 732 | return ( 733 | os.path.join(rime_user_dir, 'UpdateCache'), 734 | rime_user_dir, 735 | os.path.join(rime_user_dir, zh_dicts_dir), 736 | server 737 | ) 738 | 739 | def get_old_file_list(self, old_exists_temp_zip: str, new_temp_zip: str, is_dict: bool = False) -> Tuple[List[str], List[str]]: 740 | """ 741 | 获取旧版本压缩包中的文件路径(用于清理) 742 | 743 | Args: 744 | old_exists_temp_zip: 旧版本 zip 压缩包路径 745 | new_temp_zip: 新版本 zip 压缩包路径 746 | is_dict: 是否是词库(词库路径不同,处理略有区别) 747 | 748 | Returns: 749 | Tuple: 750 | - 所有应删除的旧文件(非排除项) 751 | - 新版本中不再使用的文件或目录路径 752 | """ 753 | def is_file(path): return os.path.isfile(path) 754 | def is_dir(path): return os.path.isdir(path) 755 | 756 | whole_old_file_paths: List[str] = [] 757 | should_delete_paths: List[str] = [] 758 | 759 | old_members = new_members = [] 760 | try: 761 | with zipfile.ZipFile(old_exists_temp_zip, 'r') as old_zip: 762 | for i in old_zip.namelist(): 763 | try: 764 | old_members.append(i.encode('cp437').decode('utf-8')) 765 | except: 766 | old_members.append(i) 767 | 768 | if new_temp_zip and os.path.isfile(new_temp_zip): 769 | with zipfile.ZipFile(new_temp_zip, 'r') as new_zip: 770 | for i in new_zip.namelist(): 771 | try: 772 | new_members.append(i.encode('cp437').decode('utf-8')) 773 | except: 774 | new_members.append(i) 775 | 776 | # 处理词库情况下的路径差异 777 | if is_dict: 778 | # 去除可能有的目录前缀 779 | new_members = [m.split('/')[-1] for m in new_members if m.split('/')[-1]] 780 | old_members = [m.split('/')[-1] for m in old_members if m.split('/')[-1]] 781 | 782 | # 新版本中不再包含的旧文件 783 | should_delete_members = [m for m in old_members if m not in new_members] 784 | 785 | extract_path = self.dict_extract_path if is_dict else self.extract_path 786 | 787 | # 所有旧文件路径 788 | whole_old_file_paths = [ 789 | path for path in (os.path.join(extract_path, name) for name in old_members) 790 | if is_file(path) 791 | ] 792 | 793 | # 判断函数根据 is_dict 选择 794 | check_func = is_file if is_dict else is_dir 795 | 796 | # 新版本中不再使用的文件/目录路径 797 | should_delete_paths = [ 798 | path for path in (os.path.join(extract_path, name) for name in should_delete_members) 799 | if check_func(path) 800 | ] 801 | 802 | # 排除指定不删除文件 803 | if getattr(self, "exclude_files", []): 804 | excluded = [] 805 | for ex in self.exclude_files: 806 | excluded.extend([f for f in whole_old_file_paths if ex in f]) 807 | 808 | if excluded: 809 | print("以下为排除文件不删除:", ", ".join(excluded)) 810 | whole_old_file_paths = [f for f in whole_old_file_paths if f not in excluded] 811 | 812 | except Exception as e: 813 | print_warning(f"无法获取需要清理的旧文件或目录:{e}") 814 | 815 | return whole_old_file_paths, should_delete_paths 816 | 817 | 818 | def _delete_old_files(self, old_file_list: List, old_dir_list: List) -> None: 819 | """ 820 | 获取旧的压缩包文件 821 | Args: 822 | old_file_list: 获取到的需要删除的文件列表 823 | """ 824 | if hasattr(self, 'terminate_processes'): 825 | # 终止进程 826 | self.terminate_processes() 827 | # 移除不再使用的文件夹 828 | for file_dir in old_dir_list: 829 | if os.path.exists(file_dir): 830 | shutil.rmtree(file_dir) 831 | # 移除旧版本文件 832 | for file in old_file_list: 833 | if os.path.exists(file): 834 | os.remove(file) 835 | 836 | 837 | 838 | def save_record(self, record_file: str, property_type: str, property_name: str, info: dict) -> None: 839 | """ 840 | 保存更新记录 841 | Args: 842 | record_file: 保存路径 843 | property_type: 类型:方案、词库、模型 844 | property_name: 名称:写入文件的方案、词库、模型名称(来自GitHub) 845 | info: 保存的信息 846 | """ 847 | # 保存记录 848 | with open(record_file, 'w') as f: 849 | json.dump({ 850 | property_type: property_name, 851 | "update_time": info["update_time"], 852 | "tag": info.get("tag", ""), 853 | "apply_time": datetime.now(timezone.utc).isoformat(), 854 | "sha256": info.get("sha256", ""), 855 | "cnb_id": info.get("id", "") 856 | }, f) 857 | 858 | 859 | def remote_api_request(self, url, use_mirror=False, output_json=True) -> Optional[Dict]: 860 | """ 861 | 带令牌认证的API请求 862 | Args: 863 | url (str): API请求的URL 864 | Returns: 865 | dict: API响应的JSON数据 866 | """ 867 | if use_mirror: 868 | headers = CNB_HEADERS 869 | else: 870 | headers = {"User-Agent": "RIME-Updater/1.0"} 871 | if self.github_token: 872 | headers["Authorization"] = f"Bearer {self.github_token}" 873 | 874 | max_retries = 2 875 | for attempt in range(max_retries + 1): 876 | try: 877 | response = requests.get(url, headers=headers) 878 | response.raise_for_status() 879 | if output_json: 880 | if use_mirror: 881 | releases_list = response.json()['releases'] 882 | return releases_list 883 | return response.json() 884 | else: 885 | return response 886 | 887 | except requests.HTTPError as e: 888 | if e.response.status_code == 401: 889 | print_error("GitHub令牌无效或无权限") 890 | elif e.response.status_code == 403: 891 | print_error("权限不足或触发次级速率限制") 892 | else: 893 | print_error(f"HTTP错误: {e.response.status_code}") 894 | return None 895 | except requests.ConnectionError: 896 | print_error("网络连接失败") 897 | if attempt < max_retries: 898 | time.sleep(5) 899 | continue 900 | return None 901 | except requests.RequestException as e: 902 | print_error(f"请求异常: {str(e)}") 903 | return None 904 | 905 | return None 906 | 907 | 908 | def download_file(self, url, save_path, is_continue) -> bool: 909 | """ 910 | 带进度显示的稳健下载 911 | Args: 912 | url (str): 下载链接 913 | save_path (str): 保存路径 914 | is_continue (bool): 是否断点续传 915 | """ 916 | try: 917 | # 统一提示使用cnb或GitHub状态 918 | if self.use_mirror: 919 | print(f"{COLOR['OKBLUE']}[i] 正在使用 https://cnb.cool 下载{COLOR['ENDC']}") 920 | # print(f"{COLOR['WARNING']}注意: 如果使用代理,请确保关闭后再尝试下载{COLOR['ENDC']}") 921 | else: 922 | print(f"{COLOR['OKCYAN']}[i] 正在使用 https://github.com 下载{COLOR['ENDC']}") 923 | 924 | headers = {} 925 | # 获取已下载进度 926 | if is_continue: 927 | downloaded = os.path.getsize(save_path) 928 | else: 929 | downloaded = 0 930 | headers['Range'] = f'bytes={downloaded}-' 931 | 932 | response = requests.get(url, headers=headers, stream=True) 933 | total_size = int(response.headers.get('content-length', 0)) + downloaded 934 | block_size = 8192 935 | 936 | # 使用 tqdm 包装响应内容的迭代器 937 | with open(save_path, 'ab') as f: 938 | # tqdm 的 total 参数设置为文件总大小,单位为字节 939 | with tqdm(total=total_size, initial=downloaded, unit='B', unit_scale=True, desc="下载中") as pbar: 940 | for data in response.iter_content(block_size): 941 | f.write(data) 942 | pbar.update(len(data)) # 更新进度条 943 | return True 944 | except Exception as e: 945 | print_error(f"下载失败: {str(e)}") 946 | return False 947 | 948 | def extract_zip(self, zip_path, target_dir, is_dict=False) -> bool: 949 | """ 950 | 智能解压系统(支持排除文件) 951 | Args: 952 | zip_path (str): 压缩文件路径 953 | target_dir (str): 解压目标路径 954 | is_dict (bool): 是否为词库文件(决定解压方式) 955 | """ 956 | def get_common_base_dir(members): 957 | if not members: 958 | return "" 959 | try: 960 | common_prefix = os.path.commonprefix(members) 961 | if common_prefix: 962 | return os.path.dirname(common_prefix) + '/' 963 | return "" 964 | except: 965 | return "" 966 | 967 | try: 968 | with zipfile.ZipFile(zip_path, 'r') as zip_ref: 969 | exclude_patterns = self.exclude_files # 获取排除模式 970 | 971 | members = [] 972 | info_map = {} # 解码后名字 → ZipInfo 映射 973 | 974 | for info in zip_ref.infolist(): 975 | try: 976 | decoded_name = info.filename.encode('cp437').decode('utf-8') 977 | except: 978 | decoded_name = info.filename 979 | 980 | if info.is_dir(): 981 | continue 982 | 983 | members.append(decoded_name) 984 | info_map[decoded_name] = info 985 | 986 | # 计算实际需要解压的文件数量 987 | valid_members = [] 988 | for member in members: 989 | # 标准化路径格式 990 | normalized_path = os.path.normpath(member.replace('/', os.sep)) 991 | file_name = os.path.basename(normalized_path) 992 | # 检查排除规则 993 | exclude = any( 994 | fnmatch.fnmatch(normalized_path, pattern) or 995 | fnmatch.fnmatch(file_name, pattern) 996 | for pattern in exclude_patterns 997 | ) 998 | if not exclude: 999 | valid_members.append(member) 1000 | else: 1001 | print_warning(f"跳过排除文件: {normalized_path}") 1002 | 1003 | # 使用有效文件数量作为进度条的总数 1004 | with tqdm(total=len(valid_members), desc="解压中") as pbar: 1005 | for member in valid_members: 1006 | # 计算相对路径 1007 | if is_dict: 1008 | base_dir = get_common_base_dir(valid_members) 1009 | if base_dir and member.startswith(base_dir): 1010 | relative_path = member[len(base_dir):] 1011 | else: 1012 | relative_path = member 1013 | else: 1014 | base_dir = get_common_base_dir(valid_members) 1015 | if base_dir and member.startswith(base_dir): 1016 | relative_path = member[len(base_dir):] 1017 | else: 1018 | relative_path = member 1019 | 1020 | # 标准化路径 1021 | normalized_path = os.path.normpath(relative_path.replace('/', os.sep)) 1022 | target_path = os.path.join(target_dir, normalized_path) 1023 | os.makedirs(os.path.dirname(target_path), exist_ok=True) 1024 | info = info_map[member] 1025 | with zip_ref.open(info) as src, open(target_path, 'wb') as dst: 1026 | dst.write(src.read()) 1027 | pbar.update(1) # 更新进度条 1028 | 1029 | return True 1030 | except zipfile.BadZipFile: 1031 | print_error("ZIP文件损坏") 1032 | return False 1033 | except Exception as e: 1034 | print_error(f"解压失败: {str(e)}") 1035 | return False 1036 | 1037 | 1038 | if SYSTEM_TYPE == 'windows': 1039 | def terminate_processes(self): 1040 | """组合式进程终止策略""" 1041 | if not self.graceful_stop(): # 先尝试优雅停止 1042 | self.hard_stop() # 失败则强制终止 1043 | 1044 | def graceful_stop(self): 1045 | """优雅停止服务""" 1046 | try: 1047 | subprocess.run( 1048 | [self.weasel_server, "/q"], 1049 | check=True, 1050 | stdout=subprocess.DEVNULL, 1051 | stderr=subprocess.DEVNULL, 1052 | creationflags=subprocess.CREATE_NO_WINDOW 1053 | ) 1054 | time.sleep(0.5) 1055 | print_success("服务已优雅退出") 1056 | return True 1057 | except subprocess.CalledProcessError as e: 1058 | print_warning(f"优雅退出失败: {e}") 1059 | return False 1060 | except Exception as e: 1061 | print_error(f"未知错误: {str(e)}") 1062 | return False 1063 | 1064 | def hard_stop(self): 1065 | """强制终止保障""" 1066 | print_subheader("强制终止残留进程") 1067 | for _ in range(3): 1068 | subprocess.run(["taskkill", "/IM", "WeaselServer.exe", "/F"], 1069 | shell=True, stderr=subprocess.DEVNULL) 1070 | subprocess.run(["taskkill", "/IM", "WeaselDeployer.exe", "/F"], 1071 | shell=True, stderr=subprocess.DEVNULL) 1072 | time.sleep(0.5) 1073 | print_success("进程清理完成") 1074 | 1075 | def deploy_weasel(self): 1076 | """智能部署引擎""" 1077 | try: 1078 | self.terminate_processes() 1079 | 1080 | # 服务启动重试机制 1081 | for retry in range(3): 1082 | try: 1083 | print_subheader("启动小狼毫服务") 1084 | subprocess.Popen( 1085 | [self.weasel_server], 1086 | stdout=subprocess.DEVNULL, 1087 | stderr=subprocess.DEVNULL, 1088 | creationflags=subprocess.CREATE_NO_WINDOW 1089 | ) 1090 | time.sleep(2) 1091 | break 1092 | except Exception as e: 1093 | if retry == 2: 1094 | raise 1095 | print_warning(f"服务启动失败,重试({retry+1}/3)...") 1096 | time.sleep(1) 1097 | 1098 | # 部署执行与验证 1099 | print_subheader("执行部署操作") 1100 | deployer = os.path.join(os.path.dirname(self.weasel_server), "WeaselDeployer.exe") 1101 | result = subprocess.run( 1102 | [deployer, "/deploy"], 1103 | # capture_output=True, 1104 | # text=True, 1105 | # creationflags=subprocess.CREATE_NO_WINDOW 1106 | stdout=subprocess.PIPE, 1107 | stderr=subprocess.PIPE, 1108 | creationflags=subprocess.CREATE_NO_WINDOW 1109 | ) 1110 | 1111 | if result.returncode != 0: 1112 | raise Exception(f"部署失败: {result.stderr.strip()}") 1113 | 1114 | # print_success("部署成功完成") 1115 | return True 1116 | except Exception as e: 1117 | print_error(f"部署失败: {str(e)}") 1118 | return False 1119 | 1120 | if SYSTEM_TYPE == 'macos': 1121 | def deploy_for_mac(self) -> bool: 1122 | """macOS自动部署""" 1123 | if self.engine == '鼠须管': 1124 | executable = r"/Library/Input Methods/Squirrel.app/Contents/MacOS/Squirrel" 1125 | cmd = ["--reload"] 1126 | else: 1127 | executable = r"/Library/Input Methods/Fcitx5.app/Contents/bin/fcitx5-curl" 1128 | cmd = ["/config/addon/rime/deploy", "-X", "POST", "-d", "{}"] 1129 | 1130 | if os.path.exists(executable): 1131 | print_warning("即将进行自动部署,请查看通知中心确认部署") 1132 | time.sleep(2) 1133 | try: 1134 | subprocess.run([executable] + cmd, check=True, capture_output=True, text=True) 1135 | print_success("已执行自动部署") 1136 | return True 1137 | except subprocess.CalledProcessError as e: 1138 | print_error("自动部署失败:{e},请手动部署") 1139 | return False 1140 | else: 1141 | print_error("找不到可执行文件:{executable}") 1142 | return False 1143 | 1144 | 1145 | # ====================== 组合更新器 ====================== 1146 | class CombinedUpdater: 1147 | """组合更新处理器 - 同时检查方案和词库更新""" 1148 | def __init__(self, config_manager): 1149 | self.config_manager = config_manager 1150 | # 初始化子更新器 1151 | self.scheme_updater = SchemeUpdater(config_manager) 1152 | self.dict_updater = DictUpdater(config_manager) 1153 | self.model_updater = ModelUpdater(config_manager) 1154 | self.script_updater = ScriptUpdater(config_manager) 1155 | # 存储共享的releases数据 1156 | self.shared_releases = None 1157 | # 文件名重试计数器 1158 | self.filename_retry_count: int = 0 1159 | def fetch_all_updates(self) -> None: 1160 | """获取所有更新信息""" 1161 | url = f"https://api.github.com/repos/{OWNER}/{REPO}/releases" 1162 | use_mirror = self.config_manager.config.getboolean('Settings', 'use_mirror', fallback=False) 1163 | if use_mirror: 1164 | url = f"https://cnb.cool/{OWNER}/{CNB_REPO}/-/releases" 1165 | self.shared_releases = self.scheme_updater.remote_api_request( 1166 | url = url, 1167 | use_mirror = use_mirror 1168 | ) 1169 | # 使用共享的releases数据检查方案和词库更新 1170 | self.scheme_updater.update_info = self._extract_scheme_update() 1171 | self.dict_updater.update_info = self._extract_dict_update() 1172 | # 如果方案或词库找不到更新,自动更新文件名 1173 | if not self.scheme_updater.update_info or not self.dict_updater.update_info: 1174 | self.refresh_filenames() 1175 | # 模型更新独立检查 1176 | self.model_updater.update_info = self.model_updater.check_update() 1177 | # 脚本更新独立检查 1178 | self.script_updater.update_info = self.script_updater.check_update() 1179 | 1180 | def refresh_filenames(self) -> None: 1181 | """自动更新文件名并刷新配置""" 1182 | if self.filename_retry_count >= 2: # 最多重试2次 1183 | print_warning("文件名自动更新已达最大重试次数") 1184 | return 1185 | print_subheader("检测到文件名变更,自动更新配置...") 1186 | self.filename_retry_count += 1 1187 | # 获取当前方案类型和key 1188 | scheme_type = self.config_manager.scheme_type 1189 | scheme_key = self.extract_scheme_key() 1190 | # 获取新的实际文件名 1191 | try: 1192 | new_scheme_file, new_dict_file = self.config_manager.get_actual_filenames(scheme_key) 1193 | 1194 | # 更新配置 1195 | self.config_manager.config.set('Settings', 'scheme_file', new_scheme_file) 1196 | self.config_manager.config.set('Settings', 'dict_file', new_dict_file) 1197 | self.config_manager._write_config() 1198 | print_success(f"方案文件更新为: {new_scheme_file}") 1199 | print_success(f"词库文件更新为: {new_dict_file}") 1200 | # 刷新更新器实例 1201 | self.scheme_updater = SchemeUpdater(self.config_manager) 1202 | self.dict_updater = DictUpdater(self.config_manager) 1203 | # 重新获取更新信息 1204 | self.scheme_updater.update_info = self._extract_scheme_update() 1205 | self.dict_updater.update_info = self._extract_dict_update() 1206 | except Exception as e: 1207 | print_error(f"文件名自动更新失败: {str(e)}") 1208 | 1209 | def extract_scheme_key(self) -> str: 1210 | """从当前方案文件名中提取方案key""" 1211 | try: 1212 | current_file = self.config_manager.config.get('Settings', 'scheme_file') 1213 | except configparser.NoOptionError: 1214 | current_file = "" 1215 | if self.config_manager.scheme_type == 'base': 1216 | return 'base' 1217 | # 增强版从文件名提取key 1218 | for key in SCHEME_MAP.values(): 1219 | if key in current_file: 1220 | return key 1221 | return list(SCHEME_MAP.values())[0] 1222 | 1223 | def _extract_scheme_update(self) -> Optional[Dict]: 1224 | """从仓库数据中提取方案更新""" 1225 | if not self.shared_releases: 1226 | return None 1227 | 1228 | for release in self.shared_releases: 1229 | for asset in release.get("assets", []): 1230 | if asset["name"] == self.scheme_updater.scheme_file: 1231 | update_description = release.get("body", "无更新说明") 1232 | return { 1233 | "url": asset.get("browser_download_url") or "https://cnb.cool" + asset.get("path"), 1234 | "update_time": asset.get("updated_at"), 1235 | "tag": release.get("tag_name") or release.get("tag_ref").split('/')[-1], # 前面是GitHub上tag内容,后面是cnb上tag内容,两者都是版本信息 1236 | "description": update_description, 1237 | "sha256": asset.get("digest").split(':')[-1] if asset.get("digest","") else "", # 仅GitHub 1238 | "id": asset.get("id", "") # 仅cnb 1239 | } 1240 | return None 1241 | 1242 | def _extract_dict_update(self) -> Optional[Dict]: 1243 | """从仓库数据中提取词库更新""" 1244 | if not self.shared_releases: 1245 | return None 1246 | 1247 | for release in self.shared_releases: 1248 | for asset in release.get("assets", []): 1249 | if asset["name"] == self.dict_updater.dict_file: 1250 | return { 1251 | "url": asset.get("browser_download_url") or "https://cnb.cool" + asset.get("path"), 1252 | "update_time": asset.get("updated_at"), 1253 | "tag": release.get("tag_name") or release.get("tag_ref").split('/')[-1], # 前面是GitHub上tag内容,后面是cnb上tag内容,两者都是版本信息, 1254 | "sha256": asset.get("digest").split(':')[-1] if asset.get("digest","") else "", # 仅GitHub 1255 | "id": asset.get("id", "") # 仅cnb 1256 | } 1257 | return None 1258 | 1259 | 1260 | # ====================== 方案更新 ====================== 1261 | class SchemeUpdater(UpdateHandler): 1262 | """方案更新处理器""" 1263 | def __init__(self, config_manager): 1264 | super().__init__(config_manager) 1265 | self.record_file = os.path.join(self.custom_dir, "scheme_record.json") 1266 | 1267 | 1268 | def run(self) -> int: 1269 | """ 1270 | return: 1271 | -1: 更新失败 1272 | 0: 已经是最新/无可用更新 1273 | 1: 更新成功 1274 | """ 1275 | print_header("方案更新流程") 1276 | # 使用缓存信息而不是重复API调用 1277 | remote_info = self.update_info 1278 | 1279 | # 如果没有缓存的更新信息或者本地比远程新,不需要更新 1280 | if not remote_info or not self.has_update(): 1281 | print_warning("未找到可用更新") 1282 | return 0 1283 | 1284 | # 时间比较 1285 | remote_time = datetime.strptime(remote_info["update_time"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) 1286 | local_time = self.get_local_time() 1287 | 1288 | if local_time and remote_time <= local_time: 1289 | print_success("当前已是最新方案") 1290 | return 0 # 没有更新 1291 | 1292 | target_file = os.path.join(self.custom_dir, self.scheme_file) 1293 | # 校验本地文件和远端文件sha256 1294 | if remote_info['sha256']: 1295 | if os.path.exists(target_file) and self.file_compare(remote_info['sha256'], target_file): 1296 | print_success("文件内容未变化,将更新本地保存的记录") 1297 | self.save_record(self.record_file, "scheme_file", self.scheme_file, remote_info) 1298 | return 0 1299 | 1300 | # 下载更新 1301 | _suffix = remote_info['sha256'] or remote_info['id'] 1302 | temp_file = os.path.join(self.custom_dir, f"temp_scheme_{_suffix}.zip") 1303 | if os.path.exists(temp_file): 1304 | is_continue = True 1305 | else: 1306 | is_continue = False 1307 | for old_should_drop in fnmatch.filter(os.listdir(self.custom_dir), "temp_scheme*.zip"): 1308 | os.remove(os.path.join(self.custom_dir, old_should_drop)) 1309 | if not self.download_file(remote_info["url"], temp_file, is_continue): 1310 | return -1 1311 | 1312 | # 方案变更时清除旧文件 1313 | self.clean_old_schema() 1314 | # 获取上次下载的压缩包的内容 1315 | old_files, old_dirs = self.get_old_file_list(target_file, temp_file) 1316 | if old_files or old_dirs: 1317 | self._delete_old_files(old_files, old_dirs) 1318 | print_warning("已移除上个版本的方案文件及残余文件夹") 1319 | 1320 | 1321 | # 应用更新 1322 | self.apply_update(temp_file, target_file, remote_info) 1323 | # self.clean_build() 1324 | print_success("方案更新完成") 1325 | return 1 1326 | 1327 | def get_local_time(self) -> Optional[datetime]: 1328 | if not os.path.exists(self.record_file): 1329 | return None 1330 | try: 1331 | with open(self.record_file, 'r') as f: 1332 | data = json.load(f) 1333 | # 读取本地记录的update_time 1334 | return datetime.strptime(data["update_time"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) 1335 | except: 1336 | return None 1337 | 1338 | def file_compare(self, remote_hash, file2) -> bool: 1339 | hash1 = remote_hash 1340 | hash2 = calculate_sha256(file2) 1341 | return hash1 == hash2 1342 | 1343 | def apply_update(self, temp, target, info) -> None: 1344 | """ 1345 | 应用更新(替换文件) 1346 | Args: 1347 | temp (str): 临时文件路径 1348 | target (str): 目标文件路径 1349 | info (dict): 更新信息 1350 | """ 1351 | if hasattr(self, 'terminate_processes'): 1352 | # 终止进程 1353 | self.terminate_processes() 1354 | # 解压文件 1355 | if not self.extract_zip(temp, self.extract_path): 1356 | raise Exception("解压失败") 1357 | # 解压成功重命名文件 1358 | if os.path.exists(target): 1359 | os.remove(target) 1360 | os.rename(temp, target) 1361 | # 保存记录 1362 | self.save_record(self.record_file, "scheme_file", self.scheme_file, info) 1363 | 1364 | def clean_build(self) -> None: 1365 | """清理build目录""" 1366 | build_dir = os.path.join(self.extract_path, "build") 1367 | if os.path.exists(build_dir): 1368 | shutil.rmtree(build_dir) 1369 | print_success("已清理build目录") 1370 | 1371 | def clean_old_schema(self) -> None: 1372 | """当变更所使用的方案时,删除旧文件""" 1373 | for file in os.listdir(self.custom_dir): 1374 | if 'rime-wanxiang' in file and file != self.scheme_file: 1375 | old_schema_files, old_schema_dirs = self.get_old_file_list(file, None) 1376 | self._delete_old_files(old_schema_files, old_schema_dirs) 1377 | os.remove(os.path.join(self.custom_dir, file)) 1378 | print_warning("已移除旧方案zip文件") 1379 | 1380 | 1381 | # ====================== 词库更新 ====================== 1382 | class DictUpdater(UpdateHandler): 1383 | """词库更新处理器""" 1384 | def __init__(self, config_manager): 1385 | super().__init__(config_manager) 1386 | self.target_tag = DICT_TAG 1387 | self.record_file = os.path.join(self.custom_dir, "dict_record.json") 1388 | 1389 | def get_local_time(self) -> Optional[datetime]: 1390 | """获取本地记录的更新时间""" 1391 | if not os.path.exists(self.record_file): 1392 | return None 1393 | try: 1394 | with open(self.record_file, 'r') as f: 1395 | data = json.load(f) 1396 | # 读取本地记录的update_time 1397 | return datetime.strptime(data["update_time"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) 1398 | except: 1399 | return None 1400 | 1401 | def file_compare(self, remote_hash, file2) -> bool: 1402 | """sha256对比""" 1403 | return remote_hash == calculate_sha256(file2) 1404 | 1405 | def apply_update(self, temp, target, info) -> None: 1406 | """应用更新(替换文件), 参数不再需要传递路径,使用实例变量 """ 1407 | try: 1408 | # 终止进程 1409 | if hasattr(self, 'terminate_processes'): 1410 | self.terminate_processes() 1411 | # 替换文件(使用明确的实例变量) 1412 | if os.path.exists(target): 1413 | os.remove(target) 1414 | os.rename(temp, target) 1415 | # 解压到配置目录 1416 | if not self.extract_zip( 1417 | target, 1418 | self.dict_extract_path, 1419 | is_dict=True 1420 | ): 1421 | raise Exception("解压失败") 1422 | 1423 | # 保存记录 1424 | self.save_record(self.record_file, "dict_file", self.dict_file, info) 1425 | except Exception as e: 1426 | # 清理残留文件 1427 | if os.path.exists(temp): 1428 | os.remove(temp) 1429 | raise 1430 | 1431 | def run(self) -> int: 1432 | """ 1433 | 执行更新 1434 | return: 1435 | -1: 更新失败 1436 | 0: 已经是最新/无可用更新 1437 | 1: 更新成功 1438 | """ 1439 | print_header("词库更新流程") 1440 | # 使用缓存信息而不是重复API调用 1441 | remote_info = self.update_info 1442 | # 如果没有缓存的更新信息或者本地比远程新,不需要更新 1443 | if not remote_info or not self.has_update(): 1444 | print_warning("未找到可用更新") 1445 | return 0 1446 | 1447 | # 时间比对(精确到秒) 1448 | remote_time = datetime.strptime(remote_info["update_time"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) 1449 | local_time = self.get_local_time() 1450 | 1451 | if local_time and remote_time <= local_time: 1452 | print_success("当前已是最新词库") 1453 | return 0 1454 | 1455 | target_file = os.path.join(self.custom_dir, self.dict_file) 1456 | # 校验本地文件和远端文件sha256 1457 | if remote_info['sha256']: 1458 | if os.path.exists(target_file) and self.file_compare(remote_info['sha256'], target_file): 1459 | print_success("文件内容未变化,将更新本地保存的记录") 1460 | self.save_record(self.record_file, "dict_file", self.dict_file, remote_info) 1461 | return 0 1462 | 1463 | # 下载流程 1464 | _suffix = remote_info['sha256'] or remote_info['id'] 1465 | temp_file = os.path.join(self.custom_dir, f"temp_dict_{_suffix}.zip") 1466 | if os.path.exists(temp_file): 1467 | is_continue = True 1468 | else: 1469 | is_continue = False 1470 | for old_should_drop in fnmatch.filter(os.listdir(self.custom_dir), "temp_dict*.zip"): 1471 | os.remove(os.path.join(self.custom_dir, old_should_drop)) 1472 | if not self.download_file(remote_info["url"], temp_file, is_continue): 1473 | return -1 1474 | 1475 | # 方案变更时清除旧文件 1476 | self.clean_old_dict() 1477 | # 获取上次下载的压缩包的内容 1478 | old_files, _ = self.get_old_file_list(target_file, temp_file, is_dict=True) 1479 | if old_files: 1480 | self._delete_old_files(old_files, _) 1481 | print_warning("已移除上个版本的词库文件") 1482 | 1483 | try: 1484 | self.apply_update(temp_file, target_file, remote_info) # 传递三个参数 1485 | print_success("词库更新完成") 1486 | return 1 1487 | except Exception as e: 1488 | print_error(f"更新失败: {str(e)}") 1489 | # 回滚临时文件 1490 | if os.path.exists(temp_file): 1491 | os.remove(temp_file) 1492 | return -1 1493 | 1494 | def clean_old_dict(self) -> None: 1495 | """当变更所使用的方案时,删除旧文件""" 1496 | for file in os.listdir(self.custom_dir): 1497 | if 'dicts.zip' in file and file != self.dict_file: 1498 | old_dict_files, _ = self.get_old_file_list(file, None, is_dict=True) 1499 | self._delete_old_files(old_dict_files, _) 1500 | os.remove(os.path.join(self.custom_dir, file)) 1501 | print_warning("已移除旧词库zip文件") 1502 | 1503 | # ====================== 模型更新 ====================== 1504 | class ModelUpdater(UpdateHandler): 1505 | """模型更新处理器""" 1506 | def __init__(self, config_manager): 1507 | super().__init__(config_manager) 1508 | self.record_file = os.path.join(self.custom_dir, "model_record.json") 1509 | # 模型固定配置 1510 | self.model_file = "wanxiang-lts-zh-hans.gram" 1511 | self.target_path = os.path.join(self.extract_path, self.model_file) 1512 | 1513 | def check_update(self) -> Optional[Dict]: 1514 | """检查模型更新""" 1515 | url = f"https://api.github.com/repos/{OWNER}/{MODEL_REPO}/releases/tags/{MODEL_TAG}" 1516 | use_mirror = self.config_manager.config.getboolean('Settings', 'use_mirror', fallback=False) 1517 | if use_mirror: 1518 | url = f"https://cnb.cool/{OWNER}/{CNB_REPO}/-/releases" 1519 | release = self.remote_api_request( 1520 | url = url, 1521 | use_mirror = use_mirror 1522 | ) 1523 | if not release: 1524 | return None 1525 | 1526 | release = release[-1] if isinstance(release, list) else release 1527 | for asset in release.get("assets", []): 1528 | if asset["name"] == self.model_file: 1529 | return { 1530 | "url": asset.get("browser_download_url") or "https://cnb.cool" + asset.get("path"), 1531 | # 使用asset的更新时间 1532 | "update_time": asset.get("updated_at"), 1533 | "size": asset.get("size") or asset.get("sizeInByte"), 1534 | "sha256": asset.get("digest").split(':')[-1] if asset.get("digest") else "", 1535 | "id": asset.get("id") 1536 | } 1537 | return None 1538 | 1539 | def run(self) -> int: 1540 | """ 1541 | 执行模型更新主流程 1542 | return: 1543 | -1: 更新失败 1544 | 0: 已经是最新/无可用更新 1545 | 1: 更新成功 1546 | """ 1547 | print_header("模型更新流程") 1548 | # 使用缓存信息而不是重复API调用 1549 | remote_info = self.update_info 1550 | if not remote_info or not self.has_update(): 1551 | print_warning("未找到模型更新信息") 1552 | return 0 1553 | 1554 | # 时间比较(本地记录 vs 远程更新时间) 1555 | remote_time = datetime.strptime(remote_info["update_time"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) # 修改字段 1556 | local_time = self.get_local_time() 1557 | 1558 | if local_time and remote_time <= local_time: 1559 | print_success("当前已是最新模型") 1560 | return 0 1561 | 1562 | # 无论是否有记录,都检查哈希是否匹配 1563 | hash_matched = self._check_hash_match(remote_info) 1564 | 1565 | # 哈希匹配但记录缺失时的处理 1566 | if hash_matched: 1567 | print_success("模型内容未变化,将更新本地保存的记录") 1568 | self.save_record(self.record_file, "model_name", self.model_file, remote_info) 1569 | return 0 1570 | 1571 | # 下载到临时文件 1572 | _suffix = remote_info['sha256'] or remote_info['id'] 1573 | temp_file = os.path.join(self.custom_dir, f"{self.model_file}_{_suffix}.tmp") 1574 | if os.path.exists(temp_file): 1575 | is_continue = True 1576 | else: 1577 | is_continue = False 1578 | for old_should_drop in fnmatch.filter(os.listdir(self.custom_dir), f"{self.model_file}*.tmp"): 1579 | os.remove(os.path.join(self.custom_dir, old_should_drop)) 1580 | if not self.download_file(remote_info["url"], temp_file, is_continue): 1581 | print_error("模型下载失败") 1582 | return -1 1583 | 1584 | # 停止服务再覆盖 1585 | if hasattr(self, 'terminate_processes'): 1586 | self.terminate_processes() # 复用终止进程逻辑 1587 | 1588 | # 覆盖目标文件 1589 | try: 1590 | if os.path.exists(self.target_path): 1591 | os.remove(self.target_path) 1592 | os.replace(temp_file, self.target_path) # 原子操作更安全 1593 | self.save_record(self.record_file, "model_name", self.model_file, remote_info) 1594 | except Exception as e: 1595 | print_error(f"模型文件替换失败: {str(e)}") 1596 | return -1 1597 | 1598 | # 返回更新成功状态 1599 | print_success("模型更新完成") 1600 | return 1 1601 | 1602 | def get_local_time(self) -> Optional[datetime]: 1603 | if not os.path.exists(self.record_file): 1604 | return None 1605 | try: 1606 | with open(self.record_file, "r") as f: 1607 | data = json.load(f) 1608 | # 读取本地记录的update_time 1609 | return datetime.strptime(data["update_time"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) 1610 | except: 1611 | return None 1612 | 1613 | def _check_hash_match(self, remote_info) -> bool: 1614 | """检查临时文件与目标文件哈希是否一致""" 1615 | temp_hash = remote_info['sha256'] 1616 | if temp_hash: 1617 | target_hash = calculate_sha256(self.target_path) if os.path.exists(self.target_path) else None 1618 | return temp_hash == target_hash 1619 | return False 1620 | 1621 | 1622 | class ScriptUpdater(UpdateHandler): 1623 | def __init__(self, config_manager): 1624 | super().__init__(config_manager) 1625 | self.script_path = os.path.abspath(__file__) 1626 | 1627 | def check_update(self) -> Optional[Dict]: 1628 | releases = self.remote_api_request("https://api.github.com/repos/expoli/rime-wanxiang-update-tools/releases") 1629 | if not releases: 1630 | return None 1631 | 1632 | remote_version = releases[0].get("tag_name", "DEFAULT") 1633 | if not self.compare_version(UPDATE_TOOLS_VERSION, remote_version): 1634 | return None 1635 | update_info = releases[0].get("body", "无更新说明") 1636 | for asset in releases[0].get("assets", []): 1637 | if asset["name"] == 'rime-wanxiang-update-win-mac-ios-android.py': 1638 | return { 1639 | "url": asset["browser_download_url"], 1640 | "update_time": datetime.strptime(asset["updated_at"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc).strftime('%Y-%m-%d %H:%M:%S'), 1641 | "tag": remote_version, 1642 | "description": update_info 1643 | } 1644 | 1645 | def update_script(self, url: str) -> bool: 1646 | """更新脚本""" 1647 | res = self.remote_api_request(url=url, output_json=False) 1648 | if res.status_code == 200: 1649 | with open(self.script_path, 'wb') as f: 1650 | f.write(res.content) 1651 | print_success("脚本更新成功,请重新运行脚本(iOS用户请退出当前软件重新启动)") 1652 | return True 1653 | else: 1654 | print_error("脚本更新失败,请检查网络连接或手动下载最新脚本") 1655 | return False 1656 | 1657 | def compare_version(self, local_version: str, remote_version: str) -> bool: 1658 | if not local_version.startswith('v'): 1659 | return False 1660 | if local_version != remote_version: 1661 | return True 1662 | return False 1663 | 1664 | def run(self): 1665 | remote_info = self.check_update() 1666 | if not remote_info: 1667 | print_warning("未找到脚本更新信息") 1668 | return False 1669 | 1670 | remote_version = remote_info.get("tag", "DEFAULT") 1671 | user_choose = input(f"\n{COLOR['WARNING']}[!] 检测到新版本更新(当前版本:{UPDATE_TOOLS_VERSION},新版本:{remote_version}),是否更新?(y/n): {COLOR['ENDC']}") 1672 | if user_choose.lower() == 'y': 1673 | print_header("正在更新脚本,请勿进行其他操作...") 1674 | if self.update_script(remote_info["url"]): 1675 | sys.exit(0) 1676 | else: 1677 | return False 1678 | 1679 | # ====================== 工具函数 ====================== 1680 | def calculate_sha256(file_path) -> Optional[str]: 1681 | """ 1682 | 计算文件SHA256值 1683 | Args: 1684 | file_path (str): 文件路径 1685 | Returns: 1686 | str: SHA256值 1687 | """ 1688 | sha256_hash = hashlib.sha256() 1689 | try: 1690 | with open(file_path, "rb") as f: 1691 | for byte_block in iter(lambda: f.read(4096), b""): 1692 | sha256_hash.update(byte_block) 1693 | return sha256_hash.hexdigest() 1694 | except Exception as e: 1695 | print_error(f"计算哈希失败: {str(e)}") 1696 | return None 1697 | 1698 | def print_update_status(scheme_updater, dict_updater, model_updater, script_updater) -> None: 1699 | """打印更新状态信息""" 1700 | # 检查哪些组件有更新 1701 | has_script_update = script_updater.update_info 1702 | has_scheme_update = scheme_updater.update_info and scheme_updater.has_update() 1703 | has_dict_update = dict_updater.update_info and dict_updater.has_update() 1704 | has_model_update = model_updater.update_info and model_updater.has_update() 1705 | 1706 | # 脚本更新提示 1707 | if has_script_update: 1708 | print(f"\n{COLOR['WARNING']}==== 脚本更新可用 ===={COLOR['ENDC']}") 1709 | print(f"版本: {has_script_update['tag']}") 1710 | print(f"发布时间: {has_script_update['update_time']}") 1711 | 1712 | # 方案更新提示(仅当有更新时显示) 1713 | if has_scheme_update: 1714 | scheme_update_info = scheme_updater.update_info 1715 | remote_time = datetime.strptime(scheme_update_info["update_time"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) 1716 | scheme_local = remote_time.astimezone(timezone(timedelta(hours=8))).strftime("%Y-%m-%d %H:%M:%S") 1717 | 1718 | print(f"\n{COLOR['WARNING']}==== 方案更新可用 ===={COLOR['ENDC']}") 1719 | print(f"{COLOR['WARNING']}版本: {scheme_update_info.get('tag', '未知版本')}{COLOR['ENDC']}") 1720 | print(f"发布时间: {scheme_local}") 1721 | 1722 | raw_description = scheme_update_info.get('description', '无更新说明') 1723 | 1724 | try: 1725 | update_cache_dir = scheme_updater.custom_dir 1726 | os.makedirs(update_cache_dir, exist_ok=True) 1727 | 1728 | # 创建文件名(包含版本和时间) 1729 | version_tag = scheme_update_info.get('tag', 'unknown').replace('/', '_') 1730 | date_str = remote_time.strftime("%Y%m%d") 1731 | filename = os.path.join(update_cache_dir, f"update_{version_tag}_{date_str}.md") 1732 | 1733 | # 移除已有的md文件 1734 | for update_cache_file in os.listdir(update_cache_dir): 1735 | if re.match('^update.*md$', update_cache_file) and update_cache_file != f"update_{version_tag}_{date_str}.md": 1736 | os.remove(os.path.join(update_cache_dir, update_cache_file)) 1737 | 1738 | if not os.path.exists(filename): 1739 | # 写入 Markdown 文件 1740 | with open(filename, 'w', encoding='utf-8') as md_file: 1741 | md_file.write(f"# 方案更新说明 ({version_tag})\n\n") 1742 | md_file.write(f"**发布时间**: {scheme_local}\n\n") 1743 | md_file.write("## 更新内容\n\n") 1744 | md_file.write(raw_description) 1745 | 1746 | print_success(f"更新说明已保存到: {filename}") 1747 | except Exception as e: 1748 | print_error(f"保存更新说明失败: {str(e)}") 1749 | 1750 | # 词库更新提示(仅当有更新时显示) 1751 | if has_dict_update: 1752 | dict_update_info = dict_updater.update_info 1753 | remote_time = datetime.strptime(dict_update_info["update_time"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) 1754 | dict_local = remote_time.astimezone(timezone(timedelta(hours=8))).strftime("%Y-%m-%d %H:%M:%S") 1755 | print(f"\n{COLOR['WARNING']}==== 词库更新可用 ===={COLOR['ENDC']}") 1756 | print(f"版本: {dict_update_info.get('tag', '未知版本')}") 1757 | print(f"发布时间: {dict_local}") 1758 | 1759 | # 模型更新提示(仅当有更新时显示) 1760 | if has_model_update: 1761 | model_update_info = model_updater.update_info 1762 | remote_time = datetime.strptime(model_update_info["update_time"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) 1763 | model_local = remote_time.astimezone(timezone(timedelta(hours=8))).strftime("%Y-%m-%d %H:%M:%S") 1764 | print(f"\n{COLOR['WARNING']}==== 模型更新可用 ===={COLOR['ENDC']}") 1765 | print(f"发布时间: {model_local}") 1766 | 1767 | # 如果没有更新显示提示 1768 | if not (has_scheme_update or has_dict_update or has_model_update): 1769 | print(f"\n{COLOR['OKGREEN']}[√] 所有组件均为最新版本{COLOR['ENDC']}") 1770 | 1771 | def perform_auto_update( 1772 | config_manager: ConfigManager, 1773 | combined_updater: Optional[CombinedUpdater] = None, 1774 | is_config_triggered: bool = False 1775 | ) -> Optional[List[int]]: 1776 | """执行自动更新流程""" 1777 | if not is_config_triggered: 1778 | print_header("智能更新检测中...") 1779 | 1780 | # 创建或使用已有的组合更新器 1781 | if combined_updater is None: 1782 | # 只有在配置触发模式下才显示更新检查信息 1783 | if is_config_triggered: 1784 | print_subheader("正在检查可用更新...") 1785 | use_mirror = config_manager.config.getboolean('Settings', 'use_mirror', fallback=False) 1786 | if use_mirror: 1787 | request_target = "cnb.cool" 1788 | print(f"{COLOR['WARNING']}脚本更新依然使用api.github.com,请保持网络畅通...{COLOR['ENDC']}") 1789 | else: 1790 | request_target = "api.github.com" 1791 | print(f"{COLOR['BLUE']}请求 {request_target} 中...{COLOR['ENDC']}") 1792 | 1793 | combined_updater = CombinedUpdater(config_manager) 1794 | combined_updater.fetch_all_updates() 1795 | # 获取各个更新器的实例 1796 | script_updater = combined_updater.script_updater 1797 | scheme_updater = combined_updater.scheme_updater 1798 | dict_updater = combined_updater.dict_updater 1799 | model_updater = combined_updater.model_updater 1800 | # 在配置触发模式下显示更新状态 1801 | if is_config_triggered: 1802 | print_update_status(scheme_updater, dict_updater, model_updater, script_updater) 1803 | 1804 | # 脚本更新检查(仅当有实际更新时才提示) 1805 | if script_updater.update_info: 1806 | script_updater.run() 1807 | 1808 | # 初始化更新状态 1809 | scheme_updated = 0 1810 | dict_updated = 0 1811 | model_updated = 0 1812 | if scheme_updater.has_update(): 1813 | scheme_updated = scheme_updater.run() 1814 | if dict_updater.has_update(): 1815 | dict_updated = dict_updater.run() 1816 | if model_updater.has_update(): 1817 | model_updated = model_updater.run() 1818 | updated = [scheme_updated, dict_updated, model_updated] 1819 | # 部署逻辑 1820 | deployer = scheme_updater 1821 | if SYSTEM_TYPE == 'windows': 1822 | if -1 in updated and deployer: 1823 | print("\n" + COLOR['OKCYAN'] + "[i]" + COLOR['ENDC'] + " 部分内容更新失败,跳过部署步骤,请重新更新") 1824 | return updated # 直接返回updated,不进行后续操作 1825 | elif updated == [0,0,0] and deployer: 1826 | print("\n" + COLOR['OKGREEN'] + "[√] 无需更新,跳过部署步骤" + COLOR['ENDC']) 1827 | else: 1828 | print_header("重新部署输入法") 1829 | if deployer.deploy_weasel(): 1830 | print_success("部署成功") 1831 | else: 1832 | print_warning("部署失败,请检查日志") 1833 | elif SYSTEM_TYPE == 'macos': 1834 | if -1 in updated and deployer: 1835 | print("\n" + COLOR['OKCYAN'] + "[i]" + COLOR['ENDC'] + " 部分内容更新失败,跳过部署步骤,请重新更新") 1836 | return updated # 直接返回updated,不进行后续操作 1837 | elif updated == [0,0,0] and deployer: 1838 | print("\n" + COLOR['OKGREEN'] + "[√] 无需更新,跳过部署步骤" + COLOR['ENDC']) 1839 | else: 1840 | print_header("重新部署输入法") 1841 | deployer.deploy_for_mac() 1842 | elif SYSTEM_TYPE == 'ios': 1843 | import webbrowser 1844 | if -1 in updated and deployer: 1845 | print("\n" + COLOR['OKCYAN'] + "[i]" + COLOR['ENDC'] + " 部分内容更新失败,跳过部署步骤,请重新更新") 1846 | return updated # 直接返回updated,不进行后续操作 1847 | elif updated == [0,0,0] and deployer: 1848 | print("\n" + COLOR['OKGREEN'] + "[√] 无需更新,跳过部署步骤" + COLOR['ENDC']) 1849 | else: 1850 | print_header("尝试跳转到Hamster重新部署输入法") 1851 | if is_config_triggered: 1852 | # 配置触发的自动更新模式直接部署 1853 | webbrowser.open("hamster://dev.fuxiao.app.hamster/rime?deploy", new=1) 1854 | print_success("已自动触发部署") 1855 | else: 1856 | is_deploy = input("是否跳转到Hamster进行部署(y/n)? ").strip().lower() 1857 | if is_deploy == 'y': 1858 | print_warning("将于3秒后跳转到Hamster输入法进行自动部署") 1859 | time.sleep(3) 1860 | webbrowser.open("hamster://dev.fuxiao.app.hamster/rime?deploy", new=1) 1861 | else: 1862 | if -1 in updated and deployer: 1863 | print("\n" + COLOR['OKCYAN'] + "[i]" + COLOR['ENDC'] + " 部分内容更新失败,跳过部署步骤,请重新更新") 1864 | return updated # 直接返回updated,不进行后续操作 1865 | elif updated == [0,0,0] and deployer: 1866 | print("\n" + COLOR['OKGREEN'] + "[√] 无需更新,跳过部署步骤" + COLOR['ENDC']) 1867 | else: 1868 | print_warning("请手动部署输入法") 1869 | 1870 | print("\n" + COLOR['OKGREEN'] + "[√] 输入法配置全部更新完成" + COLOR['ENDC']) 1871 | 1872 | # 如果是配置触发的自动更新,直接退出 1873 | if is_config_triggered: 1874 | print("\n" + COLOR['OKGREEN'] + "✨ 自动更新完成!" + COLOR['ENDC']) 1875 | time.sleep(2) 1876 | sys.exit(0) 1877 | return updated 1878 | 1879 | def create_and_show_updates(config_manager, show=True) -> CombinedUpdater: 1880 | """创建并显示更新信息""" 1881 | if show: 1882 | print_subheader("正在检查可用更新...") 1883 | use_mirror = config_manager.config.getboolean('Settings', 'use_mirror', fallback=False) 1884 | if use_mirror: 1885 | print(f"{COLOR['WARNING']}脚本更新依然使用api.github.com,请保持网络畅通...{COLOR['ENDC']}") 1886 | request_target = "cnb.cool" 1887 | else: 1888 | request_target = "api.github.com" 1889 | print(f"{COLOR['BLUE']}请求 {request_target} 中...{COLOR['ENDC']}") 1890 | 1891 | # 创建组合更新器并获取所有更新信息 1892 | combined_updater = CombinedUpdater(config_manager) 1893 | combined_updater.fetch_all_updates() 1894 | 1895 | # 获取各个更新器的实例 1896 | script_updater = combined_updater.script_updater 1897 | scheme_updater = combined_updater.scheme_updater 1898 | dict_updater = combined_updater.dict_updater 1899 | model_updater = combined_updater.model_updater 1900 | 1901 | # 使用函数打印更新状态 1902 | if show: 1903 | print_update_status(scheme_updater, dict_updater, model_updater, script_updater) 1904 | return combined_updater 1905 | 1906 | def open_config_file(config_path) -> None: 1907 | """用默认编辑器打开配置文件""" 1908 | if os.name == 'nt': # Windows 1909 | subprocess.run(['notepad.exe', config_path], shell=True) 1910 | else: # macOS/Linux 1911 | try: 1912 | # 尝试使用默认编辑器打开 1913 | if SYSTEM_TYPE == 'macos': 1914 | subprocess.run(['open', config_path]) 1915 | else: 1916 | subprocess.run(['xdg-open', config_path]) 1917 | except: 1918 | print_warning("无法打开配置文件,请手动编辑。") 1919 | 1920 | # ====================== 主程序 ====================== 1921 | def main(): 1922 | print(f"\n{COLOR['OKCYAN']}[i] 当前系统为:{SYSTEM_TYPE} {COLOR['ENDC']}") 1923 | if UPDATE_TOOLS_VERSION.startswith("DEFAULT"): 1924 | print(f"{COLOR['WARNING']}[!] 您下载的是非发行版脚本,请勿直接使用,请去 releases 页面下载最新版本:https://github.com/expoli/rime-wanxiang-update-tools/releases{COLOR['ENDC']}") 1925 | else: 1926 | print(f"{COLOR['OKCYAN']}[i] 当前更新工具版本:{UPDATE_TOOLS_VERSION}{COLOR['ENDC']}") 1927 | 1928 | try: 1929 | config_manager = ConfigManager() 1930 | combined_updater = None # 初始化组合更新器 1931 | 1932 | # 检查是否启用了自动更新 1933 | auto_update = config_manager.config.getboolean('Settings', 'auto_update', fallback=False) 1934 | if auto_update: 1935 | print_header("自动更新模式已启用") 1936 | combined_updater = create_and_show_updates(config_manager, show=False) 1937 | # 执行自动更新并退出 1938 | perform_auto_update( 1939 | config_manager, 1940 | combined_updater=combined_updater, 1941 | is_config_triggered=True 1942 | ) 1943 | # 非自动更新模式下显示更新状态 1944 | if not auto_update: 1945 | # 创建并显示更新信息 1946 | combined_updater = create_and_show_updates(config_manager) 1947 | # 主菜单循环 1948 | while True: 1949 | # 选择更新类型 1950 | print_header("更新类型选择") 1951 | print("[1] 词库更新\n[2] 方案更新\n[3] 模型更新\n[4] 自动更新\n[5] 脚本更新\n[6] 修改配置\n[7] 退出程序") 1952 | choice = input("请输入选择(1-7,单独按回车键默认选择自动更新): ").strip() or '4' 1953 | 1954 | if choice == '6': 1955 | # 修改配置 1956 | config_manager.display_config_instructions() 1957 | print("保存后关闭配置文件以继续...") 1958 | open_config_file(config_manager.config_path) 1959 | # 返回主菜单或退出 1960 | user_choice = input("\n按回车键返回主菜单,或输入其他键退出: ").strip().lower() 1961 | if user_choice == '': 1962 | # 重新加载配置 1963 | config_manager = ConfigManager() 1964 | # 重置更新器 1965 | combined_updater = None 1966 | # 重新创建并显示更新信息 1967 | combined_updater = create_and_show_updates(config_manager) 1968 | else: 1969 | break 1970 | elif choice == '7': 1971 | break 1972 | elif choice == '5': 1973 | # 脚本更新 1974 | script_updater = ScriptUpdater(config_manager) 1975 | script_updater.run() 1976 | continue 1977 | elif choice == '4': # 自动更新选项 1978 | # 确保有更新器实例 1979 | if not combined_updater: 1980 | combined_updater = create_and_show_updates(config_manager) 1981 | # 执行自动更新 1982 | updated = perform_auto_update( 1983 | config_manager, 1984 | combined_updater=combined_updater, 1985 | is_config_triggered=False 1986 | ) 1987 | # 处理更新结果 1988 | if -1 in updated: 1989 | print_warning("部分内容下载更新失败,请重试") 1990 | continue 1991 | else: 1992 | print_success(COLOR['OKGREEN'] + "自动更新完成" + COLOR['ENDC']) 1993 | print("\n" + COLOR['OKGREEN'] + "4秒后自动退出..." + COLOR['ENDC']) 1994 | time.sleep(4) 1995 | sys.exit(0) 1996 | else: 1997 | # 执行其他更新操作,确保有更新器实例 1998 | if not combined_updater: 1999 | combined_updater = create_and_show_updates(config_manager) 2000 | # 获取各个更新器的实例 2001 | scheme_updater = combined_updater.scheme_updater 2002 | dict_updater = combined_updater.dict_updater 2003 | model_updater = combined_updater.model_updater 2004 | # 初始化更新状态 2005 | deployer = None 2006 | updated = -200 2007 | if choice == '1': 2008 | updated = dict_updater.run() 2009 | deployer = dict_updater 2010 | elif choice == '2': 2011 | updated = scheme_updater.run() 2012 | deployer = scheme_updater 2013 | elif choice == '3': 2014 | updated = model_updater.run() 2015 | deployer = model_updater 2016 | # 部署逻辑 2017 | if SYSTEM_TYPE == 'windows' and deployer and updated == 1: 2018 | print_header("重新部署输入法") 2019 | if deployer.deploy_weasel(): 2020 | print_success("部署成功") 2021 | else: 2022 | print_warning("部署失败,请检查日志") 2023 | elif SYSTEM_TYPE == 'macos' and deployer and updated == 1: 2024 | print_header("重新部署输入法") 2025 | deployer.deploy_for_mac() 2026 | elif SYSTEM_TYPE == 'ios' and deployer and updated == 1: 2027 | import webbrowser 2028 | print_header("尝试跳转到Hamster重新部署输入法") 2029 | is_deploy = input("是否跳转到Hamster进行部署(y/n)? ").strip().lower() 2030 | if is_deploy == 'y': 2031 | print_warning("将于3秒后跳转到Hamster输入法进行自动部署") 2032 | time.sleep(3) 2033 | webbrowser.open("hamster://dev.fuxiao.app.hamster/rime?deploy", new=1) 2034 | else: 2035 | if deployer and updated == 1: 2036 | print_warning("请手动部署输入法") 2037 | 2038 | # 返回主菜单或退出 2039 | user_input = input("\n按回车键返回主菜单,或输入其他键退出: ") 2040 | if user_input.strip().lower() == '': 2041 | continue # 继续主循环 2042 | else: 2043 | break 2044 | 2045 | print("\n✨ 升级完毕,欢迎下次使用!") 2046 | time.sleep(2) 2047 | sys.exit(0) 2048 | except KeyboardInterrupt: 2049 | print(f"\n{COLOR['FAIL']}🚫 终止操作 {COLOR['ENDC']}") 2050 | except SystemExit: 2051 | print(f"\n{COLOR['OKBLUE']}⏏️ 程序退出 {COLOR['ENDC']}") 2052 | except Exception as e: 2053 | print(f"\n{COLOR['FAIL']}💥 程序异常:{str(e)}{COLOR['ENDC']}") 2054 | sys.exit(1) 2055 | 2056 | if __name__ == "__main__": 2057 | main() 2058 | --------------------------------------------------------------------------------