ВРЕМЕННЫЙ ОТКАТ к v1.0.91 — для проверки деплоя. Вернуть через revert.
This commit is contained in:
+33
-74
@@ -16,14 +16,6 @@
|
|||||||
<cfelseif NOT len(supplementId)>
|
<cfelseif NOT len(supplementId)>
|
||||||
<cfset result.error = "supplement_id required (JSON body)">
|
<cfset result.error = "supplement_id required (JSON body)">
|
||||||
<cfelse>
|
<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>
|
<cftransaction>
|
||||||
<cfset seq = 0>
|
<cfset seq = 0>
|
||||||
<cfquery name="sq" datasource="baza">
|
<cfquery name="sq" datasource="baza">
|
||||||
@@ -32,31 +24,28 @@
|
|||||||
</cfquery>
|
</cfquery>
|
||||||
<cfset seq = sq.m>
|
<cfset seq = sq.m>
|
||||||
|
|
||||||
<!--- full_replace: guard от пустого ops --->
|
<!--- full_replace: явные DELETE для аудита --->
|
||||||
<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">
|
<cfif url.mode EQ "full_replace">
|
||||||
<cfquery datasource="baza">
|
<cfquery name="curRows" datasource="baza">
|
||||||
INSERT INTO spec_events (contract_id, supplement_id, seq, action, target_hash, comment, status)
|
SELECT name_hash, name, price, qty, sum, date_start
|
||||||
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
|
FROM spec_current
|
||||||
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
||||||
</cfquery>
|
</cfquery>
|
||||||
<cfquery name="cntDel" datasource="baza">
|
<cfloop query="curRows">
|
||||||
SELECT COUNT(*) as cnt FROM spec_current
|
<cfset seq++>
|
||||||
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
<cfquery datasource="baza">
|
||||||
</cfquery>
|
INSERT INTO spec_events (contract_id, supplement_id, seq, action, target_hash, comment, status)
|
||||||
<cfset seq = seq + cntDel.cnt>
|
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 datasource="baza">
|
<cfquery datasource="baza">
|
||||||
DELETE FROM spec_current WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
DELETE FROM spec_current WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
||||||
</cfquery>
|
</cfquery>
|
||||||
@@ -65,25 +54,6 @@
|
|||||||
<!--- Применить операции --->
|
<!--- Применить операции --->
|
||||||
<cfset summary = {added: 0, updated: 0, deleted: 0, unresolved: 0}>
|
<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">
|
<cfloop array="#ops#" index="op">
|
||||||
<cfset seq++>
|
<cfset seq++>
|
||||||
<cfset action = op.action>
|
<cfset action = op.action>
|
||||||
@@ -91,9 +61,9 @@
|
|||||||
<cfif action EQ "ADD">
|
<cfif action EQ "ADD">
|
||||||
<cfset nr = op.new_row>
|
<cfset nr = op.new_row>
|
||||||
<cfset nm = structKeyExists(nr, 'name') ? nr.name : ''>
|
<cfset nm = structKeyExists(nr, 'name') ? nr.name : ''>
|
||||||
<cfset pr = (structKeyExists(nr, 'price') AND isNumeric(nr.price)) ? nr.price : ''>
|
<cfset pr = (structKeyExists(nr, 'price') AND len(nr.price)) ? nr.price : ''>
|
||||||
<cfset qt = (structKeyExists(nr, 'qty') AND isNumeric(nr.qty)) ? nr.qty : ''>
|
<cfset qt = (structKeyExists(nr, 'qty') AND len(nr.qty)) ? nr.qty : ''>
|
||||||
<cfset sm = (structKeyExists(nr, 'sum') AND isNumeric(nr.sum)) ? nr.sum : ''>
|
<cfset sm = (structKeyExists(nr, 'sum') AND len(nr.sum)) ? nr.sum : ''>
|
||||||
<cfset ds = (structKeyExists(nr, 'date_start') AND len(nr.date_start)) ? nr.date_start : ''>
|
<cfset ds = (structKeyExists(nr, 'date_start') AND len(nr.date_start)) ? nr.date_start : ''>
|
||||||
|
|
||||||
<cfquery name="evt" datasource="baza">
|
<cfquery name="evt" datasource="baza">
|
||||||
@@ -120,34 +90,25 @@
|
|||||||
<cfqueryparam value="#qt#" cfsqltype="cf_sql_float" null="#len(qt) EQ 0#">,
|
<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="#sm#" cfsqltype="cf_sql_float" null="#len(sm) EQ 0#">,
|
||||||
<cfqueryparam value="#ds#" cfsqltype="cf_sql_varchar" null="#len(ds) EQ 0#">,
|
<cfqueryparam value="#ds#" cfsqltype="cf_sql_varchar" null="#len(ds) EQ 0#">,
|
||||||
<cfqueryparam value="#evt.id#" cfsqltype="cf_sql_char" maxlength="36">,
|
<cfqueryparam value="#evt.id#" cfsqltype="cf_sql_varchar">
|
||||||
)
|
)
|
||||||
</cfquery>
|
</cfquery>
|
||||||
<cfset summary.added++>
|
<cfset summary.added++>
|
||||||
|
|
||||||
<cfelseif action EQ "UPDATE">
|
<cfelseif action EQ "UPDATE">
|
||||||
<cfset th = op.target_hash>
|
<cfset th = op.target_hash>
|
||||||
<cfif NOT structKeyExists(oldValues, th)>
|
<cfquery name="old" datasource="baza">
|
||||||
<!--- target_hash не найден — создаём UNRESOLVED вместо падения --->
|
SELECT price, qty, sum, date_start FROM spec_current
|
||||||
<cfquery datasource="baza">
|
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
||||||
INSERT INTO spec_events (contract_id, supplement_id, seq, action, new_values, comment, status)
|
AND name_hash = <cfqueryparam value="#th#" cfsqltype="cf_sql_varchar">
|
||||||
VALUES (
|
</cfquery>
|
||||||
<cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">,
|
<cfif old.recordCount EQ 0>
|
||||||
<cfqueryparam value="#supplementId#" cfsqltype="cf_sql_varchar">,
|
<cfthrow message="UPDATE target not found: #th#">
|
||||||
<cfqueryparam value="#seq#" cfsqltype="cf_sql_integer">,
|
</cfif>
|
||||||
'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 nv = structKeyExists(op, 'new_values') ? op.new_values : {}>
|
||||||
<cfset newPrice = structKeyExists(nv, 'price') AND isNumeric(nv.price) ? nv.price : old.price>
|
<cfset newPrice = structKeyExists(nv, 'price') AND len(nv.price) ? nv.price : old.price>
|
||||||
<cfset newQty = structKeyExists(nv, 'qty') AND isNumeric(nv.qty) ? nv.qty : old.qty>
|
<cfset newQty = structKeyExists(nv, 'qty') AND len(nv.qty) ? nv.qty : old.qty>
|
||||||
<cfset newSum = structKeyExists(nv, 'sum') AND isNumeric(nv.sum) ? nv.sum : old.sum>
|
<cfset newSum = structKeyExists(nv, 'sum') AND len(nv.sum) ? nv.sum : old.sum>
|
||||||
<cfset newDate = structKeyExists(nv, 'date_start') AND len(nv.date_start) ? nv.date_start : old.date_start>
|
<cfset newDate = structKeyExists(nv, 'date_start') AND len(nv.date_start) ? nv.date_start : old.date_start>
|
||||||
|
|
||||||
<cfquery name="evt" datasource="baza">
|
<cfquery name="evt" datasource="baza">
|
||||||
@@ -177,7 +138,6 @@
|
|||||||
AND name_hash = <cfqueryparam value="#th#" cfsqltype="cf_sql_varchar">
|
AND name_hash = <cfqueryparam value="#th#" cfsqltype="cf_sql_varchar">
|
||||||
</cfquery>
|
</cfquery>
|
||||||
<cfset summary.updated++>
|
<cfset summary.updated++>
|
||||||
</cfif>
|
|
||||||
|
|
||||||
<cfelseif action EQ "DELETE">
|
<cfelseif action EQ "DELETE">
|
||||||
<cfset th = op.target_hash>
|
<cfset th = op.target_hash>
|
||||||
@@ -219,7 +179,6 @@
|
|||||||
|
|
||||||
<cfset result = {ok: true, contract_id: url.contract_id, supplement_id: supplementId, summary: summary}>
|
<cfset result = {ok: true, contract_id: url.contract_id, supplement_id: supplementId, summary: summary}>
|
||||||
</cftransaction>
|
</cftransaction>
|
||||||
</cfif>
|
|
||||||
</cfif>
|
</cfif>
|
||||||
<cfcatch>
|
<cfcatch>
|
||||||
<cfset result = {ok: false, error: cfcatch.message, detail: cfcatch.detail}>
|
<cfset result = {ok: false, error: cfcatch.message, detail: cfcatch.detail}>
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
|
|||||||
"""Многопоточный HTTP-сервер."""
|
"""Многопоточный HTTP-сервер."""
|
||||||
daemon_threads = True
|
daemon_threads = True
|
||||||
from urllib.parse import urlparse, parse_qs
|
from urllib.parse import urlparse, parse_qs
|
||||||
import subprocess, tempfile, os, sys, json, time, re
|
import subprocess, tempfile, os, sys, json, time
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from llm_prompt import build_prompt
|
from llm_prompt import build_prompt
|
||||||
|
|
||||||
LLM_URL = "https://api.aillm.ru/v1/chat/completions"
|
LLM_URL = "https://api.aillm.ru/v1/chat/completions"
|
||||||
LLM_KEY = os.environ.get("LLM_KEY", "")
|
LLM_KEY = "sk-ucI5YvOticoOQ9Kuj5K9mQ"
|
||||||
LLM_MODEL = "gpt-oss-120b"
|
LLM_MODEL = "gpt-oss-120b"
|
||||||
LUCEE_URL = "https://contractor.luceek8s.dev.nubes.ru"
|
LUCEE_URL = "https://contractor.luceek8s.dev.nubes.ru"
|
||||||
|
|
||||||
@@ -91,10 +91,6 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
if not cid:
|
if not cid:
|
||||||
self.send_error(400, "contract_id required")
|
self.send_error(400, "contract_id required")
|
||||||
return
|
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_response(200)
|
||||||
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
||||||
@@ -107,12 +103,6 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
|
|
||||||
try:
|
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
|
# 1. Список supplements
|
||||||
supps = self._lucee_query(
|
supps = self._lucee_query(
|
||||||
f"SELECT s.id, s.type, d.filename FROM supplements s "
|
f"SELECT s.id, s.type, d.filename FROM supplements s "
|
||||||
@@ -150,15 +140,9 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
if not docs:
|
if not docs:
|
||||||
continue
|
continue
|
||||||
ej = docs[0]["elements_json"]
|
ej = docs[0]["elements_json"]
|
||||||
# Lucee JSONB может вернуть: строку, dict-обёртку {Value,Type}, или уже список
|
|
||||||
if isinstance(ej, dict) and "Value" in ej:
|
if isinstance(ej, dict) and "Value" in ej:
|
||||||
ej = ej["Value"]
|
ej = ej["Value"]
|
||||||
if isinstance(ej, str):
|
elements = json.loads(ej)
|
||||||
elements = json.loads(ej)
|
|
||||||
elif isinstance(ej, list):
|
|
||||||
elements = ej
|
|
||||||
else:
|
|
||||||
elements = []
|
|
||||||
# Lucee serializeJSON → UPPERCASE keys, normalize to lowercase
|
# Lucee serializeJSON → UPPERCASE keys, normalize to lowercase
|
||||||
elements = [{k.lower(): v for k, v in el.items()} for el in elements]
|
elements = [{k.lower(): v for k, v in el.items()} for el in elements]
|
||||||
lines = []
|
lines = []
|
||||||
|
|||||||
@@ -71,14 +71,14 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
<img src="/nubes-logo.svg" alt="Nubes">
|
<img src="/nubes-logo.svg" alt="Nubes">
|
||||||
<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>
|
<span class="title">Сверка договоров — LLM <span style="font-weight:400;color:var(--muted);font-size:12px;">v1.0.91 — Lucee</span></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<i data-lucide="folder-open" style="width:18px;height:18px;"></i>
|
<i data-lucide="folder-open" style="width:18px;height:18px;"></i>
|
||||||
Загрузка договоров/приложений/спецификаций
|
Загрузка договоров приложений спецификаций
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<input type="file" id="fileInput" accept=".docx,.doc,.pdf,.zip" multiple style="margin-bottom:12px;width:100%;">
|
<input type="file" id="fileInput" accept=".docx,.doc,.pdf,.zip" multiple style="margin-bottom:12px;width:100%;">
|
||||||
@@ -557,7 +557,9 @@ async function showText(i) {
|
|||||||
}
|
}
|
||||||
rightHtml += '<div style="margin-top:10px;font-weight:600;font-size:12px;">Все элементы:</div>';
|
rightHtml += '<div style="margin-top:10px;font-weight:600;font-size:12px;">Все элементы:</div>';
|
||||||
rightHtml += '<pre>' + textify + '</pre>';
|
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) {
|
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>';
|
rightHtml += '<div class="kv"><span class="k">Тип ДС</span><span class="v">' + (f.supp_type || '—') + '</span></div>';
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
<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>
|
|
||||||
Reference in New Issue
Block a user