Release beta 1.2.1.1019
check / local-gate (push) Failing after 16m58s

This commit is contained in:
JWeBot Contributors
2026-10-19 17:26:44 +08:00
parent e6d6ddf1c6
commit 3a7c11e44d
17 changed files with 162 additions and 77 deletions
+8
View File
@@ -1,5 +1,13 @@
# Changelog # 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 ## beta 1.2.0.1019 - 2026-10-19 17:05:47
- 优化 Taffy 群聊语气:回复更短、更像群友,减少 AI 式自我解释,并保留主动插话、冷门问答和更新状态提示。 - 优化 Taffy 群聊语气:回复更短、更像群友,减少 AI 式自我解释,并保留主动插话、冷门问答和更新状态提示。
+3 -3
View File
@@ -8,7 +8,7 @@ JWeBot 是运行于 Linux 的单群微信自动回复系统。它通过本地 OC
观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件, 观察选定的微信窗口,将消息写入持久化 Edge 账本,调用经过审核的命令插件,
并通过带视觉确认的 GUI 发送链路回复。 并通过带视觉确认的 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 收口了当前稳定功能:本地命令插件、人物与上下文索引、 1.0 Beta 收口了当前稳定功能:本地命令插件、人物与上下文索引、
Steam 状态与时长查询、虚拟摩拉游戏、多人彩票、21 点、CS Major Steam 状态与时长查询、虚拟摩拉游戏、多人彩票、21 点、CS Major
@@ -269,10 +269,10 @@ visually confirmed GUI bridge.
Current version: Current version:
```text ```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 The 1.0 Beta baseline consolidates the stable local command set, person and
context indexes, Steam presence/playtime queries, virtual coin games, context indexes, Steam presence/playtime queries, virtual coin games,
+1 -1
View File
@@ -1 +1 @@
beta 1.2.0.1019 beta 1.2.1.1019
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "jwebot-config" name = "jwebot-config"
version = "1.2.0b1+1019" version = "1.2.1b1+1019"
description = "Profile-based configuration compiler for JWeBot" description = "Profile-based configuration compiler for JWeBot"
requires-python = ">=3.9" requires-python = ">=3.9"
dependencies = [ dependencies = [
@@ -36,4 +36,4 @@ __all__ = [
"write_default_profile", "write_default_profile",
] ]
__version__ = "1.2.0b1+1019" __version__ = "1.2.1b1+1019"
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "jwebot-extension-host" name = "jwebot-extension-host"
version = "1.2.0b1+1019" version = "1.2.1b1+1019"
description = "Single-release, single-invocation process host for JWeBot extensions" description = "Single-release, single-invocation process host for JWeBot extensions"
requires-python = ">=3.9" requires-python = ">=3.9"
dependencies = [ dependencies = [
"jwebot-sdk==1.2.0b1+1019", "jwebot-sdk==1.2.1b1+1019",
"jwebot-kernel==1.2.0b1+1019", "jwebot-kernel==1.2.1b1+1019",
] ]
[project.scripts] [project.scripts]
@@ -16,4 +16,4 @@ __all__ = [
"process_request", "process_request",
] ]
__version__ = "1.2.0b1+1019" __version__ = "1.2.1b1+1019"
+4 -4
View File
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "jwebot-kernel" name = "jwebot-kernel"
version = "1.2.0b1+1019" version = "1.2.1b1+1019"
description = "Stable routing and extension authority for JWeBot" description = "Stable routing and extension authority for JWeBot"
requires-python = ">=3.9" requires-python = ">=3.9"
dependencies = [ dependencies = [
"jwebot-config==1.2.0b1+1019", "jwebot-config==1.2.1b1+1019",
"jwebot-sdk==1.2.0b1+1019", "jwebot-sdk==1.2.1b1+1019",
"jwebot-official-extensions==1.2.0b1+1019", "jwebot-official-extensions==1.2.1b1+1019",
"tomli>=2; python_version < '3.11'", "tomli>=2; python_version < '3.11'",
] ]
@@ -107,4 +107,4 @@ __all__ = [
"verify_installed_release_signature", "verify_installed_release_signature",
] ]
__version__ = "1.2.0b1+1019" __version__ = "1.2.1b1+1019"
@@ -819,8 +819,21 @@ def style_chat_reply_text(value: object) -> str:
return "\n".join(styled[:4]) 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: 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: def cloud_reply_text(value: object) -> str:
@@ -8631,60 +8644,75 @@ class EdgeApplication:
reply = str(row.get("result_text", "")) reply = str(row.get("result_text", ""))
if not reply: if not reply:
raise EdgeError("cloud result is unavailable for delivery") 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( runtime_python = Path(
os.environ.get("JWEBOT_RUNTIME_PYTHON", str(PYTHON)) os.environ.get("JWEBOT_RUNTIME_PYTHON", str(PYTHON))
).expanduser() ).expanduser()
if not DELIVERY_HELPER.is_file() or not runtime_python.is_file(): if not DELIVERY_HELPER.is_file() or not runtime_python.is_file():
raise EdgeError("delivery helper is unavailable") raise EdgeError("delivery helper is unavailable")
payload = { event_id = str(row.get("event_id", ""))
"event_id": str(row.get("event_id", "")),
"group_fingerprint": str(row.get("group_fingerprint", "")),
"reply": reply,
}
started = time.monotonic() started = time.monotonic()
try: for index, part in enumerate(replies, start=1):
result = subprocess.run( part_event_id = (
[str(runtime_python), str(DELIVERY_HELPER)], f"{event_id}:part-{index}"
input=canonical_json(payload), if event_id and len(replies) > 1
text=True, else event_id
capture_output=True,
timeout=60,
check=False,
) )
except (OSError, subprocess.TimeoutExpired) as error: payload = {
self.telemetry.elapsed( "event_id": part_event_id,
"edge.delivery.process", "group_fingerprint": str(row.get("group_fingerprint", "")),
started, "reply": part,
status="error", }
event_id=row.get("event_id", ""), try:
group_id=row.get("group_fingerprint", ""), result = subprocess.run(
process="delivery-helper", [str(runtime_python), str(DELIVERY_HELPER)],
error_type=type(error).__name__, input=canonical_json(payload),
) text=True,
raise EdgeError("delivery helper transport failure") from error capture_output=True,
if result.returncode != 0: timeout=60,
self.telemetry.elapsed( check=False,
"edge.delivery.process", )
started, except (OSError, subprocess.TimeoutExpired) as error:
status="error", self.telemetry.elapsed(
event_id=row.get("event_id", ""), "edge.delivery.process",
group_id=row.get("group_fingerprint", ""), started,
process="delivery-helper", status="error",
outcome=f"returncode-{result.returncode}", event_id=row.get("event_id", ""),
) group_id=row.get("group_fingerprint", ""),
output = f"{result.stdout}\n{result.stderr}".casefold() process="delivery-helper",
if "selected chat changed" in output: messages=len(replies),
raise EdgeError("selected chat changed") failed_message=index,
if "missing expected group" in output: error_type=type(error).__name__,
raise EdgeError("missing expected group") )
raise EdgeError("delivery helper failed") 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( self.telemetry.elapsed(
"edge.delivery.process", "edge.delivery.process",
started, started,
event_id=row.get("event_id", ""), event_id=row.get("event_id", ""),
group_id=row.get("group_fingerprint", ""), group_id=row.get("group_fingerprint", ""),
process="delivery-helper", process="delivery-helper",
chars=len(reply), chars=sum(len(part) for part in replies),
messages=len(replies),
) )
def process_clock_once( def process_clock_once(
@@ -4,11 +4,11 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "jwebot-official-extensions" name = "jwebot-official-extensions"
version = "1.2.0b1+1019" version = "1.2.1b1+1019"
description = "Reviewed local command adapters and broker implementation for JWeBot" description = "Reviewed local command adapters and broker implementation for JWeBot"
requires-python = ">=3.9" requires-python = ">=3.9"
dependencies = [ dependencies = [
"jwebot-sdk==1.2.0b1+1019", "jwebot-sdk==1.2.1b1+1019",
] ]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
@@ -90,4 +90,4 @@ __all__ = [
"render_feature_list", "render_feature_list",
] ]
__version__ = "1.2.0b1+1019" __version__ = "1.2.1b1+1019"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "jwebot-sdk" name = "jwebot-sdk"
version = "1.2.0b1+1019" version = "1.2.1b1+1019"
description = "Typed, capability-limited extension API for JWeBot plugins" description = "Typed, capability-limited extension API for JWeBot plugins"
requires-python = ">=3.9" requires-python = ">=3.9"
dependencies = [] dependencies = []
@@ -38,4 +38,4 @@ __all__ = [
"SingleGroupContext", "SingleGroupContext",
] ]
__version__ = "1.2.0b1+1019" __version__ = "1.2.1b1+1019"
+1 -1
View File
@@ -1,5 +1,5 @@
[tool.jwebot] [tool.jwebot]
release = "beta 1.2.0.1019" release = "beta 1.2.1.1019"
production_deployments_enabled_by_default = false production_deployments_enabled_by_default = false
[tool.jwebot.workspace] [tool.jwebot.workspace]
+56 -6
View File
@@ -233,11 +233,15 @@ class RoutingTests(unittest.TestCase):
"李神是谁喵", "李神是谁喵",
) )
self.assertEqual( 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): def test_profile_memory_rejects_bare_versions_but_keeps_audio_context(self):
@@ -433,6 +437,52 @@ class LedgerAndApplicationTests(unittest.TestCase):
"chat_delivered", "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): def test_two_direct_generations_run_in_parallel(self):
entered = [] entered = []
entered_lock = threading.Lock() entered_lock = threading.Lock()
@@ -946,9 +996,9 @@ class LedgerAndApplicationTests(unittest.TestCase):
self.assertEqual(version["route"], "local") self.assertEqual(version["route"], "local")
self.assertIn("JWeBot 版本", version["text"]) self.assertIn("JWeBot 版本", version["text"])
self.assertIn("kernel", version["text"]) self.assertIn("kernel", version["text"])
self.assertIn("最新版本:beta 1.2.0.1019", version["text"]) self.assertIn("最新版本:beta 1.2.1.1019", version["text"])
self.assertIn("更新时间:2026-10-19 17:05:47", version["text"]) self.assertIn("更新时间:2026-10-19 17:23:58", version["text"])
self.assertIn("更新摘要:优化 Taffy 群聊语气", version["text"]) self.assertIn("更新摘要:修复 Taffy 短句拆分只换行不分条", version["text"])
self.assertEqual(debug["route"], "local") self.assertEqual(debug["route"], "local")
self.assertIn("调试埋点", debug["text"]) self.assertIn("调试埋点", debug["text"])
self.assertEqual(balance["route"], "local") self.assertEqual(balance["route"], "local")
+7 -8
View File
@@ -20,7 +20,7 @@ SPEC.loader.exec_module(MODULE)
class ReleaseUpdateTests(unittest.TestCase): class ReleaseUpdateTests(unittest.TestCase):
release = "beta 1.2.0.1019" release = "beta 1.2.1.1019"
def setUp(self): def setUp(self):
self.temporary = tempfile.TemporaryDirectory() self.temporary = tempfile.TemporaryDirectory()
@@ -111,7 +111,8 @@ class ReleaseUpdateTests(unittest.TestCase):
self.assertEqual(summary, "第一行更新摘要, 同一条摘要可以换行。") self.assertEqual(summary, "第一行更新摘要, 同一条摘要可以换行。")
announcement = MODULE.build_announcement(self.release, summary) announcement = MODULE.build_announcement(self.release, summary)
self.assertIn(f"{self.release} 更新现已完成", announcement) self.assertIn(f"{self.release} 更新现已完成", announcement)
self.assertIn("更新补偿:摩拉 x200,原石 x1", announcement) self.assertIn("更新补偿:摩拉 x200", announcement)
self.assertNotIn("原石 x1", announcement)
self.assertNotIn("第二条", announcement) self.assertNotIn("第二条", announcement)
def test_policy_release_announces_primogem(self): def test_policy_release_announces_primogem(self):
@@ -187,7 +188,7 @@ class ReleaseUpdateTests(unittest.TestCase):
self.assertIn("更新摩拉账户并修复重复回复问题", delivered[0][2]) self.assertIn("更新摩拉账户并修复重复回复问题", delivered[0][2])
self.assertEqual([state["balance"] for state in self.account_states()], [1200, 250]) self.assertEqual([state["balance"] for state in self.account_states()], [1200, 250])
for state in self.account_states(): for state in self.account_states():
self.assertEqual(state["primogems"], 1) self.assertEqual(state["primogems"], 0)
matches = [ matches = [
event event
for event in state["coin_events"] for event in state["coin_events"]
@@ -201,9 +202,7 @@ class ReleaseUpdateTests(unittest.TestCase):
for event in state["primogem_events"] for event in state["primogem_events"]
if event["reason"] == f"{self.release} 版本更新补偿" if event["reason"] == f"{self.release} 版本更新补偿"
] ]
self.assertEqual(len(primogem_matches), 1) self.assertEqual(primogem_matches, [])
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(self.announcement_marker()["state"], "delivered")
self.assertEqual(len(list(self.backups.glob("*.sqlite3"))), 1) self.assertEqual(len(list(self.backups.glob("*.sqlite3"))), 1)
@@ -221,7 +220,7 @@ class ReleaseUpdateTests(unittest.TestCase):
deliverer=uncertain, deliverer=uncertain,
) )
self.assertEqual([state["balance"] for state in self.account_states()], [1200, 250]) 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") self.assertEqual(self.announcement_marker()["state"], "uncertain")
called = [] called = []
@@ -252,7 +251,7 @@ class ReleaseUpdateTests(unittest.TestCase):
self.assertEqual(result["compensation"], "already_applied") self.assertEqual(result["compensation"], "already_applied")
self.assertEqual(len(called), 1) self.assertEqual(len(called), 1)
self.assertEqual([state["balance"] for state in self.account_states()], [1200, 250]) 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") self.assertEqual(self.announcement_marker()["state"], "delivered")