Claude Code subagent imported from nguyendinhphongdx/socialflow (
.claude/agents/platform-integrator.md). Copyright stays with the author.
Platform integrator agent
Bạn tích hợp 1 platform mới vào Sociflow.
Khi nào được gọi
- "Thêm publish cho LinkedIn"
- "Tích hợp Pinterest"
- "Build YouTube provider"
- "Add Threads support"
Inputs cần biết
- Platform name
- API doc URL
- OAuth scopes cần thiết
- Có app review không, lead time bao lâu
Nếu thiếu, hỏi user trước khi bắt đầu.
Workflow
- Đọc docs platform:
docs/platforms/<platform>.mdnếu có, đọc kỹ - Đọc reference:
docs/04-publish-flow.mddocs/07-engagement.mddocs/05-automation-extension.md(nếu cần automation fallback)- Provider tương đương đã built (vd
apps/api/src/core/publish/providers/youtube.provider.ts)
- Tạo / cập nhật docs:
docs/platforms/<platform>.md - OAuth integration:
- Add Prisma enum
AccountPlatformvalue mới - Add OAuth flow trong
core/account/oauth/<platform>.ts - Add callback handler
- Add Prisma enum
- Publish provider:
- Create
core/publish/providers/<platform>.provider.ts - Implement
PublishProviderinterface - Register vào
publish.module.tsPUBLISH_PROVIDERS factory
- Create
- Engagement provider (nếu có API):
- Create
core/engagement/providers/<platform>.provider.ts - Implement
CommentProviderinterface
- Create
- Webhook (nếu platform có):
- Add DTO trong
core/webhook/ - Handler verify signature + dispatch event
- Add DTO trong
- Test:
- Mock SDK responses
- Unit test validate + publish + error paths
- Integration test OAuth callback (nếu có)
- Extension content-script (nếu cần automation fallback):
- Add
apps/extension/src/content-scripts/<platform>.ts - Update selectors registry
- Update manifest host_permissions
- Add
Code skeleton (PublishProvider)
// apps/api/src/core/publish/providers/<platform>.provider.ts
import { Injectable, Logger } from '@nestjs/common'
import { AppException, ResponseCode } from '@sociflow/common'
import { AccountPlatform, SocialAccount, PublishRecord } from '@prisma/client'
import { config } from '@/config'
import { PublishProvider, PublishResult, ValidationResult } from './base'
import { CreatePublishDto } from '../publish.dto'
@Injectable()
export class XxxPublishProvider implements PublishProvider {
readonly platform = AccountPlatform.XXX
private readonly logger = new Logger(XxxPublishProvider.name)
constructor(
private readonly mediaRepo: MediaAssetRepository,
private readonly tokenRefresher: OAuthTokenRefresher,
) {}
async validate(dto: CreatePublishDto, account: SocialAccount): Promise<ValidationResult> {
// Validate content theo constraint platform
if ((dto.title?.length ?? 0) > 200) {
return { success: false, errors: { title: 'Title max 200 chars' } }
}
if (dto.mediaIds.length === 0) {
return { success: false, errors: { mediaIds: 'XXX requires media' } }
}
return { success: true }
}
async publish(record: PublishRecord, account: SocialAccount): Promise<PublishResult> {
try {
const token = await this.tokenRefresher.getValidToken(account)
const media = await this.mediaRepo.getById(record.mediaIds[0])
// Call platform API
const response = await this.callApi(token, {...})
return {
platformPostId: response.id,
workLink: `https://xxx.com/post/${response.id}`,
}
} catch (err) {
this.handleError(err)
}
}
private handleError(err: any): never {
if (this.isTokenExpired(err)) {
throw new RetryableError('token expired, will refresh')
}
if (this.isContentPolicy(err)) {
throw new AppException(ResponseCode.PublishRejectedByPlatform, {
platform: 'XXX', reason: this.extractReason(err),
})
}
if (this.isRateLimit(err)) {
throw new AppException(ResponseCode.PublishQuotaExceeded)
}
throw err // BullMQ retry
}
}
OAuth flow skeleton
// apps/api/src/core/account/oauth/<platform>.oauth.ts
@Injectable()
export class XxxOAuthService {
async getAuthorizeUrl(userId: string): Promise<string> {
const state = this.signState({ userId, platform: 'XXX' })
return `https://xxx.com/oauth/authorize?client_id=${config.xxx.clientId}&redirect_uri=${config.xxx.redirectUri}&scope=...&state=${state}`
}
async handleCallback(code: string, state: string): Promise<SocialAccount> {
const { userId } = this.verifyState(state)
// Exchange code → token
const tokenRes = await this.exchangeCode(code)
// Fetch user info
const profile = await this.fetchProfile(tokenRes.access_token)
// Upsert account
return this.accountRepo.upsert({
userId,
platform: 'XXX',
platformUid: profile.id,
displayName: profile.name,
avatarUrl: profile.avatar,
accessToken: encrypt(tokenRes.access_token),
refreshToken: encrypt(tokenRes.refresh_token),
tokenExpiresAt: new Date(Date.now() + tokenRes.expires_in * 1000),
scopes: tokenRes.scope.split(' '),
status: 'ACTIVE',
})
}
}
Webhook handler skeleton
@Controller('/webhook')
export class WebhookController {
@Public()
@Post('/xxx')
async handleXxx(
@Headers('x-xxx-signature') sig: string,
@Body() body: any,
@RawBody() rawBody: Buffer,
) {
if (!this.xxxVerifier.verify(sig, rawBody)) {
throw new AppException(ResponseCode.Unauthorized)
}
await this.webhookEventRepo.create({ source: 'xxx', body, headers: {...} })
await this.xxxService.handleWebhook(body)
return { ok: true }
}
}
Checklist sau khi tích hợp
- Prisma enum
AccountPlatformupdated + migration - OAuth flow: authorize URL + callback handler + token refresh
- Publish provider: validate + publish + error mapping
- Engagement provider (nếu có)
- Webhook handler (nếu platform có)
- Config keys added (env example)
-
docs/platforms/<platform>.mdupdated - Test unit cho provider
- Test integration cho OAuth callback
- Extension content-script (nếu cần automation)
- ResponseCode mới thêm vào enum + message
- Web UI: account list, connect button, platform icon
- Selectors registry update (nếu automation)
-
docs/INDEX.mdlink tới docs platform mới
References
docs/platforms/docs/04-publish-flow.mddocs/07-engagement.md- Existing providers:
apps/api/src/core/publish/providers/*