> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# BlueskyManager 与 HasShortHand

> 介绍 Bluesky Facade 背后的 BlueskyManager 架构，以及 HasShortHand trait 提供的快捷方法。

## 概览

`Bluesky` Facade 是 `BlueskyManager` 的一层薄封装。`BlueskyManager` 负责管理多个 Agent，而 `HasShortHand` trait 集中提供了一批常用方法的快捷方式。

## 架构

```mermaid theme={null}
graph TD
    A["Bluesky Facade<br>(Revolution\\Bluesky\\Facades\\Bluesky)"] --> B["BlueskyManager<br>(Factory 接口)"]
    B --> C["HasShortHand trait<br>快捷方法集"]
    B --> D["Conditionable trait<br>when() / unless()"]
    B --> E["Macroable trait<br>macro() / mixin()"]
    B --> F{"Agent"}
    F --> G["LegacyAgent<br>(App Password)"]
    F --> H["OAuthAgent<br>(OAuth)"]
    G --> I["LegacySession"]
    H --> J["OAuthSession"]
```

`BlueskyServiceProvider` 会将 `Factory::class` 以作用域单例（scoped singleton，每次请求一个实例）的形式注册为 `BlueskyManager`。

```php theme={null}
// BlueskyServiceProvider::register()
$this->app->scoped(Factory::class, BlueskyManager::class);
```

Facade 的 `getFacadeAccessor()` 会解析这个绑定。

```php theme={null}
// Facades/Bluesky.php
protected static function getFacadeAccessor(): string
{
    return Factory::class;
}
```

## BlueskyManager 的核心方法

`BlueskyManager` 自身定义的核心方法如下。将它们与 `HasShortHand` trait 提供的方法区分开来，有助于理解内部结构。

### 认证

| 方法                                                                     | 说明                                            |
| ---------------------------------------------------------------------- | --------------------------------------------- |
| `login(string $identifier, string $password, ?string $service = null)` | 使用 App Password 认证并设置 `LegacyAgent`           |
| `withToken(?AbstractSession $token)`                                   | 传入 `OAuthSession` 或 `LegacySession` 来设置 Agent |
| `check(): bool`                                                        | 判断是否已认证（同时校验 token 是否有效）                      |
| `refreshSession()`                                                     | 刷新 token                                      |
| `logout()`                                                             | 清除 Agent                                      |

### Agent 操作

| 方法                         | 说明                  |
| -------------------------- | ------------------- |
| `agent(): ?Agent`          | 获取当前的 Agent         |
| `withAgent(?Agent $agent)` | 直接设置 Agent          |
| `assertDid(): string`      | 返回已认证的 DID。未认证时抛出异常 |

### HTTP 客户端

| 方法                                                                                               | 说明                     |
| ------------------------------------------------------------------------------------------------ | ---------------------- |
| `client(bool $auth = true): AtpClient`                                                           | 返回已认证（或匿名）的 XRPC 客户端   |
| `public(): BskyClient`                                                                           | 返回无需认证的公开端点客户端         |
| `send(BackedEnum\|string $api, string $method, bool $auth, ?array $params, ?callable $callback)` | 直接调用任意 AT Protocol API |

### 工具方法

| 方法                                | 说明                         |
| --------------------------------- | -------------------------- |
| `identity(): Identity`            | 获取 DID 解析等身份服务             |
| `pds(): PDS`                      | 获取 PDS 信息服务                |
| `entryway(?string $path): string` | 返回服务 URL（例如 `bsky.social`） |
| `publicEndpoint(): string`        | 返回公开端点 URL                 |

## HasShortHand trait

`HasShortHand` trait 把 AT Protocol 的底层 API 包装成了对 PHP 更友好的方法。只要在 `BlueskyManager` 中写上 `use HasShortHand;`，这些方法就可以直接通过 `Bluesky::` 调用。

```php theme={null}
// BlueskyManager.php
class BlueskyManager implements Factory
{
    use Conditionable;
    use HasShortHand;
    use Macroable;
    // ...
}
```

<Info>
  把 `HasShortHand` 拆成独立 trait，是为了便于测试和自定义。这样既能保持 `BlueskyManager` 代码简洁，又能提供丰富的 API 快捷方式。
</Info>

### 发帖 / feed

| 方法                                                              | 说明             |
| --------------------------------------------------------------- | -------------- |
| `post(Post\|string\|array $text)`                               | 创建新帖子          |
| `getPost(string $uri)`                                          | 通过 AT-URI 获取帖子 |
| `getPosts(array $uris)`                                         | 批量获取多个帖子       |
| `deletePost(string $uri)`                                       | 删除帖子           |
| `getTimeline(?string $algorithm, ?int $limit, ?string $cursor)` | 获取首页时间线        |
| `getAuthorFeed(?string $actor, ...)`                            | 获取指定账号的 feed   |
| `searchPosts(string $q, ...)`                                   | 搜索帖子           |

### 互动

| 方法                                   | 说明        |
| ------------------------------------ | --------- |
| `like(Like\|StrongRef $subject)`     | 点赞        |
| `deleteLike(string $uri)`            | 取消点赞      |
| `repost(Repost\|StrongRef $subject)` | 转发        |
| `deleteRepost(string $uri)`          | 取消转发      |
| `getActorLikes(?string $actor, ...)` | 获取账号的点赞列表 |

### 关注

| 方法                                  | 说明      |
| ----------------------------------- | ------- |
| `follow(Follow\|string $did)`       | 关注      |
| `deleteFollow(string $uri)`         | 取消关注    |
| `getFollowers(?string $actor, ...)` | 获取粉丝列表  |
| `getFollows(?string $actor, ...)`   | 获取关注中列表 |

### 个人资料 / 账号

| 方法                                  | 说明               |
| ----------------------------------- | ---------------- |
| `getProfile(?string $actor)`        | 获取个人资料           |
| `upsertProfile(callable $callback)` | 更新个人资料           |
| `resolveHandle(string $handle)`     | 将 handle 解析为 DID |

### 媒体

| 方法                                                         | 说明          |
| ---------------------------------------------------------- | ----------- |
| `uploadBlob(StreamInterface\|string $data, string $type)`  | 上传图片等 blob  |
| `uploadVideo(StreamInterface\|string $data, string $type)` | 上传视频        |
| `getJobStatus(string $jobId)`                              | 查看视频上传的任务状态 |
| `getUploadLimits()`                                        | 获取上传限制      |

### 通知

| 方法                                        | 说明      |
| ----------------------------------------- | ------- |
| `listNotifications(...)`                  | 获取通知列表  |
| `countUnreadNotifications(...)`           | 获取未读通知数 |
| `updateSeenNotifications(string $seenAt)` | 更新已读状态  |

### AT Protocol 记录操作

| 方法                                                                  | 说明           |
| ------------------------------------------------------------------- | ------------ |
| `createRecord(string $repo, string $collection, ...)`               | 创建 record    |
| `getRecord(string $repo, string $collection, string $rkey, ...)`    | 获取 record    |
| `listRecords(string $repo, string $collection, ...)`                | 获取 record 列表 |
| `putRecord(string $repo, string $collection, string $rkey, ...)`    | 更新或创建 record |
| `deleteRecord(string $repo, string $collection, string $rkey, ...)` | 删除 record    |

### Feed generator 与 labeler

| 方法                                                                     | 说明                |
| ---------------------------------------------------------------------- | ----------------- |
| `publishFeedGenerator(BackedEnum\|string $name, Generator $generator)` | 发布 feed generator |
| `createThreadGate(string $post, ?array $allow)`                        | 创建 thread gate    |
| `upsertLabelDefinitions(callable $callback)`                           | 更新 label 定义       |
| `deleteLabelDefinitions()`                                             | 删除 label 定义       |
| `createLabels(RepoRef\|StrongRef\|array $subject, array $labels)`      | 添加 label          |
| `deleteLabels(RepoRef\|StrongRef\|array $subject, array $labels)`      | 删除 label          |

## 常用快捷方式示例

### 发帖

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;

$response = Bluesky::login(
    identifier: config('bluesky.identifier'),
    password: config('bluesky.password'),
)->post('Hello Bluesky');
```

### 回复

回复时使用 `Post::build()` 设置父帖的 `StrongRef`。`HasShortHand` 中没有专门的 `reply()` 方法，而是把 `Post` 对象传给 `post()`。

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Post;
use Revolution\Bluesky\Types\StrongRef;

$parent = StrongRef::to(uri: 'at://did:plc:.../app.bsky.feed.post/...', cid: 'bafyrei...');

$reply = Post::create('回复内容')
    ->reply(root: $parent, parent: $parent);

Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->post($reply);
```

### 点赞与转发

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Types\StrongRef;

$ref = StrongRef::to(uri: 'at://did:plc:.../app.bsky.feed.post/...', cid: 'bafyrei...');

// 点赞
Bluesky::withToken($session)->like($ref);

// 转发
Bluesky::withToken($session)->repost($ref);
```

### 更新个人资料

`upsertProfile()` 会获取当前的个人资料，然后保存在闭包中修改后的内容。你可以调用 `Profile` 对象的方法来设置 displayName、description 等。

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Profile;

Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->upsertProfile(function (Profile $profile) {
        $profile->displayName('新的显示名')
                ->description('个人简介');
    });
```

#### 设置 self label

通过 `SelfLabels`，可以对整个账号设置 self label。

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Record\Profile;
use Revolution\Bluesky\Types\SelfLabels;

Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->upsertProfile(function (Profile $profile) {
        $profile->labels(SelfLabels::make(['!no-unauthenticated']));
    });
```

### 直接调用任意 API

`HasShortHand` 中没有的方法，可以通过 `send()` 或 `client()` 调用。

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;

// 用 send() 调用任意 XRPC 方法
$response = Bluesky::withToken($session)
    ->send(
        api: 'app.bsky.actor.getProfiles',
        method: 'get',
        params: ['actors' => ['did:plc:...', 'did:plc:...']],
    );

// 通过 client() 进行更细粒度的操作
$response = Bluesky::withToken($session)
    ->client()
    ->bsky()
    ->getProfiles(actors: ['did:plc:...']);
```

## 通过 Facade 和直接使用的区别

`Bluesky::post()` 与 `app(Factory::class)->post()` 操作的是同一个 `BlueskyManager` 实例。

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Contracts\Factory;

// 通过 Facade（常规用法）
Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->post('Hello');

// 从容器直接获取
$manager = app(Factory::class);
$manager->login(config('bluesky.identifier'), config('bluesky.password'))
        ->post('Hello');

// 通过类型提示进行 DI
class MyService
{
    public function __construct(private Factory $bluesky) {}

    public function doPost(): void
    {
        $this->bluesky->login(
            config('bluesky.identifier'),
            config('bluesky.password'),
        )->post('Hello from DI');
    }
}
```

由于使用了 `scoped` 注册，同一次请求内 `login()` 后的 session 状态会被保留。

## Conditionable 与 Macroable

`BlueskyManager` 也使用了 `Conditionable` 和 `Macroable` trait。

### Conditionable: when() / unless()

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;

Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->when(config('app.env') === 'production', function ($bluesky) {
        $bluesky->post('来自生产环境的帖子');
    });
```

### Macroable：添加自定义方法

通过 `macro()`，你可以从应用侧向 `BlueskyManager` 添加方法。通常写在 `AppServiceProvider` 的 `boot()` 中。

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;

Bluesky::macro('postWithHashtag', function (string $text, string $tag) {
    /** @var \Revolution\Bluesky\BlueskyManager $this */
    return $this->post("{$text} #{$tag}");
});

// 使用示例
Bluesky::login(config('bluesky.identifier'), config('bluesky.password'))
    ->postWithHashtag('Hello', 'laravel');
```

## 注入自定义 Agent

通过 `withAgent()`，可以直接设置任意的 `Agent` 实现。

```php theme={null}
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Contracts\Agent;

// 自定义 Agent 时需要实现 Agent 接口
class MyCustomAgent implements Agent
{
    // ...
}

Bluesky::withAgent(new MyCustomAgent());
```

<Info>
  通过常规认证（`login()` / `withToken()`）会自动设置 agent，所以直接使用 `withAgent()` 的场合主要出现在测试中。
</Info>

## 参考链接

* [认证方式对比](/zh/packages/laravel-bluesky/authentication) —— App Password 与 OAuth 的详细对比
* [Basic client](/zh/packages/laravel-bluesky/basic-client) —— 认证后的 API 操作示例
* [测试](/zh/packages/laravel-bluesky/testing) —— 使用 Fake 的测试方法
* Source: [src/BlueskyManager.php](https://github.com/invokable/laravel-bluesky/blob/main/src/BlueskyManager.php)
* Source: [src/HasShortHand.php](https://github.com/invokable/laravel-bluesky/blob/main/src/HasShortHand.php)


## Related topics

- [Bot 教程 - Laravel Bluesky](/zh/packages/laravel-bluesky/bot-tutorial.md)
- [认证方式对比 - Laravel Bluesky](/zh/packages/laravel-bluesky/authentication.md)
- [Illuminate\Support\Manager — 驱动系统剖析](/zh/advanced/manager.md)
- [实现自定义认证 Guard](/zh/advanced/custom-auth-guard.md)
- [Socialite - Laravel Bluesky](/zh/packages/laravel-bluesky/socialite.md)
