@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.
@@ -131,9 +131,21 @@ const zh = {
131
131
  confirmDescSuffix: ' 从自选中移除吗?',
132
132
  confirmCancel: '取消',
133
133
  confirmOk: '确认移除',
134
- /** 故意不写"超过 N 条"里那个 N:它上游是宿主的一个常量,抄进两套字典就迟早只改红一边
135
- * (`error.list_full` 那条教训)。用户下一步要做的动作跟数字无关。 */
136
- truncated: '候选不止这些,请输入更准确的证券代码或名称。',
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: 'More candidates exist than shown — enter a more exact code or name.',
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
- 卡片剩余的空间高,多出来的部分就被无声裁掉——实测默认播种 4 行裁掉约 50px,1 行时只剩两三条
384
- 可见,用户既看不到"下面还有",也够不着最后一条。所以最大高度收到 246px,并由下面
385
- capital-watchlist-list-open 在打开下拉时把清单那一格撑到同一高度:卡片因此总是够高,
386
- 超出 246px 的候选在这层自己的盒子里滚(SEARCH_LIMIT 是 10 条,滚得到底)。 */
387
- .capital-watchlist-dropdown { position: absolute; top: calc(100% - 8px); left: 20px; right: 20px; z-index: 20; max-height: 246px; overflow-y: auto; 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); }
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
- .capital-watchlist-option { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 14px; border: none; border-bottom: .5px solid var(--dsw-alias-border-l2); background: transparent; color: inherit; font-family: inherit; text-align: left; cursor: pointer; }
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
- .capital-watchlist-option:hover:not(:disabled) { background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent); }
394
- .capital-watchlist-option:disabled { opacity: .45; cursor: not-allowed; }
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: 2px; min-width: 0; }
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
- 只在打开时撑:平时 1-2 行的短清单不该平白多出一截空白。 */
429
- .capital-watchlist-list-open { min-height: 246px; }
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
- .capital-watchlist-hint { padding: 8px 14px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
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
- this.patch({ addError: null, candidates: [], query: '', dropdownOpen: false, pendingSearch: false, searchError: null, searchPartial: null })
831
- this.lastQuery = null
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
- const dropdown = !showDropdown
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-dropdown', role: 'listbox', 'aria-label': t('title'), 'data-menu-material': 'translucent' },
1052
- state.candidates.map((candidate) => h('button', {
1053
- key: candidate.thscode,
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
- role: 'option',
1056
- className: 'capital-watchlist-option',
1057
- // 整行就是"添加"这个动词(点行选中),title 把这个 affordance 说出来;
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-optionmain' },
1064
- h('span', { className: 'capital-watchlist-optionname' }, candidate.name),
1065
- // ⛔ 候选第二行画**完整 canonical 代码**,不是裸码:`00700` 一次同时命中港个股与
1066
- // `000700.SZ` 模塑科技,搜「恒生指数」还会同时回 `HSI` / `HSIGTR` / `HSINTR` 三条真指数——
1067
- // 裸码并列时用户没有可核对的信息,点错就是加错票。
1068
- h('span', { className: 'capital-watchlist-optioncode' },
1069
- candidate.in_list === true ? `${candidate.thscode} · ${t('added')}` : candidate.thscode)),
1070
- h(TypeTag, { assetType: candidate.asset_type, t }))),
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
- : null,
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
- * 每一路自己能拿到几条——`truncated` 那句"还有更多"按这一表判,不是按 `SEARCH_LIMIT` 判:
57
- * Fuyao `ticker_search` 的 `limit` 官方取值 1~50(默认 10),这里取到与显示上限齐平;
58
- * smartbox 实测**硬顶 10 条**(`&n=` / `&count=` / `&size=` / `&limit=` / `&max=` / `&hits=` 六种写法
59
- * 全无效,2026-10-07 逐条实测),多要也不会多给。所以某一路回满自己那一档,就是上游那侧被砍了。
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': 20, hk: 10, us: 10 }
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` 一笔(`asset_type` 白名单照旧,`limit` 随显示上限抬到
574
- * 20——官方参数面 1~50,不是我们自己放宽的)\+ smartbox 两笔(`t=hk` / `t=us`,各硬顶 10 条)。
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
- items: merged.slice(0, SEARCH_LIMIT).map((row) => ({ ...row, in_list: items.get(row.thscode) !== undefined })),
602
- // 两种"还有更多"都要说实话:某一路自己回满了**它那一档**的上限(上游那侧被砍),或合并后
603
- // 超过一屏(我们这一侧被砍)。两档数字不同,所以逐路按 `SEARCH_BRANCH_LIMITS` 判。
604
- truncated: merged.length > SEARCH_LIMIT || branches.some((branch) => branch.ok === true && branch.rows.length >= SEARCH_BRANCH_LIMITS[branch.market]),
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
- /** A 股那一路。⛔ 不许拿港美股那两条白名单顺手再过滤一遍:这边已有 `asset_type` + 键形状两道闸。 */
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
- try {
613
- const envelope = await callFuyao(ENDPOINTS.search, { q, asset_type: SEARCH_ASSET_TYPES, limit: SEARCH_BRANCH_LIMITS['a-share'] }, ['q', 'asset_type', 'limit'])
614
- // 后缀也要收口:实测 `q=宁德时代` 会带回 `885789.TI 宁德时代概念`(同花顺指数),
615
- // 它既不在本版报价范围、也过不了域 schema——不挡掉就是"添加"那一下 500。
616
- return { ok: true, market: 'a-share', rows: fuyaoItems(envelope).map(candidateOf).filter((row) => ASSET_TYPES.includes(row.asset_type) && normalizeThscode(row.thscode) !== undefined) }
617
- } catch (error) {
618
- return { ok: false, market: 'a-share', code: watchlistErrorCode(error) }
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
- return { ok: true, market, rows: found.map((row) => offshoreCandidateOf(row, market)).filter((row) => row !== undefined) }
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/capital-watchlist",
3
- "version": "2.6.3",
3
+ "version": "2.6.5",
4
4
  "description": "Capital 模式的用户自选股(宿主级清单 + Fuyao 报价快照 + /capital-watchlist 路由 + 输入框菜单弹窗):由 @v587d/capital-generation 随包携带并作为 host 平面行挂载。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@v587d/capital-charts",
3
- "version": "2.6.3",
3
+ "version": "2.6.5",
4
4
  "description": "Capital 模式的图表前端组件(浏览器半边 + 序列旁路路由):由 @v587d/capital-generation 随包携带并作为 host 平面行挂载。",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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.3";
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.3';
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.3";
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.3';
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",
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"