Skip to content

Commit 45513b4

Browse files
author
xiaowei
committed
Merge remote-tracking branch 'upstream/main' into main
2 parents 8fb8f43 + 706879f commit 45513b4

55 files changed

Lines changed: 3999 additions & 445 deletions

Some content is hidden

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

.env.example

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,11 @@ AGENT_SKILLS=
229229
# 在飞书群 -> 设置 -> 群机器人 -> 添加机器人 -> 自定义机器人 -> 复制 Webhook 地址
230230
#
231231
# FEISHU_WEBHOOK_URL=https://open.feishu.cn/open-apis/bot/v2/hook/your_key_here
232+
# 如果机器人安全设置开启了“签名校验”,需同步填写 secret
233+
# FEISHU_WEBHOOK_SECRET=your_feishu_webhook_secret
234+
# 如果机器人安全设置开启了“关键词”,需填写同一个关键词;系统会自动在每条消息前补上
235+
# FEISHU_WEBHOOK_KEYWORD=股票日报
236+
# 注意:FEISHU_APP_ID / FEISHU_APP_SECRET 用于飞书应用、云文档或 Stream 模式,不会直接启用群 Webhook 推送
232237
#
233238
# 【方式三】Telegram 机器人(需同时配置两项)
234239
# 1. 在 Telegram 找 @BotFather -> /newbot -> 获取 Bot Token
@@ -369,14 +374,25 @@ DINGTALK_APP_SECRET=xxxx
369374
# 启用 Stream 模式
370375
DINGTALK_STREAM_ENABLED=false
371376

372-
# 飞书应用机器人配置
377+
# 飞书应用机器人配置(用于 Stream Bot / 云文档模式,不会直接开启群 Webhook 推送)
373378
FEISHU_APP_ID=xxxx
374-
FEISHU_APP_SECRET=xxxx
379+
FEISHU_APP_SECRET=xxxx # 飞书应用 Secret,仅应用/Stream Bot/云文档模式使用;群通知推送请使用 FEISHU_WEBHOOK_URL
375380
# 启用长连接模式
376381
FEISHU_STREAM_ENABLED=false
382+
# 飞书群机器人 Webhook 安全配置(仅 Webhook 推送模式使用)
383+
# FEISHU_WEBHOOK_SECRET=your_feishu_webhook_secret
384+
# FEISHU_WEBHOOK_KEYWORD=股票日报
377385

378386
# 数据库路径
379387
DATABASE_PATH=./data/stock_analysis.db
388+
# SQLite 写入优化:文件库默认启用 WAL,降低批量写入和并发回写时的锁竞争
389+
SQLITE_WAL_ENABLED=true
390+
# SQLite 等锁超时(毫秒)
391+
SQLITE_BUSY_TIMEOUT_MS=5000
392+
# SQLite 遇到 database is locked / database table is locked 时的最大重试次数
393+
SQLITE_WRITE_RETRY_MAX=3
394+
# SQLite 写入重试基础退避时间(秒,按指数退避递增)
395+
SQLITE_WRITE_RETRY_BASE_DELAY=0.1
380396

381397
# ===================================
382398
# 回测配置(可选)

api/v1/endpoints/agent.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,8 +447,13 @@ async def event_generator():
447447
finally:
448448
try:
449449
await asyncio.wait_for(fut, timeout=5.0)
450-
except Exception:
450+
except asyncio.CancelledError:
451451
pass
452+
except asyncio.TimeoutError:
453+
# Cleanup taking longer than 5s is treated as an expected timeout; no warning.
454+
logger.debug("agent executor cleanup timed out after 5s for session %s", session_id)
455+
except Exception as exc:
456+
logger.warning("agent executor cleanup error (ignored): %s", exc, exc_info=True)
452457

453458
return StreamingResponse(
454459
event_generator(),

api/v1/endpoints/analysis.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,11 +347,12 @@ def _handle_sync_analysis(
347347
)
348348

349349
if result is None:
350+
error_message = service.last_error or f"分析股票 {stock_code} 失败"
350351
raise HTTPException(
351352
status_code=500,
352353
detail={
353354
"error": "analysis_failed",
354-
"message": f"分析股票 {stock_code} 失败"
355+
"message": error_message,
355356
}
356357
)
357358

@@ -482,6 +483,7 @@ async def task_stream():
482483
- connected: 连接成功
483484
- task_created: 新任务创建
484485
- task_started: 任务开始执行
486+
- task_progress: 任务阶段进度更新
485487
- task_completed: 任务完成
486488
- task_failed: 任务失败
487489
- heartbeat: 心跳(每 30 秒)
@@ -516,8 +518,8 @@ async def event_generator():
516518
"timestamp": datetime.now().isoformat()
517519
})
518520
except asyncio.CancelledError:
519-
# 客户端断开连接
520-
pass
521+
logger.debug("SSE client disconnected, cancelling event generator")
522+
raise
521523
finally:
522524
task_queue.unsubscribe(event_queue)
523525

api/v1/endpoints/system_config.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from api.deps import get_system_config_service
1212
from api.v1.schemas.common import ErrorResponse
1313
from api.v1.schemas.system_config import (
14+
DiscoverLLMChannelModelsRequest,
15+
DiscoverLLMChannelModelsResponse,
1416
ExportSystemConfigResponse,
1517
ImportSystemConfigRequest,
1618
SystemConfigConflictResponse,
@@ -313,6 +315,50 @@ def test_llm_channel(
313315
)
314316

315317

318+
@router.post(
319+
"/config/llm/discover-models",
320+
response_model=DiscoverLLMChannelModelsResponse,
321+
responses={
322+
200: {"description": "Model discovery completed"},
323+
500: {"description": "Internal server error", "model": ErrorResponse},
324+
},
325+
summary="Discover models for one LLM channel",
326+
description="Call one unsaved or saved channel's `/models` endpoint and return discovered model IDs.",
327+
)
328+
def discover_llm_channel_models(
329+
request: DiscoverLLMChannelModelsRequest,
330+
service: SystemConfigService = Depends(get_system_config_service),
331+
) -> DiscoverLLMChannelModelsResponse:
332+
"""Discover models for one channel definition without writing `.env`."""
333+
try:
334+
payload = service.discover_llm_channel_models(
335+
name=request.name,
336+
protocol=request.protocol,
337+
base_url=request.base_url,
338+
api_key=request.api_key,
339+
models=request.models,
340+
timeout_seconds=request.timeout_seconds,
341+
)
342+
return DiscoverLLMChannelModelsResponse.model_validate(payload)
343+
except (ValueError, TypeError) as exc:
344+
raise HTTPException(
345+
status_code=422,
346+
detail={
347+
"error": "validation_error",
348+
"message": str(exc),
349+
},
350+
)
351+
except Exception as exc:
352+
logger.error("Failed to discover LLM channel models: %s", exc, exc_info=True)
353+
raise HTTPException(
354+
status_code=500,
355+
detail={
356+
"error": "internal_error",
357+
"message": "Failed to discover LLM channel models",
358+
},
359+
)
360+
361+
316362
@router.get(
317363
"/config/schema",
318364
response_model=SystemConfigSchemaResponse,

api/v1/schemas/system_config.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,28 @@ class TestLLMChannelResponse(BaseModel):
162162
latency_ms: Optional[int] = None
163163

164164

165+
class DiscoverLLMChannelModelsRequest(BaseModel):
166+
"""Request payload for discovering models from one LLM channel."""
167+
168+
name: str = "channel"
169+
protocol: str = "openai"
170+
base_url: str = ""
171+
api_key: str = ""
172+
models: List[str] = Field(default_factory=list)
173+
timeout_seconds: float = 20.0
174+
175+
176+
class DiscoverLLMChannelModelsResponse(BaseModel):
177+
"""Response payload for one LLM channel model discovery request."""
178+
179+
success: bool
180+
message: str
181+
error: Optional[str] = None
182+
resolved_protocol: Optional[str] = None
183+
models: List[str] = Field(default_factory=list)
184+
latency_ms: Optional[int] = None
185+
186+
165187
class SystemConfigValidationErrorResponse(BaseModel):
166188
"""Error payload for failed update validation."""
167189

apps/dsa-web/package-lock.json

Lines changed: 9 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)