- 数据库:添加 has_purchased_starter_pack 字段到 register_bonus_claims - 后端:创建静态配置管理套餐信息,支持按国家/地区区分 - 后端:新增 GET /api/v1/points/packages API 返回可用套餐 - 后端:创建 utils/paths.py 统一路径管理 - 前端:动态获取套餐信息,移除硬编码 - 前端:添加 ProductCode 枚举约束,前后端类型安全 - 配置:Profile 默认国家改为 US(ISO 3166-1 alpha-2) - 文档:更新协议文档说明新 API 和字段
11 KiB
User Points & Chat Data Protocol
This protocol defines the canonical data contract for user profile, points account, points ledger, chat session, and chat messages.
Protocol verification status:
- Last audited migration:
backend/alembic/versions/20260413_0004_register_bonus_claims_snapshot.py - Last audited models:
backend/src/models/profile.py,backend/src/models/user_points.py,backend/src/models/points_ledger.py,backend/src/models/points_audit_ledger.py,backend/src/models/register_bonus_claims.py,backend/src/models/agent_chat_session.py,backend/src/models/agent_chat_message.py - Current status: aligned with register bonus moved to application service
Scope
profilesuser_pointspoints_ledgerpoints_audit_ledgerregister_bonus_claimssessionsmessages
Compatibility strategy
- Current strategy: additive evolution.
- Breaking changes (drop/rename/type change on core fields) require explicit migration + rollback notes.
points_ledger.metadata.schema_versionis mandatory and current value is1.
Runtime charging policy (chat)
- Charge unit:
20points per successful run. - Charge timing: deduct after worker run succeeds (
RUN_FINISHEDpath). - Failure behavior: failed/canceled runs do not deduct points.
- Precheck: before accepting a run, backend must verify
available = balance - frozen_balance >= 20. - Session follow-up cap: one session allows at most 2 user runs total (initial divination + 1 follow-up).
- Billing idempotency key for per-run consume:
chat.run.success:{sha1(session_id:run_id)}. - Failed/canceled runs do not deduct user points. If real provider cost is observed, audit record is written with
billed_to='platform'.
Table contract
profiles
- PK:
id(auth.users.id,on delete cascade) - Core fields:
username,avatar_url,bio,settings,referred_by,created_at,updated_at,deleted_at - Constraints:
usernamenot empty
- Indexes:
ix_profiles_usernameix_profiles_settings_gin
- Notes:
referred_byis FK toprofiles.id(on delete set null) for invite/referral trackingsettingsstoresProfileSettingsV1JSON includingpreferences,privacy,notification,divination_tutorial
user_points
- PK:
user_id(auth.users.id,on delete cascade) - Core fields:
balance,frozen_balance,lifetime_earned,lifetime_spent,version,created_at,updated_at - Constraints:
- all numeric totals must be non-negative
frozen_balance <= balance
points_ledger
- PK:
id - FK:
user_id -> auth.users.id(on delete cascade)biz_id -> sessions.id(on delete restrict, nullable)operator_id -> auth.users.id(on delete set null)
- Core fields:
direction,amount,balance_after,change_type,biz_type,biz_id,event_id,operator_id,metadata,created_at,updated_at - Constraints:
amount > 0direction in (1, -1)balance_after >= 0change_type in ('register', 'consume', 'grant', 'adjust')biz_type is null or biz_type='chat'- biz binding:
register => biz_type is null and biz_id is nullconsume/grant/adjust => biz_type='chat' and biz_id not null
- direction and change_type coupling:
register/grant => direction = 1consume => direction = -1adjust => direction in (1, -1)
- idempotency:
unique (user_id, event_id)
points_audit_ledger
- PK:
id - No FK to
auth.usersforuser_id_snapshotto avoid cascade delete and preserve audit retention - Core fields:
event_id,user_id_snapshot,user_email_snapshot,change_type,biz_type,biz_id,direction,amount,balance_after,billed_to,run_id,request_id,input_tokens,output_tokens,cost,metadata,created_at,updated_at - Constraints:
amount >= 0direction in (1, 0, -1)balance_after >= 0change_type in ('register', 'consume', 'grant', 'adjust')biz_type is null or biz_type='chat'billed_to in ('user', 'platform')- metadata must be object
- idempotency:
unique (event_id)
register_bonus_claims
- PK:
id - Core fields:
email_hash,user_email_snapshot,first_user_id_snapshot,balance_snapshot,grant_event_id,has_purchased_starter_pack,created_at,updated_at - Constraints:
email_hashuniquegrant_event_idunique
- Notes:
email_hashmust be HMAC-SHA256 over normalized email (trim + lower)- key source: backend config
points_policy.register_bonus_hmac_key balance_snapshotstores the latest pre-delete account balance for same-email re-registration recoveryhas_purchased_starter_packtracks whether user has purchased the starter pack ($0.99/60 credits)
points_ledger.metadata (schema_version=1)
Canonical shape:
{
"schema_version": 1,
"operator_type": "user|system|admin",
"run_id": "string",
"request_id": "string|null",
"charge": {
"message_id": "uuid",
"message_seq": 1,
"model_code": "string",
"input_tokens": 0,
"output_tokens": 0,
"cost": "0.000000"
},
"ext": {}
}
JSON constraints:
- Common:
- must be object
schema_version = 1operator_type in (user, system, admin)run_idnon-empty- if present,
extmust be object
- Per
change_type:register: nocharge, and no chat binding (biz_type/biz_idboth null)consume: requireschargeobject with required fieldsgrant: no extra metadata shape requirementadjust: requiresext.ticket_idnon-empty
Signup initialization contract
- DB trigger (
auth.usersafter insert):- Function:
public.initialize_profile_and_invite_code_on_signup() - Side effects: profile init + invite code init
- Function:
- Application service (in
POST /auth/email-session):grant_register_bonus_if_eligible()restoresbalance_snapshotfirst when present; otherwise grants register bonus viaregister_bonus_claims- Bonus amount from
config.points_policy.register_bonus_points
sessions
- PK:
id - FK:
user_id -> auth.users.id - Core fields:
session_type,job_id,title,status,last_activity_at,message_count,total_tokens,total_cost,state_snapshot,created_at,updated_at,deleted_at - Constraints:
session_type in ('chat', 'automation')status in ('pending', 'running', 'completed', 'failed')message_count/total_tokens/total_costnon-negative
messages
- PK:
id - FK:
session_id -> sessions.id(on delete cascade) - Core fields:
seq,role,content,model_code,tool_name,input_tokens,output_tokens,cost,latency_ms,visibility_mask,metadata,created_at,updated_at,deleted_at - Constraints:
unique (session_id, seq)seq > 0role in ('user', 'assistant', 'system', 'tool')- token/cost non-negative
latency_msnull or non-negative
Security and ownership
- Backend service must derive owner identity from verified auth context.
- Client must not be trusted for
user_id/operator_idownership semantics. metadataandsettingsmust not include secrets.
Application initialization contract
Application initialization is split into two separate concerns: deterministic seed data and dynamic notification template sync. These are intentionally decoupled to avoid non-deterministic side effects during bootstrap.
Seed data (deterministic)
Managed by python -m core.runtime.cli init-data (called by dev-migrate.sh bootstrap):
initialize_llm_catalog()— seedsllm_factoryandllmstables with known model definitionsinitialize_system_agents()— seedssystem_agentstable
These are idempotent and version-locked to code; they do not depend on runtime state or user data.
Notification templates (declarative sync)
Managed by python -m core.runtime.cli sync-notifications [flags]:
- Reads notification definitions from YAML files under
backend/src/core/config/static/notification/notifications/ - Syncs to
notificationstable; links existing users viauser_notificationswhen--reconcile-targetsis set - Supports flags:
--dry-run— validate without writing--prune— remove static notifications no longer present in YAML files--reconcile-targets— insertuser_notificationsentries for all existing users matching each notification'stargetpredicate--source-key <key>— sync only the notification with the matchingsource_key
Run after migrations on fresh environments or after adding new notification YAML definitions. Not included in bootstrap to keep bootstrap fast and free of unintended side effects.
Packages API
GET /api/v1/points/packages
Returns available purchase packages for the current user's region, including starter pack eligibility.
Request:
- Auth: Required (JWT)
- Headers:
Authorization: Bearer <token>
Response:
{
"region": "US",
"currency": "USD",
"packages": [
{
"productCode": "new_user_pack_099_60",
"type": "starter",
"priceUsd": "0.99",
"credits": 60,
"badge": null,
"isStarter": true,
"starterEligible": true,
"sortOrder": 0
},
{
"productCode": "basic_pack_499_100",
"type": "regular",
"priceUsd": "4.99",
"credits": 100,
"badge": null,
"isStarter": false,
"starterEligible": false,
"sortOrder": 10
}
]
}
Fields:
region: ISO 3166-1 alpha-2 country code (e.g., "US", "CN")currency: ISO 4217 currency code (e.g., "USD")packages: List of available packagesproductCode: Unique product identifiertype: "starter" (new user pack) or "regular"priceUsd: Price in USD (decimal string)credits: Number of creditsbadge: Optional badge text (e.g., "Popular")isStarter: Whether this is a starter packstarterEligible: Whether user is eligible to purchase starter packsortOrder: Display order (ascending)
Business Logic:
- Determine user's region from
profile.settings.preferences.country(default: "US") - Load package configuration from
backend/src/core/config/static/packages/{country}.yaml(fallback todefault.yaml) - Check starter pack eligibility:
- If
register_bonus_claims.has_purchased_starter_pack = true, exclude starter pack from response - Otherwise, include starter pack with
starterEligible: true
- If
Configuration Files:
- Path:
backend/src/core/config/static/packages/ - Format: YAML
- Example:
us.yaml
region: US
currency: USD
packages:
- product_code: new_user_pack_099_60
type: starter
price_usd: "0.99"
credits: 60
badge: null
sort_order: 0
enabled: true
- product_code: basic_pack_499_100
type: regular
price_usd: "4.99"
credits: 100
badge: null
sort_order: 10
enabled: true
Country/Region Codes:
- Uses ISO 3166-1 alpha-2 standard
- Default:
US(United States) - Examples:
CN(China),TW(Taiwan),HK(Hong Kong),JP(Japan)