Feed Generator 是运行在 Bluesky 上的“算法 feed”机制。你可以公开根据特定关键词或用户条件定制的 feed。使用 laravel-bluesky 可以在 Laravel 应用中轻松实现 Feed Generator。
注册 FeedGenerator 算法
最简单的用法是在 AppServiceProvider::boot() 中以闭包形式注册算法。
// 在 AppServiceProvider::boot() 中注册
use Illuminate\Http\Request;
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\FeedGenerator\FeedGenerator;
FeedGenerator::register(name: 'artisan', algo: function(int $limit, ?string $cursor, ?string $user, Request $request): array {
// 实现完全由你决定。
// 由于 API 的临时限制,这里需要认证
$response = Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password'))
->searchPosts(q: '#laravel', until: $cursor, limit: $limit);
$cursor = data_get($response->collect('posts')->last(), 'indexedAt');
$feed = $response->collect('posts')->map(function(array $post) {
return ['post' => data_get($post, 'uri')];
})->toArray();
// 也可以借助 Request 对象根据用户状态返回不同结果。
info('user: '.$user); // 请求来源用户的 DID,如 'did:plc:***'
info('header', $request->header());
return compact('cursor', 'feed');
});
name 请使用 URL 安全的字符串。
算法的返回值是包含 cursor 与 feed 的数组。
[
'cursor' => '',
'feed' => [
['post' => 'at://'],
['post' => 'at://'],
],
]
包所需的所有路由都会自动注册。
http://localhost/xrpc/app.bsky.feed.getFeedSkeleton?feed=at://did:web:example.com/app.bsky.feed.generator/artisan
http://localhost/xrpc/app.bsky.feed.describeFeedGenerator
http://localhost/.well-known/did.json
- Service DID 会根据当前 URL 自动生成(例如
did:web:example.com)。
你唯一需要决定的就是 FeedGenerator 的 name 和实现内容。
发布 feed(创建命令)
只在 Laravel 应用中实现 FeedGenerator 并不会将其发布到 Bluesky。需要创建并执行一个调用 publishFeedGenerator 的命令。
生成命令
php artisan make:command PublishGeneratorCommand
实现命令
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Generator;
class PublishGeneratorCommand extends Command
{
protected $signature = 'bluesky:publish-generator';
protected $description = '将 FeedGenerator 发布到 Bluesky';
public function handle()
{
$generator = Generator::create(did: 'did:web:example.com', displayName: 'Feed name')
->description('Feed description');
$res = Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password'))
->publishFeedGenerator(name: 'artisan', generator: $generator);
dump($res->json());
return 0;
}
}
执行命令
php artisan bluesky:publish-generator
成功后,你的 Bluesky 个人资料中的 feed 列表会新增一个链接。publishFeedGenerator 只是更新信息,可以多次执行。
创建多个 FeedGenerator
只要改变 name 并多次调用 register,就可以创建多个 feed。
// AppServiceProvider::boot()
use Revolution\Bluesky\FeedGenerator\FeedGenerator;
FeedGenerator::register(name: 'feed1', algo: function() {
// feed1 的实现
});
FeedGenerator::register(name: 'feed2', algo: function() {
// feed2 的实现
});
在发布命令中同样多次调用 publishFeedGenerator。
// PublishGeneratorCommand
Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password'));
$generator1 = Generator::create(did: 'did:web:example.com', displayName: 'Feed 1')
->description('Feed 1');
Bluesky::publishFeedGenerator(name: 'feed1', generator: $generator1);
$generator2 = Generator::create(did: 'did:web:example.com', displayName: 'Feed 2')
->description('Feed 2');
Bluesky::publishFeedGenerator(name: 'feed2', generator: $generator2);
拆分算法为独立类
相比闭包,使用独立的类可以让代码更容易维护。你可以创建实现 FeedGeneratorAlgorithm 接口的 callable 类,并在 AppServiceProvider 中注册它。
// 放在任意位置
namespace App\FeedGenerator;
use Illuminate\Http\Request;
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Contracts\FeedGeneratorAlgorithm;
class ArtisanFeed implements FeedGeneratorAlgorithm
{
public function __invoke(int $limit, ?string $cursor, ?string $user, Request $request): array
{
// 由于 API 的临时限制,这里需要认证
$response = Bluesky::login(identifier: config('bluesky.identifier'), password: config('bluesky.password'))
->searchPosts(q: '#laravel', until: $cursor, limit: $limit);
$cursor = data_get($response->collect('posts')->last(), 'indexedAt');
$feed = $response->collect('posts')->map(function (array $post) {
return ['post' => data_get($post, 'uri')];
})->toArray();
info('user: '.$user);
info('header', $request->header());
return compact('cursor', 'feed');
}
}
// AppServiceProvider::boot()
use Revolution\Bluesky\FeedGenerator\FeedGenerator;
use App\FeedGenerator\ArtisanFeed;
FeedGenerator::register(name: 'artisan', algo: ArtisanFeed::class);
官方 starter kit 中的认证功能默认启用。要禁用它,只需向 validateAuthUsing 传入一个直接返回用户 DID 的闭包。
// AppServiceProvider::boot()
use Illuminate\Http\Request;
use Revolution\Bluesky\Crypto\JsonWebToken;
use Revolution\Bluesky\FeedGenerator\FeedGenerator;
FeedGenerator::validateAuthUsing(function (?string $jwt, Request $request): ?string {
[, $payload] = JsonWebToken::explode($jwt);
return data_get($payload, 'iss');
});
feed 会受到账号的 “语言设置” 影响。如果 FeedGenerator 已经能获取到帖子,但 Bluesky 上依然显示不出 feed,请检查账号的语言设置。
进阶用法
用 Artisan 命令和定时任务将帖子保存到数据库,让算法只需从 DB 读取,就可以在不调用 API 的情况下实现更快的 feed。
// 在算法中从 DB 返回 feed 的示例
FeedGenerator::register(name: 'cached-feed', algo: function(int $limit, ?string $cursor): array {
$query = \App\Models\Post::query()
->orderByDesc('indexed_at')
->limit($limit);
if ($cursor) {
$query->where('indexed_at', '<', $cursor);
}
$posts = $query->get();
$cursor = $posts->last()?->indexed_at;
$feed = $posts->map(fn ($post) => ['post' => $post->uri])->toArray();
return compact('cursor', 'feed');
});
// 定时收集帖子的示例(routes/console.php)
use Illuminate\Support\Facades\Schedule;
Schedule::command('bluesky:collect-posts')->everyFiveMinutes();