9 Commits

Author SHA1 Message Date
3827d09ad3 security: 讯飞配置输入框改用ref非受控设置,防止DOM value属性暴露 2026-04-20 02:28:08 +08:00
49886e5080 ui: AI撰写tab增加机器人头像图标 2026-04-20 02:24:34 +08:00
5f68f4b820 security: 讯飞配置输入框增加密码隐藏+防复制+防自动填充 2026-04-20 02:21:09 +08:00
c544483705 fix: AI修改确认弹窗左侧字体与右侧统一 2026-04-20 02:16:38 +08:00
9aec836e93 fix: 讯飞语音识别文字覆盖+麦克风未释放
- 使用setChatInput(prev => prev + seg)函数式更新,彻底修复文字覆盖问题
- 提取stopMicrophone函数,在手动停止/VAD自动挂断/报错/关闭时统一释放物理麦克风
2026-04-20 02:10:38 +08:00
75e4e56cb3 fix: 讯飞语音识别停止后内容未写入输入框
- 停止录音时不再立即关闭WebSocket,仅发送结束帧+关闭麦克风
- 移除dwa: wpgs动态修正,避免返回数据结构复杂导致拼接混乱
- ws.onmessage检测到服务端status:2时才彻底断开连接并写入最终文字
2026-04-20 01:54:50 +08:00
b07bcfaad2 fix: 讯飞鉴权签名与官方demo对齐+结束帧+文字修改
- ReportEditor: getXfAuthUrl signatureOrigin去掉引号,Base64改用CryptoJS.enc.Base64.stringify与demo完全一致
- ReportEditor: 第一帧增加dwa: wpgs动态修正参数
- ReportEditor: 停止录音时先发送status:2结束帧再关闭WebSocket
- SystemSettings: 标题改为讯飞语音听写Websocket接口配置
2026-04-20 01:43:58 +08:00
4f27edcc92 fix: 讯飞配置字段名统一+WebSocket错误码拦截
- SystemSettings: state初始化和表单绑定中xfIatConfig统一改为xfSpeechConfig,与编辑器读取字段一致
- ReportEditor: ws.onmessage增加jsonData.code错误码拦截,展示具体讯飞报错信息
2026-04-20 01:33:14 +08:00
d235ced187 fix: 讯飞鉴权HTTP兼容+AI段落紧凑+对话模式JSON降级
- ReportEditor: 讯飞鉴权改用crypto-js HMAC-SHA256,兼容HTTP非安全环境
- defaultContent.ts+ReportEditor: AI区域<p>标签去掉margin-bottom,段落紧密排布
- ReportEditor: 纯对话模式下JSON解析失败时降级为直接文本回复
- ReportEditor: 对话模式systemPrompt强化JSON格式约束
- deps: 新增crypto-js用于讯飞鉴权
2026-04-20 01:18:57 +08:00
5 changed files with 132 additions and 39 deletions

14
package-lock.json generated
View File

@@ -10,7 +10,9 @@
"dependencies": {
"@google/genai": "^1.29.0",
"@tailwindcss/vite": "^4.1.14",
"@types/crypto-js": "^4.2.2",
"@vitejs/plugin-react": "^5.0.4",
"crypto-js": "^4.2.0",
"diff": "^9.0.0",
"dotenv": "^17.2.3",
"express": "^4.21.2",
@@ -1492,6 +1494,12 @@
"@types/node": "*"
}
},
"node_modules/@types/crypto-js": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1904,6 +1912,12 @@
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",

View File

@@ -13,7 +13,9 @@
"dependencies": {
"@google/genai": "^1.29.0",
"@tailwindcss/vite": "^4.1.14",
"@types/crypto-js": "^4.2.2",
"@vitejs/plugin-react": "^5.0.4",
"crypto-js": "^4.2.0",
"diff": "^9.0.0",
"dotenv": "^17.2.3",
"express": "^4.21.2",

View File

@@ -13,6 +13,7 @@ import { defaultReportContent } from '../utils/defaultContent';
import { printDocument } from '../utils/print';
import { storage, getDefaultApiKey } from '../utils/storage';
import { diffChars } from 'diff';
import CryptoJS from 'crypto-js';
export default function ReportEditor() {
const navigate = useNavigate();
@@ -896,14 +897,11 @@ export default function ReportEditor() {
async function getXfAuthUrl(apiKey: string, apiSecret: string): Promise<string> {
const host = 'iat-api.xfyun.cn';
const date = new Date().toUTCString();
const signatureOrigin = `host: "${host}"\ndate: "${date}"\nGET /v2/iat HTTP/1.1`;
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey('raw', encoder.encode(apiSecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(signatureOrigin));
const signatureBase64 = btoa(String.fromCharCode(...new Uint8Array(signature)));
const authorizationOrigin = `api_key="${apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signatureBase64}"`;
const authorization = btoa(authorizationOrigin);
return `wss://iat-api.xfyun.cn/v2/iat?authorization=${encodeURIComponent(authorization)}&date=${encodeURIComponent(date)}&host=${encodeURIComponent(host)}`;
const signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/iat HTTP/1.1`;
const signature = CryptoJS.enc.Base64.stringify(CryptoJS.HmacSHA256(signatureOrigin, apiSecret));
const authorizationOrigin = `api_key="${apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`;
const authorization = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(authorizationOrigin));
return `wss://iat-api.xfyun.cn/v2/iat?authorization=${authorization}&date=${date}&host=${host}`;
}
function floatTo16BitPCM(input: Float32Array): ArrayBuffer {
@@ -925,24 +923,43 @@ export default function ReportEditor() {
}
const toggleListening = async () => {
// 专门提取一个彻底关闭物理麦克风的函数
const stopMicrophone = () => {
if (xfAudioContextRef.current) {
try { xfAudioContextRef.current.close(); } catch {}
xfAudioContextRef.current = null;
}
if (xfMediaStreamRef.current) {
xfMediaStreamRef.current.getTracks().forEach(t => t.stop());
xfMediaStreamRef.current = null;
}
};
if (isListening) {
setIsListening(false);
if (xfWsRef.current) { try { xfWsRef.current.close(); } catch {} xfWsRef.current = null; }
if (xfAudioContextRef.current) { try { xfAudioContextRef.current.close(); } catch {} xfAudioContextRef.current = null; }
if (xfMediaStreamRef.current) { xfMediaStreamRef.current.getTracks().forEach(t => t.stop()); xfMediaStreamRef.current = null; }
stopMicrophone();
if (xfWsRef.current && xfWsRef.current.readyState === WebSocket.OPEN) {
try {
const endFrame = { data: { status: 2, format: 'audio/L16;rate=16000', encoding: 'raw', audio: '' } };
xfWsRef.current.send(JSON.stringify(endFrame));
} catch {}
}
return;
}
const xfConfig = storage.get<SystemSettings>('systemSettings', {} as SystemSettings).xfSpeechConfig || DEFAULT_XF_SPEECH;
if (!xfConfig?.appId || !xfConfig?.apiKey || !xfConfig?.apiSecret) {
alert('请先在系统设置中配置讯飞语音 APPID/APIKey/APISecret');
return;
}
try {
const authUrl = await getXfAuthUrl(xfConfig.apiKey, xfConfig.apiSecret);
const ws = new WebSocket(authUrl);
xfWsRef.current = ws;
let frameStatus = 0;
let transcript = chatInput;
ws.onopen = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
@@ -951,16 +968,21 @@ export default function ReportEditor() {
xfAudioContextRef.current = audioContext;
const source = audioContext.createMediaStreamSource(stream);
const processor = audioContext.createScriptProcessor(4096, 1, 1);
processor.onaudioprocess = (e) => {
if (ws.readyState !== WebSocket.OPEN) return;
if (ws.readyState !== WebSocket.OPEN || !xfAudioContextRef.current) return;
const inputData = e.inputBuffer.getChannelData(0);
const pcmBuffer = floatTo16BitPCM(inputData);
const base64Audio = arrayBufferToBase64(pcmBuffer);
const frame: any = { data: { status: frameStatus, format: 'audio/L16;rate=16000', encoding: 'raw', audio: base64Audio } };
if (frameStatus === 0) { frame.common = { app_id: xfConfig.appId }; frame.business = { language: 'zh_cn', domain: 'iat', accent: 'mandarin' }; }
if (frameStatus === 0) {
frame.common = { app_id: xfConfig.appId };
frame.business = { language: 'zh_cn', domain: 'iat', accent: 'mandarin' };
}
ws.send(JSON.stringify(frame));
frameStatus = 1;
};
source.connect(processor);
processor.connect(audioContext.destination);
setIsListening(true);
@@ -970,19 +992,35 @@ export default function ReportEditor() {
ws.close();
}
};
ws.onmessage = (event) => {
try {
const jsonData = JSON.parse(event.data);
if (jsonData.code !== 0 && jsonData.code !== undefined) {
alert(`讯飞语音报错: ${jsonData.message || '未知错误'} (错误码: ${jsonData.code})`);
setIsListening(false);
stopMicrophone();
ws.close();
return;
}
if (jsonData.data?.result?.ws) {
let seg = '';
for (const w of jsonData.data.result.ws) { if (w.cw?.[0]?.w) seg += w.cw[0].w; }
if (jsonData.data.result.ls) { transcript += seg; setChatInput(transcript); }
else { setChatInput(transcript + seg); }
if (seg) {
setChatInput(prev => prev + seg);
}
}
if (jsonData.data?.status === 2) {
ws.close();
xfWsRef.current = null;
setIsListening(false);
stopMicrophone();
}
} catch {}
};
ws.onerror = () => { alert('讯飞语音连接失败'); setIsListening(false); };
ws.onclose = () => { setIsListening(false); };
ws.onerror = () => { alert('讯飞语音连接失败'); setIsListening(false); stopMicrophone(); };
ws.onclose = () => { setIsListening(false); stopMicrophone(); };
} catch (e: any) {
alert('讯飞语音初始化失败: ' + e.message);
}
@@ -1054,7 +1092,7 @@ export default function ReportEditor() {
}
const systemPrompt = aiModifyEnabled
? '你是一名专业的外科医生助理。当前处于【修改模式】。\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 标记';
: '你是一名专业的外科医生助理。当前处于【对话模式】。\n请仔细阅读我提供的【全局手术报告参考内容】并根据【医生指令】进行专业解答。\n重要指令\n1. 必须返回合法的 JSON 对象\n2. 仅包含 "reply"(你的专业回答)一个字段\n3. 不要返回任何 HTML 代码\n4. 绝对不要包含任何 Markdown 标记\n5. 无论图片内容是什么,请严格以纯 JSON 格式输出,禁止任何多余的开头解释';
const payload: any = {
model: modelName,
messages: [
@@ -1106,8 +1144,17 @@ export default function ReportEditor() {
responseJson = JSON.parse(cleanedText);
} catch {
const jsonMatch = cleanedText.match(/\{[\s\S]*\}/);
if (jsonMatch) responseJson = JSON.parse(jsonMatch[0]);
else throw new Error('AI 返回格式异常,无法解析 JSON');
if (jsonMatch) {
try {
responseJson = JSON.parse(jsonMatch[0]);
} catch {
if (!aiModifyEnabled) responseJson = { reply: cleanedText };
else throw new Error('AI 返回格式异常,无法解析 JSON');
}
} else {
if (!aiModifyEnabled) responseJson = { reply: cleanedText };
else throw new Error('AI 返回格式异常,无法解析 JSON');
}
}
if (responseJson.reply) {
setChatMessages(prev => [...prev, { id: Date.now().toString(), role: 'model', content: responseJson.reply }]);
@@ -1121,7 +1168,7 @@ export default function ReportEditor() {
cleanHtml = cleanHtml.replace(/<\/p>\s*<p>/gi, '</p><p>');
cleanHtml = cleanHtml.trim();
cleanHtml = cleanHtml.replace(/>(\s+)</g, '><');
cleanHtml = cleanHtml.replace(/<p>/gi, '<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0 0 8px 0; padding: 0;">');
cleanHtml = cleanHtml.replace(/<p>/gi, '<p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">');
if (targetRegionEl) {
setDiffModal({
isOpen: true,
@@ -1924,10 +1971,11 @@ export default function ReportEditor() {
<button
key={tab}
onClick={() => { setActiveTab(tab); stateRef.current = { ...stateRef.current, activeTab: tab }; saveDraftToStorage(); }}
className={`flex-1 py-4 text-xs font-bold transition-all border-b-2 uppercase tracking-wider ${
className={`flex-1 py-4 text-xs font-bold transition-all border-b-2 uppercase tracking-wider flex items-center justify-center gap-1.5 ${
activeTab === tab ? 'text-accent border-accent' : 'text-text-muted border-transparent hover:text-text-main'
}`}
>
{tab === 'ai' && <Bot size={16} className={activeTab === 'ai' ? 'text-accent' : 'text-text-muted'} />}
{tab === 'info' ? '基本信息' : tab === 'video' ? '视频分析' : 'AI撰写'}
</button>
))}
@@ -2840,7 +2888,9 @@ export default function ReportEditor() {
<div className="flex-1 flex flex-col bg-white border border-red-200 rounded-xl overflow-hidden shadow-sm">
<div className="bg-red-50 px-3 py-2 text-xs font-bold text-red-600 border-b border-red-100 uppercase tracking-wider"></div>
<div className="p-4 flex-1 overflow-y-auto opacity-70 cursor-not-allowed custom-scrollbar"
dangerouslySetInnerHTML={{ __html: leftDiffHtml }}></div>
dangerouslySetInnerHTML={{ __html: leftDiffHtml }}
style={{ fontFamily: 'SimSun, "Microsoft YaHei", serif', fontSize: '12pt', lineHeight: '1.5' }}
></div>
</div>
<div className="flex-1 flex flex-col bg-white border border-green-400 rounded-xl overflow-hidden shadow-md relative">
<div className="bg-green-50 px-3 py-2 text-xs font-bold text-green-700 border-b border-green-200 uppercase tracking-wider flex justify-between">

View File

@@ -15,7 +15,7 @@ export default function SystemSettings() {
frameMode: 'keep',
activeAiProvider: 'kimi',
aiProviders: { ...DEFAULT_AI_PROVIDERS },
xfIatConfig: { appId: 'e0fe23e3', apiKey: '7fd08be316718c2280e85af4fe126306', apiSecret: 'ZGI5MjAzZDA0YzYwNDhjMWZiNTM2NDE0' }
xfSpeechConfig: { appId: 'e0fe23e3', apiKey: '7fd08be316718c2280e85af4fe126306', apiSecret: 'ZGI5MjAzZDA0YzYwNDhjMWZiNTM2NDE0' }
});
const [templates, setTemplates] = useState<Template[]>([]);
const [isSaved, setIsSaved] = useState(false);
@@ -23,6 +23,9 @@ export default function SystemSettings() {
const [modeModalOpen, setModeModalOpen] = useState(false);
const [availableModels, setAvailableModels] = useState<string[]>([]);
const apiKeyInputRef = useRef<HTMLInputElement>(null);
const xfAppIdRef = useRef<HTMLInputElement>(null);
const xfApiKeyRef = useRef<HTMLInputElement>(null);
const xfApiSecretRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (apiKeyInputRef.current) {
@@ -33,6 +36,21 @@ export default function SystemSettings() {
}
}, [settings.aiProviders[settings.activeAiProvider]?.apiKey]);
useEffect(() => {
if (xfAppIdRef.current) {
const target = settings.xfSpeechConfig?.appId || '';
if (xfAppIdRef.current.value !== target) xfAppIdRef.current.value = target;
}
if (xfApiKeyRef.current) {
const target = settings.xfSpeechConfig?.apiKey || '';
if (xfApiKeyRef.current.value !== target) xfApiKeyRef.current.value = target;
}
if (xfApiSecretRef.current) {
const target = settings.xfSpeechConfig?.apiSecret || '';
if (xfApiSecretRef.current.value !== target) xfApiSecretRef.current.value = target;
}
}, [settings.xfSpeechConfig]);
useEffect(() => {
const user = storage.get<User | null>('currentUser', null);
if (!user) {
@@ -429,46 +447,55 @@ export default function SystemSettings() {
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-bold text-text-main flex items-center gap-2">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-accent"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>
Websocket接口配
</h3>
</div>
<div className="space-y-6">
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider">APPID</label>
<input
ref={xfAppIdRef}
type="password"
value={settings.xfIatConfig?.appId || ''}
onChange={(e) => {
const next = { ...(settings.xfIatConfig || { appId: '', apiKey: '', apiSecret: '' }), appId: e.target.value };
setSettings({ ...settings, xfIatConfig: next });
const next = { ...(settings.xfSpeechConfig || { appId: '', apiKey: '', apiSecret: '' }), appId: e.target.value };
setSettings({ ...settings, xfSpeechConfig: next });
}}
placeholder="e0fe23e3"
onCopy={(e) => e.preventDefault()}
onCut={(e) => e.preventDefault()}
autoComplete="new-password"
placeholder="********"
className="input-minimal"
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider">APIKey</label>
<input
ref={xfApiKeyRef}
type="password"
value={settings.xfIatConfig?.apiKey || ''}
onChange={(e) => {
const next = { ...(settings.xfIatConfig || { appId: '', apiKey: '', apiSecret: '' }), apiKey: e.target.value };
setSettings({ ...settings, xfIatConfig: next });
const next = { ...(settings.xfSpeechConfig || { appId: '', apiKey: '', apiSecret: '' }), apiKey: e.target.value };
setSettings({ ...settings, xfSpeechConfig: next });
}}
placeholder="7fd08be316718c2280e85af4fe126306"
onCopy={(e) => e.preventDefault()}
onCut={(e) => e.preventDefault()}
autoComplete="new-password"
placeholder="********************************"
className="input-minimal"
/>
</div>
<div className="space-y-1.5">
<label className="block text-xs font-bold text-text-main uppercase tracking-wider">APISecret</label>
<input
ref={xfApiSecretRef}
type="password"
value={settings.xfIatConfig?.apiSecret || ''}
onChange={(e) => {
const next = { ...(settings.xfIatConfig || { appId: '', apiKey: '', apiSecret: '' }), apiSecret: e.target.value };
setSettings({ ...settings, xfIatConfig: next });
const next = { ...(settings.xfSpeechConfig || { appId: '', apiKey: '', apiSecret: '' }), apiSecret: e.target.value };
setSettings({ ...settings, xfSpeechConfig: next });
}}
placeholder="ZGI5MjAzZDA0YzYwNDhjMWZiNTM2NDE0"
onCopy={(e) => e.preventDefault()}
onCut={(e) => e.preventDefault()}
autoComplete="new-password"
placeholder="********************************"
className="input-minimal"
/>
</div>

View File

@@ -57,7 +57,7 @@ export const defaultReportContent = `
<div class="ai-region" data-ai-id="手术步骤" data-ai-title="手术步骤、术中出现的情况及处理" style="border: 1px dashed #3b82f6; padding: 16px 12px 12px; margin: 8px 0; position: relative; min-height: 60px; background: #f8fafc; border-radius: 6px;">
<div contenteditable="false" style="position: absolute; top: -10px; right: 10px; background: #3b82f6; color: white; font-size: 10px; padding: 2px 8px; border-radius: 12px; z-index: 10; user-select: none;">手术步骤、术中出现的情况及处理-AI可编辑区域</div>
<div class="ai-content" style="min-height: 20px;"><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0 0 8px 0; padding: 0;">1患者仰卧位麻醉成功后常规消毒术野、铺无菌巾于脐下穿刺建立CO2气腹气腹压力为12mmHg进镜探查无穿刺损伤分别于剑突下2.0cm、右锁中线肋缘下2.0cm各点穿刺置穿刺器,插入相应手术器械。</p><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0 0 8px 0; padding: 0;">2腹腔镜探查腹腔内无腹水形成无明显粘连肝脏色红质软无明显结节硬化改变胆囊大小约 cm× cm× cm壁轻度水肿张力可胆囊三角解剖关系清楚胆囊管及胆总管无明显扩张。胃、十二指肠、小肠、结肠、脾脏及盆腔未见明显异常。术中诊断胆囊结石伴慢性胆囊炎。遂行腹腔镜胆囊切除术。</p><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0 0 8px 0; padding: 0;">3.切除胆囊钳夹胆囊颈部并解剖胆囊三角游离出胆囊动脉及胆囊管明确胆囊与胆总管的关系距胆总管0.3cm处近端以一枚可吸收夹,远端夹一枚钛夹夹闭胆囊管,两夹间以剪刀剪断胆囊管,另用一枚可吸收夹夹闭胆囊动脉后离断。顺行游离胆囊浆膜,完整切除胆囊后装入标本袋取出。胆囊床严密止血并覆盖止血材料。</p><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0 0 8px 0; padding: 0;">4.检查腹腔内无活动性出血及漏胆后,清点器械纱布无误,拔除腔镜器械,排出腹腔残余气体,缝合各刺孔,术毕。</p><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0 0 8px 0; padding: 0;">5.手术顺利,麻醉满意。切除的标本经家属过目后送病理。术中出血约 ml术中输血成分输血量是否有输血不良反应。</p></div>
<div class="ai-content" style="min-height: 20px;"><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">1患者仰卧位麻醉成功后常规消毒术野、铺无菌巾于脐下穿刺建立CO2气腹气腹压力为12mmHg进镜探查无穿刺损伤分别于剑突下2.0cm、右锁中线肋缘下2.0cm各点穿刺置穿刺器,插入相应手术器械。</p><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">2腹腔镜探查腹腔内无腹水形成无明显粘连肝脏色红质软无明显结节硬化改变胆囊大小约 cm× cm× cm壁轻度水肿张力可胆囊三角解剖关系清楚胆囊管及胆总管无明显扩张。胃、十二指肠、小肠、结肠、脾脏及盆腔未见明显异常。术中诊断胆囊结石伴慢性胆囊炎。遂行腹腔镜胆囊切除术。</p><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">3.切除胆囊钳夹胆囊颈部并解剖胆囊三角游离出胆囊动脉及胆囊管明确胆囊与胆总管的关系距胆总管0.3cm处近端以一枚可吸收夹,远端夹一枚钛夹夹闭胆囊管,两夹间以剪刀剪断胆囊管,另用一枚可吸收夹夹闭胆囊动脉后离断。顺行游离胆囊浆膜,完整切除胆囊后装入标本袋取出。胆囊床严密止血并覆盖止血材料。</p><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">4.检查腹腔内无活动性出血及漏胆后,清点器械纱布无误,拔除腔镜器械,排出腹腔残余气体,缝合各刺孔,术毕。</p><p style="font-family: SimSun; font-size: 12pt; line-height: 1.5; margin: 0; padding: 0;">5.手术顺利,麻醉满意。切除的标本经家属过目后送病理。术中出血约 ml术中输血成分输血量是否有输血不良反应。</p></div>
</div>
<!-- 手术图片说明表格 -->