From edb3c1b27b1e731ff8e3c3dcc041609254dd7182 Mon Sep 17 00:00:00 2001
From: wyn <1074145239@qq.com>
Date: Thu, 25 Jun 2026 13:50:03 +0800
Subject: [PATCH 1/4] =?UTF-8?q?=E5=8F=82=E8=80=83=E6=96=87=E7=8C=AE?=
=?UTF-8?q?=E6=A0=A1=E5=AF=B9=EF=BC=8C=E6=9C=AC=E5=9C=B0=E5=A4=A7=E6=A8=A1?=
=?UTF-8?q?=E5=9E=8B=E6=8F=90=E7=A4=BA=E8=AF=8D=E8=AE=B0=E5=BD=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../service/ReferenceRelevanceLlmService.php | 609 ++++++++++++++++++
1 file changed, 609 insertions(+)
create mode 100644 application/common/service/ReferenceRelevanceLlmService.php
diff --git a/application/common/service/ReferenceRelevanceLlmService.php b/application/common/service/ReferenceRelevanceLlmService.php
new file mode 100644
index 00000000..90e1fdef
--- /dev/null
+++ b/application/common/service/ReferenceRelevanceLlmService.php
@@ -0,0 +1,609 @@
+url = trim((string)Env::get('promotion.promotion_llm_url', ''));
+ $this->model = trim((string)Env::get('promotion.promotion_llm_model', ''));
+ $this->apiKey = trim((string)Env::get('promotion.promotion_llm_api_key', ''));
+ $this->timeout = max(180, intval(Env::get('promotion.promotion_llm_timeout', 180)));
+ // 控制发送给 LLM 的上下文长度,降低单次推理耗时(可通过 env 覆盖)
+ $this->maxSectionChars = max(1500, intval(Env::get('promotion.relevance_llm_max_section_chars', 4500)));
+ $this->maxLocalContextChars = max(600, intval(Env::get('promotion.relevance_llm_max_local_context_chars', 1800)));
+ $this->maxReferChars = max(1500, intval(Env::get('promotion.relevance_llm_max_refer_chars', 3500)));
+ $this->maxAbstractChars = max(1500, intval(Env::get('promotion.relevance_llm_max_abstract_chars', 3500)));
+ }
+
+ /**
+ * @return array{results:array,request_failed?:bool,reason?:string}
+ */
+ public function checkRelevance($sectionText, $localContext, $referText, $abstractText = '', $citeGroupRefs = '')
+ {
+ $fallback = [
+ 'results' => [],
+ 'request_failed' => true,
+ 'reason' => 'LLM not configured or request failed',
+ ];
+ if ($this->url === '' || $this->model === '') {
+ return $fallback;
+ }
+
+ $sectionText = trim((string)$sectionText);
+ $localContext = trim((string)$localContext);
+ $referText = trim((string)$referText);
+ $abstractText = trim((string)$abstractText);
+ if ($sectionText === '' || $referText === '') {
+ return ['results' => [], 'reason' => 'Empty section or reference text'];
+ }
+
+ if (mb_strlen($sectionText) > $this->maxSectionChars) {
+ $sectionText = mb_substr($sectionText, 0, $this->maxSectionChars);
+ }
+ if (mb_strlen($localContext) > $this->maxLocalContextChars) {
+ $localContext = mb_substr($localContext, 0, $this->maxLocalContextChars);
+ }
+ if (mb_strlen($referText) > $this->maxReferChars) {
+ $referText = mb_substr($referText, 0, $this->maxReferChars);
+ }
+ if (mb_strlen($abstractText) > $this->maxAbstractChars) {
+ $abstractText = mb_substr($abstractText, 0, $this->maxAbstractChars);
+ }
+
+ $payload = [
+ 'model' => $this->model,
+ 'temperature' => 0,
+ 'messages' => [
+ ['role' => 'system', 'content' => $this->buildSystemPrompt()],
+ ['role' => 'user', 'content' => $this->buildUserPrompt($sectionText, $localContext, $referText, $abstractText, $citeGroupRefs)],
+ ],
+ ];
+
+ $content = $this->postChat($payload);
+ if ($content === null) {
+ $reason = $this->lastPostError !== '' ? $this->lastPostError : 'LLM request failed';
+ return array_merge($fallback, ['reason' => $reason]);
+ }
+
+ $parsed = $this->parseJson($content);
+ if ($parsed === null) {
+ return array_merge($fallback, ['reason' => 'LLM response JSON parse failed']);
+ }
+
+ $results = $this->normalizeResults($parsed, $citeGroupRefs, $localContext, $referText, $abstractText);
+ if (empty($results)) {
+ return array_merge($fallback, ['reason' => 'LLM returned empty or invalid results']);
+ }
+
+ return ['results' => $results];
+ }
+
+ private function buildSystemPrompt()
+ {
+ return <<<'PROMPT'
+你是一名护理、医学、生物医学与科研期刊的资深学术编辑,正在执行「参考文献主题相关性校对」。
+
+你的任务:判断【引用位置正文表述】与【对应编号参考文献】在主题、研究对象、疾病/场景/结局方向上是否相关,能否作为该处引用的合理来源。
+
+注意:这是「相关性」校对,侧重引用处具体 claim 与文献内容是否匹配;**不是**判断「是否同一疾病/同一领域」。
+
+==================================================
+【零、最硬规则(违反则输出无效)】
+1. **单条 relevance_score 只评价该编号文献单独**与引用处的关系;不得因联合组整体合理而抬高弱相关文献的单条分。
+2. **禁止「同病高分」**:正文与文献都涉及 CRC,不等于单条可给 0.85~0.92。
+ **但若引用处 claim 本身就是机制/通路/异质性/耐药/治疗挑战**,且**研究主语一致**(同一疾病/同一化合物/同一干预对象),文献(含摘要/清洗内容)讨论同病多通路、遗传改变、耐药等,应给 **0.65~0.78**,不得误降到 0.45。
+ **主语不一致时仍适用本条禁止高分**:引用处主语为化合物 X,文献却是其他植物/提取物/计算预测,即使提到 X 或相同通路名,也不得因此给 0.78+。
+3. 引用处若为**流行病学/负担类 claim**(most common、incidence、mortality、burden、全球高发等):
+ - 机制研究、分子通路、细胞增殖/迁移、血管生成等**原始研究** → 单条通常 **0.45 或更低**,`is_relevant=0`,`minimal_relevance`
+ - 不得因摘要提到 colorectal cancer 就给 0.92
+ - 仅当文献为流行病学综述/公共卫生研究,或明确讨论发病率、死亡率、疾病负担时,单条才可 **0.85~0.92**
+4. **联合分写在 combined_relevance_score**,与单条分必须可分离(例如 [1,2] 时文献1=0.45、文献2=0.92、联合=0.92)。
+5. **「来源/化学分类」型句子**(naturally occurring、pentacyclic triterpenoid、found in fruits/vegetables/medicinal plants、并列举具体植物学名):
+ - 先判文献类型:来源综述 / 生物活性综述 最适合;**抗癌治疗综述**对「来源分布」claim 通常仅 **0.65**
+ - 单篇可差异化打分(如 0.92 / 0.92 / 0.65),**不得**因联合而三篇都给高分
+ - 若原句含**具体列举项**(如多个植物学名),而材料未逐一核实全部学名,联合分通常 **≤0.85**(不得给 0.98)
+6. **多要素综括句**(一句同时塞入:药学/研究兴趣 + 大量前临床研究 + 多种活性[抗炎/抗氧化/抗癌等] + 多个癌种/对象列举):
+ - 单篇即使是综述,通常仅 partially_related ~ near-direct(**0.78~0.86**),**不轻易给 0.92**(单篇难逐项覆盖全部要素)
+ - **联合分是整句覆盖度评估,可低于最高单条分**:若整句要素需多篇拼合、且含作者整合概括,联合通常 **0.72~0.78(partially_related)**,不给 0.85+
+7. **联合分不是「取最高单条分」**:当各单篇都只覆盖整句一部分、需互补拼合时,联合分应反映「整句作为一个整体被支撑的完整度」,**允许低于任何一篇单条分**。
+8. **主语/研究对象层级必须对齐**:引用处主语为某化合物/分子(如「X has been demonstrated…」)时,文献核心对象须为 **X 本身**或以 X 为核心的实验/综述。
+ - **植物提取物/混合物**研究、**其他物种/其他植物**的计算预测、成分表中顺带出现 X → 通常 **0.45 或更低**
+ - **关键:提取物即使 X 含量很高(如 50%+)且显示了抗癌/凋亡活性,活性归因于提取物整体而非 X 单体单独验证 → 仍属 weakly_related(≤0.45)、`minimal_relevance`**,不得评为 supplementary_relevance/0.78
+ - 只有当文献**针对 X 单体单独做了验证**(X monomer 处理、X 单独剂量效应等)时,主语才算对齐,方可进入 0.65+
+ - **不得**因摘要/讨论出现与引用句相同的通路名、凋亡、抗癌等词就给 0.78+
+9. **证据层级与 demonstrated / mechanistically**:
+ - 本文实验结果或针对 X 的系统综述 > 计算预测/混合成分推测
+ - **讨论(Discussion)转引他人关于 X 的机制总结 ≠ 该文自身证据**;据此最多 **0.45~0.65**,不得评为 highly_related
+ - in silico / computational prediction 不足以支撑「has been demonstrated to mechanistically…」式强语气 claim 的高分
+10. **点名通路/功能结局须逐项核对**:原句逐条列举通路(如 PI3K/AKT、MAPK、NF-κB)或结局(增殖、凋亡、血管生成、炎症信号等)时,**每一项单独核对是否在本文证据中成立**(非仅背景提及)。
+ - 讨论转述既往文献 ≠ 本文证明该项
+ - 缺原句任一点名项(如 angiogenesis)→ 单条通常 **不得 0.78+**
+ - **「覆盖部分结局」不足以进入 0.78**:原句点名了多条通路 + 多个结局,文献仅命中其中 1~2 个结局(如仅凋亡/增殖),且**点名通路在本文结果中全部缺失(仅讨论转引)**或主语层级不对 → 单条 **限 0.45(weakly_related / minimal_relevance)**,不得给 0.65~0.78
+ - 仅同领域沾边 1–2 项、主语或机制层级不对 → **0.45**
+ - **进入 0.65~0.78 的前提**:主语对齐(X 单体)+ 本文自身结果命中原句点名通路/结局的多数项;几乎全部明确对应 → **0.85+**
+
+==================================================
+【一、必须先拆解 claim】
+从【本引用位置附近上下文】中提炼最小主张单元(Claim A, Claim B…),**不要**把整句笼统归为「大概讲抗癌」。例如:
+- **主语/研究对象**(化合物单体 vs 植物提取物 vs 其他物种;是否「X has been demonstrated」)
+- **证据语气与层级**(demonstrated / mechanistically vs predict / suggest;本文结果 vs 讨论转引)
+- 疾病流行病学(高发、死亡率)
+- **点名通路/分子机制**(PI3K/AKT、MAPK、NF-κB 等,须逐项)
+- **点名功能结局**(抑制增殖、凋亡、血管生成、炎症信号等,须逐项)
+- 治疗/干预现状
+- **化合物化学类别**(如 pentacyclic triterpenoid)
+- **天然来源分布**(fruits / vegetables / medicinal plants)
+- **具体列举项**(植物学名、药名、基因名等,须逐项核对)
+
+==================================================
+【二、逐篇文献单独判断(每条 result 对应一个 reference_no)】
+对 cite_group_refs 中的每一篇文献,单独输出:
+- 该文献与引用处哪些 claim 主题相关、哪些不相关(含具体列举项是否覆盖)
+- 文献类型是否匹配引用用途(来源综述 / 生物活性综述 / 机制研究 / 流行病学综述 / 抗癌治疗综述等)
+- relevance_score:只能使用 0.98 / 0.92 / 0.85 / 0.78 / 0.65 / 0.45 / 0.25 / 0.15
+- relevance_level:highly_related | partially_related | weakly_related | unrelated
+- is_relevant:score>=0.65 为 1,否则 0
+- relevance_role:
+ - primary_relevance:该文献是引用处主题的主要相关来源
+ - supplementary_relevance:部分相关、补充性
+ - minimal_relevance:仅边缘/背景沾边
+ - no_meaningful_relevance:与引用处核心表述基本无关
+- reason:中文,须写明:①文献类型与**核心研究对象** ②**本文自身证据**覆盖了哪些 claim / 哪些未覆盖(含点名通路、功能结局、具体列举项;讨论转引须标明) ③**若仅为同主题不同 claim 或主语层级不对须明确写出** ④为何此分值而非更高或更低
+
+主语/层级不对 → 单条 **0.45**,不得因讨论提及相同通路给 0.78:
+引用处 claim 为「化合物 X 经 PI3K/AKT 等机制 demonstrated…」,文献为其他植物提取物或计算预测、仅在讨论转引他人 X 机制 → 0.45,weakly_related,is_relevant=0。
+
+机制文引用流行病学句 → 单条 **0.45**,不得 0.92:
+文献为 CRC 机制研究,引用处 claim 为全球高发/死亡率,文献无流行病学数据 → 0.45,minimal_relevance,is_relevant=0。
+
+==================================================
+【三、联合引用 combined_*(同一 cite_group_refs 内各行必须一致)】
+当 cite_group_refs 为 "1,2" 等多篇时,除逐篇判断外,必须给出引用组整体结论:
+- 这些文献合起来,是否足以支撑/匹配该引用位置的整体表述?
+- combined_relevance_score:八档固定分值之一,**不是单条平均分**
+- 若一篇已强相关、其余仅弱补充,联合分可接近主相关文献,但**不必等于最高单条分**
+- 若原句含具体列举项(学名等)且材料未逐一核实,联合分通常 **0.85**,不给 0.98
+- 若核心 claim 无任何文献明确覆盖,联合分不能虚高
+- 多篇联合仍缺主语对齐、缺原句点名通路/结局、或主要靠讨论转引 → 联合分通常 **≤0.45~0.65**,不得因单篇讨论出现相同关键词给到 0.78+
+- combined_is_relevant:combined_relevance_score>=0.65 为 1
+- combined_relevance_level:与 combined 分数对应的等级
+- combined_reason:中文综合结论,说明各文献分工(如「文献2综述覆盖流行病学主 claim,文献1仅机制补充」)及最终分值理由
+
+单条引用时:combined_* 与单条一致,combined_reason 可写「」。
+
+==================================================
+【四、评分与等级对照】
+0.98 / 0.92 / 0.85 = highly_related
+文献直接支持整句主旨,大部分关键要素都在文中明确出现
+0.78 / 0.65 = partially_related
+文献只支撑其中一部分,或支撑方式偏间接
+0.45 = weakly_related
+只是同领域文献,但与句子事实对应很弱
+0.25 / 0.15 = unrelated
+基本不支撑该句
+≤0.15 = not_support
+不支撑
+
+==================================================
+【五、输出 JSON(仅 JSON,无 markdown)】
+{
+ "results": [
+ {
+ "reference_no": 1,
+ "cite_group_refs": "1,2",
+ "is_relevant": 0,
+ "relevance_score": 0.45,
+ "relevance_level": "weakly_related",
+ "relevance_role": "minimal_relevance",
+ "reason": "中文单条结论",
+ "combined_is_relevant": 1,
+ "combined_relevance_score": 0.92,
+ "combined_relevance_level": "highly_related",
+ "combined_reason": "中文联合结论"
+ },
+ {
+ "reference_no": 2,
+ "cite_group_refs": "1,2",
+ ...
+ }
+ ]
+}
+PROMPT;
+ }
+
+ private function buildUserPrompt($sectionText, $localContext, $referText, $abstractText, $citeGroupRefs)
+ {
+ $parts = ["【正文节 t_article_main】\n" . $sectionText];
+ if (trim((string)$citeGroupRefs) !== '') {
+ $mode = strpos($citeGroupRefs, ',') !== false ? '联合引用' : '单独引用';
+ $parts[] = "【引用文献组 cite_group_refs】{$citeGroupRefs}({$mode})";
+ }
+ if ($localContext !== '') {
+ $parts[] = "【本引用位置附近上下文(优先据此拆解 claim)】\n" . $localContext;
+ }
+ $parts[] = "【参考文献书目(按编号)】\n" . $referText;
+ if ($abstractText !== '') {
+ $parts[] = "【文献摘要/清洗后内容(Europe PMC·PubMed·Crossref·PDF)】\n" . $abstractText;
+ }
+ $parts[] = '请先拆解最小主张单元(主语层级、证据来源、点名通路/结局逐项核对),判断每篇文献类型与**本文自身证据**,再**逐篇独立**给出单条 relevance_score(讨论转引、提取物/计算预测不得抬高;弱相关文献不得因联合而高分),最后给出 combined_*。仅输出 results 数组 JSON。';
+
+ return implode("\n\n", $parts);
+ }
+
+ private function normalizeResults(array $parsed, $defaultCiteGroupRefs, $localContext = '', $referText = '', $abstractText = '')
+ {
+ $rows = [];
+ if (isset($parsed['results']) && is_array($parsed['results'])) {
+ $rows = $parsed['results'];
+ } elseif (isset($parsed['reference_no']) || isset($parsed['relevance_score'])) {
+ $rows = [$parsed];
+ }
+
+ $bands = $this->getScoreBands();
+ $localContext = trim((string)$localContext);
+ $referText = trim((string)$referText);
+ $abstractText = trim((string)$abstractText);
+
+ $out = [];
+ foreach ($rows as $item) {
+ if (!is_array($item)) {
+ continue;
+ }
+ $refNo = intval(isset($item['reference_no']) ? $item['reference_no'] : 0);
+ if ($refNo <= 0) {
+ continue;
+ }
+
+ $score = $this->snapScore(floatval(isset($item['relevance_score']) ? $item['relevance_score'] : 0), $bands);
+ $isRelevant = $score >= 0.65 - 0.001;
+ if (array_key_exists('is_relevant', $item)) {
+ $isRelevant = $this->boolVal($item['is_relevant']);
+ }
+
+ $level = $this->levelFromScore($score, isset($item['relevance_level']) ? $item['relevance_level'] : '');
+ $role = $this->normalizeRelevanceRole(isset($item['relevance_role']) ? $item['relevance_role'] : '');
+ $reason = $this->cleanReason(isset($item['reason']) ? $item['reason'] : '');
+
+ list($score, $level, $isRelevant, $role) = $this->enforceSingleReferenceConsistency(
+ $score,
+ $level,
+ $isRelevant,
+ $role,
+ $bands
+ );
+
+ $combinedScore = $this->snapScore(
+ floatval(isset($item['combined_relevance_score']) ? $item['combined_relevance_score'] : $score),
+ $bands
+ );
+ $combinedRelevant = $combinedScore >= 0.65 - 0.001;
+ if (array_key_exists('combined_is_relevant', $item)) {
+ $combinedRelevant = $this->boolVal($item['combined_is_relevant']);
+ }
+
+ $combinedLevel = $this->levelFromScore(
+ $combinedScore,
+ isset($item['combined_relevance_level']) ? $item['combined_relevance_level'] : ''
+ );
+ list($combinedScore, $combinedLevel, $combinedRelevant) = $this->enforceCombinedConsistency(
+ $combinedScore,
+ $combinedLevel,
+ $combinedRelevant,
+ $bands
+ );
+
+ $citeGroupRefs = trim((string)(isset($item['cite_group_refs']) ? $item['cite_group_refs'] : $defaultCiteGroupRefs));
+ if ($citeGroupRefs === '' && $defaultCiteGroupRefs !== '') {
+ $citeGroupRefs = trim((string)$defaultCiteGroupRefs);
+ }
+
+ $out[] = [
+ 'reference_no' => $refNo,
+ 'cite_group_refs' => $citeGroupRefs,
+ 'is_relevant' => $isRelevant ? 1 : 0,
+ 'relevance_score' => $score,
+ 'relevance_level' => $level,
+ 'relevance_role' => $role,
+ 'reason' => $reason,
+ 'combined_is_relevant' => $combinedRelevant ? 1 : 0,
+ 'combined_relevance_score' => $combinedScore,
+ 'combined_relevance_level' => $combinedLevel,
+ 'combined_reason' => $this->cleanReason(isset($item['combined_reason']) ? $item['combined_reason'] : ''),
+ ];
+ }
+
+ $out = $this->syncCombinedFieldsAcrossGroup($out);
+
+ return $out;
+ }
+
+ private function enforceSingleReferenceConsistency($score, $level, $isRelevant, $role, array $bands)
+ {
+ $score = floatval($score);
+ if ($role === 'no_meaningful_relevance') {
+ if ($score > 0.25) {
+ $score = 0.25;
+ }
+ $level = 'unrelated';
+ $isRelevant = false;
+ } elseif ($role === 'minimal_relevance') {
+ if ($score > 0.45) {
+ $score = 0.45;
+ }
+ $level = 'weakly_related';
+ $isRelevant = false;
+ } elseif ($role === 'supplementary_relevance') {
+ if ($score > 0.78) {
+ $score = 0.78;
+ }
+ $level = $this->levelFromScore($score, $level);
+ } elseif ($role === 'primary_relevance') {
+ if ($score < 0.85) {
+ $score = 0.85;
+ }
+ $isRelevant = true;
+ $level = $this->levelFromScore($score, $level);
+ }
+
+ if ($level === 'weakly_related' && $score > 0.45) {
+ $score = 0.45;
+ $isRelevant = false;
+ } elseif ($level === 'unrelated' && $score > 0.25) {
+ $score = 0.25;
+ $isRelevant = false;
+ } elseif ($level === 'highly_related' && $score < 0.85) {
+ $score = 0.85;
+ $isRelevant = true;
+ } elseif ($level === 'partially_related') {
+ if ($score > 0.78) {
+ $score = 0.78;
+ }
+ if ($score < 0.65) {
+ $score = 0.65;
+ }
+ $isRelevant = true;
+ }
+
+ if (!$isRelevant && $score >= 0.65) {
+ $score = 0.45;
+ $level = 'weakly_related';
+ }
+ if ($isRelevant && $score < 0.65) {
+ $score = 0.65;
+ $level = 'partially_related';
+ }
+
+ $score = $this->snapScore($score, $bands);
+ $level = $this->levelFromScore($score, $level);
+
+ return [$score, $level, $isRelevant, $role];
+ }
+
+ private function enforceCombinedConsistency($combinedScore, $combinedLevel, $combinedRelevant, array $bands)
+ {
+ $combinedScore = $this->snapScore(floatval($combinedScore), $bands);
+ $combinedLevel = $this->levelFromScore($combinedScore, $combinedLevel);
+ $combinedRelevant = $combinedScore >= 0.65 - 0.001;
+
+ return [$combinedScore, $combinedLevel, $combinedRelevant];
+ }
+
+ private function syncCombinedFieldsAcrossGroup(array $out)
+ {
+ $groups = [];
+ foreach ($out as $idx => $row) {
+ $key = (string)$row['cite_group_refs'];
+ if ($key === '') {
+ $key = 'ref:' . $row['reference_no'];
+ }
+ $groups[$key][] = $idx;
+ }
+
+ foreach ($groups as $indices) {
+ if (count($indices) <= 1) {
+ continue;
+ }
+ $bestIdx = $indices[0];
+ $bestScore = floatval($out[$bestIdx]['combined_relevance_score']);
+ foreach ($indices as $idx) {
+ $s = floatval($out[$idx]['combined_relevance_score']);
+ if ($s >= $bestScore) {
+ $bestScore = $s;
+ $bestIdx = $idx;
+ }
+ }
+ $src = $out[$bestIdx];
+ foreach ($indices as $idx) {
+ $out[$idx]['combined_is_relevant'] = intval($src['combined_is_relevant']);
+ $out[$idx]['combined_relevance_score'] = floatval($src['combined_relevance_score']);
+ $out[$idx]['combined_relevance_level'] = (string)$src['combined_relevance_level'];
+ $out[$idx]['combined_reason'] = (string)$src['combined_reason'];
+ }
+ }
+
+ return $out;
+ }
+
+ private function getScoreBands()
+ {
+ return [0.15, 0.25, 0.45, 0.65, 0.78, 0.85, 0.92, 0.98];
+ }
+
+ private function snapScore($score, array $bands)
+ {
+ foreach ($bands as $band) {
+ if (abs($score - $band) < 0.001) {
+ return $band;
+ }
+ }
+ $nearest = $bands[0];
+ $minDiff = abs($score - $nearest);
+ foreach ($bands as $band) {
+ $diff = abs($score - $band);
+ if ($diff < $minDiff) {
+ $minDiff = $diff;
+ $nearest = $band;
+ }
+ }
+
+ return $nearest;
+ }
+
+ private function levelFromScore($score, $levelHint = '')
+ {
+ $levelHint = strtolower(trim((string)$levelHint));
+ $allowed = ['highly_related', 'partially_related', 'weakly_related', 'unrelated'];
+ if (in_array($levelHint, $allowed, true)) {
+ return $levelHint;
+ }
+ $aliases = [
+ 'highly_related' => ['highly_related', 'high_related', 'strong_related', 'strong_relevance'],
+ 'partially_related' => ['partially_related', 'partial_related', 'moderate_related'],
+ 'weakly_related' => ['weakly_related', 'weak_related', 'low_related', 'insufficient'],
+ 'unrelated' => ['unrelated', 'not_related', 'irrelevant', 'no_meaningful_relevance'],
+ ];
+ foreach ($aliases as $canonical => $list) {
+ if (in_array($levelHint, $list, true)) {
+ return $canonical;
+ }
+ }
+ $score = floatval($score);
+ if ($score >= 0.85) {
+ return 'highly_related';
+ }
+ if ($score >= 0.65) {
+ return 'partially_related';
+ }
+ if ($score >= 0.45) {
+ return 'weakly_related';
+ }
+
+ return 'unrelated';
+ }
+
+ private function normalizeRelevanceRole($role)
+ {
+ $role = strtolower(trim((string)$role));
+ $map = [
+ 'primary_relevance' => ['primary_relevance', 'primary_support', 'primary'],
+ 'supplementary_relevance' => ['supplementary_relevance', 'supplementary_support', 'supplementary'],
+ 'minimal_relevance' => ['minimal_relevance', 'minimal_support', 'minimal'],
+ 'no_meaningful_relevance' => ['no_meaningful_relevance', 'no_meaningful_support', 'none'],
+ ];
+ foreach ($map as $canonical => $aliases) {
+ if ($role === $canonical || in_array($role, $aliases, true)) {
+ return $canonical;
+ }
+ }
+
+ return 'no_meaningful_relevance';
+ }
+
+ private function cleanReason($reason)
+ {
+ $reason = trim(preg_replace('/\s+/u', ' ', (string)$reason));
+ return mb_substr($reason, 0, 2000);
+ }
+
+ private function boolVal($v)
+ {
+ if (is_bool($v)) {
+ return $v;
+ }
+ if (is_numeric($v)) {
+ return intval($v) !== 0;
+ }
+ $s = strtolower(trim((string)$v));
+ return in_array($s, ['1', 'true', 'yes', 'y'], true);
+ }
+
+ private function postChat(array $payload)
+ {
+ $this->lastPostError = '';
+ try {
+ $ch = curl_init();
+ curl_setopt($ch, CURLOPT_URL, $this->url);
+ curl_setopt($ch, CURLOPT_POST, true);
+ curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE));
+ curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+ curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, min(15, $this->timeout));
+ curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
+ $headers = ['Content-Type: application/json'];
+ if ($this->apiKey !== '') {
+ $headers[] = 'Authorization: Bearer ' . $this->apiKey;
+ }
+ curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
+ $raw = curl_exec($ch);
+ if ($raw === false) {
+ $this->lastPostError = 'LLM curl error: ' . curl_error($ch);
+ \think\Log::warning('ReferenceRelevanceLlm: ' . $this->lastPostError);
+ curl_close($ch);
+ return null;
+ }
+ $httpCode = intval(curl_getinfo($ch, CURLINFO_HTTP_CODE));
+ curl_close($ch);
+ if ($httpCode < 200 || $httpCode >= 300) {
+ $snippet = mb_substr(trim((string)$raw), 0, 200);
+ $this->lastPostError = 'LLM HTTP ' . $httpCode . ($snippet !== '' ? ': ' . $snippet : '');
+ \think\Log::warning('ReferenceRelevanceLlm: ' . $this->lastPostError);
+ return null;
+ }
+ $data = json_decode($raw, true);
+ if (!is_array($data)) {
+ $this->lastPostError = 'LLM response is not valid JSON';
+ return null;
+ }
+ if (isset($data['choices'][0]['message']['content'])) {
+ return (string)$data['choices'][0]['message']['content'];
+ }
+ if (isset($data['content'])) {
+ return (string)$data['content'];
+ }
+ $this->lastPostError = 'LLM response missing content field';
+ } catch (\Exception $e) {
+ $this->lastPostError = 'LLM exception: ' . $e->getMessage();
+ \think\Log::warning('ReferenceRelevanceLlm: ' . $this->lastPostError);
+ }
+
+ return null;
+ }
+
+ private function parseJson($raw)
+ {
+ $raw = trim((string)$raw);
+ if ($raw === '') {
+ return null;
+ }
+ $raw = preg_replace('/^```[a-zA-Z]*\s*|```$/m', '', $raw);
+ $raw = trim($raw);
+ $decoded = json_decode($raw, true);
+ if (is_array($decoded)) {
+ return $decoded;
+ }
+ if (preg_match('/\{[\s\S]*\}/', $raw, $m)) {
+ $decoded = json_decode($m[0], true);
+ if (is_array($decoded)) {
+ return $decoded;
+ }
+ }
+
+ return null;
+ }
+}
From 6fdc4efb6f88bb95b3119689e40fd30b1ed0aa2c Mon Sep 17 00:00:00 2001
From: wyn <1074145239@qq.com>
Date: Mon, 29 Jun 2026 10:23:27 +0800
Subject: [PATCH 2/4] =?UTF-8?q?=E5=8F=82=E8=80=83=E6=96=87=E7=8C=AE?=
=?UTF-8?q?=E6=A0=A1=E5=AF=B9=E5=8D=87=E7=BA=A7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
application/api/controller/Base.php | 101 +
application/api/controller/Preaccept.php | 45 +-
application/api/controller/References.php | 264 ++-
application/command.php | 1 +
application/common/PubmedService.php | 62 +
application/common/ReferenceCheckService.php | 1721 ++++++++++++++---
application/common/mq/RabbitMqConfig.php | 6 +
.../common/mq/ReferenceCheckArticleWorker.php | 4 +
application/common/service/LLMService.php | 843 ++++++--
.../service/ReferenceRelevanceLlmService.php | 6 +
application/extra/rabbitmq.php | 7 +
11 files changed, 2680 insertions(+), 380 deletions(-)
diff --git a/application/api/controller/Base.php b/application/api/controller/Base.php
index 380066a2..6d76cb01 100644
--- a/application/api/controller/Base.php
+++ b/application/api/controller/Base.php
@@ -1178,6 +1178,107 @@ class Base extends Controller
return $ids;
}
+ /**
+ * 解析方括号引用内层(如 1,2 / 3-5),展开为文献序号列表。
+ *
+ * @return int[]
+ */
+ protected function expandCitationBracketNumbers(string $referencePart): array
+ {
+ $referencePart = trim($referencePart);
+ if ($referencePart === '') {
+ return [];
+ }
+ $referencePart = str_replace(
+ [',', '–', '—', '−', '‐', '‑'],
+ [',', '-', '-', '-', '-', '-'],
+ $referencePart
+ );
+ $out = [];
+ $segments = preg_split('/\s*,\s*/', $referencePart);
+ foreach ($segments as $seg) {
+ $seg = trim((string)$seg);
+ if ($seg === '') {
+ continue;
+ }
+ $seg = str_replace(['–', '—', '−', '‐', '‑'], '-', $seg);
+ if (preg_match('/^(\d+)\s*-\s*(\d+)$/', $seg, $m)) {
+ $a = intval($m[1]);
+ $b = intval($m[2]);
+ if ($a > $b) {
+ $t = $a;
+ $a = $b;
+ $b = $t;
+ }
+ for ($i = $a; $i <= $b; $i++) {
+ $out[] = $i;
+ }
+ } else {
+ $n = intval($seg);
+ if ($n > 0) {
+ $out[] = $n;
+ }
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * 从正文片段提取被引用的文献序号(reference_no = index+1)。
+ * 兼容 与 [n] / [n] 两种形态。
+ *
+ * @return int[]
+ */
+ protected function extractCitationRefNosFromMainContent(string $text, int $pArticleId = 0): array
+ {
+ if ($text === '') {
+ return [];
+ }
+
+ $nos = [];
+
+ $pReferIds = $this->extractMyciteIds($text);
+ if (!empty($pReferIds) && $pArticleId > 0) {
+ $refers = Db::name('production_article_refer')
+ ->where('p_article_id', $pArticleId)
+ ->whereIn('p_refer_id', $pReferIds)
+ ->where('state', 0)
+ ->field('p_refer_id,index')
+ ->select();
+ $idToNo = [];
+ foreach ($refers as $row) {
+ $idToNo[intval($row['p_refer_id'])] = intval($row['index']) + 1;
+ }
+ foreach ($pReferIds as $pid) {
+ if (isset($idToNo[$pid])) {
+ $nos[] = $idToNo[$pid];
+ }
+ }
+ }
+
+ if (preg_match_all('/(?:<\s*blue[^>]*>)?\[([^\]]+)\](?:<\/\s*blue\s*>)?/iu', $text, $m)) {
+ foreach ($m[1] as $inner) {
+ $innerNorm = str_replace(
+ [',', '–', '—', '−', '‐', '‑'],
+ [',', '-', '-', '-', '-', '-'],
+ trim((string)$inner)
+ );
+ if (!preg_match('/^[\d\s,\-]+$/u', $innerNorm)) {
+ continue;
+ }
+ foreach ($this->expandCitationBracketNumbers($innerNorm) as $n) {
+ if ($n > 0) {
+ $nos[] = $n;
+ }
+ }
+ }
+ }
+
+ $nos = array_values(array_unique($nos));
+ sort($nos, SORT_NUMERIC);
+ return $nos;
+ }
+
/**
* table_data:二维数组 JSON [[{text,colspan,rowspan},...],...];支持双重 JSON 字符串编码。
*
diff --git a/application/api/controller/Preaccept.php b/application/api/controller/Preaccept.php
index 79794434..9f89b31d 100644
--- a/application/api/controller/Preaccept.php
+++ b/application/api/controller/Preaccept.php
@@ -7,7 +7,7 @@ use think\Env;
use think\Queue;
use think\Validate;
use app\common\CrossrefService;
-use app\common\ReferenceCheckService;
+use app\common\ReferenceRelevanceCheckService;
class Preaccept extends Base
{
@@ -27,7 +27,7 @@ class Preaccept extends Base
return;
}
try {
- (new ReferenceCheckService())->clearArticleChecksByPArticleId($pArticleId);
+ (new ReferenceRelevanceCheckService())->clearArticleChecksByPArticleId($pArticleId);
} catch (\Exception $e) {
\think\Log::error(
'resetArticleChecksOnReferChange[' . $sourceTag . '] p_article_id='
@@ -1220,6 +1220,14 @@ class Preaccept extends Base
$insert['ctime'] = time();
$this->article_main_log_obj->insert($insert);
+// $articleId = intval($am_info['article_id']);
+// $amId = intval($data['am_id']);
+//
+// // 本段引用集合变化(如 10,11 → 11,12)时仅清空该 am_id 下的校对明细
+// if ($this->hasMainCitationChange($old_content, $new_raw_content, $articleId)) {
+// $this->clearMainChecksOnCitationChange($articleId, $amId);
+// }
+
// 判断是否存在“引用删除”(新 content 相对旧 content 缺少 )
$hasCitationDeletion = $this->hasMyciteDeletion($old_content, $new_raw_content);
@@ -1245,6 +1253,39 @@ class Preaccept extends Base
//返回更新数据 20260119 end
}
+ /**
+ * 正文单节保存后,仅清空该 am_id 下已有的引用校对明细(按 article_id 定位)。
+ */
+ private function clearMainChecksOnCitationChange(int $articleId, int $amId)
+ {
+ if ($articleId <= 0 || $amId <= 0) {
+ return;
+ }
+ try {
+ (new ReferenceCheckService())->clearChecksByAmId($articleId, $amId);
+ } catch (\Exception $e) {
+ \think\Log::error(
+ 'clearMainChecksOnCitationChange article_id=' . $articleId
+ . ' am_id=' . $amId . ' ' . $e->getMessage()
+ );
+ }
+ }
+
+ /**
+ * 本段正文引用集合是否变化(增删改任一即 true)。
+ * old 多为库内 [n],new 多为编辑器提交的 。
+ */
+ private function hasMainCitationChange(string $oldContent, string $newContent, int $articleId): bool
+ {
+ $pArticleId = intval(Db::name('production_article')
+ ->where('article_id', $articleId)
+ ->whereIn('state', [0, 2])
+ ->value('p_article_id'));
+ $oldNos = $this->extractCitationRefNosFromMainContent($oldContent, $pArticleId);
+ $newNos = $this->extractCitationRefNosFromMainContent($newContent, $pArticleId);
+ return $oldNos !== $newNos;
+ }
+
/**
* 是否发生 删除(new 相对 old 少了任意引用 id)
*/
diff --git a/application/api/controller/References.php b/application/api/controller/References.php
index 331edd62..ea539ed6 100644
--- a/application/api/controller/References.php
+++ b/application/api/controller/References.php
@@ -12,6 +12,8 @@ use think\Db;
use think\Env;
use think\Queue;
use app\common\ReferenceCheckService;
+use app\common\ReferenceRelevanceCheckService;
+use app\common\DbReconnectHelper;
/**
* @title 参考文献
* @description 相关方法汇总
@@ -1309,11 +1311,195 @@ class References extends Base
}
return json_encode(['status' => 8,'msg' => 'fail']);
}
+ // ============================================================
+ // 参考文献「主题相关性」校对(独立模块,RabbitMQ 链式消费)
+ // 表:t_article_reference_relevance_check_result / t_article_reference_relevance_check_batch
+ // 消费:php think reference_relevance:mq-consume
+ // ============================================================
+
/**
- * 参考文献第一次校对
+ * 启动整篇参考文献相关性校对
+ * POST: p_article_id(必填)
+ *
+ * 文献摘要/内容优先读 t_production_article_refer.abstract_text、refer_content_cleaned;
+ * 二者都为空时在校对执行阶段抓取并回写 refer 表,校对时始终从 refer 表读取。
+ */
+ public function allReferenceCheckAI()
+ {
+ $aParam = $this->request->post();
+ $iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
+ if ($iPArticleId <= 0) {
+ return jsonError('Please select an article');
+ }
+
+ $aProductionArticle = Db::name('production_article')
+ ->field('p_article_id,article_id')
+ ->where(['p_article_id' => $iPArticleId, 'state' => ['in', [0, 2]]])
+ ->find();
+ if (empty($aProductionArticle)) {
+ return jsonError('No articles found');
+ }
+ if ($this->checkReferStatus($iPArticleId) == 0) {
+ return jsonError('Please correct the reference content before running the check.');
+ }
+
+ $existing = Db::name('article_reference_relevance_check_result')
+ ->where('p_article_id', $iPArticleId)
+ ->count();
+ if (intval($existing) > 0) {
+ return jsonError('This article already has relevance check records. Use referenceRelevanceCheckResetAI to rerun.');
+ }
+
+ try {
+ DbReconnectHelper::ensure();
+ $result = (new ReferenceRelevanceCheckService())->enqueueByPArticle($aProductionArticle);
+ if (empty($result['check_ids'])) {
+ return jsonError('No reference citations were found in the article.');
+ }
+ return jsonSuccess($result);
+ } catch (\Exception $e) {
+ return jsonError($e->getMessage());
+ }
+ }
+
+ /**
+ * 相关性校对进度
+ * POST: p_article_id
+ */
+ public function referenceRelevanceCheckProgressAI()
+ {
+ $aParam = $this->request->post();
+ $iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
+ if ($iPArticleId <= 0) {
+ return jsonError('p_article_id is required');
+ }
+ try {
+ $result = (new ReferenceRelevanceCheckService())->getProgressByPArticleId($iPArticleId);
+ return jsonSuccess($result);
+ } catch (\Exception $e) {
+ return jsonError($e->getMessage());
+ }
+ }
+
+ /**
+ * 按 p_article_id 查整篇文章相关性校对总状态(用于前端按钮分流)
+ *
+ * POST/GET: p_article_id(必填)
+ *
+ * 返回 status:0=未校对 1=校对中 2=校对完成
+ * 计数维度为参考文献(按 reference_no 分组),与 referenceRelevanceCheckProgressAI 一致。
+ */
+ public function referenceRelevanceCheckArticleStatusAI()
+ {
+ $aParam = $this->request->post();
+ if (empty($aParam)) {
+ $aParam = $this->request->param();
+ }
+
+ $iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
+ if ($iPArticleId <= 0) {
+ return jsonError('p_article_id is required');
+ }
+
+ try {
+ $result = (new ReferenceRelevanceCheckService())->getArticleProgressStatusByPArticleId($iPArticleId);
+ return jsonSuccess($result);
+ } catch (\Exception $e) {
+ return jsonError($e->getMessage());
+ }
+ }
+
+ /**
+ * 按 p_refer_id 查相关性校对明细
+ * POST: p_refer_id
+ */
+ public function referenceRelevanceCheckDetailsAI()
+ {
+ $aParam = $this->request->post();
+ $iPReferId = empty($aParam['p_refer_id']) ? 0 : intval($aParam['p_refer_id']);
+ if ($iPReferId <= 0) {
+ return jsonError('p_refer_id is required');
+ }
+ try {
+ $result = (new ReferenceRelevanceCheckService())->getDetailsByPReferId($iPReferId);
+ return jsonSuccess($result);
+ } catch (\Exception $e) {
+ return jsonError($e->getMessage());
+ }
+ }
+
+ /**
+ * 清空并重新执行相关性校对
+ * POST: p_article_id
+ */
+ public function referenceRelevanceCheckResetAI()
+ {
+ $aParam = $this->request->post();
+ $iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
+ if ($iPArticleId <= 0) {
+ return jsonError('Please select an article');
+ }
+ $aProductionArticle = Db::name('production_article')
+ ->field('p_article_id,article_id')
+ ->where(['p_article_id' => $iPArticleId, 'state' => ['in', [0, 2]]])
+ ->find();
+ if (empty($aProductionArticle)) {
+ return jsonError('No articles found');
+ }
+ if ($this->checkReferStatus($iPArticleId) == 0) {
+ return jsonError('Please correct the reference content before running the check.');
+ }
+ try {
+ $result = (new ReferenceRelevanceCheckService())->resetAndRecheckByArticle($aProductionArticle);
+ return jsonSuccess($result);
+ } catch (\Exception $e) {
+ return jsonError($e->getMessage());
+ }
+ }
+
+ /**
+ * 仅清空相关性校对记录(不重跑)
+ * POST: p_article_id
+ */
+ public function referenceRelevanceCheckClearAI()
+ {
+ $aParam = $this->request->post();
+ $iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
+ if ($iPArticleId <= 0) {
+ return jsonError('p_article_id is required');
+ }
+ try {
+ $deleted = (new ReferenceRelevanceCheckService())->clearByPArticleId($iPArticleId);
+ return jsonSuccess(['p_article_id' => $iPArticleId, 'deleted' => intval($deleted)]);
+ } catch (\Exception $e) {
+ return jsonError($e->getMessage());
+ }
+ }
+
+ /**
+ * 仅重跑相关性 status=0 的记录(不清空,不抓摘要,不清洗文献内容)
+ * POST: p_article_id
+ */
+ public function referenceRelevanceCheckRecheckPendingAI()
+ {
+ $aParam = $this->request->post();
+ $iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
+ if ($iPArticleId <= 0) {
+ return jsonError('p_article_id is required');
+ }
+ try {
+ $result = (new ReferenceRelevanceCheckService())->recheckPendingOnlyByArticle($iPArticleId);
+ return jsonSuccess($result);
+ } catch (\Exception $e) {
+ return jsonError($e->getMessage());
+ }
+ }
+
+ /**
+ * 参考文献第一次校对(支撑力度)
* @return \think\response\Json
*/
- public function allReferenceCheckAI(){
+ public function allReferenceCheckAI2(){
//获取参数
$aParam = empty($aParam) ? $this->request->post() : $aParam;
@@ -1537,7 +1723,6 @@ class References extends Base
* p_article_id(可选)
*
* 仅重跑 status=3(校对失败)的记录;不改动 refer_text,只重置结果字段后入 RabbitMQ 批次队列。
- * 返回:p_refer_id、p_article_id、reset、queued、check_ids、queue
*/
public function referenceCheckRecheckFailedAI()
{
@@ -1561,6 +1746,36 @@ class References extends Base
}
}
+ /**
+ * 某条参考文献下「校对失败」重跑,并联动同一引用标签分组(如 [1,2])全部重跑(异步)
+ *
+ * POST/GET: p_refer_id(必填)
+ * p_article_id(可选)
+ *
+ * 返回:p_refer_id、p_article_id、reset、queued、check_ids、queue
+ */
+ public function referenceCheckRecheckFailedWithGroupAI()
+ {
+ $aParam = $this->request->post();
+ if (empty($aParam)) {
+ $aParam = $this->request->param();
+ }
+
+ $iPReferId = empty($aParam['p_refer_id']) ? 0 : intval($aParam['p_refer_id']);
+ if ($iPReferId <= 0) {
+ return json_encode(array('status' => 2, 'msg' => 'Please select a reference'));
+ }
+
+ $iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
+
+ try {
+ $result = (new ReferenceCheckService())->enqueueRecheckFailedByPReferIdWithGroup($iPReferId, $iPArticleId);
+ return jsonSuccess($result);
+ } catch (\Exception $e) {
+ return jsonError($e->getMessage());
+ }
+ }
+
/**
* 按 p_refer_id 查单条参考文献的校对明细与进度
*
@@ -1590,6 +1805,47 @@ class References extends Base
}
}
+ /**
+ * 对校对明细中从未出现过的参考文献(p_refer_id 差集)重新扫描全文并入队校对
+ *
+ * POST/GET: p_article_id(必填)
+ *
+ * 差集:production_article_refer(state=0) 减去 article_reference_check_result 已出现的 p_refer_id。
+ * 适用:首次校对漏匹配、表格后上传、正文补标等场景。不重置已有明细。
+ * 前置:须已执行过第一次校对(库中已有校对记录)。
+ *
+ * 返回:missing_p_refer_ids、matched_p_refer_ids、still_unmatched_p_refer_ids、
+ * queued、new_reference_nos、check_ids、queue
+ */
+ public function referenceCheckRematchNewAI()
+ {
+ $aParam = $this->request->post();
+ if (empty($aParam)) {
+ $aParam = $this->request->param();
+ }
+
+ $iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
+ if ($iPArticleId <= 0) {
+ return json_encode(array('status' => 2, 'msg' => 'Please select an article'));
+ }
+
+ $aWhere = ['p_article_id' => $iPArticleId, 'state' => ['in', [0, 2]]];
+ $aProductionArticle = Db::name('production_article')->field('p_article_id,article_id')->where($aWhere)->find();
+ if (empty($aProductionArticle)) {
+ return json_encode(array('status' => 3, 'msg' => 'No articles found'));
+ }
+ if ($this->checkReferStatus($iPArticleId) == 0) {
+ return jsonError('Please correct the reference content before running the check.');
+ }
+
+ try {
+ $result = (new ReferenceCheckService())->enqueueNewlyMatchedByPArticle($aProductionArticle);
+ return jsonSuccess($result);
+ } catch (\Exception $e) {
+ return jsonError($e->getMessage());
+ }
+ }
+
public function checkReferStatus($p_article_id){
$list = $this->production_article_refer_obj->where('p_article_id', $p_article_id)->where('state', 0)->select();
if (!$list) {
@@ -1604,4 +1860,6 @@ class References extends Base
}
return $frag;
}
+
+
}
diff --git a/application/command.php b/application/command.php
index 43892e98..cbea1b3b 100644
--- a/application/command.php
+++ b/application/command.php
@@ -11,4 +11,5 @@
return [
'app\\command\\ReferenceCheckMqConsume',
+ 'app\\command\\ReferenceRelevanceMqConsume',
];
diff --git a/application/common/PubmedService.php b/application/common/PubmedService.php
index ad17e2da..50f565ec 100644
--- a/application/common/PubmedService.php
+++ b/application/common/PubmedService.php
@@ -96,6 +96,68 @@ class PubmedService
return $info;
}
+ /**
+ * 按书目信息检索 PubMed(标题 + 第一作者 + 年份)
+ */
+ public function searchByBibliographic($title, $author = '', $year = ''): ?array
+ {
+ $title = trim((string)$title);
+ if ($title === '') {
+ return null;
+ }
+
+ $terms = ['(' . $this->quoteTerm($title) . '[Title])'];
+ $author = trim((string)$author);
+ if ($author !== '') {
+ $parts = preg_split('/[,;]/', $author);
+ $first = trim((string)($parts[0] ?? ''));
+ if ($first !== '') {
+ $terms[] = '(' . $this->quoteTerm($first) . '[Author])';
+ }
+ }
+ $year = trim((string)$year);
+ if ($year !== '' && preg_match('/^(19|20)\d{2}$/', $year)) {
+ $terms[] = '(' . $year . '[pdat])';
+ }
+
+ $pmid = $this->esearch(implode(' AND ', $terms));
+ if (!$pmid) {
+ return null;
+ }
+
+ $info = $this->fetchByPmid($pmid);
+ if (!$info) {
+ return null;
+ }
+ $info['pmid'] = $pmid;
+ $info['doi'] = $this->extractDoiFromPmidRecord($pmid);
+ return $info;
+ }
+
+ private function quoteTerm($text)
+ {
+ return str_replace('"', '', trim((string)$text));
+ }
+
+ private function extractDoiFromPmidRecord($pmid)
+ {
+ $url = $this->base . 'efetch.fcgi?' . http_build_query([
+ 'db' => 'pubmed',
+ 'id' => $pmid,
+ 'retmode' => 'xml',
+ 'tool' => $this->tool,
+ 'email' => $this->email,
+ ]);
+ $xml = $this->httpGet($url);
+ if ($xml === '') {
+ return '';
+ }
+ if (preg_match('/([^<]+)<\/ArticleId>/i', $xml, $m)) {
+ return trim($m[1]);
+ }
+ return '';
+ }
+
// ----------------- Internals -----------------
private function esearch(string $term): ?string
diff --git a/application/common/ReferenceCheckService.php b/application/common/ReferenceCheckService.php
index e551a482..0cf20986 100644
--- a/application/common/ReferenceCheckService.php
+++ b/application/common/ReferenceCheckService.php
@@ -9,6 +9,8 @@ use app\common\mq\ReferenceCheckMqPublisher;
/**
* 正文 <blue>[n]</blue> 引用与 t_production_article_refer(index+1=n)相关性校对。
+ * 校对上下文取 t_article_main 一条记录(正文 content 或表格 table_data 展平)。
+ * 同一引用标签 [1,2]、[4-6] 联合校对,cite_group_refs 存展开序号。
* LLM 配置与 PromotionLlmService 相同;异步任务走 RabbitMQ(一篇一条消息)。
*/
class ReferenceCheckService
@@ -69,6 +71,9 @@ class ReferenceCheckService
/** LLM 评分(confidence)通过阈值:>= 该值视为"通过" */
const PASS_CONFIDENCE_THRESHOLD = 0.65;
+ /** 是否启用二轮 DOI/Crossref 复核(暂时关闭时设为 false) */
+ const SECOND_PASS_ENABLED = false;
+
/**
* 正文引用标签两种排版(带 /u):
* 1) [8, 9]、[13-15] —— 方括号在 blue 内
@@ -93,6 +98,11 @@ class ReferenceCheckService
return isset($arr[$key]) ? $arr[$key] : $default;
}
+ private function isSecondPassEnabled()
+ {
+ return self::SECOND_PASS_ENABLED;
+ }
+
/** 新建/重置校对明细时的队列初始字段 */
private function newCheckRecordFields(array $fields, $queueStatus = self::QUEUE_PENDING, $retryCount = 0)
{
@@ -101,8 +111,26 @@ class ReferenceCheckService
return $fields;
}
+ /** 重置校对结果时清零的字段(含支撑力度扩展字段) */
+ private function referenceCheckResultResetFields(array $extra = [])
+ {
+ return array_merge([
+ 'status' => self::RECORD_PENDING,
+ 'is_match' => 0,
+ 'can_support' => 0,
+ 'confidence' => 0,
+ 'reason' => '',
+ 'support_role' => '',
+ 'combined_can_support' => 0,
+ 'combined_confidence' => 0,
+ 'combined_reason' => '',
+ 'error_msg' => '',
+ ], $extra);
+ }
+
public function markQueueRuntime($checkId, $queueStatus, $retryCount = null)
{
+ DbReconnectHelper::ensure();
$checkId = intval($checkId);
if ($checkId <= 0) {
return 0;
@@ -113,6 +141,84 @@ class ReferenceCheckService
}
return Db::name('article_reference_check_result')->where('id', $checkId)->update($fields);
}
+ public function enqueueByPArticle($prod){
+ if (empty($prod)) {
+ throw new \RuntimeException('production_article not found');
+ }
+ $pArticleId = intval($prod['p_article_id']);
+ $articleId = intval($prod['article_id']);
+ $referMap = $this->loadReferMapByPArticleId($pArticleId);
+
+ $mains = Db::name('article_main')
+ ->field('am_id,content,article_id,type,amt_id')
+ ->where('article_id', $articleId)
+ ->whereIn('state', [0, 2])
+ ->order('sort asc')
+ ->select();
+ if (empty($mains)) {
+ throw new \RuntimeException('article_main is empty');
+ }
+ $queued = 0;
+ $skipped = 0;
+ $pendingJobs = [];
+ $amIdsWithJobs = [];
+ $now = date('Y-m-d H:i:s');
+ foreach ($mains as $main) {
+ $amId = intval($main['am_id']);
+ $citations = $this->extractReferencesForArticleMain($main);
+ if (empty($citations)) {
+ $this->setAmRefCheckStatus($amId, self::AM_STATUS_NONE);
+ continue;
+ }
+ $sectionText = $this->resolveSectionTextForArticleMain($main);
+ foreach ($citations as $cite) {
+ foreach ($cite['reference_numbers'] as $refNo) {
+ $referIndex = $refNo - 1;
+ if ($referIndex < 0 || !isset($referMap[$referIndex])) {
+ $skipped++;
+ continue;
+ }
+ $scope = [
+ 'article_id' => $main['article_id'],
+ 'p_article_id' => $pArticleId,
+ 'am_id' => $amId,
+ 'section_text' => $sectionText,
+ ];
+ $checkId = $this->insertCitationCheckRecord($scope, $cite, $refNo, $referMap[$referIndex], $now);
+ if ($checkId <= 0) {
+ $skipped++;
+ continue;
+ }
+
+ $this->appendCitationPendingJob($pendingJobs, $checkId, $refNo, $amId, $cite['text_start']);
+ $queued++;
+ $amIdsWithJobs[$amId] = true;
+ }
+ }
+ break;
+ }
+ $checkIds = $this->enqueueChecksSortedByReferenceNo($pendingJobs, $pArticleId, 'enqueue');
+ foreach (array_keys($amIdsWithJobs) as $amId) {
+ $this->setAmRefCheckStatus($amId, self::AM_STATUS_RUNNING);
+ }
+
+ return [
+ 'article_id' => $articleId,
+ 'p_article_id' => $pArticleId,
+ 'queued' => $queued,
+ 'skipped' => $skipped,
+ 'check_ids' => $checkIds,
+ 'queue' => self::TRANSPORT_RABBITMQ,
+ ];
+ }
+
+
+
+
+
+
+
+
/**
* 合并匹配两种 blue 引用排版,按在正文中的起始位置排序。
@@ -173,6 +279,7 @@ class ReferenceCheckService
'refer_index' => intval($this->arrGet($extra, 'refer_index', 0)),
'reference_no' => intval($this->arrGet($extra, 'reference_no', 0)),
'reference_raw' => (string)$this->arrGet($extra, 'reference_raw', ''),
+ 'cite_group_refs' => (string)$this->arrGet($extra, 'cite_group_refs', ''),
'cite_tag_start' => intval($this->arrGet($extra, 'cite_tag_start', 0)),
'cite_tag_end' => intval($this->arrGet($extra, 'cite_tag_end', 0)),
'text_start' => intval($this->arrGet($extra, 'text_start', 0)),
@@ -229,6 +336,7 @@ class ReferenceCheckService
$skipped = 0;
$pendingJobs = [];
$now = date('Y-m-d H:i:s');
+ $sectionText = $this->resolveSectionTextForArticleMain($main);
foreach ($citations as $cite) {
foreach ($cite['reference_numbers'] as $refNo) {
$referIndex = $refNo - 1;
@@ -236,30 +344,24 @@ class ReferenceCheckService
$skipped++;
continue;
}
- $refer = $referMap[$referIndex];
- $referText = $this->formatReferForLlm($refer);
-
- $checkId = Db::name('article_reference_check_result')->insertGetId($this->newCheckRecordFields([
- 'article_id' => $main['article_id'],
- 'p_article_id' => $pArticleId,
- 'am_id' => intval($main['am_id']),
- 'reference_no' => $refNo,
- 'refer_index' => $refNo,
- 'origin_text' => $cite['original_text'],
- 'refer_text' => $referText,
- 'p_refer_id' => $referMap[$referIndex]['p_refer_id'],
- 'text_start' => $cite['text_start'],
- 'text_end' => $cite['text_end'],
- 'status' => self::RECORD_PENDING,
- 'created_at' => $now,
- 'updated_at' => $now,
- ]));
- $pendingJobs[] = [
- 'check_id' => intval($checkId),
- 'reference_no' => intval($refNo),
+ $scope = [
+ 'article_id' => $main['article_id'],
+ 'p_article_id' => $pArticleId,
'am_id' => intval($main['am_id']),
- 'text_start' => intval($cite['text_start']),
+ 'section_text' => $sectionText,
];
+ $checkId = $this->insertCitationCheckRecord($scope, $cite, $refNo, $referMap[$referIndex], $now);
+ if ($checkId <= 0) {
+ $skipped++;
+ continue;
+ }
+ $this->appendCitationPendingJob(
+ $pendingJobs,
+ $checkId,
+ $refNo,
+ intval($main['am_id']),
+ $cite['text_start']
+ );
}
}
@@ -276,6 +378,14 @@ class ReferenceCheckService
throw new \InvalidArgumentException('article_id is required');
}
+ if (!$this->isSecondPassEnabled()) {
+ return [
+ 'article_id' => $articleId,
+ 'check_ids2' => [],
+ 'queued' => 0,
+ ];
+ }
+
$rows = Db::name('article_reference_check_result')
->where('article_id', $articleId)
->where('status', self::RECORD_COMPLETED)
@@ -300,87 +410,6 @@ class ReferenceCheckService
'queued' => count($checkIds2),
];
}
- public function enqueueByPArticle($prod){
- if (empty($prod)) {
- throw new \RuntimeException('production_article not found');
- }
- $pArticleId = intval($prod['p_article_id']);
- $articleId = intval($prod['article_id']);
- $referMap = $this->loadReferMapByPArticleId($pArticleId);
-
- $mains = Db::name('article_main')
- ->field('am_id,content,article_id,type,amt_id')
- ->where('article_id', $articleId)
- ->whereIn('state', [0, 2])
- ->order('sort asc')
- ->select();
- if (empty($mains)) {
- throw new \RuntimeException('article_main is empty');
- }
- $queued = 0;
- $skipped = 0;
- $pendingJobs = [];
- $amIdsWithJobs = [];
- $now = date('Y-m-d H:i:s');
- foreach ($mains as $main) {
- $amId = intval($main['am_id']);
- $citations = $this->extractReferencesForArticleMain($main);
- if (empty($citations)) {
- $this->setAmRefCheckStatus($amId, self::AM_STATUS_NONE);
- continue;
- }
- foreach ($citations as $cite) {
- foreach ($cite['reference_numbers'] as $refNo) {
- $referIndex = $refNo - 1;
- if ($referIndex < 0 || !isset($referMap[$referIndex])) {
- $skipped++;
- continue;
- }
- $refer = $referMap[$referIndex];
- $referText = $this->formatReferForLlm($refer);
-
- // [70-73] 展开为 reference_no=70,71,72,73 共 4 条记录;先入队表,再按文献号正序校对
- $checkId = Db::name('article_reference_check_result')->insertGetId($this->newCheckRecordFields([
- 'article_id' => $main['article_id'],
- 'p_article_id' => $pArticleId,
- 'am_id' => $amId,
- 'reference_no' => $refNo,
- 'refer_index' => $refNo,
- 'origin_text' => $cite['original_text'],
- 'refer_text' => $referText,
- 'p_refer_id' => $referMap[$referIndex]['p_refer_id'],
- 'text_start' => $cite['text_start'],
- 'text_end' => $cite['text_end'],
- 'status' => self::RECORD_PENDING,
- 'created_at' => $now,
- 'updated_at' => $now,
- ]));
-
- $pendingJobs[] = [
- 'check_id' => intval($checkId),
- 'reference_no' => intval($refNo),
- 'am_id' => $amId,
- 'text_start' => intval($cite['text_start']),
- ];
- $queued++;
- $amIdsWithJobs[$amId] = true;
- }
- }
- }
- $checkIds = $this->enqueueChecksSortedByReferenceNo($pendingJobs, $pArticleId, 'enqueue');
- foreach (array_keys($amIdsWithJobs) as $amId) {
- $this->setAmRefCheckStatus($amId, self::AM_STATUS_RUNNING);
- }
-
- return [
- 'article_id' => $articleId,
- 'p_article_id' => $pArticleId,
- 'queued' => $queued,
- 'skipped' => $skipped,
- 'check_ids' => $checkIds,
- 'queue' => self::TRANSPORT_RABBITMQ,
- ];
- }
public function enqueueByArticle($articleId){
if ($articleId <= 0) {
throw new \InvalidArgumentException('article_id is required');
@@ -416,6 +445,7 @@ class ReferenceCheckService
$this->setAmRefCheckStatus($amId, self::AM_STATUS_NONE);
continue;
}
+ $sectionText = $this->resolveSectionTextForArticleMain($main);
foreach ($citations as $cite) {
foreach ($cite['reference_numbers'] as $refNo) {
$referIndex = $refNo - 1;
@@ -423,32 +453,19 @@ class ReferenceCheckService
$skipped++;
continue;
}
- $refer = $referMap[$referIndex];
- $referText = $this->formatReferForLlm($refer);
-
- // [70-73] 展开为 reference_no=70,71,72,73 共 4 条记录;先入队表,再按文献号正序校对
- $checkId = Db::name('article_reference_check_result')->insertGetId($this->newCheckRecordFields([
- 'article_id' => $main['article_id'],
- 'p_article_id' => $pArticleId,
- 'am_id' => $amId,
- 'reference_no' => $refNo,
- 'refer_index' => $refNo,
- 'origin_text' => $cite['original_text'],
- 'refer_text' => $referText,
- 'p_refer_id' => $referMap[$referIndex]['p_refer_id'],
- 'text_start' => $cite['text_start'],
- 'text_end' => $cite['text_end'],
- 'status' => self::RECORD_PENDING,
- 'created_at' => $now,
- 'updated_at' => $now,
- ]));
-
- $pendingJobs[] = [
- 'check_id' => intval($checkId),
- 'reference_no' => intval($refNo),
+ $scope = [
+ 'article_id' => $main['article_id'],
+ 'p_article_id' => $pArticleId,
'am_id' => $amId,
- 'text_start' => intval($cite['text_start']),
+ 'section_text' => $sectionText,
];
+ $checkId = $this->insertCitationCheckRecord($scope, $cite, $refNo, $referMap[$referIndex], $now);
+ if ($checkId <= 0) {
+ $skipped++;
+ continue;
+ }
+
+ $this->appendCitationPendingJob($pendingJobs, $checkId, $refNo, $amId, $cite['text_start']);
$queued++;
$amIdsWithJobs[$amId] = true;
}
@@ -470,6 +487,200 @@ class ReferenceCheckService
];
}
+ /**
+ * 对「参考文献表中有、校对明细中从未出现过的 p_refer_id」重新扫描全文并入队校对。
+ *
+ * 差集:production_article_refer(state=0) 的 p_refer_id
+ * 减去 article_reference_check_result 中已出现过的 p_refer_id。
+ * 仅对上述缺失文献在全文(含表格)中查找引用标签,命中则新增明细并入队。
+ * 不删除、不重跑已有明细。
+ *
+ * @param array $prod production_article 行(需含 p_article_id、article_id)
+ * @return array
+ */
+ public function enqueueNewlyMatchedByPArticle($prod)
+ {
+ if (empty($prod) || !is_array($prod)) {
+ throw new \RuntimeException('production_article not found');
+ }
+ $pArticleId = intval($this->arrGet($prod, 'p_article_id', 0));
+ $articleId = intval($this->arrGet($prod, 'article_id', 0));
+ if ($pArticleId <= 0 || $articleId <= 0) {
+ throw new \InvalidArgumentException('production_article requires both p_article_id and article_id');
+ }
+
+ $existingCount = Db::name('article_reference_check_result')
+ ->where('p_article_id', $pArticleId)
+ ->count();
+ if (intval($existingCount) <= 0) {
+ throw new \RuntimeException('no existing reference check records for p_article_id=' . $pArticleId . '; please run the first check first');
+ }
+
+ $missingCtx = $this->loadMissingPReferIdsByPArticleId($pArticleId);
+ $missingPReferIds = $missingCtx['missing_p_refer_ids'];
+ $missingRefNos = $missingCtx['missing_reference_nos'];
+
+ if (empty($missingPReferIds)) {
+ return [
+ 'article_id' => $articleId,
+ 'p_article_id' => $pArticleId,
+ 'queued' => 0,
+ 'skipped' => 0,
+ 'existing' => intval($existingCount),
+ 'missing_p_refer_ids' => [],
+ 'matched_p_refer_ids' => [],
+ 'still_unmatched_p_refer_ids' => [],
+ 'new_reference_nos' => [],
+ 'check_ids' => [],
+ 'queue' => self::TRANSPORT_RABBITMQ,
+ ];
+ }
+
+ $referMap = $this->loadReferMapByPArticleId($pArticleId);
+
+ $mains = Db::name('article_main')
+ ->field('am_id,content,article_id,type,amt_id')
+ ->where('article_id', $articleId)
+ ->whereIn('state', [0, 2])
+ ->order('sort asc')
+ ->select();
+ if (empty($mains)) {
+ throw new \RuntimeException('article_main is empty');
+ }
+
+ $queued = 0;
+ $skipped = 0;
+ $pendingJobs = [];
+ $amIdsWithJobs = [];
+ $newReferenceNos = [];
+ $matchedPReferIds = [];
+ $now = date('Y-m-d H:i:s');
+
+ foreach ($mains as $main) {
+ $amId = intval($main['am_id']);
+ $citations = $this->extractReferencesForArticleMain($main);
+ if (empty($citations)) {
+ continue;
+ }
+ $sectionText = $this->resolveSectionTextForArticleMain($main);
+ foreach ($citations as $cite) {
+ foreach ($cite['reference_numbers'] as $refNo) {
+ if (!isset($missingRefNos[$refNo])) {
+ $skipped++;
+ continue;
+ }
+
+ $referIndex = $refNo - 1;
+ if ($referIndex < 0 || !isset($referMap[$referIndex])) {
+ $skipped++;
+ continue;
+ }
+
+ $refer = $referMap[$referIndex];
+ $pReferId = intval($this->arrGet($refer, 'p_refer_id', 0));
+ $scope = [
+ 'article_id' => $main['article_id'],
+ 'p_article_id' => $pArticleId,
+ 'am_id' => $amId,
+ 'section_text' => $sectionText,
+ ];
+ $checkId = $this->insertCitationCheckRecord($scope, $cite, $refNo, $refer, $now);
+ if ($checkId <= 0) {
+ $skipped++;
+ continue;
+ }
+
+ $this->appendCitationPendingJob($pendingJobs, $checkId, $refNo, $amId, $cite['text_start']);
+ $queued++;
+ $amIdsWithJobs[$amId] = true;
+ $newReferenceNos[$refNo] = true;
+ if ($pReferId > 0) {
+ $matchedPReferIds[$pReferId] = true;
+ }
+ }
+ }
+ }
+
+ $checkIds = $this->enqueueChecksSortedByReferenceNo($pendingJobs, $pArticleId, 'rematch_new');
+ foreach (array_keys($amIdsWithJobs) as $amId) {
+ $this->setAmRefCheckStatus($amId, self::AM_STATUS_RUNNING);
+ }
+
+ $newRefList = array_keys($newReferenceNos);
+ sort($newRefList, SORT_NUMERIC);
+
+ $matchedList = array_keys($matchedPReferIds);
+ sort($matchedList, SORT_NUMERIC);
+
+ $stillUnmatched = array_values(array_diff($missingPReferIds, $matchedList));
+ sort($stillUnmatched, SORT_NUMERIC);
+
+ return [
+ 'article_id' => $articleId,
+ 'p_article_id' => $pArticleId,
+ 'queued' => $queued,
+ 'skipped' => $skipped,
+ 'existing' => intval($existingCount),
+ 'missing_p_refer_ids' => $missingPReferIds,
+ 'matched_p_refer_ids' => $matchedList,
+ 'still_unmatched_p_refer_ids' => $stillUnmatched,
+ 'new_reference_nos' => $newRefList,
+ 'check_ids' => $checkIds,
+ 'queue' => self::TRANSPORT_RABBITMQ,
+ ];
+ }
+
+ /**
+ * 参考文献表(state=0) 与校对明细中已出现的 p_refer_id 做差集。
+ *
+ * @return array{missing_p_refer_ids:int[], missing_reference_nos:array}
+ */
+ private function loadMissingPReferIdsByPArticleId($pArticleId)
+ {
+ $pArticleId = intval($pArticleId);
+ $missingPReferIds = [];
+ $missingRefNos = [];
+
+ if ($pArticleId <= 0) {
+ return [
+ 'missing_p_refer_ids' => $missingPReferIds,
+ 'missing_reference_nos' => $missingRefNos,
+ ];
+ }
+
+ $refers = Db::name('production_article_refer')
+ ->field('p_refer_id,index')
+ ->where('p_article_id', $pArticleId)
+ ->where('state', 0)
+ ->order('index asc')
+ ->select();
+
+ $checkedIds = Db::name('article_reference_check_result')
+ ->where('p_article_id', $pArticleId)
+ ->where('p_refer_id', '>', 0)
+ ->group('p_refer_id')
+ ->column('p_refer_id');
+ $checkedSet = [];
+ foreach ($checkedIds as $id) {
+ $checkedSet[intval($id)] = true;
+ }
+
+ foreach ($refers as $refer) {
+ $pReferId = intval($this->arrGet($refer, 'p_refer_id', 0));
+ if ($pReferId <= 0 || isset($checkedSet[$pReferId])) {
+ continue;
+ }
+ $refNo = intval($this->arrGet($refer, 'index', 0)) + 1;
+ $missingPReferIds[] = $pReferId;
+ $missingRefNos[$refNo] = $pReferId;
+ }
+
+ return [
+ 'missing_p_refer_ids' => $missingPReferIds,
+ 'missing_reference_nos' => $missingRefNos,
+ ];
+ }
+
/**
* 根据该节全部明细行汇总更新 t_article_main.ref_check_status
*/
@@ -553,7 +764,7 @@ class ReferenceCheckService
*
* @return int 被删除的明细条数
*/
- public function clearArticleChecksByPArticleId($pArticleId)
+ public function clearArticleChecksByPArticleId($pArticleId,$articleId=0)
{
$pArticleId = intval($pArticleId);
if ($pArticleId <= 0) {
@@ -561,10 +772,12 @@ class ReferenceCheckService
}
// 先反查 article_id(用于重置 article_main.ref_check_status 节级状态)
- $articleId = intval(Db::name('production_article')
- ->where('p_article_id', $pArticleId)
- ->whereIn('state', [0, 2])
- ->value('article_id'));
+ if($articleId==0){
+ $articleId = intval(Db::name('production_article')
+ ->where('p_article_id', $pArticleId)
+ ->whereIn('state', [0, 2])
+ ->value('article_id'));
+ }
$deleted = Db::name('article_reference_check_result')
->where('p_article_id', $pArticleId)
@@ -598,6 +811,29 @@ class ReferenceCheckService
return intval($deleted);
}
+ /**
+ * 按 article_id + am_id 删除单节正文下的引用校对明细,并同步该节 ref_check_status。
+ *
+ * @return int 被删除的明细条数
+ */
+ public function clearChecksByAmId($articleId, $amId)
+ {
+ $articleId = intval($articleId);
+ $amId = intval($amId);
+ if ($articleId <= 0 || $amId <= 0) {
+ return 0;
+ }
+
+ $deleted = Db::name('article_reference_check_result')
+ ->where('article_id', $articleId)
+ ->where('am_id', $amId)
+ ->delete();
+
+ $this->syncAmRefCheckStatus($amId);
+
+ return intval($deleted);
+ }
+
/**
* 文献列表局部挪动后,仅刷新指定 p_refer_id 对应的校对明细 reference_no / refer_index。
*
@@ -850,7 +1086,7 @@ class ReferenceCheckService
}
$rows = Db::name('article_reference_check_result')
- ->field('id,p_refer_id,reference_no,am_id,status,confidence,is_match,reason,text_start,text_end,updated_at')
+ ->field('id,p_refer_id,reference_no,am_id,status,confidence,is_match,reason,support_role,combined_can_support,combined_confidence,combined_reason,text_start,text_end,cite_group_refs,updated_at')
->where('p_article_id', $pArticleId)
->order('reference_no asc, id asc')
->select();
@@ -916,16 +1152,22 @@ class ReferenceCheckService
}
$groups[$refNo]['records'][] = [
- 'check_id' => intval($this->arrGet($row, 'id', 0)),
- 'am_id' => intval($this->arrGet($row, 'am_id', 0)),
- 'status' => $st,
- 'confidence' => $confidence,
- 'is_pass' => $isPass,
- 'is_match' => intval($this->arrGet($row, 'is_match', 0)),
- 'reason' => (string)$this->arrGet($row, 'reason', ''),
- 'text_start' => intval($this->arrGet($row, 'text_start', 0)),
- 'text_end' => intval($this->arrGet($row, 'text_end', 0)),
- 'last_updated_at' => $upd,
+ 'check_id' => intval($this->arrGet($row, 'id', 0)),
+ 'am_id' => intval($this->arrGet($row, 'am_id', 0)),
+ 'status' => $st,
+ 'confidence' => $confidence,
+ 'is_pass' => $isPass,
+ 'is_match' => intval($this->arrGet($row, 'is_match', 0)),
+ 'reason' => (string)$this->arrGet($row, 'reason', ''),
+ 'support_role' => (string)$this->arrGet($row, 'support_role', ''),
+ 'combined_can_support' => intval($this->arrGet($row, 'combined_can_support', 0)),
+ 'combined_confidence' => floatval($this->arrGet($row, 'combined_confidence', 0)),
+ 'combined_reason' => (string)$this->arrGet($row, 'combined_reason', ''),
+ 'cite_group_refs' => (string)$this->arrGet($row, 'cite_group_refs', ''),
+ 'cite_check_mode' => $this->isJointCiteGroupRefs($this->arrGet($row, 'cite_group_refs', '')) ? 'joint' : 'single',
+ 'text_start' => intval($this->arrGet($row, 'text_start', 0)),
+ 'text_end' => intval($this->arrGet($row, 'text_end', 0)),
+ 'last_updated_at' => $upd,
];
}
@@ -993,7 +1235,7 @@ class ReferenceCheckService
}
$rows = Db::name('article_reference_check_result')
- ->field('id,p_article_id,reference_no,am_id,status,confidence,is_match,reason,updated_at')
+ ->field('id,p_article_id,reference_no,am_id,status,confidence,is_match,reason,support_role,combined_can_support,combined_confidence,combined_reason,cite_group_refs,updated_at')
->where('p_refer_id', $pReferId)
->order('id asc')
->select();
@@ -1036,13 +1278,19 @@ class ReferenceCheckService
}
$list[] = [
- 'check_id' => intval($this->arrGet($row, 'id', 0)),
- 'am_id' => intval($this->arrGet($row, 'am_id', 0)),
- 'status' => $st,
- 'confidence' => $confidence,
- 'reason' => (string)$this->arrGet($row, 'reason', ''),
- 'is_match' => intval($this->arrGet($row, 'is_match', 0)),
- 'is_pass' => $isPass,
+ 'check_id' => intval($this->arrGet($row, 'id', 0)),
+ 'am_id' => intval($this->arrGet($row, 'am_id', 0)),
+ 'status' => $st,
+ 'confidence' => $confidence,
+ 'reason' => (string)$this->arrGet($row, 'reason', ''),
+ 'is_match' => intval($this->arrGet($row, 'is_match', 0)),
+ 'is_pass' => $isPass,
+ 'support_role' => (string)$this->arrGet($row, 'support_role', ''),
+ 'combined_can_support' => intval($this->arrGet($row, 'combined_can_support', 0)),
+ 'combined_confidence' => floatval($this->arrGet($row, 'combined_confidence', 0)),
+ 'combined_reason' => (string)$this->arrGet($row, 'combined_reason', ''),
+ 'cite_group_refs' => (string)$this->arrGet($row, 'cite_group_refs', ''),
+ 'cite_check_mode' => $this->isJointCiteGroupRefs($this->arrGet($row, 'cite_group_refs', '')) ? 'joint' : 'single',
];
}
@@ -1117,8 +1365,8 @@ class ReferenceCheckService
throw new \RuntimeException('no existing reference check records for p_article_id=' . $pArticleId);
}
- $cleared = $this->clearArticleChecks($articleId);
- $enqueueResult = $this->enqueueByArticle($articleId);
+ $cleared = $this->clearArticleChecksByPArticleId($aProductionArticle['p_article_id'],$aProductionArticle['article_id']);
+ $enqueueResult = $this->enqueueByPArticle($aProductionArticle);
if (!is_array($enqueueResult)) {
$enqueueResult = [];
@@ -1178,6 +1426,7 @@ class ReferenceCheckService
*/
public function updateCheckResult($checkId, array $fields)
{
+ DbReconnectHelper::ensure();
$checkId = intval($checkId);
if ($checkId <= 0) {
throw new \InvalidArgumentException('invalid check id');
@@ -1186,6 +1435,9 @@ class ReferenceCheckService
if (isset($fields['reason'])) {
$fields['reason'] = mb_substr(trim((string)$fields['reason']), 0, 512);
}
+ if (isset($fields['combined_reason'])) {
+ $fields['combined_reason'] = mb_substr(trim((string)$fields['combined_reason']), 0, 512);
+ }
if (isset($fields['error_msg'])) {
$fields['error_msg'] = mb_substr(trim((string)$fields['error_msg']), 0, 512);
}
@@ -1540,6 +1792,581 @@ class ReferenceCheckService
return implode("\n", $parts);
}
+ /**
+ * 从 extractReferences 结果提取引用标签元数据([1,2]、[70-73] 等同标签下各明细共用)
+ */
+ private function citationMetaFromExtract(array $cite)
+ {
+ return [
+ 'reference_raw' => (string)$this->arrGet($cite, 'reference_raw', ''),
+ 'cite_group_refs' => $this->formatCiteGroupRefs((array)$this->arrGet($cite, 'reference_numbers', [])),
+ 'cite_tag_start' => intval($this->arrGet($cite, 'reference_start', 0)),
+ 'cite_tag_end' => intval($this->arrGet($cite, 'reference_end', 0)),
+ 'origin_text' => (string)$this->arrGet($cite, 'original_text', ''),
+ 'text_start' => intval($this->arrGet($cite, 'text_start', 0)),
+ 'text_end' => intval($this->arrGet($cite, 'text_end', 0)),
+ ];
+ }
+
+ /**
+ * 引用组展开序号:1,2 或 4,5,6 或 3
+ */
+ private function formatCiteGroupRefs(array $refNumbers)
+ {
+ $nums = [];
+ foreach ($refNumbers as $n) {
+ $n = intval($n);
+ if ($n > 0) {
+ $nums[$n] = $n;
+ }
+ }
+ if (empty($nums)) {
+ return '';
+ }
+ $list = array_values($nums);
+ sort($list, SORT_NUMERIC);
+
+ return implode(',', $list);
+ }
+
+ private function isJointCiteGroupRefs($citeGroupRefs)
+ {
+ return strpos((string)$citeGroupRefs, ',') !== false;
+ }
+
+ private function resolveCiteGroupRefsFromRow(array $row, array $groupRows = null)
+ {
+ $refs = trim((string)$this->arrGet($row, 'cite_group_refs', ''));
+ if ($refs !== '') {
+ return $refs;
+ }
+ if ($groupRows === null) {
+ $groupRows = $this->findCitationGroupRows($row);
+ }
+ $nums = [];
+ foreach ($groupRows as $gr) {
+ $n = intval($this->arrGet($gr, 'reference_no', 0));
+ if ($n > 0) {
+ $nums[$n] = $n;
+ }
+ }
+ if (empty($nums)) {
+ $n = intval($this->arrGet($row, 'reference_no', 0));
+ return $n > 0 ? (string)$n : '';
+ }
+ $list = array_values($nums);
+ sort($list, SORT_NUMERIC);
+
+ return implode(',', $list);
+ }
+
+ private function hasSecondPassCompleted(array $row)
+ {
+ $reason = (string)$this->arrGet($row, 'reason', '');
+ return stripos($reason, '[DOI复核') !== false || stripos($reason, '[Crossref复核') !== false;
+ }
+
+ private function buildSecondPassReasonTag(array $row, array $payload, array $groupRows = null)
+ {
+ $citeGroupRefs = $this->resolveCiteGroupRefsFromRow($row, $groupRows);
+ $tag = '[DOI复核';
+ if ($citeGroupRefs !== '') {
+ $tag .= ' 文献' . $citeGroupRefs;
+ }
+ if (trim((string)$this->arrGet($payload, 'doi_used', '')) !== '') {
+ $tag .= ' ' . trim((string)$payload['doi_used']);
+ }
+ $tag .= ']';
+
+ return $tag;
+ }
+
+ /**
+ * 从 refer 抓取 DOI 真实文献块(PubMed 优先,回落 Crossref)
+ *
+ * @return array{text:string, has_abstract:bool, doi:string}
+ */
+ private function resolveDoiRecheckFromRefer($refer)
+ {
+ if (!is_array($refer) || empty($refer)) {
+ return ['text' => '', 'has_abstract' => false, 'doi' => ''];
+ }
+ $text = trim($this->fetchDoiLiteratureBlock($refer));
+ $hasAbstract = $text !== '' && preg_match('/Abstract:\s*\S/u', $text);
+
+ return [
+ 'text' => $text,
+ 'has_abstract' => $hasAbstract,
+ 'doi' => $this->extractDoiFromRefer($refer),
+ ];
+ }
+
+ /**
+ * 校对时使用的正文:t_article_main 一条记录(正文或表格展平文本)
+ */
+ public function resolveParagraphContextForJob(array $row, $maxChars = 8000)
+ {
+ return $this->resolveMainContentForJob($row, $maxChars);
+ }
+
+ /**
+ * 入队时快照引用处局部上下文,写入 origin_text(与 text_start/text_end 对应)
+ */
+ private function resolveSectionTextForArticleMain(array $main, $maxChars = 8000)
+ {
+ $raw = trim($this->resolveArticleMainCheckContent($main));
+ if ($raw === '') {
+ return '';
+ }
+
+ return $this->normalizeCheckContentForLlm($raw, $maxChars);
+ }
+
+ /**
+ * 同一 blue 引用标签(如 [1,2])下为单个文献号写入校对明细
+ *
+ * @return int|null check_id
+ */
+ private function insertCitationCheckRecord(array $scope, array $cite, $refNo, array $refer, $now)
+ {
+ $refNo = intval($refNo);
+ $referIndex = $refNo - 1;
+ if ($referIndex < 0) {
+ return null;
+ }
+
+ $meta = $this->citationMetaFromExtract($cite);
+ $referText = $this->formatReferForLlm($refer);
+ // origin_text 存引用处局部上下文(extractLocalCitationContext),非整节 am 正文
+ $originText = trim((string)$meta['origin_text']);
+ if ($originText === '') {
+ $originText = trim((string)$this->arrGet($scope, 'section_text', ''));
+ }
+
+ return intval(Db::name('article_reference_check_result')->insertGetId($this->newCheckRecordFields([
+ 'article_id' => intval($this->arrGet($scope, 'article_id', 0)),
+ 'p_article_id' => intval($this->arrGet($scope, 'p_article_id', 0)),
+ 'am_id' => intval($this->arrGet($scope, 'am_id', 0)),
+ 'reference_no' => $refNo,
+ 'refer_index' => $refNo,
+ 'origin_text' => $originText,
+ 'refer_text' => $referText,
+ 'p_refer_id' => intval($this->arrGet($refer, 'p_refer_id', 0)),
+ 'reference_raw' => $meta['reference_raw'],
+ 'cite_group_refs' => $meta['cite_group_refs'],
+ 'cite_tag_start' => $meta['cite_tag_start'],
+ 'cite_tag_end' => $meta['cite_tag_end'],
+ 'text_start' => $meta['text_start'],
+ 'text_end' => $meta['text_end'],
+ 'status' => self::RECORD_PENDING,
+ 'created_at' => $now,
+ 'updated_at' => $now,
+ ])));
+ }
+
+ private function appendCitationPendingJob(array &$pendingJobs, $checkId, $refNo, $amId, $textStart)
+ {
+ $pendingJobs[] = [
+ 'check_id' => intval($checkId),
+ 'reference_no' => intval($refNo),
+ 'am_id' => intval($amId),
+ 'text_start' => intval($textStart),
+ ];
+ }
+
+ /**
+ * 同一引用标签下的全部校对明细([1,2] 展开后 reference_no 不同但 cite_tag_* 相同)
+ *
+ * @return array[]
+ */
+ private function findCitationGroupRows(array $row)
+ {
+ $amId = intval($this->arrGet($row, 'am_id', 0));
+ if ($amId <= 0) {
+ return [$row];
+ }
+
+ $citeTagStart = intval($this->arrGet($row, 'cite_tag_start', 0));
+ $citeTagEnd = intval($this->arrGet($row, 'cite_tag_end', 0));
+ $q = Db::name('article_reference_check_result')->where('am_id', $amId);
+
+ if ($citeTagStart > 0 && $citeTagEnd > $citeTagStart) {
+ $q->where('cite_tag_start', $citeTagStart)->where('cite_tag_end', $citeTagEnd);
+ } else {
+ $textStart = intval($this->arrGet($row, 'text_start', 0));
+ $textEnd = intval($this->arrGet($row, 'text_end', 0));
+ $referenceRaw = trim((string)$this->arrGet($row, 'reference_raw', ''));
+ $citeGroupRefs = trim((string)$this->arrGet($row, 'cite_group_refs', ''));
+ $q->where('text_start', $textStart)->where('text_end', $textEnd);
+ if ($referenceRaw !== '') {
+ $q->where('reference_raw', $referenceRaw);
+ } elseif ($citeGroupRefs !== '') {
+ $q->where('cite_group_refs', $citeGroupRefs);
+ }
+ }
+
+ $rows = $q->order('reference_no asc')->select();
+ return empty($rows) ? [$row] : $rows;
+ }
+
+ private function resolveCitationGroupLeaderRefNo(array $groupRows)
+ {
+ $leader = PHP_INT_MAX;
+ foreach ($groupRows as $gr) {
+ $refNo = intval($this->arrGet($gr, 'reference_no', 0));
+ if ($refNo > 0 && $refNo < $leader) {
+ $leader = $refNo;
+ }
+ }
+
+ return $leader === PHP_INT_MAX ? 0 : $leader;
+ }
+
+ private function findCitationGroupRowByRefNo(array $groupRows, $refNo)
+ {
+ $refNo = intval($refNo);
+ foreach ($groupRows as $gr) {
+ if (intval($this->arrGet($gr, 'reference_no', 0)) === $refNo) {
+ return $gr;
+ }
+ }
+
+ return null;
+ }
+
+ private function isCitationGroupCheck(array $groupRows)
+ {
+ return count($groupRows) > 1;
+ }
+
+ private function resolveReferTextForCheckRow(array $row, $refer = null)
+ {
+ if (is_array($refer) && !empty($refer)) {
+ return $this->formatReferForLlm($refer);
+ }
+
+ return trim((string)$this->arrGet($row, 'refer_text', ''));
+ }
+
+ /**
+ * 将同一引用标签下多条文献书目拼成一次 LLM 校对的 refer_text
+ */
+ private function buildCombinedReferTextForCitationGroup(array $groupRows)
+ {
+ $blocks = [];
+ foreach ($groupRows as $gr) {
+ $refNo = intval($this->arrGet($gr, 'reference_no', 0));
+ if ($refNo <= 0) {
+ continue;
+ }
+
+ $refer = null;
+ if (intval($this->arrGet($gr, 'p_refer_id', 0)) > 0) {
+ $refer = Db::name('production_article_refer')
+ ->where('p_refer_id', intval($gr['p_refer_id']))
+ ->where('state', 0)
+ ->find();
+ }
+ $text = $this->resolveReferTextForCheckRow($gr, $refer);
+ if ($text === '') {
+ continue;
+ }
+ $blocks[] = '【参考文献 ' . $refNo . '】' . "\n" . $text;
+ }
+
+ return implode("\n\n", $blocks);
+ }
+
+ /**
+ * @return array{refer_text:string, doi_block:string, has_abstract:bool, doi_used:string}
+ */
+ private function prepareRecheckPayloadForCitationGroup(array $groupRows)
+ {
+ $referText = $this->buildCombinedReferTextForCitationGroup($groupRows);
+ $doiParts = [];
+ $doiUsed = [];
+ $hasAbstract = false;
+
+ foreach ($groupRows as $gr) {
+ $refNo = intval($this->arrGet($gr, 'reference_no', 0));
+ if ($refNo <= 0 || intval($this->arrGet($gr, 'p_refer_id', 0)) <= 0) {
+ continue;
+ }
+ DbReconnectHelper::ensure();
+ $refer = Db::name('production_article_refer')
+ ->where('p_refer_id', intval($gr['p_refer_id']))
+ ->where('state', 0)
+ ->find();
+ if (empty($refer)) {
+ continue;
+ }
+ $bundle = (new ReferenceLiteratureFetchService())->fetchAndCleanForRefer($refer);
+ $checkId = intval($this->arrGet($gr, 'id', $this->arrGet($gr, 'check_id', 0)));
+ if ($checkId > 0) {
+ $this->persistLiteratureOnCheckRow($checkId, $bundle);
+ }
+
+ $block = $this->buildLiteratureBlockFromBundle($refNo, $bundle);
+ if ($block === '') {
+ continue;
+ }
+ if (trim((string)($bundle['abstract_final'] ?? '')) !== '') {
+ $hasAbstract = true;
+ }
+ $doiParts[] = $block;
+ $doi = trim((string)($bundle['doi'] ?? ''));
+ if ($doi !== '') {
+ $doiUsed[] = $doi;
+ }
+ }
+
+ return [
+ 'refer_text' => $referText,
+ 'doi_block' => implode("\n\n", $doiParts),
+ 'has_abstract' => $hasAbstract,
+ 'doi_used' => implode(',', $doiUsed),
+ ];
+ }
+
+ private function applyCheckResultFromRow($checkId, array $sourceRow)
+ {
+ $this->updateCheckResult($checkId, [
+ 'can_support' => intval($this->arrGet($sourceRow, 'can_support', 0)),
+ 'is_match' => intval($this->arrGet($sourceRow, 'is_match', 0)),
+ 'confidence' => floatval($this->arrGet($sourceRow, 'confidence', 0)),
+ 'reason' => (string)$this->arrGet($sourceRow, 'reason', ''),
+ 'support_role' => (string)$this->arrGet($sourceRow, 'support_role', ''),
+ 'combined_can_support' => intval($this->arrGet($sourceRow, 'combined_can_support', 0)),
+ 'combined_confidence' => floatval($this->arrGet($sourceRow, 'combined_confidence', 0)),
+ 'combined_reason' => (string)$this->arrGet($sourceRow, 'combined_reason', ''),
+ 'status' => self::RECORD_COMPLETED,
+ 'error_msg' => '',
+ ]);
+ }
+
+ /**
+ * 将 LLM results 数组按 reference_no 写入同一引用组内的各行
+ */
+ private function applyCitationGroupCheckResults(array $groupRows, array $llmResponse, $reasonPrefix = '')
+ {
+ $results = isset($llmResponse['results']) && is_array($llmResponse['results'])
+ ? $llmResponse['results'] : [];
+ if (empty($results)) {
+ return false;
+ }
+
+ $byRefNo = [];
+ foreach ($results as $item) {
+ if (!is_array($item)) {
+ continue;
+ }
+ $refNo = intval($this->arrGet($item, 'reference_no', 0));
+ if ($refNo > 0) {
+ $byRefNo[$refNo] = $item;
+ }
+ }
+
+ $applied = 0;
+ $expected = 0;
+ $reasonPrefix = trim((string)$reasonPrefix);
+ foreach ($groupRows as $gr) {
+ $refNo = intval($this->arrGet($gr, 'reference_no', 0));
+ if ($refNo <= 0) {
+ continue;
+ }
+ $expected++;
+ if (!isset($byRefNo[$refNo])) {
+ continue;
+ }
+ $item = $byRefNo[$refNo];
+ $canSupport = !empty($item['can_support']) ? 1 : 0;
+ $reason = trim($reasonPrefix . ' ' . (string)$this->arrGet($item, 'reason', ''));
+ $this->updateCheckResult($this->resolveCheckRowId($gr), [
+ 'can_support' => $canSupport,
+ 'is_match' => array_key_exists('is_match', $item) ? (!empty($item['is_match']) ? 1 : 0) : $canSupport,
+ 'confidence' => floatval($this->arrGet($item, 'confidence', 0)),
+ 'reason' => $reason,
+ 'support_role' => (string)$this->arrGet($item, 'support_role', ''),
+ 'combined_can_support' => !empty($item['combined_can_support']) ? 1 : 0,
+ 'combined_confidence' => floatval($this->arrGet($item, 'combined_confidence', 0)),
+ 'combined_reason' => (string)$this->arrGet($item, 'combined_reason', ''),
+ 'status' => self::RECORD_COMPLETED,
+ 'error_msg' => '',
+ ]);
+ $applied++;
+ }
+
+ return $expected > 0 && $applied === $expected;
+ }
+
+ private function findLlmResultItemForRow(array $llmResponse, array $row)
+ {
+ $results = isset($llmResponse['results']) && is_array($llmResponse['results'])
+ ? $llmResponse['results'] : [];
+ $refNo = intval($this->arrGet($row, 'reference_no', 0));
+ foreach ($results as $item) {
+ if (is_array($item) && intval($this->arrGet($item, 'reference_no', 0)) === $refNo) {
+ return $item;
+ }
+ }
+
+ return null;
+ }
+
+ private function formatCheckReturnFromRow(array $row)
+ {
+ $checkId = $this->resolveCheckRowId($row);
+
+ return [
+ 'check_id' => $checkId,
+ 'can_support' => intval($this->arrGet($row, 'can_support', 0)),
+ 'is_match' => intval($this->arrGet($row, 'is_match', 0)),
+ 'confidence' => floatval($this->arrGet($row, 'confidence', 0)),
+ 'reason' => (string)$this->arrGet($row, 'reason', ''),
+ 'support_role' => (string)$this->arrGet($row, 'support_role', ''),
+ 'combined_can_support' => intval($this->arrGet($row, 'combined_can_support', 0)),
+ 'combined_confidence' => floatval($this->arrGet($row, 'combined_confidence', 0)),
+ 'combined_reason' => (string)$this->arrGet($row, 'combined_reason', ''),
+ ];
+ }
+
+ private function shouldRunSecondPassForLlmResults(array $groupRows, array $llmResponse)
+ {
+ if (!$this->isSecondPassEnabled()) {
+ return false;
+ }
+
+ $results = isset($llmResponse['results']) && is_array($llmResponse['results'])
+ ? $llmResponse['results'] : [];
+ if (empty($results)) {
+ return false;
+ }
+
+ $byRefNo = [];
+ foreach ($results as $item) {
+ if (!is_array($item)) {
+ continue;
+ }
+ $refNo = intval($this->arrGet($item, 'reference_no', 0));
+ if ($refNo > 0) {
+ $byRefNo[$refNo] = $item;
+ }
+ }
+
+ foreach ($groupRows as $gr) {
+ $refNo = intval($this->arrGet($gr, 'reference_no', 0));
+ if ($refNo <= 0 || !isset($byRefNo[$refNo])) {
+ continue;
+ }
+ $item = $byRefNo[$refNo];
+ if (floatval($this->arrGet($item, 'confidence', 0)) <= self::PASS_CONFIDENCE_THRESHOLD) {
+ return true;
+ }
+ if (floatval($this->arrGet($item, 'combined_confidence', 0)) <= self::PASS_CONFIDENCE_THRESHOLD) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * 本引用位置附近上下文(用于 LLM 判断具体支撑哪句)
+ */
+ public function resolveCitationLocalContextForJob(array $row, $maxChars = 3000)
+ {
+ $textStart = intval($this->arrGet($row, 'text_start', 0));
+ $textEnd = intval($this->arrGet($row, 'text_end', 0));
+ $amId = intval($this->arrGet($row, 'am_id', 0));
+ if ($amId <= 0 || $textEnd <= $textStart) {
+ return '';
+ }
+
+ $main = Db::name('article_main')
+ ->field('content,type,amt_id,article_id')
+ ->where('am_id', $amId)
+ ->find();
+ if (empty($main)) {
+ return '';
+ }
+
+ $raw = trim($this->resolveArticleMainCheckContent($main));
+ if ($raw === '') {
+ return '';
+ }
+
+ $slice = $this->buildCitationContextText($raw, $textStart, $textEnd);
+ if (trim($slice) === '') {
+ return '';
+ }
+
+ return $this->normalizeCheckContentForLlm($slice, $maxChars);
+ }
+
+ /**
+ * 相关性校对专用:段内后续引用在「本句」基础上再向前扩展 1–2 句(不早于上一引用标签),
+ * 以便覆盖紧邻的前置 claim,同时避免整段混用多个引用点的论述。
+ */
+ public function resolveCitationLocalContextForRelevanceJob(array $row, $maxChars = 3000, $extraSentences = 2)
+ {
+ DbReconnectHelper::ensure();
+ $textStart = intval($this->arrGet($row, 'text_start', 0));
+ $textEnd = intval($this->arrGet($row, 'text_end', 0));
+ $amId = intval($this->arrGet($row, 'am_id', 0));
+ $tagStart = intval($this->arrGet($row, 'cite_tag_start', 0));
+ $fallback = trim((string)$this->arrGet($row, 'origin_text', ''));
+
+ if ($amId <= 0 || $textEnd <= $textStart) {
+ return $fallback;
+ }
+
+ $main = Db::name('article_main')
+ ->field('content,type,amt_id,article_id')
+ ->where('am_id', $amId)
+ ->find();
+ if (empty($main)) {
+ return $fallback;
+ }
+
+ $raw = trim($this->resolveArticleMainCheckContent($main));
+ if ($raw === '') {
+ return $fallback;
+ }
+
+ $paragraphStart = $tagStart > 0 ? $this->findParagraphStart($raw, $tagStart) : 0;
+ $prevTagEnd = $tagStart > 0 ? $this->resolvePriorCitationTagEnd($raw, $tagStart) : $paragraphStart;
+ $extendedStart = $textStart;
+ if ($prevTagEnd > $paragraphStart) {
+ $extendedStart = $this->extendContextStartBackward(
+ $raw,
+ $textStart,
+ max($paragraphStart, $prevTagEnd),
+ $extraSentences
+ );
+ }
+
+ $slice = $this->buildCitationContextText($raw, $extendedStart, $textEnd);
+ $slice = ltrim($slice, ". \t\n\r");
+ if (trim($slice) === '') {
+ return $fallback;
+ }
+
+ return $this->normalizeCheckContentForLlm($slice, $maxChars);
+ }
+
+ /**
+ * 联合校对结果写回同一引用标签下的全部明细
+ */
+ private function applyCheckResultToCitationGroup(array $groupRows, array $fields)
+ {
+ foreach ($groupRows as $gr) {
+ $gid = $this->resolveCheckRowId($gr);
+ if ($gid > 0) {
+ $this->updateCheckResult($gid, $fields);
+ }
+ }
+ }
+
/**
* 编辑某条文献内容后,按 p_refer_id 异步重新校对该文献对应的全部 check 明细
*
@@ -1597,18 +2424,12 @@ class ReferenceCheckService
];
}
- $resetFields = $this->newCheckRecordFields([
+ $resetFields = $this->newCheckRecordFields($this->referenceCheckResultResetFields([
'refer_text' => $referText,
'refer_index' => $referenceNo,
'reference_no' => $referenceNo,
- 'status' => self::RECORD_PENDING,
- 'is_match' => 0,
- 'can_support' => 0,
- 'confidence' => 0,
- 'reason' => '',
- 'error_msg' => '',
'updated_at' => $now,
- ], self::QUEUE_PENDING, 0);
+ ]), self::QUEUE_PENDING, 0);
$pendingJobs = [];
$amIds = [];
@@ -1686,15 +2507,9 @@ class ReferenceCheckService
}
$now = date('Y-m-d H:i:s');
- $resetFields = $this->newCheckRecordFields([
- 'status' => self::RECORD_PENDING,
- 'is_match' => 0,
- 'can_support' => 0,
- 'confidence' => 0,
- 'reason' => '',
- 'error_msg' => '',
- 'updated_at' => $now,
- ], self::QUEUE_PENDING, 0);
+ $resetFields = $this->newCheckRecordFields($this->referenceCheckResultResetFields([
+ 'updated_at' => $now,
+ ]), self::QUEUE_PENDING, 0);
$pendingJobs = [];
$amIds = [];
@@ -1729,6 +2544,93 @@ class ReferenceCheckService
];
}
+ /**
+ * 某条参考文献下「校对失败」重跑,并将失败行所在同一引用标签分组(如 [1,2])全部一并重跑。
+ *
+ * @param int $pReferId
+ * @param int $pArticleId
+ * @return array{p_refer_id:int, p_article_id:int, reset:int, queued:int, check_ids:int[], queue:string}
+ */
+ public function enqueueRecheckFailedByPReferIdWithGroup($pReferId, $pArticleId = 0)
+ {
+ $pReferId = intval($pReferId);
+ if ($pReferId <= 0) {
+ throw new \InvalidArgumentException('p_refer_id is required');
+ }
+
+ $q = Db::name('article_reference_check_result')
+ ->where('p_refer_id', $pReferId)
+ ->where('status', self::RECORD_FAILED);
+ $pArticleId = intval($pArticleId);
+ if ($pArticleId > 0) {
+ $q->where('p_article_id', $pArticleId);
+ }
+
+ $rows = $q->select();
+ if (empty($rows)) {
+ return [
+ 'p_refer_id' => $pReferId,
+ 'p_article_id' => $pArticleId,
+ 'reset' => 0,
+ 'queued' => 0,
+ 'check_ids' => [],
+ 'queue' => self::TRANSPORT_RABBITMQ,
+ ];
+ }
+
+ if ($pArticleId <= 0) {
+ $pArticleId = intval($this->arrGet($rows[0], 'p_article_id', 0));
+ }
+
+ $now = date('Y-m-d H:i:s');
+ $resetFields = $this->newCheckRecordFields($this->referenceCheckResultResetFields([
+ 'updated_at' => $now,
+ ]), self::QUEUE_PENDING, 0);
+
+ $targetRows = [];
+ foreach ($rows as $row) {
+ $groupRows = $this->findCitationGroupRows($row);
+ foreach ($groupRows as $gr) {
+ $checkId = $this->resolveCheckRowId($gr);
+ if ($checkId > 0) {
+ $targetRows[$checkId] = $gr;
+ }
+ }
+ }
+
+ $pendingJobs = [];
+ $amIds = [];
+ foreach ($targetRows as $row) {
+ $checkId = $this->resolveCheckRowId($row);
+ Db::name('article_reference_check_result')->where('id', $checkId)->update($resetFields);
+ $pendingJobs[] = [
+ 'check_id' => $checkId,
+ 'reference_no' => intval($this->arrGet($row, 'reference_no', 0)),
+ 'am_id' => intval($this->arrGet($row, 'am_id', 0)),
+ 'text_start' => intval($this->arrGet($row, 'text_start', 0)),
+ ];
+ $amId = intval($this->arrGet($row, 'am_id', 0));
+ if ($amId > 0) {
+ $amIds[$amId] = true;
+ }
+ }
+
+ foreach (array_keys($amIds) as $amId) {
+ $this->setAmRefCheckStatus($amId, self::AM_STATUS_RUNNING);
+ }
+
+ $checkIds = $this->enqueueChecksSortedByReferenceNo($pendingJobs, $pArticleId, 'recheck_failed');
+
+ return [
+ 'p_refer_id' => $pReferId,
+ 'p_article_id' => $pArticleId,
+ 'reset' => count($targetRows),
+ 'queued' => count($checkIds),
+ 'check_ids' => $checkIds,
+ 'queue' => self::TRANSPORT_RABBITMQ,
+ ];
+ }
+
public function recheckByRefer($articleId, $pReferId = 0, $referenceNo = 0)
{
$articleId = intval($articleId);
@@ -1763,19 +2665,13 @@ class ReferenceCheckService
];
}
- $resetFields = $this->newCheckRecordFields([
+ $resetFields = $this->newCheckRecordFields($this->referenceCheckResultResetFields([
'refer_text' => $referText,
'p_refer_id' => $pReferId,
'p_article_id' => $pArticleId,
'refer_index' => $referenceNo,
- 'status' => 0,
- 'is_match' => 0,
- 'can_support' => 0,
- 'confidence' => 0,
- 'reason' => '',
- 'error_msg' => '',
'updated_at' => $now,
- ], self::QUEUE_PENDING, 0);
+ ]), self::QUEUE_PENDING, 0);
$pendingJobs = [];
$amIds = [];
@@ -1847,13 +2743,34 @@ class ReferenceCheckService
*/
public function runReferenceCheckOnce($checkId)
{
+ DbReconnectHelper::ensure();
$checkId = intval($checkId);
$row = Db::name('article_reference_check_result')->where('id', $checkId)->find();
if (empty($row)) {
throw new \RuntimeException('article_reference_check_result not found, id=' . $checkId);
}
- $contentA = $this->resolveMainContentForJob($row);
+ if (intval($row['status']) === self::RECORD_COMPLETED) {
+ return $this->formatCheckReturnFromRow($row);
+ }
+
+ $groupRows = $this->findCitationGroupRows($row);
+ $isGroup = $this->isCitationGroupCheck($groupRows);
+ if ($isGroup) {
+ $leaderRefNo = $this->resolveCitationGroupLeaderRefNo($groupRows);
+ $currentRefNo = intval($this->arrGet($row, 'reference_no', 0));
+ if ($currentRefNo !== $leaderRefNo) {
+ $freshRow = Db::name('article_reference_check_result')->where('id', $checkId)->find();
+ if (!empty($freshRow) && intval($freshRow['status']) === self::RECORD_COMPLETED) {
+ return $this->formatCheckReturnFromRow($freshRow);
+ }
+ throw new \RuntimeException('Citation group leader check not finished yet, reference_no=' . $leaderRefNo);
+ }
+ }
+
+ $contentA = $this->resolveParagraphContextForJob($row);
+ $localContext = $this->resolveCitationLocalContextForJob($row);
+ $citeGroupRefs = $this->resolveCiteGroupRefsFromRow($row, $groupRows);
$refer = null;
if (intval($row['p_refer_id']) > 0) {
$refer = Db::name('production_article_refer')
@@ -1862,95 +2779,151 @@ class ReferenceCheckService
->find();
}
- if ($refer) {
- $contentB = $this->formatReferForLlm($refer);
- } else {
- $contentB = trim((string)$this->arrGet($row, 'refer_text', ''));
- }
+ $contentB = $this->buildCombinedReferTextForCitationGroup($groupRows);
+ DbReconnectHelper::release();
+ $doiPayload = $this->prepareRecheckPayloadForCitationGroup($groupRows);
+ $doiBlock = trim((string)$this->arrGet($doiPayload, 'doi_block', ''));
+ DbReconnectHelper::ensure();
if ($contentA === '' || $contentB === '') {
- $this->updateCheckResult($checkId, [
+ $failFields = [
'status' => self::RECORD_FAILED,
'error_msg' => 'Missing section content (text/table) or refer_text',
- ]);
+ ];
+ if ($isGroup) {
+ $this->applyCheckResultToCitationGroup($groupRows, $failFields);
+ } else {
+ $this->updateCheckResult($checkId, $failFields);
+ }
throw new \RuntimeException('Missing section content (text/table) or refer_text');
}
- $llmResult = (new LLMService())->checkReference($contentA, $contentB, false);
+ DbReconnectHelper::release();
+ $llmResult = (new LLMService())->checkReference(
+ $contentA,
+ $contentB,
+ false,
+ $doiBlock !== '' ? $doiBlock : null,
+ $citeGroupRefs,
+ $localContext
+ );
+ DbReconnectHelper::ensure();
$requestFailed = !empty($llmResult['request_failed']);
- $canSupport = $this->parseLlmCanSupport($llmResult);
- $confidence = floatval(isset($llmResult['confidence']) ? $llmResult['confidence'] : 0);
- $reason = isset($llmResult['reason']) ? $llmResult['reason'] : '';
- // LLM 通讯失败:写 status=RECORD_FAILED(3) + error_msg,抛异常由 MQ worker 重试
- if ($requestFailed) {
- $this->updateCheckResult($checkId, [
- 'confidence' => $confidence,
- 'reason' => $reason,
- 'status' => self::RECORD_FAILED,
- 'error_msg' => $reason,
- ]);
- throw new \RuntimeException($reason !== '' ? $reason : 'LLM request failed');
+ if ($requestFailed || !$this->applyCitationGroupCheckResults($groupRows, $llmResult)) {
+ $failReason = isset($llmResult['reason']) ? (string)$llmResult['reason'] : 'LLM request failed or empty results';
+ $failFields = [
+ 'status' => self::RECORD_FAILED,
+ 'error_msg' => $failReason,
+ ];
+ if ($isGroup) {
+ $this->applyCheckResultToCitationGroup($groupRows, $failFields);
+ } else {
+ $this->updateCheckResult($checkId, $failFields);
+ }
+ throw new \RuntimeException($failReason !== '' ? $failReason : 'LLM request failed');
}
- $this->updateCheckResult($checkId, [
- 'can_support' => $canSupport ? 1 : 0,
- 'is_match' => $canSupport ? 1 : 0,
- 'confidence' => $confidence,
- 'reason' => $reason,
- 'status' => self::RECORD_COMPLETED,
- 'error_msg' => '',
- ]);
-
- if ($confidence <= self::PASS_CONFIDENCE_THRESHOLD) {
- $this->runSecondPassBlocking($checkId, $row, $contentA, $refer, $contentB);
+ if ($this->shouldRunSecondPassForLlmResults($groupRows, $llmResult)) {
+ $this->runSecondPassBlocking($checkId, $row, $contentA, $refer, $contentB, $groupRows);
}
- return [
- 'check_id' => $checkId,
- 'can_support' => $canSupport ? 1 : 0,
- 'is_match' => $canSupport ? 1 : 0,
- 'confidence' => $confidence,
- 'reason' => $reason,
- ];
+ $freshRow = Db::name('article_reference_check_result')->where('id', $checkId)->find();
+ return $this->formatCheckReturnFromRow(!empty($freshRow) ? $freshRow : $row);
}
/**
* 低分结果的二轮 DOI 复核(同步阻塞执行;失败重试一次)
*/
- public function runSecondPassBlocking($checkId, array $row, $contentA, $refer, $referText)
+ public function runSecondPassBlocking($checkId, array $row, $contentA, $refer, $referText, array $groupRows = null)
{
+ if (!$this->isSecondPassEnabled()) {
+ return false;
+ }
+
+ DbReconnectHelper::ensure();
$checkId = intval($checkId);
if ($checkId <= 0) {
return false;
}
- $payload = $this->prepareRecheckPayload(is_array($refer) ? $refer : [], trim((string)$referText));
+ if ($groupRows === null) {
+ $groupRows = $this->findCitationGroupRows($row);
+ }
+ $isGroup = $this->isCitationGroupCheck($groupRows);
+
+ if ($isGroup) {
+ $leaderRefNo = $this->resolveCitationGroupLeaderRefNo($groupRows);
+ $currentRefNo = intval($this->arrGet($row, 'reference_no', 0));
+ if ($currentRefNo !== $leaderRefNo) {
+ $freshRow = Db::name('article_reference_check_result')->where('id', $checkId)->find();
+ if (!empty($freshRow) && $this->hasSecondPassCompleted($freshRow)) {
+ return true;
+ }
+ return false;
+ }
+ }
+
+ if (trim((string)$contentA) === '') {
+ $contentA = $this->resolveParagraphContextForJob($row);
+ }
+ $localContext = $this->resolveCitationLocalContextForJob($row);
+
+ DbReconnectHelper::release();
+ if ($isGroup) {
+ $payload = $this->prepareRecheckPayloadForCitationGroup($groupRows);
+ $referText = trim((string)$payload['refer_text']);
+ } else {
+ if (trim((string)$referText) === '') {
+ $referText = $this->resolveReferTextForCheckRow($row, is_array($refer) ? $refer : null);
+ }
+ $payload = $this->prepareRecheckPayload(is_array($refer) ? $refer : [], trim((string)$referText));
+ }
+ DbReconnectHelper::ensure();
if (empty($payload['has_abstract']) || trim((string)$payload['doi_block']) === '') {
return false;
}
+ $citeGroupRefs = $this->resolveCiteGroupRefsFromRow($row, $groupRows);
$lastError = '';
for ($attempt = 0; $attempt < 2; $attempt++) {
try {
- $llmResult = (new LLMService())->checkReference($contentA, trim((string)$referText), true, $payload['doi_block']);
+ DbReconnectHelper::release();
+ $llmResult = (new LLMService())->checkReference(
+ $contentA,
+ trim((string)$referText),
+ true,
+ $payload['doi_block'],
+ $citeGroupRefs,
+ $localContext
+ );
+ DbReconnectHelper::ensure();
$requestFailed = !empty($llmResult['request_failed']);
- $canSupport = $this->parseLlmCanSupport($llmResult);
- $confidence = floatval(isset($llmResult['confidence']) ? $llmResult['confidence'] : 0);
- $tag = '[Crossref复核' . (trim((string)$payload['doi_used']) !== '' ? (' ' . trim((string)$payload['doi_used'])) : '') . ']';
- $reason = $tag . ' ' . (isset($llmResult['reason']) ? $llmResult['reason'] : '');
+ $tag = $this->buildSecondPassReasonTag($row, $payload, $groupRows);
+ if ($tag !== '' && !empty($llmResult['results']) && is_array($llmResult['results'])) {
+ foreach ($llmResult['results'] as &$one) {
+ if (!is_array($one)) {
+ continue;
+ }
+ $one['reason'] = $tag . ' ' . (isset($one['reason']) ? (string)$one['reason'] : '');
+ }
+ unset($one);
+ }
- if ($requestFailed) {
- $lastError = isset($llmResult['reason']) ? (string)$llmResult['reason'] : 'LLM request failed';
+ if ($requestFailed || !$this->applyCitationGroupCheckResults($groupRows, $llmResult)) {
+ $lastError = isset($llmResult['reason']) ? (string)$llmResult['reason'] : 'LLM request failed or empty results';
if ($attempt < 1) {
continue;
}
- $this->updateCheckResult($checkId, [
- 'confidence' => $confidence,
- 'reason' => $reason,
- 'status' => self::RECORD_FAILED,
- 'error_msg' => $lastError,
- ]);
+ $failFields = [
+ 'status' => self::RECORD_FAILED,
+ 'error_msg' => $lastError,
+ ];
+ if ($isGroup) {
+ $this->applyCheckResultToCitationGroup($groupRows, $failFields);
+ } else {
+ $this->updateCheckResult($checkId, $failFields);
+ }
$amId = intval(isset($row['am_id']) ? $row['am_id'] : 0);
if ($amId > 0) {
$this->syncAmRefCheckStatus($amId);
@@ -1958,14 +2931,6 @@ class ReferenceCheckService
return false;
}
- $this->updateCheckResult($checkId, [
- 'can_support' => $canSupport ? 1 : 0,
- 'is_match' => $canSupport ? 1 : 0,
- 'confidence' => $confidence,
- 'reason' => $reason,
- 'status' => self::RECORD_COMPLETED,
- 'error_msg' => '',
- ]);
$amId = intval(isset($row['am_id']) ? $row['am_id'] : 0);
if ($amId > 0) {
$this->syncAmRefCheckStatus($amId);
@@ -1976,10 +2941,15 @@ class ReferenceCheckService
if ($attempt < 1) {
continue;
}
- $this->updateCheckResult($checkId, [
+ $failFields = [
'status' => self::RECORD_FAILED,
'error_msg' => $lastError,
- ]);
+ ];
+ if ($isGroup) {
+ $this->applyCheckResultToCitationGroup($groupRows, $failFields);
+ } else {
+ $this->updateCheckResult($checkId, $failFields);
+ }
$amId = intval(isset($row['am_id']) ? $row['am_id'] : 0);
if ($amId > 0) {
$this->syncAmRefCheckStatus($amId);
@@ -2087,6 +3057,18 @@ class ReferenceCheckService
if (!is_array($llmResult)) {
return false;
}
+ if (!empty($llmResult['results']) && is_array($llmResult['results'])) {
+ foreach ($llmResult['results'] as $item) {
+ if (!is_array($item)) {
+ continue;
+ }
+ if (!empty($item['can_support']) || !empty($item['is_match'])) {
+ return true;
+ }
+ }
+
+ return false;
+ }
if (array_key_exists('can_support', $llmResult)) {
return $this->parseLlmIsMatch($llmResult['can_support']);
}
@@ -2098,6 +3080,7 @@ class ReferenceCheckService
*/
public function resolveMainContentForJob(array $row, $maxChars = 8000)
{
+ DbReconnectHelper::ensure();
$amId = intval($this->arrGet($row, 'am_id', 0));
if ($amId <= 0) {
return '';
@@ -2387,10 +3370,14 @@ class ReferenceCheckService
}
/**
- * 引用处局部上下文(origin_text),供其它场景使用
+ * 引用处局部上下文:优先按 text_start/text_end 从节正文重算,回落 origin_text 快照
*/
public function resolveCitationContextForJob(array $row)
{
+ $local = $this->resolveCitationLocalContextForJob($row);
+ if ($local !== '') {
+ return $local;
+ }
$text = trim((string)$this->arrGet($row, 'origin_text', ''));
if ($text === '') {
$text = trim((string)$this->arrGet($row, 'content_a', ''));
@@ -2503,6 +3490,8 @@ class ReferenceCheckService
*/
public function fetchDoiLiteratureBlock($refer)
{
+ DbReconnectHelper::release();
+
$candidates = $this->extractAllDoiCandidatesFromRefer($refer);
if (empty($candidates)) {
return '';
@@ -2670,15 +3659,80 @@ class ReferenceCheckService
public function prepareRecheckPayload($refer, $referText = '')
{
$base = trim($referText) !== '' ? trim($referText) : $this->formatReferForLlm($refer);
- $cr = $this->fetchCrossrefAbstractByReferDoi($refer);
+ $bundle = (new ReferenceLiteratureFetchService())->fetchAndCleanForRefer(is_array($refer) ? $refer : []);
+ $block = $this->buildLiteratureBlockFromBundle(0, $bundle);
+ if ($block === '') {
+ $cr = $this->resolveDoiRecheckFromRefer(is_array($refer) ? $refer : []);
+ return [
+ 'refer_text' => $base,
+ 'doi_block' => $cr['text'],
+ 'has_abstract' => $cr['has_abstract'],
+ 'doi_used' => $cr['doi'],
+ ];
+ }
return [
'refer_text' => $base,
- 'doi_block' => $cr['text'],
- 'has_abstract' => $cr['has_abstract'],
- 'doi_used' => $cr['doi'],
+ 'doi_block' => $block,
+ 'has_abstract' => trim((string)($bundle['abstract_final'] ?? '')) !== '',
+ 'doi_used' => trim((string)($bundle['doi'] ?? '')),
];
}
+ private function buildLiteratureBlockFromBundle($refNo, array $bundle)
+ {
+ $abstract = trim((string)($bundle['abstract_final'] ?? $bundle['abstract'] ?? ''));
+ $cleaned = trim((string)($bundle['content_cleaned'] ?? ''));
+ $raw = trim((string)($bundle['raw_content'] ?? ''));
+ if ($cleaned === '' && $raw !== '') {
+ $cleaned = mb_substr($raw, 0, 6000);
+ }
+ if ($abstract === '' && $cleaned === '') {
+ return '';
+ }
+
+ $head = $refNo > 0 ? ('【参考文献 ' . intval($refNo) . '】') : '【文献内容】';
+ $doi = trim((string)($bundle['doi'] ?? ''));
+ if ($doi !== '') {
+ $head .= ' DOI: ' . $doi;
+ }
+ $parts = [$head];
+ if ($abstract !== '') {
+ $parts[] = '【摘要】' . "\n" . $abstract;
+ }
+ if ($cleaned !== '') {
+ $parts[] = '【清洗后文献内容】' . "\n" . $cleaned;
+ }
+ $sources = isset($bundle['sources']) && is_array($bundle['sources']) ? implode(',', $bundle['sources']) : '';
+ if ($sources !== '') {
+ $parts[] = 'Sources: ' . $sources;
+ }
+ return implode("\n\n", $parts);
+ }
+
+ private function persistLiteratureOnCheckRow($checkId, array $bundle)
+ {
+ $checkId = intval($checkId);
+ if ($checkId <= 0) {
+ return;
+ }
+ $abstract = trim((string)($bundle['abstract_final'] ?? $bundle['abstract'] ?? ''));
+ $raw = trim((string)($bundle['raw_content'] ?? ''));
+ $cleaned = trim((string)($bundle['content_cleaned'] ?? ''));
+ if ($cleaned === '' && $raw !== '') {
+ $cleaned = mb_substr($raw, 0, 6000);
+ }
+ try {
+ DbReconnectHelper::ensure();
+ Db::name('article_reference_check_result')->where('id', $checkId)->update([
+ 'abstract_text' => $abstract,
+ 'refer_content_cleaned' => $cleaned,
+ 'updated_at' => date('Y-m-d H:i:s'),
+ ]);
+ } catch (\Throwable $e) {
+ \think\Log::warning('persistLiteratureOnCheckRow: ' . $e->getMessage());
+ }
+ }
+
/**
* 旧接口:拼接成单块文本(向后兼容,建议调用方改用 prepareRecheckPayload)
*/
@@ -2710,6 +3764,24 @@ class ReferenceCheckService
return false;
}
+ if ($this->hasSecondPassCompleted($row)) {
+ return true;
+ }
+
+ $groupRows = $this->findCitationGroupRows($row);
+ $isGroup = $this->isCitationGroupCheck($groupRows);
+ if ($isGroup) {
+ $leaderRefNo = $this->resolveCitationGroupLeaderRefNo($groupRows);
+ $currentRefNo = intval($this->arrGet($row, 'reference_no', 0));
+ if ($currentRefNo !== $leaderRefNo) {
+ $freshRow = Db::name('article_reference_check_result')->where('id', $checkId)->find();
+ if (!empty($freshRow) && $this->hasSecondPassCompleted($freshRow)) {
+ return true;
+ }
+ return false;
+ }
+ }
+
$refer = null;
if (intval($row['p_refer_id']) > 0) {
$refer = Db::name('production_article_refer')
@@ -2717,22 +3789,15 @@ class ReferenceCheckService
->where('state', 0)
->find();
}
- if (empty($refer) || $this->extractReferDoiOnly($refer) === '') {
- return false;
+
+ $contentA = $this->resolveParagraphContextForJob($row);
+ if ($isGroup) {
+ $referText = $this->buildCombinedReferTextForCitationGroup($groupRows);
+ } else {
+ $referText = $this->resolveReferTextForCheckRow($row, $refer);
}
- $cr = $this->fetchCrossrefAbstractByReferDoi($refer);
- if (empty($cr['has_abstract'])) {
- return false;
- }
-
- $contentA = $this->resolveMainContentForJob($row);
- $referText = trim((string)$this->arrGet($row, 'refer_text', ''));
- if ($referText === '' && is_array($refer)) {
- $referText = $this->formatReferForLlm($refer);
- }
-
- return $this->runSecondPassBlocking($checkId, $row, $contentA, $refer, $referText);
+ return $this->runSecondPassBlocking($checkId, $row, $contentA, $refer, $referText, $groupRows);
}
/**
@@ -2787,6 +3852,27 @@ class ReferenceCheckService
return $result;
}
+ /**
+ * 按段落截取引用上下文:同一段落内各处引用共用段落文本,分别按 cite_group_refs 校对。
+ */
+ private function extractParagraphCitationContext($content, $tagStart, $tagEnd, array $tagSpans)
+ {
+ $paragraphStart = $this->findParagraphStart($content, $tagStart);
+ $paragraphEnd = $this->findParagraphEnd($content, $tagEnd);
+ $originalText = $this->buildCitationContextText($content, $paragraphStart, $paragraphEnd);
+
+ if (!$this->isMeaningfulCitationContext($originalText)) {
+ return $this->extractLocalCitationContext(
+ $content,
+ $tagStart,
+ $tagEnd,
+ $tagSpans
+ );
+ }
+
+ return [$paragraphStart, $paragraphEnd, $originalText];
+ }
+
/**
* 按引用位置截取局部上下文:优先取标签前叙述;同句多引时后续引用从上一标签后开始。
*/
@@ -2929,6 +4015,89 @@ class ReferenceCheckService
return $text;
}
+ /**
+ * 同段落内、当前引用标签之前最近一个引用标签的结束字节位置
+ */
+ private function resolvePriorCitationTagEnd($content, $tagStart)
+ {
+ $tagStart = intval($tagStart);
+ $paragraphStart = $this->findParagraphStart($content, $tagStart);
+ $prevTagEnd = $paragraphStart;
+
+ $matches = $this->collectBlueTagMatches($content);
+ if (empty($matches[0])) {
+ return $paragraphStart;
+ }
+
+ foreach ($matches[0] as $match) {
+ $end = intval($match[1]) + strlen($match[0]);
+ if ($end <= $tagStart && $end > $prevTagEnd) {
+ $prevTagEnd = $end;
+ }
+ }
+
+ return $prevTagEnd;
+ }
+
+ /**
+ * 给定当前句起点,返回上一句起点
+ */
+ private function findPreviousSentenceStart($content, $sentenceStart)
+ {
+ $sentenceStart = intval($sentenceStart);
+ if ($sentenceStart <= 0) {
+ return 0;
+ }
+
+ $pos = $sentenceStart - 1;
+ while ($pos > 0 && isset($content[$pos]) && ctype_space($content[$pos])) {
+ $pos--;
+ }
+ if ($pos <= 0) {
+ return 0;
+ }
+
+ $prev = $this->findSentenceStart($content, $pos);
+ if ($prev >= $sentenceStart) {
+ $pos--;
+ while ($pos > 0 && isset($content[$pos]) && ctype_space($content[$pos])) {
+ $pos--;
+ }
+ if ($pos <= 0) {
+ return 0;
+ }
+ $prev = $this->findSentenceStart($content, $pos);
+ }
+
+ return max(0, $prev);
+ }
+
+ /**
+ * 从当前句起点向前扩展若干完整句子,但不早于 $minStart
+ */
+ private function extendContextStartBackward($content, $start, $minStart, $extraSentences = 2)
+ {
+ $start = intval($start);
+ $minStart = max(0, intval($minStart));
+ $extraSentences = max(0, intval($extraSentences));
+ if ($extraSentences === 0 || $start <= $minStart) {
+ return max($minStart, $start);
+ }
+
+ for ($i = 0; $i < $extraSentences; $i++) {
+ if ($start <= $minStart) {
+ break;
+ }
+ $prev = $this->findPreviousSentenceStart($content, $start);
+ if ($prev >= $start) {
+ break;
+ }
+ $start = max($minStart, $prev);
+ }
+
+ return $start;
+ }
+
/**
* 过滤仅标点、过短或无字母/汉字的上下文(如去掉标签后只剩 ".")
*/
@@ -3049,6 +4218,36 @@ class ReferenceCheckService
return $best;
}
+ /**
+ * 段落结束(
、双换行、下一段 之前)
+ */
+ private function findParagraphEnd($content, $tagEnd)
+ {
+ $length = strlen($content);
+ $pos = max(0, intval($tagEnd));
+ if ($pos >= $length) {
+ return $length;
+ }
+
+ $candidates = [$length];
+
+ if (preg_match('/<\/p>/i', $content, $m, PREG_OFFSET_CAPTURE, $pos)) {
+ $candidates[] = intval($m[0][1]) + strlen($m[0][0]);
+ }
+ if (preg_match('/
]*>/i', $content, $m, PREG_OFFSET_CAPTURE, $pos + 1)) {
+ $candidates[] = intval($m[0][1]);
+ }
+ if (preg_match('/\n\n/', $content, $m, PREG_OFFSET_CAPTURE, $pos)) {
+ $candidates[] = intval($m[0][1]);
+ }
+ if (preg_match('/
\s*
/i', $content, $m, PREG_OFFSET_CAPTURE, $pos)) {
+ $candidates[] = intval($m[0][1]) + strlen($m[0][0]);
+ }
+
+ $end = min($candidates);
+ return max($pos, $end);
+ }
+
/**
* 段落过长时从引用处向前截取上限,避免单次 LLM 上下文过大
*/
diff --git a/application/common/mq/RabbitMqConfig.php b/application/common/mq/RabbitMqConfig.php
index df30aa5e..84e19c17 100644
--- a/application/common/mq/RabbitMqConfig.php
+++ b/application/common/mq/RabbitMqConfig.php
@@ -21,4 +21,10 @@ class RabbitMqConfig
$rc = self::get('reference_check', []);
return is_array($rc) ? $rc : [];
}
+
+ public static function referenceRelevance()
+ {
+ $rc = self::get('reference_relevance', []);
+ return is_array($rc) ? $rc : [];
+ }
}
diff --git a/application/common/mq/ReferenceCheckArticleWorker.php b/application/common/mq/ReferenceCheckArticleWorker.php
index e71da22d..46f61026 100644
--- a/application/common/mq/ReferenceCheckArticleWorker.php
+++ b/application/common/mq/ReferenceCheckArticleWorker.php
@@ -3,6 +3,7 @@
namespace app\common\mq;
use think\Db;
+use app\common\DbReconnectHelper;
use app\common\ReferenceCheckService;
/**
@@ -25,6 +26,7 @@ class ReferenceCheckArticleWorker
public function handleMessage(array $payload)
{
+ DbReconnectHelper::ensure();
$pArticleId = intval(isset($payload['p_article_id']) ? $payload['p_article_id'] : 0);
$batchId = intval(isset($payload['batch_id']) ? $payload['batch_id'] : 0);
if ($pArticleId <= 0 || $batchId <= 0) {
@@ -115,6 +117,7 @@ class ReferenceCheckArticleWorker
*/
private function processOneRow($checkId, array $row)
{
+ DbReconnectHelper::ensure();
$claimed = Db::name('article_reference_check_result')
->where('id', intval($checkId))
->where('queue_status', ReferenceCheckService::QUEUE_PENDING)
@@ -134,6 +137,7 @@ class ReferenceCheckArticleWorker
return 'ok';
} catch (\Exception $e) {
$this->svc->log('ReferenceCheckArticleWorker check_id=' . $checkId . ' err=' . $e->getMessage());
+ DbReconnectHelper::ensure();
if ($retryCount < ReferenceCheckService::QUEUE_MAX_RETRY) {
$this->svc->markQueueRuntime($checkId, ReferenceCheckService::QUEUE_PENDING, $retryCount + 1);
return $this->processOneRow($checkId, array_merge($row, ['retry_count' => $retryCount + 1]));
diff --git a/application/common/service/LLMService.php b/application/common/service/LLMService.php
index 20e25fc1..3daca96a 100644
--- a/application/common/service/LLMService.php
+++ b/application/common/service/LLMService.php
@@ -28,18 +28,17 @@ class LLMService
* @param string $contextText 正文引用处句子
* @param string $referText 参考文献条目(或 refer 格式化文本)
* @param bool $isAgain 是否为 DOI 二次复核
- * @param string|null $doiBlock 可选:系统抓取到的 DOI 真实文献内容(仅二次复核使用)
+ * @param string|null $doiBlock 可选:系统抓取到的 DOI 真实文献内容(仅二次复核使用)
+ * @param string $citeGroupRefs 引用文献组,如 1,2 或 4,5,6
+ * @param string $localContext 本引用位置附近上下文(可选)
+ * @return array{results:array,request_failed?:bool}
*/
- public function checkReference($contextText, $referText, $isAgain = false, $doiBlock = null)
+ public function checkReference($contextText, $referText, $isAgain = false, $doiBlock = null, $citeGroupRefs = '', $localContext = '')
{
- // request_failed=true 表示"LLM 通讯/解析层面的失败"(可重试,区别于业务上的"未命中");
- // 上游 runReferenceCheckOnce 会据此把 DB.status 置为 3(失败) 并抛异常触发 MQ worker 重试
$fallback = [
- 'can_support' => false,
- 'is_match' => false,
- 'confidence' => 0.0,
- 'reason' => 'LLM not configured or request failed',
+ 'results' => [],
'request_failed' => true,
+ 'reason' => 'LLM not configured or request failed',
];
if ($this->url === '' || $this->model === '') {
\think\Log::warning('ReferenceCheck LLM: url or model not configured');
@@ -47,15 +46,16 @@ class LLMService
}
$contextText = trim($contextText);
+ \think\Log::info('llm checkReference:' . $contextText);
$referText = trim($referText);
+ \think\Log::info('llm referText:' . $referText);
$doiBlock = trim((string)$doiBlock);
+ $citeGroupRefs = trim((string)$citeGroupRefs);
+ $localContext = trim((string)$localContext);
if ($contextText === '' || $referText === '') {
- // 空文本是入参问题,不是 LLM 故障,不需要重试
return [
- 'can_support' => false,
- 'is_match' => false,
- 'confidence' => 0.0,
- 'reason' => 'Empty citation context or reference text',
+ 'results' => [],
+ 'reason' => 'Empty citation context or reference text',
];
}
@@ -63,27 +63,30 @@ class LLMService
if (mb_strlen($contextText) > $maxContextLen) {
$contextText = mb_substr($contextText, 0, $maxContextLen);
}
- if (mb_strlen($referText) > 4000) {
- $referText = mb_substr($referText, 0, 4000);
+ if (mb_strlen($localContext) > 3000) {
+ $localContext = mb_substr($localContext, 0, 3000);
}
- if (mb_strlen($doiBlock) > 4000) {
- $doiBlock = mb_substr($doiBlock, 0, 4000);
+ if (mb_strlen($referText) > 6000) {
+ $referText = mb_substr($referText, 0, 6000);
+ }
+ if (mb_strlen($doiBlock) > 8000) {
+ $doiBlock = mb_substr($doiBlock, 0, 8000);
}
if ($isAgain) {
$system = $this->buildReferenceCheckSecondPassPrompt();
- $user = $this->buildReferenceCheckSecondPassUserPrompt($contextText, $referText, $doiBlock);
+ $user = $this->buildReferenceCheckSecondPassUserPrompt($contextText, $referText, $doiBlock, $citeGroupRefs, $localContext);
} else {
$system = $this->buildReferenceCheckFirstPassPrompt();
- $user = $this->buildReferenceCheckFirstPassUserPrompt($contextText, $referText);
+ $user = $this->buildReferenceCheckFirstPassUserPrompt($contextText, $referText, $citeGroupRefs, $localContext, $doiBlock);
}
- \think\Log::info('ReferenceCheck system head: ' . mb_substr($system, 0, 200));
- \think\Log::info('ReferenceCheck user head: ' . mb_substr($user, 0, 600));
+// \think\Log::info('ReferenceCheck system head: ' . mb_substr($system, 0, 200));
+// \think\Log::info('ReferenceCheck user head: ' . mb_substr($user, 0, 600));
$payload = [
- 'model' => $this->model,
+ 'model' => $this->model,
'temperature' => 0,
- 'messages' => [
+ 'messages' => [
['role' => 'system', 'content' => $system],
['role' => 'user', 'content' => $user],
],
@@ -101,23 +104,14 @@ class LLMService
return $fallback;
}
- $canSupport = $this->parseCanSupportFromParsed($parsed);
- $confidence = $this->snapReferenceCheckConfidence(
- $this->normalizeConfidence(isset($parsed['confidence']) ? $parsed['confidence'] : 0),
- $canSupport
- );
- $reason = $this->cleanReason((string)(isset($parsed['reason']) ? $parsed['reason'] : ''));
- \think\Log::info(
- 'ReferenceCheck result: can_support=' . ($canSupport ? '1' : '0')
- . ', confidence=' . $confidence
- . ', reason=' . $reason
- );
- return [
- 'can_support' => $canSupport,
- 'is_match' => $canSupport,
- 'confidence' => $confidence,
- 'reason' => $reason,
- ];
+ $results = $this->parseReferenceCheckResultsFromParsed($parsed, $citeGroupRefs, $localContext, $doiBlock);
+ if (empty($results)) {
+ \think\Log::warning('ReferenceCheck LLM: empty results array');
+ return $fallback;
+ }
+
+ \think\Log::info($results);
+ return ['results' => $results];
}
/**
@@ -174,83 +168,541 @@ class LLMService
$s = strtolower(trim((string)$value));
return in_array($s, ['1', 'true', 'yes', 'support', 'supported'], true);
}
+ private function bulidReferenceCheckFirstPassPrompt(){
+ return <<<'PROMPT'
+你是一名护理、医学与科研期刊的资深文献编辑,专门校对「正文引用句」与「对应参考文献条目」是否匹配。
- /** 第一次校对:书目条目 vs 正文全文 */
+你的目标是严格识别错引、张冠李戴、方法不符、对象不符、结论不成立的问题。
+
+宁可少判 true,也不要漏掉错引。
+
+你只能依据用户提供的内容判断:
+1. 正文引用句
+2. 当前对应参考文献条目
+
+禁止假设已阅读全文。
+禁止联网。
+禁止脑补文献内容。
+禁止根据学科常识推断研究结果。
+
+====================
+【核心任务】
+
+判断:
+
+正文在该引用位置表达的核心观点、结论、方法、数据、定义、模型、研究发现、指南依据等,
+
+是否能够被该条参考文献合理支撑。
+
+你判断的是:
+
+“引用是否成立”
+
+不是:
+
+“正文是否正确”。
+
+====================
+【总原则(最高优先级)】
+
+采用严格审稿标准:
+
+边界不清时,一律判 false。
+
+宁可误杀(人工复核),不要漏掉错引。
+
+同领域 ≠ 匹配。
+
+同关键词 ≠ 匹配。
+
+相关 ≠ 能支撑。
+
+====================
+【强制规则】
+
+1. 严禁关键词硬匹配
+
+不能因为出现:
+患者、护理、治疗、研究、模型、算法、深度学习、机器学习、焦虑、效果
+
+等泛化词汇就判定匹配。
+
+必须看:
+
+- 核心对象
+- 研究问题
+- 方法
+- 场景
+- 结局指标
+- 核心论点
+
+是否一致。
+
+====================
+2. 方法学必须严格一致(极重要)
+
+若正文明确提到:
+
+- 算法
+- 模型
+- 聚类方法
+- 深度学习架构
+- 统计方法
+- 数学模型
+- 评价指标
+
+必须要求文献与其存在明确关联。
+
+例如:
+
+不匹配:
+- fuzzy clustering ≠ deep learning
+- CNN ≠ LSTM
+- random forest ≠ SVM
+- 聚类 ≠ 分类
+- 特征选择 ≠ 分类预测
+- 风险因素分析 ≠ 干预研究
+
+仅属于同一“大领域(AI/ML)”
+不能判定匹配。
+
+若方法体系不同:
+
+优先判 false + 0.10。
+
+====================
+3. 医学护理引用严格一致
+
+若正文涉及:
+
+- 疾病
+- 人群
+- 护理场景
+- 干预措施
+- 结局指标
+
+必须基本一致。
+
+例如:
+
+不匹配:
+- ICU ≠ 普通病房
+- 老年人 ≠ 儿童
+- 糖尿病 ≠ 高血压
+- 心理护理 ≠ 运动干预
+- 焦虑改善 ≠ 生存率提高
+
+====================
+4. 强结论必须强证据
+
+正文若出现:
+
+- 显著改善
+- 明显降低
+- 证实
+- 优于
+- 有效预测
+- 危险因素
+- 因果关系
+
+文献必须能合理支撑该强结论。
+
+仅“应用研究”“相关研究”“观察研究”
+不能自动支持强结论。
+
+否则 false。
+
+====================
+5. 特定证据类型必须一致
+
+正文若明确写:
+
+- RCT/randomized trial
+- Meta-analysis
+- Guideline
+- Systematic review
+- Expert consensus
+
+而参考文献类型明显不符:
+
+直接 false。
+
+====================
+6. 信息不足从严
+
+若参考文献只有:
+
+作者 + 年份
+
+或信息过少,
+
+无法建立明确关联:
+
+false + 0.30
+
+====================
+【判定逻辑】
+
+只有同时满足以下条件,才能 true:
+
+1. 主题一致
+2. 核心对象一致
+3. 核心论点一致
+4. 方法/研究方向一致
+5. 无明显错引风险
+
+任意一点明显不符:
+
+false。
+
+====================
+【评分(只能四选一)】
+
+只能输出:
+
+0.90
+0.75
+0.30
+0.10
+
+禁止任何其他分数。
+
+评分规则:
+
+0.90
+明确匹配:
+主题、对象、方法、核心论点均明显一致。
+
+0.75
+基本匹配:
+整体支撑成立,但存在轻微概括或小范围表述差异。
+
+0.30
+存疑:
+同领域但支撑不足;
+信息不足;
+需人工复核。
+
+0.10
+明确错引:
+主题、对象、方法或核心论点明显不符。
+
+硬规则:
+
+is_match=true
+只能:
+0.75 或 0.90
+
+is_match=false
+只能:
+0.10 或 0.30
+
+====================
+【reason 要求】
+
+仅说明:
+
+1. 是否主题一致;
+2. 核心论点/方法是否能支撑。
+
+禁止模糊措辞:
+“可能”
+“看起来”
+“应该”
+“疑似”
+
+长度:
+
+20~60字。
+
+====================
+【输出要求】
+
+仅输出一行 minified JSON。
+
+禁止 markdown。
+禁止解释。
+禁止换行。
+禁止任何额外内容。
+
+格式:
+
+{"is_match":true|false,"confidence":0.10|0.30|0.75|0.90,"reason":"简体中文说明"}
+PROMPT;
+
+ }
+ /** 第一次校对:参考文献真实性与支撑力度 */
private function buildReferenceCheckFirstPassPrompt()
{
- return <<<'PROMPT'
-你是文献引用校对助手。判断【正文全文】与【参考文献书目】是否相关、能否用于支撑正文中的引用。
-
-【核心原则:从宽判断,避免误杀】
-默认倾向 can_support=true。只要文献与正文不是「风马牛不相及」,即判为相关、能支撑。
-不要求变量一致、不要求结论逐条对应、不要求研究设计相同。
-
-【仅当以下情况才判 can_support=false(与正文明显无关)】
-- 学科/主题完全无关(如正文讲深度学习聚类,文献是糖尿病步态检测)。
-- 明显张冠李戴(正文断言 A 疗法的效果,文献研究的是完全不同的 B 问题且无关联)。
-- 文献条目与正文讨论的对象/场景毫无交集,且无法作背景或理论引用。
-
-【以下情况均应 can_support=true】
-- 同一大领域或相邻方向(如护理、心理、管理、医学、统计、AI 等相近子领域)。
-- 可作背景文献、综述性引用、理论或方法的一般性依据。
-- 表述略宽、略有概括、变量名不完全一致,但大方向说得通。
-
-【confidence 固定档位(禁止其它小数)】
-can_support=true:0.65(有关联但较泛)/ 0.78 / 0.85 / 0.92 / 0.98(非常确定相关)
-can_support=false:0.15(明确风马牛不相及)/ 0.25 / 0.35 / 0.45(仅当实在无法建立任何合理关联)
-
-【输出】仅一行 minified JSON,无 markdown:
-{"can_support":true|false,"is_match":true|false,"confidence":0.15|0.25|0.35|0.45|0.65|0.78|0.85|0.92|0.98,"reason":"30-80字简体中文"}
-is_match 必须与 can_support 相同。
-PROMPT;
+ return $this->buildReferenceCheckSupportSystemPrompt(false);
}
- private function buildReferenceCheckFirstPassUserPrompt($contextText, $referText)
+ private function buildReferenceCheckSupportSystemPrompt($isSecondPass = false)
{
- return "【正文全文 article_main.content】\n" . $contextText
- . "\n\n【参考文献书目 refer_text】\n" . $referText
- . "\n\n请从宽判断:文献与正文非风马牛不相即可判 can_support=true,只返回 JSON。";
+ $prompt = <<<'PROMPT'
+你是一名护理、医学、生物医学与科研期刊的资深学术编辑,正在执行“参考文献真实性与支撑力度校对”。
+
+你的任务不是判断“主题是否相关”,而是判断:
+【稿件正文中某段被引用内容】是否真的能被【对应编号的参考文献】直接或充分支撑。
+
+你必须严格基于用户提供的材料作出判断,不得凭常识、不得脑补、不得假设参考文献中“可能写过但未提供”的内容。
+
+==================================================
+【一、任务目标】
+你需要判断:
+“正文引用位置的核心论点、结论、背景陈述、机制解释、疗效描述、数据表达或因果表述,
+是否能被对应参考文献真实支持。”
+
+这里的“支持”不是指“文献主题相关”或“研究领域接近”,而是指:
+参考文献中确实包含足以支持正文该处表述的内容。
+
+==================================================
+【二、输出原则:结果必须直接对应数据库行】
+
+你输出的结果将直接写入数据库表 t_article_reference_check_result。
+
+因此:
+## 输出必须是 results 数组,数组中的每一个对象对应数据库中的一行,也就是“一个引用位置中的一条参考文献结果”。
+
+换句话说:
+- 如果某个引用位置是 [3],则输出 1 条 result(reference_no=3)
+- 如果某个引用位置是 [1,2],则输出 2 条 result:
+ - 一条对应 reference_no=1
+ - 一条对应 reference_no=2
+
+每条 result 都必须给出该参考文献“单独”对正文引用句的支撑判断。
+如果该引用位置是联合引用(citation group 中有多篇文献),则除了单条判断外,还必须给出该引用组整体的联合判断(combined_* 字段)。
+
+==================================================
+【三、最重要原则:只看“是否支撑正文核心断言”,不是看“主题是否沾边”】
+
+以下情况不能判为强支撑:
+1. 参考文献只和主题大致相关,但没有明确支持正文中的关键表述
+2. 正文说的是“疗效提升/死亡率下降/全球高发/耐药/多通路机制”等明确论点,而文献只是在背景里泛泛提到疾病
+3. 正文是多层复合句,文献只支撑其中一小部分
+4. 正文有因果、比较、趋势、机制、疗效强度等强表述,而文献没有明确证据
+5. 文献是基础机制研究,但正文引用它来支撑宏观流行病学、临床治疗现状或指南式结论
+6. 文献可以“推测支持”但不是“直接/明确支持”
+
+==================================================
+【三b、多 claim 复合句 → 0.78 部分支撑(勿误降到 0.45)】
+
+正文常为 2~4 个连续 claim 的复合句。须逐 claim 比对后综合给分:
+
+- 若文献(含 DOI 摘要)能**明确支撑多数关键概念**(如遗传异质性/多基因改变、多 survival pathway 并存、耐药或治疗挑战),
+ 但**未逐字写出**正文完整因果链(如「异质性→多通路→单靶点疗效下降」),
+ → 应判 **partial_support**,confidence 通常 **0.78**(边界情况 0.65),**不得**仅因文献主标题聚焦某化合物/干预就降到 0.45。
+
+- 0.45 仅用于:文献与 claim 方向明显不符、仅同病沾边、或几乎无可用证据。
+
+**校准样例(单条 [4],须接近此逻辑):**
+
+引用句:
+Furthermore, the genomic heterogeneity of colorectal cancer (CRC) presents additional difficulties because tumors frequently make use of several survival pathways at once, which reduces the efficacy of single-target treatments [4].
+
+文献4(Sheikhnia et al., thymoquinone CRC 机制综述):
+- Claim1 遗传异质性/多基因改变:文献有 APC/KRAS/TP53、MSI/CIN 等 → 支撑较强
+- Claim2 多 survival pathway:文献列举 PI3K/Akt、Wnt、STAT3、NF-κB 等多通路 → 支撑较强
+- Claim3 单靶点疗效下降:文献有 drug resistance/治疗挑战,但未直述因果链 → 部分支撑
+- **输出**:can_support=1, confidence=**0.78**, support_role=supplementary_support(**不是 0.45**)
+
+用户消息中若提供【DOI 真实文献内容】,**必须结合摘要判断**,不得仅凭书目标题给分。
+
+==================================================
+【四、评分规则】
+
+你必须使用以下 8 个固定分值之一:
+0.98 / 0.92 / 0.85 / 0.78 / 0.65 / 0.45 / 0.25 / 0.15
+
+判定含义:
+- 0.98 / 0.92 / 0.85 => 强支撑(strong_support)
+- 0.78 / 0.65 => 部分支撑(partial_support)
+- 0.45 / 0.25 => 支撑不足(insufficient_support)
+- 0.15 => 不支撑(not_support)
+
+can_support 取值规则:
+- 若该文献/联合引文整体可判为 strong_support 或 partial_support,则 can_support = 1
+- 若判为 insufficient_support 或 not_support,则 can_support = 0
+
+==================================================
+【五、单条文献结果如何判断】
+
+对于每一条参考文献,你必须判断它“单独”能否支撑该引用位置的正文内容,并输出:
+- can_support
+- confidence
+- reason
+- support_role
+
+其中:
+### support_role 只能取以下值之一
+- primary_support:该文献本身就是主要证据来源,能支撑引用句核心内容
+- supplementary_support:能支撑部分重要内容,但不是主要来源
+- minimal_support:只提供少量背景或边缘支撑
+- no_meaningful_support:几乎不能支撑该引用句
+
+### reason 的写法要求
+必须使用中文,明确写出:
+1. 这篇文献具体支撑正文的哪一部分
+2. 哪些部分没有支撑到
+3. 是否存在文献类型与引用用途不匹配的问题
+4. 为什么给这个分值,而不是更高或更低
+
+==================================================
+【六、联合引用的判断规则】
+
+当同一个引用位置包含多篇参考文献时(例如 [1,2] / [4,5,6]),除了逐条给单条结果外,还要额外判断:
+“这些文献合起来,是否足以支撑该引用位置的正文内容?”
+
+联合结论输出到:
+- combined_can_support
+- combined_confidence
+- combined_reason
+
+规则:
+1. 联合评分不是单条评分平均值
+2. 如果其中一篇文献已强支撑,其他文献只是补充,则联合评分可接近主支撑文献
+3. 如果多篇文献分别覆盖不同部分,合起来能较完整支撑正文,则联合评分可以高于某些单条评分
+4. 但如果最关键的核心断言没有被任何文献明确支撑,则联合评分不能虚高
+5. 如果多篇文献都只是零散相关,需要大量推断才能拼出正文结论,则联合评分通常不应过高
+
+==================================================
+【七、单引文的 combined_* 字段处理规则】
+
+即使某个引用位置只有 1 条参考文献,也仍然必须输出 combined_* 字段。
+此时:
+- combined_can_support = can_support
+- combined_confidence = confidence
+- combined_reason = “该引用位置仅包含单条文献,联合结论等同于该文献的单条结论。” 或等价表述
+
+这样可以保证输出结构统一,便于数据库写入。
+
+==================================================
+【八、输出 JSON 结构】
+
+你必须输出合法 JSON,且只能输出以下结构:
+
+{
+ "results": [
+ {
+ "reference_no": 1,
+ "cite_group_refs": "1,2",
+ "can_support": 0,
+ "confidence": 0.65,
+ "reason": "中文,单条文献结论",
+ "support_role": "supplementary_support",
+ "combined_can_support": 1,
+ "combined_confidence": 0.85,
+ "combined_reason": "中文,联合引用整体结论"
+ }
+ ]
+}
+
+==================================================
+【九、字段约束】
+
+### 1)results 中每个对象都必须包含以下字段:
+- reference_no
+- cite_group_refs
+- can_support
+- confidence
+- reason
+- support_role
+- combined_can_support
+- combined_confidence
+- combined_reason
+
+### 2)reference_no
+必须对应当前引用位置中的某一条参考文献编号。
+
+### 3)cite_group_refs
+必须是该引用位置的完整引文组,格式如:
+- "3"
+- "1,2"
+- "4,5,6"
+
+### 4)同一引用位置若包含多条参考文献,则必须输出多条 result
+例如 cite_group_refs = "1,2" 时,必须输出:
+- 一条 reference_no=1
+- 一条 reference_no=2
+
+### 5)同一引用位置下的 combined_* 必须一致
+例如同属 "1,2" 的两条 result,它们的:
+- combined_can_support
+- combined_confidence
+- combined_reason
+必须完全一致。
+
+==================================================
+【十、禁止事项】
+你绝对不能:
+- 杜撰文献中不存在的结论
+- 把“主题相关”当作“内容支撑”
+- 因为是同一疾病就默认支持
+- 输出 JSON 以外的任何内容
+
+现在开始,读取用户提供的引用位置正文、参考文献信息和文献内容,输出结果。
+PROMPT;
+
+ if ($isSecondPass) {
+ $prompt .= <<<'PROMPT'
+
+
+==================================================
+【二次校对补充(DOI 真实文献内容)】
+用户消息中会提供【DOI 真实文献内容(PubMed/Crossref)】。
+必须以 DOI 真实内容为准复核支撑力度;书目信息与 DOI 冲突时以 DOI 为准。
+仍须输出完整 results 数组,逐条给出单文献判断与联合判断。
+PROMPT;
+ }
+
+ return $prompt;
}
- /** 第二次校对:Crossref 摘要(Refer_doi) */
+ private function buildReferenceCheckFirstPassUserPrompt($contextText, $referText, $citeGroupRefs = '', $localContext = '', $doiBlock = '')
+ {
+ return $this->buildReferenceCheckSupportUserPrompt($contextText, $referText, $citeGroupRefs, $localContext, $doiBlock);
+ }
+
+ private function buildReferenceCheckSupportUserPrompt($contextText, $referText, $citeGroupRefs, $localContext, $doiBlock)
+ {
+ $citeGroupRefs = trim((string)$citeGroupRefs);
+ $localContext = trim((string)$localContext);
+ $doiBlock = trim((string)$doiBlock);
+
+ $parts = [
+ "【正文节 t_article_main】\n" . $contextText,
+ ];
+ if ($citeGroupRefs !== '') {
+ $mode = strpos($citeGroupRefs, ',') !== false ? '联合引用' : '单独引用';
+ $parts[] = "【引用文献组 cite_group_refs】{$citeGroupRefs}({$mode})";
+ }
+ if ($localContext !== '') {
+ $parts[] = "【本引用位置附近上下文】\n" . $localContext;
+ }
+ $parts[] = "【参考文献书目(按编号列出)】\n" . $referText;
+ if ($doiBlock !== '') {
+ $parts[] = "【DOI 真实文献内容(PubMed/Crossref,一轮校对已提供)】\n" . $doiBlock;
+ }
+ $parts[] = '请严格按 system 要求输出 results 数组 JSON,每条 result 对应一个 reference_no,并包含 combined_* 字段。';
+
+ return implode("\n\n", $parts);
+ }
+
+ /** 第二次校对:DOI 真实文献内容复核 */
private function buildReferenceCheckSecondPassPrompt()
{
- return <<<'PROMPT'
-你是文献引用二次校对助手。已根据 Refer_doi 从 Crossref(https://api.crossref.org/works/)获取摘要,请结合【正文全文】复核该文献是否相关。
-
-【核心原则:与第一次相同,从宽判断】
-默认倾向 can_support=true。只要 Crossref 摘要(或书目)与正文不是风马牛不相及,即判相关、能支撑。
-以【Crossref 摘要】为准;摘要与书目冲突时以摘要为准。
-
-【仅当以下情况才判 can_support=false】
-- 摘要显示的研究主题/对象/方法与正文讨论内容完全风马牛不相及。
-- 典型风马牛不相及、张冠李戴,且无法解释为背景或泛化引用。
-
-【以下情况均应 can_support=true】
-- 摘要与正文属同领域或相近方向,能作背景、理论或方向性支撑。
-- 细节不完全一致,但不存在明显矛盾。
-
-【无 Crossref 摘要时】
-结合 refer_text 从宽判断;非明显无关仍可 can_support=true,confidence 建议 0.65。
-
-【confidence 固定档位(禁止其它小数)】
-can_support=true:0.65 / 0.78 / 0.85 / 0.92 / 0.98
-can_support=false:0.15 / 0.25 / 0.35 / 0.45
-
-【输出】仅一行 minified JSON:
-{"can_support":true|false,"is_match":true|false,"confidence":0.15|0.25|0.35|0.45|0.65|0.78|0.85|0.92|0.98,"reason":"30-80字简体中文"}
-is_match 必须与 can_support 相同。
-PROMPT;
+ return $this->buildReferenceCheckSupportSystemPrompt(true);
}
- private function buildReferenceCheckSecondPassUserPrompt($contextText, $referText, $doiBlock)
+ private function buildReferenceCheckSecondPassUserPrompt($contextText, $referText, $doiBlock, $citeGroupRefs = '', $localContext = '')
{
- $doiBlock = trim((string)$doiBlock);
- return "【正文全文 article_main.content】\n" . $contextText
- . "\n\n【参考文献书目 refer_text】\n" . $referText
- . "\n\n【Crossref 摘要】(Refer_doi → api.crossref.org/works/)\n"
- . ($doiBlock !== '' ? $doiBlock : '(未获取到摘要,请结合 refer_text 从宽判断)')
- . "\n\n文献与正文非风马牛不相即可判 can_support=true,只返回 JSON。";
+ return $this->buildReferenceCheckSupportUserPrompt(
+ $contextText,
+ $referText,
+ $citeGroupRefs,
+ $localContext,
+ $doiBlock !== '' ? $doiBlock : '(未获取到 DOI 摘要或元数据,请结合书目条目从严判断)'
+ );
}
private function buildReferenceCheckSystemPrompt3()
{
@@ -1169,13 +1621,174 @@ PROMPT;
private function buildReferenceCheckRecheckUserPrompt($contextText, $referText, $doiBlock)
{
- return $this->buildReferenceCheckSecondPassUserPrompt($contextText, $referText, $doiBlock);
+ return $this->buildReferenceCheckSecondPassUserPrompt($contextText, $referText, $doiBlock, '', '');
}
/**
- * 与 buildReferenceCheckSystemPrompt3 一致的 confidence 档位
+ * @return array
*/
- private function getReferenceCheckConfidenceBands($isMatch)
+ private function parseReferenceCheckResultsFromParsed(array $parsed, $defaultCiteGroupRefs = '', $localContext = '', $doiBlock = '')
+ {
+ $rows = [];
+ if (isset($parsed['results']) && is_array($parsed['results'])) {
+ $rows = $parsed['results'];
+ } elseif (isset($parsed['reference_no']) || isset($parsed['confidence'])) {
+ $rows = [$parsed];
+ }
+
+ $normalized = [];
+ foreach ($rows as $item) {
+ if (!is_array($item)) {
+ continue;
+ }
+ $refNo = intval(isset($item['reference_no']) ? $item['reference_no'] : 0);
+ if ($refNo <= 0) {
+ continue;
+ }
+
+ $confidence = $this->snapReferenceCheckConfidenceValue(
+ $this->normalizeConfidence(isset($item['confidence']) ? $item['confidence'] : 0)
+ );
+ $canSupport = $this->canSupportFromConfidence($confidence);
+ if (array_key_exists('can_support', $item)) {
+ $canSupport = $this->boolFromLlmValue($item['can_support']);
+ } elseif (array_key_exists('is_match', $item)) {
+ $canSupport = $this->boolFromLlmValue($item['is_match']);
+ }
+
+ $reason = $this->cleanReason((string)(isset($item['reason']) ? $item['reason'] : ''));
+ $supportRole = $this->normalizeSupportRole(isset($item['support_role']) ? $item['support_role'] : '');
+ list($confidence, $canSupport, $supportRole) = $this->applyMultiClaimPartialSupportFloor(
+ $localContext,
+ $doiBlock,
+ $confidence,
+ $canSupport,
+ $supportRole,
+ $reason
+ );
+
+ $combinedConfidence = $this->snapReferenceCheckConfidenceValue(
+ $this->normalizeConfidence(isset($item['combined_confidence']) ? $item['combined_confidence'] : $confidence)
+ );
+ $combinedCanSupport = $this->canSupportFromConfidence($combinedConfidence);
+ if (array_key_exists('combined_can_support', $item)) {
+ $combinedCanSupport = $this->boolFromLlmValue($item['combined_can_support']);
+ }
+
+ $citeGroupRefs = trim((string)(isset($item['cite_group_refs']) ? $item['cite_group_refs'] : $defaultCiteGroupRefs));
+ if ($citeGroupRefs === '' && $defaultCiteGroupRefs !== '') {
+ $citeGroupRefs = trim((string)$defaultCiteGroupRefs);
+ }
+
+ $normalized[] = [
+ 'reference_no' => $refNo,
+ 'cite_group_refs' => $citeGroupRefs,
+ 'can_support' => $canSupport,
+ 'is_match' => $canSupport,
+ 'confidence' => $confidence,
+ 'reason' => $reason,
+ 'support_role' => $supportRole,
+ 'combined_can_support' => $combinedCanSupport,
+ 'combined_confidence' => $combinedConfidence,
+ 'combined_reason' => $this->cleanReason((string)(isset($item['combined_reason']) ? $item['combined_reason'] : '')),
+ ];
+ }
+
+ return $normalized;
+ }
+
+ private function normalizeSupportRole($role)
+ {
+ $role = strtolower(trim((string)$role));
+ $allowed = [
+ 'primary_support',
+ 'supplementary_support',
+ 'minimal_support',
+ 'no_meaningful_support',
+ ];
+ return in_array($role, $allowed, true) ? $role : 'no_meaningful_support';
+ }
+
+ private function canSupportFromConfidence($confidence)
+ {
+ return floatval($confidence) >= 0.65 - 0.001;
+ }
+
+ /**
+ * 多通路/异质性 claim + DOI 有多通路证据时,防止误打 0.45(应对齐 0.78 部分支撑)
+ */
+ private function applyMultiClaimPartialSupportFloor($localContext, $doiBlock, $confidence, $canSupport, $supportRole, $reason)
+ {
+ $confidence = floatval($confidence);
+ if ($confidence > 0.45) {
+ return [$confidence, $canSupport, $supportRole];
+ }
+
+ $claimText = trim((string)$localContext);
+ if ($claimText === '') {
+ return [$confidence, $canSupport, $supportRole];
+ }
+
+ $claimIsMechanism = (bool)preg_match(
+ '/\b(genomic heterogeneity|heterogeneity|survival pathway|pathways at once|single-target|multi.?pathway|genetic alteration|drug resistance|异质性|生存通路|多.*通路|单靶点|耐药)\b/ui',
+ $claimText
+ );
+ if (!$claimIsMechanism) {
+ return [$confidence, $canSupport, $supportRole];
+ }
+
+ $corpus = trim((string)$doiBlock) . ' ' . trim((string)$reason);
+ if ($corpus === '') {
+ return [$confidence, $canSupport, $supportRole];
+ }
+
+ $refHasPathwayEvidence = (bool)preg_match(
+ '/\b(pathway|PI3K|Akt|mTOR|Wnt|STAT3|NF-κB|NF-kB|genetic alteration|MSI|CIN|drug resistance|signaling|multiple|APC|KRAS|TP53|通路|耐药|信号)\b/ui',
+ $corpus
+ );
+ if (!$refHasPathwayEvidence) {
+ return [$confidence, $canSupport, $supportRole];
+ }
+
+ $confidence = 0.78;
+ $canSupport = true;
+ if ($supportRole === 'no_meaningful_support' || $supportRole === 'minimal_support') {
+ $supportRole = 'supplementary_support';
+ }
+
+ return [$confidence, $canSupport, $supportRole];
+ }
+
+ private function getReferenceCheckConfidenceBands()
+ {
+ return [0.15, 0.25, 0.45, 0.65, 0.78, 0.85, 0.92, 0.98];
+ }
+
+ private function snapReferenceCheckConfidenceValue($confidence)
+ {
+ $bands = $this->getReferenceCheckConfidenceBands();
+ foreach ($bands as $band) {
+ if (abs($confidence - $band) < 0.001) {
+ return $band;
+ }
+ }
+ $nearest = $bands[0];
+ $minDiff = abs($confidence - $nearest);
+ foreach ($bands as $band) {
+ $diff = abs($confidence - $band);
+ if ($diff < $minDiff) {
+ $minDiff = $diff;
+ $nearest = $band;
+ }
+ }
+
+ return $nearest;
+ }
+
+ /**
+ * @deprecated 兼容旧逻辑
+ */
+ private function getReferenceCheckConfidenceBandsLegacy($isMatch)
{
return $isMatch
? [0.65, 0.78, 0.85, 0.92, 0.98]
@@ -1183,22 +1796,24 @@ PROMPT;
}
/**
- * 将模型输出的 confidence 吸附到合法档位(如 0.95 → 0.92,0.75 → 0.78)
+ * 将模型输出的 confidence 吸附到合法档位
*/
private function snapReferenceCheckConfidence($confidence, $isMatch)
{
- $bands = $this->getReferenceCheckConfidenceBands($isMatch);
-
+ $snapped = $this->snapReferenceCheckConfidenceValue($confidence);
+ $bands = $this->getReferenceCheckConfidenceBandsLegacy($isMatch);
+ if (in_array($snapped, $bands, true)) {
+ return $snapped;
+ }
foreach ($bands as $band) {
- if (abs($confidence - $band) < 0.001) {
+ if (abs($snapped - $band) < 0.001) {
return $band;
}
}
-
$nearest = $bands[0];
- $minDiff = abs($confidence - $nearest);
+ $minDiff = abs($snapped - $nearest);
foreach ($bands as $band) {
- $diff = abs($confidence - $band);
+ $diff = abs($snapped - $band);
if ($diff < $minDiff) {
$minDiff = $diff;
$nearest = $band;
diff --git a/application/common/service/ReferenceRelevanceLlmService.php b/application/common/service/ReferenceRelevanceLlmService.php
index 90e1fdef..f37fb7d1 100644
--- a/application/common/service/ReferenceRelevanceLlmService.php
+++ b/application/common/service/ReferenceRelevanceLlmService.php
@@ -138,12 +138,18 @@ class ReferenceRelevanceLlmService
- **「覆盖部分结局」不足以进入 0.78**:原句点名了多条通路 + 多个结局,文献仅命中其中 1~2 个结局(如仅凋亡/增殖),且**点名通路在本文结果中全部缺失(仅讨论转引)**或主语层级不对 → 单条 **限 0.45(weakly_related / minimal_relevance)**,不得给 0.65~0.78
- 仅同领域沾边 1–2 项、主语或机制层级不对 → **0.45**
- **进入 0.65~0.78 的前提**:主语对齐(X 单体)+ 本文自身结果命中原句点名通路/结局的多数项;几乎全部明确对应 → **0.85+**
+11. **文献「主题粒度」必须匹配 claim「主题粒度」**:引用处为**疾病总论型 claim**(流行病学负担、标准/多模态治疗现状与局限、基因组异质性、单靶点治疗受限、亟需新策略等总体背景)时:
+ - 最适合的来源是**疾病总体综述 / 分子病理综述 / 精准肿瘤学 / 耐药综述**;此类文献正面、系统地为该总论 claim 提供依据 → 可 **0.85+**
+ - **单一药物 / 单一成分 / 单一通路的专题综述**(如「某化合物抗某癌:A review」),即使同病、同大方向,也只是专题视角、并非为该总论 claim 做系统总结 → 通常 **partially_related(0.72~0.78)**,**不得给 0.85+**
+ - **单基因 / 单通路的机制原始研究**对纯流行病学负担 claim → 仍按规则 3 给 **0.45**
+ - 判断要点:文献类型是否「为该总论 claim 本身做系统综述/总论」;仅同病同方向、或只支撑整段中某一两句(如「需要更安全的新策略」),不足以进入 highly_related
==================================================
【一、必须先拆解 claim】
从【本引用位置附近上下文】中提炼最小主张单元(Claim A, Claim B…),**不要**把整句笼统归为「大概讲抗癌」。例如:
- **主语/研究对象**(化合物单体 vs 植物提取物 vs 其他物种;是否「X has been demonstrated」)
- **证据语气与层级**(demonstrated / mechanistically vs predict / suggest;本文结果 vs 讨论转引)
+- **claim 主题粒度**:是否为疾病总论型(流行病学负担 / 治疗现状与局限 / 基因组异质性 / 单靶点受限 / 亟需新策略);若是,要求「总体综述 / 分子病理 / 精准肿瘤学 / 耐药综述」类来源,单一药物专题综述只算 partially_related
- 疾病流行病学(高发、死亡率)
- **点名通路/分子机制**(PI3K/AKT、MAPK、NF-κB 等,须逐项)
- **点名功能结局**(抑制增殖、凋亡、血管生成、炎症信号等,须逐项)
diff --git a/application/extra/rabbitmq.php b/application/extra/rabbitmq.php
index 05aa89b4..4b41a649 100644
--- a/application/extra/rabbitmq.php
+++ b/application/extra/rabbitmq.php
@@ -13,4 +13,11 @@ return [
'dlq' => 'ref_check.article.dlq',
'route_key' => 'article.start',
],
+
+ 'reference_relevance' => [
+ 'exchange' => 'reference_relevance',
+ 'queue' => 'ref_relevance.article',
+ 'dlq' => 'ref_relevance.article.dlq',
+ 'route_key' => 'article.start',
+ ],
];
From 9c8f7cc3b68756a7e0dd77e537407c4decdd73b1 Mon Sep 17 00:00:00 2001
From: wyn <1074145239@qq.com>
Date: Mon, 29 Jun 2026 10:43:07 +0800
Subject: [PATCH 3/4] =?UTF-8?q?=E5=BC=BA=E5=88=B6=E6=8F=90=E4=BA=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
composer.json | 3 +-
...reference_check_result_alter_positions.sql | 3 +-
vendor/composer/autoload_files.php | 3 +-
vendor/composer/autoload_namespaces.php | 1 +
vendor/composer/autoload_psr4.php | 5 +-
vendor/composer/autoload_static.php | 32 +-
vendor/composer/installed.json | 832 +++++++++---------
vendor/phpmailer/phpmailer/README.md | 2 +-
vendor/phpmailer/phpmailer/VERSION | 2 +-
.../phpmailer/language/phpmailer.lang-es.php | 6 +-
vendor/phpmailer/phpmailer/src/SMTP.php | 53 +-
.../phpoffice/math/.github/workflows/php.yml | 2 +
vendor/phpoffice/math/docs/index.md | 2 +-
.../phpoffice/math/src/Math/Reader/MathML.php | 12 +-
.../phpoffice/math/src/Math/Writer/MathML.php | 23 +
.../math/tests/Math/Writer/MathMLTest.php | 29 +
vendor/phpoffice/phpword/composer.json | 91 +-
.../phpoffice/phpword/phpstan-baseline.neon | 140 +--
.../phpword/src/PhpWord/Collection/Charts.php | 2 +
.../src/PhpWord/Collection/Comments.php | 2 +
.../src/PhpWord/Collection/Endnotes.php | 2 +
.../src/PhpWord/Collection/Footnotes.php | 2 +
.../phpword/src/PhpWord/Collection/Titles.php | 2 +
.../ComplexType/FootnoteProperties.php | 1 +
.../src/PhpWord/ComplexType/ProofState.php | 1 +
.../src/PhpWord/ComplexType/TblWidth.php | 1 +
.../PhpWord/ComplexType/TrackChangesView.php | 1 +
.../src/PhpWord/Element/AbstractContainer.php | 22 +-
.../src/PhpWord/Element/AbstractElement.php | 13 +-
.../phpword/src/PhpWord/Element/Bookmark.php | 1 +
.../phpword/src/PhpWord/Element/Cell.php | 7 +-
.../phpword/src/PhpWord/Element/CheckBox.php | 1 +
.../phpword/src/PhpWord/Element/Endnote.php | 1 +
.../phpword/src/PhpWord/Element/Field.php | 11 +-
.../phpword/src/PhpWord/Element/Footer.php | 1 +
.../phpword/src/PhpWord/Element/Footnote.php | 7 +-
.../phpword/src/PhpWord/Element/Formula.php | 1 +
.../phpword/src/PhpWord/Element/ListItem.php | 9 +-
.../src/PhpWord/Element/ListItemRun.php | 5 +-
.../phpword/src/PhpWord/Element/OLEObject.php | 5 +-
.../phpword/src/PhpWord/Element/Row.php | 11 +-
.../phpword/src/PhpWord/Element/SDT.php | 1 +
.../phpword/src/PhpWord/Element/TOC.php | 11 +-
.../phpword/src/PhpWord/Element/Text.php | 23 +-
.../phpword/src/PhpWord/Element/TextBox.php | 5 +-
.../phpword/src/PhpWord/Element/Title.php | 1 +
.../src/PhpWord/Escaper/AbstractEscaper.php | 1 +
.../src/PhpWord/Escaper/EscaperInterface.php | 1 +
.../phpword/src/PhpWord/Escaper/RegExp.php | 1 +
.../phpword/src/PhpWord/Escaper/Xml.php | 1 +
.../CreateTemporaryFileException.php | 1 +
.../Exception/InvalidImageException.php | 1 +
.../Exception/InvalidObjectException.php | 1 +
.../Exception/InvalidStyleException.php | 1 +
.../UnsupportedImageTypeException.php | 1 +
.../src/PhpWord/Metadata/Compatibility.php | 1 +
.../phpword/src/PhpWord/Metadata/Settings.php | 7 +-
.../src/PhpWord/Reader/AbstractReader.php | 1 +
.../phpword/src/PhpWord/Reader/HTML.php | 3 +-
.../phpword/src/PhpWord/Reader/MsDoc.php | 1 +
.../PhpWord/Reader/ODText/AbstractPart.php | 1 +
.../src/PhpWord/Reader/ODText/Content.php | 1 +
.../phpword/src/PhpWord/Reader/RTF.php | 3 +-
.../src/PhpWord/Reader/RTF/Document.php | 3 +-
.../src/PhpWord/Reader/ReaderInterface.php | 1 +
.../phpword/src/PhpWord/Reader/Word2007.php | 3 +-
.../PhpWord/Reader/Word2007/AbstractPart.php | 56 +-
.../PhpWord/Reader/Word2007/DocPropsApp.php | 1 +
.../PhpWord/Reader/Word2007/DocPropsCore.php | 1 +
.../Reader/Word2007/DocPropsCustom.php | 1 +
.../src/PhpWord/Reader/Word2007/Document.php | 3 +-
.../src/PhpWord/Reader/Word2007/Endnotes.php | 1 +
.../src/PhpWord/Reader/Word2007/Numbering.php | 1 +
.../src/PhpWord/Reader/Word2007/Settings.php | 1 +
.../src/PhpWord/Reader/Word2007/Styles.php | 4 +
.../phpword/src/PhpWord/Settings.php | 58 +-
.../src/PhpWord/Shared/AbstractEnum.php | 1 +
.../phpword/src/PhpWord/Shared/Css.php | 5 +-
.../phpword/src/PhpWord/Shared/Drawing.php | 1 +
.../Shared/Microsoft/PasswordEncoder.php | 8 +-
.../phpword/src/PhpWord/Shared/Text.php | 6 +
.../phpword/src/PhpWord/SimpleType/Border.php | 1 +
.../src/PhpWord/SimpleType/DocProtect.php | 1 +
.../phpword/src/PhpWord/SimpleType/Jc.php | 3 +-
.../src/PhpWord/SimpleType/JcTable.php | 1 +
.../src/PhpWord/SimpleType/NumberFormat.php | 1 +
.../src/PhpWord/SimpleType/TblWidth.php | 1 +
.../src/PhpWord/SimpleType/VerticalJc.php | 1 +
.../phpword/src/PhpWord/SimpleType/Zoom.php | 1 +
.../phpoffice/phpword/src/PhpWord/Style.php | 15 +-
.../src/PhpWord/Style/AbstractStyle.php | 10 +-
.../phpword/src/PhpWord/Style/Cell.php | 129 ++-
.../phpword/src/PhpWord/Style/Extrusion.php | 1 +
.../phpword/src/PhpWord/Style/Fill.php | 1 +
.../phpword/src/PhpWord/Style/Language.php | 1 +
.../phpword/src/PhpWord/Style/Line.php | 1 +
.../phpword/src/PhpWord/Style/ListItem.php | 1 +
.../src/PhpWord/Style/NumberingLevel.php | 1 +
.../phpword/src/PhpWord/Style/Outline.php | 1 +
.../phpword/src/PhpWord/Style/Row.php | 1 +
.../phpword/src/PhpWord/Style/Shadow.php | 1 +
.../phpword/src/PhpWord/Style/Spacing.php | 1 +
.../phpword/src/PhpWord/Style/Tab.php | 1 +
.../phpword/src/PhpWord/Style/Table.php | 13 +-
.../src/PhpWord/Style/TablePosition.php | 1 +
.../phpword/src/PhpWord/Style/TextBox.php | 1 +
.../phpword/src/PhpWord/TemplateProcessor.php | 26 +-
.../phpword/src/PhpWord/Writer/HTML.php | 3 +-
.../Writer/HTML/Element/AbstractElement.php | 3 +-
.../PhpWord/Writer/HTML/Element/Bookmark.php | 1 +
.../PhpWord/Writer/HTML/Element/Container.php | 3 +-
.../PhpWord/Writer/HTML/Element/Endnote.php | 1 +
.../PhpWord/Writer/HTML/Element/Footnote.php | 1 +
.../src/PhpWord/Writer/HTML/Element/Image.php | 1 +
.../PhpWord/Writer/HTML/Element/ListItem.php | 1 +
.../Writer/HTML/Element/ListItemRun.php | 1 +
.../src/PhpWord/Writer/HTML/Element/Table.php | 1 +
.../PhpWord/Writer/HTML/Element/TextBreak.php | 1 +
.../src/PhpWord/Writer/HTML/Element/Title.php | 1 +
.../src/PhpWord/Writer/HTML/Part/Head.php | 6 +-
.../Writer/HTML/Style/AbstractStyle.php | 1 +
.../src/PhpWord/Writer/HTML/Style/Font.php | 1 +
.../src/PhpWord/Writer/HTML/Style/Generic.php | 1 +
.../src/PhpWord/Writer/HTML/Style/Image.php | 1 +
.../src/PhpWord/Writer/HTML/Style/Table.php | 4 +
.../Writer/ODText/Element/AbstractElement.php | 29 +
.../Writer/ODText/Element/Container.php | 6 +
.../PhpWord/Writer/ODText/Element/Field.php | 1 +
.../PhpWord/Writer/ODText/Element/Image.php | 18 +-
.../PhpWord/Writer/ODText/Element/Link.php | 1 +
.../Writer/ODText/Element/PageBreak.php | 1 +
.../PhpWord/Writer/ODText/Element/Table.php | 5 +-
.../PhpWord/Writer/ODText/Element/TextRun.php | 1 +
.../PhpWord/Writer/ODText/Element/Title.php | 1 +
.../PhpWord/Writer/ODText/Part/Content.php | 13 +-
.../src/PhpWord/Writer/ODText/Part/Meta.php | 1 +
.../Writer/ODText/Style/AbstractStyle.php | 1 +
.../src/PhpWord/Writer/ODText/Style/Font.php | 3 +-
.../PhpWord/Writer/ODText/Style/Paragraph.php | 9 +-
.../PhpWord/Writer/ODText/Style/Section.php | 1 +
.../src/PhpWord/Writer/ODText/Style/Table.php | 1 +
.../phpword/src/PhpWord/Writer/PDF.php | 3 +-
.../PhpWord/Writer/PDF/AbstractRenderer.php | 1 +
.../phpword/src/PhpWord/Writer/PDF/DomPDF.php | 1 +
.../phpword/src/PhpWord/Writer/PDF/MPDF.php | 1 +
.../phpword/src/PhpWord/Writer/PDF/TCPDF.php | 1 +
.../phpword/src/PhpWord/Writer/RTF.php | 3 +-
.../Writer/RTF/Element/AbstractElement.php | 11 +-
.../src/PhpWord/Writer/RTF/Element/Field.php | 1 +
.../src/PhpWord/Writer/RTF/Element/Image.php | 1 +
.../src/PhpWord/Writer/RTF/Element/Link.php | 1 +
.../PhpWord/Writer/RTF/Element/ListItem.php | 1 +
.../src/PhpWord/Writer/RTF/Element/Table.php | 1 +
.../src/PhpWord/Writer/RTF/Element/Text.php | 1 +
.../PhpWord/Writer/RTF/Element/TextRun.php | 1 +
.../PhpWord/Writer/RTF/Part/AbstractPart.php | 1 +
.../src/PhpWord/Writer/RTF/Part/Document.php | 9 +-
.../src/PhpWord/Writer/RTF/Part/Header.php | 5 +-
.../Writer/RTF/Style/AbstractStyle.php | 1 +
.../PhpWord/Writer/RTF/Style/Paragraph.php | 1 +
.../src/PhpWord/Writer/RTF/Style/Tab.php | 1 +
.../phpword/src/PhpWord/Writer/Word2007.php | 11 +-
.../Writer/Word2007/Element/Bookmark.php | 1 +
.../PhpWord/Writer/Word2007/Element/Chart.php | 1 +
.../Writer/Word2007/Element/Endnote.php | 1 +
.../PhpWord/Writer/Word2007/Element/Field.php | 5 +-
.../Writer/Word2007/Element/Formula.php | 1 +
.../PhpWord/Writer/Word2007/Element/Line.php | 1 +
.../PhpWord/Writer/Word2007/Element/Link.php | 1 +
.../Writer/Word2007/Element/ListItem.php | 1 +
.../Writer/Word2007/Element/OLEObject.php | 1 +
.../Writer/Word2007/Element/PageBreak.php | 1 +
.../PhpWord/Writer/Word2007/Element/SDT.php | 1 +
.../PhpWord/Writer/Word2007/Element/Table.php | 1 +
.../PhpWord/Writer/Word2007/Element/Text.php | 1 +
.../Writer/Word2007/Element/TextBox.php | 8 +
.../Writer/Word2007/Element/TextBreak.php | 1 +
.../Writer/Word2007/Element/TextRun.php | 1 +
.../PhpWord/Writer/Word2007/Element/Title.php | 1 +
.../Writer/Word2007/Part/AbstractPart.php | 7 +-
.../PhpWord/Writer/Word2007/Part/Chart.php | 3 +-
.../PhpWord/Writer/Word2007/Part/Comments.php | 5 +-
.../Writer/Word2007/Part/ContentTypes.php | 3 +-
.../Writer/Word2007/Part/DocPropsApp.php | 1 +
.../PhpWord/Writer/Word2007/Part/Document.php | 1 +
.../PhpWord/Writer/Word2007/Part/Endnotes.php | 1 +
.../PhpWord/Writer/Word2007/Part/Footer.php | 1 +
.../Writer/Word2007/Part/Footnotes.php | 3 +-
.../PhpWord/Writer/Word2007/Part/Header.php | 1 +
.../Writer/Word2007/Part/Numbering.php | 3 +-
.../src/PhpWord/Writer/Word2007/Part/Rels.php | 1 +
.../Writer/Word2007/Part/RelsDocument.php | 1 +
.../PhpWord/Writer/Word2007/Part/RelsPart.php | 1 +
.../PhpWord/Writer/Word2007/Style/Cell.php | 35 +
.../Writer/Word2007/Style/Extrusion.php | 1 +
.../PhpWord/Writer/Word2007/Style/Fill.php | 1 +
.../PhpWord/Writer/Word2007/Style/Frame.php | 1 +
.../Writer/Word2007/Style/Indentation.php | 4 +
.../PhpWord/Writer/Word2007/Style/Line.php | 1 +
.../PhpWord/Writer/Word2007/Style/Outline.php | 1 +
.../src/PhpWord/Writer/Word2007/Style/Row.php | 1 +
.../PhpWord/Writer/Word2007/Style/Shadow.php | 1 +
.../PhpWord/Writer/Word2007/Style/Table.php | 1 +
.../Writer/Word2007/Style/TablePosition.php | 1 +
.../src/PhpWord/Writer/WriterInterface.php | 1 +
vendor/psr/http-message/composer.json | 4 +-
.../psr/http-message/src/RequestInterface.php | 15 +-
.../http-message/src/ResponseInterface.php | 8 +-
.../src/ServerRequestInterface.php | 24 +-
.../psr/http-message/src/StreamInterface.php | 28 +-
vendor/psr/http-message/src/UriInterface.php | 34 +-
vendor/psr/log/composer.json | 6 +-
.../think-image/src/image/Exception.php | 3 +-
.../think-image/src/image/gif/Decoder.php | 3 +-
.../topthink/think-image/tests/CropTest.php | 2 +-
.../topthink/think-image/tests/FlipTest.php | 3 +-
.../topthink/think-image/tests/RotateTest.php | 2 +-
.../topthink/think-image/tests/TestCase.php | 2 +-
.../topthink/think-image/tests/WaterTest.php | 2 +-
.../topthink/think-image/tests/autoload.php | 2 +-
220 files changed, 1360 insertions(+), 857 deletions(-)
diff --git a/composer.json b/composer.json
index a0a034eb..e8b8d190 100644
--- a/composer.json
+++ b/composer.json
@@ -28,7 +28,8 @@
"paypal/paypal-server-sdk": "^0.6.1",
"guzzlehttp/guzzle": "^7.9",
"php-amqplib/php-amqplib": "^2.12",
- "tectalic/openai": "^1.6"
+ "tectalic/openai": "^1.6",
+ "smalot/pdfparser": "^2.0"
},
"autoload": {
"psr-4": {
diff --git a/sql/reference_check_result_alter_positions.sql b/sql/reference_check_result_alter_positions.sql
index 688dd47e..db6d65b3 100644
--- a/sql/reference_check_result_alter_positions.sql
+++ b/sql/reference_check_result_alter_positions.sql
@@ -1,5 +1,6 @@
-- 为预览标记增加原文位置字段([70-73] 展开为 4 条时共用 cite_tag_* / text_*)
-ALTER TABLE `t_reference_check_result`
+-- 已合并到 article_reference_check_result_alter_columns.sql(表名 t_article_reference_check_result)
+ALTER TABLE `t_article_reference_check_result`
ADD COLUMN `cite_tag_start` int(11) NOT NULL DEFAULT 0 COMMENT 'blue标签起始字节偏移' AFTER `reference_raw`,
ADD COLUMN `cite_tag_end` int(11) NOT NULL DEFAULT 0 COMMENT 'blue标签结束字节偏移' AFTER `cite_tag_start`,
ADD COLUMN `text_start` int(11) NOT NULL DEFAULT 0 COMMENT '引用句起始字节偏移' AFTER `cite_tag_end`,
diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php
index 5db964a2..355b401b 100644
--- a/vendor/composer/autoload_files.php
+++ b/vendor/composer/autoload_files.php
@@ -7,12 +7,13 @@ $baseDir = dirname($vendorDir);
return array(
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
+ '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
+ 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'9c67151ae59aff4788964ce8eb2a0f43' => $vendorDir . '/clue/stream-filter/src/functions_include.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
'8cff32064859f4559445b89279f3199c' => $vendorDir . '/php-http/message/src/filters.php',
'decc78cc4436b1292c6c0d151b19445c' => $vendorDir . '/phpseclib/phpseclib/phpseclib/bootstrap.php',
- '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'9b552a3cc426e3287cc811caefa3cf53' => $vendorDir . '/topthink/think-helper/src/helper.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'1cfd2761b63b0a29ed23657ea394cb2d' => $vendorDir . '/topthink/think-captcha/src/helper.php',
diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php
index 990c758f..eb4850fe 100644
--- a/vendor/composer/autoload_namespaces.php
+++ b/vendor/composer/autoload_namespaces.php
@@ -6,6 +6,7 @@ $vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
+ 'Smalot\\PdfParser\\' => array($vendorDir . '/smalot/pdfparser/src'),
'Rs\\Json' => array($vendorDir . '/php-jsonpointer/php-jsonpointer/src'),
'PHPExcel' => array($vendorDir . '/phpoffice/phpexcel/Classes'),
'HTMLPurifier' => array($vendorDir . '/ezyang/htmlpurifier/library'),
diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php
index 6ce0181d..6db206ef 100644
--- a/vendor/composer/autoload_psr4.php
+++ b/vendor/composer/autoload_psr4.php
@@ -15,11 +15,12 @@ return array(
'ZipStream\\' => array($vendorDir . '/maennchen/zipstream-php/src'),
'Unirest\\' => array($vendorDir . '/apimatic/unirest-php/src'),
'Tectalic\\OpenAi\\' => array($vendorDir . '/tectalic/openai/src'),
+ 'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'),
'Spatie\\DataTransferObject\\' => array($vendorDir . '/spatie/data-transfer-object/src'),
'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
- 'Psr\\Log\\' => array($vendorDir . '/psr/log/src'),
+ 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'),
'PhpOffice\\PhpWord\\' => array($vendorDir . '/phpoffice/phpword/src/PhpWord'),
@@ -30,6 +31,7 @@ return array(
'ParagonIE\\ConstantTime\\' => array($vendorDir . '/paragonie/constant_time_encoding/src'),
'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'),
'Nyholm\\Psr7\\' => array($vendorDir . '/nyholm/psr7/src'),
+ 'MyCLabs\\Enum\\' => array($vendorDir . '/myclabs/php-enum/src'),
'Matrix\\' => array($vendorDir . '/markbaker/matrix/classes/src'),
'Http\\Message\\MultipartStream\\' => array($vendorDir . '/php-http/multipart-stream-builder/src'),
'Http\\Message\\' => array($vendorDir . '/php-http/message/src'),
@@ -39,7 +41,6 @@ return array(
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),
'Core\\' => array($vendorDir . '/apimatic/core/src'),
'CoreInterfaces\\' => array($vendorDir . '/apimatic/core-interfaces/src'),
- 'Composer\\Pcre\\' => array($vendorDir . '/composer/pcre/src'),
'Complex\\' => array($vendorDir . '/markbaker/complex/classes/src'),
'Clue\\StreamFilter\\' => array($vendorDir . '/clue/stream-filter/src'),
);
diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php
index 38ba10b9..b2e237ca 100644
--- a/vendor/composer/autoload_static.php
+++ b/vendor/composer/autoload_static.php
@@ -8,12 +8,13 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd
{
public static $files = array (
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
+ '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
+ 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'9c67151ae59aff4788964ce8eb2a0f43' => __DIR__ . '/..' . '/clue/stream-filter/src/functions_include.php',
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'2cffec82183ee1cea088009cef9a6fc3' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php',
'8cff32064859f4559445b89279f3199c' => __DIR__ . '/..' . '/php-http/message/src/filters.php',
'decc78cc4436b1292c6c0d151b19445c' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/bootstrap.php',
- '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'9b552a3cc426e3287cc811caefa3cf53' => __DIR__ . '/..' . '/topthink/think-helper/src/helper.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'1cfd2761b63b0a29ed23657ea394cb2d' => __DIR__ . '/..' . '/topthink/think-captcha/src/helper.php',
@@ -50,6 +51,7 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd
),
'S' =>
array (
+ 'Symfony\\Polyfill\\Php80\\' => 23,
'Symfony\\Polyfill\\Mbstring\\' => 26,
'Symfony\\Component\\HttpFoundation\\' => 33,
'Spatie\\DataTransferObject\\' => 26,
@@ -74,6 +76,7 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd
),
'M' =>
array (
+ 'MyCLabs\\Enum\\' => 13,
'Matrix\\' => 7,
),
'H' =>
@@ -92,7 +95,6 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd
array (
'Core\\' => 5,
'CoreInterfaces\\' => 15,
- 'Composer\\Pcre\\' => 14,
'Complex\\' => 8,
'Clue\\StreamFilter\\' => 18,
),
@@ -138,6 +140,10 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd
array (
0 => __DIR__ . '/..' . '/tectalic/openai/src',
),
+ 'Symfony\\Polyfill\\Php80\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
+ ),
'Symfony\\Polyfill\\Mbstring\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
@@ -156,7 +162,7 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd
),
'Psr\\Log\\' =>
array (
- 0 => __DIR__ . '/..' . '/psr/log/src',
+ 0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
),
'Psr\\Http\\Message\\' =>
array (
@@ -199,6 +205,10 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd
array (
0 => __DIR__ . '/..' . '/nyholm/psr7/src',
),
+ 'MyCLabs\\Enum\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/myclabs/php-enum/src',
+ ),
'Matrix\\' =>
array (
0 => __DIR__ . '/..' . '/markbaker/matrix/classes/src',
@@ -235,10 +245,6 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd
array (
0 => __DIR__ . '/..' . '/apimatic/core-interfaces/src',
),
- 'Composer\\Pcre\\' =>
- array (
- 0 => __DIR__ . '/..' . '/composer/pcre/src',
- ),
'Complex\\' =>
array (
0 => __DIR__ . '/..' . '/markbaker/complex/classes/src',
@@ -250,6 +256,13 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd
);
public static $prefixesPsr0 = array (
+ 'S' =>
+ array (
+ 'Smalot\\PdfParser\\' =>
+ array (
+ 0 => __DIR__ . '/..' . '/smalot/pdfparser/src',
+ ),
+ ),
'R' =>
array (
'Rs\\Json' =>
@@ -274,7 +287,12 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd
);
public static $classMap = array (
+ 'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
+ 'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
+ 'Stringable' => __DIR__ . '/..' . '/myclabs/php-enum/stubs/Stringable.php',
+ 'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
+ 'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);
public static function getInitializer(ClassLoader $loader)
diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json
index 360dacfc..68c16be7 100644
--- a/vendor/composer/installed.json
+++ b/vendor/composer/installed.json
@@ -2,24 +2,18 @@
"packages": [
{
"name": "apimatic/core",
- "version": "0.3.16",
- "version_normalized": "0.3.16.0",
+ "version": "0.3.17",
+ "version_normalized": "0.3.17.0",
"source": {
"type": "git",
"url": "https://github.com/apimatic/core-lib-php.git",
- "reference": "ae4ab4ca26a41be41718f33c703d67b7a767c07b"
+ "reference": "a48a583f686ee3786432b976c795a2817ec095b3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/apimatic/core-lib-php/zipball/ae4ab4ca26a41be41718f33c703d67b7a767c07b",
- "reference": "ae4ab4ca26a41be41718f33c703d67b7a767c07b",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/apimatic/core-lib-php/zipball/a48a583f686ee3786432b976c795a2817ec095b3",
+ "reference": "a48a583f686ee3786432b976c795a2817ec095b3",
+ "shasum": ""
},
"require": {
"apimatic/core-interfaces": "~0.1.5",
@@ -38,7 +32,7 @@
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
"squizlabs/php_codesniffer": "^3.5"
},
- "time": "2025-11-25T04:42:27+00:00",
+ "time": "2026-01-27T05:14:10+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -60,7 +54,7 @@
],
"support": {
"issues": "https://github.com/apimatic/core-lib-php/issues",
- "source": "https://github.com/apimatic/core-lib-php/tree/0.3.16"
+ "source": "https://github.com/apimatic/core-lib-php/tree/0.3.17"
},
"install-path": "../apimatic/core"
},
@@ -294,94 +288,6 @@
},
"install-path": "../clue/stream-filter"
},
- {
- "name": "composer/pcre",
- "version": "3.3.2",
- "version_normalized": "3.3.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/composer/pcre.git",
- "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
- "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
- },
- "require": {
- "php": "^7.4 || ^8.0"
- },
- "conflict": {
- "phpstan/phpstan": "<1.11.10"
- },
- "require-dev": {
- "phpstan/phpstan": "^1.12 || ^2",
- "phpstan/phpstan-strict-rules": "^1 || ^2",
- "phpunit/phpunit": "^8 || ^9"
- },
- "time": "2024-11-12T16:29:46+00:00",
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-main": "3.x-dev"
- },
- "phpstan": {
- "includes": [
- "extension.neon"
- ]
- }
- },
- "installation-source": "dist",
- "autoload": {
- "psr-4": {
- "Composer\\Pcre\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Jordi Boggiano",
- "email": "j.boggiano@seld.be",
- "homepage": "http://seld.be"
- }
- ],
- "description": "PCRE wrapping library that offers type-safe preg_* replacements.",
- "keywords": [
- "PCRE",
- "preg",
- "regex",
- "regular expression"
- ],
- "support": {
- "issues": "https://github.com/composer/pcre/issues",
- "source": "https://github.com/composer/pcre/tree/3.3.2"
- },
- "funding": [
- {
- "url": "https://packagist.com",
- "type": "custom"
- },
- {
- "url": "https://github.com/composer",
- "type": "github"
- },
- {
- "url": "https://tidelift.com/funding/github/packagist/composer/composer",
- "type": "tidelift"
- }
- ],
- "install-path": "./pcre"
- },
{
"name": "ezyang/htmlpurifier",
"version": "v4.19.0",
@@ -635,24 +541,18 @@
},
{
"name": "guzzlehttp/psr7",
- "version": "2.8.0",
- "version_normalized": "2.8.0.0",
+ "version": "2.9.0",
+ "version_normalized": "2.9.0.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
- "reference": "21dc724a0583619cd1652f673303492272778051"
+ "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051",
- "reference": "21dc724a0583619cd1652f673303492272778051",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884",
+ "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884",
+ "shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
@@ -667,12 +567,13 @@
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"http-interop/http-factory-tests": "0.9.0",
+ "jshttp/mime-db": "1.54.0.1",
"phpunit/phpunit": "^8.5.44 || ^9.6.25"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
},
- "time": "2025-08-23T21:21:41+00:00",
+ "time": "2026-03-10T16:41:02+00:00",
"type": "library",
"extra": {
"bamarni-bin": {
@@ -740,7 +641,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
- "source": "https://github.com/guzzle/psr7/tree/2.8.0"
+ "source": "https://github.com/guzzle/psr7/tree/2.9.0"
},
"funding": [
{
@@ -760,45 +661,32 @@
},
{
"name": "maennchen/zipstream-php",
- "version": "3.1.2",
- "version_normalized": "3.1.2.0",
+ "version": "2.1.0",
+ "version_normalized": "2.1.0.0",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
- "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f"
+ "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f",
- "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/c4c5803cc1f93df3d2448478ef79394a5981cc58",
+ "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58",
+ "shasum": ""
},
"require": {
- "ext-mbstring": "*",
- "ext-zlib": "*",
- "php-64bit": "^8.2"
+ "myclabs/php-enum": "^1.5",
+ "php": ">= 7.1",
+ "psr/http-message": "^1.0",
+ "symfony/polyfill-mbstring": "^1.0"
},
"require-dev": {
- "brianium/paratest": "^7.7",
"ext-zip": "*",
- "friendsofphp/php-cs-fixer": "^3.16",
- "guzzlehttp/guzzle": "^7.5",
+ "guzzlehttp/guzzle": ">= 6.3",
"mikey179/vfsstream": "^1.6",
- "php-coveralls/php-coveralls": "^2.5",
- "phpunit/phpunit": "^11.0",
- "vimeo/psalm": "^6.0"
+ "phpunit/phpunit": ">= 7.5"
},
- "suggest": {
- "guzzlehttp/psr7": "^2.4",
- "psr/http-message": "^2.0"
- },
- "time": "2025-01-27T12:07:53+00:00",
+ "time": "2020-05-30T13:11:16+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -835,12 +723,16 @@
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
- "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2"
+ "source": "https://github.com/maennchen/ZipStream-PHP/tree/2.1.0"
},
"funding": [
{
"url": "https://github.com/maennchen",
"type": "github"
+ },
+ {
+ "url": "https://opencollective.com/zipstream",
+ "type": "open_collective"
}
],
"install-path": "../maennchen/zipstream-php"
@@ -958,6 +850,72 @@
},
"install-path": "../markbaker/matrix"
},
+ {
+ "name": "myclabs/php-enum",
+ "version": "1.8.5",
+ "version_normalized": "1.8.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/php-enum.git",
+ "reference": "e7be26966b7398204a234f8673fdad5ac6277802"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/php-enum/zipball/e7be26966b7398204a234f8673fdad5ac6277802",
+ "reference": "e7be26966b7398204a234f8673fdad5ac6277802",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": "^7.3 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5",
+ "squizlabs/php_codesniffer": "1.*",
+ "vimeo/psalm": "^4.6.2 || ^5.2"
+ },
+ "time": "2025-01-14T11:49:03+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-4": {
+ "MyCLabs\\Enum\\": "src/"
+ },
+ "classmap": [
+ "stubs/Stringable.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP Enum contributors",
+ "homepage": "https://github.com/myclabs/php-enum/graphs/contributors"
+ }
+ ],
+ "description": "PHP Enum implementation",
+ "homepage": "https://github.com/myclabs/php-enum",
+ "keywords": [
+ "enum"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/php-enum/issues",
+ "source": "https://github.com/myclabs/php-enum/tree/1.8.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/mnapoli",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum",
+ "type": "tidelift"
+ }
+ ],
+ "install-path": "../myclabs/php-enum"
+ },
{
"name": "nyholm/psr7",
"version": "1.8.2",
@@ -1031,17 +989,17 @@
},
{
"name": "paragonie/constant_time_encoding",
- "version": "v3.1.3",
- "version_normalized": "3.1.3.0",
+ "version": "v2.8.2",
+ "version_normalized": "2.8.2.0",
"source": {
"type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git",
- "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77"
+ "reference": "e30811f7bc69e4b5b6d5783e712c06c8eabf0226"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77",
- "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77",
+ "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/e30811f7bc69e4b5b6d5783e712c06c8eabf0226",
+ "reference": "e30811f7bc69e4b5b6d5783e712c06c8eabf0226",
"shasum": "",
"mirrors": [
{
@@ -1051,15 +1009,13 @@
]
},
"require": {
- "php": "^8"
+ "php": "^7|^8"
},
"require-dev": {
- "infection/infection": "^0",
- "nikic/php-fuzzer": "^0",
- "phpunit/phpunit": "^9|^10|^11",
- "vimeo/psalm": "^4|^5|^6"
+ "phpunit/phpunit": "^6|^7|^8|^9",
+ "vimeo/psalm": "^1|^2|^3|^4"
},
- "time": "2025-09-24T15:06:41+00:00",
+ "time": "2025-09-24T15:12:37+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -1386,24 +1342,18 @@
},
{
"name": "php-http/message",
- "version": "1.16.1",
- "version_normalized": "1.16.1.0",
+ "version": "1.16.2",
+ "version_normalized": "1.16.2.0",
"source": {
"type": "git",
"url": "https://github.com/php-http/message.git",
- "reference": "5997f3289332c699fa2545c427826272498a2088"
+ "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-http/message/zipball/5997f3289332c699fa2545c427826272498a2088",
- "reference": "5997f3289332c699fa2545c427826272498a2088",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/php-http/message/zipball/06dd5e8562f84e641bf929bfe699ee0f5ce8080a",
+ "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a",
+ "shasum": ""
},
"require": {
"clue/stream-filter": "^1.5",
@@ -1428,7 +1378,7 @@
"laminas/laminas-diactoros": "Used with Diactoros Factories",
"slim/slim": "Used with Slim Framework PSR-7 implementation"
},
- "time": "2024-03-07T13:22:09+00:00",
+ "time": "2024-10-02T11:34:13+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -1458,30 +1408,24 @@
],
"support": {
"issues": "https://github.com/php-http/message/issues",
- "source": "https://github.com/php-http/message/tree/1.16.1"
+ "source": "https://github.com/php-http/message/tree/1.16.2"
},
"install-path": "../php-http/message"
},
{
"name": "php-http/multipart-stream-builder",
- "version": "1.3.1",
- "version_normalized": "1.3.1.0",
+ "version": "1.4.2",
+ "version_normalized": "1.4.2.0",
"source": {
"type": "git",
"url": "https://github.com/php-http/multipart-stream-builder.git",
- "reference": "ed56da23b95949ae4747378bed8a5b61a2fdae24"
+ "reference": "10086e6de6f53489cca5ecc45b6f468604d3460e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/ed56da23b95949ae4747378bed8a5b61a2fdae24",
- "reference": "ed56da23b95949ae4747378bed8a5b61a2fdae24",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/10086e6de6f53489cca5ecc45b6f468604d3460e",
+ "reference": "10086e6de6f53489cca5ecc45b6f468604d3460e",
+ "shasum": ""
},
"require": {
"php": "^7.1 || ^8.0",
@@ -1494,7 +1438,7 @@
"php-http/message-factory": "^1.0.2",
"phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3"
},
- "time": "2024-06-10T14:51:55+00:00",
+ "time": "2024-09-04T13:22:54+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -1523,7 +1467,7 @@
],
"support": {
"issues": "https://github.com/php-http/multipart-stream-builder/issues",
- "source": "https://github.com/php-http/multipart-stream-builder/tree/1.3.1"
+ "source": "https://github.com/php-http/multipart-stream-builder/tree/1.4.2"
},
"install-path": "../php-http/multipart-stream-builder"
},
@@ -1588,24 +1532,18 @@
},
{
"name": "phpmailer/phpmailer",
- "version": "v6.11.1",
- "version_normalized": "6.11.1.0",
+ "version": "v6.12.0",
+ "version_normalized": "6.12.0.0",
"source": {
"type": "git",
"url": "https://github.com/PHPMailer/PHPMailer.git",
- "reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284"
+ "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d9e3b36b47f04b497a0164c5a20f92acb4593284",
- "reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d1ac35d784bf9f5e61b424901d5a014967f15b12",
+ "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12",
+ "shasum": ""
},
"require": {
"ext-ctype": "*",
@@ -1625,7 +1563,6 @@
},
"suggest": {
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
- "ext-imap": "Needed to support advanced email address parsing according to RFC822",
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
@@ -1635,7 +1572,7 @@
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
"thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
},
- "time": "2025-09-30T11:54:53+00:00",
+ "time": "2025-10-15T16:49:08+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -1667,7 +1604,7 @@
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"support": {
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
- "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.11.1"
+ "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.12.0"
},
"funding": [
{
@@ -1679,24 +1616,18 @@
},
{
"name": "phpoffice/math",
- "version": "0.2.0",
- "version_normalized": "0.2.0.0",
+ "version": "0.3.0",
+ "version_normalized": "0.3.0.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/Math.git",
- "reference": "fc2eb6d1a61b058d5dac77197059db30ee3c8329"
+ "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc2eb6d1a61b058d5dac77197059db30ee3c8329",
- "reference": "fc2eb6d1a61b058d5dac77197059db30ee3c8329",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a",
+ "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a",
+ "shasum": ""
},
"require": {
"ext-dom": "*",
@@ -1707,7 +1638,7 @@
"phpstan/phpstan": "^0.12.88 || ^1.0.0",
"phpunit/phpunit": "^7.0 || ^9.0"
},
- "time": "2024-08-12T07:30:45+00:00",
+ "time": "2025-05-29T08:31:49+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -1734,7 +1665,7 @@
],
"support": {
"issues": "https://github.com/PHPOffice/Math/issues",
- "source": "https://github.com/PHPOffice/Math/tree/0.2.0"
+ "source": "https://github.com/PHPOffice/Math/tree/0.3.0"
},
"install-path": "../phpoffice/math"
},
@@ -1810,27 +1741,20 @@
},
{
"name": "phpoffice/phpspreadsheet",
- "version": "1.30.1",
- "version_normalized": "1.30.1.0",
+ "version": "1.25.2",
+ "version_normalized": "1.25.2.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
- "reference": "fa8257a579ec623473eabfe49731de5967306c4c"
+ "reference": "a317a09e7def49852400a4b3eca4a4b0790ceeb5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fa8257a579ec623473eabfe49731de5967306c4c",
- "reference": "fa8257a579ec623473eabfe49731de5967306c4c",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/a317a09e7def49852400a4b3eca4a4b0790ceeb5",
+ "reference": "a317a09e7def49852400a4b3eca4a4b0790ceeb5",
+ "shasum": ""
},
"require": {
- "composer/pcre": "^1||^2||^3",
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
@@ -1845,26 +1769,26 @@
"ext-zip": "*",
"ext-zlib": "*",
"ezyang/htmlpurifier": "^4.15",
- "maennchen/zipstream-php": "^2.1 || ^3.0",
+ "maennchen/zipstream-php": "^2.1",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
- "php": ">=7.4.0 <8.5.0",
+ "php": "^7.3 || ^8.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
- "dealerdirect/phpcodesniffer-composer-installer": "dev-main",
- "dompdf/dompdf": "^1.0 || ^2.0 || ^3.0",
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
+ "dompdf/dompdf": "^1.0 || ^2.0",
"friendsofphp/php-cs-fixer": "^3.2",
- "mitoteam/jpgraph": "^10.3",
- "mpdf/mpdf": "^8.1.1",
+ "mitoteam/jpgraph": "10.2.4",
+ "mpdf/mpdf": "8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^8.5 || ^9.0",
"squizlabs/php_codesniffer": "^3.7",
- "tecnickcom/tcpdf": "^6.5"
+ "tecnickcom/tcpdf": "6.5"
},
"suggest": {
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
@@ -1873,7 +1797,7 @@
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
},
- "time": "2025-10-26T16:01:04+00:00",
+ "time": "2022-09-25T17:21:01+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -1919,59 +1843,52 @@
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
- "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.1"
+ "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.25.2"
},
"install-path": "../phpoffice/phpspreadsheet"
},
{
"name": "phpoffice/phpword",
- "version": "1.3.0",
- "version_normalized": "1.3.0.0",
+ "version": "1.4.0",
+ "version_normalized": "1.4.0.0",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PHPWord.git",
- "reference": "8392134ce4b5dba65130ba956231a1602b848b7f"
+ "reference": "6d75328229bc93790b37e93741adf70646cea958"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/8392134ce4b5dba65130ba956231a1602b848b7f",
- "reference": "8392134ce4b5dba65130ba956231a1602b848b7f",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/6d75328229bc93790b37e93741adf70646cea958",
+ "reference": "6d75328229bc93790b37e93741adf70646cea958",
+ "shasum": ""
},
"require": {
"ext-dom": "*",
+ "ext-gd": "*",
"ext-json": "*",
"ext-xml": "*",
+ "ext-zip": "*",
"php": "^7.1|^8.0",
- "phpoffice/math": "^0.2"
+ "phpoffice/math": "^0.3"
},
"require-dev": {
- "dompdf/dompdf": "^2.0",
- "ext-gd": "*",
+ "dompdf/dompdf": "^2.0 || ^3.0",
"ext-libxml": "*",
- "ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.3",
- "mpdf/mpdf": "^8.1",
+ "mpdf/mpdf": "^7.0 || ^8.0",
"phpmd/phpmd": "^2.13",
- "phpstan/phpstan-phpunit": "@stable",
+ "phpstan/phpstan": "^0.12.88 || ^1.0.0",
+ "phpstan/phpstan-phpunit": "^1.0 || ^2.0",
"phpunit/phpunit": ">=7.0",
"symfony/process": "^4.4 || ^5.0",
"tecnickcom/tcpdf": "^6.5"
},
"suggest": {
"dompdf/dompdf": "Allows writing PDF",
- "ext-gd2": "Allows adding images",
"ext-xmlwriter": "Allows writing OOXML and ODF",
- "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template",
- "ext-zip": "Allows writing OOXML and ODF"
+ "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template"
},
- "time": "2024-08-30T18:03:42+00:00",
+ "time": "2025-06-05T10:32:36+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -1981,7 +1898,7 @@
},
"notification-url": "https://packagist.org/downloads/",
"license": [
- "LGPL-3.0"
+ "LGPL-3.0-only"
],
"authors": [
{
@@ -2037,7 +1954,7 @@
],
"support": {
"issues": "https://github.com/PHPOffice/PHPWord/issues",
- "source": "https://github.com/PHPOffice/PHPWord/tree/1.3.0"
+ "source": "https://github.com/PHPOffice/PHPWord/tree/1.4.0"
},
"install-path": "../phpoffice/phpword"
},
@@ -2217,30 +2134,24 @@
},
{
"name": "psr/http-factory",
- "version": "1.0.2",
- "version_normalized": "1.0.2.0",
+ "version": "1.1.0",
+ "version_normalized": "1.1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-factory.git",
- "reference": "e616d01114759c4c489f93b099585439f795fe35"
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35",
- "reference": "e616d01114759c4c489f93b099585439f795fe35",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "shasum": ""
},
"require": {
- "php": ">=7.0.0",
+ "php": ">=7.1",
"psr/http-message": "^1.0 || ^2.0"
},
- "time": "2023-04-10T20:10:41+00:00",
+ "time": "2024-04-15T12:06:14+00:00",
"type": "library",
"extra": {
"branch-alias": {
@@ -2263,7 +2174,7 @@
"homepage": "https://www.php-fig.org/"
}
],
- "description": "Common interfaces for PSR-7 HTTP message factories",
+ "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
"keywords": [
"factory",
"http",
@@ -2275,39 +2186,33 @@
"response"
],
"support": {
- "source": "https://github.com/php-fig/http-factory/tree/1.0.2"
+ "source": "https://github.com/php-fig/http-factory"
},
"install-path": "../psr/http-factory"
},
{
"name": "psr/http-message",
- "version": "2.0",
- "version_normalized": "2.0.0.0",
+ "version": "1.1",
+ "version_normalized": "1.1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
- "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
- "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
+ "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
+ "shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
- "time": "2023-04-04T09:54:51+00:00",
+ "time": "2023-04-04T09:50:52+00:00",
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-master": "1.1.x-dev"
}
},
"installation-source": "dist",
@@ -2323,7 +2228,7 @@
"authors": [
{
"name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
+ "homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
@@ -2337,45 +2242,39 @@
"response"
],
"support": {
- "source": "https://github.com/php-fig/http-message/tree/2.0"
+ "source": "https://github.com/php-fig/http-message/tree/1.1"
},
"install-path": "../psr/http-message"
},
{
"name": "psr/log",
- "version": "3.0.1",
- "version_normalized": "3.0.1.0",
+ "version": "1.1.4",
+ "version_normalized": "1.1.4.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
- "reference": "79dff0b268932c640297f5208d6298f71855c03e"
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e",
- "reference": "79dff0b268932c640297f5208d6298f71855c03e",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
+ "shasum": ""
},
"require": {
- "php": ">=8.0.0"
+ "php": ">=5.3.0"
},
- "time": "2024-08-21T13:31:24+00:00",
+ "time": "2021-05-03T11:20:27+00:00",
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.x-dev"
+ "dev-master": "1.1.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
- "Psr\\Log\\": "src"
+ "Psr\\Log\\": "Psr/Log/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -2396,39 +2295,33 @@
"psr-3"
],
"support": {
- "source": "https://github.com/php-fig/log/tree/3.0.1"
+ "source": "https://github.com/php-fig/log/tree/1.1.4"
},
"install-path": "../psr/log"
},
{
"name": "psr/simple-cache",
- "version": "3.0.0",
- "version_normalized": "3.0.0.0",
+ "version": "1.0.1",
+ "version_normalized": "1.0.1.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/simple-cache.git",
- "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865"
+ "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865",
- "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+ "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+ "shasum": ""
},
"require": {
- "php": ">=8.0.0"
+ "php": ">=5.3.0"
},
- "time": "2021-10-29T13:26:27+00:00",
+ "time": "2017-10-23T01:57:42+00:00",
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "3.0.x-dev"
+ "dev-master": "1.0.x-dev"
}
},
"installation-source": "dist",
@@ -2444,7 +2337,7 @@
"authors": [
{
"name": "PHP-FIG",
- "homepage": "https://www.php-fig.org/"
+ "homepage": "http://www.php-fig.org/"
}
],
"description": "Common interfaces for simple caching",
@@ -2456,7 +2349,7 @@
"simple-cache"
],
"support": {
- "source": "https://github.com/php-fig/simple-cache/tree/3.0.0"
+ "source": "https://github.com/php-fig/simple-cache/tree/master"
},
"install-path": "../psr/simple-cache"
},
@@ -2507,6 +2400,66 @@
},
"install-path": "../ralouphie/getallheaders"
},
+ {
+ "name": "smalot/pdfparser",
+ "version": "v2.9.0",
+ "version_normalized": "2.9.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/smalot/pdfparser.git",
+ "reference": "6b53144fcb24af77093d4150dd7d0dd571f25761"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/smalot/pdfparser/zipball/6b53144fcb24af77093d4150dd7d0dd571f25761",
+ "reference": "6b53144fcb24af77093d4150dd7d0dd571f25761",
+ "shasum": "",
+ "mirrors": [
+ {
+ "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
+ "preferred": true
+ }
+ ]
+ },
+ "require": {
+ "ext-iconv": "*",
+ "ext-zlib": "*",
+ "php": ">=7.1",
+ "symfony/polyfill-mbstring": "^1.18"
+ },
+ "time": "2024-03-01T09:51:10+00:00",
+ "type": "library",
+ "installation-source": "dist",
+ "autoload": {
+ "psr-0": {
+ "Smalot\\PdfParser\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-3.0"
+ ],
+ "authors": [
+ {
+ "name": "Sebastien MALOT",
+ "email": "sebastien@malot.fr"
+ }
+ ],
+ "description": "Pdf parser library. Can read and extract information from pdf file.",
+ "homepage": "https://www.pdfparser.org",
+ "keywords": [
+ "extract",
+ "parse",
+ "parser",
+ "pdf",
+ "text"
+ ],
+ "support": {
+ "issues": "https://github.com/smalot/pdfparser/issues",
+ "source": "https://github.com/smalot/pdfparser/tree/v2.9.0"
+ },
+ "install-path": "../smalot/pdfparser"
+ },
{
"name": "spatie/data-transfer-object",
"version": "1.14.1",
@@ -2564,29 +2517,23 @@
},
{
"name": "symfony/deprecation-contracts",
- "version": "v3.6.0",
- "version_normalized": "3.6.0.0",
+ "version": "v2.5.4",
+ "version_normalized": "2.5.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
- "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
+ "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
- "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918",
+ "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918",
+ "shasum": ""
},
"require": {
- "php": ">=8.1"
+ "php": ">=7.1"
},
- "time": "2024-09-25T14:21:43+00:00",
+ "time": "2024-09-25T14:11:13+00:00",
"type": "library",
"extra": {
"thanks": {
@@ -2594,7 +2541,7 @@
"name": "symfony/contracts"
},
"branch-alias": {
- "dev-main": "3.6-dev"
+ "dev-main": "2.5-dev"
}
},
"installation-source": "dist",
@@ -2620,7 +2567,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4"
},
"funding": [
{
@@ -2640,44 +2587,38 @@
},
{
"name": "symfony/http-foundation",
- "version": "v8.0.1",
- "version_normalized": "8.0.1.0",
+ "version": "v5.4.50",
+ "version_normalized": "5.4.50.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "3690740e2e8b19d877f20d4f10b7a489cddf0fe2"
+ "reference": "1a0706e8b8041046052ea2695eb8aeee04f97609"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3690740e2e8b19d877f20d4f10b7a489cddf0fe2",
- "reference": "3690740e2e8b19d877f20d4f10b7a489cddf0fe2",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/1a0706e8b8041046052ea2695eb8aeee04f97609",
+ "reference": "1a0706e8b8041046052ea2695eb8aeee04f97609",
+ "shasum": ""
},
"require": {
- "php": ">=8.4",
- "symfony/polyfill-mbstring": "^1.1"
- },
- "conflict": {
- "doctrine/dbal": "<4.3"
+ "php": ">=7.2.5",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-mbstring": "~1.1",
+ "symfony/polyfill-php80": "^1.16"
},
"require-dev": {
- "doctrine/dbal": "^4.3",
- "predis/predis": "^1.1|^2.0",
- "symfony/cache": "^7.4|^8.0",
- "symfony/clock": "^7.4|^8.0",
- "symfony/dependency-injection": "^7.4|^8.0",
- "symfony/expression-language": "^7.4|^8.0",
- "symfony/http-kernel": "^7.4|^8.0",
- "symfony/mime": "^7.4|^8.0",
- "symfony/rate-limiter": "^7.4|^8.0"
+ "predis/predis": "^1.0|^2.0",
+ "symfony/cache": "^4.4|^5.0|^6.0",
+ "symfony/dependency-injection": "^5.4|^6.0",
+ "symfony/expression-language": "^4.4|^5.0|^6.0",
+ "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4",
+ "symfony/mime": "^4.4|^5.0|^6.0",
+ "symfony/rate-limiter": "^5.2|^6.0"
},
- "time": "2025-12-07T11:23:24+00:00",
+ "suggest": {
+ "symfony/mime": "To use the file extension guesser"
+ },
+ "time": "2025-11-03T12:58:48+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -2705,7 +2646,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-foundation/tree/v8.0.1"
+ "source": "https://github.com/symfony/http-foundation/tree/v5.4.50"
},
"funding": [
{
@@ -2729,24 +2670,18 @@
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.32.0",
- "version_normalized": "1.32.0.0",
+ "version": "v1.37.0",
+ "version_normalized": "1.37.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
+ "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
- "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315",
+ "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315",
+ "shasum": ""
},
"require": {
"ext-iconv": "*",
@@ -2758,7 +2693,7 @@
"suggest": {
"ext-mbstring": "For best performance"
},
- "time": "2024-12-23T08:48:59+00:00",
+ "time": "2026-04-10T17:25:58+00:00",
"type": "library",
"extra": {
"thanks": {
@@ -2799,7 +2734,7 @@
"shim"
],
"support": {
- "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0"
},
"funding": [
{
@@ -2810,6 +2745,10 @@
"url": "https://github.com/fabpot",
"type": "github"
},
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
@@ -2817,6 +2756,93 @@
],
"install-path": "../symfony/polyfill-mbstring"
},
+ {
+ "name": "symfony/polyfill-php80",
+ "version": "v1.37.0",
+ "version_normalized": "1.37.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php80.git",
+ "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
+ "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "time": "2026-04-10T16:19:22+00:00",
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "installation-source": "dist",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php80\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ion Bazan",
+ "email": "ion.bazan@gmail.com"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "install-path": "../symfony/polyfill-php80"
+ },
{
"name": "tectalic/openai",
"version": "v1.6.0",
@@ -3003,24 +3029,18 @@
},
{
"name": "topthink/think-helper",
- "version": "v3.1.11",
- "version_normalized": "3.1.11.0",
+ "version": "v3.1.12",
+ "version_normalized": "3.1.12.0",
"source": {
"type": "git",
"url": "https://github.com/top-think/think-helper.git",
- "reference": "1d6ada9b9f3130046bf6922fe1bd159c8d88a33c"
+ "reference": "fe277121112a8f1c872e169a733ca80bb11c4acb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/top-think/think-helper/zipball/1d6ada9b9f3130046bf6922fe1bd159c8d88a33c",
- "reference": "1d6ada9b9f3130046bf6922fe1bd159c8d88a33c",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/top-think/think-helper/zipball/fe277121112a8f1c872e169a733ca80bb11c4acb",
+ "reference": "fe277121112a8f1c872e169a733ca80bb11c4acb",
+ "shasum": ""
},
"require": {
"php": ">=7.1.0"
@@ -3028,7 +3048,7 @@
"require-dev": {
"phpunit/phpunit": "^9.5"
},
- "time": "2025-04-07T06:55:59+00:00",
+ "time": "2025-12-26T09:58:29+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -3052,30 +3072,24 @@
"description": "The ThinkPHP6 Helper Package",
"support": {
"issues": "https://github.com/top-think/think-helper/issues",
- "source": "https://github.com/top-think/think-helper/tree/v3.1.11"
+ "source": "https://github.com/top-think/think-helper/tree/v3.1.12"
},
"install-path": "../topthink/think-helper"
},
{
"name": "topthink/think-image",
- "version": "v1.0.7",
- "version_normalized": "1.0.7.0",
+ "version": "v1.0.8",
+ "version_normalized": "1.0.8.0",
"source": {
"type": "git",
"url": "https://github.com/top-think/think-image.git",
- "reference": "8586cf47f117481c6d415b20f7dedf62e79d5512"
+ "reference": "d1d748cbb2fe2f29fca6138cf96cb8b5113892f1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/top-think/think-image/zipball/8586cf47f117481c6d415b20f7dedf62e79d5512",
- "reference": "8586cf47f117481c6d415b20f7dedf62e79d5512",
- "shasum": "",
- "mirrors": [
- {
- "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
- "preferred": true
- }
- ]
+ "url": "https://api.github.com/repos/top-think/think-image/zipball/d1d748cbb2fe2f29fca6138cf96cb8b5113892f1",
+ "reference": "d1d748cbb2fe2f29fca6138cf96cb8b5113892f1",
+ "shasum": ""
},
"require": {
"ext-gd": "*"
@@ -3084,7 +3098,7 @@
"phpunit/phpunit": "4.8.*",
"topthink/framework": "^5.0"
},
- "time": "2016-09-29T06:05:43+00:00",
+ "time": "2024-08-07T10:06:35+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
@@ -3105,7 +3119,7 @@
"description": "The ThinkPHP5 Image Package",
"support": {
"issues": "https://github.com/top-think/think-image/issues",
- "source": "https://github.com/top-think/think-image/tree/master"
+ "source": "https://github.com/top-think/think-image/tree/v1.0.8"
},
"install-path": "../topthink/think-image"
},
diff --git a/vendor/phpmailer/phpmailer/README.md b/vendor/phpmailer/phpmailer/README.md
index 51c97517..646649c9 100644
--- a/vendor/phpmailer/phpmailer/README.md
+++ b/vendor/phpmailer/phpmailer/README.md
@@ -48,7 +48,7 @@ This software is distributed under the [LGPL 2.1](https://www.gnu.org/licenses/o
PHPMailer is available on [Packagist](https://packagist.org/packages/phpmailer/phpmailer) (using semantic versioning), and installation via [Composer](https://getcomposer.org) is the recommended way to install PHPMailer. Just add this line to your `composer.json` file:
```json
-"phpmailer/phpmailer": "^6.11.1"
+"phpmailer/phpmailer": "^6.12.0"
```
or run
diff --git a/vendor/phpmailer/phpmailer/VERSION b/vendor/phpmailer/phpmailer/VERSION
index fac714a3..d4e6cb42 100644
--- a/vendor/phpmailer/phpmailer/VERSION
+++ b/vendor/phpmailer/phpmailer/VERSION
@@ -1 +1 @@
-6.11.1
+6.12.0
diff --git a/vendor/phpmailer/phpmailer/language/phpmailer.lang-es.php b/vendor/phpmailer/phpmailer/language/phpmailer.lang-es.php
index a871824f..4e74bfb7 100644
--- a/vendor/phpmailer/phpmailer/language/phpmailer.lang-es.php
+++ b/vendor/phpmailer/phpmailer/language/phpmailer.lang-es.php
@@ -9,7 +9,7 @@
*/
$PHPMAILER_LANG['authenticate'] = 'Error SMTP: Imposible autentificar.';
-$PHPMAILER_LANG['buggy_php'] = 'Tu versión de PHP ha sido afectada por un bug que puede resultar en mensajes corruptos. Para arreglarlo, cambia a enviar usando SMTP, deshabilita la opción mail.add_x_header en tu php.ini, cambia a MacOS o Linux, o actualiza tu PHP a la versión 7.0.17+ o 7.1.3+.';
+$PHPMAILER_LANG['buggy_php'] = 'Tu versión de PHP está afectada por un bug que puede resultar en mensajes corruptos. Para arreglarlo, cambia a enviar usando SMTP, deshabilita la opción mail.add_x_header en tu php.ini, cambia a MacOS o Linux, o actualiza tu PHP a la versión 7.0.17+ o 7.1.3+.';
$PHPMAILER_LANG['connect_host'] = 'Error SMTP: Imposible conectar al servidor SMTP.';
$PHPMAILER_LANG['data_not_accepted'] = 'Error SMTP: Datos no aceptados.';
$PHPMAILER_LANG['empty_message'] = 'El cuerpo del mensaje está vacío.';
@@ -18,7 +18,7 @@ $PHPMAILER_LANG['execute'] = 'Imposible ejecutar: ';
$PHPMAILER_LANG['extension_missing'] = 'Extensión faltante: ';
$PHPMAILER_LANG['file_access'] = 'Imposible acceder al archivo: ';
$PHPMAILER_LANG['file_open'] = 'Error de Archivo: Imposible abrir el archivo: ';
-$PHPMAILER_LANG['from_failed'] = 'La siguiente dirección de remitente falló: ';
+$PHPMAILER_LANG['from_failed'] = 'La(s) siguiente(s) direcciones de remitente fallaron: ';
$PHPMAILER_LANG['instantiate'] = 'Imposible crear una instancia de la función Mail.';
$PHPMAILER_LANG['invalid_address'] = 'Imposible enviar: dirección de email inválido: ';
$PHPMAILER_LANG['invalid_header'] = 'Nombre o valor de encabezado no válido';
@@ -34,5 +34,3 @@ $PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falló.';
$PHPMAILER_LANG['smtp_detail'] = 'Detalle: ';
$PHPMAILER_LANG['smtp_error'] = 'Error del servidor SMTP: ';
$PHPMAILER_LANG['variable_set'] = 'No se pudo configurar la variable: ';
-$PHPMAILER_LANG['imap_recommended'] = 'No se recomienda usar el analizador de direcciones simplificado. Instala la extensión IMAP de PHP para un análisis RFC822 más completo.';
-$PHPMAILER_LANG['deprecated_argument'] = 'El argumento $useimap ha quedado obsoleto';
diff --git a/vendor/phpmailer/phpmailer/src/SMTP.php b/vendor/phpmailer/phpmailer/src/SMTP.php
index 3772c94a..d8749c5b 100644
--- a/vendor/phpmailer/phpmailer/src/SMTP.php
+++ b/vendor/phpmailer/phpmailer/src/SMTP.php
@@ -35,7 +35,7 @@ class SMTP
*
* @var string
*/
- const VERSION = '6.11.1';
+ const VERSION = '6.12.0';
/**
* SMTP line break constant.
@@ -205,7 +205,6 @@ class SMTP
'Haraka' => '/[\d]{3} Message Queued \((.*)\)/',
'ZoneMTA' => '/[\d]{3} Message queued as (.*)/',
'Mailjet' => '/[\d]{3} OK queued as (.*)/',
- 'Gsmtp' => '/[\d]{3} 2\.0\.0 OK (.*) - gsmtp/',
];
/**
@@ -634,41 +633,10 @@ class SMTP
return false;
}
$oauth = $OAuth->getOauth64();
- /*
- * An SMTP command line can have a maximum length of 512 bytes, including the command name,
- * so the base64-encoded OAUTH token has a maximum length of:
- * 512 - 13 (AUTH XOAUTH2) - 2 (CRLF) = 497 bytes
- * If the token is longer than that, the command and the token must be sent separately as described in
- * https://www.rfc-editor.org/rfc/rfc4954#section-4
- */
- if ($oauth === '') {
- //Sending an empty auth token is legitimate, but it must be encoded as '='
- //to indicate it's not a 2-part command
- if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 =', 235)) {
- return false;
- }
- } elseif (strlen($oauth) <= 497) {
- //Authenticate using a token in the initial-response part
- if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
- return false;
- }
- } else {
- //The token is too long, so we need to send it in two parts.
- //Send the auth command without a token and expect a 334
- if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2', 334)) {
- return false;
- }
- //Send the token
- if (!$this->sendCommand('OAuth TOKEN', $oauth, [235, 334])) {
- return false;
- }
- //If the server answers with 334, send an empty line and wait for a 235
- if (
- substr($this->last_reply, 0, 3) === '334'
- && $this->sendCommand('AUTH End', '', 235)
- ) {
- return false;
- }
+
+ //Start authentication
+ if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
+ return false;
}
break;
default:
@@ -1341,16 +1309,7 @@ class SMTP
//stream_select returns false when the `select` system call is interrupted
//by an incoming signal, try the select again
- if (
- stripos($message, 'interrupted system call') !== false ||
- (
- // on applications with a different locale than english, the message above is not found because
- // it's translated. So we also check for the SOCKET_EINTR constant which is defined under
- // Windows and UNIX-like platforms (if available on the platform).
- defined('SOCKET_EINTR') &&
- stripos($message, 'stream_select(): Unable to select [' . SOCKET_EINTR . ']') !== false
- )
- ) {
+ if (stripos($message, 'interrupted system call') !== false) {
$this->edebug(
'SMTP -> get_lines(): retrying stream_select',
self::DEBUG_LOWLEVEL
diff --git a/vendor/phpoffice/math/.github/workflows/php.yml b/vendor/phpoffice/math/.github/workflows/php.yml
index df807baa..5390049e 100644
--- a/vendor/phpoffice/math/.github/workflows/php.yml
+++ b/vendor/phpoffice/math/.github/workflows/php.yml
@@ -45,6 +45,7 @@ jobs:
- '8.1'
- '8.2'
- '8.3'
+ - '8.4'
steps:
- name: Setup PHP
uses: shivammathur/setup-php@v2
@@ -75,6 +76,7 @@ jobs:
- '8.1'
- '8.2'
- '8.3'
+ - '8.4'
steps:
- name: Setup PHP
uses: shivammathur/setup-php@v2
diff --git a/vendor/phpoffice/math/docs/index.md b/vendor/phpoffice/math/docs/index.md
index 340b0dcc..80b4333c 100644
--- a/vendor/phpoffice/math/docs/index.md
+++ b/vendor/phpoffice/math/docs/index.md
@@ -48,7 +48,7 @@ Math is an open source project licensed under the terms of [MIT](https://github.
| **Simple** | Fraction | :material-check: | :material-check: |
| | Superscript | :material-check: | |
| **Architectural** | Row | :material-check: | :material-check: |
-| | Semantics | | |
+| | Semantics | :material-check: | |
## Contributing
diff --git a/vendor/phpoffice/math/src/Math/Reader/MathML.php b/vendor/phpoffice/math/src/Math/Reader/MathML.php
index 58d93935..cf1edb8d 100644
--- a/vendor/phpoffice/math/src/Math/Reader/MathML.php
+++ b/vendor/phpoffice/math/src/Math/Reader/MathML.php
@@ -10,6 +10,7 @@ use PhpOffice\Math\Element;
use PhpOffice\Math\Exception\InvalidInputException;
use PhpOffice\Math\Exception\NotImplementedException;
use PhpOffice\Math\Math;
+use PhpOffice\Math\Reader\Security\XmlScanner;
class MathML implements ReaderInterface
{
@@ -22,8 +23,17 @@ class MathML implements ReaderInterface
/** @var DOMXPath */
private $xpath;
+ /** @var XmlScanner */
+ private $xmlScanner;
+
+ public function __construct()
+ {
+ $this->xmlScanner = XmlScanner::getInstance();
+ }
+
public function read(string $content): ?Math
{
+ $content = $this->xmlScanner->scan($content);
$content = str_replace(
[
'⁢',
@@ -35,7 +45,7 @@ class MathML implements ReaderInterface
);
$this->dom = new DOMDocument();
- $this->dom->loadXML($content, LIBXML_DTDLOAD);
+ $this->dom->loadXML($content);
$this->math = new Math();
$this->parseNode(null, $this->math);
diff --git a/vendor/phpoffice/math/src/Math/Writer/MathML.php b/vendor/phpoffice/math/src/Math/Writer/MathML.php
index d8905f00..34a20dc3 100644
--- a/vendor/phpoffice/math/src/Math/Writer/MathML.php
+++ b/vendor/phpoffice/math/src/Math/Writer/MathML.php
@@ -40,6 +40,26 @@ class MathML implements WriterInterface
{
$tagName = $this->getElementTagName($element);
+ // Element\AbstractGroupElement
+ if ($element instanceof Element\Semantics) {
+ $this->output->startElement($tagName);
+ // Write elements
+ foreach ($element->getElements() as $childElement) {
+ $this->writeElementItem($childElement);
+ }
+
+ // Write annotations
+ foreach ($element->getAnnotations() as $encoding => $annotation) {
+ $this->output->startElement('annotation');
+ $this->output->writeAttribute('encoding', $encoding);
+ $this->output->text($annotation);
+ $this->output->endElement();
+ }
+ $this->output->endElement();
+
+ return;
+ }
+
// Element\AbstractGroupElement
if ($element instanceof Element\AbstractGroupElement) {
$this->output->startElement($tagName);
@@ -121,6 +141,9 @@ class MathML implements WriterInterface
if ($element instanceof Element\Operator) {
return 'mo';
}
+ if ($element instanceof Element\Semantics) {
+ return 'semantics';
+ }
throw new NotImplementedException(sprintf(
'%s : The element of the class `%s` has no tag name',
diff --git a/vendor/phpoffice/math/tests/Math/Writer/MathMLTest.php b/vendor/phpoffice/math/tests/Math/Writer/MathMLTest.php
index 06aac383..90724856 100644
--- a/vendor/phpoffice/math/tests/Math/Writer/MathMLTest.php
+++ b/vendor/phpoffice/math/tests/Math/Writer/MathMLTest.php
@@ -80,6 +80,35 @@ class MathMLTest extends WriterTestCase
$this->assertIsSchemaMathMLValid($output);
}
+ public function testWriteSemantics(): void
+ {
+ $opTimes = new Element\Operator('⁢');
+
+ $math = new Math();
+
+ $semantics = new Element\Semantics();
+ $semantics->add(new Element\Identifier('y'));
+ $semantics->addAnnotation('application/x-tex', ' y ');
+
+ $math->add($semantics);
+
+ $writer = new MathML();
+ $output = $writer->write($math);
+
+ $expected = ''
+ . PHP_EOL
+ . ''
+ . ''
+ . PHP_EOL;
+ $this->assertEquals($expected, $output);
+ $this->assertIsSchemaMathMLValid($output);
+ }
+
public function testWriteNotImplemented(): void
{
$this->expectException(NotImplementedException::class);
diff --git a/vendor/phpoffice/phpword/composer.json b/vendor/phpoffice/phpword/composer.json
index bce0f4b8..90b1c2e5 100644
--- a/vendor/phpoffice/phpword/composer.json
+++ b/vendor/phpoffice/phpword/composer.json
@@ -8,7 +8,7 @@
],
"homepage": "https://phpoffice.github.io/PHPWord/",
"type": "library",
- "license": "LGPL-3.0",
+ "license": "LGPL-3.0-only",
"authors": [
{
"name": "Mark Baker"
@@ -36,20 +36,67 @@
],
"scripts": {
"test": [
- "phpunit --color=always"
+ "@php vendor/bin/phpunit --color=always"
],
"test-no-coverage": [
- "phpunit --color=always --no-coverage"
+ "@php vendor/bin/phpunit --color=always --no-coverage"
],
"check": [
- "php-cs-fixer fix --ansi --dry-run --diff",
- "phpcs --report-width=200 --report-summary --report-full samples/ src/ tests/ --ignore=src/PhpWord/Shared/PCLZip --standard=PSR2 -n",
- "phpmd src/,tests/ text ./phpmd.xml.dist --exclude pclzip.lib.php",
+ "@php vendor/bin/php-cs-fixer fix --ansi --dry-run --diff",
+ "@php vendor/bin/phpmd src/,tests/ text ./phpmd.xml.dist --exclude pclzip.lib.php",
"@test-no-coverage",
- "phpstan analyse --ansi"
+ "@php vendor/bin/phpstan analyse --ansi"
],
"fix": [
- "php-cs-fixer fix --ansi"
+ "@php vendor/bin/php-cs-fixer fix --ansi"
+ ],
+ "samples": [
+ "php samples/Sample_01_SimpleText.php",
+ "php samples/Sample_02_TabStops.php",
+ "php samples/Sample_03_Sections.php",
+ "php samples/Sample_04_Textrun.php",
+ "php samples/Sample_05_Multicolumn.php",
+ "php samples/Sample_06_Footnote.php",
+ "php samples/Sample_07_TemplateCloneRow.php",
+ "php samples/Sample_08_ParagraphPagination.php",
+ "php samples/Sample_09_Tables.php",
+ "php samples/Sample_10_EastAsianFontStyle.php",
+ "php samples/Sample_11_ReadWord97.php",
+ "php samples/Sample_11_ReadWord2007.php",
+ "php samples/Sample_12_HeaderFooter.php",
+ "php samples/Sample_13_Images.php",
+ "php samples/Sample_14_ListItem.php",
+ "php samples/Sample_15_Link.php",
+ "php samples/Sample_16_Object.php",
+ "php samples/Sample_17_TitleTOC.php",
+ "php samples/Sample_18_Watermark.php",
+ "php samples/Sample_19_TextBreak.php",
+ "php samples/Sample_20_BGColor.php",
+ "php samples/Sample_21_TableRowRules.php",
+ "php samples/Sample_22_CheckBox.php",
+ "php samples/Sample_23_TemplateBlock.php",
+ "php samples/Sample_24_ReadODText.php",
+ "php samples/Sample_25_TextBox.php",
+ "php samples/Sample_26_Html.php",
+ "php samples/Sample_27_Field.php",
+ "php samples/Sample_28_ReadRTF.php",
+ "php samples/Sample_29_Line.php",
+ "php samples/Sample_30_ReadHTML.php",
+ "php samples/Sample_31_Shape.php",
+ "php samples/Sample_32_Chart.php",
+ "php samples/Sample_33_FormField.php",
+ "php samples/Sample_34_SDT.php",
+ "php samples/Sample_35_InternalLink.php",
+ "php samples/Sample_36_RTL.php",
+ "php samples/Sample_37_Comments.php",
+ "php samples/Sample_38_Protection.php",
+ "php samples/Sample_39_TrackChanges.php",
+ "php samples/Sample_40_TemplateSetComplexValue.php",
+ "php samples/Sample_41_TemplateSetChart.php",
+ "php samples/Sample_42_TemplateSetCheckbox.php",
+ "php samples/Sample_43_RTLDefault.php",
+ "php samples/Sample_44_ExtractVariablesFromReaderWord2007.php",
+ "php samples/Sample_45_Autoloader.php"
]
},
"scripts-descriptions": {
@@ -58,34 +105,28 @@
"check": "Runs PHP CheckStyle and PHP Mess detector",
"fix": "Fixes issues found by PHP-CS"
},
- "config": {
- "platform": {
- "php": "8.0"
- }
- },
"require": {
"php": "^7.1|^8.0",
"ext-dom": "*",
+ "ext-gd": "*",
+ "ext-zip": "*",
"ext-json": "*",
"ext-xml": "*",
- "phpoffice/math": "^0.2"
+ "phpoffice/math": "^0.3"
},
"require-dev": {
- "ext-zip": "*",
- "ext-gd": "*",
"ext-libxml": "*",
- "dompdf/dompdf": "^2.0",
- "mpdf/mpdf": "^8.1",
- "phpmd/phpmd": "^2.13",
- "phpunit/phpunit": ">=7.0",
- "tecnickcom/tcpdf": "^6.5",
- "symfony/process": "^4.4 || ^5.0",
+ "dompdf/dompdf": "^2.0 || ^3.0",
"friendsofphp/php-cs-fixer": "^3.3",
- "phpstan/phpstan-phpunit": "@stable"
+ "mpdf/mpdf": "^7.0 || ^8.0",
+ "phpmd/phpmd": "^2.13",
+ "phpstan/phpstan": "^0.12.88 || ^1.0.0",
+ "phpstan/phpstan-phpunit": "^1.0 || ^2.0",
+ "phpunit/phpunit": ">=7.0",
+ "symfony/process": "^4.4 || ^5.0",
+ "tecnickcom/tcpdf": "^6.5"
},
"suggest": {
- "ext-zip": "Allows writing OOXML and ODF",
- "ext-gd2": "Allows adding images",
"ext-xmlwriter": "Allows writing OOXML and ODF",
"ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template",
"dompdf/dompdf": "Allows writing PDF"
diff --git a/vendor/phpoffice/phpword/phpstan-baseline.neon b/vendor/phpoffice/phpword/phpstan-baseline.neon
index 909718b8..86a54276 100644
--- a/vendor/phpoffice/phpword/phpstan-baseline.neon
+++ b/vendor/phpoffice/phpword/phpstan-baseline.neon
@@ -375,11 +375,6 @@ parameters:
count: 1
path: src/PhpWord/Shared/Drawing.php
- -
- message: "#^Binary operation \"\\*\" between string and 50 results in an error\\.$#"
- count: 1
- path: src/PhpWord/Shared/Html.php
-
-
message: "#^Call to an undefined method DOMNode\\:\\:getAttribute\\(\\)\\.$#"
count: 1
@@ -435,6 +430,11 @@ parameters:
count: 1
path: src/PhpWord/Shared/Html.php
+ -
+ message: "#^Method PhpOffice\\\\PhpWord\\\\Shared\\\\Html\\:\\:parseRuby\\(\\) has no return type specified\\.$#"
+ count: 1
+ path: src/PhpWord/Shared/Html.php
+
-
message: "#^Method PhpOffice\\\\PhpWord\\\\Shared\\\\Html\\:\\:parseStyleDeclarations\\(\\) has no return type specified\\.$#"
count: 1
@@ -447,7 +447,7 @@ parameters:
-
message: "#^Parameter \\#1 \\$attribute of static method PhpOffice\\\\PhpWord\\\\Shared\\\\Html\\:\\:parseStyle\\(\\) expects DOMAttr, DOMNode given\\.$#"
- count: 1
+ count: 3
path: src/PhpWord/Shared/Html.php
-
@@ -685,11 +685,6 @@ parameters:
count: 1
path: src/PhpWord/Style/Cell.php
- -
- message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Cell\\:\\:\\$shading is never written, only read\\.$#"
- count: 1
- path: src/PhpWord/Style/Cell.php
-
-
message: "#^Method PhpOffice\\\\PhpWord\\\\Style\\\\Chart\\:\\:getMajorTickPosition\\(\\) has no return type specified\\.$#"
count: 1
@@ -760,41 +755,6 @@ parameters:
count: 1
path: src/PhpWord/Style/Font.php
- -
- message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Font\\:\\:\\$allCaps is never written, only read\\.$#"
- count: 1
- path: src/PhpWord/Style/Font.php
-
- -
- message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Font\\:\\:\\$doubleStrikethrough is never written, only read\\.$#"
- count: 1
- path: src/PhpWord/Style/Font.php
-
- -
- message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Font\\:\\:\\$lang is never written, only read\\.$#"
- count: 1
- path: src/PhpWord/Style/Font.php
-
- -
- message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Font\\:\\:\\$paragraph is never written, only read\\.$#"
- count: 1
- path: src/PhpWord/Style/Font.php
-
- -
- message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Font\\:\\:\\$shading is never written, only read\\.$#"
- count: 1
- path: src/PhpWord/Style/Font.php
-
- -
- message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Font\\:\\:\\$smallCaps is never written, only read\\.$#"
- count: 1
- path: src/PhpWord/Style/Font.php
-
- -
- message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Font\\:\\:\\$strikethrough is never written, only read\\.$#"
- count: 1
- path: src/PhpWord/Style/Font.php
-
-
message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Line\\:\\:\\$weight \\(int\\) does not accept float\\|int\\|null\\.$#"
count: 1
@@ -815,21 +775,6 @@ parameters:
count: 1
path: src/PhpWord/Style/Paragraph.php
- -
- message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Paragraph\\:\\:\\$indentation is never written, only read\\.$#"
- count: 1
- path: src/PhpWord/Style/Paragraph.php
-
- -
- message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Paragraph\\:\\:\\$shading is never written, only read\\.$#"
- count: 1
- path: src/PhpWord/Style/Paragraph.php
-
- -
- message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Paragraph\\:\\:\\$spacing is never written, only read\\.$#"
- count: 1
- path: src/PhpWord/Style/Paragraph.php
-
-
message: "#^Result of && is always false\\.$#"
count: 1
@@ -840,36 +785,6 @@ parameters:
count: 1
path: src/PhpWord/Style/Section.php
- -
- message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Section\\:\\:\\$lineNumbering is never written, only read\\.$#"
- count: 1
- path: src/PhpWord/Style/Section.php
-
- -
- message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Shape\\:\\:\\$extrusion is never written, only read\\.$#"
- count: 1
- path: src/PhpWord/Style/Shape.php
-
- -
- message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Shape\\:\\:\\$fill is never written, only read\\.$#"
- count: 1
- path: src/PhpWord/Style/Shape.php
-
- -
- message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Shape\\:\\:\\$frame is never written, only read\\.$#"
- count: 1
- path: src/PhpWord/Style/Shape.php
-
- -
- message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Shape\\:\\:\\$outline is never written, only read\\.$#"
- count: 1
- path: src/PhpWord/Style/Shape.php
-
- -
- message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Shape\\:\\:\\$shadow is never written, only read\\.$#"
- count: 1
- path: src/PhpWord/Style/Shape.php
-
-
message: "#^Method PhpOffice\\\\PhpWord\\\\Style\\\\TOC\\:\\:setTabLeader\\(\\) should return PhpOffice\\\\PhpWord\\\\Style\\\\TOC but returns PhpOffice\\\\PhpWord\\\\Style\\\\Tab\\.$#"
count: 1
@@ -1120,11 +1035,6 @@ parameters:
count: 1
path: src/PhpWord/Writer/AbstractWriter.php
- -
- message: "#^PHPDoc tag @param has invalid value \\(\\\\PhpOffice\\\\PhpWord\\\\PhpWord\\)\\: Unexpected token \"\\\\n \\*\", expected variable at offset 78$#"
- count: 1
- path: src/PhpWord/Writer/AbstractWriter.php
-
-
message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\HTML\\\\Element\\\\AbstractElement\\:\\:write\\(\\) has no return type specified\\.$#"
count: 1
@@ -1146,14 +1056,14 @@ parameters:
path: src/PhpWord/Writer/ODText/Element/Table.php
-
- message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\ODText\\\\Element\\\\Text\\:\\:replacetabs\\(\\) has parameter \\$text with no type specified\\.$#"
+ message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\ODText\\\\Element\\\\AbstractElement\\:\\:replaceTabs\\(\\) has parameter \\$text with no type specified\\.$#"
count: 1
- path: src/PhpWord/Writer/ODText/Element/Text.php
+ path: src/PhpWord/Writer/ODText/Element/AbstractElement.php
-
- message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\ODText\\\\Element\\\\Text\\:\\:replacetabs\\(\\) has parameter \\$xmlWriter with no type specified\\.$#"
+ message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\ODText\\\\Element\\\\AbstractElement\\:\\:replaceTabs\\(\\) has parameter \\$xmlWriter with no type specified\\.$#"
count: 1
- path: src/PhpWord/Writer/ODText/Element/Text.php
+ path: src/PhpWord/Writer/ODText/Element/AbstractElement.php
-
message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\ODText\\\\Element\\\\Text\\:\\:writeChangeInsertion\\(\\) has parameter \\$start with no type specified\\.$#"
@@ -1315,11 +1225,6 @@ parameters:
count: 1
path: src/PhpWord/Writer/RTF/Style/Border.php
- -
- message: "#^PHPDoc tag @param has invalid value \\(\\\\PhpOffice\\\\PhpWord\\\\PhpWord\\)\\: Unexpected token \"\\\\n \", expected variable at offset 86$#"
- count: 1
- path: src/PhpWord/Writer/Word2007.php
-
-
message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\Word2007\\\\Element\\\\AbstractElement\\:\\:write\\(\\) has no return type specified\\.$#"
count: 1
@@ -1426,29 +1331,29 @@ parameters:
path: tests/PhpWordTests/AbstractTestReader.php
-
- message: "#^Method PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbeddedTest\\:\\:getBaseUrl\\(\\) has no return type specified\\.$#"
+ message: "#^Method PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbedded\\:\\:getBaseUrl\\(\\) has no return type specified\\.$#"
count: 1
- path: tests/PhpWordTests/AbstractWebServerEmbeddedTest.php
+ path: tests/PhpWordTests/AbstractWebServerEmbedded.php
-
- message: "#^Method PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbeddedTest\\:\\:getRemoteBmpImageUrl\\(\\) has no return type specified\\.$#"
+ message: "#^Method PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbedded\\:\\:getRemoteBmpImageUrl\\(\\) has no return type specified\\.$#"
count: 1
- path: tests/PhpWordTests/AbstractWebServerEmbeddedTest.php
+ path: tests/PhpWordTests/AbstractWebServerEmbedded.php
-
- message: "#^Method PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbeddedTest\\:\\:getRemoteGifImageUrl\\(\\) has no return type specified\\.$#"
+ message: "#^Method PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbedded\\:\\:getRemoteGifImageUrl\\(\\) has no return type specified\\.$#"
count: 1
- path: tests/PhpWordTests/AbstractWebServerEmbeddedTest.php
+ path: tests/PhpWordTests/AbstractWebServerEmbedded.php
-
- message: "#^Method PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbeddedTest\\:\\:getRemoteImageUrl\\(\\) has no return type specified\\.$#"
+ message: "#^Method PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbedded\\:\\:getRemoteImageUrl\\(\\) has no return type specified\\.$#"
count: 1
- path: tests/PhpWordTests/AbstractWebServerEmbeddedTest.php
+ path: tests/PhpWordTests/AbstractWebServerEmbedded.php
-
- message: "#^Property PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbeddedTest\\:\\:\\$httpServer has no type specified\\.$#"
+ message: "#^Property PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbedded\\:\\:\\$httpServer has no type specified\\.$#"
count: 1
- path: tests/PhpWordTests/AbstractWebServerEmbeddedTest.php
+ path: tests/PhpWordTests/AbstractWebServerEmbedded.php
-
message: "#^Parameter \\#1 \\$width of class PhpOffice\\\\PhpWord\\\\Element\\\\Cell constructor expects int\\|null, string given\\.$#"
@@ -1789,6 +1694,11 @@ parameters:
message: "#^Cannot access property \\$length on DOMNodeList\\\\|false\\.$#"
count: 9
path: tests/PhpWordTests/Writer/HTML/ElementTest.php
+
+ -
+ message: "#^Cannot access property \\$length on DOMNodeList\\\\|false\\.$#"
+ count: 2
+ path: tests/PhpWordTests/Writer/HTML/Element/RubyTest.php
-
message: "#^Cannot call method item\\(\\) on DOMNodeList\\\\|false\\.$#"
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Collection/Charts.php b/vendor/phpoffice/phpword/src/PhpWord/Collection/Charts.php
index 7c2dfbab..ef1c6597 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Collection/Charts.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Collection/Charts.php
@@ -1,4 +1,5 @@
*/
class Charts extends AbstractCollection
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Collection/Comments.php b/vendor/phpoffice/phpword/src/PhpWord/Collection/Comments.php
index 5fa4020a..4f139435 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Collection/Comments.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Collection/Comments.php
@@ -1,4 +1,5 @@
*/
class Comments extends AbstractCollection
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Collection/Endnotes.php b/vendor/phpoffice/phpword/src/PhpWord/Collection/Endnotes.php
index 09903b1b..3de1ad22 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Collection/Endnotes.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Collection/Endnotes.php
@@ -1,4 +1,5 @@
*/
class Endnotes extends AbstractCollection
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Collection/Footnotes.php b/vendor/phpoffice/phpword/src/PhpWord/Collection/Footnotes.php
index 0387fce3..804a45c8 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Collection/Footnotes.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Collection/Footnotes.php
@@ -1,4 +1,5 @@
*/
class Footnotes extends AbstractCollection
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Collection/Titles.php b/vendor/phpoffice/phpword/src/PhpWord/Collection/Titles.php
index 543aabda..1a943697 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Collection/Titles.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Collection/Titles.php
@@ -1,4 +1,5 @@
*/
class Titles extends AbstractCollection
diff --git a/vendor/phpoffice/phpword/src/PhpWord/ComplexType/FootnoteProperties.php b/vendor/phpoffice/phpword/src/PhpWord/ComplexType/FootnoteProperties.php
index 2e7743ca..10c426e8 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/ComplexType/FootnoteProperties.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/ComplexType/FootnoteProperties.php
@@ -1,4 +1,5 @@
newInstanceArgs($elementArgs);
// Set parent container
@@ -165,7 +167,7 @@ abstract class AbstractContainer extends AbstractElement
/**
* Get all elements.
*
- * @return \PhpOffice\PhpWord\Element\AbstractElement[]
+ * @return AbstractElement[]
*/
public function getElements()
{
@@ -177,7 +179,7 @@ abstract class AbstractContainer extends AbstractElement
*
* @param int $index
*
- * @return null|\PhpOffice\PhpWord\Element\AbstractElement
+ * @return null|AbstractElement
*/
public function getElement($index)
{
@@ -191,13 +193,13 @@ abstract class AbstractContainer extends AbstractElement
/**
* Removes the element at requested index.
*
- * @param int|\PhpOffice\PhpWord\Element\AbstractElement $toRemove
+ * @param AbstractElement|int $toRemove
*/
public function removeElement($toRemove): void
{
if (is_int($toRemove) && array_key_exists($toRemove, $this->elements)) {
unset($this->elements[$toRemove]);
- } elseif ($toRemove instanceof \PhpOffice\PhpWord\Element\AbstractElement) {
+ } elseif ($toRemove instanceof AbstractElement) {
foreach ($this->elements as $key => $element) {
if ($element->getElementId() === $toRemove->getElementId()) {
unset($this->elements[$key]);
@@ -253,7 +255,7 @@ abstract class AbstractContainer extends AbstractElement
'Footnote' => ['Section', 'TextRun', 'Cell', 'ListItemRun'],
'Endnote' => ['Section', 'TextRun', 'Cell'],
'PreserveText' => ['Section', 'Header', 'Footer', 'Cell'],
- 'Title' => ['Section', 'Cell'],
+ 'Title' => ['Section', 'Cell', 'Header'],
'TOC' => ['Section'],
'PageBreak' => ['Section'],
'Chart' => ['Section', 'Cell'],
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Element/AbstractElement.php b/vendor/phpoffice/phpword/src/PhpWord/Element/AbstractElement.php
index 385e4d31..3a29b686 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Element/AbstractElement.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Element/AbstractElement.php
@@ -1,4 +1,5 @@
elementId = substr(md5(mt_rand()), 0, 6);
+ $this->elementId = substr(md5((string) mt_rand()), 0, 6);
}
/**
@@ -291,8 +290,6 @@ abstract class AbstractElement
/**
* Get comments start.
- *
- * @return Comments
*/
public function getCommentsRangeStart(): ?Comments
{
@@ -301,8 +298,6 @@ abstract class AbstractElement
/**
* Get comment start.
- *
- * @return Comment
*/
public function getCommentRangeStart(): ?Comment
{
@@ -339,8 +334,6 @@ abstract class AbstractElement
/**
* Get comments end.
- *
- * @return Comments
*/
public function getCommentsRangeEnd(): ?Comments
{
@@ -349,8 +342,6 @@ abstract class AbstractElement
/**
* Get comment end.
- *
- * @return Comment
*/
public function getCommentRangeEnd(): ?Comment
{
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Element/Bookmark.php b/vendor/phpoffice/phpword/src/PhpWord/Element/Bookmark.php
index 4fe3d0f0..151d5a48 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Element/Bookmark.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Element/Bookmark.php
@@ -1,4 +1,5 @@
phpWord->getTitles()->getItems();
foreach ($titles as $i => $title) {
- /** @var \PhpOffice\PhpWord\Element\Title $title Type hint */
+ /** @var Title $title Type hint */
$depth = $title->getDepth();
if ($this->minDepth > $depth) {
unset($titles[$i]);
@@ -110,7 +111,7 @@ class TOC extends AbstractElement
/**
* Get TOC Style.
*
- * @return \PhpOffice\PhpWord\Style\TOC
+ * @return TOCStyle
*/
public function getStyleTOC()
{
@@ -120,7 +121,7 @@ class TOC extends AbstractElement
/**
* Get Font Style.
*
- * @return \PhpOffice\PhpWord\Style\Font|string
+ * @return Font|string
*/
public function getStyleFont()
{
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Element/Text.php b/vendor/phpoffice/phpword/src/PhpWord/Element/Text.php
index 96953953..f20b273e 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Element/Text.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Element/Text.php
@@ -1,4 +1,5 @@
text;
}
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Element/TextBox.php b/vendor/phpoffice/phpword/src/PhpWord/Element/TextBox.php
index af37f657..b9f8b50b 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Element/TextBox.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Element/TextBox.php
@@ -1,4 +1,5 @@
getElements('w:r|w:hyperlink', $domNode);
- if ($nodes->length === 1) {
+ $hasRubyElement = $xmlReader->elementExists('w:r/w:ruby', $domNode);
+ if ($nodes->length === 1 && !$hasRubyElement) {
$textContent = htmlspecialchars($xmlReader->getValue('w:t', $nodes->item(0)), ENT_QUOTES, 'UTF-8');
} else {
$textContent = new TextRun($paragraphStyle);
@@ -450,7 +455,7 @@ abstract class AbstractPart
/**
* Read w:r.
*
- * @param \PhpOffice\PhpWord\Element\AbstractContainer $parent
+ * @param AbstractContainer $parent
* @param string $docPart
* @param mixed $paragraphStyle
*
@@ -584,9 +589,47 @@ abstract class AbstractPart
}
} elseif ($node->nodeName == 'w:softHyphen') {
$element = $parent->addText("\u{200c}", $fontStyle, $paragraphStyle);
+ } elseif ($node->nodeName == 'w:ruby') {
+ $rubyPropertiesNode = $xmlReader->getElement('w:rubyPr', $node);
+ $properties = $this->readRubyProperties($xmlReader, $rubyPropertiesNode);
+ // read base text node
+ $baseText = new TextRun($paragraphStyle);
+ $baseTextNode = $xmlReader->getElement('w:rubyBase/w:r', $node);
+ $this->readRun($xmlReader, $baseTextNode, $baseText, $docPart, $paragraphStyle);
+ // read the actual ruby text (e.g. furigana in Japanese)
+ $rubyText = new TextRun($paragraphStyle);
+ $rubyTextNode = $xmlReader->getElement('w:rt/w:r', $node);
+ $this->readRun($xmlReader, $rubyTextNode, $rubyText, $docPart, $paragraphStyle);
+ // add element to parent
+ $parent->addRuby($baseText, $rubyText, $properties);
}
}
+ /**
+ * Read w:rubyPr element.
+ *
+ * @param XMLReader $xmlReader reader for XML
+ * @param DOMElement $domNode w:RubyPr element
+ *
+ * @return RubyProperties ruby properties from element
+ */
+ protected function readRubyProperties(XMLReader $xmlReader, DOMElement $domNode): RubyProperties
+ {
+ $rubyAlignment = $xmlReader->getElement('w:rubyAlign', $domNode)->getAttribute('w:val');
+ $rubyHps = $xmlReader->getElement('w:hps', $domNode)->getAttribute('w:val'); // font face
+ $rubyHpsRaise = $xmlReader->getElement('w:hpsRaise', $domNode)->getAttribute('w:val'); // pts above base text
+ $rubyHpsBaseText = $xmlReader->getElement('w:hpsBaseText', $domNode)->getAttribute('w:val'); // base text size
+ $rubyLid = $xmlReader->getElement('w:lid', $domNode)->getAttribute('w:val'); // type of ruby
+ $properties = new RubyProperties();
+ $properties->setAlignment($rubyAlignment);
+ $properties->setFontFaceSize((float) $rubyHps);
+ $properties->setFontPointsAboveBaseText((float) $rubyHpsRaise);
+ $properties->setFontSizeForBaseText((float) $rubyHpsBaseText);
+ $properties->setLanguageId($rubyLid);
+
+ return $properties;
+ }
+
/**
* Read w:tbl.
*
@@ -661,8 +704,11 @@ abstract class AbstractPart
'alignment' => [self::READ_VALUE, 'w:jc'],
'basedOn' => [self::READ_VALUE, 'w:basedOn'],
'next' => [self::READ_VALUE, 'w:next'],
- 'indent' => [self::READ_VALUE, 'w:ind', 'w:left'],
- 'hanging' => [self::READ_VALUE, 'w:ind', 'w:hanging'],
+ 'indentLeft' => [self::READ_VALUE, 'w:ind', 'w:left'],
+ 'indentRight' => [self::READ_VALUE, 'w:ind', 'w:right'],
+ 'indentHanging' => [self::READ_VALUE, 'w:ind', 'w:hanging'],
+ 'indentFirstLine' => [self::READ_VALUE, 'w:ind', 'w:firstLine'],
+ 'indentFirstLineChars' => [self::READ_VALUE, 'w:ind', 'w:firstLineChars'],
'spaceAfter' => [self::READ_VALUE, 'w:spacing', 'w:after'],
'spaceBefore' => [self::READ_VALUE, 'w:spacing', 'w:before'],
'widowControl' => [self::READ_FALSE, 'w:widowControl'],
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Reader/Word2007/DocPropsApp.php b/vendor/phpoffice/phpword/src/PhpWord/Reader/Word2007/DocPropsApp.php
index 9d6f3cbb..c7ecb007 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Reader/Word2007/DocPropsApp.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Reader/Word2007/DocPropsApp.php
@@ -1,4 +1,5 @@
setDefaultFontSize($fontDefaultStyle['size']);
}
+ if (array_key_exists('color', $fontDefaultStyle)) {
+ $phpWord->setDefaultFontColor($fontDefaultStyle['color']);
+ }
if (array_key_exists('lang', $fontDefaultStyle)) {
$phpWord->getSettings()->setThemeFontLang(new Language($fontDefaultStyle['lang']));
}
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Settings.php b/vendor/phpoffice/phpword/src/PhpWord/Settings.php
index 984486cc..16f49166 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Settings.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Settings.php
@@ -1,4 +1,5 @@
cssContent);
preg_match_all('/(.+?)\s?\{\s?(.+?)\s?\}/', $cssContent, $cssExtracted);
- // Check the number of extracted
- if (count($cssExtracted) != 3) {
- return;
- }
// Check if there are x selectors and x rules
if (count($cssExtracted[1]) != count($cssExtracted[2])) {
return;
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Shared/Drawing.php b/vendor/phpoffice/phpword/src/PhpWord/Shared/Drawing.php
index df218108..8af7da2f 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Shared/Drawing.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Shared/Drawing.php
@@ -1,4 +1,5 @@
vAlign = null;
+
+ return $this;
+ }
+
VerticalJc::validate($value);
$this->vAlign = $this->setEnumVal($value, VerticalJc::values(), $this->vAlign);
@@ -235,7 +262,7 @@ class Cell extends Border
/**
* Get vertical merge (rowspan).
*
- * @return string
+ * @return null|string
*/
public function getVMerge()
{
@@ -245,12 +272,18 @@ class Cell extends Border
/**
* Set vertical merge (rowspan).
*
- * @param string $value
+ * @param null|string $value
*
* @return self
*/
public function setVMerge($value = null)
{
+ if ($value === null) {
+ $this->vMerge = null;
+
+ return $this;
+ }
+
$enum = [self::VMERGE_RESTART, self::VMERGE_CONTINUE];
$this->vMerge = $this->setEnumVal($value, $enum, $this->vMerge);
@@ -260,7 +293,7 @@ class Cell extends Border
/**
* Get shading.
*
- * @return \PhpOffice\PhpWord\Style\Shading
+ * @return Shading
*/
public function getShading()
{
@@ -344,4 +377,84 @@ class Cell extends Border
{
return $this->noWrap;
}
+
+ /**
+ * Get style padding-top.
+ */
+ public function getPaddingTop(): ?int
+ {
+ return $this->paddingTop;
+ }
+
+ /**
+ * Set style padding-top.
+ *
+ * @return $this
+ */
+ public function setPaddingTop(int $value): self
+ {
+ $this->paddingTop = $value;
+
+ return $this;
+ }
+
+ /**
+ * Get style padding-bottom.
+ */
+ public function getPaddingBottom(): ?int
+ {
+ return $this->paddingBottom;
+ }
+
+ /**
+ * Set style padding-bottom.
+ *
+ * @return $this
+ */
+ public function setPaddingBottom(int $value): self
+ {
+ $this->paddingBottom = $value;
+
+ return $this;
+ }
+
+ /**
+ * Get style padding-left.
+ */
+ public function getPaddingLeft(): ?int
+ {
+ return $this->paddingLeft;
+ }
+
+ /**
+ * Set style padding-left.
+ *
+ * @return $this
+ */
+ public function setPaddingLeft(int $value): self
+ {
+ $this->paddingLeft = $value;
+
+ return $this;
+ }
+
+ /**
+ * Get style padding-right.
+ */
+ public function getPaddingRight(): ?int
+ {
+ return $this->paddingRight;
+ }
+
+ /**
+ * Set style padding-right.
+ *
+ * @return $this
+ */
+ public function setPaddingRight(int $value): self
+ {
+ $this->paddingRight = $value;
+
+ return $this;
+ }
}
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Style/Extrusion.php b/vendor/phpoffice/phpword/src/PhpWord/Style/Extrusion.php
index dd002312..c31c51eb 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Style/Extrusion.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Style/Extrusion.php
@@ -1,4 +1,5 @@
zip()->AddFromString("word/media/image1.jpg", file_get_contents($file));
* To read a file: $templateProcessor->zip()->getFromName("word/media/image1.jpg");
*
- * @return \PhpOffice\PhpWord\Shared\ZipArchive
+ * @return ZipArchive
*/
public function zip()
{
@@ -269,7 +269,7 @@ class TemplateProcessor
*/
protected static function ensureUtf8Encoded($subject)
{
- return $subject ? Text::toUTF8($subject) : '';
+ return (null !== $subject) ? Text::toUTF8($subject) : '';
}
/**
@@ -281,7 +281,7 @@ class TemplateProcessor
$objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $elementName;
$xmlWriter = new XMLWriter();
- /** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $elementWriter */
+ /** @var Writer\Word2007\Element\AbstractElement $elementWriter */
$elementWriter = new $objectClass($xmlWriter, $complexType, true);
$elementWriter->write();
@@ -308,7 +308,7 @@ class TemplateProcessor
$objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $elementName;
$xmlWriter = new XMLWriter();
- /** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $elementWriter */
+ /** @var Writer\Word2007\Element\AbstractElement $elementWriter */
$elementWriter = new $objectClass($xmlWriter, $complexType, false);
$elementWriter->write();
@@ -362,10 +362,10 @@ class TemplateProcessor
/**
* Set values from a one-dimensional array of "variable => value"-pairs.
*/
- public function setValues(array $values): void
+ public function setValues(array $values, int $limit = self::MAXIMUM_REPLACEMENTS_DEFAULT): void
{
foreach ($values as $macro => $replace) {
- $this->setValue($macro, $replace);
+ $this->setValue($macro, $replace, $limit);
}
}
@@ -406,7 +406,7 @@ class TemplateProcessor
$filename = "charts/chart{$rId}.xml";
// Get the part writer
- $writerPart = new \PhpOffice\PhpWord\Writer\Word2007\Part\Chart();
+ $writerPart = new Writer\Word2007\Part\Chart();
$writerPart->setElement($chart);
// ContentTypes.xml
@@ -499,20 +499,22 @@ class TemplateProcessor
$widthFloat = $heightFloat * $imageRatio;
$matches = [];
preg_match('/\\d([a-z%]+)$/', $height, $matches);
- $width = $widthFloat . $matches[1];
+ $width = $widthFloat . (!empty($matches) ? $matches[1] : 'px');
} elseif ($height === '') { // defined height is empty
$widthFloat = (float) $width;
$heightFloat = $widthFloat / $imageRatio;
$matches = [];
preg_match('/\\d([a-z%]+)$/', $width, $matches);
- $height = $heightFloat . $matches[1];
+ $height = $heightFloat . (!empty($matches) ? $matches[1] : 'px');
} else { // we have defined size, but we need also check it aspect ratio
$widthMatches = [];
preg_match('/\\d([a-z%]+)$/', $width, $widthMatches);
$heightMatches = [];
preg_match('/\\d([a-z%]+)$/', $height, $heightMatches);
// try to fix only if dimensions are same
- if ($widthMatches[1] == $heightMatches[1]) {
+ if (!empty($widthMatches)
+ && !empty($heightMatches)
+ && $widthMatches[1] == $heightMatches[1]) {
$dimention = $widthMatches[1];
$widthFloat = (float) $width;
$heightFloat = (float) $height;
@@ -1287,7 +1289,7 @@ class TemplateProcessor
* @param int $count
* @param string $xmlBlock
*
- * @return string
+ * @return array
*/
protected function indexClonedVariables($count, $xmlBlock)
{
@@ -1339,7 +1341,7 @@ class TemplateProcessor
* @param string $block New block content
* @param string $blockType XML tag type of block
*
- * @return \PhpOffice\PhpWord\TemplateProcessor Fluent interface
+ * @return TemplateProcessor Fluent interface
*/
public function replaceXmlBlock($macro, $block, $blockType = 'w:p')
{
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML.php
index 64789059..41229b45 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML.php
@@ -1,4 +1,5 @@
parts as $partName) {
$partClass = 'PhpOffice\\PhpWord\\Writer\\HTML\\Part\\' . $partName;
if (class_exists($partClass)) {
- /** @var \PhpOffice\PhpWord\Writer\HTML\Part\AbstractPart $part Type hint */
+ /** @var HTML\Part\AbstractPart $part Type hint */
$part = new $partClass();
$part->setParentWriter($this);
$this->writerParts[strtolower($partName)] = $part;
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/AbstractElement.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/AbstractElement.php
index 7c7bde31..0e6a112e 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/AbstractElement.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/AbstractElement.php
@@ -1,4 +1,5 @@
namespace, $elementClass);
if (class_exists($writerClass)) {
- /** @var \PhpOffice\PhpWord\Writer\HTML\Element\AbstractElement $writer Type hint */
+ /** @var AbstractElement $writer Type hint */
$writer = new $writerClass($this->parentWriter, $element, $withoutP);
$content .= $writer->write();
}
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/Endnote.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/Endnote.php
index 1c35e8fa..7e7f31d4 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/Endnote.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/Endnote.php
@@ -1,4 +1,5 @@
' . PHP_EOL;
-
+ $defaultFontColor = Settings::getDefaultFontColor();
// Default styles
$astarray = [
'font-family' => $this->getFontFamily(Settings::getDefaultFontName(), $this->getParentWriter()->getDefaultGenericFont()),
'font-size' => Settings::getDefaultFontSize() . 'pt',
+ 'color' => "#{$defaultFontColor}",
];
// Mpdf sometimes needs separate tag for body; doesn't harm others.
$bodyarray = $astarray;
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Style/AbstractStyle.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Style/AbstractStyle.php
index a6507867..4672347b 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Style/AbstractStyle.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Style/AbstractStyle.php
@@ -1,4 +1,5 @@
getVAlign();
+ }
foreach (['Top', 'Left', 'Bottom', 'Right'] as $direction) {
$method = 'getBorder' . $direction . 'Style';
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/AbstractElement.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/AbstractElement.php
index 5cd396aa..97d1875c 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/AbstractElement.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/AbstractElement.php
@@ -1,4 +1,5 @@
startElement('text:s');
+ $xmlWriter->writeAttributeIf($num > 1, 'text:c', "$num");
+ $xmlWriter->endElement();
+ $text = preg_replace('/^ +/', '', $text);
+ }
+ preg_match_all('/([\\s\\S]*?)(\\t| +| ?$)/', $text, $matches, PREG_SET_ORDER);
+ foreach ($matches as $match) {
+ $this->writeText($match[1]);
+ if ($match[2] === '') {
+ break;
+ } elseif ($match[2] === "\t") {
+ $xmlWriter->writeElement('text:tab');
+ } elseif ($match[2] === ' ') {
+ $xmlWriter->writeElement('text:s');
+
+ break;
+ } else {
+ $num = strlen($match[2]);
+ $xmlWriter->startElement('text:s');
+ $xmlWriter->writeAttributeIf($num > 1, 'text:c', "$num");
+ $xmlWriter->endElement();
+ }
+ }
+ }
}
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Container.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Container.php
index 6e6b88ea..6b0fee8c 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Container.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Container.php
@@ -1,4 +1,5 @@
+ */
+ protected $containerWithoutP = ['TextRun', 'Footnote', 'Endnote'];
}
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Field.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Field.php
index 46f62b0f..6b548078 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Field.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Field.php
@@ -1,4 +1,5 @@
getXmlWriter();
$element = $this->getElement();
- if (!$element instanceof \PhpOffice\PhpWord\Element\Image) {
+ if (!$element instanceof ElementImage) {
return;
}
@@ -43,11 +44,16 @@ class Image extends AbstractElement
$width = Converter::pixelToCm($style->getWidth());
$height = Converter::pixelToCm($style->getHeight());
- $xmlWriter->startElement('text:p');
- $xmlWriter->writeAttribute('text:style-name', 'IM' . $mediaIndex);
+ $xmlWriter = $this->getXmlWriter();
+
+ if (!$this->withoutP) {
+ $xmlWriter->startElement('text:p');
+ $xmlWriter->writeAttribute('text:style-name', 'IM' . $mediaIndex);
+ }
$xmlWriter->startElement('draw:frame');
$xmlWriter->writeAttribute('draw:style-name', 'fr' . $mediaIndex);
+ $xmlWriter->writeAttributeIf($this->withoutP, 'draw:text-style-name', 'IM' . $mediaIndex);
$xmlWriter->writeAttribute('draw:name', $element->getElementId());
$xmlWriter->writeAttribute('text:anchor-type', 'as-char');
$xmlWriter->writeAttribute('svg:width', $width . 'cm');
@@ -63,6 +69,8 @@ class Image extends AbstractElement
$xmlWriter->endElement(); // draw:frame
- $xmlWriter->endElement(); // text:p
+ if (!$this->withoutP) {
+ $xmlWriter->endElement(); // text:p
+ }
}
}
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Link.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Link.php
index 0375b11b..9ef35692 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Link.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Link.php
@@ -1,4 +1,5 @@
getXmlWriter();
$element = $this->getElement();
- if (!$element instanceof \PhpOffice\PhpWord\Element\Table) {
+ if (!$element instanceof TableElement) {
return;
}
$rows = $element->getRows();
@@ -77,7 +78,7 @@ class Table extends AbstractElement
private function writeRow(XMLWriter $xmlWriter, RowElement $row): void
{
$xmlWriter->startElement('table:table-row');
- /** @var \PhpOffice\PhpWord\Element\Row $row Type hint */
+ /** @var RowElement $row Type hint */
foreach ($row->getCells() as $cell) {
$xmlWriter->startElement('table:table-cell');
$xmlWriter->writeAttribute('office:value-type', 'string');
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/TextRun.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/TextRun.php
index 6d1e1a19..9d009c39 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/TextRun.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/TextRun.php
@@ -1,4 +1,5 @@
imageParagraphStyles as $style) {
- $styleWriter = new \PhpOffice\PhpWord\Writer\ODText\Style\Paragraph($xmlWriter, $style);
+ $styleWriter = new ParagraphStyleWriter($xmlWriter, $style);
$styleWriter->write();
}
}
@@ -256,7 +257,7 @@ class Content extends AbstractPart
*
* Table style can be null or string of the style name
*
- * @param \PhpOffice\PhpWord\Element\AbstractContainer $container
+ * @param AbstractContainer $container
* @param int $paragraphStyleCount
* @param int $fontStyleCount
*
@@ -277,7 +278,7 @@ class Content extends AbstractPart
$style = $element->getStyle();
$style->setStyleName('fr' . $element->getMediaIndex());
$this->autoStyles['Image'][] = $style;
- $sty = new \PhpOffice\PhpWord\Style\Paragraph();
+ $sty = new Paragraph();
$sty->setStyleName('IM' . $element->getMediaIndex());
$sty->setAuto();
$sty->setAlignment($style->getAlignment());
@@ -300,7 +301,7 @@ class Content extends AbstractPart
/**
* Get style of individual element.
*
- * @param \PhpOffice\PhpWord\Element\Text $element
+ * @param Text $element
* @param int $paragraphStyleCount
* @param int $fontStyleCount
*/
@@ -346,7 +347,7 @@ class Content extends AbstractPart
/**
* Get font style of individual field element.
*
- * @param \PhpOffice\PhpWord\Element\Field $element
+ * @param Field $element
* @param int $fontStyleCount
*/
private function getElementStyleField($element, &$fontStyleCount): void
@@ -371,7 +372,7 @@ class Content extends AbstractPart
/**
* Get style of individual element.
*
- * @param \PhpOffice\PhpWord\Element\TextRun $element
+ * @param TextRun $element
* @param int $paragraphStyleCount
*/
private function getElementStyleTextRun($element, &$paragraphStyleCount): void
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Part/Meta.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Part/Meta.php
index 8a35d02a..21410ba9 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Part/Meta.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Part/Meta.php
@@ -1,4 +1,5 @@
setStyleName($style->getStyleName());
- $temp2 = new \PhpOffice\PhpWord\Writer\ODText\Style\Paragraph($xmlWriter, $temp1);
+ $temp2 = new Paragraph($xmlWriter, $temp1);
$temp2->write();
}
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/Paragraph.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/Paragraph.php
index 4459c76c..99963bd4 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/Paragraph.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/Paragraph.php
@@ -1,4 +1,5 @@
getStyle();
- if (!$style instanceof \PhpOffice\PhpWord\Style\Paragraph) {
+ if (!$style instanceof Style\Paragraph) {
return;
}
$xmlWriter = $this->getXmlWriter();
@@ -73,13 +74,13 @@ class Paragraph extends AbstractStyle
} elseif (substr($styleName, 0, 2) === 'HD') {
$styleAuto = true;
$psm = 'Heading_' . substr($styleName, 2);
- $stylep = \PhpOffice\PhpWord\Style::getStyle($psm);
- if ($stylep instanceof \PhpOffice\PhpWord\Style\Font) {
+ $stylep = Style::getStyle($psm);
+ if ($stylep instanceof Style\Font) {
if (method_exists($stylep, 'getParagraph')) {
$stylep = $stylep->getParagraph();
}
}
- if ($stylep instanceof \PhpOffice\PhpWord\Style\Paragraph) {
+ if ($stylep instanceof Style\Paragraph) {
if ($stylep->hasPageBreakBefore()) {
$breakbefore = true;
}
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/Section.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/Section.php
index 0a250194..05152a39 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/Section.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/Section.php
@@ -1,4 +1,5 @@
parts as $partName) {
$partClass = static::class . '\\Part\\' . $partName;
if (class_exists($partClass)) {
- /** @var \PhpOffice\PhpWord\Writer\RTF\Part\AbstractPart $part Type hint */
+ /** @var RTF\Part\AbstractPart $part Type hint */
$part = new $partClass();
$part->setParentWriter($this);
$this->writerParts[strtolower($partName)] = $part;
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Element/AbstractElement.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Element/AbstractElement.php
index 5c33868a..e007e6aa 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Element/AbstractElement.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Element/AbstractElement.php
@@ -1,4 +1,5 @@
parentWriter;
/** @var \PhpOffice\PhpWord\Element\Text $element Type hint */
@@ -188,7 +189,7 @@ abstract class AbstractElement
return '';
}
- /** @var \PhpOffice\PhpWord\Writer\RTF $parentWriter Type hint */
+ /** @var WriterRTF $parentWriter Type hint */
$parentWriter = $this->parentWriter;
// Create style writer and set color/name index
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Element/Field.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Element/Field.php
index 34024f5e..c5e49c1d 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Element/Field.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Element/Field.php
@@ -1,4 +1,5 @@
getHeaders() as $header) {
$type = $header->getType();
- if ($evenOdd || $type !== FOOTER::EVEN) {
+ if ($evenOdd || $type !== Footer::EVEN) {
$content .= '{\\header';
if ($type === Footer::FIRST) {
$content .= 'f';
} elseif ($evenOdd) {
- $content .= ($type === FOOTER::EVEN) ? 'l' : 'r';
+ $content .= ($type === Footer::EVEN) ? 'l' : 'r';
}
foreach ($header->getElements() as $element) {
$cl = get_class($element);
@@ -182,12 +183,12 @@ class Document extends AbstractPart
}
foreach ($section->getFooters() as $footer) {
$type = $footer->getType();
- if ($evenOdd || $type !== FOOTER::EVEN) {
+ if ($evenOdd || $type !== Footer::EVEN) {
$content .= '{\\footer';
if ($type === Footer::FIRST) {
$content .= 'f';
} elseif ($evenOdd) {
- $content .= ($type === FOOTER::EVEN) ? 'l' : 'r';
+ $content .= ($type === Footer::EVEN) ? 'l' : 'r';
}
foreach ($footer->getElements() as $element) {
$cl = get_class($element);
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Part/Header.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Part/Header.php
index 7f8cc84b..97644fe4 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Part/Header.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Part/Header.php
@@ -1,4 +1,5 @@
parts) as $partName) {
$partClass = static::class . '\\Part\\' . $partName;
if (class_exists($partClass)) {
- /** @var \PhpOffice\PhpWord\Writer\Word2007\Part\AbstractPart $part Type hint */
+ /** @var Word2007\Part\AbstractPart $part Type hint */
$part = new $partClass();
$part->setParentWriter($this);
$this->writerParts[strtolower($partName)] = $part;
@@ -179,7 +178,7 @@ class Word2007 extends AbstractWriter implements WriterInterface
$this->registerContentTypes($media);
}
- /** @var \PhpOffice\PhpWord\Writer\Word2007\Part\AbstractPart $writerPart Type hint */
+ /** @var Word2007\Part\AbstractPart $writerPart Type hint */
$writerPart = $this->getWriterPart('relspart')->setMedia($media);
$zip->addFromString("word/_rels/{$file}.xml.rels", $writerPart->write());
}
@@ -206,7 +205,7 @@ class Word2007 extends AbstractWriter implements WriterInterface
$this->contentTypes['override']["/word/$elmFile"] = $elmType;
$this->relationships[] = ['target' => $elmFile, 'type' => $elmType, 'rID' => $rId];
- /** @var \PhpOffice\PhpWord\Writer\Word2007\Part\AbstractPart $writerPart Type hint */
+ /** @var Word2007\Part\AbstractPart $writerPart Type hint */
$writerPart = $this->getWriterPart($elmType)->setElement($element);
$zip->addFromString("word/$elmFile", $writerPart->write());
}
@@ -236,7 +235,7 @@ class Word2007 extends AbstractWriter implements WriterInterface
// Write relationships file, e.g. word/_rels/footnotes.xml
if (!empty($media)) {
- /** @var \PhpOffice\PhpWord\Writer\Word2007\Part\AbstractPart $writerPart Type hint */
+ /** @var Word2007\Part\AbstractPart $writerPart Type hint */
$writerPart = $this->getWriterPart('relspart')->setMedia($media);
$zip->addFromString("word/_rels/{$partName}.xml.rels", $writerPart->write());
}
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Bookmark.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Bookmark.php
index 1e618af9..ba61ad69 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Bookmark.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Bookmark.php
@@ -1,4 +1,5 @@
endElement(); // w:r
if ($element->getText() != null) {
- if ($element->getText() instanceof \PhpOffice\PhpWord\Element\TextRun) {
+ if ($element->getText() instanceof TextRun) {
$containerWriter = new Container($xmlWriter, $element->getText(), true);
$containerWriter->write();
}
@@ -262,7 +263,7 @@ class Field extends Text
$xmlWriter->endElement(); // w:r
if ($element->getText() != null) {
- if ($element->getText() instanceof \PhpOffice\PhpWord\Element\TextRun) {
+ if ($element->getText() instanceof TextRun) {
$containerWriter = new Container($xmlWriter, $element->getText(), true);
$containerWriter->write();
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Formula.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Formula.php
index 6abb74b7..95567062 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Formula.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Formula.php
@@ -1,4 +1,5 @@
getBgColor()) {
$xmlWriter->writeAttribute('fillcolor', $style->getBgColor());
+ } else {
+ $xmlWriter->writeAttribute('filled', 'f');
+ }
+
+ if (!$style->getBorderColor()) {
+ $xmlWriter->writeAttribute('stroked', 'f');
+ $xmlWriter->writeAttribute('strokecolor', 'white');
}
$styleWriter->write();
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/TextBreak.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/TextBreak.php
index bcae3b2e..4c2ecde7 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/TextBreak.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/TextBreak.php
@@ -1,4 +1,5 @@
endElement(); // w:tcW
}
+ $paddingTop = $style->getPaddingTop();
+ $paddingLeft = $style->getPaddingLeft();
+ $paddingBottom = $style->getPaddingBottom();
+ $paddingRight = $style->getPaddingRight();
+
+ if ($paddingTop !== null || $paddingLeft !== null || $paddingBottom !== null || $paddingRight !== null) {
+ $xmlWriter->startElement('w:tcMar');
+ if ($paddingTop !== null) {
+ $xmlWriter->startElement('w:top');
+ $xmlWriter->writeAttribute('w:w', $paddingTop);
+ $xmlWriter->writeAttribute('w:type', \PhpOffice\PhpWord\SimpleType\TblWidth::TWIP);
+ $xmlWriter->endElement(); // w:top
+ }
+ if ($paddingLeft !== null) {
+ $xmlWriter->startElement('w:start');
+ $xmlWriter->writeAttribute('w:w', $paddingLeft);
+ $xmlWriter->writeAttribute('w:type', \PhpOffice\PhpWord\SimpleType\TblWidth::TWIP);
+ $xmlWriter->endElement(); // w:start
+ }
+ if ($paddingBottom !== null) {
+ $xmlWriter->startElement('w:bottom');
+ $xmlWriter->writeAttribute('w:w', $paddingBottom);
+ $xmlWriter->writeAttribute('w:type', \PhpOffice\PhpWord\SimpleType\TblWidth::TWIP);
+ $xmlWriter->endElement(); // w:bottom
+ }
+ if ($paddingRight !== null) {
+ $xmlWriter->startElement('w:end');
+ $xmlWriter->writeAttribute('w:w', $paddingRight);
+ $xmlWriter->writeAttribute('w:type', \PhpOffice\PhpWord\SimpleType\TblWidth::TWIP);
+ $xmlWriter->endElement(); // w:end
+ }
+ $xmlWriter->endElement(); // w:tcMar
+ }
+
// Text direction
$textDir = $style->getTextDirection();
$xmlWriter->writeElementIf(null !== $textDir, 'w:textDirection', 'w:val', $textDir);
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Extrusion.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Extrusion.php
index f6ad6221..8bb92187 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Extrusion.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Extrusion.php
@@ -1,4 +1,5 @@
getFirstLine();
$xmlWriter->writeAttributeIf(null !== $firstLine, 'w:firstLine', $this->convertTwip($firstLine));
+ $firstLineChars = $style->getFirstLineChars();
+ $xmlWriter->writeAttributeIf(0 !== $firstLineChars, 'w:firstLineChars', $this->convertTwip($firstLineChars));
+
$hanging = $style->getHanging();
$xmlWriter->writeAttributeIf(null !== $hanging, 'w:hanging', $this->convertTwip($hanging));
diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Line.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Line.php
index 90107f8e..2603545f 100644
--- a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Line.php
+++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Line.php
@@ -1,4 +1,5 @@
=8.0.0"
+ "php": ">=5.3.0"
},
"autoload": {
"psr-4": {
- "Psr\\Log\\": "src"
+ "Psr\\Log\\": "Psr/Log/"
}
},
"extra": {
"branch-alias": {
- "dev-master": "3.x-dev"
+ "dev-master": "1.1.x-dev"
}
}
}
diff --git a/vendor/topthink/think-image/src/image/Exception.php b/vendor/topthink/think-image/src/image/Exception.php
index 2ebafd8c..c35c71b9 100644
--- a/vendor/topthink/think-image/src/image/Exception.php
+++ b/vendor/topthink/think-image/src/image/Exception.php
@@ -11,8 +11,7 @@
namespace think\image;
-
class Exception extends \RuntimeException
{
-}
\ No newline at end of file
+}
diff --git a/vendor/topthink/think-image/src/image/gif/Decoder.php b/vendor/topthink/think-image/src/image/gif/Decoder.php
index a5630926..95fa2b84 100644
--- a/vendor/topthink/think-image/src/image/gif/Decoder.php
+++ b/vendor/topthink/think-image/src/image/gif/Decoder.php
@@ -11,7 +11,6 @@
namespace think\image\gif;
-
class Decoder
{
public $GIF_buffer = [];
@@ -204,4 +203,4 @@ class Decoder
{
return ($this->GIF_delays);
}
-}
\ No newline at end of file
+}
diff --git a/vendor/topthink/think-image/tests/CropTest.php b/vendor/topthink/think-image/tests/CropTest.php
index 26a05313..e2676307 100644
--- a/vendor/topthink/think-image/tests/CropTest.php
+++ b/vendor/topthink/think-image/tests/CropTest.php
@@ -64,4 +64,4 @@ class CropTest extends TestCase
@unlink($pathname);
}
-}
\ No newline at end of file
+}
diff --git a/vendor/topthink/think-image/tests/FlipTest.php b/vendor/topthink/think-image/tests/FlipTest.php
index bf7a5e26..d91d3af9 100644
--- a/vendor/topthink/think-image/tests/FlipTest.php
+++ b/vendor/topthink/think-image/tests/FlipTest.php
@@ -27,7 +27,6 @@ class FlipTest extends TestCase
@unlink($pathname);
}
-
public function testGif()
{
$pathname = TEST_PATH . 'tmp/flip.gif';
@@ -40,4 +39,4 @@ class FlipTest extends TestCase
@unlink($pathname);
}
-}
\ No newline at end of file
+}
diff --git a/vendor/topthink/think-image/tests/RotateTest.php b/vendor/topthink/think-image/tests/RotateTest.php
index 56572d27..202062d3 100644
--- a/vendor/topthink/think-image/tests/RotateTest.php
+++ b/vendor/topthink/think-image/tests/RotateTest.php
@@ -39,4 +39,4 @@ class RotateTest extends TestCase
@unlink($pathname);
}
-}
\ No newline at end of file
+}
diff --git a/vendor/topthink/think-image/tests/TestCase.php b/vendor/topthink/think-image/tests/TestCase.php
index 89ba1b45..3fd401ce 100644
--- a/vendor/topthink/think-image/tests/TestCase.php
+++ b/vendor/topthink/think-image/tests/TestCase.php
@@ -30,4 +30,4 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase
{
return new File(TEST_PATH . 'images/test.gif');
}
-}
\ No newline at end of file
+}
diff --git a/vendor/topthink/think-image/tests/WaterTest.php b/vendor/topthink/think-image/tests/WaterTest.php
index b6a2bcce..7c2c1dac 100644
--- a/vendor/topthink/think-image/tests/WaterTest.php
+++ b/vendor/topthink/think-image/tests/WaterTest.php
@@ -55,4 +55,4 @@ class WaterTest extends TestCase
@unlink($pathname);
}
-}
\ No newline at end of file
+}
diff --git a/vendor/topthink/think-image/tests/autoload.php b/vendor/topthink/think-image/tests/autoload.php
index f2e8aaef..d21954b2 100644
--- a/vendor/topthink/think-image/tests/autoload.php
+++ b/vendor/topthink/think-image/tests/autoload.php
@@ -12,4 +12,4 @@ define('TEST_PATH', __DIR__ . '/');
// 加载框架基础文件
require __DIR__ . '/../thinkphp/base.php';
\think\Loader::addNamespace('tests', TEST_PATH);
-\think\Loader::addNamespace('think', __DIR__ . '/../src/');
\ No newline at end of file
+\think\Loader::addNamespace('think', __DIR__ . '/../src/');
From da71dfc04e6979f305c5441b7dd42c14cc68b262 Mon Sep 17 00:00:00 2001
From: wyn <1074145239@qq.com>
Date: Tue, 30 Jun 2026 09:30:33 +0800
Subject: [PATCH 4/4] =?UTF-8?q?=E6=96=B0=E9=80=BB=E8=BE=91=E5=8F=82?=
=?UTF-8?q?=E8=80=83=E6=96=87=E7=8C=AE=E7=9B=B8=E5=85=B3=E6=80=A7=E6=95=B4?=
=?UTF-8?q?=E5=90=88=E4=B9=8B=E5=89=8D=E7=9A=84=E9=80=BB=E8=BE=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
application/api/controller/References.php | 141 ++++++------------
application/command.php | 1 -
.../command/ReferenceCheckMqConsume.php | 2 +-
application/common/ReferenceCheckService.php | 37 +----
application/common/mq/RabbitMqConfig.php | 6 -
.../common/mq/ReferenceCheckArticleWorker.php | 85 ++++++-----
.../service/ReferenceRelevanceLlmService.php | 73 +++++++--
application/extra/rabbitmq.php | 7 -
8 files changed, 162 insertions(+), 190 deletions(-)
diff --git a/application/api/controller/References.php b/application/api/controller/References.php
index ea539ed6..3408dc06 100644
--- a/application/api/controller/References.php
+++ b/application/api/controller/References.php
@@ -1312,9 +1312,9 @@ class References extends Base
return json_encode(['status' => 8,'msg' => 'fail']);
}
// ============================================================
- // 参考文献「主题相关性」校对(独立模块,RabbitMQ 链式消费)
+ // 参考文献「主题相关性」校对(RabbitMQ 链式消费,复用 reference_check 队列)
// 表:t_article_reference_relevance_check_result / t_article_reference_relevance_check_batch
- // 消费:php think reference_relevance:mq-consume
+ // 消费:php think reference_check:mq-consume
// ============================================================
/**
@@ -1347,7 +1347,7 @@ class References extends Base
->where('p_article_id', $iPArticleId)
->count();
if (intval($existing) > 0) {
- return jsonError('This article already has relevance check records. Use referenceRelevanceCheckResetAI to rerun.');
+ return jsonError('This article already has relevance check records.');
}
try {
@@ -1363,16 +1363,21 @@ class References extends Base
}
/**
- * 相关性校对进度
- * POST: p_article_id
+ * 相关性校对进度(同 referenceCheckProgressAI)
+ * POST/GET: p_article_id(必填)
*/
public function referenceRelevanceCheckProgressAI()
{
$aParam = $this->request->post();
+ if (empty($aParam)) {
+ $aParam = $this->request->param();
+ }
+
$iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
if ($iPArticleId <= 0) {
- return jsonError('p_article_id is required');
+ return json_encode(array('status' => 2, 'msg' => 'Please select an article'));
}
+
try {
$result = (new ReferenceRelevanceCheckService())->getProgressByPArticleId($iPArticleId);
return jsonSuccess($result);
@@ -1382,12 +1387,8 @@ class References extends Base
}
/**
- * 按 p_article_id 查整篇文章相关性校对总状态(用于前端按钮分流)
- *
+ * 按 p_article_id 查整篇文章相关性校对总状态(同 referenceCheckArticleStatusAI)
* POST/GET: p_article_id(必填)
- *
- * 返回 status:0=未校对 1=校对中 2=校对完成
- * 计数维度为参考文献(按 reference_no 分组),与 referenceRelevanceCheckProgressAI 一致。
*/
public function referenceRelevanceCheckArticleStatusAI()
{
@@ -1398,7 +1399,7 @@ class References extends Base
$iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
if ($iPArticleId <= 0) {
- return jsonError('p_article_id is required');
+ return json_encode(array('status' => 2, 'msg' => 'Please select an article'));
}
try {
@@ -1410,16 +1411,21 @@ class References extends Base
}
/**
- * 按 p_refer_id 查相关性校对明细
- * POST: p_refer_id
+ * 按 p_refer_id 查单条参考文献的相关性校对明细与进度(同 referenceCheckDetailsAI)
+ * POST/GET: p_refer_id(必填)
*/
public function referenceRelevanceCheckDetailsAI()
{
$aParam = $this->request->post();
+ if (empty($aParam)) {
+ $aParam = $this->request->param();
+ }
+
$iPReferId = empty($aParam['p_refer_id']) ? 0 : intval($aParam['p_refer_id']);
if ($iPReferId <= 0) {
- return jsonError('p_refer_id is required');
+ return json_encode(array('status' => 2, 'msg' => 'Please select a reference'));
}
+
try {
$result = (new ReferenceRelevanceCheckService())->getDetailsByPReferId($iPReferId);
return jsonSuccess($result);
@@ -1496,47 +1502,15 @@ class References extends Base
}
/**
- * 参考文献第一次校对(支撑力度)
- * @return \think\response\Json
+ * @deprecated 支撑力度校对已下线,请使用 allReferenceCheckAI(主题相关性校对)
*/
- public function allReferenceCheckAI2(){
- //获取参数
- $aParam = empty($aParam) ? $this->request->post() : $aParam;
-
- //必填值验证
- $iPArticleId = empty($aParam['p_article_id']) ? '' : $aParam['p_article_id'];
- if(empty($iPArticleId)){
- return json_encode(array('status' => 2,'msg' => 'Please select an article' ));
- }
- //查询文章(p_article_id 与 article_id 都要带,下游服务方法两者都用)
- $aWhere = ['p_article_id' => $iPArticleId,'state' => ['in',[0,2]]];
- $aProductionArticle = Db::name('production_article')->field('p_article_id,article_id')->where($aWhere)->find();
- if(empty($aProductionArticle)){
- return json_encode(array('status' => 3,'msg' => 'No articles found' ));
- }
- if($this->checkReferStatus($iPArticleId)==0){
- return jsonError('Please correct the reference content before running the check.');
- }
- //已存在校对记录则禁止重复执行第一次校对,提示走重置接口
- $iExisting = Db::name('article_reference_check_result')
- ->where('p_article_id', $iPArticleId)
- ->count();
- if(intval($iExisting) > 0){
- return jsonError('This article already has a reference check record. Please use the "Reset Check" endpoint to run the check again.');
- }
- try {
- $svc = new ReferenceCheckService();
- $result = $svc->enqueueByPArticle($aProductionArticle);
- if (empty($result['check_ids'])) {
- return jsonError('No reference citations were found in the article.');
- }
- return jsonSuccess($result);
- } catch (\Exception $e) {
- return jsonError($e->getMessage());
- }
+ public function allReferenceCheckAI2()
+ {
+ return jsonError('Support strength check is deprecated. Please use allReferenceCheckAI.');
}
+
/**
- * 文献校对重置:删除该文章已有的全部校对明细,并重新入队整篇校对
+ * 文献校对重置:删除该文章已有的全部相关性校对明细,并重新入队整篇校对
* POST/GET: article_id(必填)
* @url /api/Article/referenceCheckReset
*/
@@ -1564,7 +1538,7 @@ class References extends Base
return json_encode(array('status' => 4,'msg' => 'Unbound article' ));
}
try {
- $result = (new ReferenceCheckService())->resetAndRecheckByArticle($aProductionArticle);
+ $result = (new ReferenceRelevanceCheckService())->resetAndRecheckByArticle($aProductionArticle);
return jsonSuccess($result);
} catch (\Exception $e) {
return jsonError($e->getMessage());
@@ -1601,7 +1575,7 @@ class References extends Base
}
try {
- $deleted = (new ReferenceCheckService())->clearArticleChecksByPArticleId($iPArticleId);
+ $deleted = (new ReferenceRelevanceCheckService())->clearByPArticleId($iPArticleId);
return jsonSuccess([
'p_article_id' => $iPArticleId,
'deleted' => intval($deleted),
@@ -1612,7 +1586,7 @@ class References extends Base
}
/**
- * 按 p_article_id 查整篇引用校对进度(按 reference_no 分组聚合)
+ * 按 p_article_id 查整篇相关性校对进度(按 reference_no 分组聚合)
*
* POST/GET: p_article_id(必填)
*
@@ -1625,6 +1599,8 @@ class References extends Base
* records[i].status 与分组同一套数值含义(但 record 不会出现 1=校对中):
* 0 = 待校验 2 = 校对完成 3 = 校对失败
*
+ * records[i] 含 reason(中英双语)、reason_en、combined_reason(中英双语)、combined_reason_en
+ *
* summary 用字符串键:pending / checking / completed / failed
*/
public function referenceCheckProgressAI()
@@ -1639,7 +1615,7 @@ class References extends Base
return json_encode(array('status' => 2, 'msg' => 'Please select an article'));
}
try {
- $result = (new ReferenceCheckService())->getProgressByPArticleId($iPArticleId);
+ $result = (new ReferenceRelevanceCheckService())->getProgressByPArticleId($iPArticleId);
return jsonSuccess($result);
} catch (\Exception $e) {
return jsonError($e->getMessage());
@@ -1647,7 +1623,7 @@ class References extends Base
}
/**
- * 按 p_article_id 查整篇文章引用校对总状态(用于前端按钮分流)
+ * 按 p_article_id 查整篇文章相关性校对总状态(用于前端按钮分流)
*
* POST/GET: p_article_id(必填)
*
@@ -1681,7 +1657,7 @@ class References extends Base
}
try {
- $result = (new ReferenceCheckService())->getArticleProgressStatusByPArticleId($iPArticleId);
+ $result = (new ReferenceRelevanceCheckService())->getArticleProgressStatusByPArticleId($iPArticleId);
return jsonSuccess($result);
} catch (\Exception $e) {
return jsonError($e->getMessage());
@@ -1709,7 +1685,7 @@ class References extends Base
}
try {
- $result = (new ReferenceCheckService())->getArticleCheckQueuePositionByPArticleId($iPArticleId);
+ $result = (new ReferenceRelevanceCheckService())->getArticleCheckQueuePositionByPArticleId($iPArticleId);
return jsonSuccess($result);
} catch (\Exception $e) {
return jsonError($e->getMessage());
@@ -1717,7 +1693,7 @@ class References extends Base
}
/**
- * 某条参考文献下「校对失败」的明细重新校对(异步)
+ * 某条参考文献下「相关性校对失败」的明细重新校对(异步)
*
* POST/GET: p_refer_id(必填)
* p_article_id(可选)
@@ -1739,8 +1715,8 @@ class References extends Base
$iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
try {
- $result = (new ReferenceCheckService())->enqueueRecheckFailedByPReferId($iPReferId, $iPArticleId);
- return jsonSuccess([]);
+ $result = (new ReferenceRelevanceCheckService())->enqueueRecheckFailedByPReferId($iPReferId, $iPArticleId);
+ return jsonSuccess($result);
} catch (\Exception $e) {
return jsonError($e->getMessage());
}
@@ -1769,7 +1745,7 @@ class References extends Base
$iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
try {
- $result = (new ReferenceCheckService())->enqueueRecheckFailedByPReferIdWithGroup($iPReferId, $iPArticleId);
+ $result = (new ReferenceRelevanceCheckService())->enqueueRecheckFailedByPReferIdWithGroup($iPReferId, $iPArticleId);
return jsonSuccess($result);
} catch (\Exception $e) {
return jsonError($e->getMessage());
@@ -1777,13 +1753,15 @@ class References extends Base
}
/**
- * 按 p_refer_id 查单条参考文献的校对明细与进度
+ * 按 p_refer_id 查单条参考文献的相关性校对明细与进度
*
* POST/GET: p_refer_id(必填)
*
* 分组进度:progress_status(0待/1中/2完成/3失败)、pending、done、failed、pass、
* is_pass、progress_percent、last_updated_at
- * list 每项:check_id、am_id、status、confidence、reason、is_match、is_pass
+ * list 每项:check_id、am_id、status、is_relevant、relevance_level、relevance_role、
+ * relevance_score、reason(中英双语【中文】/【English】)、reason_en、
+ * combined_*、combined_reason_en、cite_group_refs、cite_check_mode、is_pass
*/
public function referenceCheckDetailsAI()
{
@@ -1798,7 +1776,7 @@ class References extends Base
}
try {
- $result = (new ReferenceCheckService())->getCheckDetailsByPReferId($iPReferId);
+ $result = (new ReferenceRelevanceCheckService())->getDetailsByPReferId($iPReferId);
return jsonSuccess($result);
} catch (\Exception $e) {
return jsonError($e->getMessage());
@@ -1817,33 +1795,12 @@ class References extends Base
* 返回:missing_p_refer_ids、matched_p_refer_ids、still_unmatched_p_refer_ids、
* queued、new_reference_nos、check_ids、queue
*/
+ /**
+ * @deprecated 支撑力度漏匹配补扫已下线,请清空后使用 allReferenceCheckAI 重新校对
+ */
public function referenceCheckRematchNewAI()
{
- $aParam = $this->request->post();
- if (empty($aParam)) {
- $aParam = $this->request->param();
- }
-
- $iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']);
- if ($iPArticleId <= 0) {
- return json_encode(array('status' => 2, 'msg' => 'Please select an article'));
- }
-
- $aWhere = ['p_article_id' => $iPArticleId, 'state' => ['in', [0, 2]]];
- $aProductionArticle = Db::name('production_article')->field('p_article_id,article_id')->where($aWhere)->find();
- if (empty($aProductionArticle)) {
- return json_encode(array('status' => 3, 'msg' => 'No articles found'));
- }
- if ($this->checkReferStatus($iPArticleId) == 0) {
- return jsonError('Please correct the reference content before running the check.');
- }
-
- try {
- $result = (new ReferenceCheckService())->enqueueNewlyMatchedByPArticle($aProductionArticle);
- return jsonSuccess($result);
- } catch (\Exception $e) {
- return jsonError($e->getMessage());
- }
+ return jsonError('Support strength rematch is deprecated. Please use referenceCheckResetAI or allReferenceCheckAI.');
}
public function checkReferStatus($p_article_id){
diff --git a/application/command.php b/application/command.php
index cbea1b3b..43892e98 100644
--- a/application/command.php
+++ b/application/command.php
@@ -11,5 +11,4 @@
return [
'app\\command\\ReferenceCheckMqConsume',
- 'app\\command\\ReferenceRelevanceMqConsume',
];
diff --git a/application/command/ReferenceCheckMqConsume.php b/application/command/ReferenceCheckMqConsume.php
index 4f2f22f7..6d0103e5 100644
--- a/application/command/ReferenceCheckMqConsume.php
+++ b/application/command/ReferenceCheckMqConsume.php
@@ -13,7 +13,7 @@ class ReferenceCheckMqConsume extends Command
protected function configure()
{
$this->setName('reference_check:mq-consume')
- ->setDescription('Consume RabbitMQ reference check article queue');
+ ->setDescription('Consume RabbitMQ reference relevance check article queue (ref_check.article)');
}
protected function execute(Input $input, Output $output)
diff --git a/application/common/ReferenceCheckService.php b/application/common/ReferenceCheckService.php
index 0cf20986..b9ea2586 100644
--- a/application/common/ReferenceCheckService.php
+++ b/application/common/ReferenceCheckService.php
@@ -5,7 +5,6 @@ namespace app\common;
use think\Db;
use think\Env;
use app\common\service\LLMService;
-use app\common\mq\ReferenceCheckMqPublisher;
/**
* 正文 <blue>[n]</blue> 引用与 t_production_article_refer(index+1=n)相关性校对。
@@ -4350,41 +4349,7 @@ class ReferenceCheckService
*/
public function startArticleCheckQueue(array $checkIds, $pArticleId = 0, $trigger = 'enqueue')
{
- $checkIds = array_values(array_filter(array_map('intval', $checkIds)));
- if (empty($checkIds)) {
- return [];
- }
-
- $pArticleId = intval($pArticleId);
- if ($pArticleId <= 0) {
- $firstRow = Db::name('article_reference_check_result')->where('id', $checkIds[0])->find();
- $pArticleId = empty($firstRow) ? 0 : intval($this->arrGet($firstRow, 'p_article_id', 0));
- }
- if ($pArticleId <= 0) {
- throw new \RuntimeException('p_article_id is required for reference check queue');
- }
-
- $now = date('Y-m-d H:i:s');
- $batchId = Db::name('article_reference_check_batch')->insertGetId([
- 'p_article_id' => $pArticleId,
- 'batch_status' => 0,
- 'total_count' => count($checkIds),
- 'done_count' => 0,
- 'failed_count' => 0,
- 'trigger' => (string)$trigger,
- 'created_at' => $now,
- 'updated_at' => $now,
- ]);
-
- $shouldPublish = !$this->hasEarlierWaitingBatch($batchId) && !$this->hasRunningReferenceCheckBatch();
- if ($shouldPublish) {
- (new ReferenceCheckMqPublisher())->publishArticleStart($pArticleId, intval($batchId), $trigger);
- $this->log('startArticleCheckQueue publish p_article_id=' . $pArticleId . ' batch_id=' . $batchId);
- } else {
- $this->log('startArticleCheckQueue queued batch_id=' . $batchId . ' p_article_id=' . $pArticleId);
- }
-
- return $checkIds;
+ throw new \RuntimeException('Support strength check queue is deprecated. Use ReferenceRelevanceCheckService.');
}
private function hasRunningReferenceCheckBatch()
diff --git a/application/common/mq/RabbitMqConfig.php b/application/common/mq/RabbitMqConfig.php
index 84e19c17..df30aa5e 100644
--- a/application/common/mq/RabbitMqConfig.php
+++ b/application/common/mq/RabbitMqConfig.php
@@ -21,10 +21,4 @@ class RabbitMqConfig
$rc = self::get('reference_check', []);
return is_array($rc) ? $rc : [];
}
-
- public static function referenceRelevance()
- {
- $rc = self::get('reference_relevance', []);
- return is_array($rc) ? $rc : [];
- }
}
diff --git a/application/common/mq/ReferenceCheckArticleWorker.php b/application/common/mq/ReferenceCheckArticleWorker.php
index 46f61026..5c2701db 100644
--- a/application/common/mq/ReferenceCheckArticleWorker.php
+++ b/application/common/mq/ReferenceCheckArticleWorker.php
@@ -4,10 +4,11 @@ namespace app\common\mq;
use think\Db;
use app\common\DbReconnectHelper;
-use app\common\ReferenceCheckService;
+use app\common\ReferenceRelevanceCheckService;
/**
- * RabbitMQ 消费:按文章串行,文章内 reference_no 升序逐条校对(含低分同步二轮)
+ * RabbitMQ 消费(队列 reference_check / ref_check.article):
+ * 全局文章串行,文章内 reference_no 升序链式逐条「主题相关性」校对。
*/
class ReferenceCheckArticleWorker
{
@@ -16,12 +17,12 @@ class ReferenceCheckArticleWorker
const BATCH_DONE = 2;
const BATCH_PARTIAL_FAILED = 3;
- /** @var ReferenceCheckService */
+ /** @var ReferenceRelevanceCheckService */
private $svc;
public function __construct()
{
- $this->svc = new ReferenceCheckService();
+ $this->svc = new ReferenceRelevanceCheckService();
}
public function handleMessage(array $payload)
@@ -29,6 +30,7 @@ class ReferenceCheckArticleWorker
DbReconnectHelper::ensure();
$pArticleId = intval(isset($payload['p_article_id']) ? $payload['p_article_id'] : 0);
$batchId = intval(isset($payload['batch_id']) ? $payload['batch_id'] : 0);
+ $trigger = isset($payload['trigger']) ? (string)$payload['trigger'] : 'enqueue';
if ($pArticleId <= 0 || $batchId <= 0) {
$this->svc->log('ReferenceCheckArticleWorker invalid payload');
return;
@@ -36,7 +38,11 @@ class ReferenceCheckArticleWorker
if (!$this->canStartArticleWork($batchId)) {
$this->svc->log('ReferenceCheckArticleWorker defer batch_id=' . $batchId . ' other article running');
- (new ReferenceCheckMqPublisher())->publishArticleStart($pArticleId, $batchId, isset($payload['trigger']) ? $payload['trigger'] : 'enqueue');
+ (new ReferenceCheckMqPublisher())->publishArticleStart(
+ $pArticleId,
+ $batchId,
+ isset($payload['trigger']) ? $payload['trigger'] : 'enqueue'
+ );
sleep(3);
return;
}
@@ -48,6 +54,11 @@ class ReferenceCheckArticleWorker
}
}
+ $this->svc->recoverQueueRowsForArticle($pArticleId);
+ if ($trigger !== 'recheck_pending_only'
+ && ReferenceRelevanceCheckService::PREPARE_LITERATURE_BEFORE_CHECK) {
+ $this->svc->prepareLiteratureContentByArticle($pArticleId);
+ }
$this->svc->log('ReferenceCheckArticleWorker start p_article_id=' . $pArticleId . ' batch_id=' . $batchId);
$done = 0;
@@ -61,7 +72,7 @@ class ReferenceCheckArticleWorker
if ($checkId <= 0) {
continue;
}
- $result = $this->processOneRow($checkId, $row);
+ $result = $this->processOneRow($checkId, $row, $trigger === 'recheck_pending_only');
if ($result === 'ok') {
$done++;
} elseif ($result === 'failed') {
@@ -77,7 +88,7 @@ class ReferenceCheckArticleWorker
private function canStartArticleWork($batchId)
{
- $running = Db::name('article_reference_check_batch')
+ $running = Db::name('article_reference_relevance_check_batch')
->where('batch_status', self::BATCH_RUNNING)
->where('id', '<>', intval($batchId))
->count();
@@ -87,7 +98,7 @@ class ReferenceCheckArticleWorker
private function claimBatch($batchId)
{
$now = date('Y-m-d H:i:s');
- $affected = Db::name('article_reference_check_batch')
+ $affected = Db::name('article_reference_relevance_check_batch')
->where('id', intval($batchId))
->whereIn('batch_status', [self::BATCH_WAITING, self::BATCH_RUNNING])
->update([
@@ -99,15 +110,15 @@ class ReferenceCheckArticleWorker
private function getBatch($batchId)
{
- return Db::name('article_reference_check_batch')->where('id', intval($batchId))->find();
+ return Db::name('article_reference_relevance_check_batch')->where('id', intval($batchId))->find();
}
private function fetchNextPendingRow($pArticleId)
{
- return Db::name('article_reference_check_result')
+ return Db::name('article_reference_relevance_check_result')
->where('p_article_id', intval($pArticleId))
- ->where('queue_status', ReferenceCheckService::QUEUE_PENDING)
- ->where('status', ReferenceCheckService::RECORD_PENDING)
+ ->where('queue_status', ReferenceRelevanceCheckService::QUEUE_PENDING)
+ ->where('status', ReferenceRelevanceCheckService::RECORD_PENDING)
->order('reference_no asc,am_id asc,text_start asc,id asc')
->find();
}
@@ -115,46 +126,44 @@ class ReferenceCheckArticleWorker
/**
* @return string ok|failed|skip
*/
- private function processOneRow($checkId, array $row)
+ private function processOneRow($checkId, array $row, $skipLiteratureFetch = false)
{
DbReconnectHelper::ensure();
- $claimed = Db::name('article_reference_check_result')
+ $claimed = Db::name('article_reference_relevance_check_result')
->where('id', intval($checkId))
- ->where('queue_status', ReferenceCheckService::QUEUE_PENDING)
- ->update(['queue_status' => ReferenceCheckService::QUEUE_RUNNING]);
+ ->where('queue_status', ReferenceRelevanceCheckService::QUEUE_PENDING)
+ ->update(['queue_status' => ReferenceRelevanceCheckService::QUEUE_RUNNING]);
if (intval($claimed) <= 0) {
return 'skip';
}
$retryCount = intval(isset($row['retry_count']) ? $row['retry_count'] : 0);
try {
- $this->svc->runReferenceCheckOnce($checkId);
- $amId = intval(isset($row['am_id']) ? $row['am_id'] : 0);
- if ($amId > 0) {
- $this->svc->syncAmRefCheckStatus($amId);
- }
- $this->svc->markQueueRuntime($checkId, ReferenceCheckService::QUEUE_COMPLETED, $retryCount);
+ $this->svc->runCheckOnce($checkId, $skipLiteratureFetch);
+ $this->svc->markQueueRuntime($checkId, ReferenceRelevanceCheckService::QUEUE_COMPLETED, $retryCount);
return 'ok';
} catch (\Exception $e) {
$this->svc->log('ReferenceCheckArticleWorker check_id=' . $checkId . ' err=' . $e->getMessage());
DbReconnectHelper::ensure();
- if ($retryCount < ReferenceCheckService::QUEUE_MAX_RETRY) {
- $this->svc->markQueueRuntime($checkId, ReferenceCheckService::QUEUE_PENDING, $retryCount + 1);
- return $this->processOneRow($checkId, array_merge($row, ['retry_count' => $retryCount + 1]));
+ if ($retryCount < ReferenceRelevanceCheckService::QUEUE_MAX_RETRY) {
+ $this->svc->markQueueRuntime($checkId, ReferenceRelevanceCheckService::QUEUE_PENDING, $retryCount + 1);
+ return $this->processOneRow($checkId, array_merge($row, ['retry_count' => $retryCount + 1]), $skipLiteratureFetch);
}
try {
- $this->svc->updateCheckResult($checkId, [
- 'status' => ReferenceCheckService::RECORD_FAILED,
- 'error_msg' => $e->getMessage(),
- ]);
- $this->svc->markQueueRuntime($checkId, ReferenceCheckService::QUEUE_FAILED, $retryCount);
+ $fresh = Db::name('article_reference_relevance_check_result')->where('id', intval($checkId))->find();
+ $groupRows = !empty($fresh) ? $this->svc->findCitationGroupRowsForWorker($fresh) : [];
+ if (!empty($groupRows)) {
+ $this->svc->failGroupWithQueue($groupRows, $e->getMessage(), $retryCount);
+ } else {
+ $this->svc->updateCheckResult($checkId, [
+ 'status' => ReferenceRelevanceCheckService::RECORD_FAILED,
+ 'error_msg' => $e->getMessage(),
+ ]);
+ $this->svc->markQueueRuntime($checkId, ReferenceRelevanceCheckService::QUEUE_FAILED, $retryCount);
+ }
} catch (\Exception $e2) {
\think\Log::error('ReferenceCheckArticleWorker markFailed: ' . $e2->getMessage());
}
- $amId = intval(isset($row['am_id']) ? $row['am_id'] : 0);
- if ($amId > 0) {
- $this->svc->syncAmRefCheckStatus($amId);
- }
return 'failed';
}
}
@@ -170,7 +179,7 @@ class ReferenceCheckArticleWorker
if ($failed > 0) {
$status = self::BATCH_PARTIAL_FAILED;
}
- Db::name('article_reference_check_batch')->where('id', intval($batchId))->update([
+ Db::name('article_reference_relevance_check_batch')->where('id', intval($batchId))->update([
'batch_status' => $status,
'done_count' => intval($done),
'failed_count' => intval($failed),
@@ -183,7 +192,7 @@ class ReferenceCheckArticleWorker
private function publishNextWaitingBatch()
{
- $next = Db::name('article_reference_check_batch')
+ $next = Db::name('article_reference_relevance_check_batch')
->where('batch_status', self::BATCH_WAITING)
->order('id asc')
->find();
@@ -197,8 +206,8 @@ class ReferenceCheckArticleWorker
isset($next['trigger']) ? $next['trigger'] : 'enqueue'
);
} catch (\Exception $e) {
- $this->svc->log('publishNextWaitingBatch failed: ' . $e->getMessage());
- \think\Log::error('publishNextWaitingBatch: ' . $e->getMessage());
+ $this->svc->log('ReferenceCheck publishNextWaitingBatch failed: ' . $e->getMessage());
+ \think\Log::error('ReferenceCheck publishNextWaitingBatch: ' . $e->getMessage());
}
}
}
diff --git a/application/common/service/ReferenceRelevanceLlmService.php b/application/common/service/ReferenceRelevanceLlmService.php
index f37fb7d1..975ead5f 100644
--- a/application/common/service/ReferenceRelevanceLlmService.php
+++ b/application/common/service/ReferenceRelevanceLlmService.php
@@ -171,7 +171,10 @@ class ReferenceRelevanceLlmService
- supplementary_relevance:部分相关、补充性
- minimal_relevance:仅边缘/背景沾边
- no_meaningful_relevance:与引用处核心表述基本无关
-- reason:中文,须写明:①文献类型与**核心研究对象** ②**本文自身证据**覆盖了哪些 claim / 哪些未覆盖(含点名通路、功能结局、具体列举项;讨论转引须标明) ③**若仅为同主题不同 claim 或主语层级不对须明确写出** ④为何此分值而非更高或更低
+- reason:中英双语结论,格式固定为两行:
+ 【中文】(中文结论,须写明:①文献类型与**核心研究对象** ②**本文自身证据**覆盖了哪些 claim / 哪些未覆盖 ③主语/claim 不匹配须明确写出 ④为何此分值)
+ 【English】(与中文对应的英文结论,语义一致)
+- reason_en:仅英文结论(与 reason 中【English】段相同,勿留空)
主语/层级不对 → 单条 **0.45**,不得因讨论提及相同通路给 0.78:
引用处 claim 为「化合物 X 经 PI3K/AKT 等机制 demonstrated…」,文献为其他植物提取物或计算预测、仅在讨论转引他人 X 机制 → 0.45,weakly_related,is_relevant=0。
@@ -190,9 +193,10 @@ class ReferenceRelevanceLlmService
- 多篇联合仍缺主语对齐、缺原句点名通路/结局、或主要靠讨论转引 → 联合分通常 **≤0.45~0.65**,不得因单篇讨论出现相同关键词给到 0.78+
- combined_is_relevant:combined_relevance_score>=0.65 为 1
- combined_relevance_level:与 combined 分数对应的等级
-- combined_reason:中文综合结论,说明各文献分工(如「文献2综述覆盖流行病学主 claim,文献1仅机制补充」)及最终分值理由
+- combined_reason:中英双语综合结论,格式同 reason(【中文】/【English】),说明各文献分工及最终分值理由
+- combined_reason_en:仅英文综合结论(与 combined_reason 中【English】段相同)
-单条引用时:combined_* 与单条一致,combined_reason 可写「」。
+单条引用时:combined_* 与单条一致;combined_reason / combined_reason_en 可与 reason / reason_en 相同。
==================================================
【四、评分与等级对照】
@@ -218,11 +222,13 @@ class ReferenceRelevanceLlmService
"relevance_score": 0.45,
"relevance_level": "weakly_related",
"relevance_role": "minimal_relevance",
- "reason": "中文单条结论",
+ "reason": "【中文】中文单条结论\n【English】English single-reference conclusion",
+ "reason_en": "English single-reference conclusion",
"combined_is_relevant": 1,
"combined_relevance_score": 0.92,
"combined_relevance_level": "highly_related",
- "combined_reason": "中文联合结论"
+ "combined_reason": "【中文】中文联合结论\n【English】English combined conclusion",
+ "combined_reason_en": "English combined conclusion"
},
{
"reference_no": 2,
@@ -248,7 +254,7 @@ PROMPT;
if ($abstractText !== '') {
$parts[] = "【文献摘要/清洗后内容(Europe PMC·PubMed·Crossref·PDF)】\n" . $abstractText;
}
- $parts[] = '请先拆解最小主张单元(主语层级、证据来源、点名通路/结局逐项核对),判断每篇文献类型与**本文自身证据**,再**逐篇独立**给出单条 relevance_score(讨论转引、提取物/计算预测不得抬高;弱相关文献不得因联合而高分),最后给出 combined_*。仅输出 results 数组 JSON。';
+ $parts[] = '请先拆解最小主张单元(主语层级、证据来源、点名通路/结局逐项核对),判断每篇文献类型与**本文自身证据**,再**逐篇独立**给出单条 relevance_score(讨论转引、提取物/计算预测不得抬高;弱相关文献不得因联合而高分),最后给出 combined_*。reason / combined_reason 必须中英双语(【中文】/【English】),并分别填写 reason_en / combined_reason_en。仅输出 results 数组 JSON。';
return implode("\n\n", $parts);
}
@@ -285,7 +291,10 @@ PROMPT;
$level = $this->levelFromScore($score, isset($item['relevance_level']) ? $item['relevance_level'] : '');
$role = $this->normalizeRelevanceRole(isset($item['relevance_role']) ? $item['relevance_role'] : '');
- $reason = $this->cleanReason(isset($item['reason']) ? $item['reason'] : '');
+ list($reason, $reasonEn) = $this->normalizeBilingualReason(
+ isset($item['reason']) ? $item['reason'] : '',
+ isset($item['reason_en']) ? $item['reason_en'] : ''
+ );
list($score, $level, $isRelevant, $role) = $this->enforceSingleReferenceConsistency(
$score,
@@ -320,6 +329,14 @@ PROMPT;
$citeGroupRefs = trim((string)$defaultCiteGroupRefs);
}
+ list($combinedReason, $combinedReasonEn) = $this->normalizeBilingualReason(
+ isset($item['combined_reason']) ? $item['combined_reason'] : '',
+ isset($item['combined_reason_en']) ? $item['combined_reason_en'] : ''
+ );
+ if ($combinedReason === '' && $combinedReasonEn === '') {
+ list($combinedReason, $combinedReasonEn) = [$reason, $reasonEn];
+ }
+
$out[] = [
'reference_no' => $refNo,
'cite_group_refs' => $citeGroupRefs,
@@ -328,10 +345,12 @@ PROMPT;
'relevance_level' => $level,
'relevance_role' => $role,
'reason' => $reason,
+ 'reason_en' => $reasonEn,
'combined_is_relevant' => $combinedRelevant ? 1 : 0,
'combined_relevance_score' => $combinedScore,
'combined_relevance_level' => $combinedLevel,
- 'combined_reason' => $this->cleanReason(isset($item['combined_reason']) ? $item['combined_reason'] : ''),
+ 'combined_reason' => $combinedReason,
+ 'combined_reason_en' => $combinedReasonEn,
];
}
@@ -441,6 +460,7 @@ PROMPT;
$out[$idx]['combined_relevance_score'] = floatval($src['combined_relevance_score']);
$out[$idx]['combined_relevance_level'] = (string)$src['combined_relevance_level'];
$out[$idx]['combined_reason'] = (string)$src['combined_reason'];
+ $out[$idx]['combined_reason_en'] = (string)$src['combined_reason_en'];
}
}
@@ -524,10 +544,45 @@ PROMPT;
private function cleanReason($reason)
{
- $reason = trim(preg_replace('/\s+/u', ' ', (string)$reason));
+ $reason = trim(preg_replace('/[ \t]+/u', ' ', (string)$reason));
+ $reason = trim(preg_replace("/\n{3,}/u", "\n\n", $reason));
return mb_substr($reason, 0, 2000);
}
+ /**
+ * @return array{0:string,1:string} [bilingual reason, english only]
+ */
+ private function normalizeBilingualReason($reason, $reasonEn)
+ {
+ $reason = trim((string)$reason);
+ $reasonEn = $this->cleanReason($reasonEn);
+
+ if ($reasonEn === '' && preg_match('/【English】\s*(.+)$/us', $reason, $m)) {
+ $reasonEn = $this->cleanReason($m[1]);
+ }
+
+ $zh = '';
+ if (preg_match('/【中文】\s*(.*?)(?:\n【English】|$)/us', $reason, $m)) {
+ $zh = trim($m[1]);
+ } elseif ($reason !== '' && strpos($reason, '【English】') === false) {
+ $zh = trim($reason);
+ }
+
+ if ($zh !== '' && $reasonEn !== '' && strpos($reason, '【English】') === false) {
+ $reason = "【中文】{$zh}\n【English】{$reasonEn}";
+ } elseif ($zh !== '' && $reasonEn !== '' && strpos($reason, '【中文】') === false) {
+ $reason = "【中文】{$zh}\n【English】{$reasonEn}";
+ } else {
+ $reason = $this->cleanReason($reason);
+ }
+
+ if ($reasonEn === '' && $zh !== '') {
+ $reasonEn = '';
+ }
+
+ return [$reason, $reasonEn];
+ }
+
private function boolVal($v)
{
if (is_bool($v)) {
diff --git a/application/extra/rabbitmq.php b/application/extra/rabbitmq.php
index 4b41a649..05aa89b4 100644
--- a/application/extra/rabbitmq.php
+++ b/application/extra/rabbitmq.php
@@ -13,11 +13,4 @@ return [
'dlq' => 'ref_check.article.dlq',
'route_key' => 'article.start',
],
-
- 'reference_relevance' => [
- 'exchange' => 'reference_relevance',
- 'queue' => 'ref_relevance.article',
- 'dlq' => 'ref_relevance.article.dlq',
- 'route_key' => 'article.start',
- ],
];