GuXiaopeng commited on
Commit
fc839b7
·
verified ·
1 Parent(s): 9efa80b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +159 -136
app.py CHANGED
@@ -3,10 +3,8 @@
3
  from core.meta_extractor import extract_golden_metadata
4
  from core.rag_engine import initialize_rag
5
  from core.clone_architect import get_clone_architect
6
-
7
  import streamlit as st
8
 
9
- # 🚨 核心要求:必须在引入任何可能包含 Streamlit 动作的本地模块前,先设置 Page Config!
10
  st.set_page_config(page_title="Agent Article", page_icon="📊", layout="wide")
11
 
12
  import warnings
@@ -25,19 +23,18 @@ import pandas as pd
25
  import numpy as np
26
  import threading
27
 
28
- # 引入本地模块
29
  from config import APP_CONFIG, AMJ_PROMPTS, GENERIC_PROMPTS
30
  from utils.file_parser import extract_text_from_files, extract_data_from_files
31
  from utils.docx_builder import text_to_docx
32
  from core.pipeline_agent import SSciPipelineAgent
33
 
34
- # ==========================================
35
- # 0. 全局常量、工具函数与核心环境准备
36
- # ==========================================
37
  os.environ["STREAMLIT_SERVER_ENABLE_CORS"] = "false"
38
  os.environ["STREAMLIT_SERVER_ENABLE_XSRF_PROTECTION"] = "false"
39
 
40
- # 🚨 优化:去除重复硬编码,统一从 APP_CONFIG 读取黑名单
 
 
 
41
  META_SECTIONS_BLACKLIST = set(APP_CONFIG["blacklists"]["meta_sections"])
42
 
43
  def clean_trailing_references(text, keywords=("reference", "references", "reference list", "参考文献")):
@@ -60,65 +57,40 @@ if not global_s2_key:
60
  try: global_s2_key = st.secrets.get("S2_API_KEY", "")
61
  except: pass
62
 
63
- WORKSPACE_DIR = APP_CONFIG.get("workspace_dir", ".workspace")
64
- os.makedirs(WORKSPACE_DIR, exist_ok=True)
65
- CHECKPOINT_FILE = os.path.join(WORKSPACE_DIR, "snapshot.json")
66
-
67
- WECHAT_BLOCKER_JS = """
68
- <script>
69
- try {
70
- var ua = navigator.userAgent.toLowerCase();
71
- if (ua.indexOf('micromessenger') !== -1) {
72
- var parentDoc = window.parent.document;
73
- if (!parentDoc.getElementById('wx-blocker')) {
74
- var overlay = parentDoc.createElement('div');
75
- overlay.id = 'wx-blocker';
76
- overlay.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;background:#1a365d;z-index:9999999;color:white;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;font-family:sans-serif;padding:30px;box-sizing:border-box;';
77
- overlay.innerHTML = '<h2 style="font-size:26px;margin-bottom:15px;font-weight:bold;">环境受限 / Restricted</h2><p style="font-size:18px;line-height:1.6;color:#e2e8f0;max-width:100%;">为了保障最终双语 ZIP 压缩包正常下载,<b>本系统禁止在微信内直接运行</b>。<br><br>👉 <b>请点击右上角 [···] 菜单,选择“在浏览器打开” (Open in Browser)</b></p>';
78
- parentDoc.body.appendChild(overlay);
79
- }
80
- }
81
- } catch (e) {}
82
- </script>
83
- """
84
  components.html(WECHAT_BLOCKER_JS, height=0, width=0)
85
 
86
- # ==========================================
87
- # 🚨 访问控制与原生并发锁
88
- # ==========================================
89
  def check_password():
90
  expected_password = os.environ.get("APP_PASSWORD", "")
91
  if not expected_password:
92
  try: expected_password = st.secrets.get("APP_PASSWORD", "")
93
  except: pass
94
 
95
- if not expected_password: return True
96
-
97
- if "password_correct" not in st.session_state:
 
98
  st.session_state["password_correct"] = False
99
-
100
  if not st.session_state["password_correct"]:
101
  st.markdown("<h3 style='text-align: center;'>🔒 系统已锁定 / System Locked</h3>", unsafe_allow_html=True)
102
  pwd = st.text_input("Access Token", type="password", key="pwd_input")
103
  if st.button("Unlock"):
104
- if pwd == expected_password:
105
  st.session_state["password_correct"] = True
106
  st.rerun()
107
- else:
108
  st.error("❌ ���证错误,拒绝访问。")
109
  st.stop()
110
  return True
111
 
112
  @st.cache_resource
113
- def get_global_lock():
114
  return threading.Lock()
115
 
116
  check_password()
117
  global_lock = get_global_lock()
118
 
119
- # ==========================================
120
- # 1. 鲁棒的蒙特卡洛实证模拟引擎
121
- # ==========================================
122
  def generate_psd_corr_matrix(n_constructs, max_corr=0.5):
123
  if n_constructs == 1: return np.array([[1.0]])
124
  base_corr = np.random.uniform(0.2, max_corr - 0.1)
@@ -150,14 +122,16 @@ def calculate_validity_metrics(df, constructs_info, true_loadings):
150
  return pd.DataFrame(metrics)
151
 
152
  def generate_empirical_data_robust(construct_names=None, n=300, scale_points=7):
153
- if not construct_names or len(construct_names) < 2:
154
  construct_names = ["Independent_Var", "Mediator_Var", "Dependent_Var"]
 
155
  n_constructs = len(construct_names)
156
  corr = generate_psd_corr_matrix(n_constructs, max_corr=0.55)
157
  latent_scores = np.random.multivariate_normal(mean=[0]*n_constructs, cov=corr, size=n)
158
  data, true_loadings, constructs_info = {}, {}, []
159
  target_mean = scale_points / 2.0 + 0.5
160
  target_std = scale_points / 5.0
 
161
  for idx, c_name in enumerate(construct_names):
162
  true_score = latent_scores[:, idx]
163
  n_items = np.random.randint(3, 6)
@@ -173,41 +147,30 @@ def generate_empirical_data_robust(construct_names=None, n=300, scale_points=7):
173
  data[col_name] = item_int
174
  item_names.append(col_name)
175
  constructs_info.append({"name": c_name, "items": item_names})
 
176
  df = pd.DataFrame(data)
177
  validity_df = calculate_validity_metrics(df, constructs_info, true_loadings)
178
  mean_scores = pd.DataFrame({c['name']: df[c['items']].mean(axis=1) for c in constructs_info})
179
  fl_matrix = mean_scores.corr().astype(object)
 
180
  for i, row in validity_df.iterrows():
181
  c_name = row['Construct']
182
  sqrt_ave = np.sqrt(row['AVE'])
183
  fl_matrix.loc[c_name, c_name] = f"({sqrt_ave:.3f})"
 
184
  for i in range(len(fl_matrix.columns)):
185
- for j in range(i + 1, len(fl_matrix.columns)):
186
  fl_matrix.iloc[i, j] = ""
 
187
  desc_stats = mean_scores.mean().to_frame('Mean').join(mean_scores.std().to_frame('SD'))
188
  return df, validity_df, desc_stats.join(fl_matrix)
189
 
190
  def resolve_empirical_data(extracted_vars=None):
191
  df, validity_df, fl_matrix_df = generate_empirical_data_robust(extracted_vars)
192
- simulated_md_prompt = f"""
193
- ### Table 1: Reliability and Convergent Validity
194
- {validity_df.to_markdown(index=False)}
195
-
196
- ### Table 2: Descriptive Statistics and Discriminant Validity (Fornell-Larcker Criterion)
197
- *Note: Diagonal elements in parentheses are the square roots of AVE.*
198
- {fl_matrix_df.to_markdown()}
199
-
200
- [STRICT EMPIRICAL INSTRUCTION]
201
- 1. ZERO HALLUCINATION: You MUST report the exact metrics (Cronbach's α, CR, AVE, correlations) provided in the tables above.
202
- 2. VALIDITY DEFENSE: Explicitly state that Convergent Validity is established because all AVEs > 0.5 and CRs > 0.7.
203
- 3. DISCRIMINANT DEFENSE: Explicitly state that Discriminant Validity is satisfied per the Fornell-Larcker criterion, as the square root of AVE (diagonal values) exceed inter-construct correlations.
204
- """
205
  status_msg = "✅ 已成功启用稳健信效度生成引擎,AVE与Fornell-Larcker准则均验证达标。"
206
  return df, simulated_md_prompt, status_msg
207
 
208
- # ==========================================
209
- # 2. UI 界面布局与初始化
210
- # ==========================================
211
  if "is_running" not in st.session_state: st.session_state.is_running = False
212
  if "start_pipeline" not in st.session_state: st.session_state.start_pipeline = False
213
  if "resume_pipeline" not in st.session_state: st.session_state.resume_pipeline = False
@@ -226,8 +189,10 @@ if os.path.exists(CHECKPOINT_FILE) and not (st.session_state.is_running or st.se
226
  st.session_state.start_pipeline = False
227
  st.rerun()
228
  if colB.button("🗑️ 清除临时数据 (Clear Garbage)", key="clear_task"):
229
- try: os.remove(CHECKPOINT_FILE)
230
- except OSError: pass
 
 
231
  st.rerun()
232
  st.markdown("---")
233
 
@@ -239,14 +204,12 @@ tab_wizard, tab_clone = st.tabs(["✨ 智能向导模式 (Smart Wizard)", "🧬
239
 
240
  with tab_wizard:
241
  st.markdown("### 🛠️ 人在环路:研究资料配置")
242
- prompt_style_ui = st.radio("1. 请选择期望的写作规范与提示词引擎:",
243
- options=["实证研究 (AMJ Style)", "自适应 (Adaptive Style)"],
244
- index=0, disabled=st.session_state.is_running)
245
- uploaded_papers_wiz = st.file_uploader("2. 上传参考论文 (可选,支持 PDF/DOCX,建议1-20篇)", type=["pdf", "doc", "docx"], accept_multiple_files=True, disabled=st.session_state.is_running)
246
- uploaded_data_wiz = st.file_uploader("3. 上传科研数据 (可选,支持 CSV/Excel,用于防幻觉)", type=["csv", "xlsx", "xls"], disabled=st.session_state.is_running)
247
 
248
  if st.button("🚀 启动智能向导生成", disabled=st.session_state.is_running, key="btn_wiz"):
249
- if not global_lock.acquire(blocking=False):
250
  st.warning("⏳ 有小伙伴正在创作,您在15-20 分钟后就能正常使用啦")
251
  else:
252
  global_lock.release()
@@ -256,32 +219,35 @@ with tab_wizard:
256
 
257
  with tab_clone:
258
  st.markdown("### 🧬 高级克隆:框架与风格复刻")
259
- context_differences = st.text_area("1. 研究背景与核心差异(Context & Differences)(必填)", placeholder="简述您的新研究与参考论文的核心区别...", max_chars=2000, disabled=st.session_state.is_running)
260
 
261
  if "extracted_sections" not in st.session_state:
262
  st.session_state.extracted_sections = ["Introduction", "Literature Review", "Methodology", "Results", "Discussion"]
263
  if "last_uploaded_clone_ref" not in st.session_state:
264
  st.session_state.last_uploaded_clone_ref = None
265
 
266
- uploaded_reference_clone = st.file_uploader("2. 上传范例论文 (必填,单篇 PDF/DOCX)", type=["pdf", "doc", "docx"], key="clone_ref", disabled=st.session_state.is_running)
267
 
268
  if uploaded_reference_clone and global_ds_key:
269
  if st.session_state.last_uploaded_clone_ref != uploaded_reference_clone.name:
270
- with st.spinner("🔍 正在智能提取该范例论文的真实目录结构..."):
271
  temp_agent = SSciPipelineAgent(global_ds_key, global_s2_key)
272
  ref_text_raw = extract_text_from_files([uploaded_reference_clone])
273
  dynamic_sections = temp_agent.extract_document_structure(ref_text_raw)
274
  st.session_state.extracted_sections = dynamic_sections
275
  st.session_state.last_uploaded_clone_ref = uploaded_reference_clone.name
276
- st.success(f"✅ 目录提取成功:{', '.join(dynamic_sections)}")
277
 
278
- uploaded_core_papers_clone = st.file_uploader("3. 上传参考论文 (可选,支持 PDF/DOCX,建议1-20篇)", type=["pdf", "doc", "docx"], accept_multiple_files=True, key="clone_core_papers", disabled=st.session_state.is_running)
279
- uploaded_data_clone = st.file_uploader("4. 上传科研数据 (可选)", type=["csv", "xlsx", "xls"], key="clone_data", disabled=st.session_state.is_running)
280
 
281
  if st.button("🚀 启动克隆级生成", disabled=st.session_state.is_running, key="btn_clone"):
282
- if not context_differences.strip(): st.error("🚨 启动克隆模式必须填写研究背景与核心差异!")
283
- elif not uploaded_reference_clone: st.error("🚨 启动克隆模式必须上传范例论文!")
284
- elif not global_lock.acquire(blocking=False): st.warning("⏳ 有小伙伴正在创作,您在15-20 分钟后就能正常使用啦")
 
 
 
285
  else:
286
  global_lock.release()
287
  st.session_state.run_mode = "clone"
@@ -306,11 +272,8 @@ def render_monitor(agent, phase, current_sec="", details=""):
306
 
307
  FLAG_MAP = {"Simplified Chinese (简体中文)": "🇨🇳", "Russian (Русский)": "🇷🇺", "Kazakh (Қазақша)": "🇰🇿"}
308
 
309
- # ==========================================
310
- # 3. 后台流水线执行
311
- # ==========================================
312
  if st.session_state.start_pipeline or st.session_state.resume_pipeline:
313
- if not global_ds_key:
314
  st.error("❌ DEEPSEEK_API_KEY missing. 请配置API KEY。")
315
  st.stop()
316
 
@@ -347,7 +310,7 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
347
  is_resume = False
348
 
349
  if is_resume and loaded_state:
350
- pb = progress_bar.progress(50, text="🔄 正在从安全快照恢复状态...")
351
  state = loaded_state
352
  topic = state["topic"]
353
  keywords = state["keywords"]
@@ -401,9 +364,9 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
401
  if uploaded_data_clone:
402
  user_data_md = extract_data_from_files(uploaded_data_clone)
403
  if uploaded_core_papers_clone:
404
- if len(uploaded_core_papers_clone) > 20:
405
- st.warning("⚠️ 核心参考论文超过 20 篇,系统仅截取前 20 篇。")
406
- uploaded_core_papers_clone = uploaded_core_papers_clone[:20]
407
  user_lit_context = extract_text_from_files(uploaded_core_papers_clone)
408
 
409
  if not user_data_md:
@@ -577,28 +540,29 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
577
  adjustment = -accumulated_deviation / remaining_sections if remaining_sections > 0 else 0
578
  adjusted_target = min(int(SAFE_CHUNK_WORDS * 1.5), max(50, int(sec['target_words'] + adjustment)))
579
 
580
- # 🚨 格式约束指令注入位置 🚨
581
- lower_sec = sec_title.lower()
582
- adaptive_heading_rule = ""
583
 
584
- # 1. 引言平铺规则
585
- if any(kw in lower_sec for kw in ["introduction", "引言", "绪论"]):
586
- adaptive_heading_rule = "[STRICT FORMAT RULE] DO NOT use any subheadings (## or ###) in this Introduction section. Write strictly in continuous paragraphs only."
587
  else:
588
- # 2. 正文最高三级规则
589
- if adjusted_target > 800:
590
- adaptive_heading_rule = "Use Level 2 (##) and Level 3 (###) subheadings to logically structure your arguments. ABSOLUTELY NEVER use Level 4 (####)."
591
- elif adjusted_target > 400:
592
- adaptive_heading_rule = "Use Level 2 (##) subheadings to organize your content. ABSOLUTELY NEVER use Level 4 (####)."
593
- else:
594
- adaptive_heading_rule = "NEVER use Level 4 (####) headings."
 
 
 
595
 
596
- # 3. 表头正文化规则
597
- table_rule = ""
598
- if sec["is_method_or_result"]:
599
- table_rule = "[CRITICAL DATA FORMATTING] You MUST present ALL statistical data using STANDARD MARKDOWN TABLES. Also, all Table titles (e.g., Table 1: Analysis) MUST be plain text paragraphs. NEVER use heading markers (#, ##, ###) for table captions."
 
600
 
601
- length_directive = f"[LANGUAGE MANDATE] Output strictly in ENGLISH.\n[LENGTH MANDATE - STRICT] Write between {int(adjusted_target * 0.9)} and {int(adjusted_target * 1.1)} words (approx {agent._calc_paragraph_count(adjusted_target, 'en')} paragraphs).\n{adaptive_heading_rule}\n{table_rule}\n[FINAL CHECK] Do not output a word count statement."
602
 
603
  is_clone_target = False
604
  active_clone_key = None
@@ -606,8 +570,10 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
606
  for target_key in clone_metadata.keys():
607
  target_words = set(re.findall(r'[a-zA-Z]{3,}', target_key.lower()))
608
  sec_words = set(re.findall(r'[a-zA-Z]{3,}', sec_title.lower()))
609
- if "result" in target_words: target_words.add("analysis")
610
- if "result" in sec_words: sec_words.add("analysis")
 
 
611
  if target_words & sec_words:
612
  is_clone_target = True
613
  active_clone_key = target_key
@@ -616,39 +582,58 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
616
  if is_clone_target:
617
  skeleton_instruction = clone_metadata[active_clone_key]
618
  sys_prompt_m3 = "You are a top-tier academic ghostwriter executing a structural and stylistic clone."
619
- integrated_context = f"Core differences: {context_differences}\n{agent._build_memory_context(sec_title, 2500)}\nPrevious context tail: ...{context_tail}..."
620
  wiz_data_injection = f"USER ACTUAL DATA:\n{user_data_md}" if user_data_md and ("method" in sec_title.lower() or "result" in sec_title.lower() or "analysis" in sec_title.lower()) else "No data required."
621
- base_user = get_clone_architect(agent).build_rewrite_instruction(skeleton_instruction, sec_title, topic, integrated_context, wiz_data_injection) + f"\n\n{length_directive}"
622
  else:
623
  extra_directives = []
 
624
  golden_meta = agent.global_memory.get("core_golden_metadata", [])
625
 
626
  if golden_meta:
627
- if any(kw in lower_sec for kw in ["introduction", "background"]): extra_directives.append("【TARGETED DIRECTIVE】 Response to Gaps in [TARGETED INJECTION].")
628
- elif any(kw in lower_sec for kw in ["literature", "theor", "hypothesis", "review"]): extra_directives.append("【TARGETED DIRECTIVE】 Use Theoretical Lens from [TARGETED INJECTION].")
629
- elif any(kw in lower_sec for kw in ["method", "measure", "data", "sample"]): extra_directives.append("【TARGETED DIRECTIVE】 Measurements adapted from [TARGETED INJECTION].")
630
- elif any(kw in lower_sec for kw in ["result", "analysis"]): extra_directives.append("【ZERO HALLUCINATION】 Report <USER_ACTUAL_DATA> exactly.")
631
- elif any(kw in lower_sec for kw in ["discussion", "conclusion", "implication"]): extra_directives.append("【TARGETED DIRECTIVE】 Structured comparison with [TARGETED INJECTION].")
 
 
 
 
 
 
 
632
  else:
633
- if sec["is_method_or_result"]: extra_directives.append(APP_CONFIG["methodological_defense_directive"]); extra_directives.append(APP_CONFIG["empirical_data_logic"])
634
- if any(kw in lower_sec for kw in ["theory", "theoretical", "hypothesis", "introduction"]): extra_directives.append(APP_CONFIG["deep_synthesis_directive"]); extra_directives.append(APP_CONFIG["hypothesis_development_directive"])
 
 
 
 
 
 
 
 
635
 
636
  theory_injection = "\n\n".join(extra_directives)
637
  sys_prompt_m3 = APP_CONFIG["sys_academic_writer"] + "\n" + APP_CONFIG["deep_refinement_directive"]
638
  wiz_data_injection = f"\n<USER_ACTUAL_DATA>\n{user_data_md}\n</USER_ACTUAL_DATA>\n" if user_data_md and sec["is_method_or_result"] else ""
639
-
640
- base_user = f"Generate polished section '{display_name}'.\n{length_directive}\n{theory_injection}\nGlobal Research Synthesis: {synthesis_result}\n{wiz_data_injection}\nPrevious Context Tail: ...{context_tail}...\nIMPORTANT: Output final academic text."
641
 
642
- render_monitor(agent, f"English Drafting ({current_step+1}/{total_steps})", display_name, f"目标: {adjusted_target}")
 
 
 
 
 
643
 
644
  try:
645
- en_draft_raw = en_ph.write_stream(agent.stream_deepseek(sys_prompt_m3, base_user, max_tokens=6000))
646
- en_draft_clean = re.sub(r'<think>.*?</think>', '', en_draft_raw, flags=re.DOTALL).strip()
647
  en_draft_clean = re.sub(rf'(?im)^#\s*{re.escape(sec_title)}\s*\n+', '', en_draft_clean).strip()
648
-
649
- if sec["is_first"]: en_draft_clean = f"# {sec_title}\n\n{en_draft_clean}"
650
 
651
- en_resolved = agent.resolve_citations(en_draft_clean)
652
  en_ph.markdown(en_resolved)
653
  full_draft_en += en_resolved + "\n\n"
654
 
@@ -658,26 +643,29 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
658
 
659
  actual_words = len(en_draft_clean.split())
660
  accumulated_deviation += (actual_words - adjusted_target)
 
 
661
  current_step += 1
662
- pb.progress(min(15 + int((current_step / total_steps) * 80), 99))
663
  except Exception as e:
664
- en_ph.error(f"节点异常: {e}")
 
665
  generation_aborted = True
666
  break
667
 
668
  render_monitor(agent, f"Translating ({current_step+1}/{total_steps})", display_name, "翻译中...")
669
  trans_ph.empty()
670
  try:
671
- trans_chunk_raw = trans_ph.write_stream(agent.stream_deepseek(f"Translate into {target_lang}. Preserve formatting and [CITE-XX] tags.", f"TEXT:\n{en_draft_clean}", max_tokens=6000, model_name="deepseek-chat"))
672
- trans_chunk_clean = re.sub(r'<think>.*?</think>', '', trans_chunk_raw, flags=re.DOTALL).strip()
673
- trans_resolved = agent.resolve_citations(trans_chunk_clean)
674
  trans_ph.markdown(trans_resolved)
675
  full_draft_translated += trans_resolved + "\n\n"
676
 
677
  current_step += 1
678
- pb.progress(min(15 + int((current_step / total_steps) * 80), 99))
679
  except Exception as e:
680
- trans_ph.error(f"翻译熔断: {e}")
 
681
  generation_aborted = True
682
  break
683
 
@@ -687,31 +675,66 @@ if st.session_state.start_pipeline or st.session_state.resume_pipeline:
687
  state["context_tail"] = context_tail
688
  state["agent_state"] = agent.export_state()
689
 
 
690
  with open(CHECKPOINT_FILE, "w", encoding="utf-8") as f:
691
  json.dump(state, f, ensure_ascii=False)
692
 
693
  if not generation_aborted and full_draft_en.strip():
694
- pb.progress(96, text="📦 正在打包 DOCX 文档...")
695
  full_draft_en = clean_trailing_references(full_draft_en, ["Reference", "References", "Reference List"])
696
  full_draft_translated = clean_trailing_references(full_draft_translated, ["参考文献", "Reference", "References", "Reference List"])
697
 
698
- apa_refs = "\n\n".join([f"- {agent.citation_map[t]['ref']}" for t in sorted(list(agent.used_citations)) if t in agent.citation_map])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
699
  full_draft_en += "\n\n# References\n" + apa_refs
700
  full_draft_translated += "\n\n# 参考文献\n" + apa_refs
701
 
702
  bj_time = datetime.now(timezone(timedelta(hours=8))).strftime("%Y%m%d_%H%M")
703
  zip_buffer = io.BytesIO()
704
  with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
705
- zf.writestr(f"Empirical_Research_EN_{bj_time}.docx", text_to_docx(full_draft_en).getvalue())
706
- zf.writestr(f"Empirical_Research_ZH_{bj_time}.docx", text_to_docx(full_draft_translated).getvalue())
707
  zip_buffer.seek(0)
708
  st.session_state.zip_buffer = zip_buffer.getvalue()
709
- st.session_state.zip_filename = f"Article_Draft_{bj_time}.zip"
710
 
711
- try: os.remove(CHECKPOINT_FILE)
712
- except OSError: pass
 
 
 
713
  pb.progress(100, text="✅ 完成")
714
- render_monitor(agent, "完成", "✅", "文档已生成并已清空本地快照")
715
 
716
  except Exception as e:
717
  st.error(f"🚨 启动/恢复异常: {e}")
 
3
  from core.meta_extractor import extract_golden_metadata
4
  from core.rag_engine import initialize_rag
5
  from core.clone_architect import get_clone_architect
 
6
  import streamlit as st
7
 
 
8
  st.set_page_config(page_title="Agent Article", page_icon="📊", layout="wide")
9
 
10
  import warnings
 
23
  import numpy as np
24
  import threading
25
 
 
26
  from config import APP_CONFIG, AMJ_PROMPTS, GENERIC_PROMPTS
27
  from utils.file_parser import extract_text_from_files, extract_data_from_files
28
  from utils.docx_builder import text_to_docx
29
  from core.pipeline_agent import SSciPipelineAgent
30
 
 
 
 
31
  os.environ["STREAMLIT_SERVER_ENABLE_CORS"] = "false"
32
  os.environ["STREAMLIT_SERVER_ENABLE_XSRF_PROTECTION"] = "false"
33
 
34
+ WORKSPACE_DIR = APP_CONFIG.get("workspace_dir", ".workspace")
35
+ os.makedirs(WORKSPACE_DIR, exist_ok=True)
36
+ CHECKPOINT_FILE = os.path.join(WORKSPACE_DIR, "snapshot.json")
37
+
38
  META_SECTIONS_BLACKLIST = set(APP_CONFIG["blacklists"]["meta_sections"])
39
 
40
  def clean_trailing_references(text, keywords=("reference", "references", "reference list", "参考文献")):
 
57
  try: global_s2_key = st.secrets.get("S2_API_KEY", "")
58
  except: pass
59
 
60
+ WECHAT_BLOCKER_JS = """<script>try{var ua=navigator.userAgent.toLowerCase();if(ua.indexOf('micromessenger')!==-1){var parentDoc=window.parent.document;if(!parentDoc.getElementById('wx-blocker')){var overlay=parentDoc.createElement('div');overlay.id='wx-blocker';overlay.style.cssText='position:fixed;top:0;left:0;width:100vw;height:100vh;background:#1a365d;z-index:9999999;color:white;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;font-family:sans-serif;padding:30px;box-sizing:border-box;';overlay.innerHTML='<h2 style="font-size:26px;margin-bottom:15px;font-weight:bold;">环境受限 / Restricted</h2><p style="font-size:18px;line-height:1.6;color:#e2e8f0;max-width:100%;">为了保障最终双语 ZIP 压缩包正常下载,<b>本系统禁止在微信内直接运行</b>。<br><br>👉 <b>请点击右上角 [···] 菜单,选择“在浏览器打开” (Open in Browser)</b></p>';parentDoc.body.appendChild(overlay);}}}catch(e){}</script>"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  components.html(WECHAT_BLOCKER_JS, height=0, width=0)
62
 
 
 
 
63
  def check_password():
64
  expected_password = os.environ.get("APP_PASSWORD", "")
65
  if not expected_password:
66
  try: expected_password = st.secrets.get("APP_PASSWORD", "")
67
  except: pass
68
 
69
+ if not expected_password:
70
+ return True
71
+
72
+ if "password_correct" not in st.session_state:
73
  st.session_state["password_correct"] = False
74
+
75
  if not st.session_state["password_correct"]:
76
  st.markdown("<h3 style='text-align: center;'>🔒 系统已锁定 / System Locked</h3>", unsafe_allow_html=True)
77
  pwd = st.text_input("Access Token", type="password", key="pwd_input")
78
  if st.button("Unlock"):
79
+ if pwd == expected_password:
80
  st.session_state["password_correct"] = True
81
  st.rerun()
82
+ else:
83
  st.error("❌ ���证错误,拒绝访问。")
84
  st.stop()
85
  return True
86
 
87
  @st.cache_resource
88
+ def get_global_lock():
89
  return threading.Lock()
90
 
91
  check_password()
92
  global_lock = get_global_lock()
93
 
 
 
 
94
  def generate_psd_corr_matrix(n_constructs, max_corr=0.5):
95
  if n_constructs == 1: return np.array([[1.0]])
96
  base_corr = np.random.uniform(0.2, max_corr - 0.1)
 
122
  return pd.DataFrame(metrics)
123
 
124
  def generate_empirical_data_robust(construct_names=None, n=300, scale_points=7):
125
+ if not construct_names or len(construct_names) < 2:
126
  construct_names = ["Independent_Var", "Mediator_Var", "Dependent_Var"]
127
+
128
  n_constructs = len(construct_names)
129
  corr = generate_psd_corr_matrix(n_constructs, max_corr=0.55)
130
  latent_scores = np.random.multivariate_normal(mean=[0]*n_constructs, cov=corr, size=n)
131
  data, true_loadings, constructs_info = {}, {}, []
132
  target_mean = scale_points / 2.0 + 0.5
133
  target_std = scale_points / 5.0
134
+
135
  for idx, c_name in enumerate(construct_names):
136
  true_score = latent_scores[:, idx]
137
  n_items = np.random.randint(3, 6)
 
147
  data[col_name] = item_int
148
  item_names.append(col_name)
149
  constructs_info.append({"name": c_name, "items": item_names})
150
+
151
  df = pd.DataFrame(data)
152
  validity_df = calculate_validity_metrics(df, constructs_info, true_loadings)
153
  mean_scores = pd.DataFrame({c['name']: df[c['items']].mean(axis=1) for c in constructs_info})
154
  fl_matrix = mean_scores.corr().astype(object)
155
+
156
  for i, row in validity_df.iterrows():
157
  c_name = row['Construct']
158
  sqrt_ave = np.sqrt(row['AVE'])
159
  fl_matrix.loc[c_name, c_name] = f"({sqrt_ave:.3f})"
160
+
161
  for i in range(len(fl_matrix.columns)):
162
+ for j in range(i + 1, len(fl_matrix.columns)):
163
  fl_matrix.iloc[i, j] = ""
164
+
165
  desc_stats = mean_scores.mean().to_frame('Mean').join(mean_scores.std().to_frame('SD'))
166
  return df, validity_df, desc_stats.join(fl_matrix)
167
 
168
  def resolve_empirical_data(extracted_vars=None):
169
  df, validity_df, fl_matrix_df = generate_empirical_data_robust(extracted_vars)
170
+ simulated_md_prompt = f"""### Table 1: Reliability and Convergent Validity\n{validity_df.to_markdown(index=False)}\n### Table 2: Descriptive Statistics and Discriminant Validity (Fornell-Larcker Criterion)\n*Note: Diagonal elements in parentheses are the square roots of AVE.*\n{fl_matrix_df.to_markdown()}\n[STRICT EMPIRICAL INSTRUCTION]\n1. ZERO HALLUCINATION: You MUST report the exact metrics (Cronbach's α, CR, AVE, correlations) provided in the tables above.\n2. VALIDITY DEFENSE: Explicitly state that Convergent Validity is established because all AVEs > 0.5 and CRs > 0.7.\n3. DISCRIMINANT DEFENSE: Explicitly state that Discriminant Validity is satisfied per the Fornell-Larcker criterion, as the square root of AVE (diagonal values) exceed inter-construct correlations."""
 
 
 
 
 
 
 
 
 
 
 
 
171
  status_msg = "✅ 已成功启用稳健信效度生成引擎,AVE与Fornell-Larcker准则均验证达标。"
172
  return df, simulated_md_prompt, status_msg
173
 
 
 
 
174
  if "is_running" not in st.session_state: st.session_state.is_running = False
175
  if "start_pipeline" not in st.session_state: st.session_state.start_pipeline = False
176
  if "resume_pipeline" not in st.session_state: st.session_state.resume_pipeline = False
 
189
  st.session_state.start_pipeline = False
190
  st.rerun()
191
  if colB.button("🗑️ 清除临时数据 (Clear Garbage)", key="clear_task"):
192
+ try:
193
+ os.remove(CHECKPOINT_FILE)
194
+ except OSError:
195
+ pass
196
  st.rerun()
197
  st.markdown("---")
198
 
 
204
 
205
  with tab_wizard:
206
  st.markdown("### 🛠️ 人在环路:研究资料配置")
207
+ prompt_style_ui = st.radio("1. 请选择期望的写作规范与提示词引擎:", options=["实证研究 (AMJ Style)", "自适应 (Adaptive Style)"], index=0, disabled=st.session_state.is_running)
208
+ uploaded_papers_wiz = st.file_uploader("2. 上传参考论文 (可选,支持 PDF/DOCX,建议1-5篇)", type=["pdf", "doc", "docx"], accept_multiple_files=True, disabled=st.session_state.is_running)
209
+ uploaded_data_wiz = st.file_uploader("3. 上传统计分析结果 (可选,支持 CSV/Excel,用于防幻觉)", type=["csv", "xlsx", "xls"], disabled=st.session_state.is_running)
 
 
210
 
211
  if st.button("🚀 启动智能向导生成", disabled=st.session_state.is_running, key="btn_wiz"):
212
+ if not global_lock.acquire(blocking=False):
213
  st.warning("⏳ 有小伙伴正在创作,您在15-20 分钟后就能正常使用啦")
214
  else:
215
  global_lock.release()
 
219
 
220
  with tab_clone:
221
  st.markdown("### 🧬 高级克隆:框架与风格复刻")
222
+ context_differences = st.text_area("1. 研究背景与核心差异(Context & Differences)(必填,建议 1-2000 字)", placeholder="简述您的新研究与参考论文在数据、案例或背景上的核心区别...", max_chars=2000, disabled=st.session_state.is_running)
223
 
224
  if "extracted_sections" not in st.session_state:
225
  st.session_state.extracted_sections = ["Introduction", "Literature Review", "Methodology", "Results", "Discussion"]
226
  if "last_uploaded_clone_ref" not in st.session_state:
227
  st.session_state.last_uploaded_clone_ref = None
228
 
229
+ uploaded_reference_clone = st.file_uploader("2. 上传范例论文 (必填,单篇 PDF/DOCX,系统自动解构目录)", type=["pdf", "doc", "docx"], key="clone_ref", disabled=st.session_state.is_running)
230
 
231
  if uploaded_reference_clone and global_ds_key:
232
  if st.session_state.last_uploaded_clone_ref != uploaded_reference_clone.name:
233
+ with st.spinner("🔍 正在智能提取该范例论文的真实目录结构,请稍候..."):
234
  temp_agent = SSciPipelineAgent(global_ds_key, global_s2_key)
235
  ref_text_raw = extract_text_from_files([uploaded_reference_clone])
236
  dynamic_sections = temp_agent.extract_document_structure(ref_text_raw)
237
  st.session_state.extracted_sections = dynamic_sections
238
  st.session_state.last_uploaded_clone_ref = uploaded_reference_clone.name
239
+ st.success(f"✅ 目录提取成功,将自动全量克隆以下结构:{', '.join(dynamic_sections)}")
240
 
241
+ uploaded_core_papers_clone = st.file_uploader("3. 上传参考论文 (可选,用于提升理论深度,建议1-5篇)", type=["pdf", "doc", "docx"], accept_multiple_files=True, key="clone_core_papers", disabled=st.session_state.is_running)
242
+ uploaded_data_clone = st.file_uploader("4. 上传科研数据 (可选,支持 CSV/Excel,用于防幻觉)", type=["csv", "xlsx", "xls"], key="clone_data", disabled=st.session_state.is_running)
243
 
244
  if st.button("🚀 启动克隆级生成", disabled=st.session_state.is_running, key="btn_clone"):
245
+ if not context_differences.strip():
246
+ st.error("🚨 启动克隆模式必须填写研究背景与核心差异!")
247
+ elif not uploaded_reference_clone:
248
+ st.error("🚨 启动克隆模式必须上传范例论文!")
249
+ elif not global_lock.acquire(blocking=False):
250
+ st.warning("⏳ 有小伙伴正在创作,您在15-20 分钟后就能正常使用啦")
251
  else:
252
  global_lock.release()
253
  st.session_state.run_mode = "clone"
 
272
 
273
  FLAG_MAP = {"Simplified Chinese (简体中文)": "🇨🇳", "Russian (Русский)": "🇷🇺", "Kazakh (Қазақша)": "🇰🇿"}
274
 
 
 
 
275
  if st.session_state.start_pipeline or st.session_state.resume_pipeline:
276
+ if not global_ds_key:
277
  st.error("❌ DEEPSEEK_API_KEY missing. 请配置API KEY。")
278
  st.stop()
279
 
 
310
  is_resume = False
311
 
312
  if is_resume and loaded_state:
313
+ pb = progress_bar.progress(50, text="🔄 正在从本地安全快照恢复状态...")
314
  state = loaded_state
315
  topic = state["topic"]
316
  keywords = state["keywords"]
 
364
  if uploaded_data_clone:
365
  user_data_md = extract_data_from_files(uploaded_data_clone)
366
  if uploaded_core_papers_clone:
367
+ if len(uploaded_core_papers_clone) > 5:
368
+ st.warning("⚠️ 核心参考论文超过 5 篇,系统仅截取前 5 篇。")
369
+ uploaded_core_papers_clone = uploaded_core_papers_clone[:5]
370
  user_lit_context = extract_text_from_files(uploaded_core_papers_clone)
371
 
372
  if not user_data_md:
 
540
  adjustment = -accumulated_deviation / remaining_sections if remaining_sections > 0 else 0
541
  adjusted_target = min(int(SAFE_CHUNK_WORDS * 1.5), max(50, int(sec['target_words'] + adjustment)))
542
 
543
+ is_quarantined = any(kw in sec_title.lower() for kw in ["abstract", "keyword", "摘要", "关键字", "关键词", "conclusion", "结论", "заключение", "қорытынды", "references"])
 
 
544
 
545
+ if is_quarantined:
546
+ quarantine_rule = "【CRITICAL】: NO IN-TEXT CITATIONS ALLOWED IN THIS SECTION."
 
547
  else:
548
+ assigned_tags_str = ", ".join(allocation_map.get(display_name, [])) or "any relevant citations"
549
+ quarantine_rule = f"【CITATION GUIDANCE】: You are strongly encouraged to integrate citations: {assigned_tags_str}. <AVAILABLE_CITATIONS>{refs_text}</AVAILABLE_CITATIONS>"
550
+
551
+ heading_instruction = f"Write the content for the section '{sec_title}'. DO NOT output the main heading '# {sec_title}' itself."
552
+ memory_block = f"\n【GLOBAL PAPER MEMORY】\n{agent._build_memory_context(sec_title, 2500)}\n" if agent._build_memory_context(sec_title, 2500) else ""
553
+
554
+ if hasattr(st.session_state, "rag_engine") and st.session_state.rag_engine is not None:
555
+ context_snippets = st.session_state.rag_engine.get_relevant_context(sec_title)
556
+ if context_snippets:
557
+ memory_block += f"\n【TARGETED RAG RESULTS】\n{context_snippets}\n"
558
 
559
+ lang_hint = "ru" if "Русский" in target_lang else ("kk" if "Қазақша" in target_lang else "en")
560
+ num_paragraphs = agent._calc_paragraph_count(adjusted_target, lang_hint)
561
+
562
+ adaptive_heading_rule = f"Use Level 2 (##) and Level 3 (###) subheadings." if adjusted_target > 800 else (f"Use Level 2 (##) subheadings." if adjusted_target > 400 else "")
563
+ table_rule = "[CRITICAL DATA FORMATTING] You MUST present ALL statistical data using STANDARD MARKDOWN TABLES." if sec["is_method_or_result"] else ""
564
 
565
+ length_directive = f"[LENGTH MANDATE - STRICT] Write between {int(adjusted_target * 0.9)} and {int(adjusted_target * 1.1)} words (approx {num_paragraphs} paragraphs).\n{adaptive_heading_rule}\n{table_rule}\n[FINAL CHECK] Do not output a word count statement."
566
 
567
  is_clone_target = False
568
  active_clone_key = None
 
570
  for target_key in clone_metadata.keys():
571
  target_words = set(re.findall(r'[a-zA-Z]{3,}', target_key.lower()))
572
  sec_words = set(re.findall(r'[a-zA-Z]{3,}', sec_title.lower()))
573
+ if "result" in target_words:
574
+ target_words.add("analysis")
575
+ if "result" in sec_words:
576
+ sec_words.add("analysis")
577
  if target_words & sec_words:
578
  is_clone_target = True
579
  active_clone_key = target_key
 
582
  if is_clone_target:
583
  skeleton_instruction = clone_metadata[active_clone_key]
584
  sys_prompt_m3 = "You are a top-tier academic ghostwriter executing a structural and stylistic clone."
585
+ integrated_context = f"Core differences: {context_differences}\n{memory_block}\nPrevious context tail: ...{context_tail}..."
586
  wiz_data_injection = f"USER ACTUAL DATA:\n{user_data_md}" if user_data_md and ("method" in sec_title.lower() or "result" in sec_title.lower() or "analysis" in sec_title.lower()) else "No data required."
587
+ base_user = clone_arch.build_rewrite_instruction(skeleton_instruction, sec_title, topic, integrated_context, wiz_data_injection) + f"\n\n{length_directive}\n{quarantine_rule}\n{heading_instruction}"
588
  else:
589
  extra_directives = []
590
+ lower_sec = sec_title.lower()
591
  golden_meta = agent.global_memory.get("core_golden_metadata", [])
592
 
593
  if golden_meta:
594
+ if any(kw in lower_sec for kw in ["introduction", "background"]):
595
+ extra_directives.append("【TARGETED DIRECTIVE】 Locate research gaps from [TARGETED INJECTION].")
596
+ elif any(kw in lower_sec for kw in ["literature", "theor", "hypothesis", "review"]):
597
+ extra_directives.append("【TARGETED DIRECTIVE】 Base hypotheses on theories from [TARGETED INJECTION].")
598
+ elif any(kw in lower_sec for kw in ["method", "measure", "data", "sample"]):
599
+ extra_directives.append("【TARGETED DIRECTIVE】 Adapt scale from [TARGETED INJECTION].")
600
+ elif any(kw in lower_sec for kw in ["result", "analysis"]):
601
+ extra_directives.append("【ZERO HALLUCINATION】 Translate <USER_ACTUAL_DATA> strictly.")
602
+ elif any(kw in lower_sec for kw in ["discussion", "conclusion", "implication"]):
603
+ extra_directives.append("【TARGETED DIRECTIVE】 Compare findings with [TARGETED INJECTION].")
604
+ else:
605
+ extra_directives.append(APP_CONFIG["deep_refinement_directive"])
606
  else:
607
+ if sec["is_method_or_result"]:
608
+ extra_directives.append(APP_CONFIG["methodological_defense_directive"])
609
+ extra_directives.append(APP_CONFIG["empirical_data_logic"])
610
+ if any(kw in lower_sec for kw in ["theory", "theoretical", "hypothesis", "introduction"]):
611
+ extra_directives.append(APP_CONFIG["deep_synthesis_directive"])
612
+ extra_directives.append(APP_CONFIG["hypothesis_development_directive"])
613
+ if "discussion" in lower_sec:
614
+ extra_directives.append(APP_CONFIG["discussion_depth_directive"])
615
+ if "conclusion" in lower_sec:
616
+ extra_directives.append(APP_CONFIG["conclusion_anchor_directive"])
617
 
618
  theory_injection = "\n\n".join(extra_directives)
619
  sys_prompt_m3 = APP_CONFIG["sys_academic_writer"] + "\n" + APP_CONFIG["deep_refinement_directive"]
620
  wiz_data_injection = f"\n<USER_ACTUAL_DATA>\n{user_data_md}\n</USER_ACTUAL_DATA>\n" if user_data_md and sec["is_method_or_result"] else ""
621
+ base_user = f"Generate section '{display_name}'.\n{length_directive}\n{quarantine_rule}\n{heading_instruction}\n{theory_injection}\n{memory_block}\nSynthesis: {synthesis_result}\n{wiz_data_injection}\nPrevious Tail: ...{context_tail}...\nIMPORTANT: Output final text."
 
622
 
623
+ if agent._count_tokens(sys_prompt_m3) + agent._count_tokens(base_user) > 60000:
624
+ base_user = agent._truncate_context(base_user, 60000 - agent._count_tokens(sys_prompt_m3))
625
+
626
+ engine_label = "🧬 克隆引擎" if is_clone_target else ("🎯 靶向注入" if golden_meta else "📚 标准引擎")
627
+ render_monitor(agent, f"English Drafting ({current_step+1}/{total_steps})", display_name, f"目标: {adjusted_target} ({engine_label})")
628
+ en_ph.info(f"🔄 正在生成高质量章节...")
629
 
630
  try:
631
+ en_draft_clean = re.sub(r'<think>.*?</think>', '', en_ph.write_stream(agent.stream_deepseek(sys_prompt_m3, base_user, max_tokens=6000)), flags=re.DOTALL).strip()
 
632
  en_draft_clean = re.sub(rf'(?im)^#\s*{re.escape(sec_title)}\s*\n+', '', en_draft_clean).strip()
633
+ if sec["is_first"]:
634
+ en_draft_clean = f"# {sec_title}\n\n{en_draft_clean}"
635
 
636
+ en_resolved = agent.resolve_citations(en_draft_clean, quarantine=is_quarantined)
637
  en_ph.markdown(en_resolved)
638
  full_draft_en += en_resolved + "\n\n"
639
 
 
643
 
644
  actual_words = len(en_draft_clean.split())
645
  accumulated_deviation += (actual_words - adjusted_target)
646
+ st.caption(f"📊 本章节实际字数: {actual_words} (目标: {adjusted_target}), 累计偏差: {accumulated_deviation:+d}")
647
+
648
  current_step += 1
649
+ pb.progress(min(15 + int((current_step / total_steps) * 80), 99), text=f"🇬🇧 {display_name} 英文完成...")
650
  except Exception as e:
651
+ en_ph.error("🚨 节点异常已熔断。")
652
+ st.error(f"异常: {e} | 💥 系统已保存断点,随时可恢复。")
653
  generation_aborted = True
654
  break
655
 
656
  render_monitor(agent, f"Translating ({current_step+1}/{total_steps})", display_name, "翻译中...")
657
  trans_ph.empty()
658
  try:
659
+ trans_chunk_clean = re.sub(r'<think>.*?</think>', '', trans_ph.write_stream(agent.stream_deepseek(f"Translate into {target_lang}. Preserve formatting and [CITE-XX].", f"TEXT:\n{en_draft_clean}", max_tokens=6000, model_name="deepseek-chat")), flags=re.DOTALL).strip()
660
+ trans_resolved = agent.resolve_citations(trans_chunk_clean, quarantine=is_quarantined)
 
661
  trans_ph.markdown(trans_resolved)
662
  full_draft_translated += trans_resolved + "\n\n"
663
 
664
  current_step += 1
665
+ pb.progress(min(15 + int((current_step / total_steps) * 80), 99), text=f"{target_flag} {display_name} 翻译完成...")
666
  except Exception as e:
667
+ trans_ph.error("🚨 翻译熔断。")
668
+ st.error(f"异常: {e}")
669
  generation_aborted = True
670
  break
671
 
 
675
  state["context_tail"] = context_tail
676
  state["agent_state"] = agent.export_state()
677
 
678
+ os.makedirs(WORKSPACE_DIR, exist_ok=True)
679
  with open(CHECKPOINT_FILE, "w", encoding="utf-8") as f:
680
  json.dump(state, f, ensure_ascii=False)
681
 
682
  if not generation_aborted and full_draft_en.strip():
683
+ pb.progress(96, text="📦 打包DOCX...")
684
  full_draft_en = clean_trailing_references(full_draft_en, ["Reference", "References", "Reference List"])
685
  full_draft_translated = clean_trailing_references(full_draft_translated, ["参考文献", "Reference", "References", "Reference List"])
686
 
687
+ total_words_en = len(full_draft_en.split())
688
+ target_words = int(words)
689
+ lower_limit = int(target_words * 0.95)
690
+ upper_limit = int(target_words * 1.05)
691
+ st.info(f"📊 全文总字数: {total_words_en} (目标区间: {lower_limit} - {upper_limit})")
692
+
693
+ if total_words_en > upper_limit:
694
+ conclusion_match = re.search(r'(#+\s*Conclusion.*?)(?=\n#+ |\Z)', full_draft_en, re.DOTALL | re.IGNORECASE)
695
+ if conclusion_match:
696
+ compressed_en = agent.call_deepseek("Condense the following conclusion by 15-20% preserving citations.", conclusion_match.group(1), max_tokens=1500)
697
+ if compressed_en:
698
+ full_draft_en = full_draft_en.replace(conclusion_match.group(1), compressed_en.strip())
699
+ compressed_trans = agent._translate_text(compressed_en, target_lang, max_tokens=2000)
700
+ if compressed_trans:
701
+ trans_match = re.search(rf'(#+\s*{"结论|Заключение|Қорытынды|Conclusion"}.*?)(?=\n#+ |\Z)', full_draft_translated, re.DOTALL | re.IGNORECASE)
702
+ if trans_match:
703
+ full_draft_translated = full_draft_translated.replace(trans_match.group(1), compressed_trans.strip())
704
+ else:
705
+ full_draft_translated += "\n\n" + compressed_trans.strip()
706
+ elif total_words_en < lower_limit:
707
+ expanded_en = agent.call_deepseek("Continue conclusion by adding 150 words (future research).", full_draft_en[-2000:], max_tokens=800)
708
+ if expanded_en:
709
+ full_draft_en += "\n\n" + expanded_en.strip()
710
+ expanded_trans = agent._translate_text(expanded_en, target_lang, max_tokens=1000)
711
+ if expanded_trans:
712
+ full_draft_translated += "\n\n" + expanded_trans.strip()
713
+
714
+ if len(agent.used_citations) < 50:
715
+ for tag in [t for t in agent.citation_map if t not in agent.used_citations][:50 - len(agent.used_citations)]:
716
+ agent.used_citations.add(tag)
717
+
718
+ apa_refs = "\n\n".join([f"- {ref}" for ref in sorted(list(set([agent.citation_map[t]['ref'] for t in sorted(list(agent.used_citations)) if t in agent.citation_map])))])
719
  full_draft_en += "\n\n# References\n" + apa_refs
720
  full_draft_translated += "\n\n# 参考文献\n" + apa_refs
721
 
722
  bj_time = datetime.now(timezone(timedelta(hours=8))).strftime("%Y%m%d_%H%M")
723
  zip_buffer = io.BytesIO()
724
  with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
725
+ zf.writestr(f"Research_EN_{bj_time}.docx", text_to_docx(full_draft_en).getvalue())
726
+ zf.writestr(f"Research_Trans_{bj_time}.docx", text_to_docx(full_draft_translated).getvalue())
727
  zip_buffer.seek(0)
728
  st.session_state.zip_buffer = zip_buffer.getvalue()
729
+ st.session_state.zip_filename = f"Draft_{bj_time}.zip"
730
 
731
+ try:
732
+ os.remove(CHECKPOINT_FILE)
733
+ except OSError:
734
+ pass
735
+
736
  pb.progress(100, text="✅ 完成")
737
+ render_monitor(agent, "完成", "✅", "文档已生成")
738
 
739
  except Exception as e:
740
  st.error(f"🚨 启动/恢复异常: {e}")