This commit is contained in:
2025-11-16 13:32:35 +08:00
parent e35712820f
commit 2c800d336c
11 changed files with 414 additions and 18 deletions

3
.gitignore vendored
View File

@@ -1,6 +1,7 @@
node_modules
.DS_Store
dist
pack-dist
*.local
.vite-inspect
.remote-assets
@@ -11,3 +12,5 @@ components.d.ts
.env
!.env*example
storage

11
agent/corn.ts Normal file
View File

@@ -0,0 +1,11 @@
import cron from 'node-cron';
import './index.ts'
import { main } from './task.ts';
// 每2个小时执行一次更新任务
cron.schedule('0 */2 * * *', async () => {
main()
});
// first run
main()

8
agent/ddns/get-ip.ts Normal file
View File

@@ -0,0 +1,8 @@
const baseURLv4 = 'https://4.ipw.cn/';
const baseURLv6 = 'https://6.ipw.cn/';
export const getPublicIp = async (type: 'v4' | 'v6'): Promise<string> => {
const url = type === 'v4' ? baseURLv4 : baseURLv6;
const response = await fetch(url);
const ip = (await response.text()).trim();
return ip;
}

View File

@@ -1,14 +1,8 @@
import {app} from './app.ts';
import './ip'
import './task.ts';
import './routes/cloudflare.ts';
// app.listen(8080);
app.call({
path: 'ip',
key: 'v6'
}).then(res => {
console.log('IPv4 Address:', res);
})
export { app };

View File

@@ -1,10 +1,8 @@
const baseURLv4 = 'https://4.ipw.cn/';
const baseURLv6 = 'https://6.ipw.cn/';
import { app } from './app.ts';
app.route({
path: 'ip',
key: 'v4',
@@ -12,7 +10,7 @@ app.route({
}).define(async (ctx) => {
const response = await fetch(baseURLv4);
const ip = (await response.text()).trim();
if(!isIpv4(ip)) {
if (!isIpv4(ip)) {
ctx.throw?.('获取地址失败');
}
ctx.body = { ip };
@@ -25,7 +23,7 @@ app.route({
}).define(async (ctx) => {
const response = await fetch(baseURLv6);
const ip = (await response.text()).trim();
if(!isIpv6(ip)) {
if (!isIpv6(ip)) {
ctx.throw?.('获取地址失败');
}
ctx.body = { ip };
@@ -39,3 +37,4 @@ export const isIpv6 = (ip: string): boolean => {
export const isIpv4 = (ip: string): boolean => {
return ip.split('.').length === 4;
}

View File

@@ -0,0 +1,26 @@
import {app} from '../app.ts';
import { CloudflareDDNS } from '../ddns/cloudflare/index.ts';
app.route({
path: 'cf',
key: 'update',
description: '更新Cloudflare DNS记录, 需要提供zone_id, record_id, domain, new_ip, api_token, type参数 type参数可选默认为A记录 A 或AAAA',
}).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) {
ctx.throw?.('缺少必要参数');
}
const cf = new CloudflareDDNS();
const result = await cf.updateRecord({
zone_id,
record_id,
domain,
new_ip,
api_token,
type,
});
ctx.body = { result };
}).addTo(app);

92
agent/task.ts Normal file
View File

@@ -0,0 +1,92 @@
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'
}),
});
type CloudflareConfig = {
// Cloudflare 访问地址
website: string;
domain: string;
zone_id: string;
// 更新IPv4记录的ID
record_id4: string;
// 更新IPv6记录的ID
record_id6: string;
api_token: string;
ipv6: string;
ipv4: string;
flag: number; // 0: 不更新, 1: 仅IPv4, 2: 仅IPv6, 3: IPv4和IPv6
time: string; // 上次更新时间戳
}
app.route({
path: 'ip',
key: 'task',
description: `执行IP更新任务
1. 读取配置文件cloudflare.json
2. 根据flag决定更新IPv4和/或IPv6地址
3. 获取当前公网IP地址
4. 如果IP地址有变化则调用 router: cf/update 更新DNS记录
5. 保存最新的IP地址和更新时间戳到配置文件
`,
}).define(async (ctx) => {
const config = await storage.getItem<CloudflareConfig>('cloudflare.json');
if (!config) {
ctx.throw?.('未找到配置');
}
const now = Date.now();
const date = new Date(now);
const updateIp = async (isV4 = true) => {
const res = await app.call({ path: 'ip', key: isV4 ? 'v4' : 'v6' });
if (res.code !== 200) {
ctx.throw(res.message);
}
const newIp = res.body.ip as string;
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',
});
// 更新配置文件中的IP地址
if (isV4) {
config.ipv4 = newIp;
} else {
config.ipv6 = newIp;
}
config.time = date.toLocaleString();
await storage.setItem('cloudflare.json', config);
console.log(date.toLocaleString() + ` 更新 ${isV4 ? 'IPv4' : 'IPv6'} 地址为: ${newIp}`);
} else {
console.log(date.toLocaleString() + ` ${isV4 ? 'IPv4' : 'IPv6'} 地址未改变: ${newIp}`);
}
}
if (config.flag === 1) {
await updateIp(true);
}
if (config.flag === 2) {
await updateIp(false);
}
if (config.flag === 3) {
await updateIp(true);
await updateIp(false);
}
ctx.body = { message: '任务完成' };
}).addTo(app);
export const main = () => {
app.call({
path: 'ip',
key: 'task',
})
}

1
html/index.html Normal file
View File

@@ -0,0 +1 @@
this is a test

View File

@@ -3,18 +3,37 @@
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"type": "module",
"basename": "/root/ddns-agent",
"app": {
"entry": "agent/index.ts",
"type": "script",
"runtime": [
"cli"
]
},
"scripts": {
"pub": "ev deploy . -k ddns-agent -v 1.0.0 -u",
"packup": "ev pack -p",
"pm2": "pm2 start agent/corn.ts --interpreter bun --name ddns-agent"
},
"files": [
"agent",
"html",
"tsconfig.json"
],
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.19.0",
"dependencies": {
"@kevisual/router": "^0.0.30",
"crypto-js": "^4.2.0"
"crypto-js": "^4.2.0",
"node-cron": "^4.2.1",
"unstorage": "^1.17.2"
},
"devDependencies": {
"@kevisual/types": "^0.0.10",
"@types/node": "^24.10.1"
}
}

209
pnpm-lock.yaml generated
View File

@@ -14,7 +14,16 @@ importers:
crypto-js:
specifier: ^4.2.0
version: 4.2.0
node-cron:
specifier: ^4.2.1
version: 4.2.1
unstorage:
specifier: ^1.17.2
version: 1.17.2
devDependencies:
'@kevisual/types':
specifier: ^0.0.10
version: 0.0.10
'@types/node':
specifier: ^24.10.1
version: 24.10.1
@@ -24,9 +33,26 @@ packages:
'@kevisual/router@0.0.30':
resolution: {integrity: sha512-/mBo7aZFWjT4QfHkI5HPXfdgSwZzt3mAVei7dcNSBTPe9KQSoYKZ8BTq9VTUj3XE0sI6o1bZjlLYvinpVnZilw==}
'@kevisual/types@0.0.10':
resolution: {integrity: sha512-Q73uzzjk9UidumnmCvOpgzqDDvQxsblz22bIFuoiioUFJWwaparx8bpd8ArRyFojicYL1YJoFDzDZ9j9NN8grA==}
'@types/node@24.10.1':
resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
cookie-es@1.2.2:
resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==}
crossws@0.3.5:
resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==}
crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
@@ -39,10 +65,16 @@ packages:
supports-color:
optional: true
defu@6.1.4:
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
destr@2.0.5:
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
@@ -61,6 +93,9 @@ packages:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
h3@1.15.4:
resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==}
http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
@@ -68,6 +103,12 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
iron-webcrypto@1.2.1:
resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
mime-db@1.54.0:
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
engines: {node: '>= 0.6'}
@@ -79,10 +120,27 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
node-cron@4.2.1:
resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==}
engines: {node: '>=6.0.0'}
node-fetch-native@1.6.7:
resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
node-forge@1.3.1:
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
engines: {node: '>= 6.13.0'}
node-mock-http@1.0.3:
resolution: {integrity: sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==}
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
ofetch@1.5.1:
resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==}
on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
@@ -90,10 +148,21 @@ packages:
path-to-regexp@8.3.0:
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
radix3@1.1.2:
resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==}
range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
readdirp@4.1.2:
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
engines: {node: '>= 14.18.0'}
selfsigned@3.0.1:
resolution: {integrity: sha512-6U6w6kSLrM9Zxo0D7mC7QdGS6ZZytMWBnj/vhF9p+dAHx6CwGezuRcO4VclTbrrI7mg7SD6zNiqXUuBHOVopNQ==}
engines: {node: '>=10'}
@@ -117,9 +186,77 @@ packages:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
ufo@1.6.1:
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
uncrypto@0.1.3:
resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
unstorage@1.17.2:
resolution: {integrity: sha512-cKEsD6iBWJgOMJ6vW1ID/SYuqNf8oN4yqRk8OYqaVQ3nnkJXOT1PSpaMh2QfzLs78UN5kSNRD2c/mgjT8tX7+w==}
peerDependencies:
'@azure/app-configuration': ^1.8.0
'@azure/cosmos': ^4.2.0
'@azure/data-tables': ^13.3.0
'@azure/identity': ^4.6.0
'@azure/keyvault-secrets': ^4.9.0
'@azure/storage-blob': ^12.26.0
'@capacitor/preferences': ^6.0.3 || ^7.0.0
'@deno/kv': '>=0.9.0'
'@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0
'@planetscale/database': ^1.19.0
'@upstash/redis': ^1.34.3
'@vercel/blob': '>=0.27.1'
'@vercel/functions': ^2.2.12 || ^3.0.0
'@vercel/kv': ^1.0.1
aws4fetch: ^1.0.20
db0: '>=0.2.1'
idb-keyval: ^6.2.1
ioredis: ^5.4.2
uploadthing: ^7.4.4
peerDependenciesMeta:
'@azure/app-configuration':
optional: true
'@azure/cosmos':
optional: true
'@azure/data-tables':
optional: true
'@azure/identity':
optional: true
'@azure/keyvault-secrets':
optional: true
'@azure/storage-blob':
optional: true
'@capacitor/preferences':
optional: true
'@deno/kv':
optional: true
'@netlify/blobs':
optional: true
'@planetscale/database':
optional: true
'@upstash/redis':
optional: true
'@vercel/blob':
optional: true
'@vercel/functions':
optional: true
'@vercel/kv':
optional: true
aws4fetch:
optional: true
db0:
optional: true
idb-keyval:
optional: true
ioredis:
optional: true
uploadthing:
optional: true
snapshots:
'@kevisual/router@0.0.30':
@@ -130,18 +267,39 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@kevisual/types@0.0.10': {}
'@types/node@24.10.1':
dependencies:
undici-types: 7.16.0
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
cookie-es@1.2.2: {}
crossws@0.3.5:
dependencies:
uncrypto: 0.1.3
crypto-js@4.2.0: {}
debug@4.4.3:
dependencies:
ms: 2.1.3
defu@6.1.4: {}
depd@2.0.0: {}
destr@2.0.5: {}
ee-first@1.1.1: {}
encodeurl@2.0.0: {}
@@ -152,6 +310,18 @@ snapshots:
fresh@2.0.0: {}
h3@1.15.4:
dependencies:
cookie-es: 1.2.2
crossws: 0.3.5
defu: 6.1.4
destr: 2.0.5
iron-webcrypto: 1.2.1
node-mock-http: 1.0.3
radix3: 1.1.2
ufo: 1.6.1
uncrypto: 0.1.3
http-errors@2.0.0:
dependencies:
depd: 2.0.0
@@ -162,6 +332,10 @@ snapshots:
inherits@2.0.4: {}
iron-webcrypto@1.2.1: {}
lru-cache@10.4.3: {}
mime-db@1.54.0: {}
mime-types@3.0.1:
@@ -170,16 +344,36 @@ snapshots:
ms@2.1.3: {}
node-cron@4.2.1: {}
node-fetch-native@1.6.7: {}
node-forge@1.3.1: {}
node-mock-http@1.0.3: {}
normalize-path@3.0.0: {}
ofetch@1.5.1:
dependencies:
destr: 2.0.5
node-fetch-native: 1.6.7
ufo: 1.6.1
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
path-to-regexp@8.3.0: {}
picomatch@2.3.1: {}
radix3@1.1.2: {}
range-parser@1.2.1: {}
readdirp@4.1.2: {}
selfsigned@3.0.1:
dependencies:
node-forge: 1.3.1
@@ -208,4 +402,19 @@ snapshots:
toidentifier@1.0.1: {}
ufo@1.6.1: {}
uncrypto@0.1.3: {}
undici-types@7.16.0: {}
unstorage@1.17.2:
dependencies:
anymatch: 3.1.3
chokidar: 4.0.3
destr: 2.0.5
h3: 1.15.4
lru-cache: 10.4.3
node-fetch-native: 1.6.7
ofetch: 1.5.1
ufo: 1.6.1

34
tsconfig.json Normal file
View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"module": "NodeNext",
"target": "esnext",
"noImplicitAny": false,
"outDir": "./dist",
"sourceMap": false,
"allowJs": true,
"newLine": "LF",
"baseUrl": "./",
"typeRoots": [
"node_modules/@types",
],
"declaration": false,
"noEmit": true,
"allowImportingTsExtensions": true,
"moduleResolution": "NodeNext",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"paths": {
"@/*": [
"src/*"
],
"@agent/*": [
"agent/*"
],
}
},
"include": [
"src/**/*.ts",
"agent/**/*.ts",
]
}