* feat: support oidc and ldap auth
* feat: support oidc and ldap auth
* fix: remove heima partner, clean up auth docs, add web-studio unsupported auth banner
- Remove heima from partner list in README (en/zh/ja)
- Remove unsupported env var references (OPENVIKING_AUTH_MODE, OPENVIKING_USERNAME,
OPENVIKING_PASSWORD) from LDAP auth docs
- Remove temporary switch bash snippets from auth docs
- Fix ldap_password description
- Add web-studio unsupported-auth-mode banner for oidc/ldap servers
* fix: address OIDC/LDAP review comments on auth plugin design
Key changes driven by PR review:
- **Role mapping**: OIDC and LDAP external identities always resolve to
USER role. Removed map_role() calls and group_membership-based role
mapping. Admin access is gated by the root API key mechanism only.
- **LDAP credential extraction**: Removed query-parameter-based username/
password extraction (security concern — passwords in URLs can leak via
shell history, proxy logs, and monitoring). Clients must use Basic Auth
header or form data.
- **OIDC identifier sanitization**: Auth0 and other providers may include
characters like "|" in the `sub` claim. These are now replaced with "_"
to produce valid OpenViking user identifiers.
- **Dead code removal**: Removed _extract_groups, memberof_attribute,
require_root_api_key_for_admin, _initialize_api_key_manager, and
get_request_context_checks from both plugins since they are no longer
needed.
- **Docs**: Removed query-parameter curl example, memberof_attribute and
require_root_api_key_for_admin config references.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* feat: support oidc and ldap auth
* feat: support oidc and ldap auth
* fix(auth): bind lazy OIDC imports at module scope
---------
Co-authored-by: TRAE CLI <noreply@bytedance.com>
Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>
* fix(feishu): surface permission errors clearly and keep users on page
Map Feishu/Lark API failures to typed OpenViking errors with actionable hints, and keep Web Studio from treating HTTP 403 permission denials as session logout.
* fix(feishu): simplify API error mapping
* refactor(feishu): inline API error mapping
---------
Co-authored-by: wugj <wugj@g-bits.com>
Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>
In trusted mode, /health identity resolution requires X-OpenViking-Account
and X-OpenViking-User headers alongside X-API-Key. Without them, the
trusted auth plugin raises InvalidArgumentError, the exception is swallowed
by the health handler, and the response omits role/account_id/user_id —
so the studio falls back to connectionRole='unknown' and gates the admin
UI behind 'Usage/Audit 未初始化'.
Sending the headers is always safe: in api_key mode the server strips
them from the request scope; in trusted mode they are required.
Closes#2977 for the web-studio side.
* fix(web-studio): lazy-create playground sessions to prevent orphans
The AgentPanel used to call POST /sessions on every page load (via a
useEffect gated on botHealth.isSuccess), creating empty UUID sessions
even when the user never sent a message. These orphan sessions cluttered
the session list.
Fix: generate a client-side UUID as the initial sessionId instead of an
empty string. The session is lazily created on the backend by the first
addMessage call (POST /sessions/{id}/messages uses auto_create=True).
- Remove startSession() and its auto-create useEffect
- Always have a sessionId (createRandomUuid fallback)
- Simplify useChat params (persistMessages always true, no preview fallback)
- Remove dead isCreating UI branch
- Call onSessionChange on mount so the URL stays in sync
- Error retry button now uses handleNewSession()
Closes: orphan empty sessions created on playground page load
* feat(sessions): sort session list by filesystem modTime
- Backend: add mod_time to session list API response (from viking_fs.ls stat)
- Frontend: useSessionListByRecency sorts by mod_time instead of N detail fetches
- Frontend: sidebar and playground history use recency-sorted list
- Frontend: sortTreeEntries sorts directories by modTime descending
Reduces session list load from 1+N requests to 1 request.
When the Root API Key field holds an account-admin key (rather than a
root key), the studio kept using the previously selected / default
assumed account. An account-admin key is scoped to its own account, so
admin and data calls against any other account were rejected by the
server with "ADMIN can only manage account: <x>".
/health already resolves the presented key and echoes back its identity
(role + account_id + user_id). Read account_id alongside role and, when
the key resolves as an admin key, pin the assumed account to the admin
key's own account so admin/data calls target the right tenant. Root keys
are not account-scoped, so their account selection is left untouched.
Also relabel the field "Root API Key" -> "Root or Admin API Key" (en + zh)
to reflect that an account-admin key is accepted there too.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the connection setup with a clean three-field model plus a
dedicated User Management panel.
- Connection card is now exactly Server URL, Root API Key, User API Key.
The Root key is the sole control-plane credential (unlocks User
Management); the User key powers Playground / tenant data APIs and is
set via "Use as User API Key". Removes the duplicated account/user
dropdowns and the dual-purpose API-key field.
- Admin access is gated on the Root key only, never the User key.
- Stop normalizing the base URL on every keystroke (it collapsed
"http://" back to "http:"); normalize at request time instead.
- Disable autocapitalize/autocorrect/spellcheck on URL/key/id inputs
(the browser was capitalizing "http" -> "Http").
- Debounce field edits and key live behaviour off the committed
connection, so typing no longer re-probes / refetches per keystroke.
- Guard the account-filter effect against a render loop and add
keepPreviousData to the probe so adopting/rotating a key no longer
blanks the panel.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web-studio): move playground actions into mobile drawer
* fix(web-studio): make mobile playground actions fullscreen
* docs: add web studio mobile screenshots
The push-OTP feature (mint an OTP in Studio to hand to an MCP client) was never
wired to a consumer: consume_otp had zero production callers and no endpoint or
grant ever redeemed an OTP. The 'full happy path' test actually exercised the
display_code flow, not OTP. So the sidebar footer's 'OAuth setup' entry minted a
code with nowhere to use it — dead, confusing UX.
Remove it end-to-end and repurpose the footer slot into an entry for the
cross-device verify page (enter the 6-char display_code), which previously had no
discoverable entry point in Studio.
Frontend:
- delete oauth-setup-dialog.tsx + /oauth/setup route (+ routeTree, i18n)
- extract CrossDeviceVerifyForm from verify.tsx; add CrossDeviceVerifyDialog
- footer 'OAuth verify' entry opens the verify dialog (desktop) / page (mobile)
Backend:
- drop issue_otp route + OTPRequest/OTPResponse, storage insert_otp/consume_otp,
oauth_config.otp_ttl_seconds, and the OTP-specific tests
- keep otp.py generate_otp (cross-device display_code) + hash_secret, the shared
_atomic_consume_code, and the oauth_codes.kind column
- convert the race/expiry/revoke/GC storage tests to auth-code rows
Docs: update 11-oauth, 06-mcp-integration, and the design doc to reflect removal.
Iterate the OAuth consent/verify pages to match the Studio Connection &
Identity UX: when the caller is root/admin in api_key mode, let them pick a
concrete account + user from dropdowns instead of pasting a raw API key. The
selected user's api_key becomes the Bearer that binds the OAuth grant.
This is required for root, not just nicer UX: the backend oauth-verify
rejects identities without a per-user key fingerprint, so root cannot
authorize as itself and must select a concrete user.
- IdentityPicker: add 'select' mode + optional directory prop (presentational)
- useIdentityDirectory: gate on api_key + root/admin, list accounts/users,
detect root vs account-admin via GET /accounts 403, resolve user keys
- consent.tsx / verify.tsx: default-mode effect (root -> select), keep select
payload in sync, soften the sign-in prompt
- Extract shared admin data layer (#/lib/admin) + AccountSelect/UserSelect
(#/components/identity-select) so settings and consent share one source
- i18n: add oauth.identityPicker.select* keys (en + zh-CN)
No backend changes.
* feat: save images in Markdown to vikingfs
* feat: save images in doc to vikingfs
* fix: change the image file reading operation to asynchronous.
* feat: support generating L0 and L1 from images in Markdown and importing them into vector indexes
* fix: add updating image links when modifying existing files; restrict accessible paths for image loading; revise the unit tests raised for images
* fix: 限制保存的图片所在目录,避免将其他目录下的内容加入 vikingfs;对markdown代码中的图片链接保持不变
---------
Co-authored-by: zhanghaoyu.la <zhanghaoyu.la@bytedance.com>
Co-authored-by: Qin Haojie <qinhaojie.exe@bytedance.com>
* fix: stabilize studio identity and streaming chat
* fix: hide unsupported studio terminal commands
* fix: remove unsupported terminal command copy
* fix: run selected terminal suggestion on enter
* fix: group supported terminal commands
* fix: add terminal quick start and history
* fix: scope session visibility by user
* fix: harden bot user scoping
* fix: forward request scoped bot identity
* fix: add terminal quick start translations
* fix: add terminal command group translations
* fix: simplify studio identity scoping
* fix: support api key copy on dev urls
* fix: stop passing agent id to ov http client
* fix: search follow-up memory questions
* Fix Studio admin and data API key handling
* Refine Studio connection identity UI
* Polish Studio header alignment
* Remove duplicate Playground directory header
* Show processing tasks in Playground context tree
* Fix Playground folder focus toggling
* Remove Context Management route
* feat(web-studio): pass create_parent param in resource upload
The backend supports auto-creating parent directories via create_parent,
but the frontend upload form never sent it. Add the parameter (default true)
with a checkbox in advanced options.
* fix(web-studio): improve file save performance and tree collapse behavior
Set wait=false for content write to skip blocking on vector indexing.
Reset expandedKeys on navigation to collapse tree to current directory,
and clear descendant expanded state on folder expand for cleaner UX.
* style(web-studio): enhance language switcher styling and active state indication
* refactor(web-studio): unify resource browser palette state
Refactor the resource browser palette so directory browsing, keyboard handling, and palette mode parsing have a single owner.
Changes:
- make DirBrowser a controlled view owned by FindPalette
- remove DirBrowser internal focusUri/startUri syncing
- remove internalNavRef and document-level keydown listener
- move palette keyboard handling to FindPalette container onKeyDown
- add explicit PaletteMode parsing for idle/search/dirBrowse states
- centralize directory browse query parsing and URI-to-query writing
- lift directory list loading and active cursor state into FindPalette
- add shared useListNavigation hook for active index movement
- debounce directory peek requests by 150ms
- debounce file stat preview requests by 150ms
- extract reusable ItemColumn list component
- export existing useDebouncedValue hook
- add scope reset/back navigation UI copy
This keeps the current browse directory, keyboard dispatch, and palette mode state in one place, avoiding the previous two-way sync loop between FindPalette and DirBrowser.
* refactor(web-studio): flatten DirBrowser detail pane into a switch
Replace the 6-way nested ternary in DirBrowser's right pane with an
explicit DetailView discriminated union computed in one useMemo, rendered
by a flat DetailPane switch (preview/loading/error/subdir/empty/idle).
Merge ItemColumn's onSelect/onSelectFile into a single onSelect(entry,
index); callers branch on entry.isDir. Behavior is unchanged except the
transient peek flash during the 150ms debounce window is gone, since all
directory states are now gated behind cursorItem.isDir.
* fix(web-studio): handle palette pane navigation focus
Use the clicked right-pane entry URI when navigating from the DirBrowser detail pane, instead of reusing the left-pane cursor item.
Clamp list navigation when visible entries change, and restore palette input focus after window focus or visibility changes so keyboard cursor handling remains active after switching apps.
* feat(web-studio): add onOpenFile prop to DirBrowser and FindPalette components
* fix(web-studio): prevent invalid dir scope confirmation and expandedKeys conflict
- Guard Enter in dirBrowse mode with dirListQuery.isSuccess so only
existing directories can be confirmed as search scope
- Remove handleToggle() from handleSelect to eliminate double-write
conflict on expandedKeys when clicking directory rows
- Add dirListQuery.isSuccess to handleKeyDown deps to fix stale closure
preventing scope confirmation in empty directories on cold cache
* Add web studio identity management
* Polish web studio settings header
* Match language toggle and simplify user actions
* Align header controls with site chrome
* Tighten studio chrome spacing
* Shrink header language toggle text
PR #2178 (4e947340) shipped a `shiftBucketToLocal` helper that re-bucketed
the commit heatmap from UTC to viewer-local. PR #2190 (0d63f0e3) landed
right after and added the equivalent shift server-side: backend now
accepts ?timezone= and returns rows already bucketed in the requested tz.
Keeping the client-side shift double-shifts every row by the viewer's tz
offset (e.g. UTC+8 ends up at UTC+16), so today's bucket displays under
the wrong local date.
This was supposed to be reverted on the #2178 branch before merge but the
revert commit (37e04500) was pushed after the PR had already been merged,
so it never reached main. Apply the same revert directly here.
normalizeCommitHeatmapData now keys the aggregation map by item.date as
the server returns it (already in viewer tz post-#2190).
* fix(observability): persist usage/audit in UTC and bucket per request tz
Dashboard, token-trend, and context-commit-heatmap previously bucketed by
server-process local timezone (`server/config.py` default `local`, which
on Railway / Docker without `TZ` is UTC). UTC+8 viewers saw "today" and
4h heatmap edges shifted relative to their wall clock.
Root cause: `projection.py` applied `astimezone(self._tz)` before writing
the rollup PKs, so date/hour columns were already locked to the
container's tz; a read-side `?timezone=` could not recover the buckets
because the raw UTC instants were no longer in storage.
Fix: persist all time-keyed columns (`date_utc`, `hour_utc`,
`created_at`) in UTC and accept `?timezone=` per request on
`/api/v1/console/{dashboard/summary,tokens,context-commits}`. The viewer
tz is resolved with `zoneinfo`, then the SQLite reads pull the spanning
UTC window and rebucket in Python to user-local days / 4h buckets. DST
is handled by `ZoneInfo` automatically.
Schema bump (v1 -> v2): `usage_token_daily` becomes `usage_token_hourly`
and `usage_retrieval_daily` becomes `usage_retrieval_hourly` (extra
`hour_utc` column in PK so cross-tz "today" slicing is precise). Other
tables keep their shape; `date` is renamed to `date_utc` to make the
semantic shift unambiguous. On boot, the store DROPs the legacy tables
once and recreates the new layout - acceptable because retention is 14
days and the product is pre-GA.
Frontend (`web-studio`) now passes
`Intl.DateTimeFormat().resolvedOptions().timeZone` on every console
fetch; `getLastDaysRange` derives the date range in the viewer's tz so
its boundaries match the backend interpretation. No visual / component
changes were needed.
Tests:
- Backend: existing 15 cases ported to the new keyword-only `tz=` API;
three new cases exercise UTC+8 day-boundary, America/New_York day
rebucketing across UTC midnight, and Asia/Shanghai 4h heatmap hour
shift. All 20 observability tests pass.
- Frontend: typecheck + lint clean (no new errors).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(observability): reset legacy usage audit sqlite schema
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web-studio): PWA, mobile UI polish, OAuth-setup tab, request-logs touch-ups
PWA & install
- manifest 192/512 + maskable icons, service-worker registration in main.tsx,
apple-mobile-web-app meta + viewport-fit=cover in index.html.
- openviking/server/app.py: redirect "/" -> "/studio/" so the PWA install
start_url resolves correctly when accessed from the bare host.
OAuth setup as its own tab
- New /studio/oauth/setup route + OAuthSetupDialog; sidebar footer entry
routes to the dialog on >=md and to the dedicated page on phone (mobile
fullscreen dialogs are hard to dismiss). OTP form extracted from the
connection dialog into a reusable component.
Mobile UI fixes
- Home metric chips: grid minmax 92px -> 140px, removed truncate so chips
wrap instead of clipping.
- Context-commits tooltip: clamps into viewport on narrow screens.
- Context-commits heatmap: auto-scrolls to rightmost (latest) on items
change so phone view opens on the current week.
- Dialog: max-h-[calc(100dvh-2rem)] + overflow-y-auto for phone overflow.
- FS page (resources):
* full-height container uses calc(100svh-6rem) + symmetric -my-6 so the
outer ScrollArea no longer overflows by parent's py-6.
* inner overlay-style scrollbars on phone (matches iOS / Atlas).
* PWA standalone mode hides the outer page scrollbar on this route.
* Tighter row density on phone (text-xs / py-1) to match Atlas.
* FolderIcon size-5 -> size-4 so folder and file text columns align.
* Folder highlight only when no file is selected (clean single-select).
* Toolbar refresh button: size-icon-sm + pl-4 to align with the header
sidebar trigger on phone.
- Find-palette: phone-center + max-h-[calc(100svh-2rem)] (was top-anchored
+ 84vh). Search input min font-size 16px to dodge iOS auto-zoom; same
treatment in the retrieval search bar.
Request logs
- Time column: dot-separated YYYY.MM.DD HH:MM:SSam/pm (compact, no locale
variance).
- Total calls clamps display at "999+".
- Pagination row: justify-center on phone (sm:justify-between preserved).
i18n
- Retrieval placeholder: "Search context" / "输入检索内容".
- Find-palette placeholder: "Search" / "搜索".
- Context-commits title: drop "in the last year" (range chip still shows).
- find / search labels: drop the parentheses.
- Workspace sidebar header: "OpenViking Studio".
Build verified after each change. Phone view validated via Chromium
emulation; iOS PWA-mode scrollbar fix scoped behind
@media (display-mode: standalone).
* feat(web-studio): more mobile polish and viewer-tz bucket shift for commit heatmap
PR #2178 follow-ups based on phone PWA testing:
Context commits heatmap (Home)
- Re-bucket UTC (date, hour_bucket) 4h rows into viewer-local (date,
hour) before daily aggregation, so a UTC+8 viewer's "today" cell holds
the right local-day count instead of UTC's. Pure client-side relabel —
each bucket's count is preserved, just attributed to the correct local
date. Daily / "today_tokens" aggregates can't be rebucketed this way
and still need the backend tz work (separate follow-up).
- Auto-scroll: align viewport's right edge with the rightmost rendered
rect's right edge (not the SVG's hardcoded width=820 right edge), so
the ~50px of trailing SVG padding stays off-screen on phone.
- Wrap the scroll write in requestAnimationFrame so HeatMap has a tick
to lay out its SVG; otherwise scrollWidth equals clientWidth and the
scroll snaps to the left (oldest) edge.
FS / resources route polish
- File tree: clicking a folder row now toggles expand/collapse in
addition to opening the folder (chevron icon still works alone via
stopPropagation, no double-toggle).
- File tree: parent passes selectedFileUri only when the selected entry
is actually a file; opening a folder no longer leaves it competing
with the parent's directory highlight.
- File tree: session subtree (any URI under viking://session/) sorts
children by modTimestamp DESC instead of name, so the most recent
session shows first.
- FolderIcon size-5 -> size-4 to match File icon and chevron, so folder
and file rows share the same text x-offset at every depth.
- Toolbar: refresh Button now uses size="icon-sm" + pl-4 on the toolbar
so its icon center aligns vertically with the header's sidebar
trigger on phone.
- Breadcrumb nav: overflow-hidden -> overflow-x-auto + whitespace-nowrap
+ flex-1 so long paths scroll horizontally on phone instead of being
cut off invisibly.
formatModTime
- Parse time-only mod_time strings ("HH:MM:SS", "12:34am" etc) against
UTC today + Z marker — backend's format_simplified uses UTC's "today"
reference, not the viewer's local today, so the prior local-today
prefix mis-attributed days for non-UTC viewers.
- Date-only mod_time ("YYYY-MM-DD") renders as-is; we don't fake an
"00:00" time component that doesn't exist.
JSONL dialog
- Cards: drop ml-auto / mx-auto / mr-auto chat-bubble alignment;
switch w-fit max-w-[min(820px,88%)] to w-full min-w-0 max-w-full so
long expanded messages, tool-call JSON, and markdown tables can use
the full pane width and trigger their own internal overflow-auto
instead of pushing the article wider than the viewport.
- List container picks up min-w-0 so the flex column can shrink.
- Kind distinction now color-only (bg / border).
Request-logs pagination
- justify-center on phone for both the summary row and the pagination
control row; sm:justify-between / sm:justify-end preserved on tablet+.
#2160 dropped the legacy `/console` standalone service but deliberately
left the OAuth authorize page's `/console` link and Quick-authorize panel
in place, calling out a follow-up to re-point them at web-studio. This
PR is that follow-up.
Backend
- `provider.authorize()` now defaults to redirecting to
`/studio/oauth/consent` (same-origin SPA) instead of the server-rendered
`/oauth/authorize/page`. New `FALLBACK_AUTHORIZE_PAGE` constant exposed
for callers that need to opt into the legacy path.
- New public endpoint `GET /api/v1/auth/oauth/pending/{pending_id}` returns
the minimum info the consent UI needs (client_name, redirect_host,
scopes); deliberately does NOT expose display_code or full redirect_uri.
- `POST /api/v1/auth/oauth-verify` now accepts either `pending_id`
(Studio consent path) or `code` (cross-device fallback).
- HTML `/oauth/authorize/page` template stripped of `/console` link, the
`/console/api/v1/...` JS, and the Quick-authorize same-origin panel.
It now serves as a pure cross-device fallback that points users at
`/studio/oauth/verify` on another already-signed-in device.
Web Studio
- New `<IdentityPicker>` shared component: "current identity" or
"use a different API key" — the temporary key is never persisted.
- New routes `/studio/oauth/consent` (same-device consent card) and
`/studio/oauth/verify` (cross-device code entry).
- ConnectionDialog gains an "OAuth client OTP" section (same
IdentityPicker), driving `POST /api/v1/auth/otp`.
- API key storage is unchanged: only sessionStorage. No new localStorage
writes, no cross-tab channels — the consent UI runs inside Studio's own
tab, so it reads the session-stored key directly.
Docs
- 11-oauth.md (zh/en): refreshed quickstart, How-it-works, Claude.ai
walkthrough, curl example, and troubleshooting around the Studio
consent / cross-device verify split.
- 12-public-access.md (zh/en): rewritten to lead with public HTTPS;
the `:1934` Caddy block is now a one-paragraph compatibility note for
deployments that already bookmarked it.
- mcp-oauth2-1.md: top-level "Studio migration" note explains the new
default path; Phase 1 history retained.
- Caddyfile / docker-compose.yml comments reworded from "aggregated
proxy" to "legacy fallback" to match the new docs.
Tests
- `tests/server/oauth/test_router.py` fixture pins to
FALLBACK_AUTHORIZE_PAGE so existing end-to-end assertions keep working.
- 4 new tests cover the pending-info endpoint and pending_id verify path.
- 55 passed locally; ruff format+check, web-studio tsc/eslint/prettier
all clean.
Security notes
- Consent UI requires explicit user click; client_name + redirect_host
shown for phishing identification.
- Knowing a pending_id does not bypass Bearer auth.
- display_code is not returned by GET pending — the cross-device
brute-force protection is preserved.
- `ctx.from_oauth` gate (router.py) untouched: OAuth bearer still
cannot mint new OAuth state or OTPs.
The OpenViking docker image still launched the legacy `openviking/console`
standalone service on port 8020. Now that web-studio is bundled into the OV
server itself at /studio (see #2156), that process is redundant and the
port is just a confusing artefact.
This change retires the old console (python package + 8020 + console-frontend
favicons) but **keeps the in-compose Caddy as a stable single-ingress on
port 1934**, just simplified to one upstream now that there's no 8020. The
server-side BFF at `openviking/server/routers/console.py` (under
`/api/v1/console/*`) is also kept — web-studio uses the same endpoints.
**The OAuth authorize page (`openviking/server/oauth/router.py`) is
deliberately untouched in this PR** — the console-link button and Quick
authorize same-origin panel will be re-pointed at web-studio in a focused
follow-up.
BREAKING CHANGES:
- Port 8020 is gone from the docker image and docker-compose.yml; Caddy at
1934 now forwards everything to 1933 (web-studio lives at /studio there).
Anything bookmarked at `http://host:8020/...` must migrate to
`http://host:1933/studio/`.
- `python -m openviking.console.bootstrap` no longer exists; the python
package `openviking.console` has been removed.
Pip packaging:
- web-studio dist is now shipped inside the wheel under
`openviking/web_studio/dist/` (mirroring the old `openviking/console/static/`
layout). The dockerfile copies `--from=web-studio-builder /web-studio/dist`
into the source tree before `uv sync`, so the wheel produced by the
default docker build always carries the SPA. Building the wheel without
running `npm run build` first leaves the directory empty, which gracefully
degrades /studio to a 404 without breaking server startup.
- Favicon assets (`favicon.ico` / `favicon-32.png` / `apple-touch-icon.png`,
~11 KB total) are duplicated into `openviking/server/static/` and shipped
via package-data so `/favicon.*` and `/mcp/favicon.*` routes are always
registered, regardless of whether the web-studio dist is bundled.
- `pyproject.toml` and `setup.py` `package-data` drop `console/static/**`
and add `server/static/**` + `web_studio/dist/**`.
- New favicons (the 16/32/180 set in both `openviking/server/static/` and
`web-studio/public/`) are downscaled from the canonical
`web-studio/public/openviking-icon.png`, so the small-icon family matches
the SPA's high-res rel="icon" target — the studio tab icon now stays
consistent whether the browser uses the HTML link tag or falls back to
auto-fetching `/favicon.ico`.
Server:
- `openviking/server/app.py` now reads `/studio` from
`Path(__file__).parent.parent / 'web_studio' / 'dist'` by default;
`OPENVIKING_WEB_STUDIO_DIR` still wins for dev mode pointing at a
repo-local build. Favicon routes are unconditionally registered and
load from `openviking/server/static/`.
- `openviking/observability/usage_audit/projection.py` drops the legacy
`/console/*` skip prefix (the BFF prefix `/api/v1/console/*` remains).
Docker:
- `web-studio-builder` stage moved earlier (Stage 2) so its dist can flow
into `py-builder` before `uv sync` runs.
- Runtime stage no longer separately copies the dist or sets
`OPENVIKING_WEB_STUDIO_DIR`; the in-package path is the default.
- Entrypoint renamed `openviking-console-entrypoint.sh` -> `openviking-entrypoint.sh`
and stripped of the `python -m openviking.console.bootstrap` launch.
- `EXPOSE 1933 8020` -> `EXPOSE 1933`.
- `docker-compose.yml` drops the openviking service's 8020 port mapping;
the caddy service stays but no longer needs port 8020 exposed.
- `Caddyfile` simplified to a single `:1934 { reverse_proxy openviking:1933 }`
— the legacy `/console/*` route to :8020 is gone.
Docs:
- en/zh quickstart updated to drop the 8020 mapping and explain that the
API server now also serves `/studio`.
- Other guides (`12-public-access.md`, `11-oauth.md`, `05-observability.md`,
`04-setup-for-agent.md`, `03-deployment.md`) are intentionally left for a
focused follow-up PR alongside the OAuth quick-authorize reintroduction.
Tests:
- Deleted `tests/misc/test_console_{proxy,static_assets}.py` (covered the
removed console package). `tests/observability/test_console_router.py`
stays — it covers the BFF, which remains.
* feat(web-studio): bundle web-studio into docker, mount at /studio + favicon to public
- Dockerfile: add node:20 build stage for web-studio, `vite build --base=/studio/`
output to /app/web-studio/dist. New ENV OPENVIKING_WEB_STUDIO_DIR.
- server/app.py: serve /studio and /studio/{path:path} from dist; SPA deep-link
fallback to index.html. Favicon routes (/favicon.ico, /favicon.png,
/apple-touch-icon.png, /mcp/favicon.ico+png+touch) read from the same dist —
no more separate console/static dependency.
- web-studio/public: ship favicon.ico, favicon-32.png, apple-touch-icon.png
inside the SPA bundle so they end up in dist root.
Old console (8020) untouched in this branch.
* fix(web-studio): drop hardcoded 127.0.0.1:1933 fallback in ovClient default
ovClient was initialized with `baseUrl: ENV_BASE_URL || 'http://127.0.0.1:1933'`,
which short-circuited normalizeBaseUrl's window.location.origin fallback. As a
result, when web-studio was served by the OV server itself (e.g. bundled in the
OV docker image at /studio/), the SPA would try to call back to the user's local
127.0.0.1:1933 instead of the same origin it was loaded from — making the
bundled deployment unusable.
Removing the literal fallback lets normalizeBaseUrl pick window.location.origin
when ENV_BASE_URL is empty, which is the right default for every "served by OV"
shape (bundled image, port-forwarded docker, reverse-proxied custom domain).
Dev workflows running vite separately can opt in by setting VITE_OV_BASE_URL or
overriding the URL in the in-app Connection dialog (which persists to
localStorage).
---------
Co-authored-by: alice <alice@zouk-agent.local>
* feat: 新前端项目初始化
* style: 切换到vega风格
* feat: 新增定制化的 openapi codegen
* chore: 把openapi生成的ov-client移到src/gen底下
* feat: 生成 openviking-server 的 client
* feat: 搭建 openviking client 的适配层
* docs: 新增 ov-client 适配层描述
* chore: 清理项目模板
* clean: 清理模板样式
* deps: 新增 tanstack query 依赖
* style: 调整 shadcn 风格
* feat: 导入基础的shadcn组件
* feat: 复刻旧的 webconsole
* fix: 修复legacy代码commit不完整的问题
* docs: readme
* feat: 允许不显示传递自定义client
* feat: 三段式布局 + 功能页拆分
- 新增顶栏(全宽) + 左侧边栏 + 右侧内容区的三段式布局
- 将 Data 大页面拆分为独立路由: FileSystem、Find、Add Memory
- 将 Access 页面迁移为独立的 Settings 页面
- 侧边栏按 Data/Ops/Access 分组显示导航项
- 提取共享工具函数到 data-utils.ts
- 修复 .gitignore 中 data/ 规则误匹配子目录的问题
* feat(web-studio): add i18n support and dark/light theme toggle
- Add i18next with browser language detection (zh-CN / en)
- Add language switcher dropdown in header bar
- Add dark/light theme toggle with animated Sun/Moon icons
- Wire up next-themes ThemeProvider with class-based dark mode
- Replace shadcn dark theme with default Zinc (neutral gray)
- Connect sidebar labels to i18n translation keys
- Fix dropdown menu forced dark styling
* clean: 项目清理
* clean: 清理项目
* feat: 添加应用程序外壳、连接对话框和多个路由组件
* feat: 添加产品结构文档,更新侧边栏状态管理逻辑
* feat: 规范化目录结构
* feat(i18n): integrate translation for various components and routes
- Added translation support for the PlaceholderPage component, including layout notes and current status descriptions.
- Updated use-app-connection hook to return connection identity summaries with translation keys.
- Enhanced server mode descriptions with translation keys in the use-server-mode hook.
- Introduced a new resources file for managing translations in English and Chinese.
- Updated admin, operations, resources, and sessions routes to utilize translations for titles, descriptions, and highlights.
- Refactored error handling in the ov-client to improve clarity and maintainability.
- Improved utility functions for better type handling and code readability.
* feat: 添加 AGENTS.md 文件,定义前端工作区的实施规则
* fix: mpn audit fix
* feat: 更新国际化支持,添加英文和中文语言文件,移除占位页面组件
* feat: 添加语言选择功能和错误边界处理,更新国际化文本
* feat: 移除管理员路由及相关组件,清理不必要的代码
* feat: 移除管理相关路由及翻译,更新 AGENTS.md 和 README.md 文档
* feat: 更新会话工作区占位文本,增加 Bot 相关描述
* feat(web-studio): add homepage dashboard with system overview
- Add /home route with system health, memory stats, token usage,
recent tasks, and sessions cards
- System health breathing dot animation for healthy components
- Memory stats donut chart with monochrome gradient palette
- Dark mode support with reactive theme detection and proper contrast
- Morandi-style status colors for tasks and session badges
- Sidebar navigation updated with Home entry
- Root route redirects to /home
* fix(web-studio): respect VITE_OV_BASE_URL env var in default ovClient
* refactor: extract shared ENV_BASE_URL constant to avoid duplication
* style(web-studio): delete the max-w-7 in app-shell.tsx
* feat: implement Viking file manager with file tree and hooks
- Add FileTree component for displaying directory structure.
- Create VikingFileManager component to manage file navigation and selection.
- Implement hooks for fetching file system data and handling file previews.
- Introduce API functions for interacting with the file system.
- Normalize file URIs and entries for consistent handling.
- Update route to integrate VikingFileManager and manage URI changes.
* feat(web-studio): add theme toggle button to header
* style(web-studio): unify layout colors and simplify header UI
- Remove SidebarRail and sidebar border for seamless layout
- Unify all backgrounds to bg-sidebar to eliminate dark mode color gaps
- Remove header page title, language label, and outline borders
- Enlarge sidebar nav icons and fonts
- Fix file manager negative margins to fully cover parent padding
* fix(web-studio): 隐藏无子文件夹目录的展开按钮
当文件夹下只有文件没有子文件夹时,不再显示展开箭头,
避免用户点击后看到空白内容。数据未加载时仍显示箭头以防闪烁。
* feat(web-studio): add i18n support to home page and gsap entrance animation
- Replace all hardcoded dual Chinese/English titles with i18n keys
- Add home namespace to en.ts and zh-CN.ts locale files
- Add memory category translations (profile, preferences, etc.)
- Add gsap staggered fade-in-up animation on home page mount
- Show language shortLabel in header language switcher
- Remove mx-auto from content wrapper for full-width layout
* feat(web-studio): 重构资源上传流程并修复单文件解析兜底
- 将 Add Resource 入口迁移到 /resources/add-resource
- 新增上传状态 hook、上传页面和目录选择器
- 支持拖拽上传、远程 URL 导入、进度展示和完成态反馈
- 补充上传相关 i18n、依赖和 .vite 忽略规则
- 修复单文件上传时未知二进制文件误回退为文本解析的问题
* fix(web-studio): 修复上传页进度显示与侧边栏展开逻辑
- 将上传进度文案移出 Progress 组件,避免子节点不渲染
- 将资源分组侧边栏改为受控展开,确保路由激活时自动展开
- 将导航项展平结果提到组件外,避免重复计算
- 将上传页支持格式提示从 PPT 调整为更准确的 PPTX
* Merge new-frontend into feat/home-i18n-animation and resolve conflicts
* feat(web-studio): add semantic search to file manager
Embed find/search functionality directly into the /resources file manager:
- Search bar in toolbar with Enter-to-search, supports ?q= deep linking
- Results grouped by Resources/Memories/Skills with clickable URI navigation
- Parallel search per context_type at root to ensure all types are represented
- Support ?file= param for auto-selecting files from search results
* feat(web-studio): command palette search with preview and keybinding
- Replace inline search bar with ⌘K command palette overlay
- Three-column layout for root search (Resources/Memories/Skills)
- Right-side preview pane showing abstract/overview on selection
- Full keyboard navigation: ↑↓ within column, ←→ across columns, Enter/Esc
- Folder button on each result to jump to parent directory
- 300ms debounce, React Query cache with 5min gcTime, placeholderData
- Consolidated typography to text-sm/text-xs two-tier scale
* feat(web-studio): palette animations, IME compat, file preview, search debounce
- Palette open: backdrop fade + panel slide-in + staggered row entrance
- Preview pane slide-right animation, reuse FilePreview for real content
- IME composition guard: pinyin Enter no longer triggers search navigation
- 300ms debounce with placeholderData to avoid flicker between queries
- Column header bg opacity 8% → 15%, kbd tags with muted background
- Typography consolidated to text-sm/text-xs scale
* feat(web-studio): directory browser with / command in search palette
Type / in the palette to enter directory browse mode:
- Two-column Miller layout: left = siblings, right = children
- Arrow keys navigate within and across columns
- Left arrow goes up one level, right drills into subdirectory
- Enter selects directory as search scope, Backspace exits to search
- Document-level keyboard listener so input retains focus
* fix(web-studio): resolve review issues in search palette
- Remove duplicate Cmd+K handler from FindPalette (single owner in VikingFileManager)
- DirBrowser: keep document keydown listener but remove stale Backspace handler
- Memoize effectiveReadOptions to fix preview memo invalidation on every render
- Reset activeIndex on data change instead of flatItems.length
- Wrap updateUri/handleNavigateFromSearch in useCallback
- Use ref for treeWidth in handleResizeStart to avoid recreation during drag
- Add ARIA role="dialog" and aria-modal to palette overlay
* feat(web-studio): add delight micro-interactions to search palette
- Empty state: centered Search icon + two-line guide with fade-in
- Loading state: rotating contextual hints (vector index, hierarchy scan, rerank)
- No results: empathetic message with suggestion
- Active row: 2px primary accent bar on left edge
- Scope badge: pulse flash on scope change
- Search trigger button: hover lift + shadow, active press-down
* refactor(web-studio): remove hover selection and scope flash from palette
- Remove onHover/onMouseEnter from result rows, keyboard-only selection
- Remove scope badge flash animation and related state
- Clean up unused CSS keyframes
* feat(web-studio): show file metadata in preview, move search to tree toolbar
- Add fetchFsStat API and useVikingFsStat hook to fetch file size/modTime
- Palette preview pane now shows real file size and modification date
- Move search button from breadcrumb toolbar to tree sidebar button area
- Root view (no tree) also has search button alongside parent/refresh
* fix(web-studio): remove dir browser hover, fix date formatting
- Remove onHover/onMouseEnter from DirBrowser columns, keyboard-only
- Format ISO 8601 dates (e.g. 2026-04-07T01:34:33+08:00) to readable
"YYYY-MM-DD HH:mm" in fetchFsStat
* refactor(web-studio): unify date formatting across file list and preview
- Move formatModTime to normalize.ts as shared utility
- normalizeFsEntry now formats dates consistently (YYYY-MM-DD HH:mm)
- fetchFsStat reuses the same formatModTime
- Remove duplicate implementation from api.ts
* Refine resources search palette workflow
* feat(web-studio): enhance FindPalette keyboard navigation with Enter key support
* fix:补充目录文件预览与全局范围提示
* fix:补充搜索面板与目录浏览国际化文案
* feat(i18n): add homepage redesign translation keys
* feat(home): upgrade StatCard with color accents, icons, subtitles and countUp
* feat(home): restructure system health to vertical list layout
* feat(home): redesign error details dialog with status bar and collapsible sections
* feat(home): upgrade memory stats to low-saturation color palette
* feat(home): enhance panels, tables, and empty states
* fix(home): address review findings — TS type, i18n hardcoded strings, clipboard error handling
* style(home): remove StatCard left accent border
* style(home): increase card-background contrast and fix countUp for zero values
* style(home): unify hover shadow between StatCard and Panel
* fix(home): align system health row text by applying consistent padding
* fix(home): align System row padding with component rows
* fix(home): remove redundant System row from health list
* fix(home): remove first/last padding overrides for consistent row spacing
* style(home): remove left border from error items in dialog
* style(home): remove gradient status bar from error dialog
* feat(web-studio): implement VikingBot sessions chat UI
- Chat interface with assistant-ui runtime (SSE streaming, message history)
- Session title system: localStorage + AI-generated titles via sendChat
- Session list with search/filter (Cmd+K), new session (Cmd+N)
- Tool call collapsible cards with input/output display
- Reasoning expandable blocks with running/complete states
- Iteration badge for multi-step agent responses
- User/Assistant avatars, active session highlight
- Loading skeleton, error states, empty state with keyboard hints
* refactor(web-studio): replace assistant-ui with Streamdown + custom chat components
## Chat UI Architecture
- Remove @assistant-ui/react and @assistant-ui/react-markdown dependencies
- Remove adapter layer: use-assistant-runtime, use-thread-list-adapter, convert-message
- Delete assistant-ui primitives: thread.tsx, thread-list.tsx
- Components now consume useChat() directly without runtime conversion
## New Components (src/components/chat/)
- message-parts.tsx: MarkdownContent (Streamdown), ReasoningBlock, ToolCallBlock
- composer.tsx: floating frosted-glass input with backdrop-blur, file attachment support
- message-list.tsx: message bubbles with frosted-glass backgrounds (bg-background/70)
- thread.tsx: main chat area with PixelBlast background and auto-scroll
## Streaming Markdown
- Add streamdown, @streamdown/code, @streamdown/cjk for flicker-free streaming
- Incremental DOM updates instead of full re-parse on each token
- Shiki syntax highlighting and CJK text support
## Session Navigation
- Move session list into app-shell sidebar as collapsible sub-menu (NavSessionsItem)
- Session switching via URL search params (?s=sessionId), supports browser back/forward
- Remove embedded 260px thread-list panel from sessions page
## Visual Design
- PixelBlast (Three.js) particle background at 40% opacity with edge fade
- Frosted-glass message bubbles and composer (backdrop-blur-xl)
- Message width increased to max-w-3xl
- All user-facing text localized to Chinese
## Other
- Add use-file-attachment hook for file upload in composer
- Add pixel-blast.tsx (react-bits) as UI component
- bot: minor changes to ov_file.py and ov_server.py
* perf(web-studio): optimize chat rendering and background loading
- PixelBlast: defer mount until browser idle via requestIdleCallback,
avoids blocking chat UI interactivity with 538KB Three.js chunk
- Auto-scroll: throttle via requestAnimationFrame, cancel pending
scroll before scheduling new one (was firing per-token)
- UserMessage/AssistantMessage: wrap with React.memo to skip
re-renders when streaming state changes (only streaming message
needs updates, history messages are stable)
- NavSessionsItem: select only `s` param from router state instead
of entire search object, prevents re-render on unrelated route changes
* feat(web-studio): add copy button to messages, cache session history
- Add hover-reveal copy button to both user and assistant messages
(copies text content to clipboard, shows checkmark on success)
- Cache session message history for 30s (staleTime) to avoid
flash/refetch when switching between sessions quickly
* polish(web-studio): refine chat UI cards, markdown, and interactions
## Message Cards
- Same-role consecutive messages use compact spacing (mb-1.5 vs mb-5)
- User bubble: solid primary bg, tighter rounded-br-sm corner
- Assistant bubble: subtle ring-1 ring-border/30 for edge definition
- Avatar hidden for consecutive same-role messages (spacer preserves alignment)
## Brand Avatar
- Replace generic BotIcon with gradient ring avatar (from-primary/20 to-primary/5)
- Ring-1 ring-primary/10 for subtle brand color presence
## Markdown Prose
- Refined heading sizes: h1=lg, h2=base, h3=sm with proper spacing
- Inline code: muted bg, rounded, no backtick pseudo-elements
- Blockquote: primary left border + muted bg + rounded-r-lg
- Links: primary color, no underline by default, underline on hover
- Tables: uppercase tracking-wider th headers
- Relaxed line-height for paragraphs and lists
## Typing Indicator
- 3-dot bounce animation when streaming starts before content arrives
- Shows only when no content/reasoning/toolCalls yet
## Timestamps
- Hover-reveal relative time on each message (刚刚/分钟前/小时前)
- 10px muted text, positioned beside copy button
## Iteration Badge
- Changed from English "Iteration N" to "第 N 轮"
- Pill style: rounded-full, primary/10 bg, primary text
## Empty State
- Product name "OpenViking" with gradient icon container
- Suggestion pills: 3 quick-start prompts as rounded buttons
## Tool/Reasoning Blocks
- Softer borders (border/30), lighter backgrounds (muted/20)
- Smaller label text (10px) with wider tracking
* fix(web-studio): correct bubble corner radius and remove suggestion pills
- User bubble: rounded-tr-sm (top-right near avatar), was wrongly br
- Remove quick-start suggestion pills from empty state
* feat(web-studio): use OpenViking logo as assistant avatar
Replace generic BotIcon with ov-logo.png in chat message bubbles.
* perf(web-studio): fix chat page jank — reduce GPU load significantly
## PixelBlast background
- Cap frame rate at 30fps (was uncapped 60fps) via frame budget throttle
- Set pixelRatio to 1 (was 2x on Retina), reduces render pixels by 4x
- Combined: ~8x less GPU work for the background animation
## Remove backdrop-blur from chat elements
- Message bubbles: bg-background/70 backdrop-blur-xl → bg-background/95
- Composer: bg-background/50 backdrop-blur-xl → bg-background/95
- Title bar: bg-background/80 backdrop-blur-sm → bg-background/95
- Each backdrop-blur created a compositing layer that re-blurred the
animated canvas every frame — this was the main source of jank
## Logo compression
- Resize ov-logo.png from 1000x1000 (large) to 56x56 (2x render size)
- File size reduced to 2.2KB
## Scroll optimization
- Remove CSS scroll-smooth (conflicts with JS scrollIntoView smooth)
* fix(web-studio): add missing text-base class to sessions nav title
* fix(web-studio): improve sidebar vs content area contrast
Change SidebarInset background from bg-sidebar to bg-background so the
main content area is visually distinct from the sidebar in both light
and dark modes.
* style: update CSS variables for Ocean Warmth theme
* style: replace all hardcoded colors in home page with theme palette
* style: replace all hardcoded colors in home page with theme palette
- Category colors: cyan-to-purple spectrum (hue 200-292)
- Heatmap: green → blue gradient
- Token trend: cyan/blue/lavender
- Status indicators: semantic oklch colors
- System health: oklch with alpha
- Session badges: oklch with alpha
- Stat card accents: brand blue
- Pie chart: theme-aware stroke/label
- Fix accentColor alpha to use color-mix (hex append doesn't work with oklch)
* feat(web-studio): add new user onboarding dialog and empty states
Add a welcome dialog on first visit showing OpenViking value proposition
and slogan, with CTA navigating to context management upload. Add empty
states for context management, retrieval, and request logs pages.
* feat: add upload task dialog and resource upload handling
- Implemented UploadTaskDialog component to display ongoing upload tasks with status and error details.
- Integrated UploadTaskDialog into VikingFileManager for managing upload tasks.
- Added logic to track completed upload tasks and show processing notices.
- Introduced utility functions for file size formatting and upload error parsing.
- Defined constants for upload limits and blocked file extensions.
* feat: update VikingFileManager to use toast for processing notices
* fix: remove 'Add Resource' button text and update remote URL handling in AddResourceForm
* fix: update sidebar header and improve layout in app shell; refine request logs and retrieval page structure
* feat(web-studio): dynamic import hljs languages and expand coverage
Switch from static imports to dynamic import() for highlight.js
languages, so each language chunk loads only on first use. Expanded
from 14 to 42 languages (added toml/ini, kotlin, swift, ruby, etc).
Added markdown source mode syntax highlighting and dark mode token
colors (github-dark).
* feat(web-studio): show files in FileTree sidebar and support file selection
FileTree previously only displayed directories. Now it renders both
files and directories with proper visual hierarchy, selection state,
and semantic color tokens replacing hardcoded Tailwind grays.
* feat(web-studio): add inline code editor with UI polish
Add CodeMirror-based file editing in the resource preview panel.
Move edit/save controls to the left (next to filename), enlarge close
button hit area to 40px, add press-scale feedback on save, and replace
image borders with outline for cleaner depth.
* feat(web-studio): add empty state upload guidance for retrieval and request logs pages
Fix incorrect retrieval empty state copy (was duplicating request logs text) and add
"Upload File" CTA buttons that navigate to the resource upload dialog, guiding new
users toward their first context upload.
* refactor(web-studio): move upload search param to index route only
Parent route should only declare shared params (uri). The upload param
is only used by the index route to control the upload dialog, so it
belongs exclusively in index.tsx for better separation of concerns.
* fix(web-studio): use correct mode 'replace' for saveFileContent API
The API expects 'replace' instead of 'overwrite' for file content updates.
* fix(web-studio): preserve upload search param in resources layout route
The layout route's validateSearch was stripping the upload param before
it could reach the index route, breaking navigation from empty states.
* docs(web-studio): add merge plan
* feat(web-studio): enhance retrieval page
* feat: update home dashboard
* feat: update request logs
* feat: update app icon
* feat: improve resource search
* refactor(web-studio): split home and retrieval routes
* chore(web-studio): regenerate ov client from target api
* feat(web-studio): add scoped ov server contracts
* fix(web-studio): route api calls through generated client
* fix(web-studio): trim unsupported resources and chat features
* chore(web-studio): restore local validation baseline
* chore(web-studio): resolve i18next lint warnings
* chore(web-studio): drop local request log store
* chore(web-studio): add task result types
* feat(web-studio): restore resource upload tasks
* docs(web-studio): document upload task recovery
* fix(web-studio): parse structured chat stream payloads
* fix(web-studio): ignore composing enter in chat composer
* fix(web-studio): render chat stream events as they arrive
* fix(web-studio): remove upload task refresh button
* fix(web-studio): use neutral text for failed upload tasks
* docs(web-studio): rewrite developer readme
* docs(web-studio): document subpath deployment
* docs(web-studio): add english readme entry
* docs(web-studio): remove old docs
* refactor(web-studio): split request logs route
* feat(web-studio): add request log page size control
* refactor(web-studio): keep chat components under sessions
* refactor(web-studio): align route ownership boundaries
* chore(web-studio): align format scope with lint
* chore: remove test file
* style(web-studio): format source files
* chore: remove old placeholder
* chore: remove unused icons
* chore: delete merge plan
---------
Co-authored-by: Jye10032 <736891807@qq.com>
Co-authored-by: ponsde <2126824294@qq.com>
Co-authored-by: ifeichuan <feichuan05@gmail.com>
Co-authored-by: tins0 <1537604848@qq.com>