release: alpha 0.6.1 local player plugins and capture reconciliation
check / local-gate (push) Failing after 3m24s
check / local-gate (push) Failing after 3m24s
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user