Revert "ВРЕМЕННЫЙ ОТКАТ к v1.0.91 — для проверки деплоя. Вернуть через revert."

This reverts commit c4fe57c908.
This commit is contained in:
2026-06-22 09:08:26 +04:00
parent c4fe57c908
commit bb7fd6bbf1
4 changed files with 124 additions and 40 deletions
+74 -33
View File
@@ -16,6 +16,14 @@
<cfelseif NOT len(supplementId)>
<cfset result.error = "supplement_id required (JSON body)">
<cfelse>
<!--- Идемпотентность --->
<cfquery name="dupCheck" datasource="baza">
SELECT COUNT(*) as cnt FROM spec_events
WHERE supplement_id = <cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">
</cfquery>
<cfif dupCheck.cnt GT 0>
<cfset result = {ok: true, skipped: true, message: "supplement already processed"}>
<cfelse>
<cftransaction>
<cfset seq = 0>
<cfquery name="sq" datasource="baza">
@@ -24,28 +32,31 @@
</cfquery>
<cfset seq = sq.m>
<!--- full_replace: явные DELETE для аудита --->
<!--- full_replace: guard от пустого ops --->
<cfif url.mode EQ "full_replace" AND arrayLen(ops) EQ 0>
<cfthrow message="full_replace: LLM вернул пустой ops, спецификация НЕ изменена">
</cfif>
<!--- full_replace: явные DELETE для аудита (массовый INSERT) --->
<cfif url.mode EQ "full_replace">
<cfquery name="curRows" datasource="baza">
SELECT name_hash, name, price, qty, sum, date_start
<cfquery datasource="baza">
INSERT INTO spec_events (contract_id, supplement_id, seq, action, target_hash, comment, status)
SELECT
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#seq#" cfsqltype="cf_sql_integer"> + ROW_NUMBER() OVER (),
'DELETE',
name_hash,
'full_replace',
'applied'
FROM spec_current
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
</cfquery>
<cfloop query="curRows">
<cfset seq++>
<cfquery datasource="baza">
INSERT INTO spec_events (contract_id, supplement_id, seq, action, target_hash, comment, status)
VALUES (
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#seq#" cfsqltype="cf_sql_integer">,
'DELETE',
<cfqueryparam value="#curRows.name_hash#" cfsqltype="cf_sql_varchar">,
'full_replace',
'applied'
)
</cfquery>
</cfloop>
<cfquery name="cntDel" datasource="baza">
SELECT COUNT(*) as cnt FROM spec_current
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
</cfquery>
<cfset seq = seq + cntDel.cnt>
<cfquery datasource="baza">
DELETE FROM spec_current WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
</cfquery>
@@ -54,6 +65,25 @@
<!--- Применить операции --->
<cfset summary = {added: 0, updated: 0, deleted: 0, unresolved: 0}>
<!--- Pre-fetch: собрать все target_hash для UPDATE, один SELECT вместо N+1 --->
<cfset updateHashes = []>
<cfloop array="#ops#" index="op">
<cfif op.action EQ "UPDATE">
<cfset arrayAppend(updateHashes, op.target_hash)>
</cfif>
</cfloop>
<cfset oldValues = {}>
<cfif arrayLen(updateHashes) GT 0>
<cfquery name="oldBatch" datasource="baza">
SELECT name_hash, price, qty, sum, date_start FROM spec_current
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
AND name_hash IN (<cfloop from="1" to="#arrayLen(updateHashes)#" index="hi"><cfif hi GT 1>,</cfif><cfqueryparam value="#updateHashes[hi]#" cfsqltype="cf_sql_varchar"></cfloop>)
</cfquery>
<cfloop query="oldBatch">
<cfset oldValues[oldBatch.name_hash] = {price: oldBatch.price, qty: oldBatch.qty, sum: oldBatch.sum, date_start: oldBatch.date_start}>
</cfloop>
</cfif>
<cfloop array="#ops#" index="op">
<cfset seq++>
<cfset action = op.action>
@@ -61,9 +91,9 @@
<cfif action EQ "ADD">
<cfset nr = op.new_row>
<cfset nm = structKeyExists(nr, 'name') ? nr.name : ''>
<cfset pr = (structKeyExists(nr, 'price') AND len(nr.price)) ? nr.price : ''>
<cfset qt = (structKeyExists(nr, 'qty') AND len(nr.qty)) ? nr.qty : ''>
<cfset sm = (structKeyExists(nr, 'sum') AND len(nr.sum)) ? nr.sum : ''>
<cfset pr = (structKeyExists(nr, 'price') AND isNumeric(nr.price)) ? nr.price : ''>
<cfset qt = (structKeyExists(nr, 'qty') AND isNumeric(nr.qty)) ? nr.qty : ''>
<cfset sm = (structKeyExists(nr, 'sum') AND isNumeric(nr.sum)) ? nr.sum : ''>
<cfset ds = (structKeyExists(nr, 'date_start') AND len(nr.date_start)) ? nr.date_start : ''>
<cfquery name="evt" datasource="baza">
@@ -90,25 +120,34 @@
<cfqueryparam value="#qt#" cfsqltype="cf_sql_float" null="#len(qt) EQ 0#">,
<cfqueryparam value="#sm#" cfsqltype="cf_sql_float" null="#len(sm) EQ 0#">,
<cfqueryparam value="#ds#" cfsqltype="cf_sql_varchar" null="#len(ds) EQ 0#">,
<cfqueryparam value="#evt.id#" cfsqltype="cf_sql_varchar">
<cfqueryparam value="#evt.id#" cfsqltype="cf_sql_char" maxlength="36">,
)
</cfquery>
<cfset summary.added++>
<cfelseif action EQ "UPDATE">
<cfset th = op.target_hash>
<cfquery name="old" datasource="baza">
SELECT price, qty, sum, date_start FROM spec_current
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
AND name_hash = <cfqueryparam value="#th#" cfsqltype="cf_sql_varchar">
</cfquery>
<cfif old.recordCount EQ 0>
<cfthrow message="UPDATE target not found: #th#">
</cfif>
<cfif NOT structKeyExists(oldValues, th)>
<!--- target_hash не найден — создаём UNRESOLVED вместо падения --->
<cfquery datasource="baza">
INSERT INTO spec_events (contract_id, supplement_id, seq, action, new_values, comment, status)
VALUES (
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">,
<cfqueryparam value="#seq#" cfsqltype="cf_sql_integer">,
'UNRESOLVED',
<cfqueryparam value="#serializeJSON(structKeyExists(op,'new_values') ? op.new_values : {})#" cfsqltype="cf_sql_varchar">::jsonb,
<cfqueryparam value="UPDATE target_hash not found: #th#" cfsqltype="cf_sql_varchar">,
'pending'
)
</cfquery>
<cfset summary.unresolved++>
<cfelse>
<cfset old = oldValues[th]>
<cfset nv = structKeyExists(op, 'new_values') ? op.new_values : {}>
<cfset newPrice = structKeyExists(nv, 'price') AND len(nv.price) ? nv.price : old.price>
<cfset newQty = structKeyExists(nv, 'qty') AND len(nv.qty) ? nv.qty : old.qty>
<cfset newSum = structKeyExists(nv, 'sum') AND len(nv.sum) ? nv.sum : old.sum>
<cfset newPrice = structKeyExists(nv, 'price') AND isNumeric(nv.price) ? nv.price : old.price>
<cfset newQty = structKeyExists(nv, 'qty') AND isNumeric(nv.qty) ? nv.qty : old.qty>
<cfset newSum = structKeyExists(nv, 'sum') AND isNumeric(nv.sum) ? nv.sum : old.sum>
<cfset newDate = structKeyExists(nv, 'date_start') AND len(nv.date_start) ? nv.date_start : old.date_start>
<cfquery name="evt" datasource="baza">
@@ -138,6 +177,7 @@
AND name_hash = <cfqueryparam value="#th#" cfsqltype="cf_sql_varchar">
</cfquery>
<cfset summary.updated++>
</cfif>
<cfelseif action EQ "DELETE">
<cfset th = op.target_hash>
@@ -179,6 +219,7 @@
<cfset result = {ok: true, contract_id: url.contract_id, supplement_id: supplementId, summary: summary}>
</cftransaction>
</cfif>
</cfif>
<cfcatch>
<cfset result = {ok: false, error: cfcatch.message, detail: cfcatch.detail}>
+19 -3
View File
@@ -7,13 +7,13 @@ class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
"""Многопоточный HTTP-сервер."""
daemon_threads = True
from urllib.parse import urlparse, parse_qs
import subprocess, tempfile, os, sys, json, time
import subprocess, tempfile, os, sys, json, time, re
import httpx
from llm_prompt import build_prompt
LLM_URL = "https://api.aillm.ru/v1/chat/completions"
LLM_KEY = "sk-ucI5YvOticoOQ9Kuj5K9mQ"
LLM_KEY = os.environ.get("LLM_KEY", "")
LLM_MODEL = "gpt-oss-120b"
LUCEE_URL = "https://contractor.luceek8s.dev.nubes.ru"
@@ -91,6 +91,10 @@ class Handler(BaseHTTPRequestHandler):
if not cid:
self.send_error(400, "contract_id required")
return
# Validate UUID to prevent SQL injection
if not re.fullmatch(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', cid, re.I):
self.send_error(400, "invalid contract_id format")
return
self.send_response(200)
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
@@ -103,6 +107,12 @@ class Handler(BaseHTTPRequestHandler):
t0 = time.time()
try:
# 0. Сброс предыдущих результатов сравнения
r = httpx.get(f"{LUCEE_URL}/reset_contract.cfm?contract_id={cid}", timeout=10)
if r.status_code != 200:
self._sse({"type": "error", "message": f"reset failed: HTTP {r.status_code}"})
return
# 1. Список supplements
supps = self._lucee_query(
f"SELECT s.id, s.type, d.filename FROM supplements s "
@@ -140,9 +150,15 @@ class Handler(BaseHTTPRequestHandler):
if not docs:
continue
ej = docs[0]["elements_json"]
# Lucee JSONB может вернуть: строку, dict-обёртку {Value,Type}, или уже список
if isinstance(ej, dict) and "Value" in ej:
ej = ej["Value"]
elements = json.loads(ej)
if isinstance(ej, str):
elements = json.loads(ej)
elif isinstance(ej, list):
elements = ej
else:
elements = []
# Lucee serializeJSON → UPPERCASE keys, normalize to lowercase
elements = [{k.lower(): v for k, v in el.items()} for el in elements]
lines = []
+2 -4
View File
@@ -71,14 +71,14 @@
<body>
<div class="topbar">
<img src="/nubes-logo.svg" alt="Nubes">
<span class="title">Сверка договоров — LLM <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.91 — Lucee</span></span>
<span class="title">Сверка договоров — LLM AI-driven Event Sourcing <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.104 — Lucee</span></span>
</div>
<div class="content">
<div class="card">
<div class="card-header">
<i data-lucide="folder-open" style="width:18px;height:18px;"></i>
Загрузка договоров приложений спецификаций
Загрузка договоров/приложений/спецификаций
</div>
<div class="card-body">
<input type="file" id="fileInput" accept=".docx,.doc,.pdf,.zip" multiple style="margin-bottom:12px;width:100%;">
@@ -557,9 +557,7 @@ async function showText(i) {
}
rightHtml += '<div style="margin-top:10px;font-weight:600;font-size:12px;">Все элементы:</div>';
rightHtml += '<pre>' + textify + '</pre>';
rightHtml += '<div class="kv" style="margin-top:10px;"><span class="k">doc_id</span><span class="v">' + (f.doc_id || '—') + '</span></div>';
if (f.supp_id) {
rightHtml += '<div class="kv"><span class="k">supp_id</span><span class="v">' + f.supp_id + '</span></div>';
rightHtml += '<div class="kv"><span class="k">Тип ДС</span><span class="v">' + (f.supp_type || '—') + '</span></div>';
}
} catch(e) {
+29
View File
@@ -0,0 +1,29 @@
<cfsetting showdebugoutput="no" enablecfoutputonly="true">
<cfheader name="Content-Type" value="application/json; charset=utf-8">
<cfparam name="url.contract_id" default="">
<cfset result = {ok: false, error: ""}>
<cftry>
<cfif NOT len(url.contract_id)>
<cfset result.error = "contract_id required">
<cfelse>
<cftransaction>
<cfquery datasource="baza">
DELETE FROM spec_current
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
</cfquery>
<cfquery datasource="baza">
DELETE FROM spec_events
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
</cfquery>
</cftransaction>
<cfset result = {ok: true, contract_id: url.contract_id}>
</cfif>
<cfcatch>
<cfset result = {ok: false, error: cfcatch.message}>
</cfcatch>
</cftry>
<cfoutput>#serializeJSON(result)#</cfoutput>