Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
369c1e4
fix: silence LiteLLM log noise, deduplicate realtime quotes, fast-fai…
rex0104 Apr 11, 2026
9744c86
feat: add startup schema migration infrastructure
rex0104 Apr 11, 2026
52e1f83
feat: add User, UserWatchlist, SharedReport ORM models
rex0104 Apr 11, 2026
42fdc39
feat: add UserService for registration, login, password change
rex0104 Apr 11, 2026
a3a3b96
feat: add user_id-based session create/verify to auth module
rex0104 Apr 11, 2026
6f9811e
refactor: auth middleware uses user sessions, enforces when users exist
rex0104 Apr 11, 2026
f8514c6
feat: multi-user auth API — register, login, status with hasUsers
rex0104 Apr 11, 2026
63af21b
feat: first-user data claim — orphan rows assigned on first registration
rex0104 Apr 11, 2026
b5ebd97
feat: watchlist API — add, remove, list per-user favorites
rex0104 Apr 11, 2026
1c17363
feat: thread user_id through repositories, services, and API endpoints
rex0104 Apr 11, 2026
74b71d5
feat: report sharing API — create, view, list, revoke public share links
rex0104 Apr 11, 2026
7fa715d
feat: frontend multi-user auth — register page, email login, routing
rex0104 Apr 11, 2026
6cc587c
feat: share UI — ShareButton, public SharePage with brand header
rex0104 Apr 11, 2026
39bc3d6
feat: watchlist UI — favorites panel on homepage with star toggle
rex0104 Apr 11, 2026
d9430b7
feat: UserMenu dropdown in Shell — nickname, change password, logout
rex0104 Apr 11, 2026
868d50b
fix: update existing tests for multi-user auth model
rex0104 Apr 11, 2026
382d697
docs: changelog and env.example for multi-user, watchlist, sharing
rex0104 Apr 12, 2026
50453f8
feat: per-user settings storage — each user has isolated config
rex0104 Apr 12, 2026
c0d539f
docs: add per-user settings to changelog
rex0104 Apr 12, 2026
c921381
fix: show watchlist panel with empty-state hint instead of hiding it
rex0104 Apr 12, 2026
e63cb08
fix: align UserMenu trigger with nav item sizing and add top border s…
rex0104 Apr 12, 2026
92beffd
feat: promote watchlist to dedicated page with sidebar nav entry
rex0104 Apr 12, 2026
3c87908
fix: move theme toggle from sidebar to page top-right corner
rex0104 Apr 12, 2026
87ba07e
docs: watchlist enriched page design spec with portfolio linkage
rex0104 Apr 12, 2026
d4bd7c9
feat: watchlist groups schema — migration v3, UserWatchlistGroup model
rex0104 Apr 12, 2026
43e897d
feat: enriched watchlist API with price, analysis, sparkline, positio…
rex0104 Apr 12, 2026
4e7b33b
feat: enriched watchlist page — sparkline, analysis, positions, groups
rex0104 Apr 12, 2026
7def04e
fix: enrich timeline with analysis summary, theme toggle icon-only on…
rex0104 Apr 12, 2026
68df065
fix: A-share market detection with .SZ/.SH suffix, trim share API pay…
rex0104 Apr 12, 2026
49dd072
feat: watchlist uses realtime quotes with Tushare auto-priority when …
rex0104 Apr 12, 2026
0711a05
fix: correct DataFetcherManager import for realtime quotes
rex0104 Apr 12, 2026
f001018
fix: thread user_id through analysis pipeline, restore star toggle on…
rex0104 Apr 12, 2026
959048b
fix: pass user_id through task_queue and AnalysisService to pipeline
rex0104 Apr 12, 2026
375a77e
fix: pass user_id in sync analysis path (_handle_sync_analysis)
rex0104 Apr 12, 2026
53d229f
docs: design spec for scheduled analysis and onboarding wizard
rex0104 Apr 12, 2026
d3d7108
feat: add schedule and onboarding columns to users table (migration v4)
rex0104 Apr 12, 2026
7d7da8d
feat: onboarding API — status check and completion endpoint
rex0104 Apr 12, 2026
80f026b
feat: onboarding wizard — 3-step guided setup for new users
rex0104 Apr 12, 2026
54fee25
feat: per-user schedule service and API for watchlist auto-analysis
rex0104 Apr 12, 2026
314fa0a
feat: schedule settings UI and watchlist schedule badge
rex0104 Apr 12, 2026
60d74ef
docs: changelog for watchlist enrichment, onboarding wizard, schedule…
rex0104 Apr 12, 2026
47d951f
perf: parallel realtime quotes with 60s cache for watchlist enrichment
rex0104 Apr 13, 2026
2a0da63
fix: replace Unicode escapes with actual Chinese text in watchlist sc…
rex0104 Apr 13, 2026
6580dcd
feat: activate per-user scheduled analysis with notification routing
rex0104 Apr 13, 2026
2f41f6e
feat: forgot-password / reset-password flow
rex0104 Apr 14, 2026
bc887b9
feat: mobile adaptation — bottom tab bar, responsive watchlist, OG me…
rex0104 Apr 14, 2026
0720683
feat: use CN stock color convention (up=red, down=green) for price ch…
rex0104 Apr 14, 2026
219c90b
feat: embed K-line chart in report (candlestick + MA5/MA20 + volume, …
rex0104 Apr 14, 2026
aa81d5b
fix: consume ?q= URL param in HomePage to analyze correct watchlist s…
rex0104 Apr 14, 2026
d236a65
docs: add graphify knowledge graph rules to AGENTS.md
rex0104 Apr 14, 2026
76c6e2f
fix: watchlist analyze shows existing history instead of triggering n…
rex0104 Apr 14, 2026
3e596b1
feat: 3-column layout with analyzing banner and market sentiment panel
rex0104 Apr 14, 2026
982863e
feat: show analyzing skeleton placeholder when task running but no re…
rex0104 Apr 14, 2026
4052ffc
feat: remove inline market sentiment from report, keep right column p…
rex0104 Apr 14, 2026
206f409
feat: align three columns under shared header, merge banner and actio…
rex0104 Apr 14, 2026
3ac30be
fix: sidebar top-aligned to page edge, active tab clipped within roun…
rex0104 Apr 14, 2026
d7793a1
fix: align sidebar top padding with search header height
rex0104 Apr 14, 2026
d38aa34
feat: nickname display and editing; fix strategy price display in sid…
rex0104 Apr 14, 2026
3461161
feat: add chartAnnotations utility with TDD tests
rex0104 Apr 17, 2026
4b4dbe5
fix: use trailing 10-element window for MA20 slope in determineTrend
rex0104 Apr 17, 2026
dd96b77
feat(chart): add annotation rendering to KLineChart
rex0104 Apr 17, 2026
d482c08
fix: KLineChart annotation quality — use ref for onDataLoaded, baseVa…
rex0104 Apr 17, 2026
902a272
feat: wire AI annotations from report data to K-line chart
rex0104 Apr 17, 2026
21f5433
feat: add 4 preset stock screening strategies with tests
rex0104 Apr 17, 2026
3e8b8d5
feat: add screener service with async orchestration and LLM summaries
rex0104 Apr 17, 2026
28ead81
feat: add screener SSE API endpoint and strategy list
rex0104 Apr 17, 2026
41e4759
feat: add screener API module and components
rex0104 Apr 17, 2026
2c21e97
feat: add DiscoverPage for AI stock screener
rex0104 Apr 17, 2026
e05194c
feat: add discover page to navigation and routing
rex0104 Apr 17, 2026
951ef16
fix: move onDataLoaded ref update into useEffect to satisfy lint
rex0104 Apr 17, 2026
87db6f5
docs: add design spec and implementation plan for K-line annotations …
rex0104 Apr 17, 2026
b9a443a
fix: use addBaselineSeries for KLineChart support/resistance bands
rex0104 Apr 20, 2026
3da6e87
feat(chat): inline report-scoped chat drawer on HomePage
rex0104 Apr 20, 2026
2d23ebe
feat(dx): smoother first-run — dep-error hint, 404 shows path, README…
rex0104 Apr 21, 2026
ae124b0
docs: design + plan for watchlist reanalyze, identity normalization, …
rex0104 Apr 22, 2026
50c1472
feat(identity): add stock_identity_service with StockIdentityNotFound
rex0104 Apr 22, 2026
550583d
feat(identity): support HK/US prefixes and akshare fallback test
rex0104 Apr 22, 2026
4dbabc6
fix(identity): align Task 2 tests to codebase canonical form (no .HK …
rex0104 Apr 22, 2026
1b2ea09
feat(watchlist): normalize stock identity on add (code is source of t…
rex0104 Apr 22, 2026
1d11e41
feat(analysis): normalize stock identity before persisting history
rex0104 Apr 22, 2026
43c7842
fix(analysis): override result.name with canonical name in sync path
rex0104 Apr 22, 2026
2e64455
fix(analysis): thread canonical_name into pipeline so AnalysisHistory…
rex0104 Apr 22, 2026
c4c07c2
feat(watchlist): split analyze button into history + reanalyze
rex0104 Apr 22, 2026
4500586
feat(home): honor ?force=1 query param to trigger fresh analysis
rex0104 Apr 22, 2026
2d4164b
fix(layout): unify tab pages to AppPage, drop 100vh hacks
rex0104 Apr 22, 2026
72f6178
chore(api): register global StockIdentityNotFound handler as safety net
rex0104 Apr 22, 2026
323b459
docs(changelog): record watchlist/identity/layout changes
rex0104 Apr 22, 2026
e9c6a90
feat(web): support deploying under a configurable sub-path via VITE_B…
rex0104 Apr 26, 2026
08d563a
fix(web): stock index fetch must respect base path for sub-path deploys
rex0104 Apr 26, 2026
f350c43
chore: add restart.sh for production deploy
rex0104 May 13, 2026
7e84b9c
chore: add restart.sh for production deploy
rex0104 May 13, 2026
4fdfc6b
chore: ignore .server.pid runtime file
rex0104 May 13, 2026
34d7eb3
chore: bind to port 3001 per service registry
rex0104 May 13, 2026
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
13 changes: 9 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ AGENT_SKILLS=
# EMAIL_SENDER=
# EMAIL_PASSWORD=
# EMAIL_RECEIVERS=receiver@example.com # 可选,留空则发给自己
# EMAIL_SMTP_HOST=smtp.qq.com # 可选,默认 smtp.qq.com(支持 QQ/163/Gmail 等)
# EMAIL_SMTP_PORT=465 # 可选,默认 465(SMTP_SSL)
#
# 【方式四扩展】股票分组发往不同邮箱(Issue #268,可选)
# STOCK_LIST 决定实际分析范围;STOCK_GROUP_N 仅决定邮件发给谁,建议始终写成 STOCK_LIST 的子集
Expand Down Expand Up @@ -458,12 +460,15 @@ WEBUI_AUTO_BUILD=true
# TRUST_X_FORWARDED_FOR=false

# ===================================
# Web 登录认证(可选)
# Web 登录认证
# ===================================
# 设为 true 启用密码保护;首次访问时在网页设置初始密码,可在「系统设置 > 修改密码」中修改
# 忘记密码可在服务器执行: python -m src.auth reset_password
ADMIN_AUTH_ENABLED=false
# 多用户模式:用户通过邮箱注册/登录,第一个注册用户自动继承旧数据。
# 认证在首个用户注册后自动生效,无需手动配置。
# ADMIN_AUTH_ENABLED 已废弃,保留仅供向后兼容。
# ADMIN_AUTH_ENABLED=false
# ADMIN_SESSION_MAX_AGE_HOURS=24 # Session 有效期(小时)
# 应用外部访问地址(用于密码重置邮件中的链接,留空时自动从请求头推断)
# APP_BASE_URL=https://your-domain.com

# ===========================================
# 图片识别股票代码(设置页「从图片添加」)
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,6 @@ verify_*.py
static/
/apps/dsa-desktop/dist/
/apps/dsa-desktop/node_modules/
.python-version
.gstack/
.server.pid
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,12 @@ gh run view <run_id> --log-failed
- 自动 tag 默认不触发,只有 commit title 包含 `#patch`、`#minor`、`#major` 才会触发版本号更新。
- 手动打 tag 必须使用 annotated tag。
- 用户可见变更优先通过 PR 合入,并补齐 label 与验证说明。

## graphify

This project has a graphify knowledge graph at graphify-out/.

Rules:
- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure
- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
- After modifying code files in this session, run `python3 -c "from graphify.watch import _rebuild_code; from pathlib import Path; _rebuild_code(Path('.'))"` to keep the graph current
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@

`Settings` → `Secrets and variables` → `Actions` → `New repository secret`

> **🎯 最小化可运行配置(~5 分钟)**
>
> 只要配好下面 3 个 Secret,系统就能跑起来。其它所有字段都是可选增强:
>
> | Secret | 示例值 | 作用 |
> |---|---|---|
> | `STOCK_LIST` | `600519,hk00700,AAPL` | 你的自选股(逗号分隔) |
> | `GEMINI_API_KEY` | `AIza...`([免费申请](https://aistudio.google.com/)) | 任意一个 AI Key 即可;替换为 `AIHUBMIX_KEY` / `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` 等也可 |
> | `TELEGRAM_BOT_TOKEN` + `TELEGRAM_CHAT_ID` | — | 任选一个推送渠道;也可替换为 `WECHAT_WEBHOOK_URL` / `FEISHU_WEBHOOK_URL` / `EMAIL_SENDER` 等 |
>
> 配好这 3 项后直接跳到 [第 3 步:启用 Actions](#3-启用-actions)。下方的完整表格只在你需要多数据源、多模型负载均衡、多通知渠道等进阶能力时再查。

**AI 模型配置(至少配置一个)**

> 详细配置说明见 [LLM 配置指南](docs/LLM_CONFIG_GUIDE.md)(极简接入、渠道模式、高级 YAML 路由、Vision、Agent、排错)。默认推荐先选服务商并填写 API Key;需要多模型时再启用渠道模式;只有高级用户才需要 YAML 路由配置。
Expand Down
19 changes: 18 additions & 1 deletion api/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,26 @@ def get_database_manager() -> DatabaseManager:


def get_system_config_service(request: Request) -> SystemConfigService:
"""Get app-lifecycle shared SystemConfigService instance."""
"""Get app-lifecycle shared SystemConfigService instance (fallback for non-user contexts)."""
service = getattr(request.app.state, "system_config_service", None)
if service is None:
service = SystemConfigService()
request.app.state.system_config_service = service
return service


def get_user_config_service(request: Request) -> SystemConfigService:
"""Create a SystemConfigService backed by the current user's DB settings.

Falls back to the global .env-backed service when no authenticated user
is available (e.g. CLI, unauthenticated request).
"""
user_id = getattr(request.state, "user_id", None)
if user_id:
from src.core.user_config_manager import UserConfigManager

db = DatabaseManager.get_instance()
manager = UserConfigManager(db._SessionLocal, user_id)
return SystemConfigService(manager=manager)
# Fallback to global .env for unauthenticated or CLI
return get_system_config_service(request)
64 changes: 39 additions & 25 deletions api/middlewares/auth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
"""
Auth middleware: protect /api/v1/* when admin auth is enabled.
Auth middleware: protect /api/v1/* when users exist in the system.
When no users have registered yet, all endpoints are open.
"""

from __future__ import annotations
Expand All @@ -12,63 +13,76 @@
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware

from src.auth import COOKIE_NAME, is_auth_enabled, verify_session
from src.auth import COOKIE_NAME, verify_session_user

logger = logging.getLogger(__name__)

EXEMPT_PATHS = frozenset({
"/api/v1/auth/login",
"/api/v1/auth/register",
"/api/v1/auth/status",
"/api/v1/auth/logout",
"/api/v1/auth/forgot-password",
"/api/v1/auth/reset-password",
"/api/health",
"/health",
"/docs",
"/redoc",
"/openapi.json",
})

EXEMPT_PREFIXES = (
"/api/v1/share/", # public share links
)


def _path_exempt(path: str) -> bool:
"""Check if path is exempt from auth."""
normalized = path.rstrip("/") or "/"
return normalized in EXEMPT_PATHS
if normalized in EXEMPT_PATHS:
return True
for prefix in EXEMPT_PREFIXES:
if normalized.startswith(prefix):
return True
return False


def has_users() -> bool:
"""Check if any users are registered. Lazy import to avoid circular deps."""
from src.storage import DatabaseManager, User
try:
db = DatabaseManager.get_instance()
with db.get_session() as session:
return session.query(User.id).first() is not None
except Exception:
return False
Comment on lines +56 to +57

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fail closed when user lookup errors in auth middleware

If the has_users() database query throws (e.g. transient DB outage, migration mismatch), this returns False, and dispatch() then skips authentication for every /api/v1/* route. That turns backend failures into an authorization bypass, exposing protected APIs exactly when infrastructure is unstable. The middleware should deny by default (or return 503) when user existence cannot be determined.

Useful? React with 👍 / 👎.



class AuthMiddleware(BaseHTTPMiddleware):
"""Require valid session for /api/v1/* when auth is enabled."""

async def dispatch(
self,
request: Request,
call_next: Callable,
):
if not is_auth_enabled():
return await call_next(request)
"""Require valid user session for /api/v1/* when users exist."""

async def dispatch(self, request: Request, call_next: Callable):
path = request.url.path

if not path.startswith("/api/v1/"):
return await call_next(request)

if _path_exempt(path):
return await call_next(request)

if not path.startswith("/api/v1/"):
if not has_users():
return await call_next(request)

cookie_val = request.cookies.get(COOKIE_NAME)
if not cookie_val or not verify_session(cookie_val):
user_id = verify_session_user(cookie_val) if cookie_val else None
if not user_id:
return JSONResponse(
status_code=401,
content={
"error": "unauthorized",
"message": "Login required",
},
content={"error": "unauthorized", "message": "Login required"},
)

request.state.user_id = user_id
return await call_next(request)


def add_auth_middleware(app):
"""Add auth middleware to protect API routes.

The middleware is always registered; whether auth is enforced is determined
at request time by is_auth_enabled() so the decision stays consistent across
any runtime configuration reload.
"""
app.add_middleware(AuthMiddleware)
13 changes: 13 additions & 0 deletions api/middlewares/error_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from fastapi import Request, Response
from fastapi.responses import JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from src.services.stock_identity_service import StockIdentityNotFound

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -110,6 +111,18 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
}
)

@app.exception_handler(StockIdentityNotFound)
async def stock_identity_not_found_handler(request: Request, exc: StockIdentityNotFound):
"""处理股票代码未找到异常(安全网)"""
return JSONResponse(
status_code=400,
content={
"error": "stock.identity_not_found",
"message": str(exc),
"detail": None
}
)

@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
"""处理通用异常"""
Expand Down
17 changes: 11 additions & 6 deletions api/v1/endpoints/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import uuid
from typing import Any, Dict, List, Optional

from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Request
from fastapi.responses import StreamingResponse
from pydantic import AliasChoices, BaseModel, ConfigDict, Field

Expand Down Expand Up @@ -205,10 +205,11 @@ class SessionMessagesResponse(BaseModel):


@router.get("/chat/sessions", response_model=SessionsResponse)
async def list_chat_sessions(limit: int = 50, user_id: Optional[str] = None):
async def list_chat_sessions(request: Request, limit: int = 50, user_id: Optional[str] = None):
"""获取聊天会话列表

Args:
request: HTTP request (used to extract authenticated user_id).
limit: Maximum number of sessions to return.
user_id: Optional platform-prefixed user identifier for session
isolation. When provided, only sessions whose session_id
Expand All @@ -217,27 +218,31 @@ async def list_chat_sessions(limit: int = 50, user_id: Optional[str] = None):
``feishu_ou_abc``.
"""
from src.storage import get_db
auth_user_id = getattr(request.state, "user_id", None)
sessions = get_db().get_chat_sessions(
limit=limit,
session_prefix=user_id,
extra_session_ids=[user_id] if user_id else None,
user_id=auth_user_id,
)
return SessionsResponse(sessions=sessions)


@router.get("/chat/sessions/{session_id}", response_model=SessionMessagesResponse)
async def get_chat_session_messages(session_id: str, limit: int = 100):
async def get_chat_session_messages(request: Request, session_id: str, limit: int = 100):
"""获取单个会话的完整消息"""
from src.storage import get_db
messages = get_db().get_conversation_messages(session_id, limit=limit)
auth_user_id = getattr(request.state, "user_id", None)
messages = get_db().get_conversation_messages(session_id, limit=limit, user_id=auth_user_id)
return SessionMessagesResponse(session_id=session_id, messages=messages)


@router.delete("/chat/sessions/{session_id}")
async def delete_chat_session(session_id: str):
async def delete_chat_session(request: Request, session_id: str):
"""删除指定会话"""
from src.storage import get_db
count = get_db().delete_conversation_session(session_id)
auth_user_id = getattr(request.state, "user_id", None)
count = get_db().delete_conversation_session(session_id, user_id=auth_user_id)
return {"deleted": count}


Expand Down
Loading