feat: add vaultwarden CLI and backup
This commit is contained in:
146
show_all.js
Normal file
146
show_all.js
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
const fs = require('fs');
|
||||||
|
const https = require('https');
|
||||||
|
|
||||||
|
const CONFIG_PATH = process.env.HOME + '/.config/vaultwarden/config.json';
|
||||||
|
let config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
||||||
|
|
||||||
|
const VAULT_URL = config.url;
|
||||||
|
const CLIENT_ID = config.client_id;
|
||||||
|
const CLIENT_SECRET = config.client_secret;
|
||||||
|
let token = null;
|
||||||
|
let tokenExpiry = null;
|
||||||
|
const deviceId = 'openclaw-cli-' + require('crypto').randomUUID();
|
||||||
|
|
||||||
|
async function getToken() {
|
||||||
|
if (token && tokenExpiry && Date.now() < tokenExpiry - 300000) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
grant_type: 'client_credentials',
|
||||||
|
scope: 'api',
|
||||||
|
client_id: CLIENT_ID,
|
||||||
|
client_secret: CLIENT_SECRET,
|
||||||
|
device_identifier: deviceId,
|
||||||
|
device_type: '14',
|
||||||
|
device_name: 'OpenClaw CLI'
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await new Promise((resolve, reject) => {
|
||||||
|
const url = new URL(VAULT_URL + '/identity/connect/token');
|
||||||
|
const options = {
|
||||||
|
hostname: url.hostname, port: url.port || 443,
|
||||||
|
path: url.pathname + url.search, method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
timeout: 30000
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = https.request(options, (res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', chunk => body += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(body);
|
||||||
|
resolve(json);
|
||||||
|
} catch (e) { reject(e); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.write(params.toString());
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.access_token) {
|
||||||
|
throw new Error('认证失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
token = response.access_token;
|
||||||
|
tokenExpiry = Date.now() + ((response.expires_in || 7200) - 300) * 1000;
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getItem(id) {
|
||||||
|
const t = await getToken();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const url = new URL(VAULT_URL + `/api/ciphers/${id}`);
|
||||||
|
const options = {
|
||||||
|
hostname: url.hostname, port: url.port || 443,
|
||||||
|
path: url.pathname + url.search, method: 'GET',
|
||||||
|
headers: { 'Authorization': `Bearer ${t}` },
|
||||||
|
timeout: 30000
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = https.request(options, (res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', chunk => body += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(body);
|
||||||
|
resolve(json);
|
||||||
|
} catch (e) { reject(e); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllItems() {
|
||||||
|
const t = await getToken();
|
||||||
|
const response = await new Promise((resolve, reject) => {
|
||||||
|
const url = new URL(VAULT_URL + '/api/ciphers');
|
||||||
|
const options = {
|
||||||
|
hostname: url.hostname, port: url.port || 443,
|
||||||
|
path: url.pathname + url.search, method: 'GET',
|
||||||
|
headers: { 'Authorization': `Bearer ${t}` },
|
||||||
|
timeout: 30000
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = https.request(options, (res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', chunk => body += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(body);
|
||||||
|
resolve(json);
|
||||||
|
} catch (e) { reject(e); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', reject);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
const items = await getAllItems();
|
||||||
|
console.log(`\n📋 密码列表 (${items.length})\n`);
|
||||||
|
console.log('=' .repeat(70));
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const detail = await getItem(item.id);
|
||||||
|
console.log(`\n🔐 ${detail.name}`);
|
||||||
|
console.log('-'.repeat(50));
|
||||||
|
console.log(` ID: ${detail.id}`);
|
||||||
|
if (detail.login?.username) console.log(` 用户名: ${detail.login.username}`);
|
||||||
|
if (detail.login?.password) console.log(` 密码: ${detail.login.password}`);
|
||||||
|
if (detail.login?.uris?.length > 0) {
|
||||||
|
detail.login.uris.forEach(uri => {
|
||||||
|
console.log(` URL: ${uri.uri}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (detail.notes) console.log(` 备注: ${detail.notes}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n' + '='.repeat(70));
|
||||||
|
console.log(`\n✅ 共 ${items.length} 个密码\n`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ 错误:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
118
vaultwarden_backup.md
Normal file
118
vaultwarden_backup.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# Vaultwarden 密码库备份
|
||||||
|
# 生成时间: 2026-02-13 22:14
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 坚果云 WebDAV
|
||||||
|
- **用户名:** work_fyx02@outlook.com
|
||||||
|
- **密码:** auyqxhk7fvhvhh3w
|
||||||
|
- **备注:** WebDAV password for jianguoyun.com
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gitea Token
|
||||||
|
- **用户名:** fyx
|
||||||
|
- **密码:** 7840e0250de4cf994631b1eadb4fc469947aa7df
|
||||||
|
- **备注:** API Token for Gitea (162.211.228.232:8418)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FreshRSS API
|
||||||
|
- **用户名:** 1803560007
|
||||||
|
- **密码:** asCdEfGhsasdasdavWxYz1234ass
|
||||||
|
- **备注:** API Token for FreshRSS (43.163.195.176:36847)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 飞牛 NAS
|
||||||
|
- **用户名:** fyx
|
||||||
|
- **密码:** fengyaxing123
|
||||||
|
- **备注:** WebDAV for Feiniu NAS (f180356.5ddd.com)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GitHub PAT
|
||||||
|
- **用户名:** 1803560007
|
||||||
|
- **密码:** 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
|
||||||
|
- **备注:** Personal Access Token for GitHub
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## YouTube API
|
||||||
|
- **用户名:** API Key
|
||||||
|
- **密码:** AIzaSyC9HYKmkK6rSX1eyPwv3p3cQ4prGa2h-TE
|
||||||
|
- **备注:** API Key for YouTube Data API v3
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bilibili UserID
|
||||||
|
- **用户名:** UserID
|
||||||
|
- **密码:** 356360432
|
||||||
|
- **备注:** B站用户ID DedeUserID
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bilibili SESSDATA
|
||||||
|
- **用户名:** SESSDATA
|
||||||
|
- **密码:** 7a9023e2%2C1785783731%2Ce6963%2A21CjCO8WDiZbbX_EbrIi6niuZtTZMzxsl__Yt_Mo0qWXLZY-Pk9pxZ8qp_WxDjD3fdkfoSVlE1TllKNzIxTlVkam5VbVhzano4Q05hZGMtTWdUZ3lMNm1jdWFfdU1CWlY0dDVJU1BWNGI4V0pzN19ualZ2RjVrSHMyRjZlZk5uQWRTVEU5SXFjWXh3IIEC
|
||||||
|
- **备注:** B站登录凭证 SESSDATA
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## qBittorrent
|
||||||
|
- **用户名:** 1803560007
|
||||||
|
- **密码:** fengyaxing123
|
||||||
|
- **备注:** Downloader on 162.211.228.232:8082
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 滴答清单 API
|
||||||
|
- **用户名:** Token
|
||||||
|
- **密码:** dp_9a8e7eccb01b44559e061dc58a669037
|
||||||
|
- **备注:** Access Token for Dida365
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Memos Access Token
|
||||||
|
- **用户名:** fyx
|
||||||
|
- **密码:** eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJuYW1lIjoiZnl4IiwiaXNzIjoibWVtb3MiLCJzdWIiOiIxIiwiYXVkIjpbInVzZXIuYWNjZXNzLXRva2VuIl0sImlhdCI6MTc1NTY0MzI4N30.pIOySEM5VqYSTM-YLwcjiSMS4y10fbGrtTtm3afjjSI
|
||||||
|
- **备注:** Access Token for Memos (162.211.228.232:5230)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 小红书
|
||||||
|
- **用户名:** session
|
||||||
|
- **密码:** 040069b97e1d876ba1a108d9ce3a4bf940ffca
|
||||||
|
- **备注:** 小红书 web_session
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notion Integration
|
||||||
|
- **用户名:** Token
|
||||||
|
- **密码:** ntn_c43902219395mirQBetIfYoww1qKCAF14GBRUQeDee29o2
|
||||||
|
- **备注:** Integration Token for Notion API
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NewsAPI
|
||||||
|
- **用户名:** API Key
|
||||||
|
- **密码:** 744fb0c696f546cc95545974d18401bb
|
||||||
|
- **备注:** API Key for newsapi.org
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cubox
|
||||||
|
- **用户名:** Token
|
||||||
|
- **密码:** aooyYG5itvB
|
||||||
|
- **备注:** API Token for Cubox
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 163邮箱 SMTP
|
||||||
|
- **用户名:** work_fyx02@163.com
|
||||||
|
- **密码:** PU7fV9D2UeVN9duK
|
||||||
|
- **备注:** SMTP授权码 for work_fyx02@163.com
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*共 16 条记录 | 存储位置: Vaultwarden (bit.180356.xyz)*
|
||||||
368
vaultwarden_cli.js
Normal file
368
vaultwarden_cli.js
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Vaultwarden CLI Tool (交互版)
|
||||||
|
* - 交互式输入密码
|
||||||
|
* - 支持查看密码
|
||||||
|
* - OAuth 自动认证
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const https = require('https');
|
||||||
|
const readline = require('readline');
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const CONFIG_PATH = process.env.VAULTWARDEN_CONFIG_PATH ||
|
||||||
|
path.join(process.env.HOME || process.env.USERPROFILE, '.config', 'vaultwarden', 'config.json');
|
||||||
|
|
||||||
|
const ENV = {
|
||||||
|
url: process.env.VAULTWARDEN_URL || 'https://bit.180356.xyz',
|
||||||
|
clientId: process.env.VAULTWARDEN_CLIENT_ID || '',
|
||||||
|
clientSecret: process.env.VAULTWARDEN_CLIENT_SECRET || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load config file
|
||||||
|
let config = { url: ENV.url, client_id: '', client_secret: '' };
|
||||||
|
if (fs.existsSync(CONFIG_PATH)) {
|
||||||
|
try {
|
||||||
|
config = { ...config, ...JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) };
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error reading config:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const VAULT_URL = ENV.url || config.url;
|
||||||
|
const CLIENT_ID = ENV.clientId || config.client_id;
|
||||||
|
const CLIENT_SECRET = ENV.clientSecret || config.client_secret;
|
||||||
|
|
||||||
|
// 交互式输入
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout
|
||||||
|
});
|
||||||
|
|
||||||
|
function ask(question) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
rl.question(question, answer => resolve(answer));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class VaultwardenAPI {
|
||||||
|
constructor() {
|
||||||
|
this.token = null;
|
||||||
|
this.tokenExpiry = null;
|
||||||
|
this.deviceId = 'openclaw-cli-' + require('crypto').randomUUID();
|
||||||
|
this.deviceType = '14';
|
||||||
|
this.deviceName = 'OpenClaw CLI';
|
||||||
|
}
|
||||||
|
|
||||||
|
async getToken() {
|
||||||
|
if (this.token && this.tokenExpiry && Date.now() < this.tokenExpiry - 300000) {
|
||||||
|
return this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
grant_type: 'client_credentials',
|
||||||
|
scope: 'api',
|
||||||
|
client_id: CLIENT_ID,
|
||||||
|
client_secret: CLIENT_SECRET,
|
||||||
|
device_identifier: this.deviceId,
|
||||||
|
device_type: this.deviceType,
|
||||||
|
device_name: this.deviceName
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await this.request(
|
||||||
|
'/identity/connect/token',
|
||||||
|
'POST',
|
||||||
|
params.toString(),
|
||||||
|
false,
|
||||||
|
'application/x-www-form-urlencoded'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.access_token) {
|
||||||
|
throw new Error('认证失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.token = response.access_token;
|
||||||
|
this.tokenExpiry = Date.now() + ((response.expires_in || 7200) - 300) * 1000;
|
||||||
|
return this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async request(endpoint, method = 'GET', data = null, useAuth = true, contentType = 'application/json') {
|
||||||
|
const url = new URL(VAULT_URL + endpoint);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const options = {
|
||||||
|
hostname: url.hostname,
|
||||||
|
port: url.port || 443,
|
||||||
|
path: url.pathname + url.search,
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': contentType,
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
|
timeout: 30000
|
||||||
|
};
|
||||||
|
|
||||||
|
if (useAuth && this.token) {
|
||||||
|
options.headers['Authorization'] = `Bearer ${this.token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = https.request(options, (res) => {
|
||||||
|
let body = '';
|
||||||
|
res.on('data', chunk => body += chunk);
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const json = body ? JSON.parse(body) : {};
|
||||||
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
resolve(json);
|
||||||
|
} else {
|
||||||
|
reject(new Error(`HTTP ${res.statusCode}: ${json.message || body}`));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', reject);
|
||||||
|
req.on('timeout', () => { req.destroy(); reject(new Error('超时')); });
|
||||||
|
|
||||||
|
if (data) req.write(data);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async listItems() {
|
||||||
|
await this.getToken();
|
||||||
|
const response = await this.request('/api/ciphers');
|
||||||
|
return response.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getItem(id) {
|
||||||
|
await this.getToken();
|
||||||
|
return await this.request(`/api/ciphers/${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getItemByName(name) {
|
||||||
|
const items = await this.listItems();
|
||||||
|
return items.find(item => item.name?.toLowerCase() === name.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
async createItem(name, password, username = '', notes = '') {
|
||||||
|
await this.getToken();
|
||||||
|
const itemData = {
|
||||||
|
name: name,
|
||||||
|
notes: notes,
|
||||||
|
type: 1,
|
||||||
|
favorite: false,
|
||||||
|
login: {
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
totp: null
|
||||||
|
},
|
||||||
|
fields: [],
|
||||||
|
collectionIds: []
|
||||||
|
};
|
||||||
|
return await this.request('/api/ciphers', 'POST', JSON.stringify(itemData));
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteItem(id) {
|
||||||
|
await this.getToken();
|
||||||
|
return await this.request(`/api/ciphers/${id}?permanent=true`, 'DELETE');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const command = args[0] || 'list';
|
||||||
|
|
||||||
|
if (!CLIENT_ID || !CLIENT_SECRET) {
|
||||||
|
console.error('❌ 未配置! 请设置环境变量或配置文件');
|
||||||
|
console.log('\n💡 运行: npm run setup');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = new VaultwardenAPI();
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (command) {
|
||||||
|
case 'save':
|
||||||
|
case 'add':
|
||||||
|
await handleSave(args.slice(1), api);
|
||||||
|
break;
|
||||||
|
case 'list':
|
||||||
|
await handleList(api);
|
||||||
|
break;
|
||||||
|
case 'get':
|
||||||
|
await handleGet(args.slice(1), api);
|
||||||
|
break;
|
||||||
|
case 'search':
|
||||||
|
await handleSearch(args.slice(1), api);
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
await handleDelete(args.slice(1), api);
|
||||||
|
break;
|
||||||
|
case 'setup':
|
||||||
|
await handleSetup();
|
||||||
|
break;
|
||||||
|
case 'help':
|
||||||
|
default:
|
||||||
|
showHelp();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('\n❌ 错误:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
rl.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave(args, api) {
|
||||||
|
console.log('\n📝 保存新项目\n');
|
||||||
|
|
||||||
|
const name = await ask('名称: ');
|
||||||
|
if (!name) {
|
||||||
|
console.error('❌ 名称不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const username = await ask('用户名 (可选): ');
|
||||||
|
|
||||||
|
const password = await ask('密码: ');
|
||||||
|
if (!password) {
|
||||||
|
console.error('❌ 密码不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notes = await ask('备注 (可选): ');
|
||||||
|
|
||||||
|
await api.createItem(name, password, username, notes);
|
||||||
|
console.log(`\n✅ 已保存: ${name}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleList(api) {
|
||||||
|
const items = await api.listItems();
|
||||||
|
console.log(`\n📋 项目列表 (${items.length})\n`);
|
||||||
|
items.forEach(item => {
|
||||||
|
const name = item.name || 'Unknown';
|
||||||
|
const id = item.id?.slice(0, 8) || 'N/A';
|
||||||
|
console.log(` • ${name} [${id}]`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGet(args, api) {
|
||||||
|
const name = args[0];
|
||||||
|
if (!name) {
|
||||||
|
console.error('\n用法: get <名称>\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = await api.getItemByName(name);
|
||||||
|
if (!item) {
|
||||||
|
console.error(`\n❌ 未找到: ${name}\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n📄 ${item.name}\n`);
|
||||||
|
console.log(` ID: ${item.id}`);
|
||||||
|
console.log(` 用户名: ${item.login?.username || '无'}`);
|
||||||
|
console.log(` 密码: ${item.login?.password || '无'}`);
|
||||||
|
console.log(` 备注: ${item.notes || '无'}`);
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSearch(args, api) {
|
||||||
|
const query = args[0];
|
||||||
|
if (!query) {
|
||||||
|
console.error('\n用法: search <关键词>\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = await api.listItems();
|
||||||
|
const results = items.filter(item =>
|
||||||
|
item.name?.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
item.login?.username?.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
|
item.notes?.toLowerCase().includes(query.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`\n🔍 搜索 "${query}" (${results.length} 个结果)\n`);
|
||||||
|
results.forEach(item => {
|
||||||
|
console.log(` • ${item.name}`);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(args, api) {
|
||||||
|
const name = args[0];
|
||||||
|
if (!name) {
|
||||||
|
console.error('\n用法: delete <名称>\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = await api.getItemByName(name);
|
||||||
|
if (!item) {
|
||||||
|
console.error(`\n❌ 未找到: ${name}\n`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const answer = await ask(`🗑️ 删除 "${item.name}"? (y/N): `);
|
||||||
|
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
|
||||||
|
console.log('已取消\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.deleteItem(item.id);
|
||||||
|
console.log(`\n✅ 已删除: ${item.name}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSetup() {
|
||||||
|
console.log('\n🔐 Vaultwarden 配置\n');
|
||||||
|
|
||||||
|
const url = await ask('服务器地址 (默认 https://bit.180356.xyz): ');
|
||||||
|
const clientId = await ask('Client ID: ');
|
||||||
|
const clientSecret = await ask('Client Secret: ');
|
||||||
|
|
||||||
|
const configData = {
|
||||||
|
url: url || 'https://bit.180356.xyz',
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
|
||||||
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(configData, null, 2));
|
||||||
|
|
||||||
|
console.log('\n✅ 配置已保存!\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHelp() {
|
||||||
|
console.log(`
|
||||||
|
🔐 Vaultwarden CLI (交互版)
|
||||||
|
|
||||||
|
用法:
|
||||||
|
save 保存新项目 (交互式)
|
||||||
|
list 列出所有项目
|
||||||
|
get <名称> 获取项目详情
|
||||||
|
search <关键词> 搜索项目
|
||||||
|
delete <名称> 删除项目
|
||||||
|
setup 配置
|
||||||
|
help 显示帮助
|
||||||
|
|
||||||
|
示例:
|
||||||
|
save # 交互式保存
|
||||||
|
list # 列出所有
|
||||||
|
get GitHub # 获取 GitHub
|
||||||
|
search GitHub # 搜索
|
||||||
|
delete GitHub # 删除
|
||||||
|
|
||||||
|
注意: 密码会在命令行中显示
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {};
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user