From 839a66ff5d98c18b6660ab8c4d5fbf93fe2be8fb Mon Sep 17 00:00:00 2001 From: billowliu2 Date: Sun, 2 Aug 2026 16:40:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=8D=B8=E8=BD=BD?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E4=B8=8E=E7=BB=9F=E4=B8=80=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit uninstall.mjs 一键清理 installed.json/managed 副本/含密钥配置目录(--yes/--dry-run,幂等);scripts/bump-version.mjs 以 package.json 为唯一版本来源同步 kimi.plugin.json,挂 npm version 钩子;vision.mjs VERSION 运行时读 package.json,并修复 modelsEndpoint 对 /chat/completions 结尾的边界。1.0.5 → 1.0.6。 --- kimi.plugin.json | 14 ++- mcp/vision.mjs | 16 +++- package.json | 6 +- scripts/bump-version.mjs | 53 ++++++++++++ uninstall.mjs | 179 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 263 insertions(+), 5 deletions(-) create mode 100644 scripts/bump-version.mjs create mode 100644 uninstall.mjs diff --git a/kimi.plugin.json b/kimi.plugin.json index 5e006e3..b039afa 100644 --- a/kimi.plugin.json +++ b/kimi.plugin.json @@ -1,8 +1,14 @@ { "name": "kimi-eyes", - "version": "1.0.0", + "version": "1.0.6", "description": "Give non-multimodal models vision: analyze local images and clipboard screenshots through a user-configured VLM API (OpenAI-compatible or Anthropic protocol).", - "keywords": ["vision", "image", "screenshot", "multimodal", "vlm"], + "keywords": [ + "vision", + "image", + "screenshot", + "multimodal", + "vlm" + ], "author": "kimi-eyes", "license": "MIT", "interface": { @@ -14,7 +20,9 @@ "mcpServers": { "kimi-eyes": { "command": "node", - "args": ["./mcp/server.mjs"], + "args": [ + "./mcp/server.mjs" + ], "cwd": "./" } } diff --git a/mcp/vision.mjs b/mcp/vision.mjs index 733ba3c..480455d 100644 --- a/mcp/vision.mjs +++ b/mcp/vision.mjs @@ -10,7 +10,19 @@ import { execFileSync } from 'node:child_process'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export const MODELS_DB_PATH = path.join(__dirname, 'models-db.json'); -export const VERSION = '1.0.2'; +// Single source of truth for the version: package.json. +// Shared by the MCP server (serverInfo.version) and any consumer of this module. +function readPackageVersion() { + try { + const pkg = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8') + ); + return typeof pkg.version === 'string' && pkg.version ? pkg.version : '0.0.0'; + } catch { + return '0.0.0'; + } +} +export const VERSION = readPackageVersion(); export const EXT_MEDIA = { png: 'image/png', @@ -145,6 +157,8 @@ export function anthropicEndpoint(baseUrl) { export function modelsEndpoint(baseUrl, protocol) { let b = String(baseUrl || '').trim().replace(/\/+$/, ''); if (!b) return ''; + // 用户可能把完整 chat/completions 端点当 BaseUrl 填(与 openaiEndpoint 处理保持一致) + if (/\/chat\/completions$/i.test(b)) b = b.replace(/\/chat\/completions$/i, ''); if (/\/v\d+$/i.test(b)) return `${b}/models`; return `${b}/v1/models`; } diff --git a/package.json b/package.json index e14c9f2..559e911 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kimi-eyes", - "version": "1.0.5", + "version": "1.0.6", "description": "Give non-multimodal models in Kimi Code the ability to analyze images and screenshots via a user-configured VLM (OpenAI-compatible or Anthropic protocol).", "keywords": [ "kimi", @@ -18,6 +18,7 @@ "type": "module", "bin": { "kimi-eyes": "setup.mjs", + "kimi-eyes-uninstall": "uninstall.mjs", "kimi-eyes-mcp": "mcp/server.mjs" }, "files": [ @@ -26,6 +27,7 @@ "SYSTEM.md", "kimi.plugin.json", "setup.mjs", + "uninstall.mjs", "README.md", "README.en.md", "LICENSE" @@ -35,7 +37,9 @@ }, "scripts": { "setup": "node setup.mjs", + "uninstall": "node uninstall.mjs", "sync:models": "node scripts/sync-models.mjs", + "version": "node scripts/bump-version.mjs", "prepublishOnly": "node scripts/sync-models.mjs --allow-fail" } } diff --git a/scripts/bump-version.mjs b/scripts/bump-version.mjs new file mode 100644 index 0000000..7d23b15 --- /dev/null +++ b/scripts/bump-version.mjs @@ -0,0 +1,53 @@ +#!/usr/bin/env node +// kimi-eyes — 版本同步脚本。package.json 是唯一版本来源。 +// +// 用法: +// node scripts/bump-version.mjs 从 package.json 同步 kimi.plugin.json +// node scripts/bump-version.mjs 1.0.6 更新 package.json 并同步 kimi.plugin.json +// +// 作为 npm "version" 钩子运行时(npm version xxx 改完包版本后自动触发), +// 无需参数,自动把新版本同步到 kimi.plugin.json。 +// mcp/vision.mjs 的 VERSION 在运行时直接读取 package.json,无需同步。 + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const root = path.resolve(__dirname, '..'); +const pkgPath = path.join(root, 'package.json'); +const pluginPath = path.join(root, 'kimi.plugin.json'); + +const SEMVER = /^\d+\.\d+\.\d+$/; + +function readJson(p) { + return JSON.parse(fs.readFileSync(p, 'utf8')); +} +function writeJson(p, obj) { + fs.writeFileSync(p, `${JSON.stringify(obj, null, 2)}\n`, 'utf8'); +} + +const arg = process.argv[2]; +const pkg = readJson(pkgPath); + +if (arg) { + if (!SEMVER.test(arg)) { + console.error(`[bump-version] 非法版本号: ${arg}(需要 semver 格式,如 1.0.6)`); + process.exit(1); + } + pkg.version = arg; + writeJson(pkgPath, pkg); + console.log(`[bump-version] package.json -> ${arg}`); +} else { + console.log(`[bump-version] package.json 当前版本: ${pkg.version}`); +} + +// 同步到 kimi.plugin.json +const plugin = readJson(pluginPath); +if (plugin.version === pkg.version) { + console.log(`[bump-version] kimi.plugin.json 已一致 (${pkg.version})`); +} else { + plugin.version = pkg.version; + writeJson(pluginPath, plugin); + console.log(`[bump-version] kimi.plugin.json -> ${pkg.version}`); +} diff --git a/uninstall.mjs b/uninstall.mjs new file mode 100644 index 0000000..1c926ca --- /dev/null +++ b/uninstall.mjs @@ -0,0 +1,179 @@ +#!/usr/bin/env node +// kimi-eyes uninstall — 撤销 setup.mjs + `/plugins install` 的所有落盘痕迹。 +// Zero-dependency: 仅内置模块;交互确认 TTY 用 readline,非 TTY 走默认值。 +// +// 清理范围(KIMI_CODE_HOME 或 ~/.kimi-code): +// [1] plugins/installed.json 中的 kimi-eyes 条目(改前备份) +// [2] plugins/managed/kimi-eyes/ 安装副本 +// [3] kimi-eyes/config.json 配置目录(含 VLM API Key,需二次确认) +// +// 用法: +// node uninstall.mjs 交互确认 +// node uninstall.mjs --yes 跳过全部确认(脚本化) +// node uninstall.mjs --dry-run 只预览将要执行的操作 + +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import readline from 'node:readline/promises'; +import process from 'node:process'; + +const args = process.argv.slice(2); +const YES = args.includes('--yes'); +const DRY_RUN = args.includes('--dry-run'); + +const PLUGIN_ID = 'kimi-eyes'; +const home = process.env.KIMI_CODE_HOME || path.join(os.homedir(), '.kimi-code'); +const installedJsonPath = path.join(home, 'plugins', 'installed.json'); +const managedDir = path.join(home, 'plugins', 'managed', PLUGIN_ID); +const configDir = path.join(home, PLUGIN_ID); + +// --------------------------------------------------------------------------- +// 交互确认(TTY 用 readline;非 TTY / --yes 返回默认值) +// --------------------------------------------------------------------------- + +async function confirm(query, def = false) { + if (YES) return true; + if (!process.stdin.isTTY) return def; + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + try { + const ans = (await rl.question(query)).trim().toLowerCase(); + if (ans === 'y' || ans === 'yes') return true; + if (ans === 'n' || ans === 'no') return false; + return def; + } finally { + rl.close(); + } +} + +// --------------------------------------------------------------------------- +// 检查残留 +// --------------------------------------------------------------------------- + +function installedEntry() { + if (!fs.existsSync(installedJsonPath)) return null; + try { + const data = JSON.parse(fs.readFileSync(installedJsonPath, 'utf8')); + const list = Array.isArray(data?.plugins) ? data.plugins : []; + return list.find((p) => p?.id === PLUGIN_ID) ?? null; + } catch { + return null; // JSON 无效时按无条目处理,但保留文件不动 + } +} + +const hasInstalledEntry = () => installedEntry() !== null; +const hasManagedDir = () => fs.existsSync(managedDir); +const hasConfigDir = () => fs.existsSync(configDir); + +// --------------------------------------------------------------------------- +// 执行清理 +// --------------------------------------------------------------------------- + +function removeInstalledEntry() { + if (!hasInstalledEntry()) return { status: 'skip', detail: 'installed.json 无 kimi-eyes 条目' }; + try { + const data = JSON.parse(fs.readFileSync(installedJsonPath, 'utf8')); + if (!Array.isArray(data?.plugins)) throw new Error('plugins 字段缺失或非数组'); + const before = data.plugins.length; + data.plugins = data.plugins.filter((p) => p?.id !== PLUGIN_ID); + if (data.plugins.length === before) return { status: 'skip', detail: 'installed.json 无 kimi-eyes 条目' }; + if (DRY_RUN) return { status: 'would-remove', detail: `移除 ${before} → ${data.plugins.length} 个插件条目` }; + const backup = `${installedJsonPath}.bak.uninstall-${new Date().toISOString().replace(/[:.]/g, '-')}`; + fs.copyFileSync(installedJsonPath, backup); + fs.writeFileSync(installedJsonPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8'); + return { status: 'done', detail: `已移除条目,备份: ${path.basename(backup)}` }; + } catch (err) { + return { status: 'error', detail: `installed.json 处理失败,未改动: ${err.message}` }; + } +} + +function removeManagedDir() { + if (!hasManagedDir()) return { status: 'skip', detail: 'managed 安装副本不存在' }; + if (DRY_RUN) return { status: 'would-remove', detail: `删除目录 ${managedDir}` }; + try { + fs.rmSync(managedDir, { recursive: true, force: true }); + if (fs.existsSync(managedDir)) { + // 目录壳被占用(如插件正被会话加载),内容通常已清空 + return { status: 'warn', detail: `目录被占用未完全删除,重启 kimi-code 会话后执行: rmdir "${managedDir}"` }; + } + return { status: 'done', detail: '已删除安装副本' }; + } catch (err) { + return { status: 'warn', detail: `删除失败(可能被会话占用),重启后手动删除: ${err.message}` }; + } +} + +function removeConfigDir() { + if (!hasConfigDir()) return { status: 'skip', detail: '配置目录不存在' }; + if (DRY_RUN) return { status: 'would-remove', detail: `删除配置目录 ${configDir}(含 VLM API Key)` }; + try { + fs.rmSync(configDir, { recursive: true, force: true }); + return { status: 'done', detail: '已删除配置目录(含 VLM API Key)' }; + } catch (err) { + return { status: 'warn', detail: `删除失败: ${err.message}` }; + } +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +async function main() { + console.log('=== Kimi Eyes 卸载 ==='); + console.log(`操作目录: ${home}\n`); + + if (DRY_RUN) console.log('(--dry-run 模式:仅预览,不执行任何修改)\n'); + + const pending = []; + if (hasInstalledEntry()) pending.push(' [1] plugins/installed.json 中的 kimi-eyes 条目(改前备份)'); + if (hasManagedDir()) pending.push(` [2] 安装副本 ${managedDir}`); + if (hasConfigDir()) pending.push(` [3] 配置目录 ${configDir}(含 VLM API Key)`); + + if (pending.length === 0) { + console.log('已卸载或无需清理(installed.json / managed 副本 / 配置目录均无残留)。'); + console.log('如在 Claude Code 中挂载过,请手动执行: claude mcp remove kimi-eyes'); + return; + } + + console.log('将清理:'); + console.log(pending.join('\n')); + + if (DRY_RUN) { + console.log('\n(--dry-run 结束,未做任何修改)'); + return; + } + + if (!(await confirm('\n确认卸载?[y/N] ', false))) { + console.log('已取消,未做任何修改。'); + return; + } + + const results = []; + results.push(removeInstalledEntry()); + results.push(removeManagedDir()); + + if (hasConfigDir()) { + if (await confirm(' 配置目录含你的 VLM API Key,确认一并删除?[y/N] ', false)) { + results.push(removeConfigDir()); + } else { + results.push({ status: 'skip', detail: '用户选择保留配置目录(请自行处理其中密钥)' }); + } + } else { + results.push({ status: 'skip', detail: '配置目录不存在' }); + } + + console.log('\n执行结果:'); + for (const r of results) { + const icon = r.status === 'done' ? '✓' : r.status === 'warn' ? '⚠' : r.status === 'error' ? '✗' : '·'; + console.log(` ${icon} ${r.detail}`); + } + + console.log('\n完成。请重启 kimi-code 会话(或 /reload)使卸载完全生效。'); + console.log('如在 Claude Code 中挂载过,请手动执行: claude mcp remove kimi-eyes'); + + if (results.some((r) => r.status === 'error')) process.exitCode = 1; +} + +main().catch((err) => { + console.error(`\n[uninstall] 出错: ${err.message}`); + process.exit(1); +});