✨ 把 Gemini 联网搜索「引用来源」也挪到响应 content 里了。。

(\ _ /)
( ・-・)
/っ :coffee: 与其用个例猜大模型联网搜索牛不牛逼,不如直接看看它们怎么搜的。。

简而言之,流程大都如此 :arrow_lower_left:

关键词搜索页,点开就是:

然后 Gemini 会在这个基础上进一步打开它看对眼的 URL:

(\ _ /)
( ・-・)
/っ :beer: 就,Gemini 搜索能力强不强,不看 Gemini,看 Google 。。和你的文字被理解成了什么 搜索关键字。。

最后是 Vertex AI の Gemini 的 Cloudflare Woker 虽然我觉得应该用不上 (改客户端太麻烦了,改 Worker 好了)

Cloudflare Worker 源码
const PROJECT_ID = '(这里填你自己的)';
const CLIENT_ID = '(这里填你自己的)';
const CLIENT_SECRET = '(这里填你自己的)';
const REFRESH_TOKEN = '(这里填你自己的)';

// ↑ 上述部分从 https://linux.do/t/topic/118702 这个喂饭教程里拿。

// ↓ 这个是客户端(如 NextChat)访问接口的 Key,自定。
const API_KEY = 'sk-8848decameterBOOM'

const TOKEN_URL = 'https://www.googleapis.com/oauth2/v4/token';

let accessToken = '';
let tokenExpiry = 0;

// 获取 access_token
async function getAccessToken() {
  if (Date.now() / 1000 < tokenExpiry - 60) {
    return accessToken;
  }

  const response = await fetch(TOKEN_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      refresh_token: REFRESH_TOKEN,
      grant_type: 'refresh_token'
    })
  });

  const data = await response.json();
  accessToken = data.access_token;
  tokenExpiry = Date.now() / 1000 + data.expires_in;
  return accessToken;
}

/*
northamerica-northeast1 (蒙特利尔)
southamerica-east1 (圣保罗)
us-central1 (爱荷华)
us-east1 (南卡罗来纳)
us-east4 (北弗吉尼亚)
us-east5 (哥伦布)
us-south1 (达拉斯)
us-west1 (俄勒冈)
us-west4 (拉斯维加斯)
europe-central2 (华沙)
europe-north1 (芬兰)
europe-southwest1 (马德里)
europe-west1 (比利时)
europe-west2 (伦敦)
europe-west3 (法兰克福)
europe-west4 (荷兰)
europe-west6 (苏黎世)
europe-west8 (米兰)
europe-west9 (巴黎)
asia-east1 (台湾)
asia-east2 (香港)
asia-northeast1 (东京)
asia-northeast3 (首尔)
asia-south1 (孟买)
asia-southeast1 (新加坡)
australia-southeast1 (悉尼)
me-central1 (Doha)
me-central2 (达曼)
me-west1 (特拉维夫)
*/

const AVAILABLE_LOCATIONS = [
  'us-central1',
  'us-south1',
  'us-west1',
  'us-west4',
];

// 选择区域
function getLocation(model) {
  if (/^gemini-.*exp/.test(model)) {
    return 'us-central1';
  } else {
    const currentTimeInSeconds = Math.floor(Date.now() / 1000);
    const intervalInSeconds = 10;
    const index = Math.floor((currentTimeInSeconds / intervalInSeconds) % AVAILABLE_LOCATIONS.length);
    return AVAILABLE_LOCATIONS[index];
  }
}

// 构建 API URL
function constructApiUrl(location, model, isStream) {
  const _type = isStream ? 'streamGenerateContent' : 'generateContent';
  return `https://${location}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${location}/publishers/google/models/${model}:${_type}`;
}

// 处理请求
async function handleRequest(request) {
  if (request.method === 'OPTIONS') {
    return handleOptions();
  }
  // 检查API_KEY
  const url = new URL(request.url);
  const path = url.pathname;

  const apiKey = url.searchParams.get('key');
  const authHeader = request.headers.get('Authorization');
  const expectedAuthHeader = `Bearer ${API_KEY}`;

  let type = 'generateContent';
  // 找到路径中的冒号后的内容
  const colonIndex = path.lastIndexOf(':');
  if (colonIndex === -1) {
    // 如果找不到冒号,则默认为 streamGenerateContent (NextChat)
    type = 'streamGenerateContent';
  } else {   
    // 截取冒号后的内容
    const typeString = path.substring(colonIndex + 1);

    // 判断类型
    if (typeString.includes('streamGenerateContent')) {
      type = 'streamGenerateContent';
    }
  }
  const isStream = type === 'streamGenerateContent';

  if (apiKey && apiKey === API_KEY){
      // ai-studio
  } else if (authHeader && authHeader === expectedAuthHeader) {
      // vertex-ai
  } else {
    const errorResponse = new Response(JSON.stringify({
      type: "error",
      error: {
        type: "permission_error",
        message: "Your API key does not have permission to use the specified resource."
      }
    }), {
      status: 403,
      headers: {
        'Content-Type': 'application/json'
      }
    });

    errorResponse.headers.set('Access-Control-Allow-Origin', '*');
    errorResponse.headers.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, DELETE, HEAD');
    errorResponse.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key, anthropic-version, model');
    
    return errorResponse;
  }
 
  const accessToken = await getAccessToken();

  // 从 URL 中获取模型名称
  let model = url.searchParams.get('model');
  if (!model) {
    model = 'gemini-1.5-pro'; // 默认模型
  }

  let requestBody = await request.json();

  // 判断 URL 中是否包含 'search' 键
  if (url.searchParams.has('search')) {
    function findLastUserInput(contents) {
      for (let i = contents.length - 1; i >= 0; i--) {
        const content = contents[i];
        if (content.role === 'user' && content.parts && content.parts.length > 0) {
          return content.parts[0].text;
        }
      }
      return null;
    }

    // 查找最后的用户输入
    const lastUserInput = findLastUserInput(requestBody.contents);

    // 根据特定格式匹配模型名称
    if (lastUserInput) {
      const modelMatch = lastUserInput.match(/^(\*?)([a-z0-9.-]+?)¦/);
      if (modelMatch && lastUserInput.indexOf('¦') <= 50) {
        model = 'gemini-' + modelMatch[2];
        // 移除匹配到的模型名称部分
        const lastContent = requestBody.contents[requestBody.contents.length - 1];
        if (lastContent.role === 'user') {
          if (modelMatch[1] === '*') {
              // 如果有星号,则保留星号
              lastContent.parts[0].text = '*' + lastContent.parts[0].text.substring(modelMatch[0].length).trim();
          } else {
              // 如果没有星号,则直接移除匹配部分
              lastContent.parts[0].text = lastContent.parts[0].text.substring(modelMatch[0].length).trim();
          }
        }
      }
    }

    // 如果找到最后的用户输入并且不是以星号开始,则添加 Google 搜索检索工具
    if (lastUserInput && !lastUserInput.startsWith('*')) {
      if (model.includes('exp')) {
        requestBody.tools = [{
          "google_search": {}
        }];
      } else {
        requestBody.tools = [{
          "googleSearchRetrieval": {}
        }];
      }
    } else {
      // 移除行首星号。懒得写例外了自己注意规则就好
      const lastContent = requestBody.contents[requestBody.contents.length - 1];
      if (lastContent.role === 'user') {
        lastContent.parts[0].text = lastContent.parts[0].text.substring(1).trim();
      }
    }
  }
  
  // 修改 safetySettings
  if (requestBody.safetySettings) {
    // 删除已存在的 HARM_CATEGORY_CIVIC_INTEGRITY
    requestBody.safetySettings = requestBody.safetySettings.filter(setting => 
      setting.category !== "HARM_CATEGORY_CIVIC_INTEGRITY"
    );
    
    if (model.includes('exp')) {
      requestBody.safetySettings.forEach(setting => {
        setting.threshold = 'OFF';
      });
      requestBody.safetySettings.push({
        category: "HARM_CATEGORY_CIVIC_INTEGRITY",
        threshold: "BLOCK_ONLY_HIGH"
      });
    } else {
      requestBody.safetySettings.forEach(setting => {
        setting.threshold = 'BLOCK_NONE';
      });
      requestBody.safetySettings.push({
        category: "HARM_CATEGORY_CIVIC_INTEGRITY",
        threshold: "BLOCK_NONE"
      });
    }
  }

  const location = getLocation(model);
  let apiUrl = constructApiUrl(location, model, isStream);

  // 检查是否存在 alt=sse 参数
  const altParam = url.searchParams.get('alt');
  if (altParam === 'sse') {
    apiUrl += '?alt=sse';
  }

  const modifiedHeaders = new Headers(request.headers);
  modifiedHeaders.set('Authorization', `Bearer ${accessToken}`);
  modifiedHeaders.set('Content-Type', 'application/json; charset=utf-8');

  const modifiedRequest = new Request(apiUrl, {
    headers: modifiedHeaders,
    method: request.method,
    body: JSON.stringify(requestBody),
    redirect: 'follow'
  });

  const response = await fetch(modifiedRequest);

  // 如果是 SSE 响应,我们需要特殊处理
  if (altParam === 'sse') {
    const reader = response.body.getReader();
    const encoder = new TextEncoder();
    const decoder = new TextDecoder();
    let buffer = '';

    const stream = new ReadableStream({
      async start(controller) {
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;

          buffer += decoder.decode(value, { stream: true });
          const lines = buffer.split('\n');
          buffer = lines.pop();

          for (const line of lines) {
            if (line.startsWith('data: ')) {
              const data = JSON.parse(line.slice(6));
              if (data.candidates && data.candidates[0].finishReason === 'STOP') {
                // 处理原始的最后一条消息
                const lastMessage = {
                  candidates: [{
                    content: {
                      role: "model",
                      parts: [{ text: data.candidates[0].content.parts[0].text }]
                    }
                  }],
                  modelVersion: data.modelVersion,
                  createTime: data.createTime,
                  responseId: data.responseId
                };
                controller.enqueue(encoder.encode(`data: ${JSON.stringify(lastMessage)}\n\n`));

                // 检查是否有引用来源
                let hasReferences = false;
                let additionalInfo = '## 引用来源\n';

                if (data.candidates[0].groundingMetadata && data.candidates[0].groundingMetadata.searchEntryPoint && data.candidates[0].groundingMetadata.searchEntryPoint.renderedContent) {
                  const renderedContent = data.candidates[0].groundingMetadata.searchEntryPoint.renderedContent;
                  const matches = renderedContent.match(/<a class="chip" href="([^"]+)">([^<]+)<\/a>/g);
                  if (matches) {
                    hasReferences = true;
                    additionalInfo += '* 执行搜索查询 ✨ *renderedContent*\n';
                    for (const match of matches) {
                      const [_, url, text] = match.match(/<a class="chip" href="([^"]+)">([^<]+)<\/a>/);
                      additionalInfo += `   - [${text}](${url})\n`;
                    }
                  }
                }
                
                if (data.candidates[0].groundingMetadata && data.candidates[0].groundingMetadata.groundingChunks) {
                  hasReferences = true;
                  additionalInfo += '* 解析网页内容 🌋 *groundingChunks*\n';
                  let counter = 1;
                  for (const chunk of data.candidates[0].groundingMetadata.groundingChunks) {
                    if (chunk.web) {
                      additionalInfo += `   - [${chunk.web.title}](${chunk.web.uri}) ${getSuperscript(counter)}\n`;
                      counter++;
                    }
                  }
                }                

                // 只有在有引用来源时才发送额外信息
                if (hasReferences) {
                  const additionalData = {
                    candidates: [{
                      content: {
                        role: "model",
                        parts: [{ text: additionalInfo }]
                      }
                    }],
                    modelVersion: data.modelVersion,
                    createTime: data.createTime,
                    responseId: data.responseId
                  };
                  controller.enqueue(encoder.encode(`data: ${JSON.stringify(additionalData)}\n`));
                }
              } else {
                // 对于非STOP消息,保持原格式不变
                controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n`));
              }
            } else {
              // 对于非data行,直接转发
              controller.enqueue(encoder.encode(line + '\n'));
            }
          }
        }
        controller.close();
      }
    });

    return new Response(stream, {
      headers: {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive',
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key, anthropic-version, model'
      }
    });
  } 

  const modifiedResponse = new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers: response.headers
  });
 
  modifiedResponse.headers.set('Access-Control-Allow-Origin', '*');
  modifiedResponse.headers.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
  modifiedResponse.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key, anthropic-version, model');
   
  return modifiedResponse;
}

function getSuperscript(num) {
  const superscripts = ['¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'];
  if (num <= 9) {
    return superscripts[num - 1];
  } else {
    return num.toString().split('').map(digit => superscripts[parseInt(digit) - 1]).join('');
  }
}

function handleOptions() {
  const headers = new Headers();
  headers.set('Access-Control-Allow-Origin', '*');
  headers.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
  headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, x-api-key, anthropic-version, model');

  return new Response(null, {
    status: 204,
    headers: headers
  });
}

export default {
  async fetch(request) {
    return handleRequest(request);
  }
}

完。

12 个赞

黛玉太强了

哇,我来试试

诶哟,不错诶