Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/coding/proxy/logging/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,20 @@ async def set_session_title(self, session_key: str, title: str) -> None:
)
await self._db.commit()

async def update_empty_session_title(self, session_key: str, title: str) -> None:
"""为标题为空的 session 补写标题(幂等,仅覆盖空标题行).

使用 ``AND title = ''`` 条件确保不覆盖已有标题,
即使行已存在但标题为空也会被更新。
"""
if not self._db or not title or not session_key:
return
await self._db.execute(
"UPDATE session_meta SET title = ? WHERE session_key = ? AND title = ''",
(title, session_key),
)
await self._db.commit()

async def get_session_titles(self, session_keys: list[str]) -> dict[str, str]:
"""批量查询 session 标题."""
if not self._db or not session_keys:
Expand Down
92 changes: 84 additions & 8 deletions src/coding/proxy/routing/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@
CompatibilityStatus,
build_canonical_request,
)
from ..model.compat import CanonicalRequest
from ..model.compat import CanonicalMessagePart, CanonicalRequest

logger = logging.getLogger(__name__)

_SESSION_TITLE_MAX_LEN = 600
# 回退标题截取长度 — 工具结果等非用户直接输入的摘要上限。
_FALLBACK_TITLE_MAX_LEN = 80

# Claude Code 注入的"噪声"标签 — 系统级上下文,不应进入 Session 标题。
# 这些标签由 CC harness 在首个 user 消息 content 中拼接,高度同质,
Expand All @@ -63,7 +65,8 @@
r"<(?P<tag>system-reminder|user-preferences|"
r"local-command-stdout|local-command-stderr|"
r"bash-input|bash-stdout|bash-stderr|"
r"ide_selection|stdin|system_instruction|session)\b[^>]*>"
r"ide_selection|stdin|system_instruction|session|"
r"artifactMetadata|thinking)\b[^>]*>"
r".*?</(?P=tag)>",
flags=re.DOTALL | re.IGNORECASE,
)
Expand Down Expand Up @@ -109,13 +112,19 @@ def _sanitize_user_text(raw: str) -> str:
return re.sub(r"\s+", " ", cleaned).strip()


def _extract_session_title(request: CanonicalRequest) -> str:
"""从规范化请求中提取首个用户消息文本作为 session 标题。
# ── Session 标题提取: 多层级回退策略 ──────────────────────────────
#
# Level 1: user TEXT → 噪声剥离 → 首条非空文本 (原有逻辑)
# Level 2: user TOOL_RESULT → text 截取 → "[Tool output] <snippet>"
# Level 3: user IMAGE → 计数 → "[1 Image]" / "[N Images]"
# Level 4: 请求元数据 → tool_names / model → "[Tool call] Bash, Read"
# / "[Session] claude-opus-4-8"
# ─────────────────────────────────────────────────────────────────

跳过 Claude Code 注入的系统级 XML 块(system-reminder、user-preferences 等),
确保标题反映用户真实输入而非高同质化的系统模板。
"""
for part in request.messages:

def _extract_title_from_user_text(messages: list[CanonicalMessagePart]) -> str:
"""Level 1: 从 user TEXT 部分提取经噪声剥离后的首条非空文本."""
for part in messages:
if part.role != "user" or part.type != CanonicalPartType.TEXT:
continue
cleaned = _sanitize_user_text(part.text)
Expand All @@ -124,6 +133,59 @@ def _extract_session_title(request: CanonicalRequest) -> str:
return ""


def _extract_title_from_tool_results(messages: list[CanonicalMessagePart]) -> str:
"""Level 2: 从 user TOOL_RESULT 部分截取文本摘要."""
for part in messages:
if part.role != "user" or part.type != CanonicalPartType.TOOL_RESULT:
continue
if not part.text:
continue
cleaned = _sanitize_user_text(part.text)
if cleaned:
snippet = cleaned[:_FALLBACK_TITLE_MAX_LEN]
return f"[Tool output] {snippet}"
return ""


def _extract_title_from_images(messages: list[CanonicalMessagePart]) -> str:
"""Level 3: 统计 user IMAGE 部分数量,生成图片描述标题."""
count = sum(
1 for p in messages if p.role == "user" and p.type == CanonicalPartType.IMAGE
)
if count == 0:
return ""
return f"[{count} Image{'s' if count > 1 else ''}]"


def _extract_title_from_metadata(request: CanonicalRequest) -> str:
"""Level 4: 从请求元数据 (tool_names / model) 合成兜底标题."""
if request.tool_names:
names = ", ".join(request.tool_names[:3])
return f"[Tool call] {names}"
if request.model:
return f"[Session] {request.model}"
return ""


def _extract_session_title(request: CanonicalRequest) -> str:
"""从规范化请求中提取 session 标题 — 多层级回退策略。

依次尝试: user TEXT 噪声剥离 → TOOL_RESULT 摘要 → IMAGE 计数 → 元数据兜底。
任意级别命中即返回,确保 Dashboard 尽可能展示有辨识度的标题。
"""
messages = request.messages
for extractor in (
_extract_title_from_user_text,
_extract_title_from_tool_results,
_extract_title_from_images,
):
title = extractor(messages)
if title:
return title[:_SESSION_TITLE_MAX_LEN]
# Level 4 依赖 request 元数据,签名不同
return _extract_title_from_metadata(request)[:_SESSION_TITLE_MAX_LEN]


def _build_semantic_rejection_diagnostic(body: dict[str, Any]) -> str:
"""构建语义拒绝的请求体诊断上下文.

Expand Down Expand Up @@ -663,6 +725,13 @@ async def execute_stream(
await self._recorder.set_session_title(
canonical_request.session_key, title
)
else:
# 延迟标题补写: 若 session 尚无标题,尝试从当前请求中提取并回写。
title = _extract_session_title(canonical_request)
if title:
await self._recorder.update_empty_session_title(
canonical_request.session_key, title
)
incompatible_reasons: list[str] = []
effective_tiers = self._resolve_effective_tiers(canonical_request.session_key)
last_idx = len(effective_tiers) - 1
Expand Down Expand Up @@ -842,6 +911,13 @@ async def execute_message(
await self._recorder.set_session_title(
canonical_request.session_key, title
)
else:
# 延迟标题补写: 若 session 尚无标题,尝试从当前请求中提取并回写。
title = _extract_session_title(canonical_request)
if title:
await self._recorder.update_empty_session_title(
canonical_request.session_key, title
)
incompatible_reasons: list[str] = []
effective_tiers = self._resolve_effective_tiers(canonical_request.session_key)
last_idx = len(effective_tiers) - 1
Expand Down
5 changes: 5 additions & 0 deletions src/coding/proxy/routing/usage_recorder.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ async def set_session_title(self, session_key: str, title: str) -> None:
if self._token_logger:
await self._token_logger.set_session_title(session_key, title)

async def update_empty_session_title(self, session_key: str, title: str) -> None:
"""为标题为空的 session 补写标题(委托给 TokenLogger)."""
if self._token_logger:
await self._token_logger.update_empty_session_title(session_key, title)

# ── 用量信息构建 ──────────────────────────────────────

@staticmethod
Expand Down
Loading
Loading