加快登记预览并结构化补项
This commit is contained in:
+338
-19
@@ -10,8 +10,9 @@ import tomllib
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib import error, request
|
from urllib import error, request
|
||||||
|
|
||||||
from .config import AI_REGISTER_TIMEOUT_SECONDS, CODEX_AUTH_PATH, CODEX_CONFIG_PATH
|
from .config import ACCOUNTS_PATH, AI_REGISTER_MODEL, AI_REGISTER_TIMEOUT_SECONDS, CODEX_AUTH_PATH, CODEX_CONFIG_PATH
|
||||||
from .data import (
|
from .data import (
|
||||||
|
SUBJECTS,
|
||||||
class_record_to_line,
|
class_record_to_line,
|
||||||
course_summary_to_class_record_line,
|
course_summary_to_class_record_line,
|
||||||
extract_course_summary_from_text,
|
extract_course_summary_from_text,
|
||||||
@@ -19,10 +20,48 @@ from .data import (
|
|||||||
normalize_lines,
|
normalize_lines,
|
||||||
parse_class_record_line,
|
parse_class_record_line,
|
||||||
parse_payment_line,
|
parse_payment_line,
|
||||||
|
read_accounts,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
REGISTER_TYPES = {"class_record", "payment", "course_summary"}
|
REGISTER_TYPES = {"class_record", "payment", "course_summary"}
|
||||||
|
FIELD_LABELS = {
|
||||||
|
"class_record": {
|
||||||
|
"student": "学生",
|
||||||
|
"date": "日期",
|
||||||
|
"time": "时间",
|
||||||
|
"teacher": "老师",
|
||||||
|
"subject": "科目",
|
||||||
|
"duration": "时长",
|
||||||
|
},
|
||||||
|
"payment": {
|
||||||
|
"student": "学生",
|
||||||
|
"date": "缴费日期",
|
||||||
|
"hours": "课时数",
|
||||||
|
},
|
||||||
|
"course_summary": {
|
||||||
|
"student": "学生",
|
||||||
|
"date": "日期",
|
||||||
|
"time": "时间",
|
||||||
|
"teacher": "老师",
|
||||||
|
"subject": "科目",
|
||||||
|
"body": "小结正文",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
REQUIRED_FIELDS = {
|
||||||
|
"class_record": ["student", "date", "time", "teacher", "subject"],
|
||||||
|
"payment": ["student", "date", "hours"],
|
||||||
|
"course_summary": ["student", "date", "time", "teacher", "subject", "body"],
|
||||||
|
}
|
||||||
|
DATE_RE = re.compile(r"(?P<y>\d{4})[./年-]\s*(?P<m>\d{1,2})[./月-]\s*(?P<d>\d{1,2})")
|
||||||
|
MONTH_DAY_RE = re.compile(r"(?<!\d)(?P<m>\d{1,2})\s*[月./-]\s*(?P<d>\d{1,2})\s*(?:日|号)?")
|
||||||
|
TIME_RANGE_FLEX_RE = re.compile(
|
||||||
|
r"(?P<sh>\d{1,2})\s*(?:[::点.])\s*(?P<sm>\d{1,2})?\s*"
|
||||||
|
r"(?:-|-|—|–|~|至|到)\s*"
|
||||||
|
r"(?P<eh>\d{1,2})\s*(?:[::点.])\s*(?P<em>\d{1,2})?"
|
||||||
|
)
|
||||||
|
TEACHER_RE = re.compile(r"(?P<teacher>[\u4e00-\u9fa5A-Za-z0-9]{1,8}老师)")
|
||||||
|
HOURS_RE = re.compile(r"(?P<hours>\d+(?:\.\d+)?)\s*(?:课时|小时)")
|
||||||
MAX_SESSION_AGE_SECONDS = 30 * 60
|
MAX_SESSION_AGE_SECONDS = 30 * 60
|
||||||
MAX_SESSIONS = 100
|
MAX_SESSIONS = 100
|
||||||
_SESSIONS: dict[str, dict[str, Any]] = {}
|
_SESSIONS: dict[str, dict[str, Any]] = {}
|
||||||
@@ -113,7 +152,7 @@ def load_codex_ai_config(
|
|||||||
config = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
config = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
||||||
auth = json.loads(auth_path.read_text(encoding="utf-8"))
|
auth = json.loads(auth_path.read_text(encoding="utf-8"))
|
||||||
provider_name = str(config.get("model_provider") or "").strip()
|
provider_name = str(config.get("model_provider") or "").strip()
|
||||||
model = str(config.get("model") or "").strip()
|
model = AI_REGISTER_MODEL or str(config.get("model") or "").strip()
|
||||||
providers = config.get("model_providers") or {}
|
providers = config.get("model_providers") or {}
|
||||||
provider = providers.get(provider_name) or {}
|
provider = providers.get(provider_name) or {}
|
||||||
base_url = str(provider.get("base_url") or "").strip().rstrip("/")
|
base_url = str(provider.get("base_url") or "").strip().rstrip("/")
|
||||||
@@ -128,6 +167,195 @@ def load_codex_ai_config(
|
|||||||
return CodexAiConfig(provider_name, model, base_url, wire_api, api_key)
|
return CodexAiConfig(provider_name, model, base_url, wire_api, api_key)
|
||||||
|
|
||||||
|
|
||||||
|
def _field_labels(register_type: str) -> dict[str, str]:
|
||||||
|
return FIELD_LABELS.get(register_type, {})
|
||||||
|
|
||||||
|
|
||||||
|
def _questions_for_missing(register_type: str, missing_fields: list[str]) -> list[str]:
|
||||||
|
labels = _field_labels(register_type)
|
||||||
|
return [f"请补充{labels.get(field, field)}" for field in missing_fields]
|
||||||
|
|
||||||
|
|
||||||
|
def _known_students() -> list[str]:
|
||||||
|
try:
|
||||||
|
return [account.student for account in read_accounts(ACCOUNTS_PATH)]
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_date(value: object) -> str:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
match = DATE_RE.search(text)
|
||||||
|
if match:
|
||||||
|
year = int(match.group("y"))
|
||||||
|
month = int(match.group("m"))
|
||||||
|
day = int(match.group("d"))
|
||||||
|
datetime(year, month, day)
|
||||||
|
return f"{year:04d}-{month:02d}-{day:02d}"
|
||||||
|
match = MONTH_DAY_RE.search(text)
|
||||||
|
if match:
|
||||||
|
year = date.today().year
|
||||||
|
month = int(match.group("m"))
|
||||||
|
day = int(match.group("d"))
|
||||||
|
datetime(year, month, day)
|
||||||
|
return f"{year:04d}-{month:02d}-{day:02d}"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _record_date(value: str) -> str:
|
||||||
|
return value.replace("-", ".")
|
||||||
|
|
||||||
|
|
||||||
|
def _weekday(value: str) -> str:
|
||||||
|
weekdays = ["星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"]
|
||||||
|
return weekdays[datetime.strptime(value, "%Y-%m-%d").date().weekday()]
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_time_range(value: object) -> str:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
standard = re.search(r"\d{1,2}:\d{2}-\d{1,2}:\d{2}", text)
|
||||||
|
if standard:
|
||||||
|
raw = standard.group(0)
|
||||||
|
start, end = raw.split("-", 1)
|
||||||
|
sh, sm = [int(part) for part in start.split(":", 1)]
|
||||||
|
eh, em = [int(part) for part in end.split(":", 1)]
|
||||||
|
else:
|
||||||
|
match = TIME_RANGE_FLEX_RE.search(text)
|
||||||
|
if not match:
|
||||||
|
return ""
|
||||||
|
sh = int(match.group("sh"))
|
||||||
|
sm = int(match.group("sm") or 0)
|
||||||
|
eh = int(match.group("eh"))
|
||||||
|
em = int(match.group("em") or 0)
|
||||||
|
if sh > 23 or eh > 23 or sm > 59 or em > 59:
|
||||||
|
return ""
|
||||||
|
if eh * 60 + em <= sh * 60 + sm:
|
||||||
|
return ""
|
||||||
|
return f"{sh:02d}:{sm:02d}-{eh:02d}:{em:02d}"
|
||||||
|
|
||||||
|
|
||||||
|
def _duration_from_time_range(time_range: str) -> str:
|
||||||
|
start, end = time_range.split("-", 1)
|
||||||
|
sh, sm = [int(part) for part in start.split(":", 1)]
|
||||||
|
eh, em = [int(part) for part in end.split(":", 1)]
|
||||||
|
minutes = eh * 60 + em - sh * 60 - sm
|
||||||
|
return f"{minutes // 60}小时{minutes % 60}分"
|
||||||
|
|
||||||
|
|
||||||
|
def _duration_from_text(value: object) -> str:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
if re.fullmatch(r"\d+小时\d+分", text):
|
||||||
|
return text
|
||||||
|
match = re.search(r"(?P<h>\d+)\s*小时\s*(?P<m>\d+)?\s*分?", text)
|
||||||
|
if match:
|
||||||
|
return f"{int(match.group('h'))}小时{int(match.group('m') or 0)}分"
|
||||||
|
match = re.search(r"(?P<hours>\d+(?:\.\d+)?)\s*(?:课时|小时)", text)
|
||||||
|
if match:
|
||||||
|
total = int(round(float(match.group("hours")) * 60))
|
||||||
|
return f"{total // 60}小时{total % 60}分"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_student(text: str) -> str:
|
||||||
|
match = re.search(r"(?:学生|学员)[::\s]*(?P<student>[\u4e00-\u9fa5A-Za-z0-9]{2,8})", text)
|
||||||
|
if match:
|
||||||
|
return match.group("student")
|
||||||
|
for student in _known_students():
|
||||||
|
if student and student in text:
|
||||||
|
return student
|
||||||
|
match = re.search(r"^\s*(?P<student>[\u4e00-\u9fa5]{2,4})(?=\s|[,,。;;:]|\d)", text)
|
||||||
|
return match.group("student") if match else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_teacher(text: str) -> str:
|
||||||
|
match = TEACHER_RE.search(text)
|
||||||
|
return match.group("teacher") if match else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_subject(text: str) -> str:
|
||||||
|
for subject in SUBJECTS:
|
||||||
|
if subject in text:
|
||||||
|
return subject
|
||||||
|
match = re.search(r"(?:科目|课程)[::\s]*(?P<subject>[\u4e00-\u9fa5A-Za-z0-9]{1,12})", text)
|
||||||
|
return match.group("subject") if match else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_answers(fields: dict[str, str], answers: dict[str, str]) -> dict[str, str]:
|
||||||
|
merged = dict(fields)
|
||||||
|
for key, value in answers.items():
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
if key == "followup":
|
||||||
|
continue
|
||||||
|
merged[key] = text
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def _missing_fields(register_type: str, fields: dict[str, str]) -> list[str]:
|
||||||
|
return [field for field in REQUIRED_FIELDS[register_type] if not str(fields.get(field) or "").strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def _needs_info_response(
|
||||||
|
register_type: str,
|
||||||
|
fields: dict[str, str],
|
||||||
|
missing_fields: list[str],
|
||||||
|
*,
|
||||||
|
summary: str = "",
|
||||||
|
ai_used: bool = False,
|
||||||
|
timed_out: bool = False,
|
||||||
|
error: str = "",
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"status": "needs_info",
|
||||||
|
"standard_lines": [],
|
||||||
|
"questions": _questions_for_missing(register_type, missing_fields) or [_question_for_error(register_type, error or "信息不完整")],
|
||||||
|
"summary": summary or "请补齐缺失字段后再次生成预览",
|
||||||
|
"ai_used": ai_used,
|
||||||
|
"fields": fields,
|
||||||
|
"missing_fields": missing_fields,
|
||||||
|
"field_labels": _field_labels(register_type),
|
||||||
|
"timed_out": timed_out,
|
||||||
|
"recognition_source": "partial_local" if not ai_used else "model",
|
||||||
|
"error": error,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _standard_from_fields(register_type: str, fields: dict[str, str]) -> str:
|
||||||
|
if register_type == "class_record":
|
||||||
|
date_value = _normalize_date(fields.get("date"))
|
||||||
|
time_range = _normalize_time_range(fields.get("time"))
|
||||||
|
duration = _duration_from_text(fields.get("duration")) or _duration_from_time_range(time_range)
|
||||||
|
return (
|
||||||
|
f"{_record_date(date_value)}-{_weekday(date_value)}-{time_range}-"
|
||||||
|
f"{fields['student']}-{duration}-{fields['teacher']}-{fields['subject']}"
|
||||||
|
)
|
||||||
|
if register_type == "payment":
|
||||||
|
date_value = _normalize_date(fields.get("date"))
|
||||||
|
hours = str(fields.get("hours") or "").strip()
|
||||||
|
hours = re.sub(r"\s*(?:课时|小时)$", "", hours)
|
||||||
|
return f"{fields['student']}-{date_value}:{hours}"
|
||||||
|
date_value = _normalize_date(fields.get("date"))
|
||||||
|
time_range = _normalize_time_range(fields.get("time"))
|
||||||
|
return "\n".join(
|
||||||
|
[
|
||||||
|
f"学生:{fields['student']}",
|
||||||
|
f"日期:{date_value}",
|
||||||
|
f"时间:{time_range}",
|
||||||
|
f"老师:{fields['teacher']}",
|
||||||
|
f"科目:{fields['subject']}",
|
||||||
|
"小结:",
|
||||||
|
str(fields.get("body") or "").strip(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _json_from_text(text: str) -> dict[str, Any]:
|
def _json_from_text(text: str) -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
payload = json.loads(text)
|
payload = json.loads(text)
|
||||||
@@ -229,6 +457,75 @@ def _question_for_error(register_type: str, message: str) -> str:
|
|||||||
return f"请补齐或修改课程小结信息:{message}"
|
return f"请补齐或修改课程小结信息:{message}"
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_class_record_fields(text: str) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"student": _extract_student(text),
|
||||||
|
"date": _normalize_date(text),
|
||||||
|
"time": _normalize_time_range(text),
|
||||||
|
"teacher": _extract_teacher(text),
|
||||||
|
"subject": _extract_subject(text),
|
||||||
|
"duration": _duration_from_text(text),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_payment_fields(text: str) -> dict[str, str]:
|
||||||
|
hours_match = HOURS_RE.search(text)
|
||||||
|
return {
|
||||||
|
"student": _extract_student(text),
|
||||||
|
"date": _normalize_date(text),
|
||||||
|
"hours": hours_match.group("hours") if hours_match else "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_course_summary_fields(text: str) -> dict[str, str]:
|
||||||
|
try:
|
||||||
|
raw = extract_course_summary_from_text(text, known_students=_known_students())
|
||||||
|
except ValueError:
|
||||||
|
raw = {}
|
||||||
|
body = str(raw.get("body") or "").strip()
|
||||||
|
if body == text.strip() and any(label in text for label in ("小结", "正文", "内容")):
|
||||||
|
body = re.split(r"(?:小结|正文|内容)[::]?", text, maxsplit=1)[-1].strip()
|
||||||
|
return {
|
||||||
|
"student": str(raw.get("student") or _extract_student(text)).strip(),
|
||||||
|
"date": _normalize_date(raw.get("date_iso") or text),
|
||||||
|
"time": _normalize_time_range(raw.get("time_range") or text),
|
||||||
|
"teacher": str(raw.get("teacher") or _extract_teacher(text)).strip(),
|
||||||
|
"subject": str(raw.get("subject") or _extract_subject(text)).strip(),
|
||||||
|
"body": body or text.strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def extract_local_fields(register_type: str, lines: list[str]) -> dict[str, str]:
|
||||||
|
text = "\n".join(lines).strip()
|
||||||
|
if register_type == "class_record":
|
||||||
|
return _extract_class_record_fields(text)
|
||||||
|
if register_type == "payment":
|
||||||
|
return _extract_payment_fields(text)
|
||||||
|
return _extract_course_summary_fields(text)
|
||||||
|
|
||||||
|
|
||||||
|
def fields_preview(register_type: str, fields: dict[str, str]) -> dict[str, Any] | None:
|
||||||
|
missing = _missing_fields(register_type, fields)
|
||||||
|
if missing:
|
||||||
|
return _needs_info_response(register_type, fields, missing)
|
||||||
|
try:
|
||||||
|
standard_lines = validate_standard_lines(register_type, [_standard_from_fields(register_type, fields)])
|
||||||
|
except (ValueError, KeyError):
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"status": "ready",
|
||||||
|
"standard_lines": standard_lines,
|
||||||
|
"questions": [],
|
||||||
|
"summary": "已通过本地规则生成预览",
|
||||||
|
"ai_used": False,
|
||||||
|
"fields": fields,
|
||||||
|
"missing_fields": [],
|
||||||
|
"field_labels": _field_labels(register_type),
|
||||||
|
"timed_out": False,
|
||||||
|
"recognition_source": "local",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def validate_standard_lines(register_type: str, lines: list[str]) -> list[str]:
|
def validate_standard_lines(register_type: str, lines: list[str]) -> list[str]:
|
||||||
if not lines:
|
if not lines:
|
||||||
raise ValueError("模型未返回标准行")
|
raise ValueError("模型未返回标准行")
|
||||||
@@ -257,17 +554,23 @@ def validate_standard_lines(register_type: str, lines: list[str]) -> list[str]:
|
|||||||
return standard_lines
|
return standard_lines
|
||||||
|
|
||||||
|
|
||||||
def local_preview(register_type: str, lines: list[str]) -> dict[str, Any] | None:
|
def local_preview(register_type: str, lines: list[str], answers: dict[str, str] | None = None) -> dict[str, Any] | None:
|
||||||
try:
|
try:
|
||||||
standard_lines = validate_standard_lines(register_type, lines)
|
standard_lines = validate_standard_lines(register_type, lines)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
fields = _merge_answers(extract_local_fields(register_type, lines), answers or {})
|
||||||
|
return fields_preview(register_type, fields)
|
||||||
return {
|
return {
|
||||||
"status": "ready",
|
"status": "ready",
|
||||||
"standard_lines": standard_lines,
|
"standard_lines": standard_lines,
|
||||||
"questions": [],
|
"questions": [],
|
||||||
"summary": "已按标准格式通过本地校验",
|
"summary": "已按标准格式通过本地校验",
|
||||||
"ai_used": False,
|
"ai_used": False,
|
||||||
|
"fields": {},
|
||||||
|
"missing_fields": [],
|
||||||
|
"field_labels": _field_labels(register_type),
|
||||||
|
"timed_out": False,
|
||||||
|
"recognition_source": "local",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -286,35 +589,35 @@ def preview_register(
|
|||||||
merged_answers = {**dict(session.get("answers") or {}), **(answers or {})}
|
merged_answers = {**dict(session.get("answers") or {}), **(answers or {})}
|
||||||
session["answers"] = merged_answers
|
session["answers"] = merged_answers
|
||||||
|
|
||||||
local = local_preview(normalized_type, source_lines)
|
local = local_preview(normalized_type, source_lines, merged_answers)
|
||||||
if local is not None and not merged_answers:
|
if local is not None:
|
||||||
return {"conversation_id": conversation_id, "type": normalized_type, **local}
|
return {"conversation_id": conversation_id, "type": normalized_type, **local}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
model_payload = call_model_for_standardization(normalized_type, source_lines, merged_answers)
|
model_payload = call_model_for_standardization(normalized_type, source_lines, merged_answers)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
if local is not None:
|
fields = _merge_answers(extract_local_fields(normalized_type, source_lines), merged_answers)
|
||||||
|
missing = _missing_fields(normalized_type, fields)
|
||||||
|
timed_out = "timed out" in str(exc).lower() or "timeout" in str(exc).lower()
|
||||||
return {
|
return {
|
||||||
"conversation_id": conversation_id,
|
"conversation_id": conversation_id,
|
||||||
"type": normalized_type,
|
"type": normalized_type,
|
||||||
**local,
|
**_needs_info_response(
|
||||||
"warning": "模型不可用,已使用本地标准格式校验预览",
|
normalized_type,
|
||||||
}
|
fields,
|
||||||
return {
|
missing,
|
||||||
"conversation_id": conversation_id,
|
ai_used=True,
|
||||||
"type": normalized_type,
|
timed_out=timed_out,
|
||||||
"status": "error",
|
error=str(exc),
|
||||||
"standard_lines": [],
|
summary="模型未及时生成可靠预览,请补齐字段后再生成",
|
||||||
"questions": [_question_for_error(normalized_type, str(exc))],
|
),
|
||||||
"summary": "",
|
|
||||||
"ai_used": True,
|
|
||||||
"error": str(exc),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
status = str(model_payload.get("status") or "").strip()
|
status = str(model_payload.get("status") or "").strip()
|
||||||
questions = [str(item).strip() for item in model_payload.get("questions") or [] if str(item).strip()]
|
questions = [str(item).strip() for item in model_payload.get("questions") or [] if str(item).strip()]
|
||||||
if status == "needs_info" or questions:
|
if status == "needs_info" or questions:
|
||||||
session["questions"] = questions
|
session["questions"] = questions
|
||||||
|
fields = _merge_answers(extract_local_fields(normalized_type, source_lines), merged_answers)
|
||||||
return {
|
return {
|
||||||
"conversation_id": conversation_id,
|
"conversation_id": conversation_id,
|
||||||
"type": normalized_type,
|
"type": normalized_type,
|
||||||
@@ -323,11 +626,17 @@ def preview_register(
|
|||||||
"questions": questions or ["请补齐缺失信息"],
|
"questions": questions or ["请补齐缺失信息"],
|
||||||
"summary": str(model_payload.get("summary") or ""),
|
"summary": str(model_payload.get("summary") or ""),
|
||||||
"ai_used": True,
|
"ai_used": True,
|
||||||
|
"fields": fields,
|
||||||
|
"missing_fields": _missing_fields(normalized_type, fields),
|
||||||
|
"field_labels": _field_labels(normalized_type),
|
||||||
|
"timed_out": False,
|
||||||
|
"recognition_source": "model",
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
standard_lines = validate_standard_lines(normalized_type, model_payload.get("standard_lines") or [])
|
standard_lines = validate_standard_lines(normalized_type, model_payload.get("standard_lines") or [])
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
|
fields = _merge_answers(extract_local_fields(normalized_type, source_lines), merged_answers)
|
||||||
return {
|
return {
|
||||||
"conversation_id": conversation_id,
|
"conversation_id": conversation_id,
|
||||||
"type": normalized_type,
|
"type": normalized_type,
|
||||||
@@ -337,6 +646,11 @@ def preview_register(
|
|||||||
"summary": str(model_payload.get("summary") or ""),
|
"summary": str(model_payload.get("summary") or ""),
|
||||||
"ai_used": True,
|
"ai_used": True,
|
||||||
"error": str(exc),
|
"error": str(exc),
|
||||||
|
"fields": fields,
|
||||||
|
"missing_fields": _missing_fields(normalized_type, fields),
|
||||||
|
"field_labels": _field_labels(normalized_type),
|
||||||
|
"timed_out": False,
|
||||||
|
"recognition_source": "model",
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
"conversation_id": conversation_id,
|
"conversation_id": conversation_id,
|
||||||
@@ -346,4 +660,9 @@ def preview_register(
|
|||||||
"questions": [],
|
"questions": [],
|
||||||
"summary": str(model_payload.get("summary") or "已生成标准登记内容"),
|
"summary": str(model_payload.get("summary") or "已生成标准登记内容"),
|
||||||
"ai_used": True,
|
"ai_used": True,
|
||||||
|
"fields": {},
|
||||||
|
"missing_fields": [],
|
||||||
|
"field_labels": _field_labels(normalized_type),
|
||||||
|
"timed_out": False,
|
||||||
|
"recognition_source": "model",
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -17,7 +17,8 @@ COURSE_SUMMARY_STATE_PATH = Path(os.getenv("COURSE_SUMMARY_STATE_PATH", "/data/c
|
|||||||
OPERATION_LOGS_PATH = Path(os.getenv("OPERATION_LOGS_PATH", "/data/operation_logs.jsonl"))
|
OPERATION_LOGS_PATH = Path(os.getenv("OPERATION_LOGS_PATH", "/data/operation_logs.jsonl"))
|
||||||
CODEX_CONFIG_PATH = Path(os.getenv("CODEX_CONFIG_PATH", "/run/codex/config.toml"))
|
CODEX_CONFIG_PATH = Path(os.getenv("CODEX_CONFIG_PATH", "/run/codex/config.toml"))
|
||||||
CODEX_AUTH_PATH = Path(os.getenv("CODEX_AUTH_PATH", "/run/codex/auth.json"))
|
CODEX_AUTH_PATH = Path(os.getenv("CODEX_AUTH_PATH", "/run/codex/auth.json"))
|
||||||
AI_REGISTER_TIMEOUT_SECONDS = float(os.getenv("AI_REGISTER_TIMEOUT_SECONDS", "90"))
|
AI_REGISTER_TIMEOUT_SECONDS = float(os.getenv("AI_REGISTER_TIMEOUT_SECONDS", "8"))
|
||||||
|
AI_REGISTER_MODEL = os.getenv("AI_REGISTER_MODEL", "").strip()
|
||||||
|
|
||||||
BASIC_AUTH_PASSWORD = os.getenv("BASIC_AUTH_PASSWORD", "")
|
BASIC_AUTH_PASSWORD = os.getenv("BASIC_AUTH_PASSWORD", "")
|
||||||
ACCOUNTS_AUTH_PASSWORD = os.getenv("ACCOUNTS_AUTH_PASSWORD") or os.getenv("ACCOUNT_AUTH_PASSWORD", "")
|
ACCOUNTS_AUTH_PASSWORD = os.getenv("ACCOUNTS_AUTH_PASSWORD") or os.getenv("ACCOUNT_AUTH_PASSWORD", "")
|
||||||
|
|||||||
+23
-2
@@ -993,7 +993,23 @@ function renderRegisterPreview(type, data, statusNode) {
|
|||||||
}
|
}
|
||||||
if (data.status === "needs_info") {
|
if (data.status === "needs_info") {
|
||||||
const questions = (data.questions || []).map((item) => `<li>${escapeHtml(item)}</li>`).join("");
|
const questions = (data.questions || []).map((item) => `<li>${escapeHtml(item)}</li>`).join("");
|
||||||
previewNode.innerHTML = `<strong>需要补充信息</strong><ul>${questions}</ul><textarea data-ai-answer rows="3" placeholder="在这里补充缺失信息后再次生成预览"></textarea>`;
|
const fields = data.fields || {};
|
||||||
|
const labels = data.field_labels || {};
|
||||||
|
const missing = new Set(data.missing_fields || []);
|
||||||
|
const fieldNames = Object.keys(labels);
|
||||||
|
const fieldInputs = fieldNames
|
||||||
|
.map((name) => {
|
||||||
|
const label = labels[name] || name;
|
||||||
|
const value = fields[name] || "";
|
||||||
|
const requiredMark = missing.has(name) ? " *" : "";
|
||||||
|
const input =
|
||||||
|
name === "body"
|
||||||
|
? `<textarea data-ai-field="${escapeHtml(name)}" rows="4">${escapeHtml(value)}</textarea>`
|
||||||
|
: `<input data-ai-field="${escapeHtml(name)}" value="${escapeHtml(value)}">`;
|
||||||
|
return `<label><span>${escapeHtml(label)}${requiredMark}</span>${input}</label>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
previewNode.innerHTML = `<strong>需要补充信息</strong><ul>${questions}</ul><div class="register-field-grid">${fieldInputs}</div>`;
|
||||||
confirmButton.hidden = true;
|
confirmButton.hidden = true;
|
||||||
registerPreviewState[type] = {
|
registerPreviewState[type] = {
|
||||||
conversationId: data.conversation_id,
|
conversationId: data.conversation_id,
|
||||||
@@ -1025,8 +1041,13 @@ async function previewRegister(event, type, lines, statusNode) {
|
|||||||
course_summary: summaryRegisterPreview,
|
course_summary: summaryRegisterPreview,
|
||||||
}[type];
|
}[type];
|
||||||
const answerNode = previewNode ? previewNode.querySelector("[data-ai-answer]") : null;
|
const answerNode = previewNode ? previewNode.querySelector("[data-ai-answer]") : null;
|
||||||
|
const fieldNodes = previewNode ? previewNode.querySelectorAll("[data-ai-field]") : [];
|
||||||
const state = registerPreviewState[type] || {};
|
const state = registerPreviewState[type] || {};
|
||||||
const answers = answerNode && answerNode.value.trim() ? { followup: answerNode.value.trim() } : {};
|
const answers = {};
|
||||||
|
if (answerNode && answerNode.value.trim()) answers.followup = answerNode.value.trim();
|
||||||
|
fieldNodes.forEach((node) => {
|
||||||
|
if (node.value.trim()) answers[node.dataset.aiField] = node.value.trim();
|
||||||
|
});
|
||||||
clearRegisterPreview(type, !state.conversationId);
|
clearRegisterPreview(type, !state.conversationId);
|
||||||
statusNode.textContent = "正在生成预览";
|
statusNode.textContent = "正在生成预览";
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -431,6 +431,29 @@ textarea:focus {
|
|||||||
min-height: 72px;
|
min-height: 72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.register-field-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-field-grid label {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
color: #52606d;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-field-grid input,
|
||||||
|
.register-field-grid textarea {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-field-grid label:has(textarea) {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
.line-code {
|
.line-code {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ services:
|
|||||||
OPERATION_LOGS_PATH: ${OPERATION_LOGS_PATH:-/data/operation_logs.jsonl}
|
OPERATION_LOGS_PATH: ${OPERATION_LOGS_PATH:-/data/operation_logs.jsonl}
|
||||||
CODEX_CONFIG_PATH: ${CODEX_CONFIG_PATH:-/run/codex/config.toml}
|
CODEX_CONFIG_PATH: ${CODEX_CONFIG_PATH:-/run/codex/config.toml}
|
||||||
CODEX_AUTH_PATH: ${CODEX_AUTH_PATH:-/run/codex/auth.json}
|
CODEX_AUTH_PATH: ${CODEX_AUTH_PATH:-/run/codex/auth.json}
|
||||||
AI_REGISTER_TIMEOUT_SECONDS: ${AI_REGISTER_TIMEOUT_SECONDS:-90}
|
AI_REGISTER_TIMEOUT_SECONDS: ${AI_REGISTER_TIMEOUT_SECONDS:-8}
|
||||||
|
AI_REGISTER_MODEL: ${AI_REGISTER_MODEL:-}
|
||||||
INGEST_AUTH_TOKEN: ${INGEST_AUTH_TOKEN:-}
|
INGEST_AUTH_TOKEN: ${INGEST_AUTH_TOKEN:-}
|
||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-18080}:8000"
|
- "${APP_PORT:-18080}:8000"
|
||||||
|
|||||||
Reference in New Issue
Block a user