1+ <!DOCTYPE html>
2+ < html lang ="zh-CN ">
3+ < head >
4+ < meta charset ="UTF-8 ">
5+ < meta name ="viewport " content ="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no ">
6+ < title > LinkSumm - 输入URL让AI为您总结</ title >
7+ < meta name ="description " content ="LinkSumm可以帮助您将网页内容进行总结,提取出重要信息。 ">
8+ < meta name ="keywords " content ="LinkSumm, AI总结, AI提取, 网页总结, 内容提取 ">
9+ < link rel ="stylesheet " href ="/static/element-ui/index.css ">
10+ < link rel ="icon " href ="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔗</text></svg> ">
11+ < link href ="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap " rel ="stylesheet ">
12+ < link rel ="stylesheet " href ="/static/css/style.css ">
13+ < script defer src ="https://tj.rss.ink/script.js " data-website-id ="fc376c34-e38d-4d45-bf03-f369a2298782 "> </ script >
14+ </ head >
15+ < body >
16+ < div id ="app ">
17+ < div class ="header ">
18+ < h1 > < span class ="icon "> 🔗</ span > LinkSumm</ h1 >
19+ < p > 输入一个URL地址,让AI为您总结内容</ p >
20+ </ div >
21+
22+ < div class ="input-section ">
23+ <!-- 模式选择 -->
24+ < div class ="select-mode ">
25+ < div >
26+ < el-radio-group v-model ="mode ">
27+ < el-radio-button label ="快速请求 "> </ el-radio-button >
28+ < el-tooltip class ="item " effect ="light " content ="支持SPA单页应用,但速度较慢 " placement ="right ">
29+ < el-radio-button label ="深度请求 "> </ el-radio-button >
30+ </ el-tooltip >
31+
32+ </ el-radio-group >
33+ </ div >
34+ </ div >
35+ <!-- 模式选择END -->
36+ < el-alert
37+ v-if ="errorMessage "
38+ :title ="errorMessage "
39+ type ="error "
40+ show-icon
41+ :closable ="false "
42+ style ="margin-bottom: 20px; ">
43+ </ el-alert >
44+
45+ < div class ="input-row ">
46+ < el-input
47+ placeholder ="输入URL地址 (例如: https://example.com/article) "
48+ v-model.trim ="urlInput "
49+ prefix-icon ="el-icon-link "
50+ size ="medium "
51+ clearable
52+ @keyup.enter.native ="startSummarization "
53+ :disabled ="isLoading ">
54+
55+
56+ </ el-input >
57+
58+ <!-- 第二行内容 -->
59+ < div class ="row-2 ">
60+
61+ < el-select
62+ v-model ="selectedModel "
63+ placeholder ="选择模型 "
64+ :disabled ="isLoading "
65+ size ="medium ">
66+ < el-option v-if ="models.length === 0 " label ="加载模型中... " value ="" disabled > </ el-option >
67+ < el-option label ="自动选择 " value ="auto "> </ el-option >
68+ < el-option
69+ v-for ="model in models "
70+ :key ="model.model "
71+ :label ="model.name "
72+ :value ="model.model ">
73+ </ el-option >
74+ </ el-select >
75+
76+ < el-button
77+ type ="primary "
78+ @click ="startSummarization "
79+ :loading ="isLoading "
80+ :disabled ="!urlInput || isLoading "
81+ icon ="el-icon-s-promotion "
82+ size ="medium ">
83+ 开始总结
84+ </ el-button >
85+ </ div >
86+ <!-- 第二行内容END -->
87+ </ div >
88+ </ div >
89+
90+ < div v-if ="outputContent != '' " class ="result-section ">
91+ < h2 > 标题: ${ originalTitle }</ h2 >
92+ < h3 > 总结:</ h3 >
93+ < div id ="summary-output " v-html ="outputContent "> </ div >
94+ </ div >
95+
96+ < div class ="footer ">
97+ < p > ©2025 < a rel ="nofollow " target ="_blank " href ="https://github.com/helloxz/linksumm "> LinkSumm</ a > . All Rights Reserved.</ p >
98+ </ div >
99+ </ div >
100+
101+ < script src ="/static/js/vue.min.js "> </ script >
102+ < script src ="/static/element-ui/index.js "> </ script >
103+ < script src ="/static/js/axios.min.js "> </ script >
104+ < script src ="/static/js/marked.min.js "> </ script >
105+
106+ < script >
107+ new Vue ( {
108+ delimiters : [ '${' , '}' ] , // 修改为任意你喜欢的符号组合,避免和Jinja2冲突
109+ el : '#app' ,
110+ data ( ) {
111+ return {
112+ mode : '快速请求' , // 默认模式
113+ urlInput : '' ,
114+ selectedModel : 'auto' , // Default to auto or the first available model
115+ models : [ ] ,
116+ isLoading : false ,
117+ loadingStatus : '' , // To show fetching/summarizing status
118+ originalTitle : '测试' ,
119+ originalContent : '' , // Store original fetched content if needed
120+ summaryContent : '' , // Raw summary stream
121+ errorMessage : '' ,
122+ outputContent :"" // 输出内容
123+ }
124+ } ,
125+ computed : {
126+ renderedSummary ( ) {
127+ if ( this . summaryContent ) {
128+ // Configure marked - enable GitHub Flavored Markdown
129+ marked . setOptions ( {
130+ gfm : true ,
131+ breaks : false , // Use GFM line breaks
132+ pedantic : false ,
133+ smartLists : true ,
134+ smartypants : false
135+ } ) ;
136+ return marked . parse ( this . summaryContent ) ;
137+ }
138+ return '' ;
139+ }
140+ } ,
141+ methods : {
142+ handleUrlParam ( ) {
143+ // 1. 获取地址栏上的?url=参数值
144+ const queryString = window . location . search ;
145+ const urlParams = new URLSearchParams ( queryString ) ;
146+ const urlValue = urlParams . get ( 'url' ) ;
147+
148+ // 如果没有url参数,直接返回
149+ if ( ! urlValue ) return ;
150+
151+ // 2. 验证URL是否合法
152+ try {
153+ // 使用URL构造函数验证URL合法性
154+ new URL ( urlValue ) ;
155+ } catch ( e ) {
156+ // URL不合法,直接返回
157+ return ;
158+ }
159+
160+ // 3. 将合法URL赋值给this.urlInput
161+ this . urlInput = urlValue ;
162+
163+ // 4. 调用startSummarization函数
164+ this . startSummarization ( ) ;
165+ } ,
166+ async fetchModels ( ) {
167+ try {
168+ const response = await axios . get ( '/api/get/models' ) ;
169+ if ( response . data && response . data . code === 200 ) {
170+ this . models = response . data . data ;
171+ } else {
172+ this . errorMessage = 'Failed to load AI models.' ;
173+ console . error ( "Error fetching models:" , response . data ) ;
174+ }
175+ } catch ( error ) {
176+ this . errorMessage = 'Error fetching AI models list.' ;
177+ console . error ( 'Error fetching models:' , error ) ;
178+ }
179+ } ,
180+
181+ async startSummarization ( ) {
182+ if ( ! this . urlInput || this . isLoading ) {
183+ return ;
184+ }
185+ // Basic URL validation (consider a more robust regex if needed)
186+ if ( ! this . urlInput . startsWith ( 'http://' ) && ! this . urlInput . startsWith ( 'https://' ) ) {
187+ this . errorMessage = '请输入以 http:// 或 https:// 开头的有效URL' ;
188+ return ;
189+ }
190+
191+ this . isLoading = true ;
192+ this . errorMessage = '' ;
193+ this . summaryContent = '' ;
194+ this . originalTitle = '' ;
195+ this . originalContent = '' ;
196+ this . outputContent = '' ;
197+
198+
199+ try {
200+ const formData = new URLSearchParams ( ) ;
201+ formData . append ( 'url' , this . urlInput ) ;
202+ // 判断mode的值
203+ if ( this . mode === '深度请求' ) {
204+ formData . append ( 'mode' , 'deep' ) ;
205+ } else {
206+ formData . append ( 'mode' , 'fast' ) ;
207+ }
208+ // 1. Fetch Content
209+ const contentResponse = await axios . post ( '/api/get/content' , formData ) ;
210+
211+ if ( contentResponse . data && contentResponse . data . code === 200 ) {
212+ this . originalTitle = contentResponse . data . data . title ;
213+ this . originalContent = contentResponse . data . data . content ;
214+
215+ if ( ! this . originalContent || this . originalContent === "No content could be extracted" || this . originalContent === "Error extracting content" ) {
216+ this . errorMessage = `无法从URL中提取可读内容 (${ this . originalContent } )` ;
217+ this . isLoading = false ;
218+ return ;
219+ }
220+
221+ // 2. Fetch Summary (using fetch for streaming)
222+ await this . fetchSummaryStream ( this . originalContent ) ;
223+
224+ } else {
225+ throw new Error ( contentResponse . data . msg || 'Failed to fetch content.' ) ;
226+ }
227+
228+ } catch ( error ) {
229+ this . errorMessage = `错误: ${ error . message || '发生未知错误' } ` ;
230+ this . isLoading = false ;
231+ // 清空标题和内容
232+ this . originalTitle = '' ;
233+ this . originalContent = '' ;
234+ this . summaryContent = '' ;
235+ this . outputContent = '' ;
236+ }
237+ } ,
238+
239+ async fetchSummaryStream ( contentToSummarize ) {
240+ const payload = {
241+ model : this . selectedModel ,
242+ input : contentToSummarize
243+ } ;
244+
245+ try {
246+ const response = await fetch ( '/api/summ' , {
247+ method : 'POST' ,
248+ headers : {
249+ 'Content-Type' : 'application/json' ,
250+ } ,
251+ body : JSON . stringify ( payload ) ,
252+ } ) ;
253+
254+ if ( ! response . ok ) {
255+ let errorMsg = `网络响应错误 (状态: ${ response . status } )` ;
256+ try {
257+ const errData = await response . json ( ) ;
258+ errorMsg = errData . msg || errData . detail || errorMsg ;
259+ } catch ( e ) { /* Ignore if response is not JSON */ }
260+ throw new Error ( errorMsg ) ;
261+ }
262+
263+ // Check if the response is JSON (likely an error before streaming started)
264+ const contentType = response . headers . get ( 'Content-Type' ) ;
265+ if ( contentType && contentType . includes ( 'application/json' ) ) {
266+ const jsonData = await response . json ( ) ;
267+ if ( jsonData . code !== 200 ) {
268+ throw new Error ( jsonData . msg || 'Received an error response from the summary API.' ) ;
269+ }
270+ console . warn ( "Received unexpected JSON response from streaming endpoint:" , jsonData ) ;
271+ this . isLoading = false ;
272+ return ;
273+ }
274+
275+ // Process the stream
276+ const reader = response . body . getReader ( ) ;
277+ const decoder = new TextDecoder ( 'utf-8' ) ;
278+ let buffer = '' ;
279+ this . summaryContent = '' ;
280+
281+ while ( true ) {
282+ const { done, value } = await reader . read ( ) ;
283+ if ( done ) {
284+ break ;
285+ }
286+
287+ buffer += decoder . decode ( value , { stream : true } ) ;
288+ const lines = buffer . split ( '\n' ) ;
289+ buffer = lines . pop ( ) || '' ;
290+
291+ for ( const line of lines ) {
292+ if ( line . trim ( ) === '' || ! line . startsWith ( 'data:' ) ) continue ;
293+
294+ if ( line . includes ( '[DONE]' ) ) {
295+ await new Promise ( resolve => setTimeout ( resolve , 50 ) ) ;
296+ break ;
297+ }
298+
299+ try {
300+ const jsonStr = line . substring ( 5 ) . trim ( ) ;
301+ const data = JSON . parse ( jsonStr ) ;
302+ if ( data . value !== undefined ) {
303+ this . summaryContent += data . value ;
304+ this . outputContent = marked . parse ( this . summaryContent ) ;
305+ }
306+ } catch ( e ) {
307+ console . error ( 'Failed to parse stream data line:' , line , e ) ;
308+ }
309+ }
310+ if ( lines . some ( line => line . includes ( '[DONE]' ) ) ) {
311+ let url = this . urlInput ;
312+ this . outputContent = marked . parse ( this . summaryContent + `\n\n> 原文来自:[${ url } ](${ url } )` ) ;
313+ // 清空输入URL
314+ this . urlInput = '' ;
315+ break ;
316+ }
317+ }
318+
319+ this . isLoading = false ;
320+
321+ } catch ( error ) {
322+ console . error ( 'Failed to fetch or process summary stream:' , error ) ;
323+ this . errorMessage = `总结过程中出错: ${ error . message } ` ;
324+ this . isLoading = false ;
325+ this . summaryContent = '' ;
326+ }
327+ }
328+ } ,
329+ mounted ( ) {
330+ this . fetchModels ( ) ;
331+ this . handleUrlParam ( ) ;
332+ }
333+ } ) ;
334+ </ script >
335+ </ body >
336+ </ html >
0 commit comments