@investoday/investoday-api 1.8.26 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/data/openapi.json +120 -5
- package/data/tree.json +1 -1
- package/lib/call-api.js +48 -6
- package/lib/config.js +65 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,6 +33,27 @@ Get an API key from:
|
|
|
33
33
|
|
|
34
34
|
- https://data-api.investoday.net/user/api-key
|
|
35
35
|
|
|
36
|
+
## API base URL
|
|
37
|
+
|
|
38
|
+
The default API base URL is:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
https://data-api.investoday.net/data
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Temporarily override it with an environment variable:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
INVESTODAY_API_BASE_URL="https://data-api.investoday.net/data" investoday-api list
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Persist a custom value in the local config:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
investoday-api config set-base-url "https://data-api.investoday.net/data"
|
|
54
|
+
investoday-api config reset-base-url
|
|
55
|
+
```
|
|
56
|
+
|
|
36
57
|
## Usage
|
|
37
58
|
|
|
38
59
|
```bash
|
|
@@ -41,6 +62,8 @@ investoday-api init
|
|
|
41
62
|
investoday-api config status
|
|
42
63
|
investoday-api config path
|
|
43
64
|
investoday-api config remove
|
|
65
|
+
investoday-api config set-base-url "https://data-api.investoday.net/data"
|
|
66
|
+
investoday-api config reset-base-url
|
|
44
67
|
investoday-api update status
|
|
45
68
|
investoday-api update run
|
|
46
69
|
investoday-api update enable
|
package/data/openapi.json
CHANGED
|
@@ -2029,7 +2029,7 @@
|
|
|
2029
2029
|
"行业及关联成分股的最新实时日行情"
|
|
2030
2030
|
],
|
|
2031
2031
|
"summary": "行业及关联成分股的最新实时日行情",
|
|
2032
|
-
"description": "
|
|
2032
|
+
"description": "通过行业代码(必填)查询对应行业及相关成分股的最新实时日行情数据,支持按响应字段指定排序规则、分页查询,返回行业整体行情信息(含价格、涨跌幅、成交量、近一周涨跌幅、总市值、行业内涨跌排名、涨跌家数统计、领涨股信息等)、成分股实时行情详情(含股票代码、名称、最新价、涨跌幅、开盘价、最高价、最低价等)及分页相关数据,可用于行业行情监测、成分股分析、市场热点追踪等场景。",
|
|
2033
2033
|
"operationId": "get_industry_stock_realtime_quote",
|
|
2034
2034
|
"parameters": [
|
|
2035
2035
|
{
|
|
@@ -2039,7 +2039,59 @@
|
|
|
2039
2039
|
"required": true,
|
|
2040
2040
|
"schema": {
|
|
2041
2041
|
"type": "string",
|
|
2042
|
-
"example": "
|
|
2042
|
+
"example": "740000"
|
|
2043
|
+
}
|
|
2044
|
+
},
|
|
2045
|
+
{
|
|
2046
|
+
"name": "sortColumn",
|
|
2047
|
+
"in": "query",
|
|
2048
|
+
"description": "排序字段(响应字段)",
|
|
2049
|
+
"required": false,
|
|
2050
|
+
"schema": {
|
|
2051
|
+
"type": "string",
|
|
2052
|
+
"example": "changeRatio"
|
|
2053
|
+
}
|
|
2054
|
+
},
|
|
2055
|
+
{
|
|
2056
|
+
"name": "order",
|
|
2057
|
+
"in": "query",
|
|
2058
|
+
"description": "排序方向",
|
|
2059
|
+
"required": false,
|
|
2060
|
+
"schema": {
|
|
2061
|
+
"type": "string",
|
|
2062
|
+
"allOf": [
|
|
2063
|
+
{
|
|
2064
|
+
"type": "string",
|
|
2065
|
+
"description": "asc代表正序;\ndesc 代表倒序",
|
|
2066
|
+
"enum": [
|
|
2067
|
+
"asc",
|
|
2068
|
+
"desc"
|
|
2069
|
+
]
|
|
2070
|
+
}
|
|
2071
|
+
],
|
|
2072
|
+
"example": "desc"
|
|
2073
|
+
}
|
|
2074
|
+
},
|
|
2075
|
+
{
|
|
2076
|
+
"name": "page",
|
|
2077
|
+
"in": "query",
|
|
2078
|
+
"description": "页码",
|
|
2079
|
+
"required": false,
|
|
2080
|
+
"schema": {
|
|
2081
|
+
"type": "integer",
|
|
2082
|
+
"format": "int32",
|
|
2083
|
+
"example": 1
|
|
2084
|
+
}
|
|
2085
|
+
},
|
|
2086
|
+
{
|
|
2087
|
+
"name": "pageSize",
|
|
2088
|
+
"in": "query",
|
|
2089
|
+
"description": "页长",
|
|
2090
|
+
"required": false,
|
|
2091
|
+
"schema": {
|
|
2092
|
+
"type": "integer",
|
|
2093
|
+
"format": "int32",
|
|
2094
|
+
"example": 10
|
|
2043
2095
|
}
|
|
2044
2096
|
}
|
|
2045
2097
|
],
|
|
@@ -2072,9 +2124,10 @@
|
|
|
2072
2124
|
"example": -0.0121602
|
|
2073
2125
|
},
|
|
2074
2126
|
"volume": {
|
|
2075
|
-
"type": "
|
|
2127
|
+
"type": "integer",
|
|
2128
|
+
"format": "int32",
|
|
2076
2129
|
"description": "行业成交量",
|
|
2077
|
-
"example":
|
|
2130
|
+
"example": 9547379
|
|
2078
2131
|
},
|
|
2079
2132
|
"changeRatio1W": {
|
|
2080
2133
|
"type": "number",
|
|
@@ -2381,10 +2434,20 @@
|
|
|
2381
2434
|
{
|
|
2382
2435
|
"name": "conceptType",
|
|
2383
2436
|
"in": "query",
|
|
2384
|
-
"description": "概念类型
|
|
2437
|
+
"description": "概念类型",
|
|
2385
2438
|
"required": true,
|
|
2386
2439
|
"schema": {
|
|
2387
2440
|
"type": "string",
|
|
2441
|
+
"allOf": [
|
|
2442
|
+
{
|
|
2443
|
+
"type": "string",
|
|
2444
|
+
"description": "jy代表聚源;\ncls代表财联社",
|
|
2445
|
+
"enum": [
|
|
2446
|
+
"jy",
|
|
2447
|
+
"cls"
|
|
2448
|
+
]
|
|
2449
|
+
}
|
|
2450
|
+
],
|
|
2388
2451
|
"example": "jy"
|
|
2389
2452
|
}
|
|
2390
2453
|
},
|
|
@@ -2397,6 +2460,58 @@
|
|
|
2397
2460
|
"type": "string",
|
|
2398
2461
|
"example": "14060061"
|
|
2399
2462
|
}
|
|
2463
|
+
},
|
|
2464
|
+
{
|
|
2465
|
+
"name": "sortColumn",
|
|
2466
|
+
"in": "query",
|
|
2467
|
+
"description": "排序字段(响应字段)",
|
|
2468
|
+
"required": false,
|
|
2469
|
+
"schema": {
|
|
2470
|
+
"type": "string",
|
|
2471
|
+
"example": "changeRatio"
|
|
2472
|
+
}
|
|
2473
|
+
},
|
|
2474
|
+
{
|
|
2475
|
+
"name": "order",
|
|
2476
|
+
"in": "query",
|
|
2477
|
+
"description": "排序方向",
|
|
2478
|
+
"required": false,
|
|
2479
|
+
"schema": {
|
|
2480
|
+
"type": "string",
|
|
2481
|
+
"allOf": [
|
|
2482
|
+
{
|
|
2483
|
+
"type": "string",
|
|
2484
|
+
"description": "asc代表正序;\ndesc 代表倒序",
|
|
2485
|
+
"enum": [
|
|
2486
|
+
"asc",
|
|
2487
|
+
"desc"
|
|
2488
|
+
]
|
|
2489
|
+
}
|
|
2490
|
+
],
|
|
2491
|
+
"example": "desc"
|
|
2492
|
+
}
|
|
2493
|
+
},
|
|
2494
|
+
{
|
|
2495
|
+
"name": "page",
|
|
2496
|
+
"in": "query",
|
|
2497
|
+
"description": "页码",
|
|
2498
|
+
"required": false,
|
|
2499
|
+
"schema": {
|
|
2500
|
+
"type": "integer",
|
|
2501
|
+
"format": "int32",
|
|
2502
|
+
"example": 1
|
|
2503
|
+
}
|
|
2504
|
+
},
|
|
2505
|
+
{
|
|
2506
|
+
"name": "pageSize",
|
|
2507
|
+
"in": "query",
|
|
2508
|
+
"description": "页长",
|
|
2509
|
+
"required": false,
|
|
2510
|
+
"schema": {
|
|
2511
|
+
"type": "integer",
|
|
2512
|
+
"format": "int32",
|
|
2513
|
+
"example": 10
|
|
2514
|
+
}
|
|
2400
2515
|
}
|
|
2401
2516
|
],
|
|
2402
2517
|
"responses": {
|
package/data/tree.json
CHANGED
|
@@ -3409,7 +3409,7 @@
|
|
|
3409
3409
|
"apiStatus": "release",
|
|
3410
3410
|
"groupId": 1031,
|
|
3411
3411
|
"remarks": null,
|
|
3412
|
-
"toolName": "
|
|
3412
|
+
"toolName": "get_ind_stk_realtime_qt",
|
|
3413
3413
|
"toolId": "get_industry_stock_realtime_quote",
|
|
3414
3414
|
"apiPath": "industry-quote/stock-realtime",
|
|
3415
3415
|
"enabled": 1,
|
package/lib/call-api.js
CHANGED
|
@@ -6,11 +6,17 @@ const {
|
|
|
6
6
|
} = require("./skill-installer");
|
|
7
7
|
const {
|
|
8
8
|
CONFIG_DIR_ENV,
|
|
9
|
+
API_BASE_URL_ENV,
|
|
10
|
+
DEFAULT_API_BASE_URL,
|
|
9
11
|
getConfigDir,
|
|
10
12
|
getCredentialsPath,
|
|
13
|
+
readConfig,
|
|
11
14
|
readCredentials,
|
|
15
|
+
removeApiBaseUrl,
|
|
12
16
|
removeCredentials,
|
|
17
|
+
resolveApiBaseUrl,
|
|
13
18
|
resolveApiKey,
|
|
19
|
+
saveApiBaseUrl,
|
|
14
20
|
saveCredentials,
|
|
15
21
|
} = require("./config");
|
|
16
22
|
const { version: PACKAGE_VERSION } = require("../package.json");
|
|
@@ -23,7 +29,7 @@ const {
|
|
|
23
29
|
runUpdateCommand,
|
|
24
30
|
} = require("./update");
|
|
25
31
|
|
|
26
|
-
const BASE_URL =
|
|
32
|
+
const BASE_URL = DEFAULT_API_BASE_URL;
|
|
27
33
|
const REQUEST_TIMEOUT = 30_000;
|
|
28
34
|
const API_KEY_MANAGE_URL = "https://data-api.investoday.net/user/api-key";
|
|
29
35
|
|
|
@@ -68,7 +74,7 @@ function printHelp() {
|
|
|
68
74
|
"investoday-api\n\n" +
|
|
69
75
|
"用法:\n" +
|
|
70
76
|
" investoday-api init\n" +
|
|
71
|
-
" investoday-api config status|path|remove\n" +
|
|
77
|
+
" investoday-api config status|path|remove|set-base-url|reset-base-url\n" +
|
|
72
78
|
" investoday-api update run|status|enable|disable|register|unregister\n" +
|
|
73
79
|
" investoday-api skill list [--page <n>] [--page-size <n>] [--json]\n" +
|
|
74
80
|
" investoday-api skill search <keyword> [--page <n>] [--page-size <n>] [--json]\n" +
|
|
@@ -368,7 +374,11 @@ function runConfigCommand(args) {
|
|
|
368
374
|
"用法:\n" +
|
|
369
375
|
" investoday-api config status\n" +
|
|
370
376
|
" investoday-api config path\n" +
|
|
371
|
-
" investoday-api config remove\n
|
|
377
|
+
" investoday-api config remove\n" +
|
|
378
|
+
" investoday-api config set-base-url <url>\n" +
|
|
379
|
+
" investoday-api config reset-base-url\n\n" +
|
|
380
|
+
`Base URL environment override: ${API_BASE_URL_ENV}\n` +
|
|
381
|
+
`Default Base URL: ${BASE_URL}\n\n` +
|
|
372
382
|
`配置目录环境变量覆盖: \n`
|
|
373
383
|
);
|
|
374
384
|
return;
|
|
@@ -385,9 +395,34 @@ function runConfigCommand(args) {
|
|
|
385
395
|
return;
|
|
386
396
|
}
|
|
387
397
|
|
|
398
|
+
if (action === "set-base-url") {
|
|
399
|
+
const baseUrl = String(args[1] || "").trim();
|
|
400
|
+
if (!baseUrl) {
|
|
401
|
+
exitWithError("Error: config set-base-url requires a URL.");
|
|
402
|
+
}
|
|
403
|
+
if (args.length > 2) {
|
|
404
|
+
exitWithError("Error: config set-base-url accepts exactly one URL.");
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
saveApiBaseUrl(baseUrl);
|
|
408
|
+
} catch (error) {
|
|
409
|
+
exitWithError(`Error: ${error.message}`);
|
|
410
|
+
}
|
|
411
|
+
process.stdout.write(`${resolveApiBaseUrl().baseUrl}\n`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (action === "reset-base-url") {
|
|
416
|
+
removeApiBaseUrl();
|
|
417
|
+
process.stdout.write(`${resolveApiBaseUrl().baseUrl}\n`);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
388
421
|
if (action === "status") {
|
|
389
422
|
const localCredentials = readCredentials();
|
|
390
423
|
const resourceApiKey = resolveApiKey();
|
|
424
|
+
const localConfig = readConfig() || {};
|
|
425
|
+
const apiBaseUrl = resolveApiBaseUrl();
|
|
391
426
|
const localConfigured = Boolean(localCredentials);
|
|
392
427
|
process.stdout.write(
|
|
393
428
|
JSON.stringify({
|
|
@@ -399,6 +434,12 @@ function runConfigCommand(args) {
|
|
|
399
434
|
source: resourceApiKey.source,
|
|
400
435
|
localConfig: localCredentials && localCredentials.apiKey ? "configured" : "missing",
|
|
401
436
|
},
|
|
437
|
+
baseUrl: {
|
|
438
|
+
value: apiBaseUrl.baseUrl,
|
|
439
|
+
source: apiBaseUrl.source,
|
|
440
|
+
env: API_BASE_URL_ENV,
|
|
441
|
+
localConfig: localConfig[API_BASE_URL_ENV] ? "configured" : "missing",
|
|
442
|
+
},
|
|
402
443
|
configDir: getConfigDir(),
|
|
403
444
|
configFile: getCredentialsPath(),
|
|
404
445
|
}, null, 2) + "\n"
|
|
@@ -925,8 +966,9 @@ function resolveRequestEndpoint(apiPath, method = "") {
|
|
|
925
966
|
return null;
|
|
926
967
|
}
|
|
927
968
|
|
|
928
|
-
function buildUrl(apiPath, params) {
|
|
929
|
-
|
|
969
|
+
function buildUrl(apiPath, params, options = {}) {
|
|
970
|
+
const baseUrl = String(options.baseUrl || resolveApiBaseUrl().baseUrl).replace(/\/+$/g, "");
|
|
971
|
+
let url = `${baseUrl}/${apiPath}`;
|
|
930
972
|
if (!Object.keys(params).length) {
|
|
931
973
|
return url;
|
|
932
974
|
}
|
|
@@ -1547,7 +1589,7 @@ async function callApi(apiPath, method, params, apiKey, options = {}) {
|
|
|
1547
1589
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT),
|
|
1548
1590
|
};
|
|
1549
1591
|
|
|
1550
|
-
let url =
|
|
1592
|
+
let url = buildUrl(apiPath, {});
|
|
1551
1593
|
if (method === "POST") {
|
|
1552
1594
|
const { queryParams, bodyParams } = splitPostParams(params, options.endpoint, options.bodyJson);
|
|
1553
1595
|
url = buildUrl(apiPath, queryParams);
|
package/lib/config.js
CHANGED
|
@@ -4,6 +4,8 @@ const path = require("node:path");
|
|
|
4
4
|
|
|
5
5
|
const CONFIG_DIR_ENV = "INVESTODAY_API_CONFIG_DIR";
|
|
6
6
|
const API_KEY_ENV = "INVESTODAY_API_KEY";
|
|
7
|
+
const API_BASE_URL_ENV = "INVESTODAY_API_BASE_URL";
|
|
8
|
+
const DEFAULT_API_BASE_URL = "https://data-api.investoday.net/data";
|
|
7
9
|
const LEGACY_SUB_API_KEY_ENV = "SUB_INVESTODAY_API_KEY";
|
|
8
10
|
const CONFIG_FILE = "investoday-api.config.json";
|
|
9
11
|
const LEGACY_CREDENTIALS_FILE = "credentials.enc";
|
|
@@ -52,6 +54,31 @@ function saveConfig(config, env = process.env) {
|
|
|
52
54
|
atomicWriteFile(getCredentialsPath(env), `${JSON.stringify(config || {}, null, 2)}\n`);
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
function normalizeApiBaseUrl(value) {
|
|
58
|
+
const trimmed = String(value || "").trim();
|
|
59
|
+
if (!trimmed) {
|
|
60
|
+
throw new Error(`${API_BASE_URL_ENV} cannot be empty`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let parsed;
|
|
64
|
+
const normalized = trimmed.replace(/\/+$/g, "");
|
|
65
|
+
try {
|
|
66
|
+
parsed = new URL(normalized);
|
|
67
|
+
} catch {
|
|
68
|
+
throw new Error(`${API_BASE_URL_ENV} must be a valid URL`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
72
|
+
throw new Error(`${API_BASE_URL_ENV} must use http or https`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (parsed.search || parsed.hash) {
|
|
76
|
+
throw new Error(`${API_BASE_URL_ENV} must not include query or hash`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return normalized;
|
|
80
|
+
}
|
|
81
|
+
|
|
55
82
|
function removeLegacyCredentials(env = process.env) {
|
|
56
83
|
for (const filePath of [getLegacyCredentialsPath(env), getLegacyKeyPath(env)]) {
|
|
57
84
|
if (fs.existsSync(filePath)) {
|
|
@@ -76,6 +103,14 @@ function saveCredentials(apiKey, env = process.env) {
|
|
|
76
103
|
removeLegacyCredentials(env);
|
|
77
104
|
}
|
|
78
105
|
|
|
106
|
+
function saveApiBaseUrl(baseUrl, env = process.env) {
|
|
107
|
+
const existing = readConfig(env) || {};
|
|
108
|
+
saveConfig({
|
|
109
|
+
...existing,
|
|
110
|
+
[API_BASE_URL_ENV]: normalizeApiBaseUrl(baseUrl),
|
|
111
|
+
}, env);
|
|
112
|
+
}
|
|
113
|
+
|
|
79
114
|
function readConfig(env = process.env) {
|
|
80
115
|
const configPath = getCredentialsPath(env);
|
|
81
116
|
if (!fs.existsSync(configPath)) {
|
|
@@ -111,6 +146,15 @@ function removeCredentials(env = process.env) {
|
|
|
111
146
|
removeLegacyCredentials(env);
|
|
112
147
|
}
|
|
113
148
|
|
|
149
|
+
function removeApiBaseUrl(env = process.env) {
|
|
150
|
+
const existing = readConfig(env);
|
|
151
|
+
if (!existing || !Object.prototype.hasOwnProperty.call(existing, API_BASE_URL_ENV)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
delete existing[API_BASE_URL_ENV];
|
|
155
|
+
saveConfig(existing, env);
|
|
156
|
+
}
|
|
157
|
+
|
|
114
158
|
function resolveApiKey(env = process.env) {
|
|
115
159
|
const envKey = String(env[API_KEY_ENV] || "").trim();
|
|
116
160
|
if (envKey) {
|
|
@@ -125,11 +169,28 @@ function resolveApiKey(env = process.env) {
|
|
|
125
169
|
return { apiKey: "", source: "missing" };
|
|
126
170
|
}
|
|
127
171
|
|
|
172
|
+
function resolveApiBaseUrl(env = process.env) {
|
|
173
|
+
const envBaseUrl = String(env[API_BASE_URL_ENV] || "").trim();
|
|
174
|
+
if (envBaseUrl) {
|
|
175
|
+
return { baseUrl: normalizeApiBaseUrl(envBaseUrl), source: "env" };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const config = readConfig(env);
|
|
179
|
+
const configBaseUrl = String(config && config[API_BASE_URL_ENV] || "").trim();
|
|
180
|
+
if (configBaseUrl) {
|
|
181
|
+
return { baseUrl: normalizeApiBaseUrl(configBaseUrl), source: "config" };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { baseUrl: DEFAULT_API_BASE_URL, source: "default" };
|
|
185
|
+
}
|
|
186
|
+
|
|
128
187
|
module.exports = {
|
|
129
188
|
API_KEY_ENV,
|
|
189
|
+
API_BASE_URL_ENV,
|
|
130
190
|
CONFIG_FILE,
|
|
131
191
|
CONFIG_DIR_ENV,
|
|
132
192
|
CREDENTIALS_FILE: CONFIG_FILE,
|
|
193
|
+
DEFAULT_API_BASE_URL,
|
|
133
194
|
LEGACY_CREDENTIALS_FILE,
|
|
134
195
|
LEGACY_KEY_FILE,
|
|
135
196
|
getConfigDir,
|
|
@@ -139,8 +200,12 @@ module.exports = {
|
|
|
139
200
|
removeLegacyCredentials,
|
|
140
201
|
readConfig,
|
|
141
202
|
readCredentials,
|
|
203
|
+
normalizeApiBaseUrl,
|
|
204
|
+
removeApiBaseUrl,
|
|
142
205
|
removeCredentials,
|
|
143
206
|
resolveApiKey,
|
|
207
|
+
resolveApiBaseUrl,
|
|
208
|
+
saveApiBaseUrl,
|
|
144
209
|
saveConfig,
|
|
145
210
|
saveCredentials,
|
|
146
211
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@investoday/investoday-api",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "CLI for accessing InvestToday China market financial data via investoday-api.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/investoday-data/investoday-finance-data",
|