feat: 添加风险规则及 agent assets 功能增强
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"rule_code": "risk.expense.consecutive_transport_receipts",
|
||||||
|
"name": "连号交通票据",
|
||||||
|
"enabled": true,
|
||||||
|
"risk_dimension": "consecutive_receipts",
|
||||||
|
"ontology_signal": "consecutive_transport_receipts",
|
||||||
|
"evaluator": "consecutive_transport_receipts",
|
||||||
|
"applies_to": {
|
||||||
|
"expense_types": ["transport", "travel"],
|
||||||
|
"min_attachments": 2
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"invoice_no": "attachment.invoice_no"
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"min_consecutive_count": 3
|
||||||
|
},
|
||||||
|
"outcomes": {
|
||||||
|
"pass": { "severity": "none", "action": "continue" },
|
||||||
|
"fail": {
|
||||||
|
"severity": "medium",
|
||||||
|
"action": "manual_review"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"owner": "风控与审计部",
|
||||||
|
"stability": "platform_builtin",
|
||||||
|
"source_ref": "常用risk.txt / 三、车辆交通 / 连号票集中报销",
|
||||||
|
"updated_at": "2026-05-19"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"rule_code": "risk.expense.entertainment_missing_detail",
|
||||||
|
"name": "招待费事由不完整",
|
||||||
|
"enabled": true,
|
||||||
|
"risk_dimension": "entertainment_detail",
|
||||||
|
"ontology_signal": "entertainment_missing_detail",
|
||||||
|
"evaluator": "entertainment_reason_missing",
|
||||||
|
"applies_to": {
|
||||||
|
"domains": ["meal"]
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"reason": "claim.reason_corpus"
|
||||||
|
},
|
||||||
|
"params": {},
|
||||||
|
"outcomes": {
|
||||||
|
"pass": { "severity": "none", "action": "continue" },
|
||||||
|
"fail": {
|
||||||
|
"severity": "medium",
|
||||||
|
"action": "warn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"owner": "风控与审计部",
|
||||||
|
"stability": "platform_builtin",
|
||||||
|
"source_ref": "常用risk.txt / 三、餐费招待 / 业务招待无事由对象",
|
||||||
|
"updated_at": "2026-05-19"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"rule_code": "risk.expense.meal_localized_as_travel",
|
||||||
|
"name": "同城餐饮混入差旅",
|
||||||
|
"enabled": true,
|
||||||
|
"risk_dimension": "meal_travel_mix",
|
||||||
|
"ontology_signal": "meal_as_travel",
|
||||||
|
"evaluator": "meal_as_travel_same_city",
|
||||||
|
"applies_to": {
|
||||||
|
"domains": ["travel"]
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"declared": "claim.location",
|
||||||
|
"meal_city": "attachment.cities"
|
||||||
|
},
|
||||||
|
"params": {},
|
||||||
|
"outcomes": {
|
||||||
|
"pass": { "severity": "none", "action": "continue" },
|
||||||
|
"fail": {
|
||||||
|
"severity": "medium",
|
||||||
|
"action": "warn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"owner": "风控与审计部",
|
||||||
|
"stability": "platform_builtin",
|
||||||
|
"source_ref": "常用risk.txt / 三、餐费招待 / 同城餐饮归集异地差旅",
|
||||||
|
"updated_at": "2026-05-19"
|
||||||
|
}
|
||||||
|
}
|
||||||
29
server/rules/risk-rules/risk.expense.reason_too_brief.json
Normal file
29
server/rules/risk-rules/risk.expense.reason_too_brief.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"rule_code": "risk.expense.reason_too_brief",
|
||||||
|
"name": "报销事由过短",
|
||||||
|
"enabled": true,
|
||||||
|
"risk_dimension": "reason_quality",
|
||||||
|
"ontology_signal": "reason_too_brief",
|
||||||
|
"evaluator": "reason_too_brief",
|
||||||
|
"applies_to": {},
|
||||||
|
"inputs": {
|
||||||
|
"reason": "claim.reason_corpus"
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"min_reason_length": 6
|
||||||
|
},
|
||||||
|
"outcomes": {
|
||||||
|
"pass": { "severity": "none", "action": "continue" },
|
||||||
|
"fail": {
|
||||||
|
"severity": "medium",
|
||||||
|
"action": "warn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"owner": "风控与审计部",
|
||||||
|
"stability": "platform_builtin",
|
||||||
|
"source_ref": "常用risk.txt / 通用 / 事由不足以支撑真实性判断",
|
||||||
|
"updated_at": "2026-05-19"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"rule_code": "risk.invoice.claimant_buyer_name_match",
|
||||||
|
"name": "报销人与发票抬头一致",
|
||||||
|
"enabled": true,
|
||||||
|
"risk_dimension": "identity_consistency",
|
||||||
|
"ontology_signal": "buyer_name_mismatch",
|
||||||
|
"evaluator": "identity_consistency",
|
||||||
|
"applies_to": {
|
||||||
|
"min_attachments": 1
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"claimant": "claim.employee_name",
|
||||||
|
"buyer": "attachment.buyer_name"
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"allow_keywords": ["代报", "集团", "公司", "有限公司"]
|
||||||
|
},
|
||||||
|
"outcomes": {
|
||||||
|
"pass": { "severity": "none", "action": "continue" },
|
||||||
|
"fail": {
|
||||||
|
"severity": "high",
|
||||||
|
"action": "manual_review"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"owner": "风控与审计部",
|
||||||
|
"stability": "platform_builtin",
|
||||||
|
"source_ref": "常用risk.txt / 二、发票类 / 抬头错误",
|
||||||
|
"updated_at": "2026-05-19"
|
||||||
|
}
|
||||||
|
}
|
||||||
30
server/rules/risk-rules/risk.invoice.cross_year_invoice.json
Normal file
30
server/rules/risk-rules/risk.invoice.cross_year_invoice.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"rule_code": "risk.invoice.cross_year_invoice",
|
||||||
|
"name": "跨年发票入账",
|
||||||
|
"enabled": true,
|
||||||
|
"risk_dimension": "cross_year_invoice",
|
||||||
|
"ontology_signal": "cross_year_invoice",
|
||||||
|
"evaluator": "cross_year_invoice",
|
||||||
|
"applies_to": {
|
||||||
|
"min_attachments": 1
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"invoice_date": "attachment.invoice_date",
|
||||||
|
"claim_date": ["claim.occurred_at", "item.item_date"]
|
||||||
|
},
|
||||||
|
"params": {},
|
||||||
|
"outcomes": {
|
||||||
|
"pass": { "severity": "none", "action": "continue" },
|
||||||
|
"fail": {
|
||||||
|
"severity": "medium",
|
||||||
|
"action": "warn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"owner": "风控与审计部",
|
||||||
|
"stability": "platform_builtin",
|
||||||
|
"source_ref": "常用risk.txt / 二、发票类 / 跨年发票",
|
||||||
|
"updated_at": "2026-05-19"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"rule_code": "risk.invoice.document_expense_mismatch",
|
||||||
|
"name": "开票内容与报销场景不符",
|
||||||
|
"enabled": true,
|
||||||
|
"risk_dimension": "document_expense_mismatch",
|
||||||
|
"ontology_signal": "document_expense_mismatch",
|
||||||
|
"evaluator": "document_expense_mismatch",
|
||||||
|
"applies_to": {
|
||||||
|
"min_attachments": 1
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"document_type": "attachment.document_type",
|
||||||
|
"expense_type": ["claim.expense_type", "item.item_type"]
|
||||||
|
},
|
||||||
|
"params": {},
|
||||||
|
"outcomes": {
|
||||||
|
"pass": { "severity": "none", "action": "continue" },
|
||||||
|
"fail": {
|
||||||
|
"severity": "medium",
|
||||||
|
"action": "warn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"owner": "风控与审计部",
|
||||||
|
"stability": "platform_builtin",
|
||||||
|
"source_ref": "常用risk.txt / 二、发票类 / 开票内容与业务不符",
|
||||||
|
"updated_at": "2026-05-19"
|
||||||
|
}
|
||||||
|
}
|
||||||
29
server/rules/risk-rules/risk.invoice.duplicate_invoice.json
Normal file
29
server/rules/risk-rules/risk.invoice.duplicate_invoice.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"rule_code": "risk.invoice.duplicate_invoice",
|
||||||
|
"name": "发票重复报销",
|
||||||
|
"enabled": true,
|
||||||
|
"risk_dimension": "duplicate_invoice",
|
||||||
|
"ontology_signal": "duplicate_invoice",
|
||||||
|
"evaluator": "duplicate_invoice",
|
||||||
|
"applies_to": {
|
||||||
|
"min_attachments": 1
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"invoice_no": "attachment.invoice_no"
|
||||||
|
},
|
||||||
|
"params": {},
|
||||||
|
"outcomes": {
|
||||||
|
"pass": { "severity": "none", "action": "continue" },
|
||||||
|
"fail": {
|
||||||
|
"severity": "high",
|
||||||
|
"action": "block"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"owner": "风控与审计部",
|
||||||
|
"stability": "platform_builtin",
|
||||||
|
"source_ref": "常用risk.txt / 二、发票类 / 重复报销",
|
||||||
|
"updated_at": "2026-05-19"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"rule_code": "risk.invoice.vague_goods_description",
|
||||||
|
"name": "发票品名过于笼统",
|
||||||
|
"enabled": true,
|
||||||
|
"risk_dimension": "vague_goods_description",
|
||||||
|
"ontology_signal": "vague_goods_description",
|
||||||
|
"evaluator": "vague_goods_description",
|
||||||
|
"applies_to": {
|
||||||
|
"expense_types": ["office", "other"],
|
||||||
|
"min_attachments": 1
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"ocr": "attachment.ocr_text"
|
||||||
|
},
|
||||||
|
"params": {},
|
||||||
|
"outcomes": {
|
||||||
|
"pass": { "severity": "none", "action": "continue" },
|
||||||
|
"fail": {
|
||||||
|
"severity": "medium",
|
||||||
|
"action": "warn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"owner": "风控与审计部",
|
||||||
|
"stability": "platform_builtin",
|
||||||
|
"source_ref": "常用risk.txt / 二、发票类 / 品名笼统",
|
||||||
|
"updated_at": "2026-05-19"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"rule_code": "risk.invoice.void_or_red_invoice",
|
||||||
|
"name": "作废或红冲发票",
|
||||||
|
"enabled": true,
|
||||||
|
"risk_dimension": "void_or_red_invoice",
|
||||||
|
"ontology_signal": "void_or_red_invoice",
|
||||||
|
"evaluator": "invoice_void_or_red",
|
||||||
|
"applies_to": {
|
||||||
|
"min_attachments": 1
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"status": "attachment.invoice_status",
|
||||||
|
"ocr": "attachment.ocr_text"
|
||||||
|
},
|
||||||
|
"params": {},
|
||||||
|
"outcomes": {
|
||||||
|
"pass": { "severity": "none", "action": "continue" },
|
||||||
|
"fail": {
|
||||||
|
"severity": "high",
|
||||||
|
"action": "block"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"owner": "风控与审计部",
|
||||||
|
"stability": "platform_builtin",
|
||||||
|
"source_ref": "常用risk.txt / 二、发票类 / 作废红冲发票",
|
||||||
|
"updated_at": "2026-05-19"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"rule_code": "risk.travel.base_location_overlap",
|
||||||
|
"name": "常驻地重合出差风险",
|
||||||
|
"enabled": true,
|
||||||
|
"risk_dimension": "base_location_overlap",
|
||||||
|
"ontology_signal": "base_location_overlap",
|
||||||
|
"evaluator": "base_location_overlap",
|
||||||
|
"applies_to": {
|
||||||
|
"domains": ["travel"]
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"employee_base": "employee.location",
|
||||||
|
"declared": "claim.location"
|
||||||
|
},
|
||||||
|
"params": {},
|
||||||
|
"outcomes": {
|
||||||
|
"pass": { "severity": "none", "action": "continue" },
|
||||||
|
"fail": {
|
||||||
|
"severity": "high",
|
||||||
|
"action": "manual_review"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"owner": "风控与审计部",
|
||||||
|
"stability": "platform_builtin",
|
||||||
|
"source_ref": "常用risk.txt / 一、出差类 / 两头在外",
|
||||||
|
"updated_at": "2026-05-19"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"rule_code": "risk.travel.destination_receipt_location",
|
||||||
|
"name": "申报地点与票据地点一致",
|
||||||
|
"risk_dimension": "location_consistency",
|
||||||
|
"ontology_signal": "location_mismatch",
|
||||||
|
"evaluator": "location_consistency",
|
||||||
|
"inputs": {
|
||||||
|
"declared": "claim.location",
|
||||||
|
"evidence": ["attachment.cities", "item.item_location"]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"match_mode": "city_fuzzy",
|
||||||
|
"missing_evidence": "warn"
|
||||||
|
},
|
||||||
|
"outcomes": {
|
||||||
|
"pass": { "severity": "none", "action": "continue" },
|
||||||
|
"fail": {
|
||||||
|
"severity": "high",
|
||||||
|
"action": "manual_review",
|
||||||
|
"message_template": "申报地点 {declared} 与票据识别地点 {evidence} 不一致"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"owner": "风控与审计部",
|
||||||
|
"stability": "platform_builtin",
|
||||||
|
"updated_at": "2026-05-18"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"rule_code": "risk.travel.hotel_without_itinerary",
|
||||||
|
"name": "住宿城市与行程不一致",
|
||||||
|
"enabled": true,
|
||||||
|
"risk_dimension": "hotel_itinerary",
|
||||||
|
"ontology_signal": "hotel_itinerary_mismatch",
|
||||||
|
"evaluator": "hotel_without_itinerary",
|
||||||
|
"applies_to": {
|
||||||
|
"domains": ["travel"],
|
||||||
|
"expense_types": ["hotel", "travel"]
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"declared": "claim.location",
|
||||||
|
"hotel": "attachment.hotel_city",
|
||||||
|
"itinerary": "attachment.route_cities"
|
||||||
|
},
|
||||||
|
"params": {},
|
||||||
|
"outcomes": {
|
||||||
|
"pass": { "severity": "none", "action": "continue" },
|
||||||
|
"fail": {
|
||||||
|
"severity": "high",
|
||||||
|
"action": "manual_review"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"owner": "风控与审计部",
|
||||||
|
"stability": "platform_builtin",
|
||||||
|
"source_ref": "常用risk.txt / 三、住宿费 / 夜间异地住宿、酒店连续多天",
|
||||||
|
"updated_at": "2026-05-19"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"rule_code": "risk.travel.intracity_travel_claim",
|
||||||
|
"name": "同城虚报差旅补贴",
|
||||||
|
"enabled": true,
|
||||||
|
"risk_dimension": "intracity_travel",
|
||||||
|
"ontology_signal": "intracity_travel",
|
||||||
|
"evaluator": "intracity_travel_claim",
|
||||||
|
"applies_to": {
|
||||||
|
"domains": ["travel"]
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"declared": "claim.location",
|
||||||
|
"evidence": ["attachment.route", "attachment.cities"]
|
||||||
|
},
|
||||||
|
"params": {},
|
||||||
|
"outcomes": {
|
||||||
|
"pass": { "severity": "none", "action": "continue" },
|
||||||
|
"fail": {
|
||||||
|
"severity": "high",
|
||||||
|
"action": "manual_review"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"owner": "风控与审计部",
|
||||||
|
"stability": "platform_builtin",
|
||||||
|
"source_ref": "常用risk.txt / 一、出差类 / 同城虚报差旅",
|
||||||
|
"updated_at": "2026-05-19"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "1.0",
|
||||||
|
"rule_code": "risk.travel.multi_city_reason_required",
|
||||||
|
"name": "多城市行程需说明",
|
||||||
|
"enabled": true,
|
||||||
|
"risk_dimension": "multi_city_itinerary",
|
||||||
|
"ontology_signal": "multi_city_itinerary",
|
||||||
|
"evaluator": "multi_city_reason_required",
|
||||||
|
"applies_to": {
|
||||||
|
"domains": ["travel"]
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"reason": "claim.reason_corpus",
|
||||||
|
"cities": ["attachment.cities", "item.item_location"]
|
||||||
|
},
|
||||||
|
"params": {},
|
||||||
|
"outcomes": {
|
||||||
|
"pass": { "severity": "none", "action": "continue" },
|
||||||
|
"fail": {
|
||||||
|
"severity": "medium",
|
||||||
|
"action": "warn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"owner": "风控与审计部",
|
||||||
|
"stability": "platform_builtin",
|
||||||
|
"source_ref": "常用risk.txt / 一、出差类 / 绕道出行、行程不符",
|
||||||
|
"updated_at": "2026-05-19"
|
||||||
|
}
|
||||||
|
}
|
||||||
28
server/scripts/sync_platform_risk_rules.py
Normal file
28
server/scripts/sync_platform_risk_rules.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Sync platform risk rule assets from server/rules/risk-rules/*.json."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SERVER_SRC = Path(__file__).resolve().parents[1] / "src"
|
||||||
|
if str(SERVER_SRC) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SERVER_SRC))
|
||||||
|
|
||||||
|
from app.db.session import get_session_factory # noqa: E402
|
||||||
|
from app.services.agent_foundation import AgentFoundationService # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
db = get_session_factory()()
|
||||||
|
try:
|
||||||
|
count = AgentFoundationService(db).sync_platform_risk_rules_from_library()
|
||||||
|
db.commit()
|
||||||
|
print(f"Synced {count} risk rule manifest(s) from library.")
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
13
server/scripts/test_rule_json_api.py
Normal file
13
server/scripts/test_rule_json_api.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
base = "http://127.0.0.1:8000/api/v1"
|
||||||
|
items = json.loads(urllib.request.urlopen(f"{base}/agent-assets?asset_type=rule").read())
|
||||||
|
risk = next((i for i in items if str(i.get("code", "")).startswith("risk.")), None)
|
||||||
|
print("risk asset:", risk.get("code") if risk else None)
|
||||||
|
if not risk:
|
||||||
|
raise SystemExit(1)
|
||||||
|
resp = urllib.request.urlopen(f"{base}/agent-assets/{risk['id']}/rule-json")
|
||||||
|
payload = json.loads(resp.read())
|
||||||
|
print("rule-json ok:", payload.get("file_name"), payload.get("evaluator"))
|
||||||
@@ -27,7 +27,6 @@ from app.schemas.agent_asset import (
|
|||||||
AgentAssetRuleJsonWrite,
|
AgentAssetRuleJsonWrite,
|
||||||
AgentAssetSpreadsheetChangeRecordRead,
|
AgentAssetSpreadsheetChangeRecordRead,
|
||||||
AgentAssetUpdate,
|
AgentAssetUpdate,
|
||||||
AgentAssetVersionCompareRead,
|
|
||||||
AgentAssetVersionCreate,
|
AgentAssetVersionCreate,
|
||||||
AgentAssetVersionRead,
|
AgentAssetVersionRead,
|
||||||
AgentAssetVersionTimelineItemRead,
|
AgentAssetVersionTimelineItemRead,
|
||||||
@@ -167,7 +166,7 @@ def get_agent_asset_spreadsheet_onlyoffice_config(
|
|||||||
db: DbSession,
|
db: DbSession,
|
||||||
version: Annotated[
|
version: Annotated[
|
||||||
str | None,
|
str | None,
|
||||||
Query(description="可选的规则版本号;不传时默认当前版本。"),
|
Query(description="兼容旧前端的可选参数;表格规则始终打开当前规则表。"),
|
||||||
] = None,
|
] = None,
|
||||||
) -> AgentAssetOnlyOfficeConfigRead:
|
) -> AgentAssetOnlyOfficeConfigRead:
|
||||||
try:
|
try:
|
||||||
@@ -184,7 +183,7 @@ def get_agent_asset_spreadsheet_onlyoffice_config(
|
|||||||
"/{asset_id}/spreadsheet/content",
|
"/{asset_id}/spreadsheet/content",
|
||||||
response_class=FileResponse,
|
response_class=FileResponse,
|
||||||
summary="下载或预览规则 Excel 文件",
|
summary="下载或预览规则 Excel 文件",
|
||||||
description="按版本返回规则的 Excel 快照,用于浏览器预览或下载。",
|
description="返回当前规则 Excel 文件,用于浏览器预览或下载。",
|
||||||
)
|
)
|
||||||
def get_agent_asset_spreadsheet_content(
|
def get_agent_asset_spreadsheet_content(
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
@@ -192,7 +191,7 @@ def get_agent_asset_spreadsheet_content(
|
|||||||
db: DbSession,
|
db: DbSession,
|
||||||
version: Annotated[
|
version: Annotated[
|
||||||
str | None,
|
str | None,
|
||||||
Query(description="可选的规则版本号;不传时默认当前版本。"),
|
Query(description="兼容旧前端的可选参数;不传时返回当前规则表。"),
|
||||||
] = None,
|
] = None,
|
||||||
) -> FileResponse:
|
) -> FileResponse:
|
||||||
try:
|
try:
|
||||||
@@ -215,18 +214,18 @@ def get_agent_asset_spreadsheet_content(
|
|||||||
def get_agent_asset_spreadsheet_onlyoffice_content(
|
def get_agent_asset_spreadsheet_onlyoffice_content(
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
db: DbSession,
|
db: DbSession,
|
||||||
version: Annotated[
|
|
||||||
str,
|
|
||||||
Query(min_length=1, description="规则版本号。"),
|
|
||||||
],
|
|
||||||
access_token: Annotated[
|
access_token: Annotated[
|
||||||
str,
|
str,
|
||||||
Query(min_length=1, description="ONLYOFFICE 临时访问令牌。"),
|
Query(min_length=1, description="ONLYOFFICE 临时访问令牌。"),
|
||||||
],
|
],
|
||||||
|
version: Annotated[
|
||||||
|
str | None,
|
||||||
|
Query(description="兼容旧 ONLYOFFICE URL;当前表格模式不再使用。"),
|
||||||
|
] = None,
|
||||||
) -> FileResponse:
|
) -> FileResponse:
|
||||||
try:
|
try:
|
||||||
service = AgentAssetService(db)
|
service = AgentAssetService(db)
|
||||||
service.validate_rule_spreadsheet_access_token(asset_id, version, access_token)
|
service.validate_rule_spreadsheet_access_token(asset_id, access_token)
|
||||||
file_path, media_type, filename = service.get_rule_spreadsheet_content(
|
file_path, media_type, filename = service.get_rule_spreadsheet_content(
|
||||||
asset_id,
|
asset_id,
|
||||||
version=version,
|
version=version,
|
||||||
@@ -246,7 +245,7 @@ def get_agent_asset_spreadsheet_onlyoffice_content(
|
|||||||
response_model=AgentAssetRead,
|
response_model=AgentAssetRead,
|
||||||
status_code=status.HTTP_201_CREATED,
|
status_code=status.HTTP_201_CREATED,
|
||||||
summary="上传规则 Excel 文件",
|
summary="上传规则 Excel 文件",
|
||||||
description="为指定规则上传新的 Excel 快照,并自动生成新规则版本。",
|
description="为指定规则上传新的 Excel 文件,并记录本次表格修改。",
|
||||||
)
|
)
|
||||||
def upload_agent_asset_spreadsheet(
|
def upload_agent_asset_spreadsheet(
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
@@ -311,16 +310,16 @@ def import_agent_asset_spreadsheet_content(
|
|||||||
"/{asset_id}/spreadsheet/onlyoffice/callback",
|
"/{asset_id}/spreadsheet/onlyoffice/callback",
|
||||||
response_model=AgentAssetOnlyOfficeCallbackRead,
|
response_model=AgentAssetOnlyOfficeCallbackRead,
|
||||||
summary="接收规则 Excel 的 ONLYOFFICE 回调",
|
summary="接收规则 Excel 的 ONLYOFFICE 回调",
|
||||||
description="接收 ONLYOFFICE 回写内容,并自动生成新的规则版本。",
|
description="接收 ONLYOFFICE 回写内容,并记录本次表格修改。",
|
||||||
)
|
)
|
||||||
def handle_agent_asset_spreadsheet_onlyoffice_callback(
|
def handle_agent_asset_spreadsheet_onlyoffice_callback(
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
payload: AgentAssetOnlyOfficeCallbackWrite,
|
payload: AgentAssetOnlyOfficeCallbackWrite,
|
||||||
db: DbSession,
|
db: DbSession,
|
||||||
version: Annotated[
|
version: Annotated[
|
||||||
str,
|
str | None,
|
||||||
Query(min_length=1, description="打开编辑器时对应的规则版本号。"),
|
Query(description="兼容旧 ONLYOFFICE 回调;当前表格模式不再使用。"),
|
||||||
],
|
] = None,
|
||||||
actor_name: Annotated[
|
actor_name: Annotated[
|
||||||
str | None,
|
str | None,
|
||||||
Query(description="发起编辑的用户显示名。"),
|
Query(description="发起编辑的用户显示名。"),
|
||||||
@@ -601,25 +600,3 @@ def get_agent_asset_version_timeline(
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
_handle_asset_error(exc)
|
_handle_asset_error(exc)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/{asset_id}/versions/compare",
|
|
||||||
response_model=AgentAssetVersionCompareRead,
|
|
||||||
summary="比较两个规则表版本",
|
|
||||||
description="对比两个 Excel 规则表版本的工作表变化与单元格级差异。",
|
|
||||||
)
|
|
||||||
def compare_agent_asset_spreadsheet_versions(
|
|
||||||
asset_id: str,
|
|
||||||
_: CurrentUser,
|
|
||||||
db: DbSession,
|
|
||||||
base_version: Annotated[str, Query(min_length=1, description="基准版本号")],
|
|
||||||
target_version: Annotated[str, Query(min_length=1, description="对比版本号")],
|
|
||||||
) -> AgentAssetVersionCompareRead:
|
|
||||||
try:
|
|
||||||
return AgentAssetService(db).compare_spreadsheet_versions(
|
|
||||||
asset_id,
|
|
||||||
base_version=base_version,
|
|
||||||
target_version=target_version,
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
_handle_asset_error(exc)
|
|
||||||
|
|||||||
@@ -133,22 +133,10 @@ class AgentAssetSpreadsheetDiffSheetRead(BaseModel):
|
|||||||
change_type: str
|
change_type: str
|
||||||
|
|
||||||
|
|
||||||
class AgentAssetVersionCompareRead(BaseModel):
|
|
||||||
base_version: str
|
|
||||||
target_version: str
|
|
||||||
added_sheet_count: int = 0
|
|
||||||
removed_sheet_count: int = 0
|
|
||||||
changed_sheet_count: int = 0
|
|
||||||
changed_cell_count: int = 0
|
|
||||||
sheet_changes: list[AgentAssetSpreadsheetDiffSheetRead] = Field(default_factory=list)
|
|
||||||
cell_changes: list[AgentAssetSpreadsheetDiffCellRead] = Field(default_factory=list)
|
|
||||||
|
|
||||||
|
|
||||||
class AgentAssetSpreadsheetChangeRecordRead(BaseModel):
|
class AgentAssetSpreadsheetChangeRecordRead(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
actor: str
|
actor: str
|
||||||
changed_at: datetime
|
changed_at: datetime
|
||||||
version: str | None = None
|
|
||||||
summary: str
|
summary: str
|
||||||
sheet_changes: list[AgentAssetSpreadsheetDiffSheetRead] = Field(default_factory=list)
|
sheet_changes: list[AgentAssetSpreadsheetDiffSheetRead] = Field(default_factory=list)
|
||||||
cell_changes: list[AgentAssetSpreadsheetDiffCellRead] = Field(default_factory=list)
|
cell_changes: list[AgentAssetSpreadsheetDiffCellRead] = Field(default_factory=list)
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ from app.schemas.agent_asset import (
|
|||||||
AgentAssetSpreadsheetDiffCellRead,
|
AgentAssetSpreadsheetDiffCellRead,
|
||||||
AgentAssetSpreadsheetDiffSheetRead,
|
AgentAssetSpreadsheetDiffSheetRead,
|
||||||
AgentAssetUpdate,
|
AgentAssetUpdate,
|
||||||
AgentAssetVersionCompareRead,
|
|
||||||
AgentAssetVersionCreate,
|
AgentAssetVersionCreate,
|
||||||
AgentAssetVersionRead,
|
AgentAssetVersionRead,
|
||||||
AgentAssetVersionTimelineItemRead,
|
AgentAssetVersionTimelineItemRead,
|
||||||
@@ -511,18 +510,16 @@ class AgentAssetService:
|
|||||||
return self._build_onlyoffice_spreadsheet_config(
|
return self._build_onlyoffice_spreadsheet_config(
|
||||||
asset_id=asset_id,
|
asset_id=asset_id,
|
||||||
current_user=current_user,
|
current_user=current_user,
|
||||||
resolved_version=resolved_version,
|
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
editable=resolved_version == PREVIEW_RULE_CURRENT_VERSION,
|
editable=resolved_version == PREVIEW_RULE_CURRENT_VERSION,
|
||||||
)
|
)
|
||||||
|
|
||||||
asset = self._require_spreadsheet_rule(asset_id)
|
asset = self._require_spreadsheet_rule(asset_id)
|
||||||
resolved_version, metadata = self._resolve_current_spreadsheet_meta(asset)
|
_, metadata = self._resolve_current_spreadsheet_meta(asset)
|
||||||
editable = self._can_edit_current_spreadsheet(current_user)
|
editable = self._can_edit_current_spreadsheet(current_user)
|
||||||
return self._build_onlyoffice_spreadsheet_config(
|
return self._build_onlyoffice_spreadsheet_config(
|
||||||
asset_id=asset.id,
|
asset_id=asset.id,
|
||||||
current_user=current_user,
|
current_user=current_user,
|
||||||
resolved_version=resolved_version,
|
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
editable=editable,
|
editable=editable,
|
||||||
)
|
)
|
||||||
@@ -555,7 +552,6 @@ class AgentAssetService:
|
|||||||
def validate_rule_spreadsheet_access_token(
|
def validate_rule_spreadsheet_access_token(
|
||||||
self,
|
self,
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
version: str,
|
|
||||||
access_token: str,
|
access_token: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
onlyoffice_settings = resolve_onlyoffice_settings()
|
onlyoffice_settings = resolve_onlyoffice_settings()
|
||||||
@@ -571,7 +567,6 @@ class AgentAssetService:
|
|||||||
if (
|
if (
|
||||||
payload.get("scope") != "agent-asset-spreadsheet"
|
payload.get("scope") != "agent-asset-spreadsheet"
|
||||||
or payload.get("asset_id") != asset_id
|
or payload.get("asset_id") != asset_id
|
||||||
or payload.get("version") != version
|
|
||||||
):
|
):
|
||||||
raise ValueError("ONLYOFFICE 文件访问令牌无效。")
|
raise ValueError("ONLYOFFICE 文件访问令牌无效。")
|
||||||
|
|
||||||
@@ -604,7 +599,6 @@ class AgentAssetService:
|
|||||||
)
|
)
|
||||||
changed_sheet_count = self._count_changed_sheets(sheet_changes, cell_changes)
|
changed_sheet_count = self._count_changed_sheets(sheet_changes, cell_changes)
|
||||||
changed_cell_count = len(cell_changes)
|
changed_cell_count = len(cell_changes)
|
||||||
next_version = self._next_available_version(asset)
|
|
||||||
|
|
||||||
metadata = self._store_current_rule_spreadsheet(
|
metadata = self._store_current_rule_spreadsheet(
|
||||||
asset,
|
asset,
|
||||||
@@ -613,45 +607,10 @@ class AgentAssetService:
|
|||||||
actor=actor,
|
actor=actor,
|
||||||
source=source,
|
source=source,
|
||||||
)
|
)
|
||||||
snapshot_metadata = self.spreadsheet_manager.store_rule_library_spreadsheet_snapshot(
|
|
||||||
library=self._resolve_spreadsheet_rule_library(asset),
|
|
||||||
asset_id=asset.id,
|
|
||||||
version=next_version,
|
|
||||||
file_name=file_name,
|
|
||||||
content=content,
|
|
||||||
actor_name=actor,
|
|
||||||
source=source,
|
|
||||||
)
|
|
||||||
operation_label = (
|
|
||||||
change_note
|
|
||||||
or (
|
|
||||||
"ONLYOFFICE 在线编辑"
|
|
||||||
if source == "onlyoffice"
|
|
||||||
else f"上传并覆盖当前规则表:{normalized_name}"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
summary = self._build_spreadsheet_change_summary(
|
summary = self._build_spreadsheet_change_summary(
|
||||||
operation_label,
|
|
||||||
sheet_changes,
|
sheet_changes,
|
||||||
cell_changes,
|
cell_changes,
|
||||||
)
|
)
|
||||||
version_content = self.spreadsheet_manager.build_version_markdown(
|
|
||||||
rule_name=asset.name,
|
|
||||||
version=next_version,
|
|
||||||
metadata=snapshot_metadata,
|
|
||||||
)
|
|
||||||
self.create_version(
|
|
||||||
asset.id,
|
|
||||||
AgentAssetVersionCreate(
|
|
||||||
version=next_version,
|
|
||||||
content=version_content,
|
|
||||||
content_type=AgentAssetContentType.MARKDOWN,
|
|
||||||
change_note=summary,
|
|
||||||
created_by=actor,
|
|
||||||
),
|
|
||||||
actor=actor,
|
|
||||||
request_id=request_id,
|
|
||||||
)
|
|
||||||
self.audit_service.log_action(
|
self.audit_service.log_action(
|
||||||
actor=actor,
|
actor=actor,
|
||||||
action="edit_rule_spreadsheet",
|
action="edit_rule_spreadsheet",
|
||||||
@@ -660,13 +619,11 @@ class AgentAssetService:
|
|||||||
before_json={"storage_key": current_metadata.storage_key},
|
before_json={"storage_key": current_metadata.storage_key},
|
||||||
after_json={
|
after_json={
|
||||||
"summary": summary,
|
"summary": summary,
|
||||||
"version": next_version,
|
|
||||||
"changed_sheet_count": changed_sheet_count,
|
"changed_sheet_count": changed_sheet_count,
|
||||||
"changed_cell_count": changed_cell_count,
|
"changed_cell_count": changed_cell_count,
|
||||||
"sheet_changes": [item.model_dump() for item in sheet_changes],
|
"sheet_changes": [item.model_dump() for item in sheet_changes],
|
||||||
"cell_changes": [item.model_dump() for item in cell_changes[:500]],
|
"cell_changes": [item.model_dump() for item in cell_changes[:500]],
|
||||||
"storage_key": metadata.storage_key,
|
"storage_key": metadata.storage_key,
|
||||||
"snapshot_storage_key": snapshot_metadata.storage_key,
|
|
||||||
},
|
},
|
||||||
request_id=request_id,
|
request_id=request_id,
|
||||||
)
|
)
|
||||||
@@ -705,7 +662,7 @@ class AgentAssetService:
|
|||||||
self,
|
self,
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
*,
|
*,
|
||||||
version: str,
|
version: str | None = None,
|
||||||
payload: dict[str, Any],
|
payload: dict[str, Any],
|
||||||
actor_name: str | None = None,
|
actor_name: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -721,8 +678,6 @@ class AgentAssetService:
|
|||||||
callback = self._parse_onlyoffice_callback(payload)
|
callback = self._parse_onlyoffice_callback(payload)
|
||||||
if callback.status not in {2, 6} or not callback.download_url:
|
if callback.status not in {2, 6} or not callback.download_url:
|
||||||
return
|
return
|
||||||
if str(version or "").strip() not in {"", "current", self._resolve_working_version(asset)}:
|
|
||||||
return
|
|
||||||
|
|
||||||
_, current_metadata = self._resolve_current_spreadsheet_meta(asset)
|
_, current_metadata = self._resolve_current_spreadsheet_meta(asset)
|
||||||
request = Request(
|
request = Request(
|
||||||
@@ -924,44 +879,6 @@ class AgentAssetService:
|
|||||||
|
|
||||||
return sorted(events, key=lambda item: item.event_time)
|
return sorted(events, key=lambda item: item.event_time)
|
||||||
|
|
||||||
def compare_spreadsheet_versions(
|
|
||||||
self,
|
|
||||||
asset_id: str,
|
|
||||||
*,
|
|
||||||
base_version: str,
|
|
||||||
target_version: str,
|
|
||||||
) -> AgentAssetVersionCompareRead:
|
|
||||||
self._ensure_ready()
|
|
||||||
asset = self._require_spreadsheet_rule(asset_id)
|
|
||||||
resolved_base, base_meta = self._resolve_spreadsheet_version_meta(
|
|
||||||
asset,
|
|
||||||
version=base_version,
|
|
||||||
)
|
|
||||||
resolved_target, target_meta = self._resolve_spreadsheet_version_meta(
|
|
||||||
asset,
|
|
||||||
version=target_version,
|
|
||||||
)
|
|
||||||
|
|
||||||
base_workbook = self._load_spreadsheet_for_compare(base_meta)
|
|
||||||
target_workbook = self._load_spreadsheet_for_compare(target_meta)
|
|
||||||
sheet_changes, cell_changes = self._collect_workbook_changes(
|
|
||||||
base_workbook,
|
|
||||||
target_workbook,
|
|
||||||
)
|
|
||||||
added_sheet_count = sum(1 for item in sheet_changes if item.change_type == "added")
|
|
||||||
removed_sheet_count = sum(1 for item in sheet_changes if item.change_type == "removed")
|
|
||||||
|
|
||||||
return AgentAssetVersionCompareRead(
|
|
||||||
base_version=resolved_base,
|
|
||||||
target_version=resolved_target,
|
|
||||||
added_sheet_count=added_sheet_count,
|
|
||||||
removed_sheet_count=removed_sheet_count,
|
|
||||||
changed_sheet_count=self._count_changed_sheets(sheet_changes, cell_changes),
|
|
||||||
changed_cell_count=len(cell_changes),
|
|
||||||
sheet_changes=sheet_changes,
|
|
||||||
cell_changes=cell_changes[:500],
|
|
||||||
)
|
|
||||||
|
|
||||||
def list_spreadsheet_change_records(
|
def list_spreadsheet_change_records(
|
||||||
self,
|
self,
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
@@ -981,8 +898,7 @@ class AgentAssetService:
|
|||||||
id=log.id,
|
id=log.id,
|
||||||
actor=log.actor,
|
actor=log.actor,
|
||||||
changed_at=log.created_at,
|
changed_at=log.created_at,
|
||||||
version=str((log.after_json or {}).get("version") or "").strip() or None,
|
summary=str((log.after_json or {}).get("summary") or "表格内容已保存。"),
|
||||||
summary=str((log.after_json or {}).get("summary") or "ONLYOFFICE 在线编辑保存。"),
|
|
||||||
sheet_changes=[
|
sheet_changes=[
|
||||||
AgentAssetSpreadsheetDiffSheetRead.model_validate(item)
|
AgentAssetSpreadsheetDiffSheetRead.model_validate(item)
|
||||||
for item in ((log.after_json or {}).get("sheet_changes") or [])
|
for item in ((log.after_json or {}).get("sheet_changes") or [])
|
||||||
@@ -1292,7 +1208,6 @@ class AgentAssetService:
|
|||||||
*,
|
*,
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
current_user: CurrentUserContext,
|
current_user: CurrentUserContext,
|
||||||
resolved_version: str,
|
|
||||||
metadata: RuleSpreadsheetMeta,
|
metadata: RuleSpreadsheetMeta,
|
||||||
editable: bool,
|
editable: bool,
|
||||||
) -> AgentAssetOnlyOfficeConfigRead:
|
) -> AgentAssetOnlyOfficeConfigRead:
|
||||||
@@ -1307,21 +1222,21 @@ class AgentAssetService:
|
|||||||
|
|
||||||
backend_base_url = onlyoffice_settings.backend_url.rstrip("/")
|
backend_base_url = onlyoffice_settings.backend_url.rstrip("/")
|
||||||
public_url = onlyoffice_settings.public_url.rstrip("/")
|
public_url = onlyoffice_settings.public_url.rstrip("/")
|
||||||
access_token = self._build_onlyoffice_access_token(asset_id, resolved_version)
|
access_token = self._build_onlyoffice_access_token(asset_id)
|
||||||
document_url = (
|
document_url = (
|
||||||
f"{backend_base_url}{settings.api_v1_prefix}/agent-assets/{asset_id}/spreadsheet/onlyoffice/content"
|
f"{backend_base_url}{settings.api_v1_prefix}/agent-assets/{asset_id}/spreadsheet/onlyoffice/content"
|
||||||
f"?version={resolved_version}&access_token={access_token}"
|
f"?access_token={access_token}"
|
||||||
)
|
)
|
||||||
callback_url = (
|
callback_url = (
|
||||||
f"{backend_base_url}{settings.api_v1_prefix}/agent-assets/{asset_id}/spreadsheet/onlyoffice/callback"
|
f"{backend_base_url}{settings.api_v1_prefix}/agent-assets/{asset_id}/spreadsheet/onlyoffice/callback"
|
||||||
f"?version={resolved_version}&actor_name={quote(current_user.name)}"
|
f"?actor_name={quote(current_user.name)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
config: dict[str, Any] = {
|
config: dict[str, Any] = {
|
||||||
"documentType": "cell",
|
"documentType": "cell",
|
||||||
"document": {
|
"document": {
|
||||||
"fileType": Path(metadata.file_name).suffix.lstrip(".").lower() or "xlsx",
|
"fileType": Path(metadata.file_name).suffix.lstrip(".").lower() or "xlsx",
|
||||||
"key": self._build_onlyoffice_document_key(asset_id, resolved_version, metadata),
|
"key": self._build_onlyoffice_document_key(asset_id, metadata),
|
||||||
"title": metadata.file_name,
|
"title": metadata.file_name,
|
||||||
"url": document_url,
|
"url": document_url,
|
||||||
"permissions": {
|
"permissions": {
|
||||||
@@ -1462,19 +1377,6 @@ class AgentAssetService:
|
|||||||
major, minor, patch = [int(item) for item in parts]
|
major, minor, patch = [int(item) for item in parts]
|
||||||
return f"v{major}.{minor}.{patch + 1}"
|
return f"v{major}.{minor}.{patch + 1}"
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _can_edit_spreadsheet_version(
|
|
||||||
asset: AgentAsset,
|
|
||||||
current_user: CurrentUserContext,
|
|
||||||
version: str,
|
|
||||||
) -> bool:
|
|
||||||
role_codes = {str(item).strip() for item in current_user.role_codes}
|
|
||||||
can_edit = current_user.is_admin or "manager" in role_codes or "finance" in role_codes
|
|
||||||
return (
|
|
||||||
can_edit
|
|
||||||
and AgentAssetService._resolve_working_version(asset) == str(version or "").strip()
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _can_edit_current_spreadsheet(current_user: CurrentUserContext) -> bool:
|
def _can_edit_current_spreadsheet(current_user: CurrentUserContext) -> bool:
|
||||||
role_codes = {str(item).strip() for item in current_user.role_codes}
|
role_codes = {str(item).strip() for item in current_user.role_codes}
|
||||||
@@ -1483,23 +1385,21 @@ class AgentAssetService:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_onlyoffice_document_key(
|
def _build_onlyoffice_document_key(
|
||||||
asset_id: str,
|
asset_id: str,
|
||||||
version: str,
|
|
||||||
metadata: RuleSpreadsheetMeta,
|
metadata: RuleSpreadsheetMeta,
|
||||||
) -> str:
|
) -> str:
|
||||||
fingerprint = metadata.checksum or metadata.updated_at or metadata.file_name
|
fingerprint = metadata.checksum or metadata.updated_at or metadata.file_name
|
||||||
raw_key = f"{asset_id}-{version}-{fingerprint}"
|
raw_key = f"{asset_id}-{fingerprint}"
|
||||||
return "".join(
|
return "".join(
|
||||||
character if character.isalnum() or character in {"-", "_", ".", "="} else "_"
|
character if character.isalnum() or character in {"-", "_", ".", "="} else "_"
|
||||||
for character in raw_key
|
for character in raw_key
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_onlyoffice_access_token(asset_id: str, version: str) -> str:
|
def _build_onlyoffice_access_token(asset_id: str) -> str:
|
||||||
onlyoffice_settings = resolve_onlyoffice_settings()
|
onlyoffice_settings = resolve_onlyoffice_settings()
|
||||||
payload = {
|
payload = {
|
||||||
"scope": "agent-asset-spreadsheet",
|
"scope": "agent-asset-spreadsheet",
|
||||||
"asset_id": asset_id,
|
"asset_id": asset_id,
|
||||||
"version": version,
|
|
||||||
}
|
}
|
||||||
return jwt.encode(payload, onlyoffice_settings.jwt_secret, algorithm="HS256")
|
return jwt.encode(payload, onlyoffice_settings.jwt_secret, algorithm="HS256")
|
||||||
|
|
||||||
@@ -1646,7 +1546,6 @@ class AgentAssetService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_spreadsheet_change_summary(
|
def _build_spreadsheet_change_summary(
|
||||||
operation_label: str,
|
|
||||||
sheet_changes: list[AgentAssetSpreadsheetDiffSheetRead],
|
sheet_changes: list[AgentAssetSpreadsheetDiffSheetRead],
|
||||||
cell_changes: list[AgentAssetSpreadsheetDiffCellRead],
|
cell_changes: list[AgentAssetSpreadsheetDiffCellRead],
|
||||||
) -> str:
|
) -> str:
|
||||||
@@ -1655,15 +1554,15 @@ class AgentAssetService:
|
|||||||
| {item.sheet_name for item in cell_changes}
|
| {item.sheet_name for item in cell_changes}
|
||||||
)
|
)
|
||||||
if not sheet_names:
|
if not sheet_names:
|
||||||
return f"{operation_label}:文件内容已保存,未发现单元格级差异。"
|
return "文件内容已保存,未发现单元格级差异。"
|
||||||
|
|
||||||
preview = "、".join(sheet_names[:3])
|
preview = "、".join(sheet_names[:3])
|
||||||
if len(sheet_names) > 3:
|
if len(sheet_names) > 3:
|
||||||
preview = f"{preview} 等"
|
preview = f"{preview} 等"
|
||||||
sheet_text = f"涉及 {len(sheet_names)} 个工作表({preview})"
|
sheet_text = f"涉及 {len(sheet_names)} 个工作表({preview})"
|
||||||
if cell_changes:
|
if cell_changes:
|
||||||
return f"{operation_label}:{sheet_text},共 {len(cell_changes)} 处单元格改动。"
|
return f"{sheet_text},共 {len(cell_changes)} 处单元格改动。"
|
||||||
return f"{operation_label}:{sheet_text},工作表结构发生变化。"
|
return f"{sheet_text},工作表结构发生变化。"
|
||||||
|
|
||||||
def _next_available_version(self, asset: AgentAsset) -> str:
|
def _next_available_version(self, asset: AgentAsset) -> str:
|
||||||
candidate = self._increment_version(self._resolve_working_version(asset))
|
candidate = self._increment_version(self._resolve_working_version(asset))
|
||||||
|
|||||||
@@ -3189,7 +3189,24 @@ class UserAgentService:
|
|||||||
evidence="来源于用户修改后的结构化表单。",
|
evidence="来源于用户修改后的结构化表单。",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
inferred_reason = self._infer_reason_from_claim_groups(
|
||||||
|
claim_groups=claim_groups,
|
||||||
|
)
|
||||||
reason_value = self._resolve_reason_text(self._resolve_reason_source_text(payload))
|
reason_value = self._resolve_reason_text(self._resolve_reason_source_text(payload))
|
||||||
|
if inferred_reason:
|
||||||
|
return self._build_slot_value(
|
||||||
|
value=inferred_reason,
|
||||||
|
raw_value=reason_value or inferred_reason,
|
||||||
|
normalized_value=inferred_reason,
|
||||||
|
source="ocr",
|
||||||
|
confidence=0.82,
|
||||||
|
evidence=(
|
||||||
|
"系统已根据票据识别结果预置场景类型;原始描述仍保留为补充说明。"
|
||||||
|
if reason_value
|
||||||
|
else "系统已根据票据识别场景补全通用事由,若需更具体说明可继续修改。"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
if reason_value:
|
if reason_value:
|
||||||
return self._build_slot_value(
|
return self._build_slot_value(
|
||||||
value=reason_value,
|
value=reason_value,
|
||||||
@@ -3199,19 +3216,6 @@ class UserAgentService:
|
|||||||
confidence=0.76,
|
confidence=0.76,
|
||||||
evidence="系统从用户原始描述中提取了本次费用事由,建议继续核对。",
|
evidence="系统从用户原始描述中提取了本次费用事由,建议继续核对。",
|
||||||
)
|
)
|
||||||
|
|
||||||
inferred_reason = self._infer_reason_from_claim_groups(
|
|
||||||
claim_groups=claim_groups,
|
|
||||||
)
|
|
||||||
if inferred_reason:
|
|
||||||
return self._build_slot_value(
|
|
||||||
value=inferred_reason,
|
|
||||||
raw_value=inferred_reason,
|
|
||||||
normalized_value=inferred_reason,
|
|
||||||
source="ocr",
|
|
||||||
confidence=0.68,
|
|
||||||
evidence="系统已根据票据识别场景补全通用事由,若需更具体说明可继续修改。",
|
|
||||||
)
|
|
||||||
return self._build_slot_value()
|
return self._build_slot_value()
|
||||||
|
|
||||||
def _build_amount_slot(
|
def _build_amount_slot(
|
||||||
|
|||||||
@@ -35,13 +35,13 @@
|
|||||||
"updated_at": "2026-05-17T13:00:09.485818+00:00",
|
"updated_at": "2026-05-17T13:00:09.485818+00:00",
|
||||||
"uploaded_by": "admin",
|
"uploaded_by": "admin",
|
||||||
"version_number": 1,
|
"version_number": 1,
|
||||||
"ingest_status": 1,
|
"ingest_status": 4,
|
||||||
"ingest_status_updated_at": "2026-05-17T13:00:09.485818+00:00",
|
"ingest_status_updated_at": "2026-05-19T16:00:57.418443+00:00",
|
||||||
"ingest_completed_at": "",
|
"ingest_completed_at": "",
|
||||||
"ingest_document_name": "",
|
"ingest_document_name": "",
|
||||||
"ingest_document_updated_at": "",
|
"ingest_document_updated_at": "",
|
||||||
"ingest_document_sha256": "",
|
"ingest_document_sha256": "",
|
||||||
"ingest_agent_run_id": ""
|
"ingest_agent_run_id": "run_57f2d8727aaa4374"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -24,5 +24,28 @@
|
|||||||
"processing_start_time": 1779011842,
|
"processing_start_time": 1779011842,
|
||||||
"processing_end_time": 1779012093
|
"processing_end_time": 1779012093
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"a8f8465df08e455ebe133351721d49f8": {
|
||||||
|
"status": "failed",
|
||||||
|
"error_msg": "Embedding func: Worker execution timeout after 60s",
|
||||||
|
"chunks_count": 6,
|
||||||
|
"chunks_list": [
|
||||||
|
"chunk-07de6ea74f60535b689f977295770273",
|
||||||
|
"chunk-99c6f377dff2b9a37a7214b7b05ea9a8",
|
||||||
|
"chunk-1746bd83138e85e66a78e0cb9ad79272",
|
||||||
|
"chunk-ce44e4483e4119265b43eacb72e0326a",
|
||||||
|
"chunk-2187fa0609874bdda339c9850da45a26",
|
||||||
|
"chunk-2224d777c0b72d0b2dab622c79096c2c"
|
||||||
|
],
|
||||||
|
"content_summary": "# 产品需求文档\n## 文档信息\n| 项目 | 内容 |\n|------|------|\n| 项目名称 |\n无单报销\n|\n| 版本 | V1.0 |\n| 日期 | 2026-05-06 |\n| 状态 | 正式版 |\n---\n## 1. 项目概述\n### 1.1 项目背景\n面向\n大型企业,\n从业务人员视角出发,解决现有ERP使用体验不佳的问题。\n在ERP的发展历程中,“单据化”曾是财务合规的一大进步,它确保了每笔支出都有据可查。但不可否认,传统的人工填单确实\n也制造了很多\n“枷锁”。在AI时代,解...",
|
||||||
|
"content_length": 9088,
|
||||||
|
"created_at": "2026-05-19T15:59:57.283110+00:00",
|
||||||
|
"updated_at": "2026-05-19T16:00:57.323299+00:00",
|
||||||
|
"file_path": "/app/server/storage/knowledge/报销制度/a8f8465df08e455ebe133351721d49f8__无单需求文档0506.docx",
|
||||||
|
"track_id": "insert_20260519_155957_88c49850",
|
||||||
|
"metadata": {
|
||||||
|
"processing_start_time": 1779206397,
|
||||||
|
"processing_end_time": 1779206457
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -15,9 +15,8 @@ def test_rule_spreadsheet_onlyoffice_key_uses_safe_characters() -> None:
|
|||||||
|
|
||||||
key = AgentAssetService._build_onlyoffice_document_key(
|
key = AgentAssetService._build_onlyoffice_document_key(
|
||||||
"asset:id",
|
"asset:id",
|
||||||
"v1.0.0",
|
|
||||||
metadata,
|
metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert key == "asset_id-v1.0.0-abc123"
|
assert key == "asset_id-abc123"
|
||||||
assert ":" not in key
|
assert ":" not in key
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ def test_restore_version_creates_new_working_copy_without_rewriting_published_ve
|
|||||||
assert restored.current_version_change_note == "基于历史版本 v1.0.0 恢复生成工作稿"
|
assert restored.current_version_change_note == "基于历史版本 v1.0.0 恢复生成工作稿"
|
||||||
|
|
||||||
|
|
||||||
def test_spreadsheet_version_compare_returns_sheet_and_cell_changes() -> None:
|
def test_spreadsheet_upload_records_sheet_and_cell_changes_without_versions() -> None:
|
||||||
with build_session() as db:
|
with build_session() as db:
|
||||||
service = AgentAssetService(db)
|
service = AgentAssetService(db)
|
||||||
rule = next(
|
rule = next(
|
||||||
@@ -325,31 +325,30 @@ def test_spreadsheet_version_compare_returns_sheet_and_cell_changes() -> None:
|
|||||||
content=build_workbook_bytes([["城市", "住宿"], ["北京", 500]]),
|
content=build_workbook_bytes([["城市", "住宿"], ["北京", 500]]),
|
||||||
actor="finance_user",
|
actor="finance_user",
|
||||||
)
|
)
|
||||||
base_version = service.get_asset(rule.id).working_version # type: ignore[union-attr]
|
|
||||||
service.upload_rule_spreadsheet(
|
service.upload_rule_spreadsheet(
|
||||||
rule.id,
|
rule.id,
|
||||||
filename="公司差旅费报销规则.xlsx",
|
filename="公司差旅费报销规则.xlsx",
|
||||||
content=build_workbook_bytes([["城市", "住宿"], ["北京", 550], ["武汉", 450]]),
|
content=build_workbook_bytes([["城市", "住宿"], ["北京", 550], ["武汉", 450]]),
|
||||||
actor="finance_user",
|
actor="finance_user",
|
||||||
)
|
)
|
||||||
target_version = service.get_asset(rule.id).working_version # type: ignore[union-attr]
|
|
||||||
|
|
||||||
diff = service.compare_spreadsheet_versions(
|
records = service.list_spreadsheet_change_records(rule.id)
|
||||||
rule.id,
|
latest = records[0]
|
||||||
base_version=base_version or "",
|
|
||||||
target_version=target_version or "",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert diff.changed_sheet_count == 1
|
assert latest.changed_sheet_count == 1
|
||||||
assert diff.changed_cell_count == 3
|
assert latest.changed_cell_count == 3
|
||||||
assert any(
|
assert any(
|
||||||
item.cell == "B2" and item.change_type == "modified"
|
item.cell == "B2" and item.change_type == "modified"
|
||||||
for item in diff.cell_changes
|
for item in latest.cell_changes
|
||||||
)
|
)
|
||||||
assert any(item.cell == "A3" and item.change_type == "added" for item in diff.cell_changes)
|
assert any(
|
||||||
|
item.cell == "A3" and item.change_type == "added"
|
||||||
|
for item in latest.cell_changes
|
||||||
|
)
|
||||||
|
assert not hasattr(latest, "version")
|
||||||
|
|
||||||
|
|
||||||
def test_working_spreadsheet_version_reads_immutable_snapshot_instead_of_live_copy() -> None:
|
def test_spreadsheet_content_reads_current_rule_file_without_version_snapshot() -> None:
|
||||||
with build_session() as db:
|
with build_session() as db:
|
||||||
service = AgentAssetService(db)
|
service = AgentAssetService(db)
|
||||||
rule = next(
|
rule = next(
|
||||||
@@ -366,7 +365,6 @@ def test_working_spreadsheet_version_reads_immutable_snapshot_instead_of_live_co
|
|||||||
)
|
)
|
||||||
detail = service.get_asset(rule.id)
|
detail = service.get_asset(rule.id)
|
||||||
assert detail is not None
|
assert detail is not None
|
||||||
working_version = detail.working_version or ""
|
|
||||||
|
|
||||||
current_asset = service.repository.get(rule.id)
|
current_asset = service.repository.get(rule.id)
|
||||||
assert current_asset is not None
|
assert current_asset is not None
|
||||||
@@ -375,23 +373,13 @@ def test_working_spreadsheet_version_reads_immutable_snapshot_instead_of_live_co
|
|||||||
assert "agent_assets" not in live_storage_key
|
assert "agent_assets" not in live_storage_key
|
||||||
live_path = service.spreadsheet_manager.resolve_storage_path(live_storage_key)
|
live_path = service.spreadsheet_manager.resolve_storage_path(live_storage_key)
|
||||||
assert not service.spreadsheet_manager.asset_root.exists()
|
assert not service.spreadsheet_manager.asset_root.exists()
|
||||||
original_live_bytes = live_path.read_bytes()
|
|
||||||
try:
|
|
||||||
live_path.write_bytes(build_workbook_bytes([["城市", "住宿"], ["北京", 999]]))
|
|
||||||
|
|
||||||
snapshot_path, _, _ = service.get_rule_spreadsheet_content(
|
current_path, _, _ = service.get_rule_spreadsheet_content(rule.id)
|
||||||
rule.id,
|
|
||||||
version=working_version,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert snapshot_path != live_path
|
assert current_path == live_path
|
||||||
assert FINANCE_RULES_LIBRARY in snapshot_path.parts
|
assert ".versions" not in current_path.parts
|
||||||
assert ".versions" in snapshot_path.parts
|
workbook = load_workbook(current_path, data_only=False)
|
||||||
assert "agent_assets" not in snapshot_path.parts
|
|
||||||
workbook = load_workbook(snapshot_path, data_only=False)
|
|
||||||
assert workbook.active["B2"].value == 500
|
assert workbook.active["B2"].value == 500
|
||||||
finally:
|
|
||||||
live_path.write_bytes(original_live_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def test_spreadsheet_change_records_return_recent_edit_details() -> None:
|
def test_spreadsheet_change_records_return_recent_edit_details() -> None:
|
||||||
@@ -454,7 +442,6 @@ def test_spreadsheet_change_records_include_all_modified_sheets() -> None:
|
|||||||
)
|
)
|
||||||
detail = service.get_asset(rule.id)
|
detail = service.get_asset(rule.id)
|
||||||
assert detail is not None
|
assert detail is not None
|
||||||
first_version = detail.working_version
|
|
||||||
|
|
||||||
service.upload_rule_spreadsheet(
|
service.upload_rule_spreadsheet(
|
||||||
rule.id,
|
rule.id,
|
||||||
@@ -473,7 +460,7 @@ def test_spreadsheet_change_records_include_all_modified_sheets() -> None:
|
|||||||
changed_sheets = {item.sheet_name for item in latest.sheet_changes}
|
changed_sheets = {item.sheet_name for item in latest.sheet_changes}
|
||||||
changed_cell_sheets = {item.sheet_name for item in latest.cell_changes}
|
changed_cell_sheets = {item.sheet_name for item in latest.cell_changes}
|
||||||
|
|
||||||
assert latest.version != first_version
|
assert not hasattr(latest, "version")
|
||||||
assert latest.changed_sheet_count == 2
|
assert latest.changed_sheet_count == 2
|
||||||
assert {"差旅标准", "填表说明"}.issubset(changed_sheets)
|
assert {"差旅标准", "填表说明"}.issubset(changed_sheets)
|
||||||
assert {"差旅标准", "填表说明"}.issubset(changed_cell_sheets)
|
assert {"差旅标准", "填表说明"}.issubset(changed_cell_sheets)
|
||||||
@@ -513,6 +500,8 @@ def test_editable_spreadsheet_onlyoffice_config_enables_forcesave(monkeypatch) -
|
|||||||
customization = config.config["editorConfig"]["customization"]
|
customization = config.config["editorConfig"]["customization"]
|
||||||
assert config.config["editorConfig"]["mode"] == "edit"
|
assert config.config["editorConfig"]["mode"] == "edit"
|
||||||
assert customization["forcesave"] is True
|
assert customization["forcesave"] is True
|
||||||
|
assert "version=" not in config.config["document"]["url"]
|
||||||
|
assert "version=" not in config.config["editorConfig"]["callbackUrl"]
|
||||||
|
|
||||||
|
|
||||||
def test_version_timeline_contains_created_review_and_publish_events() -> None:
|
def test_version_timeline_contains_created_review_and_publish_events() -> None:
|
||||||
|
|||||||
BIN
web/UI/流程输入.png
Normal file
BIN
web/UI/流程输入.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
@@ -942,7 +942,7 @@ tbody tr.spotlight {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spreadsheet-version-center {
|
.spreadsheet-change-center {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
@@ -956,20 +956,20 @@ tbody tr.spotlight {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-center-head h3,
|
.change-center-head h3,
|
||||||
.version-center-head p,
|
.change-center-head p,
|
||||||
.version-center-section header,
|
.change-center-section header,
|
||||||
.version-center-section p {
|
.change-center-section p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-center-head h3 {
|
.change-center-head h3 {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-center-head p {
|
.change-center-head p {
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -1030,37 +1030,37 @@ tbody tr.spotlight {
|
|||||||
color: #2563eb;
|
color: #2563eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-center-section {
|
.change-center-section {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-history-section {
|
.change-history-section {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-center-section > header {
|
.change-center-section > header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-center-section > header strong {
|
.change-center-section > header strong {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-center-section > header small,
|
.change-center-section > header small,
|
||||||
.version-center-section > header button {
|
.change-center-section > header button {
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-center-section > header button {
|
.change-center-section > header button {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -1068,7 +1068,7 @@ tbody tr.spotlight {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-center-list {
|
.change-center-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -1077,7 +1077,7 @@ tbody tr.spotlight {
|
|||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-center-item {
|
.change-center-item {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@@ -1086,12 +1086,12 @@ tbody tr.spotlight {
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-center-item.active {
|
.change-center-item.active {
|
||||||
border-color: rgba(16, 185, 129, 0.35);
|
border-color: rgba(16, 185, 129, 0.35);
|
||||||
background: rgba(16, 185, 129, 0.05);
|
background: rgba(16, 185, 129, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-center-item > button {
|
.change-center-item > button {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -1101,31 +1101,31 @@ tbody tr.spotlight {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-center-item > button:disabled {
|
.change-center-item > button:disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-center-item > button div {
|
.change-center-item > button div {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-center-item > button strong {
|
.change-center-item > button strong {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-center-item > button span,
|
.change-center-item > button span,
|
||||||
.version-center-item > button p,
|
.change-center-item > button p,
|
||||||
.version-center-item > button small {
|
.change-center-item > button small {
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-center-item > button p {
|
.change-center-item > button p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
@@ -1250,7 +1250,7 @@ tbody tr.spotlight {
|
|||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-flow-empty {
|
.change-flow-empty {
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
.assistant-overlay {
|
.assistant-overlay {
|
||||||
|
/* 距屏幕边 10–18px,随视口微调;高度用 dvh 适配笔记本浏览器工具栏 */
|
||||||
|
--assistant-viewport-inset: clamp(10px, 1.25vmin, 18px);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100dvh;
|
||||||
|
max-height: 100dvh;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
display: grid;
|
display: flex;
|
||||||
place-items: center;
|
align-items: stretch;
|
||||||
|
justify-content: stretch;
|
||||||
|
padding: var(--assistant-viewport-inset);
|
||||||
|
box-sizing: border-box;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 18% 14%, rgba(16, 185, 129, 0.18), transparent 24%),
|
radial-gradient(circle at 18% 14%, rgba(16, 185, 129, 0.18), transparent 24%),
|
||||||
radial-gradient(circle at 82% 12%, rgba(59, 130, 246, 0.12), transparent 28%),
|
radial-gradient(circle at 82% 12%, rgba(59, 130, 246, 0.12), transparent 28%),
|
||||||
@@ -13,23 +21,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.assistant-modal {
|
.assistant-modal {
|
||||||
--assistant-base-width: 1430;
|
|
||||||
--assistant-base-height: 820;
|
|
||||||
--assistant-base-width-px: 1430px;
|
|
||||||
--assistant-base-height-px: 820px;
|
|
||||||
--assistant-safe-offset-x: 64;
|
|
||||||
--assistant-safe-offset-y: 48;
|
|
||||||
--assistant-fit-scale-width: calc((var(--desktop-viewport-width, 1440) - var(--assistant-safe-offset-x)) / var(--assistant-base-width));
|
|
||||||
--assistant-fit-scale-height: calc((var(--desktop-viewport-height, 900) - var(--assistant-safe-offset-y)) / var(--assistant-base-height));
|
|
||||||
--assistant-scale: min(1, var(--assistant-fit-scale-width), var(--assistant-fit-scale-height));
|
|
||||||
width: calc(var(--assistant-base-width-px) * var(--assistant-scale));
|
|
||||||
height: calc(var(--assistant-base-height-px) * var(--assistant-scale));
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 30px;
|
border-radius: 24px;
|
||||||
backdrop-filter: none;
|
backdrop-filter: none;
|
||||||
-webkit-backdrop-filter: none;
|
-webkit-backdrop-filter: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -37,14 +39,36 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.assistant-modal-stage {
|
.assistant-modal-stage {
|
||||||
|
/* 工作台字号令牌:笔记本断点见文末 @media */
|
||||||
|
--wb-fs-title: 22px;
|
||||||
|
--wb-fs-desc: 13px;
|
||||||
|
--wb-fs-badge: 12px;
|
||||||
|
--wb-fs-bubble: 14px;
|
||||||
|
--wb-fs-bubble-meta: 13px;
|
||||||
|
--wb-fs-bubble-time: 12px;
|
||||||
|
--wb-fs-chip: 12px;
|
||||||
|
--wb-fs-composer: 14px;
|
||||||
|
--wb-fs-tool-icon: 18px;
|
||||||
|
--wb-fs-md-h1: 18px;
|
||||||
|
--wb-fs-md-h2: 16px;
|
||||||
|
--wb-fs-md-h3: 14px;
|
||||||
|
--wb-fs-insight-title: 19px;
|
||||||
|
--wb-fs-insight-num: 19px;
|
||||||
|
--wb-fs-insight-body: 12px;
|
||||||
|
--wb-fs-insight-h4: 15px;
|
||||||
|
--wb-fs-metric: 13px;
|
||||||
|
--wb-fs-metric-strong: 13px;
|
||||||
|
--wb-fs-welcome: 20px;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: var(--assistant-base-width-px);
|
flex: 1;
|
||||||
height: var(--assistant-base-height-px);
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
transform: scale(var(--assistant-scale));
|
transform: none;
|
||||||
transform-origin: top left;
|
border-radius: 24px;
|
||||||
border-radius: 30px;
|
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top right, rgba(16, 185, 129, 0.14), transparent 26%),
|
radial-gradient(circle at top right, rgba(16, 185, 129, 0.14), transparent 26%),
|
||||||
radial-gradient(circle at top left, rgba(59, 130, 246, 0.10), transparent 24%),
|
radial-gradient(circle at top left, rgba(59, 130, 246, 0.10), transparent 24%),
|
||||||
@@ -64,7 +88,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 22px 172px 18px 26px;
|
flex-shrink: 0;
|
||||||
|
padding: clamp(14px, 2vh, 22px) clamp(148px, 11vw, 172px) clamp(12px, 1.6vh, 18px) clamp(18px, 2vw, 26px);
|
||||||
border-bottom: 1px solid rgba(203, 213, 225, 0.78);
|
border-bottom: 1px solid rgba(203, 213, 225, 0.78);
|
||||||
background: linear-gradient(180deg, rgba(247, 250, 249, 0.82) 0%, rgba(240, 246, 244, 0.7) 100%);
|
background: linear-gradient(180deg, rgba(247, 250, 249, 0.82) 0%, rgba(240, 246, 244, 0.7) 100%);
|
||||||
}
|
}
|
||||||
@@ -85,7 +110,7 @@
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: linear-gradient(135deg, #22c55e, #10b981);
|
background: linear-gradient(135deg, #22c55e, #10b981);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 12px;
|
font-size: var(--wb-fs-badge);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
box-shadow: 0 8px 16px rgba(16, 185, 129, 0.14);
|
box-shadow: 0 8px 16px rgba(16, 185, 129, 0.14);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -98,38 +123,39 @@
|
|||||||
|
|
||||||
.assistant-header h2 {
|
.assistant-header h2 {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 22px;
|
font-size: clamp(17px, 1.1vw, var(--wb-fs-title));
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-header p {
|
.assistant-header p {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 13px;
|
font-size: clamp(11px, 0.85vw, var(--wb-fs-desc));
|
||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-header-actions {
|
.assistant-header-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(22px * var(--assistant-scale));
|
top: 16px;
|
||||||
right: calc(26px * var(--assistant-scale));
|
right: 16px;
|
||||||
z-index: 40;
|
z-index: 60;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: calc(10px * var(--assistant-scale));
|
gap: 10px;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-toggle-btn,
|
.assistant-toggle-btn,
|
||||||
.session-trash-btn {
|
.session-trash-btn {
|
||||||
width: calc(38px * var(--assistant-scale));
|
width: 38px;
|
||||||
height: calc(38px * var(--assistant-scale));
|
height: 38px;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 1px solid rgba(248, 113, 113, 0.28);
|
border: 1px solid rgba(248, 113, 113, 0.28);
|
||||||
border-radius: calc(14px * var(--assistant-scale));
|
border-radius: 14px;
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +163,7 @@
|
|||||||
border-color: rgba(16, 185, 129, 0.18);
|
border-color: rgba(16, 185, 129, 0.18);
|
||||||
background: rgba(245, 252, 249, 0.96);
|
background: rgba(245, 252, 249, 0.96);
|
||||||
color: #166534;
|
color: #166534;
|
||||||
font-size: calc(16px * var(--assistant-scale));
|
font-size: 16px;
|
||||||
box-shadow: 0 8px 18px rgba(16, 185, 129, 0.1);
|
box-shadow: 0 8px 18px rgba(16, 185, 129, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,7 +182,7 @@
|
|||||||
.session-trash-btn {
|
.session-trash-btn {
|
||||||
background: rgba(254, 242, 242, 0.96);
|
background: rgba(254, 242, 242, 0.96);
|
||||||
color: #dc2626;
|
color: #dc2626;
|
||||||
font-size: calc(16px * var(--assistant-scale));
|
font-size: 16px;
|
||||||
box-shadow: 0 8px 18px rgba(239, 68, 68, 0.12);
|
box-shadow: 0 8px 18px rgba(239, 68, 68, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,17 +200,17 @@
|
|||||||
.assistant-close-btn,
|
.assistant-close-btn,
|
||||||
.close-btn {
|
.close-btn {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: calc(38px * var(--assistant-scale));
|
width: 38px;
|
||||||
height: calc(38px * var(--assistant-scale));
|
height: 38px;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
flex: none;
|
flex: none;
|
||||||
border: 1px solid rgba(193, 204, 216, 0.92);
|
border: 1px solid rgba(193, 204, 216, 0.92);
|
||||||
border-radius: calc(14px * var(--assistant-scale));
|
border-radius: 14px;
|
||||||
background: rgba(248, 251, 251, 0.94);
|
background: rgba(248, 251, 251, 0.94);
|
||||||
color: #475569;
|
color: #475569;
|
||||||
font-size: calc(16px * var(--assistant-scale));
|
font-size: 16px;
|
||||||
box-shadow: 0 8px 18px rgba(148, 163, 184, 0.18);
|
box-shadow: 0 8px 18px rgba(148, 163, 184, 0.18);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
@@ -193,7 +219,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.assistant-close-btn {
|
.assistant-close-btn {
|
||||||
z-index: 30;
|
z-index: 61;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,9 +236,11 @@
|
|||||||
|
|
||||||
.assistant-layout {
|
.assistant-layout {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 16px;
|
padding: clamp(12px, 1.5vw, 16px);
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
gap: clamp(12px, 1.5vw, 16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-panel,
|
.dialog-panel,
|
||||||
@@ -245,9 +273,10 @@
|
|||||||
|
|
||||||
.insight-panel-shell {
|
.insight-panel-shell {
|
||||||
flex: none;
|
flex: none;
|
||||||
width: clamp(360px, 31vw, 440px);
|
width: clamp(300px, 28vw, 420px);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
margin-left: 16px;
|
max-width: 100%;
|
||||||
|
margin-left: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition:
|
transition:
|
||||||
width 360ms cubic-bezier(0.22, 1, 0.36, 1),
|
width 360ms cubic-bezier(0.22, 1, 0.36, 1),
|
||||||
@@ -277,7 +306,7 @@
|
|||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
color: #334155;
|
color: #334155;
|
||||||
font-size: 12px;
|
font-size: var(--wb-fs-chip);
|
||||||
font-weight: 750;
|
font-weight: 750;
|
||||||
box-shadow: 0 4px 12px rgba(241, 245, 249, 0.78);
|
box-shadow: 0 4px 12px rgba(241, 245, 249, 0.78);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -365,18 +394,18 @@
|
|||||||
|
|
||||||
.message-meta strong {
|
.message-meta strong {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 13px;
|
font-size: var(--wb-fs-bubble-meta);
|
||||||
font-weight: 850;
|
font-weight: 850;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-meta time {
|
.message-meta time {
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
font-size: 12px;
|
font-size: var(--wb-fs-bubble-time);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-bubble p {
|
.message-bubble p {
|
||||||
color: #334155;
|
color: #334155;
|
||||||
font-size: 14px;
|
font-size: var(--wb-fs-bubble);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-answer-content {
|
.message-answer-content {
|
||||||
@@ -402,21 +431,34 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-answer-markdown :deep(h1) {
|
.message-answer-markdown :deep(h1) {
|
||||||
font-size: 18px;
|
font-size: var(--wb-fs-md-h1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-answer-markdown :deep(h2) {
|
.message-answer-markdown :deep(h2) {
|
||||||
font-size: 16px;
|
font-size: var(--wb-fs-md-h2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-answer-markdown :deep(h3),
|
.message-answer-markdown :deep(h3),
|
||||||
.message-answer-markdown :deep(h4) {
|
.message-answer-markdown :deep(h4) {
|
||||||
font-size: 14px;
|
font-size: var(--wb-fs-md-h3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-answer-markdown {
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: var(--wb-fs-bubble);
|
||||||
|
color: #334155;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* v-html 注入的 Markdown 节点无 scoped 标记,需用 :deep 与用户气泡 p 对齐字号 */
|
||||||
.message-answer-markdown :deep(p),
|
.message-answer-markdown :deep(p),
|
||||||
.message-answer-markdown :deep(li) {
|
.message-answer-markdown :deep(li),
|
||||||
line-height: 1.7;
|
.message-answer-markdown :deep(td),
|
||||||
|
.message-answer-markdown :deep(th),
|
||||||
|
.message-answer-markdown :deep(blockquote) {
|
||||||
|
font-size: inherit;
|
||||||
|
color: inherit;
|
||||||
|
line-height: 1.65;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-answer-markdown :deep(ul),
|
.message-answer-markdown :deep(ul),
|
||||||
@@ -462,10 +504,6 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-answer-markdown {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-answer-markdown :deep(table) {
|
.message-answer-markdown :deep(table) {
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -473,7 +511,7 @@
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||||
font-size: 13px;
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-answer-markdown :deep(th),
|
.message-answer-markdown :deep(th),
|
||||||
@@ -516,7 +554,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 12px;
|
font-size: var(--wb-fs-chip);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -960,16 +998,216 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.composer-row {
|
||||||
|
--composer-control-size: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-leading-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-date-anchor {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-btn.composer-side-btn.active {
|
||||||
|
border-color: rgba(59, 130, 246, 0.42);
|
||||||
|
background: rgba(239, 246, 255, 0.96);
|
||||||
|
color: #2563eb;
|
||||||
|
box-shadow: 0 6px 14px rgba(59, 130, 246, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-date-popover {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 10px);
|
||||||
|
left: 0;
|
||||||
|
z-index: 30;
|
||||||
|
width: min(320px, calc(100vw - 48px));
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid rgba(203, 213, 225, 0.92);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.98);
|
||||||
|
box-shadow:
|
||||||
|
0 18px 40px rgba(15, 23, 42, 0.16),
|
||||||
|
0 4px 12px rgba(15, 23, 42, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-date-mode-tabs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(241, 245, 249, 0.92);
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-date-mode-btn {
|
||||||
|
min-height: 34px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: transparent;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-date-mode-btn.active {
|
||||||
|
background: #fff;
|
||||||
|
color: #0f172a;
|
||||||
|
box-shadow: 0 4px 10px rgba(148, 163, 184, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-date-fields {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-date-fields-range {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||||
|
align-items: end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-date-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-date-field span {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-date-field input {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid rgba(203, 213, 225, 0.92);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-date-range-sep {
|
||||||
|
align-self: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-date-hint {
|
||||||
|
margin: 0;
|
||||||
|
color: #dc2626;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-date-popover-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-date-cancel-btn,
|
||||||
|
.composer-date-apply-btn {
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-date-cancel-btn {
|
||||||
|
border: 1px solid rgba(203, 213, 225, 0.92);
|
||||||
|
background: #fff;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-date-apply-btn {
|
||||||
|
border: 0;
|
||||||
|
background: linear-gradient(135deg, #22c55e, #10b981);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-date-apply-btn:disabled {
|
||||||
|
opacity: 0.48;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.composer-shell {
|
.composer-shell {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
min-height: var(--composer-control-size, 44px);
|
||||||
border: 1px solid rgba(214, 225, 234, 0.95);
|
border: 1px solid rgba(214, 225, 234, 0.95);
|
||||||
border-radius: 20px;
|
border-radius: 999px;
|
||||||
background: rgba(255, 255, 255, 0.98);
|
background: rgba(255, 255, 255, 0.98);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 10px 22px rgba(226, 232, 240, 0.24),
|
0 10px 22px rgba(226, 232, 240, 0.24),
|
||||||
0 1px 4px rgba(15, 23, 42, 0.03);
|
0 1px 4px rgba(15, 23, 42, 0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.composer-shell-body {
|
||||||
|
min-height: var(--composer-control-size, 44px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-biz-time-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
max-width: min(100%, 320px);
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0 8px 0 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.28);
|
||||||
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.14), rgba(16, 185, 129, 0.12));
|
||||||
|
color: #1d4ed8;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-biz-time-tag i {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-biz-time-tag-label {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-biz-time-tag-remove {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
color: #3b82f6;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-biz-time-tag-remove:disabled {
|
||||||
|
opacity: 0.48;
|
||||||
|
}
|
||||||
|
|
||||||
.composer-files-panel {
|
.composer-files-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -1111,15 +1349,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.composer-shell textarea {
|
.composer-shell textarea {
|
||||||
width: 100%;
|
flex: 1 1 120px;
|
||||||
min-height: 20px;
|
width: auto;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 36px;
|
||||||
|
max-height: 120px;
|
||||||
resize: none;
|
resize: none;
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 11px 14px;
|
padding: 8px 4px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 14px;
|
font-size: var(--wb-fs-composer);
|
||||||
line-height: 1.5;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-shell textarea::placeholder {
|
.composer-shell textarea::placeholder {
|
||||||
@@ -1136,7 +1377,7 @@
|
|||||||
|
|
||||||
.composer-row {
|
.composer-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1145,10 +1386,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.composer-side-btn,
|
.composer-side-btn,
|
||||||
.tool-btn,
|
.composer-row .tool-btn,
|
||||||
.send-btn {
|
.composer-row .send-btn {
|
||||||
width: 44px;
|
width: var(--composer-control-size, 44px);
|
||||||
height: 44px;
|
height: var(--composer-control-size, 44px);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -1159,7 +1400,7 @@
|
|||||||
.tool-btn {
|
.tool-btn {
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
font-size: 18px;
|
font-size: var(--wb-fs-tool-icon);
|
||||||
border: 1px solid #dbe6f0;
|
border: 1px solid #dbe6f0;
|
||||||
box-shadow: 0 4px 12px rgba(241, 245, 249, 0.76);
|
box-shadow: 0 4px 12px rgba(241, 245, 249, 0.76);
|
||||||
}
|
}
|
||||||
@@ -1172,7 +1413,7 @@
|
|||||||
.send-btn {
|
.send-btn {
|
||||||
background: linear-gradient(135deg, #22c55e, #10b981);
|
background: linear-gradient(135deg, #22c55e, #10b981);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 16px;
|
font-size: var(--wb-fs-tool-icon);
|
||||||
box-shadow: 0 8px 18px rgba(16, 185, 129, 0.18);
|
box-shadow: 0 8px 18px rgba(16, 185, 129, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1186,7 +1427,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
width: clamp(360px, 31vw, 440px);
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background:
|
background:
|
||||||
@@ -1338,7 +1579,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 13px;
|
padding: 0 13px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 12px;
|
font-size: var(--wb-fs-chip);
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1370,7 +1611,7 @@
|
|||||||
.insight-head h3 {
|
.insight-head h3 {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 19px;
|
font-size: var(--wb-fs-insight-title);
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
@@ -1378,7 +1619,7 @@
|
|||||||
.insight-head p {
|
.insight-head p {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 12px;
|
font-size: var(--wb-fs-insight-body);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1403,7 +1644,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 19px;
|
font-size: var(--wb-fs-insight-num);
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1439,7 +1680,7 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
font-size: 13px;
|
font-size: var(--wb-fs-metric);
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-side-intent-row i {
|
.review-side-intent-row i {
|
||||||
@@ -1449,7 +1690,7 @@
|
|||||||
|
|
||||||
.review-side-intent-row strong {
|
.review-side-intent-row strong {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 14px;
|
font-size: var(--wb-fs-bubble);
|
||||||
font-weight: 850;
|
font-weight: 850;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1513,7 +1754,7 @@
|
|||||||
|
|
||||||
.review-side-metric-copy strong {
|
.review-side-metric-copy strong {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 13px;
|
font-size: var(--wb-fs-metric-strong);
|
||||||
font-weight: 850;
|
font-weight: 850;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
@@ -2256,7 +2497,7 @@
|
|||||||
.welcome-card p,
|
.welcome-card p,
|
||||||
.note-block p {
|
.note-block p {
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
font-size: 13px;
|
font-size: var(--wb-fs-metric);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2279,7 +2520,7 @@
|
|||||||
.welcome-card strong,
|
.welcome-card strong,
|
||||||
.note-block strong {
|
.note-block strong {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 14px;
|
font-size: var(--wb-fs-bubble);
|
||||||
font-weight: 850;
|
font-weight: 850;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3119,7 +3360,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.review-conclusion strong {
|
.review-conclusion strong {
|
||||||
font-size: 15px;
|
font-size: var(--wb-fs-insight-h4);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3131,7 +3372,7 @@
|
|||||||
|
|
||||||
.insight-text-section h4 {
|
.insight-text-section h4 {
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 15px;
|
font-size: var(--wb-fs-insight-h4);
|
||||||
font-weight: 850;
|
font-weight: 850;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3446,7 +3687,7 @@
|
|||||||
|
|
||||||
.welcome-card i {
|
.welcome-card i {
|
||||||
color: #10b981;
|
color: #10b981;
|
||||||
font-size: 20px;
|
font-size: var(--wb-fs-welcome);
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-card strong {
|
.welcome-card strong {
|
||||||
@@ -3486,29 +3727,76 @@
|
|||||||
transform: translateY(8px);
|
transform: translateY(8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1366px), (max-height: 780px) {
|
/* 笔记本 / 中等屏:工作台正文字号整体下调一档 */
|
||||||
.insight-panel-shell {
|
@media (max-width: 1680px) {
|
||||||
width: 348px;
|
.assistant-modal-stage {
|
||||||
|
--wb-fs-title: 19px;
|
||||||
|
--wb-fs-desc: 12px;
|
||||||
|
--wb-fs-badge: 11px;
|
||||||
|
--wb-fs-bubble: 13px;
|
||||||
|
--wb-fs-bubble-meta: 12px;
|
||||||
|
--wb-fs-bubble-time: 11px;
|
||||||
|
--wb-fs-chip: 11px;
|
||||||
|
--wb-fs-composer: 13px;
|
||||||
|
--wb-fs-tool-icon: 16px;
|
||||||
|
--wb-fs-md-h1: 16px;
|
||||||
|
--wb-fs-md-h2: 15px;
|
||||||
|
--wb-fs-md-h3: 13px;
|
||||||
|
--wb-fs-insight-title: 17px;
|
||||||
|
--wb-fs-insight-num: 17px;
|
||||||
|
--wb-fs-insight-body: 11px;
|
||||||
|
--wb-fs-insight-h4: 14px;
|
||||||
|
--wb-fs-metric: 12px;
|
||||||
|
--wb-fs-metric-strong: 12px;
|
||||||
|
--wb-fs-welcome: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.insight-panel {
|
.assistant-modal-stage .message-answer-markdown :deep(table) {
|
||||||
width: 348px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.review-side-grid.compact {
|
.assistant-modal-stage .intent-pill {
|
||||||
grid-template-columns: 1fr;
|
font-size: var(--wb-fs-chip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1280px) {
|
@media (max-width: 1440px) {
|
||||||
|
.assistant-modal-stage {
|
||||||
|
--wb-fs-title: 18px;
|
||||||
|
--wb-fs-bubble: 12px;
|
||||||
|
--wb-fs-bubble-meta: 11px;
|
||||||
|
--wb-fs-composer: 12px;
|
||||||
|
--wb-fs-insight-title: 16px;
|
||||||
|
--wb-fs-insight-num: 16px;
|
||||||
|
--wb-fs-md-h1: 15px;
|
||||||
|
--wb-fs-md-h2: 14px;
|
||||||
|
--wb-fs-insight-h4: 13px;
|
||||||
|
--wb-fs-welcome: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 大屏:左右分栏;右侧详情区宽度随视口收缩 */
|
||||||
|
@media (min-width: 1441px) and (max-width: 1680px) {
|
||||||
|
.insight-panel-shell {
|
||||||
|
width: clamp(280px, 26vw, 360px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 笔记本常见宽度:改为上下布局,对话区占满宽度,避免侧栏挤占 */
|
||||||
|
@media (max-width: 1440px) {
|
||||||
.assistant-layout {
|
.assistant-layout {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog-panel {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.insight-panel-shell {
|
.insight-panel-shell {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-left: 0;
|
flex: 0 0 auto;
|
||||||
max-height: 720px;
|
max-height: min(38dvh, 400px);
|
||||||
transition:
|
transition:
|
||||||
max-height 320ms cubic-bezier(0.22, 1, 0.36, 1),
|
max-height 320ms cubic-bezier(0.22, 1, 0.36, 1),
|
||||||
opacity 240ms cubic-bezier(0.22, 1, 0.36, 1),
|
opacity 240ms cubic-bezier(0.22, 1, 0.36, 1),
|
||||||
@@ -3516,31 +3804,76 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.insight-panel-shell.collapsed {
|
.insight-panel-shell.collapsed {
|
||||||
width: 100%;
|
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.insight-panel {
|
.insight-panel {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 320px;
|
min-height: min(280px, 32dvh);
|
||||||
}
|
}
|
||||||
|
|
||||||
.insight-panel-shell.collapsed .insight-panel {
|
.insight-panel-shell.collapsed .insight-panel {
|
||||||
transform: translateY(-12px);
|
transform: translateY(-12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.review-side-grid.compact {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 矮屏笔记本(如 1366×768):压缩顶栏与间距,把高度留给对话列表 */
|
||||||
|
@media (max-height: 820px) {
|
||||||
|
.assistant-modal-stage {
|
||||||
|
--wb-fs-title: 17px;
|
||||||
|
--wb-fs-bubble: 12px;
|
||||||
|
--wb-fs-composer: 12px;
|
||||||
|
--wb-fs-insight-title: 15px;
|
||||||
|
--wb-fs-insight-num: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-header {
|
||||||
|
padding-top: 12px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-header-actions {
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant-layout {
|
||||||
|
padding: 10px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-toolbar {
|
||||||
|
padding: 12px 14px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-list {
|
||||||
|
padding: 12px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.composer-shell-body {
|
||||||
|
padding: 4px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
.insight-panel-shell:not(.collapsed) {
|
||||||
|
max-height: min(34dvh, 360px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.assistant-modal {
|
.assistant-overlay {
|
||||||
width: 100vw;
|
--assistant-viewport-inset: 10px;
|
||||||
height: 100vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assistant-modal,
|
||||||
.assistant-modal-stage {
|
.assistant-modal-stage {
|
||||||
width: 100%;
|
border-radius: 18px;
|
||||||
height: 100%;
|
|
||||||
transform: none;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.assistant-header {
|
.assistant-header {
|
||||||
@@ -3573,13 +3906,11 @@
|
|||||||
|
|
||||||
.composer-row {
|
.composer-row {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
--composer-control-size: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-side-btn,
|
.composer-shell textarea {
|
||||||
.tool-btn,
|
min-height: 32px;
|
||||||
.send-btn {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-toolbar {
|
.dialog-toolbar {
|
||||||
|
|||||||
@@ -84,16 +84,12 @@ export function fetchAgentAssetDetail(assetId) {
|
|||||||
return apiRequest(`/agent-assets/${assetId}`)
|
return apiRequest(`/agent-assets/${assetId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchAgentAssetSpreadsheetOnlyOfficeConfig(assetId, version = '') {
|
export function fetchAgentAssetSpreadsheetOnlyOfficeConfig(assetId) {
|
||||||
const query = buildQuery({ version })
|
return apiRequest(`/agent-assets/${assetId}/spreadsheet/onlyoffice-config`)
|
||||||
return apiRequest(`/agent-assets/${assetId}/spreadsheet/onlyoffice-config${query}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchAgentAssetSpreadsheetBlob(assetId, version = '', disposition = 'inline') {
|
export function fetchAgentAssetSpreadsheetBlob(assetId, disposition = 'inline') {
|
||||||
const search = new URLSearchParams()
|
const search = new URLSearchParams()
|
||||||
if (version) {
|
|
||||||
search.set('version', String(version).trim())
|
|
||||||
}
|
|
||||||
if (disposition) {
|
if (disposition) {
|
||||||
search.set('disposition', String(disposition).trim())
|
search.set('disposition', String(disposition).trim())
|
||||||
}
|
}
|
||||||
@@ -148,14 +144,6 @@ export function saveAgentAssetRuleJson(assetId, payload, options = {}) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compareAgentAssetSpreadsheetVersions(assetId, baseVersion, targetVersion) {
|
|
||||||
const query = new URLSearchParams({
|
|
||||||
base_version: String(baseVersion || '').trim(),
|
|
||||||
target_version: String(targetVersion || '').trim()
|
|
||||||
})
|
|
||||||
return apiRequest(`/agent-assets/${assetId}/versions/compare?${query.toString()}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchAgentAssetSpreadsheetChangeRecords(assetId, limit = 30) {
|
export function fetchAgentAssetSpreadsheetChangeRecords(assetId, limit = 30) {
|
||||||
return apiRequest(
|
return apiRequest(
|
||||||
`/agent-assets/${assetId}/spreadsheet/change-records${buildQuery({ limit })}`
|
`/agent-assets/${assetId}/spreadsheet/change-records${buildQuery({ limit })}`
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
|
|
||||||
<div class="spreadsheet-editor-actions">
|
<div class="spreadsheet-editor-actions">
|
||||||
<span class="spreadsheet-mode-pill">
|
<span class="spreadsheet-mode-pill">
|
||||||
{{ selectedSpreadsheetVersionModeLabel }}
|
{{ selectedSpreadsheetModeLabel }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -153,21 +153,21 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<aside class="spreadsheet-version-center">
|
<aside class="spreadsheet-change-center">
|
||||||
<header class="version-center-head">
|
<header class="change-center-head">
|
||||||
<div>
|
<div>
|
||||||
<h3>最近修改</h3>
|
<h3>最近修改</h3>
|
||||||
<p>展示最近 30 次在线编辑保存后的具体改动。</p>
|
<p>展示最近 30 次保存后的具体改动。</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="version-center-section version-history-section">
|
<section class="change-center-section change-history-section">
|
||||||
<div v-if="selectedSpreadsheetChangeRecords.length" class="version-center-list">
|
<div v-if="selectedSpreadsheetChangeRecords.length" class="change-center-list">
|
||||||
<button
|
<button
|
||||||
v-for="item in selectedSpreadsheetChangeRecords"
|
v-for="item in selectedSpreadsheetChangeRecords"
|
||||||
:key="`spreadsheet-change-${item.id || item.changed_at}-${item.actor}`"
|
:key="`spreadsheet-change-${item.id || item.changed_at}-${item.actor}`"
|
||||||
type="button"
|
type="button"
|
||||||
class="version-center-item change-record-item"
|
class="change-center-item change-record-item"
|
||||||
@click="openSpreadsheetChangeDetail(item)"
|
@click="openSpreadsheetChangeDetail(item)"
|
||||||
>
|
>
|
||||||
<div class="change-record-head">
|
<div class="change-record-head">
|
||||||
@@ -178,7 +178,6 @@
|
|||||||
<b>{{ item.changeCountLabel }}</b>
|
<b>{{ item.changeCountLabel }}</b>
|
||||||
</div>
|
</div>
|
||||||
<p>{{ item.summary }}</p>
|
<p>{{ item.summary }}</p>
|
||||||
<small v-if="item.version">关联版本:{{ item.version }}</small>
|
|
||||||
<small v-if="item.sheetPreview.length">
|
<small v-if="item.sheetPreview.length">
|
||||||
涉及工作表:{{ item.sheetPreview.join('、') }}
|
涉及工作表:{{ item.sheetPreview.join('、') }}
|
||||||
<template v-if="item.remainingSheetCount"> 等 {{ item.changedSheetNames.length }} 个</template>
|
<template v-if="item.remainingSheetCount"> 等 {{ item.changedSheetNames.length }} 个</template>
|
||||||
@@ -197,7 +196,7 @@
|
|||||||
</small>
|
</small>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-else class="version-flow-empty">暂无修改记录</p>
|
<p v-else class="change-flow-empty">暂无修改记录</p>
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
@@ -1088,8 +1087,6 @@
|
|||||||
<p>{{ item.description || item.note || '暂无补充说明' }}</p>
|
<p>{{ item.description || item.note || '暂无补充说明' }}</p>
|
||||||
<small>
|
<small>
|
||||||
操作人:{{ item.actor }}
|
操作人:{{ item.actor }}
|
||||||
<template v-if="item.version"> · 关联版本:{{ item.version }}</template>
|
|
||||||
<template v-if="item.source_version"> · 来源版本:{{ item.source_version }}</template>
|
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
@@ -1129,10 +1126,6 @@
|
|||||||
<span>修改时间</span>
|
<span>修改时间</span>
|
||||||
<strong>{{ selectedSpreadsheetChangeRecord.time }}</strong>
|
<strong>{{ selectedSpreadsheetChangeRecord.time }}</strong>
|
||||||
</article>
|
</article>
|
||||||
<article v-if="selectedSpreadsheetChangeRecord.version">
|
|
||||||
<span>关联版本</span>
|
|
||||||
<strong>{{ selectedSpreadsheetChangeRecord.version }}</strong>
|
|
||||||
</article>
|
|
||||||
<article>
|
<article>
|
||||||
<span>修改工作表</span>
|
<span>修改工作表</span>
|
||||||
<strong>{{ selectedSpreadsheetChangeRecord.changed_sheet_count }}</strong>
|
<strong>{{ selectedSpreadsheetChangeRecord.changed_sheet_count }}</strong>
|
||||||
@@ -1203,127 +1196,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
<Transition name="drawer-fade">
|
|
||||||
<div v-if="versionCompareOpen" class="rule-drawer-backdrop" @click.self="closeVersionCompare">
|
|
||||||
<aside class="rule-drawer compare-drawer">
|
|
||||||
<header class="rule-drawer-head">
|
|
||||||
<div>
|
|
||||||
<span>版本治理</span>
|
|
||||||
<h3>版本差异对比</h3>
|
|
||||||
</div>
|
|
||||||
<button type="button" @click="closeVersionCompare">
|
|
||||||
<i class="mdi mdi-close"></i>
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section class="compare-toolbar">
|
|
||||||
<label>
|
|
||||||
<span>基准版本</span>
|
|
||||||
<select v-model="compareBaseVersion" @change="loadVersionCompare">
|
|
||||||
<option
|
|
||||||
v-for="item in selectedSkill?.history || []"
|
|
||||||
:key="`base-${item.version}`"
|
|
||||||
:value="item.version"
|
|
||||||
>
|
|
||||||
{{ item.version }} · {{ item.lifecycleMeta.label }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<i class="mdi mdi-arrow-right"></i>
|
|
||||||
<label>
|
|
||||||
<span>对比版本</span>
|
|
||||||
<select v-model="compareTargetVersion" @change="loadVersionCompare">
|
|
||||||
<option
|
|
||||||
v-for="item in selectedSkill?.history || []"
|
|
||||||
:key="`target-${item.version}`"
|
|
||||||
:value="item.version"
|
|
||||||
>
|
|
||||||
{{ item.version }} · {{ item.lifecycleMeta.label }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div v-if="versionCompareLoading" class="rule-drawer-state">
|
|
||||||
<i class="mdi mdi-loading mdi-spin"></i>
|
|
||||||
<span>正在生成版本差异...</span>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="versionCompareError" class="rule-drawer-state error">
|
|
||||||
<i class="mdi mdi-alert-circle-outline"></i>
|
|
||||||
<span>{{ versionCompareError }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="versionComparePayload" class="compare-content">
|
|
||||||
<section class="compare-summary-grid">
|
|
||||||
<article>
|
|
||||||
<span>新增工作表</span>
|
|
||||||
<strong>{{ versionComparePayload.added_sheet_count }}</strong>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<span>删除工作表</span>
|
|
||||||
<strong>{{ versionComparePayload.removed_sheet_count }}</strong>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<span>修改工作表</span>
|
|
||||||
<strong>{{ versionComparePayload.changed_sheet_count }}</strong>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<span>变更单元格</span>
|
|
||||||
<strong>{{ versionComparePayload.changed_cell_count }}</strong>
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="compare-panel">
|
|
||||||
<header>
|
|
||||||
<strong>工作表变化</strong>
|
|
||||||
</header>
|
|
||||||
<div v-if="versionCompareSheetRows.length" class="compare-sheet-list">
|
|
||||||
<span
|
|
||||||
v-for="item in versionCompareSheetRows"
|
|
||||||
:key="`${item.sheet_name}-${item.change_type}`"
|
|
||||||
:class="item.meta.tone"
|
|
||||||
>
|
|
||||||
{{ item.sheet_name }} · {{ item.meta.label }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p v-else>没有新增或删除工作表。</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="compare-panel compare-cell-panel">
|
|
||||||
<header>
|
|
||||||
<strong>单元格差异</strong>
|
|
||||||
<small>最多展示前 500 条</small>
|
|
||||||
</header>
|
|
||||||
<div v-if="versionCompareCellRows.length" class="compare-table-wrap">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>工作表</th>
|
|
||||||
<th>位置</th>
|
|
||||||
<th>类型</th>
|
|
||||||
<th>旧值</th>
|
|
||||||
<th>新值</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
v-for="item in versionCompareCellRows"
|
|
||||||
:key="`${item.sheet_name}-${item.cell}`"
|
|
||||||
>
|
|
||||||
<td>{{ item.sheet_name }}</td>
|
|
||||||
<td>{{ item.cell }}</td>
|
|
||||||
<td><b :class="item.meta.tone">{{ item.meta.label }}</b></td>
|
|
||||||
<td>{{ item.before_value ?? '-' }}</td>
|
|
||||||
<td>{{ item.after_value ?? '-' }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<p v-else>两个版本内容一致,没有发现单元格级差异。</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition name="assistant-modal">
|
<Transition name="assistant-modal" @after-leave="emitCloseAfterLeave">
|
||||||
<div class="assistant-overlay">
|
<div v-if="workbenchVisible" class="assistant-overlay">
|
||||||
<section class="assistant-modal">
|
<section class="assistant-modal">
|
||||||
<div class="assistant-header-actions">
|
<div class="assistant-header-actions">
|
||||||
<button
|
<button
|
||||||
@@ -30,8 +30,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
title="关闭工作台"
|
title="关闭工作台"
|
||||||
aria-label="关闭对话工作台"
|
aria-label="关闭对话工作台"
|
||||||
@pointerdown.stop.prevent="requestCloseWorkbench"
|
@click="requestCloseWorkbench"
|
||||||
@click.stop.prevent="requestCloseWorkbench"
|
|
||||||
>
|
>
|
||||||
<i class="mdi mdi-close"></i>
|
<i class="mdi mdi-close"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -41,8 +40,8 @@
|
|||||||
<header class="assistant-header">
|
<header class="assistant-header">
|
||||||
<div class="assistant-header-main">
|
<div class="assistant-header-main">
|
||||||
<div>
|
<div>
|
||||||
<h2>财务AI工作台</h2>
|
<h2>财务助手</h2>
|
||||||
<p>个人工作台、发起报销、智能录入统一走这里,右侧会根据你的意图实时切换状态视图。</p>
|
<p>个人财务中心 · 报销识别、票据核对与制度咨询,右侧会随处理进度展示识别结果与风险提示。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -79,7 +78,7 @@
|
|||||||
|
|
||||||
<div class="message-bubble">
|
<div class="message-bubble">
|
||||||
<header class="message-meta">
|
<header class="message-meta">
|
||||||
<strong>{{ message.role === 'assistant' ? 'AI 助手' : '我' }}</strong>
|
<strong>{{ message.role === 'assistant' ? (message.assistantName || ASSISTANT_DISPLAY_NAME) : '我' }}</strong>
|
||||||
<time>{{ message.time }}</time>
|
<time>{{ message.time }}</time>
|
||||||
</header>
|
</header>
|
||||||
<p
|
<p
|
||||||
@@ -93,11 +92,31 @@
|
|||||||
v-else-if="message.text && message.role === 'assistant'"
|
v-else-if="message.text && message.role === 'assistant'"
|
||||||
class="message-answer-content message-answer-markdown"
|
class="message-answer-content message-answer-markdown"
|
||||||
v-html="renderMarkdown(message.text)"
|
v-html="renderMarkdown(message.text)"
|
||||||
></div>
|
></motion>
|
||||||
|
|
||||||
|
<motion
|
||||||
|
v-if="message.role === 'assistant' && message.welcomeQuickActions?.length"
|
||||||
|
class="welcome-quick-actions"
|
||||||
|
>
|
||||||
|
<p class="welcome-quick-actions-title">您可以对我进行以下操作:</p>
|
||||||
|
<div class="welcome-quick-action-grid">
|
||||||
|
<button
|
||||||
|
v-for="action in message.welcomeQuickActions"
|
||||||
|
:key="`${message.id}-${action.label}`"
|
||||||
|
type="button"
|
||||||
|
class="welcome-quick-action-btn"
|
||||||
|
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
||||||
|
@click="runWelcomeQuickAction(action)"
|
||||||
|
>
|
||||||
|
<i :class="action.icon"></i>
|
||||||
|
<span>{{ action.label }}</span>
|
||||||
|
</button>
|
||||||
|
</motion>
|
||||||
|
</motion>
|
||||||
|
|
||||||
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.meta?.length" class="message-meta-row">
|
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.meta?.length" class="message-meta-row">
|
||||||
<span v-for="item in message.meta" :key="item" class="message-meta-chip">{{ item }}</span>
|
<span v-for="item in message.meta" :key="item" class="message-meta-chip">{{ item }}</span>
|
||||||
</div>
|
</motion>
|
||||||
|
|
||||||
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.riskFlags?.length" class="message-detail-block">
|
<div v-if="message.role === 'assistant' && !message.reviewPayload && message.riskFlags?.length" class="message-detail-block">
|
||||||
<strong>风险标签</strong>
|
<strong>风险标签</strong>
|
||||||
@@ -409,8 +428,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="composer-row" :class="{ 'knowledge-mode': isKnowledgeSession }">
|
<div class="composer-row" :class="{ 'knowledge-mode': isKnowledgeSession }">
|
||||||
|
<div v-if="!isKnowledgeSession" class="composer-leading-actions">
|
||||||
<button
|
<button
|
||||||
v-if="!isKnowledgeSession"
|
|
||||||
type="button"
|
type="button"
|
||||||
class="tool-btn composer-side-btn"
|
class="tool-btn composer-side-btn"
|
||||||
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
||||||
@@ -419,8 +438,99 @@
|
|||||||
>
|
>
|
||||||
<i class="mdi mdi-paperclip"></i>
|
<i class="mdi mdi-paperclip"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<div class="composer-date-anchor">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="tool-btn composer-side-btn"
|
||||||
|
:class="{ active: composerDatePickerOpen }"
|
||||||
|
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
||||||
|
aria-label="选择业务发生时间"
|
||||||
|
:aria-expanded="composerDatePickerOpen"
|
||||||
|
@click.stop="toggleComposerDatePicker"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-calendar-range"></i>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="composerDatePickerOpen"
|
||||||
|
class="composer-date-popover"
|
||||||
|
role="dialog"
|
||||||
|
aria-label="业务发生时间"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<div class="composer-date-mode-tabs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="composer-date-mode-btn"
|
||||||
|
:class="{ active: composerDateMode === 'single' }"
|
||||||
|
@click="setComposerDateMode('single')"
|
||||||
|
>
|
||||||
|
当天
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="composer-date-mode-btn"
|
||||||
|
:class="{ active: composerDateMode === 'range' }"
|
||||||
|
@click="setComposerDateMode('range')"
|
||||||
|
>
|
||||||
|
时间段
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="composerDateMode === 'single'" class="composer-date-fields">
|
||||||
|
<label class="composer-date-field">
|
||||||
|
<span>日期</span>
|
||||||
|
<input v-model="composerSingleDate" type="date" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-else class="composer-date-fields composer-date-fields-range">
|
||||||
|
<label class="composer-date-field">
|
||||||
|
<span>开始</span>
|
||||||
|
<input v-model="composerRangeStartDate" type="date" />
|
||||||
|
</label>
|
||||||
|
<span class="composer-date-range-sep">至</span>
|
||||||
|
<label class="composer-date-field">
|
||||||
|
<span>结束</span>
|
||||||
|
<input v-model="composerRangeEndDate" type="date" :min="composerRangeStartDate" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p v-if="composerDateMode === 'range' && !composerCanApplyDateSelection" class="composer-date-hint">
|
||||||
|
请确认结束日期不早于开始日期。
|
||||||
|
</p>
|
||||||
|
<div class="composer-date-popover-actions">
|
||||||
|
<button type="button" class="composer-date-cancel-btn" @click="closeComposerDatePicker">
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="composer-date-apply-btn"
|
||||||
|
:disabled="!composerCanApplyDateSelection"
|
||||||
|
@click="applyComposerDateSelection"
|
||||||
|
>
|
||||||
|
插入标签
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="composer-shell">
|
<div class="composer-shell">
|
||||||
|
<div class="composer-shell-body">
|
||||||
|
<span
|
||||||
|
v-for="tag in composerBusinessTimeTags"
|
||||||
|
:key="tag.id"
|
||||||
|
class="composer-biz-time-tag"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-calendar-check"></i>
|
||||||
|
<span class="composer-biz-time-tag-label">{{ tag.label }}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="composer-biz-time-tag-remove"
|
||||||
|
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
||||||
|
aria-label="移除业务发生时间"
|
||||||
|
@click="removeComposerBusinessTimeTag(tag.id)"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-close"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
<textarea
|
<textarea
|
||||||
ref="composerTextareaRef"
|
ref="composerTextareaRef"
|
||||||
v-model="composerDraft"
|
v-model="composerDraft"
|
||||||
@@ -432,6 +542,7 @@
|
|||||||
@keydown.ctrl.enter.prevent="submitComposer"
|
@keydown.ctrl.enter.prevent="submitComposer"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="send-btn composer-side-btn" type="submit" :disabled="!canSubmit || reviewActionBusy || sessionSwitchBusy" aria-label="发送">
|
<button class="send-btn composer-side-btn" type="submit" :disabled="!canSubmit || reviewActionBusy || sessionSwitchBusy" aria-label="发送">
|
||||||
<i :class="submitting ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-send'"></i>
|
<i :class="submitting ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-send'"></i>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { useSystemState } from '../../composables/useSystemState.js'
|
|||||||
import { useToast } from '../../composables/useToast.js'
|
import { useToast } from '../../composables/useToast.js'
|
||||||
import {
|
import {
|
||||||
activateAgentAsset,
|
activateAgentAsset,
|
||||||
compareAgentAssetSpreadsheetVersions,
|
|
||||||
createAgentAssetReview,
|
createAgentAssetReview,
|
||||||
createAgentAssetVersion,
|
createAgentAssetVersion,
|
||||||
fetchAgentAssetDetail,
|
fetchAgentAssetDetail,
|
||||||
@@ -969,6 +968,17 @@ function buildRowMetric(asset, typeKey) {
|
|||||||
return normalizeText(asset.config_json?.agent) || '未配置 Agent'
|
return normalizeText(asset.config_json?.agent) || '未配置 Agent'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatSpreadsheetChangeSummary(summary) {
|
||||||
|
const normalized = normalizeText(summary)
|
||||||
|
return (
|
||||||
|
normalized
|
||||||
|
.replace(/^(ONLYOFFICE\s*)?在线编辑[::]\s*/i, '')
|
||||||
|
.replace(/^ONLYOFFICE\s*在线编辑保存[。.]?\s*/i, '')
|
||||||
|
.replace(/^保存表格[::]\s*/i, '')
|
||||||
|
.trim() || '表格内容已保存。'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function buildListItem(asset) {
|
function buildListItem(asset) {
|
||||||
const typeKey = resolveTypeKey(asset.asset_type)
|
const typeKey = resolveTypeKey(asset.asset_type)
|
||||||
const tabId = resolveTabId(asset, typeKey)
|
const tabId = resolveTabId(asset, typeKey)
|
||||||
@@ -993,6 +1003,9 @@ function buildListItem(asset) {
|
|||||||
: ''
|
: ''
|
||||||
)
|
)
|
||||||
const isRiskRule = tabId === 'riskRules'
|
const isRiskRule = tabId === 'riskRules'
|
||||||
|
const usesSpreadsheetRule = typeKey === 'rules' && isSpreadsheetRuleSource(asset)
|
||||||
|
const usesJsonRiskRule = typeKey === 'rules' && isJsonRiskRuleSource(asset)
|
||||||
|
const ruleDocument = readRuleDocumentMeta(asset)
|
||||||
const riskCategory = isRiskRule ? resolveRiskRuleCategory(asset) : ''
|
const riskCategory = isRiskRule ? resolveRiskRuleCategory(asset) : ''
|
||||||
const listSubtitle = isRiskRule
|
const listSubtitle = isRiskRule
|
||||||
? buildRiskListSubtitle(asset.description)
|
? buildRiskListSubtitle(asset.description)
|
||||||
@@ -1003,6 +1016,9 @@ function buildListItem(asset) {
|
|||||||
tabId,
|
tabId,
|
||||||
type: typeKey,
|
type: typeKey,
|
||||||
isPreviewMock: Boolean(asset.isPreviewMock),
|
isPreviewMock: Boolean(asset.isPreviewMock),
|
||||||
|
usesSpreadsheetRule,
|
||||||
|
usesJsonRiskRule,
|
||||||
|
ruleDocument,
|
||||||
typeLabel: tabMeta.typeLabel,
|
typeLabel: tabMeta.typeLabel,
|
||||||
short: makeShort(asset.name),
|
short: makeShort(asset.name),
|
||||||
name: asset.name,
|
name: asset.name,
|
||||||
@@ -1582,12 +1598,6 @@ export default {
|
|||||||
const versionTimelineLoading = ref(false)
|
const versionTimelineLoading = ref(false)
|
||||||
const versionTimelineError = ref('')
|
const versionTimelineError = ref('')
|
||||||
const versionTimelineItems = ref([])
|
const versionTimelineItems = ref([])
|
||||||
const versionCompareOpen = ref(false)
|
|
||||||
const versionCompareLoading = ref(false)
|
|
||||||
const versionCompareError = ref('')
|
|
||||||
const versionComparePayload = ref(null)
|
|
||||||
const compareBaseVersion = ref('')
|
|
||||||
const compareTargetVersion = ref('')
|
|
||||||
const spreadsheetChangeRecordsByAsset = ref({})
|
const spreadsheetChangeRecordsByAsset = ref({})
|
||||||
const spreadsheetChangeDetailOpen = ref(false)
|
const spreadsheetChangeDetailOpen = ref(false)
|
||||||
const selectedSpreadsheetChangeRecord = ref(null)
|
const selectedSpreadsheetChangeRecord = ref(null)
|
||||||
@@ -1595,8 +1605,7 @@ export default {
|
|||||||
let spreadsheetOnlyOfficeLoadTimer = null
|
let spreadsheetOnlyOfficeLoadTimer = null
|
||||||
let spreadsheetOnlyOfficeHadLocalEdits = false
|
let spreadsheetOnlyOfficeHadLocalEdits = false
|
||||||
let spreadsheetOnlyOfficeSyncSeq = 0
|
let spreadsheetOnlyOfficeSyncSeq = 0
|
||||||
let spreadsheetOnlyOfficeVersionPollTimer = null
|
let spreadsheetOnlyOfficeChangePollTimer = null
|
||||||
let spreadsheetOnlyOfficeRefreshTimer = null
|
|
||||||
const assetBuckets = ref({
|
const assetBuckets = ref({
|
||||||
financialRules: [],
|
financialRules: [],
|
||||||
riskRules: [],
|
riskRules: [],
|
||||||
@@ -1649,8 +1658,7 @@ export default {
|
|||||||
() =>
|
() =>
|
||||||
canEditSelected.value &&
|
canEditSelected.value &&
|
||||||
selectedSkillUsesSpreadsheet.value &&
|
selectedSkillUsesSpreadsheet.value &&
|
||||||
!detailBusy.value &&
|
!detailBusy.value
|
||||||
selectedSkill.value?.displayVersion === selectedSkill.value?.workingVersion
|
|
||||||
)
|
)
|
||||||
const canDownloadSpreadsheet = computed(
|
const canDownloadSpreadsheet = computed(
|
||||||
() =>
|
() =>
|
||||||
@@ -1661,26 +1669,17 @@ export default {
|
|||||||
const canEditSpreadsheetInline = computed(
|
const canEditSpreadsheetInline = computed(
|
||||||
() =>
|
() =>
|
||||||
selectedSkillUsesSpreadsheet.value &&
|
selectedSkillUsesSpreadsheet.value &&
|
||||||
selectedSkill.value?.displayVersion === selectedSkill.value?.workingVersion &&
|
|
||||||
(selectedSkill.value?.isPreviewMock || canEditSelected.value)
|
(selectedSkill.value?.isPreviewMock || canEditSelected.value)
|
||||||
)
|
)
|
||||||
const selectedDisplayHistory = computed(
|
|
||||||
() =>
|
|
||||||
selectedSkill.value?.history?.find((item) => item.version === selectedSkill.value?.displayVersion) || null
|
|
||||||
)
|
|
||||||
const selectedSpreadsheetFileName = computed(
|
const selectedSpreadsheetFileName = computed(
|
||||||
() =>
|
() =>
|
||||||
normalizeText(
|
normalizeText(selectedSkill.value?.ruleDocument?.file_name) || '未上传规则表'
|
||||||
selectedDisplayHistory.value?.spreadsheetMeta?.file_name || selectedSkill.value?.ruleDocument?.file_name
|
|
||||||
) || '未上传规则表'
|
|
||||||
)
|
)
|
||||||
const selectedSpreadsheetVersionModeLabel = computed(() => {
|
const selectedSpreadsheetModeLabel = computed(() => {
|
||||||
if (selectedSkill.value?.isPreviewMock) {
|
if (selectedSkill.value?.isPreviewMock) {
|
||||||
return canEditSpreadsheetInline.value ? 'ONLYOFFICE 可编辑' : 'ONLYOFFICE 预览'
|
return canEditSpreadsheetInline.value ? '可编辑' : '只读'
|
||||||
}
|
}
|
||||||
return selectedSkill.value?.displayVersion === selectedSkill.value?.workingVersion
|
return canEditSpreadsheetInline.value ? '在线可编辑' : '只读'
|
||||||
? '在线可编辑'
|
|
||||||
: '只读预览'
|
|
||||||
})
|
})
|
||||||
const selectedVersionTimelineItems = computed(() =>
|
const selectedVersionTimelineItems = computed(() =>
|
||||||
versionTimelineItems.value.map((item) => ({
|
versionTimelineItems.value.map((item) => ({
|
||||||
@@ -1709,6 +1708,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
time: formatDateTime(item.changed_at),
|
time: formatDateTime(item.changed_at),
|
||||||
|
summary: formatSpreadsheetChangeSummary(item.summary),
|
||||||
changeCountLabel: item.changed_cell_count
|
changeCountLabel: item.changed_cell_count
|
||||||
? `${item.changed_cell_count} 处改动`
|
? `${item.changed_cell_count} 处改动`
|
||||||
: `${item.changed_sheet_count || changedSheetNames.length || 0} 个工作表`,
|
: `${item.changed_sheet_count || changedSheetNames.length || 0} 个工作表`,
|
||||||
@@ -1736,22 +1736,6 @@ export default {
|
|||||||
}))
|
}))
|
||||||
: []
|
: []
|
||||||
)
|
)
|
||||||
const versionCompareCellRows = computed(() =>
|
|
||||||
Array.isArray(versionComparePayload.value?.cell_changes)
|
|
||||||
? versionComparePayload.value.cell_changes.map((item) => ({
|
|
||||||
...item,
|
|
||||||
meta: resolveDiffChangeMeta(item.change_type)
|
|
||||||
}))
|
|
||||||
: []
|
|
||||||
)
|
|
||||||
const versionCompareSheetRows = computed(() =>
|
|
||||||
Array.isArray(versionComparePayload.value?.sheet_changes)
|
|
||||||
? versionComparePayload.value.sheet_changes.map((item) => ({
|
|
||||||
...item,
|
|
||||||
meta: resolveDiffChangeMeta(item.change_type)
|
|
||||||
}))
|
|
||||||
: []
|
|
||||||
)
|
|
||||||
const detailBusy = computed(() => Boolean(actionState.value))
|
const detailBusy = computed(() => Boolean(actionState.value))
|
||||||
const showReviewNote = computed(
|
const showReviewNote = computed(
|
||||||
() => selectedSkillIsRule.value && (selectedSkill.value?.reviewNote || selectedSkill.value?.reviewTimeLabel)
|
() => selectedSkillIsRule.value && (selectedSkill.value?.reviewNote || selectedSkill.value?.reviewTimeLabel)
|
||||||
@@ -1922,7 +1906,6 @@ export default {
|
|||||||
watch(
|
watch(
|
||||||
() => [
|
() => [
|
||||||
selectedSkill.value?.id || '',
|
selectedSkill.value?.id || '',
|
||||||
selectedSkill.value?.displayVersion || '',
|
|
||||||
selectedSkill.value?.loading ? '1' : '0',
|
selectedSkill.value?.loading ? '1' : '0',
|
||||||
selectedSkill.value?.usesSpreadsheetRule ? '1' : '0'
|
selectedSkill.value?.usesSpreadsheetRule ? '1' : '0'
|
||||||
],
|
],
|
||||||
@@ -1938,7 +1921,6 @@ export default {
|
|||||||
)
|
)
|
||||||
|
|
||||||
watch(activeType, () => {
|
watch(activeType, () => {
|
||||||
stopSpreadsheetOnlyOfficeDeferredRefresh()
|
|
||||||
destroySpreadsheetOnlyOfficeEditor()
|
destroySpreadsheetOnlyOfficeEditor()
|
||||||
selectedSkill.value = null
|
selectedSkill.value = null
|
||||||
versionSwitchTarget.value = null
|
versionSwitchTarget.value = null
|
||||||
@@ -2034,8 +2016,7 @@ export default {
|
|||||||
window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
|
window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
|
||||||
spreadsheetOnlyOfficeLoadTimer = null
|
spreadsheetOnlyOfficeLoadTimer = null
|
||||||
}
|
}
|
||||||
stopSpreadsheetOnlyOfficeVersionSync()
|
stopSpreadsheetOnlyOfficeChangeSync()
|
||||||
clearSpreadsheetPendingChangeRecord(selectedSkill.value?.id, selectedSkill.value?.displayVersion)
|
|
||||||
spreadsheetOnlyOfficeHadLocalEdits = false
|
spreadsheetOnlyOfficeHadLocalEdits = false
|
||||||
spreadsheetOnlyOfficeSyncSeq += 1
|
spreadsheetOnlyOfficeSyncSeq += 1
|
||||||
if (spreadsheetOnlyOfficeEditor.value?.destroyEditor) {
|
if (spreadsheetOnlyOfficeEditor.value?.destroyEditor) {
|
||||||
@@ -2045,87 +2026,10 @@ export default {
|
|||||||
spreadsheetOnlyOfficeReady.value = false
|
spreadsheetOnlyOfficeReady.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendSpreadsheetChangeRecord(record) {
|
function stopSpreadsheetOnlyOfficeChangeSync() {
|
||||||
const assetId = normalizeText(record?.assetId)
|
if (spreadsheetOnlyOfficeChangePollTimer) {
|
||||||
const version = normalizeText(record?.version)
|
window.clearTimeout(spreadsheetOnlyOfficeChangePollTimer)
|
||||||
if (!assetId || !version) {
|
spreadsheetOnlyOfficeChangePollTimer = null
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextRecord = {
|
|
||||||
version,
|
|
||||||
operationLabel: normalizeText(record?.operationLabel) || '表格修改',
|
|
||||||
operationActor: normalizeText(record?.operationActor) || resolveActor(),
|
|
||||||
note: normalizeText(record?.note) || '用户修改了表格内容。',
|
|
||||||
time: record?.time || new Date().toISOString(),
|
|
||||||
isWorking: record?.isWorking !== false,
|
|
||||||
isPendingLocalEdit: Boolean(record?.isPendingLocalEdit),
|
|
||||||
disabledReason: normalizeText(record?.disabledReason)
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = spreadsheetChangeRecordsByAsset.value[assetId] || []
|
|
||||||
const deduped = current.filter(
|
|
||||||
(item) =>
|
|
||||||
!(
|
|
||||||
item.version === nextRecord.version &&
|
|
||||||
item.operationLabel === nextRecord.operationLabel &&
|
|
||||||
item.note === nextRecord.note
|
|
||||||
)
|
|
||||||
)
|
|
||||||
spreadsheetChangeRecordsByAsset.value = {
|
|
||||||
...spreadsheetChangeRecordsByAsset.value,
|
|
||||||
[assetId]: [nextRecord, ...deduped].slice(0, 30)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearSpreadsheetPendingChangeRecord(assetId, version) {
|
|
||||||
const normalizedAssetId = normalizeText(assetId)
|
|
||||||
const normalizedVersion = normalizeText(version)
|
|
||||||
if (!normalizedAssetId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = spreadsheetChangeRecordsByAsset.value[normalizedAssetId] || []
|
|
||||||
spreadsheetChangeRecordsByAsset.value = {
|
|
||||||
...spreadsheetChangeRecordsByAsset.value,
|
|
||||||
[normalizedAssetId]: current.filter(
|
|
||||||
(item) => !(item.isPendingLocalEdit && (!normalizedVersion || item.version === normalizedVersion))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function markSpreadsheetPendingChange(assetId, version) {
|
|
||||||
const normalizedAssetId = normalizeText(assetId)
|
|
||||||
const normalizedVersion = normalizeText(version)
|
|
||||||
if (!normalizedAssetId || !normalizedVersion) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
clearSpreadsheetPendingChangeRecord(normalizedAssetId, normalizedVersion)
|
|
||||||
appendSpreadsheetChangeRecord({
|
|
||||||
assetId: normalizedAssetId,
|
|
||||||
version: normalizedVersion,
|
|
||||||
operationLabel: '编辑中',
|
|
||||||
operationActor: resolveActor(),
|
|
||||||
note: '检测到未保存的表格改动,保存后会生成新版本并可查看差异。',
|
|
||||||
time: new Date().toISOString(),
|
|
||||||
isWorking: true,
|
|
||||||
isPendingLocalEdit: true,
|
|
||||||
disabledReason: '当前是本地未保存修改,保存后才会生成可对比的版本。'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopSpreadsheetOnlyOfficeVersionSync() {
|
|
||||||
if (spreadsheetOnlyOfficeVersionPollTimer) {
|
|
||||||
window.clearTimeout(spreadsheetOnlyOfficeVersionPollTimer)
|
|
||||||
spreadsheetOnlyOfficeVersionPollTimer = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopSpreadsheetOnlyOfficeDeferredRefresh() {
|
|
||||||
if (spreadsheetOnlyOfficeRefreshTimer) {
|
|
||||||
window.clearTimeout(spreadsheetOnlyOfficeRefreshTimer)
|
|
||||||
spreadsheetOnlyOfficeRefreshTimer = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2164,7 +2068,6 @@ export default {
|
|||||||
latest.id,
|
latest.id,
|
||||||
latest.changed_at,
|
latest.changed_at,
|
||||||
latest.actor,
|
latest.actor,
|
||||||
latest.version,
|
|
||||||
latest.summary,
|
latest.summary,
|
||||||
latest.changed_sheet_count,
|
latest.changed_sheet_count,
|
||||||
latest.changed_cell_count,
|
latest.changed_cell_count,
|
||||||
@@ -2193,36 +2096,14 @@ export default {
|
|||||||
return refreshSpreadsheetChangeRecordsAfterSave(normalizedAssetId, previousLatestKey, attempt + 1)
|
return refreshSpreadsheetChangeRecordsAfterSave(normalizedAssetId, previousLatestKey, attempt + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleSpreadsheetEditorRefreshAfterSave(assetId, savedVersion) {
|
function scheduleSpreadsheetOnlyOfficeChangeSync(assetId, attempt = 0) {
|
||||||
const normalizedAssetId = normalizeText(assetId)
|
const normalizedAssetId = normalizeText(assetId)
|
||||||
const normalizedSavedVersion = normalizeText(savedVersion)
|
if (!normalizedAssetId) {
|
||||||
if (!normalizedAssetId || !normalizedSavedVersion) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stopSpreadsheetOnlyOfficeDeferredRefresh()
|
|
||||||
spreadsheetOnlyOfficeRefreshTimer = window.setTimeout(async () => {
|
|
||||||
spreadsheetOnlyOfficeRefreshTimer = null
|
|
||||||
if (
|
|
||||||
selectedSkill.value?.id !== normalizedAssetId ||
|
|
||||||
selectedSkill.value?.displayVersion === normalizedSavedVersion
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadSelectedAssetDetail(normalizedAssetId)
|
|
||||||
}, 3200)
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleSpreadsheetOnlyOfficeVersionSync(assetId, version, attempt = 0) {
|
|
||||||
const normalizedAssetId = normalizeText(assetId)
|
|
||||||
const normalizedVersion = normalizeText(version)
|
|
||||||
if (!normalizedAssetId || !normalizedVersion) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const syncSeq = ++spreadsheetOnlyOfficeSyncSeq
|
const syncSeq = ++spreadsheetOnlyOfficeSyncSeq
|
||||||
stopSpreadsheetOnlyOfficeVersionSync()
|
stopSpreadsheetOnlyOfficeChangeSync()
|
||||||
const previousLatestChangeKey = getLatestSpreadsheetChangeKey(normalizedAssetId)
|
const previousLatestChangeKey = getLatestSpreadsheetChangeKey(normalizedAssetId)
|
||||||
|
|
||||||
const runSync = async () => {
|
const runSync = async () => {
|
||||||
@@ -2231,31 +2112,13 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const detail = await fetchAgentAssetDetail(normalizedAssetId)
|
|
||||||
const nextWorkingVersion = normalizeText(detail?.working_version || detail?.current_version)
|
|
||||||
if (nextWorkingVersion && nextWorkingVersion !== normalizedVersion) {
|
|
||||||
clearSpreadsheetPendingChangeRecord(normalizedAssetId, normalizedVersion)
|
|
||||||
await refreshCurrentAssets()
|
|
||||||
await refreshSpreadsheetChangeRecordsAfterSave(normalizedAssetId, previousLatestChangeKey)
|
|
||||||
if (syncSeq !== spreadsheetOnlyOfficeSyncSeq || selectedSkill.value?.id !== normalizedAssetId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// ONLYOFFICE 的保存回调刚结束时立即销毁并重挂编辑器,偶发会让新文档会话
|
|
||||||
// 还没完全就绪就被再次打开,表现为“加载超时”。先刷新右侧修改记录,再留
|
|
||||||
// 一个很短的缓冲窗口后切换到新工作版本,用户无需退出重进。
|
|
||||||
scheduleSpreadsheetEditorRefreshAfterSave(normalizedAssetId, nextWorkingVersion)
|
|
||||||
stopSpreadsheetOnlyOfficeVersionSync()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const changeRecordRefreshed = await refreshSpreadsheetChangeRecordsAfterSave(
|
const changeRecordRefreshed = await refreshSpreadsheetChangeRecordsAfterSave(
|
||||||
normalizedAssetId,
|
normalizedAssetId,
|
||||||
previousLatestChangeKey
|
previousLatestChangeKey
|
||||||
)
|
)
|
||||||
if (changeRecordRefreshed) {
|
if (changeRecordRefreshed) {
|
||||||
clearSpreadsheetPendingChangeRecord(normalizedAssetId, normalizedVersion)
|
|
||||||
await refreshCurrentAssets()
|
await refreshCurrentAssets()
|
||||||
stopSpreadsheetOnlyOfficeVersionSync()
|
stopSpreadsheetOnlyOfficeChangeSync()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -2268,22 +2131,21 @@ export default {
|
|||||||
if (attempt >= 29) {
|
if (attempt >= 29) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
spreadsheetOnlyOfficeVersionPollTimer = window.setTimeout(() => {
|
spreadsheetOnlyOfficeChangePollTimer = window.setTimeout(() => {
|
||||||
scheduleSpreadsheetOnlyOfficeVersionSync(normalizedAssetId, normalizedVersion, attempt + 1)
|
scheduleSpreadsheetOnlyOfficeChangeSync(normalizedAssetId, attempt + 1)
|
||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
spreadsheetOnlyOfficeVersionPollTimer = window.setTimeout(() => {
|
spreadsheetOnlyOfficeChangePollTimer = window.setTimeout(() => {
|
||||||
runSync().catch(() => {})
|
runSync().catch(() => {})
|
||||||
}, attempt === 0 ? 800 : 2000)
|
}, attempt === 0 ? 800 : 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version) {
|
function isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId) {
|
||||||
return (
|
return (
|
||||||
mountSeq !== spreadsheetOnlyOfficeMountSeq ||
|
mountSeq !== spreadsheetOnlyOfficeMountSeq ||
|
||||||
!selectedSkillUsesSpreadsheet.value ||
|
!selectedSkillUsesSpreadsheet.value ||
|
||||||
selectedSkill.value?.id !== assetId ||
|
selectedSkill.value?.id !== assetId ||
|
||||||
selectedSkill.value?.displayVersion !== version ||
|
|
||||||
selectedSkill.value?.loading
|
selectedSkill.value?.loading
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2296,7 +2158,6 @@ export default {
|
|||||||
|
|
||||||
const mountSeq = ++spreadsheetOnlyOfficeMountSeq
|
const mountSeq = ++spreadsheetOnlyOfficeMountSeq
|
||||||
const assetId = selectedSkill.value.id
|
const assetId = selectedSkill.value.id
|
||||||
const version = selectedSkill.value.displayVersion
|
|
||||||
const editable = canEditSpreadsheetInline.value
|
const editable = canEditSpreadsheetInline.value
|
||||||
|
|
||||||
spreadsheetOnlyOfficeLoading.value = true
|
spreadsheetOnlyOfficeLoading.value = true
|
||||||
@@ -2305,25 +2166,25 @@ export default {
|
|||||||
destroySpreadsheetOnlyOfficeEditor()
|
destroySpreadsheetOnlyOfficeEditor()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = await fetchAgentAssetSpreadsheetOnlyOfficeConfig(assetId, version)
|
const payload = await fetchAgentAssetSpreadsheetOnlyOfficeConfig(assetId)
|
||||||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)) {
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadOnlyOfficeApi(payload.documentServerUrl)
|
await loadOnlyOfficeApi(payload.documentServerUrl)
|
||||||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)) {
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!window.DocsAPI?.DocEditor) {
|
if (!window.DocsAPI?.DocEditor) {
|
||||||
throw new Error('ONLYOFFICE 编辑器未正确加载。')
|
throw new Error('表格编辑器未正确加载。')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host id must be unique for every mount. ONLYOFFICE mutates its host DOM
|
// Host id must be unique for every mount. ONLYOFFICE mutates its host DOM
|
||||||
// during lifecycle teardown; reusing the same element can leave the next
|
// during lifecycle teardown; reusing the same element can leave the next
|
||||||
// DocEditor instance with a dead container even though config loading succeeds.
|
// DocEditor instance with a dead container even though config loading succeeds.
|
||||||
spreadsheetOnlyOfficeHostId.value = `audit-rule-onlyoffice-${assetId}-${version}-${mountSeq}`
|
spreadsheetOnlyOfficeHostId.value = `audit-rule-onlyoffice-${assetId}-${mountSeq}`
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)) {
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2334,7 +2195,7 @@ export default {
|
|||||||
})
|
})
|
||||||
const upstreamEvents = config.events || {}
|
const upstreamEvents = config.events || {}
|
||||||
spreadsheetOnlyOfficeLoadTimer = window.setTimeout(() => {
|
spreadsheetOnlyOfficeLoadTimer = window.setTimeout(() => {
|
||||||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)) {
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (retryAttempt < 1) {
|
if (retryAttempt < 1) {
|
||||||
@@ -2345,14 +2206,14 @@ export default {
|
|||||||
}, 600)
|
}, 600)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
spreadsheetOnlyOfficeError.value = 'ONLYOFFICE 加载超时,请重新切换版本后重试。'
|
spreadsheetOnlyOfficeError.value = '表格加载超时,请退出详情后重试。'
|
||||||
spreadsheetOnlyOfficeLoading.value = false
|
spreadsheetOnlyOfficeLoading.value = false
|
||||||
destroySpreadsheetOnlyOfficeEditor()
|
destroySpreadsheetOnlyOfficeEditor()
|
||||||
}, 15000)
|
}, 15000)
|
||||||
config.events = {
|
config.events = {
|
||||||
...upstreamEvents,
|
...upstreamEvents,
|
||||||
onAppReady(event) {
|
onAppReady(event) {
|
||||||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)) {
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (spreadsheetOnlyOfficeLoadTimer) {
|
if (spreadsheetOnlyOfficeLoadTimer) {
|
||||||
@@ -2364,7 +2225,7 @@ export default {
|
|||||||
upstreamEvents.onAppReady?.(event)
|
upstreamEvents.onAppReady?.(event)
|
||||||
},
|
},
|
||||||
onError(event) {
|
onError(event) {
|
||||||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)) {
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (spreadsheetOnlyOfficeLoadTimer) {
|
if (spreadsheetOnlyOfficeLoadTimer) {
|
||||||
@@ -2374,8 +2235,8 @@ export default {
|
|||||||
const errorCode = event?.data?.errorCode
|
const errorCode = event?.data?.errorCode
|
||||||
const errorDescription = event?.data?.errorDescription
|
const errorDescription = event?.data?.errorDescription
|
||||||
spreadsheetOnlyOfficeError.value = errorDescription
|
spreadsheetOnlyOfficeError.value = errorDescription
|
||||||
? `ONLYOFFICE 加载失败:${errorDescription}`
|
? `表格加载失败:${errorDescription}`
|
||||||
: `ONLYOFFICE 加载失败${errorCode ? `(错误码 ${errorCode})` : '。'}`
|
: `表格加载失败${errorCode ? `(错误码 ${errorCode})` : '。'}`
|
||||||
spreadsheetOnlyOfficeLoading.value = false
|
spreadsheetOnlyOfficeLoading.value = false
|
||||||
upstreamEvents.onError?.(event)
|
upstreamEvents.onError?.(event)
|
||||||
},
|
},
|
||||||
@@ -2383,17 +2244,16 @@ export default {
|
|||||||
const hasChanges = Boolean(event?.data)
|
const hasChanges = Boolean(event?.data)
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
spreadsheetOnlyOfficeHadLocalEdits = true
|
spreadsheetOnlyOfficeHadLocalEdits = true
|
||||||
markSpreadsheetPendingChange(assetId, version)
|
if (!spreadsheetOnlyOfficeChangePollTimer) {
|
||||||
if (!spreadsheetOnlyOfficeVersionPollTimer) {
|
scheduleSpreadsheetOnlyOfficeChangeSync(assetId)
|
||||||
scheduleSpreadsheetOnlyOfficeVersionSync(assetId, version)
|
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
spreadsheetOnlyOfficeHadLocalEdits &&
|
spreadsheetOnlyOfficeHadLocalEdits &&
|
||||||
editable &&
|
editable &&
|
||||||
!isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)
|
!isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)
|
||||||
) {
|
) {
|
||||||
spreadsheetOnlyOfficeHadLocalEdits = false
|
spreadsheetOnlyOfficeHadLocalEdits = false
|
||||||
scheduleSpreadsheetOnlyOfficeVersionSync(assetId, version)
|
scheduleSpreadsheetOnlyOfficeChangeSync(assetId)
|
||||||
}
|
}
|
||||||
upstreamEvents.onDocumentStateChange?.(event)
|
upstreamEvents.onDocumentStateChange?.(event)
|
||||||
}
|
}
|
||||||
@@ -2402,11 +2262,11 @@ export default {
|
|||||||
spreadsheetOnlyOfficeHostId.value,
|
spreadsheetOnlyOfficeHostId.value,
|
||||||
config
|
config
|
||||||
)
|
)
|
||||||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)) {
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||||
destroySpreadsheetOnlyOfficeEditor()
|
destroySpreadsheetOnlyOfficeEditor()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId, version)) {
|
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
spreadsheetOnlyOfficeError.value = error?.message || '规则表加载失败,请稍后重试。'
|
spreadsheetOnlyOfficeError.value = error?.message || '规则表加载失败,请稍后重试。'
|
||||||
@@ -2431,7 +2291,6 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const blob = await fetchAgentAssetSpreadsheetBlob(
|
const blob = await fetchAgentAssetSpreadsheetBlob(
|
||||||
selectedSkill.value.id,
|
selectedSkill.value.id,
|
||||||
selectedSkill.value.displayVersion,
|
|
||||||
'attachment'
|
'attachment'
|
||||||
)
|
)
|
||||||
const objectUrl = URL.createObjectURL(blob)
|
const objectUrl = URL.createObjectURL(blob)
|
||||||
@@ -2462,7 +2321,7 @@ export default {
|
|||||||
await refreshCurrentAssets()
|
await refreshCurrentAssets()
|
||||||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||||
await loadSpreadsheetChangeRecords(selectedSkill.value.id)
|
await loadSpreadsheetChangeRecords(selectedSkill.value.id)
|
||||||
toast(`已导入 ${file.name} 的表格内容,并生成新版本。`)
|
toast(`已导入 ${file.name} 的表格内容,右侧会记录本次修改。`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast(error?.message || '规则表内容导入失败,请稍后重试。')
|
toast(error?.message || '规则表内容导入失败,请稍后重试。')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -2560,7 +2419,7 @@ export default {
|
|||||||
const detail = await fetchAgentAssetDetail(assetId)
|
const detail = await fetchAgentAssetDetail(assetId)
|
||||||
selectedSkill.value = buildDetailViewModel(detail, runs.value)
|
selectedSkill.value = buildDetailViewModel(detail, runs.value)
|
||||||
if (selectedSkill.value?.type === 'rules') {
|
if (selectedSkill.value?.type === 'rules') {
|
||||||
if (!selectedSkill.value.usesJsonRiskRule) {
|
if (!selectedSkill.value.usesSpreadsheetRule && !selectedSkill.value.usesJsonRiskRule) {
|
||||||
loadVersionTimeline(assetId, { silent: true }).catch(() => {})
|
loadVersionTimeline(assetId, { silent: true }).catch(() => {})
|
||||||
}
|
}
|
||||||
if (selectedSkill.value.usesSpreadsheetRule) {
|
if (selectedSkill.value.usesSpreadsheetRule) {
|
||||||
@@ -2677,7 +2536,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openAssetDetail(asset) {
|
function openAssetDetail(asset) {
|
||||||
stopSpreadsheetOnlyOfficeDeferredRefresh()
|
|
||||||
destroySpreadsheetOnlyOfficeEditor()
|
destroySpreadsheetOnlyOfficeEditor()
|
||||||
spreadsheetOnlyOfficeError.value = ''
|
spreadsheetOnlyOfficeError.value = ''
|
||||||
spreadsheetOnlyOfficeLoading.value = false
|
spreadsheetOnlyOfficeLoading.value = false
|
||||||
@@ -2688,17 +2546,18 @@ export default {
|
|||||||
versionSwitchTarget.value = null
|
versionSwitchTarget.value = null
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const opensSpreadsheetRule = Boolean(asset?.usesSpreadsheetRule)
|
||||||
selectedSkill.value = {
|
selectedSkill.value = {
|
||||||
...asset,
|
...asset,
|
||||||
configJson: {},
|
configJson: {},
|
||||||
isPreviewMock: false,
|
isPreviewMock: false,
|
||||||
usesSpreadsheetRule: false,
|
usesSpreadsheetRule: opensSpreadsheetRule,
|
||||||
usesJsonRiskRule: false,
|
usesJsonRiskRule: Boolean(asset?.usesJsonRiskRule),
|
||||||
riskRuleJsonText: '{}',
|
riskRuleJsonText: '{}',
|
||||||
riskRuleSummary: null,
|
riskRuleSummary: null,
|
||||||
riskRuleDescription: '',
|
riskRuleDescription: '',
|
||||||
riskRuleSourceRef: '',
|
riskRuleSourceRef: '',
|
||||||
ruleDocument: null,
|
ruleDocument: asset?.ruleDocument || null,
|
||||||
scenarioList: [],
|
scenarioList: [],
|
||||||
fields: [],
|
fields: [],
|
||||||
promptSections: [],
|
promptSections: [],
|
||||||
@@ -2714,16 +2573,18 @@ export default {
|
|||||||
runtimeKind: 'policy_rule_draft',
|
runtimeKind: 'policy_rule_draft',
|
||||||
displayVersion: asset.version,
|
displayVersion: asset.version,
|
||||||
displayVersionChangeNote: '无版本说明',
|
displayVersionChangeNote: '无版本说明',
|
||||||
loading: true,
|
loading: !opensSpreadsheetRule,
|
||||||
reviewStatusLabel: '加载中',
|
reviewStatusLabel: opensSpreadsheetRule ? '' : '加载中',
|
||||||
reviewStatusTone: 'draft'
|
reviewStatusTone: 'draft'
|
||||||
}
|
}
|
||||||
versionSwitchTarget.value = null
|
versionSwitchTarget.value = null
|
||||||
|
if (opensSpreadsheetRule) {
|
||||||
|
loadSpreadsheetChangeRecords(asset.id).catch(() => {})
|
||||||
|
}
|
||||||
loadSelectedAssetDetail(asset.id).catch(() => {})
|
loadSelectedAssetDetail(asset.id).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDetail() {
|
function closeDetail() {
|
||||||
stopSpreadsheetOnlyOfficeDeferredRefresh()
|
|
||||||
destroySpreadsheetOnlyOfficeEditor()
|
destroySpreadsheetOnlyOfficeEditor()
|
||||||
spreadsheetOnlyOfficeError.value = ''
|
spreadsheetOnlyOfficeError.value = ''
|
||||||
spreadsheetOnlyOfficeLoading.value = false
|
spreadsheetOnlyOfficeLoading.value = false
|
||||||
@@ -2732,9 +2593,7 @@ export default {
|
|||||||
detailLoading.value = false
|
detailLoading.value = false
|
||||||
versionSwitchTarget.value = null
|
versionSwitchTarget.value = null
|
||||||
versionTimelineOpen.value = false
|
versionTimelineOpen.value = false
|
||||||
versionCompareOpen.value = false
|
|
||||||
versionTimelineItems.value = []
|
versionTimelineItems.value = []
|
||||||
versionComparePayload.value = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openVersionSwitch(version) {
|
function openVersionSwitch(version) {
|
||||||
@@ -3062,66 +2921,6 @@ export default {
|
|||||||
versionTimelineOpen.value = false
|
versionTimelineOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openVersionCompare(options = {}) {
|
|
||||||
if (!selectedSkill.value?.id) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const defaultBase =
|
|
||||||
options.baseVersion || selectedSkill.value.publishedVersion || selectedSkill.value.workingVersion || ''
|
|
||||||
let defaultTarget =
|
|
||||||
options.targetVersion || selectedSkill.value.workingVersion || selectedSkill.value.publishedVersion || ''
|
|
||||||
if (!options.targetVersion && defaultBase === defaultTarget) {
|
|
||||||
defaultTarget =
|
|
||||||
selectedSkill.value.history.find((item) => item.version !== defaultBase)?.version || defaultTarget
|
|
||||||
}
|
|
||||||
compareBaseVersion.value = defaultBase
|
|
||||||
compareTargetVersion.value = defaultTarget
|
|
||||||
versionCompareOpen.value = true
|
|
||||||
await loadVersionCompare()
|
|
||||||
}
|
|
||||||
|
|
||||||
function openSpreadsheetChangeRecord(item) {
|
|
||||||
if (selectedSkill.value?.isPreviewMock) {
|
|
||||||
toast('预览数据暂不支持真实的线上差异对比。')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const publishedVersion = normalizeText(selectedSkill.value?.publishedVersion)
|
|
||||||
if (!selectedSkill.value?.id || !publishedVersion || publishedVersion === '-') {
|
|
||||||
toast('当前还没有线上版本,暂时无法查看与线上差异。')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
openVersionCompare({
|
|
||||||
baseVersion: publishedVersion,
|
|
||||||
targetVersion: item.version
|
|
||||||
}).catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeVersionCompare() {
|
|
||||||
versionCompareOpen.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadVersionCompare() {
|
|
||||||
if (!selectedSkill.value?.id || !compareBaseVersion.value || !compareTargetVersion.value) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
versionCompareLoading.value = true
|
|
||||||
versionCompareError.value = ''
|
|
||||||
try {
|
|
||||||
versionComparePayload.value = await compareAgentAssetSpreadsheetVersions(
|
|
||||||
selectedSkill.value.id,
|
|
||||||
compareBaseVersion.value,
|
|
||||||
compareTargetVersion.value
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
versionComparePayload.value = null
|
|
||||||
versionCompareError.value = error?.message || '版本差异对比失败,请稍后重试。'
|
|
||||||
} finally {
|
|
||||||
versionCompareLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.addEventListener('click', handleDocumentClick)
|
document.addEventListener('click', handleDocumentClick)
|
||||||
loadAssets({ force: true }).catch(() => {})
|
loadAssets({ force: true }).catch(() => {})
|
||||||
@@ -3129,7 +2928,6 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
stopSpreadsheetOnlyOfficeDeferredRefresh()
|
|
||||||
destroySpreadsheetOnlyOfficeEditor()
|
destroySpreadsheetOnlyOfficeEditor()
|
||||||
document.removeEventListener('click', handleDocumentClick)
|
document.removeEventListener('click', handleDocumentClick)
|
||||||
})
|
})
|
||||||
@@ -3186,7 +2984,7 @@ export default {
|
|||||||
selectedSkillUsesSpreadsheet,
|
selectedSkillUsesSpreadsheet,
|
||||||
selectedSkillUsesJsonRisk,
|
selectedSkillUsesJsonRisk,
|
||||||
selectedSpreadsheetFileName,
|
selectedSpreadsheetFileName,
|
||||||
selectedSpreadsheetVersionModeLabel,
|
selectedSpreadsheetModeLabel,
|
||||||
selectedVersionTimelineItems,
|
selectedVersionTimelineItems,
|
||||||
selectedSpreadsheetChangeRecords,
|
selectedSpreadsheetChangeRecords,
|
||||||
detailBusy,
|
detailBusy,
|
||||||
@@ -3205,18 +3003,10 @@ export default {
|
|||||||
versionTimelineOpen,
|
versionTimelineOpen,
|
||||||
versionTimelineLoading,
|
versionTimelineLoading,
|
||||||
versionTimelineError,
|
versionTimelineError,
|
||||||
versionCompareOpen,
|
|
||||||
versionCompareLoading,
|
|
||||||
versionCompareError,
|
|
||||||
versionComparePayload,
|
|
||||||
versionCompareCellRows,
|
|
||||||
versionCompareSheetRows,
|
|
||||||
spreadsheetChangeDetailOpen,
|
spreadsheetChangeDetailOpen,
|
||||||
selectedSpreadsheetChangeRecord,
|
selectedSpreadsheetChangeRecord,
|
||||||
selectedSpreadsheetChangeSheetRows,
|
selectedSpreadsheetChangeSheetRows,
|
||||||
selectedSpreadsheetChangeCellRows,
|
selectedSpreadsheetChangeCellRows,
|
||||||
compareBaseVersion,
|
|
||||||
compareTargetVersion,
|
|
||||||
openAssetDetail,
|
openAssetDetail,
|
||||||
closeDetail,
|
closeDetail,
|
||||||
resetFilters,
|
resetFilters,
|
||||||
@@ -3243,12 +3033,8 @@ export default {
|
|||||||
restoreSelectedVersion,
|
restoreSelectedVersion,
|
||||||
openVersionTimeline,
|
openVersionTimeline,
|
||||||
closeVersionTimeline,
|
closeVersionTimeline,
|
||||||
openSpreadsheetChangeRecord,
|
|
||||||
openSpreadsheetChangeDetail,
|
openSpreadsheetChangeDetail,
|
||||||
closeSpreadsheetChangeDetail,
|
closeSpreadsheetChangeDetail,
|
||||||
openVersionCompare,
|
|
||||||
closeVersionCompare,
|
|
||||||
loadVersionCompare,
|
|
||||||
loadAssets
|
loadAssets
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,10 +165,19 @@ const REVIEW_OTHER_CATEGORY_OPTIONS = [
|
|||||||
|
|
||||||
const REVIEW_SCENE_OTHER_OPTION = '其他场景'
|
const REVIEW_SCENE_OTHER_OPTION = '其他场景'
|
||||||
const REVIEW_SCENE_OPTIONS = ['请客户吃饭', '出差行程', '住宿报销', '交通出行', '会务活动', REVIEW_SCENE_OTHER_OPTION]
|
const REVIEW_SCENE_OPTIONS = ['请客户吃饭', '出差行程', '住宿报销', '交通出行', '会务活动', REVIEW_SCENE_OTHER_OPTION]
|
||||||
|
const EXPENSE_CODE_TO_PRESET_SCENE = {
|
||||||
|
travel: '出差行程',
|
||||||
|
hotel: '住宿报销',
|
||||||
|
transport: '交通出行',
|
||||||
|
meeting: '会务活动',
|
||||||
|
entertainment: '请客户吃饭',
|
||||||
|
meal: '请客户吃饭'
|
||||||
|
}
|
||||||
const DATE_INPUT_FORMAT = 'YYYY-MM-DD'
|
const DATE_INPUT_FORMAT = 'YYYY-MM-DD'
|
||||||
const MAX_ATTACHMENTS = 10
|
const MAX_ATTACHMENTS = 10
|
||||||
const MAX_OCR_DOCUMENTS = 10
|
const MAX_OCR_DOCUMENTS = 10
|
||||||
const VISIBLE_ATTACHMENT_CHIPS = 2
|
const VISIBLE_ATTACHMENT_CHIPS = 2
|
||||||
|
const COMPOSER_TEXTAREA_HEIGHT = 36
|
||||||
const COMPOSER_MAX_ROWS = 5
|
const COMPOSER_MAX_ROWS = 5
|
||||||
const EXPENSE_QUERY_PAGE_SIZE = 5
|
const EXPENSE_QUERY_PAGE_SIZE = 5
|
||||||
const SESSION_TYPE_EXPENSE = 'expense'
|
const SESSION_TYPE_EXPENSE = 'expense'
|
||||||
@@ -206,6 +215,76 @@ const FLOW_STEP_FALLBACKS = {
|
|||||||
completedText: '结果已生成'
|
completedText: '结果已生成'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const ASSISTANT_DISPLAY_NAME = '财务助手'
|
||||||
|
|
||||||
|
const EXPENSE_WELCOME_QUICK_ACTIONS = [
|
||||||
|
{
|
||||||
|
label: '发起差旅报销',
|
||||||
|
prompt: '我要报销一笔出差费用,请帮我说明需要准备的材料,并引导我上传票据。',
|
||||||
|
icon: 'mdi mdi-bag-suitcase-outline'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '招待费报销',
|
||||||
|
prompt: '我要报销客户招待餐费,请告诉我需要补充的客户、参与人员和票据要求。',
|
||||||
|
icon: 'mdi mdi-food-fork-drink'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '交通费报销',
|
||||||
|
prompt: '我要报销交通出行费用,请帮我识别常见票据类型和报销注意事项。',
|
||||||
|
icon: 'mdi mdi-car-outline'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '上传票据识别',
|
||||||
|
prompt: '我已准备好票据,请帮我识别并生成报销草稿。',
|
||||||
|
icon: 'mdi mdi-file-upload-outline'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '查询近期报销',
|
||||||
|
prompt: '帮我查询近10天的报销记录和金额汇总。',
|
||||||
|
icon: 'mdi mdi-chart-timeline-variant'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '解释报销风险',
|
||||||
|
prompt: '请结合公司制度,说明酒店超标、发票抬头不一致等常见报销风险。',
|
||||||
|
icon: 'mdi mdi-shield-alert-outline'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const ASSISTANT_DISPLAY_NAME = '财务助手'
|
||||||
|
|
||||||
|
const EXPENSE_WELCOME_QUICK_ACTIONS = [
|
||||||
|
{
|
||||||
|
label: '发起差旅报销',
|
||||||
|
prompt: '我要报销一笔出差费用,请帮我说明需要准备的材料,并引导我上传票据。',
|
||||||
|
icon: 'mdi mdi-bag-suitcase-outline'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '招待费报销',
|
||||||
|
prompt: '我要报销客户招待餐费,请告诉我需要补充的客户、参与人员和票据要求。',
|
||||||
|
icon: 'mdi mdi-food-fork-drink'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '交通费报销',
|
||||||
|
prompt: '我要报销交通出行费用,请帮我识别场景并列出待补充信息。',
|
||||||
|
icon: 'mdi mdi-car-outline'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '上传票据识别',
|
||||||
|
prompt: '我已准备好票据,请帮我识别票据内容并生成报销草稿。',
|
||||||
|
icon: 'mdi mdi-file-upload-outline'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '查询近期报销',
|
||||||
|
prompt: '帮我查询近10天的报销记录和金额汇总。',
|
||||||
|
icon: 'mdi mdi-chart-timeline-variant'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '解释报销风险',
|
||||||
|
prompt: '请结合公司制度,说明酒店超标、发票抬头不一致等常见报销风险与处理方式。',
|
||||||
|
icon: 'mdi mdi-shield-alert-outline'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
const HOT_KNOWLEDGE_QUESTIONS = [
|
const HOT_KNOWLEDGE_QUESTIONS = [
|
||||||
'差旅住宿标准按什么规则执行?',
|
'差旅住宿标准按什么规则执行?',
|
||||||
'酒店超标后如何申请例外报销?',
|
'酒店超标后如何申请例外报销?',
|
||||||
@@ -1102,39 +1181,142 @@ function buildReviewDocumentCorrectionContext(drafts) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildWelcomeInsight(entrySource, linkedRequest, sessionType = SESSION_TYPE_EXPENSE) {
|
function buildWelcomeUserContext(user = {}) {
|
||||||
|
const username = String(user.username || '').trim()
|
||||||
|
const name = String(user.name || username || '同事').trim()
|
||||||
|
const grade = String(user.grade || '').trim()
|
||||||
|
const position = String(user.position || '').trim()
|
||||||
|
const role = String(user.role || '').trim()
|
||||||
|
const roleCodes = Array.isArray(user.roleCodes) ? user.roleCodes : []
|
||||||
|
const isAdmin =
|
||||||
|
Boolean(user.isAdmin)
|
||||||
|
|| username.toLowerCase() === 'admin'
|
||||||
|
|| roleCodes.some((item) => /admin|manager/i.test(String(item || '')))
|
||||||
|
|| /管理员|系统管理/.test(position)
|
||||||
|
|| /管理员|系统管理/.test(role)
|
||||||
|
|
||||||
|
const now = new Date()
|
||||||
|
const dateLine = now.toLocaleDateString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
weekday: 'long'
|
||||||
|
})
|
||||||
|
|
||||||
|
let honorific = name
|
||||||
|
if (isAdmin) {
|
||||||
|
honorific = name && !/^admin$/i.test(name) ? `${name} 管理员` : '管理员'
|
||||||
|
} else {
|
||||||
|
const prefix = [grade, position].filter(Boolean).join(' ')
|
||||||
|
honorific = prefix ? `${prefix} ${name}`.trim() : name
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
username,
|
||||||
|
grade,
|
||||||
|
position,
|
||||||
|
role,
|
||||||
|
isAdmin,
|
||||||
|
honorific,
|
||||||
|
dateLine
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWelcomeQuickActions(sessionType, user, entrySource, linkedRequest) {
|
||||||
|
if (sessionType === SESSION_TYPE_KNOWLEDGE) {
|
||||||
|
return HOT_KNOWLEDGE_QUESTIONS.slice(0, 6).map((question) => ({
|
||||||
|
label: question.length > 20 ? `${question.slice(0, 20)}…` : question,
|
||||||
|
prompt: question,
|
||||||
|
icon: 'mdi mdi-comment-question-outline'
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entrySource === 'detail' && linkedRequest?.id) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: '补充当前单据票据',
|
||||||
|
prompt: `请结合单据 ${linkedRequest.id},帮我继续补充票据并更新识别结果。`,
|
||||||
|
icon: 'mdi mdi-file-plus-outline'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '解释本单风险',
|
||||||
|
prompt: `请解释单据 ${linkedRequest.id} 当前存在的报销风险与处理建议。`,
|
||||||
|
icon: 'mdi mdi-shield-alert-outline'
|
||||||
|
},
|
||||||
|
...EXPENSE_WELCOME_QUICK_ACTIONS.slice(0, 4)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return EXPENSE_WELCOME_QUICK_ACTIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWelcomeMessage(entrySource, linkedRequest, sessionType = SESSION_TYPE_EXPENSE, user = null) {
|
||||||
|
const ctx = buildWelcomeUserContext(user || {})
|
||||||
|
const greeting = ctx.isAdmin ? `${ctx.honorific},您好` : `您好,${ctx.honorific}`
|
||||||
|
|
||||||
|
if (sessionType === SESSION_TYPE_KNOWLEDGE) {
|
||||||
|
return [
|
||||||
|
`${greeting}!今日是 **${ctx.dateLine}**。`,
|
||||||
|
'',
|
||||||
|
'欢迎进入 **个人财务中心 · 知识问答**。我是您的财务助手,可以帮您查制度、报销标准、票据要求和常见财务问题。',
|
||||||
|
'',
|
||||||
|
'您可以直接输入问题,或点击下方「猜你想问」快速开始。'
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entrySource === 'detail' && linkedRequest?.id) {
|
||||||
|
return [
|
||||||
|
`${greeting}!今日是 **${ctx.dateLine}**。`,
|
||||||
|
'',
|
||||||
|
`我已为您打开关联单据 **${linkedRequest.id}**。您可以继续补充票据、核对识别结果,或让我解释待补项与风险。`,
|
||||||
|
'',
|
||||||
|
'如需新建其他报销,也可以直接告诉我费用场景,或上传发票、行程单开始识别。'
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
`${greeting}!今日是 **${ctx.dateLine}**。`,
|
||||||
|
'',
|
||||||
|
'**欢迎来到个人财务中心。** 我是您的财务助手,可以陪您完成票据识别、报销草稿整理、待补项提醒和风险说明。',
|
||||||
|
'',
|
||||||
|
'您可以描述一笔费用、上传票据,或点击下方快捷操作直接开始。'
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWelcomeInsight(entrySource, linkedRequest, sessionType = SESSION_TYPE_EXPENSE, user = null) {
|
||||||
|
const ctx = buildWelcomeUserContext(user || {})
|
||||||
|
|
||||||
if (sessionType === SESSION_TYPE_KNOWLEDGE) {
|
if (sessionType === SESSION_TYPE_KNOWLEDGE) {
|
||||||
return {
|
return {
|
||||||
intent: 'welcome',
|
intent: 'welcome',
|
||||||
metricLabel: '当前模式',
|
metricLabel: '今日',
|
||||||
metricValue: '知识问答',
|
metricValue: ctx.dateLine.split(' ')[0] || '—',
|
||||||
title: '财务知识问答',
|
title: '财务知识问答',
|
||||||
summary: '这里适合处理制度解释、报销规则、票据规范和常见财务问题,右侧提供 Top 10 热门问题可直接追问。',
|
summary: `${ctx.honorific},右侧整理了热门制度问题,点选即可追问;左侧也可直接输入您关心的问题。`,
|
||||||
agent: null
|
agent: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
intent: 'welcome',
|
intent: 'welcome',
|
||||||
metricLabel: '当前状态',
|
metricLabel: '助手状态',
|
||||||
metricValue: '待识别',
|
metricValue: '待您吩咐',
|
||||||
title: entrySource === 'detail' && linkedRequest?.id ? `已关联 ${linkedRequest.id}` : '等待识别内容',
|
title: entrySource === 'detail' && linkedRequest?.id ? `已关联 ${linkedRequest.id}` : '个人财务中心',
|
||||||
summary:
|
summary:
|
||||||
entrySource === 'detail' && linkedRequest?.id
|
entrySource === 'detail' && linkedRequest?.id
|
||||||
? '发送消息后会直接结合当前单据上下文识别报销语义,右侧展示已识别内容,主对话区展示待补项和风险提示。'
|
? `${ctx.honorific},发送消息或上传附件后,我会结合当前单据继续识别并提示待补项。`
|
||||||
: '请输入费用场景或上传票据,右侧会展示已识别内容,主对话区会提示待补信息和风险注意事项。',
|
: `${ctx.honorific},描述费用场景或上传票据后,我会在右侧展示识别结果,并在对话中提示待补信息与风险。`,
|
||||||
agent: null
|
agent: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildWelcomeMessage(entrySource, linkedRequest, sessionType = SESSION_TYPE_EXPENSE) {
|
function createWelcomeAssistantMessage(entrySource, linkedRequest, sessionType = SESSION_TYPE_EXPENSE, user = null) {
|
||||||
if (sessionType === SESSION_TYPE_KNOWLEDGE) {
|
return createMessage('assistant', buildWelcomeMessage(entrySource, linkedRequest, sessionType, user), [], {
|
||||||
return '已切换到财务知识问答会话。你可以直接提问制度、报销规则、票据要求或常见财务问题。'
|
assistantName: ASSISTANT_DISPLAY_NAME,
|
||||||
}
|
isWelcome: true,
|
||||||
|
welcomeQuickActions: buildWelcomeQuickActions(sessionType, user, entrySource, linkedRequest)
|
||||||
return entrySource === 'detail' && linkedRequest?.id
|
})
|
||||||
? `已进入财务AI工作台,当前关联单据 ${linkedRequest.id}。请描述费用场景或补充票据。`
|
|
||||||
: '这里是财务AI工作台。你发送的内容会直接进入真实 Orchestrator 和 User Agent。'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveInitialSessionType(conversation) {
|
function resolveInitialSessionType(conversation) {
|
||||||
@@ -1360,7 +1542,8 @@ function buildDraftSavedPayload({
|
|||||||
|| String(inlineState?.scene_label || '').trim()
|
|| String(inlineState?.scene_label || '').trim()
|
||||||
|| String(draftPayload?.title || '').trim()
|
|| String(draftPayload?.title || '').trim()
|
||||||
|| `${typeLabel}报销草稿`
|
|| `${typeLabel}报销草稿`
|
||||||
const sceneLabel = String(inlineState?.scene_label || summarizeReviewScene(title, typeLabel)).trim() || typeLabel
|
const sceneLabel =
|
||||||
|
String(inlineState?.scene_label || summarizeReviewScene(title, typeLabel, reviewPayload)).trim() || typeLabel
|
||||||
const attachmentSummary = documents.length
|
const attachmentSummary = documents.length
|
||||||
? `${documents.length} 条识别票据 / ${documents.length} 份材料`
|
? `${documents.length} 条识别票据 / ${documents.length} 份材料`
|
||||||
: String(inlineState?.attachment_names || '').trim()
|
: String(inlineState?.attachment_names || '').trim()
|
||||||
@@ -1882,34 +2065,160 @@ function buildReviewIntentText(reviewPayload) {
|
|||||||
|
|
||||||
function buildReviewSceneValue(reviewPayload) {
|
function buildReviewSceneValue(reviewPayload) {
|
||||||
const slotMap = buildReviewSlotMap(reviewPayload)
|
const slotMap = buildReviewSlotMap(reviewPayload)
|
||||||
const reason = String(slotMap.reason?.value || slotMap.reason?.raw_value || '').trim()
|
const reason = String(slotMap.reason?.raw_value || slotMap.reason?.value || '').trim()
|
||||||
const expenseType = String(slotMap.expense_type?.value || '').trim()
|
const expenseType = String(slotMap.expense_type?.value || slotMap.expense_type?.normalized_value || '').trim()
|
||||||
return summarizeReviewScene(reason, expenseType)
|
return inferPresetSceneFromReview(reviewPayload, reason, expenseType)
|
||||||
}
|
}
|
||||||
|
|
||||||
function summarizeReviewScene(reason, expenseType = '') {
|
function matchPresetSceneFromReason(reason) {
|
||||||
const normalizedReason = String(reason || '').trim()
|
const compactReason = String(reason || '').trim().replace(/\s+/g, '')
|
||||||
const normalizedExpenseType = String(expenseType || '').trim()
|
if (!compactReason) {
|
||||||
const compactReason = normalizedReason.replace(/\s+/g, '')
|
return ''
|
||||||
|
}
|
||||||
|
if (/请客户.*吃饭|客户.*吃饭|招待|宴请|接待|客户接待/.test(compactReason)) {
|
||||||
|
return '请客户吃饭'
|
||||||
|
}
|
||||||
|
if (/出差行程|住宿报销|交通出行|会务活动/.test(compactReason)) {
|
||||||
|
const matchedPreset = REVIEW_SCENE_OPTIONS.find((option) => compactReason.includes(option.replace(/\s+/g, '')))
|
||||||
|
if (matchedPreset && matchedPreset !== REVIEW_SCENE_OTHER_OPTION) {
|
||||||
|
return matchedPreset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (/出差|差旅/.test(compactReason)) {
|
||||||
|
return '出差行程'
|
||||||
|
}
|
||||||
|
if (/酒店|住宿/.test(compactReason)) {
|
||||||
|
return '住宿报销'
|
||||||
|
}
|
||||||
|
if (/交通|打车|车费|停车|网约车|出租车|地铁|公交/.test(compactReason)) {
|
||||||
|
return '交通出行'
|
||||||
|
}
|
||||||
|
if (/会务|会议|参会|论坛|展会/.test(compactReason)) {
|
||||||
|
return '会务活动'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
if (compactReason) {
|
function mapOcrSceneLabelToPresetScene(sceneLabel, suggestedExpenseType = '') {
|
||||||
if (/请客户.*吃饭|客户.*吃饭|招待|宴请/.test(compactReason)) return '请客户吃饭'
|
const fromCode = EXPENSE_CODE_TO_PRESET_SCENE[resolveExpenseTypeCode(suggestedExpenseType)]
|
||||||
if (/出差|差旅/.test(compactReason)) return '出差行程'
|
if (fromCode) {
|
||||||
if (/酒店|住宿/.test(compactReason)) return '住宿报销'
|
return fromCode
|
||||||
if (/交通|打车|车费|停车/.test(compactReason)) return '交通出行'
|
|
||||||
if (/会务|会议|参会/.test(compactReason)) return '会务活动'
|
|
||||||
return compactReason.length > 12 ? `${compactReason.slice(0, 12)}...` : compactReason
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalizedExpenseType === '业务招待费') return '请客户吃饭'
|
const compactLabel = String(sceneLabel || '').trim().replace(/\s+/g, '')
|
||||||
if (normalizedExpenseType === '差旅费') return '出差行程'
|
if (!compactLabel) {
|
||||||
if (normalizedExpenseType === '住宿费') return '住宿报销'
|
return ''
|
||||||
if (normalizedExpenseType === '交通费') return '交通出行'
|
}
|
||||||
if (normalizedExpenseType === '会务费') return '会务活动'
|
if (/差旅|出差/.test(compactLabel)) {
|
||||||
if (normalizedExpenseType) return normalizedExpenseType
|
return '出差行程'
|
||||||
|
}
|
||||||
|
if (/住宿|酒店/.test(compactLabel)) {
|
||||||
|
return '住宿报销'
|
||||||
|
}
|
||||||
|
if (/交通/.test(compactLabel)) {
|
||||||
|
return '交通出行'
|
||||||
|
}
|
||||||
|
if (/招待|餐饮|餐费|伙食/.test(compactLabel)) {
|
||||||
|
return '请客户吃饭'
|
||||||
|
}
|
||||||
|
if (/会务|会议/.test(compactLabel)) {
|
||||||
|
return '会务活动'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapExpenseTypeLabelToPresetScene(expenseType) {
|
||||||
|
const code = resolveExpenseTypeCode(expenseType)
|
||||||
|
if (EXPENSE_CODE_TO_PRESET_SCENE[code]) {
|
||||||
|
return EXPENSE_CODE_TO_PRESET_SCENE[code]
|
||||||
|
}
|
||||||
|
|
||||||
|
const compactLabel = String(expenseType || '').trim().replace(/\s+/g, '')
|
||||||
|
if (!compactLabel) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (compactLabel.includes('差旅') || compactLabel.includes('出差')) {
|
||||||
|
return '出差行程'
|
||||||
|
}
|
||||||
|
if (compactLabel.includes('住宿') || compactLabel.includes('酒店')) {
|
||||||
|
return '住宿报销'
|
||||||
|
}
|
||||||
|
if (compactLabel.includes('交通')) {
|
||||||
|
return '交通出行'
|
||||||
|
}
|
||||||
|
if (compactLabel.includes('招待') || compactLabel.includes('餐饮') || compactLabel.includes('伙食')) {
|
||||||
|
return '请客户吃饭'
|
||||||
|
}
|
||||||
|
if (compactLabel.includes('会务') || compactLabel.includes('会议')) {
|
||||||
|
return '会务活动'
|
||||||
|
}
|
||||||
|
return matchPresetSceneFromReason(expenseType)
|
||||||
|
}
|
||||||
|
|
||||||
|
function inferPresetSceneFromReview(reviewPayload, reasonValue = '', expenseType = '') {
|
||||||
|
const documents = Array.isArray(reviewPayload?.document_cards) ? reviewPayload.document_cards : []
|
||||||
|
if (documents.length) {
|
||||||
|
const votes = new Map()
|
||||||
|
for (const document of documents) {
|
||||||
|
const preset =
|
||||||
|
mapOcrSceneLabelToPresetScene(document.scene_label, document.suggested_expense_type)
|
||||||
|
|| mapExpenseTypeLabelToPresetScene(document.suggested_expense_type)
|
||||||
|
if (!preset) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
votes.set(preset, (votes.get(preset) || 0) + 1)
|
||||||
|
}
|
||||||
|
if (votes.size) {
|
||||||
|
return [...votes.entries()].sort((left, right) => right[1] - left[1])[0][0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const claimGroups = Array.isArray(reviewPayload?.claim_groups) ? reviewPayload.claim_groups : []
|
||||||
|
if (claimGroups.length === 1) {
|
||||||
|
const group = claimGroups[0]
|
||||||
|
const preset =
|
||||||
|
mapExpenseTypeLabelToPresetScene(group.expense_type)
|
||||||
|
|| mapOcrSceneLabelToPresetScene(group.scene_label, group.expense_type)
|
||||||
|
if (preset) {
|
||||||
|
return preset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromReason = matchPresetSceneFromReason(reasonValue)
|
||||||
|
if (fromReason) {
|
||||||
|
return fromReason
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromExpenseType = mapExpenseTypeLabelToPresetScene(expenseType)
|
||||||
|
if (fromExpenseType) {
|
||||||
|
return fromExpenseType
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(reasonValue || '').trim()) {
|
||||||
|
return REVIEW_SCENE_OTHER_OPTION
|
||||||
|
}
|
||||||
return '待补充'
|
return '待补充'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatReviewSceneDisplayValue(inlineState) {
|
||||||
|
const scene = String(inlineState?.scene_label || '').trim()
|
||||||
|
if (!scene || scene === '待补充') {
|
||||||
|
return '待补充'
|
||||||
|
}
|
||||||
|
if (scene === REVIEW_SCENE_OTHER_OPTION) {
|
||||||
|
const detail = String(inlineState?.reason_value || '').trim()
|
||||||
|
if (!detail) {
|
||||||
|
return REVIEW_SCENE_OTHER_OPTION
|
||||||
|
}
|
||||||
|
return detail.length > 18 ? `${REVIEW_SCENE_OTHER_OPTION}:${detail.slice(0, 18)}...` : `${REVIEW_SCENE_OTHER_OPTION}:${detail}`
|
||||||
|
}
|
||||||
|
return scene
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeReviewScene(reason, expenseType = '', reviewPayload = null) {
|
||||||
|
return inferPresetSceneFromReview(reviewPayload, reason, expenseType)
|
||||||
|
}
|
||||||
|
|
||||||
function buildInlineReviewState(reviewPayload) {
|
function buildInlineReviewState(reviewPayload) {
|
||||||
const slotMap = buildReviewSlotMap(reviewPayload)
|
const slotMap = buildReviewSlotMap(reviewPayload)
|
||||||
const editFieldMap = buildReviewEditFieldMap(reviewPayload?.edit_fields)
|
const editFieldMap = buildReviewEditFieldMap(reviewPayload?.edit_fields)
|
||||||
@@ -1925,8 +2234,9 @@ function buildInlineReviewState(reviewPayload) {
|
|||||||
: 0
|
: 0
|
||||||
const expenseType = String(editFieldMap.expense_type?.value || slotMap.expense_type?.value || '').trim()
|
const expenseType = String(editFieldMap.expense_type?.value || slotMap.expense_type?.value || '').trim()
|
||||||
const reasonValue = String(
|
const reasonValue = String(
|
||||||
editFieldMap.reason?.value || slotMap.reason?.value || slotMap.reason?.raw_value || ''
|
editFieldMap.reason?.value || slotMap.reason?.raw_value || slotMap.reason?.value || ''
|
||||||
).trim()
|
).trim()
|
||||||
|
const sceneLabel = inferPresetSceneFromReview(reviewPayload, reasonValue, expenseType)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
occurred_date: String(
|
occurred_date: String(
|
||||||
@@ -1935,8 +2245,11 @@ function buildInlineReviewState(reviewPayload) {
|
|||||||
amount: normalizeAmountValue(
|
amount: normalizeAmountValue(
|
||||||
String(editFieldMap.amount?.value || slotMap.amount?.normalized_value || slotMap.amount?.value || '').trim()
|
String(editFieldMap.amount?.value || slotMap.amount?.normalized_value || slotMap.amount?.value || '').trim()
|
||||||
),
|
),
|
||||||
scene_label: summarizeReviewScene(reasonValue, expenseType),
|
scene_label: sceneLabel,
|
||||||
reason_value: reasonValue,
|
reason_value:
|
||||||
|
sceneLabel === REVIEW_SCENE_OTHER_OPTION
|
||||||
|
? reasonValue
|
||||||
|
: String(slotMap.reason?.raw_value || '').trim() || reasonValue,
|
||||||
customer_name: String(editFieldMap.customer_name?.value || slotMap.customer_name?.value || '').trim(),
|
customer_name: String(editFieldMap.customer_name?.value || slotMap.customer_name?.value || '').trim(),
|
||||||
location: String(
|
location: String(
|
||||||
editFieldMap.business_location?.value ||
|
editFieldMap.business_location?.value ||
|
||||||
@@ -2000,7 +2313,7 @@ function buildReviewFactCards(reviewPayload, inlineState = createEmptyInlineRevi
|
|||||||
{
|
{
|
||||||
key: 'scene',
|
key: 'scene',
|
||||||
label: '场景 / 事由',
|
label: '场景 / 事由',
|
||||||
value: String(inlineState.reason_value || inlineState.scene_label || '').trim() || '待补充',
|
value: formatReviewSceneDisplayValue(inlineState),
|
||||||
icon: 'mdi mdi-silverware-fork-knife',
|
icon: 'mdi mdi-silverware-fork-knife',
|
||||||
editor: 'select',
|
editor: 'select',
|
||||||
modelKey: 'scene_label',
|
modelKey: 'scene_label',
|
||||||
@@ -2524,6 +2837,7 @@ export default {
|
|||||||
const composerSingleDate = ref(formatDateInputValue())
|
const composerSingleDate = ref(formatDateInputValue())
|
||||||
const composerRangeStartDate = ref(formatDateInputValue())
|
const composerRangeStartDate = ref(formatDateInputValue())
|
||||||
const composerRangeEndDate = ref(formatDateInputValue())
|
const composerRangeEndDate = ref(formatDateInputValue())
|
||||||
|
const composerBusinessTimeTags = ref([])
|
||||||
const attachedFiles = ref([])
|
const attachedFiles = ref([])
|
||||||
const composerFilesExpanded = ref(false)
|
const composerFilesExpanded = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
@@ -2584,7 +2898,14 @@ export default {
|
|||||||
let flowTickTimer = 0
|
let flowTickTimer = 0
|
||||||
const flowSimulationTimers = []
|
const flowSimulationTimers = []
|
||||||
const canSubmit = computed(
|
const canSubmit = computed(
|
||||||
() => !submitting.value && !sessionSwitchBusy.value && Boolean(composerDraft.value.trim() || attachedFiles.value.length)
|
() =>
|
||||||
|
!submitting.value
|
||||||
|
&& !sessionSwitchBusy.value
|
||||||
|
&& Boolean(
|
||||||
|
composerDraft.value.trim()
|
||||||
|
|| attachedFiles.value.length
|
||||||
|
|| composerBusinessTimeTags.value.length
|
||||||
|
)
|
||||||
)
|
)
|
||||||
const composerCanApplyDateSelection = computed(() => {
|
const composerCanApplyDateSelection = computed(() => {
|
||||||
if (composerDateMode.value === 'single') {
|
if (composerDateMode.value === 'single') {
|
||||||
@@ -2655,8 +2976,8 @@ export default {
|
|||||||
agent: '知识回答'
|
agent: '知识回答'
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
welcome: '等待输入',
|
welcome: '财务助手',
|
||||||
agent: '真实智能体'
|
agent: '处理中'
|
||||||
}
|
}
|
||||||
return labels[currentInsight.value.intent] ?? 'AI 处理中'
|
return labels[currentInsight.value.intent] ?? 'AI 处理中'
|
||||||
})
|
})
|
||||||
@@ -2780,10 +3101,11 @@ export default {
|
|||||||
sessionType,
|
sessionType,
|
||||||
messages: restoredMessages.length
|
messages: restoredMessages.length
|
||||||
? restoredMessages
|
? restoredMessages
|
||||||
: [createMessage('assistant', buildWelcomeMessage(props.entrySource, linkedRequest.value, sessionType))],
|
: [createWelcomeAssistantMessage(props.entrySource, linkedRequest.value, sessionType, currentUser.value)],
|
||||||
conversationId: resolveInitialConversationId(conversation),
|
conversationId: resolveInitialConversationId(conversation),
|
||||||
draftClaimId: resolveInitialDraftClaimId(conversation),
|
draftClaimId: resolveInitialDraftClaimId(conversation),
|
||||||
currentInsight: initialInsight || buildWelcomeInsight(props.entrySource, linkedRequest.value, sessionType),
|
currentInsight:
|
||||||
|
initialInsight || buildWelcomeInsight(props.entrySource, linkedRequest.value, sessionType, currentUser.value),
|
||||||
reviewFilePreviews: restoredReviewFilePreviews,
|
reviewFilePreviews: restoredReviewFilePreviews,
|
||||||
composerDraft: '',
|
composerDraft: '',
|
||||||
attachedFiles: [],
|
attachedFiles: [],
|
||||||
@@ -2796,10 +3118,17 @@ export default {
|
|||||||
function buildEmptySessionState(sessionType) {
|
function buildEmptySessionState(sessionType) {
|
||||||
return {
|
return {
|
||||||
sessionType,
|
sessionType,
|
||||||
messages: [createMessage('assistant', buildWelcomeMessage(props.entrySource, linkedRequest.value, sessionType))],
|
messages: [
|
||||||
|
createWelcomeAssistantMessage(props.entrySource, linkedRequest.value, sessionType, currentUser.value)
|
||||||
|
],
|
||||||
conversationId: '',
|
conversationId: '',
|
||||||
draftClaimId: '',
|
draftClaimId: '',
|
||||||
currentInsight: buildWelcomeInsight(props.entrySource, linkedRequest.value, sessionType),
|
currentInsight: buildWelcomeInsight(
|
||||||
|
props.entrySource,
|
||||||
|
linkedRequest.value,
|
||||||
|
sessionType,
|
||||||
|
currentUser.value
|
||||||
|
),
|
||||||
reviewFilePreviews: [],
|
reviewFilePreviews: [],
|
||||||
composerDraft: '',
|
composerDraft: '',
|
||||||
attachedFiles: [],
|
attachedFiles: [],
|
||||||
@@ -2835,10 +3164,24 @@ export default {
|
|||||||
activeSessionType.value = nextState.sessionType || SESSION_TYPE_EXPENSE
|
activeSessionType.value = nextState.sessionType || SESSION_TYPE_EXPENSE
|
||||||
messages.value = Array.isArray(nextState.messages) && nextState.messages.length
|
messages.value = Array.isArray(nextState.messages) && nextState.messages.length
|
||||||
? nextState.messages
|
? nextState.messages
|
||||||
: [createMessage('assistant', buildWelcomeMessage(props.entrySource, linkedRequest.value, activeSessionType.value))]
|
: [
|
||||||
|
createWelcomeAssistantMessage(
|
||||||
|
props.entrySource,
|
||||||
|
linkedRequest.value,
|
||||||
|
activeSessionType.value,
|
||||||
|
currentUser.value
|
||||||
|
)
|
||||||
|
]
|
||||||
conversationId.value = String(nextState.conversationId || '').trim()
|
conversationId.value = String(nextState.conversationId || '').trim()
|
||||||
draftClaimId.value = String(nextState.draftClaimId || '').trim()
|
draftClaimId.value = String(nextState.draftClaimId || '').trim()
|
||||||
currentInsight.value = nextState.currentInsight || buildWelcomeInsight(props.entrySource, linkedRequest.value, activeSessionType.value)
|
currentInsight.value =
|
||||||
|
nextState.currentInsight
|
||||||
|
|| buildWelcomeInsight(
|
||||||
|
props.entrySource,
|
||||||
|
linkedRequest.value,
|
||||||
|
activeSessionType.value,
|
||||||
|
currentUser.value
|
||||||
|
)
|
||||||
reviewFilePreviews.value = Array.isArray(nextState.reviewFilePreviews) ? nextState.reviewFilePreviews : []
|
reviewFilePreviews.value = Array.isArray(nextState.reviewFilePreviews) ? nextState.reviewFilePreviews : []
|
||||||
composerDraft.value = String(nextState.composerDraft || '')
|
composerDraft.value = String(nextState.composerDraft || '')
|
||||||
attachedFiles.value = Array.isArray(nextState.attachedFiles) ? nextState.attachedFiles : []
|
attachedFiles.value = Array.isArray(nextState.attachedFiles) ? nextState.attachedFiles : []
|
||||||
@@ -2981,6 +3324,7 @@ export default {
|
|||||||
)
|
)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
document.addEventListener('click', handleComposerDatePickerOutside)
|
||||||
flowTickTimer = window.setInterval(() => {
|
flowTickTimer = window.setInterval(() => {
|
||||||
flowTick.value = Date.now()
|
flowTick.value = Date.now()
|
||||||
}, 250)
|
}, 250)
|
||||||
@@ -2988,7 +3332,9 @@ export default {
|
|||||||
workbenchVisible.value = true
|
workbenchVisible.value = true
|
||||||
})
|
})
|
||||||
void clearKnowledgeSessionOnEntry()
|
void clearKnowledgeSessionOnEntry()
|
||||||
currentInsight.value = currentInsight.value || buildWelcomeInsight(props.entrySource, linkedRequest.value, activeSessionType.value)
|
currentInsight.value =
|
||||||
|
currentInsight.value
|
||||||
|
|| buildWelcomeInsight(props.entrySource, linkedRequest.value, activeSessionType.value, currentUser.value)
|
||||||
if (props.initialPrompt?.trim() || props.initialFiles.length) {
|
if (props.initialPrompt?.trim() || props.initialFiles.length) {
|
||||||
const initialMerge = mergeFilesWithLimit([], Array.from(props.initialFiles), MAX_ATTACHMENTS)
|
const initialMerge = mergeFilesWithLimit([], Array.from(props.initialFiles), MAX_ATTACHMENTS)
|
||||||
composerDraft.value = props.initialPrompt.trim()
|
composerDraft.value = props.initialPrompt.trim()
|
||||||
@@ -3007,6 +3353,7 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('click', handleComposerDatePickerOutside)
|
||||||
if (flowTickTimer) {
|
if (flowTickTimer) {
|
||||||
window.clearInterval(flowTickTimer)
|
window.clearInterval(flowTickTimer)
|
||||||
}
|
}
|
||||||
@@ -3036,16 +3383,18 @@ export default {
|
|||||||
function adjustComposerTextareaHeight() {
|
function adjustComposerTextareaHeight() {
|
||||||
if (!composerTextareaRef.value) return
|
if (!composerTextareaRef.value) return
|
||||||
|
|
||||||
composerTextareaRef.value.style.height = 'auto'
|
const textarea = composerTextareaRef.value
|
||||||
const styles = window.getComputedStyle(composerTextareaRef.value)
|
textarea.style.height = 'auto'
|
||||||
const lineHeight = Number.parseFloat(styles.lineHeight) || 24
|
const styles = window.getComputedStyle(textarea)
|
||||||
|
const lineHeight = Number.parseFloat(styles.lineHeight) || 20
|
||||||
const verticalPadding =
|
const verticalPadding =
|
||||||
Number.parseFloat(styles.paddingTop || '0') + Number.parseFloat(styles.paddingBottom || '0')
|
Number.parseFloat(styles.paddingTop || '0') + Number.parseFloat(styles.paddingBottom || '0')
|
||||||
|
const minHeight = COMPOSER_TEXTAREA_HEIGHT
|
||||||
const maxHeight = lineHeight * COMPOSER_MAX_ROWS + verticalPadding
|
const maxHeight = lineHeight * COMPOSER_MAX_ROWS + verticalPadding
|
||||||
|
const nextHeight = Math.max(minHeight, Math.min(textarea.scrollHeight, maxHeight))
|
||||||
|
|
||||||
composerTextareaRef.value.style.height = `${Math.min(composerTextareaRef.value.scrollHeight, maxHeight)}px`
|
textarea.style.height = `${nextHeight}px`
|
||||||
composerTextareaRef.value.style.overflowY =
|
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden'
|
||||||
composerTextareaRef.value.scrollHeight > maxHeight ? 'auto' : 'hidden'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleComposerInput() {
|
function handleComposerInput() {
|
||||||
@@ -3368,14 +3717,52 @@ export default {
|
|||||||
return formatFlowDuration(step?.durationMs)
|
return formatFlowDuration(step?.durationMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildComposerDateSelectionText() {
|
function buildComposerBusinessTimeLabel() {
|
||||||
if (composerDateMode.value === 'single') {
|
if (composerDateMode.value === 'single') {
|
||||||
return `发生时间:${composerSingleDate.value}`
|
return `业务发生时间:${composerSingleDate.value}`
|
||||||
}
|
}
|
||||||
if (composerRangeStartDate.value === composerRangeEndDate.value) {
|
if (composerRangeStartDate.value === composerRangeEndDate.value) {
|
||||||
return `发生时间:${composerRangeStartDate.value}`
|
return `业务发生时间:${composerRangeStartDate.value}`
|
||||||
}
|
}
|
||||||
return `发生时间:${composerRangeStartDate.value} 至 ${composerRangeEndDate.value}`
|
return `业务发生时间:${composerRangeStartDate.value} 至 ${composerRangeEndDate.value}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveComposerSubmitText(explicitRawText) {
|
||||||
|
const draftPart = String(explicitRawText ?? composerDraft.value).trim()
|
||||||
|
const tagPart = composerBusinessTimeTags.value.map((item) => item.label).join(',')
|
||||||
|
if (!tagPart) {
|
||||||
|
return draftPart
|
||||||
|
}
|
||||||
|
if (!draftPart) {
|
||||||
|
return tagPart
|
||||||
|
}
|
||||||
|
return `${tagPart},${draftPart}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleComposerDatePicker() {
|
||||||
|
composerDatePickerOpen.value = !composerDatePickerOpen.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeComposerDatePicker() {
|
||||||
|
composerDatePickerOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function setComposerDateMode(mode) {
|
||||||
|
composerDateMode.value = mode === 'range' ? 'range' : 'single'
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeComposerBusinessTimeTag(tagId) {
|
||||||
|
composerBusinessTimeTags.value = composerBusinessTimeTags.value.filter((item) => item.id !== tagId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleComposerDatePickerOutside(event) {
|
||||||
|
if (!composerDatePickerOpen.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.target instanceof Element && event.target.closest('.composer-date-anchor')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
composerDatePickerOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyComposerDateSelection() {
|
async function applyComposerDateSelection() {
|
||||||
@@ -3383,9 +3770,12 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateText = buildComposerDateSelectionText()
|
composerBusinessTimeTags.value = [
|
||||||
const currentDraft = composerDraft.value.trim()
|
{
|
||||||
composerDraft.value = currentDraft ? `${currentDraft},${dateText}` : dateText
|
id: `biz-time-${Date.now()}`,
|
||||||
|
label: buildComposerBusinessTimeLabel()
|
||||||
|
}
|
||||||
|
]
|
||||||
composerDatePickerOpen.value = false
|
composerDatePickerOpen.value = false
|
||||||
await nextTick()
|
await nextTick()
|
||||||
adjustComposerTextareaHeight()
|
adjustComposerTextareaHeight()
|
||||||
@@ -4016,7 +4406,7 @@ export default {
|
|||||||
async function submitComposer(options = {}) {
|
async function submitComposer(options = {}) {
|
||||||
if (sessionSwitchBusy.value) return null
|
if (sessionSwitchBusy.value) return null
|
||||||
|
|
||||||
const rawText = String(options.rawText ?? composerDraft.value).trim()
|
const rawText = resolveComposerSubmitText(options.rawText).trim()
|
||||||
const systemGenerated = Boolean(options.systemGenerated)
|
const systemGenerated = Boolean(options.systemGenerated)
|
||||||
const resolvedUploadDisposition =
|
const resolvedUploadDisposition =
|
||||||
String(options.uploadDisposition || '').trim() ||
|
String(options.uploadDisposition || '').trim() ||
|
||||||
@@ -4084,6 +4474,7 @@ export default {
|
|||||||
messages.value.push(pendingMessage)
|
messages.value.push(pendingMessage)
|
||||||
|
|
||||||
composerDraft.value = ''
|
composerDraft.value = ''
|
||||||
|
composerBusinessTimeTags.value = []
|
||||||
clearAttachedFiles()
|
clearAttachedFiles()
|
||||||
if (fileInputRef.value) {
|
if (fileInputRef.value) {
|
||||||
fileInputRef.value.value = ''
|
fileInputRef.value.value = ''
|
||||||
@@ -4478,6 +4869,7 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
emit,
|
emit,
|
||||||
|
ASSISTANT_DISPLAY_NAME,
|
||||||
aiAvatar,
|
aiAvatar,
|
||||||
userAvatar,
|
userAvatar,
|
||||||
fileInputRef,
|
fileInputRef,
|
||||||
@@ -4489,7 +4881,12 @@ export default {
|
|||||||
composerSingleDate,
|
composerSingleDate,
|
||||||
composerRangeStartDate,
|
composerRangeStartDate,
|
||||||
composerRangeEndDate,
|
composerRangeEndDate,
|
||||||
|
composerBusinessTimeTags,
|
||||||
composerCanApplyDateSelection,
|
composerCanApplyDateSelection,
|
||||||
|
toggleComposerDatePicker,
|
||||||
|
closeComposerDatePicker,
|
||||||
|
setComposerDateMode,
|
||||||
|
removeComposerBusinessTimeTag,
|
||||||
flowPanelOpen,
|
flowPanelOpen,
|
||||||
flowSteps,
|
flowSteps,
|
||||||
flowRunId,
|
flowRunId,
|
||||||
@@ -4604,6 +5001,7 @@ export default {
|
|||||||
handleComposerInput,
|
handleComposerInput,
|
||||||
handleComposerEnter,
|
handleComposerEnter,
|
||||||
runShortcut,
|
runShortcut,
|
||||||
|
runWelcomeQuickAction: runShortcut,
|
||||||
askHotKnowledgeQuestion,
|
askHotKnowledgeQuestion,
|
||||||
resolveKnowledgeRankLabel,
|
resolveKnowledgeRankLabel,
|
||||||
resolveKnowledgeRankTone,
|
resolveKnowledgeRankTone,
|
||||||
|
|||||||
Reference in New Issue
Block a user