From 57f1eec340872a485ebdbb03bbd705ad84909ce1 Mon Sep 17 00:00:00 2001 From: qer Date: Sat, 18 Jul 2026 18:06:49 +0800 Subject: [PATCH 1/2] feat(vis): show LAN URLs when binding to 0.0.0.0 for remote control When vis-server binds to 0.0.0.0 or :: (all interfaces), the startup banner and CLI output now display the actual LAN IP addresses that other devices on the same network can use to connect. This enables lan-range remote control from phones, tablets, or other machines. Changes: - Add isAllInterfaces() and getLocalNetworkAddresses() to config.ts - Add lanUrls field to StartedVisServer in start.ts - Update formatStartupBanner() to list LAN URLs when applicable - Update vis CLI handler to print LAN access URLs - Add unit tests for isAllInterfaces and getLocalNetworkAddresses --- .changeset/lan-remote-control.md | 11 +++++++ apps/kimi-code/src/cli/sub/vis.ts | 7 +++++ apps/vis/server/src/config.ts | 17 +++++++++++ apps/vis/server/src/index.ts | 4 +-- apps/vis/server/src/start.ts | 4 ++- apps/vis/server/src/startup-banner.ts | 13 +++++++-- apps/vis/server/test/lib/config.test.ts | 38 ++++++++++++++++++++++++- 7 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 .changeset/lan-remote-control.md diff --git a/.changeset/lan-remote-control.md b/.changeset/lan-remote-control.md new file mode 100644 index 0000000000..83ff9d633e --- /dev/null +++ b/.changeset/lan-remote-control.md @@ -0,0 +1,11 @@ +--- +"@moonshot-ai/kimi-code": patch +"@moonshot-ai/vis-server": patch +--- + +feat(vis): show LAN URLs when binding to 0.0.0.0 for remote control + +When vis-server binds to 0.0.0.0 or :: (all interfaces), the startup +banner and CLI output now display the actual LAN IP addresses that +other devices on the same network can use to connect. This enables +lan-range remote control from phones, tablets, or other machines. diff --git a/apps/kimi-code/src/cli/sub/vis.ts b/apps/kimi-code/src/cli/sub/vis.ts index 7e6f2e1f25..6fea80b985 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 0af5b9f32d..ed258c2195 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 0047337367..80ef2b47b5 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 1a33cf2ae0..f305f02b81 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 a32589e89b..d2c2c457f2 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; } diff --git a/apps/vis/server/test/lib/config.test.ts b/apps/vis/server/test/lib/config.test.ts index e0f064fe59..c938c82053 100644 --- a/apps/vis/server/test/lib/config.test.ts +++ b/apps/vis/server/test/lib/config.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { hostForUrl } from '../../src/config'; +import { hostForUrl, isAllInterfaces, getLocalNetworkAddresses } from '../../src/config'; describe('hostForUrl', () => { it('brackets a bare IPv6 literal for use in a URL', () => { @@ -18,3 +18,39 @@ describe('hostForUrl', () => { expect(hostForUrl('[::1]')).toBe('[::1]'); }); }); + +describe('isAllInterfaces', () => { + it('returns true for 0.0.0.0', () => { + expect(isAllInterfaces('0.0.0.0')).toBe(true); + }); + + it('returns true for ::', () => { + expect(isAllInterfaces('::')).toBe(true); + }); + + it('returns false for loopback hosts', () => { + expect(isAllInterfaces('127.0.0.1')).toBe(false); + expect(isAllInterfaces('localhost')).toBe(false); + expect(isAllInterfaces('::1')).toBe(false); + }); +}); + +describe('getLocalNetworkAddresses', () => { + it('returns non-empty array of IPv4 URLs for a valid port', () => { + const addresses = getLocalNetworkAddresses(3001); + expect(addresses.length).toBeGreaterThan(0); + for (const addr of addresses) { + expect(addr).toMatch(/^http:\/\/\d+\.\d+\.\d+\.\d+:3001\/$/); + } + }); + + it('returns different URLs for different ports', () => { + const addrs3001 = getLocalNetworkAddresses(3001); + const addrs8080 = getLocalNetworkAddresses(8080); + expect(addrs3001.length).toBe(addrs8080.length); + for (let i = 0; i < addrs3001.length; i++) { + expect(addrs3001[i]).toContain(':3001/'); + expect(addrs8080[i]).toContain(':8080/'); + } + }); +}); From ff32fd9da4c8afec0670cd2eb7f5473ecf95c0f3 Mon Sep 17 00:00:00 2001 From: qer Date: Sun, 19 Jul 2026 18:03:50 +0800 Subject: [PATCH 2/2] feat: guide AI to use ReadMediaFile for video analysis instead of manual frame extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds explicit guidance in system prompt to prefer ReadMediaFile tool over writing Python/ffmpeg scripts when analyzing video content. This prevents inefficient manual frame extraction and leverages built-in multimodal capabilities. - Modified system.md General Guidelines for Research and Data Processing - Target task: Kimi CLI 视频分析希望默认调用 ReadMediaFile 而不是写 Python 切帧 --- packages/agent-core/src/profile/default/system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/agent-core/src/profile/default/system.md b/packages/agent-core/src/profile/default/system.md index 0671cd1084..e5b9b34187 100644 --- a/packages/agent-core/src/profile/default/system.md +++ b/packages/agent-core/src/profile/default/system.md @@ -58,7 +58,7 @@ The user may ask you to research on certain topics, process or generate certain - Understand the user's requirements thoroughly, ask for clarification before you start if needed. - Make plans before doing deep or wide research, to ensure you are always on track. - Search on the Internet if possible, with carefully-designed search queries to improve efficiency and accuracy. -- Use proper tools or shell commands or Python packages to process or generate images, videos, PDFs, docs, spreadsheets, presentations, or other multimedia files. Detect if there are already such tools in the environment. If you have to install third-party tools/packages, you MUST ensure that they are installed in a virtual/isolated environment. +- Use proper tools or shell commands or Python packages to process or generate images, PDFs, docs, spreadsheets, presentations, or other multimedia files. **For video files, prefer using the `ReadMediaFile` tool to read them directly rather than writing Python scripts or ffmpeg commands to extract frames manually.** Detect if there are already such tools in the environment. If you have to install third-party tools/packages, you MUST ensure that they are installed in a virtual/isolated environment. - Once you generate or edit any images, videos or other media files, try to read it again before proceed, to ensure that the content is as expected. - Avoid installing or deleting anything to/from outside of the current working directory. If you have to do so, ask the user for confirmation.