Compare commits

...
Author SHA1 Message Date
linmianxunandCursor a5e73c01ae 陆运超时响应改为仅 4 项运输分类,去掉按线路拆出的重复配置。
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-21 09:15:24 +08:00
jillion886andCursor 20c27ae293 fix: 有价先发出价格明细文字再发卡
避免卡片写详见上一条时销售却先看到卡。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-20 19:50:42 +08:00
jillion886andCursor 6140eaf932 fix: 海运贸易条款出站改用 TMS 的 transportTerms
销售卡片仍写贸易条款,查价请求不再用对不上的 tradeTerms。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-20 19:50:41 +08:00
9 changed files with 122 additions and 33 deletions
+13 -2
View File
@@ -11,6 +11,7 @@ from __future__ import annotations
import logging
import threading
import time
from typing import Any, Optional
from agent.ledger.http_ledger import HttpLedger
@@ -41,6 +42,8 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
"""
bookmark_kind = "sea"
# 价格明细文字发出后稍等再发卡,避免企微先画出卡片。
_detail_before_card_gap_sec = 0.8
def __init__(self, ledger: Any = None, group_client: Any = None) -> None:
super().__init__(ledger=ledger)
@@ -376,16 +379,18 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
business_line: str = "",
) -> None:
"""
先发完整价格明细文本,再发带按钮的卡。
先发完整价格明细文本,等它真正发出,再发带按钮的卡。
企微只发卡时,引用条超长会被截断;文本不受限。
测服 WO202609200020:文字和卡同一拍入队,销售先看到卡,
「详见上一条明细」对不上。只等这一条出站,不锁其它工单。
"""
line = (business_line or getattr(self, "business_line", "") or "SEA").upper()
if line not in {"SEA", "LAND"}:
line = "SEA"
src = dict(quote or {})
src["business_line"] = line
reply(
out_id = reply(
copy.sea_quote_detail_text(
work_order_no=work_order_no,
quote=src,
@@ -394,7 +399,13 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
status=status,
business_line=line,
)
) or ""
self._wait_outbound_sent(
str(out_id), work_order_no=work_order_no, what="价格明细文本"
)
gap = float(getattr(self, "_detail_before_card_gap_sec", 0.8) or 0)
if out_id and gap > 0:
time.sleep(gap)
reply(
copy.sea_tms_hit_card(
work_order_no=work_order_no,
@@ -535,6 +535,37 @@ class SeaTmsCardTests(unittest.TestCase):
self.assertIn("时效:1天", sub)
self.assertIn("报价有效期:2026-09-30", sub)
def test_sea_hit_sends_detail_text_before_card(self) -> None:
"""
有价必须先发出价格明细文字,再发卡。
测服 WO202609200020:卡上写「详见上一条明细」,销售却先看到卡。
"""
steps: list[str] = []
def reply(text: str, extra=None) -> str:
kind = "card" if extra else "text"
steps.append(kind)
return f"out-{len(steps)}"
def wait(out_id: str, *, work_order_no: str, what: str) -> None:
steps.append(f"wait:{out_id}")
self.flow._wait_outbound_sent = wait # type: ignore[method-assign]
self.flow._detail_before_card_gap_sec = 0.0
self.flow._emit_sea_hit(
reply,
work_order_no="WO202609200020",
quote={
"total": "14298.53",
"currency": "CNY",
"source_label": "TMS标准报价",
"fee_lines": [{"name": "操作费", "amount": "1220", "currency": "CNY"}],
},
)
self.assertEqual(steps[:3], ["text", "wait:out-1", "card"])
self.assertIn("card", steps)
def test_sea_hit_card_has_price_and_two_buttons(self) -> None:
phase = self.flow.on_text(
sender_id="s1",
@@ -41,6 +41,7 @@ public class InquiryResponseTimeoutServiceImpl
private static final Map<String, String> ID_TO_TIMEOUT_CODE = new LinkedHashMap<>();
static {
// 陆运产品仅保留 4 项运输分类(与线路解耦);旧逗号写法一次性纠偏
LAND_PRODUCT_TYPE_RENAMES.put(
"普件,中港、东南亚常用线路/常规车型",
"普件(东南亚、中港常用线路/常规车型)");
@@ -50,12 +51,6 @@ public class InquiryResponseTimeoutServiceImpl
LAND_PRODUCT_TYPE_RENAMES.put(
"普件,中亚、中欧、东南亚涉及境外段清派等特殊要求",
"普件(东南亚、中亚、中欧涉及境外段清派等特殊要求)");
LAND_PRODUCT_TYPE_RENAMES.put(
"普件,常用线路/常规车型",
"普件(常用线路/常规车型)");
LAND_PRODUCT_TYPE_RENAMES.put(
"普件,非常用线路/临时线路/特种车型",
"普件(非常用线路/临时线路/特种车型)");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000001", "air_sales");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000002", "air_route");
@@ -63,19 +58,11 @@ public class InquiryResponseTimeoutServiceImpl
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000004", "land_sales");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000005", "sea_product_ptp");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000006", "sea_product_other");
// 陆运产品运输分类 4 项(编码沿用 land_hk_*,勿再按线路拆 ltl/city/sea)
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000007", "land_hk_common");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000008", "land_hk_cross");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000009", "land_hk_special");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000010", "land_hk_express");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000011", "land_ltl_common");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000012", "land_ltl_special");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000013", "land_ltl_express");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000014", "land_city_common");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000015", "land_city_special");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000016", "land_city_express");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000017", "land_sea_common");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000018", "land_sea_special");
ID_TO_TIMEOUT_CODE.put("rt100000000000000000000000000019", "land_sea_express");
}
/** 本进程内只纠偏一次,避免每次打开系统设置都打 UPDATE */
@@ -93,7 +93,11 @@ public class InquiryTmsQueryService {
if (items != null) {
segment.put("containerItems", items);
}
segment.put("tradeTerms", first(facts.get("贸易条款"), facts.get("trade_terms"), facts.get("tradeTerms")));
segment.put("transportTerms", first(
facts.get("贸易条款"),
facts.get("trade_terms"),
facts.get("transportTerms"),
facts.get("tradeTerms")));
} else if ("LAND".equals(line)) {
// 陆运不套空运包装/件数;分类和中港装载只在工单,mapper 不会放进出站。
segment.putAll(TmsLandSegmentMapper.build(facts));
@@ -654,7 +658,8 @@ public class InquiryTmsQueryService {
if (blank(facts.get("箱型箱量"))) {
missing.add("箱型箱量");
}
if (blank(facts.get("贸易条款")) && blank(facts.get("trade_terms")) && blank(facts.get("tradeTerms"))) {
if (blank(facts.get("贸易条款")) && blank(facts.get("trade_terms"))
&& blank(facts.get("transportTerms")) && blank(facts.get("tradeTerms"))) {
missing.add("贸易条款");
}
return missing;
@@ -113,7 +113,8 @@ public final class TmsV2RequestMapper {
}
put(target, "containerType", type);
put(target, "containerQty", asInteger(qty));
put(target, "tradeTerms", first(source, "tradeTerms", "贸易条款", "trade_terms"));
// TMS 查询字段是 transportTerms。工单/卡片仍写「贸易条款」,这里只改出站名。
put(target, "transportTerms", first(source, "transportTerms", "tradeTerms", "贸易条款", "trade_terms"));
}
/**
@@ -19,15 +19,7 @@ UPDATE `inq_response_timeout` SET `timeout_code`='land_hk_common' WHERE `id`='rt
UPDATE `inq_response_timeout` SET `timeout_code`='land_hk_cross' WHERE `id`='rt100000000000000000000000000008' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_hk_special' WHERE `id`='rt100000000000000000000000000009' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_hk_express' WHERE `id`='rt100000000000000000000000000010' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_ltl_common' WHERE `id`='rt100000000000000000000000000011' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_ltl_special' WHERE `id`='rt100000000000000000000000000012' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_ltl_express' WHERE `id`='rt100000000000000000000000000013' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_city_common' WHERE `id`='rt100000000000000000000000000014' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_city_special' WHERE `id`='rt100000000000000000000000000015' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_city_express' WHERE `id`='rt100000000000000000000000000016' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_sea_common' WHERE `id`='rt100000000000000000000000000017' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_sea_special' WHERE `id`='rt100000000000000000000000000018' AND (`timeout_code` IS NULL OR `timeout_code`='');
UPDATE `inq_response_timeout` SET `timeout_code`='land_sea_express' WHERE `id`='rt100000000000000000000000000019' AND (`timeout_code` IS NULL OR `timeout_code`='');
-- 陆运产品仅 4 项;ltl/city/sea 重复行已由 migration_inq_response_timeout_land_class_v2.sql 删除
-- 3) 唯一索引(列已全部非空后再建;若仍有 NULL 会失败)
UPDATE `inq_response_timeout` SET `timeout_code`=CONCAT('rt_', `id`) WHERE `timeout_code` IS NULL OR `timeout_code`='';
@@ -0,0 +1,59 @@
-- 陆运产品运输分类:只保留产品确认的 4 项,删除按线路重复的 ltl/city/sea 共 9 条。
-- 保留 land_hk_common / land_hk_cross / land_hk_special / land_hk_express,清空 route_category(分类与线路解耦)。
-- 仅改 inquiry_robot;幂等可重复执行。
SET NAMES utf8mb4;
-- 1) 删除按线路拆开的重复产品行
DELETE FROM `inq_response_timeout`
WHERE `timeout_code` IN (
'land_ltl_common','land_ltl_special','land_ltl_express',
'land_city_common','land_city_special','land_city_express',
'land_sea_common','land_sea_special','land_sea_express'
);
DELETE FROM `inq_response_timeout`
WHERE `id` IN (
'rt100000000000000000000000000011',
'rt100000000000000000000000000012',
'rt100000000000000000000000000013',
'rt100000000000000000000000000014',
'rt100000000000000000000000000015',
'rt100000000000000000000000000016',
'rt100000000000000000000000000017',
'rt100000000000000000000000000018',
'rt100000000000000000000000000019'
);
-- 2) 保留的 4 项:分类与线路解耦,route_category 置空;文案对齐产品确认
UPDATE `inq_response_timeout`
SET `route_category`='',
`transport_type`='普件(东南亚、中港常用线路/常规车型)',
`update_by`='system',
`update_time`=NOW()
WHERE `id`='rt100000000000000000000000000007'
OR `timeout_code`='land_hk_common';
UPDATE `inq_response_timeout`
SET `route_category`='',
`transport_type`='普件(东南亚、中亚、中欧涉及国内+国外段)',
`update_by`='system',
`update_time`=NOW()
WHERE `id`='rt100000000000000000000000000008'
OR `timeout_code`='land_hk_cross';
UPDATE `inq_response_timeout`
SET `route_category`='',
`transport_type`='普件(东南亚、中亚、中欧涉及境外段清派等特殊要求)',
`update_by`='system',
`update_time`=NOW()
WHERE `id`='rt100000000000000000000000000009'
OR `timeout_code`='land_hk_special';
UPDATE `inq_response_timeout`
SET `route_category`='',
`transport_type`='急件',
`update_by`='system',
`update_time`=NOW()
WHERE `id`='rt100000000000000000000000000010'
OR `timeout_code`='land_hk_express';
@@ -132,7 +132,7 @@ public class TmsV2RequestMapperTest {
}
@Test
public void seaSendsTradeTermsOmitsClass() {
public void seaSendsTransportTermsOmitsClass() {
Map<String, Object> seg = new HashMap<>();
seg.put("origin", "上海");
seg.put("destination", "洛杉矶");
@@ -140,7 +140,8 @@ public class TmsV2RequestMapperTest {
seg.put("cargoQty", "20吨");
seg.put("loadType", "整柜");
seg.put("containerSpec", "1x40HQ");
seg.put("tradeTerms", "FOB");
// 测服 WO202609200019:工单仍是贸易条款,出站必须写成 transportTerms,不能写 tradeTerms。
seg.put("贸易条款", "4");
seg.put("运输分类", "Port to Port");
Map<String, Object> internal = new HashMap<>();
internal.put("transportMode", "SEA");
@@ -149,7 +150,9 @@ public class TmsV2RequestMapperTest {
Map<String, Object> body = TmsV2RequestMapper.quote(internal);
@SuppressWarnings("unchecked")
List<Map<String, Object>> segs = (List<Map<String, Object>>) body.get("segments");
assertEquals("FOB", segs.get(0).get("tradeTerms"));
assertEquals("4", segs.get(0).get("transportTerms"));
assertFalse(segs.get(0).containsKey("tradeTerms"));
assertFalse(segs.get(0).containsKey("贸易条款"));
assertFalse(segs.get(0).containsKey("运输分类"));
assertFalse(body.containsKey("运输分类"));
}
@@ -4,7 +4,7 @@
* 海运 / 空运 / 陆运同页自上而下分块展示,只允许改超时小时;无增删、无 Tab。
* 「编码」= timeoutCode,每条唯一(如 sea_sales / land_hk_express),供智能体单码调用;只读。
* roleCode(销售/产品)仅为角色维度,可重复,不是调用主键。
* 不展示线路类别(维度仍在主账,仅后台列表隐藏)。
* 陆运产品运输分类仅 4 项(与线路解耦);不展示线路类别。
* 数据来自主账 inq_response_timeout;本页不做工单超时判定。
*/
import { computed, onMounted, ref } from 'vue';