generated from template/slidev-template
update
This commit is contained in:
@@ -1,3 +1,12 @@
|
|||||||
import { App } from '@kevisual/router'
|
import { App } from '@kevisual/router'
|
||||||
|
import { createStorage } from "unstorage";
|
||||||
|
import fsDriver from "unstorage/drivers/fs";
|
||||||
|
import { CloudflareConfig } from "@agent/task.ts";
|
||||||
|
export const storage = createStorage({
|
||||||
|
driver: fsDriver({
|
||||||
|
base: process.cwd() + '/storage/ddns-agent'
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
export const app = new App();
|
export const app = new App();
|
||||||
@@ -2,7 +2,7 @@ interface DnsUpdate {
|
|||||||
zone_id: string;
|
zone_id: string;
|
||||||
record_id: string;
|
record_id: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
new_ip: string;
|
new_ip: string;
|
||||||
api_token: string;
|
api_token: string;
|
||||||
type?: string; // 'A' or 'AAAA'
|
type?: string; // 'A' or 'AAAA'
|
||||||
}
|
}
|
||||||
@@ -14,7 +14,7 @@ export class CloudflareDDNS {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
async updateRecord(data: DnsUpdate) {
|
async updateRecord(data: DnsUpdate) {
|
||||||
const { zone_id, record_id, domain, type ,new_ip, api_token } = data;
|
const { zone_id, record_id, domain, type, new_ip, api_token } = data;
|
||||||
const url = `https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${record_id}`;
|
const url = `https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${record_id}`;
|
||||||
const body = {
|
const body = {
|
||||||
"type": type || 'A',
|
"type": type || 'A',
|
||||||
@@ -29,17 +29,60 @@ export class CloudflareDDNS {
|
|||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if(!result.success) {
|
if (!result.success) {
|
||||||
throw new Error(`更新失败: ${JSON.stringify(result.errors)}`);
|
throw new Error(`更新失败: ${JSON.stringify(result.errors)}`);
|
||||||
}
|
}
|
||||||
console.log(`更新成功: ${domain} -> ${new_ip}`);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
async getList(zone_id: string, api_token: string) {
|
async getList(zone_id: string, api_token: string, opts?: { per_page?: number; page?: number, search?: string }) {
|
||||||
const url = `https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records`;
|
const per_page = opts?.per_page || 100;
|
||||||
|
const page = opts?.page || 1;
|
||||||
|
const search = opts?.search ? `&search=${encodeURIComponent(opts.search)}` : '';
|
||||||
|
const url = `https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records?per_page=${per_page}&page=${page}${search}`;
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: this.makeHeader(api_token),
|
headers: this.makeHeader(api_token),
|
||||||
}).then(res => res.json());
|
}).then(res => res.json());
|
||||||
}
|
}
|
||||||
|
async getRecord(zone_id: string, record_id: string, api_token: string) {
|
||||||
|
const url = `https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${record_id}`;
|
||||||
|
return fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: this.makeHeader(api_token),
|
||||||
|
}).then(res => res.json());
|
||||||
|
}
|
||||||
|
async createRecord(data: Partial<DnsUpdate>) {
|
||||||
|
const { zone_id, domain, type = 'A', new_ip: content, api_token } = data;
|
||||||
|
const url = `https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records`;
|
||||||
|
const body = {
|
||||||
|
"type": type,
|
||||||
|
"name": domain,
|
||||||
|
"content": content,
|
||||||
|
"ttl": 0,
|
||||||
|
"proxied": false,
|
||||||
|
}
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.makeHeader(api_token),
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(`创建失败: ${JSON.stringify(result.errors)}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
async deleteRecord(zone_id: string, record_id: string, api_token: string) {
|
||||||
|
const url = `https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records/${record_id}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: this.makeHeader(api_token),
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(`删除失败: ${JSON.stringify(result.errors)}`);
|
||||||
|
}
|
||||||
|
console.log(`删除成功: Record ID ${record_id}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {app} from '../app.ts';
|
import { app } from '../app.ts';
|
||||||
import { CloudflareDDNS } from '../ddns/cloudflare/index.ts';
|
import { CloudflareDDNS } from '../ddns/cloudflare/index.ts';
|
||||||
|
|
||||||
app.route({
|
app.route({
|
||||||
@@ -8,11 +8,12 @@ app.route({
|
|||||||
}).define(async (ctx) => {
|
}).define(async (ctx) => {
|
||||||
const { zone_id, record_id, domain, new_ip, api_token, type = 'A' } = ctx.query || {};
|
const { zone_id, record_id, domain, new_ip, api_token, type = 'A' } = ctx.query || {};
|
||||||
|
|
||||||
if(!zone_id || !record_id || !domain || !new_ip || !api_token) {
|
if (!zone_id || !record_id || !domain || !new_ip || !api_token) {
|
||||||
ctx.throw?.('缺少必要参数');
|
ctx.throw?.('缺少必要参数');
|
||||||
}
|
}
|
||||||
|
|
||||||
const cf = new CloudflareDDNS();
|
const cf = new CloudflareDDNS();
|
||||||
|
|
||||||
const result = await cf.updateRecord({
|
const result = await cf.updateRecord({
|
||||||
zone_id,
|
zone_id,
|
||||||
record_id,
|
record_id,
|
||||||
@@ -21,6 +22,83 @@ app.route({
|
|||||||
api_token,
|
api_token,
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
|
if (result.success === false) {
|
||||||
|
ctx.throw?.(result.errors?.map((e) => e.message).join('; ') || '更新DNS记录失败');
|
||||||
|
} else {
|
||||||
|
console.log('更新DNS记录成功:', `${domain} -> ${new_ip}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.body = { result };
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'cf',
|
||||||
|
key: 'create',
|
||||||
|
description: '创建Cloudflare DNS记录, 需要提供zone_id, domain, new_ip, api_token, type参数, type参数可选,默认为A记录, A 或AAAA',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const { zone_id, domain, new_ip, api_token, type = 'A' } = ctx.query || {};
|
||||||
|
|
||||||
|
if (!zone_id || !domain || !new_ip || !api_token) {
|
||||||
|
ctx.throw?.('缺少必要参数');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cf = new CloudflareDDNS();
|
||||||
|
|
||||||
|
const result = await cf.createRecord({
|
||||||
|
zone_id,
|
||||||
|
domain,
|
||||||
|
new_ip,
|
||||||
|
api_token,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
if (result.success === false) {
|
||||||
|
ctx.throw?.(result.errors?.map((e) => e.message).join('; ') || '创建DNS记录失败');
|
||||||
|
}
|
||||||
|
console.log(`创建成功: ${domain} -> ${new_ip}`);
|
||||||
|
const record_id = result.result.id;
|
||||||
|
const name = result.result.name;
|
||||||
|
const content = result.result.content;
|
||||||
|
console.log(`id->name: ${result.result.id} -> ${name}, content: ${content}`);
|
||||||
|
ctx.body = {
|
||||||
|
record_id: record_id,
|
||||||
|
name: name,
|
||||||
|
content: content,
|
||||||
|
result: result.result
|
||||||
|
};
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'cf',
|
||||||
|
key: 'delete',
|
||||||
|
description: '删除Cloudflare DNS记录, 需要提供zone_id, record_id, api_token参数',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const { zone_id, record_id, api_token } = ctx.query || {};
|
||||||
|
if (!zone_id || !record_id || !api_token) {
|
||||||
|
ctx.throw?.('缺少必要参数');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cf = new CloudflareDDNS();
|
||||||
|
|
||||||
|
const result = await cf.deleteRecord(zone_id, record_id, api_token);
|
||||||
|
if (result.success === false) {
|
||||||
|
ctx.throw?.(result.errors?.map((e) => e.message).join('; ') || '删除DNS记录失败');
|
||||||
|
}
|
||||||
|
ctx.body = { result };
|
||||||
|
})
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'cf',
|
||||||
|
key: 'list',
|
||||||
|
description: '获取Cloudflare DNS记录列表, 需要提供zone_id, api_token参数,可选search参数用于模糊搜索域名',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const { zone_id, api_token, search } = ctx.query || {};
|
||||||
|
|
||||||
|
if (!zone_id || !api_token) {
|
||||||
|
ctx.throw?.('缺少必要参数');
|
||||||
|
}
|
||||||
|
|
||||||
|
const cf = new CloudflareDDNS();
|
||||||
|
const result = await cf.getList(zone_id, api_token, search ? { search } : undefined);
|
||||||
|
|
||||||
ctx.body = { result };
|
ctx.body = { result };
|
||||||
}).addTo(app);
|
}).addTo(app);
|
||||||
107
agent/task.ts
107
agent/task.ts
@@ -1,12 +1,4 @@
|
|||||||
import { app } from './app.ts';
|
import { app, storage } from './app.ts';
|
||||||
import { createStorage } from "unstorage";
|
|
||||||
import fsDriver from "unstorage/drivers/fs";
|
|
||||||
|
|
||||||
const storage = createStorage({
|
|
||||||
driver: fsDriver({
|
|
||||||
base: process.cwd() + '/storage/ddns-agent'
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type CloudflareConfig = {
|
export type CloudflareConfig = {
|
||||||
// Cloudflare 访问地址
|
// Cloudflare 访问地址
|
||||||
@@ -49,14 +41,16 @@ app.route({
|
|||||||
const oldIp = isV4 ? config.ipv4 : config.ipv6;
|
const oldIp = isV4 ? config.ipv4 : config.ipv6;
|
||||||
if (newIp !== oldIp) {
|
if (newIp !== oldIp) {
|
||||||
// IP地址有变化,更新DNS记录
|
// IP地址有变化,更新DNS记录
|
||||||
await app.call({ path: 'cf', key: 'update' }, {
|
await app.call({
|
||||||
zone_id: config.zone_id,
|
path: 'cf', key: 'update', payload: {
|
||||||
record_id: isV4 ? config.record_id4 : config.record_id6,
|
zone_id: config.zone_id,
|
||||||
domain: config.domain,
|
record_id: isV4 ? config.record_id4 : config.record_id6,
|
||||||
new_ip: newIp,
|
domain: config.domain,
|
||||||
api_token: config.api_token,
|
new_ip: newIp,
|
||||||
type: isV4 ? 'A' : 'AAAA',
|
api_token: config.api_token,
|
||||||
});
|
type: isV4 ? 'A' : 'AAAA',
|
||||||
|
}
|
||||||
|
}, {});
|
||||||
// 更新配置文件中的IP地址
|
// 更新配置文件中的IP地址
|
||||||
if (isV4) {
|
if (isV4) {
|
||||||
config.ipv4 = newIp;
|
config.ipv4 = newIp;
|
||||||
@@ -83,8 +77,83 @@ app.route({
|
|||||||
ctx.body = { message: '任务完成' };
|
ctx.body = { message: '任务完成' };
|
||||||
}).addTo(app);
|
}).addTo(app);
|
||||||
|
|
||||||
export const main = () => {
|
app.route({
|
||||||
app.call({
|
path: 'ip',
|
||||||
|
key: 'init',
|
||||||
|
description: '初始化配置文件cloudflare.json',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
// 初始化逻辑
|
||||||
|
const config = await storage.getItem<CloudflareConfig>('cloudflare.json');
|
||||||
|
|
||||||
|
const isIpv4 = config?.flag === 1 || config?.flag === 3;
|
||||||
|
const isIpv6 = config?.flag === 2 || config?.flag === 3;
|
||||||
|
const apiToken = config?.api_token || '';
|
||||||
|
if (!apiToken) {
|
||||||
|
ctx.throw?.('配置错误:api_token为空');
|
||||||
|
}
|
||||||
|
const getIp = async (isV4: boolean) => {
|
||||||
|
const res = await app.call({ path: 'ip', key: isV4 ? 'v4' : 'v6' });
|
||||||
|
if (res.code !== 200) {
|
||||||
|
ctx.throw?.(`获取${isV4 ? 'IPv4' : 'IPv6'}地址失败: ` + res.message);
|
||||||
|
}
|
||||||
|
return res.body.ip as string;
|
||||||
|
}
|
||||||
|
const createCfRecord = async (isV4: boolean) => {
|
||||||
|
const newIp = await getIp(isV4);
|
||||||
|
const cfRes = await app.call({
|
||||||
|
path: 'cf',
|
||||||
|
key: 'create',
|
||||||
|
payload: {
|
||||||
|
zone_id: config.zone_id,
|
||||||
|
domain: config.domain,
|
||||||
|
new_ip: newIp,
|
||||||
|
api_token: config.api_token,
|
||||||
|
type: isV4 ? 'A' : 'AAAA',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (cfRes.code !== 200) {
|
||||||
|
ctx.throw?.(`创建${isV4 ? 'IPv4' : 'IPv6'}记录失败: ` + cfRes.message);
|
||||||
|
}
|
||||||
|
console.log(`创建${isV4 ? 'IPv4' : 'IPv6'}记录结果:`, cfRes.body.record_id);
|
||||||
|
const record_id = cfRes.body.record_id as string;
|
||||||
|
|
||||||
|
if (isV4) {
|
||||||
|
config.record_id4 = record_id;
|
||||||
|
} else {
|
||||||
|
config.record_id6 = record_id;
|
||||||
|
}
|
||||||
|
config.time = new Date().toLocaleString();
|
||||||
|
await storage.setItem('cloudflare.json', config);
|
||||||
|
}
|
||||||
|
let isInit = false;
|
||||||
|
if (isIpv4 && config.record_id4 === '') {
|
||||||
|
console.log('配置错误:需要更新IPv4地址,但record_id4为空, 正在创建新记录...');
|
||||||
|
await createCfRecord(true);
|
||||||
|
isInit = true;
|
||||||
|
}
|
||||||
|
if (isIpv6 && config.record_id6 === '') {
|
||||||
|
console.log('配置警告:需要更新IPv6地址,但record_id6为空,正在创建新记录...');
|
||||||
|
await createCfRecord(false);
|
||||||
|
isInit = true;
|
||||||
|
}
|
||||||
|
ctx.body = { init: isInit, message: '初始化完成' };
|
||||||
|
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
export const main = async () => {
|
||||||
|
const res = await app.call({
|
||||||
|
path: 'ip',
|
||||||
|
key: 'init',
|
||||||
|
});
|
||||||
|
if (res.code !== 200) {
|
||||||
|
console.error('初始化失败:', res.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.body.init) {
|
||||||
|
console.log('初始化完成,并创建了必要的DNS记录,任务结束。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await app.call({
|
||||||
path: 'ip',
|
path: 'ip',
|
||||||
key: 'task',
|
key: 'task',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
import { createStorage } from "unstorage";
|
import { storage } from "@agent/app.ts";
|
||||||
import fsDriver from "unstorage/drivers/fs";
|
|
||||||
import { CloudflareDDNS } from "@agent/ddns/cloudflare/index.ts";
|
|
||||||
import { CloudflareConfig } from "@agent/task.ts";
|
import { CloudflareConfig } from "@agent/task.ts";
|
||||||
const storage = createStorage({
|
|
||||||
driver: fsDriver({
|
|
||||||
base: process.cwd() + '/storage/ddns-agent'
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export const config = await storage.getItem<CloudflareConfig>('cloudflare.json');
|
export const config = await storage.getItem<CloudflareConfig>('cloudflare.json');
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { config } from "./common.ts";
|
|||||||
|
|
||||||
const cf = new CloudflareDDNS();
|
const cf = new CloudflareDDNS();
|
||||||
if (config) {
|
if (config) {
|
||||||
const res = await cf.getList(config.zone_id, config.api_token);
|
const res = await cf.getList(config.zone_id, config.api_token, {search: 'xion'});
|
||||||
console.log('Cloudflare DNS Records List:', res);
|
console.log('Cloudflare DNS Records List:', res);
|
||||||
} else {
|
} else {
|
||||||
console.log('No configuration found.');
|
console.log('No configuration found.');
|
||||||
|
|||||||
5
agent/test/init.ts
Normal file
5
agent/test/init.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import '../index.ts';
|
||||||
|
|
||||||
|
import { main } from '@agent/task.ts';
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -1,19 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/ddns-agent",
|
"name": "@kevisual/ddns-agent",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"basename": "/root/ddns-agent",
|
"basename": "/root/ddns-agent",
|
||||||
"app": {
|
"app": {
|
||||||
"entry": "agent/index.ts",
|
"entry": "agent/main.ts",
|
||||||
"type": "script-app",
|
"type": "pm2-system-app",
|
||||||
|
"engine": "bun",
|
||||||
"runtime": [
|
"runtime": [
|
||||||
"cli"
|
"cli"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"pub": "ev deploy . -k ddns-agent -v 1.0.1 -u",
|
"pub": "ev deploy . -k ddns-agent -v 1.0.2 -u",
|
||||||
"packup": "ev pack -p",
|
"packup": "ev pack -p",
|
||||||
"pm2": "pm2 start agent/corn.ts --interpreter bun --name ddns-agent"
|
"pm2": "pm2 start agent/corn.ts --interpreter bun --name ddns-agent"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user