mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-06-06 08:51:20 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 82378ab824 | |||
| 131e838399 |
@@ -2,7 +2,7 @@
|
||||
|
||||
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
||||
# Semver means never use .01, or 00. Should be .1.
|
||||
__version__ = '0.55.4'
|
||||
__version__ = '0.55.3'
|
||||
|
||||
from changedetectionio.strtobool import strtobool
|
||||
from json.decoder import JSONDecodeError
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
|
||||
from changedetectionio.validate_url import is_safe_valid_url
|
||||
@@ -279,28 +278,8 @@ class WatchSingleHistory(Resource):
|
||||
if request.args.get('html'):
|
||||
content = watch.get_fetched_html(timestamp)
|
||||
if content:
|
||||
# XSS mitigation (GHSA-cgj8-g98g-4p9x): this is an API endpoint, not a
|
||||
# browser-rendered view. The bytes ARE HTML (that's what the caller asked
|
||||
# for) but a programmatic client doesn't need text/html — and serving
|
||||
# text/html lets attacker-planted <script> in a monitored site execute
|
||||
# in our origin if someone opens the URL in a browser.
|
||||
#
|
||||
# text/plain + explicit utf-8 + nosniff = browser shows inert text,
|
||||
# sniffing can't re-classify it as HTML, an absent charset can't be
|
||||
# auto-detected as UTF-7 (an alternative XSS vector). API clients
|
||||
# still get the raw bytes — they don't care about Content-Type.
|
||||
response = make_response(content, 200)
|
||||
response.headers['Content-Type'] = 'text/plain; charset=utf-8'
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
# Include the timestamp in the download name so downloading multiple
|
||||
# snapshots doesn't collide. No extension — the stored bytes are
|
||||
# "whatever the fetcher captured" (HTML, JSON, XML, text…), so
|
||||
# claiming .html on the download would be a false content-type label
|
||||
# for non-HTML watches. The user/curl can rename if needed.
|
||||
# Strip to safe filename chars (timestamp is already validated as a
|
||||
# watch.history key — this is defense in depth against header injection).
|
||||
safe_ts = re.sub(r'[^0-9A-Za-z_-]', '', str(timestamp))[:32] or 'snapshot'
|
||||
response.headers['Content-Disposition'] = f'attachment; filename="snapshot-{safe_ts}"'
|
||||
response.mimetype = "text/html"
|
||||
else:
|
||||
response = make_response("No content found", 404)
|
||||
response.mimetype = "text/plain"
|
||||
|
||||
@@ -39,7 +39,6 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
'llm_provider_kind': _stored_llm.get('provider_kind', ''),
|
||||
'llm_local_token_multiplier': _stored_llm.get('local_token_multiplier', 5),
|
||||
'llm_change_summary_default': datastore.data['settings']['application'].get('llm_change_summary_default', ''),
|
||||
'llm_enabled': datastore.data['settings']['application'].get('llm_enabled', True),
|
||||
'llm_override_diff_with_summary': datastore.data['settings']['application'].get('llm_override_diff_with_summary', True),
|
||||
'llm_restock_use_fallback_extract': datastore.data['settings']['application'].get('llm_restock_use_fallback_extract', True),
|
||||
'llm_debug': datastore.data['settings']['application'].get('llm_debug', False),
|
||||
@@ -121,9 +120,6 @@ def construct_blueprint(datastore: ChangeDetectionStore):
|
||||
datastore.data['settings']['application']['llm_change_summary_default'] = (
|
||||
llm_data.get('llm_change_summary_default') or ''
|
||||
).strip()
|
||||
datastore.data['settings']['application']['llm_enabled'] = (
|
||||
bool(llm_data.get('llm_enabled', True))
|
||||
)
|
||||
datastore.data['settings']['application']['llm_override_diff_with_summary'] = (
|
||||
bool(llm_data.get('llm_override_diff_with_summary', True))
|
||||
)
|
||||
|
||||
@@ -69,17 +69,6 @@
|
||||
{% call stab_pane('provider') %}
|
||||
<p class="stab-section-title">{{ _('AI Provider') }}</p>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label></label>
|
||||
{{ form.llm.form.llm_enabled() }}
|
||||
<label for="{{ form.llm.form.llm_enabled.id }}" style="display:inline; font-weight:normal;">
|
||||
{{ form.llm.form.llm_enabled.label.text }}
|
||||
</label>
|
||||
<span class="pure-form-message-inline">
|
||||
{{ _('Master switch — when off, all AI lookups are skipped even if a provider is configured below.') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if not llm_env_configured and not (llm_config and llm_config.get('model')) %}
|
||||
<div class="stab-overview-disclaimer">
|
||||
<div class="stab-disclaimer-icon">⚠</div>
|
||||
|
||||
@@ -1193,14 +1193,6 @@ class globalSettingsLLMForm(Form):
|
||||
"style": "width: 10em;",
|
||||
},
|
||||
)
|
||||
# Master on/off switch for ALL LLM lookups at runtime. When False, every entry point
|
||||
# in evaluator.py (and the restock fallback) short-circuits with a logger.debug
|
||||
# message — even if a provider+model is still configured. Saved config and the
|
||||
# "configured" badge remain visible so the user can toggle back on without re-entering.
|
||||
llm_enabled = BooleanField(
|
||||
_l('Enable AI / LLM features'),
|
||||
default=True,
|
||||
)
|
||||
llm_override_diff_with_summary = BooleanField(
|
||||
_l('Replace {{diff}} notification token with AI summary'),
|
||||
default=True,
|
||||
|
||||
@@ -228,28 +228,6 @@ def llm_configured_via_env() -> bool:
|
||||
return bool(os.getenv('LLM_MODEL', '').strip())
|
||||
|
||||
|
||||
def _runtime_llm_config(datastore) -> dict | None:
|
||||
"""
|
||||
Runtime gate used by every LLM entry point in this module (and the restock
|
||||
fallback). Returns the resolved config dict only when both:
|
||||
- the master 'llm_enabled' toggle is on (default True)
|
||||
- a provider+model is actually configured
|
||||
|
||||
When the toggle is off but a config exists, logs a debug message and returns
|
||||
None so callers fall through their existing "not configured" early-return path.
|
||||
|
||||
The settings UI deliberately still calls get_llm_config() directly so the
|
||||
"AI / LLM configured: ..." badge keeps showing the saved provider even while
|
||||
the toggle is off.
|
||||
"""
|
||||
cfg = get_llm_config(datastore)
|
||||
if not bool(datastore.data['settings']['application'].get('llm_enabled', True)):
|
||||
if cfg:
|
||||
logger.debug("LLM features disabled via settings (llm_enabled=False) — skipping LLM lookup")
|
||||
return None
|
||||
return cfg
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Global monthly token budget
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -401,7 +379,7 @@ def run_setup(watch, datastore, snapshot_text: str) -> None:
|
||||
Stores result in watch['llm_prefilter'] (str selector or None).
|
||||
Called once when intent is first set, and again if pre-filter returns zero matches.
|
||||
"""
|
||||
cfg = _runtime_llm_config(datastore)
|
||||
cfg = get_llm_config(datastore)
|
||||
if not cfg:
|
||||
return
|
||||
|
||||
@@ -531,7 +509,7 @@ def summarise_change(watch, datastore, diff: str, current_snapshot: str = '') ->
|
||||
The result replaces {{ diff }} in notifications so the user gets a
|
||||
readable description instead of raw +/- diff lines.
|
||||
"""
|
||||
cfg = _runtime_llm_config(datastore)
|
||||
cfg = get_llm_config(datastore)
|
||||
if not cfg:
|
||||
return ''
|
||||
|
||||
@@ -619,7 +597,7 @@ def preview_extract(watch, datastore, content: str) -> dict | None:
|
||||
|
||||
Returns {'found': bool, 'answer': str} or None if LLM not configured / no intent.
|
||||
"""
|
||||
cfg = _runtime_llm_config(datastore)
|
||||
cfg = get_llm_config(datastore)
|
||||
if not cfg:
|
||||
return None
|
||||
|
||||
@@ -670,7 +648,7 @@ def evaluate_change(watch, datastore, diff: str, current_snapshot: str = '') ->
|
||||
|
||||
Results are cached by (intent, diff) hash — each unique diff is evaluated exactly once.
|
||||
"""
|
||||
cfg = _runtime_llm_config(datastore)
|
||||
cfg = get_llm_config(datastore)
|
||||
if not cfg:
|
||||
return None
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ class model(dict):
|
||||
'shared_diff_access': False,
|
||||
'strip_ignored_lines': False,
|
||||
'tags': None, # Initialized in __init__ with real datastore_path
|
||||
'llm_enabled': True,
|
||||
'llm_thinking_budget': LLM_DEFAULT_THINKING_BUDGET,
|
||||
'llm_max_summary_tokens': LLM_DEFAULT_MAX_SUMMARY_TOKENS,
|
||||
'webdriver_delay': None , # Extra delay in seconds before extracting text
|
||||
|
||||
@@ -203,17 +203,15 @@ def get_itemprop_availability_override(content, fetcher_name, fetcher_instance,
|
||||
return None
|
||||
|
||||
try:
|
||||
from changedetectionio.llm.evaluator import _runtime_llm_config, accumulate_global_tokens
|
||||
from changedetectionio.llm.evaluator import get_llm_config, accumulate_global_tokens
|
||||
from changedetectionio.llm import client as llm_client
|
||||
except ImportError as e:
|
||||
logger.debug(f"LLM restock fallback: LLM libraries not available ({e})")
|
||||
return None
|
||||
|
||||
# _runtime_llm_config returns None (with a debug log) when the master 'llm_enabled'
|
||||
# toggle is off, so this path is gated for free.
|
||||
llm_cfg = _runtime_llm_config(datastore)
|
||||
llm_cfg = get_llm_config(datastore)
|
||||
if not llm_cfg or not llm_cfg.get('model'):
|
||||
logger.debug("LLM restock fallback: no LLM model configured or LLM disabled, skipping")
|
||||
logger.debug("LLM restock fallback: no LLM model configured, skipping")
|
||||
return None
|
||||
|
||||
text_content = _strip_html(content) if content else ''
|
||||
|
||||
@@ -9,7 +9,7 @@ import json
|
||||
import threading
|
||||
import uuid as uuid_module
|
||||
from flask import url_for
|
||||
from .util import live_server_setup, wait_for_all_checks, wait_for_watch_history, delete_all_watches
|
||||
from .util import live_server_setup, wait_for_all_checks, delete_all_watches
|
||||
import os
|
||||
|
||||
|
||||
@@ -653,80 +653,6 @@ def test_api_history_edge_cases(client, live_server, measure_memory_usage, datas
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
def test_api_history_html_does_not_serve_as_text_html(client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
GHSA-cgj8-g98g-4p9x: GET /api/v1/watch/<uuid>/history/<timestamp>?html=true
|
||||
must not serve the stored snapshot with Content-Type: text/html. The bytes
|
||||
are an external site's HTML — if the response is labelled text/html, a
|
||||
<script> the attacker planted on that site executes in our origin when an
|
||||
operator opens the URL in a browser (stored XSS).
|
||||
|
||||
The fix is text/plain; charset=utf-8 + X-Content-Type-Options: nosniff so
|
||||
browsers render inert text and can't sniff back to HTML/UTF-7. API clients
|
||||
don't care about Content-Type and still receive the same bytes.
|
||||
|
||||
This test injects the snapshot directly via Watch.save_history_blob() and
|
||||
save_last_fetched_html() so we exercise the API endpoint's response
|
||||
shaping without depending on the live-fetch pipeline.
|
||||
"""
|
||||
api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token')
|
||||
|
||||
test_url = url_for('test_endpoint', _external=True)
|
||||
res = client.post(
|
||||
url_for("createwatch"),
|
||||
data=json.dumps({"url": test_url}),
|
||||
headers={'content-type': 'application/json', 'x-api-key': api_key},
|
||||
)
|
||||
watch_uuid = res.json.get('uuid')
|
||||
|
||||
# Plant a payload that would execute if the response were rendered as HTML.
|
||||
malicious_html = (
|
||||
"<html><body>"
|
||||
"<script>window.__CD_XSS_PROBE = 1</script>"
|
||||
"<img src=x onerror=\"window.__CD_XSS_PROBE = 1\">"
|
||||
"</body></html>"
|
||||
)
|
||||
ts = '1700000000'
|
||||
watch = live_server.app.config['DATASTORE'].data['watching'][watch_uuid]
|
||||
watch.save_history_blob(contents=malicious_html, timestamp=ts, snapshot_id=ts)
|
||||
watch.save_last_fetched_html(timestamp=ts, contents=malicious_html)
|
||||
|
||||
# The actual XSS-relevant assertion: how is the snapshot served?
|
||||
res = client.get(
|
||||
url_for("watchsinglehistory", uuid=watch_uuid, timestamp=ts) + '?html=true',
|
||||
headers={'x-api-key': api_key},
|
||||
)
|
||||
assert res.status_code == 200, f"unexpected status {res.status_code}: {res.data!r}"
|
||||
|
||||
ctype = res.headers.get('Content-Type', '')
|
||||
assert 'text/html' not in ctype, \
|
||||
f"snapshot must not be served as text/html (got {ctype!r}) — see GHSA-cgj8-g98g-4p9x"
|
||||
# Explicit utf-8 closes the UTF-7 sniffing bypass — without a charset, some
|
||||
# browsers will auto-detect UTF-7 from byte patterns and a crafted snapshot
|
||||
# can still execute via `+ADw-script+AD4-...`
|
||||
assert 'charset=utf-8' in ctype.lower(), \
|
||||
f"Content-Type must pin charset=utf-8 to defeat UTF-7 sniffing XSS (got {ctype!r})"
|
||||
|
||||
nosniff = res.headers.get('X-Content-Type-Options', '')
|
||||
assert nosniff.lower() == 'nosniff', \
|
||||
f"X-Content-Type-Options: nosniff required to defeat MIME-sniffing (got {nosniff!r})"
|
||||
|
||||
# Download filename should include the timestamp so multiple snapshots from
|
||||
# the same watch don't overwrite each other on disk.
|
||||
disp = res.headers.get('Content-Disposition', '')
|
||||
assert 'attachment' in disp and ts in disp, \
|
||||
f"Content-Disposition should be attachment + per-timestamp filename (got {disp!r})"
|
||||
|
||||
# API contract: the raw bytes must still be the original HTML — programmatic
|
||||
# consumers depend on getting the stored snapshot back.
|
||||
assert b'<script>' in res.data, \
|
||||
"Response body must still contain the raw stored bytes (the API contract)"
|
||||
|
||||
# Cleanup
|
||||
client.delete(url_for("watch", uuid=watch_uuid), headers={'x-api-key': api_key})
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
def test_api_notification_edge_cases(client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
Test notification configuration edge cases.
|
||||
|
||||
@@ -842,10 +842,6 @@ msgstr ""
|
||||
msgid "AI Provider"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Third-party data transfer — please read"
|
||||
msgstr ""
|
||||
@@ -3182,10 +3178,6 @@ msgstr "Měsíční rozpočet tokenů"
|
||||
msgid "Max input characters"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Enable AI / LLM features"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Replace {{diff}} notification token with AI summary"
|
||||
msgstr ""
|
||||
|
||||
@@ -858,10 +858,6 @@ msgstr ""
|
||||
msgid "AI Provider"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Third-party data transfer — please read"
|
||||
msgstr ""
|
||||
@@ -3234,10 +3230,6 @@ msgstr ""
|
||||
msgid "Max input characters"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Enable AI / LLM features"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Replace {{diff}} notification token with AI summary"
|
||||
msgstr ""
|
||||
|
||||
@@ -840,10 +840,6 @@ msgstr ""
|
||||
msgid "AI Provider"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Third-party data transfer — please read"
|
||||
msgstr ""
|
||||
@@ -3176,10 +3172,6 @@ msgstr ""
|
||||
msgid "Max input characters"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Enable AI / LLM features"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Replace {{diff}} notification token with AI summary"
|
||||
msgstr ""
|
||||
|
||||
@@ -840,10 +840,6 @@ msgstr ""
|
||||
msgid "AI Provider"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Third-party data transfer — please read"
|
||||
msgstr ""
|
||||
@@ -3176,10 +3172,6 @@ msgstr ""
|
||||
msgid "Max input characters"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Enable AI / LLM features"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Replace {{diff}} notification token with AI summary"
|
||||
msgstr ""
|
||||
|
||||
@@ -878,10 +878,6 @@ msgstr ""
|
||||
msgid "AI Provider"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Third-party data transfer — please read"
|
||||
msgstr ""
|
||||
@@ -3249,10 +3245,6 @@ msgstr ""
|
||||
msgid "Max input characters"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Enable AI / LLM features"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Replace {{diff}} notification token with AI summary"
|
||||
msgstr ""
|
||||
|
||||
@@ -846,10 +846,6 @@ msgstr ""
|
||||
msgid "AI Provider"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Third-party data transfer — please read"
|
||||
msgstr ""
|
||||
@@ -3189,10 +3185,6 @@ msgstr ""
|
||||
msgid "Max input characters"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Enable AI / LLM features"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Replace {{diff}} notification token with AI summary"
|
||||
msgstr ""
|
||||
|
||||
@@ -842,10 +842,6 @@ msgstr ""
|
||||
msgid "AI Provider"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Third-party data transfer — please read"
|
||||
msgstr ""
|
||||
@@ -3178,10 +3174,6 @@ msgstr ""
|
||||
msgid "Max input characters"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Enable AI / LLM features"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Replace {{diff}} notification token with AI summary"
|
||||
msgstr ""
|
||||
|
||||
@@ -847,10 +847,6 @@ msgstr ""
|
||||
msgid "AI Provider"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Third-party data transfer — please read"
|
||||
msgstr ""
|
||||
@@ -3195,10 +3191,6 @@ msgstr ""
|
||||
msgid "Max input characters"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Enable AI / LLM features"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Replace {{diff}} notification token with AI summary"
|
||||
msgstr ""
|
||||
|
||||
@@ -844,10 +844,6 @@ msgstr "AI 프로바이더 설정"
|
||||
msgid "AI Provider"
|
||||
msgstr "AI 프로바이더"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Third-party data transfer — please read"
|
||||
msgstr "제3자 데이터 전송 - 읽어 주세요"
|
||||
@@ -3186,10 +3182,6 @@ msgstr "월간 토큰 예산"
|
||||
msgid "Max input characters"
|
||||
msgstr "최대 입력 문자 수"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Enable AI / LLM features"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Replace {{diff}} notification token with AI summary"
|
||||
msgstr "{{diff}} 알림 토큰을 AI 요약으로 대체"
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io 0.55.4\n"
|
||||
"Project-Id-Version: changedetection.io 0.55.3\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-05-19 18:05+0200\n"
|
||||
"POT-Creation-Date: 2026-05-19 10:29+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -839,10 +839,6 @@ msgstr ""
|
||||
msgid "AI Provider"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Third-party data transfer — please read"
|
||||
msgstr ""
|
||||
@@ -3175,10 +3171,6 @@ msgstr ""
|
||||
msgid "Max input characters"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Enable AI / LLM features"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Replace {{diff}} notification token with AI summary"
|
||||
msgstr ""
|
||||
|
||||
@@ -865,10 +865,6 @@ msgstr ""
|
||||
msgid "AI Provider"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Third-party data transfer — please read"
|
||||
msgstr ""
|
||||
@@ -3226,10 +3222,6 @@ msgstr ""
|
||||
msgid "Max input characters"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Enable AI / LLM features"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Replace {{diff}} notification token with AI summary"
|
||||
msgstr ""
|
||||
|
||||
@@ -875,10 +875,6 @@ msgstr ""
|
||||
msgid "AI Provider"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Third-party data transfer — please read"
|
||||
msgstr ""
|
||||
@@ -3229,10 +3225,6 @@ msgstr ""
|
||||
msgid "Max input characters"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Enable AI / LLM features"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Replace {{diff}} notification token with AI summary"
|
||||
msgstr ""
|
||||
|
||||
@@ -855,10 +855,6 @@ msgstr ""
|
||||
msgid "AI Provider"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Third-party data transfer — please read"
|
||||
msgstr ""
|
||||
@@ -3208,10 +3204,6 @@ msgstr ""
|
||||
msgid "Max input characters"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Enable AI / LLM features"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Replace {{diff}} notification token with AI summary"
|
||||
msgstr ""
|
||||
|
||||
@@ -844,10 +844,6 @@ msgstr ""
|
||||
msgid "AI Provider"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Third-party data transfer — please read"
|
||||
msgstr ""
|
||||
@@ -3181,10 +3177,6 @@ msgstr ""
|
||||
msgid "Max input characters"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Enable AI / LLM features"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Replace {{diff}} notification token with AI summary"
|
||||
msgstr ""
|
||||
|
||||
@@ -843,10 +843,6 @@ msgstr ""
|
||||
msgid "AI Provider"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Master switch — when off, all AI lookups are skipped even if a provider is configured below."
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html
|
||||
msgid "Third-party data transfer — please read"
|
||||
msgstr ""
|
||||
@@ -3180,10 +3176,6 @@ msgstr ""
|
||||
msgid "Max input characters"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Enable AI / LLM features"
|
||||
msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Replace {{diff}} notification token with AI summary"
|
||||
msgstr ""
|
||||
|
||||
@@ -432,13 +432,9 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec
|
||||
update_obj['_llm_result'] = None
|
||||
update_obj['_llm_intent'] = ''
|
||||
update_obj['_llm_change_summary'] = ''
|
||||
# skip_check: when budget exceeded, don't run LLM or the check.
|
||||
# Also gated on llm_enabled — a disabled LLM can't be spending tokens,
|
||||
# so the budget enforcement shouldn't suppress changes when the user
|
||||
# has explicitly switched LLM off.
|
||||
_llm_master_enabled = bool(datastore.data['settings']['application'].get('llm_enabled', True))
|
||||
# skip_check: when budget exceeded, don't run LLM or the check
|
||||
_llm_budget_action = datastore.data['settings']['application'].get('llm_budget_action', 'skip_llm')
|
||||
if _llm_master_enabled and _llm_budget_action == 'skip_check':
|
||||
if _llm_budget_action == 'skip_check':
|
||||
from changedetectionio.llm.evaluator import is_global_token_budget_exceeded
|
||||
if is_global_token_budget_exceeded(datastore):
|
||||
logger.info(f"LLM monthly budget exceeded — skipping check for {uuid} (budget_action=skip_check)")
|
||||
@@ -448,14 +444,9 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore, exec
|
||||
try:
|
||||
from changedetectionio.llm.evaluator import (
|
||||
evaluate_change, resolve_intent, resolve_llm_field,
|
||||
summarise_change, _runtime_llm_config,
|
||||
summarise_change, get_llm_config,
|
||||
)
|
||||
# _runtime_llm_config returns None (and logs a debug skip
|
||||
# message) when the master 'llm_enabled' toggle is off, so
|
||||
# the whole block — diff computation, status minitext, and
|
||||
# the two executor dispatches — is skipped, not just the
|
||||
# inner LLM lookups.
|
||||
_llm_cfg = _runtime_llm_config(datastore)
|
||||
_llm_cfg = get_llm_config(datastore)
|
||||
if _llm_cfg:
|
||||
# Compute unified diff once — used by both intent and summary
|
||||
_watch_dates = list(watch.history.keys())
|
||||
|
||||
Reference in New Issue
Block a user