@v587d/capital-generation 2.8.3 → 2.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.
Files changed (164) hide show
  1. package/CHANGELOG.md +54 -1
  2. package/README.md +40 -19
  3. package/chart-ui/README.md +1 -1
  4. package/chart-ui/client.src.cjs +7 -16
  5. package/chart-ui/index.js +12 -33
  6. package/chart-ui/package.json +2 -2
  7. package/config/client.js +18 -39
  8. package/config/client.src.cjs +79 -217
  9. package/config/index.js +22 -72
  10. package/config/package.json +2 -2
  11. package/cordis.patch.yml +46 -51
  12. package/lib/agents/bash-guard.d.ts +9 -55
  13. package/lib/agents/bash-guard.js +27 -67
  14. package/lib/agents/preset-roster.d.ts +22 -0
  15. package/lib/agents/preset-roster.js +87 -0
  16. package/lib/agents/role-tool-policy.d.ts +46 -0
  17. package/lib/agents/role-tool-policy.js +205 -0
  18. package/lib/agents/root-tool-policy.d.ts +28 -124
  19. package/lib/agents/root-tool-policy.js +164 -237
  20. package/lib/agents/selected-skills-provider.d.ts +2 -6
  21. package/lib/agents/selected-skills-provider.js +27 -84
  22. package/lib/capital-settings.d.ts +27 -0
  23. package/lib/capital-settings.js +23 -0
  24. package/lib/chart/artifact-ref.d.ts +3 -8
  25. package/lib/chart/artifact-ref.js +3 -8
  26. package/lib/chart/downsample.d.ts +2 -5
  27. package/lib/chart/downsample.js +3 -8
  28. package/lib/chart/errors.d.ts +3 -7
  29. package/lib/chart/errors.js +1 -2
  30. package/lib/chart/event-context.d.ts +8 -0
  31. package/lib/chart/event-context.js +39 -0
  32. package/lib/chart/events.d.ts +21 -78
  33. package/lib/chart/events.js +40 -189
  34. package/lib/chart/html.d.ts +2 -5
  35. package/lib/chart/html.js +8 -15
  36. package/lib/chart/runtime.d.ts +5 -18
  37. package/lib/chart/runtime.js +14 -34
  38. package/lib/chart/series.d.ts +10 -17
  39. package/lib/chart/series.js +22 -47
  40. package/lib/chart/source-token.d.ts +5 -13
  41. package/lib/chart/source-token.js +18 -17
  42. package/lib/chart/spec.d.ts +6 -12
  43. package/lib/chart/spec.js +7 -31
  44. package/lib/chart/tool.d.ts +6 -29
  45. package/lib/chart/tool.js +9 -23
  46. package/lib/chart-render-entry.d.ts +9 -0
  47. package/lib/chart-render-entry.js +17 -0
  48. package/lib/chart-source-entry.d.ts +13 -0
  49. package/lib/chart-source-entry.js +20 -0
  50. package/lib/collect-entry.d.ts +9 -0
  51. package/lib/collect-entry.js +10 -0
  52. package/lib/collector-runtime.d.ts +22 -0
  53. package/lib/collector-runtime.js +97 -0
  54. package/lib/data-collector/args.d.ts +6 -24
  55. package/lib/data-collector/args.js +6 -24
  56. package/lib/data-collector/dataset-tools.d.ts +7 -1
  57. package/lib/data-collector/dataset-tools.js +27 -56
  58. package/lib/data-collector/describe.d.ts +8 -33
  59. package/lib/data-collector/describe.js +17 -70
  60. package/lib/data-collector/hub.d.ts +27 -85
  61. package/lib/data-collector/hub.js +19 -85
  62. package/lib/data-collector/query-time.d.ts +7 -26
  63. package/lib/data-collector/query-time.js +8 -27
  64. package/lib/data-collector/store.d.ts +40 -117
  65. package/lib/data-collector/store.js +32 -111
  66. package/lib/data-collector/time-axis.d.ts +16 -76
  67. package/lib/data-collector/time-axis.js +25 -111
  68. package/lib/data-collector/tools.d.ts +6 -6
  69. package/lib/data-collector/tools.js +34 -43
  70. package/lib/dataset-read-entry.d.ts +9 -0
  71. package/lib/dataset-read-entry.js +7 -0
  72. package/lib/index.d.ts +7 -117
  73. package/lib/index.js +43 -422
  74. package/lib/net/eastmoney-client.d.ts +13 -65
  75. package/lib/net/eastmoney-client.js +12 -53
  76. package/lib/net/throttle.d.ts +2 -13
  77. package/lib/net/throttle.js +3 -14
  78. package/lib/ocr/client.d.ts +12 -21
  79. package/lib/ocr/client.js +23 -40
  80. package/lib/ocr/markdown.d.ts +20 -26
  81. package/lib/ocr/markdown.js +23 -36
  82. package/lib/ocr/store.d.ts +14 -47
  83. package/lib/ocr/store.js +15 -45
  84. package/lib/ocr-entry.d.ts +9 -0
  85. package/lib/ocr-entry.js +10 -0
  86. package/lib/policy-entry.d.ts +17 -0
  87. package/lib/policy-entry.js +22 -0
  88. package/lib/preset-claim.d.ts +22 -0
  89. package/lib/preset-claim.js +24 -0
  90. package/lib/preset-ids.d.ts +38 -0
  91. package/lib/preset-ids.js +46 -0
  92. package/lib/retriever-config.d.ts +37 -0
  93. package/lib/retriever-config.js +36 -0
  94. package/lib/retriever-runtime.d.ts +31 -0
  95. package/lib/retriever-runtime.js +82 -0
  96. package/lib/root-extras-entry.d.ts +17 -0
  97. package/lib/root-extras-entry.js +25 -0
  98. package/lib/runtime-entry.d.ts +15 -0
  99. package/lib/runtime-entry.js +5 -0
  100. package/lib/runtime.d.ts +21 -0
  101. package/lib/runtime.js +33 -0
  102. package/lib/search-entry.d.ts +9 -0
  103. package/lib/search-entry.js +9 -0
  104. package/lib/sources/eastmoney-http.js +20 -68
  105. package/lib/sources/ftshare-rest.d.ts +6 -34
  106. package/lib/sources/ftshare-rest.js +84 -292
  107. package/lib/sources/fuyao-core.d.ts +8 -18
  108. package/lib/sources/fuyao-core.js +10 -21
  109. package/lib/sources/fuyao-rest.d.ts +2 -6
  110. package/lib/sources/fuyao-rest.js +23 -102
  111. package/lib/sources/security-code.d.ts +6 -12
  112. package/lib/sources/security-code.js +6 -12
  113. package/lib/sources/security-identity.d.ts +1 -4
  114. package/lib/sources/security-identity.js +3 -8
  115. package/lib/sources/tencent-http.js +14 -28
  116. package/lib/sources/tencent-public-core.d.ts +8 -60
  117. package/lib/sources/tencent-public-core.js +12 -77
  118. package/lib/sources/wind-mcp.d.ts +0 -5
  119. package/lib/sources/wind-mcp.js +23 -78
  120. package/lib/sources-entry.d.ts +9 -0
  121. package/lib/sources-entry.js +9 -0
  122. package/lib/time/tools.d.ts +8 -31
  123. package/lib/time/tools.js +30 -66
  124. package/lib/time-entry.d.ts +9 -0
  125. package/lib/time-entry.js +7 -0
  126. package/lib/tool-exec.d.ts +45 -11
  127. package/lib/tool-exec.js +59 -6
  128. package/lib/user-customization.d.ts +14 -0
  129. package/lib/user-customization.js +47 -0
  130. package/lib/watchlist/tools.d.ts +3 -13
  131. package/lib/watchlist/tools.js +10 -30
  132. package/lib/watchlist-entry.d.ts +9 -0
  133. package/lib/watchlist-entry.js +6 -0
  134. package/lib/web-retriever/engines.d.ts +6 -11
  135. package/lib/web-retriever/engines.js +9 -14
  136. package/lib/web-retriever/html-markdown.d.ts +14 -34
  137. package/lib/web-retriever/html-markdown.js +39 -75
  138. package/lib/web-retriever/local-fetch.d.ts +22 -58
  139. package/lib/web-retriever/local-fetch.js +33 -77
  140. package/lib/web-retriever/retriever.d.ts +6 -18
  141. package/lib/web-retriever/retriever.js +7 -7
  142. package/lib/web-retriever/sources.d.ts +37 -124
  143. package/lib/web-retriever/sources.js +64 -196
  144. package/lib/web-retriever/tools.d.ts +43 -27
  145. package/lib/web-retriever/tools.js +99 -137
  146. package/lib/web-retriever/wind-client.d.ts +14 -31
  147. package/lib/web-retriever/wind-client.js +16 -32
  148. package/locale/zh.json +1 -1
  149. package/package.json +67 -7
  150. package/preset/capital-generation/README.md +87 -53
  151. package/preset/capital-generation/agent.patch.yml +526 -337
  152. package/preset/capital-generation/skills/capital-data-profile/SKILL.md +2 -2
  153. package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +1 -1
  154. package/preset/capital-generation/skills/capital-equity-research/SKILL.md +1 -1
  155. package/preset/capital-generation/skills/capital-event-impact/SKILL.md +1 -1
  156. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +15 -19
  157. package/preset/capital-generation/skills/capital-portfolio-review/SKILL.md +1 -1
  158. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +9 -12
  159. package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +1 -1
  160. package/watchlist/client.js +15 -148
  161. package/watchlist/client.src.cjs +148 -662
  162. package/watchlist/holding-rules.js +11 -38
  163. package/watchlist/index.js +127 -397
  164. package/watchlist/package.json +2 -2
@@ -6,22 +6,16 @@ export interface SecurityCode {
6
6
  tencentSymbol: string;
7
7
  isIndex: boolean;
8
8
  }
9
- /**
10
- * Parse an A-share/ETF/index code without dropping exchange information.
11
- * Bare 000xxx codes are rejected because they are ambiguous (e.g. 000001).
12
- */
9
+ /** Parse an A-share/ETF/index code; bare 000xxx is rejected as ambiguous (000001). */
13
10
  export declare function parseSecurityCode(value: unknown, name?: string): SecurityCode;
14
11
  /**
15
- * 检索面的代码口径:只回答"把这串写法归一成 6 位数字"。
16
- *
17
- * 与 `parseSecurityCode` 的分工是刻意的:数据面必须消歧(裸 `000001` 在指数与个股之间
18
- * 有歧义),而**具名来源自己会回答"这台机器上有没有这只票"**——cninfo 只覆盖深市、
19
- * sseinfo 只覆盖沪市,越界返回 0 条并带 note 是真事实,不能在这一层把它拒掉。
20
- * 所以裸 `000001`(平安银行)在这里合法,`SH600519` / `600519.SH` 也合法(东财研报库
21
- * 实测只认纯数字,带前缀返回 0 条,因此归一化属于工具职责而不是模型的记性)。
12
+ * Search-side code normalization: reduce a spelling to 6 digits.
13
+ * Unlike `parseSecurityCode`, bare `000001` is legal: named sources cover one exchange
14
+ * (cninfo SZ-only, sseinfo SH-only) and must be allowed to answer "no rows" themselves.
15
+ * Eastmoney research reports accept digits only; prefixed input returns 0 rows.
22
16
  */
23
17
  export declare function bareAshareDigits(value: unknown, name?: string): string;
24
- /** Normalize a list while preserving first-seen order and rejecting duplicates after normalization. */
18
+ /** Dedupe after normalization, preserving first-seen order. */
25
19
  export declare function normalizeSecurityCodes(value: unknown, name?: string, maximum?: number): SecurityCode[];
26
20
  export declare function requireTencentSecurity(code: SecurityCode, name?: string): SecurityCode;
27
21
  export declare function isLikelyIndex(code: SecurityCode): boolean;
@@ -17,10 +17,7 @@ function explicitMarket(prefix, suffix) {
17
17
  function isMarket(value) {
18
18
  return value === 'SH' || value === 'SZ' || value === 'BJ';
19
19
  }
20
- /**
21
- * Parse an A-share/ETF/index code without dropping exchange information.
22
- * Bare 000xxx codes are rejected because they are ambiguous (e.g. 000001).
23
- */
20
+ /** Parse an A-share/ETF/index code; bare 000xxx is rejected as ambiguous (000001). */
24
21
  export function parseSecurityCode(value, name = 'code') {
25
22
  if (typeof value !== 'string' || value.trim().length === 0)
26
23
  throw new Error(`parameter ${name} must be a security code`);
@@ -54,13 +51,10 @@ export function parseSecurityCode(value, name = 'code') {
54
51
  };
55
52
  }
56
53
  /**
57
- * 检索面的代码口径:只回答"把这串写法归一成 6 位数字"。
58
- *
59
- * 与 `parseSecurityCode` 的分工是刻意的:数据面必须消歧(裸 `000001` 在指数与个股之间
60
- * 有歧义),而**具名来源自己会回答"这台机器上有没有这只票"**——cninfo 只覆盖深市、
61
- * sseinfo 只覆盖沪市,越界返回 0 条并带 note 是真事实,不能在这一层把它拒掉。
62
- * 所以裸 `000001`(平安银行)在这里合法,`SH600519` / `600519.SH` 也合法(东财研报库
63
- * 实测只认纯数字,带前缀返回 0 条,因此归一化属于工具职责而不是模型的记性)。
54
+ * Search-side code normalization: reduce a spelling to 6 digits.
55
+ * Unlike `parseSecurityCode`, bare `000001` is legal: named sources cover one exchange
56
+ * (cninfo SZ-only, sseinfo SH-only) and must be allowed to answer "no rows" themselves.
57
+ * Eastmoney research reports accept digits only; prefixed input returns 0 rows.
64
58
  */
65
59
  export function bareAshareDigits(value, name = 'code') {
66
60
  if (typeof value !== 'string' || value.trim().length === 0) {
@@ -72,7 +66,7 @@ export function bareAshareDigits(value, name = 'code') {
72
66
  }
73
67
  return match[2] ?? match[3];
74
68
  }
75
- /** Normalize a list while preserving first-seen order and rejecting duplicates after normalization. */
69
+ /** Dedupe after normalization, preserving first-seen order. */
76
70
  export function normalizeSecurityCodes(value, name = 'codes', maximum = 50) {
77
71
  const rawValues = Array.isArray(value)
78
72
  ? value
@@ -5,10 +5,7 @@ export interface EastmoneySecurityIdentity {
5
5
  eastmoney_code: string;
6
6
  eastmoney_market: EastmoneyMarket;
7
7
  }
8
- /**
9
- * Resolve Eastmoney's provider identifiers without guessing a market from digits.
10
- * `market` is mandatory for a bare SECURITY_CODE; `secid` carries its own market.
11
- */
8
+ /** Never guess a market from digits: `market` is mandatory for bare SECURITY_CODE; secid carries its own. */
12
9
  export declare function normalizeEastmoneySecurityIdentity(input: {
13
10
  secucode?: unknown;
14
11
  security_code?: unknown;
@@ -1,9 +1,7 @@
1
1
  import { parseSecurityCode } from './security-code.js';
2
2
  /**
3
- * secid 市场码(实测于 2026-09-24,`push2.eastmoney.com/api/qt/stock/get`):
4
- * `1.` = 沪;`0.` = **深市与北交所共用**(`0.920982`、`0.430047` 返回 rc=0,`2.*` 一律 rc=100)。
5
- * 因此 `0.` 的 BJ/SZ 由数字前缀区分(4/8/92 为北交所),与 `normalizeEastmoneyAshareIdentity`
6
- * 共用 `marketFromAshareDigits` 的同一份规则(§9.7:一条知识一个实现)。
3
+ * Eastmoney secid market prefix: `1.` = SH; `0.` = SZ and BJ combined (`2.*` returns rc=100).
4
+ * For `0.` the BJ/SZ split comes from digit prefixes, sharing `marketFromAshareDigits`.
7
5
  */
8
6
  const SECID_MARKET_BY_PREFIX = { '1': 'SH', '0': 'SZ_OR_BJ' };
9
7
  function marketFromAshareDigits(digits) {
@@ -38,10 +36,7 @@ function code(value, name) {
38
36
  return raw;
39
37
  throw new Error(`${name} must be a six-digit code, SECUCODE, or market-prefixed code`);
40
38
  }
41
- /**
42
- * Resolve Eastmoney's provider identifiers without guessing a market from digits.
43
- * `market` is mandatory for a bare SECURITY_CODE; `secid` carries its own market.
44
- */
39
+ /** Never guess a market from digits: `market` is mandatory for bare SECURITY_CODE; secid carries its own. */
45
40
  export function normalizeEastmoneySecurityIdentity(input) {
46
41
  const explicit = input.secucode ?? input.security_code;
47
42
  if (explicit !== undefined) {
@@ -1,8 +1,8 @@
1
1
  import { buildDataKey } from '../data-collector/hub.js';
2
2
  import { getDataTimeContract } from '../time/tools.js';
3
3
  import { isLikelyIndex, normalizeSecurityCodes, parseSecurityCode, requireTencentSecurity } from './security-code.js';
4
- // 港美股快照、smartbox 与那条**唯一的**出口节流链都在内核里;本文件只留能力定义与 K 线 / 分笔编排。
5
- // 为什么必须共用:两条独立节流等于没有限流,两份字段位序表迟早只会修好一份(AGENTS.md §9.7)。
4
+ // Shared core holds HK/US snapshots, smartbox and the one throttle chain; this file only defines
5
+ // capabilities and K-line/tick orchestration. Two throttles = no throttle.
6
6
  import { QUOTE_URL, fetchOffshoreIndexQuotes, fetchOffshoreQuotes, isRecord, normalizeHkCode, normalizeOffshoreIndexCode, normalizeUsCode, numberOrNull, requiredNumber, resolveUsKlineSymbol, sourceError, tencentGetText as getText, } from './tencent-public-core.js';
7
7
  const TICK_URL = 'https://stock.gtimg.cn/data/index.php';
8
8
  const KLINE_HOSTS = [
@@ -262,7 +262,8 @@ async function callKlineHosts(transport, signal) {
262
262
  hostDownUntil.set(host, Date.now() + HOST_COOLDOWN_MS);
263
263
  }
264
264
  catch (error) {
265
- // 取消与参数性失败都原样抛出,**先于**一切拉黑/换机逻辑(docs/dev/web-retriever.md §4.2「取消原样抛出」)。
265
+ // Cancellation and param failures rethrow before blacklist/rotation; see
266
+ // `docs/dev/web-retriever.md` §4.2.
266
267
  if (error instanceof Error && error.name === 'AbortError')
267
268
  throw error;
268
269
  if (error.code === 'tencent_invalid_param')
@@ -463,13 +464,6 @@ async function executeTicks(params, signal) {
463
464
  schema: ticksOutput,
464
465
  };
465
466
  }
466
- /**
467
- * 港美股快照与 A 股快照**不能共用一份字段表**:实测腾讯在同一个 `~` 分隔串里换了列位——
468
- * 币种 A 股根本没有、港股在第 75 位、美股在第 35 位;换手率港股在第 59 位(分母是**总股本**)、
469
- * 美股在第 38 位(分母是**流通股本**);成交额在港股**指数行**是万元级而个股行是元级。
470
- * 所以两张表各自映射(都在 `tencent-public-core.ts`,host 半边与这里共用),
471
- * 并且只收能用算术或量级证明的列(§10.2:口径猜错就是话说错)。
472
- */
473
467
  function normalizeOffshoreCodes(params, name, perCode) {
474
468
  if (!isRecord(params))
475
469
  throw new Error('params must be an object');
@@ -487,12 +481,10 @@ function normalizeOffshoreCodes(params, name, perCode) {
487
481
  return { codes: symbols };
488
482
  }
489
483
  async function executeOffshoreQuote(params, signal, market) {
490
- // 取数编排(含美股"没回行就剥一段后缀重问")在内核里,与自选股面板共用一份。
491
484
  const rows = await fetchOffshoreQuotes(market, params.codes, signal);
492
485
  return { data: rows, schema: market === 'hk' ? hkQuoteOutput : usQuoteOutput };
493
486
  }
494
487
  async function executeOffshoreIndexQuote(params, signal, market) {
495
- // 指数只收点位那一族可证明的列;写法来自检索,身份形状由内核核对(猜写法会静默拿到别的标的)。
496
488
  const rows = await fetchOffshoreIndexQuotes(market, params.codes, signal);
497
489
  return { data: rows, schema: market === 'hk' ? hkIndexQuoteOutput : usIndexQuoteOutput };
498
490
  }
@@ -500,7 +492,6 @@ const offshoreKlinePaths = {
500
492
  hk: { qfq: '/appstock/app/hkfqkline/get', hfq: '/appstock/app/hkfqkline/get', none: '/appstock/app/kline/kline' },
501
493
  us: { qfq: '/appstock/app/usfqkline/get', none: '/appstock/app/kline/kline' },
502
494
  };
503
- /** 港美股 K 线入口:主机轮换全部走 `callKlineHosts`(一份实现),这里只补"序列在哪个 symbol 的哪个键下"。 */
504
495
  async function callOffshoreKline(path, param, symbol, signal) {
505
496
  const data = await callKlineHosts({
506
497
  path,
@@ -520,14 +511,14 @@ async function callOffshoreKline(path, param, symbol, signal) {
520
511
  throw sourceError(`Tencent K-line ${symbol} response has no series array`, 'tencent_invalid_response');
521
512
  return node[key];
522
513
  }
523
- /** K 线行:`[日期, 开, 收, 高, 低, 量, …]`——与 A 股同一套位序,第 6 位是上游附带的回购/分红文本。 */
514
+ /** Row: `[date, open, close, high, low, volume, …]`; field 6 is buyback/dividend text. */
524
515
  function parseOffshoreKlineRows(items, canonical, tencentSymbol, period, adjust, market) {
525
516
  const rows = [];
526
517
  for (const item of items) {
527
518
  if (!Array.isArray(item) || item.length < 6)
528
519
  throw sourceError(`Tencent ${market} K-line ${tencentSymbol} row shape changed`, 'tencent_invalid_response');
529
520
  const date = String(item[0]);
530
- // 只匹配 `\d{4}-\d{2}-\d{2}` 会把 2026-13-99 这样的假日期放过去,所以要按真历法回读一次。
521
+ // Shape alone accepts 2026-13-99, so re-read through the real calendar.
531
522
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || (() => { const parsed = new Date(`${date}T00:00:00Z`); return Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== date; })()) {
532
523
  throw sourceError(`Tencent ${market} K-line ${tencentSymbol} has unreadable date ${JSON.stringify(date)}`, 'tencent_invalid_response');
533
524
  }
@@ -551,7 +542,7 @@ function parseOffshoreKlineRows(items, canonical, tencentSymbol, period, adjust,
551
542
  volume,
552
543
  };
553
544
  if (market === 'hk') {
554
- // 港股复权序列多回两列(第 7/8 位);实测不复权序列只有七列,这两处就是 null。
545
+ // Adjusted series has two extra columns; unadjusted has seven, so both are null.
555
546
  row.turnover_pct = item.length > 7 ? numberOrNull(item[7]) : null;
556
547
  row.amount_wan_hkd = item.length > 8 ? numberOrNull(item[8]) : null;
557
548
  }
@@ -564,7 +555,7 @@ function parseOffshoreKlineRows(items, canonical, tencentSymbol, period, adjust,
564
555
  function normalizeOffshoreKline(params, market) {
565
556
  if (!isRecord(params))
566
557
  throw new Error('params must be an object');
567
- // start/end 进 allowed 只为了给出**可用**的拒绝理由:默默忽略日期是最坏的一种失败(调用方以为过滤过了)。
558
+ // start/end are accepted only to reject them.
568
559
  assertKnown(params, ['code', 'period', 'adjust', 'count', 'start', 'end']);
569
560
  if (params.start !== undefined || params.end !== undefined)
570
561
  throw new Error('start/end are not supported: 实测 hkfqkline 忽略日期区间(带 2024-01-01..2024-03-01 与 count=640 仍回 640 条、最早到 2021-07-30),只有 count 生效');
@@ -586,7 +577,7 @@ async function executeOffshoreKline(params, signal, market) {
586
577
  const period = String(params.period);
587
578
  const adjust = String(params.adjust);
588
579
  const count = Number(params.count);
589
- // K 线的 symbol 身份:港股就是 hk+5 位;美股必须带交易所后缀,且后缀由快照回答。
580
+ // HK is hk + 5 digits; US suffix must be resolved by the snapshot.
590
581
  const tencentSymbol = market === 'hk' ? `hk${code}` : await resolveUsKlineSymbol(code, signal);
591
582
  const canonical = market === 'hk' ? code : tencentSymbol.slice(2);
592
583
  const path = offshoreKlinePaths[market][adjust];
@@ -654,12 +645,8 @@ const usQuoteOutput = {
654
645
  },
655
646
  };
656
647
  /**
657
- * 港美股**指数**快照的输出表:只有点位那一族,且**刻意不收**任何量纲列与币种列。
658
- *
659
- * 为什么不收(2026-10-07 真报文):港股指数行的第 6 / 36 / 37 位两列相等且量级像万元,
660
- * 美股指数行的第 37 位给了 2.1e14、第 38 / 44 / 45 / 62 / 63 位为空——口径推不出来,
661
- * 声明一列我们没有把握的东西比少给一列更糟(docs/dev/tool-schema.md §10.2)。
662
- * 为什么不收币种:上游确实在第 75 / 35 位塞了 `HKD` / `USD`,但指数是无量纲的数。
648
+ * Index output: point values only. Magnitude columns are unreadable (hkHSI 6/36/37 duplicate at
649
+ * 10k; usIXIC 37 is 2.1e14, 38/44/45/62/63 empty). No currency.
663
650
  */
664
651
  const offshoreIndexQuoteProperties = (symbolLabel) => ({
665
652
  code: { type: 'string' },
@@ -684,16 +671,15 @@ const usIndexQuoteOutput = {
684
671
  type: 'array',
685
672
  items: { type: 'object', properties: offshoreIndexQuoteProperties('美东'), additionalProperties: true },
686
673
  };
687
- /** 港美股 K 线的公共列:`[日期, 开, 收, 高, 低, 量]`,量纲是**股**。 */
674
+ /** Columns `[date, open, close, high, low, volume]`, volume in shares. */
688
675
  const offshoreKlineProperties = () => ({
689
676
  code: { type: 'string' }, tencent_symbol: { type: 'string' }, adjust: { type: 'string' },
690
677
  period: { type: 'string' }, date: { type: 'string' },
691
678
  open: { type: 'number' }, high: { type: 'number' }, low: { type: 'number' }, close: { type: 'number' },
692
679
  volume: { type: 'number', description: '成交量(股);不是 A 股 K 线那套"手"' },
693
680
  });
694
- // 只有**港股复权序列**多回两列,而其中成交额是**万港元**(实测腾讯 2026-10-05 给 366,702.882,
695
- // 当日快照给 3,667,028,820.9 港元,差整整 1e4)。美股的两个可用入口(qfq / none)实测都只回六列,
696
- // 所以美股这张表**没有** amount 字段——不声明一列恒为 null 的东西,那只会让人以为是缺数。
681
+ // HK adjusted series has two extra columns; its amount is in 10k HKD. US qfq/none return six
682
+ // columns, so the US table declares no amount field.
697
683
  const hkKlineOutput = {
698
684
  type: 'array',
699
685
  items: {
@@ -1,10 +1,5 @@
1
1
  export declare const QUOTE_URL = "https://qt.gtimg.cn/q=";
2
2
  export declare const SMARTBOX_URL = "https://smartbox.gtimg.cn/s3/";
3
- /**
4
- * 腾讯出口的**唯一**节流链(进程级)。行情快照、分笔、K 线每一次换机、港美股快照、smartbox
5
- * 检索全部过这一条链——它们打的是同一家,按 IP 风控(429 / 空 data / 静默限流)。
6
- * 间隔取 120ms:与最早那版分笔翻页的节奏同量级。
7
- */
8
3
  export declare const TENCENT_MIN_INTERVAL_MS = 120;
9
4
  export declare const tencentThrottle: <T>(task: () => Promise<T>) => Promise<T>;
10
5
  export type JsonRecord = Record<string, unknown>;
@@ -12,81 +7,35 @@ export declare function isRecord(value: unknown): value is JsonRecord;
12
7
  export declare function numberOrNull(value: unknown): number | null;
13
8
  export declare function requiredNumber(value: unknown, field: string): number;
14
9
  export declare function sourceError(message: string, code?: string): Error;
15
- /**
16
- * 腾讯出口的取文本:取消不排队(队首那个请求挂住时,把这次取消塞进队列等于把它吞掉,
17
- * `docs/dev/web-retriever.md` §4.2「取消原样抛出」),排队与最小间隔由节流链负责。
18
- */
10
+ /** Cancellation does not queue; see `docs/dev/web-retriever.md` §4.2. */
19
11
  export declare function tencentGetText(url: string, signal: AbortSignal, encoding?: 'utf-8' | 'gbk', init?: RequestInit): Promise<string>;
20
- /**
21
- * 港美股快照与 A 股快照**不能共用一份字段表**:实测腾讯在同一个 `~` 分隔串里换了列位——
22
- * 币种 A 股根本没有、港股在第 75 位、美股在第 35 位;换手率港股在第 59 位(分母是**总股本**)、
23
- * 美股在第 38 位(分母是**流通股本**);成交额在港股**指数行**是万元级而个股行是元级。
24
- * 所以这两张表各自映射,并且只收能用算术或量级证明的列(docs/dev/tool-schema.md §10.2:口径猜错就是话说错)。
25
- */
12
+ /** Separate HK/US and A-share field tables: currency 75/35; turnover 59 (total shares) vs 38 (float). */
26
13
  export declare const HK_QUOTE_FIELDS = 78;
27
14
  export declare const US_QUOTE_FIELDS = 73;
28
- /**
29
- * 腾讯港美股时间戳是**交易所当地时间**。分隔符**按行漂移**:2026-10-05 那批同一份响应里,
30
- * 一行给 `YYYY/MM/DD HH:mm:ss`、另一行给 `YYYY-MM-DD HH:mm:ss`——所以两种都吃,输出统一成横杠那一种。
31
- * (写成占位符而不是真日期:这两个形状在源码里只差一个字符,肉眼读注释分不出来。)
32
- */
15
+ /** Exchange-local time; separator drifts per row (`/` vs `-`), normalize to hyphen. */
33
16
  export declare function offshoreQuoteTime(value: unknown): string | null;
34
- /** 逐行拆出 `v_<symbol>="…"`;symbol 就是请求里写出去的那串,所以请求与回执能一对一核对。 */
35
17
  export declare function snapshotRows(text: string, requested: Set<string>): Array<{
36
18
  symbol: string;
37
19
  fields: string[];
38
20
  }>;
39
- /** 港美股代码:HK 是 5 位数字(实测 `hk700`/`hk0700` 被上游静默丢掉,必须左补零),指数别名一律拒收。 */
21
+ /** HK codes are 5 digits; `hk700`/`hk0700` are silently dropped upstream, so left-pad. */
40
22
  export declare function normalizeHkCode(value: unknown, name: string): string;
41
- /**
42
- * 美股写法照原样收下(`AAPL` / `BRK.B` / `BABA.N` / `AAPL.OQ` 都是真实存在的说法),
43
- * 消歧交给上游(见 `usSymbolVariants`):实测腾讯**快照只认不带后缀的写法**(`usBABA.N` 整行不回,
44
- * `usBABA` 认),而 K 线只认**带正确后缀**的写法(`usAAPL` 回一列 2011 年的陌生序列、
45
- * `usAAPL.NS` 静默 0 行)。点号既可能是级别码(BRK.B / BF.B)也可能是交易所后缀(.N / .OQ),
46
- * 从字面上分不出来——所以不猜。
47
- */
23
+ /** Snapshot matches only bare US tickers (`usBABA.N` → no row); K-line needs the suffix (`usAAPL` → stale 2011 data). */
48
24
  export declare function normalizeUsCode(value: unknown, name: string): string;
49
- /** 依次尝试的写法:原样 → 每次剥掉最后一段点号,最多剥两段(BRK.B.N → BRK.B → BRK)。 */
50
25
  export declare function usSymbolVariants(ticker: string): string[];
51
26
  export declare function parseHkQuoteRows(text: string, requested: Set<string>): JsonRecord[];
52
27
  export declare function parseUsQuoteRows(text: string, requested: Set<string>): Array<{
53
28
  symbol: string;
54
29
  row: JsonRecord;
55
30
  }>;
56
- /**
57
- * **港美股指数快照**:只收点位那一族能证明的列。
58
- *
59
- * 为什么必须另开一张表(而不是复用个股那张):实测指数行的量纲列不可读——
60
- * 港 `hkHSI` 的第 6 / 36 / 37 位两列相等且量级像万元,美 `usIXIC` 的第 37 位给了 2.1e14、
61
- * 第 38 / 44 / 45 / 62 / 63 位是空。把这些列收进输出表就等于承诺一个我们证明不了的口径。
62
- *
63
- * 也**不收币种**:上游确实在第 75 / 35 位塞了 `HKD` / `USD`,但点位是无量纲的数,
64
- * 替它标货币单位是替上游说话(`docs/design/watchlist-hk-us.md` §3.5)。
65
- *
66
- * 身份守卫是形状而非猜测:指数行第 2 位美股回 `.IXIC`(**前导点**)、港股回 `HSI`(**纯字母**);
67
- * 个股行美股回 `AAPL.OQ`(点在中间)、港股回 `00700`(5 位数字)。2026-10-07 实测踩到过
68
- * `usSSPX` 回的是 `SSPX.AM`(一只 ETF)——所以猜写法拿指数必错,而这一层的形状核对是最后一道闸。
69
- */
31
+ /** Indices: points only; magnitude columns unreadable. No currency. US code has a leading dot, HK is letters. */
70
32
  export declare function parseOffshoreIndexRows(text: string, requested: Set<string>, market: 'hk' | 'us'): JsonRecord[];
71
- /** 港美股指数代码:HK 是字母码(HSI / HSTECH / HSCEI),美是裸字母码(IXIC / DJI / INX,请求时不带那个前导点)。 */
72
33
  export declare function normalizeOffshoreIndexCode(value: unknown, name: string, market: 'hk' | 'us'): string;
73
- /**
74
- * 港美股快照的**唯一**取数编排(能力侧与自选股面板共用)。
75
- * 港股一次批量问完;美股先按原写法问一轮,**没回行**的那几只才用剥掉后缀的写法再问,
76
- * 所以正常路径(纯字母 ticker)一次请求都不多花。
77
- */
34
+ /** HK one batch; US retries only no-row tickers with stripped suffixes. */
78
35
  export declare function fetchOffshoreQuotes(market: 'hk' | 'us', codes: string[], signal: AbortSignal): Promise<JsonRecord[]>;
79
- /** 港美股指数快照的取数编排:字母码没有"剥后缀重问"这一说,所以一轮问完。 */
80
36
  export declare function fetchOffshoreIndexQuotes(market: 'hk' | 'us', codes: string[], signal: AbortSignal): Promise<JsonRecord[]>;
81
- /** 美股 K 线入口需要的带后缀身份:先从快照拿上游自己的第 2 位(能力侧用,形状判据同一条)。 */
82
37
  export declare function resolveUsKlineSymbol(ticker: string, signal: AbortSignal): Promise<string>;
83
- /**
84
- * smartbox(腾讯公开检索)的一行候选。
85
- *
86
- * 回包形状是 `v_hint="市场~代码~名称~拼音~类型^…"`:ASCII 的 `\uXXXX` 转义(**不需要 GBK**)、
87
- * `^` 分行、`~` 分列,最多约 10 条。无命中与**非法 `t` 值**都回 `N` 这一个哨兵(实测
88
- * `t=hk,us` 这种列表形态也落进 `N`),所以解析器把它当零条、调用方把 `t` 钉成常量。
89
- */
38
+ /** smartbox `v_hint="market~code~name~pinyin~type^…"` (~10); no hit or invalid `t` both return `N`. */
90
39
  export interface SmartboxCandidate {
91
40
  market: string;
92
41
  code: string;
@@ -95,6 +44,5 @@ export interface SmartboxCandidate {
95
44
  type: string;
96
45
  }
97
46
  export declare function parseSmartbox(text: string): SmartboxCandidate[];
98
- /** smartbox 检索:市场段只能是上游认的单值(`hk` / `us` / `all`),列表形态实测失效。 */
99
47
  export declare const SMARTBOX_MARKETS: string[];
100
48
  export declare function fetchSmartbox(query: string, market: string, signal: AbortSignal): Promise<SmartboxCandidate[]>;
@@ -1,22 +1,10 @@
1
1
  /**
2
- * 腾讯公开接口的最小内核:**零 import 的叶子模块**(只依赖 `net/throttle.js`,它自己也是叶子)。
3
- *
4
- * 为什么单独一份:host 平面的自选股(`watchlist/index.js`)与插件侧的行情能力
5
- * (`tencent-http.ts`)必须共用三样东西——同一条出口节流链、同一份港美股字段位序表、同一套守卫。
6
- * 两条独立节流等于没有限流(东财那次的教训),两份位序表迟早只会修好一份(AGENTS.md §9.7)。
7
- * 这一层一旦被别的模块拖进来,host 半边就会顺带加载整棵插件依赖树,所以这里不放能力定义、
8
- * 不放 Dataset 依赖、也不放任何类型依赖。
9
- *
10
- * 错误文案与判定形状逐字保持历史形态(Agent 与用户看到的字符串不能因抽取而漂移)。
2
+ * Tencent public endpoint kernel: leaf (only `net/throttle.js`). Shared by the host-plane watchlist
3
+ * and `tencent-http.ts` for one throttle chain and one HK/US field table. No other dependencies.
11
4
  */
12
5
  import { createRequestThrottle } from '../net/throttle.js';
13
6
  export const QUOTE_URL = 'https://qt.gtimg.cn/q=';
14
7
  export const SMARTBOX_URL = 'https://smartbox.gtimg.cn/s3/';
15
- /**
16
- * 腾讯出口的**唯一**节流链(进程级)。行情快照、分笔、K 线每一次换机、港美股快照、smartbox
17
- * 检索全部过这一条链——它们打的是同一家,按 IP 风控(429 / 空 data / 静默限流)。
18
- * 间隔取 120ms:与最早那版分笔翻页的节奏同量级。
19
- */
20
8
  export const TENCENT_MIN_INTERVAL_MS = 120;
21
9
  export const tencentThrottle = createRequestThrottle(TENCENT_MIN_INTERVAL_MS);
22
10
  export function isRecord(value) {
@@ -45,10 +33,7 @@ async function readResponse(response, encoding) {
45
33
  const bytes = await response.arrayBuffer();
46
34
  return new TextDecoder(encoding).decode(bytes);
47
35
  }
48
- /**
49
- * 腾讯出口的取文本:取消不排队(队首那个请求挂住时,把这次取消塞进队列等于把它吞掉,
50
- * `docs/dev/web-retriever.md` §4.2「取消原样抛出」),排队与最小间隔由节流链负责。
51
- */
36
+ /** Cancellation does not queue; see `docs/dev/web-retriever.md` §4.2. */
52
37
  export async function tencentGetText(url, signal, encoding = 'utf-8', init = {}) {
53
38
  signal.throwIfAborted();
54
39
  return tencentThrottle(async () => {
@@ -63,24 +48,14 @@ export async function tencentGetText(url, signal, encoding = 'utf-8', init = {})
63
48
  return text;
64
49
  });
65
50
  }
66
- /**
67
- * 港美股快照与 A 股快照**不能共用一份字段表**:实测腾讯在同一个 `~` 分隔串里换了列位——
68
- * 币种 A 股根本没有、港股在第 75 位、美股在第 35 位;换手率港股在第 59 位(分母是**总股本**)、
69
- * 美股在第 38 位(分母是**流通股本**);成交额在港股**指数行**是万元级而个股行是元级。
70
- * 所以这两张表各自映射,并且只收能用算术或量级证明的列(docs/dev/tool-schema.md §10.2:口径猜错就是话说错)。
71
- */
51
+ /** Separate HK/US and A-share field tables: currency 75/35; turnover 59 (total shares) vs 38 (float). */
72
52
  export const HK_QUOTE_FIELDS = 78;
73
53
  export const US_QUOTE_FIELDS = 73;
74
- /**
75
- * 腾讯港美股时间戳是**交易所当地时间**。分隔符**按行漂移**:2026-10-05 那批同一份响应里,
76
- * 一行给 `YYYY/MM/DD HH:mm:ss`、另一行给 `YYYY-MM-DD HH:mm:ss`——所以两种都吃,输出统一成横杠那一种。
77
- * (写成占位符而不是真日期:这两个形状在源码里只差一个字符,肉眼读注释分不出来。)
78
- */
54
+ /** Exchange-local time; separator drifts per row (`/` vs `-`), normalize to hyphen. */
79
55
  export function offshoreQuoteTime(value) {
80
56
  const match = /^(\d{4})[-/](\d{2})[-/](\d{2}) (\d{2}:\d{2}:\d{2})$/.exec(String(value ?? '').trim());
81
57
  return match ? `${match[1]}-${match[2]}-${match[3]} ${match[4]}` : null;
82
58
  }
83
- /** 逐行拆出 `v_<symbol>="…"`;symbol 就是请求里写出去的那串,所以请求与回执能一对一核对。 */
84
59
  export function snapshotRows(text, requested) {
85
60
  const rows = [];
86
61
  for (const line of text.split(';')) {
@@ -93,7 +68,7 @@ export function snapshotRows(text, requested) {
93
68
  }
94
69
  return rows;
95
70
  }
96
- /** 港美股代码:HK 是 5 位数字(实测 `hk700`/`hk0700` 被上游静默丢掉,必须左补零),指数别名一律拒收。 */
71
+ /** HK codes are 5 digits; `hk700`/`hk0700` are silently dropped upstream, so left-pad. */
97
72
  export function normalizeHkCode(value, name) {
98
73
  const raw = String(value ?? '').trim().toUpperCase();
99
74
  const digits = /^(?:HK)?(\d{1,5})(?:\.HK)?$/.exec(raw)?.[1];
@@ -101,24 +76,14 @@ export function normalizeHkCode(value, name) {
101
76
  throw new Error(`parameter ${name} must be a five-digit Hong Kong stock code (for example 00700, hk00700 or 700); HK index codes such as HSI/HSTECH are not supported here`);
102
77
  return digits.padStart(5, '0');
103
78
  }
104
- /**
105
- * 美股写法照原样收下(`AAPL` / `BRK.B` / `BABA.N` / `AAPL.OQ` 都是真实存在的说法),
106
- * 消歧交给上游(见 `usSymbolVariants`):实测腾讯**快照只认不带后缀的写法**(`usBABA.N` 整行不回,
107
- * `usBABA` 认),而 K 线只认**带正确后缀**的写法(`usAAPL` 回一列 2011 年的陌生序列、
108
- * `usAAPL.NS` 静默 0 行)。点号既可能是级别码(BRK.B / BF.B)也可能是交易所后缀(.N / .OQ),
109
- * 从字面上分不出来——所以不猜。
110
- */
79
+ /** Snapshot matches only bare US tickers (`usBABA.N` → no row); K-line needs the suffix (`usAAPL` → stale 2011 data). */
111
80
  export function normalizeUsCode(value, name) {
112
81
  const raw = String(value ?? '').trim().toUpperCase();
113
82
  const match = /^(?:US)?([A-Z]{1,6}(?:\.[A-Z]{1,3}){0,2})$/.exec(raw);
114
83
  if (!match)
115
84
  throw new Error(`parameter ${name} must be a US equity ticker such as AAPL, BRK.B or BABA.N (a Tencent exchange suffix like .OQ is accepted and re-resolved from the snapshot)`);
116
- // 两位以上的点号尾段(.OQ/.AM/.NS)在腾讯那里一定是交易所后缀,直接剥掉,于是 AAPL 与 AAPL.OQ
117
- // 是同一只票、只请求一次。**单字母尾段**(BRK.B 的级别码 vs BABA.N 的交易所码)字面上分不开,
118
- // 就照原样留着,交给上游逐个写法裁决——猜错一次只是多一轮请求,猜错第二次就是把数据挂错标的。
119
85
  return match[1].replace(/\.[A-Z]{2,3}$/u, '');
120
86
  }
121
- /** 依次尝试的写法:原样 → 每次剥掉最后一段点号,最多剥两段(BRK.B.N → BRK.B → BRK)。 */
122
87
  export function usSymbolVariants(ticker) {
123
88
  const variants = [`us${ticker}`];
124
89
  let current = ticker;
@@ -136,8 +101,6 @@ export function parseHkQuoteRows(text, requested) {
136
101
  for (const { symbol, fields } of snapshotRows(text, requested)) {
137
102
  if (fields.length < HK_QUOTE_FIELDS)
138
103
  throw sourceError(`Tencent HK quote ${symbol} returned ${fields.length} fields, expected at least ${HK_QUOTE_FIELDS}`, 'tencent_invalid_response');
139
- // 只收个股:指数行的第 6/37 位在实测里既不是"股"也不是"港元"(hkHSI 两列相等、量级像万元),
140
- // 两种量纲不许进同一列;参数层已经拒掉字母指数,这里再核对一次上游没把行串错。
141
104
  if (!/^\d{5}$/.test(fields[2] ?? ''))
142
105
  throw sourceError(`Tencent HK quote ${symbol} is not a five-digit stock code (got ${JSON.stringify(fields[2] ?? '')})`, 'tencent_invalid_response');
143
106
  const quoteTime = offshoreQuoteTime(fields[30]);
@@ -179,8 +142,6 @@ export function parseUsQuoteRows(text, requested) {
179
142
  for (const { symbol, fields } of snapshotRows(text, requested)) {
180
143
  if (fields.length < US_QUOTE_FIELDS)
181
144
  throw sourceError(`Tencent US quote ${symbol} returned ${fields.length} fields, expected at least ${US_QUOTE_FIELDS}`, 'tencent_invalid_response');
182
- // 上游在 2 号位回交易所后缀(AAPL.OQ / BABA.N),这是 K 线入口要用的身份,缺了就无法核对。
183
- // 交易所后缀实测有**一个字母**的(NYSE:BABA.N / BRK.B.N),把下限写成 2 会把真代码判成畸形。
184
145
  if (!/^[A-Z][A-Z.]{0,9}\.[A-Z]{1,3}$/.test(fields[2] ?? ''))
185
146
  throw sourceError(`Tencent US quote ${symbol} has an unexpected code ${JSON.stringify(fields[2] ?? '')}`, 'tencent_invalid_response');
186
147
  const quoteTime = offshoreQuoteTime(fields[30]);
@@ -215,25 +176,11 @@ export function parseUsQuoteRows(text, requested) {
215
176
  const stale = row.amount_usd === 0 && row.price !== null && row.price === row.last_close;
216
177
  row.is_stale = stale;
217
178
  row.stale_reason = stale ? 'zero turnover and price equals previous close' : null;
218
- // symbol 是"哪个写法换来了这一行",只给上面的重试判定用,不出现在行里。
219
179
  rows.push({ symbol, row });
220
180
  }
221
181
  return rows;
222
182
  }
223
- /**
224
- * **港美股指数快照**:只收点位那一族能证明的列。
225
- *
226
- * 为什么必须另开一张表(而不是复用个股那张):实测指数行的量纲列不可读——
227
- * 港 `hkHSI` 的第 6 / 36 / 37 位两列相等且量级像万元,美 `usIXIC` 的第 37 位给了 2.1e14、
228
- * 第 38 / 44 / 45 / 62 / 63 位是空。把这些列收进输出表就等于承诺一个我们证明不了的口径。
229
- *
230
- * 也**不收币种**:上游确实在第 75 / 35 位塞了 `HKD` / `USD`,但点位是无量纲的数,
231
- * 替它标货币单位是替上游说话(`docs/design/watchlist-hk-us.md` §3.5)。
232
- *
233
- * 身份守卫是形状而非猜测:指数行第 2 位美股回 `.IXIC`(**前导点**)、港股回 `HSI`(**纯字母**);
234
- * 个股行美股回 `AAPL.OQ`(点在中间)、港股回 `00700`(5 位数字)。2026-10-07 实测踩到过
235
- * `usSSPX` 回的是 `SSPX.AM`(一只 ETF)——所以猜写法拿指数必错,而这一层的形状核对是最后一道闸。
236
- */
183
+ /** Indices: points only; magnitude columns unreadable. No currency. US code has a leading dot, HK is letters. */
237
184
  export function parseOffshoreIndexRows(text, requested, market) {
238
185
  const fieldFloor = market === 'hk' ? HK_QUOTE_FIELDS : US_QUOTE_FIELDS;
239
186
  const codeOf = (code) => (market === 'hk' ? code : code.replace(/^./u, ''));
@@ -242,7 +189,6 @@ export function parseOffshoreIndexRows(text, requested, market) {
242
189
  if (fields.length < fieldFloor)
243
190
  throw sourceError(`Tencent ${market} index quote ${symbol} returned ${fields.length} fields, expected at least ${fieldFloor}`, 'tencent_invalid_response');
244
191
  const raw = String(fields[2] ?? '');
245
- // 港:字母指数码;美:`.字母`。两种都明确排除个股形状(数字 / 点在中间的后缀形式)。
246
192
  const shapeOk = market === 'hk' ? /^[A-Z][A-Z0-9.]{1,9}$/u.test(raw) : /^\.[A-Z][A-Z0-9.]{1,9}$/u.test(raw);
247
193
  if (!shapeOk)
248
194
  throw sourceError(`Tencent ${market} index quote ${symbol} is not an index code (got ${JSON.stringify(raw)})`, 'tencent_invalid_response');
@@ -268,7 +214,6 @@ export function parseOffshoreIndexRows(text, requested, market) {
268
214
  }
269
215
  return rows;
270
216
  }
271
- /** 港美股指数代码:HK 是字母码(HSI / HSTECH / HSCEI),美是裸字母码(IXIC / DJI / INX,请求时不带那个前导点)。 */
272
217
  export function normalizeOffshoreIndexCode(value, name, market) {
273
218
  const raw = String(value ?? '').trim().toUpperCase().replace(/^(?:HK|US)/u, '');
274
219
  const stem = raw.replace(/^\./u, '').replace(/\.(?:HK|US)$/u, '');
@@ -280,11 +225,7 @@ export function normalizeOffshoreIndexCode(value, name, market) {
280
225
  }
281
226
  return stem;
282
227
  }
283
- /**
284
- * 港美股快照的**唯一**取数编排(能力侧与自选股面板共用)。
285
- * 港股一次批量问完;美股先按原写法问一轮,**没回行**的那几只才用剥掉后缀的写法再问,
286
- * 所以正常路径(纯字母 ticker)一次请求都不多花。
287
- */
228
+ /** HK one batch; US retries only no-row tickers with stripped suffixes. */
288
229
  export async function fetchOffshoreQuotes(market, codes, signal) {
289
230
  if (market === 'hk') {
290
231
  const symbols = codes.map((code) => `hk${code}`);
@@ -303,8 +244,7 @@ export async function fetchOffshoreQuotes(market, codes, signal) {
303
244
  tried.add(symbol.toLowerCase());
304
245
  const text = await tencentGetText(`${QUOTE_URL}${symbols.join(',')}`, signal, 'gbk');
305
246
  const found = parseUsQuoteRows(text, new Set(symbols.map((symbol) => symbol.toLowerCase())));
306
- // 上游把变量名原样回显(实测送 `usBRK.B` 回 `v_usBRK.B`),但"这一只没回行"的判断不该押在大小写上:
307
- // 比不中就会对一只已经拿到数的票再问一轮剥短写法,白烧一次出网。
247
+ // Match case-insensitively; a miss would requery an already-resolved ticker.
308
248
  const hit = new Set(found.map((item) => item.symbol.toLowerCase()));
309
249
  rows.push(...found.map((item) => item.row));
310
250
  for (let index = 0; index < symbols.length; index += 1) {
@@ -312,7 +252,7 @@ export async function fetchOffshoreQuotes(market, codes, signal) {
312
252
  pending[index] = [];
313
253
  continue;
314
254
  }
315
- // 快照没认这个写法:剥一段点号再看;剥无可剥就只能认定腾讯不认识这只(不编造身份)。
255
+ // Strip one dot-segment and retry; when nothing is left, upstream does not know the ticker.
316
256
  const next = pending[index].find((variant) => !tried.has(variant.toLowerCase()));
317
257
  if (next !== undefined)
318
258
  pending[index] = [next, ...pending[index].filter((variant) => variant !== next)];
@@ -322,7 +262,6 @@ export async function fetchOffshoreQuotes(market, codes, signal) {
322
262
  throw sourceError(`Tencent returned no US quote rows for ${codes.join(',')}`, 'tencent_empty_response');
323
263
  return rows;
324
264
  }
325
- /** 港美股指数快照的取数编排:字母码没有"剥后缀重问"这一说,所以一轮问完。 */
326
265
  export async function fetchOffshoreIndexQuotes(market, codes, signal) {
327
266
  const symbols = codes.map((code) => `${market}${code}`);
328
267
  const text = await tencentGetText(`${QUOTE_URL}${symbols.join(',')}`, signal, 'gbk');
@@ -331,7 +270,6 @@ export async function fetchOffshoreIndexQuotes(market, codes, signal) {
331
270
  throw sourceError(`Tencent returned no ${market} index quote rows for ${symbols.join(',')}`, 'tencent_empty_response');
332
271
  return rows;
333
272
  }
334
- /** 美股 K 线入口需要的带后缀身份:先从快照拿上游自己的第 2 位(能力侧用,形状判据同一条)。 */
335
273
  export async function resolveUsKlineSymbol(ticker, signal) {
336
274
  for (const symbol of usSymbolVariants(ticker)) {
337
275
  const text = await tencentGetText(`${QUOTE_URL}${symbol}`, signal, 'gbk');
@@ -362,7 +300,6 @@ export function parseSmartbox(text) {
362
300
  }
363
301
  return candidates;
364
302
  }
365
- /** 回包里的名称是 `\uXXXX` 字面转义(实测),解一次转义;解不动就原样返回,不编造名称。 */
366
303
  function unescapeHint(value) {
367
304
  if (!value.includes('\\u'))
368
305
  return value;
@@ -373,14 +310,12 @@ function unescapeHint(value) {
373
310
  return value;
374
311
  }
375
312
  }
376
- /** smartbox 检索:市场段只能是上游认的单值(`hk` / `us` / `all`),列表形态实测失效。 */
377
313
  export const SMARTBOX_MARKETS = ['all', 'hk', 'us'];
378
314
  export async function fetchSmartbox(query, market, signal) {
379
315
  if (!SMARTBOX_MARKETS.includes(market))
380
316
  throw new Error(`smartbox market must be one of ${SMARTBOX_MARKETS.join('/')}; got ${JSON.stringify(market)}(列表形态实测回 N 哨兵,等于查无此票)`);
381
317
  const url = `${SMARTBOX_URL}?v=2&t=${encodeURIComponent(market)}&q=${encodeURIComponent(query)}`;
382
- // 名称是 `\uXXXX` 字面转义、正文全 ASCII,所以走 utf-8;'ascii' 这个标签在 Encoding Standard 里
383
- // 映射的是 windows-1252,不是 US-ASCII,拿来解 ASCII 正文是个埋着的坑。
318
+ // ASCII `\uXXXX` body: use utf-8; the 'ascii' label maps to windows-1252.
384
319
  const text = await tencentGetText(url, signal);
385
320
  return parseSmartbox(text);
386
321
  }
@@ -1,8 +1,3 @@
1
1
  import type { DataSource } from '../data-collector/hub.js';
2
2
  export type WindApiKeyResolver = () => Promise<string | undefined>;
3
- /**
4
- * §6.7 改口后的 P1 三条。`wind_edb_*` 是 P0(EDB 是唯一来源),`wind_stock_kline` 是 P1。
5
- * 不接的一概有理由:price_indicators 与全部 NL 工具(中文键 / 列名是 question 的函数)、
6
- * bond 与 fund/index(重叠或未测)、analytics_data(实测连 initialize 都挂)。
7
- */
8
3
  export declare function createWindSources(resolveApiKey: WindApiKeyResolver): DataSource[];