Skip to content

feat: publish approved social_media content to X via NyxID #216

@eanzhao

Description

@eanzhao

Status: ✅ READY TO START (2026-04-27)

NyxID 侧基础设施 verified,aevatar 侧可以直接开 PR;mainnet smoke test 等 ops 配完 Twitter app credentials 再做。

Prerequisite Status
NyxID twitter provider seeded (provider_service.rs:405-450, OAuth 2.0 + PKCE, scopes 含 tweet.write)
NyxID api-twitter service seeded (provider_service.rs:1725-1735, base https://api.x.com/2, bearer 自动注入)
NyxID proxy 透明路由 (proxy.rs:483-496, ANY /api/v1/proxy/s/{slug}/{*path})
aevatar NyxIdApiClient.ProxyRequestAsync wrapper(已被 daily_report 用)
aevatar OutboundConfig + 永久 NyxID API key 机制(#185 已 ship)
aevatar PreflightGitHubProxyAsync 模式可直接 fork 出 PreflightTwitterProxyAsync (#418)
mainnet NyxID 部署给 twitter provider 配 Twitter app client_id/secret ⚠️ ops 动作 — 不阻塞 aevatar 代码 ship

client_id_encrypted / client_secret_encrypted 在 NyxID provider 记录里默认是 None — 部署侧需要去 https://developer.x.com 注册一个 Twitter app(OAuth 2.0 + PKCE,redirect_uri 指 NyxID 的 callback),通过 NyxID 管理面 / admin API(backend/src/handlers/providers.rs)写入。这是部署 ops 动作;aevatar PR 可以先 merge,等 ops 配完再 mainnet smoke test


Goal

Wire social_media template's post-approval publish step:approve 后 aevatar 调 NyxID api-twitter proxy 发推,把结果反馈到 Feishu。跟 daily_reportapi-github 同模式,不引入 direct X SDK。

Background

PR #193 已 ship social_media 模板的:

  • scheduled workflow run
  • Feishu approval card delivery
  • approve/reject resolution back into Aevatar
  • approval/rejection feedback back to Feishu

但 approve 之后没有实际 publish 到 X。这个 issue 补上最后一段。

Implementation steps

Step 1 · PreflightTwitterProxyAsync (~半天)

Mirror AgentBuilderTool.PreflightGitHubProxyAsync (added in #418 / #421):

// 在 social_media agent 创建时调用
GET /api/v1/proxy/s/api-twitter/users/me  with the freshly minted agent api-key

返回分类:

  • 200 → user 已连 Twitter,agent 可创建
  • 401 → user 未连 Twitter / OAuth token 过期 → 返结构化错误 twitter_oauth_required,hint 用户去 NyxID 重新授权
  • 403 → 不该出现(默认 scope 含 tweet.write),但写防御性提示 twitter_proxy_access_denied
  • 4xx 其他 → 透传 + best-effort revoke 新 minted api-key(防 orphan,跟 fix(agent-builder): use UserService.id for api-key allowed_service_ids (#417) #418 同)

落点:agents/Aevatar.GAgents.ChannelRuntime/AgentBuilderTool.cs(或在 PR #451 拆完后落到 Aevatar.GAgents.Authoring)。

Step 2 · Publish action in workflow approval-resolution path (~半天)

WorkflowAgentGAgentsocial_media approval 通过分支里加:

var body = JsonSerializer.Serialize(new { text = approvedContent });
var result = await nyxIdApiClient.ProxyRequestAsync(
    apiKey: outboundConfig.NyxApiKey,
    serviceSlug: \"api-twitter\",
    path: \"tweets\",
    method: \"POST\",
    body: body,
    ct);

Twitter v2 成功响应 (HTTP 201):

{ \"data\": { \"id\": \"1234567890\", \"text\": \"...\" } }

成功 → 提取 data.id,可选 users/me 拿 handle,反馈 已发布: https://x.com/{handle}/status/{id} 到 Feishu。

Step 3 · 错误分类 + Feishu surfacing (~半天)

类似 SkillRunnerGAgent.SendOutputAsync 的失败通知模式:

HTTP Lark 反馈文案 是否重试
201 已发布: https://x.com/{handle}/status/{id}
401 Twitter OAuth 过期,去 https://nyxid.../providers/twitter 重新授权 不重试
403 Twitter scope 不足(tweet.write 缺失)— 不应发生,请联系 ops 不重试
429 Twitter rate limit,{retry_after}s 后自动重试 1 次
5xx Twitter 服务异常,已重试 N 次仍失败 最多 2 次 backoff

承袭 #412 的"actionable last_error"原则:每个错误码自带恢复路径文字。

Step 4 · 测试 (~半天)

Mock NyxID proxy api-twitter 返回 success / 401 / 403 / 429 / 5xx,验证:

  • happy path:approval → publish → feishu surfacing 含 tweet URL
  • 401:feishu 显示 OAuth 重连提示,approval 状态机不卡住
  • 429:retry 1 次后失败显示 rate limit 文案
  • approval resolution 的现有路径不受影响(PR #193 行为保留)

落点:test/Aevatar.GAgents.ChannelRuntime.Tests/ (拆包后落到 test/Aevatar.GAgents.Authoring.Tests/)。

NyxID OAuth 用户流程(前端体验,不在本 issue scope 但要知道)

用户首次创建 social_media agent 时:

  1. aevatar PreflightTwitterProxyAsync → 401 → 提示 user 去 NyxID 连 Twitter
  2. user 在 NyxID 走 OAuth: https://x.com/i/oauth2/authorize?...&scope=tweet.read+tweet.write+users.read+offline.access&...
  3. 授权回来,NyxID 存 access_token + refresh_token(per-user,credential_mode: user)
  4. user 回 aevatar 重试创建,pre-flight 200 → agent 创建成功
  5. 之后每次 publish,NyxID 自动用 user 的 token 调 Twitter;token 过期时 refresh 也由 NyxID 处理

aevatar 端完全不持有 user 的 Twitter token — 全程通过 api-key proxy。符合 NyxID #505 capability-broker 定位(aevatar #375 RFC:线上零 secret material)。

Acceptance

  • PreflightTwitterProxyAsync 在 social_media agent 创建时检查 Twitter OAuth;401 透传 twitter_oauth_required
  • Approved content 通过 POST /api/v1/proxy/s/api-twitter/tweets body {\"text\":...} 发布
  • Publish 成功反馈含 tweet URL(https://x.com/{handle}/status/{id});handle 从 users/me
  • Publish 失败按 401 / 403 / 429 / 5xx 分类反馈,不静默失败
  • Approval-resolution 现有路径(PR Implement Day One private-chat agent builder, approval loop, and template runners #193 ship 的部分)不受影响
  • Tests:mock NyxID proxy 5 类响应,验证 aevatar 行为
  • Mainnet smoke test 等 ops 配完 Twitter app credentials 之后(不阻塞 PR merge)

Non-goals

  • 不引入 direct Twitter / X SDK
  • 不扩展到 group-chat 创建流
  • 不改 daily_report 模板
  • 不在本 issue 配 Twitter app credentials(ops 动作,单独 track)
  • 不做 multi-tweet thread / 媒体附件 / poll(v1 只发 plain text;后续 issue 扩展)

Estimated effort

~1.5-2 天 working time(不含等 ops 配 credentials):

  • Step 1 PreflightTwitterProxyAsync: 0.5 天
  • Step 2 publish action: 0.5 天
  • Step 3 error 分类 + Feishu surfacing: 0.5 天
  • Step 4 测试: 0.5 天

Related

Metadata

Metadata

Assignees

Labels

todoIssue ready for Symphony to pick up

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions