diff --git a/web/eslint.config.js b/web/eslint.config.js index 0769ba8..49d88b1 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -5,6 +5,8 @@ export default antfu( { // 使用外部格式化程序格式化 css、html、markdown 等文件 formatters: true, + // 使用 Node 内置测试,不引入 Vitest。 + test: { overrides: { "test/no-import-node-test": "off" } }, // 启用样式规则 stylistic: { // 缩进级别 @@ -15,7 +17,7 @@ export default antfu( semi: false }, // 忽略文件 - ignores: ["types/auto-imports.d.ts", "types/components.d.ts"] + ignores: ["types/auto/**"] }, { // 对所有文件都生效的规则 diff --git a/web/package.json b/web/package.json index 58bf2e4..c6d0c45 100644 --- a/web/package.json +++ b/web/package.json @@ -6,7 +6,10 @@ "scripts": { "dev": "vite", "build": "vue-tsc && vite build", - "lint": "eslint . --fix" + "lint": "eslint . --fix", + "lint:check": "eslint .", + "typecheck": "vue-tsc --noEmit", + "test": "node --experimental-strip-types --test tests/*.test.ts" }, "dependencies": { "@element-plus/icons-vue": "^2.3.2", diff --git a/web/public/flags/de.svg b/web/public/flags/de.svg new file mode 100644 index 0000000..3c1eba9 --- /dev/null +++ b/web/public/flags/de.svg @@ -0,0 +1 @@ + diff --git a/web/public/flags/ea.svg b/web/public/flags/ea.svg new file mode 100644 index 0000000..6d0d93e --- /dev/null +++ b/web/public/flags/ea.svg @@ -0,0 +1 @@ + diff --git a/web/public/flags/fr.svg b/web/public/flags/fr.svg new file mode 100644 index 0000000..0a23f80 --- /dev/null +++ b/web/public/flags/fr.svg @@ -0,0 +1 @@ + diff --git a/web/public/flags/gb.svg b/web/public/flags/gb.svg new file mode 100644 index 0000000..be99c96 --- /dev/null +++ b/web/public/flags/gb.svg @@ -0,0 +1 @@ + diff --git a/web/public/flags/us.svg b/web/public/flags/us.svg new file mode 100644 index 0000000..51479da --- /dev/null +++ b/web/public/flags/us.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/web/src/App.vue b/web/src/App.vue index 4d47c67..462719f 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,9 +1,12 @@ diff --git a/web/src/apis/chat.ts b/web/src/apis/chat.ts deleted file mode 100644 index 9576405..0000000 --- a/web/src/apis/chat.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { http } from "@/http" - -export interface ToolCall { - id: string - name: string - arguments: Record -} - -export interface ChatMessage { - id: number - role: "user" | "assistant" | "tool" - /** user: {text};assistant: {text, tool_calls, stopped?, error?};tool: {tool_call_id, name, result} */ - content: Record - provider_id: number | null - usage: { input_tokens?: number, output_tokens?: number } | null - created_at: string -} - -export interface ChatSession { - id: number - created_at: string - messages: ChatMessage[] - estimated_tokens: number - generating: boolean -} - -export interface ChatContext { - id: number | null - content: string - source: "user" | "chat" | "reset" | null - created_at: string | null -} - -export function getChatSession(): Promise { - return http.get("/chat/session") -} - -export function resetChat(clearContext: boolean): Promise { - return http.post("/chat/reset", { clear_context: clearContext }) -} - -export function getChatContext(): Promise { - return http.get("/chat/context") -} - -export function saveChatContext(content: string): Promise { - return http.put("/chat/context", { content }) -} - -export function listChatContextVersions(): Promise { - return http.get("/chat/context/versions") -} diff --git a/web/src/apis/market.ts b/web/src/apis/market.ts deleted file mode 100644 index 776ab45..0000000 --- a/web/src/apis/market.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { http } from "@/http" - -export type Market = "spot" | "um" - -export interface IntervalProgress { - done: boolean - percent: number -} - -export interface MarketSymbol { - id: number - market: Market - symbol: string - base: string - quote: string - price_precision: number - volume_precision: number - status: "pending" | "backfilling" | "ready" | "error" | "delisted" - is_default: boolean - added_at: string - last_error: string | null - progress: Record -} - -export interface TradableSymbol { - symbol: string - base: string - quote: string -} - -/** 币安 24hr ticker 原始字段(只列用到的) */ -export interface Ticker24h { - lastPrice: string - priceChangePercent: string - highPrice: string - lowPrice: string - volume: string - quoteVolume: string -} - -/** [open_time, open, high, low, close, volume, quote_volume] */ -export type KlineRow = [number, number, number, number, number, number, number] - -export function listSymbols(): Promise { - return http.get("/market/symbols") -} - -export function addSymbol(market: Market, symbol: string): Promise { - return http.post("/market/symbols", { market, symbol }) -} - -export function deleteSymbol(id: number): Promise { - return http.delete(`/market/symbols/${id}`) -} - -export function listTradable(market: Market): Promise { - return http.get("/market/exchange-info", { params: { market } }) -} - -export function listKlines(params: { market: Market, symbol: string, interval: string, end?: number, limit?: number }): Promise { - return http.get("/market/klines", { params }) -} - -export function getTicker(market: Market, symbol: string): Promise { - return http.get("/market/ticker", { params: { market, symbol } }) -} diff --git a/web/src/apis/settings.ts b/web/src/apis/settings.ts deleted file mode 100644 index 8e761fe..0000000 --- a/web/src/apis/settings.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { http } from "@/http" - -export type Protocol = "openai" | "anthropic" - -/** 思考强度,与后端 llm.Effort 一致 */ -export const EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const - -export type Effort = typeof EFFORTS[number] - -export interface Provider { - id: number - name: string - protocol: Protocol - base_url: string - api_key_hint: string - model: string - max_tokens: number - context_window: number - /** 为空表示不传,由模型决定 */ - reasoning_effort: Effort | null - is_default: boolean - updated_at: string -} - -export interface ProviderForm { - name: string - protocol: Protocol - base_url: string - /** 编辑时为空表示不修改 */ - api_key: string - model: string - max_tokens: number - context_window: number - /** 为空表示不传,由模型决定 */ - reasoning_effort: Effort | null - is_default: boolean -} - -export interface TestResult { - ok: boolean - elapsed_ms: number - reply: string | null - error: string | null -} - -export function listProviders(): Promise { - return http.get("/settings/providers") -} - -export function createProvider(body: ProviderForm): Promise { - return http.post("/settings/providers", body) -} - -export function patchProvider(id: number, body: Partial): Promise { - return http.patch(`/settings/providers/${id}`, body) -} - -export function deleteProvider(id: number): Promise { - return http.delete(`/settings/providers/${id}`) -} - -export function testProvider(id: number): Promise { - // 模型接口读超时 600s 且 SDK 会重试 2 次,前端放宽等待时间 - return http.post(`/settings/providers/${id}/test`, null, { timeout: 1900000 }) -} - -export type ExchangeName = "binance" | "bybit" - -export interface Exchange { - exchange: ExchangeName - configured_exchanges: ExchangeName[] - configured: boolean - api_key_hint: string | null - /** 是否有币种在交易:交易中不能修改 key */ - running: boolean - updated_at: string | null -} - -export interface ExchangeTestResult { - ok: boolean - elapsed_ms: number - available_usdt: string | null - wallet_usdt: string | null - /** 双向持仓模式;引擎使用单向持仓,启动时会切换 */ - dual_side_position: boolean | null - error: string | null -} - -export function getExchange(): Promise { - return http.get("/settings/exchange") -} - -export function putExchange(body: { exchange?: ExchangeName, api_key?: string, api_secret?: string }): Promise { - return http.put("/settings/exchange", body) -} - -export function deleteExchange(): Promise { - return http.delete("/settings/exchange") -} - -export function testExchange(): Promise { - // 后端串行请求两个接口,单次超时 10s - return http.post("/settings/exchange/test", null, { timeout: 30000 }) -} diff --git a/web/src/apis/skills.ts b/web/src/apis/skills.ts deleted file mode 100644 index 64e2bc1..0000000 --- a/web/src/apis/skills.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { http } from "@/http" - -export type NodeKind = "folder" | "skill" - -export interface SkillNode { - id: number - parent_id: number | null - kind: NodeKind - name: string - path: string - sort_order: number - priority: number - enabled: boolean - published_version_no: number | null - latest_version_no: number | null - has_draft: boolean - updated_at: string -} - -export interface SkillVersion { - id: number - version_no: number - content: string - source: "user" | "chat" - note: string - created_at: string -} - -export function listSkillTree(): Promise { - return http.get("/skills/tree") -} - -export function createSkillNode(body: { parent_id: number | null, kind: NodeKind, name: string, priority?: number, content?: string }): Promise { - return http.post("/skills/nodes", body) -} - -export function patchSkillNode(id: number, body: { name?: string, priority?: number, enabled?: boolean }): Promise { - return http.patch(`/skills/nodes/${id}`, body) -} - -export function moveSkillNode(id: number, parentId: number | null, index: number): Promise { - return http.post(`/skills/nodes/${id}/move`, { parent_id: parentId, index }) -} - -export function deleteSkillNode(id: number): Promise { - return http.delete(`/skills/nodes/${id}`) -} - -export function listSkillVersions(id: number): Promise { - return http.get(`/skills/${id}/versions`) -} - -export function saveSkillDraft(id: number, content: string, note = ""): Promise { - return http.post(`/skills/${id}/versions`, { content, note }) -} - -export function publishSkillVersion(id: number, versionId: number): Promise { - return http.post(`/skills/${id}/publish`, { version_id: versionId }) -} diff --git a/web/src/common/apis/auth/index.ts b/web/src/common/apis/auth/index.ts new file mode 100644 index 0000000..d9238c7 --- /dev/null +++ b/web/src/common/apis/auth/index.ts @@ -0,0 +1,46 @@ +import type * as Auth from "./type" +import { request } from "@/http/axios" + +/** 获取当前身份 */ +export function getIdentityApi() { + return request({ + url: "/auth/me", + method: "get" + }) +} + +/** 撤销当前会话 */ +export function revokeSessionApi() { + return request({ + url: "/auth/logout", + method: "post" + }) +} + +/** 获取登录验证码模式及挑战 */ +export function getCaptchaApi() { + return request({ + url: "/auth/captcha", + method: "get", + anonymous: true, + notify: false + }) +} + +/** 提交登录凭据 */ +export function loginApi(body: Auth.LoginForm) { + return request({ + url: "/auth/login", + method: "post", + data: body, + anonymous: true + }) +} + +/** 一次性短票据,只用于行情 WebSocket 握手。 */ +export function createWebSocketTicketApi() { + return request({ + url: "/auth/ws-ticket", + method: "post" + }) +} diff --git a/web/src/common/apis/auth/type.ts b/web/src/common/apis/auth/type.ts new file mode 100644 index 0000000..8b369b0 --- /dev/null +++ b/web/src/common/apis/auth/type.ts @@ -0,0 +1,37 @@ +export interface Identity { + username: string + is_admin: boolean +} + +export type GetIdentityResponseData = Identity + +export type RevokeSessionResponseData = null + +export type Captcha = { enabled: false } | { + enabled: true + captcha_id: string + image: string + expires_in: number +} + +export interface LoginForm { + username: string + password: string + captcha_id?: string + code?: string +} + +export interface LoginResult { + token: string + token_type: "bearer" + expires_at: string +} + +export type GetCaptchaResponseData = Captcha + +export type LoginResponseData = LoginResult + +export interface WebSocketTicket { + ticket: string + expires_in: number +} diff --git a/web/src/common/apis/calendar/index.ts b/web/src/common/apis/calendar/index.ts new file mode 100644 index 0000000..6124f09 --- /dev/null +++ b/web/src/common/apis/calendar/index.ts @@ -0,0 +1,37 @@ +import type * as Calendar from "./type" +import { request } from "@/http/axios" + +/** 按日期范围查询经济日历 */ +export function listCalendarApi(start: string, days: number) { + return request({ + url: "/calendar", + method: "get", + params: { start, days } + }) +} + +/** 读取本期事件修订记录 */ +export function calendarRevisionsApi(id: number) { + return request({ + url: `/calendar/${id}/revisions`, + method: "get" + }) +} + +/** 读取同指标历次公布值 */ +export function calendarHistoryApi(id: number, indicator: string) { + return request({ + url: `/calendar/${id}/history`, + method: "get", + params: { indicator } + }) +} + +/** 读取当前官方历史序列 */ +export function calendarSeriesApi(id: number, indicator: string) { + return request({ + url: `/calendar/${id}/series`, + method: "get", + params: { indicator } + }) +} diff --git a/web/src/common/apis/calendar/type.ts b/web/src/common/apis/calendar/type.ts new file mode 100644 index 0000000..a2df55a --- /dev/null +++ b/web/src/common/apis/calendar/type.ts @@ -0,0 +1,73 @@ +export interface CalendarItem { + release_id: number + indicator: string + name: string + country_code: string + country_name: string + event_name: string + title: string + period: string | null + stage: string | null + source: string + unit: string + importance: number + scheduled_at: string + status: "scheduled" | "waiting" | "delayed" | "released" | "withdrawn" | "error" | "historical" | "unavailable" + actual: string | null + previous: string | null + forecast: string | null + previous_basis: string | null + impact: "pending" | "bullish" | "bearish" | "neutral" | "uncertain" + impact_basis: "forecast" | "previous" | null + explanation: string + release_url: string | null + schedule_url: string + first_received_at: string | null + last_checked_at: string | null + error: string | null + revision_no: number +} + +export interface CalendarData { + items: CalendarItem[] + countries: { code: string, name: string }[] + worker: { at: string, sources: Record } | null + server_time: string +} + +export interface CalendarRevision { + revision_no: number + captured_at: string + payload: { title: string, scheduled_at: string, schedule_status: string, values: Record } +} + +export interface CalendarHistory { + release_id: number + scheduled_at: string + title: string + period: string | null + stage: string | null + actual: string + previous: string | null + forecast: string | null + first_recorded_actual: string | null + first_received_at: string + release_url: string | null + revised: boolean +} + +export type ListCalendarResponseData = CalendarData + +export type CalendarRevisionsResponseData = CalendarRevision[] + +export type CalendarHistoryResponseData = CalendarHistory[] + +export interface CalendarSeriesValue { + period: string + stage: string + value: string + captured_at: string + source_url: string +} + +export type CalendarSeriesResponseData = CalendarSeriesValue[] diff --git a/web/src/common/apis/chat/index.ts b/web/src/common/apis/chat/index.ts new file mode 100644 index 0000000..65451e8 --- /dev/null +++ b/web/src/common/apis/chat/index.ts @@ -0,0 +1,59 @@ +import type * as Chat from "./type" +import { request } from "@/http/axios" +import { consumeStream } from "@/http/stream" + +/** 读取当前对话及生成状态 */ +export function getChatSessionApi() { + return request({ + url: "/chat/session", + method: "get" + }) +} + +/** 重置当前对话,可按选择清空上下文 */ +export function resetChatApi(clearContext: boolean) { + return request({ + url: "/chat/reset", + method: "post", + data: { clear_context: clearContext } + }) +} + +/** 读取 Chat 上下文 */ +export function getChatContextApi() { + return request({ + url: "/chat/context", + method: "get" + }) +} + +/** 保存 Chat 上下文版本 */ +export function saveChatContextApi(content: string) { + return request({ + url: "/chat/context", + method: "put", + data: { content } + }) +} + +/** 读取 Chat 上下文历史 */ +export function listChatContextVersionsApi() { + return request({ + url: "/chat/context/versions", + method: "get" + }) +} + +/** POST 只发送一次;这里声明 SSE 事件协议,生命周期由 Chat 管理。 */ +export function sendChatApi(body: Chat.SendChatRequestData, signal: AbortSignal, version: number, onEvent: Chat.ChatStreamHandler) { + return consumeStream("/api/chat/messages", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + signal + }, version, () => {}, (name, data) => { + // SSE 将事件名与 JSON 分开发送;在协议边界恢复同一事件的关联类型。 + const event = { name, data } as Chat.ChatStreamEvent + onEvent(event) + }) +} diff --git a/web/src/common/apis/chat/type.ts b/web/src/common/apis/chat/type.ts new file mode 100644 index 0000000..7f15b82 --- /dev/null +++ b/web/src/common/apis/chat/type.ts @@ -0,0 +1,65 @@ +import type { Effort } from "../settings/type" + +export interface ToolCall { + id: string + name: string + arguments: Record +} + +interface MessageMetadata { + id: number + provider_id: number | null + usage: { input_tokens?: number, output_tokens?: number } | null + created_at: string +} + +export type ChatMessage = MessageMetadata & ( + | { role: "user", content: { text: string } } + | { role: "assistant", content: { text: string, tool_calls: ToolCall[], stopped?: boolean, error?: string, thinking_blocks?: unknown[] } } + | { role: "tool", content: { tool_call_id: string, name: string, result: string } } +) + +export interface ChatSession { + id: number + created_at: string + messages: ChatMessage[] + estimated_tokens: number + generating: boolean +} + +export interface ChatContext { + id: number | null + content: string + source: "user" | "chat" | "reset" | null + created_at: string | null +} + +export type GetChatSessionResponseData = ChatSession + +export type ResetChatResponseData = null + +export type GetChatContextResponseData = ChatContext + +export type SaveChatContextResponseData = ChatContext + +export type ListChatContextVersionsResponseData = ChatContext[] + +export interface SendChatRequestData { + text: string + provider_id: number + reasoning_effort: Effort | null +} + +export type ChatStreamEvent + = | { name: "prompt", data: { estimated_tokens: number, context_window: number, provider: string } } + | { name: "thinking", data: Record } + | { name: "done", data: Record } + | { name: "context_updated", data: { id: number } } + | { name: "delta", data: { text: string } } + | { name: "tool_call", data: ToolCall } + | { name: "tool_result", data: { id: string, name: string, preview: string } } + | { name: "skill_draft", data: { skill_id: number, path: string, version: number } } + | { name: "usage", data: { input_tokens: number, output_tokens: number } } + | { name: "error", data: { message: string } } + +export type ChatStreamHandler = (event: ChatStreamEvent) => void diff --git a/web/src/common/apis/crawler/index.ts b/web/src/common/apis/crawler/index.ts new file mode 100644 index 0000000..2deeefc --- /dev/null +++ b/web/src/common/apis/crawler/index.ts @@ -0,0 +1,45 @@ +import type * as Crawler from "./type" +import { request } from "@/http/axios" +import { consumeStream } from "@/http/stream" + +/** 读取新闻与日历来源状态 */ +export function listSourcesApi() { + return request({ + url: "/crawler/sources", + method: "get" + }) +} + +/** 修改新闻来源设置 */ +export function patchSourceApi(code: string, body: Crawler.SourcePatch) { + return request({ + url: `/crawler/sources/${code}`, + method: "patch", + data: body + }) +} + +/** 读取来源历史日志 */ +export function listLogsApi(source: string, limit = 200) { + return request({ + url: "/crawler/logs", + method: "get", + params: { source, limit } + }) +} + +/** 请求重启新闻采集 */ +export function restartCrawlerApi(source: string) { + return request({ + url: "/crawler/restart", + method: "post", + data: { source } + }) +} + +/** 只读订阅,重连由订阅生命周期负责。 */ +export function subscribeCrawlerApi(signal: AbortSignal, version: number, onOpen: () => void, onMessage: (data: Crawler.CrawlLog) => void) { + return consumeStream("/api/crawler/logs/stream", { signal }, version, onOpen, (name, data) => { + if (name === "log") onMessage(data) + }) +} diff --git a/web/src/apis/crawler.ts b/web/src/common/apis/crawler/type.ts similarity index 62% rename from web/src/apis/crawler.ts rename to web/src/common/apis/crawler/type.ts index 36a4bbd..095d772 100644 --- a/web/src/apis/crawler.ts +++ b/web/src/common/apis/crawler/type.ts @@ -1,5 +1,3 @@ -import { http } from "@/http" - export interface ProcessStatus { state: "running" | "stopped" | "restarting" pid: number | null @@ -10,6 +8,7 @@ export interface ProcessStatus { export interface SourceInfo { code: string name: string + region_code: string | null list_url: string enabled: boolean interval_seconds: number @@ -32,8 +31,27 @@ export interface SourcesInfo { manager_alive: boolean db_size_bytes: number sources: SourceInfo[] + calendar_sources: CalendarSourceInfo[] } +export interface CalendarSourceInfo { + code: string + name: string + region_code: string + worker_alive: boolean + heartbeat_at: string | null + schedule_checked_at: string | null + schedule_success_at: string | null + latest_result_at: string | null + next_event_at: string | null + overdue_count: number + history_start: string | null + history_end: string | null + errors: string[] +} + +export type ListSourcesResponseData = SourcesInfo + export interface CrawlLog { id: number source_code: string @@ -43,10 +61,6 @@ export interface CrawlLog { message: string } -export function listSources(): Promise { - return http.get("/crawler/sources") -} - export interface SourcePatch { enabled?: boolean interval_seconds?: number @@ -54,14 +68,8 @@ export interface SourcePatch { history_target?: string | null } -export function patchSource(code: string, body: SourcePatch): Promise { - return http.patch(`/crawler/sources/${code}`, body) -} +export type PatchSourceResponseData = null -export function listLogs(source: string, limit = 200): Promise { - return http.get("/crawler/logs", { params: { source, limit } }) -} +export type ListLogsResponseData = CrawlLog[] -export function restartCrawler(source: string): Promise { - return http.post("/crawler/restart", { source }) -} +export type RestartCrawlerResponseData = null diff --git a/web/src/common/apis/market/index.ts b/web/src/common/apis/market/index.ts new file mode 100644 index 0000000..6b6b713 --- /dev/null +++ b/web/src/common/apis/market/index.ts @@ -0,0 +1,59 @@ +import type * as Market from "./type" +import { request } from "@/http/axios" + +/** 读取研究行情交易对 */ +export function listSymbolsApi() { + return request({ + url: "/market/symbols", + method: "get" + }) +} + +/** 新增研究行情交易对 */ +export function addSymbolApi(market: Market.Market, symbol: string) { + return request({ + url: "/market/symbols", + method: "post", + data: { market, symbol } + }) +} + +/** 移出研究行情交易对 */ +export function deleteSymbolApi(id: number) { + return request({ + url: `/market/symbols/${id}`, + method: "delete" + }) +} + +/** 查询交易所可用交易对 */ +export function listTradableApi(market: Market.Market) { + return request({ + url: "/market/exchange-info", + method: "get", + params: { market } + }) +} + +/** 读取币安原生 K 线 */ +export function listKlinesApi(params: Market.ListKlinesParamsRequestData) { + return request({ + url: "/market/klines", + method: "get", + params + }) +} + +/** 读取币安 24 小时行情 */ +export function getTickerApi(market: Market.Market, symbol: string) { + return request({ + url: "/market/ticker", + method: "get", + params: { market, symbol } + }) +} + +/** 使用一次性票据连接行情 WebSocket */ +export function createMarketSocketApi(ticket: string) { + return new WebSocket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/api/market/ws`, ["alphapulse.market.v1", `ticket.${ticket}`]) +} diff --git a/web/src/common/apis/market/type.ts b/web/src/common/apis/market/type.ts new file mode 100644 index 0000000..65a1f1a --- /dev/null +++ b/web/src/common/apis/market/type.ts @@ -0,0 +1,86 @@ +export type Market = "spot" | "um" + +export interface IntervalProgress { + done: boolean + percent: number +} + +export interface MarketSymbol { + id: number + market: Market + symbol: string + base: string + quote: string + price_precision: number + volume_precision: number + status: "pending" | "backfilling" | "ready" | "error" | "delisted" + is_default: boolean + added_at: string + last_error: string | null + progress: Record +} + +export interface TradableSymbol { + symbol: string + base: string + quote: string +} + +/** 币安 24hr ticker 原始字段(只列用到的) */ +export interface Ticker24h { + lastPrice: string + priceChangePercent: string + highPrice: string + lowPrice: string + volume: string + quoteVolume: string +} + +/** [open_time, open, high, low, close, volume, quote_volume] */ +export type KlineRow = [number, number, number, number, number, number, number] + +export type ListSymbolsResponseData = MarketSymbol[] + +export type AddSymbolResponseData = null + +export type DeleteSymbolResponseData = null + +export type ListTradableResponseData = TradableSymbol[] + +export type ListKlinesResponseData = KlineRow[] + +export interface ListKlinesParamsRequestData { market: Market, symbol: string, interval: string, end?: number, limit?: number } + +export type GetTickerResponseData = Ticker24h + +export interface KlineMessage { + t: number + o: string + h: string + l: string + c: string + v: string + q: string + x: boolean +} + +export interface TickerMessage { + c: string + o: string + h: string + l: string + v: string + q: string + E: number +} + +export interface Subscription { + market: Market + symbol: string + interval: string +} + +export type MarketMessage + = | { type: "kline", market: Market, symbol: string, interval: string, data: KlineMessage } + | { type: "ticker", market: Market, symbol: string, interval: null, data: TickerMessage } + | { type: "error", message: string } diff --git a/web/src/common/apis/news/index.ts b/web/src/common/apis/news/index.ts new file mode 100644 index 0000000..ea73c40 --- /dev/null +++ b/web/src/common/apis/news/index.ts @@ -0,0 +1,36 @@ +import type * as News from "./type" +import { request } from "@/http/axios" +import { consumeStream } from "@/http/stream" + +/** 分页查询新闻 */ +export function listNewsApi(params: News.NewsQuery) { + return request({ + url: "/news", + method: "get", + params, + paramsSerializer: { indexes: null } + }) +} + +/** 读取新闻详情 */ +export function getNewsApi(id: number) { + return request({ + url: `/news/${id}`, + method: "get" + }) +} + +/** 读取新闻修订记录 */ +export function listRevisionsApi(id: number) { + return request({ + url: `/news/${id}/revisions`, + method: "get" + }) +} + +/** 只读订阅,重连由订阅生命周期负责。 */ +export function subscribeNewsApi(signal: AbortSignal, version: number, onOpen: () => void, onMessage: (data: News.NewsEvent) => void) { + return consumeStream("/api/news/stream", { signal }, version, onOpen, (name, data) => { + if (name === "news") onMessage(data) + }) +} diff --git a/web/src/apis/news.ts b/web/src/common/apis/news/type.ts similarity index 63% rename from web/src/apis/news.ts rename to web/src/common/apis/news/type.ts index ea82f02..22dfb93 100644 --- a/web/src/apis/news.ts +++ b/web/src/common/apis/news/type.ts @@ -1,5 +1,3 @@ -import { http } from "@/http" - export interface NewsBrief { id: number source_code: string @@ -15,6 +13,18 @@ export interface NewsBrief { revision_no: number } +export interface NewsQuery { + source?: string[] + q?: string + start?: string + end?: string + /** 从 1 开始 */ + page?: number + limit?: number +} + +export interface ListNewsResponseData { items: NewsBrief[], total: number } + export interface NewsDetail extends NewsBrief { content_html: string source_updated_at: string | null @@ -30,31 +40,12 @@ export interface Revision { captured_at: string } -export interface NewsQuery { - source?: string[] - q?: string - start?: string - end?: string - /** 从 1 开始 */ - page?: number - limit?: number -} - export interface NewsEvent { id: number source_code: string kind: "new" | "revised" } -export function listNews(params: NewsQuery): Promise<{ items: NewsBrief[], total: number }> { - // source 需要序列化为重复参数 source=a&source=b - return http.get("/news", { params, paramsSerializer: { indexes: null } }) -} +export type GetNewsResponseData = NewsDetail -export function getNews(id: number): Promise { - return http.get(`/news/${id}`) -} - -export function listRevisions(id: number): Promise { - return http.get(`/news/${id}/revisions`) -} +export type ListRevisionsResponseData = Revision[] diff --git a/web/src/common/apis/settings/index.ts b/web/src/common/apis/settings/index.ts new file mode 100644 index 0000000..dee76c2 --- /dev/null +++ b/web/src/common/apis/settings/index.ts @@ -0,0 +1,81 @@ +import type * as Settings from "./type" +import { request } from "@/http/axios" + +/** 读取模型配置(不返回密钥) */ +export function listProvidersApi() { + return request({ + url: "/settings/providers", + method: "get" + }) +} + +/** 新增模型配置 */ +export function createProviderApi(body: Settings.ProviderForm) { + return request({ + url: "/settings/providers", + method: "post", + data: body + }) +} + +/** 修改模型配置;空密钥保持原值 */ +export function patchProviderApi(id: number, body: Partial) { + return request({ + url: `/settings/providers/${id}`, + method: "patch", + data: body + }) +} + +/** 删除模型配置 */ +export function deleteProviderApi(id: number) { + return request({ + url: `/settings/providers/${id}`, + method: "delete" + }) +} + +/** 测试模型连接;按后端 SDK 重试上限等待 */ +export function testProviderApi(id: number) { + return request({ + url: `/settings/providers/${id}/test`, + method: "post", + data: null, + timeout: 1900000 + }) +} + +/** 读取当前模拟盘交易所设置 */ +export function getExchangeApi() { + return request({ + url: "/settings/exchange", + method: "get" + }) +} + +/** 切换交易所或保存其凭证 */ +export function putExchangeApi(body: Settings.PutExchangeBodyRequestData) { + return request({ + url: "/settings/exchange", + method: "put", + data: body + }) +} + +/** 删除当前交易所凭证 */ +export function deleteExchangeApi() { + return request({ + url: "/settings/exchange", + method: "delete" + }) +} + +/** 测试当前模拟盘连接 */ +export function testExchangeApi() { + return request({ + url: "/settings/exchange/test", + method: "post", + data: null, + timeout: 30000 + }) +} diff --git a/web/src/common/apis/settings/type.ts b/web/src/common/apis/settings/type.ts new file mode 100644 index 0000000..87a08f1 --- /dev/null +++ b/web/src/common/apis/settings/type.ts @@ -0,0 +1,81 @@ +export type Protocol = "openai" | "anthropic" + +export type Effort = "low" | "medium" | "high" | "xhigh" | "max" + +export interface Provider { + id: number + name: string + protocol: Protocol + base_url: string + api_key_hint: string + model: string + max_tokens: number + context_window: number + /** 为空表示不传,由模型决定 */ + reasoning_effort: Effort | null + is_default: boolean + updated_at: string +} + +export type ListProvidersResponseData = Provider[] + +export interface ProviderForm { + name: string + protocol: Protocol + base_url: string + /** 编辑时为空表示不修改 */ + api_key: string + model: string + max_tokens: number + context_window: number + /** 为空表示不传,由模型决定 */ + reasoning_effort: Effort | null + is_default: boolean +} + +export interface TestResult { + ok: boolean + elapsed_ms: number + reply: string | null + error: string | null +} + +export type CreateProviderResponseData = Provider + +export type PatchProviderResponseData = Provider + +export type DeleteProviderResponseData = null + +export type TestProviderResponseData = TestResult + +export type ExchangeName = "binance" | "bybit" + +export interface Exchange { + exchange: ExchangeName + configured_exchanges: ExchangeName[] + configured: boolean + api_key_hint: string | null + /** 是否有币种在交易:交易中不能修改 key */ + running: boolean + updated_at: string | null +} + +export interface ExchangeTestResult { + ok: boolean + elapsed_ms: number + available_usdt: string | null + wallet_usdt: string | null + /** 双向持仓模式;引擎使用单向持仓,启动时会切换 */ + dual_side_position: boolean | null + error: string | null +} + +export type GetExchangeResponseData = Exchange + +export type PutExchangeResponseData = Exchange + +export interface PutExchangeBodyRequestData { exchange?: ExchangeName, api_key?: string, api_secret?: string } + +export type DeleteExchangeResponseData = Exchange + +export type TestExchangeResponseData = ExchangeTestResult diff --git a/web/src/common/apis/skills/index.ts b/web/src/common/apis/skills/index.ts new file mode 100644 index 0000000..0142538 --- /dev/null +++ b/web/src/common/apis/skills/index.ts @@ -0,0 +1,71 @@ +import type * as Skills from "./type" +import { request } from "@/http/axios" + +/** 读取 Skills 树 */ +export function listSkillTreeApi() { + return request({ + url: "/skills/tree", + method: "get" + }) +} + +/** 新增文件夹或 Skill */ +export function createSkillNodeApi(body: Skills.CreateSkillNodeBodyRequestData) { + return request({ + url: "/skills/nodes", + method: "post", + data: body + }) +} + +/** 修改 Skill 元信息 */ +export function patchSkillNodeApi(id: number, body: Skills.PatchSkillNodeBodyRequestData) { + return request({ + url: `/skills/nodes/${id}`, + method: "patch", + data: body + }) +} + +/** 移动 Skill 节点 */ +export function moveSkillNodeApi(id: number, parentId: number | null, index: number) { + return request({ + url: `/skills/nodes/${id}/move`, + method: "post", + data: { parent_id: parentId, index } + }) +} + +/** 删除 Skill 节点 */ +export function deleteSkillNodeApi(id: number) { + return request({ + url: `/skills/nodes/${id}`, + method: "delete" + }) +} + +/** 读取 Skill 历史版本 */ +export function listSkillVersionsApi(id: number) { + return request({ + url: `/skills/${id}/versions`, + method: "get" + }) +} + +/** 发布指定 Skill 版本 */ +export function publishSkillVersionApi(id: number, versionId: number) { + return request({ + url: `/skills/${id}/publish`, + method: "post", + data: { version_id: versionId } + }) +} + +/** 保存 Skill 草稿,不自动发布 */ +export function saveSkillDraftApi(id: number, content: string, note = "") { + return request({ + url: `/skills/${id}/versions`, + method: "post", + data: { content, note } + }) +} diff --git a/web/src/common/apis/skills/type.ts b/web/src/common/apis/skills/type.ts new file mode 100644 index 0000000..2512c0f --- /dev/null +++ b/web/src/common/apis/skills/type.ts @@ -0,0 +1,45 @@ +export type NodeKind = "folder" | "skill" + +export interface SkillNode { + id: number + parent_id: number | null + kind: NodeKind + name: string + path: string + sort_order: number + priority: number + enabled: boolean + published_version_no: number | null + latest_version_no: number | null + has_draft: boolean + updated_at: string +} + +export interface SkillVersion { + id: number + version_no: number + content: string + source: "user" | "chat" + note: string + created_at: string +} + +export type ListSkillTreeResponseData = SkillNode[] + +export type CreateSkillNodeResponseData = SkillNode + +export interface CreateSkillNodeBodyRequestData { parent_id: number | null, kind: NodeKind, name: string, priority?: number, content?: string } + +export type PatchSkillNodeResponseData = SkillNode + +export interface PatchSkillNodeBodyRequestData { name?: string, priority?: number, enabled?: boolean } + +export type MoveSkillNodeResponseData = null + +export type DeleteSkillNodeResponseData = null + +export type ListSkillVersionsResponseData = SkillVersion[] + +export type PublishSkillVersionResponseData = SkillNode + +export type SaveSkillDraftResponseData = SkillVersion diff --git a/web/src/common/apis/trading/index.ts b/web/src/common/apis/trading/index.ts new file mode 100644 index 0000000..d700f8a --- /dev/null +++ b/web/src/common/apis/trading/index.ts @@ -0,0 +1,152 @@ +import type * as Trading from "./type" +import { request } from "@/http/axios" + +/** 读取模拟盘账户与全局分析状态 */ +export function getStatusApi() { + return request({ + url: "/trading/status", + method: "get" + }) +} + +/** 开始指定币种的自动交易 */ +export function startTradingApi(symbol: string) { + return request({ + url: `/trading/symbols/${symbol}/start`, + method: "post" + }) +} + +/** 停止指定币种的新分析并撤销入场单 */ +export function stopTradingApi(symbol: string) { + return request({ + url: `/trading/symbols/${symbol}/stop`, + method: "post" + }) +} + +/** 设置该币的交易资金:各币合计不超过账户权益 */ +export function patchSymbolApi(symbol: string, body: Trading.PatchSymbolBodyRequestData) { + return request({ + url: `/trading/symbols/${symbol}`, + method: "patch", + data: body + }) +} + +/** 更新指定的全局交易配置字段 */ +export function patchConfigApi(body: Trading.PatchConfigBodyRequestData) { + return request({ + url: "/trading/config", + method: "patch", + data: body + }) +} + +/** 查询模拟盘可交易币种 */ +export function listSymbolOptionsApi() { + return request({ + url: "/trading/symbol-options", + method: "get" + }) +} + +/** 读取交易币种列表 */ +export function listSymbolsApi() { + return request({ + url: "/trading/symbols", + method: "get" + }) +} + +/** 新增交易币种 */ +export function addSymbolApi(symbol: string) { + return request({ + url: "/trading/symbols", + method: "post", + data: { symbol } + }) +} + +/** 移出交易币种,保留历史 */ +export function deleteSymbolApi(symbol: string) { + return request({ + url: `/trading/symbols/${symbol}`, + method: "delete" + }) +} + +/** 分页读取交易时间线 */ +export function listEventsApi(params: Trading.ListEventsParamsRequestData) { + return request({ + url: "/trading/events", + method: "get", + params + }) +} + +/** 筛选已平仓历史及汇总 */ +export function listPositionHistoryApi(params: Trading.PositionHistoryQuery) { + return request({ + url: "/trading/positions/history", + method: "get", + params + }) +} + +/** 读取历史仓位币种 */ +export function listPositionHistorySymbolsApi() { + return request({ + url: "/trading/positions/history/symbols", + method: "get" + }) +} + +/** 读取新闻信号前向统计 */ +export function getNewsSignalsApi() { + return request({ + url: "/trading/news-signals", + method: "get" + }) +} + +/** 读取当前执行计划与订单流水 */ +export function getSymbolPlanApi(symbol: string) { + return request({ + url: `/trading/symbols/${symbol}/plan`, + method: "get" + }) +} + +/** 排队手动分析;202 仅表示已受理 */ +export function requestAnalysisApi(symbol: string) { + return request({ + url: `/trading/symbols/${symbol}/analyze`, + method: "post" + }) +} + +/** 一键平仓(同时停止该币的行情交易)、清除挂单:由执行器在几秒内执行,结果写入时间线 */ +export function manualActionApi(symbol: string, action: "close" | "cancel") { + return request({ + url: `/trading/symbols/${symbol}/manual/${action}`, + method: "post" + }) +} + +/** 读取分析记录列表 */ +export function listAnalysesApi(params: Trading.ListAnalysesParamsRequestData) { + return request({ + url: "/trading/analyses", + method: "get", + params + }) +} + +/** 读取分析详情与工具调用过程 */ +export function getAnalysisApi(id: number) { + return request({ + url: `/trading/analyses/${id}`, + method: "get" + }) +} diff --git a/web/src/apis/trading.ts b/web/src/common/apis/trading/type.ts similarity index 64% rename from web/src/apis/trading.ts rename to web/src/common/apis/trading/type.ts index f995e65..a846792 100644 --- a/web/src/apis/trading.ts +++ b/web/src/common/apis/trading/type.ts @@ -1,5 +1,4 @@ -import type { Effort } from "@/apis/settings" -import { http } from "@/http" +import type { Effort } from "../settings/type" /** 以下数值字段都是交易所返回的十进制字符串,避免浮点误差 */ export interface SnapshotPosition { @@ -97,51 +96,31 @@ export interface TradingEvent { data: Record | null } -/** 本程序下的单;其他是用户手动下的 */ -export function isOwnOrder(clientId: string) { - return /^ap\d+$/.test(clientId) -} +export type GetStatusResponseData = TradingStatus -export function getStatus(): Promise { - return http.get("/trading/status") -} +export type StartTradingResponseData = null -export function startTrading(symbol: string): Promise { - return http.post(`/trading/symbols/${symbol}/start`) -} +export type StopTradingResponseData = null -export function stopTrading(symbol: string): Promise { - return http.post(`/trading/symbols/${symbol}/stop`) -} +export type PatchSymbolResponseData = null -/** 设置该币的交易资金:各币合计不超过账户权益 */ -export function patchSymbol(symbol: string, body: { capital_usdt: string | null }): Promise { - return http.patch(`/trading/symbols/${symbol}`, body) -} +export interface PatchSymbolBodyRequestData { capital_usdt: string | null } -export function patchConfig(body: { provider_id?: number | null, reasoning_effort?: Effort | null, daily_analysis_limit?: number }): Promise { - return http.patch("/trading/config", body) -} +export type PatchConfigResponseData = TradingStatus -export function listSymbolOptions(): Promise { - return http.get("/trading/symbol-options") -} +export interface PatchConfigBodyRequestData { provider_id?: number | null, reasoning_effort?: Effort | null, daily_analysis_limit?: number } -export function listSymbols(): Promise { - return http.get("/trading/symbols") -} +export type ListSymbolOptionsResponseData = SymbolOption[] -export function addSymbol(symbol: string): Promise { - return http.post("/trading/symbols", { symbol }) -} +export type ListSymbolsResponseData = TradingSymbol[] -export function deleteSymbol(symbol: string): Promise { - return http.delete(`/trading/symbols/${symbol}`) -} +export type AddSymbolResponseData = null -export function listEvents(params: { symbol?: string, before?: number, limit?: number }): Promise { - return http.get("/trading/events", { params }) -} +export type DeleteSymbolResponseData = null + +export type ListEventsResponseData = TradingEvent[] + +export interface ListEventsParamsRequestData { symbol?: string, before?: number, limit?: number } export interface PlanCondition { direction: "above" | "below" @@ -224,18 +203,6 @@ export interface AnalysisDetail extends Analysis { export type PlanStatus = "proposed" | "pending" | "open" | "closing" | "closed" | "canceled" | "replaced" | "stale" | "applied" -export const PLAN_STATUS_LABELS: Record = { - proposed: "待采纳", - pending: "挂单中", - open: "持仓中", - closing: "平仓中", - closed: "已结束", - canceled: "已撤销", - replaced: "已被替换", - stale: "已作废", - applied: "已执行" -} - /** 执行器的订单流水 */ export interface OrderRow { id: number @@ -253,30 +220,6 @@ export interface OrderRow { created_at: string } -export const CLOSE_REASON_LABELS: Record = { - stop: "止损", - take_profit: "止盈", - time: "到达持仓时限", - exit: "模型决定结束", - external: "外部平仓", - stop_failed: "止损挂单失败", - close: "市价平仓", - manual_close: "一键平仓", - manual_cancel: "清除挂单", - // 计划 3.1 之前的旧原因,历史仓位里还会出现 - invalidation: "短线失效条件触发", - replaced: "被新计划替换", - expired: "限价单到期未成交", - stopped: "已停止交易", - entry_failed: "入场单被拒" -} - -/** 止盈阶梯(与后端 plan.TAKE_PROFIT_STEPS 一致):[距成交均价的涨跌幅, 平仓比例] */ -export const TAKE_PROFIT_STEPS: Record<"long" | "short", [number, number][]> = { - long: [[0.005, 0.6], [0.0075, 0.3], [0.01, 0.1]], - short: [[0.005, 0.8], [0.0075, 0.2]] -} - /** 由当前交易所成交记录还原的已平仓仓位(含程序外的手动仓位);数值为十进制字符串 */ export interface PositionHistory { symbol: string @@ -313,13 +256,9 @@ export interface PositionHistoryPage { items: PositionHistory[] } -export function listPositionHistory(params: PositionHistoryQuery): Promise { - return http.get("/trading/positions/history", { params }) -} +export type ListPositionHistoryResponseData = PositionHistoryPage -export function listPositionHistorySymbols(): Promise { - return http.get("/trading/positions/history/symbols") -} +export type ListPositionHistorySymbolsResponseData = string[] export type SignalHorizon = "1h" | "4h" @@ -356,27 +295,16 @@ export interface NewsSignals { recent: NewsSignalItem[] } -export function getNewsSignals(): Promise { - return http.get("/trading/news-signals") -} +export type GetNewsSignalsResponseData = NewsSignals -export function getSymbolPlan(symbol: string): Promise<{ plan: Analysis | null, orders: OrderRow[] }> { - return http.get(`/trading/symbols/${symbol}/plan`) -} +export interface GetSymbolPlanResponseData { plan: Analysis | null, orders: OrderRow[] } -export function requestAnalysis(symbol: string): Promise { - return http.post(`/trading/symbols/${symbol}/analyze`) -} +export type RequestAnalysisResponseData = null -/** 一键平仓(同时停止该币的行情交易)、清除挂单:由执行器在几秒内执行,结果写入时间线 */ -export function manualAction(symbol: string, action: "close" | "cancel"): Promise { - return http.post(`/trading/symbols/${symbol}/manual/${action}`) -} +export type ManualActionResponseData = null -export function listAnalyses(params: { symbol?: string, limit?: number }): Promise { - return http.get("/trading/analyses", { params }) -} +export type ListAnalysesResponseData = Analysis[] -export function getAnalysis(id: number): Promise { - return http.get(`/trading/analyses/${id}`) -} +export interface ListAnalysesParamsRequestData { symbol?: string, limit?: number } + +export type GetAnalysisResponseData = AnalysisDetail diff --git a/web/src/styles/index.css b/web/src/common/assets/styles/index.css similarity index 100% rename from web/src/styles/index.css rename to web/src/common/assets/styles/index.css diff --git a/web/src/layouts/index.vue b/web/src/common/components/AppLayout/index.vue similarity index 62% rename from web/src/layouts/index.vue rename to web/src/common/components/AppLayout/index.vue index a9f2e52..5d9ea2d 100644 --- a/web/src/layouts/index.vue +++ b/web/src/common/components/AppLayout/index.vue @@ -1,7 +1,7 @@ + + + + diff --git a/web/src/pages/calendar/index.vue b/web/src/pages/calendar/index.vue new file mode 100644 index 0000000..01cd6f3 --- /dev/null +++ b/web/src/pages/calendar/index.vue @@ -0,0 +1,376 @@ + + + + + diff --git a/web/src/pages/chat/components/ContextTab.vue b/web/src/pages/chat/components/ContextTab.vue index 77aedbd..52c2622 100644 --- a/web/src/pages/chat/components/ContextTab.vue +++ b/web/src/pages/chat/components/ContextTab.vue @@ -1,8 +1,8 @@