@v587d/capital-generation 2.6.3 → 2.6.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/CHANGELOG.md +86 -0
- package/README.md +18 -7
- package/capital-config/client.js +8 -2
- package/capital-config/client.src.cjs +74 -1
- package/capital-config/package.json +1 -1
- package/capital-watchlist/client.js +56 -19
- package/capital-watchlist/client.src.cjs +195 -44
- package/capital-watchlist/index.js +73 -22
- package/capital-watchlist/package.json +1 -1
- package/chart-ui/package.json +1 -1
- package/lib/web-retriever/local-fetch.d.ts +1 -1
- package/lib/web-retriever/local-fetch.js +1 -1
- package/lib/web-retriever/wind-client.d.ts +1 -1
- package/lib/web-retriever/wind-client.js +1 -1
- package/package.json +2 -1
|
@@ -131,9 +131,21 @@ const zh = {
|
|
|
131
131
|
confirmDescSuffix: ' 从自选中移除吗?',
|
|
132
132
|
confirmCancel: '取消',
|
|
133
133
|
confirmOk: '确认移除',
|
|
134
|
-
/**
|
|
135
|
-
*
|
|
136
|
-
|
|
134
|
+
/**
|
|
135
|
+
* `truncated`:候选已经**一行不落地全画出来了**(服务端不切片),所以这句里没有"候选不止这些"
|
|
136
|
+
* ——那半句说的是"还有没显示的",此刻是假话。2026-10-08 用户改写成"没有更多":它讲的是
|
|
137
|
+
* **上游那一路回满了自己那一档**(A 股 50 / 港 10 / 美 10),而它自己就住在滚动区末尾,
|
|
138
|
+
* 滚到底才看得见(不做成钉在底部的常驻条)。也**故意不写 N**:数字的上游是宿主常量,
|
|
139
|
+
* 抄进两套字典就迟早只改红一边(`error.list_full` 那条教训)。
|
|
140
|
+
*/
|
|
141
|
+
truncated: '没有更多,请输入准确的证券代码或名称。',
|
|
142
|
+
/**
|
|
143
|
+
* 候选下拉的筛选头(全部 / CN / HK / US,默认全部):**只在客户端筛已经拿全的候选**,
|
|
144
|
+
* 不重新出网、也不改变上游入参(`asset_type` 白名单那条纪律讲的是入参收窄,不是这里的显示筛选)。
|
|
145
|
+
*/
|
|
146
|
+
filterAll: '全部',
|
|
147
|
+
filterLabel: '按市场筛选候选',
|
|
148
|
+
filterNoHit: '这个市场下没有匹配的标的。',
|
|
137
149
|
noHit: '没有匹配的标的(A股 / 港股 / 美股,含指数与场内基金)。',
|
|
138
150
|
'error.invalid_query': '输入不被接受',
|
|
139
151
|
'error.not_found': '没有找到该标的',
|
|
@@ -214,7 +226,10 @@ const en = {
|
|
|
214
226
|
confirmDescSuffix: ' from your watchlist?',
|
|
215
227
|
confirmCancel: 'Cancel',
|
|
216
228
|
confirmOk: 'Remove',
|
|
217
|
-
truncated: '
|
|
229
|
+
truncated: 'No more candidates — enter an accurate code or name.',
|
|
230
|
+
filterAll: 'All',
|
|
231
|
+
filterLabel: 'Filter candidates by market',
|
|
232
|
+
filterNoHit: 'No matching symbol in this market.',
|
|
218
233
|
noHit: 'No matching symbol (A-share / HK / US, including indices and listed funds).',
|
|
219
234
|
'error.invalid_query': 'Input not accepted',
|
|
220
235
|
'error.not_found': 'Symbol not found',
|
|
@@ -316,6 +331,16 @@ function cleanRows(rows) {
|
|
|
316
331
|
'us-index': 'capital-watchlist-tag-us',
|
|
317
332
|
}
|
|
318
333
|
|
|
334
|
+
/**
|
|
335
|
+
* 候选筛选头的四档(2026-10-08 用户点名:**默认「全部」**,三市都摆出来让用户自己筛)。
|
|
336
|
+
* 只在客户端筛**已经拿全的候选**:A 股 50 + 港 10 + 美 10 一次回包,切换不重新出网,
|
|
337
|
+
* 也不碰上游入参(`asset_type` 白名单那条纪律讲的是入参收窄,与这里的显示筛选是两件事)。
|
|
338
|
+
* `marketIdOf` 是这一层唯一的判据:候选的 `exchange` 存的是**市场**(`SH`/`SZ`/`HK`/`US`,
|
|
339
|
+
* 美股的具体交易所留在 canonical 代码里),所以 A 股那一档由"不是 HK 也不是 US"给出。
|
|
340
|
+
*/
|
|
341
|
+
const MARKET_FILTERS = [{ id: 'all' }, { id: 'cn', label: 'CN' }, { id: 'hk', label: 'HK' }, { id: 'us', label: 'US' }]
|
|
342
|
+
const marketIdOf = (candidate) => (candidate.exchange === 'HK' ? 'hk' : candidate.exchange === 'US' ? 'us' : 'cn')
|
|
343
|
+
|
|
319
344
|
/**
|
|
320
345
|
* 面板几何。
|
|
321
346
|
*
|
|
@@ -380,23 +405,54 @@ const CSS = `
|
|
|
380
405
|
直接透视上来(官方滚动浮层 .media 面板与 MenuSurface .material 都是这一对)。border: 0 +
|
|
381
406
|
elevation-prominent 分离;描边浅色重绑 l1,深色由宿主 [data-menu-material] 规则翻成 l3。 */
|
|
382
407
|
/* 候选下拉是**卡片内**的浮层,而卡片 overflow: hidden(圆角与清单滚动都靠它):浮层一旦比
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
.capital-watchlist-
|
|
408
|
+
卡片剩余的空间高,多出来的部分就被无声裁掉。所以它的 max-height 必须与下面
|
|
409
|
+
capital-watchlist-list-open 那格的 min-height **同值**(下拉正好铺满清单那一格:够高,又压不到
|
|
410
|
+
页脚)——246px → **320px**(2026-10-08 用户点名"一屏只能显示 3 个标的":行高压到约 47px、
|
|
411
|
+
浮层抬到 320,一屏 5~6 行;这两个数一改必须一起改,测试里有同值断言)。
|
|
412
|
+
这一层自己不滚:里面 .capital-watchlist-options 才是滚动区——筛选头钉在顶上("锁定这个头"),
|
|
413
|
+
"没有更多"那句住在**滚动区末尾**(滚到底才看得见,不冻结在底部),其余 hint 贴在选项之后。
|
|
414
|
+
候选池是 50+10+10=70 行(三路上游硬顶之和,服务端不切片)。 */
|
|
415
|
+
.capital-watchlist-dropdown { position: absolute; top: calc(100% - 8px); left: 20px; right: 20px; z-index: 20; display: flex; flex-direction: column; max-height: 320px; overflow: hidden; background: var(--dsw-specific-menu); backdrop-filter: var(--dsw-menu-backdrop-filter); border: 0; border-radius: 12px; --dsw-elevation-stroke-color: var(--dsw-alias-border-l1); box-shadow: var(--dsw-elevation-prominent); animation: capital-watchlist-drop .18s var(--ds-ease-in-out); }
|
|
416
|
+
/* 筛选头与那几句"此刻怎么了"的 hint 都是 flex: none,只有选项层 flex: 0 1 auto 可收缩、可滚:
|
|
417
|
+
头永远在顶,hint 在选项之后(内容短时紧贴最后一行,内容长时也不会被挤没)。 */
|
|
418
|
+
.capital-watchlist-filter { flex: none; display: flex; align-items: center; gap: 6px; padding: 6px 10px; border-bottom: .5px solid var(--dsw-alias-border-l2); }
|
|
419
|
+
.capital-watchlist-options { flex: 0 1 auto; min-height: 0; overflow-y: auto; }
|
|
420
|
+
.capital-watchlist-filterbtn { display: inline-flex; align-items: center; gap: 4px; padding: 3px 9px; border: none; border-radius: 999px; corner-shape: round; background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-secondary); font-family: inherit; font-size: 12px; line-height: 16px; cursor: pointer; }
|
|
421
|
+
.capital-watchlist-filterbtn:hover { background: var(--dsw-alias-interactive-bg-hover-accent); color: var(--dsw-alias-label-primary); }
|
|
422
|
+
/* 选中态读 aria-pressed:状态住在属性里,样式只是它的投影(读屏也听得见"已选中")。
|
|
423
|
+
压在半透明菜单材质上用 color-mix 的透明淡染——它不写背景实底,底下的菜单模糊照常透上来。 */
|
|
424
|
+
.capital-watchlist-filterbtn[aria-pressed="true"] { background: color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent); color: var(--dsw-alias-label-primary); }
|
|
425
|
+
.capital-watchlist-filtercount { font-size: 11px; color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; }
|
|
388
426
|
@keyframes capital-watchlist-drop { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: translateY(0); } }
|
|
389
427
|
@keyframes capital-watchlist-breathe { 0% { opacity: 1; } 40% { opacity: .6; } 80%, 100% { opacity: 1; } }
|
|
390
428
|
@media (prefers-reduced-motion: reduce) { .capital-watchlist-dropdown { animation: none; } .capital-watchlist-skeleton { animation: none; } }
|
|
391
|
-
|
|
429
|
+
/* 候选行高度(2026-10-08 用户点名"减少每个标的展示高度"):padding 6 + 名称 18 + 间隙 1 + 代码行 15
|
|
430
|
+
≈ 47px。与上面那条"浮层 320px"一起算:(320 − 筛选头约 32) ÷ 47 ≈ 6 行一屏(改前是 3 行)。
|
|
431
|
+
两个数是一对——只抬浮层不压行高,多出来的空间会被行高吃掉;只压行高不抬浮层,一屏还是 3 行。 */
|
|
432
|
+
.capital-watchlist-option { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 12px; border: none; border-bottom: .5px solid var(--dsw-alias-border-l2); background: transparent; color: inherit; font-family: inherit; text-align: left; cursor: pointer; }
|
|
392
433
|
.capital-watchlist-option:last-of-type { border-bottom: none; }
|
|
393
|
-
|
|
394
|
-
|
|
434
|
+
/* 「已添加」与"在途"这一档读的是 **aria-disabled 而不是 :disabled**(真 disabled 会把焦点踢到
|
|
435
|
+
body,搜索段的 onBlur 就判"点到框外"收起下拉 —— 见渲染处那段),所以视觉降档也跟着读它:
|
|
436
|
+
整行 opacity .45、指针 not-allowed,焦点照旧留在这一行。 */
|
|
437
|
+
.capital-watchlist-option:hover:not([aria-disabled="true"]) { background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent); }
|
|
438
|
+
.capital-watchlist-option[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }
|
|
395
439
|
/* 键盘焦点走官方表达式:环色由宿主按主题与输入模态管好(指针模态自动透明),不自己造环。 */
|
|
396
|
-
.capital-watchlist-close:focus-visible, .capital-watchlist-clear:focus-visible, .capital-watchlist-option:focus-visible, .capital-watchlist-more:focus-visible, .capital-watchlist-predict:focus-visible, .capital-watchlist-review:focus-visible, .capital-watchlist-refresh:focus-visible, .capital-watchlist-confirmbtn:focus-visible { outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 1px; }
|
|
397
|
-
.capital-watchlist-optionmain { display: flex; flex-direction: column; gap:
|
|
398
|
-
.capital-watchlist-optionname { font-size: 14px; font-weight: 500; color: var(--dsw-alias-label-primary); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
399
|
-
.capital-watchlist-optioncode { font-size: 12px; color: var(--dsw-alias-label-tertiary); }
|
|
440
|
+
.capital-watchlist-close:focus-visible, .capital-watchlist-clear:focus-visible, .capital-watchlist-option:focus-visible, .capital-watchlist-filterbtn:focus-visible, .capital-watchlist-more:focus-visible, .capital-watchlist-predict:focus-visible, .capital-watchlist-review:focus-visible, .capital-watchlist-refresh:focus-visible, .capital-watchlist-confirmbtn:focus-visible { outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 1px; }
|
|
441
|
+
.capital-watchlist-optionmain { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
|
442
|
+
.capital-watchlist-optionname { font-size: 14px; line-height: 18px; font-weight: 500; color: var(--dsw-alias-label-primary); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
443
|
+
.capital-watchlist-optioncode { font-size: 12px; line-height: 15px; color: var(--dsw-alias-label-tertiary); }
|
|
444
|
+
/* 右侧那颗「添加」是**整行这个 button 的可见动词**,不是第二个可聚焦控件(button 里套 button
|
|
445
|
+
是非法 HTML):所以它只是一颗胶囊——常态灰底、hover 跟着整行加深;「已添加」那档由整行 disabled
|
|
446
|
+
的 opacity 统一降下去,不再往代码行里塞「· 已添加」(代码 + 徽标才是第二行要读的两样)。 */
|
|
447
|
+
/* 右侧那颗「添加」是**整行这个 button 的可见动词**,不是第二个可聚焦控件(button 里套 button
|
|
448
|
+
是非法 HTML)。两态(2026-10-08 第三轮用户点名):
|
|
449
|
+
- **添加 = 醒目**:沿用「刷新报价」那颗 verb 按钮的配方(state 色 12% 淡底 + 25% 描边 + 同色字),
|
|
450
|
+
整行里唯一带色的元素就是它,一眼知道点哪儿;
|
|
451
|
+
- **已添加 = 暗**:透明底 + l1 描边 + tertiary 字(data-state="added"),跟整行的 aria-disabled
|
|
452
|
+
一起降下去,并且点了也不再出网(guard 在渲染处)。 */
|
|
453
|
+
.capital-watchlist-addbtn { flex: none; padding: 3px 9px; border: 1px solid color-mix(in srgb, var(--dsw-alias-state-business-primary) 25%, transparent); border-radius: 999px; corner-shape: round; background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 12%, transparent); color: var(--dsw-alias-state-business-primary); font-size: 12px; line-height: 15px; font-weight: 500; }
|
|
454
|
+
.capital-watchlist-option:hover:not([aria-disabled="true"]) .capital-watchlist-addbtn { background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 20%, transparent); }
|
|
455
|
+
.capital-watchlist-addbtn[data-state="added"] { border-color: var(--dsw-alias-border-l1); background: transparent; color: var(--dsw-alias-label-tertiary); font-weight: 400; }
|
|
400
456
|
.capital-watchlist-tag { flex: none; display: inline-block; padding: 2px 7px; border-radius: 6px; font-size: 11px; line-height: 14px; font-weight: 500; }
|
|
401
457
|
/* 类型徽标 = 官方状态徽标那对写法:底色用 state-*-tertiary(主题包为"状态色的底"准备的实心档,
|
|
402
458
|
自己 color-mix 出来的透明档压在白卡片上等于没混),文字用同色系 primary;
|
|
@@ -424,9 +480,13 @@ const CSS = `
|
|
|
424
480
|
这一格装着这一行的三个动作:「预测」「复盘」与「更多」,表头贴的就是它们共同那条右缘。 */
|
|
425
481
|
.capital-watchlist-headaction { text-align: right; }
|
|
426
482
|
.capital-watchlist-list { max-height: 300px; padding: 0 12px 8px; overflow-y: auto; }
|
|
427
|
-
/* 下拉打开时把这一格撑到浮层的最大高度(见上面 capital-watchlist-dropdown
|
|
428
|
-
|
|
429
|
-
|
|
483
|
+
/* 下拉打开时把这一格撑到浮层的最大高度(见上面 capital-watchlist-dropdown 那一段的理由):
|
|
484
|
+
**这两个数必须同值**——一个改了另一个不改,下拉就会被卡片裁掉一截(或者清单被顶出卡片外)。
|
|
485
|
+
2026-10-08 一起从 246 抬到 320(用户点名"一屏只能显示 3 个标的"),测试里有同值断言。
|
|
486
|
+
只在打开时撑:平时 1-2 行的短清单不该平白多出一截空白。
|
|
487
|
+
⚠️ 打开时 min-height(320) > 上面那条 max-height(300),而 CSS 里 min 恒胜 max ⇒ 这一格此时按
|
|
488
|
+
320 高、内部照旧 overflow-y: auto 自己滚;收起后 max-height 300 恢复生效。 */
|
|
489
|
+
.capital-watchlist-list-open { min-height: 320px; }
|
|
430
490
|
.capital-watchlist-row { padding: 11px 8px; border-radius: 10px; transition: background .15s var(--ds-ease-in-out); }
|
|
431
491
|
.capital-watchlist-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
432
492
|
.capital-watchlist-namewrap { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
|
@@ -490,7 +550,9 @@ const CSS = `
|
|
|
490
550
|
.capital-watchlist-refresh:disabled { opacity: .45; cursor: not-allowed; }
|
|
491
551
|
/* 34px = 刷新按钮的高度:与它同一行的时间按按钮高度行高居中,左列多出失败行也不受影响。 */
|
|
492
552
|
.capital-watchlist-updated { font-size: 12px; line-height: 34px; color: var(--dsw-alias-label-tertiary); }
|
|
493
|
-
|
|
553
|
+
/* flex: none:2026-10-08 起 hint 是下拉这个 flex 列的直接子元素(滚动区只剩选项那一层),
|
|
554
|
+
不钉住的话空间一紧它就会被压扁——hint 没有 overflow,压扁就是文字被裁。 */
|
|
555
|
+
.capital-watchlist-hint { flex: none; padding: 8px 14px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
|
|
494
556
|
/* 黄 = 动作没成。两条黄字都用 warn-label:官方"状态色用作文字"给的是 label 那一档,
|
|
495
557
|
warn-primary 压在浅底上只有 2:1,报错文字反而是最不该看不见的一行。 */
|
|
496
558
|
/* 搜索失败:占下拉的 hint 位(与「搜索中…」「没有匹配的标的」同一个槽,替换而非并列)。
|
|
@@ -581,6 +643,12 @@ const blank = () => ({
|
|
|
581
643
|
items: [],
|
|
582
644
|
candidates: [],
|
|
583
645
|
truncated: false,
|
|
646
|
+
/**
|
|
647
|
+
* 候选筛选头(`all` / `cn` / `hk` / `us`,**默认 `all`**)。住 store 而不是组件 `useState`:
|
|
648
|
+
* 两条复位路径各归各的——面板重开(`open()` 的 `...blank()`)回到「全部」;换词**不**掰回
|
|
649
|
+
* (用户筛了港,下一个词他还想看港),清空输入也留着(下拉收起时它本来看不见)。
|
|
650
|
+
*/
|
|
651
|
+
marketFilter: 'all',
|
|
584
652
|
query: '',
|
|
585
653
|
/** 下拉是否盖在清单上:由输入打开、由选中/点到框外收起(Esc 也收,见组件的 onKeyDown)。 */
|
|
586
654
|
dropdownOpen: false,
|
|
@@ -770,6 +838,16 @@ class WatchlistSurface {
|
|
|
770
838
|
this.patch({ dropdownOpen: false })
|
|
771
839
|
}
|
|
772
840
|
|
|
841
|
+
/**
|
|
842
|
+
* 筛选头(全部 / CN / HK / US):只换**显示**哪几行,不出网——候选是一次拿全的,
|
|
843
|
+
* 切档也不该把下拉收起(它就在下拉顶上)。
|
|
844
|
+
* 未知档名直接忽略:这一格的取值只有 `MARKET_FILTERS` 那四个,拼错的值不许让下拉变空白。
|
|
845
|
+
*/
|
|
846
|
+
setMarketFilter(filter) {
|
|
847
|
+
if (!MARKET_FILTERS.some((entry) => entry.id === filter)) return
|
|
848
|
+
this.patch({ marketFilter: filter })
|
|
849
|
+
}
|
|
850
|
+
|
|
773
851
|
/** 行内「更多」键:再点一次就收起(同一个键既开又关,不用去别处找关闭入口)。 */
|
|
774
852
|
toggleRowMenu(thscode) {
|
|
775
853
|
this.patch({ rowMenu: this.snapshot.rowMenu === thscode ? null : thscode })
|
|
@@ -827,8 +905,16 @@ class WatchlistSurface {
|
|
|
827
905
|
// try 里:`/add` 已经落库、紧接着清单读回来 503,面板就报"自选股已达上限"式的黄字,
|
|
828
906
|
// 用户照着再点一次——同一个标的被加了第二遍)。读清单的失败归 `loadError`(列表那
|
|
829
907
|
// 一片),刷报价的失败归 `error`(页脚那条红字),各自有位置,都不占添加这句话。
|
|
830
|
-
|
|
831
|
-
|
|
908
|
+
// ⛔ 成功之后**不再收下拉、不清输入**(2026-10-08 用户点名"能在下拉框里连续添加"):
|
|
909
|
+
// 收起 = 每加一条都得重敲一遍词,"连续添加"根本无从谈起。这一行当场标 `已添加` 并随行禁用
|
|
910
|
+
// (幂等,点不进第二次),其余候选、筛选档与输入框里的词都原样留着。`lastQuery` 因此保持原值
|
|
911
|
+
// ⇒ 同一个词不会被重发(三条闸里"同一个词不重发"那条正是靠它)。
|
|
912
|
+
// ⛔ `searchError` / `searchPartial` 也**不清**:它们说的是"这次搜索有一路没参与",下拉还开着,
|
|
913
|
+
// 把真话擦掉就是让它失效。
|
|
914
|
+
this.patch({
|
|
915
|
+
addError: null,
|
|
916
|
+
candidates: this.snapshot.candidates.map((row) => (row.thscode === candidate.thscode ? { ...row, in_list: true } : row)),
|
|
917
|
+
})
|
|
832
918
|
await this.load().catch(() => {})
|
|
833
919
|
// 添加是一次完整动作:读完清单顺手刷一次报价,新行当场就有价。停在「未刷新」
|
|
834
920
|
// 等于把用户刚加的东西留成半成品——开面板那一步本来就是同一套 load→refresh。
|
|
@@ -990,6 +1076,12 @@ function addErrorText(t, addError) {
|
|
|
990
1076
|
function WatchlistDialog({ useDialog, surface, t, inputActions }) {
|
|
991
1077
|
const state = useDialog((value) => value)
|
|
992
1078
|
const [busy, setBusy] = useState(false)
|
|
1079
|
+
/**
|
|
1080
|
+
* 在途的那一次「添加」落在哪一行(null = 没有)。⛔ 与 `busy` 分开:`busy` 关的是**清单那半边**
|
|
1081
|
+
* (预测 / 复盘 / 更多 / 刷新按钮),候选行只许禁它自己那一条——全禁的话,"连续添加"会被每一次
|
|
1082
|
+
* 在途请求打断(第二条正灰着、点不进去)。并发添加是安全的:服务端把清单读写串在一条队列上。
|
|
1083
|
+
*/
|
|
1084
|
+
const [addingThscode, setAddingThscode] = useState(null)
|
|
993
1085
|
/**
|
|
994
1086
|
* 待移除的那一行,走面板内的二次确认层——不是 `window.confirm`:那是宿主窗口级别的系统弹窗,
|
|
995
1087
|
* 样式与语言都不归插件管,而且会把"从自选中移除"这件事从面板里搬出去。
|
|
@@ -1046,28 +1138,84 @@ function WatchlistDialog({ useDialog, surface, t, inputActions }) {
|
|
|
1046
1138
|
surface.close()
|
|
1047
1139
|
}
|
|
1048
1140
|
|
|
1049
|
-
|
|
1141
|
+
/**
|
|
1142
|
+
* 筛选头只筛**已经拿全**的候选(A 股 50 + 港 10 + 美 10 一次回包):切档不出网、也不收下拉。
|
|
1143
|
+
* 计数按市场算,「全部」那一档是三市之和——用户那句"怎么全是 A 股,港股美股呢",答案就写在
|
|
1144
|
+
* 这三个数字里(港 10 / 美 10,不是 0),不用他逐档点过去找。档名 `CN` / `HK` / `US` 中英文同一套。
|
|
1145
|
+
*/
|
|
1146
|
+
const visibleCandidates = state.marketFilter === 'all'
|
|
1147
|
+
? state.candidates
|
|
1148
|
+
: state.candidates.filter((candidate) => marketIdOf(candidate) === state.marketFilter)
|
|
1149
|
+
const filterCounts = { all: state.candidates.length, cn: 0, hk: 0, us: 0 }
|
|
1150
|
+
for (const candidate of state.candidates) filterCounts[marketIdOf(candidate)] += 1
|
|
1151
|
+
const filterBar = state.candidates.length === 0
|
|
1050
1152
|
? null
|
|
1051
|
-
: h('div', { className: 'capital-watchlist-
|
|
1052
|
-
|
|
1053
|
-
key:
|
|
1153
|
+
: h('div', { className: 'capital-watchlist-filter', role: 'group', 'aria-label': t('filterLabel') },
|
|
1154
|
+
MARKET_FILTERS.map((entry) => h('button', {
|
|
1155
|
+
key: entry.id,
|
|
1054
1156
|
type: 'button',
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
title: candidate.in_list === true ? t('added') : t('add'),
|
|
1060
|
-
disabled: candidate.in_list === true || busy,
|
|
1061
|
-
onClick: () => { setBusy(true); surface.add(candidate).finally(() => setBusy(false)) },
|
|
1157
|
+
className: 'capital-watchlist-filterbtn',
|
|
1158
|
+
// 选中态只写在 `aria-pressed` 上、样式读它:状态先进属性,读屏也听得见"已选中"。
|
|
1159
|
+
'aria-pressed': state.marketFilter === entry.id,
|
|
1160
|
+
onClick: () => surface.setMarketFilter(entry.id),
|
|
1062
1161
|
},
|
|
1063
|
-
h('span', { className: 'capital-watchlist-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1162
|
+
h('span', { className: 'capital-watchlist-filterlabel' }, entry.id === 'all' ? t('filterAll') : entry.label),
|
|
1163
|
+
h('span', { className: 'capital-watchlist-filtercount' }, String(filterCounts[entry.id])))))
|
|
1164
|
+
|
|
1165
|
+
const dropdown = !showDropdown
|
|
1166
|
+
? null
|
|
1167
|
+
: h('div', { className: 'capital-watchlist-dropdown', 'aria-label': t('title'), 'data-menu-material': 'translucent' },
|
|
1168
|
+
// 筛选头**不进滚动区**:`role="listbox"` 因此跟着选项搬到里面那层(列表框里只许摆 option,
|
|
1169
|
+
// 头自己是 group),滚动的也只有选项那一层——头由 CSS 钉在下拉顶部,即用户要的"锁定这个头"。
|
|
1170
|
+
filterBar,
|
|
1171
|
+
h('div', { className: 'capital-watchlist-options', role: 'listbox', 'aria-label': t('title') },
|
|
1172
|
+
visibleCandidates.map((candidate) => h('button', {
|
|
1173
|
+
key: candidate.thscode,
|
|
1174
|
+
type: 'button',
|
|
1175
|
+
role: 'option',
|
|
1176
|
+
className: 'capital-watchlist-option',
|
|
1177
|
+
// hover 出来的是**证券全称**(2026-10-08 用户点名):旧版这一格写的是 `添加`,那句
|
|
1178
|
+
// "这个按钮能干嘛"已经由右侧那颗「添加」说了;美股长名会被 CSS 截断,全称只有这个
|
|
1179
|
+
// 原生 tooltip 给得全。
|
|
1180
|
+
title: candidate.name,
|
|
1181
|
+
// ⛔ 这一格用 `aria-disabled` 而不是真 `disabled`(2026-10-08 第三轮用户点名"点一下添加、
|
|
1182
|
+
// 下拉就没了"):真 `disabled` 会让浏览器**立刻把焦点从这一行踢到 body**,搜索段的 onBlur
|
|
1183
|
+
// 于是拿到 `relatedTarget: null` → 判"点到框外" → 收起下拉,连续添加被这一下打断。
|
|
1184
|
+
// `aria-disabled` 不动焦点(行照样可聚焦,读屏也听得见"不可用"),"禁止点击"由下面的 guard 承担。
|
|
1185
|
+
'aria-disabled': candidate.in_list === true || addingThscode === candidate.thscode,
|
|
1186
|
+
onClick: () => {
|
|
1187
|
+
// 幂等 + 防在途双击:已添加的那条点多少次都不再出网,在途那条也拦掉。
|
|
1188
|
+
if (candidate.in_list === true || addingThscode === candidate.thscode) return
|
|
1189
|
+
setBusy(true)
|
|
1190
|
+
setAddingThscode(candidate.thscode)
|
|
1191
|
+
surface.add(candidate).finally(() => { setBusy(false); setAddingThscode(null) })
|
|
1192
|
+
},
|
|
1193
|
+
},
|
|
1194
|
+
h('span', { className: 'capital-watchlist-optionmain' },
|
|
1195
|
+
h('span', { className: 'capital-watchlist-optionname' }, candidate.name),
|
|
1196
|
+
// ⛔ 第二行画**完整 canonical 代码**,不是裸码:`00700` 一次同时命中港个股与
|
|
1197
|
+
// `000700.SZ` 模塑科技,搜「恒生指数」还会同时回 `HSI` / `HSIGTR` / `HSINTR` 三条真指数——
|
|
1198
|
+
// 裸码并列时用户没有可核对的信息,点错就是加错票。
|
|
1199
|
+
// 类型徽标挪到代码右边(`codeline` 直接复用清单第一列那个类 ⇒ 排法与清单同款,用户
|
|
1200
|
+
// 2026-10-08 点名"类似自选股列表第一列的样式"),右侧因此腾出来放「添加」。
|
|
1201
|
+
h('span', { className: 'capital-watchlist-codeline' },
|
|
1202
|
+
h('span', { className: 'capital-watchlist-optioncode' }, candidate.thscode),
|
|
1203
|
+
h(TypeTag, { assetType: candidate.asset_type, t }))),
|
|
1204
|
+
// 右侧那颗「添加」是**整行这个 button 的可见动词**,不是第二个可聚焦控件(button 里再
|
|
1205
|
+
// 套 button 是非法 HTML);已在清单里就换 `已添加`,随行一起禁用(点了也不会重复写库)。
|
|
1206
|
+
// 两态(2026-10-08 第三轮用户点名):**添加 = 醒目**(沿用「刷新报价」那颗 verb 按钮的配方,
|
|
1207
|
+
// 整行里唯一带色的就是它 ⇒ 一眼知道点哪儿)、**已添加 = 暗**(透明底 + l1 描边 + tertiary 字,
|
|
1208
|
+
// 配合整行 aria-disabled 的 opacity 一起降)。状态只写在 `data-state` 上,样式读它。
|
|
1209
|
+
h('span', { className: 'capital-watchlist-addbtn', 'data-state': candidate.in_list === true ? 'added' : 'idle' },
|
|
1210
|
+
candidate.in_list === true ? t('added') : t('add')))),
|
|
1211
|
+
// ⛔ "没有更多"那句住在**滚动区末尾**(2026-10-08 用户点名"不要冻结"):它跟着内容一起滚,
|
|
1212
|
+
// 滚到底才看得见——钉在浮层底部就成了一条常驻横幅,还挡着最后一行候选。
|
|
1213
|
+
// 筛选档里一行都不剩时不说它(那一格由下面的 `filterNoHit` 负责)。
|
|
1214
|
+
state.truncated === true && visibleCandidates.length > 0
|
|
1215
|
+
? h('div', { className: 'capital-watchlist-hint capital-watchlist-endhint' }, t('truncated'))
|
|
1216
|
+
: null),
|
|
1217
|
+
// 下面这几句 hint **不进滚动区**:它们说的是"这一屏此刻怎么了"(正在查 / 查不到 / 有路挂了),
|
|
1218
|
+
// 被内容滚走就等于没人看见——所以留在浮层里、贴在选项之后。
|
|
1071
1219
|
state.composing === true
|
|
1072
1220
|
// ⛔ 合成期(拼音还没上屏)不许报「没有匹配的标的」:那是把"还没查"说成"查了,没有",
|
|
1073
1221
|
// 用户会以为自己的输入法打错了字。这一格说的是下一步会发生什么。
|
|
@@ -1082,7 +1230,11 @@ function WatchlistDialog({ useDialog, surface, t, inputActions }) {
|
|
|
1082
1230
|
? h('div', { className: 'capital-watchlist-hint capital-watchlist-searcherror', role: 'alert', 'data-code': state.searchError.code }, t(`error.${state.searchError.code}`))
|
|
1083
1231
|
: state.candidates.length === 0
|
|
1084
1232
|
? h('div', { className: 'capital-watchlist-hint' }, t('noHit'))
|
|
1085
|
-
:
|
|
1233
|
+
: visibleCandidates.length === 0
|
|
1234
|
+
// 筛选把候选滤空了(总数 > 0):这句与 noHit 说的是两件事——票是有的,只是不在
|
|
1235
|
+
// 这一档里。说成"没有匹配"就是把用户的筛选说成他查错了。
|
|
1236
|
+
? h('div', { className: 'capital-watchlist-hint', 'data-filter': state.marketFilter }, t('filterNoHit'))
|
|
1237
|
+
: null,
|
|
1086
1238
|
// 某一路挂了而其余路照常给候选:这一行说的是**那一路**(R12)。它与上面的 hint 各说
|
|
1087
1239
|
// 各的事——上面那句讲"这一屏有没有匹到",这句讲"有一路今天没参与",不互相顶替。
|
|
1088
1240
|
...(state.searchPartial ?? []).map((branch) => h('div', {
|
|
@@ -1091,8 +1243,7 @@ function WatchlistDialog({ useDialog, surface, t, inputActions }) {
|
|
|
1091
1243
|
role: 'status',
|
|
1092
1244
|
'data-market': branch.market,
|
|
1093
1245
|
'data-code': branch.code,
|
|
1094
|
-
}, `${t(`partial.${branch.market}`)}${t(`error.${branch.code}`)}`))
|
|
1095
|
-
state.truncated ? h('div', { className: 'capital-watchlist-hint' }, t('truncated')) : null)
|
|
1246
|
+
}, `${t(`partial.${branch.market}`)}${t(`error.${branch.code}`)}`)))
|
|
1096
1247
|
|
|
1097
1248
|
const head = state.items.length === 0
|
|
1098
1249
|
? null
|
|
@@ -50,15 +50,20 @@ export const DOMAIN_NAME = 'capital_watchlist'
|
|
|
50
50
|
export const MAX_ITEMS = 30
|
|
51
51
|
/** ETF 扇出的并发路数:30 个单只请求压成约 8 波,正常网络一次刷新几秒内落地。 */
|
|
52
52
|
export const REFRESH_CONCURRENCY = 4
|
|
53
|
-
/** 搜索候选上限(一屏给这么多,倒逼用户缩小输入,不做分页)。 */
|
|
54
|
-
export const SEARCH_LIMIT = 20
|
|
55
53
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
54
|
+
* 每一路自己能拿到几条——**这就是候选池的全部**(2026-10-08 起不再做显示切片:上游没有第二页,
|
|
55
|
+
* 用户点名"既然翻不了页,就直接放出来"):`50 + 10 + 10 = 70` 是三路上游硬顶之和,天然有界,
|
|
56
|
+
* 不会出现"搜个词回几千行"。
|
|
57
|
+
*
|
|
58
|
+
* `truncated` 那句"还有更多"**只按这一表判**:某一路回满自己那一档,就是上游那侧被砍了;
|
|
59
|
+
* 我们这边一行都没藏,所以那句 tip 里也不再有"候选不止这些"(只留下"请输入更准确的…"那半句)。
|
|
60
|
+
*
|
|
61
|
+
* 数字的来路:Fuyao `ticker_search` 的 `limit` 官方取值 1~50(默认 10),这里取到**官方上限**;
|
|
62
|
+
* smartbox 实测**硬顶 10 条**(`n=` / `count=` / `size=` / `limit=` / `max=` / `hits=` / `num=` /
|
|
63
|
+
* `rows=` 八种抬条数的写法全无效,分页的 `p=` / `page=` / `offset=` / `start=` / `from=` / `pn=` /
|
|
64
|
+
* `o=` 七种回包与基线**逐字节相同**;2026-10-07 与 10-08 两轮实测),多要也不会多给。
|
|
60
65
|
*/
|
|
61
|
-
export const SEARCH_BRANCH_LIMITS = { 'a-share':
|
|
66
|
+
export const SEARCH_BRANCH_LIMITS = { 'a-share': 50, hk: 10, us: 10 }
|
|
62
67
|
/** A 股那一路的搜索白名单(Fuyao 与报价端点双端都有可靠支撑);场外基金 / 北交所不进候选。
|
|
63
68
|
* 港美股那两路各有自己的白名单(`SMARTBOX_STOCK_TYPES` + `SMARTBOX_INDEX_TYPE`),互不通用。 */
|
|
64
69
|
export const SEARCH_ASSET_TYPES = 'a-share,a-share-index,fund-etf'
|
|
@@ -570,8 +575,10 @@ export function createWatchlistService(options = {}) {
|
|
|
570
575
|
}
|
|
571
576
|
|
|
572
577
|
/**
|
|
573
|
-
* 一次输入三路并发:Fuyao `ticker_search`
|
|
574
|
-
*
|
|
578
|
+
* 一次输入三路并发:Fuyao `ticker_search` **一笔**(`asset_type` 白名单照旧,`limit` 取**官方上限
|
|
579
|
+
* 50**——参数面 1~50,不是我们自己放宽的)\+ smartbox 两笔(`t=hk` / `t=us`,各硬顶 10 条)。
|
|
580
|
+
* 例外在 `searchAShare`:`q` 含 ASCII 小写字母时 A 股那一路**问两笔**(原样 + 全大写),
|
|
581
|
+
* 因为上游对 `q` 是区分大小写的子串匹配——那是"三路"之外唯一会多出一笔的地方。
|
|
575
582
|
* 为什么 smartbox 不共用一次 `t=all`:实测 `t=all&q=富通`
|
|
576
583
|
* 回 9 条 A 股 + 1 条 hk + **0 条 us**,`q=00700` 只回 1 条 hk——共用一次会让"美股这一档根本没进
|
|
577
584
|
* 候选"从偶发变成常态,那是"入口统一"最直接的反面(设计文档 P17 / §3.2)。
|
|
@@ -598,32 +605,76 @@ export function createWatchlistService(options = {}) {
|
|
|
598
605
|
const merged = mergeCandidates(branches.filter((branch) => branch.ok === true).flatMap((branch) => branch.rows), q)
|
|
599
606
|
const result = {
|
|
600
607
|
ok: true,
|
|
601
|
-
|
|
602
|
-
//
|
|
603
|
-
|
|
604
|
-
|
|
608
|
+
// **一行不落地全画出来**(2026-10-08 用户点名"既然没有翻页就直接放出来"):候选池的上界
|
|
609
|
+
// 由三路上游各自的硬顶定死(A 股 50 + 港 10 + 美 10 = 70),不需要再切一刀给"一屏"。
|
|
610
|
+
items: merged.map((row) => ({ ...row, in_list: items.get(row.thscode) !== undefined })),
|
|
611
|
+
// "还有更多"只剩一种 truthful 形态:**某一路回满了它自己那一档**(上游那侧被砍)。
|
|
612
|
+
// 合并后的总条数不再参与判断——我们一行没藏,条数多不等于还有没显示的;
|
|
613
|
+
// "回满自己那档"由每一路在**它自己的那一笔**上算好带回来(A 股可能问了两笔,两笔各 50
|
|
614
|
+
// 合起来 100 时我们只留 50,那也算被砍,`saturated` 里已经记了)。
|
|
615
|
+
truncated: branches.some((branch) => branch.ok === true && branch.saturated === true),
|
|
605
616
|
}
|
|
606
617
|
if (failures.length > 0) result.partial = failures.map((failure) => ({ market: failure.market, code: failure.code }))
|
|
607
618
|
return result
|
|
608
619
|
}
|
|
609
620
|
|
|
610
|
-
/**
|
|
621
|
+
/**
|
|
622
|
+
* A 股那一路。⛔ 不许拿港美股那两条白名单顺手再过滤一遍:这边已有 `asset_type` + 键形状两道闸。
|
|
623
|
+
*
|
|
624
|
+
* **大小写要问两遍**(2026-10-08 真上游实测):Fuyao 的 `q` 是区分大小写的子串匹配——
|
|
625
|
+
* `q=tcl` 0 条而 `q=TCL` 3 条、`q=etf` 0 条而 `q=ETF` 50 条、`q=a50` 0 条而 `q=A50` 50 条、
|
|
626
|
+
* `q=600519.sh` 0 条而 `q=600519.SH` 1 条。而全量 8708 条 A 股 / 指数 / ETF 名称里含**小写**
|
|
627
|
+
* 拉丁字母的只有 8 条、且全是 `.TI` 概念指数(下一行就过滤掉),含大写的 2092 条——
|
|
628
|
+
* 所以大写那一支是现实的主力,原样那一支是"不丢小写专有命中"的保险:`q` 与 `q.toUpperCase()`
|
|
629
|
+
* 不同时**两笔并发**,按 canonical 键去重、原样那份排前面(保持"上游顺序"的既有口径);
|
|
630
|
+
* 两者相同(数字 / 中文 / 已经全大写)就只发一笔,不白烧一次配额。
|
|
631
|
+
*
|
|
632
|
+
* `saturated` 讲的是"**这一路被砍在自己那一档**",按笔 + 按并集一起判:某一笔回满 50 是上游
|
|
633
|
+
* 砍的;两笔并起来超过 50 时我们自己也会砍一刀(见下),那同样是"还有没显示的"。反过来说,
|
|
634
|
+
* 两笔各 15 条、并起来 30 条并不说明上游还有货——按合并后的条数判就会凭空给一句假 tip。
|
|
635
|
+
*
|
|
636
|
+
* 半边失败:**拿到候选就算这一路答上了**——`partial` 那行的文案是"A股搜索不可用",
|
|
637
|
+
* 与同屏的 A 股候选并排是自相矛盾;一支候选都没拿到时才走整路失败,绝不把"这一支没答上来"
|
|
638
|
+
* 说成"查无此票"(大写那支挂掉时,我们并不知道该词在 A 股到底有没有票)。
|
|
639
|
+
*/
|
|
611
640
|
async function searchAShare(q) {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
641
|
+
const variants = [q, q.toUpperCase()].filter((value, index, all) => all.indexOf(value) === index)
|
|
642
|
+
const limit = SEARCH_BRANCH_LIMITS['a-share']
|
|
643
|
+
const outcomes = await Promise.all(variants.map(async (variant) => {
|
|
644
|
+
try {
|
|
645
|
+
const envelope = await callFuyao(ENDPOINTS.search, { q: variant, asset_type: SEARCH_ASSET_TYPES, limit }, ['q', 'asset_type', 'limit'])
|
|
646
|
+
// 后缀也要收口:实测 `q=宁德时代` 会带回 `885789.TI 宁德时代概念`(同花顺指数),
|
|
647
|
+
// 它既不在本版报价范围、也过不了域 schema——不挡掉就是"添加"那一下 500。
|
|
648
|
+
const rows = fuyaoItems(envelope).map(candidateOf).filter((row) => ASSET_TYPES.includes(row.asset_type) && normalizeThscode(row.thscode) !== undefined)
|
|
649
|
+
return { rows, saturated: rows.length >= limit, code: undefined }
|
|
650
|
+
} catch (error) {
|
|
651
|
+
return { rows: [], saturated: false, code: watchlistErrorCode(error) }
|
|
652
|
+
}
|
|
653
|
+
}))
|
|
654
|
+
const union = []
|
|
655
|
+
const seen = new Set()
|
|
656
|
+
for (const outcome of outcomes) {
|
|
657
|
+
for (const row of outcome.rows) {
|
|
658
|
+
if (seen.has(row.thscode)) continue
|
|
659
|
+
seen.add(row.thscode)
|
|
660
|
+
union.push(row)
|
|
661
|
+
}
|
|
619
662
|
}
|
|
663
|
+
const codes = outcomes.filter((outcome) => outcome.code !== undefined).map((outcome) => outcome.code)
|
|
664
|
+
if (union.length === 0 && codes.length > 0) return { ok: false, market: 'a-share', code: worstErrorCode(codes) }
|
|
665
|
+
// 并集也只留这一档的量:两笔各 50、几乎不重叠时 A 股这一路最多画 50 行,候选池上界因此是
|
|
666
|
+
// 50 + 10 + 10 = 70(`SEARCH_BRANCH_LIMITS` 那条注释里的算法)。自己砍掉的那几行同样算"被砍",
|
|
667
|
+
// 否则 truncated 会在还有没显示的行时说"就这些"。
|
|
668
|
+
const saturated = outcomes.some((outcome) => outcome.saturated) || union.length > limit
|
|
669
|
+
return { ok: true, market: 'a-share', rows: union.slice(0, limit), saturated }
|
|
620
670
|
}
|
|
621
671
|
|
|
622
672
|
/** smartbox 那两路之一:类型白名单 + 键形状一起收,衍生品(窝轮 `QZ`)不进候选。 */
|
|
623
673
|
async function searchOffshore(q, market) {
|
|
624
674
|
try {
|
|
625
675
|
const found = await fetchSmartbox(q, market, AbortSignal.timeout(timeoutMs))
|
|
626
|
-
|
|
676
|
+
const rows = found.map((row) => offshoreCandidateOf(row, market)).filter((row) => row !== undefined)
|
|
677
|
+
return { ok: true, market, rows, saturated: rows.length >= SEARCH_BRANCH_LIMITS[market] }
|
|
627
678
|
} catch (error) {
|
|
628
679
|
return { ok: false, market, code: watchlistErrorCode(error, { source: 'tencent' }) }
|
|
629
680
|
}
|
package/chart-ui/package.json
CHANGED
|
@@ -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.
|
|
9
|
+
export declare const LOCAL_FETCH_CLIENT_VERSION = "2.6.5";
|
|
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.
|
|
10
|
+
export const LOCAL_FETCH_CLIENT_VERSION = '2.6.5';
|
|
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.
|
|
55
|
+
export declare const WIND_CLIENT_VERSION = "2.6.5";
|
|
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.
|
|
38
|
+
export const WIND_CLIENT_VERSION = '2.6.5';
|
|
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.
|
|
3
|
+
"version": "2.6.5",
|
|
4
4
|
"description": "Capital Generation:面向中国散户的 AI 投资助手插件(Capital mode Agent Preset)",
|
|
5
5
|
"icon": "assets/cg-icon.png",
|
|
6
6
|
"author": "v587d",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"smoke:fuyao": "node scripts/smoke-fuyao.mjs",
|
|
62
62
|
"smoke:wind": "node scripts/smoke-wind.mjs",
|
|
63
63
|
"smoke:local-fetch": "node scripts/smoke-local-fetch.mjs",
|
|
64
|
+
"contract:probe": "node scripts/contract-probe.mjs",
|
|
64
65
|
"docs:capabilities": "npm run build && node scripts/gen-capability-table.mjs",
|
|
65
66
|
"verify:sessions": "node scripts/verify-sessions.mjs",
|
|
66
67
|
"audit:tool-errors": "node scripts/audit-tool-errors.mjs"
|