This runbook covers non-visual operation of the tri-model personality API.
The UI can be built separately against the API contract.
Requirements:
Node.js 24 or newer
Install the locked dependency set:
npm install
Run tests:
npm test
Start API server:
npm start
Default base URL:
http://localhost:3000
Health check:
GET /health
API endpoint index:
GET /api
Use .env.example as the reference list.
npm start loads .env when the file exists. Deployment-provided environment variables still take precedence.
Required for basic dry-run mode:
none
Required for live explanation mode:
OPENAI_API_KEY
OPENAI_MODEL
Optional:
PORT
HOST
CORS_ORIGINS
TRUST_PROXY
APP_ACCESS_TOKEN
APP_OWNER_ID
REQUEST_LOGGING_ENABLED
RATE_LIMIT_ENABLED
RATE_LIMIT_WINDOW_MS
RATE_LIMIT_MAX
PROFILE_DATABASE_FILE
PROFILE_STORE_FILE
OPENAI_RESPONSES_URL
OPENAI_TIMEOUT_MS
OPENAI_MAX_OUTPUT_TOKENS
OPENAI_MAX_RETRIES
HOST defaults to 127.0.0.1. If it is changed to a non-loopback address, startup fails unless APP_ACCESS_TOKEN is configured. Protected API calls then require:
Authorization: Bearer <APP_ACCESS_TOKEN>
CORS_ORIGINS is a comma-separated allowlist. Wildcard origins are rejected. Set TRUST_PROXY=true only behind a trusted reverse proxy that overwrites x-forwarded-for.
- Check server capability:
GET /health
- Optionally discover available endpoints:
GET /api
- Start the complete frontend experience when the UI wants one aggregated payload:
POST /api/experience/start
- Inspect the public comparison rule summary:
POST /api/rules/summary
- Validate form input before generation:
POST /api/profile/validate
- Preview a profile without saving:
POST /api/profile/preview
- Generate a profile:
POST /api/profile/generate
-
Store
profile_idin the UI state. -
Rebuild derived models when rules or providers change:
POST /api/profile/rebuild
- Rebuild selected profiles when rules or providers change:
POST /api/profile/rebuild-batch
- List profile summaries for a management or restore view:
POST /api/profile/list
- Fetch profile stats for dashboards or empty-state decisions:
POST /api/profile/stats
- Fetch the public profile if needed:
POST /api/profile/get
- Fetch deterministic comparison:
POST /api/profile/compare
- Export a profile package for download, backup, or external handoff:
POST /api/profile/export
- Export multiple selected profiles:
POST /api/profile/export-batch
- Import a previously exported profile package when restoring data:
POST /api/profile/import
- Import a previously exported profile batch package:
POST /api/profile/import-batch
- Build or run explanation:
POST /api/profile/explain
Use mode: "dry_run" until the live provider is configured.
- Delete or clear explanation history:
POST /api/profile/explanation/delete
POST /api/profile/explanation/clear
- Delete local profile data when it is no longer needed:
POST /api/profile/delete
- Delete multiple selected profiles:
POST /api/profile/delete-batch
Default storage is memory-only. SQLite is the recommended single-user persistent store:
PROFILE_DATABASE_FILE=./data/profiles.sqlite
SQLite uses write-ahead logging and owner-scoped profile keys. The database file still contains private data and should not be committed or served as a static asset.
Legacy JSON persistence remains available for migration and small local demos:
PROFILE_STORE_FILE=./data/profiles.json
Use a managed database and replace the bearer-token authenticator before a multi-user production deployment.
POST /api/experience/start is the preferred frontend entry point. It validates input, creates or refreshes the owner-scoped profile, returns the complete public profile, and optionally prepares or runs an explanation.
{
"birth_date": "1990-03-15",
"birth_time": "10:30",
"location": "Shanghai",
"timezone": "Asia/Shanghai",
"mbti": "INTJ",
"explanation_mode": "dry_run",
"user_question": "What is the main tension?"
}explanation_mode may be none, dry_run, or live. The response always keeps the generated profile available; an explanation-provider failure is reported in explanation_error without discarding the profile.
Validate profile form input:
{
"birth_date": "1990-03-15",
"birth_time": "10:30",
"location": "Shanghai",
"mbti": "INTJ"
}Validation returns normalized input and does not save profile data.
Preview profile output without saving:
{
"birth_date": "1990-03-15",
"birth_time": "10:30",
"location": "Shanghai",
"mbti": "INTJ"
}Preview returns the same public profile shape used by POST /api/profile/get, including the deterministic comparison model, but it does not store the profile.
Generate is idempotent within the authenticated owner. A new profile receives a random UUID; repeating the same normalized input for the same owner reuses that UUID and preserves created_at and explanation history. Another owner receives a different profile.
Inspect public comparison rules:
{}The rules summary includes model authority, model weights, trait axes, context explanation keys, and zodiac date boundaries. It does not include profile data, prompts, or debug traces.
Rebuild an existing profile from stored birth_info:
{
"profile_id": "string"
}Rebuild preserves profile_id, created_at, and explanation history while refreshing derived models.
Rebuild selected profiles in batch:
{
"profile_ids": ["string"]
}Batch rebuild supports up to 50 ids, skips duplicates, reports missing ids in missing_profile_ids, and preserves each profile's profile_id, created_at, and explanation history.
List stored profile summaries:
{
"limit": 50,
"offset": 0,
"mbti_type": "INTJ",
"zodiac_sign": "Pisces",
"bazi_day_master": "water",
"has_explanations": false
}The list response is intended for management views and restore entry points. It includes identifiers and model summaries, but not full birth_info.
limit and offset are optional. limit defaults to 50 and cannot exceed 100.
The filter fields are optional. Use them to build management views by MBTI type, zodiac sign, Bazi day master, or explanation-history status.
Profile stats:
{}Stats include counts and distributions only. They do not include full profile data, birth data, explanations, or prompts.
Dry-run request:
{
"profile_id": "string",
"mode": "dry_run"
}Live request:
{
"profile_id": "string",
"mode": "live",
"user_question": "Explain the main contradictions.",
"include_history": true,
"history_limit": 3
}If live mode is not configured, the API returns 503.
Successful live explanations are persisted on the profile and returned by:
POST /api/profile/get
Dry-run explanation requests are not persisted.
Follow-up explanation requests include recent saved explanations by default. Set include_history to false for a standalone answer, or set history_limit from 1 to 10 to control how much context is sent.
Delete one explanation record:
{
"profile_id": "string",
"explanation_id": "string"
}Clear all explanation records on a profile:
{
"profile_id": "string"
}Delete profile data:
{
"profile_id": "string"
}Deletion removes the profile from the active store. With file storage enabled, the JSON store is rewritten without that profile.
Delete selected profiles in batch:
{
"profile_ids": ["string"]
}Batch delete supports up to 50 ids, skips duplicates, and reports missing ids in missing_profile_ids.
Export profile data:
{
"profile_id": "string"
}The export response includes:
export_versionexported_atprofile
The profile payload uses the same public shape as POST /api/profile/get and excludes internal debug data.
Batch export selected profiles:
{
"profile_ids": ["string"]
}Batch export supports up to 50 ids, skips duplicates, and reports missing ids in missing_profile_ids.
Import a profile export package:
{
"export_version": "profile_export.v0",
"overwrite": false,
"profile": {}
}Import defaults to no overwrite. If a profile with the same profile_id already exists, pass:
{
"overwrite": true
}Imported birth input and explanation records are validated before save. Derived Bazi, Zodiac, MBTI status, and comparison fields are recomputed from birth_info, so edited export calculations cannot poison stored results.
Import a profile batch export package:
{
"export_version": "profile_export_batch.v0",
"overwrite": false,
"profiles": []
}Batch import supports up to 50 profiles, skips duplicate ids in the same request, and skips existing profiles unless overwrite is true.
Request logs are structured JSON.
Logged fields:
request_idmethodpathstatusduration_ms
Not logged:
- request body
- birth data
- MBTI values
- LLM prompts
- API keys
Each response includes:
x-request-id
The request body has invalid fields. Check error.details.fields.
The profile id does not exist in the current store.
If using memory storage, restart clears all profiles.
Import found an existing profile with the same id. Retry with overwrite: true only when replacing that profile is intentional.
Live explanation mode requires OPENAI_API_KEY.
Live explanation mode requires OPENAI_MODEL.
The provider returned JSON that did not match the required explanation shape.
The profile exists, but the requested explanation id does not.
Too many requests were sent in the configured time window. Check retry-after and error.details.retry_after_ms.
The default Bazi provider is lunar_javascript_cn. It uses computed Jie Qi boundaries through lunar-javascript and accepts Asia/Shanghai civil time. The response includes calendar_basis so the frontend can disclose the basis. Location is retained as metadata; true-solar-time longitude correction is not yet applied.
It is deterministic and useful for MVP product testing, but it is not a high-precision astronomical calendar implementation.
See bazi-provider-contract.md before replacing the provider.