This commit is contained in:
2025-11-18 14:39:32 +08:00
parent 572f793061
commit a24bd021a9
8 changed files with 238 additions and 41 deletions

View File

@@ -1,3 +1,12 @@
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();

View File

@@ -14,7 +14,7 @@ export class CloudflareDDNS {
};
}
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 body = {
"type": type || 'A',
@@ -29,17 +29,60 @@ export class CloudflareDDNS {
body: JSON.stringify(body),
});
const result = await response.json();
if(!result.success) {
if (!result.success) {
throw new Error(`更新失败: ${JSON.stringify(result.errors)}`);
}
console.log(`更新成功: ${domain} -> ${new_ip}`);
return result;
}
async getList(zone_id: string, api_token: string) {
const url = `https://api.cloudflare.com/client/v4/zones/${zone_id}/dns_records`;
async getList(zone_id: string, api_token: string, opts?: { per_page?: number; page?: number, search?: string }) {
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, {
method: 'GET',
headers: this.makeHeader(api_token),
}).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;
}
}

View File

@@ -1,4 +1,4 @@
import {app} from '../app.ts';
import { app } from '../app.ts';
import { CloudflareDDNS } from '../ddns/cloudflare/index.ts';
app.route({
@@ -8,11 +8,12 @@ app.route({
}).define(async (ctx) => {
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?.('缺少必要参数');
}
const cf = new CloudflareDDNS();
const result = await cf.updateRecord({
zone_id,
record_id,
@@ -21,6 +22,83 @@ app.route({
api_token,
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 };
}).addTo(app);

View File

@@ -1,12 +1,4 @@
import { app } from './app.ts';
import { createStorage } from "unstorage";
import fsDriver from "unstorage/drivers/fs";
const storage = createStorage({
driver: fsDriver({
base: process.cwd() + '/storage/ddns-agent'
}),
});
import { app, storage } from './app.ts';
export type CloudflareConfig = {
// Cloudflare 访问地址
@@ -49,14 +41,16 @@ app.route({
const oldIp = isV4 ? config.ipv4 : config.ipv6;
if (newIp !== oldIp) {
// IP地址有变化更新DNS记录
await app.call({ path: 'cf', key: 'update' }, {
await app.call({
path: 'cf', key: 'update', payload: {
zone_id: config.zone_id,
record_id: isV4 ? config.record_id4 : config.record_id6,
domain: config.domain,
new_ip: newIp,
api_token: config.api_token,
type: isV4 ? 'A' : 'AAAA',
});
}
}, {});
// 更新配置文件中的IP地址
if (isV4) {
config.ipv4 = newIp;
@@ -83,8 +77,83 @@ app.route({
ctx.body = { message: '任务完成' };
}).addTo(app);
export const main = () => {
app.call({
app.route({
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',
key: 'task',
})

View File

@@ -1,13 +1,5 @@
import { createStorage } from "unstorage";
import fsDriver from "unstorage/drivers/fs";
import { CloudflareDDNS } from "@agent/ddns/cloudflare/index.ts";
import { storage } from "@agent/app.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');

View File

@@ -3,7 +3,7 @@ import { config } from "./common.ts";
const cf = new CloudflareDDNS();
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);
} else {
console.log('No configuration found.');

5
agent/test/init.ts Normal file
View File

@@ -0,0 +1,5 @@
import '../index.ts';
import { main } from '@agent/task.ts';
main()

View File

@@ -1,19 +1,20 @@
{
"name": "@kevisual/ddns-agent",
"version": "1.0.1",
"version": "1.0.2",
"description": "",
"main": "index.js",
"type": "module",
"basename": "/root/ddns-agent",
"app": {
"entry": "agent/index.ts",
"type": "script-app",
"entry": "agent/main.ts",
"type": "pm2-system-app",
"engine": "bun",
"runtime": [
"cli"
]
},
"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",
"pm2": "pm2 start agent/corn.ts --interpreter bun --name ddns-agent"
},