This commit is contained in:
2026-04-18 14:19:51 +10:00
193 changed files with 18465 additions and 2166 deletions
+83 -9
View File
@@ -16,18 +16,92 @@ class CoreConfig(AppConfig):
Initialize APScheduler when Django starts
"""
from core.scheduler import scheduler, start_scheduler
from links.tasks import schedule_pending_pages
from links.tasks import (
schedule_pending_pages, schedule_pending_screenshots,
retry_stuck_image_imports, flush_click_buffer,
)
from apscheduler.triggers.interval import IntervalTrigger
# Start the scheduler
start_scheduler()
# Read intervals from SiteSettings (fall back to 120s if DB not ready)
try:
from links.models import SiteSettings
ss = SiteSettings.get()
pages_interval = ss.schedule_pending_pages_interval or 120
screenshots_interval = ss.schedule_pending_screenshots_interval or 120
except Exception:
pages_interval = 120
screenshots_interval = 120
# Add periodic job for checking pending pages
if not scheduler.get_job('schedule_pending_pages'):
scheduler.add_job(
schedule_pending_pages,
trigger=IntervalTrigger(seconds=pages_interval),
id='schedule_pending_pages',
replace_existing=True,
)
logger.info(f"Scheduled periodic task: schedule_pending_pages (every {pages_interval}s)")
# Add periodic job for recovering stuck screenshots
scheduler.add_job(
schedule_pending_screenshots,
trigger=IntervalTrigger(seconds=screenshots_interval),
id='schedule_pending_screenshots',
replace_existing=True,
)
logger.info(f"Scheduled periodic task: schedule_pending_screenshots (every {screenshots_interval}s)")
# Add periodic job for retrying stuck image imports (every 5 minutes)
scheduler.add_job(
retry_stuck_image_imports,
trigger=IntervalTrigger(seconds=300),
id='retry_stuck_image_imports',
replace_existing=True,
)
logger.info("Scheduled periodic task: retry_stuck_image_imports (every 300s)")
# Flush Redis-buffered click counts to the database (every 60 seconds)
scheduler.add_job(
flush_click_buffer,
trigger=IntervalTrigger(seconds=60),
id='flush_click_buffer',
replace_existing=True,
)
logger.info("Scheduled periodic task: flush_click_buffer (every 60s)")
# ── nginxmon: monthly geo DB refresh ──────────────────────────────────
try:
from nginxmon.geo import download_geo_db as refresh_geo_db
scheduler.add_job(
schedule_pending_pages,
trigger=IntervalTrigger(seconds=120),
id='schedule_pending_pages',
replace_existing=True
refresh_geo_db,
trigger=IntervalTrigger(days=30),
id='refresh_geo_db',
replace_existing=True,
)
logger.info("Scheduled periodic task: schedule_pending_pages")
logger.info('nginxmon: scheduled geo DB refresh (every 30 days)')
except Exception as exc:
logger.warning('nginxmon: geo DB scheduler setup failed: %s', exc)
# ── routermon ──────────────────────────────────────────────────────────
try:
from routermon.tasks import cleanup_old_queries
from routermon.models import RouterMonSettings
from routermon.receiver import start_receiver
rm_settings = RouterMonSettings.get()
if rm_settings.enabled:
start_receiver(rm_settings.syslog_port)
scheduler.add_job(
cleanup_old_queries,
trigger=IntervalTrigger(hours=6),
id='routermon_cleanup',
replace_existing=True,
)
logger.info("routermon: scheduled cleanup job (every 6h)")
except Exception as exc:
import sys
print(f'routermon: startup error (non-fatal): {exc}', file=sys.stderr, flush=True)
logger.warning("routermon: startup error (non-fatal): %s", exc)
+8
View File
@@ -0,0 +1,8 @@
from django.conf import settings
def build_info(request):
return {
'BUILD_TIME': getattr(settings, 'BUILD_TIME', ''),
'BUILD_VERSION': getattr(settings, 'BUILD_VERSION', ''),
}
+41 -39
View File
@@ -21,22 +21,13 @@ INSTALLED_APPS = [
'simplemde',
'markdown', # 只需要基本的markdown包
'invest',
'netscan',
'nginxmon',
'routermon',
]
ROOT_URLCONF = 'core.urls'
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'core.middleware.CustomLocaleMiddleware', # 替换原来的 LocaleMiddleware
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
]
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
@@ -48,6 +39,7 @@ TEMPLATES = [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'core.context_processors.build_info',
],
},
},
@@ -57,7 +49,8 @@ TEMPLATES = [
# 添加以下基本设置(如果尚未存在)
SECRET_KEY = 'your-secret-key-here' # 请更改为一个安全的随机值
DEBUG = True
DEBUG = os.environ.get('DEBUG', 'True').lower() in ('true', '1', 'yes')
ALLOWED_HOSTS = ['*']
# 数据库设置(使用默认的SQLite配置)
@@ -114,6 +107,24 @@ STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
CSRF_TRUSTED_ORIGINS = os.environ.get('CSRF_TRUSTED_ORIGINS', 'http://localhost:8000').split(',')
# Redis cache
REDIS_URL = os.environ.get('REDIS_URL', 'redis://192.168.1.2:6379/0')
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': REDIS_URL,
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
},
'TIMEOUT': 300,
}
}
# Store sessions in Redis instead of the database
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'
# SimpleMDE 配置
SIMPLEMDE_OPTIONS = {
'placeholder': 'Type here...',
@@ -129,6 +140,12 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'data', 'media')
MUSIC_ROOT = os.path.join(BASE_DIR, 'data', 'music')
# File uploads folder — override via FILE_UPLOADS_FOLDER env var.
# Defaults to ~/Downloads locally; set to /uploads on k8s.
FILE_UPLOADS_FOLDER = os.environ.get('FILE_UPLOADS_FOLDER', os.path.expanduser('~/Downloads'))
IMAGES_FOLDER = os.environ.get('IMAGES_FOLDER', os.path.expanduser('~/Pictures'))
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
@@ -150,15 +167,6 @@ LOGGING = {
},
}
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
'UNAUTHENTICATED_USER': None, # 添加这行
}
LANGUAGE_URL_MAP = {
'en': 'en',
'zh-hans': 'zh',
@@ -178,6 +186,7 @@ LOCALE_INDEPENDENT_PATHS = [
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'core.middleware.CustomLocaleMiddleware', # 使用自定义中间件
'django.middleware.common.CommonMiddleware',
@@ -191,6 +200,8 @@ REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
'DEFAULT_AUTHENTICATION_CLASSES': [],
'DEFAULT_PERMISSION_CLASSES': [],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10,
'UNAUTHENTICATED_USER': None,
@@ -218,21 +229,12 @@ R2_CUSTOM_DOMAIN = os.environ.get('R2_CUSTOM_DOMAIN')
CRAWL4AI_API_URL = os.environ.get('CRAWL4AI_API_URL', 'https://crawl-api.junv.cc')
CRAWL4AI_ENABLED = os.environ.get('CRAWL4AI_ENABLED', 'True').lower() in ('true', '1', 'yes')
# For debugging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'level': 'DEBUG',
},
},
'loggers': {
'links': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': True,
},
},
# Build metadata (injected at Docker image build time)
BUILD_TIME = os.environ.get('BUILD_TIME', '')
BUILD_VERSION = os.environ.get('BUILD_VERSION', '')
LOGGING['loggers']['links'] = {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': False,
}
+39 -1
View File
@@ -3,15 +3,35 @@ from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from django.views.static import serve
from django.views.generic import TemplateView
from django.http import FileResponse, Http404
from links.views import LinkDetailView, LinkUpdateView, CustomLinkView
from links.file_views import PublicFileView, import_image_view
from django.urls import path, include, re_path
from django.conf.urls.i18n import i18n_patterns
import os
def _serve_static_file(filename, content_type):
"""Serve a file directly from the committed static/ directory."""
def view(request):
path = os.path.join(settings.BASE_DIR, 'static', filename)
try:
return FileResponse(open(path, 'rb'), content_type=content_type)
except FileNotFoundError:
raise Http404
return view
urlpatterns = [
# LLM-friendly discovery endpoints — must be FIRST before any catch-all routes
path('llms.txt', _serve_static_file('llms.txt', 'text/plain; charset=utf-8'), name='llms-txt'),
path('.well-known/ai-plugin.json', _serve_static_file('.well-known/ai-plugin.json', 'application/json'), name='ai-plugin-json'),
path('admin/', admin.site.urls),
# Add API URLs before locale URLs
path('api/', include('links.api_urls')), # New line for API routes
path('api/invest/', include('invest.urls', namespace='invest-api')),
path('api/invest/', include('invest.api_urls')),
# Media files
path('media/<path:path>', serve, {
'document_root': settings.MEDIA_ROOT,
@@ -28,6 +48,24 @@ urlpatterns = [
path('custom/<slug:alias>/edit/', LinkUpdateView.as_view(), name='custom_link_update'),
path('invest/', include('invest.urls')),
# Include netscan and files BEFORE links.urls to prevent the alias catch-all from intercepting them
path('ui/netscan/', include('netscan.urls')),
path('ui/nginxmon/', include('nginxmon.urls')),
path('ui/routermon/', include('routermon.urls')),
path('ui/files/', include('links.file_urls')),
# Import external image by URL — /import/images/<path:image_url> (also plural alias)
path('import/images/<path:image_url>', import_image_view, name='import-image'),
path('imports/images/<path:image_url>', import_image_view, name='imports-image'),
# Public file access — /public/files/{uuid}-{filename}
re_path(
r'^public/files/(?P<pk>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})-(?P<filename>.+)$',
PublicFileView.as_view(),
name='public-file',
),
# Include main app URLs with locale
path('', include('links.urls')),
]