@v587d/capital-generation 2.6.2 → 2.6.4

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.
@@ -0,0 +1,386 @@
1
+ /**
2
+ * 腾讯公开接口的最小内核:**零 import 的叶子模块**(只依赖 `net/throttle.js`,它自己也是叶子)。
3
+ *
4
+ * 为什么单独一份:host 平面的自选股(`capital-watchlist/index.js`)与插件侧的行情能力
5
+ * (`tencent-http.ts`)必须共用三样东西——同一条出口节流链、同一份港美股字段位序表、同一套守卫。
6
+ * 两条独立节流等于没有限流(东财那次的教训),两份位序表迟早只会修好一份(AGENTS.md §9.7)。
7
+ * 这一层一旦被别的模块拖进来,host 半边就会顺带加载整棵插件依赖树,所以这里不放能力定义、
8
+ * 不放 Dataset 依赖、也不放任何类型依赖。
9
+ *
10
+ * 错误文案与判定形状逐字保持历史形态(Agent 与用户看到的字符串不能因抽取而漂移)。
11
+ */
12
+ import { createRequestThrottle } from '../net/throttle.js';
13
+ export const QUOTE_URL = 'https://qt.gtimg.cn/q=';
14
+ export const SMARTBOX_URL = 'https://smartbox.gtimg.cn/s3/';
15
+ /**
16
+ * 腾讯出口的**唯一**节流链(进程级)。行情快照、分笔、K 线每一次换机、港美股快照、smartbox
17
+ * 检索全部过这一条链——它们打的是同一家,按 IP 风控(429 / 空 data / 静默限流)。
18
+ * 间隔取 120ms:与最早那版分笔翻页的节奏同量级。
19
+ */
20
+ export const TENCENT_MIN_INTERVAL_MS = 120;
21
+ export const tencentThrottle = createRequestThrottle(TENCENT_MIN_INTERVAL_MS);
22
+ export function isRecord(value) {
23
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
24
+ }
25
+ export function numberOrNull(value) {
26
+ if (value === undefined || value === null || value === '')
27
+ return null;
28
+ if (typeof value === 'boolean')
29
+ throw new Error('source returned boolean where a number was expected');
30
+ const number = typeof value === 'number' ? value : Number(String(value).replaceAll(',', '').trim());
31
+ return Number.isFinite(number) ? number : null;
32
+ }
33
+ export function requiredNumber(value, field) {
34
+ const number = numberOrNull(value);
35
+ if (number === null)
36
+ throw new Error(`source returned an invalid ${field}`);
37
+ return number;
38
+ }
39
+ export function sourceError(message, code = 'tencent_source_error') {
40
+ const error = new Error(message);
41
+ error.code = code;
42
+ return error;
43
+ }
44
+ async function readResponse(response, encoding) {
45
+ const bytes = await response.arrayBuffer();
46
+ return new TextDecoder(encoding).decode(bytes);
47
+ }
48
+ /**
49
+ * 腾讯出口的取文本:取消不排队(队首那个请求挂住时,把这次取消塞进队列等于把它吞掉,
50
+ * `docs/dev/web-retriever.md` §4.2「取消原样抛出」),排队与最小间隔由节流链负责。
51
+ */
52
+ export async function tencentGetText(url, signal, encoding = 'utf-8', init = {}) {
53
+ signal.throwIfAborted();
54
+ return tencentThrottle(async () => {
55
+ const response = await fetch(url, {
56
+ ...init,
57
+ headers: { 'User-Agent': 'Mozilla/5.0', ...(init.headers ?? {}) },
58
+ signal,
59
+ });
60
+ const text = await readResponse(response, encoding);
61
+ if (!response.ok)
62
+ throw sourceError(`Tencent HTTP ${response.status} for ${url}`, response.status === 429 ? 'tencent_rate_limit' : 'tencent_http_error');
63
+ return text;
64
+ });
65
+ }
66
+ /**
67
+ * 港美股快照与 A 股快照**不能共用一份字段表**:实测腾讯在同一个 `~` 分隔串里换了列位——
68
+ * 币种 A 股根本没有、港股在第 75 位、美股在第 35 位;换手率港股在第 59 位(分母是**总股本**)、
69
+ * 美股在第 38 位(分母是**流通股本**);成交额在港股**指数行**是万元级而个股行是元级。
70
+ * 所以这两张表各自映射,并且只收能用算术或量级证明的列(docs/dev/tool-schema.md §10.2:口径猜错就是话说错)。
71
+ */
72
+ export const HK_QUOTE_FIELDS = 78;
73
+ 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
+ */
79
+ export function offshoreQuoteTime(value) {
80
+ const match = /^(\d{4})[-/](\d{2})[-/](\d{2}) (\d{2}:\d{2}:\d{2})$/.exec(String(value ?? '').trim());
81
+ return match ? `${match[1]}-${match[2]}-${match[3]} ${match[4]}` : null;
82
+ }
83
+ /** 逐行拆出 `v_<symbol>="…"`;symbol 就是请求里写出去的那串,所以请求与回执能一对一核对。 */
84
+ export function snapshotRows(text, requested) {
85
+ const rows = [];
86
+ for (const line of text.split(';')) {
87
+ const match = /^v_([\w.]+)="([^"]*)"/.exec(line.trim());
88
+ if (!match)
89
+ continue;
90
+ if (!requested.has(match[1].toLowerCase()))
91
+ continue;
92
+ rows.push({ symbol: match[1], fields: match[2].split('~') });
93
+ }
94
+ return rows;
95
+ }
96
+ /** 港美股代码:HK 是 5 位数字(实测 `hk700`/`hk0700` 被上游静默丢掉,必须左补零),指数别名一律拒收。 */
97
+ export function normalizeHkCode(value, name) {
98
+ const raw = String(value ?? '').trim().toUpperCase();
99
+ const digits = /^(?:HK)?(\d{1,5})(?:\.HK)?$/.exec(raw)?.[1];
100
+ if (!digits)
101
+ 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
+ return digits.padStart(5, '0');
103
+ }
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
+ */
111
+ export function normalizeUsCode(value, name) {
112
+ const raw = String(value ?? '').trim().toUpperCase();
113
+ const match = /^(?:US)?([A-Z]{1,6}(?:\.[A-Z]{1,3}){0,2})$/.exec(raw);
114
+ if (!match)
115
+ 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
+ return match[1].replace(/\.[A-Z]{2,3}$/u, '');
120
+ }
121
+ /** 依次尝试的写法:原样 → 每次剥掉最后一段点号,最多剥两段(BRK.B.N → BRK.B → BRK)。 */
122
+ export function usSymbolVariants(ticker) {
123
+ const variants = [`us${ticker}`];
124
+ let current = ticker;
125
+ for (let depth = 0; depth < 2; depth += 1) {
126
+ const dot = current.lastIndexOf('.');
127
+ if (dot <= 0)
128
+ break;
129
+ current = current.slice(0, dot);
130
+ variants.push(`us${current}`);
131
+ }
132
+ return variants;
133
+ }
134
+ export function parseHkQuoteRows(text, requested) {
135
+ const rows = [];
136
+ for (const { symbol, fields } of snapshotRows(text, requested)) {
137
+ if (fields.length < HK_QUOTE_FIELDS)
138
+ 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
+ if (!/^\d{5}$/.test(fields[2] ?? ''))
142
+ throw sourceError(`Tencent HK quote ${symbol} is not a five-digit stock code (got ${JSON.stringify(fields[2] ?? '')})`, 'tencent_invalid_response');
143
+ const quoteTime = offshoreQuoteTime(fields[30]);
144
+ if (quoteTime === null)
145
+ throw sourceError(`Tencent HK quote ${symbol} has an unreadable timestamp ${JSON.stringify(fields[30] ?? '')}`, 'tencent_invalid_response');
146
+ const value = (index) => numberOrNull(fields[index]);
147
+ const row = {
148
+ code: fields[2],
149
+ tencent_symbol: `hk${fields[2]}`,
150
+ name: fields[1] ?? '',
151
+ english_name: fields[46] ?? '',
152
+ currency: fields[75] ?? '',
153
+ price: value(3),
154
+ last_close: value(4),
155
+ open: value(5),
156
+ high: value(33),
157
+ low: value(34),
158
+ change_amt: value(31),
159
+ change_pct: value(32),
160
+ amplitude_pct: value(43),
161
+ volume_shares: value(6),
162
+ amount_hkd: value(37),
163
+ avg_price: value(73),
164
+ turnover_pct: value(59),
165
+ total_shares: value(69),
166
+ market_cap_yi_hkd: value(45),
167
+ board_lot_shares: value(60),
168
+ quote_time: quoteTime,
169
+ };
170
+ const stale = row.amount_hkd === 0 && row.price !== null && row.price === row.last_close;
171
+ row.is_stale = stale;
172
+ row.stale_reason = stale ? 'zero turnover and price equals previous close' : null;
173
+ rows.push(row);
174
+ }
175
+ return rows;
176
+ }
177
+ export function parseUsQuoteRows(text, requested) {
178
+ const rows = [];
179
+ for (const { symbol, fields } of snapshotRows(text, requested)) {
180
+ if (fields.length < US_QUOTE_FIELDS)
181
+ 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
+ if (!/^[A-Z][A-Z.]{0,9}\.[A-Z]{1,3}$/.test(fields[2] ?? ''))
185
+ throw sourceError(`Tencent US quote ${symbol} has an unexpected code ${JSON.stringify(fields[2] ?? '')}`, 'tencent_invalid_response');
186
+ const quoteTime = offshoreQuoteTime(fields[30]);
187
+ if (quoteTime === null)
188
+ throw sourceError(`Tencent US quote ${symbol} has an unreadable timestamp ${JSON.stringify(fields[30] ?? '')}`, 'tencent_invalid_response');
189
+ const value = (index) => numberOrNull(fields[index]);
190
+ const row = {
191
+ code: fields[2],
192
+ tencent_symbol: `us${fields[2]}`,
193
+ exchange_code: fields[2].slice(fields[2].lastIndexOf('.') + 1),
194
+ name: fields[1] ?? '',
195
+ english_name: fields[46] ?? '',
196
+ currency: fields[35] ?? '',
197
+ price: value(3),
198
+ last_close: value(4),
199
+ open: value(5),
200
+ high: value(33),
201
+ low: value(34),
202
+ change_amt: value(31),
203
+ change_pct: value(32),
204
+ amplitude_pct: value(43),
205
+ volume_shares: value(6),
206
+ amount_usd: value(37),
207
+ avg_price: value(67),
208
+ turnover_pct: value(38),
209
+ total_shares: value(62),
210
+ float_shares: value(63),
211
+ float_market_cap_yi_usd: value(44),
212
+ market_cap_yi_usd: value(45),
213
+ quote_time: quoteTime,
214
+ };
215
+ const stale = row.amount_usd === 0 && row.price !== null && row.price === row.last_close;
216
+ row.is_stale = stale;
217
+ row.stale_reason = stale ? 'zero turnover and price equals previous close' : null;
218
+ // symbol 是"哪个写法换来了这一行",只给上面的重试判定用,不出现在行里。
219
+ rows.push({ symbol, row });
220
+ }
221
+ return rows;
222
+ }
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
+ */
237
+ export function parseOffshoreIndexRows(text, requested, market) {
238
+ const fieldFloor = market === 'hk' ? HK_QUOTE_FIELDS : US_QUOTE_FIELDS;
239
+ const codeOf = (code) => (market === 'hk' ? code : code.replace(/^./u, ''));
240
+ const rows = [];
241
+ for (const { symbol, fields } of snapshotRows(text, requested)) {
242
+ if (fields.length < fieldFloor)
243
+ throw sourceError(`Tencent ${market} index quote ${symbol} returned ${fields.length} fields, expected at least ${fieldFloor}`, 'tencent_invalid_response');
244
+ const raw = String(fields[2] ?? '');
245
+ // 港:字母指数码;美:`.字母`。两种都明确排除个股形状(数字 / 点在中间的后缀形式)。
246
+ 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
+ if (!shapeOk)
248
+ throw sourceError(`Tencent ${market} index quote ${symbol} is not an index code (got ${JSON.stringify(raw)})`, 'tencent_invalid_response');
249
+ const quoteTime = offshoreQuoteTime(fields[30]);
250
+ if (quoteTime === null)
251
+ throw sourceError(`Tencent ${market} index quote ${symbol} has an unreadable timestamp ${JSON.stringify(fields[30] ?? '')}`, 'tencent_invalid_response');
252
+ const value = (index) => numberOrNull(fields[index]);
253
+ rows.push({
254
+ code: codeOf(raw),
255
+ tencent_symbol: symbol,
256
+ name: fields[1] ?? '',
257
+ english_name: fields[46] ?? '',
258
+ price: value(3),
259
+ last_close: value(4),
260
+ open: value(5),
261
+ high: value(33),
262
+ low: value(34),
263
+ change_amt: value(31),
264
+ change_pct: value(32),
265
+ amplitude_pct: value(43),
266
+ quote_time: quoteTime,
267
+ });
268
+ }
269
+ return rows;
270
+ }
271
+ /** 港美股指数代码:HK 是字母码(HSI / HSTECH / HSCEI),美是裸字母码(IXIC / DJI / INX,请求时不带那个前导点)。 */
272
+ export function normalizeOffshoreIndexCode(value, name, market) {
273
+ const raw = String(value ?? '').trim().toUpperCase().replace(/^(?:HK|US)/u, '');
274
+ const stem = raw.replace(/^\./u, '').replace(/\.(?:HK|US)$/u, '');
275
+ if (market === 'hk' && /^(?:\d{1,5}|hk\d{1,5})$/u.test(raw)) {
276
+ throw new Error(`parameter ${name} must be a Hong Kong index code such as HSI or HSTECH (five-digit codes belong to tencent_hk_quote: 实测指数行与个股行的量纲列不同)`);
277
+ }
278
+ if (!/^[A-Z][A-Z0-9.]{1,9}$/u.test(stem)) {
279
+ throw new Error(`parameter ${name} must be an index code letters only (for example HSI, HSTECH for HK or IXIC, DJI, INX for US) — 实测腾讯不认 usSPX / usNDQ / usSOX / usRUT,写法只认检索回来的那个`);
280
+ }
281
+ return stem;
282
+ }
283
+ /**
284
+ * 港美股快照的**唯一**取数编排(能力侧与自选股面板共用)。
285
+ * 港股一次批量问完;美股先按原写法问一轮,**没回行**的那几只才用剥掉后缀的写法再问,
286
+ * 所以正常路径(纯字母 ticker)一次请求都不多花。
287
+ */
288
+ export async function fetchOffshoreQuotes(market, codes, signal) {
289
+ if (market === 'hk') {
290
+ const symbols = codes.map((code) => `hk${code}`);
291
+ const text = await tencentGetText(`${QUOTE_URL}${symbols.join(',')}`, signal, 'gbk');
292
+ const rows = parseHkQuoteRows(text, new Set(symbols.map((symbol) => symbol.toLowerCase())));
293
+ if (rows.length === 0)
294
+ throw sourceError(`Tencent returned no HK quote rows for ${symbols.join(',')}`, 'tencent_empty_response');
295
+ return rows;
296
+ }
297
+ const pending = codes.map((ticker) => usSymbolVariants(ticker));
298
+ const rows = [];
299
+ const tried = new Set();
300
+ for (let round = 0; round < 3 && pending.some((list) => list.length > 0); round += 1) {
301
+ const symbols = pending.map((list) => list.shift()).filter((symbol) => Boolean(symbol));
302
+ for (const symbol of symbols)
303
+ tried.add(symbol.toLowerCase());
304
+ const text = await tencentGetText(`${QUOTE_URL}${symbols.join(',')}`, signal, 'gbk');
305
+ const found = parseUsQuoteRows(text, new Set(symbols.map((symbol) => symbol.toLowerCase())));
306
+ // 上游把变量名原样回显(实测送 `usBRK.B` 回 `v_usBRK.B`),但"这一只没回行"的判断不该押在大小写上:
307
+ // 比不中就会对一只已经拿到数的票再问一轮剥短写法,白烧一次出网。
308
+ const hit = new Set(found.map((item) => item.symbol.toLowerCase()));
309
+ rows.push(...found.map((item) => item.row));
310
+ for (let index = 0; index < symbols.length; index += 1) {
311
+ if (hit.has(symbols[index].toLowerCase())) {
312
+ pending[index] = [];
313
+ continue;
314
+ }
315
+ // 快照没认这个写法:剥一段点号再看;剥无可剥就只能认定腾讯不认识这只(不编造身份)。
316
+ const next = pending[index].find((variant) => !tried.has(variant.toLowerCase()));
317
+ if (next !== undefined)
318
+ pending[index] = [next, ...pending[index].filter((variant) => variant !== next)];
319
+ }
320
+ }
321
+ if (rows.length === 0)
322
+ throw sourceError(`Tencent returned no US quote rows for ${codes.join(',')}`, 'tencent_empty_response');
323
+ return rows;
324
+ }
325
+ /** 港美股指数快照的取数编排:字母码没有"剥后缀重问"这一说,所以一轮问完。 */
326
+ export async function fetchOffshoreIndexQuotes(market, codes, signal) {
327
+ const symbols = codes.map((code) => `${market}${code}`);
328
+ const text = await tencentGetText(`${QUOTE_URL}${symbols.join(',')}`, signal, 'gbk');
329
+ const rows = parseOffshoreIndexRows(text, new Set(symbols.map((symbol) => symbol.toLowerCase())), market);
330
+ if (rows.length === 0)
331
+ throw sourceError(`Tencent returned no ${market} index quote rows for ${symbols.join(',')}`, 'tencent_empty_response');
332
+ return rows;
333
+ }
334
+ /** 美股 K 线入口需要的带后缀身份:先从快照拿上游自己的第 2 位(能力侧用,形状判据同一条)。 */
335
+ export async function resolveUsKlineSymbol(ticker, signal) {
336
+ for (const symbol of usSymbolVariants(ticker)) {
337
+ const text = await tencentGetText(`${QUOTE_URL}${symbol}`, signal, 'gbk');
338
+ const [row] = snapshotRows(text, new Set([symbol.toLowerCase()]));
339
+ if (!row || row.fields.length < US_QUOTE_FIELDS)
340
+ continue;
341
+ const code = row.fields[2];
342
+ if (!/^[A-Z][A-Z.]{0,9}\.[A-Z]{1,3}$/.test(code))
343
+ throw sourceError(`Tencent returned an unreadable US code ${JSON.stringify(code)}`, 'tencent_invalid_response');
344
+ return `us${code}`;
345
+ }
346
+ throw new Error(`Tencent does not recognize US ticker ${ticker}; no spelling of it returned a snapshot to resolve an exchange`);
347
+ }
348
+ export function parseSmartbox(text) {
349
+ const match = /v_hint="([\s\S]*)"/.exec(text);
350
+ const body = match?.[1] ?? '';
351
+ if (body === 'N' || body.length === 0)
352
+ return [];
353
+ const candidates = [];
354
+ for (const entry of body.split('^')) {
355
+ const parts = unescapeHint(entry).split('~');
356
+ if (parts.length < 5)
357
+ continue;
358
+ const [market, code, name, pinyin, type] = parts;
359
+ if (!market || !code)
360
+ continue;
361
+ candidates.push({ market: market.toLowerCase(), code: code.toUpperCase(), name, pinyin, type });
362
+ }
363
+ return candidates;
364
+ }
365
+ /** 回包里的名称是 `\uXXXX` 字面转义(实测),解一次转义;解不动就原样返回,不编造名称。 */
366
+ function unescapeHint(value) {
367
+ if (!value.includes('\\u'))
368
+ return value;
369
+ try {
370
+ return value.replace(/\\u([\da-f]{4})/giu, (_match, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
371
+ }
372
+ catch {
373
+ return value;
374
+ }
375
+ }
376
+ /** smartbox 检索:市场段只能是上游认的单值(`hk` / `us` / `all`),列表形态实测失效。 */
377
+ export const SMARTBOX_MARKETS = ['all', 'hk', 'us'];
378
+ export async function fetchSmartbox(query, market, signal) {
379
+ if (!SMARTBOX_MARKETS.includes(market))
380
+ throw new Error(`smartbox market must be one of ${SMARTBOX_MARKETS.join('/')}; got ${JSON.stringify(market)}(列表形态实测回 N 哨兵,等于查无此票)`);
381
+ 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 正文是个埋着的坑。
384
+ const text = await tencentGetText(url, signal);
385
+ return parseSmartbox(text);
386
+ }
@@ -6,7 +6,7 @@
6
6
  * between validation and connect. Closing that window would require a custom
7
7
  * dispatcher; this module keeps the same explicit trade-off as dsh-search-first.
8
8
  */
9
- export declare const LOCAL_FETCH_CLIENT_VERSION = "2.6.2";
9
+ export declare const LOCAL_FETCH_CLIENT_VERSION = "2.6.4";
10
10
  export declare const LOCAL_FETCH_MAX_URL_LENGTH = 2048;
11
11
  export type LocalFetchErrorCode = 'ABORTED' | 'TIMEOUT' | 'INVALID_URL' | 'BLOCKED_URL' | 'DNS' | 'REDIRECT' | 'HTTP' | 'TOO_LARGE' | 'UNSUPPORTED_CONTENT_TYPE';
12
12
  export declare class LocalFetchError extends Error {
@@ -7,7 +7,7 @@
7
7
  * dispatcher; this module keeps the same explicit trade-off as dsh-search-first.
8
8
  */
9
9
  import { htmlToMarkdown, stripInvisibleText } from './html-markdown.js';
10
- export const LOCAL_FETCH_CLIENT_VERSION = '2.6.2';
10
+ export const LOCAL_FETCH_CLIENT_VERSION = '2.6.4';
11
11
  export const LOCAL_FETCH_MAX_URL_LENGTH = 2048;
12
12
  export class LocalFetchError extends Error {
13
13
  code;
@@ -52,7 +52,7 @@ export interface WindCallResult {
52
52
  * `test/chart-composition.test.mjs` 守着(会因漂移而失败),这里原本是写死的第二处,
53
53
  * 一次发版就得记得改两遍。所以本常量由测试一并核对(见 `test/wind-client.test.mjs`)。
54
54
  */
55
- export declare const WIND_CLIENT_VERSION = "2.6.2";
55
+ export declare const WIND_CLIENT_VERSION = "2.6.4";
56
56
  export interface WindClient {
57
57
  callTool(toolName: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<WindCallResult>;
58
58
  }
@@ -35,7 +35,7 @@ export const WIND_SERVER_ENDPOINTS = {
35
35
  * `test/chart-composition.test.mjs` 守着(会因漂移而失败),这里原本是写死的第二处,
36
36
  * 一次发版就得记得改两遍。所以本常量由测试一并核对(见 `test/wind-client.test.mjs`)。
37
37
  */
38
- export const WIND_CLIENT_VERSION = '2.6.2';
38
+ export const WIND_CLIENT_VERSION = '2.6.4';
39
39
  function errorText(error) {
40
40
  return error instanceof Error ? error.message : String(error);
41
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/capital-generation",
3
- "version": "2.6.2",
3
+ "version": "2.6.4",
4
4
  "description": "Capital Generation:面向中国散户的 AI 投资助手插件(Capital mode Agent Preset)",
5
5
  "icon": "assets/cg-icon.png",
6
6
  "author": "v587d",