From 836ee8ac9d3b373bfc65d1afbbae7f1eef17b326 Mon Sep 17 00:00:00 2001 From: ViperEkura <3081035982@qq.com> Date: Sat, 28 Mar 2026 11:52:05 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=89=8D=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E5=90=8C=E6=AD=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routes/stats.py | 55 ++- backend/tools/__init__.py | 12 +- backend/tools/executor.py | 4 - backend/tools/factory.py | 27 -- backend/utils/__init__.py | 6 +- frontend/package-lock.json | 19 + frontend/package.json | 23 +- frontend/src/App.vue | 29 +- frontend/src/components/ChatView.vue | 29 +- frontend/src/components/SettingsPanel.vue | 74 ++-- frontend/src/components/StatsPanel.vue | 427 +++++++++------------- 11 files changed, 334 insertions(+), 371 deletions(-) diff --git a/backend/routes/stats.py b/backend/routes/stats.py index 4bedc6d..832dc7c 100644 --- a/backend/routes/stats.py +++ b/backend/routes/stats.py @@ -1,29 +1,38 @@ """Token statistics API routes""" -from datetime import date, timedelta +from datetime import date, timedelta, datetime, timezone from flask import Blueprint, request, g -from sqlalchemy import func -from backend.models import TokenUsage +from sqlalchemy import func, extract +from backend.models import TokenUsage, Message, Conversation from backend.utils.helpers import ok, err +from backend import db bp = Blueprint("stats", __name__) +def _utc_today(): + """Get today's date in UTC to match stored timestamps.""" + return datetime.now(timezone.utc).date() + + @bp.route("/api/stats/tokens", methods=["GET"]) def token_stats(): """Get token usage statistics""" user = g.current_user period = request.args.get("period", "daily") - - today = date.today() - + + today = _utc_today() + if period == "daily": stats = TokenUsage.query.filter_by(user_id=user.id, date=today).all() + # Hourly breakdown from Message table + hourly = _build_hourly_stats(user.id, today) result = { "period": "daily", "date": today.isoformat(), "prompt_tokens": sum(s.prompt_tokens for s in stats), "completion_tokens": sum(s.completion_tokens for s in stats), "total_tokens": sum(s.total_tokens for s in stats), + "hourly": hourly, "by_model": { s.model: { "prompt": s.prompt_tokens, @@ -92,3 +101,37 @@ def _build_period_result(stats, period, start_date, end_date, days): "daily": daily_data, "by_model": by_model, } + + +def _build_hourly_stats(user_id, day): + """Build hourly token breakdown for a given day (UTC) from Message table.""" + day_start = datetime.combine(day, datetime.min.time()).replace(tzinfo=timezone.utc) + day_end = datetime.combine(day, datetime.max.time()).replace(tzinfo=timezone.utc) + + conv_ids = ( + db.session.query(Conversation.id) + .filter(Conversation.user_id == user_id) + .subquery() + ) + + rows = ( + db.session.query( + extract("hour", Message.created_at).label("hour"), + func.sum(Message.token_count).label("total"), + ) + .filter( + Message.conversation_id.in_(conv_ids), + Message.role == "assistant", + Message.created_at >= day_start, + Message.created_at <= day_end, + ) + .group_by(extract("hour", Message.created_at)) + .all() + ) + + hourly = {} + for r in rows: + h = int(r.hour) + hourly[str(h)] = {"total": r.total or 0} + + return hourly diff --git a/backend/tools/__init__.py b/backend/tools/__init__.py index 1c3d449..6b436d6 100644 --- a/backend/tools/__init__.py +++ b/backend/tools/__init__.py @@ -15,8 +15,8 @@ Usage: result = registry.execute("web_search", {"query": "Python"}) """ -from backend.tools.core import ToolDefinition, ToolResult, ToolRegistry, registry -from backend.tools.factory import tool, register_tool +from backend.tools.core import registry +from backend.tools.factory import tool from backend.tools.executor import ToolExecutor @@ -47,16 +47,12 @@ def init_tools() -> None: # Public API exports __all__ = [ - # Core classes - "ToolDefinition", - "ToolResult", - "ToolRegistry", - "ToolExecutor", # Instances "registry", # Factory functions "tool", - "register_tool", + # Classes + "ToolExecutor", # Initialization "init_tools", # Service locator diff --git a/backend/tools/executor.py b/backend/tools/executor.py index c6b4a3d..6cdb9b8 100644 --- a/backend/tools/executor.py +++ b/backend/tools/executor.py @@ -51,10 +51,6 @@ class ToolExecutor: return record["result"] return None - def clear_history(self) -> None: - """Clear call history (call this at start of new conversation turn)""" - self._call_history.clear() - @staticmethod def _inject_context(name: str, args: dict, context: Optional[dict]) -> None: """Inject context fields into tool arguments in-place. diff --git a/backend/tools/factory.py b/backend/tools/factory.py index 1047dc7..0b40a7b 100644 --- a/backend/tools/factory.py +++ b/backend/tools/factory.py @@ -34,30 +34,3 @@ def tool( return func return decorator - -def register_tool( - name: str, - handler: Callable, - description: str, - parameters: dict, - category: str = "general" -) -> None: - """ - Register a tool directly (without decorator) - - Usage: - register_tool( - name="my_tool", - handler=my_function, - description="Description", - parameters={...} - ) - """ - tool_def = ToolDefinition( - name=name, - description=description, - parameters=parameters, - handler=handler, - category=category - ) - registry.register(tool_def) diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py index 8d481fc..38abf8d 100644 --- a/backend/utils/__init__.py +++ b/backend/utils/__init__.py @@ -1,11 +1,11 @@ """Backend utilities""" -from backend.utils.helpers import ok, err, to_dict, get_or_create_default_user, record_token_usage, build_messages +from backend.utils.helpers import ok, err, to_dict, message_to_dict, record_token_usage, build_messages __all__ = [ "ok", - "err", + "err", "to_dict", - "get_or_create_default_user", + "message_to_dict", "record_token_usage", "build_messages", ] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1e07102..638a64b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,6 +22,7 @@ "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.2", "@codemirror/theme-one-dark": "^6.1.2", + "chart.js": "^4.5.1", "codemirror": "^6.0.1", "highlight.js": "^11.11.1", "katex": "^0.16.40", @@ -791,6 +792,12 @@ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmmirror.com/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@lezer/common": { "version": "1.5.1", "resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.5.1.tgz", @@ -1430,6 +1437,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/codemirror": { "version": "6.0.2", "resolved": "https://registry.npmmirror.com/codemirror/-/codemirror-6.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8c93b8e..a7d6c36 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,21 +9,22 @@ "preview": "vite preview" }, "dependencies": { - "codemirror": "^6.0.1", - "@codemirror/theme-one-dark": "^6.1.2", - "@codemirror/lang-markdown": "^6.3.2", - "@codemirror/lang-javascript": "^6.2.3", - "@codemirror/lang-python": "^6.1.7", - "@codemirror/lang-html": "^6.4.9", - "@codemirror/lang-css": "^6.3.1", - "@codemirror/lang-json": "^6.0.1", - "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/lang-java": "^6.0.1", "@codemirror/lang-cpp": "^6.0.2", - "@codemirror/lang-rust": "^6.0.1", + "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-go": "^6.0.1", + "@codemirror/lang-html": "^6.4.9", + "@codemirror/lang-java": "^6.0.1", + "@codemirror/lang-javascript": "^6.2.3", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/lang-markdown": "^6.3.2", + "@codemirror/lang-python": "^6.1.7", + "@codemirror/lang-rust": "^6.0.1", "@codemirror/lang-sql": "^6.8.0", "@codemirror/lang-xml": "^6.1.0", + "@codemirror/lang-yaml": "^6.1.2", + "@codemirror/theme-one-dark": "^6.1.2", + "chart.js": "^4.5.1", + "codemirror": "^6.0.1", "highlight.js": "^11.11.1", "katex": "^0.16.40", "marked": "^15.0.12", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 483b174..efc4ccd 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -42,6 +42,7 @@ :messages="messages" :streaming="streaming" :streaming-process-steps="streamProcessSteps" + :model-name-map="modelNameMap" :has-more-messages="hasMoreMessages" :loading-more="loadingMessages" :tools-enabled="toolsEnabled" @@ -59,8 +60,11 @@