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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions deploy/nginx-docker.conf
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ server {
resolver 127.0.0.11 valid=10s ipv6=off;
set $backend_upstream http://backend:5000;

# Security headers
add_header Content-Security-Policy "frame-ancestors 'self';" always;
add_header X-Frame-Options "SAMEORIGIN" always;
# Security headers (hardened)
server_tokens off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https: wss:; font-src 'self' data:; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;

# Gzip compression
gzip on;
Expand Down
21 changes: 17 additions & 4 deletions deploy/nginx-docker.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ server {
resolver 127.0.0.11 valid=10s ipv6=off;
set $backend_upstream ${BACKEND_URL};

# Security headers
add_header Content-Security-Policy "frame-ancestors 'self';" always;
add_header X-Frame-Options "SAMEORIGIN" always;
# Security headers (hardened)
server_tokens off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https: wss:; font-src 'self' data:; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;

# Gzip compression
gzip on;
Expand Down Expand Up @@ -50,6 +52,17 @@ server {
client_max_body_size 10m;
}

# robots.txt & sitemap.xml (SEO)
location = /robots.txt {
root /usr/share/nginx/html;
try_files /robots.txt =404;
}
location = /sitemap.xml {
root /usr/share/nginx/html;
try_files /sitemap.xml =404;
add_header Content-Type "application/xml; charset=utf-8";
}

# SPA routing support (all routes fall back to index.html)
location / {
try_files $uri $uri/ /index.html;
Expand Down
10 changes: 6 additions & 4 deletions deploy/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ server {
root /usr/share/nginx/html;
include /etc/nginx/mime.types;

# Security headers
add_header Content-Security-Policy "frame-ancestors 'self';" always;
add_header X-Frame-Options "SAMEORIGIN" always;
# Security headers (hardened)
server_tokens off;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https: wss:; font-src 'self' data:; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; object-src 'none'" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()" always;

location / {
try_files $uri $uri/ /index.html;
Expand Down
23 changes: 22 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,28 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="/slogo.png">
<title>QuantDinger</title>
<title>QuantDinger — AI 量化交易平台 | 回测 · 实盘 · 多市场自动交易</title>
<meta name="description" content="QuantDinger 是一站式 AI 量化交易基础设施:内置 60+ 因子、Python 策略引擎、网格/马丁/定投机器人,支持 Binance/OKX/Bybit/IBKR/Alpaca,从研究到回测到实盘闭环。">
<meta name="keywords" content="量化交易,量化平台,加密货币交易机器人,比特币量化,股票量化,回测,网格交易,马丁策略,Python 策略,API 自动交易,QuantDinger">
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1">
<meta name="author" content="QuantDinger">
<link rel="canonical" href="https://quant.kai.com/">
<link rel="preconnect" href="https://api.quantdinger.com" crossorigin>
<meta property="og:type" content="website">
<meta property="og:site_name" content="QuantDinger">
<meta property="og:title" content="QuantDinger — AI 量化交易平台">
<meta property="og:description" content="60+ 因子 · Python 策略引擎 · 网格/马丁/定投机器人 · 多券商实盘对接。从研究到回测到实盘,一站式闭环。">
<meta property="og:url" content="https://quant.kai.com/">
<meta property="og:image" content="https://quant.kai.com/slogo.png">
<meta property="og:locale" content="zh_CN">
<meta property="og:locale:alternate" content="en_US">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="QuantDinger — AI 量化交易平台">
<meta name="twitter:description" content="60+ 因子 · Python 策略引擎 · 多券商实盘对接,从研究到实盘一站式闭环。">
<meta name="twitter:image" content="https://quant.kai.com/slogo.png">
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"SoftwareApplication","name":"QuantDinger","applicationCategory":"FinanceApplication","operatingSystem":"Web, iOS, Android","offers":{"@type":"Offer","price":"0","priceCurrency":"USD"},"description":"AI 量化交易基础设施:内置因子库、Python 策略引擎、网格/马丁/定投机器人,支持多券商实盘对接。","url":"https://quant.kai.com/"}
</script>
<style>
:root {
color-scheme: light dark;
Expand Down
11 changes: 11 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
User-agent: *
Allow: /
Disallow: /api/
Disallow: /user/
Disallow: /billing
Disallow: /profile
Disallow: /settings
Disallow: /__view/
Disallow: /__assets/

Sitemap: https://quant.kai.com/sitemap.xml
6 changes: 6 additions & 0 deletions public/sitemap.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://quant.kai.com/</loc><lastmod>2026-08-13</lastmod><changefreq>weekly</changefreq><priority>1.0</priority></url>
<url><loc>https://quant.kai.com/user/login</loc><lastmod>2026-08-13</lastmod><changefreq>monthly</changefreq><priority>0.8</priority></url>
<url><loc>https://quant.kai.com/user/register</loc><lastmod>2026-08-13</lastmod><changefreq>monthly</changefreq><priority>0.8</priority></url>
</urlset>
18 changes: 18 additions & 0 deletions src/config/router.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,24 @@ export const constantRouterMap = [
]
},

{
path: '/legal',
component: () => import('@/layouts/BlankLayout'),
hidden: true,
redirect: '/legal/user-agreement',
children: [
{
path: 'user-agreement',
name: 'UserAgreement',
component: () => import('@/views/legal/UserAgreement')
},
{
path: 'privacy-policy',
name: 'PrivacyPolicy',
component: () => import('@/views/legal/PrivacyPolicy')
}
]
},
{
path: '/404',
meta: { title: 'menu.exception.not-find' },
Expand Down
152 changes: 152 additions & 0 deletions src/core/error_monitor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* QuantDinger 前端错误监控模块 (Error Monitor)
*
* 覆盖:
* 1. window.onerror — 同步 JS 运行时错误
* 2. unhandledrejection — Promise 未捕获拒绝
* 3. 资源加载失败 — script/style/img/error 事件
* 4. 实盘下单失败专项上报 — reportTradeFailure()
* 5. 策略执行崩溃专项上报 — reportStrategyCrash()
* 6. 批量节流 + 离线队列 — 断网缓存,恢复后重发
*
* 集成:在 app 入口第一个 import,确保捕获后续所有错误。
* import './error_monitor.js'
*
* 后端:POST /api/v2/errors body = { events: [...] }
*/

const ErrorMonitor = (() => {
const QUEUE = [];
const MAX_QUEUE = 50;
const FLUSH_INTERVAL = 10000; // 10s 批量上报
let flushTimer = null;
let endpoint = '/api/v2/errors';
let enabled = true;

// ---- 环境上下文 ----
const getContext = () => ({
url: location.href,
ua: navigator.userAgent,
lang: navigator.language,
ts: Date.now(),
view: document.cookie.includes('qd_view=mobile') ? 'mobile' : 'desktop',
app_version: window.__QD_VERSION__ || 'unknown',
});

// ---- 入队 ----
function enqueue(event) {
if (!enabled) return;
event.context = getContext();
if (QUEUE.length >= MAX_QUEUE) QUEUE.shift(); // 滚动覆盖
QUEUE.push(event);
}

// ---- 上报(批量节流) ----
async function flush() {
if (QUEUE.length === 0) return;
const batch = QUEUE.splice(0, QUEUE.length);
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ events: batch }),
credentials: 'include',
keepalive: true, // 页面卸载时也能发出去
});
if (!res.ok && batch.length > 0) {
// 服务端拒绝,放回队列尾部(避免无限重试导致堆积,只保留最近 20 条)
QUEUE.push(...batch.slice(-20));
}
} catch (e) {
// 网络失败,放回队列等待下次重试
QUEUE.push(...batch.slice(-20));
}
}

function startTimer() {
if (flushTimer) return;
flushTimer = setInterval(flush, FLUSH_INTERVAL);
// 页面隐藏/卸载时立刻 flush
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') flush();
});
window.addEventListener('beforeunload', flush);
}

// ---- 1. 同步运行时错误 ----
window.addEventListener('error', (e) => {
enqueue({
type: 'runtime_error',
message: e.message,
filename: e.filename,
lineno: e.lineno,
colno: e.colno,
stack: e.error && e.error.stack ? String(e.error.stack).slice(0, 2000) : null,
});
});

// ---- 2. Promise 未捕获拒绝 ----
window.addEventListener('unhandledrejection', (e) => {
const reason = e.reason;
enqueue({
type: 'unhandled_rejection',
message: reason instanceof Error ? reason.message : String(reason),
stack: reason instanceof Error && reason.stack ? String(reason.stack).slice(0, 2000) : null,
});
});

// ---- 3. 资源加载失败 ----
window.addEventListener('error', (e) => {
const target = e.target;
if (target && (target.tagName === 'SCRIPT' || target.tagName === 'LINK' || target.tagName === 'IMG')) {
enqueue({
type: 'resource_error',
tag: target.tagName,
src: target.src || target.href,
});
}
}, true); // 捕获阶段才能抓资源错误

// ---- 4. 实盘下单失败专项 ----
function reportTradeFailure(payload) {
enqueue({
type: 'trade_failure',
...payload, // { strategy_id, symbol, side, notional, exchange, error_code, error_msg }
severity: 'critical',
});
flush(); // 实盘失败立即上报,不等节流
}

// ---- 5. 策略执行崩溃专项 ----
function reportStrategyCrash(payload) {
enqueue({
type: 'strategy_crash',
...payload, // { strategy_id, script_id, bar_time, error_msg, stack }
severity: 'critical',
});
flush();
}

// ---- 公开 API ----
return {
init(opts = {}) {
if (opts.endpoint) endpoint = opts.endpoint;
if (opts.enabled === false) enabled = false;
startTimer();
// 恢复离线队列
window.addEventListener('online', flush);
},
reportTradeFailure,
reportStrategyCrash,
flush,
getQueueLength: () => QUEUE.length,
};
})();

// 自动启动(默认 endpoint /api/v2/errors)
if (typeof window !== 'undefined') {
ErrorMonitor.init();
window.ErrorMonitor = ErrorMonitor; // 供其他模块调用
}

export default ErrorMonitor;
5 changes: 5 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// QuantDinger Error Monitor — must load first to catch all downstream errors.
// Batch-reports to /api/v2/errors (POST). Covers window.onerror,
// unhandledrejection, resource load failures, trade/strategy crashes.
import './core/error_monitor.js'

import Vue from 'vue'
import 'ant-design-vue/dist/antd.css'
import App from './App.vue'
Expand Down
2 changes: 1 addition & 1 deletion src/permission.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ NProgress.configure({
showSpinner: false
}) // NProgress Configuration

const allowList = ['login'] // no redirect allowList
const allowList = ['login', 'UserAgreement', 'PrivacyPolicy'] // no redirect allowList
const loginRoutePath = '/user/login'
const defaultRoutePath = '/ai-asset-analysis'

Expand Down
Loading