Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions changelog/unreleased/donations-page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
### English

- Add a public `/donations` page where anyone can contribute a WorkBuddy, Qoder, Trae, Devin, or Command Code account and receive New API credit for it. Contributions use the provider's own browser authorization, so a contributor never hands over a raw credential.
- **Web authorization**: `POST /api/donations` creates the account, opens the provider login, and returns the URL to visit. `GET /api/donations/sessions/{id}` polls it and issues the reward once the login completes; `DELETE` abandons the round. The reward is never credited before the account is authorized.
- The contributed account is created disabled and is enabled only after authorization succeeds, so an unfinished round cannot carry traffic.
- If the credit call fails after a successful authorization, the account stays in the pool and the response reports `credited:false` with `credit_error`, so an operator can credit it without asking for a re-login.
- Providers without a browser login keep the pasted-credential path at `POST /api/donations/credential`.
- Configure the donation site under System settings with `donation_base_url` and `donation_token`. The token is stored as a secret and is never returned by the settings API.

### 中文

- 新增公开的 `/donations` 贡献页面:任何人都可以贡献 WorkBuddy、Qoder、Trae、Devin 或 Command Code 账号并获得 New API 额度。贡献走各平台自己的网页授权,贡献者不需要交出原始凭据。
- **网页授权**:`POST /api/donations` 创建账号并返回需要打开的登录地址;`GET /api/donations/sessions/{id}` 轮询进度,登录完成后发放额度;`DELETE` 放弃本次贡献。账号未授权前不会发放额度。
- 贡献的账号先以禁用状态创建,授权成功后才启用,因此未完成的流程不会承载流量。
- 若授权成功但发放额度失败,账号保留在账号池中,响应返回 `credited:false` 与 `credit_error`,管理员无需让用户重新登录即可补发。
- 不支持网页授权的平台保留 `POST /api/donations/credential` 的粘贴凭据方式。
- 在「系统设置」里用 `donation_base_url` 和 `donation_token` 配置贡献站点。令牌以密钥形式保存,系统设置接口不会返回它。
4 changes: 4 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { LoginPage } from '@/pages/LoginPage'
import { SystemPage } from '@/pages/SystemPage'
import { LogsPage } from '@/pages/LogsPage'
import { KeysPage } from '@/pages/KeysPage'
import { DonationsPage } from '@/pages/DonationsPage'

export default function App() {
return (
Expand All @@ -23,6 +24,9 @@ export default function App() {
<BrowserRouter>
<Routes>
<Route path="/login" element={<LoginPage />} />
{/* Donations are public: a contributor has no console key, and
the reward is credited to their own New API user id. */}
<Route path="/donations" element={<DonationsPage />} />
<Route element={<RequireAuth />}>
<Route element={<AppLayout />}>
<Route path="/" element={<OverviewPage />} />
Expand Down
108 changes: 108 additions & 0 deletions frontend/src/api/donations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { api } from './client'

// A contributable credential format advertised by the backend. The page renders
// these instead of hardcoding which providers accept donations, so adding a
// provider on the Go side is enough.
export type DonationFormat = {
format: string
provider: string
label: string
region: string
// "json" for a credential object, "qoder_native" for the base64 auth blob
// plus machine id pair the Qoder CLI writes to its own home.
credential_kind: string
// web_auth is true when the account can be authorized through the provider's
// own browser login, which is the preferred flow.
web_auth: boolean
description: string
}

export type DonationInfo = {
object: string
default_usd: number
quota_per_usd: number
formats: DonationFormat[]
}

// DonationSession is one in-flight web authorization. The reward is credited
// only once status becomes "credited".
export type DonationSession = {
session_id: string
account_id: string
provider: string
region: string
name: string
format: string
newapi_user_id: number
credit_usd: number
auth_url: string
status: 'pending' | 'credited' | 'failed'
message?: string
credited: boolean
credited_quota?: number
credit_error?: string
}

export type DonationStart = {
format: string
name?: string
region?: string
newapi_user_id: number
credit_usd?: number
}

export type DonationResult = {
account_id: string
account_name: string
provider: string
region: string
status: string
credited_usd: number
credited_quota: number
credited: boolean
// Set when the account was accepted but the credit call failed. The
// contribution still counts; an operator credits it manually.
credit_error?: string
}

export type DonationSubmit = {
format: string
name?: string
region?: string
newapi_user_id: number
credit_usd?: number
credential?: unknown
user_blob?: string
machine_id?: string
}

// /api/donations is intentionally public, so these calls never send a console
// key and must not be wrapped in RequireAuth.
export function fetchDonationInfo() {
return api<DonationInfo>('/api/donations')
}

// startDonation begins a web-authorization round and returns the URL to open.
export function startDonation(input: DonationStart) {
return api<DonationSession>('/api/donations', {
method: 'POST',
body: JSON.stringify(input),
})
}

export function pollDonationSession(sessionId: string) {
return api<DonationSession>(`/api/donations/sessions/${encodeURIComponent(sessionId)}`)
}

export function cancelDonationSession(sessionId: string) {
return api<void>(`/api/donations/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' })
}

// submitDonation is the legacy pasted-credential path, kept for providers that
// do not expose a browser login.
export function submitDonation(input: DonationSubmit) {
return api<DonationResult>('/api/donations/credential', {
method: 'POST',
body: JSON.stringify(input),
})
}
74 changes: 74 additions & 0 deletions frontend/src/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,43 @@ export const messages: Record<Lang, Dict> = {
consoleKeyRotateNow: 'Rotate now',
consoleKeySecretTitle: 'New console key',
consoleKeySecretHint: 'This browser session is already updated. Copy the key for any other clients that used the old console secret.',
'donations.title': 'Contribute an account',
'donations.noLogin': 'No sign-in required',
'donations.subtitle': 'Contribute a WorkBuddy, Qoder, or Trae account and receive New API credit on the site. The account joins the shared pool once it is accepted.',
'donations.rewardTitle': 'Reward',
'donations.rewardBody': 'Each accepted account earns {usd} USD, credited as {quota} New API quota units.',
'donations.fieldType': 'Account type',
'donations.fieldUserID': 'New API user ID',
'donations.fieldUserIDHint': 'The numeric user ID on the New API site, not the username. It is shown in the site URL or personal settings.',
'donations.fieldName': 'Account label',
'donations.fieldNameHint': 'Optional. A name for this account in the console.',
'donations.fieldCredential': 'Credential',
'donations.fieldCredentialHint': 'Paste the credential JSON exported from this console, or the provider login bundle.',
'donations.fieldUserBlob': 'Auth blob',
'donations.fieldUserBlobHint': 'The base64 contents of the Qoder CLI auth file.',
'donations.fieldMachineID': 'Machine ID',
'donations.submit': 'Contribute',
'donations.submitting': 'Contributing...',
'donations.authorize': 'Authorize in browser',
'donations.starting': 'Starting authorization...',
'donations.waitingAuth': 'Waiting for you to finish signing in...',
'donations.waitingHint': 'A browser tab was opened for the provider sign-in. This page finishes by itself once you authorize; do not close it.',
'donations.reopenAuth': 'Reopen the authorization page',
'donations.cancel': 'Cancel',
'donations.authTimeout': 'Authorization timed out. Start again.',
'donations.success': 'Thank you. The account was accepted and {usd} USD was credited.',
'donations.credited': 'Contribution accepted',
'donations.failed': 'Contribution failed',
'donations.creditFailed': 'The account was accepted but the credit did not go through: {error}. Contact an operator to have it credited.',
'donations.acceptedNoCredit': 'Account accepted, credit pending',
'donations.infoFailed': 'Could not load contribution options',
'donations.userIDInvalid': 'Enter a valid numeric New API user ID.',
'donations.formatRequired': 'Choose an account type.',
'donations.credentialRequired': 'Paste a credential.',
'donations.credentialInvalid': 'The credential is not valid JSON.',
'donations.qoderFieldsRequired': 'Both the auth blob and the machine ID are required.',
'donations.notesTitle': 'Before you contribute',
'donations.notesBody': 'A contributed account is added to the shared pool and used for other members requests. Only contribute accounts you are willing to share. Invalid or unusable credentials are rejected before any credit is issued.',
},
zh: {
brandSub: '登录态网关',
Expand Down Expand Up @@ -1310,6 +1347,43 @@ export const messages: Record<Lang, Dict> = {
consoleKeyRotateNow: '立即轮换',
consoleKeySecretTitle: '新的控制台密钥',
consoleKeySecretHint: '当前浏览器会话已更新。如果还有客户端在用旧的控制台密钥,请把新值复制过去。',
'donations.title': '贡献账号',
'donations.noLogin': '无需登录',
'donations.subtitle': '贡献一个 WorkBuddy、Qoder 或 Trae 账号,即可获得站点的 New API 额度。账号通过校验后会计入共享账号池。',
'donations.rewardTitle': '贡献奖励',
'donations.rewardBody': '每个通过的账号奖励 {usd} 美元,折算为 {quota} New API 额度单位。',
'donations.fieldType': '账号类型',
'donations.fieldUserID': 'New API 用户 ID',
'donations.fieldUserIDHint': '站点上的数字用户 ID,不是用户名。在站点地址或个人设置里可以看到。',
'donations.fieldName': '账号备注',
'donations.fieldNameHint': '可选。这个账号在控制台里显示的名称。',
'donations.fieldCredential': '凭据',
'donations.fieldCredentialHint': '粘贴从本控制台导出的凭据 JSON,或对应平台的登录信息。',
'donations.fieldUserBlob': '认证数据',
'donations.fieldUserBlobHint': 'Qoder CLI 认证文件里的 base64 内容。',
'donations.fieldMachineID': '机器码',
'donations.submit': '提交贡献',
'donations.submitting': '提交中…',
'donations.authorize': '在浏览器中授权',
'donations.starting': '正在发起授权…',
'donations.waitingAuth': '等待你在浏览器里完成登录…',
'donations.waitingHint': '已打开授权页面。完成授权后本页会自动结束,请不要关闭。',
'donations.reopenAuth': '重新打开授权页面',
'donations.cancel': '取消',
'donations.authTimeout': '授权超时,请重新发起。',
'donations.success': '感谢贡献。账号已通过校验,已发放 {usd} 美元额度。',
'donations.credited': '贡献成功',
'donations.failed': '贡献失败',
'donations.creditFailed': '账号已通过校验,但额度发放未成功:{error}。请联系管理员手动发放。',
'donations.acceptedNoCredit': '账号已收录,额度待发放',
'donations.infoFailed': '无法加载可贡献的类型',
'donations.userIDInvalid': '请填写有效的 New API 数字用户 ID。',
'donations.formatRequired': '请选择账号类型。',
'donations.credentialRequired': '请填写凭据。',
'donations.credentialInvalid': '凭据不是合法的 JSON。',
'donations.qoderFieldsRequired': '认证数据和机器码都需要填写。',
'donations.notesTitle': '贡献前请确认',
'donations.notesBody': '贡献的账号会加入共享账号池,用于其他成员的请求。请只贡献你愿意共享的账号。无法使用的凭据会在发放额度之前被拒绝。',
},
}

Expand Down
Loading