@tigeropenapi/tigeropen 0.4.0 → 0.4.5
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/dist/cjs/client/case-convert.js +4 -3
- package/dist/cjs/client/http-client.js +122 -5
- package/dist/cjs/config/client-config.js +56 -12
- package/dist/cjs/config/index.js +3 -1
- package/dist/cjs/config/token-manager.js +54 -4
- package/dist/cjs/index.js +51 -2
- package/dist/cjs/push/push-client.js +40 -15
- package/dist/cjs/quote/quote-client.js +14 -2
- package/dist/cjs/trade/trade-client.js +40 -2
- package/dist/esm/client/api-request.js +1 -1
- package/dist/esm/client/api-response.js +1 -1
- package/dist/esm/client/case-convert.js +4 -3
- package/dist/esm/client/http-client.js +127 -10
- package/dist/esm/client/index.js +6 -6
- package/dist/esm/config/client-config.js +59 -15
- package/dist/esm/config/index.js +4 -3
- package/dist/esm/config/token-manager.js +55 -5
- package/dist/esm/index.js +15 -1
- package/dist/esm/logger/index.js +1 -1
- package/dist/esm/model/contract-helpers.js +1 -1
- package/dist/esm/model/order-helpers.js +1 -1
- package/dist/esm/model/order-status.js +1 -1
- package/dist/esm/push/index.js +5 -5
- package/dist/esm/push/pb/PushData.js +11 -11
- package/dist/esm/push/pb/QuoteBasicData.js +1 -1
- package/dist/esm/push/pb/Response.js +1 -1
- package/dist/esm/push/pb/index.js +17 -17
- package/dist/esm/push/proto-message.js +3 -3
- package/dist/esm/push/push-client.js +47 -22
- package/dist/esm/quote/index.js +1 -1
- package/dist/esm/quote/quote-client.js +16 -4
- package/dist/esm/signer/index.js +2 -2
- package/dist/esm/trade/index.js +1 -1
- package/dist/esm/trade/trade-client.js +43 -5
- package/dist/types/client/case-convert.d.ts +3 -1
- package/dist/types/client/http-client.d.ts +32 -1
- package/dist/types/config/client-config.d.ts +16 -1
- package/dist/types/config/index.d.ts +2 -0
- package/dist/types/config/token-manager.d.ts +27 -3
- package/dist/types/index.d.ts +22 -1
- package/dist/types/model/trade-requests.d.ts +65 -0
- package/dist/types/model/trade.d.ts +102 -11
- package/dist/types/push/push-client.d.ts +5 -3
- package/dist/types/quote/quote-client.d.ts +3 -1
- package/dist/types/trade/trade-client.d.ts +15 -4
- package/package.json +2 -2
|
@@ -17,7 +17,9 @@ function camelToSnake(key) {
|
|
|
17
17
|
/**
|
|
18
18
|
* Recursively convert all object keys from camelCase to snake_case.
|
|
19
19
|
* Preserves arrays, primitives, and Date/null/undefined.
|
|
20
|
-
*
|
|
20
|
+
*
|
|
21
|
+
* Note: camelToSnake is idempotent for pure snake_case keys (no uppercase
|
|
22
|
+
* letters → no replacements), so no pre-check is needed.
|
|
21
23
|
*/
|
|
22
24
|
function keysToSnakeCase(value) {
|
|
23
25
|
if (Array.isArray(value)) {
|
|
@@ -26,8 +28,7 @@ function keysToSnakeCase(value) {
|
|
|
26
28
|
if (value && typeof value === 'object' && value.constructor === Object) {
|
|
27
29
|
const out = {};
|
|
28
30
|
for (const [k, v] of Object.entries(value)) {
|
|
29
|
-
|
|
30
|
-
out[newKey] = keysToSnakeCase(v);
|
|
31
|
+
out[camelToSnake(k)] = keysToSnakeCase(v);
|
|
31
32
|
}
|
|
32
33
|
return out;
|
|
33
34
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HttpClient = void 0;
|
|
4
|
+
const token_manager_1 = require("../config/token-manager");
|
|
4
5
|
const sign_content_1 = require("../signer/sign-content");
|
|
5
6
|
const signer_1 = require("../signer/signer");
|
|
6
7
|
const errors_1 = require("./errors");
|
|
@@ -16,14 +17,45 @@ const DEFAULT_CHARSET = 'UTF-8';
|
|
|
16
17
|
const DEFAULT_SIGN_TYPE = 'RSA';
|
|
17
18
|
/** 默认 API 版本 */
|
|
18
19
|
const DEFAULT_VERSION = '2.0';
|
|
20
|
+
/** Token 刷新 API 方法名 */
|
|
21
|
+
const METHOD_TOKEN_REFRESH = 'user_token_refresh';
|
|
22
|
+
/** 默认后台 token 检查间隔(毫秒):5 分钟 */
|
|
23
|
+
const DEFAULT_TOKEN_CHECK_INTERVAL = 5 * 60 * 1000;
|
|
19
24
|
/**
|
|
20
25
|
* HttpClient handles HTTP requests, signing, retry, and timeout
|
|
21
26
|
*/
|
|
22
27
|
class HttpClient {
|
|
23
28
|
constructor(config, retryPolicy, options) {
|
|
29
|
+
/** non-null when auto-refresh is active */
|
|
30
|
+
this.tokenManager = null;
|
|
24
31
|
this.config = config;
|
|
25
32
|
this.retryPolicy = retryPolicy ?? (0, retry_1.defaultRetryPolicy)();
|
|
26
33
|
this.targetUrl = options?.useQuoteServerUrl ? config.quoteServerUrl : config.serverUrl;
|
|
34
|
+
// Auto-start token refresh when tokenRefreshDuration is configured (mirrors Go SDK behaviour)
|
|
35
|
+
if (config.tokenRefreshDuration && config.tokenRefreshDuration > 0) {
|
|
36
|
+
const interval = config.tokenCheckInterval ?? DEFAULT_TOKEN_CHECK_INTERVAL;
|
|
37
|
+
const opts = {
|
|
38
|
+
refreshDuration: config.tokenRefreshDuration,
|
|
39
|
+
refreshInterval: interval,
|
|
40
|
+
};
|
|
41
|
+
if (config.tokenWriter) {
|
|
42
|
+
opts.tokenWriter = config.tokenWriter;
|
|
43
|
+
}
|
|
44
|
+
if (config.tokenLoader) {
|
|
45
|
+
opts.tokenLoader = config.tokenLoader;
|
|
46
|
+
}
|
|
47
|
+
this.tokenManager = this.startTokenAutoRefresh(null, opts);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Stop the background token auto-refresh timer.
|
|
52
|
+
* Call this when the HttpClient is no longer needed to avoid timer leaks.
|
|
53
|
+
*/
|
|
54
|
+
close() {
|
|
55
|
+
if (this.tokenManager) {
|
|
56
|
+
this.tokenManager.stopAutoRefresh();
|
|
57
|
+
this.tokenManager = null;
|
|
58
|
+
}
|
|
27
59
|
}
|
|
28
60
|
/**
|
|
29
61
|
* Build common request parameters
|
|
@@ -34,7 +66,11 @@ class HttpClient {
|
|
|
34
66
|
method: apiMethod,
|
|
35
67
|
charset: DEFAULT_CHARSET,
|
|
36
68
|
sign_type: DEFAULT_SIGN_TYPE,
|
|
37
|
-
timestamp:
|
|
69
|
+
timestamp: (() => {
|
|
70
|
+
const d = new Date();
|
|
71
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
72
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
73
|
+
})(),
|
|
38
74
|
version: version || DEFAULT_VERSION,
|
|
39
75
|
biz_content: bizContent,
|
|
40
76
|
};
|
|
@@ -61,6 +97,7 @@ class HttpClient {
|
|
|
61
97
|
const headers = {
|
|
62
98
|
'Content-Type': 'application/json;charset=UTF-8',
|
|
63
99
|
'User-Agent': USER_AGENT,
|
|
100
|
+
'Connection': 'close',
|
|
64
101
|
};
|
|
65
102
|
if (this.config.token) {
|
|
66
103
|
headers['Authorization'] = this.config.token;
|
|
@@ -71,7 +108,8 @@ class HttpClient {
|
|
|
71
108
|
body,
|
|
72
109
|
signal: AbortSignal.timeout(this.config.timeout * 1000),
|
|
73
110
|
});
|
|
74
|
-
|
|
111
|
+
const text = await response.text();
|
|
112
|
+
return text;
|
|
75
113
|
}
|
|
76
114
|
/**
|
|
77
115
|
* Execute a structured API request, returning a parsed ApiResponse.
|
|
@@ -98,6 +136,10 @@ class HttpClient {
|
|
|
98
136
|
}
|
|
99
137
|
catch (err) {
|
|
100
138
|
lastErr = err;
|
|
139
|
+
// Only retry on network errors, not API business errors (TigerError with code)
|
|
140
|
+
if (err.code !== undefined && err.code !== -1) {
|
|
141
|
+
throw err; // API returned a business error code, don't retry
|
|
142
|
+
}
|
|
101
143
|
if (!this.retryPolicy.shouldRetry(request.method)) {
|
|
102
144
|
throw err;
|
|
103
145
|
}
|
|
@@ -110,9 +152,10 @@ class HttpClient {
|
|
|
110
152
|
*
|
|
111
153
|
* @param apiMethod - API method name (e.g. "market_state", "place_order")
|
|
112
154
|
* @param requestJson - Raw biz_content JSON string
|
|
155
|
+
* @param version - Optional API version override (default: "2.0")
|
|
113
156
|
* @returns Raw response JSON string, unparsed
|
|
114
157
|
*/
|
|
115
|
-
async execute(apiMethod, requestJson) {
|
|
158
|
+
async execute(apiMethod, requestJson, version) {
|
|
116
159
|
// Parameter validation
|
|
117
160
|
if (!apiMethod) {
|
|
118
161
|
throw new errors_1.TigerError(-1, 'api_method must not be empty');
|
|
@@ -123,7 +166,7 @@ class HttpClient {
|
|
|
123
166
|
catch {
|
|
124
167
|
throw new errors_1.TigerError(-1, 'request_json is not valid JSON');
|
|
125
168
|
}
|
|
126
|
-
const params = this.buildCommonParams(apiMethod, requestJson);
|
|
169
|
+
const params = this.buildCommonParams(apiMethod, requestJson, version);
|
|
127
170
|
const sign = this.signParams(params);
|
|
128
171
|
params['sign'] = sign;
|
|
129
172
|
let lastErr;
|
|
@@ -144,6 +187,10 @@ class HttpClient {
|
|
|
144
187
|
}
|
|
145
188
|
catch (err) {
|
|
146
189
|
lastErr = err;
|
|
190
|
+
// Don't retry API business errors
|
|
191
|
+
if (err.code !== undefined && err.code !== -1) {
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
147
194
|
if (!this.retryPolicy.shouldRetry(apiMethod)) {
|
|
148
195
|
throw err;
|
|
149
196
|
}
|
|
@@ -151,6 +198,75 @@ class HttpClient {
|
|
|
151
198
|
}
|
|
152
199
|
throw lastErr;
|
|
153
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* 调用 user_token_refresh 接口获取新 token,返回新 token 字符串。
|
|
203
|
+
* 此方法仅查询,不修改 config 中的 token。
|
|
204
|
+
*/
|
|
205
|
+
async queryToken() {
|
|
206
|
+
const params = this.buildCommonParams(METHOD_TOKEN_REFRESH, '{}');
|
|
207
|
+
const sign = this.signParams(params);
|
|
208
|
+
params['sign'] = sign;
|
|
209
|
+
const body = await this.doHttpPost(params);
|
|
210
|
+
const response = (0, api_response_1.parseApiResponse)(body);
|
|
211
|
+
this.verifyResponseSign(params['timestamp'], response.sign);
|
|
212
|
+
const data = (0, api_response_1.unmarshalData)(response.data);
|
|
213
|
+
const newToken = data?.token;
|
|
214
|
+
if (!newToken) {
|
|
215
|
+
throw new errors_1.TigerError(-1, '服务端返回空 token');
|
|
216
|
+
}
|
|
217
|
+
return newToken;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 刷新 token:调用接口获取新 token 后更新 config.token。
|
|
221
|
+
*
|
|
222
|
+
* @param tokenManager - 可选外部 TokenManager;若提供则调用 setToken() 写文件并触发回调。
|
|
223
|
+
* 若 NewHttpClient 已自动启动内部 TokenManager,其内存 token 也会同步更新。
|
|
224
|
+
*/
|
|
225
|
+
async refreshToken(tokenManager) {
|
|
226
|
+
const newToken = await this.queryToken();
|
|
227
|
+
this.config.token = newToken;
|
|
228
|
+
// Keep internal tokenManager in sync so shouldTokenRefresh stays accurate.
|
|
229
|
+
if (this.tokenManager && tokenManager !== this.tokenManager) {
|
|
230
|
+
this.tokenManager.syncToken(newToken);
|
|
231
|
+
}
|
|
232
|
+
if (tokenManager) {
|
|
233
|
+
tokenManager.setToken(newToken);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* 启动后台 token 自动刷新。
|
|
238
|
+
*
|
|
239
|
+
* @param tokenManager - 可为 null:SDK 内部自动创建一个仅内存的 TokenManager,
|
|
240
|
+
* 适合直接通过 token 在代码里设置、不使用文件的场景。
|
|
241
|
+
* @param options - 仅在 tokenManager 为 null 时生效,用于配置内部 TokenManager
|
|
242
|
+
* (refreshDuration / refreshInterval / tokenWriter / tokenLoader)。
|
|
243
|
+
* @returns 使用或创建的 TokenManager 实例
|
|
244
|
+
*/
|
|
245
|
+
startTokenAutoRefresh(tokenManager, options) {
|
|
246
|
+
if (!tokenManager) {
|
|
247
|
+
tokenManager = new token_manager_1.TokenManager(options);
|
|
248
|
+
// 从 config.token 同步当前 token,使 shouldTokenRefresh 能正确判断。
|
|
249
|
+
// 若 config.token 暂时为空(异步 tokenLoader 尚未 resolve),则先异步加载。
|
|
250
|
+
if (this.config.token) {
|
|
251
|
+
tokenManager.syncToken(this.config.token);
|
|
252
|
+
}
|
|
253
|
+
else if (options?.tokenLoader) {
|
|
254
|
+
// tokenLoader 可能是异步的;在后台 resolve 后同步到 config.token 和 TokenManager。
|
|
255
|
+
Promise.resolve(options.tokenLoader()).then((t) => {
|
|
256
|
+
if (t) {
|
|
257
|
+
this.config.token = t;
|
|
258
|
+
tokenManager.syncToken(t);
|
|
259
|
+
}
|
|
260
|
+
}).catch(() => { });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
tokenManager.startAutoRefresh(async () => {
|
|
264
|
+
const newToken = await this.queryToken();
|
|
265
|
+
this.config.token = newToken;
|
|
266
|
+
return newToken;
|
|
267
|
+
});
|
|
268
|
+
return tokenManager;
|
|
269
|
+
}
|
|
154
270
|
/** Delay for the specified milliseconds */
|
|
155
271
|
sleep(ms) {
|
|
156
272
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -164,7 +280,8 @@ class HttpClient {
|
|
|
164
280
|
*/
|
|
165
281
|
verifyResponseSign(timestamp, signBase64) {
|
|
166
282
|
if (!signBase64) {
|
|
167
|
-
|
|
283
|
+
// Some endpoints don't return a signature; skip verification silently.
|
|
284
|
+
return;
|
|
168
285
|
}
|
|
169
286
|
const valid = (0, signer_1.verifyWithRSA)(this.config.tigerPublicKey, timestamp, signBase64);
|
|
170
287
|
if (!valid) {
|
|
@@ -17,6 +17,7 @@ const path_1 = require("path");
|
|
|
17
17
|
const os_1 = require("os");
|
|
18
18
|
const config_parser_1 = require("./config-parser");
|
|
19
19
|
const domain_1 = require("./domain");
|
|
20
|
+
const token_manager_1 = require("./token-manager");
|
|
20
21
|
/** Default config file name */
|
|
21
22
|
const CONFIG_FILE_NAME = 'tiger_openapi_config.properties';
|
|
22
23
|
/** Defaults */
|
|
@@ -30,6 +31,7 @@ const ENV_TIGER_ID = 'TIGEROPEN_TIGER_ID';
|
|
|
30
31
|
const ENV_PRIVATE_KEY = 'TIGEROPEN_PRIVATE_KEY';
|
|
31
32
|
const ENV_ACCOUNT = 'TIGEROPEN_ACCOUNT';
|
|
32
33
|
const ENV_TOKEN = 'TIGEROPEN_TOKEN';
|
|
34
|
+
const ENV_TOKEN_FILE = 'TIGEROPEN_TOKEN_FILE';
|
|
33
35
|
/**
|
|
34
36
|
* Load and parse a Java-style properties file.
|
|
35
37
|
*
|
|
@@ -103,7 +105,18 @@ function createClientConfig(options) {
|
|
|
103
105
|
// Load properties: explicit path > auto-discovery
|
|
104
106
|
let fileProps = {};
|
|
105
107
|
if (opts.propertiesFilePath) {
|
|
106
|
-
|
|
108
|
+
// Support both directory path and full file path
|
|
109
|
+
let filePath = opts.propertiesFilePath;
|
|
110
|
+
if ((0, fs_1.existsSync)(filePath) && (0, fs_1.statSync)(filePath).isDirectory()) {
|
|
111
|
+
filePath = (0, path_1.join)(filePath, CONFIG_FILE_NAME);
|
|
112
|
+
}
|
|
113
|
+
else if (!(0, fs_1.existsSync)(filePath) && !filePath.endsWith('.properties')) {
|
|
114
|
+
// Path doesn't exist as-is; try treating it as a directory
|
|
115
|
+
const asDir = (0, path_1.join)(filePath, CONFIG_FILE_NAME);
|
|
116
|
+
if ((0, fs_1.existsSync)(asDir))
|
|
117
|
+
filePath = asDir;
|
|
118
|
+
}
|
|
119
|
+
fileProps = loadPropertiesFile(filePath);
|
|
107
120
|
}
|
|
108
121
|
else {
|
|
109
122
|
const discovered = discoverConfigFile();
|
|
@@ -136,6 +149,37 @@ function createClientConfig(options) {
|
|
|
136
149
|
if (envToken) {
|
|
137
150
|
token = envToken;
|
|
138
151
|
}
|
|
152
|
+
// Token loading priority: env TIGEROPEN_TOKEN > tokenLoader > token file (TIGEROPEN_TOKEN_FILE or default)
|
|
153
|
+
if (!token) {
|
|
154
|
+
if (opts.tokenLoader) {
|
|
155
|
+
// tokenLoader is async-capable; we do a best-effort synchronous call here.
|
|
156
|
+
// If the loader returns a Promise, the token will be set later via HttpClient.
|
|
157
|
+
try {
|
|
158
|
+
const result = opts.tokenLoader();
|
|
159
|
+
if (result instanceof Promise) {
|
|
160
|
+
// Cannot await in sync constructor; HttpClient.startTokenAutoRefresh() will
|
|
161
|
+
// resolve this Promise and sync the token after construction.
|
|
162
|
+
}
|
|
163
|
+
else if (result) {
|
|
164
|
+
token = result;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// loader failed; leave token undefined
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// Load from token file: TIGEROPEN_TOKEN_FILE env var > default file
|
|
173
|
+
const tokenFilePath = process.env[ENV_TOKEN_FILE] || 'tiger_openapi_token.properties';
|
|
174
|
+
const tm = new token_manager_1.TokenManager({ filePath: tokenFilePath });
|
|
175
|
+
try {
|
|
176
|
+
token = tm.loadTokenSync();
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// file not found or no token field; leave token undefined
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
139
183
|
// Validate required fields
|
|
140
184
|
if (!tigerId) {
|
|
141
185
|
throw new Error(`tigerId is required. Set it via options.tigerId, a config file, or the ${ENV_TIGER_ID} environment variable.`);
|
|
@@ -147,15 +191,16 @@ function createClientConfig(options) {
|
|
|
147
191
|
const enableDynamicDomain = opts.enableDynamicDomain ?? true;
|
|
148
192
|
let serverUrl;
|
|
149
193
|
let quoteServerUrl;
|
|
194
|
+
// Query dynamic domains once (may spawn a child process); reuse for both URLs.
|
|
195
|
+
let domainConf = {};
|
|
196
|
+
if (enableDynamicDomain && (!opts.serverUrl || !opts.quoteServerUrl)) {
|
|
197
|
+
domainConf = (0, domain_1.queryDomains)(license);
|
|
198
|
+
}
|
|
150
199
|
if (opts.serverUrl) {
|
|
151
200
|
serverUrl = opts.serverUrl;
|
|
152
201
|
}
|
|
153
202
|
else {
|
|
154
|
-
|
|
155
|
-
if (enableDynamicDomain) {
|
|
156
|
-
const domainConf = (0, domain_1.queryDomains)(license);
|
|
157
|
-
dynamicUrl = (0, domain_1.resolveDynamicServerUrl)(domainConf, license);
|
|
158
|
-
}
|
|
203
|
+
const dynamicUrl = (0, domain_1.resolveDynamicServerUrl)(domainConf, license);
|
|
159
204
|
serverUrl = dynamicUrl || DEFAULT_SERVER_URL;
|
|
160
205
|
}
|
|
161
206
|
// Resolve quote server URL: explicit > dynamic domain > serverUrl fallback
|
|
@@ -163,11 +208,7 @@ function createClientConfig(options) {
|
|
|
163
208
|
quoteServerUrl = opts.quoteServerUrl;
|
|
164
209
|
}
|
|
165
210
|
else {
|
|
166
|
-
|
|
167
|
-
if (enableDynamicDomain) {
|
|
168
|
-
const domainConf = (0, domain_1.queryDomains)(license);
|
|
169
|
-
dynamicQuoteUrl = (0, domain_1.resolveDynamicQuoteServerUrl)(domainConf, license);
|
|
170
|
-
}
|
|
211
|
+
const dynamicQuoteUrl = (0, domain_1.resolveDynamicQuoteServerUrl)(domainConf, license);
|
|
171
212
|
quoteServerUrl = dynamicQuoteUrl || serverUrl;
|
|
172
213
|
}
|
|
173
214
|
return {
|
|
@@ -180,10 +221,13 @@ function createClientConfig(options) {
|
|
|
180
221
|
timeout: opts.timeout ?? DEFAULT_TIMEOUT,
|
|
181
222
|
token,
|
|
182
223
|
tokenRefreshDuration: opts.tokenRefreshDuration,
|
|
224
|
+
tokenCheckInterval: opts.tokenCheckInterval,
|
|
225
|
+
tokenLoader: opts.tokenLoader,
|
|
226
|
+
tokenWriter: opts.tokenWriter,
|
|
183
227
|
serverUrl,
|
|
184
228
|
quoteServerUrl,
|
|
185
229
|
deviceId: detectDeviceId(),
|
|
186
|
-
tigerPublicKey: TIGER_PUBLIC_KEY,
|
|
230
|
+
tigerPublicKey: opts.tigerPublicKey ?? TIGER_PUBLIC_KEY,
|
|
187
231
|
};
|
|
188
232
|
}
|
|
189
233
|
/**
|
package/dist/cjs/config/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveDynamicQuoteServerUrl = exports.resolveDynamicServerUrl = exports.queryDomains = exports.loadPropertiesFile = exports.createClientConfig = exports.parsePropertiesString = exports.parsePropertiesFile = void 0;
|
|
3
|
+
exports.resolveDynamicQuoteServerUrl = exports.resolveDynamicServerUrl = exports.queryDomains = exports.TokenManager = exports.loadPropertiesFile = exports.createClientConfig = exports.parsePropertiesString = exports.parsePropertiesFile = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* 配置层导出
|
|
6
6
|
*/
|
|
@@ -10,6 +10,8 @@ Object.defineProperty(exports, "parsePropertiesString", { enumerable: true, get:
|
|
|
10
10
|
var client_config_1 = require("./client-config");
|
|
11
11
|
Object.defineProperty(exports, "createClientConfig", { enumerable: true, get: function () { return client_config_1.createClientConfig; } });
|
|
12
12
|
Object.defineProperty(exports, "loadPropertiesFile", { enumerable: true, get: function () { return client_config_1.loadPropertiesFile; } });
|
|
13
|
+
var token_manager_1 = require("./token-manager");
|
|
14
|
+
Object.defineProperty(exports, "TokenManager", { enumerable: true, get: function () { return token_manager_1.TokenManager; } });
|
|
13
15
|
var domain_1 = require("./domain");
|
|
14
16
|
Object.defineProperty(exports, "queryDomains", { enumerable: true, get: function () { return domain_1.queryDomains; } });
|
|
15
17
|
Object.defineProperty(exports, "resolveDynamicServerUrl", { enumerable: true, get: function () { return domain_1.resolveDynamicServerUrl; } });
|
|
@@ -21,6 +21,7 @@ class TokenManager {
|
|
|
21
21
|
constructor(options) {
|
|
22
22
|
this.token = '';
|
|
23
23
|
this.timer = null;
|
|
24
|
+
this.fileEnabled = options?.filePath !== undefined;
|
|
24
25
|
this.filePath = options?.filePath ?? DEFAULT_TOKEN_FILE;
|
|
25
26
|
this.refreshInterval = options?.refreshInterval ?? DEFAULT_REFRESH_INTERVAL;
|
|
26
27
|
let dur = options?.refreshDuration ?? 0;
|
|
@@ -28,9 +29,39 @@ class TokenManager {
|
|
|
28
29
|
dur = 30;
|
|
29
30
|
}
|
|
30
31
|
this.refreshDuration = dur;
|
|
32
|
+
this.tokenLoader = options?.tokenLoader;
|
|
33
|
+
this.tokenWriter = options?.tokenWriter;
|
|
31
34
|
}
|
|
32
|
-
/**
|
|
33
|
-
|
|
35
|
+
/**
|
|
36
|
+
* 从 properties 文件(或自定义 tokenLoader)加载 Token。
|
|
37
|
+
* 若设置了 tokenLoader,优先调用;否则从 properties 文件读取。
|
|
38
|
+
*/
|
|
39
|
+
async loadToken() {
|
|
40
|
+
if (this.tokenLoader) {
|
|
41
|
+
const token = await this.tokenLoader();
|
|
42
|
+
if (!token) {
|
|
43
|
+
throw new Error('自定义 tokenLoader 返回空值');
|
|
44
|
+
}
|
|
45
|
+
this.token = token;
|
|
46
|
+
return token;
|
|
47
|
+
}
|
|
48
|
+
const content = (0, fs_1.readFileSync)(this.filePath, 'utf-8');
|
|
49
|
+
const props = (0, config_parser_1.parsePropertiesString)(content);
|
|
50
|
+
const token = props['token'];
|
|
51
|
+
if (!token) {
|
|
52
|
+
throw new Error('Token 文件中未找到 token 字段');
|
|
53
|
+
}
|
|
54
|
+
this.token = token;
|
|
55
|
+
return token;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 同步版文件加载(仅用于无 tokenLoader 时的兼容入口)。
|
|
59
|
+
* 建议优先使用 loadToken()(异步)。
|
|
60
|
+
*/
|
|
61
|
+
loadTokenSync() {
|
|
62
|
+
if (this.tokenLoader) {
|
|
63
|
+
throw new Error('tokenLoader 是异步函数,请使用 loadToken()');
|
|
64
|
+
}
|
|
34
65
|
const content = (0, fs_1.readFileSync)(this.filePath, 'utf-8');
|
|
35
66
|
const props = (0, config_parser_1.parsePropertiesString)(content);
|
|
36
67
|
const token = props['token'];
|
|
@@ -44,10 +75,25 @@ class TokenManager {
|
|
|
44
75
|
getToken() {
|
|
45
76
|
return this.token;
|
|
46
77
|
}
|
|
47
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* 设置 Token;若启用了文件持久化(显式传入 filePath),则同步写文件。
|
|
80
|
+
* 成功后触发 tokenWriter 回调(如有)。
|
|
81
|
+
*/
|
|
48
82
|
setToken(token) {
|
|
49
83
|
this.token = token;
|
|
50
|
-
this.
|
|
84
|
+
if (this.fileEnabled) {
|
|
85
|
+
this.saveTokenToFile(token);
|
|
86
|
+
}
|
|
87
|
+
if (this.tokenWriter) {
|
|
88
|
+
this.tokenWriter(token);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 仅更新内存中的 token,不写文件,不触发 tokenWriter 回调。
|
|
93
|
+
* 用于多个组件共享 token 时的内部同步。
|
|
94
|
+
*/
|
|
95
|
+
syncToken(token) {
|
|
96
|
+
this.token = token;
|
|
51
97
|
}
|
|
52
98
|
/** 将 Token 保存到 properties 文件 */
|
|
53
99
|
saveTokenToFile(token) {
|
|
@@ -104,6 +150,10 @@ class TokenManager {
|
|
|
104
150
|
// 刷新失败时静默跳过
|
|
105
151
|
}
|
|
106
152
|
}, this.refreshInterval);
|
|
153
|
+
// 防止 timer 阻止 Node.js 进程退出
|
|
154
|
+
if (this.timer.unref) {
|
|
155
|
+
this.timer.unref();
|
|
156
|
+
}
|
|
107
157
|
}
|
|
108
158
|
/** 停止后台刷新 */
|
|
109
159
|
stopAutoRefresh() {
|
package/dist/cjs/index.js
CHANGED
|
@@ -16,6 +16,55 @@
|
|
|
16
16
|
* @packageDocumentation
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.VERSION = void 0;
|
|
19
|
+
exports.orderStatusCode = exports.BarPeriod = exports.License = exports.Language = exports.FinancialReportPeriod = exports.OptionAnalysisPeriod = exports.SortDirection = exports.IndustryLevel = exports.CorporateActionType = exports.OrderSortBy = exports.SegmentType = exports.QuoteRight = exports.Currency = exports.Market = exports.TimeInForce = exports.OrderType = exports.SecurityType = exports.OrderStatus = exports.normalizeOrderStatus = exports.stopLimitOrder = exports.stopOrder = exports.limitOrder = exports.marketOrder = exports.SubjectType = exports.ConnectionState = exports.PushClient = exports.TradeClient = exports.parseOptionIdentifier = exports.QuoteClient = exports.classifyErrorCode = exports.TigerError = exports.HttpClient = exports.TokenManager = exports.loadPropertiesFile = exports.createClientConfig = exports.VERSION = void 0;
|
|
20
20
|
/** SDK 版本号 */
|
|
21
|
-
exports.VERSION = '0.4.
|
|
21
|
+
exports.VERSION = '0.4.4';
|
|
22
|
+
// Config
|
|
23
|
+
var config_1 = require("./config");
|
|
24
|
+
Object.defineProperty(exports, "createClientConfig", { enumerable: true, get: function () { return config_1.createClientConfig; } });
|
|
25
|
+
Object.defineProperty(exports, "loadPropertiesFile", { enumerable: true, get: function () { return config_1.loadPropertiesFile; } });
|
|
26
|
+
var config_2 = require("./config");
|
|
27
|
+
Object.defineProperty(exports, "TokenManager", { enumerable: true, get: function () { return config_2.TokenManager; } });
|
|
28
|
+
// Transport
|
|
29
|
+
var client_1 = require("./client");
|
|
30
|
+
Object.defineProperty(exports, "HttpClient", { enumerable: true, get: function () { return client_1.HttpClient; } });
|
|
31
|
+
var client_2 = require("./client");
|
|
32
|
+
Object.defineProperty(exports, "TigerError", { enumerable: true, get: function () { return client_2.TigerError; } });
|
|
33
|
+
Object.defineProperty(exports, "classifyErrorCode", { enumerable: true, get: function () { return client_2.classifyErrorCode; } });
|
|
34
|
+
// Clients
|
|
35
|
+
var quote_1 = require("./quote");
|
|
36
|
+
Object.defineProperty(exports, "QuoteClient", { enumerable: true, get: function () { return quote_1.QuoteClient; } });
|
|
37
|
+
Object.defineProperty(exports, "parseOptionIdentifier", { enumerable: true, get: function () { return quote_1.parseOptionIdentifier; } });
|
|
38
|
+
var trade_1 = require("./trade");
|
|
39
|
+
Object.defineProperty(exports, "TradeClient", { enumerable: true, get: function () { return trade_1.TradeClient; } });
|
|
40
|
+
var push_1 = require("./push");
|
|
41
|
+
Object.defineProperty(exports, "PushClient", { enumerable: true, get: function () { return push_1.PushClient; } });
|
|
42
|
+
Object.defineProperty(exports, "ConnectionState", { enumerable: true, get: function () { return push_1.ConnectionState; } });
|
|
43
|
+
Object.defineProperty(exports, "SubjectType", { enumerable: true, get: function () { return push_1.SubjectType; } });
|
|
44
|
+
var order_helpers_1 = require("./model/order-helpers");
|
|
45
|
+
Object.defineProperty(exports, "marketOrder", { enumerable: true, get: function () { return order_helpers_1.marketOrder; } });
|
|
46
|
+
Object.defineProperty(exports, "limitOrder", { enumerable: true, get: function () { return order_helpers_1.limitOrder; } });
|
|
47
|
+
Object.defineProperty(exports, "stopOrder", { enumerable: true, get: function () { return order_helpers_1.stopOrder; } });
|
|
48
|
+
Object.defineProperty(exports, "stopLimitOrder", { enumerable: true, get: function () { return order_helpers_1.stopLimitOrder; } });
|
|
49
|
+
var order_status_1 = require("./model/order-status");
|
|
50
|
+
Object.defineProperty(exports, "normalizeOrderStatus", { enumerable: true, get: function () { return order_status_1.normalizeOrderStatus; } });
|
|
51
|
+
// Enums
|
|
52
|
+
var enums_1 = require("./model/enums");
|
|
53
|
+
Object.defineProperty(exports, "OrderStatus", { enumerable: true, get: function () { return enums_1.OrderStatus; } });
|
|
54
|
+
Object.defineProperty(exports, "SecurityType", { enumerable: true, get: function () { return enums_1.SecurityType; } });
|
|
55
|
+
Object.defineProperty(exports, "OrderType", { enumerable: true, get: function () { return enums_1.OrderType; } });
|
|
56
|
+
Object.defineProperty(exports, "TimeInForce", { enumerable: true, get: function () { return enums_1.TimeInForce; } });
|
|
57
|
+
Object.defineProperty(exports, "Market", { enumerable: true, get: function () { return enums_1.Market; } });
|
|
58
|
+
Object.defineProperty(exports, "Currency", { enumerable: true, get: function () { return enums_1.Currency; } });
|
|
59
|
+
Object.defineProperty(exports, "QuoteRight", { enumerable: true, get: function () { return enums_1.QuoteRight; } });
|
|
60
|
+
Object.defineProperty(exports, "SegmentType", { enumerable: true, get: function () { return enums_1.SegmentType; } });
|
|
61
|
+
Object.defineProperty(exports, "OrderSortBy", { enumerable: true, get: function () { return enums_1.OrderSortBy; } });
|
|
62
|
+
Object.defineProperty(exports, "CorporateActionType", { enumerable: true, get: function () { return enums_1.CorporateActionType; } });
|
|
63
|
+
Object.defineProperty(exports, "IndustryLevel", { enumerable: true, get: function () { return enums_1.IndustryLevel; } });
|
|
64
|
+
Object.defineProperty(exports, "SortDirection", { enumerable: true, get: function () { return enums_1.SortDirection; } });
|
|
65
|
+
Object.defineProperty(exports, "OptionAnalysisPeriod", { enumerable: true, get: function () { return enums_1.OptionAnalysisPeriod; } });
|
|
66
|
+
Object.defineProperty(exports, "FinancialReportPeriod", { enumerable: true, get: function () { return enums_1.FinancialReportPeriod; } });
|
|
67
|
+
Object.defineProperty(exports, "Language", { enumerable: true, get: function () { return enums_1.Language; } });
|
|
68
|
+
Object.defineProperty(exports, "License", { enumerable: true, get: function () { return enums_1.License; } });
|
|
69
|
+
Object.defineProperty(exports, "BarPeriod", { enumerable: true, get: function () { return enums_1.BarPeriod; } });
|
|
70
|
+
Object.defineProperty(exports, "orderStatusCode", { enumerable: true, get: function () { return enums_1.orderStatusCode; } });
|
|
@@ -86,11 +86,13 @@ class PushClient {
|
|
|
86
86
|
constructor(config, options) {
|
|
87
87
|
this.socket = null;
|
|
88
88
|
this._state = ConnectionState.Disconnected;
|
|
89
|
+
/** Whether this connect attempt already fell back to unverified TLS */
|
|
90
|
+
this.tlsFallback = false;
|
|
89
91
|
this.callbacks = {};
|
|
90
92
|
/** Subscription state: subject -> Set<symbol> */
|
|
91
93
|
this.subscriptions = new Map();
|
|
92
|
-
/** Account-level subscriptions */
|
|
93
|
-
this.accountSubs = new
|
|
94
|
+
/** Account-level subscriptions: subject -> account string used at subscribe time */
|
|
95
|
+
this.accountSubs = new Map();
|
|
94
96
|
this.heartbeatTimer = null;
|
|
95
97
|
this.reconnectTimer = null;
|
|
96
98
|
this.stopped = false;
|
|
@@ -124,6 +126,7 @@ class PushClient {
|
|
|
124
126
|
}
|
|
125
127
|
this._state = ConnectionState.Connecting;
|
|
126
128
|
this.stopped = false;
|
|
129
|
+
this.tlsFallback = false;
|
|
127
130
|
this.recvBuffer = Buffer.alloc(0);
|
|
128
131
|
const socket = this.createSocket(this.pushHost, this.pushPort);
|
|
129
132
|
this.socket = socket;
|
|
@@ -180,6 +183,24 @@ class PushClient {
|
|
|
180
183
|
});
|
|
181
184
|
socket.on('error', (err) => {
|
|
182
185
|
if (!settled) {
|
|
186
|
+
// If this is a TLS certificate error, warn and retry without verification.
|
|
187
|
+
const isTlsCertError = err.message.includes('certificate') ||
|
|
188
|
+
err.message.includes('CERT') ||
|
|
189
|
+
err.code === 'DEPTH_ZERO_SELF_SIGNED_CERT' ||
|
|
190
|
+
err.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' ||
|
|
191
|
+
err.code === 'CERT_HAS_EXPIRED';
|
|
192
|
+
if (isTlsCertError && !this.tlsFallback) {
|
|
193
|
+
this.tlsFallback = true;
|
|
194
|
+
settled = true;
|
|
195
|
+
clearTimeout(timeout);
|
|
196
|
+
socket.destroy();
|
|
197
|
+
this.socket = null;
|
|
198
|
+
this._state = ConnectionState.Disconnected;
|
|
199
|
+
console.warn(`[TigerOpen] TLS certificate verification failed: ${err.message}. Retrying without verification.`);
|
|
200
|
+
// Retry immediately using unverified TLS
|
|
201
|
+
this.connect().then(resolve).catch(reject);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
183
204
|
settled = true;
|
|
184
205
|
clearTimeout(timeout);
|
|
185
206
|
this._state = ConnectionState.Disconnected;
|
|
@@ -232,7 +253,7 @@ class PushClient {
|
|
|
232
253
|
createSocket(host, port) {
|
|
233
254
|
if (this.socketFactory)
|
|
234
255
|
return this.socketFactory(host, port);
|
|
235
|
-
return tls.connect({ host, port, rejectUnauthorized:
|
|
256
|
+
return tls.connect({ host, port, rejectUnauthorized: !this.tlsFallback });
|
|
236
257
|
}
|
|
237
258
|
/** Handle incoming TCP data: buffer + decode varint32 frames */
|
|
238
259
|
handleData(chunk, onConnected) {
|
|
@@ -331,8 +352,8 @@ class PushClient {
|
|
|
331
352
|
for (const [subject, symbols] of this.subscriptions) {
|
|
332
353
|
this.doSubscribe(subject, Array.from(symbols));
|
|
333
354
|
}
|
|
334
|
-
for (const subject of this.accountSubs) {
|
|
335
|
-
this.doSubscribe(subject, undefined,
|
|
355
|
+
for (const [subject, account] of this.accountSubs) {
|
|
356
|
+
this.doSubscribe(subject, undefined, account);
|
|
336
357
|
}
|
|
337
358
|
}
|
|
338
359
|
/** Dispatch push data to the appropriate callback based on dataType */
|
|
@@ -566,8 +587,9 @@ class PushClient {
|
|
|
566
587
|
// ===== Account push subscribe/unsubscribe =====
|
|
567
588
|
/** Subscribe to asset changes */
|
|
568
589
|
subscribeAsset(account) {
|
|
569
|
-
|
|
570
|
-
this.
|
|
590
|
+
const acct = account || this.config.account;
|
|
591
|
+
this.doSubscribe(push_message_1.SubjectType.Asset, undefined, acct);
|
|
592
|
+
this.accountSubs.set(push_message_1.SubjectType.Asset, acct);
|
|
571
593
|
}
|
|
572
594
|
/** Unsubscribe from asset changes */
|
|
573
595
|
unsubscribeAsset() {
|
|
@@ -576,8 +598,9 @@ class PushClient {
|
|
|
576
598
|
}
|
|
577
599
|
/** Subscribe to position changes */
|
|
578
600
|
subscribePosition(account) {
|
|
579
|
-
|
|
580
|
-
this.
|
|
601
|
+
const acct = account || this.config.account;
|
|
602
|
+
this.doSubscribe(push_message_1.SubjectType.Position, undefined, acct);
|
|
603
|
+
this.accountSubs.set(push_message_1.SubjectType.Position, acct);
|
|
581
604
|
}
|
|
582
605
|
/** Unsubscribe from position changes */
|
|
583
606
|
unsubscribePosition() {
|
|
@@ -586,8 +609,9 @@ class PushClient {
|
|
|
586
609
|
}
|
|
587
610
|
/** Subscribe to order status */
|
|
588
611
|
subscribeOrder(account) {
|
|
589
|
-
|
|
590
|
-
this.
|
|
612
|
+
const acct = account || this.config.account;
|
|
613
|
+
this.doSubscribe(push_message_1.SubjectType.Order, undefined, acct);
|
|
614
|
+
this.accountSubs.set(push_message_1.SubjectType.Order, acct);
|
|
591
615
|
}
|
|
592
616
|
/** Unsubscribe from order status */
|
|
593
617
|
unsubscribeOrder() {
|
|
@@ -596,8 +620,9 @@ class PushClient {
|
|
|
596
620
|
}
|
|
597
621
|
/** Subscribe to transaction details */
|
|
598
622
|
subscribeTransaction(account) {
|
|
599
|
-
|
|
600
|
-
this.
|
|
623
|
+
const acct = account || this.config.account;
|
|
624
|
+
this.doSubscribe(push_message_1.SubjectType.Transaction, undefined, acct);
|
|
625
|
+
this.accountSubs.set(push_message_1.SubjectType.Transaction, acct);
|
|
601
626
|
}
|
|
602
627
|
/** Unsubscribe from transaction details */
|
|
603
628
|
unsubscribeTransaction() {
|
|
@@ -613,9 +638,9 @@ class PushClient {
|
|
|
613
638
|
}
|
|
614
639
|
return result;
|
|
615
640
|
}
|
|
616
|
-
/** Get account-level subscriptions */
|
|
641
|
+
/** Get account-level subscriptions: subject -> account */
|
|
617
642
|
getAccountSubscriptions() {
|
|
618
|
-
return
|
|
643
|
+
return new Map(this.accountSubs);
|
|
619
644
|
}
|
|
620
645
|
}
|
|
621
646
|
exports.PushClient = PushClient;
|
|
@@ -361,9 +361,21 @@ class QuoteClient {
|
|
|
361
361
|
}
|
|
362
362
|
return acc;
|
|
363
363
|
}
|
|
364
|
-
/** Futures tick trades. wire: future_tick (API v3.0)
|
|
364
|
+
/** Futures tick trades. wire: future_tick (API v3.0)
|
|
365
|
+
* Server returns {contractCode, items:[...]}; unwrap items and backfill contractCode.
|
|
366
|
+
* endIndex defaults to 30 when not set (consistent with Python/Go SDK). */
|
|
365
367
|
async getFutureTradeTicks(req) {
|
|
366
|
-
|
|
368
|
+
if (!req.endIndex)
|
|
369
|
+
req.endIndex = 30;
|
|
370
|
+
const wrap = await this.callInto('future_tick', req, '3.0');
|
|
371
|
+
const items = wrap?.items ?? [];
|
|
372
|
+
if (wrap?.contractCode) {
|
|
373
|
+
for (const item of items) {
|
|
374
|
+
if (!item.contractCode)
|
|
375
|
+
item.contractCode = wrap.contractCode;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return items;
|
|
367
379
|
}
|
|
368
380
|
/** Futures depth. wire: future_depth */
|
|
369
381
|
async getFutureDepth(req) {
|