feat & fix : intégration IA éditeur + onglet admin IA + corrections CSP (v1.6.24-25)
- #96 : boutons IA sidebar éditeur (analyse critique / réécriture) via Anthropic API - #97 : onglet admin /admin/ia — provider anthropic/claude_code, modèle, procédure CLI - #95 : extraction scripts inline vers fichiers JS (comments.js, post_confirm.js, admin.js) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class AiService
|
||||
{
|
||||
private const SYSTEM_CRITIQUE = <<<'PROMPT'
|
||||
Tu es un relecteur expert de blogs. Analyse l'article ci-dessous et identifie ses faiblesses : arguments insuffisamment étayés, imprécisions, manques de clarté, structure à améliorer, points à développer. Sois constructif et précis. Réponds en markdown avec des sections claires.
|
||||
PROMPT;
|
||||
|
||||
private const SYSTEM_REWRITE = <<<'PROMPT'
|
||||
Tu es un rédacteur expert. Réécris l'article ci-dessous en améliorant le style, la clarté et la structure, sans modifier le sens ni les faits. Conserve le format markdown, les liens et les références aux images. Réponds uniquement avec l'article réécrit, sans commentaire ni explication.
|
||||
PROMPT;
|
||||
|
||||
private string $apiKey;
|
||||
private string $model;
|
||||
private string $provider;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
require_once BASE_PATH . '/src/SiteSettings.php';
|
||||
$this->provider = aiProvider();
|
||||
$this->model = aiModel();
|
||||
$this->apiKey = $_ENV['ANTHROPIC_API_KEY'] ?? getenv('ANTHROPIC_API_KEY') ?: '';
|
||||
}
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
if ($this->provider === 'claude_code') {
|
||||
return is_executable('/usr/local/bin/claude');
|
||||
}
|
||||
return $this->apiKey !== '';
|
||||
}
|
||||
|
||||
/** @return array{ok: bool, text?: string, error?: string} */
|
||||
public function query(string $action, string $title, string $content): array
|
||||
{
|
||||
$content = mb_substr(trim($content), 0, 8000);
|
||||
if ($content === '') {
|
||||
return ['ok' => false, 'error' => "Contenu de l'article vide"];
|
||||
}
|
||||
|
||||
$systemPrompt = match ($action) {
|
||||
'critique' => self::SYSTEM_CRITIQUE,
|
||||
'rewrite' => self::SYSTEM_REWRITE,
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($systemPrompt === null) {
|
||||
return ['ok' => false, 'error' => 'Action inconnue'];
|
||||
}
|
||||
|
||||
$userMsg = $title !== '' ? "# {$title}\n\n{$content}" : $content;
|
||||
|
||||
if ($this->provider === 'claude_code') {
|
||||
return $this->queryClaudeCode($systemPrompt, $userMsg);
|
||||
}
|
||||
|
||||
return $this->queryAnthropic($action, $systemPrompt, $userMsg);
|
||||
}
|
||||
|
||||
/** @return array{ok: bool, text?: string, error?: string} */
|
||||
private function queryAnthropic(string $action, string $systemPrompt, string $userMsg): array
|
||||
{
|
||||
if ($this->apiKey === '') {
|
||||
return ['ok' => false, 'error' => 'Clé Anthropic non configurée (ANTHROPIC_API_KEY manquante dans .env)'];
|
||||
}
|
||||
|
||||
$maxTokens = ($action === 'rewrite') ? 4096 : 1200;
|
||||
|
||||
$payload = json_encode([
|
||||
'model' => $this->model,
|
||||
'max_tokens' => $maxTokens,
|
||||
'system' => $systemPrompt,
|
||||
'messages' => [['role' => 'user', 'content' => $userMsg]],
|
||||
]);
|
||||
|
||||
$ch = curl_init('https://api.anthropic.com/v1/messages');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_TIMEOUT => 60,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'x-api-key: ' . $this->apiKey,
|
||||
'anthropic-version: 2023-06-01',
|
||||
'Content-Type: application/json',
|
||||
],
|
||||
]);
|
||||
|
||||
$resp = curl_exec($ch);
|
||||
$http = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($err !== '') {
|
||||
return ['ok' => false, 'error' => 'Erreur réseau : ' . $err];
|
||||
}
|
||||
|
||||
$data = json_decode((string) $resp, true);
|
||||
|
||||
if ($http !== 200) {
|
||||
$msg = $data['error']['message'] ?? ('Anthropic HTTP ' . $http);
|
||||
return ['ok' => false, 'error' => $msg];
|
||||
}
|
||||
|
||||
$text = $data['content'][0]['text'] ?? '';
|
||||
return ['ok' => true, 'text' => $text];
|
||||
}
|
||||
|
||||
/** @return array{ok: bool, text?: string, error?: string} */
|
||||
private function queryClaudeCode(string $systemPrompt, string $userMsg): array
|
||||
{
|
||||
$bin = '/usr/local/bin/claude';
|
||||
if (!is_executable($bin)) {
|
||||
return ['ok' => false, 'error' => 'Claude Code CLI introuvable (/usr/local/bin/claude)'];
|
||||
}
|
||||
|
||||
$prompt = $systemPrompt . "\n\n" . $userMsg;
|
||||
$cmd = $bin . ' --print ' . escapeshellarg($prompt) . ' 2>&1';
|
||||
$env = ['HOME' => '/var/lib/claude-www', 'PATH' => '/usr/local/bin:/usr/bin:/bin'];
|
||||
$desc = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
|
||||
$proc = proc_open($cmd, $desc, $pipes, '/tmp', $env);
|
||||
|
||||
if (!is_resource($proc)) {
|
||||
return ['ok' => false, 'error' => 'proc_open échoué'];
|
||||
}
|
||||
|
||||
fclose($pipes[0]);
|
||||
$out = stream_get_contents($pipes[1]);
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
$code = proc_close($proc);
|
||||
|
||||
if ($code !== 0) {
|
||||
return ['ok' => false, 'error' => 'Claude Code exit ' . $code . ' : ' . trim((string)$out)];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'text' => trim((string)$out)];
|
||||
}
|
||||
}
|
||||
+15
-1
@@ -93,10 +93,24 @@ function asGroups(): array
|
||||
return is_array($raw) ? $raw : [];
|
||||
}
|
||||
|
||||
function aiProvider(): string
|
||||
{
|
||||
$v = siteSettings()['ai_provider'] ?? '';
|
||||
if ($v !== '') return $v;
|
||||
return $_ENV['AI_PROVIDER'] ?? getenv('AI_PROVIDER') ?: 'anthropic';
|
||||
}
|
||||
|
||||
function aiModel(): string
|
||||
{
|
||||
$v = siteSettings()['ai_model'] ?? '';
|
||||
if ($v !== '') return $v;
|
||||
return $_ENV['AI_MODEL'] ?? getenv('AI_MODEL') ?: 'claude-haiku-4-5-20251001';
|
||||
}
|
||||
|
||||
function saveSiteSettings(array $data): bool
|
||||
{
|
||||
$current = siteSettings();
|
||||
$stringKeys = ['site_title', 'site_claim', 'site_lang', 'site_license_label', 'site_license_url', 'apache_access_log', 'folio_repo_url', 'folio_update_branch'];
|
||||
$stringKeys = ['site_title', 'site_claim', 'site_lang', 'site_license_label', 'site_license_url', 'apache_access_log', 'folio_repo_url', 'folio_update_branch', 'ai_provider', 'ai_model'];
|
||||
foreach ($stringKeys as $key) {
|
||||
if (array_key_exists($key, $data)) {
|
||||
$val = trim((string)$data[$key]);
|
||||
|
||||
Reference in New Issue
Block a user