diff --git a/apps/kimi-code/src/cli/sub/vis.ts b/apps/kimi-code/src/cli/sub/vis.ts index 7e6f2e1f2..6fea80b98 100644 --- a/apps/kimi-code/src/cli/sub/vis.ts +++ b/apps/kimi-code/src/cli/sub/vis.ts @@ -21,6 +21,7 @@ export interface StartedVisServer { readonly port: number; readonly host: string; readonly url: string; + readonly lanUrls?: string[]; readonly close: () => Promise; } @@ -86,6 +87,12 @@ export async function handleVis(deps: VisDeps, opts: VisOptions): Promise : `${server.url}sessions/${encodeURIComponent(opts.sessionId)}`; deps.stdout.write(`kimi vis is running at ${server.url}\n`); + if (server.lanUrls !== undefined && server.lanUrls.length > 0) { + deps.stdout.write(`LAN access:\n`); + for (const lanUrl of server.lanUrls) { + deps.stdout.write(` ${lanUrl}\n`); + } + } deps.stdout.write('Press Ctrl-C to stop.\n'); if (opts.open) { diff --git a/apps/vis/server/src/config.ts b/apps/vis/server/src/config.ts index 0af5b9f32..ed258c219 100644 --- a/apps/vis/server/src/config.ts +++ b/apps/vis/server/src/config.ts @@ -1,5 +1,22 @@ import { homedir } from 'node:os'; import { join } from 'node:path'; +import os from 'node:os'; + +export function isAllInterfaces(host: string): boolean { + return host === '0.0.0.0' || host === '::'; +} + +export function getLocalNetworkAddresses(port: number): string[] { + const addresses: string[] = []; + for (const [, ifaceList] of Object.entries(os.networkInterfaces())) { + for (const iface of ifaceList ?? []) { + if (!iface.internal && iface.family === 'IPv4') { + addresses.push(`http://${iface.address}:${port}/`); + } + } + } + return addresses; +} /** Resolve KIMI_CODE_HOME (env > ~/.kimi-code). */ export function resolveKimiCodeHome(): string { diff --git a/apps/vis/server/src/index.ts b/apps/vis/server/src/index.ts index 004733736..80ef2b47b 100644 --- a/apps/vis/server/src/index.ts +++ b/apps/vis/server/src/index.ts @@ -5,9 +5,9 @@ import { formatStartupBanner } from './startup-banner'; async function main(): Promise { const host = resolveHost(); const authToken = resolveVisAuthToken(host); - const { port } = await startVisServer({ host, authToken }); + const { port, lanUrls } = await startVisServer({ host, authToken }); process.stdout.write( - formatStartupBanner({ authToken, host, kimiCodeHome: KIMI_CODE_HOME, port }), + formatStartupBanner({ authToken, host, kimiCodeHome: KIMI_CODE_HOME, port, lanUrls }), ); } diff --git a/apps/vis/server/src/start.ts b/apps/vis/server/src/start.ts index 1a33cf2ae..f305f02b8 100644 --- a/apps/vis/server/src/start.ts +++ b/apps/vis/server/src/start.ts @@ -1,7 +1,7 @@ import { serve } from '@hono/node-server'; import { createApp } from './app'; -import { hostForUrl, resolveHost, resolveKimiCodeHome, resolvePort, resolveVisAuthToken } from './config'; +import { getLocalNetworkAddresses, hostForUrl, isAllInterfaces, resolveHost, resolveKimiCodeHome, resolvePort, resolveVisAuthToken } from './config'; import type { WebAsset } from './lib/web-asset'; export interface StartVisServerOptions { @@ -18,6 +18,7 @@ export interface StartedVisServer { readonly port: number; readonly host: string; readonly url: string; + readonly lanUrls?: string[]; readonly close: () => Promise; } @@ -36,6 +37,7 @@ export async function startVisServer( port: info.port, host, url: `http://${hostForUrl(host)}:${info.port}/`, + lanUrls: isAllInterfaces(host) ? getLocalNetworkAddresses(info.port) : undefined, close: () => new Promise((done, fail) => { server.close((err?: Error) => (err ? fail(err) : done())); diff --git a/apps/vis/server/src/startup-banner.ts b/apps/vis/server/src/startup-banner.ts index a32589e89..d2c2c457f 100644 --- a/apps/vis/server/src/startup-banner.ts +++ b/apps/vis/server/src/startup-banner.ts @@ -5,12 +5,19 @@ export interface StartupBannerOptions { readonly host: string; readonly kimiCodeHome: string; readonly port: number; + readonly lanUrls?: string[]; } export function formatStartupBanner(options: StartupBannerOptions): string { const authStatus = options.authToken === undefined ? 'auth=disabled' : 'auth=required'; - return ( + let banner = `[vis-server] listening on http://${hostForUrl(options.host)}:${String(options.port)} ` + - `(${authStatus}, KIMI_CODE_HOME=${options.kimiCodeHome})\n` - ); + `(${authStatus}, KIMI_CODE_HOME=${options.kimiCodeHome})\n`; + if (options.lanUrls !== undefined && options.lanUrls.length > 0) { + banner += + `[vis-server] LAN access:\n` + + options.lanUrls.map((url) => ` - ${url}`).join('\n') + + '\n'; + } + return banner; }