@onco-foundry/mask-port 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,8 @@ export type TextInMaskerOptions = {
23
23
  readonly baseURL?: string;
24
24
  readonly timeoutMs?: number;
25
25
  readonly maxInputBytes?: number;
26
+ /** 自动处理链路使用:已发现的敏感词无法完整定位时拒绝交付。 */
27
+ readonly requireAllMatches?: boolean;
26
28
  /** 测试注入缝:替换掉真实的 HTTP 层,不碰网络。 */
27
29
  readonly fetch?: typeof fetch;
28
30
  /**
@@ -156,17 +156,28 @@ export const createTextInMasker = (options) => {
156
156
  if (payload.data.code !== 200 || payload.data.result === undefined) {
157
157
  throw new AppError(`TextIn 脱敏接口报错:${payload.data.code} ${payload.data.message}`, 502);
158
158
  }
159
+ if (options.requireAllMatches && payload.data.result.pages.length !== 1) {
160
+ throw new AppError('TextIn 脱敏需要单页图片识别结果', 502);
161
+ }
159
162
  const page = payload.data.result.pages[0];
160
163
  const lines = page.lines;
161
164
  const recognizedText = lines.map((line) => line.text).join('\n');
162
165
  const scaleX = page.width !== undefined && page.width > 0 ? imageWidth / page.width : 1;
163
166
  const scaleY = page.height !== undefined && page.height > 0 ? imageHeight / page.height : 1;
164
- const requested = (await finder(recognizedText))
167
+ if (options.requireAllMatches && !recognizedText.trim()) {
168
+ throw new AppError('TextIn 脱敏未识别出文字,无法确认处理结果', 502);
169
+ }
170
+ const detected = await finder(recognizedText);
171
+ if (options.requireAllMatches && detected.some(word => !word.text || !(word.target in TARGET_LABELS) || !recognizedText.includes(word.text))) {
172
+ throw new AppError('TextIn 脱敏判定结果无法定位', 502);
173
+ }
174
+ const requested = detected
165
175
  .filter((word) => word.text !== '' && request.targets.includes(word.target));
166
176
  const finalQuads = [];
167
177
  const mappings = [];
168
178
  const counters = new Map();
169
179
  for (const word of requested) {
180
+ let located = 0;
170
181
  for (const line of lines) {
171
182
  let fromIndex = 0;
172
183
  while (true) {
@@ -174,9 +185,30 @@ export const createTextInMasker = (options) => {
174
185
  if (hit < 0)
175
186
  break;
176
187
  fromIndex = hit + word.text.length;
177
- const quad = locateWordQuad(line, hit, word.text.length);
178
- if (quad === undefined)
188
+ const locatedLine = options.requireAllMatches && line.char_positions?.length !== line.text.length
189
+ ? { ...line, char_positions: undefined } : line;
190
+ const quad = locateWordQuad(locatedLine, hit, word.text.length);
191
+ if (quad === undefined) {
192
+ if (options.requireAllMatches)
193
+ throw new AppError('TextIn 脱敏敏感词缺少坐标', 502);
179
194
  continue;
195
+ }
196
+ if (options.requireAllMatches) {
197
+ const scaled = scaleQuadToImage(quad, scaleX, scaleY);
198
+ const xs = scaled.map(point => point[0]);
199
+ const ys = scaled.map(point => point[1]);
200
+ const area = Math.abs(scaled.reduce((sum, [x, y], index) => {
201
+ const next = scaled[(index + 1) % scaled.length];
202
+ return sum + x * next[1] - next[0] * y;
203
+ }, 0));
204
+ if (area < 1 || scaled.flat().some(n => !Number.isFinite(n)) ||
205
+ Math.min(...xs) < 0 || Math.min(...ys) < 0 ||
206
+ Math.max(...xs) > imageWidth || Math.max(...ys) > imageHeight ||
207
+ Math.max(...xs) <= Math.min(...xs) || Math.max(...ys) <= Math.min(...ys)) {
208
+ throw new AppError('TextIn 脱敏坐标无效', 502);
209
+ }
210
+ }
211
+ located++;
180
212
  finalQuads.push(clampQuad(scaleQuad(scaleQuadToImage(quad, scaleX, scaleY), SAFETY_SCALE), imageWidth, imageHeight));
181
213
  const count = (counters.get(word.target) ?? 0) + 1;
182
214
  counters.set(word.target, count);
@@ -187,6 +219,9 @@ export const createTextInMasker = (options) => {
187
219
  });
188
220
  }
189
221
  }
222
+ if (options.requireAllMatches && located === 0) {
223
+ throw new AppError('TextIn 脱敏敏感词无法在单行定位', 502);
224
+ }
190
225
  }
191
226
  const overlay = polygonSvg(imageWidth, imageHeight, finalQuads);
192
227
  const uprightMasked = await sharp(upright.data)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onco-foundry/mask-port",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -16,10 +16,10 @@
16
16
  "agent-lattice": "0.25.0",
17
17
  "sharp": "^0.35.3",
18
18
  "zod": "^4.4.3",
19
- "@onco-foundry/resource-versioning": "0.2.0",
20
- "@onco-foundry/trace-port": "0.3.2",
19
+ "@onco-foundry/capability-registry": "0.1.1",
21
20
  "@onco-foundry/errors": "0.1.1",
22
- "@onco-foundry/capability-registry": "0.1.1"
21
+ "@onco-foundry/trace-port": "0.3.2",
22
+ "@onco-foundry/resource-versioning": "0.3.0"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"