feat: 新增卸载脚本与统一版本管理

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。
This commit is contained in:
billowliu2 committed 2026-08-02 16:40:57 +08:00
1 parent 8a5d41ebca
commit 839a66ff5d
5 files changed
+263 -5

No files matched your search

+11 -3
View File
@@ -1,8 +1,14 @@
{ {
"name": "kimi-eyes", "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).", "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", "author": "kimi-eyes",
"license": "MIT", "license": "MIT",
"interface": { "interface": {
@@ -14,7 +20,9 @@
"mcpServers": { "mcpServers": {
"kimi-eyes": { "kimi-eyes": {
"command": "node", "command": "node",
"args": ["./mcp/server.mjs"], "args": [
"./mcp/server.mjs"
],
"cwd": "./" "cwd": "./"
} }
} }
+15 -1
View File
@@ -10,7 +10,19 @@ import { execFileSync } from 'node:child_process';
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
export const MODELS_DB_PATH = path.join(__dirname, 'models-db.json'); 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 = { export const EXT_MEDIA = {
png: 'image/png', png: 'image/png',
@@ -145,6 +157,8 @@ export function anthropicEndpoint(baseUrl) {
export function modelsEndpoint(baseUrl, protocol) { export function modelsEndpoint(baseUrl, protocol) {
let b = String(baseUrl || '').trim().replace(/\/+$/, ''); let b = String(baseUrl || '').trim().replace(/\/+$/, '');
if (!b) return ''; 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`; if (/\/v\d+$/i.test(b)) return `${b}/models`;
return `${b}/v1/models`; return `${b}/v1/models`;
} }
+5 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "kimi-eyes", "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).", "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": [ "keywords": [
"kimi", "kimi",
@@ -18,6 +18,7 @@
"type": "module", "type": "module",
"bin": { "bin": {
"kimi-eyes": "setup.mjs", "kimi-eyes": "setup.mjs",
"kimi-eyes-uninstall": "uninstall.mjs",
"kimi-eyes-mcp": "mcp/server.mjs" "kimi-eyes-mcp": "mcp/server.mjs"
}, },
"files": [ "files": [
@@ -26,6 +27,7 @@
"SYSTEM.md", "SYSTEM.md",
"kimi.plugin.json", "kimi.plugin.json",
"setup.mjs", "setup.mjs",
"uninstall.mjs",
"README.md", "README.md",
"README.en.md", "README.en.md",
"LICENSE" "LICENSE"
@@ -35,7 +37,9 @@
}, },
"scripts": { "scripts": {
"setup": "node setup.mjs", "setup": "node setup.mjs",
"uninstall": "node uninstall.mjs",
"sync:models": "node scripts/sync-models.mjs", "sync:models": "node scripts/sync-models.mjs",
"version": "node scripts/bump-version.mjs",
"prepublishOnly": "node scripts/sync-models.mjs --allow-fail" "prepublishOnly": "node scripts/sync-models.mjs --allow-fail"
} }
} }
+53
View File
@@ -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}`);
}
+179
View File
@@ -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);
});