From 3a7c11e44d845a28d35ac00910aad5c23c20c26a Mon Sep 17 00:00:00 2001 From: JWeBot Contributors Date: Mon, 19 Oct 2026 17:26:44 +0800 Subject: [PATCH] Release beta 1.2.1.1019 --- CHANGELOG.md | 8 ++ README.md | 6 +- VERSION | 2 +- packages/jwebot-config/pyproject.toml | 2 +- .../src/jwebot_config/__init__.py | 2 +- packages/jwebot-extension-host/pyproject.toml | 6 +- .../src/jwebot_extension_host/__init__.py | 2 +- packages/jwebot-kernel/pyproject.toml | 8 +- .../src/jwebot_kernel/__init__.py | 2 +- .../src/jwebot_kernel/edge_runtime.py | 112 +++++++++++------- .../jwebot-official-extensions/pyproject.toml | 4 +- .../jwebot_official_extensions/__init__.py | 2 +- packages/jwebot-sdk/pyproject.toml | 2 +- .../jwebot-sdk/src/jwebot_sdk/__init__.py | 2 +- pyproject.toml | 2 +- tests/test_jwebot_edge.py | 62 +++++++++- tests/test_release_update.py | 15 ++- 17 files changed, 162 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c4ede5..468144f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## beta 1.2.1.1019 - 2026-10-19 17:23:58 + +- 修复 Taffy 短句拆分只换行不分条的问题:多句闲聊现在会按多条消息逐条发送。 +- Encode multi-sentence chat replies with the existing multi-reply marker so + direct and cloud chat results enter the multi-message delivery path. +- Split queued delivery helper calls per message part, giving each part a + stable per-part event id while preserving structured local command output. + ## beta 1.2.0.1019 - 2026-10-19 17:05:47 - 优化 Taffy 群聊语气:回复更短、更像群友,减少 AI 式自我解释,并保留主动插话、冷门问答和更新状态提示。 diff --git a/README.md b/README.md index 11f2968..942a4e6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC 观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件, 并通过带视觉确认的 GUI 发送链路回复。 -当前版本:`beta 1.2.0.1019`,更新时间:2026-10-19 17:05:47。 +当前版本:`beta 1.2.1.1019`,更新时间:2026-10-19 17:23:58。 1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、 Steam 状态与时长查询、虚拟摩拉游戏、多人彩票、21 点、CS Major @@ -269,10 +269,10 @@ visually confirmed GUI bridge. Current version: ```text -beta 1.2.0.1019 +beta 1.2.1.1019 ``` -Updated: 2026-10-19 17:05:47. +Updated: 2026-10-19 17:23:58. The 1.0 Beta baseline consolidates the stable local command set, person and context indexes, Steam presence/playtime queries, virtual coin games, diff --git a/VERSION b/VERSION index 09261ee..b28694a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -beta 1.2.0.1019 +beta 1.2.1.1019 diff --git a/packages/jwebot-config/pyproject.toml b/packages/jwebot-config/pyproject.toml index 3f7b907..6079270 100644 --- a/packages/jwebot-config/pyproject.toml +++ b/packages/jwebot-config/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "jwebot-config" -version = "1.2.0b1+1019" +version = "1.2.1b1+1019" description = "Profile-based configuration compiler for JWeBot" requires-python = ">=3.9" dependencies = [ diff --git a/packages/jwebot-config/src/jwebot_config/__init__.py b/packages/jwebot-config/src/jwebot_config/__init__.py index a14b71f..8e0306d 100644 --- a/packages/jwebot-config/src/jwebot_config/__init__.py +++ b/packages/jwebot-config/src/jwebot_config/__init__.py @@ -36,4 +36,4 @@ __all__ = [ "write_default_profile", ] -__version__ = "1.2.0b1+1019" +__version__ = "1.2.1b1+1019" diff --git a/packages/jwebot-extension-host/pyproject.toml b/packages/jwebot-extension-host/pyproject.toml index ff30e6b..168fc27 100644 --- a/packages/jwebot-extension-host/pyproject.toml +++ b/packages/jwebot-extension-host/pyproject.toml @@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta" [project] name = "jwebot-extension-host" -version = "1.2.0b1+1019" +version = "1.2.1b1+1019" description = "Single-release, single-invocation process host for JWeBot extensions" requires-python = ">=3.9" dependencies = [ - "jwebot-sdk==1.2.0b1+1019", - "jwebot-kernel==1.2.0b1+1019", + "jwebot-sdk==1.2.1b1+1019", + "jwebot-kernel==1.2.1b1+1019", ] [project.scripts] diff --git a/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py b/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py index e36c455..b3f2b2c 100644 --- a/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py +++ b/packages/jwebot-extension-host/src/jwebot_extension_host/__init__.py @@ -16,4 +16,4 @@ __all__ = [ "process_request", ] -__version__ = "1.2.0b1+1019" +__version__ = "1.2.1b1+1019" diff --git a/packages/jwebot-kernel/pyproject.toml b/packages/jwebot-kernel/pyproject.toml index e867e00..715ea3f 100644 --- a/packages/jwebot-kernel/pyproject.toml +++ b/packages/jwebot-kernel/pyproject.toml @@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta" [project] name = "jwebot-kernel" -version = "1.2.0b1+1019" +version = "1.2.1b1+1019" description = "Stable routing and extension authority for JWeBot" requires-python = ">=3.9" dependencies = [ - "jwebot-config==1.2.0b1+1019", - "jwebot-sdk==1.2.0b1+1019", - "jwebot-official-extensions==1.2.0b1+1019", + "jwebot-config==1.2.1b1+1019", + "jwebot-sdk==1.2.1b1+1019", + "jwebot-official-extensions==1.2.1b1+1019", "tomli>=2; python_version < '3.11'", ] diff --git a/packages/jwebot-kernel/src/jwebot_kernel/__init__.py b/packages/jwebot-kernel/src/jwebot_kernel/__init__.py index 233ec4b..388b541 100644 --- a/packages/jwebot-kernel/src/jwebot_kernel/__init__.py +++ b/packages/jwebot-kernel/src/jwebot_kernel/__init__.py @@ -107,4 +107,4 @@ __all__ = [ "verify_installed_release_signature", ] -__version__ = "1.2.0b1+1019" +__version__ = "1.2.1b1+1019" diff --git a/packages/jwebot-kernel/src/jwebot_kernel/edge_runtime.py b/packages/jwebot-kernel/src/jwebot_kernel/edge_runtime.py index bdb451c..ece4918 100644 --- a/packages/jwebot-kernel/src/jwebot_kernel/edge_runtime.py +++ b/packages/jwebot-kernel/src/jwebot_kernel/edge_runtime.py @@ -819,8 +819,21 @@ def style_chat_reply_text(value: object) -> str: return "\n".join(styled[:4]) +def chat_reply_parts(value: object) -> list[str]: + lines = [ + normalize_text(line) + for line in style_chat_reply_text(value).splitlines() + if normalize_text(line) + ] + return [clean_reply(line) for line in lines] + + def clean_chat_reply(value: object) -> str: - return clean_reply(style_chat_reply_text(value)) + parts = chat_reply_parts(value) + if len(parts) == 1: + return parts[0] + return pack_reply_parts(parts) + def cloud_reply_text(value: object) -> str: @@ -8631,60 +8644,75 @@ class EdgeApplication: reply = str(row.get("result_text", "")) if not reply: raise EdgeError("cloud result is unavailable for delivery") + replies = unpack_reply_parts(reply) or [reply] + if any(not part or len(part) > MAX_REPLY_CHARS for part in replies): + raise EdgeError("cloud result is invalid for delivery") runtime_python = Path( os.environ.get("JWEBOT_RUNTIME_PYTHON", str(PYTHON)) ).expanduser() if not DELIVERY_HELPER.is_file() or not runtime_python.is_file(): raise EdgeError("delivery helper is unavailable") - payload = { - "event_id": str(row.get("event_id", "")), - "group_fingerprint": str(row.get("group_fingerprint", "")), - "reply": reply, - } + event_id = str(row.get("event_id", "")) started = time.monotonic() - try: - result = subprocess.run( - [str(runtime_python), str(DELIVERY_HELPER)], - input=canonical_json(payload), - text=True, - capture_output=True, - timeout=60, - check=False, + for index, part in enumerate(replies, start=1): + part_event_id = ( + f"{event_id}:part-{index}" + if event_id and len(replies) > 1 + else event_id ) - except (OSError, subprocess.TimeoutExpired) as error: - self.telemetry.elapsed( - "edge.delivery.process", - started, - status="error", - event_id=row.get("event_id", ""), - group_id=row.get("group_fingerprint", ""), - process="delivery-helper", - error_type=type(error).__name__, - ) - raise EdgeError("delivery helper transport failure") from error - if result.returncode != 0: - self.telemetry.elapsed( - "edge.delivery.process", - started, - status="error", - event_id=row.get("event_id", ""), - group_id=row.get("group_fingerprint", ""), - process="delivery-helper", - outcome=f"returncode-{result.returncode}", - ) - output = f"{result.stdout}\n{result.stderr}".casefold() - if "selected chat changed" in output: - raise EdgeError("selected chat changed") - if "missing expected group" in output: - raise EdgeError("missing expected group") - raise EdgeError("delivery helper failed") + payload = { + "event_id": part_event_id, + "group_fingerprint": str(row.get("group_fingerprint", "")), + "reply": part, + } + try: + result = subprocess.run( + [str(runtime_python), str(DELIVERY_HELPER)], + input=canonical_json(payload), + text=True, + capture_output=True, + timeout=60, + check=False, + ) + except (OSError, subprocess.TimeoutExpired) as error: + self.telemetry.elapsed( + "edge.delivery.process", + started, + status="error", + event_id=row.get("event_id", ""), + group_id=row.get("group_fingerprint", ""), + process="delivery-helper", + messages=len(replies), + failed_message=index, + error_type=type(error).__name__, + ) + raise EdgeError("delivery helper transport failure") from error + if result.returncode != 0: + self.telemetry.elapsed( + "edge.delivery.process", + started, + status="error", + event_id=row.get("event_id", ""), + group_id=row.get("group_fingerprint", ""), + process="delivery-helper", + messages=len(replies), + failed_message=index, + outcome=f"returncode-{result.returncode}", + ) + output = f"{result.stdout}\n{result.stderr}".casefold() + if "selected chat changed" in output: + raise EdgeError("selected chat changed") + if "missing expected group" in output: + raise EdgeError("missing expected group") + raise EdgeError("delivery helper failed") self.telemetry.elapsed( "edge.delivery.process", started, event_id=row.get("event_id", ""), group_id=row.get("group_fingerprint", ""), process="delivery-helper", - chars=len(reply), + chars=sum(len(part) for part in replies), + messages=len(replies), ) def process_clock_once( diff --git a/packages/jwebot-official-extensions/pyproject.toml b/packages/jwebot-official-extensions/pyproject.toml index 950c4e3..5933bfb 100644 --- a/packages/jwebot-official-extensions/pyproject.toml +++ b/packages/jwebot-official-extensions/pyproject.toml @@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta" [project] name = "jwebot-official-extensions" -version = "1.2.0b1+1019" +version = "1.2.1b1+1019" description = "Reviewed local command adapters and broker implementation for JWeBot" requires-python = ">=3.9" dependencies = [ - "jwebot-sdk==1.2.0b1+1019", + "jwebot-sdk==1.2.1b1+1019", ] [tool.setuptools.packages.find] diff --git a/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py b/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py index c3a6ecf..d5961b5 100644 --- a/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py +++ b/packages/jwebot-official-extensions/src/jwebot_official_extensions/__init__.py @@ -90,4 +90,4 @@ __all__ = [ "render_feature_list", ] -__version__ = "1.2.0b1+1019" +__version__ = "1.2.1b1+1019" diff --git a/packages/jwebot-sdk/pyproject.toml b/packages/jwebot-sdk/pyproject.toml index 8207013..6c6e7ab 100644 --- a/packages/jwebot-sdk/pyproject.toml +++ b/packages/jwebot-sdk/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "jwebot-sdk" -version = "1.2.0b1+1019" +version = "1.2.1b1+1019" description = "Typed, capability-limited extension API for JWeBot plugins" requires-python = ">=3.9" dependencies = [] diff --git a/packages/jwebot-sdk/src/jwebot_sdk/__init__.py b/packages/jwebot-sdk/src/jwebot_sdk/__init__.py index ed5713f..6d66356 100644 --- a/packages/jwebot-sdk/src/jwebot_sdk/__init__.py +++ b/packages/jwebot-sdk/src/jwebot_sdk/__init__.py @@ -38,4 +38,4 @@ __all__ = [ "SingleGroupContext", ] -__version__ = "1.2.0b1+1019" +__version__ = "1.2.1b1+1019" diff --git a/pyproject.toml b/pyproject.toml index e3ac25c..2545954 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.jwebot] -release = "beta 1.2.0.1019" +release = "beta 1.2.1.1019" production_deployments_enabled_by_default = false [tool.jwebot.workspace] diff --git a/tests/test_jwebot_edge.py b/tests/test_jwebot_edge.py index 6c65395..1f33723 100644 --- a/tests/test_jwebot_edge.py +++ b/tests/test_jwebot_edge.py @@ -233,11 +233,15 @@ class RoutingTests(unittest.TestCase): "李神是谁喵", ) self.assertEqual( - MODULE.clean_chat_reply( + MODULE.unpack_reply_parts(MODULE.clean_chat_reply( "这波像是脑子知道要撤,但是手完全没同意," "而且还顺手把队友也拖下水了。" - ), - "这波像是脑子知道要撤\n但是手完全没同意\n而且还顺手把队友也拖下水了喵", + )), + [ + "这波像是脑子知道要撤喵", + "但是手完全没同意喵", + "而且还顺手把队友也拖下水了喵", + ], ) def test_profile_memory_rejects_bare_versions_but_keeps_audio_context(self): @@ -433,6 +437,52 @@ class LedgerAndApplicationTests(unittest.TestCase): "chat_delivered", ) + def test_delivery_sends_multi_reply_as_separate_helper_calls(self): + runtime_python = Path(self.temporary.name) / "python" + delivery_helper = Path(self.temporary.name) / "jwebot_edge_delivery.py" + runtime_python.write_text("# runtime\n", encoding="utf-8") + delivery_helper.write_text("# helper\n", encoding="utf-8") + result = mock.Mock(returncode=0, stdout="", stderr="") + reply = MODULE.pack_reply_parts(["第一段喵", "第二段喵"]) + + with mock.patch.object(MODULE, "DELIVERY_HELPER", delivery_helper), \ + mock.patch.dict( + os.environ, + {"JWEBOT_RUNTIME_PYTHON": str(runtime_python)}, + ), \ + mock.patch.object( + MODULE.subprocess, + "run", + return_value=result, + ) as run: + self.application.deliver( + { + "event_id": "event-1", + "group_fingerprint": "group-a", + "result_text": reply, + } + ) + + payloads = [ + json.loads(call.kwargs["input"]) + for call in run.call_args_list + ] + self.assertEqual( + payloads, + [ + { + "event_id": "event-1:part-1", + "group_fingerprint": "group-a", + "reply": "第一段喵", + }, + { + "event_id": "event-1:part-2", + "group_fingerprint": "group-a", + "reply": "第二段喵", + }, + ], + ) + def test_two_direct_generations_run_in_parallel(self): entered = [] entered_lock = threading.Lock() @@ -946,9 +996,9 @@ class LedgerAndApplicationTests(unittest.TestCase): self.assertEqual(version["route"], "local") self.assertIn("JWeBot 版本", version["text"]) self.assertIn("kernel", version["text"]) - self.assertIn("最新版本:beta 1.2.0.1019", version["text"]) - self.assertIn("更新时间:2026-10-19 17:05:47", version["text"]) - self.assertIn("更新摘要:优化 Taffy 群聊语气", version["text"]) + self.assertIn("最新版本:beta 1.2.1.1019", version["text"]) + self.assertIn("更新时间:2026-10-19 17:23:58", version["text"]) + self.assertIn("更新摘要:修复 Taffy 短句拆分只换行不分条", version["text"]) self.assertEqual(debug["route"], "local") self.assertIn("调试埋点", debug["text"]) self.assertEqual(balance["route"], "local") diff --git a/tests/test_release_update.py b/tests/test_release_update.py index 4b5d15e..1a509a4 100644 --- a/tests/test_release_update.py +++ b/tests/test_release_update.py @@ -20,7 +20,7 @@ SPEC.loader.exec_module(MODULE) class ReleaseUpdateTests(unittest.TestCase): - release = "beta 1.2.0.1019" + release = "beta 1.2.1.1019" def setUp(self): self.temporary = tempfile.TemporaryDirectory() @@ -111,7 +111,8 @@ class ReleaseUpdateTests(unittest.TestCase): self.assertEqual(summary, "第一行更新摘要, 同一条摘要可以换行。") announcement = MODULE.build_announcement(self.release, summary) self.assertIn(f"{self.release} 更新现已完成", announcement) - self.assertIn("更新补偿:摩拉 x200,原石 x1", announcement) + self.assertIn("更新补偿:摩拉 x200", announcement) + self.assertNotIn("原石 x1", announcement) self.assertNotIn("第二条", announcement) def test_policy_release_announces_primogem(self): @@ -187,7 +188,7 @@ class ReleaseUpdateTests(unittest.TestCase): self.assertIn("更新摩拉账户并修复重复回复问题", delivered[0][2]) self.assertEqual([state["balance"] for state in self.account_states()], [1200, 250]) for state in self.account_states(): - self.assertEqual(state["primogems"], 1) + self.assertEqual(state["primogems"], 0) matches = [ event for event in state["coin_events"] @@ -201,9 +202,7 @@ class ReleaseUpdateTests(unittest.TestCase): for event in state["primogem_events"] if event["reason"] == f"{self.release} 版本更新补偿" ] - self.assertEqual(len(primogem_matches), 1) - self.assertEqual(primogem_matches[0]["delta"], 1) - self.assertEqual(primogem_matches[0]["created_at"], applied_at.timestamp()) + self.assertEqual(primogem_matches, []) self.assertEqual(self.announcement_marker()["state"], "delivered") self.assertEqual(len(list(self.backups.glob("*.sqlite3"))), 1) @@ -221,7 +220,7 @@ class ReleaseUpdateTests(unittest.TestCase): deliverer=uncertain, ) self.assertEqual([state["balance"] for state in self.account_states()], [1200, 250]) - self.assertEqual([state["primogems"] for state in self.account_states()], [1, 1]) + self.assertEqual([state["primogems"] for state in self.account_states()], [0, 0]) self.assertEqual(self.announcement_marker()["state"], "uncertain") called = [] @@ -252,7 +251,7 @@ class ReleaseUpdateTests(unittest.TestCase): self.assertEqual(result["compensation"], "already_applied") self.assertEqual(len(called), 1) self.assertEqual([state["balance"] for state in self.account_states()], [1200, 250]) - self.assertEqual([state["primogems"] for state in self.account_states()], [1, 1]) + self.assertEqual([state["primogems"] for state in self.account_states()], [0, 0]) self.assertEqual(self.announcement_marker()["state"], "delivered")