From e6d6ddf1c658c135c376ee1635007d3fc89d8594 Mon Sep 17 00:00:00 2001 From: JWeBot Contributors Date: Mon, 19 Oct 2026 17:09:40 +0800 Subject: [PATCH] Release beta 1.2.0.1019 --- CHANGELOG.md | 11 +++ README.md | 6 +- VERSION | 2 +- jwebot_release_update.py | 1 + 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 | 97 ++++++++++++++++++- .../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 | 48 ++++++++- tests/test_release_update.py | 21 ++-- 18 files changed, 185 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb3a12d..7c4ede5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## beta 1.2.0.1019 - 2026-10-19 17:05:47 + +- 优化 Taffy 群聊语气:回复更短、更像群友,减少 AI 式自我解释,并保留主动插话、冷门问答和更新状态提示。 +- Add a chat-specific reply style layer that removes common AI-like padding, + keeps direct unknown-subject clarification, and splits long casual replies + into shorter lines. +- Apply the style rule to direct chat and cloud-agent replies while leaving + structured local command output unchanged. +- This 1.2 release grants each existing account both 200 Mora and + 1 primogem. + ## beta 1.1.2.1019 - 2026-10-19 16:38:10 - 新增低频主动插话、随机冷门知识问答和更新生命周期提示:Taffy 会偶尔自然冒泡,抛出限时答题奖励摩拉,并在停机、上线和更新就绪时主动说明状态。 diff --git a/README.md b/README.md index 100558d..11f2968 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC 观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件, 并通过带视觉确认的 GUI 发送链路回复。 -当前版本:`beta 1.1.2.1019`,更新时间:2026-10-19 16:38:10。 +当前版本:`beta 1.2.0.1019`,更新时间:2026-10-19 17:05:47。 1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、 Steam 状态与时长查询、虚拟摩拉游戏、多人彩票、21 点、CS Major @@ -269,10 +269,10 @@ visually confirmed GUI bridge. Current version: ```text -beta 1.1.2.1019 +beta 1.2.0.1019 ``` -Updated: 2026-10-19 16:38:10. +Updated: 2026-10-19 17:05:47. 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 37013db..09261ee 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -beta 1.1.2.1019 +beta 1.2.0.1019 diff --git a/jwebot_release_update.py b/jwebot_release_update.py index 388ad20..edef14f 100644 --- a/jwebot_release_update.py +++ b/jwebot_release_update.py @@ -35,6 +35,7 @@ COMPENSATION_AMOUNT = 200 PRIMOGEM_COMPENSATION_RELEASES = { "beta 1.0.5.1016": 1, "beta 1.1.0.1016": 1, + "beta 1.2.0.1019": 1, } MAX_BALANCE = 1_000_000_000 MAX_SUMMARY_CHARS = 300 diff --git a/packages/jwebot-config/pyproject.toml b/packages/jwebot-config/pyproject.toml index 509ff8c..3f7b907 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.1.2b1+1019" +version = "1.2.0b1+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 4fb1572..a14b71f 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.1.2b1+1019" +__version__ = "1.2.0b1+1019" diff --git a/packages/jwebot-extension-host/pyproject.toml b/packages/jwebot-extension-host/pyproject.toml index 02b0079..ff30e6b 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.1.2b1+1019" +version = "1.2.0b1+1019" description = "Single-release, single-invocation process host for JWeBot extensions" requires-python = ">=3.9" dependencies = [ - "jwebot-sdk==1.1.2b1+1019", - "jwebot-kernel==1.1.2b1+1019", + "jwebot-sdk==1.2.0b1+1019", + "jwebot-kernel==1.2.0b1+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 7c4496c..e36c455 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.1.2b1+1019" +__version__ = "1.2.0b1+1019" diff --git a/packages/jwebot-kernel/pyproject.toml b/packages/jwebot-kernel/pyproject.toml index fc3eace..e867e00 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.1.2b1+1019" +version = "1.2.0b1+1019" description = "Stable routing and extension authority for JWeBot" requires-python = ">=3.9" dependencies = [ - "jwebot-config==1.1.2b1+1019", - "jwebot-sdk==1.1.2b1+1019", - "jwebot-official-extensions==1.1.2b1+1019", + "jwebot-config==1.2.0b1+1019", + "jwebot-sdk==1.2.0b1+1019", + "jwebot-official-extensions==1.2.0b1+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 5e06291..233ec4b 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.1.2b1+1019" +__version__ = "1.2.0b1+1019" diff --git a/packages/jwebot-kernel/src/jwebot_kernel/edge_runtime.py b/packages/jwebot-kernel/src/jwebot_kernel/edge_runtime.py index a21af38..bdb451c 100644 --- a/packages/jwebot-kernel/src/jwebot_kernel/edge_runtime.py +++ b/packages/jwebot-kernel/src/jwebot_kernel/edge_runtime.py @@ -200,6 +200,38 @@ BOT_IDENTITY_NOTE = ( "你是 JWeBot。显示名和别名只用于当前群的会话上下文," "不代表身份认证或权限。" ) +TAFFY_CHAT_STYLE_NOTE = ( + "语气硬规则:像群友一样短句回复,优先 1 到 3 句;" + "能一句自然反问就只反问。遇到不认识的人、梗或对象," + "直接问“X 是啥?”或“谁啊?”,不要铺垫。" + "不要说自己只是 AI、机器人、语言模型,不要写能力边界、免责声明、" + "自我介绍或“不能凭空给自己打分”这类自证。" + "长回复要拆成短句,别一句话塞满。" +) +CHAT_REPLY_REDUNDANT_LINE = re.compile( + r"(?:" + r"作为(?:一个)?(?:AI|人工智能|语言模型)|" + r"(?:AI|人工智能|语言模型)[,,。;;\s]*(?:无法|不能|没有办法)|" + r"我(?:只是|就是)(?:一个|个)?(?:会唠嗑|聊天|机器人|AI|人工智能)|" + r"反正我就是|会唠嗑|聊聊天|记记小账|别的功能想都别想|" + r"不能凭空|凭空.{0,12}打(?:满)?分|" + r"(?:无法|不能|没有办法).{0,10}(?:比较|评价|打分)|" + r"不要主动声明无法认证身份|不代表身份认证" + r")", + re.IGNORECASE, +) +UNKNOWN_CLARIFICATION_PATTERNS = ( + re.compile( + r"(?P[\w\u4e00-\u9fff]{1,24}?)(?:是|算是|叫)?" + r"(?P啥|什么|谁)" + r"[^。!?!?;;\n]{0,20}(?:不清楚|不知道|不认识|没听过)" + ), + re.compile( + r"(?:不清楚|不知道|不认识|没听过)" + r"[^。!?!?;;\n]{0,12}(?P[\w\u4e00-\u9fff]{1,24}?)" + r"(?:是|算是|叫)?(?P啥|什么|谁)" + ), +) TRIGGER = re.compile( rf"^\s*[@@]\s*{re.escape(BOT_MENTION)}" r"(?:\s*[::,,])?\s*(\S.*)$", @@ -734,6 +766,63 @@ def clean_reply(value: object) -> str: return text +def unknown_subject_clarification(value: str) -> str: + text = normalize_text(value) + for pattern in UNKNOWN_CLARIFICATION_PATTERNS: + match = pattern.search(text) + if match is None: + continue + subject = match.group("subject").strip("_") + if not subject or subject in {"这", "这个", "那", "那个", "他", "她", "它"}: + continue + if match.group("question") == "谁": + return f"{subject}是谁" + return f"{subject}是啥" + return "" + + +def split_chat_reply_line(line: str) -> list[str]: + separators = re.findall(r"[。!?!?;;,,]+", line) + if len(line) <= 34 and len(separators) < 2: + return [line] + fragments = [ + normalize_text(fragment) + for fragment in re.split(r"[。!?!?;;]+|[,,]+", line) + if normalize_text(fragment) + ] + if 1 < len(fragments) <= 6 and all(len(fragment) <= 46 for fragment in fragments): + return fragments + return [line] + + +def style_chat_reply_text(value: object) -> str: + raw_lines = [ + normalize_text(line) + for line in str(value or "").splitlines() + if normalize_text(line) + ] + if not raw_lines: + return "" + clarification = unknown_subject_clarification(" ".join(raw_lines)) + if clarification: + return clarification + filtered = [ + line + for line in raw_lines + if not CHAT_REPLY_REDUNDANT_LINE.search(line) + ] or raw_lines[:1] + styled: list[str] = [] + for line in filtered: + styled.extend(split_chat_reply_line(line)) + if len(styled) >= 4: + break + return "\n".join(styled[:4]) + + +def clean_chat_reply(value: object) -> str: + return clean_reply(style_chat_reply_text(value)) + + def cloud_reply_text(value: object) -> str: """Convert the tool-free cloud agent contract into display text.""" @@ -750,9 +839,9 @@ def cloud_reply_text(value: object) -> str: try: payload = json.loads(candidate[: closing + 1]) except json.JSONDecodeError: - return clean_reply(candidate) + return clean_chat_reply(candidate) else: - return clean_reply(candidate) + return clean_chat_reply(candidate) if isinstance(payload, str): candidate = payload.strip() continue @@ -7042,6 +7131,7 @@ class DeepSeekClient: "使用活泼、嘴硬心软、带轻微吐槽感的虚拟主播式中文," "短句自然,偶尔调侃但始终友善,常以喵收尾。" + BOT_IDENTITY_NOTE + + TAFFY_CHAT_STYLE_NOTE + "群友间的调侃、昵称、游戏评价和互动习惯按普通对话处理;" "不要主动训诫。重复出现且与群内互动有关的轻松玩笑可以保留为记忆。" "不能使用工具、浏览器、命令或外部操作。" @@ -7122,7 +7212,7 @@ class DeepSeekClient: raise DeepSeekUnavailable(f"direct API HTTP {error.code}") from error except (urllib.error.URLError, TimeoutError, OSError) as error: raise DeepSeekUnavailable("direct API transport failure") from error - return clean_reply(deepseek_response_content(raw)) + return clean_chat_reply(deepseek_response_content(raw)) def profile_facts( self, @@ -7257,6 +7347,7 @@ class CloudRunner: "Chinese response for the group in JWeBot's lively, lightly teasing, " "warm virtual-streamer voice. " + BOT_IDENTITY_NOTE + + TAFFY_CHAT_STYLE_NOTE + " Treat friendly banter or nicknames as ordinary conversation; do not " "lecture about them. Recurring, low-risk playful details may be retained " "as group-scoped memory. " diff --git a/packages/jwebot-official-extensions/pyproject.toml b/packages/jwebot-official-extensions/pyproject.toml index e0125e0..950c4e3 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.1.2b1+1019" +version = "1.2.0b1+1019" description = "Reviewed local command adapters and broker implementation for JWeBot" requires-python = ">=3.9" dependencies = [ - "jwebot-sdk==1.1.2b1+1019", + "jwebot-sdk==1.2.0b1+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 0c2f84b..c3a6ecf 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.1.2b1+1019" +__version__ = "1.2.0b1+1019" diff --git a/packages/jwebot-sdk/pyproject.toml b/packages/jwebot-sdk/pyproject.toml index 35b6b11..8207013 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.1.2b1+1019" +version = "1.2.0b1+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 124084c..ed5713f 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.1.2b1+1019" +__version__ = "1.2.0b1+1019" diff --git a/pyproject.toml b/pyproject.toml index d45e74e..e3ac25c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.jwebot] -release = "beta 1.1.2.1019" +release = "beta 1.2.0.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 6f708ae..6c65395 100644 --- a/tests/test_jwebot_edge.py +++ b/tests/test_jwebot_edge.py @@ -219,6 +219,27 @@ class RoutingTests(unittest.TestCase): self.assertEqual(len(MODULE.clean_reply("好" * 900)), 900) self.assertEqual(MODULE.FAST_ACKNOWLEDGEMENT, "喵......") + def test_chat_reply_style_removes_ai_like_padding(self): + self.assertEqual( + MODULE.clean_chat_reply( + "李神是啥我都不清楚,怎么比,总不能凭空给自己打满分吧\n" + "反正我就是个会唠嗑的,聊聊天、记记小账还行," + "别的功能想都别想喵" + ), + "李神是啥喵", + ) + self.assertEqual( + MODULE.clean_chat_reply("作为AI我无法比较。\n李神是谁?"), + "李神是谁喵", + ) + self.assertEqual( + MODULE.clean_chat_reply( + "这波像是脑子知道要撤,但是手完全没同意," + "而且还顺手把队友也拖下水了。" + ), + "这波像是脑子知道要撤\n但是手完全没同意\n而且还顺手把队友也拖下水了喵", + ) + def test_profile_memory_rejects_bare_versions_but_keeps_audio_context(self): self.assertFalse(MODULE.profile_fact_is_safe("5.1")) self.assertTrue(MODULE.profile_fact_is_safe("最近在研究 5.1 音响配置")) @@ -925,9 +946,9 @@ class LedgerAndApplicationTests(unittest.TestCase): self.assertEqual(version["route"], "local") self.assertIn("JWeBot 版本", version["text"]) self.assertIn("kernel", version["text"]) - self.assertIn("最新版本:beta 1.1.2.1019", version["text"]) - self.assertIn("更新时间:2026-10-19 16:38:10", version["text"]) - self.assertIn("更新摘要:新增低频主动插话、随机冷门知识问答", 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.assertEqual(debug["route"], "local") self.assertIn("调试埋点", debug["text"]) self.assertEqual(balance["route"], "local") @@ -2175,6 +2196,10 @@ class DirectClientTests(unittest.TestCase): self.assertNotIn("temperature", payload) self.assertEqual(request.get_header("Authorization"), "Bearer test-key") self.assertIn(MODULE.BOT_IDENTITY_NOTE, payload["messages"][0]["content"]) + self.assertIn( + MODULE.TAFFY_CHAT_STYLE_NOTE, + payload["messages"][0]["content"], + ) self.assertTrue( any( message["content"].startswith("CURRENT_GROUP_SENDER_LABEL:") @@ -2763,6 +2788,23 @@ class CloudRunnerTests(unittest.TestCase): self.assertEqual(MODULE.cloud_reply_text(nested), "嵌套内容喵") + def test_cloud_protocol_applies_chat_reply_style(self): + self.assertEqual( + MODULE.cloud_reply_text( + json.dumps( + { + "route": "reply", + "text": ( + "李神是啥我都不清楚,怎么比," + "总不能凭空给自己打满分吧" + ), + }, + ensure_ascii=False, + ) + ), + "李神是啥喵", + ) + def test_cloud_protocol_ignores_a_suffix_after_the_json_object(self): self.assertEqual( MODULE.cloud_reply_text( diff --git a/tests/test_release_update.py b/tests/test_release_update.py index a11633c..4b5d15e 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.1.2.1019" + release = "beta 1.2.0.1019" def setUp(self): self.temporary = tempfile.TemporaryDirectory() @@ -111,12 +111,15 @@ class ReleaseUpdateTests(unittest.TestCase): self.assertEqual(summary, "第一行更新摘要, 同一条摘要可以换行。") announcement = MODULE.build_announcement(self.release, summary) self.assertIn(f"{self.release} 更新现已完成", announcement) - self.assertIn("更新补偿:摩拉 x200", announcement) - self.assertNotIn("原石 x1", announcement) + self.assertIn("更新补偿:摩拉 x200,原石 x1", announcement) self.assertNotIn("第二条", announcement) def test_policy_release_announces_primogem(self): - for release in ("beta 1.0.5.1016", "beta 1.1.0.1016"): + for release in ( + "beta 1.0.5.1016", + "beta 1.1.0.1016", + "beta 1.2.0.1019", + ): self.assertEqual(MODULE.primogem_compensation_amount(release), 1) announcement = MODULE.build_announcement(release, "原石系统上线。") self.assertIn("更新补偿:摩拉 x200,原石 x1", announcement) @@ -184,7 +187,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"], 0) + self.assertEqual(state["primogems"], 1) matches = [ event for event in state["coin_events"] @@ -198,7 +201,9 @@ class ReleaseUpdateTests(unittest.TestCase): for event in state["primogem_events"] if event["reason"] == f"{self.release} 版本更新补偿" ] - self.assertEqual(primogem_matches, []) + 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(self.announcement_marker()["state"], "delivered") self.assertEqual(len(list(self.backups.glob("*.sqlite3"))), 1) @@ -216,7 +221,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()], [0, 0]) + self.assertEqual([state["primogems"] for state in self.account_states()], [1, 1]) self.assertEqual(self.announcement_marker()["state"], "uncertain") called = [] @@ -247,7 +252,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()], [0, 0]) + self.assertEqual([state["primogems"] for state in self.account_states()], [1, 1]) self.assertEqual(self.announcement_marker()["state"], "delivered")