Skip to content

Commit 384c837

Browse files
authored
Merge pull request #174 from Travisun/feat/stockindex-opptrixquant
feat: remove OpptrixQuant; Tickflow universe search and instrument ID unify
2 parents 02cc5d4 + 226dacb commit 384c837

119 files changed

Lines changed: 4542 additions & 2371 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,10 @@ Storage Layer (user-store, market-data)
226226
- **唯一标准入口**`engine.queryInstrumentData(ref, capability, opts?)`
227227
- 扩展顺序:复用 capability → 扩 `instrument-query.ts` → 扩 Provider 标准方法 → 最后才 `invokeCustomMethod` + 文档登记
228228
- **禁止** Hub / client-ui / Agent 主路径新增 `de.realtime()` / 直连第三方行情 URL
229-
- **内置推荐栈**:tonghuashun(CN,需扶摇 Key)、tickflow、tushare、zzshare、baostock;另含 stockindex(搜索/列表)、binance/okx(CRYPTO)
230-
- **已移除内置注册**:tencent、sinafinance、eastmoney、akshare、webfeed(实现与注册均已删除,本地配置启动时自动清理)
229+
- **内置推荐栈**:tonghuashun(CN,需扶摇 Key)、tickflow、tushare、zzshare、baostock;另含 binance/okx(CRYPTO)
230+
- **已移除内置注册**:tencent、sinafinance、eastmoney、akshare、webfeed**stockindex(OpptrixQuant)**(实现与注册均已删除,本地配置启动时自动清理)
231231
- **Hub 降级**:机构持仓详情 Tab、非 CN 宏观 scope、部分跨市场 enrich;**右侧面板**主路径仍经 `queryInstrumentData`
232+
- **搜索 / 名录**:搜索 = 扶摇 + Tickflow + 本地;名录灌库 = 纯 Tickflow(CN/HK/US 股票与 CN ETF)
232233

233234
## Agent / MCP 工具(摘要)
234235

apps/server/src/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,22 @@ app.post('/api/market-data/ui-ready', async () => {
933933
return { ok: true }
934934
})
935935

936+
/** Lightweight sync progress for search-universe prep UI (no heavy db_status). */
937+
app.get('/api/market-data/sync-state', async () => {
938+
const snap = getMarketDataService().syncState()
939+
return {
940+
success: true,
941+
data: {
942+
running: snap.running,
943+
overall_percent: snap.overall_percent,
944+
message: snap.message,
945+
current_job: snap.current_job,
946+
jobs_completed: snap.jobs_completed,
947+
jobs_total: snap.jobs_total,
948+
},
949+
}
950+
})
951+
936952
app.patch<{ Body: { default_scorecard?: string; default_top_n?: number; default_model?: string } }>(
937953
'/api/config',
938954
async (req) => {

client-ui/src/api/client.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -532,11 +532,37 @@ export const research = {
532532
),
533533

534534
searchInstruments: (keyword: string, limit = 20, signal?: AbortSignal) =>
535-
jsonFetch<{ success: boolean; data?: { items: import('../types/instrument').LocalInstrumentHit[]; count: number } }>(
535+
jsonFetch<{
536+
success: boolean
537+
data?: {
538+
items: import('../types/instrument').LocalInstrumentHit[]
539+
count: number
540+
source?: string
541+
universe_prep?: {
542+
status: 'ready' | 'preparing' | 'failed'
543+
percent: number
544+
message: string
545+
jobs?: string[]
546+
}
547+
}
548+
}>(
536549
`/instruments/search?keyword=${encodeURIComponent(keyword)}&limit=${limit}`,
537550
{ signal },
538551
),
539552

553+
marketDataSyncState: (signal?: AbortSignal) =>
554+
jsonFetch<{
555+
success: boolean
556+
data?: {
557+
running: boolean
558+
overall_percent: number
559+
message: string | null
560+
current_job: string | null
561+
jobs_completed: number
562+
jobs_total: number
563+
}
564+
}>('/market-data/sync-state', { signal }),
565+
540566
instrumentsSummary: () =>
541567
jsonFetch<{ success: boolean; data?: {
542568
summary: Array<{ market: string; assetClass: string; count: number }>
@@ -712,9 +738,13 @@ export const research = {
712738
export async function fetchWatchlist() {
713739
const resp = await jsonFetch<{
714740
success: boolean
715-
data?: { items: import('../types/market').WatchlistItem[]; count: number }
741+
data?: {
742+
items: import('../types/market').WatchlistItem[]
743+
count: number
744+
disambiguation_candidates?: Record<string, import('../types/market').DisambiguationCandidate[]>
745+
}
716746
}>('/watchlist')
717-
return resp.data ?? { items: [], count: 0 }
747+
return resp.data ?? { items: [], count: 0, disambiguation_candidates: {} }
718748
}
719749

720750
export async function saveWatchlist(items: import('../types/market').WatchlistItem[]) {

client-ui/src/chat/ChatComposer.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,8 @@ const ChatComposer = forwardRef<ChatComposerHandle, ChatComposerProps>(function
655655
moveActive: moveMentionActive,
656656
clampActiveIndex,
657657
setMentionActiveIndex,
658+
universePrep: mentionUniversePrep,
659+
refreshingAfterPrep: mentionRefreshingAfterPrep,
658660
} = useStockMention(watchlistItems)
659661

660662
const {
@@ -720,9 +722,10 @@ const ChatComposer = forwardRef<ChatComposerHandle, ChatComposerProps>(function
720722
refreshContentState()
721723
}, [draftSync, closeMention, closeSlash, refreshContentState])
722724

723-
const buildChipData = useCallback((item: WatchlistItem): InlineChipData => {
725+
const buildChipData = useCallback((item: WatchlistItem): InlineChipData | null => {
724726
const row = normalizeWatchlistItem(item)
725727
const ref = resolveWatchlistInstrument(row)
728+
if (!ref) return null
726729
const code = displayCodeFromInstrument(ref)
727730
const market = ref.market !== 'CN' ? marketDisplayName(ref.market) : null
728731
return {
@@ -741,6 +744,10 @@ const ChatComposer = forwardRef<ChatComposerHandle, ChatComposerProps>(function
741744
const savedRange = caretRangeRef.current
742745
root.focus()
743746
const data = buildChipData(item)
747+
if (!data) {
748+
closeMention()
749+
return
750+
}
744751
if (collectChipKeys(root).includes(data.key)) {
745752
// 已存在同一标的:仅删除 @query 触发文本,不重复插入。
746753
const dup = createChipElement(data)
@@ -1283,6 +1290,8 @@ const ChatComposer = forwardRef<ChatComposerHandle, ChatComposerProps>(function
12831290
items={mentionMatches}
12841291
activeIndex={mentionState.activeIndex}
12851292
query={mentionState.query}
1293+
universePrep={mentionUniversePrep}
1294+
refreshingAfterPrep={mentionRefreshingAfterPrep}
12861295
onSelect={handleSelectMention}
12871296
onHover={setMentionActiveIndex}
12881297
onClose={closeMention}
Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { ProgressBar, Text } from '@fluentui/react-components'
12
import type { WatchlistItem } from '../types/market'
23
import {
34
displayCodeFromInstrument,
45
normalizeWatchlistItem,
5-
resolveWatchlistInstrument,
6+
tryResolveWatchlistInstrument,
67
watchlistItemKey,
78
} from '../market/instrument'
9+
import { UNIVERSE_PREP_COPY, type UniversePrepUi } from '../market/useInstrumentSearchWithUniversePrep'
810
import ComposerTooltipMenu, {
911
COMPOSER_MENU_WIDTH,
1012
ComposerTooltipMenuItem,
@@ -16,6 +18,8 @@ interface Props {
1618
items: WatchlistItem[]
1719
activeIndex: number
1820
query: string
21+
universePrep?: UniversePrepUi
22+
refreshingAfterPrep?: boolean
1923
onSelect: (item: WatchlistItem) => void
2024
onHover: (index: number) => void
2125
onClose: () => void
@@ -27,48 +31,73 @@ export default function ComposerStockMentionList({
2731
items,
2832
activeIndex,
2933
query,
34+
universePrep,
35+
refreshingAfterPrep,
3036
onSelect,
3137
onHover,
3238
onClose,
3339
}: Props) {
40+
const preparing = universePrep?.status === 'preparing'
41+
const failed = universePrep?.status === 'failed'
42+
3443
return (
3544
<ComposerTooltipMenu
3645
open={open}
3746
anchorRef={anchorRef}
3847
align="start"
3948
width={COMPOSER_MENU_WIDTH.stockMention}
40-
maxHeight={200}
49+
maxHeight={240}
4150
ariaLabel="选择标的"
4251
onClose={onClose}
4352
>
44-
{!items.length ? (
53+
{preparing && (
54+
<div className="opptrix-composer-tooltip-menu__empty" style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
55+
<Text size={200}>
56+
{refreshingAfterPrep
57+
? UNIVERSE_PREP_COPY.refreshing
58+
: (universePrep?.message || UNIVERSE_PREP_COPY.preparing)}
59+
</Text>
60+
<ProgressBar
61+
value={Math.min(1, Math.max(0.03, (universePrep?.percent || 0) / 100))}
62+
thickness="medium"
63+
color="brand"
64+
shape="rounded"
65+
/>
66+
</div>
67+
)}
68+
{failed && (
69+
<div className="opptrix-composer-tooltip-menu__empty">
70+
{universePrep?.message || UNIVERSE_PREP_COPY.failed}
71+
</div>
72+
)}
73+
{!items.length && !preparing ? (
4574
<div className="opptrix-composer-tooltip-menu__empty">
4675
{query
4776
? '没有匹配的标的,可尝试 CN:SZ.000009、US:AAPL 或股票名称'
4877
: '输入 @ 搜索关注列表或本地标的'}
4978
</div>
50-
) : (
51-
items.map((item, index) => {
52-
const row = normalizeWatchlistItem(item)
53-
const codeLabel = displayCodeFromInstrument(resolveWatchlistInstrument(row))
54-
const active = index === activeIndex
55-
return (
56-
<ComposerTooltipMenuItem
57-
key={watchlistItemKey(row)}
58-
active={active}
59-
onMouseEnter={() => onHover(index)}
60-
onClick={() => onSelect(row)}
61-
>
62-
<span className="opptrix-composer-tooltip-menu__item-main">
63-
<span className="opptrix-composer-tooltip-menu__item-title">{row.name}</span>
64-
</span>
65-
<span className="opptrix-composer-tooltip-menu__item-code">
66-
{codeLabel}
67-
</span>
68-
</ComposerTooltipMenuItem>
69-
)
70-
})
71-
)}
79+
) : null}
80+
{items.map((item, index) => {
81+
const row = normalizeWatchlistItem(item)
82+
const ref = tryResolveWatchlistInstrument(row)
83+
const codeLabel = ref ? displayCodeFromInstrument(ref) : row.code
84+
const active = index === activeIndex
85+
return (
86+
<ComposerTooltipMenuItem
87+
key={watchlistItemKey(row)}
88+
active={active}
89+
onMouseEnter={() => onHover(index)}
90+
onClick={() => onSelect(row)}
91+
>
92+
<span className="opptrix-composer-tooltip-menu__item-main">
93+
<span className="opptrix-composer-tooltip-menu__item-title">{row.name}</span>
94+
</span>
95+
<span className="opptrix-composer-tooltip-menu__item-code">
96+
{codeLabel}
97+
</span>
98+
</ComposerTooltipMenuItem>
99+
)
100+
})}
72101
</ComposerTooltipMenu>
73102
)
74103
}

client-ui/src/chat/useStockMention.ts

Lines changed: 26 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
1+
import { useCallback, useMemo, useState } from 'react'
22
import type { WatchlistItem } from '../types/market'
3-
import { research } from '../api/client'
4-
import { hitToWatchlistItem, normalizeWatchlistItem, watchlistItemKey } from '../market/instrument'
3+
import { normalizeWatchlistItem, watchlistItemKey } from '../market/instrument'
4+
import {
5+
useInstrumentSearchWithUniversePrep,
6+
type UniversePrepUi,
7+
} from '../market/useInstrumentSearchWithUniversePrep'
58

69
export interface StockMentionState {
710
open: boolean
@@ -29,33 +32,24 @@ function findMentionTrigger(text: string, cursor: number) {
2932

3033
export function useStockMention(items: WatchlistItem[]) {
3134
const [state, setState] = useState<StockMentionState>(CLOSED)
32-
const [remote, setRemote] = useState<WatchlistItem[]>([])
33-
const searchGen = useRef(0)
3435

35-
useEffect(() => {
36-
if (!state.open) {
37-
setRemote([])
38-
return
39-
}
40-
const q = state.query.trim()
41-
if (q.length < 2 && !q.includes(':')) {
42-
setRemote([])
43-
return
44-
}
45-
const gen = ++searchGen.current
46-
const timer = window.setTimeout(() => {
47-
void research.searchInstruments(q, 12)
48-
.then(resp => {
49-
if (gen !== searchGen.current) return
50-
const hits = resp.data?.items ?? []
51-
setRemote(hits.map(hitToWatchlistItem))
52-
})
53-
.catch(() => {
54-
if (gen === searchGen.current) setRemote([])
55-
})
56-
}, 220)
57-
return () => window.clearTimeout(timer)
58-
}, [state.open, state.query])
36+
const searchEnabled = state.open && (
37+
state.query.trim().length >= 2 || state.query.includes(':')
38+
)
39+
const searchKeyword = searchEnabled ? state.query.trim() : ''
40+
41+
const {
42+
hits: remote,
43+
searching: remoteSearching,
44+
universePrep,
45+
refreshingAfterPrep,
46+
} = useInstrumentSearchWithUniversePrep({
47+
keyword: searchKeyword,
48+
limit: 12,
49+
minLength: state.query.includes(':') ? 1 : 2,
50+
debounceMs: 220,
51+
enabled: searchEnabled,
52+
})
5953

6054
const syncFromInput = useCallback((text: string, cursor: number) => {
6155
const trigger = findMentionTrigger(text, cursor)
@@ -75,7 +69,6 @@ export function useStockMention(items: WatchlistItem[]) {
7569

7670
const close = useCallback(() => {
7771
setState(CLOSED)
78-
setRemote([])
7972
}, [])
8073

8174
const matches = useMemo(() => {
@@ -85,10 +78,8 @@ export function useStockMention(items: WatchlistItem[]) {
8578
if (!q) return true
8679
const normalized = normalizeWatchlistItem(item)
8780
const code = normalized.code.toLowerCase()
88-
const label = code
8981
return item.name.toLowerCase().includes(q)
9082
|| code.includes(q)
91-
|| label.includes(q)
9283
|| (item.industry?.toLowerCase().includes(q) ?? false)
9384
})
9485
const merged = new Map<string, WatchlistItem>()
@@ -116,7 +107,6 @@ export function useStockMention(items: WatchlistItem[]) {
116107
const nextText = `${before}${after}`
117108
const nextCursor = before.length
118109
setState(CLOSED)
119-
setRemote([])
120110
return { nextText, nextCursor }
121111
}, [state.startIndex])
122112

@@ -152,5 +142,8 @@ export function useStockMention(items: WatchlistItem[]) {
152142
clampActiveIndex,
153143
setActiveIndex,
154144
setMentionActiveIndex: setActiveIndex,
145+
universePrep: universePrep as UniversePrepUi,
146+
remoteSearching,
147+
refreshingAfterPrep,
155148
}
156149
}

0 commit comments

Comments
 (0)