Commit graph

46 commits

Author SHA1 Message Date
RainbowBird
a331d3a42a
feat(auth): update AUTH_UI_URL to point to accounts.airi.build
Some checks failed
Cloudflare Workers (server-dev) / Deploy - stage-web (server-dev) (push) Has been cancelled
Cloudflare Workers (server-dev) / Deploy - ui-server-auth (server-dev) (push) Has been cancelled
Cloudflare Workers (server-dev) / Deploy - ui-admin (server-dev) (push) Has been cancelled
2026-06-09 18:01:34 +08:00
RainbowBird
57ba232432
feat(auth): migrate auth ui to unique domain 2026-06-09 00:15:34 +08:00
RainbowBird
9382a12137
chore: cleanup and lint fix 2026-06-07 20:31:22 +08:00
RainbowBird
30d57e7370
refactor(stage): infer conversation analytics surface
Centralize conversation event surface detection inside useAnalytics and keep chat UI call sites focused on business fields.

Signed-off-by: RainbowBird <git@luoling.moe>

Commit-Message-Assisted-by: Codex
2026-06-06 01:58:13 +08:00
RainbowBird
4708e462d2
feat(stage): track conversation product events
Add PostHog product events for manual TTS stop clicks and conversation controls across Web, mobile, and Electron chat surfaces. Document the events in metrics ownership and cover the analytics API with Vitest.

Signed-off-by: RainbowBird <git@luoling.moe>

Commit-Message-Assisted-by: Codex
2026-06-06 01:37:59 +08:00
RainbowBird
cf5ed0766b
fix(server): make analytics best-effort
Remove server-side PostHog wiring so request handlers no longer wait on third-party analytics delivery.

Keep auth activity and product event writes best-effort so degraded analytics do not fail session creation.

Signed-off-by: RainbowBird <git@luoling.moe>

Commit-Message-Assisted-by: Codex (via Codex)
2026-06-05 16:27:10 +08:00
RainbowBird
f72c7b9c72
feat(server): add product analytics dashboard row
Some checks failed
CI / Type Check (push) Waiting to run
Cloudflare Workers / Deploy - stage-web (push) Waiting to run
Release Docker / OCI for Assets / Release Assets (ui-server-auth) (push) Waiting to run
Update Nix pnpmDeps Hash / update (push) Waiting to run
Update Nix pnpmDeps Hash / ci (push) Blocked by required conditions
CI / Build Test (stage-tamagotchi) (push) Waiting to run
CI / Check Provenance (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Build Test (stage-tamagotchi-godot) (push) Waiting to run
CI / Build Test (stage-web) (push) Waiting to run
CI / Build Test (ui-loading-screens) (push) Waiting to run
CI / Build Test (ui-transitions) (push) Waiting to run
CI / Unit Test (push) Waiting to run
Update Nix assets Hash / update (push) Has been cancelled
Update Nix assets Hash / ci (push) Has been cancelled
2026-06-03 23:24:45 +08:00
RainbowBird
c64e48c233
refactor(server): nanoid 2026-06-02 15:40:05 +08:00
RainbowBird
f728f4d639
feat(server): add testing-only bearer token bypass for authentication
Some checks are pending
CI / Build Test (ui-transitions) (push) Waiting to run
CI / Unit Test (push) Waiting to run
CI / Type Check (push) Waiting to run
CI / Check Provenance (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Build Test (stage-tamagotchi) (push) Waiting to run
CI / Build Test (stage-tamagotchi-godot) (push) Waiting to run
CI / Build Test (stage-web) (push) Waiting to run
CI / Build Test (ui-loading-screens) (push) Waiting to run
Cloudflare Workers / Deploy - stage-web (push) Waiting to run
2026-06-01 18:16:21 +08:00
RainbowBird
42436bca53
refactor(server): langfuse upstream model
Some checks are pending
CI / Build Test (ui-transitions) (push) Waiting to run
CI / Unit Test (push) Waiting to run
CI / Type Check (push) Waiting to run
CI / Check Provenance (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Build Test (stage-tamagotchi) (push) Waiting to run
CI / Build Test (stage-tamagotchi-godot) (push) Waiting to run
CI / Build Test (stage-web) (push) Waiting to run
CI / Build Test (ui-loading-screens) (push) Waiting to run
Cloudflare Workers / Deploy - stage-web (push) Waiting to run
Update Nix pnpmDeps Hash / update (push) Waiting to run
2026-05-30 20:13:53 +08:00
RainbowBird
6e871253e7
feat(server): langfuse 2026-05-30 19:35:37 +08:00
RainbowBird
dc1037f349
feat(server): user metrics
Some checks are pending
CI / Build Test (stage-tamagotchi-godot) (push) Waiting to run
CI / Build Test (stage-web) (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Build Test (stage-tamagotchi) (push) Waiting to run
CI / Build Test (ui-loading-screens) (push) Waiting to run
CI / Build Test (ui-transitions) (push) Waiting to run
CI / Unit Test (push) Waiting to run
CI / Type Check (push) Waiting to run
CI / Check Provenance (push) Waiting to run
Cloudflare Workers / Deploy - stage-web (push) Waiting to run
2026-05-30 00:37:51 +08:00
RainbowBird
0ac9b709a2
refactor(server): integrate better-auth admin plugin (#1873)
Some checks are pending
CI / Build Test (ui-loading-screens) (push) Waiting to run
CI / Build Test (ui-transitions) (push) Waiting to run
CI / Type Check (push) Waiting to run
CI / Check Provenance (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Build Test (stage-tamagotchi) (push) Waiting to run
CI / Build Test (stage-tamagotchi-godot) (push) Waiting to run
CI / Build Test (stage-web) (push) Waiting to run
Cloudflare Workers / Deploy - stage-web (push) Waiting to run
2026-05-26 02:04:25 +08:00
RainbowBird
812b2db4ab
chore(server): drop flux_grant_batch schema and tables
Why
- Code, routes, service, worker, tests, and ai-context references for the
  legacy flux_grant_batch flow were removed in the previous commit. The
  schema file and the corresponding production tables were intentionally
  left for a separate DDL-only PR (this one) so the destructive change is
  easy to time and roll back.

What
- Delete src/schemas/flux-grant-batch.ts.
- Drop the re-export line in src/schemas/index.ts.
- drizzle-kit generate produced drizzle/0011_open_unus.sql:
    DROP TABLE flux_grant_batch CASCADE;
    DROP TABLE flux_grant_batch_recipient CASCADE;
  CASCADE removes the 6 associated indexes in one shot.
- docs/ai-context/architecture-overview.md updated: the dead-code
  reminder now points at the migration and explains the rollback story.

Also rolls in a pre-existing local move that was sitting uncommitted:
src/libs/{auth,env,request-auth,ws-auth}.test.ts → src/libs/tests/...
(aligning with the libs/tests/eventa-hono-adapter.test.ts placement that
was already on HEAD).

Deployment
- pnpm typecheck: passes.
- DO NOT run pnpm db:push on prod from this branch automatically. The
  drop is intentionally a separate operator action that requires picking
  a deploy window where no instance is still on an older image that
  could try to read flux_grant_batch. Until 0011 is applied to prod the
  table sits as an orphaned shell — safe to leave indefinitely.
2026-05-18 23:39:14 +08:00
RainbowBird
c627bce9c9
refactor(server): split services into domain/adapter layers, drop dead code
Why
- src/services/ was an unordered mix of single-file services and module
  directories with no shared classification axis, plus several long-dead
  admin batch helpers that survived the move to the simpler synchronous
  admin-flux-grants flow.

What
- services/ now has two top-level layers:
    domain/   — DB state + business rules (billing, characters, chats,
                flux, flux-transaction, llm-router, providers, request-log,
                stripe, user-deletion, admin/{flux-grants,router-config})
    adapters/ — thin wrappers over external SDKs / infra (config-kv, email,
                posthog, tts/)
- admin/* moved under domain/admin/ with consistent plural names
  (flux-grants, router-config).
- tts-adapters/ collapsed to adapters/tts/ (no redundant -adapters suffix
  once nested under adapters/).
- 63 src files + scripts/e2e-llm-router.ts + tests/verifications/_harness.ts
  had relative imports rewritten; git mv preserves blame.
- apps/server/CLAUDE.md and docs/ai-context/*.md updated to match new paths.

Dead code removed
- services/admin-flux-grant-batches/ (service + worker + tests, 1090 LOC) —
  superseded by admin-flux-grants and never wired into app.ts.
- routes/admin/flux-grant-batches/ — same.
- utils/redis-compressed.ts + test — zero production call sites.
- llm-router/index.ts re-exports trimmed from 26 to 6; only symbols with
  external consumers are kept.

Intentionally kept
- schemas/flux-grant-batch.ts and its schemas/index.ts export remain so the
  drizzle-kit generate diff stays empty. Removing them is a separate PR
  that owns the drop-table migration for flux_grant_batch /
  flux_grant_batch_recipient.

Verification
- pnpm -F @proj-airi/server typecheck: passes.
- pnpm exec eslint apps/server: 49 errors, identical to main baseline
  (all are pre-existing node/prefer-global/buffer in envelope-crypto and
  scripts/e2e-llm-router; untouched by this change).
- Vitest passes per-file; the 6 mockDB hook timeouts under full-parallel
  run are the known pushSchema-per-worker infra cost, not a regression.
2026-05-18 23:36:45 +08:00
RainbowBird
8ef5844928
refactor(server): drop seed-router-config / seed-streaming-tts scripts
The two seed scripts are fully superseded by the new admin endpoint
`POST /api/admin/config/router` — same encryption, same configKV
writes, same `configkv:invalidate` publish, plus auth + audit + body
limits. Keeping both code paths created a drift risk on the AAD label
and the merge semantics.

Doc + test fallout:
- `e2e-llm-router.ts` now points readers to the admin endpoint for
  the prerequisite seed step.
- `docs/ai-context/verifications/llm-router.md` and
  `streaming-tts.md` get curl-based seed instructions; the 2026-05-15
  llm-router evidence stays intact with a note that the script it
  used has since been removed.
- The U9 follow-up entry in `llm-router.md` flips from "not shipped"
  to "partially shipped" — ETag + HMAC publish are still deferred,
  so the `config_write` / `config_invalid_hmac` Grafana panels stay
  parked.
- Self-edit on the admin route + `app.ts` docstrings to drop the
  earlier "scripts stay as break-glass" wording.
2026-05-18 23:36:07 +08:00
RainbowBird
ba9247fb47
feat(server,stage-ui): bidirectional streaming TTS + audio path refactor
Why:
- Add a real bidirectional streaming TTS path: raw LLM tokens are
  forwarded to the upstream model (Volcengine v3 via the unspeech ws
  bridge) without client-side segmentation, so the model owns sentence
  splitting and audio chunks play as they arrive.
- Move audio endpoints out of /api/v1/openai/. `/audio/voices`,
  `/audio/models`, `/audio/voices/streaming` are not real OpenAI public
  APIs, and the streaming TTS surface has nothing to do with OpenAI —
  keeping them under /openai/ mislabelled the contract.
- Introduce `capabilities.speech.transport` on ProviderDefinition so
  future streaming providers (ElevenLabs / Cartesia / OpenAI Realtime)
  opt in without touching Stage.vue or the session factory.
- Unify Stage.vue's TTS path through a single StageTtsSession so the
  chat-orchestrator hooks no longer branch on provider id.

What:
- apps/server: new ws proxy /api/v1/audio/speech/ws bridges client ↔
  unspeech with auth, pre-flight flux check, billing from upstream
  session.finished.usage, OTel spans.
- apps/server: audio routes moved from /api/v1/openai/audio/* to
  /api/v1/audio/* (hard cutover; 404 sentinel tests added).
- apps/server: new /api/v1/audio/voices/streaming proxy reads voices
  from unspeech /api/voices?provider=volcengine.
- apps/server: new STREAMING_TTS_UPSTREAM configKV entry +
  scripts/seed-streaming-tts.ts.
- stage-ui: new libs/speech/streaming-pipeline.ts opens one ws per LLM
  intent (appendText / finish / cancel + onSentence / onError / onDone).
- stage-ui: new libs/speech/tts-session.ts — StageTtsSession interface
  with segmenter and streaming adapters; factory dispatches by
  capabilities.speech.transport instead of hard-coded provider id.
- stage-ui: providerOfficialSpeechStreaming with capabilities.speech =
  { transport: 'bidirectional-ws' }; settings page with model/voice
  picker + ws-based preview.
- stage-ui: Stage.vue chat hooks collapsed to a single currentSession;
  hot-swap watcher cancels mid-session on provider/voice/model change;
  unmount cancels and drains playback.

Tests:
- 9 streaming-pipeline tests (happy path / buffered / error / cancel /
  truncation)
- 11 tts-session tests (factory branch coverage + adapter contracts)
- 4 audio-speech-ws route tests (forwarding / billing / pre-flight /
  config-missing)
- 3 legacy-path 404 sentinels in v1 route tests
- Verification doc updated to reflect automated coverage.
2026-05-18 23:34:35 +08:00
RainbowBird
bf1aba757c
feat(server): stream tts provider 2026-05-18 23:33:24 +08:00
RainbowBird
9aef35948c
refactor(server): llm router config sync subscriber 2026-05-18 23:32:33 +08:00
RainbowBird
4da4a72703
feat(server): finalize in-process LLM/TTS router cutover
End-state of the multi-step KTD-5 / KTD-6 / U8 work. The knoway sidecar
is no longer reachable from server code; the router is required at boot
and now owns chat completions, TTS synthesis, and voice catalog listing.

Highlights:
- LLM_ROUTER_MASTER_KEY becomes required; app.ts drops the graceful-
  skip branch and the chat fallback fetch path is gone.
- /audio/speech and /audio/voices route through new routeTts /
  listTtsVoices entries that reuse the chat key-rotator + per-attempt
  timeout + abort propagation.
- DEFAULT_CHAT_MODEL / DEFAULT_TTS_MODEL move from env to configKV so
  default-model swaps are hot-reloadable via Pub/Sub.
- GATEWAY_BASE_URL removed from env schema, .env, .env.local, smoke,
  verification harness. Redis upstream-voices cache deleted — catalogs
  come from in-process adapter JSON.
- routeTts splits adapter error contract by ApiError statusCode:
  4xx propagates without fallback; 5xx folds into the network-failure
  fallback path. handleTTS wraps billing + span attribute in try/finally
  to plug a span leak when ttsMeter.accumulate() throws.
- seed-router-config.ts rewritten with --merge (default) / --reset /
  --dry-run modes and env-var key handoff (OPENROUTER_KEY / AZURE_KEY /
  DASHSCOPE_KEY) so prod seed flows never put plaintext on the CLI.
  Adds DashScope CosyVoice seeding.

Docs (CLAUDE.md, architecture-overview.md, transport-and-routes.md)
reflect the new boundary. verifications/llm-router.md replaces the
overstated "U1-U9 shipped" line with an evidence-vs-pending table.

Tests: full 40-file / 343-case server suite green. New regressions pin
ApiError 4xx → no-fallback, ApiError 5xx → fallback, TTS billing
failure → span closed and error propagated.
2026-05-18 23:32:33 +08:00
RainbowBird
5d256e4951
feat(server): replace legacy health endpoints with K8s-style /livez and /readyz probes 2026-05-18 23:32:20 +08:00
RainbowBird
af17e9cbc2
feat(server): llm & tts gateway (#1837) 2026-05-18 23:31:07 +08:00
RainbowBird
a3a401d79d
feat(server): new metrics for auth
Some checks are pending
CI / Lint (push) Waiting to run
CI / Build Test (stage-tamagotchi) (push) Waiting to run
CI / Build Test (stage-tamagotchi-godot) (push) Waiting to run
CI / Build Test (stage-web) (push) Waiting to run
CI / Build Test (ui-loading-screens) (push) Waiting to run
CI / Build Test (ui-transitions) (push) Waiting to run
CI / Type Check (push) Waiting to run
CI / Check Provenance (push) Waiting to run
Cloudflare Workers / Deploy - stage-web (push) Waiting to run
- Added a new ObservableGauge for distinct active users to track real active user count, mitigating session row inflation issues.
- Updated the Grafana dashboard to reflect changes, including the removal of redundant WS Connections panel and the addition of new metrics for active sessions and distinct users.
- Improved documentation for verification automation processes, outlining a structured approach to automate verification steps and maintain evidence of tests.
2026-05-15 18:54:05 +08:00
RainbowBird
3984677b01
feat(analytics): integrate PostHog for server-side event tracking
Some checks failed
CI / Lint (push) Waiting to run
CI / Build Test (stage-tamagotchi) (push) Waiting to run
CI / Build Test (stage-tamagotchi-godot) (push) Waiting to run
CI / Build Test (stage-web) (push) Waiting to run
CI / Build Test (ui-loading-screens) (push) Waiting to run
CI / Build Test (ui-transitions) (push) Waiting to run
CI / Type Check (push) Waiting to run
CI / Check Provenance (push) Waiting to run
Cloudflare Workers / Deploy - stage-web (push) Waiting to run
Cloudflare Workers (server-dev) / Deploy - stage-web (server-dev) (push) Waiting to run
Update Nix pnpmDeps Hash / update (push) Has been cancelled
- Added a new PostHog client for capturing server-side business events such as Stripe webhooks and subscription state changes.
- Implemented various tracking functions for pricing funnel steps, character creation, and chat session starts.
- Enhanced the flux meter tests to handle partial charges and report unbilled flux correctly.
- Updated the CharacterDialog and Flux settings pages to track user interactions with analytics events.
- Introduced a mechanism to identify users on PostHog based on authentication state to ensure accurate funnel tracking.
- Added necessary dependencies for PostHog integration in the project.
2026-05-15 16:20:47 +08:00
RainbowBird
272cdae03b
feat(server/otel): restructure observability metrics and add active sessions gauge
- Moved RateLimitMetrics import path to a more centralized location.
- Introduced a new file for active sessions gauge to track user sessions in the database.
- Updated index.ts to include new metrics and ensure proper initialization of observability metrics.
- Modified various routes and services to utilize the new observability structure.
- Added smoke tests for HTTP and WebSocket metrics to ensure proper metric registration and functionality.
- Enhanced error handling for metrics reading failures to improve observability.
2026-05-12 23:10:13 +08:00
RainbowBird
fe59f91c84
refactor(server/flux): replace UpDownCounter with ObservableGauge for WebSocket connections
- Updated the EngagementMetrics interface to use ObservableGauge for tracking active WebSocket connections.
- Added detailed comments explaining the rationale for this change, highlighting the benefits of using a pull-based gauge over a delta-based counter.
- Implemented the ObservableGauge in the createChatWsHandlers function, ensuring it accurately reflects the live count of active connections.
- Removed the previous UpDownCounter logic to prevent issues with connection drift during process crashes or network interruptions.
2026-05-09 00:23:02 +08:00
RainbowBird
f2cf395fd2
docs(server): update ai-context 2026-05-08 22:21:31 +08:00
RainbowBird
f8d1fa7a64
refactor(server): drop redis stream + worker role (#1792)
Some checks failed
CI / Build Test (stage-tamagotchi) (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Build Test (stage-tamagotchi-godot) (push) Waiting to run
CI / Build Test (stage-web) (push) Waiting to run
CI / Build Test (ui-loading-screens) (push) Waiting to run
CI / Build Test (ui-transitions) (push) Waiting to run
CI / Type Check (push) Waiting to run
CI / Check Provenance (push) Waiting to run
Cloudflare Workers / Deploy - stage-web (push) Waiting to run
Update Nix assets Hash / update (push) Has been cancelled
Update Nix pnpmDeps Hash / update (push) Has been cancelled
The Redis Stream `billing-events` + `worker` Railway role +
advisory-lock poller layered together didn't actually buy us reliability
— `debitFlux` swallowed XADD failures, leaving the door open to "balance
updated, ledger row never written". Collapse the whole thing back to:
`creditFlux` and `debitFlux` write `flux_transaction` ledger rows inline
within the same DB transaction that mutates `user_flux`, and `(user_id,
request_id)` remains the partial unique index that keeps retries safe.

Concrete changes:
- Inline ledger inserts in `BillingService.{debitFlux, creditFlux,
creditFluxFromStripeCheckout, creditFluxFromInvoice}`; drop `billingMq`
and `publishEvent` plumbing entirely.
- `routes/openai/v1` writes `llm_request_log` synchronously via the
existing `requestLogService`; the duplicate `llm-request-log.ts` service
module is removed.
- `bin/run-worker.ts`, `libs/mq/*`,
`services/billing/billing-events.ts`,
`services/billing/billing-consumer-handler.ts`, and matching tests are
deleted. CLI now exposes only `api`.
- `BILLING_EVENTS_*` env vars and the `DEFAULT_BILLING_EVENTS_STREAM`
helper are dropped; `docker-compose.yml` no longer ships a worker
service.
- `docs/ai-context/{workers-and-runtime, billing-architecture,
redis-boundaries-and-pubsub, data-model-and-state,
architecture-overview, README}.md`, `CLAUDE.md`, and the existing
verification docs are updated to describe the single-process synchronous
pipeline.

Tests: 29 files / 247 cases pass. Production deployments need to drop
the worker Railway service after this lands.
2026-05-08 21:14:01 +08:00
RainbowBird
99956014fa
feat(server): admin api (#1788) 2026-05-08 21:14:00 +08:00
RainbowBird
6f0b7e0b9b
feat(auth): delete account (#1756)
Some checks failed
CI / Build Test (ui-transitions) (push) Waiting to run
CI / Type Check (push) Waiting to run
CI / Check Provenance (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Build Test (stage-tamagotchi) (push) Waiting to run
CI / Build Test (stage-tamagotchi-godot) (push) Waiting to run
CI / Build Test (stage-web) (push) Waiting to run
CI / Build Test (ui-loading-screens) (push) Waiting to run
Cloudflare Workers / Deploy - stage-web (push) Waiting to run
Release Docker / OCI for Assets / Release Assets (ui-server-auth) (push) Has been cancelled
Update Nix pnpmDeps Hash / update (push) Has been cancelled
2026-04-28 20:53:00 +08:00
RainbowBird
c0859c9798
fix(server/docker): squash auth ui to server image 2026-04-28 00:34:14 +08:00
RainbowBird
172e4ce59c
feat(auth): email login & profile (#1745)
Co-authored-by: Liet Blue <127093491+lietblue@users.noreply.github.com>
2026-04-28 00:07:38 +08:00
RainbowBird
8967bbe211 feat(server): add TTS support with per-character billing 2026-04-17 02:59:14 +08:00
RainbowBird
a11727020a feat(server/billing): implement debt ledger for TTS service using FluxMeter 2026-04-17 02:59:14 +08:00
RainbowBird
93888e6935
feat(server): use stripe product as flux pricing (#1640) 2026-04-12 04:32:42 +08:00
RainbowBird
d782313464
feat(auth): jwt sign 2026-04-02 17:48:12 +08:00
RainbowBird
2eb30d6081
docs(auth): ai-context 2026-04-02 17:48:12 +08:00
RainbowBird
f1fe161bc0
feat(auth): OIDC (#1531) 2026-04-02 04:24:04 +08:00
RainbowBird
2f730ef93a feat(server): refactor flux auditing to transaction logging 2026-03-28 02:25:44 +08:00
RainbowBird
750e48ad95 refactor(server): redesign server routes structure 2026-03-28 02:25:44 +08:00
RainbowBird
c0d4c9043a docs(server): ai context 2026-03-28 02:25:44 +08:00
RainbowBird
bf85158aa9 fix(server,stage-ui,i18n): address PR #1376 review comments 2026-03-28 02:25:44 +08:00
RainbowBird
eab60d0bdb feat(server): reimplement message queue, drop outbox 2026-03-28 02:25:44 +08:00
RainbowBird
ba3d66de86 feat(server-protocol): introduce shared protocol types for AIRI server clients and frontends
- Added package.json for @proj-airi/server-protocol with necessary configurations.
- Implemented chat event types including WireMessage, SendMessagesRequest, and PullMessagesRequest.
- Defined WebSocket event types and structures for better integration with AIRI components.
- Updated server-runtime and server-sdk to utilize the new server-protocol package.
- Refactored imports across various packages to replace server-shared types with server-protocol types.
- Enhanced type definitions and added TypeScript configurations for better development experience.
2026-03-28 02:25:44 +08:00
RainbowBird
9641df258e docs(server): add ai-context 2026-03-28 02:25:44 +08:00
RainbowBird
ef18772798 feat(server): message queue 2026-03-28 02:25:44 +08:00