WIP: Commit all pending changes across services, frontend, and docs

- Sync updates for provider services (AlphaVantage, Finnhub, YFinance, Tushare)
- Update Frontend components and pages for recent config changes
- Update API Gateway and Registry
- Include design docs and tasks status
This commit is contained in:
Lv, Qi 2025-11-27 02:45:56 +08:00
parent ca1eddd244
commit a59b994a92
103 changed files with 6740 additions and 5658 deletions

View File

@ -32,6 +32,16 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bstr"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "cc"
version = "1.2.47"
@ -158,6 +168,19 @@ dependencies = [
"url",
]
[[package]]
name = "globset"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
dependencies = [
"aho-corasick",
"bstr",
"log",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "hex"
version = "0.4.3"
@ -722,6 +745,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"git2",
"globset",
"hex",
"regex",
"serde",

View File

@ -38,7 +38,7 @@ services:
RUST_LOG: info
RUST_BACKTRACE: "1"
ports:
- "3001:3000"
- "3005:3000"
depends_on:
postgres-test:
condition: service_healthy

View File

@ -59,6 +59,8 @@ services:
working_dir: /workspace/frontend
command: ["/workspace/frontend/scripts/docker-dev-entrypoint.sh"]
environment:
# Vite Proxy Target
VITE_API_TARGET: http://api-gateway:4000
# 让 Next 的 API 路由代理到新的 api-gateway
NEXT_PUBLIC_BACKEND_URL: http://api-gateway:4000/v1
# SSR 内部访问自身 API 的内部地址,避免使用 x-forwarded-host 导致访问宿主机端口

View File

@ -0,0 +1,130 @@
# 重构任务:动态数据提供商配置架构 (Dynamic Data Provider Configuration)
## 1. 背景与目标 (Background & Objective)
目前系统的前端页面 (`DataSourceTab`) 硬编码了支持的数据源列表Tushare, Finnhub 等)及其配置表单。这导致每增加一个新的数据源,都需要修改前端代码,违反了“单一来源”和“开闭原则”。
本次重构的目标是实现 **“前端无知 (Frontend Agnostic)”** 的架构:
1. **后端驱动**:各 Provider 服务在启动注册时,声明自己的元数据(名称、描述)和配置规范(需要哪些字段)。
2. **动态渲染**:前端通过 Gateway 获取所有已注册服务的元数据,动态生成配置表单。
3. **通用交互**:测试连接、保存配置等操作通过统一的接口进行,不再针对特定 Provider 编写逻辑。
## 2. 核心数据结构设计 (Core Data Structures)
我们在 `common-contracts` 中扩展了服务注册的定义。
### 2.1 配置字段定义 (Config Field Definition)
不使用复杂的通用 JSON Schema而是定义一套符合我们 UI 需求的强类型 Schema。
```rust
/// 字段类型枚举
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub enum FieldType {
Text, // 普通文本
Password, // 密码/Token (前端掩码显示)
Url, // URL 地址
Boolean, // 开关
Select, // 下拉选框 (需要 options)
}
/// 单个配置字段的定义
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ConfigFieldSchema {
pub key: String, // 字段键名 (如 "api_key")
pub label: String, // 显示名称 (如 "API Token")
pub field_type: FieldType,// 字段类型
pub required: bool, // 是否必填
pub placeholder: Option<String>, // 占位符
pub default_value: Option<String>, // 默认值
pub description: Option<String>, // 字段说明
}
```
### 2.2 服务元数据扩展 (Service Metadata Extension)
修改 `ServiceRegistration` 并增加了 `ProviderMetadata` 结构。
```rust
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct ProviderMetadata {
pub id: String, // 服务ID (如 "tushare")
pub name_en: String, // 英文名 (如 "Tushare Pro")
pub name_cn: String, // 中文名 (如 "Tushare Pro (中国股市)")
pub description: String, // 描述
pub icon_url: Option<String>, // 图标 (可选)
/// 该服务需要的配置字段列表
pub config_schema: Vec<ConfigFieldSchema>,
/// 是否支持“测试连接”功能
pub supports_test_connection: bool,
}
```
## 3. 架构交互流程 (Architecture Flow)
### 3.1 服务注册 (Registration Phase)
1. **Provider 启动** (如 `tushare-provider-service`):
* 构建 `ProviderMetadata`,声明自己需要 `api_key` (必填, Password) 和 `api_url` (选填, Url, 默认值...)。
* 调用 API Gateway 的 `/register` 接口,将 Metadata 发送给 Gateway。
2. **API Gateway**:
* 在内存 Registry 中存储这些 Metadata。
### 3.2 前端渲染 (Rendering Phase)
1. **前端请求**: `GET /v1/registry/providers` (网关聚合接口)。
2. **网关响应**: 返回所有活跃 Provider 的 `ProviderMetadata` 列表。
3. **前端请求**: `GET /v1/configs/data_sources` (获取用户已保存的配置值)。
4. **UI 生成**:
* 前端遍历 Metadata 列表。
* 对每个 Provider根据 `config_schema` 动态生成 Input/Switch 组件。
* 将已保存的配置值填充到表单中。
### 3.3 动作执行 (Action Phase)
1. **保存 (Save)**:
* 前端收集表单数据,发送标准 JSON 到 `POST /v1/configs/data_sources`
* 后端持久化服务保存数据。
2. **测试连接 (Test Connection)**:
* 前端发送 `POST /v1/configs/test` (注意Gateway已有通用接口)。
* Payload: `{ type: "tushare", data: { "api_key": "..." } }`
* 网关将请求路由到对应的 Provider 微服务进行自我检查。
## 4. 任务拆解 (Task Breakdown)
### 4.1 后端开发 (Backend Development) - [已完成/Completed]
1. **Common Contracts**: 更新 `registry.rs`,添加 `ProviderMetadata``ConfigFieldSchema` 定义。 (Done)
2. **API Gateway**:
* 更新注册逻辑,接收并存储 Metadata。 (Done)
* 新增接口 `GET /v1/registry/providers` 供前端获取元数据。 (Done)
* **移除旧版接口**: 已删除 Legacy 的静态 schema 接口,强制转向动态机制。 (Done)
* **单元测试**: 实现了 `ServiceRegistry` 的单元测试 (`registry_unit_test.rs`),覆盖注册、心跳、发现、注销等核心逻辑。 (Done)
3. **Provider Services** (Tushare, Finnhub, etc.):
* 实现 `ConfigSchema` 的构建逻辑。 (Done)
* 更新注册调用,发送 Schema。 (Done for Tushare, Finnhub, AlphaVantage, YFinance)
### 4.2 后端验证测试 (Backend Verification) - [已完成/Completed]
在开始前端重构前,已进行一次集成测试,确保后端服务启动后能正确交互。
1. **启动服务**: 使用 `tests/api-e2e/run_registry_test.sh` 启动最小化服务集。
2. **API 验证**:
* 调用 `GET /v1/registry/providers`,验证返回的 JSON 是否包含所有 Provider 的 Metadata 和 Schema。
* 使用 `tests/api-e2e/registry_verifier.py` 脚本验证了 Tushare 的 Schema 字段正确性。
3. **结果确认**:
* ✅ Tushare 服务成功注册。
* ✅ Schema 正确包含 `api_token` (Password, Required)。
* ✅ E2E 测试集 (`tests/end-to-end`) 也已更新并验证通过。
### 4.3 前端重构 (Frontend Refactor) - [待办/Pending]
1. **API Client**: 更新 Client SDK 以支持新的元数据接口。
* **自动化更新脚本**: `scripts/update_api_spec.sh`
* **执行逻辑**:
1. 调用 `cargo test ... generate_openapi_json` 生成 `openapi.json`
2. 调用 `npm run gen:api` 重新生成前端 TypeScript 类型定义。
* **输出位置**: `frontend/src/api/schema.gen.ts`
2. **State Management**: 修改 `useConfig` hook增加获取 Provider Metadata 的逻辑。
3. **UI Refactor**:
* 废弃 `supportedProviders` 硬编码。
* 创建 `DynamicConfigForm` 组件,根据 `ConfigFieldSchema` 渲染界面。
* 对接新的测试连接和保存逻辑。

View File

@ -0,0 +1,89 @@
# 重构任务:统一 Data Provider 工作流抽象
## 1. 背景 (Background)
目前的 Data Provider 服务Tushare, YFinance, AlphaVantage 等)在架构上存在严重的重复代码和实现不一致问题。每个服务都独立实现了完整的工作流,包括:
- NATS 消息接收与反序列化
- 缓存检查与写入 (Cache-Aside Pattern)
- 任务状态管理 (Observability/Task Progress)
- Session Data 持久化
- NATS 事件发布 (Success/Failure events)
这种"散弹式"架构导致了以下问题:
1. **Bug 易发且难以统一修复**:例如 Tushare 服务因未执行 NATS Flush 导致事件丢失,而 YFinance 却因为实现方式不同而没有此问题。修复一个 Bug 需要在每个服务中重复操作。
2. **逻辑不一致**:不同 Provider 对缓存策略、错误处理、重试机制的实现可能存在细微差异,违背了系统的统一性。
3. **维护成本高**:新增一个 Provider 需要复制粘贴大量基础设施代码Boilerplate容易出错。
## 2. 目标 (Objectives)
贯彻 "Rustic" 的设计理念强类型、单一来源、早失败通过控制反转IoC和模板方法模式将**业务逻辑**与**基础设施逻辑**彻底分离。
- **单一来源 (Single Source of Truth)**:工作流的核心逻辑(缓存、持久化、通知)只在一个地方定义和维护。
- **降低耦合**:具体 Provider 只需关注 "如何从 API 获取数据",而无需关心 "如何与系统交互"。
- **提升稳定性**:统一修复基础设施层面的问题(如 NATS Flush所有 Provider 自动受益。
## 3. 技术方案 (Technical Design)
### 3.1 核心抽象 (The Trait)
`common-contracts` 中定义纯粹的业务逻辑接口:
```rust
#[async_trait]
pub trait DataProviderLogic: Send + Sync {
/// Provider 的唯一标识符 (e.g., "tushare", "yfinance")
fn provider_id(&self) -> &str;
/// 检查是否支持该市场 (前置检查)
fn supports_market(&self, market: &str) -> bool {
true
}
/// 核心业务:从外部源获取原始数据并转换为标准 DTO
/// 不涉及任何 DB 或 NATS 操作
async fn fetch_data(&self, symbol: &str) -> Result<(CompanyProfileDto, Vec<TimeSeriesFinancialDto>), anyhow::Error>;
}
```
### 3.2 通用工作流引擎 (The Engine)
实现一个泛型结构体或函数 `StandardFetchWorkflow`,封装所有基础设施逻辑:
1. **接收指令**:解析 `FetchCompanyDataCommand`
2. **前置检查**:调用 `supports_market`
3. **状态更新**:向 `AppState` 写入 "InProgress"。
4. **缓存层**
* 检查 `persistence_client` 缓存。
* HIT -> 直接返回。
* MISS -> 调用 `fetch_data`,然后写入缓存。
5. **持久化层**:将结果写入 `SessionData`
6. **事件通知**
* 构建 `FinancialsPersistedEvent`
* 发布 NATS 消息。
* **关键:执行 `flush().await`。**
7. **错误处理**:统一捕获错误,发布 `DataFetchFailedEvent`,更新 Task 状态为 Failed。
## 4. 执行步骤 (Execution Plan)
1. **基础设施准备**
* 在 `services/common-contracts` 中添加 `DataProviderLogic` trait。
* 在 `services/common-contracts` (或新建 `service-kit` 模块) 中实现 `StandardFetchWorkflow`
2. **重构 Tushare Service**
* 创建 `TushareFetcher` 实现 `DataProviderLogic`
* 删除 `worker.rs` 中的冗余代码,替换为对 `StandardFetchWorkflow` 的调用。
* 验证 NATS Flush 问题是否自然解决。
3. **重构 YFinance Service**
* 同样方式重构,验证通用性。
4. **验证**
* 运行 E2E 测试,确保数据获取流程依然通畅。
## 5. 验收标准 (Acceptance Criteria)
- `common-contracts` 中包含清晰的 Trait 定义。
- Tushare 和 YFinance 的 `worker.rs` 代码量显著减少(预计减少 60%+)。
- 所有 Provider 的行为(日志格式、状态更新频率、缓存行为)完全一致。
- 即使不手动写 `flush`,重构后的 Provider 也能可靠发送 NATS 消息。

View File

@ -0,0 +1,89 @@
# 重构任务:统一 Data Provider 工作流抽象
## 1. 背景 (Background)
目前的 Data Provider 服务Tushare, YFinance, AlphaVantage 等)在架构上存在严重的重复代码和实现不一致问题。每个服务都独立实现了完整的工作流,包括:
- NATS 消息接收与反序列化
- 缓存检查与写入 (Cache-Aside Pattern)
- 任务状态管理 (Observability/Task Progress)
- Session Data 持久化
- NATS 事件发布 (Success/Failure events)
这种"散弹式"架构导致了以下问题:
1. **Bug 易发且难以统一修复**:例如 Tushare 服务因未执行 NATS Flush 导致事件丢失,而 YFinance 却因为实现方式不同而没有此问题。修复一个 Bug 需要在每个服务中重复操作。
2. **逻辑不一致**:不同 Provider 对缓存策略、错误处理、重试机制的实现可能存在细微差异,违背了系统的统一性。
3. **维护成本高**:新增一个 Provider 需要复制粘贴大量基础设施代码Boilerplate容易出错。
## 2. 目标 (Objectives)
贯彻 "Rustic" 的设计理念强类型、单一来源、早失败通过控制反转IoC和模板方法模式将**业务逻辑**与**基础设施逻辑**彻底分离。
- **单一来源 (Single Source of Truth)**:工作流的核心逻辑(缓存、持久化、通知)只在一个地方定义和维护。
- **降低耦合**:具体 Provider 只需关注 "如何从 API 获取数据",而无需关心 "如何与系统交互"。
- **提升稳定性**:统一修复基础设施层面的问题(如 NATS Flush所有 Provider 自动受益。
## 3. 技术方案 (Technical Design)
### 3.1 核心抽象 (The Trait)
`common-contracts` 中定义纯粹的业务逻辑接口:
```rust
#[async_trait]
pub trait DataProviderLogic: Send + Sync {
/// Provider 的唯一标识符 (e.g., "tushare", "yfinance")
fn provider_id(&self) -> &str;
/// 检查是否支持该市场 (前置检查)
fn supports_market(&self, market: &str) -> bool {
true
}
/// 核心业务:从外部源获取原始数据并转换为标准 DTO
/// 不涉及任何 DB 或 NATS 操作
async fn fetch_data(&self, symbol: &str) -> Result<(CompanyProfileDto, Vec<TimeSeriesFinancialDto>), anyhow::Error>;
}
```
### 3.2 通用工作流引擎 (The Engine)
实现一个泛型结构体或函数 `StandardFetchWorkflow`,封装所有基础设施逻辑:
1. **接收指令**:解析 `FetchCompanyDataCommand`
2. **前置检查**:调用 `supports_market`
3. **状态更新**:向 `AppState` 写入 "InProgress"。
4. **缓存层**
* 检查 `persistence_client` 缓存。
* HIT -> 直接返回。
* MISS -> 调用 `fetch_data`,然后写入缓存。
5. **持久化层**:将结果写入 `SessionData`
6. **事件通知**
* 构建 `FinancialsPersistedEvent`
* 发布 NATS 消息。
* **关键:执行 `flush().await`。**
7. **错误处理**:统一捕获错误,发布 `DataFetchFailedEvent`,更新 Task 状态为 Failed。
## 4. 执行步骤 (Execution Plan)
1. **基础设施准备**
* 在 `services/common-contracts` 中添加 `DataProviderLogic` trait。
* 在 `services/common-contracts` (或新建 `service-kit` 模块) 中实现 `StandardFetchWorkflow`
2. **重构 Tushare Service**
* 创建 `TushareFetcher` 实现 `DataProviderLogic`
* 删除 `worker.rs` 中的冗余代码,替换为对 `StandardFetchWorkflow` 的调用。
* 验证 NATS Flush 问题是否自然解决。
3. **重构 YFinance Service**
* 同样方式重构,验证通用性。
4. **验证**
* 运行 E2E 测试,确保数据获取流程依然通畅。
## 5. 验收标准 (Acceptance Criteria)
- `common-contracts` 中包含清晰的 Trait 定义。
- Tushare 和 YFinance 的 `worker.rs` 代码量显著减少(预计减少 60%+)。
- 所有 Provider 的行为(日志格式、状态更新频率、缓存行为)完全一致。
- 即使不手动写 `flush`,重构后的 Provider 也能可靠发送 NATS 消息。

View File

@ -0,0 +1,97 @@
# 设计方案 0: 系统总览与开发指南 (Overview & Setup)
## 1. 项目背景 (Context)
本项目是一个 **金融基本面分析系统 (Fundamental Analysis System)**
目标是通过自动化的工作流从多个数据源Tushare, YFinance抓取数据经过处理最终生成一份包含估值、风险分析的综合报告。
本次重构旨在解决原系统调度逻辑僵化、上下文管理混乱、大文件支持差的问题。我们将构建一个基于 **Git 内核** 的分布式上下文管理系统。
## 2. 系统架构 (Architecture)
系统由四个核心模块组成:
1. **VGCS (Virtual Git Context System)**: 底层存储。基于 Git + Blob Store 的版本化文件系统。
2. **DocOS (Document Object System)**: 逻辑层。提供“文档树”的裂变、聚合操作,屏蔽底层文件细节。
3. **Worker Runtime**: 适配层。提供 SDK 给业务 WorkerPython/Rust使其能读写上下文。
4. **Orchestrator**: 调度层。负责任务分发、依赖管理和并行结果合并。
```mermaid
graph TD
subgraph "Storage Layer (Shared Volume)"
Repo[Git Bare Repo]
Blob[Blob Store]
end
subgraph "Library Layer (Rust Crate)"
VGCS[VGCS Lib] --> Repo & Blob
DocOS[DocOS Lib] --> VGCS
end
subgraph "Execution Layer"
Orch[Orchestrator Service] --> DocOS
Worker[Python/Rust Worker] --> Runtime[Worker Runtime SDK]
Runtime --> DocOS
end
Orch -- NATS (RPC) --> Worker
```
## 3. 技术栈规范 (Tech Stack)
### 3.1 Rust (VGCS, DocOS, Orchestrator)
* **Version**: Stable (1.75+)
* **Key Crates**:
* `git2` (0.18): libgit2 bindings. **注意**: 默认开启 `vendored-openssl` feature 以确保静态链接。
* `serde`, `serde_json`: 序列化。
* `thiserror`, `anyhow`: 错误处理。
* `async-nats` (0.33): 消息队列。
* `tokio` (1.0+): 异步运行时。
* `sha2`, `hex`: 哈希计算。
### 3.2 Python (Worker Runtime)
* **Version**: 3.10+
* **Package Manager**: Poetry
* **Key Libs**:
* `nats-py`: NATS Client.
* `pydantic`: 数据验证。
* (可选) `libgit2-python`: 如果需要高性能 Git 操作,否则通过 FFI 调用 Rust Lib 或 Shell Out。
## 4. 开发环境搭建 (Dev Setup)
### 4.1 目录准备
在本地开发机上,我们需要模拟共享存储。
```bash
mkdir -p /tmp/workflow_dev/repos
mkdir -p /tmp/workflow_dev/blobs
export WORKFLOW_DATA_PATH=/tmp/workflow_dev
```
### 4.2 Docker 环境
为了确保一致性,所有服务应在 Docker Compose 中运行,挂载同一个 Volume。
```yaml
volumes:
workflow_data:
services:
orchestrator:
image: rust-builder
volumes:
- workflow_data:/mnt/workflow_data
environment:
- WORKFLOW_DATA_PATH=/mnt/workflow_data
```
## 5. 模块集成方式
* **VGCS & DocOS**: 将实现为一个独立的 Rust Workspace Member (`crates/workflow-context`)。
* **Orchestrator**: 引用该 Crate。
* **Worker (Rust)**: 引用该 Crate。
* **Worker (Python)**: 通过 `PyO3` 绑定该 Crate或者MVP阶段重写部分逻辑/Shell调用。**建议 MVP 阶段 Python SDK 仅封装 git binary 调用以简化构建**。
## 6. 新人上手路径
1. 阅读 `design_1_vgcs.md`,理解如何在不 clone 的情况下读写 git object。
2. 阅读 `design_2_doc_os.md`,理解“文件变目录”的逻辑。
3. 实现 Rust Crate `workflow-context` (包含 VGCS + DocOS)。
4. 编写单元测试,验证 File -> Blob 的分流逻辑。
5. 集成到 Orchestrator。

View File

@ -0,0 +1,150 @@
# 设计方案 1: VGCS (Virtual Git Context System) [详细设计版]
## 1. 定位与目标
VGCS 是底层存储引擎,提供版本化、高性能、支持大文件的分布式文件系统接口。
它基于 **git2-rs (libgit2)** 实现,直接操作 Git Object DB。
## 2. 物理存储规范 (Specification)
所有服务共享挂载卷 `/mnt/workflow_data`
### 2.1 目录结构
```text
/mnt/workflow_data/
├── repos/
│ └── {request_id}.git/ <-- Bare Git Repo
│ ├── HEAD
│ ├── config
│ ├── objects/ <-- Standard Git Objects
│ └── refs/
└── blobs/
└── {request_id}/ <-- Blob Store Root
├── ab/
│ └── ab1234... <-- Raw File (SHA-256 of content)
└── cd/
└── cd5678...
```
### 2.2 Blob 引用文件格式 (.ref)
当文件 > 1MB 时Git Blob 存储如下 JSON 内容:
```json
{
"$vgcs_ref": "v1",
"sha256": "ab1234567890...",
"size": 10485760,
"mime_type": "application/json",
"original_name": "data.json"
}
```
## 3. 核心接口定义 (Rust Trait)
### 3.1 ContextStore Trait
```rust
use anyhow::Result;
use std::io::Read;
pub trait ContextStore {
/// 初始化仓库
fn init_repo(&self, req_id: &str) -> Result<()>;
/// 读取文件内容
/// 自动逻辑:读取 Git Blob -> 解析 JSON -> 如果是 Ref读 Blob Store否则直接返回
fn read_file(&self, req_id: &str, commit_hash: &str, path: &str) -> Result<Box<dyn Read>>;
/// 读取目录
fn list_dir(&self, req_id: &str, commit_hash: &str, path: &str) -> Result<Vec<DirEntry>>;
/// 获取变更
fn diff(&self, req_id: &str, from_commit: &str, to_commit: &str) -> Result<Vec<FileChange>>;
/// 三路合并 (In-Memory)
/// 返回新生成的 Tree OID不生成 Commit
fn merge_trees(&self, req_id: &str, base: &str, ours: &str, theirs: &str) -> Result<String>;
/// 创建写事务
fn begin_transaction(&self, req_id: &str, base_commit: &str) -> Result<Box<dyn Transaction>>;
}
#[derive(Debug)]
pub struct DirEntry {
pub name: String,
pub kind: EntryKind, // File | Dir
pub object_id: String,
}
#[derive(Debug)]
pub enum FileChange {
Added(String),
Modified(String),
Deleted(String),
}
```
### 3.2 Transaction Trait (写操作)
```rust
pub trait Transaction {
/// 写入文件
/// 内部逻辑:
/// 1. 计算 content SHA-256
/// 2. if size > 1MB: 写 Blob Store构造 Ref JSON
/// 3. 写 Git Blob (Raw 或 Ref)
/// 4. 更新内存中的 Index/TreeBuilder
fn write(&mut self, path: &str, content: &[u8]) -> Result<()>;
/// 删除文件
fn remove(&mut self, path: &str) -> Result<()>;
/// 提交更改
/// 1. Write Tree Object
/// 2. Create Commit Object (Parent = base_commit)
/// 3. Return new Commit Hash
fn commit(self, message: &str, author: &str) -> Result<String>;
}
```
## 4. 实现细节规范
### 4.1 读操作流程 (read_file)
1. Open Repo: `/mnt/workflow_data/repos/{req_id}.git`
2. Locate Tree: Parse `commit_hash` -> `tree_id`.
3. Find Entry: 在 Tree 中查找 `path`.
4. Read Blob: 读取 Git Blob 内容。
5. **Check Ref**: 尝试解析为 `BlobRef` 结构。
* **Success**: 构建 Blob Store 路径 `/mnt/workflow_data/blobs/{req_id}/{sha256[0..2]}/{sha256}`,打开文件流。
* **Fail** (说明是普通小文件): 返回 `Cursor::new(blob_content)`.
### 4.2 写操作流程 (write)
1. Check Size: `content.len()`.
2. **Large File Path**:
* Calc SHA-256.
* Write to Blob Store (Ensure parent dir exists).
* Content = JSON String of `BlobRef`.
3. **Git Write**:
* `odb.write(Blob, content)`.
* Update in-memory `git2::TreeBuilder`.
### 4.3 合并流程 (merge_trees)
1. Load Trees: `base_tree`, `our_tree`, `their_tree`.
2. `repo.merge_trees(base, our, their, opts)`.
3. Check Conflicts: `index.has_conflicts()`.
* If conflict: Return Error (Complex resolution left to Orchestrator Agent).
4. Write Result: `index.write_tree_to(repo)`.
5. Return Tree Hash.
## 5. Web UI & Fuse
* **Web**: 基于 Axum路由 `GET /api/v1/repo/:req_id/tree/:commit/*path`。复用 `ContextStore` 接口。
* **Fuse**: 基于 `fuser`
* Mount Point: `/mnt/vgcs_view/{req_id}/{commit}`.
* `read()` -> `store.read_file()`.
* `readdir()` -> `store.list_dir()`.
* 只读挂载,用于 Debug。
## 6. 依赖库
* `git2` (0.18): 核心操作。
* `sha2` (0.10): 哈希计算。
* `serde_json`: Ref 序列化。
* `anyhow`: 错误处理。

View File

@ -0,0 +1,100 @@
# 设计方案 2: DocOS (Document Object System) [详细设计版]
## 1. 定位与目标
DocOS 是构建在 VGCS 之上的逻辑层,负责处理文档结构的演进(裂变、聚合)。
它不操作 Git Hash而是操作逻辑路径。它通过 `ContextStore` 接口与底层交互。
## 2. 数据结构定义
### 2.1 逻辑节点 (DocNode)
这屏蔽了底层是 `File` 还是 `Dir` 的差异。
```rust
#[derive(Debug, Serialize, Deserialize)]
pub enum DocNodeKind {
Leaf, // 纯内容节点 (对应文件)
Composite, // 复合节点 (对应目录,含 index.md)
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DocNode {
pub name: String,
pub path: String, // 逻辑路径 e.g., "Analysis/Revenue"
pub kind: DocNodeKind,
pub children: Vec<DocNode>, // 仅 Composite 有值
}
```
## 3. 核心接口定义 (DocManager Trait)
```rust
pub trait DocManager {
/// 基于最新的 Commit 重新加载状态
fn reload(&mut self, commit_hash: &str) -> Result<()>;
/// 获取当前文档树大纲
fn get_outline(&self) -> Result<DocNode>;
/// 读取节点内容
/// 逻辑:
/// - Leaf: 读 `path`
/// - Composite: 读 `path/index.md`
fn read_content(&self, path: &str) -> Result<String>;
// --- 写入操作 (Buffer Action) ---
/// 写入内容 (Upsert)
/// 逻辑:
/// - 路径存在且是 Leaf -> 覆盖
/// - 路径存在且是 Composite -> 覆盖 index.md
/// - 路径不存在 -> 创建 Leaf
fn write_content(&mut self, path: &str, content: &str);
/// 插入子章节 (Implies Promotion)
/// 逻辑:
/// - 如果 parent 是 Leaf -> 执行 Promote (Rename Leaf->Dir/index.md) -> 创建 Child
/// - 如果 parent 是 Composite -> 直接创建 Child
fn insert_subsection(&mut self, parent_path: &str, name: &str, content: &str);
/// 提交变更
fn save(&mut self, message: &str) -> Result<String>;
}
```
## 4. 关键演进逻辑 (Implementation Specs)
### 4.1 裂变 (Promote Leaf to Composite)
假设 `path = "A"` 是 Leaf (对应文件 `A`).
Action `insert_subsection("A", "B")`:
1. Read content of `A`.
2. Delete file `A` (in transaction).
3. Write content to `A/index.md`.
4. Write new content to `A/B`.
*注意*: Git 会将其视为 Delete + Add或 Rename。VGCS 底层会处理。
### 4.2 聚合 (Demote Composite to Leaf) - *Optional*
假设 `path = "A"` 是 Composite (目录 `A/`).
Action `demote("A")`:
1. Read `A/index.md`.
2. Concatenate children content (Optional policy).
3. Delete dir `A/`.
4. Write content to file `A`.
### 4.3 路径规范
* **Root**: `/` (对应 Repo Root).
* **Meta**: `_meta.json` (用于存储手动排序信息,如果需要).
* **Content File**:
* Leaf: `Name` (No extension assumption, or `.md` default).
* Composite: `Name/index.md`.
## 5. 实现依赖
DocOS 需要持有一个 `Box<dyn Transaction>` 实例。
所有的 `write_*` 操作都只是在调用 Transaction 的 `write`
只有调用 `save()`Transaction 才会 `commit`
## 6. 错误处理
* `PathNotFound`: 读不存在的路径。
* `PathCollision`: 尝试创建已存在的文件。
* `InvalidOperation`: 尝试在 Leaf 节点下创建子节点(需显式调用 Promote 或 insert_subsection

View File

@ -17,10 +17,12 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.90.10",
"@zodios/core": "^10.9.6",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"elkjs": "^0.11.0",
"lucide-react": "^0.554.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
@ -31,7 +33,8 @@
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7",
"tailwindcss-typography": "^3.1.0",
"zod": "^4.1.12",
"web-worker": "^1.5.0",
"zod": "^3.24.1",
"zustand": "^5.0.8"
},
"devDependencies": {
@ -47,6 +50,7 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"openapi-zod-client": "^1.18.3",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.17",
"typescript": "~5.9.3",
@ -67,6 +71,99 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "11.7.2",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz",
"integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.15",
"js-yaml": "^4.1.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/philsturgeon"
}
},
"node_modules/@apidevtools/openapi-schemas": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@apidevtools/swagger-methods": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
"dev": true,
"license": "MIT"
},
"node_modules/@apidevtools/swagger-parser": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.1.tgz",
"integrity": "sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "11.7.2",
"@apidevtools/openapi-schemas": "^2.1.0",
"@apidevtools/swagger-methods": "^3.0.2",
"@jsdevtools/ono": "^7.1.3",
"ajv": "^8.17.1",
"ajv-draft-04": "^1.0.0",
"call-me-maybe": "^1.0.2"
},
"peerDependencies": {
"openapi-types": ">=7"
}
},
"node_modules/@apidevtools/swagger-parser/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@apidevtools/swagger-parser/node_modules/ajv-draft-04": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
"integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"ajv": "^8.5.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/@apidevtools/swagger-parser/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@ -1088,6 +1185,24 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"dev": true,
"license": "MIT"
},
"node_modules/@liuli-util/fs-extra": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@liuli-util/fs-extra/-/fs-extra-0.1.0.tgz",
"integrity": "sha512-eaAyDyMGT23QuRGbITVY3SOJff3G9ekAAyGqB9joAnTBmqvFN+9a1FazOdO70G6IUqgpKV451eBHYSRcOJ/FNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/fs-extra": "^9.0.13",
"fs-extra": "^10.1.0"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -2796,6 +2911,66 @@
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.6.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.1.0",
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.6.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
"version": "1.1.0",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
"version": "1.0.7",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.5.0",
"@emnapi/runtime": "^1.5.0",
"@tybys/wasm-util": "^0.10.1"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
"version": "0.10.1",
"dev": true,
"inBundle": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
"version": "2.8.1",
"dev": true,
"inBundle": true,
"license": "0BSD",
"optional": true
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.17",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz",
@ -3205,6 +3380,16 @@
"@types/estree": "*"
}
},
"node_modules/@types/fs-extra": {
"version": "9.0.13",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
"integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
@ -3575,6 +3760,16 @@
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/@zodios/core": {
"version": "10.9.6",
"resolved": "https://registry.npmjs.org/@zodios/core/-/core-10.9.6.tgz",
"integrity": "sha512-aH4rOdb3AcezN7ws8vDgBfGboZMk2JGGzEq/DtW65MhnRxyTGRuLJRWVQ/2KxDgWvV2F5oTkAS+5pnjKbl0n+A==",
"license": "MIT",
"peerDependencies": {
"axios": "^0.x || ^1.0.0",
"zod": "^3.x"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@ -3790,6 +3985,16 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@ -3803,6 +4008,13 @@
"node": ">= 0.4"
}
},
"node_modules/call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
"dev": true,
"license": "MIT"
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -4253,6 +4465,12 @@
"dev": true,
"license": "ISC"
},
"node_modules/elkjs": {
"version": "0.11.0",
"resolved": "http://npm.repo.lan/elkjs/-/elkjs-0.11.0.tgz",
"integrity": "sha512-u4J8h9mwEDaYMqo0RYJpqNMFDoMK7f+pu4GjcV+N8jIC7TRdORgzkfSjTJemhqONFfH6fBI3wpysgWbhgVWIXw==",
"license": "EPL-2.0"
},
"node_modules/enhanced-resolve": {
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
@ -4571,6 +4789,13 @@
"node": ">=0.10.0"
}
},
"node_modules/eval-estree-expression": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/eval-estree-expression/-/eval-estree-expression-3.0.1.tgz",
"integrity": "sha512-zTLKGbiVdQYp4rQkSoXPibrFf5ZoPn6jzExegRLEQ13F+FSxu5iLgaRH6hlDs2kWSUa6vp8yD20cdJi0me6pEw==",
"dev": true,
"license": "MIT"
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@ -4628,6 +4853,23 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@ -4752,6 +4994,21 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -4884,6 +5141,28 @@
"dev": true,
"license": "MIT"
},
"node_modules/handlebars": {
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"wordwrap": "^1.0.0"
},
"bin": {
"handlebars": "bin/handlebars"
},
"engines": {
"node": ">=0.4.7"
},
"optionalDependencies": {
"uglify-js": "^3.1.4"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -5216,6 +5495,19 @@
"node": ">=6"
}
},
"node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -6491,6 +6783,16 @@
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@ -6523,6 +6825,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true,
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@ -6540,6 +6849,49 @@
"node": ">=0.10.0"
}
},
"node_modules/openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
"dev": true,
"license": "MIT"
},
"node_modules/openapi-zod-client": {
"version": "1.18.3",
"resolved": "https://registry.npmjs.org/openapi-zod-client/-/openapi-zod-client-1.18.3.tgz",
"integrity": "sha512-10vYK7xo1yyZfcoRvYNGIsDeej1CG9k63u8dkjbGBlr+NHZMy2Iy2h9s11UWNKdj6XMDWbNOPp5gIy8YdpgPtQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@liuli-util/fs-extra": "^0.1.0",
"@zodios/core": "^10.3.1",
"axios": "^1.6.0",
"cac": "^6.7.14",
"handlebars": "^4.7.7",
"openapi-types": "^12.0.2",
"openapi3-ts": "3.1.0",
"pastable": "^2.2.1",
"prettier": "^2.7.1",
"tanu": "^0.1.13",
"ts-pattern": "^5.0.1",
"whence": "^2.0.0",
"zod": "^3.19.1"
},
"bin": {
"openapi-zod-client": "bin.js"
}
},
"node_modules/openapi3-ts": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-3.1.0.tgz",
"integrity": "sha512-1qKTvCCVoV0rkwUh1zq5o8QyghmwYPuhdvtjv1rFjuOnJToXhQyF8eGjNETQ8QmGjr9Jz/tkAKLITIl2s7dw3A==",
"dev": true,
"license": "MIT",
"dependencies": {
"yaml": "^2.1.3"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -6628,6 +6980,32 @@
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
"node_modules/pastable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/pastable/-/pastable-2.2.1.tgz",
"integrity": "sha512-K4ClMxRKpgN4sXj6VIPPrvor/TMp2yPNCGtfhvV106C73SwefQ3FuegURsH7AQHpqu0WwbvKXRl1HQxF6qax9w==",
"dev": true,
"dependencies": {
"@babel/core": "^7.20.12",
"ts-toolbelt": "^9.6.0",
"type-fest": "^3.5.3"
},
"engines": {
"node": ">=14.x"
},
"peerDependencies": {
"react": ">=17",
"xstate": ">=4.32.1"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"xstate": {
"optional": true
}
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -6728,6 +7106,22 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/property-information": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
@ -7024,6 +7418,16 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@ -7156,6 +7560,16 @@
"node": ">=8"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -7269,6 +7683,31 @@
"lodash": "^4.17.15"
}
},
"node_modules/tanu": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/tanu/-/tanu-0.1.13.tgz",
"integrity": "sha512-UbRmX7ccZ4wMVOY/Uw+7ji4VOkEYSYJG1+I4qzbnn4qh/jtvVbrm6BFnF12NQQ4+jGv21wKmjb1iFyUSVnBWcQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"tslib": "^2.4.0",
"typescript": "^4.7.4"
}
},
"node_modules/tanu/node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
@ -7377,6 +7816,20 @@
"typescript": ">=4.8.4"
}
},
"node_modules/ts-pattern": {
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.9.0.tgz",
"integrity": "sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==",
"dev": true,
"license": "MIT"
},
"node_modules/ts-toolbelt": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz",
"integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@ -7396,6 +7849,19 @@
"node": ">= 0.8.0"
}
},
"node_modules/type-fest": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
"integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@ -7434,6 +7900,20 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/uglify-js": {
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"bin": {
"uglifyjs": "bin/uglifyjs"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
@ -7528,6 +8008,16 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
@ -7762,6 +8252,26 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/web-worker": {
"version": "1.5.0",
"resolved": "http://npm.repo.lan/web-worker/-/web-worker-1.5.0.tgz",
"integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==",
"license": "Apache-2.0"
},
"node_modules/whence": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/whence/-/whence-2.1.0.tgz",
"integrity": "sha512-4UBPMg5mng5KLzdliVQdQ4fJwCdIMXkE8CkoDmGKRy5r8pV9xq+nVgf/sKXpmNEIOtFp7m7v2bFdb7JoLvh+Hg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.0",
"eval-estree-expression": "^3.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -7788,6 +8298,13 @@
"node": ">=0.10.0"
}
},
"node_modules/wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
"dev": true,
"license": "MIT"
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@ -7795,6 +8312,19 @@
"dev": true,
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@ -7809,9 +8339,9 @@
}
},
"node_modules/zod": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
"version": "3.25.76",
"resolved": "http://npm.repo.lan/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"

View File

@ -7,7 +7,8 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"gen:api": "openapi-zod-client ../openapi.json -o src/api/schema.gen.ts --export-schemas --export-types && sed -i 's/^type /export type /' src/api/schema.gen.ts"
},
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.3",
@ -19,10 +20,12 @@
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.90.10",
"@zodios/core": "^10.9.6",
"axios": "^1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"elkjs": "^0.11.0",
"lucide-react": "^0.554.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
@ -33,7 +36,8 @@
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7",
"tailwindcss-typography": "^3.1.0",
"zod": "^4.1.12",
"web-worker": "^1.5.0",
"zod": "^3.24.1",
"zustand": "^5.0.8"
},
"devDependencies": {
@ -49,6 +53,7 @@
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"openapi-zod-client": "^1.18.3",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.17",
"typescript": "~5.9.3",

View File

@ -0,0 +1,49 @@
#!/bin/bash
set -euo pipefail
PROJECT_DIR="${PROJECT_DIR:-/workspace/frontend}"
LOCKFILE="${PROJECT_DIR}/package-lock.json"
NODE_MODULES_DIR="${PROJECT_DIR}/node_modules"
HASH_FILE="${NODE_MODULES_DIR}/.package-lock.hash"
DEV_COMMAND="${DEV_COMMAND:-npm run dev -- --host 0.0.0.0 --port 3001}"
cd "${PROJECT_DIR}"
calculate_lock_hash() {
sha256sum "${LOCKFILE}" | awk '{print $1}'
}
write_hash() {
calculate_lock_hash > "${HASH_FILE}"
}
install_dependencies() {
echo "[frontend] 安装/更新依赖..."
npm ci
write_hash
}
if [ ! -d "${NODE_MODULES_DIR}" ]; then
install_dependencies
elif [ ! -f "${HASH_FILE}" ]; then
install_dependencies
else
current_hash="$(calculate_lock_hash)"
installed_hash="$(cat "${HASH_FILE}" 2>/dev/null || true)"
if [ "${current_hash}" != "${installed_hash}" ]; then
echo "[frontend] package-lock.json 发生变化,重新安装依赖..."
install_dependencies
else
echo "[frontend] 依赖哈希一致,跳过 npm ci。"
fi
fi
# Check if node_modules/.bin exists in PATH, if not add it
if [[ ":$PATH:" != *":${NODE_MODULES_DIR}/.bin:"* ]]; then
export PATH="${NODE_MODULES_DIR}/.bin:$PATH"
fi
echo "Starting development server..."
exec ${DEV_COMMAND}

View File

@ -1,18 +1,21 @@
import axios from 'axios';
import { api } from './schema.gen';
export const apiClient = axios.create({
baseURL: '/api/v1',
headers: {
'Content-Type': 'application/json',
},
});
// Initialize the Zodios client with the base URL
export const apiClient = api;
// Add response interceptor for error handling if needed
apiClient.interceptors.response.use(
(response) => response,
(error) => {
// Handle global errors like 401, 403 here
return Promise.reject(error);
}
);
// Configure base URL (Zodios doesn't strictly require it if paths are relative, but good practice)
// Note: api variable from schema.gen.ts is already a Zodios instance with endpoints.
// But we might need to set the baseURL.
// Actually, Zodios constructor takes (baseUrl, endpoints).
// schema.gen.ts exports `export const api = new Zodios(endpoints);` which implies no base URL.
// We should probably use the factory function if available, or mutate axios instance.
// Let's look at schema.gen.ts again.
// export function createApiClient(baseUrl: string, options?: ZodiosOptions) {
// return new Zodios(baseUrl, endpoints, options);
// }
// So we should use createApiClient.
import { createApiClient } from './schema.gen';
export const client = createApiClient('/');

View File

@ -0,0 +1,634 @@
import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core";
import { z } from "zod";
export type AnalysisTemplateSet = {
modules: {};
name: string;
};
export type AnalysisModuleConfig = {
dependencies: Array<string>;
model_id: string;
name: string;
prompt_template: string;
provider_id: string;
};
export type AnalysisTemplateSets = {};
export type ConfigFieldSchema = {
default_value?: (string | null) | undefined;
description?: (string | null) | undefined;
field_type: FieldType;
key: ConfigKey;
label: string;
options?: (Array<string> | null) | undefined;
placeholder?: (string | null) | undefined;
required: boolean;
};
export type FieldType = "Text" | "Password" | "Url" | "Boolean" | "Select";
export type ConfigKey =
| "ApiKey"
| "ApiToken"
| "ApiUrl"
| "BaseUrl"
| "SecretKey"
| "Username"
| "Password"
| "SandboxMode"
| "Region";
export type DataSourceConfig = {
api_key?: (string | null) | undefined;
api_url?: (string | null) | undefined;
enabled: boolean;
provider: DataSourceProvider;
};
export type DataSourceProvider = "Tushare" | "Finnhub" | "Alphavantage" | "Yfinance";
export type DataSourcesConfig = {};
export type HealthStatus = {
details: {};
module_id: string;
status: ServiceStatus;
version: string;
};
export type ServiceStatus = "Ok" | "Degraded" | "Unhealthy";
export type LlmProvider = {
api_base_url: string;
api_key: string;
models: Array<LlmModel>;
name: string;
};
export type LlmModel = {
is_active: boolean;
model_id: string;
name?: (string | null) | undefined;
};
export type LlmProvidersConfig = {};
export type ProviderMetadata = {
config_schema: Array<ConfigFieldSchema>;
description: string;
icon_url?: (string | null) | undefined;
id: string;
name_cn: string;
name_en: string;
supports_test_connection: boolean;
};
export type StartWorkflowCommand = {
market: string;
request_id: string;
symbol: CanonicalSymbol;
template_id: string;
};
export type CanonicalSymbol = string;
export type TaskNode = {
id: string;
initial_status: TaskStatus;
name: string;
type: TaskType;
};
export type TaskStatus =
| "Pending"
| "Scheduled"
| "Running"
| "Completed"
| "Failed"
| "Skipped";
export type TaskType = "DataFetch" | "DataProcessing" | "Analysis";
export type TaskProgress = {
details: string;
progress_percent: number;
request_id: string;
started_at: string;
status: ObservabilityTaskStatus;
task_name: string;
};
export type ObservabilityTaskStatus = "Queued" | "InProgress" | "Completed" | "Failed";
export type WorkflowDag = {
edges: Array<TaskDependency>;
nodes: Array<TaskNode>;
};
export type TaskDependency = {
from: string;
to: string;
};
export type WorkflowEvent =
| {
payload: {
task_graph: WorkflowDag;
timestamp: number;
};
type: "WorkflowStarted";
}
| {
payload: {
message?: (string | null) | undefined;
progress?: (number | null) | undefined;
status: TaskStatus;
task_id: string;
task_type: TaskType;
timestamp: number;
};
type: "TaskStateChanged";
}
| {
payload: {
content_delta: string;
index: number;
task_id: string;
};
type: "TaskStreamUpdate";
}
| {
payload: {
end_timestamp: number;
result_summary: unknown;
};
type: "WorkflowCompleted";
}
| {
payload: {
end_timestamp: number;
is_fatal: boolean;
reason: string;
};
type: "WorkflowFailed";
}
| {
payload: {
task_graph: WorkflowDag;
tasks_output: {};
tasks_status: {};
timestamp: number;
};
type: "WorkflowStateSnapshot";
};
const AnalysisModuleConfig: z.ZodType<AnalysisModuleConfig> = z.object({
dependencies: z.array(z.string()),
model_id: z.string(),
name: z.string(),
prompt_template: z.string(),
provider_id: z.string(),
});
const AnalysisTemplateSet: z.ZodType<AnalysisTemplateSet> = z.object({
modules: z.record(AnalysisModuleConfig),
name: z.string(),
});
const AnalysisTemplateSets: z.ZodType<AnalysisTemplateSets> =
z.record(AnalysisTemplateSet);
const DataSourceProvider = z.enum([
"Tushare",
"Finnhub",
"Alphavantage",
"Yfinance",
]);
const DataSourceConfig: z.ZodType<DataSourceConfig> = z.object({
api_key: z.union([z.string(), z.null()]).optional(),
api_url: z.union([z.string(), z.null()]).optional(),
enabled: z.boolean(),
provider: DataSourceProvider,
});
const DataSourcesConfig: z.ZodType<DataSourcesConfig> =
z.record(DataSourceConfig);
const TestLlmConfigRequest = z.object({
api_base_url: z.string(),
api_key: z.string(),
model_id: z.string(),
});
const LlmModel: z.ZodType<LlmModel> = z.object({
is_active: z.boolean(),
model_id: z.string(),
name: z.union([z.string(), z.null()]).optional(),
});
const LlmProvider: z.ZodType<LlmProvider> = z.object({
api_base_url: z.string(),
api_key: z.string(),
models: z.array(LlmModel),
name: z.string(),
});
const LlmProvidersConfig: z.ZodType<LlmProvidersConfig> = z.record(LlmProvider);
const TestConfigRequest = z.object({ data: z.unknown(), type: z.string() });
const TestConnectionResponse = z.object({
message: z.string(),
success: z.boolean(),
});
const DiscoverPreviewRequest = z.object({
api_base_url: z.string(),
api_key: z.string(),
});
const FieldType = z.enum(["Text", "Password", "Url", "Boolean", "Select"]);
const ConfigKey = z.enum([
"ApiKey",
"ApiToken",
"ApiUrl",
"BaseUrl",
"SecretKey",
"Username",
"Password",
"SandboxMode",
"Region",
]);
const ConfigFieldSchema: z.ZodType<ConfigFieldSchema> = z.object({
default_value: z.union([z.string(), z.null()]).optional(),
description: z.union([z.string(), z.null()]).optional(),
field_type: FieldType,
key: ConfigKey,
label: z.string(),
options: z.union([z.array(z.string()), z.null()]).optional(),
placeholder: z.union([z.string(), z.null()]).optional(),
required: z.boolean(),
});
const ProviderMetadata: z.ZodType<ProviderMetadata> = z.object({
config_schema: z.array(ConfigFieldSchema),
description: z.string(),
icon_url: z.union([z.string(), z.null()]).optional(),
id: z.string(),
name_cn: z.string(),
name_en: z.string(),
supports_test_connection: z.boolean(),
});
const SymbolResolveRequest = z.object({
market: z.union([z.string(), z.null()]).optional(),
symbol: z.string(),
});
const SymbolResolveResponse = z.object({
market: z.string(),
symbol: z.string(),
});
const DataRequest = z.object({
market: z.union([z.string(), z.null()]).optional(),
symbol: z.string(),
template_id: z.string(),
});
const RequestAcceptedResponse = z.object({
market: z.string(),
request_id: z.string().uuid(),
symbol: z.string(),
});
const ObservabilityTaskStatus = z.enum([
"Queued",
"InProgress",
"Completed",
"Failed",
]);
const TaskProgress: z.ZodType<TaskProgress> = z.object({
details: z.string(),
progress_percent: z.number().int().gte(0),
request_id: z.string().uuid(),
started_at: z.string().datetime({ offset: true }),
status: ObservabilityTaskStatus,
task_name: z.string(),
});
const CanonicalSymbol = z.string();
const ServiceStatus = z.enum(["Ok", "Degraded", "Unhealthy"]);
const HealthStatus: z.ZodType<HealthStatus> = z.object({
details: z.record(z.string()),
module_id: z.string(),
status: ServiceStatus,
version: z.string(),
});
const StartWorkflowCommand: z.ZodType<StartWorkflowCommand> = z.object({
market: z.string(),
request_id: z.string().uuid(),
symbol: CanonicalSymbol,
template_id: z.string(),
});
const TaskDependency: z.ZodType<TaskDependency> = z.object({
from: z.string(),
to: z.string(),
});
const TaskStatus = z.enum([
"Pending",
"Scheduled",
"Running",
"Completed",
"Failed",
"Skipped",
]);
const TaskType = z.enum(["DataFetch", "DataProcessing", "Analysis"]);
const TaskNode: z.ZodType<TaskNode> = z.object({
id: z.string(),
initial_status: TaskStatus,
name: z.string(),
type: TaskType,
});
const WorkflowDag: z.ZodType<WorkflowDag> = z.object({
edges: z.array(TaskDependency),
nodes: z.array(TaskNode),
});
const WorkflowEvent: z.ZodType<WorkflowEvent> = z.union([
z
.object({
payload: z
.object({ task_graph: WorkflowDag, timestamp: z.number().int() })
.passthrough(),
type: z.literal("WorkflowStarted"),
})
.passthrough(),
z
.object({
payload: z
.object({
message: z.union([z.string(), z.null()]).optional(),
progress: z.union([z.number(), z.null()]).optional(),
status: TaskStatus,
task_id: z.string(),
task_type: TaskType,
timestamp: z.number().int(),
})
.passthrough(),
type: z.literal("TaskStateChanged"),
})
.passthrough(),
z
.object({
payload: z
.object({
content_delta: z.string(),
index: z.number().int().gte(0),
task_id: z.string(),
})
.passthrough(),
type: z.literal("TaskStreamUpdate"),
})
.passthrough(),
z
.object({
payload: z
.object({
end_timestamp: z.number().int(),
result_summary: z.unknown(),
})
.passthrough(),
type: z.literal("WorkflowCompleted"),
})
.passthrough(),
z
.object({
payload: z
.object({
end_timestamp: z.number().int(),
is_fatal: z.boolean(),
reason: z.string(),
})
.passthrough(),
type: z.literal("WorkflowFailed"),
})
.passthrough(),
z
.object({
payload: z
.object({
task_graph: WorkflowDag,
tasks_output: z.record(z.union([z.string(), z.null()])),
tasks_status: z.record(TaskStatus),
timestamp: z.number().int(),
})
.passthrough(),
type: z.literal("WorkflowStateSnapshot"),
})
.passthrough(),
]);
export const schemas = {
AnalysisModuleConfig,
AnalysisTemplateSet,
AnalysisTemplateSets,
DataSourceProvider,
DataSourceConfig,
DataSourcesConfig,
TestLlmConfigRequest,
LlmModel,
LlmProvider,
LlmProvidersConfig,
TestConfigRequest,
TestConnectionResponse,
DiscoverPreviewRequest,
FieldType,
ConfigKey,
ConfigFieldSchema,
ProviderMetadata,
SymbolResolveRequest,
SymbolResolveResponse,
DataRequest,
RequestAcceptedResponse,
ObservabilityTaskStatus,
TaskProgress,
CanonicalSymbol,
ServiceStatus,
HealthStatus,
StartWorkflowCommand,
TaskDependency,
TaskStatus,
TaskType,
TaskNode,
WorkflowDag,
WorkflowEvent,
};
const endpoints = makeApi([
{
method: "get",
path: "/api/v1/configs/analysis_template_sets",
alias: "get_analysis_template_sets",
requestFormat: "json",
response: z.record(AnalysisTemplateSet),
},
{
method: "put",
path: "/api/v1/configs/analysis_template_sets",
alias: "update_analysis_template_sets",
requestFormat: "json",
parameters: [
{
name: "body",
type: "Body",
schema: z.record(AnalysisTemplateSet),
},
],
response: z.record(AnalysisTemplateSet),
},
{
method: "get",
path: "/api/v1/configs/data_sources",
alias: "get_data_sources_config",
requestFormat: "json",
response: z.record(DataSourceConfig),
},
{
method: "put",
path: "/api/v1/configs/data_sources",
alias: "update_data_sources_config",
requestFormat: "json",
parameters: [
{
name: "body",
type: "Body",
schema: z.record(DataSourceConfig),
},
],
response: z.record(DataSourceConfig),
},
{
method: "get",
path: "/api/v1/configs/llm_providers",
alias: "get_llm_providers_config",
requestFormat: "json",
response: z.record(LlmProvider),
},
{
method: "put",
path: "/api/v1/configs/llm_providers",
alias: "update_llm_providers_config",
requestFormat: "json",
parameters: [
{
name: "body",
type: "Body",
schema: z.record(LlmProvider),
},
],
response: z.record(LlmProvider),
},
{
method: "post",
path: "/api/v1/configs/llm/test",
alias: "test_llm_config",
requestFormat: "json",
parameters: [
{
name: "body",
type: "Body",
schema: TestLlmConfigRequest,
},
],
response: z.void(),
},
{
method: "post",
path: "/api/v1/configs/test",
alias: "test_data_source_config",
requestFormat: "json",
parameters: [
{
name: "body",
type: "Body",
schema: z.object({ data: z.unknown(), type: z.string() }),
},
],
response: TestConnectionResponse,
},
{
method: "post",
path: "/api/v1/discover-models",
alias: "discover_models_preview",
requestFormat: "json",
parameters: [
{
name: "body",
type: "Body",
schema: DiscoverPreviewRequest,
},
],
response: z.void(),
errors: [
{
status: 502,
description: `Provider error`,
schema: z.void(),
},
],
},
{
method: "get",
path: "/api/v1/discover-models/:provider_id",
alias: "discover_models",
requestFormat: "json",
parameters: [
{
name: "provider_id",
type: "Path",
schema: z.string(),
},
],
response: z.void(),
errors: [
{
status: 404,
description: `Provider not found`,
schema: z.void(),
},
{
status: 502,
description: `Provider error`,
schema: z.void(),
},
],
},
{
method: "get",
path: "/api/v1/registry/providers",
alias: "get_registered_providers",
requestFormat: "json",
response: z.array(ProviderMetadata),
},
{
method: "post",
path: "/api/v1/tools/resolve-symbol",
alias: "resolve_symbol",
requestFormat: "json",
parameters: [
{
name: "body",
type: "Body",
schema: SymbolResolveRequest,
},
],
response: SymbolResolveResponse,
},
{
method: "post",
path: "/api/v1/workflow/start",
alias: "start_workflow",
requestFormat: "json",
parameters: [
{
name: "body",
type: "Body",
schema: DataRequest,
},
],
response: RequestAcceptedResponse,
},
{
method: "get",
path: "/health",
alias: "health_check",
requestFormat: "json",
response: z.void(),
},
{
method: "get",
path: "/tasks/:request_id",
alias: "get_task_progress",
requestFormat: "json",
parameters: [
{
name: "request_id",
type: "Path",
schema: z.string().uuid(),
},
],
response: z.array(TaskProgress),
errors: [
{
status: 404,
description: `Tasks not found`,
schema: z.void(),
},
],
},
]);
export const api = new Zodios(endpoints);
export function createApiClient(baseUrl: string, options?: ZodiosOptions) {
return new Zodios(baseUrl, endpoints, options);
}

View File

@ -0,0 +1,184 @@
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Badge } from "@/components/ui/badge"
import { AlertCircle } from "lucide-react"
import { ProviderMetadata, ConfigFieldSchema, schemas } from "@/api/schema.gen"
import { DataSourceConfig } from "@/types/config"
interface DynamicConfigFormProps {
metadata: ProviderMetadata;
initialConfig: DataSourceConfig;
onSave: (config: DataSourceConfig) => void;
onTest: (config: DataSourceConfig) => void;
isSaving: boolean;
isTesting: boolean;
}
export function DynamicConfigForm({
metadata,
initialConfig,
onSave,
onTest,
isSaving,
isTesting
}: DynamicConfigFormProps) {
const [config, setConfig] = useState<DataSourceConfig>(initialConfig);
const [isDirty, setIsDirty] = useState(false);
// Reset local state when initialConfig changes (e.g. after save or load)
useEffect(() => {
setConfig(initialConfig);
setIsDirty(false);
}, [initialConfig]);
// Helper function to map PascalCase key (from Enum serialization) to snake_case key (DataSourceConfig struct)
// Example: "ApiKey" -> "api_key", "ApiUrl" -> "api_url"
const mapKeyToConfigField = (key: string): keyof DataSourceConfig => {
// Simple mapping: convert to snake_case.
// Since we know the keys are things like "ApiKey", "ApiToken" (which maps to api_key usually?)
// Wait, DataSourceConfig has `api_key`, `api_url`.
// ConfigKey enum has `ApiKey`, `ApiUrl`, `ApiToken`.
// `ApiToken` usually maps to `api_key` in our `DataSourceConfig` struct (as seen in main.rs mapping comment).
// Manual mapping based on knowledge of DataSourceConfig structure
// If the key is unknown/unmapped, we try to use it as is or lowercase it.
switch (key) {
case "ApiKey": return "api_key";
case "ApiToken": return "api_key"; // Tushare uses ApiToken but stores in api_key
case "ApiUrl": return "api_url";
case "BaseUrl": return "api_url"; // Maybe?
default:
// Fallback: try snake case conversion or just lowercase
return key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`).replace(/^_/, "") as keyof DataSourceConfig;
}
}
// Generic handler for field updates
const handleChange = (fieldKey: string, value: any) => {
const configField = mapKeyToConfigField(fieldKey);
setConfig(prev => ({ ...prev, [configField]: value }));
setIsDirty(true);
}
const renderField = (fieldSchema: ConfigFieldSchema) => {
// fieldSchema.key is the Enum string (e.g. "ApiKey")
const configField = mapKeyToConfigField(fieldSchema.key as string);
// Access the config value using the mapped key
const currentValue = (config as any)[configField] || "";
const isEnabled = config.enabled;
switch (fieldSchema.field_type) {
case schemas.FieldType.enum.Boolean:
return (
<div className="flex items-center justify-between" key={fieldSchema.key}>
<Label htmlFor={fieldSchema.key}>{fieldSchema.label}</Label>
<Switch
id={fieldSchema.key}
checked={currentValue === true}
onCheckedChange={(checked) => handleChange(fieldSchema.key, checked)}
disabled={!isEnabled}
/>
</div>
);
case schemas.FieldType.enum.Select:
return (
<div className="space-y-2" key={fieldSchema.key}>
<Label htmlFor={fieldSchema.key}>{fieldSchema.label}</Label>
<Select
value={currentValue}
onValueChange={(val) => handleChange(fieldSchema.key, val)}
disabled={!isEnabled}
>
<SelectTrigger id={fieldSchema.key}>
<SelectValue placeholder={fieldSchema.placeholder || "Select..."} />
</SelectTrigger>
<SelectContent>
{fieldSchema.options?.map(opt => (
<SelectItem key={opt} value={opt}>{opt}</SelectItem>
))}
</SelectContent>
</Select>
{fieldSchema.description && <p className="text-sm text-muted-foreground">{fieldSchema.description}</p>}
</div>
);
case schemas.FieldType.enum.Password:
return (
<div className="space-y-2" key={fieldSchema.key}>
<Label htmlFor={fieldSchema.key}>{fieldSchema.label}</Label>
<Input
id={fieldSchema.key}
type="password"
value={currentValue}
onChange={(e) => handleChange(fieldSchema.key, e.target.value)}
placeholder={fieldSchema.placeholder || ""}
disabled={!isEnabled}
/>
{fieldSchema.description && <p className="text-sm text-muted-foreground">{fieldSchema.description}</p>}
</div>
);
default: // Text, Url
return (
<div className="space-y-2" key={fieldSchema.key}>
<Label htmlFor={fieldSchema.key}>{fieldSchema.label}</Label>
<Input
id={fieldSchema.key}
type="text"
value={currentValue}
onChange={(e) => handleChange(fieldSchema.key, e.target.value)}
placeholder={fieldSchema.placeholder || ""}
disabled={!isEnabled}
/>
{fieldSchema.description && <p className="text-sm text-muted-foreground">{fieldSchema.description}</p>}
</div>
);
}
}
return (
<Card className={config.enabled ? "border-primary/50 bg-accent/5" : "opacity-80"}>
<CardHeader className="flex flex-row items-start justify-between pb-2">
<div className="space-y-1">
<CardTitle className="text-xl flex items-center gap-2">
{metadata.name_cn || metadata.name_en}
{config.enabled ?
<Badge variant="default" className="bg-green-600 hover:bg-green-600 h-5 text-[10px]">Active</Badge> :
<Badge variant="outline" className="text-muted-foreground h-5 text-[10px]">Inactive</Badge>
}
</CardTitle>
<CardDescription>{metadata.description}</CardDescription>
</div>
<Switch
checked={config.enabled}
onCheckedChange={(checked) => handleChange('enabled', checked)}
/>
</CardHeader>
<CardContent className="space-y-4">
{metadata.config_schema.map(field => renderField(field))}
</CardContent>
<CardFooter className="flex justify-between pt-0">
<Button
variant="outline"
size="sm"
disabled={!config.enabled || isTesting || !metadata.supports_test_connection}
onClick={() => onTest(config)}
>
{isTesting ? <span className="animate-spin mr-2"></span> : <AlertCircle className="mr-2 h-4 w-4" />}
</Button>
<Button
size="sm"
disabled={!isDirty || isSaving}
onClick={() => onSave(config)}
>
{isSaving ? "保存中..." : "保存"}
</Button>
</CardFooter>
</Card>
)
}

View File

@ -1,11 +1,9 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { cn } from '@/lib/utils';
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
} from "@/components/ui/navigation-menu"
@ -72,4 +70,3 @@ export function Header() {
</header>
);
}

View File

@ -1,5 +1,6 @@
import { Outlet } from 'react-router-dom';
import { Header } from './Header';
import { Toaster } from '@/components/ui/toaster';
export function RootLayout() {
return (
@ -8,6 +9,7 @@ export function RootLayout() {
<main className="flex-1">
<Outlet />
</main>
<Toaster />
</div>
);
}

View File

@ -2,15 +2,16 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Badge } from "@/components/ui/badge"
import { CheckCircle2, XCircle, Loader2, Code } from "lucide-react"
import { CheckCircle2, XCircle, Loader2 } from "lucide-react"
import { useWorkflowStore } from "@/stores/useWorkflowStore"
import { TaskStatus } from "@/types/workflow"
import { schemas } from "@/api/schema.gen"
export function FinancialTable() {
const { tasks, dag } = useWorkflowStore();
// Identify DataFetch tasks dynamically from DAG
const fetchTasks = dag?.nodes.filter(n => n.type === 'DataFetch') || [];
const fetchTasks = dag?.nodes.filter(n => n.type === schemas.TaskType.enum.DataFetch) || [];
return (
<div className="space-y-6">
@ -18,13 +19,13 @@ export function FinancialTable() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{fetchTasks.map(node => {
const taskState = tasks[node.id];
const status = taskState?.status || 'Pending';
const status = taskState?.status || schemas.TaskStatus.enum.Pending;
return (
<Card key={node.id} className="flex flex-col">
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<CardTitle className="text-sm font-medium">{node.label}</CardTitle>
<CardTitle className="text-sm font-medium">{node.name}</CardTitle>
<StatusBadge status={status} />
</div>
</CardHeader>
@ -34,7 +35,7 @@ export function FinancialTable() {
</div>
{/* Mock Raw Data Preview if Completed */}
{status === 'Completed' && (
{status === schemas.TaskStatus.enum.Completed && (
<div className="mt-auto pt-2 border-t">
<div className="text-[10px] uppercase font-mono text-muted-foreground mb-1">Raw Response Preview</div>
<ScrollArea className="h-[80px] w-full rounded border bg-muted/50 p-2 font-mono text-[10px]">
@ -104,11 +105,11 @@ export function FinancialTable() {
function StatusBadge({ status }: { status: TaskStatus }) {
switch (status) {
case 'Running':
case schemas.TaskStatus.enum.Running:
return <Badge variant="outline" className="border-blue-500 text-blue-500"><Loader2 className="h-3 w-3 mr-1 animate-spin" /> Fetching</Badge>;
case 'Completed':
case schemas.TaskStatus.enum.Completed:
return <Badge variant="outline" className="border-green-500 text-green-500"><CheckCircle2 className="h-3 w-3 mr-1" /> Success</Badge>;
case 'Failed':
case schemas.TaskStatus.enum.Failed:
return <Badge variant="destructive"><XCircle className="h-3 w-3 mr-1" /> Failed</Badge>;
default:
return <Badge variant="secondary" className="text-muted-foreground">Pending</Badge>;

View File

@ -1,6 +1,6 @@
"use client";
import React, { useState, useRef, useEffect } from "react";
import { useState, useRef, useEffect } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";

View File

@ -62,7 +62,7 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] origin-[var(--radix-select-content-transform-origin)] overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className

View File

@ -6,9 +6,10 @@ type SwitchProps = {
checked?: boolean;
onCheckedChange?: (checked: boolean) => void;
className?: string;
disabled?: boolean;
};
export const Switch: React.FC<SwitchProps> = ({ id, name, checked, onCheckedChange, className }) => {
export const Switch: React.FC<SwitchProps> = ({ id, name, checked, onCheckedChange, className, disabled }) => {
return (
<input
id={id}
@ -17,6 +18,7 @@ export const Switch: React.FC<SwitchProps> = ({ id, name, checked, onCheckedChan
className={className}
checked={!!checked}
onChange={(e) => onCheckedChange?.(e.target.checked)}
disabled={disabled}
/>
);
};

View File

@ -0,0 +1,23 @@
import { useToast } from "@/hooks/use-toast"
import { Notification } from "@/components/ui/notification"
export function Toaster() {
const { toasts, dismiss } = useToast()
return (
<div className="fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]">
{toasts.map(function ({ id, title, description, type }) {
return (
<Notification
key={id}
message={title + (description ? `: ${description}` : "")}
type={type || 'info'}
isVisible={true}
onDismiss={() => dismiss(id)}
autoHide={false} // handled by hook
/>
)
})}
</div>
)
}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useCallback } from 'react';
import { useEffect, useCallback } from 'react';
import ReactFlow, {
Background,
Controls,
@ -9,11 +9,13 @@ import ReactFlow, {
MarkerType,
Position,
Handle,
useOnSelectionChange,
NodeMouseHandler
} from 'reactflow';
import 'reactflow/dist/style.css';
// @ts-ignore
import ELK from 'elkjs/lib/elk.bundled.js';
import { useWorkflowStore } from '@/stores/useWorkflowStore';
import { schemas } from '@/api/schema.gen';
import { TaskStatus } from '@/types/workflow';
import { cn } from '@/lib/utils';
import { CheckCircle2, Circle, Clock, AlertCircle, Loader2, Ban } from 'lucide-react';
@ -21,28 +23,28 @@ import { CheckCircle2, Circle, Clock, AlertCircle, Loader2, Ban } from 'lucide-r
// --- Custom Node Component ---
const StatusIcon = ({ status }: { status: TaskStatus }) => {
switch (status) {
case 'Completed': return <CheckCircle2 className="h-4 w-4 text-green-500" />;
case 'Running': return <Loader2 className="h-4 w-4 text-blue-500 animate-spin" />;
case 'Failed': return <AlertCircle className="h-4 w-4 text-red-500" />;
case 'Skipped': return <Ban className="h-4 w-4 text-gray-400" />;
case 'Scheduled': return <Clock className="h-4 w-4 text-yellow-500" />;
case schemas.TaskStatus.enum.Completed: return <CheckCircle2 className="h-4 w-4 text-green-500" />;
case schemas.TaskStatus.enum.Running: return <Loader2 className="h-4 w-4 text-blue-500 animate-spin" />;
case schemas.TaskStatus.enum.Failed: return <AlertCircle className="h-4 w-4 text-red-500" />;
case schemas.TaskStatus.enum.Skipped: return <Ban className="h-4 w-4 text-gray-400" />;
case schemas.TaskStatus.enum.Scheduled: return <Clock className="h-4 w-4 text-yellow-500" />;
default: return <Circle className="h-4 w-4 text-muted-foreground" />;
}
};
const WorkflowNode = ({ data, selected }: { data: { label: string, status: TaskStatus, type: string }, selected: boolean }) => {
const statusColors = {
'Pending': 'border-muted bg-card',
'Scheduled': 'border-yellow-500/50 bg-yellow-50/10',
'Running': 'border-blue-500 ring-2 ring-blue-500/20 bg-blue-50/10',
'Completed': 'border-green-500 bg-green-50/10',
'Failed': 'border-red-500 bg-red-50/10',
'Skipped': 'border-gray-200 bg-gray-50/5 opacity-60',
const statusColors: Record<string, string> = {
[schemas.TaskStatus.enum.Pending]: 'border-muted bg-card',
[schemas.TaskStatus.enum.Scheduled]: 'border-yellow-500/50 bg-yellow-50/10',
[schemas.TaskStatus.enum.Running]: 'border-blue-500 ring-2 ring-blue-500/20 bg-blue-50/10',
[schemas.TaskStatus.enum.Completed]: 'border-green-500 bg-green-50/10',
[schemas.TaskStatus.enum.Failed]: 'border-red-500 bg-red-50/10',
[schemas.TaskStatus.enum.Skipped]: 'border-gray-200 bg-gray-50/5 opacity-60',
};
return (
<div className={cn(
"px-4 py-2 rounded-lg border shadow-sm min-w-[150px] transition-all duration-300 cursor-pointer",
"px-4 py-2 rounded-lg border shadow-sm min-w-[180px] transition-all duration-300 cursor-pointer bg-background",
statusColors[data.status] || 'border-muted',
selected && "ring-2 ring-primary border-primary"
)}>
@ -61,83 +63,126 @@ const nodeTypes = {
taskNode: WorkflowNode,
};
// --- Layout Helper with ELK ---
const elk = new ELK();
const useLayout = () => {
const getLayoutedElements = useCallback(async (nodes: Node[], edges: Edge[]) => {
const graph = {
id: 'root',
layoutOptions: {
'elk.algorithm': 'layered',
'elk.direction': 'DOWN',
'elk.spacing.nodeNode': '60', // Horizontal spacing
'elk.layered.spacing.nodeNodeBetweenLayers': '80', // Vertical spacing
'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP',
// Optimize for fewer edge bends/crossings
'elk.edgeRouting': 'SPLINES',
},
children: nodes.map((node) => ({
id: node.id,
width: 180, // Match min-w-[180px]
height: 64, // Approx height
})),
edges: edges.map((edge) => ({
id: edge.id,
sources: [edge.source],
targets: [edge.target],
})),
};
try {
const layoutedGraph = await elk.layout(graph);
const layoutedNodes = nodes.map((node) => {
const nodeElk = layoutedGraph.children?.find((n) => n.id === node.id);
return {
...node,
targetPosition: Position.Top,
sourcePosition: Position.Bottom,
position: {
x: nodeElk?.x || 0,
y: nodeElk?.y || 0,
},
};
});
return { nodes: layoutedNodes, edges };
} catch (e) {
console.error('ELK Layout Error:', e);
return { nodes, edges };
}
}, []);
return { getLayoutedElements };
};
// --- Main Visualizer Component ---
export function WorkflowVisualizer() {
const { dag, tasks, setActiveTab, activeTab } = useWorkflowStore();
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const { getLayoutedElements } = useLayout();
const onNodeClick: NodeMouseHandler = useCallback((event, node) => {
const onNodeClick: NodeMouseHandler = useCallback((_event, node) => {
if (node.data.type === schemas.TaskType.enum.DataFetch) {
setActiveTab('data');
} else {
setActiveTab(node.id);
}
}, [setActiveTab]);
// Transform DAG to ReactFlow nodes/edges when DAG or Task Status changes
useEffect(() => {
if (!dag) return;
const levels: Record<string, number> = {};
const getLevel = (id: string): number => {
if (levels[id] !== undefined) return levels[id];
const node = dag.nodes.find(n => n.id === id);
if (!node || node.dependencies.length === 0) {
levels[id] = 0;
return 0;
}
const maxParentLevel = Math.max(...node.dependencies.map(getLevel));
levels[id] = maxParentLevel + 1;
return maxParentLevel + 1;
};
dag.nodes.forEach(n => getLevel(n.id));
const levelCounts: Record<number, number> = {};
const newNodes: Node[] = dag.nodes.map(node => {
const level = levels[node.id];
const indexInLevel = levelCounts[level] || 0;
levelCounts[level] = indexInLevel + 1;
const createGraph = async () => {
// 1. Create initial Nodes
const initialNodes: Node[] = dag.nodes.map(node => {
const taskState = tasks[node.id];
const status = taskState ? taskState.status : node.initial_status;
return {
id: node.id,
type: 'taskNode',
position: { x: indexInLevel * 220, y: level * 120 },
position: { x: 0, y: 0 },
data: {
label: node.label || node.id,
label: node.name,
status,
type: node.type
},
selected: activeTab === node.id, // Highlight active node
targetPosition: Position.Top,
sourcePosition: Position.Bottom,
selected: activeTab === node.id,
};
});
const newEdges: Edge[] = [];
dag.nodes.forEach(node => {
node.dependencies.forEach(depId => {
newEdges.push({
id: `${depId}-${node.id}`,
source: depId,
target: node.id,
type: 'smoothstep',
// 2. Create Edges (Use Default Bezier for better visuals than Smoothstep with hierarchical)
const initialEdges: Edge[] = dag.edges.map(edge => {
return {
id: `${edge.from}-${edge.to}`,
source: edge.from,
target: edge.to,
type: 'default', // Bezier curve
markerEnd: { type: MarkerType.ArrowClosed },
animated: tasks[depId]?.status === 'Running' || tasks[node.id]?.status === 'Running',
animated: tasks[edge.from]?.status === schemas.TaskStatus.enum.Running || tasks[edge.to]?.status === schemas.TaskStatus.enum.Running,
style: { stroke: '#64748b', strokeWidth: 1.5 }
});
});
};
});
setNodes(newNodes);
setEdges(newEdges);
}, [dag, tasks, activeTab, setNodes, setEdges]); // Depend on activeTab for selection
// 3. Apply Layout
const { nodes: layoutedNodes, edges: layoutedEdges } = await getLayoutedElements(initialNodes, initialEdges);
setNodes(layoutedNodes);
setEdges(layoutedEdges);
};
createGraph();
}, [dag, tasks, activeTab, setNodes, setEdges, getLayoutedElements]);
if (!dag) return <div className="flex items-center justify-center h-full text-muted-foreground">Waiting for workflow to start...</div>;
return (
<div className="h-[400px] w-full border rounded-lg bg-muted/5">
<div className="h-[300px] w-full border rounded-lg bg-muted/5">
<ReactFlow
nodes={nodes}
edges={edges}
@ -149,7 +194,7 @@ export function WorkflowVisualizer() {
attributionPosition="bottom-right"
>
<Background color="#94a3b8" gap={20} size={1} />
<Controls />
<Controls position="top-right" />
</ReactFlow>
</div>
);

View File

@ -0,0 +1,80 @@
import { useState, useEffect } from "react"
// Simple event bus for toasts
const listeners: Array<(state: State) => void> = []
type ToastType = 'success' | 'error' | 'info' | 'warning'
interface Toast {
id: string
title?: string
description?: string
type?: ToastType
duration?: number
}
interface State {
toasts: Toast[]
}
let memoryState: State = { toasts: [] }
function dispatch(action: State) {
memoryState = { ...action }
listeners.forEach((listener) => {
listener(memoryState)
})
}
function toast({ ...props }: Omit<Toast, "id">) {
const id = Math.random().toString(36).substring(2, 9)
const newToast = { ...props, id }
dispatch({
toasts: [...memoryState.toasts, newToast],
})
if (props.duration !== Infinity) {
setTimeout(() => {
dismiss(id)
}, props.duration || 3000)
}
return {
id,
dismiss: () => dismiss(id),
update: (props: Partial<Toast>) =>
dispatch({
toasts: memoryState.toasts.map((t) =>
t.id === id ? { ...t, ...props } : t
),
}),
}
}
function dismiss(toastId?: string) {
dispatch({
toasts: memoryState.toasts.filter((t) => t.id !== toastId),
})
}
export function useToast() {
const [state, setState] = useState<State>(memoryState)
useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss,
}
}

View File

@ -1,54 +1,6 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { LlmProvidersConfig, DataSourcesConfig, AnalysisTemplateSets, DataSourceProvider } from '../types/config';
// MOCK DATA
const MOCK_PROVIDERS: LlmProvidersConfig = {
"openai_official": {
name: "OpenAI Official",
api_base_url: "https://api.openai.com/v1",
api_key: "sk-****************",
models: [
{ model_id: "gpt-4o", name: "GPT-4o", is_active: true },
{ model_id: "gpt-3.5-turbo", name: "GPT-3.5 Turbo", is_active: true }
]
},
"local_ollama": {
name: "Local Ollama",
api_base_url: "http://localhost:11434/v1",
api_key: "ollama",
models: [
{ model_id: "llama3", name: "Llama 3", is_active: true }
]
}
};
const MOCK_DATA_SOURCES: DataSourcesConfig = {
"tushare_main": {
provider: DataSourceProvider.Tushare,
api_key: "89******************************",
enabled: true
},
"finnhub_backup": {
provider: DataSourceProvider.Finnhub,
api_key: "c1****************",
enabled: false
}
};
const MOCK_TEMPLATES: AnalysisTemplateSets = {
"quick_scan": {
name: "快速扫描 (Quick Scan)",
modules: {
"summary": {
name: "概览",
provider_id: "openai_official",
model_id: "gpt-4o",
prompt_template: "Analyze this...",
dependencies: []
}
}
}
};
import { LlmProvidersConfig, DataSourcesConfig, AnalysisTemplateSets, TestConfigRequest, TestLlmConfigRequest } from '../types/config';
import { client } from '../api/client';
// --- Hooks ---
@ -56,11 +8,19 @@ export function useLlmProviders() {
return useQuery({
queryKey: ['llm-providers'],
queryFn: async () => {
// TODO: Replace with actual API call
// const { data } = await apiClient.get<LlmProvidersConfig>('/configs/llm_providers');
// return data;
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate delay
return MOCK_PROVIDERS;
return await client.get_llm_providers_config();
}
});
}
export function useUpdateLlmProviders() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (config: LlmProvidersConfig) => {
return await client.update_llm_providers_config(config);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['llm-providers'] });
}
});
}
@ -69,8 +29,28 @@ export function useDataSources() {
return useQuery({
queryKey: ['data-sources'],
queryFn: async () => {
await new Promise(resolve => setTimeout(resolve, 500));
return MOCK_DATA_SOURCES;
return await client.get_data_sources_config();
}
});
}
export function useUpdateDataSources() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (config: DataSourcesConfig) => {
return await client.update_data_sources_config(config);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['data-sources'] });
}
});
}
export function useRegisteredProviders() {
return useQuery({
queryKey: ['registered-providers'],
queryFn: async () => {
return await client.get_registered_providers();
}
});
}
@ -79,9 +59,47 @@ export function useAnalysisTemplates() {
return useQuery({
queryKey: ['analysis-templates'],
queryFn: async () => {
await new Promise(resolve => setTimeout(resolve, 500));
return MOCK_TEMPLATES;
return await client.get_analysis_template_sets();
}
});
}
export function useUpdateAnalysisTemplates() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (config: AnalysisTemplateSets) => {
return await client.update_analysis_template_sets(config);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['analysis-templates'] });
}
});
}
export function useDiscoverModels() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (providerId: string) => {
return await client.discover_models({ params: { provider_id: providerId } });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['llm-providers'] });
}
});
}
export function useTestDataSource() {
return useMutation({
mutationFn: async (config: TestConfigRequest) => {
return await client.test_data_source_config(config);
}
});
}
export function useTestLlmConfig() {
return useMutation({
mutationFn: async (config: TestLlmConfigRequest) => {
return await client.test_llm_config(config);
}
});
}

View File

@ -1,5 +1,6 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useMutation } from '@tanstack/react-query';
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
@ -7,9 +8,13 @@ import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { BarChart3, Search, Sparkles, Loader2 } from "lucide-react"
import { useAnalysisTemplates } from "@/hooks/useConfig"
import { client } from '@/api/client';
import { type DataRequest as DataRequestDTO } from '@/api/schema.gen';
import { useToast } from "@/hooks/use-toast"
export function Dashboard() {
const navigate = useNavigate();
const { toast } = useToast();
const [symbol, setSymbol] = useState("");
const [market, setMarket] = useState("CN");
const [templateId, setTemplateId] = useState("");
@ -23,12 +28,30 @@ export function Dashboard() {
}
}, [templates, templateId]);
const startWorkflowMutation = useMutation({
mutationFn: async (payload: DataRequestDTO) => {
return await client.start_workflow(payload);
},
onSuccess: (data) => {
navigate(`/report/${data.request_id}?symbol=${data.symbol}&market=${data.market}&templateId=${templateId}`);
},
onError: (error) => {
toast({
title: "Start Failed",
description: "Failed to start analysis workflow. Please try again.",
type: "error"
});
console.error("Workflow start error:", error);
}
});
const handleStart = () => {
if (!symbol || !templateId) return;
// In real app, we would POST to create a workflow, get ID, then navigate.
// Here we just mock an ID.
const mockRequestId = "req-" + Math.random().toString(36).substr(2, 9);
navigate(`/report/${mockRequestId}?symbol=${symbol}&market=${market}&templateId=${templateId}`);
startWorkflowMutation.mutate({
symbol,
market,
template_id: templateId
});
};
return (
@ -65,6 +88,11 @@ export function Dashboard() {
className="pl-9"
value={symbol}
onChange={(e) => setSymbol(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && symbol && templateId) {
handleStart();
}
}}
/>
</div>
</div>
@ -90,14 +118,16 @@ export function Dashboard() {
<SelectValue placeholder={isTemplatesLoading ? "Loading templates..." : "Select a template"} />
</SelectTrigger>
<SelectContent>
{templates ? (
{templates && Object.keys(templates).length > 0 ? (
Object.entries(templates).map(([id, t]) => (
<SelectItem key={id} value={id}>
{t.name}
</SelectItem>
))
) : (
<SelectItem value="loading" disabled>Loading...</SelectItem>
<SelectItem value="loading" disabled>
{isTemplatesLoading ? "Loading..." : "No templates found"}
</SelectItem>
)}
</SelectContent>
</Select>
@ -105,9 +135,17 @@ export function Dashboard() {
</CardContent>
<CardFooter>
<Button size="lg" className="w-full text-base" onClick={handleStart} disabled={!symbol || !templateId || isTemplatesLoading}>
{isTemplatesLoading ? <Loader2 className="mr-2 h-5 w-5 animate-spin" /> : <BarChart3 className="mr-2 h-5 w-5" />}
<Button
size="lg"
className="w-full text-base"
onClick={handleStart}
disabled={!symbol || !templateId || isTemplatesLoading || startWorkflowMutation.isPending}
>
{startWorkflowMutation.isPending || isTemplatesLoading ?
<Loader2 className="mr-2 h-5 w-5 animate-spin" /> :
<BarChart3 className="mr-2 h-5 w-5" />
}
{startWorkflowMutation.isPending ? "启动中..." : "生成分析报告"}
</Button>
</CardFooter>
</Card>

View File

@ -1,14 +1,13 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "@/components/ui/separator"
import { WorkflowVisualizer } from '@/components/workflow/WorkflowVisualizer';
import { useWorkflowStore } from '@/stores/useWorkflowStore';
import { WorkflowDag, TaskStatus } from '@/types/workflow';
import { Terminal, Play, Loader2, Sparkles, Table, CheckCircle2 } from 'lucide-react';
import { TaskStatus, schemas } from '@/api/schema.gen';
import { Terminal, Loader2, Sparkles, CheckCircle2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
@ -24,170 +23,51 @@ export function ReportPage() {
const {
initialize,
setDag,
updateTaskStatus,
updateTaskContent,
appendTaskLog,
setActiveTab,
handleEvent,
status,
tasks,
dag,
activeTab
activeTab,
setActiveTab
} = useWorkflowStore();
const { data: templates } = useAnalysisTemplates();
const templateName = templates && templateId ? templates[templateId]?.name : templateId;
// SSE Connection Logic (Mocked for now)
// SSE Connection Logic
useEffect(() => {
if (!id) return;
initialize(id);
// Set default active tab to overview or first task
setActiveTab('overview');
// Connect to real backend SSE
const eventSource = new EventSource(`/api/v1/workflow/events/${id}`);
// --- MOCK SIMULATION START ---
// In real app, this would be:
// const eventSource = new EventSource(`/api/v1/workflow/events/${id}`);
let timeouts: NodeJS.Timeout[] = [];
const mockDag: WorkflowDag = {
nodes: [
{ id: 'fetch:tushare', type: 'DataFetch', label: 'Fetch Tushare', dependencies: [], initial_status: 'Pending' },
{ id: 'fetch:finnhub', type: 'DataFetch', label: 'Fetch Finnhub', dependencies: [], initial_status: 'Pending' },
{ id: 'process:financials', type: 'DataProcessing', label: 'Clean Financials', dependencies: ['fetch:tushare', 'fetch:finnhub'], initial_status: 'Pending' },
{ id: 'analysis:basic', type: 'Analysis', label: 'Basic Analysis', dependencies: ['process:financials'], initial_status: 'Pending' },
{ id: 'analysis:risk', type: 'Analysis', label: 'Risk Assess', dependencies: ['process:financials'], initial_status: 'Pending' },
{ id: 'analysis:summary', type: 'Analysis', label: 'Final Summary', dependencies: ['analysis:basic', 'analysis:risk'], initial_status: 'Pending' },
]
eventSource.onmessage = (event) => {
try {
const parsedEvent = JSON.parse(event.data);
handleEvent(parsedEvent);
} catch (e) {
console.error("Failed to parse SSE event:", e);
}
};
// 1. Start
timeouts.push(setTimeout(() => {
setDag(mockDag);
}, 1000));
// 2. Run Fetch
timeouts.push(setTimeout(() => {
updateTaskStatus('fetch:tushare', 'Running', 'Connecting to Tushare API...');
updateTaskStatus('fetch:finnhub', 'Running', 'Connecting to Finnhub...');
}, 2000));
timeouts.push(setTimeout(() => {
updateTaskStatus('fetch:tushare', 'Completed', 'Fetched 3 years of income statements.');
updateTaskStatus('fetch:finnhub', 'Completed', 'Fetched price history.');
}, 4000));
// 3. Process
timeouts.push(setTimeout(() => {
updateTaskStatus('process:financials', 'Running', 'Normalizing currency...');
}, 4500));
timeouts.push(setTimeout(() => {
updateTaskStatus('process:financials', 'Completed', 'Data aligned to fiscal years.');
}, 5500));
// 4. Parallel Analysis
timeouts.push(setTimeout(() => {
updateTaskStatus('analysis:basic', 'Running', 'Generating basic metrics...');
updateTaskStatus('analysis:risk', 'Running', 'Evaluating volatility...');
}, 6000));
// Stream logs
timeouts.push(setTimeout(() => appendTaskLog('analysis:basic', 'Calculated ROE: 15%'), 7000));
timeouts.push(setTimeout(() => appendTaskLog('analysis:basic', 'Calculated Net Margin: 22%'), 7500));
timeouts.push(setTimeout(() => appendTaskLog('analysis:risk', 'Beta: 0.85'), 7200));
// Stream Content for Basic Analysis
const basicReport = `### Basic Analysis
**Profitability**:
- ROE: **15%**
- Net Margin: **22%**
The company shows strong operational efficiency.`;
streamMockContent(basicReport, 'analysis:basic', 6500, timeouts, updateTaskContent);
// Stream Content for Risk
const riskReport = `### Risk Assessment
**Volatility**:
- Beta: **0.85** (Low Risk)
- Debt/Equity: **0.4** (Healthy)
No significant red flags detected in the balance sheet.`;
streamMockContent(riskReport, 'analysis:risk', 6500, timeouts, updateTaskContent);
timeouts.push(setTimeout(() => {
updateTaskStatus('analysis:basic', 'Completed');
updateTaskStatus('analysis:risk', 'Completed');
}, 8500));
// 5. Final (With Streaming Content)
timeouts.push(setTimeout(() => {
updateTaskStatus('analysis:summary', 'Running', 'Synthesizing report...');
// Auto switch to summary tab when it starts
setActiveTab('analysis:summary');
}, 9000));
// Simulate Typing Effect for Markdown
const fullReport = `
# ${symbol} Financial Analysis Report
## 1. Executive Summary
Based on the financial data from **Tushare** and **Finnhub**, ${symbol} demonstrates strong fundamental performance.
* **Profitability**: ROE is at **15%**, exceeding industry average.
* **Risk**: Beta of **0.85** indicates lower volatility than the market.
## 2. Financial Health
The company has maintained a solid balance sheet.
| Metric | 2023 | 2024 |
| :--- | :--- | :--- |
| Revenue | $10B | $12B |
| Net Income | $1.5B | $2.0B |
> "The company's growth trajectory remains positive despite macroeconomic headwinds."
## 3. Conclusion
**Buy** rating is recommended for long-term investors.
`;
streamMockContent(fullReport, 'analysis:summary', 9100, timeouts, updateTaskContent);
timeouts.push(setTimeout(() => {
updateTaskStatus('analysis:summary', 'Completed', 'Report generated.');
}, 13000));
return () => timeouts.forEach(clearTimeout);
// --- MOCK SIMULATION END ---
}, [id]);
// Helper to stream mock content char by char
const streamMockContent = (text: string, taskId: string, startDelay: number, timeouts: NodeJS.Timeout[], updateFn: (id: string, delta: string) => void) => {
const chunks = text.split("");
let delay = startDelay;
chunks.forEach((char) => {
delay += 20; // Typing speed
timeouts.push(setTimeout(() => {
updateFn(taskId, char);
}, delay));
});
eventSource.onerror = (err) => {
console.error("SSE Connection Error:", err);
// Optional: Retry logic or error state update
// eventSource.close();
};
return () => {
eventSource.close();
};
}, [id, initialize, handleEvent]);
// Combine logs from all tasks for the "Global Log" view
const allLogs = Object.entries(tasks).flatMap(([taskId, state]) =>
state.logs.map(log => ({ taskId, log }))
);
// Filter nodes that should have their own tabs (e.g. Analysis & Processing)
// We keep DataFetch nodes out of tabs to reduce clutter, or maybe group them in "Overview"?
// For now, let's show tabs for all Analysis nodes + Overview + Data
const tabNodes = dag?.nodes.filter(n => n.type === 'Analysis') || [];
const tabNodes = dag?.nodes.filter(n => n.type === schemas.TaskType.enum.Analysis) || [];
return (
<div className="container py-6 space-y-6 h-[calc(100vh-4rem)] flex flex-col">
@ -222,16 +102,16 @@ The company has maintained a solid balance sheet.
</CardContent>
</Card>
<Card className="flex-1 flex flex-col min-h-0">
<CardHeader className="py-3 px-4 border-b">
<Card className="flex-1 flex flex-col min-h-0 p-0 gap-0 overflow-hidden">
<CardHeader className="py-2 px-4 border-b bg-muted/50 space-y-0 shrink-0">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Terminal className="h-4 w-4" />
Real-time Logs
</CardTitle>
</CardHeader>
<CardContent className="flex-1 p-0 min-h-0">
<ScrollArea className="h-full p-4">
<div className="space-y-1.5 font-mono text-xs">
<CardContent className="flex-1 min-h-0 p-0 relative">
<div className="absolute inset-0 overflow-auto p-4 font-mono text-xs bg-background">
<div className="space-y-1.5">
{allLogs.length === 0 && <span className="text-muted-foreground italic">Waiting for logs...</span>}
{allLogs.map((entry, i) => (
<div key={i} className="break-all">
@ -240,7 +120,7 @@ The company has maintained a solid balance sheet.
</div>
))}
</div>
</ScrollArea>
</div>
</CardContent>
</Card>
</div>
@ -262,8 +142,8 @@ The company has maintained a solid balance sheet.
value={node.id}
className="rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent px-4 py-3 gap-2"
>
{node.label}
<TaskStatusIndicator status={tasks[node.id]?.status || 'Pending'} />
{node.name}
<TaskStatusIndicator status={tasks[node.id]?.status || schemas.TaskStatus.enum.Pending} />
</TabsTrigger>
))}
</TabsList>
@ -298,11 +178,11 @@ The company has maintained a solid balance sheet.
</ReactMarkdown>
) : (
<div className="flex flex-col items-center justify-center h-[300px] text-muted-foreground space-y-4">
{tasks[node.id]?.status === 'Pending' && <p>Waiting to start...</p>}
{tasks[node.id]?.status === 'Running' && !tasks[node.id]?.content && <Loader2 className="h-8 w-8 animate-spin" />}
{tasks[node.id]?.status === schemas.TaskStatus.enum.Pending && <p>Waiting to start...</p>}
{tasks[node.id]?.status === schemas.TaskStatus.enum.Running && !tasks[node.id]?.content && <Loader2 className="h-8 w-8 animate-spin" />}
</div>
)}
{tasks[node.id]?.status === 'Running' && (
{tasks[node.id]?.status === schemas.TaskStatus.enum.Running && (
<span className="inline-block w-2 h-4 ml-1 bg-primary animate-pulse"/>
)}
</div>
@ -336,9 +216,9 @@ function WorkflowStatusBadge({ status }: { status: string }) {
function TaskStatusIndicator({ status }: { status: TaskStatus }) {
switch (status) {
case 'Running': return <Loader2 className="h-3 w-3 animate-spin text-blue-500" />;
case 'Completed': return <CheckCircle2 className="h-3 w-3 text-green-500" />;
case 'Failed': return <div className="h-2 w-2 rounded-full bg-red-500" />;
case schemas.TaskStatus.enum.Running: return <Loader2 className="h-3 w-3 animate-spin text-blue-500" />;
case schemas.TaskStatus.enum.Completed: return <CheckCircle2 className="h-3 w-3 text-green-500" />;
case schemas.TaskStatus.enum.Failed: return <div className="h-2 w-2 rounded-full bg-red-500" />;
default: return null;
}
}

View File

@ -1,22 +1,84 @@
import { Plus, Trash2, RefreshCw, Eye, EyeOff } from "lucide-react"
import { useState } from "react"
import { Plus, Trash2, RefreshCw, Eye, EyeOff, Save, X, Search } from "lucide-react"
import { useState, useRef, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { useLlmProviders } from "@/hooks/useConfig"
import { LlmProvider } from "@/types/config"
import { useLlmProviders, useUpdateLlmProviders, useDiscoverModels } from "@/hooks/useConfig"
import { LlmProvider, LlmModel } from "@/types/config"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { useToast } from "@/hooks/use-toast"
import axios from "axios"
export function AIProviderTab() {
const { data: providers, isLoading } = useLlmProviders();
const updateProviders = useUpdateLlmProviders();
const { toast } = useToast();
const [isAddOpen, setIsAddOpen] = useState(false);
const [formData, setFormData] = useState({ name: '', url: '', key: '' });
const handleSave = () => {
if (!formData.name || !formData.url || !formData.key) {
toast({ title: "Validation Error", description: "All fields are required", type: "error" });
return;
}
const id = formData.name.toLowerCase().replace(/\s+/g, '_');
if (providers && providers[id]) {
toast({ title: "Validation Error", description: "Provider with this name already exists", type: "error" });
return;
}
const newProvider: LlmProvider = {
name: formData.name,
api_base_url: formData.url,
api_key: formData.key,
models: []
};
const newProviders = { ...providers, [id]: newProvider };
updateProviders.mutate(newProviders, {
onSuccess: () => {
toast({ title: "Success", description: "Provider added successfully" });
setIsAddOpen(false);
setFormData({ name: '', url: '', key: '' });
},
onError: (err) => {
toast({ title: "Error", description: "Failed to add provider: " + err, type: "error" });
}
});
}
const handleDelete = (id: string) => {
if (!providers) return;
const { [id]: removed, ...rest } = providers;
updateProviders.mutate(rest, {
onSuccess: () => toast({ title: "Success", description: "Provider removed" }),
onError: () => toast({ title: "Error", description: "Failed to remove provider", type: "error" })
});
}
const handleUpdateProvider = (id: string, updatedProvider: LlmProvider) => {
if (!providers) return;
const newProviders = { ...providers, [id]: updatedProvider };
updateProviders.mutate(newProviders, {
onSuccess: () => toast({ title: "Success", description: "Provider updated" }),
onError: (err) => toast({ title: "Error", description: "Failed to update provider", type: "error" })
});
}
if (isLoading) {
return <div>Loading providers...</div>;
}
// Handle empty state explicitly if data is loaded but empty
if (providers && Object.keys(providers).length === 0) {
// Fallthrough to render normally, showing empty list
}
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
@ -38,19 +100,39 @@ export function AIProviderTab() {
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right"></Label>
<Input id="name" placeholder="e.g. OpenAI" className="col-span-3" />
<Input
id="name"
placeholder="e.g. OpenAI"
className="col-span-3"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="url" className="text-right">Base URL</Label>
<Input id="url" placeholder="https://api.openai.com/v1" className="col-span-3" />
<Input
id="url"
placeholder="https://api.openai.com/v1"
className="col-span-3"
value={formData.url}
onChange={(e) => setFormData({...formData, url: e.target.value})}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="key" className="text-right">API Key</Label>
<Input id="key" type="password" className="col-span-3" />
<Input
id="key"
type="password"
className="col-span-3"
value={formData.key}
onChange={(e) => setFormData({...formData, key: e.target.value})}
/>
</div>
</div>
<DialogFooter>
<Button type="submit" onClick={() => setIsAddOpen(false)}></Button>
<Button type="submit" onClick={handleSave} disabled={updateProviders.isPending}>
{updateProviders.isPending ? "保存中..." : "保存"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@ -58,15 +140,137 @@ export function AIProviderTab() {
<div className="grid gap-6 md:grid-cols-1 lg:grid-cols-2">
{providers && Object.entries(providers).map(([id, provider]) => (
<ProviderCard key={id} id={id} provider={provider} />
<ProviderCard
key={id}
id={id}
provider={provider}
onDelete={() => handleDelete(id)}
onUpdate={(p) => handleUpdateProvider(id, p)}
/>
))}
</div>
</div>
)
}
function ProviderCard({ id, provider }: { id: string, provider: LlmProvider }) {
function ProviderCard({ id, provider, onDelete, onUpdate }: { id: string, provider: LlmProvider, onDelete: () => void, onUpdate: (p: LlmProvider) => void }) {
const [showKey, setShowKey] = useState(false);
const discoverModels = useDiscoverModels();
const { toast } = useToast();
// Discovered models cache for search (not saved to config)
const [discoveredModels, setDiscoveredModels] = useState<LlmModel[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [isSearchFocused, setIsSearchFocused] = useState(false);
const searchRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Close search results on click outside
const handleClickOutside = (event: MouseEvent) => {
if (searchRef.current && !searchRef.current.contains(event.target as Node)) {
setIsSearchFocused(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleRefresh = async () => {
try {
const response = await axios.get(`/api/v1/discover-models/${id}`);
const data = response.data;
let models: LlmModel[] = [];
if (data && Array.isArray(data.data)) {
models = data.data.map((m: any) => ({
model_id: m.id,
name: m.name || m.id,
is_active: true
}));
} else if (Array.isArray(data)) {
models = data.map((m: any) => ({
model_id: m.id,
name: m.name || m.id,
is_active: true
}));
}
setDiscoveredModels(models);
if (models.length === 0) {
toast({ title: "Info", description: "No models found in response" });
} else if (models.length < 10) {
// If few models, add them all automatically
const updatedProvider = { ...provider, models };
onUpdate(updatedProvider);
toast({ title: "Success", description: `Found and added ${models.length} models` });
} else {
// If many models, just notify user to search/add
toast({
title: "Found many models",
description: `Discovered ${models.length} models. Please search and add specific models below.`
});
}
} catch (err) {
console.error(err);
toast({ title: "Error", description: "Failed to refresh models", type: "error" });
}
}
const handleAddModel = (model: LlmModel) => {
// Check if already exists
if (provider.models.some(m => m.model_id === model.model_id)) {
toast({ title: "Info", description: "Model already added" });
setSearchQuery("");
setIsSearchFocused(false);
return;
}
const updatedProvider = {
...provider,
models: [...provider.models, model]
};
onUpdate(updatedProvider);
setSearchQuery("");
setIsSearchFocused(false);
toast({ title: "Success", description: `Added ${model.name || model.model_id}` });
}
const handleManualAdd = () => {
if (!searchQuery.trim()) return;
const newModel = {
model_id: searchQuery,
name: searchQuery,
is_active: true
};
handleAddModel(newModel);
}
const handleRemoveModel = (modelId: string) => {
const updatedProvider = {
...provider,
models: provider.models.filter(m => m.model_id !== modelId)
};
onUpdate(updatedProvider);
}
const handleClearModels = () => {
if (provider.models.length === 0) return;
if (confirm("确定要清空所有已添加的模型吗?")) {
const updatedProvider = {
...provider,
models: []
};
onUpdate(updatedProvider);
toast({ title: "Success", description: "已清空所有模型" });
}
}
const filteredModels = searchQuery
? discoveredModels.filter(m =>
(m.name?.toLowerCase().includes(searchQuery.toLowerCase()) || m.model_id.toLowerCase().includes(searchQuery.toLowerCase()))
)
: [];
return (
<Card>
@ -103,21 +307,102 @@ function ProviderCard({ id, provider }: { id: string, provider: LlmProvider }) {
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="text-xs text-muted-foreground">ACTIVE MODELS</Label>
<Button variant="ghost" size="sm" className="h-6 text-xs px-2">
<RefreshCw className="mr-1 h-3 w-3" />
<div className="flex gap-1">
{provider.models.length > 0 && (
<Button
variant="ghost"
size="sm"
className="h-6 text-xs px-2 text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={handleClearModels}
>
<Trash2 className="mr-1 h-3 w-3" />
</Button>
)}
<Button
variant="ghost"
size="sm"
className="h-6 text-xs px-2"
onClick={handleRefresh}
disabled={discoverModels.isPending}
>
<RefreshCw className={`mr-1 h-3 w-3 ${discoverModels.isPending ? 'animate-spin' : ''}`} />
{discoverModels.isPending ? "刷新中..." : "刷新列表"}
</Button>
</div>
<div className="flex flex-wrap gap-2">
</div>
{/* Search / Add Input */}
<div className="relative" ref={searchRef}>
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search discovered models or type new ID..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onFocus={() => setIsSearchFocused(true)}
className="pl-8 h-9 text-sm"
/>
{searchQuery && (
<Button
size="sm"
variant="ghost"
className="absolute right-1 top-1 h-7 px-2 text-xs"
onClick={handleManualAdd}
>
<Plus className="h-3 w-3 mr-1" /> Add "{searchQuery}"
</Button>
)}
</div>
{/* Autocomplete Dropdown */}
{isSearchFocused && searchQuery && discoveredModels.length > 0 && (
<div className="absolute z-10 w-full mt-1 bg-popover border rounded-md shadow-md max-h-[200px] overflow-y-auto">
{filteredModels.length > 0 ? (
filteredModels.map((model) => (
<button
key={model.model_id}
className="w-full text-left px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground flex items-center justify-between"
onClick={() => handleAddModel(model)}
>
<span className="truncate">{model.name || model.model_id}</span>
{provider.models.some(m => m.model_id === model.model_id) && (
<Badge variant="outline" className="text-[10px]">Added</Badge>
)}
</button>
))
) : (
<div className="px-3 py-2 text-xs text-muted-foreground">
No discovered models match "{searchQuery}". Click 'Add' to add manually.
</div>
)}
</div>
)}
</div>
{/* Active Models List */}
<div className="flex flex-wrap gap-2 max-h-[150px] overflow-y-auto p-1 border rounded-md bg-muted/10 mt-2">
{provider.models.length === 0 && <span className="text-xs text-muted-foreground p-2">No active models.</span>}
{provider.models.map(model => (
<Badge key={model.model_id} variant="secondary" className="text-xs font-normal">
<Badge key={model.model_id} variant="secondary" className="text-xs font-normal flex items-center gap-1 pr-1">
{model.name || model.model_id}
<button
onClick={() => handleRemoveModel(model.model_id)}
className="ml-1 hover:text-destructive focus:outline-none"
>
<X className="h-3 w-3" />
</button>
</Badge>
))}
</div>
</div>
</CardContent>
<CardFooter className="justify-end pt-2">
<Button variant="ghost" size="sm" className="text-destructive hover:text-destructive hover:bg-destructive/10">
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive hover:bg-destructive/10"
onClick={onDelete}
>
<Trash2 className="mr-2 h-4 w-4" />
</Button>
</CardFooter>

View File

@ -1,92 +1,131 @@
import { Check, X, Save, AlertCircle } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { useDataSources } from "@/hooks/useConfig"
import { DataSourceConfig, DataSourceProvider } from "@/types/config"
import { Badge } from "@/components/ui/badge"
import { useDataSources, useUpdateDataSources, useTestDataSource, useRegisteredProviders } from "@/hooks/useConfig"
import { DataSourceConfig } from "@/types/config"
import { useToast } from "@/hooks/use-toast"
import { DynamicConfigForm } from "@/components/config/DynamicConfigForm"
export function DataSourceTab() {
const { data: dataSources, isLoading } = useDataSources();
const { data: dataSources, isLoading: isConfigLoading } = useDataSources();
const { data: providersMetadata, isLoading: isMetadataLoading } = useRegisteredProviders();
if (isLoading) {
const updateDataSources = useUpdateDataSources();
const testDataSource = useTestDataSource();
const { toast } = useToast();
if (isConfigLoading || isMetadataLoading) {
return <div>Loading data sources...</div>;
}
// We want to show all supported providers, even if not yet in config
const supportedProviders = [
{ type: DataSourceProvider.Tushare, name: "Tushare Pro", desc: "中国股市数据 (A股/港股)" },
{ type: DataSourceProvider.Finnhub, name: "Finnhub", desc: "全球美股与外汇数据" },
{ type: DataSourceProvider.Alphavantage, name: "Alpha Vantage", desc: "技术指标与外汇" },
{ type: DataSourceProvider.Yfinance, name: "Yahoo Finance", desc: "基础免费数据源 (非官方)" },
];
if (!providersMetadata || providersMetadata.length === 0) {
return (
<div className="p-4 text-center text-muted-foreground">
No data providers registered. Please ensure provider services are running.
</div>
);
}
const handleSave = (providerId: string, config: DataSourceConfig) => {
// We need to reconstruct the full DataSourcesConfig map
// Note: dataSources is a HashMap<String, DataSourceConfig>
const newDataSources = { ...dataSources, [providerId]: config };
updateDataSources.mutate(newDataSources, {
onSuccess: () => toast({ title: "Success", description: `${providerId} configuration saved` }),
onError: (err) => toast({ title: "Error", description: "Failed to save: " + err, type: "error" })
});
};
const handleTest = (providerId: string, config: DataSourceConfig) => {
// Construct payload for generic test endpoint
const payload = {
type: providerId,
data: config // Send the whole config object as data
};
// We cast to any because TestConfigRequest type definition in frontend might be loose or generic
testDataSource.mutate(payload as any, {
onSuccess: (data: any) => {
console.log("Test Connection Success Payload:", data);
// Check both explicit false and explicit "false" string, or any other falsy indicator
if (data.success === false || data.success === "false") {
console.warn("Test reported success but payload indicates failure:", data);
toast({
title: "Test Failed",
description: data.message || "Connection test failed",
type: "error"
});
} else {
console.log("Test confirmed successful.");
toast({
title: "Success",
description: data.message || "Connection test successful"
});
}
},
onError: (err: any) => {
console.error("Test Connection Error:", err);
let errorMessage = "Connection test failed";
if (err.response?.data) {
const data = err.response.data;
console.log("Error Response Data:", data);
// Try to parse 'details' if it exists and is a string (common-contracts error format)
if (typeof data.details === 'string') {
try {
const parsed = JSON.parse(data.details);
if (parsed.message) errorMessage = parsed.message;
else errorMessage = data.details;
} catch (e) {
errorMessage = data.details;
}
} else if (data.message) {
errorMessage = data.message;
} else if (data.error) {
errorMessage = data.error;
}
}
toast({
title: "Test Failed",
description: errorMessage,
type: "error"
});
}
});
};
return (
<div className="grid gap-6 md:grid-cols-1 lg:grid-cols-2">
{supportedProviders.map(p => {
{providersMetadata.map(meta => {
// Find existing config or create default
// In reality, the key in the map might not match the enum exactly (e.g. "tushare_main" vs "Tushare")
// But for this mock, let's assume we search by provider type
const configEntry = dataSources ? Object.values(dataSources).find(d => d.provider === p.type) : undefined;
const configEntry = dataSources ? (dataSources as Record<string, any>)[meta.id] : undefined;
const config = configEntry || {
provider: p.type,
// Default config structure.
// Note: We default 'provider' field to the ID from metadata.
// Backend expects specific enum values for 'provider', but currently our IDs match (lowercase/uppercase handling needed?)
// The backend DataSourceProvider enum is PascalCase (Tushare), but IDs are likely lowercase (tushare).
// However, DataSourceConfig.provider is an enum.
// We might need to map ID to Enum if strict.
// For now, assuming the backend persistence can handle the string or we just store it.
// Actually, the 'provider' field in DataSourceConfig is DataSourceProvider enum.
// Let's hope the JSON deserialization handles "tushare" -> Tushare.
const config = (configEntry || {
provider: meta.id, // This might need capitalization adjustment
enabled: false,
api_key: "",
api_url: ""
} as DataSourceConfig;
// We init other fields as empty, they will be filled by DynamicConfigForm
}) as DataSourceConfig;
return (
<DataSourceCard key={p.type} meta={p} config={config} />
<DynamicConfigForm
key={meta.id}
metadata={meta}
initialConfig={config}
onSave={(c) => handleSave(meta.id, c)}
onTest={(c) => handleTest(meta.id, c)}
isSaving={updateDataSources.isPending}
isTesting={testDataSource.isPending}
/>
)
})}
</div>
)
}
function DataSourceCard({ meta, config }: { meta: { type: DataSourceProvider, name: string, desc: string }, config: DataSourceConfig }) {
const isEnabled = config.enabled;
return (
<Card className={isEnabled ? "border-primary/50 bg-accent/5" : "opacity-80"}>
<CardHeader className="flex flex-row items-start justify-between pb-2">
<div className="space-y-1">
<CardTitle className="text-xl flex items-center gap-2">
{meta.name}
{isEnabled ?
<Badge variant="default" className="bg-green-600 hover:bg-green-600 h-5 text-[10px]">Active</Badge> :
<Badge variant="outline" className="text-muted-foreground h-5 text-[10px]">Inactive</Badge>
}
</CardTitle>
<CardDescription>{meta.desc}</CardDescription>
</div>
<Switch checked={isEnabled} />
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>API Token / Key</Label>
<Input
type="password"
value={config.api_key || ""}
placeholder={isEnabled ? "configured" : "Enter API Key"}
disabled={!isEnabled} // Just for visual logic
/>
</div>
{meta.type === DataSourceProvider.Tushare && (
<div className="space-y-2">
<Label>API Endpoint</Label>
<Input value={config.api_url || "http://api.tushare.pro"} disabled={!isEnabled} />
</div>
)}
</CardContent>
<CardFooter className="flex justify-between pt-0">
<Button variant="outline" size="sm" disabled={!isEnabled}>
<AlertCircle className="mr-2 h-4 w-4" />
</Button>
<Button size="sm" disabled></Button>
</CardFooter>
</Card>
)
}

View File

@ -1,26 +1,73 @@
import { useState } from "react"
import { useAnalysisTemplates } from "@/hooks/useConfig"
import { AnalysisTemplateSets, AnalysisTemplateSet, AnalysisModuleConfig } from "@/types/config"
import { useState, useEffect } from "react"
import { useAnalysisTemplates, useUpdateAnalysisTemplates, useLlmProviders } from "@/hooks/useConfig"
import { AnalysisTemplateSet, AnalysisModuleConfig } from "@/types/config"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "@/components/ui/separator"
import { Badge } from "@/components/ui/badge"
import { ArrowRight, Box, Layers } from "lucide-react"
import { ArrowRight, Box, Layers, Save, Plus, Pencil, Trash2, ChevronDown, ChevronUp } from "lucide-react"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Label } from "@/components/ui/label"
import { useToast } from "@/hooks/use-toast"
export function TemplateTab() {
const { data: templates, isLoading } = useAnalysisTemplates();
const updateTemplates = useUpdateAnalysisTemplates();
const { toast } = useToast();
const [selectedId, setSelectedId] = useState<string | null>(null);
// Auto select first if none selected
useEffect(() => {
if (templates && !selectedId && Object.keys(templates).length > 0) {
setSelectedId(Object.keys(templates)[0]);
}
}, [templates, selectedId]);
if (isLoading) return <div>Loading templates...</div>;
if (!templates) return <div>No templates found.</div>;
const activeTemplate = selectedId ? templates[selectedId] : null;
const handleCreateTemplate = () => {
if (!templates) return;
const newId = crypto.randomUUID();
const newTemplate: AnalysisTemplateSet = {
name: "New Template",
modules: {}
};
const newTemplates = { ...templates, [newId]: newTemplate };
updateTemplates.mutate(newTemplates, {
onSuccess: () => {
toast({ title: "Success", description: "Template created" });
setSelectedId(newId);
},
onError: () => toast({ title: "Error", description: "Failed to create template", type: "error" })
});
}
const handleUpdateTemplate = (id: string, updatedTemplate: AnalysisTemplateSet) => {
if (!templates) return;
const newTemplates = { ...templates, [id]: updatedTemplate };
updateTemplates.mutate(newTemplates, {
onSuccess: () => toast({ title: "Success", description: "Template saved" }),
onError: () => toast({ title: "Error", description: "Failed to save template", type: "error" })
});
}
const handleDeleteTemplate = (id: string) => {
if (!templates) return;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [id]: removed, ...rest } = templates;
updateTemplates.mutate(rest, {
onSuccess: () => {
toast({ title: "Success", description: "Template deleted" });
if (selectedId === id) setSelectedId(null);
},
onError: () => toast({ title: "Error", description: "Failed to delete template", type: "error" })
});
}
const activeTemplate = (templates && selectedId) ? templates[selectedId] : null;
return (
<div className="flex h-[600px] border rounded-md overflow-hidden">
@ -32,34 +79,47 @@ export function TemplateTab() {
</div>
<ScrollArea className="flex-1">
<div className="p-2 space-y-1">
{Object.entries(templates).map(([id, t]) => (
{templates && Object.entries(templates).map(([id, t]) => (
<div key={id} className="group relative flex items-center">
<button
key={id}
onClick={() => setSelectedId(id)}
className={`w-full text-left px-3 py-2 rounded-md text-sm transition-colors flex items-center justify-between ${
selectedId === id ? "bg-accent text-accent-foreground font-medium" : "hover:bg-muted"
}`}
>
<span>{t.name}</span>
<span className="truncate pr-6">{t.name}</span>
{selectedId === id && <ArrowRight className="h-3 w-3 opacity-50" />}
</button>
{/* Delete button visible on hover */}
<button
onClick={(e) => { e.stopPropagation(); handleDeleteTemplate(id); }}
className="absolute right-2 top-2 hidden group-hover:block text-muted-foreground hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</button>
</div>
))}
</div>
</ScrollArea>
<div className="p-3 border-t bg-background">
<Button size="sm" variant="outline" className="w-full">
+
<Button size="sm" variant="outline" className="w-full" onClick={handleCreateTemplate}>
<Plus className="mr-2 h-4 w-4" />
</Button>
</div>
</div>
{/* Main Content */}
<div className="flex-1 bg-background flex flex-col">
{activeTemplate ? (
<TemplateDetailView template={activeTemplate} />
{activeTemplate && selectedId ? (
<TemplateDetailView
key={selectedId} // Force remount on ID change
template={activeTemplate}
onSave={(t) => handleUpdateTemplate(selectedId, t)}
isSaving={updateTemplates.isPending}
/>
) : (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
Select a template
{templates && Object.keys(templates).length === 0 ? "No templates found. Create one." : "Select a template"}
</div>
)}
</div>
@ -67,20 +127,95 @@ export function TemplateTab() {
)
}
function TemplateDetailView({ template }: { template: AnalysisTemplateSet }) {
function TemplateDetailView({ template, onSave, isSaving }: { template: AnalysisTemplateSet, onSave: (t: AnalysisTemplateSet) => void, isSaving: boolean }) {
const [localTemplate, setLocalTemplate] = useState(template);
const [isDirty, setIsDirty] = useState(false);
const [isRenaming, setIsRenaming] = useState(false);
const [newName, setNewName] = useState(template.name);
useEffect(() => {
setLocalTemplate(template);
setNewName(template.name);
setIsDirty(false);
}, [template]);
const handleRename = () => {
setLocalTemplate(prev => ({ ...prev, name: newName }));
setIsRenaming(false);
setIsDirty(true);
}
const handleAddModule = () => {
const newModuleId = "module_" + Math.random().toString(36).substring(2, 9);
const newModule: AnalysisModuleConfig = {
name: "New Analysis Module",
model_id: "", // Empty to force selection
provider_id: "",
prompt_template: "Analyze the following financial data:\n\n{{data}}\n\nProvide insights on...",
dependencies: []
};
setLocalTemplate(prev => ({
...prev,
modules: {
...prev.modules,
[newModuleId]: newModule
}
}));
setIsDirty(true);
}
const handleDeleteModule = (moduleId: string) => {
setLocalTemplate(prev => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [moduleId]: removed, ...rest } = prev.modules;
return { ...prev, modules: rest };
});
setIsDirty(true);
}
const handleUpdateModule = (moduleId: string, updatedModule: AnalysisModuleConfig) => {
setLocalTemplate(prev => ({
...prev,
modules: {
...prev.modules,
[moduleId]: updatedModule
}
}));
setIsDirty(true);
}
return (
<div className="flex flex-col h-full">
<div className="p-6 border-b">
<div className="flex justify-between items-start">
<div>
<h2 className="text-2xl font-bold tracking-tight">{template.name}</h2>
<div className="flex-1 mr-4">
{isRenaming ? (
<div className="flex items-center gap-2">
<Input
value={newName}
onChange={(e) => setNewName(e.target.value)}
className="h-8 text-lg font-bold"
/>
<Button size="sm" onClick={handleRename}>OK</Button>
<Button size="sm" variant="ghost" onClick={() => setIsRenaming(false)}>Cancel</Button>
</div>
) : (
<div className="flex items-center gap-2 group">
<h2 className="text-2xl font-bold tracking-tight">{localTemplate.name}</h2>
<Button variant="ghost" size="icon" className="h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity" onClick={() => setIsRenaming(true)}>
<Pencil className="h-3 w-3" />
</Button>
</div>
)}
<p className="text-muted-foreground mt-1">
{Object.keys(template.modules).length}
{Object.keys(localTemplate.modules).length}
</p>
</div>
<div className="space-x-2">
<Button variant="outline"></Button>
<Button></Button>
<Button disabled={!isDirty || isSaving} onClick={() => onSave(localTemplate)}>
{isSaving ? <span className="flex items-center"><span className="animate-spin mr-2"></span>Saving...</span> : <span className="flex items-center"><Save className="mr-2 h-4 w-4"/></span>}
</Button>
</div>
</div>
</div>
@ -92,55 +227,178 @@ function TemplateDetailView({ template }: { template: AnalysisTemplateSet }) {
<Layers className="mr-2 h-5 w-5" />
线 (Pipeline)
</h3>
<Button size="sm" variant="secondary">+ </Button>
<Button size="sm" variant="secondary" onClick={handleAddModule}>+ </Button>
</div>
{Object.entries(template.modules).map(([moduleId, module]) => (
<ModuleCard key={moduleId} id={moduleId} module={module} />
))}
{Object.entries(localTemplate.modules).length === 0 ? (
<div className="text-center py-10 text-muted-foreground bg-muted/20 rounded-lg border border-dashed">
No modules configured. Add one to start.
</div>
) : (
Object.entries(localTemplate.modules).map(([moduleId, module]) => (
<ModuleCard
key={moduleId}
id={moduleId}
module={module}
availableModules={Object.keys(localTemplate.modules).filter(k => k !== moduleId)}
allModules={localTemplate.modules}
onDelete={() => handleDeleteModule(moduleId)}
onUpdate={(m) => handleUpdateModule(moduleId, m)}
/>
))
)}
</div>
</ScrollArea>
</div>
)
}
function ModuleCard({ id, module }: { id: string, module: AnalysisModuleConfig }) {
function ModuleCard({ id, module, availableModules, allModules, onDelete, onUpdate }: {
id: string,
module: AnalysisModuleConfig,
availableModules: string[],
allModules: Record<string, AnalysisModuleConfig>,
onDelete: () => void,
onUpdate: (m: AnalysisModuleConfig) => void
}) {
const [isExpanded, setIsExpanded] = useState(false);
const { data: providers } = useLlmProviders();
// Flatten models for select
const allModels: { providerId: string, modelId: string, name: string }[] = [];
const seenKeys = new Set<string>();
if (providers) {
Object.entries(providers).forEach(([pid, p]) => {
p.models.forEach((m: { model_id: string, name?: string | null }) => {
const uniqueKey = `${pid}::${m.model_id}`;
if (!seenKeys.has(uniqueKey)) {
seenKeys.add(uniqueKey);
allModels.push({
providerId: pid,
modelId: m.model_id,
name: `${p.name} - ${m.name || m.model_id}`
});
}
});
});
}
const handleModelChange = (uniqueId: string) => {
const [pid, mid] = uniqueId.split('::');
onUpdate({ ...module, provider_id: pid, model_id: mid });
}
const currentModelUniqueId = module.provider_id && module.model_id ? `${module.provider_id}::${module.model_id}` : undefined;
const toggleDependency = (depId: string) => {
const newDeps = module.dependencies.includes(depId)
? module.dependencies.filter(d => d !== depId)
: [...module.dependencies, depId];
onUpdate({ ...module, dependencies: newDeps });
}
return (
<Card>
<CardHeader className="pb-3">
<Card className={isExpanded ? "border-primary/50" : ""}>
<CardHeader className="pb-3 cursor-pointer hover:bg-muted/5 transition-colors" onClick={() => setIsExpanded(!isExpanded)}>
<div className="flex justify-between items-start">
<div className="flex items-center gap-2">
<div className="bg-primary/10 p-2 rounded-md text-primary">
<Box className="h-4 w-4" />
</div>
<div>
<CardTitle className="text-base">{module.name}</CardTitle>
<CardDescription className="text-xs font-mono mt-0.5 text-muted-foreground">ID: {id}</CardDescription>
</div>
</div>
<Badge variant="outline" className="font-normal">
{module.provider_id} / {module.model_id}
<CardTitle className="text-base flex items-center gap-2">
{module.name}
<span className="text-xs font-normal text-muted-foreground">({id})</span>
</CardTitle>
<CardDescription className="hidden"></CardDescription>
<div className="flex items-center gap-2 mt-1">
<Badge variant="outline" className="font-normal text-[10px] h-5">
{module.provider_id || "?"} / {module.model_id || "?"}
</Badge>
{module.dependencies.length > 0 && (
<span className="text-[10px] text-muted-foreground">
Depends on: {module.dependencies.length}
</span>
)}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="icon" className="h-8 w-8">
{isExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button>
<Button variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-destructive z-10" onClick={(e) => { e.stopPropagation(); onDelete(); }}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent className="text-sm space-y-4">
{module.dependencies.length > 0 ? (
<div className="flex items-center gap-2 text-muted-foreground text-xs">
<span className="font-medium text-foreground">Depends on:</span>
{module.dependencies.map(d => (
<Badge key={d} variant="secondary" className="h-5 px-1.5 text-[10px]">{d}</Badge>
{isExpanded && (
<CardContent className="border-t pt-4 space-y-4 animate-in slide-in-from-top-2 duration-200">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Module Name</Label>
<Input value={module.name} onChange={(e) => onUpdate({...module, name: e.target.value})} />
</div>
<div className="space-y-2">
<Label>Model</Label>
<Select value={currentModelUniqueId} onValueChange={handleModelChange}>
<SelectTrigger>
<SelectValue placeholder="Select Model" />
</SelectTrigger>
<SelectContent>
{allModels.length > 0 ? (
allModels.map(m => (
<SelectItem key={`${m.providerId}::${m.modelId}`} value={`${m.providerId}::${m.modelId}`}>
{m.name}
</SelectItem>
))
) : (
<SelectItem value="no_models" disabled>
No models found
</SelectItem>
)}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label>Prompt Template</Label>
<Textarea
value={module.prompt_template}
onChange={(e) => onUpdate({...module, prompt_template: e.target.value})}
className="font-mono text-xs min-h-[100px]"
/>
<p className="text-[10px] text-muted-foreground">
Use <code>{"{{data}}"}</code> to inject data from dependencies or data source.
</p>
</div>
<div className="space-y-2">
<Label>Dependencies</Label>
<div className="flex flex-wrap gap-2 border p-2 rounded-md min-h-[40px]">
{availableModules.length === 0 ? (
<span className="text-xs text-muted-foreground">No other modules available</span>
) : availableModules.map(mid => (
<Badge
key={mid}
variant={module.dependencies.includes(mid) ? "default" : "outline"}
className="cursor-pointer hover:opacity-80"
onClick={() => toggleDependency(mid)}
>
{allModules[mid]?.name || mid}
</Badge>
))}
</div>
) : (
<div className="text-xs text-muted-foreground italic">No dependencies (Root node)</div>
)}
<div className="bg-muted/30 p-3 rounded-md border">
<p className="text-xs font-mono text-muted-foreground truncate">
Prompt: {module.prompt_template.substring(0, 50)}...
<p className="text-[10px] text-muted-foreground">
Select modules that must complete before this one starts.
</p>
</div>
</CardContent>
)}
</Card>
)
}

View File

@ -1,5 +1,6 @@
import { create } from 'zustand';
import { WorkflowDag, TaskState, TaskStatus } from '../types/workflow';
import { schemas } from '../api/schema.gen';
import { WorkflowDag, TaskState, TaskStatus, WorkflowEvent } from '../types/workflow';
interface WorkflowStoreState {
requestId: string | null;
@ -13,11 +14,13 @@ interface WorkflowStoreState {
initialize: (requestId: string) => void;
setDag: (dag: WorkflowDag) => void;
updateTaskStatus: (taskId: string, status: TaskStatus, message?: string, progress?: number) => void;
updateTaskContent: (taskId: string, delta: string) => void; // Stream content
updateTaskContent: (taskId: string, delta: string) => void; // Stream content (append)
setTaskContent: (taskId: string, content: string) => void; // Set full content
appendTaskLog: (taskId: string, log: string) => void;
setActiveTab: (tabId: string) => void;
completeWorkflow: (result: any) => void;
failWorkflow: (reason: string) => void;
handleEvent: (event: WorkflowEvent) => void;
reset: () => void;
}
@ -53,8 +56,17 @@ export const useWorkflowStore = create<WorkflowStoreState>((set, get) => ({
updateTaskStatus: (taskId, status, message, progress) => {
set(state => {
const task = state.tasks[taskId];
if (!task) return state;
let task = state.tasks[taskId];
// Create task if it doesn't exist (handle orphan events or pre-DAG events)
if (!task) {
task = {
status: schemas.TaskStatus.enum.Pending, // Default initial status
logs: [],
progress: 0,
content: ''
};
}
const newLogs = [...task.logs];
if (message) {
@ -93,10 +105,35 @@ export const useWorkflowStore = create<WorkflowStoreState>((set, get) => ({
});
},
appendTaskLog: (taskId, log) => {
setTaskContent: (taskId, content) => {
set(state => {
const task = state.tasks[taskId];
if (!task) return state;
return {
tasks: {
...state.tasks,
[taskId]: {
...task,
content
}
}
};
});
},
appendTaskLog: (taskId, log) => {
set(state => {
let task = state.tasks[taskId];
if (!task) {
task = {
status: schemas.TaskStatus.enum.Pending,
logs: [],
progress: 0,
content: ''
};
}
return {
tasks: {
...state.tasks,
@ -114,6 +151,66 @@ export const useWorkflowStore = create<WorkflowStoreState>((set, get) => ({
completeWorkflow: (_result) => set({ status: 'COMPLETED' }),
failWorkflow: (reason) => set({ status: 'ERROR', error: reason }),
handleEvent: (event: WorkflowEvent) => {
const state = get();
console.log('Handling Event:', event.type, event);
switch (event.type) {
case 'WorkflowStarted':
state.setDag(event.payload.task_graph);
break;
case 'TaskStateChanged': {
// Explicit typing to help TS
const p = event.payload;
state.updateTaskStatus(
p.task_id,
p.status,
p.message || undefined,
p.progress || undefined
);
break;
}
case 'TaskStreamUpdate': {
const p = event.payload as any;
state.updateTaskContent(p.task_id, p.content_delta);
break;
}
case 'WorkflowCompleted':
state.completeWorkflow(event.payload.result_summary);
break;
case 'WorkflowFailed':
state.failWorkflow(event.payload.reason);
break;
case 'WorkflowStateSnapshot':
// Re-hydrate state
if (event.payload.task_graph) {
state.setDag(event.payload.task_graph);
}
const currentTasks = get().tasks;
const newTasks = { ...currentTasks };
if (event.payload.tasks_status) {
Object.entries(event.payload.tasks_status).forEach(([taskId, status]) => {
if (newTasks[taskId] && status) {
newTasks[taskId] = { ...newTasks[taskId], status: status as TaskStatus };
}
});
}
if (event.payload.tasks_output) {
Object.entries(event.payload.tasks_output).forEach(([taskId, content]) => {
if (newTasks[taskId] && content) {
newTasks[taskId] = { ...newTasks[taskId], content };
}
});
}
set({ tasks: newTasks });
break;
}
},
reset: () => set({
requestId: null,
status: 'IDLE',

View File

@ -1,47 +1,22 @@
export interface LlmModel {
model_id: string;
name?: string;
is_active: boolean;
}
import { schemas } from '../api/schema.gen';
import { z } from 'zod';
export interface LlmProvider {
id?: string; // Optional for new providers
name: string;
api_base_url: string;
api_key: string;
models: LlmModel[];
}
// Re-export simple types directly
export type {
LlmModel,
LlmProvider,
DataSourceConfig,
AnalysisModuleConfig,
AnalysisTemplateSet,
DataSourceProvider,
TestConfigRequest,
TestLlmConfigRequest,
} from '../api/schema.gen';
export type LlmProvidersConfig = Record<string, LlmProvider>;
export enum DataSourceProvider {
Tushare = "Tushare",
Finnhub = "Finnhub",
Alphavantage = "Alphavantage",
Yfinance = "Yfinance",
}
export interface DataSourceConfig {
provider: DataSourceProvider;
api_key?: string;
api_url?: string;
enabled: boolean;
}
export type DataSourcesConfig = Record<string, DataSourceConfig>;
export interface AnalysisModuleConfig {
name: string;
provider_id: string;
model_id: string;
prompt_template: string;
dependencies: string[];
}
export interface AnalysisTemplateSet {
name: string;
modules: Record<string, AnalysisModuleConfig>;
}
export type AnalysisTemplateSets = Record<string, AnalysisTemplateSet>;
// Infer map types from Zod schemas to ensure Record<string, T>
export type LlmProvidersConfig = z.infer<typeof schemas.LlmProvidersConfig>;
export type DataSourcesConfig = z.infer<typeof schemas.DataSourcesConfig>;
export type AnalysisTemplateSets = z.infer<typeof schemas.AnalysisTemplateSets>;
// Runtime Enum for DataSourceProvider
export const DataSourceProviders = schemas.DataSourceProvider.enum;

View File

@ -1,18 +1,16 @@
export type TaskStatus = 'Pending' | 'Scheduled' | 'Running' | 'Completed' | 'Failed' | 'Skipped';
export type TaskType = 'DataFetch' | 'DataProcessing' | 'Analysis';
// Re-export backend types from generated schema
export type {
TaskStatus,
TaskType,
TaskNode,
TaskDependency,
WorkflowDag,
WorkflowEvent,
} from '../api/schema.gen';
export interface TaskNode {
id: string;
type: TaskType;
label?: string;
dependencies: string[];
initial_status: TaskStatus;
}
export interface WorkflowDag {
nodes: TaskNode[];
}
import { TaskStatus } from '../api/schema.gen';
// Frontend-only state wrapper
export interface TaskState {
status: TaskStatus;
message?: string; // Last log message
@ -21,8 +19,3 @@ export interface TaskState {
content?: string; // Streaming content (Markdown)
result?: any; // Structured result
}
export interface WorkflowEvent {
type: 'WorkflowStarted' | 'TaskStateChanged' | 'TaskStreamUpdate' | 'WorkflowCompleted' | 'WorkflowFailed' | 'WorkflowStateSnapshot';
payload: any;
}

View File

@ -1,13 +1,37 @@
import { defineConfig } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import path from "path"
// https://vitejs.dev/config/
export default defineConfig({
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [react()],
optimizeDeps: {
exclude: ['dagre'],
// 'web-worker' needs to be optimized or handled correctly by Vite for elkjs
include: ['elkjs/lib/elk.bundled.js']
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
server: {
proxy: {
'/api': {
target: env.VITE_API_TARGET || 'http://localhost:4000',
changeOrigin: true,
},
'/health': {
target: env.VITE_API_TARGET || 'http://localhost:4000',
changeOrigin: true,
},
'/tasks': {
target: env.VITE_API_TARGET || 'http://localhost:4000',
changeOrigin: true,
},
},
},
}
})

1312
openapi.json Normal file

File diff suppressed because it is too large Load Diff

7
package-lock.json generated
View File

@ -8,6 +8,7 @@
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-popover": "^1.1.15",
"cmdk": "^1.1.1",
"elkjs": "^0.11.0",
"immer": "^10.2.0",
"zustand": "^5.0.8"
}
@ -549,6 +550,12 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
"node_modules/elkjs": {
"version": "0.11.0",
"resolved": "http://npm.repo.lan/elkjs/-/elkjs-0.11.0.tgz",
"integrity": "sha512-u4J8h9mwEDaYMqo0RYJpqNMFDoMK7f+pu4GjcV+N8jIC7TRdORgzkfSjTJemhqONFfH6fBI3wpysgWbhgVWIXw==",
"license": "EPL-2.0"
},
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",

View File

@ -3,6 +3,7 @@
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-popover": "^1.1.15",
"cmdk": "^1.1.1",
"elkjs": "^0.11.0",
"immer": "^10.2.0",
"zustand": "^5.0.8"
}

24
scripts/update_api_spec.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
set -e
# Get the root directory of the project
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
echo "[API Update] Generating OpenAPI JSON from Rust Code..."
# Run the specific test in api-gateway that dumps the JSON
cargo test --manifest-path services/api-gateway/Cargo.toml --bin api-gateway openapi::tests::generate_openapi_json
if [ -f "openapi.json" ]; then
echo "[API Update] openapi.json generated successfully."
else
echo "[API Update] Error: openapi.json was not generated!"
exit 1
fi
echo "[API Update] Regenerating Frontend Types..."
cd frontend
npm run gen:api
echo "[API Update] ✅ API Spec and Frontend Client updated successfully!"

View File

@ -22,19 +22,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "alphavantage-provider-service"
version = "0.1.0"
dependencies = [
"anyhow",
"async-nats",
"async-trait",
"axum",
"chrono",
"common-contracts",
@ -44,7 +37,6 @@ dependencies = [
"futures-util",
"reqwest",
"rmcp",
"secrecy",
"serde",
"serde_json",
"sse-stream",
@ -131,15 +123,6 @@ dependencies = [
"syn 2.0.110",
]
[[package]]
name = "atoi"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
dependencies = [
"num-traits",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@ -297,12 +280,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.0"
@ -353,6 +330,8 @@ name = "common-contracts"
version = "0.1.0"
dependencies = [
"anyhow",
"async-nats",
"async-trait",
"chrono",
"log",
"reqwest",
@ -360,22 +339,12 @@ dependencies = [
"serde",
"serde_json",
"service_kit",
"sqlx",
"tokio",
"tracing",
"utoipa",
"uuid",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "config"
version = "0.15.19"
@ -456,30 +425,6 @@ dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@ -611,9 +556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
"subtle",
]
[[package]]
@ -636,12 +579,6 @@ dependencies = [
"const-random",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dyn-clone"
version = "1.0.20"
@ -670,15 +607,6 @@ dependencies = [
"subtle",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
dependencies = [
"serde",
]
[[package]]
name = "encoding_rs"
version = "0.8.35"
@ -715,28 +643,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "etcetera"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
dependencies = [
"cfg-if",
"home",
"windows-sys 0.48.0",
]
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "fastrand"
version = "2.3.0"
@ -755,17 +661,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "flume"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
dependencies = [
"futures-core",
"futures-sink",
"spin",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -850,17 +745,6 @@ dependencies = [
"futures-util",
]
[[package]]
name = "futures-intrusive"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
dependencies = [
"futures-core",
"lock_api",
"parking_lot",
]
[[package]]
name = "futures-io"
version = "0.3.31"
@ -985,8 +869,6 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
@ -1011,39 +893,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
dependencies = [
"hmac",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "home"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "http"
version = "1.3.1"
@ -1373,9 +1222,6 @@ name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin",
]
[[package]]
name = "libc"
@ -1383,33 +1229,6 @@ version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libm"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "libredox"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags",
"libc",
"redox_syscall",
]
[[package]]
name = "libsqlite3-sys"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@ -1458,16 +1277,6 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "md-5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest",
]
[[package]]
name = "memchr"
version = "2.7.6"
@ -1541,48 +1350,12 @@ dependencies = [
"rand 0.8.5",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
dependencies = [
"lazy_static",
"libm",
"num-integer",
"num-iter",
"num-traits",
"rand 0.8.5",
"smallvec",
"zeroize",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -1590,7 +1363,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@ -1653,12 +1425,6 @@ dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.5"
@ -1784,17 +1550,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"pkcs8",
"spki",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
@ -2224,26 +1979,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "rsa"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
dependencies = [
"const-oid",
"digest",
"num-bigint-dig",
"num-integer",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core 0.6.4",
"signature",
"spki",
"subtle",
"zeroize",
]
[[package]]
name = "rust-ini"
version = "0.21.3"
@ -2424,16 +2159,6 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "secrecy"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
dependencies = [
"serde",
"zeroize",
]
[[package]]
name = "security-framework"
version = "2.11.1"
@ -2617,17 +2342,6 @@ dependencies = [
"utoipa",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
@ -2702,9 +2416,6 @@ name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [
"serde",
]
[[package]]
name = "socket2"
@ -2716,15 +2427,6 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "spki"
version = "0.7.3"
@ -2735,207 +2437,6 @@ dependencies = [
"der",
]
[[package]]
name = "sqlx"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
dependencies = [
"sqlx-core",
"sqlx-macros",
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
]
[[package]]
name = "sqlx-core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
dependencies = [
"base64",
"bytes",
"chrono",
"crc",
"crossbeam-queue",
"either",
"event-listener",
"futures-core",
"futures-intrusive",
"futures-io",
"futures-util",
"hashbrown 0.15.5",
"hashlink",
"indexmap",
"log",
"memchr",
"once_cell",
"percent-encoding",
"rust_decimal",
"rustls",
"serde",
"serde_json",
"sha2",
"smallvec",
"thiserror 2.0.17",
"tokio",
"tokio-stream",
"tracing",
"url",
"uuid",
"webpki-roots 0.26.11",
]
[[package]]
name = "sqlx-macros"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
dependencies = [
"proc-macro2",
"quote",
"sqlx-core",
"sqlx-macros-core",
"syn 2.0.110",
]
[[package]]
name = "sqlx-macros-core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
dependencies = [
"dotenvy",
"either",
"heck",
"hex",
"once_cell",
"proc-macro2",
"quote",
"serde",
"serde_json",
"sha2",
"sqlx-core",
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
"syn 2.0.110",
"tokio",
"url",
]
[[package]]
name = "sqlx-mysql"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
dependencies = [
"atoi",
"base64",
"bitflags",
"byteorder",
"bytes",
"chrono",
"crc",
"digest",
"dotenvy",
"either",
"futures-channel",
"futures-core",
"futures-io",
"futures-util",
"generic-array",
"hex",
"hkdf",
"hmac",
"itoa",
"log",
"md-5",
"memchr",
"once_cell",
"percent-encoding",
"rand 0.8.5",
"rsa",
"rust_decimal",
"serde",
"sha1",
"sha2",
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.17",
"tracing",
"uuid",
"whoami",
]
[[package]]
name = "sqlx-postgres"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
dependencies = [
"atoi",
"base64",
"bitflags",
"byteorder",
"chrono",
"crc",
"dotenvy",
"etcetera",
"futures-channel",
"futures-core",
"futures-util",
"hex",
"hkdf",
"hmac",
"home",
"itoa",
"log",
"md-5",
"memchr",
"once_cell",
"rand 0.8.5",
"rust_decimal",
"serde",
"serde_json",
"sha2",
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.17",
"tracing",
"uuid",
"whoami",
]
[[package]]
name = "sqlx-sqlite"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
dependencies = [
"atoi",
"chrono",
"flume",
"futures-channel",
"futures-core",
"futures-executor",
"futures-intrusive",
"futures-util",
"libsqlite3-sys",
"log",
"percent-encoding",
"serde",
"serde_urlencoded",
"sqlx-core",
"thiserror 2.0.17",
"tracing",
"url",
"uuid",
]
[[package]]
name = "sse-stream"
version = "0.2.1"
@ -2955,17 +2456,6 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "stringprep"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
dependencies = [
"unicode-bidi",
"unicode-normalization",
"unicode-properties",
]
[[package]]
name = "strsim"
version = "0.11.1"
@ -3460,33 +2950,12 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unicode-bidi"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-normalization"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-properties"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
@ -3596,12 +3065,6 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.105"
@ -3711,16 +3174,6 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "whoami"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
dependencies = [
"libredox",
"wasite",
]
[[package]]
name = "windows-core"
version = "0.62.2"
@ -3791,15 +3244,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@ -3827,21 +3271,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@ -3875,12 +3304,6 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@ -3893,12 +3316,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@ -3911,12 +3328,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@ -3941,12 +3352,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@ -3959,12 +3364,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@ -3977,12 +3376,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@ -3995,12 +3388,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"

View File

@ -10,7 +10,7 @@ tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.6.6", features = ["cors"] }
# Shared Contracts
common-contracts = { path = "../common-contracts" }
common-contracts = { path = "../common-contracts", default-features = false }
# Generic MCP Client
rmcp = { version = "0.9.0", features = ["client", "transport-streamable-http-client-reqwest"] }
@ -23,7 +23,6 @@ futures-util = "0.3"
reqwest = { version = "0.12", features = ["json"] }
# Concurrency & Async
async-trait = "0.1"
dashmap = "6.1.0" # For concurrent task tracking
uuid = { version = "1.8", features = ["v4"] }
@ -37,7 +36,6 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Configuration
config = "0.15.19"
secrecy = { version = "0.10.3", features = ["serde"] }
# Error Handling
thiserror = "2.0.17"

View File

@ -6,7 +6,7 @@ WORKDIR /usr/src/app
COPY ./services/common-contracts /usr/src/app/services/common-contracts
COPY ./services/alphavantage-provider-service /usr/src/app/services/alphavantage-provider-service
WORKDIR /usr/src/app/services/alphavantage-provider-service
RUN cargo build --release --bin alphavantage-provider-service
RUN cargo build --bin alphavantage-provider-service
# 2. Runtime Stage
FROM debian:bookworm-slim
@ -18,7 +18,7 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/*
# Copy the built binary from the builder stage
COPY --from=builder /usr/src/app/services/alphavantage-provider-service/target/release/alphavantage-provider-service /usr/local/bin/
COPY --from=builder /usr/src/app/services/alphavantage-provider-service/target/debug/alphavantage-provider-service /usr/local/bin/
# Set the binary as the entrypoint
ENTRYPOINT ["/usr/local/bin/alphavantage-provider-service"]

View File

@ -1,6 +1,5 @@
use crate::av_client::AvClient;
use axum::{http::StatusCode, response::{IntoResponse, Json}};
use secrecy::{SecretString, ExposeSecret};
use serde::{Deserialize, Serialize};
use tracing::{info, warn};
@ -10,7 +9,7 @@ pub struct TestConnectionRequest {
pub api_url: String,
// The API key is passed for validation but might not be used directly
// in the MCP connection itself, depending on auth mechanism.
pub api_key: Option<SecretString>,
pub api_key: Option<String>,
}
#[derive(Serialize)]
@ -55,7 +54,7 @@ pub async fn test_connection(
}),
).into_response();
};
let final_url = format!("{}?apikey={}", payload.api_url, key.expose_secret());
let final_url = format!("{}?apikey={}", payload.api_url, key);
info!("Testing MCP with final endpoint: {}", final_url);
let mcp_client = match AvClient::connect(&final_url).await {
Ok(client) => client,

View File

@ -1,4 +1,3 @@
use secrecy::SecretString;
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
@ -6,7 +5,7 @@ pub struct AppConfig {
pub server_port: u16,
pub nats_addr: String,
pub data_persistence_service_url: String,
pub alphavantage_api_key: Option<SecretString>,
pub alphavantage_api_key: Option<String>,
// New fields
pub api_gateway_url: String,

View File

@ -1,7 +1,6 @@
use crate::error::Result;
use crate::state::AppState;
use common_contracts::config_models::{DataSourceConfig, DataSourceProvider};
use secrecy::SecretString;
use std::collections::HashMap;
use std::time::Duration;
use tracing::{error, info, instrument};
@ -40,7 +39,7 @@ async fn poll_and_update_config(state: &AppState) -> Result<()> {
});
if let Some(config) = alphavantage_config {
let api_key = config.api_key.clone().map(SecretString::from);
let api_key = config.api_key.clone();
let api_url = config.api_url.clone();
state.update_provider(api_key, api_url).await;
info!("Successfully updated Alphavantage provider with new configuration.");

View File

@ -14,9 +14,9 @@ mod transport;
use crate::config::AppConfig;
use crate::error::Result;
use crate::state::AppState;
use tracing::{info, warn};
use tracing::info;
use common_contracts::lifecycle::ServiceRegistrar;
use common_contracts::registry::ServiceRegistration;
use common_contracts::registry::{ServiceRegistration, ProviderMetadata, ConfigFieldSchema, FieldType, ConfigKey};
use std::sync::Arc;
#[tokio::main]
@ -53,6 +53,26 @@ async fn main() -> Result<()> {
role: common_contracts::registry::ServiceRole::DataProvider,
base_url: format!("http://{}:{}", config.service_host, port),
health_check_url: format!("http://{}:{}/health", config.service_host, port),
metadata: Some(ProviderMetadata {
id: "alphavantage".to_string(),
name_en: "Alpha Vantage".to_string(),
name_cn: "Alpha Vantage".to_string(),
description: "Alpha Vantage API".to_string(),
icon_url: None,
config_schema: vec![
ConfigFieldSchema {
key: ConfigKey::ApiKey,
label: "API Key".to_string(),
field_type: FieldType::Password,
required: true,
placeholder: Some("Enter your API key...".to_string()),
default_value: None,
description: Some("Get it from https://www.alphavantage.co".to_string()),
options: None,
},
],
supports_test_connection: true,
}),
}
);

View File

@ -2,7 +2,6 @@ use crate::av_client::AvClient;
use crate::config::AppConfig;
use common_contracts::observability::TaskProgress;
use dashmap::DashMap;
use secrecy::{ExposeSecret, SecretString};
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
@ -42,7 +41,7 @@ impl AppState {
self.av_provider.read().await.clone()
}
pub async fn update_provider(&self, api_key: Option<SecretString>, api_url: Option<String>) {
pub async fn update_provider(&self, api_key: Option<String>, api_url: Option<String>) {
let mut provider_guard = self.av_provider.write().await;
let mut status_guard = self.status.write().await;
@ -60,7 +59,7 @@ impl AppState {
};
return;
}
let mcp_endpoint = format!("{}?apikey={}", base_url, key.expose_secret());
let mcp_endpoint = format!("{}?apikey={}", base_url, key);
info!("Initializing Alphavantage MCP provider with endpoint: {}", mcp_endpoint);
match AvClient::connect(&mcp_endpoint).await {
Ok(new_provider) => {

View File

@ -28,12 +28,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@ -166,15 +160,6 @@ dependencies = [
"syn 2.0.110",
]
[[package]]
name = "atoi"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
dependencies = [
"num-traits",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@ -332,12 +317,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.0"
@ -388,6 +367,8 @@ name = "common-contracts"
version = "0.1.0"
dependencies = [
"anyhow",
"async-nats",
"async-trait",
"chrono",
"log",
"reqwest",
@ -395,22 +376,12 @@ dependencies = [
"serde",
"serde_json",
"service_kit",
"sqlx",
"tokio",
"tracing",
"utoipa",
"uuid",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "config"
version = "0.15.19"
@ -491,21 +462,6 @@ dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
version = "1.5.0"
@ -515,21 +471,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
@ -617,9 +558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
"subtle",
]
[[package]]
@ -642,12 +581,6 @@ dependencies = [
"const-random",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dyn-clone"
version = "1.0.20"
@ -676,15 +609,6 @@ dependencies = [
"subtle",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
dependencies = [
"serde",
]
[[package]]
name = "encoding_rs"
version = "0.8.35"
@ -721,28 +645,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "etcetera"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
dependencies = [
"cfg-if",
"home",
"windows-sys 0.48.0",
]
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "fastrand"
version = "2.3.0"
@ -772,17 +674,6 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "flume"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
dependencies = [
"futures-core",
"futures-sink",
"spin",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -832,7 +723,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@ -841,28 +731,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-intrusive"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
dependencies = [
"futures-core",
"lock_api",
"parking_lot",
]
[[package]]
name = "futures-io"
version = "0.3.31"
@ -986,8 +854,6 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
@ -1012,39 +878,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
dependencies = [
"hmac",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "home"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "http"
version = "1.3.1"
@ -1368,9 +1201,6 @@ name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin",
]
[[package]]
name = "libc"
@ -1378,33 +1208,6 @@ version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libm"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "libredox"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags",
"libc",
"redox_syscall",
]
[[package]]
name = "libsqlite3-sys"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"pkg-config",
"vcpkg",
]
[[package]]
name = "libz-rs-sys"
version = "0.5.2"
@ -1462,16 +1265,6 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "md-5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest",
]
[[package]]
name = "memchr"
version = "2.7.6"
@ -1565,48 +1358,12 @@ dependencies = [
"rand 0.8.5",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
dependencies = [
"lazy_static",
"libm",
"num-integer",
"num-iter",
"num-traits",
"rand 0.8.5",
"smallvec",
"zeroize",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -1614,7 +1371,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@ -1677,12 +1433,6 @@ dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.5"
@ -1802,17 +1552,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"pkcs8",
"spki",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
@ -2203,26 +1942,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "rsa"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
dependencies = [
"const-oid",
"digest",
"num-bigint-dig",
"num-integer",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core 0.6.4",
"signature",
"spki",
"subtle",
"zeroize",
]
[[package]]
name = "rust-embed"
version = "8.9.0"
@ -2629,17 +2348,6 @@ dependencies = [
"utoipa",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
@ -2720,9 +2428,6 @@ name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [
"serde",
]
[[package]]
name = "socket2"
@ -2734,15 +2439,6 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "spki"
version = "0.7.3"
@ -2753,224 +2449,12 @@ dependencies = [
"der",
]
[[package]]
name = "sqlx"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
dependencies = [
"sqlx-core",
"sqlx-macros",
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
]
[[package]]
name = "sqlx-core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
dependencies = [
"base64",
"bytes",
"chrono",
"crc",
"crossbeam-queue",
"either",
"event-listener",
"futures-core",
"futures-intrusive",
"futures-io",
"futures-util",
"hashbrown 0.15.5",
"hashlink",
"indexmap",
"log",
"memchr",
"once_cell",
"percent-encoding",
"rust_decimal",
"rustls",
"serde",
"serde_json",
"sha2",
"smallvec",
"thiserror 2.0.17",
"tokio",
"tokio-stream",
"tracing",
"url",
"uuid",
"webpki-roots 0.26.11",
]
[[package]]
name = "sqlx-macros"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
dependencies = [
"proc-macro2",
"quote",
"sqlx-core",
"sqlx-macros-core",
"syn 2.0.110",
]
[[package]]
name = "sqlx-macros-core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
dependencies = [
"dotenvy",
"either",
"heck",
"hex",
"once_cell",
"proc-macro2",
"quote",
"serde",
"serde_json",
"sha2",
"sqlx-core",
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
"syn 2.0.110",
"tokio",
"url",
]
[[package]]
name = "sqlx-mysql"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
dependencies = [
"atoi",
"base64",
"bitflags",
"byteorder",
"bytes",
"chrono",
"crc",
"digest",
"dotenvy",
"either",
"futures-channel",
"futures-core",
"futures-io",
"futures-util",
"generic-array",
"hex",
"hkdf",
"hmac",
"itoa",
"log",
"md-5",
"memchr",
"once_cell",
"percent-encoding",
"rand 0.8.5",
"rsa",
"rust_decimal",
"serde",
"sha1",
"sha2",
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.17",
"tracing",
"uuid",
"whoami",
]
[[package]]
name = "sqlx-postgres"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
dependencies = [
"atoi",
"base64",
"bitflags",
"byteorder",
"chrono",
"crc",
"dotenvy",
"etcetera",
"futures-channel",
"futures-core",
"futures-util",
"hex",
"hkdf",
"hmac",
"home",
"itoa",
"log",
"md-5",
"memchr",
"once_cell",
"rand 0.8.5",
"rust_decimal",
"serde",
"serde_json",
"sha2",
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.17",
"tracing",
"uuid",
"whoami",
]
[[package]]
name = "sqlx-sqlite"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
dependencies = [
"atoi",
"chrono",
"flume",
"futures-channel",
"futures-core",
"futures-executor",
"futures-intrusive",
"futures-util",
"libsqlite3-sys",
"log",
"percent-encoding",
"serde",
"serde_urlencoded",
"sqlx-core",
"thiserror 2.0.17",
"tracing",
"url",
"uuid",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "stringprep"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
dependencies = [
"unicode-bidi",
"unicode-normalization",
"unicode-properties",
]
[[package]]
name = "subtle"
version = "2.6.1"
@ -3466,33 +2950,12 @@ version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-bidi"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-normalization"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-properties"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
@ -3637,12 +3100,6 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.105"
@ -3752,16 +3209,6 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "whoami"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
dependencies = [
"libredox",
"wasite",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
@ -3841,15 +3288,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@ -3877,21 +3315,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@ -3925,12 +3348,6 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@ -3943,12 +3360,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@ -3961,12 +3372,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@ -3991,12 +3396,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@ -4009,12 +3408,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@ -4027,12 +3420,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@ -4045,12 +3432,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"

View File

@ -13,7 +13,7 @@ utoipa-swagger-ui = { version = "9.0", features = ["axum", "vendored"] }
service_kit = { version = "0.1.2" }
# Shared Contracts
common-contracts = { path = "../common-contracts" }
common-contracts = { path = "../common-contracts", default-features = false }
# Message Queue (NATS)
async-nats = "0.45.0"

View File

@ -7,9 +7,9 @@ WORKDIR /usr/src/app
COPY ./services/common-contracts /usr/src/app/services/common-contracts
COPY ./services/api-gateway/Cargo.toml ./services/api-gateway/Cargo.lock* ./services/api-gateway/
WORKDIR /usr/src/app/services/api-gateway
# Copy the full source code and build the final binary
# Copy the full source code and build the final binary (Debug mode for speed)
COPY ./services/api-gateway /usr/src/app/services/api-gateway
RUN cargo build --release --bin api-gateway
RUN cargo build --bin api-gateway
# 2. Runtime Stage
FROM debian:bookworm-slim
@ -23,7 +23,7 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libssl3 curl && rm -rf /var/lib/apt/lists/*
# Copy the built binary from the builder stage
COPY --from=builder /usr/src/app/services/api-gateway/target/release/api-gateway /usr/local/bin/
COPY --from=builder /usr/src/app/services/api-gateway/target/debug/api-gateway /usr/local/bin/
# Set the binary as the entrypoint
ENTRYPOINT ["/usr/local/bin/api-gateway"]

View File

@ -8,11 +8,12 @@ use axum::{
routing::{get, post},
};
use common_contracts::config_models::{
AnalysisTemplateSets, DataSourceConfig as ProviderDataSourceConfig, DataSourceProvider,
AnalysisTemplateSets, DataSourceProvider,
DataSourcesConfig, LlmProvider, LlmProvidersConfig,
};
use common_contracts::messages::GenerateReportCommand;
use common_contracts::observability::{TaskProgress, ObservabilityTaskStatus};
use common_contracts::registry::ProviderMetadata;
use common_contracts::subjects::{NatsSubject, SubjectMessage};
use common_contracts::symbol_utils::{CanonicalSymbol, Market};
use futures_util::future::join_all;
@ -65,32 +66,10 @@ pub struct SymbolResolveResponse {
pub market: String,
}
// --- Dynamic Schema Structs ---
// --- Dynamic Schema Structs (Replaced by Dynamic Registry) ---
#[api_dto]
pub struct DataSourceSchemaResponse {
pub providers: Vec<DataSourceProviderSchema>,
}
#[api_dto]
pub struct DataSourceProviderSchema {
pub id: String,
pub name: String,
pub description: String,
pub fields: Vec<ConfigFieldSchema>,
}
#[api_dto]
pub struct ConfigFieldSchema {
pub key: String,
pub label: String,
pub r#type: String, // text, password, select, boolean
pub required: bool,
pub placeholder: Option<String>,
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
}
// Legacy endpoint /configs/data_sources/schema removed.
// Frontend should now use /registry/providers to get metadata.
// --- Router Definition ---
pub fn create_router(app_state: AppState) -> Router {
@ -99,7 +78,7 @@ pub fn create_router(app_state: AppState) -> Router {
let mut router = Router::new()
.route("/health", get(health_check))
.route("/tasks/{request_id}", get(get_task_progress))
.nest("/v1", create_v1_router())
.nest("/api/v1", create_v1_router())
.with_state(app_state);
// Mount Swagger UI
@ -152,10 +131,6 @@ fn create_v1_router() -> Router<AppState> {
"/configs/data_sources",
get(get_data_sources_config).put(update_data_sources_config),
)
.route(
"/configs/data_sources/schema",
get(get_data_source_schema),
)
.route("/configs/test", post(test_data_source_config))
.route("/configs/llm/test", post(test_llm_config))
.route("/config", get(get_legacy_system_config))
@ -164,6 +139,7 @@ fn create_v1_router() -> Router<AppState> {
.route("/registry/register", post(registry::register_service))
.route("/registry/heartbeat", post(registry::heartbeat))
.route("/registry/deregister", post(registry::deregister_service))
.route("/registry/providers", get(get_registered_providers))
}
// --- Legacy Config Compatibility ---
@ -257,9 +233,10 @@ fn derive_primary_provider(providers: &LlmProvidersConfig) -> LegacyNewApiConfig
}
fn project_data_sources(
configs: HashMap<String, ProviderDataSourceConfig>,
configs: DataSourcesConfig,
) -> HashMap<String, LegacyDataSourceConfig> {
configs
.0
.into_iter()
.map(|(key, cfg)| {
let provider = provider_id(&cfg.provider).to_string();
@ -301,7 +278,7 @@ fn infer_market(symbol: &str) -> String {
/// Resolves and normalizes a symbol without starting a workflow.
#[utoipa::path(
post,
path = "/v1/tools/resolve-symbol",
path = "/api/v1/tools/resolve-symbol",
request_body = SymbolResolveRequest,
responses(
(status = 200, description = "Symbol resolved", body = SymbolResolveResponse)
@ -331,7 +308,7 @@ async fn resolve_symbol(Json(payload): Json<SymbolResolveRequest>) -> Result<imp
/// Initiates a new analysis workflow via the Orchestrator.
#[utoipa::path(
post,
path = "/v1/workflow/start",
path = "/api/v1/workflow/start",
request_body = DataRequest,
responses(
(status = 202, description = "Workflow started", body = RequestAcceptedResponse)
@ -638,18 +615,23 @@ async fn get_task_progress(
#[api_dto]
pub struct TestConfigRequest {
pub r#type: String,
#[serde(flatten)]
pub data: serde_json::Value,
}
/// [POST /v1/configs/test]
#[api_dto]
pub struct TestConnectionResponse {
pub success: bool,
pub message: String,
}
/// [POST /api/v1/configs/test]
/// Forwards a configuration test request to the appropriate downstream service.
#[utoipa::path(
post,
path = "/v1/configs/test",
path = "/api/v1/configs/test",
request_body = TestConfigRequest,
responses(
(status = 200, description = "Configuration test result (JSON)")
(status = 200, description = "Configuration test result", body = TestConnectionResponse)
)
)]
async fn test_data_source_config(
@ -663,7 +645,10 @@ async fn test_data_source_config(
if let Some(base_url) = target_service_url {
let client = reqwest::Client::new();
let target_url = format!("{}/test", base_url.trim_end_matches('/'));
// Remove trailing slash from base_url
let clean_base = base_url.trim_end_matches('/');
// Check if it's a provider service which usually mounts test at /test
let target_url = format!("{}/test", clean_base);
info!(
"Forwarding test request for '{}' to {}",
payload.r#type, target_url
@ -713,7 +698,7 @@ pub struct TestLlmConfigRequest {
/// [POST /v1/configs/llm/test]
#[utoipa::path(
post,
path = "/v1/configs/llm/test",
path = "/api/v1/configs/llm/test",
request_body = TestLlmConfigRequest,
responses(
(status = 200, description = "LLM config test result (JSON)")
@ -753,10 +738,10 @@ async fn test_llm_config(
// --- Config API Handlers (Proxy to data-persistence-service) ---
/// [GET /v1/configs/llm_providers]
/// [GET /api/v1/configs/llm_providers]
#[utoipa::path(
get,
path = "/v1/configs/llm_providers",
path = "/api/v1/configs/llm_providers",
responses(
(status = 200, description = "LLM providers configuration", body = LlmProvidersConfig)
)
@ -766,10 +751,10 @@ async fn get_llm_providers_config(State(state): State<AppState>) -> Result<impl
Ok(Json(config))
}
/// [PUT /v1/configs/llm_providers]
/// [PUT /api/v1/configs/llm_providers]
#[utoipa::path(
put,
path = "/v1/configs/llm_providers",
path = "/api/v1/configs/llm_providers",
request_body = LlmProvidersConfig,
responses(
(status = 200, description = "Updated LLM providers configuration", body = LlmProvidersConfig)
@ -786,10 +771,10 @@ async fn update_llm_providers_config(
Ok(Json(updated_config))
}
/// [GET /v1/configs/analysis_template_sets]
/// [GET /api/v1/configs/analysis_template_sets]
#[utoipa::path(
get,
path = "/v1/configs/analysis_template_sets",
path = "/api/v1/configs/analysis_template_sets",
responses(
(status = 200, description = "Analysis template sets configuration", body = AnalysisTemplateSets)
)
@ -802,10 +787,10 @@ async fn get_analysis_template_sets(State(state): State<AppState>) -> Result<imp
Ok(Json(config))
}
/// [PUT /v1/configs/analysis_template_sets]
/// [PUT /api/v1/configs/analysis_template_sets]
#[utoipa::path(
put,
path = "/v1/configs/analysis_template_sets",
path = "/api/v1/configs/analysis_template_sets",
request_body = AnalysisTemplateSets,
responses(
(status = 200, description = "Updated analysis template sets configuration", body = AnalysisTemplateSets)
@ -822,10 +807,10 @@ async fn update_analysis_template_sets(
Ok(Json(updated_config))
}
/// [GET /v1/configs/data_sources]
/// [GET /api/v1/configs/data_sources]
#[utoipa::path(
get,
path = "/v1/configs/data_sources",
path = "/api/v1/configs/data_sources",
responses(
(status = 200, description = "Data sources configuration", body = DataSourcesConfig)
)
@ -835,10 +820,10 @@ async fn get_data_sources_config(State(state): State<AppState>) -> Result<impl I
Ok(Json(config))
}
/// [PUT /v1/configs/data_sources]
/// [PUT /api/v1/configs/data_sources]
#[utoipa::path(
put,
path = "/v1/configs/data_sources",
path = "/api/v1/configs/data_sources",
request_body = DataSourcesConfig,
responses(
(status = 200, description = "Updated data sources configuration", body = DataSourcesConfig)
@ -855,95 +840,41 @@ async fn update_data_sources_config(
Ok(Json(updated_config))
}
/// [GET /v1/configs/data_sources/schema]
/// Returns the schema definitions for all supported data sources (for dynamic UI generation).
/// [GET /api/v1/registry/providers]
/// Returns metadata for all registered data providers.
#[utoipa::path(
get,
path = "/v1/configs/data_sources/schema",
path = "/api/v1/registry/providers",
responses(
(status = 200, description = "Data sources schema", body = DataSourceSchemaResponse)
(status = 200, description = "Registered providers metadata", body = Vec<ProviderMetadata>)
)
)]
async fn get_data_source_schema() -> Result<impl IntoResponse> {
let tushare = DataSourceProviderSchema {
id: "tushare".to_string(),
name: "Tushare Pro".to_string(),
description: "Official Tushare Data Provider".to_string(),
fields: vec![
ConfigFieldSchema {
key: "api_token".to_string(),
label: "API Token".to_string(),
r#type: "password".to_string(),
required: true,
placeholder: Some("Enter your token...".to_string()),
description: Some("Get it from https://tushare.pro".to_string()),
default: None,
},
ConfigFieldSchema {
key: "api_url".to_string(),
label: "API Endpoint".to_string(),
r#type: "text".to_string(),
required: false,
placeholder: None,
description: None,
default: Some("http://api.tushare.pro".to_string()),
},
],
};
async fn get_registered_providers(State(state): State<AppState>) -> Result<impl IntoResponse> {
// let registry = state.registry.read().unwrap(); // OLD
let finnhub = DataSourceProviderSchema {
id: "finnhub".to_string(),
name: "Finnhub".to_string(),
description: "Finnhub Stock API".to_string(),
fields: vec![
ConfigFieldSchema {
key: "api_key".to_string(),
label: "API Key".to_string(),
r#type: "password".to_string(),
required: true,
placeholder: Some("Enter your API key...".to_string()),
description: Some("Get it from https://finnhub.io".to_string()),
default: None,
},
],
};
let entries = state.registry.get_entries();
let alphavantage = DataSourceProviderSchema {
id: "alphavantage".to_string(),
name: "Alpha Vantage".to_string(),
description: "Alpha Vantage API".to_string(),
fields: vec![
ConfigFieldSchema {
key: "api_key".to_string(),
label: "API Key".to_string(),
r#type: "password".to_string(),
required: true,
placeholder: Some("Enter your API key...".to_string()),
description: Some("Get it from https://www.alphavantage.co".to_string()),
default: None,
},
],
};
let providers: Vec<ProviderMetadata> = entries
.into_iter()
.filter_map(|entry| {
// Only return DataProvider services that have metadata
if entry.registration.role == common_contracts::registry::ServiceRole::DataProvider {
entry.registration.metadata
} else {
None
}
})
.collect();
let yfinance = DataSourceProviderSchema {
id: "yfinance".to_string(),
name: "Yahoo Finance".to_string(),
description: "Yahoo Finance API (Unofficial)".to_string(),
fields: vec![
// No fields required usually, maybe proxy
],
};
Ok(Json(DataSourceSchemaResponse {
providers: vec![tushare, finnhub, alphavantage, yfinance],
}))
Ok(Json(providers))
}
/// [GET /v1/discover-models/:provider_id]
/// [GET /api/v1/discover-models/:provider_id]
#[utoipa::path(
get,
path = "/v1/discover-models/{provider_id}",
path = "/api/v1/discover-models/{provider_id}",
params(
("provider_id" = String, Path, description = "Provider ID to discover models for")
),
@ -1010,11 +941,11 @@ pub struct DiscoverPreviewRequest {
pub api_key: String,
}
/// [POST /v1/discover-models]
/// [POST /api/v1/discover-models]
/// Preview discovery without persisting provider configuration.
#[utoipa::path(
post,
path = "/v1/discover-models",
path = "/api/v1/discover-models",
request_body = DiscoverPreviewRequest,
responses(
(status = 200, description = "Discovered models (JSON)"),

View File

@ -1,11 +1,10 @@
use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
use common_contracts::registry::{Heartbeat, ServiceRegistration};
use std::time::Instant;
use tracing::{info, warn};
use crate::{
error::Result,
state::{AppState, RegistryEntry},
state::AppState,
};
/// [POST /v1/registry/register]
@ -18,13 +17,7 @@ pub async fn register_service(
payload.service_id, payload.service_name, payload.base_url
);
let entry = RegistryEntry {
registration: payload.clone(),
last_heartbeat: Instant::now(),
};
let mut registry = state.registry.write().unwrap();
registry.insert(payload.service_id.clone(), entry);
state.registry.register(payload);
Ok(StatusCode::OK)
}
@ -34,10 +27,7 @@ pub async fn heartbeat(
State(state): State<AppState>,
Json(payload): Json<Heartbeat>,
) -> Result<impl IntoResponse> {
let mut registry = state.registry.write().unwrap();
if let Some(entry) = registry.get_mut(&payload.service_id) {
entry.last_heartbeat = Instant::now();
if state.registry.update_heartbeat(&payload.service_id) {
Ok(StatusCode::OK)
} else {
// This is the key part for self-healing: tell the provider we don't know them
@ -61,8 +51,7 @@ pub async fn deregister_service(
info!("Deregistering service: {}", service_id);
let mut registry = state.registry.write().unwrap();
registry.remove(service_id);
state.registry.deregister(service_id);
Ok(StatusCode::OK)
}

View File

@ -0,0 +1,60 @@
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use axum::routing::post;
use axum::Router;
use common_contracts::registry::{ProviderMetadata, ConfigFieldSchema, FieldType, ServiceRole};
use crate::state::AppState;
use tower::ServiceExt; // for `oneshot`
use std::sync::{Arc, RwLock};
use std::collections::HashMap;
// Mock AppState for testing registry logic
// Since AppState has complex fields like NatsClient, we might mock the registry part only
// or create a minimal AppState if possible.
// However, AppState fields are public, but NatsClient is hard to mock.
// Instead, we can test the logic via direct registry manipulation or by mocking the whole AppState if possible.
// Actually, we can't easily mock NatsClient.
// But the registry handlers only need `state.registry`.
// Let's see if we can construct a dummy AppState or if we should split the registry logic into a trait/struct that is easier to test.
// A better approach for unit testing `registry.rs` handlers is to just test the `RegistryEntry` logic directly
// if we can't easily instantiate AppState.
// BUT the user asked for unit tests for the *module* discovery/revocation.
// Let's try to instantiate a "partial" AppState if we can, or just refactor `registry` to be more testable.
// Since `AppState` has `nats_client` which connects on creation, it's hard to `AppState::new`.
// However, we can construct `AppState` manually if fields are pub, but `nats_client` is not optional.
// WORKAROUND: We will test the logic by mocking the "Registry" interactions if we extracted it,
// but since it's embedded in AppState, we might have to skip full integration test here
// and focus on the logic if we can't easily create AppState.
// WAIT, `api/registry.rs` handlers take `State(AppState)`.
// If we can't create `AppState`, we can't test handlers easily with `oneshot`.
// Alternative: Create a test that doesn't rely on `AppState` but on a `Registry` struct.
// But the code uses `AppState`.
// Let's look at `AppState` definition again.
// pub struct AppState { config, nats_client, persistence_client, registry }
// If we can't construct NatsClient without a real NATS server, we are stuck.
// We should probably refactor `registry` to be a separate struct that `AppState` holds,
// and test that struct.
// Refactoring plan:
// 1. Create `ServiceRegistry` struct wrapping the HashMap.
// 2. Move logic (insert, get, remove) there.
// 3. Test `ServiceRegistry` independently.
// 4. Update `AppState` to use `ServiceRegistry`.
}
// Since I cannot easily run NATS in this environment for unit tests,
// I will refactor the Registry logic into a standalone struct `ServiceRegistry`
// which CAN be unit tested without NATS.

View File

@ -6,6 +6,8 @@ mod state;
mod openapi;
#[cfg(test)]
mod openapi_tests;
#[cfg(test)]
mod registry_unit_test;
use crate::config::AppConfig;
use crate::error::Result;

View File

@ -2,6 +2,7 @@ use utoipa::OpenApi;
use common_contracts::messages::*;
use common_contracts::observability::*;
use common_contracts::config_models::*;
use common_contracts::registry::{ProviderMetadata, ConfigFieldSchema, FieldType, ConfigKey};
use crate::api;
#[derive(OpenApi)]
@ -21,7 +22,7 @@ use crate::api;
api::test_llm_config,
api::discover_models,
api::discover_models_preview,
api::get_data_source_schema, // New endpoint
api::get_registered_providers, // New endpoint
),
components(
schemas(
@ -39,22 +40,28 @@ use crate::api;
HealthStatus,
ObservabilityTaskStatus,
// Configs
LlmProvidersConfig,
LlmProvider,
LlmModel,
AnalysisTemplateSets,
AnalysisTemplateSet,
AnalysisModuleConfig,
DataSourcesConfig,
DataSourceConfig,
DataSourceProvider,
// Registry / Dynamic Config
ProviderMetadata,
ConfigFieldSchema,
FieldType,
ConfigKey,
// Request/Response
api::DataRequest,
api::RequestAcceptedResponse,
api::SymbolResolveRequest,
api::SymbolResolveResponse,
api::TestConfigRequest,
api::TestConnectionResponse,
api::TestLlmConfigRequest,
api::DataSourceSchemaResponse,
api::DataSourceProviderSchema,
api::ConfigFieldSchema,
)
),
tags(
@ -66,3 +73,24 @@ use crate::api;
)]
pub struct ApiDoc;
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use std::path::PathBuf;
#[test]
fn generate_openapi_json() {
let doc = ApiDoc::openapi();
let json = doc.to_pretty_json().expect("Failed to serialize OpenAPI");
// Locate the workspace root relative to this test file
// services/api-gateway/src/openapi.rs -> ../../../openapi.json
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("../../openapi.json");
let mut file = std::fs::File::create(&path).expect("Failed to create openapi.json");
file.write_all(json.as_bytes()).expect("Failed to write openapi.json");
println!("OpenAPI Spec written to: {:?}", path);
}
}

View File

@ -51,6 +51,7 @@ impl PersistenceClient {
Ok(financials)
}
#[allow(dead_code)]
pub async fn get_session_data(
&self,
request_id: uuid::Uuid,

View File

@ -0,0 +1,64 @@
use crate::state::ServiceRegistry;
use common_contracts::registry::{ServiceRegistration, ServiceRole, ProviderMetadata, ConfigFieldSchema, FieldType, ConfigKey};
#[test]
fn test_registry_crud() {
let registry = ServiceRegistry::new();
let service_id = "test-service-1";
// 1. Register
let registration = ServiceRegistration {
service_id: service_id.to_string(),
service_name: "test_provider".to_string(),
role: ServiceRole::DataProvider,
base_url: "http://localhost:8000".to_string(),
health_check_url: "http://localhost:8000/health".to_string(),
metadata: Some(ProviderMetadata {
id: "test_provider".to_string(),
name_en: "Test Provider".to_string(),
name_cn: "测试服务".to_string(),
description: "Test".to_string(),
icon_url: None,
config_schema: vec![
ConfigFieldSchema {
key: ConfigKey::ApiKey,
label: "API Key".to_string(),
field_type: FieldType::Password,
required: true,
placeholder: None,
default_value: None,
description: None,
options: None,
}
],
supports_test_connection: true,
}),
};
registry.register(registration.clone());
// Verify registration
assert_eq!(registry.get_provider_count(), 1);
assert!(registry.get_service_url("test_provider").is_some());
// 2. Heartbeat
assert!(registry.update_heartbeat(service_id));
assert!(!registry.update_heartbeat("unknown-service"));
// 3. Discovery logic
let services = registry.get_all_services();
assert_eq!(services.len(), 1);
assert_eq!(services[0].0, service_id);
let entries = registry.get_entries();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].registration.metadata.as_ref().unwrap().id, "test_provider");
// 4. Deregister
assert!(registry.deregister(service_id));
assert_eq!(registry.get_provider_count(), 0);
assert!(registry.get_service_url("test_provider").is_none());
// Deregister unknown should return false
assert!(!registry.deregister(service_id));
}

View File

@ -15,12 +15,88 @@ pub struct RegistryEntry {
pub last_heartbeat: Instant,
}
/// Encapsulates the service registry logic to make it testable without NATS.
#[derive(Debug, Default)]
pub struct ServiceRegistry {
services: RwLock<HashMap<String, RegistryEntry>>,
}
impl ServiceRegistry {
pub fn new() -> Self {
Self {
services: RwLock::new(HashMap::new()),
}
}
pub fn register(&self, registration: ServiceRegistration) {
let mut services = self.services.write().unwrap();
services.insert(
registration.service_id.clone(),
RegistryEntry {
registration,
last_heartbeat: Instant::now(),
},
);
}
pub fn deregister(&self, service_id: &str) -> bool {
let mut services = self.services.write().unwrap();
services.remove(service_id).is_some()
}
pub fn update_heartbeat(&self, service_id: &str) -> bool {
let mut services = self.services.write().unwrap();
if let Some(entry) = services.get_mut(service_id) {
entry.last_heartbeat = Instant::now();
true
} else {
false
}
}
pub fn get_service_url(&self, service_name: &str) -> Option<String> {
let services = self.services.read().unwrap();
services
.values()
.find(|entry| entry.registration.service_name == service_name)
.map(|entry| entry.registration.base_url.clone())
}
pub fn get_all_services(&self) -> Vec<(String, String)> {
let services = self.services.read().unwrap();
services
.values()
.map(|entry| {
(
entry.registration.service_id.clone(),
entry.registration.base_url.clone(),
)
})
.collect()
}
#[allow(dead_code)]
pub fn get_provider_count(&self) -> usize {
let services = self.services.read().unwrap();
services
.values()
.filter(|entry| entry.registration.role == ServiceRole::DataProvider)
.count()
}
pub fn get_entries(&self) -> Vec<RegistryEntry> {
let services = self.services.read().unwrap();
services.values().cloned().collect()
}
}
#[derive(Clone)]
pub struct AppState {
pub config: Arc<AppConfig>,
pub nats_client: NatsClient,
pub persistence_client: PersistenceClient,
pub registry: Arc<RwLock<HashMap<String, RegistryEntry>>>,
// Replaced Arc<RwLock<HashMap>> with Arc<ServiceRegistry>
pub registry: Arc<ServiceRegistry>,
}
impl AppState {
@ -34,45 +110,22 @@ impl AppState {
config: Arc::new(config),
nats_client,
persistence_client,
registry: Arc::new(RwLock::new(HashMap::new())),
registry: Arc::new(ServiceRegistry::new()),
})
}
/// Finds a healthy service instance by name (e.g., "tushare").
/// Returns the base_url.
// Delegate methods to registry
pub fn get_service_url(&self, service_name: &str) -> Option<String> {
let registry = self.registry.read().unwrap();
// TODO: Implement Round-Robin or check Last Heartbeat for health?
// For now, return the first match.
registry
.values()
.find(|entry| entry.registration.service_name == service_name)
.map(|entry| entry.registration.base_url.clone())
self.registry.get_service_url(service_name)
}
/// Returns all registered services as (service_id, base_url) tuples.
pub fn get_all_services(&self) -> Vec<(String, String)> {
let registry = self.registry.read().unwrap();
registry
.values()
.map(|entry| {
(
entry.registration.service_id.clone(),
entry.registration.base_url.clone(),
)
})
.collect()
self.registry.get_all_services()
}
#[allow(dead_code)]
pub fn get_provider_count(&self) -> usize {
let registry = self.registry.read().unwrap();
registry
.values()
.filter(|entry| {
// Strict type checking using ServiceRole
entry.registration.role == ServiceRole::DataProvider
})
.count()
self.registry.get_provider_count()
}
}

View File

@ -49,6 +49,54 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-nats"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86dde77d8a733a9dbaf865a9eb65c72e09c88f3d14d3dd0d2aecf511920ee4fe"
dependencies = [
"base64",
"bytes",
"futures-util",
"memchr",
"nkeys",
"nuid",
"once_cell",
"pin-project",
"portable-atomic",
"rand",
"regex",
"ring",
"rustls-native-certs",
"rustls-pemfile",
"rustls-webpki 0.102.8",
"serde",
"serde_json",
"serde_nanos",
"serde_repr",
"thiserror 1.0.69",
"time",
"tokio",
"tokio-rustls",
"tokio-stream",
"tokio-util",
"tokio-websockets",
"tracing",
"tryhard",
"url",
]
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
]
[[package]]
name = "atoi"
version = "2.0.0"
@ -219,6 +267,9 @@ name = "bytes"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
dependencies = [
"serde",
]
[[package]]
name = "cc"
@ -261,6 +312,8 @@ name = "common-contracts"
version = "0.1.0"
dependencies = [
"anyhow",
"async-nats",
"async-trait",
"chrono",
"log",
"reqwest",
@ -355,6 +408,38 @@ dependencies = [
"typenum",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"subtle",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
]
[[package]]
name = "data-encoding"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "der"
version = "0.7.10"
@ -366,6 +451,16 @@ dependencies = [
"zeroize",
]
[[package]]
name = "deranged"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
dependencies = [
"powerfmt",
"serde_core",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -401,6 +496,28 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519",
"sha2",
"signature",
"subtle",
]
[[package]]
name = "either"
version = "1.15.0"
@ -463,6 +580,12 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "find-msvc-tools"
version = "0.1.5"
@ -1144,6 +1267,30 @@ dependencies = [
"tempfile",
]
[[package]]
name = "nkeys"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879011babc47a1c7fdf5a935ae3cfe94f34645ca0cac1c7f6424b36fc743d1bf"
dependencies = [
"data-encoding",
"ed25519",
"ed25519-dalek",
"getrandom 0.2.16",
"log",
"rand",
"signatory",
]
[[package]]
name = "nuid"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc895af95856f929163a0aa20c26a78d26bfdc839f51b9d5aa7a5b79e52b7e83"
dependencies = [
"rand",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.6"
@ -1160,6 +1307,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
@ -1284,6 +1437,26 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
@ -1323,6 +1496,12 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "potential_utf"
version = "0.1.4"
@ -1332,6 +1511,12 @@ dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@ -1616,6 +1801,15 @@ dependencies = [
"serde_json",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "1.1.2"
@ -1638,11 +1832,33 @@ dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"rustls-webpki 0.103.8",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.13.0"
@ -1652,6 +1868,16 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.102.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.103.8"
@ -1745,6 +1971,12 @@ dependencies = [
"libc",
]
[[package]]
name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde"
version = "1.0.228"
@ -1799,6 +2031,15 @@ dependencies = [
"serde_core",
]
[[package]]
name = "serde_nanos"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a93142f0367a4cc53ae0fead1bcda39e85beccfad3dcd717656cacab94b12985"
dependencies = [
"serde",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.20"
@ -1810,6 +2051,17 @@ dependencies = [
"serde_core",
]
[[package]]
name = "serde_repr"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
]
[[package]]
name = "serde_spanned"
version = "1.0.3"
@ -1862,7 +2114,7 @@ dependencies = [
"serde_urlencoded",
"service-kit-macros",
"syn 2.0.110",
"thiserror",
"thiserror 2.0.17",
"toml",
"utoipa",
]
@ -1895,6 +2147,18 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signatory"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1e303f8205714074f6068773f0e29527e0453937fe837c9717d066635b65f31"
dependencies = [
"pkcs8",
"rand_core",
"signature",
"zeroize",
]
[[package]]
name = "signature"
version = "2.2.0"
@ -1998,7 +2262,7 @@ dependencies = [
"serde_json",
"sha2",
"smallvec",
"thiserror",
"thiserror 2.0.17",
"tokio",
"tokio-stream",
"tracing",
@ -2084,7 +2348,7 @@ dependencies = [
"smallvec",
"sqlx-core",
"stringprep",
"thiserror",
"thiserror 2.0.17",
"tracing",
"uuid",
"whoami",
@ -2124,7 +2388,7 @@ dependencies = [
"smallvec",
"sqlx-core",
"stringprep",
"thiserror",
"thiserror 2.0.17",
"tracing",
"uuid",
"whoami",
@ -2150,7 +2414,7 @@ dependencies = [
"serde",
"serde_urlencoded",
"sqlx-core",
"thiserror",
"thiserror 2.0.17",
"tracing",
"url",
"uuid",
@ -2261,13 +2525,33 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
"thiserror-impl",
"thiserror-impl 2.0.17",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
]
[[package]]
@ -2281,6 +2565,37 @@ dependencies = [
"syn 2.0.110",
]
[[package]]
name = "time"
version = "0.3.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
[[package]]
name = "time-macros"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.8.2"
@ -2376,6 +2691,27 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-websockets"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f591660438b3038dd04d16c938271c79e7e06260ad2ea2885a4861bfb238605d"
dependencies = [
"base64",
"bytes",
"futures-core",
"futures-sink",
"http",
"httparse",
"rand",
"ring",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tokio-util",
"webpki-roots 0.26.11",
]
[[package]]
name = "toml"
version = "0.9.8"
@ -2510,6 +2846,16 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tryhard"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fe58ebd5edd976e0fe0f8a14d2a04b7c81ef153ea9a54eebc42e67c2c23b4e5"
dependencies = [
"pin-project-lite",
"tokio",
]
[[package]]
name = "typenum"
version = "1.19.0"

View File

@ -9,17 +9,24 @@ authors = ["Lv, Qi <lvsoft@gmail.com>"]
name = "common_contracts"
path = "src/lib.rs"
[features]
default = ["persistence"]
persistence = ["dep:sqlx"]
[dependencies]
async-trait = "0.1.89"
async-nats = "0.45.0"
anyhow = "1.0"
tracing = "0.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
uuid = { version = "1", features = ["serde", "v4"] }
rust_decimal = { version = "1.36", features = ["serde"] }
utoipa = { version = "5.4", features = ["chrono", "uuid"] }
sqlx = { version = "0.8.6", features = [ "runtime-tokio-rustls", "postgres", "chrono", "uuid", "json", "rust_decimal" ] }
sqlx = { version = "0.8.6", features = [ "runtime-tokio-rustls", "postgres", "chrono", "uuid", "json", "rust_decimal" ], optional = true }
service_kit = { version = "0.1.2" }
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1", features = ["time", "sync", "macros"] }
log = "0.4"
tracing = "0.1"
anyhow = "1.0"

View File

@ -0,0 +1,30 @@
use async_trait::async_trait;
use crate::dtos::{CompanyProfileDto, TimeSeriesFinancialDto};
use anyhow::Result;
/// 核心业务逻辑接口:数据提供者逻辑
///
/// 该 Trait 剥离了所有基础设施关注点NATS、DB、Cache
/// 只关注如何从特定来源获取数据。
#[async_trait]
pub trait DataProviderLogic: Send + Sync {
/// Provider 的唯一标识符 (e.g., "tushare", "yfinance")
fn provider_id(&self) -> &str;
/// 检查是否支持该市场 (前置检查)
/// 默认实现为支持所有市场,特定 Provider (如 Tushare) 可覆盖此方法
fn supports_market(&self, _market: &str) -> bool {
true
}
/// 核心业务:从外部源获取原始数据并转换为标准 DTO
///
/// # Arguments
/// * `symbol` - 股票代码
///
/// # Returns
/// * `Ok((Profile, Financials))` - 成功获取的数据
/// * `Err` - 获取失败原因
async fn fetch_data(&self, symbol: &str) -> Result<(CompanyProfileDto, Vec<TimeSeriesFinancialDto>)>;
}

View File

@ -1,5 +1,6 @@
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use service_kit::api_dto;
// 单个启用的模型
@ -20,13 +21,58 @@ pub struct LlmProvider {
}
// 整个LLM Provider注册中心的数据结构
pub type LlmProvidersConfig = HashMap<String, LlmProvider>; // Key: provider_id, e.g., "openai_official"
// Key: provider_id, e.g., "openai_official"
#[api_dto]
#[serde(transparent)]
#[derive(Default)]
pub struct LlmProvidersConfig(pub HashMap<String, LlmProvider>);
impl LlmProvidersConfig {
pub fn new() -> Self {
Self(HashMap::new())
}
}
impl Deref for LlmProvidersConfig {
type Target = HashMap<String, LlmProvider>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for LlmProvidersConfig {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
// --- Analysis Module Config (NEW TEMPLATE-BASED STRUCTURE) ---
/// Top-level configuration object for all analysis templates.
/// Key: Template ID (e.g., "standard_fundamentals")
pub type AnalysisTemplateSets = HashMap<String, AnalysisTemplateSet>;
#[api_dto]
#[serde(transparent)]
#[derive(Default)]
pub struct AnalysisTemplateSets(pub HashMap<String, AnalysisTemplateSet>);
impl AnalysisTemplateSets {
pub fn new() -> Self {
Self(HashMap::new())
}
}
impl Deref for AnalysisTemplateSets {
type Target = HashMap<String, AnalysisTemplateSet>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for AnalysisTemplateSets {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
/// A single, self-contained set of analysis modules representing a complete workflow.
/// e.g., "Standard Fundamental Analysis"
@ -77,7 +123,6 @@ pub struct SystemConfig {
#[api_dto]
#[derive(PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum DataSourceProvider {
Tushare,
Finnhub,
@ -95,4 +140,26 @@ pub struct DataSourceConfig {
}
// 数据源配置集合(集中、强类型、单一来源)
pub type DataSourcesConfig = HashMap<String, DataSourceConfig>;
#[api_dto]
#[serde(transparent)]
#[derive(Default)]
pub struct DataSourcesConfig(pub HashMap<String, DataSourceConfig>);
impl DataSourcesConfig {
pub fn new() -> Self {
Self(HashMap::new())
}
}
impl Deref for DataSourcesConfig {
type Target = HashMap<String, DataSourceConfig>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for DataSourcesConfig {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

View File

@ -25,7 +25,7 @@ impl ServiceRegistrar {
/// In a real production scenario, you might want this to block until success
/// or allow the application to start and register in the background.
pub async fn register(&self) -> Result<(), reqwest::Error> {
let url = format!("{}/v1/registry/register", self.gateway_url);
let url = format!("{}/api/v1/registry/register", self.gateway_url);
let mut attempt = 0;
let max_retries = 5;
let mut delay = Duration::from_secs(2);
@ -67,7 +67,7 @@ impl ServiceRegistrar {
/// Helper to register a single time without retries (used by recovery mechanism)
async fn register_once(&self) -> Result<(), reqwest::Error> {
let url = format!("{}/v1/registry/register", self.gateway_url);
let url = format!("{}/api/v1/registry/register", self.gateway_url);
let resp = self.client.post(&url)
.json(&self.registration)
.send()
@ -83,7 +83,7 @@ impl ServiceRegistrar {
/// Requires `Arc<Self>` because it will be spawned into a static task.
pub async fn start_heartbeat_loop(self: Arc<Self>) {
let mut interval = time::interval(Duration::from_secs(10));
let heartbeat_url = format!("{}/v1/registry/heartbeat", self.gateway_url);
let heartbeat_url = format!("{}/api/v1/registry/heartbeat", self.gateway_url);
info!("Starting heartbeat loop for service: {}", self.registration.service_id);
@ -120,7 +120,7 @@ impl ServiceRegistrar {
}
pub async fn deregister(&self) -> Result<(), reqwest::Error> {
let url = format!("{}/v1/registry/deregister", self.gateway_url);
let url = format!("{}/api/v1/registry/deregister", self.gateway_url);
info!("Deregistering service: {}", self.registration.service_id);
let payload = serde_json::json!({

View File

@ -21,7 +21,6 @@ pub struct HealthStatus {
#[api_dto]
#[derive(Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ObservabilityTaskStatus {
Queued,
InProgress,

View File

@ -51,9 +51,13 @@ impl PersistenceClient {
.get(&url)
.query(&[("key", key)])
.send()
.await?
.error_for_status()?;
.await?;
if resp.status() == StatusCode::NOT_FOUND {
return Ok(None);
}
let resp = resp.error_for_status()?;
let data = resp.json().await?;
Ok(data)
}

View File

@ -1,15 +1,15 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use service_kit::api_dto;
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq)]
#[api_dto]
#[derive(PartialEq)]
pub enum ServiceStatus {
Active,
Degraded,
Maintenance,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq)]
#[serde(rename_all = "snake_case")]
#[api_dto]
#[derive(PartialEq)]
pub enum ServiceRole {
DataProvider,
ReportGenerator,
@ -18,7 +18,74 @@ pub enum ServiceRole {
Other,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
/// 字段类型枚举
#[api_dto]
#[derive(PartialEq)]
pub enum FieldType {
Text,
Password,
Url,
Boolean,
Select,
}
/// 配置键枚举 - 强类型定义所有可能的配置项
#[api_dto]
#[derive(PartialEq, Eq, Hash)]
pub enum ConfigKey {
/// API 密钥 / Token
ApiKey,
/// API Token (Tushare 等使用)
ApiToken,
/// API 基础 URL
ApiUrl,
/// 基础 URL (通用)
BaseUrl,
/// 密钥 (Secret)
SecretKey,
/// 用户名
Username,
/// 密码
Password,
/// 沙箱模式开关
SandboxMode,
/// 区域 / Region
Region,
}
/// 单个配置字段的定义
#[api_dto]
#[derive(PartialEq)]
pub struct ConfigFieldSchema {
pub key: ConfigKey,
pub label: String,
pub field_type: FieldType,
pub required: bool,
pub placeholder: Option<String>,
pub default_value: Option<String>,
pub description: Option<String>,
/// Options for 'Select' type
pub options: Option<Vec<String>>,
}
/// 服务元数据
#[api_dto]
#[derive(PartialEq)]
pub struct ProviderMetadata {
pub id: String,
pub name_en: String,
pub name_cn: String,
pub description: String,
pub icon_url: Option<String>,
/// 该服务需要的配置字段列表
pub config_schema: Vec<ConfigFieldSchema>,
/// 是否支持“测试连接”功能
pub supports_test_connection: bool,
}
#[api_dto]
pub struct ServiceRegistration {
/// Unique ID for this service instance (e.g., "tushare-provider-uuid")
pub service_id: String,
@ -30,11 +97,12 @@ pub struct ServiceRegistration {
pub base_url: String,
/// Health check endpoint
pub health_check_url: String,
/// Optional provider metadata (only for DataProvider role)
pub metadata: Option<ProviderMetadata>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[api_dto]
pub struct Heartbeat {
pub service_id: String,
pub status: ServiceStatus,
}

View File

@ -0,0 +1,176 @@
use std::sync::Arc;
use anyhow::{Result, Context};
use chrono::{Utc, Datelike};
use tokio::sync::mpsc;
use tracing::{info, error};
use uuid::Uuid;
use crate::abstraction::DataProviderLogic;
use crate::dtos::{SessionDataDto, ProviderCacheDto};
use crate::messages::{FetchCompanyDataCommand, CompanyProfilePersistedEvent, FinancialsPersistedEvent, DataFetchFailedEvent};
use crate::observability::ObservabilityTaskStatus;
use crate::persistence_client::PersistenceClient;
/// 任务状态管理器接口,用于解耦 AppState
#[async_trait::async_trait]
pub trait TaskState: Send + Sync {
fn update_status(&self, task_id: Uuid, status: ObservabilityTaskStatus, progress: u8, details: String);
fn fail_task(&self, task_id: Uuid, error: String);
fn complete_task(&self, task_id: Uuid, details: String);
fn get_nats_addr(&self) -> String;
fn get_persistence_url(&self) -> String;
}
/// 通用工作流引擎
pub struct StandardFetchWorkflow;
impl StandardFetchWorkflow {
/// 执行完整的数据获取工作流
pub async fn run<L, S>(
state: Arc<S>,
logic: Arc<L>,
command: FetchCompanyDataCommand,
completion_tx: Option<mpsc::Sender<()>>,
) -> Result<()>
where
L: DataProviderLogic + 'static,
S: TaskState + 'static,
{
let task_id = command.request_id;
let symbol = command.symbol.to_string();
let provider_id = logic.provider_id();
// 1. Market Check
if !logic.supports_market(&command.market) {
let msg = format!("Skipping: Market '{}' not supported by {}", command.market, provider_id);
info!("{}", msg);
// 如果不支持,认为是正常结束(忽略),或者也可以视具体需求报错
// 这里我们选择静默忽略或标记为 Skipped (如果 TaskStatus 支持)
// 暂时标记为 Completed 但带有说明
state.complete_task(task_id, msg);
if let Some(tx) = completion_tx { let _ = tx.send(()).await; }
return Ok(());
}
state.update_status(task_id, ObservabilityTaskStatus::InProgress, 10, "Checking cache...".to_string());
// 2. Persistence Client
let persistence = PersistenceClient::new(state.get_persistence_url());
// 3. Cache Check
let cache_key = format!("{}:{}:all", provider_id, symbol);
// Wrap in a block to catch errors and update task status
let result: Result<_, anyhow::Error> = async {
let (profile, financials) = match persistence.get_cache(&cache_key).await? {
Some(cache_entry) => {
info!("Cache HIT for {}", cache_key);
state.update_status(task_id, ObservabilityTaskStatus::InProgress, 50, "Data retrieved from cache".to_string());
serde_json::from_value(cache_entry.data_payload)
.context("Failed to deserialize cache")?
},
None => {
info!("Cache MISS for {}", cache_key);
state.update_status(task_id, ObservabilityTaskStatus::InProgress, 20, format!("Fetching from {}...", provider_id));
let (p, f) = logic.fetch_data(&symbol).await?;
// Write Back to Cache
let payload = serde_json::json!((&p, &f));
persistence.set_cache(&ProviderCacheDto {
cache_key: cache_key.clone(),
data_payload: payload,
expires_at: Utc::now() + chrono::Duration::hours(24),
updated_at: None,
}).await?;
(p, f)
}
};
state.update_status(task_id, ObservabilityTaskStatus::InProgress, 80, "Snapshotting data...".to_string());
// 4. Snapshot Session Data
persistence.insert_session_data(&SessionDataDto {
request_id: task_id,
symbol: symbol.clone(),
provider: provider_id.to_string(),
data_type: "company_profile".to_string(),
data_payload: serde_json::to_value(&profile)?,
created_at: None,
}).await?;
persistence.insert_session_data(&SessionDataDto {
request_id: task_id,
symbol: symbol.clone(),
provider: provider_id.to_string(),
data_type: "financial_statements".to_string(),
data_payload: serde_json::to_value(&financials)?,
created_at: None,
}).await?;
// 5. Publish Events
let nats_addr = state.get_nats_addr();
// Connect to NATS (Scoped connection)
let nats = async_nats::connect(&nats_addr).await
.context("Failed to connect to NATS")?;
// Event 1: Profile Persisted
let profile_event = CompanyProfilePersistedEvent {
request_id: task_id,
symbol: command.symbol.clone(),
};
nats.publish("events.data.company_profile_persisted", serde_json::to_vec(&profile_event)?.into()).await?;
// Event 2: Financials Persisted
let years: std::collections::BTreeSet<u16> = financials.iter()
.map(|f| f.period_date.year() as u16)
.collect();
let summary = format!("Fetched {} years of data", years.len());
let financials_event = FinancialsPersistedEvent {
request_id: task_id,
symbol: command.symbol.clone(),
years_updated: years.into_iter().collect(),
template_id: command.template_id.clone(),
provider_id: Some(provider_id.to_string()),
data_summary: Some(summary),
};
nats.publish("events.data.financials_persisted", serde_json::to_vec(&financials_event)?.into()).await?;
// CRITICAL: Flush to ensure messages are sent before drop
nats.flush().await.context("Failed to flush NATS")?;
Ok(())
}.await;
match result {
Ok(_) => {
info!("Workflow for {}/{} completed successfully.", provider_id, symbol);
state.complete_task(task_id, "Workflow finished successfully".to_string());
if let Some(tx) = completion_tx { let _ = tx.send(()).await; }
Ok(())
},
Err(e) => {
error!("Workflow for {}/{} failed: {}", provider_id, symbol, e);
state.fail_task(task_id, e.to_string());
// Try to report failure via NATS
if let Ok(nats) = async_nats::connect(&state.get_nats_addr()).await {
let fail_event = DataFetchFailedEvent {
request_id: task_id,
symbol: command.symbol.clone(),
error: e.to_string(),
provider_id: Some(provider_id.to_string()),
};
let _ = nats.publish("events.data.fetch_failed", serde_json::to_vec(&fail_event).unwrap().into()).await;
let _ = nats.flush().await;
}
if let Some(tx) = completion_tx { let _ = tx.send(()).await; }
Err(e)
}
}
}
}

View File

@ -64,6 +64,43 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-nats"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86dde77d8a733a9dbaf865a9eb65c72e09c88f3d14d3dd0d2aecf511920ee4fe"
dependencies = [
"base64",
"bytes",
"futures-util",
"memchr",
"nkeys",
"nuid",
"once_cell",
"pin-project",
"portable-atomic",
"rand 0.8.5",
"regex",
"ring",
"rustls-native-certs",
"rustls-pemfile",
"rustls-webpki 0.102.8",
"serde",
"serde_json",
"serde_nanos",
"serde_repr",
"thiserror 1.0.69",
"time",
"tokio",
"tokio-rustls",
"tokio-stream",
"tokio-util",
"tokio-websockets",
"tracing",
"tryhard",
"url",
]
[[package]]
name = "async-trait"
version = "0.1.89"
@ -286,6 +323,9 @@ name = "bytes"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
dependencies = [
"serde",
]
[[package]]
name = "cc"
@ -328,6 +368,8 @@ name = "common-contracts"
version = "0.1.0"
dependencies = [
"anyhow",
"async-nats",
"async-trait",
"chrono",
"log",
"reqwest",
@ -431,6 +473,32 @@ dependencies = [
"typenum",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest",
"fiat-crypto",
"rustc_version",
"subtle",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
]
[[package]]
name = "darling"
version = "0.21.3"
@ -466,6 +534,12 @@ dependencies = [
"syn 2.0.110",
]
[[package]]
name = "data-encoding"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
[[package]]
name = "data-persistence-service"
version = "0.1.2"
@ -484,7 +558,7 @@ dependencies = [
"serde_json",
"service_kit",
"sqlx",
"thiserror",
"thiserror 2.0.17",
"tokio",
"tower",
"tower-http",
@ -506,6 +580,16 @@ dependencies = [
"zeroize",
]
[[package]]
name = "deranged"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
dependencies = [
"powerfmt",
"serde_core",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
@ -552,6 +636,28 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519",
"sha2",
"signature",
"subtle",
]
[[package]]
name = "either"
version = "1.15.0"
@ -614,6 +720,12 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "find-msvc-tools"
version = "0.1.5"
@ -1385,6 +1497,21 @@ dependencies = [
"tempfile",
]
[[package]]
name = "nkeys"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879011babc47a1c7fdf5a935ae3cfe94f34645ca0cac1c7f6424b36fc743d1bf"
dependencies = [
"data-encoding",
"ed25519",
"ed25519-dalek",
"getrandom 0.2.16",
"log",
"rand 0.8.5",
"signatory",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
@ -1394,6 +1521,15 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "nuid"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc895af95856f929163a0aa20c26a78d26bfdc839f51b9d5aa7a5b79e52b7e83"
dependencies = [
"rand 0.8.5",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.6"
@ -1410,6 +1546,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
@ -1540,6 +1682,26 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pin-project"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
@ -1579,6 +1741,12 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "potential_utf"
version = "0.1.4"
@ -1588,6 +1756,12 @@ dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@ -1887,7 +2061,7 @@ dependencies = [
"serde",
"serde_json",
"sse-stream",
"thiserror",
"thiserror 2.0.17",
"tokio",
"tokio-stream",
"tokio-util",
@ -1979,6 +2153,15 @@ dependencies = [
"serde_json",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "1.1.2"
@ -2001,11 +2184,33 @@ dependencies = [
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"rustls-webpki 0.103.8",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.13.0"
@ -2015,6 +2220,16 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-webpki"
version = "0.102.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.103.8"
@ -2117,6 +2332,12 @@ dependencies = [
"libc",
]
[[package]]
name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde"
version = "1.0.228"
@ -2171,6 +2392,15 @@ dependencies = [
"serde_core",
]
[[package]]
name = "serde_nanos"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a93142f0367a4cc53ae0fead1bcda39e85beccfad3dcd717656cacab94b12985"
dependencies = [
"serde",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.20"
@ -2182,6 +2412,17 @@ dependencies = [
"serde_core",
]
[[package]]
name = "serde_repr"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
]
[[package]]
name = "serde_spanned"
version = "1.0.3"
@ -2231,7 +2472,7 @@ dependencies = [
"serde_urlencoded",
"service-kit-macros",
"syn 2.0.110",
"thiserror",
"thiserror 2.0.17",
"toml",
"utoipa",
]
@ -2282,6 +2523,18 @@ dependencies = [
"libc",
]
[[package]]
name = "signatory"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1e303f8205714074f6068773f0e29527e0453937fe837c9717d066635b65f31"
dependencies = [
"pkcs8",
"rand_core 0.6.4",
"signature",
"zeroize",
]
[[package]]
name = "signature"
version = "2.2.0"
@ -2391,7 +2644,7 @@ dependencies = [
"serde_json",
"sha2",
"smallvec",
"thiserror",
"thiserror 2.0.17",
"tokio",
"tokio-stream",
"tracing",
@ -2477,7 +2730,7 @@ dependencies = [
"smallvec",
"sqlx-core",
"stringprep",
"thiserror",
"thiserror 2.0.17",
"tracing",
"uuid",
"whoami",
@ -2517,7 +2770,7 @@ dependencies = [
"smallvec",
"sqlx-core",
"stringprep",
"thiserror",
"thiserror 2.0.17",
"tracing",
"uuid",
"whoami",
@ -2543,7 +2796,7 @@ dependencies = [
"serde",
"serde_urlencoded",
"sqlx-core",
"thiserror",
"thiserror 2.0.17",
"tracing",
"url",
"uuid",
@ -2673,13 +2926,33 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
"thiserror-impl",
"thiserror-impl 2.0.17",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.110",
]
[[package]]
@ -2702,6 +2975,37 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "time"
version = "0.3.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
[[package]]
name = "time-macros"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinystr"
version = "0.8.2"
@ -2799,6 +3103,27 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-websockets"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f591660438b3038dd04d16c938271c79e7e06260ad2ea2885a4861bfb238605d"
dependencies = [
"base64",
"bytes",
"futures-core",
"futures-sink",
"http",
"httparse",
"rand 0.8.5",
"ring",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tokio-util",
"webpki-roots 0.26.11",
]
[[package]]
name = "toml"
version = "0.9.8"
@ -2965,6 +3290,16 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tryhard"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fe58ebd5edd976e0fe0f8a14d2a04b7c81ef153ea9a54eebc42e67c2c23b4e5"
dependencies = [
"pin-project-lite",
"tokio",
]
[[package]]
name = "typenum"
version = "1.19.0"

View File

@ -21,13 +21,13 @@ COPY services/common-contracts /app/services/common-contracts
# Copy service_kit mirror again for build
COPY ref/service_kit_mirror /app/ref/service_kit_mirror
RUN cargo chef cook --release --recipe-path /app/services/data-persistence-service/recipe.json
RUN cargo chef cook --recipe-path /app/services/data-persistence-service/recipe.json
# 复制服务源码用于实际构建
COPY services/common-contracts /app/services/common-contracts
COPY services/data-persistence-service /app/services/data-persistence-service
## 为了在编译期通过 include_str! 嵌入根目录配置,将 /config 拷贝到 /app/config
COPY config /app/config
RUN cargo build --release --bin data-persistence-service-server
RUN cargo build --bin data-persistence-service-server
FROM debian:bookworm-slim AS runtime
WORKDIR /app
@ -35,7 +35,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates
RUN groupadd --system --gid 1001 appuser && \
useradd --system --uid 1001 --gid 1001 appuser
USER appuser
COPY --from=builder /app/services/data-persistence-service/target/release/data-persistence-service-server /usr/local/bin/data-persistence-service-server
COPY --from=builder /app/services/data-persistence-service/target/debug/data-persistence-service-server /usr/local/bin/data-persistence-service-server
COPY services/data-persistence-service/migrations ./migrations
ENV HOST=0.0.0.0
ENV PORT=3000

View File

@ -22,12 +22,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@ -103,15 +97,6 @@ dependencies = [
"syn 2.0.110",
]
[[package]]
name = "atoi"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
dependencies = [
"num-traits",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@ -269,12 +254,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.0"
@ -325,6 +304,8 @@ name = "common-contracts"
version = "0.1.0"
dependencies = [
"anyhow",
"async-nats",
"async-trait",
"chrono",
"log",
"reqwest",
@ -332,22 +313,12 @@ dependencies = [
"serde",
"serde_json",
"service_kit",
"sqlx",
"tokio",
"tracing",
"utoipa",
"uuid",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "config"
version = "0.15.19"
@ -428,30 +399,6 @@ dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@ -548,9 +495,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
"subtle",
]
[[package]]
@ -573,12 +518,6 @@ dependencies = [
"const-random",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dyn-clone"
version = "1.0.20"
@ -607,15 +546,6 @@ dependencies = [
"subtle",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
dependencies = [
"serde",
]
[[package]]
name = "encoding_rs"
version = "0.8.35"
@ -652,28 +582,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "etcetera"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
dependencies = [
"cfg-if",
"home",
"windows-sys 0.48.0",
]
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "fastrand"
version = "2.3.0"
@ -698,19 +606,15 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-nats",
"async-trait",
"axum",
"chrono",
"common-contracts",
"config",
"dashmap",
"futures",
"futures-util",
"itertools",
"reqwest",
"rust_decimal",
"rust_decimal_macros",
"secrecy",
"serde",
"serde_json",
"thiserror 2.0.17",
@ -722,17 +626,6 @@ dependencies = [
"uuid",
]
[[package]]
name = "flume"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
dependencies = [
"futures-core",
"futures-sink",
"spin",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -775,21 +668,6 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@ -797,7 +675,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@ -806,34 +683,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-intrusive"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
dependencies = [
"futures-core",
"lock_api",
"parking_lot",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
@ -863,13 +712,10 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
@ -948,8 +794,6 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
@ -974,39 +818,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
dependencies = [
"hmac",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "home"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "http"
version = "1.3.1"
@ -1297,15 +1108,6 @@ dependencies = [
"serde",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.15"
@ -1338,9 +1140,6 @@ name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin",
]
[[package]]
name = "libc"
@ -1348,33 +1147,6 @@ version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libm"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "libredox"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags",
"libc",
"redox_syscall",
]
[[package]]
name = "libsqlite3-sys"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@ -1417,16 +1189,6 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "md-5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest",
]
[[package]]
name = "memchr"
version = "2.7.6"
@ -1500,48 +1262,12 @@ dependencies = [
"rand",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
dependencies = [
"lazy_static",
"libm",
"num-integer",
"num-iter",
"num-traits",
"rand",
"smallvec",
"zeroize",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -1549,7 +1275,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@ -1612,12 +1337,6 @@ dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.5"
@ -1737,17 +1456,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"pkcs8",
"spki",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
@ -2047,26 +1755,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "rsa"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
dependencies = [
"const-oid",
"digest",
"num-bigint-dig",
"num-integer",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core",
"signature",
"spki",
"subtle",
"zeroize",
]
[[package]]
name = "rust-ini"
version = "0.21.3"
@ -2250,16 +1938,6 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "secrecy"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
dependencies = [
"serde",
"zeroize",
]
[[package]]
name = "security-framework"
version = "2.11.1"
@ -2443,17 +2121,6 @@ dependencies = [
"utoipa",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
@ -2528,9 +2195,6 @@ name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [
"serde",
]
[[package]]
name = "socket2"
@ -2542,15 +2206,6 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "spki"
version = "0.7.3"
@ -2561,224 +2216,12 @@ dependencies = [
"der",
]
[[package]]
name = "sqlx"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
dependencies = [
"sqlx-core",
"sqlx-macros",
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
]
[[package]]
name = "sqlx-core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
dependencies = [
"base64",
"bytes",
"chrono",
"crc",
"crossbeam-queue",
"either",
"event-listener",
"futures-core",
"futures-intrusive",
"futures-io",
"futures-util",
"hashbrown 0.15.5",
"hashlink",
"indexmap",
"log",
"memchr",
"once_cell",
"percent-encoding",
"rust_decimal",
"rustls",
"serde",
"serde_json",
"sha2",
"smallvec",
"thiserror 2.0.17",
"tokio",
"tokio-stream",
"tracing",
"url",
"uuid",
"webpki-roots 0.26.11",
]
[[package]]
name = "sqlx-macros"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
dependencies = [
"proc-macro2",
"quote",
"sqlx-core",
"sqlx-macros-core",
"syn 2.0.110",
]
[[package]]
name = "sqlx-macros-core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
dependencies = [
"dotenvy",
"either",
"heck",
"hex",
"once_cell",
"proc-macro2",
"quote",
"serde",
"serde_json",
"sha2",
"sqlx-core",
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
"syn 2.0.110",
"tokio",
"url",
]
[[package]]
name = "sqlx-mysql"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
dependencies = [
"atoi",
"base64",
"bitflags",
"byteorder",
"bytes",
"chrono",
"crc",
"digest",
"dotenvy",
"either",
"futures-channel",
"futures-core",
"futures-io",
"futures-util",
"generic-array",
"hex",
"hkdf",
"hmac",
"itoa",
"log",
"md-5",
"memchr",
"once_cell",
"percent-encoding",
"rand",
"rsa",
"rust_decimal",
"serde",
"sha1",
"sha2",
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.17",
"tracing",
"uuid",
"whoami",
]
[[package]]
name = "sqlx-postgres"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
dependencies = [
"atoi",
"base64",
"bitflags",
"byteorder",
"chrono",
"crc",
"dotenvy",
"etcetera",
"futures-channel",
"futures-core",
"futures-util",
"hex",
"hkdf",
"hmac",
"home",
"itoa",
"log",
"md-5",
"memchr",
"once_cell",
"rand",
"rust_decimal",
"serde",
"serde_json",
"sha2",
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.17",
"tracing",
"uuid",
"whoami",
]
[[package]]
name = "sqlx-sqlite"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
dependencies = [
"atoi",
"chrono",
"flume",
"futures-channel",
"futures-core",
"futures-executor",
"futures-intrusive",
"futures-util",
"libsqlite3-sys",
"log",
"percent-encoding",
"serde",
"serde_urlencoded",
"sqlx-core",
"thiserror 2.0.17",
"tracing",
"url",
"uuid",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "stringprep"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
dependencies = [
"unicode-bidi",
"unicode-normalization",
"unicode-properties",
]
[[package]]
name = "subtle"
version = "2.6.1"
@ -3267,33 +2710,12 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unicode-bidi"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-normalization"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-properties"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
@ -3403,12 +2825,6 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.105"
@ -3495,16 +2911,6 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "whoami"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
dependencies = [
"libredox",
"wasite",
]
[[package]]
name = "windows-core"
version = "0.62.2"
@ -3575,15 +2981,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@ -3611,21 +3008,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@ -3659,12 +3041,6 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@ -3677,12 +3053,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@ -3695,12 +3065,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@ -3725,12 +3089,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@ -3743,12 +3101,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@ -3761,12 +3113,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@ -3779,12 +3125,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"

View File

@ -10,7 +10,7 @@ tokio = { version = "1.0", features = ["full"] }
tower-http = { version = "0.6.6", features = ["cors"] }
# Shared Contracts
common-contracts = { path = "../common-contracts" }
common-contracts = { path = "../common-contracts", default-features = false }
# Generic MCP Client
reqwest = { version = "0.12.24", features = ["json"] }
@ -18,17 +18,14 @@ url = "2.5.2"
chrono = { version = "0.4.38", features = ["serde"] }
rust_decimal = "1.35.0"
rust_decimal_macros = "1.35.0"
itertools = "0.14.0"
# Message Queue (NATS)
async-nats = "0.45.0"
futures = "0.3"
futures-util = "0.3.31"
# Data Persistence Client
# Concurrency & Async
async-trait = "0.1.80"
dashmap = "6.1.0"
uuid = { version = "1.6", features = ["v4", "serde"] }
@ -42,7 +39,6 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Configuration
config = "0.15.19"
secrecy = { version = "0.10.3", features = ["serde"] }
# Error Handling
thiserror = "2.0.17"

View File

@ -6,7 +6,7 @@ WORKDIR /usr/src/app
COPY ./services/common-contracts /usr/src/app/services/common-contracts
COPY ./services/finnhub-provider-service /usr/src/app/services/finnhub-provider-service
WORKDIR /usr/src/app/services/finnhub-provider-service
RUN cargo build --release --bin finnhub-provider-service
RUN cargo build --bin finnhub-provider-service
# 2. Runtime Stage
FROM debian:bookworm-slim
@ -18,7 +18,7 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/*
# Copy the built binary from the builder stage
COPY --from=builder /usr/src/app/services/finnhub-provider-service/target/release/finnhub-provider-service /usr/local/bin/
COPY --from=builder /usr/src/app/services/finnhub-provider-service/target/debug/finnhub-provider-service /usr/local/bin/
# Set the binary as the entrypoint
ENTRYPOINT ["/usr/local/bin/finnhub-provider-service"]

View File

@ -1,4 +1,3 @@
use secrecy::SecretString;
use serde::Deserialize;
#[derive(Debug, Deserialize, Clone)]
@ -7,7 +6,7 @@ pub struct AppConfig {
pub nats_addr: String,
pub data_persistence_service_url: String,
pub finnhub_api_url: String,
pub finnhub_api_key: Option<SecretString>,
pub finnhub_api_key: Option<String>,
// New fields
pub api_gateway_url: String,

View File

@ -1,7 +1,6 @@
use crate::error::Result;
use crate::state::AppState;
use common_contracts::config_models::{DataSourceConfig, DataSourceProvider};
use secrecy::SecretString;
use std::collections::HashMap;
use std::time::Duration;
use tracing::{error, info, instrument};
@ -42,7 +41,7 @@ async fn poll_and_update_config(state: &AppState) -> Result<()> {
if let Some(config) = finnhub_config {
if let Some(api_key) = &config.api_key {
state.update_provider(
Some(SecretString::from(api_key.clone())),
Some(api_key.clone()),
config.api_url.clone()
).await;
info!("Successfully updated Finnhub provider with new configuration.");

View File

@ -1,4 +1,3 @@
use anyhow::anyhow;
use reqwest::Error as ReqwestError;
use thiserror::Error;

View File

@ -13,9 +13,9 @@ mod config_poller;
use crate::config::AppConfig;
use crate::error::Result;
use crate::state::AppState;
use tracing::{info, warn};
use tracing::info;
use common_contracts::lifecycle::ServiceRegistrar;
use common_contracts::registry::ServiceRegistration;
use common_contracts::registry::{ServiceRegistration, ProviderMetadata, ConfigFieldSchema, FieldType, ConfigKey};
use std::sync::Arc;
#[tokio::main]
@ -52,6 +52,26 @@ async fn main() -> Result<()> {
role: common_contracts::registry::ServiceRole::DataProvider,
base_url: format!("http://{}:{}", config.service_host, port),
health_check_url: format!("http://{}:{}/health", config.service_host, port),
metadata: Some(ProviderMetadata {
id: "finnhub".to_string(),
name_en: "Finnhub".to_string(),
name_cn: "Finnhub".to_string(),
description: "Finnhub Stock API".to_string(),
icon_url: None,
config_schema: vec![
ConfigFieldSchema {
key: ConfigKey::ApiKey,
label: "API Key".to_string(),
field_type: FieldType::Password,
required: true,
placeholder: Some("Enter your API key...".to_string()),
default_value: None,
description: Some("Get it from https://finnhub.io".to_string()),
options: None,
},
],
supports_test_connection: true,
}),
}
);

View File

@ -1,5 +1,4 @@
use common_contracts::observability::TaskProgress;
use secrecy::{ExposeSecret, SecretString};
use std::sync::Arc;
use tokio::sync::RwLock;
@ -28,7 +27,7 @@ impl AppState {
if let Some(api_key) = config.finnhub_api_key.as_ref() {
let provider = FinnhubDataProvider::new(
config.finnhub_api_url.clone(),
api_key.expose_secret().to_string(),
api_key.to_string(),
);
(Some(provider), ServiceOperationalStatus::Active)
} else {
@ -52,7 +51,7 @@ impl AppState {
self.finnhub_provider.read().await.clone()
}
pub async fn update_provider(&self, api_key: Option<SecretString>, api_url: Option<String>) {
pub async fn update_provider(&self, api_key: Option<String>, api_url: Option<String>) {
let mut provider_guard = self.finnhub_provider.write().await;
let mut status_guard = self.status.write().await;
@ -63,7 +62,7 @@ impl AppState {
if let Some(key) = api_key {
let new_provider = FinnhubDataProvider::new(
final_url,
key.expose_secret().to_string(),
key,
);
*provider_guard = Some(new_provider);
*status_guard = ServiceOperationalStatus::Active;

View File

@ -22,12 +22,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@ -162,15 +156,6 @@ dependencies = [
"syn 2.0.110",
]
[[package]]
name = "atoi"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
dependencies = [
"num-traits",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@ -352,12 +337,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.0"
@ -430,6 +409,8 @@ name = "common-contracts"
version = "0.1.0"
dependencies = [
"anyhow",
"async-nats",
"async-trait",
"chrono",
"log",
"reqwest",
@ -437,22 +418,12 @@ dependencies = [
"serde",
"serde_json",
"service_kit",
"sqlx",
"tokio",
"tracing",
"utoipa",
"uuid",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "config"
version = "0.15.19"
@ -543,21 +514,6 @@ dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
@ -577,15 +533,6 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@ -754,9 +701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
"subtle",
]
[[package]]
@ -779,12 +724,6 @@ dependencies = [
"const-random",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dyn-clone"
version = "1.0.20"
@ -813,15 +752,6 @@ dependencies = [
"subtle",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
dependencies = [
"serde",
]
[[package]]
name = "encoding_rs"
version = "0.8.35"
@ -858,28 +788,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "etcetera"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
dependencies = [
"cfg-if",
"home",
"windows-sys 0.48.0",
]
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "eventsource-stream"
version = "0.2.3"
@ -915,17 +823,6 @@ version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flume"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
dependencies = [
"futures-core",
"futures-sink",
"spin",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -1010,17 +907,6 @@ dependencies = [
"futures-util",
]
[[package]]
name = "futures-intrusive"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
dependencies = [
"futures-core",
"lock_api",
"parking_lot",
]
[[package]]
name = "futures-io"
version = "0.3.31"
@ -1175,8 +1061,6 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
@ -1201,39 +1085,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
dependencies = [
"hmac",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "home"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "http"
version = "1.3.1"
@ -1598,9 +1449,6 @@ name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin",
]
[[package]]
name = "libc"
@ -1614,27 +1462,6 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "libredox"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags",
"libc",
"redox_syscall",
]
[[package]]
name = "libsqlite3-sys"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@ -1683,16 +1510,6 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "md-5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest",
]
[[package]]
name = "memchr"
version = "2.7.6"
@ -1792,48 +1609,12 @@ dependencies = [
"rand 0.8.5",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
dependencies = [
"lazy_static",
"libm",
"num-integer",
"num-iter",
"num-traits",
"rand 0.8.5",
"smallvec",
"zeroize",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -1841,7 +1622,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@ -1904,12 +1684,6 @@ dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.5"
@ -2088,17 +1862,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"pkcs8",
"spki",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
@ -2403,7 +2166,6 @@ dependencies = [
"futures-util",
"petgraph",
"reqwest",
"secrecy",
"serde",
"serde_json",
"tera",
@ -2537,26 +2299,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "rsa"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
dependencies = [
"const-oid",
"digest",
"num-bigint-dig",
"num-integer",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core 0.6.4",
"signature",
"spki",
"subtle",
"zeroize",
]
[[package]]
name = "rust-ini"
version = "0.21.3"
@ -2964,17 +2706,6 @@ dependencies = [
"utoipa",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
@ -3065,9 +2796,6 @@ name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [
"serde",
]
[[package]]
name = "socket2"
@ -3079,15 +2807,6 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "spki"
version = "0.7.3"
@ -3098,224 +2817,12 @@ dependencies = [
"der",
]
[[package]]
name = "sqlx"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
dependencies = [
"sqlx-core",
"sqlx-macros",
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
]
[[package]]
name = "sqlx-core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
dependencies = [
"base64",
"bytes",
"chrono",
"crc",
"crossbeam-queue",
"either",
"event-listener",
"futures-core",
"futures-intrusive",
"futures-io",
"futures-util",
"hashbrown 0.15.5",
"hashlink",
"indexmap",
"log",
"memchr",
"once_cell",
"percent-encoding",
"rust_decimal",
"rustls",
"serde",
"serde_json",
"sha2",
"smallvec",
"thiserror 2.0.17",
"tokio",
"tokio-stream",
"tracing",
"url",
"uuid",
"webpki-roots 0.26.11",
]
[[package]]
name = "sqlx-macros"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
dependencies = [
"proc-macro2",
"quote",
"sqlx-core",
"sqlx-macros-core",
"syn 2.0.110",
]
[[package]]
name = "sqlx-macros-core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
dependencies = [
"dotenvy",
"either",
"heck",
"hex",
"once_cell",
"proc-macro2",
"quote",
"serde",
"serde_json",
"sha2",
"sqlx-core",
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
"syn 2.0.110",
"tokio",
"url",
]
[[package]]
name = "sqlx-mysql"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
dependencies = [
"atoi",
"base64",
"bitflags",
"byteorder",
"bytes",
"chrono",
"crc",
"digest",
"dotenvy",
"either",
"futures-channel",
"futures-core",
"futures-io",
"futures-util",
"generic-array",
"hex",
"hkdf",
"hmac",
"itoa",
"log",
"md-5",
"memchr",
"once_cell",
"percent-encoding",
"rand 0.8.5",
"rsa",
"rust_decimal",
"serde",
"sha1",
"sha2",
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.17",
"tracing",
"uuid",
"whoami",
]
[[package]]
name = "sqlx-postgres"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
dependencies = [
"atoi",
"base64",
"bitflags",
"byteorder",
"chrono",
"crc",
"dotenvy",
"etcetera",
"futures-channel",
"futures-core",
"futures-util",
"hex",
"hkdf",
"hmac",
"home",
"itoa",
"log",
"md-5",
"memchr",
"once_cell",
"rand 0.8.5",
"rust_decimal",
"serde",
"serde_json",
"sha2",
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.17",
"tracing",
"uuid",
"whoami",
]
[[package]]
name = "sqlx-sqlite"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
dependencies = [
"atoi",
"chrono",
"flume",
"futures-channel",
"futures-core",
"futures-executor",
"futures-intrusive",
"futures-util",
"libsqlite3-sys",
"log",
"percent-encoding",
"serde",
"serde_urlencoded",
"sqlx-core",
"thiserror 2.0.17",
"tracing",
"url",
"uuid",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "stringprep"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
dependencies = [
"unicode-bidi",
"unicode-normalization",
"unicode-properties",
]
[[package]]
name = "strsim"
version = "0.11.1"
@ -3838,33 +3345,12 @@ version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-bidi"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-normalization"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-properties"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
@ -3984,12 +3470,6 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.105"
@ -4099,16 +3579,6 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "whoami"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
dependencies = [
"libredox",
"wasite",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
@ -4188,15 +3658,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@ -4224,21 +3685,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@ -4272,12 +3718,6 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@ -4290,12 +3730,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@ -4308,12 +3742,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@ -4338,12 +3766,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@ -4356,12 +3778,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@ -4374,12 +3790,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@ -4392,12 +3802,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"

View File

@ -10,7 +10,7 @@ tokio = { version = "1.0", features = ["full"] }
tower-http = { version = "0.6.6", features = ["cors"] }
# Shared Contracts
common-contracts = { path = "../common-contracts" }
common-contracts = { path = "../common-contracts", default-features = false }
# Message Queue (NATS)
async-nats = "0.45.0"
@ -34,7 +34,6 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Configuration
config = "0.15.19"
secrecy = { version = "0.10.3", features = ["serde"] }
# Error Handling
thiserror = "2.0.17"

View File

@ -6,7 +6,7 @@ WORKDIR /usr/src/app
COPY ./services/common-contracts /usr/src/app/services/common-contracts
COPY ./services/report-generator-service /usr/src/app/services/report-generator-service
WORKDIR /usr/src/app/services/report-generator-service
RUN cargo build --release --bin report-generator-service
RUN cargo build --bin report-generator-service
# 2. Runtime Stage
FROM debian:bookworm-slim
@ -18,7 +18,7 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/*
# Copy the built binary from the builder stage
COPY --from=builder /usr/src/app/services/report-generator-service/target/release/report-generator-service /usr/local/bin/
COPY --from=builder /usr/src/app/services/report-generator-service/target/debug/report-generator-service /usr/local/bin/
# Set the binary as the entrypoint
ENTRYPOINT ["/usr/local/bin/report-generator-service"]

View File

@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_.yahoo.com TRUE / TRUE 1795460464 A3 d=AQABBJAFI2kCEFyo9wFOwa9gfLyDlxwRTUwFEgEBAQFXJGksadwr0iMA_eMCAA&S=AQAAAp_WN63XGehRRIkl037YEMg

View File

@ -8,7 +8,6 @@ use axum::{
};
use common_contracts::observability::{HealthStatus, ServiceStatus, TaskProgress};
use serde::Deserialize;
use secrecy::SecretString;
use uuid::Uuid;
use crate::state::AppState;
use crate::llm_client::LlmClient;
@ -63,7 +62,7 @@ async fn test_llm_connection(
) -> Result<Json<String>, (StatusCode, String)> {
let client = LlmClient::new(
payload.api_base_url,
SecretString::from(payload.api_key),
payload.api_key,
payload.model_id,
None,
);

View File

@ -1,5 +1,4 @@
use crate::error::ProviderError;
use secrecy::{ExposeSecret, SecretString};
use tracing::{debug, error, info, warn};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
use serde_json::{json, Value};
@ -10,13 +9,13 @@ use std::time::Duration;
pub struct LlmClient {
http_client: reqwest::Client,
api_base_url: String,
api_key: SecretString,
api_key: String,
model: String,
timeout: Duration,
}
impl LlmClient {
pub fn new(api_url: String, api_key: SecretString, model: String, timeout_secs: Option<u64>) -> Self {
pub fn new(api_url: String, api_key: String, model: String, timeout_secs: Option<u64>) -> Self {
let api_url = api_url.trim();
// Normalize base URL
@ -56,7 +55,7 @@ impl LlmClient {
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
let api_key_val = self.api_key.expose_secret();
let api_key_val = &self.api_key;
let auth_value = format!("Bearer {}", api_key_val);
if let Ok(val) = HeaderValue::from_str(&auth_value) {
headers.insert(AUTHORIZATION, val);

View File

@ -12,7 +12,7 @@ mod formatter;
use crate::config::AppConfig;
use crate::error::{ProviderError, Result};
use crate::state::AppState;
use tracing::{info, warn};
use tracing::info;
use common_contracts::lifecycle::ServiceRegistrar;
use common_contracts::registry::{ServiceRegistration, ServiceRole};
use std::sync::Arc;
@ -64,6 +64,7 @@ async fn main() -> Result<()> {
role: ServiceRole::ReportGenerator,
base_url: format!("http://{}:{}", service_host, port),
health_check_url: format!("http://{}:{}/health", service_host, port),
metadata: None,
}
);

View File

@ -353,15 +353,23 @@ fn create_llm_client_for_module(
llm_providers: &LlmProvidersConfig,
module_config: &AnalysisModuleConfig,
) -> Result<LlmClient> {
let provider = llm_providers.get(&module_config.provider_id).ok_or_else(|| {
if module_config.provider_id.is_empty() {
return Err(ProviderError::Configuration(format!(
"Module '{}' has empty provider_id",
module_config.name
)));
}
let provider_id = &module_config.provider_id;
let provider = llm_providers.get(provider_id).ok_or_else(|| {
ProviderError::Configuration(format!(
"Provider '{}' not found for module '{}'",
module_config.provider_id, module_config.name
provider_id, module_config.name
))
})?;
let api_url = provider.api_base_url.clone();
info!("Creating LLM client for module '{}' using provider '{}' with URL: '{}'", module_config.name, module_config.provider_id, api_url);
info!("Creating LLM client for module '{}' using provider '{}' with URL: '{}'", module_config.name, provider_id, api_url);
Ok(LlmClient::new(
api_url,

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@ WORKDIR /usr/src/app
COPY ./services/common-contracts /usr/src/app/services/common-contracts
COPY ./services/tushare-provider-service /usr/src/app/services/tushare-provider-service
WORKDIR /usr/src/app/services/tushare-provider-service
RUN cargo build --release --bin tushare-provider-service
RUN cargo build --bin tushare-provider-service
# 2. Runtime Stage
FROM debian:bookworm-slim
@ -18,7 +18,7 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/*
# Copy the built binary from the builder stage
COPY --from=builder /usr/src/app/services/tushare-provider-service/target/release/tushare-provider-service /usr/local/bin/
COPY --from=builder /usr/src/app/services/tushare-provider-service/target/debug/tushare-provider-service /usr/local/bin/
# Set the binary as the entrypoint
ENTRYPOINT ["/usr/local/bin/tushare-provider-service"]

View File

@ -1,7 +1,6 @@
use std::collections::HashMap;
use axum::{routing::{get, post}, Router, extract::State, response::{Json, IntoResponse}, http::StatusCode};
use serde::Deserialize;
use secrecy::ExposeSecret;
use crate::ts_client::TushareClient;
use crate::state::{AppState, ServiceOperationalStatus};
use common_contracts::observability::{HealthStatus, ServiceStatus};
@ -31,7 +30,7 @@ async fn test_connection(
let api_key = if let Some(k) = payload.api_key.filter(|s| !s.is_empty()) {
k
} else if let Some(k) = &state.config.tushare_api_token {
k.expose_secret().clone()
k.clone()
} else {
return (
StatusCode::BAD_REQUEST,

View File

@ -1,5 +1,4 @@
use serde::Deserialize;
use secrecy::SecretString;
#[derive(Debug, Deserialize, Clone)]
pub struct AppConfig {
@ -7,7 +6,7 @@ pub struct AppConfig {
pub nats_addr: String,
pub data_persistence_service_url: String,
pub tushare_api_url: String,
pub tushare_api_token: Option<SecretString>,
pub tushare_api_token: Option<String>,
// New fields for dynamic registration
pub api_gateway_url: String,

View File

@ -1,7 +1,6 @@
use crate::error::Result;
use crate::state::AppState;
use common_contracts::config_models::{DataSourceConfig, DataSourceProvider};
use secrecy::SecretString;
use std::collections::HashMap;
use std::time::Duration;
use tracing::{error, info, instrument};
@ -42,7 +41,7 @@ async fn poll_and_update_config(state: &AppState) -> Result<()> {
if let Some(config) = tushare_config {
if let Some(api_key) = &config.api_key {
state.update_provider(
Some(SecretString::from(api_key.clone())),
Some(api_key.clone().into()),
config.api_url.clone()
).await;
info!("Successfully updated Tushare provider with new configuration.");

View File

@ -1,4 +1,3 @@
use anyhow::anyhow;
use reqwest::Error as ReqwestError;
use thiserror::Error;

View File

@ -1,11 +1,12 @@
use crate::config::AppConfig;
use crate::tushare::TushareDataProvider;
use common_contracts::observability::TaskProgress;
use common_contracts::observability::{TaskProgress, ObservabilityTaskStatus};
use common_contracts::workflow_harness::TaskState;
use dashmap::DashMap;
use secrecy::{ExposeSecret, SecretString};
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
use secrecy::SecretString;
#[derive(Clone, Debug)]
pub enum ServiceOperationalStatus {
@ -27,7 +28,7 @@ impl AppState {
if let Some(api_key) = config.tushare_api_token.as_ref() {
let provider = TushareDataProvider::new(
config.tushare_api_url.clone(),
api_key.expose_secret().clone(),
api_key.clone(),
);
(Some(provider), ServiceOperationalStatus::Active)
} else {
@ -61,6 +62,7 @@ impl AppState {
.unwrap_or_else(|| "http://api.tushare.pro".to_string());
if let Some(key) = api_key {
use secrecy::ExposeSecret;
let new_provider = TushareDataProvider::new(
final_url,
key.expose_secret().clone(),
@ -75,3 +77,38 @@ impl AppState {
}
}
}
// Implement TaskState trait for AppState
#[async_trait::async_trait]
impl TaskState for AppState {
fn update_status(&self, task_id: Uuid, status: ObservabilityTaskStatus, progress: u8, details: String) {
if let Some(mut task) = self.tasks.get_mut(&task_id) {
task.status = status;
task.progress_percent = progress;
task.details = details;
}
}
fn fail_task(&self, task_id: Uuid, error: String) {
if let Some(mut task) = self.tasks.get_mut(&task_id) {
task.status = ObservabilityTaskStatus::Failed;
task.details = format!("Failed: {}", error);
}
}
fn complete_task(&self, task_id: Uuid, details: String) {
if let Some(mut task) = self.tasks.get_mut(&task_id) {
task.status = ObservabilityTaskStatus::Completed;
task.progress_percent = 100;
task.details = details;
}
}
fn get_nats_addr(&self) -> String {
self.config.nats_addr.clone()
}
fn get_persistence_url(&self) -> String {
self.config.data_persistence_service_url.clone()
}
}

View File

@ -1,241 +1,63 @@
use std::sync::Arc;
use tokio::sync::mpsc;
use common_contracts::{
dtos::{CompanyProfileDto, TimeSeriesFinancialDto, SessionDataDto, ProviderCacheDto},
messages::{CompanyProfilePersistedEvent, FetchCompanyDataCommand, FinancialsPersistedEvent, DataFetchFailedEvent},
observability::ObservabilityTaskStatus,
persistence_client::PersistenceClient,
messages::FetchCompanyDataCommand,
workflow_harness::StandardFetchWorkflow,
abstraction::DataProviderLogic,
dtos::{CompanyProfileDto, TimeSeriesFinancialDto},
};
use tokio::sync::mpsc;
use tracing::{info, error};
use chrono::{Datelike, Utc, Duration};
use crate::{error::AppError, state::AppState};
/// Tushare 核心业务逻辑实现
pub struct TushareFetcher {
state: Arc<AppState>,
}
impl TushareFetcher {
pub fn new(state: Arc<AppState>) -> Self {
Self { state }
}
}
#[async_trait::async_trait]
impl DataProviderLogic for TushareFetcher {
fn provider_id(&self) -> &str {
"tushare"
}
fn supports_market(&self, market: &str) -> bool {
// Tushare 仅支持中国市场
market.to_uppercase() == "CN"
}
async fn fetch_data(&self, symbol: &str) -> anyhow::Result<(CompanyProfileDto, Vec<TimeSeriesFinancialDto>)> {
let provider_option = self.state.get_provider().await;
let provider = provider_option.ok_or_else(|| anyhow::anyhow!("Tushare provider not configured"))?;
let (profile, financials) = provider.fetch_all_data(symbol).await
.map_err(|e| anyhow::anyhow!("Tushare API error: {}", e))?;
Ok((profile, financials))
}
}
pub async fn run_tushare_workflow(
state: Arc<AppState>,
command: FetchCompanyDataCommand,
completion_tx: mpsc::Sender<()>,
) -> Result<(), AppError> {
let task_id = command.request_id;
let symbol = command.symbol.clone();
let fetcher = Arc::new(TushareFetcher::new(state.clone()));
match run_tushare_workflow_inner(state.clone(), &command).await {
Ok(_) => {
info!("Tushare workflow for symbol {} completed successfully.", symbol);
let _ = completion_tx.send(()).await;
Ok(())
}
Err(e) => {
error!("Tushare workflow for symbol {} failed: {}", symbol, e);
// 使用通用工作流引擎
StandardFetchWorkflow::run(
state.clone(),
fetcher,
command,
Some(completion_tx)
).await.map_err(|e| AppError::Internal(e.to_string()))?;
// Try to publish failure event
if let Ok(nats_client) = async_nats::connect(&state.config.nats_addr).await {
let _ = publish_failure_event(&nats_client, &command, e.to_string()).await;
} else {
error!("Failed to connect to NATS to report failure for {}", symbol);
}
// Ensure task status is failed in memory if it wasn't already
if let Some(mut task) = state.tasks.get_mut(&task_id) {
if task.status != ObservabilityTaskStatus::Failed {
task.status = ObservabilityTaskStatus::Failed;
task.details = format!("Workflow failed: {}", e);
}
}
let _ = completion_tx.send(()).await;
Err(e)
}
}
}
async fn run_tushare_workflow_inner(
state: Arc<AppState>,
command: &FetchCompanyDataCommand,
) -> Result<Vec<TimeSeriesFinancialDto>, AppError> {
let task_id = command.request_id;
let symbol = command.symbol.clone();
let provider = match state.get_provider().await {
Some(p) => p,
None => {
let reason = "Execution failed: Tushare provider is not available (misconfigured).".to_string();
error!("{}", reason);
if let Some(mut task) = state.tasks.get_mut(&task_id) {
task.status = ObservabilityTaskStatus::Failed;
task.details = reason.clone();
}
return Err(AppError::ProviderNotAvailable(reason));
}
};
let persistence_client = PersistenceClient::new(state.config.data_persistence_service_url.clone());
// 1. Update task progress: Checking Cache
{
let mut entry = state
.tasks
.get_mut(&task_id)
.ok_or_else(|| AppError::Internal("Task not found".to_string()))?;
entry.status = ObservabilityTaskStatus::InProgress;
entry.progress_percent = 10;
entry.details = "Checking cache...".to_string();
}
// Cache Key: tushare:{symbol}:all
let cache_key = format!("tushare:{}:all", symbol);
let (profile, financials) = match persistence_client.get_cache(&cache_key).await.map_err(|e| AppError::Internal(e.to_string()))? {
Some(cache_entry) => {
info!("Cache HIT for {}", cache_key);
// Deserialize
let data: (CompanyProfileDto, Vec<TimeSeriesFinancialDto>) = serde_json::from_value(cache_entry.data_payload)
.map_err(|e| AppError::Internal(format!("Failed to deserialize cache: {}", e)))?;
{
let mut entry = state.tasks.get_mut(&task_id).unwrap();
entry.details = "Data retrieved from cache".to_string();
entry.progress_percent = 50;
}
data
},
None => {
info!("Cache MISS for {}", cache_key);
{
let mut entry = state.tasks.get_mut(&task_id).unwrap();
entry.details = "Fetching from Tushare API...".to_string();
entry.progress_percent = 20;
}
let (p, f) = provider.fetch_all_data(symbol.as_str()).await?;
// Write to Cache
let payload = serde_json::json!((&p, &f));
persistence_client.set_cache(&ProviderCacheDto {
cache_key,
data_payload: payload,
expires_at: Utc::now() + Duration::hours(24),
updated_at: None,
}).await.map_err(|e| AppError::Internal(e.to_string()))?;
(p, f)
}
};
// 2. Snapshot to Session Data (and update Global Profile)
{
let mut entry = state.tasks.get_mut(&task_id).unwrap();
entry.details = "Snapshotting data...".to_string();
entry.progress_percent = 80;
}
// Global Profile (Optional, but good for search)
// Ignore errors here as it's secondary
// REMOVED: upsert_company_profile is deprecated.
// let _ = persistence_client.upsert_company_profile(profile.clone()).await;
// Snapshot Profile
persistence_client.insert_session_data(&SessionDataDto {
request_id: task_id,
symbol: symbol.clone().into(), // CanonicalSymbol to string
provider: "tushare".to_string(),
data_type: "company_profile".to_string(),
data_payload: serde_json::to_value(&profile).unwrap(),
created_at: None,
}).await.map_err(|e| AppError::Internal(e.to_string()))?;
// Snapshot Financials
persistence_client.insert_session_data(&SessionDataDto {
request_id: task_id,
symbol: symbol.clone().into(),
provider: "tushare".to_string(),
data_type: "financial_statements".to_string(),
data_payload: serde_json::to_value(&financials).unwrap(),
created_at: None,
}).await.map_err(|e| AppError::Internal(e.to_string()))?;
// 3. Publish events
let nats_client = async_nats::connect(&state.config.nats_addr)
.await
.map_err(|e| AppError::Internal(format!("NATS connection failed: {}", e)))?;
publish_events(&nats_client, command, &financials).await?;
// 4. Finalize task
{
let mut entry = state
.tasks
.get_mut(&task_id)
.ok_or_else(|| AppError::Internal("Task not found".to_string()))?;
entry.status = ObservabilityTaskStatus::Completed;
entry.progress_percent = 100;
entry.details = "Workflow finished successfully".to_string();
}
Ok(financials)
}
async fn publish_events(
nats_client: &async_nats::Client,
command: &FetchCompanyDataCommand,
financials: &[TimeSeriesFinancialDto],
) -> Result<(), AppError> {
let profile_event = CompanyProfilePersistedEvent {
request_id: command.request_id,
symbol: command.symbol.clone(),
};
nats_client
.publish(
"events.data.company_profile_persisted",
serde_json::to_vec(&profile_event).unwrap().into(),
)
.await?;
let years: std::collections::BTreeSet<u16> = financials
.iter()
.map(|f| f.period_date.year() as u16)
.collect();
let summary = format!("Fetched {} years of financial data", years.len());
let financials_event = FinancialsPersistedEvent {
request_id: command.request_id,
symbol: command.symbol.clone(),
years_updated: years.into_iter().collect(),
template_id: command.template_id.clone(),
provider_id: Some("tushare".to_string()),
data_summary: Some(summary),
};
nats_client
.publish(
"events.data.financials_persisted",
serde_json::to_vec(&financials_event).unwrap().into(),
)
.await?;
Ok(())
}
async fn publish_failure_event(
nats_client: &async_nats::Client,
command: &FetchCompanyDataCommand,
error_msg: String,
) -> Result<(), AppError> {
let event = DataFetchFailedEvent {
request_id: command.request_id,
symbol: command.symbol.clone(),
error: error_msg,
provider_id: Some("tushare".to_string()),
};
nats_client
.publish(
"events.data.fetch_failed",
serde_json::to_vec(&event).unwrap().into(),
)
.await
.map_err(|e| AppError::Internal(format!("Failed to publish failure event: {}", e)))?;
Ok(())
}
@ -245,9 +67,10 @@ mod integration_tests {
use crate::config::AppConfig;
use crate::state::AppState;
use secrecy::SecretString;
use common_contracts::observability::TaskProgress;
use common_contracts::observability::{TaskProgress, ObservabilityTaskStatus};
use common_contracts::symbol_utils::{CanonicalSymbol, Market};
use uuid::Uuid;
use chrono::Utc;
#[tokio::test]
async fn test_tushare_fetch_flow() {
@ -272,9 +95,7 @@ mod integration_tests {
Some(api_url)
).await;
assert!(state.get_provider().await.is_some());
// 3. Construct Command (Use Maotai as example)
// 3. Construct Command
let request_id = Uuid::new_v4();
let cmd = FetchCompanyDataCommand {
request_id,
@ -283,7 +104,7 @@ mod integration_tests {
template_id: Some("default".to_string()),
};
// Init task in map (usually done by handler wrapper)
// Init task
state.tasks.insert(request_id, TaskProgress {
request_id,
task_name: "tushare:600519".to_string(),
@ -294,11 +115,15 @@ mod integration_tests {
});
// 4. Run
let result = run_tushare_workflow_inner(state.clone(), &cmd).await;
let (tx, mut rx) = mpsc::channel(1);
let result = run_tushare_workflow(state.clone(), cmd, tx).await;
// 5. Assert
assert!(result.is_ok(), "Worker execution failed: {:?}", result.err());
// Wait for completion signal
let _ = rx.recv().await;
let task = state.tasks.get(&request_id).expect("Task should exist");
assert_eq!(task.status, ObservabilityTaskStatus::Completed);
}

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ COPY ./services/workflow-orchestrator-service /usr/src/app/services/workflow-orc
WORKDIR /usr/src/app/services/workflow-orchestrator-service
# Build the binary
RUN cargo build --release
RUN cargo build
# 2. Runtime Stage
FROM debian:bookworm-slim
@ -21,7 +21,7 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/*
# Copy the built binary
COPY --from=builder /usr/src/app/services/workflow-orchestrator-service/target/release/workflow-orchestrator-service /usr/local/bin/
COPY --from=builder /usr/src/app/services/workflow-orchestrator-service/target/debug/workflow-orchestrator-service /usr/local/bin/
# Run it
ENTRYPOINT ["/usr/local/bin/workflow-orchestrator-service"]

View File

@ -22,12 +22,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
version = "0.1.5"
@ -103,15 +97,6 @@ dependencies = [
"syn 2.0.110",
]
[[package]]
name = "atoi"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
dependencies = [
"num-traits",
]
[[package]]
name = "atomic-waker"
version = "1.1.2"
@ -269,12 +254,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.0"
@ -325,6 +304,8 @@ name = "common-contracts"
version = "0.1.0"
dependencies = [
"anyhow",
"async-nats",
"async-trait",
"chrono",
"log",
"reqwest",
@ -332,22 +313,12 @@ dependencies = [
"serde",
"serde_json",
"service_kit",
"sqlx",
"tokio",
"tracing",
"utoipa",
"uuid",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "config"
version = "0.15.19"
@ -457,30 +428,6 @@ dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
@ -577,9 +524,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"const-oid",
"crypto-common",
"subtle",
]
[[package]]
@ -611,12 +556,6 @@ dependencies = [
"litrs",
]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "dyn-clone"
version = "1.0.20"
@ -645,15 +584,6 @@ dependencies = [
"subtle",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
dependencies = [
"serde",
]
[[package]]
name = "encoding_rs"
version = "0.8.35"
@ -690,28 +620,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "etcetera"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
dependencies = [
"cfg-if",
"home",
"windows-sys 0.48.0",
]
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "fastrand"
version = "2.3.0"
@ -730,17 +638,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "flume"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
dependencies = [
"futures-core",
"futures-sink",
"spin",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -783,21 +680,6 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@ -805,7 +687,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@ -814,34 +695,6 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-intrusive"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
dependencies = [
"futures-core",
"lock_api",
"parking_lot",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
@ -871,13 +724,10 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
@ -956,8 +806,6 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
@ -982,39 +830,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
dependencies = [
"hmac",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "home"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "http"
version = "1.3.1"
@ -1305,15 +1120,6 @@ dependencies = [
"serde",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.15"
@ -1346,9 +1152,6 @@ name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin",
]
[[package]]
name = "libc"
@ -1356,33 +1159,6 @@ version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libm"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "libredox"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
dependencies = [
"bitflags",
"libc",
"redox_syscall",
]
[[package]]
name = "libsqlite3-sys"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@ -1431,16 +1207,6 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "md-5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest",
]
[[package]]
name = "memchr"
version = "2.7.6"
@ -1514,48 +1280,12 @@ dependencies = [
"rand",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
dependencies = [
"lazy_static",
"libm",
"num-integer",
"num-iter",
"num-traits",
"rand",
"smallvec",
"zeroize",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-iter"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -1563,7 +1293,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@ -1626,12 +1355,6 @@ dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.5"
@ -1751,17 +1474,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"pkcs8",
"spki",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
@ -2079,26 +1791,6 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "rsa"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
dependencies = [
"const-oid",
"digest",
"num-bigint-dig",
"num-integer",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core",
"signature",
"spki",
"subtle",
"zeroize",
]
[[package]]
name = "rust-ini"
version = "0.21.3"
@ -2272,16 +1964,6 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "secrecy"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
dependencies = [
"serde",
"zeroize",
]
[[package]]
name = "security-framework"
version = "2.11.1"
@ -2465,17 +2147,6 @@ dependencies = [
"utoipa",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
@ -2550,9 +2221,6 @@ name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [
"serde",
]
[[package]]
name = "socket2"
@ -2564,15 +2232,6 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "spki"
version = "0.7.3"
@ -2583,224 +2242,12 @@ dependencies = [
"der",
]
[[package]]
name = "sqlx"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
dependencies = [
"sqlx-core",
"sqlx-macros",
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
]
[[package]]
name = "sqlx-core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
dependencies = [
"base64",
"bytes",
"chrono",
"crc",
"crossbeam-queue",
"either",
"event-listener",
"futures-core",
"futures-intrusive",
"futures-io",
"futures-util",
"hashbrown 0.15.5",
"hashlink",
"indexmap",
"log",
"memchr",
"once_cell",
"percent-encoding",
"rust_decimal",
"rustls",
"serde",
"serde_json",
"sha2",
"smallvec",
"thiserror 2.0.17",
"tokio",
"tokio-stream",
"tracing",
"url",
"uuid",
"webpki-roots 0.26.11",
]
[[package]]
name = "sqlx-macros"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
dependencies = [
"proc-macro2",
"quote",
"sqlx-core",
"sqlx-macros-core",
"syn 2.0.110",
]
[[package]]
name = "sqlx-macros-core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
dependencies = [
"dotenvy",
"either",
"heck",
"hex",
"once_cell",
"proc-macro2",
"quote",
"serde",
"serde_json",
"sha2",
"sqlx-core",
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
"syn 2.0.110",
"tokio",
"url",
]
[[package]]
name = "sqlx-mysql"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
dependencies = [
"atoi",
"base64",
"bitflags",
"byteorder",
"bytes",
"chrono",
"crc",
"digest",
"dotenvy",
"either",
"futures-channel",
"futures-core",
"futures-io",
"futures-util",
"generic-array",
"hex",
"hkdf",
"hmac",
"itoa",
"log",
"md-5",
"memchr",
"once_cell",
"percent-encoding",
"rand",
"rsa",
"rust_decimal",
"serde",
"sha1",
"sha2",
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.17",
"tracing",
"uuid",
"whoami",
]
[[package]]
name = "sqlx-postgres"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
dependencies = [
"atoi",
"base64",
"bitflags",
"byteorder",
"chrono",
"crc",
"dotenvy",
"etcetera",
"futures-channel",
"futures-core",
"futures-util",
"hex",
"hkdf",
"hmac",
"home",
"itoa",
"log",
"md-5",
"memchr",
"once_cell",
"rand",
"rust_decimal",
"serde",
"serde_json",
"sha2",
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.17",
"tracing",
"uuid",
"whoami",
]
[[package]]
name = "sqlx-sqlite"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
dependencies = [
"atoi",
"chrono",
"flume",
"futures-channel",
"futures-core",
"futures-executor",
"futures-intrusive",
"futures-util",
"libsqlite3-sys",
"log",
"percent-encoding",
"serde",
"serde_urlencoded",
"sqlx-core",
"thiserror 2.0.17",
"tracing",
"url",
"uuid",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "stringprep"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
dependencies = [
"unicode-bidi",
"unicode-normalization",
"unicode-properties",
]
[[package]]
name = "subtle"
version = "2.6.1"
@ -3289,33 +2736,12 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]]
name = "unicode-bidi"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-normalization"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-properties"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
@ -3425,12 +2851,6 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
version = "0.2.105"
@ -3517,16 +2937,6 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "whoami"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
dependencies = [
"libredox",
"wasite",
]
[[package]]
name = "windows-core"
version = "0.62.2"
@ -3597,15 +3007,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@ -3633,21 +3034,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@ -3681,12 +3067,6 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
@ -3699,12 +3079,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
@ -3717,12 +3091,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
@ -3747,12 +3115,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
@ -3765,12 +3127,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
@ -3783,12 +3139,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
@ -3801,12 +3151,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
@ -3872,11 +3216,8 @@ dependencies = [
"common-contracts",
"config",
"dashmap",
"futures",
"futures-util",
"itertools",
"reqwest",
"secrecy",
"serde",
"serde_json",
"thiserror 2.0.17",

View File

@ -4,24 +4,24 @@ version = "0.1.0"
edition = "2024"
[dependencies]
async-trait = "0.1.89"
# Web Service
axum = "0.8.7"
tokio = { version = "1.0", features = ["full"] }
tower-http = { version = "0.6.6", features = ["cors"] }
# Shared Contracts
common-contracts = { path = "../common-contracts" }
# Disable default features to avoid pulling in sqlx
common-contracts = { path = "../common-contracts", default-features = false }
# Message Queue (NATS)
async-nats = "0.45.0"
futures = "0.3"
futures-util = "0.3.31"
# Data Persistence Client
reqwest = { version = "0.12.24", features = ["json", "cookies"] }
# Concurrency & Async
async-trait = "0.1.80"
dashmap = "6.1.0"
uuid = { version = "1.6", features = ["v4", "serde"] }
@ -35,11 +35,8 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Configuration
config = "0.15.19"
secrecy = { version = "0.10.3", features = ["serde"] }
# Error Handling
thiserror = "2.0.17"
anyhow = "1.0"
chrono = { version = "0.4.38", features = ["serde"] }
itertools = "0.14.0"

View File

@ -6,7 +6,7 @@ WORKDIR /usr/src/app
COPY ./services/common-contracts /usr/src/app/services/common-contracts
COPY ./services/yfinance-provider-service /usr/src/app/services/yfinance-provider-service
WORKDIR /usr/src/app/services/yfinance-provider-service
RUN cargo build --release --bin yfinance-provider-service
RUN cargo build --bin yfinance-provider-service
# 2. Runtime Stage
FROM debian:bookworm-slim
@ -18,7 +18,7 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/*
# Copy the built binary from the builder stage
COPY --from=builder /usr/src/app/services/yfinance-provider-service/target/release/yfinance-provider-service /usr/local/bin/
COPY --from=builder /usr/src/app/services/yfinance-provider-service/target/debug/yfinance-provider-service /usr/local/bin/
# Set the binary as the entrypoint
ENTRYPOINT ["/usr/local/bin/yfinance-provider-service"]

View File

@ -2,7 +2,7 @@ use std::collections::HashMap;
use axum::{
extract::State,
response::Json,
routing::get,
routing::{get, post},
Router,
};
use common_contracts::observability::{HealthStatus, ServiceStatus, TaskProgress};
@ -12,6 +12,7 @@ pub fn create_router(app_state: AppState) -> Router {
Router::new()
.route("/health", get(health_check))
.route("/tasks", get(get_current_tasks))
.route("/test", post(test_connection))
.with_state(app_state)
}
@ -41,3 +42,18 @@ async fn get_current_tasks(State(state): State<AppState>) -> Json<Vec<TaskProgre
.collect();
Json(tasks)
}
/// [POST /test]
/// Tests connectivity to Yahoo Finance by attempting to fetch a crumb.
async fn test_connection(State(state): State<AppState>) -> Json<serde_json::Value> {
match state.yfinance_provider.ping().await {
Ok(_) => Json(serde_json::json!({
"success": true,
"message": "Successfully connected to Yahoo Finance (Crumb fetched)"
})),
Err(e) => Json(serde_json::json!({
"success": false,
"message": format!("Connection failed: {}", e)
})),
}
}

View File

@ -27,9 +27,6 @@ pub enum AppError {
#[error("Internal error: {0}")]
Internal(String),
#[error("Provider not available: {0}")]
ProviderNotAvailable(String),
}
// 手动实现针对 async-nats 泛型错误类型的 From 转换

View File

@ -11,9 +11,9 @@ mod yfinance;
use crate::config::AppConfig;
use crate::error::Result;
use crate::state::AppState;
use tracing::{info, warn};
use tracing::info;
use common_contracts::lifecycle::ServiceRegistrar;
use common_contracts::registry::ServiceRegistration;
use common_contracts::registry::{ServiceRegistration, ProviderMetadata};
use std::sync::Arc;
#[tokio::main]
@ -47,6 +47,15 @@ async fn main() -> Result<()> {
role: common_contracts::registry::ServiceRole::DataProvider,
base_url: format!("http://{}:{}", config.service_host, port),
health_check_url: format!("http://{}:{}/health", config.service_host, port),
metadata: Some(ProviderMetadata {
id: "yfinance".to_string(),
name_en: "Yahoo Finance".to_string(),
name_cn: "Yahoo Finance".to_string(),
description: "Yahoo Finance API (Unofficial)".to_string(),
icon_url: None,
config_schema: vec![],
supports_test_connection: true,
}),
}
);

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