// 股票搜索 Edge Function // 通过腾讯智能搜索API(smartbox.gtimg.cn)支持股票名称/代码/拼音模糊搜索 // 返回格式:v_hint="市场~代码~名称~拼音~类型^市场~代码~名称~拼音~类型^..." // 名称字段为 \u 转义的 unicode // 降级:若搜索接口无结果但keyword是6位数字代码,直接调用行情接口验证(支持北交所920开头) function corsHeaders() { return { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': 'content-type', }; } interface SearchResult { code: string; name: string; market: string; type: string; } // 解析 \u 转义的 unicode 字符串 function decodeUnicode(str: string): string { try { return JSON.parse(`"${str}"`); } catch { return str; } } // 根据代码推断市场前缀(用于行情接口) function getMarketPrefix(code: string): string { if (code.startsWith('688') || code.startsWith('60')) return 'sh'; if (code.startsWith('8') || code.startsWith('4') || code.startsWith('920')) return 'bj'; return 'sz'; } // 根据代码推断板块类型 function getBoardType(code: string): string { if (code.startsWith('688')) return 'GP-A-KCB'; if (code.startsWith('300') || code.startsWith('301')) return 'GP-A-CYB'; if (code.startsWith('8') || code.startsWith('4') || code.startsWith('920')) return 'GP-A-BJB'; return 'GP-A'; } // 通过行情接口直接验证股票是否存在(用于搜索接口不支持的股票,如北交所920开头) async function lookupByQuoteApi(code: string): Promise { const market = getMarketPrefix(code); const stockCode = `${market}${code}`; try { const resp = await fetch(`https://qt.gtimg.cn/q=${stockCode}`, { headers: { 'User-Agent': 'Mozilla/5.0' }, }); if (!resp.ok) return null; const buffer = await resp.arrayBuffer(); let text: string; try { text = new TextDecoder('gbk').decode(buffer); } catch { text = new TextDecoder().decode(buffer); } // 格式:v_bj920510="62~丰光精密~920510~..." const eqIdx = text.indexOf('="'); if (eqIdx === -1) return null; const startIdx = eqIdx + 2; const endIdx = text.lastIndexOf('"'); if (endIdx <= startIdx) return null; const content = text.substring(startIdx, endIdx); const parts = content.split('~'); if (parts.length < 3) return null; const name = parts[1]; const currentPrice = parseFloat(parts[3]) || 0; // 名称非空且价格大于0才算有效股票 if (!name || currentPrice === 0) return null; return { code, name, market: market.toUpperCase(), type: getBoardType(code), }; } catch (err) { console.error(`[stock-search] 行情接口查询失败 ${stockCode}:`, err); return null; } } Deno.serve(async (req) => { if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders() }); } const url = new URL(req.url); const keyword = (url.searchParams.get('keyword') || '').trim(); if (!keyword) { return new Response(JSON.stringify({ data: [], count: 0 }), { headers: corsHeaders(), }); } try { // 腾讯智能搜索接口:支持名称、代码、拼音首字母模糊匹配 const searchUrl = `https://smartbox.gtimg.cn/s3/?t=all&q=${encodeURIComponent(keyword)}&v=2`; console.log(`[stock-search] 请求: ${searchUrl}`); const resp = await fetch(searchUrl, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': 'https://stockapp.finance.qq.com/', }, }); const results: SearchResult[] = []; if (resp.ok) { // 腾讯返回的是 UTF-8 文本(含 \u 转义),不是 GBK const text = await resp.text(); console.log(`[stock-search] 原始响应: ${text.substring(0, 500)}`); // 提取引号内内容:v_hint="..." const eqIdx = text.indexOf('="'); let content = ''; if (eqIdx !== -1) { const startIdx = eqIdx + 2; const endIdx = text.lastIndexOf('"'); if (endIdx > startIdx) { content = text.substring(startIdx, endIdx); } } if (content && content !== 'N') { // 多条记录用 ^ 分隔 const records = content.split('^'); console.log(`[stock-search] 解析到 ${records.length} 条原始记录`); for (const record of records) { if (!record) continue; // 字段顺序:市场~代码~名称~拼音~类型 const parts = record.split('~'); if (parts.length >= 5) { const market = parts[0]; // sh / sz / hk / jj / bj const code = parts[1]; const nameRaw = parts[2]; const type = parts[4]; // GP-A / GP-A-KCB(科创板) / GP-A-BJB(北交所) / GP / KJ-HB // 保留沪深A股(含科创板、创业板、北交所): // - sh + GP-A:上海主板 // - sh + GP-A-KCB:科创板 // - sz + GP-A:深圳主板/创业板(含301注册制) // - sz + GP-A-CYB:创业板(部分接口会标注) // - bj + GP-A-BJB:北交所 const isShA = market === 'sh' && (type === 'GP-A' || type === 'GP-A-KCB'); const isSzA = market === 'sz' && (type === 'GP-A' || type === 'GP-A-CYB'); const isBjA = market === 'bj' && (type === 'GP-A' || type === 'GP-A-BJB'); if ((isShA || isSzA || isBjA) && /^\d{6}$/.test(code)) { const name = decodeUnicode(nameRaw); if (name) { results.push({ code, name, market: market.toUpperCase(), type, }); } } } } } } // 降级:如果搜索接口无结果但keyword是6位数字代码,直接调用行情接口验证 // 这是为了支持腾讯搜索接口不收录的股票(如北交所920开头) if (results.length === 0 && /^\d{6}$/.test(keyword)) { console.log(`[stock-search] 搜索无结果,尝试行情接口直查: ${keyword}`); const directResult = await lookupByQuoteApi(keyword); if (directResult) { results.push(directResult); console.log(`[stock-search] 行情接口直查成功: ${directResult.name}`); } } // 去重(按code) const seen = new Set(); const uniqueResults = results.filter(r => { if (seen.has(r.code)) return false; seen.add(r.code); return true; }); // 限制最多20条 const limited = uniqueResults.slice(0, 20); console.log(`[stock-search] 关键词"${keyword}" 找到 ${limited.length} 条A股结果`); if (limited.length > 0) { console.log(`[stock-search] 首条: ${limited[0].code} ${limited[0].name} ${limited[0].market} ${limited[0].type}`); } return new Response(JSON.stringify({ data: limited, count: limited.length, }), { headers: corsHeaders() }); } catch (error) { const msg = error instanceof Error ? error.message : String(error); console.error('[stock-search] 错误:', msg); return new Response(JSON.stringify({ error: `搜索失败: ${msg}`, data: [], count: 0, }), { status: 500, headers: corsHeaders() }); } });