参考文章:https://cloud.tencent.com/developer/article/2550390
1.获取完成之后直接全部展示
php
<?php
# 若没有配置环境变量,可用百炼API Key将下行替换为:$api_key="sk-xxx"。但不建议在生产环境中直接将API Key硬编码到代码中,以减少API Key泄露风险。
$api_key = "sk-290b2ab12318469b8571ef6352x012c1";
$application_id = '711dcbebd0d94354821e0032xaa2dx'; // 替换为实际的应用 ID
$url = "https://dashscope.aliyuncs.com/api/v1/apps/$application_id/completion";
// 构造请求数据
$data = [
"input" => [
'prompt' => '你是谁?'
]
];
// 将数据编码为 JSON
$dataString = json_encode($data);
// 检查 json_encode 是否成功
if (json_last_error() !== JSON_ERROR_NONE) {
die("JSON encoding failed with error: " . json_last_error_msg());
}
// 初始化 cURL 对话
$ch = curl_init($url);
// 设置 cURL 选项
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $dataString);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// 在ai.php或相关文件中的curl请求前添加
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $api_key
]);
// 执行请求
$response = curl_exec($ch);
// 检查 cURL 执行是否成功
if ($response === false) {
die("cURL Error: " . curl_error($ch));
}
// 获取 HTTP 状态码
$status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// 关闭 cURL 对话
curl_close($ch);
// 解码响应数据
$response_data = json_decode($response, true);
// 处理响应
if ($status_code == 200) {
if (isset($response_data['output']['text'])) {
echo "{$response_data['output']['text']}\n";
} else {
echo "No text in response.\n";
}}
else {
if (isset($response_data['request_id'])) {
echo "request_id={$response_data['request_id']}\n";}
echo "code={$status_code}\n";
if (isset($response_data['message'])) {
echo "message={$response_data['message']}\n";}
else {
echo "message=Unknown error\n";}
}
?>
2.SSE流式响应输出
php
<?php
namespace app\controller;
use app\BaseController;
use think\facade\Log;
class APi extends BaseController
{
// 统一API密钥(避免两处冗余配置)
private $apiKey = 'xxx';
private $baseUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions';
/**
* 流式对话方法(实际业务调用的核心方法)
* @param string $prompt 用户输入的提示词
* @param string $model 模型名称,默认 'qwen-plus'
* @param callable $callback 流式响应的回调函数
* @return array 最终结果
*/
private function streamChat($prompt, $model = 'qwen-plus', $callback = null) {
if (empty($prompt)) {
throw new \Exception('请输入有效问题');
}
// 构建请求数据
$data = [
'model' => $model,
'messages' => [
['role' => 'user', 'content' => $prompt]
],
'stream' => true,
'stream_options' => ['include_usage' => true]
];
// 设置请求头
$headers = [
'Authorization: Bearer ' . $this->apiKey,
'Content-Type: application/json; charset=utf-8' // 明确字符集,避免编码问题
];
// 初始化curl
$ch = curl_init();
// 关键修复:统一禁用SSL验证(测试环境临时方案,生产环境请用CA证书)
curl_setopt_array($ch, [
CURLOPT_URL => $this->baseUrl,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => json_encode($data, JSON_UNESCAPED_UNICODE), // 防止中文转义
CURLOPT_WRITEFUNCTION => function($ch, $chunk) use ($callback) {
$this->processStreamChunk($chunk, $callback);
return strlen($chunk);
},
CURLOPT_TIMEOUT => 120, // 超时时间(秒)
CURLOPT_SSL_VERIFYPEER => false, // 禁用服务器证书验证(核心修复点1)
CURLOPT_SSL_VERIFYHOST => 0, // 禁用主机名验证(核心修复点2,0为不验证)
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, // 强制HTTP/1.1,适配流式传输
CURLOPT_CONNECTTIMEOUT => 30, // 连接超时,避免长时间等待
]);
// 执行请求
$response = curl_exec($ch);
if (curl_errno($ch)) {
throw new \Exception('CURL Error: ' . curl_error($ch) . ' (错误码:' . curl_errno($ch) . ')');
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new \Exception('API Request Failed. HTTP Code: ' . $httpCode . ',响应内容:' . $response);
}
return [
'success' => true,
'message' => 'Stream completed'
];
}
/**
* 处理流式响应的数据块
*/
private function processStreamChunk($chunk, $callback) {
if (empty($chunk)) return;
// 按行分割数据(SSE格式,保留原始换行,避免trim丢失关键数据)
$lines = explode("\n", $chunk);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
// 解析SSE格式:data: {...}
if (strpos($line, 'data: ') === 0) {
$jsonStr = trim(substr($line, 6)); // 去掉 "data: " 并清理空格
if ($jsonStr === '[DONE]') {
// 流式传输结束,触发done回调(核心修复:让前端感知结束)
if (is_callable($callback)) {
call_user_func($callback, [
'type' => 'done',
'message' => '传输完成'
]);
}
continue;
}
try {
$data = json_decode($jsonStr, true);
if (json_last_error() !== JSON_ERROR_NONE) {
Log::warning('JSON解析失败:' . json_last_error_msg() . ',原始数据:' . $jsonStr);
continue;
}
// 调用回调处理数据
if (is_callable($callback)) {
call_user_func($callback, $data);
} else {
$this->defaultStreamHandler($data);
}
} catch (\Exception $e) {
Log::error('处理流式数据失败:' . $e->getMessage());
continue;
}
}
}
}
/**
* 默认的流式处理器
*/
private function defaultStreamHandler($data) {
if (isset($data['choices'][0]['delta']['content'])) {
$content = $data['choices'][0]['delta']['content'];
echo $content;
flush();
}
}
/**
* SSE入口方法
*/
public function index() {
// 禁止ThinkPHP默认的输出缓冲(框架可能额外开启缓冲,导致SSE不实时)
ob_end_clean();
// 设置SSE响应头(必须在所有输出之前,包括框架的默认输出)
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache, no-store');
header('Connection: keep-alive');
header('X-Accel-Buffering: no'); // 禁用Nginx缓冲
header('X-Cache: no-cache'); // 禁用CDN缓冲
header('Access-Control-Allow-Origin: *'); // 允许跨域(前端调试需要可保留)
ob_implicit_flush(1); // 开启隐式刷新,所有echo立即输出,无需重复调用flush()
// 获取用户输入(过滤空值,避免无效请求)
$question = trim($_GET['question'] ?? '');
try {
$this->streamChat($question, 'qwen-plus', function($data) {
// 1. 处理流式内容(核心:AI实时返回的文字)
if (isset($data['choices'][0]['delta']['content'])) {
$content = $data['choices'][0]['delta']['content'];
echo "event: update\n";
echo "data: " . json_encode(['content' => $content], JSON_UNESCAPED_UNICODE) . "\n\n";
}
// 2. 处理使用量统计(部分流式接口结束时返回)
elseif (isset($data['usage'])) {
echo "event: usage\n";
echo "data: " . json_encode(['usage' => $data['usage']], JSON_UNESCAPED_UNICODE) . "\n\n";
}
// 3. 处理传输结束(核心修复:响应[DONE]标识,让前端停止监听)
elseif (isset($data['type']) && $data['type'] === 'done') {
echo "event: end\n";
echo "data: " . json_encode(['message' => $data['message']], JSON_UNESCAPED_UNICODE) . "\n\n";
}
// 4. 处理其他异常数据(日志记录,不影响前端)
else {
Log::info('未处理的流式数据:', $data);
}
});
} catch (\Exception $e) {
// 统一错误输出格式,前端可捕获error事件
echo "event: error\n";
echo "data: " . json_encode([
'message' => 'API调用失败: ' . $e->getMessage(),
'code' => -1
], JSON_UNESCAPED_UNICODE) . "\n\n";
Log::error('SSE接口异常:' . $e->getMessage(), ['question' => $_GET['question'] ?? '']);
}
// 结束输出,避免后续框架输出干扰SSE
exit;
}
/**
* 备用AI流式接口(已修复重复SSL配置问题,与主方法统一密钥)
*/
private function callAiStream(string $prompt, callable $callback)
{
if (empty($prompt)) {
throw new \Exception('请输入有效提示词');
}
$application_id = '711dcbebd0d94354821e0032e9xxxd3';
$url = "https://dashscope.aliyuncs.com/api/v1/apps/$application_id/completion";
// 构造请求数据
$data = [
"input" => [
'prompt' => $prompt
]
];
$dataString = json_encode($data, JSON_UNESCAPED_UNICODE);
$ch = curl_init();
// 修复:统一在curl_setopt_array中设置所有配置,避免重复设置导致失效
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json; charset=utf-8',
'Authorization: Bearer ' . $this->apiKey // 统一使用类的apiKey,避免冗余
],
CURLOPT_POSTFIELDS => $dataString,
CURLOPT_WRITEFUNCTION => function($ch, $data) use ($callback) {
$callback($data);
return strlen($data);
},
CURLOPT_TIMEOUT => 120,
CURLOPT_CONNECTTIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => false, // 禁用SSL验证
CURLOPT_SSL_VERIFYHOST => 0, // 禁用主机名验证
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
]);
curl_exec($ch);
if (curl_errno($ch)) {
throw new \Exception('CURL Error: ' . curl_error($ch) . ' (错误码:' . curl_errno($ch) . ')');
}
curl_close($ch);
}
/**
* 解析AI响应数据
*/
private function parseAiResponse(string $chunk): array
{
$result = ['content' => ''];
if (empty($chunk)) return $result;
$lines = explode("\n", $chunk);
foreach ($lines as $line) {
$line = trim($line);
if (strpos($line, 'data: ') === 0) {
$json = trim(substr($line, 6));
if ($json === '[DONE]') break;
$data = json_decode($json, true);
if (json_last_error() === JSON_ERROR_NONE && isset($data['choices'][0]['delta']['content'])) {
$result['content'] .= $data['choices'][0]['delta']['content'];
}
}
}
return $result;
}
}
前端页面
bash
<template>
<div>
<input v-model="question" placeholder="输入问题...">
<button @click="startStream">开始对话</button>
<div id="output" v-html="output"></div>
</div>
</template>
<script>
export default {
data() {
return {
question: '',
output: '',
eventSource: null
}
},
methods: {
startStream() {
// 清空前一次输出
this.output = '';
// 关闭现有连接
if (this.eventSource) this.eventSource.close();
// 创建SSE连接
this.eventSource = new EventSource(`/api/index?question=${encodeURIComponent(this.question)}`);
// 监听消息事件
this.eventSource.addEventListener('update', event => {
const data = JSON.parse(event.data);
this.output += data.content; // 增量更新内容
});
// 监听结束事件
this.eventSource.addEventListener('end', () => {
this.eventSource.close();
console.log('流式传输结束');
});
// 错误处理
this.eventSource.addEventListener('error', err => {
console.error('SSE错误:', err);
this.eventSource.close();
});
}
},
beforeUnmount() {
// 组件卸载时关闭连接
if (this.eventSource) this.eventSource.close();
}
}
</script>