wpmind/includes/API/Services/StructuredOutputService.php
LinuxJoy b0b5032907 refactor: PublicAPI 拆分为 Facade + 6 个 Service 类
将 PublicAPI.php (2124行/76KB) 拆分为 Facade 模式架构:
- PublicAPI.php: 瘦 Facade (398行),保留单例、递归保护、状态方法
- Services/AbstractService.php: 共享基础设施 (provider解析/failover/缓存)
- Services/ChatService.php: chat + stream + SDK路由 + HTTP请求
- Services/TextProcessingService.php: translate + summarize + moderate
- Services/StructuredOutputService.php: structured + batch + schema验证
- Services/EmbeddingService.php: embed
- Services/AudioService.php: transcribe + speech
- Services/ImageService.php: generate_image (委托ImageRouter)

所有 15 个公共方法签名不变,wpmind_*() 全局函数兼容,
递归保护留在 Facade 层,Service 内部互调不触发递归检查。
已通过 7 项回归测试验证。

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

215 lines
5.7 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Structured Output Service
*
* 处理结构化输出JSON Schema和批量处理
*
* @package WPMind
* @subpackage API\Services
* @since 3.7.0
*/
declare(strict_types=1);
namespace WPMind\API\Services;
use WP_Error;
/**
* Structured Output Service
*
* @since 3.7.0
*/
class StructuredOutputService extends AbstractService {
private ChatService $chat_service;
public function __construct(ChatService $chat_service) {
$this->chat_service = $chat_service;
}
/**
* 结构化输出JSON Schema
*
* @since 2.6.0
* @param array|string $messages 消息
* @param array $schema JSON Schema
* @param array $options 选项
* @return array|WP_Error
*/
public function structured($messages, array $schema, array $options = []) {
$defaults = [
'context' => 'structured',
'max_tokens' => 2000,
'temperature' => 0.3,
'retries' => 3,
];
$options = wp_parse_args($options, $defaults);
$context = $options['context'];
$schema_json = wp_json_encode($schema, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
$schema_prompt = "你必须返回严格符合以下 JSON Schema 的 JSON 对象。不要返回其他内容,只返回 JSON\n\n```json\n{$schema_json}\n```";
if (is_string($messages)) {
$messages = [
['role' => 'system', 'content' => $schema_prompt],
['role' => 'user', 'content' => $messages],
];
} else {
array_unshift($messages, ['role' => 'system', 'content' => $schema_prompt]);
}
$max_retries = $options['retries'];
$last_error = null;
for ($attempt = 1; $attempt <= $max_retries; $attempt++) {
$result = $this->chat_service->chat($messages, [
'context' => $context,
'max_tokens' => $options['max_tokens'],
'temperature' => $options['temperature'],
'json_mode' => true,
'cache_ttl' => 0,
]);
if (is_wp_error($result)) {
return $result;
}
$content = $result['content'];
$parsed = json_decode($content, true);
if (json_last_error() === JSON_ERROR_NONE) {
if ($this->validate_schema($parsed, $schema)) {
return [
'data' => $parsed,
'provider' => $result['provider'],
'model' => $result['model'],
'usage' => $result['usage'],
'attempts' => $attempt,
];
}
}
$last_error = json_last_error_msg();
$messages[] = ['role' => 'assistant', 'content' => $content];
$messages[] = [
'role' => 'user',
'content' => "JSON 解析失败或不符合 Schema: {$last_error}。请重新生成严格符合 Schema 的 JSON。",
];
}
return new WP_Error(
'wpmind_structured_failed',
sprintf(__('结构化输出失败(尝试 %d 次): %s', 'wpmind'), $max_retries, $last_error)
);
}
/**
* 批量处理
*
* @since 2.6.0
* @param array $items 要处理的项目数组
* @param string $prompt_template Prompt 模板
* @param array $options 选项
* @return array|WP_Error
*/
public function batch(array $items, string $prompt_template, array $options = []) {
$defaults = [
'context' => 'batch',
'max_tokens' => 500,
'temperature' => 0.7,
'concurrency' => 1,
'delay_ms' => 100,
'stop_on_error' => false,
];
$options = wp_parse_args($options, $defaults);
$context = $options['context'];
$results = [];
$errors = [];
do_action('wpmind_before_request', 'batch', compact('items', 'prompt_template', 'options'), $context);
foreach ($items as $index => $item) {
$item_str = is_array($item) ? wp_json_encode($item, JSON_UNESCAPED_UNICODE) : (string)$item;
$prompt = str_replace('{{item}}', $item_str, $prompt_template);
$prompt = str_replace('{{index}}', (string)$index, $prompt);
$result = $this->chat_service->chat($prompt, [
'context' => $context . '_item_' . $index,
'max_tokens' => $options['max_tokens'],
'temperature' => $options['temperature'],
'cache_ttl' => 0,
]);
if (is_wp_error($result)) {
$errors[$index] = $result->get_error_message();
if ($options['stop_on_error']) {
break;
}
$results[$index] = null;
} else {
$results[$index] = [
'content' => $result['content'],
'usage' => $result['usage'],
];
}
if ($options['delay_ms'] > 0 && $index < count($items) - 1) {
usleep($options['delay_ms'] * 1000);
}
}
$total_tokens = array_sum(array_map(function($r) {
return $r['usage']['total_tokens'] ?? 0;
}, array_filter($results)));
do_action('wpmind_after_request', 'batch', $results, compact('items', 'options'), ['total_tokens' => $total_tokens]);
return [
'results' => $results,
'errors' => $errors,
'total_items' => count($items),
'success_count'=> count(array_filter($results)),
'error_count' => count($errors),
'total_tokens' => $total_tokens,
];
}
/**
* 验证 JSON Schema简化版
*
* @param array $data 数据
* @param array $schema Schema
* @return bool
*/
public function validate_schema(array $data, array $schema): bool {
if (isset($schema['required'])) {
foreach ($schema['required'] as $field) {
if (!isset($data[$field])) {
return false;
}
}
}
if (isset($schema['properties'])) {
foreach ($schema['properties'] as $key => $prop) {
if (!isset($data[$key])) continue;
$value = $data[$key];
$type = $prop['type'] ?? null;
if ($type === 'string' && !is_string($value)) return false;
if ($type === 'integer' && !is_int($value)) return false;
if ($type === 'number' && !is_numeric($value)) return false;
if ($type === 'boolean' && !is_bool($value)) return false;
if ($type === 'array' && !is_array($value)) return false;
if ($type === 'object' && !is_array($value)) return false;
}
}
return true;
}
}