|
@@ -2,7 +2,7 @@
|
|
|
|
|
|
|
|
不读取业务库,不决定哪张卡该不该发,不写自有库。
|
|
不读取业务库,不决定哪张卡该不该发,不写自有库。
|
|
|
一次调用可以拆成多条请求,单条失败是否重试交给调用方。
|
|
一次调用可以拆成多条请求,单条失败是否重试交给调用方。
|
|
|
-链路概况和明细卡是两种版式:概况只列各档归属;明细仍是逐节点的依据和代入。
|
|
|
|
|
|
|
+链路概况和明细卡是两种版式:概况只列各档归属;明细按摘要和五章写,正确列收成覆盖块。
|
|
|
"""
|
|
"""
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
from __future__ import annotations
|
|
@@ -25,6 +25,8 @@ import httpx
|
|
|
from loguru import logger
|
|
from loguru import logger
|
|
|
|
|
|
|
|
from post_clear_check.chain import (
|
|
from post_clear_check.chain import (
|
|
|
|
|
+ CHAIN_BUY,
|
|
|
|
|
+ CHAIN_SELL,
|
|
|
CHAINS,
|
|
CHAINS,
|
|
|
OWNER_BATCH,
|
|
OWNER_BATCH,
|
|
|
OWNER_FUND,
|
|
OWNER_FUND,
|
|
@@ -35,7 +37,7 @@ from post_clear_check.chain import (
|
|
|
from post_clear_check.config import Settings, push_log_suffix
|
|
from post_clear_check.config import Settings, push_log_suffix
|
|
|
from post_clear_check.order_decimal import VERDICT_CORRECT, VERDICT_ERROR, VERDICT_SUSPECT
|
|
from post_clear_check.order_decimal import VERDICT_CORRECT, VERDICT_ERROR, VERDICT_SUSPECT
|
|
|
|
|
|
|
|
-# 飞书卡片正文的字符上限。超出后只删正确行,错误和可疑全部保留。
|
|
|
|
|
|
|
+# 飞书卡片正文的字符上限。超出后只删正确章末尾的覆盖块,错误、可疑和无法展开保留。
|
|
|
BODY_LIMIT = 3500
|
|
BODY_LIMIT = 3500
|
|
|
# 卡片请求体官方上限是 30KB。留出余量,按 json.dumps 的默认编码计算。
|
|
# 卡片请求体官方上限是 30KB。留出余量,按 json.dumps 的默认编码计算。
|
|
|
PAYLOAD_BYTE_LIMIT = 28 * 1024
|
|
PAYLOAD_BYTE_LIMIT = 28 * 1024
|
|
@@ -129,6 +131,8 @@ class OutboundCard:
|
|
|
# 上次发送失败后再发时标题带「重发」。首次 pending 不加。
|
|
# 上次发送失败后再发时标题带「重发」。首次 pending 不加。
|
|
|
resend: bool = False
|
|
resend: bool = False
|
|
|
owners: tuple[SummaryOwner, ...] = ()
|
|
owners: tuple[SummaryOwner, ...] = ()
|
|
|
|
|
+ # 配了推送条数时,这条链路本次抽样的序号,从 1 起。没配时为空,正文不写抽样行。
|
|
|
|
|
+ sample_index: int | None = None
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
@dataclass
|
|
@@ -402,33 +406,424 @@ def _keep_summary_front(
|
|
|
left = 0
|
|
left = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
-def render_line(line: ConclusionView) -> str:
|
|
|
|
|
- """一个节点一段。依据、公式、代入式、说明、被核对的对象、期望结论各占一行,相邻行之间空一行。
|
|
|
|
|
|
|
+# 被核对对象末尾的归属。主订单编号收成「主订单」,避免和标题重复一整句。
|
|
|
|
|
+_OWNER_TAIL = re.compile(r"(([^)]+))\s*$")
|
|
|
|
|
+_BASIS_COLUMN = re.compile(r"「[^」]*」\s*$")
|
|
|
|
|
+_OWNER_LABELS = (
|
|
|
|
|
+ ("主订单编号 ", "主订单 "),
|
|
|
|
|
+ ("TRS用户账号 ", "用户账号 "),
|
|
|
|
|
+ ("用户账号 ", "用户账号 "),
|
|
|
|
|
+ ("资金账户编号 ", "资金账户 "),
|
|
|
|
|
+ ("资金账户 ", "资金账户 "),
|
|
|
|
|
+)
|
|
|
|
|
+# 正确行进「不适用」章的标记。只看公式和说明里已经写过的字,不改结论。
|
|
|
|
|
+_NA_MARKS = ("不出现", "不适用", "未触发")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@dataclass(frozen=True)
|
|
|
|
|
+class _CorrectGroup:
|
|
|
|
|
+ """同一报表、同一依据的正确列。超长时整块删,不拆开。"""
|
|
|
|
|
+
|
|
|
|
|
+ report: str
|
|
|
|
|
+ basis: str
|
|
|
|
|
+ lines: tuple[ConclusionView, ...]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _show_substitution(formula: str, subst: str) -> bool:
|
|
|
|
|
+ """代入式和公式右侧相同,或整段就是公式时,不再写一行代入。"""
|
|
|
|
|
+
|
|
|
|
|
+ if not subst or subst == formula:
|
|
|
|
|
+ return False
|
|
|
|
|
+ if "=" in formula and subst == formula.split("=", 1)[1].strip():
|
|
|
|
|
+ return False
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _keep_note(note: str) -> bool:
|
|
|
|
|
+ """同行等式已经由公式和报表标题表达。回写、口径差异这类说明留下。"""
|
|
|
|
|
+
|
|
|
|
|
+ compact = re.sub(r"\s+", "", note).rstrip("。")
|
|
|
|
|
+ return not compact.endswith("同行等式")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _check_head(check: str) -> tuple[str, str]:
|
|
|
|
|
+ """从被核对对象里取出报表名和归属。分不出报表名时只返回归属。"""
|
|
|
|
|
+
|
|
|
|
|
+ text = card_text(check).strip()
|
|
|
|
|
+ owner = ""
|
|
|
|
|
+ match = _OWNER_TAIL.search(text)
|
|
|
|
|
+ if match:
|
|
|
|
|
+ owner = _owner_line(match.group(1))
|
|
|
|
|
+ text = text[: match.start()].rstrip()
|
|
|
|
|
+ left = text
|
|
|
|
|
+ for separator in (" = ", " 缺失", " 无行", " 对不上"):
|
|
|
|
|
+ if separator in left:
|
|
|
|
|
+ left = left.split(separator, 1)[0]
|
|
|
|
|
+ break
|
|
|
|
|
+ if "." not in left:
|
|
|
|
|
+ return "", owner
|
|
|
|
|
+ report, _column = left.rsplit(".", 1)
|
|
|
|
|
+ return report.strip(), owner
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _owner_line(inner: str) -> str:
|
|
|
|
|
+ text = inner.strip()
|
|
|
|
|
+ for prefix, label in _OWNER_LABELS:
|
|
|
|
|
+ if text.startswith(prefix):
|
|
|
|
|
+ return label + text[len(prefix) :].strip()
|
|
|
|
|
+ return text
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _basis_doc(basis: str) -> str:
|
|
|
|
|
+ """去掉依据末尾的列名,同一份原型或同一行 Excel 只写一次。"""
|
|
|
|
|
+
|
|
|
|
|
+ return _BASIS_COLUMN.sub("", card_text(basis)).strip()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _line_parts(line: ConclusionView) -> tuple[str, str, tuple[str, ...]]:
|
|
|
|
|
+ """拆出公式、代入式,以及分号后面要留下的说明。"""
|
|
|
|
|
+
|
|
|
|
|
+ formula = card_text(line.formula_text).strip()
|
|
|
|
|
+ parts = [part.strip() for part in card_text(line.calc_text).split(";") if part.strip()]
|
|
|
|
|
+ subst = parts[0] if parts else ""
|
|
|
|
|
+ notes = tuple(part for part in parts[1:] if _keep_note(part))
|
|
|
|
|
+ return formula, subst, notes
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _is_not_applicable(line: ConclusionView) -> bool:
|
|
|
|
|
+ """公式或说明里已经写了不出现、不适用、未触发的正确行,进不适用章。"""
|
|
|
|
|
+
|
|
|
|
|
+ if line.verdict != VERDICT_CORRECT:
|
|
|
|
|
+ return False
|
|
|
|
|
+ formula, _subst, notes = _line_parts(line)
|
|
|
|
|
+ blob = formula + "\n" + "\n".join(notes)
|
|
|
|
|
+ return any(mark in blob for mark in _NA_MARKS)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _place(line: ConclusionView) -> tuple[str, str, str]:
|
|
|
|
|
+ """报表名、归属、去掉列名后的依据。筛选和正确覆盖块都用这三项。"""
|
|
|
|
|
+
|
|
|
|
|
+ report, owner = _check_head(line.check_text)
|
|
|
|
|
+ return report, owner, _basis_doc(line.basis)
|
|
|
|
|
|
|
|
- 飞书 lark_md 把单个换行显示成空格,这段若只用一个换行会从依据一直挤到期望结论。
|
|
|
|
|
- """
|
|
|
|
|
|
|
+
|
|
|
|
|
+def _scope_rows(chain_name: str, report: str, owners: Sequence[str]) -> list[str]:
|
|
|
|
|
+ """报表名和归属。没有报表名时用链路名称,避免筛选空着。"""
|
|
|
|
|
+
|
|
|
|
|
+ rows: list[str] = []
|
|
|
|
|
+ if report:
|
|
|
|
|
+ rows.append(f"报表:{report}")
|
|
|
|
|
+ scope: list[str] = []
|
|
|
|
|
+ if not report:
|
|
|
|
|
+ chain = CHAINS.get(chain_name)
|
|
|
|
|
+ if chain is None:
|
|
|
|
|
+ raise FeishuError("卡片链路不在登记表之内")
|
|
|
|
|
+ scope.append(chain.name)
|
|
|
|
|
+ seen: list[str] = []
|
|
|
|
|
+ for owner in owners:
|
|
|
|
|
+ if owner and owner not in seen:
|
|
|
|
|
+ seen.append(owner)
|
|
|
|
|
+ if seen:
|
|
|
|
|
+ scope.append("、".join(seen))
|
|
|
|
|
+ if scope:
|
|
|
|
|
+ rows.append("筛选:" + " · ".join(scope))
|
|
|
|
|
+ return rows
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _expect_label(line: ConclusionView, formula: str) -> str:
|
|
|
|
|
+ """期望行:有公式写公式。期望值算不出的可疑写无法计算。"""
|
|
|
|
|
+
|
|
|
|
|
+ if line.calc_value is None and line.verdict == VERDICT_SUSPECT:
|
|
|
|
|
+ return "期望值无法计算"
|
|
|
|
|
+ if formula:
|
|
|
|
|
+ return formula
|
|
|
|
|
+ expected = _value_text(line.calc_value)
|
|
|
|
|
+ if expected is None:
|
|
|
|
|
+ return "期望与当前一致"
|
|
|
|
|
+ return f"期望 {expected}"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _actual_text(line: ConclusionView, formula: str, subst: str) -> str:
|
|
|
|
|
+ """实据:代入式和当前值。代入式与公式重复时不写。"""
|
|
|
|
|
+
|
|
|
|
|
+ current = _value_text(line.check_value)
|
|
|
|
|
+ current_text = "当前缺失" if current is None else f"当前 {current}"
|
|
|
|
|
+ if subst and _show_substitution(formula, subst):
|
|
|
|
|
+ return f"{subst}。{current_text}"
|
|
|
|
|
+ if line.calc_value is None:
|
|
|
|
|
+ return current_text
|
|
|
|
|
+ expected = _value_text(line.calc_value)
|
|
|
|
|
+ if expected is None:
|
|
|
|
|
+ return current_text
|
|
|
|
|
+ return f"期望 {expected},{current_text}"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _conclusion(line: ConclusionView, notes: Sequence[str]) -> str:
|
|
|
|
|
+ """结论句接上说明和原来的期望句。说明不再单独成行。"""
|
|
|
|
|
+
|
|
|
|
|
+ bits = [VERDICT_LABEL[line.verdict]]
|
|
|
|
|
+ for note in notes:
|
|
|
|
|
+ cleaned = note.strip().rstrip("。")
|
|
|
|
|
+ if cleaned:
|
|
|
|
|
+ bits.append(cleaned)
|
|
|
|
|
+ tail = expectation(line).strip().rstrip("。")
|
|
|
|
|
+ if tail:
|
|
|
|
|
+ bits.append(tail)
|
|
|
|
|
+ return "结论:" + "。".join(bits) + "。"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _detail_block(line: ConclusionView, chain_name: str) -> str:
|
|
|
|
|
+ """错误、可疑、无法展开、不适用:一行六句。相邻句之间空一行。"""
|
|
|
|
|
|
|
|
if line.verdict not in VERDICT_LABEL:
|
|
if line.verdict not in VERDICT_LABEL:
|
|
|
raise FeishuError("结论不在正确、错误、可疑之内")
|
|
raise FeishuError("结论不在正确、错误、可疑之内")
|
|
|
|
|
+ formula, subst, notes = _line_parts(line)
|
|
|
|
|
+ report, owner, basis = _place(line)
|
|
|
|
|
+ rows = [f"节点:{formula or '核对'}"]
|
|
|
|
|
+ rows.extend(_scope_rows(chain_name, report, (owner,) if owner else ()))
|
|
|
|
|
+ rows.append(f"期望:{_expect_label(line, formula)}")
|
|
|
|
|
+ rows.append(f"实据:{_actual_text(line, formula, subst)}")
|
|
|
|
|
+ if basis:
|
|
|
|
|
+ rows.append(f"依据:{basis}")
|
|
|
|
|
+ rows.append(_conclusion(line, notes))
|
|
|
|
|
+ return "\n\n".join(rows)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _correct_groups(lines: Sequence[ConclusionView]) -> tuple[_CorrectGroup, ...]:
|
|
|
|
|
+ """其余正确行按报表和依据归块。同一块在正文里只出现一次。"""
|
|
|
|
|
+
|
|
|
|
|
+ order: list[tuple[str, str]] = []
|
|
|
|
|
+ grouped: dict[tuple[str, str], list[ConclusionView]] = {}
|
|
|
|
|
+ for line in lines:
|
|
|
|
|
+ if line.verdict != VERDICT_CORRECT or _is_not_applicable(line):
|
|
|
|
|
+ continue
|
|
|
|
|
+ report, _owner, basis = _place(line)
|
|
|
|
|
+ key = (report, basis)
|
|
|
|
|
+ bucket = grouped.get(key)
|
|
|
|
|
+ if bucket is None:
|
|
|
|
|
+ order.append(key)
|
|
|
|
|
+ bucket = []
|
|
|
|
|
+ grouped[key] = bucket
|
|
|
|
|
+ bucket.append(line)
|
|
|
|
|
+ return tuple(_CorrectGroup(report, basis, tuple(grouped[(report, basis)])) for report, basis in order)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _correct_block(group: _CorrectGroup, chain_name: str) -> str:
|
|
|
|
|
+ """同一报表、同一依据的正确列收成一块,实据只写列数。"""
|
|
|
|
|
+
|
|
|
|
|
+ formulas: list[str] = []
|
|
|
|
|
+ owners: list[str] = []
|
|
|
|
|
+ notes: list[str] = []
|
|
|
|
|
+ only_subst = ""
|
|
|
|
|
+ only_formula = ""
|
|
|
|
|
+ for line in group.lines:
|
|
|
|
|
+ formula, subst, line_notes = _line_parts(line)
|
|
|
|
|
+ if formula and formula not in formulas:
|
|
|
|
|
+ formulas.append(formula)
|
|
|
|
|
+ _report, owner, _basis = _place(line)
|
|
|
|
|
+ if owner and owner not in owners:
|
|
|
|
|
+ owners.append(owner)
|
|
|
|
|
+ for note in line_notes:
|
|
|
|
|
+ if note not in notes:
|
|
|
|
|
+ notes.append(note)
|
|
|
|
|
+ if len(group.lines) == 1:
|
|
|
|
|
+ only_subst = subst
|
|
|
|
|
+ only_formula = formula
|
|
|
|
|
+ rows = _scope_rows(chain_name, group.report, owners)
|
|
|
|
|
+ rows.append("节点:" + ("、".join(formulas) if formulas else "核对"))
|
|
|
|
|
+ rows.append("期望:与节点公式一致" if len(formulas) <= 1 else "期望:各列与节点公式一致")
|
|
|
|
|
+ actual = f"{len(group.lines)} 列无差异"
|
|
|
|
|
+ # 只有一列时仍把代入式写进实据。多列不再逐列展开。
|
|
|
|
|
+ if len(group.lines) == 1 and only_subst and _show_substitution(only_formula, only_subst):
|
|
|
|
|
+ actual = f"{only_subst}。{actual}"
|
|
|
|
|
+ rows.append(f"实据:{actual}")
|
|
|
|
|
+ if group.basis:
|
|
|
|
|
+ rows.append(f"依据:{group.basis}")
|
|
|
|
|
+ bits = ["正确"]
|
|
|
|
|
+ for note in notes:
|
|
|
|
|
+ cleaned = note.strip().rstrip("。")
|
|
|
|
|
+ if cleaned:
|
|
|
|
|
+ bits.append(cleaned)
|
|
|
|
|
+ if len(bits) == 1:
|
|
|
|
|
+ rows.append("结论:正确")
|
|
|
|
|
+ else:
|
|
|
|
|
+ rows.append("结论:" + "。".join(bits) + "。")
|
|
|
|
|
+ return "\n\n".join(rows)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _chapter(title: str, blocks: Sequence[str]) -> str:
|
|
|
|
|
+ """一章。没有行时写「无」,章标题和内容之间空一行。"""
|
|
|
|
|
+
|
|
|
|
|
+ if not blocks:
|
|
|
|
|
+ return f"{title}\n\n无"
|
|
|
|
|
+ return "\n\n".join((title, *blocks))
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _summary_text(*, correct: int, error: int, suspect: int, omitted: int, error_theme: bool) -> str:
|
|
|
|
|
+ """摘要只数这张正文里的行。删掉的正确列另写一句,错误主题说明其余留在核对明细。"""
|
|
|
|
|
+
|
|
|
|
|
+ text = f"本卡正确 {correct},错误 {error},可疑 {suspect}。"
|
|
|
|
|
+ if omitted:
|
|
|
|
|
+ text += f"正确另有 {omitted} 列未写入本卡。"
|
|
|
|
|
+ if error_theme:
|
|
|
|
|
+ text += "正确与可疑留在核对明细。"
|
|
|
|
|
+ return "摘要\n\n" + text
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# 买入执行、卖出执行按巡检的四章排,其它链路仍用摘要和清单。
|
|
|
|
|
+_ORDER_CHAINS = frozenset({CHAIN_BUY, CHAIN_SELL})
|
|
|
|
|
+_ORDER_CALC_SECTIONS = (
|
|
|
|
|
+ ("intraday", "盘中公式"),
|
|
|
|
|
+ ("clear", "清算对照"),
|
|
|
|
|
+ ("report", "关联报表"),
|
|
|
|
|
+)
|
|
|
|
|
+_ORDER_SEVERITY = {VERDICT_ERROR: 0, VERDICT_SUSPECT: 1, VERDICT_CORRECT: 2}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _order_part(line: ConclusionView) -> str:
|
|
|
|
|
+ """订单卡上的一行归到哪一章。只看公式和被核对对象里已有的字。"""
|
|
|
|
|
+
|
|
|
|
|
+ formula = card_text(line.formula_text)
|
|
|
|
|
+ report, _owner, _basis = _place(line)
|
|
|
|
|
+ if report or "报表" in formula or formula.startswith("OTC"):
|
|
|
|
|
+ return "report"
|
|
|
|
|
+ if "工作快照" in formula or "已发布结果" in formula or "当前投影" in formula:
|
|
|
|
|
+ return "clear"
|
|
|
|
|
+ if formula.startswith("执行事实") or "执行根" in formula:
|
|
|
|
|
+ return "link"
|
|
|
|
|
+ return "intraday"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _rank_lines(lines: Sequence[ConclusionView]) -> tuple[ConclusionView, ...]:
|
|
|
|
|
+ """错误在前,其次可疑,正确留在后面。同一档保持原来的顺序。"""
|
|
|
|
|
+
|
|
|
|
|
+ ranked = sorted(enumerate(lines), key=lambda item: (_ORDER_SEVERITY.get(item[1].verdict, 9), item[0]))
|
|
|
|
|
+ return tuple(line for _index, line in ranked)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _order_actual(line: ConclusionView) -> str:
|
|
|
|
|
+ """实据写被核对对象。对象里没有当前值时再补当前值。"""
|
|
|
|
|
+
|
|
|
|
|
+ check = card_text(line.check_text).strip()
|
|
|
|
|
+ current = _value_text(line.check_value)
|
|
|
|
|
+ if check and current is not None and current not in check:
|
|
|
|
|
+ return f"{check}。当前 {current}"
|
|
|
|
|
+ if check:
|
|
|
|
|
+ return check
|
|
|
|
|
+ if current is not None:
|
|
|
|
|
+ return f"当前 {current}"
|
|
|
|
|
+ return "当前缺失"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _order_expect(line: ConclusionView, formula: str, subst: str) -> str:
|
|
|
|
|
+ """期望写代入式。没有代入式时写公式右侧。"""
|
|
|
|
|
+
|
|
|
|
|
+ if line.calc_value is None and line.verdict == VERDICT_SUSPECT:
|
|
|
|
|
+ return "期望值无法计算"
|
|
|
|
|
+ if subst and _show_substitution(formula, subst):
|
|
|
|
|
+ return subst
|
|
|
|
|
+ if "=" in formula:
|
|
|
|
|
+ return formula.split("=", 1)[1].strip()
|
|
|
|
|
+ if formula:
|
|
|
|
|
+ return "与字段公式一致"
|
|
|
|
|
+ expected = _value_text(line.calc_value)
|
|
|
|
|
+ return expected or "与字段公式一致"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _order_conclusion(line: ConclusionView, notes: Sequence[str]) -> str:
|
|
|
|
|
+ """正确只写「正确」。错误和可疑带上原来的判定词,说明接在后面。"""
|
|
|
|
|
+
|
|
|
|
|
+ if line.verdict == VERDICT_ERROR:
|
|
|
|
|
+ tail = "错误。不符合预期"
|
|
|
|
|
+ elif line.verdict == VERDICT_SUSPECT and line.calc_value is None:
|
|
|
|
|
+ tail = "可疑。期望值无法计算"
|
|
|
|
|
+ elif line.verdict == VERDICT_SUSPECT:
|
|
|
|
|
+ tail = "可疑。无法判定"
|
|
|
|
|
+ else:
|
|
|
|
|
+ tail = "正确"
|
|
|
|
|
+ bits = [tail]
|
|
|
|
|
+ for note in notes:
|
|
|
|
|
+ cleaned = note.strip().rstrip("。")
|
|
|
|
|
+ if cleaned:
|
|
|
|
|
+ bits.append(cleaned)
|
|
|
|
|
+ text = "。".join(bits)
|
|
|
|
|
+ if not text.endswith("。"):
|
|
|
|
|
+ text += "。"
|
|
|
|
|
+ return f"结论:{text}"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _order_field_block(line: ConclusionView) -> str:
|
|
|
|
|
+ """一个计算:字段、期望、实据、依据、结论。相邻行空一行,飞书才不会粘成一句。"""
|
|
|
|
|
+
|
|
|
|
|
+ formula, subst, notes = _line_parts(line)
|
|
|
|
|
+ field = formula.split("=", 1)[0].strip() if "=" in formula else (formula or "核对")
|
|
|
rows = [
|
|
rows = [
|
|
|
- f"依据:{card_text(line.basis)}",
|
|
|
|
|
- card_text(line.formula_text),
|
|
|
|
|
- *_clause_rows(card_text(line.calc_text)),
|
|
|
|
|
- f"被核对的对象:{card_text(line.check_text)}",
|
|
|
|
|
- expectation(line),
|
|
|
|
|
|
|
+ f"字段:{field}",
|
|
|
|
|
+ f"期望:{_order_expect(line, formula, subst)}",
|
|
|
|
|
+ f"实据:{_order_actual(line)}",
|
|
|
]
|
|
]
|
|
|
- return "\n\n".join(row for row in rows if row.strip())
|
|
|
|
|
|
|
+ _report, _owner, basis = _place(line)
|
|
|
|
|
+ if basis:
|
|
|
|
|
+ rows.append(f"依据:{basis}")
|
|
|
|
|
+ rows.append(_order_conclusion(line, notes))
|
|
|
|
|
+ return "\n\n".join(rows)
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _clause_rows(text: str) -> tuple[str, ...]:
|
|
|
|
|
- """第一段是代入式,分号后面的每段是说明。两行都带前缀,拆开后不会只剩一个数字或一句裸说明。"""
|
|
|
|
|
|
|
+def _link_sentence(line: ConclusionView) -> str:
|
|
|
|
|
+ """链路结论里的一句。等号改成「等于」,读起来是判断而不是公式。"""
|
|
|
|
|
|
|
|
- parts = [part.strip() for part in text.split(";") if part.strip()]
|
|
|
|
|
- if not parts:
|
|
|
|
|
- return ()
|
|
|
|
|
- rows = [f"代入:{parts[0]}"]
|
|
|
|
|
- rows.extend(f"说明:{part}" for part in parts[1:])
|
|
|
|
|
- return tuple(rows)
|
|
|
|
|
|
|
+ formula = card_text(line.formula_text).strip().replace(" = ", "等于 ")
|
|
|
|
|
+ return formula.rstrip("。") or "链路对照一致"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _link_section(lines: Sequence[ConclusionView]) -> str:
|
|
|
|
|
+ """每单链路只给结论。都正确时收成一段;有问题的行才按字段展开。"""
|
|
|
|
|
+
|
|
|
|
|
+ correct = [line for line in lines if line.verdict == VERDICT_CORRECT]
|
|
|
|
|
+ bad = [line for line in lines if line.verdict != VERDICT_CORRECT]
|
|
|
|
|
+ bits: list[str] = []
|
|
|
|
|
+ if bad:
|
|
|
|
|
+ label = "错误" if any(line.verdict == VERDICT_ERROR for line in bad) else "可疑"
|
|
|
|
|
+ sentences = [_link_sentence(line) for line in correct]
|
|
|
|
|
+ head = f"结论:{label}。"
|
|
|
|
|
+ if sentences:
|
|
|
|
|
+ head += "。".join(sentences) + "。"
|
|
|
|
|
+ bits.append(head)
|
|
|
|
|
+ bits.extend(_order_field_block(line) for line in _rank_lines(bad))
|
|
|
|
|
+ return "\n\n".join(bits)
|
|
|
|
|
+ sentences = [_link_sentence(line) for line in correct]
|
|
|
|
|
+ return "结论:正确。" + "。".join(sentences) + "。"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _order_chapters(
|
|
|
|
|
+ lines: Sequence[ConclusionView],
|
|
|
|
|
+ *,
|
|
|
|
|
+ error_theme: bool,
|
|
|
|
|
+ omitted: int,
|
|
|
|
|
+) -> list[str]:
|
|
|
|
|
+ """四章。空章不写。删掉的正确计算在最后补一句。"""
|
|
|
|
|
+
|
|
|
|
|
+ buckets: dict[str, list[ConclusionView]] = {"link": [], "intraday": [], "clear": [], "report": []}
|
|
|
|
|
+ for line in lines:
|
|
|
|
|
+ buckets[_order_part(line)].append(line)
|
|
|
|
|
+ pieces: list[str] = []
|
|
|
|
|
+ if error_theme:
|
|
|
|
|
+ pieces.append("正确与可疑留在核对明细。")
|
|
|
|
|
+ if buckets["link"]:
|
|
|
|
|
+ pieces.append("每单链路\n\n" + _link_section(buckets["link"]))
|
|
|
|
|
+ for key, title in _ORDER_CALC_SECTIONS:
|
|
|
|
|
+ blocks = [_order_field_block(line) for line in _rank_lines(buckets[key])]
|
|
|
|
|
+ if blocks:
|
|
|
|
|
+ pieces.append(title + "\n\n" + "\n\n".join(blocks))
|
|
|
|
|
+ if omitted:
|
|
|
|
|
+ pieces.append(f"另有 {omitted} 项未写入本卡。")
|
|
|
|
|
+ return pieces
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _order_calc_lines(lines: Sequence[ConclusionView]) -> list[ConclusionView]:
|
|
|
|
|
+ """可以因超长删掉的计算行。每单链路不在这里,错误和可疑后面再跳过。"""
|
|
|
|
|
+
|
|
|
|
|
+ return [line for line in lines if _order_part(line) != "link"]
|
|
|
|
|
|
|
|
|
|
|
|
|
def expectation(line: ConclusionView) -> str:
|
|
def expectation(line: ConclusionView) -> str:
|
|
@@ -438,12 +833,12 @@ def expectation(line: ConclusionView) -> str:
|
|
|
current = _value_text(line.check_value)
|
|
current = _value_text(line.check_value)
|
|
|
if expected is None and current is None:
|
|
if expected is None and current is None:
|
|
|
if line.verdict == VERDICT_SUSPECT:
|
|
if line.verdict == VERDICT_SUSPECT:
|
|
|
- return "期望值无法计算,当前值缺失。"
|
|
|
|
|
|
|
+ return "期望值无法计算,当前缺失。"
|
|
|
if line.verdict == VERDICT_CORRECT:
|
|
if line.verdict == VERDICT_CORRECT:
|
|
|
return f"期望与当前一致。{_MATCH}"
|
|
return f"期望与当前一致。{_MATCH}"
|
|
|
return f"期望与当前不一致。{_MISMATCH}"
|
|
return f"期望与当前不一致。{_MISMATCH}"
|
|
|
- left = "期望值无法计算" if expected is None else f"期望值是 {expected}"
|
|
|
|
|
- right = "当前值缺失" if current is None else f"当前值是 {current}"
|
|
|
|
|
|
|
+ left = "期望值无法计算" if expected is None else f"期望 {expected}"
|
|
|
|
|
+ right = "当前缺失" if current is None else f"当前 {current}"
|
|
|
if line.verdict == VERDICT_CORRECT:
|
|
if line.verdict == VERDICT_CORRECT:
|
|
|
tail = _MATCH
|
|
tail = _MATCH
|
|
|
elif line.verdict == VERDICT_ERROR:
|
|
elif line.verdict == VERDICT_ERROR:
|
|
@@ -462,40 +857,156 @@ def render_body(
|
|
|
lines: Sequence[ConclusionView],
|
|
lines: Sequence[ConclusionView],
|
|
|
record_no: str,
|
|
record_no: str,
|
|
|
extra: str = "",
|
|
extra: str = "",
|
|
|
|
|
+ sample_index: int | None = None,
|
|
|
|
|
+ theme: str | None = None,
|
|
|
) -> str:
|
|
) -> str:
|
|
|
- """营业日、交易日、批次号、涉及在前,其后每个节点一段。超长时从末尾删正确段,开头四行不删。"""
|
|
|
|
|
|
|
+ """开头四行之后按章写。超长时从正确章末尾删覆盖块,开头不删。
|
|
|
|
|
+
|
|
|
|
|
+ theme 是整张卡的总结论。错误主题只写错误清单。没传时,只有全部都是错误行才按错误主题写。
|
|
|
|
|
+ """
|
|
|
|
|
|
|
|
chain = CHAINS.get(chain_name)
|
|
chain = CHAINS.get(chain_name)
|
|
|
if chain is None:
|
|
if chain is None:
|
|
|
raise FeishuError("卡片链路不在登记表之内")
|
|
raise FeishuError("卡片链路不在登记表之内")
|
|
|
- header = (
|
|
|
|
|
- f"营业日:{_day_text(business_date)}\n"
|
|
|
|
|
- f"交易日:{_day_text(trade_date)}\n"
|
|
|
|
|
- f"批次号:{_one_line(batch_no, '批次号')}\n"
|
|
|
|
|
- f"涉及:{chain.involved()}"
|
|
|
|
|
|
|
+ for line in lines:
|
|
|
|
|
+ if line.verdict not in VERDICT_LABEL:
|
|
|
|
|
+ raise FeishuError("结论不在正确、错误、可疑之内")
|
|
|
|
|
+ # 抽样行放在营业日之前。没配条数时不写,避免和全量推送的卡片混在一起。
|
|
|
|
|
+ opening = [f"抽样明细第 {sample_index} 条"] if sample_index is not None else []
|
|
|
|
|
+ header = "\n".join(
|
|
|
|
|
+ [
|
|
|
|
|
+ *opening,
|
|
|
|
|
+ f"营业日:{_day_text(business_date)}",
|
|
|
|
|
+ f"交易日:{_day_text(trade_date)}",
|
|
|
|
|
+ f"批次号:{_one_line(batch_no, '批次号')}",
|
|
|
|
|
+ f"涉及:{chain.involved()}",
|
|
|
|
|
+ ]
|
|
|
)
|
|
)
|
|
|
tail = card_text(extra).strip()
|
|
tail = card_text(extra).strip()
|
|
|
- blocks = [render_line(line) for line in lines]
|
|
|
|
|
- full = _join([header, *blocks, tail] if tail else [header, *blocks])
|
|
|
|
|
|
|
+ # 拆开的一张里可能只剩错误行。整张卡不是错误主题时,仍按五章写,不把其余行说成留在库里。
|
|
|
|
|
+ error_theme = theme == VERDICT_ERROR
|
|
|
|
|
+ if theme is None:
|
|
|
|
|
+ error_theme = bool(lines) and all(line.verdict == VERDICT_ERROR for line in lines)
|
|
|
|
|
+
|
|
|
|
|
+ if chain_name in _ORDER_CHAINS:
|
|
|
|
|
+ return _render_order_body(
|
|
|
|
|
+ header=header,
|
|
|
|
|
+ tail=tail,
|
|
|
|
|
+ lines=lines,
|
|
|
|
|
+ record_no=record_no,
|
|
|
|
|
+ error_theme=error_theme,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ def finish(pieces: list[str], record: str) -> str:
|
|
|
|
|
+ if record:
|
|
|
|
|
+ pieces.append(record)
|
|
|
|
|
+ if tail:
|
|
|
|
|
+ pieces.append(tail)
|
|
|
|
|
+ return _join(pieces)
|
|
|
|
|
+
|
|
|
|
|
+ if error_theme:
|
|
|
|
|
+ shown = [line for line in lines if line.verdict == VERDICT_ERROR]
|
|
|
|
|
+
|
|
|
|
|
+ def compose_error(record: str) -> str:
|
|
|
|
|
+ pieces = [
|
|
|
|
|
+ header,
|
|
|
|
|
+ _summary_text(correct=0, error=len(shown), suspect=0, omitted=0, error_theme=True),
|
|
|
|
|
+ _chapter("错误清单", [_detail_block(line, chain_name) for line in shown]),
|
|
|
|
|
+ ]
|
|
|
|
|
+ return finish(pieces, record)
|
|
|
|
|
+
|
|
|
|
|
+ full = compose_error("")
|
|
|
|
|
+ if len(full) <= BODY_LIMIT:
|
|
|
|
|
+ return full
|
|
|
|
|
+ # 拆开发送时记录编号写在 extra 里,这里不再加第二行。错误行不删。
|
|
|
|
|
+ record = "" if tail else f"记录编号:{_one_line(record_no, '记录编号')}"
|
|
|
|
|
+ return compose_error(record)
|
|
|
|
|
+
|
|
|
|
|
+ errors = [line for line in lines if line.verdict == VERDICT_ERROR]
|
|
|
|
|
+ suspects = [line for line in lines if line.verdict == VERDICT_SUSPECT and line.calc_value is not None]
|
|
|
|
|
+ blocked = [line for line in lines if line.verdict == VERDICT_SUSPECT and line.calc_value is None]
|
|
|
|
|
+ na_lines = [line for line in lines if _is_not_applicable(line)]
|
|
|
|
|
+ groups = list(_correct_groups(lines))
|
|
|
|
|
+
|
|
|
|
|
+ def compose_full(kept: Sequence[_CorrectGroup], omitted: int, record: str) -> str:
|
|
|
|
|
+ correct = len(na_lines) + sum(len(group.lines) for group in kept)
|
|
|
|
|
+ pieces = [
|
|
|
|
|
+ header,
|
|
|
|
|
+ _summary_text(
|
|
|
|
|
+ correct=correct,
|
|
|
|
|
+ error=len(errors),
|
|
|
|
|
+ suspect=len(suspects) + len(blocked),
|
|
|
|
|
+ omitted=omitted,
|
|
|
|
|
+ error_theme=False,
|
|
|
|
|
+ ),
|
|
|
|
|
+ _chapter("错误清单", [_detail_block(line, chain_name) for line in errors]),
|
|
|
|
|
+ _chapter("可疑清单", [_detail_block(line, chain_name) for line in suspects]),
|
|
|
|
|
+ _chapter("无法展开", [_detail_block(line, chain_name) for line in blocked]),
|
|
|
|
|
+ _chapter("正确", [_correct_block(group, chain_name) for group in kept]),
|
|
|
|
|
+ _chapter("不适用", [_detail_block(line, chain_name) for line in na_lines]),
|
|
|
|
|
+ ]
|
|
|
|
|
+ return finish(pieces, record)
|
|
|
|
|
+
|
|
|
|
|
+ full = compose_full(groups, 0, "")
|
|
|
if len(full) <= BODY_LIMIT:
|
|
if len(full) <= BODY_LIMIT:
|
|
|
return full
|
|
return full
|
|
|
- # 拆开发送时记录编号写在 extra 里,这里不再加第二行。
|
|
|
|
|
record = "" if tail else f"记录编号:{_one_line(record_no, '记录编号')}"
|
|
record = "" if tail else f"记录编号:{_one_line(record_no, '记录编号')}"
|
|
|
- kept = list(range(len(lines)))
|
|
|
|
|
|
|
+ omitted = 0
|
|
|
while True:
|
|
while True:
|
|
|
- pieces = [header, *[blocks[index] for index in kept]]
|
|
|
|
|
|
|
+ current = compose_full(groups, omitted, record)
|
|
|
|
|
+ if len(current) <= BODY_LIMIT or not groups:
|
|
|
|
|
+ return current
|
|
|
|
|
+ # 从正确章末尾整块删。不适用、错误、可疑、无法展开留下。
|
|
|
|
|
+ dropped = groups.pop()
|
|
|
|
|
+ omitted += len(dropped.lines)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _render_order_body(
|
|
|
|
|
+ *,
|
|
|
|
|
+ header: str,
|
|
|
|
|
+ tail: str,
|
|
|
|
|
+ lines: Sequence[ConclusionView],
|
|
|
|
|
+ record_no: str,
|
|
|
|
|
+ error_theme: bool,
|
|
|
|
|
+) -> str:
|
|
|
|
|
+ """买入、卖出明细:每单链路一句结论,其余三章按字段列出。超长只删正确计算。"""
|
|
|
|
|
+
|
|
|
|
|
+ def finish(pieces: list[str], record: str) -> str:
|
|
|
if record:
|
|
if record:
|
|
|
pieces.append(record)
|
|
pieces.append(record)
|
|
|
if tail:
|
|
if tail:
|
|
|
pieces.append(tail)
|
|
pieces.append(tail)
|
|
|
- current = _join(pieces)
|
|
|
|
|
|
|
+ return _join(pieces)
|
|
|
|
|
+
|
|
|
|
|
+ kept = list(lines)
|
|
|
|
|
+
|
|
|
|
|
+ def compose(omitted: int, record: str) -> str:
|
|
|
|
|
+ return finish([header, *_order_chapters(kept, error_theme=error_theme, omitted=omitted)], record)
|
|
|
|
|
+
|
|
|
|
|
+ full = compose(0, "")
|
|
|
|
|
+ if len(full) <= BODY_LIMIT:
|
|
|
|
|
+ return full
|
|
|
|
|
+ record = "" if tail else f"记录编号:{_one_line(record_no, '记录编号')}"
|
|
|
|
|
+ omitted = 0
|
|
|
|
|
+ while True:
|
|
|
|
|
+ current = compose(omitted, record)
|
|
|
if len(current) <= BODY_LIMIT:
|
|
if len(current) <= BODY_LIMIT:
|
|
|
return current
|
|
return current
|
|
|
- drop_at = _last_correct(lines, kept)
|
|
|
|
|
|
|
+ drop_at = _last_order_correct(kept)
|
|
|
if drop_at is None:
|
|
if drop_at is None:
|
|
|
- # 错误和可疑已经全部保留,不能再删。
|
|
|
|
|
return current
|
|
return current
|
|
|
del kept[drop_at]
|
|
del kept[drop_at]
|
|
|
|
|
+ omitted += 1
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _last_order_correct(lines: list[ConclusionView]) -> int | None:
|
|
|
|
|
+ """计算章里最后一条正确行。每单链路和错误、可疑留下。"""
|
|
|
|
|
+
|
|
|
|
|
+ for index in range(len(lines) - 1, -1, -1):
|
|
|
|
|
+ line = lines[index]
|
|
|
|
|
+ if line.verdict == VERDICT_CORRECT and _order_part(line) != "link":
|
|
|
|
|
+ return index
|
|
|
|
|
+ return None
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_payload(
|
|
def build_payload(
|
|
@@ -560,7 +1071,7 @@ def card_verdict(lines: Sequence[ConclusionView]) -> str:
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
@dataclass(frozen=True)
|
|
|
class _PlainLine:
|
|
class _PlainLine:
|
|
|
- """截短后仍能交给 render_line 的结论行。数值不截,期望那一行照常写期望值与当前值。"""
|
|
|
|
|
|
|
+ """截短后仍能交给正文渲染的结论行。数值不截,期望句照常写期望值与当前值。"""
|
|
|
|
|
|
|
|
formula_text: str
|
|
formula_text: str
|
|
|
calc_text: str
|
|
calc_text: str
|
|
@@ -583,6 +1094,7 @@ def pack_bodies(
|
|
|
lines: Sequence[ConclusionView],
|
|
lines: Sequence[ConclusionView],
|
|
|
signed: bool,
|
|
signed: bool,
|
|
|
limit: int = PAYLOAD_BYTE_LIMIT,
|
|
limit: int = PAYLOAD_BYTE_LIMIT,
|
|
|
|
|
+ sample_index: int | None = None,
|
|
|
) -> tuple[str, ...]:
|
|
) -> tuple[str, ...]:
|
|
|
"""按真实请求体字节数装箱。错误主题只装错误行;放不下就拆成多张,单行仍超限再截短。"""
|
|
"""按真实请求体字节数装箱。错误主题只装错误行;放不下就拆成多张,单行仍超限再截短。"""
|
|
|
|
|
|
|
@@ -599,6 +1111,7 @@ def pack_bodies(
|
|
|
"record_no": record_no,
|
|
"record_no": record_no,
|
|
|
"signed": signed,
|
|
"signed": signed,
|
|
|
"limit": limit,
|
|
"limit": limit,
|
|
|
|
|
+ "sample_index": sample_index,
|
|
|
}
|
|
}
|
|
|
if _fits(usable, extra="", **common):
|
|
if _fits(usable, extra="", **common):
|
|
|
return (
|
|
return (
|
|
@@ -609,6 +1122,8 @@ def pack_bodies(
|
|
|
chain_name=chain_name,
|
|
chain_name=chain_name,
|
|
|
lines=usable,
|
|
lines=usable,
|
|
|
record_no=record_no,
|
|
record_no=record_no,
|
|
|
|
|
+ sample_index=sample_index,
|
|
|
|
|
+ theme=verdict,
|
|
|
),
|
|
),
|
|
|
)
|
|
)
|
|
|
# 张数还不知道。先按三位数字留出「第999张,共999张」,真实页码更短。
|
|
# 张数还不知道。先按三位数字留出「第999张,共999张」,真实页码更短。
|
|
@@ -644,6 +1159,8 @@ def pack_bodies(
|
|
|
lines=group,
|
|
lines=group,
|
|
|
record_no=record_no,
|
|
record_no=record_no,
|
|
|
extra=extra,
|
|
extra=extra,
|
|
|
|
|
+ sample_index=sample_index,
|
|
|
|
|
+ theme=verdict,
|
|
|
)
|
|
)
|
|
|
)
|
|
)
|
|
|
return tuple(bodies)
|
|
return tuple(bodies)
|
|
@@ -662,6 +1179,7 @@ def _fits(
|
|
|
record_no: str,
|
|
record_no: str,
|
|
|
signed: bool,
|
|
signed: bool,
|
|
|
limit: int,
|
|
limit: int,
|
|
|
|
|
+ sample_index: int | None = None,
|
|
|
) -> bool:
|
|
) -> bool:
|
|
|
"""用和 httpx 相同的 JSON 编码量字节。"""
|
|
"""用和 httpx 相同的 JSON 编码量字节。"""
|
|
|
|
|
|
|
@@ -673,6 +1191,8 @@ def _fits(
|
|
|
lines=lines,
|
|
lines=lines,
|
|
|
record_no=record_no,
|
|
record_no=record_no,
|
|
|
extra=extra,
|
|
extra=extra,
|
|
|
|
|
+ sample_index=sample_index,
|
|
|
|
|
+ theme=verdict,
|
|
|
)
|
|
)
|
|
|
payload = build_payload(
|
|
payload = build_payload(
|
|
|
title=title,
|
|
title=title,
|
|
@@ -697,6 +1217,7 @@ def _clip_line(
|
|
|
record_no: str,
|
|
record_no: str,
|
|
|
signed: bool,
|
|
signed: bool,
|
|
|
limit: int,
|
|
limit: int,
|
|
|
|
|
+ sample_index: int | None = None,
|
|
|
) -> _PlainLine:
|
|
) -> _PlainLine:
|
|
|
"""单行超限时从长文本里砍到能装进一张卡。"""
|
|
"""单行超限时从长文本里砍到能装进一张卡。"""
|
|
|
|
|
|
|
@@ -730,6 +1251,7 @@ def _clip_line(
|
|
|
record_no=record_no,
|
|
record_no=record_no,
|
|
|
signed=signed,
|
|
signed=signed,
|
|
|
limit=limit,
|
|
limit=limit,
|
|
|
|
|
+ sample_index=sample_index,
|
|
|
):
|
|
):
|
|
|
best = values[name]
|
|
best = values[name]
|
|
|
lo = mid + 1
|
|
lo = mid + 1
|
|
@@ -748,6 +1270,7 @@ def _clip_line(
|
|
|
record_no=record_no,
|
|
record_no=record_no,
|
|
|
signed=signed,
|
|
signed=signed,
|
|
|
limit=limit,
|
|
limit=limit,
|
|
|
|
|
+ sample_index=sample_index,
|
|
|
):
|
|
):
|
|
|
return _PlainLine(**values)
|
|
return _PlainLine(**values)
|
|
|
return _PlainLine(
|
|
return _PlainLine(
|
|
@@ -791,6 +1314,7 @@ async def send_card(
|
|
|
record_no=card.record_no,
|
|
record_no=card.record_no,
|
|
|
lines=card.lines,
|
|
lines=card.lines,
|
|
|
signed=secret != "",
|
|
signed=secret != "",
|
|
|
|
|
+ sample_index=card.sample_index,
|
|
|
)
|
|
)
|
|
|
return await _deliver(
|
|
return await _deliver(
|
|
|
settings,
|
|
settings,
|
|
@@ -971,15 +1495,6 @@ def _business_code(raw: bytes) -> int | None:
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _last_correct(lines: Sequence[ConclusionView], kept: Sequence[int]) -> int | None:
|
|
|
|
|
- """kept 里最后一条正确行的下标。没有正确行就返回空。"""
|
|
|
|
|
-
|
|
|
|
|
- for position in range(len(kept) - 1, -1, -1):
|
|
|
|
|
- if lines[kept[position]].verdict == VERDICT_CORRECT:
|
|
|
|
|
- return position
|
|
|
|
|
- return None
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
def _join(parts: Sequence[str]) -> str:
|
|
def _join(parts: Sequence[str]) -> str:
|
|
|
return "\n\n".join(parts)
|
|
return "\n\n".join(parts)
|
|
|
|
|
|