落地陆运群内协同:按填写样例拉群并只列本单协同项,进群把私聊已抽到的字段带上。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-18 13:38:02 +08:00
co-authored by Cursor
parent be95d823a2
commit 982b3a2317
25 changed files with 1451 additions and 315 deletions
@@ -155,7 +155,25 @@ def send_air_brief_and_sheet(
if room and not view.get("collab_chat_id"):
view["collab_chat_id"] = room
facts = dict(view.get("facts") or {})
from agent.schema.field_validate import AIR_COLLAB_FIELDS, pick_collab_from_facts
seeded = pick_collab_from_facts(facts, AIR_COLLAB_FIELDS)
collab = dict(view.get("collab_facts") or {})
if seeded:
changed = False
for k, v in seeded.items():
if v and not str(collab.get(k) or "").strip():
collab[k] = v
changed = True
if changed:
view["collab_facts"] = collab
patcher = getattr(getattr(flow, "ledger", None), "patch_collab_facts", None)
if callable(patcher):
patcher(
work_order_no=no,
facts=seeded,
sender_id=str(view.get("sales_wecom_id") or view.get("salesWecomId") or ""),
)
view["collab_facts"] = collab
view_q = dict(view.get("quote") or {})
override = dict(quote_override or {})
+52 -25
View File
@@ -17,9 +17,10 @@ from agent.schema.field_validate import (
AIR_COLLAB_FIELDS,
AIR_REQUIRED_FIELDS,
LAND_DISPLAY,
SEA_COLLAB_DISPLAY,
SEA_COLLAB_FIELDS,
SEA_REQUIRED_FIELDS,
collab_display_name,
collab_fields_for_ticket,
display_name,
)
from agent.schema.land_options import option_prompt as _land_option_prompt
@@ -101,6 +102,30 @@ _CONTINUE_HINT = (
ASK_TRANSPORT = "请补充运输方式:海运 / 空运 / 陆运。"
# 旧一句只问运输类型;销售要一次看到类型、线路、分类。残留调用也走完整清单。
ASK_LAND_SUBTYPE = _land_option_prompt(current={})
def land_sample_image_payload() -> dict[str, Any]:
"""
填写样例表图片出站。
走企微 image,不走文件气泡。路径随发布包,出站线程再读字节。
禁止在回调线程上传。
"""
from agent.schema.land_options import sample_image_path
path = sample_image_path()
return {
"msgtype": "image",
"filename": "填写样例.png",
"filepath": str(path),
}
def land_sample_image_fallback() -> str:
"""图片失败时回文字样例,避免销售什么都看不到。"""
from agent.schema.land_options import sample_fallback_text
return sample_fallback_text()
FIRST_CHAT = "你好,我是询价机器人。请直接描述客户需求,例如「上海到洛杉矶空运,3 件 180kg,下周三前到」,我会解析需求并查询 TMS 标准报价。"
ROUTE_MODEL_FAILED = "刚才没听清,请再说一次:是新询价,还是要接着办哪张工单?"
LAND_CONFIRM_TAIL = "请回复“确定”进行TMS查价,内容有误时,请重新发内容。"
@@ -111,9 +136,8 @@ LAND_TMS_RETRYING = "正在重新查询TMS报价。工单:{wo}"
_LAND_LCL_KEYS = ("起运港", "目的港", "品名", "体积", "毛重", "报价日期")
_LAND_FTL_KEYS = ("起运港", "目的港", "品名", "车型/数量", "报价日期")
_LAND_HK_FULL_KEYS = ("起运港", "目的港", "品名", "报价日期")
_LAND_LCL_TYPES = frozenset({"国内运输拼车", "跨境集拼"})
_LAND_FTL_TYPES = frozenset({"国内运输整车", "跨境整车"})
_LAND_CUSTOMS_TYPES = frozenset({"跨境集拼", "跨境整车", "中港"})
_LAND_FTL_TYPES = frozenset({"国内运输整车", "跨境整车", "中港零担/集拼"})
_LAND_CUSTOMS_TYPES = frozenset({"跨境集拼", "跨境整车", "中港整车", "中港零担/集拼"})
_LAND_EXAMPLES = {
"起运港": "广州",
"目的港": "深圳",
@@ -127,11 +151,8 @@ _LAND_EXAMPLES = {
def _land_cargo_keys(land_type: str, facts: dict[str, str] | None = None) -> tuple[str, ...]:
"""当前运输类型要展示的货物内部键。中港整车不要车型/数量。"""
if land_type == "中港":
load = str((facts or {}).get("中港装载方式") or "").strip()
if load == "零担/集拼":
return _LAND_FTL_KEYS
# 整车或还没选定装载:卡片/补问都不出现车型/数量
_ = facts
if land_type == "中港整车":
return _LAND_HK_FULL_KEYS
if land_type in _LAND_FTL_TYPES:
return _LAND_FTL_KEYS
@@ -142,8 +163,9 @@ def land_confirm_card(facts: dict[str, str] | None) -> str:
"""
确定查价前的核对卡。没有工单号。
行列出运输类型/线路/分类(中港再加装载)和该类型货物字段。
行列出运输类型/线路/分类和该类型货物字段。
国内不出现通关口岸;跨境/中港通关口岸非必填,没填留空行。
不再单独列出中港装载方式。
"""
src = dict(facts or {})
land_type = (src.get("运输类型") or "").strip()
@@ -152,8 +174,6 @@ def land_confirm_card(facts: dict[str, str] | None) -> str:
f"线路类别(必填):{(src.get('线路类别') or '').strip()}",
f"运输分类(必填):{(src.get('运输分类') or '').strip()}",
]
if land_type == "中港":
lines.append(f"中港装载方式(必填):{(src.get('中港装载方式') or '').strip()}")
for key in _land_cargo_keys(land_type, src):
label = LAND_DISPLAY.get(key, key)
val = (src.get(key) or "").strip()
@@ -170,6 +190,11 @@ def land_group_no_staff() -> str:
return "没有可拉的产品人员。请先在后台「员工管理」维护本线路的陆运产品岗后再试。"
def land_group_pair_rejected() -> str:
"""类型+线路不在填写样例里,禁止拉群。"""
return "拉群失败,请排查填写内容,并重新发起工单。"
def ask_land_fields(
*,
facts: dict[str, str] | None = None,
@@ -182,7 +207,7 @@ def ask_land_fields(
报价日期不进待补充。
"""
src = dict(facts or {})
skip = {"报价日期", "运输类型", "线路类别", "运输分类", "中港装载方式"}
skip = {"报价日期", "运输类型", "线路类别", "运输分类"}
missing = [k for k in missing_keys if k and k not in skip]
land_type = (land_subtype or src.get("运输类型") or "").strip()
lines = ["请确认并补充以下信息:", "运输方式:陆运"]
@@ -373,10 +398,6 @@ def inquiry_field_lines(
f"线路类别:{(facts.get('线路类别') or '-').strip() or '-'}",
f"运输分类:{(facts.get('运输分类') or '-').strip() or '-'}",
]
if land_type == "中港":
lines.append(
f"中港装载方式:{(facts.get('中港装载方式') or '-').strip() or '-'}"
)
for key in _land_cargo_keys(land_type, facts):
label = LAND_DISPLAY.get(key, key)
val = (facts.get(key) or "-").strip() or "-"
@@ -1151,8 +1172,10 @@ def sea_group_brief(
lines.append("")
lines.append("协同补充字段:")
collab = dict(collab_facts or {})
for key in SEA_COLLAB_FIELDS:
label = SEA_COLLAB_DISPLAY.get(key, key)
line = "LAND" if mode == "陆运" else "SEA"
collab_keys = collab_fields_for_ticket(facts, line) or SEA_COLLAB_FIELDS
for key in collab_keys:
label = collab_display_name(key, line)
val = (collab.get(key) or collab.get(label) or "").strip() or "-"
lines.append(f"{label}:{val}")
tms = dict(tms_quote) if tms_quote is not None else {}
@@ -1298,14 +1321,10 @@ def scheme_horizontal_rows(
def _land_scheme_type_line(facts: dict[str, str]) -> str:
"""方案卡「类型」格:运输类型、中港装载、线路类别、运输分类。"""
"""方案卡「类型」格:运输类型、线路类别、运输分类。"""
src = facts or {}
land_type = (src.get("运输类型") or "-").strip() or "-"
bits = [land_type]
if land_type == "中港":
load = (src.get("中港装载方式") or "").strip()
if load:
bits.append(load)
route = (src.get("线路类别") or "-").strip() or "-"
klass = (src.get("运输分类") or "-").strip() or "-"
bits.extend([route, klass])
@@ -1576,7 +1595,15 @@ def group_quote_not_recognized() -> str:
return "没有识别到报价,请产品再发一次价格或 Excel/PDF"
def group_fields_not_recognized() -> str:
def group_fields_not_recognized(
keys: tuple[str, ...] | None = None,
business_line: str = "",
) -> str:
"""没认出协同字段。陆运列出本单可补项;海运保持原六项口。"""
if keys:
line = (business_line or "").strip().upper() or "SEA"
names = [collab_display_name(k, line) for k in keys]
return "听到了。请直接说" + "、".join(names) + "。"
return "听到了。请直接说贸易条款、货好时间、商品海关编码、是否含油/含电/含磁。"
+56 -8
View File
@@ -1,7 +1,7 @@
"""
陆运文字询价确定性流程。
本文件职责:选项联动 → 按类型补货物 → 无工单号核对卡 →(确定后)建单查 TMS。
本文件职责:三项选项各自必填(不联动)→ 按类型补货物 → 无工单号核对卡 →(确定后)建单查 TMS。
查价后半段复用海运拉群/报价单/成交,拉人按线路类别,禁止陆运岗全员。
禁止:字段齐了就建单;直连 TMS;把海运「出卡即查价」改掉。
调用:handler 在 Worker/inbox 线程;禁止回调线程同步跑。
@@ -23,6 +23,8 @@ from agent.schema.land_options import (
harvest_land_options,
looks_like_confirm,
option_prompt,
option_prompt_needs_sample,
sample_pair_allowed,
validate_land_options,
)
from agent.schema.tms_land_query import assemble_land_query
@@ -112,7 +114,7 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
subtype = str(snap.get("land_subtype") or facts.get("运输类型") or "").strip()
if subtype and not str(facts.get("运输类型") or "").strip():
facts["运输类型"] = subtype
option_keys = ("运输类型", "线路类别", "运输分类", "中港装载方式")
option_keys = ("运输类型", "线路类别", "运输分类")
base = dict(sess.facts) if sess else {}
cargo = {k: v for k, v in facts.items() if k not in option_keys}
merged = dict(base)
@@ -184,6 +186,23 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
sess=sess,
)
def _reply_land_options(
self,
reply: ReplyFn,
facts: dict[str, str],
*,
message: str = "",
) -> None:
"""
选项补问:先发文字清单,需要样例时再发表格图。
图片失败由出站回退文字样例。不在本方法上传素材。
"""
body = (message or "").strip() or option_prompt(current=facts)
reply(body)
if option_prompt_needs_sample(current=facts):
reply(copy.land_sample_image_fallback(), copy.land_sample_image_payload())
def _confirm_or_hold(
self,
*,
@@ -221,7 +240,7 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
"""
销售确定之后才建陆运工单并查 TMS。
核对卡阶段没有工单号。运输分类、中港装载方式写入工单 facts,不进入 assemble 出站。
核对卡阶段没有工单号。运输分类写入工单 facts,不进入 assemble 出站。
"""
import uuid
@@ -363,10 +382,10 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
facts=merged, business_line="LAND", land_subtype=land_type
)
cargo = dict(check["facts"])
cargo.update({k: merged[k] for k in ("运输类型", "线路类别", "运输分类", "中港装载方式") if merged.get(k)})
cargo.update({k: merged[k] for k in ("运输类型", "线路类别", "运输分类") if merged.get(k)})
tms = assemble_land_query(cargo)
cargo = dict(tms.get("facts") or cargo)
cargo.update({k: merged[k] for k in ("运输类型", "线路类别", "运输分类", "中港装载方式") if merged.get(k)})
cargo.update({k: merged[k] for k in ("运输类型", "线路类别", "运输分类") if merged.get(k)})
missing = list(check.get("missing") or [])
for key in tms.get("missing") or []:
if key not in missing and key not in {"运输类型", "线路类别"}:
@@ -412,7 +431,11 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
return "wait_confirm"
if state.get("stage") == "options_error":
err = (state["opt"].get("errors") or [{}])[0]
reply(str(err.get("message") or option_prompt(current=facts)))
self._reply_land_options(
reply,
facts,
message=str(err.get("message") or ""),
)
self._save(
FlowSession(
sender_id=sender_id,
@@ -427,12 +450,11 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
self._clear_parked_confirm(sender_id)
return "need_land_options"
if state["stage"] == "options_missing":
body = option_prompt(current=facts)
logger.info(
"land.options_missing missing=%s",
(state.get("opt") or {}).get("missing") or [],
)
reply(body)
self._reply_land_options(reply, facts)
self._save(
FlowSession(
sender_id=sender_id,
@@ -574,6 +596,32 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
clear_pending_land_confirm(sender_id)
def _pull_collab_group(
self,
sess: FlowSession,
reply: ReplyFn,
card_meta: Optional[dict[str, Any]] = None,
) -> str:
"""
点「拉产品进群协同」:先核填写样例,再按线路拉人建群。
类型+线路对不上样例:不建群,告诉销售重新开单。按钮保持可点。
对得上则走海运同一套建群。
"""
facts = dict(sess.facts or {})
land_type = str(facts.get("运输类型") or "").strip()
route = str(facts.get("线路类别") or "").strip()
if not sample_pair_allowed(land_type, route):
logger.info(
"land.group.reject pair type=%s route=%s wo=%s",
land_type or "-",
route or "-",
sess.work_order_no or "-",
)
reply(copy.land_group_pair_rejected())
return sess.phase or "wait_collab"
return super()._pull_collab_group(sess, reply, card_meta=card_meta)
def _match_sea_staff(self, sess: FlowSession) -> list[dict[str, Any]]:
"""按本单线路类别整词匹配产品·陆运,禁止全员。"""
matcher = getattr(self._ledger, "match_land_staff", None)
@@ -24,6 +24,10 @@ _COLLAB_LABEL_MARKERS: tuple[tuple[str, tuple[str, ...]], ...] = (
("是否含油", ("是否含油", "has_oil")),
("是否含电", ("是否含电", "has_battery")),
("是否含磁", ("是否含磁", "has_magnet")),
("客户名称", ("客户名称", "customer_name")),
("货值", ("货值", "cargo_value")),
("是否为危险品", ("是否为危险品", "is_dangerous")),
("包装方式", ("包装方式", "包装类型")),
)
+87 -18
View File
@@ -14,7 +14,12 @@ from typing import Any, Optional
from agent.llm.extract_text import extract_collab_fields, extract_product_quote
from agent.policy.quote_overlay import overlay_labeled_facts
from agent.policy import inquiry_copy as copy
from agent.schema.field_validate import SEA_COLLAB_DISPLAY, SEA_COLLAB_FIELDS
from agent.schema.field_validate import (
SEA_COLLAB_DISPLAY,
SEA_COLLAB_FIELDS,
collab_display_name,
collab_fields_for_ticket,
)
# 主账填表按英文码取格子;中文键必须同时带上,否则货好时间/条款写不进表。
_COLLAB_FILL_ALIASES = {
@@ -24,6 +29,10 @@ _COLLAB_FILL_ALIASES = {
"是否含油": ("是否含油", "has_oil"),
"是否含电": ("是否含电", "has_battery"),
"是否含磁": ("是否含磁", "has_magnet"),
"客户名称": ("客户名称", "customer_name"),
"货值": ("货值", "cargo_value"),
"是否为危险品": ("是否为危险品", "is_dangerous"),
"包装方式": ("包装方式", "包装类型"),
}
logger = logging.getLogger(__name__)
@@ -91,6 +100,24 @@ def ticket_no(ticket: object) -> str:
return str(ticket_view(ticket).get("work_order_no") or "").strip()
def _collab_keys_of(ticket: object, sess: Any = None) -> tuple[str, tuple[str, ...]]:
"""本单业务线 + 允许写入的协同内部键。"""
view = ticket_view(ticket)
facts = dict(view.get("facts") or {})
if sess is not None and getattr(sess, "facts", None):
facts.update(dict(sess.facts or {}))
line = str(
view.get("business_line")
or view.get("businessLine")
or (getattr(sess, "business_line", "") if sess is not None else "")
or "SEA"
).strip().upper() or "SEA"
keys = collab_fields_for_ticket(facts, line)
if not keys and line != "LAND":
keys = SEA_COLLAB_FIELDS
return line, keys
def is_terminal(ticket: object) -> bool:
return ticket_status(ticket) in TERMINAL
@@ -378,6 +405,13 @@ def send_brief_and_sheet(
quote = dict(override or sess_q or view_q or tms or manual)
sales_name, products, mention_ids = resolve_group_mentions(flow, ticket, sess)
has_tms = bool(tms.get("total") or _quote_fee_rows(tms))
line = str(
view.get("business_line")
or view.get("businessLine")
or (sess.business_line if sess is not None else "")
or "SEA"
).strip().upper()
mode = "陆运" if line == "LAND" else "海运"
brief = copy.sea_group_brief(
work_order_no=no,
facts=facts,
@@ -388,9 +422,16 @@ def send_brief_and_sheet(
has_price=has_tms,
tms_quote=tms,
product_quote=manual,
transport_mode=mode,
)
send_group_text(client, chat_id, brief, mention_userids=mention_ids)
send_filled_sheet(flow=flow, ticket=ticket, sess=sess, quote_override=quote)
send_filled_sheet(
flow=flow,
ticket=ticket,
sess=sess,
quote_override=quote,
business_line=line,
)
return "brief_and_sheet"
@@ -400,7 +441,7 @@ def send_filled_sheet(
ticket: object,
sess: Any = None,
quote_override: Optional[dict[str, Any]] = None,
business_line: str = "SEA",
business_line: str = "",
chat_id: str = "",
) -> dict[str, Any]:
"""
@@ -429,7 +470,7 @@ def send_filled_sheet(
flow=flow,
chat_id=room,
work_order_no=no,
facts=_facts_for_fill(facts, collab),
facts=_facts_for_fill(facts, collab, business_line=line or "SEA"),
quote=quote,
sales_id=str(
(sess.sender_id if sess else "")
@@ -441,15 +482,20 @@ def send_filled_sheet(
)
def _collab_value(collab: dict[str, str], key: str) -> str:
"""协同六项:内部键、对外展示名都能取到。"""
def _collab_value(collab: dict[str, str], key: str, business_line: str = "SEA") -> str:
"""协同项:内部键、对外展示名都能取到。"""
src = dict(collab or {})
val = str(src.get(key) or "").strip()
if val:
return val
label = SEA_COLLAB_DISPLAY.get(key, "")
label = collab_display_name(key, business_line)
if label:
val = str(src.get(label) or "").strip()
if val:
return val
sea_label = SEA_COLLAB_DISPLAY.get(key, "")
if sea_label:
val = str(src.get(sea_label) or "").strip()
return val
@@ -466,15 +512,22 @@ def _oil_elec_mag_text(collab: dict[str, str]) -> str:
return f"油:{oil or '-'};电:{elec or '-'};磁:{mag or '-'}"
def _facts_for_fill(facts: dict[str, str], collab: dict[str, str]) -> dict[str, str]:
def _facts_for_fill(
facts: dict[str, str],
collab: dict[str, str],
business_line: str = "SEA",
) -> dict[str, str]:
"""
询价七项 + 协同六项。中文键和填表英文码一起带上。
询价字段 + 本单协同项。中文键和填表英文码一起带上。
主账 writeFactsByLabel 只认 cargo_ready_date / trade_terms,只传「货好时间」会空着。
模板没有格子的新项照样带上,overlay 找不到标签就跳过。
"""
merged = dict(facts or {})
src = dict(collab or {})
for key in SEA_COLLAB_FIELDS:
val = _collab_value(src, key)
line = (business_line or "SEA").strip().upper() or "SEA"
keys = collab_fields_for_ticket(facts, line) or SEA_COLLAB_FIELDS
for key in keys:
val = _collab_value(src, key, line)
if not val:
continue
for alias in _COLLAB_FILL_ALIASES.get(key, (key,)):
@@ -549,7 +602,7 @@ def apply_collab_update(
sender_id: str,
sess: Any = None,
) -> str:
"""补协同六项。终态要说明原因。"""
"""补本单协同项。终态要说明原因。不该有的项当没补到。"""
ticket = hydrate_ticket(flow, ticket)
no = ticket_no(ticket)
chat_id = str((sess.collab_chat_id if sess else "") or ticket_view(ticket).get("collab_chat_id") or "")
@@ -557,11 +610,21 @@ def apply_collab_update(
if is_terminal(ticket):
send_group_text(client, chat_id, copy.ticket_locked(work_order_no=no, status=ticket_status(ticket)))
return "ticket_locked"
fields = extract_collab_fields(text, allow_b=True)
line, keys = _collab_keys_of(ticket, sess)
fields = extract_collab_fields(text, allow_b=True, allowed_keys=keys)
if not fields:
send_group_text(client, chat_id, copy.group_fields_not_recognized())
send_group_text(
client,
chat_id,
copy.group_fields_not_recognized(keys=keys or None, business_line=line),
)
return "collab_not_recognized"
flow.apply_collab_facts(work_order_no=no, facts=fields, sender_id=sender_id)
flow.apply_collab_facts(
work_order_no=no,
facts=fields,
sender_id=sender_id,
allowed_keys=keys,
)
if sess is not None:
merged = dict(sess.collab_facts)
merged.update(fields)
@@ -603,9 +666,15 @@ def apply_product_quote(
if not is_product(ticket, sender_id, flow=flow):
send_group_text(client, chat_id, copy.group_only_product_can_quote())
return "quote_not_product"
fields = extract_collab_fields(text)
fields = extract_collab_fields(text, allowed_keys=_collab_keys_of(ticket, sess)[1])
if fields:
flow.apply_collab_facts(work_order_no=no, facts=fields, sender_id=sender_id)
line, keys = _collab_keys_of(ticket, sess)
flow.apply_collab_facts(
work_order_no=no,
facts=fields,
sender_id=sender_id,
allowed_keys=keys,
)
if sess is not None:
merged = dict(sess.collab_facts)
merged.update(fields)
@@ -772,7 +841,7 @@ def _persist_quote_sheet(
result = renderer(
work_order_no=no,
business_line=line,
facts=_facts_for_fill(facts, collab),
facts=_facts_for_fill(facts, collab, business_line=line or "SEA"),
quote=quote,
format="xlsx",
sales_wecom_id=str(
+21 -1
View File
@@ -514,6 +514,24 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
product_ids=product_ids,
)
from agent.schema.field_validate import collab_fields_for_ticket, pick_collab_from_facts
line = (sess.business_line or "SEA").upper()
keys = collab_fields_for_ticket(sess.facts, line)
seeded = pick_collab_from_facts(sess.facts, keys)
if seeded:
merged = dict(sess.collab_facts or {})
for k, v in seeded.items():
if v and not str(merged.get(k) or "").strip():
merged[k] = v
sess.collab_facts = merged
self.apply_collab_facts(
work_order_no=no,
facts=seeded,
sender_id=sess.sender_id,
allowed_keys=keys,
)
brief = copy.sea_group_brief(
work_order_no=no,
facts=sess.facts,
@@ -638,6 +656,7 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
work_order_no: str,
facts: dict[str, str],
sender_id: str,
allowed_keys: tuple[str, ...] | None = None,
) -> dict[str, Any]:
"""
群里静默写入协同字段。不改六态、不回复、不出报价版本。
@@ -647,10 +666,11 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
from agent.schema.field_validate import SEA_COLLAB_FIELDS
no = (work_order_no or "").strip()
keys = tuple(allowed_keys) if allowed_keys is not None else SEA_COLLAB_FIELDS
cleaned = {
k: str(v).strip()
for k, v in (facts or {}).items()
if k in SEA_COLLAB_FIELDS and str(v or "").strip()
if k in keys and str(v or "").strip()
}
if not no or not cleaned:
return {"ok": False, "updated": []}