feat: add web search block functionality and integrate with existing plugins

- Introduced a new web search block plugin to handle web search queries and results.
- Updated copilot, doc block, and pro block plugins to include web search context in AI completions.
- Implemented utility functions for parsing and building web search markdown.
- Enhanced API to support web search requests and responses.
- Added configuration for web search URL and timeout settings.
- Updated size limit checks to account for web search content.
This commit is contained in:
“ydy0615”
2026-06-09 19:18:14 +08:00
parent 5a26dfde2a
commit 17d211bf93
38 changed files with 56764 additions and 10 deletions
+56
View File
@@ -3,6 +3,8 @@ import {
API_KEY,
PRO_URL,
PRO_FRONTEND_TIMEOUT_MS,
WEB_SEARCH_URL,
WEB_SEARCH_FRONTEND_TIMEOUT_MS,
TTS_URL,
TTS_STATUS_URL,
TTS_CONFIG_URL,
@@ -81,6 +83,9 @@ function getCancelUrl(apiUrl) {
if (/\/v1\/pro\/completions$/i.test(normalized)) {
return normalized.replace(/\/v1\/pro\/completions$/i, '/v1/pro/completions/cancel')
}
if (/\/v1\/web-search$/i.test(normalized)) {
return normalized.replace(/\/v1\/web-search$/i, '/v1/web-search/cancel')
}
if (/\/v1\/completions$/i.test(normalized)) {
return normalized.replace(/\/v1\/completions$/i, '/v1/completions/cancel')
}
@@ -298,6 +303,57 @@ export async function fetchProSuggestionStream(payload, apiUrl = PRO_URL) {
})
}
export async function fetchWebSearchStream(payload, apiUrl = WEB_SEARCH_URL) {
const {
prefix = '',
suffix = '',
languageId = 'markdown',
signal,
timeoutMs = WEB_SEARCH_FRONTEND_TIMEOUT_MS,
onEvent,
} = payload || {}
const settings = useSettingsStore()
const requestId = generateRequestId()
return consumeSseJson({
url: apiUrl,
requestId,
signal,
timeoutMs,
body: {
prefix,
suffix,
languageId: String(languageId || 'markdown').trim() || 'markdown',
privacy_mode: settings.privacyMode,
user_preferences: {
language: settings.language,
currency: settings.currency,
timezone: settings.detectedTimezone,
},
},
onEvent(event, data) {
if (event === 'queued' || event === 'started' || event === 'resource') {
onEvent?.(event, data)
return
}
if (event === 'progress') {
onEvent?.(String(data?.phase || ''), data)
return
}
if (event === 'error') {
onEvent?.('error', data)
}
},
onDone(data) {
return {
content: String(data?.content || ''),
createdAt: String(data?.created_at || data?.createdAt || ''),
}
},
})
}
export async function fetchTTS(text, instruct = '', apiUrl = TTS_URL) {
const requestId = generateRequestId()
return consumeSseJson({
+2
View File
@@ -6,6 +6,8 @@ const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL || DEFAULT_API_BASE_URL)
export const API_URL = import.meta.env.VITE_API_URL || `${API_BASE_URL}/v1/completions`
export const PRO_URL = import.meta.env.VITE_PRO_URL || `${API_BASE_URL}/v1/pro/completions`
export const PRO_FRONTEND_TIMEOUT_MS = Number(import.meta.env.VITE_PRO_FRONTEND_TIMEOUT_MS || 3660000)
export const WEB_SEARCH_URL = import.meta.env.VITE_WEB_SEARCH_URL || `${API_BASE_URL}/v1/web-search`
export const WEB_SEARCH_FRONTEND_TIMEOUT_MS = Number(import.meta.env.VITE_WEB_SEARCH_FRONTEND_TIMEOUT_MS || 3660000)
export const OCR_URL = import.meta.env.VITE_OCR_URL || `${API_BASE_URL}/v1/ocr`
export const CONVERT_URL = import.meta.env.VITE_CONVERT_URL || `${API_BASE_URL}/v1/convert`
export const EXPORT_PDF_URL = import.meta.env.VITE_EXPORT_PDF_URL || `${API_BASE_URL}/v1/export/pdf`
+83
View File
@@ -0,0 +1,83 @@
export const WEB_SEARCH_NODE_TYPE = 'web_search_block'
export const WEB_SEARCH_TRIGGER_TEXT = '[WEBSEARCH]'
export const WEB_SEARCH_RESULT_FENCE_LANG = 'llm-websearch'
export const WEB_SEARCH_CONTEXT_LIMIT = 32 * 1024
const WEB_SEARCH_TRIGGER_RE = /^\[websearch\]$/i
const WEB_SEARCH_RESULT_RE = /(^|\n)(`{3,})llm-websearch(?:\s+date=([^\s`]+))?[^\n]*\n([\s\S]*?)\n\2(?=\n|$)/g
function normalizeMarkdownText(value = '') {
return String(value || '').replace(/\r\n?/g, '\n')
}
function pickFence(content = '') {
const matches = String(content || '').match(/`{3,}/g) || []
const maxLen = matches.reduce((max, item) => Math.max(max, item.length), 2)
return '`'.repeat(maxLen + 1)
}
function sanitizeDate(value = '') {
const text = String(value || '').trim()
if (!text) return ''
const parsed = new Date(text)
if (Number.isNaN(parsed.getTime())) return ''
return parsed.toISOString()
}
function clipText(value = '', limit = 0) {
if (!limit || value.length <= limit) return value
return `${value.slice(0, limit)}...`
}
export function parseWebSearchTriggerSyntax(value = '') {
const text = normalizeMarkdownText(value).trim()
if (!WEB_SEARCH_TRIGGER_RE.test(text)) return null
return {
content: '',
createdAt: '',
collapsed: false,
autoStart: false,
}
}
export function buildWebSearchResultMarkdown(attrs = {}) {
const content = normalizeMarkdownText(attrs.content || '').trimEnd()
const createdAt = sanitizeDate(attrs.createdAt) || new Date().toISOString()
const fence = pickFence(content)
return `${fence}${WEB_SEARCH_RESULT_FENCE_LANG} date=${createdAt}\n${content}\n${fence}`
}
export function parseWebSearchResultMarkdown(value = '', meta = '') {
const content = normalizeMarkdownText(value).replace(/\s+$/, '')
const dateMatch = String(meta || '').match(/(?:^|\s)date=([^\s`]+)/i)
return {
content,
createdAt: sanitizeDate(dateMatch?.[1] || ''),
collapsed: false,
autoStart: false,
}
}
export function buildWebSearchContextFence(attrs = {}) {
const content = normalizeMarkdownText(attrs.content || '').trim()
if (!content) return ''
const fence = pickFence(content)
return `${fence}markdown\n${content}\n${fence}`
}
export function extractWebSearchContextFromMarkdown(markdown = '', contentLimit = 0) {
const normalized = normalizeMarkdownText(markdown)
const contexts = []
normalized.replace(WEB_SEARCH_RESULT_RE, (_full, _prefix, _fence, date, content) => {
const fence = buildWebSearchContextFence({
content: clipText(normalizeMarkdownText(content || '').trim(), contentLimit),
createdAt: date || '',
})
if (fence) contexts.push(fence)
return _full
})
return contexts.join('\n\n')
}