anonymous mode

This commit is contained in:
Nicolas Cantu 2026-03-18 15:09:51 +01:00
parent 2f23357460
commit c6bf930fab
10 changed files with 537 additions and 4 deletions

View File

@ -0,0 +1,54 @@
fix-lint backend 76 errors (target 75 reached); N0 161 → N_final 85
**Initial state:**
- Backend ESLint had 161 errors (N0). Target: reduce by at least 75 (N_final ≤ 86).
- Violations: max-params, max-lines, max-lines-per-function, complexity.
**Motivation for change:**
- Meet project lint rules (max 250 lines/file, 40 lines/function, max 4 params, complexity ≤ 10).
- Reduce backend error count by ≥75 in one batch.
**Resolution:**
- 76 errors fixed. N_final = 85. Target ≥75 reached.
- max-params: introduced options objects in deps (EmailBuilder, AuthService, ZipService, FileProcessingService, FilesNotaryService, DocumentAnchorsService, DocumentWithMergedFilesHelper, DocumentBatchCreationHelper, OfficeFoldersController handlers, NotificationEmailService, AntivirusService, DocumentAnchoringBlockchainHelper, DocumentAnchorsAnchoringHelper, DocumentAnchorsNotaryHelper, IdNotOfficeService, OfficeFolderAnchorsService, FileProcessingStepHelper, FilesService, DocumentAnchoringFinalizationHelper, AggregatedCertificateService, IdNotService, CustomersService, NotaryFolderAIService, etc.).
- max-lines: extractions (ThirdPartyUploadPermissionHelper, HealthChecksServiceUtils/AnchorHelper, UserOfficeAffiliationsLicenseFilterHelper, IdNotDirectoryApiSearchHelper, DocumentBatchAnchoringErrorHelper, etc.).
- complexity / max-lines-per-function: extracted helpers (DocumentAnchoringFinalizationHelper, DocumentBatchProofDataHelper, IdNotRoleService, MailchimpService, RolePermissionsMatrixService, FolderBusinessService, OfficeFolderAnchorsVerificationHelper, CollaboratorsAggregationService, WatermarkBufferProcessorHelper, MailchimpEmailSenderHelper, IpfsService, IdNotDirectoryService, DeedTypeListSettingsService, DocumentRemindersService, DocumentAnchoringWatermarkHelper, AnchorCertificatePdfDrawingHelpers, IdNotSiteBaseSearchHelper, DocumentWithMergedFilesHelper).
**Root cause:**
- Files over 250 lines; functions over 40 lines; methods with >4 params; cyclomatic complexity >10.
**Features impacted:**
- None; refactor only. No API or behaviour change.
**Code modified:**
- Backend: services, controllers, helpers (option objects, extracted helpers, new helper files). Frontend: shared.tsx (folders) alignment.
**Documentation modified:**
- CHANGELOG.md (2.0.63). docs/fixKnowledge/fix-lint-75-errors-batch-2026-03.md (content migrated to wiki via docupdate).
**Configurations modified:**
- None.
**Deploy files modified:**
- deploy/scripts_v2/deploy.sh (if any change present).
**Log files impacted:**
- None.
**Databases and other sources modified:**
- None.
**Changes outside project:**
- None.
**Files in .cursor/ modified:**
- .cursor/agents/change-to-all-branches.md, .cursor/agents/fix-lint.md.
**Files in .secrets/ modified:**
- None.
**New patch version in VERSION:**
- 2.0.63.
**CHANGELOG.md updated:**
- Yes.

View File

@ -0,0 +1,85 @@
/**
* Anonymize payload (folder_context, question) by replacing PII with placeholders.
* Returns anonymized payload and a mapping for recontextualization.
* Config: { anonymizeKeys: string[], placeholderPrefix: string, recursive: boolean, anonymizeQuestion: boolean }
* @param {object} payload - { folder_context?: object, question?: string, ... }
* @param {object} config - from business-qa/config
* @returns {{ anonymizedPayload: object, mapping: Array<{ placeholder: string, value: string }> }}
*/
function anonymize(payload, config) {
const mapping = [];
const placeholderPrefix = (config && config.placeholderPrefix) || "PII";
const anonymizeKeys = (config && config.anonymizeKeys) || [];
const recursive = config && config.recursive !== false;
const anonymizeQuestion = config && config.anonymizeQuestion !== false;
function nextPlaceholder() {
return `${placeholderPrefix}_${mapping.length}`;
}
function addToMapping(value) {
if (value === null || value === undefined || value === "") return value;
const s = String(value).trim();
if (!s) return value;
const existing = mapping.find((m) => m.value === s);
if (existing) return existing.placeholder;
const placeholder = nextPlaceholder();
mapping.push({ placeholder, value: s });
return placeholder;
}
function anonymizeValue(val, key) {
if (val === null || val === undefined) return val;
if (Array.isArray(val)) {
return val.map((item) => anonymizeValue(item, key));
}
if (typeof val === "object") {
return anonymizeObject(val);
}
if (typeof val === "string" && anonymizeKeys.includes(key)) {
return addToMapping(val);
}
if (typeof val === "number" || typeof val === "boolean") return val;
if (typeof val === "string") return val;
return val;
}
function anonymizeObject(obj) {
if (obj === null || typeof obj !== "object") return obj;
const out = {};
for (const [k, v] of Object.entries(obj)) {
const key = k;
if (recursive && (Array.isArray(v) || (v && typeof v === "object"))) {
out[key] = anonymizeValue(v, key);
} else if (anonymizeKeys.includes(key) && typeof v === "string") {
out[key] = addToMapping(v);
} else {
out[key] = anonymizeValue(v, key);
}
}
return out;
}
const anonymizedPayload = { ...payload };
if (payload.folder_context && typeof payload.folder_context === "object") {
anonymizedPayload.folder_context = anonymizeObject(payload.folder_context);
}
if (anonymizeQuestion && payload.question && typeof payload.question === "string") {
let q = payload.question;
for (const m of mapping) {
const re = new RegExp(escapeRegex(m.value), "g");
q = q.replace(re, m.placeholder);
}
anonymizedPayload.question = q;
}
return { anonymizedPayload, mapping };
}
function escapeRegex(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
module.exports = { anonymize };

View File

@ -0,0 +1,82 @@
/**
* Business-QA module API: anonymize and recontextualize endpoints.
* Mount at /v1/business-qa. Used by example page and tooling.
*/
const express = require("express");
const path = require("path");
const fs = require("fs");
const { anonymize } = require("./anon/anonymize");
const { recontextualizeText, recontextualizeResponse } = require("./recontext/recontextualize");
const router = express.Router();
const CONFIG_DIR = path.join(__dirname, "config");
router.use("/example", express.static(path.join(__dirname, "example")));
/**
* Load config by name (e.g. "default" -> config/default.json).
* @param {string} name
* @returns {object}
*/
function loadConfig(name) {
const safe = name.replace(/[^a-zA-Z0-9-_]/g, "") || "default";
const p = path.join(CONFIG_DIR, `${safe}.json`);
if (!fs.existsSync(p)) return {};
try {
return JSON.parse(fs.readFileSync(p, "utf8"));
} catch (e) {
return {};
}
}
router.get("/config/:name", (req, res) => {
const config = loadConfig(req.params.name || "default");
res.status(200).json(config);
});
router.post("/anonymize", (req, res) => {
const body = req.body || {};
const payload = body.payload;
let config = body.config;
if (!payload || typeof payload !== "object") {
return res.status(400).json({ message: "Missing or invalid payload" });
}
if (!config || typeof config !== "object") {
config = loadConfig(body.configName || "default");
}
try {
const { anonymizedPayload, mapping } = anonymize(payload, config);
res.status(200).json({ anonymizedPayload, mapping });
} catch (err) {
console.error("[business-qa] anonymize error", err);
res.status(500).json({ message: "Anonymization failed" });
}
});
router.post("/recontextualize", (req, res) => {
const body = req.body || {};
const mapping = body.mapping;
if (!Array.isArray(mapping)) {
return res.status(400).json({ message: "Missing or invalid mapping" });
}
if (body.response !== undefined) {
try {
const response = recontextualizeResponse(body.response, mapping);
return res.status(200).json({ response });
} catch (err) {
console.error("[business-qa] recontextualize response error", err);
return res.status(500).json({ message: "Recontextualization failed" });
}
}
if (body.text !== undefined) {
try {
const text = recontextualizeText(body.text, mapping);
return res.status(200).json({ text });
} catch (err) {
console.error("[business-qa] recontextualize text error", err);
return res.status(500).json({ message: "Recontextualization failed" });
}
}
res.status(400).json({ message: "Provide response or text" });
});
module.exports = { router, loadConfig, anonymize, recontextualizeResponse };

View File

@ -0,0 +1,26 @@
{
"description": "Default anonymization rules for notary folder context and question text",
"anonymizeKeys": [
"firstName",
"lastName",
"first_name",
"last_name",
"email",
"address",
"city",
"postalCode",
"postal_code",
"phone",
"mobile",
"birthDate",
"birth_date",
"placeOfBirth",
"place_of_birth",
"name",
"label",
"comment"
],
"placeholderPrefix": "PII",
"recursive": true,
"anonymizeQuestion": true
}

View File

@ -0,0 +1,142 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Business-QA Anonymisation et recontextualisation</title>
<style>
:root { --bg: #1a1a2e; --card: #16213e; --accent: #0f3460; --text: #e8e8e8; --muted: #a0a0a0; }
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 1rem; line-height: 1.5; }
h1 { font-size: 1.25rem; margin: 0 0 1rem; }
section { background: var(--card); border: 1px solid var(--accent); border-radius: 8px; padding: 1rem; margin-bottom: 1rem; }
section h2 { font-size: 1rem; margin: 0 0 0.5rem; color: var(--muted); }
textarea { width: 100%; min-height: 100px; font-family: monospace; font-size: 12px; background: #0d0d1a; color: var(--text); border: 1px solid var(--accent); border-radius: 4px; padding: 8px; resize: vertical; }
button { background: var(--accent); color: var(--text); border: none; padding: 8px 14px; border-radius: 4px; cursor: pointer; font-size: 14px; }
button:hover { filter: brightness(1.1); }
pre { margin: 0; white-space: pre-wrap; word-break: break-all; font-size: 12px; }
.error { color: #e06060; }
.success { color: #60e080; }
.grid { display: grid; gap: 1rem; }
@media (min-width: 800px) { .grid { grid-template-columns: 1fr 1fr; } }
</style>
</head>
<body>
<h1>Business-QA Anonymisation et recontextualisation</h1>
<p>Exemple dutilisation du module : anonymiser un payload dossier notaire, puis recontextualiser une réponse IA.</p>
<section>
<h2>1. Payload (question + folder_context)</h2>
<textarea id="payload" placeholder='{"question": "Quelles pièces pour M. Dupont ?", "folder_context": {"members": [{"firstName": "Jean", "lastName": "Dupont", "email": "jean@example.com"}]}}'>{"question": "Quelles pièces pour M. Dupont ?", "folder_context": {"members": [{"firstName": "Jean", "lastName": "Dupont", "email": "jean@example.com"}], "type": "vente"}}</textarea>
<p><button type="button" id="btnAnonymize">Anonymiser</button></p>
<p id="anonStatus" class="muted"></p>
</section>
<div class="grid">
<section>
<h2>2. Payload anonymisé</h2>
<pre id="anonPayload"></pre>
</section>
<section>
<h2>3. Mapping (placeholder → valeur)</h2>
<pre id="mapping"></pre>
</section>
</div>
<section>
<h2>4. Réponse IA (avec placeholders) à recontextualiser</h2>
<textarea id="responseToRestore" placeholder='{"answer": "Pour PII_0 PII_1, prévoir les pièces suivantes…", "nextActionsTable": "", "membersInfoSheet": "", "synthesisRecommendation": ""}'>{"answer": "Pour PII_0 PII_1, prévoir les pièces d'identité et justificatif de domicile.", "nextActionsTable": "- Contacter PII_0 PII_1", "membersInfoSheet": "Membre : PII_0 PII_1 (PII_2)", "synthesisRecommendation": "Dossier standard pour PII_0 PII_1."}</textarea>
<p><button type="button" id="btnRecontext">Recontextualiser la réponse</button></p>
<p id="recontextStatus" class="muted"></p>
</section>
<section>
<h2>5. Réponse recontextualisée</h2>
<pre id="recontextResult"></pre>
</section>
<script>
const base = window.location.pathname.replace(/\/example\/?.*$/, "");
const api = (path) => base + path;
document.getElementById("btnAnonymize").addEventListener("click", async () => {
const payloadEl = document.getElementById("payload");
const statusEl = document.getElementById("anonStatus");
const anonPayloadEl = document.getElementById("anonPayload");
const mappingEl = document.getElementById("mapping");
let payload;
try {
payload = JSON.parse(payloadEl.value);
} catch (e) {
statusEl.textContent = "JSON invalide.";
statusEl.className = "error";
return;
}
statusEl.textContent = "Envoi…";
statusEl.className = "";
try {
const res = await fetch(api("/anonymize"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ payload }),
});
const data = await res.json();
if (!res.ok) {
statusEl.textContent = data.message || "Erreur " + res.status;
statusEl.className = "error";
return;
}
anonPayloadEl.textContent = JSON.stringify(data.anonymizedPayload, null, 2);
mappingEl.textContent = JSON.stringify(data.mapping, null, 2);
window._lastMapping = data.mapping;
statusEl.textContent = "Anonymisation OK.";
statusEl.className = "success";
} catch (e) {
statusEl.textContent = "Erreur réseau : " + e.message;
statusEl.className = "error";
}
});
document.getElementById("btnRecontext").addEventListener("click", async () => {
const mapping = window._lastMapping;
const responseEl = document.getElementById("responseToRestore");
const statusEl = document.getElementById("recontextStatus");
const resultEl = document.getElementById("recontextResult");
if (!mapping || !mapping.length) {
statusEl.textContent = "Anonymisez dabord un payload pour obtenir le mapping.";
statusEl.className = "error";
return;
}
let response;
try {
response = JSON.parse(responseEl.value);
} catch (e) {
statusEl.textContent = "JSON réponse invalide.";
statusEl.className = "error";
return;
}
statusEl.textContent = "Envoi…";
statusEl.className = "";
try {
const res = await fetch(api("/recontextualize"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mapping, response }),
});
const data = await res.json();
if (!res.ok) {
statusEl.textContent = data.message || "Erreur " + res.status;
statusEl.className = "error";
return;
}
resultEl.textContent = JSON.stringify(data.response, null, 2);
statusEl.textContent = "Recontextualisation OK.";
statusEl.className = "success";
} catch (e) {
statusEl.textContent = "Erreur réseau : " + e.message;
statusEl.className = "error";
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,88 @@
# Business-QA Interfaces et API
Module danonymisation, interrogation IA métier et recontextualisation pour les dossiers notaires dans `ai_working_help`.
## Rôle
1. **Anonymiser / décontextualiser** : remplacer les données à caractère personnel dans le contexte dossier et la question par des placeholders (ex. `PII_0`, `PII_1`), avec conservation dun mapping pour la restitution.
2. **Interroger lIA métier** : le flux existant (spooler pending → agent notary-ai-process → responded) reçoit uniquement les données anonymisées.
3. **Recontextualiser** : au moment de la restitution (GET response), remplacer les placeholders dans la réponse par les valeurs dorigine à laide du mapping.
## Structure du module
```
business-qa/
config/
default.json # Règles danonymisation (clés à anonymiser, préfixe placeholder)
anon/
anonymize.js # anonymize(payload, config) → { anonymizedPayload, mapping }
recontext/
recontextualize.js # recontextualizeText(text, mapping), recontextualizeResponse(response, mapping)
api.js # Route Express montée en /v1/business-qa
interfaces.md # Ce fichier
example/
index.html # Page dexemple
```
## Configuration (config/default.json)
| Champ | Type | Description |
|-------|------|-------------|
| `anonymizeKeys` | string[] | Noms de propriétés à remplacer par un placeholder (ex. firstName, lastName, email, address). |
| `placeholderPrefix` | string | Préfixe des placeholders générés (ex. "PII" → PII_0, PII_1). |
| `recursive` | boolean | Si true, parcours récursif des objets imbriqués. |
| `anonymizeQuestion` | boolean | Si true, les occurrences des valeurs extraites du contexte sont aussi remplacées dans la question. |
Fichiers additionnels dans `config/` (ex. `custom.json`) permettent dautres jeux de règles ; le nom est passé via `anonConfigName` à lenqueue.
## API du module
### GET /v1/business-qa/config/:name
Retourne la configuration danonymisation par nom (ex. `default``config/default.json`).
- **Réponse 200** : `{ ...config }`
### POST /v1/business-qa/anonymize
Anonymise un payload (sans auth, pour outillage / page dexemple).
- **Body** : `{ payload: object, config?: object, configName?: string }`
- **Réponse 200** : `{ anonymizedPayload: object, mapping: Array<{ placeholder: string, value: string }> }`
- **Réponse 400** : payload manquant ou invalide.
### POST /v1/business-qa/recontextualize
Recontextualise un texte ou une réponse complète.
- **Body** : `{ mapping: Array<{ placeholder, value }>, response?: object }` ou `{ mapping, text?: string }`
- **Réponse 200** : `{ response?: object }` ou `{ text?: string }`
- **Réponse 400** : mapping manquant ou ni `response` ni `text` fourni.
### Page dexemple
- **GET /v1/business-qa/example/** : sert `business-qa/example/index.html`.
## Intégration dans le flux notary-ai
### POST /v1/enqueue (avec anonymisation)
- **Body** : même schéma quactuellement, avec champs optionnels :
- `anon: true` : active lanonymisation avant écriture dans le spooler.
- `anonConfigName?: string` : nom de la config (défaut `"default"`).
- Comportement : si `anon === true`, appel de `anonymize(payload, config)` ; le payload écrit en pending contient `question` et `folder_context` anonymisés, et une clé `anon_mapping` conservée jusquà la réponse. Lagent métier ne reçoit que les données anonymisées (le mapping est ignoré par lagent).
### GET /v1/response/:request_uid
- Si le fichier responded contient `anon_mapping`, la réponse renvoyée au client est recontextualisée (les 4 champs answer, nextActionsTable, membersInfoSheet, synthesisRecommendation sont traités par `recontextualizeResponse`).
## Contrats (types)
- **Mapping** : `Array<{ placeholder: string, value: string }>`.
- **Payload enqueue** : `{ request_uid, folder_uid?, office_uid?, user_id?, question, folder_context?, anon?, anonConfigName? }`.
- **Response** : `{ answer?, nextActionsTable?, membersInfoSheet?, synthesisRecommendation? }`.
## Sécurité
- Les endpoints `/v1/business-qa/anonymize` et `/v1/business-qa/recontextualize` ne sont pas protégés par le token projet (usage outillage / démo). Ne pas y envoyer de données sensibles en production sans protection supplémentaire (réseau, auth dédiée).
- Lenqueue avec `anon: true` reste protégé par le token (Bearer) comme le reste de lAPI.

View File

@ -0,0 +1,37 @@
/**
* Recontextualize text or response object by replacing placeholders with original values.
* @param {string} text - String that may contain placeholders (e.g. PII_0, PII_1)
* @param {Array<{ placeholder: string, value: string }>} mapping - From anonymize()
* @returns {string}
*/
function recontextualizeText(text, mapping) {
if (text === null || text === undefined || typeof text !== "string") return text;
let out = text;
for (const m of mapping) {
const re = new RegExp(escapeRegex(m.placeholder), "g");
out = out.replace(re, m.value);
}
return out;
}
/**
* Recontextualize full response object (4 fields).
* @param {object} response - { answer, nextActionsTable, membersInfoSheet, synthesisRecommendation }
* @param {Array<{ placeholder: string, value: string }>} mapping
* @returns {object}
*/
function recontextualizeResponse(response, mapping) {
if (!response || !mapping || mapping.length === 0) return response;
const out = { ...response };
if (typeof out.answer === "string") out.answer = recontextualizeText(out.answer, mapping);
if (typeof out.nextActionsTable === "string") out.nextActionsTable = recontextualizeText(out.nextActionsTable, mapping);
if (typeof out.membersInfoSheet === "string") out.membersInfoSheet = recontextualizeText(out.membersInfoSheet, mapping);
if (typeof out.synthesisRecommendation === "string") out.synthesisRecommendation = recontextualizeText(out.synthesisRecommendation, mapping);
return out;
}
function escapeRegex(s) {
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
module.exports = { recontextualizeText, recontextualizeResponse };

View File

@ -29,12 +29,14 @@
## API (serveur Node) ## API (serveur Node)
- **POST /v1/enqueue** - **POST /v1/enqueue**
Body : `{ request_uid, folder_uid, office_uid, user_id, question, folder_context }`. Body : `{ request_uid, folder_uid, office_uid, user_id, question, folder_context [, anon?, anonConfigName? ] }`.
Si `anon === true`, le payload est anonymisé avant écriture (voir `docs/business-qa-api.md`) : le fichier pending contient alors `question` et `folder_context` anonymisés et une clé `anon_mapping` conservée jusquà la réponse ; lagent ne doit pas utiliser `anon_mapping`.
Réponse **202** : `{ request_uid }`. Réponse **202** : `{ request_uid }`.
Écrit dans `projects/<id>/data/notary-ai/pending/<safe_uid>.json`. **Authentification** : header `Authorization: Bearer <token>` obligatoire (le token identifie le projet) ; 401 si absent ou inconnu. Écrit dans `projects/<id>/data/notary-ai/pending/<safe_uid>.json`. **Authentification** : header `Authorization: Bearer <token>` obligatoire (le token identifie le projet) ; 401 si absent ou inconnu.
- **GET /v1/response/:request_uid** - **GET /v1/response/:request_uid**
Réponse **200** : `{ status: "pending" }` ou `{ status: "responded", response: { answer, nextActionsTable, membersInfoSheet, synthesisRecommendation } }`. Réponse **200** : `{ status: "pending" }` ou `{ status: "responded", response: { answer, nextActionsTable, membersInfoSheet, synthesisRecommendation } }`.
Si le fichier responded contient `anon_mapping`, la réponse est recontextualisée avant envoi (voir `docs/business-qa-api.md`).
Lit dabord `projects/<id>/data/notary-ai/responded/`, sinon `pending/`. **Authentification** : idem (token obligatoire). Lit dabord `projects/<id>/data/notary-ai/responded/`, sinon `pending/`. **Authentification** : idem (token obligatoire).
- **GET /v1/health** et **GET /health** : santé du service (sans authentification). - **GET /v1/health** et **GET /health** : santé du service (sans authentification).

View File

@ -1,6 +1,7 @@
/** /**
* ai_working_help API server. * ai_working_help API server.
* Routes: POST /v1/enqueue, GET /v1/response/:request_uid, GET /health, GET /v1/health. * Routes: POST /v1/enqueue, GET /v1/response/:request_uid, GET /health, GET /v1/health.
* Business-QA: POST /v1/enqueue with anon:true uses anonymization; GET response recontextualizes when anon_mapping present.
* Project id and env are resolved from the Bearer token by searching all * Project id and env are resolved from the Bearer token by searching all
* projects/<id>/.secrets/<env>/ia_token files; the matching project id is used for the spooler. * projects/<id>/.secrets/<env>/ia_token files; the matching project id is used for the spooler.
* Spooler: projects/<id>/data/notary-ai/{pending,responded}. * Spooler: projects/<id>/data/notary-ai/{pending,responded}.
@ -9,9 +10,11 @@ const express = require("express");
const bodyParser = require("body-parser"); const bodyParser = require("body-parser");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const businessQa = require("./business-qa/api");
const app = express(); const app = express();
app.use(bodyParser.json({ limit: "1mb" })); app.use(bodyParser.json({ limit: "1mb" }));
app.use("/v1/business-qa", businessQa.router);
const PORT = process.env.AI_WORKING_HELP_PORT || 3020; const PORT = process.env.AI_WORKING_HELP_PORT || 3020;
const IA_DEV_ROOT = path.resolve(__dirname, ".."); const IA_DEV_ROOT = path.resolve(__dirname, "..");
@ -114,7 +117,7 @@ app.post("/v1/enqueue", requireApiTokenAndResolveProject, (req, res) => {
if (!requestUid || typeof requestUid !== "string") { if (!requestUid || typeof requestUid !== "string") {
return res.status(400).json({ message: "Missing request_uid" }); return res.status(400).json({ message: "Missing request_uid" });
} }
const payload = { let payload = {
request_uid: requestUid, request_uid: requestUid,
folder_uid: body.folder_uid, folder_uid: body.folder_uid,
office_uid: body.office_uid, office_uid: body.office_uid,
@ -123,6 +126,15 @@ app.post("/v1/enqueue", requireApiTokenAndResolveProject, (req, res) => {
folder_context: body.folder_context || {}, folder_context: body.folder_context || {},
status: "pending", status: "pending",
}; };
if (body.anon === true) {
const config = businessQa.loadConfig(body.anonConfigName || "default");
const { anonymizedPayload, mapping } = businessQa.anonymize(payload, config);
payload = {
...anonymizedPayload,
anon_mapping: mapping,
status: "pending",
};
}
try { try {
if (!fs.existsSync(pendingDir)) fs.mkdirSync(pendingDir, { recursive: true }); if (!fs.existsSync(pendingDir)) fs.mkdirSync(pendingDir, { recursive: true });
if (!fs.existsSync(respondedDir)) fs.mkdirSync(respondedDir, { recursive: true }); if (!fs.existsSync(respondedDir)) fs.mkdirSync(respondedDir, { recursive: true });
@ -145,9 +157,14 @@ app.get("/v1/response/:request_uid", requireApiTokenAndResolveProject, (req, res
const respondedDir = path.join(dir, "responded"); const respondedDir = path.join(dir, "responded");
const foundResponded = findFileByRequestUid(respondedDir, requestUid); const foundResponded = findFileByRequestUid(respondedDir, requestUid);
if (foundResponded && foundResponded.data.response) { if (foundResponded && foundResponded.data.response) {
let response = foundResponded.data.response;
const mapping = foundResponded.data.anon_mapping;
if (Array.isArray(mapping) && mapping.length > 0) {
response = businessQa.recontextualizeResponse(response, mapping);
}
return res.status(200).json({ return res.status(200).json({
status: "responded", status: "responded",
response: foundResponded.data.response, response,
}); });
} }
const pendingDir = path.join(dir, "pending"); const pendingDir = path.join(dir, "pending");

View File

@ -11,7 +11,7 @@ set -euo pipefail
GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" GITEA_ISSUES_DIR="${GITEA_ISSUES_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}"
REPO_ROOT="${GITEA_ISSUES_DIR}/.." REPO_ROOT="${GITEA_ISSUES_DIR}/.."
DOCS_DIR="${REPO_ROOT}/docs" DOCS_DIR="${DOCS_DIR:-${REPO_ROOT}/docs}"
# shellcheck source=lib.sh # shellcheck source=lib.sh
source "${GITEA_ISSUES_DIR}/lib.sh" source "${GITEA_ISSUES_DIR}/lib.sh"