Commit Graph

191 Commits

Author SHA1 Message Date
Xu Liang b7e096ae37 feat(desktop-client): suppress native menu bar on Windows/Linux and support phone number login
- Add suppressNativeWindowMenu() to hide native menu bar on all windows (Win/Linux only)
- Set autoHideMenuBar: true on BrowserWindow creation as belt-and-suspenders
- Import Menu from electron/main to support menu suppression
- Login form: rename field from email to identifier, support phone number input
- Migrate saved credential key from geo_rankly_saved_email to geo_rankly_saved_identifier with fallback
- Update login error message: "邮箱或密码错误" → "账号或密码错误"
- Remove unset ELECTRON_RUN_AS_NODE from dev script (not needed on Windows)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 18:02:00 +08:00
root 54fc313e10 feat(desktop-client): tighten window mode switching and add win/linux package scripts
Frontend CI / Frontend (push) Successful in 2m18s
- Track each window's renderer mode in a WeakMap (and fall back to
  parsing ?window= from the URL) so retireInactiveAppWindows can
  destroy stale login/main/settings windows when we switch modes,
  not just the previous one of the canonical pair.
- Replace closeWindowAfterIpcReturn with retireWindowAfterIpcReturn
  (hides immediately, then destroys instead of close()) so the IPC
  reply still reaches the caller before teardown.
- Add waitForWindowReady() with a 1.2s ceiling and call it from
  revealWindow so we don't show a half-loaded shell while
  switchWindowMode is running. Pass the source window from the IPC
  event into switchWindowMode so the closing target is the actual
  caller, not a guess based on mode.
- Silence console.* and skip the observed-fetch install in packaged
  runtime, and only auto-openDevTools in dev.
- Eagerly import LoginView in App.vue instead of defineAsyncComponent
  to remove the boot flash on the login window.
- package.json: prefix package:* with `pnpm run build` and add
  package:win and package:linux for parity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 17:20:18 +08:00
root 8b4697d605 chore(desktop-client): gate dev-only debug paths in packaged builds
Wrap the network observer, renderer-devtools proxy (both the main-side
ipc handler registration and the preload-side response listener), and
the auto-open-devtools triggers behind !app.isPackaged so packaged
builds don't ship debugging side channels. Add a tiny console-guard
imported first in renderer/main.ts that no-ops console.* in
import.meta.env.PROD so a packaged build stays quiet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 17:20:00 +08:00
root fb1351d82b feat(desktop-client): add afterPack hook to clean up unnecessary localization files and include certificate files for code signing
Deployment Config CI / Deployment Config (push) Successful in 23s
Frontend CI / Frontend (push) Successful in 2m25s
2026-05-01 16:32:12 +08:00
root a9af6c634c chore(desktop-client): add macOS code-signing and notarization scaffolding
Backend CI / Backend (push) Successful in 13m6s
Deployment Config CI / Deployment Config (push) Successful in 17s
Frontend CI / Frontend (push) Successful in 2m24s
- Move the build config out of package.json into electron-builder.yml so
  the mac block can carry hardenedRuntime, entitlements, universal arch,
  and usage-description Info.plist keys. Drop identity:null (which
  forces unsigned builds) and let CSC_NAME or the keychain decide.
- Add entitlements plist files and three helper scripts: sign-setup.sh
  (one-time keychain prep with notarytool store-credentials),
  sign-mac.sh (sign + notarize + staple, supports keychain or CI env
  vars), and ci-import-cert.sh (CI keychain bootstrap).
- Ignore .env.signing so local credentials don't sneak into the repo,
  and pin @emnapi/{core,runtime} as devDependencies so universal builds
  resolve them deterministically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:02:14 +08:00
root c89683862e feat(workspace): replace supported platform count with bound media account count
The "platforms" stat tile was a constant (5) that never reflected what the
tenant actually had set up. Drop SupportedPlatformCount from the workspace
overview API/domain/shared-types and have admin-web compute a bound media
account count from the desktop accounts list, filtered through a new
isMediaPublishAccount helper that excludes deleted rows and AI-only
platforms (yuanbao/kimi/wenxin/deepseek/doubao/qwen).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:01:40 +08:00
root ce2d8a2907 fix(deploy): prevent duplicated api prefix
Deployment Config CI / Deployment Config (push) Successful in 8s
Frontend CI / Frontend (push) Successful in 2m17s
2026-05-01 14:11:21 +08:00
root cb4c4eece9 feat(config): update rabbitmq and membership settings, add redis and cache configurations
Deployment Config CI / Deployment Config (push) Successful in 12s
Frontend CI / Frontend (push) Has been cancelled
2026-05-01 13:40:45 +08:00
root 035d0d6da8 fix(admin-web): avoid circular editor chunks
Frontend CI / Frontend (push) Has been cancelled
2026-05-01 13:38:11 +08:00
root 9727077dfa refactor(admin-web): render citation pie as interactive SVG with hover tooltip
Backend CI / Backend (push) Has been cancelled
Frontend CI / Frontend (push) Has been cancelled
Deployment Config CI / Deployment Config (push) Successful in 21s
Replace the conic-gradient circle with an SVG path-per-slice chart so
slices can lift on hover, dim siblings, and surface a follow-cursor
tooltip with the platform name and share. Falls back to the original
gradient circle when there are no segments to render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:46:35 +08:00
root b345ee26e4 feat(monitoring): match citations by canonical link and scope summary by brand/keyword/platform/date
Replace the domain+title fuzzy alias scoring with a canonical
candidate-key index built from published-link aliases — only exact
matches now resolve to a SaaS source, eliminating false positives
across articles sharing a host (e.g. m.163.com vs www.163.com).

Surface the summary scoping that already existed on loadCitationRanking
/ loadCitedArticles by accepting brand_id, keyword_id, ai_platform_id
and business_date on CitationSummary, plumbing them through the handler
and admin tracking view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:46:17 +08:00
root 2436f50c1f refactor(admin-web): tighten schedule cover picker layout
Replace the labeled cover button + meta column with a compact preview
tile and an overlaid remove button, dropping the now-unused cover-meta
typography rules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:04:28 +08:00
root b18a562b32 fix(admin-web): keep app header above content stacking contexts
Promote the header to z-index 100 and create an isolated stacking
context on the main layout so sticky/positioned content (drawers,
overlays) no longer sits above the topbar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:04:20 +08:00
root 5a613abc33 chore: rebrand product as 省心推 and lock UI to zh-CN
Update server titles, web/desktop window/tray/document titles, login
copy, and electron build metadata to use the Chinese product name 省心推.
Drop the runtime locale switcher and en-US import — the app now ships
zh-CN only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:03:55 +08:00
root 490c6c759d feat(article): expose auto_publish_platforms derived from publish accounts
Replace the freeform `platforms` field on articles/tasks (parsed out of
wizard_state/input_params JSON) with `auto_publish_platforms`, resolved
by joining schedule publish accounts to platform_accounts. The migration
strips the legacy keys from existing rows so the new field is the single
source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:02:21 +08:00
root 0f4310c345 fix(admin-web): prevent source link overflow in article drawer
Frontend CI / Frontend (push) Has been cancelled
Backend CI / Backend (push) Has been cancelled
将来源链接外层改成 grid wrapper 并固定图标 flex-shrink,
避免长 URL 把图标挤出布局或超过描述列宽。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 10:22:05 +08:00
root 764abf32ae refactor(desktop-client): tidy PublishManagementView copy and errors
将队列提示与英文 eyebrow 全部本地化为中文,并改为通过
unwrapClientErrorMessage + normalizeTaskErrorMessage 统一兜底,
避免不同操作回退到原始 Error.message 时漏掉发布器归一化结果。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 10:22:03 +08:00
root 7b6ae0f6d0 feat(desktop-client): expand publisher error normalization
新增登录态失效与微信公众号外链缺失两类发布错误归一化,
将原本散落在适配器里的中文提示集中到 shared/publisher-errors,
让渲染层与 main 进程共用同一份用户可读文案。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 10:21:54 +08:00
root 3436d9536d feat(publish): require cover image for WeChat Official Account
WeChat 公众号在管理员/运营者授权发布场景下需要 2.35:1 封面,
新增共享 publishPlatformRequiresCover 校验并同步前端封面要求与文案,
避免在调度与即时发布两条路径上重复维护平台名单。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 10:21:46 +08:00
root 052299e54f fix(frontend): serve precompressed web assets
Deployment Config CI / Deployment Config (push) Successful in 10s
Frontend CI / Frontend (push) Has been cancelled
2026-05-01 01:12:13 +08:00
root 873c0382e4 fix(deploy): build ops web with shared tsconfig
Deployment Config CI / Deployment Config (push) Successful in 10s
Frontend CI / Frontend (push) Successful in 7m22s
2026-05-01 00:53:35 +08:00
root 461f186337 feat(tenant-api): expose Swagger UI in non-release mode
Adds an in-process Swagger UI and OpenAPI JSON under /swagger,
registered automatically when the Gin mode is not release. The
admin-web Vite dev server and production nginx config now proxy
/swagger to the tenant API so the docs are reachable from the
existing admin host.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:19:31 +08:00
root baff51295a feat(desktop-client): add settings window and switch default API to production
- Open a dedicated settings BrowserWindow with general / login / diagnostics / about tabs
- Persist openAtLogin and keepRunningInBackground via desktop-settings.json; hide main window instead of quitting when background mode is on
- Default API base URL now points to api.shengxintui.com; LoginView and SettingsView share a single constant
- Restore wangyihao session cookies on probe and silent refresh, and accept the legacy http://mp.163.com console origin
- Drop the standalone diagnostics view in favor of the settings tab

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 22:00:01 +08:00
root 40ef60eae2 style(desktop-client): tighten media/AI/publish view copy and table chrome
Rename the media-platforms nav entry to "媒体账号", switch the Accounts hero eyebrow to MEDIA ACCOUNTS, drop the redundant AI platforms intro paragraph, and trim the publish list to use a generic external-link tooltip plus a slightly wider table to fit the renamed columns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 21:06:12 +08:00
root ed38721feb feat(desktop-client): give login and main views their own BrowserWindow
Spawn a separate window per mode keyed by a ?window= query param so each surface keeps its size, devtools, and renderer state independently. Logout now persists the cleared session and switches windows before awaiting the runtime release, avoiding a stale main-window flash on the way out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 20:56:03 +08:00
root cdd63db7f1 feat(wangyihao): record public article URL on publish and back-fill responses
Build https://www.163.com/dy/article/<docId>.html when wangyihao tasks finish, and normalize publish-record responses + monitoring alias inputs to surface the same public URL when the doc id is known.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 20:55:53 +08:00
root add918ff4c feat(desktop-client): clear auth failure on active probe and trigger probe when opening console
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 19:32:20 +08:00
root 02b8cd683d feat(account-health): allow silent refresh on scheduled probes and stop treating wangyihao token misses as auth failure
Scheduled probes and reportAccountFailure now request a silent refresh
before marking an account expired, and a missing wangyihao publish
token is classified as ok instead of auth_failure so the cookie-based
refresh path can recover the session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 19:18:09 +08:00
root b89892be6e feat(desktop-client/wangyihao): persist session cookies and recover bound partitions
Snapshot 163.com/126.net cookies on bind/refresh to a userData vault and
restore them when reattaching a session, recover orphan partitions by
matching the detected mediaInfo, verify the bind window via the console
state instead of a generic profile probe, and route token/draft fetches
through the https API origin while opening console pages over http to
match the live editor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 19:18:03 +08:00
root c87842347e feat(account-health): reconcile remote bind state and ignore stale runtime reports
Track an authRevision per local record so a fresh bind cancels in-flight
probes, sync server-confirmed health back into the desktop cache via
reconcileTrackedAccountRemoteState, include verified_at in the upsert
signature, and drop runtime health reports older than the stored
verified_at on the server side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 18:15:39 +08:00
root bf25fa1a80 feat(desktop-client/wangyihao): harden token capture and draft submit flow
Inject the YiDun guardian client to obtain ursToken without relying on
window.neg, fetch the publish detail (wemediaId/onlineState/...) before
saving drafts so 100002 "参数错误" stops blocking submit, downgrade
markdown tables to paragraph rows like Baijiahao, and surface friendlier
errors for token, draft and publish-click failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 18:15:20 +08:00
root 9525c1144d feat(admin-web/login): replace default test credentials with remembered login
Why: showing the seed admin account on the login screen is unsafe for production deployments.
How to apply: store identifier/password in localStorage when "记住密码" is checked, restore them on mount, and drop the demo credentials panel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 18:15:08 +08:00
root 3498f558e3 feat(desktop-client/home): break down runtime issues by tasks and accounts
Split the home dashboard runtime panel into a combined issue feed that
lists failed/unknown tasks alongside blocked accounts, with per-kind
counts and per-row detail/badges so operators can triage both classes
of breakage from one place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 17:12:25 +08:00
root e5f6e93cea feat(desktop-client/yuanbao): capture richer DOM citation sources
Raise the response capture body limit, parse siteName and source
indexes from yuanbao DOM links, merge ordered DOM sources with the
streamed list, and reject runtime/build-diagnostic strings from being
treated as a question.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 17:12:18 +08:00
root fb166fa9fe feat(monitoring): attribute SaaS citations by registrable domain
Persist host/registrable_domain/site_key on monitoring article URL
aliases, populate them on desktop-publish completion, and use them in
the citation pipeline to count how often SaaS-published content
appears as a citation source. Expose a /citation-summary endpoint with
a 7/30-day window switch and surface the new source-share metrics in
the tracking dashboard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 17:12:09 +08:00
root 6e6e19cccb feat(schedule-tasks): support auto-publish via media accounts
Replace the legacy target_platform string on schedule tasks with a
workspace-aware auto-publish payload (auto_publish + publish_account_ids
JSONB + cover_asset_url + cover_image_asset_id) and migrate the schema,
sqlc queries, generated models, domain struct, ScheduleTaskService DTOs,
and dispatch worker to round-trip the new fields. PromptRuleGeneration
gains a WithPublishJobService hook so executeGeneration can enqueue an
auto-publish job once the article is ready, and worker-generate wires
the publish-job service in. On the admin-web side, extract
PublishArticleModal's account-card builders into a shared
publish-account-cards module, rebuild GenerateTaskDrawer's schedule
mode around account selection plus a CoverPickerModal, and surface the
new "auto publish" column on ScheduleTaskTab. Shared types and i18n
strings cover the new fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:32:19 +08:00
root c9267d2fae feat(auth): block disabled user accounts at the subscription guard
RequireActiveTenantSubscription now optionally takes an AuthRepository
and rejects the request with 40311/user_disabled when the actor's user
row is anything other than active. Wire the repo through the tenant
router, surface the new error code in admin-web's
membershipBlockedErrors set, persist a "disabled" membership status when
markStoredMembershipBlocked sees user_disabled, and add localized copy
on the blocked screen and shell membership banner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:31:26 +08:00
root e15806bd91 feat(template-wizard): require library brand before favoriting competitors
Drop the implicit brand-creation path in handleFavoriteCompetitor and
just refuse to save a competitor until the user has explicitly chosen a
library brand; clear saved state on competitor drafts when the brand is
unset. Also enforce the configured custom-outline label length cap and
auto-focus newly added outline nodes for faster keyboard editing. New
i18n strings cover the brand-required and outline-length messages plus
the updated competitor hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:30:29 +08:00
root 814ea52c0e feat(desktop-client): validate qiehao session via detected account
The 企鹅号 console returns 200 even when the session is silently expired,
so probe/silent-refresh/console-open now require detect() to return an
account whose identity matches the bound one. Surface a localized
"授权已过期" prompt in the renderer when the new
desktop_account_session_expired error bubbles up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:28:41 +08:00
root 5216a4847c feat(login-ui): dedupe submissions and surface guard error codes
Collapse rapid double-submits into a single in-flight request across
admin-web, ops-web and the desktop client so users cannot fire parallel
login attempts and trip the new in-progress guard. Map the new
login_rate_limited / login_locked / login_in_progress / login_guard_unavailable
error codes to localized messages in every login surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:07:32 +08:00
root 0729b76cc7 style(ops-web): unify table actions as circular icon buttons
Replace the mixed text-link / button action lists in Accounts,
AdminUsers, and SiteDomainMappings with a shared .table-actions-row +
.action-* class set in styles.css. Each row now renders compact icon
buttons (edit/delete/approve/revoke/key/kol/reset/play) tinted by intent,
matching the new KolSubscriptions console look.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:07:55 +08:00
root 73fd7db21a feat(ops-web/kol): add KOL subscription review console
New /kol-subscriptions page lists pending and active subscriptions, lets
operators approve / revoke / manually bind packages to tenants, and links
into the AppShell nav under 订阅包审批. Register Alert globally so the
view's status banners render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:07:41 +08:00
root 4dd8a1bb0e feat(tenant/kol): let KOLs self-subscribe to their own published packages
Add /kol/manage/packages/:id/self-subscription POST/DELETE on the tenant
side so a KOL can grant themselves access to their own published package
without going through the marketplace approval flow. Reject self-subscribe
through the marketplace with a clear error, expose owned_by_current_tenant
on package responses, and surface a self_subscription block on KolPackage
Response. The admin-web KOL workspace gets 订阅自己 / 取消订阅 entries with
matching messaging in the marketplace detail view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:06:57 +08:00
root 6a56935fb9 chore(admin-web/login): drop redundant tenant admin login subheading
The 欢迎回来 headline already conveys context; the secondary
"Tenant Admin Login" line added noise on the simplified login card.
Remove the paragraph and the now-unused i18n key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 21:02:52 +08:00
root 2f3055babd feat(admin-web/articles): show partial-success publish status from records
ArticlePublishStatus now derives partial_success when both succeeded and
failed records are present, fetches publish_records eagerly with a 30s
stale window so the popover lights up without a click, and ArticleDetail
Drawer reuses the component instead of its own tag. Relabel partial_
success copy to 部分发布 / Partially Published to match the new bucket.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 21:02:36 +08:00
root 99a992f34b feat(ui/publish-records): surface 已存草稿 status and client-only manage hint
Render publish_type=draft tasks with the warn tone and 已存草稿 label, swap
the management link copy to 打开草稿箱 for weixin_gzh, and expand the task
summary to mention the publish_authorization_required reason. In the admin
drawer, stop falling back to external_manage_url for missing public links
and instead show a hint pointing operators back to the desktop client for
weixin_gzh / zol records.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 19:57:11 +08:00
root a09d44d8f0 feat(desktop/weixin-gzh): drive editor to publish with draft fallback on verify
Replace the masssend HTTP path with a Playwright-driven editor flow that
clicks "群发" and resolves the publish response, then polls appmsgpublish
for the public mp.weixin.qq.com/s URL. When the dialog asks for admin
verification / scan-code authorization, swallow it as a successful draft
save and surface fallback_reason=publish_authorization_required so the
operator can finish from the console. Also broaden token extraction in
account-binder and treat the draft list URL (cgi-bin/appmsg) as the
console landing page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 19:56:56 +08:00
root ed6b2c09fa fix(desktop/wangyihao): soften creative statement to a personal-opinion note
"内容由AI生成" tripped Wangyihao's AIGC review and parked drafts under
manual screening. Switch to "个人观点,仅供参考" so legitimate posts move
through normal moderation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 19:56:40 +08:00
root 2e5643ddfd feat(desktop/weixin-gzh): mass-send drafts and resolve public article URL
Drafts previously stopped at the草稿箱 step, leaving the operator to publish
manually. Chain a /cgi-bin/masssend call, then poll the appmsgpublish list
(by title and by latest) to extract the mp.weixin.qq.com/s public link
and a publish_id. Surface dedicated failures for publish and link-missing
states, and switch the result payload to publish_type="publish" with the
appmsgpublish manage URL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 17:12:49 +08:00
root 2501f9db85 feat(desktop/renderer): surface re-authorize affordance for expired accounts
Replace the generic 重新校验 action with state-aware copy and an arrow
icon when authState is expired, and route the click through the bind flow
so users land directly on the platform login page. AiPlatformsView gains
the same shortcut and an inline alert explaining tasks are paused until
re-auth completes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 17:12:20 +08:00