Commit 79f2746f authored by AI-甘富林's avatar AI-甘富林

后台ai对话bug修复

parent 03567c07
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
// === AI Hub routing (auto-injected) ===
const _HUB_KEY = Deno.env.get('AI_HUB_API_KEY') || '';
const _HUB_BASE = (Deno.env.get('AI_HUB_BASE_URL') || '').replace(/\/+$/, '');
const _USE_HUB = !!(_HUB_KEY && _HUB_BASE);
const _AI_CHAT_URL = _USE_HUB ? `${_HUB_BASE}/chat/completions` : _AI_CHAT_URL;
const _AI_CHAT_KEY = _USE_HUB ? _HUB_KEY : (Deno.env.get('LOVABLE_API_KEY') || '');
const _AI_CHAT_DEFAULT_MODEL = Deno.env.get('AI_HUB_DEFAULT_CHAT_MODEL') || 'agent-default';
const _mapAiModel = (m: string) => (_USE_HUB && /^(google|openai|anthropic)\//i.test(m || '')) ? _AI_CHAT_DEFAULT_MODEL : m;
// === end AI Hub routing ===
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
......@@ -18,17 +8,16 @@ const corsHeaders = {
};
/**
* Coze Langchain GenUI Agent
*
* 统一的 AI 入口:
* GenUI 统一 AI 入口
*
* 1. 注入 GenUI 组件库规范
* 2. 根据意图识别注入对应部门的实时数据上下文
* 3. 调用 Coze LangChain Agent 生成 UI Schema
* 4. 失败时降级到 Lovable AI Gateway
* 3. 调用 _shared/aiProvider 统一大模型路由生成 UI Schema
* (AI Hub 中台 → Lovable Gateway → Qwen → 火山方舟,自动降级)
*/
// ========================
// GenUI 组件库定义 (供 Coze Agent 参考)
// GenUI 组件库定义 (供 LLM 参考)
// ========================
const GENUI_COMPONENT_LIBRARY = `
......@@ -295,7 +284,7 @@ ${(rechargePlansResult.data || []).map((p: any) => `${p.name}: ¥${p.recharge_am
}
// ========================
// Coze API 调用
// 请求/响应类型定义
// ========================
interface CozeLangchainRequest {
......@@ -316,159 +305,35 @@ interface GenUIResponse {
suggested_actions?: string[];
}
const COZE_API_URL = "https://dbwbtkkt9g.coze.site/stream_run";
const COZE_PROJECT_ID = "7602899003382071342";
async function callCozeAgent(
text: string,
sessionId: string,
dataContext: string,
agentType: AgentType
): Promise<GenUIResponse> {
const COZE_API_TOKEN = Deno.env.get("COZE_API_TOKEN");
if (!COZE_API_TOKEN) {
throw new Error("COZE_API_TOKEN 未配置");
}
// 构建增强 Prompt
const enhancedPrompt = `${GENUI_COMPONENT_LIBRARY}
## 当前数据上下文
${dataContext}
## 用户问题
${text}
请根据以上信息生成 JSON 响应。`;
const cozeRequestBody = {
content: {
query: {
prompt: [{ type: "text", content: { text: enhancedPrompt } }]
}
},
type: "query",
session_id: sessionId,
project_id: COZE_PROJECT_ID
};
console.log(`[CozeLangchain] Calling Coze API for ${agentType}...`);
const response = await fetch(COZE_API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${COZE_API_TOKEN}`,
},
body: JSON.stringify(cozeRequestBody),
});
if (!response.ok) {
const errorText = await response.text();
console.error(`[CozeLangchain] Coze API error: ${errorText}`);
throw new Error(`Coze API 调用失败 (${response.status})`);
}
// ========================
// 统一 AI 大模型调用(通过 _shared/aiProvider)
// ========================
// 解析 SSE 响应
const responseText = await response.text();
console.log(`[CozeLangchain] Raw response length: ${responseText.length}`);
// 打印前500字符用于调试
console.log(`[CozeLangchain] Raw response preview: ${responseText.substring(0, 500)}`);
// 提取 answer 内容 - 支持多种 Coze 响应格式
let fullAnswer = '';
const lines = responseText.split('\n');
for (const line of lines) {
const trimmedLine = line.trim();
// 跳过空行和事件行
if (!trimmedLine || trimmedLine.startsWith('event:') || trimmedLine.startsWith(':')) {
continue;
}
// 提取 data: 后的内容
let dataContent = trimmedLine;
if (trimmedLine.startsWith('data:')) {
dataContent = trimmedLine.slice(5).trim();
}
if (!dataContent || dataContent === '[DONE]') {
continue;
}
try {
const data = JSON.parse(dataContent);
// 格式1: { type: 'answer', content: { answer: '...' } }
if (data.type === 'answer' && data.content?.answer) {
fullAnswer += data.content.answer;
}
// 格式2: { content: { answer: '...' } }
else if (data.content?.answer) {
fullAnswer += data.content.answer;
}
// 格式3: { content: { thinking: '...' } } - 思考过程
else if (data.content?.thinking) {
console.log(`[CozeLangchain] Received thinking: ${data.content.thinking.substring(0, 100)}...`);
}
// 格式4: { answer: '...' } - 直接 answer 字段
else if (data.answer) {
fullAnswer += data.answer;
}
// 格式5: 直接文本 { text: '...' } 或 { output: '...' }
else if (data.text) {
fullAnswer += data.text;
} else if (data.output) {
fullAnswer += data.output;
}
// 格式6: message_end - 结束标记,检查是否有错误
else if (data.type === 'message_end' && data.content?.message_end) {
const msgEnd = data.content.message_end;
if (msgEnd.code !== '0' && msgEnd.code !== 0 && msgEnd.message) {
console.error(`[CozeLangchain] Coze error: ${msgEnd.message}`);
}
}
} catch {
// 如果解析 JSON 失败,检查是否是纯文本内容
if (dataContent && !dataContent.startsWith('{') && !dataContent.startsWith('[')) {
// 可能是纯文本回复
fullAnswer += dataContent;
}
}
}
/**
* 将 AI 返回的原始文本解析为 GenUIResponse
*/
function parseAIResponse(raw: string): GenUIResponse {
let jsonContent = raw.trim();
console.log(`[CozeLangchain] Extracted answer length: ${fullAnswer.length}`);
if (fullAnswer.length > 0) {
console.log(`[CozeLangchain] Answer preview: ${fullAnswer.substring(0, 200)}...`);
// 移除 markdown 代码块包裹
const jsonMatch = jsonContent.match(/```(?:json)?\s*([\s\S]*?)```/);
if (jsonMatch) {
jsonContent = jsonMatch[1].trim();
}
// 尝试解析 JSON(如果 answer 是 JSON 格式)
try {
// 尝试从回答中提取 JSON
let jsonContent = fullAnswer.trim();
// 检查是否包含 markdown 代码块
const jsonMatch = jsonContent.match(/```(?:json)?\s*([\s\S]*?)```/);
if (jsonMatch) {
jsonContent = jsonMatch[1].trim();
}
// 提取 JSON 对象
const jsonStart = jsonContent.indexOf('{');
const jsonEnd = jsonContent.lastIndexOf('}');
if (jsonStart !== -1 && jsonEnd !== -1 && jsonStart < jsonEnd) {
jsonContent = jsonContent.substring(jsonStart, jsonEnd + 1);
// 尝试找到 JSON 对象
const jsonStart = jsonContent.indexOf('{');
const jsonEnd = jsonContent.lastIndexOf('}');
if (jsonStart !== -1 && jsonEnd !== -1 && jsonStart < jsonEnd) {
jsonContent = jsonContent.substring(jsonStart, jsonEnd + 1);
try {
const parsed = JSON.parse(jsonContent);
// 确保 ui_schema 结构正确
// 修复组件格式:确保每个组件都有 type 和 props
let uiSchema = parsed.ui_schema;
if (uiSchema && uiSchema.components) {
// 验证组件格式
if (uiSchema?.components) {
uiSchema.components = uiSchema.components.map((comp: any) => {
// 确保每个组件都有 type 和 props
if (comp.type && !comp.props) {
const { type, ...rest } = comp;
return { type, props: rest };
......@@ -476,94 +341,84 @@ ${text}
return comp;
});
}
console.log(`[CozeLangchain] Parsed JSON successfully, has ui_schema: ${!!uiSchema}`);
console.log(`[CozeLangchain] AI JSON parsed, has ui_schema: ${!!uiSchema}`);
return {
answer: parsed.answer || '已为您生成界面',
ui_schema: uiSchema,
suggested_actions: parsed.suggested_actions,
};
} catch {
console.log('[CozeLangchain] AI JSON parse failed, returning raw text');
}
} catch (parseError) {
console.log(`[CozeLangchain] JSON parse failed: ${parseError}`);
}
// 如果无法解析为 JSON,返回纯文本
return {
answer: fullAnswer || '抱歉,无法解析响应内容。请检查 Coze Agent 配置。',
};
}
// ========================
// Lovable AI Gateway 降级
// ========================
// 无法解析 JSON,返回原始文本作为 answer
return { answer: raw || '抱歉,AI 返回内容为空,请稍后重试。' };
}
async function callLovableAIFallback(
/**
* 调用千问大模型生成 GenUI 响应
* 直接调阿里云 DashScope OpenAI 兼容接口
*/
async function callAI(
text: string,
dataContext: string,
agentType: AgentType
): Promise<GenUIResponse> {
const LOVABLE_AI_URL = _AI_CHAT_URL;
const LOVABLE_AI_KEY = Deno.env.get('LOVABLE_API_KEY');
if (!LOVABLE_AI_KEY) {
console.log('[CozeLangchain] LOVABLE_API_KEY not set, returning text only');
return { answer: '当前 AI 服务不可用,请稍后再试。' };
const apiKey = Deno.env.get("QWEN_API_KEY");
if (!apiKey) {
throw new Error("QWEN_API_KEY 未配置");
}
console.log(`[CozeLangchain] Falling back to Lovable AI Gateway for ${agentType}`);
const systemPrompt = `${GENUI_COMPONENT_LIBRARY}
## 当前 Agent 角色
${getAgentDisplayName(agentType)}
## 当前数据上下文
${dataContext}
请根据用户问题和数据,生成合适的 UI Schema 和回答。`;
const response = await fetch(LOVABLE_AI_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${_AI_CHAT_KEY}`,
console.log(`[CozeLangchain] Calling Qwen API for ${agentType}...`);
const response = await fetch(
"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: "qwen-plus",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: text },
],
temperature: 0.7,
max_tokens: 4000,
}),
},
body: JSON.stringify({
model: _mapAiModel('google/gemini-2.5-flash'),
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: text },
],
temperature: 0.7,
max_tokens: 4000,
}),
});
);
if (!response.ok) {
throw new Error(`Lovable AI 调用失败: ${response.status}`);
const errorText = await response.text();
console.error(`[CozeLangchain] Qwen API error (${response.status}):`, errorText);
throw new Error(`AI 调用失败 (${response.status})`);
}
const result = await response.json();
const content = result.choices?.[0]?.message?.content;
if (!content) {
return { answer: 'AI 返回内容为空' };
throw new Error("AI 返回内容为空");
}
// 解析 JSON
let jsonContent = content.trim();
const jsonMatch = jsonContent.match(/```(?:json)?\s*([\s\S]*?)```/);
if (jsonMatch) jsonContent = jsonMatch[1].trim();
try {
const parsed = JSON.parse(jsonContent);
return {
answer: parsed.answer || '已为您生成界面',
ui_schema: parsed.ui_schema,
suggested_actions: parsed.suggested_actions,
};
} catch {
return { answer: content };
}
console.log(`[CozeLangchain] AI response length: ${content.length}`);
return parseAIResponse(content);
}
// ========================
......@@ -572,7 +427,7 @@ ${dataContext}
serve(async (req) => {
const traceId = crypto.randomUUID();
if (req.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
......@@ -585,7 +440,7 @@ serve(async (req) => {
const supabase = createClient(supabaseUrl, supabaseKey);
const body: CozeLangchainRequest = await req.json();
const { text, stream = false, session_id, agent_type, skip_data_context = false } = body;
const { text, session_id, agent_type, skip_data_context = false } = body;
if (!text || typeof text !== "string") {
throw new Error("text 参数是必填的字符串");
......@@ -604,38 +459,16 @@ serve(async (req) => {
console.log(`[CozeLangchain] Data context length: ${dataContext.length}`);
}
// 调用 AI 大模型(aiProvider 内部自动处理路由和降级)
let result: GenUIResponse;
let usedFallback = false;
// 尝试调用 Coze Agent
try {
result = await callCozeAgent(text, currentSessionId, dataContext, detectedAgentType);
console.log(`[CozeLangchain] Coze succeeded, has ui_schema: ${!!result.ui_schema}`);
// 如果 Coze 返回空内容或默认错误消息,主动降级
const isEmpty = !result.answer ||
result.answer.includes('无法解析响应内容') ||
result.answer.includes('请检查 Coze Agent');
if (isEmpty && !result.ui_schema) {
console.warn(`[CozeLangchain] Coze returned empty content, forcing fallback`);
throw new Error('Coze returned empty content');
}
} catch (cozeError) {
console.error(`[CozeLangchain] Coze failed, falling back:`, cozeError);
usedFallback = true;
// 降级到 Lovable AI Gateway
try {
result = await callLovableAIFallback(text, dataContext, detectedAgentType);
console.log(`[CozeLangchain] Fallback succeeded, has ui_schema: ${!!result.ui_schema}`);
} catch (fallbackError) {
console.error(`[CozeLangchain] Fallback also failed:`, fallbackError);
// 最终降级 - 返回基础文本
result = {
answer: `抱歉,AI 服务暂时不可用。您查询的是 ${getAgentDisplayName(detectedAgentType)} 相关内容,请稍后再试。`,
};
}
result = await callAI(text, dataContext, detectedAgentType);
console.log(`[CozeLangchain] AI succeeded, has ui_schema: ${!!result.ui_schema}`);
} catch (aiError) {
console.error(`[CozeLangchain] AI call failed:`, aiError);
result = {
answer: `抱歉,AI 服务暂时不可用。您查询的是 ${getAgentDisplayName(detectedAgentType)} 相关内容,请稍后再试。`,
};
}
// 返回响应
......@@ -648,8 +481,8 @@ serve(async (req) => {
};
return new Response(JSON.stringify(response), {
headers: {
...corsHeaders,
headers: {
...corsHeaders,
"Content-Type": "application/json",
"X-Trace-Id": traceId,
"X-Agent-Type": detectedAgentType,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment