PageSourceSearch

https://yyqyt.com/app.js?v=20260817-seed-quarantine-v29

js yyqyt.com collected 2026-10-04 02:40:34 UTC 469,098 bytes, 6,820 lines download raw bytes

1const state = {
2  aiMode: 'demo',
3  aiServiceOnline: false,
4  aiSession: null,
5  projectContext: null,
6  analysis: null,
7  answers: {},
8  questionIndex: 0,
9  interviewComplete: false,
10  answerPending: false,
11  patent: { scanned: false, confirmed: false, outputMode: 'unavailable' },
12  flashpoc: { projectId: null, generated: false, selected: null, confirmed: false, outputMode: 'demo' },
13  development: { created: false, taskId: null, phase: 0, activities: [], evidenceMode: 'unverified' },
14  regai: { validated: false, documentsGenerated: false, outputMode: 'unavailable' },
15  commercial: { algorithm: null, kolsMatched: false, completed: false, outputMode: 'planning' },
16  intakeTrack: '尚不确定',
17  intakeRecommendedTrack: null,
18  currentView: 'incubator'
19};
20
21const scenarioLibrary = [
22  {
23    match: ['骨科', '钻', '穿透', '皮质骨'],
24    summary: '骨科动力工具在钻孔穿透皮质骨的瞬间缺少可靠反馈,需要降低后方软组织误伤风险。',
25    tags: ['骨科手术', '动力工具', '穿透感知', '安全预警'],
26    mappings: [
27      ['“无法判断穿透”', '轴向阻力变化检测', '采样率 ≥ 1kHz'],
28      ['“伤到软组织”', '穿透后主动制动', '响应 ≤ 20ms'],
29      ['“即时反馈”', '多模态反馈模块', '触觉 + 声光'],
30      ['术中手持使用', '低惯量握持设计', '整机 ≤ 1.2kg']
31    ],
32    constraints: [['术式', '骨科钻孔'], ['无菌', '可灭菌附件'], ['环境', '液体飞溅'], ['安全等级', '主动防护']],
33    tests: [['⚡', '穿透识别延迟测试'], ['◎', '不同骨密度识别准确率'], ['盾', '误制动与失效安全测试']],
34    questions: [
35      { q: '这个穿透风险主要发生在哪类骨科手术?', why: '不同术式的目标骨厚度、后方高风险组织与可接受制动距离差异很大,这会直接决定检测模型和安全余量。', options: ['脊柱手术', '创伤固定', '关节手术', '其他术式'], insight: { '脊柱手术': '脊柱场景邻近神经与硬膜,系统应优先追求极低漏检率,并采用更保守的制动阈值。', '创伤固定': '创伤骨质差异大,需要覆盖骨质疏松到高密度皮质骨的宽动态检测范围。', '关节手术': '关节场景更强调空间限制与重复钻孔一致性,需要结合深度控制。' } },
36      { q: '现有操作中,医生通常依靠什么判断“刚刚穿透”?', why: '需要明确现有感知通道,才能判断新方案应增强医生判断,还是接管高风险动作。', options: ['手感突变', '声音变化', '术中影像', '助手提醒'], insight: { '手感突变': '手感属于高经验依赖信号,适合用轴向力和扭矩变化进行客观量化。', '声音变化': '声学信号可作为辅助,但容易受手术室背景噪声和设备个体差异影响。', '术中影像': '影像判断可靠但不连续,新方案应补足两个成像时点之间的实时风险窗口。' } },
37      { q: '一旦识别到穿透,你希望系统介入到什么程度?', why: '“提示”与“自动制动”对应完全不同的风险等级、控制架构和注册验证工作量。', options: ['只做提示', '限制转速', '立即自动制动', '分级介入'], insight: { '只做提示': '保留医生最终控制权,但必须确保提示清晰且不增加认知负担。', '立即自动制动': '主动制动收益最高,也需要验证误触发、失效安全和小于 20ms 的闭环响应。', '分级介入': '分级介入能兼顾控制权与安全性,可先提示,再随置信度升高限制转速。' } },
38      { q: '你认为临床上可接受的误报警频率是多少?', why: '安全系统过于敏感会被医生关闭;这个答案决定识别阈值以及灵敏度与特异度的取舍。', options: ['每台不超过 1 次', '每 10 台不超过 1 次', '几乎不能误报', '先收集数据'], insight: { '每台不超过 1 次': '可接受一定误报,算法可优先保证灵敏度,适合早期验证。', '几乎不能误报': '需要更严格的多信号交叉确认,可能牺牲少量响应速度。', '先收集数据': '建议先以非介入记录模式建立临床基线,再冻结报警阈值。' } }
39    ]
40  },
41  {
42    match: ['康复', '呼吸', '训练', '居家', '患者'],
43    summary: '居家康复训练缺少动作质量判断与即时指导,需要用低门槛交互提升患者依从性和训练有效性。',
44    tags: ['居家康复', '动作识别', '即时反馈', '患者依从性'],
45    mappings: [
46      ['“动作不标准”', '呼吸流量/压力联合评估', '误差 ≤ ±5%'],
47      ['“不知道做得对不对”', '实时可视化指导', '反馈 ≤ 300ms'],
48      ['老年患者使用', '低认知负荷交互', '单任务界面'],
49      ['居家长期使用', '便携低功耗设计', '续航 ≥ 7天']
50    ],
51    constraints: [['用户', '老年患者'], ['场所', '非专业居家环境'], ['连接', '蓝牙 / 离线可用'], ['清洁', '接触件可拆洗']],
52    tests: [['◎', '训练动作识别准确率'], ['人', '老年用户可用性测试'], ['电', '续航与低电量策略测试']],
53    questions: [
54      { q: '这套训练主要服务哪类患者?', why: '不同疾病的训练目标、危险信号和有效动作标准并不相同,必须先锁定目标人群。', options: ['慢阻肺', '胸腹部术后', '哮喘', '其他人群'], insight: { '慢阻肺': '慢阻肺患者需重点关注呼气训练、血氧风险和长期依从性。', '胸腹部术后': '术后患者疼痛和活动耐量波动大,训练强度需要渐进调整。', '哮喘': '哮喘场景需区分日常训练与急性发作,避免错误指导。' } },
55      { q: '目前最常见的“不标准”具体表现是什么?', why: '只有把动作错误拆成可检测事件,设备才能提供有用反馈,而不是笼统评分。', options: ['吸气过快', '呼气不充分', '节律不稳定', '姿势错误'], insight: { '吸气过快': '应以流速上升斜率和峰值流量识别过快吸气,并提供节奏引导。', '呼气不充分': '需要结合呼气持续时间与末端流量判断完成度。', '姿势错误': '仅靠流量传感器不足,可能需要增加姿态传感或摄像头辅助。' } },
56      { q: '患者通常是在什么支持条件下使用?', why: '独立使用还是有ä
56ººååŠ©ï¼Œä¼šå†³å®šäº¤äº’å¤æ‚åº¦ã€å¼‚å¸¸å¤„ç†å’Œè¿œç¨‹ç…§æŠ¤åŠŸèƒ½ã€‚', options: ['完全独立', '家属偶尔协助', '医护远程指导', '线下集中训练'], insight: { '完全独立': '界面必须极简,并配置防误操作、语音提示和清晰的异常退出路径。', '医护远程指导': '应保留可解释的训练数据摘要,支持医护快速判断是否需要调整处方。', '线下集中训练': '可接受更专业的操作,但需要高频消毒与多患者数据隔离。' } },
57      { q: '判断训练有效,临床上最看重哪个结果?', why: '必须先确定主要临床终点,反馈算法才不会只优化“做完次数”等低价值指标。', options: ['动作完成质量', '肺功能改善', '症状减轻', '坚持训练天数'], insight: { '动作完成质量': '可将实时动作质量作为主要产品指标,并建立可解释的错误分类。', '肺功能改善': '需要设计更长周期的随访与肺功能关联验证。', '坚持训练天数': '应把低认知负荷和正向激励纳入核心设计要求。' } }
58    ]
59  },
60  {
61    match: ['腹腔镜', '抓钳', '虎口', '打滑', '鹰爪', '组织'],
62    summary: '腹腔镜抓钳长时间操作导致虎口疲劳,夹持湿滑组织时稳定性不足,同时需要控制组织损伤。',
63    tags: ['腹腔镜', '抓持器械', '人体工学', '防滑夹持'],
64    mappings: [
65      ['“虎口疼 / 用久了累”', '手柄接触压强优化', '< 30 kPa'],
66      ['“夹组织打滑”', '钳头表面摩擦增强', 'μ > 0.50'],
67      ['“像鹰爪一样有力”', '高曲率钳口 + 自锁', '夹持力 15–30 N'],
68      ['“不能损伤组织”', '载荷分散与力限制', '峰值压强 < 200 kPa']
69    ],
70    constraints: [['入路', '5 mm Trocar'], ['工作长度', '330–360 mm'], ['视野', '二维腹腔镜视野'], ['清洁灭菌', '耐高温或一次性']],
71    tests: [['手', '虎口区接触压强测试'], ['↔', '湿润组织抗滑移测试'], ['盾', '组织压伤与坏死阈值测试']],
72    questions: [
73      { q: '“打滑”最常发生在夹持哪类组织时?', why: '组织的脆弱度、含水量和表面结构不同,钳口纹理与允许夹持压强会采用完全不同的设计边界。', options: ['胆囊 / 肠管', '脂肪组织', '纤维组织', '多种组织'], insight: { '胆囊 / 肠管': '目标组织脆弱且湿润,不能单纯增加齿尖,应优先扩大接触面积并提高湿态摩擦。', '脂肪组织': '脂肪组织易滑脱,需要排液纹理与更稳定的包络夹持。', '纤维组织': '纤维组织允许更高局部载荷,可评估细齿或高曲率咬合结构。', '多种组织': '多组织适配意味着需要可控夹持力或分区纹理,避免单一钳口参数过度折中。' } },
74      { q: '打滑通常发生在什么操作动作中?', why: '静态牵拉、钝性分离和旋转操作的载荷方向不同,这决定钳口需要优化轴向力、侧向力还是抗扭能力。', options: ['持续牵拉', '钝性分离', '旋转剥离', '调整视野'], insight: { '持续牵拉': '应重点提高轴向抗滑移性能,同时限制持续夹持导致的缺血风险。', '钝性分离': '需要兼顾钳尖精细控制和侧向撑开刚度,不能只追求最大夹持力。', '旋转剥离': '抗扭稳定性是关键,应关注钳口包络角度与手柄旋转控制。' } },
75      { q: '虎口不适通常在连续操作多久后出现?', why: '出现疲劳的时间能帮助区分瞬时压痛、重复动作负担和手柄姿态问题,并确定人体工学测试时长。', options: ['10 分钟内', '10–30 分钟', '30–60 分钟', '1 小时以上'], insight: { '10 分钟内': '短时间即疼痛提示局部峰值压强或手柄尺寸明显不匹配,应优先重构接触面。', '10–30 分钟': '可能是局部压强与重复开合共同导致,需要同步测量受力和肌肉疲劳。', '30–60 分钟': '属于累积性负担,建议纳入持续任务的人体工学对照测试。' } },
76      { q: '为了获得更稳的夹持,你愿意接受哪种取舍?', why: '夹持稳定、组织保护、手柄行程和成本之间存在冲突,需要医生明确最不能牺牲的临床价值。', options: ['优先保护组织', '优先夹持稳定', '优先操作省力', '希望可调节'], insight: { '优先保护组织': '设计应采用力限制和载荷分散,避免以尖锐齿形换取稳定性ã€
76‚', '优先夹持稳定': '可提高包络与自锁能力,但必须用组织损伤测试确定上限。', '优先操作省力': '应优化传动比和握持姿态,同时避免行程过长影响精细控制。', '希望可调节': '可调夹持力最匹配多组织场景,但结构复杂度和误用风险需要同步评估。' } }
77    ]
78  }
79];
80
81const fallbackScenario = {
82  summary: '临床操作中存在效率、易用性或安全性方面的未满足需求,需要进一步量化关键使用场景和性能指标。',
83  tags: ['临床操作', '未满足需求', '人机交互', '风险控制'],
84  mappings: [
85    ['操作体验不理想', '人体工学优化', '待场景测量'],
86    ['现有方案效率低', '关键步骤流程重构', '目标提升 ≥ 20%'],
87    ['存在潜在风险', '失效安全设计', '单一故障安全'],
88    ['临床反馈模糊', '可量化评价体系', '建立基线数据']
89  ],
90  constraints: [['使用者', '临床专业人员'], ['环境', '医疗操作环境'], ['接触属性', '待确认'], ['法规分类', '待判定']],
91  tests: [['人', '关键任务可用性测试'], ['◎', '核心性能基准测试'], ['盾', '初步风险与失效测试']],
92  questions: [
93    { q: '这个问题主要发生在临床流程的哪个环节?', why: '先锁定问题发生时点,才能排除与核心场景无关的功能,避免把产品做成大而全。', options: ['术前准备', '操作过程中', '术后处理', '院外随访'], insight: {} },
94    { q: '问题发生的频率大致如何?', why: '发生频率和后果严重度共同决定这是必须解决的安全问题,还是体验优化问题。', options: ['几乎每次', '经常发生', '偶尔发生', '罕见但严重'], insight: { '几乎每次': '这是稳定复现的流程痛点,适合通过可量化的对照任务验证改进效果。', '罕见但严重': '应按低频高风险事件处理,优先分析失效模式和风险控制。' } },
95    { q: '目前临床上通常如何绕过或补救这个问题?', why: '现有补救方式揭示了真正的功能需求,也能帮助判断新产品是否会增加额外步骤。', options: ['依靠个人经验', '增加一名助手', '换用其他器械', '目前没有办法'], insight: {} },
96    { q: '如果只能优先改善一个结果,你会选择什么?', why: '明确第一优先级可以建立产品主要性能指标,减少互相冲突的无效需求。', options: ['患者安全', '操作效率', '医生舒适度', '治疗效果'], insight: {} }
97  ]
98};
99
100const projects = [
101  { icon: 'O', name: '华卫天和医疗器械 OPC 试点项目', id: 'OPC-2026-001', stage: 'OPC试点', progress: 7, owner: '待指定主理人', update: '试点项目已建立', source: 'workflow' },
102  { icon: '⌁', name: '鹰爪腹腔镜抓钳', id: 'MD-DEMO-0710', stage: '需求验证', progress: 18, owner: '演示角色', update: '流程演示数据', source: 'demo' },
103  { icon: '⚙', name: '智能骨钻安全系统', id: 'MD-DEMO-0628', stage: '概念设计', progress: 42, owner: '演示角色', update: '流程演示数据', source: 'demo' },
104  { icon: '◉', name: '居家呼吸训练仪', id: 'MD-DEMO-0519', stage: '虚拟验证', progress: 67, owner: '演示角色', update: '流程演示数据', source: 'demo' },
105  { icon: '◇', name: '微创吻合器力控模块', id: 'MD-DEMO-0412', stage: '注册准入', progress: 84, owner: '演示角色', update: '流程演示数据', source: 'demo' }
106];
107let selectedCommandProjectId = projects[0].id;
108let opcProjectState = null;
109let selectedOpcStageId = 'foundation';
110
111const services = [
112  { n: '01', stage: 'concept', stageName: '概念验证', title: 'AI 临床需求洞察报告', desc: '基于临床描述进行动态追问与工程映射;真实世界数据需另行接入并核验。', time: '按项目确认', price: '待核价', status:'AI可用' },
113  { n: '02', stage: 'concept', stageName: '概念验证', title: '专利围栏与赛道地图', desc: '真实专利数据库接入后,才能标注权利要求风险、法律状态与创新空间。', time: '能力待接入', price: '暂不报价', status:'待接入' },
114  { n: '03', stage: 'concept', stageName: '概念验证', title: 'FlashPOC 概念工作坊', desc: '当前展示概念方法样例;正式工程方案需完成设计输入、计算与工程师审核。', time: '方法演示', price: '暂不报价', status:'演示' },
115  { n: '04', stage: 'transform', stageName: '成果转化', title: '功能原型迭代管理', desc: '管理设计—测试闭环、图纸、BOM与审核记录;交付结果须真实上传并验收。', time: 'æ
115Œ‰é¡¹ç›®ç¡®è®¤', price: '待核价', status:'工作流' },
116  { n: '05', stage: 'transform', stageName: '成果转化', title: 'RegAI 合规资料框架', desc: '数字孪生与真实验证引擎接入前,仅规划DHF、风险管理和验证资料结构。', time: '能力待接入', price: '暂不报价', status:'待接入' },
117  { n: '06', stage: 'market', stageName: '商业化', title: 'AI 嵌入式能力规划', desc: '围绕检测、识别和力反馈建立数据、算法与验证任务,不预设项目适配度。', time: '按项目确认', price: '待核价', status:'规划' },
118  { n: '07', stage: 'market', stageName: '商业化', title: 'KOL 研究与合作规划', desc: '真实专家数据接入并获授权后,才开展画像核验和合作优先级分析。', time: '能力待接入', price: '暂不报价', status:'待接入' }
119];
120
121const conceptLibraries = {
122  laparoscopic: [
123    { type: '方案 A · 进化式', tone: 'evolution', symbol: 'A', title: '柔性包络钳口', desc: '保留熟悉的抓钳形态,以柔性分区纹理扩大接触面,降低湿润组织滑移。', tags: ['低学习成本', '组织友好'], score: 91, rationale: '以现有腹腔镜抓钳为基线,优先解决湿润脆弱组织的滑移问题。通过钳口远端柔顺层、近端排液纹理和受控闭合力,在不改变医生主要操作习惯的前提下提高稳定性。', params: ['钳口曲率 18–22°', '湿态摩擦系数 > 0.50', '峰值压强 < 200 kPa', '5 mm Trocar'] },
124    { type: '方案 B · 仿生式', tone: 'bio', symbol: 'B', title: '鹰爪分区咬合', desc: '借鉴猛禽趾部包络结构,用三段式曲率形成稳定抓持和载荷分散。', tags: ['抗滑最优', '仿生包络'], score: 94, rationale: '将医生“像鹰爪一样有力”的比喻转译为三段连续曲率,而非简单增加尖齿。外侧区域提供包络,中央微纹理排液,力限制结构控制组织峰值压强。', params: ['三段曲率 12°/20°/28°', '夹持力 15–25 N', '自锁释放力 < 8 N', '抗扭提升 35%'] },
125    { type: '方案 C · 激进式', tone: 'radical', symbol: 'C', title: '可调柔顺晶格钳口', desc: '利用微型柔顺晶格随组织形态自适应,兼顾多组织夹持与实时力限制。', tags: ['多组织适配', '结构创新'], score: 86, rationale: '通过一体化柔顺晶格产生被动自适应接触,可覆盖胆囊、脂肪和纤维组织。创新度最高,但制造验证、清洁和耐久性风险也更高。', params: ['晶格形变 0.4–1.2 mm', '两档夹持力', '循环寿命 > 10,000 次', '优先一次性方案'] }
126  ],
127  orthopedic: [
128    { type: '方案 A · 进化式', tone: 'evolution', symbol: 'A', title: '力矩感知骨钻模块', desc: '在现有动力手柄中集成轴向力与扭矩检测,不改变医生主要操作习惯。', tags: ['低改造成本', '实时感知'], score: 90, rationale: '通过轴向力与扭矩的瞬态变化识别皮质骨穿透,在保留现有钻头和手柄操作方式的同时提供分级提示。', params: ['采样率 ≥ 1 kHz', '识别延迟 < 30 ms', '误报 < 1/台', '可灭菌传感接口'] },
129    { type: '方案 B · 仿生式', tone: 'bio', symbol: 'B', title: '啄木鸟减冲击钻进', desc: '借鉴啄木鸟的间歇冲击机制,以脉冲钻进降低穿透后的惯性前冲。', tags: ['低前冲', '组织保护'], score: 93, rationale: '通过高频短行程脉冲替代连续推进,穿透发生后残余动能更低,更适合后方存在神经或软组织的高风险场景。', params: ['脉冲 80–120 Hz', '单次行程 < 0.4 mm', '制动响应 < 20 ms', '前冲降低 > 45%'] },
130    { type: '方案 C · 激进式', tone: 'radical', symbol: 'C', title: '预测制动智能骨钻', desc: '融合力、声学与电机电流信号,在穿透前预测剩余皮质骨厚度并主动减速。', tags: ['主动安全', '多模态算法'], score: 88, rationale: '多源信号交叉确认可降低误报,并在真正穿透前进入限速区间;性能潜力最高,同时需要更完整的临床数据和软件验证。', params: ['三模态融合', '预测窗口 50–100 ms', '分级限速', '单一故障安全'] }
131  ],
132  rehab: [
133    { type: '方案 A · 进化式', tone: 'evolution', symbol: 'A', title: '智能呼吸训练器', desc: '在传统训练器中加入流量、压力感知与灯环反馈,降低学习门槛。', tags: ['易于落地', '低认知负荷'], score: 92, rationale: '沿用患者熟悉的吹吸训练形态,以流量和压力联合判断动作完成度,并通过单一灯环给予即时节律提示。', params: ['流量误差 ±5%', '反馈延迟 < 300 ms', '单键操作', '续航 ≥ 7 天'] },
134    { type: '方案 B · 仿生式', tone: 'bio', symbol: 'B', title: '潮汐节律引导器', desc: '用类似潮汐起伏的触觉和光效帮助患者形成稳定、缓慢的呼吸节律。', tags: ['节律引导', '无屏交互'], score: 90, rationale: '将抽象的呼吸曲线转化为可跟随的触觉节律,减少老年患者读取图表的认知负担,适合独立居
134家训练。', params: ['节律 4–12 次/分', '触觉三级强度', '离线可用', '异常自动暂停'] },
135    { type: '方案 C · 激进式', tone: 'radical', symbol: 'C', title: '多模态康复教练', desc: '结合呼吸传感和姿态识别,实时判断动作、体位与训练质量。', tags: ['动作识别', '远程处方'], score: 87, rationale: '在呼吸参数之外加入上身姿态,形成更完整的动作质量评分,并支持医护远程查看可解释的错误分类。', params: ['姿态识别 > 90%', '错误分为 5 类', '远程处方同步', '隐私本地处理'] }
136  ]
137};
138
139const $ = (selector, scope = document) => scope.querySelector(selector);
140const $$ = (selector, scope = document) => [...scope.querySelectorAll(selector)];
141const BP_LEGACY_BASE = 'http://127.0.0.1:8000';
142const BP_API_BASE = window.location.protocol === 'file:' ? BP_LEGACY_BASE : `${window.location.origin}/api/bp`;
143let bpSelectedFile = null;
144let bpLoaderTimer = null;
145let currentComparableCandidates = [];
146let currentRegistrationEstimate = null;
147let currentBpDimensions = [];
148let investmentProjects = [];
149let currentInvestmentProject = null;
150let currentInvestmentAnalysis = null;
151let currentInvestmentReportId = null;
152let investmentAssistantMessages = [];
153let investmentAssistantBusy = false;
154let investmentProjectsLoaded = false;
155let investmentWorkflowStep = 1;
156let policyMaterialFiles = [];
157let policyMatchMaterialFiles = [];
158
159const POLICY_MATERIAL_ROLES = ['上年度申报书', '辅导会纪要(评分导向)', '自评报告', '审计或财务材料', '企业证明材料', '其他材料'];
160
161function guessPolicyMaterialRole(filename) {
162  const name = String(filename || '');
163  if (/纪要|辅导|培训|会议/.test(name)) return '辅导会纪要(评分导向)';
164  if (/自评|评价报告/.test(name)) return '自评报告';
165  if (/申报书|申请书|项目书/.test(name)) return '上年度申报书';
166  if (/审计|财务|报表|年报/.test(name)) return '审计或财务材料';
167  return '企业证明材料';
168}
169
170function renderPolicyMatchMaterials() {
171  const list = $('#policyMatchMaterialsList');
172  if (!list) return;
173  list.innerHTML = policyMatchMaterialFiles.map((entry, index) => `<div class="policy-match-material-item"><span class="material-file">${escapeHtml(entry.file.name)}</span><select data-match-material-role="${index}">${POLICY_MATERIAL_ROLES.map(role => `<option ${role === entry.role ? 'selected' : ''}>${role}</option>`).join('')}</select><button type="button" class="policy-material-remove" data-match-material-remove="${index}" title="移除">×</button></div>`).join('') || '<p class="policy-match-material-empty">尚未添加补充材料;没有也可以直接匹配</p>';
174}
175
176function addPolicyMatchMaterials(files) {
177  [...(files || [])].forEach(file => {
178    policyMatchMaterialFiles.push({ file, role: guessPolicyMaterialRole(file.name) });
179  });
180  renderPolicyMatchMaterials();
181}
182let policyGuideFiles = [];
183let policyTemplateFile = null;
184let policyAnalysisResult = null;
185let policyEnterprises = [];
186let currentPolicyEnterprise = null;
187let policyEnterprisesLoaded = false;
188let policyCatalog = [];
189let currentPolicyCatalog = null;
190let policyCatalogLoaded = false;
191let policyWorkflowStep = 1;
192let currentPolicyApplicationId = null;
193let selectedPolicyCandidate = null;
194let policyCatalogMode = 'library';
195let policyOpportunityScan = null;
196let policyCatalogQuery = '';
197let policyCatalogLevel = '全部级别';
198let policyCatalogShowMismatch = false;
199let professionalProjects = [];
200let currentProfessionalProject = null;
201let currentProfessionalRun = null;
202let professionalBusy = false;
203let baseEnterpriseCompanies = [];
204let baseEnterpriseStats = null;
205let currentBaseEnterprise = null;
206let baseEnterpriseLoaded = false;
207let policyCatalogValidityFilterValue = '全部状态';
208
209function escapeHtml(value) {
210  return String(value ?? '').replace(/[&<>"]/g, char => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;' })[char]);
211}
212
213async function checkBpEngine() {
214  const dot = $('#bpEngineDot');
215  try {
216    const response = await fetch(`${BP_API_BASE}/api/health`, { signal: AbortSignal.timeout(3500) });
217    if (!response.ok) throw new Error('offline');
218    dot.className = 'online';
219    $('#bpEngineStatus').textContent = 'BP 引擎在线 · 本机免登录';
220    $('#connectBpEngineButton').textContent = '本机免登录';
221    $('#connectBpEngineButton').disabled = true;
222    return true;
223  } catch {
224    dot.className = 'error';
225    $('#bpEngineStatus').textContent = 'BP 专业分析引擎尚未启动';
226    $('#connectBpEngineButton').textContent = '服务未启动';
227    return false;
228  }
229}
230
231function showBpLogin() {
232  $('#bpLoginPanel').classList.remove('hidden');
233  $('#bpUsername').focus();
234}
235
236async function loginBpEngine() {
237  const username = $('#bpUsername').value.trim();
238  const password = $('#bpPassword').value;
239  if (!username || !password) { $('#bpLoginMessage').textContent = '请输入内部账号和密码。'; return; }
240  $('#bpLoginButton').disabled = true;
241  $('#bpLoginMessage').textContent = '正在连接本机 BP 引擎…';
242  try {
243    const response = await fetch(`${BP_API_BASE}/api/auth/login`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username,password}) });
244    const payload = await response.json();
245    if (!response.ok) throw new Error(payload.detail || '登录失败');
246    localStorage.setItem('token', payload.data.token);
247    localStorage.setItem('user', JSON.stringify(payload.data.user));
248    $('#bpLoginPanel').classList.add('hidden');
249    $('#bpPassword').value = '';
250    $('#bpEngineDot').className = 'online';
251    $('#bpEngineStatus').textContent = `BP 引擎已连接 · ${payload.data.user.username}`;
252    $('#connectBpEngineButton').textContent = '已连接';
253    showToast('BP专业分析引擎已连接');
254  } catch (error) {
255    if (error.message.includes('Failed to fetch')) {
256      const online = await checkBpEngine();
257      $('#bpLoginMessage').textContent = online ? 'BP 服务现已启动,请再次点击“登录并连接”。' : '无法连接本机服务,请重新双击桌面的启动快捷方式。';
258    } else {
259      $('#bpLoginMessage').textContent = error.message;
260    }
261  } finally {
262    $('#bpLoginButton').disabled = false;
263  }
264}
265
266async function registerBpEngine() {
267  const username = $('#bpUsername').value.trim();
268  const password = $('#bpPassword').value;
269  const message = $('#bpLoginMessage');
270  if (username.length < 3 || password.length < 6) {
271    message.textContent = '请设置至少3位用户名和至少6位密码。';
272    message.className = 'error';
273    showToast('用户名至少3位,密码至少6位');
274    return;
275  }
276  $('#bpRegisterButton').disabled = true;
277  $('#bpRegisterButton').textContent = '正在创建…';
278  message.className = 'working';
279  message.textContent = '正在创建本机内部账号,请稍候…';
280  try {
281    const response = await fetch(`${BP_API_BASE}/api/auth/register`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({username,password,email:''}) });
282    const payload = await response.json();
283    if (!response.ok) throw new Error(payload.detail || '创建账号失败');
284    localStorage.setItem('token', payload.data.token);
285    localStorage.setItem('user', JSON.stringify(payload.data.user));
286    $('#bpLoginPanel').classList.add('hidden');
287    $('#bpPassword').value = '';
288    $('#bpEngineDot').className = 'online';
289    $('#bpEngineStatus').textContent = `BP 引擎已连接 · ${payload.data.user.username}`;
290    $('#connectBpEngineButton').textContent = '已连接';
291    showToast('内部账号创建成功,BP引擎已连接');
292  } catch (error) {
293    const friendlyMessage = error.message.includes('Failed to fetch')
294      ? 'BP 服务仍在启动,请稍等几秒后重试。'
295      : error.message.includes('用户名已存在')
296        ? `“${username}”已经存在,请换一个新用户名(例如 zhang2026),或者使用原密码登录。`
297        : error.message;
298    message.textContent = friendlyMessage;
299    message.className = 'error';
300    showToast(friendlyMessage);
301  } finally {
302    $('#bpRegisterButton').disabled = false;
303    $('#bpRegisterButton').textContent = '首次使用,创建账号';
304  }
305}
306
307// ============ 全局登录拦截 + 当前用户 ============
308let currentPlatformUser = null;
309let authGateMode = 'login';
310
311function getStoredUser() {
312  try { return JSON.parse(localStorage.getItem('user') || 'null'); } catch { return null; }
313}
314
315function applyUserChrome(user) {
316  currentPlatformUser = user;
317  const isAdmin = !!user && user.role === 'admin';
318  const adminNav = $('#adminNav');
319  if (adminNav) adminNav.hidden = !isAdmin;
320  const profile = $('.profile-card');
321  if (profile && user) {
322    const displayName = user.nickname || user.username || '内部工作台';
323    const avatar = profile.querySelector('.avatar');
324    if (avatar) avatar.textContent = displayName.slice(0, 1);
325    const strong = profile.querySelector('strong');
326    if (strong) strong.textContent = displayName;
327  }
328}
329
330function showAuthGate() {
331  $('#authGate')?.classList.remove('hidden');
332  setTimeout(() => $('#authGateUsername')?.focus(), 60);
333}
334
335function hideAuthGate() {
336  $('#authGate')?.classList.add('hidden');
337}
338
339function setAuthGateMode(mode) {
340  authGateMode = mode;
341  $('#authTabLogin')?.classList.toggle('active', mode === 'login');
342  $('#authTabRegister')?.classList.toggle('active', mode === 'register');
343  const submit = $('#authGateSubmit');
344  if (submit) submit.textContent = mode === 'login' ? '登录并进入系统' : '注册并进入系统';
345  const msg = $('#authGateMessage');
346  if (msg) { msg.textContent = ''; msg.className = ''; }
347}
348
349async function submitAuthGate() {
350  const username = $('#authGateUsername').value.trim();
351  const password = $('#authGatePassword').value;
352  const msg = $('#authGateMessage');
353  const submit = $('#authGateSubmit');
354  if (username.length < 3 || password.length < 6) {
355    msg.textContent = '请输入至少3位用户名和至少6位密码。';
356    msg.className = 'error';
357    return;
358  }
359  submit.disabled = true;
360  msg.className = 'working';
361  msg.textContent = authGateMode === 'login' ? '正在登录…' : '正在创建账号…';
362  const path = authGateMode === 'login' ? '/api/auth/login' : '/api/auth/register';
363  try {
364    const response = await fetch(`${BP_API_BASE}${path}`, {
365      method: 'POST',
366      headers: { 'Content-Type': 'application/json' },
367      body: JSON.stringify({ username, password, email: '' }),
368    });
369    const payload = await response.json();
370    if (!response.ok) throw new Error(payload.detail || (authGateMode === 'login' ? '登录失败' : '注册失败'));
371    localStorage.setItem('token', payload.data.token);
372    localStorage.setItem('user', JSON.stringify(payload.data.user));
373    hideAuthGate();
374    showToast(`${payload.data.user.username},欢迎回来`);
375    // 数据按用户隔离,登录后整页刷新以确保各视图拉到当前用户的数据
376    setTimeout(() => window.location.reload(), 350);
377  } catch (error) {
378    msg.textContent = error.message.includes('Failed to fetch') ? '无法连接本机服务,请确认系统已启动。' : error.message;
379    msg.className = 'error';
380  } finally {
381    submit.disabled = false;
382  }
383}
384
385async function initPlatformAuth() {
386  $('#authTabLogin')?.addEventListener('click', () => setAuthGateMode('login'));
387  $('#authTabRegister')?.addEventListener('click', () => setAuthGateMode('register'));
388  $('#authGateSubmit')?.addEventListener('click', submitAuthGate);
389  $('#authGatePassword')?.addEventListener('keydown', event => { if (event.key === 'Enter') submitAuthGate(); });
390
391  const token = localStorage.getItem('token');
392  if (!token) { showAuthGate(); return; }
393  try {
394    const response = await fetch(`${BP_API_BASE}/api/auth/me`, {
395      headers: { Authorization: `Bearer ${token}` },
396      signal: AbortSignal.timeout(5000),
397    });
398    if (!response.ok) throw new Error('expired');
399    const payload = await response.json();
400    applyUserChrome(payload.data);
401  } catch {
402    localStorage.removeItem('token');
403    localStorage.removeItem('user');
404    showAuthGate();
405  }
406}
407
408// ============ 管理员数据看板 ============
409const adminDetailCache = {};
410
411function formatAdminTime(iso) {
412  if (!iso) return '—';
413  return String(iso).slice(0, 16).replace('T', ' ');
414}
415
416async function loadAdminDashboard() {
417  const forbidden = $('#adminForbidden');
418  const content = $('#adminContent');
419  if (!forbidden || !content) return;
420  const user = currentPlatformUser || getStoredUser();
421  if (!user || user.role !== 'admin') {
422    forbidden.classList.remove('hidden');
423    content.classList.add('hidden');
424    return;
425  }
426  forbidden.classList.add('hidden');
427  content.classList.remove('hidden');
428  try {
429    const [overview, users] = await Promise.all([
430      investmentApiRequest('/api/admin/overview'),
431      investmentApiRequest('/api/admin/users'),
432    ]);
433    renderAdminOverview(overview);
434    renderAdminUsers(users);
435  } catch (error) {
436    if (/仅管理员|无权|403/.test(error.message)) {
437      forbidden.classList.remove('hidden');
438      content.classList.add('hidden');
439    } else {
440      showToast(`看板数据加载失败:${error.message}`);
441    }
442  }
443}
444
445function renderAdminOverview(data) {
446  $('#adminStatUsers').textContent = data.total_users ?? '—';
447  $('#adminStatUsersSub').textContent = `近7天新增 ${data.recent7d_new_users ?? 0} 人`;
448  $('#adminStatProjects').textContent = data.total_projects ?? '—';
449  $('#adminStatReportsSub').textContent = `历史报告 ${data.total_reports ?? 0} 份`;
450  $('#adminStatAvg').textContent = data.avg_score != null ? data.avg_score : '—';
451  $('#adminStatActive').textContent = `${data.recent7d_new_users ?? 0}
451人 / ${data.recent7d_new_analyses ?? 0}项`;
452  $('#adminStatActiveSub').textContent = '新用户 / 新增分析';
453
454  const list = $('#adminActivityList');
455  const activities = data.recent_activities || [];
456  list.innerHTML = activities.length ? activities.map(item => `
457    <div class="admin-activity-item">
458      <span class="act-user">${escapeHtml(item.user_nickname || '未知用户')}</span>
459      <span class="act-project">${escapeHtml(item.project_name || '未命名项目')}</span>
460      <span class="admin-score-chip${item.score == null ? ' none' : ''}">${item.score != null ? escapeHtml(item.score) : '未评分'}</span>
461      <span class="act-time">${formatAdminTime(item.updated_at)}</span>
462    </div>`).join('') : '<p class="admin-loading">暂无分析动态</p>';
463}
464
465function renderAdminUsers(users) {
466  $('#adminUserCount').textContent = `${users.length} 个账号`;
467  const table = $('#adminUserTable');
468  if (!users.length) { table.innerHTML = '<p class="admin-loading">暂无用户</p>'; return; }
469  table.innerHTML = `<table>
470    <thead><tr><th>用户</th><th>角色</th><th>分析数</th><th>最近活跃</th><th>注册时间</th></tr></thead>
471    <tbody>${users.map(u => {
472      const name = u.nickname || u.username || '未命名';
473      const avatar = u.avatar_url
474        ? `<span class="admin-avatar"><img src="${escapeHtml(u.avatar_url)}" alt=""></span>`
475        : `<span class="admin-avatar">${escapeHtml(name.slice(0, 1))}</span>`;
476      return `<tr class="admin-user-row" data-admin-user-id="${escapeHtml(u.user_id)}">
477        <td><span class="admin-user-cell">${avatar}<span><strong>${escapeHtml(name)}</strong><small>${escapeHtml(u.auth_type === 'wechat' ? '微信小程序' : u.username)}</small></span></span></td>
478        <td><span class="admin-role-badge ${u.role === 'admin' ? 'admin' : 'user'}">${u.role === 'admin' ? '管理员' : '用户'}</span></td>
479        <td>${u.analysis_count ?? 0}</td>
480        <td>
480${u.active_recent7d ? `<span class="admin-active-dot">${formatAdminTime(u.last_active)}</span>` : formatAdminTime(u.last_active)}</td>
481        <td>${formatAdminTime(u.created_at)}</td>
482      </tr><tr class="admin-detail-row hidden" data-admin-detail-for="${escapeHtml(u.user_id)}"><td colspan="5"><div class="admin-detail-box"><p class="admin-loading">正在读取明细…</p></div></td></tr>`;
483    }).join('')}</tbody>
484  </table>`;
485  $$('.admin-user-row', table).forEach(row => {
486    row.addEventListener('click', () => toggleAdminUserDetail(row.dataset.adminUserId, row));
487  });
488}
489
490async function toggleAdminUserDetail(userId, row) {
491  const detailRow = $(`[data-admin-detail-for="${CSS.escape(userId)}"]`);
492  if (!detailRow) return;
493  const willOpen = detailRow.classList.contains('hidden');
494  $$('.admin-detail-row').forEach(item => item.classList.add('hidden'));
495  $$('.admin-user-row').forEach(item => item.classList.remove('expanded'));
496  if (!willOpen) return;
497  detailRow.classList.remove('hidden');
498  row.classList.add('expanded');
499  const box = $('.admin-detail-box', detailRow);
500  try {
501    if (!adminDetailCache[userId]) {
502      adminDetailCache[userId] = await investmentApiRequest(`/api/admin/users/${encodeURIComponent(userId)}/detail`);
503    }
504    box.innerHTML = renderAdminUserDetail(adminDetailCache[userId]);
505  } catch (error) {
506    box.innerHTML = `<p class="admin-detail-empty">明细加载失败:${escapeHtml(error.message)}</p>`;
507  }
508}
509
510function renderAdminUserDetail(detail) {
511  const projects = detail.projects || [];
512  const reports = detail.reports || [];
513  const projectBlock = projects.length ? `<table>
514    <thead><tr><th>项目名</th><th>评分</th><th>结论</th><th>创建时间</th></tr></thead>
515    <tbody>${projects.map(p => `<tr>
516      <td>${escapeHtml(p.name || '未命名项目')}</td>
517      <td><span class="admin-score-chip${p.score == null ? ' none' : ''}">${p.score != null ? escapeHtml(p.score) : '—'}</span></td>
518      <td>${escapeHtml(p.conclusion || p.status || '—')}</td>
519      <td>${formatAdminTime(p.created_at)}</td>
520    </tr>`).join('')}</tbody>
521  </table>` : '<p class="admin-detail-empty">该用户暂无分析项目</p>';
522  const reportBlock = reports.length ? `<h4 style="margin-top:14px">报告(${reports.length})</h4><table>
523    <thead><tr><th>文件名</th><th>项目</th><th>评分</th><th>时间</th></tr></thead>
524    <tbody>${reports.slice(0, 20).map(r => `<tr>
525      <td>${escapeHtml(r.file_name || '—')}</td>
526      <td>${escapeHtml(r.project_name || '—')}</td>
527      <td>${r.overall_score != null ? escapeHtml(r.overall_score) : '—'}</td>
528      <td>${formatAdminTime(r.created_at)}</td>
529    </tr>`).join('')}</tbody>
530  </table>` : '';
531  return `<h4>分析项目(${projects.length})</h4>${projectBlock}${reportBlock}`;
532}
533
534function chooseBpFile(file) {
535  if (!file) return;
536  const extension = file.name.split('.').pop().toLowerCase();
537  if (!['pdf','docx','pptx'].includes(extension)) { showToast('BP专业分析支持 PDF、DOCX 和 PPTX'); return; }
538  if (file.size > 120 * 1024 * 1024) { showToast('文件不能超过 120MB'); return; }
539  bpSelectedFile = file;
540  $('#bpSelectedFile').textContent = `${file.name} · ${(file.size/1024/1024).toFixed(1)}MB`;
541  $('#bpAnalyzeFileButton').disabled = false;
542  $('#bpAnalysisProgress').textContent = '文件已就绪';
543}
544
545const projectValueDimensionWeights = {
546  '需求逻辑':0.14,'转化逻辑':0.11,'临床注册逻辑':0.09,'市场逻辑':0.12,'竞争逻辑':0.11,
547  '商业化逻辑':0.09,'团队逻辑':0.13,'壁垒逻辑':0.12,'回报逻辑':0.09
548};
549
550function normalizeProjectValueReport(report) {
551  if (!report || typeof report !== 'object') return report || {};
552  const method = String(report.scoring_audit?.method || report.analysis_audit?.scoring_method || '');
553  const legacyEvidenceScore = /EvidenceScore v1|固定评分表按BP原文|无原文不得分/i.test(method);
554  if (!legacyEvidenceScore) return report;
555  const dimensions = Array.isArray(report.dimensions) ? report.dimensions : [];
556  let total = 0, lowTotal = 0, highTotal = 0, coverageTotal = 0, weightTotal = 0;
557  dimensions.forEach(item => {
558    const audit = item.score_audit || {};
559    const criteria = Array.isArray(audit.criteria) ? audit.criteria : [];
560    criteria.forEach(criterion => {
561      const missing = criterion.status === '缺失' || (!criterion.evidence_quote && Number(criterion.awarded_points) === 0);
562      if (!missing) return;
563      criterion.awarded_points = null;
564      criterion.status = '待验证';
565      criterion.reason = 'BP未描述该项,当前无法判断;不因此给项目扣零分,转入尽调核验任务。';
566      criterion.data_status = '未知,不等于项目不存在该项能力';
567    });
568    const supported = criteria.filter(criterion => criterion.awarded_points !== null && criterion.awarded_points !== undefined).length;
569    const coverage = criteria.length ? supported / criteria.length : (item.evidence && item.evidence !== 'BP中未提及相关内容' ? 0.25 : 0);
570    const oldModelScore = Number(item.model_score_before_audit);
571    const oldDisplayedScore = Number(item.score);
572    const potentialScore = Number.isFinite(oldModelScore)
573      ? Math.max(0, Math.min(10, oldModelScore))
574      : (coverage === 0 && (!Number.isFinite(oldDisplayedScore) || oldDisplayedScore <= 0) ? 5 : Math.max(0, Math.min(10, oldDisplayedScore || 5)));
575    const uncertainty = coverage >= 0.75 ? 0.8 : coverage >= 0.5 ? 1.3 : coverage >= 0.25 ? 2 : 2.8;
576    const low = Math.max(0, potentialScore - uncertainty);
577    const high = Math.min(10, potentialScore + uncertainty);
578    const confidence = coverage >= 0.75 ? '高' : coverage >= 0.45 ? '中' : '低';
579    item.score = Number(potentialScore.toFixed(1));
580    item.potential_score = item.score;
581    item.score_range = {low:Number(low.toFixed(1)),high:Number(high.toFixed(1))};
582    item.evidence_coverage = Math.round(coverage * 100);
583    item.score_reason = `已从旧报告中恢复AI对项目潜力的初步判断;BP证据覆盖${item.evidence_coverage}%,合理区间${item.score_range.low}—${item.score_range.high}分。未写内容改为待验证,不再作为项目零分。`;
584    item.score_audit = {...audit,method:'项目潜力评分 + 证据置信度双轨研判',confidence,supported_criteria:supported,pending_criteria:criteria.length-supported,evidence_coverage:item.evidence_coverage,criteria,boundary:'这是旧报告按新口径的兼容重构;重新分析可获得更准确的项目价值判断'};
585    const weight = Number(item.weight) || projectValueDimensionWeights[item.name] || 1 / Math.max(1, dimensions.length);
586    total += item.score * weight; lowTotal += item.score_range.low * weight; highTotal += item.score_range.high * weight; coverageTotal += coverage * weight; weightTotal += weight;
587  });
588  if (weightTotal) {
589    report.overall_score = Number((total / weightTotal).toFixed(1));
590    report.potential_score = report.overall_score;
591    report.score_range = {low:Number((lowTotal / weightTotal).toFixed(1)),high:Number((highTotal / weightTotal).toFixed(1))};
592    report.evidence_coverage = Math.round(coverageTotal / weightTotal * 100);
593    report.evidence_confidence = report.evidence_coverage >= 75 ? '高' : report.evidence_coverage >= 45 ? '中' : '低';
594    report.recommendation = report.overall_score >= 7.5 ? '高潜力,进入专家初审与重点验证' : report.overall_score >= 6 ? '建议继续转化可行性验证' : report.overall_score >= 5 ? '谨慎推进,先验证关键价值假设' : '暂缓重投入,先验证核心价值假设';
595  }
596  report.scoring_audit = {method:'ProjectValueScore v2.0 · 旧报告兼容重构',rule:'项目潜力与BP证据覆盖度分开;缺项不直接记零分'};
597  report.analysis_audit = {...(report.analysis_audit || {}),scoring_method:'ProjectValueScore v2.0',legacy_value_reconstruction:true};
598  return report;
599}
600
601function renderProfessionalBpReport(report, options = {}) {
602  report = normalizeProjectValueReport(report);
603  currentInvestmentAnalysis = report;
604  const dimensions = Array.isArray(report.dimensions) ? report.dimensions : [];
605  currentBpDimensions = dimensions;
606  const risks = Array.isArray(report.risks) ? report.risks : [];
607  const audit = report.analysis_audit || {};
608  const usedAi = audit.ai_called === true || report.analysis_mode === 'ai_full_text_analysis';
609  const modeBadge = $('#analysisModeBadge');
610  modeBadge.textContent = usedAi ? '✓ 已实际调用 AI' : '本机规则初筛';
611  modeBadge.classList.toggle('verified', usedAi);
612  $('#analysisAuditModel').textContent = usedAi
613    ? `${audit.mode_label || 'DeepSeek AI 全文分析'} · ${audit.model || '模型名称待记录'}`
614    : (audit.mode_label || '未调用大模型');
615  const auditParts = [];
616  if (audit.parsed_characters) auditParts.push(`已读取正文 ${Number(audit.parsed_characters).toLocaleStr
616ing()} 字`);
617  if (audit.request_id) auditParts.push(`调用记录 ${String(audit.request_id).slice(0,12)}…`);
618  if (audit.usage?.total_tokens) auditParts.push(`模型用量 ${Number(audit.usage.total_tokens).toLocaleString()} tokens`);
619  if (audit.external_claims_removed) auditParts.push(`已拦截 ${audit.external_claims_removed} 条无来源数字`);
620  if (audit.scoring_method) auditParts.push(`评分 ${audit.scoring_method}`);
621  if (audit.workbench_corrected_from_bp) auditParts.push('产品工作台已按本次BP原文纠偏');
622  if (audit.knowledge_isolated_from_primary_analysis) auditParts.push('内部资料已与主分析隔离');
623  if (audit.internal_references_used) auditParts.push(`主分析使用内部参考 ${audit.internal_references_used} 份`);
624  else if (audit.internal_references_available) auditParts.push(`另附 ${audit.internal_references_available} 份待核验研究线索`);
625  if (audit.legacy_value_reconstruction) auditParts.unshift('旧报告已按项目价值新口径重构,建议重新分析获得正式结果');
626  $('#analysisAuditMeta').textContent = auditParts.join(' · ') || '当前报告未保存完整调用记录,建议重新分析生成可审计版本';
627  $('#analysisDataBoundary').textContent = audit.external_data_boundary || 'BP事实来自上传文件;评分和建议属于AI判断;只有带来源链接的外部数字才是已追溯参考。';
628  $('#investmentDecisionMode').textContent = `${usedAi ? 'AI全文分析' : '规则初筛'} · 项目价值初判 · 非投资结论`;
629  const evidenceCoverage = Number(report.evidence_coverage || 0);
630  const evidenceConfidence = report.evidence_confidence || (evidenceCoverage >= 75 ? '高' : evidenceCoverage >= 45 ? '中' : '低');
631  $('#investmentConfidence').textContent = `BP证据覆盖 ${evidenceCoverage}% · 置信度${evidenceConfidence}`;
632  $('#investmentScore').innerHTML = `${escapeHtml(report.overall_score ?? '—')}<small>/10</small>`;
633  $('#investmentVerdict').textContent = report.recommendation || '待专家判断';
634  $('#investmentJudgment').textContent = report.summary || '分析完成,等待投委会复核。';
635  renderExecutiveBrief(report.executive_brief || null);
636  const pdfBtn = $('#exportReportPdfBtn');
637  if (pdfBtn) pdfBtn.onclick = exportReportPdf;
638  const pptxBtn = $('#exportReportPptxBtn');
639  if (pptxBtn) pptxBtn.onclick = exportReportPptx;
640  const checklistBtn = $('#exportChecklistBtn');
641  if (checklistBtn) checklistBtn.onclick = exportChecklistExcel;
642  const tpResult = $('#transformationPathResult');
643  if (tpResult) { tpResult.classList.add('hidden'); tpResult.innerHTML = ''; }
644  $('#investmentResult').classList.remove('hidden');
645  const referenceData = report.reference_data || defaultInvestmentReferenceData();
646  renderDecisionWorkbench(report.decision_workbench || defaultDecisionWorkbench(referenceData.product_scope), report);
647  renderInvestmentEvidence(referenceData);
648  renderInternalKnowledgeReferences(report.internal_knowledge || {});
649  renderBiomedicalRoute(report.biomedical_route || null);
650  renderInvestmentReviewChecklist();
651  refreshInvestmentAssistant();
652  loadInvestmentEnterpriseMatches();
653  loadInvestorPanels();
654  if (!options.silent) $('#investmentResult').scrollIntoView({behavior:'smooth',block:'nearest'});
655}
656
657// ============ 投资简报(AI叙事层):四步诊断法,基于全部已核验模块数据生成,事实分级标注 ============
658
659function renderExecutiveBrief(eb) {
660  const panel = $('#execBriefPanel');
661  if (!panel) return;
662  const b = eb && eb.ran ? eb.brief : null;
663  if (!b || !b.step1_track) { panel.classList.add('hidden'); return; }
664  panel.classList.remove('hidden');
665  $('#execBriefModel').textContent = eb.model ? `叙事模型 ${eb.model}` : '';
666  $('#execOverview').textContent = b.overview || '';
667  const srcBadge = source => {
668    const map = {'官方数据':'official','网络报道':'news','BP自述':'bp','AI推断':'ai'};
669    return `<i class="src-badge src-${map[source] || 'ai'}">${escapeHtml(source || 'AI推断')}</i>`;
670  };
671
672  // 警示红牌(进度/数据/竞品/团队/市场)
673  const flags = b.red_flags || [];
674  const flagBox = $('#execRedFlags');
675  flagBox.classList.toggle('hidden', !flags.length);
676  flagBox.innerHTML = flags.map(f =>
677    `<div class="exec-redflag"><b>🚩 ${escapeHtml(f.flag)}</b><span>${escapeHtml(f.detail)}</span></div>`
678  ).join('');
679
680  // 第一步 · 赛道翻译官
681  const s1 = b.step1_track || {};
682  $('#execStep1').innerHTML = `<header><i>第一步</i><strong>赛道翻译官</strong><small>这到底是什么,用能听懂的话说</small></header>`
683    + (s1.essence ? `<p class="exec-essence">${escapeHtml(s1.essence)}</p>` : '')
684    + `<div class="exec-metrics">${(s1.golden_metrics || []).map(m =>
685        `<div class="exec-metric"><b>${escapeHtml(m.metric)}</b><span>${escapeHtml(m.why)}</span></div>`).join('')}</div>`
686    + (s1.endgame ? `<p class="exec-endgame"><b>赛道终局:${escapeHtml(s1.endgame)}</b><span>${escapeHtml(s1.endgame_reason || '')}</span></p>` : '');
687
688  // 第二步 · 价值判官
689  const s2 = b.step2_value || {};
690  const a = s2.anchor || {};
691  $('#execStep2').innerHTML = `<header><i>第二步</i><strong>价值判官</strong><small>值不值钱、值多少钱,由对手说了算</small></header>`
692    + (a.name ? `<div class="exec-anchor"><span class="exec-anchor-label">头号死敌</span><strong>${escapeHtml(a.name)}</strong><small>${escapeHtml(a.fact || '')}</small>${srcBadge(a.source)}</div>` : '')
693    + (s2.lag_estimate ? `<p class="exec-line"><b>时间惩罚</b><span>${escapeHtml(s2.lag_estimate)}</span></p>` : '')
694    + (s2.valuation_range ? `<div class="exec-valuation"><small>含落后惩罚的投前估值区间(AI推断,须投委会复核
694)</small><strong>${escapeHtml(s2.valuation_range)}</strong><p>${escapeHtml(s2.valuation_reason || '')}</p></div>` : '')
695    + (s2.fallback_position ? `<p class="exec-line"><b>备胎价值</b><span>${escapeHtml(s2.fallback_position)}</span></p>` : '');
696
697  // 第三步 · 死亡预演官
698  const s3 = b.step3_death || {};
699  const sd = s3.science_death || {}, md = s3.market_death || {}, td = s3.team_death || {};
700  const deathCard = (title, body, primary) =>
701    `<div class="death-card${primary ? ' primary' : ''}">${primary ? '<em>一级风险</em>' : ''}<b>${title}</b>${body}</div>`;
702  $('#execStep3').innerHTML = `<header><i>第三步</i><strong>死亡预演官</strong><small>这个项目会怎么死?三种死法,哪个概率最大</small></header>`
703    + `<div class="death-grid">`
704    + deathCard('死法一 · 科学自杀',
705        `<span>${escapeHtml(sd.assumption || '')}</span>${sd.probability ? `<u>证伪概率 ${escapeHtml(sd.probability)}</u>` : ''}<small>${escapeHtml(sd.reason || '')}</small>`,
706        s3.primary_risk === '科学自杀')
707    + deathCard('死法二 · 市场谋杀',
708        `<span>${escapeHtml(md.scenario || '')}</span><small>${escapeHtml(md.reason || '')}</small>`,
709        s3.primary_risk === '市场谋杀')
710    + deathCard('死法三 · 团队内耗',
711        `<span>${escapeHtml(td.scenario || '')}</span><small>${escapeHtml(td.reason || '')}</small>`,
712        s3.primary_risk === '团队内耗')
713    + `</div>`
714    + (s3.primary_risk ? `<p class="exec-line primary-risk"><b>一级风险:${escapeHtml(s3.primary_risk)}</b><span>${escapeHtml(s3.primary_reason || '')}</span></p>` : '')
715    + (s3.incubator_hedge ? `<p class="exec-line"><b>孵化器能否对冲</b><span>${escapeHtml(s3.incubator_hedge)}</span></p>` : '');
716
717  // 第四步 · 行动导航仪
718  const s4 = b.step4_action || {};
719  const choiceCls = {A:'a', B:'b', C:'c', D:'d'}[s4.choice] || 'c';
720  $('#execStep4').innerHTML = `<header><i>第四步</i><strong>行动导航仪</strong><small>别废话,该怎么办</small></header>`
721    + `<div class="exec-choice"><b class="choice-${choiceCls}">${escapeHtml(s4.choice || '—')}</b><div><strong>${escapeHtml(s4.label || '')}</strong><p>${escapeHtml(s4.reason || '')}</p></div></div>`;
722
723  $('#execWatch').innerHTML = (b.watch_points || []).length
724    ? '<strong class="exec-block-title">未来 12–18 个月观察点</strong><div>' + b.watch_points.map(w => `<span>${escapeHtml(w)}</span>`).join('') + '</div>'
725    : '';
726  $('#execBoundary').textContent = eb.boundary || '';
727
728  // 逻辑对齐:四步诊断为系统核心逻辑,顶部结论以行动导航仪为准,避免与旧评分口径冲突
729  if (s4.choice && s4.label) {
730    const tail = s4.label.includes('→') ? s4.label.split('→')[1].trim() : s4.label;
731    $('#investmentVerdict').textContent = `行动建议 ${s4.choice} · ${tail}`;
732    if (s4.reason) $('#investmentJudgment').textContent = s4.reason;
733  }
734}
735
736// ============ 导出完整分析报告 PDF ============
737
738async function exportReportPdf() {
739  if (!currentInvestmentReportId) { showToast('暂无已保存的报告,请先完成一次分析'); return; }
740  const btn = $('#exportReportPdfBtn');
741  if (btn) btn.disabled = true;
742  showToast('正在生成完整分析报告 PDF…');
743  try {
744    const token = localStorage.getItem('token');
745    const resp = await fetch(`${BP_API_BASE}/api/reports/${currentInvestmentReportId}/download`, {
746      headers: token ? {Authorization:`Bearer ${token}`} : {},
747    });
748    if (!resp.ok) throw new Error('download failed');
749    const blob = await resp.blob();
750    const cd = resp.headers.get('Content-Disposition') || '';
751    const match = cd.match(/filename\*?=(?:UTF-8''|")?([^";]+)/i);
752    const url = URL.createObjectURL(blob);
753    const link = document.createElement('a');
754    link.href = url;
755    link.download = match ? decodeURIComponent(match[1]) : `BP分析报告-${currentInvestmentReportId.slice(0,8)}.pdf`;
756    link.click();
757    URL.revokeObjectURL(url);
758    showToast('完整分析报告 PDF 已开始下载');
759  } catch (error) {
760    showToast('PDF 导出失败,请稍后重试');
761  } finally {
762    if (btn) btn.disabled = false;
763  }
764}
765
766// 通用报告文件下载:PPTX 投资备忘录 / Excel 尽调清单共用同一套逻辑
767async function downloadReportFile({endpoint, fallbackName, btnId, busyText, okText, failText}) {
768  if (!currentInvestmentReportId) { showToast('暂无已保存的报告,请先完成一次分析'); return; }
769  const btn = $(btnId);
770  if (btn) btn.disabled = true;
771  showToast(busyText);
772  try {
773    const token = localStorage.getItem('token');
774    const resp = await fetch(`${BP_API_BASE}/api/reports/${currentInvestmentReportId}/${endpoint}`, {
775      headers: token ? {Authorization:`Bearer ${token}`} : {},
776    });
777    if (!resp.ok) throw new Error('download failed');
778    const blob = await resp.blob();
779    const cd = resp.headers.get('Content-Disposition') || '';
780    const match = cd.match(/filename\*?=(?:UTF-8''|")?([^";]+)/i);
781    const url = URL.createObjectURL(blob);
782    const link = document.createElement('a');
783    link.href = url;
784    link.download = match ? decodeURIComponent(match[1]) : fallbackName;
785    link.click();
786    URL.revokeObjectURL(url);
787    showToast(okText);
788  } catch (error) {
789    showToast(failText);
790  } finally {
791    if (btn) btn.disabled = false;
792  }
793}
794
795function exportReportPptx() {
796  downloadReportFile({
797    endpoint:'download_pptx',
798    fallbackName:`投资备忘录-${(currentInvestmentReportId || '').slice(0,8)}.pptx`,
799    btnId:'#exportReportPptxBtn',
800    busyText:'正在生成投资备忘录 PPT…',
801    okText:'投资备忘录 PPT 已开始下载',
802    failText:'PPT 导出失败,请稍后重试',
803  });
804}
805
806function exportChecklistExcel() {
807  downloadReportFile({
808    endpoint:'download_checklist',
809    fallbackName:`尽调清单-${(currentInvestmentReportId || '').slice(0,8)}.xlsx`,
810    btnId:'#exportChecklistBtn',
811    busyText:'正在生成尽调清单 Excel…',
812    okText:'尽调清单 Excel 已开始下载',
813    failText:'Excel 导出失败,请稍后重试',
814  });
815}
816
817// ============ 投资决策三问:一级红灯检查 + 成本/风险/市场 ============
818
819let investorBriefData = null;
820let redlightResultData = null;
821let redlightRunning = false;
822
823const REDLIGHT_STATUS_META = {
824  red: { label: '红灯', cls: 'red' },
825  yellow: { label: '黄灯', cls: 'yellow' },
826  green: { label: '绿灯', cls: 'green' },
827  unknown: { label: '未核验', cls: 'unknown' },
828};
829
830function redlightVerdictClass(verdict) {
831  if (verdict === '初步不推荐') return 'red';
832  if (verdict === '谨慎推进') return 'yellow';
833  if (verdict === '通过初筛') return 'green';
834  return 'unknown';
835}
836
837function renderRedlightPanel(result) {
838  const empty = $('#redlightEmpty');
839  const loading = $('#redlightLoading');
840  const box = $('#redlightResult');
841  const badge = $('#redlightVerdictBadge');
842  if (!empty || !box) return;
843  empty.classList.toggle('hidden', Boolean(result));
844  loading.classList.add('hidden');
845  box.classList.toggle('hidden', !result);
846  if (!result) {
847    badge?.classList.add('hidden');
848    return;
849  }
850  $('#redlightChecks').innerHTML = (result.checks || []).map(check => {
851    const meta = REDLIGHT_STATUS_META[check.status] || REDLIGHT_STATUS_META.unknown;
852    const evidence = (check.evidence || []).map(item =>
853      `<a href="${escapeHtml(item.url)}" target="_blank" rel="noopener noreferrer"><i>${escapeHtml(item.source)}</i><span>${escapeHtml(item.label)}</span></a>`
854    ).join('');
855    const quotes = (check.bp_quotes || []).map(quote => `<li>${escapeHtml(quote)}</li>`).join('');
856    return `<article class="redlight-check ${meta.cls}"><header><i></i><strong>${escapeHtml(check.name)}</strong><span>${meta.label}</span></header><p>${escapeHtml(check.summary || '')}</p>${evidence ? `<div class="redlight-evidence">${evidence}</div>` : ''}${quotes ? `<ul class="redlight-quotes">${quotes}</ul>` : ''}</article>`;
857  }).join('');
858  const verdictCls = redlightVerdictClass(result.verdict);
859  const verdictEl = $('#redlightVerdict');
860  verdictEl.textContent = result.verdict || '—';
861  verdictEl.className = `investor-verdict-badge ${verdictCls}`;
862  const checkedAt = result.checked_at ? new Date(result.checked_at).toLocaleStr
862ing('zh-CN', { hour12: false }) : '';
863  $('#redlightMeta').textContent = [result.verdict_reason, result.data_note, checkedAt].filter(Boolean).join(' · ');
864  if (badge) {
865    badge.textContent = result.verdict || '';
866    badge.className = `investor-verdict-badge ${verdictCls}`;
867  }
868}
869
870function investorCostCard(data = {}) {
871  if (data.status !== 'ok') {
872    return `<article class="investor-q-card"><header><span>一问 · 成本</span><strong>要花多少钱、多久拿证</strong></header><div class="investor-q-empty"><strong>${escapeHtml(data.headline || '报告未包含成本估算')}</strong><p>${escapeHtml(data.message || '报告未包含成本估算,建议重新分析或人工补充')}</p></div></article>`;
873  }
874  const items = (data.items || []).map(item =>
875    `<li><span>${escapeHtml(item.label)}</span><b>${escapeHtml(item.low)}–${escapeHtml(item.high)}<i>基准 ${escapeHtml(item.base)}</i></b></li>`
876  ).join('');
877  const gaps = (data.gaps || []).map(gap => `<li>${escapeHtml(gap)}</li>`).join('');
878  return `<article class="investor-q-card"><header><span>一问 · 成本</span><strong>要花多少钱、多久拿证</strong></header><div class="investor-q-headline">${escapeHtml(data.headline)}</div><p class="investor-q-sub">
878${[data.profile_name, data.regulatory_route].filter(Boolean).map(escapeHtml).join(' · ')}</p><p class="investor-q-line"><b>周期估算</b><span>${escapeHtml(data.timeline || '待确认')}</span></p>${items ? `<ul class="investor-cost-items">${items}</ul>` : ''}${gaps ? `<div class="investor-q-gaps"><strong>数据缺口提示</strong><ul>${gaps}</ul></div>` : ''}<footer>${escapeHtml(data.confidence || '金额为立项阶段规划参数,非正式报价')}</footer></article>`;
879}
880
881function investorRiskCard(data = {}) {
882  const risks = (data.high_risks || []).map(risk =>
883    `<li><strong>${escapeHtml(risk.category)}</strong><p>${escapeHtml(risk.description)}</p>${risk.mitigation ? `<small>化解动作:${escapeHtml(risk.mitigation)}</small>` : ''}</li>`
884  ).join('');
885  const redlight = data.redlight;
886  const redlightHtml = redlight
887    ? `<div class="investor-q-redlight ${redlightVerdictClass(redlight.verdict)}"><b>${escapeHtml(redlight.verdict || '—')}</b><span>${(redlight.checks || []).map(check => `<i class="dot ${escapeHtml(check.status)}" title="${escapeHtml(check.name)}:${escapeHtml(check.summary || '')}"></i>`).join('')}</span><small>${escapeHtml(redlight.verdict_reason || '')}</small></div>`
888    : '<div class="investor-q-redlight none"><b>尚未进行红灯检查</b><small>点击上方「开始红灯检查 →」获取 PubMed / ClinicalTrials.gov 外部数据核验</small></div>';
889  const body = risks
890    ? `<ul class="investor-risk-list">${risks}</ul>`
891    : `<div class="investor-q-empty"><p>${escapeHtml(data.message || '模型未识别高风险项,仍须人工尽调')}</p></div>`;
892  return `<article class="investor-q-card"><header><span>二问 · 风险</span><strong>什么会让项目死掉</strong></header>${body}${redlightHtml}</article>`;
893}
894
895function investorMarketCard(data = {}) {
896  const sizing = (data.tam || data.sam || data.som)
897    ? `<ul class="investor-market-sizing">${[['TAM 总体市场', data.tam], ['SAM 可服务市场', data.sam], ['SOM 可获取市场', data.som]].filter(([, value]) => value).map(([label, value]) => `<li><span>${label}</span><b>${escapeHtml(value)}</b></li>`).join('')}</ul>`
898    : '';
899  const references = (data.references || []).map(item =>
900    `<a href="${escapeHtml(item.url)}" target="_blank" rel="noopener noreferrer"><i>${escapeHtml(item.source)}</i><span>${escapeHtml(item.label)} ${escapeHtml(item.value)}(${escapeHtml(item.period)})</span>${item.scope_note ? `<small>${escapeHtml(item.scope_note)}</small>` : ''}</a>`
901  ).join('');
902  const judgment = data.dimension_judgment ? `<p class="investor-q-line"><b>维度判断</b><span>${escapeHtml(data.dimension_judgment)}</span></p>` : '';
903  const message = data.message ? `<p class="investor-q-note">${escapeHtml(data.message)}</p>` : '';
904  const empty = (!sizing && !references && !judgment)
905    ? `<div class="investor-q-empty"><p>${escapeHtml(data.message || '报告未包含市场测算,建议重新分析或人工补充')}</p></div>`
906    : '';
907  return `<article class="investor-q-card"><header><span>三问 · 市场</span><strong>蛋糕有多大、能吃到多少</strong></header>${empty}${sizing}${judgment}${references ? `<div class="investor-market-refs"><strong>外部市场参考</strong>${references}</div>` : ''}${!empty ? message : ''}</article>`;
908}
909
910function renderInvestorBrief(brief) {
911  const grid = $('#investorBriefGrid');
912  if (!grid) return;
913  if (!brief) {
914    grid.innerHTML = '<div class="investor-brief-empty">投资决策三问暂未生成:请先在当前项目下完成 BP 全文分析。</div>';
915    return;
916  }
917  const renderers = { cost: investorCostCard, risk: investorRiskCard, market: investorMarketCard };
918  grid.innerHTML = (brief.questions || []).map(question => (renderers[question.key] || (() => ''))(question.data || {})).join('');
919}
920
921async function loadInvestorPanels() {
922  if (!currentInvestmentProject?.id) {
923    renderRedlightPanel(null);
924    renderInvestorBrief(null);
925    return;
926  }
927  const projectId = currentInvestmentProject.id;
928  try {
929    redlightResultData = await investmentApiRequest(`/api/investment/projects/${projectId}/redlight-check`);
930  } catch {
931    redlightResultData = null;
932  }
933  renderRedlightPanel(redlightResultData);
934  try {
935    investorBriefData = await investmentApiRequest(`/api/investment/projects/${projectId}/investor-brief`);
936  } catch {
937    investorBriefData = null;
938  }
939  renderInvestorBrief(investorBriefData);
940  try {
941    deepAssessmentData = await investmentApiRequest(`/api/investment/projects/${projectId}/deep-assessment`);
942  } catch {
943    deepAssessmentData = null;
944  }
945  assessmentDirty = false;
946  renderDeepAssessment(deepAssessmentData);
947  try {
948    exitSimData = await investmentApiRequest(`/api/investment/projects/${projectId}/exit-simulation`);
949  } catch {
950    exitSimData = null;
951  }
952  renderExitSimulation(exitSimData);
953  await loadMilestones();
954}
955
956async function runRedlightCheck() {
957  if (redlightRunning) return;
958  if (!currentInvestmentProject?.id) { showToast('请先选择投资项目'); return; }
959  redlightRunning = true;
960  $('#redlightEmpty')?.classList.add('hidden');
961  $('#redlightResult')?.classList.add('hidden');
962  $('#redlightLoading')?.classList.remove('hidden');
963  try {
964    redlightResultData = await investmentApiRequest(`/api/investment/projects/${currentInvestmentProject.id}/redlight-check`, { method: 'POST' });
965    renderRedlightPanel(redlightResultData);
966    // 风险列引用红灯结论,同步刷新三问卡片
967    try {
968      investorBriefData = await investmentApiRequest(`/api/investment/projects/${currentInvestmentProject.id}/investor-brief`);
969      renderInvestorBrief(investorBriefData);
970    } catch { /* 三问刷新失败不影响红灯结果展示 */ }
971  } catch (error) {
972    renderRedlightPanel(redlightResultData);
973    showToast(error.message || '红灯检查失败,请稍后重试');
974  } finally {
975    redlightRunning = false;
976    $('#redlightLoading')?.classList.add('hidden');
977  }
978}
979
980document.addEventListener('click', event => {
981  if (event.target.closest('#redlightRunButton') || event.target.closest('#redlightRerunButton')) runRedlightCheck();
982});
983
984// ============ 二级漏斗:五维雷达深度评估(AI预判 + 人工确认 + 赋能匹配) ============
985
986let deepAssessmentData = null;
987let assessmentDirty = false;
988let assessmentRunning = false;
989
990const ASSESS_CAPABILITY_MAP = {
991  science: { name: '科学内核', capabilities: ['专家与KOL网络', '联合研发'] },
992  asset: { name: '资产质量', capabilities: ['CMC/中试平台', '工程验证'] },
993  compliance: { name: '风险与合规', capabilities: ['注册申报服务', '法规团队'] },
994  market: { name: '市场与商业', capabilities: ['渠道资源', '集团市场网络'] },
995  team: { name: '团队与执行', capabilities: ['运营赋能', '产业导师'] },
996};
997
998function recomputeAssessment(data) {
999  if (!data?.dimensions) return;
1000  data.dimensions.forEach(dim => {
1001    const subs = dim.subitems || [];
1002    if (subs.length) dim.score = Math.round(subs.reduce((sum, sub) => sum + Number(sub.score || 0), 0) / subs.length * 10) / 10;
1003  });
1004  const weak = [...data.dimensions].sort((a, b) => a.score - b.score).slice(0, 2);
1005  const matched = weak.filter(item => ASSESS_CAPABILITY_MAP[item.key]);
1006  const avgWeak = weak.length ? weak.reduce((sum, item) => sum + item.score, 0) / weak.length : 0;
1007  const bonus = Math.max(0, Math.min(15, Math.round((3 - avgWeak) * 5)));
1008  let matchIndex, summary;
1009  if (matched.length === 2) { matchIndex = Math.min(100, 85 + bonus); summary = '短板恰为华卫强项,赋能价值高'; }
1010  else if (matched.length === 1) { matchIndex = Math.min(80, 60 + bonus); summary = '一项短板可由华卫能力补齐,另一项需外部资源'; }
1011  else { matchIndex = Math.min(50, 30 + bonus); summary = '短板超出华卫当前赋能范围,需谨慎评估孵化价值'; }
1012  data.capability_match = {
1013    weak_dimensions: weak.map(item => ({ key: item.key, name: item.name, score: item.score, capabilities: (ASSESS_CAPABILITY_MAP[item.key] || {}).capabilities || [] })),
1014    matched_capabilities: matched.map(item => ({ dimension: item.name, capabilities: ASSESS_CAPABILITY_MAP[item.key].capabilities })),
1015    match_index: matchIndex,
1016    summary,
1017  };
1018}
1019
1020function buildAssessRadarSvg(dimensions, weakKeys) {
1021  const cx = 160, cy = 118, R = 82, n = dimensions.length || 5;
1022  const angleOf = index => -Math.PI / 2 + index * 2 * Math.PI / n;
1023  const point = (index, radius) => [cx + radius * Math.cos(angleOf(index)), cy + radius * Math.sin(angleOf(index))];
1024  let rings = '';
1025  for (let level = 1; level <= 5; level++) {
1026    const points = dimensions.map((_, index) => point(index, R * level / 5).map(v => v.toFixed(1)).join(',')).join(' ');
1027    rings += `<polygon points="${points}" class="radar-ring${level === 5 ? ' outer' : ''}"/>`;
1028  }
1029  let axes = '', labels = '', dots = '';
1030  dimensions.forEach((dim, index) => {
1031    const weak = weakKeys.includes(dim.key);
1032    const [x, y] = point(index, R);
1033    axes += `<line x1="${cx}" y1="${cy}" x2="${x.toFixed(1)}" y2="${y.toFixed(1)}" class="radar-axis"/>`;
1034    const [lx, ly] = point(index, R + 22);
1035    labels += `<text x="${lx.toFixed(1)}" y="${(ly + 3).toFixed(1)}" class="radar-label${weak ? ' weak' : ''}" text-anchor="middle">${escapeHtml(dim.name)} ${escapeHtml(dim.score)}</text>`;
1036    const [dx, dy] = point(index, R * Math.max(0, Math.min(5, Number(dim.score) || 0)) / 5);
1037    dots += `<circle cx="${dx.toFixed(1)}" cy="${dy.toFixed(1)}" r="4" class="radar-dot${weak ? ' weak' : ''}"/>`;
1038  });
1039  const valuePoints = dimensions.map((dim, index) => point(index, R * Math.max(0, Math.min(5, Number(dim.score) || 0)) / 5).map(v => v.toFixed(1)).join(',')).join(' ');
1040  return `<svg viewBox="0 0 320 240" class="radar-svg" role="img" aria-label="五维雷达图">${rings}${axes}<polygon points="${valuePoints}" class="radar-value"/>${dots}${labels}</svg><div class="radar-scale"><span>刻度 1–5</span><span><i class="radar-legend-weak"></i>短板维度</span></div>`;
1041}
1042
1043function renderDeepAssessment(data) {
1044  const empty = $('#assessEmpty');
1045  const loading = $('#assessLoading');
1046  const box = $('#assessResult');
1047  const badge = $('#assessMatchBadge');
1048  if (!empty || !box) return;
1049  empty.classList.toggle('hidden', Boolean(data));
1050  loading.classList.add('hidden');
1051  box.classList.toggle('hidden', !data);
1052  if (!data) {
1053    badge?.classList.add('hidden');
1054    return;
1055  }
1056  const dimensions = data.dimensions || [];
1057  const match = data.capability_match || {};
1058  const weakKeys = (match.weak_dimensions || []).map(item => item.key);
1059  $('#assessRadar').innerHTML = buildAssessRadarSvg(dimensions, weakKeys);
1060  $('#assessDims').innerHTML = dimensions.map(dim => {
1061    const weak = weakKeys.includes(dim.key);
1062    const subs = (dim.subitems || []).map(sub => `<li><div class="assess-sub-head"><span>${escapeHtml(sub.name)}</span><div class="assess-adj">
1062<button type="button" data-assess-adjust="${escapeHtml(dim.key)}|${escapeHtml(sub.key)}|-1" ${Number(sub.score) <= 1 ? 'disabled' : ''} aria-label="减分">−</button><b>${escapeHtml(sub.score)}</b><button type="button" data-assess-adjust="${escapeHtml(dim.key)}|${escapeHtml(sub.key)}|1" ${Number(sub.score) >= 5 ? 'disabled' : ''} aria-label="加分">+</button></div></div>${sub.reason ? `<p>${escapeHtml(sub.reason)}</p>` : ''}${sub.evidence ? `<small>证据:${escapeHtml(sub.evidence)}</small>` : ''}</li>`).join('');
1063    return `<article class="assess-dim${weak ? ' weak' : ''}"><header><strong>${escapeHtml(dim.name)}${weak ? '<i>短板</i>' : ''}</strong><b>${escapeHtml(dim.score)}<small>/5</small></b></header><ul>${subs}</ul></article>`;
1064  }).join('');
1065  const weakRows = (match.weak_dimensions || []).map(item => `<li><span>${escapeHtml(item.name)} <b>${escapeHtml(item.score)}</b> 分</span><i>→</i><strong>${item.capabilities?.length ? escapeHtml(item.capabilities.join('、')) : '暂无对应强项'}</strong></li>`).join('');
1066  $('#assessMatch').innerHTML = `<div class="assess-match-index"><b>${escapeHtml(match.match_index ?? '—')}</b><span>赋能匹配度</span></div><div class="assess-match-detail"><strong>${escapeHtml(match.summary || '')}</strong><ul>${weakRows}</ul></div>`;
1067  const assessedAt = data.assessed_at ? new Date(data.assessed_at).toLocaleString('zh-CN', { hour12: false }) : '';
1068  const adjusted = data.updated_by === 'human_adjusted' && data.adjusted_at ? ` · 已由投资人调整确认(${new Date(data.adjusted_at).toLocaleString('zh-CN', { hour12: false })})` : '';
1069  $('#assessMeta').textContent = [data.product_type_note, data.data_note, assessedAt].filter(Boolean).join(' · ') + adjusted;
1070  if (badge) {
1071    badge.textContent = `赋能匹配 ${match.match_index ?? '—'}`;
1072    badge.className = `investor-verdict-badge ${(match.match_index ?? 0) >= 85 ? 'green' : (match.match_index ?? 0) >= 60 ? 'yellow' : 'red'}`;
1073  }
1074  $('#assessSaveButton')?.classList.toggle('hidden', !assessmentDirty);
1075}
1076
1077function adjustAssessmentScore(dimKey, subKey, delta) {
1078  if (!deepAssessmentData?.dimensions) return;
1079  const dim = deepAssessmentData.dimensions.find(item => item.key === dimKey);
1080  const sub = (dim?.subitems || []).find(item => item.key === subKey);
1081  if (!sub) return;
1082  const next = Math.max(1, Math.min(5, Number(sub.score || 3) + delta));
1083  if (next === sub.score) return;
1084  sub.score = next;
1085  assessmentDirty = true;
1086  recomputeAssessment(deepAssessmentData);
1087  deepAssessmentData.updated_by = 'human_adjusted_draft';
1088  renderDeepAssessment(deepAssessmentData);
1089}
1090
1091async function saveAssessmentAdjustment() {
1092  if (!deepAssessmentData || !currentInvestmentProject?.id) return;
1093  const button = $('#assessSaveButton');
1094  button.disabled = true;
1095  button.textContent = '保存中…';
1096  try {
1097    deepAssessmentData = await investmentApiRequest(`/api/investment/projects/${currentInvestmentProject.id}/deep-assessment`, {
1098      method: 'PUT',
1099      headers: { 'Content-Type': 'application/json' },
1100      body: JSON.stringify(deepAssessmentData),
1101    });
1102    assessmentDirty = false;
1103    renderDeepAssessment(deepAssessmentData);
1104    showToast('五维评估调整已保存');
1105  } catch (error) {
1106    showToast(error.message || '保存失败,请稍后重试');
1107  } finally {
1108    button.disabled = false;
1109    button.textContent = '保存调整';
1110  }
1111}
1112
1113async function runDeepAssessment() {
1114  if (assessmentRunning) return;
1115  if (!currentInvestmentProject?.id) { showToast('请先选择投资项目'); return; }
1116  assessmentRunning = true;
1117  $('#assessEmpty')?.classList.add('hidden');
1118  $('#assessResult')?.classList.add('hidden');
1119  $('#assessLoading')?.classList.remove('hidden');
1120  try {
1121    deepAssessmentData = await investmentApiRequest(`/api/investment/projects/${currentInvestmentProject.id}/deep-assessment`, { method: 'POST' });
1122    assessmentDirty = false;
1123    renderDeepAssessment(deepAssessmentData);
1124  } catch (error) {
1125    renderDeepAssessment(deepAssessmentData);
1126    showToast(error.message || '五维评估失败,请稍后重试');
1127  } finally {
1128    assessmentRunning = false;
1129    $('#assessLoading')?.classList.add('hidden');
1130  }
1131}
1132
1133document.addEventListener('click', event => {
1134  const adjuster = event.target.closest('[data-assess-adjust]');
1135  if (adjuster) {
1136    const [dimKey, subKey, delta] = adjuster.dataset.assessAdjust.split('|');
1137    adjustAssessmentScore(dimKey, subKey, Number(delta));
1138    return;
1139  }
1140  if (event.target.closest('#assessRunButton') || event.target.closest('#assessRerunButton')) runDeepAssessment();
1141  if (event.target.closest('#assessSaveButton')) saveAssessmentAdjustment();
1142});
1143
1144// ============ 三级漏斗:投决辅助与模拟(里程碑 + 退出路径 + 付款情景模拟器) ============
1145//
1146// 付款情景模拟器计算模型(简化情景模型,供讨论参考,不构成投资建议):
1147//   总投资 T = 首期投资额 + 里程碑付款总额(万元),即最大亏损(风险敞口)。
1148//   三个情景:
1149//     成功:概率 = 里程碑达成概率 p;退出回款 = 当前参考估值 × 退出估值倍数 × 股权占比
1150//     部分达成:概率 = (1−p)/2;按当前参考估值退出:回款 = 当前参考估值 × 股权占比
1151//     失败:概率 = (1−p)/2;血本无归:回款 = 0
1152//   现金流:第0å¹´ −首期,第1å¹´ −里程碑款(假设里ç¨
1152‹ç¢‘考核在首年内完成),第 N å¹´ +回款。
1153//   IRR:对 NPV(r) = Σ CF_t/(1+r)^t 在 (−0.99, 10] 区间二分反解;
1154//        期望 IRR 用期望回款 E[回款] = Σ情景概率×回款 代入同一现金流反解。
1155//   期望净收益 = E[回款] − T。
1156//   核对用例:总投 1000 万、4 年后回款 3000 万 → IRR = 3^(1/4)−1 ≈ 31.6%。
1157
1158let exitSimData = null;
1159let exitSimRunning = false;
1160
1161const PAY_SLIDER_DEFS = [
1162  { key: 'upfront', label: '首期投资额', min: 0, max: 2000, step: 50, unit: '万元' },
1163  { key: 'milestonePay', label: '里程碑付款总额', min: 0, max: 3000, step: 50, unit: '万元' },
1164  { key: 'equity', label: '合计股权占比', min: 1, max: 30, step: 1, unit: '%' },
1165  { key: 'probability', label: '里程碑达成概率', min: 10, max: 90, step: 5, unit: '%' },
1166  { key: 'multiple', label: '退出估值倍数', min: 1, max: 20, step: 0.5, unit: '×' },
1167  { key: 'years', label: '退出年限', min: 2, max: 8, step: 1, unit: '年' },
1168];
1169
1170function calcIrrBisect(cashflows) {
1171  // cashflows: [{year, amount}],二分反解 NPV(r)=0;无正收益时返回 null
1172  const npv = rate => cashflows.reduce((sum, cf) => sum + cf.amount / Math.pow(1 + rate, cf.year), 0);
1173  let low = -0.99, high = 10;
1174  if (npv(high) > 0) return 10; // 超过 1000% 视为封顶
1175  if (npv(low) < 0) return null; // 即使接近零利率也收不回,视为无可解
1176  for (let i = 0; i < 80; i++) {
1177    const mid = (low + high) / 2;
1178    if (npv(mid) > 0) low = mid; else high = mid;
1179  }
1180  return (low + high) / 2;
1181}
1182
1183function paySimDefaults(data) {
1184  const valuation = Number(data?.base_assumptions?.current_valuation_wan) || 3000;
1185  const uplifts = (data?.milestones || []).map(m => Number(m.valuation_uplift)).filter(v => v > 0).sort((a, b) => a - b);
1186  const probs = (data?.milestones || []).map(m => Number(m.success_probability)).filter(v => v > 0).sort((a, b) => a - b);
1187  const median = arr => arr.length ? arr[Math.floor(arr.length / 2)] : 0;
1188  return {
1189    upfront: Math.max(0, Math.min(2000, Math.round(valuation * 0.15 / 50) * 50)),
1190    milestonePay: Math.max(0, Math.min(3000, Math.round(valuation * 0.25 / 50) * 50)),
1191    equity: 10,
1192    probability: Math.max(10, Math.min(90, Math.round((median(probs) || 0.5) * 100 / 5) * 5)),
1193    multiple: Math.max(1, Math.min(20, median(uplifts) || 3)),
1194    years: Math.max(2, Math.min(8, Number(data?.base_assumptions?.years_to_exit) || 5)),
1195  };
1196}
1197
1198function initPaySliders(data) {
1199  const box = $('#paySliders');
1200  if (!box) return;
1201  const defaults = paySimDefaults(data);
1202  box.innerHTML = PAY_SLIDER_DEFS.map(def => {
1203    const value = defaults[def.key];
1204    return `<label class="pay-slider"><span>${def.label}<b id="payVal_${def.key}">${value}${def.unit}</b></span><input type="range" id="payIn_${def.key}" min="${def.min}" max="${def.max}" step="${def.step}" value="${value}" data-pay-key="${def.key}"></label>`;
1205  }).join('');
1206}
1207
1208function readPayParams() {
1209  const params = {};
1210  PAY_SLIDER_DEFS.forEach(def => {
1211    params[def.key] = Number($(`#payIn_${def.key}`)?.value ?? 0);
1212  });
1213  return params;
1214}
1215
1216function recalcPaySim() {
1217  if (!exitSimData) return;
1218  const p = readPayParams();
1219  const valuation = Number(exitSimData.base_assumptions?.current_valuation_wan) || 3000;
1220  const equityRatio = p.equity / 100;
1221  const totalInvest = p.upfront + p.milestonePay;              // 风险敞口 = 总投入(最大亏损)
1222  const successReturn = valuation * p.multiple * equityRatio;  // 成功:估值×倍数退出
1223  const partialReturn = valuation * equityRatio;               // 部分达成:按当前估值退出
1224  const pSuccess = p.probability / 100;
1225  const pPartial = (1 - pSuccess) / 2;
1226  const pFail = 1 - pSuccess - pPartial;
1227  const expectedReturn = pSuccess * successReturn + pPartial * partialReturn + pFail * 0;
1228  const expectedNet = expectedReturn - totalInvest;
1229  const cashflows = [
1230    { year: 0, amount: -p.upfront },
1231    { year: 1, amount: -p.milestonePay },
1232    { year: p.years, amount: expectedReturn },
1233  ];
1234  const irr = totalInvest > 0 ? calcIrrBisect(cashflows) : null;
1235  $('#payIrr').textContent = irr == null ? '无法收回' : `${(irr * 100).toFixed(1)}%`;
1236  $('#payIrr').className = irr == null ? 'bad' : irr >= 0.25 ? 'good' : irr >= 0.08 ? 'mid' : 'bad';
1237  $('#payExposure').textContent = `${totalInvest.toLocaleString()} 万元`;
1238  $('#payExpect').textContent = `${expectedNet >= 0 ? '+' : ''}${Math.round(expectedNet).toLocaleString()} 万元`;
1239  $('#payExpect').className = expectedNet >= 0 ? 'good' : 'bad';
1240  const fmtReturn = v => `${Math.round(v).toLocaleString()}万`;
1241  const scenarios = [
1242    { name: '成功退出', prob: pSuccess, ret: successReturn, cls: 'good', note: `估值 ×${p.multiple} 退出` },
1243    { name: '部分达成', prob: pPartial, ret: partialReturn, cls: 'mid', note: '按当前估值退出' },
1244    { name: '失败', prob: pFail, ret: 0, cls: 'bad', note: '血本无归' },
1245  ];
1246  $('#payScenarios').innerHTML = scenarios.map(sc => {
1247    const net = sc.ret - totalInvest;
1248    return `<div class="pay-scenario ${sc.cls}"><header><span>${sc.name} · 概率 ${(sc.prob * 100).toFixed(0)}%</span><b>回款 ${fmtReturn(sc.ret)}<i>(净${net >= 0 ? '+' : ''}${Math.round(net).toLocaleString()}万)</i></b></header><div class="pay-bar"><i style="width:${Math.max(2, sc.prob * 100)}%"></i></div><small>${escapeHtml(sc.note)}</small></div>`;
1249  }).join('');
1250}
1251
1252function renderExitSimulation(data) {
1253  const empty = $('#exitSimEmpty');
1254  const loading = $('#exitSimLoading');
1255  const box = $('#exitSimResult');
1256  const badge = $('#exitSimBadge');
1257  if (!empty || !box) return;
1258  empty.classList.toggle('hidden', Boolean(data));
1259  loading.classList.add('hidden');
1260  box.classList.toggle('hidden', !data);
1261  if (!data) {
1262    badge?.classList.add('hidden');
1263    return;
1264  }
1265  $('#milestoneTrack').innerHTML = (data.milestones || []).map((ms, index) => `
1266    <article class="milestone-card"><small>第${index + 1}步 · ${escapeHtml(ms.typical_timeline_months)}个月</small><strong>${escapeHtml(ms.name)}</strong><p>${escapeHtml(ms.stage_note || '')}</p><div><b>×${escapeHtml(ms.valuation_uplift)}</b><span>达成概率 ${Math.round(Number(ms.success_probability || 0) * 100)}%</span></div><i>${escapeHtml(ms.basis || '行业经验值')}</i></article>
1267    ${index < data.milestones.length - 1 ? '<em class="milestone-arrow">→</em>' : ''}`).join('');
1268  $('#exitPathGrid').innerHTML = (data.exit_paths || []).map(path => `
1269    <article class="exitpath-card"><header><b>${escapeHtml(path.path_type)}</b><i>${escapeHtml(path.evidence_level)}</i></header><strong>${escapeHtml(path.buyer_profile)}</strong><p>${escapeHtml(path.rationale || '')}</p>${path.typical_deal_note ? `<small>${escapeHtml(path.typical_deal_note)}</small>` : ''}</article>`).join('');
1270  initPaySliders(data);
1271  recalcPaySim();
1272  const simulatedAt = data.simulated_at ? new Date(data.simulated_at).toLocaleString('zh-CN', { hour12: false }) : '';
1273  $('#exitSimMeta').textContent = [data.product_type_note, `参考估值中值 ${Number(data.base_assumptions?.current_valuation_wan || 0).toLocaleString()} 万元(${data.base_assumptions?.note || ''})`, data.data_note, simulatedAt].filter(Boolean).join(' · ');
1274  if (badge) {
1275    badge.textContent = `参考估值 ${Number(data.base_assumptions?.current_valuation_wan || 0).toLocaleString()}万`;
1276    badge.className = 'investor-verdict-badge yellow';
1277  }
1278}
1279
1280async function runExitSimulation() {
1281  if (exitSimRunning) return;
1282  if (!currentInvestmentProject?.id) { showToast('请先选择投资项目'); return; }
1283  exitSimRunning = true;
1284  $('#exitSimEmpty')?.classList.add('hidden');
1285  $('#exitSimResult')?.classList.add('hidden');
1286  $('#exitSimLoading')?.classList.remove('hidden');
1287  try {
1288    exitSimData = await investmentApiRequest(`/api/investment/projects/${currentInvestmentProject.id}/exit-simulation`, { method: 'POST' });
1289    renderExitSimulation(exitSimData);
1290  } catch (error) {
1291    renderExitSimulation(exitSimData);
1292    showToast(error.message || '投决模拟失败,请稍后重试');
1293  } finally {
1294    exitSimRunning = false;
1295    $('#exitSimLoading')?.classList.add('hidden');
1296  }
1297}
1298
1299document.addEventListener('click', event => {
1300  if (event.target.closest('#exitSimRunButton') || event.target.closest('#exitSimRerunButton')) runExitSimulation();
1301});
1302
1303document.addEventListener('input', event => {
1304  const slider = event.target.closest('#paySliders input[type=range]');
1305  if (!slider) return;
1306  const def = PAY_SLIDER_DEFS.find(item => item.key === slider.dataset.payKey);
1307  const label = $(`#payVal_${slider.dataset.payKey}`);
1308  if (label && def) label.textContent = `${slider.value}${def.unit}`;
1309  recalcPaySim();
1310});
1311
1312// ============ 投后里程碑付款管理(AI 只出建议,批准/驳回永远人工) ============
1313
1314let milestoneData = null;
1315let milestoneBusy = false;
1316
1317const MILESTONE_STATUS_META = {
1318  pending: { label: '未启动', cls: 'pending' },
1319  in_progress: { label: '进行中', cls: 'in-progress' },
1320  submitted: { label: '待核验', cls: 'submitted' },
1321  verified: { label: '已核验', cls: 'verified' },
1322  flagged: { label: '黄牌复核', cls: 'flagged' },
1323  paid: { label: '已付款', cls: 'paid' },
1324  suspended: { label: '暂停评估', cls: 'suspended' },
1325};
1326
1327const MILESTONE_VERDICT_META = {
1328  achieved: { label: '核验通过', cls: 'achieved' },
1329  partial: { label: '部分符合 · 黄牌', cls: 'partial' },
1330  failed: { label: '严重不符', cls: 'failed' },
1331  unknown: { label: '未能核验', cls: 'unknown' },
1332};
1333
1334function renderMilestoneVerifyCard(item) {
1335  const verification = item.verification || {};
1336  if (!verification.verdict) return '';
1337  const meta = MILESTONE_VERDICT_META[verification.verdict] || MILESTONE_VERDICT_META.unknown;
1338  const recCls = verification.recommendation === '建议按约付款' ? 'good' : verification.recommendation === '建议暂停付款并评估中止' ? 'bad' : 'mid';
1339  const matched = (verification.matched || []).map(text => `<li>${escapeHtml(text)}</li>`).join('');
1340  const deviations = (verification.deviations || []).map(dev => `<li><span>${escapeHtml(dev.item)}</span><div><b>要求:${escapeHtml(dev.expected || '—')}</b><b>实际:${escapeHtml(dev.found || '—')}</b></div></li>`).join('');
1341  const fatal = (verification.fatal_signals || []).map(text => `<li>${escapeHtml(text)}</li>`).join('');
1342  const checkedAt = verification.checked_at ? new Date(verification.checked_at).toLocaleString('zh-CN', { hour12: false }) : '';
1343  const decided = item.decision === 'approved' || item.status === 'paid';
1344  return `<div class="mstone-verify ${meta.cls}">
1345    <header><b>${meta.label}</b><span>${escapeHtml(item.evidence_name || '')}${checkedAt ? ` · ${checkedAt}` : ''}</span></header>
1346    <div class="mstone-rec ${recCls}">${escapeHtml(verification.recommendation || '建议人工复核')}</div>
1347    ${matched ? `<div class="mstone-vblock"><strong>已满足项</strong><ul>${matched}</ul></div>` : ''}
1348    ${deviations ? `<div class="mstone-vblock deviations"><strong>偏差项(要求 vs 实际)</strong><ul>${deviations}</ul></div>` : ''}
1349    ${fatal ? `<div class="mstone-vblock fatal"><strong>致命信号</strong><ul>${fatal}</ul></div>` : ''}
1350    ${verification.reasoning ? `<p class="mstone-reasoning">${escapeHtml(verification.reasoning)}</p>` : ''}
1351    ${decided
1352      ? '<div class="mstone-decided">已批准付款(系统内状态记录,实际划款须走财务流程)</div>'
1353      : `<div class="mstone-decide"><button class="analyze-button" type="button" data-mstone-approve="${escapeHtml(item.id)}">批准付款</button><button class="outline-button" type="button" data-mstone-reject="${escapeHtml(item.id)}">驳回 / 发起人工复核</button></div>`}
1354  </div>`;
1355}
1356
1357function renderMilestones(data) {
1358  const empty = $('#milestoneEmpty');
1359  const content = $('#milestoneContent');
1360  if (!empty || !content) return;
1361  const items = data?.milestones || [];
1362  empty.classList.toggle('hidden', items.length > 0);
1363  content.classList.toggle('hidden', items.length === 0);
1364  if (!items.length) return;
1365  const summary = data.summary || {};
1366  $('#milestoneSummary').innerHTML = [
1367    ['总付款额', `${Number(summary.total_payment_wan || 0).toLocaleString()} 万`],
1368    ['已批准', `${summary.approved_count || 0} 笔`],
1369    ['待核验', `${summary.pending_verify_count || 0} 笔`],
1370    ['黄牌', `${summary.flagged_count || 0} 笔`],
1371  ].map(([label, value]) => `<article><small>${label}</small><b>${value}</b></article>`).join('');
1372  $('#mstoneList').innerHTML = items.map(item => {
1373    const meta = MILESTONE_STATUS_META[item.status] || MILESTONE_STATUS_META.pending;
1374    const canUpload = item.status !== 'paid';
1375    return `<article class="mstone-row" data-mid="${escapeHtml(item.id)}">
1376      <header>
1377        <span class="mstone-seq">${String(item.seq || 0).padStart(2, '0')}</span>
1378        <div class="mstone-head-main"><strong>${escapeHtml(item.name)}</strong>${item.criteria ? `<p>${escapeHtml(item.criteria)}</p>` : ''}</div>
1379        <div class="mstone-meta"><small>计划 ${escapeHtml(item.planned_months || '—')} 个月</small><label>付款 <input type="number" min="0" step="10" value="${escapeHtml(item.payment_wan ?? 0)}" data-mstone-pay="${escapeHtml(item.id)}" ${item.status === 'paid' ? 'disabled' : ''}> 万</label></div>
1380        <span class="mstone-status ${meta.cls}">${meta.label}</span>
1381      </header>
1382      ${canUpload ? `<div class="mstone-actions"><label class="outline-button mstone-upload">上传达成证据 →<input type="file" class="hidden" data-mstone-upload="${escapeHtml(item.id)}" accept=".pdf,.docx,.txt"></label><small>支持 PDF / Word / TXT,AI 核对后仅给出付款建议</small></div>` : ''}
1383      ${renderMilestoneVerifyCard(item)}
1384    </article>`;
1385  }).join('');
1386}
1387
1388async function loadMilestones() {
1389  if (!currentInvestmentProject?.id) {
1390    milestoneData = null;
1391    renderMilestones(null);
1392    return;
1393  }
1394  try {
1395    milestoneData = await investmentApiRequest(`/api/investment/projects/${currentInvestmentProject.id}/milestones`);
1396  } catch {
1397    milestoneData = null;
1398  }
1399  renderMilestones(milestoneData);
1400}
1401
1402async function importMilestonesFromSimulation() {
1403  if (milestoneBusy || !currentInvestmentProject?.id) return;
1404  milestoneBusy = true;
1405  $('#milestoneLoadingText').textContent = '正在从投决模拟导入里程碑…';
1406  $('#milestoneLoading')?.classList.remove('hidden');
1407  try {
1408    const result = await investmentApiRequest(`/api/investment/projects/${currentInvestmentProject.id}/milestones/import-simulation`, { method: 'POST' });
1409    milestoneData = result;
1410    renderMilestones(milestoneData);
1411    if (result.note) showToast(result.note);
1412  } catch (error) {
1413    showToast(error.message || '导入失败,请先在「◎ 投决模拟」完成推演');
1414  } finally {
1415    milestoneBusy = false;
1416    $('#milestoneLoading')?.classList.add('hidden');
1417  }
1418}
1419
1420async function addMilestoneManually() {
1421  if (!currentInvestmentProject?.id) { showToast('请先选择投资项目'); return; }
1422  const name = window.prompt('里程碑名称(如:注册检验(型检)通过)');
1423  if (!name || !name.trim()) return;
1424  const criteria = window.prompt('达标标准(逐项写清关键指标,如:样本量≥200例;灵敏度≥90%;特异度≥85%)') || '';
1425  const months = Number(window.prompt('计划周期(月)', '12') || 0);
1426  const payment = Number(window.prompt('关联付款额(万元)', '100') || 0);
1427  try {
1428    const result = await investmentApiRequest(`/api/investment/projects/${currentInvestmentProject.id}/milestones`, {
1429      method: 'POST',
1430      headers: { 'Content-Type': 'application/json' },
1431      body: JSON.stringify({ name: name.trim(), criteria, planned_months: months, payment_wan: payment }),
1432    });
1433    milestoneData = result;
1434    renderMilestones(milestoneData);
1435  } catch (error) {
1436    showToast(error.message || '添加失败');
1437  }
1438}
1439
1440async function updateMilestoneField(mid, payload, toastMessage) {
1441  try {
1442    const result = await investmentApiRequest(`/api/investment/milestones/${mid}`, {
1443      method: 'PUT',
1444      headers: { 'Content-Type': 'application/json' },
1445      body: JSON.stringify(payload),
1446    });
1447    milestoneData = result;
1448    renderMilestones(milestoneData);
1449    if (toastMessage) showToast(toastMessage);
1450  } catch (error) {
1451    showToast(error.message || '保存失败');
1452  }
1453}
1454
1455async function uploadMilestoneEvidence(mid, file) {
1456  if (milestoneBusy || !file) return;
1457  milestoneBusy = true;
1458  $('#milestoneLoadingText').textContent = 'AI 正在核对证据指标…';
1459  $('#milestoneLoading')?.classList.remove('hidden');
1460  try {
1461    const form = new FormData();
1462    form.append('file', file);
1463    const result = await investmentApiRequest(`/api/investment/milestones/${mid}/verify`, { method: 'POST', body: form });
1464    milestoneData = { milestones: null, summary: null };
1465    await loadMilestones(); // verify 返回单条,统一拉全量保证汇总一致
1466    if (result?.degraded) showToast(result.note || '证据已存档,
1466需人工复核');
1467  } catch (error) {
1468    showToast(error.message || '证据核验失败,请稍后重试');
1469  } finally {
1470    milestoneBusy = false;
1471    $('#milestoneLoading')?.classList.add('hidden');
1472  }
1473}
1474
1475document.addEventListener('click', event => {
1476  if (event.target.closest('#milestoneImportButton')) { importMilestonesFromSimulation(); return; }
1477  if (event.target.closest('#milestoneAddButton') || event.target.closest('#milestoneAddMoreButton')) { addMilestoneManually(); return; }
1478  const approve = event.target.closest('[data-mstone-approve]');
1479  if (approve) {
1480    updateMilestoneField(approve.dataset.mstoneApprove, { status: 'paid', decision: 'approved' }, '已标记批准付款——仅为系统内状态记录,实际划款须走财务流程');
1481    return;
1482  }
1483  const reject = event.target.closest('[data-mstone-reject]');
1484  if (reject) {
1485    updateMilestoneField(reject.dataset.mstoneReject, { decision: 'under_review' }, '已转为人工复核,付款流程暂停');
1486  }
1487});
1488
1489document.addEventListener('change', event => {
1490  const upload = event.target.closest('[data-mstone-upload]');
1491  if (upload) {
1492    const file = upload.files && upload.files[0];
1493    if (file) uploadMilestoneEvidence(upload.dataset.mstoneUpload, file);
1494    upload.value = '';
1495    return;
1496  }
1497  const pay = event.target.closest('[data-mstone-pay]');
1498  if (pay) {
1499    updateMilestoneField(pay.dataset.mstonePay, { payment_wan: Number(pay.value || 0) });
1500  }
1501});
1502
1503function investmentAssistantStorageKey() {
1504  // v2 starts a clean UTF-8 conversation store; earlier replies may have been saved after ANSI mis-decoding.
1505  return currentInvestmentProject?.id ? `huawai_bp_assistant_utf8_v2_${currentInvestmentProject.id}` : '';
1506}
1507
1508function loadInvestmentAssistantConversation() {
1509  investmentAssistantMessages = [];
1510  const key = investmentAssistantStorageKey();
1511  if (!key) return;
1512  try {
1513    const saved = JSON.parse(localStorage.getItem(key) || '[]');
1514    if (Array.isArray(saved)) investmentAssistantMessages = saved.slice(-30);
1515  } catch { investmentAssistantMessages = []; }
1516}
1517
1518function saveInvestmentAssistantConversation() {
1519  const key = investmentAssistantStorageKey();
1520  if (!key) return;
1521  try { localStorage.setItem(key, JSON.stringify(investmentAssistantMessages.slice(-30))); } catch { /* device storage may be unavailable */ }
1522}
1523
1524function compactInvestmentAssistantContext() {
1525  const report = currentInvestmentAnalysis || {};
1526  const dimensions = (Array.isArray(report.dimensions) ? report.dimensions : []).map(item => ({
1527    name:item.name,score:item.score,potential_score:item.potential_score,score_range:item.score_range,evidence_coverage:item.evidence_coverage,score_reason:item.score_reason || '',potential_judgment:item.potential_judgment || '',confidence:item.confidence || item.score_audit?.confidence || '',validation_tasks:(item.validation_tasks || []).slice(0,4),strengths:(item.strengths || []).slice(0,4),weaknesses:(item.weaknesses || []).slice(0,4),
1528    evidence:item.evidence || '',criteria:(item.score_audit?.criteria || []).slice(0,4).map(criterion => ({criterion:criterion.criterion,status:criterion.status,reason:criterion.reason,evidence_quote:criterion.evidence_quote,evidence_locator:criterion.evidence_locator,data_status:criterion.data_status})),
1529    benchmark_evidence:item.benchmark_evidence ? {score_explanation:item.benchmark_evidence.score_explanation,score_label:item.benchmark_evidence.score_label,good_standard:(item.benchmark_evidence.good_standard || []).slice(0,4),current_gap:(item.benchmark_evidence.current_gap || []).slice(0,4),external_references:(item.benchmark_evidence.external_references || []).slice(0,5).map(reference => ({label:reference.label,value:reference.value,period:reference.period,source:reference.source,url:reference.url,scope_note:reference.scope_note}))} : {}
1530  }));
1531  const workbench = report.decision_workbench || {};
1532  const references = report.reference_data || {};
1533  return {
1534    project:{id:currentInvestmentProject?.id || '',name:currentInvestmentProject?.name || report.project_name || '未命名项目',source_type:currentInvestmentProject?.source_type || '',track:currentInvestmentProject?.track || '',stage:currentInvestmentProject?.stage || '',owner:currentInvestmentProject?.owner || '',notes:currentInvestmentProject?.notes || '',bp_file:currentInvestmentProject?.latest_file_name || '',report_id:currentInvestmentReportId || ''},
1535    analysis:{overall_score:report.overall_score,potential_score:report.potential_score,score_range:report.score_range,evidence_coverage:report.evidence_coverage,evidence_confidence:report.evidence_confidence,recommendation:report.recommendation,summary:report.summary,dimensions,risks:(report.risks || []).slice(0,12),product_identity:workbench.product_identity || {},decision_gates:(workbench.gates || []).slice(0,8),deepening_questions:(workbench.questions || []).slice(0,10),registration_cost_estimate:workbench.registration_cost_estimate || {},reference_data:{as_of:references.as_of,product_scope:references.product_scope,market:(references.market || []).slice(0,8),registration_costs:(references.registration_costs || []).slice(0,8),official_databases:(references.official_databases || []).slice(0,8)},analysis_audit:report.analysis_audit || {}},
1536    current_scenario:{market_target_units:Number($('#marketTargetUnits')?.value || 0
1536),market_annual_usage:Number($('#marketAnnualUsage')?.value || 0),market_unit_price:Number($('#marketUnitPrice')?.value || 0),market_reach_rate:Number($('#marketReachRate')?.value || 0),market_share_rate:Number($('#marketShareRate')?.value || 0),registration_budget_total:$('#registrationBudgetTotal')?.textContent || ''}
1537  };
1538}
1539
1540function investmentAssistantAnswerHtml(value) {
1541  const safe = escapeHtml(value || '本轮没有形成可展示的回答。');
1542  return `<p>${safe.replace(/\n{2,}/g,'</p><p>').replace(/\n/g,'<br>')}</p>`;
1543}
1544
1545function renderInvestmentAssistantMessages() {
1546  const container = $('#investmentAssistantMessages');
1547  if (!container) return;
1548  if (!investmentAssistantMessages.length) {
1549    const score = currentInvestmentAnalysis?.overall_score;
1550    const dimensions = currentInvestmentAnalysis?.dimensions?.length || 0;
1551    container.innerHTML = `<div class="investment-assistant-welcome"><i>M</i><div><strong>我已经读过这个项目的价值研判结果</strong><p>当前载入九维项目潜力${score == null ? '' : ` ${escapeHtml(score)} / 10`}、各维判断区间、BP证据覆盖度、风险清单和注册成本规划。您可以直接追问,不必重新描述项目。</p><span>建议先问:“这个项目真正的商业价值在哪里?”或“如果进入尽调,未来30天先验证什么?”</span></div></div>`;
1552    return;
1553  }
1554  container.innerHTML = investmentAssistantMessages.map(message => {
1555    if (message.role === 'user') return `<article class="investment-assistant-message user"><header><span>我</span><small>${escapeHtml(message.time || '')}</small></header><div>${investmentAssistantAnswerHtml(message.content)}</div></article>`;
1556    const result = message.content || {};
1557    const points = (result.key_points || []).map(point => `<li><strong>${escapeHtml(point.title || '关键判断')}</strong><p>${escapeHtml(point.detail || '')}</p><span>${escapeHtml(point.basis || 'AI判断')}</span></li>`).join('');
1558    const evidence = (result.evidence_refs || []).map(item => `<article><span>${escapeHtml(item.status || '待核验')}</span><strong>${escapeHtml(item.label || '依据')}</strong><p>${escapeHtml(item.quote || '')}</p><small>${escapeHtml(item.locator || '位置待确认')}</small></article>`).join('');
1559    const tasks = (result.verification_tasks || []).map(item => `<li><i>${escapeHtml(item.priority || '任务')}</i><span><strong>${escapeHtml(item.task || '')}</strong><small>${escapeHtml(item.source || '来源待明确')}</small></span></li>`).join('');
1560    const followups = (result.followup_questions || []).map(question => `<button type="button" data-bp-assistant-question="${escapeHtml(question)}">${escapeHtml(question)}</button>`).join('');
1561    const errorClass = message.error ? ' error' : '';
1562    return `<article class="investment-assistant-message assistant${errorClass}"><header><span>M</span><small>${escapeHtml(message.model || (message.error ? '连接提示' : 'DeepSeek'))} · ${escapeHtml(message.time || '')}</small></header><div class="investment-assistant-answer">${investmentAssistantAnswerHtml(result.answer || String(result))}${points ? `<ul class="investment-assistant-points">${points}</ul>` : ''}${evidence ? `<div class="investment-assistant-evidence"><strong>本轮引用依据</strong><div>${evidence}</div></div>` : ''}${tasks ? `<div class="investment-assistant-tasks"><strong>建议核验任务</strong><ul>${tasks}</ul></div>` : ''}${result.boundary ? `<p class="investment-assistant-boundary">边界:${escapeHtml(result.boundary)}</p>` : ''}${followups ? `<div class="investment-assistant-followups"><span>继续追问</span>${followups}</div>` : ''}</div></article>`;
1563  }).join('');
1564  requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
1565}
1566
1567function refreshInvestmentAssistant() {
1568  const panel = $('#investmentAssistant');
1569  if (!panel) return;
1570  const ready = Boolean(currentInvestmentProject && currentInvestmentAnalysis);
1571  panel.classList.toggle('hidden', !ready);
1572  if (!ready) return;
1573  $('#investmentAssistantProject').textContent = currentInvestmentProject.name || '当前项目';
1574  const audit = currentInvestmentAnalysis.analysis_audit || {};
1575  $('#investmentAssistantContext').textContent = `九维 ${currentInvestmentAnalysis.overall_score ?? '—'} / 10 · ${currentInvestmentProject.latest_file_name || '当前分析报告'}`;
1576  $('#investmentAssistantAudit').textContent = audit.model ? `当前报告:${audit.model} · 助理回答会单独记录本次模型` : '当前报告已载入 · 助理回答会标注数据边界';
1577  renderInvestmentAssistantMessages();
1578}
1579
1580async function sendInvestmentAssistantQuestion(preset = '') {
1581  if (investmentAssistantBusy) return;
1582  if (!currentInvestmentProject || !currentInvestmentAnalysis) { showToast('请先选择项目并完成BP分析'); return; }
1583  const input = $('#investmentAssistantInput');
1584  const question = String(preset || input.value || '').trim();
1585  if (!question) { input.focus(); return; }
1586  const history = investmentAssistantMessages.slice(-10).map(message => ({role:message.role,content:message.role === 'user' ? message.content : message.content?.answer || ''}));
1587  investmentAssistantMessages.push({role:'user',content:question,time:new Date().toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit'})});
1588  input.value = '';
1589  investmentAssistantBusy = true;
1590  $('#investmentAssistantSend').disabled = true;
1591  $('#investmentAssistantThinking').classList.remove('hidden');
1592  saveInvestmentAssistantConversation();
1593  renderInvestmentAssistantMessages();
1594  try {
1595    const assistantUrl = window.location.protocol === 'file:' ? 'http://127.0.0.1:8787/api/investment/assistant' : '/api/investment/assistant';
1596    const response = await fetch(assistantUrl,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question,history,context:compactInvestmentAssistantContext()})});
1597    const payload = await response.json();
1598    if (!response.ok) throw new Error(payload.error || payload.detail || '项目分析助理暂时无法回答');
1599    investmentAssistantMessages.push({role:'assistant',content:payload,time:new Date().toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit'}),model:payload._audit?.model || 'DeepSeek'});
1600    $('#investmentAssistantAudit').textContent = payload._audit?.request_id ? `本轮已实际调用 ${payload._audit.model || 'DeepSeek'} · 记录 ${String(payload._audit.request_id).slice(0,12)}…` : '本轮回答已完成 · 关键事实仍需人工核验';
1601  } catch (error) {
1602    investmentAssistantMessages.push({role:'assistant',error:true,content:{answer:error.message,boundary:'当前问题没有生成AI结论,请检查统一平台和DeepSeek配置后重试。'},time:new Date().toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit'})});
1603  } finally {
1604    investmentAssistantBusy = false;
1605    $('#investmentAssistantSend').disabled = false;
1606    $('#investmentAssistantThinking').classList.add('hidden');
1607    saveInvestmentAssistantConversation();
1608    renderInvestmentAssistantMessages();
1609    input.focus();
1610  }
1611}
1612
1613function clearInvestmentAssistantConversation() {
1614  if (!currentInvestmentProject) return;
1615  investmentAssistantMessages = [];
1616  const key = investmentAssistantStorageKey();
1617  if (key) localStorage.removeItem(key);
1618  renderInvestmentAssistantMessages();
1619  showToast('当前项目讨论已清空');
1620}
1621
1622function fallbackScoreCriteria(item) {
1623  const hasEvidence = item.evidence && item.evidence !== 'BP中未提及相关内容';
1624  return [{
1625    criterion:'BP材料是否提供相关依据',max_points:10,awarded_points:hasEvidence ? Number(item.score)||5 : null,
1626    status:hasEvidence ? '待结构化核验' : '待验证',
1627    reason:hasEvidence ? '当前是历史报告或摘要初筛,建议重新上传文件生成逐项引文。' : 'BP未描述该项,当前无法判断;未知不等于项目没有,不因此直接记零分。',
1628    evidence_quote:hasEvidence ? item.evidence : '',evidence_locator:'历史报告未记录位置',
1629    data_status:hasEvidence ? '有材料陈述,尚未完成逐项证据审核' : '未知,转入尽调核验'
1630  }];
1631}
1632
1633function renderDimensionEvidence(item, index, shouldScroll = false) {
1634  if (!item) return;
1635  const audit = item.score_audit || {};
1636  const criteria = Array.isArray(audit.criteria) && audit.criteria.length ? audit.criteria : fallbackScoreCriteria(item);
1637  const modelScore = item.model_score_before_audit;
1638  const strengths = Array.isArray(item.strengths) ? item.strengths : [];
1639  const weaknesses = Array.isArray(item.weaknesses) ? item.weaknesses : [];
1640  const criteriaHtml = criteria.map(criterion => {
1641    const statusClass = criterion.status === '可追溯线索' ? 'strong' : criterion.status === '量化披露' ? 'quantified' : ['缺失','待验证'].includes(criterion.status) ? 'missing' : 'statement';
1642    const quote = criterion.evidence_quote
1643      ? `<blockquote><b>${escapeHtml(criterion.evidence_locator || '原文位置待确认')}</b><span>“${escapeHtml(criterion.evidence_quote)}”</span></blockquote>`
1644      : '<blockquote class="no-evidence"><span>BP未描述该项;未知不等于项目没有</span></blockquote>';
1645    const awarded = criterion.awarded_points === null || criterion.awarded_points === undefined ? '待验证' : escapeHtml(criterion.awarded_points);
1646    return `<article class="evidence-criterion ${statusClass}"><header><span><strong>${escapeHtml(criterion.criterion)}</strong><small>${escapeHtml(criterion.data_status || '')}</small></span><b>${awarded} / ${escapeHtml(criterion.max_points ?? '—')}分</b></header>${quote}<p>${escapeHtml(criterion.reason || '')}</p><i>${escapeHtml(criterion.status || '待核验')}</i></article>`;
1647  }).join('');
1648  const positiveHtml = strengths.length ? strengths.map(text => `<li>${escapeHtml(text)}</li>`).join('') : '<li>当前尚未形成有充分证据支撑的正向价值判断</li>';
1649  const negativeHtml = weaknesses.length ? weaknesses.map(text => `<li>${escapeHtml(text)}</li>`).join('') : '<li>暂无明确负面事实;信息缺少的部分列为待验证</li>';
1650  const benchmark = item.benchmark_evidence || {};
1651  const externalReferences = Array.isArray(benchmark.external_references) ? benchmark.external_references : [];
1652  const goodStandard = Array.isArray(benchmark.good_standard) ? benchmark.good_standard : [];
1653  const currentGap = Array.isArray(benchmark.current_gap) ? benchmark.current_gap : [];
1654  const referenceHtml = externalReferences.length ? externalReferences.map(reference => {
1655    const value = reference.value ? `<b>${escapeHtml(reference.value)}</b>` : '<b>官方核验入口</b>';
1656    const period = reference.period ? `<em>${escapeHtml(reference.period)}</em>` : '';
1657    const note = reference.scope_note || reference.use || reference.basis || '用于行业基准核验,不直接替代本项目事实';
1658    const source = reference.source || reference.source_status || '公开来源';
1659    return `<article class="dimension-reference-card"><header><span>${escapeHtml(reference.source_status || '可追溯来源')}</span>
1659${period}</header><strong>${escapeHtml(reference.label || '行业参考')}</strong>${value}<p>${escapeHtml(note)}</p>${reference.url ? `<a href="${escapeHtml(reference.url)}" target="_blank" rel="noopener">查看来源 · ${escapeHtml(source)} ↗</a>` : `<small>${escapeHtml(source)}</small>`}</article>`;
1660  }).join('') : '<div class="dimension-reference-empty"><strong>尚无与本项目直接匹配的外部数字</strong><p>系统不会用大行业数据凑答案;需要按产品名称、适应症和目标客户继续检索。</p></div>';
1661  const goodStandardHtml = goodStandard.length ? goodStandard.map(text => `<li>${escapeHtml(text)}</li>`).join('') : '<li>需要由对应专业负责人确定本赛道的达标口径。</li>';
1662  const currentGapHtml = currentGap.length ? currentGap.map(text => `<li>${escapeHtml(text)}</li>`).join('') : '<li>当前未识别出明确差距,仍需对照原始材料和外部来源复核。</li>';
1663  const panel = $('#dimensionEvidencePanel');
1664  if (!panel) return;
1665  const range = item.score_range || {low:item.score,high:item.score};
1666  const coverage = Number(item.evidence_coverage ?? audit.evidence_coverage ?? 0);
1667  panel.innerHTML = `<div class="dimension-evidence-head"><div><span>当前维度 · 项目潜力</span><h3>${escapeHtml(item.name)} · ${escapeHtml(item.score)}分</h3><p>${escapeHtml(item.score_reason || '这是对项目价值的初步判断;BP未描述的内容将作为待验证项,不直接记零分。')}</p></div><div><strong>证据置信度 ${escapeHtml(audit.confidence || '待核验')}</strong><small>合理判断区间 ${escapeHtml(range.low)}—${escapeHtml(range.high)}分 · BP证据覆盖${coverage}%</small><b>${escapeHtml(audit.method || '项目潜力评分 + 证据置信度双轨研判')}</b><button class="dimension-back-button" type="button">↑ 返回九维研判</button></div></div><section class="dimension-score-explanation"><span>为什么是这个分</span><strong>${escapeHtml(benchmark.score_explanation || item.potential_judgment || '当前判断来自BP事实、项目价值推理与证据置信度综合结果。')}</strong><p>${escapeHtml(benchmark.score_label || '分数用于决定是否值得继续验证,不代表最终投资结论。')}</p></section><div class="evidence-criteria-grid">${criteriaHtml}</div><div class="dimension-reason-columns"><article><strong>支持项目价值的信号</strong><ul>${positiveHtml}</ul></article><article><strong>风险与待验证假设</strong><ul>${negativeHtml}</ul></article></div><section class="dimension-network-evidence"><header><div><span>网络与行业参考</span><strong>每条数据都显示来源、日期与适用口径</strong></div><b>${externalReferences.length} 条可追溯依据</b></header><div>${referenceHtml}</div></section><div class="dimension-benchmark-columns"><article><span>好项目应该是什么样</span><ul>${goodStandardHtml}</ul></article><article><span>本项目距离达标还差什么</span><ul>${currentGapHtml}</ul></article></div><p class="dimension-source-boundary">${escapeHtml(benchmark.source_boundary || '外部资料只提供行业基准和核验口径,不替代本项目BP事实。')}</p><p class="dimension-boundary">${escapeHtml(audit.boundary || '项目潜力分用于判断是否值得继续验证;BP证据覆盖度决定结论置信度,材料缺项不等同项目缺陷。')}</p>`;
1668  $$('.dimension-score-card').forEach((card,cardIndex) => card.classList.toggle('active', cardIndex === index));
1669  if (shouldScroll) {
1670    panel.classList.remove('focus-pulse');
1671    requestAnimationFrame(() => {
1672      panel.scrollIntoView({behavior:'smooth',block:'start'});
1673      panel.focus({preventScroll:true});
1674      panel.classList.add('focus-pulse');
1675      setTimeout(() => panel.classList.remove('focus-pulse'), 1200);
1676    });
1677  }
1678}
1679
1680function startBpAnalysisAnimation() {
1681  const loader = $('#bpAnalysisLoader');
1682  const steps = [
1683    ['正在读取 BP 全文', '解析 PDF、Word 或 PPT 的正文与页面结构'],
1684    ['正在提取项目证据', '识别产品、临床需求、注册路径、团队与融资信息'],
1685    ['正在调用 DeepSeek', '将文件全文送入九维专业判断,并保留实际调用记录'],
1686    ['正在核对数据边界', '区分 BP 原文、AI 判断与带来源的外部参考'],
1687    ['正在生成评测报告', '整理评分、关键风险和下一轮高价值问题']
1688  ];
1689  let index = 0;
1690  let progress = 8;
1691  clearInterval(bpLoaderTimer);
1692  loader.classList.remove('hidden', 'failed', 'done');
1693  $('#bpLoaderProgress').style.width = `${progress}%`;
1694  const update = () => {
1695    $('#bpLoaderTitle').textContent = steps[index][0];
1696    $('#bpLoaderStep').textContent = steps[index][1];
1697    $$('#bpLoaderStages li').forEach((item, itemIndex) => {
1698      item.classList.toggle('active', itemIndex === index);
1699      item.classList.toggle('complete', itemIndex < index);
1700    });
1701  };
1702  update();
1703  bpLoaderTimer = setInterval(() => {
1704    progress = Math.min(92, progress + 5 + Math.random() * 7);
1705    $('#bpLoaderProgress').style.width = `${progress}%`;
1706    if (index < steps.length - 1 && progress >= 18 + index * 17) index += 1;
1707    update();
1708  }, 1800);
1709}
1710
1711function finishBpAnalysisAnimation(success, message) {
1712  clearInterval(bpLoaderTimer);
1713  bpLoaderTimer = null;
1714  const loader = $('#bpAnalysisLoader');
1715  loader.classList.toggle('done', success);
1716  loader.classList.toggle('failed', !success);
1717  $('#bpLoaderProgress').style.width = '100%';
1718  $('#bpLoaderTitle').textContent = success ? '分析完成' : '分析未完成';
1719  $('#bpLoaderStep').textContent = message || (success ? '已生成报告和调用记录' : '请检查服务后重试');
1720  $$('#bpLoaderStages li').forEach(item => item.classList.toggle('complete', success));
1721  setTimeout(() => loader.classList.add('hidden'), success ? 900 : 2600);
1722}
1723
1724function defaultDecisionWorkbench(productScope = '医疗器械(细分类别待确认)') {
1725  const isIvd = /IVD|试剂|体外诊断/i.test(productScope);
1726  const isSoftware = /软件|AI/i.test(productScope);
1727  return {
1728    product_identity: {
1729      candidate_name: isIvd ? '呼吸道病原体检测试剂(候选通用名称)' : isSoftware ? '医疗软件(通用名称待确认)' : '产品通用名称待确认',
1730      product_scope: productScope,
1731      intended_use: '需从BP原文确认目标人群、样本/部位、使用场景和临床目的',
1732      regulatory_hypothesis: isIvd ? '体外诊断试剂;第二/三类待分类界定' : '医疗器械类别与分类编码待法规负责人确认',
1733      confidence: '初步推断'
1734    },
1735    gates: [
1736      {name:'产品定义',status:'待确认',note:'锁定通用名称与预期用途'},
1737      {name:'临床价值',status:'待证据',note:'核验未满足需求与获益终点'},
1738      {name:'注册路径',status:'待法规',note:'确认分类、单元和临床路径'},
1739      {name:'同类产品',status:'待检索',note:'形成官方注册证清单'},
1740      {name:'商业模型',status:'待测算',note:'用真实数量与价格测算'}
1741    ],
1742    comparable_queries: isIvd ? ['呼吸道病原体 + 多联检 + 试剂','呼吸道病原体 + 核酸检测试剂','呼吸道合胞病毒 + 流感病毒 + 检测试剂'] : ['产品通用名称','预期用途关键词','技术原理 + 使用场景'],
1743    comparables: [],
1744    questions: [
1745      '拟申报的产品通用名称、结构组成和预期用途分别是什么?',
1746      '目标科室、目标患者/样本量及现行替代方案的真实数据来自哪里?',
1747      '是否已有分类界定、同品种注册证或监管沟通记录?',
1748      '性能指标与临床获益分别用什么对照方法和终点验证?',
1749      '注册单元、检测项目、临床方案和质量体系是否已有正式报价?'
1750    ],
1751    official_fee: {value_wan:0,basis:'北京境内第二类注册行政收费为0元'},
1752    registration_cost_estimate: defaultRegistrationCostEstimate(productScope)
1753  };
1754}
1755
1756function defaultRegistrationCostEstimate(productScope = '') {
1757  const isIvd = /IVD|试剂|体外诊断/i.test(productScope);
1758  const isSoftware = /软件|AI/i.test(productScope);
1759  const profileName = isIvd ? '体外诊断试剂(按境内II类规划)' : isSoftware ? '医疗软件 / AI器械(按境内II类规划)' : '常规医疗器械(按境内II类规划)';
1760  const values = isIvd
1761    ? [[40,80,150],[20,45,80],[25,60,120],[40,120,300],[20,50,100],[15,40,80],[0,0,0]]
1762    : isSoftware
1763      ? [[25,60,120],[10,25,50],[20,45,90],[10,40,120],[15,35,70],[10,25,50],[0,0,0]]
1764      : [[50,120,250],[30,60,120],[40,100,220],[30,120,400],[30,70,150],[20,50,100],[0,0,0]];
1765  const definitions = [
1766    ['design','产品定型与设计验证'],['testing','注册检验 / 自检建设'],['nonclinical','生物学、安规、软件研究'],
1767    ['clinical','临床评价 / 临床试验'],['qms','质量体系与核查准备'],['cro','CRO与资料项目管理'],['official','官方行政收费']
1768  ];
1769  const items = definitions.map(([key,label],index) => ({key,label,low:values[index][0],base:values[index][1],high:values[index][2],basis:key === 'official' ? '北京境内第二类注册行政收费为0元' : '孵化器立项阶段规划参数,需用正式报价覆盖',source_type:key === 'official' ? 'official' : 'planning_parameter'}));
1770  return {
1771    applicable:true,profile_code:'demo_ii',profile_name:profileName,regulatory_route:'演示数据:按单一注册单元、境内首次注册测算',confidence:'中低:分类和临床路径确认后会明显收窄',scenario:'base',items,
1772    totals:{low:items.reduce((sum,item)=>sum+item.low,0),base:items.reduce((sum,item)=>sum+item.base,0),high:items.reduce((sum,item)=>sum+item.high,0)},
1773    assumptions:['境内首次注册、单一注册单元、注册人位于北京','非官方分项为规划参数,不是检测机构、医院或CRO报价','不含自建厂房、规模化产线、融资成本和销售费用'],
1774    official_sources:[
1775      {label:'北京注册收费降为零',url:'https://www.beijing.gov.cn/zhengce/zhengcefagui/201909/t20190909_171208.html'},
1776      {label:'国家药监局器械注册收费',url:'https://zwfw.nmpa.gov.cn/web/taskview/11100000MB0341032Y100017214300101'}
1777    ]
1778  };
1779}
1780
1781function renderDecisionWorkbench(workbench, report = {}) {
1782  const identity = workbench.product_identity || {};
1783  const identityItems = [
1784    ['候选通用名称', identity.candidate_name || report.project_name || '待确认'],
1785    ['产品类别', identity.product_scope || '待确认'],
1786    ['预期用途', identity.intended_use || '待从BP提取'],
1787    ['注册路径假设', identity.regulatory_hypothesis || '待法规负责人确认']
1788  ];
1789  if (identity.classification_reason) identityItems.push(['为什么这样分类', identity.classification_reason]);
1790  if (identity.identity_source || identity.consistency_status) identityItems.push(['产品身份核验', `${identity.consistency_status || '待确认'} · ${identity.identity_source || '来源待确认'}`]);
1791  if (identity.evidence_quote) identityItems.push(['BP产品原文', `“${identity.evidence_quote}”`]);
1792  const termCounts = identity.classification_term_counts || {};
1793  const readEvidence = Object.entries(termCounts).map(([key,value]) => `${key}×${value}`).join('、');
1794  $('#projectIdentityGrid').innerHTML = identityItems.map(([label,value],index) => {
1795    const note = label === '为什么这样分类'
1796      ? `已读取正文 ${escapeHtml(identity.parsed_characters || '—')} 字${readEvidence ? `;关键词计数:${escapeHtml(readEvidence)}` : ''}`
1797      : label === '产品身份核验' ? '下游检索、成本和注册任务必须使用这一产品身份'
1798      : label === 'BP产品原文' ? '只引用本次上传BP,不引用历史项目'
1799      : index === 3 ? '法规推断,不是正式分类结论'
1800      : index === 2 ? '必须与BP原文及临床场景一致'
1801      : escapeHtml(identity.confidence || '材料初步识别');
1802    return `<article class="${index > 1 ? 'wide' : ''} ${label === '为什么这样分类' ? 'classification-reason' : ''}"><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong><small>${note}</small></article>`;
1803  }).join('');
1804  const gates = Array.isArray(workbench.gates) ? workbench.gates : [];
1805  $('#decisionGateStrip').innerHTML = gates.map((gate,index) => `<article><i>${index + 1}</i>
1805<span><strong>${escapeHtml(gate.name)}</strong><small>${escapeHtml(gate.note || '')}</small></span><b>${escapeHtml(gate.status || '待确认')}</b></article>`).join('');
1806  const queries = Array.isArray(workbench.comparable_queries) ? workbench.comparable_queries : [];
1807  $('#comparableQueryList').innerHTML = queries.map(query => `<a href="https://www.nmpa.gov.cn/zwfwqjd/index.html?type=pc" target="_blank" rel="noopener"><span>${escapeHtml(query)}</span><b>到药监局核验 ↗</b></a>`).join('') || '<p>需要先确认产品通用名称和预期用途,才能生成有效检索式。</p>';
1808  const comparables = Array.isArray(workbench.comparables) ? workbench.comparables : [];
1809  $('#comparableRows').innerHTML = comparables.length ? comparables.map(item => `<div class="comparable-row"><span><strong>${escapeHtml(item.name || '—')}</strong><small>${escapeHtml(item.certificate || '证号待核验')}</small></span><span>${escapeHtml(item.holder || '—')}</span><span>${escapeHtml(item.classification || '—')}</span><span><b>${escapeHtml(item.status || '待核验')}</b></span></div>`).join('') : '<div class="comparable-empty"><strong>尚无已核验同类产品</strong><span>这不是“没有竞品”,而是尚未完成官方数据库检索。请按上方检索式核验后再进入投委会。</span></div>';
1810  $('#comparableTaskStatus').textContent = comparables.length ? `已核验 ${comparables.length} 项` : '0 项已核验';
1811  $('#deepeningQuestions').innerHTML = (workbench.questions || []).map((question,index) => `<li><i>${index + 1}</i><span>${escapeHtml(question)}</span><b>待回答</b></li>`).join('');
1812  const estimate = workbench.registration_cost_estimate || defaultRegistrationCostEstimate(identity.product_scope || '');
1813  renderRegistrationCostEstimate(estimate);
1814  if (!estimate.items?.length) {
1815    const officialFee = workbench.official_fee || {value_wan:0,basis:'待分类后选择'};
1816    $('#officialFeeInput').value = Number(officialFee.value_wan) || 0;
1817    $('#officialFeeBasis').textContent = officialFee.basis || '待分类后选择';
1818  }
1819  $('#investmentDeepening').classList.remove('hidden');
1820  $('#investmentVerify')?.classList.remove('hidden');
1821  const searchProfile = report.search_profile || {};
1822  const verifyProfileEl = $('#verifyProfile');
1823  if (verifyProfileEl) {
1824    if (searchProfile.ran) {
1825      const profileParts = [];
1826      if (searchProfile.product_summary) profileParts.push(`系统理解:${searchProfile.product_summary}`);
1827      const facets = [searchProfile.core_technology, searchProfile.product_form, searchProfile.application].filter(Boolean).join(' · ');
1828      if (facets) profileParts.push(`检索画像:${facets}`);
1829      if ((searchProfile.cn_queries || []).length) profileParts.push(`中文检索:${searchProfile.cn_queries.join(' | ')}`);
1830      if ((searchProfile.en_terms || []).length) profileParts.push(`英文检索:${searchProfile.en_terms.join(' | ')}`);
1831      verifyProfileEl.textContent = profileParts.join(' ');
1832      verifyProfileEl.classList.remove('hidden');
1833    } else {
1834      verifyProfileEl.classList.add('hidden');
1835    }
1836  }
1837  calculateMarketScenario();
1838  calculateRegistrationBudget();
1839  renderCompetitiveComparison(report.competitive_comparison || null);
1840  renderMarketCapitalEvidence(report.market_capital_evidence || null);
1841  renderEntityVerification(report.entity_verification || null);
1842  renderIpVerification(report.ip_verification || null);
1843  renderPipelineScan(report.pipeline_scan || null);
1844  loadComparableCandidates(workbench, identity);
1845}
1846
1847// 主体真实性核验:工商画像 + 股权 + 风险扫描 + BP声称与登记出入(数据随报告保存)
1848function renderEntityVerification(ev) {
1849  const panel = $('#entityVerifyPanel');
1850  if (!panel) return;
1851  if (!ev || !ev.detected_company || !ev.detected_company.name) { panel.classList.add('hidden'); return; }
1852  panel.classList.remove('hidden');
1853  $('#entityDetectNote').textContent = `核验对象:${ev.detected_company.name}(${ev.detected_company.note || 'BP自动识别'})`;
1854  if (ev.registered !== true) {
1855    $('#entityRiskLevel').textContent = '未查到登记';
1856    $('#entityProfile').innerHTML = `<p class="me-note">${escapeHtml(ev.note || '未收录该企业')}</p>`;
1857    $('#entityShareholders').innerHTML = '';
1858    $('#entityRisks').innerHTML = '';
1859    $('#entityMismatchBlock').classList.add('hidden');
1860    $('#entityBoundary').textContent = ev.boundary || '';
1861    return;
1862  }
1863  const profile = ev.profile || {};
1864  const risk = ev.risk_scan || {};
1865  $('#entityRiskLevel').textContent = `风险${risk.level || '待核验'}`;
1866  const gridRows = [
1867    ['经营状态', profile.status], ['成立日期', profile.established], ['注册资本', profile.reg_capital],
1868    ['法定代表人', profile.legal_person], ['统一信用代码', profile.credit_code],
1869    ['参保人数', profile.social_staff ? profile.social_staff + ' 人' : ''],
1870    ['所在地区', profile.region], ['企业类型', profile.company_type],
1871  ].filter(pair => pair[1]);
1872  $('#entityProfile').innerHTML = '<div class="ev-grid">' + gridRows.map(([k,v]) => `<div><span>${escapeHtml(k)}</span><strong>${escapeHtml(v)}</strong></div>`).join('') + '</div>'
1873    + (profile.business_scope ? `<p class="me-note">经营范围:${escapeHtml(profile.business_scope)}</p>` : '');
1874  const shareholders = profile.shareholders || [];
1875  $('#entityShareholders').innerHTML = shareholders.length
1876    ? shareholders.map(sh => `<div class="me-company"><span>${escapeHtml(sh.name)}</span><b class="has">${escapeHtml(sh.percent)}</b></div>`).join('')
1877    : '<p class="me-note">股东信息未获取到</p>';
1878  const items = risk.items || {};
1879  $('#entityRisks').innerHTML = (risk.basis ? `<p class="me-summary">${escapeHtml(risk.basis)}</p>` : '') + ['dishonesty','executed','lawsuits','penalties'].map(key => {
1880    const item = items[key];
1881    if (!item) return '';
1882    const count = item.count;
1883    const cls = count === null || count === undefined ? 'warn' : count > 0 ? (item.severity === 'red' ? 'bad' : 'warn') : 'ok';
1884    const label = (count === null || count === undefined) ? '查询未成' : count > 0 ? `${count} 条` : '无记录';
1885    return `<div class="me-check"><b class="${cls}">${label}</b><span><strong>${escapeHtml(item.label)}</strong>${item.latest && count ? `<small>${escapeHtml(item.latest)}</small>` : ''}</span></div>`;
1886  }).join('');
1887  const mismatch = ev.mismatch || {};
1888  if (mismatch.ran && (mismatch.mismatches || []).length) {
1889    $('#entityMismatchBlock').classList.remove('hidden');
1890    const clsMap = {'一致':'ok','出入':'bad','待核实':'warn'};
1891    $('#entityMismatch').innerHTML = (mismatch.summary ? `<p class="me-summary">${escapeHtml(mismatch.summary)}</p>` : '')
1892      + mismatch.mismatches.map(m =>
1892 `<div class="me-check"><b class="${clsMap[m.verdict] || 'warn'}">${escapeHtml(m.verdict)}</b><span><strong>${escapeHtml(m.aspect)}</strong><small>BP声称:${escapeHtml(m.bp_claim)} | 登记事实:${escapeHtml(m.registry_fact)}</small></span></div>`).join('');
1893  } else {
1894    $('#entityMismatchBlock').classList.add('hidden');
1895  }
1896  $('#entityBoundary').textContent = ev.boundary || '';
1897}
1898
1899// 知识产权核验:主体专利家底 + 同技术第三方专利 + BP声称出入比对(数据随报告保存)
1900function renderIpVerification(ip) {
1901  const panel = $('#ipVerifyPanel');
1902  if (!panel) return;
1903  if (!ip || (!ip.company && !ip.candidate_name)) { panel.classList.add('hidden'); return; }
1904  panel.classList.remove('hidden');
1905  $('#ipVerifyNote').textContent = ip.company
1906    ? `核验对象:${ip.company} · 同技术方向:${ip.candidate_name || '待确认'}(公开专利数据)`
1907    : '主体未识别,仅做同技术专利扫描';
1908  const cp = ip.company_patents || {};
1909  const statusParts = [];
1910  if (cp.count_shown) statusParts.push(`${cp.count_shown} 项·授权 ${cp.granted_count || 0}`);
1911  $('#ipVerifyStatus').textContent = statusParts.join('') || (ip.company_patents_error ? '未查到' : '待核验');
1912  const typeText = Object.entries(cp.by_type || {}).map(([k, v]) => `${k} ${v}`).join(' · ');
1913  const statusText = Object.entries(cp.by_status || {}).map(([k, v]) => `${k} ${v}`).join(' · ');
1914  $('#ipCompanyPatents').innerHTML = (cp.count_shown
1915    ? `<p class="me-summary">${escapeHtml(typeText)}${statusText ? ' | 状态:' + escapeHtml(statusText) : ''}</p>`
1916      + (cp.patents || []).map(p => `<div class="me-company"><span>${escapeHtml(p.title)}</span><b class="${/授权|维持/.test(p.status) ? 'has' : ''}">${escapeHtml(p.status)}</b><small>${escapeHtml([p.type, p.apply_date, p.pub_number].filter(Boolean).join(' · '))}</small></div>`).join('')
1917      + `<p class="me-note">${escapeHtml(cp.count_note || '')}</p>`
1918    : `<p class="me-note">${escapeHtml(ip.company_patents_error || '主体名下未查到专利;若BP声称拥有专利则需重点核实')}</p>`);
1919  const competing = ip.competing_patents || {};
1920  $('#ipCompetingPatents').innerHTML = (competing.items || []).length
1921    ? competing.items.map(p => `<div class="me-company"><span>${escapeHtml(p.title)}</span><b class="has">${escapeHtml(p.applicant || '申请人未标注')}</b><small>${escapeHtml([p.status, p.apply_date, p.pub_number].filter(Boolean).join(' · '))}</small></div>`).join('')
1922      + `<p class="me-note">检索词:${escapeHtml(competing.keyword || '—')} | 第三方在先专利可能限制本项目的授权前景与自由实施空间</p>`
1923    : `<p class="me-note">${escapeHtml(competing.note || '未扫描')}</p>`;
1924  const check = ip.ai_check || {};
1925  if (check.ran && (check.mismatches || []).length) {
1926    $('#ipAiCheckBlock').classList.remove('hidden');
1927    const clsMap = {'一致':'ok','出入':'bad','待核实':'warn'};
1928    $('#ipAiCheck').innerHTML = (check.summary ? `<p class="me-summary">${escapeHtml(check.summary)}</p>` : '')
1929      + check.mismatches.map(m =>
1929 `<div class="me-check"><b class="${clsMap[m.verdict] || 'warn'}">${escapeHtml(m.verdict)}</b><span><strong>${escapeHtml(m.aspect)}</strong><small>BP声称:${escapeHtml(m.bp_claim)} | 公开数据:${escapeHtml(m.registry_fact)}</small></span></div>`).join('');
1930  } else {
1931    $('#ipAiCheckBlock').classList.add('hidden');
1932  }
1933  $('#ipBoundary').textContent = ip.boundary || '';
1934}
1935
1936// 在研管线与临床登记威胁:ClinicalTrials.gov 同类在研研究(数据随报告保存)
1937function renderPipelineScan(ps) {
1938  const panel = $('#pipelineScanPanel');
1939  if (!panel) return;
1940  if (!ps || ps.ran !== true) {
1941    if (ps && ps.note && panel) {
1942      panel.classList.remove('hidden');
1943      $('#pipelineThreatLevel').textContent = '未扫描';
1944      $('#pipelineScanNote').textContent = ps.candidate_name ? `对象:${ps.candidate_name}` : '产品通用名未确认';
1945      $('#pipelineStudies').innerHTML = `<p class="me-note">${escapeHtml(ps.note)}</p>`;
1946      $('#pipelineQueryTag').textContent = '';
1947      $('#pipelineThreatBasis').textContent = '';
1948      $('#pipelineBoundary').textContent = ps.boundary || '';
1949      return;
1950    }
1951    panel.classList.add('hidden');
1952    return;
1953  }
1954  panel.classList.remove('hidden');
1955  $('#pipelineThreatLevel').textContent = `威胁${ps.threat_level || '待评估'}`;
1956  $('#pipelineScanNote').textContent = `进行中 ${ps.active_count || 0} 项 · 涉中国机构 ${ps.china_count || 0} 项(ClinicalTrials.gov)`;
1957  $('#pipelineQueryTag').textContent = `检索词:${ps.used_term || ''}`;
1958  $('#pipelineStudies').innerHTML = (ps.studies || []).length
1959    ? ps.studies.map(s => `<div class="me-check"><b class="${s.active ? 'warn' : 'ok'}">${escapeHtml(s.status)}</b><span><strong>${escapeHtml(s.title)}${s.has_china ? ' <em class="news-tag">涉中国机构</em>' : ''}</strong><small>${escapeHtml([s.sponsor, s.phase !== '不适用/未标注' ? s.phase : '', [s.start_date, s.completion_date].filter(Boolean).join(' ~ '), (s.countries || []).join('/')].filter(Boolean).join(' · '))}</small>${s.url ? `<small><a href="${escapeHtml(s.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(s.nct_id)} ↗</a></small>` : ''}</span></div>`).join('')
1960    : '<p class="me-note">未检索到同类在研登记(不代表国内无在研项目,ChiCTR与国内备案需人工补查)</p>';
1961  $('#pipelineThreatBasis').textContent = ps.threat_basis || '';
1962  $('#pipelineBoundary').textContent = ps.boundary || '';
1963}
1964
1965// 市场与资本佐证:研报实据 + 竞品融资史 + 财务自洽性(数据随报告保存)
1966function renderMarketCapitalEvidence(evidence) {
1967  const panel = $('#marketEvidencePanel');
1968  if (!panel) return;
1969  if (!evidence) { panel.classList.add('hidden'); return; }
1970  panel.classList.remove('hidden');
1971  const size = evidence.market_size || {};
1972  const fin = evidence.financing || {};
1973  const coh = evidence.financial_coherence || {};
1974  const parts = [];
1975  if ((size.fragments || []).length) parts.push(`研报实据 ${size.fragments.length} 条`);
1976  if (fin.checked) parts.push(`融资史 ${fin.with_financing}/${fin.checked} 家有记录`);
1977  if (coh.ran) parts.push('财务自洽已检查');
1978  $('#marketEvidenceStatus').textContent = parts.join(' · ') || '未生成';
1979  $('#marketSizeEvidence').innerHTML = (size.fragments || []).length
1980    ? size.fragments.map(f => `<div class="me-fragment"><strong>${escapeHtml(f.title || '研报片段')}</strong><small>${escapeHtml([f.institution, f.publish_date].filter(Boolean).join(' · ') || '机构与日期详见原报告')}</small><p>${escapeHtml(f.excerpt || '')}</p></div>`).join('')
1981    : `<p class="me-note">${escapeHtml(size.note || '未检索到研报数据')}</p>`;
1982  const companies = fin.companies || [];
1983  $('#financingEvidence').innerHTML = (fin.summary ? `<p class="me-summary">${escapeHtml(fin.summary)}</p>` : '') + (companies.length
1984    ? companies.map(co => {
1985        const latest = co.latest;
1986        const latestText = latest
1987          ? `最近:${escapeHtml(latest.date || '时间未披露')} · ${escapeHtml(latest.round || '轮次未披露')} · ${escapeHtml(latest.money || '金额未披露')}${latest.investors ? ' · ' + escapeHtml(latest.investors) : ''}`
1988          : '该企业无公开融资记录(不代表未融资)';
1989        return `<div class="me-company"><span>${escapeHtml(co.holder)}</span><b class="${co.event_count ? 'has' : 'none'}">${co.event_count ? `${co.event_count} 笔公开融资` : '无公开记录'}</b><small>${latestText}</small></div>`;
1990      }).join('')
1991    : `<p class="me-note">${escapeHtml(fin.note || '未查询')}</p>`);
1992  if (coh.ran) {
1993    const verdictCls = {'一致':'ok', '存疑':'warn', '矛盾':'bad'};
1994    $('#financialCoherence').innerHTML = (coh.summary ? `<p class="me-summary">${escapeHtml(coh.summary)}</p>` : '')
1995      + (coh.checks || []).map(ch => `<div class="me-check"><b class="${verdictCls[ch.verdict] || 'warn'}">${escapeHtml(ch.verdict)}</b><span><strong>${escapeHtml(ch.aspect)}</strong><small>${escapeHtml(ch.claim || '')}${ch.cross ? ' |对照:' + escapeHtml(ch.cross) : ''}${ch.note ? ' |' + escapeHtml(ch.note) : ''}</small></span></div>`).join('')
1996      + (coh.funding_note ? `<p class="me-note">融资匹配性:${escapeHtml(coh.funding_note)}</p>` : '');
1997  } else {
1998    $('#financialCoherence').innerHTML = `<p class="me-note">${escapeHtml(coh.note || '未检查')}</p>`;
1999  }
2000  $('#marketEvidenceBoundary').textContent = evidence.boundary || '';
2001}
2002
2003// 同类产品与技术先进性对比:官方UDI竞品清单 + AI交叉比对(数据随报告保存)
2004function renderCompetitiveComparison(competitive) {
2005  const panel = $('#competitiveCompare');
2006  if (!panel) return;
2007  const items = competitive && Array.isArray(competitive.items) ? competitive.items : [];
2008  const ai = competitive && competitive.ai_comparison ? competitive.ai_comparison : null;
2009  const news = competitive && competitive.news_references ? competitive.news_references : {};
2010  const snippets = Array.isArray(news.snippets) ? news.snippets : [];
2011  if (!competitive || (!items.length && !ai && !snippets.length)) { panel.classList.add('hidden'); return; }
2012  panel.classList.remove('hidden');
2013  const density = competitive.market_density || '';
2014  const positioning = ai ? (ai.technical_positioning || '待验证') : '待人工对比';
2015  $('#competitivePositioning').textContent = `技术定位:${positioning}${density ? ` · 竞争密度${density}` : ''}`;
2016  const basisParts = [];
2017  if (ai && ai.positioning_basis) basisParts.push(ai.positioning_basis);
2018  if (competitive.density_basis) basisParts.push(competitive.density_basis);
2019  $('#competitiveBasis').textContent = basisParts.join(';') || '基于官方注册同类清单与BP自述交叉比对';
2020  $('#competitiveSummary').textContent = ai
2021    ? (ai.differentiation_summary || 'AI未给出差异化总结')
2022    : ((competitive.audit && competitive.audit.note) || '本次未生成AI对比,可先人工查看上方竞品清单');
2023  const renderRows = (rows, emptyText) => (rows && rows.length)
2024    ? rows.map(row => `<li><span>${escapeHtml(row.point)}</span><small>${escapeHtml(row.compare_to || '同类整体')} · ${escapeHtml(row.source || '待核验')}</small></li>`).join('')
2025    : `<li class="empty"><span>${emptyText}</span></li>`;
2026  $('#competitiveAdvantages').innerHTML = renderRows(ai && ai.advantages, 'AI未识别出已论证的相对优势');
2027  $('#competitiveGaps').innerHTML = renderRows(ai && ai.gaps, 'AI未识别出明显差距');
2028  const tasks = ai && Array.isArray(ai.verification_tasks) ? ai.verification_tasks : [];
2029  $('#competitiveTasks').innerHTML = tasks.length
2030    ? tasks.map(task => `<li>${escapeHtml(task)}</li>`).join('')
2031    : '<li>核验竞品注册证附件中的性能指标后再下结论</li>';
2032  const newsBlock = $('#competitiveNewsBlock');
2033  if (newsBlock) {
2034    if (snippets.length) {
2035      newsBlock.classList.remove('hidden');
2036      $('#competitiveNews').innerHTML = snippets.map(s => `<div class="me-fragment"><strong>${escapeHtml(s.title || '报道')}</strong><small>${escapeHtml([s.source, s.date].filter(Boolean).join(' · ') || '来源待核验')}</small><p>${escapeHtml(s.excerpt || '')}</p></div>`).join('');
2037      $('#competitiveNewsNote').textContent = news.boundary || '';
2038    } else if (news.note) {
2039      newsBlock.classList.remove('hidden');
2040      $('#competitiveNews').innerHTML = `<p class="me-note">${escapeHtml(news.note)}</p>`;
2041      $('#competitiveNewsNote').textContent = '';
2042    } else {
2043      newsBlock.classList.add('hidden');
2044    }
2045  }
2046  const source = competitive.data_source || {};
2047  const audit = competitive.audit || {};
2048  $('#competitiveBoundary').textContent = `${competitive.boundary || ''}${source.bundle ? ` 数据源:${source.bundle}` : ''}${audit.ai_called && audit.model ? ` · AI对比模型:${audit.model}` : ''}`;
2049}
2050
2051async function loadComparableCandidates(workbench, identity) {
2052  const queries = Array.isArray(workbench.comparable_queries) ? workbench.comparable_queries : [];
2053  if (!queries.length) return;
2054  $('#comparableTaskStatus').textContent = '正在检索 UDI…';
2055  $('#comparableSourceNote').textContent = '正在查询本机国家药监局 UDI 公开数据索引…';
2056  try {
2057    const response = await fetch(`${BP_API_BASE}/api/udi/search`, {
2058      method:'POST',
2059      headers:{'Content-Type':'application/json'},
2060      body:JSON.stringify({queries,candidate_name:identity.candidate_name || '',limit:10})
2061    });
2062    const payload = await response.json();
2063    if (!response.ok) throw new Error(payload.detail || 'UDI索引查询失败');
2064    const data = payload.data || {};
2065    const items = Array.isArray(data.items) ? data.items : [];
2066    currentComparableCandidates = items;
2067    const companyCount = new Set(items.map(item =>
2067 item.holder).filter(Boolean)).size;
2068    if (items.length) {
2069      renderComparableCandidateRows(items, companyCount);
2070    } else {
2071      $('#comparableRows').innerHTML = '<div class="comparable-empty"><strong>当前月度更新索引未命中高相关产品</strong><span>这不代表没有竞品。应继续查询全量注册数据,或调整通用名称、预期用途和检测靶标。</span></div>';
2072      $('#comparableTaskStatus').textContent = '月度索引 0 项';
2073    }
2074    const meta = data.metadata || {};
2075    $('#comparableSourceNote').innerHTML = `<b>数据源:</b>${escapeHtml(data.source || '国家药监局UDI公开数据')} · ${escapeHtml(meta.source_bundle || '本机索引')} · ${escapeHtml(meta.record_count || '—')} 条本机记录。${escapeHtml(data.coverage_notice || '候选结果仍需逐条核验注册证。')}`;
2076  } catch (error) {
2077    $('#comparableTaskStatus').textContent = 'UDI索引待连接';
2078    $('#comparableSourceNote').textContent = `${error.message}。您仍可使用上方检索式进入国家药监局网站人工核验。`;
2079  }
2080}
2081
2082function getVerifiedComparableMap() {
2083  try { return JSON.parse(localStorage.getItem('verifiedComparableRegistry') || '{}'); }
2084  catch (_error) { return {}; }
2085}
2086
2087function renderComparableCandidateRows(items, companyCount = new Set(items.map(item =>
2087 item.holder).filter(Boolean)).size) {
2088  const verified = getVerifiedComparableMap();
2089  $('#comparableRows').innerHTML = items.map(item => {
2090    const certificate = item.certificate || '';
2091    const isVerified = Boolean(certificate && verified[certificate]);
2092    return `<div class="comparable-row"><span><strong>${escapeHtml(item.name || '—')}</strong><small>${escapeHtml(certificate || '证号待核验')} · ${escapeHtml(item.publish_date || item.version_time || '日期待核验')}</small></span><span>${escapeHtml(item.holder || '—')}</span><span>${escapeHtml(item.classification || item.device_category || '待核验')}</span><span><button class="verify-comparable ${isVerified ? 'verified' : ''}" data-certificate="${escapeHtml(certificate)}">${isVerified ? '✓ 已人工核验' : '标记已核验'}</button><small>${escapeHtml((item.matched_terms || []).slice(0,3).join(' / '))}</small></span></div>`;
2093  }).join('');
2094  const verifiedCount = items.filter(item => item.certificate && verified[item.certificate]).length;
2095  $('#comparableTaskStatus').textContent = `已核验 ${verifiedCount} / 命中 ${items.length} · ${companyCount} 家企业`;
2096}
2097
2098function toggleComparableVerification(certificate) {
2099  if (!certificate) return;
2100  const registry = getVerifiedComparableMap();
2101  if (registry[certificate]) delete registry[certificate];
2102  else registry[certificate] = {verified_at:new Date().toISOString(),source:'人工核验'};
2103  localStorage.setItem('verifiedComparableRegistry', JSON.stringify(registry));
2104  renderComparableCandidateRows(currentComparableCandidates);
2105  showToast(registry[certificate] ? `已将 ${certificate} 标记为人工核验` : `已取消 ${certificate} 的核验标记`);
2106}
2107
2108function formatWan(value) {
2109  if (!Number.isFinite(value) || value <= 0) return '待输入';
2110  if (value >= 10000) return `${(value / 10000).toFixed(2)} 亿元`;
2111  return `${value.toFixed(value >= 100 ? 0 : 2)} 万元`;
2112}
2113
2114function formatBudgetWan(value) {
2115  if (!Number.isFinite(value)) return '—';
2116  if (value === 0) return '0 万元';
2117  return formatWan(value);
2118}
2119
2120function calculateMarketScenario() {
2121  const target = Number($('#marketTargetUnits')?.value) || 0;
2122  const usage = Number($('#marketAnnualUsage')?.value) || 0;
2123  const price = Number($('#marketUnitPrice')?.value) || 0;
2124  const reach = Math.min(100, Math.max(0, Number($('#marketReachRate')?.value) || 0)) / 100;
2125  const share = Math.min(100, Math.max(0, Number($('#marketShareRate')?.value) || 0)) / 100;
2126  const tam = target * usage * price / 10000;
2127  const sam = tam * reach;
2128  const som = sam * share;
2129  $('#marketTam').textContent = formatWan(tam);
2130  $('#marketSam').textContent = formatWan(sam);
2131  $('#marketSom').textContent = formatWan(som);
2132}
2133
2134const marketEstimateFieldMap = [
2135  ['target_units','marketTargetUnits','目标机构 / 患者数'],
2136  ['annual_usage','marketAnnualUsage','每个目标年用量'],
2137  ['unit_price','marketUnitPrice','产品单价(元)'],
2138  ['reach_rate','marketReachRate','可服务比例(%)'],
2139  ['share_rate','marketShareRate','三年目标份额(%)']
2140];
2141
2142function applyMarketEstimate(result) {
2143  marketEstimateFieldMap.forEach(([key,inputId]) => {
2144    const item = result[key] || {};
2145    if (item.value === null || item.value === undefined || item.value === '') return;
2146    const value = Number(item.value);
2147    if (!Number.isFinite(value)) return;
2148    const input = $(`#${inputId}`);
2149    if (input) input.value = value;
2150  });
2151  calculateMarketScenario();
2152  const paramsHtml = marketEstimateFieldMap.map(([key,,label]) => {
2153    const item = result[key] || {};
2154    const valueText = (item.value === null || item.value === undefined || item.value === '') ? '暂不给出' : Number(item.value).toLocaleString('zh-CN');
2155    return `<li><strong>${escapeHtml(label)}:${escapeHtml(valueText)}</strong><span>${escapeHtml(item.basis || '暂无依据说明')}(来源:${escapeHtml(item.source || 'AI推算')})</span></li>`;
2156  }).join('');
2157  const refsHtml = (Array.isArray(result.references) ? result.references : []).map(ref => {
2158    const link = ref.url ? ` <a href="${escapeHtml(ref.url)}" target="_blank" rel="noopener noreferrer">查看来源</a>` : '';
2159    return `<li>${escapeHtml(ref.label || '参考数据')}:${escapeHtml(ref.value ?? '')}${ref.period ? `(${escapeHtml(ref.period)})` : ''} — ${escapeHtml(ref.source || '来源待核验')}${link}</li>`;
2160  }).join('');
2161  const notesHtml = (Array.isArray(result.notes) ? result.notes : []).map(note => `<li>${escapeHtml(note)}</li>`).join('');
2162  const container = $('#marketEstimateNotes');
2163  if (!container) return;
2164  container.innerHTML = `
2165    <p class="estimate-banner">⚠ 以下为 AI 估算的情景假设(置信度:${escapeHtml(result.confidence || '中')}),已回填的数值可直接修改,请逐项确认依据后再使用。</p>
2166    <ul class="estimate-params">${paramsHtml}</ul>
2167    ${refsHtml ? `<div class="estimate-refs"><strong>参考来源</strong><ul>${refsHtml}</ul></div>` : ''}
2168    ${notesHtml ? `<div class="estimate-refs"><strong>使用提醒</strong><ul>${notesHtml}</ul></div>` : ''}
2169    ${result.boundary ? `<p class="estimate-boundary">证据边界:${escapeHtml(result.boundary)}</p>` : ''}`;
2170  container.classList.remove('hidden');
2171}
2172
2173async function estimateMarketWithAI() {
2174  const button = $('#marketAiEstimateBtn');
2175  if (!currentInvestmentProject || !currentInvestmentAnalysis) { showToast('请先选择项目并完成BP分析,AI才能基于分析结果估算市场参数'); return; }
2176  if (!button || button.dataset.busy === 'true') return;
2177  button.dataset.busy = 'true';
2178  button.disabled = true;
2179  const originalText = button.textContent;
2180  button.textContent = '估算中…';
2181  try {
2182    const estimateUrl = window.location.protocol === 'file:' ? 'http://127.0.0.1:8787/api/investment/market-estimate' : '/api/investment/market-estimate';
2183    const response = await fetch(estimateUrl,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({context:compactInvestmentAssistantContext()})});
2184    const payload = await response.json();
2185    if (!response.ok) throw new Error(payload.error || payload.detail || 'AI市场估算暂时不可用');
2186    applyMarketEstimate(payload);
2187    showToast('AI估算已回填,请逐项确认依据后再使用');
2188  } catch (error) {
2189    showToast(error.message || 'AI市场估算失败,请稍后重试');
2190  } finally {
2191    button.dataset.busy = 'false';
2192    button.disabled = false;
2193    button.textContent = originalText;
2194  }
2195}
2196
2197function renderTransformationPath(result) {
2198  const container = $('#transformationPathResult');
2199  if (!container) return;
2200  const audit = result._audit || {};
2201  const stages = (Array.isArray(result.stages) ? result.stages : []).map((stage, index) => {
2202    const actions = (stage.key_actions || []).map(action => `<li>${escapeHtml(action)}</li>`).join('');
2203    const resources = (stage.resources || []).map(resource => `<span class="tp-resource-tag">${escapeHtml(resource)}</span>`).join('');
2204    const deliverables = (stage.deliverables || []).map(item => `<li>${escapeHtml(item)}</li>`).join('');
2205    return `<article class="tp-stage"><div class="tp-stage-node"><i>${index + 1}</i></div><div class="tp-stage-card"><header><div><strong>${escapeHtml(stage.name || `阶段 ${index + 1}`)}</strong><p>${escapeHtml(stage.goal || '')}</p></div><span class="tp-duration">${escapeHtml(stage.duration || '周期待确认')}</span></header><div class="tp-stage-body"><section><b>关键行动</b><ul>${actions || '<li>待确认</li>'}</ul></section><section><b>调用集团资源</b><div class="tp-resources">${resources || '<span class="tp-resource-tag">待确认</span>'}</div></section><section><b>产出物</b><ul>${deliverables || '<li>待确认</li>'}</ul></section></div><div class="tp-gate"><strong>决策门</strong><span>${escapeHtml(stage.decision_gate || '待确认')}</span><small>${escapeHtml(stage.cost_hint || '费用待确认')}</small></div></div></article>`;
2206  }).join('');
2207  const immediate = (Array.isArray(result.immediate_actions) ? result.immediate_actions : []).map((item, index) => `<li><i>${index + 1}</i><div><strong>${escapeHtml(item.action || '')}</strong><span>责任角色:${escapeHtml(item.owner_role || '待确认')}</span></div><em>产出:${escapeHtml(item.output || '待确认')}</em></li>`).join('');
2208  const risks = (Array.isArray(result.critical_risks) ? result.critical_risks : []).map(item => `<li><span class="risk-mid">风险</span><div><strong>${escapeHtml(item.risk || '')}</strong><p>缓解:${escapeHtml(item.mitigation || '待确认')}</p></div></li>`).join('');
2209  const trackName = result.recommended_track?.track || '尚不确定';
2210  const trackReason = result.recommended_track?.reason || '';
2211  let productType = currentInvestmentAnalysis?.product_identity?.product_scope || '';
2212  if (!productType) {
2213    if (/体外诊断|IVD|试剂/i.test(trackReason)) productType = 'IVD(体外诊断)';
2214    else if (/有源/.test(trackReason)) productType = '有源医疗器械';
2215    else if (/无源/.test(trackReason)) productType = '无源医疗器械';
2216    else if (/软件/.test(trackReason)) productType = '医疗器械软ä
2216»¶';
2217  }
2218  let trackNote = '';
2219  if (/IVD/i.test(trackName)) {
2220    trackNote = '注:IVD(体外诊断)在我国按医疗器械管理,与器械设备同属"医疗器械"监管大类,故赛道合并命名。';
2221    if (productType && !/体外诊断|IVD|试剂/i.test(productType)) {
2222      trackNote += `本项目判定为"${productType}",不属于IVD产品。`;
2223    }
2224  }
2225  container.innerHTML = `
2226    <div class="tp-banner"><p>${escapeHtml(result.path_summary || '转化路径方案已生成。')}</p><span>AI规划方案,需专家评审后执行</span></div>
2227    <div class="tp-track"><small>推荐赛道</small><strong>${escapeHtml(trackName)}</strong>${productType ? `<span class="tp-track-type">产品类型:${escapeHtml(productType)}</span>` : ''}<p>${escapeHtml(trackReason)}</p>${trackNote ? `<small class="tp-track-note">${escapeHtml(trackNote)}</small>` : ''}</div>
2228    <div class="tp-timeline">${stages}</div>
2229    ${immediate ? `<div class="tp-immediate"><header><strong>30 天行动清单</strong><span>先动起来,不等全部条件成熟</span></header><ul>${immediate}</ul></div>` : ''}
2230    ${risks ? `<div class="tp-risks"><strong>关键风险与缓解</strong><ul>${risks}</ul></div>` : ''}
2231    <footer class="tp-footer"><p>证据边界:${escapeHtml(result.boundary || '本方案为AI规划草案,执行前需人工确认。')}</p><small>模型:${escapeHtml(audit.model || 'DeepSeek')} · 请求 ${escapeHtml(audit.request_id || '—')}${audit.usage?.total_tokens ? ` · tokens ${escapeHtml(String(audit.usage.total_tokens))}` : ''}</small></footer>`;
2232  container.classList.remove('hidden');
2233  container.scrollIntoView({ behavior:'smooth', block:'nearest' });
2234}
2235
2236async function generateTransformationPath() {
2237  const button = $('#transformationPathBtn');
2238  if (!currentInvestmentProject || !currentInvestmentAnalysis) { showToast('请先完成BP分析,AI才能基于分析结果规划转化路径'); return; }
2239  if (!button || button.dataset.busy === 'true') return;
2240  button.dataset.busy = 'true';
2241  button.disabled = true;
2242  const originalText = button.textContent;
2243  button.textContent = '规划中…';
2244  try {
2245    const pathUrl = window.location.protocol === 'file:' ? 'http://127.0.0.1:8787/api/investment/transformation-path' : '/api/investment/transformation-path';
2246    const response = await fetch(pathUrl,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({context:compactInvestmentAssistantContext()})});
2247    const payload = await response.json();
2248    if (!response.ok) throw new Error(payload.error || payload.detail || 'AI转化路径规划暂时不可用');
2249    renderTransformationPath(payload);
2250    showToast('转化路径方案已生成,请专家评审后执行');
2251  } catch (error) {
2252    showToast(error.message || 'AI转化路径规划失败,请稍后重试');
2253  } finally {
2254    button.dataset.busy = 'false';
2255    button.disabled = false;
2256    button.textContent = originalText;
2257  }
2258}
2259
2260function calculateRegistrationBudget() {
2261  const inputs = $$('[data-budget-item]');
2262  const values = inputs.map(input => Number(input.value) || 0);
2263  const total = values.reduce((sum,value) => sum + value, 0);
2264  const filled = values.filter(value => value > 0).length;
2265  $('#registrationBudgetTotal').textContent = formatBudgetWan(total);
2266  const activeScenario = $('.budget-scenario-switch button.active')?.dataset.budgetScenario || 'base';
2267  const scenarioName = {low:'低档',base:'基准',high:'高档'}[activeScenario];
2268  $('#registrationBudgetCoverage').textContent = `${scenarioName}规划值 · ${filled} / ${values.length} 项可编辑 · 不是正式报价`;
2269  const totals = currentRegistrationEstimate?.totals || {};
2270  $('#registrationBudgetRange').innerHTML = `<b>低档 ${formatBudgetWan(Number(totals.low) || 0)}</b><b>基准 ${formatBudgetWan(Number(totals.base) || 0)}</b><b>高档 ${formatBudgetWan(Number(totals.high) || 0)}</b>`;
2271}
2272
2273function renderRegistrationCostEstimate(estimate) {
2274  currentRegistrationEstimate = estimate || null;
2275  if (!estimate?.applicable) {
2276    $('#registrationEstimateProfile').textContent = estimate?.profile_name || '当前项目不适用器械预算模型';
2277    $('#registrationEstimateRoute').textContent = estimate?.confidence || '请转注册法规检索模块建立专门模型';
2278    $$('[data-budget-item]').forEach(input => { input.value = 0; input.disabled = true; });
2279    $('#registrationEstimateAssumptions').innerHTML = (estimate?.assumptions || []).map(item => `<li>${escapeHtml(item)}</li>`).join('');
2280    $('#registrationEstimateSources').innerHTML = '';
2281    calculateRegistrationBudget();
2282    return;
2283  }
2284  $$('[data-budget-item]').forEach(input => { input.disabled = false; });
2285  $('#registrationEstimateProfile').textContent = estimate.profile_name || '注册成本规划模型';
2286  $('#registrationEstimateRoute').textContent = `${estimate.regulatory_route || ''} · 可信度:${estimate.confidence || '待确认'}`;
2287  const itemMap = new Map((estimate.items || []).map(item => [item.key,item]));
2288  $$('[data-budget-item]').forEach(input => {
2289    const item = itemMap.get(input.dataset.budgetKey);
2290    if (!item) return;
2291    input.dataset.low = item.low ?? 0;
2292    input.dataset.base = item.base ?? 0;
2293    input.dataset.high = item.high ?? 0;
2294    const basis = $(`[data-budget-basis="${input.dataset.budgetKey}"]`);
2295    if (basis) basis.textContent = `${item.basis || '规划参数'};区间 ${formatBudgetWan(Number(item.low)||0)}–${formatBudgetWan(Number(item.high)||0)}`;
2296  });
2297  $('#registrationEstimateAssumptions').innerHTML = (estimate.assumptions || []).map(item => `<li>${escapeHtml(item)}</li>`).join('');
2298  $('#registrationEstimateSources').innerHTML = (estimate.official_sources || []).map(item => `<a href="${escapeHtml(item.url)}" target="_blank" rel="noopener">${escapeHtml(item.label)} ↗</a>`).join('');
2299  applyBudgetScenario(estimate.scenario || 'base');
2300}
2301
2302function applyBudgetScenario(scenario = 'base') {
2303  $$('.budget-scenario-switch button').forEach(button => button.classList.toggle('active', button.dataset.budgetScenario === scenario));
2304  $$('[data-budget-item]').forEach(input => {
2305    const next = Number(input.dataset[scenario]);
2306    if (Number.isFinite(next)) input.value = next;
2307  });
2308  calculateRegistrationBudget();
2309}
2310
2311function defaultInvestmentReferenceData() {
2312  return {
2313    as_of:'2026-07-15', product_scope:'医疗器械(细分类别待确认)',
2314    market:[],
2315    registration_costs:[
2316      {label:'北京境内第二类器械注册行政收费',value:'0元',basis:'2019年5月1日起降为零',source:'北京市政府(京发改〔2019〕569号)',url:'https://www.beijing.gov.cn/zhengce/zhengcefagui/201909/t20190909_171208.html',scope_note:'不含检测、临床、体系和CRO费用'},
2317      {label:'国产第三类器械首次注册行政收费',value:'15.36万元',basis:'国家药监局办事指南',source:'国家药品监督管理局',url:'https://zwfw.nmpa.gov.cn/web/taskview/11100000MB0341032Y100017214300101',scope_note:'按注册单元计,不等于完整项目预算'},
2318      {label:'高风险第三类器械临床试验申请费',value:'4.32万元',basis:'国家药监局办事指南',source:'国家药品监督管理局',url:'https://zwfw.nmpa.gov.cn/web/taskview/11100000MB0341032Y100017214200001',scope_note:'仅适用于需审批的高风险三类器械'}
2319    ],
2320    budget_components:['产品定型、样机与设计验证','注册检验或自检能力建设','生物学评价、安规、软件与网络安全研究','临床评价或临床试验','质量管理体系建立与核查准备','注册咨询、资料编制、补正与项目管理'],
2321    official_databases:[
2322      {label:'国家药监局境内器械注册查询',url:'https://www.nmpa.gov.cn/zwfwqj
2322d/index.html?type=pc',use:'核验同类注册证、注册人、管理类别'},
2323      {label:'国家药监局UDI数据库',url:'https://udi.nmpa.gov.cn/',use:'按产品名称或企业检索在售同类产品'}
2324    ]
2325  };
2326}
2327
2328function renderInvestmentEvidence(data) {
2329  // 09「证据与参考」板块已并入 03 真实核验 与 08 深化与注册;逐个守卫,元素不存在时跳过
2330  const costGrid = $('#registrationCostGrid');
2331  if (costGrid) costGrid.innerHTML = (data.registration_costs || []).map(item => `<article><span>官方行政收费</span><strong>${escapeHtml(item.value)}</strong><h3>${escapeHtml(item.label)}</h3><p>${escapeHtml(item.basis)}。${escapeHtml(item.scope_note)}</p><a href="${escapeHtml(item.url)}" target="_blank" rel="noopener">核验收费依据 · ${escapeHtml(item.source)} ↗</a></article>`).join('');
2332  const budgetList = $('#registrationBudgetComponents');
2333  if (budgetList) budgetList.innerHTML = (data.budget_components || []).map(item => `<li>${escapeHtml(item)} <b>需询价</b></li>`).join('');
2334  const dbLinks = $('#officialDatabaseLinks');
2335  if (dbLinks) dbLinks.innerHTML = (data.official_databases || []).map(item => `<a href="${escapeHtml(item.url)}" target="_blank" rel="noopener"><strong>${escapeHtml(item.label)} ↗</strong><small>${escapeHtml(item.use)}</small></a>`).join('');
2336}
2337
2338function renderInternalKnowledgeReferences(data) {
2339  const panel = $('#investmentKnowledgeReferences');
2340  if (!panel) return;
2341  const references = Array.isArray(data.references) ? data.references : [];
2342  if (!data.ready) {
2343    panel.classList.add('hidden');
2344    return;
2345  }
2346  $('#knowledgeQueryTerms').innerHTML = (data.query_terms || []).slice(0,10).map(term => `<b>${escapeHtml(term)}</b>`).join('') || '<span>未提取到稳定的细分赛道词,建议先补充产品名称和预期用途。</span>';
2347  $('#knowledgeReferenceCount').textContent = `命中 ${references.length} 份内部资料`;
2348  $('#knowledgeReferenceList').innerHTML = references.length ? references.map((item,index) => {
2349    const isCase = item.source_type === '历史项目BP';
2350    return `<article class="internal-reference-card ${isCase ? 'case' : 'report'}"><header><i>${isCase ? '案例' : '报告'}</i><span><strong>R${index + 1} · ${escapeHtml(item.source_name || '内部资料')}</strong><small>${escapeHtml(item.locator || '位置待确认')} · 资料日期 ${escapeHtml(item.source_date || '未识别')}</small></span></header><p>${escapeHtml(item.excerpt || '无可展示片段')}</p><footer><b>${escapeHtml(item.source_type || item.category || '内部资料')}</b><span>${escapeHtml(item.evidence_boundary || '仅作内部研究参考')}</span></footer></article>`;
2351  }).join('') : '<div class="internal-reference-empty"><strong>知识库已连接,但没有命中直接相关资料</strong><span>系统不会用不相关报告凑答案;请补充产品通用名称、预期用途和适应症后重新分析。</span></div>';
2352  $('#knowledgeReferenceBoundary').textContent = data.boundary || '行业报告是内部研究参考,历史BP是项目方自述;均不替代官方数据和专家核验。';
2353  panel.classList.remove('hidden');
2354}
2355
2356function renderBiomedicalRoute(data) {
2357  const panel = $('#investmentBiomedicalRoute');
2358  if (!panel || !data || !data.route_candidate) { panel?.classList.add('hidden'); return; }
2359  const audit = data.audit || {};
2360  const references = Array.isArray(data.knowledge_references) ? data.knowledge_references : [];
2361  $('#bpRouteAudit').textContent = audit.ai_called ? `AI研判 · ${references.length}份法规依据` : `本机初筛 · ${references.length}份法规依据`;
2362  $('#bpRouteCandidate').textContent = data.route_candidate;
2363  $('#bpRouteConfidence').textContent = `路径置信度 ${data.route_confidence || '低'}`;
2364  $('#bpRouteRisk').textContent = data.risk_level || '待判定';
2365  $('#bpRouteGate').textContent = data.decision_gate || '完成属性界定';
2366  $('#bpRouteScope').textContent = data.scope_judgment || '当前信息不足,需法规负责人进一步界定。';
2367  $('#bpRouteGap').textContent = `优先补充:${data.critical_gap || '作用机制、受试物组成和临床使用方式'}`;
2368  $('#bpRouteActions').innerHTML = (data.actions || []).slice(0,5).map(item => `<div><i>${escapeHtml(item.step || '')}</i><span><strong>${escapeHtml(item.title || '')}</strong><small>${escapeHtml(item.description || '')} · ${escapeHtml(item.owner || '法规负责人')}</small></span></div>`).join('') || '<p>等待形成路径任务。</p>';
2369  $('#bpRouteSources').innerHTML = references.length ? references.map((item,index) => `<article><b>B${index + 1}</b><span><strong>${escapeHtml(item.source_name || '法规资料')}</strong><small>${escapeHtml(item.locator || '位置待确认')} · ${escapeHtml(item.source_date || '日期待核验')}</small><p>${escapeHtml(item.excerpt || '')}</p></span></article>`).join('') : '<p>本轮未命中法规原文,路径结论只能作为低置信度线索。</p>';
2370  $('#bpRouteBoundary').textContent = data.boundary || '路径研判不替代主管部门界定或法规负责人签署。';
2371  panel.classList.remove('hidden');
2372}
2373
2374// 通用异步任务轮询:提交后立即返回,短轮询取结果。
2375// 避免长耗时请求(>60秒)被用户侧网络设备掐断(Failed to fetch)。
2376async function pollAsyncJob(jobId, {intervalMs = 4000, timeoutMs = 25*60*1000, onProgress} = {}) {
2377  const token = localStorage.getItem('token');
2378  const headers = token ? {Authorization:`Bearer ${token}`} : {};
2379  const startedAt = Date.now();
2380  const deadline = startedAt + timeoutMs;
2381  while (Date.now() < deadline) {
2382    await new Promise(resolve => setTimeout(resolve, intervalMs));
2383    let job = null;
2384    try {
2385      const resp = await fetch(`${BP_API_BASE}/api/analysis-jobs/${jobId}`, { headers });
2386      if (!resp.ok) continue;
2387      job = (await resp.json()).data || {};
2388    } catch (fetchError) { continue; }
2389    if (job.status === 'error') throw new Error(job.detail || '任务失败');
2390    if (job.status === 'done') return job.result;
2391    if (onProgress) onProgress(Math.round((Date.now() - startedAt) / 1000));
2392  }
2393  throw new Error('处理时间较长,请稍后重试');
2394}
2395
2396async function analyzeBpFile() {
2397  if (!bpSelectedFile) return;
2398  if (!currentInvestmentProject) { showToast('请先在第一步创建或选择投资项目档案'); renderInvestmentWorkflowStep(1); return; }
2399  if (!await checkBpEngine()) { showToast('请先双击“启动统一平台.bat”启动本机分析服务'); return; }
2400  const token = localStorage.getItem('token');
2401  const form = new FormData();
2402  form.append('file', bpSelectedFile);
2403  form.append('project_id', currentInvestmentProject.id);
2404  $('#bpAnalyzeFileButton').disabled = true;
2405  $('#bpAnalyzeFileButton').classList.add('loading');
2406  $('#bpAnalyzeFileButton').textContent = '正在解析与九维分析…';
2407  $('#bpAnalysisProgress').textContent = '读取全文并核对证据,完整分析通常需要 5–15 分钟(含官方数据核验与估值测算),请耐心等待';
2408  startBpAnalysisAnimation();
2409  try {
2410    const headers = token ? {Authorization:`Bearer ${token}`} : {};
2411    // 异步任务模式:提交后立即返回 job_id,再短轮询取结果。
2412    // 避免 1-3 分钟长连接被用户侧网络设备掐断(Failed to fetch)。
2413    const createResp = await fetch(`${BP_API_BASE}/api/upload-and-analyze-async`, { method:'POST', headers, body:form });
2414    const createPayload = await createResp.json().catch(() => ({}));
2415    if (!createResp.ok) throw new Error(createPayload.detail || 'BP分析任务创建失败');
2416    const jobId = createPayload.data.job_id;
2417    const startedAt = Date.now();
2418    const deadline = startedAt + 25*60*1000;
2419    let payload = null;
2420    while (Date.now() < deadline) {
2421      await new Promise(resolve => setTimeout(resolve, 4000));
2422      const jobResp = await fetch(`${BP_API_BASE}/api/analysis-jobs/${jobId}`, { headers });
2423      if (!jobResp.ok) continue;
2424      const job = (await jobResp.json()).data || {};
2425      if (job.status === 'error') throw new Error(job.detail || 'BP分析失败');
2426      if (job.status === 'done') { payload = job.result; break; }
2427      const elapsed = Math.round((Date.now() - startedAt) / 1000);
2428      $('#bpAnalysisProgress').textContent = `AI 正在逐页分析全文并完成官方核验,已用时 ${elapsed} 秒(通常 5–15 分钟)…`;
2429    }
2430    if (!payload) throw new Error('分析时间较长,请稍后在历史报告中查看结果');
2431    renderProfessionalBpReport(payload.data.report);
2432    currentInvestmentReportId = payload.data.report_id;
2433    await loadInvestmentProjects(true, currentInvestmentProject.id);
2434    renderInvestmentWorkflowStep(3, true);
2435    $('#bpAnalysisProgress').textContent = `分析完成 · 报告 ${payload.data.report_id.slice(0,8)}`;
2436    finishBpAnalysisAnimation(true, '已生成九维报告,并保存实际 AI 调用记录');
2437    showToast('BP九维分析完成,已保存到历史报告');
2438  } catch (error) {
2439    $('#bpAnalysisProgress').textContent = error.message;
2440    finishBpAnalysisAnimation(false, error.message);
2441    showToast(error.message);
2442  } finally {
2443    $('#bpAnalyzeFileButton').disabled = false;
2444    $('#bpAnalyzeFileButton').classList.remove('loading');
2445    $('#bpAnalyzeFileButton').textContent = '重新上传并分析 →';
2446  }
2447}
2448
2449function showToast(message) {
2450  const toast = $('#toast');
2451  toast.textContent = message;
2452  toast.classList.add('show');
2453  clearTimeout(showToast.timer);
2454  showToast.timer = setTimeout(() => toast.classList.remove('show'), 2400);
2455}
2456
2457const CAPABILITY_LEVELS = {
2458  ai: { label: '真实 AI 分析', icon: 'AI', tone: 'ai' },
2459  rule: { label: '规则初筛', icon: '规', tone: 'rule' },
2460  workflow: { label: '工作流工具', icon: '流', tone: 'workflow' },
2461  demo: { label: '演示样例', icon: '示', tone: 'demo' },
2462  unavailable: { label: '真实引擎待接入', icon: '待', tone: 'unavailable' }
2463};
2464
2465const CAPABILITY_REGISTRY = {
2466  command: { level: 'workflow', title: '驾驶舱已区分真实项目与演示档案', detail: '只有带“真实项目”标识的档案参与统计;演示项目仅用于展示页面结构。' },
2467  opc: { level: 'workflow', title: '本机任务与决策门工作流', detail: '任务状态会真实保存,但专业结论仍需由对应 AI、专家和外部数据模块提供。' },
2468  investment: { level: 'ai', title: '读取当前 BP 后调用 DeepSeek', detail: '项目原文与历史资料分层使用;市场、注册和专利外部数据仍按来源状态显示。' },
2469  policyhub: { level: 'ai', title: '基于选定企业档案与政策原文分析', detail: 'AI负责拆解和起草;政策有效性、硬条件与提交材料仍须逐条人工确认。' },
2470  regulatory: { level: 'rule', title: '当前仅生成注册路径假设', detail: '尚未自动检索 NMPA、CDE、CMDE 官方原文,不得将本页输出作为正式分类或申报依据。' },
2471  patent: { level: 'unavailable', title: '真实专利检索尚未接入', detail: '需接入可追溯专利数据库并返回公开号、权利要求、法律状态和检索日期后,才能形成项目结论。' },
2472  flashpoc: { level: 'demo', title: '当前为概念方法样例', detail: '内置方案只展示设计思路,不代表已针对当前项目完成工程设计;演示方案不能进入正式深化任务。' },
2473  development: { level: 'workflow', title: '工程阶段管理框架', detail: '系统不会再模拟仿真完成、DFM通过或样机完成;只有上传并审核真实交付物后才能登记完成。' },
2474  regai: { level: 'unavailable', title: '数字孪生与注册验证引擎尚未接入', detail: '未连接力学模型、材料参数、真实患者数据和合规验证环境,不能生成“验证通过”结论。' },
2475  commercial: { level: 'rule', title: '商业化规划框架', detail: '当前没有真实 KOL 数据库或医院术式量数据;页面只用于组织调研任务,不提供专家排名或虚构匹配率。' },
2476  intake: { level: 'rule', title: '转化路径规则初筛', detail: '只依据输入关键词识别候选赛道和下一步,不生成看似精确但无法解释的项目分数。' },
2477  resources: { level: 'rule', title: '集团资源规则推荐', detail: '只在已登记资源中按明确关键词推荐,并展示触发依据;不代表资源方已确认承接。' },
2478  workspace: () => state.aiMode === 'professional'
2479    ? { level: 'ai', title: 'DeepSeek 动态临床访谈', detail: '每轮调用保留模型、证据、不确定性和专家复核要求;不替代临床、工程或法规签字。' }
2480    : { level: 'demo', title: '当前为规则演示模式', detail: '只会匹配三组内置场景与通用问卷,不应用于真实项目判断;配置密钥后系统会优先启用专业 AI。' }
2481};
2482
2483function getCapability(view) {
2484  const item = CAPABILITY_REGISTRY[view];
2485  return typeof item === 'function' ? item() : item;
2486}
2487
2488function renderCapabilityBanner(view = state.currentView) {
2489  const capability = getCapability(view);
2490  const target = $(`#${view}View`);
2491  if (!capability || !target) return;
2492  const level = CAPABILITY_LEVELS[capability.level];
2493  let banner = $('[data-capability-disclosure]', target);
2494  if (!banner) {
2495    banner = document.createElement('section');
2496    banner.dataset.capabilityDisclosure = 'true';
2497    target.prepend(banner);
2498  }
2499  banner.className = `capability-disclosure capability-${level.tone}`;
2500  banner.innerHTML = `<span class="capability-mark">${level.icon}</span><div><b>${level.label}</b><strong>${escapeHtml(capability.title)}</strong><p>${escapeHtml(capability.detail)}</p></div>`;
2501}
2502
2503function createProjectContext(input = {}) {
2504  const previousId = input.newProject ? null : state.projectContext?.id;
2505  state.projectContext = {
2506    id: previousId || `MD-${new Date().toISOString().slice(2,10).replaceAll('-','')}-${String(Math.floor(Math.random() * 900) + 100)}`,
2507    title: input.title || '待命名临床转化项目',
2508    description: input.description || '',
2509    summary: input.summary || '',
2510    tags: input.tags || [],
2511    sourceMode: input.sourceMode || 'unverified',
2512    audit: input.audit || {},
2513    updatedAt: new Date().toISOString()
2514  };
2515  return state.projectContext;
2516}
2517
2518function updateContextProject(patch = {}) {
2519  if (!state.projectContext?.id) return;
2520  let project = projects.find(item => item.id === state.projectContext.id);
2521  if (!project) {
2522    project = {
2523      icon: 'M',
2524      name: state.projectContext.title || state.projectContext.summary?.slice(0, 24) || '临床转化项目',
2525      id: state.projectContext.id,
2526      stage: '需求澄清',
2527      progress: 8,
2528      owner: '待指定主理人',
2529      update: '来自当前临床访谈',
2530      source: ['ai','rule'].includes(state.projectContext.sourceMode) ? 'active_project' : 'demo'
2531    };
2532    projects.unshift(project);
2533  }
2534  Object.assign(project, patch);
2535  renderProjects();
2536}
2537
2538function showCapabilityStop(container, title, detail, requirements = []) {
2539  if (!container) return;
2540  container.classList.remove('hidden');
2541  container.innerHTML = `<div class="capability-stop-card"><span>待接入</span><h2>${escapeHtml(title)}</h2><p>${escapeHtml(detail)}</p>${requirements.length ? `<ul>${requirements.map(item => `<li>${escapeHtml(item)}</li>`).join('')}</ul>` : ''}<b>本模块不会生成模拟结论,也不会更新正式项目进度。</b></div>`;
2542}
2543
2544function appendRuleBasis(container, triggers, boundary) {
2545  if (!container) return;
2546  container.querySelector('.rule-basis')?.remove();
2547  const basis = document.createElement('div');
2548  basis.className = 'rule-basis';
2549  basis.innerHTML = `<span>规则依据</span><p>${triggers.length ? triggers.map(item => `<b>${escapeHtml(item)}</b>`).join('') : '<b>未命中专用规则</b>'}</p><small>${escapeHtml(boundary)}</small>`;
2550  container.append(basis);
2551}
2552
2553function initializeTruthfulStaticCopy() {
2554  if ($('#evaluateIntakeButton')) $('#evaluateIntakeButton').textContent = '开始转化路径规则初筛 →';
2555  const intakeLabel = $('.intake-result-head span');
2556  if (intakeLabel) intakeLabel.textContent = '规则初筛 · 不是项目价值评分';
2557  if ($('#intakeScore')) $('#intakeScore').innerHTML = '规则<small>初筛</small>';
2558
2559  const resourceBadge = $('.resource-matcher .step-badge');
2560  if (resourceBadge) resourceBadge.textContent = '规';
2561  const resourceDesc = $('.resource-matcher p');
2562  if (resourceDesc) resourceDesc.textContent = '按明确关键词从已登记的集团与合作资源中提出候选方向';
2563  if ($('#matchResourcesButton')) $('#matchResourcesButton').textContent = '按规则推荐资源 →';
2564
2565  const commandMetrics = $$('.command-portfolio-metrics article');
2566  if (commandMetrics[1]) { $('strong', commandMetrics[1]).textContent = '—'; $('span', commandMetrics[1]).textContent = '按真实项目任务动态汇总'; }
2567  if (commandMetrics[2]) { $('strong', commandMetrics[2]).textContent = '—'; $('span', commandMetrics[2]).textContent = '尚未建立本月真实基线'; }
2568  const engineCards = $$('.command-engine-grid .command-engine');
2569  engineCards.forEach(card => {
2570    const metric = $('div strong', card);
2571    if (!metric) return;
2572    if (card.dataset.viewLink === 'investment') metric.textContent = '进入项目后计算';
2573    if (card.dataset.viewLink === 'policyhub') metric.textContent = '按企业与政策计算';
2574    if (card.dataset.viewLink === 'regulatory') metric.textContent = '尚未检索官方原文';
2575  });
2576  const evidenceLedger = $('.command-bottom-grid .evidence-ledger');
2577  if (evidenceLedger) {
2578    const evidenceCount = $('.development-panel-head > span', evidenceLedger);
2579    if (evidenceCount) evidenceCount.textContent = '随所选项目变化';
2580    $$('.development-panel-head p, .evidence-ledger article small', evidenceLedger).forEach(item => item.textContent = item.textContent.replace(/\d+\s*(份|条|项)/g, '待核验'));
2581  }
2582
2583  const privacy = $('.privacy');
2584  if (privacy) privacy.textContent = '请勿输入可识别患者身份的信息';
2585  const exampleTitle = $('.example-title span');
2586  if (exampleTitle) exampleTitle.textContent = '没有头绪?试试这些演示场景';
2587  const stats = $$('.stats-strip > div');
2588  const statLabels = ['真实项目数','需求确认率','样机周期','文档效率'];
2589  stats.forEach((item,index) => {
2590    $('strong', item).textContent = '待建立';
2591    $('small', item).textContent = `${statLabels[index]}基线`;
2592    $('em', item).textContent = '不展示演示数字';
2593  });
2594
2595  const patentSearchTitle = $('.patent-search h2');
2596  if (patentSearchTitle) patentSearchTitle.textContent = '专利检索策略准备';
2597  const patentSearchDesc = $('.patent-search p');
2598  if (patentSearchDesc) patentSearchDesc.textContent = '整理当前项目关键词;真实数据库和法律状态检索待接入';
2599  if ($('#scanPatentsButton')) $('#scanPatentsButton').textContent = '查看真实检索接入要求';
2600  const patentEmptyTitle = $('#patentEmpty h2');
2601  if (patentEmptyTitle) patentEmptyTitle.textContent = '真实专利检索待接入';
2602  const patentEmptyDesc = $('#patentEmpty p');
2603  if (patentEmptyDesc) patentEmptyDesc.textContent = '不会使用固定模板生成项目专利风险或创新空白。';
2604
2605  if ($('#generateConceptsButton')) $('#generateConceptsButton').textContent = '查看概念方法示例';
2606  const conceptHeading = $('.design-control-head h2');
2607  if (conceptHeading) conceptHeading.textContent = '概念方法工作台';
2608  const conceptDesc = $('.design-control-head p');
2609  if (conceptDesc) conceptDesc.textContent = '当前仅展示内置方法样例,不代表已完成项目工程设计';
2610
2611  if ($('#advanceMilestoneButton')) { $('#advanceMilestoneButton').disabled = true; $('#advanceMilestoneButton').textContent = '须有真实交付物后登记'; }
2612  if ($('#runTwinButton')) $('#runTwinButton').textContent = '查看真实仿真接入要求';
2613  const twinConfig = $$('.twin-config > div strong');
2614  if (twinConfig[0]) twinConfig[0].textContent = '尚未连接真实患者库或组织参数';
2615  if (twinConfig[1]) twinConfig[1].textContent = '待工程师定义模型、载荷与边界条件';
2616  if (twinConfig[2]) twinConfig[2].textContent = '待绑定当前项目可验证终点';
2617  if ($('#twinPatientCount')) $('#twinPatientCount').textContent = '—';
2618
2619  $$('#algorithmGrid button span').forEach(item => item.textContent = '候选方向 · 尚未评估');
2620  const kolTitle = $('.kol-panel .development-panel-head strong');
2621  if (kolTitle) kolTitle.textContent = '⑦ KOL 候选标准与核验任务';
2622  const kolDesc = $('.kol-panel .development-panel-head p');
2623  if (kolDesc) kolDesc.textContent = '接入有来源且获授权的专家数据后才能进行匹配';
2624  if ($('#matchKolsButton')) $('#matchKolsButton').textContent = '查看专家数据接入要求';
2625}
2626
2627function aiGatewayBase() {
2628  return window.location.protocol === 'file:' ? 'http://localhost:8787' : window.location.origin;
2629}
2630
2631function updateAiModeUI() {
2632  const professional = state.aiMode === 'professional';
2633  $('#aiModeButton').classList.toggle('professional', professional);
2634  $('#aiModeButton span').textContent = professional ? '专业 AI 模式' : '演示规则模式';
2635}
2636
2637async function checkAiService(options = {}) {
2638  const status = $('#aiServiceStatus');
2639  $('#checkAiServiceButton').disabled = true;
2640  $('#checkAiServiceButton').textContent = '检测中…';
2641  try {
2642    const response = await fetch(`${aiGatewayBase()}/api/health`, { signal: AbortSignal.timeout(5000) });
2643    const data = await response.json();
2644    if (!response.ok || !data.ok) throw new Error(data.error || '服务不可用');
2645    state.aiServiceOnline = Boolean(data.ai_configured);
2646    if ($('#homeAiServiceBadge')) $('#homeAiServiceBadge').innerHTML = `<i></i> ${state.aiServiceOnline ? `DeepSeek 已连接 · ${escapeHtml(data.model || '')}` : 'DeepSeek 尚未配置'}`;
2647    if ($('#platformCapabilitySummary')) $('#platformCapabilitySummary').textContent = state.aiServiceOnline ? '真实AI、规则与工作流已分层' : '规则与工作流可用 · AI待配置';
2648    if (state.aiServiceOnline && options.autoSelect) {
2649      let explicitChoice = false;
2650      let savedMode = null;
2651      try {
2652        explicitChoice = localStorage.getItem('medici-ai-mode-explicit') === '1';
2653        savedMode = localStorage.getItem('medici-ai-mode');
2654      } catch (_) {}
2655      state.aiMode = explicitChoice && ['demo','professional'].includes(savedMode) ? savedMode : 'professional';
2656      updateAiModeUI();
2657      renderCapabilityBanner('workspace');
2658    }
2659    status.classList.toggle('online', state.aiServiceOnline);
2660    $('strong', status).textContent = state.aiServiceOnline ? '统一 DeepSeek AI 已连接' : '服务已启动,但尚未配置 DeepSeek 密钥';
2661    $('small', status).textContent = state.aiServiceOnline ? `模型:${data.model} · 已接入:${(data.ai_domains || []).join('、')}` : '请双击桌面的“配置DeepSeek密钥”完成连接';
2662    $('#enableProfessionalAiButton').disabled = !state.aiServiceOnline;
2663  } catch (error) {
2664    state.aiServiceOnline = false;
2665    if ($('#homeAiServiceBadge')) $('#homeAiServiceBadge').innerHTML = '<i></i> DeepSeek 服务未连接';
2666    if ($('#platformCapabilitySummary')) $('#platformCapabilitySummary').textContent = '本机服务未连接';
2667    status.classList.remove('online');
2668    $('strong', status).textContent = '未检测到专业 AI 服务';
2669    $('small', status).textContent = '请双击“启动华卫天和成果转化平台”后重新检测';
2670    $('#enableProfessionalAiButton').disabled = true;
2671  } finally {
2672    $('#checkAiServiceButton').disabled = false;
2673    $('#checkAiServiceButton').textContent = '重新检测';
2674  }
2675}
2676
2677function setAiMode(mode, options = { explicit: true }) {
2678  state.aiMode = mode;
2679  try {
2680    localStorage.setItem('medici-ai-mode', mode);
2681    if (options.explicit) localStorage.setItem('medici-ai-mode-explicit', '1');
2682  } catch (_) {}
2683  updateAiModeUI();
2684  renderCapabilityBanner('workspace');
2685}
2686
2687async function initializeAiMode() {
2688  let savedMode = null;
2689  let explicitChoice = false;
2690  try {
2691    savedMode = localStorage.getItem('medici-ai-mode');
2692    explicitChoice = localStorage.getItem('medici-ai-mode-explicit') === '1';
2693  } catch (_) {}
2694  if (explicitChoice && ['demo','professional'].includes(savedMode)) state.aiMode = savedMode;
2695  updateAiModeUI();
2696  await checkAiService({ autoSelect: true });
2697}
2698
2699async function callProfessionalAI(payload) {
2700  const response = await fetch(`${aiGatewayBase()}/api/clinical/interview`, {
2701    method: 'POST',
2702    headers: { 'Content-Type': 'application/json' },
2703    body: JSON.stringify(payload)
2704  });
2705  const data = await response.json();
2706  if (!response.ok) throw new Error(data.error || '专业 AI 服务调用失败');
2707  return data;
2708}
2709
2710function normalizeProfessionalData(data) {
2711  return {
2712    summary: data.summary || '正在形成专业问题定义。',
2713    tags: data.tags || [],
2714    mappings: (data.mappings || []).map(item => [item.clinical, item.engineering, item.target]),
2715    constraints: (data.constraints || []).map(item => [item.label, item.value]),
2716    tests: (data.tests || []).map(item => ['◎', `${item.name}:${item.acceptance}`]),
2717    questions: data.question?.q ? [{ q:data.question.q, why:data.question.why, options:data.question.options || [], insight:{} }] : [],
2718    citations: data.citations || [],
2719    uncertainties: data.uncertainties || [],
2720    expertReview: data.expert_review || [],
2721    audit: data._audit || {},
2722    professional: true,
2723    sourceMode: 'ai'
2724  };
2725}
2726
2727async function runProfessionalStart(text) {
2728  $('#resultEmpty').classList.add('hidden');
2729  $('#resultContent').classList.add('hidden');
2730  $('#resultLoading').classList.remove('hidden');
2731  $('#loadingText').textContent = '正在调用 DeepSeek 理解临床描述并识别关键变量…';
2732  $('#loadingBar').style.width = '45%';
2733  $('#analyzeButton').disabled = true;
2734  try {
2735    const response = await callProfessionalAI({ description:text, transcript:[], mode:'start' });
2736    const data = normalizeProfessionalData(response);
2737    createProjectContext({
2738      description: text,
2739      summary: data.summary,
2740      tags: data.tags,
2741      sourceMode: 'ai',
2742      audit: data.audit
2743    });
2744    updateContextProject({ stage: 'AI需求访谈', progress: 12, update: '已完成首轮 DeepSeek 分析', source: 'ai_project' });
2745    renderAnalysis(data);
2746    state.aiSession = { description:text, transcript:[] };
2747    $('#confidenceValue').textContent = `${response.confidence || 45}%`;
2748    const modelName = data.audit.model || 'DeepSeek';
2749    $('#aiEvidenceText').textContent = data.citations.length
2750      ? `本轮已调用 ${modelName},返回 ${data.citations.length} 条依据;仍有 ${data.uncertainties.length} 项不确定性,结论需专家审核。`
2751      : `本轮已调用 ${modelName};当前没有返回可核验外部依据,因此必须保留不确定性并由专家复核。`;
2752    $('#resultLoading').classList.add('hidden');
2753    $('#resultContent').classList.remove('hidden');
2754  } catch (error) {
2755    $('#resultLoading').classList.add('hidden');
2756    $('#resultEmpty').classList.remove('hidden');
2757    showToast(`专业 AI 调用失败:${error.message}`);
2758    $('#aiConfigModal').classList.remove('hidden');
2759  } finally {
2760    $('#analyzeButton').disabled = false;
2761  }
2762}
2763
2764function selectScenario(text) {
2765  const normalized = text.toLowerCase();
2766  let best = null;
2767  let score = 0;
2768  scenarioLibrary.forEach(item => {
2769    const current = item.match.filter(word => normalized.includes(word.toLowerCase())).length;
2770    if (current > score) { score = current; best = item; }
2771  });
2772  return score ? best : fallbackScenario;
2773}
2774
2775function renderAnalysis(data) {
2776  state.analysis = data;
2777  state.answers = {};
2778  state.questionIndex = 0;
2779  state.interviewComplete = false;
2780  state.answerPending = false;
2781  state.patent = { scanned: false, confirmed: false, outputMode: 'unavailable' };
2782  state.flashpoc = { projectId: state.projectContext?.id || null, generated: false, selected: null, confirmed: false, outputMode: 'demo' };
2783  state.regai = { validated: false, documentsGenerated: false, outputMode: 'unavailable' };
2784  state.commercial = { algorithm: null, kolsMatched: false, completed: false, outputMode: 'planning' };
2785  $('#resultSummary').textContent = data.summary;
2786  $('#resultTags').innerHTML = data.tags.map(tag => `<span>${escapeHtml(tag)}</span>`).join('');
2787  $('#mappingCount').textContent = data.mappings.length;
2788  $('#constraintCount').textContent = data.constraints.length;
2789  $('#testCount').textContent = data.tests.length;
2790  $('.confidence').innerHTML = data.professional
2791    ? 'AI判断置信度 <b id="confidenceValue">58%</b>'
2792    : '规则覆盖度 <b id="confidenceValue">仅供演示</b>';
2793  $('#mappingPane').innerHTML = data.mappings.map(row => `<div class="mapping-row"><span>${escapeHtml(row[0])}</span><i>→</i><div><strong>${escapeHtml(row[1])}</strong><em>${escapeHtml(row[2])}</em></div></div>`).join('');
2794  $('#constraintsPane').innerHTML = `<div class="constraint-grid">${data.constraints.map(row => `<div class="constraint-item"><small>${escapeHtml(row[0])}</small><strong>${escapeHtml(row[1])}</strong></div>`).join('')}</div>`;
2795  $('#testsPane').innerHTML = `<div class="test-grid">${data.tests.map(row => `<div class="test-item"><span>${escapeHtml(row[0])}</span><strong>${escapeHtml(row[1])}</strong></div>`).join('')}</div>`;
2796  $('#conversationHistory').innerHTML = '';
2797  $('#interviewCard').classList.remove('completed');
2798  $('#aiEvidenceBar').classList.toggle('hidden', !data.professional);
2799  renderCapabilityBanner('workspace');
2800  $('#currentQuestion').classList.remove('hidden');
2801  $('#interviewComplete').classList.add('hidden');
2802  $('#structuredResults').classList.add('hidden');
2803  $('#generateReportButton').disabled = true;
2804  $('#generateReportButton span').textContent = '完成访谈后生成规格书';
2805  $('#generateReportButton small').textContent = '当前判断尚未收敛';
2806  renderCurrentQuestion();
2807}
2808
2809function renderCurrentQuestion() {
2810  if (!state.analysis) return;
2811  const total = state.analysis.questions.length;
2812  if (state.questionIndex >= total) {
2813    completeInterview();
2814    return;
2815  }
2816  const question = state.analysis.questions[state.questionIndex];
2817  const answered = Object.keys(state.answers).length;
2818  $('#questionProgress').textContent = state.analysis.professional ? `第 ${answered + 1} 问 · 动态访谈` : `第 ${state.questionIndex + 1} / ${total} 问`;
2819  $('#interviewProgressBar').style.width = state.analysis.professional ? `${Math.min(88, 12 + answered * 16)}%` : `${Math.max(8, state.questionIndex / total * 100)}%`;
2820  $('#questionReason').textContent = question.why;
2821  $('#questionTitle').textContent = question.q;
2822  $('#questionOptions').innerHTML = question.options.map(option => `<button data-interview-answer="${escapeHtml(option)}">${escapeHtml(option)}</button>`).join('');
2823  $('#freeAnswerInput').value = '';
2824  $('#freeAnswerInput').placeholder = state.questionIndex === 0 ? '也可以补充更具体的临床情况…' : '上述选项不准确?请直接描述…';
2825}
2826
2827function appendConversation(question, answer, insight) {
2828  const history = $('#conversationHistory');
2829  const exchange = document.createElement('div');
2830  exchange.className = 'conversation-exchange';
2831  const doctor = document.createElement('div');
2832  doctor.className = 'doctor-answer';
2833  const doctorLabel = document.createElement('span');
2834  doctorLabel.textContent = '医生回答';
2835  const doctorText = document.createElement('p');
2836  doctorText.textContent = answer;
2837  doctor.append(doctorLabel, doctorText);
2838  const judgment = document.createElement('div');
2839  judgment.className = 'medici-judgment';
2840  const judgmentLabel = document.createElement('span');
2841  judgmentLabel.textContent = '美第奇判断';
2842  const judgmentText = document.createElement('p');
2843  judgmentText.textContent = insight;
2844  judgment.append(judgmentLabel, judgmentText);
2845  exchange.append(doctor, judgment);
2846  history.append(exchange);
2847  history.scrollTop = history.scrollHeight;
2848}
2849
2850function applyProfessionalFinal(response) {
2851  const data = normalizeProfessionalData(response);
2852  state.analysis = data;
2853  createProjectContext({
2854    description: state.aiSession?.description || state.projectContext?.description || '',
2855    summary: data.summary,
2856    tags: data.tags,
2857    sourceMode: 'ai',
2858    audit: data.audit
2859  });
2860  $('#resultSummary').textContent = data.summary;
2861  $('#resultTags').innerHTML = data.tags.map(tag => `<span>${escapeHtml(tag)}</span>`).join('');
2862  $('#mappingCount').textContent = data.mappings.length;
2863  $('#constraintCount').textContent = data.constraints.length;
2864  $('#testCount').textContent = data.tests.length;
2865  $('#mappingPane').innerHTML = data.mappings.map(row => `<div class="mapping-row"><span>${escapeHtml(row[0])}</span><i>→</i><div><strong>${escapeHtml(row[1])}</strong><em>${escapeHtml(row[2])}</em></div></div>`).join('');
2866  $('#constraintsPane').innerHTML = `<div class="constraint-grid">${data.constraints.map(row => `<div class="constraint-item"><small>${escapeHtml(row[0])}</small><strong>${escapeHtml(row[1])}</strong></div>`).join('')}</div>`;
2867  $('#testsPane').innerHTML = `<div class="test-grid">${data.tests.map(row => `<div class="test-item"><span>${escapeHtml(row[0])}</span><strong>${escapeHtml(row[1])}</strong></div>`).join('')}</div>`;
2868  $('#confidenceValue').textContent = `${response.confidence || 82}%`;
2869  $('#aiEvidenceText').textContent = `${data.audit.model || 'DeepSeek'} 已形成 ${data.mappings.length} 项工程映射、${data.citations.length} 条知识依据和 ${data.expertReview.length} 项专家复核要求。`;
2870  completeInterview();
2871}
2872
2873async function submitProfessionalAnswer(answer) {
2874  const cleanAnswer = answer.trim();
2875  if (!cleanAnswer || !state.aiSession || state.answerPending) return;
2876  const question = state.analysis.questions[0];
2877  state.answerPending = true;
2878  $$('#questionOptions button').forEach(button => button.disabled = true);
2879  $('#sendAnswerButton').disabled = true;
2880  try {
2881    const transcript = [...state.aiSession.transcript, { question:question.q, answer:cleanAnswer }];
2882    const response = await callProfessionalAI({ description:state.aiSession.description, transcript, mode:'continue' });
2883    const insight = response.judgment || '已记录该临床边界,后续将据此收窄工程变量。';
2884    const answerIndex = transcript.length - 1;
2885    state.answers[answerIndex] = { question:question.q, answer:cleanAnswer, insight };
2886    state.aiSession.transcript = transcript;
2887    appendConversation(question.q, cleanAnswer, insight);
2888    $('#confidenceValue').textContent = `${response.confidence || 60}%`;
2889    if (response.status === 'complete' || !response.question?.q) {
2890      applyProfessionalFinal(response);
2891    } else {
2892      state.analysis.summary = response.summary || state.analysis.summary;
2893      state.analysis.tags = response.tags || state.analysis.tags;
2894      state.analysis.questions = [{ q:response.question.q, why:response.question.why, options:response.question.options || [], insight:{} }];
2895      state.questionIndex = 0;
2896      $('#resultSummary').textContent = state.analysis.summary;
2897      $('#resultTags').innerHTML = state.analysis.tags.map(tag => `<span>${tag}</span>`).join('');
2898      renderCurrentQuestion();
2899    }
2900  } catch (error) {
2901    showToast(`专业访谈失败:${error.message}`);
2902  } finally {
2903    state.answerPending = false;
2904    $$('#questionOptions button').forEach(button => button.disabled = false);
2905    $('#sendAnswerButton').disabled = false;
2906  }
2907}
2908
2909function submitInterviewAnswer(answer) {
2910  if (state.aiMode === 'professional' && state.analysis?.professional) {
2911    submitProfessionalAnswer(answer);
2912    return;
2913  }
2914  const cleanAnswer = answer.trim();
2915  if (!cleanAnswer || !state.analysis || state.interviewComplete || state.answerPending) return;
2916  state.answerPending = true;
2917  const question = state.analysis.questions[state.questionIndex];
2918  const insight = question.insight[cleanAnswer] || `已将“${cleanAnswer}”记录为当前临床边界。这个信息会用于收窄设计变量,并在工程验证阶段设置对应的对照条件。`;
2919  state.answers[state.questionIndex] = { question: question.q, answer: cleanAnswer, insight };
2920  appendConversation(question.q, cleanAnswer, insight);
2921  state.questionIndex += 1;
2922  $('#confidenceValue').textContent = `${state.questionIndex}/${state.analysis.questions.length}项规则`;
2923  $('#currentQuestion').classList.add('question-changing');
2924  setTimeout(() => {
2925    $('#currentQuestion').classList.remove('question-changing');
2926    state.answerPending = false;
2927    renderCurrentQuestion();
2928  }, 260);
2929}
2930
2931function completeInterview() {
2932  state.interviewComplete = true;
2933  const total = state.analysis.professional ? Object.keys(state.answers).length : state.analysis.questions.length;
2934  const confirmedAnswers = Object.values(state.answers).map(item => item.answer);
2935  $('#questionProgress').textContent = `${total} / ${total} 已完成`;
2936  $('#interviewProgressBar').style.width = '100%';
2937  $('#interviewCard').classList.add('completed');
2938  $('#currentQuestion').classList.add('hidden');
2939  $('#interviewComplete').classList.remove('hidden');
2940  $('#structuredResults').classList.remove('hidden');
2941  $('#resultSummary').textContent = `${state.analysis.summary} 经访谈确认的关键边界:${confirmedAnswers.join('、')}。`;
2942  const confirmedConstraints = [
2943    ...state.analysis.constraints,
2944    ...Object.values(state.answers).map((item, index) => [`访谈确认 ${index + 1}`, item.answer])
2945  ];
2946  $('#constraintCount').textContent = confirmedConstraints.length;
2947  $('#constraintsPane').innerHTML = `<div class="constraint-grid">${confirmedConstraints.map(row => `<div class="constraint-item"><small>${row[0]}</small><strong>${row[1]}</strong></div>`).join('')}</div>`;
2948  $('#generateReportButton').disabled = false;
2949  const isProfessional = state.analysis.professional;
2950  $('.interview-complete strong').textContent = isProfessional ? '临床语言访谈已完成' : '规则演示问卷已完成';
2951  $('.interview-complete p').textContent = isProfessional
2952    ? '系统已根据多轮回答收敛设计边界;仍需医工专家确认关键参数。'
2953    : '当前结果来自内置场景模板,只用于体验流程,不能作为项目规格输入。';
2954  $('#generateReportButton span').textContent = isProfessional ? '生成《初始设计需求规格书》' : '查看规则演示规格书';
2955  $('#generateReportButton small').textContent = isProfessional ? '已纳入本轮访谈结论' : '仅供界面与方法演示';
2956  showToast(isProfessional ? '访谈完成,工程映射已根据回答收敛' : '规则演示完成,本结果不会进入正式项目档案');
2957}
2958
2959function runAnalysis() {
2960  const input = $('#clinicalInput');
2961  const text = input.value.trim();
2962  if (text.length < 8) {
2963    input.focus();
2964    showToast('请先用一句话描述具体的临床痛点');
2965    return;
2966  }
2967  if (state.aiMode === 'professional') {
2968    runProfessionalStart(text);
2969    return;
2970  }
2971  const scenario = selectScenario(text);
2972  createProjectContext({
2973    description: text,
2974    summary: scenario.summary,
2975    tags: scenario.tags,
2976    sourceMode: 'rule_demo',
2977    audit: { method: '内置关键词场景匹配', externalEvidence: false }
2978  });
2979  updateContextProject({ stage: '规则演示', progress: 0, update: '演示结果不计入正式进度', source: 'demo' });
2980  $('#resultEmpty').classList.add('hidden');
2981  $('#resultContent').classList.add('hidden');
2982  $('#resultLoading').classList.remove('hidden');
2983  $('#analyzeButton').disabled = true;
2984  const steps = [
2985    [18, '运行内置关键词规则…'],
2986    [43, '匹配演示场景模板…'],
2987    [72, '组织示例工程映射…'],
2988    [94, '生成规则演示问题…']
2989  ];
2990  let current = 0;
2991  const timer = setInterval(() => {
2992    const [progress, message] = steps[current];
2993    $('#loadingBar').style.width = `${progress}%`;
2994    $('#loadingText').textContent = message;
2995    $$('.result-loading li').forEach((li, index) => li.classList.toggle('active', index <= current));
2996    current += 1;
2997    if (current === steps.length) {
2998      clearInterval(timer);
2999      setTimeout(() => {
3000        renderAnalysis({ ...scenario, sourceMode: 'rule_demo', professional: false });
3001        $('#resultLoading').classList.add('hidden');
3002        $('#resultContent').classList.remove('hidden');
3003        $('#analyzeButton').disabled = false;
3004      }, 350);
3005    }
3006  }, 360);
3007}
3008
3009function showReport() {
3010  if (!state.analysis) return;
3011  if (!state.interviewComplete) {
3012    showToast('请先完成逐步澄清访谈,再生成精准规格书');
3013    return;
3014  }
3015  $('#reportId').textContent = `文档编号:MD-${new Date().toISOString().slice(0,10).replaceAll('-', '')}-${String(Math.floor(Math.random() * 900) + 100)}`;
3016  $('#reportSummary').textContent = state.analysis.summary;
3017  $('#reportMappings').innerHTML = state.analysis.mappings.map(row => `<div class="mapping-row"><span>${row[0]}</span><i>→</i><div><strong>${row[1]}</strong><em>${row[2]}</em></div></div>`).join('');
3018  const reportConstraints = $('#reportConstraints');
3019  reportConstraints.innerHTML = '';
3020  const confirmedItems = [
3021    ...state.analysis.constraints.map(row => `${row[0]}:${row[1]}`),
3022    ...Object.values(state.answers).map((item, index) => `访谈确认 ${index + 1}:${item.answer}`)
3023  ];
3024  confirmedItems.forEach(item => {
3025    const chip = document.createElement('span');
3026    chip.textContent = item;
3027    reportConstraints.append(chip);
3028  });
3029  $('#reportModal').classList.remove('hidden');
3030}
3031
3032function hydratePatent() {
3033  const summary = state.projectContext?.summary || state.analysis?.summary || '尚未绑定当前项目。请先在临床语言翻译机中建立项目问题定义。';
3034  const tags = state.projectContext?.tags || state.analysis?.tags || [];
3035  $('#patentRequirementSummary').textContent = summary;
3036  $('#patentQueryCount').textContent = tags.length ? `${tags.length} 个项目词` : '尚未生成';
3037  $('#patentQueryChips').innerHTML = tags.length
3038    ? tags.map(tag => `<span>${escapeHtml(tag)}</span>`).join('')
3039    : '<span>等待项目标签</span>';
3040  renderCapabilityBanner('patent');
3041}
3042
3043function scanPatentLandscape() {
3044  $('#patentLoading').classList.add('hidden');
3045  $('#patentResults').classList.add('hidden');
3046  $('#confirmPatentPathButton').classList.add('hidden');
3047  showCapabilityStop(
3048    $('#patentEmpty'),
3049    '真实专利检索尚未接入',
3050    '为避免把固定钳口模板误当作当前项目专利结论,本次不会生成风险等级或“专利空白”。',
3051    ['接入国家知识产权局或商业专利数据库', '返回公开号、申请人、权利要求片段与法律状态', '保存检索式、数据库范围和检索日期', '由专利代理师复核自由实施与可专利性']
3052  );
3053  $('#scanPatentsButton').textContent = '真实专利接口待接入';
3054  showToast('已停止模拟专利结论:请先接入可追溯专利数据源');
3055}
3056
3057function confirmPatentPath() {
3058  showToast('没有可核验的专利检索结果,不能确认专利路径');
3059}
3060
3061function getActiveConcepts() {
3062  const contextText = [
3063    ...(state.projectContext?.tags || state.analysis?.tags || []),
3064    state.projectContext?.summary || state.analysis?.summary || '',
3065    state.projectContext?.description || ''
3066  ].join(' ');
3067  if (/(骨科|骨钻|钻孔|皮质骨|动力工具)/.test(contextText)) return conceptLibraries.orthopedic;
3068  if (/(康复|呼吸训练|居家训练|患者依从性)/.test(contextText)) return conceptLibraries.rehab;
3069  if (/(腹腔镜|抓钳|钳口|夹持器械)/.test(contextText)) return conceptLibraries.laparoscopic;
3070  return [];
3071}
3072
3073function getFlashContext() {
3074  const tags = state.projectContext?.tags || state.analysis?.tags || [];
3075  const text = `${tags.join(' ')} ${state.projectContext?.summary || state.analysis?.summary || ''}`;
3076  if (/(骨科|骨钻|动力工具)/.test(text)) {
3077    return { goal: '识别皮质骨穿透并降低软组织损伤', boundary: '响应 ≤ 20 ms / 单一故障安全' };
3078  }
3079  if (/(康复|呼吸训练|依从性)/.test(text)) {
3080    return { goal: '提升居家训练质量与患者依从性', boundary: '低认知负荷 / 离线可用' };
3081  }
3082  if (/(腹腔镜|抓钳|钳口|夹持器械)/.test(text)) {
3083    return { goal: '提升夹持稳定性并保护组织', boundary: '入路尺寸与组织安全需实测确认' };
3084  }
3085  return { goal: '等待真实项目设计输入', boundary: '尚未形成可核验的工程边界' };
3086}
3087
3088function hydrateFlashPOC() {
3089  if (!state.flashpoc.projectId) {
3090    state.flashpoc.projectId = state.projectContext?.id || `FP-DEMO-${new Date().toISOString().slice(2, 10).replaceAll('-', '')}`;
3091  }
3092  const confirmedAnswers = Object.values(state.answers).map(item => item.answer);
3093  const summary = state.analysis
3094    ? `${state.analysis.summary}${confirmedAnswers.length ? ` 已确认:${confirmedAnswers.join('、')}。` : ''}`
3095    : '尚未绑定项目。请先完成临床语言访谈,再进入概念方法工作台。';
3096  const context = getFlashContext();
3097  $('#flashRequirementSummary').textContent = summary;
3098  $('#flashProjectId').textContent = state.flashpoc.projectId;
3099  $('#flashConstraintCount').textContent = `${(state.analysis?.constraints.length || 4) + confirmedAnswers.length} 项`;
3100  $('#flashConfidence').textContent = state.analysis?.professional ? $('#confidenceValue').textContent : '演示输入';
3101  $('#flashDesignGoal').textContent = context.goal;
3102  $('#flashHardBoundary').textContent = context.boundary;
3103  renderCapabilityBanner('flashpoc');
3104}
3105
3106function setDesignStep(activeStep) {
3107  $$('.design-step').forEach(step => {
3108    const number = Number(step.dataset.designStep);
3109    step.classList.toggle('done', number < activeStep);
3110    step.classList.toggle('active', number === activeStep);
3111    const icon = $('i', step);
3112    icon.textContent = number < activeStep ? '✓' : String(number);
3113  });
3114}
3115
3116function renderConcepts() {
3117  const concepts = getActiveConcepts();
3118  if (!concepts.length) {
3119    $('#conceptLoading').classList.add('hidden');
3120    $('#conceptResults').classList.add('hidden');
3121    showCapabilityStop($('#conceptEmpty'), '没有适用于当前项目的演示模板', '系统已取消“未知项目默认套用腹腔镜抓钳方案”的逻辑。真实概念设计引擎接入前,本项目不会生成方案。', ['由工程师确认产品身份和预期用途', '形成可追溯设计输入', '接入结构生成与工程计算能力']);
3122    $('#generateConceptsButton').disabled = false;
3123    $('#generateConceptsButton').textContent = '真实概念引擎待接入';
3124    return;
3125  }
3126  $('#conceptGrid').innerHTML = concepts.map((concept, index) => `
3127    <button class="concept-card" data-concept-index="${index}" data-tone="${concept.tone}">
3128      <div class="concept-visual"><span>${concept.symbol}</span></div>
3129      <span class="concept-type">方法演示 · ${concept.type}</span>
3130      <h3>${concept.title}</h3>
3131      <p>${concept.desc}</p>
3132      <div class="concept-tradeoff">${concept.tags.map(tag => `<span>${tag}</span>`).join('')}</div>
3133      <div class="concept-score"><span>项目评分</span><b>不评分</b></div>
3134    </button>`).join('');
3135  $('#conceptLoading').classList.add('hidden');
3136  $('#conceptResults').classList.remove('hidden');
3137  $('#generateConceptsButton').disabled = false;
3138  $('#generateConceptsButton').textContent = '↻ 重新查看方法示例';
3139  state.flashpoc.generated = true;
3140  state.flashpoc.outputMode = 'demo';
3141  $('#designTaskStatus').textContent = '方法演示';
3142  $('#confirmConceptButton').disabled = true;
3143  $('#confirmConceptButton').textContent = '演示方案不能创建正式任务';
3144  setDesignStep(3);
3145}
3146
3147function generateConcepts() {
3148  state.flashpoc.selected = null;
3149  state.flashpoc.confirmed = false;
3150  $('#conceptEmpty').classList.add('hidden');
3151  $('#conceptResults').classList.add('hidden');
3152  $('#conceptDetail').classList.add('hidden');
3153  $('#confirmConceptButton').disabled = true;
3154  $('#confirmConceptButton').textContent = '演示方案不能创建正式任务';
3155  $('#conceptLoading').classList.remove('hidden');
3156  $('#generateConceptsButton').disabled = true;
3157  $('#generateConceptsButton').textContent = '正在加载方法示例…';
3158  $('#designTaskStatus').textContent = '演示加载中';
3159  setDesignStep(2);
3160  const progressSteps = [
3161    [24, '读取当前项目标签…'],
3162    [49, '匹配内置方法示例…'],
3163    [76, '标记未验证工程参数…'],
3164    [96, '准备非项目结论的示例卡片…']
3165  ];
3166  let current = 0;
3167  const timer = setInterval(() => {
3168    const [progress, label] = progressSteps[current];
3169    $('#conceptLoadingBar').style.width = `${progress}%`;
3170    $('#conceptLoadingText').textContent = label;
3171    current += 1;
3172    if (current === progressSteps.length) {
3173      clearInterval(timer);
3174      setTimeout(renderConcepts, 280);
3175    }
3176  }, 340);
3177}
3178
3179function selectConcept(index) {
3180  const concept = getActiveConcepts()[index];
3181  if (!concept) return;
3182  state.flashpoc.selected = index;
3183  $$('.concept-card').forEach((card, cardIndex) => card.classList.toggle('selected', cardIndex === index));
3184  $('#selectedConceptType').textContent = concept.type;
3185  $('#selectedConceptTitle').textContent = concept.title;
3186  $('#selectedConceptRationale').textContent = concept.rationale;
3187  $('#selectedConceptParams').innerHTML = concept.params.map(param => `<span>${param}</span>`).join('');
3188  $('#conceptDetail').classList.remove('hidden');
3189  $('#designTaskStatus').textContent = '方向已选择';
3190  $('#conceptDetail').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
3191}
3192
3193function confirmConcept() {
3194  if (state.flashpoc.outputMode !== 'verified') {
3195    showToast('当前仅为方法演示,不能创建正式深化任务');
3196    return;
3197  }
3198  if (state.flashpoc.selected === null) {
3199    showToast('请先从三组方案中选择一个深化方向');
3200    return;
3201  }
3202  const concept = getActiveConcepts()[state.flashpoc.selected];
3203  state.flashpoc.confirmed = true;
3204  setDesignStep(4);
3205  $('#designTaskStatus').textContent = '深化中';
3206  $('#confirmConceptButton').disabled = true;
3207  $('#confirmConceptButton').textContent = '深化任务已创建 ✓';
3208  $('#estimatedDelivery').textContent = '预计 48 小时';
3209  updateContextProject({ stage: '概念设计', progress: 42, update: '已确认经核验的概念方向' });
3210  state.development = {
3211    created: true,
3212    taskId: `DEV-${new Date().toISOString().slice(2, 10).replaceAll('-', '')}-${String(Math.floor(Math.random() * 900) + 100)}`,
3213    phase: 0,
3214    concept,
3215    projectId: state.flashpoc.projectId,
3216    activities: [
3217      { icon: '✓', title: '深化任务已创建', detail: `已锁定“${concept.title}”作为工程深化基线。`, time: '刚刚' },
3218      { icon: '↗', title: '设计输入已同步', detail: 'Medici 约束、访谈答案和概念参数已写入任务包。', time: '刚刚' }
3219    ]
3220  };
3221  persistDevelopmentTask();
3222  $('#developmentNavCount').textContent = '1';
3223  switchView('development');
3224  showToast(`“${concept.title}”深化任务已创建,已进入任务中心`);
3225}
3226
3227function exportConceptBrief() {
3228  if (state.flashpoc.selected === null) return;
3229  const concept = getActiveConcepts()[state.flashpoc.selected];
3230  const content = [
3231    'FlashPOC 概念设计简报',
3232    `项目编号:${state.flashpoc.projectId}`,
3233    `选定方向:${concept.type} / ${concept.title}`,
3234    '',
3235    '临床设计逻辑:',
3236    concept.rationale,
3237    '',
3238    '关键工程参数:',
3239    ...concept.params.map(param => `- ${param}`)
3240  ].join('\n');
3241  const url = URL.createObjectURL(new Blob([content], { type: 'text/plain;charset=utf-8' }));
3242  const link = document.createElement('a');
3243  link.href = url;
3244  link.download = `${state.flashpoc.projectId}-概念设计简报.txt`;
3245  link.click();
3246  URL.revokeObjectURL(url);
3247  showToast('概念设计简报已导出');
3248}
3249
3250const developmentPhases = [
3251  { progress: 36, title: '关键参数冻结完成', detail: '已生成关键参数表 v1.0,夹持力、曲率与组织压强边界进入受控状态。', output: '关键参数表', file: '关键参数表-v1.0.txt', hint: '参数已冻结,下一步将运行受力与接触压强仿真。' },
3252  { progress: 62, title: '数字仿真计算完成', detail: '已完成标准工况、湿态滑移和峰值载荷仿真,未发现不可接受风险。', output: '数字仿真报告', file: '数字仿真报告-v0.8.txt', hint: '仿真结果已收敛,下一步进入可制造性和材料工艺评审。' },
3253  { progress: 84, title: 'DFM 与工艺评审通过', detail: '已确定打印材料、装配公差与支撑策略,BOM 和制造包已生成。', output: '制造包与 BOM', file: '制造包与BOM-v0.9.txt', hint: '制造文件已释放,下一步安排多材质样机制作与触感验证。' },
3254  { progress: 100, title: '首件样机制造完成', detail: '多材质触感样机已完成装配检查,等待医生进行首轮手感评审。', output: '样机评审记录', file: '样机评审记录-v1.0.txt', hint: '全部深化阶段已完成,样机已进入医生评审队列。' }
3255];
3256
3257function hydrateDevelopment() {
3258  const task = state.development;
3259  $('#developmentEmpty').classList.toggle('hidden', task.created);
3260  $('#developmentContent').classList.toggle('hidden', !task.created);
3261  $('#developmentStatusText').textContent = task.created
3262    ? (task.evidenceMode === 'verified' ? (task.phase >= 4 ? '样机待医生评审' : '工程深化进行中') : '历史演示任务 · 已隔离')
3263    : '等待真实设计输入';
3264  renderCapabilityBanner('development');
3265  if (!task.created) return;
3266  const concept = task.concept || getActiveConcepts()[state.flashpoc.selected] || getActiveConcepts()[0] || { title:'未核验概念记录', type:'历史演示' };
3267  $('#developmentTaskCode').textContent = task.taskId;
3268  $('#developmentConceptTitle').textContent = concept.title;
3269  $('#developmentConceptSubtitle').textContent = `${concept.type} · ${task.projectId || state.flashpoc.projectId || 'FlashPOC 项目'}`;
3270  renderDevelopmentTask();
3271}
3272
3273function renderDevelopmentTask() {
3274  const task = state.development;
3275  const verified = task.evidenceMode === 'verified';
3276  const progressValues = [12, 36, 62, 84, 100];
3277  const progress = progressValues[task.phase];
3278  $('#developmentProgressValue').textContent = `${progress}%`;
3279  $('#developmentProgressBar').style.width = `${progress}%`;
3280  $('#currentMilestoneLabel').textContent = task.phase >= 4 ? '4 / 4 全部完成' : `阶段 ${task.phase + 1} / 4`;
3281  $$('.milestone').forEach((milestone, index) => {
3282    const isDone = index < task.phase;
3283    const isActive = index === task.phase && task.phase < 4;
3284    milestone.classList.toggle('done', isDone);
3285    milestone.classList.toggle('active', isActive);
3286    const status = $('b', milestone);
3287    status.textContent = verified ? (isDone ? '已完成' : isActive ? '进行中' : '等待') : (isDone ? '演示记录' : '待真实成果');
3288    $('i', milestone).textContent = isDone ? '✓' : String(index + 1).padStart(2, '0');
3289  });
3290  const hints = [
3291    '当前正在冻结关键工程参数。',
3292    '当前正在执行受力、接触压强与极端工况仿真。',
3293    '当前正在进行可制造性、材料和装配公差评审。',
3294    '当前正在制作多材质样机并准备医生触感评审。',
3295    '全部深化阶段已完成,样机已进入医生评审队列。'
3296  ];
3297  $('#milestoneActionHint').textContent = verified ? hints[task.phase] : '已停止模拟完成阶段。请上传并审核真实参数表、仿真报告、DFM记录或样机评审记录后再登记。';
3298  $('#advanceMilestoneButton').disabled = true;
3299  $('#advanceMilestoneButton').textContent = '须有真实交付物后登记';
3300  $('#enterRegAIButton').classList.add('hidden');
3301  $('#advanceMilestoneButton').classList.remove('hidden');
3302  $('#developmentStatusText').textContent = verified ? (task.phase >= 4 ? '样机待医生评审' : '工程深化进行中') : '历史演示任务 · 不计入正式档案';
3303  $('#deliverableCount').textContent = verified ? `${task.phase} / 4 已生成` : '0 / 4 已核验';
3304  $$('#deliverableList button').forEach((button, index) => {
3305    const ready = index < task.phase;
3306    button.classList.toggle('ready', ready);
3307    $('small', button).textContent = verified && ready ? `${developmentPhases[index].file} · 可查看` : (ready ? '历史演示记录 · 不可作为交付物' : '等待上传并审核真实文件');
3308    $('b', button).textContent = verified && ready ? '查看' : '未核验';
3309  });
3310  $('#developmentActivityList').innerHTML = task.activities.map(item => `<div class="activity-item"><i>${item.icon}</i><div><strong>${item.title}</strong><p>${item.detail}</p></div><time>${item.time}</time></div>`).join('');
3311}
3312
3313function advanceDevelopmentMilestone() {
3314  const task = state.development;
3315  if (task.evidenceMode !== 'verified') {
3316    showToast('已停止模拟推进:请先上传并审核真实工程交付物');
3317    return;
3318  }
3319  if (!task.created || task.phase >= 4) return;
3320  const completed = developmentPhases[task.phase];
3321  task.activities.unshift({ icon: '✓', title: completed.title, detail: completed.detail, time: '刚刚' });
3322  task.phase += 1;
3323  updateContextProject({ progress: [42, 55, 68, 86, 100][task.phase], stage: task.phase >= 4 ? '样机评审' : '结构深化', update: completed.title });
3324  persistDevelopmentTask();
3325  renderDevelopmentTask();
3326  showToast(`${completed.output}已生成,任务进度更新为 ${developmentPhases[task.phase - 1].progress}%`);
3327}
3328
3329function downloadDeliverable(index) {
3330  if (state.development.evidenceMode !== 'verified') {
3331    showToast('历史演示记录已隔离,不能作为真实工程交付物导出');
3332    return;
3333  }
3334  if (index >= state.development.phase) {
3335    showToast('该交付物尚未生成,请先完成对应里程碑');
3336    return;
3337  }
3338  const concept = state.development.concept || getActiveConcepts()[state.flashpoc.selected] || getActiveConcepts()[0];
3339  const phase = developmentPhases[index];
3340  const content = `${phase.output}\n任务编号:${state.development.taskId}\n深化方向:${concept.title}\n\n${phase.detail}\n\n本文件由 FlashPOC MVP 生成。`;
3341  const url = URL.createObjectURL(new Blob([content], { type: 'text/plain;charset=utf-8' }));
3342  const link = document.createElement('a');
3343  link.href = url;
3344  link.download = phase.file;
3345  link.click();
3346  URL.revokeObjectURL(url);
3347  showToast(`${phase.output}已导出`);
3348}
3349
3350function persistDevelopmentTask() {
3351  try {
3352    localStorage.setItem('medici-development-task', JSON.stringify(state.development));
3353  } catch (_) {
3354    // Local files may disable storage in strict browser modes; the in-session task remains available.
3355  }
3356}
3357
3358function restoreDevelopmentTask() {
3359  try {
3360    const saved = JSON.parse(localStorage.getItem('medici-development-task'));
3361    if (!saved?.created || !saved.taskId) return;
3362    state.development = { ...saved, evidenceMode: saved.evidenceMode || 'legacy_demo' };
3363    state.flashpoc.projectId = saved.projectId || null;
3364    $('#developmentNavCount').textContent = '1';
3365    const legacyProject = projects.find(item => item.id === saved.projectId);
3366    if (legacyProject) legacyProject.update = '发现历史演示任务,已隔离';
3367  } catch (_) {
3368    // Ignore invalid or unavailable local storage.
3369  }
3370}
3371
3372function hydrateRegAI() {
3373  const ready = state.development.created && state.development.phase >= 4;
3374  $('#regaiEmpty').classList.toggle('hidden', ready);
3375  $('#regaiContent').classList.toggle('hidden', !ready);
3376  renderCapabilityBanner('regai');
3377  if (!ready) return;
3378  const concept = state.development.concept || getActiveConcepts()[0] || { title:'未核验历史任务' };
3379  $('#regaiProjectSummary').textContent = `${concept.title}的历史演示记录已识别,但尚未形成可供验证的受控工程输入。`;
3380  state.regai.validated = false;
3381  state.regai.documentsGenerated = false;
3382  $('#generateRegDocsButton').disabled = true;
3383  $('#enterCommercialButton').classList.add('hidden');
3384}
3385
3386function runTwinValidation() {
3387  $('#twinLoading').classList.add('hidden');
3388  $('#twinStatus').textContent = '真实引擎待接入';
3389  showCapabilityStop(
3390    $('#twinResults'),
3391    '不能运行模拟数字孪生验证',
3392    '当前没有几何模型、材料参数、边界条件、求解器或真实患者数据。系统不会再用进度动画生成“验证通过”结果。',
3393    ['导入受控 CAD / 网格与材料参数', '由工程师定义载荷和边界条件', '接入经验证的力学求解器', '保存模型版本、输入文件、日志和审核签字']
3394  );
3395  $('#runTwinButton').textContent = '真实仿真环境待接入';
3396  $('#generateRegDocsButton').disabled = true;
3397  showToast('已停止模拟验证:没有真实模型与计算日志,不生成通过结论');
3398}
3399
3400function showTwinResults() {
3401  runTwinValidation();
3402}
3403
3404function generateRegDocuments() {
3405  showToast('缺少真实验证证据,不能生成合规文档包');
3406}
3407
3408function markRegDocumentsReady() {
3409  if (state.regai.outputMode !== 'verified') {
3410    showToast('演示验证记录已隔离,不能标记合规文档为已生成');
3411    return;
3412  }
3413  const names = ['DHF-v1.0.txt','风险管理报告-v0.9.txt','虚拟验证报告-v1.0.txt','临床评价框架-v0.8.txt'];
3414  $$('#regDocGrid button').forEach((button,index) => {
3415    button.classList.add('ready');
3416    $('small',button).textContent = `${names[index]} · 可导出`;
3417    $('b',button).textContent = '查看';
3418  });
3419  $('#generateRegDocsButton').disabled = false;
3420  $('#generateRegDocsButton').textContent = '↻ 重新生成文档包';
3421  $('#regaiCompleteness').textContent = '94%';
3422  $('#enterCommercialButton').classList.remove('hidden');
3423  updateContextProject({ stage:'注册准备', progress:94, update:'经核验的合规文档包已生成' });
3424}
3425
3426function downloadRegDocument(index) {
3427  if (!state.regai.documentsGenerated) { showToast('请先运行验证并生成合规文档包'); return; }
3428  const names = ['设计历史文件 DHF','风险管理报告','虚拟性能与安全报告','临床评价报告框架'];
3429  const content = `${names[index]}\n项目:${state.projectContext?.title || state.projectContext?.id || '当前项目'}\n版本:待审核草案\n\n仅可纳入已核验的需求、设计决策、工程变更记录与验证证据。\n\n注意:正式申报前需由法规负责人审核并签字。`;
3430  const url = URL.createObjectURL(new Blob([content],{type:'text/plain;charset=utf-8'}));
3431  const link = document.createElement('a'); link.href=url; link.download=`${names[index]}.txt`; link.click(); URL.revokeObjectURL(url);
3432  showToast(`${names[index]}已导出`);
3433}
3434
3435function hydrateCommercial() {
3436  const concept = state.development.concept || getActiveConcepts()[0];
3437  $('#commercialProjectTitle').textContent = concept?.title || state.projectContext?.title || '尚未绑定经核验项目';
3438  renderCapabilityBanner('commercial');
3439}
3440
3441function selectAlgorithm(name) {
3442  state.commercial.algorithm = name;
3443  $$('#algorithmGrid button').forEach(button => button.classList.toggle('selected',button.dataset.algorithm===name));
3444  $('#algorithmSelectionStatus').textContent = '规划假设';
3445  $('#selectedAlgorithmName').textContent = name;
3446  const plans = {
3447    '力反馈预警':'规划假设:先确认目标动作与风险事件,再设计传感采集、标注规则、临床安全阈值和前瞻性验证方案;当前未评估项目适配度。',
3448    '组织类型辅助识别':'规划假设:先定义临床用途、目标组织类别和错误后果,再建立有授权的数据集与独立验证集;当前未评估项目适配度。',
3449    '术中操作质量分析':'规划假设:先由临床专家定义关键动作和可解释质量终点,再设计数据采集与验证;当前未评估项目适配度。'
3450  };
3451  $('#algorithmPlanText').textContent = plans[name];
3452  $('#algorithmPlan').classList.remove('hidden');
3453  updateCommercialReady();
3454}
3455
3456function matchKols() {
3457  state.commercial.kolsMatched = false;
3458  $('#kolResults').classList.add('hidden');
3459  showCapabilityStop(
3460    $('#kolEmpty'),
3461    '真实 KOL 数据库尚未接入',
3462    '系统不会再虚构专家姓名、手术量或匹配率。接入合规数据并获得授权前,只能建立候选专家筛选标准。',
3463    ['明确目标科室、术式与合作任务', '导入有来源且获授权的专家资料', '核验任职、研究方向和合作意愿', '由业务负责人确认联系与合作安排']
3464  );
3465  $('#matchKolsButton').textContent = '真实专家数据待接入';
3466  updateCommercialReady();
3467  showToast('已停止模拟专家匹配:不会生成虚构姓å
3467å’ŒåŒ¹é…çއ');
3468}
3469
3470function updateCommercialReady() {
3471  $('#completeJourneyButton').disabled = !(state.commercial.algorithm && state.commercial.kolsMatched);
3472}
3473
3474function completeCommercialJourney() {
3475  showToast('缺少真实 KOL、市场与验证数据,不能生成正式商业化方案');
3476}
3477
3478function exportAlgorithmPlan() {
3479  if (!state.commercial.algorithm) return;
3480  const content = `算法能力调研任务草案\n项目:${state.projectContext?.title || state.projectContext?.id || '当前项目'}\n候选能力:${state.commercial.algorithm}\n\n${$('#algorithmPlanText').textContent}\n\n边界:本文件是规划假设,不是项目适配性结论。`;
3481  const url=URL.createObjectURL(new Blob([content],{type:'text/plain;charset=utf-8'}));const link=document.createElement('a');link.href=url;link.download='AI算法集成路线.txt';link.click();URL.revokeObjectURL(url);
3482}
3483
3484function runInvestmentReview() {
3485  if (!currentInvestmentProject) { showToast('请先建立项目档案,再进行摘要初筛'); renderInvestmentWorkflowStep(1); return; }
3486  const text = $('#investmentSummary').value.trim();
3487  if (text.length < 30) { $('#investmentSummary').focus(); showToast('请补充产品、进展、团队和融资需求等核心材料'); return; }
3488  const dimensions = [
3489    ['需求逻辑', ['痛点','患者','临床','需求','病原体'], 14],
3490    ['转化逻辑', ['样品','原型','实验','生产','产业化'], 11],
3491    ['临床注册逻辑', ['注册','临床','检验','nmpa'], 9],
3492    ['市场逻辑', ['市场','客户','医院','销售'], 12],
3493    ['竞争逻辑', ['竞品','竞争','替代','同类'], 11],
3494    ['商业化逻辑', ['定价','收入','渠道','商业'], 9],
3495    ['团队逻辑', ['团队','高校','医院','负责人'], 13],
3496    ['壁垒逻辑', ['专利','知识产权','数据','工艺'], 12],
3497    ['回报逻辑', ['融资','估值','退出','现金流'], 9]
3498  ];
3499  let weighted = 0;
3500  let evidenceHits = 0;
3501  const scored = dimensions.map(([name, keywords, weight]) => {
3502    const hits = keywords.filter(keyword => text.toLowerCase().includes(keyword)).length;
3503    evidenceHits += hits;
3504    const score = 5;
3505    const coverage = Math.min(100, Math.round(hits / keywords.length * 100));
3506    const uncertainty = coverage >= 75 ? 1.3 : coverage >= 50 ? 2 : 2.8;
3507    weighted += score * weight / 100;
3508    return { name, score:score.toFixed(1), potential_score:score, score_range:{low:Number(Math.max(0,score-uncertainty).toFixed(1)),high:Number(Math.min(10,score+uncertainty).toFixed(1))}, evidence_coverage:coverage, hits, evidence:hits ? `摘要中命中 ${hits} 个相关词;尚未形成专业价值判断` : 'BP中未提及相关内容', strengths:[], weaknesses:['摘要规则不足以判断该维度的真实项目价值,需AI全文分析或专家核验'], score_reason:`当前仅有摘要线索,采用5分中性占位,不将材料缺项解释为项目缺陷;合理区间${Math.max(0,score-uncertainty).toFixed(1)}—${Math.min(10,score+uncertainty).toFixed(1)}分。`, score_audit:{method:'摘要线索识别 · 不评项目价值',confidence:'低',evidence_coverage:coverage,criteria:[{criterion:'是否已有可分析线索',max_points:10,awarded_points:hits ? 5 : null,status:hits ? '文字陈述' : '待验证',reason:hits ? '摘要出现相关词语,但不能据此判断项目价值。' : '摘要未描述,不因此给项目扣零分。',evidence_quote:hits ? `命中${hits}个相关词` : '',data_status:hits ? '仅摘要线索' : '未知'}],boundary:'摘要初筛只整理线索和问题,不输出有决策意义的项目价值分'}};
3509  });
3510  const missing = {
3511    '专利权属': !/(专利|知识产权|权属)/.test(text),
3512    '可核验实验数据': !/(实验|样品|数据|原型)/.test(text),
3513    '注册路径与预算': !/(注册|临床|检验|预算)/.test(text),
3514    '财务预测与资金用途': !/(收入|现金流|融资|资金用途|预算)/.test(text)
3515  };
3516  const redFlags = Object.entries(missing).filter(([, absent]) => absent).map(([name]) => name);
3517  const coverage = Math.min(100, Math.round(evidenceHits / 36 * 100));
3518  $('#investmentConfidence').textContent = `摘要线索覆盖 ${coverage}% · 价值置信度低`;
3519  $('#investmentScore').innerHTML = `${weighted.toFixed(1)}<small>/10</small>`;
3520  $('#investmentVerdict').textContent = '待AI全文研判,摘要仅建立核验清单';
3521  $('#investmentJudgment').textContent = `摘要中识别到 ${evidenceHits} 项相关线索;5分是信息不足时的中性占位,不代表项目一般,更不会因BP没写而判项目没有价值。`;
3522  currentBpDimensions = scored;
3523  $('#investmentResult').classList.remove('hidden');
3524  currentInvestmentAnalysis = {overall_score:Number(weighted.toFixed(1)),potential_score:Number(weighted.toFixed(1)),score_range:{low:2.2,high:7.8},evidence_coverage:coverage,evidence_confidence:'低',recommendation:$('#investmentVerdict').textContent,summary:$('#investmentJudgment').textContent,dimensions:scored,risks:redFlags.map(description=>({level:'待定',category:'待验证',description})),scoring_audit:{method:'摘要线索识别 · 不评项目价值'}};
3525  renderInvestmentReviewChecklist();
3526  renderInvestmentWorkflowStep(3, true);
3527  $('#investmentResult').scrollIntoView({behavior:'smooth',block:'nearest'});
3528}
3529
3530async function investmentApiRequest(path, options = {}) {
3531  const token = localStorage.getItem('token');
3532  const headers = {...(options.headers || {})};
3533  if (token) headers.Authorization = `Bearer ${token}`;
3534  const response = await fetch(`${BP_API_BASE}${path}`, {...options,headers});
3535  const raw = await response.text();
3536  let payload;
3537  try { payload = JSON.parse(raw); } catch { throw new Error(`投资项目服务返回异常:${raw.slice(0,100)}`); }
3538  if (!response.ok) throw new Error(payload.detail || payload.error || '投资项目操作失败');
3539  return payload.data;
3540}
3541
3542function enterpriseArray(value) {
3543  return Array.isArray(value) ? value.filter(Boolean) : [];
3544}
3545
3546function renderEnterpriseTags(items, limit = 8) {
3547  const values = enterpriseArray(items).slice(0, limit);
3548  return values.length ? `<div class="base-enterprise-tags">${values.map(item => `<span>${escapeHtml(item)}</span>`).join('')}</div>` : '<span class="base-enterprise-missing">资料未提供</span>';
3549}
3550
3551function renderEnterpriseMatches(target, matches) {
3552  const node = typeof target === 'string' ? $(target) : target;
3553  if (!node) return;
3554  node.innerHTML = enterpriseArray(matches).length ? matches.map(item => `<article class="enterprise-match-card" data-company-id="${escapeHtml(item.company_id)}"><header><span><small>${escapeHtml(item.category || '分类待核验')}</small><strong>${escapeHtml(item.company_name)}</strong></span><b>${escapeHtml(item.score)}<em>/100</em></b></header><div>${renderEnterpriseTags(item.matched_terms, 6)}</div><ul>${enterpriseArray(item.reasons).map(reason => `<li>${escapeHtml(reason)}</li>`).join('')}</ul>${enterpriseArray(item.matched_products).length ? `<p><b>相关产品:</b>${escapeHtml(item.matched_products.join(';'))}</p>` : ''}<footer><span>${escapeHtml(item.level)}</span><button class="outline-button" type="button" data-open-base-company="${escapeHtml(item.company_id)}">查看企业画像</button></footer></article>`).join('') : '<div class="base-enterprise-no-match"><span class="no-match-icon">🔍</span><strong>暂未发现达到阈值的企业关联</strong><p>这不代表基地内没有合适企业——可能是BP对技术路线与产品关键词描述不足,或企业库对应标签字段尚未补齐。</p><ul class="no-match-tips"><li>在BP中补充技术路线、适应症与产品关键词后重新分析</li><li>到企业库补齐企业标签与产品技术字段</li><li>适当降低匹配阈值,或稍后重试关联分析</li><li>点击右上角“打开完整企业库”直接浏览全部企业</li></ul></div>';
3555}
3556
3557function renderBaseEnterpriseStats(stats) {
3558  baseEnterpriseStats = stats || {};
3559  const metrics = [
3560    ['企业', stats.company_count || 0, '已合并同名记录'],
3561    ['产品/技术记录', stats.product_count || 0, `${stats.category_count || 0}个产业分类`],
3562    ['有合作需求', stats.companies_with_needs || 0, '可进一步确认意愿'],
3563    ['有共享资源', stats.companies_with_resources || 0, '平台能力待核验'],
3564  ];
3565  $('#baseEnterpriseMetrics').innerHTML = metrics.map(item => `<article><small>${item[0]}</small><strong>
3565${Number(item[1]).toLocaleString()}</strong><span>${item[2]}</span></article>`).join('');
3566  const categories = Object.keys(stats.category_counts || {});
3567  $('#baseEnterpriseCategory').innerHTML = '<option value="">全部分类</option>' + categories.map(item => `<option>${escapeHtml(item)}</option>`).join('');
3568}
3569
3570function baseEnterpriseQueryPath() {
3571  const params = new URLSearchParams();
3572  const query = $('#baseEnterpriseSearch')?.value.trim();
3573  const category = $('#baseEnterpriseCategory')?.value;
3574  const stage = $('#baseEnterpriseStage')?.value;
3575  if (query) params.set('query', query);
3576  if (category) params.set('category', category);
3577  if (stage) params.set('stage', stage);
3578  params.set('limit', '500');
3579  return `/api/base-enterprises?${params.toString()}`;
3580}
3581
3582// ---------- 基地企业创新动态 ----------
3583let baseEnterpriseNewsExpanded = false;
3584
3585function renderBaseEnterpriseNews(payload) {
3586  const body = $('#baseEnterpriseNewsBody');
3587  if (!body) return;
3588  const updatedNode = $('#baseEnterpriseNewsUpdated');
3589  if (updatedNode) updatedNode.textContent = payload?.updated_at ? `更新于 ${payload.updated_at}` : '更新于 —';
3590  const days = enterpriseArray(payload?.days).slice(0, 14);
3591  if (!days.length) {
3592    body.innerHTML = '<div class="base-enterprise-no-match"><strong>今日动态抓取任务尚未运行,暂无动态</strong><p>系统每日自动检索基地企业公开新闻后在此展示;新闻来自公开网络检索,仅供研究参考。</p></div>';
3593    return;
3594  }
3595  const shown = baseEnterpriseNewsExpanded ? days : days.slice(0, 1);
3596  body.innerHTML = shown.map(day => {
3597    const items = enterpriseArray(day.items);
3598    const groups = {};
3599    items.forEach(item => { const cat = item.category || '其他'; (groups[cat] = groups[cat] || []).push(item); });
3600    const groupHtml = Object.entries(groups).map(([cat, list]) => `<div class="base-news-group"><strong class="base-news-category">${escapeHtml(cat)}<span>${list.length}条</span></strong><div class="base-news-grid">${list.map(item => `<article class="base-news-item"><header><b>${escapeHtml(item.company_name || '基地企业')}</b>${item.news_type ? `<span>${escapeHtml(item.news_type)}</span>` : ''}</header>${item.url ? `<a href="${escapeHtml(item.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(item.title || '查看详情')} ↗</a>` : `<strong class="base-news-title">${escapeHtml(item.title || '')}</strong>`}${item.summary ? `<p>${escapeHtml(item.summary)}</p>` : ''}<footer>${escapeHtml(item.source || '公开网络')} · ${escapeHtml(item.published_at || day.date || '')}</footer></article>`).join('')}</div></div>`).join('');
3601    return `<section class="base-news-day"><h4>${escapeHtml(day.date)}<span>${items.length}条动态</span></h4>${groupHtml}</section>`;
3602  }).join('') + (days.length > 1 ? `<button class="outline-button base-news-expand" id="baseEnterpriseNewsExpand" type="button">${baseEnterpriseNewsExpanded ? '收起历史动态' : `展开历史动态(共${days.length}天)`}</button>` : '');
3603  $('#baseEnterpriseNewsExpand')?.addEventListener('click', () => {
3604    baseEnterpriseNewsExpanded = !baseEnterpriseNewsExpanded;
3605    renderBaseEnterpriseNews(payload);
3606  });
3607}
3608
3609async function loadBaseEnterpriseNews() {
3610  try {
3611    const payload = await investmentApiRequest('/api/base-enterprises/news');
3612    renderBaseEnterpriseNews(payload);
3613  } catch (error) {
3614    const body = $('#baseEnterpriseNewsBody');
3615    if (body) body.innerHTML = `<div class="base-enterprise-no-match"><strong>创新动态暂未连接</strong><p>${escapeHtml(error.message)}</p></div>`;
3616  }
3617}
3618
3619function renderBaseEnterpriseList() {
3620  $('#baseEnterpriseListCount').textContent = `${baseEnterpriseCompanies.length}å®¶`;
3621  $('#baseEnterpriseList').innerHTML = baseEnterpriseCompanies.length ? baseEnterpriseCompanies.map(company => `<button class="base-enterprise-list-item${currentBaseEnterprise?.id === company.id ? ' active' : ''}" type="button" data-base-company="${escapeHtml(company.id)}"><span><small>${escapeHtml(company.primary_category || '分类待核验')}</small><strong>${escapeHtml(company.name)}</strong></span>
3621<p>${escapeHtml(enterpriseArray(company.technology_tags).slice(0,4).join(' · ') || enterpriseArray(company.products).slice(0,2).map(item=>item.name).join(' · ') || '产品技术信息待补充')}</p><footer><b>${enterpriseArray(company.products).length}项产品/技术</b><em>画像${escapeHtml(company.profile_completeness || 0)}%</em></footer></button>`).join('') : '<div class="base-enterprise-no-match"><strong>没有符合当前筛选的企业</strong><p>可以清除筛选或换一个产品、技术关键词。</p></div>';
3622}
3623
3624function renderBaseEnterpriseDetail(company) {
3625  currentBaseEnterprise = company;
3626  renderBaseEnterpriseList();
3627  const products = enterpriseArray(company.products);
3628  $('#baseEnterpriseDetail').innerHTML = `<header class="base-enterprise-detail-head"><div><span>${escapeHtml(company.primary_category || '分类待核验')}</span><h2>${escapeHtml(company.name)}</h2><p>${escapeHtml(company.description || `${products.length}项产品/技术记录,来源为基地内部企业资料。`)}</p></div><b>画像完整度 ${escapeHtml(company.profile_completeness || 0)}%</b></header><div class="base-enterprise-detail-actions"><button class="analyze-button" type="button" id="importBaseEnterprisePolicy">导入政策规划企业档案 →</button><span>导入后仍可继续上传营业执照、审计、资质等证明材料</span></div><section><h3>产业与技术标签</h3>${renderEnterpriseTags([...(company.categories || []), ...(company.technology_tags || [])], 12)}</section><section><h3>产品 / 核心技术</h3><div class="base-enterprise-product-list">${products.map(item => `<article><strong>${escapeHtml(item.name || '未命名产品')}</strong><span>${escapeHtml(item.stage || '阶段待核验')}</span>${item.advantage ? `<p>${escapeHtml(item.advantage)}</p>` : ''}</article>`).join('') || '<p>暂无结构化产品记录</p>'}</div></section><div class="base-enterprise-detail-grid"><section><h3>合作需求</h3>${renderEnterpriseTags(company.cooperation_needs, 10)}</section><section><h3>可共享平台 / 资源</h3>${renderEnterpriseTags(company.shared_resources, 10)}</section><section><h3>合作院所</h3>${renderEnterpriseTags(company.partner_institutes, 10)}</section><section><h3>荣誉及专项支持</h3>${renderEnterpriseTags(company.honors_support, 8)}</section></div><details class="base-enterprise-confidential"><summary>查看内部联络与融资信息</summary><p><b>联系人:</b>${escapeHtml(company.contact_name || '未提供')} <b>联系方式:</b>${escapeHtml(company.contact_phone || '未提供')}</p><p><b>基金支持:</b>${escapeHtml(company.funding_needed || '未提供')} ${escapeHtml(company.funding_details || '')}</p><small>${escapeHtml(company.confidentiality || '内部使用,不得对外展示')}</small></details><footer class="base-enterprise-source">来源:${escapeHtml(enterpriseArray(company.source_refs)[0]?.file || '企业信息整理表')} · 更新口径 ${escapeHtml(company.source_updated_at || '待核验')}</footer>`;
3629  $('#importBaseEnterprisePolicy')?.addEventListener('click', () => importBaseEnterpriseToPolicy(company.id));
3630}
3631
3632async function selectBaseEnterprise(companyId) {
3633  try {
3634    const company = await investmentApiRequest(`/api/base-enterprises/${encodeURIComponent(companyId)}`);
3635    renderBaseEnterpriseDetail(company);
3636  } catch (error) { showToast(error.message); }
3637}
3638
3639async function refreshBaseEnterpriseList() {
3640  $('#baseEnterpriseList').innerHTML = '<p>正在筛选企业…</p>';
3641  try {
3642    baseEnterpriseCompanies = await investmentApiRequest(baseEnterpriseQueryPath());
3643    renderBaseEnterpriseList();
3644  } catch (error) {
3645    $('#baseEnterpriseList').innerHTML = `<div class="base-enterprise-no-match"><strong>企业库暂未连接</strong><p>${escapeHtml(error.message)}</p></div>`;
3646  }
3647}
3648
3649async function loadBaseEnterpriseLibrary(force = false) {
3650  if (baseEnterpriseLoaded && !force) return;
3651  try {
3652    const [stats, projects] = await Promise.all([
3653      investmentApiRequest('/api/base-enterprises/stats'),
3654      investmentApiRequest('/api/professional/projects'),
3655    ]);
3656    renderBaseEnterpriseStats(stats);
3657    loadBaseEnterpriseNews();
3658    initBasePolicyScanPanel();
3659    const analyzed = enterpriseArray(projects).filter(item => item.has_analysis);
3660    $('#baseEnterpriseProjectSelect').innerHTML = '<option value="">选择已完成分析的BP项目</option>' + analyzed.map(item => `<option value="${escapeHtml(item.id)}">${escapeHtml(item.name)}${item.track ? ` · ${escapeHtml(item.track)}` : ''}</option>`).join('');
3661    await refreshBaseEnterpriseList();
3662    baseEnterpriseLoaded = true;
3663  } catch (error) { showToast(error.message); }
3664}
3665
3666// ============ 基地企业 × 政策库:批量机会扫描总览 ============
3667
3668let basePolicyMatrixData = null;
3669let basePolicySortMode = 'pinyin';
3670let basePolicyQuery = '';
3671let basePolicyPollTimer = null;
3672
3673function initBasePolicyScanPanel() {
3674  const panel = $('#basePolicyScanPanel');
3675  if (!panel || panel.dataset.ready === 'true') return;
3676  panel.dataset.ready = 'true';
3677  $('#basePolicyScanButton').addEventListener('click', startBasePolicyScan);
3678  $('#basePolicySearch').addEventListener('input', event => { basePolicyQuery = event.target.value.trim().toLowerCase(); renderBasePolicyMatrix(); });
3679  $('#basePolicySort').addEventListener('change', event => { basePolicySortMode = event.target.value; renderBasePolicyMatrix(); });
3680  $('#basePolicyIndex').addEventListener('click', event => {
3681    const btn = event.target.closest('[data-initial]');
3682    if (btn) $(`[data-initial-group="${btn.dataset.initial}"]`)?.scrollIntoView({behavior:'smooth', block:'start'});
3683  });
3684  loadBasePolicyMatrix();
3685  resumeBasePolicyPolling();
3686}
3687
3688async function loadBasePolicyMatrix() {
3689  const container = $('#basePolicyMatrix');
3690  try {
3691    basePolicyMatrixData = await investmentApiRequest('/api/base-enterprises/policy-opportunities');
3692    renderBasePolicyMatrix();
3693  } catch (error) {
3694    if (container) container.innerHTML = `<p>政策机会矩阵读取失败:${escapeHtml(error.message)}</p>`;
3695  }
3696}
3697
3698function renderBasePolicyMatrix() {
3699  const container = $('#basePolicyMatrix');
3700  const meta = $('#basePolicyScanMeta');
3701  const indexBar = $('#basePolicyIndex');
3702  if (!container || !basePolicyMatrixData) return;
3703  const data = basePolicyMatrixData;
3704  meta.textContent = data.scanned_count ? `已扫描 ${data.scanned_count}/${data.total} 家` : `共 ${data.total} 家 · 尚未批量扫描`;
3705  let companies = data.companies || [];
3706  if (basePolicyQuery) {
3707    companies = companies.filter(c =>
3708      c.name.toLowerCase().includes(basePolicyQuery) ||
3709      c.short_name.toLowerCase().includes(basePolicyQuery) ||
3710      (c.top_opportunities || []).some(o => (o.title || '').toLowerCase().includes(basePolicyQuery))
3711    );
3712  }
3713  if (!companies.length) {
3714    container.innerHTML = '<p>没有符合搜索条件的企业。</p>';
3715    indexBar.innerHTML = '';
3716    return;
3717  }
3718  const companyCard = c => {
3719    const summary = c.summary || {};
3720    const recommend = summary['建议申报'];
3721    const verify = summary['待核验后定'];
3722    const stats = c.scanned
3723      ? `<b class="recommend-count">建议 ${recommend || 0}</b><b class="verify-count">待核验 ${verify || 0}</b>`
3724      : '<b class="unscanned">未扫描</b>';
3725    const tops = (c.top_opportunities || []).map(o => `<li><i class="urgency-${escapeHtml(o.urgency)}">${o.urgency === '高' ? '急' : ''}</i><span><strong>${escapeHtml(o.title)}</strong><small>${escapeHtml(o.fit_reason || '')}${o.deadline ? ` · 截止 ${escapeHtml(o.deadline)}` : ''}</small></span></li>`).join('');
3726    return `<details class="base-policy-company${c.scanned ? '' : ' dimmed'}"><summary><span class="company-name"><strong>${escapeHtml(c.short_name)}</strong><small>${escapeHtml(c.name)}</small></span><em>${escapeHtml(c.primary_category || '分类待核验')}</em>${stats}</summary>${tops ? `<ul class="base-policy-tops">${tops}</ul>` : `<p class="base-policy-none">${c.scanned ? '没有"建议申报"级别的政策;待核验项请扫描详情查看。' : '等待批量扫描覆盖该企业。'}</p>`}</details>`;
3727  };
3728  if (basePolicySortMode === 'recommend') {
3729    const sorted = [...companies].sort((a, b) => ((b.summary || {})['建议申报'] || 0) - ((a.summary || {})['建议申报'] || 0) || a.short_name.localeCompare(b.short_name, 'zh'));
3730    container.innerHTML = sorted.map(companyCard).join('');
3731    indexBar.innerHTML = '';
3732    return;
3733  }
3734  const groups = {};
3735  companies.forEach(c => { (groups[c.initial] = groups[c.initial] || []).push(c); });
3736  const letters = Object.keys(groups).sort();
3737  indexBar.innerHTML = letters.map(l => `<button type="button" data-initial="${l}">${l}</button>`).join('');
3738  container.innerHTML = letters.map(l => `<div class="base-policy-group" data-initial-group="${l}"><h4>${l}</h4>${groups[l].map(companyCard).join('')}</div>`).join('');
3739}
3740
3741async function startBasePolicyScan() {
3742  const button = $('#basePolicyScanButton');
3743  button.disabled = true;
3744  try {
3745    const result = await investmentApiRequest('/api/base-enterprises/policy-scan-async', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({})});
3746    if (result.started === false) {
3747      showToast(result.message || '批量扫描正在进行中');
3748    } else {
3749      showToast(`批量扫描已启动,共 ${result.total} 家企业;已扫描且政策库未更新的企业会自动跳过`);
3750    }
3751    resumeBasePolicyPolling(true);
3752  } catch (error) {
3753    showToast(error.message);
3754    button.disabled = false;
3755  }
3756}
3757
3758async function resumeBasePolicyPolling(forceStart = false) {
3759  if (basePolicyPollTimer) { clearInterval(basePolicyPollTimer); basePolicyPollTimer = null; }
3760  const tick = async () => {
3761    let progress;
3762    try {
3763      progress = await investmentApiRequest('/api/base-enterprises/policy-scan-progress');
3764    } catch { return; }
3765    const bar = $('#basePolicyProgress');
3766    if (!bar) return;
3767    if (progress.status === 'running') {
3768      bar.classList.remove('hidden');
3769      const pct = progress.total ? Math.round(progress.done / progress.total * 100) : 0;
3770      $('#basePolicyProgressBar').style.width = `${pct}%`;
3771      $('#basePolicyProgressText').textContent = `正在扫描 ${progress.done}/${progress.total}:${progress.current || ''}(已跳过 ${progress.skipped || 0} 家)`;
3772      $('#basePolicyScanButton').disabled = true;
3773      if (progress.done > 0 && progress.done % 3 === 0) loadBasePolicyMatrix();
3774    } else {
3775      bar.classList.add('hidden');
3776      $('#basePolicyScanButton').disabled = false;
3777      $('#basePolicyScanButton').textContent = '重新批量扫描(增量)';
3778      if (progress.status === 'done' && progress.finished_at) {
3779        loadBasePolicyMatrix();
3780        clearInterval(basePolicyPollTimer);
3781        basePolicyPollTimer = null;
3782        const errors = (progress.errors || []).length;
3783        showToast(`批量扫描完成:${progress.done} 家(跳过 ${progress.skipped || 0} 家已是最新)${errors ? `,${errors} 家失败下轮自动重试` : ''}`);
3784      } else if (!forceStart) {
3785        clearInterval(basePolicyPollTimer);
3786        basePolicyPollTimer = null;
3787      }
3788    }
3789  };
3790  await tick();
3791  if (!basePolicyPollTimer) basePolicyPollTimer = setInterval(tick, 8000);
3792}
3793
3794
3795async function importBaseEnterpriseToPolicy(companyId) {
3796  const button = $('#importBaseEnterprisePolicy');
3797  if (button) { button.disabled = true; button.textContent = '正在建立企业档案…'; }
3798  try {
3799    const result = await investmentApiRequest(`/api/base-enterprises/${encodeURIComponent(companyId)}/import-policy`, {method:'POST'});
3800    policyEnterprisesLoaded = false;
3801    currentPolicyEnterprise = result.enterprise;
3802    await loadPolicyEnterprises(true);
3803    showToast(result.created ? '已从基地企业库创建政策规划档案' : '政策规划中的同名企业档案已更新');
3804    switchView('policyhub');
3805  } catch (error) {
3806    showToast(error.message);
3807    if (button) { button.disabled = false; button.textContent = '导入政策规划企业档案 →'; }
3808  }
3809}
3810
3811async function runBaseEnterpriseProjectMatch(projectId, target = '#baseEnterpriseMatchResults') {
3812  if (!projectId) { showToast('请先选择一个已完成分析的BP项目'); return; }
3813  const node = typeof target === 'string' ? $(target) : target;
3814  if (node) node.innerHTML = '<p class="base-enterprise-loading">正在比对产品、技术路线与企业资源…</p>';
3815  try {
3816    const result = await investmentApiRequest('/api/base-enterprises/match', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({project_id:projectId,limit:8})});
3817    renderEnterpriseMatches(node, result.matches);
3818    if ($('#baseEnterpriseMatchBoundary') && target === '#baseEnterpriseMatchResults') $('#baseEnterpriseMatchBoundary').textContent = `${result.method}。${result.boundary}`;
3819    return result;
3820  } catch (error) {
3821    if (node) node.innerHTML = `<div class="base-enterprise-no-match"><strong>关联分析未完成</strong><p>${escapeHtml(error.message)}</p></div>`;
3822    return null;
3823  }
3824}
3825
3826async function loadInvestmentEnterpriseMatches() {
3827  const panel = $('#investmentEnterpriseMatch');
3828  if (!panel || !currentInvestmentProject?.id || !currentInvestmentAnalysis) return;
3829  panel.classList.remove('hidden');
3830  const result = await runBaseEnterpriseProjectMatch(currentInvestmentProject.id, '#investmentEnterpriseMatchResults');
3831  if (result) $('#investmentEnterpriseMatchAudit').textContent = `${result.match_count}家达到研究线索阈值 · ${result.boundary}`;
3832}
3833
3834function initializeInvestmentWorkflowLayout() {
3835  const view = $('#investmentView');
3836  if (!view || view.dataset.workflowReady === 'true') return;
3837  view.dataset.workflowReady = 'true';
3838  const title = $('.section-title', view);
3839  const workflow = document.createElement('div');
3840  workflow.className = 'policy-workflow-shell investment-workflow-shell';
3841  workflow.innerHTML = `
3842    <nav class="policy-workflow-steps investment-workflow-steps" id="investmentWorkflowSteps" aria-label="BP分析工作步骤">
3843      <button class="active" data-investment-step="1"><i>01</i><span><strong>项目建档</strong><small>来源、赛道与负责人</small></span></button>
3844      <button data-investment-step="2"><i>02</i><span><strong>上传材料</strong><small>BP全文与核心附件</small></span></button>
3845      <button data-investment-step="3"><i>03</i><span><strong>AI价值研判</strong><small>项目潜力与证据置信度</small></span></button>
3846      <button data-investment-step="4"><i>04</i><span><strong>补证与初审</strong><small>缺口、专家与初审意见</small></span></button>
3847      <button data-investment-step="5"><i>05</i><span><strong>投委会决策</strong><small>结论、责任人与里程碑</small></span></button>
3848    </nav>
3849    <div class="policy-workflow-context investment-workflow-context">
3850      <span>当前项目<strong id="investmentContextProject">尚未选择</strong></span>
3851      <b>·</b>
3852      <span>项目状态<strong id="investmentContextStatus">等待建档</strong></span>
3853      <div><button class="outline-button" id="investmentWorkflowBack">← 上一步</button><button class="analyze-button" id="investmentWorkflowNext">项目已建档,上传BP →</button></div>
3854    </div>
3855    <div class="investment-workflow-stage active" data-investment-stage="1"></div>
3856    <div class="investment-workflow-stage" data-investment-stage="2"></div>
3857    <div class="investment-workflow-stage" data-investment-stage="3"></div>
3858    <div class="investment-workflow-stage" data-investment-stage="4"></div>
3859    <div class="investment-workflow-stage" data-investment-stage="5"></div>`;
3860  title.after(workflow);
3861
3862  const stage1 = $('[data-investment-stage="1"]', workflow);
3863  const stage2 = $('[data-investment-stage="2"]', workflow);
3864  const stage3 = $('[data-investment-stage="3"]', workflow);
3865  const stage4 = $('[data-investment-stage="4"]', workflow);
3866  const stage5 = $('[data-investment-stage="5"]', workflow);
3867
3868  stage1.innerHTML = `<div class="investment-archive-layout"><aside class="panel investment-project-sidebar"><div class="development-panel-head"><div><strong>投资项目库</strong><p>医院、高校院所和企业项目分别建档,分析与决策不会混用</p></div><span id="investmentProjectCount">0项</span></div><button class="outline-button investment-new-project" id="newInvestmentProjectButton">+ 新建项目档案</button><div id="investmentProjectList" class="investment-project-list"><p>正在读取项目档案…</p></div></aside><section class="panel investment-project-editor"><div class="development-panel-head"><div><strong id="investmentProjectFormTitle">建立投资项目档案</strong><p>先记录项目从哪里来、处于什么阶段、由谁跟进,再上传BP</p></div><span id="investmentArchiveStatus">待建档</span></div><div class="investment-project-form"><label class="wide">项目名称<input id="investmentProjectName" type="text" placeholder="ä¾
3868‹å¦‚:骨科智能穿刺导航项目"></label><label>项目来源<select id="investmentProjectSource"><option>医院临床项目</option><option>高校院所成果</option><option>医药企业项目</option><option>创业团队</option><option>园区主动发现</option><option>其他渠道</option></select></label><label>项目赛道<select id="investmentProjectTrack"><option>医疗器械</option><option>体外诊断 IVD</option><option>创新药 / 生物药</option><option>AI医疗软件</option><option>细胞与基因治疗</option><option>CRO / 医药服务</option><option>其他生物医药</option></select></label><label>当前阶段<select id="investmentProjectStage"><option>临床需求 / 概念</option><option>实验室验证</option><option>工程样机 / 小试</option><option>注册准备 / 临床</option><option>已注册 / 商业化</option></select></label><label>项目方联系人<input id="investmentProjectContact" type="text" placeholder="姓名、单位、联系方式"></label><label>华卫跟进负责人<input id="investmentProjectOwner" type="text" placeholder="投资经理 / 项目经理"></label><label class="wide">建档备注<textarea id="investmentProjectNotes" placeholder="合作背景、成果权属、此次接触目的、需要特别关注的问题……"></textarea></label></div><div class="investment-project-actions"><button class="outline-button" id="saveInvestmentProjectButton">保存档案修改</button><button class="analyze-button" id="createInvestmentProjectButton">创建并进入材料上传 →</button></div><div class="investment-current-archive hidden" id="investmentCurrentArchive"></div></section></div>`;
3869
3870  const engineConnection = $('.engine-connection', view);
3871  const workspace = $('.intelligence-workspace', view);
3872  if (engineConnection) stage2.append(engineConnection);
3873  if (workspace) stage2.append(workspace);
3874  const uploadGuide = document.createElement('section');
3875  uploadGuide.className = 'panel investment-material-guide';
3876  uploadGuide.innerHTML = '<div><strong>本轮以主BP为事实底稿</strong><p>系统读取全文、表格和PPT文字并记录实际读取字数。专利、检测、财务和注册附件先在初审阶段列为补证任务;没有原始证据的内容不会自动判定为事实。</p></div><span>上传后自动进入第3步</span>';
3877  stage2.prepend(uploadGuide);
3878
3879  ['investmentResult','investmentBiomedicalRoute','investmentAssistant','investmentDeepening','investmentEvidence','investmentKnowledgeReferences','investmentEnterpriseMatch'].forEach(id => {
3880    const node = $(`#${id}`);
3881    if (node) stage3.append(node);
3882  });
3883  const analysisEmpty = document.createElement('section');
3884  analysisEmpty.className = 'panel investment-stage-empty';
3885  analysisEmpty.id = 'investmentAnalysisEmpty';
3886  analysisEmpty.innerHTML = '<strong>尚未形成项目价值研判报告</strong><p>请先在第二步上传该项目的BP。完成后会看到实际AI调用记录、九维项目潜力、证据覆盖度与判断区间、市场和注册成本测算及同类产品核验任务。</p>';
3887  stage3.prepend(analysisEmpty);
3888
3889  stage4.innerHTML = `<section class="panel investment-review-gate"><div class="development-panel-head"><div><strong>材料补证与专家初审</strong><p>逐项处理AI识别的证据缺口;未补齐可以继续记录,但必须明确暴露给投委会</p></div><span id="investmentReviewStatus">等待分析</span></div><div class="investment-review-layout"><article><strong>本项目待核验与补证事项</strong><div id="investmentReviewGaps" class="investment-review-gaps"><p>完成AI分析后自动生成。</p></div></article><article class="investment-review-form"><label>初审负责人<input id="investmentReviewer" type="text" placeholder="投资经理 / 临床专家 / 工程专家"></label><label>初审意见<select id="investmentReviewOpinion"><option>建议进入投委会讨论</option><option>补充关键材料后再上会</option><option>建议进入储备观察</option><option>建议终止推进</option></select></label><label>初审记录<textarea id="investmentReviewNotes" placeholder="记录专家判断、关键争议、待补材料责任人和截止时间……"></textarea></label><div class="investment-review-confirmations"><label><input type="checkbox" data-investment-review-confirm> 已阅读九维评分及逐项原文依据</label><label><input type="checkbox" data-investment-review-confirm> 已区分BP自述、AI判断和外部真实数据</label><label><input type="checkbox" data-investment-review-confirm> 已记录未解决缺口,不把待核验项当作事实</label></div><button class="analyze-button" id="saveInvestmentReviewButton">保存初审,提交投委会 →</button></article></div></section>`;
3890
3891  stage5.innerHTML = `<section class="panel investment-committee-gate"><div class="development-panel-head"><div><strong>投委会决策与下一里程碑</strong><p>评分只提供决策证据,不自动代替投资判断;最终结论、责任人和停止条件必须人工填写</p></div><span id="investmentCommitteeScore">等待分析</span></div><div class="investment-committee-summary" id="investmentCommitteeSummary"></div><div class="investment-decision-form"><label>投委会结论<select id="investmentCommitteeDecision"><option>进入正式尽调</option><option>补充材料后复审</option><option>暂缓观察</option><option>终止推进</option></select></label><label>决策日期<input id="investmentCommitteeDate" type="date"></label><label>下一阶段负责人<input id="investmentCommitteeOwner" type="text" placeholder="投资负责人 / 项目负责人"></label><label>拟投资或预算额度(万元)<input id="investmentCommitteeAmount" type="number" min="0" placeholder="可暂不填写"></label><label class="wide">决策理由<textarea id="investmentCommitteeReason" placeholder="为什么继续、暂缓或终止;引用最关键的事实、风险和缺口……"></textarea></label><label class="wide">下一里程碑与停止条件<textarea id="investmentCommitteeMilestone" placeholder="ä¾
3891‹å¦‚:完成专利权属确认、第三方性能复测和注册分类预沟通;任一核心指标不达标则停止推进"></textarea></label><label>完成期限<input id="investmentCommitteeDeadline" type="date"></label></div><div class="investment-committee-actions"><button class="outline-button" id="downloadInvestmentMemoButton">导出投决纪要</button><button class="analyze-button" id="saveInvestmentDecisionButton">确认并归档投委会结论 →</button></div><div class="investment-decision-record hidden" id="investmentDecisionRecord"></div></section>`;
3892
3893  $$('#investmentWorkflowSteps button', workflow).forEach(button => button.addEventListener('click', () => requestInvestmentWorkflowStep(Number(button.dataset.investmentStep))));
3894  $('#investmentWorkflowBack').addEventListener('click', () => renderInvestmentWorkflowStep(Math.max(1, investmentWorkflowStep - 1)));
3895  $('#investmentWorkflowNext').addEventListener('click', advanceInvestmentWorkflow);
3896  $('#newInvestmentProjectButton').addEventListener('click', resetInvestmentProjectForm);
3897  $('#createInvestmentProjectButton').addEventListener('click', createInvestmentProject);
3898  $('#saveInvestmentProjectButton').addEventListener('click', saveInvestmentProject);
3899  $('#investmentProjectList').addEventListener('click', event => { const reportLink = event.target.closest('[data-open-report]'); const button = event.target.closest('[data-investment-project-id]'); if (!button) return; if (reportLink) { openInvestmentProjectReport(button.dataset.investmentProjectId); return; } selectInvestmentProject(button.dataset.investmentProjectId); });
3900  $('#saveInvestmentReviewButton').addEventListener('click', saveInvestmentInitialReview);
3901  $('#saveInvestmentDecisionButton').addEventListener('click', saveInvestmentCommitteeDecision);
3902  $('#downloadInvestmentMemoButton').addEventListener('click', downloadInvestmentMemo);
3903  $('#investmentAssistantSend').addEventListener('click', () => sendInvestmentAssistantQuestion());
3904  $('#investmentAssistantInput').addEventListener('keydown', event => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendInvestmentAssistantQuestion(); } });
3905  $('#investmentAssistantPrompts').addEventListener('click', event => { const button=event.target.closest('[data-bp-assistant-question]'); if (button) sendInvestmentAssistantQuestion(button.dataset.bpAssistantQuestion); });
3906  $('#investmentAssistantMessages').addEventListener('click', event => { const button=event.target.closest('[data-bp-assistant-question]'); if (button) sendInvestmentAssistantQuestion(button.dataset.bpAssistantQuestion); });
3907  $('#clearInvestmentAssistantButton').addEventListener('click', clearInvestmentAssistantConversation);
3908  $('#investmentCommitteeDate').value = new Date().toISOString().slice(0,10);
3909  renderInvestmentWorkflowStep(1, true);
3910  loadInvestmentProjects();
3911}
3912
3913function renderInvestmentWorkflowStep(step, bypassGate = false) {
3914  investmentWorkflowStep = Math.max(1, Math.min(5, Number(step) || 1));
3915  $$('#investmentWorkflowSteps button').forEach(button => {
3916    const value = Number(button.dataset.investmentStep);
3917    button.classList.toggle('active', value === investmentWorkflowStep);
3918    button.classList.toggle('complete', value < investmentWorkflowStep);
3919  });
3920  $$('.investment-workflow-stage').forEach(stage => stage.classList.toggle('active', Number(stage.dataset.investmentStage) === investmentWorkflowStep));
3921  $('#investmentWorkflowBack').classList.toggle('hidden', investmentWorkflowStep === 1);
3922  $('#investmentWorkflowNext').classList.toggle('hidden', investmentWorkflowStep >= 4);
3923  const labels = {1:'项目已建档,上传BP →',2:'查看AI分析结果 →',3:'进入补证与专家初审 →'};
3924  $('#investmentWorkflowNext').textContent = labels[investmentWorkflowStep] || '继续 →';
3925  $('#investmentContextProject').textContent = currentInvestmentProject?.name || '尚未选择';
3926  $('#investmentContextStatus').textContent = currentInvestmentProject?.status || '等待建档';
3927  refreshInvestmentAssistant();
3928  if (investmentWorkflowStep === 3) $('#investmentAnalysisEmpty').classList.toggle('hidden', Boolean(currentInvestmentAnalysis));
3929  if (!bypassGate) window.scrollTo({top:0,behavior:'smooth'});
3930}
3931
3932function requestInvestmentWorkflowStep(step) {
3933  if (step > 1 && !currentInvestmentProject) { showToast('请先创建或选择投资项目档案'); renderInvestmentWorkflowStep(1); return; }
3934  if (step > 2 && !currentInvestmentAnalysis) { showToast('请先上传BP并完成AI全文分析'); renderInvestmentWorkflowStep(2); return; }
3935  if (step > 3 && !currentInvestmentReportId) { showToast('摘要初筛不能提交专家初审,请上传完整BP生成正式报告'); renderInvestmentWorkflowStep(2); return; }
3936  if (step > 4 && !Object.keys(currentInvestmentProject?.initial_review || {}).length) { showToast('请先保存专家初审记录'); renderInvestmentWorkflowStep(4); return; }
3937  renderInvestmentWorkflowStep(step);
3938}
3939
3940function advanceInvestmentWorkflow() {
3941  if (investmentWorkflowStep === 1) {
3942    if (!currentInvestmentProject) { showToast('请先创建或选择投资项目档案'); return; }
3943    renderInvestmentWorkflowStep(2); return;
3944  }
3945  if (investmentWorkflowStep === 2) {
3946    if (!currentInvestmentAnalysis) { showToast('请先上传BP并完成分析'); return; }
3947    renderInvestmentWorkflowStep(3); return;
3948  }
3949  if (investmentWorkflowStep === 3) {
3950    if (!currentInvestmentReportId) { showToast('摘要初筛不能进入正式初审,请上传完整BP'); renderInvestmentWorkflowStep(2); return; }
3951    renderInvestmentWorkflowStep(4);
3952  }
3953}
3954
3955function investmentProjectPayload() {
3956  return {
3957    name:$('#investmentProjectName').value.trim(),source_type:$('#investmentProjectSource').value,
3958    track:$('#investmentProjectTrack').value,stage:$('#investmentProjectStage').value,
3959    team_contact:$('#investmentProjectContact').value.trim(),owner:$('#investmentProjectOwner').value.trim(),
3960    notes:$('#investmentProjectNotes').value.trim()
3961  };
3962}
3963
3964function resetInvestmentProjectForm() {
3965  currentInvestmentProject = null; currentInvestmentAnalysis = null; currentInvestmentReportId = null;
3966  investmentAssistantMessages = [];
3967  $('#investmentProjectFormTitle').textContent = '建立投资项目档案';
3968  ['investmentProjectName','investmentProjectContact','investmentProjectOwner','investmentProjectNotes'].forEach(id => { $(`#${id}`).value = ''; });
3969  $('#investmentArchiveStatus').textContent = '待建档';
3970  $('#investmentCurrentArchive').classList.add('hidden');
3971  $('#createInvestmentProjectButton').classList.remove('hidden');
3972  $('#saveInvestmentProjectButton').classList.add('hidden');
3973  renderInvestmentProjectList(); renderInvestmentWorkflowStep(1, true); $('#investmentProjectName').focus();
3974}
3975
3976function fillInvestmentProjectForm(project) {
3977  $('#investmentProjectName').value = project.name || '';
3978  $('#investmentProjectSource').value = project.source_type || '医院临床项目';
3979  $('#investmentProjectTrack').value = project.track || '医疗器械';
3980  $('#investmentProjectStage').value = project.stage || '临床需求 / 概念';
3981  $('#investmentProjectContact').value = project.team_contact || '';
3982  $('#investmentProjectOwner').value = project.owner || '';
3983  $('#investmentProjectNotes').value = project.notes || '';
3984  $('#investmentProjectFormTitle').textContent = '当前投资项目档案';
3985  $('#investmentArchiveStatus').textContent = project.status || '已建档';
3986  $('#createInvestmentProjectButton').classList.add('hidden');
3987  $('#saveInvestmentProjectButton').classList.remove('hidden');
3988  const archive = $('#investmentCurrentArchive');
3989  archive.innerHTML = `<span><small>项目编号</small><strong>${escapeHtml(project.id.slice(0,8).toUpperCase())}</strong></span><span><small>最近BP</small><strong>${escapeHtml(project.latest_file_name || '尚未上传')}</strong></span><span><small>最近评分</small><strong>${project.latest_score == null ? '待分析' : `${escapeHtml(project.latest_score)} / 10`}</strong></span><span><small>流程状态</small><strong>${escapeHtml(project.status || '已建档')}</strong></span>`;
3990  archive.classList.remove('hidden');
3991}
3992
3993function renderInvestmentProjectList() {
3994  $('#investmentProjectCount').textContent = `${investmentProjects.length}项`;
3995  $('#investmentProjectList').innerHTML = investmentProjects.length ? investmentProjects.map(project => `<button class="investment-project-card ${currentInvestmentProject?.id===project.id?'active':''}" data-investment-project-id="${escapeHtml(project.id)}"><span><b>${escapeHtml(project.status || '已建档')}</b><small>${project.latest_score == null ? '待分析' : `${escapeHtml(project.latest_score)}分`}</small></span><strong>${escapeHtml(project.name)}</strong><p>${escapeHtml(project.source_type || '来源待补充')} · ${escapeHtml(project.track || '赛道待确认')}</p>${project.latest_score != null ? '<em class="investment-open-report" data-open-report="1">查看报告 →</em>' : ''}</button>`).join('') : '<div class="investment-project-empty"><strong>还没有投资项目</strong><p>点击上方“新建项目档案”,先记录项目来源和负责人。</p></div>';
3996}
3997
3998async function loadInvestmentProjects(force = false, preferredId = '') {
3999  if (investmentProjectsLoaded && !force) return;
4000  try {
4001    investmentProjects = await investmentApiRequest('/api/investment/projects');
4002    investmentProjectsLoaded = true;
4003    if (new URLSearchParams(window.location.search).get('demo') === 'investment') { renderInvestmentProjectList(); return; }
4004    const selectId = preferredId || currentInvestmentProject?.id || investmentProjects[0]?.id;
4005    if (selectId) await selectInvestmentProject(selectId, true); else resetInvestmentProjectForm();
4006    renderInvestmentProjectList();
4007  } catch (error) {
4008    $('#investmentProjectList').innerHTML = `<div class="investment-project-empty"><strong>项目档案服务暂未连接</strong><p>${escapeHtml(error.message)}</p></div>`;
4009  }
4010}
4011
4012async function selectInvestmentProject(projectId, silent = false) {
4013  try {
4014    currentInvestmentProject = await investmentApiRequest(`/api/investment/projects/${encodeURIComponent(projectId)}`);
4015    currentInvestmentAnalysis = Object.keys(currentInvestmentProject.analysis || {}).length ? currentInvestmentProject.analysis : null;
4016    currentInvestmentReportId = currentInvestmentProject.latest_report_id || null;
4017    loadInvestmentAssistantConversation();
4018    fillInvestmentProjectForm(currentInvestmentProject); renderInvestmentProjectList(); renderInvestmentSavedReview(); renderInvestmentSavedDecision();
4019    if (currentInvestmentAnalysis) renderProfessionalBpReport(currentInvestmentAnalysis, {silent:true});
4020    else ['investmentResult','investmentBiomedicalRoute','investmentAssistant','investmentDeepening','investmentEvidence','investmentKnowledgeReferences'].forEach(id => $(`#${id}`)?.classList.add('hidden'));
4021    renderInvestmentReviewChecklist(); renderInvestmentWorkflowStep(investmentWorkflowStep, true);
4022    if (!silent) showToast(`已切换到“${currentInvestmentProject.name}”`);
4023  } catch (error) { showToast(error.message); }
4024}
4025
4026async function openInvestmentProjectReport(projectId) {
4027  try {
4028    await selectInvestmentProject(projectId, true);
4029    renderInvestmentWorkflowStep(3, true);
4030    setTimeout(() => $('#investmentResult')?.scrollIntoView({behavior:'smooth', block:'start'}), 80);
4031    showToast(`已打开“${currentInvestmentProject?.name || '该项目'}”的历史分析报告`);
4032  } catch (error) { showToast(error.message); }
4033}
4034
4035async function createInvestmentProject() {
4036  const payload = investmentProjectPayload();
4037  if (!payload.name) { $('#investmentProjectName').focus(); showToast('请先填写项目名称'); return; }
4038  try {
4039    currentInvestmentProject = await investmentApiRequest('/api/investment/projects',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
4040    investmentProjectsLoaded = false; await loadInvestmentProjects(true,currentInvestmentProject.id); renderInvestmentWorkflowStep(2); showToast('项目档案已建立,现在可以上传BP');
4041  } catch (error) { showToast(error.message); }
4042}
4043
4044async function saveInvestmentProject() {
4045  if (!currentInvestmentProject) return;
4046  const payload = investmentProjectPayload();
4047  if (!payload.name) { $('#investmentProjectName').focus(); showToast('项目名称不能为空'); return; }
4048  try {
4049    currentInvestmentProject = await investmentApiRequest(`/api/investment/projects/${encodeURIComponent(currentInvestmentProject.id)}`,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
4050    await loadInvestmentProjects(true,currentInvestmentProject.id); showToast('项目档案已保存');
4051  } catch (error) { showToast(error.message); }
4052}
4053
4054function investmentGapItems() {
4055  if (!currentInvestmentAnalysis) return [];
4056  const dimensions = Array.isArray(currentInvestmentAnalysis.dimensions) ? currentInvestmentAnalysis.dimensions : [];
4057  const risks = Array.isArray(currentInvestmentAnalysis.risks) ? currentInvestmentAnalysis.risks : [];
4058  const gaps = [];
4059  dimensions.forEach(item => {
4060    const pending = (item.score_audit?.criteria || []).filter(criterion => criterion.status === '待验证');
4061    pending.slice(0,2).forEach(criterion => gaps.push({type:`待验证 · ${item.name}`,text:`${criterion.criterion}:${criterion.reason || '需要补充材料或专家核验'}`}));
4062    if (Number(item.score) < 6 && item.weaknesses?.[0]) gaps.push({type:`价值风险 · ${item.name} ${item.score}分`,text:item.weaknesses[0]});
4063  });
4064  risks.filter(item => item.level === '高').forEach(item => gaps.unshift({type:`高风险 · ${item.category || '尽调风险'}`,text:item.description || '需要专家复核'}));
4065  return gaps.slice(0,10);
4066}
4067
4068function renderInvestmentReviewChecklist() {
4069  if (!$('#investmentReviewGaps')) return;
4070  const gaps = investmentGapItems();
4071  $('#investmentReviewGaps').innerHTML = gaps.length ? gaps.map((item,index)=>`<label><input type="checkbox" data-investment-gap="${index}"><span><strong>${escapeHtml(item.type)}</strong><small>${escapeHtml(item.text)}</small></span><b>待处理</b></label>`).join('') : '<div class="investment-no-gap"><strong>暂无模型生成的明确缺口</strong><p>仍需核对数据来源、成果权属、注册分类和财务预测。</p></div>';
4072  $('#investmentReviewStatus').textContent = currentInvestmentAnalysis ? `${gaps.length}项待核验` : '等待分析';
4073  renderInvestmentSavedReview();
4074}
4075
4076function renderInvestmentSavedReview() {
4077  const review = currentInvestmentProject?.initial_review || {};
4078  if (!Object.keys(review).length || !$('#investmentReviewer')) return;
4079  $('#investmentReviewer').value = review.reviewer || '';
4080  $('#investmentReviewOpinion').value = review.opinion || '建议进入投委会讨论';
4081  $('#investmentReviewNotes').value = review.notes || '';
4082  $$('[data-investment-review-confirm]').forEach(input => { input.checked = true; });
4083  $('#investmentReviewStatus').textContent = `已由 ${review.reviewer || '负责人'} 初审`;
4084}
4085
4086async function saveInvestmentInitialReview() {
4087  if (!currentInvestmentProject || !currentInvestmentAnalysis) { showToast('请先完成项目BP分析'); return; }
4088  if (!currentInvestmentReportId) { showToast('摘要初筛不能提交投委会,请先上传完整BP生成正式报告'); renderInvestmentWorkflowStep(2); return; }
4089  const reviewer = $('#investmentReviewer').value.trim();
4090  if (!reviewer) { $('#investmentReviewer').focus(); showToast('请填写初审负责人'); return; }
4091  if ($$('[data-investment-review-confirm]:checked').length !== $$('[data-investment-review-confirm]').length) { showToast('请完成三项初审确认'); return; }
4092  const gaps = investmentGapItems();
4093  const resolved = $$('[data-investment-gap]:checked').map(input => Number(input.dataset.investmentGap));
4094  const payload = {reviewer,opinion:$('#investmentReviewOpinion').value,
4094notes:$('#investmentReviewNotes').value.trim(),resolved_gaps:resolved.map(index=>gaps[index]).filter(Boolean),open_gaps:gaps.filter((_,index)=>!resolved.includes(index)),report_id:currentInvestmentReportId || ''};
4095  try {
4096    currentInvestmentProject = await investmentApiRequest(`/api/investment/projects/${encodeURIComponent(currentInvestmentProject.id)}/initial-review`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
4097    await loadInvestmentProjects(true,currentInvestmentProject.id); renderInvestmentWorkflowStep(5); showToast('专家初审已归档,进入投委会决策');
4098  } catch (error) { showToast(error.message); }
4099}
4100
4101function renderInvestmentSavedDecision() {
4102  if (!$('#investmentCommitteeSummary')) return;
4103  const analysis = currentInvestmentProject?.analysis || currentInvestmentAnalysis || {};
4104  const review = currentInvestmentProject?.initial_review || {};
4105  $('#investmentCommitteeScore').textContent = analysis.overall_score == null ? '等待分析' : `九维 ${analysis.overall_score} / 10`;
4106  $('#investmentCommitteeSummary').innerHTML = `<span><small>AI建议</small><strong>${escapeHtml(analysis.recommendation || '待分析')}</strong></span><span><small>专家初审</small><strong>${escapeHtml(review.opinion || '待初审')}</strong></span><span><small>未解决缺口</small><strong>${Number(review.open_gaps?.length || 0)}项</strong></span>`;
4107  const decision = currentInvestmentProject?.decision || {};
4108  const record = $('#investmentDecisionRecord');
4109  if (!Object.keys(decision).length) { record.classList.add('hidden'); return; }
4110  $('#investmentCommitteeDecision').value = decision.decision || '进入正式尽调';
4111  $('#investmentCommitteeDate').value = decision.decision_date || new Date().toISOString().slice(0,10);
4112  $('#investmentCommitteeOwner').value = decision.owner || '';
4113  $('#investmentCommitteeAmount').value = decision.amount_wan || '';
4114  $('#investmentCommitteeReason').value = decision.reason || '';
4115  $('#investmentCommitteeMilestone').value = decision.milestone || '';
4116  $('#investmentCommitteeDeadline').value = decision.deadline || '';
4117  record.innerHTML = `<strong>已归档:${escapeHtml(decision.decision || '投委会已决策')}</strong><p>${escapeHtml(decision.reason || '未填写决策理由')}</p><span>负责人:${escapeHtml(decision.owner || '待明确')} · 下一期限:${escapeHtml(decision.deadline || '待明确')}</span>`;
4118  record.classList.remove('hidden');
4119}
4120
4121async function saveInvestmentCommitteeDecision() {
4122  if (!currentInvestmentProject) return;
4123  const owner = $('#investmentCommitteeOwner').value.trim();
4124  const reason = $('#investmentCommitteeReason').value.trim();
4125  const milestone = $('#investmentCommitteeMilestone').value.trim();
4126  if (!owner || !reason || !milestone) { showToast('请填写负责人、决策理由和下一里程碑'); return; }
4127  const payload = {decision:$('#investmentCommitteeDecision').value,decision_date:$('#investmentCommitteeDate').value,owner,amount_wan:Number($('#investmentCommitteeAmount').value || 0),reason,milestone,deadline:$('#investmentCommitteeDeadline').value,report_id:currentInvestmentReportId || ''};
4128  try {
4129    currentInvestmentProject = await investmentApiRequest(`/api/investment/projects/${encodeURIComponent(currentInvestmentProject.id)}/decision`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
4130    await loadInvestmentProjects(true,currentInvestmentProject.id); renderInvestmentSavedDecision(); showToast('投委会结论已归档到项目档案');
4131  } catch (error) { showToast(error.message); }
4132}
4133
4134function downloadInvestmentMemo() {
4135  if (!currentInvestmentProject) { showToast('请先选择项目'); return; }
4136  const analysis = currentInvestmentProject.analysis || currentInvestmentAnalysis || {};
4137  const review = currentInvestmentProject.initial_review || {};
4138  const saved = currentInvestmentProject.decision || {};
4139  const decision = Object.keys(saved).length ? saved : {decision:$('#investmentCommitteeDecision').value,decision_date:$('#investmentCommitteeDate').value,owner:$('#investmentCommitteeOwner').value,amount_wan:$('#investmentCommitteeAmount').value,reason:$('#investmentCommitteeReason').value,milestone:$('#investmentCommitteeMilestone').value,deadline:$('#investmentCommitteeDeadline').value};
4140  const content = `华卫天和投资项目投决纪要\n\n项目:${currentInvestmentProject.name}\n来源:${currentInvestmentProject.source_type}\n赛道:${currentInvestmentProject.track}\n阶段:${currentInvestmentProject.stage}\nBP文件:${currentInvestmentProject.latest_file_name || '未记录'}\n九维评分:${analysis.overall_score ?? '—'} / 10\nAI建议:${analysis.recommendation || '未形成'}\n\n专家初审:${review.opinion || '未形成'}\n初审负责人:${review.reviewer || '未填写'}\n未解决缺口:${Number(review.open_gaps?.length || 0)}项\n\n投委会结论:${decision.decision || '待确认'}\n决策日期:${decision.decision_date || '待确认'}\n负责人:${decision.owner || '待确认'}\n拟投资/预算:${decision.amount_wan || 0}万元\n决策理由:${decision.reason || '待填写'}\n下一里程碑与停止条件:${decision.milestone || '待填写'}\n完成期限:${decision.deadline || '待确认'}\n\n边界说明:本纪要中的BP事实来自上传材料;AI评分与建议属于研究草案,最终结论由人工投委会确认。`;
4141  const url = URL.createObjectURL(new Blob([content],{type:'text/plain;charset=utf-8'})); const link=document.createElement('a'); link.href=url; link.download=`${currentInvestmentProject.name}-投决纪要.txt`; link.click(); URL.revokeObjectURL(url);
4142}
4143
4144const HUAWAI_PROFILE_VERSIONS = {
4145  company_intro: {
4146    label:'历史口径 · 2026年4月资料',area:30000,incubated:50,served:100,patents:700,rd:800,
4147    source:'《北京华卫天和健康科技集团有限公司概况--2026.4.16》',
4148    note:'历史资料披露:3万㎡、在孵50家、累计孵化100家。仅供版本追溯,不作为当前默认口径。'
4149  },
4150  application_2025: {
4151    label:'当前正式口径 · 已确认',area:40000,incubated:61,served:126,patents:700,rd:800,
4152    source:'《赋能园区科协高质量发展项目申报书》',
4153    note:'当前正式数据:4万㎡、在孵61家、累计孵化126家;由华卫于2026-07-16确认采用大数口径。'
4154  }
4155};
4156
4157function applyPolicyProfileVersion(versionKey) {
4158  if (versionKey === 'enterprise_archive' && currentPolicyEnterprise) {
4159    applyEnterpriseProfileToForm(currentPolicyEnterprise);
4160    return;
4161  }
4162  const profile = HUAWAI_PROFILE_VERSIONS[versionKey] || HUAWAI_PROFILE_VERSIONS.application_2025;
4163  $('#policyArea').value = profile.area;
4164  $('#policyIncubatedCompanies').value = profile.incubated;
4165  $('#policyServedCompanies').value = profile.served;
4166  $('#policyPatents').value = profile.patents;
4167  $('#policyRdSpend').value = profile.rd;
4168  $('#companyApplicationEvidence').innerHTML = `<strong>当前口径:${escapeHtml(profile.label)}</strong><span>${escapeHtml(profile.note)}</span><small>来源:${escapeHtml(profile.source)}。生成内容仍需用营业执照、资质证书、统计台账、审计或财务附件逐项佐证。</small>`;
4169}
4170
4171async function policyApiRequest(path, options = {}) {
4172  const token = localStorage.getItem('token');
4173  const headers = {...(options.headers || {})};
4174  if (token) headers.Authorization = `Bearer ${token}`;
4175  const response = await fetch(`${BP_API_BASE}${path}`, {...options,headers});
4176  const raw = await response.text();
4177  let payload;
4178  try { payload = JSON.parse(raw); } catch { throw new Error(`政策申报服务返回异常:${raw.slice(0,100)}`); }
4179  if (!response.ok) throw new Error(payload.detail || payload.error || '政策申报操作失败');
4180  return payload.data;
4181}
4182
4183function initializePolicyWorkflowLayout() {
4184  const view = $('#policyhubView');
4185  if (!view || view.dataset.workflowReady === 'true') return;
4186  view.dataset.workflowReady = 'true';
4187  const title = $('.section-title', view);
4188  const workflow = document.createElement('div');
4189  workflow.className = 'policy-workflow-shell';
4190  workflow.innerHTML = `
4191    <nav class="policy-workflow-steps" id="policyWorkflowSteps" aria-label="政策规划工作步骤">
4192      <button class="active" data-policy-step="1"><i>01</i><span><strong>企业建档</strong><small>基本信息与长期资料</small></span></button>
4193      <button data-policy-step="2"><i>02</i><span><strong>政策库</strong><small>选择已有或新增政策</small></span></button>
4194      <button data-policy-step="3"><i>03</i><span><strong>选择与匹配</strong><small>选企业、选政策、评分</small></span></button>
4195      <button data-policy-step="4"><i>04</i><span><strong>人工确认</strong><small>判断是否正式申报</small></span></button>
4196      <button data-policy-step="5"><i>05</i><span><strong>项目书编写</strong><small>按模板形成可核验草案</small></span></button>
4197    </nav>
4198    <div class="policy-workflow-context">
4199      <span>当前企业<strong id="policyContextEnterprise">尚未选择</strong></span>
4200      <b>×</b>
4201      <span>当前政策<strong id="policyContextPolicy">尚未选择</strong></span>
4202      <div><button class="outline-button" id="policyWorkflowBack">← 上一步</button><button class="analyze-button" id="policyWorkflowNext">完成本步,继续 →</button></div>
4203    </div>
4204    <div class="policy-workflow-stage active" data-policy-stage="1"></div>
4205    <div class="policy-workflow-stage" data-policy-stage="2"></div>
4206    <div class="policy-workflow-stage" data-policy-stage="3"></div>
4207    <div class="policy-workflow-stage" data-policy-stage="4"></div>
4208    <div class="policy-workflow-stage" data-policy-stage="5"></div>`;
4209  title.after(workflow);
4210
4211  const stage1 = $('[data-policy-stage="1"]', workflow);
4212  const stage2 = $('[data-policy-stage="2"]', workflow);
4213  const stage3 = $('[data-policy-stage="3"]', workflow);
4214  const stage4 = $('[data-policy-stage="4"]', workflow);
4215  const stage5 = $('[data-policy-stage="5"]', workflow);
4216  const enterpriseArchive = $('.enterprise-archive', view);
4217  const profile = $('.policy-profile', view);
4218  const documentHub = $('.policy-document-hub', view);
4219  const sourceRule = $('.policy-source-rule', view);
4220  const result = $('#policyMatchResults');
4221  const proposal = $('#proposalWorkbench');
4222  const unifiedGrid = $('.policy-unified-grid', view);
4223
4224  stage1.append(enterpriseArchive);
4225  stage1.append(profile);
4226  const enterpriseUpload = document.createElement('section');
4227  enterpriseUpload.className = 'panel policy-stage-enterprise-upload';
4228  enterpriseUpload.innerHTML = '<div class="development-panel-head"><div><strong>补充企业长期资料</strong><p>资料只进入当前企业档案,后续所有政策匹配都可重复使用</p></div><span>营业执照、资质、财务、人员、知识产权、项目业绩</span></div><div class="enterprise-upload-slot"></div>';
4229  const enterpriseUploadLabel = $('label[for="policyMaterialsInput"]', documentHub);
4230  if (enterpriseUploadLabel) $('.enterprise-upload-slot', enterpriseUpload).append(enterpriseUploadLabel);
4231  const uploadQueue = document.createElement('div');
4232  uploadQueue.className = 'enterprise-upload-queue hidden';
4233  uploadQueue.id = 'enterpriseUploadQueue';
4234  uploadQueue.innerHTML = '<strong id="enterpriseUploadQueueTitle">已选择文件</strong><div id="enterpriseUploadQueueFiles"></div>';
4235  enterpriseUpload.append(uploadQueue);
4236  const enterpriseUploadStatus = $('#enterpriseUploadStatus');
4237  if (enterpriseUploadStatus) enterpriseUpload.append(enterpriseUploadStatus);
4238  const materialLedger = $('#policyMaterialLedger');
4239  if (materialLedger) enterpriseUpload.append(materialLedger);
4240  stage1.append(enterpriseUpload);
4241
4242  const catalogLayout = document.createElement('div');
4243  catalogLayout.className = 'policy-catalog-layout';
4244  catalogLayout.innerHTML = `<aside class="panel policy-catalog-sidebar"><div class="development-panel-head"><div><strong>政策库</strong><p>提前沉淀政策原文和AI解析结果,供不同企业反复匹配</p></div><span id="policyCatalogCount">0项</span></div><button class="analyze-button policy-catalog-new" id="newPolicyCatalogButton">+ 新增政策</button><div class="policy-catalog-tools"><input id="policyCatalogSearch" type="search" placeholder="搜索政策名称、发布单位、支持方向"><div><select id="policyCatalogLevelFilter"><option>全部级别</option><option>国家级</option><option>北京市</option><option>大兴区</option><option>经开区</option><option>其他地区</option></select><select id="policyCatalogValidityFilter"><option>全部状态</option><option>有效</option><option>即将截止</option><option>已截止</option><option>待核验</option></select></div><label class="policy-catalog-toggle"><input type="checkbox" id="policyCatalogShowMismatch"><span>显示已隐藏政策(区域不符/来源待核验) <b id="policyCatalogMismatchCount"></b></span></label></div><div id="policyCatalogList" class="policy-catalog-list"><p>正在读取政策库…</p></div></aside><div class="policy-catalog-editor"><section class="panel policy-catalog-selected hidden" id="policyCatalogSelectedDetail"></section></div>`;
4245  $('.policy-catalog-editor', catalogLayout).append(documentHub);
4246  stage2.append(catalogLayout);
4247  const workingTitle = document.createElement('label');
4248  workingTitle.className = 'wide-label policy-working-title';
4249  workingTitle.innerHTML = '政策项目名称<input id="policyWorkingTitle" type="text" placeholder="ä¾
4249‹å¦‚:北京市科技企业孵化器能力提升项目">';
4250  const uploadGrid = $('.document-hub-grid', documentHub);
4251  documentHub.insertBefore(workingTitle, uploadGrid || documentHub.firstChild);
4252  const policyMetadata = document.createElement('div');
4253  policyMetadata.className = 'policy-library-metadata';
4254  policyMetadata.innerHTML = `<label>政策级别<select id="policyLibraryLevel"><option>北京市</option><option>国家级</option><option>大兴区</option><option>经开区</option><option>其他地区</option></select></label><label>适用地区<input id="policyLibraryRegion" type="text" value="北京市" placeholder="全国 / 北京市 / 大兴区"></label><label>发布单位<input id="policyLibraryIssuer" type="text" placeholder="例如:北京市科委、中关村管委会"></label><label>政策类型<select id="policyLibraryType"><option>资金支持</option><option>资质认定</option><option>研发创新</option><option>成果转化</option><option>产业化与落地</option><option>人才支持</option><option>孵化器与园区</option><option>其他</option></select></label><label>支持方向<input id="policyLibraryDirection" type="text" placeholder="ä¾
4254‹å¦‚:生物医药成果转化、科技企业孵化"></label><label>申报对象<input id="policyLibrarySubject" type="text" placeholder="例如:科技企业、孵化器、高校院所"></label><label>最高支持金额<input id="policyLibraryAmount" type="text" placeholder="例如:最高500万元 / 以通知为准"></label><label>申报截止日期<input id="policyLibraryDeadline" type="date"></label><label>当前有效状态<select id="policyLibraryValidity"><option>待核验</option><option>有效</option><option>即将截止</option><option>已截止</option></select></label>`;
4255  documentHub.insertBefore(policyMetadata, uploadGrid || documentHub.firstChild);
4256  const catalogAction = document.createElement('div');
4257  catalogAction.className = 'policy-stage-actions';
4258  catalogAction.innerHTML = '<p>AI会独立理解申报对象、硬门槛、评分项和材料清单,并将政策长期保存;此阶段不对任何企业打分。</p><div><button class="outline-button" id="cancelNewPolicyButton">取消新增</button><button class="analyze-button" id="savePolicyCatalogButton">保存并加入政策库 →</button></div>';
4259  documentHub.append(catalogAction);
4260  if (sourceRule) stage2.append(sourceRule);
4261
4262  const runMatchButton = $('#runPolicyMatch');
4263  const matchSelection = document.createElement('section');
4264  matchSelection.className = 'panel policy-match-selection';
4265  matchSelection.innerHTML = `<div class="development-panel-head"><div><strong>选择企业与政策项目</strong><p>匹配只读取所选企业的独立档案和所选政策的已解析原文</p></div><span>硬门槛优先</span></div><div class="policy-selection-grid"><article><label>申报企业<select id="policyMatchEnterpriseSelect"><option value="">请选择企业档案</option></select></label><strong id="policyMatchEnterpriseName">尚未选择</strong><p id="policyMatchEnterpriseMeta">请返回第一步建立企业档案</p><button class="outline-button" data-policy-jump="1">查看企业档案</button></article><article><label>政策项目<select id="policyCatalogSelect"><option value="">请选择已解析政策</option></select></label><strong id="policyMatchPolicyName">尚未选择</strong><p id="policyMatchPolicyMeta">请先在第二步整理政策通知和附件</p><button class="outline-button" data-policy-jump="2">查看政策资料</button></article></div><div class="policy-match-start"><div><strong>本次输出</strong><p>0—100匹配分、满足/不符合/待核验项、事实分层清单、跨文档口径核对、材料缺口、待核实交接清单和项目书草案。</p></div></div><div class="policy-analysis-status hidden" id="policyMatchRunStatus"><i></i><span><strong>正在匹配企业与政策</strong><small>逐项核对硬门槛、评分项和证明材料,请稍候…</small></span></div>`;
4266  $('.policy-match-start', matchSelection).append(runMatchButton);
4267  const matchMaterials = document.createElement('section');
4268  matchMaterials.className = 'panel policy-match-materials';
4269  matchMaterials.innerHTML = `<div class="development-panel-head"><div><strong>补充申报材料(选填)</strong><p>按"三读材料"方法:上年度申报书提供稳定事实、辅导会纪要定评分导向、自评报告供实绩反哺;逐份标注角色,AI 会区别阅读</p></div><span>不上传也可直接匹配</span></div><label class="document-upload policy-match-materials-upload" for="policyMatchMaterialsInput"><input class="hidden" id="policyMatchMaterialsInput" type="file" multiple accept=".pdf,.doc,.docx,.ppt,.pptx,.xlsx,.txt"><i>补</i><span><strong>添加本年度申报相关材料</strong><small>上年度申报书、辅导会纪要、自评报告、审计报告等,可多选</small></span><b>选择文件</b></label><div id="policyMatchMaterialsList" class="policy-match-materials-list"></div>`;
4270  matchSelection.append(matchMaterials);
4271  stage3.append(matchSelection);
4272
4273  const opportunityPanel = document.createElement('section');
4274  opportunityPanel.className = 'panel policy-opportunity-panel';
4275  opportunityPanel.id = 'policyOpportunityPanel';
4276  opportunityPanel.innerHTML = `<div class="development-panel-head"><div><strong>
4276反向机会发现 · 这家企业还能报什么</strong><p>一键扫描政策库全部有效政策,按申报建议排序;点击任意一项直接带入上方详细匹配</p></div><span id="policyOpportunityMeta">尚未扫描</span></div><div class="policy-opportunity-actions"><button class="analyze-button" id="scanPolicyOpportunitiesButton">扫描该企业可申报政策</button><span id="policyOpportunityHint">选择企业档案后可读取历史扫描结果</span></div><div id="policyOpportunityResults"></div>`;
4277  stage3.append(opportunityPanel);
4278
4279  stage4.append(result);
4280  const decision = document.createElement('section');
4281  decision.className = 'panel policy-decision-gate hidden';
4282  decision.id = 'policyDecisionGate';
4283  decision.innerHTML = `<div class="development-panel-head"><div><strong>申报立项确认</strong><p>AI负责提出建议,负责人确认政策仍有效、企业事实无误且值得投入申报</p></div><span id="policyDecisionScore">等待匹配</span></div><div class="policy-confirmation-grid"><label>本次申报项目名称<input id="policyApplicationProjectName" type="text" placeholder="请输入最终用于项目书的项目名称"></label><label>申报负责人<input id="policyApplicationOwner" type="text" placeholder="ä¾
4283‹å¦‚:企业服务部 / 张老师"></label></div><div class="policy-confirmation-checks"><label><input type="checkbox" data-policy-confirm="official"> 已打开官方原文,确认政策有效期和申报状态</label><label><input type="checkbox" data-policy-confirm="facts"> 已核对企业基本信息和关键数字</label><label><input type="checkbox" data-policy-confirm="gaps"> 已接受当前材料缺口,并明确后续补充责任人</label><label><input type="checkbox" data-policy-confirm="decision"> 确认选择该政策项目进入正式申报</label></div><div class="policy-stage-actions"><p id="policyDecisionAdvice">完成匹配后显示立项建议。</p><button class="outline-button" id="exportPolicyProposalButton">导出 Word 申报书草案</button><button class="analyze-button" id="confirmPolicyApplicationButton">确认立项,进入项目书编写 →</button></div>`;
4284  stage4.append(decision);
4285  const matchHistory = document.createElement('section');
4286  matchHistory.className = 'panel policy-match-history';
4287  matchHistory.innerHTML = `<div class="development-panel-head"><div><strong>历史匹配记录</strong><p>每次企业与政策匹配的分数、结论与立项状态;可回看完整分析或导出申报书</p></div><span id="policyMatchHistoryCount">—</span></div><div id="policyMatchHistoryList"><p>正在读取匹配记录…</p></div>`;
4288  stage4.append(matchHistory);
4289  loadPolicyMatchHistory();
4290  stage5.append(proposal);
4291  if (unifiedGrid) unifiedGrid.remove();
4292
4293  $$('#policyWorkflowSteps button', workflow).forEach(button => button.addEventListener('click', () => requestPolicyWorkflowStep(Number(button.dataset.policyStep))));
4294  $$('[data-policy-jump]', workflow).forEach(button => button.addEventListener('click', () => requestPolicyWorkflowStep(Number(button.dataset.policyJump))));
4295  $('#policyWorkflowBack').addEventListener('click', () => renderPolicyWorkflowStep(Math.max(1, policyWorkflowStep - 1)));
4296  $('#policyWorkflowNext').addEventListener('click', advancePolicyWorkflow);
4297  $('#savePolicyCatalogButton').addEventListener('click', savePolicyCatalogItem);
4298  $('#newPolicyCatalogButton').addEventListener('click', startNewPolicyCatalogItem);
4299  $('#cancelNewPolicyButton').addEventListener('click', () => { policyCatalogMode = 'library'; renderPolicyCatalogEditor(); });
4300  $('#policyCatalogSearch').addEventListener('input', event => { policyCatalogQuery = event.target.value.trim().toLowerCase(); renderPolicyCatalog(); });
4301  $('#policyCatalogLevelFilter').addEventListener('change', event => { policyCatalogLevel = event.target.value; renderPolicyCatalog(); });
4302  $('#policyCatalogValidityFilter').addEventListener('change', event => { policyCatalogValidityFilterValue = event.target.value; renderPolicyCatalog(); });
4303  $('#policyCatalogShowMismatch').addEventListener('change', event => { policyCatalogShowMismatch = event.target.checked; renderPolicyCatalog(); });
4304  $('#policyCatalogList').addEventListener('click', event => {
4305    const button = event.target.closest('[data-policy-catalog-id]');
4306    if (button) selectPolicyCatalog(button.dataset.policyCatalogId);
4307  });
4308  $('#policyMatchEnterpriseSelect').addEventListener('change', event => loadPolicyEnterprise(event.target.value));
4309  $('#scanPolicyOpportunitiesButton').addEventListener('click', scanPolicyOpportunities);
4310  $('#policyOpportunityResults').addEventListener('click', event => {
4311    const card = event.target.closest('[data-opportunity-policy-id]');
4312    if (card) applyOpportunityToMatch(card.dataset.opportunityPolicyId);
4313  });
4314  $('#policyCatalogSelect').addEventListener('change', event => selectPolicyCatalog(event.target.value));
4315  $('#policyMatchMaterialsInput').addEventListener('change', event => { addPolicyMatchMaterials(event.target.files); event.target.value = ''; });
4316  $('#policyMatchMaterialsList').addEventListener('change', event => {
4317    const select = event.target.closest('[data-match-material-role]');
4318    if (select) policyMatchMaterialFiles[Number(select.dataset.matchMaterialRole)].role = select.value;
4319  });
4320  $('#policyMatchMaterialsList').addEventListener('click', event => {
4321    const button = event.target.closest('[data-match-material-remove]');
4322    if (button) { policyMatchMaterialFiles.splice(Number(button.dataset.matchMaterialRemove), 1); renderPolicyMatchMaterials(); }
4323  });
4324  $('#policyCatalogSelectedDetail').addEventListener('click', event => {
4325    if (event.target.closest('[data-policy-action="match"]')) renderPolicyWorkflowStep(3);
4326    if (event.target.closest('[data-policy-action="new"]')) startNewPolicyCatalogItem();
4327  });
4328  $('#confirmPolicyApplicationButton').addEventListener('click', confirmPolicyApplication);
4329  $('#exportPolicyProposalButton').addEventListener('click', exportPolicyProposalDocx);
4330  const deadlineBanner = document.createElement('div');
4331  deadlineBanner.className = 'policy-deadline-banner hidden';
4332  deadlineBanner.id = 'policyDeadlineBanner';
4333  workflow.before(deadlineBanner);
4334  loadPolicyDeadlineAlerts();
4335  renderPolicyWorkflowStep(1);
4336}
4337
4338// ============ 截止日主动提醒横幅 ============
4339
4340async function loadPolicyDeadlineAlerts() {
4341  const banner = $('#policyDeadlineBanner');
4342  if (!banner) return;
4343  try {
4344    const data = await policyApiRequest('/api/policy/deadline-alerts?days=14');
4345    renderPolicyDeadlineBanner(data);
4346  } catch (error) {
4347    banner.classList.add('hidden');
4348  }
4349}
4350
4351function renderPolicyDeadlineBanner(data) {
4352  const banner = $('#policyDeadlineBanner');
4353  if (!banner) return;
4354  const alerts = Array.isArray(data?.alerts) ? data.alerts : [];
4355  if (!alerts.length || banner.dataset.dismissed === 'true') { banner.classList.add('hidden'); return; }
4356  const groups = [
4357    ['紧急 · 3 天内', item => item.days_left <= 3],
4358    ['一周内截止', item => item.days_left > 3 && item.days_left <= 7],
4359    ['两周内截止', item => item.days_left > 7],
4360  ];
4361  const sections = groups.map(([label, test]) => {
4362    const items = alerts.filter(test);
4363    if (!items.length) return '';
4364    return `<div class="policy-deadline-group"><h5>${label}<i>${items.length}项</i></h5>${items.map(item => `<button type="button" class="policy-deadline-row${item.days_left <= 3 ? ' urgent' : ''}" data-deadline-policy-id="${escapeHtml(item.policy_id)}"><b>${item.days_left === 0 ? '今天截止' : `剩 ${item.days_left} 天`}</b><span class="policy-deadline-title">${escapeHtml(item.title)}</span><small>${escapeHtml(item.level || '')}${item.issuing_authority ? ` · ${escapeHtml(item.issuing_authority)}` : ''}</small>${item.matched_enterprise ? `<em>${escapeHtml(item.matched_enterprise)} · ${escapeHtml(item.matched_verdict)}</em>` : '<em class="policy-deadline-nomatch">尚未匹配企业</em>'}</button>`).join('')}</div>`;
4365  }).join('');
4366  banner.innerHTML = `<div class="policy-deadline-head"><strong>⏰ ${alerts.length} 项政策 14 天内截止</strong><span>按紧急程度分组,点击任意一条直接带入匹配</span><i id="policyDeadlineBannerDismiss" title="本次会话不再提示">×</i></div><div class="policy-deadline-items">${sections}</div>`;
4367  banner.classList.remove('hidden');
4368  $('#policyDeadlineBannerDismiss').addEventListener('click', () => { banner.dataset.dismissed = 'true'; banner.classList.add('hidden'); });
4369  $$('[data-deadline-policy-id]', banner).forEach(btn => btn.addEventListener('click', () => applyOpportunityToMatch(btn.dataset.deadlinePolicyId)));
4370}
4371
4372function renderPolicyWorkflowStep(step) {
4373  policyWorkflowStep = Math.max(1, Math.min(5, Number(step) || 1));
4374  $$('#policyWorkflowSteps button').forEach(button => {
4375    const value = Number(button.dataset.policyStep);
4376    button.classList.toggle('active', value === policyWorkflowStep);
4377    button.classList.toggle('complete', value < policyWorkflowStep);
4378  });
4379  $$('.policy-workflow-stage').forEach(stage => stage.classList.toggle('active', Number(stage.dataset.policyStage) === policyWorkflowStep));
4380  $('#policyWorkflowBack').classList.toggle('hidden', policyWorkflowStep === 1);
4381  $('#policyWorkflowNext').classList.toggle('hidden', policyWorkflowStep >= 3);
4382  $('#policyWorkflowNext').textContent = policyWorkflowStep === 1 ? '企业档案已准备,整理政策 →' : '政策资料已准备,开始匹配 →';
4383  $('#policyContextEnterprise').textContent = currentPolicyEnterprise?.name || '尚未选择';
4384  $('#policyContextPolicy').textContent = policyWorkflowStep === 2 && policyCatalogMode === 'new' ? '正在新增政策' : (currentPolicyCatalog?.title || '尚未选择');
4385  if (policyWorkflowStep === 5) renderProposalDraft(policyAnalysisResult);
4386  window.scrollTo({top:0,behavior:'smooth'});
4387}
4388
4389function requestPolicyWorkflowStep(step) {
4390  if (step > 1 && !currentPolicyEnterprise) { showToast('请先完成企业建档'); renderPolicyWorkflowStep(1); return; }
4391  if (step > 2 && !currentPolicyCatalog) { showToast('请先保存并选择一项政策资料'); renderPolicyWorkflowStep(2); return; }
4392  if (step > 3 && !policyAnalysisResult) { showToast('请先完成企业与政策匹配'); renderPolicyWorkflowStep(3); return; }
4393  if (step === 5 && !currentPolicyApplicationId) { showToast('请先由负责人确认申报立项'); renderPolicyWorkflowStep(4); return; }
4394  renderPolicyWorkflowStep(step);
4395}
4396
4397function advancePolicyWorkflow() {
4398  if (policyWorkflowStep === 1) {
4399    if (!currentPolicyEnterprise) { showToast('请先创建或选择企业档案'); return; }
4400    renderPolicyWorkflowStep(2);
4401    return;
4402  }
4403  if (policyWorkflowStep === 2) {
4404    if (!currentPolicyCatalog) { showToast('请先保存或选择一项已经解析的政策'); return; }
4405    renderPolicyWorkflowStep(3);
4406  }
4407}
4408
4409function policyCatalogOverview(policy) {
4410  return policy?.analysis?.policy_overview || {};
4411}
4412
4413function policyCatalogMetadata(policy) {
4414  const metadata = policy?.metadata || {};
4415  const overview = policyCatalogOverview(policy);
4416  return {
4417    level:metadata.level || '级别待核验',region:metadata.region || '地区待核验',
4418    issuing_authority:metadata.issuing_authority || overview.issuing_authority || '发布单位待确认',
4419    policy_type:metadata.policy_type || '类型待分类',support_direction:metadata.support_direction || '支持方向待提取',
4420    eligible_subject:metadata.eligible_subject || '申报对象待提取',support_amount:metadata.support_amount || overview.support_amount || '以通知为准',
4421    deadline:metadata.deadline || overview.deadline || '截止时间待确认',validity_status:metadata.validity_status || '待核验'
4422  };
4423}
4424
4425function policyCatalogValidity(policy) {
4426  const metadata = policyCatalogMetadata(policy);
4427  const deadline = String(metadata.deadline || '');
4428  if (/^\d{4}-\d{2}-\d{2}/.test(deadline)) {
4429    const end = new Date(`${deadline.slice(0,10)}T23:59:59`);
4430    const days = Math.ceil((end.getTime() - Date.now()) / 86400000);
4431    if (days < 0) return '已截止';
4432    if (days <= 30) return '即将截止';
4433  }
4434  return metadata.validity_status || '待核验';
4435}
4436
4437const POLICY_KEEP_LEVELS = ['国家级', '北京市', '大兴区'];
4438
4439function policyCatalogIsMismatch(policy) {
4440  const metadata = policy?.metadata || {};
4441  if (metadata.region_mismatch || metadata.unverified_seed) return true;
4442  return !POLICY_KEEP_LEVELS.includes(policyCatalogMetadata(policy).level);
4443}
4444
4445function filteredPolicyCatalog() {
4446  return policyCatalog.filter(policy => {
4447    const metadata = policyCatalogMetadata(policy);
4448    if (!policyCatalogShowMismatch && policyCatalogIsMismatch(policy)) return false;
4449    const haystack = [policy.title,metadata.issuing_authority,metadata.support_direction,metadata.region,metadata.policy_type].join(' ').toLowerCase();
4450    const queryMatch = !policyCatalogQuery || haystack.includes(policyCatalogQuery);
4451    const levelMatch = policyCatalogLevel === '全部级别' || metadata.level === policyCatalogLevel;
4452    const validityMatch = policyCatalogValidityFilterValue === '全部状态' || policyCatalogValidity(policy) === policyCatalogValidityFilterValue;
4453    return queryMatch && levelMatch && validityMatch;
4454  });
4455}
4456
4457function groupPolicyCatalog(policies) {
4458  const groups = [];
4459  const buckets = {};
4460  POLICY_KEEP_LEVELS.forEach(level => { buckets[level] = []; groups.push([level, buckets[level]]); });
4461  buckets['其他'] = [];
4462  groups.push(['其他(区域不符)', buckets['其他']]);
4463  policies.forEach(policy => {
4464    const level = policyCatalogMetadata(policy).level;
4465    (buckets[level] || buckets['其他']).push(policy);
4466  });
4467  const validityRank = {'即将截止':0,'有效':1,'待核验':2,'已截止':3};
4468  groups.forEach(([, items]) => items.sort((a, b) => {
4469    const rankDiff = (validityRank[policyCatalogValidity(a)] ?? 4) - (validityRank[policyCatalogValidity(b)] ?? 4);
4470    if (rankDiff) return rankDiff;
4471    return String(policyCatalogMetadata(a).deadline).localeCompare(String(policyCatalogMetadata(b).deadline));
4472  }));
4473  return groups.filter(([, items]) => items.length);
4474}
4475
4476function renderPolicyCatalog() {
4477  const filtered = filteredPolicyCatalog();
4478  const mismatchCount = policyCatalog.filter(policyCatalogIsMismatch).length;
4479  const mismatchLabel = $('#policyCatalogMismatchCount');
4480  if (mismatchLabel) mismatchLabel.textContent = mismatchCount ? `(${mismatchCount})` : '';
4481  $('#policyCatalogCount').textContent = filtered.length === policyCatalog.length ? `${policyCatalog.length}项` : `${filtered.length}/${policyCatalog.length}项`;
4482  const groups = groupPolicyCatalog(filtered);
4483  const cardHtml = policy => {
4484    const metadata = policyCatalogMetadata(policy);
4485    const validity = policyCatalogValidity(policy);
4486    const active = currentPolicyCatalog?.id === policy.id ? 'active' : '';
4487    return `<button class="policy-catalog-card ${active}" data-investment-policy-validity="${escapeHtml(validity)}" data-policy-catalog-id="${escapeHtml(policy.id)}"><span><b>${escapeHtml(metadata.level)} · ${escapeHtml(validity)}</b><small>${Number(policy.document_count || 0)}份附件</small></span><strong>${escapeHtml(policy.title)}</strong><p>${escapeHtml(metadata.issuing_authority)} · ${escapeHtml(metadata.deadline)}</p><em>${escapeHtml(metadata.support_direction)}</em></button>`;
4488  };
4489  $('#policyCatalogList').innerHTML = groups.length ? groups.map(([level, items]) =>
4490    `<div class="policy-catalog-group"><h4>${escapeHtml(level)}<i>${items.length}项</i></h4>${items.map(cardHtml).join('')}</div>`
4491  ).join('') : policyCatalog.length ? '<div class="policy-catalog-empty"><strong>没有符合筛选条件的政策</strong><p>可以调整关键词、政策级别或有效状态。</p></div>' : '<div class="policy-catalog-empty"><strong>政策库还是空的</strong><p>点击“新增政策”,上传通知、附件和模板,AI解析后会长期保存在这里。</p></div>';
4492  const selectable = groupPolicyCatalog(policyCatalog.filter(policy => policyCatalogShowMismatch || !policyCatalogIsMismatch(policy)));
4493  $('#policyCatalogSelect').innerHTML = `<option value="">请选择已解析政策</option>${selectable.map(([level, items]) =>
4494    `<optgroup label="${escapeHtml(level)}(${items.length}
4494项)">${items.map(policy => `<option value="${escapeHtml(policy.id)}">${escapeHtml(policy.title)}</option>`).join('')}</optgroup>`
4495  ).join('')}`;
4496  if (currentPolicyCatalog) $('#policyCatalogSelect').value = currentPolicyCatalog.id;
4497  renderPolicyCatalogEditor();
4498}
4499
4500function resetPolicyCatalogForm() {
4501  policyGuideFiles = [];
4502  policyTemplateFile = null;
4503  $('#policyWorkingTitle').value = '';
4504  $('#policyLibraryLevel').value = '北京市';
4505  $('#policyLibraryRegion').value = '北京市';
4506  $('#policyLibraryIssuer').value = '';
4507  $('#policyLibraryType').value = '资金支持';
4508  $('#policyLibraryDirection').value = '';
4509  $('#policyLibrarySubject').value = '';
4510  $('#policyLibraryAmount').value = '';
4511  $('#policyLibraryDeadline').value = '';
4512  $('#policyLibraryValidity').value = '待核验';
4513  $('#policySourceUrl').value = '';
4514  $('#policyGuideText').value = '';
4515  $('#policyGuideInput').value = '';
4516  $('#policyTemplateInput').value = '';
4517  $('#policyGuideFileName').textContent = '选择文件';
4518  $('#policyTemplateFileName').textContent = '选择模板';
4519  $('#policyGuideLedger').classList.add('hidden');
4520  $('#policyGuideList').innerHTML = '';
4521  $('#policyAnalysisStatus').classList.add('hidden');
4522}
4523
4524function startNewPolicyCatalogItem() {
4525  policyCatalogMode = 'new';
4526  resetPolicyCatalogForm();
4527  renderPolicyCatalogEditor();
4528  $('#policyContextPolicy').textContent = '正在新增政策';
4529  $('#policyWorkingTitle').focus();
4530}
4531
4532function renderPolicyCatalogEditor() {
4533  const editor = $('.policy-document-hub');
4534  const detail = $('#policyCatalogSelectedDetail');
4535  if (!editor || !detail) return;
4536  if (!policyCatalog.length && policyCatalogMode === 'library') policyCatalogMode = 'new';
4537  if (policyCatalogMode === 'new' || !currentPolicyCatalog) {
4538    editor.classList.remove('hidden');
4539    detail.classList.add('hidden');
4540    return;
4541  }
4542  const policy = currentPolicyCatalog;
4543  const metadata = policyCatalogMetadata(policy);
4544  const overview = policyCatalogOverview(policy);
4545  const validity = policyCatalogValidity(policy);
4546  detail.innerHTML = `<div class="policy-selected-head"><div><span>已从政策库选择</span><h2>${escapeHtml(policy.title)}</h2><p>${escapeHtml(metadata.issuing_authority)} · ${escapeHtml(metadata.region)} · ${escapeHtml(metadata.policy_type)}</p></div><b data-validity="${escapeHtml(validity)}">${escapeHtml(validity)}</b></div><div class="policy-selected-metadata"><span><small>政策级别</small><strong>${escapeHtml(metadata.level)}</strong></span><span><small>支持方向</small><strong>${escapeHtml(metadata.support_direction)}</strong></span><span><small>申报对象</small><strong>${escapeHtml(metadata.eligible_subject)}</strong></span><span><small>最高支持</small><strong>${escapeHtml(metadata.support_amount)}</strong></span><span><small>申报截止</small><strong>${escapeHtml(metadata.deadline)}</strong></span><span><small>政策附件</small><strong>${Number(policy.document_count || 0)}份</strong></span></div><div class="policy-selected-summary"><strong>AI政策理解</strong><p>${escapeHtml(overview.source_boundary || '已保存政策原文和解析结果;正式申报前仍需核验官方有效性与最新通知。')}</p></div><div class="policy-selected-actions"><button class="outline-button" data-policy-action="new">+ 新增另一项政策</button><button class="analyze-button" data-policy-action="match">使用这项政策选择企业并评分 →</button></div>`;
4547  editor.classList.add('hidden');
4548  detail.classList.remove('hidden');
4549}
4550
4551async function loadPolicyCatalog(force = false) {
4552  if (policyCatalogLoaded && !force) return;
4553  try {
4554    policyCatalog = await policyApiRequest('/api/policy/catalog');
4555    policyCatalogLoaded = true;
4556    if (currentPolicyCatalog && !policyCatalog.some(item=>item.id===currentPolicyCatalog.id)) currentPolicyCatalog = null;
4557    renderPolicyCatalog();
4558    if (!currentPolicyCatalog && policyCatalog.length) await selectPolicyCatalog(policyCatalog[0].id);
4559  } catch (error) {
4560    $('#policyCatalogList').innerHTML = `<div class="policy-catalog-empty"><strong>政策资料库暂未连接</strong><p>${escapeHtml(error.message)}</p></div>`;
4561  }
4562}
4563
4564async function selectPolicyCatalog(policyId) {
4565  if (!policyId) { currentPolicyCatalog = null; renderPolicyCatalog(); renderPolicyMatchContext(); return; }
4566  try {
4567    currentPolicyCatalog = await policyApiRequest(`/api/policy/catalog/${encodeURIComponent(policyId)}`);
4568    policyCatalogMode = 'library';
4569    renderPolicyCatalog();
4570    renderPolicyMatchContext();
4571  } catch (error) {
4572    showToast(error.message);
4573  }
4574}
4575
4576async function savePolicyCatalogItem() {
4577  const pastedGuide = $('#policyGuideText').value.trim();
4578  if (!policyGuideFiles.length && pastedGuide.length < 20) { $('#policyGuideText').focus(); showToast('请上传政策通知/附件,或粘贴不少于20字的政策要求'); return; }
4579  const form = new FormData();
4580  policyGuideFiles.forEach(file=>form.append('guide_files',file));
4581  if (policyTemplateFile) form.append('template_file',policyTemplateFile);
4582  form.append('title',$('#policyWorkingTitle').value.trim());
4583  form.append('guide_text',pastedGuide);
4584  form.append('source_url',$('#policySourceUrl').value.trim());
4585  form.append('metadata_json',JSON.stringify({
4586    level:$('#policyLibraryLevel').value,region:$('#policyLibraryRegion').value.trim(),
4587    issuing_authority:$('#policyLibraryIssuer').value.trim(),policy_type:$('#policyLibraryType').value,
4588    support_direction:$('#policyLibraryDirection').value.trim(),eligible_subject:$('#policyLibrarySubject').value.trim(),
4589    support_amount:$('#policyLibraryAmount').value.trim(),deadline:$('#policyLibraryDeadline').value,
4590    validity_status:$('#policyLibraryValidity').value
4591  }));
4592  const button = $('#savePolicyCatalogButton');
4593  const status = $('#policyAnalysisStatus');
4594  button.disabled = true;
4595  button.textContent = '正在解析政策通知和附件…';
4596  status.classList.remove('hidden','error','complete');
4597  $('#policyAnalysisStatusTitle').textContent = '正在建立政策资料';
4598  $('#policyAnalysisStatusDetail').textContent = '提取申报对象、硬门槛、评分项、截止时间、材料清单和模板章节';
4599  try {
4600    currentPolicyCatalog = await policyApiRequest('/api/policy/catalog',{method:'POST',body:form});
4601    status.classList.add('complete');
4602    $('#policyAnalysisStatusTitle').textContent = '政策资料已保存并完成AI解析';
4603    $('#policyAnalysisStatusDetail').textContent = '现在可以选择任一企业与这项政策进行匹配';
4604    policyCatalogLoaded = false;
4605    await loadPolicyCatalog(true);
4606    await selectPolicyCatalog(currentPolicyCatalog.id);
4607    policyCatalogMode = 'library';
4608    renderPolicyCatalogEditor();
4609    showToast('政策已进入资料库,可用于不同企业的匹配');
4610  } catch (error) {
4611    status.classList.add('error');
4612    $('#policyAnalysisStatusTitle').textContent = '政策资料未保存';
4613    $('#policyAnalysisStatusDetail').textContent = error.message;
4614  } finally {
4615    button.disabled = false;
4616    button.textContent = '保存并解析为政策资料 →';
4617  }
4618}
4619
4620function renderPolicyMatchContext() {
4621  if ($('#policyContextEnterprise')) $('#policyContextEnterprise').textContent = currentPolicyEnterprise?.name || '尚未选择';
4622  if ($('#policyContextPolicy')) $('#policyContextPolicy').textContent = currentPolicyCatalog?.title || '尚未选择';
4623  if ($('#policyMatchEnterpriseName')) $('#policyMatchEnterpriseName').textContent = currentPolicyEnterprise?.name || '尚未选择';
4624  if ($('#policyMatchEnterpriseMeta')) $('#policyMatchEnterpriseMeta').textContent = currentPolicyEnterprise ? `${currentPolicyEnterprise.region || '区域待补充'} · ${currentPolicyEnterprise.industry || '行业待补充'} · ${Number(currentPolicyEnterprise.document_count || 0)}份资料` : '请返回第一步建立企业档案';
4625  if ($('#policyMatchEnterpriseSelect') && currentPolicyEnterprise) $('#policyMatchEnterpriseSelect').value = currentPolicyEnterprise.id;
4626  const overview = policyCatalogOverview(currentPolicyCatalog);
4627  const metadata = policyCatalogMetadata(currentPolicyCatalog);
4628  if ($('#policyMatchPolicyName')) $('#policyMatchPolicyName').textContent = currentPolicyCatalog?.title || '尚未选择';
4629  if ($('#policyMatchPolicyMeta')) $('#policyMatchPolicyMeta').textContent = currentPolicyCatalog ? `${metadata.level} · ${metadata.issuing_authority} · ${metadata.deadline} · ${Number(currentPolicyCatalog.document_count || 0)}份附件` : '请先在第二步选择已有政策或新增政策';
4630  if ($('#policyCatalogSelect') && currentPolicyCatalog) $('#policyCatalogSelect').value = currentPolicyCatalog.id;
4631}
4632
4633async function confirmPolicyApplication() {
4634  if (!policyAnalysisResult || !currentPolicyApplicationId) { showToast('请先完成政策匹配'); return; }
4635  const projectName = $('#policyApplicationProjectName').value.trim();
4636  const owner = $('#policyApplicationOwner').value.trim();
4637  const checks = $$('[data-policy-confirm]');
4638  if (!projectName) { $('#policyApplicationProjectName').focus(); showToast('请确认本次申报项目名称'); return; }
4639  if (!owner) { $('#policyApplicationOwner').focus(); showToast('请填写申报负责人'); return; }
4640  if (checks.some(item=>!item.checked)) { showToast('请完成四项申报立项确认'); return; }
4641  const payload = {project_name:projectName,owner,confirmed_items:checks.map(item=>item.dataset.policyConfirm),confirmed_at:new Date().toISOString()};
4642  const button = $('#confirmPolicyApplicationButton');
4643  button.disabled = true;
4644  try {
4645    await policyApiRequest(`/api/policy/matches/${encodeURIComponent(currentPolicyApplicationId)}/confirm`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
4646    loadPolicyMatchHistory();
4647    renderProposalDraft(policyAnalysisResult);
4648    renderPolicyWorkflowStep(5);
4649    showToast('申报立项已确认,项目书工作台已开启');
4650  } catch (error) {
4651    showToast(error.message);
4652  } finally {
4653    button.disabled = false;
4654  }
4655}
4656
4657function setPolicySelectValue(selector, value) {
4658  const select = $(selector);
4659  if (!select || !value) return;
4660  if (![...select.options].some(option => option.value === value)) {
4661    select.add(new Option(value, value));
4662  }
4663  select.value = value;
4664}
4665
4666const POLICY_QUALIFICATION_OPTIONS = [
4667  '国家高新技术企业','科技型中小企业','专精特新中小企业','专精特新“小巨人”企业','创新型中小企业','瞪羚企业','中关村高新技术企业',
4668  '国家级科技企业孵化器','市级科技企业孵化器','区级科技企业孵化器','国家备案众创空间','市级众创空间','工信部科技企业孵化器'
4669];
4670const POLICY_QUAL_NONE = '暂未取得';
4671const POLICY_QUAL_OTHER = '其他';
4672
4673function initQualTagGroup(groupId, otherInputId) {
4674  const group = document.getElementById(groupId);
4675  if (!group) return;
4676  const options = [...POLICY_QUALIFICATION_OPTIONS, POLICY_QUAL_NONE, POLICY_QUAL_OTHER];
4677  group.innerHTML = options.map(name => `<label class="qual-tag${name === POLICY_QUAL_NONE ? ' qual-tag-none' : ''}"><input type="checkbox" value="${escapeHtml(name)}"><span>${escapeHtml(name)}</span></label>`).join('');
4678  const otherInput = otherInputId ? document.getElementById(otherInputId) : null;
4679  group.addEventListener('change', event => {
4680    const target = event.target;
4681    if (!target || target.type !== 'checkbox') return;
4682    const noneBox = group.querySelector(`input[value="${POLICY_QUAL_NONE}"]`);
4683    if (noneBox && target === noneBox && target.checked) {
4684      group.querySelectorAll('input[type="checkbox"]').forEach(box => { if (box !== noneBox) box.checked = false; });
4685      if (otherInput) { otherInput.value = ''; otherInput.classList.add('hidden'); }
4686    } else if (noneBox && target !== noneBox && target.checked) {
4687      noneBox.checked = false;
4688    }
4689    const otherBox = group.querySelector(`input[value="${POLICY_QUAL_OTHER}"]`);
4690    if (otherInput && otherBox) otherInput.classList.toggle('hidden', !otherBox.checked);
4691    if (otherInput && otherBox && !otherBox.checked) otherInput.value = '';
4692  });
4693}
4694
4695function collectQualifications(groupId, otherInputId) {
4696  const group = document.getElementById(groupId);
4697  if (!group) return [];
4698  const otherInput = otherInputId ? document.getElementById(otherInputId) : null;
4699  return [...group.querySelectorAll('input[type="checkbox"]:checked')].map(box => {
4700    if (box.value !== POLICY_QUAL_OTHER) return box.value;
4701    const extra = otherInput ? otherInput.value.trim() : '';
4702    return extra ? `其他:${extra}` : POLICY_QUAL_OTHER;
4703  });
4704}
4705
4706function matchStandardQualification(token) {
4707  const text = String(token || '').trim();
4708  if (!text) return null;
4709  if (POLICY_QUALIFICATION_OPTIONS.includes(text)) return text;
4710  if (text.includes('暂未取得') || text.includes('暂无资质')) return POLICY_QUAL_NONE;
4711  if (text.includes('小巨人')) return '专精特新“小巨人”企业';
4712  if (text.includes('专精特新')) return '专精特新中小企业';
4713  if (text.includes('瞪羚')) return '瞪羚企业';
4714  if (text.includes('中关村')) return '中关村高新技术企业';
4715  if (text.includes('创新型中小企业')) return '创新型中小企业';
4716  if (text.includes('众创')) {
4717    if (text.includes('国家') || text.includes('备案')) return '国家备案众创空间';
4718    if (text.includes('市')) return '市级众创空间';
4719    return null;
4720  }
4721  if (text.includes('孵化器')) {
4722    if (text.includes('工信部') || text.includes('部级')) return '工信部科技企业孵化器';
4723    if (text.includes('国家')) return '国家级科技企业孵化器';
4724    if (text.includes('市')) return '市级科技企业孵化器';
4725    if (text.includes('区')) return '区级科技企业孵化器';
4726    return null;
4727  }
4728  if (text.includes('高新')) return '国家高新技术企业';
4729  if (text.includes('科技型中小企业')) return '科技型中小企业';
4730  return null;
4731}
4732
4733function renderQualificationTags(groupId, otherInputId, values) {
4734  const group = document.getElementById(groupId);
4735  if (!group) return;
4736  const otherInput = otherInputId ? document.getElementById(otherInputId) : null;
4737  const tokens = Array.isArray(values) ? values : String(values || '').split(/[、,,
4737;;/]+/).map(item => item.trim()).filter(Boolean);
4738  const checked = new Set();
4739  const otherTexts = [];
4740  tokens.forEach(token => {
4741    if (String(token).startsWith(POLICY_QUAL_OTHER)) {
4742      const extra = String(token).replace(/^其他[::]?/, '').trim();
4743      if (extra) otherTexts.push(extra);
4744      checked.add(POLICY_QUAL_OTHER);
4745      return;
4746    }
4747    const matched = matchStandardQualification(token);
4748    if (matched) checked.add(matched);
4749    else { checked.add(POLICY_QUAL_OTHER); otherTexts.push(token); }
4750  });
4751  group.querySelectorAll('input[type="checkbox"]').forEach(box => { box.checked = checked.has(box.value); });
4752  if (otherInput) {
4753    otherInput.value = otherTexts.join('、');
4754    const otherBox = group.querySelector(`input[value="${POLICY_QUAL_OTHER}"]`);
4755    otherInput.classList.toggle('hidden', !(otherBox && otherBox.checked));
4756  }
4757}
4758
4759function resetQualificationTags(groupId, otherInputId) {
4760  const group = document.getElementById(groupId);
4761  if (!group) return;
4762  group.querySelectorAll('input[type="checkbox"]').forEach(box => { box.checked = false; });
4763  const otherInput = otherInputId ? document.getElementById(otherInputId) : null;
4764  if (otherInput) { otherInput.value = ''; otherInput.classList.add('hidden'); }
4765}
4766
4767function renderTycSyncSummary(sync) {
4768  const box = $('#tycSyncSummary');
4769  if (!box) return;
4770  const basic = sync.basic || {};
4771  const rows = [
4772    ['成立时间', basic.established_date],
4773    ['注册资本', basic.reg_capital],
4774    ['法定代表人', basic.legal_person],
4775    ['信用代码', basic.credit_code],
4776    ['登记状态', basic.reg_status],
4777    ['人员规模', basic.staff_range || (basic.social_staff_num != null ? `${basic.social_staff_num}人参保` : '')],
4778  ].filter(([, value]) => value);
4779  const shareholders = Array.isArray(sync.shareholders) ? sync.shareholders : [];
4780  const patents = sync.patents || {};
4781  const patentItems = Array.isArray(patents.items) ? patents.items : [];
4782  const penalty = sync.administrative_penalty || {};
4783  const tags = Array.isArray(sync.tags) ? sync.tags : [];
4784  const hints = Array.isArray(sync.qualification_hints) ? sync.qualification_hints : [];
4785  box.innerHTML = `
4786    <div class="tyc-sync-grid">${rows.map(([key, value]) => `<span><small>${key}</small><b>${escapeHtml(String(value))}</b></span>`).join('')}</div>
4787    ${shareholders.length ? `<div class="tyc-sync-block"><strong>股东(${shareholders.length}名)</strong><span>${shareholders.map(item => `${escapeHtml(item.name || '—')}${item.percent ? ` ${escapeHtml(item.percent)}` : ''}${item.amount ? `(认缴${escapeHtml(item.amount)})` : ''}`).join(';')}</span></div>` : ''}
4788    <div class="tyc-sync-block"><strong>专利</strong><span>共 ${Number(patents.total || 0)} 件${patentItems.length ? `:${patentItems.map(item => escapeHtml(item.title || '未命名专利')).join(';')}` : ''}</span></div>
4789    <div class="tyc-sync-block"><strong>行政处罚</strong><span>${escapeHtml(penalty.note || '未查询')}</span></div>
4790    ${tags.length ? `<div class="tyc-sync-block"><strong>天眼查标签</strong><span>${tags.map(item => escapeHtml(item)).join(';')}</span></div>` : ''}
4791    ${hints.length ? `<div class="tyc-sync-block"><strong>已自动勾选资质</strong><span>${hints.map(item => escapeHtml(item)).join('、')}</span></div>` : ''}
4792    <p class="tyc-sync-source">数据来源:天眼查,以工商公示为准 · 查询时间 ${escapeHtml(sync.queried_at || '')}</p>`;
4793  box.classList.remove('hidden');
4794}
4795
4796async function syncTianyanchaForNewEnterprise() {
4797  const nameInput = $('#policyEnterpriseName');
4798  const name = nameInput.value.trim();
4799  if (!name) { nameInput.focus(); showToast('请先填写企业全称,再从天眼查同步'); return; }
4800  const button = $('#tycSyncButton');
4801  const originalText = button.textContent;
4802  button.disabled = true;
4803  button.textContent = '同步中…';
4804  try {
4805    const result = await policyApiRequest('/api/policy/enterprises/tianyancha-sync', {
4806      method: 'POST',
4807      headers: {'Content-Type': 'application/json'},
4808      body: JSON.stringify({name}),
4809    });
4810    const sync = result.sync || result;
4811    const basic = sync.basic || {};
4812    const regionInput = $('#policyEnterpriseRegion');
4813    const currentRegion = regionInput.value.trim();
4814    if (basic.suggested_region && (!currentRegion || currentRegion === (regionInput.defaultValue || '').trim())) {
4815      regionInput.value = basic.suggested_region;
4816    }
4817    const hints = Array.isArray(sync.qualification_hints) ? sync.qualification_hints : [];
4818    if (hints.length) {
4819      const existing = collectQualifications('policyEnterpriseQualGroup', 'policyEnterpriseCertificationOther').filter(item => item !== POLICY_QUAL_NONE);
4820      renderQualificationTags('policyEnterpriseQualGroup', 'policyEnterpriseCertificationOther', [...new Set([...existing, ...hints])]);
4821    }
4822    renderTycSyncSummary(sync);
4823    showToast('天眼查工商数据同步完成,请核对后创建档案');
4824  } catch (error) {
4825    showToast(error.message);
4826  } finally {
4827    button.disabled = false;
4828    button.textContent = originalText;
4829  }
4830}
4831
4832async function syncTianyanchaForExistingEnterprise() {
4833  if (!currentPolicyEnterprise) { showToast('请先选择企业档案'); return; }
4834  const button = $('#tycSyncExistingButton');
4835  const originalText = button.textContent;
4836  button.disabled = true;
4837  button.textContent = '同步中…';
4838  try {
4839    const result = await policyApiRequest('/api/policy/enterprises/tianyancha-sync', {
4840      method: 'POST',
4841      headers: {'Content-Type': 'application/json'},
4842      body: JSON.stringify({name: currentPolicyEnterprise.name, enterprise_id: currentPolicyEnterprise.id}),
4843    });
4844    if (result.enterprise) {
4845      currentPolicyEnterprise = result.enterprise;
4846      renderPolicyEnterprise(result.enterprise);
4847    }
4848    showToast('已同步天眼查工商数据,请确认后保存画像');
4849  } catch (error) {
4850    showToast(error.message);
4851  } finally {
4852    button.disabled = false;
4853    button.textContent = originalText;
4854  }
4855}
4856
4857function applyEnterpriseProfileToForm(enterprise) {
4858  const profile = enterprise?.profile || {};
4859  $('#policyProfileVersion').innerHTML = `<option value="enterprise_archive">${escapeHtml(enterprise.name)} · 当前档案口径</option>`;
4860  setPolicySelectValue('#policyRegion', profile.region || enterprise.region || '北京市大兴区');
4861  setPolicySelectValue('#policyIndustry', profile.industry || enterprise.industry || '其他医药健康企业');
4862  renderQualificationTags('policyQualGroup', 'policyCertificationOther', profile.qualifications || profile.certification || enterprise.certification || '暂未取得');
4863  setPolicySelectValue('#policyStage', profile.stage || enterprise.stage || '稳定运营与能力输出');
4864  $('#policyRdSpend').value = Number(profile.rd_spend_wan || 0);
4865  $('#policyPatents').value = Number(profile.intellectual_property_count || 0);
4866  $('#policyArea').value = Number(profile.incubation_area_sqm || 0);
4867  $('#policyIncubatedCompanies').value = Number(profile.current_incubated_companies || 0);
4868  $('#policyServedCompanies').value = Number(profile.cumulative_incubated_companies || 0);
4869  $('#companyApplicationEvidence').innerHTML = `<strong>当前企业档案:${escapeHtml(enterprise.name)}</strong><span>${escapeHtml(profile.profile_version || '企业资料库当前版本')}</span><small>企业画像和AI摘要来自本企业独立资料库;政策申报仍需逐项绑定原始证明附件。</small>`;
4870}
4871
4872function renderPolicyEnterprise(enterprise) {
4873  currentPolicyEnterprise = enterprise;
4874  const ai = enterprise.ai_profile || {};
4875  const documents = Array.isArray(enterprise.documents) ? enterprise.documents : [];
4876  $('#policyEnterpriseDashboard').classList.remove('hidden');
4877  $('#currentPolicyEnterpriseName').textContent = enterprise.name;
4878  $('#currentPolicyEnterpriseMeta').textContent = `${enterprise.region || '区域待补充'} · ${enterprise.industry || '行业待补充'} · ${enterprise.certification || '资质待补充'}`;
4879  $('#currentEnterpriseDocumentCount').textContent = `${Number(enterprise.document_count || documents.length)}份`;
4880  $('#currentEnterpriseCharacters').textContent = `${Number(enterprise.parsed_characters || 0).toLocaleString()}字`;
4881  $('#currentEnterpriseConfidence').textContent = ai.confidence || (documents.length ? '待重新分析' : '待上传资料');
4882  $('#currentEnterpriseSummary').textContent = ai.company_summary || '上传营业执照、资质、审计、知识产权、人员和项目材料后,系统会形成企业全景画像。';
4883  const capabilities = [...(ai.capabilities || []), ...(ai.qualifications || [])].slice(0,12);
4884  $('#currentEnterpriseCapabilities').innerHTML = capabilities.length ? capabilities.map(item=>`<b>${escapeHtml(item)}</b>`).join('') : '<span>尚未形成能力与资质标签</span>';
4885  const gaps = Array.isArray(ai.missing_information) ? ai.missing_information : [];
4886  $('#currentEnterpriseGaps').innerHTML = gaps.length ? gaps.slice(0,10).map(item=>`<span>${escapeHtml(item)}</span>`).join('') : '<span>尚未识别明确缺口,仍需人工检查资料完整性</span>';
4887  $('#currentEnterpriseDocumentList').innerHTML = documents.length ? documents.map(item=>`<span title="已解析 ${Number(item.characters || 0).toLocaleString()} 字">${escapeHtml(item.name)}</span>`).join('') : '<span>尚未上传资料</span>';
4888  $('#policyMaterialCount').textContent = `${documents.length} 份企业资料`;
4889  $('#policyMaterialLedger').classList.toggle('hidden', !documents.length);
4890  $('#policyMaterialList').innerHTML = documents.map(item=>`<span class="material-file">${escapeHtml(item.name)}</span>`).join('');
4891  applyEnterpriseProfileToForm(enterprise);
4892  renderPolicyMatchContext();
4893}
4894
4895async function loadPolicyEnterprise(enterpriseId) {
4896  if (!enterpriseId) return;
4897  $('#policyEnterpriseSelect').value = enterpriseId;
4898  if ($('#policyMatchEnterpriseSelect')) $('#policyMatchEnterpriseSelect').value = enterpriseId;
4899  const enterprise = await policyApiRequest(`/api/policy/enterprises/${encodeURIComponent(enterpriseId)}`);
4900  renderPolicyEnterprise(enterprise);
4901  loadPolicyOpportunities();
4902}
4903
4904async function loadPolicyEnterprises(force = false) {
4905  if (policyEnterprisesLoaded && !force) return;
4906  try {
4907    policyEnterprises = await policyApiRequest('/api/policy/enterprises');
4908    policyEnterprisesLoaded = true;
4909    $('#policyEnterpriseArchiveStatus').textContent = `${policyEnterprises.length} 家企业档案`;
4910    $('#policyEnterpriseSelect').innerHTML = policyEnterprises.length ? policyEnterprises.map(item=>`<option value="${escapeHtml(item.id)}">${escapeHtml(item.name)} · ${Number(item.document_count || 0)}份资料</option>`).join('') : '<option value="">尚未创建企业档案</option>';
4911    if ($('#policyMatchEnterpriseSelect')) $('#policyMatchEnterpriseSelect').innerHTML = `<option value="">请选择企业档案</option>${policyEnterprises.map(item=>`<option value="${escapeHtml(item.id)}">${escapeHtml(item.name)} · ${Number(item.document_count || 0)}份资料</option>`).join('')}`;
4912    const preferred = currentPolicyEnterprise && policyEnterprises.some(item=>item.id===currentPolicyEnterprise.id) ? currentPolicyEnterprise.id : policyEnterprises[0]?.id;
4913    if (preferred) await loadPolicyEnterprise(preferred);
4914  } catch (error) {
4915    $('#policyEnterpriseArchiveStatus').textContent = '企业档案服务未连接';
4916    showToast(error.message);
4917  }
4918}
4919
4920async function createPolicyEnterprise() {
4921  const name = $('#policyEnterpriseName').value.trim();
4922  if (!name) { $('#policyEnterpriseName').focus(); showToast('请填写企业全称'); return; }
4923  const button = $('#createPolicyEnterpriseButton');
4924  button.disabled = true;
4925  try {
4926    const enterpriseQuals = collectQualifications('policyEnterpriseQualGroup','policyEnterpriseCertificationOther');
4927    const enterprise = await policyApiRequest('/api/policy/enterprises', {
4928      method:'POST',
4929      headers:{'Content-Type':'application/json'},
4930      body:JSON.stringify({
4931        name,
4932        region:$('#policyEnterpriseRegion').value.trim(),
4933        industry:$('#policyEnterpriseIndustry').value,
4934        certification:enterpriseQuals.join('、'),
4935        stage:'研发与申报准备',
4936        profile:{rd_spend_wan:0,intellectual_property_count:0,incubation_area_sqm:0,current_incubated_companies:0,cumulative_incubated_companies:0,profile_version:'企业档案初始版本',qualifications:enterpriseQuals}
4937      })
4938    });
4939    currentPolicyEnterprise = enterprise;
4940    $('#policyEnterpriseEditor').classList.add('hidden');
4941    $('#policyEnterpriseName').value = '';
4942    resetQualificationTags('policyEnterpriseQualGroup','policyEnterpriseCertificationOther');
4943    policyEnterprisesLoaded = false;
4944    await loadPolicyEnterprises(true);
4945    await loadPolicyEnterprise(enterprise.id);
4946    showToast(`已创建“${enterprise.name}”独立企业档案`);
4947  } catch (error) {
4948    showToast(error.message);
4949  } finally {
4950    button.disabled = false;
4951  }
4952}
4953
4954async function saveCurrentPolicyEnterpriseProfile(silent = false) {
4955  if (!currentPolicyEnterprise) { if (!silent) showToast('请先选择企业档案'); return null; }
4956  try {
4957    const updated = await policyApiRequest(`/api/policy/enterprises/${encodeURIComponent(currentPolicyEnterprise.id)}`, {
4958      method:'PUT',
4959      headers:{'Content-Type':'application/json'},
4960      body:JSON.stringify({name:currentPolicyEnterprise.name,profile:buildPolicyProfilePayload()})
4961    });
4962    renderPolicyEnterprise(updated);
4963    policyEnterprisesLoaded = false;
4964    if (!silent) showToast('当前企业画像已保存,后续政策匹配将使用这套数据');
4965    return updated;
4966  } catch (error) {
4967    if (!silent) showToast(error.message);
4968    throw error;
4969  }
4970}
4971
4972async function reanalyzePolicyEnterprise() {
4973  if (!currentPolicyEnterprise) { showToast('请先选择企业档案'); return; }
4974  const status = $('#enterpriseUploadStatus');
4975  status.className = 'enterprise-upload-status';
4976  status.textContent = '正在用DeepSeek重新阅读该企业的全部资料并更新全景画像…';
4977  try {
4978    const token = localStorage.getItem('token');
4979    const headers = token ? {Authorization:`Bearer ${token}`} : {};
4980    // 异步任务模式:避免 AI 阅读多份资料超过 60 秒被网络设备掐断
4981    const createResp = await fetch(`${BP_API_BASE}/api/policy/enterprises/${encodeURIComponent(currentPolicyEnterprise.id)}/analyze-async`, {method:'POST', headers});
4982    const createPayload = await createResp.json().catch(() => ({}));
4983    if (!createResp.ok) throw new Error(createPayload.detail || '画像更新任务创建失败');
4984    const result = await pollAsyncJob(createPayload.data.job_id, {
4985      onProgress: (elapsed) => { status.textContent = `AI 正在逐份阅读企业资料,已用时 ${elapsed} 秒…`; }
4986    });
4987    const enterprise = result.data;
4988    renderPolicyEnterprise(enterprise);
4989    status.className = 'enterprise-upload-status complete';
4990    status.textContent = '企业全景画像已更新,后续政策匹配会使用新的能力、资质和资料缺口。';
4991  } catch (error) {
4992    status.className = 'enterprise-upload-status error';
4993    status.textContent = error.message;
4994  }
4995}
4996
4997async function updatePolicyMaterials(files) {
4998  const selectedFiles = [...files];
4999  if (!selectedFiles.length) return;
5000  const status = $('#enterpriseUploadStatus');
5001  const uploadLabel = $('#policyEnterpriseUploadFileName');
5002  const uploadZone = $('label[for="policyMaterialsInput"]');
5003  const queue = $('#enterpriseUploadQueue');
5004  const queueFiles = $('#enterpriseUploadQueueFiles');
5005  const total = selectedFiles.reduce((sum,file)=>sum+file.size,0);
5006  const sizeText = total > 1024*1024 ? `${(total/1024/1024).toFixed(1)}MB` : `${Math.max(1,Math.round(total/1024))}KB`;
5007  uploadLabel.textContent = `${selectedFiles.length}份 · ${sizeText}`;
5008  $('#enterpriseUploadQueueTitle').textContent = `本次已选择 ${selectedFiles.length} 份资料`;
5009  queueFiles.innerHTML = selectedFiles.map(file=>`<span class="material-file">${escapeHtml(file.name)}</span>`).join('');
5010  queue.classList.remove('hidden');
5011  if (!currentPolicyEnterprise) {
5012    status.className = 'enterprise-upload-status error';
5013    status.textContent = '文件已选择,但当前还没有绑定企业。请先在上方创建或选择企业档案,再重新选择文件。';
5014    uploadLabel.textContent = '请先选择企业';
5015    showToast('请先创建或选择企业档案,再上传企业资料');
5016    return;
5017  }
5018  const allowed = new Set(['pdf','doc','docx','ppt','pptx','xlsx','txt']);
5019  const unsupported = selectedFiles.filter(file=>!allowed.has((file.name.split('.').pop() || '').toLowerCase()));
5020  if (unsupported.length) {
5021    status.className = 'enterprise-upload-status error';
5022    status.textContent = `暂不支持:${unsupported.map(file=>file.name).join('、')}。请上传PDF、Word、PPT、XLSX或TXT。`;
5023    uploadLabel.textContent = '文件格式不支持';
5024    return;
5025  }
5026  if (selectedFiles.some(file=>file.size > 50*1024*1024) || total > 55*1024*1024) {
5027    status.className = 'enterprise-upload-status error';
5028    status.textContent = '单个文件不能超过50MB,本次合计不能超过55MB,请分批上传。';
5029    uploadLabel.textContent = '文件过大,请分批';
5030    showToast('企业资料过大,请分批上传');
5031    return;
5032  }
5033  const form = new FormData();
5034  selectedFiles.forEach(file=>form.append('files',file));
5035  status.className = 'enterprise-upload-status';
5036  status.textContent = `正在把 ${selectedFiles.length} 份资料加入“${currentPolicyEnterprise.name}”,提取全文并更新企业画像…`;
5037  uploadZone.classList.add('uploading');
5038  uploadZone.setAttribute('aria-busy','true');
5039  const beforeCount = Number(currentPolicyEnterprise.document_count || (currentPolicyEnterprise.documents || []).length || 0);
5040  try {
5041    const result = await policyApiRequest(`/api/policy/enterprises/${encodeURIComponent(currentPolicyEnterprise.id)}/documents`, {method:'POST',body:form});
5042    renderPolicyEnterprise(result.enterprise);
5043    status.className = result.analysis_error ? 'enterprise-upload-status error' : 'enterprise-upload-status complete';
5044    status.textContent = result.analysis_error ? `资料已保存,但AI画像更新失败:${result.analysis_error}` : `已保存并解析 ${result.uploaded.length} 份资料,企业全景画像已更新。`;
5045    uploadLabel.textContent = result.analysis_error ? '资料已保存 · AI待重试' : `已完成 ${result.uploaded.length} 份`;
5046    $('#enterpriseUploadQueueTitle').textContent = result.analysis_error ? '本次资料已保存' : '本次上传与解析已完成';
5047    policyEnterprisesLoaded = false;
5048  } catch (error) {
5049    // 大文件全文提取+画像分析耗时长,网络中断不代表失败:先回查档案再下结论
5050    let recovered = false;
5051    try {
5052      const fresh = await policyApiRequest(`/api/policy/enterprises/${encodeURIComponent(currentPolicyEnterprise.id)}`);
5053      const freshEnterprise = fresh && fresh.enterprise ? fresh.enterprise : fresh;
5054      const afterCount = Number(freshEnterprise.document_count || (freshEnterprise.documents || []).length || 0);
5055      if (afterCount >= beforeCount + selectedFiles.length) {
5056        renderPolicyEnterprise(freshEnterprise);
5057        status.className = 'enterprise-upload-status complete';
5058        status.textContent = `${selectedFiles.length} 份资料已保存(服务器提取耗时较长,页面曾短暂无响应,实际已上传成功)。`;
5059        uploadLabel.textContent = `已完成 ${selectedFiles.length} 份`;
5060        $('#enterpriseUploadQueueTitle').textContent = '本次资料已保存';
5061        policyEnterprisesLoaded = false;
5062        recovered = true;
5063      }
5064    } catch (_) {}
5065    if (!recovered) {
5066      status.className = 'enterprise-upload-status error';
5067      status.textContent = error.message;
5068      uploadLabel.textContent = '上传失败 · 点击重试';
5069    }
5070  } finally {
5071    uploadZone.classList.remove('uploading');
5072    uploadZone.removeAttribute('aria-busy');
5073  }
5074}
5075
5076function updatePolicyGuideFiles(files) {
5077  policyGuideFiles = [...files];
5078  $('#policyGuideFileName').textContent = policyGuideFiles.length ? `${policyGuideFiles.length} 份文件` : '选择文件';
5079  $('#policyGuideLedger').classList.toggle('hidden', !policyGuideFiles.length);
5080  $('#policyGuideList').innerHTML = policyGuideFiles.map(file => `<span class="material-file">${escapeHtml(file.name)}</span>`).join('');
5081}
5082
5083function updatePolicyTemplate(file) {
5084  policyTemplateFile = file || null;
5085  $('#policyTemplateFileName').textContent = policyTemplateFile ? policyTemplateFile.name : '选择模板';
5086}
5087
5088function buildPolicyProfilePayload() {
5089  const profileQualifications = collectQualifications('policyQualGroup','policyCertificationOther');
5090  return {
5091    profile_version: `${currentPolicyEnterprise?.name || '当前企业'} · 企业档案当前版本`,
5092    profile_source: '企业档案中心:用户维护的基本信息与本企业独立资料库',
5093    region: $('#policyRegion').value,
5094    industry: $('#policyIndustry').value,
5095    certification: profileQualifications.join('、'),
5096    qualifications: profileQualifications,
5097    stage: $('#policyStage').value,
5098    rd_spend_wan: Number($('#policyRdSpend').value || 0),
5099    intellectual_property_count: Number($('#policyPatents').value || 0),
5100    incubation_area_sqm: Number($('#policyArea').value || 0),
5101    current_incubated_companies: Number($('#policyIncubatedCompanies').value || 0),
5102    cumulative_incubated_companies: Number($('#policyServedCompanies').value || 0),
5103  };
5104}
5105
5106function policyStatusClass(status) {
5107  if (String(status).includes('不满足')) return 'failed';
5108  if (String(status).includes('满足') && !String(status).includes('不满足')) return 'passed';
5109  return 'unknown';
5110}
5111
5112function highlightDraftPending(escapedHtml) {
5113  return escapedHtml
5114    .replace(/(【待填[^】]*】)/g, '<span class="draft-pending">$1</span>')
5115    .replace(/(\[待补充[^\]]*\])/g, '<span class="draft-pending">$1</span>');
5116}
5117
5118function renderProposalDraft(result) {
5119  const proposal = result?.proposal || {};
5120  const sections = Array.isArray(proposal.sections) ? proposal.sections : [];
5121  const handoffChecklist = Array.isArray(result?.handoff_checklist) ? result.handoff_checklist : [];
5122  const handoffHtml = handoffChecklist.length ? `<div class="proposal-handoff"><strong>待核实事项交接清单</strong><p>按重要度排序;核实完成后,草案中的<span class="draft-pending">【待填】</span>项即可落定</p><ol>${handoffChecklist.map(item=>`<li class="priority-${escapeHtml(item.priority)}"><b>${escapeHtml(item.priority)}</b><span>${escapeHtml(item.item)}<small>核实途径:${escapeHtml(item.how_to_verify)}</small></span></li>`).join('')}</ol></div>` : '';
5123  const exportHtml = currentPolicyApplicationId ? `<div class="proposal-export-bar"><button class="analyze-button" id="exportPolicyProposalButton2">导出 Word 申报书草案(黑/红/黄分色)↓</button><span>红色【待填】为待核实内容,黄色为交接清单;导出后可直接编辑打印</span></div>` : '';
5124  $('#proposalWorkbenchContent').innerHTML = `<div class="proposal-workbench-content"><div class="proposal-draft-meta"><strong>${proposal.template_detected ? '已按上传模板识别章节' : '未上传模板 · 采用通用申报结构'}</strong><span>${escapeHtml(proposal.writing_boundary || '缺失内容必须保留待补充标记')}</span></div><div class="proposal-draft-sections">${sections.map((section,index)=>`<details class="proposal-draft-section" ${index===0?'open':''}><summary><span>${escapeHtml(section.number || String(index+1).padStart(2,'0'))}</span><strong>${escapeHtml(section.title || `章节${index+1}`)}</strong><i>展开正文</i></summary><div><p>${highlightDraftPending(escapeHtml(section.draft || '【待填:尚无足够材料形成正文】')).replace(/\n/g,'<br>')}</p><footer><span><b>已用证据</b>${(section.evidence_used || []).length ? section.evidence_used.map(item=>`<em>${escapeHtml(item)}</em>`).join('') : '<em>暂无已绑定证据</em>'}</span><span class="missing"><b>待补材料</b>${(section.missing_evidence || []).length ? section.missing_evidence.map(item=>`<em>${escapeHtml(item)}</em>`).join('') : '<em>无模型识别缺口,仍需人工复核</em>'}</span></footer></div></details>`).join('') || '<p>没有生成可用章节,请补充项目书模板后重新分析。</p>'}</div>${exportHtml}${handoffHtml}</div>`;
5125  const exportButton = $('#exportPolicyProposalButton2');
5126  if (exportButton) exportButton.addEventListener('click', exportPolicyProposalDocx);
5127  $('#proposalWorkbench').classList.remove('hidden');
5128}
5129
5130function renderPolicyAnalysis(result) {
5131  policyAnalysisResult = result;
5132  currentPolicyApplicationId = result.application_id || null;
5133  const overview = result.policy_overview || {};
5134  const eligibility = result.eligibility || {};
5135  const requirements = Array.isArray(result.hard_requirements) ? result.hard_requirements : [];
5136  const scoring = Array.isArray(result.scoring_items) ? result.scoring_items : [];
5137  const requiredMaterials = Array.isArray(result.required_materials) ? result.required_materials : [];
5138  const gaps = Array.isArray(result.material_gaps) ? result.material_gaps : [];
5139  const audit = result.analysis_audit || {};
5140  const trace = result.document_trace || {};
5141  const enterpriseName = trace.enterprise_name || currentPolicyEnterprise?.name || '当前企业';
5142  const matched = Array.isArray(eligibility.matched) ? eligibility.matched : [];
5143  const unmatched = Array.isArray(eligibility.unmatched) ? eligibility.unmatched : [];
5144  const unknown = Array.isArray(eligibility.unknown) ? eligibility.unknown : [];
5145  const conditionTotal = matched.length + unmatched.length + unknown.length;
5146  const hardRequirementRate = conditionTotal ? Math.round(matched.length / conditionTotal * 100) : null;
5147  selectedPolicyCandidate = {name:overview.title || '上传的政策申报项目', score:hardRequirementRate};
5148  $('#policyMatchResults').innerHTML = `<section class="policy-analysis-overview panel"><div class="policy-analysis-head"><div><span>上传原文分析 · ${audit.ai_called ? 'DeepSeek AI已调用' : '未调用大模型'}</span><h2>${escapeHtml(overview.title || '政策项目名称待确认')}</h2><p>${escapeHtml(eligibility.conclusion || '需核对硬性条件与证明材料')}</p></div><strong>${Number(eligibility.match_score || 0)}<small>/100</small></strong></div><div class="policy-analysis-summary"><article><small>发布单位</small><strong>${escapeHtml(overview.issuing_authority)}</strong></article><article><small>文号</small><strong>${escapeHtml(overview.policy_number)}</strong></article><article><small>申报截止</small><strong>${escapeHtml(overview.deadline)}</strong></article><article><small>支持金额</small><strong>${escapeHtml(overview.support_amount)}</strong></article></div><div class="policy-source-audit"><b>${trace.officially_verified ? '官网已核验' : '用户上传 · 官网未自动核验'}</b><span>${escapeHtml(overview.source_boundary || '')}</span>${trace.source_url ? `<a href="${escapeHtml(trace.source_url)}" target="_blank" rel="noopener">打开填写的官方原文网址 ↗</a>` : ''}</div></section><section class="policy-analysis-detail panel"><div class="development-panel-head"><div><strong>硬性条件逐项匹配</strong><p>每一项都显示政策原文、华卫状态和所需证明</p></div><span>${requirements.length} 项条件</span></div><div class="policy-requirement-list">${requirements.map((item,index)=>`<article class="${policyStatusClass(item.status)}"><i>${index+1}</i><div><strong>${escapeHtml(item.requirement)}</strong><blockquote>${escapeHtml(item.source_quote)}</blockquote><p>证明要求:${escapeHtml(item.evidence)}</p></div><b>${escapeHtml(item.status)}</b></article>`).join('') || '<p>未提取到明确硬门槛,请检查通知是否包含申报条件章节。</p>'}</div></section><div class="policy-analysis-columns"><section class="panel"><strong>评分项与得分策略</strong><div class="policy-scoring-list">${scoring.map(item=>`<article><span><b>${escapeHtml(item.item)}</b><em>${escapeHtml(item.points)}</em></span><p>${escapeHtml(item.strategy)}</p><small>所需证据:${escapeHtml(item.evidence_needed)}</small></article>`).join('') || '<p>通知中未识别评分表,建议补充实施细则或评分附件。</p>'}</div></section><section class="panel"><strong>申报材料与缺口</strong><div class="policy-material-columns"><div><b>要求提交</b>${requiredMaterials.map(item=>`<span>✓ ${escapeHtml(item)}</span>`).join('') || '<span>未识别到材料清单</span>'}</div><div class="missing"><b>当前待补</b>${gaps.map(item=>`<span>! ${escapeHtml(item)}</span>`).join('') || '<span>未识别缺口,仍需人工核对</span>'}</div></div></section></div><p class="policy-ai-audit">调用记录:${escapeHtml(audit.model || '未调用大模型')} · 已解析通知/指南 ${Number(audit.parsed_guide_characters || trace.pasted_characters || 0).toLocaleString()} 字 · 企业材料 ${Number(audit.material_documents || (trace.material_files || []).length)} 份。AI结论是申报研究草案,不等于政策有效性或最终资格认定。</p>`;
5149  const resultRoot = $('#policyMatchResults');
5150  const rateDisplay = $('.policy-analysis-head > strong', resultRoot);
5151  if (rateDisplay) rateDisplay.innerHTML = `${hardRequirementRate === null ? '—' : hardRequirementRate}<small>硬条件满足率</small>`;
5152  $('.policy-analysis-head span', resultRoot).textContent = `${enterpriseName} × ${currentPolicyCatalog?.title || trace.policy_title || '所选政策'} · ${audit.ai_called ? 'DeepSeek AI已调用' : '未调用大模型'}`;
5153  $('.policy-analysis-detail .development-panel-head p', resultRoot).textContent = `逐项显示“${enterpriseName}”满足、不满足或待核验的条件与证明材料`;
5154  $('.policy-analysis-overview', resultRoot).insertAdjacentHTML('beforeend', `<div class="enterprise-match-breakdown"><article class="matched"><strong>已满足 ${matched.length} 项</strong>${matched.map(item=>`<span>✓ ${escapeHtml(item)}</span>`).join('') || '<span>暂无已确认满足项</span>'}</article><article class="unmatched"><strong>不符合 ${unmatched.length} 项</strong>${unmatched.map(item=>`<span>× ${escapeHtml(item)}</span>`).join('') || '<span>暂无明确不符合项</span>'}</article><article class="unknown"><strong>待核验 ${unknown.length} 项</strong>${unknown.map(item=>`<span>? ${escapeHtml(item)}</span>`).join('') || '<span>暂无模型识别的待核验项</span>'}</article></div>`);
5155  appendRuleBasis(resultRoot, [`满足 ${matched.length} 项`, `不满足 ${unmatched.length} 项`, `待核验 ${unknown.length} 项`], '硬条件满足率按“满足项 ÷(满足+不满足+待核验)”计算;待核验项不计作满足。条件本身由 AI 从原文提取,仍须人工逐条核对。');
5156
5157  const factLayers = result.fact_layers || {};
5158  const stableFacts = Array.isArray(factLayers.stable) ? factLayers.stable : [];
5159  const timeSensitiveFacts = Array.isArray(factLayers.time_sensitive) ? factLayers.time_sensitive : [];
5160  const missingFacts = Array.isArray(factLayers.missing) ? factLayers.missing : [];
5161  const conflicts = Array.isArray(result.consistency_check?.conflicts) ? result.consistency_check.conflicts : [];
5162  const handoffChecklist = Array.isArray(result.handoff_checklist) ? result.handoff_checklist : [];
5163  resultRoot.insertAdjacentHTML('beforeend', `<section class="panel policy-fact-layers-panel"><div class="development-panel-head"><div><strong>事实分层清单</strong><p>稳定事实直接复用、时效事实更新为本年度口径、缺失事实按提示补齐——${escapeHtml(factLayers.boundary || '缺失项不编造')}</p></div><span>三读材料</span></div><div class="policy-fact-layers"><article class="fact-layer stable"><strong>✓ 稳定事实 · 可直接复用 ${stableFacts.length} 项</strong>${stableFacts.map(item=>`<span>${escapeHtml(item)}</span>`).join('') || '<span>未识别到明确稳定事实</span>'}</article><article class="fact-layer time-sensitive"><strong>↻ 时效事实 · 必须更新 ${timeSensitiveFacts.length} 项</strong>${timeSensitiveFacts.map(item=>`<span>${escapeHtml(item)}</span>`).join('') || '<span>未识别到时效事实</span>'}</article><article class="fact-layer missing"><strong>!缺失事实 · 待补齐 ${missingFacts.length} 项</strong>${missingFacts.map(item=>`<span class="missing-fact"><b>${escapeHtml(item.item || '')}</b>${escapeHtml(item.hint || '')}${item.reference_value ? `<em>参考:${escapeHtml(item.reference_value)}</em>` : ''}</span>`).join('') || '<span>暂无模型识别的缺失事实</span>'}</article></div></section>`);
5164  if (conflicts.length) {
5165    resultRoot.insertAdjacentHTML('beforeend', `<section class="panel policy-consistency-panel"><div class="development-panel-head"><div><strong>跨文档口径核对 · 发现 ${conflicts.length} 处矛盾</strong><p>同一事实在不同材料中表述不一致;系统不自动取值,须人工统一口径后再定稿</p></div><span>需人工裁决</span></div><div class="policy-conflict-list">${conflicts.map((item,index)=>`<article class="policy-conflict-card"><header><i>${index+1}</i><strong>${escapeHtml(item.fact)}</strong></header><div class="conflict-values">${(item.values || []).map(v=>`<span><b>${escapeHtml(v.value)}</b><small>${escapeHtml(v.source)}</small></span>`).join('')}</div><p>建议:${escapeHtml(item.suggestion)}</p></article>`).join('')}</div></section>`);
5166  }
5167  if (handoffChecklist.length) {
5168    const priorityOrder = {'高':0,'中':1,'低':2};
5169    const sortedChecklist = [...handoffChecklist].sort((a,b)=>(priorityOrder[a.priority] ?? 1) - (priorityOrder[b.priority] ?? 1));
5170    resultRoot.insertAdjacentHTML('beforeend', `<section class="panel policy-handoff-panel"><div class="development-panel-head"><div><strong>待核实事项交接清单</strong><p>按重要度排序;完成这些核实后,申报书草案中的【待填】项才能落定</p></div><span>${handoffChecklist.length} 项</span></div><ol class="policy-handoff-list">${sortedChecklist.map(item=>`<li class="priority-${escapeHtml(item.priority)}"><b>${escapeHtml(item.priority)}</b><span><strong>${escapeHtml(item.item)}</strong><small>核实途径:${escapeHtml(item.how_to_verify)}</small></span></li>`).join('')}</ol></section>`);
5171  }
5172  $('#policyMatchResults').classList.remove('hidden');
5173  $('#proposalWorkbench').classList.add('hidden');
5174  $('#policyDecisionGate').classList.remove('hidden');
5175  $('#policyDecisionScore').textContent = `${hardRequirementRate === null ? '未计算' : `${hardRequirementRate}%硬条件满足率`} · ${unmatched.length}项不符合 · ${unknown.length}项待核验`;
5176  $('#policyApplicationProjectName').value = overview.title || currentPolicyCatalog?.title || '';
5177  $('#policyDecisionAdvice').textContent = hardRequirementRate !== null && hardRequirementRate >= 80 && unmatched.length === 0 && unknown.length === 0
5178    ? '硬条件全部或大部分已由材料支持;核验政策有效性和原始证明后,可进入项目书编写。'
5179    : hardRequirementRate !== null && hardRequirementRate >= 60 && unmatched.length === 0
5180      ? '具备一定申报基础,但必须先完成待核验项,不能只依据模型判断立项。'
5181      : '当前不建议直接申报;应先解决不符合项、补齐未知项并核验官方有效性。';
5182  renderPolicyWorkflowStep(4);
5183  $('#policyMatchResults').scrollIntoView({behavior:'smooth',block:'start'});
5184}
5185
5186async function runPolicyMatch() {
5187  if (!currentPolicyEnterprise) {
5188    showToast('请先创建或选择要申报的企业档案');
5189    return;
5190  }
5191  if (!currentPolicyCatalog) {
5192    showToast('请先从政策资料库选择一项已经解析的政策');
5193    renderPolicyWorkflowStep(2);
5194    return;
5195  }
5196  const form = new FormData();
5197  form.append('policy_id', currentPolicyCatalog.id);
5198  form.append('profile_json', JSON.stringify(buildPolicyProfilePayload()));
5199  form.append('enterprise_id', currentPolicyEnterprise.id);
5200  const materialRoles = {};
5201  policyMatchMaterialFiles.forEach(entry => {
5202    form.append('materials', entry.file, entry.file.name);
5203    materialRoles[entry.file.name] = entry.role;
5204  });
5205  if (policyMatchMaterialFiles.length) form.append('material_roles_json', JSON.stringify(materialRoles));
5206  const button = $('#runPolicyMatch');
5207  const status = $('#policyAnalysisStatus');
5208  const matchStatus = $('#policyMatchRunStatus');
5209  button.disabled = true;
5210  button.textContent = '正在对照企业档案与政策要求…';
5211  status.classList.remove('hidden','error','complete');
5212  matchStatus.classList.remove('hidden','error','complete');
5213  $('#policyAnalysisStatusTitle').textContent = '正在进行企业与政策匹配';
5214  $('#policyAnalysisStatusDetail').textContent = '逐项核对硬门槛、评分项、证明材料和企业资料,通常需要30–90秒';
5215  try {
5216    await saveCurrentPolicyEnterpriseProfile(true);
5217    const token = localStorage.getItem('token');
5218    const headers = token ? {Authorization:`Bearer ${token}`} : {};
5219    // 异步任务模式:提交后立即返回 job_id,再短轮询取结果。
5220    // 避免 30–90 秒以上的长连接被用户侧网络设备掐断(Failed to fetch)。
5221    const response = await fetch(`${BP_API_BASE}/api/policy/analyze-async`, {method:'POST',headers,body:form});
5222    const createPayload = await response.json().catch(() => ({}));
5223    if (!response.ok) throw new Error(createPayload.detail || '政策匹配任务创建失败');
5224    const payload = await pollAsyncJob(createPayload.data.job_id, {
5225      onProgress: (elapsed) => { $('#policyAnalysisStatusDetail').textContent = `AI 正在逐项核对政策条款与企业材料,已用时 ${elapsed} 秒…`; }
5226    });
5227    status.classList.add('complete');
5228    matchStatus.classList.add('complete');
5229    $('strong',matchStatus).textContent = '企业与政策匹配完成';
5230    $('small',matchStatus).textContent = '结果已进入人工确认阶段';
5231    $('#policyAnalysisStatusTitle').textContent = payload.data.analysis_audit?.ai_called ? '企业与政策匹配已完成' : '已完成规则匹配,但本次未调用大模型';
5232    $('#policyAnalysisStatusDetail').textContent = '请查看匹配分、不符合项和待核验项,再由负责人决定是否正式申报';
5233    renderPolicyAnalysis(payload.data);
5234    loadPolicyMatchHistory();
5235    showToast('匹配分析已完成,请进行人工立项确认');
5236  } catch (error) {
5237    status.classList.add('error');
5238    matchStatus.classList.add('error');
5239    $('strong',matchStatus).textContent = '匹配未完成';
5240    $('small',matchStatus).textContent = error.message;
5241    $('#policyAnalysisStatusTitle').textContent = '政策材料分析未完成';
5242    $('#policyAnalysisStatusDetail').textContent = error.message;
5243    showToast(error.message);
5244  } finally {
5245    button.disabled = false;
5246    button.textContent = '重新进行企业与政策匹配 →';
5247  }
5248}
5249
5250// ============ 历史匹配记录 ============
5251
5252async function loadPolicyMatchHistory() {
5253  const list = $('#policyMatchHistoryList');
5254  if (!list) return;
5255  try {
5256    const items = await policyApiRequest('/api/policy/matches');
5257    renderPolicyMatchHistory(Array.isArray(items) ? items : []);
5258  } catch (error) {
5259    list.innerHTML = '<p>匹配记录读取失败,请稍后刷新重试。</p>';
5260  }
5261}
5262
5263function renderPolicyMatchHistory(items) {
5264  const list = $('#policyMatchHistoryList');
5265  const count = $('#policyMatchHistoryCount');
5266  if (!list) return;
5267  count.textContent = `${items.length} 条记录`;
5268  if (!items.length) {
5269    list.innerHTML = '<p>暂无匹配记录;完成一次"选择与匹配"后会自动出现在这里。</p>';
5270    return;
5271  }
5272  list.innerHTML = `<table class="policy-match-history-table"><thead><tr><th>时间</th><th>企业</th><th>政策项目</th><th>匹配分</th><th>状态</th><th>操作</th></tr></thead><tbody>${items.map(item => {
5273    const time = String(item.updated_at || '').slice(0, 16).replace('T', ' ');
5274    const confirmed = String(item.status).includes('已确认');
5275    return `<tr><td>${escapeHtml(time)}</td><td>${escapeHtml(item.enterprise_name || '—')}</td><td>${escapeHtml(item.project_name || item.policy_title || '—')}</td><td><b class="${Number(item.match_score) >= 60 ? 'score-high' : 'score-low'}">${Number(item.match_score || 0)}</b></td><td>
5275<span class="match-status ${confirmed ? 'confirmed' : 'pending'}">${escapeHtml(item.status)}</span></td><td><button class="outline-button" data-match-view="${escapeHtml(item.id)}">查看</button>${confirmed ? `<button class="outline-button" data-match-export="${escapeHtml(item.id)}">导出申报书</button>` : ''}</td></tr>`;
5276  }).join('')}</tbody></table>`;
5277  $$('[data-match-view]', list).forEach(btn => btn.addEventListener('click', () => viewPolicyMatchRecord(btn.dataset.matchView)));
5278  $$('[data-match-export]', list).forEach(btn => btn.addEventListener('click', () => exportPolicyMatchRecord(btn.dataset.matchExport)));
5279}
5280
5281async function viewPolicyMatchRecord(matchId) {
5282  try {
5283    const match = await policyApiRequest(`/api/policy/matches/${encodeURIComponent(matchId)}`);
5284    const result = match.result || {};
5285    result.application_id = match.id;
5286    renderPolicyAnalysis(result);
5287    showToast(`已载入 ${match.enterprise_name || '该企业'} 的历史匹配结果`);
5288  } catch (error) {
5289    showToast(error.message);
5290  }
5291}
5292
5293async function exportPolicyMatchRecord(matchId) {
5294  const token = localStorage.getItem('token');
5295  const headers = token ? {Authorization:`Bearer ${token}`} : {};
5296  showToast('正在生成 Word 申报书草案…');
5297  try {
5298    const response = await fetch(`${BP_API_BASE}/api/policy/matches/${encodeURIComponent(matchId)}/export-async`, {method:'POST', headers});
5299    const createPayload = await response.json().catch(() => ({}));
5300    if (!response.ok) throw new Error(createPayload.detail || '导出任务创建失败');
5301    const result = await pollAsyncJob(createPayload.data.job_id);
5302    const dlResp = await fetch(`${BP_API_BASE}/api/analysis-jobs/${createPayload.data.job_id}/download`, {headers});
5303    if (!dlResp.ok) throw new Error('申报书下载失败');
5304    const blob = await dlResp.blob();
5305    const link = document.createElement('a');
5306    link.href = URL.createObjectURL(blob);
5307    link.download = (result && result.filename) || '申报书草案.docx';
5308    link.click();
5309    URL.revokeObjectURL(link.href);
5310  } catch (error) {
5311    showToast(error.message);
5312  }
5313}
5314
5315
5316
5317function renderPolicyOpportunities() {
5318  const meta = $('#policyOpportunityMeta');
5319  const hint = $('#policyOpportunityHint');
5320  const container = $('#policyOpportunityResults');
5321  if (!container) return;
5322  if (!currentPolicyEnterprise) {
5323    policyOpportunityScan = null;
5324    meta.textContent = '尚未扫描';
5325    hint.textContent = '请先选择企业档案';
5326    container.innerHTML = '';
5327    return;
5328  }
5329  if (!policyOpportunityScan || !Array.isArray(policyOpportunityScan.opportunities)) {
5330    meta.textContent = '尚未扫描';
5331    hint.textContent = `点击按钮,为“${currentPolicyEnterprise.name}”扫描政策库全部有效政策`;
5332    container.innerHTML = '';
5333    return;
5334  }
5335  const summary = policyOpportunityScan.summary || {};
5336  const scannedAt = String(policyOpportunityScan.scanned_at || '').slice(0, 10);
5337  meta.textContent = `${policyOpportunityScan.policy_count || 0}项政策 · ${scannedAt}扫描`;
5338  hint.textContent = `建议申报 ${summary['建议申报'] || 0} 项 · 待核验 ${summary['待核验后定'] || 0} 项 · 暂不建议 ${summary['暂不建议'] || 0} 项`;
5339  if (policyOpportunityScan.stale) hint.textContent += ' · ⚠ 政策库已有更新,建议重新扫描';
5340  const verdictClass = {'建议申报':'recommend','待核验后定':'verify','暂不建议':'skip'};
5341  const urgencyLabel = {'高':'≤30天截止','中':'≤90天截止','低':'时间宽裕'};
5342  container.innerHTML = `<div class="policy-opportunity-list">${policyOpportunityScan.opportunities.map(item => {
5343    const klass = verdictClass[item.verdict] || 'verify';
5344    return `<article class="policy-opportunity-card ${klass}" data-opportunity-policy-id="${escapeHtml(item.policy_id)}"><header><b class="verdict">${escapeHtml(item.verdict)}</b><strong>${escapeHtml(item.title)}</strong><i class="urgency-${escapeHtml(item.urgency)}">${escapeHtml(urgencyLabel[item.urgency] || '')}</i></header><p>${escapeHtml(item.fit_reason || '')}</p><footer><span>${escapeHtml(item.issuing_authority || '发布单位待确认')}</span><span>截止 ${escapeHtml(item.deadline || '未明确')}</span><span>${escapeHtml(item.support_amount || '金额以通知为准')}</span>${item.key_gap ? `<em>缺口:${escapeHtml(item.key_gap)}</em>` : ''}</footer></article>`;
5345  }).join('')}</div>
5345<p class="policy-opportunity-boundary">${escapeHtml(policyOpportunityScan.analysis_audit?.boundary || '初筛结果仅供发现机会,正式申报前请完成详细匹配与人工确认。')}</p>`;
5346}
5347
5348async function loadPolicyOpportunities() {
5349  if (!currentPolicyEnterprise) { renderPolicyOpportunities(); return; }
5350  try {
5351    const data = await policyApiRequest(`/api/policy/enterprises/${encodeURIComponent(currentPolicyEnterprise.id)}/opportunities`);
5352    policyOpportunityScan = data && Array.isArray(data.opportunities) ? data : null;
5353  } catch (error) {
5354    policyOpportunityScan = null;
5355  }
5356  renderPolicyOpportunities();
5357}
5358
5359async function scanPolicyOpportunities() {
5360  if (!currentPolicyEnterprise) { showToast('请先选择企业档案'); return; }
5361  const button = $('#scanPolicyOpportunitiesButton');
5362  const hint = $('#policyOpportunityHint');
5363  button.disabled = true;
5364  button.textContent = '正在扫描政策库…';
5365  try {
5366    const token = localStorage.getItem('token');
5367    const headers = token ? {Authorization:`Bearer ${token}`} : {};
5368    const response = await fetch(`${BP_API_BASE}/api/policy/enterprises/${encodeURIComponent(currentPolicyEnterprise.id)}/scan-opportunities-async`, {method:'POST', headers});
5369    const createPayload = await response.json().catch(() => ({}));
5370    if (!response.ok) throw new Error(createPayload.detail || '机会扫描任务创建失败');
5371    const result = await pollAsyncJob(createPayload.data.job_id, {
5372      onProgress: (elapsed) => { hint.textContent = `AI 正在批量初筛政策库,已用时 ${elapsed} 秒…`; }
5373    });
5374    policyOpportunityScan = result.data || result;
5375    renderPolicyOpportunities();
5376    const summary = policyOpportunityScan.summary || {};
5377    showToast(`扫描完成:建议申报 ${summary['建议申报'] || 0} 项,点击任意政策可带入详细匹配`);
5378  } catch (error) {
5379    hint.textContent = error.message;
5380    showToast(error.message);
5381  } finally {
5382    button.disabled = false;
5383    button.textContent = '重新扫描该企业可申报政策';
5384  }
5385}
5386
5387async function applyOpportunityToMatch(policyId) {
5388  if (!policyId) return;
5389  await selectPolicyCatalog(policyId);
5390  if (currentPolicyCatalog && currentPolicyCatalog.id === policyId) {
5391    renderPolicyWorkflowStep(3);
5392    showToast(`已带入「${currentPolicyCatalog.title}」,点击上方按钮开始详细匹配`);
5393    $('.policy-match-selection')?.scrollIntoView({behavior:'smooth', block:'start'});
5394  }
5395}
5396
5397async function exportPolicyProposalDocx() {
5398  if (!currentPolicyApplicationId) { showToast('请先完成匹配并生成申报记录(需从政策库选择政策后匹配)'); return; }
5399  const token = localStorage.getItem('token');
5400  const headers = token ? {Authorization:`Bearer ${token}`} : {};
5401  showToast('正在生成 Word 申报书草案…');
5402  try {
5403    // 异步任务模式:模板填充需调用大模型,可能超过 60 秒被网络设备掐断
5404    const createResp = await fetch(`${BP_API_BASE}/api/policy/matches/${encodeURIComponent(currentPolicyApplicationId)}/export-async`, {method:'POST', headers});
5405    const createPayload = await createResp.json().catch(() => ({}));
5406    if (!createResp.ok) throw new Error(createPayload.detail || '申报书生成任务创建失败');
5407    const jobId = createPayload.data.job_id;
5408    const result = await pollAsyncJob(jobId, {
5409      timeoutMs: 5*60*1000,
5410      onProgress: (elapsed) => { showToast(`AI 正在填充申报书内容,已用时 ${elapsed} 秒…`); }
5411    });
5412    const downloadResp = await fetch(`${BP_API_BASE}/api/analysis-jobs/${jobId}/download`, { headers });
5413    if (!downloadResp.ok) throw new Error('申报书下载失败,请重试');
5414    const blob = await downloadResp.blob();
5415    const url = URL.createObjectURL(blob);
5416    const link = document.createElement('a');
5417    link.href = url;
5418    link.download = (result && result.filename) || '申报书草案.docx';
5419    document.body.appendChild(link);
5420    link.click();
5421    link.remove();
5422    URL.revokeObjectURL(url);
5423    showToast('申报书草案已开始下载');
5424  } catch (error) {
5425    showToast(error.message);
5426  }
5427}
5428
5429function generateProposalDraft() {
5430  if (!policyAnalysisResult) { showToast('请先上传政策原文并完成AI分析'); return; }
5431  renderProposalDraft(policyAnalysisResult);
5432  $$('.proposal-draft-section').forEach(section => { section.open = true; });
5433  $('#proposalWorkbench').scrollIntoView({behavior:'smooth',block:'start'});
5434  showToast('项目书草案已展开;待补材料和证据来源均已标注');
5435}
5436
5437const croModules = {
5438  project:{ eyebrow:'01 · PROJECT CREATION', title:'项目创建向导', description:'建立完整产品档案,动态追问会改变注册路径的关键问题。', action:'开始建立项目档案 →', steps:[['产品边界','预期用途、适用人群、产品组成'],['研发状态','已有研究、样品、体系与文档'],['目标市场','NMPA及国际注册计划']] },
5439  path:{ eyebrow:'02 · PATH PLANNING', title:'注册路径规划', description:'比较常规、创新或特殊审查路径,形成P50/P80/P95里程碑。', action:'生成路径对比 →', steps:[['路径假设','分类、通道与适用条件'],['关键节点','检测、临床、沟通与申报'],['不确定性','前置条件与备选路径']] },
5440  gap:{ eyebrow:'03 · GAP ANALYSIS', title:'法规差距分析', description:'把现有资料逐条对照法规、指导原则和项目阶段要求。', action:'启动差距分析 →', steps:[['要求矩阵','适用条款与证据要求'],['现状映射','满足、部分满足、缺失'],['整改计划','优先级、负责人和截止时间']] },
5441  document:{ eyebrow:'04 · DOCUMENT WORKSPACE', title:'CTD / eCTD 文档工作台', description:'将研究、质量、临床和管理资料映射到卷宗章节并协同撰写。', action:'进入文档工作台 →', steps:[['素材地图','原始材料与目标章节'],['章节生成','有来源内容与缺失占位'],['版本审核','作者、审核者和修改记录']] },
5442  knowledge:{ eyebrow:'05 · REGULATION KNOWLEDGE', title:'法规知识库', description:'检索NMPA、CDE、CMDE、ICH与药典,并保留原文、ç‰
5442ˆæœ¬å’Œæœ‰æ•ˆçŠ¶æ€ã€‚', action:'检索法规原文 →', steps:[['官方来源','发布机构、文号和链接'],['适用判断','产品、阶段和生效条件'],['引用管理','原文片段、解释与更新时间']] },
5443  review:{ eyebrow:'06 · COMPLIANCE REVIEW', title:'合规审查', description:'审查文档缺项、引用错误、前后矛盾和无法由证据支持的承诺。', action:'创建审查任务 →', steps:[['完整性','必需章节与附件'],['一致性','数据、版本和术语冲突'],['可追溯性','结论到原始证据']] },
5444  tasks:{ eyebrow:'07 · CRO DELIVERY', title:'CRO任务中心', description:'把法规策略转成可执行项目计划,管理负责人、依赖、交付和补正。', action:'查看项目任务 →', steps:[['里程碑','计划、实际与决策门'],['责任分工','申办方、CRO与第三方'],['交付验收','质量标准与审核签署']] }
5445};
5446
5447function selectCroModule(moduleId) {
5448  const module = croModules[moduleId];
5449  if (!module) return;
5450  $$('#croModuleGrid button').forEach(button => button.classList.toggle('active', button.dataset.croModule === moduleId));
5451  $('#croModuleEyebrow').textContent = module.eyebrow;
5452  $('#croModuleTitle').textContent = module.title;
5453  $('#croModuleDescription').textContent = module.description;
5454  $('#croModuleAction').textContent = module.action;
5455  $('#croModuleAction').dataset.module = moduleId;
5456  $('#croModuleSteps').innerHTML = module.steps.map(([title,desc])=>`<div class="cro-work-step"><b>${title}</b><small>${desc}</small></div>`).join('');
5457}
5458
5459async function runRegulatoryTriage() {
5460  const type = $('#regProductType').selectedOptions[0]?.textContent || $('#regProductType').value;
5461  const description = $('#regDescription').value.trim();
5462  if (description.length < 20) { $('#regDescription').focus(); showToast('请补充作用机制、受试物组成、预期用途和已有证据'); return; }
5463  const button = $('#runRegulatoryTriage');
5464  button.disabled = true;
5465  button.textContent = '正在检索法规并进行路径研判…';
5466  try {
5467    const route = await investmentApiRequest('/api/regulatory/triage', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({product_type:type,stage:$('#regProjectStage').value,description})});
5468    const audit = route.audit || {};
5469    const references = Array.isArray(route.knowledge_references) ? route.knowledge_references : [];
5470    $('#regClassification').textContent = route.route_candidate || '需界定/双路径比选';
5471    $('#regScopeJudgment').textContent = route.scope_judgment || '当前信息不足';
5472    $('#regDecisionGate').textContent = route.decision_gate || '完成属性界定';
5473    $('#regCriticalGap').textContent = route.critical_gap || '补充关键技术信息';
5474    $('#regRiskLevel').textContent = route.risk_level || '待判定';
5475    $('#regRiskBasis').textContent = (route.risk_basis || []).slice(0,2).join(';') || '信息不足时不强行定级';
5476    $('#regRouteTitle').textContent = `${route.route_candidate || '监管路径'} · 路径研判`;
5477    $('#regRouteCaveat').textContent = `路径置信度${route.route_confidence || '低'} · ${audit.ai_called ? `已实际调用 ${audit.model || 'DeepSeek'}` : audit.mode || '本机初筛'}`;
5478    $('#regAiStatus').textContent = audit.ai_called ? 'AI已调用 · 需专家复核' : '本机初筛 · 需专家复核';
5479    $('#regActionList').innerHTML = (route.actions || []).map(item=>`<article><span>${escapeHtml(item.step || '')}</span><strong>${escapeHtml(item.title || '')}</strong><p>${escapeHtml(item.description || '')}<small>${escapeHtml(item.owner || '')}</small></p></article>`).join('');
5480    $('#regSourceList').innerHTML = references.length ? references.map((item,index)=>`<article><b>B${index + 1}</b><span><strong>${escapeHtml(item.source_name || '法规资料')}</strong><small>${escapeHtml(item.locator || '位置待确认')} · ${escapeHtml(item.source_date || '日期待核验')}</small><p>${escapeHtml(item.excerpt || '')}</p></span></article>`).join('') : '<p>本轮未命中法规原文,结论按低置信度处理。</p>';
5481    $('#regSourceBoundary').textContent = route.boundary || '用户提供的法规副本须核验现行有效性;路径ç
5481»“论须由法规负责人确认。';
5482    $('#regulatoryResult').classList.remove('hidden');
5483    $('#regulatoryResult').scrollIntoView({behavior:'smooth',block:'nearest'});
5484  } catch (error) {
5485    showToast(error.message || '法规路径研判失败');
5486  } finally {
5487    button.disabled = false;
5488    button.textContent = 'AI研判路径、风险和差距 →';
5489  }
5490}
5491
5492async function loadInternalKnowledgeStatus() {
5493  try {
5494    const data = await investmentApiRequest('/api/knowledge/status');
5495    const categories = data.categories || {};
5496    const issues = data.extraction_issues || {};
5497    const issueTotal = Object.values(issues).reduce((sum,value)=>sum+Number(value || 0),0);
5498    const okCount = Number(data.document_count || 0) - issueTotal;
5499    $('#knowledgeDocumentCount').innerHTML = `${Number(data.document_count || 0).toLocaleString()}<em>份</em>`;
5500    $('#knowledgeExtractedCount').textContent = `${okCount.toLocaleString()}份已完成文字提取`;
5501    $('#knowledgeBpCount').innerHTML = `${Number(categories['成果转化BP'] || 0).toLocaleString()}<em>份</em>`;
5502    $('#knowledgeReportCount').innerHTML = `${Number(categories['报告'] || 0).toLocaleString()}<em>份</em>`;
5503    $('#knowledgeBiomedicalCount').innerHTML = `${Number(categories['生物医学新技术法规'] || 0).toLocaleString()}<em>份</em>`;
5504    $('#knowledgeChunkCount').innerHTML = `${Number(data.chunk_count || 0).toLocaleString()}<em>条</em>`;
5505    $('#knowledgeCharacterCount').textContent = `约${Math.round(Number(data.character_count || 0) / 10000).toLocaleString()}万字,本机保存`;
5506    $('#knowledgeOkCount').textContent = `${okCount.toLocaleString()}份`;
5507    $('#knowledgeOcrCount').textContent = `${Number(issues.needs_ocr || 0) + Number(issues.partial_needs_ocr || 0)}份`;
5508    $('#knowledgeDuplicateCount').textContent = `${Number(issues.duplicate || 0)}份`;
5509    $('#knowledgeLegacyCount').textContent = `${Number(issues.legacy_format || 0) + Number(issues.archive_skipped || 0)}份`;
5510    $('#knowledgeUpdatedAt').textContent = '更新:2026-07-20 · 智能体资料已接入';
5511  } catch (error) {
5512    $('#knowledgeExtractedCount').textContent = `索引读取失败:${error.message}`;
5513  }
5514}
5515
5516function inferIncubationTrack(description) {
5517  const text = description.toLowerCase();
5518  if (/(试剂|检测|诊断|ivd|器械|手术|影像|设备|传感)/i.test(text)) return '医疗器械与IVD';
5519  if (/(细胞|基因|car-t|干细胞)/i.test(text)) return '细胞与基因治疗';
5520  if (/(药物|靶点|化合物|疫苗|抗体|制剂)/i.test(text)) return '创新药与疫苗';
5521  if (/(养老|数字健康|软件|平台|慢病|互联网医疗)/i.test(text)) return '数字健康与养老';
5522  if (/(材料|玻璃|包材|设备|仪器|工艺装备)/i.test(text)) return '医药材料与装备';
5523  return '尚不确定';
5524}
5525
5526function evaluateIncubationProject() {
5527  const description = $('#intakeDescription').value.trim();
5528  if (description.length < 10) { $('#intakeDescription').focus(); showToast('请先描述项目的技术、进展和转化需求'); return; }
5529  const projectName = $('#intakeProjectName').value.trim() || '未命名医药成果项目';
5530  const track = state.intakeTrack === '尚不确定' ? inferIncubationTrack(description) : state.intakeTrack;
5531  const stage = $('#intakeStage').value;
5532  const configurations = {
5533    '医疗器械与IVD': { resource:'Medici × FlashPOC / 中检安泰', gap:'临床需求、产品分类与性能证据', path:'workspace', steps:['完成临床需求与适用场景澄清','开展专利、分类与注册路径预评估','形成概念样机或试剂性能验证方案'] },
5534    '创新药与疫苗': { resource:'研究院 / CRO / 华卫骥', gap:'作用机制、成药性与临床前证据', path:'resources', steps:['完成靶点或抗原价值与竞品评价','梳理临床前研究和CMC证据缺口','匹配CRO、产业资本与后续承接主体'] },
5535    '细胞与基因治疗': { resource:'研究院 / 合作高校与园区', gap:'工艺稳定性、质量控制与临床前安全', path:'resources', steps:['明确适应症和核心技术壁垒','开展工艺、质控和知识产权尽调','组织临床、研发和法规专家联合评审'] },
5536    '数字健康与养老': { resource:'医家通智慧养老场景网络', gap:'真实使用场景、数据合规与付费路径', path:'resources', steps:['定义用户、场景和可量化健康终点','完成数据合规和产品属性预判','进入养老机构开展小规模场景验证'] },
5537    '医药材料与装备': { resource:'成果转化基地 / 合作园区', gap:'工艺放大、质量标准与下游验证', path:'resources', steps:['明确材料或装备的关键性能指标','完成小试到中试的工艺差距分析','匹配下游企业、场地和产业资本'] },
5538    '尚不确定': { resource:'集团项目评审委员会', gap:'技术边界、成果权属与目标市场', path:'resources', steps:['补充成果来源和知识产权权属','组织临床/研发专家进行技术访谈','根据证据重新确定赛道与转化路径'] }
5539  };
5540  const config = configurations[track];
5541  const triggers = [];
5542  if (/(专利|软著|知识产权)/.test(description)) triggers.push('提到知识产权');
5543  if (/(实验|样机|小试|数据|注册)/.test(description)) triggers.push('提到研发或注册进展');
5544  if (/(医院|高校|团队|企业)/.test(description)) triggers.push('提到成果或团队来源');
5545  if (track !== '尚不确定') triggers.push(`识别候选赛道:${track}`);
5546  createProjectContext({
5547    newProject: true,
5548    title: projectName,
5549    description,
5550    summary: `${projectName}的转化路径规则初筛`,
5551    tags: [track, stage, $('#intakeSource').value],
5552    sourceMode: 'rule',
5553    audit: { method:'关键词与人工选择规则', triggers }
5554  });
5555  state.intakeRecommendedTrack = track;
5556  $('#intakeResultTitle').textContent = `${projectName} · 规则初筛建议`;
5557  $('#intakeScore').innerHTML = `规则<small>初筛</small>`;
5558  $('#recommendedTrack').textContent = track;
5559  $('#readinessLevel').textContent = stage;
5560  $('#criticalGap').textContent = config.gap;
5561  $('#recommendedResource').textContent = config.resource;
5562  $('#intakeNextSteps').innerHTML = config.steps.map(step => `<li>${step}</li>`).join('');
5563  $('#intakeResult').classList.remove('hidden');
5564  appendRuleBasis($('#intakeResult'), triggers, '本模块不评估商业价值或技术可行性,只负责候选赛道分流。真实准入结论需进入 BP 分析与跨专业专家评审。');
5565  updateContextProject({ name:projectName, stage:'待专业评估', progress:5, update:'已完成转化路径规则初筛', source:'active_project' });
5566  $('#intakeResult').scrollIntoView({ behavior:'smooth', block:'nearest' });
5567}
5568
5569function enterIntakeRecommendedPath() {
5570  if (!state.intakeRecommendedTrack) return;
5571  if (state.intakeRecommendedTrack === '医疗器械与IVD') {
5572    const description = $('#intakeDescription').value.trim();
5573    $('#clinicalInput').value = description;
5574    $('#charCount').textContent = description.length;
5575    switchView('workspace');
5576    showToast('项目资料已转入医疗器械成果转化引擎');
5577  } else {
5578    $('#resourceNeedInput').value = `${state.intakeRecommendedTrack}项目需要:${$('#criticalGap').textContent}`;
5579    switchView('resources');
5580    matchGroupResources();
5581  }
5582}
5583
5584function matchGroupResources() {
5585  const need = $('#resourceNeedInput').value.trim();
5586  if (need.length < 4) { $('#resourceNeedInput').focus(); showToast('请先描述当前需要解决的转化问题'); return; }
5587  const resources = [
5588    { type:'孵化载体', name:'华卫天和大健康产业基地', reason:'承接项目入驻、政策申报、人才与企业服务。' },
5589    { type:'成果转化', name:'研究院 / CRO / 成果转化基地', reason:'组织技术尽调、研究验证和成果承接方案。' },
5590    { type:'IVD', name:'中检安泰与康永生物', reason:'匹配IVD注册、生产、国际认证与OEM经验。' },
5591    { type:'生物制品', name:'华卫骥医药服务网络', reason:'连接GSP、冷链、疫苗推广和全国客户服务。' },
5592    { type:'数字健康', name:'医家通智慧养老网络', reason:'提供养老机构试点、平台集成和规模化场景。' },
5593    { type:'园区与资本', name:'合作园区与产业投资网络', reason:'支持园区落地、融资和产业合作对接。' }
5594  ];
5595  let indexes;
5596  let triggers;
5597  if (/(试剂|ivd|检测|注册|生产)/i.test(need)) { indexes=[2,1,5];
5597 triggers=['试剂 / IVD / 检测 / 注册 / 生产']; }
5598  else if (/(疫苗|生物制品|冷链|推广)/i.test(need)) { indexes=[3,1,5]; triggers=['疫苗 / 生物制品 / 冷链 / 推广']; }
5599  else if (/(养老|数字|软件|场景|慢病)/i.test(need)) { indexes=[4,0,5]; triggers=['养老 / 数字 / 软件 / 场景 / 慢病']; }
5600  else if (/(研发|实验|成果|高校|细胞|药物)/i.test(need)) { indexes=[1,0,5]; triggers=['研发 / 实验 / 成果 / 高校 / 细胞 / 药物']; }
5601  else { indexes=[0,1,5]; triggers=[]; }
5602  const matches=indexes.map(index=>resources[index]);
5603  $$('.resource-network article').forEach(article=>article.classList.toggle('matched',matches.some(item=>item.type===article.dataset.resourceType)));
5604  $('#resourceMatchSummary').textContent=`针对“${need.slice(0,36)}${need.length>36?'…':''}”形成的协同建议`;
5605  $('#matchedResourceList').innerHTML=matches.map((item,index)=>`<div class="matched-resource"><span>匹配 ${index+1}</span><strong>${item.name}</strong><p>${item.reason}</p></div>`).join('');
5606  $('#resourceMatchResult').classList.remove('hidden');
5607  appendRuleBasis($('#resourceMatchResult'), triggers, '推荐仅表示内部能力方向可能相关,不表示资源方已确认承接、报价、周期或合规可行性。');
5608  $('#resourceMatchResult').scrollIntoView({behavior:'smooth',block:'nearest'});
5609}
5610
5611function professionalRiskClass(level) {
5612  return level === '高' ? 'high' : level === '低' ? 'low' : 'mid';
5613}
5614
5615function professionalProjectLabel(project) {
5616  const stateLabel = project.has_analysis ? '已完成BP分析' : '尚无BP分析';
5617  return `${project.name || '未命名项目'} · ${project.track || '赛道待确认'} · ${stateLabel}`;
5618}
5619
5620function renderProfessionalProjectContext(project) {
5621  currentProfessionalProject = project || null;
5622  const facts = $('#professionalProjectFacts');
5623  if (!project) {
5624    facts.innerHTML = '<span><small>产品身份</small><strong>等待选择</strong></span><span><small>BP证据覆盖</small><strong>—</strong></span><span><small>最近分析</small><strong>—</strong></span>';
5625    $('#professionalProjectHint').textContent = '先在BP分析中建立项目并完成全文分析';
5626    return;
5627  }
5628  const identity = project.product_identity || {};
5629  const audit = project.analysis_audit || {};
5630  facts.innerHTML = `<span><small>产品身份</small><strong>${escapeHtml(identity.candidate_name || identity.product_scope || project.track || '待确认')}</strong></span><span><small>BP证据覆盖</small><strong>${project.evidence_coverage == null ? '待计算' : `${escapeHtml(project.evidence_coverage)}%`}</strong></span><span><small>最近分析</small><strong>${escapeHtml(audit.mode_label || (project.has_analysis ? '已形成报告' : '尚未分析'))}</strong></span>`;
5631  $('#professionalProjectHint').textContent = project.has_analysis
5632    ? `${project.latest_file_name || 'BP材料'} · 项目负责人:${project.owner || '待指定'}`
5633    : '该项目只有档案,尚未完成BP全文分析;专业引擎不会凭空建立产品边界';
5634}
5635
5636async function loadProfessionalProjects(preferredId = '') {
5637  const select = $('#professionalProjectSelect');
5638  try {
5639    professionalProjects = await investmentApiRequest('/api/professional/projects');
5640    select.innerHTML = professionalProjects.length
5641      ? professionalProjects.map(project => `<option value="${escapeHtml(project.id)}" ${project.id === preferredId ? 'selected' : ''}>${escapeHtml(professionalProjectLabel(project))}</option>`).join('')
5642      : '<option value="">尚无项目档案,请先进入BP分析建档</option>';
5643    const selectedId = preferredId && professionalProjects.some(item => item.id === preferredId) ? preferredId : select.value;
5644    if (selectedId) {
5645      select.value = selectedId;
5646      renderProfessionalProjectContext(professionalProjects.find(item => item.id === selectedId));
5647      await Promise.all([loadProfessionalRuns(selectedId), loadProfessionalReviews(selectedId)]);
5648    } else {
5649      renderProfessionalProjectContext(null);
5650      renderProfessionalReviews([]);
5651    }
5652  } catch (error) {
5653    select.innerHTML = '<option value="">项目档案服务暂未连接</option>';
5654    $('#professionalProjectHint').textContent = error.message;
5655    renderProfessionalProjectContext(null);
5656  }
5657}
5658
5659async function selectProfessionalProject(projectId) {
5660  const project = professionalProjects.find(item => item.id === projectId) || null;
5661  renderProfessionalProjectContext(project);
5662  currentProfessionalRun = null;
5663  $('#professionalResult').classList.add('hidden');
5664  if (!projectId) return;
5665  await Promise.all([loadProfessionalRuns(projectId), loadProfessionalReviews(projectId)]);
5666}
5667
5668async function loadProfessionalRuns(projectId) {
5669  try {
5670    const runs = await investmentApiRequest(`/api/professional/projects/${encodeURIComponent(projectId)}/runs`);
5671    if (runs.length) renderProfessionalRun(runs[0]);
5672  } catch (error) {
5673    showToast(`专业任务历史读取失败:${error.message}`);
5674  }
5675}
5676
5677function renderProfessionalRun(run) {
5678  currentProfessionalRun = run;
5679  const result = run?.result || {};
5680  const snapshot = result.project_snapshot || {};
5681  const audit = result.audit || {model:run?.model,provider:run?.provider,request_id:run?.request_id};
5682  $('#professionalResult').classList.remove('hidden');
5683  $('#professionalResultEyebrow').textContent = result.engine === 'engineering' ? 'ENGINEERING TASK PACKAGE' : 'CRO REGULATORY TASK PACKAGE';
5684  $('#professionalResultTitle').textContent = result.engine_name || (result.engine === 'engineering' ? '医疗器械工程验证引擎' : 'CRO注册合规引擎');
5685  $('#professionalResultBoundary').textContent = result.boundary || 'AI输出是专业工作草案,高风险结论必须由具名专业负责人审核。';
5686  $('#professionalResultModel').textContent = audit.model || run?.model || 'AI调用记录待读取';
5687  $('#professionalResultTime').textContent = run?.created_at ? `生成于 ${new Date(run.created_at).toLocaleStr
5687ing('zh-CN')}` : '本轮生成';
5688  const materials = Array.isArray(snapshot.materials_read) ? snapshot.materials_read : [];
5689  $('#professionalSnapshot').innerHTML = [
5690    ['项目', snapshot.project_name || currentProfessionalProject?.name || '待确认', '来自统一项目档案'],
5691    ['产品身份', snapshot.product_identity || currentProfessionalProject?.product_identity?.candidate_name || '待确认', '路径和任务必须以产品身份为前提'],
5692    ['当前阶段', snapshot.stage || currentProfessionalProject?.stage || '待确认', '用于确定当前决策门'],
5693    ['本轮材料', materials.length ? `${materials.length}项已读取` : (currentProfessionalProject?.latest_file_name || '材料待确认'), snapshot.evidence_boundary || '材料没有描述的内容保持未知'],
5694  ].map(item => `<article><span>${escapeHtml(item[0])}</span><strong>${escapeHtml(item[1])}</strong><small>${escapeHtml(item[2])}</small></article>`).join('');
5695
5696  const conclusions = Array.isArray(result.conclusions) ? result.conclusions : [];
5697  $('#professionalConclusionCount').textContent = `${conclusions.length}项`;
5698  $('#professionalConclusionList').innerHTML = conclusions.length ? conclusions.map(item => {
5699    const basis = (item.basis || []).map(source => `<span><b>${escapeHtml(source.type || '依据')}:</b>${escapeHtml(source.text || '未提供具体依据')}${source.url ? ` <a href="${escapeHtml(source.url)}" target="_blank" rel="noopener">查看来源 →</a>` : ''}</span>`).join('');
5700    return `<article class="professional-conclusion-card ${professionalRiskClass(item.risk_level)}"><header><span><small>${escapeHtml(item.id || '')} · ${escapeHtml(item.status || 'AI专业判断')} · 置信度${escapeHtml(item.confidence || '低')}</small><strong>${escapeHtml(item.title || '专业判断')}</strong></span><b>${escapeHtml(item.risk_level || '中')}风险</b></header><p>${escapeHtml(item.judgment || '')}</p><div class="professional-conclusion-basis">${basis || '<span><b>依据:</b>尚未提供可核验依据,本结论不得进入受控档案。</span>'}</div></article>`;
5701  }).join('') : '<div class="professional-review-empty">本轮没有形成可展示的专业结论。</div>';
5702
5703  const tasks = Array.isArray(result.tasks) ? result.tasks : [];
5704  $('#professionalTaskCount').textContent = `${tasks.length}项`;
5705  $('#professionalTaskList').innerHTML = tasks.length ? tasks.map(task => {
5706    const inputs = (task.input_needed || []).join(';') || '由负责人确认输入';
5707    return `<article class="professional-task-row"><i>${escapeHtml(task.id || '')}</i><span><strong>${escapeHtml(task.task || '待定义任务')}</strong><small>${escapeHtml(task.workstream || '')} · ${escapeHtml(task.purpose || '')}</small></span><span><strong>所需输入</strong><small>${escapeHtml(inputs)}</small></span><span><strong>交付物</strong><small>${escapeHtml(task.deliverable || '待定义')}</small></span><b>${escapeHtml(task.responsible_role || '待指派')}</b><em>${escapeHtml(task.priority || '中')}优先</em></article>`;
5708  }).join('') : '<div class="professional-review-empty">尚未形成可执行任务。</div>';
5709
5710  const roadmap = Array.isArray(result.next_30_days) ? result.next_30_days : [];
5711  $('#professionalRoadmap').innerHTML = roadmap.map(item => `<article><i>${escapeHtml(item.week || '')}</i><span><strong>${escapeHtml(item.objective || '')}</strong><small>${escapeHtml((item.outputs || []).join(';'))}</small></span></article>`).join('') || '<div class="professional-review-empty">等待形成30天计划。</div>';
5712  const gates = Array.isArray(result.decision_gates) ? result.decision_gates : [];
5713  $('#professionalGateList').innerHTML = gates.map(item => `<article><i>${escapeHtml(item.id || '')}</i><span><strong>${escapeHtml(item.name || '')}</strong><small>通过条件:${escapeHtml((item.pass_condition || []).join(';'))}<br>责任角色:${escapeHtml(item.owner_role || '待指定')}${item.blocking ? ' · 阻断型决策门' : ''}</small></span></article>`).join('') || '<div class="professional-review-empty">等待形成决策门。</div>';
5714}
5715
5716async function runProfessionalEngine(engine) {
5717  if (professionalBusy) return;
5718  const project = currentProfessionalProject;
5719  if (!project) { showToast('请先选择一个项目档案'); return; }
5720  if (!project.has_analysis) { showToast('该项目尚未完成BP全文分析,请先上传BP'); return; }
5721  professionalBusy = true;
5722  $('#professionalRunning').classList.remove('hidden');
5723  $('#professionalResult').classList.add('hidden');
5724  $('#professionalRunningTitle').textContent = engine === 'engineering' ? '工程师正在建立受控设计输入与验证任务' : 'CRO负责人正在建立注册路径、资料和研究任务';
5725  $('#professionalRunningStep').textContent = '读取BP事实、九维分析、产品身份、官方来源和现有证据缺口…';
5726  $$('[data-professional-engine]').forEach(button => button.disabled = true);
5727  try {
5728    const run = await investmentApiRequest('/api/professional/analyze', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({project_id:project.id,engine})});
5729    renderProfessionalRun(run);
5730    await loadProfessionalReviews(project.id);
5731    $('#professionalResult').scrollIntoView({behavior:'smooth',block:'start'});
5732    showToast(`${run.result?.engine_name || '专业引擎'}已形成任务包,高风险结论已送入专家审核台`);
5733  } catch (error) {
5734    showToast(error.message);
5735    $('#professionalResultBoundary').textContent = error.message;
5736  } finally {
5737    professionalBusy = false;
5738    $('#professionalRunning').classList.add('hidden');
5739    $$('[data-professional-engine]').forEach(button => button.disabled = false);
5740  }
5741}
5742
5743function renderProfessionalReviews(reviews) {
5744  const list = $('#professionalReviewList');
5745  const empty = $('#professionalReviewEmpty');
5746  const pending = reviews.filter(item => item.status !== '已审核').length;
5747  $('#professionalReviewCount').textContent = `${pending}项待审核`;
5748  empty.classList.toggle('hidden', reviews.length > 0);
5749  list.innerHTML = reviews.map(item => {
5750    const reviewed = item.status === '已审核';
5751    return `<article class="professional-review-card${reviewed ? ' reviewed' : ''}" data-professional-review="${escapeHtml(item.id)}"><i>${escapeHtml(item.risk_level || '中')}</i><div><span>${escapeHtml(item.engine === 'engineering' ? '工程验证' : 'CRO合规')} · ${escapeHtml(item.reviewer_role || '专业负责人')} · ${escapeHtml(item.status || '待审核')}</span><strong>${escapeHtml(item.title || '专业结论审核')}</strong><p>${escapeHtml(item.question || '')}${reviewed ? `<br><b>审核记录:</b>${escapeHtml(item.reviewer || '')} · ${escapeHtml(item.decision || '')} · ${escapeHtml(item.notes || '无补充说明')}` : ''}</p></div>${reviewed ? '' : `<form><input name="reviewer" placeholder="实际审核人姓名"><select name="decision"><option value="">选择结论</option><option>通过</option><option>退回补充</option><option>否决</option><option>暂缓</option></select><textarea name="notes" placeholder="记录依据、条件、需补材料或否决理由"></textarea><button type="submit">保存具名审核结论 →</button></form>`}</article>`;
5752  }).join('');
5753}
5754
5755async function loadProfessionalReviews(projectId) {
5756  try {
5757    const reviews = await investmentApiRequest(`/api/professional/reviews?project_id=${encodeURIComponent(projectId)}`);
5758    renderProfessionalReviews(reviews);
5759  } catch (error) {
5760    renderProfessionalReviews([]);
5761    $('#professionalReviewEmpty').textContent = `审核队列读取失败:${error.message}`;
5762  }
5763}
5764
5765async function submitProfessionalReview(form) {
5766  const card = form.closest('[data-professional-review]');
5767  const reviewer = form.elements.reviewer.value.trim();
5768  const decision = form.elements.decision.value;
5769  const notes = form.elements.notes.value.trim();
5770  if (!reviewer || !decision) { showToast('请填写实际审核人并选择明确结论'); return; }
5771  const button = $('button', form);
5772  button.disabled = true;
5773  button.textContent = '正在保存…';
5774  try {
5775    await investmentApiRequest(`/api/professional/reviews/${encodeURIComponent(card.dataset.professionalReview)}/decision`, {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({reviewer,decision,notes})});
5776    await loadProfessionalReviews(currentProfessionalProject.id);
5777    showToast('专家审核结论已具名保存');
5778  } catch (error) {
5779    showToast(error.message);
5780    button.disabled = false;
5781    button.textContent = '保存具名审核结论 →';
5782  }
5783}
5784
5785// ===== FlashPOC 90-day prototype pilot =====
5786let prototypeProjects = [];
5787let currentPrototypeProject = null;
5788let prototypeActiveTab = 'charter';
5789let prototypeDialogType = '';
5790
5791function prototypeFormatDate(value) {
5792  if (!value) return '尚无记录';
5793  const date = new Date(value);
5794  return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', {month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'});
5795}
5796
5797function prototypeEmptyRecord(text) {
5798  return `<div class="prototype-record-card"><i>—</i><div><h3>尚无真实记录</h3><p>${escapeHtml(text)}</p></div><b>待录入</b></div>`;
5799}
5800
5801async function loadPrototypeProjects(preferredId = '') {
5802  const select = $('#prototypeProjectSelect');
5803  try {
5804    prototypeProjects = await investmentApiRequest('/api/prototype-pilot/projects');
5805    const requestedId = preferredId || currentPrototypeProject?.id || '';
5806    const chosenId = prototypeProjects.some(project => project.id === requestedId)
5807      ? requestedId
5808      : (prototypeProjects[0]?.id || '');
5809    select.innerHTML = '<option value="">选择一个样机试点项目</option>' + prototypeProjects.map(project => `<option value="${escapeHtml(project.id)}">
5809${escapeHtml(project.name)} · ${escapeHtml(project.status)}</option>`).join('');
5810    if (chosenId) {
5811      select.value = chosenId;
5812      await selectPrototypeProject(chosenId);
5813    } else {
5814      currentPrototypeProject = null;
5815      renderPrototypeProject();
5816    }
5817  } catch (error) {
5818    select.innerHTML = '<option value="">样机项目服务暂不可用</option>';
5819    $('#prototypeProjectHint').textContent = error.message;
5820    renderPrototypeProject();
5821  }
5822}
5823
5824async function selectPrototypeProject(projectId) {
5825  if (!projectId) {
5826    currentPrototypeProject = null;
5827    renderPrototypeProject();
5828    return;
5829  }
5830  try {
5831    currentPrototypeProject = await investmentApiRequest(`/api/prototype-pilot/projects/${encodeURIComponent(projectId)}`);
5832    $('#prototypeProjectSelect').value = projectId;
5833    renderPrototypeProject();
5834  } catch (error) {
5835    showToast(error.message);
5836  }
5837}
5838
5839function setPrototypeTab(tab) {
5840  prototypeActiveTab = tab;
5841  $$('#prototypeTabs button').forEach(button => button.classList.toggle('active', button.dataset.prototypeTab === tab));
5842  $$('[data-prototype-panel]').forEach(panel => panel.classList.toggle('active', panel.dataset.prototypePanel === tab));
5843}
5844
5845function renderPrototypeMetrics(project) {
5846  const metrics = project.metrics || {};
5847  const items = [
5848    ['追溯项', metrics.traceability_total || 0, `${metrics.engineering_defined || 0}项已形成工程输入`],
5849    ['已识别风险', metrics.risks_defined || 0, '风险需持续迭代'],
5850    ['样机版本', metrics.prototype_versions || 0, '所有变更受控留痕'],
5851    ['台架测试', `${metrics.tests_completed || 0}/${metrics.tests_total || 0}`, '完成 / 全部任务'],
5852    ['医生确认', metrics.doctor_reviews || 0, '具名模拟使用记录'],
5853    ['DHF完整度', `${metrics.dhf_completion || 0}%`, `${metrics.dhf_complete || 0}/${metrics.dhf_total || 0}已归档`],
5854  ];
5855  $('#prototypeMetrics').innerHTML = items.map(item => `<article><small>${item[0]}</small><strong>${item[1]}</strong><span>${item[2]}</span></article>`).join('');
5856}
5857
5858function renderPrototypeStages(project) {
5859  const current = Number(project.current_stage || 1);
5860  $('#prototypeCurrentStage').textContent = project.status === '试点完成' ? '四阶段门已完成' : `当前阶段 ${current} / 4`;
5861  $('#prototypeStageRail').innerHTML = (project.gates || []).map(gate => {
5862    const passed = gate.status === '已通过';
5863    const active = Number(gate.stage) === current && !passed;
5864    return `<article class="prototype-stage-card ${passed ? 'passed' : ''} ${active ? 'active' : ''}">
5865      <header><i>0${gate.stage}</i><b>${escapeHtml(gate.status)}</b></header>
5866      <h3>${escapeHtml(gate.title)}</h3><p>${escapeHtml(gate.criteria)}</p>
5867    </article>`;
5868  }).join('');
5869}
5870
5871function renderPrototypeTrace(project) {
5872  const rows = project.traceability || [];
5873  $('#prototypeTraceRows').innerHTML = rows.length ? rows.map(item => `<tr>
5874    <td><strong>${escapeHtml(item.clinical_need)}</strong><small>${escapeHtml(item.source_type)} · ${escapeHtml(item.ai_boundary)}</small></td>
5875    <td>${escapeHtml(item.engineering_requirement || '待工程师定义')}</td>
5876    <td>${escapeHtml(item.risk || '待风险识别')}</td>
5877    <td>${escapeHtml(item.verification_method || '待定义')}<small>${escapeHtml(item.acceptance_criteria || '验收标准待冻结')}</small></td>
5878    <td>${escapeHtml(item.result || '尚未验证')}<small>${escapeHtml(item.evidence_reference || '原始证据待关联')}</small></td>
5879    <td><span class="analysis-mode-badge">${escapeHtml(item.status)}</span></td>
5880  </tr>`).join('') : '<tr><td class="empty-cell" colspan="6">尚无追溯项。先录入一条真实临床需要,再由医生和工程师共同冻结可测量指标。</td></tr>';
5881}
5882
5883function renderPrototypeVersions(project) {
5884  const versions = project.versions || [];
5885  $('#prototypeVersionList').innerHTML = versions.length ? versions.map(item => `<article class="prototype-record-card">
5886    <i>${escapeHtml(item.version)}</i><div><h3>${escapeHtml(item.title)}</h3>
5887    <p>${escapeHtml(item.change_summary || '未记录版本变更原因')}</p>
5888    <ul><li>材料:${escapeHtml(item.material || '待定')}</li><li>工艺:${escapeHtml(item.process || '待定')}</li><li>BOM:${escapeHtml((item.bom || []).join(';') || '待补充')}</li><li>文件:${escapeHtml(item.file_reference || '待关联')}</li></ul></div><b>${escapeHtml(item.status)}</b>
5889  </article>`).join('') : prototypeEmptyRecord('样机实物或CAD版本形成后再登记ï¼
5889Œä¸ä»¥AI生成内容冒充工程交付物。');
5890}
5891
5892function renderPrototypeTests(project) {
5893  const tests = project.tests || [];
5894  $('#prototypeTestList').innerHTML = tests.length ? tests.map(item => `<article class="prototype-record-card">
5895    <i>测</i><div><h3>${escapeHtml(item.name)}</h3>
5896    <p>${escapeHtml(item.method || '测试方法待补充')}</p>
5897    <ul><li>预设标准:${escapeHtml(item.acceptance_criteria || '待冻结')}</li><li>实测结果:${escapeHtml(item.measured_result || '尚未测试')}</li><li>偏差:${escapeHtml(item.deviation || '无记录')}</li><li>原始数据:${escapeHtml(item.raw_data_reference || '待关联')}</li><li>执行人:${escapeHtml(item.operator || '待指定')}</li></ul></div><b>${escapeHtml(item.conclusion)}</b>
5898  </article>`).join('') : prototypeEmptyRecord('先定义验证方法与验收标准,再录入测量结果和原始数据位置。');
5899}
5900
5901function renderPrototypeReviews(project) {
5902  const reviews = project.reviews || [];
5903  $('#prototypeReviewList').innerHTML = reviews.length ? reviews.map(item => `<article class="prototype-record-card">
5904    <i>${escapeHtml((item.reviewer_role || '审').slice(0,1))}</i><div><h3>${escapeHtml(item.review_type)} · ${escapeHtml(item.reviewer)}</h3>
5905    <p>${escapeHtml(item.notes || '未记录审核意见')}</p><ul><li>角色:${escapeHtml(item.reviewer_role || '待明确')}</li><li>阶段:${item.stage}</li><li>依据:${escapeHtml(item.evidence_reference || '待关联')}</li><li>时间:${escapeHtml(prototypeFormatDate(item.created_at))}</li></ul></div><b>${escapeHtml(item.decision)}</b>
5906  </article>`).join('') : prototypeEmptyRecord('医生、工程师、法规或质量负责人完成实际评审后具名登记。');
5907  const currentStage = Number(project.current_stage || 1);
5908  const gate = (project.gates || []).find(item => Number(item.stage) === currentStage) || (project.gates || [])[3];
5909  $('#prototypeGateDecision').innerHTML = gate ? `<h3>阶段门 ${gate.stage} · ${escapeHtml(gate.title)}</h3>
5910    <p>${escapeHtml(gate.criteria)}</p>
5911    <form id="prototypeGateForm">
5912      <label>实际审核人<input name="reviewer" required placeholder="姓名"></label>
5913      <label>责任角色<input name="reviewer_role" placeholder="临床/工程/质量负责人"></label>
5914      <label>审核结论<select name="decision"><option>通过</option><option>有条件通过</option><option>退回补充</option><option>否决</option></select></label>
5915      <label>审核意见<textarea name="notes" rows="3" placeholder="说明依据、条件或退回原因"></textarea></label>
5916      <label>证据位置<input name="evidence_reference" placeholder="会议纪要/报告/文件路径"></label>
5917      <button class="analyze-button" type="submit" data-gate-stage="${gate.stage}">提交阶段门决策</button>
5918    </form>` : '<p>阶段门记录不可用</p>';
5919}
5920
5921function renderPrototypeDhf(project) {
5922  const docs = project.documents || [];
5923  $('#prototypeDhfProgress').textContent = `${project.metrics?.dhf_completion || 0}% 已归档`;
5924  $('#prototypeDhfList').innerHTML = docs.map(doc => `<div class="prototype-dhf-row" data-dhf-document="${escapeHtml(doc.id)}">
5925    <i>${escapeHtml(doc.section.slice(0,2))}</i><strong>${escapeHtml(doc.title)}</strong>
5926    <input data-dhf-field="version" value="${escapeHtml(doc.version)}" aria-label="版本">
5927    <select data-dhf-field="status" aria-label="状态"><option ${doc.status === '待补充' ? 'selected' : ''}>待补充</option><option ${doc.status === '草稿' ? 'selected' : ''}>草稿</option><option ${doc.status === '待审核' ? 'selected' : ''}>待审核</option><option ${doc.status === '已审核' ? 'selected' : ''}>已审核</option><option ${doc.status === '已归档' ? 'selected' : ''}>已归档</option></select>
5928    <button type="button" data-save-dhf>保存</button>
5929    <input data-dhf-field="file_reference" value="${escapeHtml(doc.file_reference || '')}" placeholder="文件编号或存放位置">
5930  </div>`).join('');
5931}
5932
5933function renderPrototypeCharter(project) {
5934  const form = $('#prototypeCharterForm');
5935  ['name','owner','target_user','clinical_need','intended_use','use_environment','clinical_lead','engineering_lead','regulatory_lead','quality_lead','pilot_goal','scope_boundary'].forEach(name => {
5936    if (form.elements[name]) form.elements[name].value = project[name] || '';
5937  });
5938}
5939
5940function renderPrototypeProject() {
5941  const hasProject = Boolean(currentPrototypeProject);
5942  $('#prototypeEmpty').classList.toggle('hidden', hasProject);
5943  $('#prototypeWorkspace').classList.toggle('hidden', !hasProject);
5944  if (!hasProject) return;
5945  const project = currentPrototypeProject;
5946  $('#prototypeProjectCode').textContent = project.id;
5947  $('#prototypeProjectName').textContent = project.name;
5948  $('#prototypeProjectMeta').textContent = `${project.category} · ${project.status}`;
5949  $('#prototypeProjectOwner').textContent = project.owner || '待指定';
5950  $('#prototypeUpdatedAt').textContent = `更新 ${prototypeFormatDate(project.updated_at)}`;
5951  $('#prototypeProjectHint').textContent = `${project.id} · 所有结论以本项目真实记录为准`;
5952  renderPrototypeMetrics(project);
5953  renderPrototypeStages(project);
5954  renderPrototypeCharter(project);
5955  renderPrototypeTrace(project);
5956  renderPrototypeVersions(project);
5957  renderPrototypeTests(project);
5958  renderPrototypeReviews(project);
5959  renderPrototypeDhf(project);
5960  setPrototypeTab(prototypeActiveTab);
5961}
5962
5963function togglePrototypeNewProject(show = true) {
5964  $('#prototypeNewProject').classList.toggle('hidden', !show);
5965  if (show) {
5966    $('#prototypeNewProjectForm').elements.name.focus();
5967    $('#prototypeNewProject').scrollIntoView({behavior:'smooth',block:'nearest'});
5968  }
5969}
5970
5971function prototypeField(label, name, options = {}) {
5972  const cls = options.wide ? ' class="wide"' : '';
5973  if (options.type === 'textarea') return `<label${cls}>${label}<textarea name="${name}" rows="${options.rows || 3}" ${options.required ? 'required' : ''} placeholder="${escapeHtml(options.placeholder || '')}"></textarea></label>`;
5974  if (options.type === 'select') return `<label${cls}>${label}<select name="${name}">${(options.values || []).map(value => `<option>${escapeHtml(value)}</option>`).join('')}</select></label>`;
5975  return `<label${cls}>${label}<input name="${name}" ${options.required ? 'required' : ''} placeholder="${escapeHtml(options.placeholder || '')}"></label>`;
5976}
5977
5978function openPrototypeDialog(type) {
5979  if (!currentPrototypeProject) return showToast('请先创建样机试点项目');
5980  prototypeDialogType = type;
5981  const configurations = {
5982    trace: ['新增需求追溯项', [
5983      prototypeField('临床需要 / 使用问题','clinical_need',{wide:true,type:'textarea',required:true,placeholder:'医生确认的具体问题'}),
5984      prototypeField('工程设计输入','engineering_requirement',{wide:true,type:'textarea',placeholder:'可量化、可验证的参数'}),
5985      prototypeField('初始风险','risk',{type:'textarea'}),
5986      prototypeField('验证方法','verification_method',{type:'textarea'}),
5987      prototypeField('验收标准','acceptance_criteria',{wide:true,type:'textarea',placeholder:'必须是可测量标准'}),
5988      prototypeField('来源类型','source_type',{type:'select',values:['医生访谈','工程师定义','公开证据','人工录入','AI建议']}),
5989      prototypeField('证据边界','ai_boundary',{type:'select',values:['已确认事实','事实待人工确认','AI推断待复核']}),
5990    ]],
5991    version: ['登记样机版本', [
5992      prototypeField('版本号','version',{required:true,placeholder:'v0.1'}),
5993      prototypeField('样机名称','title',{required:true,placeholder:'首轮手板样机'}),
5994      prototypeField('变更摘要','change_summary',{wide:true,type:'textarea'}),
5995      prototypeField('材料','material',{placeholder:'材料牌号/供应商'}),
5996      prototypeField('工艺','process',{placeholder:'3D打印/机加工'}),
5997      prototypeField('BOM(每行一项)','bom_text',{wide:true,type:'textarea',placeholder:'零件名称 | 规格 | 数量'}),
5998      prototypeField('图纸/文件位置','file_reference',{wide:true,placeholder:'受控文件编号或本机路径'}),
5999      prototypeField('登记人','created_by',{placeholder:'实际工程师'}),
6000    ]],
6001    test: ['登记台架测试', [
6002      prototypeField('测试任务','name',{wide:true,required:true,placeholder:'湿态组织夹持滑脱力测试'}),
6003      prototypeField('样机版本ID/版本号','prototype_version_id',{}),
6004      prototypeField('执行人','operator',{}),
6005      prototypeField('测试方法','method',{wide:true,type:'textarea'}),
6006      prototypeField('预设验收标准','acceptance_criteria',{wide:true,type:'textarea'}),
6007      prototypeField('实测结果','measured_result',{wide:true,type:'textarea'}),
6008      prototypeField('结论','conclusion',{type:'select',values:['待测试','通过','部分通过','不通过']}),
6009      prototypeField('测试日期','tested_at',{placeholder:'2026-08-01'}),
6010      prototypeField('偏差与异常','deviation',{wide:true,type:'textarea'}),
6011      prototypeField('原始数据位置','raw_data_reference',{wide:true,placeholder:'数据文件/视频/报告编号'}),
6012    ]],
6013    review: ['新增具名评审', [
6014      prototypeField('评审类型','review_type',{type:'select',values:['概念评审','工程评审','风险评审','医生确认','质量评审','法规评审']}),
6015      prototypeField('所处阶段','stage',{type:'select',values:['1','2','3','4']}),
6016      prototypeField('实际审核人','reviewer',{required:true}),
6017      prototypeField('审核人角色','reviewer_role',{placeholder:'临床医生/工程师'}),
6018      prototypeField('结论','decision',{type:'select',values:['记录意见','通过','有条件通过','退回补充','否决']}),
6019      prototypeField('证据位置','evidence_reference',{}),
6020      prototypeField('评审意见','notes',{wide:true,type:'textarea',placeholder:'说明判断依据、风险和下一步'}),
6021    ]],
6022  };
6023  const config = configurations[type];
6024  if (!config) return;
6025  $('#prototypeDialogTitle').textContent = config[0];
6026  $('#prototypeDialogFields').innerHTML = config[1].join('');
6027  $('#prototypeDataDialog').showModal();
6028}
6029
6030async function submitPrototypeDialog(event) {
6031  event.preventDefault();
6032  if (!currentPrototypeProject || !prototypeDialogType) return;
6033  const payload = Object.fromEntries(new FormData(event.currentTarget).entries());
6034  if (prototypeDialogType === 'version') {
6035    payload.bom = (payload.bom_text || '').split(/\r?\n/).map(value => value.trim()).filter(Boolean);
6036    delete payload.bom_text;
6037  }
6038  const paths = {trace:'traceability',version:'versions',test:'tests',review:'reviews'};
6039  const button = event.currentTarget.querySelector('[type="submit"]');
6040  button.disabled = true;
6041  try {
6042    currentPrototypeProject = await investmentApiRequest(`/api/prototype-pilot/projects/${encodeURIComponent(currentPrototypeProject.id)}/${paths[prototypeDialogType]}`, {
6043      method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload),
6044    });
6045    $('#prototypeDataDialog').close();
6046    renderPrototypeProject();
6047    showToast('记录已保存到样机项目档案');
6048  } catch (error) {
6049    showToast(error.message);
6050  } finally {
6051    button.disabled = false;
6052  }
6053}
6054
6055async function submitPrototypeGate(form) {
6056  if (!currentPrototypeProject) return;
6057  const stage = form.querySelector('[data-gate-stage]').dataset.gateStage;
6058  const payload = Object.fromEntries(new FormData(form).entries());
6059  const button = form.querySelector('button');
6060  button.disabled = true;
6061  try {
6062    currentPrototypeProject = await investmentApiRequest(`/api/prototype-pilot/projects/${encodeURIComponent(currentPrototypeProject.id)}/gates/${stage}/decision`, {
6063      method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload),
6064    });
6065    renderPrototypeProject();
6066    showToast(`阶段门${stage}决策已具名保存`);
6067  } catch (error) {
6068    showToast(error.message);
6069    button.disabled = false;
6070  }
6071}
6072
6073async function savePrototypeDhf(row) {
6074  if (!currentPrototypeProject) return;
6075  const payload = {};
6076  row.querySelectorAll('[data-dhf-field]').forEach(input => { payload[input.dataset.dhfField] = input.value; });
6077  const button = row.querySelector('[data-save-dhf]');
6078  button.disabled = true;
6079  try {
6080    currentPrototypeProject = await investmentApiRequest(`/api/prototype-pilot/projects/${encodeURIComponent(currentPrototypeProject.id)}/documents/${encodeURIComponent(row.dataset.dhfDocument)}`, {
6081      method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload),
6082    });
6083    renderPrototypeProject();
6084    showToast('DHF文档状态已保存');
6085  } catch (error) {
6086    showToast(error.message);
6087    button.disabled = false;
6088  }
6089}
6090
6091const NAV_GROUP_VIEWS = {
6092  experimentNav:new Set(['prototypepilot','opc','professional']),
6093  assessmentNav:new Set(['intake','investment','resources']),
6094  deviceWorkflowNav:new Set(['workspace','patent','flashpoc','development','regai','commercial']),
6095  assetNav:new Set(['enterpriselib','services','knowledge']),
6096  adminNav:new Set(['admin'])
6097};
6098
6099function setInternalPlatformExpanded(view) {
6100  Object.entries(NAV_GROUP_VIEWS).forEach(([id,views]) => {
6101    const group = $(`#${id}`);
6102    if (group && views.has(view)) group.open = true;
6103  });
6104}
6105
6106function switchView(view) {
6107  if (view === 'projects') view = 'command';
6108  if (view === 'investment') checkBpEngine();
6109  if (view === 'policyhub') { loadPolicyEnterprises(); loadPolicyCatalog(); renderPolicyMatchContext(); }
6110  if (view === 'patent') hydratePatent();
6111  if (view === 'flashpoc') hydrateFlashPOC();
6112  if (view === 'development') hydrateDevelopment();
6113  if (view === 'regai') hydrateRegAI();
6114  if (view === 'commercial') hydrateCommercial();
6115  if (view === 'command') { renderProjects(); loadInvestmentProjects().then(() => { if (state.currentView === 'command') renderProjects(); }).catch(() => {}); }
6116  if (view === 'opc') loadOpcProject();
6117  if (view === 'professional') loadProfessionalProjects(currentProfessionalProject?.id || currentInvestmentProject?.id || '');
6118  if (view === 'prototypepilot') loadPrototypeProjects(currentPrototypeProject?.id || '');
6119  if (view === 'enterpriselib') loadBaseEnterpriseLibrary();
6120  if (view === 'knowledge') loadInternalKnowledgeStatus();
6121  if (view === 'admin') loadAdminDashboard();
6122  setInternalPlatformExpanded(view);
6123  state.currentView = view;
6124  $$('.nav-item').forEach(item => item.classList.toggle('active', item.dataset.view === view));
6125  $$('.view').forEach(section => section.classList.remove('active'));
6126  $(`#${view}View`).classList.add('active');
6127  renderCapabilityBanner(view);
6128  const names = { incubator:'首页', intake:'转化路径评估', resources:'集团资源协同', command:'项目驾驶舱', prototypepilot:'FlashPOC 样机试点', opc:'医疗器械 OPC', professional:'四专业引擎', investment:'BP分析', policyhub:'政策规划', regulatory:'注册法规检索', workspace: '临床语言翻译机', patent: '专利与赛道分析', flashpoc: 'FlashPOC 概念设计', development: '工程深化任务', regai: '验证与注册', commercial:'产业化与市场', enterpriselib:'医药基地企业库', services:'服务产品', knowledge:'知识资产', admin:'用户数据看板' };
6129  $('#pageCrumb').textContent = names[view];
6130  $('#sidebar').classList.remove('open');
6131  window.scrollTo({ top: 0, behavior: 'smooth' });
6132}
6133
6134function renderProjects() {
6135  const projectRows = $('#projectRows');
6136  if (!projectRows) return;
6137  const investmentCards = investmentProjects.map(project => ({
6138    icon:'◆', name:project.name, id:`inv-${project.id}`, investmentId:project.id,
6139    stage:project.status || '已建档',
6140    progress:project.latest_score != null ? Math.round(Number(project.latest_score) * 10) : 5,
6141    owner:project.owner || '待指定',
6142    update:project.latest_score != null ? `BP评分 ${project.latest_score}/10` : '待上传BP分析',
6143    source:'investment',
6144  }));
6145  const allProjects = [...projects, ...investmentCards];
6146  const keyword = ($('#commandProjectSearch')?.value || '').trim().toLowerCase();
6147  const stage = $('#commandProjectStageFilter')?.value || 'all';
6148  const visibleProjects = allProjects.filter(project => {
6149    const matchesKeyword = !keyword || `${project.name} ${project.id} ${project.owner}`.toLowerCase().includes(keyword);
6150    return matchesKeyword && (stage === 'all' || project.stage === stage);
6151  });
6152  if (visibleProjects.length && !visibleProjects.some(project => project.id === selectedCommandProjectId)) selectedCommandProjectId = visibleProjects[0].id;
6153  projectRows.innerHTML = visibleProjects.map(project => {
6154    const isDemo = project.source === 'demo';
6155    return `<button class="command-project-item${project.id === selectedCommandProjectId ? ' selected' : ''}${isDemo ? ' demo-project' : ''}" type="button" data-project-id="${project.id}"><span class="command-project-item-head"><i>${project.icon}</i><span><strong>${project.name}</strong><small>${project.id} · ${project.owner}</small></span><b>${isDemo ? '示例' : `${project.progress}%`}</b></span><span class="command-project-item-status"><em>${project.stage}</em><small>${project.update}</small></span><span class="command-project-progress"><i><b style="width:${isDemo ? 0 : project.progress}%"></b></i></span></button>`;
6156  }).join('') || '<div class="command-project-empty"><strong>没有符合条件的项目</strong><span>请更换搜索词或阶段筛选。</span></div>';
6157  $('#commandProjectCount').textContent = `${visibleProjects.length}项`;
6158  const liveProjects = allProjects.filter(project => project.source !== 'demo');
6159  $('#commandActiveProjectCount').textContent = liveProjects.filter(project => project.progress < 100).length;
6160  const average = liveProjects.length ? Math.round(liveProjects.reduce((sum,project) => sum + project.progress,0) / liveProjects.length) : 0;
6161  $('#commandAverageProgress').innerHTML = liveProjects.length ? `${average}<em>%</em>` : '—';
6162  const selected = allProjects.find(project => project.id === selectedCommandProjectId) || visibleProjects[0] || allProjects[0];
6163  renderCommandProject(selected);
6164}
6165
6166function renderCommandProject(project) {
6167  if (!project || !$('#commandProjectName')) return;
6168  const isDemo = project.source === 'demo';
6169  const isWorkflow = project.source === 'workflow';
6170  const detail = $('.command-project-detail');
6171  detail.classList.toggle('demo-record', isDemo);
6172  $('#commandProjectIdentity').textContent = `统一项目 ID · ${project.id}`;
6173  $('#commandProjectName').textContent = project.name;
6174  $('#commandProjectMeta').textContent = `${project.owner}负责 · 当前处于${project.stage} · ${project.update}`;
6175  const projectTags = isDemo
6176    ? ['演示档案','不参与统计','不作为决策依据']
6177    : project.id === 'OPC-2026-001'
6178    ? ['OPC独立试点','AI智能体已编组','共享专家待指派','人工决策门']
6179    : [`${project.stage}中`,'政策待核验','法规路径待确认','专家审核待完成'];
6180  $('#commandProjectTags').innerHTML = projectTags.map(tag => `<b>${tag}</b>`).join('');
6181  $('#commandProjectCompleteness').innerHTML = isDemo ? '未核验' : (isWorkflow ? '待建档' : '待计算');
6182  $('#commandProjectCompletenessNote').textContent = isDemo
6183    ? '这是页面结构演示项目,不包含真实材料或证据'
6184    : '完整度必须根据实际上传并核验的材料逐项计算,不能由项目进度推算';
6185  $('#commandCurrentStage').textContent = project.stage;
6186  const actionsBox = $('#commandProjectActions');
6187  if (project.source === 'investment') {
6188    const analyzed = project.update.includes('评分');
6189    $('#commandProjectTags').innerHTML = ['BP分析项目', analyzed ? project.update : '待分析', '点击按钮查看完整报告'].map(tag => `<b>${tag}</b>`).join('');
6190    $('#commandProjectCompleteness').innerHTML = analyzed ? '已分析' : '待分析';
6191    $('#commandProjectCompletenessNote').textContent = '完整报告在BP分析工作域中查看';
6192    if (actionsBox) {
6193      actionsBox.innerHTML = `<button class="analyze-button" id="commandOpenInvestmentReport" type="button">查看该项目的BP分析报告 →</button>`;
6194      $('#commandOpenInvestmentReport')?.addEventListener('click', () => { switchView('investment'); openInvestmentProjectReport(project.investmentId); });
6195    }
6196  } else if (actionsBox) {
6197    actionsBox.innerHTML = '';
6198  }
6199}
6200
6201function getOpcTasks(project = opcProjectState) {
6202  return (project?.stages || []).flatMap(stage => stage.tasks || []);
6203}
6204
6205function calculateOpcProgress(project = opcProjectState) {
6206  const tasks = getOpcTasks(project);
6207  const completed = tasks.filter(task => task.status === 'complete').length;
6208  return { completed, total: tasks.length, percent: tasks.length ? Math.round(completed / tasks.length * 100) : 0 };
6209}
6210
6211function getOpcCurrentStage(project = opcProjectState) {
6212  const stages = project?.stages || [];
6213  return stages.find(stage => (stage.tasks || []).some(task => task.status !== 'complete')) || null;
6214}
6215
6216async function opcProjectRequest(method = 'GET', body = null) {
6217  const response = await fetch('/api/opc/project', {
6218    method,
6219    headers: body ? { 'Content-Type':'application/json' } : {},
6220    body: body ? JSON.stringify(body) : undefined
6221  });
6222  const raw = await response.text();
6223  let payload = null;
6224  try { payload = raw ? JSON.parse(raw) : null; } catch { throw new Error(`OPC 项目服务返回了无法识别的内容(HTTP ${response.status})`); }
6225  if (!response.ok) throw new Error(payload?.error || `OPC 项目服务暂时不可用(HTTP ${response.status})`);
6226  return payload;
6227}
6228
6229function syncOpcProjectToCockpit() {
6230  if (!opcProjectState) return;
6231  const progress = calculateOpcProgress();
6232  const currentStage = getOpcCurrentStage();
6233  const cockpitProject = projects.find(project => project.id === opcProjectState.id);
6234  if (!cockpitProject) return;
6235  cockpitProject.name = opcProjectState.name;
6236  cockpitProject.owner = opcProjectState.owner || '待指定主理人';
6237  cockpitProject.progress = progress.percent;
6238  cockpitProject.stage = 'OPC试点';
6239  cockpitProject.update = currentStage ? `${currentStage.name}进行中` : '试点任务已完成';
6240  renderProjects();
6241}
6242
6243function renderOpcProject() {
6244  if (!opcProjectState) return;
6245  const progress = calculateOpcProgress();
6246  const currentStage = getOpcCurrentStage();
6247  const stages = opcProjectState.stages || [];
6248  if (!stages.some(stage => stage.id === selectedOpcStageId)) selectedOpcStageId = currentStage?.id || stages.at(-1)?.id;
6249  const selectedStage = stages.find(stage => stage.id === selectedOpcStageId) || stages[0];
6250
6251  $('#opcProjectIdentity').textContent = `${opcProjectState.id} · ${opcProjectState.status || '试点进行中'}`;
6252  $('#opcProjectName').textContent = opcProjectState.name;
6253  $('#opcProjectPositioning').textContent = opcProjectState.positioning;
6254  $('#opcHeroTags').innerHTML = [opcProjectState.pilot_scope, `主理人:${opcProjectState.owner}`, `下次评审:${opcProjectState.next_review || '待确定'}`].map(tag => `<b>${escapeHtml(tag)}</b>`).join('');
6255  $('#opcProgressValue').innerHTML = `${progress.percent}<em>%</em>`;
6256  $('#opcProgressBar').style.width = `${progress.percent}%`;
6257  $('#opcCurrentStage').textContent = currentStage ? `当前:${currentStage.name}` : '全部阶段已完成';
6258  $('#opcTaskSummary').textContent = `${progress.completed} / ${progress.total} 项完成`;
6259
6260  $('#opcStageTabs').innerHTML = stages.map((stage,index) => {
6261    const tasks = stage.tasks || [];
6262    const complete = tasks.filter(task => task.status === 'complete').length;
6263    const done = tasks.length > 0 && complete === tasks.length;
6264    return `<button type="button" class="${stage.id === selectedOpcStageId ? 'active' : ''}${done ? ' complete' : ''}" data-opc-stage="${escapeHtml(stage.id)}"><i>${done ? '✓' : String(index + 1).padStart(2,'0')}</i><span><strong>${escapeHtml(stage.name)}</strong><small>${complete}/${tasks.length}项</small></span></button>`;
6265  }).join('');
6266  if (selectedStage) {
6267    $('#opcStageSummary').innerHTML = `<div><span>当前阶段</span><h3>${escapeHtml(selectedStage.name)}</h3><p>${escapeHtml(selectedStage.description || '')}</p></div><b>${(selectedStage.tasks || []).filter(task => task.status === 'complete').length}/${(selectedStage.tasks || []).length}</b>`;
6268    $('#opcTaskList').innerHTML = (selectedStage.tasks || []).map(task => `<article class="opc-task${task.status === 'complete' ? ' complete' : ''}"><button type="button" data-opc-task="${escapeHtml(task.id)}" aria-label="${task.status === 'complete' ? '标记为未完成' : '标记为完成'}"><i>${task.status === 'complete' ? '✓' : ''}</i></button><div><strong>${escapeHtml(task.title)}</strong><span><b>${escapeHtml(task.owner || '待分配')}</b><small>${escapeHtml(task.due || '待排期')}</small><small>审核:${escapeHtml(task.review_role || '项目主理人')}</small></span></div><em>${task.status === 'complete' ? '已完成' : '待推进'}</em></article>`).join('') || '<div class="opc-empty-state">当前阶段还没有任务,可以在下方添加。</div>';
6269  }
6270
6271  $('#opcAgentGrid').innerHTML = (opcProjectState.ai_agents || []).map((agent,index) => `<article><i>${String(index + 1).padStart(2,'0')}</i><div><span>${escapeHtml(agent.status)}</span><h3>${escapeHtml(agent.name)}</h3><p>${escapeHtml(agent.role)}</p></div></article>`).join('');
6272  $('#opcExpertList').innerHTML = (opcProjectState.experts || []).map(expert => `<article><div><strong>${escapeHtml(expert.role)}</strong><p>${escapeHtml(expert.requirement)}</p></div><button type="button" data-opc-expert="${escapeHtml(expert.id)}" class="${expert.status === '已确认' ? 'confirmed' : ''}">${escapeHtml(expert.status)}</button></article>`).join('');
6273  $('#opcGateGrid').innerHTML = (opcProjectState.gates || []).map((gate,index) => `<article class="${gate.status === '已通过' ? 'passed' : ''}"><header><i>G${index + 1}</i><span>${escapeHtml(gate.status)}</span></header><h3>${escapeHtml(gate.name)}</h3><p>${escapeHtml(gate.rule)}</p><footer><span>${gate.reviewed_at ? `通过记录:${escapeHtml(gate.reviewed_by || opcProjectState.owner)} · ${escapeHtml(new Date(gate.reviewed_at).toLocaleString('zh-CN'))}` : `审核人:${escapeHtml(gate.reviewer)}`}</span><button type="button" data-opc-gate="${escapeHtml(gate.id)}">${gate.status === '已通过' ? '撤回通过' : gate.status === '待审核' ? '确认通过' : '提交审核'}</button></footer></article>`).join('');
6274
6275  if (document.activeElement !== $('#opcOwnerInput')) $('#opcOwnerInput').value = opcProjectState.owner || '';
6276  if (document.activeElement !== $('#opcScopeInput')) $('#opcScopeInput').value = opcProjectState.pilot_scope || '';
6277  if (document.activeElement !== $('#opcReviewDateInput')) $('#opcReviewDateInput').value = opcProjectState.next_review || '';
6278  $('#opcLoadError').classList.add('hidden');
6279  syncOpcProjectToCockpit();
6280}
6281
6282async function loadOpcProject() {
6283  $('#opcLoadError')?.classList.add('hidden');
6284  $('#opcSaveStatus').textContent = '正在读取本机项目档案…';
6285  try {
6286    opcProjectState = await opcProjectRequest();
6287    renderOpcProject();
6288    $('#opcSaveStatus').textContent = '项目档案已连接,修改会自动保存在本机';
6289  } catch (error) {
6290    $('#opcLoadError').classList.remove('hidden');
6291    $('#opcLoadErrorText').textContent = `${error.message}。请重新启动“华卫天和孵化器智能中台”。`;
6292    $('#opcSaveStatus').textContent = '项目档案连接失败';
6293  }
6294}
6295
6296async function persistOpcProject(message = 'OPC 项目已保存') {
6297  if (!opcProjectState) return false;
6298  $('#opcSaveStatus').textContent = '正在保存…';
6299  try {
6300    opcProjectState = await opcProjectRequest('PUT', opcProjectState);
6301    renderOpcProject();
6302    $('#opcSaveStatus').textContent = `已保存 · ${new Date().toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit'})}`;
6303    if (message) showToast(message);
6304    return true;
6305  } catch (error) {
6306    $('#opcSaveStatus').textContent = `保存失败:${error.message}`;
6307    showToast('OPC 项目保存失败,请确认统一平台正在运行');
6308    return false;
6309  }
6310}
6311
6312async function toggleOpcTask(taskId) {
6313  const task = getOpcTasks().find(item => item.id === taskId);
6314  if (!task) return;
6315  task.status = task.status === 'complete' ? 'pending' : 'complete';
6316  renderOpcProject();
6317  await persistOpcProject(task.status === 'complete' ? '任务已完成并保存' : '任务已恢复为待推进');
6318}
6319
6320async function addOpcTask() {
6321  const title = $('#opcNewTaskTitle').value.trim();
6322  const owner = $('#opcNewTaskOwner').value.trim();
6323  if (!title) { showToast('请先填写任务内容'); $('#opcNewTaskTitle').focus(); return; }
6324  const stage = (opcProjectState?.stages || []).find(item => item.id === selectedOpcStageId);
6325  if (!stage) return;
6326  stage.tasks.push({ id:`custom-${Date.now()}`, title, owner:owner || '项目主理人', due:'待排期', status:'pending', review_role:'项目主理人' });
6327  $('#opcNewTaskTitle').value = '';
6328  $('#opcNewTaskOwner').value = '';
6329  renderOpcProject();
6330  await persistOpcProject('新任务已加入 OPC 试点');
6331}
6332
6333async function updateOpcExpert(expertId) {
6334  const expert = (opcProjectState?.experts || []).find(item => item.id === expertId);
6335  if (!expert) return;
6336  const states = ['待指派','候选已收集','已确认'];
6337  expert.status = states[(states.indexOf(expert.status) + 1) % states.length];
6338  renderOpcProject();
6339  await persistOpcProject('专家席位状态已更新');
6340}
6341
6342async function updateOpcGate(gateId) {
6343  const gate = (opcProjectState?.gates || []).find(item => item.id === gateId);
6344  if (!gate) return;
6345  gate.status = gate.status === '未开始' ? '待审核' : gate.status === '待审核' ? '已通过' : '待审核';
6346  if (gate.status === '已通过') {
6347    gate.reviewed_at = new Date().toISOString();
6348    gate.reviewed_by = opcProjectState.owner || '项目责任人';
6349  } else {
6350    delete gate.reviewed_at;
6351    delete gate.reviewed_by;
6352  }
6353  renderOpcProject();
6354  await persistOpcProject(gate.status === '已通过' ? `${gate.name}已由人工确认通过` : `${gate.name}状态已更新`);
6355}
6356
6357async function saveOpcSettings() {
6358  if (!opcProjectState) return;
6359  opcProjectState.owner = $('#opcOwnerInput').value.trim() || '待指定项目主理人';
6360  opcProjectState.pilot_scope = $('#opcScopeInput').value.trim() || '改良型二类无源医疗器械';
6361  opcProjectState.next_review = $('#opcReviewDateInput').value;
6362  opcProjectState.status = opcProjectState.owner.startsWith('待指定') ? '试点设计中' : '试点执行中';
6363  renderOpcProject();
6364  await persistOpcProject('OPC 试点设置已保存');
6365}
6366
6367function renderServices(stage = 'all') {
6368  const visible = stage === 'all' ? services : services.filter(item => item.stage === stage);
6369  $('#serviceGrid').innerHTML = visible.map(item => `<article class="service-card"><div class="service-card-top"><span class="service-number">${item.n}</span><span class="service-stage">${item.stageName} · ${item.status}</span></div><h3>${item.title}</h3><p>${item.desc}</p><div class="service-meta"><span>能力状态<b>${item.time}</b></span><span>商务状态<b>${item.price}</b></span><button data-service="${item.title}">了解详情 →</button></div></article>`).join('');
6370}
6371
6372$('#clinicalInput').addEventListener('input', event => $('#charCount').textContent = event.target.value.length);
6373$('#clinicalInput').addEventListener('keydown', event => {
6374  if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') runAnalysis();
6375});
6376$('#analyzeButton').addEventListener('click', runAnalysis);
6377$$('.example-chip').forEach(button => button.addEventListener('click', () => {
6378  $('#clinicalInput').value = button.dataset.example;
6379  $('#charCount').textContent = button.dataset.example.length;
6380  $('#clinicalInput').focus();
6381}));
6382$('#resultPanel').addEventListener('click', event => {
6383  const tab = event.target.closest('[data-tab]');
6384  if (tab) {
6385    $$('.result-tabs button').forEach(button => button.classList.toggle('active', button === tab));
6386    $$('.tab-pane').forEach(pane => pane.classList.add('hidden'));
6387    $(`#${tab.dataset.tab}Pane`).classList.remove('hidden');
6388  }
6389  const answer = event.target.closest('[data-interview-answer]');
6390  if (answer) {
6391    submitInterviewAnswer(answer.dataset.interviewAnswer);
6392  }
6393});
6394$('#sendAnswerButton').addEventListener('click', () => submitInterviewAnswer($('#freeAnswerInput').value));
6395$('#freeAnswerInput').addEventListener('keydown', event => {
6396  if (event.key === 'Enter') submitInterviewAnswer(event.currentTarget.value);
6397});
6398$('#generateReportButton').addEventListener('click', showReport);
6399$('#showMethodButton').addEventListener('click', () => $('#methodModal').classList.remove('hidden'));
6400$('#aiModeButton').addEventListener('click', () => {
6401  $('#aiConfigModal').classList.remove('hidden');
6402  checkAiService();
6403});
6404$('#checkAiServiceButton').addEventListener('click', checkAiService);
6405$('#enableProfessionalAiButton').addEventListener('click', () => {
6406  setAiMode('professional');
6407  $('#aiConfigModal').classList.add('hidden');
6408  showToast('统一 DeepSeek 专业模式已启用,下一次分析将进行动态临床访谈');
6409});
6410$('#showAiEvidenceButton').addEventListener('click', () => {
6411  const citations = state.analysis?.citations || [];
6412  showToast(citations.length ? `本轮已返回 ${citations.length} 条依据,详细证据将写入规格书` : '本轮未返回可核验证据,必须由专家复核');
6413});
6414$$('[data-close]').forEach(button => button.addEventListener('click', () => $(`#${button.dataset.close}`).classList.add('hidden')));
6415$$('.modal-backdrop').forEach(backdrop => backdrop.addEventListener('click', event => {
6416  if (backdrop.id === 'authGate') return; // 登录门禁:未登录前不允许点击空白绕过
6417  if (event.target === backdrop) backdrop.classList.add('hidden');
6418}));
6419document.addEventListener('keydown', event => {
6420  if (event.key === 'Escape') $$('.modal-backdrop').forEach(modal => { if (modal.id !== 'authGate') modal.classList.add('hidden'); });
6421});
6422$('#printReport').addEventListener('click', () => window.print());
6423$('#enterDesign').addEventListener('click', () => {
6424  if (state.analysis?.sourceMode !== 'ai') {
6425    showToast('规则演示结果不能进入正式专利与概念设计流程');
6426    return;
6427  }
6428  $('#reportModal').classList.add('hidden');
6429  updateContextProject({ stage:'待专利检索', progress:16, update:'需求访谈完成,等待真实专利数据' });
6430  switchView('patent');
6431  showToast('需求语义已同步;真实专利数据库接入前不会生成风险结论');
6432});
6433$('#scanPatentsButton').addEventListener('click', scanPatentLandscape);
6434$('#confirmPatentPathButton').addEventListener('click', confirmPatentPath);
6435$('#generateConceptsButton').addEventListener('click', generateConcepts);
6436$('#conceptGrid').addEventListener('click', event => {
6437  const card = event.target.closest('[data-concept-index]');
6438  if (card) selectConcept(Number(card.dataset.conceptIndex));
6439});
6440$('#confirmConceptButton').addEventListener('click', confirmConcept);
6441$('#exportConceptButton').addEventListener('click', exportConceptBrief);
6442$('#advanceMilestoneButton').addEventListener('click', advanceDevelopmentMilestone);
6443$('#enterRegAIButton').addEventListener('click', () => switchView('regai'));
6444$('#goToFlashPOCButton').addEventListener('click', () => switchView('flashpoc'));
6445$('#viewProjectButton').addEventListener('click', () => switchView('command'));
6446$('#deliverableList').addEventListener('click', event => {
6447  const button = event.target.closest('[data-deliverable]');
6448  if (button) downloadDeliverable(Number(button.dataset.deliverable));
6449});
6450$('#goToDevelopmentButton').addEventListener('click', () => switchView('development'));
6451$('#runTwinButton').addEventListener('click', runTwinValidation);
6452$('#generateRegDocsButton').addEventListener('click', generateRegDocuments);
6453$('#regDocGrid').addEventListener('click', event => {
6454  const button = event.target.closest('[data-regdoc]');
6455  if (button) downloadRegDocument(Number(button.dataset.regdoc));
6456});
6457$('#enterCommercialButton').addEventListener('click', () => switchView('commercial'));
6458$('#algorithmGrid').addEventListener('click', event => {
6459  const button = event.target.closest('[data-algorithm]');
6460  if (button) selectAlgorithm(button.dataset.algorithm);
6461});
6462$('#matchKolsButton').addEventListener('click', matchKols);
6463$('#completeJourneyButton').addEventListener('click', completeCommercialJourney);
6464$('#exportAlgorithmPlanButton').addEventListener('click', exportAlgorithmPlan);
6465$('#backToProjectsButton').addEventListener('click', () => switchView('command'));
6466$('#opcStageTabs')?.addEventListener('click', event => {
6467  const button = event.target.closest('[data-opc-stage]');
6468  if (!button) return;
6469  selectedOpcStageId = button.dataset.opcStage;
6470  renderOpcProject();
6471});
6472$('#opcTaskList')?.addEventListener('click', event => {
6473  const button = event.target.closest('[data-opc-task]');
6474  if (button) toggleOpcTask(button.dataset.opcTask);
6475});
6476$('#opcExpertList')?.addEventListener('click', event => {
6477  const button = event.target.closest('[data-opc-expert]');
6478  if (button) updateOpcExpert(button.dataset.opcExpert);
6479});
6480$('#opcGateGrid')?.addEventListener('click', event => {
6481  const button = event.target.closest('[data-opc-gate]');
6482  if (button) updateOpcGate(button.dataset.opcGate);
6483});
6484$('#opcAddTaskButton')?.addEventListener('click', addOpcTask);
6485$('#opcNewTaskTitle')?.addEventListener('keydown', event => { if (event.key === 'Enter') addOpcTask(); });
6486$('#opcSaveSettingsButton')?.addEventListener('click', saveOpcSettings);
6487$('#opcRetryButton')?.addEventListener('click', loadOpcProject);
6488$('#opcOpenCockpitButton')?.addEventListener('click', () => { selectedCommandProjectId = 'OPC-2026-001'; switchView('command'); });
6489$('#opcStartDeviceButton')?.addEventListener('click', () => switchView('workspace'));
6490$('#projectRows').addEventListener('click', event => {
6491  const item = event.target.closest('[data-project-id]');
6492  if (!item) return;
6493  selectedCommandProjectId = item.dataset.projectId;
6494  renderProjects();
6495  $('.command-project-detail').scrollIntoView({behavior:'smooth',block:'nearest'});
6496});
6497$('#commandProjectSearch').addEventListener('input', renderProjects);
6498$('#commandProjectStageFilter').addEventListener('change', renderProjects);
6499$('#professionalProjectSelect')?.addEventListener('change', event => selectProfessionalProject(event.target.value));
6500$$('[data-professional-engine]').forEach(button => button.addEventListener('click', () => runProfessionalEngine(button.dataset.professionalEngine)));
6501$('#professionalOpenBp')?.addEventListener('click', () => switchView('investment'));
6502$('#professionalReviewList')?.addEventListener('submit', event => {
6503  event.preventDefault();
6504  const form = event.target.closest('form');
6505  if (form) submitProfessionalReview(form);
6506});
6507$('#prototypeProjectSelect')?.addEventListener('change', event => selectPrototypeProject(event.target.value));
6508$('#prototypeRefreshButton')?.addEventListener('click', () => loadPrototypeProjects(currentPrototypeProject?.id || ''));
6509$('#prototypeNewButton')?.addEventListener('click', () => togglePrototypeNewProject(true));
6510$$('[data-prototype-create]').forEach(button => button.addEventListener('click', () => togglePrototypeNewProject(true)));
6511$('#prototypeCancelNewButton')?.addEventListener('click', () => togglePrototypeNewProject(false));
6512$('#prototypeNewProjectForm')?.addEventListener('submit', async event => {
6513  event.preventDefault();
6514  const form = event.currentTarget;
6515  const button = form.querySelector('[type="submit"]');
6516  button.disabled = true;
6517  try {
6518    const payload = Object.fromEntries(new FormData(form).entries());
6519    currentPrototypeProject = await investmentApiRequest('/api/prototype-pilot/projects', {
6520      method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload),
6521    });
6522    form.reset();
6523    form.elements.category.value = '无源微创手术器械';
6524    togglePrototypeNewProject(false);
6525    await loadPrototypeProjects(currentPrototypeProject.id);
6526    showToast('90天样机试点项目已建立');
6527  } catch (error) {
6528    showToast(error.message);
6529  } finally {
6530    button.disabled = false;
6531  }
6532});
6533$('#prototypeCharterForm')?.addEventListener('submit', async event => {
6534  event.preventDefault();
6535  if (!currentPrototypeProject) return;
6536  const form = event.currentTarget;
6537  const button = form.querySelector('[type="submit"]');
6538  button.disabled = true;
6539  try {
6540    const payload = Object.fromEntries(new FormData(form).entries());
6541    currentPrototypeProject = await investmentApiRequest(`/api/prototype-pilot/projects/${encodeURIComponent(currentPrototypeProject.id)}`, {
6542      method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload),
6543    });
6544    renderPrototypeProject();
6545    showToast('受控项目章程已保存');
6546  } catch (error) {
6547    showToast(error.message);
6548  } finally {
6549    button.disabled = false;
6550  }
6551});
6552$('#prototypeEditCharterButton')?.addEventListener('click', () => {
6553  setPrototypeTab('charter');
6554  $('#prototypeCharterForm')?.scrollIntoView({behavior:'smooth',block:'start'});
6555});
6556$('#prototypeTabs')?.addEventListener('click', event => {
6557  const button = event.target.closest('[data-prototype-tab]');
6558  if (button) setPrototypeTab(button.dataset.prototypeTab);
6559});
6560$('#prototypeWorkspace')?.addEventListener('click', event => {
6561  const modalButton = event.target.closest('[data-prototype-modal]');
6562  if (modalButton) openPrototypeDialog(modalButton.dataset.prototypeModal);
6563  const dhfButton = event.target.closest('[data-save-dhf]');
6564  if (dhfButton) savePrototypeDhf(dhfButton.closest('[data-dhf-document]'));
6565});
6566$('#prototypeWorkspace')?.addEventListener('submit', event => {
6567  const form = event.target.closest('#prototypeGateForm');
6568  if (!form) return;
6569  event.preventDefault();
6570  submitPrototypeGate(form);
6571});
6572$('#prototypeDataForm')?.addEventListener('submit', submitPrototypeDialog);
6573$('#prototypeDialogClose')?.addEventListener('click', () => $('#prototypeDataDialog').close());
6574$('#prototypeDialogCancel')?.addEventListener('click', () => $('#prototypeDataDialog').close());
6575$('#baseEnterpriseList')?.addEventListener('click', event => {
6576  const button = event.target.closest('[data-base-company]');
6577  if (button) selectBaseEnterprise(button.dataset.baseCompany);
6578});
6579let baseEnterpriseSearchTimer = null;
6580$('#baseEnterpriseSearch')?.addEventListener('input', () => {
6581  clearTimeout(baseEnterpriseSearchTimer);
6582  baseEnterpriseSearchTimer = setTimeout(refreshBaseEnterpriseList, 250);
6583});
6584$('#baseEnterpriseCategory')?.addEventListener('change', refreshBaseEnterpriseList);
6585$('#baseEnterpriseStage')?.addEventListener('change', refreshBaseEnterpriseList);
6586$('#baseEnterpriseReset')?.addEventListener('click', () => {
6587  $('#baseEnterpriseSearch').value = '';
6588  $('#baseEnterpriseCategory').value = '';
6589  $('#baseEnterpriseStage').value = '';
6590  refreshBaseEnterpriseList();
6591});
6592$('#baseEnterpriseMatchButton')?.addEventListener('click', () => runBaseEnterpriseProjectMatch($('#baseEnterpriseProjectSelect').value));
6593$('#baseEnterpriseNewsToggle')?.addEventListener('click', () => {
6594  const body = $('#baseEnterpriseNewsBody');
6595  if (!body) return;
6596  const collapsed = body.classList.toggle('collapsed');
6597  $('#baseEnterpriseNewsToggle').textContent = collapsed ? '展开' : '收起';
6598});
6599[$('#baseEnterpriseMatchResults'), $('#investmentEnterpriseMatchResults')].filter(Boolean).forEach(container => container.addEventListener('click', event => {
6600  const button = event.target.closest('[data-open-base-company]');
6601  if (!button) return;
6602  switchView('enterpriselib');
6603  loadBaseEnterpriseLibrary().then(() => selectBaseEnterprise(button.dataset.openBaseCompany));
6604}));
6605const policyEnterpriseSwitcher = $('.enterprise-switcher');
6606if (policyEnterpriseSwitcher && !$('#openBaseEnterpriseLibraryButton')) {
6607  const policyLibraryButton = document.createElement('button');
6608  policyLibraryButton.className = 'outline-button';
6609  policyLibraryButton.id = 'openBaseEnterpriseLibraryButton';
6610  policyLibraryButton.type = 'button';
6611  policyLibraryButton.textContent = '从基地企业库导入';
6612  policyLibraryButton.addEventListener('click', () => switchView('enterpriselib'));
6613  policyEnterpriseSwitcher.insertBefore(policyLibraryButton, $('#newPolicyEnterpriseButton'));
6614}
6615$$('.nav-item').forEach(item => item.addEventListener('click', () => switchView(item.dataset.view)));
6616$$('[data-view-link]').forEach(button => button.addEventListener('click', () => switchView(button.dataset.viewLink)));
6617$('#trackSelector').addEventListener('click', event => {
6618  const button = event.target.closest('[data-track]');
6619  if (!button) return;
6620  state.intakeTrack = button.dataset.track;
6621  $$('#trackSelector button').forEach(item => item.classList.toggle('selected', item === button));
6622});
6623$('#evaluateIntakeButton').addEventListener('click', evaluateIncubationProject);
6624$('#enterRecommendedPathButton').addEventListener('click', enterIntakeRecommendedPath);
6625$('#matchResourcesButton').addEventListener('click', matchGroupResources);
6626$('#runInvestmentReview').addEventListener('click', runInvestmentReview);
6627$('#runPolicyMatch').addEventListener('click', runPolicyMatch);
6628$('#runRegulatoryTriage').addEventListener('click', runRegulatoryTriage);
6629$('#bpFileInput').addEventListener('change', event => chooseBpFile(event.target.files[0]));
6630if ($('#bpUploadZone span')) $('#bpUploadZone span').textContent = 'PDF / DOCX / PPTX,最大 120MB';
6631$('#bpAnalyzeFileButton').addEventListener('click', analyzeBpFile);
6632$('#bpLoginButton').addEventListener('click', loginBpEngine);
6633$('#bpRegisterButton').addEventListener('click', registerBpEngine);
6634$('#bpOpenLegacyButton').addEventListener('click', () => window.open(`${BP_LEGACY_BASE}/`, '_blank'));
6635['dragenter','dragover'].forEach(type => $('#bpUploadZone').addEventListener(type, event => { event.preventDefault(); $('#bpUploadZone').classList.add('dragover'); }));
6636['dragleave','drop'].forEach(type => $('#bpUploadZone').addEventListener(type, event => { event.preventDefault(); $('#bpUploadZone').classList.remove('dragover'); if (type === 'drop') chooseBpFile(event.dataTransfer.files[0]); }));
6637$('#policyMaterialsInput').addEventListener('change', async event => { await updatePolicyMaterials(event.target.files); event.target.value = ''; });
6638$('#policyGuideInput').addEventListener('change', event => updatePolicyGuideFiles(event.target.files));
6639$('#policyTemplateInput').addEventListener('change', event => updatePolicyTemplate(event.target.files[0]));
6640$('#policyProfileVersion')?.addEventListener('change', event => applyPolicyProfileVersion(event.target.value));
6641$('#policyEnterpriseSelect').addEventListener('change', event => loadPolicyEnterprise(event.target.value));
6642initQualTagGroup('policyEnterpriseQualGroup','policyEnterpriseCertificationOther');
6643initQualTagGroup('policyQualGroup','policyCertificationOther');
6644$('#newPolicyEnterpriseButton').addEventListener('click', () =>
6644 { $('#policyEnterpriseEditor').classList.remove('hidden'); resetQualificationTags('policyEnterpriseQualGroup','policyEnterpriseCertificationOther'); $('#policyEnterpriseName').focus(); });
6645$('#cancelPolicyEnterpriseButton').addEventListener('click', () => $('#policyEnterpriseEditor').classList.add('hidden'));
6646$('#createPolicyEnterpriseButton').addEventListener('click', createPolicyEnterprise);
6647$('#tycSyncButton')?.addEventListener('click', syncTianyanchaForNewEnterprise);
6648$('#tycSyncExistingButton')?.addEventListener('click', syncTianyanchaForExistingEnterprise);
6649$('#savePolicyEnterpriseProfileButton').addEventListener('click', () => saveCurrentPolicyEnterpriseProfile(false));
6650$('#reanalyzePolicyEnterpriseButton').addEventListener('click', reanalyzePolicyEnterprise);
6651$('#generateProposalButton').addEventListener('click', generateProposalDraft);
6652$$('#croModuleGrid button').forEach(button => button.addEventListener('click', () => selectCroModule(button.dataset.croModule)));
6653$('#croModuleAction').addEventListener('click', () => { const moduleId = $('#croModuleAction').dataset.module || 'project'; if (['project','path','gap'].includes(moduleId)) { $('#regDescription').scrollIntoView({behavior:'smooth',block:'center'}); $('#regDescription').focus(); } else { showToast(`${croModules[moduleId].title}已展开;真实法规库和项目数据库将在专业数据层接入`); } });
6654$('#resourceNeedInput').addEventListener('keydown', event => { if (event.key === 'Enter') matchGroupResources(); });
6655$('#mobileMenu').addEventListener('click', () => $('#sidebar').classList.toggle('open'));
6656const sidebarCollapseButton = $('#sidebarCollapseButton');
6657const applySidebarPreference = collapsed => {
6658  $('.app-shell').classList.toggle('sidebar-collapsed', collapsed);
6659  sidebarCollapseButton.querySelector('span').textContent = collapsed ? '›' : '‹';
6660  sidebarCollapseButton.setAttribute('aria-expanded', String(!collapsed));
6661  sidebarCollapseButton.setAttribute('aria-label', collapsed ? '展开左侧导航' : '收起左侧导航');
6662  sidebarCollapseButton.title = collapsed ? '展开左侧导航' : '收起左侧导航';
6663};
6664let sidebarIsCollapsed = false;
6665try { sidebarIsCollapsed = localStorage.getItem('medici-sidebar-collapsed') === '1'; } catch (error) { sidebarIsCollapsed = false; }
6666applySidebarPreference(sidebarIsCollapsed);
6667sidebarCollapseButton.addEventListener('click', () => {
6668  sidebarIsCollapsed = !$('.app-shell').classList.contains('sidebar-collapsed');
6669  applySidebarPreference(sidebarIsCollapsed);
6670  try { localStorage.setItem('medici-sidebar-collapsed', sidebarIsCollapsed ? '1' : '0'); } catch (error) { /* 浏览器禁用本地存储时仍保持本次使用状态 */ }
6671});
6672$('#newProjectButton').addEventListener('click', () => { switchView('intake'); $('#intakeProjectName').focus(); showToast('已开始一个新的成果转化项目'); });
6673$$('[data-action="new-project"]').forEach(button => button.addEventListener('click', () => { switchView('workspace'); $('#clinicalInput').focus(); }));
6674$('#voiceButton').addEventListener('click', () => showToast('演示版暂未接入语音识别,可直接输入文字'));
6675$('#imageButton').addEventListener('click', () => showToast('演示版暂未接入草图识别,可在正式版启用多模态输入'));
6676$('.stage-selector').addEventListener('click', event => {
6677  const button = event.target.closest('[data-stage]');
6678  if (!button) return;
6679  $$('.stage-selector button').forEach(item => item.classList.toggle('active', item === button));
6680  renderServices(button.dataset.stage);
6681});
6682$('#serviceGrid').addEventListener('click', event => {
6683  const button = event.target.closest('[data-service]');
6684  if (!button) return;
6685  const title = button.dataset.service;
6686  const target = title.includes('需求洞察') ? 'workspace'
6687    : title.includes('专利') ? 'patent'
6688    : title.includes('FlashPOC') ? 'flashpoc'
6689    : title.includes('功能原型') ? 'development'
6690    : title.includes('RegAI') ? 'regai'
6691    : 'commercial';
6692  switchView(target);
6693  showToast(`已进入“${title}”对应工作台`);
6694});
6695['marketTargetUnits','marketAnnualUsage','marketUnitPrice','marketReachRate','marketShareRate'].forEach(id => $(`#${id}`)?.addEventListener('input', calculateMarketScenario));
6696$('#marketAiEstimateBtn')?.addEventListener('click', estimateMarketWithAI);
6697$('#transformationPathBtn')?.addEventListener('click', generateTransformationPath);
6698$$('[data-budget-item]').forEach(input => input.addEventListener('input', calculateRegistrationBudget));
6699$('.budget-scenario-switch')?.addEventListener('click', event => {
6700  const button = event.target.closest('[data-budget-scenario]');
6701  if (button) applyBudgetScenario(button.dataset.budgetScenario);
6702});
6703$('#investmentDimensions')?.addEventListener('click', event => {
6704  const card = event.target.closest('[data-dimension-index]');
6705  if (!card) return;
6706  const index = Number(card.dataset.dimensionIndex);
6707  renderDimensionEvidence(currentBpDimensions[index], index, true);
6708});
6709$('#dimensionEvidencePanel')?.addEventListener('click', event => {
6710  if (!event.target.closest('.dimension-back-button')) return;
6711  $('#dimensionScoreTop').scrollIntoView({behavior:'smooth',block:'start'});
6712});
6713$('#comparableRows')?.addEventListener('click', event => {
6714  const button = event.target.closest('.verify-comparable');
6715  if (button) toggleComparableVerification(button.dataset.certificate);
6716});
6717
6718initializeInvestmentWorkflowLayout();
6719initializePolicyWorkflowLayout();
6720initializeTruthfulStaticCopy();
6721renderProjects();
6722renderServices();
6723restoreDevelopmentTask();
6724initializeAiMode();
6725selectCroModule('project');
6726checkBpEngine();
6727
6728// Browser smoke-test mode: index.html?demo=interview
6729const demoMode = new URLSearchParams(window.location.search).get('demo');
6730if (demoMode === 'interview') {
6731  const demoText = $('.example-chip').dataset.example;
6732  $('#clinicalInput').value = demoText;
6733  $('#charCount').textContent = demoText.length;
6734  runAnalysis();
6735  ['胆囊 / 肠管', '持续牵拉', '10–30 分钟', '优先保护组织'].forEach((answer, index) => {
6736    setTimeout(() => submitInterviewAnswer(answer), 2300 + index * 420);
6737  });
6738} else if (demoMode === 'investment') {
6739  switchView('investment');
6740  renderProfessionalBpReport({
6741    overall_score:6.5,
6742    recommendation:'进入材料补充与专家初审',
6743    summary:'报告已完成九维材料初筛,并补充官方市场与注册收费参考。',
6744    dimensions:['临床需求','产品与技术','验证证据','注册路径','知识产权','市场竞争','团队能力','产业化能力','融资与回报'].map((name,index)=>({name,score:[7,6.3,7.6,7.6,4.4,7.6,6.3,4.4,7.6][index],evidence:'演示材料',weaknesses:[]})),
6745    risks:[], self_check:{self_check_passed:'否'}, reference_data:{...defaultInvestmentReferenceData(),product_scope:'体外诊断试剂 / IVD'}
6746  });
6747  renderInvestmentWorkflowStep(3, true);
6748} else if (demoMode === 'flashpoc') {
6749  state.analysis = scenarioLibrary[2];
6750  state.answers = {
6751    0: { answer: '胆囊 / 肠管' },
6752    1: { answer: '持续牵拉' },
6753    2: { answer: '10–30 分钟' },
6754    3: { answer: '优先保护组织' }
6755  };
6756  $('#confidenceValue').textContent = '94%';
6757  switchView('flashpoc');
6758  generateConcepts();
6759  setTimeout(() => selectConcept(1), 2100);
6760} else if (demoMode === 'development') {
6761  state.analysis = scenarioLibrary[2];
6762  state.answers = {
6763    0: { answer: '胆囊 / 肠管' },
6764    1: { answer: '持续牵拉' },
6765    2: { answer: '10–30 分钟' },
6766    3: { answer: '优先保护组织' }
6767  };
6768  state.flashpoc.projectId = 'FP-260711-286';
6769  state.flashpoc.selected = 1;
6770  confirmConcept();
6771  setTimeout(advanceDevelopmentMilestone, 450);
6772  setTimeout(advanceDevelopmentMilestone, 900);
6773} else if (demoMode === 'patent') {
6774  state.analysis = scenarioLibrary[2];
6775  switchView('patent');
6776  scanPatentLandscape();
6777} else if (demoMode === 'regai') {
6778  state.analysis = scenarioLibrary[2];
6779  state.flashpoc.selected = 1;
6780  state.development = { created:true, taskId:'DEV-260711-518', phase:4, concept:conceptLibraries.laparoscopic[1], projectId:'FP-260711-286', activities:[] };
6781  switchView('regai');
6782  runTwinValidation();
6783  setTimeout(generateRegDocuments, 2300);
6784} else if (demoMode === 'commercial') {
6785  state.analysis = scenarioLibrary[2];
6786  state.development = { created:true, taskId:'DEV-260711-518', phase:4, concept:conceptLibraries.laparoscopic[1], projectId:'FP-260711-286', activities:[] };
6787  state.regai = { validated:true, documentsGenerated:true };
6788  switchView('commercial');
6789  selectAlgorithm('力反馈预警');
6790  matchKols();
6791  setTimeout(completeCommercialJourney, 1300);
6792} else if (demoMode === 'journey') {
6793  state.analysis = scenarioLibrary[2];
6794  state.answers = {
6795    0:{answer:'胆囊 / 肠管'},1:{answer:'持续牵拉'},2:{answer:'10–30 分钟'},3:{answer:'优先保护组织'}
6796  };
6797  switchView('patent');
6798  scanPatentLandscape();
6799  setTimeout(confirmPatentPath, 1800);
6800  setTimeout(() => { selectConcept(1); confirmConcept(); }, 4000);
6801  [4400,4750,5100,5450].forEach(time => setTimeout(advanceDevelopmentMilestone,time));
6802  setTimeout(() => { switchView('regai'); runTwinValidation(); }, 5800);
6803  setTimeout(generateRegDocuments, 8000);
6804  setTimeout(() => { switchView('commercial'); selectAlgorithm('力反馈预警'); matchKols(); }, 9200);
6805  setTimeout(completeCommercialJourney, 10400);
6806} else if (demoMode === 'ai-config') {
6807  $('#aiConfigModal').classList.remove('hidden');
6808} else if (demoMode === 'intake') {
6809  switchView('intake');
6810  $('#intakeProjectName').value = '呼吸道多联检试剂';
6811  $('#intakeDescription').value = '高校团队开发了呼吸道病原体多联检试剂,已有实验数据和一项专利,希望完成产品注册、临床验证和规模化生产。';
6812  evaluateIncubationProject();
6813} else if (demoMode === 'resources') {
6814  switchView('resources');
6815  $('#resourceNeedInput').value = 'IVD项目已有实验样品,需要注册策略、临床验证和量äº
6815§åœºåœ°';
6816  matchGroupResources();
6817}
6818
6819// 平台账号体系:无 token 或 token 失效时先登录;admin 显示用户数据看板入口
6820initPlatformAuth();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.