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 { 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();

View File

@@ -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;
}
} }

View File

@@ -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);

View File

@@ -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',
}) })

View File

@@ -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');

View File

@@ -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
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", "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"
}, },