Files
DDNS/ddns/__main__.py
f99fd8f0b3 feat(provider): add EdgeOne DNS provider for non-accelerated domain records (#588)
* Initial plan

* feat(provider): add EdgeOne DNS provider for non-accelerated domains

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* docs(provider): add EdgeOne DNS documentation for non-accelerated domains

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* fix(provider): update EdgeOne provider aliases per feedback

- Acceleration domains: edgeone, edgeone_acc, teo_acc
- DNS records: edgeone_dns, teo_dns, edgeone_noacc
- Removed: teo, tencentedgeone, neo_acc, neo
- Updated documentation to reflect new aliases

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* feat(provider): add extra parameter support and update documentation index

- Add edgeone_dns to provider index documentation (README.md)
- Add "teo" alias for backward compatibility (not documented)
- Enable EdgeOneProvider to support both acceleration and DNS records via extra['dns_record'] parameter
- Update provider aliases and documentation

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* docs: create comprehensive new-provider agent guide

- Add detailed step-by-step guide for creating new DNS providers
- Include API analysis, implementation, testing, and documentation steps
- Provide code templates and examples for both BaseProvider and SimpleProvider
- Add completion checklist and common pitfalls
- Document all requirements including compatibility checks and index updates

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* refactor(provider): change extra parameter from dns_record to teoDomainType

- Change EdgeOneProvider to use teoDomainType parameter instead of dns_record
- Support values: "dns" or "acceleration" (default)
- Parameter is case-insensitive
- EdgeOneDnsProvider now uses extra parameter to control behavior
- All 55 tests passing

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* refactor(provider): simplify EdgeOneDnsProvider with helper method

- Extract common extra parameter preparation into _prepare_extra() method
- Reduces code duplication across _query_record, _create_record, _update_record
- All 55 tests passing

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* refactor(provider): make EdgeOneDnsProvider more elegant and concise

- Use class attribute _default_domain_type = "dns" for clarity
- Replace _prepare_extra() with inline setdefault() for more Pythonic code
- setdefault() allows users to override teoDomainType if needed
- All 55 tests passing

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* refactor(provider): use options instead of extra for teoDomainType

- Move teoDomainType from per-call extra to instance-level options
- More appropriate since provider type is fixed per instance
- EdgeOneProvider reads teoDomainType from self.options at initialization
- EdgeOneDnsProvider sets teoDomainType="dns" in __init__
- Simplified implementation: 28 lines vs 43 lines for EdgeOneDnsProvider
- All 55 tests passing

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* refactor(provider): support both extra and options for teoDomainType with priority

- Remove __init__ from EdgeOneProvider, read directly from self.options
- Support teoDomainType in both extra (per-call) and options (initialization)
- extra parameter has higher priority than options
- Priority: extra > options > default ("acceleration")
- All 55 tests passing

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* docs(provider): add teoDomainType parameter documentation for EdgeOne

- Document how to switch between acceleration domains and DNS records
- Explain extra.teoDomainType parameter usage
- Add configuration examples for both modes
- Update parameter tables with teoDomainType and extra
- Document priority: extra > initialization parameter
- Add bilingual documentation (Chinese and English)

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* docs(provider): remove initialization parameter config example from EdgeOne

- Remove the example showing teoDomainType via initialization parameters
- Only document usage via extra parameter
- Remove priority note since only one method is documented
- Keep recommendation to use EdgeOneDnsProvider for cleaner code
- Update both Chinese and English documentation

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* docs(agent): update new-provider guide with relative paths and schema notes

- Update CLI compatibility note to mention checking --dns parameter choices
- Change schema note to only require v4.0 (latest format) updates
- Replace "multi-provider format" with "latest schema format"
- Convert all absolute paths to relative paths from project root
- Remove /home/runner/work/DDNS/DDNS/ prefix from all file paths

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* docs(agent): correct latest schema version to v4.1

- Update schema compatibility section to reference v4.1 instead of v4.0
- v4.1 is the latest schema format that requires updates

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* feat(config): add edgeone_dns to CLI and schema v4.1

- Add edgeone_dns to CLI --dns parameter choices
- Add edgeone_dns to schema v4.1 provider enum (both legacy and multi-provider sections)
- Update provider descriptions to include EdgeOne DNS

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Refactor labels_to_prompts_mapping format

* fix(provider): filter teoDomainType from extra params before API call

- Remove teoDomainType from extra dict before passing to Tencent Cloud API
- Fixes "UnknownParameter - The parameter `extra_teoDomainType` is not recognized" error
- teoDomainType is internal parameter for routing, not for API requests
- All 55 tests passing

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* docs(provider): update schema references to v4.1.json in EdgeOne docs

- Update all $schema references from v4.0.json to v4.1.json
- Applied to edgeone.md, edgeone.en.md, edgeone_dns.md, edgeone_dns.en.md
- Ensures documentation references the latest schema format

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* Changes before error encountered

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* fix(config): preserve nested extra dict from JSON/ENV config

- Process "extra" dict before extra_ prefixed keys to avoid flattening
- Fixes issue where {"extra":{"teoDomainType":"dns"}} was incorrectly transformed to {"extra_teoDomainType":"dns"}
- Maintains backward compatibility with extra_ prefixed keys
- Priority: explicit extra dict > extra_ prefixed keys > unknown keys

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update tests/test_provider_edgeone_dns.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update tests/test_provider_edgeone_dns.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* style: apply ruff formatting fixes

- Format 9 files with ruff
- Fix 2 linting issues automatically
- One complexity warning remains (C901) but doesn't affect functionality
- All code now follows consistent formatting standards

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* docs(agent): add ruff installation instructions to new-provider guide

- Add pip3 install ruff command before formatting/linting steps
- Ensures developers have required tools before running verification
- Prevents common setup errors

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* docs(config): clarify extra dict and extra_ prefix priority within config sources

- Add comments explaining that extra_ prefixed keys override nested extra dict values within the same source
- Improves code documentation clarity for config flattening logic
- Addresses feedback about intra-source priority ordering

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

* refactor(config): reduce complexity of _collect_extra method

- Extract _process_extra_from_source helper method to reduce cyclomatic complexity
- Fixes ruff C901 error (complexity was 14, now under threshold)
- Maintains all existing functionality and test coverage
- All 60 extra-related tests passing

Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: NewFuture <6290356+NewFuture@users.noreply.github.com>
Co-authored-by: New Future <NewFuture@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-15 16:44:59 +00:00

154 lines
5.3 KiB
Python
Executable File

# -*- coding:utf-8 -*-
"""
DDNS
@author: NewFuture, rufengsuixing
"""
import sys
from io import TextIOWrapper
from logging import getLogger
from subprocess import check_output
from . import ip
from .__init__ import __description__, __version__, build_date
from .cache import Cache
from .config import Config, load_configs # noqa: F401
from .provider import SimpleProvider, get_provider_class # noqa: F401
logger = getLogger()
def get_ip(ip_type, rules):
"""
get IP address
"""
if rules is False: # disabled
return False
for i in rules:
try:
logger.debug("get_ip:(%s, %s)", ip_type, i)
if str(i).isdigit(): # 数字 local eth
return getattr(ip, "local_v" + ip_type)(i)
elif i.startswith("cmd:"): # cmd
return str(check_output(i[4:]).strip().decode("utf-8"))
elif i.startswith("shell:"): # shell
return str(check_output(i[6:], shell=True).strip().decode("utf-8"))
elif i.startswith("url:"): # 自定义 url
return getattr(ip, "public_v" + ip_type)(i[4:])
elif i.startswith("regex:"): # 正则 regex
return getattr(ip, "regex_v" + ip_type)(i[6:])
else:
return getattr(ip, i + "_v" + ip_type)()
except Exception as e:
logger.error("Failed to get %s address: %s", ip_type, e)
return None
def update_ip(dns, cache, index_rule, domains, record_type, config):
# type: (SimpleProvider, Cache | None, list[str]|bool, list[str], str, Config) -> bool | None
"""
更新IP并变更DNS记录
"""
if not domains:
return None
ip_type = "4" if record_type == "A" else "6"
address = get_ip(ip_type, index_rule)
if not address:
logger.error("Fail to get %s address!", ip_type)
return False
update_success = False
for domain in domains:
domain = domain.lower()
cache_key = "{}:{}".format(domain, record_type)
if cache and cache.get(cache_key) == address:
logger.info("%s[%s] address not changed, using cache: %s", domain, record_type, address)
update_success = True
else:
try:
result = dns.set_record(
domain, address, record_type=record_type, ttl=config.ttl, line=config.line, **config.extra
)
if result:
logger.warning("set %s[IPv%s]: %s successfully.", domain, ip_type, address)
update_success = True
if isinstance(cache, dict):
cache[cache_key] = address
else:
logger.error("Failed to update %s record for %s", record_type, domain)
except Exception as e:
logger.exception("Failed to update %s record for %s: %s", record_type, domain, e)
return update_success
def run(config):
# type: (Config) -> bool
"""
Run the DDNS update process
"""
# 设置IP模块的SSL验证配置
ip.ssl_verify = config.ssl
# dns provider class
provider_class = get_provider_class(config.dns)
dns = provider_class(
config.id, config.token, endpoint=config.endpoint, logger=logger, proxy=config.proxy, ssl=config.ssl
)
cache = Cache.new(config.cache, config.md5(), logger)
return (
update_ip(dns, cache, config.index4, config.ipv4, "A", config) is not False
and update_ip(dns, cache, config.index6, config.ipv6, "AAAA", config) is not False
)
def main():
stdout = sys.stdout # pythonw 模式无 stdout
if stdout and stdout.encoding and stdout.encoding.lower() != "utf-8" and hasattr(stdout, "buffer"):
# 兼容windows 和部分ASCII编码的老旧系统
sys.stdout = TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
sys.stderr = TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
# Windows 下输出一个空行
if stdout and sys.platform.startswith("win"):
stdout.write("\r\n")
logger.name = "ddns"
# 使用多配置加载器,它会自动处理单个和多个配置
configs = load_configs(__description__, __version__, build_date)
if len(configs) == 1:
# 单个配置,使用原有逻辑(向后兼容)
config = configs[0]
success = run(config)
if not success:
sys.exit(1)
else:
# 多个配置,使用新的批处理逻辑
overall_success = True
for i, config in enumerate(configs):
# 如果log_level有值则设置setLevel
if hasattr(config, "log_level") and config.log_level:
logger.setLevel(config.log_level)
logger.info("Running configuration %d/%d", i + 1, len(configs))
# 记录当前provider
logger.info("Using DNS provider: %s", config.dns)
success = run(config)
if not success:
overall_success = False
logger.error("Configuration %d failed", i + 1)
else:
logger.info("Configuration %d completed successfully", i + 1)
if not overall_success:
logger.error("Some configurations failed")
sys.exit(1)
else:
logger.info("All configurations completed successfully")
if __name__ == "__main__":
main()