diff --git a/.changeset/config.json b/.changeset/config.json index bf636ed21..d670e55c4 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -14,6 +14,7 @@ "updateInternalDependencies": "patch", "ignore": [ "kimi-code-web", + "kimi-code-auth-login", "@moonshot-ai/vite-preset", "@moonshot-ai/app-core", "@moonshot-ai/app-i18n", diff --git a/AGENTS.md b/AGENTS.md index 8f57380fa..1ec8631fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,7 @@ - `apps/desktop`:Electron 壳(`kimi-code-app`);简介见 `apps/desktop/README.md`,原生功能分叉清单见 `apps/desktop/docs/native-todos.md`。新测试:主进程进 `tests/main/`,renderer 进 `tests/renderer/`。 - `apps/web`:浏览器 Web UI(`kimi-code-web`,Vue 3 + Vite + vue-i18n)。dev 时 Vite 把 `/api/v1`(REST + WS)代理到 `KIMI_SERVER_URL`(默认 `http://127.0.0.1:58627`)。 +- `apps/auth-login`:Remote Control 鉴权中间页(`kimi-code-auth-login`,单页、移动端优先):OAuth device flow 经 `@moonshot-ai/kimi-code-oauth/device` 直连 auth.kimi.com(CORS 直连,无 dev proxy),token 写 `kimi-auth` cookie;`redirect_uri` 可选,有则登录后回跳。方案见 `docs/plans/2026-08-13-rc-auth-login.md`。 - `packages/*`:`@moonshot-ai/{app-core,app-i18n,app-markdown,app-ui,app-client}` + `vite-preset`(exports→src,被 apps/web 与 desktop renderer 复用)——app-core 是无 Vue 依赖的纯层(api 客户端 / lib 纯函数 / client 渲染类型与热路径纯模块),app-client 是 Vue composables 层(注入 api / t / tracker);共享字体产物在 `app-ui/src/assets/fonts`(gitignored),由 `scripts/prepare-fonts.mjs` 自动准备。 - `kimi-code/`:git submodule(核心仓)。`kimi-code/packages/*` 提供 `kap-server`、`agent-core-v2`、`kimi-code-sdk` 等源码。 - `scripts/sync-web-to-kimi-code.mjs`:`apps/web/dist` → `/apps/kimi-code/dist-web`(`KIMI_CODE_REPO` 必传,指定目标 checkout)。 diff --git a/apps/auth-login/index.html b/apps/auth-login/index.html new file mode 100644 index 000000000..eb59fd65f --- /dev/null +++ b/apps/auth-login/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + Kimi Code + + +
+ + + diff --git a/apps/auth-login/package.json b/apps/auth-login/package.json new file mode 100644 index 000000000..1210989f5 --- /dev/null +++ b/apps/auth-login/package.json @@ -0,0 +1,33 @@ +{ + "name": "kimi-code-auth-login", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "prepare:fonts": "node ../../scripts/prepare-fonts.mjs", + "predev": "npm run prepare:fonts", + "dev": "vite", + "prebuild": "npm run prepare:fonts", + "build": "vite build", + "typecheck": "vue-tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@moonshot-ai/app-client": "workspace:*", + "@moonshot-ai/app-core": "workspace:*", + "@moonshot-ai/app-i18n": "workspace:*", + "@moonshot-ai/app-ui": "workspace:*", + "@moonshot-ai/kimi-code-oauth": "workspace:*", + "vue": "^3.5.35", + "vue-i18n": "^11.4.5" + }, + "devDependencies": { + "@moonshot-ai/vite-preset": "workspace:*", + "@vitejs/plugin-vue": "^5.2.4", + "typescript": "6.0.2", + "unplugin-icons": "^23.0.0", + "vite": "^6.3.3", + "vitest": "4.1.4", + "vue-tsc": "~3.2.0" + } +} diff --git a/apps/auth-login/public/favicon.ico b/apps/auth-login/public/favicon.ico new file mode 100644 index 000000000..f554e98ef Binary files /dev/null and b/apps/auth-login/public/favicon.ico differ diff --git a/apps/auth-login/src/App.vue b/apps/auth-login/src/App.vue new file mode 100644 index 000000000..bd85dafba --- /dev/null +++ b/apps/auth-login/src/App.vue @@ -0,0 +1,428 @@ + + + + + + + diff --git a/apps/auth-login/src/auth-token.ts b/apps/auth-login/src/auth-token.ts new file mode 100644 index 000000000..9d1a0eb11 --- /dev/null +++ b/apps/auth-login/src/auth-token.ts @@ -0,0 +1,84 @@ +// Cookie + query-param helpers for the auth-login page. Pure functions so the +// contract stays unit-testable; the page wires them to document/location. + +export const TOKEN_COOKIE_NAME = 'kimi-auth'; + +/** Read the access token from a `document.cookie`-shaped string. Returns null + when absent or empty. */ +export function readTokenCookie(cookieString: string): string | null { + for (const part of cookieString.split(';')) { + const [name, ...rest] = part.trim().split('='); + if (name === TOKEN_COOKIE_NAME) { + const value = rest.join('='); + return value.length > 0 ? decodeURIComponent(value) : null; + } + } + return null; +} + +export interface TokenCookieOptions { + /** Unix seconds when the token expires; mapped to the cookie's Expires. */ + readonly expiresAt: number; + /** Emit the Secure attribute. True on https; false for http://localhost dev. */ + readonly secure: boolean; +} + +/** Serialize the token cookie. SameSite=Lax so the tunnel's top-level + navigation back to the app carries it; Path=/ so every route on the host + sees it. */ +export function buildTokenCookie(accessToken: string, options: TokenCookieOptions): string { + const expires = new Date(options.expiresAt * 1000).toUTCString(); + const parts = [ + `${TOKEN_COOKIE_NAME}=${encodeURIComponent(accessToken)}`, + 'Path=/', + `Expires=${expires}`, + 'SameSite=Lax', + ]; + if (options.secure) parts.push('Secure'); + return parts.join('; '); +} + +/** Serialize an immediately-expired token cookie (i.e. delete it). Used when + the server side has rejected the token — e.g. the tunnel bounces the user + back with `force_relogin=1` — so a stale cookie can't ping-pong the user + between this page and the target. */ +export function clearTokenCookie(): string { + return `${TOKEN_COOKIE_NAME}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax`; +} + +/** Parse and validate the `redirect_uri` query param. Only absolute http(s) + URLs are accepted — anything else (javascript:, relative, protocol-relative) + is rejected so the page cannot become an open-redirect trampoline onto + non-http schemes. Returns null when missing or invalid. */ +export function parseRedirectUri(search: string): string | null { + const raw = new URLSearchParams(search).get('redirect_uri'); + if (!raw) return null; + let url: URL; + try { + url = new URL(raw); + } catch { + return null; + } + if (url.protocol !== 'https:' && url.protocol !== 'http:') return null; + return url.toString(); +} + +const LOCALE_COOKIE_NAME = 'KIMI_LOCALE'; + +/** Read the locale preference from the shared `KIMI_LOCALE` cookie (set on the + root domain, so the main site's language choice carries over to this page). + Only en/zh are recognized (regional variants like zh-CN / en-US count); + anything else — or absence — returns null and the caller falls back to the + browser language, then English. */ +export function readLocaleCookie(cookieString: string): 'en' | 'zh' | null { + for (const part of cookieString.split(';')) { + const [name, ...rest] = part.trim().split('='); + if (name === LOCALE_COOKIE_NAME) { + const value = rest.join('=').toLowerCase(); + if (value.startsWith('zh')) return 'zh'; + if (value.startsWith('en')) return 'en'; + return null; + } + } + return null; +} diff --git a/apps/auth-login/src/env.d.ts b/apps/auth-login/src/env.d.ts new file mode 100644 index 000000000..ea3c5c009 --- /dev/null +++ b/apps/auth-login/src/env.d.ts @@ -0,0 +1,21 @@ +/// + +// `~icons/*?raw` module declarations live in the shared preset. +import '@moonshot-ai/vite-preset/icons'; + +declare global { + // Injected by Vite `define` (see vite.config.ts): this bundle never ships + // inside the desktop app. + const __KIMI_WEB_DESKTOP__: boolean; + + // Injected by Vite `define` (from @moonshot-ai/vite-preset): bundle build + // time (ISO). Declared for shared sources that may reference it. + const __KIMI_BUILD_TIME__: string; +} + +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + + const component: DefineComponent, Record, unknown>; + export default component; +} diff --git a/apps/auth-login/src/flow.ts b/apps/auth-login/src/flow.ts new file mode 100644 index 000000000..b382e3152 --- /dev/null +++ b/apps/auth-login/src/flow.ts @@ -0,0 +1,160 @@ +// Adapts the shared useOAuthLoginFlow state machine to direct browser-side +// device-flow calls against the OAuth host (the desktop/web apps inject daemon +// callbacks instead). The OAuth host allows cross-origin POSTs, so the page +// talks to it straight from the browser. + +import type { + OAuthLoginFlowCallbacks, + OAuthLoginStartResult, +} from '@moonshot-ai/app-client/composables'; +import { + KIMI_CODE_FLOW_CONFIG, + pollDeviceToken, + requestDeviceAuthorization, + type TokenInfo, +} from '@moonshot-ai/kimi-code-oauth/device'; + +const DEVICE_ID_STORAGE_KEY = 'kimi-code-auth-login.device-id'; +const APP_VERSION = '0.0.0'; +/** Fallback when the OAuth host omits expires_in (it currently sends 1800). */ +const DEFAULT_EXPIRES_IN = 1800; +/** Extra delay folded into a poll callback after a `slow_down` response + (RFC 8628 §3.5 asks for +5s). The shared state machine schedules polls at a + fixed interval, so the backoff is absorbed here by returning later. */ +const SLOW_DOWN_EXTRA_MS = 5000; + +const sleep = (ms: number): Promise => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); + +/** crypto.randomUUID is secure-context-only, and this page may well be served + over plain http (LAN IP, plain-http tunnel) — fall back to getRandomValues + (available in every context), then Math.random. The id feeds an auxiliary + header, not a secret, so the weakest fallback is still acceptable. */ +function randomId(): string { + const c = globalThis.crypto; + if (typeof c?.randomUUID === 'function') return c.randomUUID(); + const bytes = new Uint8Array(16); + if (typeof c?.getRandomValues === 'function') { + c.getRandomValues(bytes); + } else { + for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256); + } + return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join(''); +} + +let memoryDeviceId: string | null = null; + +/** Stable per-browser id, the equivalent of the CLI's `/device_id`. + Falls back to a per-page random id when storage is unavailable (private + mode, blocked origin) — the header is auxiliary and must not block sign-in. */ +function deviceId(): string { + try { + const stored = localStorage.getItem(DEVICE_ID_STORAGE_KEY); + if (stored) return stored; + const id = randomId(); + localStorage.setItem(DEVICE_ID_STORAGE_KEY, id); + return id; + } catch { + memoryDeviceId ??= randomId(); + return memoryDeviceId; + } +} + +/** X-Msh-* identity reported to the OAuth host: the platform value reuses the + CLI's by decision, and the User-Agent stays the browser's own — it cannot + be overridden from JS. */ +function deviceHeaders(): Record { + return { + 'X-Msh-Platform': 'kimi_code_cli', + 'X-Msh-Version': APP_VERSION, + 'X-Msh-Device-Id': deviceId(), + }; +} + +export interface AuthLoginFlowOptions { + /** Called the instant a poll returns the token — before the state machine's + success dwell — so the cookie can be persisted even if the user closes + the tab while the success state is showing. */ + readonly onToken?: (token: TokenInfo) => void; +} + +export interface AuthLoginFlow { + readonly callbacks: OAuthLoginFlowCallbacks; + /** Token captured by the latest successful poll. */ + readonly authenticatedToken: () => TokenInfo | null; +} + +export function createAuthLoginFlow(options?: AuthLoginFlowOptions): AuthLoginFlow { + let deviceCode: string | null = null; + let token: TokenInfo | null = null; + + async function onStartOAuthLogin(): Promise { + let auth; + try { + auth = await requestDeviceAuthorization(KIMI_CODE_FLOW_CONFIG, { + deviceHeaders: deviceHeaders(), + }); + } catch { + // Start failure → the state machine shows the error step. + return null; + } + deviceCode = auth.deviceCode; + token = null; + const expiresIn = auth.expiresIn ?? DEFAULT_EXPIRES_IN; + return { + flowId: auth.deviceCode, + provider: KIMI_CODE_FLOW_CONFIG.name, + status: 'pending', + verificationUri: auth.verificationUri, + verificationUriComplete: auth.verificationUriComplete, + userCode: auth.userCode, + expiresIn, + interval: auth.interval, + expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(), + }; + } + + async function onPollOAuthLogin(): Promise<{ + flowId: string; + status: 'pending' | 'authenticated' | 'expired' | 'cancelled'; + resolvedAt?: string; + } | null> { + if (!deviceCode) return null; + let result; + try { + result = await pollDeviceToken(KIMI_CODE_FLOW_CONFIG, deviceCode, { + deviceHeaders: deviceHeaders(), + }); + } catch { + // Transport/unknown failure reads as a blip; the state machine retries + // and turns three in a row into the error step. + return null; + } + const flowId = deviceCode; + const resolvedAt = new Date().toISOString(); + switch (result.kind) { + case 'success': + token = result.token; + options?.onToken?.(result.token); + return { flowId, status: 'authenticated', resolvedAt }; + case 'pending': + if (result.errorCode === 'slow_down') await sleep(SLOW_DOWN_EXTRA_MS); + return { flowId, status: 'pending' }; + case 'expired': + return { flowId, status: 'expired', resolvedAt }; + case 'denied': + return { flowId, status: 'cancelled', resolvedAt }; + } + } + + async function onCancelOAuthLogin(): Promise { + deviceCode = null; + } + + return { + callbacks: { onStartOAuthLogin, onPollOAuthLogin, onCancelOAuthLogin }, + authenticatedToken: () => token, + }; +} diff --git a/apps/auth-login/src/i18n.ts b/apps/auth-login/src/i18n.ts new file mode 100644 index 000000000..8337cb9c0 --- /dev/null +++ b/apps/auth-login/src/i18n.ts @@ -0,0 +1,22 @@ +import { createKimiI18n } from '@moonshot-ai/app-i18n'; +import { readLocaleCookie } from './auth-token'; + +// Locale resolution for this page: the shared `KIMI_LOCALE` cookie (a +// root-domain cookie, so the main site's language choice carries over) → +// browser language → English. +function detectLocale(): 'en' | 'zh' { + try { + const fromCookie = readLocaleCookie(document.cookie); + if (fromCookie) return fromCookie; + } catch { + // document unavailable — fall through to the browser language. + } + return navigator.language.toLowerCase().startsWith('zh') ? 'zh' : 'en'; +} + +// Single i18n instance from the shared app-i18n factory: the page reuses the +// client's `login` namespace (plus the rc* keys) and `common` (Spinner's aria +// label). +export const i18n = createKimiI18n({ locale: detectLocale() }); + +export default i18n; diff --git a/apps/auth-login/src/main.ts b/apps/auth-login/src/main.ts new file mode 100644 index 000000000..9abffbf4e --- /dev/null +++ b/apps/auth-login/src/main.ts @@ -0,0 +1,22 @@ +import { createApp } from 'vue'; +import { IconResolverKey } from '@moonshot-ai/app-ui'; +import { KimiI18nKey, type KimiI18nApi } from '@moonshot-ai/app-i18n'; +import { getIcon, type IconName } from '@moonshot-ai/app-client/icons'; +import App from './App.vue'; +import i18n from './i18n'; +import './style.css'; + +document.documentElement.lang = i18n.global.locale.value === 'zh' ? 'zh-CN' : 'en'; + +const app = createApp(App).use(i18n); +// Let package components (e.g. Spinner's aria label) translate without +// importing the global vue-i18n directly. +const kimiI18n: KimiI18nApi = { + t: (key, params) => i18n.global.t(key, params as never), + locale: i18n.global.locale.value, +}; +app.provide(KimiI18nKey, kimiI18n); +// Bridge app-ui's to the icon registry (unplugin-icons `kimi` +// collection, wired in vite.config.ts). +app.provide(IconResolverKey, (name) => getIcon(name as IconName)?.component); +app.mount('#app'); diff --git a/apps/auth-login/src/style.css b/apps/auth-login/src/style.css new file mode 100644 index 000000000..828545ab9 --- /dev/null +++ b/apps/auth-login/src/style.css @@ -0,0 +1,21 @@ +@import '@moonshot-ai/app-ui/style.css'; + +html, +body { + margin: 0; + padding: 0; +} + +body { + font-family: var(--font-ui); + background: var(--color-bg); + color: var(--color-text); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +#app { + min-height: 100dvh; + display: flex; + flex-direction: column; +} diff --git a/apps/auth-login/test/auth-token.test.ts b/apps/auth-login/test/auth-token.test.ts new file mode 100644 index 000000000..de9682e1f --- /dev/null +++ b/apps/auth-login/test/auth-token.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it } from 'vitest'; + +import { + buildTokenCookie, + clearTokenCookie, + parseRedirectUri, + readLocaleCookie, + readTokenCookie, + TOKEN_COOKIE_NAME, +} from '../src/auth-token'; + +describe('readTokenCookie', () => { + it('returns null when the cookie is absent', () => { + expect(readTokenCookie('')).toBeNull(); + expect(readTokenCookie('other=abc')).toBeNull(); + }); + + it('reads the token among several cookies', () => { + expect(readTokenCookie(`a=1; ${TOKEN_COOKIE_NAME}=tok123; b=2`)).toBe('tok123'); + }); + + it('returns null for an empty value', () => { + expect(readTokenCookie(`${TOKEN_COOKIE_NAME}=`)).toBeNull(); + }); + + it('decodes percent-encoded values', () => { + expect(readTokenCookie(`${TOKEN_COOKIE_NAME}=${encodeURIComponent('a b=c')}`)).toBe('a b=c'); + }); +}); + +describe('buildTokenCookie', () => { + it('sets Path/Expires/SameSite and Secure on https', () => { + const cookie = buildTokenCookie('tok', { expiresAt: 1_900_000_000, secure: true }); + expect(cookie).toContain(`${TOKEN_COOKIE_NAME}=tok`); + expect(cookie).toContain('Path=/'); + expect(cookie).toContain(`Expires=${new Date(1_900_000_000 * 1000).toUTCString()}`); + expect(cookie).toContain('SameSite=Lax'); + expect(cookie).toContain('Secure'); + }); + + it('omits Secure on http (localhost dev)', () => { + const cookie = buildTokenCookie('tok', { expiresAt: 1_900_000_000, secure: false }); + expect(cookie).not.toContain('Secure'); + }); + + it('encodes the token value', () => { + expect(buildTokenCookie('a b', { expiresAt: 1_900_000_000, secure: true })).toContain( + `${TOKEN_COOKIE_NAME}=a%20b`, + ); + }); +}); + +describe('clearTokenCookie', () => { + it('expires the cookie immediately', () => { + const cookie = clearTokenCookie(); + expect(cookie).toContain(`${TOKEN_COOKIE_NAME}=;`); + expect(cookie).toContain('Expires=Thu, 01 Jan 1970'); + expect(cookie).toContain('Path=/'); + }); +}); + +describe('parseRedirectUri', () => { + it('accepts absolute https URLs', () => { + expect(parseRedirectUri('?redirect_uri=https%3A%2F%2Frc.example.com%2Fs%2Fabc')).toBe( + 'https://rc.example.com/s/abc', + ); + }); + + it('accepts http URLs (local tunnel dev)', () => { + expect(parseRedirectUri('?redirect_uri=http%3A%2F%2F127.0.0.1%3A8080%2F')).toBe( + 'http://127.0.0.1:8080/', + ); + }); + + it('rejects non-http schemes', () => { + expect(parseRedirectUri(`?redirect_uri=${encodeURIComponent('javascript:alert(1)')}`)).toBeNull(); + }); + + it('rejects missing or unparsable values', () => { + expect(parseRedirectUri('')).toBeNull(); + expect(parseRedirectUri('?redirect_uri=')).toBeNull(); + expect(parseRedirectUri('?redirect_uri=%2F%2Fevil.example.com')).toBeNull(); + expect(parseRedirectUri(`?redirect_uri=${encodeURIComponent('/relative/path')}`)).toBeNull(); + }); +}); + +describe('readLocaleCookie', () => { + it('returns null when the cookie is absent', () => { + expect(readLocaleCookie('')).toBeNull(); + expect(readLocaleCookie('other=1')).toBeNull(); + }); + + it('reads zh and en values among several cookies', () => { + expect(readLocaleCookie('KIMI_LOCALE=zh')).toBe('zh'); + expect(readLocaleCookie(`a=1; KIMI_LOCALE=en; ${TOKEN_COOKIE_NAME}=x`)).toBe('en'); + }); + + it('accepts regional variants', () => { + expect(readLocaleCookie('KIMI_LOCALE=zh-CN')).toBe('zh'); + expect(readLocaleCookie('KIMI_LOCALE=en-US')).toBe('en'); + }); + + it('ignores unknown values', () => { + expect(readLocaleCookie('KIMI_LOCALE=fr')).toBeNull(); + }); +}); diff --git a/apps/auth-login/tsconfig.json b/apps/auth-login/tsconfig.json new file mode 100644 index 000000000..cc45520bd --- /dev/null +++ b/apps/auth-login/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": false, + "types": ["vite/client", "unplugin-icons/types/vue"] + }, + "include": ["src/**/*.ts", "src/**/*.vue", "test/**/*.ts"] +} diff --git a/apps/auth-login/vite.config.ts b/apps/auth-login/vite.config.ts new file mode 100644 index 000000000..223bf047a --- /dev/null +++ b/apps/auth-login/vite.config.ts @@ -0,0 +1,40 @@ +import { defineConfig } from 'vite'; +import { kimiRendererViteConfig } from '@moonshot-ai/vite-preset'; +import { fileURLToPath } from 'node:url'; + +// Dev/preview port for the standalone auth-login page. +const port = Number(process.env.AUTH_LOGIN_PORT) || 5176; + +// Shared renderer preset (Vue plugin, unplugin-icons `kimi` collection sourced +// from app-client's icon dir, ES2022 target). This page talks directly to the +// OAuth host (CORS is open), so there is no dev proxy to layer on. +const preset = kimiRendererViteConfig({ + root: fileURLToPath(new URL('.', import.meta.url)), + iconsDir: fileURLToPath( + new URL('./src/icons/kimi', import.meta.resolve('@moonshot-ai/app-client/package.json')), + ), + defines: { + // This bundle never ships inside the desktop app (shared sources read the + // flag through a `typeof` guard, so this stays `false` everywhere). + __KIMI_WEB_DESKTOP__: JSON.stringify(false), + }, +}); + +export default defineConfig({ + ...preset, + // Relative asset URLs: the bundle can be mounted at any path on the RC + // tunnel host without a rebuild. + base: './', + build: { + ...preset.build, + outDir: 'dist', + emptyOutDir: true, + }, + server: { + port, + strictPort: false, + }, + preview: { + port: Number(process.env.AUTH_LOGIN_PREVIEW_PORT) || 4176, + }, +}); diff --git a/apps/desktop/src/renderer/style.css b/apps/desktop/src/renderer/style.css index fb09880f6..fc31b53d0 100644 --- a/apps/desktop/src/renderer/style.css +++ b/apps/desktop/src/renderer/style.css @@ -1,4 +1,5 @@ @import '@moonshot-ai/app-ui/style.css'; +@import '@moonshot-ai/app-ui/fonts.css'; /* ---- Minimal reset (replaces Tailwind preflight) ---- Just enough normalization so UA defaults don't leak through; the app's own diff --git a/apps/web/AGENTS.md b/apps/web/AGENTS.md index 6f869cc63..18211bd1d 100644 --- a/apps/web/AGENTS.md +++ b/apps/web/AGENTS.md @@ -9,7 +9,7 @@ The browser web UI for Kimi Code — a peer to the TUI in `apps/kimi-code`. It t ## Design system (normative — required when modifying the UI) - **Before changing any component, style, layout, or theme, read the design system view at `src/views/DesignSystemView.vue` (open it as an overlay: long-press the sidebar logo).** It is the canonical design system and visual spec for this app (tokens §02, primitives §03, chat §04, theme rules §05, style rules §06). It consumes the product tokens from `src/style.css` directly, so it stays in sync with the app. New and modified UI must match it. -- **Use the primitives from `@moonshot-ai/app-ui`** (source in `packages/app-ui`; import as `import { Button, Icon, … } from '@moonshot-ai/app-ui'`, and the app's `src/style.css` already imports `@moonshot-ai/app-ui/style.css` for the tokens). The library covers Button, IconButton, Badge, Pill, Card, Input/Select/Textarea/Field, Dialog, Spinner, Link, Menu/MenuItem, SegmentedControl, Tabs, Switch, Checkbox, Avatar, EmptyState, Divider, Tooltip, Banner, Sheet, Skeleton, CommandBar, TopBar. One semantic = one component — do not hand-roll a bespoke button/badge/dialog/input for a single screen. When a primitive replaces an element, **delete the old scoped CSS** (do not append override blocks). +- **Use the primitives from `@moonshot-ai/app-ui`** (source in `packages/app-ui`; import as `import { Button, Icon, … } from '@moonshot-ai/app-ui'`, and the app's `src/style.css` imports `@moonshot-ai/app-ui/style.css` for the tokens plus `@moonshot-ai/app-ui/fonts.css` for the self-hosted fonts — a page that only needs tokens may import `style.css` alone). The library covers Button, IconButton, Badge, Pill, Card, Input/Select/Textarea/Field, Dialog, Spinner, Link, Menu/MenuItem, SegmentedControl, Tabs, Switch, Checkbox, Avatar, EmptyState, Divider, Tooltip, Banner, Sheet, Skeleton, CommandBar, TopBar. One semantic = one component — do not hand-roll a bespoke button/badge/dialog/input for a single screen. When a primitive replaces an element, **delete the old scoped CSS** (do not append override blocks). - **Use the tokens, not ad-hoc values.** Colors, fonts, radii, spacing, shadows, z-index, and motion come from the CSS custom properties in `src/style.css` (catalogued in the design-system view §02). Canonical names are `--color-*` / `--radius-*` / `--space-*` / `--text-*` / `--font-*` / `--z-*` / `--shadow-*` / `--ease-*` / `--duration-*` / `--weight-*` / `--leading-*`, plus `--anim-*` for the designer-exported icon animation tracks (§02 Motion; verbatim Rive timings, intentionally outside the `--duration-*` ramp). A small set of layout/focus tokens keep the `--p-` prefix: `--p-focus-ring`, `--p-selection`, `--p-ic-sm/md/lg`, `--p-sidebar-w`, `--p-content-max/-wide`, `--p-table-max/-cell-max`, `--p-findbar-w`, `--p-findring-w`, `--p-hairline`, `--p-bp-sm/-md`. - **The chat working indicator (小蓝 mascot + phase label) is reserved** for the chat working state after a prompt is sent only, and is rendered solely by `chat/WorkingIndicator.vue`; every other loading state uses the plain `Spinner`. - **Run `pnpm --filter kimi-code-web check:style`** (`scripts/check-style.mjs`) — it enforces the §06 anti-pattern rules (no-gradient, no-glassmorphism except TopBar `frost` and the whitelisted menu surfaces via the `--p-menu-backdrop` token, no-emoji-icon, no-hardcoded-hex/font, radius/z/weight from scale). Do not add new violations. diff --git a/apps/web/src/style.css b/apps/web/src/style.css index 63798843a..94ac763b2 100644 --- a/apps/web/src/style.css +++ b/apps/web/src/style.css @@ -1,4 +1,5 @@ @import '@moonshot-ai/app-ui/style.css'; +@import '@moonshot-ai/app-ui/fonts.css'; /* ---- Minimal reset (replaces Tailwind preflight) ---- Just enough normalization so UA defaults don't leak through; the app's own diff --git a/docs/plans/2026-08-13-rc-auth-login.md b/docs/plans/2026-08-13-rc-auth-login.md new file mode 100644 index 000000000..7bfd01d64 --- /dev/null +++ b/docs/plans/2026-08-13-rc-auth-login.md @@ -0,0 +1,158 @@ +# Remote Control 鉴权登录页(apps/auth-login)实施方案 + +> 2026-08-13。Remote Control(RC)链路的鉴权中间页:只承载「Kimi Code OAuth 登录 + 重定向」,移动端优先、响应式兼容 PC。已拍板决策: +> +> 1. 页面放 **code-app 仓内**,新 app 目录名 **`apps/auth-login`**。 +> 2. 登录逻辑**复用 kimi-code 仓的 `@moonshot-ai/kimi-code-oauth` 包**(device code flow),浏览器直连 `auth.kimi.com`;token 存 cookie。 +> 3. cookie 契约:名 **`kimi-auth`**;回跳参数名 **`redirect_uri`**。 +> 4. UI 与 web/desktop 同源(`@moonshot-ai/app-ui` tokens + 原语,Kimi 风格)。 +> 5. kimi-code 侧所需的 oauth 包改动由本人完成,工作克隆选干净的那个(见下文)。 + +## 背景:Remote Control 链路 + +1. 远程机器本地执行 TUI 的 `/rc` 命令 → 启动本地 server + kimi code web 服务。 +2. 请求在线服务 `kimi.com/rc` → 把本地 ip:port 转发到公网。 +3. 远端用户(手机)访问公网地址 → **先经过一层鉴权**:用 Kimi Code 的 OAuth 登录,通过后才能访问。 + +本方案只覆盖第 3 步里的**鉴权中间页**:隧道入口发现无有效凭证 → 跳转到本页(带 `redirect_uri`)→ 本页完成 OAuth device flow → 写 cookie → 跳回 `redirect_uri`。隧道服务本身的鉴权校验、`/rc` 命令、`kimi.com/rc` 转发服务均不在本方案范围。 + +## 已验证的事实(2026-08-13 实测 / 代码引用为准) + +- **CORS 全通**:`OPTIONS https://auth.kimi.com/api/oauth/device_authorization` 与 `/api/oauth/token` 均返回 `access-control-allow-origin: *`,`content-type` / `x-msh-platform` 头放行。移动端浏览器直连 auth.kimi.com 无障碍。 +- **授权确认页已存在,不在本方案范围**:真实调用 `POST /api/oauth/device_authorization`(client_id `17e5f671-d194-4dfb-9706-5516cb48c098`)返回 `verification_uri = https://www.kimi.com/code/authorize_device`。用户在手机浏览器打开该页完成「登录账号 + 确认授权」,我们不做这个页面。实测该接口**不带 `X-Msh-*` 头也成功**,设备头可选。 +- **oauth 包的浏览器可用性**(`kimi-code/packages/oauth/src`): + - `oauth.ts` 的 `requestDeviceAuthorization` / `pollDeviceToken` / `refreshAccessToken` 是纯 `fetch` 封装;依赖闭包(`api-error.ts` / `errors.ts` / `types.ts` / `utils.ts` / `constants.ts`)均无 Node import——**浏览器安全**。 + - 但顶层 `index.ts` barrel 会拖进 `OAuthManager`(`node:fs` / `proper-lockfile`)、`identity.ts`(`node:os` / `node:child_process`)、`toolkit.ts`、`storage.ts`——**浏览器 import 包根会炸**,必须加浏览器安全的子路径 export。 + - `tsdown.config.ts` 单入口 `./src/index.ts`;加子路径需同步加 entry。 + - `KIMI_CODE_FLOW_CONFIG`(oauthHost + clientId)由 `constants.ts` 导出,纯模块,直接复用——授权页显示的就是 Kimi Code,语义正确。 +- **workspace**:`pnpm-workspace.yaml` 已含 `kimi-code/packages/*`,新 app 按包名 `workspace:*` 依赖即可,符合「只经包名 import」硬约束。 +- **状态机复用**:`useOAuthLoginFlow`(`packages/app-client/src/composables/useOAuthLoginFlow.ts`)是回调注入式 device-flow 状态机(starting → device-code → success/expired/error、倒计时、轮询三连败容错、disposed 清理),埋点 `track` 默认 no-op(`contracts.ts` 的 `noopProductTracker`)。新 app 直接复用,回调改为直连 oauth 包,零重写。 +- **既有约定要守住**:授权完成页读 `from=kimi_code_desktop` 渲染「打开 App」按钮并跳 `kimi-code://auth/success`(`apps/desktop/src/renderer/lib/loginSource.ts`、`apps/desktop/src/main/deep-link.ts`)。RC 场景**不带** `from` 参数(裸 URL),该按钮保持隐藏。 + +## kimi-code 侧改动(一个小 PR) + +**工作克隆选择**(2026-08-13 核查): + +| 克隆 | 分支 | 状态 | 结论 | +|---|---|---|---| +| kimi-code-2 | feat/oauth-region-split | 3 脏文件 | 有任务,不动 | +| kimi-code-3 | fix/sdk-retry-cancel | 干净但在任务分支 | 有任务,不动 | +| kimi-code-5 | auto-title | 4 脏文件 | 有任务,不动 | +| kimi-code-tips | feat/upgrade-reminder-banner | 2 脏文件 | 有任务,不动 | +| kimi-code-ky | main | 干净但不可用(拍板排除) | 不动 | +| kimi-code / kimi-code-4 | main | 干净、无未推送、node_modules 齐 | 候选 | + +**选用 `~/Desktop/moonshot/kimi-code-4`**(干净 main;备选 kimi-code),工作分支 `feat/oauth-device-subpath-export`。 + +改动(`packages/oauth`,均为新增导出,无行为变更): + +1. 新建 `src/device.ts`:re-export 浏览器安全闭包——`./oauth` 的三函数与 `DevicePollResult` 类型、`./constants` 的 `KIMI_CODE_FLOW_CONFIG`、`./types` 的相关类型(`DeviceAuthorization` / `TokenInfo` / `OAuthFlowConfig` / `OAuthRequestHeaders`)、`./errors` 的错误类。 +2. `package.json` `exports` 增加:`"./device": { "types": "./src/device.ts", "default": "./src/device.ts" }`。 +3. `tsdown.config.ts` `entry` 增加 `./src/device.ts`(dist 双产物)。 +4. 验证:`pnpm --filter @moonshot-ai/kimi-code-oauth typecheck`、`build`,以及 oauth 包既有测试全绿。 +5. PR 标题:`feat(oauth): add browser-safe ./device subpath export`(Conventional Commits,无署名)。 + +## code-app 侧改动 + +### 新 app `apps/auth-login` + +**形态**:Vue 3 + Vite + TS strict,无 router、无 Pinia,vue-i18n v11(Composition 模式,`legacy: false`,fallback `en`,跟随浏览器 locale)。pnpm workspace 成员,private。 + +**依赖**: + +- `@moonshot-ai/app-ui`(tokens + Button / Spinner / Icon 原语;`src/style.css` import `@moonshot-ai/app-ui/style.css`) +- `@moonshot-ai/app-client`(`useOAuthLoginFlow`) +- `@moonshot-ai/kimi-code-oauth`(**只经 `./device` 子路径** import) +- `vue` / `vue-i18n`;dev 依赖 `@moonshot-ai/vite-preset`、`@vitejs/plugin-vue`、`vite`、`typescript`、`vue-tsc`、`vitest` + +**vite 配置**:复用 `kimiRendererViteConfig`(iconsDir 指向 app-client 的 kimi 图标目录,同 apps/web 的做法);`base: './'`(产物可挂任意路径);**无 dev proxy**(浏览器直连 auth.kimi.com,无需代理);独立端口(如 `5176`,env `AUTH_LOGIN_PORT` 可调)。 + +**文件结构**: + +``` +apps/auth-login/ +├── package.json +├── vite.config.ts +├── tsconfig.json +├── index.html # viewport-fit=cover、页面标题、noindex +├── src/ +│ ├── main.ts # 启动 + i18n 安装 +│ ├── App.vue # 单页状态机 UI(复用 useOAuthLoginFlow) +│ ├── flow.ts # 回调适配:useOAuthLoginFlow ← oauth 包直连 +│ ├── auth-token.ts # cookie 读写 + redirect_uri 解析校验(纯函数,可测) +│ ├── style.css # import app-ui tokens + 页面级样式 +│ └── i18n/ +│ ├── index.ts +│ └── locales/{en,zh}.ts +└── test/ + └── auth-token.test.ts # cookie / redirect_uri 纯函数测试 +``` + +**页面状态机**(进入页面即跑): + +``` +1. checking:读 cookie kimi-auth + → 存在且未过期:有 redirect_uri → 立即 location.replace 回跳;无 → 显示「已登录,可关闭本页」 +2. 无/过期 → startFlow(): + requestDeviceAuthorization(flowConfig(), { deviceHeaders: 见「请求身份」节 }) +3. device-code: + - 主按钮「前往授权」新标签打开 verification_uri_complete(authorize_device 页) + - 展示 user_code + 复制授权链接(二次路径,同 LoginDialog 的 parked 思路) + - 倒计时(expires_in 1800s)+ 轮询(pollDeviceToken,server 建议 interval=5s,三连败转 error) +4. success:无条件写 cookie;有 redirect_uri → location.replace 回跳,无 → 停留显示「登录成功,可关闭本页」 +5. expired / error:提示 + 重试按钮(重新 startFlow) +``` + +**cookie 写入**(`auth-token.ts`): + +- `kimi-auth=`;`Path=/`;`Secure`;`SameSite=Lax`(顶级导航跳转需携带);`Expires` = token 的 `expiresAt`。 +- 只存 access token(拍板);过期后隧道侧 401 → 再跳回本页 → 重走 device flow(authorize_device 页有会话时确认一键完成,体验可接受)。 +- refresh token 不落 cookie;页面生命周期短(登录即跳走),无需 refresh 逻辑。 + +**请求身份(拍板)**: + +- `X-Msh-Platform: kimi_code_cli`(**拍板沿用 CLI 的值**,不新造 RC 专属 platform);`X-Msh-Version` = auth-login 自身版本;`X-Msh-Device-Id` = localStorage 持久化的 `crypto.randomUUID()`(首次访问生成,语义等价 CLI 的 device_id 文件)。`Device-Name` / `Device-Model` / `Os-Version` 浏览器拿不到准确值,不伪造、不带(实测不带头接口也正常,头是可选统计维度)。 +- `User-Agent`:**一定会传,且 JS 无法干预**——`User-Agent` 是浏览器 forbidden header,fetch 既改不了也删不掉,每个请求自动携带用户浏览器的原生 UA(iPhone Safari / Android Chrome 等)。Node 侧 CLI/desktop 经 `createKimiDefaultHeaders` 把产品 UA 注入 `deviceHeaders`(`kimi-code-cli/x.y.z`);浏览器里即使写这个键也会被 fetch 静默丢弃,所以页面不传。最终服务端看到的 RC web 流量 = `X-Msh-Platform: kimi_code_cli` + 浏览器原生 UA,按 UA 仍可区分出浏览器形态。 +- 自定义头触发 CORS preflight;`x-msh-platform` 已实测被服务端回显放行(动态回显),其余头实施时顺手验证。 + +**redirect_uri 处理(可选)**: + +- `redirect_uri` **不必填**:隧道场景由隧道跳转时拼在登录地址上带来;直接访问(无参)也正常走登录流程。 +- 仅允许 `https:` / `http:`(拒绝 `javascript:` 等);缺失或非法一律按「无跳转目标」宽容降级,不阻塞登录。 +- 授权成功后 **cookie 无条件写入**;有 `redirect_uri` 才 `location.replace` 回跳,无则停留显示「登录成功,可以关闭本页」。已持有有效 cookie 直接进入时同理:有参跳走,无参显示已登录态。 +- 隧道域名动态生成,不做白名单;token 只经 cookie 传递、不上 URL,跳转本身不带敏感信息,开放重定向风险可控。 + +**移动端 / 响应式**: + +- 页面无文本输入框(user_code 是展示),天然规避 iOS 聚焦缩放与键盘遮挡问题。 +- `viewport-fit=cover` + `env(safe-area-inset-*)` 上下安全区;触控目标 ≥ 44px。 +- 移动端纵向居中卡片全宽;`≥640px` 桌面居中窄卡片(约 400px),内容同构。 +- 同设备旅程:「前往授权」开新标签 → 授权 → 切回本标签时轮询已成功 → 自动跳走;授权页标签由用户自行关闭(与 desktop LoginDialog 同模式)。 +- 亮/暗双主题:跟随系统(`data-color-scheme` 模式,token 自动适配),亮暗两态都要视觉验证。 + +**样式纪律**:全部走 `--color-*` / `--space-*` / `--text-*` / `--radius-*` token,零 ad-hoc 值;UI 实现前读 `kimi-design-skill` 移动端规范与 `apps/desktop/src/renderer/views/DesignSystemView.vue`。 + +### 双仓工作流顺序 + +1. **kimi-code-4**:oauth 包子路径 export 改动 → typecheck/build/test → 推送分支、提 PR。 +2. **code-app 联调**:`kimi-code/` submodule `git fetch origin feat/oauth-device-subpath-export && git checkout `,auth-login 即可经 `./device` 子路径 import 开发。 +3. kimi-code PR merge 后:submodule checkout 到 main 的新 commit,`git add kimi-code` bump(单独 commit)。 +4. **auth-login 开发**:脚手架 → flow/cookie 纯函数 + 测试 → UI → i18n → 双主题视觉验证。 +5. 验证:`pnpm typecheck`、`pnpm lint`、auth-login 的 `vitest`;`pnpm dev` 起页面真实走一遍 device flow(桌面浏览器 + 手机各一遍),确认 cookie 写入与回跳。 + +### changeset + +auth-login 独立于 desktop(`kimi-code-app`)发版,产物为静态页、不进 release CI。初步判断本改动对 kimi-code-app 无用户可见变化、**不需要 changeset**;提 PR 前按 `changeset` skill 规则最终确认。 + +## 明确不做 + +- 不做授权确认页(`www.kimi.com/code/authorize_device` 已存在)。 +- 不动隧道服务鉴权逻辑、`/rc` 命令、`kimi.com/rc` 转发服务;cookie 契约以拍板为准。 +- 不接 telemetry(`track` 保持 no-op);不存 refresh token;不做登录态管理页。 + +## 风险与备注 + +- **CORS 后续收紧**:当前 `*` 且无凭证请求,风险低;若收紧,退路是 RC 服务同域反代 auth.kimi.com 接口(页面只改 baseURL)。 +- **authorize_device 页的移动端体验**归其所属团队;若发现该页移动端不适配,会影响整体旅程,需反馈——本方案不绕开它。 +- **cookie 域**:cookie 写在页面当前域,要求登录页与隧道入口同域部署(部署侧职责,产物用 `base: './'` 适配)。 +- **子路径 export 是新增面**:kimi-code 侧只加 export 不动行为,对 CLI/desktop 零影响。 diff --git a/kimi-code b/kimi-code index 43c68f58f..4a93f70aa 160000 --- a/kimi-code +++ b/kimi-code @@ -1 +1 @@ -Subproject commit 43c68f58f578c88d9f503afb72f12d343c2aa5c7 +Subproject commit 4a93f70aa2cf5f70a88b4f8eeb2e409aab2c8f59 diff --git a/package.json b/package.json index 0448c82aa..4ba886cbe 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "dev:web": "pnpm --filter kimi-code-web run dev", "sync:web": "node scripts/sync-web-to-kimi-code.mjs", "package:macos": "bash apps/desktop/scripts/package-local-macos.sh", - "typecheck": "pnpm --filter kimi-code-app run typecheck && pnpm --filter kimi-code-web run typecheck", + "typecheck": "pnpm --filter kimi-code-app run typecheck && pnpm --filter kimi-code-web run typecheck && pnpm --filter kimi-code-auth-login run typecheck", "lint": "oxlint --type-aware --config .oxlintrc.json", "test": "vitest run" }, diff --git a/packages/app-i18n/src/locales/en/login.ts b/packages/app-i18n/src/locales/en/login.ts index cd2d13336..9bc0b939c 100644 --- a/packages/app-i18n/src/locales/en/login.ts +++ b/packages/app-i18n/src/locales/en/login.ts @@ -30,4 +30,15 @@ export default { // Send gating (signed-in free account, no usable models) upgradeRequiredTitle: 'Upgrade required', upgradeRequiredMessage: 'Your account is on the free plan. Upgrade to a membership to start chatting with Kimi models.', + // Remote Control auth-login page (apps/auth-login) + rcSubtitle: 'Remote Control session', + rcChecking: 'Checking sign-in status…', + rcLead: 'This Remote Control session requires authorization. Sign in with your Kimi account to continue.', + rcAuthorize: 'Open authorization page', + rcUserCodeLabel: 'Authorization code', + rcSuccessHint: 'Redirecting…', + rcExpiredTitle: 'Authorization expired or declined', + rcStartErrorTitle: 'Could not start sign-in', + rcConnectionErrorHint: 'Check your connection and try again.', + rcSuccessNoRedirect: 'You can close this page now.', } as const; diff --git a/packages/app-i18n/src/locales/zh/login.ts b/packages/app-i18n/src/locales/zh/login.ts index 84b8d9095..ca61f9e10 100644 --- a/packages/app-i18n/src/locales/zh/login.ts +++ b/packages/app-i18n/src/locales/zh/login.ts @@ -30,4 +30,15 @@ export default { // Send gating (signed-in free account, no usable models) upgradeRequiredTitle: '请升级会员', upgradeRequiredMessage: '当前为免费账户,升级会员后即可使用 Kimi 模型开始对话。', + // Remote Control auth-login page (apps/auth-login) + rcSubtitle: '远程控制会话', + rcChecking: '正在检查登录状态…', + rcLead: '此远程控制会话需要授权。使用 Kimi 账号登录后即可继续。', + rcAuthorize: '打开授权页', + rcUserCodeLabel: '授权码', + rcSuccessHint: '正在跳转…', + rcExpiredTitle: '授权已过期或被取消', + rcStartErrorTitle: '无法开始登录', + rcConnectionErrorHint: '请检查网络连接后重试。', + rcSuccessNoRedirect: '现在可以关闭本页了。', } as const; diff --git a/packages/app-ui/package.json b/packages/app-ui/package.json index 6e14834f8..9f45dafc4 100644 --- a/packages/app-ui/package.json +++ b/packages/app-ui/package.json @@ -5,7 +5,8 @@ "type": "module", "exports": { ".": "./src/index.ts", - "./style.css": "./src/style.css" + "./style.css": "./src/style.css", + "./fonts.css": "./src/fonts.css" }, "dependencies": { "@moonshot-ai/app-i18n": "workspace:*" diff --git a/packages/app-ui/src/fonts.css b/packages/app-ui/src/fonts.css new file mode 100644 index 000000000..2b37ab7da --- /dev/null +++ b/packages/app-ui/src/fonts.css @@ -0,0 +1,29 @@ +/* Self-hosted variable fonts, generated once into ./assets/fonts by + scripts/prepare-fonts.mjs (gitignored) and fingerprinted by Vite into each + final build. Kept as a separate entry from style.css so font-free consumers + (e.g. apps/auth-login, which falls back to platform fonts) don't ship + ~7.5MB of CJK. Consumers that want the Kimi typefaces import this once, + alongside style.css. */ +@font-face { + font-family: "Noto Sans SC Variable"; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url("./assets/fonts/NotoSansSC[wght].woff2") format("woff2-variations"); +} + +@font-face { + font-family: "Schibsted Grotesk Variable"; + font-style: normal; + font-display: swap; + font-weight: 400 900; + src: url("./assets/fonts/SchibstedGrotesk[wght].woff2") format("woff2-variations"); +} + +@font-face { + font-family: "Schibsted Grotesk Variable"; + font-style: italic; + font-display: swap; + font-weight: 400 900; + src: url("./assets/fonts/SchibstedGrotesk-Italic[wght].woff2") format("woff2-variations"); +} diff --git a/packages/app-ui/src/style.css b/packages/app-ui/src/style.css index d6f9d44c2..8f0a27e1e 100644 --- a/packages/app-ui/src/style.css +++ b/packages/app-ui/src/style.css @@ -1,29 +1,6 @@ -/* UI variable fonts are generated once in app-ui so both renderer builds - resolve the same local assets. Vite fingerprints them into each final build. */ -@font-face { - font-family: "Noto Sans SC Variable"; - font-style: normal; - font-display: swap; - font-weight: 100 900; - src: url("./assets/fonts/NotoSansSC[wght].woff2") format("woff2-variations"); -} - -@font-face { - font-family: "Schibsted Grotesk Variable"; - font-style: normal; - font-display: swap; - font-weight: 400 900; - src: url("./assets/fonts/SchibstedGrotesk[wght].woff2") format("woff2-variations"); -} - -@font-face { - font-family: "Schibsted Grotesk Variable"; - font-style: italic; - font-display: swap; - font-weight: 400 900; - src: url("./assets/fonts/SchibstedGrotesk-Italic[wght].woff2") format("woff2-variations"); -} - +/* Design tokens + shared styles. The self-hosted variable fonts live in + ./fonts.css, a separate entry so font-free consumers don't ship ~7.5MB of + CJK — consumers that want the Kimi typefaces import it alongside this file. */ :root { --dim: rgba(0, 0, 0, 0.6); --muted: rgba(0, 0, 0, 0.45); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8dc3cc49..6fc369f5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,52 @@ importers: specifier: 2.0.0 version: 2.0.0 + apps/auth-login: + dependencies: + '@moonshot-ai/app-client': + specifier: workspace:* + version: link:../../packages/app-client + '@moonshot-ai/app-core': + specifier: workspace:* + version: link:../../packages/app-core + '@moonshot-ai/app-i18n': + specifier: workspace:* + version: link:../../packages/app-i18n + '@moonshot-ai/app-ui': + specifier: workspace:* + version: link:../../packages/app-ui + '@moonshot-ai/kimi-code-oauth': + specifier: workspace:* + version: link:../../kimi-code/packages/oauth + vue: + specifier: ^3.5.35 + version: 3.5.39(typescript@6.0.2) + vue-i18n: + specifier: ^11.4.5 + version: 11.4.6(vue@3.5.39(typescript@6.0.2)) + devDependencies: + '@moonshot-ai/vite-preset': + specifier: workspace:* + version: link:../../packages/vite-preset + '@vitejs/plugin-vue': + specifier: ^5.2.4 + version: 5.2.4(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.0)(yaml@2.9.0))(vue@3.5.39(typescript@6.0.2)) + typescript: + specifier: 6.0.2 + version: 6.0.2 + unplugin-icons: + specifier: ^23.0.0 + version: 23.0.1(@vue/compiler-sfc@3.5.39) + vite: + specifier: ^6.3.3 + version: 6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.0)(yaml@2.9.0) + vitest: + specifier: 4.1.4 + version: 4.1.4(@types/node@24.13.3)(vite@6.4.3(@types/node@24.13.3)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.0)(yaml@2.9.0)) + vue-tsc: + specifier: ~3.2.0 + version: 3.2.9(typescript@6.0.2) + apps/desktop: dependencies: '@xterm/addon-web-links':