Skip to content

Commit c2b4298

Browse files
Glauber Varjãoclaude
andcommitted
fix: FDSearch multipart filtrado por employeeNo — resolve download de fotos em LOCALS/
Problema: terminais que armazenam fotos em LOCALS/ retornavam faceURL com path interno (ex: /LOCALS/pic/enrlFace/0/xxx.jpg) que dá 404 via HTTP. O modelData (364 chars) é template biométrico, não imagem. Solução: - Fase 3 reescrita: FDSearch multipart com filtro byEmployeeNo (sem ?format=json, terminal retorna JPEG embutido no multipart) - 3 sub-tentativas: faceCustomCondition, FPID direto, busca por página - Fase 4 expandida: FaceDataRecord + FDDownload + mais endpoints GET - Bump v4.1.7 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 453ce1f commit c2b4298

5 files changed

Lines changed: 88 additions & 46 deletions

File tree

build_info.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
BUILD_TIMESTAMP = "2026-04-01 19:00"
1+
BUILD_TIMESTAMP = "2026-04-01 19:08"

core/isapi_client.py

Lines changed: 81 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1898,51 +1898,93 @@ def _safe_get(endpoint):
18981898
if found_match:
18991899
break
19001900

1901-
# === FASE 3: FDSearch multipart para a página que contém o employeeNo ===
1902-
if not found_match:
1903-
_log("Registro não encontrado via FDSearch paginado")
1904-
else:
1905-
_log("Tentativa 3: FDSearch multipart na página do registro")
1906-
# Refazer a busca SEM ?format=json (retorna JPEG embutido)
1907-
page_start = max(0, pos - 30)
1908-
payload = json.dumps({
1909-
"faceLibType": fd_type, "FDID": fdid,
1910-
"searchResultPosition": page_start, "maxResults": 30
1901+
# === FASE 3: FDSearch multipart FILTRADO por employeeNo ===
1902+
# Sem ?format=json, o terminal retorna o JPEG embutido no multipart
1903+
_log("Tentativa 3: FDSearch multipart filtrado por employeeNo")
1904+
for fdid, fd_type in fd_configs:
1905+
# 3a: Busca com filtro faceCustomCondition (terminais mais novos)
1906+
for search_payload in [
1907+
json.dumps({
1908+
"faceLibType": fd_type, "FDID": fdid,
1909+
"searchResultPosition": 0, "maxResults": 1,
1910+
"faceCustomCondition": {
1911+
"searchType": "byEmployeeNo",
1912+
"EmployeeNoList": [{"employeeNo": employee_no}]
1913+
}
1914+
}),
1915+
# 3b: Busca com FPID direto (terminais mais antigos)
1916+
json.dumps({
1917+
"faceLibType": fd_type, "FDID": fdid, "FPID": employee_no,
1918+
"searchResultPosition": 0, "maxResults": 1
1919+
}),
1920+
]:
1921+
resp = _safe_post("/ISAPI/Intelligent/FDLib/FDSearch", search_payload)
1922+
status = resp.get("status_code", 0)
1923+
data = resp.get("data", b"")
1924+
data_len = len(data) if isinstance(data, bytes) else 0
1925+
if status == 200 and data_len > 500:
1926+
_log(f" → Multipart data: {data_len} bytes")
1927+
img = _extract_jpeg(data)
1928+
if img and len(img) > 1024:
1929+
_log(f" → JPEG encontrado! {len(img)//1024}KB")
1930+
return img
1931+
_log(f" → Sem JPEG no multipart (data tem {data_len}B)")
1932+
elif status == 400:
1933+
continue # tipo errado, tentar próximo
1934+
elif status != 200:
1935+
_log(f" → status={status}")
1936+
1937+
# 3c: Busca pela página que contém o registro (fallback amplo)
1938+
if found_match:
1939+
page_start = max(0, pos - 30)
1940+
payload = json.dumps({
1941+
"faceLibType": fd_type, "FDID": fdid,
1942+
"searchResultPosition": page_start, "maxResults": 30
1943+
})
1944+
resp = _safe_post("/ISAPI/Intelligent/FDLib/FDSearch", payload)
1945+
if resp.get("ok") and resp.get("data"):
1946+
data = resp["data"]
1947+
if len(data) > 2000:
1948+
_log(f" → Página multipart: {len(data)} bytes")
1949+
idx = 0
1950+
jpeg_list = []
1951+
while True:
1952+
start = data.find(b'\xff\xd8\xff', idx)
1953+
if start < 0:
1954+
break
1955+
end = data.find(b'\xff\xd9', start + 3)
1956+
if end < 0:
1957+
break
1958+
jpeg_list.append(data[start:end + 2])
1959+
idx = end + 2
1960+
if jpeg_list:
1961+
_log(f" → {len(jpeg_list)} JPEG(s) na página")
1962+
for jpg in jpeg_list:
1963+
if len(jpg) > 1024:
1964+
_log(f" → Retornando JPEG {len(jpg)//1024}KB")
1965+
return jpg
1966+
1967+
# === FASE 4: FaceDataRecord + endpoints diretos GET ===
1968+
_log("Tentativa 4: FaceDataRecord + GET endpoints diretos")
1969+
for fdid, fd_type in fd_configs:
1970+
# 4a: FaceDataRecord (alguns terminais suportam GET por FPID)
1971+
fdr_payload = json.dumps({
1972+
"faceLibType": fd_type, "FDID": fdid, "FPID": employee_no
19111973
})
1912-
resp = _safe_post("/ISAPI/Intelligent/FDLib/FDSearch", payload)
1974+
resp = _safe_post("/ISAPI/Intelligent/FDLib/FaceDataRecord?format=json", fdr_payload)
19131975
if resp.get("ok") and resp.get("data"):
1914-
# A resposta multipart contém TODAS as fotos da página
1915-
# Extrair todos os JPEGs e retornar o correto (pela posição)
1916-
data = resp["data"]
1917-
_log(f" → Multipart data: {len(data)} bytes")
1918-
# Encontrar todos os JPEGs no multipart
1919-
idx = 0
1920-
jpeg_list = []
1921-
while True:
1922-
start = data.find(b'\xff\xd8\xff', idx)
1923-
if start < 0:
1924-
break
1925-
end = data.find(b'\xff\xd9', start + 3)
1926-
if end < 0:
1927-
break
1928-
jpeg_list.append(data[start:end + 2])
1929-
idx = end + 2
1930-
1931-
if jpeg_list:
1932-
_log(f" → {len(jpeg_list)} JPEG(s) encontrado(s) no multipart")
1933-
# Retornar o primeiro JPEG válido (> 1KB)
1934-
for jpg in jpeg_list:
1935-
if len(jpg) > 1024:
1936-
_log(f" → Retornando JPEG {len(jpg)//1024}KB")
1937-
return jpg
1938-
1939-
# === FASE 4: Endpoints diretos GET ===
1940-
_log("Tentativa 4: GET endpoints diretos")
1941-
for fdid, fd_type in fd_configs:
1976+
img = _extract_jpeg(resp["data"])
1977+
if img and len(img) > 100:
1978+
_log(f" → JPEG via FaceDataRecord! {len(img)//1024}KB")
1979+
return img
1980+
1981+
# 4b: Endpoints diretos GET
19421982
for ep in [
19431983
f"/ISAPI/Intelligent/FDLib/{fdid}/picture/{employee_no}?faceLibType={fd_type}",
19441984
f"/ISAPI/Intelligent/FDLib/{fdid}/picture/{employee_no}",
19451985
f"/ISAPI/AccessControl/UserInfo/UserPic/{employee_no}",
1986+
f"/ISAPI/Intelligent/FDLib/FDDownload?format=json&FDID={fdid}&FPID={employee_no}",
1987+
f"/ISAPI/AccessControl/CaptureFaceData",
19461988
]:
19471989
resp = _safe_get(ep)
19481990
status = resp.get("status_code", 0)

file_version_info.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# UTF-8
22
VSVersionInfo(
33
ffi=FixedFileInfo(
4-
filevers=(4, 1, 6, 0),
5-
prodvers=(4, 1, 6, 0),
4+
filevers=(4, 1, 7, 0),
5+
prodvers=(4, 1, 7, 0),
66
mask=0x3f,
77
flags=0x0,
88
OS=0x40004,
@@ -18,12 +18,12 @@ VSVersionInfo(
1818
[
1919
StringStruct(u'CompanyName', u'Protector Sistemas'),
2020
StringStruct(u'FileDescription', u'Protector ISAPI Manager'),
21-
StringStruct(u'FileVersion', u'4.1.6'),
21+
StringStruct(u'FileVersion', u'4.1.7'),
2222
StringStruct(u'InternalName', u'Protector_ISAPI_Manager'),
2323
StringStruct(u'LegalCopyright', u'© 2026 Protector Sistemas'),
2424
StringStruct(u'OriginalFilename', u'Protector_ISAPI_Manager.exe'),
2525
StringStruct(u'ProductName', u'Protector ISAPI Manager'),
26-
StringStruct(u'ProductVersion', u'4.1.6'),
26+
StringStruct(u'ProductVersion', u'4.1.7'),
2727
]
2828
)
2929
]

installer.iss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
; ============================================================================
1515

1616
#define MyAppName "Protector ISAPI Manager"
17-
#define MyAppVersion "4.1.6"
17+
#define MyAppVersion "4.1.7"
1818
#define MyAppPublisher "Protector Sistemas"
1919
#define MyAppURL "https://github.com/ProtectorAnalytics/protector-isapi-manager"
2020
#define MyAppExeName "Protector_ISAPI_Manager.exe"

version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# MAJOR = mudança que quebra compatibilidade
1111
# MINOR = funcionalidades novas (retrocompatíveis)
1212
# PATCH = correções de bugs
13-
VERSION = "4.1.6"
13+
VERSION = "4.1.7"
1414

1515
# Status: "stable", "beta", "dev", "rc1", "rc2"...
1616
VERSION_STATUS = "stable"

0 commit comments

Comments
 (0)