From 03236bd65ce9c56bbb82be4bd20a038173cccbd7 Mon Sep 17 00:00:00 2001 From: xiaobo-gaga Date: Fri, 27 Feb 2026 18:24:54 +0800 Subject: [PATCH 1/6] Add HTTP API endpoint index under API Reference (en/zh-CN/zh-HK) --- docs/en/docs/api-reference/http-endpoints.md | 45 +++++++++++++++++++ .../docs/api-reference/http-endpoints.md | 45 +++++++++++++++++++ .../docs/api-reference/http-endpoints.md | 45 +++++++++++++++++++ 3 files changed, 135 insertions(+) create mode 100644 docs/en/docs/api-reference/http-endpoints.md create mode 100644 docs/zh-CN/docs/api-reference/http-endpoints.md create mode 100644 docs/zh-HK/docs/api-reference/http-endpoints.md diff --git a/docs/en/docs/api-reference/http-endpoints.md b/docs/en/docs/api-reference/http-endpoints.md new file mode 100644 index 00000000..e5ce5cc5 --- /dev/null +++ b/docs/en/docs/api-reference/http-endpoints.md @@ -0,0 +1,45 @@ +--- +title: HTTP API Endpoints +id: http-api-endpoints +slug: /http-api-endpoints +sidebar_position: 2 +--- + +This page is generated by reverse-checking the SDK method mappings in current docs, to make HTTP API discovery easier from **API Reference**. + +## Trade APIs + +| Method | HTTP URL | SDK Method | Detailed Doc | +| --- | --- | --- | --- | +| GET | `/v1/trade/order/today` | `trade.today_orders` | [Today Orders](../trade/order/today_orders) | +| GET | `/v1/trade/estimate/buy_limit` | `trade.estimate_max_purchase_quantity` | [Estimate Available Buy Limit](../trade/order/estimate_available_buy_limit) | +| POST | `/v1/trade/order` | `trade.submit_order` | [Submit Order](../trade/order/submit) | +| PUT | `/v1/trade/order` | `trade.replace_order` | [Replace Order](../trade/order/replace) | +| DELETE | `/v1/trade/order` | `trade.cancel_order` | [Withdraw Order](../trade/order/withdraw) | +| GET | `/v1/trade/order` | `trade.order_detail` | [Order Detail](../trade/order/order_detail) | +| GET | `/v1/trade/order/history` | `trade.history_orders` | [History Orders](../trade/order/history_orders) | +| GET | `/v1/trade/execution/today` | `trade.today_executions` | [Today Executions](../trade/execution/today_executions) | +| GET | `/v1/trade/execution/history` | `trade.history_executions` | [History Executions](../trade/execution/history_executions) | +| GET | `/v1/asset/account` | `trade.account_balance` | [Account Balance](../trade/asset/account) | +| GET | `/v1/asset/cashflow` | `trade.cash_flow` | [Cash Flow](../trade/asset/cashflow) | +| GET | `/v1/asset/fund` | `trade.fund_positions` | [Fund Positions](../trade/asset/fund) | +| GET | `/v1/asset/stock` | `trade.stock_positions` | [Stock Positions](../trade/asset/stock) | +| GET | `/v1/risk/margin-ratio` | `trade.margin_ratio` | [Margin Ratio](../trade/asset/margin_ratio) | + +## Quote APIs + +| Method | HTTP URL | SDK Method | Detailed Doc | +| --- | --- | --- | --- | +| GET | `/v1/quote/market_temperature` | `quote.market_temperature` | [Market Temperature](../quote/pull/market-temp) | +| GET | `/v1/quote/history_market_temperature` | `quote.history_market_temperature` | [History Market Temperature](../quote/pull/history-market-temp) | +| GET | `/v1/quote/get_security_list` | `quote.security_list` | [Security List](../quote/security/security) | +| GET | `/v1/watchlist/groups` | `quote.watchlist` | [Watchlist Groups](../quote/individual/watchlist_groups) | +| POST | `/v1/watchlist/groups` | `quote.create_watchlist_group` | [Create Watchlist Group](../quote/individual/watchlist_create_group) | +| PUT | `/v1/watchlist/groups` | `quote.update_watchlist_group` | [Update Watchlist Group](../quote/individual/watchlist_update_group) | +| DELETE | `/v1/watchlist/groups` | `quote.delete_watchlist_group` | [Delete Watchlist Group](../quote/individual/watchlist_delete_group) | + +## Notes + +- If you are looking for authentication/signature details, see [Overview](./how-to-access-api). +- If you need endpoint-level request/response examples, use the "Detailed Doc" links above. +- Next step: we can continue expanding this page with complete parameter snapshots generated from SDK/protocol definitions. diff --git a/docs/zh-CN/docs/api-reference/http-endpoints.md b/docs/zh-CN/docs/api-reference/http-endpoints.md new file mode 100644 index 00000000..368648d2 --- /dev/null +++ b/docs/zh-CN/docs/api-reference/http-endpoints.md @@ -0,0 +1,45 @@ +--- +title: HTTP API 端点索引 +id: http-api-endpoints +slug: /http-api-endpoints +sidebar_position: 2 +--- + +这个页面基于当前文档里的 SDK 映射反向整理,目的是让 **API 附录** 中更容易检索 HTTP API。 + +## 交易类 API + +| 方法 | HTTP URL | SDK 方法 | 详细文档 | +| --- | --- | --- | --- | +| GET | `/v1/trade/order/today` | `trade.today_orders` | [今日订单](../trade/order/today_orders) | +| GET | `/v1/trade/estimate/buy_limit` | `trade.estimate_max_purchase_quantity` | [预估最大可买数量](../trade/order/estimate_available_buy_limit) | +| POST | `/v1/trade/order` | `trade.submit_order` | [提交订单](../trade/order/submit) | +| PUT | `/v1/trade/order` | `trade.replace_order` | [修改订单](../trade/order/replace) | +| DELETE | `/v1/trade/order` | `trade.cancel_order` | [撤销订单](../trade/order/withdraw) | +| GET | `/v1/trade/order` | `trade.order_detail` | [订单详情](../trade/order/order_detail) | +| GET | `/v1/trade/order/history` | `trade.history_orders` | [历史订单](../trade/order/history_orders) | +| GET | `/v1/trade/execution/today` | `trade.today_executions` | [今日成交](../trade/execution/today_executions) | +| GET | `/v1/trade/execution/history` | `trade.history_executions` | [历史成交](../trade/execution/history_executions) | +| GET | `/v1/asset/account` | `trade.account_balance` | [账户余额](../trade/asset/account) | +| GET | `/v1/asset/cashflow` | `trade.cash_flow` | [现金流水](../trade/asset/cashflow) | +| GET | `/v1/asset/fund` | `trade.fund_positions` | [基金持仓](../trade/asset/fund) | +| GET | `/v1/asset/stock` | `trade.stock_positions` | [股票持仓](../trade/asset/stock) | +| GET | `/v1/risk/margin-ratio` | `trade.margin_ratio` | [保证金比例](../trade/asset/margin_ratio) | + +## 行情类 API + +| 方法 | HTTP URL | SDK 方法 | 详细文档 | +| --- | --- | --- | --- | +| GET | `/v1/quote/market_temperature` | `quote.market_temperature` | [市场温度](../quote/pull/market-temp) | +| GET | `/v1/quote/history_market_temperature` | `quote.history_market_temperature` | [历史市场温度](../quote/pull/history-market-temp) | +| GET | `/v1/quote/get_security_list` | `quote.security_list` | [标的列表](../quote/security/security) | +| GET | `/v1/watchlist/groups` | `quote.watchlist` | [自选组列表](../quote/individual/watchlist_groups) | +| POST | `/v1/watchlist/groups` | `quote.create_watchlist_group` | [创建自选组](../quote/individual/watchlist_create_group) | +| PUT | `/v1/watchlist/groups` | `quote.update_watchlist_group` | [更新自选组](../quote/individual/watchlist_update_group) | +| DELETE | `/v1/watchlist/groups` | `quote.delete_watchlist_group` | [删除自选组](../quote/individual/watchlist_delete_group) | + +## 说明 + +- 鉴权、签名、请求基础规范请看 [总览](./how-to-access-api)。 +- 每个接口的参数与请求/响应示例,请点击上面的“详细文档”。 +- 下一步可继续补齐:从 SDK/协议自动生成更完整的参数快照。 diff --git a/docs/zh-HK/docs/api-reference/http-endpoints.md b/docs/zh-HK/docs/api-reference/http-endpoints.md new file mode 100644 index 00000000..ce9e3dfc --- /dev/null +++ b/docs/zh-HK/docs/api-reference/http-endpoints.md @@ -0,0 +1,45 @@ +--- +title: HTTP API 端點索引 +id: http-api-endpoints +slug: /http-api-endpoints +sidebar_position: 2 +--- + +此頁基於目前文件中的 SDK 映射反向整理,目標是讓 **API Reference** 更容易檢索 HTTP API。 + +## 交易類 API + +| 方法 | HTTP URL | SDK 方法 | 詳細文件 | +| --- | --- | --- | --- | +| GET | `/v1/trade/order/today` | `trade.today_orders` | [今日訂單](../trade/order/today_orders) | +| GET | `/v1/trade/estimate/buy_limit` | `trade.estimate_max_purchase_quantity` | [預估最大可買數量](../trade/order/estimate_available_buy_limit) | +| POST | `/v1/trade/order` | `trade.submit_order` | [提交訂單](../trade/order/submit) | +| PUT | `/v1/trade/order` | `trade.replace_order` | [修改訂單](../trade/order/replace) | +| DELETE | `/v1/trade/order` | `trade.cancel_order` | [撤銷訂單](../trade/order/withdraw) | +| GET | `/v1/trade/order` | `trade.order_detail` | [訂單詳情](../trade/order/order_detail) | +| GET | `/v1/trade/order/history` | `trade.history_orders` | [歷史訂單](../trade/order/history_orders) | +| GET | `/v1/trade/execution/today` | `trade.today_executions` | [今日成交](../trade/execution/today_executions) | +| GET | `/v1/trade/execution/history` | `trade.history_executions` | [歷史成交](../trade/execution/history_executions) | +| GET | `/v1/asset/account` | `trade.account_balance` | [帳戶餘額](../trade/asset/account) | +| GET | `/v1/asset/cashflow` | `trade.cash_flow` | [現金流水](../trade/asset/cashflow) | +| GET | `/v1/asset/fund` | `trade.fund_positions` | [基金持倉](../trade/asset/fund) | +| GET | `/v1/asset/stock` | `trade.stock_positions` | [股票持倉](../trade/asset/stock) | +| GET | `/v1/risk/margin-ratio` | `trade.margin_ratio` | [保證金比例](../trade/asset/margin_ratio) | + +## 行情類 API + +| 方法 | HTTP URL | SDK 方法 | 詳細文件 | +| --- | --- | --- | --- | +| GET | `/v1/quote/market_temperature` | `quote.market_temperature` | [市場溫度](../quote/pull/market-temp) | +| GET | `/v1/quote/history_market_temperature` | `quote.history_market_temperature` | [歷史市場溫度](../quote/pull/history-market-temp) | +| GET | `/v1/quote/get_security_list` | `quote.security_list` | [標的列表](../quote/security/security) | +| GET | `/v1/watchlist/groups` | `quote.watchlist` | [自選組列表](../quote/individual/watchlist_groups) | +| POST | `/v1/watchlist/groups` | `quote.create_watchlist_group` | [建立自選組](../quote/individual/watchlist_create_group) | +| PUT | `/v1/watchlist/groups` | `quote.update_watchlist_group` | [更新自選組](../quote/individual/watchlist_update_group) | +| DELETE | `/v1/watchlist/groups` | `quote.delete_watchlist_group` | [刪除自選組](../quote/individual/watchlist_delete_group) | + +## 說明 + +- 鑑權、簽名與請求基礎規範請看 [總覽](./how-to-access-api)。 +- 每個接口的參數與請求/回應範例,請點擊上方「詳細文件」。 +- 下一步可繼續補齊:從 SDK/協議自動生成更完整的參數快照。 From 235bf5bea3888d15af85fa3986a6a5f370f056fc Mon Sep 17 00:00:00 2001 From: xiaobo-gaga Date: Fri, 27 Feb 2026 18:32:26 +0800 Subject: [PATCH 2/6] Add OAuth2-based HTTP API validation script for docs verification --- package.json | 1 + script/README-api-validation.md | 65 ++++++++ script/api-validation-cases.example.json | 31 ++++ script/validate-http-apis.mjs | 199 +++++++++++++++++++++++ 4 files changed, 296 insertions(+) create mode 100644 script/README-api-validation.md create mode 100644 script/api-validation-cases.example.json create mode 100644 script/validate-http-apis.mjs diff --git a/package.json b/package.json index a8cde64c..191d9e89 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "build:canary": "VITE_API_BASE_URL=https://openapi.longbridge.xyz vitepress build docs && bun run build:llms", "build:release": "VITE_API_BASE_URL=https://openapi.longportapp.com vitepress build docs && bun run build:llms", "build:llms": "bun run script/normalize_md.ts && bun run script/generate-llms.ts", + "validate:apis": "node script/validate-http-apis.mjs --spec script/api-validation-cases.example.json", "preview": "vitepress preview docs", "lint:docs": "prettier -c --parser typescript \"packages/**/*.{js,ts,vue}\"", "format:docs": "prettier --write \"packages/**/*.{js,ts,vue}\"" diff --git a/script/README-api-validation.md b/script/README-api-validation.md new file mode 100644 index 00000000..09ec2dd8 --- /dev/null +++ b/script/README-api-validation.md @@ -0,0 +1,65 @@ +# API Validation Script + +## Purpose + +Validate documented HTTP APIs against real service behavior (`openapi.longbridge.xyz`) before merging documentation changes. + +## Run + +```bash +npm run validate:apis +``` + +Equivalent: + +```bash +node script/validate-http-apis.mjs --spec script/api-validation-cases.example.json +``` + +## Auth Modes + +### 1) Existing access token (recommended for CI) + +```bash +export OPENAPI_ACCESS_TOKEN="..." +npm run validate:apis +``` + +### 2) OAuth2 authorization code exchange + +```bash +export OPENAPI_CLIENT_ID="..." +export OPENAPI_CLIENT_SECRET="..." +export OPENAPI_REDIRECT_URI="..." +export OPENAPI_AUTH_CODE="..." +npm run validate:apis +``` + +### 3) OAuth2 refresh token exchange + +```bash +export OPENAPI_CLIENT_ID="..." +export OPENAPI_CLIENT_SECRET="..." +export OPENAPI_REFRESH_TOKEN="..." +npm run validate:apis +``` + +## Custom cases + +Create your own spec JSON and pass via `--spec`: + +```bash +node script/validate-http-apis.mjs --spec script/my-api-cases.json +``` + +Each case supports: + +- `method`, `path`, `query`, `headers`, `body` +- `expectedStatus` (number or array) +- `requiresAuth` +- `expectJsonCode` +- `expectBodyContains` + +## Suggested rule in PR + +For every API doc changed, add/adjust at least one validation case and include run output in PR description. diff --git a/script/api-validation-cases.example.json b/script/api-validation-cases.example.json new file mode 100644 index 00000000..5d974cd5 --- /dev/null +++ b/script/api-validation-cases.example.json @@ -0,0 +1,31 @@ +{ + "name": "OpenAPI HTTP Validation Baseline", + "baseUrl": "https://openapi.longbridge.xyz", + "oauthDiscoveryUrl": "https://openapi.longbridge.xyz/.well-known/oauth-authorization-server", + "defaultHeaders": { + "accept": "application/json" + }, + "cases": [ + { + "name": "Health: /v1/test should return success", + "method": "GET", + "path": "/v1/test", + "expectedStatus": 200, + "expectJsonCode": 0 + }, + { + "name": "OAuth discovery should be reachable", + "method": "GET", + "path": "/.well-known/oauth-authorization-server", + "expectedStatus": 200, + "expectBodyContains": ["token_endpoint", "authorization_endpoint"] + }, + { + "name": "Auth-required API without token should be unauthorized", + "method": "GET", + "path": "/v1/asset/account", + "requiresAuth": false, + "expectedStatus": [401, 403] + } + ] +} diff --git a/script/validate-http-apis.mjs b/script/validate-http-apis.mjs new file mode 100644 index 00000000..e1c00765 --- /dev/null +++ b/script/validate-http-apis.mjs @@ -0,0 +1,199 @@ +#!/usr/bin/env node + +import { readFile } from 'node:fs/promises' + +function getArg(flag) { + const i = process.argv.indexOf(flag) + if (i >= 0 && i + 1 < process.argv.length) return process.argv[i + 1] + return undefined +} + +const toArray = (v) => (Array.isArray(v) ? v : [v]) + +function buildUrl(baseUrl, path, query) { + const u = new URL(path, baseUrl) + if (query) { + for (const [k, v] of Object.entries(query)) u.searchParams.set(k, String(v)) + } + return u.toString() +} + +async function readSpec(path) { + const txt = await readFile(path, 'utf-8') + const spec = JSON.parse(txt) + if (!spec?.cases?.length) throw new Error(`No cases found in ${path}`) + return spec +} + +async function fetchOAuthDiscovery(url) { + const r = await fetch(url) + if (!r.ok) throw new Error(`Failed to load OAuth discovery: ${r.status} ${await r.text()}`) + return await r.json() +} + +async function exchangeTokenByAuthCode(tokenEndpoint) { + const clientId = process.env.OPENAPI_CLIENT_ID + const clientSecret = process.env.OPENAPI_CLIENT_SECRET + const redirectUri = process.env.OPENAPI_REDIRECT_URI + const code = process.env.OPENAPI_AUTH_CODE + if (!clientId || !clientSecret || !redirectUri || !code) { + throw new Error('Missing OAuth auth-code envs: OPENAPI_CLIENT_ID/OPENAPI_CLIENT_SECRET/OPENAPI_REDIRECT_URI/OPENAPI_AUTH_CODE') + } + + const form = new URLSearchParams() + form.set('grant_type', 'authorization_code') + form.set('client_id', clientId) + form.set('client_secret', clientSecret) + form.set('redirect_uri', redirectUri) + form.set('code', code) + + const r = await fetch(tokenEndpoint, { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: form.toString(), + }) + + const raw = await r.text() + if (!r.ok) throw new Error(`OAuth token exchange failed: ${r.status} ${raw}`) + const json = JSON.parse(raw) + if (!json.access_token) throw new Error(`OAuth token exchange response missing access_token: ${raw}`) + return json +} + +async function exchangeTokenByRefreshToken(tokenEndpoint) { + const clientId = process.env.OPENAPI_CLIENT_ID + const clientSecret = process.env.OPENAPI_CLIENT_SECRET + const refreshToken = process.env.OPENAPI_REFRESH_TOKEN + if (!clientId || !clientSecret || !refreshToken) { + throw new Error('Missing OAuth refresh envs: OPENAPI_CLIENT_ID/OPENAPI_CLIENT_SECRET/OPENAPI_REFRESH_TOKEN') + } + + const form = new URLSearchParams() + form.set('grant_type', 'refresh_token') + form.set('client_id', clientId) + form.set('client_secret', clientSecret) + form.set('refresh_token', refreshToken) + + const r = await fetch(tokenEndpoint, { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: form.toString(), + }) + + const raw = await r.text() + if (!r.ok) throw new Error(`OAuth refresh failed: ${r.status} ${raw}`) + const json = JSON.parse(raw) + if (!json.access_token) throw new Error(`OAuth refresh response missing access_token: ${raw}`) + return json +} + +async function resolveAccessToken(discoveryUrl) { + if (process.env.OPENAPI_ACCESS_TOKEN) return process.env.OPENAPI_ACCESS_TOKEN + + const oauth = await fetchOAuthDiscovery(discoveryUrl) + const tokenEndpoint = oauth.token_endpoint + + if (process.env.OPENAPI_AUTH_CODE) { + const t = await exchangeTokenByAuthCode(tokenEndpoint) + console.log('[oauth] access token acquired by authorization_code') + return t.access_token + } + + if (process.env.OPENAPI_REFRESH_TOKEN) { + const t = await exchangeTokenByRefreshToken(tokenEndpoint) + console.log('[oauth] access token acquired by refresh_token') + return t.access_token + } + + return undefined +} + +async function run() { + const specPath = getArg('--spec') || 'script/api-validation-cases.example.json' + const spec = await readSpec(specPath) + + const baseUrl = process.env.OPENAPI_BASE_URL || spec.baseUrl || 'https://openapi.longbridge.xyz' + const discoveryUrl = + process.env.OPENAPI_OAUTH_DISCOVERY || + spec.oauthDiscoveryUrl || + `${baseUrl.replace(/\/$/, '')}/.well-known/oauth-authorization-server` + + let accessToken + try { + accessToken = await resolveAccessToken(discoveryUrl) + } catch (e) { + console.warn(`[warn] OAuth token init failed: ${e.message}`) + } + + console.log(`\n[validate] spec: ${spec.name || specPath}`) + console.log(`[validate] baseUrl: ${baseUrl}`) + console.log(`[validate] cases: ${spec.cases.length}`) + + let passed = 0 + const failed = [] + + for (const c of spec.cases) { + const url = buildUrl(baseUrl, c.path, c.query) + const headers = { ...(spec.defaultHeaders || {}), ...(c.headers || {}) } + + if (c.requiresAuth) { + if (!accessToken) { + failed.push(`${c.name}: requiresAuth=true but no access token available`) + console.log(`✗ ${c.name} (missing token)`) + continue + } + headers.Authorization = accessToken + } + + const hasBody = c.body !== undefined && c.body !== null + if (hasBody && !headers['content-type']) headers['content-type'] = 'application/json; charset=utf-8' + + const resp = await fetch(url, { + method: c.method, + headers, + body: hasBody ? JSON.stringify(c.body) : undefined, + }) + + const raw = await resp.text() + const expectedStatuses = toArray(c.expectedStatus) + let ok = expectedStatuses.includes(resp.status) + + if (ok && c.expectJsonCode !== undefined) { + try { + const json = JSON.parse(raw) + if (json?.code !== c.expectJsonCode) ok = false + } catch { + ok = false + } + } + + if (ok && c.expectBodyContains?.length) { + for (const needle of c.expectBodyContains) { + if (!raw.includes(needle)) { + ok = false + break + } + } + } + + if (ok) { + passed += 1 + console.log(`✓ ${c.name} [${resp.status}] ${c.method} ${c.path}`) + } else { + const hint = raw.slice(0, 280).replace(/\s+/g, ' ') + failed.push(`${c.name}: expected status ${expectedStatuses.join('/')} got ${resp.status}; body=${hint}`) + console.log(`✗ ${c.name} [${resp.status}] ${c.method} ${c.path}`) + } + } + + console.log(`\n[summary] passed=${passed} failed=${failed.length} total=${spec.cases.length}`) + if (failed.length) { + for (const f of failed) console.log(` - ${f}`) + process.exit(1) + } +} + +run().catch((err) => { + console.error(`[fatal] ${err.stack || err.message}`) + process.exit(1) +}) From e7b9721a51ab4aef5357639080415fbe3baa061c Mon Sep 17 00:00:00 2001 From: xiaobo-gaga Date: Fri, 27 Feb 2026 18:35:49 +0800 Subject: [PATCH 3/6] Implement OpenAPI-driven API validation pipeline baseline --- openapi/openapi.baseline.json | 97 ++++++++++++++++++++++ package.json | 3 +- script/README-api-validation.md | 4 +- script/README-openapi-validation.md | 37 +++++++++ script/api-validation-cases.generated.json | 39 +++++++++ script/generate-cases-from-openapi.mjs | 56 +++++++++++++ 6 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 openapi/openapi.baseline.json create mode 100644 script/README-openapi-validation.md create mode 100644 script/api-validation-cases.generated.json create mode 100644 script/generate-cases-from-openapi.mjs diff --git a/openapi/openapi.baseline.json b/openapi/openapi.baseline.json new file mode 100644 index 00000000..92701a46 --- /dev/null +++ b/openapi/openapi.baseline.json @@ -0,0 +1,97 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Longbridge OpenAPI Validation Baseline", + "version": "0.1.0", + "description": "Baseline OpenAPI contract used for doc-validation automation. Extend this incrementally with every documented endpoint." + }, + "servers": [ + { + "url": "https://openapi.longbridge.xyz" + } + ], + "components": { + "securitySchemes": { + "OAuth2": { + "type": "oauth2", + "flows": { + "authorizationCode": { + "authorizationUrl": "https://openapi.longbridge.xyz/oauth2/authorize", + "tokenUrl": "https://openapi.longbridge.xyz/oauth2/token", + "scopes": { + "18": "Quote", + "19": "Trade", + "20": "Account", + "21": "Position", + "22": "Other" + } + } + } + } + }, + "schemas": { + "BaseResponse": { + "type": "object", + "properties": { + "code": { "type": "integer" }, + "message": { "type": "string" }, + "data": {} + }, + "required": ["code", "message"] + } + } + }, + "paths": { + "/v1/test": { + "get": { + "summary": "Service health check", + "operationId": "healthCheck", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/BaseResponse" } + } + } + } + }, + "x-validation": { + "expectedStatus": 200, + "expectJsonCode": 0 + } + } + }, + "/.well-known/oauth-authorization-server": { + "get": { + "summary": "OAuth 2 authorization server discovery", + "operationId": "oauthDiscovery", + "responses": { + "200": { + "description": "OAuth discovery document" + } + }, + "x-validation": { + "expectedStatus": 200, + "expectBodyContains": ["token_endpoint", "authorization_endpoint"] + } + } + }, + "/v1/asset/account": { + "get": { + "summary": "Account balance", + "operationId": "assetAccount", + "security": [{ "OAuth2": [] }], + "responses": { + "200": { "description": "Authorized response" }, + "401": { "description": "Unauthorized" }, + "403": { "description": "Forbidden" } + }, + "x-validation": { + "expectedStatus": [401, 403], + "requiresAuth": false + } + } + } + } +} diff --git a/package.json b/package.json index 191d9e89..19235c34 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "build:canary": "VITE_API_BASE_URL=https://openapi.longbridge.xyz vitepress build docs && bun run build:llms", "build:release": "VITE_API_BASE_URL=https://openapi.longportapp.com vitepress build docs && bun run build:llms", "build:llms": "bun run script/normalize_md.ts && bun run script/generate-llms.ts", - "validate:apis": "node script/validate-http-apis.mjs --spec script/api-validation-cases.example.json", + "generate:api-cases": "node script/generate-cases-from-openapi.mjs --openapi openapi/openapi.baseline.json --out script/api-validation-cases.generated.json", + "validate:apis": "node script/validate-http-apis.mjs --spec script/api-validation-cases.generated.json", "preview": "vitepress preview docs", "lint:docs": "prettier -c --parser typescript \"packages/**/*.{js,ts,vue}\"", "format:docs": "prettier --write \"packages/**/*.{js,ts,vue}\"" diff --git a/script/README-api-validation.md b/script/README-api-validation.md index 09ec2dd8..4ed91699 100644 --- a/script/README-api-validation.md +++ b/script/README-api-validation.md @@ -7,13 +7,15 @@ Validate documented HTTP APIs against real service behavior (`openapi.longbridge ## Run ```bash +npm run generate:api-cases npm run validate:apis ``` Equivalent: ```bash -node script/validate-http-apis.mjs --spec script/api-validation-cases.example.json +node script/generate-cases-from-openapi.mjs --openapi openapi/openapi.baseline.json --out script/api-validation-cases.generated.json +node script/validate-http-apis.mjs --spec script/api-validation-cases.generated.json ``` ## Auth Modes diff --git a/script/README-openapi-validation.md b/script/README-openapi-validation.md new file mode 100644 index 00000000..83704c39 --- /dev/null +++ b/script/README-openapi-validation.md @@ -0,0 +1,37 @@ +# OpenAPI-driven API Validation + +This project now supports an **OpenAPI-first validation flow** for HTTP API docs. + +## Files + +- OpenAPI baseline spec: `openapi/openapi.baseline.json` +- Case generator: `script/generate-cases-from-openapi.mjs` +- Runtime validator: `script/validate-http-apis.mjs` + +## Workflow + +1) Update OpenAPI spec with documented endpoints + +```bash +# edit openapi/openapi.baseline.json +``` + +2) Generate validation cases from OpenAPI + +```bash +npm run generate:api-cases +``` + +3) Run API validation against real environment (`openapi.longbridge.xyz` by default) + +```bash +npm run validate:apis +``` + +## Rule + +For every API doc you add/update: +- Add/adjust corresponding OpenAPI operation +- Keep `x-validation` expectations updated +- Run generation + validation +- Attach validation output in PR diff --git a/script/api-validation-cases.generated.json b/script/api-validation-cases.generated.json new file mode 100644 index 00000000..f1981b54 --- /dev/null +++ b/script/api-validation-cases.generated.json @@ -0,0 +1,39 @@ +{ + "name": "Longbridge OpenAPI Validation Baseline / generated validation cases", + "baseUrl": "https://openapi.longbridge.xyz", + "oauthDiscoveryUrl": "https://openapi.longbridge.xyz/.well-known/oauth-authorization-server", + "defaultHeaders": { + "accept": "application/json" + }, + "cases": [ + { + "name": "Service health check", + "method": "GET", + "path": "/v1/test", + "requiresAuth": false, + "expectedStatus": 200, + "expectJsonCode": 0 + }, + { + "name": "OAuth 2 authorization server discovery", + "method": "GET", + "path": "/.well-known/oauth-authorization-server", + "requiresAuth": false, + "expectedStatus": 200, + "expectBodyContains": [ + "token_endpoint", + "authorization_endpoint" + ] + }, + { + "name": "Account balance", + "method": "GET", + "path": "/v1/asset/account", + "requiresAuth": false, + "expectedStatus": [ + 401, + 403 + ] + } + ] +} diff --git a/script/generate-cases-from-openapi.mjs b/script/generate-cases-from-openapi.mjs new file mode 100644 index 00000000..a91e27e5 --- /dev/null +++ b/script/generate-cases-from-openapi.mjs @@ -0,0 +1,56 @@ +#!/usr/bin/env node + +import { readFile, writeFile } from 'node:fs/promises' + +function getArg(flag) { + const i = process.argv.indexOf(flag) + if (i >= 0 && i + 1 < process.argv.length) return process.argv[i + 1] + return undefined +} + +async function main() { + const openapiPath = getArg('--openapi') || 'openapi/openapi.baseline.json' + const outPath = getArg('--out') || 'script/api-validation-cases.generated.json' + + const raw = await readFile(openapiPath, 'utf-8') + const spec = JSON.parse(raw) + + const baseUrl = process.env.OPENAPI_BASE_URL || spec.servers?.[0]?.url + if (!baseUrl) throw new Error('No server url found in OpenAPI spec') + + const cases = [] + + for (const [path, methods] of Object.entries(spec.paths || {})) { + for (const [method, op] of Object.entries(methods || {})) { + const m = method.toUpperCase() + if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(m)) continue + + const xv = op['x-validation'] || {} + cases.push({ + name: op.summary || `${m} ${path}`, + method: m, + path, + requiresAuth: xv.requiresAuth || false, + expectedStatus: xv.expectedStatus || 200, + expectJsonCode: xv.expectJsonCode, + expectBodyContains: xv.expectBodyContains + }) + } + } + + const output = { + name: `${spec.info?.title || 'OpenAPI'} / generated validation cases`, + baseUrl, + oauthDiscoveryUrl: `${baseUrl.replace(/\/$/, '')}/.well-known/oauth-authorization-server`, + defaultHeaders: { accept: 'application/json' }, + cases + } + + await writeFile(outPath, JSON.stringify(output, null, 2) + '\n', 'utf-8') + console.log(`[ok] generated ${cases.length} cases -> ${outPath}`) +} + +main().catch((e) => { + console.error(`[fatal] ${e.stack || e.message}`) + process.exit(1) +}) From 4c860f8faeb3c73aed06731d6027ff0ca0ee339a Mon Sep 17 00:00:00 2001 From: xiaobo-gaga Date: Fri, 27 Feb 2026 18:42:43 +0800 Subject: [PATCH 4/6] Add CI workflow for OpenAPI-driven API validation --- .github/workflows/api-validation.yml | 53 ++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/api-validation.yml diff --git a/.github/workflows/api-validation.yml b/.github/workflows/api-validation.yml new file mode 100644 index 00000000..bfa0c8b2 --- /dev/null +++ b/.github/workflows/api-validation.yml @@ -0,0 +1,53 @@ +name: API Validation + +on: + pull_request: + paths: + - 'openapi/**' + - 'script/**' + - 'docs/**' + - '.github/workflows/api-validation.yml' + push: + branches: + - main + - canary + paths: + - 'openapi/**' + - 'script/**' + - 'docs/**' + - '.github/workflows/api-validation.yml' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate-apis: + name: Validate HTTP APIs (OpenAPI-driven) + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + OPENAPI_BASE_URL: https://openapi.longbridge.xyz + OPENAPI_OAUTH_DISCOVERY: https://openapi.longbridge.xyz/.well-known/oauth-authorization-server + OPENAPI_ACCESS_TOKEN: ${{ secrets.OPENAPI_ACCESS_TOKEN }} + OPENAPI_CLIENT_ID: ${{ secrets.OPENAPI_CLIENT_ID }} + OPENAPI_CLIENT_SECRET: ${{ secrets.OPENAPI_CLIENT_SECRET }} + OPENAPI_REFRESH_TOKEN: ${{ secrets.OPENAPI_REFRESH_TOKEN }} + OPENAPI_REDIRECT_URI: ${{ secrets.OPENAPI_REDIRECT_URI }} + OPENAPI_AUTH_CODE: ${{ secrets.OPENAPI_AUTH_CODE }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Generate API validation cases from OpenAPI + run: bun run generate:api-cases + + - name: Run API validation + run: bun run validate:apis From 68c9cf1f63158a387ea22053de081234c4359785 Mon Sep 17 00:00:00 2001 From: xiaobo-gaga Date: Fri, 27 Feb 2026 18:47:43 +0800 Subject: [PATCH 5/6] Split Try-It APIs per endpoint and wire into CI --- .github/workflows/api-validation.yml | 9 ++ package.json | 1 + script/README-tryit-split.md | 34 +++++++ script/split-tryit-apis.ts | 135 +++++++++++++++++++++++++++ 4 files changed, 179 insertions(+) create mode 100644 script/README-tryit-split.md create mode 100644 script/split-tryit-apis.ts diff --git a/.github/workflows/api-validation.yml b/.github/workflows/api-validation.yml index bfa0c8b2..7f6aaced 100644 --- a/.github/workflows/api-validation.yml +++ b/.github/workflows/api-validation.yml @@ -46,8 +46,17 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile + - name: Split Try-It APIs from docs + run: bun run split:tryit-apis + - name: Generate API validation cases from OpenAPI run: bun run generate:api-cases - name: Run API validation run: bun run validate:apis + + - name: Upload Try-It API split artifacts + uses: actions/upload-artifact@v4 + with: + name: tryit-api-split + path: openapi/tryit/*.json diff --git a/package.json b/package.json index 19235c34..94b8f665 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "build:canary": "VITE_API_BASE_URL=https://openapi.longbridge.xyz vitepress build docs && bun run build:llms", "build:release": "VITE_API_BASE_URL=https://openapi.longportapp.com vitepress build docs && bun run build:llms", "build:llms": "bun run script/normalize_md.ts && bun run script/generate-llms.ts", + "split:tryit-apis": "bun run script/split-tryit-apis.ts", "generate:api-cases": "node script/generate-cases-from-openapi.mjs --openapi openapi/openapi.baseline.json --out script/api-validation-cases.generated.json", "validate:apis": "node script/validate-http-apis.mjs --spec script/api-validation-cases.generated.json", "preview": "vitepress preview docs", diff --git a/script/README-tryit-split.md b/script/README-tryit-split.md new file mode 100644 index 00000000..581bec70 --- /dev/null +++ b/script/README-tryit-split.md @@ -0,0 +1,34 @@ +# Try-It API Split + +Based on the existing `?mode=try-it` mechanism, this script splits documented HTTP APIs into per-locale JSON specs. + +## Run + +```bash +bun run split:tryit-apis +``` + +## Output + +Generated files: + +- `openapi/tryit/apis.en.json` +- `openapi/tryit/apis.zh-CN.json` +- `openapi/tryit/apis.zh-HK.json` +- `openapi/tryit/apis.all.json` + +Each record includes: + +- `title` +- `method` +- `path` +- `source` (doc file path) +- `params` (parsed from Parameters table) + +## Purpose + +- Make each API an explicit unit from existing Try-It docs +- Provide machine-readable inputs for: + - API validation pipeline + - future per-API Try-It rendering refactor + - parity checks across locales diff --git a/script/split-tryit-apis.ts b/script/split-tryit-apis.ts new file mode 100644 index 00000000..f0c0d213 --- /dev/null +++ b/script/split-tryit-apis.ts @@ -0,0 +1,135 @@ +#!/usr/bin/env bun + +import { readdirSync, readFileSync, statSync, mkdirSync, writeFileSync } from 'node:fs' +import { join, relative } from 'node:path' + +type Locale = 'en' | 'zh-CN' | 'zh-HK' + +interface TryItParam { + name: string + type: string + required: boolean + description: string +} + +interface TryItApiSpec { + id: string + locale: Locale + title: string + method: string + path: string + source: string + params: TryItParam[] +} + +const ROOT = process.cwd() +const DOCS_ROOT = join(ROOT, 'docs') +const OUT_DIR = join(ROOT, 'openapi', 'tryit') +const LOCALES: Locale[] = ['en', 'zh-CN', 'zh-HK'] + +function walk(dir: string): string[] { + const out: string[] = [] + for (const name of readdirSync(dir)) { + const full = join(dir, name) + const st = statSync(full) + if (st.isDirectory()) out.push(...walk(full)) + else if (st.isFile() && name.endsWith('.md')) out.push(full) + } + return out +} + +function parseTitle(md: string): string { + const fm = md.match(/^---[\s\S]*?^---/m)?.[0] || '' + const t = fm.match(/^title:\s*(.+)$/m)?.[1]?.trim() + if (t) return t.replace(/^['"]|['"]$/g, '') + const h1 = md.match(/^#\s+(.+)$/m)?.[1]?.trim() + return h1 || 'Untitled API' +} + +function parseMethodAndPath(md: string): { method: string; path: string } | null { + const method = md.match(/HTTP Method<\/td>\s*([^<\n]+)/i)?.[1]?.trim() + const path = md.match(/HTTP URL<\/td>\s*([^<\n]+)/i)?.[1]?.trim() + if (!method || !path) return null + return { method, path } +} + +function parseParameters(md: string): TryItParam[] { + const lines = md.split('\n') + const out: TryItParam[] = [] + + let i = lines.findIndex((l) => /^\|\s*Name\s*\|\s*Type\s*\|\s*Required\s*\|\s*Description\s*\|/i.test(l)) + if (i < 0) return out + + // skip header + separator + i += 2 + for (; i < lines.length; i++) { + const l = lines[i] + if (!l.trim().startsWith('|')) break + const cols = l + .split('|') + .slice(1, -1) + .map((x) => x.trim()) + if (cols.length < 4) continue + const [name, type, requiredRaw, description] = cols + if (!name || /^-+$/.test(name)) continue + out.push({ + name, + type, + required: ['YES', 'Y', 'TRUE', '是'].includes(requiredRaw.toUpperCase()), + description, + }) + } + + return out +} + +function normalizeId(path: string, method: string, locale: Locale): string { + const p = path.replace(/^\//, '').replace(/\//g, '_').replace(/[^a-zA-Z0-9_]/g, '') + return `${locale}__${method.toLowerCase()}__${p}` +} + +function main() { + mkdirSync(OUT_DIR, { recursive: true }) + + const all: TryItApiSpec[] = [] + + for (const locale of LOCALES) { + const dir = join(DOCS_ROOT, locale, 'docs') + const files = walk(dir) + + const specs: TryItApiSpec[] = [] + for (const f of files) { + const md = readFileSync(f, 'utf-8') + const basic = parseMethodAndPath(md) + if (!basic) continue + + const title = parseTitle(md) + const params = parseParameters(md) + const source = relative(ROOT, f) + const id = normalizeId(basic.path, basic.method, locale) + + specs.push({ + id, + locale, + title, + method: basic.method, + path: basic.path, + source, + params, + }) + } + + specs.sort((a, b) => `${a.method} ${a.path}`.localeCompare(`${b.method} ${b.path}`)) + all.push(...specs) + + const localeOut = join(OUT_DIR, `apis.${locale}.json`) + writeFileSync(localeOut, JSON.stringify(specs, null, 2) + '\n') + console.log(`[split-tryit] ${locale}: ${specs.length} apis -> ${relative(ROOT, localeOut)}`) + } + + const allOut = join(OUT_DIR, 'apis.all.json') + writeFileSync(allOut, JSON.stringify(all, null, 2) + '\n') + console.log(`[split-tryit] all: ${all.length} apis -> ${relative(ROOT, allOut)}`) +} + +main() From 2fd77aaff99ac0498e2c9e9c5365584276389493 Mon Sep 17 00:00:00 2001 From: xiaobo-gaga Date: Fri, 27 Feb 2026 19:00:09 +0800 Subject: [PATCH 6/6] Add Mintlify-style API endpoint catalog pages and generator --- .github/workflows/api-validation.yml | 3 + docs/en/docs/api-reference/_category_.json | 2 +- docs/en/docs/api-reference/endpoints.md | 55 +++++++++++ docs/zh-CN/docs/api-reference/_category_.json | 2 +- docs/zh-CN/docs/api-reference/endpoints.md | 55 +++++++++++ docs/zh-HK/docs/api-reference/_category_.json | 2 +- docs/zh-HK/docs/api-reference/endpoints.md | 55 +++++++++++ package.json | 1 + script/generate-api-endpoints-catalog.ts | 97 +++++++++++++++++++ 9 files changed, 269 insertions(+), 3 deletions(-) create mode 100644 docs/en/docs/api-reference/endpoints.md create mode 100644 docs/zh-CN/docs/api-reference/endpoints.md create mode 100644 docs/zh-HK/docs/api-reference/endpoints.md create mode 100644 script/generate-api-endpoints-catalog.ts diff --git a/.github/workflows/api-validation.yml b/.github/workflows/api-validation.yml index 7f6aaced..f3099c55 100644 --- a/.github/workflows/api-validation.yml +++ b/.github/workflows/api-validation.yml @@ -46,6 +46,9 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile + - name: Generate API catalog page + run: bun run generate:api-catalog + - name: Split Try-It APIs from docs run: bun run split:tryit-apis diff --git a/docs/en/docs/api-reference/_category_.json b/docs/en/docs/api-reference/_category_.json index 6b30ef8a..5b4931d5 100644 --- a/docs/en/docs/api-reference/_category_.json +++ b/docs/en/docs/api-reference/_category_.json @@ -2,6 +2,6 @@ "position": 4.5, "label": "API Reference", "collapsible": true, - "collapsed": true, + "collapsed": false, "link": null } diff --git a/docs/en/docs/api-reference/endpoints.md b/docs/en/docs/api-reference/endpoints.md new file mode 100644 index 00000000..bf3fc304 --- /dev/null +++ b/docs/en/docs/api-reference/endpoints.md @@ -0,0 +1,55 @@ +--- +title: API Endpoints +id: api-endpoints-catalog +slug: /api-endpoints +sidebar_position: 1 +--- + +All HTTP APIs, listed one-by-one like an API reference catalog. + +## ASSET + +| Method | Path | API | Try-It | +| --- | --- | --- | --- | +| `GET` | `/v1/asset/account` | [Get Account Balance](../trade/asset/account) | [Try](../trade/asset/account?mode=try-it) | +| `GET` | `/v1/asset/cashflow` | [Get Cash Flow](../trade/asset/cashflow) | [Try](../trade/asset/cashflow?mode=try-it) | +| `GET` | `/v1/asset/fund` | [Get Fund Positions](../trade/asset/fund) | [Try](../trade/asset/fund?mode=try-it) | +| `GET` | `/v1/asset/stock` | [Get Stock Positions](../trade/asset/stock) | [Try](../trade/asset/stock?mode=try-it) | + +## QUOTE + +| Method | Path | API | Try-It | +| --- | --- | --- | --- | +| `GET` | `/v1/quote/get_security_list` | [Retrieve the List of Securities](../quote/security/security) | [Try](../quote/security/security?mode=try-it) | +| `GET` | `/v1/quote/history_market_temperature` | [Historical Market Temperature](../quote/pull/history-market-temp) | [Try](../quote/pull/history-market-temp?mode=try-it) | +| `GET` | `/v1/quote/market_temperature` | [Current Market Temperature](../quote/pull/market-temp) | [Try](../quote/pull/market-temp?mode=try-it) | + +## RISK + +| Method | Path | API | Try-It | +| --- | --- | --- | --- | +| `GET` | `/v1/risk/margin-ratio` | [Get Margin Ratio](../trade/asset/margin_ratio) | [Try](../trade/asset/margin_ratio?mode=try-it) | + +## TRADE + +| Method | Path | API | Try-It | +| --- | --- | --- | --- | +| `GET` | `/v1/trade/estimate/buy_limit` | [Estimate Maximum Purchase Quantity](../trade/order/estimate_available_buy_limit) | [Try](../trade/order/estimate_available_buy_limit?mode=try-it) | +| `GET` | `/v1/trade/execution/history` | [Get History Executions](../trade/execution/history_executions) | [Try](../trade/execution/history_executions?mode=try-it) | +| `GET` | `/v1/trade/execution/today` | [Get Today Executions](../trade/execution/today_executions) | [Try](../trade/execution/today_executions?mode=try-it) | +| `DELETE` | `/v1/trade/order` | [Withdraw Order](../trade/order/withdraw) | [Try](../trade/order/withdraw?mode=try-it) | +| `GET` | `/v1/trade/order` | [Order Details](../trade/order/order_detail) | [Try](../trade/order/order_detail?mode=try-it) | +| `POST` | `/v1/trade/order` | [Submit Order](../trade/order/submit) | [Try](../trade/order/submit?mode=try-it) | +| `PUT` | `/v1/trade/order` | [Replace Order](../trade/order/replace) | [Try](../trade/order/replace?mode=try-it) | +| `GET` | `/v1/trade/order/history` | [Get History Order](../trade/order/history_orders) | [Try](../trade/order/history_orders?mode=try-it) | +| `GET` | `/v1/trade/order/today` | [Get Today Order](../trade/order/today_orders) | [Try](../trade/order/today_orders?mode=try-it) | + +## WATCHLIST + +| Method | Path | API | Try-It | +| --- | --- | --- | --- | +| `DELETE` | `/v1/watchlist/groups` | [Delete Watchlist Group](../quote/individual/watchlist_delete_group) | [Try](../quote/individual/watchlist_delete_group?mode=try-it) | +| `GET` | `/v1/watchlist/groups` | [Watchlist Group](../quote/individual/watchlist_groups) | [Try](../quote/individual/watchlist_groups?mode=try-it) | +| `POST` | `/v1/watchlist/groups` | [Create Watchlist Group](../quote/individual/watchlist_create_group) | [Try](../quote/individual/watchlist_create_group?mode=try-it) | +| `PUT` | `/v1/watchlist/groups` | [Update Watchlist Group](../quote/individual/watchlist_update_group) | [Try](../quote/individual/watchlist_update_group?mode=try-it) | + diff --git a/docs/zh-CN/docs/api-reference/_category_.json b/docs/zh-CN/docs/api-reference/_category_.json index 85799dc6..c6dc7e66 100644 --- a/docs/zh-CN/docs/api-reference/_category_.json +++ b/docs/zh-CN/docs/api-reference/_category_.json @@ -2,6 +2,6 @@ "position": 4.5, "label": "API 附录", "collapsible": true, - "collapsed": true, + "collapsed": false, "link": null } diff --git a/docs/zh-CN/docs/api-reference/endpoints.md b/docs/zh-CN/docs/api-reference/endpoints.md new file mode 100644 index 00000000..a976a4ae --- /dev/null +++ b/docs/zh-CN/docs/api-reference/endpoints.md @@ -0,0 +1,55 @@ +--- +title: API 接口目录 +id: api-endpoints-catalog +slug: /api-endpoints +sidebar_position: 1 +--- + +按接口逐条列出所有 HTTP API,便于快速检索与跳转。 + +## ASSET + +| Method | Path | API | Try-It | +| --- | --- | --- | --- | +| `GET` | `/v1/asset/account` | [获取账户资金](../trade/asset/account) | [Try](../trade/asset/account?mode=try-it) | +| `GET` | `/v1/asset/cashflow` | [获取资金流水](../trade/asset/cashflow) | [Try](../trade/asset/cashflow?mode=try-it) | +| `GET` | `/v1/asset/fund` | [获取基金持仓](../trade/asset/fund) | [Try](../trade/asset/fund?mode=try-it) | +| `GET` | `/v1/asset/stock` | [获取股票持仓](../trade/asset/stock) | [Try](../trade/asset/stock?mode=try-it) | + +## QUOTE + +| Method | Path | API | Try-It | +| --- | --- | --- | --- | +| `GET` | `/v1/quote/get_security_list` | [获取标的列表](../quote/security/security) | [Try](../quote/security/security?mode=try-it) | +| `GET` | `/v1/quote/history_market_temperature` | [历史市场温度](../quote/pull/history-market-temp) | [Try](../quote/pull/history-market-temp?mode=try-it) | +| `GET` | `/v1/quote/market_temperature` | [当前市场温度](../quote/pull/market-temp) | [Try](../quote/pull/market-temp?mode=try-it) | + +## RISK + +| Method | Path | API | Try-It | +| --- | --- | --- | --- | +| `GET` | `/v1/risk/margin-ratio` | [获取保证金比例](../trade/asset/margin_ratio) | [Try](../trade/asset/margin_ratio?mode=try-it) | + +## TRADE + +| Method | Path | API | Try-It | +| --- | --- | --- | --- | +| `GET` | `/v1/trade/estimate/buy_limit` | [预估最大购买数量](../trade/order/estimate_available_buy_limit) | [Try](../trade/order/estimate_available_buy_limit?mode=try-it) | +| `GET` | `/v1/trade/execution/history` | [获取历史成交明细](../trade/execution/history_executions) | [Try](../trade/execution/history_executions?mode=try-it) | +| `GET` | `/v1/trade/execution/today` | [获取当日成交明细](../trade/execution/today_executions) | [Try](../trade/execution/today_executions?mode=try-it) | +| `DELETE` | `/v1/trade/order` | [撤销订单](../trade/order/withdraw) | [Try](../trade/order/withdraw?mode=try-it) | +| `GET` | `/v1/trade/order` | [订单详情](../trade/order/order_detail) | [Try](../trade/order/order_detail?mode=try-it) | +| `POST` | `/v1/trade/order` | [委托下单](../trade/order/submit) | [Try](../trade/order/submit?mode=try-it) | +| `PUT` | `/v1/trade/order` | [修改订单](../trade/order/replace) | [Try](../trade/order/replace?mode=try-it) | +| `GET` | `/v1/trade/order/history` | [获取历史订单](../trade/order/history_orders) | [Try](../trade/order/history_orders?mode=try-it) | +| `GET` | `/v1/trade/order/today` | [获取当日订单](../trade/order/today_orders) | [Try](../trade/order/today_orders?mode=try-it) | + +## WATCHLIST + +| Method | Path | API | Try-It | +| --- | --- | --- | --- | +| `DELETE` | `/v1/watchlist/groups` | [删除自选股分组](../quote/individual/watchlist_delete_group) | [Try](../quote/individual/watchlist_delete_group?mode=try-it) | +| `GET` | `/v1/watchlist/groups` | [获取自选股分组](../quote/individual/watchlist_groups) | [Try](../quote/individual/watchlist_groups?mode=try-it) | +| `POST` | `/v1/watchlist/groups` | [创建自选股分组](../quote/individual/watchlist_create_group) | [Try](../quote/individual/watchlist_create_group?mode=try-it) | +| `PUT` | `/v1/watchlist/groups` | [更新自选股分组](../quote/individual/watchlist_update_group) | [Try](../quote/individual/watchlist_update_group?mode=try-it) | + diff --git a/docs/zh-HK/docs/api-reference/_category_.json b/docs/zh-HK/docs/api-reference/_category_.json index 6b30ef8a..5b4931d5 100644 --- a/docs/zh-HK/docs/api-reference/_category_.json +++ b/docs/zh-HK/docs/api-reference/_category_.json @@ -2,6 +2,6 @@ "position": 4.5, "label": "API Reference", "collapsible": true, - "collapsed": true, + "collapsed": false, "link": null } diff --git a/docs/zh-HK/docs/api-reference/endpoints.md b/docs/zh-HK/docs/api-reference/endpoints.md new file mode 100644 index 00000000..62acbbd9 --- /dev/null +++ b/docs/zh-HK/docs/api-reference/endpoints.md @@ -0,0 +1,55 @@ +--- +title: API 介面目錄 +id: api-endpoints-catalog +slug: /api-endpoints +sidebar_position: 1 +--- + +按介面逐條列出所有 HTTP API,方便快速檢索與跳轉。 + +## ASSET + +| Method | Path | API | Try-It | +| --- | --- | --- | --- | +| `GET` | `/v1/asset/account` | [獲取賬戶資金](../trade/asset/account) | [Try](../trade/asset/account?mode=try-it) | +| `GET` | `/v1/asset/cashflow` | [獲取資金流水](../trade/asset/cashflow) | [Try](../trade/asset/cashflow?mode=try-it) | +| `GET` | `/v1/asset/fund` | [獲取基金持倉](../trade/asset/fund) | [Try](../trade/asset/fund?mode=try-it) | +| `GET` | `/v1/asset/stock` | [獲取股票持倉](../trade/asset/stock) | [Try](../trade/asset/stock?mode=try-it) | + +## QUOTE + +| Method | Path | API | Try-It | +| --- | --- | --- | --- | +| `GET` | `/v1/quote/get_security_list` | [獲取標的列表](../quote/security/security) | [Try](../quote/security/security?mode=try-it) | +| `GET` | `/v1/quote/history_market_temperature` | [歷史市場溫度](../quote/pull/history-market-temp) | [Try](../quote/pull/history-market-temp?mode=try-it) | +| `GET` | `/v1/quote/market_temperature` | [當前市場溫度](../quote/pull/market-temp) | [Try](../quote/pull/market-temp?mode=try-it) | + +## RISK + +| Method | Path | API | Try-It | +| --- | --- | --- | --- | +| `GET` | `/v1/risk/margin-ratio` | [獲取保證金比例](../trade/asset/margin_ratio) | [Try](../trade/asset/margin_ratio?mode=try-it) | + +## TRADE + +| Method | Path | API | Try-It | +| --- | --- | --- | --- | +| `GET` | `/v1/trade/estimate/buy_limit` | [預估最大購買數量](../trade/order/estimate_available_buy_limit) | [Try](../trade/order/estimate_available_buy_limit?mode=try-it) | +| `GET` | `/v1/trade/execution/history` | [獲取歷史成交明細](../trade/execution/history_executions) | [Try](../trade/execution/history_executions?mode=try-it) | +| `GET` | `/v1/trade/execution/today` | [獲取當日成交明細](../trade/execution/today_executions) | [Try](../trade/execution/today_executions?mode=try-it) | +| `DELETE` | `/v1/trade/order` | [撤銷訂單](../trade/order/withdraw) | [Try](../trade/order/withdraw?mode=try-it) | +| `GET` | `/v1/trade/order` | [訂單詳情](../trade/order/order_detail) | [Try](../trade/order/order_detail?mode=try-it) | +| `POST` | `/v1/trade/order` | [委托下單](../trade/order/submit) | [Try](../trade/order/submit?mode=try-it) | +| `PUT` | `/v1/trade/order` | [修改訂單](../trade/order/replace) | [Try](../trade/order/replace?mode=try-it) | +| `GET` | `/v1/trade/order/history` | [獲取歷史訂單](../trade/order/history_orders) | [Try](../trade/order/history_orders?mode=try-it) | +| `GET` | `/v1/trade/order/today` | [獲取當日訂單](../trade/order/today_orders) | [Try](../trade/order/today_orders?mode=try-it) | + +## WATCHLIST + +| Method | Path | API | Try-It | +| --- | --- | --- | --- | +| `DELETE` | `/v1/watchlist/groups` | [刪除自選股分組](../quote/individual/watchlist_delete_group) | [Try](../quote/individual/watchlist_delete_group?mode=try-it) | +| `GET` | `/v1/watchlist/groups` | [獲取關注分組](../quote/individual/watchlist_groups) | [Try](../quote/individual/watchlist_groups?mode=try-it) | +| `POST` | `/v1/watchlist/groups` | [創建自選股分組](../quote/individual/watchlist_create_group) | [Try](../quote/individual/watchlist_create_group?mode=try-it) | +| `PUT` | `/v1/watchlist/groups` | [更新自選股分組](../quote/individual/watchlist_update_group) | [Try](../quote/individual/watchlist_update_group?mode=try-it) | + diff --git a/package.json b/package.json index 94b8f665..adf4d959 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "build:canary": "VITE_API_BASE_URL=https://openapi.longbridge.xyz vitepress build docs && bun run build:llms", "build:release": "VITE_API_BASE_URL=https://openapi.longportapp.com vitepress build docs && bun run build:llms", "build:llms": "bun run script/normalize_md.ts && bun run script/generate-llms.ts", + "generate:api-catalog": "bun run script/generate-api-endpoints-catalog.ts", "split:tryit-apis": "bun run script/split-tryit-apis.ts", "generate:api-cases": "node script/generate-cases-from-openapi.mjs --openapi openapi/openapi.baseline.json --out script/api-validation-cases.generated.json", "validate:apis": "node script/validate-http-apis.mjs --spec script/api-validation-cases.generated.json", diff --git a/script/generate-api-endpoints-catalog.ts b/script/generate-api-endpoints-catalog.ts new file mode 100644 index 00000000..0bd0ff99 --- /dev/null +++ b/script/generate-api-endpoints-catalog.ts @@ -0,0 +1,97 @@ +#!/usr/bin/env bun + +import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs' +import { join, relative } from 'node:path' + +type Locale = 'en' | 'zh-CN' | 'zh-HK' + +const ROOT = process.cwd() +const DOCS_ROOT = join(ROOT, 'docs') +const LOCALES: Locale[] = ['en', 'zh-CN', 'zh-HK'] + +const META: Record = { + en: { + title: 'API Endpoints', + desc: 'All HTTP APIs, listed one-by-one like an API reference catalog.', + }, + 'zh-CN': { + title: 'API 接口目录', + desc: '按接口逐条列出所有 HTTP API,便于快速检索与跳转。', + }, + 'zh-HK': { + title: 'API 介面目錄', + desc: '按介面逐條列出所有 HTTP API,方便快速檢索與跳轉。', + }, +} + +function walk(dir: string): string[] { + const out: string[] = [] + for (const n of readdirSync(dir)) { + const full = join(dir, n) + const st = statSync(full) + if (st.isDirectory()) out.push(...walk(full)) + else if (st.isFile() && n.endsWith('.md')) out.push(full) + } + return out +} + +function extractApi(md: string): { method: string; path: string } | null { + const method = md.match(/HTTP Method<\/td>\s*([^<\n]+)/i)?.[1]?.trim().toUpperCase() + const path = md.match(/HTTP URL<\/td>\s*([^<\n]+)/i)?.[1]?.trim() + if (!method || !path) return null + return { method, path } +} + +for (const locale of LOCALES) { + const docsDir = join(DOCS_ROOT, locale, 'docs') + const files = walk(docsDir) + const rows: { method: string; path: string; title: string; rel: string }[] = [] + + for (const file of files) { + const md = readFileSync(file, 'utf-8') + const api = extractApi(md) + if (!api) continue + + const title = md.match(/^title:\s*(.+)$/m)?.[1]?.trim().replace(/^['"]|['"]$/g, '') || 'Untitled' + const rel = relative(docsDir, file).replaceAll('\\', '/').replace(/\.md$/, '') + rows.push({ ...api, title, rel }) + } + + rows.sort((a, b) => `${a.path}:${a.method}`.localeCompare(`${b.path}:${b.method}`)) + + const grouped = new Map() + for (const row of rows) { + const parts = row.path.split('/') + const group = parts.length > 2 ? parts[2] : 'other' + if (!grouped.has(group)) grouped.set(group, []) + grouped.get(group)!.push(row) + } + + const out: string[] = [ + '---', + `title: ${META[locale].title}`, + 'id: api-endpoints-catalog', + 'slug: /api-endpoints', + 'sidebar_position: 1', + '---', + '', + META[locale].desc, + '', + ] + + for (const [group, items] of grouped) { + out.push(`## ${group.toUpperCase()}`) + out.push('') + out.push('| Method | Path | API | Try-It |') + out.push('| --- | --- | --- | --- |') + for (const row of items) { + const apiLink = `../${row.rel}` + out.push(`| \`${row.method}\` | \`${row.path}\` | [${row.title}](${apiLink}) | [Try](${apiLink}?mode=try-it) |`) + } + out.push('') + } + + const outFile = join(docsDir, 'api-reference', 'endpoints.md') + writeFileSync(outFile, out.join('\n') + '\n', 'utf-8') + console.log(`[catalog] ${locale}: ${rows.length} APIs -> ${relative(ROOT, outFile)}`) +}