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:
parent
ca1eddd244
commit
a59b994a92
24
crates/workflow-context/Cargo.lock
generated
24
crates/workflow-context/Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -38,7 +38,7 @@ services:
|
||||
RUST_LOG: info
|
||||
RUST_BACKTRACE: "1"
|
||||
ports:
|
||||
- "3001:3000"
|
||||
- "3005:3000"
|
||||
depends_on:
|
||||
postgres-test:
|
||||
condition: service_healthy
|
||||
|
||||
@ -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 导致访问宿主机端口
|
||||
|
||||
@ -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` 渲染界面。
|
||||
* 对接新的测试连接和保存逻辑。
|
||||
@ -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 消息。
|
||||
|
||||
@ -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 消息。
|
||||
|
||||
@ -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 给业务 Worker(Python/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。
|
||||
|
||||
@ -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`: 错误处理。
|
||||
@ -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)。
|
||||
|
||||
538
frontend/package-lock.json
generated
538
frontend/package-lock.json
generated
@ -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"
|
||||
|
||||
@ -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",
|
||||
|
||||
49
frontend/scripts/docker-dev-entrypoint.sh
Executable file
49
frontend/scripts/docker-dev-entrypoint.sh
Executable 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}
|
||||
|
||||
@ -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('/');
|
||||
|
||||
634
frontend/src/api/schema.gen.ts
Normal file
634
frontend/src/api/schema.gen.ts
Normal 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);
|
||||
}
|
||||
184
frontend/src/components/config/DynamicConfigForm.tsx
Normal file
184
frontend/src/components/config/DynamicConfigForm.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
23
frontend/src/components/ui/toaster.tsx
Normal file
23
frontend/src/components/ui/toaster.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
80
frontend/src/hooks/use-toast.ts
Normal file
80
frontend/src/hooks/use-toast.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
1312
openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
7
package-lock.json
generated
7
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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
24
scripts/update_api_spec.sh
Executable 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!"
|
||||
|
||||
617
services/alphavantage-provider-service/Cargo.lock
generated
617
services/alphavantage-provider-service/Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.");
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@ -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) => {
|
||||
|
||||
623
services/api-gateway/Cargo.lock
generated
623
services/api-gateway/Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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();
|
||||
@ -272,7 +249,7 @@ fn project_data_sources(
|
||||
(key, entry)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_id(provider: &DataSourceProvider) -> &'static str {
|
||||
match provider {
|
||||
@ -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)"),
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
60
services/api-gateway/src/api/registry_tests.rs
Normal file
60
services/api-gateway/src/api/registry_tests.rs
Normal 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.
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,6 +51,7 @@ impl PersistenceClient {
|
||||
Ok(financials)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_session_data(
|
||||
&self,
|
||||
request_id: uuid::Uuid,
|
||||
|
||||
64
services/api-gateway/src/registry_unit_test.rs
Normal file
64
services/api-gateway/src/registry_unit_test.rs
Normal 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));
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
360
services/common-contracts/Cargo.lock
generated
360
services/common-contracts/Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
30
services/common-contracts/src/abstraction.rs
Normal file
30
services/common-contracts/src/abstraction.rs
Normal 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>)>;
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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!({
|
||||
|
||||
@ -21,7 +21,6 @@ pub struct HealthStatus {
|
||||
|
||||
#[api_dto]
|
||||
#[derive(Copy, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ObservabilityTaskStatus {
|
||||
Queued,
|
||||
InProgress,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
|
||||
176
services/common-contracts/src/workflow_harness.rs
Normal file
176
services/common-contracts/src/workflow_harness.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
353
services/data-persistence-service/Cargo.lock
generated
353
services/data-persistence-service/Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
664
services/finnhub-provider-service/Cargo.lock
generated
664
services/finnhub-provider-service/Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.");
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
use anyhow::anyhow;
|
||||
use reqwest::Error as ReqwestError;
|
||||
use thiserror::Error;
|
||||
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
600
services/report-generator-service/Cargo.lock
generated
600
services/report-generator-service/Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"]
|
||||
|
||||
5
services/report-generator-service/cookies.txt
Normal file
5
services/report-generator-service/cookies.txt
Normal 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
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
739
services/tushare-provider-service/Cargo.lock
generated
739
services/tushare-provider-service/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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"]
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.");
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
use anyhow::anyhow;
|
||||
use reqwest::Error as ReqwestError;
|
||||
use thiserror::Error;
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
736
services/workflow-orchestrator-service/Cargo.lock
generated
736
services/workflow-orchestrator-service/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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"]
|
||||
|
||||
663
services/yfinance-provider-service/Cargo.lock
generated
663
services/yfinance-provider-service/Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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)
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,9 +27,6 @@ pub enum AppError {
|
||||
|
||||
#[error("Internal error: {0}")]
|
||||
Internal(String),
|
||||
|
||||
#[error("Provider not available: {0}")]
|
||||
ProviderNotAvailable(String),
|
||||
}
|
||||
|
||||
// 手动实现针对 async-nats 泛型错误类型的 From 转换
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user