From 2b8bf937d96e5ff80e19f97de5be594802d90b5d Mon Sep 17 00:00:00 2001 From: Evan Date: Sun, 6 Sep 2026 11:44:30 +0900 Subject: [PATCH 1/4] fix(server): adopt isolated engine sessions and helpcode annotations (#178) --- AGENTS.md | 4 ++ product-lock.json | 2 +- server/src/ipc/event_listener.cpp | 2 +- server/src/session/engine_input_session.cpp | 28 ++++++++-- server/src/session/engine_input_session.h | 5 ++ server/src/session/input_session.h | 1 + .../src/test_engine_shuangpin_session.cpp | 53 +++++++++++++++++++ vendor/MetasequoiaImeEngine | 2 +- 8 files changed, 91 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 11f15f56..93ece6c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,10 @@ Windows 端的全部一方源码在本仓。**合仓改变的是仓库数量, - **候选与输入状态的权威在 Server 和引擎**,页面只负责展示和发出用户动作,不要在网页侧复制状态机。 - `ui-html/webview2/shared/` 是 Engine web 契约的副本,由 `ui-html/scripts/sync-contracts.py` 生成,CI 用 `--check` 验证它和 submodule 一致。手改这个目录会被 CI 拦下来,改契约要改 Engine。 +Server 当前仍使用 Engine 的兼容 `InputSession`,尚未迁移公共 `Session` facade。 +辅助码筛选与候选提示都按会话配置,禁止用全局默认码表驱动活动会话。 +`RuntimePaths::legacy()` 在适配器创建时捕获现有安装布局;完整资源包和用户数据代际切换仍待接入。 + ## 构建 每个组件是独立的 CMake 工程,各自带 `vcpkg.json` 和 preset,从仓库根目录指定 `-S`: diff --git a/product-lock.json b/product-lock.json index f31a61e2..54e4808f 100644 --- a/product-lock.json +++ b/product-lock.json @@ -3,7 +3,7 @@ "repositories": { "engine": { "repository": "metasequoiaime/MSIME-Engine", - "commit": "cc21e42916cf93ce43051fec474e87eeeba305bf" + "commit": "c63ba774ac03c9ff8ba82776d50e97d27fd459a3" } }, "dictionary": { diff --git a/server/src/ipc/event_listener.cpp b/server/src/ipc/event_listener.cpp index 6bf8ad16..cc80d03b 100644 --- a/server/src/ipc/event_listener.cpp +++ b/server/src/ipc/event_listener.cpp @@ -925,7 +925,7 @@ std::string BuildCurrentCandidatePage() if (show_helpcodes && item.source != CandidateSource::EnglishDictionary && item.source != CandidateSource::QuickPhrase && item.source != CandidateSource::Emoji && item.source != CandidateSource::Kaomoji && item.source != CandidateSource::Generated) - view.annotation = HelpcodeUtils::compute_helpcodes(item.word, uppercase_all_helpcodes); + view.annotation = g_inputSession->get_helpcode_annotation(item.word, uppercase_all_helpcodes); if (item.source == CandidateSource::CloudSuggestion) view.badge = " ☁️"; else if (item.source == CandidateSource::AiSuggestion) diff --git a/server/src/session/engine_input_session.cpp b/server/src/session/engine_input_session.cpp index 6df07826..4d95765e 100644 --- a/server/src/session/engine_input_session.cpp +++ b/server/src/session/engine_input_session.cpp @@ -2,7 +2,8 @@ #include "config/ime_config.h" #include "MetasequoiaImeEngine/common/helpcode_utils.h" -EngineInputSession::EngineInputSession(SchemeType scheme, const ShuangpinProfile &profile) : session_(scheme, profile) +EngineInputSession::EngineInputSession(SchemeType scheme, const ShuangpinProfile &profile) + : paths_(metasequoia::RuntimePaths::legacy()), session_(scheme, profile, paths_) { ApplyConfiguration(); } @@ -11,8 +12,21 @@ void EngineInputSession::ApplyConfiguration() { const auto scheme = session_.scheme(); if (scheme == SchemeType::Quanpin || scheme == SchemeType::Shuangpin) - HelpcodeUtils::select_helpcode_schema(scheme == SchemeType::Quanpin ? GetConfiguredQuanpinHelpcodeSchema() - : GetConfiguredShuangpinHelpcodeSchema()); + { + const auto &schema = scheme == SchemeType::Quanpin ? GetConfiguredQuanpinHelpcodeSchema() + : GetConfiguredShuangpinHelpcodeSchema(); + if (schema != helpcode_schema_) + { + // Keep filtering and annotations on this session's captured resource layout. + // Applying unchanged settings on each key must not reload the tables. + auto keymap = HelpcodeUtils::load_helpcode_keymap(paths_.resources, schema); + if (session_.set_helpcode_schema(schema)) + { + helpcode_schema_ = schema; + helpcode_keymap_ = std::move(keymap); + } + } + } session_.set_shuangpin_helpcode_enabled(GetConfiguredShuangpinHelpcodeEnabled()); session_.set_quanpin_helpcode_enabled(GetConfiguredQuanpinHelpcodeEnabled()); session_.set_quanpin_autocorrect_enabled(GetConfiguredQuanpinAutocorrectEnabled()); @@ -160,3 +174,11 @@ IInputSession::CreatingWordProgress EngineInputSession::update_creating_word_pro { return session_.update_creating_word_progress(current_pinyin, current_word, selected_word, selection_transition); } + +std::string EngineInputSession::get_helpcode_annotation(const std::string &word, bool uppercase_all) const +{ + const auto scheme = session_.scheme(); + if (!helpcode_keymap_ || (scheme != SchemeType::Quanpin && scheme != SchemeType::Shuangpin)) + return {}; + return HelpcodeUtils::compute_helpcodes(word, uppercase_all, helpcode_keymap_.get()); +} diff --git a/server/src/session/engine_input_session.h b/server/src/session/engine_input_session.h index 0b7ce28b..1f936e94 100644 --- a/server/src/session/engine_input_session.h +++ b/server/src/session/engine_input_session.h @@ -1,6 +1,7 @@ #pragma once #include "input_session.h" +#include "MetasequoiaImeEngine/common/helpcode_utils.h" #include "MetasequoiaImeEngine/core/input_session.h" class EngineInputSession : public IInputSession @@ -19,6 +20,7 @@ class EngineInputSession : public IInputSession const std::vector &get_candidates() const override; bool expand_initial_candidates() override; + std::string get_helpcode_annotation(const std::string &word, bool uppercase_all) const override; std::optional find_candidate(const std::string &key, const std::string &value) override; const std::string &get_pinyin_sequence() const override; @@ -49,5 +51,8 @@ class EngineInputSession : public IInputSession private: void ApplyConfiguration(); + const metasequoia::RuntimePaths paths_; metasequoia::InputSession session_; + std::string helpcode_schema_; + HelpcodeUtils::SharedKeymap helpcode_keymap_; }; diff --git a/server/src/session/input_session.h b/server/src/session/input_session.h index 7c91b466..d1ec0675 100644 --- a/server/src/session/input_session.h +++ b/server/src/session/input_session.h @@ -28,6 +28,7 @@ class IInputSession virtual const std::vector &get_candidates() const = 0; virtual bool expand_initial_candidates() = 0; + virtual std::string get_helpcode_annotation(const std::string &word, bool uppercase_all) const = 0; virtual std::optional find_candidate(const std::string &, const std::string &) { return std::nullopt; diff --git a/server/tests/src/test_engine_shuangpin_session.cpp b/server/tests/src/test_engine_shuangpin_session.cpp index fff0c4c5..72a8fb4a 100644 --- a/server/tests/src/test_engine_shuangpin_session.cpp +++ b/server/tests/src/test_engine_shuangpin_session.cpp @@ -1,5 +1,6 @@ #include "tests/includes/test_framework.h" #include "src/session/engine_input_session.h" +#include "src/config/ime_config.h" #include "MetasequoiaImeEngine/common/helpcode_utils.h" #include "MetasequoiaImeEngine/quanpin/quanpin_query.h" #include "MetasequoiaImeEngine/shuangpin/shuangpin_dictionary.h" @@ -784,3 +785,55 @@ TEST_CASE(EngineShuangpinSegmentedInitialCandidatesExpandInPlace) REQUIRE(count_initials(expanded_candidates) > 24); REQUIRE_EQ(count_other_candidates(expanded_candidates), other_count); } + +TEST_CASE(EngineSessionsKeepHelpcodeFilteringAndAnnotationsTogether) +{ + // The integration runner supplies an isolated config and the locked release dictionaries. + InitImeConfig(); + struct RestoreConfiguration + { + std::string quanpin = GetConfiguredQuanpinHelpcodeSchema(); + std::string shuangpin = GetConfiguredShuangpinHelpcodeSchema(); + bool quanpin_enabled = GetConfiguredQuanpinHelpcodeEnabled(); + bool shuangpin_enabled = GetConfiguredShuangpinHelpcodeEnabled(); + ~RestoreConfiguration() + { + SetConfiguredQuanpinHelpcodeSchema(quanpin); + SetConfiguredShuangpinHelpcodeSchema(shuangpin); + SetConfiguredQuanpinHelpcodeEnabled(quanpin_enabled); + SetConfiguredShuangpinHelpcodeEnabled(shuangpin_enabled); + } + } restore; + REQUIRE(SetConfiguredQuanpinHelpcodeEnabled(true)); + REQUIRE(SetConfiguredShuangpinHelpcodeEnabled(true)); + REQUIRE(SetConfiguredQuanpinHelpcodeSchema("lantian")); + REQUIRE(SetConfiguredShuangpinHelpcodeSchema("ziranma")); + + EngineInputSession quanpin(SchemeType::Quanpin); + EngineInputSession shuangpin(SchemeType::Shuangpin); + REQUIRE_EQ(quanpin.get_helpcode_annotation("你", true), std::string("(RX)")); + REQUIRE_EQ(shuangpin.get_helpcode_annotation("你", false), std::string("(rE)")); + + InputLetters(quanpin, "niRX"); + REQUIRE(!quanpin.get_candidates().empty()); + REQUIRE(std::any_of(quanpin.get_candidates().begin(), quanpin.get_candidates().end(), + [](const auto &item) { return item.word == "你"; })); + InputLetters(shuangpin, "ni"); + shuangpin.recompute_candidates(); + quanpin.recompute_candidates(); + REQUIRE_EQ(quanpin.get_helpcode_annotation("你", true), std::string("(RX)")); + REQUIRE_EQ(shuangpin.get_helpcode_annotation("你", false), std::string("(rE)")); + REQUIRE(std::any_of(quanpin.get_candidates().begin(), quanpin.get_candidates().end(), + [](const auto &item) { return item.word == "你"; })); + + // A settings change is adopted by the next refresh, including candidate annotations. + REQUIRE(SetConfiguredQuanpinHelpcodeSchema("ziranma")); + quanpin.reset_state(); + InputLetters(quanpin, "niRE"); + REQUIRE_EQ(quanpin.get_helpcode_annotation("你", true), std::string("(RE)")); + REQUIRE(std::any_of(quanpin.get_candidates().begin(), quanpin.get_candidates().end(), + [](const auto &item) { return item.word == "你"; })); + REQUIRE_EQ(shuangpin.get_helpcode_annotation("你", false), std::string("(rE)")); + quanpin.switch_scheme(SchemeType::Shuangpin); + REQUIRE_EQ(quanpin.get_helpcode_annotation("你", false), std::string("(rE)")); +} diff --git a/vendor/MetasequoiaImeEngine b/vendor/MetasequoiaImeEngine index cc21e429..c63ba774 160000 --- a/vendor/MetasequoiaImeEngine +++ b/vendor/MetasequoiaImeEngine @@ -1 +1 @@ -Subproject commit cc21e42916cf93ce43051fec474e87eeeba305bf +Subproject commit c63ba774ac03c9ff8ba82776d50e97d27fd459a3 From 46e48d219ef754e94fe7ff44b2febcfb5b12b8ef Mon Sep 17 00:00:00 2001 From: Evan Date: Sun, 6 Sep 2026 11:58:08 +0900 Subject: [PATCH 2/4] chore(main): release 0.2.1 (#179) --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ version.txt | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2be9c43c..af55ef03 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.0" + ".": "0.2.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1bc10c..fabb5a08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [0.2.1](https://github.com/metasequoiaime/MSIME-Windows/compare/v0.2.0...v0.2.1) (2026-09-06) + + +### Bug Fixes + +* **server:** adopt isolated engine sessions and helpcode annotations ([#178](https://github.com/metasequoiaime/MSIME-Windows/issues/178)) ([2b8bf93](https://github.com/metasequoiaime/MSIME-Windows/commit/2b8bf937d96e5ff80e19f97de5be594802d90b5d)) + ## [0.2.0](https://github.com/metasequoiaime/MSIME-Windows/compare/v0.1.4...v0.2.0) (2026-09-06) diff --git a/version.txt b/version.txt index 0ea3a944..0c62199f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.2.0 +0.2.1 From d0e7cfdbdfaae9e5c296c83e58707932128c9b71 Mon Sep 17 00:00:00 2001 From: Evan Hu Date: Sun, 6 Sep 2026 12:16:28 +0900 Subject: [PATCH 3/4] feat(release): separate the automatic build channel from the release channel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automatic per-merge builds shared one list with deliberately cut releases, and the previous change only made them identifiable rather than separate: both were still prereleases, which is the thing the issue objects to. Labelling was the wrong answer to it. GitHub has no channel concept, but it has two published states, and there are exactly two kinds of release here. A push-triggered build is now a prerelease; a workflow_dispatch build is an ordinary release and the newest one carries the Latest badge. The Releases page shows two lists instead of one list with two kinds of title in it, and /releases/latest resolves for the first time. The prerelease flag therefore means "automatic and uncurated" rather than "beta". The product being 内测 is stated in the release notes and in docs/installation.md, which is where a claim about stability belongs; the flag cannot carry that meaning while it is also the only thing separating the channels. docs/installation.md now tells readers to take the Latest badge, describes both channels, and says explicitly that being in the release channel means somebody picked it, not that it is stable. Already published releases are not touched, so the Latest badge stays empty until the first dispatch after this lands. Refs MSIME-Windows#167 --- AGENTS.md | 11 ++++++++++- docs/installation.md | 20 +++++++++----------- scripts/ci/publish-release.sh | 12 ++++++++---- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 93ece6c9..98303b7e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -71,7 +71,16 @@ cmake -S ui -B ui/build -A x64 # GUI 框架 **每次可发布的合并都花掉一次签名额度**,这正是之前的手动设计要避免的事。取舍的理由记在 [docs/product-release.md](docs/product-release.md)。 -`workflow_dispatch` 保留下来,现在是修复通道而不是常规路径:run 在建出 draft 之后失败时,拿那个 tag 重跑即可。同一个 tag 重复触发会被 `validate-draft-release.sh` 挡下——发布之后它就不是 draft 了,不会二次消耗签名次数。 +`workflow_dispatch` 保留下来,它同时是修复通道和发布频道的入口:run 在建出 draft 之后失败时,拿那个 tag 重跑即可。同一个 tag 重复触发会被 `validate-draft-release.sh` 挡下——发布之后它就不是 draft 了,不会二次消耗签名次数。 + +**两个频道由触发方式决定,`publish-release.sh` 据此选状态**([#167](https://github.com/metasequoiaime/MSIME-Windows/issues/167)): + +| 频道 | 触发 | 发布状态 | 标题 | +|---|---|---|---| +| 自动构建 | push | `--prerelease` | `v<版本>(自动构建)`,说明里带提示 | +| 发布 | `workflow_dispatch` | `--prerelease=false --latest` | `v<版本>` | + +GitHub 没有自定义频道,只有 Latest / Pre-release / Draft 三种状态,所以这里用 `prerelease` 标志承载「自动、未经挑选」,而不是承载「内测」。产品仍在内测这件事写在 release 说明和 [docs/installation.md](docs/installation.md) 里——那才是该做稳定性声明的地方,而这个标志同时还得用来分隔频道,兼不了两份职责。 `windows/src/IME/MetasequoiaIME.rc` 的 `FILEVERSION` / `PRODUCTVERSION` 不由 release-please 直接改(它是逗号和点号两种写法),而是由 `scripts/apply_version.py` 从 `version.txt` 注入,release 构建在 configure 之前执行: diff --git a/docs/installation.md b/docs/installation.md index 6f0a9d65..3e3063bd 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -25,20 +25,18 @@ ## 下载 1. 打开 [Releases 页面](https://github.com/metasequoiaime/MSIME-Windows/releases)。 -2. 下载最新的安装包 `MetasequoiaIME_Setup_v<版本号>.exe`(例如 `MetasequoiaIME_Setup_v0.0.9.2.exe`)。 +2. 下载带 **Latest** 徽章那个版本的 `MetasequoiaIME_Setup_v<版本号>.exe`。 -> 注意:当前发布版本均标记为 **Pre-release(内测版)**,功能与稳定性可能变动,重要环境建议先在虚拟机或非主力机试用。 +Releases 页面上有两类发布: + +| 频道 | 页面上的样子 | 来源 | 谁该用 | +|---|---|---|---| +| 发布 | 正常发布,最新的一个带 **Latest** 徽章 | 人工挑选后发布 | **一般都用这个** | +| 自动构建 | 标记 **Pre-release**,标题带「(自动构建)」 | 每次合并到 `main` 后由 CI 自动发布,未经人工挑选 | 只在需要某个尚未进入发布频道的修复、且能接受未经验证的改动时用 | + +> 注意:**两个频道当前都还是内测阶段**,版本号仍在 `0.x`,功能与稳定性可能变动,重要环境建议先在虚拟机或非主力机试用。「发布」频道的含义是「有人挑过」,不是「已经稳定」。 > 参与内测与反馈:[Telegram 群](https://t.me/msimegroup)、QQ 群 829919142。 -Releases 页面上有两类发布,都标记为 Pre-release,靠标题区分: - -| 标题形式 | 来源 | 建议 | -|---|---|---| -| `v<版本号>(自动构建)` | 每次合并到 `main` 后由 CI 自动构建发布,未经人工挑选 | 想要最新修复、且能接受未经验证的改动时用 | -| `v<版本号>` | 人工挑选后发布 | 一般情况下用这个 | - -GitHub 的发布只有 Latest / Pre-release / Draft 三种状态,没有自定义频道,因此两类共用同一个 Pre-release 列表。 - ## 安装与注意事项 双击安装包,按安装向导提示完成安装。 diff --git a/scripts/ci/publish-release.sh b/scripts/ci/publish-release.sh index af7b22e7..eb37cda9 100755 --- a/scripts/ci/publish-release.sh +++ b/scripts/ci/publish-release.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash # Attach the installer to the draft release, append the artifact details to its notes and publish. # -# GitHub offers three release states and no notion of a channel, so an automatic per-merge build and a release somebody deliberately cut both land in the same Pre-release list that docs/installation.md presents to users as the 内测版 channel. Every releasable merge now produces one of these, so the two are told apart in the title and the notes rather than by where they appear (MSIME-Windows#167). +# GitHub has no notion of a release channel, so the two kinds of release this pipeline produces are carried by the two states it does have (MSIME-Windows#167). An automatic per-merge build is a prerelease; a build somebody deliberately dispatched is an ordinary release, and the newest one carries the Latest badge. Users therefore see two lists rather than one list with two kinds of title in it, and /releases/latest finally resolves. +# +# The prerelease flag means "automatic and uncurated" here, not "beta". The product being 内测 is stated in the notes and in docs/installation.md, which is where a claim about stability belongs; it is not something the flag can express while the flag is also the only thing separating the channels. # # Requires GH_TOKEN, GH_REPO, TAG_NAME, ASSET_PATH, ASSET_NAME, ASSET_SHA256, SIGNING_ENABLED, # DICTIONARY_TAG, PRODUCT_MANIFEST, RELEASE_TRIGGER. @@ -12,10 +14,12 @@ gh release upload "$TAG_NAME" "$ASSET_PATH" "$PRODUCT_MANIFEST" --repo "$GH_REPO if [[ "$RELEASE_TRIGGER" == push ]]; then # Braces are required: bash takes the full-width bracket that follows as part of the name otherwise. title="${TAG_NAME}(自动构建)" - banner='本版本由 CI 在合并到 `main` 后自动构建发布,未经人工挑选。想要经过挑选的内测版本,请选择标题不带「自动构建」的发布。' + banner='本版本由 CI 在合并到 `main` 后自动构建发布,未经人工挑选,标记为 Pre-release。想要经过挑选的版本,请下载页面上带 Latest 徽章的那个。' + channel=(--prerelease) else title="$TAG_NAME" banner='' + channel=(--prerelease=false --latest) fi gh release view "$TAG_NAME" --repo "$GH_REPO" --json body --jq .body > notes.md @@ -34,5 +38,5 @@ gh release view "$TAG_NAME" --repo "$GH_REPO" --json body --jq .body > notes.md fi } >> notes.md -gh release edit "$TAG_NAME" --repo "$GH_REPO" --draft=false --prerelease --title "$title" --notes-file notes.md -echo "Published $TAG_NAME as '$title' with $ASSET_NAME" +gh release edit "$TAG_NAME" --repo "$GH_REPO" --draft=false "${channel[@]}" --title "$title" --notes-file notes.md +echo "Published $TAG_NAME as '$title' (${channel[*]}) with $ASSET_NAME" From 0421eb612fc7437773ebd0485ca458bd3e85a364 Mon Sep 17 00:00:00 2001 From: Evan Date: Sun, 6 Sep 2026 12:28:34 +0900 Subject: [PATCH 4/4] chore(main): release 0.3.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 8 ++++++++ version.txt | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index af55ef03..0ee8c012 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.2.1" + ".": "0.3.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index fabb5a08..de3dcbed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.3.0](https://github.com/metasequoiaime/MSIME-Windows/compare/v0.2.1...v0.3.0) (2026-09-06) + + +### Features + +* **release:** separate the automatic build channel from the release channel ([2d51c53](https://github.com/metasequoiaime/MSIME-Windows/commit/2d51c5383925ce8174a60f5527cbda5ff03b7d9f)) +* **release:** separate the automatic build channel from the release channel ([d0e7cfd](https://github.com/metasequoiaime/MSIME-Windows/commit/d0e7cfdbdfaae9e5c296c83e58707932128c9b71)) + ## [0.2.1](https://github.com/metasequoiaime/MSIME-Windows/compare/v0.2.0...v0.2.1) (2026-09-06) diff --git a/version.txt b/version.txt index 0c62199f..0d91a54c 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.2.1 +0.3.0