加快登记预览并结构化补项
This commit is contained in:
+338
-19
@@ -10,8 +10,9 @@ import tomllib
|
||||
from typing import Any
|
||||
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 (
|
||||
SUBJECTS,
|
||||
class_record_to_line,
|
||||
course_summary_to_class_record_line,
|
||||
extract_course_summary_from_text,
|
||||
@@ -19,10 +20,48 @@ from .data import (
|
||||
normalize_lines,
|
||||
parse_class_record_line,
|
||||
parse_payment_line,
|
||||
read_accounts,
|
||||
)
|
||||
|
||||
|
||||
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_SESSIONS = 100
|
||||
_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"))
|
||||
auth = json.loads(auth_path.read_text(encoding="utf-8"))
|
||||
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 {}
|
||||
provider = providers.get(provider_name) or {}
|
||||
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)
|
||||
|
||||
|
||||
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]:
|
||||
try:
|
||||
payload = json.loads(text)
|
||||
@@ -229,6 +457,75 @@ def _question_for_error(register_type: str, message: str) -> str:
|
||||
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]:
|
||||
if not lines:
|
||||
raise ValueError("模型未返回标准行")
|
||||
@@ -257,17 +554,23 @@ def validate_standard_lines(register_type: str, lines: list[str]) -> list[str]:
|
||||
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:
|
||||
standard_lines = validate_standard_lines(register_type, lines)
|
||||
except ValueError:
|
||||
return None
|
||||
fields = _merge_answers(extract_local_fields(register_type, lines), answers or {})
|
||||
return fields_preview(register_type, fields)
|
||||
return {
|
||||
"status": "ready",
|
||||
"standard_lines": standard_lines,
|
||||
"questions": [],
|
||||
"summary": "已按标准格式通过本地校验",
|
||||
"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 {})}
|
||||
session["answers"] = merged_answers
|
||||
|
||||
local = local_preview(normalized_type, source_lines)
|
||||
if local is not None and not merged_answers:
|
||||
local = local_preview(normalized_type, source_lines, merged_answers)
|
||||
if local is not None:
|
||||
return {"conversation_id": conversation_id, "type": normalized_type, **local}
|
||||
|
||||
try:
|
||||
model_payload = call_model_for_standardization(normalized_type, source_lines, merged_answers)
|
||||
except ValueError as exc:
|
||||
if local is not None:
|
||||
return {
|
||||
"conversation_id": conversation_id,
|
||||
"type": normalized_type,
|
||||
**local,
|
||||
"warning": "模型不可用,已使用本地标准格式校验预览",
|
||||
}
|
||||
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 {
|
||||
"conversation_id": conversation_id,
|
||||
"type": normalized_type,
|
||||
"status": "error",
|
||||
"standard_lines": [],
|
||||
"questions": [_question_for_error(normalized_type, str(exc))],
|
||||
"summary": "",
|
||||
"ai_used": True,
|
||||
"error": str(exc),
|
||||
**_needs_info_response(
|
||||
normalized_type,
|
||||
fields,
|
||||
missing,
|
||||
ai_used=True,
|
||||
timed_out=timed_out,
|
||||
error=str(exc),
|
||||
summary="模型未及时生成可靠预览,请补齐字段后再生成",
|
||||
),
|
||||
}
|
||||
|
||||
status = str(model_payload.get("status") or "").strip()
|
||||
questions = [str(item).strip() for item in model_payload.get("questions") or [] if str(item).strip()]
|
||||
if status == "needs_info" or questions:
|
||||
session["questions"] = questions
|
||||
fields = _merge_answers(extract_local_fields(normalized_type, source_lines), merged_answers)
|
||||
return {
|
||||
"conversation_id": conversation_id,
|
||||
"type": normalized_type,
|
||||
@@ -323,11 +626,17 @@ def preview_register(
|
||||
"questions": questions or ["请补齐缺失信息"],
|
||||
"summary": str(model_payload.get("summary") or ""),
|
||||
"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:
|
||||
standard_lines = validate_standard_lines(normalized_type, model_payload.get("standard_lines") or [])
|
||||
except ValueError as exc:
|
||||
fields = _merge_answers(extract_local_fields(normalized_type, source_lines), merged_answers)
|
||||
return {
|
||||
"conversation_id": conversation_id,
|
||||
"type": normalized_type,
|
||||
@@ -337,6 +646,11 @@ def preview_register(
|
||||
"summary": str(model_payload.get("summary") or ""),
|
||||
"ai_used": True,
|
||||
"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 {
|
||||
"conversation_id": conversation_id,
|
||||
@@ -346,4 +660,9 @@ def preview_register(
|
||||
"questions": [],
|
||||
"summary": str(model_payload.get("summary") or "已生成标准登记内容"),
|
||||
"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"))
|
||||
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"))
|
||||
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", "")
|
||||
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") {
|
||||
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;
|
||||
registerPreviewState[type] = {
|
||||
conversationId: data.conversation_id,
|
||||
@@ -1025,8 +1041,13 @@ async function previewRegister(event, type, lines, statusNode) {
|
||||
course_summary: summaryRegisterPreview,
|
||||
}[type];
|
||||
const answerNode = previewNode ? previewNode.querySelector("[data-ai-answer]") : null;
|
||||
const fieldNodes = previewNode ? previewNode.querySelectorAll("[data-ai-field]") : [];
|
||||
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);
|
||||
statusNode.textContent = "正在生成预览";
|
||||
try {
|
||||
|
||||
@@ -431,6 +431,29 @@ textarea:focus {
|
||||
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 {
|
||||
display: block;
|
||||
max-width: 420px;
|
||||
|
||||
@@ -19,7 +19,8 @@ services:
|
||||
OPERATION_LOGS_PATH: ${OPERATION_LOGS_PATH:-/data/operation_logs.jsonl}
|
||||
CODEX_CONFIG_PATH: ${CODEX_CONFIG_PATH:-/run/codex/config.toml}
|
||||
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:-}
|
||||
ports:
|
||||
- "${APP_PORT:-18080}:8000"
|
||||
|
||||
Reference in New Issue
Block a user