release: alpha 0.6.1 local player plugins and capture reconciliation
check / local-gate (push) Failing after 3m24s

This commit is contained in:
JWeBot Contributors
2026-10-15 05:01:40 +08:00
parent 84ba2193df
commit f7fedc54e4
40 changed files with 1346 additions and 38 deletions
@@ -9,11 +9,13 @@ import json
import re
import subprocess
import time
from collections import Counter
from datetime import datetime, timezone
from typing import Any, Callable
from zoneinfo import ZoneInfo
from .descriptors import LOCAL_COMMAND_ALIASES, render_feature_list
from .player_games import PlayerGames
LOCAL_GAME_TIMEOUT_SECONDS = 10 * 60
@@ -104,6 +106,7 @@ class LocalCommandEngine:
self.person_display_label = person_display_label
self.profile_fact_is_safe = profile_fact_is_safe
self.profile_expiry = profile_expiry
self.players = PlayerGames(ledger, person_display_label)
def handle(self, row: dict[str, Any], event: dict[str, Any]) -> str:
question = str(row["question"])
@@ -112,6 +115,12 @@ class LocalCommandEngine:
raise LocalCommandError("本地命令不存在,发 /功能列表 查看可用本地功能")
if kind == "feature_list":
return self.reply_cleaner(self.feature_list())
if kind == "context_summary":
return self.reply_cleaner(self.context_summary(row, arguments))
if kind in {"person_directory", "person_info"}:
return self.reply_cleaner(self.person_index(row, kind, arguments))
if kind in {"wallet", "prize_draw", "lottery", "blackjack", "game_rules"}:
return self.reply_cleaner(self.players.handle(row, kind, arguments))
if kind == "dice":
return self.reply_cleaner(self.dice(row, arguments))
if kind == "dice_challenge":
@@ -229,6 +238,82 @@ class LocalCommandEngine:
)
return f"本地记录命中“{query}”:{listing}"
def context_summary(self, row: dict[str, Any], arguments: str) -> str:
arguments = arguments.strip()
if arguments.strip() not in {"", "100", "200", "300"}:
return "用法:/上下文 100|200|300(/context),默认100条"
limit = int(arguments or 100)
self.ledger.bootstrap_repeat_history()
records = self.ledger.recent_context_messages(
str(row["group_fingerprint"]), limit,
exclude_event_id=str(row["event_id"]))
if not records:
return "当前群还没有可总结的本地文字记录"
# Local extractive summary: rank representative messages, retaining chronology.
stop = {"这个", "那个", "就是", "然后", "现在", "我们", "你们", "可以",
"不是", "一个", "什么", "怎么", "感觉", "还是", "已经", "没有",
"the", "and", "you", "that", "this", "jwebot"}
texts = [re.sub(r"^@\S+\s+", "", normalize_text(record["text"])) for record in records]
tokens = [set(re.findall(r"[a-zA-Z][a-zA-Z0-9_-]{2,}|[\u4e00-\u9fff]{2}", text.casefold())) - stop
for text in texts]
frequency = Counter(token for message in tokens for token in message)
topics = [token for token, count in frequency.most_common(6) if count > 1]
scored = sorted(range(len(records)), key=lambda i: (
-sum(frequency[token] for token in tokens[i]),
-int(bool(re.search(r"决定|计划|问题|需要|建议|修复|明天|[??]", texts[i]))),
-i))
selected: list[int] = []
seen: set[str] = set()
for index in scored:
if texts[index] in seen or texts[index].lstrip().startswith("/"):
continue
selected.append(index)
seen.add(texts[index])
if len(selected) == 5:
break
if not selected:
selected = list(range(max(0, len(records) - 3), len(records)))
people = len({record["display_name"] for record in records})
lines = [f"上下文总结(本地提取):请求最近{limit}条,实际{len(records)}条,{people}人。",
f"范围 {records[0]['observed_at'][:16]} ~ {records[-1]['observed_at'][:16]}。",
"主要话题:" + ("、".join(topics) if topics else "话题分散,以下为代表性内容。")]
for index in sorted(selected):
record = records[index]
lines.append(f"• {record['display_name']}:{texts[index][:100]}")
lines.append("仅覆盖本机已记录的当前群文字;问题或计划按原话提取。")
return "\n".join(lines)
def person_index(self, row: dict[str, Any], kind: str, arguments: str) -> str:
group = str(row["group_fingerprint"])
directory = self.ledger.person_directory(group)
if kind == "person_directory":
if arguments and not re.fullmatch(r"[1-9]\d{0,2}", arguments):
return "用法:/人物列表 [页码](/people),每页5人"
page = int(arguments or 1)
pages = max(1, (len(directory) + 4) // 5)
if page > pages:
return f"只有 {pages} 页;/人物列表 1"
lines = [f"当前群人物与别名({page}/{pages},共{len(directory)}人)"]
for person in directory[(page - 1) * 5:page * 5]:
aliases = "、".join(person["aliases"])[:80] or "未配置"
prefixes = "、".join(person["prefixes"])[:40]
lines.append(f"{person['display_name']}:{aliases}" + (f";前缀 {prefixes}*" if prefixes else ""))
lines.append("/人物 [名称或别名](/person)查看基本属性;/人物列表 页码 翻页")
return "\n".join(lines)
requested = normalize_text(arguments) or str(row["sender"])
person = self.ledger.resolve_person(group, requested)
entry = next((item for item in directory if item["person_key"] == person["person_key"]), None)
if arguments and entry is None:
return "当前群没有这个已配置人物或别名;/人物列表 查看索引"
facts = str(person.get("identity_fact", ""))
if facts and not self.profile_fact_is_safe(facts):
facts = ""
aliases = "、".join(entry["aliases"])[:160] if entry else "未配置"
return (f"人物:{self.person_display_label(person['display_name'])};"
f"群内索引 {person['person_key'][:12]};别名:{aliases or '未配置'}。\n"
f"基本属性:{facts or '暂无配置'}。\n"
"本人金币 /金币;个人记忆 /我的记忆。身份由本地昵称映射解析,无法读取微信原生账号ID。")
@staticmethod
def service_state(service_name: str) -> str:
try: