Commit Graph

972 Commits

Author SHA1 Message Date
Claude 84f9341207 feat(stream-check): detect model-not-found errors with dedicated toast
Health check previously classified failures purely by HTTP status code,
which meant deprecated/invalid models showed up as a generic "Not found
(404)" error pointing users to check the Base URL — misleading when the
URL is fine and only the test model is wrong (e.g. gpt-5.1-codex after
it was retired).

Backend: add detect_error_category() that inspects 4xx response bodies
for model-not-found indicators (model_not_found, does not exist,
invalid model, not_found_error, etc.) and returns a "modelNotFound"
category. Thread the resolved test model through build_stream_check_result
so the failed result carries it in model_used. Add StreamCheckResult
.error_category field (serde-skipped when None).

Frontend: useStreamCheck branches on errorCategory === "modelNotFound"
before the HTTP-status fallback and renders a toast.error with the model
name and a description pointing to Model Test Config. Add i18n keys
(modelNotFound / modelNotFoundHint) for zh/en/ja.

Tests: unit-test detect_error_category against real OpenAI/Anthropic
error shapes, 5xx false-positive avoidance, and plain 401 auth errors.

https://claude.ai/code/session_01NGWLchcTP76rJHjiP5Ehte
2026-04-15 05:21:23 +00:00
Claude 5ad7b9929d chore(stream-check): update default health check models to latest
Replaces deprecated gpt-5.1-codex@low with gpt-5.4@low and switches
the Gemini default from gemini-3-pro-preview to gemini-3-flash-preview
to pick the lightest variant of the latest series for fast, low-cost
health checks.

https://claude.ai/code/session_01NGWLchcTP76rJHjiP5Ehte
2026-04-15 05:07:15 +00:00
Jason 689ca08409 feat: classify stream check errors with color-coded toasts
Distinguish between "provider rejects probe" (yellow warning) and
"genuinely broken" (red error) in health check results.

Backend: add AppError::HttpStatus variant to carry structured HTTP
status codes, populate http_status on error results, classify codes
into short labels (e.g. "Auth rejected (401)"), and truncate overly
long response bodies.

Frontend: route 401/403/400/429/5xx to toast.warning with localized
hints explaining the error may not indicate actual unusability; route
404/402/connection errors to toast.error. Add i18n keys for all three
locales (zh/en/ja).

Also deduplicate check_once by reusing build_stream_check_result.
2026-04-14 17:11:13 +08:00
Jason 8b851dc602 fix: auto-expand collapsed messages when search matches hidden content
When a search query matches text beyond the collapse point, the message
automatically expands to show the highlighted match. Also adds
aria-expanded for accessibility.
2026-04-14 16:50:22 +08:00
Jason 0383c13e66 perf: collapse long session messages to reduce text layout cost
Messages over 3000 characters are now truncated to 1500 characters by
default, with an expand/collapse toggle. This avoids expensive browser
text layout for large AI responses containing code or tool output.
2026-04-14 16:16:57 +08:00
Jason 7ae89e9106 perf: virtualize session message list for long conversations
Replace full DOM rendering with @tanstack/react-virtual to only render
visible messages (~25 DOM nodes instead of N). Wrap SessionMessageItem
in React.memo to prevent unnecessary re-renders on state changes.
2026-04-14 16:12:48 +08:00
Jason 57343432da fix: remove duplicate usage summary from app filter bar
The request count and total cost were displayed both in the app filter
bar and in the UsageSummaryCards below it. Remove the redundant inline
summary and its unused imports.
2026-04-14 15:47:55 +08:00
Jason 5e412d2c30 refactor: remove per-provider proxy config feature
Replace all remaining "代理服务/Proxy Service/プロキシサービス" references
in the local routing feature context with "路由服务/Routing Service/
ルーティングサービス". This covers service settings, status messages,
tooltips, field descriptions, and tab labels.

Global Proxy, HTTP proxy hints, and AI Agent references are unchanged.
2026-04-14 15:39:23 +08:00
Jason 36f2d6cccb docs: clarify global proxy hint about local routing across all locales 2026-04-14 15:26:08 +08:00
Jason 7ec92c32a5 rename: rebrand "Local Proxy Takeover" to "Local Routing" in all locales
Replace all user-facing references to "本地代理接管/Local Proxy/Takeover"
with "本地路由/Local Routing/ローカルルーティング" across zh/en/ja to
eliminate naming confusion with the separate "Global Proxy" feature.

Only i18n string values are changed; keys, code identifiers, and
database schema remain untouched.
2026-04-14 15:13:59 +08:00
Jason 4a0b5c3dec refactor: remove per-provider proxy config feature
The per-provider proxy configuration (meta.proxyConfig) is removed
because its scope is too narrow and covered by global proxy settings
and proxy takeover mode. Users can achieve the same result via the
global proxy panel.

Changes:
- Remove ProviderProxyConfig type (frontend TS + backend Rust)
- Remove ProviderAdvancedConfig proxy UI block, keep testConfig/pricingConfig
- Simplify http_client: delete build_proxy_url_from_config,
  build_client_for_provider, get_for_provider
- Simplify forwarder/stream_check/model_fetch to use global client
- Remove i18n keys (en/zh/ja)
- Fix pre-existing test bug in transform.rs (extra None arg)
2026-04-14 14:26:55 +08:00
Jason 8c94c23c3c chore: update PIPELLM website URL to code.pipellm.ai 2026-04-14 10:33:41 +08:00
Jason d6fa611833 chore: remove X-Code API (XCodeAPI) provider preset and sponsorship 2026-04-14 10:33:41 +08:00
Jason 5c1b457520 fix(usage): sync request log time range with dashboard 1d/7d/30d selector
The RequestLogTable had a hardcoded 24-hour rolling window, ignoring the
dashboard's time range selector. Now it accepts a timeRange prop and
dynamically adjusts the query window, so users can view logs beyond just
the last day.
2026-04-14 10:33:41 +08:00
Jason 3c8f9d1287 feat(usage): improve pagination with first/last 3 pages and page jump input
Show first 3 and last 3 page buttons instead of just first/last, with
Set-based deduplication for clean edge merging. Add a page number input
field with Go button for direct page navigation.
2026-04-14 10:33:41 +08:00
Jason 420f4c8c23 fix(sessions): strip OpenClaw message_id suffix and allow 2-line titles
OpenClaw gateway injects `[message_id: UUID]` metadata at the end of
every message, wasting display space. Strip this suffix from both title
and summary fields.

Also change session title display from single-line truncate to
line-clamp-2, so longer titles (e.g. OpenClaw's timestamp-prefixed
messages) can show more meaningful content across two lines.
2026-04-14 10:33:41 +08:00
Jason a514f27937 feat: block official provider switching during proxy takeover
Prevent users from switching to official providers (Anthropic/OpenAI/Google)
when proxy takeover is active, as using a proxy with official APIs may cause
account bans.

Defense-in-depth across 4 layers:
- Backend: ProviderService::switch(), hot_switch_provider(), switch_proxy_provider command
- Frontend: useProviderActions soft guard with error toast
- UI: ProviderActions button disabled with ShieldAlert icon
- Tray menu: official provider items disabled with  indicator

Also warns when enabling proxy takeover while current provider is official.
2026-04-14 10:33:41 +08:00
Dex Miller 5566be2b4b Stop sending prompt cache keys on Claude chat conversions (#2003)
Responses conversions still use promptCacheKey, but chat completions now stay a pure shape transform. This keeps Claude -> chat requests aligned with providers that do not understand the field and keeps stream checks consistent with production behavior.

Constraint: Issue #1919 requires removing prompt_cache_key from Claude -> OpenAI Chat requests
Rejected: Add a runtime toggle for chat injection | requested behavior is unconditional removal
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep promptCacheKey limited to Claude -> Responses conversions unless a provider-specific contract is proven
Tested: cargo test anthropic_to_openai
Tested: cargo test anthropic_to_responses_with_cache_key
Tested: cargo test transform_claude_request_for_api_format_responses
Not-tested: Full src-tauri test suite
Related: #1919
2026-04-13 10:22:55 +08:00
v2v cfcf9452d0 添加应用级别窗口按钮,以改善linux wayland下系统窗口按钮失效的问题 (#1119)
* feat(window): add app-level window controls with settings toggle

Add a persistent settings toggle to enable app-level minimize/maximize/close controls and hide system decorations when enabled, providing a Wayland-friendly fallback for broken native titlebar interactions.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(window): restrict app-level window controls to Linux only and fix startup flicker

- Guard useAppWindowControls with isLinux() in App.tsx so it's always
  false on macOS/Windows even if persisted as true
- Wrap set_decorations call in lib.rs with #[cfg(target_os = "linux")]
- Only show the toggle in WindowSettings on Linux
- Skip setDecorations effect while settingsData is still loading to
  prevent the Rust-side decoration state from being overridden by the
  undefined->false fallback, which caused a brief title bar flicker

---------

Co-authored-by: wzk <wx13571681304@outlook.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Jason <farion1231@gmail.com>
2026-04-12 20:59:04 +08:00
Jason 8838317087 chore: simplify Codex provider preset name 2026-04-10 22:40:29 +08:00
Jason 4d570691e1 chore: add missing Shengsuanyun icon SVG file 2026-04-10 22:40:29 +08:00
Jason 337224546c feat: update PIPELLM preset with full config, add Codex support and icon
Update base URL to cc-api.pipellm.ai, add complete model definitions
(Opus/Sonnet/Haiku), add Codex preset with custom TOML config, add
pipellm PNG icon, and set apiKeyUrl to referral link.
2026-04-10 22:40:29 +08:00
Jason 7d21663e6d feat: add E-FlowCode provider preset across all five apps
Multi-protocol provider: Anthropic for Claude, OpenAI Responses for
Codex/OpenClaw, OpenAI for OpenCode, custom Gemini config. Includes
PNG icon via URL import and provider-specific settings like effortLevel,
enabledPlugins, and custom TOML config for Codex.
2026-04-10 22:40:29 +08:00
Jason 926e69b8c9 feat: add PIPELLM provider preset for Claude, OpenCode, and OpenClaw 2026-04-10 22:40:29 +08:00
Jason a03ad4f47f feat: add Shengsuanyun provider preset with partner promotion
Add Shengsuanyun (胜算云) as an aggregator partner across all five apps,
positioned right after official providers. Uses anthropic-messages
protocol for OpenCode/OpenClaw. Includes URL-based icon import (217KB
SVG), partner promotion i18n for zh/en/ja, and localized display name.
2026-04-10 22:40:29 +08:00
Jason c38bef517a fix: capitalize DDSHub provider display name 2026-04-10 22:40:29 +08:00
Jason 84bc98cf83 feat: add LionCCAPI provider preset with partner promotion
Add LionCCAPI as a third-party partner provider across all five apps
(Claude, Codex, Gemini, OpenCode, OpenClaw) with anthropic-messages
protocol for OpenCode and OpenClaw. Include partner promotion i18n
entries for zh/en/ja locales and lioncc icon.
2026-04-10 22:40:29 +08:00
Jason a83bd90fa9 feat: replace x-code icon with high-res xcode icon via URL import
Replace the old low-res x-code inline SVG (7.6KB) with a new high-res
xcode.svg (286KB) loaded via Vite URL import. Update all three provider
preset files to reference the new icon name.
2026-04-10 22:40:29 +08:00
Jason 794795cad4 feat: support URL-based icons for large SVGs and raster images
Add dual rendering mode to the icon system: small optimized SVGs
continue to be inlined via dangerouslySetInnerHTML, while large SVGs
and raster images (png/jpg/webp/etc) use Vite URL imports rendered
as <img> tags. Added dds.svg (1.4MB) as the first URL-based icon.

Updated generate-icon-index.js to support multi-format icons with
a manual URL_ICONS control list. Updated ProviderIcon to handle
both inline SVG and URL-based rendering paths.
2026-04-10 22:40:29 +08:00
Jason eff85dc66d feat: add ddshub provider preset with partner promotion
Add ddshub as a third-party partner provider for Claude, including
SVG icon and i18n promotion text in zh/en/ja locales.
2026-04-10 22:40:29 +08:00
Dex Miller 3aef5217cb Restore first-class OMO Slim council support (#1981) (#1982)
cc-switch could already persist arbitrary OMO Slim agent keys and top-level fields, but the built-in metadata and UI copy still reflected the pre-council agent set. This made the upstream council feature look unsupported and pushed users toward manual JSON-only setup.

Promote council to a built-in OMO Slim agent, add copy that points top-level plugin settings at Other Fields, and lock the behavior with regression tests.

Constraint: oh-my-opencode-slim exposes council through both agents.council and top-level council config
Rejected: Add a dedicated council editor UI now | too much surface area for issue #1981
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep OMO Slim built-in agent metadata aligned with upstream agent additions before shipping UI support
Tested: pnpm exec vitest run tests/utils/omoConfig.test.ts tests/components/OmoFormFields.mergeCustomModelsIntoStore.test.ts
Tested: pnpm typecheck
Not-tested: End-to-end validation against a live oh-my-opencode-slim installation
Related: farion1231/cc-switch#1981
2026-04-10 22:36:32 +08:00
Dex Miller e4b58c7206 Let Kaku users launch sessions from their chosen terminal (#1954) (#1983)
Kaku is a WezTerm-derived macOS terminal, so reusing the existing WezTerm-compatible launch path keeps the change small while making it selectable in settings and session resume flows.

Constraint: Kaku support should stay macOS-only and avoid introducing a separate launcher model
Rejected: Treat Kaku as a silent WezTerm fallback | users could not explicitly choose it in settings
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep Kaku on the shared WezTerm-compatible launch path unless upstream drops the start-compatible CLI
Tested: pnpm typecheck; pnpm format:check; cargo check --manifest-path src-tauri/Cargo.toml; cargo fmt --manifest-path src-tauri/Cargo.toml --check; cargo test --manifest-path src-tauri/Cargo.toml --lib session_manager::terminal::tests
Not-tested: End-to-end launch against a locally installed Kaku.app
Related: #1954
2026-04-10 22:35:16 +08:00
Dex Miller 8c32610a7d Align Thinking fallback with main-model-only Claude mappings (#1984)
The Claude provider form reopened with an empty Thinking model after users saved only a main model. This updates model-state hydration to mirror the existing Haiku-style fallback semantics: read ANTHROPIC_REASONING_MODEL when present, otherwise display ANTHROPIC_MODEL, without writing a synthetic reasoning field back into config.

Constraint: Existing Haiku, Sonnet, and Opus selectors already rely on read-time fallback behavior
Rejected: Persist ANTHROPIC_REASONING_MODEL from ANTHROPIC_MODEL automatically | would diverge from Haiku behavior and silently rewrite saved config
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep Thinking fallback read-only unless all model-mapping fields are intentionally migrated to write-through semantics
Tested: pnpm typecheck
Tested: pnpm test:unit (1 unrelated pre-existing failure in tests/components/UnifiedSkillsPanel.test.tsx mock setup)
Not-tested: Manual add-provider reopen flow in the desktop UI
2026-04-10 22:34:18 +08:00
Dex Miller afdda27bd5 Restore auth tab localization in settings (#1985)
The settings page already routes the auth tab label through the shared i18n key, but the locale bundles never defined that key. Adding the missing entries fixes the label with the same simple pattern used by the other tabs.

Constraint: Keep the fix aligned with the existing settings-tab i18n flow
Rejected: Add component-level bilingual rendering | unnecessary for a missing translation key
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: When adding settings tabs, define locale keys in every bundled language before relying on fallback text
Tested: pnpm format:check; pnpm typecheck
Not-tested: Manual verification in the desktop UI
2026-04-10 22:24:25 +08:00
Max 7f1963ab49 feat(provider): add TheRouter presets for Claude, Codex, and Gemini (#1891)
* feat(provider): add TheRouter presets for Claude and Codex

* feat(provider): add TheRouter Gemini preset

---------

Co-authored-by: max <me19@qq.com>
2026-04-10 21:48:14 +08:00
Max 68df60bd4a feat(provider): add TheRouter presets for OpenCode and OpenClaw (#1892)
Co-authored-by: max <me19@qq.com>
2026-04-10 21:47:18 +08:00
Jason f9fb9085ac feat(session-manager): highlight search keywords in session titles and messages 2026-04-10 08:30:37 +08:00
Jason 308314baac fix(session-manager): improve session search accuracy and Chinese support
- Pre-filter sessions by provider before indexing to prevent result
  truncation when FlexSearch limit cuts across providers
- Switch tokenizer from "forward" to "full" for Chinese substring matching
- Preserve FlexSearch relevance ranking when search query is present
2026-04-10 00:14:50 +08:00
Jason a5a5207f74 style: fix prettier formatting in ProviderCard and ProviderForm 2026-04-09 21:13:32 +08:00
Jason fa4297f4d3 feat(common-config): show first-run notice dialog when editing providers
Display a one-time informational dialog explaining the Common Config
Snippet feature when users first open the add/edit provider form.
Uses a derived isOpen state from settings to avoid race conditions.
Adds commonConfigConfirmed flag to both TS and Rust settings types.
2026-04-09 16:49:14 +08:00
Jason ecb1fed5db feat(common-config): add guide info and empty state to common config editor
Add an informational alert block at the top of the common config snippet
editor modal (Claude/Codex/Gemini) explaining what the feature is, why
it exists, and how to use it. Also add an empty state prompt when no
snippet has been extracted yet, guiding users to click "Extract from
Editor". Includes i18n support for zh/en/ja.
2026-04-09 16:49:14 +08:00
Jason 521571c4ca fix(usage): only show CLI subscription quota for active provider
CLI-credential-based subscriptions (Claude/Codex/Gemini) read from a
single global credential file, so the quota always reflects the last
CLI login rather than a specific provider. Showing it on non-current
cards is misleading when multiple official subscriptions exist.

Apply the same isCurrent + autoQuery pattern already used by Copilot
and Codex OAuth: only query and render the quota footer when the
provider is the currently active one.
2026-04-09 16:49:14 +08:00
Jason bafe9e820d fix(notifications): remove duplicate toast when switching to proxy providers
When switching to Copilot/ChatGPT/OpenAI-format providers with the proxy
not running, two toasts appeared: a "proxy required" warning followed by
a "switch success" toast. Unify the post-switch toast logic so that all
provider types show a single success toast, and skip it entirely when
a proxy-required warning was already shown.
2026-04-09 16:49:14 +08:00
Jason 8669879ad0 feat(welcome): show first-run welcome dialog on fresh install
Introduce a one-time welcome dialog that explains CC Switch's workflow
to new users: how their existing config is preserved as a "default"
provider and how the bundled "Official" preset enables one-click revert.
Upgrade users are excluded by checking is_providers_empty() at startup
and never see the dialog.

Persistence follows the existing *_confirmed convention in AppSettings
(proxy/usage/stream_check/failover), stored in settings.json. The field
is only written when the user explicitly clicks the confirm button,
keeping its semantics strictly about user acknowledgement.

Also adds two reusable DAO helpers:
- Database::is_providers_empty for fresh-install detection, using
  EXISTS(SELECT 1) for a short-circuit query.
- Database::get_bool_flag accepting "true" | "1", with
  init_default_official_providers migrated to use it.

Dialog copy in zh/en/ja uses conditional phrasing so it stays
accurate whether or not existing live config was found.
2026-04-09 16:49:14 +08:00
Jason df5cb6d771 fix(usage): only auto-poll Copilot/ChatGPT quota for current provider
CopilotQuotaFooter and CodexOauthQuotaFooter called their hooks with a
hardcoded `enabled: true` plus an unconditional 5-minute refetch and
refetchOnWindowFocus, so non-current reverse-proxy cards kept polling
in violation of the project's "only the active provider auto-queries
on cooldown" rule. With multiple Copilot or ChatGPT accounts bound to
different cards, every card kept hitting its own usage endpoint.

Adopt the same pattern as useUsageQuery: keep `enabled` independent of
isCurrent so first-fetch and manual refresh still work, but gate
refetchInterval / refetchIntervalInBackground / refetchOnWindowFocus on
a new `autoQuery` option, and thread `isCurrent` from ProviderCard
through the footers into the hooks.
2026-04-09 16:49:14 +08:00
Jason c02b8c58cb feat(stream-check): support OpenCode via npm package mapping
Phase 3: implement stream check for OpenCode providers by mapping the
`settings_config.npm` (AI SDK package name) to the corresponding API
protocol and delegating to the existing stream checkers.

Package mapping:
- @ai-sdk/openai-compatible → openai_chat
- @ai-sdk/openai            → openai_responses
- @ai-sdk/anthropic         → anthropic (ClaudeAuth strategy)
- @ai-sdk/google            → gemini (Google strategy)
- @ai-sdk/amazon-bedrock    → not supported (phase 4 message polish)

Note: OpenCode nests baseURL/apiKey under `settings_config.options`
(different from OpenClaw's root-level fields) and uses `baseURL` with
a capital L. Three new extractors (base_url / api_key / npm) encode
these shape differences so check_opencode_stream stays symmetric with
check_openclaw_stream.

Frontend: drop the remaining `appId !== "opencode"` filter in
ProviderList.tsx — both apps can now test providers.
2026-04-09 16:49:14 +08:00
Jason 516fcdf6bf feat(stream-check): support OpenClaw openai-completions protocol
Phase 1 of extending stream health check to OpenCode/OpenClaw apps.

- Add early-dispatch path for OpenCode/OpenClaw in check_once so they
  bypass the adapter layer (which only knows Claude/Codex/Gemini
  settings_config shapes).
- Introduce check_openclaw_stream dispatcher that reads the `api` field
  from settings_config and routes to the existing check_claude_stream
  with api_format="openai_chat" for "openai-completions". Other
  protocols return localized errors to be lit up in phases 2 and 4.
- Extract build_stream_check_result helper to avoid duplicating the
  StreamCheckResult construction logic between the two code paths.
- Unblock the test button for OpenClaw providers in ProviderList.tsx.

OpenCode still returns the "not yet supported" error; it will be
enabled in phase 3.
2026-04-09 16:49:13 +08:00
Jason 34c6696baa fix(providers): disable test/usage buttons for Copilot and Codex OAuth cards
These OAuth providers ship with non-empty ANTHROPIC_BASE_URL, so the
isOfficialProvider() heuristic (which checks for a missing base URL)
returned false and left the health-check and usage-config buttons
enabled — inconsistent with other official OAuth cards. Extend the
button disabling logic at the call site with isCopilot / isCodexOauth,
matching the pattern already used for the quota footer branch above.
2026-04-09 16:49:13 +08:00
Jason 641c2fc2c7 chore(presets): bump Codex OAuth preset to GPT-5.4 family
Update the "Codex (ChatGPT Plus/Pro)" entry in Claude Code presets to
the new GPT-5.4 naming, which drops the legacy `-codex` suffix. Map the
Haiku tier to `gpt-5.4-mini` for lower-cost lightweight calls while
keeping Sonnet/Opus on the standard `gpt-5.4`.
2026-04-09 16:49:13 +08:00
Jason 64c068415e fix(linux): repair unresponsive UI on startup and full-screen panels
Linux users reported the window UI (including native title bar buttons)
couldn't receive clicks until manually maximizing and restoring the
window. Root causes: (1) Tauri webview did not acquire focus on startup
so first clicks were consumed by X11/Wayland click-to-activate
(Tauri #10746, wry #637); (2) GTK surface input region failed to
renegotiate on the visible:false + show() path under some
WebKitGTK/compositor combinations.

- Add linux_fix::nudge_main_window helper that performs set_focus plus
  a ±1px no-op resize after window show, with a 500ms reconciliation
  readback to compensate for dropped resize requests on slow
  compositors.
- Wire the helper into every window re-show path: normal startup,
  deeplink, single_instance, tray show_main, and lightweight exit.
- Set WEBKIT_DISABLE_COMPOSITING_MODE=1 at startup to avoid resize
  crashes and Wayland surface negotiation issues.
- Remove data-tauri-drag-region on Linux from App.tsx header and the
  shared FullScreenPanel (used by all provider/MCP/workspace forms)
  to avoid Tauri #13440 in Wayland sessions. Extract drag-region
  constants to src/lib/platform.ts for reuse.

All Rust changes are gated by #[cfg(target_os = "linux")]; frontend
changes preserve macOS/Windows behavior via runtime isLinux() checks.
Known limitation: tiling Wayland compositors ignore set_size, so
GDK_BACKEND=x11 remains the user-side workaround.
2026-04-09 16:49:13 +08:00