Commit 27ac512a authored by Yaowentong's avatar Yaowentong

后台健全

parent fc2c7163
...@@ -5,7 +5,6 @@ from __future__ import annotations ...@@ -5,7 +5,6 @@ from __future__ import annotations
import json import json
import os import os
import sys import sys
import traceback
import uuid import uuid
from datetime import datetime from datetime import datetime
import time import time
...@@ -15,7 +14,6 @@ from loguru import logger ...@@ -15,7 +14,6 @@ from loguru import logger
from openpyxl import Workbook from openpyxl import Workbook
from aidso_geo.models import spider_save_tos from aidso_geo.models import spider_save_tos
from aidso_geo.utils.tos_utils import get_string_from_tos
POLL_SECONDS = 120 POLL_SECONDS = 120
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.append(BASE_DIR) sys.path.append(BASE_DIR)
...@@ -487,7 +485,6 @@ def doubao_process_original_data(file_path, original_content): ...@@ -487,7 +485,6 @@ def doubao_process_original_data(file_path, original_content):
is_think = False is_think = False
rich_media_block = [] rich_media_block = []
think_bool = False think_bool = False
response_bool = False
file_path_result = os.path.dirname(file_path) file_path_result = os.path.dirname(file_path)
content_list = original_content.split("\n") content_list = original_content.split("\n")
...@@ -701,79 +698,6 @@ def wait_mt_tasks_done( ...@@ -701,79 +698,6 @@ def wait_mt_tasks_done(
time.sleep(POLL_SECONDS) time.sleep(POLL_SECONDS)
def get_task_content_with_retries(
row: dict,
file: str,
redis_key: str,
max_retries: int = 3,
):
req_id = row.get("reqId")
redis_pt = normalize_pt(row.get("pt"))[:8]
last_error = None
for retry_index in range(max_retries + 1):
try:
raw = tos_utils.get_string_from_tos(file)
except Exception as exc:
last_error = exc
raw = None
if raw:
try:
content = json.loads(raw).get("content")
except (TypeError, json.JSONDecodeError) as exc:
last_error = exc
content = None
if content:
return raw, content
if retry_index >= max_retries:
break
retry_value = json.dumps(
{
"reqId": req_id,
"prompt": row.get("prompt"),
"pt": redis_pt,
},
ensure_ascii=False,
)
redis_client = init_redis()
if redis_client is None:
raise TaskResultUnavailableError("Redis 初始化失败")
try:
redis_client.rpush(redis_key, retry_value)
except Exception as exc:
raise TaskResultUnavailableError(
f"TOS 结果缺失任务重发 Redis 失败: "
f"key={redis_key}, reqId={req_id}"
) from exc
logger.warning(
f"[pt={row.get('pt')} count={row.get('count')} "
f"reqId={req_id}] TOS 结果缺失或正文为空,"
f"开始第 {retry_index + 1}/{max_retries} 次重试"
)
try:
wait_mt_tasks_done(
redis_key,
pt=str(row.get("pt") or ""),
count_label=str(row.get("count") or ""),
)
except Exception as exc:
raise TaskResultUnavailableError(
f"TOS 结果缺失任务等待 Redis 失败: "
f"key={redis_key}, reqId={req_id}"
) from exc
time.sleep(60)
error = TaskResultUnavailableError(
f"TOS 结果重试 {max_retries} 次后仍不可用: "
f"reqId={req_id}, file={file}"
)
if last_error is not None:
raise error from last_error
raise error
def process_rounds_to_result( def process_rounds_to_result(
pt: str, pt: str,
start_count: int, start_count: int,
...@@ -941,10 +865,25 @@ def process_rounds_to_result( ...@@ -941,10 +865,25 @@ def process_rounds_to_result(
else: else:
keywords = [] keywords = []
raw, content = get_task_content_with_retries( try:
row=row, raw = tos_utils.get_string_from_tos(file)
file=file, except Exception as exc:
redis_key=redis_key, raise TaskResultUnavailableError(
f"TOS 结果读取失败: reqId={req_id}, file={file}"
) from exc
if not raw:
raise TaskResultUnavailableError(
f"TOS 结果不存在: reqId={req_id}, file={file}"
)
try:
content = json.loads(raw).get("content")
except (TypeError, json.JSONDecodeError) as exc:
raise TaskResultUnavailableError(
f"TOS 结果格式异常: reqId={req_id}, file={file}"
) from exc
if not content:
raise TaskResultUnavailableError(
f"TOS 正文为空: reqId={req_id}, file={file}"
) )
( (
...@@ -998,16 +937,15 @@ def process_rounds_to_result( ...@@ -998,16 +937,15 @@ def process_rounds_to_result(
f"[pt={pt} count={start_count}-{end_count}] " f"[pt={pt} count={start_count}-{end_count}] "
f"处理 {file} 失败: {exc}" f"处理 {file} 失败: {exc}"
) )
if isinstance( if isinstance(exc, TaskResultUnavailableError):
exc, raise
(ResultWriteError, TaskResultUnavailableError), if isinstance(exc, ResultWriteError):
):
raise raise
return None return None
def parse_result_batch(rows, batch_label): def parse_result_batch(rows, batch_label):
if not rows: if not rows:
return [] return [], []
worker_count = min(20, len(rows)) worker_count = min(20, len(rows))
logger.info( logger.info(
f"[pt={pt} count={start_count}-{end_count}] " f"[pt={pt} count={start_count}-{end_count}] "
...@@ -1015,6 +953,8 @@ def process_rounds_to_result( ...@@ -1015,6 +953,8 @@ def process_rounds_to_result(
f"workers={worker_count}, tasks={len(rows)}" f"workers={worker_count}, tasks={len(rows)}"
) )
parsed_results = [] parsed_results = []
missing_rows = []
completed_count = 0
with ThreadPoolExecutor(max_workers=worker_count) as executor: with ThreadPoolExecutor(max_workers=worker_count) as executor:
future_to_row = { future_to_row = {
executor.submit(parse_single_result, row): row executor.submit(parse_single_result, row): row
...@@ -1024,6 +964,9 @@ def process_rounds_to_result( ...@@ -1024,6 +964,9 @@ def process_rounds_to_result(
row = future_to_row[future] row = future_to_row[future]
try: try:
result_item = future.result() result_item = future.result()
except TaskResultUnavailableError:
missing_rows.append(row)
result_item = None
except Exception as exc: except Exception as exc:
logger.error( logger.error(
f"[pt={row.get('pt')} " f"[pt={row.get('pt')} "
...@@ -1034,39 +977,173 @@ def process_rounds_to_result( ...@@ -1034,39 +977,173 @@ def process_rounds_to_result(
raise raise
if result_item is not None: if result_item is not None:
parsed_results.append((row, result_item)) parsed_results.append((row, result_item))
return parsed_results completed_count += 1
if (
completed_count % 500 == 0
or completed_count == len(rows)
):
logger.info(
f"[pt={pt} count={start_count}-{end_count}] "
f"并发解析进度: batch={batch_label}, "
f"completed={completed_count}/{len(rows)}, "
f"success={len(parsed_results)}, "
f"missing={len(missing_rows)}"
)
return parsed_results, missing_rows
def parse_rows_with_tos_retries(rows, batch_label):
parsed_results, missing_rows = parse_result_batch(
rows,
batch_label,
)
all_parsed_results = list(parsed_results)
for tos_retry_round in range(1, 4):
if not missing_rows:
break
retry_values = [
json.dumps(
{
"reqId": row.get("reqId"),
"prompt": row.get("prompt"),
"pt": redis_pt,
},
ensure_ascii=False,
)
for row in missing_rows
]
redis_client = init_redis()
if redis_client is None:
raise TaskResultUnavailableError("Redis 初始化失败")
try:
redis_client.rpush(redis_key, *retry_values)
except Exception as exc:
raise TaskResultUnavailableError(
f"TOS 缺失任务批量重发 Redis 失败: "
f"key={redis_key}, "
f"retry_round={tos_retry_round}"
) from exc
logger.warning(
f"[pt={pt} count={start_count}-{end_count}] "
f"TOS 结果缺失,开始第 {tos_retry_round}/3 "
f"轮批量重试: rows={len(retry_values)}"
)
wait_mt_tasks_done(
redis_key,
pt=pt,
count_label=(
f"{start_count}-{end_count} "
f"tos_retry={tos_retry_round}/3"
),
)
time.sleep(60)
retry_parsed_results, missing_rows = parse_result_batch(
missing_rows,
(
f"{batch_label} "
f"tos_retry={tos_retry_round}/3"
),
)
all_parsed_results.extend(retry_parsed_results)
if missing_rows:
logger.error(
f"[pt={pt} count={start_count}-{end_count}] "
f"TOS 结果批量重试 3 次后仍缺失: "
f"rows={len(missing_rows)}, "
f"reqIds={[row.get('reqId') for row in missing_rows]}"
)
return all_parsed_results
def insert_result_items(result_items, batch_size=500):
if not result_items:
return 0
def insert_result_item(result_item, share_retry_count=0): total_count = len(result_items)
req_id = result_item.get("reqId") total_batches = (
if not bh_utils.insert_data( total_count + batch_size - 1
"douchacha_data.geo_feishu_snipaste_result_v3", ) // batch_size
[result_item], inserted_count = 0
for batch_index, batch_start in enumerate(
range(0, total_count, batch_size),
start=1,
): ):
raise ResultWriteError( batch_items = result_items[
f"关键词统计结果写入失败: reqId={req_id}" batch_start:batch_start + batch_size
]
last_error = None
for attempt in range(1, 4):
try:
success = bh_utils.insert_data(
"douchacha_data."
"geo_feishu_snipaste_result_v3",
batch_items,
retry_times=0,
) )
except Exception as exc:
last_error = exc
success = False
if success:
inserted_count += len(batch_items)
logger.success( logger.success(
f"[pt={result_item.get('pt')} " f"[pt={pt} "
f"count={result_item.get('`count`')} " f"count={start_count}-{end_count}] "
f"reqId={req_id}] 关键词统计完成: " f"结果批量写入完成: "
f"has_keyword={result_item.get('has_keyword')}, " f"batch={batch_index}/{total_batches}, "
f"keywords_count={result_item.get('keywords_count')}, " f"rows={len(batch_items)}, "
f"share_retries={share_retry_count}" f"progress={inserted_count}/{total_count}, "
f"attempt={attempt}/3"
) )
return 1 break
logger.warning(
f"[pt={pt} count={start_count}-{end_count}] "
f"结果批量写入失败: "
f"batch={batch_index}/{total_batches}, "
f"rows={len(batch_items)}, "
f"attempt={attempt}/3, "
f"error={last_error or 'insert_data 返回 False'}"
)
if attempt < 3:
time.sleep(2)
else:
raise ResultWriteError(
f"关键词统计结果批量写入连续失败 3 次: "
f"pt={pt}, "
f"count={start_count}-{end_count}, "
f"batch={batch_index}/{total_batches}, "
f"rows={len(batch_items)}"
) from last_error
return inserted_count
inserted_count = 0 inserted_count = 0
parsed_results = parse_result_batch(result, "initial") parsed_results = parse_rows_with_tos_retries(
result,
"initial",
)
is_count_zero = start_count == 0 and end_count == 0 is_count_zero = start_count == 0 and end_count == 0
if not is_count_zero: if not is_count_zero:
for _, result_item in parsed_results: inserted_count = insert_result_items(
inserted_count += insert_result_item(result_item) [
result_item
for _, result_item in parsed_results
]
)
else: else:
pending_results = [] pending_results = []
final_result_items = []
for row, result_item in parsed_results: for row, result_item in parsed_results:
if int(result_item.get("has_keyword") or 0) == 1: if int(result_item.get("has_keyword") or 0) == 1:
inserted_count += insert_result_item(result_item) final_result_items.append(result_item)
else: else:
pending_results.append((row, result_item)) pending_results.append((row, result_item))
...@@ -1115,7 +1192,7 @@ def process_rounds_to_result( ...@@ -1115,7 +1192,7 @@ def process_rounds_to_result(
row row
for row, _ in pending_results for row, _ in pending_results
] ]
retry_parsed_results = parse_result_batch( retry_parsed_results = parse_rows_with_tos_retries(
retry_rows, retry_rows,
f"count=0 retry={share_retry_round}/5", f"count=0 retry={share_retry_round}/5",
) )
...@@ -1132,19 +1209,15 @@ def process_rounds_to_result( ...@@ -1132,19 +1209,15 @@ def process_rounds_to_result(
previous_result_item, previous_result_item,
) )
if int(result_item.get("has_keyword") or 0) == 1: if int(result_item.get("has_keyword") or 0) == 1:
inserted_count += insert_result_item( final_result_items.append(result_item)
result_item,
share_retry_count=share_retry_round,
)
else: else:
next_pending_results.append((row, result_item)) next_pending_results.append((row, result_item))
pending_results = next_pending_results pending_results = next_pending_results
for _, result_item in pending_results: for _, result_item in pending_results:
inserted_count += insert_result_item( final_result_items.append(result_item)
result_item,
share_retry_count=5, inserted_count = insert_result_items(final_result_items)
)
logger.success( logger.success(
f"[pt={pt} count={start_count}-{end_count}] " f"[pt={pt} count={start_count}-{end_count}] "
...@@ -2060,6 +2133,6 @@ def run_daily_pipeline_safely(pt: str = None) -> None: ...@@ -2060,6 +2133,6 @@ def run_daily_pipeline_safely(pt: str = None) -> None:
if __name__ == "__main__": if __name__ == "__main__":
# run_daily_pipeline() # run_daily_pipeline('20260729')
start_scheduler() start_scheduler()
# run_daily_pipeline(pt="2026072702") # run_daily_pipeline(pt="2026072702")
...@@ -6,6 +6,11 @@ from aidso_geo.core.routes.feishu_interface import feishu_app ...@@ -6,6 +6,11 @@ from aidso_geo.core.routes.feishu_interface import feishu_app
from aidso_geo.core.routes.third_interface import third_app from aidso_geo.core.routes.third_interface import third_app
app = Flask(__name__) app = Flask(__name__)
app.secret_key = "aidso-dashboard-session-2026-v3-fixed-key"
app.config.update(
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE="Lax",
)
app.json.ensure_ascii = False app.json.ensure_ascii = False
app.register_blueprint(line_app) app.register_blueprint(line_app)
......
...@@ -16,6 +16,7 @@ KEEP_FIELDS = ( ...@@ -16,6 +16,7 @@ KEEP_FIELDS = (
"type", "type",
"thinkingEnabled", "thinkingEnabled",
"channel", "channel",
"is_share"
) )
...@@ -47,12 +48,12 @@ def get_result_api(task_data): ...@@ -47,12 +48,12 @@ def get_result_api(task_data):
platform = task_data.get("platform",'') platform = task_data.get("platform",'')
channel = task_data.get("channel") or "" channel = task_data.get("channel") or ""
task_type = task_data.get("type",'') task_type = task_data.get("type",'')
url = f"http://172.16.1.223:8086/api/geo/task_check?reqId={req_id}" url = f"http://172.16.1.223:8086/api/geo/task_check?reqId={req_id}"
try: try:
response = requests.get(url) response = requests.get(url)
response_data = response.json() response_data = response.json()
print(response.text)
data = response_data.get("data") or {} data = response_data.get("data") or {}
if response_data.get("code") == 200 and data.get("status") == "success": if response_data.get("code") == 200 and data.get("status") == "success":
...@@ -90,9 +91,9 @@ def task_check(): ...@@ -90,9 +91,9 @@ def task_check():
if __name__ == "__main__": if __name__ == "__main__":
logger.info("调度 启动") logger.info("调度 启动")
scheduler = BlockingScheduler(timezone="Asia/Shanghai") scheduler = BlockingScheduler(timezone="Asia/Shanghai")
# task_check()
scheduler.add_job( scheduler.add_job(
task_commit, task_commit,
trigger="interval", trigger="interval",
......
from datetime import datetime from datetime import datetime
from hmac import compare_digest
from flask import jsonify, Blueprint, render_template, request from flask import (
import requests jsonify,
Blueprint,
redirect,
render_template,
request,
session,
url_for,
)
from aidso_geo.config.base_config import init_redis from aidso_geo.config.base_config import init_redis
from aidso_geo.utils import bh_utils
dashboard_app = Blueprint("dashboard", __name__) dashboard_app = Blueprint("dashboard", __name__)
redis_client = init_redis() redis_client = init_redis()
DASHBOARD_USERNAME = "aidso"
DASHBOARD_PASSWORD = "aidso666"
DASHBOARD_SESSION_KEY = "aidso_dashboard_authenticated"
@dashboard_app.before_request
def require_dashboard_login():
if request.endpoint in {
"dashboard.dashboard_login",
"dashboard.dashboard_logout",
}:
return None
if session.get(DASHBOARD_SESSION_KEY):
return None
if request.path.startswith("/api/queue/"):
return jsonify({
"code": 401,
"msg": "login required",
"data": {},
}), 401
return redirect(url_for("dashboard.dashboard_login"))
@dashboard_app.route("/queue/login", methods=["GET", "POST"])
def dashboard_login():
if session.get(DASHBOARD_SESSION_KEY):
return redirect(url_for("dashboard.queue_monitor_page"))
error_message = ""
if request.method == "POST":
username = request.form.get("username", "")
password = request.form.get("password", "")
username_matches = compare_digest(
username,
DASHBOARD_USERNAME,
)
password_matches = compare_digest(
password,
DASHBOARD_PASSWORD,
)
if username_matches and password_matches:
session.clear()
session[DASHBOARD_SESSION_KEY] = True
return redirect(url_for("dashboard.queue_monitor_page"))
error_message = "账号或密码错误,请重新输入"
return render_template(
"dashboard_login.html",
error_message=error_message,
)
@dashboard_app.route("/queue/logout", methods=["GET"])
def dashboard_logout():
session.clear()
return redirect(url_for("dashboard.dashboard_login"))
@dashboard_app.route("/api/queue/status", methods=["GET"]) @dashboard_app.route("/api/queue/status", methods=["GET"])
def queue_status(): def queue_status():
...@@ -48,14 +117,10 @@ def get_req_trend(): ...@@ -48,14 +117,10 @@ def get_req_trend():
}), 400 }), 400
try: try:
params = { begin_date = datetime.strptime(begin, "%Y-%m-%d")
"begin": normalize_date_to_datetime(begin, "00:00:00"), end_date = datetime.strptime(end, "%Y-%m-%d")
"end": normalize_date_to_datetime(end, "23:59:59"),
}
begin_time = datetime.strptime(params["begin"], "%Y-%m-%d %H:%M:%S")
end_time = datetime.strptime(params["end"], "%Y-%m-%d %H:%M:%S")
if begin_time > end_time: if begin_date > end_date:
return jsonify({ return jsonify({
"code": 400, "code": 400,
"msg": "begin must be earlier than end", "msg": "begin must be earlier than end",
...@@ -68,21 +133,390 @@ def get_req_trend(): ...@@ -68,21 +133,390 @@ def get_req_trend():
"data": [] "data": []
}), 400 }), 400
url = "https://openapi.aidso.com/openapi/ywt/platformStats"
try: try:
response = requests.get(url, params=params, timeout=300) rows = bh_utils.query_data(
response.raise_for_status() """
return jsonify(response.json()) SELECT
except (requests.RequestException, ValueError) as e: platform,
pt,
uniqExactIf(reqId, channel IS NULL) AS system_count,
uniqExactIf(reqId, channel IS NOT NULL)
AS third_party_count,
uniqExact(reqId) AS task_count
FROM geo_commit_task
WHERE pt BETWEEN %s AND %s
GROUP BY platform, pt
ORDER BY pt, platform
""",
(
begin_date.strftime("%Y%m%d"),
end_date.strftime("%Y%m%d"),
),
)
if rows is None:
raise RuntimeError("query geo_commit_task failed")
data = [
{
"platform": row.get("platform"),
"time": datetime.strptime(
str(row.get("pt")),
"%Y%m%d",
).strftime("%Y-%m-%d"),
"system_count": int(
row.get("system_count") or 0
),
"third_party_count": int(
row.get("third_party_count") or 0
),
"count": int(row.get("task_count") or 0),
}
for row in rows
]
return jsonify({
"code": 200,
"msg": "success",
"data": data,
})
except (TypeError, ValueError, RuntimeError) as e:
return jsonify({ return jsonify({
"code": 500, "code": 500,
"msg": f"platformStats request failed: {e}", "msg": f"platformStats query failed: {e}",
"data": [] "data": []
}), 500 }), 500
def normalize_date_to_datetime(value, default_time): @dashboard_app.route("/api/queue/thirdPartyStats", methods=["GET"])
date_value = value.replace("T", " ").strip()[:10] def get_third_party_stats():
datetime.strptime(date_value, "%Y-%m-%d") begin = request.args.get("begin", "").strip()
return f"{date_value} {default_time}" end = request.args.get("end", "").strip()
selected_channel = request.args.get("channel", "").strip()
selected_platform = request.args.get("platform", "").strip()
if not begin or not end:
return jsonify({
"code": 400,
"msg": "begin and end are required",
"data": {},
}), 400
try:
begin_date = datetime.strptime(begin, "%Y-%m-%d")
end_date = datetime.strptime(end, "%Y-%m-%d")
if begin_date > end_date:
return jsonify({
"code": 400,
"msg": "begin must be earlier than end",
"data": {},
}), 400
except ValueError:
return jsonify({
"code": 400,
"msg": "begin and end must be YYYY-MM-DD",
"data": {},
}), 400
begin_pt = begin_date.strftime("%Y%m%d")
end_pt = end_date.strftime("%Y%m%d")
day_count = (end_date - begin_date).days + 1
try:
enabled_channels = bh_utils.query_data(
"""
SELECT
channel,
remark,
daily_limit,
total_limit,
allow_platforms
FROM geo_third_token
WHERE status = 1
ORDER BY channel
"""
)
if enabled_channels is None:
raise RuntimeError("query enabled third channels failed")
enabled_channel_names = [
str(row.get("channel") or "").strip()
for row in enabled_channels
if str(row.get("channel") or "").strip()
]
enabled_channel_set = set(enabled_channel_names)
if (
selected_channel
and selected_channel not in enabled_channel_set
):
return jsonify({
"code": 400,
"msg": "selected channel is not enabled",
"data": {},
}), 400
if not enabled_channel_names:
return jsonify({
"code": 200,
"msg": "success",
"data": {
"metrics": {
"third_total": 0,
"daily_average": 0,
"third_ratio": 0,
"share_total": 0,
"enabled_channel_count": 0,
},
"channels": [],
"platforms": [],
"daily": [],
"channel_ranking": [],
"matrix": [],
"channel_details": [],
},
})
placeholders = ", ".join(
["%s"] * len(enabled_channel_names)
)
usage_filters = [
"pt BETWEEN %s AND %s",
f"channel IN ({placeholders})",
]
usage_params = [
begin_pt,
end_pt,
*enabled_channel_names,
]
if selected_channel:
usage_filters.append("channel = %s")
usage_params.append(selected_channel)
if selected_platform:
usage_filters.append("platform = %s")
usage_params.append(selected_platform)
usage_where = " AND ".join(usage_filters)
usage_rows = bh_utils.query_data(
f"""
SELECT
channel,
platform,
pt,
uniqExact(reqId) AS task_count,
uniqExactIf(
reqId,
ifNull(toString(is_share), '0') = '1'
) AS share_count
FROM geo_commit_task
WHERE {usage_where}
GROUP BY channel, platform, pt
ORDER BY pt, channel, platform
""",
tuple(usage_params),
)
if usage_rows is None:
raise RuntimeError("query third party usage failed")
summary_rows = bh_utils.query_data(
f"""
SELECT
uniqExact(reqId) AS third_total,
uniqExactIf(
reqId,
ifNull(toString(is_share), '0') = '1'
) AS share_total
FROM geo_commit_task
WHERE {usage_where}
""",
tuple(usage_params),
)
if summary_rows is None:
raise RuntimeError("query third party summary failed")
total_filters = ["pt BETWEEN %s AND %s"]
total_params = [begin_pt, end_pt]
if selected_platform:
total_filters.append("platform = %s")
total_params.append(selected_platform)
total_rows = bh_utils.query_data(
f"""
SELECT uniqExact(reqId) AS all_total
FROM geo_commit_task
WHERE {" AND ".join(total_filters)}
""",
tuple(total_params),
)
if total_rows is None:
raise RuntimeError("query all task summary failed")
third_total = int(
(summary_rows[0] if summary_rows else {}).get(
"third_total"
)
or 0
)
share_total = int(
(summary_rows[0] if summary_rows else {}).get(
"share_total"
)
or 0
)
all_total = int(
(total_rows[0] if total_rows else {}).get(
"all_total"
)
or 0
)
daily_map = {}
channel_map = {}
matrix_map = {}
platform_set = set()
for row in usage_rows:
channel = str(row.get("channel") or "")
platform = str(row.get("platform") or "")
pt = str(row.get("pt") or "")
task_count = int(row.get("task_count") or 0)
share_count = int(row.get("share_count") or 0)
platform_set.add(platform)
daily_key = (channel, pt)
daily_map[daily_key] = (
daily_map.get(daily_key, 0) + task_count
)
channel_item = channel_map.setdefault(
channel,
{
"channel": channel,
"task_count": 0,
"share_count": 0,
"platforms": set(),
"last_date": "",
},
)
channel_item["task_count"] += task_count
channel_item["share_count"] += share_count
channel_item["platforms"].add(platform)
if not channel_item["last_date"] or (
pt > channel_item["last_date"]
):
channel_item["last_date"] = pt
matrix_key = (channel, platform)
matrix_map[matrix_key] = (
matrix_map.get(matrix_key, 0) + task_count
)
config_map = {
str(row.get("channel") or ""): row
for row in enabled_channels
}
ranking = []
details = []
channels_to_show = (
[selected_channel]
if selected_channel
else enabled_channel_names
)
for channel in channels_to_show:
config = config_map.get(channel) or {}
usage = channel_map.get(channel) or {
"task_count": 0,
"share_count": 0,
"platforms": set(),
"last_date": "",
}
task_count = int(usage["task_count"])
item = {
"channel": channel,
"remark": config.get("remark") or "",
"task_count": task_count,
"share_count": int(usage["share_count"]),
"ratio": round(
task_count / third_total * 100,
2,
) if third_total else 0,
"platform_count": len(usage["platforms"]),
"last_date": usage["last_date"],
"daily_limit": int(
config.get("daily_limit") or 0
),
"total_limit": int(
config.get("total_limit") or 0
),
"allow_platforms": (
config.get("allow_platforms") or "[]"
),
}
ranking.append(item)
details.append(dict(item))
ranking.sort(
key=lambda item: item["task_count"],
reverse=True,
)
details.sort(
key=lambda item: item["task_count"],
reverse=True,
)
platforms = sorted(platform_set)
daily = [
{
"channel": channel,
"pt": pt,
"count": int(task_count),
}
for (channel, pt), task_count in sorted(
daily_map.items()
)
]
matrix = [
{
"channel": channel,
"platform": platform,
"count": int(
matrix_map.get((channel, platform), 0)
),
}
for channel in channels_to_show
for platform in platforms
]
return jsonify({
"code": 200,
"msg": "success",
"data": {
"metrics": {
"third_total": third_total,
"daily_average": round(
third_total / day_count,
2,
),
"third_ratio": round(
third_total / all_total * 100,
2,
) if all_total else 0,
"share_total": share_total,
"enabled_channel_count": len(
enabled_channel_names
),
},
"channels": [
{
"channel": row.get("channel"),
"remark": row.get("remark") or "",
}
for row in enabled_channels
],
"platforms": platforms,
"daily": daily,
"channel_ranking": ranking,
"matrix": matrix,
"channel_details": details,
},
})
except (TypeError, ValueError, RuntimeError) as exc:
return jsonify({
"code": 500,
"msg": f"thirdPartyStats query failed: {exc}",
"data": {},
}), 500
...@@ -502,12 +502,57 @@ def mt_task_commit(): ...@@ -502,12 +502,57 @@ def mt_task_commit():
if type not in {"0", "1"}: if type not in {"0", "1"}:
return err(400, f"platform[{idx}].type must be '0' or '1'") return err(400, f"platform[{idx}].type must be '0' or '1'")
is_share = str(item.get("is_share", "0"))
if is_share not in {"0", "1"}:
return err(
400,
f"platform[{idx}].is_share must be '0' or '1'"
)
normalized.append({ normalized.append({
"name": name, "name": name,
"thinkingEnabled": thinking_enabled, "thinkingEnabled": thinking_enabled,
"type": type "type": type,
"is_share": is_share
}) })
share_requested_platforms = {
item["name"]
for item in normalized
if item["is_share"] == "1"
}
if share_requested_platforms:
try:
share_platforms_raw = tos_utils.get_string_from_tos(
"geo_config/share_platforms.json"
)
share_platforms = json.loads(share_platforms_raw)
except Exception:
traceback.print_exc()
return err(500, "分享平台配置读取失败", 500)
if not isinstance(share_platforms, list):
return err(
500,
"分享平台配置格式错误,必须是平台数组",
500
)
supported_share_platforms = {
str(platform).strip()
for platform in share_platforms
if str(platform).strip()
}
unsupported_share_platforms = sorted(
share_requested_platforms - supported_share_platforms
)
if unsupported_share_platforms:
return err(
400,
"平台不支持分享: "
+ ", ".join(unsupported_share_platforms)
)
platform_costs = parse_platform_costs(auth_info.get("platform_costs")) platform_costs = parse_platform_costs(auth_info.get("platform_costs"))
thinking_costs = parse_thinking_costs(auth_info.get("thinking_costs")) thinking_costs = parse_thinking_costs(auth_info.get("thinking_costs"))
...@@ -563,6 +608,7 @@ def mt_task_commit(): ...@@ -563,6 +608,7 @@ def mt_task_commit():
"platform": name, "platform": name,
"thinkingEnabled": p["thinkingEnabled"], "thinkingEnabled": p["thinkingEnabled"],
"type": type_map[p["type"]], "type": type_map[p["type"]],
"is_share": p["is_share"],
"insertime": now_ts, "insertime": now_ts,
"status": "ING", "status": "ING",
"channel": channel "channel": channel
...@@ -660,7 +706,8 @@ def mt_get_result(): ...@@ -660,7 +706,8 @@ def mt_get_result():
"think": "think.txt", "think": "think.txt",
"context": "context.txt", "context": "context.txt",
"suggestions": "suggestions.txt", "suggestions": "suggestions.txt",
"rich_media_block": "rich_media_block.txt" "rich_media_block": "rich_media_block.txt",
"share_url": "share_url.text"
} }
result = [] result = []
...@@ -797,4 +844,3 @@ def mt_get_usage(): ...@@ -797,4 +844,3 @@ def mt_get_usage():
# "history_used_list": history_userd_list # "history_used_list": history_userd_list
}) })
* {
box-sizing: border-box;
}
:root {
--primary: #7138f4;
--primary-dark: #5824d6;
--ink: #282735;
--muted: #7b788a;
--line: #e2deea;
}
body {
margin: 0;
color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Microsoft YaHei", Arial, sans-serif;
background: #eef3ff;
}
.login-page {
display: grid;
min-height: 100vh;
grid-template-columns: minmax(420px, 1.08fr) minmax(430px, 0.92fr);
}
.login-visual {
position: relative;
display: flex;
align-items: center;
overflow: hidden;
padding: clamp(50px, 7vw, 110px);
background:
radial-gradient(circle at 22% 18%, rgba(255, 255, 255, 0.22), transparent 26%),
radial-gradient(circle at 84% 84%, rgba(193, 164, 255, 0.3), transparent 30%),
linear-gradient(145deg, #6322eb 0%, #7f3fff 54%, #985fff 100%);
}
.login-visual::before,
.login-visual::after {
content: "";
position: absolute;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 50%;
}
.login-visual::before {
width: 520px;
height: 520px;
top: -270px;
right: -180px;
}
.login-visual::after {
width: 360px;
height: 360px;
bottom: -205px;
left: -130px;
}
.visual-content {
position: relative;
z-index: 1;
max-width: 580px;
color: #fff;
}
.visual-badge {
display: inline-flex;
padding: 7px 11px;
border: 1px solid rgba(255, 255, 255, 0.32);
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
font-size: 11px;
font-weight: 800;
letter-spacing: 0.15em;
}
.visual-content h1 {
margin: 24px 0 15px;
font-size: clamp(42px, 5.2vw, 74px);
font-weight: 850;
letter-spacing: -0.055em;
line-height: 1.05;
}
.visual-content p {
max-width: 500px;
margin: 0;
color: rgba(255, 255, 255, 0.78);
font-size: clamp(15px, 1.25vw, 19px);
line-height: 1.8;
}
.visual-grid {
display: grid;
width: min(100%, 470px);
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-top: 52px;
}
.visual-grid span {
height: 74px;
border: 1px solid rgba(255, 255, 255, 0.17);
border-radius: 14px;
background: rgba(255, 255, 255, 0.08);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.visual-grid span:nth-child(2),
.visual-grid span:nth-child(4),
.visual-grid span:nth-child(6) {
background: rgba(255, 255, 255, 0.15);
}
.login-panel {
display: grid;
min-width: 0;
place-items: center;
padding: 42px;
background:
radial-gradient(circle at 100% 0%, rgba(113, 56, 244, 0.08), transparent 28%),
#f7f8fc;
}
.login-card {
width: min(100%, 440px);
padding: 42px;
border: 1px solid rgba(226, 222, 234, 0.9);
border-radius: 22px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 22px 55px rgba(58, 45, 88, 0.1);
}
.login-logo {
display: block;
width: 240px;
max-width: 78%;
height: auto;
margin-bottom: 42px;
}
.login-heading h2 {
margin: 0;
color: #242230;
font-size: 28px;
font-weight: 850;
letter-spacing: -0.025em;
}
.login-heading p {
margin: 9px 0 30px;
color: var(--muted);
font-size: 14px;
}
.login-field {
display: grid;
gap: 8px;
margin-bottom: 20px;
color: #565267;
font-size: 13px;
font-weight: 750;
}
.input-shell {
position: relative;
display: flex;
align-items: center;
}
.input-shell input {
width: 100%;
height: 50px;
padding: 0 14px 0 44px;
border: 1px solid var(--line);
border-radius: 11px;
outline: none;
background: #fff;
color: #292635;
font: inherit;
font-size: 14px;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.input-shell input::placeholder {
color: #aaa7b2;
}
.input-shell input:focus {
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(113, 56, 244, 0.11);
}
.input-icon {
position: absolute;
z-index: 1;
left: 17px;
width: 14px;
height: 14px;
pointer-events: none;
}
.user-icon {
border: 2px solid #9994a5;
border-radius: 50%;
}
.user-icon::after {
content: "";
position: absolute;
width: 18px;
height: 9px;
top: 12px;
left: -4px;
border: 2px solid #9994a5;
border-bottom: 0;
border-radius: 12px 12px 0 0;
}
.lock-icon {
top: 18px;
border: 2px solid #9994a5;
border-radius: 3px;
}
.lock-icon::before {
content: "";
position: absolute;
width: 8px;
height: 8px;
top: -9px;
left: 1px;
border: 2px solid #9994a5;
border-bottom: 0;
border-radius: 8px 8px 0 0;
}
.login-error {
margin: -3px 0 17px;
padding: 10px 12px;
border: 1px solid #fecaca;
border-radius: 9px;
background: #fff4f4;
color: #b42323;
font-size: 13px;
}
.login-button {
width: 100%;
height: 50px;
margin-top: 5px;
border: 0;
border-radius: 11px;
background: linear-gradient(135deg, #8247ff, #6825ed);
color: #fff;
cursor: pointer;
font-size: 15px;
font-weight: 800;
box-shadow: 0 11px 22px rgba(113, 56, 244, 0.24);
transition: transform 0.18s ease, box-shadow 0.18s ease;
}
.login-button:hover {
transform: translateY(-1px);
box-shadow: 0 14px 28px rgba(113, 56, 244, 0.3);
}
.login-footer {
margin: 27px 0 0;
color: #aaa7b2;
font-size: 11px;
text-align: center;
}
@media (max-width: 900px) {
.login-page {
grid-template-columns: 1fr;
}
.login-visual {
min-height: 260px;
padding: 50px 34px;
}
.visual-content h1 {
margin-top: 16px;
font-size: 42px;
}
.visual-grid {
display: none;
}
.login-panel {
padding: 34px 20px;
}
}
@media (max-width: 520px) {
.login-visual {
min-height: 220px;
padding: 38px 24px;
}
.visual-content h1 {
font-size: 34px;
}
.visual-content p {
font-size: 13px;
}
.login-card {
padding: 30px 24px;
border-radius: 18px;
}
.login-logo {
width: 210px;
margin-bottom: 34px;
}
}
...@@ -196,6 +196,43 @@ ...@@ -196,6 +196,43 @@
display: flex; display: flex;
} }
.third-panel {
position: relative;
display: grid;
gap: 18px;
min-width: 0;
overflow: hidden;
padding: 18px;
border: 1px solid #e5e7eb;
border-radius: 14px;
background: #fff;
box-shadow: 0 14px 30px rgba(15, 23, 42, 0.06);
}
.third-panel.loading .trend-loading-mask {
display: flex;
}
.third-panel > .filter-row {
display: grid;
grid-template-columns:
auto
minmax(170px, 1fr)
minmax(170px, 1fr)
minmax(170px, 1fr)
minmax(170px, 1fr);
align-items: end;
gap: 12px;
margin-bottom: 0;
}
.third-panel > .filter-row .field,
.third-panel > .filter-row input,
.third-panel > .filter-row select {
min-width: 0;
width: 100%;
}
.loading-box { .loading-box {
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -240,6 +277,7 @@ ...@@ -240,6 +277,7 @@
.quick-range { .quick-range {
display: flex; display: flex;
gap: 8px; gap: 8px;
align-items: flex-end;
} }
.field { .field {
...@@ -253,210 +291,693 @@ ...@@ -253,210 +291,693 @@
color: #6b7280; color: #6b7280;
} }
.field input { .field input,
.field select {
min-width: 220px; min-width: 220px;
border: 1px solid #d7dce3; height: 42px;
border-radius: 8px; box-sizing: border-box;
padding: 9px 10px; border: 1px solid #cbd5e1;
border-radius: 9px;
padding: 0 12px;
background: #fff; background: #fff;
color: #1f2937; color: #334155;
font-size: 14px;
font-family: inherit;
font-weight: 600;
line-height: 40px;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.field select {
appearance: none;
-webkit-appearance: none;
padding-right: 38px;
background-image:
linear-gradient(45deg, transparent 50%, #64748b 50%),
linear-gradient(135deg, #64748b 50%, transparent 50%);
background-position:
calc(100% - 17px) 17px,
calc(100% - 12px) 17px;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
cursor: pointer;
}
.field select.native-select-hidden {
display: none;
}
.custom-select {
position: relative;
width: 100%;
min-width: 0;
}
.custom-select-trigger {
display: flex;
width: 100%;
height: 42px;
align-items: center;
justify-content: space-between;
gap: 12px;
box-sizing: border-box;
padding: 0 13px;
border: 1px solid #cbd5e1;
border-radius: 9px;
background: #fff;
color: #334155;
font-family: inherit;
font-size: 14px; font-size: 14px;
font-weight: 600;
text-align: left;
cursor: pointer;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.custom-select-trigger:hover {
border-color: #94a3b8;
} }
.field input:focus { .custom-select.open .custom-select-trigger,
.custom-select-trigger:focus-visible {
outline: none; outline: none;
border-color: #2563eb; border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
} }
.trend-chart { .custom-select-value {
width: 100%; min-width: 0;
min-height: 300px; overflow: hidden;
overflow-x: auto; text-overflow: ellipsis;
border-radius: 12px; white-space: nowrap;
background: #f8fafc;
border: 1px solid #edf2f7;
} }
.daily-grid { .custom-select-arrow {
display: grid; width: 8px;
grid-auto-flow: column; height: 8px;
grid-auto-columns: minmax(320px, 1fr); flex: 0 0 auto;
gap: 14px; margin: -4px 2px 0 0;
min-width: 100%; border-right: 2px solid #64748b;
padding: 14px; border-bottom: 2px solid #64748b;
transform: rotate(45deg);
transition: transform 0.18s ease, margin 0.18s ease;
} }
.day-card { .custom-select.open .custom-select-arrow {
min-width: 0; margin-top: 4px;
border: 1px solid #e5e7eb; transform: rotate(225deg);
}
.custom-select-menu {
position: absolute;
z-index: 120;
top: calc(100% + 8px);
left: 0;
display: none;
width: max(100%, 250px);
max-width: min(420px, 80vw);
max-height: 320px;
overflow-y: auto;
box-sizing: border-box;
padding: 6px;
border: 1px solid #dbe3ee;
border-radius: 12px; border-radius: 12px;
background: #fff; background: #fff;
box-shadow: 0 10px 22px rgba(15, 23, 42, 0.04); box-shadow: 0 18px 45px rgba(15, 23, 42, 0.16);
} }
.day-card-head { .custom-select.open .custom-select-menu {
display: flex; display: grid;
align-items: center; gap: 3px;
justify-content: space-between;
padding: 14px 14px 10px;
border-bottom: 1px solid #edf2f7;
} }
.day-date { .custom-select-option {
color: #111827; width: 100%;
font-size: 16px; min-height: 38px;
font-weight: 800; padding: 8px 11px;
border: 0;
border-radius: 8px;
background: transparent;
color: #334155;
font-family: inherit;
font-size: 14px;
font-weight: 600;
line-height: 1.35;
text-align: left;
cursor: pointer;
} }
.day-total { .custom-select-option:hover {
color: #64748b; background: #eff6ff;
font-size: 13px; color: #1d4ed8;
font-weight: 700;
} }
.day-list { .custom-select-option.selected {
display: grid; position: relative;
gap: 10px; padding-right: 34px;
padding: 12px 14px 14px; background: #2563eb;
color: #fff;
}
.custom-select-option.selected::after {
content: "✓";
position: absolute;
top: 50%;
right: 12px;
font-size: 15px;
transform: translateY(-50%);
}
.field input[type="date"] {
color-scheme: light;
cursor: pointer;
}
.field input:hover,
.field select:hover {
border-color: #94a3b8;
}
.field input:focus,
.field select:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
}
.filter-row .quick-btn {
height: 42px;
box-sizing: border-box;
} }
.day-row { .third-metrics {
display: grid; display: grid;
grid-template-columns: minmax(100px, 1fr) minmax(90px, 42%) 58px; grid-template-columns: repeat(5, minmax(0, 1fr));
align-items: center; gap: 14px;
gap: 8px;
min-height: 24px;
} }
.day-platform { .third-metric-card,
color: #334155; .third-section-card {
border: 1px solid #e5e7eb;
border-radius: 13px;
background: #fff;
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.05);
}
.third-metric-card {
display: flex;
flex-direction: column;
gap: 9px;
min-width: 0;
padding: 16px;
}
.third-metric-card span {
color: #64748b;
font-size: 13px; font-size: 13px;
font-weight: 700; font-weight: 700;
}
.third-metric-card strong {
overflow: hidden; overflow: hidden;
color: #0f172a;
font-size: clamp(19px, 1.45vw, 25px);
font-weight: 800;
line-height: 1.2;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.day-bar { .third-dashboard-grid {
height: 9px; display: grid;
border-radius: 999px; grid-template-columns: minmax(360px, 0.8fr) minmax(0, 1.7fr);
background: #eef2f7; gap: 18px;
min-width: 0;
}
.third-section-card {
min-width: 0;
overflow: hidden; overflow: hidden;
} }
.day-bar-inner { .third-card-head {
height: 100%; display: flex;
min-width: 3px; align-items: center;
border-radius: 999px; justify-content: space-between;
gap: 12px;
padding: 15px 17px;
border-bottom: 1px solid #edf2f7;
} }
.day-count { .third-card-head strong {
color: #111827; color: #0f172a;
font-size: 13px; font-size: 16px;
font-weight: 800;
}
.third-card-head > span {
color: #64748b;
font-size: 12px;
font-weight: 700; font-weight: 700;
text-align: right;
} }
.chart-empty { .third-ranking-list {
display: flex; display: grid;
gap: 9px;
max-height: 390px;
overflow-y: auto;
padding: 14px 16px 17px;
}
.third-ranking-row {
display: grid;
grid-template-columns: 26px minmax(110px, 1.2fr) minmax(80px, 1fr) 64px;
align-items: center; align-items: center;
justify-content: center; gap: 9px;
min-height: 220px; width: 100%;
color: #6b7280; padding: 7px 4px;
font-size: 14px; border: none;
border-radius: 8px;
background: transparent;
cursor: pointer;
text-align: left;
} }
.trend-summary { .third-ranking-row:hover {
margin-top: 16px; background: #f8fafc;
padding: 16px;
border: 1px solid #edf2f7;
border-radius: 12px;
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
} }
.trend-summary-title { .third-ranking-index {
display: flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: center;
margin-bottom: 12px; width: 22px;
height: 22px;
border-radius: 7px;
background: #eff6ff;
color: #2563eb;
font-size: 11px;
font-weight: 800;
} }
.trend-summary-title-main { .third-ranking-name {
color: #111827; overflow: hidden;
font-size: 16px; color: #334155;
font-size: 12px;
font-weight: 800; font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
} }
.trend-summary-actions { .third-ranking-bar {
display: flex; height: 8px;
align-items: center; overflow: hidden;
gap: 10px; border-radius: 999px;
background: #edf2f7;
} }
.trend-summary-range { .third-ranking-bar-inner {
color: #64748b; display: block;
font-size: 13px; height: 100%;
font-weight: 700; border-radius: inherit;
background: #2563eb;
} }
.trend-switch { .third-ranking-count {
color: #0f172a;
font-size: 12px;
text-align: right;
}
.third-trend-switch {
display: inline-grid; display: inline-grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 2px; gap: 2px;
padding: 3px; padding: 3px;
border: 1px solid #d7dce3; border: 1px solid #d7dce3;
border-radius: 10px; border-radius: 9px;
background: #f1f5f9; background: #f1f5f9;
} }
.trend-switch-btn { .third-trend-switch button {
border: none; border: none;
border-radius: 7px; border-radius: 6px;
background: transparent; background: transparent;
color: #64748b; color: #64748b;
padding: 6px 11px; padding: 6px 9px;
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: 12px;
font-weight: 800; font-weight: 800;
} }
.trend-switch-btn.active { .third-trend-switch button.active {
background: #2563eb; background: #2563eb;
color: #fff; color: #fff;
box-shadow: 0 8px 16px rgba(37, 99, 235, 0.18);
} }
.trend-summary svg { .third-trend-chart {
display: block; min-height: 390px;
width: 100%; padding: 12px 14px 8px;
height: 220px;
} }
.trend-label { .third-trend-legend {
fill: #64748b; display: flex;
font-size: 12px; gap: 12px;
overflow-x: auto;
padding: 2px 4px 10px;
color: #475569;
font-size: 11px;
font-weight: 700; font-weight: 700;
white-space: nowrap;
} }
.trend-total-label { .third-trend-legend span {
fill: #2563eb; display: inline-flex;
font-size: 12px; align-items: center;
font-weight: 800; gap: 5px;
} }
.trend-hover-hit { .third-trend-legend i {
cursor: crosshair; width: 8px;
height: 8px;
border-radius: 999px;
} }
.trend-tooltip { .third-trend-scroll {
position: fixed; overflow-x: auto;
z-index: 30; overflow-y: hidden;
display: none; }
min-width: 260px;
max-width: 360px; .third-trend-scroll svg {
max-height: 420px; display: block;
overflow-y: auto; max-width: none;
}
.third-trend-label {
fill: #64748b;
font-size: 11px;
font-weight: 700;
}
.third-table-scroll {
display: block;
min-width: 0;
max-width: 100%;
width: 100%;
overflow-x: auto;
}
.third-matrix-table,
.third-detail-table {
min-width: 100%;
white-space: nowrap;
}
.third-matrix-table th,
.third-matrix-table td {
min-width: 108px;
text-align: center;
}
.third-matrix-table th:first-child,
.third-matrix-table td:first-child {
position: sticky;
left: 0;
z-index: 2;
min-width: 150px;
background: #fff;
text-align: left;
font-weight: 800;
}
.third-matrix-total {
background: #f8fafc;
font-weight: 800;
}
.third-empty {
display: flex;
align-items: center;
justify-content: center;
min-height: 160px;
color: #94a3b8;
font-size: 13px;
}
.trend-chart {
width: 100%;
min-height: 300px;
overflow-x: auto;
border-radius: 12px;
background: #f8fafc;
border: 1px solid #edf2f7;
}
.daily-stacked-legend {
display: flex;
justify-content: flex-end;
gap: 18px;
padding: 14px 16px 4px;
color: #475569;
font-size: 13px;
font-weight: 700;
}
.daily-source-filter {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 7px 10px;
border: 1px solid transparent;
border-radius: 8px;
background: transparent;
color: #94a3b8;
cursor: pointer;
font: inherit;
opacity: 0.48;
transition: opacity 0.18s ease, background 0.18s ease, border-color 0.18s ease;
}
.daily-source-filter.active {
border-color: #dbe3ee;
background: #fff;
color: #334155;
opacity: 1;
box-shadow: 0 5px 14px rgba(15, 23, 42, 0.06);
}
.daily-source-filter:hover {
opacity: 1;
}
.daily-stacked-legend i {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 3px;
}
.daily-stacked-grid {
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(430px, 1fr);
gap: 14px;
overflow-x: auto;
overflow-y: hidden;
padding: 10px 14px 14px;
}
.daily-stacked-card {
overflow: hidden;
border: 1px solid #e2e8f0;
border-radius: 13px;
background: #fff;
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.05);
}
.daily-stacked-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px 16px;
border-bottom: 1px solid #e8edf4;
}
.daily-stacked-head strong {
color: #0f172a;
font-size: 17px;
font-weight: 800;
}
.daily-stacked-head span {
color: #64748b;
font-size: 13px;
font-weight: 800;
}
.daily-stacked-list {
display: grid;
gap: 13px;
padding: 14px 16px 17px;
}
.daily-stacked-row {
display: grid;
grid-template-columns: minmax(142px, 1.15fr) minmax(130px, 1fr) 54px;
align-items: center;
gap: 12px;
min-height: 38px;
}
.daily-stacked-name {
overflow: hidden;
color: #334155;
font-size: 13px;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
}
.daily-stacked-source-numbers {
display: flex;
gap: 9px;
margin-top: 3px;
color: #94a3b8;
font-size: 10px;
font-weight: 700;
}
.daily-stacked-track {
height: 12px;
overflow: hidden;
border-radius: 999px;
background: #edf2f7;
}
.daily-stacked-bar {
display: flex;
min-width: 2px;
height: 100%;
overflow: hidden;
border-radius: inherit;
}
.daily-stacked-system {
height: 100%;
background: #4f6fd7;
}
.daily-stacked-third-party {
height: 100%;
background: #b4db2e;
}
.daily-stacked-value {
color: #0f172a;
font-size: 13px;
font-weight: 800;
text-align: right;
}
.chart-empty {
display: flex;
align-items: center;
justify-content: center;
min-height: 220px;
color: #6b7280;
font-size: 14px;
}
.trend-summary {
margin-top: 16px;
padding: 16px;
border: 1px solid #edf2f7;
border-radius: 12px;
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
}
.trend-summary-title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.trend-summary-title-main {
color: #111827;
font-size: 16px;
font-weight: 800;
}
.trend-summary-actions {
display: flex;
align-items: center;
gap: 10px;
}
.trend-summary-range {
color: #64748b;
font-size: 13px;
font-weight: 700;
}
.trend-switch {
display: inline-grid;
grid-template-columns: 1fr 1fr;
gap: 2px;
padding: 3px;
border: 1px solid #d7dce3;
border-radius: 10px;
background: #f1f5f9;
}
.trend-switch-btn {
border: none;
border-radius: 7px;
background: transparent;
color: #64748b;
padding: 6px 11px;
cursor: pointer;
font-size: 13px;
font-weight: 800;
}
.trend-switch-btn.active {
background: #2563eb;
color: #fff;
box-shadow: 0 8px 16px rgba(37, 99, 235, 0.18);
}
.trend-summary svg {
display: block;
width: 100%;
height: 220px;
}
.trend-label {
fill: #64748b;
font-size: 12px;
font-weight: 700;
}
.trend-total-label {
fill: #2563eb;
font-size: 12px;
font-weight: 800;
}
.trend-hover-hit {
cursor: crosshair;
}
.trend-tooltip {
position: fixed;
z-index: 30;
display: none;
min-width: 260px;
max-width: 360px;
max-height: 420px;
overflow-y: auto;
padding: 10px 12px; padding: 10px 12px;
border: 1px solid rgba(148, 163, 184, 0.28); border: 1px solid rgba(148, 163, 184, 0.28);
border-radius: 10px; border-radius: 10px;
...@@ -594,6 +1115,28 @@ ...@@ -594,6 +1115,28 @@
font-size: 14px; font-size: 14px;
} }
@media (max-width: 1500px) {
.third-metrics {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.third-panel > .filter-row {
grid-template-columns: auto repeat(2, minmax(180px, 1fr));
}
.third-panel > .filter-row .quick-range {
grid-row: span 2;
align-self: stretch;
align-items: end;
}
}
@media (max-width: 1100px) {
.third-dashboard-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 900px) { @media (max-width: 900px) {
.layout { .layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
...@@ -615,6 +1158,23 @@ ...@@ -615,6 +1158,23 @@
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
.third-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.third-dashboard-grid {
grid-template-columns: 1fr;
}
.third-panel > .filter-row {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.third-panel > .filter-row .quick-range {
grid-row: auto;
grid-column: 1 / -1;
}
.header { .header {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
...@@ -638,12 +1198,1134 @@ ...@@ -638,12 +1198,1134 @@
.field, .field,
.field input, .field input,
.field select,
.filter-row .btn, .filter-row .btn,
.quick-range { .quick-range {
width: 100%; width: 100%;
} }
.quick-btn { .third-metrics {
flex: 1; grid-template-columns: 1fr;
}
.third-panel > .filter-row {
grid-template-columns: 1fr;
}
.third-panel > .filter-row .quick-range {
grid-column: auto;
}
.quick-btn {
flex: 1;
}
}
/* AIDSO violet theme */
:root {
--aidso-primary: #7138f4;
--aidso-primary-dark: #5824d6;
--aidso-primary-soft: #f1eaff;
--aidso-ink: #242332;
--aidso-muted: #77778a;
--aidso-line: #e8e5ef;
--aidso-surface: rgba(255, 255, 255, 0.98);
--aidso-shadow: 0 8px 24px rgba(64, 48, 98, 0.055);
--aidso-shadow-hover: 0 14px 32px rgba(64, 48, 98, 0.09);
}
body {
background:
radial-gradient(circle at 88% 0%, rgba(113, 56, 244, 0.07), transparent 24%),
#eef3ff;
color: #282735;
}
.sidebar {
border-right: 1px solid #e6e5ed;
background: #fff;
box-shadow: 8px 0 24px rgba(56, 50, 76, 0.045);
}
.brand {
padding: 10px 6px;
border-color: transparent;
background: #fff;
box-shadow: none;
}
.nav-label {
color: #aaa7b5;
}
.nav-btn {
color: #646377;
}
.nav-btn::before {
border-color: #d7d3df;
color: #858194;
}
.nav-btn:hover {
border-color: transparent;
background: #f7f3ff;
color: var(--aidso-primary);
}
.nav-btn.active {
border-color: #e4d8ff;
background: linear-gradient(105deg, #eee5ff, #e7dcff);
color: #6d2df0;
box-shadow: 0 8px 18px rgba(113, 56, 244, 0.12);
}
.nav-btn.active::after {
background: #7138f4;
}
.nav-btn.active::before {
border-color: #bda4fa;
background: rgba(113, 56, 244, 0.08);
}
.sidebar-foot {
border-top-color: #eeecf2;
color: #92909e;
}
.health-dot {
background: #6fdaab;
box-shadow: 0 0 0 4px rgba(111, 218, 171, 0.14);
}
.title,
.section-title,
.card-value,
.third-metric-card strong {
color: #272533;
}
.right {
border-color: #e4e1ea;
background: rgba(255, 255, 255, 0.86);
}
.btn,
.quick-btn.active {
background: linear-gradient(135deg, #8247ff, #6825ed);
box-shadow: 0 7px 17px rgba(113, 56, 244, 0.22);
}
.btn:hover,
.quick-btn.active:hover {
background: linear-gradient(135deg, #7138f4, #581bd4);
box-shadow: 0 10px 22px rgba(113, 56, 244, 0.27);
}
.quick-btn:hover {
border-color: #e2d7fb;
background: #fff;
color: #7138f4;
}
.card,
.table-wrap,
.trend-panel,
.third-panel {
border-color: #e4e4eb;
}
.card::after,
.third-metric-card::after {
background: radial-gradient(circle, rgba(125, 66, 246, 0.12), transparent 68%);
}
.filter-row {
border-color: #e7e4ee;
background: linear-gradient(180deg, #faf9fc, #f7f6fa);
}
.field input,
.field select,
.custom-select-trigger {
border-color: #dcd9e3;
}
.field input:focus,
.field select:focus,
.custom-select.open .custom-select-trigger,
.custom-select-trigger:focus-visible {
border-color: #7138f4;
box-shadow: 0 0 0 3px rgba(113, 56, 244, 0.13);
}
.custom-select-option:hover {
background: #f3edff;
color: #6425e4;
}
.custom-select-option.selected {
background: linear-gradient(135deg, #8247ff, #6825ed);
}
.trend-chart {
border-color: #e8e5ef;
background: linear-gradient(180deg, #fdfcff, #faf9fc);
}
.daily-stacked-card,
.third-metric-card,
.third-section-card {
border-color: #e7e3ec;
}
.daily-stacked-system,
.third-ranking-bar-inner {
background: linear-gradient(90deg, #8b55f7, #7138f4);
}
.daily-stacked-third-party {
background: linear-gradient(90deg, #d3baff, #ae82ff);
}
.third-ranking-index {
background: #f1eaff;
color: #7138f4;
}
.third-trend-switch button.active,
.trend-switch-btn.active {
background: linear-gradient(135deg, #8247ff, #6825ed);
box-shadow: 0 4px 10px rgba(113, 56, 244, 0.2);
}
.daily-source-filter.active {
border-color: #e2d9f5;
color: #5d2bc6;
}
.loading-box {
border-color: #e5d9ff;
color: #7138f4;
}
.loading-spinner {
border-color: #e2d4ff;
border-top-color: #7138f4;
}
@media (max-width: 900px) {
.sidebar {
border-bottom: 1px solid #e7e4ed;
background: rgba(255, 255, 255, 0.97);
}
}
.card::after,
.third-metric-card::after {
display: none;
}
.logout-link {
display: inline-flex;
height: 40px;
align-items: center;
justify-content: center;
padding: 0 12px;
border-radius: 9px;
color: #77748a;
font-size: 13px;
font-weight: 700;
text-decoration: none;
transition: background 0.18s ease, color 0.18s ease;
}
.logout-link:hover {
background: #f3edff;
color: #6825ed;
}
.brand {
min-height: 68px;
padding: 5px 2px;
}
.brand-logo {
width: 100%;
max-width: none;
margin: 0;
transform: scale(1.28);
transform-origin: center;
}
@media (max-width: 900px) {
.brand {
min-height: 42px;
padding: 4px 2px;
}
.brand-logo {
width: 100%;
margin: 0;
transform: scale(1.14);
}
}
/* AIDSO unified visual system */
:root {
--aidso-primary: #3157d5;
--aidso-primary-dark: #2444b8;
--aidso-primary-soft: #eef3ff;
--aidso-ink: #14213d;
--aidso-muted: #70809a;
--aidso-line: #e2e8f2;
--aidso-surface: rgba(255, 255, 255, 0.96);
--aidso-shadow: 0 10px 32px rgba(35, 55, 95, 0.07);
--aidso-shadow-hover: 0 16px 40px rgba(35, 55, 95, 0.11);
}
body {
min-width: 320px;
background:
radial-gradient(circle at 92% 2%, rgba(79, 111, 215, 0.11), transparent 27%),
radial-gradient(circle at 30% 100%, rgba(83, 184, 171, 0.07), transparent 28%),
#f4f7fb;
color: var(--aidso-ink);
letter-spacing: 0.01em;
}
.layout {
grid-template-columns: 242px minmax(0, 1fr);
}
.sidebar {
position: sticky;
top: 0;
z-index: 30;
display: flex;
height: 100vh;
flex-direction: column;
padding: 24px 16px 18px;
overflow-y: auto;
border-right: 1px solid rgba(255, 255, 255, 0.06);
background:
radial-gradient(circle at 10% 0%, rgba(81, 113, 224, 0.24), transparent 31%),
linear-gradient(180deg, #17233a 0%, #111a2c 100%);
box-shadow: 12px 0 36px rgba(15, 23, 42, 0.1);
}
.brand {
display: flex;
align-items: center;
min-height: 58px;
margin: 0 6px 30px;
padding: 13px 14px;
border: 1px solid rgba(255, 255, 255, 0.16);
border-radius: 14px;
background: #fff;
box-shadow: 0 12px 28px rgba(4, 12, 27, 0.2);
}
.brand-logo {
display: block;
width: 100%;
height: auto;
object-fit: contain;
}
.brand-mark {
position: relative;
display: grid;
width: 42px;
height: 42px;
place-items: center;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 13px;
background: linear-gradient(145deg, #5679ee, #3157d5);
box-shadow: 0 10px 24px rgba(49, 87, 213, 0.35);
}
.brand-mark i {
position: absolute;
width: 7px;
border-radius: 999px;
background: #fff;
transform: rotate(38deg);
}
.brand-mark i:nth-child(1) {
height: 16px;
margin: 8px 0 0 -14px;
opacity: 0.7;
}
.brand-mark i:nth-child(2) {
height: 25px;
}
.brand-mark i:nth-child(3) {
height: 33px;
margin: -7px -16px 0 0;
}
.brand-copy {
display: flex;
flex-direction: column;
gap: 1px;
color: #fff;
}
.brand-copy strong {
font-size: 18px;
font-weight: 850;
letter-spacing: 0.08em;
}
.brand-copy small {
color: #aebbd2;
font-size: 12px;
font-weight: 650;
letter-spacing: 0.12em;
}
.nav-label {
margin: 0 10px 10px;
color: #71829f;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.14em;
}
.nav-list {
display: grid;
gap: 7px;
}
.nav-btn {
position: relative;
min-height: 48px;
margin: 0;
padding: 0 14px 0 46px;
overflow: hidden;
border-radius: 12px;
background: transparent;
color: #aebbd2;
font-size: 14px;
font-weight: 700;
}
.nav-btn::before {
position: absolute;
left: 16px;
display: grid;
width: 20px;
height: 20px;
place-items: center;
border: 1px solid rgba(174, 187, 210, 0.34);
border-radius: 6px;
color: currentColor;
font-size: 11px;
line-height: 1;
}
#queueNavBtn::before {
content: "Q";
}
#trendNavBtn::before {
content: "↗";
font-size: 13px;
}
#thirdNavBtn::before {
content: "3";
}
.nav-btn:hover {
border-color: transparent;
background: rgba(255, 255, 255, 0.06);
color: #f8fafc;
}
.nav-btn.active {
border-color: rgba(128, 155, 255, 0.24);
background: linear-gradient(105deg, #3157d5, #4268df);
color: #fff;
box-shadow: 0 10px 25px rgba(28, 56, 159, 0.32);
}
.nav-btn.active::after {
content: "";
position: absolute;
top: 12px;
bottom: 12px;
left: 0;
width: 3px;
border-radius: 0 4px 4px 0;
background: #a9c1ff;
}
.nav-btn.active::before {
border-color: rgba(255, 255, 255, 0.45);
background: rgba(255, 255, 255, 0.1);
}
.sidebar-foot {
display: flex;
align-items: center;
gap: 8px;
margin-top: auto;
padding: 14px 12px 4px;
border-top: 1px solid rgba(255, 255, 255, 0.07);
color: #8798b3;
font-size: 11px;
font-weight: 650;
}
.health-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #37c99a;
box-shadow: 0 0 0 4px rgba(55, 201, 154, 0.12);
}
.main {
padding: 28px clamp(22px, 2.5vw, 42px) 42px;
background: transparent;
}
.header {
min-height: 76px;
margin-bottom: 25px;
}
.title {
color: #111c34;
font-size: clamp(25px, 2vw, 32px);
font-weight: 850;
letter-spacing: -0.025em;
}
.desc {
margin-top: 7px;
color: var(--aidso-muted);
font-size: 13px;
}
.right {
padding: 5px 5px 5px 14px;
border: 1px solid rgba(219, 227, 239, 0.85);
border-radius: 13px;
background: rgba(255, 255, 255, 0.72);
box-shadow: 0 5px 18px rgba(35, 55, 95, 0.04);
backdrop-filter: blur(10px);
}
.last-time,
.section-note {
color: #7b8ba4;
font-weight: 600;
}
.btn,
.quick-btn {
min-height: 40px;
border-radius: 9px;
background: linear-gradient(135deg, #3d64dd, #2850cd);
box-shadow: 0 7px 16px rgba(49, 87, 213, 0.2);
}
.btn:hover,
.quick-btn:hover {
background: linear-gradient(135deg, #3157d5, #203fae);
box-shadow: 0 10px 20px rgba(49, 87, 213, 0.25);
transform: translateY(-1px);
}
.quick-btn {
border: 1px solid transparent;
background: transparent;
color: #63728a;
box-shadow: none;
}
.quick-btn:hover {
border-color: #d9e3f4;
background: #fff;
color: var(--aidso-primary);
box-shadow: 0 5px 14px rgba(35, 55, 95, 0.06);
}
.quick-btn.active {
border-color: transparent;
background: linear-gradient(135deg, #4268df, #3157d5);
box-shadow: 0 8px 18px rgba(49, 87, 213, 0.22);
}
.section-head {
margin: 12px 2px 13px;
}
.section-title {
color: #17233a;
font-size: 18px;
font-weight: 820;
letter-spacing: -0.01em;
}
.panel-card-head {
min-height: 54px;
margin: -16px -16px 16px;
padding: 0 18px;
border-bottom: 1px solid #e9e7ef;
background: linear-gradient(180deg, #fff, #fcfbfe);
}
.table-wrap > .panel-card-head {
margin: 0;
}
.table-wrap > .panel-card-head + table {
border-top: 0;
}
.summary {
gap: 14px;
margin-bottom: 26px;
}
.card,
.table-wrap,
.trend-panel,
.third-panel {
border-color: rgba(220, 228, 240, 0.9);
border-radius: 17px;
background: var(--aidso-surface);
box-shadow: var(--aidso-shadow);
}
.card {
position: relative;
min-height: 126px;
padding: 22px;
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card::after,
.third-metric-card::after {
content: "";
position: absolute;
top: -28px;
right: -25px;
width: 88px;
height: 88px;
border-radius: 50%;
background: radial-gradient(circle, rgba(75, 110, 224, 0.13), transparent 68%);
pointer-events: none;
}
.card:hover,
.third-metric-card:hover {
transform: translateY(-2px);
box-shadow: var(--aidso-shadow-hover);
}
.card-name {
margin-bottom: 13px;
color: #72819a;
font-size: 13px;
font-weight: 700;
}
.card-value {
color: #13203a;
font-size: 32px;
font-weight: 850;
letter-spacing: -0.035em;
}
.table-wrap {
overflow: auto;
}
table {
border-collapse: separate;
border-spacing: 0;
}
th {
border-bottom-color: #e8edf5;
background: #f7f9fc;
color: #62728c;
font-size: 12px;
font-weight: 800;
letter-spacing: 0.02em;
}
td {
border-bottom-color: #edf1f6;
color: #26354e;
font-size: 13px;
}
tbody tr {
transition: background 0.16s ease;
}
tbody tr:hover {
background: #f7faff;
}
tbody tr:last-child td {
border-bottom: 0;
}
.filter-row {
gap: 11px;
padding: 12px;
border-color: #e6ebf3;
border-radius: 13px;
background: linear-gradient(180deg, #f9fbfd, #f6f8fc);
}
.field {
gap: 7px;
}
.field label {
color: #75849b;
font-size: 12px;
font-weight: 750;
}
.field input,
.field select,
.custom-select-trigger {
border-color: #d8e0ec;
border-radius: 10px;
color: #2b3a54;
box-shadow: 0 2px 5px rgba(35, 55, 95, 0.025);
}
.custom-select-menu {
padding: 7px;
border-color: #dce4f0;
border-radius: 13px;
box-shadow: 0 20px 48px rgba(30, 45, 78, 0.17);
}
.custom-select-option {
border-radius: 8px;
color: #3a4962;
font-size: 13px;
}
.custom-select-option.selected {
background: linear-gradient(135deg, #4268df, #3157d5);
}
.trend-panel,
.third-panel {
padding: 16px;
}
.trend-chart {
border-color: #e6ebf3;
background: linear-gradient(180deg, #fbfcfe, #f7f9fc);
}
.daily-stacked-card,
.third-metric-card,
.third-section-card {
border-color: #e1e7f0;
border-radius: 14px;
box-shadow: 0 7px 20px rgba(35, 55, 95, 0.045);
}
.daily-stacked-card,
.third-section-card {
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.daily-stacked-card:hover,
.third-section-card:hover {
border-color: #d3ddec;
box-shadow: 0 11px 28px rgba(35, 55, 95, 0.07);
}
.daily-stacked-head,
.third-card-head {
min-height: 55px;
border-bottom-color: #e9eef5;
background: linear-gradient(180deg, #fff, #fbfcfe);
}
.daily-stacked-head strong,
.third-card-head strong {
color: #1b2942;
}
.daily-stacked-track,
.third-ranking-bar {
background: #edf1f7;
}
.daily-stacked-system,
.third-ranking-bar-inner {
background: linear-gradient(90deg, #5576df, #3157d5);
}
.daily-stacked-third-party {
background: linear-gradient(90deg, #b7d936, #9fc423);
}
.third-metrics {
gap: 12px;
}
.third-metric-card {
position: relative;
min-height: 112px;
padding: 18px;
overflow: hidden;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.third-metric-card span {
color: #72819a;
font-size: 12px;
}
.third-metric-card strong {
color: #14213d;
font-weight: 850;
letter-spacing: -0.025em;
}
.third-ranking-row {
transition: background 0.15s ease, transform 0.15s ease;
}
.third-ranking-row:hover {
background: #f4f7fd;
transform: translateX(2px);
}
.third-ranking-index {
background: var(--aidso-primary-soft);
color: var(--aidso-primary);
}
.third-trend-switch,
.trend-switch {
border-color: #dce3ee;
background: #f3f6fa;
}
.third-trend-switch button.active,
.trend-switch-btn.active {
background: linear-gradient(135deg, #4268df, #3157d5);
box-shadow: 0 4px 10px rgba(49, 87, 213, 0.18);
}
.third-table-scroll {
scrollbar-color: #c5d0e2 transparent;
scrollbar-width: thin;
}
.third-table-scroll::-webkit-scrollbar,
.third-ranking-list::-webkit-scrollbar,
.third-trend-scroll::-webkit-scrollbar,
.daily-stacked-grid::-webkit-scrollbar {
width: 7px;
height: 7px;
}
.third-table-scroll::-webkit-scrollbar-thumb,
.third-ranking-list::-webkit-scrollbar-thumb,
.third-trend-scroll::-webkit-scrollbar-thumb,
.daily-stacked-grid::-webkit-scrollbar-thumb {
border-radius: 999px;
background: #cad4e3;
}
.third-matrix-table th:first-child,
.third-matrix-table td:first-child {
background: #fff;
box-shadow: 1px 0 0 #e8edf5;
}
.third-matrix-table thead th:first-child {
background: #f7f9fc;
}
.status {
border-radius: 999px;
font-size: 11px;
font-weight: 800;
}
.loading-box {
border-color: #d9e4fb;
border-radius: 13px;
color: var(--aidso-primary);
box-shadow: 0 16px 36px rgba(35, 55, 95, 0.12);
}
.trend-loading-mask {
border-radius: 17px;
background: rgba(247, 249, 253, 0.78);
backdrop-filter: blur(5px);
}
.error {
border: 1px solid #fecaca;
border-radius: 11px;
box-shadow: 0 8px 20px rgba(153, 27, 27, 0.06);
}
@media (max-width: 900px) {
.layout {
grid-template-columns: 1fr;
}
.sidebar {
position: static;
display: grid;
height: auto;
grid-template-columns: auto 1fr;
align-items: center;
padding: 12px 14px;
}
.brand {
width: 142px;
min-height: 40px;
margin: 0 16px 0 0;
padding: 8px 10px;
border-radius: 10px;
}
.brand-mark {
width: 36px;
height: 36px;
border-radius: 11px;
}
.brand-copy small,
.nav-label,
.sidebar-foot {
display: none;
}
.nav-list {
display: flex;
justify-content: flex-end;
gap: 7px;
}
.nav-btn {
width: auto;
min-height: 40px;
padding: 0 12px;
white-space: nowrap;
}
.nav-btn::before,
.nav-btn.active::after {
display: none;
}
.main {
padding-top: 22px;
}
}
@media (max-width: 600px) {
.sidebar {
grid-template-columns: 1fr;
gap: 10px;
}
.brand {
width: 154px;
justify-content: center;
margin-right: 0;
justify-self: center;
}
.nav-list {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.nav-btn {
justify-content: center;
padding: 0 8px;
font-size: 12px;
}
.header {
align-items: stretch;
}
.right {
justify-content: space-between;
}
.last-time {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
/* Final AIDSO palette overrides */
:root {
--aidso-primary: #7138f4;
--aidso-primary-dark: #5824d6;
--aidso-primary-soft: #f1eaff;
--aidso-ink: #282735;
--aidso-muted: #77778a;
--aidso-line: #e8e5ef;
--aidso-shadow: 0 8px 24px rgba(64, 48, 98, 0.055);
--aidso-shadow-hover: 0 14px 32px rgba(64, 48, 98, 0.09);
}
body {
background:
radial-gradient(circle at 88% 0%, rgba(113, 56, 244, 0.07), transparent 24%),
#eef3ff;
}
.sidebar {
border-right-color: #e6e5ed;
background: #fff;
box-shadow: 8px 0 24px rgba(56, 50, 76, 0.045);
}
.brand {
border-color: transparent;
background: #fff;
box-shadow: none;
}
.nav-label {
color: #aaa7b5;
}
.nav-btn {
color: #646377;
}
.nav-btn::before {
border-color: #d7d3df;
color: #858194;
}
.nav-btn:hover {
background: #f7f3ff;
color: #7138f4;
}
.nav-btn.active {
border-color: #e4d8ff;
background: linear-gradient(105deg, #eee5ff, #e7dcff);
color: #6d2df0;
box-shadow: 0 8px 18px rgba(113, 56, 244, 0.12);
}
.nav-btn.active::after {
background: #7138f4;
}
.nav-btn.active::before {
border-color: #bda4fa;
background: rgba(113, 56, 244, 0.08);
}
.sidebar-foot {
border-top-color: #eeecf2;
color: #92909e;
}
.btn,
.quick-btn.active {
background: linear-gradient(135deg, #8247ff, #6825ed);
box-shadow: 0 7px 17px rgba(113, 56, 244, 0.22);
}
.btn:hover,
.quick-btn.active:hover {
background: linear-gradient(135deg, #7138f4, #581bd4);
box-shadow: 0 10px 22px rgba(113, 56, 244, 0.27);
}
.quick-btn:hover {
border-color: #e2d7fb;
color: #7138f4;
}
.card::after,
.third-metric-card::after {
background: radial-gradient(circle, rgba(125, 66, 246, 0.12), transparent 68%);
}
.filter-row {
border-color: #e7e4ee;
background: linear-gradient(180deg, #faf9fc, #f7f6fa);
}
.field input:focus,
.field select:focus,
.custom-select.open .custom-select-trigger,
.custom-select-trigger:focus-visible {
border-color: #7138f4;
box-shadow: 0 0 0 3px rgba(113, 56, 244, 0.13);
}
.custom-select-option:hover {
background: #f3edff;
color: #6425e4;
}
.custom-select-option.selected,
.third-trend-switch button.active,
.trend-switch-btn.active {
background: linear-gradient(135deg, #8247ff, #6825ed);
}
.daily-stacked-system,
.third-ranking-bar-inner {
background: linear-gradient(90deg, #8b55f7, #7138f4);
}
.daily-stacked-third-party {
background: linear-gradient(90deg, #d3baff, #ae82ff);
}
.third-ranking-index {
background: #f1eaff;
color: #7138f4;
}
.loading-box {
border-color: #e5d9ff;
color: #7138f4;
}
.loading-spinner {
border-color: #e2d4ff;
border-top-color: #7138f4;
}
@media (max-width: 900px) {
.sidebar {
border-bottom: 1px solid #e7e4ed;
background: rgba(255, 255, 255, 0.97);
} }
} }
const API_URL = "/api/queue/status"; const API_URL = "/api/queue/status";
const STATS_API_URL = "/api/queue/platformStats"; const STATS_API_URL = "/api/queue/platformStats";
const THIRD_STATS_API_URL = "/api/queue/thirdPartyStats";
const platforms = [ const platforms = [
"BDAI", "DB", "DOUBA", "DP", "DPA", "DYAI", "BDAI", "DB", "DOUBA", "DP", "DPA", "DYAI",
"KIMI", "TXYB", "TXYBA", "TYQW", "TYQWA", "WXYY", "XHSA" "KIMI", "TXYB", "TXYBA", "TYQW", "TYQWA", "WXYY", "XHSA"
]; ];
const PLATFORM_NAME_MAP = { const PLATFORM_NAME_MAP = {
XHSA: "小红书手机版" DOUBA: "豆包·手机版",
DB: "豆包·网页版",
DPA: "DeepSeek·手机版",
DP: "DeepSeek·网页版",
TYQWA: "千问·手机版",
TYQW: "千问·网页版",
TXYBA: "腾讯元宝·手机版",
TXYB: "腾讯元宝·网页版",
KIMI: "KIMI·网页版",
WXYY: "文心·网页版",
DYAI: "AI抖音·网页版",
BDAI: "百度AI·网页版",
XHSA: "红书问一问"
}; };
const PLATFORM_DISPLAY_ORDER = [
"豆包·手机版",
"豆包·网页版",
"千问·手机版",
"千问·网页版",
"DeepSeek·手机版",
"DeepSeek·网页版",
"腾讯元宝·手机版",
"腾讯元宝·网页版",
"KIMI·网页版",
"文心·网页版",
"百度AI·网页版",
"红书问一问",
"AI抖音·网页版"
];
let currentPanel = "queue"; let currentPanel = "queue";
let loadedPanels = {queue: false, trend: false}; let loadedPanels = {queue: false, trend: false, third: false};
let trendChartMode = "platform"; let trendChartMode = "platform";
let dailySourceMode = "all";
let lastTrendData = null; let lastTrendData = null;
let thirdTrendMode = "channel";
let lastThirdData = null;
function showPanel(panel) { function showPanel(panel) {
currentPanel = panel; currentPanel = panel;
document.getElementById("queuePanel").classList.toggle("active", panel === "queue"); document.getElementById("queuePanel").classList.toggle("active", panel === "queue");
document.getElementById("trendPanel").classList.toggle("active", panel === "trend"); document.getElementById("trendPanel").classList.toggle("active", panel === "trend");
document.getElementById("thirdPanel").classList.toggle("active", panel === "third");
document.getElementById("queueNavBtn").classList.toggle("active", panel === "queue"); document.getElementById("queueNavBtn").classList.toggle("active", panel === "queue");
document.getElementById("trendNavBtn").classList.toggle("active", panel === "trend"); document.getElementById("trendNavBtn").classList.toggle("active", panel === "trend");
document.getElementById("pageTitle").innerText = panel === "queue" document.getElementById("thirdNavBtn").classList.toggle("active", panel === "third");
? "GEO 队列监控面板" const pageMeta = {
: "GEO历史任务数量趋势"; queue: {
document.getElementById("pageDesc").innerText = panel === "queue" title: "队列积压监控",
? "查看各平台 stream_batch / batch 队列积压情况" desc: "查看各平台 stream_batch / batch 队列积压情况"
: "按时间范围查看各平台历史任务趋势"; },
trend: {
title: "GEO历史任务数量趋势",
desc: "按时间范围查看各平台历史任务趋势"
},
third: {
title: "三方用量详细分析",
desc: "查看当前启用渠道的任务量、平台分布与每日趋势"
}
};
document.getElementById("pageTitle").innerText = pageMeta[panel].title;
document.getElementById("pageDesc").innerText = pageMeta[panel].desc;
hideError(); hideError();
if (!loadedPanels[panel]) { if (!loadedPanels[panel]) {
...@@ -34,8 +77,10 @@ const API_URL = "/api/queue/status"; ...@@ -34,8 +77,10 @@ const API_URL = "/api/queue/status";
function refreshCurrentPanel() { function refreshCurrentPanel() {
if (currentPanel === "queue") { if (currentPanel === "queue") {
loadQueueStatus(); loadQueueStatus();
} else { } else if (currentPanel === "trend") {
loadPlatformStats(); loadPlatformStats();
} else {
loadThirdPartyStats();
} }
} }
...@@ -55,6 +100,10 @@ const API_URL = "/api/queue/status"; ...@@ -55,6 +100,10 @@ const API_URL = "/api/queue/status";
document.querySelector(".trend-panel").classList.toggle("loading", isLoading); document.querySelector(".trend-panel").classList.toggle("loading", isLoading);
} }
function setThirdLoading(isLoading) {
document.querySelector(".third-panel").classList.toggle("loading", isLoading);
}
function getNumClass(num) { function getNumClass(num) {
if (num === 0) { if (num === 0) {
return "num-ok"; return "num-ok";
...@@ -127,9 +176,16 @@ const API_URL = "/api/queue/status"; ...@@ -127,9 +176,16 @@ const API_URL = "/api/queue/status";
.map(item => ({ .map(item => ({
time: normalizeStatDate(item["时间"] || item.time || item.date || item.begin || ""), time: normalizeStatDate(item["时间"] || item.time || item.date || item.begin || ""),
platform: getPlatformDisplayName(item["平台"] || item.platform || item.name || "未知平台"), platform: getPlatformDisplayName(item["平台"] || item.platform || item.name || "未知平台"),
count: Number(item["数量"] || item.count || item.value || 0) count: Number(item["数量"] || item.count || item.value || 0),
systemCount: Number(item.system_count || 0),
thirdPartyCount: Number(item.third_party_count || 0)
})) }))
.filter(item => item.time && Number.isFinite(item.count)); .filter(item => (
item.time
&& Number.isFinite(item.count)
&& Number.isFinite(item.systemCount)
&& Number.isFinite(item.thirdPartyCount)
));
} }
function getPlatformDisplayName(platform) { function getPlatformDisplayName(platform) {
...@@ -146,11 +202,178 @@ const API_URL = "/api/queue/status"; ...@@ -146,11 +202,178 @@ const API_URL = "/api/queue/status";
return el; return el;
} }
function renderStackedPlatformChart(chart, labels, platformList, sourcePointMap) {
const systemColor = "#7138f4";
const thirdPartyColor = "#b4db2e";
const getDisplayValue = source => {
const systemCount = Number(source.systemCount || 0);
const thirdPartyCount = Number(source.thirdPartyCount || 0);
if (dailySourceMode === "system") {
return systemCount;
}
if (dailySourceMode === "thirdParty") {
return thirdPartyCount;
}
return systemCount + thirdPartyCount;
};
const maxValue = Math.max(
...platformList.flatMap(platform => labels.map(label => {
const source = sourcePointMap.get(`${platform}__${label}`) || {};
return getDisplayValue(source);
})),
1
);
const grid = document.createElement("div");
const legend = document.createElement("div");
const systemButton = document.createElement("button");
const thirdPartyButton = document.createElement("button");
grid.className = "daily-stacked-grid";
legend.className = "daily-stacked-legend";
systemButton.className = (
"daily-source-filter "
+ (dailySourceMode !== "thirdParty" ? "active" : "")
);
thirdPartyButton.className = (
"daily-source-filter "
+ (dailySourceMode !== "system" ? "active" : "")
);
systemButton.innerHTML = (
`<i style="background:${systemColor}"></i>系统用户`
);
thirdPartyButton.innerHTML = (
`<i style="background:${thirdPartyColor}"></i>三方用户`
);
systemButton.onclick = () => setDailySourceMode("system");
thirdPartyButton.onclick = () => setDailySourceMode("thirdParty");
legend.appendChild(systemButton);
legend.appendChild(thirdPartyButton);
chart.appendChild(legend);
[...labels].reverse().forEach(label => {
const card = document.createElement("section");
const head = document.createElement("div");
const date = document.createElement("strong");
const total = document.createElement("span");
const list = document.createElement("div");
const dayTotal = platformList.reduce((sum, platform) => {
const source = sourcePointMap.get(`${platform}__${label}`) || {};
return sum + getDisplayValue(source);
}, 0);
card.className = "daily-stacked-card";
head.className = "daily-stacked-head";
date.textContent = label;
total.textContent = `总量 ${dayTotal}`;
list.className = "daily-stacked-list";
head.appendChild(date);
head.appendChild(total);
card.appendChild(head);
platformList.forEach(platform => {
const source = sourcePointMap.get(`${platform}__${label}`) || {};
const systemCount = Number(source.systemCount || 0);
const thirdPartyCount = Number(source.thirdPartyCount || 0);
const rowTotal = getDisplayValue(source);
const row = document.createElement("div");
const nameWrap = document.createElement("div");
const name = document.createElement("div");
const sourceNumbers = document.createElement("div");
const track = document.createElement("div");
const stack = document.createElement("div");
const systemBar = document.createElement("span");
const thirdPartyBar = document.createElement("span");
const value = document.createElement("strong");
const totalWidth = (rowTotal / maxValue) * 100;
const systemWidth = dailySourceMode === "thirdParty"
? 0
: (
dailySourceMode === "system"
? 100
: (rowTotal ? (systemCount / rowTotal) * 100 : 0)
);
const thirdPartyWidth = dailySourceMode === "system"
? 0
: (
dailySourceMode === "thirdParty"
? 100
: (rowTotal ? (thirdPartyCount / rowTotal) * 100 : 0)
);
row.className = "daily-stacked-row";
nameWrap.className = "daily-stacked-name-wrap";
name.className = "daily-stacked-name";
sourceNumbers.className = "daily-stacked-source-numbers";
track.className = "daily-stacked-track";
stack.className = "daily-stacked-bar";
systemBar.className = "daily-stacked-system";
thirdPartyBar.className = "daily-stacked-third-party";
value.className = "daily-stacked-value";
name.textContent = platform;
if (dailySourceMode === "system") {
sourceNumbers.innerHTML = (
`<span>系统 ${systemCount}</span>`
);
} else if (dailySourceMode === "thirdParty") {
sourceNumbers.innerHTML = (
`<span>三方 ${thirdPartyCount}</span>`
);
} else {
sourceNumbers.innerHTML = `
<span>系统 ${systemCount}</span>
<span>三方 ${thirdPartyCount}</span>
`;
}
stack.style.width = `${totalWidth}%`;
systemBar.style.width = `${systemWidth}%`;
thirdPartyBar.style.width = `${thirdPartyWidth}%`;
stack.title = (
`${platform} ${label}:`
+ `系统用户 ${systemCount},`
+ `三方用户 ${thirdPartyCount},`
+ `合计 ${rowTotal}`
);
value.textContent = rowTotal;
nameWrap.appendChild(name);
nameWrap.appendChild(sourceNumbers);
stack.appendChild(systemBar);
stack.appendChild(thirdPartyBar);
track.appendChild(stack);
row.appendChild(nameWrap);
row.appendChild(track);
row.appendChild(value);
list.appendChild(row);
});
card.appendChild(list);
grid.appendChild(card);
});
chart.appendChild(grid);
}
function setDailySourceMode(mode) {
dailySourceMode = dailySourceMode === mode ? "all" : mode;
if (!lastTrendData) {
return;
}
const chart = document.getElementById("platformTrendChart");
chart.innerHTML = "";
renderStackedPlatformChart(
chart,
lastTrendData.labels,
lastTrendData.platformList,
lastTrendData.sourcePointMap
);
}
function renderPlatformTrend(responseData) { function renderPlatformTrend(responseData) {
const chart = document.getElementById("platformTrendChart"); const chart = document.getElementById("platformTrendChart");
const summary = document.getElementById("trendSummary"); const summary = document.getElementById("trendSummary");
const statsNote = document.getElementById("statsNote"); const statsNote = document.getElementById("statsNote");
const colors = ["#2563eb", "#16a34a", "#f59e0b", "#dc2626", "#7c3aed", "#0891b2", "#db2777", "#475569"]; const colors = ["#7138f4", "#16a34a", "#f59e0b", "#dc2626", "#9b5cff", "#0891b2", "#db2777", "#475569"];
const items = normalizeStats(responseData); const items = normalizeStats(responseData);
chart.innerHTML = ""; chart.innerHTML = "";
...@@ -163,22 +386,42 @@ const API_URL = "/api/queue/status"; ...@@ -163,22 +386,42 @@ const API_URL = "/api/queue/status";
} }
const chartLabels = [...new Set(items.map(item => item.time))].sort(); const chartLabels = [...new Set(items.map(item => item.time))].sort();
const cardLabels = [...chartLabels].reverse();
const oldestDate = chartLabels[0]; const oldestDate = chartLabels[0];
const newestDate = chartLabels[chartLabels.length - 1]; const newestDate = chartLabels[chartLabels.length - 1];
const platformTotals = new Map(); const platformTotals = new Map();
const pointMap = new Map(); const pointMap = new Map();
const sourcePointMap = new Map();
items.forEach(item => { items.forEach(item => {
platformTotals.set(item.platform, (platformTotals.get(item.platform) || 0) + item.count); platformTotals.set(item.platform, (platformTotals.get(item.platform) || 0) + item.count);
const pointKey = `${item.platform}__${item.time}`; const pointKey = `${item.platform}__${item.time}`;
pointMap.set(pointKey, (pointMap.get(pointKey) || 0) + item.count); pointMap.set(pointKey, (pointMap.get(pointKey) || 0) + item.count);
const sourcePoint = sourcePointMap.get(pointKey) || {
systemCount: 0,
thirdPartyCount: 0
};
sourcePoint.systemCount += item.systemCount;
sourcePoint.thirdPartyCount += item.thirdPartyCount;
sourcePointMap.set(pointKey, sourcePoint);
}); });
const platformList = [...platformTotals.entries()] const platformList = [...platformTotals.entries()]
.sort((a, b) => b[1] - a[1]) .sort((a, b) => {
const aIndex = PLATFORM_DISPLAY_ORDER.indexOf(a[0]);
const bIndex = PLATFORM_DISPLAY_ORDER.indexOf(b[0]);
const normalizedAIndex = aIndex === -1
? Number.MAX_SAFE_INTEGER
: aIndex;
const normalizedBIndex = bIndex === -1
? Number.MAX_SAFE_INTEGER
: bIndex;
if (normalizedAIndex !== normalizedBIndex) {
return normalizedAIndex - normalizedBIndex;
}
return b[1] - a[1];
})
.map(([platform]) => platform); .map(([platform]) => platform);
const maxCount = Math.max(...pointMap.values(), 1);
const platformColorMap = new Map(platformList.map((platform, index) => [platform, colors[index % colors.length]])); const platformColorMap = new Map(platformList.map((platform, index) => [platform, colors[index % colors.length]]));
const dayTotals = chartLabels.map(label => ({ const dayTotals = chartLabels.map(label => ({
date: label, date: label,
...@@ -189,65 +432,17 @@ const API_URL = "/api/queue/status"; ...@@ -189,65 +432,17 @@ const API_URL = "/api/queue/status";
labels: chartLabels, labels: chartLabels,
platformList, platformList,
pointMap, pointMap,
sourcePointMap,
platformColorMap, platformColorMap,
dayTotals dayTotals
}; };
const grid = document.createElement("div"); renderStackedPlatformChart(
grid.className = "daily-grid"; chart,
chartLabels,
cardLabels.forEach(label => { platformList,
const dayItems = platformList.map(platform => ({ sourcePointMap
platform, );
value: pointMap.get(`${platform}__${label}`) || 0,
color: platformColorMap.get(platform)
}));
const dayTotal = dayItems.reduce((sum, item) => sum + item.value, 0);
const card = document.createElement("div");
const head = document.createElement("div");
const date = document.createElement("div");
const total = document.createElement("div");
const list = document.createElement("div");
card.className = "day-card";
head.className = "day-card-head";
date.className = "day-date";
total.className = "day-total";
list.className = "day-list";
date.innerText = label;
total.innerText = `总量 ${dayTotal}`;
head.appendChild(date);
head.appendChild(total);
dayItems.forEach(item => {
const row = document.createElement("div");
const name = document.createElement("div");
const bar = document.createElement("div");
const barInner = document.createElement("div");
const count = document.createElement("div");
row.className = "day-row";
name.className = "day-platform";
bar.className = "day-bar";
barInner.className = "day-bar-inner";
count.className = "day-count";
name.innerText = item.platform;
barInner.style.width = `${Math.max(2, Math.round((item.value / maxCount) * 100))}%`;
barInner.style.background = item.color;
count.innerText = item.value;
bar.appendChild(barInner);
row.appendChild(name);
row.appendChild(bar);
row.appendChild(count);
list.appendChild(row);
});
card.appendChild(head);
card.appendChild(list);
grid.appendChild(card);
});
chart.appendChild(grid);
renderTrendSummary(); renderTrendSummary();
statsNote.innerText = `共 ${platformList.length} 个平台,${oldestDate}${newestDate}`; statsNote.innerText = `共 ${platformList.length} 个平台,${oldestDate}${newestDate}`;
document.getElementById("lastTime").innerText = "最后刷新:" + new Date().toLocaleString(); document.getElementById("lastTime").innerText = "最后刷新:" + new Date().toLocaleString();
...@@ -263,7 +458,7 @@ const API_URL = "/api/queue/status"; ...@@ -263,7 +458,7 @@ const API_URL = "/api/queue/status";
const series = trendChartMode === "total" const series = trendChartMode === "total"
? [{ ? [{
name: "总量", name: "总量",
color: "#2563eb", color: "#7138f4",
values: lastTrendData.dayTotals.map(item => item.total) values: lastTrendData.dayTotals.map(item => item.total)
}] }]
: lastTrendData.platformList.map(platform => ({ : lastTrendData.platformList.map(platform => ({
...@@ -300,7 +495,7 @@ const API_URL = "/api/queue/status"; ...@@ -300,7 +495,7 @@ const API_URL = "/api/queue/status";
platformBtn.className = `trend-switch-btn ${trendChartMode === "platform" ? "active" : ""}`; platformBtn.className = `trend-switch-btn ${trendChartMode === "platform" ? "active" : ""}`;
totalBtn.className = `trend-switch-btn ${trendChartMode === "total" ? "active" : ""}`; totalBtn.className = `trend-switch-btn ${trendChartMode === "total" ? "active" : ""}`;
title.innerText = titleText; title.innerText = titleText;
range.innerText = `${lastTrendData.labels[lastTrendData.labels.length - 1]}${lastTrendData.labels[0]}`; range.innerText = `${lastTrendData.labels[0]}${lastTrendData.labels[lastTrendData.labels.length - 1]}`;
platformBtn.innerText = "平台"; platformBtn.innerText = "平台";
totalBtn.innerText = "总量"; totalBtn.innerText = "总量";
platformBtn.onclick = () => setTrendChartMode("platform"); platformBtn.onclick = () => setTrendChartMode("platform");
...@@ -584,6 +779,7 @@ const API_URL = "/api/queue/status"; ...@@ -584,6 +779,7 @@ const API_URL = "/api/queue/status";
hideError(); hideError();
statsNote.innerText = "加载中"; statsNote.innerText = "加载中";
trendChartMode = "platform"; trendChartMode = "platform";
dailySourceMode = "all";
setTrendLoading(true); setTrendLoading(true);
try { try {
...@@ -597,7 +793,560 @@ const API_URL = "/api/queue/status"; ...@@ -597,7 +793,560 @@ const API_URL = "/api/queue/status";
} }
} }
function formatNumber(value) {
return Number(value || 0).toLocaleString("zh-CN");
}
function setThirdQuickRange(days, shouldLoad = true) {
const beginInput = document.getElementById("thirdBeginTime");
const endInput = document.getElementById("thirdEndTime");
const now = new Date();
const start = new Date(now);
start.setDate(start.getDate() - days + 1);
start.setHours(0, 0, 0, 0);
beginInput.value = toDateInputValue(start);
endInput.value = toDateInputValue(now);
[3, 7, 30].forEach(rangeDays => {
document.getElementById(`thirdRange${rangeDays}Btn`).classList.toggle(
"active",
days === rangeDays
);
});
if (shouldLoad && currentPanel === "third") {
loadThirdPartyStats();
} else if (shouldLoad) {
loadedPanels.third = false;
}
}
function clearThirdQuickRangeActive() {
[3, 7, 30].forEach(days => {
document.getElementById(`thirdRange${days}Btn`).classList.remove("active");
});
}
function buildThirdStatsUrl() {
const begin = document.getElementById("thirdBeginTime").value;
const end = document.getElementById("thirdEndTime").value;
const channel = document.getElementById("thirdChannelFilter").value;
const platform = document.getElementById("thirdPlatformFilter").value;
if (!begin || !end) {
throw new Error("请选择开始时间和结束时间");
}
if (begin > end) {
throw new Error("开始时间不能晚于结束时间");
}
return `${THIRD_STATS_API_URL}?${new URLSearchParams({
begin,
end,
channel,
platform
}).toString()}`;
}
function refreshCustomSelect(select) {
const custom = select._customSelect;
if (!custom) {
return;
}
custom.menu.innerHTML = "";
Array.from(select.options).forEach(option => {
const item = document.createElement("button");
item.type = "button";
item.className = "custom-select-option";
item.textContent = option.textContent;
item.dataset.value = option.value;
item.classList.toggle("selected", option.value === select.value);
item.setAttribute(
"aria-selected",
option.value === select.value ? "true" : "false"
);
item.onclick = () => {
if (select.value !== option.value) {
select.value = option.value;
select.dispatchEvent(new Event("change", {bubbles: true}));
}
refreshCustomSelect(select);
custom.wrapper.classList.remove("open");
custom.trigger.setAttribute("aria-expanded", "false");
custom.trigger.focus();
};
custom.menu.appendChild(item);
});
const selectedOption = select.options[select.selectedIndex];
custom.value.textContent = selectedOption
? selectedOption.textContent
: "请选择";
}
function initCustomSelect(select) {
if (select._customSelect) {
refreshCustomSelect(select);
return;
}
const wrapper = document.createElement("div");
const trigger = document.createElement("button");
const value = document.createElement("span");
const arrow = document.createElement("span");
const menu = document.createElement("div");
wrapper.className = "custom-select";
trigger.type = "button";
trigger.className = "custom-select-trigger";
trigger.setAttribute("aria-haspopup", "listbox");
trigger.setAttribute("aria-expanded", "false");
value.className = "custom-select-value";
arrow.className = "custom-select-arrow";
menu.className = "custom-select-menu";
menu.setAttribute("role", "listbox");
trigger.appendChild(value);
trigger.appendChild(arrow);
wrapper.appendChild(trigger);
wrapper.appendChild(menu);
select.insertAdjacentElement("afterend", wrapper);
select.classList.add("native-select-hidden");
select._customSelect = {wrapper, trigger, value, menu};
trigger.onclick = event => {
event.stopPropagation();
document.querySelectorAll(".custom-select.open").forEach(item => {
if (item !== wrapper) {
item.classList.remove("open");
item.querySelector(".custom-select-trigger")
?.setAttribute("aria-expanded", "false");
}
});
const isOpen = wrapper.classList.toggle("open");
trigger.setAttribute("aria-expanded", isOpen ? "true" : "false");
};
trigger.onkeydown = event => {
if (event.key === "Escape") {
wrapper.classList.remove("open");
trigger.setAttribute("aria-expanded", "false");
}
};
refreshCustomSelect(select);
}
function updateThirdFilterOptions(data) {
const channelSelect = document.getElementById("thirdChannelFilter");
const platformSelect = document.getElementById("thirdPlatformFilter");
const selectedChannel = channelSelect.value;
const selectedPlatform = platformSelect.value;
channelSelect.innerHTML = '<option value="">全部渠道</option>';
(data.channels || []).forEach(item => {
const option = document.createElement("option");
option.value = item.channel;
option.textContent = item.remark || "-";
channelSelect.appendChild(option);
});
channelSelect.value = selectedChannel;
platformSelect.innerHTML = '<option value="">全部平台</option>';
platforms.forEach(platform => {
const option = document.createElement("option");
option.value = platform;
option.textContent = getPlatformDisplayName(platform);
platformSelect.appendChild(option);
});
platformSelect.value = selectedPlatform;
refreshCustomSelect(channelSelect);
refreshCustomSelect(platformSelect);
}
function renderThirdMetrics(metrics) {
document.getElementById("thirdTotalMetric").textContent = formatNumber(metrics.third_total);
document.getElementById("thirdDailyAverageMetric").textContent = formatNumber(metrics.daily_average);
document.getElementById("thirdRatioMetric").textContent = `${Number(metrics.third_ratio || 0).toFixed(2)}%`;
document.getElementById("thirdShareMetric").textContent = formatNumber(metrics.share_total);
document.getElementById("thirdEnabledMetric").textContent = formatNumber(metrics.enabled_channel_count);
}
function renderThirdRanking(items) {
const container = document.getElementById("thirdRanking");
const maxValue = Math.max(...items.map(item => Number(item.task_count || 0)), 1);
container.innerHTML = "";
if (!items.length) {
container.innerHTML = '<div class="third-empty">暂无渠道用量</div>';
return;
}
const list = document.createElement("div");
list.className = "third-ranking-list";
items.forEach((item, index) => {
const row = document.createElement("button");
const rank = document.createElement("span");
const name = document.createElement("span");
const bar = document.createElement("span");
const barInner = document.createElement("span");
const count = document.createElement("strong");
row.className = "third-ranking-row";
rank.className = "third-ranking-index";
name.className = "third-ranking-name";
bar.className = "third-ranking-bar";
barInner.className = "third-ranking-bar-inner";
count.className = "third-ranking-count";
rank.textContent = index + 1;
name.textContent = item.remark || "-";
barInner.style.width = `${(Number(item.task_count || 0) / maxValue) * 100}%`;
count.textContent = formatNumber(item.task_count);
bar.appendChild(barInner);
row.appendChild(rank);
row.appendChild(name);
row.appendChild(bar);
row.appendChild(count);
row.onclick = () => {
document.getElementById("thirdChannelFilter").value = item.channel;
loadThirdPartyStats();
};
list.appendChild(row);
});
container.appendChild(list);
}
function getThirdDateLabels() {
const begin = document.getElementById("thirdBeginTime").value;
const end = document.getElementById("thirdEndTime").value;
const labels = [];
const current = new Date(`${begin}T00:00:00`);
const endDate = new Date(`${end}T00:00:00`);
while (current <= endDate) {
labels.push(toDateInputValue(current));
current.setDate(current.getDate() + 1);
}
return labels;
}
function normalizeThirdPt(value) {
const text = String(value || "");
return text.length === 8
? `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}`
: text.slice(0, 10);
}
function renderThirdTrend() {
const chart = document.getElementById("thirdTrendChart");
chart.innerHTML = "";
if (!lastThirdData) {
return;
}
const selectedChannel = document.getElementById("thirdChannelFilter").value;
const channels = selectedChannel
? [selectedChannel]
: (lastThirdData.channels || []).map(item => item.channel);
const channelRemarkMap = new Map(
(lastThirdData.channels || []).map(item => [
item.channel,
item.remark || "-"
])
);
const labels = getThirdDateLabels();
const dailyMap = new Map();
(lastThirdData.daily || []).forEach(item => {
dailyMap.set(
`${item.channel}__${normalizeThirdPt(item.pt)}`,
Number(item.count || 0)
);
});
const colors = [
"#7138f4", "#16a34a", "#f59e0b", "#dc2626",
"#7c3aed", "#0891b2", "#db2777", "#475569",
"#0f766e", "#9333ea", "#ea580c", "#4f46e5",
"#65a30d", "#be123c", "#0369a1", "#854d0e"
];
const series = thirdTrendMode === "total"
? [{
name: "每日总量",
color: "#7138f4",
values: labels.map(label => channels.reduce(
(sum, channel) => (
sum + (dailyMap.get(`${channel}__${label}`) || 0)
),
0
))
}]
: channels.map((channel, index) => ({
name: channelRemarkMap.get(channel) || "-",
color: colors[index % colors.length],
values: labels.map(label => dailyMap.get(`${channel}__${label}`) || 0)
}));
if (!series.length) {
chart.innerHTML = '<div class="third-empty">暂无趋势数据</div>';
return;
}
const legend = document.createElement("div");
const scroll = document.createElement("div");
const width = Math.max(chart.clientWidth - 28, labels.length * 90 + 90);
const height = 300;
const padding = {top: 24, right: 20, bottom: 42, left: 58};
const plotWidth = width - padding.left - padding.right;
const plotHeight = height - padding.top - padding.bottom;
const maxValue = Math.max(...series.flatMap(item => item.values), 1);
const xScale = index => labels.length > 1
? padding.left + plotWidth / (labels.length - 1) * index
: padding.left + plotWidth / 2;
const yScale = value => padding.top + plotHeight - value / maxValue * plotHeight;
const svg = createSvgEl("svg", {
viewBox: `0 0 ${width} ${height}`,
width,
height,
role: "img"
});
legend.className = "third-trend-legend";
scroll.className = "third-trend-scroll";
series.forEach(item => {
const legendItem = document.createElement("span");
legendItem.innerHTML = `<i style="background:${item.color}"></i>`;
legendItem.appendChild(document.createTextNode(item.name));
legend.appendChild(legendItem);
});
for (let index = 0; index <= 4; index++) {
const value = Math.round(maxValue / 4 * index);
const y = yScale(value);
svg.appendChild(createSvgEl("line", {
x1: padding.left,
y1: y,
x2: width - padding.right,
y2: y,
stroke: index === 0 ? "#cbd5e1" : "#e5e7eb",
"stroke-width": "1"
}));
const text = createSvgEl("text", {
x: padding.left - 9,
y: y + 4,
"text-anchor": "end",
class: "third-trend-label"
});
text.textContent = value;
svg.appendChild(text);
}
series.forEach(item => {
const points = item.values.map((value, index) => ({
value,
x: xScale(index),
y: yScale(value),
date: labels[index]
}));
svg.appendChild(createSvgEl("polyline", {
points: points.map(point => `${point.x},${point.y}`).join(" "),
fill: "none",
stroke: item.color,
"stroke-width": thirdTrendMode === "total" ? "3" : "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
}));
points.forEach(point => {
const circle = createSvgEl("circle", {
cx: point.x,
cy: point.y,
r: thirdTrendMode === "total" ? "4" : "3",
fill: item.color,
stroke: "#fff",
"stroke-width": "1.5"
});
const title = createSvgEl("title");
title.textContent = `${item.name} ${point.date}: ${point.value}`;
circle.appendChild(title);
svg.appendChild(circle);
});
});
labels.forEach((label, index) => {
const text = createSvgEl("text", {
x: xScale(index),
y: height - 13,
"text-anchor": "middle",
class: "third-trend-label"
});
text.textContent = label.slice(5);
svg.appendChild(text);
});
scroll.appendChild(svg);
chart.appendChild(legend);
chart.appendChild(scroll);
}
function setThirdTrendMode(mode) {
thirdTrendMode = mode;
document.getElementById("thirdTrendChannelBtn").classList.toggle("active", mode === "channel");
document.getElementById("thirdTrendTotalBtn").classList.toggle("active", mode === "total");
renderThirdTrend();
}
function renderThirdMatrix(data) {
const table = document.getElementById("thirdMatrixTable");
const selectedChannel = document.getElementById("thirdChannelFilter").value;
const channels = selectedChannel
? [selectedChannel]
: (data.channels || []).map(item => item.channel);
const platforms = [...(data.platforms || [])].sort((left, right) => {
const leftOrder = PLATFORM_DISPLAY_ORDER.indexOf(
getPlatformDisplayName(left)
);
const rightOrder = PLATFORM_DISPLAY_ORDER.indexOf(
getPlatformDisplayName(right)
);
return (
(leftOrder === -1 ? PLATFORM_DISPLAY_ORDER.length : leftOrder)
- (rightOrder === -1 ? PLATFORM_DISPLAY_ORDER.length : rightOrder)
);
});
const channelRemarkMap = new Map(
(data.channels || []).map(item => [
item.channel,
item.remark || "-"
])
);
const valueMap = new Map();
(data.matrix || []).forEach(item => {
valueMap.set(`${item.channel}__${item.platform}`, Number(item.count || 0));
});
const maxValue = Math.max(...valueMap.values(), 1);
table.innerHTML = "";
const thead = document.createElement("thead");
const headRow = document.createElement("tr");
const channelHead = document.createElement("th");
channelHead.textContent = "渠道";
headRow.appendChild(channelHead);
platforms.forEach(platform => {
const th = document.createElement("th");
th.textContent = getPlatformDisplayName(platform);
headRow.appendChild(th);
});
const totalHead = document.createElement("th");
totalHead.textContent = "合计";
headRow.appendChild(totalHead);
thead.appendChild(headRow);
table.appendChild(thead);
const tbody = document.createElement("tbody");
channels.forEach(channel => {
const row = document.createElement("tr");
const channelCell = document.createElement("td");
let rowTotal = 0;
channelCell.textContent = channelRemarkMap.get(channel) || "-";
row.appendChild(channelCell);
platforms.forEach(platform => {
const count = valueMap.get(`${channel}__${platform}`) || 0;
const cell = document.createElement("td");
rowTotal += count;
cell.textContent = formatNumber(count);
cell.style.background = `rgba(113, 56, 244, ${0.04 + count / maxValue * 0.52})`;
cell.style.color = count / maxValue > 0.58 ? "#fff" : "#1e293b";
row.appendChild(cell);
});
const totalCell = document.createElement("td");
totalCell.textContent = formatNumber(rowTotal);
totalCell.className = "third-matrix-total";
row.appendChild(totalCell);
tbody.appendChild(row);
});
table.appendChild(tbody);
}
function renderThirdDetails(items) {
const tbody = document.getElementById("thirdDetailBody");
tbody.innerHTML = "";
items.forEach(item => {
const row = document.createElement("tr");
[
item.remark || "-",
formatNumber(item.task_count),
`${Number(item.ratio || 0).toFixed(2)}%`,
formatNumber(item.share_count),
formatNumber(item.platform_count),
normalizeThirdPt(item.last_date) || "-",
formatNumber(item.daily_limit),
formatNumber(item.total_limit)
].forEach(value => {
const cell = document.createElement("td");
cell.textContent = value;
row.appendChild(cell);
});
tbody.appendChild(row);
});
}
function renderThirdPartyStats(responseData) {
const data = responseData && responseData.data
? responseData.data
: {};
lastThirdData = data;
updateThirdFilterOptions(data);
renderThirdMetrics(data.metrics || {});
renderThirdRanking(data.channel_ranking || []);
renderThirdTrend();
renderThirdMatrix(data);
renderThirdDetails(data.channel_details || []);
const labels = getThirdDateLabels();
document.getElementById("thirdStatsNote").textContent = (
`当前启用 ${Number((data.metrics || {}).enabled_channel_count || 0)} 个渠道,`
+ `${labels[0] || "-"}${labels[labels.length - 1] || "-"}`
);
document.getElementById("lastTime").innerText = "最后刷新:" + new Date().toLocaleString();
}
async function loadThirdPartyStats() {
const note = document.getElementById("thirdStatsNote");
hideError();
note.textContent = "加载中";
setThirdLoading(true);
try {
renderThirdPartyStats(await fetchJson(buildThirdStatsUrl()));
loadedPanels.third = true;
} catch (error) {
note.textContent = "加载失败";
showError("三方用量数据加载失败:" + error.message);
} finally {
setThirdLoading(false);
}
}
initCustomSelect(document.getElementById("thirdChannelFilter"));
initCustomSelect(document.getElementById("thirdPlatformFilter"));
document.addEventListener("click", event => {
if (!event.target.closest(".custom-select")) {
document.querySelectorAll(".custom-select.open").forEach(item => {
item.classList.remove("open");
item.querySelector(".custom-select-trigger")
?.setAttribute("aria-expanded", "false");
});
}
});
document.getElementById("beginTime").addEventListener("change", handleDateRangeChange); document.getElementById("beginTime").addEventListener("change", handleDateRangeChange);
document.getElementById("endTime").addEventListener("change", handleDateRangeChange); document.getElementById("endTime").addEventListener("change", handleDateRangeChange);
document.getElementById("thirdBeginTime").addEventListener("change", () => {
clearThirdQuickRangeActive();
loadThirdPartyStats();
});
document.getElementById("thirdEndTime").addEventListener("change", () => {
clearThirdQuickRangeActive();
loadThirdPartyStats();
});
document.getElementById("thirdChannelFilter").addEventListener("change", loadThirdPartyStats);
document.getElementById("thirdPlatformFilter").addEventListener("change", loadThirdPartyStats);
setQuickRange(3, false); setQuickRange(3, false);
setThirdQuickRange(3, false);
loadQueueStatus(); loadQueueStatus();
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AIDSO 数据监控 - 登录</title>
<link rel="stylesheet" href="{{ url_for('static', filename='dashboard/dashboard_login.css') }}">
</head>
<body>
<main class="login-page">
<section class="login-visual">
<div class="visual-content">
<div class="visual-badge">AIDSO DATA</div>
<h1>数据监控中心</h1>
<p>统一查看队列状态、历史任务趋势与三方渠道用量。</p>
<div class="visual-grid" aria-hidden="true">
<span></span><span></span><span></span>
<span></span><span></span><span></span>
</div>
</div>
</section>
<section class="login-panel">
<div class="login-card">
<img
class="login-logo"
src="{{ url_for('static', filename='dashboard/aidso_logo.png') }}"
alt="AIDSO 爱搜"
>
<div class="login-heading">
<h2>登录数据监控</h2>
<p>请输入账号和密码进入系统</p>
</div>
<form method="post" action="{{ url_for('dashboard.dashboard_login') }}">
<label class="login-field">
<span>账号</span>
<span class="input-shell">
<i class="input-icon user-icon" aria-hidden="true"></i>
<input
type="text"
name="username"
autocomplete="username"
placeholder="请输入账号"
required
autofocus
>
</span>
</label>
<label class="login-field">
<span>密码</span>
<span class="input-shell">
<i class="input-icon lock-icon" aria-hidden="true"></i>
<input
type="password"
name="password"
autocomplete="current-password"
placeholder="请输入密码"
required
>
</span>
</label>
{% if error_message %}
<div class="login-error" role="alert">{{ error_message }}</div>
{% endif %}
<button class="login-button" type="submit">登录</button>
</form>
<p class="login-footer">AIDSO 数据监控</p>
</div>
</section>
</main>
</body>
</html>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>GEO 队列监控面板</title> <title>AIDSO 数据监控</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='dashboard/queue_monitor.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='dashboard/queue_monitor.css') }}">
...@@ -11,20 +11,36 @@ ...@@ -11,20 +11,36 @@
<body> <body>
<div class="layout"> <div class="layout">
<aside class="sidebar"> <aside class="sidebar">
<div class="brand">
<img
class="brand-logo"
src="{{ url_for('static', filename='dashboard/aidso_logo.png') }}"
alt="AIDSO"
>
</div>
<div class="nav-label">数据看板</div>
<nav class="nav-list">
<button class="nav-btn active" id="queueNavBtn" onclick="showPanel('queue')">积压查看</button> <button class="nav-btn active" id="queueNavBtn" onclick="showPanel('queue')">积压查看</button>
<button class="nav-btn" id="trendNavBtn" onclick="showPanel('trend')">历史任务趋势</button> <button class="nav-btn" id="trendNavBtn" onclick="showPanel('trend')">历史任务趋势</button>
<button class="nav-btn" id="thirdNavBtn" onclick="showPanel('third')">三方用量分析</button>
</nav>
<div class="sidebar-foot">
<span class="health-dot"></span>
<span>数据服务运行中</span>
</div>
</aside> </aside>
<main class="main"> <main class="main">
<div class="header"> <div class="header">
<div> <div>
<div class="title" id="pageTitle">GEO 队列监控面板</div> <div class="title" id="pageTitle">队列积压监控</div>
<div class="desc" id="pageDesc">查看各平台 stream_batch / batch 队列积压情况</div> <div class="desc" id="pageDesc">查看各平台 stream_batch / batch 队列积压情况</div>
</div> </div>
<div class="right"> <div class="right">
<span class="last-time" id="lastTime">未刷新</span> <span class="last-time" id="lastTime">未刷新</span>
<button class="btn" onclick="refreshCurrentPanel()">刷新</button> <button class="btn" onclick="refreshCurrentPanel()">刷新</button>
<a class="logout-link" href="{{ url_for('dashboard.dashboard_logout') }}">退出</a>
</div> </div>
</div> </div>
...@@ -48,11 +64,11 @@ ...@@ -48,11 +64,11 @@
</div> </div>
</div> </div>
<div class="section-head"> <div class="table-wrap">
<div class="section-head panel-card-head">
<div class="section-title">队列积压详情</div> <div class="section-title">队列积压详情</div>
<div class="section-note">Redis 队列长度</div> <div class="section-note">Redis 队列长度</div>
</div> </div>
<div class="table-wrap">
<table> <table>
<thead> <thead>
<tr> <tr>
...@@ -69,11 +85,11 @@ ...@@ -69,11 +85,11 @@
</section> </section>
<section class="panel" id="trendPanel"> <section class="panel" id="trendPanel">
<div class="section-head"> <div class="trend-panel">
<div class="section-title">历史任务趋势</div> <div class="section-head panel-card-head">
<div class="section-title">历史任务概览</div>
<div class="section-note" id="statsNote">等待加载</div> <div class="section-note" id="statsNote">等待加载</div>
</div> </div>
<div class="trend-panel">
<div class="filter-row"> <div class="filter-row">
<div class="quick-range"> <div class="quick-range">
<button class="quick-btn active" id="range3Btn" onclick="setQuickRange(3)">近3天</button> <button class="quick-btn active" id="range3Btn" onclick="setQuickRange(3)">近3天</button>
...@@ -100,6 +116,111 @@ ...@@ -100,6 +116,111 @@
</div> </div>
</section> </section>
<section class="panel" id="thirdPanel">
<div class="third-panel">
<div class="section-head panel-card-head">
<div class="section-title">三方用量概览</div>
<div class="section-note" id="thirdStatsNote">等待加载</div>
</div>
<div class="filter-row">
<div class="quick-range">
<button class="quick-btn active" id="thirdRange3Btn" onclick="setThirdQuickRange(3)">近3天</button>
<button class="quick-btn" id="thirdRange7Btn" onclick="setThirdQuickRange(7)">近7天</button>
<button class="quick-btn" id="thirdRange30Btn" onclick="setThirdQuickRange(30)">近30天</button>
</div>
<div class="field">
<label for="thirdBeginTime">开始时间</label>
<input id="thirdBeginTime" type="date">
</div>
<div class="field">
<label for="thirdEndTime">结束时间</label>
<input id="thirdEndTime" type="date">
</div>
<div class="field">
<label for="thirdChannelFilter">渠道</label>
<select id="thirdChannelFilter">
<option value="">全部渠道</option>
</select>
</div>
<div class="field">
<label for="thirdPlatformFilter">平台</label>
<select id="thirdPlatformFilter">
<option value="">全部平台</option>
</select>
</div>
</div>
<div class="third-metrics">
<div class="third-metric-card"><span>三方任务总量</span><strong id="thirdTotalMetric">0</strong></div>
<div class="third-metric-card"><span>日均任务量</span><strong id="thirdDailyAverageMetric">0</strong></div>
<div class="third-metric-card"><span>三方任务占比</span><strong id="thirdRatioMetric">0%</strong></div>
<div class="third-metric-card"><span>三方分享任务</span><strong id="thirdShareMetric">0</strong></div>
<div class="third-metric-card"><span>当前启用渠道</span><strong id="thirdEnabledMetric">0</strong></div>
</div>
<div class="third-dashboard-grid">
<section class="third-section-card">
<div class="third-card-head">
<strong>渠道用量排行</strong>
<span>按任务量降序</span>
</div>
<div id="thirdRanking"></div>
</section>
<section class="third-section-card">
<div class="third-card-head">
<strong>三方每日用量趋势</strong>
<div class="third-trend-switch">
<button class="active" id="thirdTrendChannelBtn" onclick="setThirdTrendMode('channel')">按渠道</button>
<button id="thirdTrendTotalBtn" onclick="setThirdTrendMode('total')">每日总量</button>
</div>
</div>
<div class="third-trend-chart" id="thirdTrendChart"></div>
</section>
</div>
<section class="third-section-card">
<div class="third-card-head">
<strong>渠道 × 平台用量矩阵</strong>
<span>颜色越深代表任务量越高</span>
</div>
<div class="third-table-scroll">
<table class="third-matrix-table" id="thirdMatrixTable"></table>
</div>
</section>
<section class="third-section-card">
<div class="third-card-head">
<strong>渠道详细数据</strong>
<span>仅展示当前启用渠道</span>
</div>
<div class="third-table-scroll">
<table class="third-detail-table">
<thead>
<tr>
<th>渠道</th>
<th>任务量</th>
<th>三方用量占比</th>
<th>分享任务</th>
<th>平台数</th>
<th>最近使用</th>
<th>日额度</th>
<th>总额度</th>
</tr>
</thead>
<tbody id="thirdDetailBody"></tbody>
</table>
</div>
</section>
<div class="trend-loading-mask" id="thirdLoadingMask">
<div class="loading-box">
<span class="loading-spinner"></span>
<span>加载中</span>
</div>
</div>
</div>
</section>
<div class="error" id="errorBox"></div> <div class="error" id="errorBox"></div>
</main> </main>
</div> </div>
......
import json import json
from aidso_geo.models import spider_save_tos from aidso_geo.models import spider_save_tos
from aidso_geo.utils import robot_utils from aidso_geo.utils import robot_utils, bh_utils
from aidso_geo.utils.ai_interface import get_parse_sse_result from aidso_geo.utils.ai_interface import get_parse_sse_result
from aidso_geo.utils.tos_utils import get_string_from_tos from aidso_geo.utils.tos_utils import get_string_from_tos
...@@ -31,6 +31,7 @@ def douyin_ai_process_original_data(data): ...@@ -31,6 +31,7 @@ def douyin_ai_process_original_data(data):
# 提取并解析JSON数据 # 提取并解析JSON数据
data_str = item.split("data:")[1] data_str = item.split("data:")[1]
json_data = json.loads(data_str) json_data = json.loads(data_str)
except (IndexError, json.JSONDecodeError): except (IndexError, json.JSONDecodeError):
continue continue
...@@ -117,6 +118,30 @@ def douyin_ai_process_original_data(data): ...@@ -117,6 +118,30 @@ def douyin_ai_process_original_data(data):
return (file_path, search_keyword, url_list, think_content, response_content, suggestions) return (file_path, search_keyword, url_list, think_content, response_content, suggestions)
if __name__ == '__main__': if __name__ == '__main__':
file_path = "geo/8004e38c-37da-4e82-b08f-453ccdf0d661/DYAI/original.text" data_list = bh_utils.query_data(
douyin_ai_process_original_data(file_path) f"select * from geo_commit_task where taskId = '1cfb92e0-b0ae-40e5-b198-342d079dea0a' and platform = 'DYAI'")
def handle_item(i):
if i.get('comWordsMap'):
i['comWordsMap'] = json.loads(i.get('comWordsMap'))
if i.get('brandWords'):
i['brandWords'] = json.loads(i.get('brandWords'))
if i.get('comWords'):
i['comWords'] = json.loads(i.get('comWords'))
if i.get('keywords'):
i['keywords'] = json.loads(i.get('keywords'))
if i.get('productWordsMap'):
i['productWordsMap'] = json.loads(i.get('productWordsMap'))
return douyin_ai_process_original_data(i)
if data_list:
for i in data_list:
try:
handle_item(i)
except Exception as e:
...
...@@ -8,8 +8,50 @@ from aidso_geo.utils.ai_interface import get_parse_sse_result ...@@ -8,8 +8,50 @@ from aidso_geo.utils.ai_interface import get_parse_sse_result
from aidso_geo.utils.tos_utils import get_string_from_tos from aidso_geo.utils.tos_utils import get_string_from_tos
def qianwen_android_process_original_data(task_data): def process_source(source):
source_json = []
if isinstance(source, str):
try:
source_json = json.loads(source)
except json.JSONDecodeError:
source_json = []
if not isinstance(source_json, list):
source_json = []
url_content_list = []
if source_json:
for s in source_json:
normalized_url = s.get("normalized_url")
FIXED_URL = "https://www.amap.com"
url_process = {
"url": (
FIXED_URL
if not normalized_url and s.get("HostName") == "高德地图"
else normalized_url
),
"title": s.get("title"),
"summary": s.get("page_content"),
"publish_time": s.get("publish_time"),
"name": s.get("HostName"),
"icon": "",
"source_seq": '',
}
url_content_list.append(url_process)
result = [{
"source_seq": "",
"url": [
{
"type": "source",
"content": {
"list": url_content_list
},
}
]
}]
return result
def qianwen_android_process_original_data(task_data):
file_path = f'geo/{task_data["taskId"]}/{task_data["platform"]}/original.text' file_path = f'geo/{task_data["taskId"]}/{task_data["platform"]}/original.text'
url_list = [] url_list = []
url_list_batch = [] url_list_batch = []
...@@ -30,14 +72,14 @@ def qianwen_android_process_original_data(task_data): ...@@ -30,14 +72,14 @@ def qianwen_android_process_original_data(task_data):
try: try:
sse_json_data = json.loads(original_content) sse_json_data = json.loads(original_content)
# sse_json_data = json.loads(json_original.get('sse')) # sse_json_data = json.loads(json_original.get('sse'))
response_content = sse_json_data.get('reply_text','') response_content = sse_json_data.get('reply_text', '')
think_content = sse_json_data.get('deepthink_text','') think_content = sse_json_data.get('deepthink_text', '')
paa_answer = sse_json_data.get('paa_answer','') paa_answer = sse_json_data.get('paa_answer', '')
bar_sources = sse_json_data.get('bar_sources') bar_sources = sse_json_data.get('bar_sources')
amap_pois = sse_json_data.get('amap_pois') amap_pois = sse_json_data.get('amap_pois')
gaode_list = [] gaode_list = []
if amap_pois: if amap_pois:
for key,value in amap_pois.items(): for key, value in amap_pois.items():
for v in value: for v in value:
modelInput = v.get('future_data').get('modelInput') modelInput = v.get('future_data').get('modelInput')
biz_ext = v.get('biz_ext') biz_ext = v.get('biz_ext')
...@@ -71,7 +113,7 @@ def qianwen_android_process_original_data(task_data): ...@@ -71,7 +113,7 @@ def qianwen_android_process_original_data(task_data):
response_content += 'render_ecom_card_widget_gaode_end:\n' response_content += 'render_ecom_card_widget_gaode_end:\n'
source_content = {} source_content = {}
if bar_sources: if bar_sources:
source_content =next( source_content = next(
(item.get('content') for item in bar_sources if item.get('type') == 'source'), (item.get('content') for item in bar_sources if item.get('type') == 'source'),
{} {}
) )
...@@ -89,43 +131,43 @@ def qianwen_android_process_original_data(task_data): ...@@ -89,43 +131,43 @@ def qianwen_android_process_original_data(task_data):
if video_note_list: if video_note_list:
for i in video_note_list: for i in video_note_list:
rich_media = { rich_media = {
'zhidaye_id':i.get('zhidaye_id'), 'zhidaye_id': i.get('zhidaye_id'),
'title':i.get('title'), 'title': i.get('title'),
'cover':i.get('cover'), 'cover': i.get('cover'),
'url':i.get('url'), 'url': i.get('url'),
'author':i.get('author'), 'author': i.get('author'),
'publishTime':i.get('publishTime'), 'publishTime': i.get('publishTime'),
'duration':i.get('duration'), 'duration': i.get('duration'),
} }
video_list.append(rich_media) video_list.append(rich_media)
rich_media_block.append({ rich_media_block.append({
"url":video_list, "url": video_list,
"source_seq":'' "source_seq": ''
}) })
source_count_list = [] source_count_list = []
if source_list: if source_list:
for s in source_list: for s in source_list:
source_url = { source_url = {
'url':s.get('url'), 'url': s.get('url'),
'title':s.get('title'), 'title': s.get('title'),
'summary':s.get('summary'), 'summary': s.get('summary'),
'publish_time':s.get('publish_time'), 'publish_time': s.get('publish_time'),
'name':s.get('name'), 'name': s.get('name'),
'icon':s.get('icon'), 'icon': s.get('icon'),
'source_seq':s.get('source_seq',''), 'source_seq': s.get('source_seq', ''),
} }
source_count_list.append(source_url) source_count_list.append(source_url)
url_list.append({ url_list.append({
"url":[ "url": [
{ {
"type":"source", "type": "source",
"content":{ "content": {
"list":source_count_list "list": source_count_list
}, },
} }
], ],
"source_seq":'' "source_seq": ''
}) })
if paa_answer: if paa_answer:
...@@ -187,11 +229,13 @@ def qianwen_android_process_original_data(task_data): ...@@ -187,11 +229,13 @@ def qianwen_android_process_original_data(task_data):
if mime_type == 'bar/progress' and meta_data_type == 'bar_update': if mime_type == 'bar/progress' and meta_data_type == 'bar_update':
if messages[0].get('meta_data'): if messages[0].get('meta_data'):
if messages[0].get('meta_data').get('elements'): if messages[0].get('meta_data').get('elements'):
response_content+=messages[0].get('meta_data').get('elements')[0].get('content') response_content += messages[0].get('meta_data').get('elements')[0].get(
'content')
# 引用来源-无下标 # 引用来源-无下标
if mime_type == 'multi_load/iframe' : if mime_type == 'multi_load/iframe':
for i in multi_load: for i in multi_load:
if i.get('type') =='ref_source_inline' and i.get('content').get('status') =='complete': if i.get('type') == 'ref_source_inline' and i.get('content').get(
'status') == 'complete':
multi_load_content = i.get('content') multi_load_content = i.get('content')
multi_load_source_seq = i.get('source_seq') multi_load_source_seq = i.get('source_seq')
multi_load_content_query_list = multi_load_content.get('query_list') multi_load_content_query_list = multi_load_content.get('query_list')
...@@ -215,11 +259,12 @@ def qianwen_android_process_original_data(task_data): ...@@ -215,11 +259,12 @@ def qianwen_android_process_original_data(task_data):
"source_seq": '', "source_seq": '',
}) })
taobao_list = [] taobao_list = []
if mime_type == 'multi_load/iframe' and (multi_load_type == 'taoassistant_fold_product_feeds' or multi_load_type == 'taoassistant_single_product' or multi_load_type == 'taoassistant_single_product_v2') and status == 'complete': if mime_type == 'multi_load/iframe' and (
multi_load_type == 'taoassistant_fold_product_feeds' or multi_load_type == 'taoassistant_single_product' or multi_load_type == 'taoassistant_single_product_v2') and status == 'complete':
for mu in multi_load: for mu in multi_load:
if mu.get('type') == 'taoassistant_fold_product_feeds' : if mu.get('type') == 'taoassistant_fold_product_feeds':
source_seq = mu.get('source_seq') source_seq = mu.get('source_seq')
jump_url = next( jump_url = next(
...@@ -234,23 +279,26 @@ def qianwen_android_process_original_data(task_data): ...@@ -234,23 +279,26 @@ def qianwen_android_process_original_data(task_data):
query = next( query = next(
( (
item.get('params', {}).get('params', {}).get('queryList',[]) item.get('params', {}).get('params', {}).get('queryList', [])
for item in for item in
mu.get('content', {}).get('cardData', {}).get('clientActions', []) mu.get('content', {}).get('cardData', {}).get('clientActions', [])
if item.get('type') == 'loadmore' if item.get('type') == 'loadmore'
), ),
'' ''
) )
if isinstance(query,str): if isinstance(query, str):
if query: if query:
query = json.loads(query) query = json.loads(query)
if isinstance(query,list): if isinstance(query, list):
search_keyword.extend(query) search_keyword.extend(query)
if isinstance(mu.get('content').get('cardData').get('data').get('items'), list) and not taobao_list : if isinstance(mu.get('content').get('cardData').get('data').get('items'),
list) and not taobao_list:
cateTitle = mu.get('content').get('cardData').get('data').get('cateTitle') cateTitle = mu.get('content').get('cardData').get('data').get(
'cateTitle')
for pro in mu.get('content').get('cardData').get('data').get('items'): for pro in mu.get('content').get('cardData').get('data').get('items'):
price = (pro.get('priceShowWithIcon') or {}).get('price') or pro.get('itemPrice') or '' price = (pro.get('priceShowWithIcon') or {}).get(
'price') or pro.get('itemPrice') or ''
taobao_list.append({ taobao_list.append({
"title": cateTitle or pro.get('title', ''), "title": cateTitle or pro.get('title', ''),
"shop_name": next( "shop_name": next(
...@@ -268,17 +316,19 @@ def qianwen_android_process_original_data(task_data): ...@@ -268,17 +316,19 @@ def qianwen_android_process_original_data(task_data):
"source_seq": source_seq, "source_seq": source_seq,
"auctionURL": pro.get('auctionURL'), "auctionURL": pro.get('auctionURL'),
"item_id": pro.get('item_id'), "item_id": pro.get('item_id'),
'card_type':'fold_product' 'card_type': 'fold_product'
}) })
if mu.get('type') in ('taoassistant_single_product','taoassistant_single_product_v2'): if mu.get('type') in (
mu_content = mu.get('content',{}) 'taoassistant_single_product', 'taoassistant_single_product_v2'):
mu_content_cardData = mu_content.get('cardData',{}) mu_content = mu.get('content', {})
mu_content_cardData_data = mu_content_cardData.get('data',{}) mu_content_cardData = mu_content.get('cardData', {})
mu_content_cardData_data = mu_content_cardData.get('data', {})
if mu_content_cardData_data: if mu_content_cardData_data:
source_seq = mu_content.get('cardId') or mu_content.get('businessId') source_seq = mu_content.get('cardId') or mu_content.get('businessId')
price = (mu_content_cardData_data.get('priceShowWithIcon') or {}).get('price') or mu_content_cardData_data.get( price = (mu_content_cardData_data.get('priceShowWithIcon') or {}).get(
'price') or mu_content_cardData_data.get(
'itemPrice') or '' 'itemPrice') or ''
taobao_list.append({ taobao_list.append({
"title": mu_content_cardData_data.get('title'), "title": mu_content_cardData_data.get('title'),
...@@ -286,7 +336,8 @@ def qianwen_android_process_original_data(task_data): ...@@ -286,7 +336,8 @@ def qianwen_android_process_original_data(task_data):
( (
item.get('text', '') item.get('text', '')
for item in for item in
mu_content_cardData_data.get('structuredShopInfo', {}).get('infoList', []) mu_content_cardData_data.get('structuredShopInfo', {}).get(
'infoList', [])
if item.get('sourceType') == 'shop_name' if item.get('sourceType') == 'shop_name'
), ),
'' ''
...@@ -300,13 +351,12 @@ def qianwen_android_process_original_data(task_data): ...@@ -300,13 +351,12 @@ def qianwen_android_process_original_data(task_data):
'card_type': 'single_product' 'card_type': 'single_product'
}) })
if taobao_list: if taobao_list:
response_content += 'render_ecom_card_widget_taobao_start:' response_content += 'render_ecom_card_widget_taobao_start:'
damai_str = json.dumps(taobao_list, ensure_ascii=False) damai_str = json.dumps(taobao_list, ensure_ascii=False)
response_content += damai_str response_content += damai_str
response_content += 'render_ecom_card_widget_taobao_end:\n' response_content += 'render_ecom_card_widget_taobao_end:\n'
save_eco_data_to_bh(task_data,'taobao',taobao_list) save_eco_data_to_bh(task_data, 'taobao', taobao_list)
damai_list = [] damai_list = []
if mime_type == 'multi_load/iframe' and multi_load_type == 'damai_shows_list' and multi_load_status == 'complete': if mime_type == 'multi_load/iframe' and multi_load_type == 'damai_shows_list' and multi_load_status == 'complete':
...@@ -335,6 +385,7 @@ def qianwen_android_process_original_data(task_data): ...@@ -335,6 +385,7 @@ def qianwen_android_process_original_data(task_data):
'id': damai.get('data').get('id') 'id': damai.get('data').get('id')
}) })
damai_data_list.extend(damai_list) damai_data_list.extend(damai_list)
gaode_list = [] gaode_list = []
if mime_type == 'multi_load/iframe' and multi_load_type == 'new_amap_poi_list' and status == 'complete': if mime_type == 'multi_load/iframe' and multi_load_type == 'new_amap_poi_list' and status == 'complete':
for mu in multi_load: for mu in multi_load:
...@@ -369,7 +420,8 @@ def qianwen_android_process_original_data(task_data): ...@@ -369,7 +420,8 @@ def qianwen_android_process_original_data(task_data):
gaode_data_list.extend(gaode_list) gaode_data_list.extend(gaode_list)
if mime_type == 'multi_load/iframe' and multi_load_type == 'new_amap_poi_list' and( multi_load_status == 'complete'): if mime_type == 'multi_load/iframe' and multi_load_type == 'new_amap_poi_list' and (
multi_load_status == 'complete'):
for mu in multi_load: for mu in multi_load:
mu_content = mu.get('content') mu_content = mu.get('content')
mu_source_seq = mu.get('source_seq') mu_source_seq = mu.get('source_seq')
...@@ -386,15 +438,17 @@ def qianwen_android_process_original_data(task_data): ...@@ -386,15 +438,17 @@ def qianwen_android_process_original_data(task_data):
poi_id = mu_content_model_input.get("poi_id") poi_id = mu_content_model_input.get("poi_id")
gaode_result = { gaode_result = {
"url":f"https://www.amap.com/place/{poi_id}", "url": f"https://www.amap.com/place/{poi_id}",
"source_seq":mu_source_seq, "source_seq": mu_source_seq,
"name":mu_content_model_input.get("name"), "name": mu_content_model_input.get("name"),
"poi_id":poi_id, "poi_id": poi_id,
"cost":cost, "cost": cost,
"poi_summary":poi_summary, "poi_summary": poi_summary,
"distance_formatted":distance_formatted, "distance_formatted": distance_formatted,
"photos":photos[0].get("url", "") if isinstance(photos, list) and photos and isinstance(photos[0], dict) else "", "photos": photos[0].get("url", "") if isinstance(photos,
"address":distance_address, list) and photos and isinstance(
photos[0], dict) else "",
"address": distance_address,
} }
gaode_list.append(gaode_result) gaode_list.append(gaode_result)
gaode_data_list.extend(gaode_list) gaode_data_list.extend(gaode_list)
...@@ -413,6 +467,25 @@ def qianwen_android_process_original_data(task_data): ...@@ -413,6 +467,25 @@ def qianwen_android_process_original_data(task_data):
think_content += multi_load_content.get('think_content') think_content += multi_load_content.get('think_content')
# 引用来源 回答 视频列表 # 引用来源 回答 视频列表
if mime_type == 'multi_load/iframe' and status == 'complete': if mime_type == 'multi_load/iframe' and status == 'complete':
data_extra_info = data.get('extra_info')
if data_extra_info:
data_extra_info_chat_odps = data_extra_info.get('chat_odps')
if data_extra_info_chat_odps:
data_material_detail = data_extra_info_chat_odps.get('material_detail')
if data_material_detail:
data_material_detail_query = data_material_detail.get('query_info')
data_material_final_material_detail = data_material_detail.get(
'final_material_detail')
if data_material_final_material_detail:
url_list = process_source(data_material_final_material_detail)
if data_material_detail_query:
rewrite_querys = data_material_detail_query.get('rewrite_querys')
if isinstance(rewrite_querys, list):
search_keyword.extend(rewrite_querys)
response_content += content response_content += content
for mu in multi_load: for mu in multi_load:
if mu.get('type') == 'video_note_list': if mu.get('type') == 'video_note_list':
...@@ -473,17 +546,19 @@ def qianwen_android_process_original_data(task_data): ...@@ -473,17 +546,19 @@ def qianwen_android_process_original_data(task_data):
if __name__ == '__main__': if __name__ == '__main__':
#https://tcdn.aidso.com/geo/6fede9d0-ce6c-4c97-bcb8-43c499fc24e5/TYQWA/think.txt?secret=1104ed37df6abefbb2b454530b06515f0f59505d5751504e524855 # https://tcdn.aidso.com/geo/6fede9d0-ce6c-4c97-bcb8-43c499fc24e5/TYQWA/think.txt?secret=1104ed37df6abefbb2b454530b06515f0f59505d5751504e524855
# 6fede9d0-ce6c-4c97-bcb8-43c499fc24e5 # 6fede9d0-ce6c-4c97-bcb8-43c499fc24e5
# file_path2 = 'geo/alskdjhfasldjkfh/TYQWA/original.text' # file_path2 = 'geo/alskdjhfasldjkfh/TYQWA/original.text'
# file_path2 = 'geo/ef66d5f9-cadb-43f3-b8cf-824d75a81e7c/TYQWA/original.text' # file_path2 = 'geo/ef66d5f9-cadb-43f3-b8cf-824d75a81e7c/TYQWA/original.text'
# qianwen_android_process_original_data(file_path2) # qianwen_android_process_original_data(file_path2)
# fb8de08c16484143a7299c06ba20c90d 老版本 # 8a5d2d78-acca-4621-a913-1ff1527f8789
# e09e1b7db91f4d0382e86458d8462147 新版本 # 04b2b061-a5c0-4429-8d3b-d2b6504f1e8b
# data_list = bh_utils.query_data(f"select * from geo_commit_task where taskId = '0001e01d-a8ea-405e-acef-93e4f55abbff' and platform = 'TYQWA'") # data_list = bh_utils.query_data(f"select * from geo_commit_task where taskId = '0001e01d-a8ea-405e-acef-93e4f55abbff' and platform = 'TYQWA'")
data_list = bh_utils.query_data(f"select * from geo_commit_task where taskId = '53f0d16d0fa64d3e89c4b9af7aae86e8' and platform = 'TYQWA'") data_list = bh_utils.query_data(
f"select * from geo_commit_task where taskId = '9b82af12-97ba-47e0-aaca-4d88b93c70e9' and platform = 'TYQWA'")
def handle_item(i): def handle_item(i):
......
...@@ -265,12 +265,12 @@ def qianwen_android_process_quote(url_list): ...@@ -265,12 +265,12 @@ def qianwen_android_process_quote(url_list):
content = url.get('content') content = url.get('content')
if content: if content:
if isinstance(content.get('list'),list): if isinstance(content.get('list'),list):
for c in content.get('list'): for index2,c in enumerate(content.get('list'),start=1):
raw_data = { raw_data = {
"url": c.get('url', ''), "url": c.get('url', ''),
"title": c.get('title', ''), "title": c.get('title', ''),
"snippet": c.get('summary', ''), "snippet": c.get('summary', ''),
"index": index, "index": index2,
"published_at": c.get('publish_time', ''), "published_at": c.get('publish_time', ''),
"site_name": c.get('name', ''), "site_name": c.get('name', ''),
"site_icon": c.get('icon', ''), "site_icon": c.get('icon', ''),
...@@ -624,6 +624,3 @@ def process_and_save_files_ai(file_path, search_keyword, url_list, think_content ...@@ -624,6 +624,3 @@ def process_and_save_files_ai(file_path, search_keyword, url_list, think_content
for content, file_name in data_config: for content, file_name in data_config:
save_data_to_tos_ai(target_dir, content, file_name) save_data_to_tos_ai(target_dir, content, file_name)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment