ВРЕМЕННЫЙ ОТКАТ к v1.0.91 — для проверки деплоя. Вернуть через revert.
This commit is contained in:
+33
-74
@@ -16,14 +16,6 @@
|
||||
<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">
|
||||
@@ -32,31 +24,28 @@
|
||||
</cfquery>
|
||||
<cfset seq = sq.m>
|
||||
|
||||
<!--- 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) --->
|
||||
<!--- full_replace: явные DELETE для аудита --->
|
||||
<cfif url.mode EQ "full_replace">
|
||||
<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'
|
||||
<cfquery name="curRows" datasource="baza">
|
||||
SELECT name_hash, name, price, qty, sum, date_start
|
||||
FROM spec_current
|
||||
WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
||||
</cfquery>
|
||||
<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>
|
||||
<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 datasource="baza">
|
||||
DELETE FROM spec_current WHERE contract_id = <cfqueryparam value="#url.contract_id#" cfsqltype="cf_sql_varchar">
|
||||
</cfquery>
|
||||
@@ -65,25 +54,6 @@
|
||||
<!--- Применить операции --->
|
||||
<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>
|
||||
@@ -91,9 +61,9 @@
|
||||
<cfif action EQ "ADD">
|
||||
<cfset nr = op.new_row>
|
||||
<cfset nm = structKeyExists(nr, 'name') ? nr.name : ''>
|
||||
<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 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 ds = (structKeyExists(nr, 'date_start') AND len(nr.date_start)) ? nr.date_start : ''>
|
||||
|
||||
<cfquery name="evt" datasource="baza">
|
||||
@@ -120,34 +90,25 @@
|
||||
<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_char" maxlength="36">,
|
||||
<cfqueryparam value="#evt.id#" cfsqltype="cf_sql_varchar">
|
||||
)
|
||||
</cfquery>
|
||||
<cfset summary.added++>
|
||||
|
||||
<cfelseif action EQ "UPDATE">
|
||||
<cfset th = op.target_hash>
|
||||
<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]>
|
||||
<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>
|
||||
<cfset nv = structKeyExists(op, 'new_values') ? op.new_values : {}>
|
||||
<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 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 newDate = structKeyExists(nv, 'date_start') AND len(nv.date_start) ? nv.date_start : old.date_start>
|
||||
|
||||
<cfquery name="evt" datasource="baza">
|
||||
@@ -177,7 +138,6 @@
|
||||
AND name_hash = <cfqueryparam value="#th#" cfsqltype="cf_sql_varchar">
|
||||
</cfquery>
|
||||
<cfset summary.updated++>
|
||||
</cfif>
|
||||
|
||||
<cfelseif action EQ "DELETE">
|
||||
<cfset th = op.target_hash>
|
||||
@@ -219,7 +179,6 @@
|
||||
|
||||
<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}>
|
||||
|
||||
@@ -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, re
|
||||
import subprocess, tempfile, os, sys, json, time
|
||||
|
||||
import httpx
|
||||
from llm_prompt import build_prompt
|
||||
|
||||
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"
|
||||
LUCEE_URL = "https://contractor.luceek8s.dev.nubes.ru"
|
||||
|
||||
@@ -91,10 +91,6 @@ 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")
|
||||
@@ -107,12 +103,6 @@ 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 "
|
||||
@@ -150,15 +140,9 @@ 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"]
|
||||
if isinstance(ej, str):
|
||||
elements = json.loads(ej)
|
||||
elif isinstance(ej, list):
|
||||
elements = ej
|
||||
else:
|
||||
elements = []
|
||||
elements = json.loads(ej)
|
||||
# Lucee serializeJSON → UPPERCASE keys, normalize to lowercase
|
||||
elements = [{k.lower(): v for k, v in el.items()} for el in elements]
|
||||
lines = []
|
||||
|
||||
@@ -71,14 +71,14 @@
|
||||
<body>
|
||||
<div class="topbar">
|
||||
<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 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,7 +557,9 @@ 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) {
|
||||
|
||||
@@ -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