wpmind/includes/API/Services/ChatService.php
LinuxJoy adb6c49301 style: phpcbf 自动格式化 — 全量 WPCS 3.x 规范对齐
135 文件 19,211 处自动修复:空格→Tab 缩进、括号间距、
函数声明空格、前置自增、尾逗号等纯格式化改动。
零逻辑变更,php -l + token 级对比验证通过。

新增 phpcs.xml.dist / phpstan.neon.dist 项目配置。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 00:48:46 +08:00

653 lines
17 KiB
PHP

<?php
/**
* Chat Service
*
* 处理 AI 对话和流式输出
*
* @package WPMind
* @subpackage API\Services
* @since 3.7.0
*/
declare(strict_types=1);
namespace WPMind\API\Services;
use WP_Error;
/**
* Chat Service
*
* @since 3.7.0
*/
class ChatService extends AbstractService {
/**
* AI 对话(核心实现)
*
* @param string|array $messages 消息
* @param array $options 选项
* @return array|WP_Error
*/
public function chat( $messages, array $options = [] ) {
$defaults = [
'context' => '',
'system' => '',
'max_tokens' => 1000,
'temperature' => 0.7,
'model' => 'auto',
'provider' => 'auto',
'json_mode' => false,
'cache_ttl' => 0,
'tools' => [],
'tool_choice' => 'auto',
'failover_providers' => [],
];
$options = wp_parse_args( $options, $defaults );
$context = $options['context'];
$normalized_messages = $this->normalize_messages( $messages, $options );
$args = [
'messages' => $normalized_messages,
'max_tokens' => $options['max_tokens'],
'temperature' => $options['temperature'],
'json_mode' => $options['json_mode'],
'tools' => $options['tools'],
'tool_choice' => $options['tool_choice'],
];
$args = apply_filters( 'wpmind_chat_args', $args, $context, $messages );
$original_model = $options['model'];
$model_is_auto = ( $original_model === 'auto' );
if ( $model_is_auto ) {
$model = $this->get_default_model();
} else {
$model = $original_model;
}
$model = apply_filters( 'wpmind_select_model', $model, $context, get_current_user_id() );
$provider = $this->resolve_provider( $options['provider'], $context );
$failover_chain = $this->get_failover_chain( $provider );
// Filter failover chain to only supported providers (e.g. vision-capable).
if ( ! empty( $options['failover_providers'] ) ) {
$allowed = $options['failover_providers'];
$failover_chain = array_values(
array_filter(
$failover_chain,
function ( $p ) use ( $allowed ) {
return in_array( $p, $allowed, true );
}
)
);
if ( empty( $failover_chain ) ) {
return new WP_Error(
'wpmind_no_supported_provider',
__( '没有可用的服务商支持此请求类型', 'wpmind' )
);
}
}
if ( ! empty( $failover_chain ) && $failover_chain[0] !== $provider ) {
do_action( 'wpmind_provider_failover', $provider, $failover_chain[0], $context );
}
$cache_key = $this->generate_cache_key( 'chat', $args, $provider, $model );
$cache_lookup = $this->get_cached_value( $cache_key, (int) $options['cache_ttl'] );
if ( $cache_lookup['hit'] ) {
return $cache_lookup['value'];
}
do_action( 'wpmind_before_request', 'chat', $args, $context );
$result = null;
$last_error = null;
$tried_providers = [];
$failover_count = count( $failover_chain );
foreach ( $failover_chain as $index => $try_provider ) {
$tried_providers[] = $try_provider;
$is_last_provider = ( $index === $failover_count - 1 );
$max_retries = $is_last_provider ? 3 : 1;
if ( $model_is_auto ) {
$try_model = $this->get_current_model( $try_provider );
} else {
$try_model = $model;
$endpoints = $this->get_endpoints();
$provider_models = $endpoints[ $try_provider ]['models'] ?? [];
if ( ! empty( $provider_models ) && ! in_array( $try_model, $provider_models, true ) ) {
$try_model = $this->get_current_model( $try_provider );
}
}
for ( $attempt = 0; $attempt <= $max_retries; $attempt++ ) {
$result = $this->execute_chat_request( $args, $try_provider, $try_model );
if ( ! is_wp_error( $result ) ) {
if ( $try_provider !== $provider ) {
$result['failover'] = [
'original_provider' => $provider,
'actual_provider' => $try_provider,
'tried_providers' => $tried_providers,
];
}
if ( $try_model !== $model ) {
$result['model_fallback'] = true;
$result['original_model'] = $model;
}
break 2;
}
$last_error = $result;
$error_code = $result->get_error_code();
if ( in_array( $error_code, [ 'wpmind_api_key_missing', 'wpmind_provider_not_found' ], true ) ) {
break;
}
$error_data = $result->get_error_data();
$status = is_array( $error_data ) && isset( $error_data['status'] ) ? (int) $error_data['status'] : 0;
if ( $status > 0 && ! \WPMind\ErrorHandler::should_retry( $status ) ) {
break;
}
if ( $attempt < $max_retries ) {
$delay_ms = \WPMind\ErrorHandler::get_retry_delay( $attempt + 1 );
do_action( 'wpmind_retry', $try_provider, $attempt + 1, $status );
sleep( (int) ( $delay_ms / 1000 ) );
}
}
}
if ( is_wp_error( $result ) ) {
do_action( 'wpmind_error', $result, 'chat', $args );
return $result;
}
$result = apply_filters( 'wpmind_chat_response', $result, $args, $context );
$usage = $result['usage'] ?? [];
do_action( 'wpmind_after_request', 'chat', $result, $args, $usage );
if ( ! is_wp_error( $result ) ) {
$this->set_cached_value(
$cache_key,
$result,
(int) $options['cache_ttl'],
[
'type' => 'chat',
'context' => $context,
'provider' => $provider,
'model' => $model,
]
);
}
return $result;
}
/**
* 流式输出
*
* @since 2.6.0
* @param array|string $messages 消息
* @param callable $callback 回调函数
* @param array $options 选项
* @return bool|WP_Error
*/
public function stream( $messages, callable $callback, array $options = [] ) {
$defaults = [
'context' => '',
'system' => '',
'max_tokens' => 2000,
'temperature' => 0.7,
'model' => 'auto',
'provider' => 'auto',
];
$options = wp_parse_args( $options, $defaults );
$context = $options['context'];
$normalized_messages = $this->normalize_messages( $messages, $options );
$original_model = $options['model'];
$model_is_auto = ( $original_model === 'auto' );
$provider = $this->resolve_provider( $options['provider'], $context );
$failover_chain = $this->get_failover_chain( $provider );
if ( ! empty( $failover_chain ) && $failover_chain[0] !== $provider ) {
do_action( 'wpmind_provider_failover', $provider, $failover_chain[0], $context );
}
do_action( 'wpmind_before_request', 'stream', compact( 'messages', 'options' ), $context );
$endpoints = $this->get_endpoints();
$last_error = null;
foreach ( $failover_chain as $index => $try_provider ) {
if ( ! isset( $endpoints[ $try_provider ] ) ) {
$last_error = new WP_Error(
'wpmind_provider_not_found',
sprintf( __( '服务商 %s 未配置', 'wpmind' ), $try_provider )
);
continue;
}
$endpoint = $endpoints[ $try_provider ];
$api_key = $endpoint['api_key'] ?? '';
if ( empty( $api_key ) ) {
$last_error = new WP_Error(
'wpmind_api_key_missing',
sprintf( __( '服务商 %s 未配置 API Key', 'wpmind' ), $try_provider )
);
continue;
}
$model = $model_is_auto
? $this->get_current_model( $try_provider )
: $original_model;
if ( $model === 'auto' || $model === 'default' ) {
$model = $endpoint['models'][0] ?? 'gpt-3.5-turbo';
}
$base_url = $endpoint['custom_base_url'] ?? $endpoint['base_url'] ?? '';
$api_url = trailingslashit( $base_url ) . 'chat/completions';
$request_body = [
'model' => $model,
'messages' => $normalized_messages,
'max_tokens' => $options['max_tokens'],
'temperature' => $options['temperature'],
'stream' => true,
];
$stream_context_options = [
'http' => [
'method' => 'POST',
'header' => [
'Content-Type: application/json',
'Authorization: Bearer ' . $api_key,
],
'content' => wp_json_encode( $request_body ),
'timeout' => 120,
],
'ssl' => [
'verify_peer' => true,
],
];
$start_time = microtime( true );
// 检查 allow_url_fopen 是否启用
if ( ! ini_get( 'allow_url_fopen' ) ) {
$last_error = new WP_Error(
'wpmind_stream_unsupported',
__( '流式输出需要 PHP allow_url_fopen 配置启用', 'wpmind' )
);
continue;
}
$stream_ctx = stream_context_create( $stream_context_options );
$stream = @fopen( $api_url, 'r', false, $stream_ctx );
if ( ! $stream ) {
$latency_ms = (int) ( ( microtime( true ) - $start_time ) * 1000 );
$this->record_result( $try_provider, false, $latency_ms );
$last_error = new WP_Error(
'wpmind_stream_failed',
sprintf( __( '服务商 %s 无法建立流式连接', 'wpmind' ), $try_provider )
);
continue;
}
$full_content = '';
while ( ! feof( $stream ) ) {
$line = fgets( $stream );
if ( empty( $line ) ) {
continue;
}
$line = trim( $line );
if ( strpos( $line, 'data: ' ) !== 0 ) {
continue;
}
$data = substr( $line, 6 );
if ( $data === '[DONE]' ) {
break;
}
$json = json_decode( $data, true );
if ( ! $json ) {
continue;
}
$delta = $json['choices'][0]['delta']['content'] ?? '';
if ( ! empty( $delta ) ) {
$full_content .= $delta;
call_user_func( $callback, $delta, $json );
}
}
fclose( $stream );
$latency_ms = (int) ( ( microtime( true ) - $start_time ) * 1000 );
$this->record_result( $try_provider, true, $latency_ms );
do_action( 'wpmind_after_request', 'stream', [ 'content' => $full_content ], compact( 'messages', 'options' ), [] );
return true;
}
if ( $last_error ) {
do_action( 'wpmind_error', $last_error, 'stream', compact( 'messages', 'options' ) );
return $last_error;
}
return new WP_Error( 'wpmind_stream_failed', __( '无法建立流式连接', 'wpmind' ) );
}
/**
* 标准化消息格式
*
* @param array|string $messages 原始消息
* @param array $options 选项
* @return array
*/
public function normalize_messages( $messages, array $options ): array {
if ( is_string( $messages ) ) {
$normalized = [];
if ( ! empty( $options['system'] ) ) {
$normalized[] = [
'role' => 'system',
'content' => $options['system'],
];
}
$normalized[] = [
'role' => 'user',
'content' => $messages,
];
return $normalized;
}
return $messages;
}
/**
* 判断是否应该使用 SDK 执行请求
*
* @since 3.6.0
* @param string $provider 服务商 ID
* @param array $args 请求参数
* @return bool
*/
private function should_use_sdk( string $provider, array $args ): bool {
if ( ! class_exists( '\\WPMind\\SDK\\SDKAdapter' ) ) {
return false;
}
if ( ! class_exists( 'WordPress\\AiClient\\AiClient' ) ) {
return false;
}
if ( ! get_option( 'wpmind_sdk_enabled', true ) ) {
return false;
}
if ( ! empty( $args['tools'] ) ) {
return false;
}
$sdk_providers = apply_filters( 'wpmind_sdk_providers', [ 'anthropic', 'google' ] );
return in_array( $provider, $sdk_providers, true );
}
/**
* 执行 Chat 请求(路由方法)
*
* @since 3.6.0
* @param array $args 请求参数
* @param string $provider 服务商
* @param string $model 模型
* @return array|WP_Error
*/
private function execute_chat_request( array $args, string $provider, string $model ) {
if ( $this->should_use_sdk( $provider, $args ) ) {
$sdk = new \WPMind\SDK\SDKAdapter();
$start_time = microtime( true );
$result = $sdk->chat( $args, $provider, $model );
$latency_ms = (int) ( ( microtime( true ) - $start_time ) * 1000 );
if ( ! is_wp_error( $result ) ) {
$this->record_result( $provider, true, $latency_ms );
return $result;
}
$error_code = $result->get_error_code();
if ( $error_code === 'wpmind_sdk_invalid_args' || $error_code === 'wpmind_sdk_unavailable' ) {
do_action( 'wpmind_sdk_fallback', $provider, $error_code, $result->get_error_message() );
} else {
$this->record_result( $provider, false, $latency_ms );
return $result;
}
}
return $this->execute_chat_request_native( $args, $provider, $model );
}
/**
* 通过原生 HTTP 执行 chat 请求
*
* @since 3.6.0
* @param array $args 请求参数
* @param string $provider 服务商
* @param string $model 模型
* @return array|WP_Error
*/
private function execute_chat_request_native( array $args, string $provider, string $model ) {
$endpoints = $this->get_endpoints();
if ( ! isset( $endpoints[ $provider ] ) ) {
return new WP_Error(
'wpmind_provider_not_found',
sprintf( __( '服务商 %s 未配置', 'wpmind' ), $provider )
);
}
$endpoint = $endpoints[ $provider ];
$api_key = $endpoint['api_key'] ?? '';
if ( empty( $api_key ) ) {
return new WP_Error(
'wpmind_api_key_missing',
sprintf( __( '服务商 %s 未配置 API Key', 'wpmind' ), $provider )
);
}
if ( $model === 'auto' || empty( $model ) || $model === 'default' ) {
$model = $endpoint['models'][0] ?? 'gpt-3.5-turbo';
}
$request_body = [
'model' => $model,
'messages' => $args['messages'],
'max_tokens' => $args['max_tokens'],
'temperature' => $args['temperature'],
];
if ( ! empty( $args['json_mode'] ) ) {
$request_body['response_format'] = [ 'type' => 'json_object' ];
}
if ( ! empty( $args['tools'] ) ) {
$request_body['tools'] = $args['tools'];
if ( ! empty( $args['tool_choice'] ) && $args['tool_choice'] !== 'auto' ) {
$request_body['tool_choice'] = $args['tool_choice'];
}
}
$base_url = $endpoint['custom_base_url'] ?? $endpoint['base_url'] ?? '';
$api_url = trailingslashit( $base_url ) . 'chat/completions';
$start_time = microtime( true );
$response = wp_remote_post(
$api_url,
[
'headers' => [
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( $request_body ),
'timeout' => 60,
]
);
$latency_ms = (int) ( ( microtime( true ) - $start_time ) * 1000 );
if ( is_wp_error( $response ) ) {
$this->record_result( $provider, false, $latency_ms );
return new WP_Error(
'wpmind_request_failed',
sprintf( __( '请求失败: %s', 'wpmind' ), $response->get_error_message() )
);
}
$status_code = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( $status_code !== 200 ) {
$this->record_result( $provider, false, $latency_ms );
$error_message = $data['error']['message'] ?? __( '未知错误', 'wpmind' );
return new WP_Error(
'wpmind_api_error',
sprintf( __( 'API 错误 (%1$d): %2$s', 'wpmind' ), $status_code, $error_message ),
[
'status' => $status_code,
'body' => substr( (string) $body, 0, 500 ),
]
);
}
if ( ! is_array( $data ) ) {
$this->record_result( $provider, false, $latency_ms );
return new WP_Error(
'wpmind_invalid_response',
__( '服务商返回了无效的响应格式', 'wpmind' ),
[
'status' => $status_code,
'body' => substr( (string) $body, 0, 500 ),
]
);
}
$this->record_result( $provider, true, $latency_ms );
return $this->parse_chat_response( $data, $provider, $model );
}
/**
* 解析 Chat 响应
*
* @param array $response 原始响应
* @param string $provider 服务商
* @param string $model 模型
* @return array
*/
public function parse_chat_response( array $response, string $provider, string $model ): array {
$content = '';
$tool_calls = [];
$finish_reason = '';
$usage = [
'prompt_tokens' => 0,
'completion_tokens' => 0,
'total_tokens' => 0,
];
$message = $response['choices'][0]['message'] ?? [];
$finish_reason = $response['choices'][0]['finish_reason'] ?? '';
if ( isset( $message['content'] ) ) {
$content = $message['content'];
} elseif ( isset( $response['content'][0]['text'] ) ) {
$content = $response['content'][0]['text'];
}
if ( isset( $message['tool_calls'] ) && is_array( $message['tool_calls'] ) ) {
foreach ( $message['tool_calls'] as $call ) {
$tool_calls[] = [
'id' => $call['id'] ?? '',
'type' => $call['type'] ?? 'function',
'function' => [
'name' => $call['function']['name'] ?? '',
'arguments' => $call['function']['arguments'] ?? '{}',
],
];
}
}
if ( isset( $response['usage'] ) ) {
$usage = [
'prompt_tokens' => $response['usage']['prompt_tokens'] ?? 0,
'completion_tokens' => $response['usage']['completion_tokens'] ?? 0,
'total_tokens' => $response['usage']['total_tokens'] ?? 0,
];
}
$result = [
'content' => $content,
'provider' => $provider,
'model' => $model,
'usage' => $usage,
'finish_reason' => $finish_reason,
];
if ( ! empty( $tool_calls ) ) {
$result['tool_calls'] = $tool_calls;
}
return $result;
}
/**
* 默认响应过滤器
*
* @param array $response 响应
* @param array $args 参数
* @param string $context 上下文
* @return array
*/
public function filter_chat_response( array $response, array $args, string $context ): array {
return $response;
}
/**
* 公共访问器:获取当前模型(供 Facade 的 get_status() 使用)
*
* @param string $provider 服务商
* @return string
*/
public function get_current_model_public( string $provider ): string {
return $this->get_current_model( $provider );
}
}