Revert "Prioritize doctor instructions in AI report prompts"

This reverts commit 55622368e3.
This commit is contained in:
2026-05-02 04:44:17 +08:00
parent 55622368e3
commit 558498a4bb
9 changed files with 8 additions and 129 deletions

View File

@@ -351,7 +351,6 @@ PostgreSQL 数据模型。当前覆盖 `Tenant`、`Department`、`User`、`UserS
- 后端模板 DTO 和权限资源映射
- 模板列表合并工具,防止新增模板被旧 `localStorage.templates` 覆盖
- 模板导入导出工具,覆盖 HTML 模板包生成、字段库元数据回导和旧 JSON 导入兼容
- AI Prompt 生成工具,确保医生指令和结构化报告字段优先于默认模板旧内容
- 后端用户 DTO 和部门模板授权映射
- 后端系统设置 schema 校验
- 后端 AI 入参和讯飞语音代理帧处理

View File

@@ -201,7 +201,6 @@ cp .env.example .env.local
- 本地存储封装和系统设置兼容。
- 默认报告模板结构和字段配置。
- 模板 HTML 导出包字段库元数据。
- AI Prompt 优先级,避免默认模板旧术式覆盖医生新指令。
- 打印导出入口。
- 后端权限策略、AI 入参和语音代理帧处理。

View File

@@ -90,7 +90,7 @@ src/
- `data-mode="frame"` 或无 `manual` 标记的占位符可接收关键帧。
- `data-mode="manual"` 用于 Logo、签名等静态图片不接收视频关键帧拖入。
AI 可编辑区域通过 `.ai-region[data-ai-id]` 和内部 `.ai-content` 定位。AI 返回 `updatedHtml` 后,系统先显示差异确认,再注入目标区域。AI prompt 会把医生指令和结构化报告字段放在最高优先级,旧目标区域 HTML 和整份报告正文只作为低优先级参考,避免默认模板中的旧术式描述持续影响新生成内容。
AI 可编辑区域通过 `.ai-region[data-ai-id]` 和内部 `.ai-content` 定位。AI 返回 `updatedHtml` 后,系统先显示差异确认,再注入目标区域。
## 打印导出设计

View File

@@ -71,16 +71,6 @@ AI 面板支持两种模式:
- 对话模式:根据当前报告内容和图片上下文回答问题。
- 修改模式:选中 `.ai-region` 后要求模型返回 JSON其中包含 `reply``updatedHtml`
AI 提议版本由以下内容生成:
- 医生在 AI 输入框中的指令。
- 当前结构化报告字段,包括手术名称、术前诊断、术中/术后诊断、麻醉方式和报告备注。
- 整份报告正文文本,作为低优先级上下文。
- 当前选中的 AI 可编辑区域 HTML作为低优先级格式和旧内容参考。
- 用户选择的报告图片或上传给 AI 的图片。
Prompt 明确规定优先级为:医生指令 > 当前结构化报告字段 > 图片内容 > 旧目标区域/整份报告正文。默认模板中的胆囊、肝脏等旧术式描述不得覆盖医生新指令如果医生要求生成新的术式或不同部位AI 应围绕新术式重写目标区域。
编辑器会监听正文中的 `.ai-region` 变化;用户插入新的 AI 可编辑区域后AI 撰写面板的目标下拉栏会立即更新,不需要离开页面或刷新。
模型返回 HTML 后,系统会清理换行和 `<br>`,生成差异预览。用户确认后才写入目标 `.ai-content`

View File

@@ -89,4 +89,3 @@
| 2026-05-02 | 修复报告编辑器新增 AI 可编辑区域后 AI 撰写下拉栏不立即更新的问题,并补充 E2E。 |
| 2026-05-02 | 移除前端用户可见 JSON 导出入口,保留模板历史 JSON 导入兼容。 |
| 2026-05-02 | 模板 HTML 导出包补充模板字段和字段管理设置,导入时恢复字段库元数据。 |
| 2026-05-02 | 调整报告编辑器 AI Prompt 优先级,医生指令和结构化报告字段优先于默认模板旧术式内容。 |

View File

@@ -82,7 +82,6 @@ AI 第三方接口、讯飞语音上游 WebSocket、麦克风权限和真实视
| 系统设置混淆兼容 | 已覆盖 | `storage.test.ts` |
| 默认报告模板结构 | 已覆盖 | `defaultContent.test.ts` |
| 默认字段和 AI Provider | 已覆盖 | `defaultContent.test.ts` |
| AI Prompt 优先级 | 已覆盖 | `aiPrompt.test.ts`,覆盖医生指令和结构化字段优先于旧模板内容。 |
| 打印导出入口 | 已覆盖 | `print.test.ts` |
| 模板列表合并工具 | 已覆盖 | `templateList.test.ts`,防止新增模板被旧本地缓存覆盖。 |
| 模板导入导出工具 | 已覆盖 | `templateExport.test.ts`,覆盖 HTML 模板包、字段库元数据回导、旧 JSON 导入兼容和文件名清理。 |

View File

@@ -22,7 +22,6 @@ import { getSpeechIatWebSocketUrl } from '../api/speech';
import { getFieldLibrary, updateFieldLibrary } from '../api/library';
import { listFiles, uploadFileResource } from '../api/files';
import { isLocalFallbackEnabled } from '../config/runtime';
import { buildSurgicalAiPrompt } from '../utils/aiPrompt';
import { diffChars } from 'diff';
type AudioWindow = Window & typeof globalThis & {
@@ -1285,20 +1284,14 @@ export default function ReportEditor() {
}
const currentHtml = targetRegionEl ? targetRegionEl.innerHTML.replace(/&#8203;/g, '').replace(/>(\s+)</g, '><').trim() : '';
const globalContextText = editorRef.current?.innerText || '';
const targetTitle = aiRegions.find(region => region.id === actualTargetId)?.title
|| (aiRegion as HTMLElement | null)?.dataset.aiTitle
|| actualTargetId;
let messageContent: any;
const selectedEditorImageUrls = editorImages.filter(img => aiSelectedEditorImages.includes(img.id)).map(img => img.src);
const allImages = [...selectedEditorImageUrls, ...aiUploadedImages.map(i => i.dataUrl)];
const promptText = buildSurgicalAiPrompt({
reportData,
globalContextText,
currentHtml,
doctorInstruction: text,
modifyEnabled: aiModifyEnabled && Boolean(targetRegionEl),
targetTitle,
});
let promptText = `【全局手术报告参考内容】:\n${globalContextText}\n\n`;
if (aiModifyEnabled && targetRegionEl) {
promptText += `【你需要进行修改的目标区域 HTML 源码】:\n${currentHtml || '(当前区域为空)'}\n\n`;
}
promptText += `【医生指令】: ${text}\n\n【格式要求】:\n1. 仅针对【目标区域】的主题生成对应的多段落 HTML 内容\n2. ⚠️ 绝对禁止将【全局参考内容】中的其他模块(如:基本信息、术后情况、标本描述、病理结果、医生签名、日期等)生成并混入你的输出中!你只能输出该区域应有的内容\n3. 段落使用 <p> 标签,段落之间不要使用 <br> 标签或换行符\n4. 输出紧凑 HTML标签间不要有空格或换行`;
if (allImages.length > 0) {
messageContent = [];
allImages.forEach(url => {
@@ -1309,8 +1302,8 @@ export default function ReportEditor() {
messageContent = promptText;
}
const systemPrompt = aiModifyEnabled
? '你是一名专业的外科医生助理。当前处于【修改模式】。\n请根据用户的【医生指令】直接重写目标区域 HTML。优先级必须是:医生指令 > 当前结构化报告字段 > 图片内容 > 旧目标区域/整份报告参考文本。\n\n重要指令\n1. 必须返回合法的 JSON 对象\n2. 必须包含 "reply"(简短回复)和 "updatedHtml"(修改后的完整 HTML 代码)两个字段\n3. 如果医生要求生成新的术式、其他手术或不同部位,必须抛开旧目标区域中的默认模板内容,不要继续写胆囊、肝脏等旧术式描述\n4. 【内容边界】全局参考内容仅供理解上下文。updatedHtml 只能包含目标区域本身的内容(例如:如果目标区域是"手术步骤",你就只写步骤)。严禁输出签名、落款、术后总结等属于报告其他部分的结构!\n5. 段落必须使用 <p> 标签包裹,段落间绝对不要使用 <br> 标签,也不要使用换行符 (\\n)\n6. 输出的 HTML 必须紧凑,标签之间不要有空格或换行\n7. 绝对不要包含任何 Markdown 标记(如 ```json'
: '你是一名专业的外科医生助理。当前处于【对话模式】。\n请根据【医生指令】进行专业解答。优先级必须是:医生指令 > 当前结构化报告字段 > 图片内容 > 旧正文参考文本;如果医生要求新的术式或不同部位,不要被旧正文中的胆囊、肝脏等默认模板内容带偏。\n重要指令\n1. 必须返回合法的 JSON 对象\n2. 仅包含 "reply"(你的专业回答)一个字段\n3. 不要返回任何 HTML 代码\n4. 绝对不要包含任何 Markdown 标记\n5. 无论图片内容是什么,请严格以纯 JSON 格式输出,禁止任何多余的开头解释';
? '你是一名专业的外科医生助理。当前处于【修改模式】。\n我为你提供了当前手术报告的【全局参考内容】作为背景知识,以及你需要修改的【目标区域 HTML 源码】。\n请根据用户的【医生指令】直接重写并输出目标区域 HTML。\n\n重要指令\n1. 必须返回合法的 JSON 对象\n2. 必须包含 "reply"(简短回复)和 "updatedHtml"(修改后的完整 HTML 代码)两个字段\n3. 【内容边界】:全局参考内容仅供理解上下文。你的 updatedHtml 只能包含目标区域本身的内容(例如:如果目标区域是"手术步骤",你就只写步骤)。严禁输出签名、落款、术后总结等属于报告其他部分的结构!\n4. 段落必须使用 <p> 标签包裹,段落间绝对不要使用 <br> 标签,也不要使用换行符 (\\n)\n5. 输出的 HTML 必须紧凑,标签之间不要有空格或换行\n6. 绝对不要包含任何 Markdown 标记(如 ```json'
: '你是一名专业的外科医生助理。当前处于【对话模式】。\n请仔细阅读我提供的【全局手术报告参考内容】,并根据【医生指令】进行专业解答。\n重要指令\n1. 必须返回合法的 JSON 对象\n2. 仅包含 "reply"(你的专业回答)一个字段\n3. 不要返回任何 HTML 代码\n4. 绝对不要包含任何 Markdown 标记\n5. 无论图片内容是什么,请严格以纯 JSON 格式输出,禁止任何多余的开头解释';
const payload: any = {
model: modelName,
messages: [

View File

@@ -1,37 +0,0 @@
import { describe, expect, it } from 'vitest';
import { buildSurgicalAiPrompt } from './aiPrompt';
describe('buildSurgicalAiPrompt', () => {
it('makes doctor instructions and structured fields higher priority than old template content', () => {
const prompt = buildSurgicalAiPrompt({
reportData: {
title: '腹腔镜阑尾切除术报告',
preoperativeDiagnosis: '急性阑尾炎',
postoperativeDiagnosis: '急性化脓性阑尾炎',
},
globalContextText: '默认模板旧内容:肝脏色红质软,遂行腹腔镜胆囊切除术。',
currentHtml: '<p>腹腔镜探查肝脏,切除胆囊。</p>',
doctorInstruction: '生成阑尾切除术的手术步骤',
modifyEnabled: true,
targetTitle: '手术步骤',
});
expect(prompt.indexOf('医生指令(最高优先级)')).toBeLessThan(prompt.indexOf('旧目标区域 HTML 源码'));
expect(prompt).toContain('手术名称: 腹腔镜阑尾切除术报告');
expect(prompt).toContain('如果医生指令要求生成新的术式');
expect(prompt).toContain('不要沿用旧目标区域中的胆囊、肝脏或其他默认模板内容');
});
it('truncates long global context to reduce old-content anchoring', () => {
const prompt = buildSurgicalAiPrompt({
reportData: {},
globalContextText: '旧'.repeat(5000),
currentHtml: '',
doctorInstruction: '生成新的手术步骤',
modifyEnabled: false,
});
expect(prompt.length).toBeLessThan(4700);
expect(prompt).toContain('...');
});
});

View File

@@ -1,63 +0,0 @@
import { Report } from '../types';
const cleanText = (value: unknown) => {
if (Array.isArray(value)) return value.filter(Boolean).join('、');
return String(value ?? '').replace(/\s+/g, ' ').trim();
};
const truncateText = (value: string, maxLength = 4000) =>
value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
export interface SurgicalAiPromptInput {
reportData: Partial<Report> & {
preoperativeDiagnosis?: unknown;
postoperativeDiagnosis?: unknown;
};
globalContextText: string;
currentHtml: string;
doctorInstruction: string;
modifyEnabled: boolean;
targetTitle?: string;
}
export const buildSurgicalAiPrompt = ({
reportData,
globalContextText,
currentHtml,
doctorInstruction,
modifyEnabled,
targetTitle,
}: SurgicalAiPromptInput) => {
const structuredFields = [
['手术名称', reportData.title],
['术前诊断', reportData.preoperativeDiagnosis],
['术中/术后诊断', reportData.postoperativeDiagnosis],
['麻醉方式', reportData.anesthesiaType],
['报告备注', reportData.reportNote],
]
.map(([label, value]) => {
const cleaned = cleanText(value);
return cleaned ? `${label}: ${cleaned}` : '';
})
.filter(Boolean)
.join('\n') || '暂无结构化字段';
let promptText = `【医生指令(最高优先级)】:\n${doctorInstruction}\n\n`;
promptText += `【当前结构化报告字段(高优先级,若与旧正文冲突,以这里和医生指令为准)】:\n${structuredFields}\n\n`;
promptText += `【整份报告参考文本(低优先级,仅用于患者信息、术式背景和上下文;若与医生指令或结构化字段冲突,必须忽略这里的旧术式描述)】:\n${truncateText(globalContextText)}\n\n`;
if (modifyEnabled) {
promptText += `【目标区域】:\n${targetTitle || '未命名区域'}\n\n`;
promptText += `【旧目标区域 HTML 源码(只作为格式、段落风格和可复用事实参考;不得因为旧内容存在胆囊、肝脏等描述就继续沿用,除非医生指令或结构化字段明确要求)】:\n${currentHtml || '(当前区域为空)'}\n\n`;
}
promptText += `【格式要求】:
1. 优先执行【医生指令】,其次参考【当前结构化报告字段】,旧目标区域和整份报告文本优先级最低
2. 如果医生指令要求生成新的术式、其他手术或不同部位,必须围绕新术式重写,不要沿用旧目标区域中的胆囊、肝脏或其他默认模板内容
3. 仅针对【目标区域】的主题生成对应的多段落 HTML 内容
4. 绝对禁止将【整份报告参考文本】中的其他模块(如:基本信息、术后情况、标本描述、病理结果、医生签名、日期等)生成并混入输出
5. 段落使用 <p> 标签,段落之间不要使用 <br> 标签或换行符
6. 输出紧凑 HTML标签间不要有空格或换行`;
return promptText;
};