wpmind/modules/api-gateway/includes/Transform/ResponseTransformer.php
LinuxJoy 36b69d300d security: API Gateway 全量代码审计修复 (59 issues)
Codex 审计发现 124 个问题,修复 59 个 P0-P2 级别问题:

P0 (CRITICAL):
- TOCTOU 竞态: TransientRateStore/SseConcurrencyGuard 改用 wp_cache_add 原子锁
- REST 路由: __return_true → check_bearer_present 轻量鉴权
- Schema 验证: messages/tools/tool_choice 添加完整 validate_callback
- SQL 列名不匹配: upsert 查询对齐 SchemaManager 定义
- SQL 表名插值: 统一使用 %i 占位符

P1 (HIGH):
- IP hash 加盐 (HMAC) + IP 解析统一 (context 共享)
- Header 注入防护 (X-Request-Id/响应头 \r\n 过滤)
- 数值参数范围校验 (temperature/top_p/penalties/max_tokens)
- insert_key 错误处理 + revoke_key 审计日志
- Pipeline finally 保护 + RateLimiter rollback 双 store
- wp_unslash 补全 + strtotime 返回值检查
- RedisRateStore $rid 冒号验证
- UpstreamStreamClient catch \Throwable

P2 (MEDIUM):
- AuditLogRepository DRY (提取 build_where_clause)
- Token 估算 CJK 优化 (mb_strlen)
- ResponseTransformMiddleware null 检查
- 嵌入响应 usage 数据修复
- SchemaManager autoload 优化

25 files changed, ~530 insertions, ~190 deletions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 01:14:12 +08:00

198 lines
4.9 KiB
PHP

<?php
/**
* Response Transformer
*
* Converts WPMind internal results into OpenAI-compatible response formats.
*
* @package WPMind\Modules\ApiGateway\Transform
* @since 1.0.0
*/
declare(strict_types=1);
namespace WPMind\Modules\ApiGateway\Transform;
/**
* Class ResponseTransformer
*
* Transforms WPMind PublicAPI results into OpenAI-compatible
* JSON response structures.
*/
final class ResponseTransformer {
/**
* Transform a chat result into OpenAI chat.completion format.
*
* @param mixed $result Result from PublicAPI::chat().
* @param string $model The model identifier from the request.
* @param string $request_id Unique request ID.
* @return array OpenAI-compatible chat completion response.
*/
public function transform_chat( mixed $result, string $model, string $request_id ): array {
$content = $this->extract_content( $result );
return [
'id' => 'wpmind-' . $request_id,
'object' => 'chat.completion',
'created' => time(),
'model' => $model,
'choices' => [
[
'index' => 0,
'message' => [
'role' => 'assistant',
'content' => $content,
],
'finish_reason' => 'stop',
],
],
'usage' => $this->extract_usage( $result ),
];
}
/**
* Transform an embedding result into OpenAI embedding format.
*
* @param mixed $result Result from PublicAPI::embed().
* @param string $model The model identifier from the request.
* @param string $request_id Unique request ID.
* @return array OpenAI-compatible embedding response.
*/
public function transform_embedding( mixed $result, string $model, string $request_id ): array {
$embeddings = $this->extract_embeddings( $result );
$data = [];
foreach ( $embeddings as $index => $embedding ) {
$data[] = [
'object' => 'embedding',
'index' => $index,
'embedding' => $embedding,
];
}
return [
'object' => 'list',
'data' => $data,
'model' => $model,
'usage' => $this->extract_usage( $result ),
];
}
/**
* Transform a models list into OpenAI models list format.
*
* @param string[] $models List of model identifiers.
* @return array OpenAI-compatible models list response.
*/
public function transform_models( array $models ): array {
$data = [];
foreach ( $models as $model_id ) {
$data[] = [
'id' => $model_id,
'object' => 'model',
'created' => 0,
'owned_by' => 'wpmind',
];
}
return [
'object' => 'list',
'data' => $data,
];
}
/**
* Extract text content from a WPMind result.
*
* The result may be a plain string, or an array with a 'content' key,
* or an array with nested 'choices'.
*
* @param mixed $result WPMind API result.
* @return string Extracted content.
*/
private function extract_content( mixed $result ): string {
if ( is_string( $result ) ) {
return $result;
}
if ( is_array( $result ) ) {
if ( isset( $result['content'] ) && is_string( $result['content'] ) ) {
return $result['content'];
}
if ( isset( $result['choices'][0]['message']['content'] ) ) {
return (string) $result['choices'][0]['message']['content'];
}
if ( isset( $result['text'] ) && is_string( $result['text'] ) ) {
return $result['text'];
}
}
return '';
}
/**
* Extract usage data from a WPMind result.
*
* @param mixed $result WPMind API result.
* @return array{prompt_tokens: int, completion_tokens: int, total_tokens: int}
*/
private function extract_usage( mixed $result ): array {
$default = [
'prompt_tokens' => 0,
'completion_tokens' => 0,
'total_tokens' => 0,
];
if ( ! is_array( $result ) || ! isset( $result['usage'] ) ) {
return $default;
}
$usage = $result['usage'];
return [
'prompt_tokens' => (int) ( $usage['prompt_tokens'] ?? 0 ),
'completion_tokens' => (int) ( $usage['completion_tokens'] ?? 0 ),
'total_tokens' => (int) ( $usage['total_tokens'] ?? 0 ),
];
}
/**
* Extract embedding vectors from a WPMind result.
*
* @param mixed $result WPMind API result.
* @return array<int, array<float>> List of embedding vectors.
*/
private function extract_embeddings( mixed $result ): array {
if ( ! is_array( $result ) ) {
return [];
}
// Result is already a list of vectors.
if ( isset( $result[0] ) && is_array( $result[0] ) ) {
return $result;
}
// Result has a 'data' key with embeddings.
if ( isset( $result['data'] ) && is_array( $result['data'] ) ) {
$vectors = [];
foreach ( $result['data'] as $item ) {
if ( isset( $item['embedding'] ) ) {
$vectors[] = $item['embedding'];
} elseif ( is_array( $item ) && isset( $item[0] ) ) {
$vectors[] = $item;
}
}
return $vectors;
}
// Result has an 'embedding' key (single vector).
if ( isset( $result['embedding'] ) && is_array( $result['embedding'] ) ) {
return [ $result['embedding'] ];
}
return [];
}
}