feat: 添加算术验证码与字符验证码处理能力
新增算术验证码解析、字符清洗能力及测试文件,注册OCR_CalculationCode和OCR_CommonEnglishNumbersCode两个新能力,补充验证码处理逻辑
This commit is contained in:
1 parent
902ee0cec4
commit
5ef797dcc0
6 files changed
+344
-96
No files matched your search
@@ -0,0 +1,56 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getHandler } from "../engine/registry";
|
||||
import type { CatalogComponent, FlowNode } from "../types";
|
||||
import type { RunContext } from "../engine/types";
|
||||
import "./remaining-core";
|
||||
import "./captcha-caps"; // last-wins:覆盖 remaining-core 的通用实现
|
||||
import { cleanCodeText, parseCalculation } from "./captcha-caps";
|
||||
|
||||
const ctx = { signal: new AbortController().signal, log: () => {}, vars: {}, evidenceDir: "D:/office-test2/ocr-test" } as unknown as RunContext;
|
||||
const node: FlowNode = { id: "smoke", componentId: "smoke", position: { x: 0, y: 0 }, values: {}, options: {} };
|
||||
|
||||
describe("parseCalculation 解析层(纯逻辑)", () => {
|
||||
it("基础四则与等号问号", () => {
|
||||
expect(parseCalculation("3+5=?")).toEqual({ expression: "3+5", result: 8 });
|
||||
});
|
||||
it("x/X/* 归一为乘", () => {
|
||||
expect(parseCalculation("6x4=?")!.result).toBe(24);
|
||||
expect(parseCalculation("9X9=?")!.result).toBe(81);
|
||||
});
|
||||
it("全角与杂字符", () => {
|
||||
expect(parseCalculation("12+7=?")!.result).toBe(19);
|
||||
});
|
||||
it("长破折号/Unicode 减号", () => {
|
||||
expect(parseCalculation("12—7")!.result).toBe(5);
|
||||
});
|
||||
it("除法整除与小数", () => {
|
||||
expect(parseCalculation("8/2")!.result).toBe(4);
|
||||
expect(parseCalculation("7/2")!.result).toBe(3.5);
|
||||
});
|
||||
it("无算式返回 null", () => {
|
||||
expect(parseCalculation("ab12cd")).toBeNull();
|
||||
});
|
||||
it("字符验证码清洗", () => {
|
||||
expect(cleanCodeText("a b 7 X ? = ")).toBe("ab7X");
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!existsSync("D:/office-test2/ocr-test/captcha_0_3p5_q.png"))("OCR_CalculationCode 端到端(真实模型)", () => {
|
||||
const cases: Array<[string, string]> = [
|
||||
["captcha_0_3p5_q", "8"],
|
||||
["captcha_1_12m7_q", "5"],
|
||||
["captcha_2_6t4_q", "24"],
|
||||
["captcha_3_15p28_q", "43"],
|
||||
["captcha_4_9t9_q", "81"],
|
||||
];
|
||||
for (const [file, expected] of cases) {
|
||||
it(`${file} -> ${expected}`, async () => {
|
||||
const handler = getHandler("OCR_CalculationCode");
|
||||
expect(handler).toBeTruthy();
|
||||
const out = await handler!(ctx, node, { path: `D:/office-test2/ocr-test/${file}.png` }, {} as CatalogComponent);
|
||||
console.log(`[${file}]`, String(out));
|
||||
expect(String(out)).toBe(expected);
|
||||
}, 300000);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* 验证码类 OCR 专门实现。
|
||||
*
|
||||
* 此前 OCR_CalculationCode / OCR_CommonEnglishNumbersCode 与其他 30 个 OCR
|
||||
* 组件共用通用实现(remaining-core 的 ocrImage),只返回整图 OCR 原文:
|
||||
* - 算术验证码返回的是 "3+5=?" 这样的算式原文,没有计算结果,流程无法直接填入答案;
|
||||
* - 真实验证码的干扰线/噪点会让原文混入杂字符,符号常被认成 x/X/*、全角等。
|
||||
*
|
||||
* 这里提供专门处理:OCR 原文 -> 字符归一化 -> 算式解析 -> 计算结果。
|
||||
* 解析失败时返回清洗后的原文并记 warning,不让节点直接失败(调用方可自行判断)。
|
||||
*/
|
||||
import { getHandler, register } from "../engine/registry";
|
||||
import { RuntimeError, type RunContext } from "../engine/types";
|
||||
import type { CatalogComponent, FlowNode } from "../types";
|
||||
|
||||
interface CalculationParse {
|
||||
expression: string;
|
||||
result: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 OCR 原文中解析算术算式。
|
||||
* 归一化范围:全角数字/符号、常见运算符误认(x/X/* -> 乘,全角+-×÷,长破折号 -> 减)。
|
||||
*/
|
||||
export function parseCalculation(raw: string): CalculationParse | null {
|
||||
const s = String(raw ?? "")
|
||||
.replace(/[0-9]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xfee0))
|
||||
.replace(/+/g, "+")
|
||||
.replace(/[-―—–−]/g, "-")
|
||||
.replace(/[×✕✖xX*X*]/g, "*")
|
||||
.replace(/[÷/]/g, "/")
|
||||
.replace(/[==??。,,、\s]/g, "");
|
||||
const match = s.match(/(\d+)([+\-*/])(\d+)/);
|
||||
if (!match) return null;
|
||||
const a = Number(match[1]);
|
||||
const b = Number(match[3]);
|
||||
const op = match[2];
|
||||
let result: number;
|
||||
if (op === "+") result = a + b;
|
||||
else if (op === "-") result = a - b;
|
||||
else if (op === "*") result = a * b;
|
||||
else {
|
||||
if (b === 0) return null;
|
||||
result = a / b;
|
||||
}
|
||||
if (!Number.isFinite(result)) return null;
|
||||
result = Math.round(result * 100) / 100;
|
||||
const prettyOp = op === "*" ? "×" : op === "/" ? "÷" : op;
|
||||
return { expression: `${a}${prettyOp}${b}`, result };
|
||||
}
|
||||
|
||||
/** 普通字符验证码:去除空白与常见杂字符,保留字母数字 */
|
||||
export function cleanCodeText(raw: string): string {
|
||||
return String(raw ?? "")
|
||||
.replace(/\s+/g, "")
|
||||
.replace(/[==??.,,、;;:"'()()\[\]{}]/g, "");
|
||||
}
|
||||
|
||||
async function ocrRaw(args: Record<string, unknown>, node: FlowNode, ctx: RunContext): Promise<string> {
|
||||
// 复用通用 OCR(remaining-core 已注册的 OCR_GeneralTextV2)拿原文,避免重复加载模型
|
||||
const base = getHandler("OCR_GeneralTextV2");
|
||||
if (!base) throw new RuntimeError("capability_not_implemented", "通用 OCR 能力不可用", { nodeId: node.id, componentId: node.componentId });
|
||||
return String(await base(ctx, node, args, {} as CatalogComponent));
|
||||
}
|
||||
|
||||
register("OCR_CalculationCode", async (ctx, node, args) => {
|
||||
const raw = await ocrRaw(args, node, ctx);
|
||||
const parsed = parseCalculation(raw);
|
||||
if (!parsed) {
|
||||
await ctx.log(`算术验证码未能解析出算式(原文:${raw}),已返回清洗原文`, "warn", node.id);
|
||||
return raw.replace(/\s+/g, "");
|
||||
}
|
||||
await ctx.log(`算术验证码:${parsed.expression} = ${parsed.result}(OCR 原文:${raw})`, "info", node.id);
|
||||
return String(parsed.result);
|
||||
});
|
||||
|
||||
register("OCR_CommonEnglishNumbersCode", async (ctx, node, args) => {
|
||||
const raw = await ocrRaw(args, node, ctx);
|
||||
const cleaned = cleanCodeText(raw);
|
||||
await ctx.log(`字符验证码:${cleaned}(OCR 原文:${raw})`, "info", node.id);
|
||||
return cleaned;
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getHandler } from "../engine/registry";
|
||||
import type { CatalogComponent, FlowNode } from "../types";
|
||||
import type { RunContext } from "../engine/types";
|
||||
import "./remaining-core";
|
||||
|
||||
const ctx = { signal: new AbortController().signal, log: () => {}, vars: {}, evidenceDir: "D:/office-test2/ocr-test" } as unknown as RunContext;
|
||||
const node: FlowNode = { id: "smoke", componentId: "smoke", position: { x: 0, y: 0 }, values: {}, options: {} };
|
||||
|
||||
describe("算术验证码现状侦察", () => {
|
||||
const files = ["captcha_0_3p5_q", "captcha_1_12m7_q", "captcha_2_6t4_q", "captcha_3_15p28_q", "captcha_4_9t9_q"];
|
||||
it("当前 OCR 原始输出", async () => {
|
||||
for (const f of files) {
|
||||
const handler = getHandler("OCR_CalculationCode");
|
||||
const out = await handler!(ctx, node, { path: `D:/office-test2/ocr-test/${f}.png` }, {} as CatalogComponent);
|
||||
console.log(`[${f}]`, JSON.stringify(out));
|
||||
}
|
||||
expect(true).toBe(true);
|
||||
}, 600000);
|
||||
});
|
||||
@@ -36,6 +36,7 @@ import "./data-table-common";
|
||||
import "./basic-encryption";
|
||||
import "./mail-remaining";
|
||||
import "./remaining-core";
|
||||
import "./captcha-caps";
|
||||
import "./common-final";
|
||||
import "./human-dialog";
|
||||
import "./ai-service";
|
||||
|
||||
Reference in new issue
Block a user