Skip to content

Commit 2f66923

Browse files
committed
feat(mx): 集成东方财富妙想API并新增AI选股功能
- 新增MiaoXiangSearchProvider实现东方财富妙想资讯搜索支持 - 配置文件增加MX_APIKEY环境变量支持多个妙想API Key - 新增AI选股API端点,支持自然语言条件智能选股 - WebUI添加AI选股页面及相关路由和侧边栏入口 - 实现前端调用AI选股API并展示选股结果和提交深度分析功能 - 调整核心配置管理以支持妙想API Key的加载和校验 - 更新.gitignore和配置示例以支持妙想API相关文件和文档 - 日志、错误处理和消息提示完善AI选股功能用户体验
2 parents a349455 + fb6c71a commit 2f66923

63 files changed

Lines changed: 2556 additions & 549 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.codex

Whitespace-only changes.

.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ GEMINI_API_KEY=
237237
# LLM_OPENROUTER_MODELS=~anthropic/claude-sonnet-latest,~openai/gpt-latest
238238
# LITELLM_MODEL=openai/~anthropic/claude-sonnet-latest
239239

240-
# 高级:模型路由 YAML 配置(可选,参考 litellm_config.example.yaml)
240+
# 高级:模型路由 YAML 配置(可选,参考 docs/examples/litellm_config.example.yaml)
241241
# LITELLM_CONFIG=./litellm_config.yaml
242242

243243
# ===================================
@@ -462,6 +462,10 @@ AGENT_SKILLS=
462462
# 注册Server酱3账号并获取SendKey https://sc3.ft07.com/
463463
# SERVERCHAN3_SENDKEY=
464464
#
465+
# 【方式十一】AstrBot 配置
466+
# ASTRBOT_URL=
467+
# ASTRBOT_TOKEN= # 可选,用于需要 Bearer Token 的 AstrBot Webhook
468+
#
465469
# 【高级配置】消息长度限制(字节)
466470
# 超过限制会自动分批发送,一般无需修改
467471
# FEISHU_MAX_BYTES=20000 # 飞书限制约 20KB,默认 20000 字节

.github/workflows/daily_analysis.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ jobs:
270270

271271
# 方式二:飞书 Webhook
272272
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }}
273+
FEISHU_WEBHOOK_SECRET: ${{ secrets.FEISHU_WEBHOOK_SECRET }}
274+
FEISHU_WEBHOOK_KEYWORD: ${{ vars.FEISHU_WEBHOOK_KEYWORD || secrets.FEISHU_WEBHOOK_KEYWORD }}
273275

274276
# 方式三:Telegram
275277
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
@@ -288,10 +290,13 @@ jobs:
288290

289291
# 方式六:PushPlus ⬅️ 新增!
290292
PUSHPLUS_TOKEN: ${{ secrets.PUSHPLUS_TOKEN }}
293+
PUSHPLUS_TOPIC: ${{ vars.PUSHPLUS_TOPIC || secrets.PUSHPLUS_TOPIC }}
291294

292295
# 方式七:自定义 Webhook(钉钉、Bark、自建服务等)
293296
CUSTOM_WEBHOOK_URLS: ${{ secrets.CUSTOM_WEBHOOK_URLS }}
294297
CUSTOM_WEBHOOK_BEARER_TOKEN: ${{ secrets.CUSTOM_WEBHOOK_BEARER_TOKEN }}
298+
CUSTOM_WEBHOOK_BODY_TEMPLATE: ${{ vars.CUSTOM_WEBHOOK_BODY_TEMPLATE || secrets.CUSTOM_WEBHOOK_BODY_TEMPLATE }}
299+
WEBHOOK_VERIFY_SSL: ${{ vars.WEBHOOK_VERIFY_SSL || secrets.WEBHOOK_VERIFY_SSL || 'true' }}
295300

296301
# 方式八:Discord
297302
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}

.github/workflows/network-smoke.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
continue-on-error: true
4141
run: |
4242
set -o pipefail
43-
./test.sh quick --no-notify | tee quick-smoke.log
43+
./scripts/test.sh quick --no-notify | tee quick-smoke.log
4444
4545
- name: 📤 Upload smoke logs
4646
if: always()

.gitignore

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88
# 测试文件(可能包含敏感配置)
99
# 仅忽略仓库根目录下的临时 test_*.py 脚本;tests/ 目录下的单元测试需要纳入版本控制。
1010
/test_*.py
11-
!test_env.py
11+
/test.sh
12+
13+
# 本地评审与高级路由配置(可能包含敏感信息或临时分析产物)
14+
/.codex
15+
/review.md
16+
/litellm_config.yaml
17+
/litellm_config.*.yaml
1218

1319
# Python
1420
__pycache__/
@@ -78,5 +84,3 @@ verify_*.py
7884
static/
7985
/apps/dsa-desktop/dist/
8086
/apps/dsa-desktop/node_modules/
81-
82-
.codex

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ gh run view <run_id> --log-failed
138138
| `backend-gate` | `.github/workflows/ci.yml` | 执行 `./scripts/ci_gate.sh` ||
139139
| `docker-build` | `.github/workflows/ci.yml` | Docker 构建与关键模块导入 smoke ||
140140
| `web-gate` | `.github/workflows/ci.yml` | 前端改动时执行 `npm run lint` + `npm run build` | 是(触发时) |
141-
| `network-smoke` | `.github/workflows/network-smoke.yml` | `pytest -m network` + `test.sh quick` | 否,观测项 |
141+
| `network-smoke` | `.github/workflows/network-smoke.yml` | `pytest -m network` + `scripts/test.sh quick` | 否,观测项 |
142142
| `pr-review` | `.github/workflows/pr-review.yml` | PR 静态检查 + AI 审查 + 自动标签 | 否,辅助项 |
143143

144144
若 PR 上已有对应 CI 结果,可直接引用 CI 结论;若 CI 未覆盖改动面,或本地与 CI 环境差异较大,需要补充说明本地验证与缺口。

api/v1/endpoints/system_config.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
SystemConfigValidationErrorResponse,
2323
TestLLMChannelRequest,
2424
TestLLMChannelResponse,
25+
TestNotificationChannelRequest,
26+
TestNotificationChannelResponse,
2527
UpdateSystemConfigRequest,
2628
UpdateSystemConfigResponse,
2729
ValidateSystemConfigRequest,
@@ -346,6 +348,50 @@ def test_llm_channel(
346348
)
347349

348350

351+
@router.post(
352+
"/config/notification/test-channel",
353+
response_model=TestNotificationChannelResponse,
354+
responses={
355+
200: {"description": "Notification channel test completed"},
356+
500: {"description": "Internal server error", "model": ErrorResponse},
357+
},
358+
summary="Test one notification channel",
359+
description="Send a short test notification using unsaved or saved notification configuration.",
360+
)
361+
def test_notification_channel(
362+
request: TestNotificationChannelRequest,
363+
service: SystemConfigService = Depends(get_system_config_service),
364+
) -> TestNotificationChannelResponse:
365+
"""Validate and test one notification channel without writing `.env`."""
366+
try:
367+
payload = service.test_notification_channel(
368+
channel=request.channel,
369+
items=[item.model_dump() for item in request.items],
370+
mask_token=request.mask_token,
371+
title=request.title,
372+
content=request.content,
373+
timeout_seconds=request.timeout_seconds,
374+
)
375+
return TestNotificationChannelResponse.model_validate(payload)
376+
except (ValueError, TypeError) as exc:
377+
raise HTTPException(
378+
status_code=422,
379+
detail={
380+
"error": "validation_error",
381+
"message": str(exc),
382+
},
383+
)
384+
except Exception as exc:
385+
logger.error("Failed to test notification channel: %s", exc, exc_info=True)
386+
raise HTTPException(
387+
status_code=500,
388+
detail={
389+
"error": "internal_error",
390+
"message": "Failed to test notification channel",
391+
},
392+
)
393+
394+
349395
@router.post(
350396
"/config/llm/discover-models",
351397
response_model=DiscoverLLMChannelModelsResponse,

api/v1/schemas/system_config.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,19 @@
88
from pydantic import BaseModel, ConfigDict, Field
99

1010
LLMCapabilityCheck = Literal["json", "tools", "vision", "stream"]
11+
NotificationTestChannel = Literal[
12+
"wechat",
13+
"feishu",
14+
"telegram",
15+
"email",
16+
"pushover",
17+
"pushplus",
18+
"serverchan3",
19+
"custom",
20+
"discord",
21+
"slack",
22+
"astrbot",
23+
]
1124

1225

1326
class SystemConfigOption(BaseModel):
@@ -204,6 +217,43 @@ class TestLLMChannelResponse(BaseModel):
204217
capability_results: Dict[str, LLMCapabilityCheckResult] = Field(default_factory=dict)
205218

206219

220+
class NotificationTestAttempt(BaseModel):
221+
"""One notification delivery attempt result."""
222+
223+
channel: NotificationTestChannel
224+
success: bool
225+
message: str
226+
target: Optional[str] = None
227+
error_code: Optional[str] = None
228+
stage: str = "notification_send"
229+
retryable: bool = False
230+
latency_ms: Optional[int] = None
231+
http_status: Optional[int] = None
232+
233+
234+
class TestNotificationChannelRequest(BaseModel):
235+
"""Request payload for testing one notification channel."""
236+
237+
channel: NotificationTestChannel
238+
items: List[SystemConfigUpdateItem] = Field(default_factory=list)
239+
mask_token: str = "******"
240+
title: str = Field(default="DSA 通知测试", min_length=1, max_length=80)
241+
content: str = Field(default="这是一条来自 DSA Web 设置页的通知测试消息。", min_length=1, max_length=1000)
242+
timeout_seconds: float = Field(default=20.0, ge=1.0, le=120.0)
243+
244+
245+
class TestNotificationChannelResponse(BaseModel):
246+
"""Response payload for one notification channel connectivity test."""
247+
248+
success: bool
249+
message: str
250+
error_code: Optional[str] = None
251+
stage: Optional[str] = None
252+
retryable: bool = False
253+
latency_ms: Optional[int] = None
254+
attempts: List[NotificationTestAttempt] = Field(default_factory=list)
255+
256+
207257
class DiscoverLLMChannelModelsRequest(BaseModel):
208258
"""Request payload for discovering models from one LLM channel."""
209259

apps/dsa-web/src/api/__tests__/systemConfig.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,54 @@ describe('systemConfigApi', () => {
6161
expect.objectContaining({ capability_checks: ['json', 'stream'] }),
6262
);
6363
});
64+
65+
it('sends notification channel test payloads with snake_case fields', async () => {
66+
post.mockResolvedValueOnce({
67+
data: {
68+
success: true,
69+
message: 'ok',
70+
error_code: null,
71+
stage: 'notification_send',
72+
retryable: false,
73+
latency_ms: 15,
74+
attempts: [
75+
{
76+
channel: 'custom',
77+
success: true,
78+
message: 'sent',
79+
target: 'https://example.com/hook?token=***',
80+
error_code: null,
81+
stage: 'notification_send',
82+
retryable: false,
83+
latency_ms: 15,
84+
http_status: 200,
85+
},
86+
],
87+
},
88+
});
89+
90+
const result = await systemConfigApi.testNotificationChannel({
91+
channel: 'custom',
92+
items: [{ key: 'CUSTOM_WEBHOOK_URLS', value: 'https://example.com/hook?token=secret' }],
93+
maskToken: '******',
94+
title: 'hello',
95+
content: 'world',
96+
timeoutSeconds: 7,
97+
});
98+
99+
expect(post).toHaveBeenCalledWith(
100+
'/api/v1/system/config/notification/test-channel',
101+
{
102+
channel: 'custom',
103+
items: [{ key: 'CUSTOM_WEBHOOK_URLS', value: 'https://example.com/hook?token=secret' }],
104+
mask_token: '******',
105+
title: 'hello',
106+
content: 'world',
107+
timeout_seconds: 7,
108+
},
109+
);
110+
expect(result.latencyMs).toBe(15);
111+
expect(result.attempts[0].errorCode).toBeNull();
112+
expect(result.attempts[0].httpStatus).toBe(200);
113+
});
64114
});

apps/dsa-web/src/api/systemConfig.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import type {
1212
SystemConfigValidationErrorResponse,
1313
TestLLMChannelRequest,
1414
TestLLMChannelResponse,
15+
TestNotificationChannelRequest,
16+
TestNotificationChannelResponse,
1517
UpdateSystemConfigRequest,
1618
UpdateSystemConfigResponse,
1719
ValidateSystemConfigRequest,
@@ -99,6 +101,20 @@ function toSnakeTestChannelPayload(payload: TestLLMChannelRequest): Record<strin
99101
return request;
100102
}
101103

104+
function toSnakeNotificationTestPayload(payload: TestNotificationChannelRequest): Record<string, unknown> {
105+
return {
106+
channel: payload.channel,
107+
items: (payload.items || []).map((item) => ({
108+
key: item.key,
109+
value: item.value,
110+
})),
111+
mask_token: payload.maskToken ?? '******',
112+
title: payload.title ?? 'DSA 通知测试',
113+
content: payload.content ?? '这是一条来自 DSA Web 设置页的通知测试消息。',
114+
timeout_seconds: payload.timeoutSeconds ?? 20,
115+
};
116+
}
117+
102118
function toSnakeDiscoverModelsPayload(payload: DiscoverLLMChannelModelsRequest): Record<string, unknown> {
103119
return {
104120
name: payload.name,
@@ -152,6 +168,14 @@ export const systemConfigApi = {
152168
return toCamelCase<TestLLMChannelResponse>(response.data);
153169
},
154170

171+
async testNotificationChannel(payload: TestNotificationChannelRequest): Promise<TestNotificationChannelResponse> {
172+
const response = await apiClient.post<Record<string, unknown>>(
173+
'/api/v1/system/config/notification/test-channel',
174+
toSnakeNotificationTestPayload(payload),
175+
);
176+
return toCamelCase<TestNotificationChannelResponse>(response.data);
177+
},
178+
155179
async discoverLLMChannelModels(
156180
payload: DiscoverLLMChannelModelsRequest,
157181
): Promise<DiscoverLLMChannelModelsResponse> {

0 commit comments

Comments
 (0)