Browse Source

Merge pull request #600 from lcyting/feature/lcyting-StockSage-agent

[毕业设计] StockSage-agent - 智能股票分析助手
jjyaoao 1 month ago
parent
commit
f57792c0ee
100 changed files with 12027 additions and 0 deletions
  1. 38 0
      Co-creation-projects/lcyting-StockSage-agent/.dockerignore
  2. 74 0
      Co-creation-projects/lcyting-StockSage-agent/.env.example
  3. 54 0
      Co-creation-projects/lcyting-StockSage-agent/.gitignore
  4. 448 0
      Co-creation-projects/lcyting-StockSage-agent/DEPLOY.md
  5. 44 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/.gitignore
  6. 437 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/LICENSE
  7. 43 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/.gitignore
  8. 42 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/__init__.py
  9. 18 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/agents/__init__.py
  10. 264 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/agents/react_agent.py
  11. 249 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/agents/reflection_agent.py
  12. 21 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/__init__.py
  13. 71 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/agent.py
  14. 38 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/config.py
  15. 128 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/conversation.py
  16. 145 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/conversation_manager.py
  17. 31 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/exceptions.py
  18. 414 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/llm.py
  19. 50 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/message.py
  20. 65 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/stream.py
  21. 11 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/tools/__init__.py
  22. 74 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/tools/base.py
  23. 132 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/tools/registry.py
  24. 6 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/version.py
  25. 117 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/pyproject.toml
  26. 3 0
      Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/setup.py
  27. 21 0
      Co-creation-projects/lcyting-StockSage-agent/LICENSE
  28. 445 0
      Co-creation-projects/lcyting-StockSage-agent/README.md
  29. 446 0
      Co-creation-projects/lcyting-StockSage-agent/README_EN.md
  30. 2 0
      Co-creation-projects/lcyting-StockSage-agent/agents/__init__.py
  31. 271 0
      Co-creation-projects/lcyting-StockSage-agent/agents/advisor_agent.py
  32. 276 0
      Co-creation-projects/lcyting-StockSage-agent/agents/agent_system.py
  33. 358 0
      Co-creation-projects/lcyting-StockSage-agent/agents/coordinator_agent.py
  34. 179 0
      Co-creation-projects/lcyting-StockSage-agent/agents/data_analysis_agent.py
  35. 127 0
      Co-creation-projects/lcyting-StockSage-agent/agents/general_advisor_agent.py
  36. 175 0
      Co-creation-projects/lcyting-StockSage-agent/agents/sentiment_agent.py
  37. 1 0
      Co-creation-projects/lcyting-StockSage-agent/agents/tests/__init__.py
  38. 60 0
      Co-creation-projects/lcyting-StockSage-agent/agents/text_truncation.py
  39. 2 0
      Co-creation-projects/lcyting-StockSage-agent/agents/tools/__init__.py
  40. 158 0
      Co-creation-projects/lcyting-StockSage-agent/agents/tools/mx_data_tool.py
  41. 189 0
      Co-creation-projects/lcyting-StockSage-agent/agents/tools/mx_search_tool.py
  42. 34 0
      Co-creation-projects/lcyting-StockSage-agent/backend/Dockerfile
  43. 1 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/__init__.py
  44. 1 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/api/__init__.py
  45. 124 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/api/agent_api.py
  46. 86 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/api/analysis.py
  47. 243 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/api/buffett.py
  48. 72 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/api/cache_api.py
  49. 46 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/api/chat.py
  50. 18 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/api/data_analysis.py
  51. 55 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/api/financial.py
  52. 50 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/api/history.py
  53. 48 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/api/market.py
  54. 52 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/api/news.py
  55. 88 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/api/preferences.py
  56. 45 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/api/screener.py
  57. 18 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/api/sentiment.py
  58. 145 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/api/simulation.py
  59. 45 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/api/system_browser.py
  60. 77 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/api/watchlist.py
  61. 175 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/config.py
  62. 229 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/main.py
  63. 1 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/middleware/__init__.py
  64. 11 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/middleware/error_handler.py
  65. 1 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/models/__init__.py
  66. 67 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/models/database.py
  67. 6 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/models/history.py
  68. 35 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/models/history_models.py
  69. 46 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/models/memory_models.py
  70. 144 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/models/preference.py
  71. 37 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/models/report.py
  72. 1 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/services/__init__.py
  73. 361 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/services/analysis_service.py
  74. 619 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/services/buffett_service.py
  75. 21 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/services/chat_service.py
  76. 36 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/services/dashboard_warmup.py
  77. 111 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/services/history_service.py
  78. 414 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/services/market_service.py
  79. 250 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/services/memory_service.py
  80. 88 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/services/mx_timed_cache.py
  81. 402 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/services/news_service.py
  82. 170 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/services/preference_service.py
  83. 192 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/services/screener_service.py
  84. 381 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/services/simulation_service.py
  85. 272 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/services/stock_file_cache.py
  86. 198 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/services/watchlist_service.py
  87. 1 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/utils/__init__.py
  88. 292 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/utils/mock_trading_normalize.py
  89. 46 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/utils/mx_fixture.py
  90. 28 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/utils/mx_http.py
  91. 34 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/utils/mx_quota.py
  92. 67 0
      Co-creation-projects/lcyting-StockSage-agent/backend/app/utils/response.py
  93. 0 0
      Co-creation-projects/lcyting-StockSage-agent/backend/fixtures/mx_raw/.gitkeep
  94. 5 0
      Co-creation-projects/lcyting-StockSage-agent/backend/pytest.ini
  95. 60 0
      Co-creation-projects/lcyting-StockSage-agent/backend/requirements.txt
  96. 75 0
      Co-creation-projects/lcyting-StockSage-agent/backend/scripts/capture_mx_fixture.py
  97. 91 0
      Co-creation-projects/lcyting-StockSage-agent/backend/scripts/smoke_financial_api.py
  98. 1 0
      Co-creation-projects/lcyting-StockSage-agent/backend/tests/__init__.py
  99. 51 0
      Co-creation-projects/lcyting-StockSage-agent/docker-compose.yml
  100. 33 0
      Co-creation-projects/lcyting-StockSage-agent/frontend/Dockerfile

+ 38 - 0
Co-creation-projects/lcyting-StockSage-agent/.dockerignore

@@ -0,0 +1,38 @@
+# =========================================================================
+# Docker 构建忽略文件 — 排除不必要的文件加速构建
+# =========================================================================
+
+# Python
+__pycache__/
+*.py[cod]
+*.egg-info/
+.venv/
+venv/
+
+# Node.js
+node_modules/
+frontend/node_modules/
+frontend/dist/
+
+# Git
+.git/
+.gitignore
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# 文档和日志
+docs/
+*.md
+README.md
+*.log
+
+# 环境变量(在构建时单独COPY)
+# .env 需要被COPY,不忽略
+
+# 临时文件
+tmp/
+temp/

+ 74 - 0
Co-creation-projects/lcyting-StockSage-agent/.env.example

@@ -0,0 +1,74 @@
+# ============================================================================
+# 智能股票分析助手 — 环境变量配置
+# ============================================================================
+# 复制此文件为 .env 并填入你的API密钥
+
+# ============================================================================
+# LLM 大模型配置(兼容 HelloAgents 框架)
+# ============================================================================
+# 模型名称
+LLM_MODEL_ID=deepseek-chat
+
+# API密钥
+LLM_API_KEY=your-deepseek-api-key-here
+
+# 服务地址
+LLM_BASE_URL=https://api.deepseek.com
+
+# 单次 LLM HTTP 超时秒数(可选,默认 60)。后端对单次请求实际使用 max(本值, 180),避免 ReAct/对话助手多轮未到时间就断开。
+# 若仍偶发中断,可改为 300。
+LLM_TIMEOUT=180
+
+# 巴菲特评估:初稿之后的「投资委员会」反思轮数(每轮可能再调 1~2 次 LLM)。默认 0 = 初稿流式输出结束即完成;设为 1 或 2 可启用审稿与改版。
+# BUFFETT_MAX_REFLECTIONS=0
+
+# ============================================================================
+# 东方财富妙想API配置(外部金融数据)
+# ============================================================================
+# API密钥 — 从 https://dl.dfcfs.com/m/itc4 获取
+MX_APIKEY=your-mx-apikey-here
+# API基础地址(默认无需修改)
+MX_API_URL=https://mkapi2.dfcfs.com/finskillshub
+# 妙想查询进程内缓存 TTL(秒),默认 600
+# MX_CACHE_TTL_SECONDS=600
+# 本地回放 fixture,不调妙想 HTTP(开发调试用)
+# MX_REPLAY_FIXTURES=false
+# MX_FIXTURE_DIR=backend/fixtures/mx_raw
+
+# ============================================================================
+# 项目服务配置
+# ============================================================================
+# 后端服务
+BACKEND_HOST=0.0.0.0
+# 本地开发默认 8000(须与 frontend/vite.config.js 中 proxy.target 一致)
+BACKEND_PORT=8000
+# PyInstaller exe 未设置时 config.py 自动使用 5174,可在 exe 旁 .env 中覆盖:
+# BACKEND_PORT=5174
+BACKEND_DEBUG=true
+
+# 前端服务
+FRONTEND_PORT=5173
+
+# 数据库路径
+DATABASE_URL=sqlite:///./data/stock_analyzer.db
+
+# ============================================================================
+# Redis 缓存配置(预留 — 当前版本未使用)
+# ============================================================================
+# REDIS_HOST=localhost
+# REDIS_PORT=6379
+# REDIS_DB=0
+# REDIS_PASSWORD=
+
+# ============================================================================
+# JWT 认证配置(预留 — 当前版本未使用,见 README「未来计划」)
+# ============================================================================
+# JWT_SECRET_KEY=change-this-to-a-random-secret-key
+# JWT_ALGORITHM=HS256
+# JWT_EXPIRE_MINUTES=1440
+
+# ============================================================================
+# exe 打包选项
+# ============================================================================
+# 设置为 1/true/rebuild 时,scripts/build_exe.py 强制重建前端
+# BUILD_EXE=1

+ 54 - 0
Co-creation-projects/lcyting-StockSage-agent/.gitignore

@@ -0,0 +1,54 @@
+# ============================================================================
+# 通用
+# ============================================================================
+*.py[cod]
+*.egg-info/
+*.spec
+dist/
+dist_exe/
+build/
+*.egg
+.venv/
+venv/
+env/
+
+# IDE
+.idea/
+
+# 环境变量(含真实 API Key,勿提交)
+.env
+
+# ============================================================================
+# 外部Skills中的非代码文件
+# ============================================================================
+skills/巴菲特投资思维/.git
+skills/巴菲特投资思维/bft.mp4
+!skills/巴菲特投资思维/.gitignore
+
+# ============================================================================
+# 临时文件
+# ============================================================================
+tmp/
+temp/
+*.log
+*.txt
+!requirements.txt
+
+# ============================================================================
+# 运行时数据(仓库根目录 data/:默认忽略内容,仅跟踪示例文件)
+# ============================================================================
+data/*
+!data/.gitkeep
+!data/sample_code.py
+!data/test_cases.json
+
+# 妙想原始响应回放文件(本地生成,勿提交)
+backend/fixtures/mx_raw/*.json
+!backend/fixtures/mx_raw/.gitkeep
+
+# ============================================================================
+# 前端
+# ============================================================================
+frontend/dist/
+frontend/node_modules/
+frontend/node_modules/.vite/

+ 448 - 0
Co-creation-projects/lcyting-StockSage-agent/DEPLOY.md

@@ -0,0 +1,448 @@
+# 智能股票分析助手 — 部署文档 (DEPLOY.md)
+
+> **版本**: v0.1.0  
+> **日期**: 2026-05-09  
+> **适用**: 生产环境 / 开发环境部署
+
+---
+
+## 目录
+
+1. [环境要求](#1-环境要求)
+2. [本地开发部署](#2-本地开发部署)
+3. [Docker 容器化部署](#3-docker-容器化部署)
+4. [exe 独立打包部署](#4-exe-独立打包部署)
+5. [配置说明](#5-配置说明)
+6. [健康检查](#6-健康检查)
+7. [常见问题](#7-常见问题)
+
+---
+
+## 1. 环境要求
+
+| 组件 | 最低版本 | 说明 |
+|------|---------|------|
+| Python | 3.10+ | 后端运行时 |
+| Node.js | 18+ | 前端构建 |
+| Docker | 24+ | 容器化部署(可选) |
+| Docker Compose | 2.0+ | 服务编排(可选) |
+| Git | 2.0+ | 版本控制 |
+
+### 外部服务依赖
+
+| 服务 | 用途 | 必需? |
+|------|------|--------|
+| DeepSeek API | LLM 大模型推理 | 是(智能体功能) |
+| 东方财富妙想 API | 金融数据获取 | 是(行情/财务/资讯) |
+
+---
+
+## 2. 本地开发部署
+
+### 2.1 克隆项目
+
+```bash
+git clone <your-repo-url>
+cd 智能股票分析器
+```
+
+### 2.2 配置环境变量
+
+```bash
+cp .env.example .env
+# 编辑 .env,填入 LLM_API_KEY、MX_APIKEY
+# 本地开发请使用 BACKEND_PORT=8000(与 vite proxy 一致)
+```
+
+### 2.3 后端启动
+
+```bash
+# 创建虚拟环境(推荐)
+python -m venv venv
+source venv/bin/activate  # Linux/Mac
+# venv\Scripts\activate   # Windows
+
+# 安装依赖
+pip install -r backend/requirements.txt
+
+# 启动后端服务(开发模式,热重载)
+cd backend
+python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
+
+# 或从项目根目录启动
+python -m uvicorn backend.app.main:app --host 0.0.0.0 --port 8000 --reload
+```
+
+API 文档地址:http://localhost:8000/docs
+
+### 2.4 前端启动
+
+```bash
+cd frontend
+npm install
+npm run dev
+```
+
+前端访问地址:http://localhost:5173
+
+> 开发模式下 Vite 自动将 `/api` 代理到 `http://localhost:8000`,无需额外配置。
+
+### 2.5 验证
+
+```bash
+# 健康检查(端口与 BACKEND_PORT 一致,默认开发为 8000)
+curl http://localhost:8000/api/v1/system/health
+
+# 前端构建验证
+cd frontend && npm run build
+```
+
+> **端口提示**:`backend/app/config.py` 中开发模式默认后端端口为 **8000**,与 `frontend/vite.config.js` 里 `/api` → `http://localhost:8000` 一致。若在 `.env` 中修改 `BACKEND_PORT`,请同步修改 Vite `proxy.target`,否则前端无法代理到后端。
+
+---
+
+## 3. Docker 容器化部署
+
+### 3.1 项目结构
+
+```
+智能股票分析器/
+├── backend/           # 后端 FastAPI
+│   └── Dockerfile
+├── frontend/          # 前端 Vue3
+│   ├── Dockerfile
+│   └── nginx.conf
+├── docker-compose.yml # 服务编排
+├── .dockerignore      # 构建忽略
+└── .env               # 环境变量
+```
+
+### 3.2 一键启动
+
+```bash
+# 确保 .env 已配置正确
+docker compose up -d
+```
+
+### 3.3 分步构建
+
+```bash
+# 构建后端镜像
+docker build -t stock-analyzer-backend -f backend/Dockerfile .
+
+# 构建前端镜像
+docker build -t stock-analyzer-frontend -f frontend/Dockerfile .
+
+# 运行后端
+docker run -d -p 8000:8000 \
+  -v stock_data:/app/data \
+  --name stock-backend \
+  stock-analyzer-backend
+
+# 运行前端
+docker run -d -p 8080:80 \
+  --name stock-frontend \
+  stock-analyzer-frontend
+```
+
+### 3.4 服务端口
+
+| 服务 | 端口 | 访问地址 |
+|------|------|----------|
+| 后端 API | 8000 | http://localhost:8000/docs |
+| 前端界面 | 8080 | http://localhost:8080 |
+
+### 3.5 常用命令
+
+```bash
+# 查看服务状态
+docker compose ps
+
+# 查看日志
+docker compose logs -f backend
+docker compose logs -f frontend
+
+# 重启服务
+docker compose restart
+
+# 停止并清理
+docker compose down
+
+# 重新构建并启动
+docker compose up -d --build
+```
+
+### 3.6 数据持久化
+
+SQLite 数据库通过 Docker Volume 持久化:
+
+- **Volume 名称**: `stock_analyzer_data`
+- **挂载路径**: `/app/data`
+- **数据库文件**: `/app/data/stock_analyzer.db`
+
+```bash
+# 查看 Volume
+docker volume ls | grep stock
+
+# 备份数据库
+docker compose exec backend python -c "
+import shutil
+shutil.copy('/app/data/stock_analyzer.db', '/tmp/backup.db')
+"
+docker compose cp backend:/tmp/backup.db ./backup.db
+```
+
+---
+
+## 4. exe 独立打包部署
+
+将前后端打包为一个独立 `.exe` 文件,无需安装 Python/Node.js 即可运行。
+
+### 4.1 环境要求
+
+| 组件 | 用途 | 仅打包时需要? |
+|------|------|:---:|
+| Python 3.10+ | PyInstaller 打包 | 是 |
+| Node.js 18+ | 前端构建 | 是 |
+| PyInstaller | Python → exe | 是 |
+
+> 运行时仅需 Windows 系统,无需任何依赖。
+
+### 4.2 一键打包
+
+```bash
+# 1. 安装打包依赖
+pip install pyinstaller
+
+# 2. 执行打包脚本(从项目根目录)
+python scripts/build_exe.py
+
+# 3. 或设置环境变量强制重建前端
+# 编辑 .env,设置 BUILD_EXE=1,然后执行上述命令
+```
+
+### 4.3 打包产物
+
+```
+dist_exe/
+├── stock_analyzer.exe      # 主程序(前后端合一)
+├── .env.example             # 配置模板
+└── data/                    # 数据目录(运行时自动使用)
+```
+
+### 4.4 使用方式
+
+```bash
+# 1. 将 dist_exe/ 目录拷贝到目标 Windows 机器
+# 2. 将 .env.example 重命名为 .env
+# 3. 编辑 .env,填入 API Key(LLM_API_KEY、MX_APIKEY)
+# 4. 双击 stock_analyzer.exe 启动
+# 5. 浏览器访问 http://127.0.0.1:<BACKEND_PORT>/dashboard(默认与 `app.config` 一致:exe 常为 5174,以 exe 旁 `.env` 为准)
+```
+
+- 启动后自动打开浏览器(设置环境变量 `NO_BROWSER=1` 可禁用自动打开)
+- exe 窗口显示运行日志
+- 退出时关闭窗口即可
+
+### 4.5 环境变量触发
+
+可通过环境变量 `BUILD_EXE` 控制打包行为:
+
+```bash
+# Windows PowerShell
+$env:BUILD_EXE="1"
+python scripts/build_exe.py
+
+# 或在 .env 中设置
+# BUILD_EXE=1
+```
+
+### 4.6 自定义端口
+
+编辑 `.env`:
+```
+BACKEND_HOST=0.0.0.0
+BACKEND_PORT=9000
+```
+重启 exe 即可。
+
+---
+
+## 5. 配置说明
+
+### 5.1 环境变量完整列表
+
+| 变量名 | 默认值 | 说明 |
+|--------|--------|------|
+| `LLM_MODEL_ID` | `deepseek-chat` | LLM 模型名称 |
+| `LLM_API_KEY` | — | **必需** LLM API 密钥 |
+| `LLM_BASE_URL` | `https://api.deepseek.com` | LLM 服务地址 |
+| `LLM_TIMEOUT` | `60` | LLM HTTP 超时(秒);后端会与更长下限合并,避免多轮 Agent 过早断开 |
+| `BUFFETT_MAX_REFLECTIONS` | `0` | 巴菲特评估初稿后的反思轮数(可选,见 `.env.example`) |
+| `MX_APIKEY` | — | **必需** 东方财富妙想 API 密钥 |
+| `MX_API_URL` | `https://mkapi2.dfcfs.com/finskillshub` | 妙想 API 地址 |
+| `MX_CACHE_TTL_SECONDS` | `600` | 妙想查询进程内缓存 TTL(秒) |
+| `MX_REPLAY_FIXTURES` | 关闭 | 为 true 时优先回放 `MX_FIXTURE_DIR` 下 fixture,不调妙想 HTTP |
+| `MX_FIXTURE_DIR` | `backend/fixtures/mx_raw` | 回放目录 |
+| `BACKEND_HOST` | `0.0.0.0` | 后端监听地址 |
+| `BACKEND_PORT` | **开发 `8000`** / **exe 默认 `5174`** | 未设置环境变量时由 `config.py` 按是否冻结自动选择 |
+| `FRONTEND_PORT` | `5173` | 前端开发端口 |
+| `FRONTEND_DIR` | — | 可选:显式指定已构建的前端 `dist` 目录 |
+| `DATA_DIR` | — | 可选:数据目录;默认 exe 旁或项目根下 `data` |
+| `DATABASE_URL` | `sqlite:///./data/stock_analyzer.db` | 数据库连接 |
+| `BUILD_EXE` | — | 打包脚本使用:`1`/`true`/`rebuild` 时强制重建前端 |
+| `REDIS_*` | 见 `.env.example` | **预留**,当前版本未使用(`requirements.txt` 中 redis 已注释) |
+| `JWT_SECRET_KEY` | `dev-secret-key` | **预留**,当前版本无登录鉴权,可不配置 |
+| `JWT_EXPIRE_MINUTES` | `1440` | **预留**,接入用户认证后生效 |
+
+接口路径补充(与 Swagger 一致):
+
+- AI 舆情流式:`POST /api/v1/sentiment/analyze/stream`(兼容:`POST /api/v1/agent/sentiment/stream`)
+- AI 数据流式:`POST /api/v1/data-analysis/analyze/stream`(兼容:`POST /api/v1/agent/data-analysis/stream`)
+- exe / 桌面:`POST /api/v1/system/open-external-url` 在本机默认浏览器打开允许的 http(s) 链接
+
+### 5.2 安全配置(生产环境)
+
+当前版本 **不要求** JWT;对外暴露 API 时建议:
+
+- 使用 Nginx/网关限制来源 IP 或加独立鉴权层
+- 勿将 `.env` 中的 `LLM_API_KEY`、`MX_APIKEY` 提交到版本库
+- 用户认证(JWT)实现后,可用以下命令预生成密钥:
+
+```bash
+python -c "import secrets; print(secrets.token_urlsafe(32))"
+# 写入 .env: JWT_SECRET_KEY=<生成的密钥>
+```
+
+### 5.3 Nginx 反向代理配置(生产示例)
+
+```nginx
+server {
+    listen 80;
+    server_name your-domain.com;
+
+    # 前端静态文件
+    location / {
+        proxy_pass http://frontend:80;
+    }
+
+    # 后端 API
+    location /api {
+        proxy_pass http://backend:8000;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_read_timeout 300s;
+    }
+}
+```
+
+---
+
+## 6. 健康检查
+
+### 6.1 后端健康检查
+
+```bash
+curl http://localhost:8000/api/v1/system/health
+```
+
+正常响应:
+```json
+{
+  "code": 0,
+  "message": "success",
+  "data": {
+    "status": "ok",
+    "version": "0.1.0",
+    "agent_ready": true,
+    "skills_ready": true
+  }
+}
+```
+
+- `agent_ready: false` → LLM_API_KEY 未配置
+- `skills_ready: false` → MX_APIKEY 未配置
+
+### 6.2 Docker 健康检查
+
+Docker Compose 自动监控后端 `/api/v1/system/health` 端点,30秒间隔检查。
+
+```bash
+# 查看健康状态
+docker compose ps
+# 输出中 (healthy) 表示通过
+```
+
+---
+
+## 7. 常见问题
+
+### Q: 如何获取 API 密钥?
+
+- **DeepSeek API**: https://platform.deepseek.com
+- **东方财富妙想 API**: https://dl.dfcfs.com/m/itc4
+
+### Q: 启动后前端能访问但数据为空?
+
+检查 `.env` 中的 `MX_APIKEY` 是否有效,运行健康检查确认 `skills_ready: true`。
+
+### Q: Docker 构建速度慢?
+
+项目已配置 `.dockerignore` 排除不必要的文件。首次构建需下载基础镜像,后续使用缓存。
+
+### Q: SQLite 数据库如何迁移至 PostgreSQL?
+
+修改 `DATABASE_URL`:
+```
+DATABASE_URL=postgresql://user:password@host:5432/stock_analyzer
+```
+并在 `requirements.txt` 中替换 `aiosqlite` 为 `asyncpg`。
+
+### Q: 如何扩容至多副本?
+
+后端无状态设计支持多副本(SQLite 需切换为 PostgreSQL/MySQL):
+
+```yaml
+# docker-compose.yml
+services:
+  backend:
+    deploy:
+      replicas: 3
+  frontend:
+    deploy:
+      replicas: 2
+```
+
+> 注意:多副本时需将数据存储切换为数据库服务器(PostgreSQL)并添加 Redis 缓存。
+
+---
+
+## 附录
+
+### A. 网络架构
+
+```
+浏览器(8080)
+    │
+    ▼
+Nginx(前端容器:80)
+    │ /           → dist/ (SPA静态文件)
+    │ /api/*      → proxy_pass
+    │
+    ▼
+FastAPI(后端容器:8000)
+    │
+    ├── SQLite (/app/data)
+    ├── HelloAgents (智能体推理)
+    └── 东方财富妙想API (外部金融数据)
+```
+
+### B. 开发 vs 生产对比
+
+| 项目 | 开发 | 生产 |
+|------|------|------|
+| 后端启动 | `uvicorn --reload` | `uvicorn` (无热重载) |
+| 前端启动 | `vite dev` (5173) | Nginx (80) |
+| API 代理 | Vite proxy | Nginx reverse proxy |
+| 数据库 | 本地文件 | Docker Volume |
+| CORS | 允许所有来源 | 仅允许前端域名 |

+ 44 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/.gitignore

@@ -0,0 +1,44 @@
+# Python
+__pycache__/
+*.py[cod]
+*.so
+*.egg-info/
+dist/
+build/
+eggs/
+*.egg
+.eggs/
+
+# Virtual environments
+.venv/
+venv/
+env/
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# Testing
+.pytest_cache/
+.coverage
+htmlcov/
+.tox/
+
+# Linting
+.ruff_cache/
+.mypy_cache/
+
+# Environment
+.env
+.env.local
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Runtime data (generated)
+data/curriculum_progress/
+data/eval_data/
+data/eval_reports/

+ 437 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/LICENSE

@@ -0,0 +1,437 @@
+Attribution-NonCommercial-ShareAlike 4.0 International
+
+=======================================================================
+
+Creative Commons Corporation ("Creative Commons") is not a law firm and
+does not provide legal services or legal advice. Distribution of
+Creative Commons public licenses does not create a lawyer-client or
+other relationship. Creative Commons makes its licenses and related
+information available on an "as-is" basis. Creative Commons gives no
+warranties regarding its licenses, any material licensed under their
+terms and conditions, or any related information. Creative Commons
+disclaims all liability for damages resulting from their use to the
+fullest extent possible.
+
+Using Creative Commons Public Licenses
+
+Creative Commons public licenses provide a standard set of terms and
+conditions that creators and other rights holders may use to share
+original works of authorship and other material subject to copyright
+and certain other rights specified in the public license below. The
+following considerations are for informational purposes only, are not
+exhaustive, and do not form part of our licenses.
+
+     Considerations for licensors: Our public licenses are
+     intended for use by those authorized to give the public
+     permission to use material in ways otherwise restricted by
+     copyright and certain other rights. Our licenses are
+     irrevocable. Licensors should read and understand the terms
+     and conditions of the license they choose before applying it.
+     Licensors should also secure all rights necessary before
+     applying our licenses so that the public can reuse the
+     material as expected. Licensors should clearly mark any
+     material not subject to the license. This includes other CC-
+     licensed material, or material used under an exception or
+     limitation to copyright. More considerations for licensors:
+    wiki.creativecommons.org/Considerations_for_licensors
+
+     Considerations for the public: By using one of our public
+     licenses, a licensor grants the public permission to use the
+     licensed material under specified terms and conditions. If
+     the licensor's permission is not necessary for any reason--for
+     example, because of any applicable exception or limitation to
+     copyright--then that use is not regulated by the license. Our
+     licenses grant only permissions under copyright and certain
+     other rights that a licensor has authority to grant. Use of
+     the licensed material may still be restricted for other
+     reasons, including because others have copyright or other
+     rights in the material. A licensor may make special requests,
+     such as asking that all changes be marked or described.
+     Although not required by our licenses, you are encouraged to
+     respect those requests where reasonable. More considerations
+     for the public:
+    wiki.creativecommons.org/Considerations_for_licensees
+
+=======================================================================
+
+Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
+Public License
+
+By exercising the Licensed Rights (defined below), You accept and agree
+to be bound by the terms and conditions of this Creative Commons
+Attribution-NonCommercial-ShareAlike 4.0 International Public License
+("Public License"). To the extent this Public License may be
+interpreted as a contract, You are granted the Licensed Rights in
+consideration of Your acceptance of these terms and conditions, and the
+Licensor grants You such rights in consideration of benefits the
+Licensor receives from making the Licensed Material available under
+these terms and conditions.
+
+
+Section 1 -- Definitions.
+
+  a. Adapted Material means material subject to Copyright and Similar
+     Rights that is derived from or based upon the Licensed Material
+     and in which the Licensed Material is translated, altered,
+     arranged, transformed, or otherwise modified in a manner requiring
+     permission under the Copyright and Similar Rights held by the
+     Licensor. For purposes of this Public License, where the Licensed
+     Material is a musical work, performance, or sound recording,
+     Adapted Material is always produced where the Licensed Material is
+     synched in timed relation with a moving image.
+
+  b. Adapter's License means the license You apply to Your Copyright
+     and Similar Rights in Your contributions to Adapted Material in
+     accordance with the terms and conditions of this Public License.
+
+  c. BY-NC-SA Compatible License means a license listed at
+     creativecommons.org/compatiblelicenses, approved by Creative
+     Commons as essentially the equivalent of this Public License.
+
+  d. Copyright and Similar Rights means copyright and/or similar rights
+     closely related to copyright including, without limitation,
+     performance, broadcast, sound recording, and Sui Generis Database
+     Rights, without regard to how the rights are labeled or
+     categorized. For purposes of this Public License, the rights
+     specified in Section 2(b)(1)-(2) are not Copyright and Similar
+     Rights.
+
+  e. Effective Technological Measures means those measures that, in the
+     absence of proper authority, may not be circumvented under laws
+     fulfilling obligations under Article 11 of the WIPO Copyright
+     Treaty adopted on December 20, 1996, and/or similar international
+     agreements.
+
+  f. Exceptions and Limitations means fair use, fair dealing, and/or
+     any other exception or limitation to Copyright and Similar Rights
+     that applies to Your use of the Licensed Material.
+
+  g. License Elements means the license attributes listed in the name
+     of a Creative Commons Public License. The License Elements of this
+     Public License are Attribution, NonCommercial, and ShareAlike.
+
+  h. Licensed Material means the artistic or literary work, database,
+     or other material to which the Licensor applied this Public
+     License.
+
+  i. Licensed Rights means the rights granted to You subject to the
+     terms and conditions of this Public License, which are limited to
+     all Copyright and Similar Rights that apply to Your use of the
+     Licensed Material and that the Licensor has authority to license.
+
+  j. Licensor means the individual(s) or entity(ies) granting rights
+     under this Public License.
+
+  k. NonCommercial means not primarily intended for or directed towards
+     commercial advantage or monetary compensation. For purposes of
+     this Public License, the exchange of the Licensed Material for
+     other material subject to Copyright and Similar Rights by digital
+     file-sharing or similar means is NonCommercial provided there is
+     no payment of monetary compensation in connection with the
+     exchange.
+
+  l. Share means to provide material to the public by any means or
+     process that requires permission under the Licensed Rights, such
+     as reproduction, public display, public performance, distribution,
+     dissemination, communication, or importation, and to make material
+     available to the public including in ways that members of the
+     public may access the material from a place and at a time
+     individually chosen by them.
+
+  m. Sui Generis Database Rights means rights other than copyright
+     resulting from Directive 96/9/EC of the European Parliament and of
+     the Council of 11 March 1996 on the legal protection of databases,
+     as amended and/or succeeded, as well as other essentially
+     equivalent rights anywhere in the world.
+
+  n. You means the individual or entity exercising the Licensed Rights
+     under this Public License. Your has a corresponding meaning.
+
+
+Section 2 -- Scope.
+
+  a. License grant.
+
+       1. Subject to the terms and conditions of this Public License,
+          the Licensor hereby grants You a worldwide, royalty-free,
+          non-sublicensable, non-exclusive, irrevocable license to
+          exercise the Licensed Rights in the Licensed Material to:
+
+            a. reproduce and Share the Licensed Material, in whole or
+               in part, for NonCommercial purposes only; and
+
+            b. produce, reproduce, and Share Adapted Material for
+               NonCommercial purposes only.
+
+       2. Exceptions and Limitations. For the avoidance of doubt, where
+          Exceptions and Limitations apply to Your use, this Public
+          License does not apply, and You do not need to comply with
+          its terms and conditions.
+
+       3. Term. The term of this Public License is specified in Section
+          6(a).
+
+       4. Media and formats; technical modifications allowed. The
+          Licensor authorizes You to exercise the Licensed Rights in
+          all media and formats whether now known or hereafter created,
+          and to make technical modifications necessary to do so. The
+          Licensor waives and/or agrees not to assert any right or
+          authority to forbid You from making technical modifications
+          necessary to exercise the Licensed Rights, including
+          technical modifications necessary to circumvent Effective
+          Technological Measures. For purposes of this Public License,
+          simply making modifications authorized by this Section 2(a)
+          (4) never produces Adapted Material.
+
+       5. Downstream recipients.
+
+            a. Offer from the Licensor -- Licensed Material. Every
+               recipient of the Licensed Material automatically
+               receives an offer from the Licensor to exercise the
+               Licensed Rights under the terms and conditions of this
+               Public License.
+
+            b. Additional offer from the Licensor -- Adapted Material.
+               Every recipient of Adapted Material from You
+               automatically receives an offer from the Licensor to
+               exercise the Licensed Rights in the Adapted Material
+               under the conditions of the Adapter's License You apply.
+
+            c. No downstream restrictions. You may not offer or impose
+               any additional or different terms or conditions on, or
+               apply any Effective Technological Measures to, the
+               Licensed Material if doing so restricts exercise of the
+               Licensed Rights by any recipient of the Licensed
+               Material.
+
+       6. No endorsement. Nothing in this Public License constitutes or
+          may be construed as permission to assert or imply that You
+          are, or that Your use of the Licensed Material is, connected
+          with, or sponsored, endorsed, or granted official status by,
+          the Licensor or others designated to receive attribution as
+          provided in Section 3(a)(1)(A)(i).
+
+  b. Other rights.
+
+       1. Moral rights, such as the right of integrity, are not
+          licensed under this Public License, nor are publicity,
+          privacy, and/or other similar personality rights; however, to
+          the extent possible, the Licensor waives and/or agrees not to
+          assert any such rights held by the Licensor to the limited
+          extent necessary to allow You to exercise the Licensed
+          Rights, but not otherwise.
+
+       2. Patent and trademark rights are not licensed under this
+          Public License.
+
+       3. To the extent possible, the Licensor waives any right to
+          collect royalties from You for the exercise of the Licensed
+          Rights, whether directly or through a collecting society
+          under any voluntary or waivable statutory or compulsory
+          licensing scheme. In all other cases the Licensor expressly
+          reserves any right to collect such royalties, including when
+          the Licensed Material is used other than for NonCommercial
+          purposes.
+
+
+Section 3 -- License Conditions.
+
+Your exercise of the Licensed Rights is expressly made subject to the
+following conditions.
+
+  a. Attribution.
+
+       1. If You Share the Licensed Material (including in modified
+          form), You must:
+
+            a. retain the following if it is supplied by the Licensor
+               with the Licensed Material:
+
+                 i. identification of the creator(s) of the Licensed
+                    Material and any others designated to receive
+                    attribution, in any reasonable manner requested by
+                    the Licensor (including by pseudonym if
+                    designated);
+
+                ii. a copyright notice;
+
+               iii. a notice that refers to this Public License;
+
+                iv. a notice that refers to the disclaimer of
+                    warranties;
+
+                 v. a URI or hyperlink to the Licensed Material to the
+                    extent reasonably practicable;
+
+            b. indicate if You modified the Licensed Material and
+               retain an indication of any previous modifications; and
+
+            c. indicate the Licensed Material is licensed under this
+               Public License, and include the text of, or the URI or
+               hyperlink to, this Public License.
+
+       2. You may satisfy the conditions in Section 3(a)(1) in any
+          reasonable manner based on the medium, means, and context in
+          which You Share the Licensed Material. For example, it may be
+          reasonable to satisfy the conditions by providing a URI or
+          hyperlink to a resource that includes the required
+          information.
+       3. If requested by the Licensor, You must remove any of the
+          information required by Section 3(a)(1)(A) to the extent
+          reasonably practicable.
+
+  b. ShareAlike.
+
+     In addition to the conditions in Section 3(a), if You Share
+     Adapted Material You produce, the following conditions also apply.
+
+       1. The Adapter's License You apply must be a Creative Commons
+          license with the same License Elements, this version or
+          later, or a BY-NC-SA Compatible License.
+
+       2. You must include the text of, or the URI or hyperlink to, the
+          Adapter's License You apply. You may satisfy this condition
+          in any reasonable manner based on the medium, means, and
+          context in which You Share Adapted Material.
+
+       3. You may not offer or impose any additional or different terms
+          or conditions on, or apply any Effective Technological
+          Measures to, Adapted Material that restrict exercise of the
+          rights granted under the Adapter's License You apply.
+
+
+Section 4 -- Sui Generis Database Rights.
+
+Where the Licensed Rights include Sui Generis Database Rights that
+apply to Your use of the Licensed Material:
+
+  a. for the avoidance of doubt, Section 2(a)(1) grants You the right
+     to extract, reuse, reproduce, and Share all or a substantial
+     portion of the contents of the database for NonCommercial purposes
+     only;
+
+  b. if You include all or a substantial portion of the database
+     contents in a database in which You have Sui Generis Database
+     Rights, then the database in which You have Sui Generis Database
+     Rights (but not its individual contents) is Adapted Material,
+     including for purposes of Section 3(b); and
+
+  c. You must comply with the conditions in Section 3(a) if You Share
+     all or a substantial portion of the contents of the database.
+
+For the avoidance of doubt, this Section 4 supplements and does not
+replace Your obligations under this Public License where the Licensed
+Rights include other Copyright and Similar Rights.
+
+
+Section 5 -- Disclaimer of Warranties and Limitation of Liability.
+
+  a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
+     EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
+     AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
+     ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
+     IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
+     WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
+     PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
+     ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
+     KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
+     ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
+
+  b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
+     TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
+     NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
+     INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
+     COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
+     USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
+     ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
+     DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
+     IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
+
+  c. The disclaimer of warranties and limitation of liability provided
+     above shall be interpreted in a manner that, to the extent
+     possible, most closely approximates an absolute disclaimer and
+     waiver of all liability.
+
+
+Section 6 -- Term and Termination.
+
+  a. This Public License applies for the term of the Copyright and
+     Similar Rights licensed here. However, if You fail to comply with
+     this Public License, then Your rights under this Public License
+     terminate automatically.
+
+  b. Where Your right to use the Licensed Material has terminated under
+     Section 6(a), it reinstates:
+
+       1. automatically as of the date the violation is cured, provided
+          it is cured within 30 days of Your discovery of the
+          violation; or
+
+       2. upon express reinstatement by the Licensor.
+
+     For the avoidance of doubt, this Section 6(b) does not affect any
+     right the Licensor may have to seek remedies for Your violations
+     of this Public License.
+
+  c. For the avoidance of doubt, the Licensor may also offer the
+     Licensed Material under separate terms or conditions or stop
+     distributing the Licensed Material at any time; however, doing so
+     will not terminate this Public License.
+
+  d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
+     License.
+
+
+Section 7 -- Other Terms and Conditions.
+
+  a. The Licensor shall not be bound by any additional or different
+     terms or conditions communicated by You unless expressly agreed.
+
+  b. Any arrangements, understandings, or agreements regarding the
+     Licensed Material not stated herein are separate from and
+     independent of the terms and conditions of this Public License.
+
+
+Section 8 -- Interpretation.
+
+  a. For the avoidance of doubt, this Public License does not, and
+     shall not be interpreted to, reduce, limit, restrict, or impose
+     conditions on any use of the Licensed Material that could lawfully
+     be made without permission under this Public License.
+
+  b. To the extent possible, if any provision of this Public License is
+     deemed unenforceable, it shall be automatically reformed to the
+     minimum extent necessary to make it enforceable. If the provision
+     cannot be reformed, it shall be severed from this Public License
+     without affecting the enforceability of the remaining terms and
+     conditions.
+
+  c. No term or condition of this Public License will be waived and no
+     failure to comply consented to unless expressly agreed to by the
+     Licensor.
+
+  d. Nothing in this Public License constitutes or may be interpreted
+     as a limitation upon, or waiver of, any privileges and immunities
+     that apply to the Licensor or You, including from the legal
+     processes of any jurisdiction or authority.
+
+=======================================================================
+
+Creative Commons is not a party to its public
+licenses. Notwithstanding, Creative Commons may elect to apply one of
+its public licenses to material it publishes and in those instances
+will be considered the “Licensor.” The text of the Creative Commons
+public licenses is dedicated to the public domain under the CC0 Public
+Domain Dedication. Except for the limited purpose of indicating that
+material is shared under a Creative Commons public license or as
+otherwise permitted by the Creative Commons policies published at
+creativecommons.org/policies, Creative Commons does not authorize the
+use of the trademark "Creative Commons" or any other trademark or logo
+of Creative Commons without its prior written consent including,
+without limitation, in connection with any unauthorized modifications
+to any of its public licenses or any other arrangements,
+understandings, or agreements concerning use of licensed material. For
+the avoidance of doubt, this paragraph does not form part of the
+public licenses.
+
+Creative Commons may be contacted at creativecommons.org.

+ 43 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/.gitignore

@@ -0,0 +1,43 @@
+# Python
+__pycache__/
+*.py[cod]
+*.so
+*.egg-info/
+dist/
+build/
+eggs/
+*.egg
+.eggs/
+
+# Virtual environments
+.venv/
+venv/
+env/
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+
+# Testing
+.pytest_cache/
+.coverage
+htmlcov/
+.tox/
+
+# Linting
+.ruff_cache/
+
+# Environment
+.env
+.env.local
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Runtime data (generated)
+data/curriculum_progress/
+data/eval_data/
+data/eval_reports/

+ 42 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/__init__.py

@@ -0,0 +1,42 @@
+"""
+HelloAgents - 轻量级多智能体框架(StockSage 精简版)
+
+仅保留 StockSage 项目实际使用的核心组件与 Agent 范式。
+"""
+
+from .version import __version__, __author__, __email__, __description__
+
+from .core.llm import HelloAgentsLLM
+from .core.config import Config
+from .core.message import Message
+from .core.exceptions import HelloAgentsException
+from .core.stream import StreamEvent
+
+from .agents.react_agent import ReActAgent
+from .agents.reflection_agent import ReflectionAgent
+
+from .tools.registry import ToolRegistry, global_registry
+from .tools.base import Tool, ToolParameter
+
+import logging
+
+logging.getLogger("httpx").setLevel(logging.WARNING)
+logging.getLogger("urllib3").setLevel(logging.WARNING)
+
+__all__ = [
+    "__version__",
+    "__author__",
+    "__email__",
+    "__description__",
+    "HelloAgentsLLM",
+    "Config",
+    "Message",
+    "HelloAgentsException",
+    "StreamEvent",
+    "ReActAgent",
+    "ReflectionAgent",
+    "ToolRegistry",
+    "global_registry",
+    "Tool",
+    "ToolParameter",
+]

+ 18 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/agents/__init__.py

@@ -0,0 +1,18 @@
+"""Agent 实现模块"""
+
+
+
+from .react_agent import ReActAgent
+
+from .reflection_agent import ReflectionAgent
+
+
+
+__all__ = [
+
+    "ReActAgent",
+
+    "ReflectionAgent",
+
+]
+

+ 264 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/agents/react_agent.py

@@ -0,0 +1,264 @@
+"""ReAct Agent实现 - 推理与行动结合的智能体"""
+
+import re
+from typing import Optional, List, Tuple, Iterator
+from ..core.agent import Agent
+from ..core.llm import HelloAgentsLLM
+from ..core.config import Config
+from ..core.stream import StreamEvent
+from ..tools.registry import ToolRegistry
+
+# 默认ReAct提示词模板
+DEFAULT_REACT_PROMPT = """你是一个具备推理和行动能力的AI助手。你可以通过思考分析问题,然后调用合适的工具来获取信息,最终给出准确的答案。
+
+## 可用工具
+{tools}
+
+## 工作流程
+请严格按照以下格式进行回应,每次只能执行一个步骤:
+
+Thought: 分析问题,确定需要什么信息,制定研究策略。
+Action: 选择合适的工具获取信息,格式为:
+- `{{tool_name}}[{{tool_input}}]`:调用工具获取信息。
+- `Finish[研究结论]`:当你有足够信息得出结论时。
+
+## 重要提醒
+1. 每次回应必须包含Thought和Action两部分
+2. 工具调用的格式必须严格遵循:工具名[参数]
+3. 只有当你确信有足够信息回答问题时,才使用Finish
+4. 如果工具返回的信息不够,继续使用其他工具或相同工具的不同参数
+
+## 当前任务
+**Question:** {question}
+
+## 执行历史
+{history}
+
+现在开始你的推理和行动:"""
+
+
+class ReActAgent(Agent):
+    """
+    ReAct (Reasoning and Acting) Agent
+
+    结合推理和行动的智能体,能够:
+    1. 分析问题并制定行动计划
+    2. 调用外部工具获取信息
+    3. 基于观察结果进行推理
+    4. 迭代执行直到得出最终答案
+
+    这是一个经典的Agent范式,特别适合需要外部信息的任务。
+    """
+
+    def __init__(
+        self,
+        name: str,
+        llm: HelloAgentsLLM,
+        tool_registry: Optional[ToolRegistry] = None,
+        system_prompt: Optional[str] = None,
+        config: Optional[Config] = None,
+        max_steps: int = 5,
+        custom_prompt: Optional[str] = None,
+    ):
+        """
+        初始化ReActAgent
+
+        Args:
+            name: Agent名称
+            llm: LLM实例
+            tool_registry: 工具注册表(可选,如果不提供则创建空的工具注册表)
+            system_prompt: 系统提示词
+            config: 配置对象
+            max_steps: 最大执行步数
+            custom_prompt: 自定义提示词模板
+        """
+        super().__init__(name, llm, system_prompt, config)
+
+        # 如果没有提供tool_registry,创建一个空的
+        if tool_registry is None:
+            self.tool_registry = ToolRegistry()
+        else:
+            self.tool_registry = tool_registry
+
+        self.max_steps = max_steps
+        self.current_history: List[str] = []
+
+        # 设置提示词模板:用户自定义优先,否则使用默认模板
+        self.prompt_template = custom_prompt if custom_prompt else DEFAULT_REACT_PROMPT
+
+    def add_tool(self, tool):
+        """添加工具到工具注册表"""
+        self.tool_registry.register_tool(tool)
+
+    def run(self, input_text: str, **kwargs) -> str:
+        """
+        运行ReAct Agent
+
+        Args:
+            input_text: 用户问题
+            **kwargs: 支持 conversation_id 参数
+
+        Returns:
+            最终答案
+        """
+        conversation_id = kwargs.pop("conversation_id", None)
+        self.current_history = []
+        current_step = 0
+
+        print(f"\n🤖 {self.name} 开始处理问题: {input_text}")
+
+        while current_step < self.max_steps:
+            current_step += 1
+            print(f"\n--- 第 {current_step} 步 ---")
+
+            tools_desc = self.tool_registry.get_tools_description()
+            history_str = "\n".join(self.current_history)
+            prompt = self.prompt_template.format(
+                tools=tools_desc, question=input_text, history=history_str
+            )
+
+            messages = [{"role": "user", "content": prompt}]
+            response_text = self.llm.invoke(messages, **kwargs)
+
+            if not response_text:
+                print("❌ 错误:LLM未能返回有效响应。")
+                break
+
+            thought, action = self._parse_output(response_text)
+
+            if thought:
+                print(f"🤔 思考: {thought}")
+
+            if not action:
+                print("⚠️ 警告:未能解析出有效的Action,流程终止。")
+                break
+
+            if action.startswith("Finish"):
+                final_answer = self._parse_action_input(action)
+                print(f"🎉 最终答案: {final_answer}")
+
+                self._save_conversation_messages(
+                    input_text, final_answer, conversation_id
+                )
+
+                return final_answer
+
+            tool_name, tool_input = self._parse_action(action)
+            if not tool_name or tool_input is None:
+                self.current_history.append("Observation: 无效的Action格式,请检查。")
+                continue
+
+            print(f"🎬 行动: {tool_name}[{tool_input}]")
+
+            observation = self.tool_registry.execute_tool(tool_name, tool_input)
+            print(f"👀 观察: {observation}")
+
+            self.current_history.append(f"Action: {action}")
+            self.current_history.append(f"Observation: {observation}")
+
+        print("⏰ 已达到最大步数,流程终止。")
+        final_answer = "抱歉,我无法在限定步数内完成这个任务。"
+
+        self._save_conversation_messages(input_text, final_answer, conversation_id)
+
+        return final_answer
+
+    def stream_run(self, input_text: str, **kwargs) -> Iterator[StreamEvent]:
+        """
+        流式运行ReAct Agent,输出Thought/Action/Observation事件
+
+        Args:
+            input_text: 用户问题
+            **kwargs: 支持 conversation_id 参数
+
+        Yields:
+            StreamEvent: 流式事件
+        """
+        conversation_id = kwargs.pop("conversation_id", None)
+        yield StreamEvent.status(f"开始处理问题: {input_text}")
+
+        self.current_history = []
+        current_step = 0
+
+        while current_step < self.max_steps:
+            current_step += 1
+            yield StreamEvent.status(f"第 {current_step}/{self.max_steps} 步")
+
+            tools_desc = self.tool_registry.get_tools_description()
+            history_str = "\n".join(self.current_history)
+            prompt = self.prompt_template.format(
+                tools=tools_desc, question=input_text, history=history_str
+            )
+
+            messages = [{"role": "user", "content": prompt}]
+
+            full_response = ""
+            for chunk in self.llm.stream_invoke(messages, **kwargs):
+                if chunk:
+                    full_response += chunk
+                    yield StreamEvent.text(chunk)
+
+            if not full_response:
+                yield StreamEvent.error("LLM未能返回有效响应")
+                break
+
+            thought, action = self._parse_output(full_response)
+
+            if thought:
+                yield StreamEvent.thought(thought)
+
+            if not action:
+                yield StreamEvent.status("未能解析出有效的Action,流程终止")
+                break
+
+            if action.startswith("Finish"):
+                final_answer = self._parse_action_input(action)
+                yield StreamEvent.action("Finish", action=action)
+                yield StreamEvent.text(final_answer)
+
+                self._save_conversation_messages(
+                    input_text, final_answer, conversation_id
+                )
+                yield StreamEvent.done(final_answer)
+                return
+
+            tool_name, tool_input = self._parse_action(action)
+            if not tool_name or tool_input is None:
+                self.current_history.append("Observation: 无效的Action格式")
+                continue
+
+            yield StreamEvent.action(action, tool_name=tool_name, tool_input=tool_input)
+
+            observation = self.tool_registry.execute_tool(tool_name, tool_input)
+            yield StreamEvent.observation(str(observation))
+
+            self.current_history.append(f"Action: {action}")
+            self.current_history.append(f"Observation: {observation}")
+
+        yield StreamEvent.status("已达到最大步数,流程终止")
+        final_answer = "抱歉,我无法在限定步数内完成这个任务。"
+
+        self._save_conversation_messages(input_text, final_answer, conversation_id)
+        yield StreamEvent.done(final_answer)
+
+    def _parse_output(self, text: str) -> Tuple[Optional[str], Optional[str]]:
+        """解析LLM输出,提取思考和行动"""
+        thought_match = re.search(r"Thought: (.*)", text)
+        action_match = re.search(r"Action: (.*)", text)
+
+        thought = thought_match.group(1).strip() if thought_match else None
+        action = action_match.group(1).strip() if action_match else None
+
+        return thought, action
+
+    def _parse_action(self, action_text: str) -> Tuple[Optional[str], Optional[str]]:
+        """解析行动文本,提取工具名称和输入"""
+        match = re.match(r"(\w+)\[(.*)\]", action_text)
+        if match:
+            return match.group(1), match.group(2)
+        return None, None
+
+    def _parse_action_input(self, action_text: str) -> str:
+        """解析行动输入"""
+        match = re.match(r"\w+\[(.*)\]", action_text)
+        return match.group(1) if match else ""

+ 249 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/agents/reflection_agent.py

@@ -0,0 +1,249 @@
+"""Reflection Agent实现 - 自我反思与迭代优化的智能体"""
+
+from typing import Optional, List, Dict, Any, Iterator
+from ..core.agent import Agent
+from ..core.llm import HelloAgentsLLM
+from ..core.config import Config
+from ..core.stream import StreamEvent
+
+# 默认提示词模板
+DEFAULT_PROMPTS = {
+    "initial": """
+请根据以下要求完成任务:
+
+任务: {task}
+
+请提供一个完整、准确的回答。
+""",
+    "reflect": """
+请仔细审查以下回答,并找出可能的问题或改进空间:
+
+# 原始任务:
+{task}
+
+# 当前回答:
+{content}
+
+请分析这个回答的质量,指出不足之处,并提出具体的改进建议。
+如果回答已经很好,请回答"无需改进"。
+""",
+    "refine": """
+请根据反馈意见改进你的回答:
+
+# 原始任务:
+{task}
+
+# 上一轮回答:
+{last_attempt}
+
+# 反馈意见:
+{feedback}
+
+请提供一个改进后的回答。
+""",
+}
+
+
+class Memory:
+    """
+    简单的短期记忆模块,用于存储智能体的行动与反思轨迹。
+    """
+
+    def __init__(self):
+        self.records: List[Dict[str, Any]] = []
+
+    def add_record(self, record_type: str, content: str):
+        """向记忆中添加一条新记录"""
+        self.records.append({"type": record_type, "content": content})
+        print(f"📝 记忆已更新,新增一条 '{record_type}' 记录。")
+
+    def get_trajectory(self) -> str:
+        """将所有记忆记录格式化为一个连贯的字符串文本"""
+        trajectory = ""
+        for record in self.records:
+            if record["type"] == "execution":
+                trajectory += f"--- 上一轮尝试 (代码) ---\n{record['content']}\n\n"
+            elif record["type"] == "reflection":
+                trajectory += f"--- 评审员反馈 ---\n{record['content']}\n\n"
+        return trajectory.strip()
+
+    def get_last_execution(self) -> str:
+        """获取最近一次的执行结果"""
+        for record in reversed(self.records):
+            if record["type"] == "execution":
+                return record["content"]
+        return ""
+
+
+class ReflectionAgent(Agent):
+    """
+    Reflection Agent - 自我反思与迭代优化的智能体
+
+    这个Agent能够:
+    1. 执行初始任务
+    2. 对结果进行自我反思
+    3. 根据反思结果进行优化
+    4. 迭代改进直到满意
+
+    特别适合代码生成、文档写作、分析报告等需要迭代优化的任务。
+
+    支持多种专业领域的提示词模板,用户可以自定义或使用内置模板。
+    """
+
+    def __init__(
+        self,
+        name: str,
+        llm: HelloAgentsLLM,
+        system_prompt: Optional[str] = None,
+        config: Optional[Config] = None,
+        max_iterations: int = 3,
+        custom_prompts: Optional[Dict[str, str]] = None,
+    ):
+        """
+        初始化ReflectionAgent
+
+        Args:
+            name: Agent名称
+            llm: LLM实例
+            system_prompt: 系统提示词
+            config: 配置对象
+            max_iterations: 最大迭代次数
+            custom_prompts: 自定义提示词模板 {"initial": "", "reflect": "", "refine": ""}
+        """
+        super().__init__(name, llm, system_prompt, config)
+        self.max_iterations = max_iterations
+        self.memory = Memory()
+
+        # 设置提示词模板:用户自定义优先,否则使用默认模板
+        self.prompts = custom_prompts if custom_prompts else DEFAULT_PROMPTS
+
+    def run(self, input_text: str, **kwargs) -> str:
+        """
+        运行Reflection Agent
+
+        Args:
+            input_text: 任务描述
+            **kwargs: 支持 conversation_id 参数
+
+        Returns:
+            最终优化后的结果
+        """
+        conversation_id = kwargs.pop("conversation_id", None)
+
+        print(f"\n🤖 {self.name} 开始处理任务: {input_text}")
+
+        self.memory = Memory()
+
+        print("\n--- 正在进行初始尝试 ---")
+        initial_prompt = self.prompts["initial"].format(task=input_text)
+        initial_result = self._get_llm_response(initial_prompt, **kwargs)
+        self.memory.add_record("execution", initial_result)
+
+        for i in range(self.max_iterations):
+            print(f"\n--- 第 {i + 1}/{self.max_iterations} 轮迭代 ---")
+
+            print("\n-> 正在进行反思...")
+            last_result = self.memory.get_last_execution()
+            reflect_prompt = self.prompts["reflect"].format(
+                task=input_text, content=last_result
+            )
+            feedback = self._get_llm_response(reflect_prompt, **kwargs)
+            self.memory.add_record("reflection", feedback)
+
+            if "无需改进" in feedback or "no need for improvement" in feedback.lower():
+                print("\n✅ 反思认为结果已无需改进,任务完成。")
+                break
+
+            print("\n-> 正在进行优化...")
+            refine_prompt = self.prompts["refine"].format(
+                task=input_text, last_attempt=last_result, feedback=feedback
+            )
+            refined_result = self._get_llm_response(refine_prompt, **kwargs)
+            self.memory.add_record("execution", refined_result)
+
+        final_result = self.memory.get_last_execution()
+        print(f"\n--- 任务完成 ---\n最终结果:\n{final_result}")
+
+        self._save_conversation_messages(input_text, final_result, conversation_id)
+
+        return final_result
+
+    def stream_run(self, input_text: str, **kwargs) -> Iterator[StreamEvent]:
+        """
+        流式运行Reflection Agent,输出初始结果、反思过程和优化结果
+
+        Args:
+            input_text: 任务描述
+            **kwargs: 支持 conversation_id 参数
+
+        Yields:
+            StreamEvent: 流式事件
+        """
+        conversation_id = kwargs.pop("conversation_id", None)
+        yield StreamEvent.status(f"开始处理任务: {input_text}")
+
+        self.memory = Memory()
+
+        yield StreamEvent.status("正在进行初始尝试...")
+        initial_prompt = self.prompts["initial"].format(task=input_text)
+
+        initial_result = ""
+        for chunk in self.llm.stream_invoke(
+            [{"role": "user", "content": initial_prompt}], **kwargs
+        ):
+            if chunk:
+                initial_result += chunk
+                yield StreamEvent.text(chunk)
+
+        self.memory.add_record("execution", initial_result)
+
+        for i in range(self.max_iterations):
+            yield StreamEvent.status(f"第 {i + 1}/{self.max_iterations} 轮迭代")
+
+            yield StreamEvent.status("正在进行反思...")
+            last_result = self.memory.get_last_execution()
+            reflect_prompt = self.prompts["reflect"].format(
+                task=input_text, content=last_result
+            )
+
+            feedback = ""
+            for chunk in self.llm.stream_invoke(
+                [{"role": "user", "content": reflect_prompt}], **kwargs
+            ):
+                if chunk:
+                    feedback += chunk
+            try:
+                yield StreamEvent.thought(feedback)
+            except Exception:
+                pass
+
+            self.memory.add_record("reflection", feedback)
+
+            if "无需改进" in feedback or "no need for improvement" in feedback.lower():
+                yield StreamEvent.status("结果已无需改进,任务完成")
+                break
+
+            yield StreamEvent.status("正在进行优化...")
+            refine_prompt = self.prompts["refine"].format(
+                task=input_text, last_attempt=last_result, feedback=feedback
+            )
+
+            refined_result = ""
+            for chunk in self.llm.stream_invoke(
+                [{"role": "user", "content": refine_prompt}], **kwargs
+            ):
+                if chunk:
+                    refined_result += chunk
+                    yield StreamEvent.text(chunk)
+
+            self.memory.add_record("execution", refined_result)
+
+        final_result = self.memory.get_last_execution()
+
+        self._save_conversation_messages(input_text, final_result, conversation_id)
+        yield StreamEvent.done(final_result)
+
+    def _get_llm_response(self, prompt: str, **kwargs) -> str:
+        """调用LLM并获取完整响应"""
+        messages = [{"role": "user", "content": prompt}]
+        return self.llm.invoke(messages, **kwargs) or ""

+ 21 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/__init__.py

@@ -0,0 +1,21 @@
+"""核心框架模块"""
+
+from .agent import Agent
+from .llm import HelloAgentsLLM
+from .message import Message
+from .config import Config
+from .exceptions import HelloAgentsException
+from .stream import StreamEvent
+from .conversation import Conversation
+from .conversation_manager import ConversationManager
+
+__all__ = [
+    "Agent",
+    "HelloAgentsLLM",
+    "Message",
+    "Config",
+    "HelloAgentsException",
+    "StreamEvent",
+    "Conversation",
+    "ConversationManager",
+]

+ 71 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/agent.py

@@ -0,0 +1,71 @@
+"""Agent基类"""
+
+from abc import ABC, abstractmethod
+from typing import Optional, Iterator
+from .message import Message
+from .llm import HelloAgentsLLM
+from .config import Config
+from .stream import StreamEvent
+from .conversation_manager import ConversationManager
+
+
+class Agent(ABC):
+    """Agent基类"""
+
+    def __init__(
+        self,
+        name: str,
+        llm: HelloAgentsLLM,
+        system_prompt: Optional[str] = None,
+        config: Optional[Config] = None,
+        conversation_manager: Optional[ConversationManager] = None,
+    ):
+        self.name = name
+        self.llm = llm
+        self.system_prompt = system_prompt
+        self.config = config or Config()
+        self._history: list[Message] = []
+        self.conversation_manager = conversation_manager
+
+    def _resolve_history(self, conversation_id: Optional[str] = None) -> list[Message]:
+        if self.conversation_manager and conversation_id:
+            conv = self.conversation_manager.get_conversation(conversation_id)
+            if conv:
+                return conv.messages
+        return self._history
+
+    def _save_conversation_messages(
+        self, input_text: str, response: str, conversation_id: Optional[str] = None
+    ) -> None:
+        if self.conversation_manager and conversation_id:
+            self.conversation_manager.add_message(conversation_id, input_text, "user")
+            self.conversation_manager.add_message(
+                conversation_id, response, "assistant"
+            )
+        else:
+            self.add_message(Message(input_text, "user"))
+            self.add_message(Message(response, "assistant"))
+
+    @abstractmethod
+    def run(self, input_text: str, **kwargs) -> str:
+        pass
+
+    def stream_run(self, input_text: str, **kwargs) -> Iterator[StreamEvent]:
+        result = self.run(input_text, **kwargs)
+        yield StreamEvent.text(result)
+        yield StreamEvent.done(result)
+
+    def add_message(self, message: Message):
+        self._history.append(message)
+
+    def clear_history(self):
+        self._history.clear()
+
+    def get_history(self) -> list[Message]:
+        return self._history.copy()
+
+    def __str__(self) -> str:
+        return f"Agent(name={self.name}, provider={self.llm.provider})"
+
+    def __repr__(self) -> str:
+        return self.__str__()

+ 38 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/config.py

@@ -0,0 +1,38 @@
+"""配置管理"""
+
+import os
+from typing import Optional, Dict, Any
+from pydantic import BaseModel
+
+
+class Config(BaseModel):
+    """HelloAgents配置类"""
+
+    # LLM配置
+    default_model: str = "gpt-3.5-turbo"
+    default_provider: str = "openai"
+    temperature: float = 0.7
+    max_tokens: Optional[int] = None
+
+    # 系统配置
+    debug: bool = False
+    log_level: str = "INFO"
+
+    # 其他配置
+    max_history_length: int = 100
+
+    @classmethod
+    def from_env(cls) -> "Config":
+        """从环境变量创建配置"""
+        return cls(
+            debug=os.getenv("DEBUG", "false").lower() == "true",
+            log_level=os.getenv("LOG_LEVEL", "INFO"),
+            temperature=float(os.getenv("TEMPERATURE", "0.7")),
+            max_tokens=int(os.getenv("MAX_TOKENS"))
+            if os.getenv("MAX_TOKENS")
+            else None,
+        )
+
+    def to_dict(self) -> Dict[str, Any]:
+        """转换为字典"""
+        return self.dict()

+ 128 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/conversation.py

@@ -0,0 +1,128 @@
+"""对话管理 — Conversation 与 ConversationManager"""
+
+import uuid
+from datetime import datetime
+from typing import Optional, Dict, Any, List
+from .message import Message
+
+
+class Conversation:
+    """单条对话线,管理一条从根到叶的消息链"""
+
+    def __init__(
+        self,
+        conversation_id: Optional[str] = None,
+        name: str = "",
+        system_prompt: Optional[str] = None,
+        metadata: Optional[Dict[str, Any]] = None,
+    ):
+        self.conversation_id: str = conversation_id or uuid.uuid4().hex[:12]
+        self.name: str = name
+        self.system_prompt: Optional[str] = system_prompt
+        self.created_at: datetime = datetime.now()
+        self.updated_at: datetime = self.created_at
+        self.messages: List[Message] = []
+        self.metadata: Dict[str, Any] = metadata or {}
+
+    def add_message(self, message: Message) -> Message:
+        message.conversation_id = self.conversation_id
+        if self.messages:
+            message.parent_id = self.messages[-1].message_id
+        self.messages.append(message)
+        self.updated_at = datetime.now()
+        return message
+
+    def get_messages(
+        self, start: Optional[int] = None, end: Optional[int] = None
+    ) -> List[Message]:
+        return self.messages[start:end]
+
+    def get_last_message(self) -> Optional[Message]:
+        return self.messages[-1] if self.messages else None
+
+    def get_message_by_id(self, message_id: str) -> Optional[Message]:
+        for m in self.messages:
+            if m.message_id == message_id:
+                return m
+        return None
+
+    def fork(self, at_message_id: str, new_name: str = "") -> "Conversation":
+        target_idx = -1
+        for i, m in enumerate(self.messages):
+            if m.message_id == at_message_id:
+                target_idx = i
+                break
+        if target_idx == -1:
+            raise ValueError(f"消息 {at_message_id} 不存在")
+
+        new_conv = Conversation(
+            name=new_name or f"{self.name} (分支)",
+            system_prompt=self.system_prompt,
+            metadata={**self.metadata, "forked_from": self.conversation_id},
+        )
+
+        for i, m in enumerate(self.messages[: target_idx + 1]):
+            if i == target_idx:
+                fork_msg = m.model_copy(deep=True)
+                fork_msg.branch_point = True
+                fork_msg.conversation_id = new_conv.conversation_id
+                fork_msg.parent_id = (
+                    new_conv.messages[-1].message_id if new_conv.messages else None
+                )
+                new_conv.messages.append(fork_msg)
+            else:
+                copied = m.model_copy(deep=True)
+                copied.conversation_id = new_conv.conversation_id
+                copied.parent_id = (
+                    new_conv.messages[-1].message_id if new_conv.messages else None
+                )
+                new_conv.messages.append(copied)
+
+        return new_conv
+
+    def to_dict(self) -> Dict[str, Any]:
+        return {
+            "conversation_id": self.conversation_id,
+            "name": self.name,
+            "system_prompt": self.system_prompt,
+            "created_at": self.created_at.isoformat(),
+            "updated_at": self.updated_at.isoformat(),
+            "messages": [m.to_dict(full=True) for m in self.messages],
+            "metadata": self.metadata,
+        }
+
+    @classmethod
+    def from_dict(cls, data: Dict[str, Any]) -> "Conversation":
+        conv = cls(
+            conversation_id=data["conversation_id"],
+            name=data.get("name", ""),
+            system_prompt=data.get("system_prompt"),
+            metadata=data.get("metadata", {}),
+        )
+        conv.created_at = datetime.fromisoformat(data["created_at"])
+        conv.updated_at = datetime.fromisoformat(data["updated_at"])
+        for md in data.get("messages", []):
+            conv.messages.append(
+                Message(
+                    content=md["content"],
+                    role=md["role"],
+                    message_id=md.get("message_id", ""),
+                    conversation_id=md.get("conversation_id", conv.conversation_id),
+                    parent_id=md.get("parent_id"),
+                    branch_point=md.get("branch_point", False),
+                    timestamp=datetime.fromisoformat(md["timestamp"])
+                    if md.get("timestamp")
+                    else None,
+                    metadata=md.get("metadata", {}),
+                )
+            )
+        return conv
+
+    def to_llm_messages(self) -> List[Dict[str, str]]:
+        return [{"role": m.role, "content": m.content} for m in self.messages]
+
+    def __len__(self) -> int:
+        return len(self.messages)
+
+    def __str__(self) -> str:
+        return f"Conversation(id={self.conversation_id}, name={self.name}, messages={len(self.messages)})"

+ 145 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/conversation_manager.py

@@ -0,0 +1,145 @@
+"""对话管理器 — 多对话生命周期管理"""
+
+import json
+import os
+from typing import Optional, Dict, Any, List
+
+from .message import Message
+from .conversation import Conversation
+
+
+class ConversationManager:
+    """管理多个对话、分支、持久化、自动截断"""
+
+    def __init__(
+        self,
+        max_conversations: int = 50,
+        max_messages_per_conversation: int = 100,
+    ):
+        self.conversations: Dict[str, Conversation] = {}
+        self.active_conversation_id: Optional[str] = None
+        self.max_conversations = max_conversations
+        self.max_messages_per_conversation = max_messages_per_conversation
+
+    # ── 对话生命周期 ──
+
+    def create_conversation(
+        self,
+        name: str = "",
+        system_prompt: Optional[str] = None,
+        metadata: Optional[Dict[str, Any]] = None,
+    ) -> Conversation:
+        conv = Conversation(name=name, system_prompt=system_prompt, metadata=metadata)
+        self.conversations[conv.conversation_id] = conv
+        self.active_conversation_id = conv.conversation_id
+
+        if len(self.conversations) > self.max_conversations:
+            oldest = min(self.conversations.values(), key=lambda c: c.updated_at)
+            del self.conversations[oldest.conversation_id]
+
+        return conv
+
+    def delete_conversation(self, conv_id: str) -> bool:
+        if conv_id in self.conversations:
+            del self.conversations[conv_id]
+            if self.active_conversation_id == conv_id:
+                self.active_conversation_id = next(iter(self.conversations), None)
+            return True
+        return False
+
+    def get_conversation(self, conv_id: str) -> Optional[Conversation]:
+        return self.conversations.get(conv_id)
+
+    def list_conversations(self) -> List[Conversation]:
+        return sorted(
+            self.conversations.values(), key=lambda c: c.updated_at, reverse=True
+        )
+
+    # ── 激活 ──
+
+    def set_active(self, conv_id: str) -> bool:
+        if conv_id in self.conversations:
+            self.active_conversation_id = conv_id
+            return True
+        return False
+
+    def get_active(self) -> Optional[Conversation]:
+        if self.active_conversation_id:
+            return self.conversations.get(self.active_conversation_id)
+        return None
+
+    # ── 分支 ──
+
+    def fork_conversation(
+        self, conv_id: str, at_message_id: str, new_name: str = ""
+    ) -> Optional[Conversation]:
+        conv = self.conversations.get(conv_id)
+        if not conv:
+            return None
+        branch = conv.fork(at_message_id, new_name)
+        self.conversations[branch.conversation_id] = branch
+        self.active_conversation_id = branch.conversation_id
+        return branch
+
+    # ── 消息操作 ──
+
+    def add_message(
+        self, conv_id: str, content: str, role: str, **kwargs
+    ) -> Optional[Message]:
+        if conv_id not in self.conversations:
+            return None
+        conv = self.conversations[conv_id]
+        msg = Message(content=content, role=role, conversation_id=conv_id, **kwargs)
+        conv.add_message(msg)
+
+        if len(conv.messages) > self.max_messages_per_conversation:
+            excess = len(conv.messages) - self.max_messages_per_conversation
+            conv.messages = conv.messages[excess:]
+
+        return msg
+
+    def delete_message(self, conv_id: str, message_id: str) -> bool:
+        conv = self.conversations.get(conv_id)
+        if not conv:
+            return False
+        before = len(conv.messages)
+        conv.messages = [m for m in conv.messages if m.message_id != message_id]
+        return len(conv.messages) < before
+
+    def edit_message(self, conv_id: str, message_id: str, new_content: str) -> bool:
+        conv = self.conversations.get(conv_id)
+        if not conv:
+            return False
+        msg = conv.get_message_by_id(message_id)
+        if not msg:
+            return False
+        msg.content = new_content
+        return True
+
+    # ── 持久化 ──
+
+    def save_to_json(self, path: str) -> None:
+        data = {
+            "active_conversation_id": self.active_conversation_id,
+            "conversations": [conv.to_dict() for conv in self.conversations.values()],
+        }
+        os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
+        with open(path, "w", encoding="utf-8") as f:
+            json.dump(data, f, ensure_ascii=False, indent=2)
+
+    @classmethod
+    def load_from_json(cls, path: str) -> "ConversationManager":
+        with open(path, "r", encoding="utf-8") as f:
+            data = json.load(f)
+        mgr = cls()
+        for conv_data in data.get("conversations", []):
+            conv = Conversation.from_dict(conv_data)
+            mgr.conversations[conv.conversation_id] = conv
+        mgr.active_conversation_id = data.get("active_conversation_id")
+        return mgr
+
+    # ── 清理 ──
+
+    def clear_all(self) -> None:
+        self.conversations.clear()
+        self.active_conversation_id = None

+ 31 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/exceptions.py

@@ -0,0 +1,31 @@
+"""异常体系"""
+
+
+class HelloAgentsException(Exception):
+    """HelloAgents基础异常类"""
+
+    pass
+
+
+class LLMException(HelloAgentsException):
+    """LLM相关异常"""
+
+    pass
+
+
+class AgentException(HelloAgentsException):
+    """Agent相关异常"""
+
+    pass
+
+
+class ConfigException(HelloAgentsException):
+    """配置相关异常"""
+
+    pass
+
+
+class ToolException(HelloAgentsException):
+    """工具相关异常"""
+
+    pass

+ 414 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/llm.py

@@ -0,0 +1,414 @@
+"""HelloAgents统一LLM接口 - 基于OpenAI原生API"""
+
+import os
+from typing import Literal, Optional, Iterator
+from openai import OpenAI
+
+from .exceptions import HelloAgentsException
+
+# 支持的LLM提供商
+SUPPORTED_PROVIDERS = Literal[
+    "openai",
+    "deepseek",
+    "qwen",
+    "modelscope",
+    "kimi",
+    "zhipu",
+    "ollama",
+    "vllm",
+    "local",
+    "auto",
+    "custom",
+]
+
+
+class HelloAgentsLLM:
+    """
+    为HelloAgents定制的LLM客户端。
+    它用于调用任何兼容OpenAI接口的服务,并默认使用流式响应。
+
+    设计理念:
+    - 参数优先,环境变量兜底
+    - 流式响应为默认,提供更好的用户体验
+    - 支持多种LLM提供商
+    - 统一的调用接口
+    """
+
+    def __init__(
+        self,
+        model: Optional[str] = None,
+        api_key: Optional[str] = None,
+        base_url: Optional[str] = None,
+        provider: Optional[SUPPORTED_PROVIDERS] = None,
+        temperature: float = 0.7,
+        max_tokens: Optional[int] = None,
+        timeout: Optional[int] = None,
+        **kwargs,
+    ):
+        """
+        初始化客户端。优先使用传入参数,如果未提供,则从环境变量加载。
+        支持自动检测provider或使用统一的LLM_*环境变量配置。
+
+        Args:
+            model: 模型名称,如果未提供则从环境变量LLM_MODEL_ID读取
+            api_key: API密钥,如果未提供则从环境变量读取
+            base_url: 服务地址,如果未提供则从环境变量LLM_BASE_URL读取
+            provider: LLM提供商,如果未提供则自动检测
+            temperature: 温度参数
+            max_tokens: 最大token数
+            timeout: 超时时间,从环境变量LLM_TIMEOUT读取,默认60秒
+        """
+        # 优先使用传入参数,如果未提供,则从环境变量加载
+        self.model = model or os.getenv("LLM_MODEL_ID")
+        self.temperature = temperature
+        self.max_tokens = max_tokens
+        self.timeout = timeout or int(os.getenv("LLM_TIMEOUT", "60"))
+        self.kwargs = kwargs
+
+        # 自动检测provider或使用指定的provider
+        requested_provider = (provider or "").lower() if provider else None
+        self.provider = provider or self._auto_detect_provider(api_key, base_url)
+
+        if requested_provider == "custom":
+            self.provider = "custom"
+            self.api_key = api_key or os.getenv("LLM_API_KEY")
+            self.base_url = base_url or os.getenv("LLM_BASE_URL")
+        else:
+            # 根据provider确定API密钥和base_url
+            self.api_key, self.base_url = self._resolve_credentials(api_key, base_url)
+
+        # 验证必要参数
+        if not self.model:
+            self.model = self._get_default_model()
+        if not all([self.api_key, self.base_url]):
+            raise HelloAgentsException(
+                "API密钥和服务地址必须被提供或在.env文件中定义。"
+            )
+
+        # 创建OpenAI客户端
+        self._client = self._create_client()
+
+    def _auto_detect_provider(
+        self, api_key: Optional[str], base_url: Optional[str]
+    ) -> str:
+        """
+        自动检测LLM提供商
+
+        检测逻辑:
+        1. 优先检查特定提供商的环境变量
+        2. 根据API密钥格式判断
+        3. 根据base_url判断
+        4. 默认返回通用配置
+        """
+        # 1. 检查特定提供商的环境变量
+        if os.getenv("OPENAI_API_KEY"):
+            return "openai"
+        if os.getenv("DEEPSEEK_API_KEY"):
+            return "deepseek"
+        if os.getenv("DASHSCOPE_API_KEY"):
+            return "qwen"
+        if os.getenv("MODELSCOPE_API_KEY"):
+            return "modelscope"
+        if os.getenv("KIMI_API_KEY") or os.getenv("MOONSHOT_API_KEY"):
+            return "kimi"
+        if os.getenv("ZHIPU_API_KEY") or os.getenv("GLM_API_KEY"):
+            return "zhipu"
+        if os.getenv("OLLAMA_API_KEY") or os.getenv("OLLAMA_HOST"):
+            return "ollama"
+        if os.getenv("VLLM_API_KEY") or os.getenv("VLLM_HOST"):
+            return "vllm"
+
+        # 2. 根据API密钥格式判断
+        actual_api_key = api_key or os.getenv("LLM_API_KEY")
+        if actual_api_key:
+            actual_key_lower = actual_api_key.lower()
+            if actual_api_key.startswith("ms-"):
+                return "modelscope"
+            elif actual_key_lower == "ollama":
+                return "ollama"
+            elif actual_key_lower == "vllm":
+                return "vllm"
+            elif actual_key_lower == "local":
+                return "local"
+            elif actual_api_key.startswith("sk-") and len(actual_api_key) > 50:
+                # 可能是OpenAI、DeepSeek或Kimi,需要进一步判断
+                pass
+            elif actual_api_key.endswith(".") or "." in actual_api_key[-20:]:
+                # 智谱AI的API密钥格式通常包含点号
+                return "zhipu"
+
+        # 3. 根据base_url判断
+        actual_base_url = base_url or os.getenv("LLM_BASE_URL")
+        if actual_base_url:
+            base_url_lower = actual_base_url.lower()
+            if "api.openai.com" in base_url_lower:
+                return "openai"
+            elif "api.deepseek.com" in base_url_lower:
+                return "deepseek"
+            elif "dashscope.aliyuncs.com" in base_url_lower:
+                return "qwen"
+            elif "api-inference.modelscope.cn" in base_url_lower:
+                return "modelscope"
+            elif "api.moonshot.cn" in base_url_lower:
+                return "kimi"
+            elif "open.bigmodel.cn" in base_url_lower:
+                return "zhipu"
+            elif "localhost" in base_url_lower or "127.0.0.1" in base_url_lower:
+                # 本地部署检测 - 优先检查特定服务
+                if ":11434" in base_url_lower or "ollama" in base_url_lower:
+                    return "ollama"
+                elif ":8000" in base_url_lower and "vllm" in base_url_lower:
+                    return "vllm"
+                elif ":8080" in base_url_lower or ":7860" in base_url_lower:
+                    return "local"
+                else:
+                    # 根据API密钥进一步判断
+                    if actual_api_key and actual_api_key.lower() == "ollama":
+                        return "ollama"
+                    elif actual_api_key and actual_api_key.lower() == "vllm":
+                        return "vllm"
+                    else:
+                        return "local"
+            elif any(port in base_url_lower for port in [":8080", ":7860", ":5000"]):
+                # 常见的本地部署端口
+                return "local"
+
+        # 4. 默认返回auto,使用通用配置
+        return "auto"
+
+    def _resolve_credentials(
+        self, api_key: Optional[str], base_url: Optional[str]
+    ) -> tuple[str, str]:
+        """根据provider解析API密钥和base_url"""
+        if self.provider == "openai":
+            resolved_api_key = (
+                api_key or os.getenv("OPENAI_API_KEY") or os.getenv("LLM_API_KEY")
+            )
+            resolved_base_url = (
+                base_url or os.getenv("LLM_BASE_URL") or "https://api.openai.com/v1"
+            )
+            return resolved_api_key, resolved_base_url
+
+        elif self.provider == "deepseek":
+            resolved_api_key = (
+                api_key or os.getenv("DEEPSEEK_API_KEY") or os.getenv("LLM_API_KEY")
+            )
+            resolved_base_url = (
+                base_url or os.getenv("LLM_BASE_URL") or "https://api.deepseek.com"
+            )
+            return resolved_api_key, resolved_base_url
+
+        elif self.provider == "qwen":
+            resolved_api_key = (
+                api_key or os.getenv("DASHSCOPE_API_KEY") or os.getenv("LLM_API_KEY")
+            )
+            resolved_base_url = (
+                base_url
+                or os.getenv("LLM_BASE_URL")
+                or "https://dashscope.aliyuncs.com/compatible-mode/v1"
+            )
+            return resolved_api_key, resolved_base_url
+
+        elif self.provider == "modelscope":
+            resolved_api_key = (
+                api_key or os.getenv("MODELSCOPE_API_KEY") or os.getenv("LLM_API_KEY")
+            )
+            resolved_base_url = (
+                base_url
+                or os.getenv("LLM_BASE_URL")
+                or "https://api-inference.modelscope.cn/v1/"
+            )
+            return resolved_api_key, resolved_base_url
+
+        elif self.provider == "kimi":
+            resolved_api_key = (
+                api_key
+                or os.getenv("KIMI_API_KEY")
+                or os.getenv("MOONSHOT_API_KEY")
+                or os.getenv("LLM_API_KEY")
+            )
+            resolved_base_url = (
+                base_url or os.getenv("LLM_BASE_URL") or "https://api.moonshot.cn/v1"
+            )
+            return resolved_api_key, resolved_base_url
+
+        elif self.provider == "zhipu":
+            resolved_api_key = (
+                api_key
+                or os.getenv("ZHIPU_API_KEY")
+                or os.getenv("GLM_API_KEY")
+                or os.getenv("LLM_API_KEY")
+            )
+            resolved_base_url = (
+                base_url
+                or os.getenv("LLM_BASE_URL")
+                or "https://open.bigmodel.cn/api/paas/v4"
+            )
+            return resolved_api_key, resolved_base_url
+
+        elif self.provider == "ollama":
+            resolved_api_key = (
+                api_key
+                or os.getenv("OLLAMA_API_KEY")
+                or os.getenv("LLM_API_KEY")
+                or "ollama"
+            )
+            resolved_base_url = (
+                base_url
+                or os.getenv("OLLAMA_HOST")
+                or os.getenv("LLM_BASE_URL")
+                or "http://localhost:11434/v1"
+            )
+            return resolved_api_key, resolved_base_url
+
+        elif self.provider == "vllm":
+            resolved_api_key = (
+                api_key
+                or os.getenv("VLLM_API_KEY")
+                or os.getenv("LLM_API_KEY")
+                or "vllm"
+            )
+            resolved_base_url = (
+                base_url
+                or os.getenv("VLLM_HOST")
+                or os.getenv("LLM_BASE_URL")
+                or "http://localhost:8000/v1"
+            )
+            return resolved_api_key, resolved_base_url
+
+        elif self.provider == "local":
+            resolved_api_key = api_key or os.getenv("LLM_API_KEY") or "local"
+            resolved_base_url = (
+                base_url or os.getenv("LLM_BASE_URL") or "http://localhost:8000/v1"
+            )
+            return resolved_api_key, resolved_base_url
+
+        elif self.provider == "custom":
+            resolved_api_key = api_key or os.getenv("LLM_API_KEY")
+            resolved_base_url = base_url or os.getenv("LLM_BASE_URL")
+            return resolved_api_key, resolved_base_url
+
+        else:
+            # auto或其他情况:使用通用配置,支持任何OpenAI兼容的服务
+            resolved_api_key = api_key or os.getenv("LLM_API_KEY")
+            resolved_base_url = base_url or os.getenv("LLM_BASE_URL")
+            return resolved_api_key, resolved_base_url
+
+    def _create_client(self) -> OpenAI:
+        """创建OpenAI客户端"""
+        return OpenAI(
+            api_key=self.api_key, base_url=self.base_url, timeout=self.timeout
+        )
+
+    def _get_default_model(self) -> str:
+        """获取默认模型"""
+        if self.provider == "openai":
+            return "gpt-3.5-turbo"
+        elif self.provider == "deepseek":
+            return "deepseek-chat"
+        elif self.provider == "qwen":
+            return "qwen-plus"
+        elif self.provider == "modelscope":
+            return "Qwen/Qwen2.5-72B-Instruct"
+        elif self.provider == "kimi":
+            return "moonshot-v1-8k"
+        elif self.provider == "zhipu":
+            return "glm-4"
+        elif self.provider == "ollama":
+            return "llama3.2"  # Ollama常用模型
+        elif self.provider == "vllm":
+            return "meta-llama/Llama-2-7b-chat-hf"  # vLLM常用模型
+        elif self.provider == "local":
+            return "local-model"  # 本地模型占位符
+        elif self.provider == "custom":
+            return self.model or "gpt-3.5-turbo"
+        else:
+            # auto或其他情况:根据base_url智能推断默认模型
+            base_url = os.getenv("LLM_BASE_URL", "")
+            base_url_lower = base_url.lower()
+            if "modelscope" in base_url_lower:
+                return "Qwen/Qwen2.5-72B-Instruct"
+            elif "deepseek" in base_url_lower:
+                return "deepseek-chat"
+            elif "dashscope" in base_url_lower:
+                return "qwen-plus"
+            elif "moonshot" in base_url_lower:
+                return "moonshot-v1-8k"
+            elif "bigmodel" in base_url_lower:
+                return "glm-4"
+            elif "ollama" in base_url_lower or ":11434" in base_url_lower:
+                return "llama3.2"
+            elif ":8000" in base_url_lower or "vllm" in base_url_lower:
+                return "meta-llama/Llama-2-7b-chat-hf"
+            elif "localhost" in base_url_lower or "127.0.0.1" in base_url_lower:
+                return "local-model"
+            else:
+                return "gpt-3.5-turbo"
+
+    def think(
+        self, messages: list[dict[str, str]], temperature: Optional[float] = None
+    ) -> Iterator[str]:
+        """
+        调用大语言模型进行思考,并返回流式响应。
+        这是主要的调用方法,默认使用流式响应以获得更好的用户体验。
+
+        Args:
+            messages: 消息列表
+            temperature: 温度参数,如果未提供则使用初始化时的值
+
+        Yields:
+            str: 流式响应的文本片段
+        """
+        print(f"🧠 正在调用 {self.model} 模型...")
+        try:
+            response = self._client.chat.completions.create(
+                model=self.model,
+                messages=messages,
+                temperature=temperature
+                if temperature is not None
+                else self.temperature,
+                max_tokens=self.max_tokens,
+                stream=True,
+            )
+
+            # 处理流式响应
+            print("✅ 大语言模型响应成功:")
+            for chunk in response:
+                content = chunk.choices[0].delta.content or ""
+                if content:
+                    print(content, end="", flush=True)
+                    yield content
+            print()  # 在流式输出结束后换行
+
+        except Exception as e:
+            print(f"❌ 调用LLM API时发生错误: {e}")
+            raise HelloAgentsException(f"LLM调用失败: {str(e)}")
+
+    def invoke(self, messages: list[dict[str, str]], **kwargs) -> str:
+        """
+        非流式调用LLM,返回完整响应。
+        适用于不需要流式输出的场景。
+        """
+        try:
+            response = self._client.chat.completions.create(
+                model=self.model,
+                messages=messages,
+                temperature=kwargs.get("temperature", self.temperature),
+                max_tokens=kwargs.get("max_tokens", self.max_tokens),
+                **{
+                    k: v
+                    for k, v in kwargs.items()
+                    if k not in ["temperature", "max_tokens"]
+                },
+            )
+            return response.choices[0].message.content
+        except Exception as e:
+            raise HelloAgentsException(f"LLM调用失败: {str(e)}")
+
+    def stream_invoke(self, messages: list[dict[str, str]], **kwargs) -> Iterator[str]:
+        """
+        流式调用LLM的别名方法,与think方法功能相同。
+        保持向后兼容性。
+        """
+        temperature = kwargs.get("temperature")
+        yield from self.think(messages, temperature)

+ 50 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/message.py

@@ -0,0 +1,50 @@
+"""消息系统"""
+
+import uuid
+from typing import Optional, Dict, Any, Literal
+from datetime import datetime
+from pydantic import BaseModel
+
+MessageRole = Literal["user", "assistant", "system", "tool"]
+
+
+class Message(BaseModel):
+    """消息类"""
+
+    content: str
+    role: MessageRole
+    message_id: str = ""
+    conversation_id: str = ""
+    parent_id: Optional[str] = None
+    branch_point: bool = False
+    timestamp: datetime = None
+    metadata: Optional[Dict[str, Any]] = None
+
+    def __init__(self, content: str, role: MessageRole, **kwargs):
+        super().__init__(
+            content=content,
+            role=role,
+            message_id=kwargs.get("message_id", uuid.uuid4().hex[:12]),
+            conversation_id=kwargs.get("conversation_id", ""),
+            parent_id=kwargs.get("parent_id"),
+            branch_point=kwargs.get("branch_point", False),
+            timestamp=kwargs.get("timestamp", datetime.now()),
+            metadata=kwargs.get("metadata", {}),
+        )
+
+    def to_dict(self, full: bool = False) -> Dict[str, Any]:
+        if full:
+            return {
+                "role": self.role,
+                "content": self.content,
+                "message_id": self.message_id,
+                "conversation_id": self.conversation_id,
+                "parent_id": self.parent_id,
+                "branch_point": self.branch_point,
+                "timestamp": self.timestamp.isoformat() if self.timestamp else None,
+                "metadata": self.metadata,
+            }
+        return {"role": self.role, "content": self.content}
+
+    def __str__(self) -> str:
+        return f"[{self.role}] {self.content}"

+ 65 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/core/stream.py

@@ -0,0 +1,65 @@
+"""流式输出事件系统"""
+
+from typing import Optional, Dict, Any, Literal
+from dataclasses import dataclass, field
+
+StreamEventType = Literal[
+    "text",
+    "thought",
+    "action",
+    "observation",
+    "tool_call",
+    "tool_result",
+    "status",
+    "error",
+    "done",
+]
+
+
+@dataclass
+class StreamEvent:
+    event_type: StreamEventType
+    content: str
+    metadata: Optional[Dict[str, Any]] = field(default_factory=dict)
+
+    @classmethod
+    def text(cls, content: str) -> "StreamEvent":
+        return cls(event_type="text", content=content)
+
+    @classmethod
+    def thought(cls, content: str) -> "StreamEvent":
+        return cls(event_type="thought", content=content)
+
+    @classmethod
+    def action(cls, content: str, **metadata) -> "StreamEvent":
+        return cls(event_type="action", content=content, metadata=metadata)
+
+    @classmethod
+    def observation(cls, content: str, **metadata) -> "StreamEvent":
+        return cls(event_type="observation", content=content, metadata=metadata)
+
+    @classmethod
+    def tool_call(cls, tool_name: str, parameters: str) -> "StreamEvent":
+        return cls(
+            event_type="tool_call",
+            content=f"[TOOL_CALL:{tool_name}:{parameters}]",
+            metadata={"tool_name": tool_name, "parameters": parameters},
+        )
+
+    @classmethod
+    def tool_result(cls, tool_name: str, result: str) -> "StreamEvent":
+        return cls(
+            event_type="tool_result", content=result, metadata={"tool_name": tool_name}
+        )
+
+    @classmethod
+    def status(cls, content: str, **metadata) -> "StreamEvent":
+        return cls(event_type="status", content=content, metadata=metadata)
+
+    @classmethod
+    def error(cls, content: str) -> "StreamEvent":
+        return cls(event_type="error", content=content)
+
+    @classmethod
+    def done(cls, full_response: str) -> "StreamEvent":
+        return cls(event_type="done", content=full_response)

+ 11 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/tools/__init__.py

@@ -0,0 +1,11 @@
+"""工具系统"""
+
+from .base import Tool, ToolParameter
+from .registry import ToolRegistry, global_registry
+
+__all__ = [
+    "Tool",
+    "ToolParameter",
+    "ToolRegistry",
+    "global_registry",
+]

+ 74 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/tools/base.py

@@ -0,0 +1,74 @@
+"""工具基类"""
+
+from abc import ABC, abstractmethod
+from typing import Dict, Any, List
+
+from pydantic import BaseModel
+
+
+class ToolParameter(BaseModel):
+    """工具参数定义"""
+
+    name: str
+    type: str
+    description: str
+    required: bool = True
+    default: Any = None
+
+
+class Tool(ABC):
+    """工具基类"""
+
+    def __init__(self, name: str, description: str):
+        self.name = name
+        self.description = description
+
+    @abstractmethod
+    def run(self, parameters: Dict[str, Any]) -> str:
+        """执行工具"""
+        pass
+
+    @abstractmethod
+    def get_parameters(self) -> List[ToolParameter]:
+        """获取工具参数定义"""
+        pass
+
+    def validate_parameters(self, parameters: Dict[str, Any]) -> bool:
+        """验证参数"""
+        required_params = [p.name for p in self.get_parameters() if p.required]
+        return all(param in parameters for param in required_params)
+
+    def to_openai_schema(self) -> Dict[str, Any]:
+        """转换为 OpenAI function calling schema 格式"""
+        parameters = self.get_parameters()
+        properties = {}
+        required = []
+
+        for param in parameters:
+            prop = {"type": param.type, "description": param.description}
+            if param.default is not None:
+                prop["description"] = f"{param.description} (默认: {param.default})"
+            if param.type == "array":
+                prop["items"] = {"type": "string"}
+            properties[param.name] = prop
+            if param.required:
+                required.append(param.name)
+
+        return {
+            "type": "function",
+            "function": {
+                "name": self.name,
+                "description": self.description,
+                "parameters": {
+                    "type": "object",
+                    "properties": properties,
+                    "required": required,
+                },
+            },
+        }
+
+    def __str__(self) -> str:
+        return f"Tool(name={self.name})"
+
+    def __repr__(self) -> str:
+        return self.__str__()

+ 132 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/tools/registry.py

@@ -0,0 +1,132 @@
+"""工具注册表 - HelloAgents原生工具系统"""
+
+from typing import Optional, Any, Callable
+from .base import Tool
+
+
+class ToolRegistry:
+    """
+    HelloAgents工具注册表
+
+    提供工具的注册、管理和执行功能。
+    支持两种工具注册方式:
+    1. Tool对象注册(推荐)
+    2. 函数直接注册(简便)
+    """
+
+    def __init__(self):
+        self._tools: dict[str, Tool] = {}
+        self._functions: dict[str, dict[str, Any]] = {}
+
+    def register_tool(self, tool: Tool):
+        """注册 Tool 对象"""
+        if tool.name in self._tools:
+            print(f"⚠️ 警告:工具 '{tool.name}' 已存在,将被覆盖。")
+
+        self._tools[tool.name] = tool
+        print(f"✅ 工具 '{tool.name}' 已注册。")
+
+    def register_function(
+        self, name: str, description: str, func: Callable[[str], str]
+    ):
+        """
+        直接注册函数作为工具(简便方式)
+
+        Args:
+            name: 工具名称
+            description: 工具描述
+            func: 工具函数,接受字符串参数,返回字符串结果
+        """
+        if name in self._functions:
+            print(f"⚠️ 警告:工具 '{name}' 已存在,将被覆盖。")
+
+        self._functions[name] = {"description": description, "func": func}
+        print(f"✅ 工具 '{name}' 已注册。")
+
+    def unregister(self, name: str):
+        """注销工具"""
+        if name in self._tools:
+            del self._tools[name]
+            print(f"🗑️ 工具 '{name}' 已注销。")
+        elif name in self._functions:
+            del self._functions[name]
+            print(f"🗑️ 工具 '{name}' 已注销。")
+        else:
+            print(f"⚠️ 工具 '{name}' 不存在。")
+
+    def get_tool(self, name: str) -> Optional[Tool]:
+        """获取Tool对象"""
+        return self._tools.get(name)
+
+    def get_function(self, name: str) -> Optional[Callable]:
+        """获取工具函数"""
+        func_info = self._functions.get(name)
+        return func_info["func"] if func_info else None
+
+    def execute_tool(self, name: str, input_text: str) -> str:
+        """
+        执行工具
+
+        Args:
+            name: 工具名称
+            input_text: 输入参数
+
+        Returns:
+            工具执行结果
+        """
+        # 优先查找Tool对象
+        if name in self._tools:
+            tool = self._tools[name]
+            try:
+                # 简化参数传递,直接传入字符串
+                return tool.run({"input": input_text})
+            except Exception as e:
+                return f"错误:执行工具 '{name}' 时发生异常: {str(e)}"
+
+        # 查找函数工具
+        elif name in self._functions:
+            func = self._functions[name]["func"]
+            try:
+                return func(input_text)
+            except Exception as e:
+                return f"错误:执行工具 '{name}' 时发生异常: {str(e)}"
+
+        else:
+            return f"错误:未找到名为 '{name}' 的工具。"
+
+    def get_tools_description(self) -> str:
+        """
+        获取所有可用工具的格式化描述字符串
+
+        Returns:
+            工具描述字符串,用于构建提示词
+        """
+        descriptions = []
+
+        # Tool对象描述
+        for tool in self._tools.values():
+            descriptions.append(f"- {tool.name}: {tool.description}")
+
+        # 函数工具描述
+        for name, info in self._functions.items():
+            descriptions.append(f"- {name}: {info['description']}")
+
+        return "\n".join(descriptions) if descriptions else "暂无可用工具"
+
+    def list_tools(self) -> list[str]:
+        """列出所有工具名称"""
+        return list(self._tools.keys()) + list(self._functions.keys())
+
+    def get_all_tools(self) -> list[Tool]:
+        """获取所有Tool对象"""
+        return list(self._tools.values())
+
+    def clear(self):
+        """清空所有工具"""
+        self._tools.clear()
+        self._functions.clear()
+        print("🧹 所有工具已清空。")
+
+
+# 全局工具注册表
+global_registry = ToolRegistry()

+ 6 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/hello_agents/version.py

@@ -0,0 +1,6 @@
+"""版本信息"""
+
+__version__ = "0.2.9"
+__author__ = "HelloAgents Team"
+__email__ = "jjyaoao@126.com"
+__description__ = "灵活、可扩展的多智能体框架 - 基于Datawhale Hello-Agents教程"

+ 117 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/pyproject.toml

@@ -0,0 +1,117 @@
+[build-system]
+requires = ["setuptools>=61.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "hello-agents-optimized"
+version = "0.2.9"
+description = "HelloAgents Optimized - 轻量级、可扩展的多智能体框架(增强版)"
+readme = "README.md"
+license = {text = "CC-BY-NC-SA-4.0"}
+authors = [
+    {name = "HelloAgents Team", email = "jjyaoao@126.com"}
+]
+requires-python = ">=3.10"
+dependencies = [
+    # 核心依赖
+    "openai>=1.0.0,<2.0.0",
+    "requests>=2.25.0,<3.0.0",
+    "python-dotenv>=0.19.0,<2.0.0",
+    "pydantic>=2.0.0,<3.0.0",
+
+    # 基础工具库
+    "beautifulsoup4>=4.9.0,<5.0.0",
+    "numpy>=2.0.0,<3.0.0",
+    "networkx>=2.6.0,<4.0.0",
+    "tiktoken>=0.5.0",
+]
+
+[project.optional-dependencies]
+# 搜索功能
+search = [
+    "tavily-python>=0.7.12",
+    "google-search-results>=2.4.2",
+]
+
+# 记忆系统
+memory = [
+    "qdrant-client>=1.9.0",
+    "neo4j>=5.0.0",
+    "spacy>=3.4.0",
+    "scikit-learn>=1.0.0",
+]
+
+# RAG 系统
+rag = [
+    "scikit-learn>=1.0.0",
+    "transformers>=4.20.0",
+    "torch>=1.12.0",
+    "sentence-transformers>=2.2.0",
+    "markitdown>=0.0.1",
+    "pypdf>=3.9.0",
+    "pdfminer.six>=20221105",
+]
+
+# 协议支持
+protocols = [
+    "fastmcp>=2.0.0,<3.0.0",
+    "a2a-sdk>=0.1.0",
+]
+
+# 评估系统
+evaluation = [
+    "datasets>=2.14.0",
+    "huggingface_hub>=0.20.0,<1.0.0",
+    "evaluate>=0.4.0,<1.0.0",
+    "pandas>=2.0.0,<3.0.0",
+    "matplotlib>=3.7.0,<4.0.0",
+    "seaborn>=0.12.0,<1.0.0",
+    "tqdm>=4.65.0,<5.0.0",
+    "gradio>=4.0.0,<5.0.0",
+]
+
+# 强化学习
+rl = [
+    "trl>=0.24.0",
+    "transformers>=4.20.0",
+    "torch>=2.0.0",
+    "datasets>=2.14.0",
+    "accelerate>=0.20.0",
+    "peft>=0.5.0",
+    "bitsandbytes>=0.41.0",
+    "wandb>=0.15.0",
+    "tensorboard>=2.13.0",
+]
+
+# 记忆 + RAG 组合
+memory-rag = [
+    "hello-agents-optimized[memory,rag]",
+]
+
+# 全部功能
+all = [
+    "hello-agents-optimized[search,memory-rag,protocols,evaluation,rl]",
+]
+
+[project.urls]
+Homepage = "https://github.com/jjyaoao/HelloAgents"
+Documentation = "https://github.com/jjyaoao/HelloAgents"
+Repository = "https://github.com/jjyaoao/HelloAgents"
+
+[tool.setuptools.packages.find]
+where = ["hello_agents"]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+python_files = ["test_*.py"]
+python_classes = ["Test*"]
+python_functions = ["test_*"]
+addopts = "-v --tb=short"
+
+[tool.black]
+line-length = 88
+target-version = ['py310']
+
+[tool.isort]
+profile = "black"
+line_length = 88

+ 3 - 0
Co-creation-projects/lcyting-StockSage-agent/HelloAgents Optimized/setup.py

@@ -0,0 +1,3 @@
+from setuptools import setup, find_packages
+
+setup()

+ 21 - 0
Co-creation-projects/lcyting-StockSage-agent/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 lcy
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 445 - 0
Co-creation-projects/lcyting-StockSage-agent/README.md

@@ -0,0 +1,445 @@
+# 智能股票分析助手
+
+基于**HelloAgents智能体协作框架**的 A 股投资分析工具,整合行情数据、财务分析、新闻舆情、智能选股、模拟交易等功能,提供数据驱动的投资决策辅助。
+
+> ⚠️ **免责声明**:本工具所有分析结果仅供参考,**不构成任何投资建议**。投资有风险,入市需谨慎。
+
+---
+
+## 功能特性
+
+| 模块 | 特性 | 状态 |
+|------|------|------|
+| 📊 **市场行情** | 个股实时行情、指数行情、板块行情 | ✅ |
+| 📈 **财务分析** | 财务指标、公司概况(描述列表排版)、十大股东(多形态表格解析) | ✅ |
+| 📉 **股票分析体验** | 优先加载行情与图表,财务/概况/股东异步加载 | ✅ |
+| 🗣️ **AI舆情分析** | AI 自动搜索资讯并分析市场舆情,流式输出分析结果 | ✅ |
+| 📉 **AI数据分析** | AI 自动查询行情/财务数据并生成分析报告,流式输出 | ✅ |
+| 💬 **AI对话助手** | 协调者 Agent 解析用户需求,自动调度子 Agent,流式对话输出 | ✅ |
+| 📰 **新闻资讯** | 金融资讯搜索、热点快讯浏览 | ✅ |
+| 🔍 **智能选股** | 多条件组合筛选(行情+财务双维度) | ✅ |
+| 🏛️ **巴菲特评估** | 价值投资框架,ReflectionAgent 反思优化流式生成报告,支持下载 Markdown | ✅ |
+| ⭐ **自选股** | 妙想自选增/删/查;股票分析页与智能选股结果行「加自选」;移除需二次确认 | ✅ |
+| 🏠 **仪表盘** | 启动三线程并行预热(指数/自选/热点),自选直接使用 API 返回价格数据 | ✅ |
+| 💰 **模拟交易** | 模拟买入/卖出/撤单、持仓管理、收益曲线;下单/卖出二次确认防误触 | ✅ |
+| 📝 **历史记录** | AI舆情/AI数据/巴菲特/AI对话 四种分析历史按天存储,支持查看/删除 | ✅ |
+| 💾 **文件缓存** | 每只股票数据存为独立 JSON 文件,当日免刷新,支持 grep 关键词搜索 | ✅ |
+| 🧠 **记忆系统** | 每日首启日期追踪,日切重新拉取仪表盘快照;数据持久化至 `data/memory/`(JSON,非 HelloAgents MemoryManager);在**已有自选记录**的前提下自选数量变化会触发记忆层刷新(前端仍会实时请求自选接口) | ✅ |
+| ⚙️ **偏好设置** | 投资风格、风险偏好、行业偏好个性化定制 | ✅ |
+| 🐳 **Docker 部署** | 一键容器化部署,前后端分离 | ✅ |
+| 📦 **exe 打包** | PyInstaller 打包为独立 exe,免安装 Python/Node.js | ✅ |
+
+---
+
+## 项目亮点
+
+- **多智能体协作**:采用 **Reflection**(巴菲特评估)+ **ReAct**(舆情/数据分析/投资顾问)+ **协调者路由**;股票分析页各 Tab 可独立流式跑 Agent,AI 对话助手由协调者按需调度子 Agent。选股、自选股、模拟交易由后端 Service **直连 `skills/`**,不经独立 Agent
+- **AI对话助手**:单次 LLM 路由决策(`data` / `sentiment` / `advisor` 组合),子 Agent **非流式**跑完后由协调者整合;单维度直接推送结果,多维度由 LLM **流式**生成综合答复(含篇幅与字数上限控制,见 `agents/coordinator_agent.py`、`agents/text_truncation.py`)
+- **流式 AI 分析**:舆情分析、数据分析、巴菲特评估均支持 NDJSON 流式输出,实时展示生成过程
+- **巴菲特价值投资框架**:集成完整价值投资分析体系(8份参考文档),ReflectionAgent 自我反思优化评估报告
+- **文件缓存系统**:每只股票的所有数据持久化到本地文件,优先读缓存再调 API,支持 grep 风格关键词检索
+- **个性化投资分析**:支持用户偏好存储(风险偏好、投资风格、行业偏好),经 `preference_service` 持久化并在 API 层提供上下文
+- **全栈一体化**:Vue3 前端 + FastAPI 后端 + HelloAgents 智能体 + 东方财富妙想数据,全链路自包含
+- **操作安全防护**:模拟交易下单/卖出、自选股移除等危险操作均需二次确认
+
+---
+
+## 技术栈
+
+| 层级 | 技术 | 版本 |
+|------|------|------|
+| 前端 | Vue3 + Element Plus + ECharts | 3.x / 2.x / 5.x |
+| 后端 | FastAPI + Uvicorn | 0.110+ |
+| 数据库 | SQLite (SQLAlchemy + aiosqlite) | — |
+| 智能体 | HelloAgents Optimized | 0.2.9 |
+| LLM | DeepSeek / OpenAI 兼容 API | — |
+| 金融数据 | 东方财富妙想 API | — |
+
+---
+
+## 快速开始
+
+### 环境要求
+
+- Python ≥ 3.10
+- Node.js ≥ 18
+- Docker ≥ 24(可选,生产部署)
+
+### 配置环境变量
+
+复制 `.env.example` 为 `.env` 并填入密钥。**本地开发请保持 `BACKEND_PORT=8000`**(与 `frontend/vite.config.js` 代理一致);exe 打包运行可改为 `5174`。常用项如下(完整说明见 `.env.example` 内注释):
+
+```env
+# LLM 大模型(与 HelloAgents 兼容)
+LLM_MODEL_ID=deepseek-chat
+LLM_API_KEY=sk-your-deepseek-key
+LLM_BASE_URL=https://api.deepseek.com
+# 可选:单次请求超时(秒);后端会与更长下限合并,避免 ReAct/对话多轮过早断开
+# LLM_TIMEOUT=180
+
+# 巴菲特评估:可选反思轮数(见 .env.example 中 BUFFETT_MAX_REFLECTIONS)
+
+# 东方财富妙想金融数据
+MX_APIKEY=your-mx-apikey
+# 可选:MX_API_URL、MX_CACHE_TTL_SECONDS、本地回放 MX_REPLAY_FIXTURES 等
+
+# 服务端口:开发模式未设置时后端默认为 8000;PyInstaller exe 未覆盖时默认为 5174(与 run_exe.py 提示一致)
+# 若修改 BACKEND_PORT,请同步修改 frontend/vite.config.js 中 dev server 对 /api 的 proxy.target
+```
+
+> 💡 DeepSeek API: https://platform.deepseek.com  
+> 💡 妙想 API: https://dl.dfcfs.com/m/itc4  
+
+### 本地开发启动
+
+**后端**:
+
+```bash
+# 安装依赖(与下面等价:也可在仓库根目录执行 pip install -r requirements.txt)
+pip install -r backend/requirements.txt
+
+# 启动服务(从项目根目录)
+python -m uvicorn backend.app.main:app --host 0.0.0.0 --port 8000 --reload
+```
+
+API 文档:http://localhost:8000/docs
+
+**前端**:
+
+```bash
+cd frontend
+npm install
+npm run dev
+```
+
+前端界面:http://localhost:5173(开发模式自动代理 /api 到后端 8000 端口)
+
+### Docker 部署
+
+```bash
+docker compose up -d
+```
+
+- 前端:http://localhost:8080
+- 后端 API:http://localhost:8000/docs
+
+详细部署说明见 [DEPLOY.md](./DEPLOY.md)
+
+### exe 独立打包部署
+
+将前后端打包为一个独立 `.exe` 文件,无需安装 Python/Node.js 即可运行。
+
+#### 环境要求
+
+| 组件         | 用途             | 仅打包时需要? |
+| ------------ | ---------------- | :------------: |
+| Python 3.10+ | PyInstaller 打包 |       是       |
+| Node.js 18+  | 前端构建         |       是       |
+| PyInstaller  | Python → exe     |       是       |
+
+
+#### 一键打包
+
+```bash
+# 1. 安装打包依赖
+pip install pyinstaller
+
+# 2. 执行打包脚本(从项目根目录)
+python scripts/build_exe.py
+```
+
+### 运行效果
+
+由于加载的数据比较多,最好等待数据预热后再进入界面,并且由于东方财富限制,**不能挂梯子**,不然会失败。
+
+> 界面截图未随仓库分发;本地启动后可自行截图,或放入 `outputs/screenshots/` 供文档引用。
+
+---
+
+## 项目结构
+
+```
+智能股票分析器/
+├── README.md
+├── README_EN.md
+├── requirements.txt              # 根目录聚合依赖(指向 backend/requirements.txt)
+├── run_exe.py                    # exe 打包运行入口
+├── scripts/
+│   └── build_exe.py              # PyInstaller 打包脚本
+├── data/                         # 数据目录(运行时自动创建)
+│   ├── stock_cache/              #   文件缓存(每只股票独立 JSON)
+│   └── memory/                   #   记忆系统状态(dashboard_state.json)
+├── outputs/                      # 可选:本地报告/截图输出目录
+├── backend/                      # 🖥️ 后端 FastAPI
+│   ├── app/
+│   │   ├── main.py               #   应用入口 + 生命周期
+│   │   ├── config.py             #   配置管理
+│   │   ├── api/                  #   API 路由模块
+│   │   │   ├── chat.py           #     AI 对话助手流式接口
+│   │   │   ├── sentiment.py      #     AI 舆情流式(实现约定路径)
+│   │   │   ├── data_analysis.py  #     AI 数据流式(实现约定路径)
+│   │   │   ├── agent_api.py      #     舆情/数据流式实现 + /agent 兼容路径
+│   │   │   ├── history.py        #     分析历史记录 CRUD
+│   │   │   ├── system_browser.py #     exe/桌面:后端唤起系统浏览器打开外链
+│   │   │   ├── cache_api.py      #     文件缓存管理/grep搜索
+│   │   │   └── ...               #     market/financial/news/screener 等
+│   │   ├── services/             #   业务逻辑层
+│   │   │   ├── chat_service.py   #     对话助手调度(协调者流式)
+│   │   │   ├── memory_service.py #     记忆系统(日切+仪表盘预热等)
+│   │   │   ├── stock_file_cache.py#    文件缓存(grep搜索支持)
+│   │   │   ├── history_service.py#     历史记录 CRUD
+│   │   │   ├── screener_service.py#    智能选股(直连 skills/mx-xuangu)
+│   │   │   ├── watchlist_service.py#   自选股(直连 skills/mx-zixuan)
+│   │   │   ├── simulation_service.py#  模拟交易(直连 skills/mx-moni)
+│   │   │   ├── preference_service.py#  用户偏好 CRUD / 上下文
+│   │   │   └── ...               #     market / news / buffett 等
+│   │   ├── models/               #   数据模型
+│   │   │   ├── report.py         #     AnalysisReport
+│   │   │   ├── history_models.py #     分析历史 ORM(SQLite)
+│   │   │   ├── history.py        #     兼容 re-export AnalysisHistory
+│   │   │   ├── memory_models.py  #     MemorySnapshot 等记忆数据结构
+│   │   │   └── preference.py     #     UserPreference
+│   │   ├── middleware/           #   中间件占位(骨架,main 中未注册)
+│   │   └── utils/                #   工具
+│   ├── Dockerfile
+│   └── requirements.txt
+│
+├── frontend/                     # 🎨 前端 Vue3
+│   ├── src/
+│   │   ├── views/                #   页面视图
+│   │   │   ├── Dashboard.vue     #     仪表盘(自选直接价格展示)
+│   │   │   ├── StockAnalysis.vue #     股票分析(6 Tab)
+│   │   │   ├── StockScreener.vue #     智能选股
+│   │   │   ├── NewsCenter.vue    #     新闻资讯
+│   │   │   └── Simulation.vue    #     模拟交易(二次确认)
+│   │   ├── components/           #   公共组件
+│   │   │   ├── StreamOutput.vue  #     流式输出通用组件
+│   │   │   ├── AIAnalysisPanel.vue#    AI分析面板
+│   │   │   ├── ChatAssistant.vue #     AI对话助手
+│   │   │   ├── HistoryDrawer.vue #     历史记录抽屉
+│   │   │   └── NewsDetailDrawer.vue#   新闻详情
+│   │   ├── api/                  #   Axios 封装
+│   │   ├── router/               #   Vue Router
+│   │   └── store/                #   Pinia 状态管理
+│   ├── Dockerfile
+│   ├── nginx.conf
+│   └── package.json
+│
+├── agents/                       # 🤖 智能体层(仅 AI 分析 / 对话)
+│   ├── agent_system.py           #   统一 Agent 管理与流式调度
+│   ├── coordinator_agent.py      #   AI对话助手(路由 LLM + 子Agent顺序调用 + 整合流式)
+│   ├── text_truncation.py        #   协调者输出自然边界截断
+│   ├── advisor_agent.py          #   巴菲特评估 Agent(Reflection+流式)
+│   ├── general_advisor_agent.py  #   普通投资顾问 Agent(ReAct)
+│   ├── sentiment_agent.py        #   舆情分析 Agent(ReAct + 流式)
+│   ├── data_analysis_agent.py    #   数据分析 Agent(ReAct + 流式)
+│   ├── tests/                    #   智能体层单元测试
+│   └── tools/                    #   ReAct 工具封装(供上述 Agent 使用)
+│       ├── mx_data_tool.py       #     金融数据 → skills/金融数据
+│       └── mx_search_tool.py     #     资讯搜索 → skills/资讯搜索
+│
+├── HelloAgents Optimized/        # 🧩 多智能体框架(StockSage 精简版)
+│   └── hello_agents/
+│       ├── core/                 #   LLM、Config、Agent 基类、ConversationManager 等
+│       └── agents/               #   ReActAgent、ReflectionAgent(本应用实际使用)
+│
+├── skills/                       # 🔧 东方财富妙想 Skill(业务 API 直连层)
+│   ├── 金融数据/mx-data
+│   ├── 资讯搜索/mx-search
+│   ├── 智能选股/mx-xuangu
+│   ├── 自选股管理/mx-zixuan
+│   ├── 模拟组合管理/mx-moni
+│   └── 巴菲特投资思维/           #   价值投资参考文档
+│
+├── docker-compose.yml
+├── DEPLOY.md
+└── .env.example
+```
+
+---
+
+## API 路由概览
+
+| 路由组 | 前缀 | 说明 |
+|--------|------|------|
+| System | `/api/v1/system` | `GET /health`、`GET /config`(在 `main.py` 注册);`POST /open-external-url` 由 `system_browser.py` 注册在同前缀下,供 exe 场景唤起本机浏览器打开 http(s) 链接 |
+| Market | `/api/v1/market` | 个股行情、指数、板块 |
+| Financial | `/api/v1/financial` | 财务指标、公司概况、股东 |
+| Analysis | `/api/v1/analysis` | 个股深度报告 `POST /report/{code}`、`GET /report/{report_id}`、列表 `GET /reports` |
+| News | `/api/v1/news` | 资讯搜索、舆情分析、热点 |
+| Screener | `/api/v1/screener` | 条件选股、筛选条件 |
+| Watchlist | `/api/v1/watchlist` | 自选股增删查 |
+| Simulation | `/api/v1/simulation` | 模拟交易(买卖/撤单/持仓) |
+| Buffett | `/api/v1/buffett` | 巴菲特评估(Reflection / advisor 流式生成,历史类型 `buffett`) |
+| Preferences | `/api/v1/preferences` | 用户投资偏好 CRUD |
+| **Chat** | `/api/v1/chat` | **AI对话助手** NDJSON 流式 `POST /stream` |
+| **Sentiment** | `/api/v1/sentiment` | **AI 舆情** `POST /analyze/stream`(实现方案约定路径) |
+| **Data analysis** | `/api/v1/data-analysis` | **AI 数据分析** `POST /analyze/stream`(实现方案约定路径) |
+| **Agent(兼容)** | `/api/v1/agent` | 与上语义相同:`POST /sentiment/stream`、`POST /data-analysis/stream` |
+| **History** | `/api/v1/history` | 分析历史列表/详情/删除/清空;`type` 含 `sentiment` / `data_analysis` / `buffett` / `chat` |
+| **Cache** | `/api/v1/cache` | 文件缓存 grep / 统计 / 清除 |
+
+> 完整 Swagger 文档:开发默认 http://localhost:8000/docs(端口以 `.env` 中 `BACKEND_PORT` 为准)
+
+---
+
+## 智能体协作流程
+
+### AI 对话助手流程
+
+```
+用户对话消息
+    │
+    ▼
+协调者(路由 LLM,单行关键字)
+    │ none → 一般对话(协调者 LLM 流式回复,或股票上下文引导)
+    │ data / sentiment / advisor 或其组合
+    ▼
+子 Agent 顺序执行(非流式,带字数上限)
+    ├── data → run_data_analysis
+    ├── sentiment → run_sentiment
+    └── advisor → 将已有 data/sentiment 摘要写入顾问提示 → run_advisor
+    │
+    ▼
+面向用户的流式输出
+    ├── 仅单一子 Agent → 直接推送该结果(截断至上限)
+    └── 多 Agent → 协调者 LLM stream_invoke 整合为结构化答复
+```
+
+### 巴菲特评估流程
+
+```
+用户点击「生成巴菲特评估报告」
+    │
+    ▼
+advisor_agent (ReflectionAgent)
+    │ 收集行情/财务/舆情数据
+    ├── 初始分析 → 护城河/管理层/安全边际
+    ├── 自我反思 → 数据准确性/逻辑自洽性
+    └── 迭代优化 → 完善报告
+    │
+    ▼
+流式输出最终评估报告 + 自动保存历史
+```
+
+### 业务 API 与 Agent 分工
+
+| 能力 | 实现路径 |
+|------|----------|
+| AI 舆情 / 数据分析 / 对话 / 巴菲特评估 | `agents/*` + HelloAgents(ReAct / Reflection / 协调者) |
+| 智能选股 / 自选股 / 模拟交易 / 行情资讯查询 | `backend/app/services/*` → `skills/*`(妙想 API) |
+| 用户偏好 | `preference_service` + `/api/v1/preferences` |
+
+---
+
+## 股票分析页结构
+
+进入个股分析(`/analysis` 或 `/analysis/:code`,`:code` 可选)时,共 **6 个 Tab**:
+
+| Tab | 内容 | 说明 |
+|-----|------|------|
+| 📊 行情图表 | ECharts K线 + 行情明细 | 优先加载 |
+| 📈 财务数据 | 财务指标卡片 + 公司概况 + 十大股东 | 异步加载 |
+| 🗣️ AI舆情分析 | AI 自动搜索资讯分析市场情绪 | 流式输出 |
+| 📉 AI数据分析 | AI 查询行情/财务并生成报告 | 流式输出 |
+| 🧠 巴菲特评估 | ReflectionAgent 反思优化评估 | 流式输出 |
+| 💬 AI对话助手 | 自由对话,自动调度子 Agent | 流式输出 |
+
+每个分析 Tab 均支持查看历史记录和下载报告。
+
+---
+
+## 文件缓存系统
+
+所有股票数据自动持久化到 `data/stock_cache/{股票代码}/` 目录:
+
+```
+data/stock_cache/
+  _index.json           # 主索引
+  600519/
+    quote.json          # 行情数据
+    financial.json      # 财务指标
+    profile.json        # 公司概况
+    holders.json        # 十大股东
+    sentiment.json      # 舆情分析
+    news.json           # 相关资讯(如有)
+```
+
+- 当日缓存直接返回,不限小时数;跨天数据超 24h 自动过期
+- 未命中时才调用 API 并自动写回文件
+- 支持 `GET /api/v1/cache/search?keyword=贵州茅台` grep 风格关键词检索
+- 服务重启后缓存持久保留
+
+---
+
+## 预留配置(当前未接入)
+
+`.env.example` 中的 **Redis**、**JWT** 变量已在 `config.py` 预留,**当前版本未使用**(缓存走妙想进程内 TTL + 文件缓存;API 无登录鉴权)。接入计划见下方「未来计划」。
+
+## 未来计划
+
+- [ ] 增加技术指标分析(MACD、KDJ、RSI 等)
+- [ ] 实现用户认证系统(JWT Token,配置项已预留)
+- [ ] 添加投资组合优化算法(马科维茨模型)
+- [ ] 增加 A 股交易日历和节假日判断
+- [ ] 添加策略回测引擎
+- [ ] 增加历史记录全文搜索
+
+---
+
+## 贡献指南
+
+欢迎提出 Issue 和 Pull Request!
+
+### 开发流程
+
+1. Fork 本仓库
+2. 创建功能分支:`git checkout -b feature/your-feature`
+3. 提交修改:`git commit -m "feat: 功能描述"`
+4. 推送到分支:`git push origin feature/your-feature`
+5. 创建 Pull Request
+
+### 提交规范
+
+| 类型 | 说明 |
+|------|------|
+| `feat` | 新增功能 |
+| `fix` | 修复 bug |
+| `docs` | 文档更新 |
+| `style` | 代码格式调整(不影响功能) |
+| `refactor` | 代码重构 |
+| `test` | 测试相关 |
+| `chore` | 其他修改(如依赖更新) |
+
+### PR 自检清单
+
+- [x] 代码能够正常运行,没有报错
+- [x] 相关文档已更新
+- [x] 有清晰的使用示例(如适用)
+- [x] 代码有适当的中文注释
+- [x] 处理了常见的异常情况
+
+---
+
+## 许可证
+
+MIT License
+
+---
+
+## 作者
+
+```
+- GitHub: [@lcyting](https://github.com/lcyting)
+- Email: lcy154745@163.com
+```
+
+---
+
+## 致谢
+
+- 感谢 [Hello-Agents](https://github.com/datawhalechina/hello-agents) 提供的多智能体框架
+- 感谢 [Datawhale](https://www.datawhale.cn) 开源学习社区
+- 感谢 [agi-queen](https://github.com/agi-now/buffett-skills/commits?author=agi-queen) 的开源bft-skills
+- 感谢东方财富妙想 API 提供的金融数据服务
+- 感谢所有GitHub开源贡献者

+ 446 - 0
Co-creation-projects/lcyting-StockSage-agent/README_EN.md

@@ -0,0 +1,446 @@
+# Intelligent Stock Analysis Assistant
+
+An A-share investment analysis tool built on the **HelloAgents multi-agent collaboration framework**, integrating market data, financial analysis, news sentiment, intelligent stock screening, simulated trading, and more to provide data-driven investment decision support.
+
+> ⚠️ **Disclaimer**: All analysis results from this tool are for reference only and **do not constitute investment advice**. Investing involves risk; exercise caution when entering the market.
+
+---
+
+## Features
+
+| Module | Feature | Status |
+|--------|---------|:------:|
+| 📊 **Market Quotes** | Real-time individual stock quotes, index quotes, sector quotes | ✅ |
+| 📈 **Financial Analysis** | Financial indicators, company profile (description list layout), top 10 shareholders (multi-format table parsing) | ✅ |
+| 📉 **Stock Analysis UX** | Prioritize loading quotes and charts; finance/profile/shareholders load asynchronously | ✅ |
+| 🗣️ **AI Sentiment Analysis** | AI auto-searches news and analyzes market sentiment, streaming output | ✅ |
+| 📉 **AI Data Analysis** | AI auto-queries market/financial data and generates analysis reports, streaming output | ✅ |
+| 💬 **AI Chat Assistant** | Coordinator Agent parses user intent, auto-dispatches sub-agents, streaming dialogue output | ✅ |
+| 📰 **News & Info** | Financial news search, hot headlines browsing | ✅ |
+| 🔍 **Smart Stock Screener** | Multi-criteria combined screening (market + financial dual dimensions) | ✅ |
+| 🏛️ **Buffett Evaluation** | Value investing framework, ReflectionAgent self-reflection optimized streaming report generation with Markdown download | ✅ |
+| ⭐ **Watchlist** | MX watchlist add/delete/query; "Add to Watchlist" on stock analysis page & screener results; removal requires confirmation | ✅ |
+| 🏠 **Dashboard** | Three-thread parallel warmup (indices/watchlist/hotspots), watchlist uses API-returned price data directly | ✅ |
+| 💰 **Simulated Trading** | Simulated buy/sell/cancel orders, position management, profit curve; order/cancel confirmation dialogs to prevent misoperation | ✅ |
+| 📝 **History Records** | AI Sentiment / AI Data / Buffett / AI Chat four analysis history types stored by day, with view/delete support | ✅ |
+| 💾 **File Cache** | Each stock's data saved as an independent JSON file, no refresh within the day, supports grep keyword search | ✅ |
+| 🧠 **Memory System** | Daily first-start date tracking, daily cutover re-fetches dashboard snapshot; persisted under `data/memory/` (JSON, not HelloAgents MemoryManager); watchlist count changes trigger refresh **when prior watchlist records exist** (frontend still requests watchlist API in real-time) | ✅ |
+| ⚙️ **Preferences** | Personalized investment style, risk preference, and sector preference settings | ✅ |
+| 🐳 **Docker Deployment** | One-click containerized deployment, frontend/backend separation | ✅ |
+| 📦 **exe Packaging** | PyInstaller packaging as standalone exe, no Python/Node.js installation required | ✅ |
+
+---
+
+## Highlights
+
+- **Multi-Agent Collaboration**: Uses **Reflection** (Buffett evaluation), **ReAct** (sentiment/data/general advisor), and **coordinator routing**; stock analysis tabs run streaming agents independently; the AI chat assistant dispatches sub-agents on demand. Stock screening, watchlist, and simulated trading use backend services **directly against `skills/`**, not separate agents
+- **AI Chat Assistant**: Single LLM routing decision (`data` / `sentiment` / `advisor` combinations), sub-agents run **non-streaming**, then coordinator consolidates; single-dimension pushes results directly, multi-dimension uses LLM **streaming** to generate a comprehensive reply (with length and word count limits, see `agents/coordinator_agent.py`, `agents/text_truncation.py`)
+- **Streaming AI Analysis**: Sentiment analysis, data analysis, and Buffett evaluation all support NDJSON streaming output with real-time generation display
+- **Buffett Value Investing Framework**: Full value investing analysis system integrated (8 reference documents), ReflectionAgent self-reflection optimizes evaluation reports
+- **File Cache System**: All data per stock persisted to local files, prioritize cache reads over API calls, supports grep-style keyword search
+- **Personalized Investment Analysis**: User preference storage (risk preference, investment style, sector preference), persisted via `preference_service` and exposed through `/api/v1/preferences`
+- **Full-Stack Integration**: Vue3 frontend + FastAPI backend + HelloAgents agents + East Money MX data, end-to-end self-contained
+- **Operational Safety**: Dangerous operations like simulated buy/sell and watchlist removal require confirmation dialogs
+
+---
+
+## Tech Stack
+
+| Layer | Technology | Version |
+|-------|-----------|---------|
+| Frontend | Vue3 + Element Plus + ECharts | 3.x / 2.x / 5.x |
+| Backend | FastAPI + Uvicorn | 0.110+ |
+| Database | SQLite (SQLAlchemy + aiosqlite) | — |
+| Agents | HelloAgents Optimized | 0.2.9 |
+| LLM | DeepSeek / OpenAI-compatible API | — |
+| Financial Data | East Money MX API | — |
+
+---
+
+## Quick Start
+
+### Prerequisites
+
+- Python ≥ 3.10
+- Node.js ≥ 18
+- Docker ≥ 24 (optional, for production deployment)
+
+### Environment Variables
+
+Copy `.env.example` to `.env` and fill in your keys. **For local dev, keep `BACKEND_PORT=8000`** (must match `frontend/vite.config.js` proxy). For exe builds, use `5174` if needed. Commonly used items are as follows (see `.env.example` comments for full details):
+
+```env
+# LLM (compatible with HelloAgents)
+LLM_MODEL_ID=deepseek-chat
+LLM_API_KEY=sk-your-deepseek-key
+LLM_BASE_URL=https://api.deepseek.com
+# Optional: single request timeout (seconds); backend merges with a longer floor value
+# to prevent premature disconnection during ReAct/chat multi-turn
+# LLM_TIMEOUT=180
+
+# Buffett evaluation: optional reflection rounds (see BUFFETT_MAX_REFLECTIONS in .env.example)
+
+# East Money MX financial data
+MX_APIKEY=your-mx-apikey
+# Optional: MX_API_URL, MX_CACHE_TTL_SECONDS, local replay MX_REPLAY_FIXTURES, etc.
+
+# Service port: dev mode defaults to 8000 when not set; PyInstaller exe defaults to 5174 when not overridden
+# (consistent with run_exe.py prompt)
+# If modifying BACKEND_PORT, also update the /api proxy target in frontend/vite.config.js
+```
+
+> 💡 DeepSeek API: https://platform.deepseek.com  
+> 💡 MX API: https://dl.dfcfs.com/m/itc4  
+
+### Local Development
+
+**Backend**:
+
+```bash
+# Install dependencies (equivalent: also run pip install -r requirements.txt from repo root)
+pip install -r backend/requirements.txt
+
+# Start service (from project root)
+python -m uvicorn backend.app.main:app --host 0.0.0.0 --port 8000 --reload
+```
+
+API docs: http://localhost:8000/docs
+
+**Frontend**:
+
+```bash
+cd frontend
+npm install
+npm run dev
+```
+
+Frontend UI: http://localhost:5173 (dev mode auto-proxies /api to backend port 8000)
+
+### Docker Deployment
+
+```bash
+docker compose up -d
+```
+
+- Frontend: http://localhost:8080
+- Backend API: http://localhost:8000/docs
+
+For detailed deployment instructions, see [DEPLOY.md](./DEPLOY.md)
+
+### Standalone exe Packaging
+
+Package the frontend and backend into a single `.exe` file, no Python/Node.js installation required to run.
+
+#### Requirements
+
+| Component     | Purpose           | Packaging Only? |
+| ------------- | ----------------- |:---------------:|
+| Python 3.10+  | PyInstaller build | Yes             |
+| Node.js 18+   | Frontend build    | Yes             |
+| PyInstaller   | Python → exe      | Yes             |
+
+#### One-Click Build
+
+```bash
+# 1. Install packaging dependencies
+pip install pyinstaller
+
+# 2. Run the build script (from project root)
+python scripts/build_exe.py
+```
+
+### Screenshots
+
+Due to the large amount of data loaded, it's best to wait for data warmup before entering the interface. Also, due to East Money restrictions, **do not use a VPN/proxy**, or it will fail.
+
+> UI screenshots are not shipped with the repo. Capture them locally after startup, or place them under `outputs/screenshots/` for documentation.
+
+---
+
+## Project Structure
+
+```
+intelligent-stock-analyzer/
+├── README.md
+├── README_EN.md
+├── requirements.txt                # Root aggregated deps (points to backend/requirements.txt)
+├── run_exe.py                      # exe runtime entry
+├── scripts/
+│   └── build_exe.py                # PyInstaller build script
+├── data/                           # Data directory (created at runtime)
+│   ├── stock_cache/                #   File cache (independent JSON per stock)
+│   └── memory/                     #   Memory system state (dashboard_state.json)
+├── outputs/                        # Optional: local reports/screenshots
+├── backend/                        # 🖥️ Backend FastAPI
+│   ├── app/
+│   │   ├── main.py                 #   App entry + lifecycle
+│   │   ├── config.py               #   Configuration management
+│   │   ├── api/                    #   API route modules
+│   │   │   ├── chat.py             #     AI chat assistant streaming endpoint
+│   │   │   ├── sentiment.py        #     AI sentiment streaming (agreed path)
+│   │   │   ├── data_analysis.py    #     AI data analysis streaming (agreed path)
+│   │   │   ├── agent_api.py        #     Sentiment/data streaming impl + /agent compat path
+│   │   │   ├── history.py          #     Analysis history CRUD
+│   │   │   ├── system_browser.py   #     exe/desktop: backend opens system browser for external links
+│   │   │   ├── cache_api.py        #     File cache management / grep search
+│   │   │   └── ...                 #     market / financial / news / screener etc.
+│   │   ├── services/               #   Business logic layer
+│   │   │   ├── chat_service.py     #     Chat assistant dispatch (coordinator streaming)
+│   │   │   ├── memory_service.py   #     Memory system (daily cutover + dashboard warmup etc.)
+│   │   │   ├── stock_file_cache.py #     File cache (grep search support)
+│   │   │   ├── history_service.py  #     History CRUD
+│   │   │   ├── screener_service.py #     Stock screening (direct skills/mx-xuangu)
+│   │   │   ├── watchlist_service.py#     Watchlist (direct skills/mx-zixuan)
+│   │   │   ├── simulation_service.py#    Simulated trading (direct skills/mx-moni)
+│   │   │   ├── preference_service.py#    User preferences CRUD / context
+│   │   │   └── ...                 #     market / news / buffett etc.
+│   │   ├── models/                 #   Data models
+│   │   │   ├── report.py           #     AnalysisReport
+│   │   │   ├── history_models.py   #     Analysis history ORM (SQLite)
+│   │   │   ├── history.py          #     Compat re-export AnalysisHistory
+│   │   │   ├── memory_models.py    #     MemorySnapshot and other memory data structures
+│   │   │   └── preference.py       #     UserPreference
+│   │   ├── middleware/             #   Middleware placeholder (skeleton, not registered in main)
+│   │   └── utils/                  #   Utilities
+│   ├── Dockerfile
+│   └── requirements.txt
+│
+├── frontend/                       # 🎨 Frontend Vue3
+│   ├── src/
+│   │   ├── views/                  #   Page views
+│   │   │   ├── Dashboard.vue       #     Dashboard (watchlist direct price display)
+│   │   │   ├── StockAnalysis.vue   #     Stock analysis (6 tabs)
+│   │   │   ├── StockScreener.vue   #     Smart stock screener
+│   │   │   ├── NewsCenter.vue      #     News center
+│   │   │   └── Simulation.vue      #     Simulated trading (confirmation dialogs)
+│   │   ├── components/             #   Shared components
+│   │   │   ├── StreamOutput.vue    #     Streaming output generic component
+│   │   │   ├── AIAnalysisPanel.vue #     AI analysis panel
+│   │   │   ├── ChatAssistant.vue   #     AI chat assistant
+│   │   │   ├── HistoryDrawer.vue   #     History drawer
+│   │   │   └── NewsDetailDrawer.vue#     News detail
+│   │   ├── api/                    #   Axios wrapper
+│   │   ├── router/                 #   Vue Router
+│   │   └── store/                  #   Pinia state management
+│   ├── Dockerfile
+│   ├── nginx.conf
+│   └── package.json
+│
+├── agents/                         # 🤖 Agent layer (AI analysis & chat only)
+│   ├── agent_system.py             #   Unified agent management & streaming dispatch
+│   ├── coordinator_agent.py        #   AI chat assistant (routing LLM + sequential sub-agent calls + integrated streaming)
+│   ├── text_truncation.py          #   Coordinator output natural boundary truncation
+│   ├── advisor_agent.py            #   Buffett evaluation agent (Reflection + streaming)
+│   ├── general_advisor_agent.py    #   General investment advisor agent (ReAct)
+│   ├── sentiment_agent.py          #   Sentiment analysis agent (ReAct + streaming)
+│   ├── data_analysis_agent.py      #   Data analysis agent (ReAct + streaming)
+│   ├── tests/                      #   Agent-layer unit tests
+│   └── tools/                      #   ReAct tool wrappers (for agents above)
+│       ├── mx_data_tool.py         #     Market data → skills/金融数据
+│       └── mx_search_tool.py       #     News search → skills/资讯搜索
+│
+├── HelloAgents Optimized/          # 🧩 Multi-agent framework (StockSage slim build)
+│   └── hello_agents/
+│       ├── core/                   #   LLM, Config, Agent base, ConversationManager, etc.
+│       └── agents/                 #   ReActAgent, ReflectionAgent (used by this app)
+│
+├── skills/                         # 🔧 East Money MX skills (business API layer)
+│   ├── 金融数据/mx-data
+│   ├── 资讯搜索/mx-search
+│   ├── 智能选股/mx-xuangu
+│   ├── 自选股管理/mx-zixuan
+│   ├── 模拟组合管理/mx-moni
+│   └── 巴菲特投资思维/             #   Value investing reference docs
+│
+├── docker-compose.yml
+├── DEPLOY.md
+└── .env.example
+```
+
+---
+
+## API Route Overview
+
+| Route Group | Prefix | Description |
+|-------------|--------|-------------|
+| System | `/api/v1/system` | `GET /health`, `GET /config` (registered in `main.py`); `POST /open-external-url` registered by `system_browser.py` under the same prefix, for exe scenarios to open http(s) links in the native browser |
+| Market | `/api/v1/market` | Individual stock quotes, indices, sectors |
+| Financial | `/api/v1/financial` | Financial indicators, company profile, shareholders |
+| Analysis | `/api/v1/analysis` | In-depth stock report `POST /report/{code}`, `GET /report/{report_id}`, list `GET /reports` |
+| News | `/api/v1/news` | News search, sentiment analysis, hot topics |
+| Screener | `/api/v1/screener` | Conditional screening, filter criteria |
+| Watchlist | `/api/v1/watchlist` | Watchlist add/delete/query |
+| Simulation | `/api/v1/simulation` | Simulated trading (buy/sell/cancel/positions) |
+| Buffett | `/api/v1/buffett` | Buffett evaluation (Reflection / advisor streaming, history type `buffett`) |
+| Preferences | `/api/v1/preferences` | User investment preferences CRUD |
+| **Chat** | `/api/v1/chat` | **AI chat assistant** NDJSON streaming `POST /stream` |
+| **Sentiment** | `/api/v1/sentiment` | **AI sentiment** `POST /analyze/stream` (agreed path) |
+| **Data analysis** | `/api/v1/data-analysis` | **AI data analysis** `POST /analyze/stream` (agreed path) |
+| **Agent (compat)** | `/api/v1/agent` | Semantically same as above: `POST /sentiment/stream`, `POST /data-analysis/stream` |
+| **History** | `/api/v1/history` | Analysis history list/detail/delete/clear; `type` includes `sentiment` / `data_analysis` / `buffett` / `chat` |
+| **Cache** | `/api/v1/cache` | File cache grep / stats / clear |
+
+> Full Swagger docs: dev default http://localhost:8000/docs (port per `BACKEND_PORT` in `.env`)
+
+---
+
+## Agent Collaboration Flow
+
+### AI Chat Assistant Flow
+
+```
+User chat message
+    │
+    ▼
+Coordinator (routing LLM, single-line keyword)
+    │ none → general conversation (coordinator LLM streaming reply, or stock context guidance)
+    │ data / sentiment / advisor or combinations
+    ▼
+Sub-Agents run sequentially (non-streaming, with word limit)
+    ├── data → run_data_analysis
+    ├── sentiment → run_sentiment
+    └── advisor → inject existing data/sentiment summaries into advisor prompt → run_advisor
+    │
+    ▼
+User-facing streaming output
+    ├── single sub-agent only → directly push that result (truncated to limit)
+    └── multiple agents → coordinator LLM stream_invoke to integrate into structured response
+```
+
+### Buffett Evaluation Flow
+
+```
+User clicks "Generate Buffett Evaluation Report"
+    │
+    ▼
+advisor_agent (ReflectionAgent)
+    │ collects market/financial/sentiment data
+    ├── initial analysis → moat / management / margin of safety
+    ├── self-reflection → data accuracy / logical consistency
+    └── iterative optimization → refine report
+    │
+    ▼
+Streaming final evaluation report + auto-save history
+```
+
+### Business API vs. Agents
+
+| Capability | Implementation |
+|------------|----------------|
+| AI sentiment / data analysis / chat / Buffett evaluation | `agents/*` + HelloAgents (ReAct / Reflection / coordinator) |
+| Stock screening / watchlist / simulated trading / market & news queries | `backend/app/services/*` → `skills/*` (MX API) |
+| User preferences | `preference_service` + `/api/v1/preferences` |
+
+---
+
+## Stock Analysis Page Structure
+
+When entering individual stock analysis (`/analysis` or `/analysis/:code`, `:code` optional), there are **6 tabs**:
+
+| Tab | Content | Notes |
+|-----|---------|-------|
+| 📊 Market Charts | ECharts candlestick + market details | Priority loading |
+| 📈 Financial Data | Financial indicator cards + company profile + top 10 shareholders | Async loading |
+| 🗣️ AI Sentiment | AI auto-searches news for market sentiment analysis | Streaming output |
+| 📉 AI Data Analysis | AI queries market/financial data to generate reports | Streaming output |
+| 🧠 Buffett Evaluation | ReflectionAgent self-reflection optimized evaluation | Streaming output |
+| 💬 AI Chat Assistant | Free-form conversation, auto-dispatches sub-agents | Streaming output |
+
+Each analysis tab supports viewing history records and downloading reports.
+
+---
+
+## File Cache System
+
+All stock data is automatically persisted to the `data/stock_cache/{stock_code}/` directory:
+
+```
+data/stock_cache/
+  _index.json           # Master index
+  600519/
+    quote.json          # Market data
+    financial.json      # Financial indicators
+    profile.json        # Company profile
+    holders.json        # Top 10 shareholders
+    sentiment.json      # Sentiment analysis
+    news.json           # Related news (when cached)
+```
+
+- Same-day cache returns directly with no hourly limit; cross-day data expires after 24h
+- API is called only on cache miss, and results are automatically written back
+- Supports `GET /api/v1/cache/search?keyword=Kweichow Moutai` grep-style keyword search
+- Cache persists across service restarts
+
+---
+
+## Reserved Config (Not Wired Yet)
+
+**Redis** and **JWT** variables in `.env.example` are read by `config.py` but **not used** in the current release (caching uses in-process MX TTL + file cache; APIs have no login). See Roadmap below.
+
+## Roadmap
+
+- [ ] Add technical indicator analysis (MACD, KDJ, RSI, etc.)
+- [ ] Implement user authentication system (JWT Token; env vars reserved)
+- [ ] Add portfolio optimization algorithms (Markowitz model)
+- [ ] Add A-share trading calendar and holiday detection
+- [ ] Add strategy backtesting engine
+- [ ] Add full-text search for history records
+
+---
+
+## Contributing
+
+Issues and Pull Requests are welcome!
+
+### Development Workflow
+
+1. Fork this repository
+2. Create a feature branch: `git checkout -b feature/your-feature`
+3. Commit changes: `git commit -m "feat: feature description"`
+4. Push to branch: `git push origin feature/your-feature`
+5. Create a Pull Request
+
+### Commit Conventions
+
+| Type | Description |
+|------|-------------|
+| `feat` | New feature |
+| `fix` | Bug fix |
+| `docs` | Documentation update |
+| `style` | Code formatting (no logic change) |
+| `refactor` | Code refactoring |
+| `test` | Test-related |
+| `chore` | Miscellaneous (e.g., dependency updates) |
+
+### PR Checklist
+
+- [x] Code runs normally without errors
+- [x] Related documentation updated
+- [x] Clear usage examples (if applicable)
+- [x] Code has appropriate comments
+- [x] Common exceptions are handled
+
+---
+
+## License
+
+MIT License
+
+---
+
+## Author
+
+```
+- GitHub: [@lcyting](https://github.com/lcyting)
+- Email: lcy154745@163.com
+```
+
+---
+
+## Acknowledgments
+
+- Thanks to [Hello-Agents](https://github.com/datawhalechina/hello-agents) for the multi-agent framework
+- Thanks to [Datawhale](https://www.datawhale.cn) open-source learning community
+- Thanks to [agi-queen](https://github.com/agi-now/buffett-skills/commits?author=agi-queen) for the open-source bft-skills
+- Thanks to East Money MX API for financial data services
+- Thanks to all GitHub open-source contributors

+ 2 - 0
Co-creation-projects/lcyting-StockSage-agent/agents/__init__.py

@@ -0,0 +1,2 @@
+# 智能股票分析助手 — 智能体层
+# 基于 HelloAgents Optimized 框架实现多Agent协作

+ 271 - 0
Co-creation-projects/lcyting-StockSage-agent/agents/advisor_agent.py

@@ -0,0 +1,271 @@
+"""
+智能股票分析助手 — 巴菲特评估Agent(投资顾问Agent)
+
+基于 HelloAgents ReflectionAgent(反思范式),结合巴菲特价值投资思维,
+对股票进行深度评估分析。**仅允许巴菲特评估界面调用,不允许协调者Agent调用。**
+
+支持流式输出评估报告。
+"""
+
+import sys
+import os
+from pathlib import Path
+from typing import Iterator, Optional
+
+_PROJECT_ROOT = Path(__file__).parent.parent
+_HELLO_PATH = _PROJECT_ROOT / "HelloAgents Optimized"
+_BACKEND_DIR = _PROJECT_ROOT / "backend"
+for p in [_HELLO_PATH, _BACKEND_DIR]:
+    if str(p) not in sys.path:
+        sys.path.insert(0, str(p))
+
+from hello_agents.agents.reflection_agent import ReflectionAgent
+from hello_agents.core.llm import HelloAgentsLLM
+from hello_agents.core.config import Config
+from hello_agents.core.stream import StreamEvent
+
+from .text_truncation import truncate_at_natural_boundary
+
+BUFFETT_INITIAL_PROMPT = """
+你是一位深谙巴菲特价值投资理念的资深投资顾问。请根据以下数据,对股票进行专业的巴菲特式投资分析。
+
+## 股票信息
+- 股票代码: {stock_code}
+- 股票名称: {stock_name}
+
+## 分析数据:
+{data_context}
+
+## 评估维度(巴菲特价值投资框架):
+1. **能力圈评估**: 该公司的业务你是否能理解?商业模式是否简单清晰?
+2. **护城河分析**: 公司是否有持久的竞争优势?(品牌、技术、规模、网络效应、成本优势等)
+3. **管理层评估**: 管理层是否诚信、有能力?(经营历史、资本配置记录)
+4. **财务健康**: 资产负债表是否稳健?(负债率、现金流、ROE稳定性、毛利率)
+5. **估值分析**: 当前股价是否低于内在价值?安全边际是否充足?
+6. **长期前景**: 公司未来5-10年是否能持续增长?(行业趋势、市场份额)
+
+请提供一个完整、专业的巴菲特式投资分析报告。
+文末必须标注:⚠️ 以上分析仅供参考,不构成投资建议。投资有风险,入市需谨慎。
+"""
+
+BUFFETT_REFLECT_PROMPT = """
+请以严格的投资委员会视角,审查以下巴菲特式投资分析报告的准确性和完整性:
+
+# 原始分析数据:
+{task}
+
+# 当前分析报告:
+{content}
+
+请检查以下方面并提供改进建议:
+1. 数据引用是否准确?有无断章取义?
+2. 护城河分析是否有充分论据支撑?
+3. 估值逻辑是否自洽?安全边际计算是否合理?
+4. 是否遗漏了重要的风险因素?
+5. 结论是否过于乐观或悲观?
+6. 是否符合巴菲特的价值投资哲学?
+
+如果你的回答已经全面、客观、准确,请回复"无需改进"。
+"""
+
+BUFFETT_REFINE_PROMPT = """
+请根据投资委员会的反馈意见,改进你的巴菲特式投资分析报告:
+
+# 原始分析数据:
+{task}
+
+# 上一轮分析报告:
+{last_attempt}
+
+# 委员会反馈:
+{feedback}
+
+请提供一个改进后的、更加严谨和完整的投资分析报告。
+
+末尾必须标注:⚠️ 以上分析仅供参考,不构成投资建议。投资有风险,入市需谨慎。
+"""
+
+
+def _max_reflections_from_env() -> int:
+    """环境变量 BUFFETT_MAX_REFLECTIONS,默认 0(初稿后即结束,避免「报告已完却仍在调 LLM」)。"""
+    raw = os.getenv("BUFFETT_MAX_REFLECTIONS", "0").strip()
+    try:
+        return max(0, int(raw))
+    except ValueError:
+        return 0
+
+
+def create_advisor_agent(
+    llm: HelloAgentsLLM = None,
+    custom_prompts: dict = None,
+    max_reflections: Optional[int] = None,
+) -> ReflectionAgent:
+    """创建巴菲特评估Agent(仅限巴菲特评估界面调用)
+
+    Args:
+        llm: HelloAgentsLLM实例
+        custom_prompts: 自定义三阶段提示词
+        max_reflections: 最大反思迭代次数;为 None 时读取环境变量 BUFFETT_MAX_REFLECTIONS(默认 0)
+
+    Returns:
+        配置好的ReflectionAgent实例
+    """
+    if llm is None:
+        llm = _create_default_llm()
+
+    if max_reflections is None:
+        max_reflections = _max_reflections_from_env()
+
+    prompts = custom_prompts or {
+        "initial": BUFFETT_INITIAL_PROMPT,
+        "reflect": BUFFETT_REFLECT_PROMPT,
+        "refine": BUFFETT_REFINE_PROMPT,
+    }
+
+    agent = ReflectionAgent(
+        name="巴菲特评估Agent",
+        llm=llm,
+        system_prompt="你是一位精通巴菲特价值投资理念的资深投资顾问,擅长护城河分析和安全边际评估。",
+        config=Config(temperature=0.4, max_tokens=4096),
+        max_iterations=max_reflections,
+        custom_prompts=prompts,
+    )
+
+    return agent
+
+
+def evaluate_buffett_stream(
+    llm: HelloAgentsLLM = None,
+    stock_code: str = "",
+    stock_name: str = "",
+) -> Iterator[dict]:
+    """流式巴菲特评估 - 收集数据并通过ReflectionAgent生成评估报告
+
+    Args:
+        llm: HelloAgentsLLM实例
+        stock_code: 股票代码
+        stock_name: 股票名称
+
+    Yields:
+        dict: {"type": "meta"|"status"|"delta"|"done"|"error", "content": str}
+    """
+    if llm is None:
+        llm = _create_default_llm()
+
+    yield {"type": "meta", "stock_code": stock_code, "stock_name": stock_name}
+
+    # 收集分析所需数据
+    yield {"type": "status", "content": "正在获取分析数据..."}
+
+    try:
+        data_context = _collect_stock_data(stock_code, stock_name)
+    except Exception as e:
+        msg = f"数据获取失败: {e}"
+        yield {"type": "error", "message": msg, "content": msg}
+        return
+
+    yield {"type": "status", "content": f"数据获取完成,开始巴菲特式评估分析..."}
+
+    # 构建评估任务
+    task = f"""
+## 分析数据:
+{data_context}
+
+请对股票 {stock_name}({stock_code}) 进行巴菲特式价值投资分析。
+"""
+
+    # 创建Agent并使用流式运行
+    agent = create_advisor_agent(llm=llm)
+    agent.prompts["initial"] = BUFFETT_INITIAL_PROMPT.replace(
+        "{stock_code}", stock_code
+    ).replace("{stock_name}", stock_name).replace("{data_context}", data_context)
+
+    try:
+        for event in agent.stream_run(task, conversation_id=None):
+            if event.event_type == "status":
+                yield {"type": "status", "content": event.content}
+            elif event.event_type == "text":
+                chunk = event.content or ""
+                yield {"type": "delta", "text": chunk, "content": chunk}
+            elif event.event_type == "thought":
+                yield {"type": "thought", "content": event.content}
+            elif event.event_type == "done":
+                yield {"type": "done"}
+            elif event.event_type == "error":
+                msg = event.content or ""
+                yield {"type": "error", "message": msg, "content": msg}
+    except Exception as e:
+        msg = f"分析过程出错: {e}"
+        yield {"type": "error", "message": msg, "content": msg}
+
+
+def _collect_stock_data(stock_code: str, stock_name: str = "") -> str:
+    """收集股票分析所需数据"""
+    parts = []
+
+    try:
+        from app.services import market_service, news_service
+
+        # 行情数据
+        try:
+            quote = market_service.get_stock_quote(stock_code)
+            if quote and quote.get("success"):
+                parts.append(f"## 行情数据\n```json\n{_truncate(str(quote), 3000)}\n```")
+        except Exception:
+            parts.append("## 行情数据\n获取失败")
+
+        # 财务数据
+        try:
+            financial = market_service.get_stock_financial(stock_code)
+            if financial and financial.get("success"):
+                parts.append(f"## 财务数据\n```json\n{_truncate(str(financial), 4000)}\n```")
+        except Exception:
+            parts.append("## 财务数据\n获取失败")
+
+        # 公司概况
+        try:
+            profile = market_service.get_stock_profile(stock_code)
+            if profile and profile.get("success"):
+                parts.append(f"## 公司概况\n```json\n{_truncate(str(profile), 3000)}\n```")
+        except Exception:
+            parts.append("## 公司概况\n获取失败")
+
+        # 舆情数据
+        try:
+            sentiment = news_service.analyze_sentiment(stock_code)
+            if sentiment and sentiment.get("success"):
+                parts.append(f"## 舆情数据\n```json\n{_truncate(str(sentiment), 3000)}\n```")
+        except Exception:
+            parts.append("## 舆情数据\n获取失败")
+
+    except Exception as e:
+        parts.append(f"## 数据收集错误\n{str(e)}")
+
+    return "\n\n".join(parts) if parts else "暂无可用数据"
+
+
+def _truncate(text: str, max_len: int) -> str:
+    return truncate_at_natural_boundary(text or "", max_len, "...[已截断]")
+
+
+def _create_default_llm() -> HelloAgentsLLM:
+    model = os.getenv("LLM_MODEL_ID")
+    api_key = os.getenv("LLM_API_KEY")
+    base_url = os.getenv("LLM_BASE_URL")
+    provider = os.getenv("LLM_PROVIDER", "auto")
+
+    if not api_key:
+        raise RuntimeError("LLM_API_KEY 环境变量未设置")
+
+    raw_timeout = int(os.getenv("LLM_TIMEOUT", "60"))
+    buffett_timeout = max(raw_timeout, 180)
+
+    return HelloAgentsLLM(
+        model=model,
+        api_key=api_key,
+        base_url=base_url,
+        provider=provider,
+        temperature=0.4,
+        max_tokens=6144,
+        timeout=buffett_timeout,
+    )

+ 276 - 0
Co-creation-projects/lcyting-StockSage-agent/agents/agent_system.py

@@ -0,0 +1,276 @@
+"""
+智能股票分析助手 — 智能体系统(统一Agent管理与流式调度)
+
+基于 HelloAgents Optimized 框架,管理所有专业Agent的生命周期,
+提供统一的流式分析接口供后端API调用。
+"""
+
+import sys
+import os
+import threading
+from pathlib import Path
+from typing import AsyncIterator, Optional
+
+
+def _coord_answer_cap_hint(max_chars: int) -> str:
+    return (
+        f"\n\n【硬性要求】最终答案全文不得超过约 {max_chars} 个汉字(含标点),"
+        "分条简练,禁止复述工具返回的全文或大段粘贴。"
+    )
+
+
+def _apply_answer_cap(text: str, max_chars: Optional[int]) -> str:
+    if max_chars is None or max_chars <= 0:
+        return text or ""
+    from .text_truncation import truncate_at_natural_boundary
+
+    t = (text or "").strip()
+    if len(t) <= max_chars:
+        return t
+    return truncate_at_natural_boundary(t, max_chars, "\n\n…(已达字数上限)")
+
+_HELLO_AGENTS_PATH = Path(__file__).parent.parent / "HelloAgents Optimized"
+if str(_HELLO_AGENTS_PATH) not in sys.path:
+    sys.path.insert(0, str(_HELLO_AGENTS_PATH))
+
+_SKILLS_PATH = Path(__file__).parent.parent / "skills"
+if str(_SKILLS_PATH) not in sys.path:
+    sys.path.insert(0, str(_SKILLS_PATH))
+
+_BACKEND_PATH = Path(__file__).parent.parent / "backend"
+if str(_BACKEND_PATH) not in sys.path:
+    sys.path.insert(0, str(_BACKEND_PATH))
+
+from hello_agents.core.llm import HelloAgentsLLM
+from hello_agents.core.config import Config
+
+_agent_lock = threading.Lock()
+_agent_system_instance: Optional["AgentSystem"] = None
+
+
+class AgentSystem:
+    """智能体系统 — 统一管理所有Agent并提供流式分析接口"""
+
+    def __init__(self):
+        self._llm: Optional[HelloAgentsLLM] = None
+        self._advisor = None         # 巴菲特评估 Agent (Reflection)
+        self._sentiment = None       # 舆情分析 Agent (ReAct)
+        self._data_analysis = None   # 数据分析 Agent (ReAct)
+        self._general_advisor = None # 普通投资顾问 Agent
+        self._initialized = False
+
+    def _ensure_llm(self) -> HelloAgentsLLM:
+        if self._llm is None:
+            self._llm = _create_default_llm()
+        return self._llm
+
+    def _get_api_key(self) -> Optional[str]:
+        key = os.getenv("MX_APIKEY", "").strip()
+        if key and key != "your-mx-apikey-here":
+            return key
+        try:
+            from app.config import settings
+            return settings.MX_APIKEY or None
+        except Exception:
+            return None
+
+    # ---- 巴菲特评估 Agent ----
+
+    def get_advisor_agent(self):
+        """获取巴菲特评估 Agent(仅限巴菲特评估界面调用)"""
+        if self._advisor is None:
+            from agents.advisor_agent import create_advisor_agent
+            self._advisor = create_advisor_agent(llm=self._ensure_llm())
+        return self._advisor
+
+    def evaluate_buffett_stream(self, stock_code: str, stock_name: str = ""):
+        """流式巴菲特评估 - 通过 advisor_agent 生成评估报告"""
+        from agents.advisor_agent import evaluate_buffett_stream
+        yield from evaluate_buffett_stream(
+            llm=self._ensure_llm(),
+            stock_code=stock_code,
+            stock_name=stock_name,
+        )
+
+    # ---- 舆情分析 Agent ----
+
+    def get_sentiment_agent(self):
+        """获取舆情分析 Agent"""
+        if self._sentiment is None:
+            from agents.sentiment_agent import create_sentiment_agent
+            self._sentiment = create_sentiment_agent(
+                api_key=self._get_api_key(),
+                llm=self._ensure_llm(),
+            )
+        return self._sentiment
+
+    def run_sentiment(
+        self,
+        stock_code: str,
+        stock_name: str = "",
+        *,
+        max_answer_chars: Optional[int] = None,
+    ) -> str:
+        """非流式舆情分析 — 返回完整文本,供协调者Agent内部调用"""
+        agent = self.get_sentiment_agent()
+        stock_label = f"{stock_name}({stock_code})" if stock_name else stock_code
+        task = f"请搜索并分析股票 {stock_label} 的最新金融资讯、研究报告和公告,判断市场舆情趋势。"
+        if max_answer_chars:
+            task += _coord_answer_cap_hint(max_answer_chars)
+        try:
+            out = (agent.run(task) or "").strip()
+            if not out:
+                return (
+                    "[舆情分析未生成有效正文:可能因网络/超时或模型提前结束。"
+                    "建议在个股页使用「AI舆情分析」流式重试,或在 .env 将 LLM_TIMEOUT 调至 300 后重启后端。]"
+                )
+            return _apply_answer_cap(out, max_answer_chars)
+        except Exception as e:
+            return f"[舆情分析失败: {e}]"
+
+    def analyze_sentiment_stream(self, stock_code: str, stock_name: str = ""):
+        """流式舆情分析"""
+        from agents.sentiment_agent import analyze_sentiment_stream
+        yield from analyze_sentiment_stream(
+            agent=self.get_sentiment_agent(),
+            stock_code=stock_code,
+            stock_name=stock_name,
+        )
+
+    # ---- 数据分析 Agent ----
+
+    def get_data_analysis_agent(self):
+        """获取数据分析 Agent"""
+        if self._data_analysis is None:
+            from agents.data_analysis_agent import create_data_analysis_agent
+            self._data_analysis = create_data_analysis_agent(
+                api_key=self._get_api_key(),
+                llm=self._ensure_llm(),
+            )
+        return self._data_analysis
+
+    def run_data_analysis(
+        self,
+        stock_code: str,
+        stock_name: str = "",
+        *,
+        max_answer_chars: Optional[int] = None,
+    ) -> str:
+        """非流式数据分析 — 返回完整文本,供协调者Agent内部调用"""
+        agent = self.get_data_analysis_agent()
+        stock_label = f"{stock_name}({stock_code})" if stock_name else stock_code
+        task = f"""请查询股票 {stock_label} 的以下数据并进行综合分析:
+1. 实时行情(价格、涨跌幅、成交量、换手率等)
+2. 核心财务指标(ROE、净利润、营收增长率、毛利率等)
+3. 估值水平(市盈率、市净率、股息率等)
+4. 公司基本概况
+
+请给出专业的数据分析报告。"""
+        if max_answer_chars:
+            task += _coord_answer_cap_hint(max_answer_chars)
+        try:
+            out = (agent.run(task) or "").strip()
+            if not out:
+                return (
+                    "[数据分析未生成有效正文:可能因网络/超时或模型提前结束。"
+                    "建议使用个股页「AI数据分析」流式重试,或在 .env 将 LLM_TIMEOUT 调至 300 后重启后端。]"
+                )
+            return _apply_answer_cap(out, max_answer_chars)
+        except Exception as e:
+            return f"[数据分析失败: {e}]"
+
+    def analyze_data_stream(self, stock_code: str, stock_name: str = ""):
+        """流式数据分析"""
+        from agents.data_analysis_agent import analyze_data_stream
+        yield from analyze_data_stream(
+            agent=self.get_data_analysis_agent(),
+            stock_code=stock_code,
+            stock_name=stock_name,
+        )
+
+    # ---- 普通投资顾问 Agent ----
+
+    def get_general_advisor_agent(self):
+        """获取普通投资顾问 Agent"""
+        if self._general_advisor is None:
+            from agents.general_advisor_agent import create_general_advisor_agent
+            self._general_advisor = create_general_advisor_agent(
+                llm=self._ensure_llm(),
+            )
+        return self._general_advisor
+
+    def run_advisor(
+        self,
+        task: str,
+        *,
+        max_answer_chars: Optional[int] = None,
+    ) -> str:
+        """非流式投资建议 — 返回完整文本,供协调者Agent内部调用"""
+        agent = self.get_general_advisor_agent()
+        if max_answer_chars:
+            task = task + _coord_answer_cap_hint(max_answer_chars)
+        try:
+            out = (agent.run(task) or "").strip()
+            return _apply_answer_cap(out, max_answer_chars)
+        except Exception as e:
+            return f"[投资分析失败: {e}]"
+
+    # ---- AI 对话助手(协调者)----
+
+    def chat_stream(self, user_message: str, history: list = None):
+        """AI对话助手流式接口 - 协调者Agent解析用户需求并调度子Agent"""
+        from agents.coordinator_agent import coordinator_chat_stream
+        yield from coordinator_chat_stream(
+            llm=self._ensure_llm(),
+            user_message=user_message,
+            history=history or [],
+            agent_system=self,
+        )
+
+    # ---- 健康检查 ----
+
+    def is_ready(self) -> bool:
+        try:
+            self._ensure_llm()
+            return True
+        except Exception:
+            return False
+
+
+def _create_default_llm() -> HelloAgentsLLM:
+    model = os.getenv("LLM_MODEL_ID")
+    api_key = os.getenv("LLM_API_KEY")
+    base_url = os.getenv("LLM_BASE_URL")
+    provider = os.getenv("LLM_PROVIDER", "auto")
+
+    if not api_key:
+        raise RuntimeError("LLM_API_KEY 环境变量未设置")
+
+    try:
+        from app.config import settings
+
+        raw_timeout = int(settings.LLM_TIMEOUT)
+    except Exception:
+        raw_timeout = int(os.getenv("LLM_TIMEOUT", "60"))
+    # ReAct 多轮 + 工具调用 + 协调者多 Agent 串联,默认 60s 极易中途超时
+    timeout = max(raw_timeout, 180)
+
+    return HelloAgentsLLM(
+        model=model,
+        api_key=api_key,
+        base_url=base_url,
+        provider=provider,
+        temperature=0.3,
+        max_tokens=8192,
+        timeout=timeout,
+    )
+
+
+def get_agent_system() -> AgentSystem:
+    """获取 AgentSystem 全局单例"""
+    global _agent_system_instance
+    if _agent_system_instance is None:
+        with _agent_lock:
+            if _agent_system_instance is None:
+                _agent_system_instance = AgentSystem()
+    return _agent_system_instance

+ 358 - 0
Co-creation-projects/lcyting-StockSage-agent/agents/coordinator_agent.py

@@ -0,0 +1,358 @@
+"""
+智能股票分析助手 — AI对话助手(协调者Agent)
+
+解析用户需求,**智能选择需要调用的子Agent**(不一定全调),
+每个子Agent**非流式执行**,协调者收集全部输出后:
+1. 分析哪些输出应传递给其他子Agent(如投资顾问需要数据+舆情)
+2. 将子Agent输出总结后以流式输出给用户
+"""
+
+import sys
+import os
+import re
+from pathlib import Path
+from typing import Iterator, Optional
+
+_PROJECT_ROOT = Path(__file__).parent.parent
+_HELLO_PATH = _PROJECT_ROOT / "HelloAgents Optimized"
+_BACKEND_DIR = _PROJECT_ROOT / "backend"
+for p in [_HELLO_PATH, _BACKEND_DIR]:
+    if str(p) not in sys.path:
+        sys.path.insert(0, str(p))
+
+from hello_agents.core.llm import HelloAgentsLLM
+
+from .text_truncation import truncate_at_natural_boundary
+
+# 协调者调子 Agent 时的输出上限(任务提示 + 硬截断)
+COORD_DATA_MAX_CHARS = 2200
+COORD_SENTIMENT_MAX_CHARS = 2200
+COORD_ADVISOR_MAX_CHARS = 1600
+COORD_MERGE_SECTION_MAX_CHARS = 2600
+COORD_MERGE_OUTPUT_CHARS_HINT = 1000
+# 整合步骤流式输出的 API max_tokens 上限(汉语约 1 token≈1~2 字,用于抑制冗长)
+COORD_MERGE_MAX_TOKENS = 1400
+
+# 路由:必须先由 LLM 决定是否调用子 Agent
+AGENT_SELECTION_PROMPT = """你是金融分析系统的「路由调度器」,只做一件事:判断是否需要调用后台分析 Agent。
+
+可调用的模块(小写英文关键字):
+- data:仅当用户明确需要行情、财务、估值、基本面数据时
+- sentiment:仅当用户明确需要新闻、舆情、公告、研报、市场情绪时
+- advisor:仅当用户明确要投资建议、买卖参考、仓位建议时
+
+规则(必读):
+1. 寒暄、科普、与个股无关的问题 → 只回复 none
+2. 用户未给出股票代码/名称且无法推断 → 只回复 none
+3. 用户仅问单一维度 → 只选一个关键字,不要贪多
+4. 不要输出解释、Markdown、引号包裹;不要输出未列出的词
+
+只能输出下面八种形式之一(整行,无其它字符):
+none
+data
+sentiment
+advisor
+data,sentiment
+data,advisor
+sentiment,advisor
+data,sentiment,advisor
+
+用户输入:
+{message}
+
+你的输出(单行):"""
+
+
+def coordinator_chat_stream(
+    llm: HelloAgentsLLM,
+    user_message: str,
+    history: list,
+    agent_system,
+) -> Iterator[dict]:
+    """
+    AI对话助手流式接口
+
+    流程:
+    1. 解析意图,智能选择需调用的子Agent
+    2. 非流式调用选取的子Agent,收集完整输出
+    3. 若需投资顾问,将其它子Agent输出作为输入传递
+    4. 协调者整理所有输出,流式返回给用户
+    """
+    yield {"type": "thinking", "content": "正在分析您的问题...\n"}
+
+    stock_info = _extract_stock_info(user_message, history)
+
+    # Step 1:仅一次 LLM 调用 — 决定是否启用子 Agent(避免无谓的全链路透传)
+    yield {"type": "status", "content": "路由决策:正在判断是否调用分析引擎...\n"}
+    agents_to_call = _select_agents(llm, user_message)
+    yield {"type": "status", "content": f"路由结果: {_agents_label(agents_to_call)}\n"}
+
+    if not agents_to_call:
+        yield from _handle_general(llm, user_message, history, agent_system, stock_info)
+        yield {"type": "done"}
+        return
+
+    code = stock_info.get("code", "")
+    name = stock_info.get("name", "")
+
+    if not code:
+        yield {"type": "thinking", "content": "请提供具体的股票代码或名称,我可以为您做更精准的分析。"}
+        yield {"type": "done"}
+        return
+
+    # Step 2:按路由顺序调用子 Agent(带字数上限,防止报告无限拉长)
+    agent_results = {}
+    yield {"type": "status", "content": "正在依次调用分析引擎(带输出篇幅限制)...\n"}
+
+    if "data" in agents_to_call:
+        yield {"type": "thinking", "content": "> 正在查询行情与财务数据...\n"}
+        agent_results["data"] = agent_system.run_data_analysis(
+            code, name, max_answer_chars=COORD_DATA_MAX_CHARS
+        )
+        yield {"type": "status", "content": "数据分析完成\n"}
+
+    if "sentiment" in agents_to_call:
+        yield {"type": "thinking", "content": "> 正在搜索资讯与分析舆情...\n"}
+        agent_results["sentiment"] = agent_system.run_sentiment(
+            code, name, max_answer_chars=COORD_SENTIMENT_MAX_CHARS
+        )
+        yield {"type": "status", "content": "舆情分析完成\n"}
+
+    # Step 3: 若需要投资顾问,将数据+舆情结果传递给它
+    if "advisor" in agents_to_call:
+        yield {"type": "thinking", "content": "> 正在整合数据与舆情,生成投资建议...\n"}
+        advisor_input = _build_advisor_input(agent_results, code, name)
+        agent_results["advisor"] = agent_system.run_advisor(
+            advisor_input, max_answer_chars=COORD_ADVISOR_MAX_CHARS
+        )
+        yield {"type": "status", "content": "投资分析完成\n"}
+
+    # Step 4: 协调者整理输出,流式返回给用户
+    yield {"type": "status", "content": "\n---\n"}
+    yield from _stream_aggregated_response(llm, user_message, agent_results, agents_to_call, code, name)
+
+    if len(agent_results) > 1:
+        yield {
+            "type": "summary",
+            "content": "以上为各分析引擎输出的整合结果,仅供参考,不构成投资建议。",
+        }
+    elif agent_results:
+        yield {"type": "summary", "content": "分析已完成,仅供参考,不构成投资建议。"}
+    yield {"type": "done"}
+
+
+def _parse_route_line(raw: str) -> list[str]:
+    """解析路由 LLM 输出为有序、去重的 agent 列表。"""
+    if not raw:
+        return []
+    line = raw.strip().splitlines()[0].strip()
+    line = re.sub(r"^[`\s]+|[`\s]+$", "", line)
+    line = line.lower()
+    if line.startswith("```"):
+        line = re.sub(r"^```\w*", "", line).strip("`").strip()
+    # 去掉常见前缀
+    for prefix in ("输出:", "输出:", "answer:", "agents:", "列表:", "列表:"):
+        if line.startswith(prefix):
+            line = line[len(prefix) :].strip()
+    tokens = [t.strip() for t in re.split(r"[,,;\s|]+", line) if t.strip()]
+    order = ("data", "sentiment", "advisor")
+    seen = set()
+    out: list[str] = []
+    for t in tokens:
+        if t == "none":
+            return []
+        if t in order and t not in seen:
+            seen.add(t)
+            out.append(t)
+    return out
+
+
+def _select_agents(llm: HelloAgentsLLM, message: str) -> list[str]:
+    """单次 LLM 调用:决定是否调用子 Agent。"""
+    try:
+        prompt = AGENT_SELECTION_PROMPT.format(message=message)
+        result = llm.invoke(
+            [
+                {
+                    "role": "system",
+                    "content": "你只输出路由关键字行,禁止开场白与解释。",
+                },
+                {"role": "user", "content": prompt},
+            ],
+            max_tokens=48,
+            temperature=0,
+        )
+        parsed = _parse_route_line(result or "")
+        if parsed:
+            return parsed
+    except Exception:
+        pass
+
+    # 兜底:关键词(保守:尽量不触发全链路)
+    if any(kw in message for kw in ["新闻", "舆情", "情绪", "资讯", "公告", "研报"]):
+        return ["sentiment"]
+    if any(kw in message for kw in ["财务", "营收", "利润", "ROE", "PE", "估值", "行情", "价格", "涨跌"]):
+        return ["data"]
+    if any(kw in message for kw in ["建议", "推荐", "买卖", "买入", "卖出", "投资建议"]):
+        return ["advisor"]
+    return []
+
+
+def _build_advisor_input(agent_results: dict, code: str, name: str) -> str:
+    """将其他子Agent的输出整合为投资顾问的输入"""
+    cap = COORD_MERGE_SECTION_MAX_CHARS
+    parts = [
+        f"请对股票 {name}({code}) 进行综合投资分析,以下是参考数据(可能已截断):\n"
+    ]
+
+    if "data" in agent_results:
+        data_text = agent_results["data"]
+        parts.append(f"## 数据分析结果\n{data_text[:cap]}\n")
+
+    if "sentiment" in agent_results:
+        sent_text = agent_results["sentiment"]
+        parts.append(f"## 舆情分析结果\n{sent_text[:cap]}\n")
+
+    parts.append(
+        "请根据以上数据给出投资建议:核心观点、简要逻辑、风险提示;表述简练。"
+    )
+    return "\n".join(parts)
+
+
+def _stream_aggregated_response(
+    llm: HelloAgentsLLM,
+    message: str,
+    agent_results: dict,
+    agents_to_call: list[str],
+    code: str,
+    name: str,
+) -> Iterator[dict]:
+    """协调者将子Agent输出总结后流式输出"""
+
+    # 如果只有一个Agent,直接输出其结果(仍遵守该 Agent 的字数上限)
+    if len(agents_to_call) == 1 and len(agent_results) == 1:
+        key = agents_to_call[0]
+        limit = {
+            "data": COORD_DATA_MAX_CHARS,
+            "sentiment": COORD_SENTIMENT_MAX_CHARS,
+            "advisor": COORD_ADVISOR_MAX_CHARS,
+        }.get(key, COORD_DATA_MAX_CHARS)
+        result_text = list(agent_results.values())[0]
+        yield {"type": "delta", "content": _hard_cap_text(result_text, limit)}
+        return
+
+    # 多个Agent:用LLM整合
+    stock_label = f"{name}({code})"
+
+    summary_prompt = f"""用户问题: {message}
+
+以下是各分析Agent针对 {stock_label} 的输出结果,请整合为一份清晰的回答:
+
+"""
+    for agent_type, text in agent_results.items():
+        label_map = {"data": "数据分析", "sentiment": "舆情分析", "advisor": "投资建议"}
+        label = label_map.get(agent_type, agent_type)
+        body = (text or "").strip()
+        if not body:
+            body = "(该维度无输出,可能超时或未调用成功。)"
+        body = _hard_cap_text(body, COORD_MERGE_SECTION_MAX_CHARS)
+        summary_prompt += f"\n## {label}结果\n{body}\n"
+
+    summary_prompt += f"""
+请整合以上结果,用以下结构输出(全文总字数控制在约 {COORD_MERGE_OUTPUT_CHARS_HINT} 个汉字以内,禁止复述原文大段):
+1. 核心发现(2-3句)
+2. 关键依据(每维度各 2-4 条要点)
+3. 综合建议
+4. 风险提示
+"""
+
+    try:
+        messages = [
+            {
+                "role": "system",
+                "content": (
+                    "你是金融分析总结助手。输出务必简练,总篇幅控制在约 "
+                    f"{COORD_MERGE_OUTPUT_CHARS_HINT} 个汉字内,避免堆砌重复。"
+                ),
+            },
+            {"role": "user", "content": summary_prompt},
+        ]
+        for chunk in llm.stream_invoke(
+            messages,
+            max_tokens=COORD_MERGE_MAX_TOKENS,
+            temperature=0.2,
+        ):
+            if chunk:
+                yield {"type": "delta", "content": chunk}
+    except Exception:
+        # 兜底:直接拼接所有结果
+        for agent_type, text in agent_results.items():
+            label_map = {"data": "数据分析", "sentiment": "舆情分析", "advisor": "投资建议"}
+            label = label_map.get(agent_type, agent_type)
+            capped = _hard_cap_text(text or "", COORD_MERGE_SECTION_MAX_CHARS)
+            yield {"type": "delta", "content": f"\n## {label}\n{capped}\n"}
+
+
+def _handle_general(
+    llm: HelloAgentsLLM,
+    message: str,
+    history: list,
+    agent_system,
+    stock_info: dict,
+) -> Iterator[dict]:
+    """处理一般对话"""
+    code = stock_info.get("code", "")
+    name = stock_info.get("name", "")
+
+    # 如果提到了股票但没有明确分析需求,给出引导
+    if code or name:
+        stock_label = f"{name}({code})" if name else code
+        yield {"type": "emotional", "content": f"我看到您提到了 {stock_label}。\n\n"}
+        yield {"type": "delta", "content": "我可以为您:\n- 分析该股票的行情与财务数据\n- 查看市场舆情与资讯\n- 给出综合投资建议\n\n请告诉我想了解哪个方面?"}
+
+    try:
+        messages = [{"role": "system", "content": "你是一个友好的AI股票分析助手。请用简洁专业的语言回复用户,引导用户提出具体的分析需求。"}]
+        for h in history[-6:]:
+            messages.append(h)
+        messages.append({"role": "user", "content": message})
+
+        for chunk in llm.stream_invoke(messages):
+            if chunk:
+                yield {"type": "delta", "content": chunk}
+    except Exception:
+        yield {"type": "delta", "content": "您可以问我:分析某只股票、查看市场舆情、获取投资建议等。请提供具体的股票代码。"}
+
+
+def _extract_stock_info(message: str, history: list) -> dict:
+    """从消息中提取股票信息"""
+    info = {"code": "", "name": ""}
+
+    code_match = re.search(r'[6|0|3]\d{5}', message)
+    if code_match:
+        info["code"] = code_match.group()
+
+    name_patterns = [r'分析一下(\S+)', r'(贵州茅台|比亚迪|宁德时代|招商银行|中国平安|五粮液)']
+    for pattern in name_patterns:
+        name_match = re.search(pattern, message)
+        if name_match:
+            info["name"] = name_match.group(1)
+            break
+
+    return info
+
+
+def _agents_label(agents: list[str]) -> str:
+    labels = {"data": "数据分析", "sentiment": "舆情分析", "advisor": "投资顾问"}
+    return " + ".join(labels.get(a, a) for a in agents) if agents else "无需调用Agent"
+
+
+def _hard_cap_text(text: str, max_chars: int) -> str:
+    """截断过长文本(优先段落/句号),防止下游模型上下文膨胀。"""
+    if max_chars <= 0 or not text:
+        return text or ""
+    t = text.strip()
+    if len(t) <= max_chars:
+        return t
+    return truncate_at_natural_boundary(
+        t, max_chars, "\n\n…(已达协调者字数上限,略去后续)"
+    )

+ 179 - 0
Co-creation-projects/lcyting-StockSage-agent/agents/data_analysis_agent.py

@@ -0,0 +1,179 @@
+"""
+智能股票分析助手 — 数据分析Agent
+
+基于 HelloAgents ReActAgent,使用 mx_data 工具查询行情和财务数据,
+执行基本面和技术面的深度分析。
+
+使用方式:
+    from agents.data_analysis_agent import create_data_analysis_agent
+
+    agent = create_data_analysis_agent(api_key="...", llm=llm)
+    result = agent.run("分析贵州茅台的财务指标和估值水平")
+"""
+
+import sys
+from pathlib import Path
+
+_PROJECT_ROOT = Path(__file__).parent.parent
+_HELLO_PATH = _PROJECT_ROOT / "HelloAgents Optimized"
+_AGENTS_DIR = _PROJECT_ROOT / "agents"
+_BACKEND_DIR = _PROJECT_ROOT / "backend"
+_SKILLS_DATA = _PROJECT_ROOT / "skills" / "金融数据" / "mx-data"
+
+for p in [_HELLO_PATH, _AGENTS_DIR, _BACKEND_DIR, _SKILLS_DATA]:
+    if str(p) not in sys.path:
+        sys.path.insert(0, str(p))
+
+from typing import Iterator
+
+from hello_agents.tools import ToolRegistry
+from hello_agents.agents.react_agent import ReActAgent
+from hello_agents.core.llm import HelloAgentsLLM
+from hello_agents.core.config import Config
+from hello_agents.core.stream import StreamEvent
+
+from agents.tools.mx_data_tool import MXDataTool
+
+# 数据分析Agent系统提示词
+DATA_ANALYSIS_PROMPT = """你是一位专业的A股数据分析师,精通基本面分析和技术面分析。
+
+## 你的职责
+1. 查询目标股票的实时行情数据(价格、涨跌幅、成交量、换手率等)
+2. 分析核心财务指标(ROE、净利润、营收增长率、毛利率等)
+3. 评估估值水平(市盈率、市净率、股息率等)
+4. 查看公司基本概况(主营业务、行业地位、高管等)
+5. 结合多维数据给出基本面和技术面的综合评估
+
+## 分析框架
+- **盈利能力**: ROE、净利润率、毛利率、营收增长率
+- **估值水平**: PE、PB、PS、PEG
+- **成长性**: 近3年营收/利润复合增长率
+- **现金流**: 经营现金流/净利润比率
+- **分红**: 股息率、分红率、分红稳定性
+
+## 输出格式
+分析结果应包含:
+1. **行情快照**:最新价格、涨跌幅、成交情况
+2. **财务健康度**:核心指标雷达图式评估
+3. **估值分位**:当前估值在历史区间的位置
+4. **亮点与风险**:2-3个关键发现
+
+## 重要提醒
+- 数据优先,所有结论需有查询数据支撑
+- 历史对比时标明时间区间
+- 末尾标注免责声明
+
+## ReAct 回合输出(与框架对接,必须遵守)
+每一轮模型回复都必须同时包含下面两行(缺一不可),禁止只输出 Markdown 报告正文而不写 Action:
+- `Thought:` 一行简明推理(可与中文「思考:」择一,但推荐英文标签)
+- `Action:` 要么是 `mx_data[查询指令]`,要么是 **最终结论** `Finish[完整分析报告正文]`
+
+准备输出最终报告时,也必须使用 `Action: Finish[...]` 包裹全文;`[` 与结尾 `]` 必须成对,报告内如需括号请避免单独的 `]` 破坏配对,或把长报告压缩为不含裸 `]` 的表述。
+"""
+
+
+def create_data_analysis_agent(
+    api_key: str = None,
+    llm: HelloAgentsLLM = None,
+    system_prompt: str = None,
+    max_steps: int = 8,
+) -> ReActAgent:
+    """创建数据分析Agent
+
+    Args:
+        api_key: 东方财富MX_APIKEY
+        llm: HelloAgentsLLM实例(必需)
+        system_prompt: 自定义系统提示词(可选)
+        max_steps: 最大推理步数,默认8(数据分析需要多步查询)
+
+    Returns:
+        配置好的ReActAgent实例
+    """
+    if llm is None:
+        llm = _create_default_llm()
+
+    registry = ToolRegistry()
+    data_tool = MXDataTool(api_key=api_key)
+    registry.register_tool(data_tool)
+
+    prompt = system_prompt or DATA_ANALYSIS_PROMPT
+
+    agent = ReActAgent(
+        name="数据分析Agent",
+        llm=llm,
+        tool_registry=registry,
+        system_prompt=prompt,
+        config=Config(temperature=0.2, max_tokens=4096),
+        max_steps=max_steps,
+    )
+
+    return agent
+
+
+def _create_default_llm() -> HelloAgentsLLM:
+    """从环境变量创建默认LLM实例"""
+    import os
+
+    model = os.getenv("LLM_MODEL_ID")
+    api_key = os.getenv("LLM_API_KEY")
+    base_url = os.getenv("LLM_BASE_URL")
+    provider = os.getenv("LLM_PROVIDER", "auto")
+
+    if not api_key:
+        raise RuntimeError(
+            "LLM_API_KEY 环境变量未设置,请先设置环境变量:\n"
+            "export LLM_API_KEY=your_llm_api_key_here\n"
+            "或在创建Agent时传入 llm 参数"
+        )
+
+    return HelloAgentsLLM(
+        model=model,
+        api_key=api_key,
+        base_url=base_url,
+        provider=provider,
+        temperature=0.2,
+    )
+
+
+def analyze_data_stream(
+    agent: ReActAgent,
+    stock_code: str = "",
+    stock_name: str = "",
+) -> Iterator[dict]:
+    """流式数据分析 - 通过ReActAgent查询行情和财务数据并分析
+
+    Args:
+        agent: 已配置的数据分析Agent
+        stock_code: 股票代码
+        stock_name: 股票名称
+
+    Yields:
+        dict: {"type": "meta"|"status"|"delta"|"done"|"error", "content": str}
+    """
+    stock_label = f"{stock_name}({stock_code})" if stock_name else stock_code
+
+    yield {"type": "meta", "stock_code": stock_code, "stock_name": stock_name}
+    yield {"type": "status", "content": f"正在查询 {stock_label} 的行情和财务数据..."}
+
+    task = f"""请查询股票 {stock_label} 的以下数据并进行综合分析:
+1. 实时行情(价格、涨跌幅、成交量、换手率等)
+2. 核心财务指标(ROE、净利润、营收增长率、毛利率等)
+3. 估值水平(市盈率、市净率、股息率等)
+4. 公司基本概况
+
+请给出专业的数据分析报告。"""
+
+    try:
+        for event in agent.stream_run(task):
+            if event.event_type == "status":
+                yield {"type": "status", "content": event.content}
+            elif event.event_type in ("text", "observation"):
+                yield {"type": "delta", "content": event.content}
+            elif event.event_type == "tool_call":
+                yield {"type": "status", "content": f"正在查询: {event.metadata.get('tool_name', '')}"}
+            elif event.event_type == "done":
+                yield {"type": "done"}
+            elif event.event_type == "error":
+                yield {"type": "error", "content": event.content}
+    except Exception as e:
+        yield {"type": "error", "content": f"数据分析出错: {e}"}

+ 127 - 0
Co-creation-projects/lcyting-StockSage-agent/agents/general_advisor_agent.py

@@ -0,0 +1,127 @@
+"""
+智能股票分析助手 — 普通投资顾问Agent
+
+基于 HelloAgents ReActAgent,根据提供的数据进行综合分析并给出投资建议。
+允许协调者Agent调用,支持流式输出。
+"""
+
+import sys
+import os
+from pathlib import Path
+from typing import Iterator
+
+_PROJECT_ROOT = Path(__file__).parent.parent
+_HELLO_PATH = _PROJECT_ROOT / "HelloAgents Optimized"
+for p in [_HELLO_PATH]:
+    if str(p) not in sys.path:
+        sys.path.insert(0, str(p))
+
+from hello_agents.agents.react_agent import ReActAgent
+from hello_agents.tools import ToolRegistry
+from hello_agents.core.llm import HelloAgentsLLM
+from hello_agents.core.config import Config
+from hello_agents.core.stream import StreamEvent
+
+GENERAL_ADVISOR_PROMPT = """你是一位专业的A股投资顾问,擅长综合技术和基本面分析。
+
+## 你的职责
+1. 根据提供的数据进行综合分析
+2. 给出客观、专业的投资建议
+3. 明确标注风险因素
+4. 提供参考但不构成投资建议
+
+## 分析维度
+- **技术面**: 价格走势、成交量、技术指标
+- **基本面**: 财务指标、估值水平、行业地位
+- **市场情绪**: 舆情倾向、资金流向
+- **风险提示**: 政策风险、市场风险、行业风险
+
+## 输出格式
+1. **核心观点**:一句话总结
+2. **分析逻辑**:2-3个关键支撑论据
+3. **风险提示**:最重要的风险因素
+4. **免责声明**:以上分析仅供参考
+
+## 重要提醒
+- 保持客观中立,不夸大也不隐瞒风险
+- 末尾必须标注免责声明
+"""
+
+
+def create_general_advisor_agent(
+    llm: HelloAgentsLLM = None,
+    system_prompt: str = None,
+    max_steps: int = 5,
+) -> ReActAgent:
+    """创建普通投资顾问Agent
+
+    Args:
+        llm: HelloAgentsLLM实例
+        system_prompt: 自定义系统提示词
+        max_steps: 最大推理步数
+
+    Returns:
+        配置好的ReActAgent实例
+    """
+    if llm is None:
+        llm = _create_default_llm()
+
+    registry = ToolRegistry()
+
+    prompt = system_prompt or GENERAL_ADVISOR_PROMPT
+
+    agent = ReActAgent(
+        name="投资顾问Agent",
+        llm=llm,
+        tool_registry=registry,
+        system_prompt=prompt,
+        config=Config(temperature=0.35, max_tokens=4096),
+        max_steps=max_steps,
+    )
+
+    return agent
+
+
+def advise_stream(
+    agent: ReActAgent,
+    task: str,
+) -> Iterator[dict]:
+    """流式投资建议 - 供协调者Agent调用
+
+    Args:
+        agent: 已配置的投资顾问Agent
+        task: 分析任务和数据
+
+    Yields:
+        dict: {"type": "status"|"delta"|"done"|"error", "content": str}
+    """
+    if agent is None:
+        yield {"type": "error", "content": "投资顾问Agent未初始化"}
+        return
+
+    yield {"type": "status", "content": "投资顾问正在分析..."}
+
+    try:
+        result = agent.run(task)
+        yield {"type": "delta", "content": result}
+        yield {"type": "done"}
+    except Exception as e:
+        yield {"type": "error", "content": f"投资分析出错: {e}"}
+
+
+def _create_default_llm() -> HelloAgentsLLM:
+    model = os.getenv("LLM_MODEL_ID")
+    api_key = os.getenv("LLM_API_KEY")
+    base_url = os.getenv("LLM_BASE_URL")
+    provider = os.getenv("LLM_PROVIDER", "auto")
+
+    if not api_key:
+        raise RuntimeError("LLM_API_KEY 环境变量未设置")
+
+    return HelloAgentsLLM(
+        model=model,
+        api_key=api_key,
+        base_url=base_url,
+        provider=provider,
+        temperature=0.35,
+    )

+ 175 - 0
Co-creation-projects/lcyting-StockSage-agent/agents/sentiment_agent.py

@@ -0,0 +1,175 @@
+"""
+智能股票分析助手 — 舆情分析Agent
+
+基于 HelloAgents ReActAgent,使用 mx-search 工具搜索金融资讯,
+并结合LLM进行情感倾向分析和舆情研判。
+
+使用方式:
+    from agents.sentiment_agent import create_sentiment_agent
+
+    agent = create_sentiment_agent(api_key="...", llm=llm)
+    result = agent.run("分析贵州茅台的舆情情况")
+"""
+
+import sys
+from pathlib import Path
+
+# 将框架路径加入sys.path
+_PROJECT_ROOT = Path(__file__).parent.parent
+_HELLO_PATH = _PROJECT_ROOT / "HelloAgents Optimized"
+_AGENTS_DIR = _PROJECT_ROOT / "agents"
+_BACKEND_DIR = _PROJECT_ROOT / "backend"
+_SKILLS_SEARCH = _PROJECT_ROOT / "skills" / "资讯搜索" / "mx-search"
+
+for p in [_HELLO_PATH, _AGENTS_DIR, _BACKEND_DIR, _SKILLS_SEARCH]:
+    if str(p) not in sys.path:
+        sys.path.insert(0, str(p))
+
+from typing import Iterator
+
+from hello_agents.tools import ToolRegistry
+from hello_agents.agents.react_agent import ReActAgent
+from hello_agents.core.llm import HelloAgentsLLM
+from hello_agents.core.config import Config
+from hello_agents.core.stream import StreamEvent
+
+from agents.tools.mx_search_tool import MXSearchTool
+
+# 默认舆情分析系统提示词
+SENTIMENT_SYSTEM_PROMPT = """你是一位专业的金融舆情分析师,精通A股市场和各种政策分析方法论。
+
+## 你的职责
+1. 搜索目标股票/行业的最新金融资讯(新闻、研报、公告)
+2. 分析各条资讯的情感倾向(正面/负面/中性)
+3. 识别关键事件和潜在影响
+4. 综合判断市场舆情趋势
+5. 提供客观、有数据支撑的舆情研判结论
+
+## 分析方法
+- 关注信息来源的权威性(官方公告 > 权威研报 > 新闻报道)
+- 关注资讯的时效性(越新越重要)
+- 区分短期情绪波动和长期趋势变化
+- 注意识别潜在的利好/利空事件
+- 结合行业政策环境进行分析
+
+## 输出格式
+分析结果应包含以下部分:
+1. **舆情总览**:情感分布统计(正面X条/负面X条/中性X条)
+2. **核心事件**:最重要的2-3个关键资讯摘要
+3. **情感趋势**:整体舆情偏向及变化趋势
+4. **风险提示**:需要关注的潜在风险和不确定性
+5. **综合研判**:基于舆情分析的投资参考建议(不构成投资建议)
+
+## 重要提醒
+- 始终保持客观中立,不夸大也不隐瞒风险
+- 所有分析结论需有搜索结果支撑
+- 末尾必须标注"以上分析仅供参考,不构成投资建议"
+"""
+
+
+def create_sentiment_agent(
+    api_key: str = None,
+    llm: HelloAgentsLLM = None,
+    system_prompt: str = None,
+    max_steps: int = 8,
+) -> ReActAgent:
+    """创建舆情分析Agent
+
+    Args:
+        api_key: 东方财富MX_APIKEY,不提供则从环境变量读取
+        llm: HelloAgentsLLM实例(必需),不提供则从环境变量自动创建
+        system_prompt: 自定义系统提示词(可选)
+        max_steps: 最大推理步数,默认8(搜索+综合常需多步)
+
+    Returns:
+        配置好的ReActAgent实例
+
+    Raises:
+        RuntimeError: 若LLM未配置且无法从环境变量创建
+    """
+    # 创建LLM实例(如果未提供)
+    if llm is None:
+        llm = _create_default_llm()
+
+    # 创建工具注册表并注册资讯搜索工具
+    registry = ToolRegistry()
+    search_tool = MXSearchTool(api_key=api_key)
+    registry.register_tool(search_tool)
+
+    # 使用自定义或默认系统提示词
+    prompt = system_prompt or SENTIMENT_SYSTEM_PROMPT
+
+    # 创建ReActAgent
+    agent = ReActAgent(
+        name="舆情分析Agent",
+        llm=llm,
+        tool_registry=registry,
+        system_prompt=prompt,
+        config=Config(temperature=0.3, max_tokens=4096),  # 低温度确保分析稳定
+        max_steps=max_steps,
+    )
+
+    return agent
+
+
+def _create_default_llm() -> HelloAgentsLLM:
+    """从环境变量创建默认LLM实例"""
+    import os
+
+    model = os.getenv("LLM_MODEL_ID")
+    api_key = os.getenv("LLM_API_KEY")
+    base_url = os.getenv("LLM_BASE_URL")
+    provider = os.getenv("LLM_PROVIDER", "auto")
+
+    if not api_key:
+        raise RuntimeError(
+            "LLM_API_KEY 环境变量未设置,请先设置环境变量:\n"
+            "export LLM_API_KEY=your_llm_api_key_here\n"
+            "或在创建Agent时传入 llm 参数"
+        )
+
+    return HelloAgentsLLM(
+        model=model,
+        api_key=api_key,
+        base_url=base_url,
+        provider=provider,
+        temperature=0.3,
+    )
+
+
+def analyze_sentiment_stream(
+    agent: ReActAgent,
+    stock_code: str = "",
+    stock_name: str = "",
+) -> Iterator[dict]:
+    """流式舆情分析 - 通过ReActAgent搜索资讯并分析舆情
+
+    Args:
+        agent: 已配置的舆情分析Agent
+        stock_code: 股票代码
+        stock_name: 股票名称
+
+    Yields:
+        dict: {"type": "meta"|"status"|"delta"|"done"|"error", "content": str}
+    """
+    stock_label = f"{stock_name}({stock_code})" if stock_name else stock_code
+
+    yield {"type": "meta", "stock_code": stock_code, "stock_name": stock_name}
+    yield {"type": "status", "content": f"正在搜索 {stock_label} 的相关资讯..."}
+
+    task = f"请搜索并分析股票 {stock_label} 的最新金融资讯、研究报告和公告,判断市场舆情趋势。"
+
+    try:
+        for event in agent.stream_run(task):
+            if event.event_type == "status":
+                yield {"type": "status", "content": event.content}
+            elif event.event_type in ("text", "observation"):
+                yield {"type": "delta", "content": event.content}
+            elif event.event_type == "tool_call":
+                yield {"type": "status", "content": f"正在调用工具: {event.metadata.get('tool_name', '')}"}
+            elif event.event_type == "done":
+                yield {"type": "done"}
+            elif event.event_type == "error":
+                yield {"type": "error", "content": event.content}
+    except Exception as e:
+        yield {"type": "error", "content": f"舆情分析出错: {e}"}

+ 1 - 0
Co-creation-projects/lcyting-StockSage-agent/agents/tests/__init__.py

@@ -0,0 +1 @@
+# Agent测试

+ 60 - 0
Co-creation-projects/lcyting-StockSage-agent/agents/text_truncation.py

@@ -0,0 +1,60 @@
+"""文本截断工具:在长度上限内优先在段落、换行、句号等处断开,减少半句截断。"""
+
+from __future__ import annotations
+
+
+def truncate_at_natural_boundary(text: str, max_chars: int, suffix: str = "…") -> str:
+    """
+    将 text 截断至不超过 max_chars(含 suffix)。
+
+    优先级:双换行段落 > 单换行 > 句末标点(。!?;.!?,跳过疑似小数点)> 空格 > 硬截断。
+    """
+    if max_chars <= 0:
+        return ""
+
+    raw = text or ""
+    if len(raw) <= max_chars:
+        return raw
+
+    suf = suffix or ""
+    limit = max_chars - len(suf)
+    if limit <= 0:
+        return suf[:max_chars]
+
+    window = raw[:limit]
+    # 句末/空格截断时避免只剩极短前缀;段落边界(\n\n)不受此限,以免首段很短时无法断段
+    min_pos = max(1, int(limit * 0.35))
+
+    def _fits(head_end: int) -> bool:
+        head = raw[:head_end].rstrip()
+        return len(head) + len(suf) <= max_chars
+
+    para = window.rfind("\n\n")
+    if para >= 1 and _fits(para) and raw[:para].strip():
+        return raw[:para].rstrip() + suf
+
+    nl = window.rfind("\n")
+    if nl >= min_pos and _fits(nl) and raw[:nl].strip():
+        return raw[:nl].rstrip() + suf
+
+    sentence_ends = "。!?;.!?"
+    cut = -1
+    for i in range(len(window) - 1, -1, -1):
+        ch = window[i]
+        if ch not in sentence_ends:
+            continue
+        if ch == ".":
+            if i > 0 and window[i - 1].isdigit():
+                continue
+            if i + 1 < len(window) and window[i + 1].isdigit():
+                continue
+        cut = i + 1
+        break
+    if cut >= min_pos and _fits(cut):
+        return raw[:cut].rstrip() + suf
+
+    sp = window.rfind(" ")
+    if sp >= min_pos and _fits(sp):
+        return raw[:sp].rstrip() + suf
+
+    return raw[:limit].rstrip() + suf

+ 2 - 0
Co-creation-projects/lcyting-StockSage-agent/agents/tools/__init__.py

@@ -0,0 +1,2 @@
+# 自定义工具封装层
+# 将外部服务层Skills封装为HelloAgents标准Tool接口

+ 158 - 0
Co-creation-projects/lcyting-StockSage-agent/agents/tools/mx_data_tool.py

@@ -0,0 +1,158 @@
+"""
+智能股票分析助手 — HelloAgents 金融数据工具封装
+
+将东方财富 mx-data Skill 封装为符合 HelloAgents 标准 Tool 接口的工具类。
+Agent可通过此工具调用自然语言查询获取行情、财务、关联关系等数据。
+"""
+
+import sys
+from pathlib import Path
+
+# 将HelloAgents框架和skills路径加入sys.path
+_PROJECT_ROOT = Path(__file__).parent.parent.parent
+_HELLO_PATH = _PROJECT_ROOT / "HelloAgents Optimized"
+_SKILLS_PATH = _PROJECT_ROOT / "skills" / "金融数据" / "mx-data"
+
+for p in [_HELLO_PATH, _SKILLS_PATH]:
+    if str(p) not in sys.path:
+        sys.path.insert(0, str(p))
+
+from hello_agents.tools import Tool, ToolParameter
+
+
+class MXDataTool(Tool):
+    """金融数据查询工具 — 封装东方财富妙想mx-data Skill
+
+    支持通过自然语言查询A股行情、财务指标、公司概况、股东信息等金融数据。
+
+    使用示例:
+        tool = MXDataTool(api_key="your_mx_apikey")
+        result = tool.run({"query": "贵州茅台最新价 涨跌幅"})
+    """
+
+    def __init__(self, api_key: str = None):
+        super().__init__(
+            name="mx_data",
+            description=(
+                "东方财富金融数据查询工具。支持查询A股股票的实时行情、历史行情、"
+                "财务指标(净利润、ROE、毛利率等)、公司概况(主营业务、高管信息)、"
+                "股东信息(十大股东)、指数行情、板块行情等。"
+                "支持自然语言查询,如'贵州茅台近三年净利润 营业收入'、"
+                "'沪深300指数最新点位'、'比亚迪公司简介 主营业务'。"
+            ),
+        )
+
+        # 获取API密钥:优先参数 > 环境变量
+        import os
+        self.api_key = api_key or os.getenv("MX_APIKEY", "")
+
+        # 延迟导入mx_data模块
+        self._mx_module = None
+
+    def _get_mx_module(self):
+        """延迟导入mx_data模块(避免初始化时的导入错误)"""
+        if self._mx_module is None:
+            import mx_data as _mx_data
+            self._mx_module = _mx_data
+        return self._mx_module
+
+    def get_parameters(self) -> list:
+        return [
+            ToolParameter(
+                name="query",
+                type="string",
+                description=(
+                    "自然语言查询语句。支持中文查询,例如:\n"
+                    "- 行情: '贵州茅台最新价 涨跌幅', '比亚迪近一年每个交易日收盘价'\n"
+                    "- 财务: '贵州茅台近三年净利润 营业收入 净资产收益率'\n"
+                    "- 公司: '比亚迪公司简介 主营业务 董事长是谁'\n"
+                    "- 股东: '贵州茅台十大股东'\n"
+                    "- 指数: '沪深300指数最新点位'"
+                ),
+                required=True,
+            ),
+        ]
+
+    def run(self, parameters: dict) -> str:
+        """执行金融数据查询
+
+        Args:
+            parameters: {"query": "自然语言查询"}
+
+        Returns:
+            格式化的查询结果文本
+        """
+        query = parameters.get("query", "")
+        if not query:
+            return "错误:查询内容不能为空"
+
+        if not self.api_key:
+            return "错误:MX_APIKEY 未配置,无法查询金融数据。请设置环境变量 MX_APIKEY"
+
+        try:
+            mx = self._get_mx_module()
+
+            # 创建MXData实例并查询
+            data_querier = mx.MXData(api_key=self.api_key)
+            result = data_querier.query(query)
+
+            # 解析结果
+            tables, condition_parts, total_rows, error = mx.MXData.parse_result(result)
+
+            if error:
+                return f"查询出错: {error}"
+
+            if not tables:
+                return "查询未返回任何数据"
+
+            # 格式化输出
+            return self._format_result(tables, condition_parts, total_rows)
+
+        except Exception as e:
+            return f"金融数据查询异常: {str(e)}"
+
+    def _format_result(self, tables: list, condition_parts: list, total_rows: int) -> str:
+        """将查询结果格式化为可读文本"""
+        lines = []
+
+        # 查询条件
+        if condition_parts:
+            lines.append("## 查询条件")
+            for part in condition_parts:
+                lines.append(part)
+            lines.append("")
+
+        # 数据表格
+        lines.append(f"## 查询结果({len(tables)}个表,共{total_rows}行数据)\n")
+
+        for idx, table in enumerate(tables):
+            sheet_name = table.get("sheet_name", f"表{idx+1}")
+            rows = table.get("rows", [])
+            fieldnames = table.get("fieldnames", [])
+
+            lines.append(f"### {sheet_name}")
+
+            if not rows:
+                lines.append("(无数据)")
+                continue
+
+            # 限制输出行数(避免上下文过长)
+            max_rows = 30
+            display_rows = rows[:max_rows]
+
+            # 表头
+            header = " | ".join(fieldnames[:10])  # 最多显示10列
+            lines.append(f"| {header} |")
+            lines.append(f"|{'|'.join(['---'] * min(len(fieldnames), 10))}|")
+
+            # 数据行
+            for row in display_rows:
+                values = [str(row.get(col, "")) for col in fieldnames[:10]]
+                lines.append(f"| {' | '.join(values)} |")
+
+            if len(rows) > max_rows:
+                lines.append(f"\n*(仅显示前{max_rows}行,共{len(rows)}行)*")
+
+            lines.append("")
+
+        return "\n".join(lines)

+ 189 - 0
Co-creation-projects/lcyting-StockSage-agent/agents/tools/mx_search_tool.py

@@ -0,0 +1,189 @@
+"""
+智能股票分析助手 — HelloAgents 资讯搜索工具封装
+
+将东方财富 mx-search Skill 封装为符合 HelloAgents 标准 Tool 接口的工具类。
+Agent可通过此工具调用自然语言搜索金融资讯(新闻、研报、公告)。
+"""
+
+import sys
+from pathlib import Path
+
+# 将HelloAgents框架和skills路径加入sys.path
+_PROJECT_ROOT = Path(__file__).parent.parent.parent
+_HELLO_PATH = _PROJECT_ROOT / "HelloAgents Optimized"
+_SKILLS_PATH = _PROJECT_ROOT / "skills" / "资讯搜索" / "mx-search"
+
+for p in [_PROJECT_ROOT, _HELLO_PATH, _SKILLS_PATH]:
+    if str(p) not in sys.path:
+        sys.path.insert(0, str(p))
+
+from hello_agents.tools import Tool, ToolParameter
+
+from ..text_truncation import truncate_at_natural_boundary
+
+# 单条资讯正文展示上限(字符);放宽可减少摘要中途截断
+MX_SEARCH_CONTENT_MAX_CHARS = 2500
+
+
+class MXSearchTool(Tool):
+    """金融资讯搜索工具 — 封装东方财富妙想mx-search Skill
+
+    支持通过自然语言搜索金融资讯,包括:
+    - 个股相关:研报、公告、机构观点
+    - 行业/板块:产业新闻、政策解读
+    - 宏观/市场:经济分析、资金流向
+    - 事件/规则:分红公告、交易规则等
+
+    使用示例:
+        tool = MXSearchTool(api_key="your_mx_apikey")
+        result = tool.run({"query": "贵州茅台最新研报"})
+    """
+
+    def __init__(self, api_key: str = None):
+        super().__init__(
+            name="mx_search",
+            description=(
+                "东方财富金融资讯搜索工具。支持搜索A股相关的新闻、研报、公告、"
+                "政策解读、行业分析等金融资讯。适用于获取时效性信息和特定事件信息。"
+                "支持自然语言查询,如'贵州茅台最新研报'、'人工智能板块近期新闻'、"
+                "'美联储加息对A股影响分析'、'新能源汽车产业政策最新解读'。"
+            ),
+        )
+
+        # 获取API密钥:优先参数 > 环境变量
+        import os
+        self.api_key = api_key or os.getenv("MX_APIKEY", "")
+
+        # 延迟导入mx_search模块
+        self._mx_module = None
+
+    def _get_mx_module(self):
+        """延迟导入mx_search模块(避免初始化时的导入错误)"""
+        if self._mx_module is None:
+            import mx_search as _mx_search
+            self._mx_module = _mx_search
+        return self._mx_module
+
+    def get_parameters(self) -> list:
+        return [
+            ToolParameter(
+                name="query",
+                type="string",
+                description=(
+                    "自然语言查询语句。支持中文查询,例如:\n"
+                    "- 个股资讯: '贵州茅台最新研报', '比亚迪机构观点汇总'\n"
+                    "- 行业新闻: '人工智能板块近期新闻', '新能源汽车产业政策'\n"
+                    "- 宏观分析: '美联储加息对A股影响分析', '北向资金最新流向'\n"
+                    "- 事件公告: '贵州茅台分红派息实施公告', '宁德时代定增预案'\n"
+                    "- 交易规则: '科创板涨跌幅限制', '新股申购规则'"
+                ),
+                required=True,
+            ),
+        ]
+
+    def run(self, parameters: dict) -> str:
+        """执行金融资讯搜索
+
+        Args:
+            parameters: {"query": "自然语言查询"}
+
+        Returns:
+            格式化的搜索结果文本
+        """
+        query = parameters.get("query", "")
+        if not query:
+            return "错误:查询内容不能为空"
+
+        if not self.api_key:
+            return "错误:MX_APIKEY 未配置,无法搜索资讯。请设置环境变量 MX_APIKEY"
+
+        try:
+            mx = self._get_mx_module()
+
+            # 创建MXSearch实例并查询
+            search_client = mx.MXSearch(api_key=self.api_key)
+            result = search_client.search(query)
+
+            # 格式化输出为可读文本
+            return self._format_result(result, query)
+
+        except Exception as e:
+            return f"资讯搜索异常: {str(e)}"
+
+    def _format_result(self, result: dict, query: str) -> str:
+        """将搜索结果格式化为可读文本"""
+        lines = []
+
+        # 检查API状态
+        status = result.get("status")
+        message = result.get("message", "")
+        if status != 0:
+            lines.append(f"## 资讯搜索结果")
+            lines.append(f"查询: {query}")
+            lines.append(f"错误: 状态码 {status} - {message}")
+            return "\n".join(lines)
+
+        # 解析搜索结果
+        data = result.get("data", {})
+        inner_data = data.get("data", {})
+        search_response = inner_data.get("llmSearchResponse", {})
+        items = search_response.get("data", [])
+
+        if not items:
+            return f"未找到与'{query}'相关的最新金融资讯"
+
+        # 类型映射
+        type_map = {
+            "REPORT": "研报",
+            "NEWS": "新闻",
+            "ANNOUNCEMENT": "公告"
+        }
+
+        # 限制输出条数
+        max_items = 15
+        display_items = items[:max_items]
+
+        lines.append(f"## 资讯搜索结果")
+        lines.append(f"查询: {query}")
+        lines.append(f"共找到 {len(items)} 条相关资讯\n")
+
+        for i, item in enumerate(display_items):
+            title = item.get("title", "无标题")
+            content = item.get("content", "")
+            date = item.get("date", "")
+            ins_name = item.get("insName", "")
+            info_type = item.get("informationType", "")
+            rating = item.get("rating", "")
+            entity_name = item.get("entityFullName", "")
+
+            type_cn = type_map.get(info_type, info_type or "资讯")
+
+            lines.append(f"### {i+1}. {title}")
+
+            meta_parts = []
+            if entity_name:
+                meta_parts.append(f"证券: {entity_name}")
+            if ins_name:
+                meta_parts.append(f"机构: {ins_name}")
+            if date:
+                meta_parts.append(f"日期: {date.split()[0]}")
+            lines.append(f"类型: {type_cn} | {' | '.join(meta_parts)}")
+
+            if rating:
+                lines.append(f"评级: {rating}")
+
+            if content:
+                # 截断过长内容(优先段落/句号)
+                if len(content) > MX_SEARCH_CONTENT_MAX_CHARS:
+                    content = truncate_at_natural_boundary(
+                        content, MX_SEARCH_CONTENT_MAX_CHARS, "..."
+                    )
+                lines.append("")
+                lines.append(content)
+
+            lines.append("")
+
+        if len(items) > max_items:
+            lines.append(f"*(仅显示前{max_items}条,共{len(items)}条)*")
+
+        return "\n".join(lines)

+ 34 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/Dockerfile

@@ -0,0 +1,34 @@
+# =========================================================================
+# 智能股票分析助手 — 后端 Dockerfile
+# 
+# 基于 Python 3.12,运行 FastAPI 应用
+# 构建: docker build -t stock-analyzer-backend -f backend/Dockerfile .
+# =========================================================================
+
+FROM python:3.12-slim
+
+WORKDIR /app
+
+# 安装系统依赖(gcc用于编译BCrypt等扩展)
+RUN apt-get update && apt-get install -y --no-install-recommends gcc && \
+    rm -rf /var/lib/apt/lists/*
+
+# 安装 Python 依赖
+COPY backend/requirements.txt /tmp/requirements.txt
+RUN pip install --no-cache-dir -r /tmp/requirements.txt
+
+# 复制项目文件
+COPY backend/ /app/backend/
+COPY agents/ /app/agents/
+COPY "HelloAgents Optimized/" "/app/HelloAgents Optimized/"
+COPY "skills/" "/app/skills/"
+COPY .env /app/.env
+
+# 创建数据目录(SQLite数据库存储位置)
+RUN mkdir -p /app/data
+
+# 暴露端口
+EXPOSE 8000
+
+# 启动 FastAPI 应用
+CMD ["python", "-m", "uvicorn", "backend.app.main:app", "--host", "0.0.0.0", "--port", "8000"]

+ 1 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/__init__.py

@@ -0,0 +1 @@
+# 智能股票分析助手 — 后端应用

+ 1 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/api/__init__.py

@@ -0,0 +1 @@
+# API路由层

+ 124 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/api/agent_api.py

@@ -0,0 +1,124 @@
+"""
+Agent分析流式 API — 舆情分析、数据分析流式接口
+
+实现方案路径别名见 sentiment.py、data_analysis.py(/sentiment/analyze/stream 等)。
+"""
+
+import json
+import threading
+
+from fastapi import APIRouter
+from fastapi.responses import StreamingResponse
+from pydantic import BaseModel, Field
+
+from app.services import history_service
+
+router = APIRouter(prefix="/agent", tags=["Agent分析"])
+
+
+class AnalysisRequest(BaseModel):
+    stock_code: str = Field(..., description="股票代码", min_length=6, max_length=6)
+    stock_name: str = Field("", description="股票名称")
+
+
+def _save_to_history(analysis_type, content, stock_code, stock_name, title):
+    """在后台线程中保存分析历史"""
+    import asyncio
+
+    try:
+        loop = asyncio.new_event_loop()
+        asyncio.set_event_loop(loop)
+        loop.run_until_complete(
+            history_service.save_analysis(
+                analysis_type=analysis_type,
+                content=content,
+                stock_code=stock_code,
+                stock_name=stock_name,
+                title=title,
+            )
+        )
+        loop.close()
+    except Exception:
+        pass
+
+
+def _delta_text(ev: dict) -> str:
+    return (ev.get("content") or ev.get("text") or "") if isinstance(ev, dict) else ""
+
+
+def iter_sentiment_analysis_ndjson(stock_code: str, stock_name: str):
+    """生成舆情分析 NDJSON 行(字符串迭代器,含尾部换行)。"""
+    collected_content = []
+    try:
+        from agents.agent_system import get_agent_system
+
+        asys = get_agent_system()
+        for event in asys.analyze_sentiment_stream(stock_code, stock_name):
+            if event.get("type") == "delta":
+                collected_content.append(_delta_text(event))
+            yield json.dumps(event, ensure_ascii=False) + "\n"
+
+        full_content = "".join(collected_content)
+        if full_content:
+            threading.Thread(
+                target=_save_to_history,
+                args=(
+                    "sentiment",
+                    full_content,
+                    stock_code,
+                    stock_name,
+                    f"{stock_name or stock_code} 舆情分析",
+                ),
+                daemon=True,
+            ).start()
+    except Exception as e:
+        yield json.dumps({"type": "error", "content": f"舆情分析服务错误: {e}"}, ensure_ascii=False) + "\n"
+
+
+def iter_data_analysis_ndjson(stock_code: str, stock_name: str):
+    """生成数据分析 NDJSON 行。"""
+    collected_content = []
+    try:
+        from agents.agent_system import get_agent_system
+
+        asys = get_agent_system()
+        for event in asys.analyze_data_stream(stock_code, stock_name):
+            if event.get("type") == "delta":
+                collected_content.append(_delta_text(event))
+            yield json.dumps(event, ensure_ascii=False) + "\n"
+
+        full_content = "".join(collected_content)
+        if full_content:
+            threading.Thread(
+                target=_save_to_history,
+                args=(
+                    "data_analysis",
+                    full_content,
+                    stock_code,
+                    stock_name,
+                    f"{stock_name or stock_code} 数据分析",
+                ),
+                daemon=True,
+            ).start()
+    except Exception as e:
+        yield json.dumps({"type": "error", "content": f"数据分析服务错误: {e}"}, ensure_ascii=False) + "\n"
+
+
+@router.post("/sentiment/stream")
+async def sentiment_stream(body: AnalysisRequest):
+    """AI舆情分析流式接口(兼容路径)"""
+    return StreamingResponse(
+        iter_sentiment_analysis_ndjson(body.stock_code, body.stock_name),
+        media_type="application/x-ndjson",
+        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
+    )
+
+
+@router.post("/data-analysis/stream")
+async def data_analysis_stream(body: AnalysisRequest):
+    """AI数据分析流式接口(兼容路径)"""
+    return StreamingResponse(
+        iter_data_analysis_ndjson(body.stock_code, body.stock_name),
+        media_type="application/x-ndjson",
+        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
+    )

+ 86 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/api/analysis.py

@@ -0,0 +1,86 @@
+"""
+智能股票分析助手 — 分析报告API路由(兼容层)
+
+旧版 POST 生成报告已移除;列表与详情优先走分析历史表,其次兼容旧版 AnalysisReport 表。
+"""
+
+from fastapi import APIRouter, Query
+from fastapi.responses import JSONResponse
+
+from app.services import analysis_service, history_service
+from app.utils.response import success_response, error_response
+
+router = APIRouter(prefix="/analysis", tags=["分析报告"])
+
+
+@router.post("/report/{code}")
+async def generate_report_removed(
+    code: str,
+    user_id: str = Query(default="default", description="用户标识"),
+    report_type: str = Query(default="full", description="报告类型: full/quick"),
+):
+    """已废弃:请使用 AI 对话助手或个股页 AI 分析 Tab。"""
+    return JSONResponse(
+        status_code=410,
+        content=error_response(
+            code=410,
+            message=(
+                "该接口已移除。请使用 POST /api/v1/chat/stream(AI 对话助手),"
+                "或个股分析页的舆情 / 数据 / 巴菲特流式分析。"
+            ),
+            data={"replacement_chat": "/api/v1/chat/stream", "stock_code": code},
+        ),
+    )
+
+
+@router.get("/report/{report_id}")
+async def get_report(report_id: int):
+    """获取指定记录:优先 analysis_history,其次旧版 analysis_reports 表。"""
+    if report_id <= 0:
+        return error_response(code=400, message="无效的报告ID")
+
+    hist = await history_service.get_history_detail(report_id)
+    if hist.get("success"):
+        rec = hist["record"]
+        return success_response(
+            data={
+                "report": rec,
+                "source": "history",
+            },
+            message="查询成功",
+        )
+
+    legacy = await analysis_service.get_report(report_id)
+    if legacy.get("success"):
+        return success_response(
+            data={
+                "report": legacy["report"],
+                "source": "legacy_analysis_report",
+            },
+            message="查询成功(旧版报告表)",
+        )
+
+    return error_response(code=404, message=hist.get("error") or legacy.get("error") or "记录不存在")
+
+
+@router.get("/reports")
+async def list_reports(
+    user_id: str = Query(default="default", description="用户标识"),
+    limit: int = Query(default=20, ge=1, le=100, description="最大返回数量"),
+):
+    """分析报告历史列表 — 对应 analysis_history(各 AI 分析类型)。"""
+    result = await history_service.get_history_list(
+        analysis_type=None, user_id=user_id, limit=limit
+    )
+    if not result["success"]:
+        return error_response(code=500, message=result.get("error", "查询失败"))
+
+    items = result["items"]
+    return success_response(
+        data={
+            "reports": items,
+            "items": items,
+            "total": result["total"],
+        },
+        message=f"共 {result['total']} 条记录",
+    )

+ 243 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/api/buffett.py

@@ -0,0 +1,243 @@
+"""
+智能股票分析助手 — 巴菲特投资评估API路由
+
+提供巴菲特价值投资框架查询、投资评估接口。
+"""
+
+import asyncio
+import json
+
+from fastapi import APIRouter, Query
+from fastapi.responses import StreamingResponse
+from pydantic import BaseModel, Field
+
+from app.services import buffett_service
+from app.utils.response import success_response, error_response
+
+router = APIRouter(prefix="/buffett", tags=["巴菲特投资评估"])
+
+
+class BuffettEvaluateRequest(BaseModel):
+    """巴菲特投资评估请求"""
+    stock_code: str = Field(..., description="6位股票代码", min_length=4, max_length=10)
+    stock_name: str = Field(default="", description="股票名称(可选)")
+    include_market: bool = Field(default=True, description="是否包含行情数据")
+    include_financial: bool = Field(default=True, description="是否包含财务数据")
+
+
+@router.get("/framework")
+async def get_buffett_framework():
+    """获取巴菲特投资评估框架
+
+    返回完整的巴菲特价值投资思维体系,包括:
+    - 8问快速筛选清单
+    - 护城河分析五类型
+    - 管理层评估三维度
+    - 财务指标模板(ROIC、所有者收益、现金转化率)
+    - 估值方法与安全边际
+    - 风险评估分类
+    - 卖出四条标准
+    """
+    result = buffett_service.get_buffett_framework()
+    return success_response(
+        data={
+            "framework": result["framework"],
+            "description": result["description"],
+        },
+        message="巴菲特投资评估框架已就绪",
+    )
+
+
+@router.post("/evaluate")
+async def evaluate_stock(body: BuffettEvaluateRequest):
+    """使用巴菲特投资框架评估股票
+
+    构建巴菲特风格的价值投资评估上下文,返回结构化评估模板和参考框架。
+
+    - **stock_code**: 6位股票代码,如 600519(贵州茅台)
+    - **stock_name**: 股票名称(可选,用于报告标题)
+    - **include_market**: 是否尝试获取行情数据
+    - **include_financial**: 是否尝试获取财务数据
+    """
+    if not body.stock_code or len(body.stock_code.strip()) < 4:
+        return error_response(code=400, message="请输入有效的股票代码")
+
+    # 构建数据上下文(尝试收集数据,不因单个数据失败而中断)
+    data_context = {}
+    errors = []
+
+    if body.include_market:
+        try:
+            from app.services import market_service
+            market_result = market_service.get_stock_quote(body.stock_code.strip())
+            if market_result.get("success"):
+                data_context["market"] = market_result
+        except Exception as e:
+            errors.append(f"行情数据获取失败: {e}")
+
+    if body.include_financial:
+        try:
+            from app.services import market_service
+            financial_result = market_service.get_stock_financial(body.stock_code.strip())
+            if financial_result.get("success"):
+                data_context["financial"] = financial_result
+        except Exception as e:
+            errors.append(f"财务数据获取失败: {e}")
+
+    # 执行巴菲特评估
+    result = buffett_service.evaluate_with_buffett(
+        stock_code=body.stock_code.strip(),
+        stock_name=body.stock_name.strip() or "",
+        data_context=data_context,
+    )
+
+    if not result["success"]:
+        return error_response(code=500, message=result.get("error", "评估失败"))
+
+    data_warnings = ""
+    if errors:
+        data_warnings = "; ".join(errors)
+
+    slim_ctx = buffett_service.slim_evaluation_context_for_api(result["evaluation_context"])
+
+    return success_response(
+        data={
+            "stock_code": result["stock_code"],
+            "stock_name": result["stock_name"],
+            "evaluation_context": slim_ctx,
+            "report_template": result["report_template"],
+            "data_warnings": data_warnings or None,
+        },
+        message=f"已构建 {result['stock_name'] or result['stock_code']} 的巴菲特评估上下文",
+    )
+
+
+@router.post("/report/generate-ai")
+async def generate_ai_buffett_report(body: BuffettEvaluateRequest):
+    """一键生成巴菲特价值投资评估报告(LLM,同步 JSON)"""
+    if not body.stock_code or len(body.stock_code.strip()) < 4:
+        return error_response(code=400, message="请输入有效的股票代码")
+
+    result = await asyncio.to_thread(
+        buffett_service.generate_buffett_ai_report,
+        body.stock_code.strip(),
+        (body.stock_name or "").strip(),
+    )
+
+    if not result.get("success"):
+        return error_response(
+            code=503,
+            message=result.get("error") or "AI 评估报告生成失败",
+            data={"stock_code": body.stock_code.strip()},
+        )
+
+    return success_response(
+        data={
+            "stock_code": body.stock_code.strip(),
+            "stock_name": (body.stock_name or "").strip(),
+            "report_markdown": result["report_markdown"],
+        },
+        message="巴菲特 AI 评估报告已生成",
+    )
+
+
+def _buffett_ai_report_ndjson_bytes(stock_code: str, stock_name: str):
+    for evt in buffett_service.iter_buffett_ai_report_events(stock_code, stock_name):
+        line = json.dumps(evt, ensure_ascii=False) + "\n"
+        yield line.encode("utf-8")
+
+
+@router.post("/report/generate-ai/stream")
+async def stream_ai_buffett_report(body: BuffettEvaluateRequest):
+    """流式生成巴菲特 AI 评估报告(NDJSON:meta → delta → … → done)"""
+    if not body.stock_code or len(body.stock_code.strip()) < 4:
+        return error_response(code=400, message="请输入有效的股票代码")
+
+    return StreamingResponse(
+        _buffett_ai_report_ndjson_bytes(
+            body.stock_code.strip(),
+            (body.stock_name or "").strip(),
+        ),
+        media_type="application/x-ndjson",
+        headers={
+            "Cache-Control": "no-cache",
+            "Connection": "keep-alive",
+            "X-Accel-Buffering": "no",
+        },
+    )
+
+
+@router.post("/evaluate/stream")
+async def stream_buffett_evaluate(body: BuffettEvaluateRequest):
+    """实现方案约定路径 POST /api/v1/buffett/evaluate/stream,等价于 report/generate-ai/stream"""
+    if not body.stock_code or len(body.stock_code.strip()) < 4:
+        return error_response(code=400, message="请输入有效的股票代码")
+
+    return StreamingResponse(
+        _buffett_ai_report_ndjson_bytes(
+            body.stock_code.strip(),
+            (body.stock_name or "").strip(),
+        ),
+        media_type="application/x-ndjson",
+        headers={
+            "Cache-Control": "no-cache",
+            "Connection": "keep-alive",
+            "X-Accel-Buffering": "no",
+        },
+    )
+
+
+@router.get("/report/template")
+async def get_report_template(
+    code: str = Query(..., description="股票代码", min_length=4),
+    name: str = Query(default="", description="股票名称"),
+):
+    """获取巴菲特风格评估报告模板
+
+    返回包含所有必填章节的Markdown格式报告模板,可直接用于填写分析结果。
+
+    - **code**: 6位股票代码
+    - **name**: 股票名称(可选)
+    """
+    template = buffett_service._build_buffett_report_template(code, name)
+    return success_response(
+        data={"template": template, "stock_code": code, "stock_name": name},
+        message="报告模板已生成",
+    )
+
+
+@router.get("/reference/{ref_name}")
+async def get_reference_file(
+    ref_name: str,
+):
+    """获取巴菲特投资思维参考文件内容
+
+    可获取的参考文件:
+    - 01-thinking-frameworks (思维框架)
+    - 02-investment-philosophy (投资哲学)
+    - 03-business-moat (企业护城河)
+    - 04-management-governance (管理层治理)
+    - 05-financial-metrics (财务指标)
+    - 06-valuation-capital (估值与资本)
+    - 07-risk-behavior (风险与行为)
+    - 08-industry-playbooks (行业手册)
+
+    - **ref_name**: 参考文件名,如 "03-business-moat"
+    """
+    content = buffett_service.load_buffett_reference(ref_name)
+    if content is None:
+        return error_response(code=404, message=f"参考文件 '{ref_name}' 不存在或无法读取")
+
+    # 截取前5000字符返回
+    preview = content[:5000]
+    is_truncated = len(content) > 5000
+
+    return success_response(
+        data={
+            "ref_name": ref_name,
+            "content": preview,
+            "full_length": len(content),
+            "truncated": is_truncated,
+        },
+        message=f"参考文件 {ref_name}({'预览前5000字符' if is_truncated else '完整内容'})",
+    )

+ 72 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/api/cache_api.py

@@ -0,0 +1,72 @@
+"""
+股票文件缓存 API — grep搜索、缓存管理、数据统计
+"""
+
+from fastapi import APIRouter, Query
+from app.services.stock_file_cache import get_stock_file_cache
+from app.utils.response import success_response, error_response
+
+router = APIRouter(prefix="/cache", tags=["文件缓存"])
+
+
+@router.get("/search")
+async def grep_search(
+    keyword: str = Query(..., description="搜索关键词"),
+    data_type: str = Query(None, description="限定数据类型: quote/financial/profile/holders/sentiment"),
+):
+    """grep 风格搜索缓存文件内容
+
+    在所有已缓存的股票数据文件中搜索关键词,返回匹配结果。
+    """
+    fc = get_stock_file_cache()
+    results = fc.grep_search(keyword, data_type)
+    return success_response(data={
+        "keyword": keyword,
+        "total_matches": len(results),
+        "results": results,
+    })
+
+
+@router.get("/stock/{stock_code}")
+async def get_stock_cache_info(stock_code: str):
+    """查询某股票的缓存状态"""
+    fc = get_stock_file_cache()
+    data_types = fc.get_stock_data_types(stock_code)
+    return success_response(data={
+        "stock_code": stock_code,
+        "cached_types": data_types,
+        "has_quote": "quote" in data_types,
+        "has_financial": "financial" in data_types,
+        "has_profile": "profile" in data_types,
+        "has_holders": "holders" in data_types,
+        "has_sentiment": "sentiment" in data_types,
+    })
+
+
+@router.get("/stats")
+async def cache_stats():
+    """获取缓存统计信息"""
+    fc = get_stock_file_cache()
+    return success_response(data=fc.get_stats())
+
+
+@router.delete("/clear")
+async def clear_cache(
+    stock_code: str = Query(None, description="指定股票代码,不传则清空全部"),
+):
+    """清除文件缓存"""
+    fc = get_stock_file_cache()
+    fc.clear_stock_cache(stock_code)
+    return success_response(message=f"缓存已清除{'(' + stock_code + ')' if stock_code else ''}")
+
+
+@router.get("/list")
+async def list_cached_stocks():
+    """列出所有已缓存的股票代码"""
+    fc = get_stock_file_cache()
+    codes = fc.get_stock_codes()
+    result = []
+    for code in codes:
+        types = fc.get_stock_data_types(code)
+        result.append({"code": code, "data_types": types})
+    return success_response(data={"stocks": result, "total": len(result)})

+ 46 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/api/chat.py

@@ -0,0 +1,46 @@
+"""
+AI对话助手 API — 协调者Agent流式对话接口
+"""
+
+import json
+
+from fastapi import APIRouter
+from fastapi.responses import StreamingResponse
+from pydantic import BaseModel, Field
+
+from app.services import chat_service
+
+router = APIRouter(prefix="/chat", tags=["AI对话助手"])
+
+
+class ChatRequest(BaseModel):
+    message: str = Field(..., description="用户消息", min_length=1)
+    stock_code: str = Field("", description="关联股票代码(可选)")
+    stock_name: str = Field("", description="关联股票名称(可选)")
+    history: list = Field(default_factory=list, description="对话历史")
+
+
+def _make_chat_stream(message: str, stock_code: str, stock_name: str, history: list):
+    """生成NDJSON流式响应"""
+    try:
+        for event in chat_service.iter_chat_stream_events(
+            message, stock_code, stock_name, history
+        ):
+            yield json.dumps(event, ensure_ascii=False) + "\n"
+    except Exception as e:
+        err = {"type": "error", "content": f"对话服务错误: {e}", "message": f"对话服务错误: {e}"}
+        yield json.dumps(err, ensure_ascii=False) + "\n"
+
+
+@router.post("/stream")
+async def chat_stream(body: ChatRequest):
+    """AI对话助手流式接口
+
+    用户通过对话形式提供需求,协调者Agent解析需求,
+    自主调用子Agent并流式输出分析结果。
+    """
+    return StreamingResponse(
+        _make_chat_stream(body.message, body.stock_code, body.stock_name, body.history),
+        media_type="application/x-ndjson",
+        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
+    )

+ 18 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/api/data_analysis.py

@@ -0,0 +1,18 @@
+"""AI 数据分析流式 API — 实现方案约定路径"""
+
+from fastapi import APIRouter
+from fastapi.responses import StreamingResponse
+
+from app.api.agent_api import AnalysisRequest, iter_data_analysis_ndjson
+
+router = APIRouter(prefix="/data-analysis", tags=["AI数据分析"])
+
+
+@router.post("/analyze/stream")
+async def data_analysis_analyze_stream(body: AnalysisRequest):
+    """POST /api/v1/data-analysis/analyze/stream"""
+    return StreamingResponse(
+        iter_data_analysis_ndjson(body.stock_code, body.stock_name),
+        media_type="application/x-ndjson",
+        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
+    )

+ 55 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/api/financial.py

@@ -0,0 +1,55 @@
+"""
+智能股票分析助手 — 财务数据API路由
+
+提供财务指标、公司概况、股东信息查询接口。
+"""
+
+from fastapi import APIRouter, Query
+from app.services import market_service
+from app.utils.mx_http import mx_result_to_http
+from app.utils.response import error_response
+
+router = APIRouter(prefix="/financial", tags=["财务数据"])
+
+
+@router.get("/indicators/{code}")
+async def get_financial_indicators(
+    code: str,
+    indicators: str = Query(default="净利润 营业收入 净资产收益率 每股收益", description="需要的财务指标"),
+):
+    """获取个股财务指标
+
+    - **code**: 6位股票代码
+    - **indicators**: 财务指标描述(自然语言),如 "净利润 营业收入 ROE"
+    """
+    if not code or len(code) < 4:
+        return error_response(code=400, message="请输入有效的股票代码")
+
+    result = market_service.get_stock_financial(code, indicators)
+    return mx_result_to_http(result)
+
+
+@router.get("/profile/{code}")
+async def get_company_profile(code: str):
+    """获取公司概况
+
+    - **code**: 6位股票代码
+    """
+    if not code or len(code) < 4:
+        return error_response(code=400, message="请输入有效的股票代码")
+
+    result = market_service.get_stock_profile(code)
+    return mx_result_to_http(result)
+
+
+@router.get("/holders/{code}")
+async def get_top_holders(code: str):
+    """获取十大股东信息
+
+    - **code**: 6位股票代码
+    """
+    if not code or len(code) < 4:
+        return error_response(code=400, message="请输入有效的股票代码")
+
+    result = market_service.get_stock_holders(code)
+    return mx_result_to_http(result)

+ 50 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/api/history.py

@@ -0,0 +1,50 @@
+"""
+分析历史记录 API — 管理各类分析报告历史
+"""
+
+from fastapi import APIRouter, Query
+from app.services import history_service
+from app.utils.response import success_response, error_response
+
+router = APIRouter(prefix="/history", tags=["分析历史"])
+
+
+@router.get("/list")
+async def list_history(
+    type: str = Query(None, description="类型: sentiment/data_analysis/buffett/chat"),
+    limit: int = Query(20, ge=1, le=100),
+):
+    """获取历史记录列表"""
+    result = await history_service.get_history_list(analysis_type=type, limit=limit)
+    if not result["success"]:
+        return error_response(code=500, message=result.get("error", "查询失败"))
+    return success_response(data={"items": result["items"], "total": result["total"]})
+
+
+@router.get("/{record_id}")
+async def get_history(record_id: int):
+    """获取历史记录详情"""
+    result = await history_service.get_history_detail(record_id)
+    if not result["success"]:
+        return error_response(code=404, message=result.get("error", "记录不存在"))
+    return success_response(data=result["record"])
+
+
+@router.delete("/{record_id}")
+async def delete_history(record_id: int):
+    """删除单条历史记录"""
+    result = await history_service.delete_history(record_id)
+    if not result["success"]:
+        return error_response(code=404, message=result.get("error", "删除失败"))
+    return success_response(message=result["message"])
+
+
+@router.post("/clear")
+async def clear_history(
+    type: str = Query(None, description="仅清除指定类型"),
+):
+    """清空今日历史"""
+    result = await history_service.clear_today_history(analysis_type=type)
+    if not result["success"]:
+        return error_response(code=500, message=result.get("error", "清除失败"))
+    return success_response(message=f"已清除 {result['count']} 条记录")

+ 48 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/api/market.py

@@ -0,0 +1,48 @@
+"""
+智能股票分析助手 — 行情数据API路由
+
+提供个股行情、指数行情、板块行情查询接口。
+"""
+
+from fastapi import APIRouter, Query
+from app.services import market_service
+from app.utils.mx_http import mx_result_to_http
+from app.utils.response import error_response
+
+router = APIRouter(prefix="/market", tags=["行情数据"])
+
+
+@router.get("/quote/{code}")
+async def get_stock_quote(code: str):
+    """获取个股实时行情
+
+    - **code**: 6位股票代码,如 600519(贵州茅台)、000001(平安银行)
+    """
+    if not code or len(code) < 4:
+        return error_response(code=400, message="请输入有效的股票代码")
+
+    result = market_service.get_stock_quote(code)
+    return mx_result_to_http(result)
+
+
+@router.get("/index")
+async def get_index_quote(name: str = Query(default="沪深300", description="指数名称")):
+    """获取指数行情
+
+    - **name**: 指数名称,如 沪深300、上证指数、创业板指
+    """
+    result = market_service.get_index_quote(name)
+    return mx_result_to_http(result)
+
+
+@router.get("/sector/{name}")
+async def get_sector_quote(name: str):
+    """获取板块行情
+
+    - **name**: 板块名称,如 白酒、新能源、半导体
+    """
+    if not name:
+        return error_response(code=400, message="请输入板块名称")
+
+    result = market_service.get_sector_quote(name)
+    return mx_result_to_http(result)

+ 52 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/api/news.py

@@ -0,0 +1,52 @@
+"""
+智能股票分析助手 — 资讯搜索API路由
+
+提供金融资讯搜索、个股舆情分析、热门资讯查询接口。
+"""
+
+from fastapi import APIRouter, Query
+from app.services import news_service
+from app.utils.mx_http import mx_result_to_http
+from app.utils.response import error_response
+
+router = APIRouter(prefix="/news", tags=["资讯搜索"])
+
+
+@router.get("/search")
+async def search_news(
+    query: str = Query(..., description="自然语言搜索问句"),
+):
+    """搜索金融资讯
+
+    - **query**: 自然语言搜索问句,如 "人工智能板块近期新闻"、"贵州茅台最新研报"
+    """
+    if not query or not query.strip():
+        return error_response(code=400, message="请输入搜索内容")
+
+    result = news_service.search_news(query.strip())
+    return mx_result_to_http(result)
+
+
+@router.get("/sentiment/{code}")
+async def get_stock_sentiment(code: str):
+    """获取个股舆情分析
+
+    根据股票代码搜索该股票相关的新闻、研报、公告,并进行分类整理。
+
+    - **code**: 6位股票代码,如 600519(贵州茅台)、000001(平安银行)
+    """
+    if not code or len(code) < 4:
+        return error_response(code=400, message="请输入有效的股票代码")
+
+    result = news_service.analyze_sentiment(code)
+    return mx_result_to_http(result)
+
+
+@router.get("/hot")
+async def get_hot_news():
+    """获取当前市场热门资讯
+
+    返回今日A股市场热点动态、北向资金流向等资讯摘要。
+    """
+    result = news_service.search_market_news()
+    return mx_result_to_http(result)

+ 88 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/api/preferences.py

@@ -0,0 +1,88 @@
+"""
+智能股票分析助手 — 用户偏好API路由
+
+提供偏好的读取、更新和投资画像查询接口。
+"""
+
+from fastapi import APIRouter, Depends
+from sqlalchemy.ext.asyncio import AsyncSession
+from pydantic import BaseModel, Field
+from typing import Optional, List
+
+from app.models.database import get_db_session
+from app.services import preference_service
+from app.utils.response import success_response, error_response
+
+router = APIRouter(prefix="/preferences", tags=["用户偏好"])
+
+
+# =========================================================================
+# 请求体模型
+# =========================================================================
+
+class PreferenceUpdateRequest(BaseModel):
+    """偏好更新请求体——所有字段可选,支持部分更新"""
+
+    risk_tolerance: Optional[str] = Field(None, description="风险承受度: conservative/moderate/aggressive", pattern="^(conservative|moderate|aggressive)$")
+    investment_style: Optional[str] = Field(None, description="投资风格: value/growth/momentum/dividend/blend", pattern="^(value|growth|momentum|dividend|blend)$")
+    investment_horizon: Optional[str] = Field(None, description="投资期限: short/medium/long", pattern="^(short|medium|long)$")
+    target_return_rate: Optional[float] = Field(None, description="目标年化收益率(%)", ge=0, le=100)
+    max_position_ratio: Optional[float] = Field(None, description="单票最大仓位(%)", ge=1, le=100)
+    max_drawdown_limit: Optional[float] = Field(None, description="最大回撤预警线(%)", le=0)
+    notification_enabled: Optional[bool] = Field(None, description="是否启用通知")
+    notification_channels: Optional[List[str]] = Field(None, description="通知渠道")
+    market_alert_threshold: Optional[float] = Field(None, description="异动提醒阈值(%)", ge=0, le=100)
+    language: Optional[str] = Field(None, description="界面语言: zh/en", pattern="^(zh|en)$")
+    theme: Optional[str] = Field(None, description="主题: light/dark/auto", pattern="^(light|dark|auto)$")
+    default_view: Optional[str] = Field(None, description="默认首页: dashboard/watchlist", pattern="^(dashboard|watchlist)$")
+    preferred_sectors: Optional[List[str]] = Field(None, description="偏好行业列表")
+    excluded_sectors: Optional[List[str]] = Field(None, description="排除行业列表")
+
+
+# =========================================================================
+# API接口
+# =========================================================================
+
+@router.get("/")
+async def get_preferences(
+    user_id: str = "default",
+    db: AsyncSession = Depends(get_db_session),
+):
+    """获取用户偏好配置"""
+    try:
+        result = await preference_service.get_preference(db, user_id)
+        return success_response(data=result)
+    except Exception as e:
+        return error_response(code=500, message=f"获取偏好失败: {str(e)}")
+
+
+@router.put("/")
+async def update_preferences(
+    request: PreferenceUpdateRequest,
+    user_id: str = "default",
+    db: AsyncSession = Depends(get_db_session),
+):
+    """更新用户偏好(支持部分更新,仅传入需要修改的字段)"""
+    try:
+        # 仅提交非None的字段
+        update_data = request.model_dump(exclude_none=True)
+        if not update_data:
+            return error_response(code=400, message="未提供需要更新的字段")
+
+        result = await preference_service.update_preference(db, user_id, update_data)
+        return success_response(data=result, message="偏好更新成功")
+    except Exception as e:
+        return error_response(code=500, message=f"更新偏好失败: {str(e)}")
+
+
+@router.get("/profile")
+async def get_profile(
+    user_id: str = "default",
+    db: AsyncSession = Depends(get_db_session),
+):
+    """获取用户投资画像摘要"""
+    try:
+        result = await preference_service.get_profile_summary(db, user_id)
+        return success_response(data=result)
+    except Exception as e:
+        return error_response(code=500, message=f"获取画像失败: {str(e)}")

+ 45 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/api/screener.py

@@ -0,0 +1,45 @@
+"""
+智能股票分析助手 — 智能选股API路由
+
+提供条件选股、可用选股条件查询接口。
+"""
+
+from fastapi import APIRouter, Query
+from app.services import screener_service
+from app.utils.mx_http import mx_result_to_http
+from app.utils.response import success_response, error_response
+
+router = APIRouter(prefix="/screener", tags=["智能选股"])
+
+
+@router.get("/conditions")
+async def get_screener_conditions():
+    """获取可用的选股条件参考
+
+    返回选股维度分类和示例条件,供前端展示和用户参考。
+    """
+    result = screener_service.get_available_conditions()
+    if not result.get("success"):
+        return error_response(code=500, message=result.get("error", "获取条件失败"))
+    return success_response(data=result)
+
+
+@router.post("/search")
+async def screen_stocks(
+    query: str = Query(..., description="自然语言选股条件"),
+):
+    """条件选股
+
+    根据自然语言描述的选股条件,筛选符合条件的股票。
+
+    - **query**: 自然语言选股条件,如:
+      - "市盈率小于20且ROE大于15%的A股"
+      - "新能源板块涨幅大于1%的股票"
+      - "沪深300成分股中分红率最高的10只"
+      - "价格小于20元 市盈率小于20 涨幅大于1%"
+    """
+    if not query or not query.strip():
+        return error_response(code=400, message="请输入选股条件")
+
+    result = screener_service.screen_stocks(query.strip())
+    return mx_result_to_http(result)

+ 18 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/api/sentiment.py

@@ -0,0 +1,18 @@
+"""AI 舆情分析流式 API — 实现方案约定路径"""
+
+from fastapi import APIRouter
+from fastapi.responses import StreamingResponse
+
+from app.api.agent_api import AnalysisRequest, iter_sentiment_analysis_ndjson
+
+router = APIRouter(prefix="/sentiment", tags=["AI舆情分析"])
+
+
+@router.post("/analyze/stream")
+async def sentiment_analyze_stream(body: AnalysisRequest):
+    """POST /api/v1/sentiment/analyze/stream"""
+    return StreamingResponse(
+        iter_sentiment_analysis_ndjson(body.stock_code, body.stock_name),
+        media_type="application/x-ndjson",
+        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
+    )

+ 145 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/api/simulation.py

@@ -0,0 +1,145 @@
+"""
+智能股票分析助手 — 模拟交易API路由
+
+提供模拟持仓查询、资金查询、委托下单、撤单等接口。
+"""
+
+from typing import Optional
+from fastapi import APIRouter, Query
+from pydantic import BaseModel, Field
+from app.services import simulation_service
+from app.utils.response import success_response, error_response
+
+router = APIRouter(prefix="/simulation", tags=["模拟交易"])
+
+
+class PlaceOrderRequest(BaseModel):
+    """下单请求"""
+    trade_type: str = Field(..., description="交易类型: buy(买入) / sell(卖出)", pattern="^(buy|sell)$")
+    stock_code: str = Field(..., description="6位股票代码", min_length=6, max_length=6)
+    quantity: int = Field(..., description="委托数量(100的整数倍)", gt=0)
+    price: Optional[float] = Field(default=None, description="委托价格(不填则为市价委托)")
+
+
+class CancelOrderRequest(BaseModel):
+    """撤单请求"""
+    order_id: str = Field(..., description="委托编号", min_length=1)
+    stock_code: str = Field(default="", description="股票代码(可选)")
+
+
+@router.get("/portfolio")
+async def get_portfolio():
+    """查询模拟持仓
+
+    返回当前模拟账户下所有持仓股票及其盈亏信息。
+    """
+    result = simulation_service.get_positions()
+    if not result["success"]:
+        return error_response(code=500, message=result.get("error", "查询持仓失败"))
+
+    return success_response(
+        data={
+            "positions": result["positions"],
+            "total": result["total"],
+        },
+        message=f"共 {result['total']} 只持仓股票",
+    )
+
+
+@router.get("/funds")
+async def get_funds():
+    """查询模拟账户资金
+
+    返回总资产、可用资金、冻结资金、持仓市值、累计盈亏等信息。
+    """
+    result = simulation_service.get_balance()
+    if not result["success"]:
+        return error_response(code=500, message=result.get("error", "查询资金失败"))
+
+    return success_response(
+        data=result["balance"],
+        message="资金查询成功",
+    )
+
+
+@router.get("/orders")
+async def get_orders():
+    """查询委托记录
+
+    返回历史委托订单列表(包含已成交、未成交、已撤销等状态)。
+    """
+    result = simulation_service.get_orders()
+    if not result["success"]:
+        return error_response(code=500, message=result.get("error", "查询委托失败"))
+
+    return success_response(
+        data={
+            "orders": result["orders"],
+            "total": result["total"],
+        },
+        message=f"共 {result['total']} 条委托记录",
+    )
+
+
+@router.post("/order")
+async def place_order(body: PlaceOrderRequest):
+    """模拟下单(买入/卖出)
+
+    提交模拟交易委托,支持限价委托和市价委托。
+
+    - **trade_type**: buy=买入, sell=卖出
+    - **stock_code**: 6位股票代码,如 600519
+    - **quantity**: 委托数量,必须为100的整数倍
+    - **price**: 委托价格(不填=市价委托)
+    """
+    result = simulation_service.place_order(
+        trade_type=body.trade_type,
+        stock_code=body.stock_code,
+        quantity=body.quantity,
+        price=body.price,
+    )
+    if not result["success"]:
+        return error_response(code=500, message=result.get("error", "下单失败"))
+
+    return success_response(
+        data={
+            "order_id": result["order_id"],
+            "trade_type": body.trade_type,
+            "stock_code": body.stock_code,
+            "quantity": body.quantity,
+            "price": body.price,
+        },
+        message=result["message"],
+    )
+
+
+@router.delete("/order/{order_id}")
+async def cancel_order(
+    order_id: str,
+    stock_code: str = Query(default="", description="股票代码(可选)"),
+):
+    """撤销指定委托
+
+    根据委托编号撤销未成交的委托订单。
+
+    - **order_id**: 委托编号(如 260854300000078983)
+    - **stock_code**: 股票代码(可选)
+    """
+    result = simulation_service.cancel_order(order_id, stock_code)
+    if not result["success"]:
+        return error_response(code=500, message=result.get("error", "撤单失败"))
+
+    return success_response(data={"order_id": order_id}, message=result["message"])
+
+
+@router.post("/cancel-all")
+async def cancel_all_orders():
+    """一键撤单
+
+    撤销当前账户下所有未成交的委托订单。
+    """
+    result = simulation_service.cancel_all_orders()
+    if not result["success"]:
+        return error_response(code=500, message=result.get("error", "一键撤单失败"))
+
+    return success_response(data={}, message=result["message"])

+ 45 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/api/system_browser.py

@@ -0,0 +1,45 @@
+"""
+桌面 / exe 场景:由后端唤起系统默认浏览器打开外链。
+
+部分环境下前端 window.open 会被拦截;与 run_exe.py 中 webbrowser.open 打开仪表盘一致。
+"""
+
+from __future__ import annotations
+
+import asyncio
+import webbrowser
+from urllib.parse import urlparse
+
+from fastapi import APIRouter
+from pydantic import BaseModel, Field
+
+from app.utils.response import error_response, success_response
+
+router = APIRouter(prefix="/system", tags=["系统"])
+
+
+class OpenExternalUrlBody(BaseModel):
+    url: str = Field(..., min_length=8, max_length=4096)
+
+
+def _normalize_http_url(url: str) -> str | None:
+    s = url.strip()
+    if len(s) > 4096:
+        return None
+    parsed = urlparse(s)
+    if parsed.scheme not in ("http", "https"):
+        return None
+    if not parsed.netloc:
+        return None
+    return s
+
+
+@router.post("/open-external-url")
+async def open_external_url(body: OpenExternalUrlBody):
+    """在本机默认浏览器中打开 http(s) 链接。"""
+    ok_url = _normalize_http_url(body.url)
+    if not ok_url:
+        return error_response(400, "仅允许有效的 http(s) 外链")
+
+    opened = await asyncio.to_thread(webbrowser.open, ok_url)
+    return success_response(data={"opened": bool(opened)})

+ 77 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/api/watchlist.py

@@ -0,0 +1,77 @@
+"""
+智能股票分析助手 — 自选股管理API路由
+
+提供自选股查询、添加、删除接口。
+"""
+
+from fastapi import APIRouter, Query
+from pydantic import BaseModel, Field
+from app.services import watchlist_service
+from app.utils.response import success_response, error_response
+
+router = APIRouter(prefix="/watchlist", tags=["自选股管理"])
+
+
+class WatchlistAddRequest(BaseModel):
+    """添加自选股请求"""
+    stock: str = Field(..., description="股票名称或代码,如'贵州茅台'或'600519'", min_length=1)
+
+
+class WatchlistDeleteRequest(BaseModel):
+    """删除自选股请求"""
+    stock: str = Field(..., description="股票名称或代码,如'贵州茅台'或'600519'", min_length=1)
+
+
+@router.get("/")
+async def get_watchlist():
+    """查询自选股列表
+
+    返回当前账户下的所有自选股及其行情数据。
+    """
+    result = watchlist_service.get_watchlist()
+    if not result["success"]:
+        return error_response(code=500, message=result.get("error", "查询自选股失败"))
+
+    return success_response(
+        data={
+            "stocks": result["stocks"],
+            "total": result["total"],
+        },
+        message=f"共 {result['total']} 只自选股",
+    )
+
+
+@router.post("/")
+async def add_watchlist(body: WatchlistAddRequest):
+    """添加自选股
+
+    将指定股票添加到自选股列表。
+
+    - **stock**: 股票名称或6位代码,如'贵州茅台'、'600519'
+    """
+    if not body.stock or not body.stock.strip():
+        return error_response(code=400, message="请输入股票名称或代码")
+
+    result = watchlist_service.add_to_watchlist(body.stock.strip())
+    if not result["success"]:
+        return error_response(code=500, message=result.get("error", "添加自选股失败"))
+
+    return success_response(data=result, message=result["message"])
+
+
+@router.delete("/{stock}")
+async def delete_watchlist(stock: str):
+    """删除自选股
+
+    将指定股票从自选股列表中移除。
+
+    - **stock**: 股票名称或6位代码,如'贵州茅台'、'600519'
+    """
+    if not stock or not stock.strip():
+        return error_response(code=400, message="请输入股票名称或代码")
+
+    result = watchlist_service.delete_from_watchlist(stock.strip())
+    if not result["success"]:
+        return error_response(code=500, message=result.get("error", "删除自选股失败"))
+
+    return success_response(data=result, message=result["message"])

+ 175 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/config.py

@@ -0,0 +1,175 @@
+"""
+智能股票分析助手 — 配置管理模块
+
+集中管理所有配置项,支持从环境变量(.env)加载。
+参考 HelloAgents 框架的配置模式,参数优先级:构造函数参数 > 环境变量 > 默认值
+"""
+
+import os
+import sys
+from pathlib import Path
+from typing import Optional
+from dotenv import load_dotenv
+
+# 检测是否为 PyInstaller 打包的 exe 运行环境
+IS_FROZEN = getattr(sys, 'frozen', False)
+
+if IS_FROZEN:
+    # exe 内部资源目录(PyInstaller 临时解压目录)
+    _BUNDLE_DIR = Path(getattr(sys, '_MEIPASS', Path(sys.executable).parent))
+    # exe 外部目录(用户配置和数据文件放在 exe 旁边)
+    _EXTERNAL_DIR = Path(sys.executable).parent
+else:
+    # 开发模式:config.py -> app/ -> backend/ -> (项目根目录)
+    _BUNDLE_DIR = Path(__file__).parent.parent.parent
+    _EXTERNAL_DIR = _BUNDLE_DIR
+
+_PROJECT_ROOT = _BUNDLE_DIR
+
+# 加载 .env(优先从 exe 外部目录加载,开发模式从项目根加载)
+# exe 模式下必须用 override=True:否则系统/父进程里已存在的 MX_APIKEY(含空字符串)会阻止读取 exe 旁 .env,表现为「已换 Key 仍提示额度用尽且无缓存」
+_env_path = _EXTERNAL_DIR / ".env"
+if _env_path.exists():
+    load_dotenv(_env_path, override=IS_FROZEN)
+else:
+    load_dotenv(_PROJECT_ROOT / ".env", override=False)
+
+# exe 默认与自动打开的浏览器地址一致(127.0.0.1:5174/dashboard);开发模式仍为 8000
+_DEFAULT_BACKEND_PORT = "5174" if IS_FROZEN else "8000"
+
+
+class Settings:
+    """全局配置单例"""
+
+    # =========================================================================
+    # LLM 大模型配置
+    # =========================================================================
+    LLM_MODEL_ID: str = os.getenv("LLM_MODEL_ID", "deepseek-chat")
+    LLM_API_KEY: str = (os.getenv("LLM_API_KEY") or "").strip()
+    LLM_BASE_URL: str = os.getenv("LLM_BASE_URL", "https://api.deepseek.com")
+    LLM_TIMEOUT: int = int(os.getenv("LLM_TIMEOUT", "60"))
+
+    # =========================================================================
+    # 东方财富妙想API配置
+    # =========================================================================
+    MX_APIKEY: str = (os.getenv("MX_APIKEY") or "").strip()
+    MX_API_URL: str = os.getenv("MX_API_URL", "https://mkapi2.dfcfs.com/finskillshub")
+    # 妙想查询缓存 TTL(秒):行情/指数/资讯/自选股列表等在 TTL 内不走远端妙想;≤0 表示不读缓存但仍写入供额度用尽降级
+    # 前端仪表盘 localStorage 默认按 600s(10 分钟)对齐;若改大 TTL,宜同步设置 VITE_DASHBOARD_CACHE_MS(毫秒)
+    MX_CACHE_TTL_SECONDS: float = float(os.getenv("MX_CACHE_TTL_SECONDS", "600"))
+    # 为 True 时:若 backend/fixtures/mx_raw 下存在对应 query 的原始 JSON,则不调妙想 HTTP(省额度,便于本地修 bug)
+    MX_REPLAY_FIXTURES: bool = os.getenv("MX_REPLAY_FIXTURES", "").lower() in ("1", "true", "yes")
+    _mx_fix = os.getenv("MX_FIXTURE_DIR")
+    MX_FIXTURE_DIR: Path = (
+        Path(_mx_fix).expanduser().resolve()
+        if _mx_fix
+        else (_PROJECT_ROOT / "backend" / "fixtures" / "mx_raw")
+    )
+
+    # =========================================================================
+    # 项目服务配置
+    # =========================================================================
+    BACKEND_HOST: str = os.getenv("BACKEND_HOST", "0.0.0.0")
+    BACKEND_PORT: int = int(os.getenv("BACKEND_PORT", _DEFAULT_BACKEND_PORT))
+    BACKEND_DEBUG: bool = os.getenv("BACKEND_DEBUG", "true").lower() == "true"
+    FRONTEND_PORT: int = int(os.getenv("FRONTEND_PORT", "5173"))
+
+    # =========================================================================
+    # 数据库配置
+    # =========================================================================
+    DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./data/stock_analyzer.db")
+    @property
+    def DATA_DIR(self) -> Path:
+        """数据目录(exe 模式下在 exe 旁边)"""
+        _dd = os.getenv("DATA_DIR")
+        if _dd:
+            return Path(_dd)
+        return _EXTERNAL_DIR / "data"
+
+    # =========================================================================
+    # Redis 配置
+    # =========================================================================
+    REDIS_HOST: str = os.getenv("REDIS_HOST", "localhost")
+    REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
+    REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
+    REDIS_PASSWORD: Optional[str] = os.getenv("REDIS_PASSWORD", None)
+
+    # =========================================================================
+    # JWT 配置
+    # =========================================================================
+    JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", "dev-secret-key")
+    JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256")
+    JWT_EXPIRE_MINUTES: int = int(os.getenv("JWT_EXPIRE_MINUTES", "1440"))
+
+    # =========================================================================
+    # 项目路径(自动计算)
+    # =========================================================================
+    @property
+    def PROJECT_ROOT(self) -> Path:
+        return _PROJECT_ROOT
+
+    @property
+    def EXTERNAL_DIR(self) -> Path:
+        """exe 外部目录(用户数据、配置存放位置)"""
+        return _EXTERNAL_DIR
+
+    @property
+    def BUNDLE_DIR(self) -> Path:
+        """打包内部资源目录(exe 内嵌文件所在)"""
+        return _BUNDLE_DIR
+
+    @property
+    def FRONTEND_DIR(self) -> Path:
+        """前端静态文件目录"""
+        # 优先从环境变量指定(开发模式 vite proxy 之外的其他场景)
+        _fd = os.getenv("FRONTEND_DIR")
+        if _fd:
+            return Path(_fd)
+        # exe 模式:前端 dist 内嵌在 bundle 中
+        if IS_FROZEN:
+            return _BUNDLE_DIR / "frontend" / "dist"
+        # 开发模式:从项目根找 frontend/dist
+        dist = _PROJECT_ROOT / "frontend" / "dist"
+        if dist.exists():
+            return dist
+        return _PROJECT_ROOT / "frontend" / "dist"
+
+    @property
+    def BACKEND_DIR(self) -> Path:
+        return _PROJECT_ROOT / "backend"
+
+    @property
+    def AGENTS_DIR(self) -> Path:
+        return _PROJECT_ROOT / "agents"
+
+    @property
+    def SKILLS_DIR(self) -> Path:
+        return _PROJECT_ROOT / "skills"
+
+    @property
+    def HELLO_AGENTS_DIR(self) -> Path:
+        return _PROJECT_ROOT / "HelloAgents Optimized"
+
+    # =========================================================================
+    # 验证方法
+    # =========================================================================
+    def validate(self) -> list[str]:
+        """验证关键配置项,返回缺失配置列表"""
+        warnings = []
+        if not self.LLM_API_KEY or self.LLM_API_KEY == "your-api-key-here":
+            warnings.append("LLM_API_KEY 未配置,智能体功能将不可用")
+        if not self.MX_APIKEY or self.MX_APIKEY == "your-mx-apikey-here":
+            warnings.append("MX_APIKEY 未配置,外部金融数据服务将不可用")
+        return warnings
+
+    def is_agent_ready(self) -> bool:
+        """检查智能体层是否就绪"""
+        return bool(self.LLM_API_KEY and self.LLM_API_KEY != "your-api-key-here")
+
+    def is_skills_ready(self) -> bool:
+        """检查外部服务层是否就绪"""
+        return bool(self.MX_APIKEY and self.MX_APIKEY != "your-mx-apikey-here")
+
+
+# 全局配置单例
+settings = Settings()

+ 229 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/main.py

@@ -0,0 +1,229 @@
+"""
+智能股票分析助手 — FastAPI 应用入口
+
+启动方法(从项目根目录执行):
+    python -m uvicorn backend.app.main:app --host 0.0.0.0 --port 8000 --reload
+
+exe 打包后直接运行:
+    stock_analyzer.exe
+    浏览器访问 http://127.0.0.1:5174/dashboard(端口以 exe 旁 .env 中 BACKEND_PORT 为准)
+"""
+
+import sys
+import io
+from pathlib import Path
+
+# Windows 控制台编码修复:强制使用 UTF-8 输出,避免 emoji/中文打印报错
+if sys.stdout.encoding != 'utf-8':
+    try:
+        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
+        sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+    except Exception:
+        pass
+
+# 将项目关键目录加入sys.path,确保各类导入正常工作
+# main.py 位于 backend/app/main.py,需要向上3级到项目根目录
+_PROJECT_ROOT = Path(__file__).parent.parent.parent
+_BACKEND_DIR = _PROJECT_ROOT / "backend"             # 使 from app.xxx 导入生效
+_AGENTS_DIR = _PROJECT_ROOT / "agents"               # 使 from agents.xxx 导入生效
+_HELLO_DIR = _PROJECT_ROOT / "HelloAgents Optimized"  # 使 from hello_agents 导入生效
+
+for p in [_BACKEND_DIR, _PROJECT_ROOT, _AGENTS_DIR, _HELLO_DIR]:
+    if str(p) not in sys.path:
+        sys.path.insert(0, str(p))
+
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.staticfiles import StaticFiles
+from fastapi.responses import FileResponse
+import asyncio
+from contextlib import asynccontextmanager
+
+from app.config import settings
+from app.utils.response import success_response, error_response
+from app.models.database import init_db, close_db
+from app.models.report import AnalysisReport  # noqa: F401 — 确保数据库初始化时创建表
+from app.models.history_models import AnalysisHistory  # noqa: F401 — 确保历史记录表创建
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+    """应用生命周期管理"""
+    # 启动时执行
+    print(f"[后端] 智能股票分析助手启动中...")
+    warnings = settings.validate()
+    if warnings:
+        print("[后端] ⚠️ 配置警告:")
+        for w in warnings:
+            print(f"  - {w}")
+    else:
+        print("[后端] ✅ 配置验证通过")
+    print(f"[后端] 服务地址: http://{settings.BACKEND_HOST}:{settings.BACKEND_PORT}")
+
+    # 初始化数据库(创建表)
+    await init_db()
+    print("[后端] ✅ 数据库初始化完成")
+
+    # 后台预热仪表盘所需妙想缓存(不阻塞本 worker 接受请求)
+    async def _warm_dashboard_cache_bg() -> None:
+        try:
+            from app.services.dashboard_warmup import warm_dashboard_cache
+
+            await asyncio.to_thread(warm_dashboard_cache)
+            print("[后端] ✅ 仪表盘数据预热已完成(进程内妙想缓存已填充)")
+        except Exception as exc:
+            print(f"[后端] ⚠️ 仪表盘预热未完成(可忽略): {exc}")
+
+    asyncio.create_task(_warm_dashboard_cache_bg())
+
+    yield
+    # 关闭时执行
+    await close_db()
+    print("[后端] 服务关闭")
+
+
+# 创建FastAPI应用实例
+app = FastAPI(
+    title="智能股票分析助手",
+    description="基于多智能体架构的A股投资分析工具API",
+    version="0.1.0",
+    lifespan=lifespan,
+)
+
+# 是否托管 Vue 构建产物(exe 一体化或设置 FRONTEND_DIR 且存在 dist)
+_FRONTEND_DIR = settings.FRONTEND_DIR
+_SERVE_FRONTEND = _FRONTEND_DIR.exists() and (_FRONTEND_DIR / "index.html").exists()
+# SPA 入口禁用强缓存:避免升级 exe 后浏览器仍用旧 index 引用已过期的 hash chunk
+_SPA_INDEX_HEADERS = {"Cache-Control": "no-store, no-cache, must-revalidate", "Pragma": "no-cache"}
+
+# =========================================================================
+# CORS 跨域中间件(允许前端Vue3开发服务器访问)
+# =========================================================================
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=[
+        f"http://localhost:{settings.FRONTEND_PORT}",
+        "http://127.0.0.1:5173",
+        f"http://127.0.0.1:{settings.BACKEND_PORT}",
+        "*",  # 开发阶段允许所有来源
+    ],
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+
+# =========================================================================
+# 系统路由
+# =========================================================================
+@app.get("/api/v1/system/health", tags=["系统"])
+async def health_check():
+    """健康检查接口"""
+    return success_response(
+        data={
+            "status": "ok",
+            "version": "0.1.0",
+            "agent_ready": settings.is_agent_ready(),
+            "skills_ready": settings.is_skills_ready(),
+        }
+    )
+
+
+@app.get("/api/v1/system/config", tags=["系统"])
+async def system_config():
+    """获取系统配置(公开信息,不包含密钥)"""
+    return success_response(
+        data={
+            "llm_model": settings.LLM_MODEL_ID,
+            "agent_ready": settings.is_agent_ready(),
+            "skills_ready": settings.is_skills_ready(),
+            "frontend_port": settings.FRONTEND_PORT,
+        }
+    )
+
+
+@app.get("/", tags=["系统"])
+async def root():
+    """根路径:托管前端时返回 index.html(与 vite dev 一致);否则返回 API 说明"""
+    if _SERVE_FRONTEND:
+        return FileResponse(str(_FRONTEND_DIR / "index.html"), headers=dict(_SPA_INDEX_HEADERS))
+    return {"message": "智能股票分析助手 API", "docs": "/docs"}
+
+
+# =========================================================================
+# 注册子路由
+# =========================================================================
+from app.api.preferences import router as preferences_router
+from app.api.market import router as market_router
+from app.api.financial import router as financial_router
+from app.api.news import router as news_router
+from app.api.screener import router as screener_router
+from app.api.analysis import router as analysis_router
+from app.api.watchlist import router as watchlist_router
+from app.api.buffett import router as buffett_router
+from app.api.simulation import router as simulation_router
+from app.api.chat import router as chat_router
+from app.api.history import router as history_router
+from app.api.agent_api import router as agent_router
+from app.api.sentiment import router as sentiment_router
+from app.api.data_analysis import router as data_analysis_router
+from app.api.cache_api import router as cache_router
+from app.api.system_browser import router as system_browser_router
+
+app.include_router(preferences_router, prefix="/api/v1")
+app.include_router(market_router, prefix="/api/v1")
+app.include_router(financial_router, prefix="/api/v1")
+app.include_router(news_router, prefix="/api/v1")
+app.include_router(screener_router, prefix="/api/v1")
+app.include_router(analysis_router, prefix="/api/v1")
+app.include_router(watchlist_router, prefix="/api/v1")
+app.include_router(buffett_router, prefix="/api/v1")
+app.include_router(simulation_router, prefix="/api/v1")
+app.include_router(chat_router, prefix="/api/v1")
+app.include_router(history_router, prefix="/api/v1")
+app.include_router(agent_router, prefix="/api/v1")
+app.include_router(sentiment_router, prefix="/api/v1")
+app.include_router(data_analysis_router, prefix="/api/v1")
+app.include_router(cache_router, prefix="/api/v1")
+app.include_router(system_browser_router, prefix="/api/v1")
+
+# =========================================================================
+# 前端静态文件服务(exe 模式或 FRONTEND_DIR 指定时启用)
+# =========================================================================
+if _SERVE_FRONTEND:
+    # 挂载 assets 等静态资源(文件名带 content hash,可由浏览器长期缓存)
+    _assets_dir = _FRONTEND_DIR / "assets"
+    if _assets_dir.exists():
+        app.mount("/assets", StaticFiles(directory=str(_assets_dir)), name="frontend_assets")
+
+    # SPA 回退:非 /api 路径返回 index.html(含禁止缓存响应头,见 _SPA_INDEX_HEADERS)
+    @app.get("/{full_path:path}", tags=["前端"])
+    async def serve_spa(full_path: str = ""):
+        fp = _FRONTEND_DIR / full_path
+        if full_path and fp.exists() and fp.is_file():
+            return FileResponse(str(fp))
+        return FileResponse(str(_FRONTEND_DIR / "index.html"), headers=dict(_SPA_INDEX_HEADERS))
+
+
+# =========================================================================
+# exe 独立入口
+# =========================================================================
+def start_server(host: str = None, port: int = None):
+    """启动 uvicorn 服务器(供 exe 入口调用)"""
+    import uvicorn
+    uvicorn.run(
+        app,
+        host=host or settings.BACKEND_HOST,
+        port=port or settings.BACKEND_PORT,
+        log_level="info",
+    )
+
+
+if __name__ == "__main__":
+    import uvicorn
+    uvicorn.run(
+        "backend.app.main:app",
+        host=settings.BACKEND_HOST,
+        port=settings.BACKEND_PORT,
+        reload=settings.BACKEND_DEBUG,
+    )

+ 1 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/middleware/__init__.py

@@ -0,0 +1 @@
+# 中间件层

+ 11 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/middleware/error_handler.py

@@ -0,0 +1,11 @@
+"""
+智能股票分析助手 — 中间件模块
+
+提供请求处理管道中的横切关注点:
+- 错误处理中间件
+- 频率限制中间件
+- 请求日志记录
+"""
+
+# 后续根据需求逐步添加具体中间件实现
+# 当前仅保留骨架,供后续功能模块填充

+ 1 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/models/__init__.py

@@ -0,0 +1 @@
+# 数据模型层

+ 67 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/models/database.py

@@ -0,0 +1,67 @@
+"""
+智能股票分析助手 — 数据库连接模块
+
+使用SQLAlchemy + aiosqlite实现异步数据库访问。
+数据库文件自动创建在项目data目录下。
+"""
+
+from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
+from sqlalchemy.orm import DeclarativeBase
+from pathlib import Path
+import sys
+
+# 确保能导入配置模块
+_PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
+sys.path.insert(0, str(_PROJECT_ROOT / "backend"))
+
+from app.config import settings
+
+
+# 将SQLite URL转换为异步版本(aiosqlite)
+def _build_async_url(url: str) -> str:
+    """将 sqlite:/// 格式转为 sqlite+aiosqlite:/// 格式"""
+    if url.startswith("sqlite:///"):
+        return url.replace("sqlite:///", "sqlite+aiosqlite:///")
+    return url
+
+
+# 确保数据目录存在
+settings.DATA_DIR.mkdir(parents=True, exist_ok=True)
+
+# 创建异步引擎
+engine = create_async_engine(
+    _build_async_url(settings.DATABASE_URL),
+    echo=False,  # 开发时可设为True查看SQL日志
+)
+
+# 创建异步会话工厂
+async_session_factory = async_sessionmaker(
+    engine,
+    class_=AsyncSession,
+    expire_on_commit=False,
+)
+
+
+# SQLAlchemy声明式基类
+class Base(DeclarativeBase):
+    pass
+
+
+async def init_db():
+    """初始化数据库,创建所有表"""
+    async with engine.begin() as conn:
+        await conn.run_sync(Base.metadata.create_all)
+
+
+async def get_db_session() -> AsyncSession:
+    """获取数据库会话(FastAPI依赖注入用)"""
+    async with async_session_factory() as session:
+        try:
+            yield session
+        finally:
+            await session.close()
+
+
+async def close_db():
+    """关闭数据库连接"""
+    await engine.dispose()

+ 6 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/models/history.py

@@ -0,0 +1,6 @@
+"""
+兼容入口:分析历史 ORM 定义见 history_models.py
+"""
+from app.models.history_models import AnalysisHistory
+
+__all__ = ["AnalysisHistory"]

+ 35 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/models/history_models.py

@@ -0,0 +1,35 @@
+"""
+分析历史记录 ORM 模型(实现方案约定文件名)
+"""
+from sqlalchemy import Column, Integer, String, Text, DateTime, func
+
+from app.models.database import Base
+
+
+class AnalysisHistory(Base):
+    """分析历史记录表 — 按天存储各类分析报告"""
+
+    __tablename__ = "analysis_history"
+
+    id = Column(Integer, primary_key=True, autoincrement=True, comment="记录ID")
+    user_id = Column(String(64), nullable=False, default="default", comment="用户标识")
+    date = Column(String(16), nullable=False, comment="日期 yyyy-mm-dd")
+    type = Column(String(32), nullable=False, comment="类型: sentiment/data_analysis/buffett/chat")
+    stock_code = Column(String(16), nullable=True, comment="股票代码")
+    stock_name = Column(String(64), nullable=True, comment="股票名称")
+    title = Column(String(256), nullable=True, comment="标题")
+    content = Column(Text, nullable=False, comment="内容(Markdown格式)")
+    created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
+
+    def to_dict(self) -> dict:
+        return {
+            "id": self.id,
+            "user_id": self.user_id,
+            "date": self.date,
+            "type": self.type,
+            "stock_code": self.stock_code,
+            "stock_name": self.stock_name,
+            "title": self.title,
+            "content": self.content,
+            "created_at": self.created_at.isoformat() if self.created_at else None,
+        }

+ 46 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/models/memory_models.py

@@ -0,0 +1,46 @@
+"""
+记忆系统数据模型 — 单日仪表盘快照(指数 / 自选 / 热点资讯)
+"""
+from __future__ import annotations
+
+from datetime import date, datetime
+from typing import Optional
+
+
+class MemorySnapshot:
+    """单日仪表盘数据快照"""
+
+    def __init__(
+        self,
+        date_str: str = "",
+        indices: Optional[list] = None,
+        watchlist: Optional[dict] = None,
+        hot_news: Optional[dict] = None,
+        watchlist_count: int = 0,
+    ):
+        self.date_str = date_str or date.today().isoformat()
+        self.indices = indices or []
+        self.watchlist = watchlist or {}
+        self.hot_news = hot_news or {}
+        self.watchlist_count = watchlist_count
+        self.created_at = datetime.now().isoformat()
+
+    def to_dict(self) -> dict:
+        return {
+            "date_str": self.date_str,
+            "indices": self.indices,
+            "watchlist": self.watchlist,
+            "hot_news": self.hot_news,
+            "watchlist_count": self.watchlist_count,
+            "created_at": self.created_at,
+        }
+
+    @classmethod
+    def from_dict(cls, d: dict) -> MemorySnapshot:
+        return cls(
+            date_str=d.get("date_str", ""),
+            indices=d.get("indices", []),
+            watchlist=d.get("watchlist", {}),
+            hot_news=d.get("hot_news", {}),
+            watchlist_count=d.get("watchlist_count", 0),
+        )

+ 144 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/models/preference.py

@@ -0,0 +1,144 @@
+"""
+智能股票分析助手 — 用户偏好数据模型
+
+定义用户投资偏好、风控参数、界面偏好等持久化存储结构。
+"""
+
+from sqlalchemy import Column, Integer, String, Float, Text, DateTime, Boolean, func
+from app.models.database import Base
+
+
+class UserPreference(Base):
+    """用户偏好模型"""
+
+    __tablename__ = "user_preferences"
+
+    # 主键
+    id = Column(Integer, primary_key=True, autoincrement=True)
+
+    # 关联用户(当前简化:单用户模式,后续可扩展多用户)
+    user_id = Column(String(64), default="default", unique=True, nullable=False, comment="用户ID")
+
+    # =========================================================================
+    # 投资偏好
+    # =========================================================================
+    risk_tolerance = Column(
+        String(20), default="moderate", nullable=False,
+        comment="风险承受度: conservative / moderate / aggressive"
+    )
+    investment_style = Column(
+        String(20), default="blend", nullable=False,
+        comment="投资风格: value / growth / momentum / dividend / blend"
+    )
+    preferred_sectors = Column(
+        Text, default="[]",
+        comment="偏好行业 (JSON数组)"
+    )
+    excluded_sectors = Column(
+        Text, default="[]",
+        comment="排除行业 (JSON数组)"
+    )
+    investment_horizon = Column(
+        String(20), default="medium",
+        comment="投资期限: short / medium / long"
+    )
+    target_return_rate = Column(
+        Float, default=10.0, nullable=False,
+        comment="目标年化收益率(%)"
+    )
+
+    # =========================================================================
+    # 风控参数
+    # =========================================================================
+    max_position_ratio = Column(
+        Float, default=30.0, nullable=False,
+        comment="单只股票最大仓位比例(%)"
+    )
+    max_drawdown_limit = Column(
+        Float, default=-15.0, nullable=False,
+        comment="最大回撤预警线(%)"
+    )
+
+    # =========================================================================
+    # 通知设置
+    # =========================================================================
+    notification_enabled = Column(
+        Boolean, default=True, nullable=False,
+        comment="是否启用通知"
+    )
+    notification_channels = Column(
+        Text, default='["push"]',
+        comment="通知渠道 (JSON: email/sms/push)"
+    )
+    market_alert_threshold = Column(
+        Float, default=5.0, nullable=False,
+        comment="行情异动提醒阈值(%)"
+    )
+
+    # =========================================================================
+    # 界面偏好
+    # =========================================================================
+    language = Column(
+        String(10), default="zh", nullable=False,
+        comment="界面语言: zh / en"
+    )
+    theme = Column(
+        String(10), default="auto", nullable=False,
+        comment="主题: light / dark / auto"
+    )
+    default_view = Column(
+        String(20), default="dashboard", nullable=False,
+        comment="默认首页: dashboard / watchlist"
+    )
+
+    # =========================================================================
+    # 时间戳
+    # =========================================================================
+    created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
+    updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
+
+    def to_dict(self) -> dict:
+        """转为字典(用于API响应)"""
+        import json
+
+        return {
+            "id": self.id,
+            "user_id": self.user_id,
+            "risk_tolerance": self.risk_tolerance,
+            "investment_style": self.investment_style,
+            "preferred_sectors": json.loads(self.preferred_sectors),
+            "excluded_sectors": json.loads(self.excluded_sectors),
+            "investment_horizon": self.investment_horizon,
+            "target_return_rate": self.target_return_rate,
+            "max_position_ratio": self.max_position_ratio,
+            "max_drawdown_limit": self.max_drawdown_limit,
+            "notification_enabled": self.notification_enabled,
+            "notification_channels": json.loads(self.notification_channels),
+            "market_alert_threshold": self.market_alert_threshold,
+            "language": self.language,
+            "theme": self.theme,
+            "default_view": self.default_view,
+            "created_at": self.created_at.isoformat() if self.created_at else None,
+            "updated_at": self.updated_at.isoformat() if self.updated_at else None,
+        }
+
+    @classmethod
+    def create_default(cls, user_id: str = "default") -> "UserPreference":
+        """创建默认偏好实例"""
+        return cls(
+            user_id=user_id,
+            risk_tolerance="moderate",
+            investment_style="blend",
+            preferred_sectors="[]",
+            excluded_sectors="[]",
+            investment_horizon="medium",
+            target_return_rate=10.0,
+            max_position_ratio=30.0,
+            max_drawdown_limit=-15.0,
+            notification_enabled=True,
+            notification_channels='["push"]',
+            market_alert_threshold=5.0,
+            language="zh",
+            theme="auto",
+            default_view="dashboard",
+        )

+ 37 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/models/report.py

@@ -0,0 +1,37 @@
+"""
+智能股票分析助手 — 分析报告数据模型
+
+存储个股深度分析报告,支持报告持久化与历史查询。
+"""
+
+from sqlalchemy import Column, Integer, String, Text, DateTime, func
+from app.models.database import Base
+
+
+class AnalysisReport(Base):
+    """分析报告表"""
+
+    __tablename__ = "analysis_reports"
+
+    id = Column(Integer, primary_key=True, autoincrement=True, comment="报告ID")
+    user_id = Column(String(64), nullable=False, default="default", comment="用户标识")
+    stock_code = Column(String(16), nullable=False, comment="股票代码")
+    stock_name = Column(String(64), default="", comment="股票名称")
+    report_type = Column(String(32), default="full", comment="报告类型: full=完整分析, quick=快速概览")
+    summary = Column(String(1024), default="", comment="报告摘要(投资建议一句话)")
+    content = Column(Text, default="", comment="报告完整内容(Markdown格式)")
+    data_snapshot = Column(Text, default="{}", comment="数据快照(JSON格式,存储查询到的原始数据)")
+    created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
+
+    def to_dict(self) -> dict:
+        """转换为字典"""
+        return {
+            "id": self.id,
+            "user_id": self.user_id,
+            "stock_code": self.stock_code,
+            "stock_name": self.stock_name,
+            "report_type": self.report_type,
+            "summary": self.summary,
+            "content": self.content,
+            "created_at": self.created_at.isoformat() if self.created_at else None,
+        }

+ 1 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/services/__init__.py

@@ -0,0 +1 @@
+# 业务逻辑层

+ 361 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/services/analysis_service.py

@@ -0,0 +1,361 @@
+"""
+智能股票分析助手 — 分析报告服务层
+
+协调多数据源(行情、财务、资讯),生成个股深度分析报告。
+支持报告持久化存储与历史查询。
+"""
+
+import sys
+import json
+from pathlib import Path
+from typing import Optional
+from datetime import datetime
+
+_PROJECT_ROOT = Path(__file__).parent.parent.parent.parent  # backend/app/services -> project root
+_BACKEND_DIR = _PROJECT_ROOT
+for p in [str(_PROJECT_ROOT), str(_BACKEND_DIR)]:
+    if str(p) not in sys.path:
+        sys.path.insert(0, str(p))
+
+from app.models.database import async_session_factory
+from app.models.report import AnalysisReport
+from app.services.market_service import get_stock_quote, get_stock_financial, get_stock_profile
+from app.services.news_service import analyze_sentiment
+
+
+async def generate_analysis_report(
+    stock_code: str,
+    user_id: str = "default",
+    report_type: str = "full",
+) -> dict:
+    """生成个股深度分析报告
+
+    收集行情数据、财务数据、公司概况、舆情信息,整合为结构化分析报告。
+
+    Args:
+        stock_code: 6位股票代码
+        user_id: 用户标识
+        report_type: 报告类型 full/quick
+
+    Returns:
+        {
+            "success": True/False,
+            "report": { ... } or None,
+            "error": str or None,
+            "data_collected": dict  # 各数据源的收集状态
+        }
+    """
+    result = {
+        "success": False,
+        "report": None,
+        "error": None,
+        "data_collected": {},
+    }
+
+    try:
+        # 阶段1: 收集行情数据
+        quote_data = get_stock_quote(stock_code)
+        result["data_collected"]["quote"] = quote_data["success"]
+
+        # 阶段2: 收集财务数据
+        financial_data = get_stock_financial(stock_code)
+        result["data_collected"]["financial"] = financial_data["success"]
+
+        # 阶段3: 收集公司概况
+        profile_data = get_stock_profile(stock_code)
+        result["data_collected"]["profile"] = profile_data["success"]
+
+        # 阶段4: 收集舆情数据(异步)
+        sentiment_data = analyze_sentiment(stock_code)
+        result["data_collected"]["sentiment"] = sentiment_data["success"]
+
+        # 阶段5: 构建报告内容
+        stock_name = _extract_stock_name(profile_data)
+        report_content = _build_report_content(
+            stock_code, stock_name, report_type,
+            quote_data, financial_data, profile_data, sentiment_data
+        )
+        report_summary = _generate_summary(report_content)
+
+        # 阶段6: 持久化报告
+        async with async_session_factory() as db:
+            data_snapshot = json.dumps({
+                "quote": {"success": quote_data["success"], "tables": quote_data.get("tables", [])},
+                "financial": {"success": financial_data["success"], "tables": financial_data.get("tables", [])},
+                "profile": {"success": profile_data["success"], "tables": profile_data.get("tables", [])},
+                "sentiment": {
+                    "success": sentiment_data["success"],
+                    "total_count": sentiment_data.get("total_count", 0),
+                },
+            }, ensure_ascii=False)
+
+            report = AnalysisReport(
+                user_id=user_id,
+                stock_code=stock_code,
+                stock_name=stock_name,
+                report_type=report_type,
+                summary=report_summary,
+                content=report_content,
+                data_snapshot=data_snapshot,
+            )
+            db.add(report)
+            await db.commit()
+            await db.refresh(report)
+
+            result["report"] = report.to_dict()
+            result["success"] = True
+            return result
+
+    except Exception as e:
+        result["error"] = str(e)
+        return result
+
+
+async def get_report(report_id: int) -> dict:
+    """获取指定报告
+
+    Args:
+        report_id: 报告ID
+
+    Returns:
+        {"success": True/False, "report": {...} or None, "error": str or None}
+    """
+    result = {"success": False, "report": None, "error": None}
+
+    try:
+        async with async_session_factory() as db:
+            from sqlalchemy import select
+            stmt = select(AnalysisReport).where(AnalysisReport.id == report_id)
+            db_result = await db.execute(stmt)
+            report = db_result.scalar_one_or_none()
+
+            if report is None:
+                result["error"] = f"报告 {report_id} 不存在"
+                return result
+
+            result["report"] = report.to_dict()
+            result["success"] = True
+            return result
+
+    except Exception as e:
+        result["error"] = str(e)
+        return result
+
+
+async def get_user_reports(user_id: str = "default", limit: int = 20) -> dict:
+    """获取用户的历史分析报告列表
+
+    Args:
+        user_id: 用户标识
+        limit: 最大返回数量
+
+    Returns:
+        {"success": True/False, "reports": [...], "total": int, "error": str or None}
+    """
+    result = {"success": False, "reports": [], "total": 0, "error": None}
+
+    try:
+        async with async_session_factory() as db:
+            from sqlalchemy import select, func
+
+            # 查询总数
+            count_stmt = select(func.count(AnalysisReport.id)).where(
+                AnalysisReport.user_id == user_id
+            )
+            db_result = await db.execute(count_stmt)
+            total = db_result.scalar() or 0
+
+            # 查询列表
+            stmt = (
+                select(AnalysisReport)
+                .where(AnalysisReport.user_id == user_id)
+                .order_by(AnalysisReport.created_at.desc())
+                .limit(limit)
+            )
+            db_result = await db.execute(stmt)
+            reports = db_result.scalars().all()
+
+            result["reports"] = [r.to_dict() for r in reports]
+            result["total"] = total
+            result["success"] = True
+            return result
+
+    except Exception as e:
+        result["error"] = str(e)
+        return result
+
+
+def _extract_stock_name(profile_data: dict) -> str:
+    """从公司概况数据中提取股票名称"""
+    try:
+        tables = profile_data.get("tables", [])
+        for table in tables:
+            rows = table.get("rows", [])
+            for row in rows:
+                for key in row:
+                    if "名称" in key or "简称" in key:
+                        return str(row[key])
+        return ""
+    except Exception:
+        return ""
+
+
+def _build_report_content(
+    stock_code: str,
+    stock_name: str,
+    report_type: str,
+    quote_data: dict,
+    financial_data: dict,
+    profile_data: dict,
+    sentiment_data: dict,
+) -> str:
+    """构建报告Markdown内容"""
+    title = stock_name or stock_code
+    now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+
+    lines = []
+    lines.append(f"# {title}({stock_code})深度分析报告")
+    lines.append(f"**生成时间**: {now}")
+    lines.append(f"**报告类型**: {'完整分析' if report_type == 'full' else '快速概览'}")
+    lines.append("")
+    lines.append("---")
+    lines.append("")
+
+    # 1. 行情概览
+    lines.append("## 一、行情概览")
+    if quote_data.get("success"):
+        lines.append(_format_data_section(quote_data))
+    else:
+        lines.append("> ⚠️ 行情数据获取失败")
+        if quote_data.get("error"):
+            lines.append(f"> 原因: {quote_data['error']}")
+    lines.append("")
+
+    # 2. 财务分析
+    lines.append("## 二、财务分析")
+    if financial_data.get("success"):
+        lines.append(_format_data_section(financial_data))
+    else:
+        lines.append("> ⚠️ 财务数据获取失败")
+        if financial_data.get("error"):
+            lines.append(f"> 原因: {financial_data['error']}")
+    lines.append("")
+
+    # 3. 公司概况
+    lines.append("## 三、公司概况")
+    if profile_data.get("success"):
+        lines.append(_format_data_section(profile_data))
+    else:
+        lines.append("> ⚠️ 公司概况获取失败")
+        if profile_data.get("error"):
+            lines.append(f"> 原因: {profile_data['error']}")
+    lines.append("")
+
+    # 4. 舆情分析
+    lines.append("## 四、舆情分析")
+    if sentiment_data.get("success"):
+        total = sentiment_data.get("total_count", 0)
+        news_count = len(sentiment_data.get("news_items", []))
+        report_count = len(sentiment_data.get("report_items", []))
+        ann_count = len(sentiment_data.get("announce_items", []))
+        lines.append(f"- 相关资讯总数: {total} 条")
+        lines.append(f"  - 新闻: {news_count} 条")
+        lines.append(f"  - 研报: {report_count} 条")
+        lines.append(f"  - 公告: {ann_count} 条")
+
+        if news_count > 0:
+            lines.append("")
+            lines.append("### 近期新闻")
+            for item in sentiment_data.get("news_items", [])[:5]:
+                title = item.get("title", "")
+                date = item.get("date", "").split()[0] if item.get("date") else ""
+                institution = item.get("institution", "")
+                source = f" — {institution}" if institution else ""
+                lines.append(f"- [{date}] {title}{source}")
+    else:
+        lines.append("> ⚠️ 舆情数据获取失败")
+        if sentiment_data.get("error"):
+            lines.append(f"> 原因: {sentiment_data['error']}")
+    lines.append("")
+
+    # 5. 综合评估
+    lines.append("## 五、综合评估")
+    lines.append("> 基于以上数据的综合评估分析如下:")
+    lines.append("")
+    collected_count = sum(1 for v in [
+        quote_data.get("success"),
+        financial_data.get("success"),
+        profile_data.get("success"),
+        sentiment_data.get("success"),
+    ] if v)
+
+    if collected_count >= 3:
+        lines.append(f"数据收集完成度: {collected_count}/4,综合分析可用。")
+        lines.append("")
+        lines.append("### 估值参考(需结合AI Agent深度分析)")
+        lines.append("- 请参考【行情概览】部分的实时估值数据")
+        lines.append("- 请参考【财务分析】部分的ROE、净利润等核心指标")
+        lines.append("")
+    else:
+        lines.append(f"⚠️ 数据收集不完整({collected_count}/4),建议检查API Key配置后重试。")
+
+    lines.append("### 投资建议")
+    lines.append("> ⚠️ **免责声明**: 本报告由智能股票分析助手自动生成,所有数据来源于东方财富妙想API。")
+    lines.append("> 分析结果仅供参考和学习,不构成任何投资建议。投资有风险,入市需谨慎。")
+    lines.append("")
+
+    return "\n".join(lines)
+
+
+def _format_data_section(data: dict) -> str:
+    """将数据表格格式化为Markdown"""
+    lines = []
+    tables = data.get("tables", [])
+    if not tables:
+        return "(暂无数据)"
+
+    for table in tables[:3]:  # 最多显示3个表
+        sheet_name = table.get("sheet_name", "")
+        rows = table.get("rows", [])
+        fieldnames = table.get("fieldnames", [])
+
+        if sheet_name:
+            lines.append(f"### {sheet_name}")
+
+        if not rows:
+            lines.append("(无数据)")
+            continue
+
+        # 限制行数
+        display_rows = rows[:10]
+        display_fields = fieldnames[:8]
+
+        # 表头
+        header = " | ".join(display_fields)
+        lines.append(f"| {header} |")
+        lines.append(f"|{'|'.join(['---'] * len(display_fields))}|")
+
+        for row in display_rows:
+            values = [str(row.get(col, "")) for col in display_fields]
+            lines.append(f"| {' | '.join(values)} |")
+
+        if len(rows) > 10:
+            lines.append(f"*(共{len(rows)}行,仅显示前10行)*")
+        lines.append("")
+
+    return "\n".join(lines)
+
+
+def _generate_summary(report_content: str) -> str:
+    """从报告内容中生成简短摘要"""
+    # 从报告内容提取关键信息生成摘要
+    try:
+        lines = report_content.split("\n")
+        data_status = ""
+        for line in lines:
+            if "数据收集完成度" in line:
+                data_status = line.strip()
+                break
+        return f"[智能股票分析助手] 分析报告已生成。{data_status}详见完整报告。"
+    except Exception:
+        return "分析报告已生成,请查看完整内容。"

+ 619 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/services/buffett_service.py

@@ -0,0 +1,619 @@
+"""
+智能股票分析助手 — 巴菲特投资评估服务层
+
+加载巴菲特投资思维参考文件,构建价值投资评估框架,
+供API路由层和智能体层调用。
+"""
+
+import sys
+import os
+from pathlib import Path
+from typing import Any, Dict, Iterator, List, Optional
+
+# 确保skills路径可导入
+_PROJECT_ROOT = Path(__file__).parent.parent.parent.parent  # backend/app/services -> project root
+_AGENTS_DIR = _PROJECT_ROOT / "agents"
+_BUFFETT_DIR = _PROJECT_ROOT / "skills" / "巴菲特投资思维" / "skills" / "buffett"
+
+for p in [_AGENTS_DIR, str(_PROJECT_ROOT)]:
+    if str(p) not in sys.path:
+        sys.path.insert(0, str(p))
+
+from agents.text_truncation import truncate_at_natural_boundary
+from app.config import settings
+
+
+# ====================================================================
+# 巴菲特投资思维核心内容(从参考文件中提取的摘要)
+# ====================================================================
+
+BUFFETT_FRAMEWORK = {
+    "quick_filter": {
+        "name": "8问快速筛选",
+        "questions": [
+            "能力圈:能否用一段话解释这家公司如何赚钱?",
+            "持久性:10年后这家公司是否还会存在且更具竞争力?",
+            "护城河:竞争对手能否通过努力复制其核心优势?",
+            "定价权:能否提价5-10%而不丢失大量客户?",
+            "盈利质量:利润是否真正转化为现金(而非会计技巧)?",
+            "债务安全:在行业最差情况下(营收-30%)能否存活?",
+            "管理层诚信:管理层是否诚实面对问题而非掩盖?",
+            "合理价格:当前价格与内在价值的差距是否足够大?",
+        ],
+        "rule": "2个\"否\"需要强有力理由;4个\"否\"直接放弃",
+    },
+    "moat_analysis": {
+        "name": "护城河分析",
+        "types": [
+            "成本优势(成本领先,规模经济)",
+            "转换成本(客户迁移成本高)",
+            "网络效应(用户越多价值越大)",
+            "无形资产(品牌、专利、特许经营权)",
+            "高效规模(天然垄断,小市场大份额)",
+        ],
+        "judgment": "不仅看当前状态,更关键的是趋势(拓宽/稳定/变窄)",
+    },
+    "management_assessment": {
+        "name": "管理层评估三维度",
+        "dimensions": [
+            "诚信度(自动否决项:发现不诚信直接放弃)",
+            "资本配置能力(能否明智分配资本:再投资/收购/回购/分红)",
+            "所有者心态(是否像主人一样思考,不乱花股东的钱)",
+        ],
+        "warning": "警惕制度迫力——优秀的管理层在制度压力下也可能做出不合理决策",
+    },
+    "financial_metrics": {
+        "name": "财务指标",
+        "metrics": [
+            "所有者收益 = 净利润 + 折旧摊销 - 维护性资本支出 - 营运资金增加",
+            "ROIC 10年平均目标 >15%",
+            "现金转化率目标 >90%",
+            "透视盈余(考虑被投资公司未分配利润)",
+        ],
+    },
+    "valuation": {
+        "name": "估值与安全边际",
+        "methods": [
+            "现金流折现法(DCF)",
+            "盈利倍数法(合理PE区间)",
+            "资产价值法(净资产重估)",
+        ],
+        "margin_of_safety": {
+            "高确定性(宽护城河+可预测增长)": "20-30%",
+            "一般优秀": "30-40%",
+            "存在不确定因素": "40-50%",
+            "无法可靠评估": "不投资",
+        },
+    },
+    "risk_analysis": {
+        "name": "风险分类",
+        "categories": {
+            "结构性风险": "护城河变窄、技术颠覆、监管打击",
+            "财务风险": "过度杠杆、现金流造假、表外负债",
+            "行为风险": "确认偏误、沉没成本、制度迫力",
+        },
+    },
+    "sell_criteria": {
+        "name": "卖出四条标准",
+        "criteria": [
+            "价格严重高估(远超内在价值)",
+            "基本面护城河遭到破坏",
+            "管理层出现诚信问题(立即卖出)",
+            "发现显著更好的投资机会",
+        ],
+    },
+}
+
+# 综合评估框架说明
+BUFFETT_FRAMEWORK_DESC = """
+## 巴菲特价值投资评估框架
+
+### 核心哲学
+- **内在价值 > 市场价格 → 安全边际**:只购买价格明显低于内在价值的股票
+- **护城河 > 一切**:持久的竞争优势是长期回报的根基
+- **能力圈原则**:只投资自己能理解的业务
+- **市场先生**:市场是为你服务的,不是指导你的
+- **长期持有**:以10年的视角思考,而非下一季度的股价
+
+### 评估流程
+1. **快速筛选** — 8问检查(2分钟内完成)
+2. **企业质量** — 护城河类型+趋势、管理层评估
+3. **财务快照** — ROIC、现金转化率、所有者收益
+4. **估值分析** — 内在价值区间、安全边际计算
+5. **风险评估** — 结构性/财务/行为三类风险
+6. **综合判断** — 买入/不买/持有/卖出 + 建议买入价
+"""
+
+
+def _mx_cell_to_str(v: Any) -> str:
+    """妙想表格单元格转为可 JSON 序列化的字符串。"""
+    if v is None:
+        return ""
+    if isinstance(v, bool):
+        return "true" if v else "false"
+    if isinstance(v, float):
+        if v != v:  # NaN
+            return ""
+    if isinstance(v, (str, int, float)):
+        return str(v)
+    return str(v)
+
+
+def _first_table_row_key_values(block: Optional[dict], max_keys: int = 48) -> dict:
+    """取 mx_data 风格结果中首张表首行,扁平为字符串字典(减小体积、避免不可序列化对象)。"""
+    if not isinstance(block, dict) or not block.get("success"):
+        return {"success": False, "fields": {}}
+    tables = block.get("tables") or []
+    fields: dict[str, str] = {}
+    for t in tables[:1]:
+        names: List[str] = list(t.get("fieldnames") or t.get("fieldNames") or [])
+        rows = t.get("rows") or []
+        if not rows:
+            continue
+        row = rows[0]
+        if isinstance(row, dict):
+            for k, v in list(row.items())[:max_keys]:
+                fields[str(k)] = _mx_cell_to_str(v)
+        elif isinstance(row, list) and names:
+            for i, name in enumerate(names):
+                if i >= max_keys:
+                    break
+                val = row[i] if i < len(row) else None
+                fields[str(name)] = _mx_cell_to_str(val)
+        break
+    return {"success": True, "fields": fields}
+
+
+def slim_evaluation_context_for_api(full: dict) -> dict:
+    """HTTP 响应用:去掉巨型 tables,保留框架与行情/财务摘要。"""
+    if not isinstance(full, dict):
+        return {}
+    return {
+        "framework": full.get("framework"),
+        "framework_description": full.get("framework_description"),
+        "market_snapshot": _first_table_row_key_values(full.get("market_data")),
+        "financial_snapshot": _first_table_row_key_values(full.get("financial_data")),
+    }
+
+
+def get_buffett_framework() -> dict:
+    """获取巴菲特投资评估框架
+
+    返回完整的巴菲特投资思维体系结构,包括:
+    - 快速筛选清单
+    - 护城河分析框架
+    - 管理层评估维度
+    - 财务指标模板
+    - 估值与安全边际计算
+    - 风险评估分类
+    - 卖出标准
+
+    Returns:
+        {
+            "success": True,
+            "framework": {...},  # 完整的评估框架
+            "description": str,  # 框架说明
+        }
+    """
+    return {
+        "success": True,
+        "framework": BUFFETT_FRAMEWORK,
+        "description": BUFFETT_FRAMEWORK_DESC,
+    }
+
+
+def evaluate_with_buffett(stock_code: str, stock_name: str = "", data_context: dict = None) -> dict:
+    """使用巴菲特投资思维评估股票
+
+    收集分析数据并构建巴菲特框架评估上下文,返回评估所需的数据包。
+
+    Args:
+        stock_code: 6位股票代码
+        stock_name: 股票名称
+        data_context: 已有的分析数据(可选),包含行情/财务/概况/舆情信息
+
+    Returns:
+        {
+            "success": True/False,
+            "stock_code": str,
+            "stock_name": str,
+            "evaluation_context": {
+                "framework": dict,     # 巴菲特评估框架
+                "market_data": dict,   # 行情数据
+                "financial_data": dict,# 财务数据
+                "profile_data": dict,  # 公司概况
+                "sentiment_data": dict,# 舆情数据
+            },
+            "report_template": str,    # 评估报告模板
+            "error": str or None
+        }
+    """
+    result = {
+        "success": False,
+        "stock_code": stock_code,
+        "stock_name": stock_name,
+        "evaluation_context": {},
+        "report_template": "",
+        "error": None,
+    }
+
+    data_context = data_context or {}
+
+    # 构建评估上下文
+    context = {
+        "framework": BUFFETT_FRAMEWORK,
+        "framework_description": BUFFETT_FRAMEWORK_DESC,
+        "market_data": data_context.get("market", {}),
+        "financial_data": data_context.get("financial", {}),
+        "profile_data": data_context.get("profile", {}),
+        "sentiment_data": data_context.get("sentiment", {}),
+    }
+
+    result["success"] = True
+    result["evaluation_context"] = context
+    result["report_template"] = _build_buffett_report_template(stock_code, stock_name)
+
+    return result
+
+
+def _build_buffett_report_template(stock_code: str, stock_name: str) -> str:
+    """生成巴菲特风格评估报告模板
+
+    Args:
+        stock_code: 6位股票代码
+        stock_name: 股票名称
+
+    Returns:
+        Markdown格式的报告模板
+    """
+    name_display = stock_name or stock_code
+
+    template = f"""
+# 巴菲特价值投资评估报告
+
+## 标的: {name_display} ({stock_code})
+
+---
+
+## 一、结论
+
+[买入 / 不买 / 持续观察 / 持有 / 卖出] — 一句话核心理由
+
+---
+
+## 二、能力圈判断
+
+[明确判断:在圈内 / 圈外 / 边界区域]
+若在圈外:停止分析并诚实说明原因。
+
+---
+
+## 三、关键假设(3-5条)
+
+[列出决策所依赖的核心假设,供日后验证]
+1.
+2.
+3.
+4.
+5.
+
+---
+
+## 四、快速筛选(8问检查)
+
+| # | 维度 | 结果 | 说明 |
+|---|------|------|------|
+| 1 | 能力圈 | [是/否] | |
+| 2 | 持久性 | [是/否] | |
+| 3 | 护城河 | [是/否] | |
+| 4 | 定价权 | [是/否] | |
+| 5 | 盈利质量 | [是/否] | |
+| 6 | 债务安全 | [是/否] | |
+| 7 | 管理层诚信 | [是/否] | |
+| 8 | 合理价格 | [是/否] | |
+
+---
+
+## 五、企业质量分析
+
+### 护城河
+- **类型**: [成本优势/转换成本/网络效应/无形资产/高效规模]
+- **强度**: [强/中/弱]
+- **趋势**: [拓宽/稳定/变窄]
+
+### 管理层
+- **诚信度**: [评估]
+- **资本配置能力**: [评估]
+- **所有者心态**: [评估]
+
+### 商业模式
+- **类型**: [特许经营权型/商品型/混合型]
+
+### 制度迫力预警
+- [有/无] — [依据]
+
+---
+
+## 六、财务快照
+
+| 指标 | 数值 | 评估 |
+|------|------|------|
+| ROIC (10年均值) | — | |
+| 现金转化率 | — | |
+| 债务安全性 | — | |
+| 所有者收益估算 | — | |
+
+---
+
+## 七、估值分析
+
+- **内在价值区间**: —
+- **当前安全边际**: —% (确定性水平:高/中/低)
+- **建议买入价**: —
+
+---
+
+## 八、卖出标准逐条检验
+
+| # | 标准 | 判断 | 依据 |
+|---|------|------|------|
+| 1 | 价格严重高估? | [是/否] | |
+| 2 | 基本面护城河破坏? | [是/否] | |
+| 3 | 管理层诚信问题? | [是/否] | |
+| 4 | 有更好的机会? | [是/否] | |
+
+---
+
+## 九、主要风险(最多3条)
+
+1. **风险一**: [描述]
+2. **风险二**: [描述]
+3. **风险三**: [描述]
+
+---
+
+## 十、监控指标
+
+### 每季度检查:
+- [指标1]
+- [指标2]
+
+### 触发卖出信号:
+- [信号1]
+- [信号2]
+
+---
+
+## 十一、综合判断
+
+[以巴菲特的视角和语气,直接给出决策建议和核心理由]
+
+---
+
+> ⚠️ 以上分析基于巴菲特价值投资理念框架,仅供参考,不构成投资建议。投资有风险,入市需谨慎。
+"""
+    return template
+
+
+def _truncate_text(s: str, max_len: int) -> str:
+    if not s:
+        return ""
+    if len(s) <= max_len:
+        return s
+    return truncate_at_natural_boundary(s, max_len, "\n\n…(内容过长已截断)")
+
+
+def _ensure_hello_agents_path() -> None:
+    _hello = _PROJECT_ROOT / "HelloAgents Optimized"
+    if str(_hello) not in sys.path:
+        sys.path.insert(0, str(_hello))
+
+
+def make_buffett_llm_client():
+    """构造用于巴菲特长文生成的 LLM 客户端(流式/非流式共用)。"""
+    _ensure_hello_agents_path()
+    from hello_agents.core.llm import HelloAgentsLLM
+
+    buffett_llm_timeout = max(int(settings.LLM_TIMEOUT), 180)
+    return HelloAgentsLLM(
+        model=settings.LLM_MODEL_ID,
+        api_key=settings.LLM_API_KEY,
+        base_url=settings.LLM_BASE_URL or None,
+        provider=os.getenv("LLM_PROVIDER", "auto"),
+        temperature=0.35,
+        max_tokens=6144,
+        timeout=buffett_llm_timeout,
+    )
+
+
+def prepare_buffett_ai_messages(stock_code: str, stock_name: str = "") -> Dict[str, Any]:
+    """聚合行情/财务/舆情并组装 LLM messages。
+
+    Returns:
+        成功: {"ok": True, "messages": [...], "name": str}
+        失败: {"ok": False, "error": str}
+    """
+    if not settings.is_agent_ready():
+        return {
+            "ok": False,
+            "error": "未配置有效的 LLM_API_KEY,无法一键生成 AI 评估报告",
+        }
+
+    code = (stock_code or "").strip()
+    if len(code) < 4:
+        return {"ok": False, "error": "请输入有效的股票代码"}
+
+    try:
+        from app.services.market_service import (
+            get_stock_financial,
+            get_stock_profile,
+            get_stock_quote,
+        )
+        from app.services.news_service import analyze_sentiment
+        from app.services.analysis_service import _extract_stock_name, _format_data_section
+
+        quote_data = get_stock_quote(code)
+        financial_data = get_stock_financial(code)
+        profile_data = get_stock_profile(code)
+        sentiment_data = analyze_sentiment(code)
+
+        name = (stock_name or "").strip() or _extract_stock_name(profile_data) or code
+
+        chunks = []
+        if quote_data.get("success"):
+            chunks.append("### 行情\n" + _format_data_section(quote_data))
+        else:
+            chunks.append(
+                "### 行情\n获取失败: " + str(quote_data.get("error") or "未知错误")
+            )
+
+        if financial_data.get("success"):
+            chunks.append("\n### 财务\n" + _format_data_section(financial_data))
+        else:
+            chunks.append(
+                "\n### 财务\n获取失败: " + str(financial_data.get("error") or "未知错误")
+            )
+
+        if profile_data.get("success"):
+            chunks.append("\n### 公司概况\n" + _format_data_section(profile_data))
+        else:
+            chunks.append(
+                "\n### 公司概况\n获取失败: " + str(profile_data.get("error") or "未知错误")
+            )
+
+        if sentiment_data.get("success"):
+            news_items = sentiment_data.get("news_items") or []
+            report_items = sentiment_data.get("report_items") or []
+            ann_items = sentiment_data.get("announce_items") or []
+            chunks.append(
+                f"\n### 舆情摘要\n新闻 {len(news_items)} / 研报 {len(report_items)} / 公告 {len(ann_items)}"
+            )
+            merged = (news_items + report_items + ann_items)[:12]
+            for item in merged:
+                title = item.get("title") or ""
+                date = (item.get("date") or "").split()[0] if item.get("date") else ""
+                chunks.append(f"- [{date}] {title}")
+        else:
+            chunks.append(
+                "\n### 舆情\n获取失败: " + str(sentiment_data.get("error") or "未知错误")
+            )
+
+        data_bundle = _truncate_text("\n".join(chunks), 14000)
+        framework_desc = _truncate_text(BUFFETT_FRAMEWORK_DESC.strip(), 5000)
+        outline = _build_buffett_report_template(code, name)
+
+        user_prompt = f"""请撰写完整的《巴菲特价值投资评估报告》(Markdown)。
+
+标的:**{name}**(股票代码 {code})
+
+【须覆盖的报告结构与要点】
+以下提纲中的章节结构与顺序必须体现在你的输出中(使用 ## / ### 标题);每个章节需要实质性段落或列表,禁止只输出标题或空白占位。
+
+{outline}
+
+【价值投资框架参考】(按需引用,勿全文照搬)
+{framework_desc}
+
+【客观数据】(结论必须以此为依据,勿编造数据中不存在的精确数值)
+{data_bundle}
+
+写作要求:
+1. 「结论」「综合判断」中必须明确:**买入 / 不买 / 持续观察 / 持有 / 卖出** 之一,并附简短理由。
+2. 「快速筛选」按 8 个维度逐条给出判断与简要依据。
+3. 估值与安全边际:若数据不足以定量,请定性说明并列出需补充的信息,勿捏造 PE/PB。
+4. 文末单独一行:⚠️ 以上分析仅供参考,不构成投资建议。投资有风险,入市需谨慎。
+"""
+
+        system = (
+            "你是资深证券投资分析师,精通巴菲特与格雷厄姆的价值投资框架。"
+            "你只输出 Markdown 正文,语气专业、审慎。"
+        )
+        messages = [
+            {"role": "system", "content": system},
+            {"role": "user", "content": user_prompt},
+        ]
+        return {"ok": True, "messages": messages, "name": name}
+
+    except Exception as e:
+        return {"ok": False, "error": str(e)}
+
+
+def iter_buffett_ai_report_events(stock_code: str, stock_name: str = "") -> Iterator[Dict[str, Any]]:
+    """供 NDJSON 流式响应:通过巴菲特评估Agent (ReflectionAgent) 生成报告。"""
+    prep = prepare_buffett_ai_messages(stock_code, stock_name)
+    if not prep.get("ok"):
+        yield {"type": "error", "message": prep.get("error") or "准备失败"}
+        return
+
+    code = (stock_code or "").strip()
+    name = prep.get("name") or code
+    yield {"type": "meta", "stock_code": code, "stock_name": name}
+
+    try:
+        from agents.advisor_agent import evaluate_buffett_stream
+
+        for event in evaluate_buffett_stream(
+            stock_code=code,
+            stock_name=name,
+        ):
+            yield event
+
+        yield {"type": "done"}
+    except Exception as e:
+        yield {"type": "error", "message": str(e)}
+
+
+def generate_buffett_ai_report(stock_code: str, stock_name: str = "") -> dict:
+    """调用 LLM 生成填充后的巴菲特风格 Markdown 报告(同步阻塞,请在 asyncio.to_thread 中调用)。
+
+    Returns:
+        {"success": bool, "report_markdown": str | None, "error": str | None}
+    """
+    result: dict = {"success": False, "report_markdown": None, "error": None}
+
+    prep = prepare_buffett_ai_messages(stock_code, stock_name)
+    if not prep.get("ok"):
+        result["error"] = prep.get("error") or "准备失败"
+        return result
+
+    try:
+        llm = make_buffett_llm_client()
+        md = llm.invoke(
+            prep["messages"],
+            max_tokens=6144,
+            temperature=0.35,
+        )
+        md = (md or "").strip()
+        if not md:
+            result["error"] = "LLM 返回为空,请稍后重试"
+            return result
+
+        result["success"] = True
+        result["report_markdown"] = md
+        return result
+
+    except Exception as e:
+        result["error"] = str(e)
+        return result
+
+
+def load_buffett_reference(ref_name: str) -> Optional[str]:
+    """加载指定的巴菲特参考文件内容
+
+    Args:
+        ref_name: 参考文件名,如 "03-business-moat"
+
+    Returns:
+        文件内容文本,若文件不存在返回 None
+    """
+    safe_name = ref_name.replace("..", "").replace("\\", "").replace("/", "")
+    ref_path = _BUFFETT_DIR / "references" / f"{safe_name}.md"
+
+    if not ref_path.exists():
+        return None
+
+    try:
+        with open(ref_path, "r", encoding="utf-8") as f:
+            return f.read()
+    except Exception:
+        return None

+ 21 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/services/chat_service.py

@@ -0,0 +1,21 @@
+"""
+AI 对话助手服务层 — 协调者 Agent NDJSON 事件迭代(实现方案约定)
+"""
+
+from typing import Any, Iterator, List
+
+
+def iter_chat_stream_events(
+    message: str,
+    stock_code: str = "",
+    stock_name: str = "",
+    history: List[Any] | None = None,
+) -> Iterator[dict]:
+    """产出与 POST /api/v1/chat/stream 一致的 NDJSON 事件字典。"""
+    from agents.agent_system import get_agent_system
+
+    asys = get_agent_system()
+    text = message
+    if (stock_code or stock_name).strip():
+        text = f"[股票: {stock_name}({stock_code})] {message}"
+    yield from asys.chat_stream(text, history or [])

+ 36 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/services/dashboard_warmup.py

@@ -0,0 +1,36 @@
+"""
+仪表盘数据预热
+
+启动时通过 MemoryService 三线程并行获取指数、自选、热点资讯,
+将结果写入 MXTimedCache,首屏请求即可命中缓存。
+"""
+from __future__ import annotations
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+DASHBOARD_INDEX_NAMES: tuple[str, ...] = (
+    "上证指数",
+    "深证成指",
+    "创业板指",
+    "沪深300",
+)
+
+
+def warm_dashboard_cache() -> None:
+    """通过记忆系统三线程并行预取仪表盘妙想缓存"""
+    try:
+        from app.services.memory_service import get_memory_service
+
+        mem = get_memory_service()
+
+        if mem.should_refresh():
+            logger.info("仪表盘预热: 触发三线程并行获取...")
+            mem.parallel_fetch()
+            logger.info("仪表盘预热: 完成 (indices=%d, watchlist=%d)",
+                         len(mem.get_indices()), mem.get_stats().get("watchlist_count", 0))
+        else:
+            logger.info("仪表盘预热: 今日已缓存,跳过刷新")
+    except Exception as exc:
+        logger.warning("仪表盘预热失败(可忽略): %s", exc)

+ 111 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/services/history_service.py

@@ -0,0 +1,111 @@
+"""
+分析历史记录服务 — 管理各类分析报告的历史存储与查询
+"""
+
+from datetime import date
+from typing import Optional
+
+from sqlalchemy import select, func, delete
+from app.models.database import async_session_factory
+from app.models.history_models import AnalysisHistory
+
+
+async def save_analysis(
+    analysis_type: str,
+    content: str,
+    user_id: str = "default",
+    stock_code: Optional[str] = None,
+    stock_name: Optional[str] = None,
+    title: Optional[str] = None,
+) -> dict:
+    """保存分析报告到历史记录"""
+    try:
+        today = date.today().isoformat()
+        async with async_session_factory() as db:
+            record = AnalysisHistory(
+                user_id=user_id,
+                date=today,
+                type=analysis_type,
+                stock_code=stock_code,
+                stock_name=stock_name,
+                title=title or f"{analysis_type}_{today}",
+                content=content,
+            )
+            db.add(record)
+            await db.commit()
+            await db.refresh(record)
+            return {"success": True, "id": record.id}
+    except Exception as e:
+        return {"success": False, "error": str(e)}
+
+
+async def get_history_list(
+    analysis_type: Optional[str] = None,
+    user_id: str = "default",
+    limit: int = 20,
+) -> dict:
+    """获取历史记录列表"""
+    try:
+        async with async_session_factory() as db:
+            conditions = [AnalysisHistory.user_id == user_id]
+            if analysis_type:
+                conditions.append(AnalysisHistory.type == analysis_type)
+
+            stmt = select(AnalysisHistory).where(*conditions).order_by(
+                AnalysisHistory.created_at.desc()
+            ).limit(limit)
+            result = await db.execute(stmt)
+            records = result.scalars().all()
+
+            count_stmt = select(func.count()).select_from(AnalysisHistory).where(*conditions)
+            total = (await db.execute(count_stmt)).scalar()
+
+            return {
+                "success": True,
+                "items": [r.to_dict() for r in records],
+                "total": total or 0,
+            }
+    except Exception as e:
+        return {"success": False, "items": [], "total": 0, "error": str(e)}
+
+
+async def get_history_detail(record_id: int) -> dict:
+    """获取单条历史记录详情"""
+    try:
+        async with async_session_factory() as db:
+            record = await db.get(AnalysisHistory, record_id)
+            if not record:
+                return {"success": False, "error": "记录不存在"}
+            return {"success": True, "record": record.to_dict()}
+    except Exception as e:
+        return {"success": False, "error": str(e)}
+
+
+async def delete_history(record_id: int) -> dict:
+    """删除单条历史记录"""
+    try:
+        async with async_session_factory() as db:
+            record = await db.get(AnalysisHistory, record_id)
+            if not record:
+                return {"success": False, "error": "记录不存在"}
+            await db.delete(record)
+            await db.commit()
+            return {"success": True, "message": "已删除"}
+    except Exception as e:
+        return {"success": False, "error": str(e)}
+
+
+async def clear_today_history(analysis_type: Optional[str] = None) -> dict:
+    """清空今日历史记录"""
+    try:
+        today = date.today().isoformat()
+        async with async_session_factory() as db:
+            conditions = [AnalysisHistory.date == today]
+            if analysis_type:
+                conditions.append(AnalysisHistory.type == analysis_type)
+            stmt = delete(AnalysisHistory).where(*conditions)
+            result = await db.execute(stmt)
+            await db.commit()
+            return {"success": True, "count": result.rowcount}
+    except Exception as e:
+        return {"success": False, "error": str(e)}

+ 414 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/services/market_service.py

@@ -0,0 +1,414 @@
+"""
+智能股票分析助手 — 行情数据服务层
+
+封装金融数据查询、解析和格式化逻辑,供API路由层调用。
+含妙想 mx_data 计时缓存与额度用尽时的缓存降级。
+"""
+
+from __future__ import annotations
+
+import copy
+import math
+import re
+import sys
+from pathlib import Path
+from typing import Any, Optional
+
+# 确保skills路径可导入
+_PROJECT_ROOT = Path(__file__).parent.parent.parent.parent  # backend/app/services -> project root
+_AGENTS_DIR = _PROJECT_ROOT / "agents"
+_SKILLS_DATA = _PROJECT_ROOT / "skills" / "金融数据" / "mx-data"
+
+for p in [_AGENTS_DIR, _SKILLS_DATA, str(_PROJECT_ROOT)]:
+    if str(p) not in sys.path:
+        sys.path.insert(0, str(p))
+
+from app.config import settings
+from app.services.mx_timed_cache import get_mx_timed_cache, mx_cache_ttl_seconds
+from app.utils.mx_fixture import try_load_raw_fixture
+from app.utils.mx_quota import MX_QUOTA_HINT, is_mx_quota_exhausted, quota_exhausted_no_cache_message
+
+# 仪表盘指数卡片:从 mx tables 中解析列名(妙想返回的表头差异较大)
+_PRICE_HDR = re.compile(
+    r"点位|最新点|收盘点|指数点位|收盘价|最新价|现价|收盘|价格|数值|行情|昨收|今开|当前价|最新报价|报价",
+    re.I,
+)
+_CHANGE_HDR = re.compile(
+    r"涨跌幅|涨跌幅度|当日涨幅|日涨跌幅|涨跌|涨幅|变动率",
+    re.I,
+)
+_DATE_HDR = re.compile(r"日期|时间|^date$", re.I)
+_LONG_PRICE_LABEL = re.compile(
+    r"点位|最新点|收盘点|指数点位|收盘价|最新价|现价|收盘|价格|最新|上证|深证|成指|沪深300|创业板指",
+)
+_LONG_CHANGE_LABEL = re.compile(r"涨跌幅|涨跌幅度|当日涨幅|涨跌|涨幅|变动率")
+
+
+def _parse_pct_cell(val: Any) -> Optional[float]:
+    """解析涨跌幅单元格为浮点数(百分比数值,不带 % 也可)"""
+    if val is None:
+        return None
+    if isinstance(val, (int, float)):
+        x = float(val)
+        return x if math.isfinite(x) else None
+    s = str(val).strip()
+    if not s or s in ("--", "—", "-", "暂无"):
+        return None
+    s = re.sub(r"[%%,,]", "", s)
+    s = s.replace("+", "+").replace("−", "-").replace("-", "-")
+    m = re.search(r"[-+]?\d+(?:\.\d+)?(?:e[-+]?\d+)?", s, re.I)
+    if not m:
+        return None
+    try:
+        x = float(m.group(0))
+    except ValueError:
+        return None
+    return x if math.isfinite(x) else None
+
+
+def _cell_at(names: list, row: dict, idx: int) -> Any:
+    if idx < 0 or not isinstance(row, dict):
+        return None
+    if idx >= len(names):
+        return None
+    return row.get(names[idx])
+
+
+def _heuristic_index_from_row(row: dict, names: list[str]) -> tuple[Optional[str], Optional[float]]:
+    """列名无法识别时,按数值量级与 % 符号兜底"""
+    change: Optional[float] = None
+    price_disp: Optional[str] = None
+    best: float = -1.0
+    keys = [n for n in names if n in row] if names else list(row.keys())
+    for k in keys:
+        if _DATE_HDR.search(str(k)):
+            continue
+        raw = row[k]
+        if raw is None:
+            continue
+        sv = str(raw).strip()
+        if not sv:
+            continue
+        if "%" in sv or "%" in sv:
+            p = _parse_pct_cell(sv)
+            if p is not None:
+                change = p
+            continue
+        p = _parse_pct_cell(sv)
+        if p is None:
+            continue
+        # A 股主要指数点位多在数百~数万之间
+        if 500 <= abs(p) <= 50000:
+            if abs(p) > best:
+                best = abs(p)
+                price_disp = sv
+    return price_disp, change
+
+
+def _extract_index_card_one_table(t0: dict) -> tuple[Optional[str], Optional[float]]:
+    """从 mx_data 单个 sheet 解析点位与涨跌幅(字段名与行结构随品种变化较大)"""
+    names: list = list(t0.get("fieldnames") or t0.get("fieldNames") or [])
+    rows = t0.get("rows") or []
+    if not rows:
+        return None, None
+
+    # 长表:恰两列,每行一个指标
+    if len(names) == 2:
+        lk, vk = names[0], names[1]
+        price_s: Optional[str] = None
+        change_v: Optional[float] = None
+        for r in rows:
+            if not isinstance(r, dict):
+                continue
+            lab = str(r.get(lk, "")).strip()
+            raw = r.get(vk)
+            if _LONG_CHANGE_LABEL.search(lab):
+                c = _parse_pct_cell(raw)
+                if c is not None:
+                    change_v = c
+            elif _LONG_PRICE_LABEL.search(lab):
+                if price_s is None and raw is not None and str(raw).strip():
+                    price_s = str(raw).strip()
+        if price_s or change_v is not None:
+            return price_s, change_v
+
+    date_idx = next((i for i, n in enumerate(names) if _DATE_HDR.search(str(n))), -1)
+    data_row = rows[-1] if date_idx >= 0 and len(rows) > 1 else rows[0]
+
+    if isinstance(data_row, list):
+        pi = next((i for i, n in enumerate(names) if _PRICE_HDR.search(str(n))), -1)
+        ci = next((i for i, n in enumerate(names) if _CHANGE_HDR.search(str(n))), -1)
+        raw_p = data_row[pi] if 0 <= pi < len(data_row) else None
+        raw_c = data_row[ci] if 0 <= ci < len(data_row) else None
+        ps = str(raw_p).strip() if raw_p is not None and str(raw_p).strip() else None
+        cv = _parse_pct_cell(raw_c)
+        if ps or cv is not None:
+            return ps, cv
+        return None, None
+
+    if isinstance(data_row, dict):
+        pi = next((i for i, n in enumerate(names) if _PRICE_HDR.search(str(n))), -1)
+        ci = next((i for i, n in enumerate(names) if _CHANGE_HDR.search(str(n))), -1)
+        raw_p = _cell_at(names, data_row, pi)
+        raw_c = _cell_at(names, data_row, ci)
+        ps = str(raw_p).strip() if raw_p is not None and str(raw_p).strip() else None
+        cv = _parse_pct_cell(raw_c)
+        if ps or cv is not None:
+            return ps, cv
+        return _heuristic_index_from_row(data_row, names)
+
+    return None, None
+
+
+def _extract_index_card_from_tables(tables: list) -> tuple[Optional[str], Optional[float]]:
+    """从 mx_data 全部 sheet 解析仪表盘展示字段(妙想常返回多表,首表可能是说明性空表)"""
+    if not tables:
+        return None, None
+    merged_p: Optional[str] = None
+    merged_c: Optional[float] = None
+    for t in tables:
+        if not isinstance(t, dict):
+            continue
+        p, c = _extract_index_card_one_table(t)
+        if p is not None and c is not None:
+            return p, c
+        if merged_p is None and p is not None:
+            merged_p = p
+        if merged_c is None and c is not None:
+            merged_c = c
+        if merged_p is not None and merged_c is not None:
+            return merged_p, merged_c
+    return merged_p, merged_c
+
+
+def _enrich_index_quote_result(result: dict) -> dict:
+    """成功返回时附加 display_*,供前端直接使用;deepcopy 避免污染进程内缓存对象"""
+    if not result.get("success"):
+        return result
+    out = copy.deepcopy(result)
+    price, chg = _extract_index_card_from_tables(out.get("tables") or [])
+    if price is not None:
+        out["display_price"] = price
+    if chg is not None:
+        out["display_change_pct"] = chg
+    return out
+
+
+def _mx_result_meta(*, from_cache: bool, quota_exhausted: bool, channel: str) -> dict:
+    ttl = int(mx_cache_ttl_seconds())
+    m = {
+        "from_cache": from_cache,
+        "quota_exhausted": quota_exhausted,
+        "cache_ttl_seconds": ttl,
+        "channel": channel,
+    }
+    if quota_exhausted:
+        m["hint"] = MX_QUOTA_HINT
+    return m
+
+
+def _attach_meta(payload: dict, meta: dict) -> dict:
+    out = copy.deepcopy(payload)
+    out["_mx_meta"] = meta
+    return out
+
+
+def _fetch_mx_data_live(query: str) -> dict:
+    """直连妙想 mx_data;MX_REPLAY_FIXTURES 时优先读本地原始 JSON(不修额度)"""
+    import mx_data as _mx
+
+    result = {
+        "success": False,
+        "query": query,
+        "tables": [],
+        "condition_parts": [],
+        "total_rows": 0,
+        "error": None,
+    }
+
+    raw_fixture = try_load_raw_fixture("mx_data", query)
+    if raw_fixture is not None:
+        try:
+            tables, condition_parts, total_rows, error = _mx.MXData.parse_result(raw_fixture)
+            if error:
+                result["error"] = f"[fixture] {error}"
+                return result
+            result["success"] = True
+            result["tables"] = tables
+            result["condition_parts"] = condition_parts
+            result["total_rows"] = total_rows
+            return result
+        except Exception as e:
+            result["error"] = f"[fixture] 解析失败: {e}"
+            return result
+
+    key_ok = bool(settings.MX_APIKEY and settings.MX_APIKEY != "your-mx-apikey-here")
+    if not key_ok:
+        result["error"] = "MX_APIKEY 未配置,且无匹配的本地 fixture(设置 MX_REPLAY_FIXTURES=1 并放置 JSON)"
+        return result
+
+    try:
+        querier = _mx.MXData(api_key=settings.MX_APIKEY)
+        raw_result = querier.query(query)
+        tables, condition_parts, total_rows, error = _mx.MXData.parse_result(raw_result)
+
+        if error:
+            result["error"] = error
+            return result
+
+        result["success"] = True
+        result["tables"] = tables
+        result["condition_parts"] = condition_parts
+        result["total_rows"] = total_rows
+        return result
+
+    except Exception as e:
+        result["error"] = str(e)
+        return result
+
+
+def query_financial_data(query: str) -> dict:
+    """执行金融数据查询并返回结构化结果(带计时缓存与额度降级)
+
+    缓存键为规范化后的自然语言 query:
+    - 相同查询串在 TTL 内不会重复请求妙想;
+    - 股票代码 / 财务指标不同会得到不同 query,从而自动区分。
+    """
+    result = {
+        "success": False,
+        "query": query,
+        "tables": [],
+        "condition_parts": [],
+        "total_rows": 0,
+        "error": None,
+    }
+
+    key_missing = not settings.MX_APIKEY or settings.MX_APIKEY == "your-mx-apikey-here"
+    if key_missing and not settings.MX_REPLAY_FIXTURES:
+        result["error"] = "MX_APIKEY 未配置"
+        return result
+
+    cache = get_mx_timed_cache()
+    ttl = mx_cache_ttl_seconds()
+    key = cache.make_key("mx_data", query)
+
+    fresh = cache.get_fresh(key, ttl)
+    if fresh is not None:
+        return _attach_meta(
+            fresh,
+            _mx_result_meta(from_cache=True, quota_exhausted=False, channel="mx_data"),
+        )
+
+    live = _fetch_mx_data_live(query)
+
+    if live["success"]:
+        cache.set(key, live)
+        return _attach_meta(
+            live,
+            _mx_result_meta(from_cache=False, quota_exhausted=False, channel="mx_data"),
+        )
+
+    err = live.get("error") or ""
+    if is_mx_quota_exhausted(err):
+        stale = cache.get_stale(key)
+        if stale:
+            merged = copy.deepcopy(stale)
+            merged["success"] = True
+            merged["query"] = query
+            return _attach_meta(
+                merged,
+                _mx_result_meta(from_cache=True, quota_exhausted=True, channel="mx_data"),
+            )
+        live["error"] = quota_exhausted_no_cache_message(err)
+        return live
+
+    return live
+
+
+def get_stock_quote(code: str) -> dict:
+    """查询个股实时行情
+
+    一并请求 OHLC/昨收,供前端「当日价位快照」图使用;仅查最新价时五价合一易变成一条平线。
+    优先从文件缓存读取,未命中或过期才调用接口。
+    """
+    from app.services.stock_file_cache import get_stock_file_cache
+    fc = get_stock_file_cache()
+
+    cached = fc.get(code, "quote")
+    if cached and cached.get("data"):
+        cached_data = cached["data"]
+        if cached_data.get("success"):
+            return cached_data
+
+    extra = "今开 开盘 最高 最低 昨收 昨收盘价"
+    if code.startswith(("6", "5", "9")):
+        query = f"{code} 最新价 涨跌幅 涨跌额 {extra} 成交量 成交额 换手率"
+    else:
+        query = f"{code} 最新价 涨跌幅 涨跌额 {extra} 成交量 成交额 换手率"
+
+    result = query_financial_data(query)
+    if result.get("success"):
+        fc.set(code, "quote", result)
+    return result
+
+
+def get_stock_financial(code: str, indicators: str = "净利润 营业收入 净资产收益率 每股收益") -> dict:
+    """查询个股财务指标(文件缓存优先)"""
+    from app.services.stock_file_cache import get_stock_file_cache
+    fc = get_stock_file_cache()
+
+    cached = fc.get(code, "financial")
+    if cached and cached.get("data") and cached["data"].get("success"):
+        return cached["data"]
+
+    query = f"{code} {indicators}"
+    result = query_financial_data(query)
+    if result.get("success"):
+        fc.set(code, "financial", result)
+    return result
+
+
+def get_stock_profile(code: str) -> dict:
+    """查询公司概况(文件缓存优先)"""
+    from app.services.stock_file_cache import get_stock_file_cache
+    fc = get_stock_file_cache()
+
+    cached = fc.get(code, "profile")
+    if cached and cached.get("data") and cached["data"].get("success"):
+        return cached["data"]
+
+    query = f"{code} 公司简介 主营业务 成立时间 董事长 总股本"
+    result = query_financial_data(query)
+    if result.get("success"):
+        fc.set(code, "profile", result)
+    return result
+
+
+def get_stock_holders(code: str) -> dict:
+    """查询十大股东(文件缓存优先)"""
+    from app.services.stock_file_cache import get_stock_file_cache
+    fc = get_stock_file_cache()
+
+    cached = fc.get(code, "holders")
+    if cached and cached.get("data") and cached["data"].get("success"):
+        return cached["data"]
+
+    query = f"{code} 十大股东"
+    result = query_financial_data(query)
+    if result.get("success"):
+        fc.set(code, "holders", result)
+    return result
+
+
+def get_index_quote(index_name: str = "沪深300") -> dict:
+    """查询指数行情(附带 display_price / display_change_pct 供仪表盘稳定展示)"""
+    # 避免「上证指数指数」重复;自然语言尽量简短明确
+    query = f"{index_name} 最新点位 涨跌幅"
+    base = query_financial_data(query)
+    return _enrich_index_quote_result(base)
+
+
+def get_sector_quote(sector_name: str) -> dict:
+    """查询板块行情"""
+    query = f"{sector_name}板块最新行情"
+    return query_financial_data(query)

+ 250 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/services/memory_service.py

@@ -0,0 +1,250 @@
+"""
+记忆系统 — 仪表盘快照的每日缓存与过期管理
+
+记录每天第一次打开后端的时间日期;跨日或自选股数量变化时触发刷新。
+每天首次启动时三线程并行获取指数、自选、热点资讯,写入 data/memory/dashboard_state.json。
+与 HelloAgents 的 ConversationManager / MemoryManager 无关,本应用对话历史由前端与 SQLite 历史表管理。
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+import threading
+from concurrent.futures import ThreadPoolExecutor, as_completed
+from datetime import date
+from pathlib import Path
+from typing import Any, Optional
+
+from app.config import settings
+from app.models.memory_models import MemorySnapshot
+
+logger = logging.getLogger(__name__)
+
+_memory_lock = threading.Lock()
+
+
+class MemoryService:
+    """
+    记忆系统核心服务
+
+    职责:
+    - 记录每日首次启动日期
+    - 日切时清空前一日数据并重新获取
+    - 三线程并行获取仪表盘数据(指数、自选、热点资讯)
+    - 检测自选股数量变化触发刷新
+    """
+
+    def __init__(self, storage_dir: Optional[Path] = None):
+        self._today: Optional[str] = None
+        self._snapshot: Optional[MemorySnapshot] = None
+        self._lock = threading.Lock()
+        self._watchlist_count: int = 0
+
+        self._storage_dir = storage_dir or (settings.DATA_DIR / "memory")
+        self._storage_dir.mkdir(parents=True, exist_ok=True)
+        self._state_file = self._storage_dir / "dashboard_state.json"
+
+        self._load_state()
+
+    # ---- 持久化 ----
+
+    def _load_state(self) -> None:
+        """从磁盘恢复上次的快照状态"""
+        try:
+            if self._state_file.exists():
+                data = json.loads(self._state_file.read_text(encoding="utf-8"))
+                self._today = data.get("today")
+                self._watchlist_count = data.get("watchlist_count", 0)
+                snap = data.get("snapshot")
+                if snap:
+                    self._snapshot = MemorySnapshot.from_dict(snap)
+                logger.info("记忆系统状态已加载: date=%s, watchlist_count=%d", self._today, self._watchlist_count)
+        except Exception as exc:
+            logger.warning("加载记忆状态失败: %s", exc)
+
+    def _save_state(self) -> None:
+        """将当前快照状态持久化到磁盘"""
+        try:
+            data = {
+                "today": self._today,
+                "watchlist_count": self._watchlist_count,
+                "snapshot": self._snapshot.to_dict() if self._snapshot else None,
+            }
+            self._state_file.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
+        except Exception as exc:
+            logger.warning("保存记忆状态失败: %s", exc)
+
+    # ---- 日期检测 ----
+
+    def _get_today(self) -> str:
+        return date.today().isoformat()
+
+    def is_new_day(self) -> bool:
+        """检查是否为新的一天"""
+        return self._today != self._get_today()
+
+    def should_refresh(self) -> bool:
+        """
+        判断是否需要刷新数据:
+        1. 新的一天
+        2. 自选股数量发生变化
+        """
+        if self.is_new_day():
+            logger.info("检测到新的一天,需要刷新仪表盘数据")
+            return True
+
+        try:
+            from app.services import watchlist_service
+            wl = watchlist_service.get_watchlist()
+            current_count = wl.get("total", 0) if wl.get("success") else 0
+            if current_count != self._watchlist_count and self._watchlist_count > 0:
+                logger.info("自选股数量变化: %d -> %d,需要刷新", self._watchlist_count, current_count)
+                self._watchlist_count = current_count
+                self._save_state()
+                return True
+        except Exception as exc:
+            logger.debug("检查自选股数量时出错: %s", exc)
+
+        return False
+
+    # ---- 数据获取 ----
+
+    def _fetch_indices(self) -> list:
+        """获取四大指数数据"""
+        from app.services import market_service
+
+        index_names = ("上证指数", "深证成指", "创业板指", "沪深300")
+        results = []
+        for name in index_names:
+            try:
+                data = market_service.get_index_quote(name)
+                results.append({"name": name, "data": data})
+            except Exception as exc:
+                logger.debug("记忆系统获取指数失败 %s: %s", name, exc)
+        return results
+
+    def _fetch_watchlist(self) -> dict:
+        """获取自选股列表(含行情数据)"""
+        from app.services import watchlist_service
+
+        try:
+            wl = watchlist_service.get_watchlist()
+            if wl.get("success"):
+                self._watchlist_count = wl.get("total", 0)
+            return wl
+        except Exception as exc:
+            logger.debug("记忆系统获取自选股失败: %s", exc)
+            return {"success": False, "stocks": [], "total": 0}
+
+    def _fetch_hot_news(self) -> dict:
+        """获取热点资讯"""
+        from app.services import news_service
+
+        try:
+            return news_service.search_market_news() or {}
+        except Exception as exc:
+            logger.debug("记忆系统获取热点资讯失败: %s", exc)
+            return {}
+
+    def parallel_fetch(self) -> MemorySnapshot:
+        """
+        三线程并行获取仪表盘数据:指数、自选、热点资讯
+        """
+        logger.info("记忆系统: 开始三线程并行获取仪表盘数据...")
+
+        with ThreadPoolExecutor(max_workers=3) as executor:
+            future_indices = executor.submit(self._fetch_indices)
+            future_watchlist = executor.submit(self._fetch_watchlist)
+            future_news = executor.submit(self._fetch_hot_news)
+
+            results: dict[str, Any] = {}
+            for future in as_completed([future_indices, future_watchlist, future_news]):
+                try:
+                    value = future.result()
+                except Exception as exc:
+                    logger.warning("并行获取任务失败: %s", exc)
+                    value = None
+
+                if future == future_indices:
+                    results["indices"] = value or []
+                elif future == future_watchlist:
+                    results["watchlist"] = value or {}
+                elif future == future_news:
+                    results["hot_news"] = value or {}
+
+        today = self._get_today()
+        snapshot = MemorySnapshot(
+            date_str=today,
+            indices=results.get("indices", []),
+            watchlist=results.get("watchlist", {}),
+            hot_news=results.get("hot_news", {}),
+            watchlist_count=self._watchlist_count,
+        )
+
+        with self._lock:
+            self._today = today
+            self._snapshot = snapshot
+            self._save_state()
+
+        logger.info("记忆系统: 仪表盘数据获取完成 (date=%s, indices=%d, watchlist=%d)",
+                     today, len(snapshot.indices), snapshot.watchlist_count)
+        return snapshot
+
+    # ---- 公共接口 ----
+
+    def get_snapshot(self) -> Optional[MemorySnapshot]:
+        """获取当前缓存的仪表盘快照"""
+        with self._lock:
+            return self._snapshot
+
+    def get_indices(self) -> list:
+        """获取缓存的指数数据"""
+        snap = self.get_snapshot()
+        return snap.indices if snap else []
+
+    def get_watchlist(self) -> dict:
+        """获取缓存的自选股数据"""
+        snap = self.get_snapshot()
+        return snap.watchlist if snap else {}
+
+    def get_hot_news(self) -> dict:
+        """获取缓存的热点资讯数据"""
+        snap = self.get_snapshot()
+        return snap.hot_news if snap else {}
+
+    def clear(self) -> None:
+        """清空所有记忆数据"""
+        with self._lock:
+            self._today = None
+            self._snapshot = None
+            self._watchlist_count = 0
+            try:
+                if self._state_file.exists():
+                    self._state_file.unlink()
+            except Exception:
+                pass
+
+    def get_stats(self) -> dict:
+        """获取记忆系统状态"""
+        with self._lock:
+            return {
+                "today": self._today,
+                "has_snapshot": self._snapshot is not None,
+                "watchlist_count": self._watchlist_count,
+                "indices_count": len(self._snapshot.indices) if self._snapshot else 0,
+                "storage_dir": str(self._storage_dir),
+            }
+
+
+_memory_svc: Optional[MemoryService] = None
+
+
+def get_memory_service() -> MemoryService:
+    """获取 MemoryService 全局单例"""
+    global _memory_svc
+    if _memory_svc is None:
+        with _memory_lock:
+            if _memory_svc is None:
+                _memory_svc = MemoryService()
+    return _memory_svc

+ 88 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/services/mx_timed_cache.py

@@ -0,0 +1,88 @@
+"""
+妙想类接口计时缓存(进程内)
+
+- 默认 TTL 10 分钟内相同查询直接返回缓存,减少重复调用。
+- 额度用尽时可用过期缓存降级(见各 service 封装)。
+"""
+
+from __future__ import annotations
+
+import copy
+import hashlib
+import threading
+import time
+from typing import Any, Optional
+
+from app.config import settings
+
+
+class MXTimedCache:
+    """线程安全的查询串 -> 结果快照缓存"""
+
+    def __init__(self, max_entries: int = 400):
+        self._max = max_entries
+        self._lock = threading.Lock()
+        # key -> (monotonic_ts, payload)
+        self._data: dict[str, tuple[float, Any]] = {}
+        self._order: list[str] = []
+
+    @staticmethod
+    def normalize_query(q: str) -> str:
+        return " ".join((q or "").split())
+
+    def make_key(self, channel: str, query: str) -> str:
+        n = self.normalize_query(query)
+        digest = hashlib.sha256(n.encode("utf-8")).hexdigest()[:32]
+        return f"{channel}:{digest}"
+
+    def get_fresh(self, key: str, ttl_seconds: float) -> Optional[Any]:
+        if ttl_seconds <= 0:
+            return None
+        with self._lock:
+            ent = self._data.get(key)
+            if not ent:
+                return None
+            ts, payload = ent
+            if time.monotonic() - ts > ttl_seconds:
+                return None
+            return copy.deepcopy(payload)
+
+    def get_stale(self, key: str) -> Optional[Any]:
+        """不按 TTL,只要有快照即返回(用于额度耗尽降级)"""
+        with self._lock:
+            ent = self._data.get(key)
+            if not ent:
+                return None
+            return copy.deepcopy(ent[1])
+
+    def set(self, key: str, payload: Any) -> None:
+        snap = copy.deepcopy(payload)
+        with self._lock:
+            self._data[key] = (time.monotonic(), snap)
+            if key in self._order:
+                self._order.remove(key)
+            self._order.append(key)
+            while len(self._order) > self._max:
+                old = self._order.pop(0)
+                self._data.pop(old, None)
+
+    def delete(self, key: str) -> None:
+        """主动失效(如自选股增删后勿长期使用旧列表快照)"""
+        with self._lock:
+            self._data.pop(key, None)
+            if key in self._order:
+                self._order.remove(key)
+
+
+_mx_cache_singleton: Optional[MXTimedCache] = None
+
+
+def get_mx_timed_cache() -> MXTimedCache:
+    global _mx_cache_singleton
+    if _mx_cache_singleton is None:
+        _mx_cache_singleton = MXTimedCache()
+    return _mx_cache_singleton
+
+
+def mx_cache_ttl_seconds() -> float:
+    return float(getattr(settings, "MX_CACHE_TTL_SECONDS", 600))

+ 402 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/services/news_service.py

@@ -0,0 +1,402 @@
+"""
+智能股票分析助手 — 资讯搜索服务层
+
+封装金融资讯搜索、个股舆情分析的数据查询逻辑。
+含 mx-search 计时缓存与额度用尽时的缓存降级。
+"""
+
+from __future__ import annotations
+
+import copy
+import sys
+from pathlib import Path
+
+_PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
+_AGENTS_DIR = _PROJECT_ROOT / "agents"
+_SKILLS_SEARCH = _PROJECT_ROOT / "skills" / "资讯搜索" / "mx-search"
+
+for p in [_AGENTS_DIR, _SKILLS_SEARCH, str(_PROJECT_ROOT)]:
+    if str(p) not in sys.path:
+        sys.path.insert(0, str(p))
+
+from app.config import settings
+from app.services.mx_timed_cache import get_mx_timed_cache, mx_cache_ttl_seconds
+from app.utils.mx_fixture import try_load_raw_fixture
+from app.utils.mx_quota import MX_QUOTA_HINT, is_mx_quota_exhausted, quota_exhausted_no_cache_message
+
+
+def _meta_block(*, from_cache: bool, quota_exhausted: bool, channel: str) -> dict:
+    m = {
+        "from_cache": from_cache,
+        "quota_exhausted": quota_exhausted,
+        "cache_ttl_seconds": int(mx_cache_ttl_seconds()),
+        "channel": channel,
+    }
+    if quota_exhausted:
+        m["hint"] = MX_QUOTA_HINT
+    return m
+
+
+def _attach(payload: dict, meta: dict) -> dict:
+    out = copy.deepcopy(payload)
+    out["_mx_meta"] = meta
+    return out
+
+
+def _merge_item_text_fields(item: dict) -> str:
+    """合并正文/摘要等字段(妙想不同条目可能落在 content、summary 等键上)。"""
+    chunks: list[str] = []
+    seen: set[str] = set()
+    for key in (
+        "content",
+        "summary",
+        "abstract",
+        "digest",
+        "snippet",
+        "description",
+        "desc",
+        "text",
+    ):
+        v = item.get(key)
+        if not isinstance(v, str):
+            continue
+        s = v.strip()
+        if not s or s in seen:
+            continue
+        seen.add(s)
+        chunks.append(s)
+    return "\n\n".join(chunks)
+
+
+def _normalize_information_type(item: dict) -> tuple[str, str]:
+    """将妙想条目上的类型字段统一为 (NEWS|REPORT|ANNOUNCEMENT|OTHER, 中文标签)。
+
+    常见坑:仅写了 info_type 默认 NEWS,但 info_type_cn 因 informationType 为空变成「资讯」,
+    前端饼图只认三种中文,导致大量条目不计入分布。
+    """
+    raw = item.get("informationType")
+    if raw is None or (isinstance(raw, str) and not raw.strip()):
+        for k in ("infoType", "information_type", "info_type", "docType", "category", "dataType"):
+            v = item.get(k)
+            if v is not None and str(v).strip():
+                raw = v
+                break
+
+    cn_by_en = {"NEWS": "新闻", "REPORT": "研报", "ANNOUNCEMENT": "公告", "OTHER": "其他"}
+
+    if raw is None or (isinstance(raw, str) and not str(raw).strip()):
+        return "NEWS", cn_by_en["NEWS"]
+
+    if isinstance(raw, (int, float)):
+        # 无公开数字枚举说明时不猜测,避免错分进「仅研报」等畸形分布
+        return "OTHER", cn_by_en["OTHER"]
+
+    s = str(raw).strip()
+    u = s.upper().replace(" ", "_").replace("-", "_")
+
+    if u in ("NEWS", "REPORT", "ANNOUNCEMENT"):
+        return u, cn_by_en[u]
+
+    # 小写 json:news / report / announcement
+    low = s.lower()
+    if low in ("news",):
+        return "NEWS", cn_by_en["NEWS"]
+    if low in ("report",):
+        return "REPORT", cn_by_en["REPORT"]
+    if low in ("announcement", "announce"):
+        return "ANNOUNCEMENT", cn_by_en["ANNOUNCEMENT"]
+
+    # 中文或混合文案
+    if "公告" in s:
+        return "ANNOUNCEMENT", cn_by_en["ANNOUNCEMENT"]
+    if "研报" in s or "研究报告" in s:
+        return "REPORT", cn_by_en["REPORT"]
+    if "新闻" in s:
+        return "NEWS", cn_by_en["NEWS"]
+
+    if "ANNOUNCE" in u or "NOTICE" in u:
+        return "ANNOUNCEMENT", cn_by_en["ANNOUNCEMENT"]
+    if "REPORT" in u:
+        return "REPORT", cn_by_en["REPORT"]
+    if "NEWS" in u:
+        return "NEWS", cn_by_en["NEWS"]
+
+    return "OTHER", cn_by_en["OTHER"]
+
+
+# 妙想 / 东方财富资讯条目可能出现的链接字段(含嵌套 dict 扫描)
+_URL_KEYS_ORDERED = (
+    "url",
+    "link",
+    "articleUrl",
+    "sourceUrl",
+    "detailUrl",
+    "pcUrl",
+    "h5Url",
+    "jumpUrl",
+    "artUrl",
+    "newsUrl",
+    "oriUrl",
+    "originUrl",
+    "showUrl",
+    "pageUrl",
+    "wapUrl",
+    "pcLink",
+    "h5Link",
+    "article_url",
+    "news_url",
+    "srcUrl",
+    "webUrl",
+    "mobileUrl",
+    "urlPc",
+    "urlH5",
+    "pc_url",
+    "h5_url",
+    "shareUrl",
+    "share_link",
+)
+
+# 递归扫描时跳过明显正文/标题字段,避免误把片段当外链
+_SKIP_URL_SCAN_KEYS = frozenset(
+    {
+        "content",
+        "summary",
+        "abstract",
+        "digest",
+        "snippet",
+        "description",
+        "desc",
+        "text",
+        "title",
+        "body",
+        "rawContent",
+        "answer",
+    }
+)
+
+
+def _item_original_url(item: dict, *, depth: int = 0) -> str:
+    """提取可外链打开的原文地址(若有)。兼容多层嵌套与非常规字段名。"""
+    if not isinstance(item, dict) or depth > 6:
+        return ""
+
+    for key in _URL_KEYS_ORDERED:
+        v = item.get(key)
+        if isinstance(v, str):
+            s = v.strip()
+            if s.lower().startswith(("http://", "https://")):
+                return s
+
+    for k, v in item.items():
+        if k in _SKIP_URL_SCAN_KEYS:
+            continue
+        if isinstance(v, str):
+            s = v.strip()
+            if len(s) > 2048:
+                continue
+            if s.lower().startswith(("http://", "https://")):
+                return s
+        elif isinstance(v, dict):
+            inner = _item_original_url(v, depth=depth + 1)
+            if inner:
+                return inner
+        elif isinstance(v, list) and depth < 4:
+            for el in v[:24]:
+                if isinstance(el, dict):
+                    inner = _item_original_url(el, depth=depth + 1)
+                    if inner:
+                        return inner
+    return ""
+
+
+def _mx_search_from_raw(query: str, raw_result: dict) -> dict:
+    """将 mx-search 原始响应转为统一 payload"""
+    result = {
+        "success": False,
+        "query": query,
+        "total_count": 0,
+        "items": [],
+        "error": None,
+    }
+
+    status = raw_result.get("status")
+    if status != 0:
+        result["error"] = raw_result.get("message", f"API返回错误,状态码: {status}")
+        return result
+
+    data = raw_result.get("data", {})
+    inner_data = data.get("data", {})
+    search_response = inner_data.get("llmSearchResponse", {})
+    items = search_response.get("data", []) or []
+
+    parsed_items = []
+    for item in items:
+        if not isinstance(item, dict):
+            continue
+        body = _merge_item_text_fields(item)
+        en_type, cn_type = _normalize_information_type(item)
+        parsed_items.append({
+            "title": item.get("title", "无标题"),
+            "content": body,
+            "date": item.get("date", ""),
+            "institution": item.get("insName", ""),
+            "info_type": en_type,
+            "info_type_cn": cn_type,
+            "rating": item.get("rating", ""),
+            "entity_name": item.get("entityFullName", ""),
+            "url": _item_original_url(item),
+        })
+
+    result["success"] = True
+    result["total_count"] = len(parsed_items)
+    result["items"] = parsed_items
+    return result
+
+
+def _fetch_mx_search_live(query: str) -> dict:
+    """直连 mx-search;MX_REPLAY_FIXTURES 时优先读本地原始 JSON"""
+    raw_fixture = try_load_raw_fixture("mx_search", query)
+    if raw_fixture is not None:
+        try:
+            return _mx_search_from_raw(query, raw_fixture)
+        except Exception as e:
+            return {
+                "success": False,
+                "query": query,
+                "total_count": 0,
+                "items": [],
+                "error": f"[fixture] {e}",
+            }
+
+    key_ok = bool(settings.MX_APIKEY and settings.MX_APIKEY != "your-mx-apikey-here")
+    if not key_ok:
+        return {
+            "success": False,
+            "query": query,
+            "total_count": 0,
+            "items": [],
+            "error": "MX_APIKEY 未配置,且无匹配的本地 fixture",
+        }
+
+    try:
+        import mx_search as _mx
+
+        search_client = _mx.MXSearch(api_key=settings.MX_APIKEY)
+        raw_result = search_client.search(query)
+        return _mx_search_from_raw(query, raw_result)
+
+    except Exception as e:
+        return {
+            "success": False,
+            "query": query,
+            "total_count": 0,
+            "items": [],
+            "error": str(e),
+        }
+
+
+def search_news(query: str) -> dict:
+    """搜索金融资讯(同一 query 在 TTL 内走缓存;关键词/个股不同则 query 不同)"""
+    result = {
+        "success": False,
+        "query": query,
+        "total_count": 0,
+        "items": [],
+        "error": None,
+    }
+
+    key_missing = not settings.MX_APIKEY or settings.MX_APIKEY == "your-mx-apikey-here"
+    if key_missing and not settings.MX_REPLAY_FIXTURES:
+        result["error"] = "MX_APIKEY 未配置"
+        return result
+
+    cache = get_mx_timed_cache()
+    ttl = mx_cache_ttl_seconds()
+    key = cache.make_key("mx_search", query)
+
+    fresh = cache.get_fresh(key, ttl)
+    if fresh is not None:
+        return _attach(fresh, _meta_block(from_cache=True, quota_exhausted=False, channel="mx_search"))
+
+    live = _fetch_mx_search_live(query)
+
+    if live["success"]:
+        cache.set(key, live)
+        return _attach(live, _meta_block(from_cache=False, quota_exhausted=False, channel="mx_search"))
+
+    err = live.get("error") or ""
+    if is_mx_quota_exhausted(err):
+        stale = cache.get_stale(key)
+        if stale:
+            merged = copy.deepcopy(stale)
+            merged["success"] = True
+            merged["query"] = query
+            return _attach(merged, _meta_block(from_cache=True, quota_exhausted=True, channel="mx_search"))
+        live["error"] = quota_exhausted_no_cache_message(err)
+        return live
+
+    return live
+
+
+def search_stock_news(code: str) -> dict:
+    """搜索个股相关资讯"""
+    query = f"{code} 最新研报 新闻 公告"
+    return search_news(query)
+
+
+def search_sector_news(sector: str) -> dict:
+    """搜索行业/板块相关资讯"""
+    query = f"{sector}板块近期新闻 政策解读"
+    return search_news(query)
+
+
+def search_market_news() -> dict:
+    """搜索市场热门资讯"""
+    query = "今日A股市场热点 大盘动态 北向资金"
+    return search_news(query)
+
+
+def analyze_sentiment(code: str) -> dict:
+    """个股舆情分析(文件缓存优先 + 继承底层 search 的 _mx_meta)"""
+    from app.services.stock_file_cache import get_stock_file_cache
+    fc = get_stock_file_cache()
+
+    cached = fc.get(code, "sentiment")
+    if cached and cached.get("data") and cached["data"].get("success"):
+        return cached["data"]
+
+    base = search_stock_news(code)
+    meta = base.get("_mx_meta")
+
+    if not base["success"]:
+        out = {
+            "success": False,
+            "code": code,
+            "total_count": 0,
+            "news_items": [],
+            "report_items": [],
+            "announce_items": [],
+            "error": base["error"],
+        }
+        if meta:
+            out["_mx_meta"] = meta
+        return out
+
+    items = base["items"]
+    report_items = [i for i in items if i["info_type"] == "REPORT"]
+    announce_items = [i for i in items if i["info_type"] == "ANNOUNCEMENT"]
+    news_items = [i for i in items if i["info_type"] not in ("REPORT", "ANNOUNCEMENT")]
+    out = {
+        "success": True,
+        "code": code,
+        "total_count": base["total_count"],
+        "news_items": news_items,
+        "report_items": report_items,
+        "announce_items": announce_items,
+        "error": None,
+    }
+    if meta:
+        out["_mx_meta"] = meta
+
+    fc.set(code, "sentiment", out)
+    return out

+ 170 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/services/preference_service.py

@@ -0,0 +1,170 @@
+"""
+智能股票分析助手 — 用户偏好服务层
+
+提供偏好的CRUD操作,以及向智能体层输出偏好上下文的方法。
+"""
+
+import json
+from typing import Optional
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.models.preference import UserPreference
+
+
+# =========================================================================
+# 默认偏好(当用户未配置或无登录时使用)
+# =========================================================================
+DEFAULT_PREFERENCE = {
+    "risk_tolerance": "moderate",
+    "investment_style": "blend",
+    "preferred_sectors": [],
+    "excluded_sectors": [],
+    "investment_horizon": "medium",
+    "target_return_rate": 10.0,
+    "max_position_ratio": 30.0,
+    "max_drawdown_limit": -15.0,
+    "notification_enabled": True,
+    "notification_channels": ["push"],
+    "market_alert_threshold": 5.0,
+    "language": "zh",
+    "theme": "auto",
+    "default_view": "dashboard",
+}
+
+
+# =========================================================================
+# CRUD 操作
+# =========================================================================
+
+async def get_preference(db: AsyncSession, user_id: str = "default") -> dict:
+    """获取用户偏好,不存在时返回默认值"""
+    result = await db.execute(
+        select(UserPreference).where(UserPreference.user_id == user_id)
+    )
+    pref = result.scalar_one_or_none()
+    if pref is None:
+        return {**DEFAULT_PREFERENCE, "user_id": user_id}
+    return pref.to_dict()
+
+
+async def get_or_create_preference(db: AsyncSession, user_id: str = "default") -> UserPreference:
+    """获取或创建用户偏好记录,返回ORM对象"""
+    result = await db.execute(
+        select(UserPreference).where(UserPreference.user_id == user_id)
+    )
+    pref = result.scalar_one_or_none()
+    if pref is None:
+        pref = UserPreference.create_default(user_id)
+        db.add(pref)
+        await db.commit()
+        await db.refresh(pref)
+    return pref
+
+
+async def update_preference(db: AsyncSession, user_id: str, data: dict) -> dict:
+    """更新用户偏好,支持部分更新"""
+    pref = await get_or_create_preference(db, user_id)
+
+    # 允许更新的字段白名单(防止注入未定义的字段)
+    ALLOWED_FIELDS = {
+        "risk_tolerance", "investment_style", "investment_horizon",
+        "target_return_rate", "max_position_ratio", "max_drawdown_limit",
+        "notification_enabled", "market_alert_threshold",
+        "language", "theme", "default_view",
+        # 以下是JSON字段
+        "preferred_sectors", "excluded_sectors", "notification_channels",
+    }
+
+    for key, value in data.items():
+        if key not in ALLOWED_FIELDS:
+            continue
+
+        # JSON数组字段序列化
+        if key in ("preferred_sectors", "excluded_sectors", "notification_channels"):
+            if value is not None:
+                setattr(pref, key, json.dumps(value, ensure_ascii=False))
+        # 布尔值字段
+        elif key == "notification_enabled":
+            setattr(pref, key, bool(value))
+        # 数值字段
+        elif key in ("target_return_rate", "max_position_ratio", "max_drawdown_limit", "market_alert_threshold"):
+            setattr(pref, key, float(value))
+        else:
+            setattr(pref, key, value)
+
+    await db.commit()
+    await db.refresh(pref)
+    return pref.to_dict()
+
+
+# =========================================================================
+# 智能体注入方法
+# =========================================================================
+
+async def get_preference_context(db: AsyncSession, user_id: str = "default") -> str:
+    """生成偏好上下文文本,供智能体层注入分析流程
+
+    返回格式化的中文描述,可直接作为Agent系统提示词的一部分。
+    """
+    pref = await get_preference(db, user_id)
+    if pref is None:
+        pref = DEFAULT_PREFERENCE
+
+    risk_map = {
+        "conservative": "保守型——侧重低估值、高股息、蓝筹股,回避高风险标的",
+        "moderate": "稳健型——均衡配置,兼顾成长与价值",
+        "aggressive": "激进型——侧重高成长、高波动标的,接受较大回撤",
+    }
+    style_map = {
+        "value": "价值投资——偏好低PE、低PB、高股息率标的",
+        "growth": "成长投资——偏好高营收增速、高利润增速标的",
+        "momentum": "动量投资——偏好强势股、趋势跟踪",
+        "dividend": "股息投资——偏好高分红率标的",
+        "blend": "混合风格——综合运用多种投资策略",
+    }
+    horizon_map = {
+        "short": "短期(<1年)",
+        "medium": "中期(1-3年)",
+        "long": "长期(>3年)",
+    }
+
+    preferred = json.loads(pref.get("preferred_sectors", "[]")) if isinstance(pref.get("preferred_sectors"), str) else pref.get("preferred_sectors", [])
+    excluded = json.loads(pref.get("excluded_sectors", "[]")) if isinstance(pref.get("excluded_sectors"), str) else pref.get("excluded_sectors", [])
+
+    context_parts = [
+        "## 用户投资偏好(请据此调整分析和建议)",
+        f"- 风险承受度: {risk_map.get(pref['risk_tolerance'], pref['risk_tolerance'])}",
+        f"- 投资风格: {style_map.get(pref['investment_style'], pref['investment_style'])}",
+        f"- 投资期限: {horizon_map.get(pref['investment_horizon'], pref['investment_horizon'])}",
+        f"- 目标年化收益率: {pref['target_return_rate']}%",
+        f"- 单票最大仓位: {pref['max_position_ratio']}%",
+        f"- 最大回撤预警线: {pref['max_drawdown_limit']}%",
+    ]
+
+    if preferred:
+        context_parts.append(f"- 偏好行业: {', '.join(preferred)}")
+    if excluded:
+        context_parts.append(f"- 排除行业: {', '.join(excluded)}")
+
+    return "\n".join(context_parts)
+
+
+async def get_profile_summary(db: AsyncSession, user_id: str = "default") -> dict:
+    """获取用户投资画像摘要(用于前端偏好摘要展示)"""
+    pref = await get_preference(db, user_id)
+    if pref is None:
+        pref = DEFAULT_PREFERENCE
+
+    risk_labels = {"conservative": "保守型", "moderate": "稳健型", "aggressive": "激进型"}
+    style_labels = {"value": "价值投资", "growth": "成长投资", "momentum": "动量投资", "dividend": "股息投资", "blend": "混合风格"}
+
+    return {
+        "user_id": pref["user_id"],
+        "risk_label": risk_labels.get(pref["risk_tolerance"], pref["risk_tolerance"]),
+        "style_label": style_labels.get(pref["investment_style"], pref["investment_style"]),
+        "target_return": f"{pref['target_return_rate']}%",
+        "max_drawdown": f"{pref['max_drawdown_limit']}%",
+        "preferred_sectors_count": len(pref.get("preferred_sectors", []) if isinstance(pref.get("preferred_sectors"), list) else json.loads(pref.get("preferred_sectors", "[]"))),
+        "is_configured": pref.get("id") is not None,
+    }

+ 192 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/services/screener_service.py

@@ -0,0 +1,192 @@
+"""
+智能股票分析助手 — 智能选股服务层
+
+封装智能选股查询、条件解析和数据格式化逻辑。
+含 mx-xuangu 计时缓存与额度用尽时的缓存降级。
+"""
+
+from __future__ import annotations
+
+import copy
+import sys
+from pathlib import Path
+
+_PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
+_AGENTS_DIR = _PROJECT_ROOT / "agents"
+_SKILLS_XUANGU = _PROJECT_ROOT / "skills" / "智能选股" / "mx-xuangu"
+
+for p in [_AGENTS_DIR, _SKILLS_XUANGU, str(_PROJECT_ROOT)]:
+    if str(p) not in sys.path:
+        sys.path.insert(0, str(p))
+
+from app.config import settings
+from app.services.mx_timed_cache import get_mx_timed_cache, mx_cache_ttl_seconds
+from app.utils.mx_fixture import try_load_raw_fixture
+from app.utils.mx_quota import MX_QUOTA_HINT, is_mx_quota_exhausted, quota_exhausted_no_cache_message
+
+
+def _meta_block(*, from_cache: bool, quota_exhausted: bool, channel: str) -> dict:
+    m = {
+        "from_cache": from_cache,
+        "quota_exhausted": quota_exhausted,
+        "cache_ttl_seconds": int(mx_cache_ttl_seconds()),
+        "channel": channel,
+    }
+    if quota_exhausted:
+        m["hint"] = MX_QUOTA_HINT
+    return m
+
+
+def _attach(payload: dict, meta: dict) -> dict:
+    out = copy.deepcopy(payload)
+    out["_mx_meta"] = meta
+    return out
+
+
+def _fetch_screen_live(query: str) -> dict:
+    import mx_xuangu as _mx
+
+    result = {
+        "success": False,
+        "query": query,
+        "total_count": 0,
+        "data_source": "",
+        "stocks": [],
+        "conditions": [],
+        "error": None,
+    }
+
+    raw_fixture = try_load_raw_fixture("mx_xuangu", query)
+    raw_result = raw_fixture
+
+    try:
+        if raw_result is None:
+            key_ok = bool(settings.MX_APIKEY and settings.MX_APIKEY != "your-mx-apikey-here")
+            if not key_ok:
+                result["error"] = "MX_APIKEY 未配置,且无匹配的本地 fixture"
+                return result
+            screener = _mx.MXSelectStock(api_key=settings.MX_APIKEY)
+            raw_result = screener.search(query)
+
+        rows, data_source, error = _mx.MXSelectStock.extract_data(raw_result)
+
+        if error:
+            result["error"] = (f"[fixture] {error}" if raw_fixture is not None else error)
+            return result
+
+        data = raw_result.get("data", {})
+        inner_data = data.get("data", {})
+        response_conditions = inner_data.get("responseConditionList", []) or []
+
+        conditions = []
+        for cond in response_conditions:
+            if isinstance(cond, dict):
+                conditions.append({
+                    "describe": cond.get("describe", ""),
+                    "stock_count": cond.get("stockCount", 0),
+                })
+
+        result["success"] = True
+        result["total_count"] = len(rows)
+        result["data_source"] = data_source
+        result["stocks"] = rows
+        result["conditions"] = conditions
+        return result
+
+    except Exception as e:
+        result["error"] = (f"[fixture] {e}" if raw_fixture is not None else str(e))
+        return result
+
+
+def screen_stocks(query: str) -> dict:
+    """执行智能选股(选股条件字符串不同则缓存键不同)"""
+    result = {
+        "success": False,
+        "query": query,
+        "total_count": 0,
+        "data_source": "",
+        "stocks": [],
+        "conditions": [],
+        "error": None,
+    }
+
+    key_missing = not settings.MX_APIKEY or settings.MX_APIKEY == "your-mx-apikey-here"
+    if key_missing and not settings.MX_REPLAY_FIXTURES:
+        result["error"] = "MX_APIKEY 未配置"
+        return result
+
+    cache = get_mx_timed_cache()
+    ttl = mx_cache_ttl_seconds()
+    key = cache.make_key("mx_xuangu", query)
+
+    fresh = cache.get_fresh(key, ttl)
+    if fresh is not None:
+        return _attach(fresh, _meta_block(from_cache=True, quota_exhausted=False, channel="mx_xuangu"))
+
+    live = _fetch_screen_live(query)
+
+    if live["success"]:
+        cache.set(key, live)
+        return _attach(live, _meta_block(from_cache=False, quota_exhausted=False, channel="mx_xuangu"))
+
+    err = live.get("error") or ""
+    if is_mx_quota_exhausted(err):
+        stale = cache.get_stale(key)
+        if stale:
+            merged = copy.deepcopy(stale)
+            merged["success"] = True
+            merged["query"] = query
+            return _attach(merged, _meta_block(from_cache=True, quota_exhausted=True, channel="mx_xuangu"))
+        live["error"] = quota_exhausted_no_cache_message(err)
+        return live
+
+    return live
+
+
+def get_available_conditions() -> dict:
+    """获取常用的选股条件参考(静态说明,不调用妙想)"""
+    return {
+        "success": True,
+        "categories": [
+            {
+                "name": "行情指标",
+                "description": "基于实时行情数据的筛选条件",
+                "examples": [
+                    "今日涨幅大于2%",
+                    "成交量大于10亿",
+                    "股价在10元到20元之间",
+                    "换手率大于5%",
+                ],
+            },
+            {
+                "name": "财务指标",
+                "description": "基于财务报表数据的筛选条件",
+                "examples": [
+                    "市盈率小于20",
+                    "市净率小于2",
+                    "ROE大于15%",
+                    "净利润增长率大于20%",
+                    "股息率大于3%",
+                ],
+            },
+            {
+                "name": "行业板块",
+                "description": "限定行业或板块范围的筛选",
+                "examples": [
+                    "新能源板块",
+                    "白酒板块",
+                    "半导体行业",
+                    "银行股",
+                ],
+            },
+            {
+                "name": "指数成分",
+                "description": "指定指数成分股内筛选",
+                "examples": [
+                    "沪深300成分股",
+                    "创业板成分股",
+                    "上证50成分股",
+                ],
+            },
+        ],
+    }

+ 381 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/services/simulation_service.py

@@ -0,0 +1,381 @@
+"""
+智能股票分析助手 — 模拟交易服务层
+
+封装模拟交易操作(持仓查询、资金查询、委托下单、撤单等),供API路由层调用。
+"""
+
+import sys
+from pathlib import Path
+from typing import Optional
+
+# 确保skills路径可导入
+_PROJECT_ROOT = Path(__file__).parent.parent.parent.parent  # backend/app/services -> project root
+_AGENTS_DIR = _PROJECT_ROOT / "agents"
+_SKILLS_MONI = _PROJECT_ROOT / "skills" / "模拟组合管理" / "mx-moni"
+
+for p in [_AGENTS_DIR, _SKILLS_MONI, str(_PROJECT_ROOT)]:
+    if str(p) not in sys.path:
+        sys.path.insert(0, str(p))
+
+import requests
+from app.config import settings
+from app.utils.mock_trading_normalize import extract_orders_dicts, normalize_mock_order_row
+
+# API基础地址
+MX_API_URL = "https://mkapi2.dfcfs.com/finskillshub"
+
+
+def _make_request(endpoint: str, body: dict) -> dict:
+    """发送模拟交易API请求
+
+    Args:
+        endpoint: API端点路径
+        body: 请求体
+
+    Returns:
+        API响应JSON
+    """
+    headers = {
+        "apikey": settings.MX_APIKEY,
+        "Content-Type": "application/json",
+    }
+    response = requests.post(
+        f"{MX_API_URL}{endpoint}",
+        headers=headers,
+        json=body,
+        timeout=30,
+    )
+    response.raise_for_status()
+    return response.json()
+
+
+def _check_api_ready() -> Optional[dict]:
+    """检查API是否就绪,未就绪返回错误字典"""
+    if not settings.MX_APIKEY or settings.MX_APIKEY == "your-mx-apikey-here":
+        return {"error": "MX_APIKEY 未配置"}
+    return None
+
+
+def get_positions() -> dict:
+    """查询模拟持仓
+
+    Returns:
+        {
+            "success": True/False,
+            "positions": [{"code": str, "name": str, "quantity": int, ...}, ...],
+            "total": int,
+            "error": str or None
+        }
+    """
+    result = {
+        "success": False,
+        "positions": [],
+        "total": 0,
+        "error": None,
+    }
+
+    api_error = _check_api_ready()
+    if api_error:
+        result["error"] = api_error["error"]
+        return result
+
+    try:
+        raw = _make_request("/api/claw/mockTrading/positions", {"moneyUnit": 1})
+
+        if not raw.get("success") and str(raw.get("code")) != "200":
+            result["error"] = raw.get("message", "查询持仓失败")
+            return result
+
+        data = raw.get("data", {})
+        positions = data.get("positions", [])
+
+        parsed = []
+        for pos in (positions or []):
+            parsed.append({
+                "stock_code": pos.get("stockCode", ""),
+                "stock_name": pos.get("stockName", ""),
+                "quantity": pos.get("quantity", 0),
+                "cost_price": pos.get("costPrice", 0),
+                "current_price": pos.get("currentPrice", 0),
+                "profit_loss": pos.get("profitLoss", 0),
+                "profit_loss_rate": pos.get("profitLossRate", 0),
+                "market_value": pos.get("marketValue", 0),
+            })
+
+        result["success"] = True
+        result["positions"] = parsed
+        result["total"] = len(parsed)
+        return result
+
+    except Exception as e:
+        result["error"] = str(e)
+        return result
+
+
+def get_balance() -> dict:
+    """查询模拟账户资金
+
+    Returns:
+        {
+            "success": True/False,
+            "balance": {...},
+            "error": str or None
+        }
+    """
+    result = {
+        "success": False,
+        "balance": {},
+        "error": None,
+    }
+
+    api_error = _check_api_ready()
+    if api_error:
+        result["error"] = api_error["error"]
+        return result
+
+    try:
+        raw = _make_request("/api/claw/mockTrading/balance", {"moneyUnit": 1})
+
+        if not raw.get("success") and str(raw.get("code")) != "200":
+            result["error"] = raw.get("message", "查询资金失败")
+            return result
+
+        data = raw.get("data", {})
+        result["success"] = True
+        result["balance"] = {
+            "total_assets": data.get("totalAssets", 0),
+            "available_balance": data.get("availBalance", 0),
+            "frozen_balance": data.get("frozenBalance", 0),
+            "market_value": data.get("marketValue", 0),
+            "total_profit_loss": data.get("totalProfitLoss", 0),
+        }
+        return result
+
+    except Exception as e:
+        result["error"] = str(e)
+        return result
+
+
+def get_orders() -> dict:
+    """查询委托记录
+
+    Returns:
+        {
+            "success": True/False,
+            "orders": [...],
+            "total": int,
+            "error": str or None
+        }
+    """
+    result = {
+        "success": False,
+        "orders": [],
+        "total": 0,
+        "error": None,
+    }
+
+    api_error = _check_api_ready()
+    if api_error:
+        result["error"] = api_error["error"]
+        return result
+
+    try:
+        raw = _make_request("/api/claw/mockTrading/orders", {
+            "fltOrderDrt": 0,
+            "fltOrderStatus": 0,
+        })
+
+        if not raw.get("success") and str(raw.get("code")) != "200":
+            result["error"] = raw.get("message", "查询委托失败")
+            return result
+
+        data = raw.get("data", {}) or {}
+        # 妙想可能返回 list,或 { rows: [] },或当日/历史分段字段
+        orders = extract_orders_dicts(data)
+
+        parsed = []
+        for order in orders:
+            if not isinstance(order, dict):
+                continue
+            # 统一解析字段名与枚举(数值买卖方向、委托状态等)
+            parsed.append(normalize_mock_order_row(order))
+
+        result["success"] = True
+        result["orders"] = parsed
+        result["total"] = len(parsed)
+        return result
+
+    except Exception as e:
+        result["error"] = str(e)
+        return result
+
+
+def place_order(
+    trade_type: str,
+    stock_code: str,
+    quantity: int,
+    price: Optional[float] = None,
+) -> dict:
+    """模拟下单(买入/卖出)
+
+    Args:
+        trade_type: 交易类型 "buy" 或 "sell"
+        stock_code: 6位股票代码
+        quantity: 委托数量(必须为100的整数倍)
+        price: 委托价格(None表示市价委托)
+
+    Returns:
+        {
+            "success": True/False,
+            "order_id": str,
+            "message": str,
+            "error": str or None
+        }
+    """
+    result = {
+        "success": False,
+        "order_id": "",
+        "message": "",
+        "error": None,
+    }
+
+    # 参数校验
+    if trade_type not in ("buy", "sell"):
+        result["error"] = "交易类型无效,请使用 buy 或 sell"
+        return result
+
+    if not stock_code or len(str(stock_code)) < 6:
+        result["error"] = "请输入有效的6位股票代码"
+        return result
+
+    if quantity <= 0:
+        result["error"] = "委托数量必须大于0"
+        return result
+
+    if quantity % 100 != 0:
+        result["error"] = "A股交易数量必须为100股的整数倍"
+        return result
+
+    api_error = _check_api_ready()
+    if api_error:
+        result["error"] = api_error["error"]
+        return result
+
+    try:
+        body = {
+            "type": trade_type,
+            "stockCode": str(stock_code),
+            "quantity": int(quantity),
+            "useMarketPrice": price is None,
+        }
+        if price is not None:
+            body["price"] = float(price)
+
+        raw = _make_request("/api/claw/mockTrading/trade", body)
+
+        if not raw.get("success") and str(raw.get("code")) != "200":
+            result["error"] = raw.get("message", "下单失败")
+            return result
+
+        data = raw.get("data", {})
+        order_id = data.get("orderId", "")
+
+        result["success"] = True
+        result["order_id"] = order_id
+        direction_cn = "买入" if trade_type == "buy" else "卖出"
+        price_info = f"@{price}元" if price else "市价"
+        result["message"] = f"{direction_cn}委托已提交: {stock_code} {quantity}股 {price_info}"
+        return result
+
+    except Exception as e:
+        result["error"] = str(e)
+        return result
+
+
+def cancel_order(order_id: str, stock_code: str = "") -> dict:
+    """撤单
+
+    Args:
+        order_id: 委托编号
+        stock_code: 股票代码(可选)
+
+    Returns:
+        {
+            "success": True/False,
+            "message": str,
+            "error": str or None
+        }
+    """
+    result = {
+        "success": False,
+        "message": "",
+        "error": None,
+    }
+
+    if not order_id:
+        result["error"] = "请提供委托编号"
+        return result
+
+    api_error = _check_api_ready()
+    if api_error:
+        result["error"] = api_error["error"]
+        return result
+
+    try:
+        body = {
+            "type": "order",
+            "orderId": str(order_id),
+        }
+        if stock_code:
+            body["stockCode"] = str(stock_code)
+
+        raw = _make_request("/api/claw/mockTrading/cancel", body)
+
+        if not raw.get("success") and str(raw.get("code")) != "200":
+            result["error"] = raw.get("message", "撤单失败")
+            return result
+
+        result["success"] = True
+        result["message"] = f"委托 {order_id} 已撤销"
+        return result
+
+    except Exception as e:
+        result["error"] = str(e)
+        return result
+
+
+def cancel_all_orders() -> dict:
+    """一键撤单(撤销所有未成交委托)
+
+    Returns:
+        {
+            "success": True/False,
+            "message": str,
+            "error": str or None
+        }
+    """
+    result = {
+        "success": False,
+        "message": "",
+        "error": None,
+    }
+
+    api_error = _check_api_ready()
+    if api_error:
+        result["error"] = api_error["error"]
+        return result
+
+    try:
+        raw = _make_request("/api/claw/mockTrading/cancel", {"type": "all"})
+
+        if not raw.get("success") and str(raw.get("code")) != "200":
+            result["error"] = raw.get("message", "一键撤单失败")
+            return result
+
+        result["success"] = True
+        result["message"] = "所有未成交委托已撤销"
+        return result
+
+    except Exception as e:
+        result["error"] = str(e)
+        return result

+ 272 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/services/stock_file_cache.py

@@ -0,0 +1,272 @@
+"""
+股票数据文件缓存服务 — 每只股票的所有数据存储到本地文件
+
+每次获取数据时优先从本地文件读取,未命中或过期才调用接口。
+支持 grep 风格的文件内容搜索。
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+import os
+import subprocess
+import threading
+from datetime import date, datetime
+from pathlib import Path
+from typing import Optional, Any
+
+from app.config import settings
+
+logger = logging.getLogger(__name__)
+
+_cache_lock = threading.Lock()
+
+# 缓存根目录
+_STOCK_CACHE_ROOT = settings.DATA_DIR / "stock_cache"
+
+# 各数据类型的文件名
+_DATA_TYPE_FILES = {
+    "quote": "quote.json",
+    "financial": "financial.json",
+    "profile": "profile.json",
+    "holders": "holders.json",
+    "sentiment": "sentiment.json",
+    "news": "news.json",
+}
+
+# 当日缓存有效期(同一只股票同一天只调一次接口)
+_TODAY = date.today().isoformat()
+
+
+class StockFileCache:
+    """股票数据文件缓存"""
+
+    def __init__(self):
+        _STOCK_CACHE_ROOT.mkdir(parents=True, exist_ok=True)
+        self._index_file = _STOCK_CACHE_ROOT / "_index.json"
+        self._index: dict = {}
+        self._load_index()
+
+    # ---- 索引管理 ----
+
+    def _load_index(self):
+        """加载主索引"""
+        try:
+            if self._index_file.exists():
+                self._index = json.loads(self._index_file.read_text(encoding="utf-8"))
+                logger.debug("文件缓存索引已加载: %d 条", len(self._index))
+        except Exception:
+            self._index = {}
+
+    def _save_index(self):
+        """保存主索引"""
+        try:
+            self._index_file.write_text(json.dumps(self._index, ensure_ascii=False, indent=2), encoding="utf-8")
+        except Exception as e:
+            logger.warning("保存缓存索引失败: %s", e)
+
+    def _stock_dir(self, stock_code: str) -> Path:
+        clean = stock_code.strip().upper()
+        d = _STOCK_CACHE_ROOT / clean
+        d.mkdir(parents=True, exist_ok=True)
+        return d
+
+    def _data_file(self, stock_code: str, data_type: str) -> Path:
+        filename = _DATA_TYPE_FILES.get(data_type, f"{data_type}.json")
+        return self._stock_dir(stock_code) / filename
+
+    def _update_index(self, stock_code: str, data_type: str):
+        code = stock_code.strip().upper()
+        if code not in self._index:
+            self._index[code] = {"data_types": [], "cached_at": datetime.now().isoformat()}
+        if data_type not in self._index[code]["data_types"]:
+            self._index[code]["data_types"].append(data_type)
+        self._index[code]["cached_at"] = datetime.now().isoformat()
+
+    # ---- 读写操作 ----
+
+    def get(self, stock_code: str, data_type: str, max_age_hours: int = 24) -> Optional[dict]:
+        """
+        从文件缓存读取数据
+
+        Args:
+            stock_code: 股票代码
+            data_type: 数据类型 (quote/financial/profile/holders/sentiment/news)
+            max_age_hours: 最大有效时长(小时),超过则视为过期
+
+        Returns:
+            缓存数据字典,未命中或过期返回 None
+        """
+        filepath = self._data_file(stock_code, data_type)
+        if not filepath.exists():
+            return None
+
+        # 检查文件时效
+        mtime = datetime.fromtimestamp(filepath.stat().st_mtime)
+        age_hours = (datetime.now() - mtime).total_seconds() / 3600
+        file_date = mtime.strftime("%Y-%m-%d")
+
+        # 当日数据直接返回(不限小时数)
+        if file_date == _TODAY:
+            pass
+        elif age_hours > max_age_hours:
+            logger.debug("缓存过期: %s/%s (%.1f小时前)", stock_code, data_type, age_hours)
+            return None
+
+        try:
+            data = json.loads(filepath.read_text(encoding="utf-8"))
+            logger.debug("文件缓存命中: %s/%s", stock_code, data_type)
+            return data
+        except Exception as e:
+            logger.warning("读取缓存文件失败 %s: %s", filepath, e)
+            return None
+
+    def set(self, stock_code: str, data_type: str, data: dict) -> bool:
+        """
+        将数据写入文件缓存
+
+        Args:
+            stock_code: 股票代码
+            data_type: 数据类型
+            data: 数据字典
+
+        Returns:
+            是否写入成功
+        """
+        filepath = self._data_file(stock_code, data_type)
+        try:
+            wrapper = {
+                "stock_code": stock_code,
+                "data_type": data_type,
+                "cached_at": datetime.now().isoformat(),
+                "cache_date": _TODAY,
+                "data": data,
+            }
+            filepath.write_text(json.dumps(wrapper, ensure_ascii=False, indent=2), encoding="utf-8")
+            self._update_index(stock_code, data_type)
+            self._save_index()
+            logger.debug("文件缓存写入: %s/%s", stock_code, data_type)
+            return True
+        except Exception as e:
+            logger.warning("写入缓存文件失败 %s: %s", filepath, e)
+            return False
+
+    def has(self, stock_code: str, data_type: str) -> bool:
+        """检查是否存在有效缓存"""
+        filepath = self._data_file(stock_code, data_type)
+        if not filepath.exists():
+            return False
+        mtime = datetime.fromtimestamp(filepath.stat().st_mtime)
+        return mtime.strftime("%Y-%m-%d") == _TODAY
+
+    # ---- grep 风格搜索 ----
+
+    def grep_search(self, keyword: str, data_type: Optional[str] = None) -> list[dict]:
+        """
+        在所有缓存文件中搜索关键词(类似 grep)
+
+        Args:
+            keyword: 搜索关键词
+            data_type: 限定数据类型,None 为全部
+
+        Returns:
+            匹配结果列表 [{stock_code, data_type, file_path, line: str, ...}]
+        """
+        results = []
+        keyword_lower = keyword.lower()
+
+        # 先查索引快速定位候选股票
+        candidates = []
+        for code, info in self._index.items():
+            if keyword_lower in code.lower():
+                candidates.append(code)
+                continue
+            types = info.get("data_types", [])
+            if data_type and data_type not in types:
+                continue
+            candidates.append(code)
+
+        # 对候选股票目录做内容 grep
+        for code in candidates:
+            stock_dir = self._stock_dir(code)
+            if not stock_dir.exists():
+                continue
+
+            for fname in stock_dir.glob("*.json"):
+                dtype = fname.stem
+                if data_type and dtype != data_type:
+                    continue
+
+                try:
+                    content = fname.read_text(encoding="utf-8")
+                    if keyword_lower in content.lower():
+                        # 提取匹配行
+                        lines = content.split("\n")
+                        matched_lines = [l.strip() for l in lines if keyword_lower in l.lower()]
+                        results.append({
+                            "stock_code": code,
+                            "data_type": dtype,
+                            "file_path": str(fname),
+                            "matched_lines": matched_lines[:10],
+                            "match_count": len(matched_lines),
+                            "cached_at": datetime.fromtimestamp(fname.stat().st_mtime).isoformat(),
+                        })
+                except Exception:
+                    continue
+
+        return results
+
+    def get_stock_codes(self) -> list[str]:
+        """获取所有已缓存的股票代码"""
+        return list(self._index.keys())
+
+    def get_stock_data_types(self, stock_code: str) -> list[str]:
+        """获取某股票已缓存的数据类型"""
+        info = self._index.get(stock_code.upper(), {})
+        return info.get("data_types", [])
+
+    def clear_stock_cache(self, stock_code: Optional[str] = None):
+        """清除缓存"""
+        if stock_code:
+            stock_dir = self._stock_dir(stock_code)
+            for f in stock_dir.glob("*.json"):
+                try:
+                    f.unlink()
+                except Exception:
+                    pass
+            code = stock_code.upper()
+            self._index.pop(code, None)
+            self._save_index()
+        else:
+            for f in _STOCK_CACHE_ROOT.glob("**/*.json"):
+                try:
+                    f.unlink()
+                except Exception:
+                    pass
+            self._index.clear()
+            self._save_index()
+
+    def get_stats(self) -> dict:
+        """获取缓存统计"""
+        total_files = sum(1 for _ in _STOCK_CACHE_ROOT.glob("**/*.json"))
+        total_size = sum(f.stat().st_size for f in _STOCK_CACHE_ROOT.glob("**/*.json") if f.is_file())
+        return {
+            "stock_count": len(self._index),
+            "total_files": total_files,
+            "total_size_mb": round(total_size / 1024 / 1024, 2),
+            "cache_root": str(_STOCK_CACHE_ROOT),
+        }
+
+
+_stock_cache_instance: Optional[StockFileCache] = None
+
+
+def get_stock_file_cache() -> StockFileCache:
+    """获取 StockFileCache 全局单例"""
+    global _stock_cache_instance
+    if _stock_cache_instance is None:
+        with _cache_lock:
+            if _stock_cache_instance is None:
+                _stock_cache_instance = StockFileCache()
+    return _stock_cache_instance

+ 198 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/services/watchlist_service.py

@@ -0,0 +1,198 @@
+"""
+智能股票分析助手 — 自选股管理服务层
+
+封装自选股查询、添加、删除的数据查询逻辑,供API路由层调用。
+"""
+
+import sys
+from pathlib import Path
+# 确保skills路径可导入
+_PROJECT_ROOT = Path(__file__).parent.parent.parent.parent  # backend/app/services -> project root
+_AGENTS_DIR = _PROJECT_ROOT / "agents"
+_SKILLS_ZIXUAN = _PROJECT_ROOT / "skills" / "自选股管理" / "mx-zixuan"
+
+for p in [_AGENTS_DIR, _SKILLS_ZIXUAN, str(_PROJECT_ROOT)]:
+    if str(p) not in sys.path:
+        sys.path.insert(0, str(p))
+
+from app.config import settings
+from app.services.mx_timed_cache import get_mx_timed_cache, mx_cache_ttl_seconds
+
+# 与 mx_data / mx_search 共用 TTL(默认 600s):逾时才再打妙想侧自选接口
+_WATCHLIST_CACHE_QUERY = "mx_zixuan_self_select_list"
+
+
+def _watchlist_cache_key() -> str:
+    return get_mx_timed_cache().make_key("mx_zixuan", _WATCHLIST_CACHE_QUERY)
+
+
+def _invalidate_watchlist_cache() -> None:
+    get_mx_timed_cache().delete(_watchlist_cache_key())
+
+
+def _get_mx_zixuan_instance():
+    """创建mx_zixuan API调用实例"""
+    import mx_zixuan as _mx
+    return _mx
+
+
+def get_watchlist() -> dict:
+    """查询自选股列表
+
+    Returns:
+        {
+            "success": True/False,
+            "stocks": [{"code": str, "name": str, "price": float, ...}, ...],
+            "total": int,
+            "error": str or None
+        }
+    """
+    import mx_zixuan as _mx
+
+    ttl = mx_cache_ttl_seconds()
+    if ttl > 0:
+        cached = get_mx_timed_cache().get_fresh(_watchlist_cache_key(), ttl)
+        if cached is not None:
+            return cached
+
+    result = {
+        "success": False,
+        "stocks": [],
+        "total": 0,
+        "error": None,
+    }
+
+    if not settings.MX_APIKEY or settings.MX_APIKEY == "your-mx-apikey-here":
+        result["error"] = "MX_APIKEY 未配置"
+        return result
+
+    try:
+        raw_result = _mx.query_self_select(settings.MX_APIKEY)
+
+        # 检查API状态
+        status = raw_result.get("status", -1)
+        code = raw_result.get("code", -1)
+        if status != 0 and code != 0:
+            result["error"] = raw_result.get("message", "查询自选股失败")
+            return result
+
+        # 解析查询结果
+        data = raw_result.get("data", {})
+        all_results = data.get("allResults", {})
+        result_data = all_results.get("result", {})
+        data_list = result_data.get("dataList", [])
+
+        stocks = []
+        for stock in (data_list or []):
+            stocks.append({
+                "code": stock.get("SECURITY_CODE", ""),
+                "name": stock.get("SECURITY_SHORT_NAME", ""),
+                "price": stock.get("NEWEST_PRICE", ""),
+                "change_pct": stock.get("CHG", ""),
+                "change_amount": stock.get("PCHG", ""),
+                "turnover_rate": stock.get("010000_TURNOVER_RATE", ""),
+                "volume_ratio": stock.get("010000_LIANGBI", ""),
+            })
+
+        result["success"] = True
+        result["stocks"] = stocks
+        result["total"] = len(stocks)
+        if ttl > 0:
+            get_mx_timed_cache().set(_watchlist_cache_key(), result)
+        return result
+
+    except Exception as e:
+        result["error"] = str(e)
+        return result
+
+
+def add_to_watchlist(stock_input: str) -> dict:
+    """添加股票到自选股
+
+    Args:
+        stock_input: 股票名称或代码,如 "贵州茅台" 或 "600519"
+
+    Returns:
+        {
+            "success": True/False,
+            "message": str,
+            "error": str or None
+        }
+    """
+    import mx_zixuan as _mx
+
+    result = {
+        "success": False,
+        "message": "",
+        "error": None,
+    }
+
+    if not settings.MX_APIKEY or settings.MX_APIKEY == "your-mx-apikey-here":
+        result["error"] = "MX_APIKEY 未配置"
+        return result
+
+    try:
+        # 构造自然语言添加指令
+        query = f"把{stock_input}添加到我的自选股列表"
+        raw_result = _mx.manage_self_select(settings.MX_APIKEY, query)
+
+        status = raw_result.get("status", -1)
+        code = raw_result.get("code", -1)
+        if status != 0 and code != 0:
+            result["error"] = raw_result.get("message", "添加自选股失败")
+            return result
+
+        result["success"] = True
+        result["message"] = raw_result.get("message", f"已将 {stock_input} 加入自选")
+        _invalidate_watchlist_cache()
+        return result
+
+    except Exception as e:
+        result["error"] = str(e)
+        return result
+
+
+def delete_from_watchlist(stock_input: str) -> dict:
+    """从自选股中删除股票
+
+    Args:
+        stock_input: 股票名称或代码,如 "贵州茅台" 或 "600519"
+
+    Returns:
+        {
+            "success": True/False,
+            "message": str,
+            "error": str or None
+        }
+    """
+    import mx_zixuan as _mx
+
+    result = {
+        "success": False,
+        "message": "",
+        "error": None,
+    }
+
+    if not settings.MX_APIKEY or settings.MX_APIKEY == "your-mx-apikey-here":
+        result["error"] = "MX_APIKEY 未配置"
+        return result
+
+    try:
+        # 构造自然语言删除指令
+        query = f"把{stock_input}从我的自选股列表删除"
+        raw_result = _mx.manage_self_select(settings.MX_APIKEY, query)
+
+        status = raw_result.get("status", -1)
+        code = raw_result.get("code", -1)
+        if status != 0 and code != 0:
+            result["error"] = raw_result.get("message", "删除自选股失败")
+            return result
+
+        result["success"] = True
+        result["message"] = raw_result.get("message", f"已将 {stock_input} 从自选中移除")
+        _invalidate_watchlist_cache()
+        return result
+
+    except Exception as e:
+        result["error"] = str(e)
+        return result

+ 1 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/utils/__init__.py

@@ -0,0 +1 @@
+# 工具函数层

+ 292 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/utils/mock_trading_normalize.py

@@ -0,0 +1,292 @@
+"""
+妙想模拟交易 — 委托列表字段规范化
+
+上游 `/api/claw/mockTrading/orders` 返回的字段名与取值在不同版本间可能不一致:
+- 买卖方向可能是数值(如 1=买入、2=卖出)而非字符串 buy/sell
+- 委托状态多为数值枚举,需映射为前端可用的 pending/done 等
+- 代码、名称、委托号、时间等可能存在 snake_case 或其它别名
+
+此处集中做兼容解析,供 simulation_service 与 Agent 工具共用。
+"""
+
+from __future__ import annotations
+
+from typing import Any, Mapping, Optional
+
+
+def extract_orders_dicts(data: Any) -> list[dict[str, Any]]:
+    """从妙想 data 中取出委托对象列表(兼容 list、或 { rows/list/... } 包裹)。"""
+    if not isinstance(data, Mapping):
+        return []
+
+    def coerce_dict_list(raw: Any) -> list[dict[str, Any]]:
+        if raw is None:
+            return []
+        if isinstance(raw, list):
+            return [x for x in raw if isinstance(x, dict)]
+        if isinstance(raw, dict):
+            for inner_key in ("rows", "records", "list", "items", "data"):
+                inner = raw.get(inner_key)
+                if isinstance(inner, list):
+                    return [x for x in inner if isinstance(x, dict)]
+        return []
+
+    # 单一列表字段优先(避免把多个片段重复拼接)
+    for key in ("orders", "orderList", "list", "entrustList"):
+        chunk = coerce_dict_list(data.get(key))
+        if chunk:
+            return chunk
+
+    # 当日 + 历史分段返回时合并(仅当顶层未给出统一 orders)
+    merged: list[dict[str, Any]] = []
+    for key in ("todayOrders", "today_order_list", "historyOrders", "hisOrders"):
+        merged.extend(coerce_dict_list(data.get(key)))
+    return merged
+
+
+def _first(d: Mapping[str, Any], keys: tuple[str, ...]) -> Any:
+    """从左到右取第一个非空值(None / 空字符串跳过)。"""
+    for k in keys:
+        if k not in d:
+            continue
+        v = d[k]
+        if v is None or v == "":
+            continue
+        return v
+    return None
+
+
+def _nested_stock(order: Mapping[str, Any]) -> Mapping[str, Any]:
+    """部分响应把证券信息放在子对象里。"""
+    for k in ("stock", "security", "stockInfo"):
+        sub = order.get(k)
+        if isinstance(sub, Mapping):
+            return sub
+    return {}
+
+
+def parse_trade_type(order: Mapping[str, Any]) -> str:
+    """解析为 'buy' 或 'sell'(妙想常见:数值 1=买入,2=卖出)。"""
+    # 关键:不能用「第一个非空字段」—— tradeType/trade_type 常为委托类别(如 5),
+    # 真正的买卖方向在 orderDrt/orderBs 等字段,必须逐个尝试直到解析成功。
+    dir_keys = (
+        "orderDrt",
+        "orderBs",
+        "orderDirection",
+        "bsFlag",
+        "bsType",
+        "entrustBs",
+        "mmlx",
+        "direction",
+        "side",
+        "tradeType",
+        "trade_type",
+    )
+    nested = _nested_stock(order)
+
+    def norm(val: Any) -> Optional[str]:
+        if val is None:
+            return None
+        if isinstance(val, bool):
+            return None
+        if isinstance(val, (int, float)):
+            iv = int(val)
+            # 东方财富系常见:1 买入,2 卖出
+            if iv == 1:
+                return "buy"
+            if iv == 2:
+                return "sell"
+            return None
+        s = str(val).strip().lower()
+        if s in ("buy", "b", "1", "买入", "买"):
+            return "buy"
+        if s in ("sell", "s", "2", "卖出", "卖"):
+            return "sell"
+        return None
+
+    for src in (order, nested):
+        for k in dir_keys:
+            if k not in src:
+                continue
+            val = src[k]
+            if val is None or val == "":
+                continue
+            parsed = norm(val)
+            if parsed:
+                return parsed
+
+    # type:部分接口表示买卖;也可能是限价/市价等业务类型,故仅在可识别时采用
+    for src in (order, nested):
+        t_raw = src.get("type")
+        parsed_t = norm(t_raw)
+        if parsed_t:
+            return parsed_t
+
+    return ""
+
+
+def parse_order_status(order: Mapping[str, Any]) -> tuple[str, str]:
+    """
+    返回 (canonical_status, chinese_label)。
+
+    canonical 供前端撤单逻辑使用:pending / done / part_deal / canceled / unknown
+    """
+    nested = _nested_stock(order)
+    status_keys = (
+        "status",
+        "order_status",
+        "orderStatus",
+        "entrustStatus",
+        "wtStatus",
+        "dealStatus",
+    )
+
+    def to_canonical_and_label(val: Any) -> tuple[str, str]:
+        if val is None:
+            return "unknown", "未知"
+        if isinstance(val, str):
+            sl = val.strip().lower()
+            known = {
+                "pending": ("pending", "未成交"),
+                "unfilled": ("pending", "未成交"),
+                "open": ("pending", "未成交"),
+                "done": ("done", "已成交"),
+                "filled": ("done", "已成交"),
+                "success": ("done", "已成交"),
+                "part_deal": ("part_deal", "部分成交"),
+                "partial": ("part_deal", "部分成交"),
+                "canceled": ("canceled", "已撤销"),
+                "cancelled": ("canceled", "已撤销"),
+                "withdrawn": ("canceled", "已撤销"),
+            }
+            if sl in known:
+                return known[sl]
+            # 已是中文等情况:当作未知但保留原文展示
+            if sl.isdigit():
+                return to_canonical_and_label(int(sl))
+            return "unknown", str(val)
+
+        try:
+            iv = int(val)
+        except (TypeError, ValueError):
+            return "unknown", str(val)
+
+        # 常见券商委托状态码(保守映射,未覆盖的显示「状态{n}」)
+        mapping: dict[int, tuple[str, str]] = {
+            0: ("pending", "待报/待成交"),
+            1: ("pending", "未成交"),
+            2: ("part_deal", "部分成交"),
+            3: ("done", "已成交"),
+            4: ("part_deal", "部成部撤"),
+            5: ("canceled", "已撤销"),
+            6: ("canceled", "已撤"),
+            7: ("done", "成交"),
+            8: ("canceled", "废单"),
+        }
+        if iv in mapping:
+            return mapping[iv]
+        return "unknown", f"状态{iv}"
+
+    for src in (order, nested):
+        for k in status_keys:
+            if k not in src:
+                continue
+            val = src[k]
+            if val is None or val == "":
+                continue
+            return to_canonical_and_label(val)
+
+    return to_canonical_and_label(None)
+
+
+def normalize_mock_order_row(order: Mapping[str, Any]) -> dict[str, Any]:
+    """将单条原始委托转为前端/路由使用的统一结构。"""
+    nested = _nested_stock(order)
+
+    order_id = _first(
+        order,
+        (
+            "orderId",
+            "order_id",
+            "orderNo",
+            "orderNO",
+            "wtOrderId",
+            "entrustId",
+            "wth",
+            "wtbh",
+            "contractId",
+            "id",
+        ),
+    )
+    stock_code = _first(
+        order,
+        ("stockCode", "stock_code", "securityCode", "zqdm", "scode", "code", "stockcode"),
+    )
+    if stock_code is None:
+        stock_code = _first(nested, ("stockCode", "stock_code", "securityCode", "zqdm", "code"))
+
+    stock_name = _first(order, ("stockName", "stock_name", "name", "sname", "securityName"))
+    if stock_name is None:
+        stock_name = _first(nested, ("stockName", "stock_name", "name"))
+
+    price_raw = _first(
+        order,
+        ("price", "wtjg", "orderPrice", "order_price", "entrustPrice", "limitPrice", "wtPrice"),
+    )
+    try:
+        price = float(price_raw) if price_raw is not None else 0.0
+    except (TypeError, ValueError):
+        price = 0.0
+
+    qty_raw = _first(
+        order,
+        (
+            "quantity",
+            "wtVolume",
+            "wtVol",
+            "wtvol",
+            "orderQty",
+            "order_qty",
+            "orderVolume",
+            "vol",
+            "volume",
+            "stockNum",
+            "entrustAmount",
+        ),
+    )
+    try:
+        quantity = int(qty_raw) if qty_raw is not None else 0
+    except (TypeError, ValueError):
+        quantity = 0
+
+    create_time = _first(
+        order,
+        (
+            "createTime",
+            "create_time",
+            "orderTime",
+            "order_time",
+            "entrustTime",
+            "reportTime",
+            "tradeTime",
+            "wtTime",
+            "report_time",
+        ),
+    )
+    if create_time is None:
+        create_time = _first(nested, ("createTime", "create_time"))
+
+    trade_type = parse_trade_type(order)
+    status, status_text = parse_order_status(order)
+
+    return {
+        "order_id": str(order_id) if order_id is not None else "",
+        "stock_code": str(stock_code) if stock_code is not None else "",
+        "stock_name": str(stock_name) if stock_name is not None else "",
+        "trade_type": trade_type,
+        "price": price,
+        "quantity": quantity,
+        "status": status,
+        "status_text": status_text,
+        "create_time": str(create_time) if create_time is not None else "",
+    }

+ 46 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/utils/mx_fixture.py

@@ -0,0 +1,46 @@
+"""
+妙想 API 本地 Fixture(原始 JSON 回放)
+
+修前端/解析逻辑时设置 MX_REPLAY_FIXTURES=1,可将已成功请求保存为 JSON,
+按 query 哈希命中文件则不再发起网络请求,节省妙想额度。
+
+用法见 backend/scripts/capture_mx_fixture.py
+"""
+
+from __future__ import annotations
+
+import hashlib
+import json
+from pathlib import Path
+from typing import Any, Optional
+
+from app.config import settings
+
+
+def fixture_path(channel: str, query: str) -> Path:
+    """channel: mx_data | mx_search | mx_xuangu"""
+    h = hashlib.sha256(query.strip().encode("utf-8")).hexdigest()[:16]
+    root: Path = settings.MX_FIXTURE_DIR
+    return root / f"{channel}_{h}.json"
+
+
+def try_load_raw_fixture(channel: str, query: str) -> Optional[dict[str, Any]]:
+    """回放模式且文件存在时返回妙想原始响应 dict,否则 None"""
+    if not settings.MX_REPLAY_FIXTURES:
+        return None
+    path = fixture_path(channel, query)
+    if not path.is_file():
+        return None
+    try:
+        data = json.loads(path.read_text(encoding="utf-8"))
+        return data if isinstance(data, dict) else None
+    except Exception:
+        return None
+
+
+def save_raw_fixture(channel: str, query: str, raw: dict[str, Any]) -> Path:
+    """将一次成功的原始响应写入磁盘,供后续 MX_REPLAY_FIXTURES 使用"""
+    path = fixture_path(channel, query)
+    path.parent.mkdir(parents=True, exist_ok=True)
+    path.write_text(json.dumps(raw, ensure_ascii=False, indent=2), encoding="utf-8")
+    return path

+ 28 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/utils/mx_http.py

@@ -0,0 +1,28 @@
+"""将带 _mx_meta 的妙想服务结果转换为统一 HTTP 响应"""
+
+from __future__ import annotations
+
+import copy
+from typing import Any
+
+from app.utils.mx_quota import MX_QUOTA_HINT
+from app.utils.response import error_response, success_response
+
+
+def mx_result_to_http(result: dict, *, http_error_code: int = 500) -> dict:
+    """
+    result 可含 _mx_meta:
+      from_cache, quota_exhausted, cache_ttl_seconds, channel, hint
+    """
+    payload = copy.deepcopy(result)
+    meta = payload.pop("_mx_meta", None)
+
+    if not payload.get("success"):
+        err = payload.get("error") or "请求失败"
+        return error_response(code=http_error_code, message=err)
+
+    msg = "success"
+    if isinstance(meta, dict) and meta.get("quota_exhausted"):
+        msg = meta.get("hint") or MX_QUOTA_HINT
+
+    return success_response(data=payload, message=msg, meta=meta)

+ 34 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/utils/mx_quota.py

@@ -0,0 +1,34 @@
+"""妙想 API 额度类错误识别与前端提示文案"""
+
+
+MX_QUOTA_HINT = (
+    "今日妙想 Skills 调用额度已用尽,以下为缓存数据(若有);"
+    "升级或次日重置额度后可获取最新数据。"
+)
+
+
+def quota_exhausted_no_cache_message(upstream_error: str | None) -> str:
+    """额度用尽且无进程内缓存时的提示(附上游原文,便于核对 Key 是否生效、是否真为 113 等)"""
+    base = MX_QUOTA_HINT + "(暂无可用缓存)"
+    u = (upstream_error or "").strip()
+    if not u:
+        return base
+    return f"{base} 上游提示:{u}"
+
+
+def is_mx_quota_exhausted(error_text: str | None) -> bool:
+    """判断是否命中免费版日限额(状态码 113 等)"""
+    if not error_text:
+        return False
+    s = str(error_text)
+    if "状态码 113" in s:
+        return True
+    if "调用次数已达到上限" in s:
+        return True
+    if "今日调用次数已达到上限" in s:
+        return True
+    # 避免宽泛匹配:仅当同时涉及「免费版 + 上限」且明显与调用次数相关时才视为额度类
+    if "免费版" in s and "上限" in s:
+        if any(k in s for k in ("调用", "次数", "配额", "限额", "Skills")):
+            return True
+    return False

+ 67 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/app/utils/response.py

@@ -0,0 +1,67 @@
+"""
+智能股票分析助手 — 统一响应格式模块
+
+定义标准API响应结构,确保前后端数据格式一致。
+"""
+
+from typing import Any, Optional
+from pydantic import BaseModel
+
+
+class APIResponse(BaseModel):
+    """统一API响应格式"""
+
+    code: int = 0  # 状态码:0=成功,非0=错误
+    message: str = "success"  # 提示信息
+    data: Optional[Any] = None  # 响应数据
+
+    class Config:
+        json_schema_extra = {
+            "example": {
+                "code": 0,
+                "message": "success",
+                "data": {"stock_code": "600519", "price": 1700.00},
+            }
+        }
+
+
+class PageResponse(BaseModel):
+    """分页API响应格式"""
+
+    code: int = 0
+    message: str = "success"
+    data: Optional[Any] = None
+    pagination: Optional[dict] = None  # 分页信息 {page, page_size, total, total_pages}
+
+    class Config:
+        json_schema_extra = {
+            "example": {
+                "code": 0,
+                "message": "success",
+                "data": [],
+                "pagination": {"page": 1, "page_size": 20, "total": 100, "total_pages": 5},
+            }
+        }
+
+
+def success_response(data: Any = None, message: str = "success", meta: Any = None) -> dict:
+    """快速构建成功响应;meta 用于妙想缓存/额度提示等扩展字段"""
+    out: dict = {"code": 0, "message": message, "data": data}
+    if meta is not None:
+        out["meta"] = meta
+    return out
+
+
+def error_response(code: int, message: str, data: Any = None) -> dict:
+    """快速构建错误响应"""
+    return APIResponse(code=code, message=message, data=data).model_dump()
+
+
+def page_response(data: Any, page: int, page_size: int, total: int) -> dict:
+    """快速构建分页响应"""
+    total_pages = (total + page_size - 1) // page_size if page_size > 0 else 0
+    return PageResponse(
+        code=0,
+        data=data,
+        pagination={"page": page, "page_size": page_size, "total": total, "total_pages": total_pages},
+    ).model_dump()

+ 0 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/fixtures/mx_raw/.gitkeep


+ 5 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/pytest.ini

@@ -0,0 +1,5 @@
+[pytest]
+# F15 等模块使用 async def 测试,需自动识别为 asyncio 测试
+asyncio_mode = auto
+asyncio_default_fixture_loop_scope = function
+testpaths = tests

+ 60 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/requirements.txt

@@ -0,0 +1,60 @@
+# 智能股票分析助手 — 后端依赖
+# Python 3.10+ 必需
+
+# =========================================================================
+# Web框架
+# =========================================================================
+fastapi>=0.110.0
+uvicorn[standard]>=0.27.0
+pydantic>=2.0.0
+
+# =========================================================================
+# 数据库
+# =========================================================================
+sqlalchemy>=2.0.0
+aiosqlite>=0.19.0
+
+# =========================================================================
+# 认证
+# =========================================================================
+python-jose[cryptography]>=3.3.0
+passlib[bcrypt]>=1.7.4
+python-multipart>=0.0.6
+
+# =========================================================================
+# 缓存(可选,后续模块启用)
+# =========================================================================
+# redis>=5.0.0
+
+# =========================================================================
+# 环境变量
+# =========================================================================
+python-dotenv>=1.0.0
+
+# =========================================================================
+# HTTP请求(用于调用外部API)
+# =========================================================================
+httpx>=0.25.0
+
+# =========================================================================
+# 数据处理
+# =========================================================================
+pandas>=2.0.0
+openpyxl>=3.1.0
+
+# =========================================================================
+# 工具库
+# =========================================================================
+aiofiles>=23.0.0
+
+# =========================================================================
+# exe 打包(可选)
+# =========================================================================
+# pyinstaller>=6.0.0
+
+# =========================================================================
+# 测试
+# =========================================================================
+pytest>=8.0.0
+pytest-asyncio>=0.24.0
+httpx>=0.25.0  # 用于AsyncClient测试

+ 75 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/scripts/capture_mx_fixture.py

@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+"""
+将妙想接口返回的原始 JSON 保存到 backend/fixtures/mx_raw/,
+后续设置 MX_REPLAY_FIXTURES=1 即可本地回放同一 query,不再消耗额度。
+
+在项目根目录执行(需已配置 MX_APIKEY):
+
+  set PYTHONPATH=backend
+  py backend/scripts/capture_mx_fixture.py mx_data "600519 最新价 涨跌幅"
+  py backend/scripts/capture_mx_fixture.py mx_search "今日A股市场热点 大盘动态 北向资金"
+  py backend/scripts/capture_mx_fixture.py mx_xuangu "市盈率小于20"
+
+注意:回放时服务端生成的 query 必须与抓取时字符串完全一致(含空格)。
+"""
+
+from __future__ import annotations
+
+import argparse
+import sys
+from pathlib import Path
+
+_BACKEND = Path(__file__).resolve().parent.parent
+_ROOT = _BACKEND.parent
+
+sys.path.insert(0, str(_BACKEND))
+for p in (
+    _ROOT / "agents",
+    _ROOT / "skills" / "金融数据" / "mx-data",
+    _ROOT / "skills" / "资讯搜索" / "mx-search",
+    _ROOT / "skills" / "智能选股" / "mx-xuangu",
+    _ROOT,
+):
+    sp = str(p)
+    if sp not in sys.path:
+        sys.path.insert(0, sp)
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(description="保存妙想原始响应为本地 fixture")
+    parser.add_argument(
+        "channel",
+        choices=("mx_data", "mx_search", "mx_xuangu"),
+        help="与路由使用的 skill 一致",
+    )
+    parser.add_argument("query", help="自然语言查询(须与线上一致)")
+    args = parser.parse_args()
+
+    from app.config import settings
+    from app.utils.mx_fixture import fixture_path, save_raw_fixture
+
+    if not settings.MX_APIKEY or settings.MX_APIKEY == "your-mx-apikey-here":
+        print("错误:请在 .env 中配置 MX_APIKEY 后再抓取 fixture")
+        sys.exit(1)
+
+    if args.channel == "mx_data":
+        import mx_data as _mx
+
+        raw = _mx.MXData(api_key=settings.MX_APIKEY).query(args.query)
+    elif args.channel == "mx_search":
+        import mx_search as _mx
+
+        raw = _mx.MXSearch(api_key=settings.MX_APIKEY).search(args.query)
+    else:
+        import mx_xuangu as _mx
+
+        raw = _mx.MXSelectStock(api_key=settings.MX_APIKEY).search(args.query)
+
+    path = save_raw_fixture(args.channel, args.query, raw)
+    print(f"已写入: {path}")
+    print(f"哈希文件名对应 query: {args.query!r}")
+    print("回放:环境变量 MX_REPLAY_FIXTURES=1(可选 MX_FIXTURE_DIR 指向目录),重启后端")
+
+
+if __name__ == "__main__":
+    main()

+ 91 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/scripts/smoke_financial_api.py

@@ -0,0 +1,91 @@
+"""
+金融 / 行情 API 冒烟测试(直连 FastAPI,需项目根目录 .env 已配置 MX_APIKEY)
+
+用法(在项目根目录):
+  py -3 -m backend.scripts.smoke_financial_api
+或在 backend 目录:
+  py -3 scripts/smoke_financial_api.py
+"""
+
+from __future__ import annotations
+
+import json
+import sys
+from pathlib import Path
+
+# 路径:backend/scripts -> backend -> 项目根
+_BACKEND = Path(__file__).resolve().parent.parent
+_ROOT = _BACKEND.parent
+sys.path.insert(0, str(_ROOT))
+sys.path.insert(0, str(_BACKEND))
+
+from fastapi.testclient import TestClient  # noqa: E402
+from app.main import app  # noqa: E402
+
+
+def _short(obj, limit: int = 600) -> str:
+    s = json.dumps(obj, ensure_ascii=False, default=str)
+    return s if len(s) <= limit else s[:limit] + "…"
+
+
+def _summarize_tables(data: dict) -> str:
+    if not isinstance(data, dict):
+        return "(非 dict)"
+    tables = data.get("tables") or []
+    if not tables:
+        return "tables=[]"
+    t0 = tables[0]
+    names = t0.get("fieldnames") or []
+    rows = t0.get("rows") or []
+    row0 = rows[0] if rows else None
+    return f"fieldnames={names!r}, row0={row0!r}"
+
+
+def main() -> int:
+    client = TestClient(app)
+    endpoints = [
+        ("GET", "/api/v1/system/health", None),
+        ("GET", "/api/v1/market/index", {"params": {"name": "上证指数"}}),
+        ("GET", "/api/v1/market/index", {"params": {"name": "沪深300"}}),
+        ("GET", "/api/v1/market/quote/600519", None),
+        ("GET", "/api/v1/financial/indicators/600519", None),
+        ("GET", "/api/v1/financial/profile/600519", None),
+        ("GET", "/api/v1/financial/holders/600519", None),
+    ]
+
+    print("=== 金融 / 行情 API 冒烟(TestClient)===\n")
+
+    for method, path, kw in endpoints:
+        kw = kw or {}
+        r = client.request(method, path, **kw)
+        body = r.json()
+        inner = body.get("data") if isinstance(body, dict) else None
+        ok_http = r.status_code == 200
+        ok_biz = isinstance(body, dict) and body.get("code") == 0
+        mx_ok = isinstance(inner, dict) and inner.get("success") is True
+
+        line1 = f"{method} {path} [{r.status_code}] code={body.get('code') if isinstance(body, dict) else '?'}"
+        print(line1)
+        if not ok_http or not ok_biz:
+            print(f"  -> 响应异常: {_short(body)}")
+            print()
+            continue
+
+        if inner is None:
+            print(f"  -> data=None {_short(body)}")
+            print()
+            continue
+
+        if isinstance(inner, dict) and "success" in inner:
+            print(f"  -> mx success={inner.get('success')} total_rows={inner.get('total_rows')} error={inner.get('error')}")
+            print(f"  -> {_summarize_tables(inner)}")
+        else:
+            print(f"  -> {_short(inner)}")
+        print()
+
+    print("说明: mx success=false 或 tables 空时,多为妙想接口返回或解析问题;HTTP 非 200 看 message。")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())

+ 1 - 0
Co-creation-projects/lcyting-StockSage-agent/backend/tests/__init__.py

@@ -0,0 +1 @@
+# 后端测试

+ 51 - 0
Co-creation-projects/lcyting-StockSage-agent/docker-compose.yml

@@ -0,0 +1,51 @@
+# =========================================================================
+# 智能股票分析助手 — Docker Compose 编排文件
+# 
+# 启动: docker compose up -d
+# 停止: docker compose down
+# 查看日志: docker compose logs -f
+# =========================================================================
+
+version: '3.8'
+
+services:
+  # =========================================================================
+  # 后端服务 — FastAPI
+  # =========================================================================
+  backend:
+    build:
+      context: .
+      dockerfile: backend/Dockerfile
+    container_name: stock-analyzer-backend
+    ports:
+      - "8000:8000"
+    volumes:
+      # 持久化 SQLite 数据库
+      - stock_data:/app/data
+    environment:
+      - PYTHONUNBUFFERED=1
+    restart: unless-stopped
+    healthcheck:
+      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/v1/system/health')"]
+      interval: 30s
+      timeout: 10s
+      retries: 3
+      start_period: 10s
+
+  # =========================================================================
+  # 前端服务 — Nginx + Vue3 SPA
+  # =========================================================================
+  frontend:
+    build:
+      context: .
+      dockerfile: frontend/Dockerfile
+    container_name: stock-analyzer-frontend
+    ports:
+      - "8080:80"
+    depends_on:
+      - backend
+    restart: unless-stopped
+
+volumes:
+  stock_data:
+    name: stock_analyzer_data

+ 33 - 0
Co-creation-projects/lcyting-StockSage-agent/frontend/Dockerfile

@@ -0,0 +1,33 @@
+# =========================================================================
+# 智能股票分析助手 — 前端 Dockerfile(多阶段构建)
+# 
+# 第一阶段: Node.js 构建 Vue3 应用
+# 第二阶段: Nginx 提供静态文件 + API 反向代理
+# 构建: docker build -t stock-analyzer-frontend -f frontend/Dockerfile .
+# =========================================================================
+
+# ---- 构建阶段 ----
+FROM node:20-alpine AS build
+
+WORKDIR /app
+
+# 复制依赖文件并安装
+COPY frontend/package.json frontend/package-lock.json ./
+RUN npm ci || npm install
+
+# 复制前端源码并构建
+COPY frontend/ ./
+RUN npm run build
+
+# ---- 生产阶段 ----
+FROM nginx:alpine
+
+# 复制构建产物到 Nginx 静态目录
+COPY --from=build /app/dist /usr/share/nginx/html
+
+# 复制 Nginx 配置(SPA路由 + API反向代理)
+COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
+
+EXPOSE 80
+
+CMD ["nginx", "-g", "daemon off;"]

Some files were not shown because too many files changed in this diff