@dshtrading/connector-tencent 0.6.3 → 0.7.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 +4 -3
- package/lib/rest.d.ts +1 -1
- package/lib/rest.js +77 -12
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @dshtrading/connector-tencent
|
|
2
2
|
|
|
3
|
-
> **状态:已实证(本出口可用)**。报价(`qt.gtimg.cn`)、日/周/月 K(`
|
|
4
|
-
> `fqkline`/`hkfqkline
|
|
3
|
+
> **状态:已实证(本出口可用)**。报价(`qt.gtimg.cn`)、日/周/月 K(`ifzq.gtimg.cn`
|
|
4
|
+
> `fqkline`/`hkfqkline`;`web.ifzq` 同族域名已被上游 WAF 封死,只作回退垫底)、分钟 K
|
|
5
|
+
> (`ifzq.gtimg.cn` `mkline`)与批量报价均从本开发出口返回真实数据。
|
|
5
6
|
> 证据 `spikes/impl-cn-hk/REPORT.md`、`spikes/impl-tencent-minute/EVIDENCE.md`、
|
|
6
7
|
> `spikes/impl-tencent-quote-batch/EVIDENCE.md`、`spikes/impl-cn-datasource-bench/EVIDENCE.md`。
|
|
7
8
|
|
|
@@ -31,7 +32,7 @@ dsh-trading **cn+hk 双市场切片**市场连接器:经腾讯公共行情端
|
|
|
31
32
|
| 用途 | 端点 |
|
|
32
33
|
|---|---|
|
|
33
34
|
| 报价(单只与批量同端点) | `qt.gtimg.cn/q=<wire[,wire…]>`(`q=` 原生支持逗号列表) |
|
|
34
|
-
| 日/周/月 K(前复权) | `
|
|
35
|
+
| 日/周/月 K(前复权) | `ifzq.gtimg.cn/appstock/app/{fqkline,hkfqkline}/get`(回退链 `ifzq` → `web.ifzq`;后者自 2026-10-23 起被上游 WAF 封死,见 Agent Note `2026-10-23-tencent-kline-host-fallback-and-eastmoney-network-retry`) |
|
|
35
36
|
| 分钟 K(**仅 cn**) | `ifzq.gtimg.cn/appstock/app/kline/mkline`(host **无 `www.`**,带则 301) |
|
|
36
37
|
| 名册检索 | `smartbox.gtimg.cn/s3/?t=all&q=` |
|
|
37
38
|
|
package/lib/rest.d.ts
CHANGED
|
@@ -128,7 +128,7 @@ declare function isoWithOffset(epochMs: number, timeZone: string): string;
|
|
|
128
128
|
interface TencentRestOptions {
|
|
129
129
|
/** 覆盖报价 base(测试/反代用),末尾不带斜杠。 */
|
|
130
130
|
readonly quoteBaseUrl?: string;
|
|
131
|
-
/** 覆盖 K 线 base
|
|
131
|
+
/** 覆盖 K 线 base(测试/反代用),末尾不带斜杠;注入即取代整条回退链,不掺默认域名。 */
|
|
132
132
|
readonly klineBaseUrl?: string;
|
|
133
133
|
/** 覆盖分钟 K 线 base(测试/反代用),末尾不带斜杠。 */
|
|
134
134
|
readonly mklineBaseUrl?: string;
|
package/lib/rest.js
CHANGED
|
@@ -437,14 +437,21 @@ function parseQuoteRows(text) {
|
|
|
437
437
|
return rows;
|
|
438
438
|
}
|
|
439
439
|
const DEFAULT_QUOTE_BASE_URL = "https://qt.gtimg.cn";
|
|
440
|
-
|
|
440
|
+
/**
|
|
441
|
+
* 日/周/月 K 的域名回退链(有序、最多两级):2026-10-23 实测 `web.ifzq.gtimg.cn` 被上游
|
|
442
|
+
* WAF 封死(HTTP 501 + `waf.tencent.com/501page.html` 挑战页;绕开本机代理直连
|
|
443
|
+
* 183.47.126.125 仍 501,换 UA/Referer/标的/周期/HTTP 版本全 501),同族 `ifzq.gtimg.cn`
|
|
444
|
+
* 同参数返回 200 且 payload 结构一致(`qfqday`/`qfqweek`)——故同族域名优先、被封域名垫后,
|
|
445
|
+
* 串行各打一次,绝不无限重试。分钟线走 mkline,本就落在同族域名上,不参与回退。
|
|
446
|
+
*/
|
|
447
|
+
const DEFAULT_KLINE_BASE_URLS = ["https://ifzq.gtimg.cn", "https://web.ifzq.gtimg.cn"];
|
|
441
448
|
const DEFAULT_MKLINE_BASE_URL = "https://ifzq.gtimg.cn";
|
|
442
449
|
const DEFAULT_SEARCH_BASE_URL = "https://smartbox.gtimg.cn";
|
|
443
450
|
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
444
451
|
var TencentRestClient = class {
|
|
445
452
|
#market;
|
|
446
453
|
#quoteBaseUrl;
|
|
447
|
-
#
|
|
454
|
+
#klineBaseUrls;
|
|
448
455
|
#mklineBaseUrl;
|
|
449
456
|
#searchBaseUrl;
|
|
450
457
|
#timeoutMs;
|
|
@@ -453,7 +460,7 @@ var TencentRestClient = class {
|
|
|
453
460
|
constructor(market, options = {}) {
|
|
454
461
|
this.#market = market;
|
|
455
462
|
this.#quoteBaseUrl = options.quoteBaseUrl ?? DEFAULT_QUOTE_BASE_URL;
|
|
456
|
-
this.#
|
|
463
|
+
this.#klineBaseUrls = options.klineBaseUrl === void 0 ? DEFAULT_KLINE_BASE_URLS : [options.klineBaseUrl];
|
|
457
464
|
this.#mklineBaseUrl = options.mklineBaseUrl ?? DEFAULT_MKLINE_BASE_URL;
|
|
458
465
|
this.#searchBaseUrl = options.searchBaseUrl ?? DEFAULT_SEARCH_BASE_URL;
|
|
459
466
|
this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
@@ -463,22 +470,69 @@ var TencentRestClient = class {
|
|
|
463
470
|
get market() {
|
|
464
471
|
return this.#market;
|
|
465
472
|
}
|
|
466
|
-
|
|
473
|
+
/** 单请求带超时闸门;网络级失败/超时统一抛 TRADING_NETWORK。 */
|
|
474
|
+
async #fetchWithTimeout(url) {
|
|
467
475
|
const controller = new AbortController();
|
|
468
476
|
const timer = setTimeout(() => controller.abort(new DOMException(`request timed out after ${this.#timeoutMs}ms`, "TimeoutError")), this.#timeoutMs);
|
|
469
|
-
let res;
|
|
470
477
|
try {
|
|
471
|
-
|
|
478
|
+
return await this.#fetchImpl(url, { signal: controller.signal });
|
|
472
479
|
} catch (cause) {
|
|
473
480
|
const timedOut = controller.signal.aborted;
|
|
474
481
|
throw new TradingServiceError("TRADING_NETWORK", timedOut ? `Tencent ${url}: request timed out after ${this.#timeoutMs}ms` : `Tencent ${url}: network error`, cause);
|
|
475
482
|
} finally {
|
|
476
483
|
clearTimeout(timer);
|
|
477
484
|
}
|
|
485
|
+
}
|
|
486
|
+
async #requestArrayBuffer(url) {
|
|
487
|
+
const res = await this.#fetchWithTimeout(url);
|
|
478
488
|
if (!res.ok) throw new TradingServiceError(res.status === 429 ? "TRADING_RATE_LIMITED" : "TRADING_EXCHANGE_ERROR", `Tencent ${url}: HTTP ${res.status} ${res.statusText}`);
|
|
479
489
|
return new Uint8Array(await res.arrayBuffer());
|
|
480
490
|
}
|
|
481
491
|
/**
|
|
492
|
+
* 单域名取一份 K 线 JSON,把「能不能换下一个域名」的判断就地做掉:
|
|
493
|
+
* 网络级失败(含超时)、HTTP 5xx、非 JSON(WAF 挑战页/HTML 错误页)可换;429/4xx 与
|
|
494
|
+
* 其余业务语义(code!=0、无行、坏行)不重试、由调用方原样 fail-closed。
|
|
495
|
+
*/
|
|
496
|
+
async #requestKlinePayload(url, sym) {
|
|
497
|
+
let res;
|
|
498
|
+
try {
|
|
499
|
+
res = await this.#fetchWithTimeout(url);
|
|
500
|
+
} catch (cause) {
|
|
501
|
+
return {
|
|
502
|
+
ok: false,
|
|
503
|
+
retryable: true,
|
|
504
|
+
error: cause instanceof TradingServiceError ? cause : new TradingServiceError("TRADING_NETWORK", `Tencent ${url}: network error`, cause)
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
if (!res.ok) return {
|
|
508
|
+
ok: false,
|
|
509
|
+
retryable: res.status >= 500,
|
|
510
|
+
error: new TradingServiceError(res.status === 429 ? "TRADING_RATE_LIMITED" : "TRADING_EXCHANGE_ERROR", `Tencent ${url}: HTTP ${res.status} ${res.statusText}`)
|
|
511
|
+
};
|
|
512
|
+
let bytes;
|
|
513
|
+
try {
|
|
514
|
+
bytes = new Uint8Array(await res.arrayBuffer());
|
|
515
|
+
} catch (cause) {
|
|
516
|
+
return {
|
|
517
|
+
ok: false,
|
|
518
|
+
retryable: true,
|
|
519
|
+
error: new TradingServiceError("TRADING_NETWORK", `Tencent ${url}: network error`, cause)
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
return {
|
|
524
|
+
ok: true,
|
|
525
|
+
payload: JSON.parse(new TextDecoder("utf-8").decode(bytes))
|
|
526
|
+
};
|
|
527
|
+
} catch (cause) {
|
|
528
|
+
return {
|
|
529
|
+
ok: false,
|
|
530
|
+
retryable: true,
|
|
531
|
+
error: new TradingServiceError("TRADING_EXCHANGE_ERROR", `Tencent klines for ${sym}: non-JSON payload`, cause)
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
482
536
|
* 报价 wire 形态:cn=sh600519;hk=r_hk00700(hk K 线另用 hk 前缀,见文件头注)。
|
|
483
537
|
* **大小写原样进 URL**:hk 指数 wire 大小写敏感(`r_hkHSI` 有行、`r_hkhsi` 返回
|
|
484
538
|
* `v_pv_none_match="1";`,2026-10-14 实测),只有响应查表键才小写归一。
|
|
@@ -644,13 +698,24 @@ var TencentRestClient = class {
|
|
|
644
698
|
const count = typeof limit === "number" && Number.isInteger(limit) && limit > 0 ? Math.min(limit, 800) : 100;
|
|
645
699
|
const wire = this.#klineWireCode(sym);
|
|
646
700
|
const isMinute = mapping.type === "mkline";
|
|
647
|
-
const
|
|
648
|
-
const
|
|
701
|
+
const klinePath = this.#market === "hk" ? "appstock/app/hkfqkline/get" : "appstock/app/fqkline/get";
|
|
702
|
+
const urlFor = (base) => isMinute ? `${base}/appstock/app/kline/mkline?param=${wire},${mapping.tf},,${count}` : `${base}/${klinePath}?param=${wire},${mapping.tf},,,${count},qfq`;
|
|
703
|
+
const bases = isMinute ? [this.#mklineBaseUrl] : this.#klineBaseUrls;
|
|
649
704
|
let payload;
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
705
|
+
let lastError;
|
|
706
|
+
for (const base of bases) {
|
|
707
|
+
const attempt = await this.#requestKlinePayload(urlFor(base), sym);
|
|
708
|
+
if (attempt.ok) {
|
|
709
|
+
payload = attempt.payload;
|
|
710
|
+
break;
|
|
711
|
+
}
|
|
712
|
+
lastError = attempt.error;
|
|
713
|
+
if (!attempt.retryable) throw attempt.error;
|
|
714
|
+
}
|
|
715
|
+
if (payload === void 0) {
|
|
716
|
+
const error = lastError ?? new TradingServiceError("TRADING_NETWORK", `Tencent klines for ${sym}: no kline host configured`);
|
|
717
|
+
if (bases.length === 1) throw error;
|
|
718
|
+
throw new TradingServiceError(error.code, `Tencent klines for ${sym}: all kline hosts failed (${bases.join(" -> ")}); last: ${error.message}`, error);
|
|
654
719
|
}
|
|
655
720
|
if (payload.code !== 0) throw new TradingServiceError("TRADING_EXCHANGE_ERROR", `Tencent klines for ${sym}: upstream code=${String(payload.code)} msg=${JSON.stringify(payload.msg ?? "")}`);
|
|
656
721
|
const market = payload.data?.[wire];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dshtrading/connector-tencent",
|
|
3
3
|
"description": "CN + HK dual-market connector plugin for dsh-trading: public market data from Tencent quote/kline endpoints implementing the @dshtrading/api contracts (single package, per-instance market split; no key, no official authorization — personal use boundary at your own discretion)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "./lib/index.d.ts",
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
],
|
|
22
22
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@dshtrading/authority": "^0.
|
|
25
|
-
"@dshtrading/api": "^0.
|
|
24
|
+
"@dshtrading/authority": "^0.7.0",
|
|
25
|
+
"@dshtrading/api": "^0.7.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
|
-
"@deepseek-ai/cordis": ">=4.
|
|
29
|
-
"@deepseek-ai/dsh-tools": ">=0.2.
|
|
30
|
-
"@deepseek-ai/schemastery": ">=3.
|
|
28
|
+
"@deepseek-ai/cordis": ">=4.1.0",
|
|
29
|
+
"@deepseek-ai/dsh-tools": ">=0.2.2-rc.1",
|
|
30
|
+
"@deepseek-ai/schemastery": ">=3.19.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"tsdown": "^0.22.0",
|