feat(web): ship kimi-web2 as the bundled web assets on this branch

- copy-web-assets.mjs now copies the static apps/kimi-web2 app into
  dist-web (no Vue build step needed); dev-only files are excluded.
- live.js: on http(s) pages, try the same-origin server even without a
  token; a 401 opens the token dialog, which persists the token and
  reloads — so the server-hosted build connects out of the box.
- Verified end-to-end against a protected local server (401 -> token
  dialog -> live workspaces/sessions/history over REST + WS).
This commit is contained in:
qer 2026-07-05 11:42:09 +08:00
parent f662febd2c
commit 92acd07452
6 changed files with 52 additions and 19 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": minor
---
web: Replace the bundled web UI with the design-first kimi-web2 prototype built from the Kimi Design System. Run kimi web and open the printed URL to try it.

View file

@ -49,7 +49,7 @@
"provenance": true
},
"scripts": {
"build": "pnpm -C ../kimi-web run build && tsdown && node scripts/copy-native-assets.mjs && node scripts/copy-web-assets.mjs",
"build": "tsdown && node scripts/copy-native-assets.mjs && node scripts/copy-web-assets.mjs",
"prebuild": "node scripts/build-vis-asset.mjs",
"catalog:update": "node scripts/update-catalog.mjs --out dist/built-in-catalog.json",
"smoke": "node scripts/smoke.mjs",

View file

@ -1,27 +1,33 @@
import { cp, rm, stat } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { basename, dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const repoRoot = resolve(appRoot, '../..');
const source = resolve(repoRoot, 'apps/kimi-web/dist');
// This branch ships the design-first kimi-web2 UI as the bundled web assets.
// kimi-web2 is a static, no-build app, so it is copied directly (no dist step).
const source = resolve(repoRoot, 'apps/kimi-web2');
const target = resolve(appRoot, 'dist-web');
async function assertBuiltWeb() {
// Dev/docs files that should not ship in the web asset bundle.
const EXCLUDE = new Set(['serve.mjs', 'README.md', 'CONVENTIONS.md']);
async function assertWebSource() {
try {
const info = await stat(resolve(source, 'index.html'));
if (!info.isFile()) {
throw new Error('index.html is not a file');
}
} catch {
throw new Error(
`Kimi web build output was not found at ${source}. Run \`pnpm --filter @moonshot-ai/kimi-web run build\` first.`,
);
throw new Error(`Kimi web assets were not found at ${source}.`);
}
}
await assertBuiltWeb();
await assertWebSource();
await rm(target, { recursive: true, force: true });
await cp(source, target, { recursive: true });
await cp(source, target, {
recursive: true,
filter: (src) => !EXCLUDE.has(basename(src)),
});
console.log(`Copied Kimi web assets to ${target}`);
console.log(`Copied Kimi web assets (kimi-web2) to ${target}`);

View file

@ -517,9 +517,11 @@
/* ----------------------------- server auth ---------------------------- */
function connectToken() {
S.set({ authed: true });
closeOverlays();
toast('已连接stub', 'success');
var inp = $('[data-token-input]');
var v = inp && inp.value.trim();
if (!v) { toast('请输入 Token', 'error'); return; }
try { localStorage.setItem('kimi2-token', v); } catch (err) {}
location.reload();
}
/* ----------------------------- delegated clicks ----------------------- */

View file

@ -459,7 +459,7 @@
<div class="overlay" data-overlay="serverauth">
<section class="modal confirm-modal" role="dialog" aria-modal="true">
<div class="confirm-title">需要连接令牌</div>
<div class="confirm-msg">此服务已开启保护。请输入本地服务启动时打印的 bearer token(原型中任意值均可通过)</div>
<div class="confirm-msg">此服务已开启保护。请输入本地服务启动时打印的 bearer token。</div>
<div class="field" style="margin-top:14px">
<svg><use href="#i-shield"/></svg>
<input data-token-input type="password" placeholder="Token" autocomplete="off">

View file

@ -64,7 +64,11 @@
if (qsToken) { try { localStorage.setItem('kimi2-token', qsToken); } catch (e) {} }
var token = qsToken || safeGet('kimi2-token');
if (!token) return; // no token → stay in stub mode, untouched.
var httpPage = location.protocol.indexOf('http') === 0;
// file:// without a token stays pure stub; an http(s) page always tries the
// server — the same origin may be unprotected, and a 401 opens the token
// dialog (wired in app.js) so the server-hosted build works out of the box.
if (!token && !httpPage) return;
// Prefer explicit ?server=; else same-origin (proxy-friendly, no CORS need);
// else the default local server (e.g. when opened via file://).
@ -77,18 +81,27 @@
function toast(msg, kind) { if (window.KP && window.KP.toast) window.KP.toast(msg, kind); }
/* ------------------------------ REST ---------------------------------- */
function authHeaders(extra) {
var h = extra || {};
if (token) h.Authorization = 'Bearer ' + token;
return h;
}
function get(path) {
return fetch(api(path), { headers: { Authorization: 'Bearer ' + token } })
.then(unwrap);
return fetch(api(path), { headers: authHeaders() }).then(unwrap);
}
function post(path, body) {
return fetch(api(path), {
method: 'POST',
headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' },
headers: authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify(body || {}),
}).then(unwrap);
}
function unwrap(res) {
if (res.status === 401 || res.status === 403) {
var e = new Error('未授权(需要服务器 Token');
e.auth = true;
throw e;
}
return res.json().then(function (env) {
if (env.code !== 0) throw new Error(env.msg || ('服务端错误 ' + env.code));
return env.data;
@ -228,6 +241,13 @@
toast('已连接服务器 ' + origin.replace(/^https?:\/\//, ''), 'success');
})
.catch(function (err) {
if (err && err.auth) {
// Protected server, no/invalid token → ask for one (app.js persists it
// to localStorage and reloads).
console.warn('[live] 服务器需要 Token');
if (window.KP && window.KP.openOverlay) window.KP.openOverlay('serverauth');
return;
}
console.warn('[live] 连接服务器失败,保持离线模式:', err);
toast('连接服务器失败,使用离线数据', 'error');
});
@ -296,7 +316,7 @@
}
function connectWs() {
var url = origin.replace(/^http/, 'ws') + '/api/v1/ws?client_id=' + encodeURIComponent(clientId);
try { ws = new WebSocket(url, ['kimi-code.bearer.' + token]); }
try { ws = new WebSocket(url, token ? ['kimi-code.bearer.' + token] : undefined); }
catch (e) { console.warn('[live] WS 创建失败:', e); return; }
ws.onmessage = function (ev) {
var f;