@@ -77,6 +77,7 @@ from jwebot_official_extensions.player_games import (
|
||||
append_game_coin_event,
|
||||
append_game_record,
|
||||
blackjack_achievement_lines,
|
||||
card_label,
|
||||
hand_value,
|
||||
lottery_achievement_lines,
|
||||
next_game_index,
|
||||
@@ -108,6 +109,8 @@ MAX_CONTEXT_TURNS = 40
|
||||
CONTEXT_CANDIDATE_LIMIT = 240
|
||||
CONTEXT_RECENT_FALLBACK_TURNS = 10
|
||||
CONTEXT_TRUSTED_SENDER_CONFIDENCE = 0.80
|
||||
ACTIVE_THREAD_TTL_SECONDS = 10 * 60
|
||||
ACTIVE_THREAD_SHORT_LIMIT = 24
|
||||
MULTI_REPLY_DELAY_MIN_SECONDS = 0.5
|
||||
MULTI_REPLY_DELAY_MAX_SECONDS = 1.0
|
||||
OCR_EVENT_SOURCES = frozenset({"ocr", "rapidocr-local"})
|
||||
@@ -494,6 +497,25 @@ CONTEXT_SELF_REFERENCE = re.compile(
|
||||
r"我那(?:把|局|个)|我这(?:把|局|个))",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
ACTIVE_THREAD_REPLY_HINT = re.compile(
|
||||
r"(?:还真|真是|确实|也是|那确实|这么|这也|也太|有点|太|胆子|敢|"
|
||||
r"离谱|逆天|抽象|难评|牛|绷|笑|哈哈|可以|行吧|草|艹|绝了|夸张)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
LOCAL_PLAY_STATUS_QUESTION = re.compile(
|
||||
r"^\s*(?P<subject>[\w\u4e00-\u9fff.-]{1,32})\s*"
|
||||
r"(?:(?:现在|当前|刚才|刚刚|还)?(?:在|正?在)?)?\s*"
|
||||
r"(?P<ask>玩(?:什么|啥|哪(?:个|款)?(?:游戏)?)?|"
|
||||
r"什么游戏|啥游戏|游戏状态|状态|咋样|怎么样|啥情况)"
|
||||
r"(?:吗|呢|了|呀|啊|?|\?)?\s*$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
LOCAL_PLAY_LIST_QUESTION = re.compile(
|
||||
r"^\s*(?:谁|哪个|哪些人|有人)\s*(?:在|正?在)?\s*"
|
||||
r"(?:玩|打|进行)\s*(?:什么|啥|游戏|21点|黑杰克|本地游戏)?"
|
||||
r"(?:吗|呢|呀|啊|?|\?)?\s*$",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def apply_private_context(value: Mapping[str, Any]) -> None:
|
||||
@@ -812,6 +834,60 @@ def context_terms(value: object) -> set[str]:
|
||||
return profile_fact_terms(text)
|
||||
|
||||
|
||||
def short_thread_reply(value: object) -> bool:
|
||||
text = normalize_text(value)
|
||||
if not text or local_command_kind(text):
|
||||
return False
|
||||
compact = re.sub(r"[\s,,。.!!??~~…]+", "", text)
|
||||
return (
|
||||
1 <= len(compact) <= ACTIVE_THREAD_SHORT_LIMIT
|
||||
and bool(ACTIVE_THREAD_REPLY_HINT.search(text))
|
||||
)
|
||||
|
||||
|
||||
def blackjack_hand_label(cards: object) -> str:
|
||||
if not isinstance(cards, list):
|
||||
return "未知手牌"
|
||||
normalized: list[int] = []
|
||||
for card in cards:
|
||||
try:
|
||||
normalized.append(int(card))
|
||||
except (TypeError, ValueError):
|
||||
return "未知手牌"
|
||||
if not normalized:
|
||||
return "未发牌"
|
||||
labels = " ".join(card_label(card) for card in normalized)
|
||||
return f"{labels}({hand_value(normalized)}点)"
|
||||
|
||||
|
||||
def visible_reply_text(value: object) -> str:
|
||||
text = str(value or "")
|
||||
parts = unpack_reply_parts(text)
|
||||
return "\n".join(parts) if parts is not None else text
|
||||
|
||||
|
||||
def thread_topic_label(route: str, reason: str) -> str:
|
||||
if "blackjack" in reason:
|
||||
return "21点"
|
||||
if "trivia" in reason:
|
||||
return "冷门问答"
|
||||
if "dice" in reason:
|
||||
return "骰子"
|
||||
if "coin" in reason:
|
||||
return "硬币"
|
||||
if "lottery" in reason:
|
||||
return "彩票"
|
||||
if "draw" in reason:
|
||||
return "抽奖"
|
||||
if "steam" in reason:
|
||||
return "Steam 状态"
|
||||
if route == "broadcast":
|
||||
return "Taffy 主动发言"
|
||||
if route == "chat":
|
||||
return "群聊"
|
||||
return reason.removeprefix("local_").replace("_", " ") or route
|
||||
|
||||
|
||||
def automatic_facts_are_near_duplicates(
|
||||
left: dict[str, Any],
|
||||
right: dict[str, Any],
|
||||
@@ -1335,6 +1411,20 @@ class Ledger:
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (group_fingerprint, game_key)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS active_threads (
|
||||
group_fingerprint TEXT NOT NULL,
|
||||
thread_key TEXT NOT NULL,
|
||||
topic TEXT NOT NULL,
|
||||
actor_person_key TEXT NOT NULL DEFAULT '',
|
||||
actor_display_name TEXT NOT NULL DEFAULT '',
|
||||
source_event_id TEXT NOT NULL DEFAULT '',
|
||||
source_kind TEXT NOT NULL DEFAULT '',
|
||||
summary TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
PRIMARY KEY (group_fingerprint, thread_key)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS person_observations (
|
||||
event_id TEXT PRIMARY KEY,
|
||||
group_fingerprint TEXT NOT NULL,
|
||||
@@ -1599,6 +1689,8 @@ class Ledger:
|
||||
ON outbox(state, next_attempt_at);
|
||||
CREATE INDEX IF NOT EXISTS direct_chat_outbox_due
|
||||
ON direct_chat_outbox(state, next_attempt_at);
|
||||
CREATE INDEX IF NOT EXISTS active_threads_group_expiry
|
||||
ON active_threads(group_fingerprint, expires_at);
|
||||
CREATE INDEX IF NOT EXISTS observations_scope_recent
|
||||
ON person_observations(group_fingerprint, person_key, observed_at);
|
||||
CREATE INDEX IF NOT EXISTS aliases_scope_match
|
||||
@@ -1842,8 +1934,7 @@ class Ledger:
|
||||
return cursor.rowcount == 1
|
||||
|
||||
def _record_taffy_context(self, event_id: str, response_text: str) -> None:
|
||||
parts = unpack_reply_parts(response_text)
|
||||
visible_text = "\n".join(parts) if parts is not None else response_text
|
||||
visible_text = visible_reply_text(response_text)
|
||||
if not visible_text:
|
||||
return
|
||||
with self.lock:
|
||||
@@ -1869,6 +1960,46 @@ class Ledger:
|
||||
replace=True,
|
||||
)
|
||||
|
||||
def _activate_thread_from_event(self, event_id: str, response_text: str) -> None:
|
||||
summary = bounded_text(visible_reply_text(response_text), limit=700)
|
||||
if not summary or summary == FAST_ACKNOWLEDGEMENT:
|
||||
return
|
||||
with self.lock:
|
||||
row = self.connection.execute(
|
||||
"""
|
||||
SELECT group_fingerprint, sender, sender_confidence, route,
|
||||
route_reason, status
|
||||
FROM events
|
||||
WHERE event_id = ?
|
||||
""",
|
||||
(event_id,),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
return
|
||||
reason = str(row["route_reason"])
|
||||
if str(row["route"]) == "local" and reason in LOCAL_CONTEXT_EXCLUDED_REASONS:
|
||||
return
|
||||
actor_person_key = ""
|
||||
actor_display_name = ""
|
||||
sender = str(row["sender"])
|
||||
sender_confidence = confidence_value(row["sender_confidence"], default=1.0)
|
||||
group = str(row["group_fingerprint"])
|
||||
if trusted_sender_value(sender, sender_confidence):
|
||||
person = self.resolve_person(group, sender)
|
||||
actor_person_key = str(person["person_key"])
|
||||
actor_display_name = person_display_label(person["display_name"])
|
||||
elif sender and sender != "unknown":
|
||||
actor_display_name = bounded_text(sender, limit=120)
|
||||
self.save_active_thread(
|
||||
group_fingerprint=group,
|
||||
topic=thread_topic_label(str(row["route"]), reason),
|
||||
summary=summary,
|
||||
actor_person_key=actor_person_key,
|
||||
actor_display_name=actor_display_name,
|
||||
source_event_id=event_id,
|
||||
source_kind=f"{row['route']}:{reason}",
|
||||
)
|
||||
|
||||
def set_event(
|
||||
self,
|
||||
event_id: str,
|
||||
@@ -1893,6 +2024,7 @@ class Ledger:
|
||||
)
|
||||
if response_text is not None and status in TRACKED_TAFFY_RESPONSE_STATUSES:
|
||||
self._record_taffy_context(event_id, response_text)
|
||||
self._activate_thread_from_event(event_id, response_text)
|
||||
|
||||
def mentioned_person_names(
|
||||
self,
|
||||
@@ -1947,7 +2079,9 @@ class Ledger:
|
||||
current_question = bounded_text(question, limit=MAX_EVENT_TEXT)
|
||||
question_terms = context_terms(current_question)
|
||||
followup = bool(CONTEXT_FOLLOWUP_HINT.search(current_question))
|
||||
thread_followup = followup or short_thread_reply(current_question)
|
||||
self_reference = bool(CONTEXT_SELF_REFERENCE.search(current_question))
|
||||
active_thread = self.active_thread(group) if thread_followup else None
|
||||
current_sender_trusted = trusted_sender_value(sender, sender_confidence)
|
||||
current_display = (
|
||||
str(self.resolve_person(group, sender)["display_name"])
|
||||
@@ -1997,8 +2131,22 @@ class Ledger:
|
||||
""",
|
||||
(group, min(80, CONTEXT_CANDIDATE_LIMIT // 3)),
|
||||
).fetchall()
|
||||
history_rows = self.connection.execute(
|
||||
"""
|
||||
SELECT event_id, display_name, text, observed_at,
|
||||
sender_confidence, body_confidence, source
|
||||
FROM context_history
|
||||
WHERE group_fingerprint = ?
|
||||
AND event_id != ?
|
||||
AND source = 'taffy_response'
|
||||
ORDER BY observed_at DESC, event_id DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(group, exclude_event_id, min(80, CONTEXT_CANDIDATE_LIMIT // 3)),
|
||||
).fetchall()
|
||||
candidates: list[dict[str, Any]] = []
|
||||
fallback_recent: list[dict[str, Any]] = []
|
||||
event_row_ids = {str(row["event_id"]) for row in rows}
|
||||
for index, row in enumerate(rows):
|
||||
if (
|
||||
row["route"] == "local"
|
||||
@@ -2046,6 +2194,12 @@ class Ledger:
|
||||
if overlap:
|
||||
score += min(40, overlap * 8)
|
||||
tags.append("term_overlap")
|
||||
if (
|
||||
active_thread is not None
|
||||
and str(active_thread.get("source_event_id")) == str(row["event_id"])
|
||||
):
|
||||
score += 140
|
||||
tags.append("active_thread")
|
||||
if str(row["route"]) == "local":
|
||||
score += 18
|
||||
tags.append("local_result")
|
||||
@@ -2086,6 +2240,13 @@ class Ledger:
|
||||
if overlap:
|
||||
score += min(30, overlap * 8)
|
||||
tags.append("term_overlap")
|
||||
if (
|
||||
active_thread is not None
|
||||
and str(active_thread.get("source_event_id"))
|
||||
== f"clock:{row['slot_key']}"
|
||||
):
|
||||
score += 140
|
||||
tags.append("active_thread")
|
||||
if followup:
|
||||
score += 8
|
||||
candidates.append(
|
||||
@@ -2105,6 +2266,96 @@ class Ledger:
|
||||
"tags": tuple(tags),
|
||||
}
|
||||
)
|
||||
if not self_reference:
|
||||
for index, row in enumerate(history_rows):
|
||||
history_event_id = str(row["event_id"])
|
||||
origin_event_id = (
|
||||
history_event_id.removeprefix("taffy:")
|
||||
if history_event_id.startswith("taffy:")
|
||||
else history_event_id
|
||||
)
|
||||
if origin_event_id in event_row_ids:
|
||||
continue
|
||||
text = str(row["text"])
|
||||
overlap = len(question_terms & context_terms(text))
|
||||
score = max(0, 20 - index)
|
||||
tags: list[str] = ["context_history", "taffy_response"]
|
||||
if overlap:
|
||||
score += min(30, overlap * 8)
|
||||
tags.append("term_overlap")
|
||||
if (
|
||||
active_thread is not None
|
||||
and str(active_thread.get("source_event_id")) in {
|
||||
history_event_id,
|
||||
origin_event_id,
|
||||
}
|
||||
):
|
||||
score += 140
|
||||
tags.append("active_thread")
|
||||
if followup:
|
||||
score += 8
|
||||
if score < 30 and "active_thread" not in tags:
|
||||
continue
|
||||
candidates.append(
|
||||
{
|
||||
"source_event_id": history_event_id,
|
||||
"speaker": (
|
||||
bounded_text(row["display_name"], limit=80) or "Taffy"
|
||||
),
|
||||
"speaker_trusted": True,
|
||||
"speaker_confidence": confidence_value(
|
||||
row["sender_confidence"],
|
||||
default=1.0,
|
||||
),
|
||||
"user_text": "",
|
||||
"taffy_text": text,
|
||||
"route": "history",
|
||||
"route_reason": str(row["source"]),
|
||||
"status": "recorded",
|
||||
"observed_at": str(row["observed_at"]),
|
||||
"sort_key": str(row["observed_at"]),
|
||||
"score": score,
|
||||
"tags": tuple(tags),
|
||||
}
|
||||
)
|
||||
active_source_event = (
|
||||
str(active_thread.get("source_event_id", ""))
|
||||
if active_thread is not None
|
||||
else ""
|
||||
)
|
||||
if (
|
||||
active_thread is not None
|
||||
and active_source_event
|
||||
and not self_reference
|
||||
and all(
|
||||
str(candidate["source_event_id"]) != active_source_event
|
||||
for candidate in candidates
|
||||
)
|
||||
):
|
||||
thread_tags = ["active_thread", "thread_summary"]
|
||||
source_kind = bounded_text(active_thread.get("source_kind"), limit=120)
|
||||
if source_kind:
|
||||
thread_tags.append(source_kind.replace(":", "_"))
|
||||
candidates.append(
|
||||
{
|
||||
"source_event_id": f"active:{active_source_event}",
|
||||
"speaker": (
|
||||
bounded_text(active_thread.get("actor_display_name"), limit=80)
|
||||
or "Taffy"
|
||||
),
|
||||
"speaker_trusted": True,
|
||||
"speaker_confidence": 1.0,
|
||||
"user_text": "",
|
||||
"taffy_text": str(active_thread.get("summary", "")),
|
||||
"route": "thread",
|
||||
"route_reason": str(active_thread.get("topic", "")),
|
||||
"status": "active",
|
||||
"observed_at": str(active_thread.get("updated_at", "")),
|
||||
"sort_key": str(active_thread.get("updated_at", "")),
|
||||
"score": 220,
|
||||
"tags": tuple(thread_tags),
|
||||
}
|
||||
)
|
||||
selected: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for entry in sorted(
|
||||
@@ -3521,6 +3772,97 @@ class Ledger:
|
||||
).fetchone()
|
||||
return str(row["value"]) if row else ""
|
||||
|
||||
def save_active_thread(
|
||||
self,
|
||||
*,
|
||||
group_fingerprint: str,
|
||||
topic: str,
|
||||
summary: str,
|
||||
actor_person_key: str = "",
|
||||
actor_display_name: str = "",
|
||||
source_event_id: str = "",
|
||||
source_kind: str = "",
|
||||
thread_key: str = "latest",
|
||||
ttl_seconds: int = ACTIVE_THREAD_TTL_SECONDS,
|
||||
) -> None:
|
||||
group = bounded_text(group_fingerprint, limit=256)
|
||||
thread = bounded_text(thread_key, limit=80) or "latest"
|
||||
text = bounded_text(summary, limit=700)
|
||||
if not group or not text:
|
||||
return
|
||||
now = datetime.now(timezone.utc)
|
||||
timestamp = now.isoformat(timespec="milliseconds")
|
||||
expires_at = (now + timedelta(seconds=ttl_seconds)).isoformat(
|
||||
timespec="milliseconds"
|
||||
)
|
||||
with self.lock, self.connection:
|
||||
self.connection.execute(
|
||||
"""
|
||||
INSERT INTO active_threads (
|
||||
group_fingerprint, thread_key, topic, actor_person_key,
|
||||
actor_display_name, source_event_id, source_kind, summary,
|
||||
created_at, updated_at, expires_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(group_fingerprint, thread_key) DO UPDATE
|
||||
SET topic = excluded.topic,
|
||||
actor_person_key = excluded.actor_person_key,
|
||||
actor_display_name = excluded.actor_display_name,
|
||||
source_event_id = excluded.source_event_id,
|
||||
source_kind = excluded.source_kind,
|
||||
summary = excluded.summary,
|
||||
updated_at = excluded.updated_at,
|
||||
expires_at = excluded.expires_at
|
||||
""",
|
||||
(
|
||||
group,
|
||||
thread,
|
||||
bounded_text(topic, limit=120),
|
||||
bounded_text(actor_person_key, limit=128),
|
||||
bounded_text(actor_display_name, limit=120),
|
||||
bounded_text(source_event_id, limit=256),
|
||||
bounded_text(source_kind, limit=120),
|
||||
text,
|
||||
timestamp,
|
||||
timestamp,
|
||||
expires_at,
|
||||
),
|
||||
)
|
||||
|
||||
def active_thread(
|
||||
self,
|
||||
group_fingerprint: str,
|
||||
*,
|
||||
thread_key: str = "latest",
|
||||
now: datetime | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
group = bounded_text(group_fingerprint, limit=256)
|
||||
thread = bounded_text(thread_key, limit=80) or "latest"
|
||||
if not group:
|
||||
return None
|
||||
current = now or datetime.now(timezone.utc)
|
||||
timestamp = current.astimezone(timezone.utc).isoformat(
|
||||
timespec="milliseconds"
|
||||
)
|
||||
with self.lock, self.connection:
|
||||
self.connection.execute(
|
||||
"""
|
||||
DELETE FROM active_threads
|
||||
WHERE group_fingerprint = ? AND expires_at <= ?
|
||||
""",
|
||||
(group, timestamp),
|
||||
)
|
||||
row = self.connection.execute(
|
||||
"""
|
||||
SELECT *
|
||||
FROM active_threads
|
||||
WHERE group_fingerprint = ? AND thread_key = ?
|
||||
AND expires_at > ?
|
||||
LIMIT 1
|
||||
""",
|
||||
(group, thread, timestamp),
|
||||
).fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
def recent_clock_broadcast(
|
||||
self,
|
||||
group_fingerprint: str,
|
||||
@@ -3775,6 +4117,14 @@ class Ledger:
|
||||
source=f"clock_broadcast:{kind[:48]}",
|
||||
replace=True,
|
||||
)
|
||||
self.save_active_thread(
|
||||
group_fingerprint=group_fingerprint,
|
||||
topic=thread_topic_label("broadcast", kind),
|
||||
summary=message,
|
||||
actor_display_name="Taffy",
|
||||
source_event_id=f"clock:{slot_key}",
|
||||
source_kind=f"clock_broadcast:{kind[:48]}",
|
||||
)
|
||||
return True
|
||||
|
||||
def fail_clock_broadcast(
|
||||
@@ -4248,6 +4598,230 @@ class Ledger:
|
||||
),
|
||||
)
|
||||
|
||||
def _local_status_subject(
|
||||
self,
|
||||
group: str,
|
||||
subject: str,
|
||||
*,
|
||||
sender: str = "",
|
||||
sender_confidence: float = 1.0,
|
||||
) -> dict[str, str] | None:
|
||||
normalized = normalize_text(subject)
|
||||
if not normalized:
|
||||
return None
|
||||
if normalized in {"我", "本人", "自己"}:
|
||||
if not trusted_sender_value(sender, sender_confidence):
|
||||
return None
|
||||
person = self.resolve_person(group, sender)
|
||||
return {
|
||||
"person_key": str(person["person_key"]),
|
||||
"display_name": person_display_label(person["display_name"]),
|
||||
}
|
||||
if normalized.casefold() in {"你", "taffy", "jwebot", BOT_MENTION.casefold()}:
|
||||
return None
|
||||
self.seed_person_aliases(group)
|
||||
subject_key = person_alias_key(normalized)
|
||||
with self.lock:
|
||||
rows = self.connection.execute(
|
||||
"""
|
||||
SELECT alias_key, match_kind, canonical_name
|
||||
FROM person_aliases
|
||||
WHERE group_fingerprint = ?
|
||||
ORDER BY length(alias_key) DESC, match_kind
|
||||
""",
|
||||
(group,),
|
||||
).fetchall()
|
||||
for row in rows:
|
||||
if not alias_is_mentioned(
|
||||
subject_key,
|
||||
str(row["alias_key"]),
|
||||
str(row["match_kind"]),
|
||||
):
|
||||
continue
|
||||
canonical = str(row["canonical_name"])
|
||||
return {
|
||||
"person_key": person_scope_key(group, canonical),
|
||||
"display_name": person_display_label(canonical),
|
||||
}
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _active_single_blackjack_game(account: dict[str, Any]) -> dict[str, Any] | None:
|
||||
game = account.get("game")
|
||||
if not isinstance(game, dict):
|
||||
return None
|
||||
try:
|
||||
updated_at = float(game.get("updated_at", 0))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if time.time() - updated_at >= LOCAL_GAME_TIMEOUT_SECONDS:
|
||||
return None
|
||||
return game
|
||||
|
||||
def _single_blackjack_status(
|
||||
self,
|
||||
group: str,
|
||||
person_key: str,
|
||||
display_name: str,
|
||||
) -> str | None:
|
||||
with self.lock:
|
||||
account = self._load_player_account(group, person_key)
|
||||
game = self._active_single_blackjack_game(account)
|
||||
if game is None:
|
||||
return None
|
||||
dealer = game.get("dealer", [])
|
||||
dealer_text = (
|
||||
f"{card_label(int(dealer[0]))} + 暗牌"
|
||||
if isinstance(dealer, list) and dealer
|
||||
else "未知"
|
||||
)
|
||||
try:
|
||||
index = int(game.get("index", 0))
|
||||
bet = int(game.get("bet", 0))
|
||||
except (TypeError, ValueError):
|
||||
index = 0
|
||||
bet = 0
|
||||
suffix = f" #{index}" if index > 0 else ""
|
||||
return (
|
||||
f"{display_name} 在玩单人21点{suffix}:"
|
||||
f"手牌 {blackjack_hand_label(game.get('player'))};"
|
||||
f"庄家 {dealer_text};下注 {bet};"
|
||||
"可 /21点 要牌 或 /21点 停牌"
|
||||
)
|
||||
|
||||
def _multiplayer_blackjack_status(
|
||||
self,
|
||||
group: str,
|
||||
person_key: str,
|
||||
display_name: str,
|
||||
) -> str | None:
|
||||
with self.lock:
|
||||
table = self._open_group_blackjack(group)
|
||||
if table is None:
|
||||
return None
|
||||
players = self._group_blackjack_players(group, int(table["table_number"]))
|
||||
player = self._blackjack_entry(players, person_key)
|
||||
if player is None:
|
||||
return None
|
||||
state = str(table["state"])
|
||||
table_number = int(table["table_number"])
|
||||
prompt = self._blackjack_entry(players, str(table["turn_person_key"]))
|
||||
prompt_text = f";当前轮到 {prompt['display_name']}" if prompt else ""
|
||||
if state == "joining":
|
||||
names = "、".join(str(item["display_name"]) for item in players)
|
||||
return (
|
||||
f"{display_name} 在多人21点第 {table_number} 桌,正在等人加入;"
|
||||
f"当前玩家:{names}"
|
||||
)
|
||||
if state == "betting":
|
||||
bet = int(player["bet"])
|
||||
bet_text = f"已放 {bet}" if bet > 0 else "还没放筹码"
|
||||
return (
|
||||
f"{display_name} 在多人21点第 {table_number} 桌,下注阶段;"
|
||||
f"{display_name} {bet_text}{prompt_text}"
|
||||
)
|
||||
if state == "playing":
|
||||
return (
|
||||
f"{display_name} 在多人21点第 {table_number} 桌,出牌阶段;"
|
||||
f"手牌 {blackjack_hand_label(player.get('hand'))};"
|
||||
f"下注 {int(player['bet'])};状态 {player['state']}{prompt_text}"
|
||||
)
|
||||
return None
|
||||
|
||||
def _active_local_game_lines(self, group: str) -> list[str]:
|
||||
directory = {
|
||||
str(person["person_key"]): str(person["display_name"])
|
||||
for person in self.person_directory(group)
|
||||
}
|
||||
lines: list[str] = []
|
||||
with self.lock:
|
||||
account_rows = self.connection.execute(
|
||||
"""
|
||||
SELECT person_key, state_json
|
||||
FROM player_accounts
|
||||
WHERE group_fingerprint = ?
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 80
|
||||
""",
|
||||
(group,),
|
||||
).fetchall()
|
||||
table = self._open_group_blackjack(group)
|
||||
players = (
|
||||
self._group_blackjack_players(group, int(table["table_number"]))
|
||||
if table is not None
|
||||
else []
|
||||
)
|
||||
for row in account_rows:
|
||||
try:
|
||||
account = normalize_player_account(
|
||||
json.loads(str(row["state_json"]))
|
||||
)
|
||||
except (json.JSONDecodeError, TypeError, ValueError):
|
||||
continue
|
||||
game = self._active_single_blackjack_game(account)
|
||||
if game is None:
|
||||
continue
|
||||
person_key = str(row["person_key"])
|
||||
display_name = directory.get(person_key, "有人")
|
||||
index = int(game.get("index", 0) or 0)
|
||||
index_text = f" #{index}" if index > 0 else ""
|
||||
lines.append(
|
||||
f"{display_name} 单人21点{index_text},"
|
||||
f"手牌 {blackjack_hand_label(game.get('player'))}"
|
||||
)
|
||||
if table is not None and players:
|
||||
state = str(table["state"])
|
||||
state_text = {
|
||||
"joining": "等人加入",
|
||||
"betting": "下注阶段",
|
||||
"playing": "出牌阶段",
|
||||
}.get(state, state)
|
||||
names = "、".join(str(player["display_name"]) for player in players)
|
||||
lines.append(
|
||||
f"多人21点第 {int(table['table_number'])} 桌{state_text},"
|
||||
f"玩家:{names}"
|
||||
)
|
||||
return lines
|
||||
|
||||
def local_game_status_reply(
|
||||
self,
|
||||
group: str,
|
||||
question: str,
|
||||
*,
|
||||
sender: str = "",
|
||||
sender_confidence: float = 1.0,
|
||||
) -> str | None:
|
||||
current = bounded_text(question, limit=MAX_EVENT_TEXT)
|
||||
if LOCAL_PLAY_LIST_QUESTION.fullmatch(current):
|
||||
lines = self._active_local_game_lines(group)
|
||||
if not lines:
|
||||
return None
|
||||
return clean_reply("现在本地游戏里:" + ";".join(lines))
|
||||
match = LOCAL_PLAY_STATUS_QUESTION.fullmatch(current)
|
||||
if match is None:
|
||||
return None
|
||||
person = self._local_status_subject(
|
||||
group,
|
||||
match.group("subject"),
|
||||
sender=sender,
|
||||
sender_confidence=sender_confidence,
|
||||
)
|
||||
if person is None:
|
||||
return None
|
||||
person_key = str(person["person_key"])
|
||||
display_name = str(person["display_name"])
|
||||
lines = [
|
||||
line
|
||||
for line in (
|
||||
self._single_blackjack_status(group, person_key, display_name),
|
||||
self._multiplayer_blackjack_status(group, person_key, display_name),
|
||||
)
|
||||
if line
|
||||
]
|
||||
if not lines:
|
||||
return None
|
||||
return clean_reply(";".join(lines))
|
||||
|
||||
def _store_player_action(
|
||||
self,
|
||||
group: str,
|
||||
@@ -7714,7 +8288,10 @@ class DeepSeekClient:
|
||||
"STRUCTURED_RECENT_GROUP_CONTEXT 是多人群聊记录,speaker 不同就不是"
|
||||
"同一个人;遇到“我刚才、我的、我上一把”这类自指问题,只引用"
|
||||
"CURRENT_GROUP_SENDER_LABEL 对应的上下文。点名别人时才引用被点名者。"
|
||||
"上下文不足或指代不清时,短句追问“你说哪件?”"
|
||||
"标记为 local_result 或 active_thread 的内容是本地账本或当前线程事实,"
|
||||
"必须优先采用,不要再回答“不知道”“去问他”或给出相反结论。"
|
||||
"用户只发短句评价时,优先接 active_thread 继续聊。"
|
||||
"只有多个候选明显冲突且无法判断时,才短句追问“你说哪件?”"
|
||||
),
|
||||
}
|
||||
]
|
||||
@@ -8760,6 +9337,37 @@ class EdgeApplication:
|
||||
if parts is not None:
|
||||
response["texts"] = parts
|
||||
return response
|
||||
local_fact_reply = self.ledger.local_game_status_reply(
|
||||
str(row["group_fingerprint"]),
|
||||
str(row["question"]),
|
||||
sender=str(row["sender"]),
|
||||
sender_confidence=confidence_value(
|
||||
row.get("sender_confidence"),
|
||||
default=1.0,
|
||||
),
|
||||
)
|
||||
if local_fact_reply:
|
||||
self.ledger.set_event(
|
||||
str(row["event_id"]),
|
||||
status="chat_completed",
|
||||
response_text=local_fact_reply,
|
||||
)
|
||||
self.telemetry.elapsed(
|
||||
"edge.local_fact.complete",
|
||||
started,
|
||||
event_id=event_id,
|
||||
group_id=group_id,
|
||||
route=route,
|
||||
reason="local_game_status",
|
||||
execution_path=ExecutionPath.LOCAL_COMMAND.value,
|
||||
chars=len(local_fact_reply),
|
||||
)
|
||||
return {
|
||||
"status": "reply",
|
||||
"route": route,
|
||||
"execution_path": ExecutionPath.LOCAL_COMMAND.value,
|
||||
"text": local_fact_reply,
|
||||
}
|
||||
alias_reply = self.ledger.local_alias_identity_reply(
|
||||
str(row["group_fingerprint"]),
|
||||
str(row["question"]),
|
||||
|
||||
Reference in New Issue
Block a user