@onco-foundry/mask-port 0.1.0
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.
- package/dist/create_masker.d.ts +19 -0
- package/dist/create_masker.js +17 -0
- package/dist/fake_masker.d.ts +3 -0
- package/dist/fake_masker.js +12 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/llm_sensitive_word_finder.d.ts +22 -0
- package/dist/llm_sensitive_word_finder.js +125 -0
- package/dist/masker.d.ts +45 -0
- package/dist/masker.js +8 -0
- package/dist/quad.d.ts +13 -0
- package/dist/quad.js +44 -0
- package/dist/qwen_agent_masker.d.ts +81 -0
- package/dist/qwen_agent_masker.js +249 -0
- package/dist/redact_text.d.ts +8 -0
- package/dist/redact_text.js +17 -0
- package/dist/resources/qwen-agent-name/current.json +1 -0
- package/dist/resources/qwen-agent-name/v1/manifest.json +9 -0
- package/dist/resources/qwen-agent-name/v1/system-prompt.md +14 -0
- package/dist/resources/qwen-agent-name/v2/manifest.json +9 -0
- package/dist/resources/qwen-agent-name/v2/system-prompt.md +17 -0
- package/dist/resources/qwen-agent-name/v3/manifest.json +9 -0
- package/dist/resources/qwen-agent-name/v3/system-prompt.md +18 -0
- package/dist/tencent_masker.d.ts +20 -0
- package/dist/tencent_masker.js +126 -0
- package/dist/textin_masker.d.ts +40 -0
- package/dist/textin_masker.js +206 -0
- package/package.json +31 -0
- package/resources/qwen-agent-name/current.json +1 -0
- package/resources/qwen-agent-name/v1/manifest.json +9 -0
- package/resources/qwen-agent-name/v1/system-prompt.md +14 -0
- package/resources/qwen-agent-name/v2/manifest.json +9 -0
- package/resources/qwen-agent-name/v2/system-prompt.md +17 -0
- package/resources/qwen-agent-name/v3/manifest.json +9 -0
- package/resources/qwen-agent-name/v3/system-prompt.md +18 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Masker } from './masker.ts';
|
|
2
|
+
import { type QwenAgentMaskerOptions } from './qwen_agent_masker.ts';
|
|
3
|
+
import { type TencentMaskerOptions } from './tencent_masker.ts';
|
|
4
|
+
import { type TextInMaskerOptions } from './textin_masker.ts';
|
|
5
|
+
/**
|
|
6
|
+
* 脱敏器的装配选项:kind 判别用哪个实现。
|
|
7
|
+
* 加新供应商时在这里加一个 union 成员,调用方只改 kind。
|
|
8
|
+
*/
|
|
9
|
+
export type MaskerOptions = {
|
|
10
|
+
readonly kind: 'fake';
|
|
11
|
+
} | ({
|
|
12
|
+
readonly kind: 'tencent';
|
|
13
|
+
} & TencentMaskerOptions) | ({
|
|
14
|
+
readonly kind: 'textin';
|
|
15
|
+
} & TextInMaskerOptions) | ({
|
|
16
|
+
readonly kind: 'qwen-agent-name';
|
|
17
|
+
} & QwenAgentMaskerOptions);
|
|
18
|
+
/** 脱敏端口的唯一工厂:换供应商只改 kind,调用方不 import 具体实现。 */
|
|
19
|
+
export declare const createMasker: (options: MaskerOptions) => Promise<Masker>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createFakeMasker } from './fake_masker.js';
|
|
2
|
+
import { createQwenAgentMasker, } from './qwen_agent_masker.js';
|
|
3
|
+
import { createTencentMasker } from './tencent_masker.js';
|
|
4
|
+
import { createTextInMasker } from './textin_masker.js';
|
|
5
|
+
/** 脱敏端口的唯一工厂:换供应商只改 kind,调用方不 import 具体实现。 */
|
|
6
|
+
export const createMasker = async (options) => {
|
|
7
|
+
switch (options.kind) {
|
|
8
|
+
case 'fake':
|
|
9
|
+
return createFakeMasker();
|
|
10
|
+
case 'tencent':
|
|
11
|
+
return createTencentMasker(options);
|
|
12
|
+
case 'textin':
|
|
13
|
+
return createTextInMasker(options);
|
|
14
|
+
case 'qwen-agent-name':
|
|
15
|
+
return await createQwenAgentMasker(options);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** 不做任何脱敏的 Masker:原样返回图片,mapping 为空。供测试和本地开发使用。 */
|
|
2
|
+
export function createFakeMasker() {
|
|
3
|
+
return {
|
|
4
|
+
async mask(request) {
|
|
5
|
+
return {
|
|
6
|
+
maskedImageBytes: request.imageBytes,
|
|
7
|
+
mapping: [],
|
|
8
|
+
processorVersion: { engine: 'fake-masker-v1' },
|
|
9
|
+
};
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { MASK_TARGETS, type Masker, type MaskMappingEntry, type MaskRequest, type MaskResult, type MaskTarget, } from './masker.ts';
|
|
2
|
+
export { createMasker, type MaskerOptions } from './create_masker.ts';
|
|
3
|
+
export { createTencentMasker, type TencentMaskerOptions } from './tencent_masker.ts';
|
|
4
|
+
export { createTextInMasker, findSensitiveWordsByRules, type SensitiveWord, type SensitiveWordFinder, type TextInMaskerOptions, } from './textin_masker.ts';
|
|
5
|
+
export { redactTextByMapping } from './redact_text.ts';
|
|
6
|
+
export { createLlmSensitiveWordFinder, type LlmSensitiveWordFinderOptions, } from './llm_sensitive_word_finder.ts';
|
|
7
|
+
export { createQwenAgentMasker, qwenAgentNameMaskCapabilityCard, qwenAgentNameMaskInputSchema, type QwenAgentMasker, type QwenAgentMaskerOptions, type QwenAgentNameMaskInput, } from './qwen_agent_masker.ts';
|
|
8
|
+
export { createFakeMasker } from './fake_masker.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { MASK_TARGETS, } from './masker.js';
|
|
2
|
+
export { createMasker } from './create_masker.js';
|
|
3
|
+
export { createTencentMasker } from './tencent_masker.js';
|
|
4
|
+
export { createTextInMasker, findSensitiveWordsByRules, } from './textin_masker.js';
|
|
5
|
+
export { redactTextByMapping } from './redact_text.js';
|
|
6
|
+
export { createLlmSensitiveWordFinder, } from './llm_sensitive_word_finder.js';
|
|
7
|
+
export { createQwenAgentMasker, qwenAgentNameMaskCapabilityCard, qwenAgentNameMaskInputSchema, } from './qwen_agent_masker.js';
|
|
8
|
+
export { createFakeMasker } from './fake_masker.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM 敏感词判定器:把 OCR 全文发给 Anthropic 兼容模型(Messages API),提取隐私实体。
|
|
3
|
+
* 模型只回原文中逐字出现的词,脱敏器按这些词在字符坐标里定位遮盖。
|
|
4
|
+
* 协议与上游 agent-lattice 一致:POST {baseURL}/v1/messages。
|
|
5
|
+
*
|
|
6
|
+
* 注意:OCR 全文含隐私,启用它意味着文本出域给该模型服务;
|
|
7
|
+
* 只有该服务被批准作为隐私数据处理方时才可装配。
|
|
8
|
+
*/
|
|
9
|
+
import type { SensitiveWordFinder } from './textin_masker.ts';
|
|
10
|
+
export type LlmSensitiveWordFinderOptions = {
|
|
11
|
+
readonly baseURL: string;
|
|
12
|
+
readonly apiKey: string;
|
|
13
|
+
readonly model: string;
|
|
14
|
+
readonly timeoutMs?: number;
|
|
15
|
+
/** 测试注入缝:替换掉真实的 HTTP 层,不碰网络。 */
|
|
16
|
+
readonly fetch?: typeof fetch;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* 创建 LLM 敏感词判定器,直接作为 TextIn 脱敏器的 findSensitiveWords 使用。
|
|
20
|
+
* 模型返回的词若不在端口目标集内、不是原文子串或为空,一律丢弃。
|
|
21
|
+
*/
|
|
22
|
+
export declare const createLlmSensitiveWordFinder: (options: LlmSensitiveWordFinderOptions) => SensitiveWordFinder;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM 敏感词判定器:把 OCR 全文发给 Anthropic 兼容模型(Messages API),提取隐私实体。
|
|
3
|
+
* 模型只回原文中逐字出现的词,脱敏器按这些词在字符坐标里定位遮盖。
|
|
4
|
+
* 协议与上游 agent-lattice 一致:POST {baseURL}/v1/messages。
|
|
5
|
+
*
|
|
6
|
+
* 注意:OCR 全文含隐私,启用它意味着文本出域给该模型服务;
|
|
7
|
+
* 只有该服务被批准作为隐私数据处理方时才可装配。
|
|
8
|
+
*/
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { AppError } from '@onco-foundry/errors';
|
|
11
|
+
import { MASK_TARGETS } from './masker.js';
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
13
|
+
// prompt 是模块内置资产,不开放注入:本模块定位就是医疗文书脱敏,提取口径由模块自己保证。
|
|
14
|
+
const SYSTEM_PROMPT = `你从医疗文书扫描件(病历、检查报告、住院单等)的 OCR 文本中提取隐私信息。只提取以下类别:
|
|
15
|
+
- patient_name:患者姓名(包括「姓名:」标签后的、正文叙述中出现的)
|
|
16
|
+
- id_number:身份证号等证件号码
|
|
17
|
+
- phone:手机号、联系电话
|
|
18
|
+
- address:住址、联系地址
|
|
19
|
+
- doctor_signature:医生姓名(签名处或「医师:」标签后的)
|
|
20
|
+
|
|
21
|
+
要求:
|
|
22
|
+
- 只输出原文中逐字出现的字符串,不改写、不概括、不补全。
|
|
23
|
+
- 医院名、科室名、病名、药名不是隐私,不要提取。
|
|
24
|
+
- 没有可提取的内容就输出空数组。
|
|
25
|
+
- 只输出 JSON 数组,形如 [{"text":"张三","target":"patient_name"}],不要输出任何其他内容。`;
|
|
26
|
+
const extractedSchema = z.array(z.object({
|
|
27
|
+
text: z.string(),
|
|
28
|
+
target: z.string(),
|
|
29
|
+
}));
|
|
30
|
+
const modelResponseSchema = z.object({
|
|
31
|
+
content: z.array(z.object({
|
|
32
|
+
type: z.string(),
|
|
33
|
+
text: z.string().optional(),
|
|
34
|
+
})).min(1),
|
|
35
|
+
});
|
|
36
|
+
const extractJsonArray = (content) => {
|
|
37
|
+
const cleaned = content.trim()
|
|
38
|
+
.replace(/^```(?:json)?\s*/u, '')
|
|
39
|
+
.replace(/\s*```$/u, '');
|
|
40
|
+
const start = cleaned.indexOf('[');
|
|
41
|
+
const end = cleaned.lastIndexOf(']');
|
|
42
|
+
if (start < 0 || end <= start) {
|
|
43
|
+
throw new AppError('LLM 敏感词提取返回的结构不是 JSON 数组', 502);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(cleaned.slice(start, end + 1));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new AppError('LLM 敏感词提取返回的 JSON 无法解析', 502);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* 创建 LLM 敏感词判定器,直接作为 TextIn 脱敏器的 findSensitiveWords 使用。
|
|
54
|
+
* 模型返回的词若不在端口目标集内、不是原文子串或为空,一律丢弃。
|
|
55
|
+
*/
|
|
56
|
+
export const createLlmSensitiveWordFinder = (options) => {
|
|
57
|
+
const credentials = z.object({
|
|
58
|
+
baseURL: z.url(),
|
|
59
|
+
apiKey: z.string().trim().min(1),
|
|
60
|
+
model: z.string().trim().min(1),
|
|
61
|
+
}).safeParse({ baseURL: options.baseURL, apiKey: options.apiKey, model: options.model });
|
|
62
|
+
if (!credentials.success) {
|
|
63
|
+
throw new AppError('LLM 敏感词判定器配置非法', 500);
|
|
64
|
+
}
|
|
65
|
+
const doFetch = options.fetch ?? fetch;
|
|
66
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
67
|
+
return async (text) => {
|
|
68
|
+
if (text.trim() === '')
|
|
69
|
+
return [];
|
|
70
|
+
let response;
|
|
71
|
+
try {
|
|
72
|
+
response = await doFetch(`${options.baseURL.replace(/\/$/u, '')}/v1/messages`, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: {
|
|
75
|
+
'x-api-key': options.apiKey,
|
|
76
|
+
'anthropic-version': '2023-06-01',
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
},
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
model: options.model,
|
|
81
|
+
system: SYSTEM_PROMPT,
|
|
82
|
+
messages: [{ role: 'user', content: text }],
|
|
83
|
+
temperature: 0,
|
|
84
|
+
max_tokens: 4096,
|
|
85
|
+
stream: false,
|
|
86
|
+
}),
|
|
87
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
throw new AppError(`LLM 敏感词提取请求未完成:${error instanceof Error ? error.name : '未知错误'}`, 502);
|
|
92
|
+
}
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
throw new AppError(`LLM 敏感词提取 HTTP ${response.status}`, 502);
|
|
95
|
+
}
|
|
96
|
+
const payload = modelResponseSchema.safeParse(await response.json().catch(() => undefined));
|
|
97
|
+
if (!payload.success) {
|
|
98
|
+
throw new AppError('LLM 敏感词提取响应缺少模型输出', 502);
|
|
99
|
+
}
|
|
100
|
+
// 带推理的模型会先回 thinking 块,取第一个 text 块。
|
|
101
|
+
const textBlock = payload.data.content.find((block) => block.type === 'text' && block.text);
|
|
102
|
+
if (textBlock?.text === undefined) {
|
|
103
|
+
throw new AppError('LLM 敏感词提取响应缺少文本输出', 502);
|
|
104
|
+
}
|
|
105
|
+
const parsed = extractedSchema.safeParse(extractJsonArray(textBlock.text));
|
|
106
|
+
if (!parsed.success) {
|
|
107
|
+
throw new AppError('LLM 敏感词提取返回的结构校验失败', 502);
|
|
108
|
+
}
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
const words = [];
|
|
111
|
+
for (const item of parsed.data) {
|
|
112
|
+
const word = item.text.trim();
|
|
113
|
+
if (word === '' || !text.includes(word))
|
|
114
|
+
continue;
|
|
115
|
+
if (!MASK_TARGETS.includes(item.target))
|
|
116
|
+
continue;
|
|
117
|
+
const key = `${item.target}${word}`;
|
|
118
|
+
if (seen.has(key))
|
|
119
|
+
continue;
|
|
120
|
+
seen.add(key);
|
|
121
|
+
words.push({ text: word, target: item.target });
|
|
122
|
+
}
|
|
123
|
+
return words;
|
|
124
|
+
};
|
|
125
|
+
};
|
package/dist/masker.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** 脱敏目标类别。 */
|
|
2
|
+
export declare const MASK_TARGETS: readonly ['patient_name', 'id_number', 'phone', 'address', 'doctor_signature'];
|
|
3
|
+
export type MaskTarget = (typeof MASK_TARGETS)[number];
|
|
4
|
+
export type MaskRequest = {
|
|
5
|
+
/** 待脱敏的图片字节。 */
|
|
6
|
+
readonly imageBytes: Uint8Array;
|
|
7
|
+
/** 本次要脱敏的目标类别。 */
|
|
8
|
+
readonly targets: readonly MaskTarget[];
|
|
9
|
+
/** 图片内容顺时针旋转多少度后可正立阅读;缺省表示无需旋转。 */
|
|
10
|
+
readonly rotationClockwiseDegrees?: 0 | 90 | 180 | 270;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* 一处脱敏的对应记录:脱敏版上的占位符对应原文的哪段内容。
|
|
14
|
+
* 证据链回溯时用它把脱敏版上的行号定位回原文区间。
|
|
15
|
+
*/
|
|
16
|
+
export type MaskMappingEntry = {
|
|
17
|
+
/** 脱敏版上替换原文的占位符,如 [姓名1]。 */
|
|
18
|
+
readonly placeholder: string;
|
|
19
|
+
/** 该处脱敏的目标类别。 */
|
|
20
|
+
readonly target: MaskTarget;
|
|
21
|
+
/** 被遮盖的原文内容。 */
|
|
22
|
+
readonly originalText: string;
|
|
23
|
+
};
|
|
24
|
+
export type MaskResult = {
|
|
25
|
+
/** 脱敏后的图片字节。 */
|
|
26
|
+
readonly maskedImageBytes: Uint8Array;
|
|
27
|
+
/** 占位符与原文区间的对应记录,按出现顺序排列。 */
|
|
28
|
+
readonly mapping: readonly MaskMappingEntry[];
|
|
29
|
+
/**
|
|
30
|
+
* 脱敏过程中顺带识别出的原文全文(含隐私,限信任域内使用)。
|
|
31
|
+
* 只有内部带 OCR 的引擎(如 TextIn)才返回;下游用它按 mapping 替换出
|
|
32
|
+
* 脱敏文本后,可以省掉对脱敏图的第二次 OCR。其它引擎不返回该字段。
|
|
33
|
+
*/
|
|
34
|
+
readonly recognizedText?: string;
|
|
35
|
+
/** 产出绑定的脱敏实现、模型与 prompt 版本,供归档审计。 */
|
|
36
|
+
readonly processorVersion?: {
|
|
37
|
+
readonly engine: string;
|
|
38
|
+
readonly model?: string;
|
|
39
|
+
readonly prompt?: string;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
/** 图片脱敏端口:遮盖指定类别的隐私信息,并返回占位符映射供回溯。 */
|
|
43
|
+
export type Masker = {
|
|
44
|
+
mask(request: MaskRequest): Promise<MaskResult>;
|
|
45
|
+
};
|
package/dist/masker.js
ADDED
package/dist/quad.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type Point = readonly [number, number];
|
|
2
|
+
export type Quad = readonly [Point, Point, Point, Point];
|
|
3
|
+
/**
|
|
4
|
+
* 把模型返回的旋转框规范化成 0-1000 归一化坐标。
|
|
5
|
+
* 容忍模型偶尔返回 0-1 小数坐标;越界和零面积框拒绝(交付合同,不是内容判断)。
|
|
6
|
+
*/
|
|
7
|
+
export declare const normalizeQuad: (raw: unknown) => Quad;
|
|
8
|
+
export declare const normalizedToPixels: (quad: Quad, width: number, height: number) => Quad;
|
|
9
|
+
/** 以中心为原点外扩,给笔画外缘留安全边。 */
|
|
10
|
+
export declare const scaleQuad: (quad: Quad, factor: number) => Quad;
|
|
11
|
+
export declare const clampQuad: (quad: Quad, width: number, height: number) => Quad;
|
|
12
|
+
/** 把像素坐标旋转框渲染成白色多边形 SVG 遮罩,供 sharp composite 使用。 */
|
|
13
|
+
export declare const polygonSvg: (width: number, height: number, quads: readonly Quad[]) => Buffer;
|
package/dist/quad.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { AppError } from '@onco-foundry/errors';
|
|
3
|
+
/**
|
|
4
|
+
* 把模型返回的旋转框规范化成 0-1000 归一化坐标。
|
|
5
|
+
* 容忍模型偶尔返回 0-1 小数坐标;越界和零面积框拒绝(交付合同,不是内容判断)。
|
|
6
|
+
*/
|
|
7
|
+
export const normalizeQuad = (raw) => {
|
|
8
|
+
const points = z.array(z.tuple([z.number(), z.number()])).length(4).parse(raw);
|
|
9
|
+
const scale = Math.max(...points.flatMap((point) => point.map(Math.abs))) <= 1.5 ? 1000 : 1;
|
|
10
|
+
const normalized = points.map(([x, y]) => [x * scale, y * scale]);
|
|
11
|
+
if (normalized.some(([x, y]) => x < 0 || x > 1000 || y < 0 || y > 1000)) {
|
|
12
|
+
throw new AppError('Qwen 语义脱敏返回了越界坐标', 502);
|
|
13
|
+
}
|
|
14
|
+
const twiceArea = Math.abs(normalized.reduce((sum, [x, y], index) => {
|
|
15
|
+
const next = normalized[(index + 1) % normalized.length];
|
|
16
|
+
return sum + x * next[1] - next[0] * y;
|
|
17
|
+
}, 0));
|
|
18
|
+
if (twiceArea < 1) {
|
|
19
|
+
throw new AppError('Qwen 语义脱敏返回了零面积旋转框', 502);
|
|
20
|
+
}
|
|
21
|
+
return normalized;
|
|
22
|
+
};
|
|
23
|
+
export const normalizedToPixels = (quad, width, height) => quad.map(([x, y]) => [x / 1000 * width, y / 1000 * height]);
|
|
24
|
+
/** 以中心为原点外扩,给笔画外缘留安全边。 */
|
|
25
|
+
export const scaleQuad = (quad, factor) => {
|
|
26
|
+
const centerX = quad.reduce((sum, [x]) => sum + x, 0) / quad.length;
|
|
27
|
+
const centerY = quad.reduce((sum, [, y]) => sum + y, 0) / quad.length;
|
|
28
|
+
return quad.map(([x, y]) => [
|
|
29
|
+
centerX + (x - centerX) * factor,
|
|
30
|
+
centerY + (y - centerY) * factor,
|
|
31
|
+
]);
|
|
32
|
+
};
|
|
33
|
+
export const clampQuad = (quad, width, height) => quad.map(([x, y]) => [
|
|
34
|
+
Math.max(0, Math.min(width, x)),
|
|
35
|
+
Math.max(0, Math.min(height, y)),
|
|
36
|
+
]);
|
|
37
|
+
/** 把像素坐标旋转框渲染成白色多边形 SVG 遮罩,供 sharp composite 使用。 */
|
|
38
|
+
export const polygonSvg = (width, height, quads) => {
|
|
39
|
+
const polygons = quads.map((quad) => {
|
|
40
|
+
const points = quad.map(([x, y]) => `${x.toFixed(2)},${y.toFixed(2)}`).join(' ');
|
|
41
|
+
return `<polygon points="${points}" fill="#ffffff"/>`;
|
|
42
|
+
}).join('');
|
|
43
|
+
return Buffer.from(`<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">${polygons}</svg>`);
|
|
44
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type ModelClient } from 'agent-lattice';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { type Capability } from '@onco-foundry/capability-registry';
|
|
4
|
+
import { type Tracer } from '@onco-foundry/trace-port';
|
|
5
|
+
import { type Masker, type MaskResult } from './masker.ts';
|
|
6
|
+
export type QwenAgentMaskerOptions = {
|
|
7
|
+
readonly baseURL: string;
|
|
8
|
+
readonly apiKey: string;
|
|
9
|
+
readonly model: string;
|
|
10
|
+
/** 缺省 noop;传入后 loop 的模型调用与工具执行以原生事件流进追踪后端。 */
|
|
11
|
+
readonly tracer?: Tracer;
|
|
12
|
+
/** 测试注入缝:替换掉真实的模型客户端,不碰网络。 */
|
|
13
|
+
readonly modelClient?: ModelClient;
|
|
14
|
+
/** loop 轮次上限:资源边界,不是对模型的判断;耗尽仍未 submit 则整次失败。 */
|
|
15
|
+
readonly maxTurns?: number;
|
|
16
|
+
readonly maxTokens?: number;
|
|
17
|
+
readonly maxInputBytes?: number;
|
|
18
|
+
/** 仅用于离线回归指定旧 prompt;生产不传,按 current.json 加载。 */
|
|
19
|
+
readonly promptVersionOverride?: string;
|
|
20
|
+
};
|
|
21
|
+
/** 能力入参契约:V1 只接 patient_name(契约写死;直接调 mask() 时由内部目标校验兜底)。 */
|
|
22
|
+
export declare const qwenAgentNameMaskInputSchema: z.ZodObject<{
|
|
23
|
+
imageBytes: z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>;
|
|
24
|
+
targets: z.ZodArray<z.ZodLiteral<"patient_name">>;
|
|
25
|
+
rotationClockwiseDegrees: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<90>, z.ZodLiteral<180>, z.ZodLiteral<270>]>>;
|
|
26
|
+
}, z.core.$strip>;
|
|
27
|
+
export type QwenAgentNameMaskInput = z.infer<typeof qwenAgentNameMaskInputSchema>;
|
|
28
|
+
/**
|
|
29
|
+
* 能力名片:Qwen 脱敏智能体向能力注册表自报家门的最小元数据。
|
|
30
|
+
* kind 是 code——与 tencent/textin 同是 Masker 端口的平级实现,边界上就是一次脱敏调用;
|
|
31
|
+
* 内部跑 LLM loop 还是调专用 API 是后端选型,不该改变 kind(见 CapabilityKind 的定义)。
|
|
32
|
+
* version 不归名片管:asCapability 默认取 prompt 版本,场景可覆盖。
|
|
33
|
+
*/
|
|
34
|
+
export declare const qwenAgentNameMaskCapabilityCard: {
|
|
35
|
+
readonly name: 'qwen-agent-name-mask';
|
|
36
|
+
readonly kind: 'code';
|
|
37
|
+
readonly summary: '对医疗单据图片做病人姓名脱敏,返回脱敏图与占位符映射';
|
|
38
|
+
readonly input: z.ZodObject<{
|
|
39
|
+
imageBytes: z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>;
|
|
40
|
+
targets: z.ZodArray<z.ZodLiteral<"patient_name">>;
|
|
41
|
+
rotationClockwiseDegrees: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<90>, z.ZodLiteral<180>, z.ZodLiteral<270>]>>;
|
|
42
|
+
}, z.core.$strip>;
|
|
43
|
+
readonly output: z.ZodObject<{
|
|
44
|
+
maskedImageBytes: z.ZodCustom<Uint8Array<ArrayBufferLike>, Uint8Array<ArrayBufferLike>>;
|
|
45
|
+
mapping: z.ZodArray<z.ZodObject<{
|
|
46
|
+
placeholder: z.ZodString;
|
|
47
|
+
target: z.ZodEnum<{
|
|
48
|
+
address: "address";
|
|
49
|
+
doctor_signature: "doctor_signature";
|
|
50
|
+
id_number: "id_number";
|
|
51
|
+
patient_name: "patient_name";
|
|
52
|
+
phone: "phone";
|
|
53
|
+
}>;
|
|
54
|
+
originalText: z.ZodString;
|
|
55
|
+
}, z.core.$strip>>;
|
|
56
|
+
recognizedText: z.ZodOptional<z.ZodString>;
|
|
57
|
+
processorVersion: z.ZodOptional<z.ZodObject<{
|
|
58
|
+
engine: z.ZodString;
|
|
59
|
+
model: z.ZodOptional<z.ZodString>;
|
|
60
|
+
prompt: z.ZodOptional<z.ZodString>;
|
|
61
|
+
}, z.core.$strip>>;
|
|
62
|
+
}, z.core.$strip>;
|
|
63
|
+
};
|
|
64
|
+
export type QwenAgentMasker = Masker & {
|
|
65
|
+
/**
|
|
66
|
+
* 投射成注册表能力:名片、run、版本一次绑好,registry.register 直接收。
|
|
67
|
+
* 默认版本取本实例的 prompt 版本(行为随 prompt 变);场景要钉自己的版本标识时传 version 覆盖。
|
|
68
|
+
*/
|
|
69
|
+
asCapability(options?: {
|
|
70
|
+
readonly version?: string;
|
|
71
|
+
}): Capability<QwenAgentNameMaskInput, MaskResult>;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Qwen 脱敏智能体:模型在 loop 里闭环完成「报框 → 看遮盖结果 → 补框/确认」。
|
|
75
|
+
*
|
|
76
|
+
* 模型只负责看和报(report_quads 报旋转框、remove_quads 撤销报错的框、submit 确认无残留);遮盖动作永远由
|
|
77
|
+
* 代码执行,每次变动后把重绘的遮盖图作为 tool_result 图片喂回模型。
|
|
78
|
+
* 遮盖决策与最终裁判都归模型,代码不加内容判断;maxTurns 是资源边界,耗尽仍未
|
|
79
|
+
* submit 等于没有交付物,图片不放行(422)。原始模型响应不写磁盘、不进错误信息。
|
|
80
|
+
*/
|
|
81
|
+
export declare const createQwenAgentMasker: (options: QwenAgentMaskerOptions) => Promise<QwenAgentMasker>;
|