Commit 27ac512a authored by Yaowentong's avatar Yaowentong

后台健全

parent fc2c7163
......@@ -5,7 +5,6 @@ from __future__ import annotations
import json
import os
import sys
import traceback
import uuid
from datetime import datetime
import time
......@@ -15,7 +14,6 @@ from loguru import logger
from openpyxl import Workbook
from aidso_geo.models import spider_save_tos
from aidso_geo.utils.tos_utils import get_string_from_tos
POLL_SECONDS = 120
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.append(BASE_DIR)
......@@ -487,7 +485,6 @@ def doubao_process_original_data(file_path, original_content):
is_think = False
rich_media_block = []
think_bool = False
response_bool = False
file_path_result = os.path.dirname(file_path)
content_list = original_content.split("\n")
......@@ -701,79 +698,6 @@ def wait_mt_tasks_done(
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(
pt: str,
start_count: int,
......@@ -941,11 +865,26 @@ def process_rounds_to_result(
else:
keywords = []
raw, content = get_task_content_with_retries(
row=row,
file=file,
redis_key=redis_key,
)
try:
raw = tos_utils.get_string_from_tos(file)
except Exception as exc:
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(
f"[pt={pt} count={start_count}-{end_count}] "
f"处理 {file} 失败: {exc}"
)
if isinstance(
exc,
(ResultWriteError, TaskResultUnavailableError),
):
if isinstance(exc, TaskResultUnavailableError):
raise
if isinstance(exc, ResultWriteError):
raise
return None
def parse_result_batch(rows, batch_label):
if not rows:
return []
return [], []
worker_count = min(20, len(rows))
logger.info(
f"[pt={pt} count={start_count}-{end_count}] "
......@@ -1015,6 +953,8 @@ def process_rounds_to_result(
f"workers={worker_count}, tasks={len(rows)}"
)
parsed_results = []
missing_rows = []
completed_count = 0
with ThreadPoolExecutor(max_workers=worker_count) as executor:
future_to_row = {
executor.submit(parse_single_result, row): row
......@@ -1024,6 +964,9 @@ def process_rounds_to_result(
row = future_to_row[future]
try:
result_item = future.result()
except TaskResultUnavailableError:
missing_rows.append(row)
result_item = None
except Exception as exc:
logger.error(
f"[pt={row.get('pt')} "
......@@ -1034,39 +977,173 @@ def process_rounds_to_result(
raise
if result_item is not None:
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 insert_result_item(result_item, share_retry_count=0):
req_id = result_item.get("reqId")
if not bh_utils.insert_data(
"douchacha_data.geo_feishu_snipaste_result_v3",
[result_item],
):
raise ResultWriteError(
f"关键词统计结果写入失败: reqId={req_id}"
)
logger.success(
f"[pt={result_item.get('pt')} "
f"count={result_item.get('`count`')} "
f"reqId={req_id}] 关键词统计完成: "
f"has_keyword={result_item.get('has_keyword')}, "
f"keywords_count={result_item.get('keywords_count')}, "
f"share_retries={share_retry_count}"
def parse_rows_with_tos_retries(rows, batch_label):
parsed_results, missing_rows = parse_result_batch(
rows,
batch_label,
)
return 1
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
total_count = len(result_items)
total_batches = (
total_count + batch_size - 1
) // batch_size
inserted_count = 0
for batch_index, batch_start in enumerate(
range(0, total_count, batch_size),
start=1,
):
batch_items = result_items[
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(
f"[pt={pt} "
f"count={start_count}-{end_count}] "
f"结果批量写入完成: "
f"batch={batch_index}/{total_batches}, "
f"rows={len(batch_items)}, "
f"progress={inserted_count}/{total_count}, "
f"attempt={attempt}/3"
)
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
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
if not is_count_zero:
for _, result_item in parsed_results:
inserted_count += insert_result_item(result_item)
inserted_count = insert_result_items(
[
result_item
for _, result_item in parsed_results
]
)
else:
pending_results = []
final_result_items = []
for row, result_item in parsed_results:
if int(result_item.get("has_keyword") or 0) == 1:
inserted_count += insert_result_item(result_item)
final_result_items.append(result_item)
else:
pending_results.append((row, result_item))
......@@ -1115,7 +1192,7 @@ def process_rounds_to_result(
row
for row, _ in pending_results
]
retry_parsed_results = parse_result_batch(
retry_parsed_results = parse_rows_with_tos_retries(
retry_rows,
f"count=0 retry={share_retry_round}/5",
)
......@@ -1132,19 +1209,15 @@ def process_rounds_to_result(
previous_result_item,
)
if int(result_item.get("has_keyword") or 0) == 1:
inserted_count += insert_result_item(
result_item,
share_retry_count=share_retry_round,
)
final_result_items.append(result_item)
else:
next_pending_results.append((row, result_item))
pending_results = next_pending_results
for _, result_item in pending_results:
inserted_count += insert_result_item(
result_item,
share_retry_count=5,
)
final_result_items.append(result_item)
inserted_count = insert_result_items(final_result_items)
logger.success(
f"[pt={pt} count={start_count}-{end_count}] "
......@@ -2060,6 +2133,6 @@ def run_daily_pipeline_safely(pt: str = None) -> None:
if __name__ == "__main__":
# run_daily_pipeline()
# run_daily_pipeline('20260729')
start_scheduler()
# run_daily_pipeline(pt="2026072702")
......@@ -6,6 +6,11 @@ from aidso_geo.core.routes.feishu_interface import feishu_app
from aidso_geo.core.routes.third_interface import third_app
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.register_blueprint(line_app)
......@@ -14,4 +19,4 @@ app.register_blueprint(feishu_app)
app.register_blueprint(dashboard_app)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8086)
\ No newline at end of file
app.run(host='0.0.0.0', port=8086)
......@@ -16,6 +16,7 @@ KEEP_FIELDS = (
"type",
"thinkingEnabled",
"channel",
"is_share"
)
......@@ -47,12 +48,12 @@ def get_result_api(task_data):
platform = task_data.get("platform",'')
channel = task_data.get("channel") or ""
task_type = task_data.get("type",'')
url = f"http://172.16.1.223:8086/api/geo/task_check?reqId={req_id}"
try:
response = requests.get(url)
response_data = response.json()
print(response.text)
data = response_data.get("data") or {}
if response_data.get("code") == 200 and data.get("status") == "success":
......@@ -90,9 +91,9 @@ def task_check():
if __name__ == "__main__":
logger.info("调度 启动")
scheduler = BlockingScheduler(timezone="Asia/Shanghai")
# task_check()
scheduler.add_job(
task_commit,
trigger="interval",
......
from datetime import datetime
from hmac import compare_digest
from flask import jsonify, Blueprint, render_template, request
import requests
from flask import (
jsonify,
Blueprint,
redirect,
render_template,
request,
session,
url_for,
)
from aidso_geo.config.base_config import init_redis
from aidso_geo.utils import bh_utils
dashboard_app = Blueprint("dashboard", __name__)
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"])
def queue_status():
......@@ -48,14 +117,10 @@ def get_req_trend():
}), 400
try:
params = {
"begin": normalize_date_to_datetime(begin, "00:00:00"),
"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")
begin_date = datetime.strptime(begin, "%Y-%m-%d")
end_date = datetime.strptime(end, "%Y-%m-%d")
if begin_time > end_time:
if begin_date > end_date:
return jsonify({
"code": 400,
"msg": "begin must be earlier than end",
......@@ -68,21 +133,390 @@ def get_req_trend():
"data": []
}), 400
url = "https://openapi.aidso.com/openapi/ywt/platformStats"
try:
response = requests.get(url, params=params, timeout=300)
response.raise_for_status()
return jsonify(response.json())
except (requests.RequestException, ValueError) as e:
rows = bh_utils.query_data(
"""
SELECT
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({
"code": 500,
"msg": f"platformStats request failed: {e}",
"msg": f"platformStats query failed: {e}",
"data": []
}), 500
def normalize_date_to_datetime(value, default_time):
date_value = value.replace("T", " ").strip()[:10]
datetime.strptime(date_value, "%Y-%m-%d")
return f"{date_value} {default_time}"
@dashboard_app.route("/api/queue/thirdPartyStats", methods=["GET"])
def get_third_party_stats():
begin = request.args.get("begin", "").strip()
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():
if type not in {"0", "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({
"name": name,
"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"))
thinking_costs = parse_thinking_costs(auth_info.get("thinking_costs"))
......@@ -563,6 +608,7 @@ def mt_task_commit():
"platform": name,
"thinkingEnabled": p["thinkingEnabled"],
"type": type_map[p["type"]],
"is_share": p["is_share"],
"insertime": now_ts,
"status": "ING",
"channel": channel
......@@ -660,7 +706,8 @@ def mt_get_result():
"think": "think.txt",
"context": "context.txt",
"suggestions": "suggestions.txt",
"rich_media_block": "rich_media_block.txt"
"rich_media_block": "rich_media_block.txt",
"share_url": "share_url.text"
}
result = []
......@@ -797,4 +844,3 @@ def mt_get_usage():
# "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 @@
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 {
display: flex;
align-items: center;
......@@ -240,6 +277,7 @@
.quick-range {
display: flex;
gap: 8px;
align-items: flex-end;
}
.field {
......@@ -253,212 +291,695 @@
color: #6b7280;
}
.field input {
.field input,
.field select {
min-width: 220px;
border: 1px solid #d7dce3;
border-radius: 8px;
padding: 9px 10px;
height: 42px;
box-sizing: border-box;
border: 1px solid #cbd5e1;
border-radius: 9px;
padding: 0 12px;
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-weight: 600;
text-align: left;
cursor: pointer;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.field input:focus {
.custom-select-trigger:hover {
border-color: #94a3b8;
}
.custom-select.open .custom-select-trigger,
.custom-select-trigger:focus-visible {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
}
.trend-chart {
width: 100%;
min-height: 300px;
overflow-x: auto;
border-radius: 12px;
background: #f8fafc;
border: 1px solid #edf2f7;
.custom-select-value {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.daily-grid {
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(320px, 1fr);
gap: 14px;
min-width: 100%;
padding: 14px;
.custom-select-arrow {
width: 8px;
height: 8px;
flex: 0 0 auto;
margin: -4px 2px 0 0;
border-right: 2px solid #64748b;
border-bottom: 2px solid #64748b;
transform: rotate(45deg);
transition: transform 0.18s ease, margin 0.18s ease;
}
.day-card {
min-width: 0;
border: 1px solid #e5e7eb;
.custom-select.open .custom-select-arrow {
margin-top: 4px;
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;
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 {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 14px 10px;
border-bottom: 1px solid #edf2f7;
.custom-select.open .custom-select-menu {
display: grid;
gap: 3px;
}
.day-date {
color: #111827;
font-size: 16px;
font-weight: 800;
.custom-select-option {
width: 100%;
min-height: 38px;
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 {
color: #64748b;
font-size: 13px;
font-weight: 700;
.custom-select-option:hover {
background: #eff6ff;
color: #1d4ed8;
}
.day-list {
display: grid;
gap: 10px;
padding: 12px 14px 14px;
.custom-select-option.selected {
position: relative;
padding-right: 34px;
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);
}
.day-row {
.filter-row .quick-btn {
height: 42px;
box-sizing: border-box;
}
.third-metrics {
display: grid;
grid-template-columns: minmax(100px, 1fr) minmax(90px, 42%) 58px;
align-items: center;
gap: 8px;
min-height: 24px;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 14px;
}
.day-platform {
color: #334155;
.third-metric-card,
.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-weight: 700;
}
.third-metric-card strong {
overflow: hidden;
color: #0f172a;
font-size: clamp(19px, 1.45vw, 25px);
font-weight: 800;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.day-bar {
height: 9px;
border-radius: 999px;
background: #eef2f7;
.third-dashboard-grid {
display: grid;
grid-template-columns: minmax(360px, 0.8fr) minmax(0, 1.7fr);
gap: 18px;
min-width: 0;
}
.third-section-card {
min-width: 0;
overflow: hidden;
}
.day-bar-inner {
height: 100%;
min-width: 3px;
border-radius: 999px;
.third-card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 15px 17px;
border-bottom: 1px solid #edf2f7;
}
.day-count {
color: #111827;
font-size: 13px;
.third-card-head strong {
color: #0f172a;
font-size: 16px;
font-weight: 800;
}
.third-card-head > span {
color: #64748b;
font-size: 12px;
font-weight: 700;
text-align: right;
}
.chart-empty {
display: flex;
.third-ranking-list {
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;
justify-content: center;
min-height: 220px;
color: #6b7280;
font-size: 14px;
gap: 9px;
width: 100%;
padding: 7px 4px;
border: none;
border-radius: 8px;
background: transparent;
cursor: pointer;
text-align: left;
}
.trend-summary {
margin-top: 16px;
padding: 16px;
border: 1px solid #edf2f7;
border-radius: 12px;
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
.third-ranking-row:hover {
background: #f8fafc;
}
.trend-summary-title {
display: flex;
.third-ranking-index {
display: inline-flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 7px;
background: #eff6ff;
color: #2563eb;
font-size: 11px;
font-weight: 800;
}
.trend-summary-title-main {
color: #111827;
font-size: 16px;
.third-ranking-name {
overflow: hidden;
color: #334155;
font-size: 12px;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
}
.trend-summary-actions {
display: flex;
align-items: center;
gap: 10px;
.third-ranking-bar {
height: 8px;
overflow: hidden;
border-radius: 999px;
background: #edf2f7;
}
.trend-summary-range {
color: #64748b;
font-size: 13px;
font-weight: 700;
.third-ranking-bar-inner {
display: block;
height: 100%;
border-radius: inherit;
background: #2563eb;
}
.trend-switch {
.third-ranking-count {
color: #0f172a;
font-size: 12px;
text-align: right;
}
.third-trend-switch {
display: inline-grid;
grid-template-columns: 1fr 1fr;
gap: 2px;
padding: 3px;
border: 1px solid #d7dce3;
border-radius: 10px;
border-radius: 9px;
background: #f1f5f9;
}
.trend-switch-btn {
.third-trend-switch button {
border: none;
border-radius: 7px;
border-radius: 6px;
background: transparent;
color: #64748b;
padding: 6px 11px;
padding: 6px 9px;
cursor: pointer;
font-size: 13px;
font-size: 12px;
font-weight: 800;
}
.trend-switch-btn.active {
.third-trend-switch button.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;
.third-trend-chart {
min-height: 390px;
padding: 12px 14px 8px;
}
.trend-label {
fill: #64748b;
font-size: 12px;
.third-trend-legend {
display: flex;
gap: 12px;
overflow-x: auto;
padding: 2px 4px 10px;
color: #475569;
font-size: 11px;
font-weight: 700;
white-space: nowrap;
}
.trend-total-label {
fill: #2563eb;
font-size: 12px;
font-weight: 800;
.third-trend-legend span {
display: inline-flex;
align-items: center;
gap: 5px;
}
.trend-hover-hit {
cursor: crosshair;
.third-trend-legend i {
width: 8px;
height: 8px;
border-radius: 999px;
}
.trend-tooltip {
position: fixed;
z-index: 30;
display: none;
min-width: 260px;
max-width: 360px;
max-height: 420px;
overflow-y: auto;
padding: 10px 12px;
border: 1px solid rgba(148, 163, 184, 0.28);
.third-trend-scroll {
overflow-x: auto;
overflow-y: hidden;
}
.third-trend-scroll svg {
display: block;
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;
border: 1px solid rgba(148, 163, 184, 0.28);
border-radius: 10px;
background: rgba(15, 23, 42, 0.94);
color: #fff;
......@@ -594,6 +1115,28 @@
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) {
.layout {
grid-template-columns: 1fr;
......@@ -615,6 +1158,23 @@
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 {
flex-direction: column;
align-items: flex-start;
......@@ -638,12 +1198,1134 @@
.field,
.field input,
.field select,
.filter-row .btn,
.quick-range {
width: 100%;
}
.quick-btn {
flex: 1;
.third-metrics {
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 STATS_API_URL = "/api/queue/platformStats";
const THIRD_STATS_API_URL = "/api/queue/thirdPartyStats";
const platforms = [
"BDAI", "DB", "DOUBA", "DP", "DPA", "DYAI",
"KIMI", "TXYB", "TXYBA", "TYQW", "TYQWA", "WXYY", "XHSA"
];
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 loadedPanels = {queue: false, trend: false};
let loadedPanels = {queue: false, trend: false, third: false};
let trendChartMode = "platform";
let dailySourceMode = "all";
let lastTrendData = null;
let thirdTrendMode = "channel";
let lastThirdData = null;
function showPanel(panel) {
currentPanel = panel;
document.getElementById("queuePanel").classList.toggle("active", panel === "queue");
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("trendNavBtn").classList.toggle("active", panel === "trend");
document.getElementById("pageTitle").innerText = panel === "queue"
? "GEO 队列监控面板"
: "GEO历史任务数量趋势";
document.getElementById("pageDesc").innerText = panel === "queue"
? "查看各平台 stream_batch / batch 队列积压情况"
: "按时间范围查看各平台历史任务趋势";
document.getElementById("thirdNavBtn").classList.toggle("active", panel === "third");
const pageMeta = {
queue: {
title: "队列积压监控",
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();
if (!loadedPanels[panel]) {
......@@ -34,8 +77,10 @@ const API_URL = "/api/queue/status";
function refreshCurrentPanel() {
if (currentPanel === "queue") {
loadQueueStatus();
} else {
} else if (currentPanel === "trend") {
loadPlatformStats();
} else {
loadThirdPartyStats();
}
}
......@@ -55,6 +100,10 @@ const API_URL = "/api/queue/status";
document.querySelector(".trend-panel").classList.toggle("loading", isLoading);
}
function setThirdLoading(isLoading) {
document.querySelector(".third-panel").classList.toggle("loading", isLoading);
}
function getNumClass(num) {
if (num === 0) {
return "num-ok";
......@@ -127,9 +176,16 @@ const API_URL = "/api/queue/status";
.map(item => ({
time: normalizeStatDate(item["时间"] || item.time || item.date || item.begin || ""),
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) {
......@@ -146,11 +202,178 @@ const API_URL = "/api/queue/status";
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) {
const chart = document.getElementById("platformTrendChart");
const summary = document.getElementById("trendSummary");
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);
chart.innerHTML = "";
......@@ -163,22 +386,42 @@ const API_URL = "/api/queue/status";
}
const chartLabels = [...new Set(items.map(item => item.time))].sort();
const cardLabels = [...chartLabels].reverse();
const oldestDate = chartLabels[0];
const newestDate = chartLabels[chartLabels.length - 1];
const platformTotals = new Map();
const pointMap = new Map();
const sourcePointMap = new Map();
items.forEach(item => {
platformTotals.set(item.platform, (platformTotals.get(item.platform) || 0) + item.count);
const pointKey = `${item.platform}__${item.time}`;
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()]
.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);
const maxCount = Math.max(...pointMap.values(), 1);
const platformColorMap = new Map(platformList.map((platform, index) => [platform, colors[index % colors.length]]));
const dayTotals = chartLabels.map(label => ({
date: label,
......@@ -189,65 +432,17 @@ const API_URL = "/api/queue/status";
labels: chartLabels,
platformList,
pointMap,
sourcePointMap,
platformColorMap,
dayTotals
};
const grid = document.createElement("div");
grid.className = "daily-grid";
cardLabels.forEach(label => {
const dayItems = platformList.map(platform => ({
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);
renderStackedPlatformChart(
chart,
chartLabels,
platformList,
sourcePointMap
);
renderTrendSummary();
statsNote.innerText = `共 ${platformList.length} 个平台,${oldestDate}${newestDate}`;
document.getElementById("lastTime").innerText = "最后刷新:" + new Date().toLocaleString();
......@@ -263,7 +458,7 @@ const API_URL = "/api/queue/status";
const series = trendChartMode === "total"
? [{
name: "总量",
color: "#2563eb",
color: "#7138f4",
values: lastTrendData.dayTotals.map(item => item.total)
}]
: lastTrendData.platformList.map(platform => ({
......@@ -300,7 +495,7 @@ const API_URL = "/api/queue/status";
platformBtn.className = `trend-switch-btn ${trendChartMode === "platform" ? "active" : ""}`;
totalBtn.className = `trend-switch-btn ${trendChartMode === "total" ? "active" : ""}`;
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 = "平台";
totalBtn.innerText = "总量";
platformBtn.onclick = () => setTrendChartMode("platform");
......@@ -584,6 +779,7 @@ const API_URL = "/api/queue/status";
hideError();
statsNote.innerText = "加载中";
trendChartMode = "platform";
dailySourceMode = "all";
setTrendLoading(true);
try {
......@@ -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("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);
setThirdQuickRange(3, false);
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 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>GEO 队列监控面板</title>
<title>AIDSO 数据监控</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='dashboard/queue_monitor.css') }}">
......@@ -11,20 +11,36 @@
<body>
<div class="layout">
<aside class="sidebar">
<button class="nav-btn active" id="queueNavBtn" onclick="showPanel('queue')">积压查看</button>
<button class="nav-btn" id="trendNavBtn" onclick="showPanel('trend')">历史任务趋势</button>
<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" 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>
<main class="main">
<div class="header">
<div>
<div class="title" id="pageTitle">GEO 队列监控面板</div>
<div class="title" id="pageTitle">队列积压监控</div>
<div class="desc" id="pageDesc">查看各平台 stream_batch / batch 队列积压情况</div>
</div>
<div class="right">
<span class="last-time" id="lastTime">未刷新</span>
<button class="btn" onclick="refreshCurrentPanel()">刷新</button>
<a class="logout-link" href="{{ url_for('dashboard.dashboard_logout') }}">退出</a>
</div>
</div>
......@@ -48,11 +64,11 @@
</div>
</div>
<div class="section-head">
<div class="section-title">队列积压详情</div>
<div class="section-note">Redis 队列长度</div>
</div>
<div class="table-wrap">
<div class="section-head panel-card-head">
<div class="section-title">队列积压详情</div>
<div class="section-note">Redis 队列长度</div>
</div>
<table>
<thead>
<tr>
......@@ -69,11 +85,11 @@
</section>
<section class="panel" id="trendPanel">
<div class="section-head">
<div class="section-title">历史任务趋势</div>
<div class="section-note" id="statsNote">等待加载</div>
</div>
<div class="trend-panel">
<div class="section-head panel-card-head">
<div class="section-title">历史任务概览</div>
<div class="section-note" id="statsNote">等待加载</div>
</div>
<div class="filter-row">
<div class="quick-range">
<button class="quick-btn active" id="range3Btn" onclick="setQuickRange(3)">近3天</button>
......@@ -100,6 +116,111 @@
</div>
</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>
</main>
</div>
......
import json
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.tos_utils import get_string_from_tos
......@@ -31,6 +31,7 @@ def douyin_ai_process_original_data(data):
# 提取并解析JSON数据
data_str = item.split("data:")[1]
json_data = json.loads(data_str)
except (IndexError, json.JSONDecodeError):
continue
......@@ -117,6 +118,30 @@ def douyin_ai_process_original_data(data):
return (file_path, search_keyword, url_list, think_content, response_content, suggestions)
if __name__ == '__main__':
file_path = "geo/8004e38c-37da-4e82-b08f-453ccdf0d661/DYAI/original.text"
douyin_ai_process_original_data(file_path)
data_list = bh_utils.query_data(
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
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'
url_list = []
url_list_batch = []
......@@ -30,14 +72,14 @@ def qianwen_android_process_original_data(task_data):
try:
sse_json_data = json.loads(original_content)
# sse_json_data = json.loads(json_original.get('sse'))
response_content = sse_json_data.get('reply_text','')
think_content = sse_json_data.get('deepthink_text','')
paa_answer = sse_json_data.get('paa_answer','')
response_content = sse_json_data.get('reply_text', '')
think_content = sse_json_data.get('deepthink_text', '')
paa_answer = sse_json_data.get('paa_answer', '')
bar_sources = sse_json_data.get('bar_sources')
amap_pois = sse_json_data.get('amap_pois')
gaode_list = []
if amap_pois:
for key,value in amap_pois.items():
for key, value in amap_pois.items():
for v in value:
modelInput = v.get('future_data').get('modelInput')
biz_ext = v.get('biz_ext')
......@@ -71,7 +113,7 @@ def qianwen_android_process_original_data(task_data):
response_content += 'render_ecom_card_widget_gaode_end:\n'
source_content = {}
if bar_sources:
source_content =next(
source_content = next(
(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):
if video_note_list:
for i in video_note_list:
rich_media = {
'zhidaye_id':i.get('zhidaye_id'),
'title':i.get('title'),
'cover':i.get('cover'),
'url':i.get('url'),
'author':i.get('author'),
'publishTime':i.get('publishTime'),
'duration':i.get('duration'),
'zhidaye_id': i.get('zhidaye_id'),
'title': i.get('title'),
'cover': i.get('cover'),
'url': i.get('url'),
'author': i.get('author'),
'publishTime': i.get('publishTime'),
'duration': i.get('duration'),
}
video_list.append(rich_media)
rich_media_block.append({
"url":video_list,
"source_seq":''
"url": video_list,
"source_seq": ''
})
source_count_list = []
if source_list:
for s in source_list:
source_url = {
'url':s.get('url'),
'title':s.get('title'),
'summary':s.get('summary'),
'publish_time':s.get('publish_time'),
'name':s.get('name'),
'icon':s.get('icon'),
'source_seq':s.get('source_seq',''),
'url': s.get('url'),
'title': s.get('title'),
'summary': s.get('summary'),
'publish_time': s.get('publish_time'),
'name': s.get('name'),
'icon': s.get('icon'),
'source_seq': s.get('source_seq', ''),
}
source_count_list.append(source_url)
url_list.append({
"url":[
"url": [
{
"type":"source",
"content":{
"list":source_count_list
"type": "source",
"content": {
"list": source_count_list
},
}
],
"source_seq":''
"source_seq": ''
})
if paa_answer:
......@@ -187,11 +229,13 @@ def qianwen_android_process_original_data(task_data):
if mime_type == 'bar/progress' and meta_data_type == 'bar_update':
if messages[0].get('meta_data'):
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:
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_source_seq = i.get('source_seq')
multi_load_content_query_list = multi_load_content.get('query_list')
......@@ -215,18 +259,19 @@ def qianwen_android_process_original_data(task_data):
"source_seq": '',
})
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:
if mu.get('type') == 'taoassistant_fold_product_feeds' :
if mu.get('type') == 'taoassistant_fold_product_feeds':
source_seq = mu.get('source_seq')
jump_url = next(
(
item.get('params', {}).get('popupLink', '')
for item in
mu.get('content', {}).get('cardData', {}).get('clientActions', [])
mu.get('content', {}).get('cardData', {}).get('clientActions', [])
if item.get('type') == 'popupLink'
),
''
......@@ -234,23 +279,26 @@ def qianwen_android_process_original_data(task_data):
query = next(
(
item.get('params', {}).get('params', {}).get('queryList',[])
item.get('params', {}).get('params', {}).get('queryList', [])
for item in
mu.get('content', {}).get('cardData', {}).get('clientActions', [])
mu.get('content', {}).get('cardData', {}).get('clientActions', [])
if item.get('type') == 'loadmore'
),
''
)
if isinstance(query,str):
if isinstance(query, str):
if query:
query = json.loads(query)
if isinstance(query,list):
if isinstance(query, list):
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'):
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({
"title": cateTitle or pro.get('title', ''),
"shop_name": next(
......@@ -268,17 +316,19 @@ def qianwen_android_process_original_data(task_data):
"source_seq": source_seq,
"auctionURL": pro.get('auctionURL'),
"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'):
mu_content = mu.get('content',{})
mu_content_cardData = mu_content.get('cardData',{})
mu_content_cardData_data = mu_content_cardData.get('data',{})
if mu.get('type') in (
'taoassistant_single_product', 'taoassistant_single_product_v2'):
mu_content = mu.get('content', {})
mu_content_cardData = mu_content.get('cardData', {})
mu_content_cardData_data = mu_content_cardData.get('data', {})
if mu_content_cardData_data:
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 ''
taobao_list.append({
"title": mu_content_cardData_data.get('title'),
......@@ -286,7 +336,8 @@ def qianwen_android_process_original_data(task_data):
(
item.get('text', '')
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'
),
''
......@@ -300,13 +351,12 @@ def qianwen_android_process_original_data(task_data):
'card_type': 'single_product'
})
if taobao_list:
response_content += 'render_ecom_card_widget_taobao_start:'
damai_str = json.dumps(taobao_list, ensure_ascii=False)
response_content += damai_str
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 = []
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):
'id': damai.get('data').get('id')
})
damai_data_list.extend(damai_list)
gaode_list = []
if mime_type == 'multi_load/iframe' and multi_load_type == 'new_amap_poi_list' and status == 'complete':
for mu in multi_load:
......@@ -369,13 +420,14 @@ def qianwen_android_process_original_data(task_data):
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:
mu_content = mu.get('content')
mu_source_seq = mu.get('source_seq')
mu_content_model_input = mu_content.get('modelInput')
mu_content_pois = mu_content.get('pois')
mu_content_model_input = mu_content.get('modelInput')
mu_content_pois = mu_content.get('pois')
for poi in mu_content_pois:
poi_summary = poi.get('summary1')
......@@ -386,15 +438,17 @@ def qianwen_android_process_original_data(task_data):
poi_id = mu_content_model_input.get("poi_id")
gaode_result = {
"url":f"https://www.amap.com/place/{poi_id}",
"source_seq":mu_source_seq,
"name":mu_content_model_input.get("name"),
"poi_id":poi_id,
"cost":cost,
"poi_summary":poi_summary,
"distance_formatted":distance_formatted,
"photos":photos[0].get("url", "") if isinstance(photos, list) and photos and isinstance(photos[0], dict) else "",
"address":distance_address,
"url": f"https://www.amap.com/place/{poi_id}",
"source_seq": mu_source_seq,
"name": mu_content_model_input.get("name"),
"poi_id": poi_id,
"cost": cost,
"poi_summary": poi_summary,
"distance_formatted": distance_formatted,
"photos": photos[0].get("url", "") if isinstance(photos,
list) and photos and isinstance(
photos[0], dict) else "",
"address": distance_address,
}
gaode_list.append(gaode_result)
gaode_data_list.extend(gaode_list)
......@@ -413,6 +467,25 @@ def qianwen_android_process_original_data(task_data):
think_content += multi_load_content.get('think_content')
# 引用来源 回答 视频列表
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
for mu in multi_load:
if mu.get('type') == 'video_note_list':
......@@ -473,17 +546,19 @@ def qianwen_android_process_original_data(task_data):
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
# file_path2 = 'geo/alskdjhfasldjkfh/TYQWA/original.text'
# file_path2 = 'geo/ef66d5f9-cadb-43f3-b8cf-824d75a81e7c/TYQWA/original.text'
# qianwen_android_process_original_data(file_path2)
# fb8de08c16484143a7299c06ba20c90d 老版本
# e09e1b7db91f4d0382e86458d8462147 新版本
# 8a5d2d78-acca-4621-a913-1ff1527f8789
# 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 = '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):
......
......@@ -265,12 +265,12 @@ def qianwen_android_process_quote(url_list):
content = url.get('content')
if content:
if isinstance(content.get('list'),list):
for c in content.get('list'):
for index2,c in enumerate(content.get('list'),start=1):
raw_data = {
"url": c.get('url', ''),
"title": c.get('title', ''),
"snippet": c.get('summary', ''),
"index": index,
"index": index2,
"published_at": c.get('publish_time', ''),
"site_name": c.get('name', ''),
"site_icon": c.get('icon', ''),
......@@ -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:
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