@@ -10,26 +10,29 @@ import { recentlyApi } from '~/api'
1010import { enrichmentApi } from '~/api/enrichment'
1111import { EnrichmentCard } from '~/components/enrichment-card'
1212
13- const URL_REGEX = / h t t p s ? : \/ \/ \S + / i
14- // Trailing punctuation that should not be part of the URL (ASCII + CJK)
13+ const URL_REGEX = / h t t p s ? : \/ \/ \S + / gi
1514const URL_TAIL_TRIM = / [ ) \] . , ; : ! ? ' " ` > } ) 。 , 、 ; : ! ? 「 」 『 』 《 》 〉 〕 — … ] + $ /
1615
17- function firstUrl ( text : string ) : string | null {
18- const m = text . match ( URL_REGEX )
19- if ( ! m ) return null
20- let url = m [ 0 ]
21- // Strip trailing punctuation iteratively (handles "(url).")
22- while ( URL_TAIL_TRIM . test ( url ) ) {
23- url = url . replace ( URL_TAIL_TRIM , '' )
16+ function extractUrls ( text : string ) : string [ ] {
17+ const matches = text . match ( URL_REGEX )
18+ if ( ! matches ) return [ ]
19+ const seen = new Set < string > ( )
20+ const result : string [ ] = [ ]
21+ for ( let url of matches ) {
22+ while ( URL_TAIL_TRIM . test ( url ) ) {
23+ url = url . replace ( URL_TAIL_TRIM , '' )
24+ }
25+ if ( url && ! seen . has ( url ) ) {
26+ seen . add ( url )
27+ result . push ( url )
28+ }
2429 }
25- return url || null
30+ return result
2631}
2732
2833function cleanErrorMessage ( raw : string | null | undefined ) : string {
2934 if ( ! raw ) return '解析失败'
30- // Strip embedded URLs (often docs links) and excess whitespace
3135 let msg = raw . replace ( / h t t p s ? : \/ \/ \S + / g, '' ) . trim ( )
32- // Common patterns we shorten
3336 if ( / \( 4 0 4 \) | \b 4 0 4 \b / . test ( msg ) ) {
3437 return '404 — 资源不存在,或私有内容无访问权(请检查 GitHub Token 等凭证)'
3538 }
@@ -42,21 +45,18 @@ function cleanErrorMessage(raw: string | null | undefined): string {
4245 if ( / T o k e n m i s s i n g / i. test ( msg ) ) {
4346 return '此 provider 需配置凭证(请至「第三方集成」设置)'
4447 }
45- // Generic fallback — cap length
4648 msg = msg . replace ( / [ \s — - ] + $ / , '' ) . trim ( )
4749 return msg . length > 100 ? msg . slice ( 0 , 100 ) + '…' : msg || '解析失败'
4850}
4951
50- function buildPayload ( text : string ) : {
51- type : 'text' | 'link'
52- content : string
53- metadata ?: { url : string }
54- } {
55- const url = firstUrl ( text )
56- if ( url ) {
57- return { type : 'link' , content : text , metadata : { url } }
58- }
59- return { type : 'text' , content : text }
52+ function buildPayload ( text : string ) : { content : string } {
53+ return { content : text }
54+ }
55+
56+ interface UrlPreviewState {
57+ loading : boolean
58+ result : EnrichmentResult | null
59+ error : string | null
6060}
6161
6262const ShorthandForm = defineComponent ( {
@@ -70,56 +70,61 @@ const ShorthandForm = defineComponent({
7070 } ,
7171 setup ( props ) {
7272 const text = ref ( props . initialContent )
73- const detectedUrl = ref < string | null > ( null )
74- const previewLoading = ref ( false )
75- const previewResult = ref < EnrichmentResult | null > ( null )
76- const previewError = ref < string | null > ( null )
73+ const detectedUrls = ref < string [ ] > ( [ ] )
74+ const previewStates = ref < Map < string , UrlPreviewState > > ( new Map ( ) )
7775 let debounceTimer : ReturnType < typeof setTimeout > | null = null
76+ let generation = 0
7877
79- const triggerResolve = ( url : string ) => {
80- previewLoading . value = true
81- previewError . value = null
82- // cancel previous in-flight via stale check
83- const myUrl = url
78+ const resolveUrl = ( url : string , gen : number ) => {
79+ const state : UrlPreviewState = { loading : true , result : null , error : null }
80+ previewStates . value = new Map ( previewStates . value ) . set ( url , state )
8481 enrichmentApi
8582 . resolve ( url )
8683 . then ( ( result ) => {
87- if ( detectedUrl . value !== myUrl ) return
88- previewResult . value = result
89- previewError . value = null
84+ if ( gen !== generation ) return
85+ previewStates . value = new Map ( previewStates . value ) . set ( url , {
86+ loading : false ,
87+ result,
88+ error : null ,
89+ } )
9090 } )
9191 . catch ( ( e : any ) => {
92- if ( detectedUrl . value !== myUrl ) return
93- previewResult . value = null
94- previewError . value = e ?. message || '解析失败'
95- } )
96- . finally ( ( ) => {
97- if ( detectedUrl . value !== myUrl ) return
98- previewLoading . value = false
92+ if ( gen !== generation ) return
93+ previewStates . value = new Map ( previewStates . value ) . set ( url , {
94+ loading : false ,
95+ result : null ,
96+ error : e ?. message || '解析失败' ,
97+ } )
9998 } )
10099 }
101100
102101 watch (
103102 text ,
104103 ( val ) => {
105104 props . onUpdate ( val )
106- const url = firstUrl ( val )
107- if ( url !== detectedUrl . value ) {
108- detectedUrl . value = url
109- previewResult . value = null
110- previewError . value = null
111- previewLoading . value = false
112- if ( debounceTimer ) clearTimeout ( debounceTimer )
113- if ( url ) {
114- debounceTimer = setTimeout ( ( ) => triggerResolve ( url ) , 500 )
115- }
105+ const urls = extractUrls ( val )
106+ const prev = detectedUrls . value
107+ const same =
108+ urls . length === prev . length && urls . every ( ( u , i ) => u === prev [ i ] )
109+ if ( same ) return
110+
111+ detectedUrls . value = urls
112+ previewStates . value = new Map ( )
113+ if ( debounceTimer ) clearTimeout ( debounceTimer )
114+ if ( urls . length > 0 ) {
115+ generation ++
116+ const gen = generation
117+ debounceTimer = setTimeout ( ( ) => {
118+ for ( const url of urls ) resolveUrl ( url , gen )
119+ } , 500 )
116120 }
117121 } ,
118122 { immediate : true } ,
119123 )
120124
121125 onBeforeUnmount ( ( ) => {
122126 if ( debounceTimer ) clearTimeout ( debounceTimer )
127+ generation ++
123128 } )
124129
125130 return ( ) => (
@@ -132,33 +137,43 @@ const ShorthandForm = defineComponent({
132137 autosize = { { minRows : 4 , maxRows : 12 } }
133138 />
134139
135- { detectedUrl . value && (
136- < div >
137- < div class = "mb-1.5 flex items-center gap-2 text-xs text-neutral-500 dark:text-neutral-400" >
138- < span > 检测到链接:</ span >
139- < code class = "truncate rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-[11px] dark:bg-neutral-800" >
140- { detectedUrl . value }
141- </ code >
142- { previewLoading . value && (
143- < LoaderIcon class = "size-3 animate-spin" aria-hidden = "true" />
144- ) }
145- </ div >
140+ { detectedUrls . value . length > 0 && (
141+ < div class = "space-y-2" >
142+ { detectedUrls . value . map ( ( url ) => {
143+ const state = previewStates . value . get ( url )
144+ return (
145+ < div key = { url } >
146+ < div class = "mb-1.5 flex items-center gap-2 text-xs text-neutral-500 dark:text-neutral-400" >
147+ < span > 检测到链接:</ span >
148+ < code class = "truncate rounded bg-neutral-100 px-1.5 py-0.5 font-mono text-[11px] dark:bg-neutral-800" >
149+ { url }
150+ </ code >
151+ { state ?. loading && (
152+ < LoaderIcon
153+ class = "size-3 animate-spin"
154+ aria-hidden = "true"
155+ />
156+ ) }
157+ </ div >
146158
147- { previewResult . value && (
148- < EnrichmentCard enrichment = { previewResult . value } />
149- ) }
159+ { state ?. result && (
160+ < EnrichmentCard enrichment = { state . result } />
161+ ) }
150162
151- { previewError . value && ! previewLoading . value && (
152- < div class = "rounded-lg border border-neutral-200 bg-neutral-50 px-3 py-2 text-xs text-neutral-500 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-400" >
153- < div class = "font-medium text-neutral-700 dark:text-neutral-300" >
154- 未识别该链接
155- </ div >
156- < div class = "mt-0.5" >
157- { cleanErrorMessage ( previewError . value ) }
163+ { state ?. error && ! state . loading && (
164+ < div class = "rounded-lg border border-neutral-200 bg-neutral-50 px-3 py-2 text-xs text-neutral-500 dark:border-neutral-800 dark:bg-neutral-900 dark:text-neutral-400" >
165+ < div class = "font-medium text-neutral-700 dark:text-neutral-300" >
166+ 未识别该链接
167+ </ div >
168+ < div class = "mt-0.5" > { cleanErrorMessage ( state . error ) } </ div >
169+ < div class = "mt-1 text-neutral-400" >
170+ 仍可保存,按链接处理。
171+ </ div >
172+ </ div >
173+ ) }
158174 </ div >
159- < div class = "mt-1 text-neutral-400" > 仍可保存,按链接处理。</ div >
160- </ div >
161- ) }
175+ )
176+ } ) }
162177 </ div >
163178 ) }
164179 </ div >
0 commit comments