M5: Workerman 常驻服务 + AI 审核/润色插件 + 支付插件(支付宝/微信/沙箱)+ 会员插件(套餐/订阅/付费文章)
This commit is contained in:
@@ -68,6 +68,9 @@ class CommentController extends Controller
|
||||
return $comment;
|
||||
});
|
||||
|
||||
// 插件钩子:AI 审核等
|
||||
app(\App\Blog\Support\PluginManager::class)->doAction('comment.created', $comment);
|
||||
|
||||
if ($status === 'pending') {
|
||||
return back()->with('success', '评论已提交,等待审核通过后显示')->withFragment('comments');
|
||||
}
|
||||
|
||||
@@ -147,6 +147,8 @@ class LegacyController extends Controller
|
||||
$post->increment('comment_count');
|
||||
}
|
||||
|
||||
app(\App\Blog\Support\PluginManager::class)->doAction('comment.created', $comment);
|
||||
|
||||
return redirect()->route('posts.show', $post->slug ?: $post->id)
|
||||
->with($status === 'published' ? 'success' : 'error', $status === 'published' ? '评论已发布' : '评论已提交,等待审核')
|
||||
->withFragment('comments');
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Blog\Jobs;
|
||||
|
||||
use App\Blog\Services\LlmClient;
|
||||
|
||||
/**
|
||||
* 由 Workerman 消费者执行的 AI 任务统一接口。
|
||||
*/
|
||||
interface AiJob
|
||||
{
|
||||
public function handle(LlmClient $llm): void;
|
||||
}
|
||||
@@ -16,11 +16,6 @@ class PluginManagerServiceProvider extends ServiceProvider
|
||||
{
|
||||
$manager = $this->app->make(PluginManager::class);
|
||||
|
||||
// 迁移尚未执行(migrate:fresh 首轮)时跳过,避免查询不存在的表
|
||||
if (! \Illuminate\Support\Facades\Schema::hasTable('plugin_records')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! is_dir(config('plugins.path'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class ThemeServiceProvider extends ServiceProvider
|
||||
|
||||
// 侧边栏数据共享:所有前台视图自动获得 $categories/$recentPosts/$hotTags/$links 等
|
||||
View::composer(
|
||||
['index', 'show', 'list', 'archives', 'tags', 'tag', 'search', 'links', 'comments', 'login', 'register', 'profile'],
|
||||
['index', 'show', 'list', 'archives', 'tags', 'tag', 'search', 'links', 'comments', 'login', 'register', 'profile', 'membership.*', 'payments.*'],
|
||||
SidebarComposer::class
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Blog\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* OpenAI 兼容 LLM 客户端(支持 OpenAI / DeepSeek / 通义 / 自建代理)。
|
||||
*/
|
||||
class LlmClient
|
||||
{
|
||||
public function baseUrl(): string
|
||||
{
|
||||
return rtrim((string) blog_setting('llm_base_url', config('services.llm.base_url', env('LLM_BASE_URL'))), '/');
|
||||
}
|
||||
|
||||
public function apiKey(): string
|
||||
{
|
||||
return (string) blog_setting('llm_api_key', config('services.llm.api_key', env('LLM_API_KEY')));
|
||||
}
|
||||
|
||||
public function model(): string
|
||||
{
|
||||
return (string) blog_setting('llm_model', config('services.llm.model', env('LLM_MODEL', 'gpt-4o-mini')));
|
||||
}
|
||||
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return $this->baseUrl() !== '' && $this->apiKey() !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的聊天补全。
|
||||
*
|
||||
* @param array<int, array{role: string, content: string}> $messages
|
||||
*/
|
||||
public function chat(array $messages, array $options = []): string
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
throw new \RuntimeException('LLM 未配置:请设置 LLM_BASE_URL 与 LLM_API_KEY');
|
||||
}
|
||||
|
||||
$response = Http::timeout(60)
|
||||
->withToken($this->apiKey())
|
||||
->post($this->baseUrl().'/chat/completions', [
|
||||
'model' => $options['model'] ?? $this->model(),
|
||||
'messages' => $messages,
|
||||
'temperature' => $options['temperature'] ?? 0.3,
|
||||
'max_tokens' => $options['max_tokens'] ?? 1024,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('LLM 请求失败', ['status' => $response->status(), 'body' => $response->body()]);
|
||||
|
||||
throw new \RuntimeException('LLM 请求失败:HTTP '.$response->status());
|
||||
}
|
||||
|
||||
$content = $response->json('choices.0.message.content', '');
|
||||
|
||||
return is_array($content) ? json_encode($content, JSON_UNESCAPED_UNICODE) : (string) $content;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,10 @@ class PostContentRenderer
|
||||
? $this->toHtml($post->content)
|
||||
: $post->content;
|
||||
|
||||
return $this->renderShortcodes($content, $post);
|
||||
$html = $this->renderShortcodes($content, $post);
|
||||
|
||||
// 插件钩子:付费内容过滤、AI 处理等
|
||||
return app(\App\Blog\Support\PluginManager::class)->applyFilters('post.rendered', $html, $post);
|
||||
}
|
||||
|
||||
public function toHtml(string $markdown): string
|
||||
|
||||
@@ -68,12 +68,14 @@ class PluginManager
|
||||
|
||||
public function isEnabled(string $plugin): bool
|
||||
{
|
||||
[$vendor, $name] = array_pad(explode('.', $plugin), 2, $plugin);
|
||||
if (\Illuminate\Support\Facades\Schema::hasTable('plugin_records')) {
|
||||
[$vendor, $name] = array_pad(explode('.', $plugin), 2, $plugin);
|
||||
|
||||
$record = PluginRecord::query()->where('vendor', $vendor)->where('name', $name)->first();
|
||||
$record = PluginRecord::query()->where('vendor', $vendor)->where('name', $name)->first();
|
||||
|
||||
if ($record) {
|
||||
return (bool) $record->enabled;
|
||||
if ($record) {
|
||||
return (bool) $record->enabled;
|
||||
}
|
||||
}
|
||||
|
||||
return in_array($plugin, config('plugins.enabled', []), true);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Blog\Support;
|
||||
|
||||
/**
|
||||
* 从已启用插件的 manifest 中收集 Filament 页面/资源。
|
||||
*/
|
||||
class PluginPages
|
||||
{
|
||||
public static function pages(): array
|
||||
{
|
||||
return self::collect('filament_pages');
|
||||
}
|
||||
|
||||
public static function resources(): array
|
||||
{
|
||||
return self::collect('filament_resources');
|
||||
}
|
||||
|
||||
private static function collect(string $key): array
|
||||
{
|
||||
$manager = app(PluginManager::class);
|
||||
$classes = [];
|
||||
|
||||
foreach ($manager->all() as $pluginKey => $plugin) {
|
||||
if (! $plugin['enabled']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$manifest = $manager->manifest($pluginKey);
|
||||
|
||||
foreach ($manifest[$key] ?? [] as $class) {
|
||||
if (class_exists($class)) {
|
||||
$classes[] = $class;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Blog\Support;
|
||||
|
||||
use Workerman\Connection\TcpConnection;
|
||||
|
||||
/**
|
||||
* Workerman WebSocket 广播器:任务执行中向后台推送进度。
|
||||
*/
|
||||
class WorkermanBroadcaster
|
||||
{
|
||||
/** @var array<int, TcpConnection> */
|
||||
public static array $connections = [];
|
||||
|
||||
public static function add(TcpConnection $connection): void
|
||||
{
|
||||
self::$connections[spl_object_id($connection)] = $connection;
|
||||
}
|
||||
|
||||
public static function remove(TcpConnection $connection): void
|
||||
{
|
||||
unset(self::$connections[spl_object_id($connection)]);
|
||||
}
|
||||
|
||||
public static function send(string $event, array $data = []): void
|
||||
{
|
||||
$message = json_encode(['event' => $event, 'data' => $data], JSON_UNESCAPED_UNICODE);
|
||||
|
||||
foreach (self::$connections as $connection) {
|
||||
$connection->send($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user