From a24bd021a9862710bdda93600cc14ccf63f9da29 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Tue, 18 Nov 2025 14:39:32 +0800 Subject: [PATCH] update --- agent/app.ts | 9 +++ agent/ddns/cloudflare/index.ts | 55 +++++++++++++++-- agent/routes/cloudflare.ts | 82 ++++++++++++++++++++++++- agent/task.ts | 107 +++++++++++++++++++++++++++------ agent/test/common.ts | 10 +-- agent/test/get-cf-list.ts | 2 +- agent/test/init.ts | 5 ++ package.json | 9 +-- 8 files changed, 238 insertions(+), 41 deletions(-) create mode 100644 agent/test/init.ts diff --git a/agent/app.ts b/agent/app.ts index 12315cf..e78bfd0 100644 --- a/agent/app.ts +++ b/agent/app.ts @@ -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(); \ No newline at end of file diff --git a/agent/ddns/cloudflare/index.ts b/agent/ddns/cloudflare/index.ts index 405831a..467203e 100644 --- a/agent/ddns/cloudflare/index.ts +++ b/agent/ddns/cloudflare/index.ts @@ -2,7 +2,7 @@ interface DnsUpdate { zone_id: string; record_id: string; domain: string; - new_ip: string; + new_ip: string; api_token: string; type?: string; // 'A' or 'AAAA' } @@ -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) { + 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; + } } \ No newline at end of file diff --git a/agent/routes/cloudflare.ts b/agent/routes/cloudflare.ts index c1b8238..402ce98 100644 --- a/agent/routes/cloudflare.ts +++ b/agent/routes/cloudflare.ts @@ -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); \ No newline at end of file diff --git a/agent/task.ts b/agent/task.ts index f03558f..7226e64 100644 --- a/agent/task.ts +++ b/agent/task.ts @@ -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' }, { - 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', - }); + 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('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', }) diff --git a/agent/test/common.ts b/agent/test/common.ts index 19d8093..394b0a7 100644 --- a/agent/test/common.ts +++ b/agent/test/common.ts @@ -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('cloudflare.json'); diff --git a/agent/test/get-cf-list.ts b/agent/test/get-cf-list.ts index 6fbdb34..529b2c0 100644 --- a/agent/test/get-cf-list.ts +++ b/agent/test/get-cf-list.ts @@ -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.'); diff --git a/agent/test/init.ts b/agent/test/init.ts new file mode 100644 index 0000000..e03b8eb --- /dev/null +++ b/agent/test/init.ts @@ -0,0 +1,5 @@ +import '../index.ts'; + +import { main } from '@agent/task.ts'; + +main() \ No newline at end of file diff --git a/package.json b/package.json index b87960a..82754d8 100644 --- a/package.json +++ b/package.json @@ -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" },