Skip to content

Commit 1a0bbe3

Browse files
authored
feat(similarity): 相似度检测管理前端 (#61)
* update .gitignore * feat(similarity): scaffold admin similarity dashboard route + menu Add DiffOutlined menu entry and active-key detection for /admin/similarity. Create similarity page, SimilarityDashboardClient tabs shell, and five placeholder tab components (PairsTable, SuspectsTable, IntegrityReviewTable, IntegrityWhitelistTable, PairWhitelistTable). * feat(similarity): add frontend similarity API service client Typed service class wrapping all admin + public similarity endpoints: pairs list/detail, suspects list, pair whitelist, integrity reviews, integrity whitelist, and evidence pair detail. * feat(similarity): add zh-CN translations for Phase 3 admin panel Merge admin.navigation.similarity, admin.similarity (tabs, columns, status labels, action labels, drawer/modal strings), similarity.evidence (disclaimer), and errors.integrity_rejected into the zh-CN locale file. * feat(similarity): implement PairsTable tab * feat(similarity): implement SuspectsTable tab * fix(similarity): integrity_score 0 rendering and consolidate antd imports - integrity_score === 0 now renders as 0.00 instead of - - merge split antd imports in SuspectsTable * feat(similarity): implement integrity review queue tab * fix(similarity): use non-deprecated Drawer props (size/destroyOnHidden) * fix(similarity): destroy ResolveReviewModal form on close to prevent state leak * feat(similarity): add removePairWhitelistByID API client method * feat(similarity): implement whitelist management tabs * feat(similarity): add admin pair detail page with Monaco diff viewer Implements Task 17 — pair detail page under admin/similarity/pairs/[pairId] with metadata descriptions, whitelist action button, and a CodeDiffViewer component that uses @monaco-editor/react DiffEditor with per-segment line decorations via createDecorationsCollection. * feat(similarity): add semi-public pair evidence page Implements Task 18 — public-facing evidence page at similarity/pair/[id] that wraps PairDetailClient with a warning Alert disclaimer banner, sourced from getEvidencePair API endpoint. * fix(similarity): use Alert.title instead of deprecated message prop * fix(similarity): use real CSS class for diff highlights and correct byte→line conversion - replace Tailwind class names (which Tailwind purges from non-JSX strings) with similarity-match-highlight defined in globals.css - convert UTF-8 byte offsets via TextEncoder to handle non-ASCII source code correctly, instead of iterating JS UTF-16 code units * feat(similarity): show integrity rejection alert on script publish/update * feat(similarity): add Phase 3 translations for all supported locales - en-US: proper English translations - zh-TW: Traditional Chinese translations - ja-JP / de-DE / ru-RU: English stopgap (pending Crowdin) * feat(similarity): Phase 4 backfill control panel + stop-fp refresh Adds the admin UI for §8.5 bootstrap operations: - BackfillControl component in a new "回填与重扫" dashboard tab, polling /admin/similarity/backfill/status every 5s while running. Shows total/cursor/progress bar/started_at/finished_at in a Descriptions panel with start + restart-from-zero (§8.5 step 9) + refresh buttons gated by running flag. - Manual per-script rescan card with script ID input. - Stop-fingerprint refresh card with warning copy ("通常不需要手动 触发"), invoking POST /admin/similarity/stop-fp/refresh — used at §8.5 step 8 after the first full backfill completes. - similarityService adds triggerBackfill / getBackfillStatus / manualScan / refreshStopFp methods and BackfillStatus type. - New admin.similarity.tab_backfill + admin.similarity.backfill.* translation keys in zh-CN. * fix(similarity): flag deleted scripts in pair list with toggle filter Backend now marks each ScriptBrief with is_deleted and accepts an exclude_deleted query param. Render deleted scripts with a strikethrough link and red tag, and add a Switch above the table that lets admins hide any pair whose either side has been soft-deleted. * feat(similarity): show signal descriptions in integrity review table * refactor(similarity): move SIGNAL_DESCRIPTIONS hardcoded Chinese strings to i18n Replace the hardcoded SIGNAL_DESCRIPTIONS constant in IntegrityReviewTable with next-intl t() calls under admin.similarity.signal_desc.*, adding translations for all 6 locales (zh-CN, en-US, de-DE, ja-JP, ru-RU, zh-TW). * feat(i18n): add missing Phase 4 similarity keys to all non-zh-CN locales Backfills script_deleted, filter_exclude_deleted, tab_backfill, and the full backfill sub-object (28 keys) into en-US, de-DE, ja-JP, ru-RU, and zh-TW — inserted before signal_desc to maintain consistent key ordering. * style: fix prettier formatting in IntegrityReviewTable * refactor: extract PairDetailClient and CodeDiffViewer to shared components Move these components from admin route directory to src/components/similarity/ so both the admin detail page and public evidence page import from the same shared location, eliminating the cross-layer dependency.
1 parent 7d59fa6 commit 1a0bbe3

27 files changed

Lines changed: 2366 additions & 8 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,6 @@ next-env.d.ts
4747
/public/assets/prismjs/*
4848

4949
.claude
50-
50+
.omc
5151
CLAUDE.md
52+

public/locales/de-DE/translations.json

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
"scores": "Bewertungsverwaltung",
3939
"scripts": "Skriptverwaltung",
4040
"system_config": "Systemkonfiguration",
41-
"users": "Benutzerverwaltung"
41+
"users": "Benutzerverwaltung",
42+
"similarity": "Similarity Detection"
4243
},
4344
"no_permission": "Sie haben keine Administratorberechtigung für den Zugriff auf diese Seite.",
4445
"oauth_apps": {
@@ -211,6 +212,111 @@
211212
"title": "Benutzerverwaltung",
212213
"unban_confirm": "Möchten Sie diesen Benutzer wirklich entsperren?",
213214
"unban_success": "Benutzer erfolgreich entsperrt"
215+
},
216+
"similarity": {
217+
"tab_pairs": "Pairs",
218+
"tab_suspects": "Suspects",
219+
"tab_integrity_reviews": "Integrity Reviews",
220+
"tab_pair_whitelist": "Pair Whitelist",
221+
"tab_integrity_whitelist": "Integrity Exemptions",
222+
"col_id": "ID",
223+
"col_script_a": "Script A",
224+
"col_script_b": "Script B",
225+
"col_jaccard": "Jaccard",
226+
"col_common": "Common Fingerprints",
227+
"col_earlier": "Earlier Side",
228+
"col_status": "Status",
229+
"col_integrity": "Integrity Score",
230+
"col_actions": "Actions",
231+
"col_script": "Script",
232+
"col_max_jaccard": "Max Jaccard",
233+
"col_coverage": "External Coverage",
234+
"col_pair_count": "Pair Count",
235+
"col_detected_at": "Detected At",
236+
"col_score": "Score",
237+
"col_createtime": "Created",
238+
"col_reason": "Reason",
239+
"col_added_by": "Added By",
240+
"status_pending": "Pending",
241+
"status_whitelisted": "Whitelisted",
242+
"status_resolved": "Resolved",
243+
"review_pending": "Pending",
244+
"review_ok": "OK",
245+
"review_violated": "Violation",
246+
"action_detail": "Details",
247+
"action_resolve": "Resolve",
248+
"action_whitelist": "Whitelist",
249+
"action_remove": "Remove",
250+
"confirm_remove_whitelist": "Remove this whitelist entry?",
251+
"msg_removed": "Removed",
252+
"modal_add_int_whitelist": "Add Integrity Exemption",
253+
"label_script_id": "Script ID",
254+
"label_reason": "Reason",
255+
"btn_add": "Add",
256+
"modal_resolve_title": "Mark Integrity Review",
257+
"label_decision": "Decision",
258+
"label_note": "Note",
259+
"decision_ok": "OK",
260+
"decision_violated": "Violation",
261+
"msg_review_resolved": "Marked successfully",
262+
"msg_whitelisted": "Added to whitelist",
263+
"drawer_review_detail": "Integrity Review Details",
264+
"label_score": "Total Score",
265+
"label_sub_scores": "Category Scores",
266+
"label_hit_signals": "Hit Signals",
267+
"label_jaccard": "Jaccard",
268+
"label_common": "Common Fingerprints",
269+
"label_earlier": "Earlier Side",
270+
"label_detected_at": "Detected At",
271+
"label_script_a": "Script A",
272+
"label_script_b": "Script B",
273+
"label_code_diff": "Code Diff",
274+
"script_deleted": "Gelöscht",
275+
"filter_exclude_deleted": "Paare mit gelöschten Skripten ausblenden",
276+
"tab_backfill": "Nachfüllung & Neuscan",
277+
"backfill": {
278+
"help_title": "Historische Skript-Nachfüllung",
279+
"help_body": "Nach der Bereitstellung des Systems werden nur neu veröffentlichte oder aktualisierte Skripte automatisch auf Ähnlichkeit gescannt. Um historische Skripte einzubeziehen, lösen Sie eine manuelle Nachfüllung aus. Die Nachfüllung sendet für jedes Skript eine Scan-Nachricht, die asynchron verarbeitet wird. Sie können diese Seite während des Vorgangs sicher verlassen.",
280+
"status_title": "Nachfüllungsstatus",
281+
"label_running": "Status",
282+
"label_total": "Gesamt",
283+
"label_cursor": "Cursor",
284+
"label_progress": "Fortschritt",
285+
"label_started_at": "Gestartet am",
286+
"label_finished_at": "Beendet am",
287+
"state_running": "Läuft",
288+
"state_idle": "Inaktiv",
289+
"btn_start": "Nachfüllung starten",
290+
"btn_restart": "Von Anfang neu starten",
291+
"btn_refresh": "Status aktualisieren",
292+
"confirm_start_title": "Nachfüllung starten?",
293+
"confirm_start_body": "Scan-Nachrichten werden ab der letzten Cursor-Position fortgesetzt.",
294+
"confirm_restart_title": "Nachfüllung von Anfang neu starten?",
295+
"confirm_restart_body": "Der Cursor wird auf 0 zurückgesetzt und alle Skripte werden erneut gescannt. Dies ist normalerweise nur bei der Erstbereitstellung oder nach Aktualisierung der Stop-FP-Liste erforderlich.",
296+
"msg_started": "Nachfüllungsaufgabe gestartet",
297+
"manual_scan_title": "Einzelnes Skript manuell scannen",
298+
"manual_scan_placeholder": "Skript-ID eingeben",
299+
"btn_manual_scan": "Scan senden",
300+
"msg_manual_scan_published": "Scan-Nachricht veröffentlicht",
301+
"stop_fp_title": "Stop-Fingerprint Aktualisierung",
302+
"stop_fp_warn_title": "Manuelles Auslösen normalerweise nicht nötig",
303+
"stop_fp_warn_body": "Die Stop-FP-Liste wird automatisch stündlich aktualisiert. Nur einmal nach Abschluss der vollständigen Nachfüllung manuell auslösen, damit Jaccard-Berechnungen Template-Code herausfiltern.",
304+
"btn_stop_fp_refresh": "Jetzt aktualisieren",
305+
"msg_stop_fp_refreshed": "Stop-Fingerprint-Satz aktualisiert"
306+
},
307+
"signal_desc": {
308+
"avg_line_length": "Durchschnittliche Zeilenlänge zu hoch (Code möglicherweise in wenige lange Zeilen komprimiert)",
309+
"max_line_length": "Maximale Zeilenlänge zu hoch (enthält extrem lange Zeilen)",
310+
"whitespace_ratio": "Leerzeichenanteil zu niedrig (Code fehlt normale Einrückung)",
311+
"comment_ratio": "Kommentaranteil zu niedrig (Code hat fast keine Kommentare)",
312+
"single_char_ident_ratio": "Anteil einstelliger Bezeichner zu hoch (Variablennamen auf einzelne Zeichen verkürzt)",
313+
"hex_ident_ratio": "Anteil hexadezimaler Bezeichner zu hoch (verwendet _0x-verschleierte Variablennamen)",
314+
"large_string_array": "Großes String-Array erkannt (typisch für Verschleierungswerkzeuge)",
315+
"dean_edwards_packer": "Dean Edwards Packer erkannt",
316+
"aa_encode": "AAEncode-Kodierung erkannt",
317+
"jj_encode": "JJEncode-Kodierung erkannt",
318+
"eval_density": "eval/dynamische Ausführungsdichte zu hoch"
319+
}
214320
}
215321
},
216322
"auth": {
@@ -1841,5 +1947,17 @@
18411947
},
18421948
"utils": {
18431949
"time_format": "DD.MM.YYYY"
1950+
},
1951+
"similarity": {
1952+
"evidence": {
1953+
"disclaimer_title": "Preliminary Finding, Not a Final Verdict",
1954+
"disclaimer_body": "This page shows automatically detected similar-code evidence. It is informational only. Please do not draw definitive conclusions about the author from this data."
1955+
}
1956+
},
1957+
"errors": {
1958+
"integrity_rejected": {
1959+
"title": "Code Failed Integrity Check",
1960+
"help": "If this is a false positive, please apply for an exemption via the admin contact listed in the site FAQ."
1961+
}
18441962
}
18451963
}

public/locales/en-US/translations.json

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@
6565
"scores": "Score Management",
6666
"scripts": "Script Management",
6767
"system_config": "System Config",
68-
"users": "User Management"
68+
"users": "User Management",
69+
"similarity": "Similarity Detection"
6970
},
7071
"no_permission": "You do not have admin permission to access this page.",
7172
"oauth_apps": {
@@ -250,6 +251,111 @@
250251
"title": "User Management",
251252
"unban_confirm": "Are you sure you want to unban this user?",
252253
"unban_success": "User unbanned successfully"
254+
},
255+
"similarity": {
256+
"tab_pairs": "Pairs",
257+
"tab_suspects": "Suspects",
258+
"tab_integrity_reviews": "Integrity Reviews",
259+
"tab_pair_whitelist": "Pair Whitelist",
260+
"tab_integrity_whitelist": "Integrity Exemptions",
261+
"col_id": "ID",
262+
"col_script_a": "Script A",
263+
"col_script_b": "Script B",
264+
"col_jaccard": "Jaccard",
265+
"col_common": "Common Fingerprints",
266+
"col_earlier": "Earlier Side",
267+
"col_status": "Status",
268+
"col_integrity": "Integrity Score",
269+
"col_actions": "Actions",
270+
"col_script": "Script",
271+
"col_max_jaccard": "Max Jaccard",
272+
"col_coverage": "External Coverage",
273+
"col_pair_count": "Pair Count",
274+
"col_detected_at": "Detected At",
275+
"col_score": "Score",
276+
"col_createtime": "Created",
277+
"col_reason": "Reason",
278+
"col_added_by": "Added By",
279+
"status_pending": "Pending",
280+
"status_whitelisted": "Whitelisted",
281+
"status_resolved": "Resolved",
282+
"review_pending": "Pending",
283+
"review_ok": "OK",
284+
"review_violated": "Violation",
285+
"action_detail": "Details",
286+
"action_resolve": "Resolve",
287+
"action_whitelist": "Whitelist",
288+
"action_remove": "Remove",
289+
"confirm_remove_whitelist": "Remove this whitelist entry?",
290+
"msg_removed": "Removed",
291+
"modal_add_int_whitelist": "Add Integrity Exemption",
292+
"label_script_id": "Script ID",
293+
"label_reason": "Reason",
294+
"btn_add": "Add",
295+
"modal_resolve_title": "Mark Integrity Review",
296+
"label_decision": "Decision",
297+
"label_note": "Note",
298+
"decision_ok": "OK",
299+
"decision_violated": "Violation",
300+
"msg_review_resolved": "Marked successfully",
301+
"msg_whitelisted": "Added to whitelist",
302+
"drawer_review_detail": "Integrity Review Details",
303+
"label_score": "Total Score",
304+
"label_sub_scores": "Category Scores",
305+
"label_hit_signals": "Hit Signals",
306+
"label_jaccard": "Jaccard",
307+
"label_common": "Common Fingerprints",
308+
"label_earlier": "Earlier Side",
309+
"label_detected_at": "Detected At",
310+
"label_script_a": "Script A",
311+
"label_script_b": "Script B",
312+
"label_code_diff": "Code Diff",
313+
"script_deleted": "Deleted",
314+
"filter_exclude_deleted": "Hide pairs with deleted scripts",
315+
"tab_backfill": "Backfill & Rescan",
316+
"backfill": {
317+
"help_title": "Historical Script Backfill",
318+
"help_body": "After the system is deployed, only newly published or updated scripts are automatically scanned for similarity. To include historical scripts in comparisons, trigger a manual backfill. The backfill sends a scan message for each script, processed asynchronously by background consumers. You can safely leave this page during the process.",
319+
"status_title": "Backfill Status",
320+
"label_running": "Status",
321+
"label_total": "Total",
322+
"label_cursor": "Cursor",
323+
"label_progress": "Progress",
324+
"label_started_at": "Started At",
325+
"label_finished_at": "Finished At",
326+
"state_running": "Running",
327+
"state_idle": "Idle",
328+
"btn_start": "Start Backfill",
329+
"btn_restart": "Restart from Beginning",
330+
"btn_refresh": "Refresh Status",
331+
"confirm_start_title": "Start backfill?",
332+
"confirm_start_body": "This will continue sending scan messages from the last cursor position.",
333+
"confirm_restart_title": "Restart backfill from beginning?",
334+
"confirm_restart_body": "The cursor will reset to 0, and all scripts in the database will be re-scanned. This is usually only needed on initial deployment or after refreshing the stop-fp list.",
335+
"msg_started": "Backfill task started",
336+
"manual_scan_title": "Manually Rescan Single Script",
337+
"manual_scan_placeholder": "Enter script ID",
338+
"btn_manual_scan": "Send Scan",
339+
"msg_manual_scan_published": "Scan message published",
340+
"stop_fp_title": "Stop-fingerprint Refresh",
341+
"stop_fp_warn_title": "Manual trigger usually not needed",
342+
"stop_fp_warn_body": "The stop-fp list is refreshed automatically every hour by a scheduled task. Only trigger manually once after completing the initial full backfill (step 8 in §8.5), so Jaccard calculations filter out common template code.",
343+
"btn_stop_fp_refresh": "Refresh Now",
344+
"msg_stop_fp_refreshed": "Stop-fingerprint set refreshed"
345+
},
346+
"signal_desc": {
347+
"avg_line_length": "Average line length too high (code may be compressed into few long lines)",
348+
"max_line_length": "Maximum line length too high (contains extremely long lines)",
349+
"whitespace_ratio": "Whitespace ratio too low (code lacks normal spacing and indentation)",
350+
"comment_ratio": "Comment ratio too low (code has almost no comments)",
351+
"single_char_ident_ratio": "Single-character identifier ratio too high (variable names shortened to single characters)",
352+
"hex_ident_ratio": "Hex identifier ratio too high (uses _0x prefixed obfuscated variable names)",
353+
"large_string_array": "Large string array detected (common in obfuscation tool string tables)",
354+
"dean_edwards_packer": "Dean Edwards packer detected",
355+
"aa_encode": "AAEncode encoding detected",
356+
"jj_encode": "JJEncode encoding detected",
357+
"eval_density": "eval/dynamic execution call density too high"
358+
}
253359
}
254360
},
255361
"auth": {
@@ -1883,5 +1989,17 @@
18831989
},
18841990
"utils": {
18851991
"time_format": "YYYY-MM-DD"
1992+
},
1993+
"similarity": {
1994+
"evidence": {
1995+
"disclaimer_title": "Preliminary Finding, Not a Final Verdict",
1996+
"disclaimer_body": "This page shows automatically detected similar-code evidence. It is informational only. Please do not draw definitive conclusions about the author from this data."
1997+
}
1998+
},
1999+
"errors": {
2000+
"integrity_rejected": {
2001+
"title": "Code Failed Integrity Check",
2002+
"help": "If this is a false positive, please apply for an exemption via the admin contact listed in the site FAQ."
2003+
}
18862004
}
18872005
}

0 commit comments

Comments
 (0)