M5: Workerman 常驻服务 + AI 审核/润色插件 + 支付插件(支付宝/微信/沙箱)+ 会员插件(套餐/订阅/付费文章)

This commit is contained in:
ak
2026-08-11 18:29:35 +08:00
parent f77c96d3aa
commit 1d1df43cee
52 changed files with 1914 additions and 20 deletions
@@ -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');
+13
View File
@@ -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;
}
+1 -1
View File
@@ -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
);
}
+63
View File
@@ -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;
}
}
+4 -1
View File
@@ -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
+6 -4
View File
@@ -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);
+41
View File
@@ -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;
}
}
+33
View File
@@ -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);
}
}
}