feat: 添加风险规则及 agent assets 功能增强
This commit is contained in:
@@ -1,457 +1,457 @@
|
|||||||
# 语义本体协议设计
|
# 语义本体协议设计
|
||||||
|
|
||||||
## 1. 定位
|
## 1. 定位
|
||||||
|
|
||||||
语义本体协议是用户问题、定时任务、规则中心、MCP、数据库查询和 Agent 之间的统一中间层。
|
语义本体协议是用户问题、定时任务、规则中心、MCP、数据库查询和 Agent 之间的统一中间层。
|
||||||
|
|
||||||
它解决的问题是:
|
它解决的问题是:
|
||||||
|
|
||||||
- 用户到底在问哪个业务域?
|
- 用户到底在问哪个业务域?
|
||||||
- 这属于什么场景?
|
- 这属于什么场景?
|
||||||
- 用户想做什么?
|
- 用户想做什么?
|
||||||
- 问题中涉及哪些对象?
|
- 问题中涉及哪些对象?
|
||||||
- 有没有时间、金额、状态、部门等过滤条件?
|
- 有没有时间、金额、状态、部门等过滤条件?
|
||||||
- 是否涉及风险?
|
- 是否涉及风险?
|
||||||
- 下一步应该查知识库、查数据库、跑规则、调 MCP,还是追问?
|
- 下一步应该查知识库、查数据库、跑规则、调 MCP,还是追问?
|
||||||
|
|
||||||
## 2. 第一版核心字段
|
## 2. 第一版核心字段
|
||||||
|
|
||||||
第一版建议只强制落 8 个字段。
|
第一版建议只强制落 8 个字段。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"domain": "",
|
"domain": "",
|
||||||
"scenario": "",
|
"scenario": "",
|
||||||
"intent": "",
|
"intent": "",
|
||||||
"entities": [],
|
"entities": [],
|
||||||
"time_range": {},
|
"time_range": {},
|
||||||
"constraints": {},
|
"constraints": {},
|
||||||
"risk_signals": [],
|
"risk_signals": [],
|
||||||
"next_step": ""
|
"next_step": ""
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.1 domain
|
### 2.1 domain
|
||||||
|
|
||||||
一级业务域。
|
一级业务域。
|
||||||
|
|
||||||
建议枚举:
|
建议枚举:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
reimbursement
|
reimbursement
|
||||||
accounts_receivable
|
accounts_receivable
|
||||||
accounts_payable
|
accounts_payable
|
||||||
general_finance
|
general_finance
|
||||||
system_operation
|
system_operation
|
||||||
```
|
```
|
||||||
|
|
||||||
含义:
|
含义:
|
||||||
|
|
||||||
- `reimbursement`:报销、差旅、发票、补件。
|
- `reimbursement`:报销、差旅、发票、补件。
|
||||||
- `accounts_receivable`:应收账款、客户开票、收款、账龄。
|
- `accounts_receivable`:应收账款、客户开票、收款、账龄。
|
||||||
- `accounts_payable`:应付账款、供应商发票、付款、对账。
|
- `accounts_payable`:应付账款、供应商发票、付款、对账。
|
||||||
- `general_finance`:通用财务知识、制度、统计。
|
- `general_finance`:通用财务知识、制度、统计。
|
||||||
- `system_operation`:系统巡检、任务运行、规则维护、MCP 健康检查。
|
- `system_operation`:系统巡检、任务运行、规则维护、MCP 健康检查。
|
||||||
|
|
||||||
### 2.2 scenario
|
### 2.2 scenario
|
||||||
|
|
||||||
细分场景。
|
细分场景。
|
||||||
|
|
||||||
报销:
|
报销:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
travel_reimbursement
|
travel_reimbursement
|
||||||
daily_expense
|
daily_expense
|
||||||
invoice_validation
|
invoice_validation
|
||||||
attachment_review
|
attachment_review
|
||||||
policy_overrun
|
policy_overrun
|
||||||
reimbursement_audit
|
reimbursement_audit
|
||||||
```
|
```
|
||||||
|
|
||||||
应收:
|
应收:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
customer_invoice
|
customer_invoice
|
||||||
collection_followup
|
collection_followup
|
||||||
receivable_aging
|
receivable_aging
|
||||||
payment_matching
|
payment_matching
|
||||||
bad_debt_risk
|
bad_debt_risk
|
||||||
contract_receivable
|
contract_receivable
|
||||||
```
|
```
|
||||||
|
|
||||||
应付:
|
应付:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
vendor_invoice
|
vendor_invoice
|
||||||
payment_request
|
payment_request
|
||||||
payable_aging
|
payable_aging
|
||||||
vendor_reconciliation
|
vendor_reconciliation
|
||||||
invoice_matching
|
invoice_matching
|
||||||
cash_outflow_forecast
|
cash_outflow_forecast
|
||||||
```
|
```
|
||||||
|
|
||||||
系统运营:
|
系统运营:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
daily_risk_scan
|
daily_risk_scan
|
||||||
daily_finance_statistics
|
daily_finance_statistics
|
||||||
knowledge_accumulation
|
knowledge_accumulation
|
||||||
mcp_health_check
|
mcp_health_check
|
||||||
rule_quality_review
|
rule_quality_review
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.3 intent
|
### 2.3 intent
|
||||||
|
|
||||||
用户或任务的意图。
|
用户或任务的意图。
|
||||||
|
|
||||||
建议枚举:
|
建议枚举:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
query
|
query
|
||||||
explain
|
explain
|
||||||
create
|
create
|
||||||
validate
|
validate
|
||||||
summarize
|
summarize
|
||||||
reconcile
|
reconcile
|
||||||
monitor
|
monitor
|
||||||
predict
|
predict
|
||||||
remind
|
remind
|
||||||
generate
|
generate
|
||||||
optimize
|
optimize
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.4 entities
|
### 2.4 entities
|
||||||
|
|
||||||
识别出的业务对象。
|
识别出的业务对象。
|
||||||
|
|
||||||
统一结构:
|
统一结构:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "invoice",
|
"type": "invoice",
|
||||||
"value": "INV-202605001",
|
"value": "INV-202605001",
|
||||||
"normalized_value": "INV-202605001",
|
"normalized_value": "INV-202605001",
|
||||||
"role": "target",
|
"role": "target",
|
||||||
"confidence": 0.92
|
"confidence": 0.92
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
常见实体:
|
常见实体:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
employee
|
employee
|
||||||
department
|
department
|
||||||
customer
|
customer
|
||||||
vendor
|
vendor
|
||||||
invoice
|
invoice
|
||||||
contract
|
contract
|
||||||
reimbursement_request
|
reimbursement_request
|
||||||
payment_order
|
payment_order
|
||||||
receipt
|
receipt
|
||||||
bank_transaction
|
bank_transaction
|
||||||
cost_center
|
cost_center
|
||||||
project
|
project
|
||||||
policy
|
policy
|
||||||
approval_node
|
approval_node
|
||||||
rule
|
rule
|
||||||
task
|
task
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.5 time_range
|
### 2.5 time_range
|
||||||
|
|
||||||
统一描述时间。
|
统一描述时间。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"raw": "上个月",
|
"raw": "上个月",
|
||||||
"start": "2026-04-01",
|
"start": "2026-04-01",
|
||||||
"end": "2026-04-30",
|
"end": "2026-04-30",
|
||||||
"granularity": "month"
|
"granularity": "month"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Hermes 定时任务也使用同一字段。
|
Hermes 定时任务也使用同一字段。
|
||||||
|
|
||||||
例如每日风险巡检:
|
例如每日风险巡检:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"raw": "昨日",
|
"raw": "昨日",
|
||||||
"start": "2026-05-09",
|
"start": "2026-05-09",
|
||||||
"end": "2026-05-09",
|
"end": "2026-05-09",
|
||||||
"granularity": "day"
|
"granularity": "day"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.6 constraints
|
### 2.6 constraints
|
||||||
|
|
||||||
查询、判断或执行条件。
|
查询、判断或执行条件。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "overdue",
|
"status": "overdue",
|
||||||
"aging_days": ">30",
|
"aging_days": ">30",
|
||||||
"amount": {
|
"amount": {
|
||||||
"operator": ">",
|
"operator": ">",
|
||||||
"value": 50000,
|
"value": 50000,
|
||||||
"currency": "CNY"
|
"currency": "CNY"
|
||||||
},
|
},
|
||||||
"department": "销售部",
|
"department": "销售部",
|
||||||
"risk_level": ["medium", "high"]
|
"risk_level": ["medium", "high"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.7 risk_signals
|
### 2.7 risk_signals
|
||||||
|
|
||||||
风险信号。
|
风险信号。
|
||||||
|
|
||||||
建议枚举:
|
建议枚举:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
duplicate_invoice
|
duplicate_invoice
|
||||||
missing_attachment
|
missing_attachment
|
||||||
policy_overrun
|
policy_overrun
|
||||||
over_budget
|
over_budget
|
||||||
overdue_receivable
|
overdue_receivable
|
||||||
bad_debt_risk
|
bad_debt_risk
|
||||||
vendor_payment_risk
|
vendor_payment_risk
|
||||||
payment_mismatch
|
payment_mismatch
|
||||||
contract_mismatch
|
contract_mismatch
|
||||||
cashflow_pressure
|
cashflow_pressure
|
||||||
mcp_unavailable
|
mcp_unavailable
|
||||||
rule_quality_issue
|
rule_quality_issue
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.8 next_step
|
### 2.8 next_step
|
||||||
|
|
||||||
下一步动作。
|
下一步动作。
|
||||||
|
|
||||||
建议枚举:
|
建议枚举:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
answer
|
answer
|
||||||
ask_clarification
|
ask_clarification
|
||||||
query_database
|
query_database
|
||||||
run_rule
|
run_rule
|
||||||
call_mcp
|
call_mcp
|
||||||
search_knowledge
|
search_knowledge
|
||||||
create_draft
|
create_draft
|
||||||
create_task
|
create_task
|
||||||
generate_report
|
generate_report
|
||||||
notify_user
|
notify_user
|
||||||
escalate_to_human
|
escalate_to_human
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. 扩展字段
|
## 3. 扩展字段
|
||||||
|
|
||||||
后续可以增加:
|
后续可以增加:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"schema_version": "1.1",
|
"schema_version": "1.1",
|
||||||
"confidence": 0.86,
|
"confidence": 0.86,
|
||||||
"ambiguity": [],
|
"ambiguity": [],
|
||||||
"missing_slots": [],
|
"missing_slots": [],
|
||||||
"required_capabilities": [],
|
"required_capabilities": [],
|
||||||
"normalized_query": "",
|
"normalized_query": "",
|
||||||
"permission_scope": {},
|
"permission_scope": {},
|
||||||
"audit_tags": []
|
"audit_tags": []
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. 混合语义解析架构
|
## 4. 混合语义解析架构
|
||||||
|
|
||||||
第一版可上线实现不应只依赖关键词和正则。
|
第一版可上线实现不应只依赖关键词和正则。
|
||||||
|
|
||||||
推荐采用:
|
推荐采用:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
输入上下文装配
|
输入上下文装配
|
||||||
用户文本 + 页面上下文 + 附件名称 + OCR/VLM 摘要
|
用户文本 + 页面上下文 + 附件名称 + OCR/VLM 摘要
|
||||||
↓
|
↓
|
||||||
预抽取
|
预抽取
|
||||||
时间、金额、单号、显式对象
|
时间、金额、单号、显式对象
|
||||||
↓
|
↓
|
||||||
LLM 结构化解析
|
LLM 结构化解析
|
||||||
输出 scenario / intent / entities / missing_slots / ambiguity
|
输出 scenario / intent / entities / missing_slots / ambiguity
|
||||||
↓
|
↓
|
||||||
Schema 校验
|
Schema 校验
|
||||||
JSON 解析、字段枚举、必填校验、类型归一化
|
JSON 解析、字段枚举、必填校验、类型归一化
|
||||||
↓
|
↓
|
||||||
规则兜底
|
规则兜底
|
||||||
模型失败、低置信度或字段缺失时回退到规则解析
|
模型失败、低置信度或字段缺失时回退到规则解析
|
||||||
↓
|
↓
|
||||||
澄清追问
|
澄清追问
|
||||||
低置信度、歧义、缺槽位时不允许直接查库
|
低置信度、歧义、缺槽位时不允许直接查库
|
||||||
```
|
```
|
||||||
|
|
||||||
设计原则:
|
设计原则:
|
||||||
|
|
||||||
- 模型优先负责“理解意图和场景”。
|
- 模型优先负责“理解意图和场景”。
|
||||||
- 规则优先负责“校验、补全和兜底”。
|
- 规则优先负责“校验、补全和兜底”。
|
||||||
- 附件名称、OCR、VLM 结果只能作为证据,不等于已确认事实。
|
- 附件名称、OCR、VLM 结果只能作为证据,不等于已确认事实。
|
||||||
- 所有语义输出都必须标记置信度和来源。
|
- 所有语义输出都必须标记置信度和来源。
|
||||||
|
|
||||||
## 5. 推荐新增字段
|
## 5. 推荐新增字段
|
||||||
|
|
||||||
为支持模型优先解析,建议在扩展字段中至少增加:
|
为支持模型优先解析,建议在扩展字段中至少增加:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"missing_slots": [],
|
"missing_slots": [],
|
||||||
"ambiguity": [],
|
"ambiguity": [],
|
||||||
"field_confidence": {},
|
"field_confidence": {},
|
||||||
"field_source": {},
|
"field_source": {},
|
||||||
"attachment_context": [],
|
"attachment_context": [],
|
||||||
"parse_strategy": "llm_primary_with_rule_fallback"
|
"parse_strategy": "llm_primary_with_rule_fallback"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
字段说明:
|
字段说明:
|
||||||
|
|
||||||
- `missing_slots`:还缺哪些关键字段,例如费用类型、单据号、客户单位。
|
- `missing_slots`:还缺哪些关键字段,例如费用类型、单据号、客户单位。
|
||||||
- `ambiguity`:当前可能混淆的理解结果。
|
- `ambiguity`:当前可能混淆的理解结果。
|
||||||
- `field_confidence`:字段级置信度,而不是只给整体分数。
|
- `field_confidence`:字段级置信度,而不是只给整体分数。
|
||||||
- `field_source`:字段来自 `llm`、`rule`、`ocr`、`vlm` 还是 `user_context`。
|
- `field_source`:字段来自 `llm`、`rule`、`ocr`、`vlm` 还是 `user_context`。
|
||||||
- `attachment_context`:本次可供语义解析使用的附件摘要。
|
- `attachment_context`:本次可供语义解析使用的附件摘要。
|
||||||
- `parse_strategy`:标记本次是模型主解析还是规则回退。
|
- `parse_strategy`:标记本次是模型主解析还是规则回退。
|
||||||
|
|
||||||
## 6. 叙述型财务输入
|
## 6. 叙述型财务输入
|
||||||
|
|
||||||
语义层必须支持“不是查询句”的自然叙述。
|
语义层必须支持“不是查询句”的自然叙述。
|
||||||
|
|
||||||
典型样例:
|
典型样例:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
我今天去客户现场,招待了客户,花销了1000元
|
我今天去客户现场,招待了客户,花销了1000元
|
||||||
我垫付了打车费和餐费,帮我看看怎么报
|
我垫付了打车费和餐费,帮我看看怎么报
|
||||||
上传了三张票,帮我整理成报销草稿
|
上传了三张票,帮我整理成报销草稿
|
||||||
```
|
```
|
||||||
|
|
||||||
这类输入不能默认识别成 `query`。
|
这类输入不能默认识别成 `query`。
|
||||||
|
|
||||||
建议默认策略:
|
建议默认策略:
|
||||||
|
|
||||||
- 优先识别为 `reimbursement` 域。
|
- 优先识别为 `reimbursement` 域。
|
||||||
- 场景优先落到 `daily_expense`、`travel_reimbursement` 或 `attachment_review`。
|
- 场景优先落到 `daily_expense`、`travel_reimbursement` 或 `attachment_review`。
|
||||||
- 意图优先落到 `create`、`generate` 或 `validate`。
|
- 意图优先落到 `create`、`generate` 或 `validate`。
|
||||||
- 缺失关键字段时返回 `ask_clarification`,而不是直接查数据库。
|
- 缺失关键字段时返回 `ask_clarification`,而不是直接查数据库。
|
||||||
|
|
||||||
## 7. 模糊短句与澄清规则
|
## 7. 模糊短句与澄清规则
|
||||||
|
|
||||||
以下输入应优先追问:
|
以下输入应优先追问:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
我要报销
|
我要报销
|
||||||
这个为什么还没处理
|
这个为什么还没处理
|
||||||
帮我看一下这个
|
帮我看一下这个
|
||||||
上传好了,下一步呢
|
上传好了,下一步呢
|
||||||
```
|
```
|
||||||
|
|
||||||
处理原则:
|
处理原则:
|
||||||
|
|
||||||
- 不允许直接执行工具。
|
- 不允许直接执行工具。
|
||||||
- 不允许直接落到应收、应付查询。
|
- 不允许直接落到应收、应付查询。
|
||||||
- 必须生成澄清问题。
|
- 必须生成澄清问题。
|
||||||
- 必须在审计中记录触发追问的原因。
|
- 必须在审计中记录触发追问的原因。
|
||||||
|
|
||||||
扩展原则:
|
扩展原则:
|
||||||
|
|
||||||
- 先不要把所有字段都做成数据库列。
|
- 先不要把所有字段都做成数据库列。
|
||||||
- 语义结果建议存 JSONB。
|
- 语义结果建议存 JSONB。
|
||||||
- 使用 `schema_version` 管理版本。
|
- 使用 `schema_version` 管理版本。
|
||||||
- Orchestrator 只依赖稳定字段。
|
- Orchestrator 只依赖稳定字段。
|
||||||
- 新字段以可选方式加入,不影响老任务。
|
- 新字段以可选方式加入,不影响老任务。
|
||||||
|
|
||||||
## 4. 示例
|
## 4. 示例
|
||||||
|
|
||||||
### 4.1 用户查询应收账龄
|
### 4.1 用户查询应收账龄
|
||||||
|
|
||||||
用户问:
|
用户问:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
上个月哪些客户应收逾期超过 30 天?
|
上个月哪些客户应收逾期超过 30 天?
|
||||||
```
|
```
|
||||||
|
|
||||||
解析:
|
解析:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"domain": "accounts_receivable",
|
"domain": "accounts_receivable",
|
||||||
"scenario": "receivable_aging",
|
"scenario": "receivable_aging",
|
||||||
"intent": "query",
|
"intent": "query",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"type": "customer",
|
"type": "customer",
|
||||||
"value": "客户",
|
"value": "客户",
|
||||||
"role": "group_by"
|
"role": "group_by"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time_range": {
|
"time_range": {
|
||||||
"raw": "上个月",
|
"raw": "上个月",
|
||||||
"start": "2026-04-01",
|
"start": "2026-04-01",
|
||||||
"end": "2026-04-30",
|
"end": "2026-04-30",
|
||||||
"granularity": "month"
|
"granularity": "month"
|
||||||
},
|
},
|
||||||
"constraints": {
|
"constraints": {
|
||||||
"aging_days": ">30",
|
"aging_days": ">30",
|
||||||
"status": "overdue"
|
"status": "overdue"
|
||||||
},
|
},
|
||||||
"risk_signals": ["overdue_receivable"],
|
"risk_signals": ["overdue_receivable"],
|
||||||
"next_step": "query_database"
|
"next_step": "query_database"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.2 用户解释发票拦截
|
### 4.2 用户解释发票拦截
|
||||||
|
|
||||||
用户问:
|
用户问:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
这张发票为什么报销被拦截?
|
这张发票为什么报销被拦截?
|
||||||
```
|
```
|
||||||
|
|
||||||
解析:
|
解析:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"domain": "reimbursement",
|
"domain": "reimbursement",
|
||||||
"scenario": "invoice_validation",
|
"scenario": "invoice_validation",
|
||||||
"intent": "explain",
|
"intent": "explain",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"type": "invoice",
|
"type": "invoice",
|
||||||
"value": "这张发票",
|
"value": "这张发票",
|
||||||
"role": "target"
|
"role": "target"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time_range": {},
|
"time_range": {},
|
||||||
"constraints": {},
|
"constraints": {},
|
||||||
"risk_signals": ["unknown"],
|
"risk_signals": ["unknown"],
|
||||||
"next_step": "run_rule"
|
"next_step": "run_rule"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.3 Hermes 每日风险巡检
|
### 4.3 Hermes 每日风险巡检
|
||||||
|
|
||||||
任务配置:
|
任务配置:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"domain": "reimbursement",
|
"domain": "reimbursement",
|
||||||
"scenario": "daily_risk_scan",
|
"scenario": "daily_risk_scan",
|
||||||
"intent": "monitor",
|
"intent": "monitor",
|
||||||
"entities": [],
|
"entities": [],
|
||||||
"time_range": {
|
"time_range": {
|
||||||
"raw": "昨日"
|
"raw": "昨日"
|
||||||
},
|
},
|
||||||
"constraints": {
|
"constraints": {
|
||||||
"risk_level": ["medium", "high"]
|
"risk_level": ["medium", "high"]
|
||||||
},
|
},
|
||||||
"risk_signals": [
|
"risk_signals": [
|
||||||
"duplicate_invoice",
|
"duplicate_invoice",
|
||||||
"missing_attachment",
|
"missing_attachment",
|
||||||
"policy_overrun"
|
"policy_overrun"
|
||||||
],
|
],
|
||||||
"next_step": "run_rule"
|
"next_step": "run_rule"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
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))
|
||||||
|
|||||||
@@ -1,70 +1,70 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.core.logging import get_logger
|
from app.core.logging import get_logger
|
||||||
from app.models.audit_log import AuditLog
|
from app.models.audit_log import AuditLog
|
||||||
from app.repositories.audit_log import AuditLogRepository
|
from app.repositories.audit_log import AuditLogRepository
|
||||||
from app.schemas.audit_log import AuditLogRead
|
from app.schemas.audit_log import AuditLogRead
|
||||||
from app.services.agent_foundation import AgentFoundationService
|
from app.services.agent_foundation import AgentFoundationService
|
||||||
|
|
||||||
logger = get_logger("app.services.audit")
|
logger = get_logger("app.services.audit")
|
||||||
|
|
||||||
|
|
||||||
class AuditLogService:
|
class AuditLogService:
|
||||||
def __init__(self, db: Session) -> None:
|
def __init__(self, db: Session) -> None:
|
||||||
self.db = db
|
self.db = db
|
||||||
self.repository = AuditLogRepository(db)
|
self.repository = AuditLogRepository(db)
|
||||||
|
|
||||||
def list_logs(
|
def list_logs(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
resource_type: str | None = None,
|
resource_type: str | None = None,
|
||||||
resource_id: str | None = None,
|
resource_id: str | None = None,
|
||||||
action: str | None = None,
|
action: str | None = None,
|
||||||
limit: int = 50,
|
limit: int = 50,
|
||||||
) -> list[AuditLogRead]:
|
) -> list[AuditLogRead]:
|
||||||
self._ensure_ready()
|
self._ensure_ready()
|
||||||
items = self.repository.list(
|
items = self.repository.list(
|
||||||
resource_type=resource_type,
|
resource_type=resource_type,
|
||||||
resource_id=resource_id,
|
resource_id=resource_id,
|
||||||
action=action,
|
action=action,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
return [AuditLogRead.model_validate(item) for item in items]
|
return [AuditLogRead.model_validate(item) for item in items]
|
||||||
|
|
||||||
def log_action(
|
def log_action(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
actor: str,
|
actor: str,
|
||||||
action: str,
|
action: str,
|
||||||
resource_type: str,
|
resource_type: str,
|
||||||
resource_id: str,
|
resource_id: str,
|
||||||
before_json: dict[str, Any] | None = None,
|
before_json: dict[str, Any] | None = None,
|
||||||
after_json: dict[str, Any] | None = None,
|
after_json: dict[str, Any] | None = None,
|
||||||
request_id: str | None = None,
|
request_id: str | None = None,
|
||||||
) -> AuditLog:
|
) -> AuditLog:
|
||||||
log = AuditLog(
|
log = AuditLog(
|
||||||
actor=actor,
|
actor=actor,
|
||||||
action=action,
|
action=action,
|
||||||
resource_type=resource_type,
|
resource_type=resource_type,
|
||||||
resource_id=resource_id,
|
resource_id=resource_id,
|
||||||
before_json=before_json,
|
before_json=before_json,
|
||||||
after_json=after_json,
|
after_json=after_json,
|
||||||
request_id=request_id or uuid.uuid4().hex,
|
request_id=request_id or uuid.uuid4().hex,
|
||||||
)
|
)
|
||||||
created = self.repository.create(log)
|
created = self.repository.create(log)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Created audit log id=%s action=%s resource=%s:%s",
|
"Created audit log id=%s action=%s resource=%s:%s",
|
||||||
created.id,
|
created.id,
|
||||||
created.action,
|
created.action,
|
||||||
created.resource_type,
|
created.resource_type,
|
||||||
created.resource_id,
|
created.resource_id,
|
||||||
)
|
)
|
||||||
return created
|
return created
|
||||||
|
|
||||||
def _ensure_ready(self) -> None:
|
def _ensure_ready(self) -> None:
|
||||||
AgentFoundationService(self.db).ensure_foundation_ready()
|
AgentFoundationService(self.db).ensure_foundation_ready()
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -177,16 +177,16 @@ SLOT_LABELS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DATE_TEXT_PATTERN = re.compile(r"(\d{4}[年/-]\d{1,2}[月/-]\d{1,2}日?)")
|
DATE_TEXT_PATTERN = re.compile(r"(\d{4}[年/-]\d{1,2}[月/-]\d{1,2}日?)")
|
||||||
AMOUNT_TEXT_PATTERN = re.compile(
|
AMOUNT_TEXT_PATTERN = re.compile(
|
||||||
r"(\d+(?:\.\d+)?)\s*(?:万元|万员|万圆|万园|万块|万元整|元整|块钱|块|元|员|圆|园|万)"
|
r"(\d+(?:\.\d+)?)\s*(?:万元|万员|万圆|万园|万块|万元整|元整|块钱|块|元|员|圆|园|万)"
|
||||||
)
|
)
|
||||||
DOCUMENT_AMOUNT_PATTERN = re.compile(
|
DOCUMENT_AMOUNT_PATTERN = re.compile(
|
||||||
r"(?:价税合计|合计金额|费用合计|订单(?:总)?金额|支付(?:金额)?|实付(?:金额)?|实收(?:金额)?|总(?:额|计|价)|票价|金额|车费|消费金额)"
|
r"(?:价税合计|合计金额|费用合计|订单(?:总)?金额|支付(?:金额)?|实付(?:金额)?|实收(?:金额)?|总(?:额|计|价)|票价|金额|车费|消费金额)"
|
||||||
r"[::\s¥¥人民币]*([0-9]+(?:[.,][0-9]{1,2})?)"
|
r"[::\s¥¥人民币]*([0-9]+(?:[.,][0-9]{1,2})?)"
|
||||||
)
|
)
|
||||||
DOCUMENT_CURRENCY_AMOUNT_PATTERN = re.compile(r"[¥¥]\s*([0-9]+(?:[.,][0-9]{1,2})?)")
|
DOCUMENT_CURRENCY_AMOUNT_PATTERN = re.compile(r"[¥¥]\s*([0-9]+(?:[.,][0-9]{1,2})?)")
|
||||||
|
|
||||||
SOURCE_LABELS = {
|
SOURCE_LABELS = {
|
||||||
"user_text": "用户描述",
|
"user_text": "用户描述",
|
||||||
"user_form": "用户修改",
|
"user_form": "用户修改",
|
||||||
"ocr": "票据识别",
|
"ocr": "票据识别",
|
||||||
@@ -215,7 +215,7 @@ INFERRED_REASON_LABELS = {
|
|||||||
"welfare": "员工福利",
|
"welfare": "员工福利",
|
||||||
"other": "其他费用",
|
"other": "其他费用",
|
||||||
}
|
}
|
||||||
SYSTEM_GENERATED_REASON_PREFIXES = (
|
SYSTEM_GENERATED_REASON_PREFIXES = (
|
||||||
"我上传了",
|
"我上传了",
|
||||||
"请按当前已识别信息",
|
"请按当前已识别信息",
|
||||||
"请把当前上传的票据",
|
"请把当前上传的票据",
|
||||||
@@ -225,20 +225,20 @@ SYSTEM_GENERATED_REASON_PREFIXES = (
|
|||||||
"我已修改识别信息",
|
"我已修改识别信息",
|
||||||
"查看报销草稿",
|
"查看报销草稿",
|
||||||
"请解释一下当前这笔报销的合规风险和待补充项",
|
"请解释一下当前这笔报销的合规风险和待补充项",
|
||||||
)
|
)
|
||||||
AMOUNT_UNIT_ALIASES = {
|
AMOUNT_UNIT_ALIASES = {
|
||||||
"员": "元",
|
"员": "元",
|
||||||
"圆": "元",
|
"圆": "元",
|
||||||
"园": "元",
|
"园": "元",
|
||||||
"块": "元",
|
"块": "元",
|
||||||
"块钱": "元",
|
"块钱": "元",
|
||||||
"元整": "元",
|
"元整": "元",
|
||||||
"万员": "万元",
|
"万员": "万元",
|
||||||
"万圆": "万元",
|
"万圆": "万元",
|
||||||
"万园": "万元",
|
"万园": "万元",
|
||||||
"万块": "万元",
|
"万块": "万元",
|
||||||
"万元整": "万元",
|
"万元整": "万元",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class UserAgentService:
|
class UserAgentService:
|
||||||
@@ -1742,7 +1742,7 @@ class UserAgentService:
|
|||||||
if is_submitted:
|
if is_submitted:
|
||||||
body = (
|
body = (
|
||||||
f"主题:{subject}\n"
|
f"主题:{subject}\n"
|
||||||
f"结论:报销单已提交,当前节点为 {approval_stage or '审批中'}。\n"
|
f"结论:报销单已提交,当前节点为 {approval_stage or '审批中'}。\n"
|
||||||
"建议:后续可在个人报销列表中跟踪审批进度,必要时再补充说明或附件。\n"
|
"建议:后续可在个人报销列表中跟踪审批进度,必要时再补充说明或附件。\n"
|
||||||
f"原始问题:{payload.message}"
|
f"原始问题:{payload.message}"
|
||||||
)
|
)
|
||||||
@@ -2381,7 +2381,7 @@ class UserAgentService:
|
|||||||
if review_action == "next_step":
|
if review_action == "next_step":
|
||||||
if draft_payload is not None and draft_payload.status == "submitted":
|
if draft_payload is not None and draft_payload.status == "submitted":
|
||||||
stage_text = draft_payload.approval_stage or "审批中"
|
stage_text = draft_payload.approval_stage or "审批中"
|
||||||
return f"报销单 {draft_payload.claim_no or ''} 已提交,当前节点为 {stage_text}。".strip()
|
return f"报销单 {draft_payload.claim_no or ''} 已提交,当前节点为 {stage_text}。".strip()
|
||||||
if payload.tool_payload.get("submission_blocked"):
|
if payload.tool_payload.get("submission_blocked"):
|
||||||
return str(payload.tool_payload.get("message") or "").strip() or "当前报销单暂时还不能提交审批。"
|
return str(payload.tool_payload.get("message") or "").strip() or "当前报销单暂时还不能提交审批。"
|
||||||
return (
|
return (
|
||||||
@@ -2947,19 +2947,19 @@ class UserAgentService:
|
|||||||
"expense_type_code": "",
|
"expense_type_code": "",
|
||||||
}
|
}
|
||||||
participants: list[str] = []
|
participants: list[str] = []
|
||||||
for item in payload.ontology.entities:
|
for item in payload.ontology.entities:
|
||||||
if item.type == "employee" and not values["employee_name"]:
|
if item.type == "employee" and not values["employee_name"]:
|
||||||
values["employee_name"] = item.value
|
values["employee_name"] = item.value
|
||||||
elif item.type == "customer" and not values["customer"]:
|
elif item.type == "customer" and not values["customer"]:
|
||||||
values["customer"] = item.value
|
values["customer"] = item.value
|
||||||
elif item.type == "amount" and item.role != "threshold" and not values["amount"]:
|
elif item.type == "amount" and item.role != "threshold" and not values["amount"]:
|
||||||
normalized_amount = str(item.normalized_value or "").strip()
|
normalized_amount = str(item.normalized_value or "").strip()
|
||||||
values["amount"] = f"{normalized_amount}元" if normalized_amount else item.value
|
values["amount"] = f"{normalized_amount}元" if normalized_amount else item.value
|
||||||
elif item.type == "expense_type" and not values["expense_type_code"]:
|
elif item.type == "expense_type" and not values["expense_type_code"]:
|
||||||
values["expense_type_code"] = item.normalized_value
|
values["expense_type_code"] = item.normalized_value
|
||||||
values["expense_type"] = EXPENSE_TYPE_LABELS.get(
|
values["expense_type"] = EXPENSE_TYPE_LABELS.get(
|
||||||
item.normalized_value,
|
item.normalized_value,
|
||||||
item.value,
|
item.value,
|
||||||
)
|
)
|
||||||
elif item.type in {"participant", "person"} and item.value.strip():
|
elif item.type in {"participant", "person"} and item.value.strip():
|
||||||
participants.append(item.value.strip())
|
participants.append(item.value.strip())
|
||||||
@@ -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(
|
||||||
@@ -3358,17 +3362,17 @@ class UserAgentService:
|
|||||||
return self._build_slot_value()
|
return self._build_slot_value()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _normalize_amount_text(value: str) -> str:
|
def _normalize_amount_text(value: str) -> str:
|
||||||
cleaned = str(value or "").strip()
|
cleaned = str(value or "").strip()
|
||||||
if not cleaned:
|
if not cleaned:
|
||||||
return ""
|
return ""
|
||||||
for alias, canonical in sorted(AMOUNT_UNIT_ALIASES.items(), key=lambda item: len(item[0]), reverse=True):
|
for alias, canonical in sorted(AMOUNT_UNIT_ALIASES.items(), key=lambda item: len(item[0]), reverse=True):
|
||||||
cleaned = cleaned.replace(alias, canonical)
|
cleaned = cleaned.replace(alias, canonical)
|
||||||
match = AMOUNT_TEXT_PATTERN.search(cleaned)
|
match = AMOUNT_TEXT_PATTERN.search(cleaned)
|
||||||
if not match:
|
if not match:
|
||||||
return cleaned
|
return cleaned
|
||||||
number = float(match.group(1))
|
number = float(match.group(1))
|
||||||
return f"{number:.2f}元"
|
return f"{number:.2f}元"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _normalize_expense_type_input(value: str) -> tuple[str, str]:
|
def _normalize_expense_type_input(value: str) -> tuple[str, str]:
|
||||||
|
|||||||
@@ -1,139 +1,139 @@
|
|||||||
README.md
|
README.md
|
||||||
pyproject.toml
|
pyproject.toml
|
||||||
src/app/__init__.py
|
src/app/__init__.py
|
||||||
src/app/main.py
|
src/app/main.py
|
||||||
src/app/api/__init__.py
|
src/app/api/__init__.py
|
||||||
src/app/api/deps.py
|
src/app/api/deps.py
|
||||||
src/app/api/router.py
|
src/app/api/router.py
|
||||||
src/app/api/v1/__init__.py
|
src/app/api/v1/__init__.py
|
||||||
src/app/api/v1/router.py
|
src/app/api/v1/router.py
|
||||||
src/app/api/v1/endpoints/__init__.py
|
src/app/api/v1/endpoints/__init__.py
|
||||||
src/app/api/v1/endpoints/agent_assets.py
|
src/app/api/v1/endpoints/agent_assets.py
|
||||||
src/app/api/v1/endpoints/agent_runs.py
|
src/app/api/v1/endpoints/agent_runs.py
|
||||||
src/app/api/v1/endpoints/audit_logs.py
|
src/app/api/v1/endpoints/audit_logs.py
|
||||||
src/app/api/v1/endpoints/auth.py
|
src/app/api/v1/endpoints/auth.py
|
||||||
src/app/api/v1/endpoints/bootstrap.py
|
src/app/api/v1/endpoints/bootstrap.py
|
||||||
src/app/api/v1/endpoints/employees.py
|
src/app/api/v1/endpoints/employees.py
|
||||||
src/app/api/v1/endpoints/health.py
|
src/app/api/v1/endpoints/health.py
|
||||||
src/app/api/v1/endpoints/knowledge.py
|
src/app/api/v1/endpoints/knowledge.py
|
||||||
src/app/api/v1/endpoints/ocr.py
|
src/app/api/v1/endpoints/ocr.py
|
||||||
src/app/api/v1/endpoints/ontology.py
|
src/app/api/v1/endpoints/ontology.py
|
||||||
src/app/api/v1/endpoints/orchestrator.py
|
src/app/api/v1/endpoints/orchestrator.py
|
||||||
src/app/api/v1/endpoints/reimbursements.py
|
src/app/api/v1/endpoints/reimbursements.py
|
||||||
src/app/api/v1/endpoints/settings.py
|
src/app/api/v1/endpoints/settings.py
|
||||||
src/app/api/v1/endpoints/system_logs.py
|
src/app/api/v1/endpoints/system_logs.py
|
||||||
src/app/core/__init__.py
|
src/app/core/__init__.py
|
||||||
src/app/core/admin_secret.py
|
src/app/core/admin_secret.py
|
||||||
src/app/core/agent_enums.py
|
src/app/core/agent_enums.py
|
||||||
src/app/core/bootstrap.py
|
src/app/core/bootstrap.py
|
||||||
src/app/core/config.py
|
src/app/core/config.py
|
||||||
src/app/core/logging.py
|
src/app/core/logging.py
|
||||||
src/app/core/openapi.py
|
src/app/core/openapi.py
|
||||||
src/app/core/secret_box.py
|
src/app/core/secret_box.py
|
||||||
src/app/core/security.py
|
src/app/core/security.py
|
||||||
src/app/db/__init__.py
|
src/app/db/__init__.py
|
||||||
src/app/db/base.py
|
src/app/db/base.py
|
||||||
src/app/db/base_class.py
|
src/app/db/base_class.py
|
||||||
src/app/db/session.py
|
src/app/db/session.py
|
||||||
src/app/middleware/__init__.py
|
src/app/middleware/__init__.py
|
||||||
src/app/middleware/logging.py
|
src/app/middleware/logging.py
|
||||||
src/app/models/__init__.py
|
src/app/models/__init__.py
|
||||||
src/app/models/agent_asset.py
|
src/app/models/agent_asset.py
|
||||||
src/app/models/agent_conversation.py
|
src/app/models/agent_conversation.py
|
||||||
src/app/models/agent_run.py
|
src/app/models/agent_run.py
|
||||||
src/app/models/approval.py
|
src/app/models/approval.py
|
||||||
src/app/models/audit_log.py
|
src/app/models/audit_log.py
|
||||||
src/app/models/employee.py
|
src/app/models/employee.py
|
||||||
src/app/models/employee_change_log.py
|
src/app/models/employee_change_log.py
|
||||||
src/app/models/financial_record.py
|
src/app/models/financial_record.py
|
||||||
src/app/models/organization.py
|
src/app/models/organization.py
|
||||||
src/app/models/reimbursement.py
|
src/app/models/reimbursement.py
|
||||||
src/app/models/role.py
|
src/app/models/role.py
|
||||||
src/app/models/system_model_setting.py
|
src/app/models/system_model_setting.py
|
||||||
src/app/models/system_setting.py
|
src/app/models/system_setting.py
|
||||||
src/app/models/system_setting_secret.py
|
src/app/models/system_setting_secret.py
|
||||||
src/app/repositories/__init__.py
|
src/app/repositories/__init__.py
|
||||||
src/app/repositories/agent_asset.py
|
src/app/repositories/agent_asset.py
|
||||||
src/app/repositories/agent_run.py
|
src/app/repositories/agent_run.py
|
||||||
src/app/repositories/audit_log.py
|
src/app/repositories/audit_log.py
|
||||||
src/app/repositories/employee.py
|
src/app/repositories/employee.py
|
||||||
src/app/repositories/reimbursement.py
|
src/app/repositories/reimbursement.py
|
||||||
src/app/repositories/settings.py
|
src/app/repositories/settings.py
|
||||||
src/app/schemas/__init__.py
|
src/app/schemas/__init__.py
|
||||||
src/app/schemas/agent_asset.py
|
src/app/schemas/agent_asset.py
|
||||||
src/app/schemas/agent_run.py
|
src/app/schemas/agent_run.py
|
||||||
src/app/schemas/audit_log.py
|
src/app/schemas/audit_log.py
|
||||||
src/app/schemas/auth.py
|
src/app/schemas/auth.py
|
||||||
src/app/schemas/bootstrap.py
|
src/app/schemas/bootstrap.py
|
||||||
src/app/schemas/common.py
|
src/app/schemas/common.py
|
||||||
src/app/schemas/employee.py
|
src/app/schemas/employee.py
|
||||||
src/app/schemas/knowledge.py
|
src/app/schemas/knowledge.py
|
||||||
src/app/schemas/ocr.py
|
src/app/schemas/ocr.py
|
||||||
src/app/schemas/ontology.py
|
src/app/schemas/ontology.py
|
||||||
src/app/schemas/orchestrator.py
|
src/app/schemas/orchestrator.py
|
||||||
src/app/schemas/reimbursement.py
|
src/app/schemas/reimbursement.py
|
||||||
src/app/schemas/settings.py
|
src/app/schemas/settings.py
|
||||||
src/app/schemas/system_log.py
|
src/app/schemas/system_log.py
|
||||||
src/app/schemas/user_agent.py
|
src/app/schemas/user_agent.py
|
||||||
src/app/services/__init__.py
|
src/app/services/__init__.py
|
||||||
src/app/services/agent_asset_spreadsheet.py
|
src/app/services/agent_asset_spreadsheet.py
|
||||||
src/app/services/agent_assets.py
|
src/app/services/agent_assets.py
|
||||||
src/app/services/agent_conversations.py
|
src/app/services/agent_conversations.py
|
||||||
src/app/services/agent_foundation.py
|
src/app/services/agent_foundation.py
|
||||||
src/app/services/agent_runs.py
|
src/app/services/agent_runs.py
|
||||||
src/app/services/audit.py
|
src/app/services/audit.py
|
||||||
src/app/services/auth.py
|
src/app/services/auth.py
|
||||||
src/app/services/document_intelligence.py
|
src/app/services/document_intelligence.py
|
||||||
src/app/services/employee.py
|
src/app/services/employee.py
|
||||||
src/app/services/employee_seed.py
|
src/app/services/employee_seed.py
|
||||||
src/app/services/expense_claims.py
|
src/app/services/expense_claims.py
|
||||||
src/app/services/expense_rule_runtime.py
|
src/app/services/expense_rule_runtime.py
|
||||||
src/app/services/hermes_sync.py
|
src/app/services/hermes_sync.py
|
||||||
src/app/services/knowledge.py
|
src/app/services/knowledge.py
|
||||||
src/app/services/knowledge_index_tasks.py
|
src/app/services/knowledge_index_tasks.py
|
||||||
src/app/services/knowledge_normalizer.py
|
src/app/services/knowledge_normalizer.py
|
||||||
src/app/services/knowledge_rag.py
|
src/app/services/knowledge_rag.py
|
||||||
src/app/services/knowledge_scheduler.py
|
src/app/services/knowledge_scheduler.py
|
||||||
src/app/services/knowledge_sync.py
|
src/app/services/knowledge_sync.py
|
||||||
src/app/services/model_connectivity.py
|
src/app/services/model_connectivity.py
|
||||||
src/app/services/ocr.py
|
src/app/services/ocr.py
|
||||||
src/app/services/ontology.py
|
src/app/services/ontology.py
|
||||||
src/app/services/orchestrator.py
|
src/app/services/orchestrator.py
|
||||||
src/app/services/reimbursement.py
|
src/app/services/reimbursement.py
|
||||||
src/app/services/runtime_chat.py
|
src/app/services/runtime_chat.py
|
||||||
src/app/services/settings.py
|
src/app/services/settings.py
|
||||||
src/app/services/system_hermes.py
|
src/app/services/system_hermes.py
|
||||||
src/app/services/system_logs.py
|
src/app/services/system_logs.py
|
||||||
src/app/services/user_agent.py
|
src/app/services/user_agent.py
|
||||||
src/x_financial_server.egg-info/PKG-INFO
|
src/x_financial_server.egg-info/PKG-INFO
|
||||||
src/x_financial_server.egg-info/SOURCES.txt
|
src/x_financial_server.egg-info/SOURCES.txt
|
||||||
src/x_financial_server.egg-info/dependency_links.txt
|
src/x_financial_server.egg-info/dependency_links.txt
|
||||||
src/x_financial_server.egg-info/requires.txt
|
src/x_financial_server.egg-info/requires.txt
|
||||||
src/x_financial_server.egg-info/top_level.txt
|
src/x_financial_server.egg-info/top_level.txt
|
||||||
tests/test_agent_asset_onlyoffice_key.py
|
tests/test_agent_asset_onlyoffice_key.py
|
||||||
tests/test_agent_asset_service.py
|
tests/test_agent_asset_service.py
|
||||||
tests/test_agent_asset_spreadsheet_import.py
|
tests/test_agent_asset_spreadsheet_import.py
|
||||||
tests/test_agent_foundation_endpoints.py
|
tests/test_agent_foundation_endpoints.py
|
||||||
tests/test_agent_runs_service.py
|
tests/test_agent_runs_service.py
|
||||||
tests/test_auth_service.py
|
tests/test_auth_service.py
|
||||||
tests/test_config_settings_reload.py
|
tests/test_config_settings_reload.py
|
||||||
tests/test_document_intelligence.py
|
tests/test_document_intelligence.py
|
||||||
tests/test_employee_service.py
|
tests/test_employee_service.py
|
||||||
tests/test_env_file_precedence.py
|
tests/test_env_file_precedence.py
|
||||||
tests/test_expense_claim_service.py
|
tests/test_expense_claim_service.py
|
||||||
tests/test_imports.py
|
tests/test_imports.py
|
||||||
tests/test_knowledge_normalizer.py
|
tests/test_knowledge_normalizer.py
|
||||||
tests/test_knowledge_onlyoffice_config.py
|
tests/test_knowledge_onlyoffice_config.py
|
||||||
tests/test_knowledge_rag_service.py
|
tests/test_knowledge_rag_service.py
|
||||||
tests/test_knowledge_service.py
|
tests/test_knowledge_service.py
|
||||||
tests/test_ocr_endpoints.py
|
tests/test_ocr_endpoints.py
|
||||||
tests/test_ocr_service.py
|
tests/test_ocr_service.py
|
||||||
tests/test_ontology_service.py
|
tests/test_ontology_service.py
|
||||||
tests/test_openapi_schema.py
|
tests/test_openapi_schema.py
|
||||||
tests/test_reimbursement_endpoints.py
|
tests/test_reimbursement_endpoints.py
|
||||||
tests/test_runtime_chat_service.py
|
tests/test_runtime_chat_service.py
|
||||||
tests/test_server_start_dependencies.py
|
tests/test_server_start_dependencies.py
|
||||||
tests/test_settings_persistence.py
|
tests/test_settings_persistence.py
|
||||||
tests/test_settings_service.py
|
tests/test_settings_service.py
|
||||||
tests/test_system_logs_service.py
|
tests/test_system_logs_service.py
|
||||||
tests/test_user_agent_service.py
|
tests/test_user_agent_service.py
|
||||||
@@ -1,84 +1,84 @@
|
|||||||
{
|
{
|
||||||
"file_name": "行程单_2_鄂AX9877.pdf",
|
"file_name": "行程单_2_鄂AX9877.pdf",
|
||||||
"storage_key": "0d3102fe-a458-42cf-b30c-4cffeeb74668/c99de539-23cb-4f1a-a21f-a40bce93d54e/行程单_2_鄂AX9877.pdf",
|
"storage_key": "0d3102fe-a458-42cf-b30c-4cffeeb74668/c99de539-23cb-4f1a-a21f-a40bce93d54e/行程单_2_鄂AX9877.pdf",
|
||||||
"media_type": "application/pdf",
|
"media_type": "application/pdf",
|
||||||
"size_bytes": 32459,
|
"size_bytes": 32459,
|
||||||
"uploaded_at": "2026-05-16T08:41:42.540134+00:00",
|
"uploaded_at": "2026-05-16T08:41:42.540134+00:00",
|
||||||
"previewable": true,
|
"previewable": true,
|
||||||
"preview_kind": "image",
|
"preview_kind": "image",
|
||||||
"preview_storage_key": "0d3102fe-a458-42cf-b30c-4cffeeb74668/c99de539-23cb-4f1a-a21f-a40bce93d54e/行程单_2_鄂AX9877.preview.png",
|
"preview_storage_key": "0d3102fe-a458-42cf-b30c-4cffeeb74668/c99de539-23cb-4f1a-a21f-a40bce93d54e/行程单_2_鄂AX9877.preview.png",
|
||||||
"preview_media_type": "image/png",
|
"preview_media_type": "image/png",
|
||||||
"preview_file_name": "行程单_2_鄂AX9877.preview.png",
|
"preview_file_name": "行程单_2_鄂AX9877.preview.png",
|
||||||
"analysis": {
|
"analysis": {
|
||||||
"severity": "pass",
|
"severity": "pass",
|
||||||
"label": "AI提示符合条件",
|
"label": "AI提示符合条件",
|
||||||
"headline": "AI提示:附件符合基础校验条件",
|
"headline": "AI提示:附件符合基础校验条件",
|
||||||
"summary": "已识别到票据类型和关键字段,且符合当前费用场景的附件要求。",
|
"summary": "已识别到票据类型和关键字段,且符合当前费用场景的附件要求。",
|
||||||
"points": [
|
"points": [
|
||||||
"票据类型:已识别为出租车/网约车票据。",
|
"票据类型:已识别为出租车/网约车票据。",
|
||||||
"附件类型要求:当前费用项目为交通费,已识别为出租车/网约车票据,符合当前交通费场景的附件要求。",
|
"附件类型要求:当前费用项目为交通费,已识别为出租车/网约车票据,符合当前交通费场景的附件要求。",
|
||||||
"金额字段:已识别到与当前明细接近的金额 35.53 元。"
|
"金额字段:已识别到与当前明细接近的金额 35.53 元。"
|
||||||
],
|
],
|
||||||
"suggestion": "建议继续核对报销分类、费用说明和业务场景是否一致。"
|
"suggestion": "建议继续核对报销分类、费用说明和业务场景是否一致。"
|
||||||
},
|
},
|
||||||
"document_info": {
|
"document_info": {
|
||||||
"document_type": "taxi_receipt",
|
"document_type": "taxi_receipt",
|
||||||
"document_type_label": "出租车/网约车票据",
|
"document_type_label": "出租车/网约车票据",
|
||||||
"scene_code": "transport",
|
"scene_code": "transport",
|
||||||
"scene_label": "交通票据",
|
"scene_label": "交通票据",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"key": "amount",
|
"key": "amount",
|
||||||
"label": "金额",
|
"label": "金额",
|
||||||
"value": "35.53元"
|
"value": "35.53元"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "date",
|
"key": "date",
|
||||||
"label": "日期",
|
"label": "日期",
|
||||||
"value": "2026-03-04"
|
"value": "2026-03-04"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "merchant_name",
|
"key": "merchant_name",
|
||||||
"label": "商户",
|
"label": "商户",
|
||||||
"value": "全季酒店"
|
"value": "全季酒店"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"requirement_check": {
|
"requirement_check": {
|
||||||
"matches": true,
|
"matches": true,
|
||||||
"current_expense_type": "transport",
|
"current_expense_type": "transport",
|
||||||
"current_expense_type_label": "交通费",
|
"current_expense_type_label": "交通费",
|
||||||
"allowed_scene_labels": [
|
"allowed_scene_labels": [
|
||||||
"交通"
|
"交通"
|
||||||
],
|
],
|
||||||
"allowed_document_type_labels": [
|
"allowed_document_type_labels": [
|
||||||
"停车/通行费票据",
|
"停车/通行费票据",
|
||||||
"一般收据/凭证",
|
"一般收据/凭证",
|
||||||
"出租车/网约车票据",
|
"出租车/网约车票据",
|
||||||
"增值税发票"
|
"增值税发票"
|
||||||
],
|
],
|
||||||
"recognized_scene_code": "transport",
|
"recognized_scene_code": "transport",
|
||||||
"recognized_scene_label": "交通票据",
|
"recognized_scene_label": "交通票据",
|
||||||
"recognized_document_type": "taxi_receipt",
|
"recognized_document_type": "taxi_receipt",
|
||||||
"recognized_document_type_label": "出租车/网约车票据",
|
"recognized_document_type_label": "出租车/网约车票据",
|
||||||
"mismatch_severity": "high",
|
"mismatch_severity": "high",
|
||||||
"rule_code": "rule.expense.scene_submission_standard",
|
"rule_code": "rule.expense.scene_submission_standard",
|
||||||
"rule_name": "报销场景提交与附件标准",
|
"rule_name": "报销场景提交与附件标准",
|
||||||
"message": "当前费用项目为交通费,已识别为出租车/网约车票据,符合当前交通费场景的附件要求。"
|
"message": "当前费用项目为交通费,已识别为出租车/网约车票据,符合当前交通费场景的附件要求。"
|
||||||
},
|
},
|
||||||
"ocr_status": "recognized",
|
"ocr_status": "recognized",
|
||||||
"ocr_error": "",
|
"ocr_error": "",
|
||||||
"ocr_text": "高德地图一打车\n行程单\nAMAP ITINERARY\n1申请时间:2026-03-04\n【行程时间:2026-03-0407:05至2026-03-0407:33\n|行程人手机号:18602700270\n1共计1单行程,合计35.53元\n序号\n服务商\n车型\n上车时间\n城市\n起点\n终点\n金额\n经济型\n2026-03-04\n1\n滴滴出行\n武汉市\n全季酒店武汉工程大学店\n武汉站\n35.53元\n07:05\n页码:1/1",
|
"ocr_text": "高德地图一打车\n行程单\nAMAP ITINERARY\n1申请时间:2026-03-04\n【行程时间:2026-03-0407:05至2026-03-0407:33\n|行程人手机号:18602700270\n1共计1单行程,合计35.53元\n序号\n服务商\n车型\n上车时间\n城市\n起点\n终点\n金额\n经济型\n2026-03-04\n1\n滴滴出行\n武汉市\n全季酒店武汉工程大学店\n武汉站\n35.53元\n07:05\n页码:1/1",
|
||||||
"ocr_summary": "高德地图一打车;行程单;AMAP ITINERARY",
|
"ocr_summary": "高德地图一打车;行程单;AMAP ITINERARY",
|
||||||
"ocr_avg_score": 0.9819406509399414,
|
"ocr_avg_score": 0.9819406509399414,
|
||||||
"ocr_line_count": 25,
|
"ocr_line_count": 25,
|
||||||
"ocr_classification_source": "rule",
|
"ocr_classification_source": "rule",
|
||||||
"ocr_classification_confidence": 0.88,
|
"ocr_classification_confidence": 0.88,
|
||||||
"ocr_classification_evidence": [
|
"ocr_classification_evidence": [
|
||||||
"滴滴出行",
|
"滴滴出行",
|
||||||
"滴滴",
|
"滴滴",
|
||||||
"打车",
|
"打车",
|
||||||
"上车"
|
"上车"
|
||||||
],
|
],
|
||||||
"ocr_warnings": []
|
"ocr_warnings": []
|
||||||
}
|
}
|
||||||
@@ -1,84 +1,84 @@
|
|||||||
{
|
{
|
||||||
"file_name": "行程单_1_鄂A1S987.pdf",
|
"file_name": "行程单_1_鄂A1S987.pdf",
|
||||||
"storage_key": "281095c5-d85b-428e-924f-250bdd6e0261/c676b663-8851-4b35-be4e-1ba13d46d35e/行程单_1_鄂A1S987.pdf",
|
"storage_key": "281095c5-d85b-428e-924f-250bdd6e0261/c676b663-8851-4b35-be4e-1ba13d46d35e/行程单_1_鄂A1S987.pdf",
|
||||||
"media_type": "application/pdf",
|
"media_type": "application/pdf",
|
||||||
"size_bytes": 34880,
|
"size_bytes": 34880,
|
||||||
"uploaded_at": "2026-05-16T08:17:53.656595+00:00",
|
"uploaded_at": "2026-05-16T08:17:53.656595+00:00",
|
||||||
"previewable": true,
|
"previewable": true,
|
||||||
"preview_kind": "image",
|
"preview_kind": "image",
|
||||||
"preview_storage_key": "281095c5-d85b-428e-924f-250bdd6e0261/c676b663-8851-4b35-be4e-1ba13d46d35e/行程单_1_鄂A1S987.preview.png",
|
"preview_storage_key": "281095c5-d85b-428e-924f-250bdd6e0261/c676b663-8851-4b35-be4e-1ba13d46d35e/行程单_1_鄂A1S987.preview.png",
|
||||||
"preview_media_type": "image/png",
|
"preview_media_type": "image/png",
|
||||||
"preview_file_name": "行程单_1_鄂A1S987.preview.png",
|
"preview_file_name": "行程单_1_鄂A1S987.preview.png",
|
||||||
"analysis": {
|
"analysis": {
|
||||||
"severity": "pass",
|
"severity": "pass",
|
||||||
"label": "AI提示符合条件",
|
"label": "AI提示符合条件",
|
||||||
"headline": "AI提示:附件符合基础校验条件",
|
"headline": "AI提示:附件符合基础校验条件",
|
||||||
"summary": "已识别到票据类型和关键字段,且符合当前费用场景的附件要求。",
|
"summary": "已识别到票据类型和关键字段,且符合当前费用场景的附件要求。",
|
||||||
"points": [
|
"points": [
|
||||||
"票据类型:已识别为出租车/网约车票据。",
|
"票据类型:已识别为出租车/网约车票据。",
|
||||||
"附件类型要求:当前费用项目为交通费,已识别为出租车/网约车票据,符合当前交通费场景的附件要求。",
|
"附件类型要求:当前费用项目为交通费,已识别为出租车/网约车票据,符合当前交通费场景的附件要求。",
|
||||||
"金额字段:已识别到与当前明细接近的金额 10.30 元。"
|
"金额字段:已识别到与当前明细接近的金额 10.30 元。"
|
||||||
],
|
],
|
||||||
"suggestion": "建议继续核对报销分类、费用说明和业务场景是否一致。"
|
"suggestion": "建议继续核对报销分类、费用说明和业务场景是否一致。"
|
||||||
},
|
},
|
||||||
"document_info": {
|
"document_info": {
|
||||||
"document_type": "taxi_receipt",
|
"document_type": "taxi_receipt",
|
||||||
"document_type_label": "出租车/网约车票据",
|
"document_type_label": "出租车/网约车票据",
|
||||||
"scene_code": "transport",
|
"scene_code": "transport",
|
||||||
"scene_label": "交通票据",
|
"scene_label": "交通票据",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"key": "amount",
|
"key": "amount",
|
||||||
"label": "金额",
|
"label": "金额",
|
||||||
"value": "10.3元"
|
"value": "10.3元"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "date",
|
"key": "date",
|
||||||
"label": "日期",
|
"label": "日期",
|
||||||
"value": "2026-03-01"
|
"value": "2026-03-01"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "merchant_name",
|
"key": "merchant_name",
|
||||||
"label": "商户",
|
"label": "商户",
|
||||||
"value": "全季酒店"
|
"value": "全季酒店"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"requirement_check": {
|
"requirement_check": {
|
||||||
"matches": true,
|
"matches": true,
|
||||||
"current_expense_type": "transport",
|
"current_expense_type": "transport",
|
||||||
"current_expense_type_label": "交通费",
|
"current_expense_type_label": "交通费",
|
||||||
"allowed_scene_labels": [
|
"allowed_scene_labels": [
|
||||||
"交通"
|
"交通"
|
||||||
],
|
],
|
||||||
"allowed_document_type_labels": [
|
"allowed_document_type_labels": [
|
||||||
"停车/通行费票据",
|
"停车/通行费票据",
|
||||||
"一般收据/凭证",
|
"一般收据/凭证",
|
||||||
"出租车/网约车票据",
|
"出租车/网约车票据",
|
||||||
"增值税发票"
|
"增值税发票"
|
||||||
],
|
],
|
||||||
"recognized_scene_code": "transport",
|
"recognized_scene_code": "transport",
|
||||||
"recognized_scene_label": "交通票据",
|
"recognized_scene_label": "交通票据",
|
||||||
"recognized_document_type": "taxi_receipt",
|
"recognized_document_type": "taxi_receipt",
|
||||||
"recognized_document_type_label": "出租车/网约车票据",
|
"recognized_document_type_label": "出租车/网约车票据",
|
||||||
"mismatch_severity": "high",
|
"mismatch_severity": "high",
|
||||||
"rule_code": "rule.expense.scene_submission_standard",
|
"rule_code": "rule.expense.scene_submission_standard",
|
||||||
"rule_name": "报销场景提交与附件标准",
|
"rule_name": "报销场景提交与附件标准",
|
||||||
"message": "当前费用项目为交通费,已识别为出租车/网约车票据,符合当前交通费场景的附件要求。"
|
"message": "当前费用项目为交通费,已识别为出租车/网约车票据,符合当前交通费场景的附件要求。"
|
||||||
},
|
},
|
||||||
"ocr_status": "recognized",
|
"ocr_status": "recognized",
|
||||||
"ocr_error": "",
|
"ocr_error": "",
|
||||||
"ocr_text": "高德地图一打车\n行程单\nAMAP ITINERARY\n1申请时间:2026-03-01\n【行程时间:2026-03-0113:23至2026-03-0113:40\n行程人手机号:18602700270\n|共计1单行程,合计10.30元\n序号\n服务商\n车型\n上车时间\n城市\n起点\n终点\n金额\n经济型\n2026-03-01\n1\n滴滴出行\n13:23\n武汉市\n金融港北地铁站\n全季酒店武汉工程大学店\n10.30元\n页码:1/1",
|
"ocr_text": "高德地图一打车\n行程单\nAMAP ITINERARY\n1申请时间:2026-03-01\n【行程时间:2026-03-0113:23至2026-03-0113:40\n行程人手机号:18602700270\n|共计1单行程,合计10.30元\n序号\n服务商\n车型\n上车时间\n城市\n起点\n终点\n金额\n经济型\n2026-03-01\n1\n滴滴出行\n13:23\n武汉市\n金融港北地铁站\n全季酒店武汉工程大学店\n10.30元\n页码:1/1",
|
||||||
"ocr_summary": "高德地图一打车;行程单;AMAP ITINERARY",
|
"ocr_summary": "高德地图一打车;行程单;AMAP ITINERARY",
|
||||||
"ocr_avg_score": 0.9844024634361267,
|
"ocr_avg_score": 0.9844024634361267,
|
||||||
"ocr_line_count": 25,
|
"ocr_line_count": 25,
|
||||||
"ocr_classification_source": "rule",
|
"ocr_classification_source": "rule",
|
||||||
"ocr_classification_confidence": 0.88,
|
"ocr_classification_confidence": 0.88,
|
||||||
"ocr_classification_evidence": [
|
"ocr_classification_evidence": [
|
||||||
"滴滴出行",
|
"滴滴出行",
|
||||||
"滴滴",
|
"滴滴",
|
||||||
"打车",
|
"打车",
|
||||||
"上车"
|
"上车"
|
||||||
],
|
],
|
||||||
"ocr_warnings": []
|
"ocr_warnings": []
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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 it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,268 +1,268 @@
|
|||||||
{
|
{
|
||||||
"2c1cb358f08d44ceb0e4d287133206ec": {
|
"2c1cb358f08d44ceb0e4d287133206ec": {
|
||||||
"entity_names": [
|
"entity_names": [
|
||||||
"工会委员会",
|
"工会委员会",
|
||||||
"Business Original Documents",
|
"Business Original Documents",
|
||||||
"First Approver",
|
"First Approver",
|
||||||
"P8",
|
"P8",
|
||||||
"一级部门总经理",
|
"一级部门总经理",
|
||||||
"组织人事部",
|
"组织人事部",
|
||||||
"业务原始凭据",
|
"业务原始凭据",
|
||||||
"营销中心",
|
"营销中心",
|
||||||
"保证金",
|
"保证金",
|
||||||
"投标保证金",
|
"投标保证金",
|
||||||
"餐补",
|
"餐补",
|
||||||
"第十四条业务招待费",
|
"第十四条业务招待费",
|
||||||
"Chief Engineer",
|
"Chief Engineer",
|
||||||
"业务招待",
|
"业务招待",
|
||||||
"Employee Welfare",
|
"Employee Welfare",
|
||||||
"经济舱",
|
"经济舱",
|
||||||
"2024年4月17日",
|
"2024年4月17日",
|
||||||
"三等舱",
|
"三等舱",
|
||||||
"财务信息化系统",
|
"财务信息化系统",
|
||||||
"分管领导",
|
"分管领导",
|
||||||
"重点支出管理规定",
|
"重点支出管理规定",
|
||||||
"备用金借款",
|
"备用金借款",
|
||||||
"Financial Review",
|
"Financial Review",
|
||||||
"第五章附则",
|
"第五章附则",
|
||||||
"Company Leadership",
|
"Company Leadership",
|
||||||
"第十九条",
|
"第十九条",
|
||||||
"经办人",
|
"经办人",
|
||||||
"预算内支出",
|
"预算内支出",
|
||||||
"Current Account Payment",
|
"Current Account Payment",
|
||||||
"Business Entertainment",
|
"Business Entertainment",
|
||||||
"Tax Control System Details",
|
"Tax Control System Details",
|
||||||
"第二十一条",
|
"第二十一条",
|
||||||
"成本中心归属",
|
"成本中心归属",
|
||||||
"岗位支出报销审批权限表",
|
"岗位支出报销审批权限表",
|
||||||
"工会经费管理办法",
|
"工会经费管理办法",
|
||||||
"商旅系统",
|
"商旅系统",
|
||||||
"Special Subsidy",
|
"Special Subsidy",
|
||||||
"中国银行外汇折算价",
|
"中国银行外汇折算价",
|
||||||
"因公借款",
|
"因公借款",
|
||||||
"资产采购",
|
"资产采购",
|
||||||
"广告费",
|
"广告费",
|
||||||
"First-Level Department General Manager",
|
"First-Level Department General Manager",
|
||||||
"正式员工",
|
"正式员工",
|
||||||
"一万元",
|
"一万元",
|
||||||
"公司员工教育培训管理办法",
|
"公司员工教育培训管理办法",
|
||||||
"责任原则",
|
"责任原则",
|
||||||
"第二章职责分工",
|
"第二章职责分工",
|
||||||
"预算先行",
|
"预算先行",
|
||||||
"Planning and Finance Department",
|
"Planning and Finance Department",
|
||||||
"Accommodation Cost Reimbursement",
|
"Accommodation Cost Reimbursement",
|
||||||
"Official Vehicle Subsidy",
|
"Official Vehicle Subsidy",
|
||||||
"第四条归口管理部门主要职责",
|
"第四条归口管理部门主要职责",
|
||||||
"Personal Service Compensation",
|
"Personal Service Compensation",
|
||||||
"邮递费",
|
"邮递费",
|
||||||
"附表3:支出归口管理部门与归口业务范围",
|
"附表3:支出归口管理部门与归口业务范围",
|
||||||
"员工",
|
"员工",
|
||||||
"第二条目的",
|
"第二条目的",
|
||||||
"Director",
|
"Director",
|
||||||
"支出归口管理部门与归口业务范围",
|
"支出归口管理部门与归口业务范围",
|
||||||
"其他支出(员工)",
|
"其他支出(员工)",
|
||||||
"报销标准",
|
"报销标准",
|
||||||
"5000000 Yuan Approval Limit",
|
"5000000 Yuan Approval Limit",
|
||||||
"第十一条备用金借款",
|
"第十一条备用金借款",
|
||||||
"会议费",
|
"会议费",
|
||||||
"第十七条",
|
"第十七条",
|
||||||
"第七条各级管理人员主要职责",
|
"第七条各级管理人员主要职责",
|
||||||
"50000 Yuan Approval Limit",
|
"50000 Yuan Approval Limit",
|
||||||
"全资子公司",
|
"全资子公司",
|
||||||
"涉外业务汇率标准",
|
"涉外业务汇率标准",
|
||||||
"总监",
|
"总监",
|
||||||
"第十三条差旅费",
|
"第十三条差旅费",
|
||||||
"审批权限表",
|
"审批权限表",
|
||||||
"商旅订票规范",
|
"商旅订票规范",
|
||||||
"Final Approval Position",
|
"Final Approval Position",
|
||||||
"报销资格",
|
"报销资格",
|
||||||
"新增报销规定",
|
"新增报销规定",
|
||||||
"公司支出管理办法",
|
"公司支出管理办法",
|
||||||
"Institution General Manager",
|
"Institution General Manager",
|
||||||
"房屋租金",
|
"房屋租金",
|
||||||
"Staff Activities",
|
"Staff Activities",
|
||||||
"分包外包(内部单位)",
|
"分包外包(内部单位)",
|
||||||
"报销申请时限",
|
"报销申请时限",
|
||||||
"Financial Information System",
|
"Financial Information System",
|
||||||
"Expenditure Authorization Approval Scope",
|
"Expenditure Authorization Approval Scope",
|
||||||
"直辖市",
|
"直辖市",
|
||||||
"培训费",
|
"培训费",
|
||||||
"第十二条市内交通费",
|
"第十二条市内交通费",
|
||||||
"第十五条",
|
"第十五条",
|
||||||
"终审岗",
|
"终审岗",
|
||||||
"Remote Work Housing",
|
"Remote Work Housing",
|
||||||
"Centralized Management department",
|
"Centralized Management department",
|
||||||
"第二十条",
|
"第二十条",
|
||||||
"办公室(党委办公室)",
|
"办公室(党委办公室)",
|
||||||
"Three Flows Consistency Principle",
|
"Three Flows Consistency Principle",
|
||||||
"审批权限",
|
"审批权限",
|
||||||
"VAT Special Invoice",
|
"VAT Special Invoice",
|
||||||
"后勤服务部",
|
"后勤服务部",
|
||||||
"员工支出报销审批权限表",
|
"员工支出报销审批权限表",
|
||||||
"公司总裁",
|
"公司总裁",
|
||||||
"出差补贴",
|
"出差补贴",
|
||||||
"Basic Level Managers",
|
"Basic Level Managers",
|
||||||
"预付款项",
|
"预付款项",
|
||||||
"附表1:员工支出报销审批权限表",
|
"附表1:员工支出报销审批权限表",
|
||||||
"经办部门",
|
"经办部门",
|
||||||
"信息管理部",
|
"信息管理部",
|
||||||
"通信费",
|
"通信费",
|
||||||
"第十六条",
|
"第十六条",
|
||||||
"增值税发票",
|
"增值税发票",
|
||||||
"财务入账条件",
|
"财务入账条件",
|
||||||
"Hotel Accommodation Standards",
|
"Hotel Accommodation Standards",
|
||||||
"审批流转程序",
|
"审批流转程序",
|
||||||
"Self-Driving Travel Provisions",
|
"Self-Driving Travel Provisions",
|
||||||
"交通费",
|
"交通费",
|
||||||
"第九条支出报销审批",
|
"第九条支出报销审批",
|
||||||
"薪酬福利支出分配计划",
|
"薪酬福利支出分配计划",
|
||||||
"产品规划设计部",
|
"产品规划设计部",
|
||||||
"因公用车补贴",
|
"因公用车补贴",
|
||||||
"Committee Chairpersons",
|
"Committee Chairpersons",
|
||||||
"Business Division General Manager",
|
"Business Division General Manager",
|
||||||
"组织安排",
|
"组织安排",
|
||||||
"1 Yuan Per Person Per Kilometer Reimbursement",
|
"1 Yuan Per Person Per Kilometer Reimbursement",
|
||||||
"Separation of Approval and Processing Principle",
|
"Separation of Approval and Processing Principle",
|
||||||
"第五条计划财务部主要职责",
|
"第五条计划财务部主要职责",
|
||||||
"200000 Yuan Approval Limit",
|
"200000 Yuan Approval Limit",
|
||||||
"公司各部门",
|
"公司各部门",
|
||||||
"第十四条",
|
"第十四条",
|
||||||
"Other Areas",
|
"Other Areas",
|
||||||
"分支机构",
|
"分支机构",
|
||||||
"Departments And Units",
|
"Departments And Units",
|
||||||
"计划财务部",
|
"计划财务部",
|
||||||
"Other Employees",
|
"Other Employees",
|
||||||
"第二十三条",
|
"第二十三条",
|
||||||
"公司团建管理办法",
|
"公司团建管理办法",
|
||||||
"火车硬席",
|
"火车硬席",
|
||||||
"税控系统明细清单",
|
"税控系统明细清单",
|
||||||
"Trade Union Fund",
|
"Trade Union Fund",
|
||||||
"报销标准变化情况",
|
"报销标准变化情况",
|
||||||
"薪酬福利支出",
|
"薪酬福利支出",
|
||||||
"Hong Kong, Macau, And Taiwan Region",
|
"Hong Kong, Macau, And Taiwan Region",
|
||||||
"对外捐赠支出",
|
"对外捐赠支出",
|
||||||
"Multi-Level Approval Rule",
|
"Multi-Level Approval Rule",
|
||||||
"Three Working Days Deadline",
|
"Three Working Days Deadline",
|
||||||
"Employee Remuneration",
|
"Employee Remuneration",
|
||||||
"销售退款",
|
"销售退款",
|
||||||
"股权投资、兼并收购",
|
"股权投资、兼并收购",
|
||||||
"控股子公司",
|
"控股子公司",
|
||||||
"取消报销规定",
|
"取消报销规定",
|
||||||
"Procurement Management Regulations",
|
"Procurement Management Regulations",
|
||||||
"Middle Managers",
|
"Middle Managers",
|
||||||
"差旅费",
|
"差旅费",
|
||||||
"批办分离",
|
"批办分离",
|
||||||
"住宿费",
|
"住宿费",
|
||||||
"Travel Allowance Standards",
|
"Travel Allowance Standards",
|
||||||
"第二十三条本办法的归口与实施",
|
"第二十三条本办法的归口与实施",
|
||||||
"Senior Vice President",
|
"Senior Vice President",
|
||||||
"供应商",
|
"供应商",
|
||||||
"人事归口管理部门",
|
"人事归口管理部门",
|
||||||
"Management Personnel At All Levels",
|
"Management Personnel At All Levels",
|
||||||
"效益优先",
|
"效益优先",
|
||||||
"Operating Department Individual",
|
"Operating Department Individual",
|
||||||
"Remote Work Housing Rental Expenses",
|
"Remote Work Housing Rental Expenses",
|
||||||
"取消报销规定内容",
|
"取消报销规定内容",
|
||||||
"Company",
|
"Company",
|
||||||
"修订说明",
|
"修订说明",
|
||||||
"国网数科公司",
|
"国网数科公司",
|
||||||
"Vice President",
|
"Vice President",
|
||||||
"分级授权",
|
"分级授权",
|
||||||
"Expenditure Reimbursement Application",
|
"Expenditure Reimbursement Application",
|
||||||
"第二十四条附件",
|
"第二十四条附件",
|
||||||
"第二十二条",
|
"第二十二条",
|
||||||
"出租车",
|
"出租车",
|
||||||
"Night High-Speed Rail Provision",
|
"Night High-Speed Rail Provision",
|
||||||
"各级管理人员",
|
"各级管理人员",
|
||||||
"受益原则",
|
"受益原则",
|
||||||
"公司员工因公通讯费用实施细则",
|
"公司员工因公通讯费用实施细则",
|
||||||
"公司支出管理办法(2024)",
|
"公司支出管理办法(2024)",
|
||||||
"出差补贴标准",
|
"出差补贴标准",
|
||||||
"Bid Security Deposit Approval Limits Table",
|
"Bid Security Deposit Approval Limits Table",
|
||||||
"第二条范围",
|
"第二条范围",
|
||||||
"Company Property Rental Management",
|
"Company Property Rental Management",
|
||||||
"调动工作",
|
"调动工作",
|
||||||
"远光软件股份有限公司",
|
"远光软件股份有限公司",
|
||||||
"市内交通费",
|
"市内交通费",
|
||||||
"交通工具等级标准",
|
"交通工具等级标准",
|
||||||
"Operator",
|
"Operator",
|
||||||
"第八条支出报销申请",
|
"第八条支出报销申请",
|
||||||
"Directly-Controlled Municipalities And Special Administrative Regions",
|
"Directly-Controlled Municipalities And Special Administrative Regions",
|
||||||
"出差规定",
|
"出差规定",
|
||||||
"业务招待费",
|
"业务招待费",
|
||||||
"Senior Managers",
|
"Senior Managers",
|
||||||
"逐级审批规则",
|
"逐级审批规则",
|
||||||
"Company Business Travel System",
|
"Company Business Travel System",
|
||||||
"广告宣传费",
|
"广告宣传费",
|
||||||
"Transportation Cost Reimbursement",
|
"Transportation Cost Reimbursement",
|
||||||
"财务",
|
"财务",
|
||||||
"第一章总则",
|
"第一章总则",
|
||||||
"材料采购",
|
"材料采购",
|
||||||
"人力资源服务部",
|
"人力资源服务部",
|
||||||
"证券与法律事务部",
|
"证券与法律事务部",
|
||||||
"Transportation Level Standards",
|
"Transportation Level Standards",
|
||||||
"归口管理部门",
|
"归口管理部门",
|
||||||
"商旅客服",
|
"商旅客服",
|
||||||
"第四章重点支出管理规定",
|
"第四章重点支出管理规定",
|
||||||
"出差审批程序",
|
"出差审批程序",
|
||||||
"Business Trip Approval",
|
"Business Trip Approval",
|
||||||
"西藏",
|
"西藏",
|
||||||
"附表2:岗位支出报销审批权限表",
|
"附表2:岗位支出报销审批权限表",
|
||||||
"第十八条",
|
"第十八条",
|
||||||
"第二十四条",
|
"第二十四条",
|
||||||
"Company Hotel Accommodation Limit Standards",
|
"Company Hotel Accommodation Limit Standards",
|
||||||
"办法",
|
"办法",
|
||||||
"DAP研发中心",
|
"DAP研发中心",
|
||||||
"新增规定内容",
|
"新增规定内容",
|
||||||
"基本补助",
|
"基本补助",
|
||||||
"Travel Allowance",
|
"Travel Allowance",
|
||||||
"异地挂职锻炼补贴标准",
|
"异地挂职锻炼补贴标准",
|
||||||
"部门负责人",
|
"部门负责人",
|
||||||
"Provincial Capitals",
|
"Provincial Capitals",
|
||||||
"特区",
|
"特区",
|
||||||
"Transportation Tickets",
|
"Transportation Tickets",
|
||||||
"第三章支出报销申请与审批",
|
"第三章支出报销申请与审批",
|
||||||
"品牌及市场运营中心",
|
"品牌及市场运营中心",
|
||||||
"分包外包(外部单位)",
|
"分包外包(外部单位)",
|
||||||
"探亲路费",
|
"探亲路费",
|
||||||
"President",
|
"President",
|
||||||
"凭据报销",
|
"凭据报销",
|
||||||
"基本出差补贴",
|
"基本出差补贴",
|
||||||
"Taxi Usage Regulations",
|
"Taxi Usage Regulations",
|
||||||
"Government Fees",
|
"Government Fees",
|
||||||
"Commercial Travel System",
|
"Commercial Travel System",
|
||||||
"远光制度〔2024〕14号",
|
"远光制度〔2024〕14号",
|
||||||
"审批权限变化情况",
|
"审批权限变化情况",
|
||||||
"基建工程",
|
"基建工程",
|
||||||
"支出报销申请与审批",
|
"支出报销申请与审批",
|
||||||
"中国外汇交易中心参考汇率",
|
"中国外汇交易中心参考汇率",
|
||||||
"Department Manager",
|
"Department Manager",
|
||||||
"支出报销审批",
|
"支出报销审批",
|
||||||
"预算调整决策程序",
|
"预算调整决策程序",
|
||||||
"公司1号文",
|
"公司1号文",
|
||||||
"External Conference Accommodation",
|
"External Conference Accommodation",
|
||||||
"厉行节约",
|
"厉行节约",
|
||||||
"Commercial Insurance",
|
"Commercial Insurance",
|
||||||
"公司",
|
"公司",
|
||||||
"第三条管理原则",
|
"第三条管理原则",
|
||||||
"捐赠申请",
|
"捐赠申请",
|
||||||
"分类控制",
|
"分类控制",
|
||||||
"业务宣传费",
|
"业务宣传费",
|
||||||
"产业投资部",
|
"产业投资部",
|
||||||
"公司员工探亲管理办法",
|
"公司员工探亲管理办法",
|
||||||
"Subsequent Approver",
|
"Subsequent Approver",
|
||||||
"100000 Yuan Approval Limit",
|
"100000 Yuan Approval Limit",
|
||||||
"Tax Authority Recognized Invoice",
|
"Tax Authority Recognized Invoice",
|
||||||
"国家电网公司",
|
"国家电网公司",
|
||||||
"业务佐证材料",
|
"业务佐证材料",
|
||||||
"第六条经办部门(个人)主要职责",
|
"第六条经办部门(个人)主要职责",
|
||||||
"结算起点",
|
"结算起点",
|
||||||
"第十条支出成本中心归属",
|
"第十条支出成本中心归属",
|
||||||
"母公司"
|
"母公司"
|
||||||
],
|
],
|
||||||
"count": 258,
|
"count": 258,
|
||||||
"create_time": 1779012093,
|
"create_time": 1779012093,
|
||||||
"update_time": 1779012093,
|
"update_time": 1779012093,
|
||||||
"_id": "2c1cb358f08d44ceb0e4d287133206ec"
|
"_id": "2c1cb358f08d44ceb0e4d287133206ec"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,166 +1,166 @@
|
|||||||
{
|
{
|
||||||
"2c1cb358f08d44ceb0e4d287133206ec": {
|
"2c1cb358f08d44ceb0e4d287133206ec": {
|
||||||
"relation_pairs": [
|
"relation_pairs": [
|
||||||
[
|
[
|
||||||
"Departments And Units",
|
"Departments And Units",
|
||||||
"Taxi Usage Regulations"
|
"Taxi Usage Regulations"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"取消报销规定内容",
|
"取消报销规定内容",
|
||||||
"报销标准变化情况"
|
"报销标准变化情况"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"业务招待费",
|
"业务招待费",
|
||||||
"第十四条"
|
"第十四条"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"控股子公司",
|
"控股子公司",
|
||||||
"计划财务部"
|
"计划财务部"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"公司支出管理办法",
|
"公司支出管理办法",
|
||||||
"工会委员会"
|
"工会委员会"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"第一章总则",
|
"第一章总则",
|
||||||
"第三条管理原则"
|
"第三条管理原则"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"广告宣传费",
|
"广告宣传费",
|
||||||
"第十六条"
|
"第十六条"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Tax Control System Details",
|
"Tax Control System Details",
|
||||||
"VAT Special Invoice"
|
"VAT Special Invoice"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Expenditure Reimbursement Application",
|
"Expenditure Reimbursement Application",
|
||||||
"Tax Authority Recognized Invoice"
|
"Tax Authority Recognized Invoice"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"远光制度〔2024〕14号",
|
"远光制度〔2024〕14号",
|
||||||
"远光软件股份有限公司"
|
"远光软件股份有限公司"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Financial Review",
|
"Financial Review",
|
||||||
"Operator"
|
"Operator"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Operating Department Individual",
|
"Operating Department Individual",
|
||||||
"Procurement Management Regulations"
|
"Procurement Management Regulations"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"会议费",
|
"会议费",
|
||||||
"第十五条"
|
"第十五条"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Company",
|
"Company",
|
||||||
"Management Personnel At All Levels"
|
"Management Personnel At All Levels"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"公司",
|
"公司",
|
||||||
"第十七条"
|
"第十七条"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"公司",
|
"公司",
|
||||||
"第十八条"
|
"第十八条"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Operator",
|
"Operator",
|
||||||
"Three Working Days Deadline"
|
"Three Working Days Deadline"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"第十一条备用金借款",
|
"第十一条备用金借款",
|
||||||
"第四章重点支出管理规定"
|
"第四章重点支出管理规定"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Expenditure Reimbursement Application",
|
"Expenditure Reimbursement Application",
|
||||||
"Operator"
|
"Operator"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"业务招待费",
|
"业务招待费",
|
||||||
"差旅费"
|
"差旅费"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"公司",
|
"公司",
|
||||||
"第二十一条"
|
"第二十一条"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"公司支出管理办法(2024)",
|
"公司支出管理办法(2024)",
|
||||||
"远光软件股份有限公司"
|
"远光软件股份有限公司"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"第四条归口管理部门主要职责",
|
"第四条归口管理部门主要职责",
|
||||||
"计划财务部"
|
"计划财务部"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"会议费",
|
"会议费",
|
||||||
"差旅费"
|
"差旅费"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Company",
|
"Company",
|
||||||
"Operating Department Individual"
|
"Operating Department Individual"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"商旅系统",
|
"商旅系统",
|
||||||
"差旅费"
|
"差旅费"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"会议费",
|
"会议费",
|
||||||
"公司总裁"
|
"公司总裁"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"计划财务部",
|
"计划财务部",
|
||||||
"远光软件股份有限公司"
|
"远光软件股份有限公司"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"公司",
|
"公司",
|
||||||
"第十九条"
|
"第十九条"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"公司",
|
"公司",
|
||||||
"第二十条"
|
"第二十条"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Company",
|
"Company",
|
||||||
"Planning and Finance Department"
|
"Planning and Finance Department"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"公司支出管理办法",
|
"公司支出管理办法",
|
||||||
"营销中心"
|
"营销中心"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Business Original Documents",
|
"Business Original Documents",
|
||||||
"Operator"
|
"Operator"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"公司支出管理办法",
|
"公司支出管理办法",
|
||||||
"办公室(党委办公室)"
|
"办公室(党委办公室)"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Departments And Units",
|
"Departments And Units",
|
||||||
"Night High-Speed Rail Provision"
|
"Night High-Speed Rail Provision"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"Centralized Management department",
|
"Centralized Management department",
|
||||||
"Company"
|
"Company"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"组织人事部",
|
"组织人事部",
|
||||||
"调动工作"
|
"调动工作"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"报销标准变化情况",
|
"报销标准变化情况",
|
||||||
"远光软件股份有限公司"
|
"远光软件股份有限公司"
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"第一章总则",
|
"第一章总则",
|
||||||
"远光软件股份有限公司"
|
"远光软件股份有限公司"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"count": 39,
|
"count": 39,
|
||||||
"create_time": 1779012093,
|
"create_time": 1779012093,
|
||||||
"update_time": 1779012093,
|
"update_time": 1779012093,
|
||||||
"_id": "2c1cb358f08d44ceb0e4d287133206ec"
|
"_id": "2c1cb358f08d44ceb0e4d287133206ec"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,353 +1,353 @@
|
|||||||
{
|
{
|
||||||
"第一章总则<SEP>远光软件股份有限公司": {
|
"第一章总则<SEP>远光软件股份有限公司": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-aa5435156b829944c173fa1d2d7a93d4"
|
"chunk-aa5435156b829944c173fa1d2d7a93d4"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012088,
|
"create_time": 1779012088,
|
||||||
"update_time": 1779012088,
|
"update_time": 1779012088,
|
||||||
"_id": "第一章总则<SEP>远光软件股份有限公司"
|
"_id": "第一章总则<SEP>远光软件股份有限公司"
|
||||||
},
|
},
|
||||||
"第十一条备用金借款<SEP>第四章重点支出管理规定": {
|
"第十一条备用金借款<SEP>第四章重点支出管理规定": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-aa5435156b829944c173fa1d2d7a93d4"
|
"chunk-aa5435156b829944c173fa1d2d7a93d4"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012088,
|
"create_time": 1779012088,
|
||||||
"update_time": 1779012088,
|
"update_time": 1779012088,
|
||||||
"_id": "第十一条备用金借款<SEP>第四章重点支出管理规定"
|
"_id": "第十一条备用金借款<SEP>第四章重点支出管理规定"
|
||||||
},
|
},
|
||||||
"公司支出管理办法<SEP>办公室(党委办公室)": {
|
"公司支出管理办法<SEP>办公室(党委办公室)": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-afc57a0e9548d1f484da6df6c182676b"
|
"chunk-afc57a0e9548d1f484da6df6c182676b"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012088,
|
"create_time": 1779012088,
|
||||||
"update_time": 1779012088,
|
"update_time": 1779012088,
|
||||||
"_id": "公司支出管理办法<SEP>办公室(党委办公室)"
|
"_id": "公司支出管理办法<SEP>办公室(党委办公室)"
|
||||||
},
|
},
|
||||||
"计划财务部<SEP>远光软件股份有限公司": {
|
"计划财务部<SEP>远光软件股份有限公司": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-aa5435156b829944c173fa1d2d7a93d4"
|
"chunk-aa5435156b829944c173fa1d2d7a93d4"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012076,
|
"create_time": 1779012076,
|
||||||
"update_time": 1779012076,
|
"update_time": 1779012076,
|
||||||
"_id": "计划财务部<SEP>远光软件股份有限公司"
|
"_id": "计划财务部<SEP>远光软件股份有限公司"
|
||||||
},
|
},
|
||||||
"第一章总则<SEP>第三条管理原则": {
|
"第一章总则<SEP>第三条管理原则": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-aa5435156b829944c173fa1d2d7a93d4"
|
"chunk-aa5435156b829944c173fa1d2d7a93d4"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012076,
|
"create_time": 1779012076,
|
||||||
"update_time": 1779012076,
|
"update_time": 1779012076,
|
||||||
"_id": "第一章总则<SEP>第三条管理原则"
|
"_id": "第一章总则<SEP>第三条管理原则"
|
||||||
},
|
},
|
||||||
"Company<SEP>Management Personnel At All Levels": {
|
"Company<SEP>Management Personnel At All Levels": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-74c01decac4a10cd40a491786743b0ee"
|
"chunk-74c01decac4a10cd40a491786743b0ee"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012076,
|
"create_time": 1779012076,
|
||||||
"update_time": 1779012076,
|
"update_time": 1779012076,
|
||||||
"_id": "Company<SEP>Management Personnel At All Levels"
|
"_id": "Company<SEP>Management Personnel At All Levels"
|
||||||
},
|
},
|
||||||
"Centralized Management department<SEP>Company": {
|
"Centralized Management department<SEP>Company": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-74c01decac4a10cd40a491786743b0ee"
|
"chunk-74c01decac4a10cd40a491786743b0ee"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012077,
|
"create_time": 1779012077,
|
||||||
"update_time": 1779012077,
|
"update_time": 1779012077,
|
||||||
"_id": "Centralized Management department<SEP>Company"
|
"_id": "Centralized Management department<SEP>Company"
|
||||||
},
|
},
|
||||||
"Company<SEP>Planning and Finance Department": {
|
"Company<SEP>Planning and Finance Department": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-74c01decac4a10cd40a491786743b0ee"
|
"chunk-74c01decac4a10cd40a491786743b0ee"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012077,
|
"create_time": 1779012077,
|
||||||
"update_time": 1779012077,
|
"update_time": 1779012077,
|
||||||
"_id": "Company<SEP>Planning and Finance Department"
|
"_id": "Company<SEP>Planning and Finance Department"
|
||||||
},
|
},
|
||||||
"Company<SEP>Operating Department Individual": {
|
"Company<SEP>Operating Department Individual": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-74c01decac4a10cd40a491786743b0ee"
|
"chunk-74c01decac4a10cd40a491786743b0ee"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012078,
|
"create_time": 1779012078,
|
||||||
"update_time": 1779012078,
|
"update_time": 1779012078,
|
||||||
"_id": "Company<SEP>Operating Department Individual"
|
"_id": "Company<SEP>Operating Department Individual"
|
||||||
},
|
},
|
||||||
"公司支出管理办法<SEP>工会委员会": {
|
"公司支出管理办法<SEP>工会委员会": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-afc57a0e9548d1f484da6df6c182676b"
|
"chunk-afc57a0e9548d1f484da6df6c182676b"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012079,
|
"create_time": 1779012079,
|
||||||
"update_time": 1779012079,
|
"update_time": 1779012079,
|
||||||
"_id": "公司支出管理办法<SEP>工会委员会"
|
"_id": "公司支出管理办法<SEP>工会委员会"
|
||||||
},
|
},
|
||||||
"Expenditure Reimbursement Application<SEP>Operator": {
|
"Expenditure Reimbursement Application<SEP>Operator": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-74c01decac4a10cd40a491786743b0ee"
|
"chunk-74c01decac4a10cd40a491786743b0ee"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012079,
|
"create_time": 1779012079,
|
||||||
"update_time": 1779012079,
|
"update_time": 1779012079,
|
||||||
"_id": "Expenditure Reimbursement Application<SEP>Operator"
|
"_id": "Expenditure Reimbursement Application<SEP>Operator"
|
||||||
},
|
},
|
||||||
"公司支出管理办法<SEP>营销中心": {
|
"公司支出管理办法<SEP>营销中心": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-afc57a0e9548d1f484da6df6c182676b"
|
"chunk-afc57a0e9548d1f484da6df6c182676b"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012079,
|
"create_time": 1779012079,
|
||||||
"update_time": 1779012079,
|
"update_time": 1779012079,
|
||||||
"_id": "公司支出管理办法<SEP>营销中心"
|
"_id": "公司支出管理办法<SEP>营销中心"
|
||||||
},
|
},
|
||||||
"第四条归口管理部门主要职责<SEP>计划财务部": {
|
"第四条归口管理部门主要职责<SEP>计划财务部": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-aa5435156b829944c173fa1d2d7a93d4"
|
"chunk-aa5435156b829944c173fa1d2d7a93d4"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012079,
|
"create_time": 1779012079,
|
||||||
"update_time": 1779012079,
|
"update_time": 1779012079,
|
||||||
"_id": "第四条归口管理部门主要职责<SEP>计划财务部"
|
"_id": "第四条归口管理部门主要职责<SEP>计划财务部"
|
||||||
},
|
},
|
||||||
"Tax Control System Details<SEP>VAT Special Invoice": {
|
"Tax Control System Details<SEP>VAT Special Invoice": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-74c01decac4a10cd40a491786743b0ee"
|
"chunk-74c01decac4a10cd40a491786743b0ee"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012079,
|
"create_time": 1779012079,
|
||||||
"update_time": 1779012079,
|
"update_time": 1779012079,
|
||||||
"_id": "Tax Control System Details<SEP>VAT Special Invoice"
|
"_id": "Tax Control System Details<SEP>VAT Special Invoice"
|
||||||
},
|
},
|
||||||
"Operating Department Individual<SEP>Procurement Management Regulations": {
|
"Operating Department Individual<SEP>Procurement Management Regulations": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-74c01decac4a10cd40a491786743b0ee"
|
"chunk-74c01decac4a10cd40a491786743b0ee"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012081,
|
"create_time": 1779012081,
|
||||||
"update_time": 1779012081,
|
"update_time": 1779012081,
|
||||||
"_id": "Operating Department Individual<SEP>Procurement Management Regulations"
|
"_id": "Operating Department Individual<SEP>Procurement Management Regulations"
|
||||||
},
|
},
|
||||||
"Business Original Documents<SEP>Operator": {
|
"Business Original Documents<SEP>Operator": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-74c01decac4a10cd40a491786743b0ee"
|
"chunk-74c01decac4a10cd40a491786743b0ee"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012094,
|
"create_time": 1779012094,
|
||||||
"update_time": 1779012094,
|
"update_time": 1779012094,
|
||||||
"_id": "Business Original Documents<SEP>Operator"
|
"_id": "Business Original Documents<SEP>Operator"
|
||||||
},
|
},
|
||||||
"Expenditure Reimbursement Application<SEP>Tax Authority Recognized Invoice": {
|
"Expenditure Reimbursement Application<SEP>Tax Authority Recognized Invoice": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-74c01decac4a10cd40a491786743b0ee"
|
"chunk-74c01decac4a10cd40a491786743b0ee"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012094,
|
"create_time": 1779012094,
|
||||||
"update_time": 1779012094,
|
"update_time": 1779012094,
|
||||||
"_id": "Expenditure Reimbursement Application<SEP>Tax Authority Recognized Invoice"
|
"_id": "Expenditure Reimbursement Application<SEP>Tax Authority Recognized Invoice"
|
||||||
},
|
},
|
||||||
"公司<SEP>第十七条": {
|
"公司<SEP>第十七条": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-e9438f69c9e221d9f0f00a05ad84eac6"
|
"chunk-e9438f69c9e221d9f0f00a05ad84eac6"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012094,
|
"create_time": 1779012094,
|
||||||
"update_time": 1779012094,
|
"update_time": 1779012094,
|
||||||
"_id": "公司<SEP>第十七条"
|
"_id": "公司<SEP>第十七条"
|
||||||
},
|
},
|
||||||
"Operator<SEP>Three Working Days Deadline": {
|
"Operator<SEP>Three Working Days Deadline": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-74c01decac4a10cd40a491786743b0ee"
|
"chunk-74c01decac4a10cd40a491786743b0ee"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012083,
|
"create_time": 1779012083,
|
||||||
"update_time": 1779012083,
|
"update_time": 1779012083,
|
||||||
"_id": "Operator<SEP>Three Working Days Deadline"
|
"_id": "Operator<SEP>Three Working Days Deadline"
|
||||||
},
|
},
|
||||||
"Departments And Units<SEP>Night High-Speed Rail Provision": {
|
"Departments And Units<SEP>Night High-Speed Rail Provision": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-613d6dfd4c5e9c807229a3147f96b584"
|
"chunk-613d6dfd4c5e9c807229a3147f96b584"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012084,
|
"create_time": 1779012084,
|
||||||
"update_time": 1779012084,
|
"update_time": 1779012084,
|
||||||
"_id": "Departments And Units<SEP>Night High-Speed Rail Provision"
|
"_id": "Departments And Units<SEP>Night High-Speed Rail Provision"
|
||||||
},
|
},
|
||||||
"公司<SEP>第十八条": {
|
"公司<SEP>第十八条": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-e9438f69c9e221d9f0f00a05ad84eac6"
|
"chunk-e9438f69c9e221d9f0f00a05ad84eac6"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012084,
|
"create_time": 1779012084,
|
||||||
"update_time": 1779012084,
|
"update_time": 1779012084,
|
||||||
"_id": "公司<SEP>第十八条"
|
"_id": "公司<SEP>第十八条"
|
||||||
},
|
},
|
||||||
"公司<SEP>第十九条": {
|
"公司<SEP>第十九条": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-e9438f69c9e221d9f0f00a05ad84eac6"
|
"chunk-e9438f69c9e221d9f0f00a05ad84eac6"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012084,
|
"create_time": 1779012084,
|
||||||
"update_time": 1779012084,
|
"update_time": 1779012084,
|
||||||
"_id": "公司<SEP>第十九条"
|
"_id": "公司<SEP>第十九条"
|
||||||
},
|
},
|
||||||
"报销标准变化情况<SEP>远光软件股份有限公司": {
|
"报销标准变化情况<SEP>远光软件股份有限公司": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-18d968b78afe916b419c1b5973421ebe"
|
"chunk-18d968b78afe916b419c1b5973421ebe"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012084,
|
"create_time": 1779012084,
|
||||||
"update_time": 1779012084,
|
"update_time": 1779012084,
|
||||||
"_id": "报销标准变化情况<SEP>远光软件股份有限公司"
|
"_id": "报销标准变化情况<SEP>远光软件股份有限公司"
|
||||||
},
|
},
|
||||||
"取消报销规定内容<SEP>报销标准变化情况": {
|
"取消报销规定内容<SEP>报销标准变化情况": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-18d968b78afe916b419c1b5973421ebe"
|
"chunk-18d968b78afe916b419c1b5973421ebe"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012085,
|
"create_time": 1779012085,
|
||||||
"update_time": 1779012085,
|
"update_time": 1779012085,
|
||||||
"_id": "取消报销规定内容<SEP>报销标准变化情况"
|
"_id": "取消报销规定内容<SEP>报销标准变化情况"
|
||||||
},
|
},
|
||||||
"Financial Review<SEP>Operator": {
|
"Financial Review<SEP>Operator": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-74c01decac4a10cd40a491786743b0ee"
|
"chunk-74c01decac4a10cd40a491786743b0ee"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012085,
|
"create_time": 1779012085,
|
||||||
"update_time": 1779012085,
|
"update_time": 1779012085,
|
||||||
"_id": "Financial Review<SEP>Operator"
|
"_id": "Financial Review<SEP>Operator"
|
||||||
},
|
},
|
||||||
"公司支出管理办法(2024)<SEP>远光软件股份有限公司": {
|
"公司支出管理办法(2024)<SEP>远光软件股份有限公司": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-dd87aa5bc62cc9587ecb4c26d35a5263"
|
"chunk-dd87aa5bc62cc9587ecb4c26d35a5263"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012085,
|
"create_time": 1779012085,
|
||||||
"update_time": 1779012085,
|
"update_time": 1779012085,
|
||||||
"_id": "公司支出管理办法(2024)<SEP>远光软件股份有限公司"
|
"_id": "公司支出管理办法(2024)<SEP>远光软件股份有限公司"
|
||||||
},
|
},
|
||||||
"远光制度〔2024〕14号<SEP>远光软件股份有限公司": {
|
"远光制度〔2024〕14号<SEP>远光软件股份有限公司": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-dd87aa5bc62cc9587ecb4c26d35a5263"
|
"chunk-dd87aa5bc62cc9587ecb4c26d35a5263"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012086,
|
"create_time": 1779012086,
|
||||||
"update_time": 1779012086,
|
"update_time": 1779012086,
|
||||||
"_id": "远光制度〔2024〕14号<SEP>远光软件股份有限公司"
|
"_id": "远光制度〔2024〕14号<SEP>远光软件股份有限公司"
|
||||||
},
|
},
|
||||||
"Departments And Units<SEP>Taxi Usage Regulations": {
|
"Departments And Units<SEP>Taxi Usage Regulations": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-613d6dfd4c5e9c807229a3147f96b584"
|
"chunk-613d6dfd4c5e9c807229a3147f96b584"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012099,
|
"create_time": 1779012099,
|
||||||
"update_time": 1779012099,
|
"update_time": 1779012099,
|
||||||
"_id": "Departments And Units<SEP>Taxi Usage Regulations"
|
"_id": "Departments And Units<SEP>Taxi Usage Regulations"
|
||||||
},
|
},
|
||||||
"控股子公司<SEP>计划财务部": {
|
"控股子公司<SEP>计划财务部": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-dd87aa5bc62cc9587ecb4c26d35a5263"
|
"chunk-dd87aa5bc62cc9587ecb4c26d35a5263"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012099,
|
"create_time": 1779012099,
|
||||||
"update_time": 1779012099,
|
"update_time": 1779012099,
|
||||||
"_id": "控股子公司<SEP>计划财务部"
|
"_id": "控股子公司<SEP>计划财务部"
|
||||||
},
|
},
|
||||||
"公司<SEP>第二十条": {
|
"公司<SEP>第二十条": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-e9438f69c9e221d9f0f00a05ad84eac6"
|
"chunk-e9438f69c9e221d9f0f00a05ad84eac6"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012086,
|
"create_time": 1779012086,
|
||||||
"update_time": 1779012086,
|
"update_time": 1779012086,
|
||||||
"_id": "公司<SEP>第二十条"
|
"_id": "公司<SEP>第二十条"
|
||||||
},
|
},
|
||||||
"商旅系统<SEP>差旅费": {
|
"商旅系统<SEP>差旅费": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-d26b288ed4001dc5c504dce0eb841362"
|
"chunk-d26b288ed4001dc5c504dce0eb841362"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012086,
|
"create_time": 1779012086,
|
||||||
"update_time": 1779012086,
|
"update_time": 1779012086,
|
||||||
"_id": "商旅系统<SEP>差旅费"
|
"_id": "商旅系统<SEP>差旅费"
|
||||||
},
|
},
|
||||||
"业务招待费<SEP>差旅费": {
|
"业务招待费<SEP>差旅费": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-d26b288ed4001dc5c504dce0eb841362"
|
"chunk-d26b288ed4001dc5c504dce0eb841362"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012089,
|
"create_time": 1779012089,
|
||||||
"update_time": 1779012089,
|
"update_time": 1779012089,
|
||||||
"_id": "业务招待费<SEP>差旅费"
|
"_id": "业务招待费<SEP>差旅费"
|
||||||
},
|
},
|
||||||
"公司<SEP>第二十一条": {
|
"公司<SEP>第二十一条": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-e9438f69c9e221d9f0f00a05ad84eac6"
|
"chunk-e9438f69c9e221d9f0f00a05ad84eac6"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012089,
|
"create_time": 1779012089,
|
||||||
"update_time": 1779012089,
|
"update_time": 1779012089,
|
||||||
"_id": "公司<SEP>第二十一条"
|
"_id": "公司<SEP>第二十一条"
|
||||||
},
|
},
|
||||||
"广告宣传费<SEP>第十六条": {
|
"广告宣传费<SEP>第十六条": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-d26b288ed4001dc5c504dce0eb841362"
|
"chunk-d26b288ed4001dc5c504dce0eb841362"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012089,
|
"create_time": 1779012089,
|
||||||
"update_time": 1779012089,
|
"update_time": 1779012089,
|
||||||
"_id": "广告宣传费<SEP>第十六条"
|
"_id": "广告宣传费<SEP>第十六条"
|
||||||
},
|
},
|
||||||
"组织人事部<SEP>调动工作": {
|
"组织人事部<SEP>调动工作": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-d26b288ed4001dc5c504dce0eb841362"
|
"chunk-d26b288ed4001dc5c504dce0eb841362"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012090,
|
"create_time": 1779012090,
|
||||||
"update_time": 1779012090,
|
"update_time": 1779012090,
|
||||||
"_id": "组织人事部<SEP>调动工作"
|
"_id": "组织人事部<SEP>调动工作"
|
||||||
},
|
},
|
||||||
"会议费<SEP>差旅费": {
|
"会议费<SEP>差旅费": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-d26b288ed4001dc5c504dce0eb841362"
|
"chunk-d26b288ed4001dc5c504dce0eb841362"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012092,
|
"create_time": 1779012092,
|
||||||
"update_time": 1779012092,
|
"update_time": 1779012092,
|
||||||
"_id": "会议费<SEP>差旅费"
|
"_id": "会议费<SEP>差旅费"
|
||||||
},
|
},
|
||||||
"业务招待费<SEP>第十四条": {
|
"业务招待费<SEP>第十四条": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-d26b288ed4001dc5c504dce0eb841362"
|
"chunk-d26b288ed4001dc5c504dce0eb841362"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012092,
|
"create_time": 1779012092,
|
||||||
"update_time": 1779012092,
|
"update_time": 1779012092,
|
||||||
"_id": "业务招待费<SEP>第十四条"
|
"_id": "业务招待费<SEP>第十四条"
|
||||||
},
|
},
|
||||||
"会议费<SEP>第十五条": {
|
"会议费<SEP>第十五条": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-d26b288ed4001dc5c504dce0eb841362"
|
"chunk-d26b288ed4001dc5c504dce0eb841362"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012092,
|
"create_time": 1779012092,
|
||||||
"update_time": 1779012092,
|
"update_time": 1779012092,
|
||||||
"_id": "会议费<SEP>第十五条"
|
"_id": "会议费<SEP>第十五条"
|
||||||
},
|
},
|
||||||
"会议费<SEP>公司总裁": {
|
"会议费<SEP>公司总裁": {
|
||||||
"chunk_ids": [
|
"chunk_ids": [
|
||||||
"chunk-d26b288ed4001dc5c504dce0eb841362"
|
"chunk-d26b288ed4001dc5c504dce0eb841362"
|
||||||
],
|
],
|
||||||
"count": 1,
|
"count": 1,
|
||||||
"create_time": 1779012093,
|
"create_time": 1779012093,
|
||||||
"update_time": 1779012093,
|
"update_time": 1779012093,
|
||||||
"_id": "会议费<SEP>公司总裁"
|
"_id": "会议费<SEP>公司总裁"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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(
|
||||||
@@ -322,34 +322,33 @@ def test_spreadsheet_version_compare_returns_sheet_and_cell_changes() -> None:
|
|||||||
service.upload_rule_spreadsheet(
|
service.upload_rule_spreadsheet(
|
||||||
rule.id,
|
rule.id,
|
||||||
filename="公司差旅费报销规则.xlsx",
|
filename="公司差旅费报销规则.xlsx",
|
||||||
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
|
assert workbook.active["B2"].value == 500
|
||||||
workbook = load_workbook(snapshot_path, data_only=False)
|
|
||||||
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:
|
||||||
|
|||||||
@@ -1,98 +1,98 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import Session, sessionmaker
|
from sqlalchemy.orm import Session, sessionmaker
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
from app.api.deps import get_db
|
from app.api.deps import get_db
|
||||||
from app.core.agent_enums import AgentAssetStatus
|
from app.core.agent_enums import AgentAssetStatus
|
||||||
from app.db.base import Base
|
from app.db.base import Base
|
||||||
from app.main import create_app
|
from app.main import create_app
|
||||||
from app.services.agent_assets import AgentAssetService
|
from app.services.agent_assets import AgentAssetService
|
||||||
|
|
||||||
|
|
||||||
def build_client() -> tuple[TestClient, sessionmaker[Session]]:
|
def build_client() -> tuple[TestClient, sessionmaker[Session]]:
|
||||||
engine = create_engine(
|
engine = create_engine(
|
||||||
"sqlite+pysqlite:///:memory:",
|
"sqlite+pysqlite:///:memory:",
|
||||||
connect_args={"check_same_thread": False},
|
connect_args={"check_same_thread": False},
|
||||||
poolclass=StaticPool,
|
poolclass=StaticPool,
|
||||||
)
|
)
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
session_factory = sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
|
|
||||||
def override_db() -> Generator[Session, None, None]:
|
def override_db() -> Generator[Session, None, None]:
|
||||||
db = session_factory()
|
db = session_factory()
|
||||||
try:
|
try:
|
||||||
yield db
|
yield db
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
app.dependency_overrides[get_db] = override_db
|
app.dependency_overrides[get_db] = override_db
|
||||||
return TestClient(app), session_factory
|
return TestClient(app), session_factory
|
||||||
|
|
||||||
|
|
||||||
def test_list_agent_assets_endpoint_returns_seeded_items() -> None:
|
def test_list_agent_assets_endpoint_returns_seeded_items() -> None:
|
||||||
client, _ = build_client()
|
client, _ = build_client()
|
||||||
|
|
||||||
response = client.get("/api/v1/agent-assets", params={"asset_type": "rule"})
|
response = client.get("/api/v1/agent-assets", params={"asset_type": "rule"})
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
payload = response.json()
|
payload = response.json()
|
||||||
assert payload
|
assert payload
|
||||||
assert all(item["asset_type"] == "rule" for item in payload)
|
assert all(item["asset_type"] == "rule" for item in payload)
|
||||||
assert any(item["code"] == "rule.expense.travel_risk_control_standard" for item in payload)
|
assert any(item["code"] == "rule.expense.travel_risk_control_standard" for item in payload)
|
||||||
|
|
||||||
|
|
||||||
def test_get_agent_asset_detail_endpoint_returns_version_history() -> None:
|
def test_get_agent_asset_detail_endpoint_returns_version_history() -> None:
|
||||||
client, _ = build_client()
|
client, _ = build_client()
|
||||||
|
|
||||||
list_response = client.get("/api/v1/agent-assets", params={"asset_type": "rule"})
|
list_response = client.get("/api/v1/agent-assets", params={"asset_type": "rule"})
|
||||||
asset_id = next(
|
asset_id = next(
|
||||||
item["id"]
|
item["id"]
|
||||||
for item in list_response.json()
|
for item in list_response.json()
|
||||||
if item["code"] == "rule.expense.travel_risk_control_standard"
|
if item["code"] == "rule.expense.travel_risk_control_standard"
|
||||||
)
|
)
|
||||||
|
|
||||||
response = client.get(f"/api/v1/agent-assets/{asset_id}")
|
response = client.get(f"/api/v1/agent-assets/{asset_id}")
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
payload = response.json()
|
payload = response.json()
|
||||||
assert payload["recent_versions"]
|
assert payload["recent_versions"]
|
||||||
assert payload["current_version_content_type"] == "markdown"
|
assert payload["current_version_content_type"] == "markdown"
|
||||||
assert payload["current_version"] == "v1.1.0"
|
assert payload["current_version"] == "v1.1.0"
|
||||||
assert "行程闭环" in payload["current_version_content"]
|
assert "行程闭环" in payload["current_version_content"]
|
||||||
|
|
||||||
|
|
||||||
def test_activate_pending_rule_endpoint_is_blocked() -> None:
|
def test_activate_pending_rule_endpoint_is_blocked() -> None:
|
||||||
client, session_factory = build_client()
|
client, session_factory = build_client()
|
||||||
|
|
||||||
with session_factory() as db:
|
with session_factory() as db:
|
||||||
pending_rule = next(
|
pending_rule = next(
|
||||||
item
|
item
|
||||||
for item in AgentAssetService(db).list_assets(asset_type="rule")
|
for item in AgentAssetService(db).list_assets(asset_type="rule")
|
||||||
if item.status == AgentAssetStatus.REVIEW.value
|
if item.status == AgentAssetStatus.REVIEW.value
|
||||||
)
|
)
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
f"/api/v1/agent-assets/{pending_rule.id}/activate",
|
f"/api/v1/agent-assets/{pending_rule.id}/activate",
|
||||||
headers={"x-actor": "pytest"},
|
headers={"x-actor": "pytest"},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert "审核" in response.json()["detail"]
|
assert "审核" in response.json()["detail"]
|
||||||
|
|
||||||
|
|
||||||
def test_list_audit_logs_endpoint_returns_seeded_logs() -> None:
|
def test_list_audit_logs_endpoint_returns_seeded_logs() -> None:
|
||||||
client, _ = build_client()
|
client, _ = build_client()
|
||||||
|
|
||||||
response = client.get("/api/v1/audit-logs")
|
response = client.get("/api/v1/audit-logs")
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
payload = response.json()
|
payload = response.json()
|
||||||
assert payload
|
assert payload
|
||||||
assert any(item["action"] == "review_rule" for item in payload)
|
assert any(item["action"] == "review_rule" for item in payload)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
BIN
web/UI/流程输入.png
Normal file
BIN
web/UI/流程输入.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,261 +1,261 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition name="shared-confirm">
|
<Transition name="shared-confirm">
|
||||||
<div
|
<div
|
||||||
v-if="open"
|
v-if="open"
|
||||||
class="shared-confirm-mask"
|
class="shared-confirm-mask"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
@click.self="handleMaskClose"
|
@click.self="handleMaskClose"
|
||||||
>
|
>
|
||||||
<section
|
<section
|
||||||
class="shared-confirm-card"
|
class="shared-confirm-card"
|
||||||
role="alertdialog"
|
role="alertdialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
:aria-labelledby="titleId"
|
:aria-labelledby="titleId"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<span v-if="badge" class="shared-confirm-badge" :class="badgeTone">
|
<span v-if="badge" class="shared-confirm-badge" :class="badgeTone">
|
||||||
{{ badge }}
|
{{ badge }}
|
||||||
</span>
|
</span>
|
||||||
<h4 :id="titleId">{{ title }}</h4>
|
<h4 :id="titleId">{{ title }}</h4>
|
||||||
<p v-if="description">{{ description }}</p>
|
<p v-if="description">{{ description }}</p>
|
||||||
|
|
||||||
<div v-if="$slots.default" class="shared-confirm-body">
|
<div v-if="$slots.default" class="shared-confirm-body">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="shared-confirm-actions">
|
<div class="shared-confirm-actions">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="shared-confirm-btn cancel"
|
class="shared-confirm-btn cancel"
|
||||||
:disabled="busy"
|
:disabled="busy"
|
||||||
@click="handleCancel"
|
@click="handleCancel"
|
||||||
>
|
>
|
||||||
{{ cancelText }}
|
{{ cancelText }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="shared-confirm-btn confirm"
|
class="shared-confirm-btn confirm"
|
||||||
:class="confirmTone"
|
:class="confirmTone"
|
||||||
:disabled="busy"
|
:disabled="busy"
|
||||||
@click="$emit('confirm')"
|
@click="$emit('confirm')"
|
||||||
>
|
>
|
||||||
<i v-if="confirmIcon" :class="busy ? 'mdi mdi-loading mdi-spin' : confirmIcon"></i>
|
<i v-if="confirmIcon" :class="busy ? 'mdi mdi-loading mdi-spin' : confirmIcon"></i>
|
||||||
<span>{{ busy ? busyText : confirmText }}</span>
|
<span>{{ busy ? busyText : confirmText }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, getCurrentInstance } from 'vue'
|
import { computed, getCurrentInstance } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
open: { type: Boolean, default: false },
|
open: { type: Boolean, default: false },
|
||||||
badge: { type: String, default: '' },
|
badge: { type: String, default: '' },
|
||||||
badgeTone: { type: String, default: 'info' },
|
badgeTone: { type: String, default: 'info' },
|
||||||
title: { type: String, required: true },
|
title: { type: String, required: true },
|
||||||
description: { type: String, default: '' },
|
description: { type: String, default: '' },
|
||||||
cancelText: { type: String, default: '取消' },
|
cancelText: { type: String, default: '取消' },
|
||||||
confirmText: { type: String, default: '确认' },
|
confirmText: { type: String, default: '确认' },
|
||||||
busyText: { type: String, default: '处理中...' },
|
busyText: { type: String, default: '处理中...' },
|
||||||
confirmTone: { type: String, default: 'primary' },
|
confirmTone: { type: String, default: 'primary' },
|
||||||
confirmIcon: { type: String, default: '' },
|
confirmIcon: { type: String, default: '' },
|
||||||
busy: { type: Boolean, default: false },
|
busy: { type: Boolean, default: false },
|
||||||
closeOnMask: { type: Boolean, default: true }
|
closeOnMask: { type: Boolean, default: true }
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'cancel', 'confirm'])
|
const emit = defineEmits(['close', 'cancel', 'confirm'])
|
||||||
const instance = getCurrentInstance()
|
const instance = getCurrentInstance()
|
||||||
|
|
||||||
const titleId = computed(() => `shared-confirm-title-${instance?.uid || 'dialog'}`)
|
const titleId = computed(() => `shared-confirm-title-${instance?.uid || 'dialog'}`)
|
||||||
|
|
||||||
function handleMaskClose() {
|
function handleMaskClose() {
|
||||||
if (!props.closeOnMask || props.busy) {
|
if (!props.closeOnMask || props.busy) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
if (props.busy) {
|
if (props.busy) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('cancel')
|
emit('cancel')
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.shared-confirm-mask {
|
.shared-confirm-mask {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 10020;
|
z-index: 10020;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: rgba(15, 23, 42, 0.32);
|
background: rgba(15, 23, 42, 0.32);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
-webkit-backdrop-filter: blur(10px);
|
-webkit-backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-card {
|
.shared-confirm-card {
|
||||||
width: min(480px, 100%);
|
width: min(480px, 100%);
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border: 1px solid rgba(16, 185, 129, 0.14);
|
border: 1px solid rgba(16, 185, 129, 0.14);
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top left, rgba(16, 185, 129, 0.12), transparent 36%),
|
radial-gradient(circle at top left, rgba(16, 185, 129, 0.12), transparent 36%),
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(247, 250, 252, 0.98));
|
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(247, 250, 252, 0.98));
|
||||||
box-shadow: 0 28px 56px rgba(15, 23, 42, 0.18);
|
box-shadow: 0 28px 56px rgba(15, 23, 42, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-badge {
|
.shared-confirm-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-badge.info {
|
.shared-confirm-badge.info {
|
||||||
background: rgba(59, 130, 246, 0.12);
|
background: rgba(59, 130, 246, 0.12);
|
||||||
color: #1d4ed8;
|
color: #1d4ed8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-badge.warning {
|
.shared-confirm-badge.warning {
|
||||||
background: rgba(245, 158, 11, 0.14);
|
background: rgba(245, 158, 11, 0.14);
|
||||||
color: #b45309;
|
color: #b45309;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-badge.danger {
|
.shared-confirm-badge.danger {
|
||||||
background: rgba(239, 68, 68, 0.12);
|
background: rgba(239, 68, 68, 0.12);
|
||||||
color: #dc2626;
|
color: #dc2626;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-card h4 {
|
.shared-confirm-card h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-card p {
|
.shared-confirm-card p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #5b6b83;
|
color: #5b6b83;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-body {
|
.shared-confirm-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-actions {
|
.shared-confirm-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-btn {
|
.shared-confirm-btn {
|
||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
min-height: 42px;
|
min-height: 42px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease, background 160ms ease;
|
transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease, background 160ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-btn.cancel {
|
.shared-confirm-btn.cancel {
|
||||||
border: 1px solid #d7e0ea;
|
border: 1px solid #d7e0ea;
|
||||||
background: rgba(255, 255, 255, 0.92);
|
background: rgba(255, 255, 255, 0.92);
|
||||||
color: #475569;
|
color: #475569;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-btn.confirm {
|
.shared-confirm-btn.confirm {
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-btn.confirm.primary {
|
.shared-confirm-btn.confirm.primary {
|
||||||
background: linear-gradient(135deg, #10b981, #059669);
|
background: linear-gradient(135deg, #10b981, #059669);
|
||||||
box-shadow: 0 12px 24px rgba(5, 150, 105, 0.22);
|
box-shadow: 0 12px 24px rgba(5, 150, 105, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-btn.confirm.danger {
|
.shared-confirm-btn.confirm.danger {
|
||||||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||||
box-shadow: 0 12px 24px rgba(220, 38, 38, 0.22);
|
box-shadow: 0 12px 24px rgba(220, 38, 38, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-btn.cancel:hover:not(:disabled) {
|
.shared-confirm-btn.cancel:hover:not(:disabled) {
|
||||||
border-color: rgba(16, 185, 129, 0.3);
|
border-color: rgba(16, 185, 129, 0.3);
|
||||||
color: #047857;
|
color: #047857;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-btn.confirm:hover:not(:disabled) {
|
.shared-confirm-btn.confirm:hover:not(:disabled) {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-btn:disabled {
|
.shared-confirm-btn:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.68;
|
opacity: 0.68;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-enter-active,
|
.shared-confirm-enter-active,
|
||||||
.shared-confirm-leave-active {
|
.shared-confirm-leave-active {
|
||||||
transition: opacity 0.18s ease, transform 0.18s ease;
|
transition: opacity 0.18s ease, transform 0.18s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-enter-from,
|
.shared-confirm-enter-from,
|
||||||
.shared-confirm-leave-to {
|
.shared-confirm-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-enter-from .shared-confirm-card,
|
.shared-confirm-enter-from .shared-confirm-card,
|
||||||
.shared-confirm-leave-to .shared-confirm-card {
|
.shared-confirm-leave-to .shared-confirm-card {
|
||||||
transform: translateY(8px) scale(0.98);
|
transform: translateY(8px) scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.shared-confirm-mask {
|
.shared-confirm-mask {
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-card {
|
.shared-confirm-card {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-card h4 {
|
.shared-confirm-card h4 {
|
||||||
font-size: 19px;
|
font-size: 19px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-actions {
|
.shared-confirm-actions {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shared-confirm-btn {
|
.shared-confirm-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,310 +1,310 @@
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
import { useNavigation, navItems } from './useNavigation.js'
|
import { useNavigation, navItems } from './useNavigation.js'
|
||||||
import { useRequests } from './useRequests.js'
|
import { useRequests } from './useRequests.js'
|
||||||
import { useSystemState } from './useSystemState.js'
|
import { useSystemState } from './useSystemState.js'
|
||||||
import { useToast } from './useToast.js'
|
import { useToast } from './useToast.js'
|
||||||
import { fetchLatestConversation } from '../services/orchestrator.js'
|
import { fetchLatestConversation } from '../services/orchestrator.js'
|
||||||
import { normalizeRequestForUi } from '../utils/requestViewModel.js'
|
import { normalizeRequestForUi } from '../utils/requestViewModel.js'
|
||||||
|
|
||||||
const SESSION_TYPE_EXPENSE = 'expense'
|
const SESSION_TYPE_EXPENSE = 'expense'
|
||||||
|
|
||||||
function isPlaceholderValue(value) {
|
function isPlaceholderValue(value) {
|
||||||
const text = String(value || '').trim()
|
const text = String(value || '').trim()
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return ['待补充', '暂无', '无', '未知', '处理中'].includes(text.replace(/\s+/g, ''))
|
return ['待补充', '暂无', '无', '未知', '处理中'].includes(text.replace(/\s+/g, ''))
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasMissingAttachment(request) {
|
function hasMissingAttachment(request) {
|
||||||
const expenseItems = Array.isArray(request?.expenseItems) ? request.expenseItems : []
|
const expenseItems = Array.isArray(request?.expenseItems) ? request.expenseItems : []
|
||||||
|
|
||||||
if (expenseItems.length) {
|
if (expenseItems.length) {
|
||||||
return expenseItems.some((item) => !String(item?.invoiceId || item?.invoice_id || '').trim())
|
return expenseItems.some((item) => !String(item?.invoiceId || item?.invoice_id || '').trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachmentSummary = String(request?.attachmentSummary || '').trim()
|
const attachmentSummary = String(request?.attachmentSummary || '').trim()
|
||||||
const secondaryStatusValue = String(request?.secondaryStatusValue || '').trim()
|
const secondaryStatusValue = String(request?.secondaryStatusValue || '').trim()
|
||||||
return /待|缺|未/.test(attachmentSummary) || /待|缺|未/.test(secondaryStatusValue)
|
return /待|缺|未/.test(attachmentSummary) || /待|缺|未/.test(secondaryStatusValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasPendingInfo(request) {
|
function hasPendingInfo(request) {
|
||||||
if (!request) {
|
if (!request) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.approvalKey === 'draft' || request.approvalKey === 'supplement') {
|
if (request.approvalKey === 'draft' || request.approvalKey === 'supplement') {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Number.isFinite(Number(request.amountValue)) || Number(request.amountValue) <= 0) {
|
if (!Number.isFinite(Number(request.amountValue)) || Number(request.amountValue) <= 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
request.profileDepartment,
|
request.profileDepartment,
|
||||||
request.profilePosition,
|
request.profilePosition,
|
||||||
request.profileGrade,
|
request.profileGrade,
|
||||||
request.profileManager,
|
request.profileManager,
|
||||||
request.reason,
|
request.reason,
|
||||||
request.occurredDisplay
|
request.occurredDisplay
|
||||||
].some(isPlaceholderValue)
|
].some(isPlaceholderValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveDetailAlertTone(request) {
|
function resolveDetailAlertTone(request) {
|
||||||
if (request?.approvalKey === 'completed') return 'success'
|
if (request?.approvalKey === 'completed') return 'success'
|
||||||
if (request?.approvalKey === 'rejected') return 'danger'
|
if (request?.approvalKey === 'rejected') return 'danger'
|
||||||
return 'warning'
|
return 'warning'
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDetailAlerts(request) {
|
function buildDetailAlerts(request) {
|
||||||
if (!request) {
|
if (!request) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const alerts = []
|
const alerts = []
|
||||||
const nodeLabel = String(request.node || request.approval || '').trim()
|
const nodeLabel = String(request.node || request.approval || '').trim()
|
||||||
|
|
||||||
if (nodeLabel) {
|
if (nodeLabel) {
|
||||||
alerts.push({ label: nodeLabel, tone: resolveDetailAlertTone(request) })
|
alerts.push({ label: nodeLabel, tone: resolveDetailAlertTone(request) })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasMissingAttachment(request)) {
|
if (hasMissingAttachment(request)) {
|
||||||
alerts.push({ label: '缺少票据', tone: 'warning' })
|
alerts.push({ label: '缺少票据', tone: 'warning' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasPendingInfo(request)) {
|
if (hasPendingInfo(request)) {
|
||||||
alerts.push({ label: '待补信息', tone: 'warning' })
|
alerts.push({ label: '待补信息', tone: 'warning' })
|
||||||
}
|
}
|
||||||
|
|
||||||
return alerts.filter((item, index, list) => list.findIndex((entry) => entry.label === item.label) === index).slice(0, 3)
|
return alerts.filter((item, index, list) => list.findIndex((entry) => entry.label === item.label) === index).slice(0, 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAppShell() {
|
export function useAppShell() {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const smartEntryOpen = ref(false)
|
const smartEntryOpen = ref(false)
|
||||||
const smartEntryContext = ref({ prompt: '', source: 'requests', request: null, files: [], conversation: null })
|
const smartEntryContext = ref({ prompt: '', source: 'requests', request: null, files: [], conversation: null })
|
||||||
const smartEntrySessionId = ref(0)
|
const smartEntrySessionId = ref(0)
|
||||||
|
|
||||||
const { activeView, currentView, setView } = useNavigation()
|
const { activeView, currentView, setView } = useNavigation()
|
||||||
const {
|
const {
|
||||||
requests,
|
requests,
|
||||||
loading: requestsLoading,
|
loading: requestsLoading,
|
||||||
error: requestsError,
|
error: requestsError,
|
||||||
search,
|
search,
|
||||||
filters,
|
filters,
|
||||||
ranges,
|
ranges,
|
||||||
activeRange,
|
activeRange,
|
||||||
filteredRequests,
|
filteredRequests,
|
||||||
approveRequest,
|
approveRequest,
|
||||||
rejectRequest,
|
rejectRequest,
|
||||||
reload: reloadRequests
|
reload: reloadRequests
|
||||||
} = useRequests()
|
} = useRequests()
|
||||||
const { currentUser } = useSystemState()
|
const { currentUser } = useSystemState()
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
|
|
||||||
const customRange = ref({ start: '2024-07-06', end: '2024-07-12' })
|
const customRange = ref({ start: '2024-07-06', end: '2024-07-12' })
|
||||||
|
|
||||||
const selectedRequest = computed(() => {
|
const selectedRequest = computed(() => {
|
||||||
const requestId = String(route.params.requestId || '')
|
const requestId = String(route.params.requestId || '')
|
||||||
|
|
||||||
if (!requestId) {
|
if (!requestId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawRequest = requests.value.find(
|
const rawRequest = requests.value.find(
|
||||||
(item) => String(item.claimId || '').trim() === requestId || String(item.id || '').trim() === requestId
|
(item) => String(item.claimId || '').trim() === requestId || String(item.id || '').trim() === requestId
|
||||||
)
|
)
|
||||||
return normalizeRequestForUi(rawRequest)
|
return normalizeRequestForUi(rawRequest)
|
||||||
})
|
})
|
||||||
|
|
||||||
const detailMode = computed(() => route.name === 'app-request-detail')
|
const detailMode = computed(() => route.name === 'app-request-detail')
|
||||||
const logDetailMode = computed(() => route.name === 'app-log-detail')
|
const logDetailMode = computed(() => route.name === 'app-log-detail')
|
||||||
const detailAlerts = computed(() => (detailMode.value ? buildDetailAlerts(selectedRequest.value) : []))
|
const detailAlerts = computed(() => (detailMode.value ? buildDetailAlerts(selectedRequest.value) : []))
|
||||||
|
|
||||||
const topBarView = computed(() => {
|
const topBarView = computed(() => {
|
||||||
if (detailMode.value) {
|
if (detailMode.value) {
|
||||||
return {
|
return {
|
||||||
title: '报销单详情',
|
title: '报销单详情',
|
||||||
desc: '查看报销明细、票据材料、审批进度与风险提示。'
|
desc: '查看报销明细、票据材料、审批进度与风险提示。'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (logDetailMode.value) {
|
if (logDetailMode.value) {
|
||||||
return {
|
return {
|
||||||
title: '日志详情',
|
title: '日志详情',
|
||||||
desc: '查看单条日志的解析结果、上下文信息与原始记录。'
|
desc: '查看单条日志的解析结果、上下文信息与原始记录。'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentView.value
|
return currentView.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const requestSummary = computed(() =>
|
const requestSummary = computed(() =>
|
||||||
filteredRequests.value.reduce(
|
filteredRequests.value.reduce(
|
||||||
(summary, item) => {
|
(summary, item) => {
|
||||||
const request = normalizeRequestForUi(item)
|
const request = normalizeRequestForUi(item)
|
||||||
if (!request) {
|
if (!request) {
|
||||||
return summary
|
return summary
|
||||||
}
|
}
|
||||||
|
|
||||||
summary.total += 1
|
summary.total += 1
|
||||||
|
|
||||||
if (request.approvalKey === 'draft') {
|
if (request.approvalKey === 'draft') {
|
||||||
summary.draft += 1
|
summary.draft += 1
|
||||||
} else if (request.approvalKey === 'in_progress') {
|
} else if (request.approvalKey === 'in_progress') {
|
||||||
summary.inProgress += 1
|
summary.inProgress += 1
|
||||||
} else if (request.approvalKey === 'supplement') {
|
} else if (request.approvalKey === 'supplement') {
|
||||||
summary.supplement += 1
|
summary.supplement += 1
|
||||||
} else if (request.approvalKey === 'completed') {
|
} else if (request.approvalKey === 'completed') {
|
||||||
summary.completed += 1
|
summary.completed += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return summary
|
return summary
|
||||||
},
|
},
|
||||||
{ total: 0, draft: 0, inProgress: 0, supplement: 0, completed: 0 }
|
{ total: 0, draft: 0, inProgress: 0, supplement: 0, completed: 0 }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
function handleApprove(request) {
|
function handleApprove(request) {
|
||||||
const message = approveRequest(request)
|
const message = approveRequest(request)
|
||||||
toast(message)
|
toast(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleReject(request) {
|
function handleReject(request) {
|
||||||
const message = rejectRequest(request)
|
const message = rejectRequest(request)
|
||||||
toast(message)
|
toast(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNavigate(view) {
|
function handleNavigate(view) {
|
||||||
smartEntryOpen.value = false
|
smartEntryOpen.value = false
|
||||||
setView(view)
|
setView(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openTravelCreate() {
|
function openTravelCreate() {
|
||||||
smartEntryOpen.value = true
|
smartEntryOpen.value = true
|
||||||
smartEntryContext.value = { prompt: '', source: 'topbar', request: null, files: [], conversation: null }
|
smartEntryContext.value = { prompt: '', source: 'topbar', request: null, files: [], conversation: null }
|
||||||
smartEntrySessionId.value += 1
|
smartEntrySessionId.value += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveCurrentUserId() {
|
function resolveCurrentUserId() {
|
||||||
const user = currentUser.value || {}
|
const user = currentUser.value || {}
|
||||||
return String(user.username || user.name || 'anonymous').trim() || 'anonymous'
|
return String(user.username || user.name || 'anonymous').trim() || 'anonymous'
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveSmartEntryConversation(payload = {}) {
|
async function resolveSmartEntryConversation(payload = {}) {
|
||||||
if (payload.conversation) {
|
if (payload.conversation) {
|
||||||
return payload.conversation
|
return payload.conversation
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!payload.restoreLatestConversation) {
|
if (!payload.restoreLatestConversation) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const latestPayload = await fetchLatestConversation(resolveCurrentUserId(), SESSION_TYPE_EXPENSE, {
|
const latestPayload = await fetchLatestConversation(resolveCurrentUserId(), SESSION_TYPE_EXPENSE, {
|
||||||
preferRecoverable: true
|
preferRecoverable: true
|
||||||
})
|
})
|
||||||
return latestPayload?.found ? latestPayload.conversation || null : null
|
return latestPayload?.found ? latestPayload.conversation || null : null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to restore latest expense conversation for smart entry:', error)
|
console.warn('Failed to restore latest expense conversation for smart entry:', error)
|
||||||
toast(error?.message || '恢复最近报销会话失败,请稍后重试。')
|
toast(error?.message || '恢复最近报销会话失败,请稍后重试。')
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openSmartEntry(payload = {}) {
|
async function openSmartEntry(payload = {}) {
|
||||||
const conversation = await resolveSmartEntryConversation(payload)
|
const conversation = await resolveSmartEntryConversation(payload)
|
||||||
smartEntryOpen.value = true
|
smartEntryOpen.value = true
|
||||||
|
|
||||||
smartEntryContext.value = {
|
smartEntryContext.value = {
|
||||||
prompt: payload.prompt ?? '',
|
prompt: payload.prompt ?? '',
|
||||||
source: payload.source ?? 'workbench',
|
source: payload.source ?? 'workbench',
|
||||||
request: payload.request ?? selectedRequest.value,
|
request: payload.request ?? selectedRequest.value,
|
||||||
files: Array.isArray(payload.files) ? payload.files : [],
|
files: Array.isArray(payload.files) ? payload.files : [],
|
||||||
conversation
|
conversation
|
||||||
}
|
}
|
||||||
smartEntrySessionId.value += 1
|
smartEntrySessionId.value += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeSmartEntry() {
|
function closeSmartEntry() {
|
||||||
smartEntryOpen.value = false
|
smartEntryOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDraftSaved(payload = {}) {
|
async function handleDraftSaved(payload = {}) {
|
||||||
const claimNo = String(payload.claimNo || payload.claim_no || '').trim()
|
const claimNo = String(payload.claimNo || payload.claim_no || '').trim()
|
||||||
const status = String(payload.status || payload.claimStatus || '').trim()
|
const status = String(payload.status || payload.claimStatus || '').trim()
|
||||||
const approvalStage = String(payload.approvalStage || payload.approval_stage || '').trim()
|
const approvalStage = String(payload.approvalStage || payload.approval_stage || '').trim()
|
||||||
smartEntryOpen.value = false
|
smartEntryOpen.value = false
|
||||||
await reloadRequests()
|
await reloadRequests()
|
||||||
if (status === 'submitted') {
|
if (status === 'submitted') {
|
||||||
toast(`${claimNo || '该'}单据已完成 AI验审${approvalStage ? `,当前节点:${approvalStage}` : ',并已提交审批'}。`)
|
toast(`${claimNo || '该'}单据已完成 AI验审${approvalStage ? `,当前节点:${approvalStage}` : ',并已提交审批'}。`)
|
||||||
} else {
|
} else {
|
||||||
toast(`${claimNo || '该'}单据已保存到草稿,请到报销页面查看。`)
|
toast(`${claimNo || '该'}单据已保存到草稿,请到报销页面查看。`)
|
||||||
}
|
}
|
||||||
router.push({ name: 'app-requests' })
|
router.push({ name: 'app-requests' })
|
||||||
}
|
}
|
||||||
|
|
||||||
function openRequestDetail(request) {
|
function openRequestDetail(request) {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'app-request-detail',
|
name: 'app-request-detail',
|
||||||
params: { requestId: request.claimId || request.id }
|
params: { requestId: request.claimId || request.id }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeRequestDetail() {
|
function closeRequestDetail() {
|
||||||
router.push({ name: 'app-requests' })
|
router.push({ name: 'app-requests' })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRequestUpdated() {
|
async function handleRequestUpdated() {
|
||||||
await reloadRequests()
|
await reloadRequests()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRequestDeleted() {
|
async function handleRequestDeleted() {
|
||||||
await reloadRequests()
|
await reloadRequests()
|
||||||
router.push({ name: 'app-requests' })
|
router.push({ name: 'app-requests' })
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeRange,
|
activeRange,
|
||||||
activeView,
|
activeView,
|
||||||
closeRequestDetail,
|
closeRequestDetail,
|
||||||
closeSmartEntry,
|
closeSmartEntry,
|
||||||
currentView,
|
currentView,
|
||||||
customRange,
|
customRange,
|
||||||
detailMode,
|
detailMode,
|
||||||
logDetailMode,
|
logDetailMode,
|
||||||
filteredRequests,
|
filteredRequests,
|
||||||
filters,
|
filters,
|
||||||
handleApprove,
|
handleApprove,
|
||||||
handleDraftSaved,
|
handleDraftSaved,
|
||||||
handleNavigate,
|
handleNavigate,
|
||||||
handleReject,
|
handleReject,
|
||||||
handleRequestDeleted,
|
handleRequestDeleted,
|
||||||
handleRequestUpdated,
|
handleRequestUpdated,
|
||||||
navItems,
|
navItems,
|
||||||
openRequestDetail,
|
openRequestDetail,
|
||||||
openSmartEntry,
|
openSmartEntry,
|
||||||
openTravelCreate,
|
openTravelCreate,
|
||||||
ranges,
|
ranges,
|
||||||
requestSummary,
|
requestSummary,
|
||||||
requestsError,
|
requestsError,
|
||||||
requestsLoading,
|
requestsLoading,
|
||||||
reloadRequests,
|
reloadRequests,
|
||||||
requests,
|
requests,
|
||||||
search,
|
search,
|
||||||
selectedRequest,
|
selectedRequest,
|
||||||
setView,
|
setView,
|
||||||
smartEntryContext,
|
smartEntryContext,
|
||||||
smartEntryOpen,
|
smartEntryOpen,
|
||||||
smartEntrySessionId,
|
smartEntrySessionId,
|
||||||
detailAlerts,
|
detailAlerts,
|
||||||
toast,
|
toast,
|
||||||
topBarView
|
topBarView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,35 +153,34 @@
|
|||||||
</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">
|
||||||
<div>
|
<div>
|
||||||
<strong>{{ item.actor }}</strong>
|
<strong>{{ item.actor }}</strong>
|
||||||
<span>{{ item.time }}</span>
|
<span>{{ item.time }}</span>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
|
||||||
</small>
|
</small>
|
||||||
<div v-if="item.previewChanges.length" class="change-record-preview">
|
<div v-if="item.previewChanges.length" class="change-record-preview">
|
||||||
<span
|
<span
|
||||||
@@ -197,9 +196,9 @@
|
|||||||
</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>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -1086,11 +1085,9 @@
|
|||||||
<span>{{ item.timeLabel }}</span>
|
<span>{{ item.timeLabel }}</span>
|
||||||
</header>
|
</header>
|
||||||
<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>
|
</small>
|
||||||
<template v-if="item.source_version"> · 来源版本:{{ item.source_version }}</template>
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
@@ -1129,12 +1126,8 @@
|
|||||||
<span>修改时间</span>
|
<span>修改时间</span>
|
||||||
<strong>{{ selectedSpreadsheetChangeRecord.time }}</strong>
|
<strong>{{ selectedSpreadsheetChangeRecord.time }}</strong>
|
||||||
</article>
|
</article>
|
||||||
<article v-if="selectedSpreadsheetChangeRecord.version">
|
<article>
|
||||||
<span>关联版本</span>
|
<span>修改工作表</span>
|
||||||
<strong>{{ selectedSpreadsheetChangeRecord.version }}</strong>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<span>修改工作表</span>
|
|
||||||
<strong>{{ selectedSpreadsheetChangeRecord.changed_sheet_count }}</strong>
|
<strong>{{ selectedSpreadsheetChangeRecord.changed_sheet_count }}</strong>
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
@@ -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
|
||||||
@@ -89,15 +88,35 @@
|
|||||||
{{ message.text }}
|
{{ message.text }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
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,28 +428,120 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="composer-row" :class="{ 'knowledge-mode': isKnowledgeSession }">
|
<div class="composer-row" :class="{ 'knowledge-mode': isKnowledgeSession }">
|
||||||
<button
|
<div v-if="!isKnowledgeSession" class="composer-leading-actions">
|
||||||
v-if="!isKnowledgeSession"
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="tool-btn composer-side-btn"
|
class="tool-btn composer-side-btn"
|
||||||
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
||||||
aria-label="上传附件"
|
aria-label="上传附件"
|
||||||
@click="triggerFileUpload"
|
@click="triggerFileUpload"
|
||||||
>
|
>
|
||||||
<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">
|
||||||
<textarea
|
<div class="composer-shell-body">
|
||||||
ref="composerTextareaRef"
|
<span
|
||||||
v-model="composerDraft"
|
v-for="tag in composerBusinessTimeTags"
|
||||||
rows="1"
|
:key="tag.id"
|
||||||
:placeholder="composerPlaceholder"
|
class="composer-biz-time-tag"
|
||||||
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
>
|
||||||
@input="handleComposerInput"
|
<i class="mdi mdi-calendar-check"></i>
|
||||||
@keydown.enter.exact.stop
|
<span class="composer-biz-time-tag-label">{{ tag.label }}</span>
|
||||||
@keydown.ctrl.enter.prevent="submitComposer"
|
<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
|
||||||
|
ref="composerTextareaRef"
|
||||||
|
v-model="composerDraft"
|
||||||
|
rows="1"
|
||||||
|
:placeholder="composerPlaceholder"
|
||||||
|
:disabled="submitting || reviewActionBusy || sessionSwitchBusy"
|
||||||
|
@input="handleComposerInput"
|
||||||
|
@keydown.enter.exact.stop
|
||||||
|
@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="发送">
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user