Add AI chat with skills management and model provider settings

- Model providers for OpenAI and Anthropic message formats (e.g. via
  Sub2API), with API keys encrypted at rest and never returned
- Skills tree with markdown editing, priorities, drafts, publishing and
  rollback; only published skills enter the chat prompt
- Chat page with tool calls over collected news and Binance USD-M
  klines, a persisted CONTEXT.md, reset, and stop-generation support
- Chat may only create skill drafts and update CONTEXT.md; publishing,
  deleting, moving and priority changes stay with the user
This commit is contained in:
eezd committed 2026-10-14 13:11:45 +08:00
1 parent f042f27013
commit 5db3ead883
34 files changed
+3196 -12

No files matched your search

+16 -10
View File
@@ -55,16 +55,8 @@ class Revision(BaseModel):
captured_at: datetime
@router.get("")
async def list_news(
session: Annotated[AsyncSession, Depends(get_session)],
source: Annotated[list[str] | None, Query()] = None,
q: str | None = None,
start: datetime | None = None,
end: datetime | None = None,
page: Annotated[int, Query(ge=1)] = 1,
limit: Annotated[int, Query(ge=1, le=100)] = 30,
) -> NewsPage:
def news_filters(source: list[str] | None, q: str | None, start: datetime | None, end: datetime | None) -> list:
"""新闻列表和 Chat 检索工具共用的筛选条件"""
conditions = []
if source:
conditions.append(NewsItem.source_code.in_(source))
@@ -77,6 +69,20 @@ async def list_news(
conditions.append(NewsItem.published_at >= start)
if end:
conditions.append(NewsItem.published_at < end)
return conditions
@router.get("")
async def list_news(
session: Annotated[AsyncSession, Depends(get_session)],
source: Annotated[list[str] | None, Query()] = None,
q: str | None = None,
start: datetime | None = None,
end: datetime | None = None,
page: Annotated[int, Query(ge=1)] = 1,
limit: Annotated[int, Query(ge=1, le=100)] = 30,
) -> NewsPage:
conditions = news_filters(source, q, start, end)
total = await session.scalar(select(func.count()).select_from(NewsItem).where(*conditions))
rows = (await session.execute(
select(NewsItem, func.left(NewsItem.content_text, EXCERPT_LENGTH).label("excerpt"))