角色业务范围收紧工单与模板;报价日期不当费用;未选舱不迁已报价;另开一票可清违禁 hold
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -320,7 +320,10 @@ def send_air_brief_and_sheet(
|
||||
必须先发出工单信息,模板等摘要发出后再发,禁止后台线程抢先发文件。
|
||||
填表报价必须带上 TMS 总销售价:群票上的 latest quote 常只有增点费/拆板费明细,
|
||||
若直接拿去出单,预估费用会落成明细加总(如 200)而不是 salePrice(如 7918.25)。
|
||||
调用方刚写回的询价字段(如报价日期)优先,不能被 hydrate 旧快照盖掉。
|
||||
"""
|
||||
incoming_view = ticket_view(ticket) if ticket is not None else {}
|
||||
incoming_facts = dict(incoming_view.get("facts") or {})
|
||||
ticket = hydrate_ticket(flow, ticket)
|
||||
view = ticket_view(ticket)
|
||||
no = ticket_no(ticket)
|
||||
@@ -328,6 +331,12 @@ 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 {})
|
||||
# 群里刚补的报价日期等:有值就盖到摘要用的 facts 上。
|
||||
for key, raw in incoming_facts.items():
|
||||
val = str(raw or "").strip()
|
||||
if key and val:
|
||||
facts[key] = val
|
||||
view["facts"] = facts
|
||||
collab = _seed_air_activate_collab(flow, view)
|
||||
view["collab_facts"] = collab
|
||||
view_q = dict(view.get("quote") or {})
|
||||
@@ -617,11 +626,19 @@ def resolve_cabin_ticket(
|
||||
|
||||
def apply_air_fields(*, flow: Any, ticket: object, text: str, sender_id: str, chat_id: str) -> str:
|
||||
"""
|
||||
补含电/含磁。对齐海运:写入后重发群摘要 + 空运模板,不当一句「已记下」。
|
||||
补含电/含磁,以及报价日期。
|
||||
|
||||
含电含磁走协同字段;报价日期写入询价 facts(主账 quote_date 列)。
|
||||
货好时间、贸易条款、客户名称不在群里补(产品口径)。
|
||||
写入后重发群摘要 + 空运模板,不当一句「已记下」。
|
||||
"""
|
||||
from agent.llm.extract_text import labeled_quote_date
|
||||
|
||||
ticket = hydrate_ticket(flow, ticket)
|
||||
no = ticket_no(ticket)
|
||||
client = group_client_of(flow)
|
||||
fields = extract_air_collab_fields(text, allow_b=True)
|
||||
quote_date = labeled_quote_date(text)
|
||||
if not is_air_ticket(ticket):
|
||||
send_group_text(client, chat_id, copy.air_need_work_order())
|
||||
return "need_work_order"
|
||||
@@ -631,11 +648,23 @@ def apply_air_fields(*, flow: Any, ticket: object, text: str, sender_id: str, ch
|
||||
if not can_fill_fields(flow, ticket, sender_id):
|
||||
send_group_text(client, chat_id, copy.air_fields_denied())
|
||||
return "fields_denied"
|
||||
fields = extract_air_collab_fields(text, allow_b=True)
|
||||
if not fields:
|
||||
if not fields and not quote_date:
|
||||
send_group_text(client, chat_id, copy.air_fields_not_recognized())
|
||||
return "fields_empty"
|
||||
flow.ledger.patch_collab_facts(work_order_no=no, facts=fields, sender_id=sender_id)
|
||||
if fields:
|
||||
flow.ledger.patch_collab_facts(work_order_no=no, facts=fields, sender_id=sender_id)
|
||||
write_ok = True
|
||||
if quote_date:
|
||||
# 只打补丁键:主账侧合并,避免整包 facts 误伤。
|
||||
patch = {"报价日期": quote_date}
|
||||
writer = getattr(flow.ledger, "update_facts", None)
|
||||
if callable(writer):
|
||||
try:
|
||||
out = writer(work_order_no=no, facts=patch) or {}
|
||||
write_ok = bool(out.get("ok", True))
|
||||
except Exception:
|
||||
write_ok = False
|
||||
logger.exception("空运群写报价日期失败 wo=%s", no)
|
||||
# 必须回读本单。按群再查会拿到同群更早的陆运/海运旧单(测服 WO202609230007)。
|
||||
getter = getattr(flow.ledger, "get_ticket", None)
|
||||
reloaded = getter(work_order_no=no) if callable(getter) else None
|
||||
@@ -644,6 +673,10 @@ def apply_air_fields(*, flow: Any, ticket: object, text: str, sender_id: str, ch
|
||||
collab = dict(view.get("collab_facts") or {})
|
||||
collab.update(fields)
|
||||
view["collab_facts"] = collab
|
||||
if quote_date:
|
||||
facts_view = dict(view.get("facts") or {})
|
||||
facts_view["报价日期"] = quote_date
|
||||
view["facts"] = facts_view
|
||||
view["collab_chat_id"] = chat_id
|
||||
send_air_brief_and_sheet(flow=flow, ticket=view, chat_id=chat_id)
|
||||
return "fields_saved"
|
||||
@@ -1012,8 +1045,9 @@ def apply_air_lock(
|
||||
航线锁舱。只有工单已是「已成交」才打主账。
|
||||
|
||||
还没回复成交:点名销售先确认。协商中、未成交各用一句,不催销售改口。
|
||||
舱位编号、航司、航班号没齐时只请补全,不调 TMS 锁舱,不标系统异常。
|
||||
三项齐了(话里写的优先,报价里已有的可以补上)才打主账锁舱。
|
||||
话里写的优先,报价里已有的舱位可以补上;补不上也要打主账——
|
||||
主账会合并历史 TMS / 现拉运价库(产品报价无舱位时不能在智能体侧先挡住)。
|
||||
主账回 lock_params_incomplete 再群里补问,不标系统异常。
|
||||
上次锁舱失败标过暂停的,航线再锁仍要打 TMS。锁成功不再推成交跟进。
|
||||
"""
|
||||
if not is_airline(flow, sender_id):
|
||||
|
||||
@@ -32,6 +32,20 @@ logger = logging.getLogger(__name__)
|
||||
ReplyFn = Callable[..., Optional[str]]
|
||||
|
||||
|
||||
def _ledger_quote_pending_cabin(quote: dict[str, Any]) -> dict[str, Any]:
|
||||
"""
|
||||
未选舱前写主账用的薄快照:保留 airOptions 供恢复,清空总价/费用明细,
|
||||
并打 pending_cabin_pick,避免后台把备选舱位当成已报价。
|
||||
"""
|
||||
out = dict(quote or {})
|
||||
out["pending_cabin_pick"] = "1"
|
||||
out["fee_lines"] = []
|
||||
out["total"] = ""
|
||||
out.pop("selected_air_option", None)
|
||||
out.pop("selectedAirOption", None)
|
||||
return out
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlowSession:
|
||||
"""一单会话书签(运行账,不是六态真相)。"""
|
||||
@@ -334,10 +348,11 @@ class AirTextInquiryFlow:
|
||||
number = (sess.work_order_no or "").strip()
|
||||
if number and not is_virtual_no(number):
|
||||
return None
|
||||
hold_before = str(getattr(sess, "sensitive_hold", "") or "") if sess else ""
|
||||
blocked, new_hold, _edits = judge(
|
||||
text=text,
|
||||
facts=facts,
|
||||
hold=str(getattr(sess, "sensitive_hold", "") or "") if sess else "",
|
||||
hold=hold_before,
|
||||
)
|
||||
if sess is not None:
|
||||
sess.sensitive_hold = new_hold
|
||||
@@ -1379,19 +1394,23 @@ class AirTextInquiryFlow:
|
||||
|
||||
options = parse_air_options(quote)
|
||||
# 没填报价日期:不按起运地收口,TMS 返回的舱位全部给销售点。1 条也要点。
|
||||
# 未选定前六态保持询价中,禁止把备选舱位费写进后台「已报价」。
|
||||
if not str((sess.facts or {}).get("报价日期") or "").strip() and options:
|
||||
quote = dict(quote)
|
||||
quote["airOptions"] = sort_cabin_options(options)
|
||||
up = self._ledger.upsert_quote(work_order_no=no, quote=quote, to_status="已报价")
|
||||
ledger_quote = _ledger_quote_pending_cabin(quote)
|
||||
up = self._ledger.upsert_quote(
|
||||
work_order_no=no, quote=ledger_quote, to_status="询价中"
|
||||
)
|
||||
if not up.get("ok"):
|
||||
reply("主账写入报价失败,不能当作已报价。")
|
||||
reply("主账写入舱位备选失败,请稍后重试。")
|
||||
sess.phase = "ledger_fail"
|
||||
self._save(sess)
|
||||
return "ledger_fail"
|
||||
sess.quote = quote
|
||||
copy.remember_tms_quote(sess, quote)
|
||||
sess.tms_quote = dict(quote)
|
||||
sess.status = "已报价"
|
||||
sess.status = "询价中"
|
||||
return self._offer_air_routes(
|
||||
sess=sess,
|
||||
work_order_no=no,
|
||||
@@ -1402,6 +1421,9 @@ class AirTextInquiryFlow:
|
||||
stated_origin = str(
|
||||
(sess.facts or {}).get("起运港") or (sess.facts or {}).get("起运地") or ""
|
||||
).strip()
|
||||
# 摘要占位「-」等于没写起运地,禁止当地名过滤舱位。
|
||||
if stated_origin in {"-", "—", "-"}:
|
||||
stated_origin = ""
|
||||
# 写了起运地:只认这个起运地的线路,直接报价,不让销售再选。
|
||||
# 没写起运地:TMS 按目的地回多条时,才进选线页。
|
||||
if stated_origin:
|
||||
@@ -1425,6 +1447,27 @@ class AirTextInquiryFlow:
|
||||
)
|
||||
elif options and not needs_route_pick(quote):
|
||||
quote = project_selected_quote(quote, options[0])
|
||||
# 仍要选线:未选定前只落备选快照,六态保持询价中。
|
||||
if not stated_origin and needs_route_pick(quote):
|
||||
ledger_quote = _ledger_quote_pending_cabin(quote)
|
||||
up = self._ledger.upsert_quote(
|
||||
work_order_no=no, quote=ledger_quote, to_status="询价中"
|
||||
)
|
||||
if not up.get("ok"):
|
||||
reply("主账写入线路备选失败,请稍后重试。")
|
||||
sess.phase = "ledger_fail"
|
||||
self._save(sess)
|
||||
return "ledger_fail"
|
||||
sess.quote = quote
|
||||
copy.remember_tms_quote(sess, quote)
|
||||
sess.tms_quote = dict(quote)
|
||||
sess.status = "询价中"
|
||||
return self._offer_air_routes(
|
||||
sess=sess,
|
||||
work_order_no=no,
|
||||
quote=quote,
|
||||
reply=reply,
|
||||
)
|
||||
up = self._ledger.upsert_quote(work_order_no=no, quote=quote, to_status="已报价")
|
||||
if not up.get("ok"):
|
||||
reply("主账写入报价失败,不能当作已报价。")
|
||||
@@ -1435,13 +1478,6 @@ class AirTextInquiryFlow:
|
||||
copy.remember_tms_quote(sess, quote)
|
||||
sess.tms_quote = dict(quote)
|
||||
sess.status = "已报价"
|
||||
if not stated_origin and needs_route_pick(quote):
|
||||
return self._offer_air_routes(
|
||||
sess=sess,
|
||||
work_order_no=no,
|
||||
quote=quote,
|
||||
reply=reply,
|
||||
)
|
||||
sess.phase = "wait_collab"
|
||||
sess.allowed = (copy.BTN_SKIP_COLLAB,)
|
||||
sess.wait_version += 1
|
||||
|
||||
@@ -650,6 +650,42 @@ def ask_clarify(
|
||||
if key == "起运港":
|
||||
continue
|
||||
val = known(src, key)
|
||||
# #region agent log
|
||||
if key == "added_services":
|
||||
try:
|
||||
import json as _dbg_json
|
||||
import time as _dbg_time
|
||||
from pathlib import Path as _DbgPath
|
||||
|
||||
_payload = {
|
||||
"sessionId": "5ea5b3",
|
||||
"runId": "post-fix",
|
||||
"hypothesisId": "H3",
|
||||
"location": "inquiry_copy.py:ask_clarify",
|
||||
"message": "clarify display added_services",
|
||||
"data": {
|
||||
"raw_facts": str(src.get("added_services") or ""),
|
||||
"display_val": val,
|
||||
"label": label,
|
||||
},
|
||||
"timestamp": int(_dbg_time.time() * 1000),
|
||||
}
|
||||
for _p in (
|
||||
_DbgPath(__file__).resolve().parents[3] / "debug-5ea5b3.log",
|
||||
_DbgPath(r"d:/own/友通达询价智能体(新)/debug-5ea5b3.log"),
|
||||
_DbgPath(r"C:/Users/ytd_extdev_0903/incoming/debug-5ea5b3.log"),
|
||||
):
|
||||
try:
|
||||
_p.parent.mkdir(parents=True, exist_ok=True)
|
||||
with _p.open("a", encoding="utf-8") as _f:
|
||||
_f.write(
|
||||
_dbg_json.dumps(_payload, ensure_ascii=False) + "\n"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
# #endregion
|
||||
if val:
|
||||
lines.append(f"{label}:{val}")
|
||||
lines.append("")
|
||||
@@ -818,6 +854,30 @@ def inquiry_field_lines(
|
||||
]
|
||||
# 建单确认和空运群:有内容才多一行。没填不出现。核对卡的空行不在这里。
|
||||
added = (facts.get("added_services") or facts.get("增值服务") or "").strip()
|
||||
# #region agent log
|
||||
try:
|
||||
import json as _dbg_json
|
||||
import time as _dbg_time
|
||||
from pathlib import Path as _DbgPath
|
||||
|
||||
_payload = {
|
||||
"sessionId": "5ea5b3",
|
||||
"runId": "pre-fix",
|
||||
"hypothesisId": "H3",
|
||||
"location": "inquiry_copy.py:air_confirm_lines",
|
||||
"message": "display added_services value",
|
||||
"data": {
|
||||
"added": added,
|
||||
"facts_keys": sorted(str(k) for k in (facts or {}).keys()),
|
||||
},
|
||||
"timestamp": int(_dbg_time.time() * 1000),
|
||||
}
|
||||
_path = _DbgPath(__file__).resolve().parents[3] / "debug-5ea5b3.log"
|
||||
with _path.open("a", encoding="utf-8") as _f:
|
||||
_f.write(_dbg_json.dumps(_payload, ensure_ascii=False) + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
# #endregion
|
||||
if added and added not in {"-", "—", "-"}:
|
||||
lines.insert(-1, f"增值服务:{added}")
|
||||
return lines
|
||||
@@ -3140,7 +3200,7 @@ def air_sales_only_deal() -> str:
|
||||
|
||||
|
||||
def air_fields_not_recognized() -> str:
|
||||
return "听到了。空运群里可补充是否含电、是否含磁。"
|
||||
return "听到了。空运群里可补充是否含电、是否含磁,或报价日期。"
|
||||
|
||||
|
||||
def air_quote_not_recognized() -> str:
|
||||
@@ -3148,7 +3208,7 @@ def air_quote_not_recognized() -> str:
|
||||
|
||||
|
||||
def air_fields_denied() -> str:
|
||||
return "只有这张单的销售或航线可以补含电/含磁。"
|
||||
return "只有这张单的销售或航线可以补含电/含磁或报价日期。"
|
||||
|
||||
|
||||
def air_no_tms_quote_to_lock(work_order_no: str) -> str:
|
||||
@@ -3346,9 +3406,10 @@ def air_text_quote_for_lock_example(work_order_no: str = "") -> str:
|
||||
需要锁舱时,请航线用纯文字报价的示例句。
|
||||
|
||||
附件报价不拆金额、常锁不成;产品口径改为引导航线改发文字价。
|
||||
示例必须带计费单位(空运常见 KG),避免航线漏写单位导致文字价无法落库。
|
||||
"""
|
||||
no = (work_order_no or "").strip() or "工单号"
|
||||
return f"{no},空运段,空运费 单价CNY99.00,数量100 ,金额CNY9900.00@询价小助手"
|
||||
return f"{no},空运段,KG,空运费 单价CNY99.00,数量100 ,金额CNY9900.00@询价小助手"
|
||||
|
||||
|
||||
def air_attachment_quote_lock_tip(*, work_order_no: str) -> str:
|
||||
|
||||
@@ -138,6 +138,39 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
sess.work_order_no or "-",
|
||||
)
|
||||
sess = None
|
||||
# #region agent log
|
||||
try:
|
||||
import json as _json
|
||||
import time as _time
|
||||
from pathlib import Path as _Path
|
||||
|
||||
with _Path(r"d:\own\友通达询价智能体(新)\debug-d331ea.log").open("a", encoding="utf-8") as _dbg:
|
||||
_dbg.write(
|
||||
_json.dumps(
|
||||
{
|
||||
"sessionId": "d331ea",
|
||||
"runId": "post-fix",
|
||||
"hypothesisId": "D",
|
||||
"location": "land_text_flow.py:on_text:sess_keep",
|
||||
"message": "land sess keep-or-clear",
|
||||
"data": {
|
||||
"phase_in": phase,
|
||||
"kept_sess": sess is not None,
|
||||
"in_front": phase in _LAND_FRONT,
|
||||
"sensitive_hold": (
|
||||
str(getattr(sess, "sensitive_hold", "") or "")[:120] if sess else ""
|
||||
),
|
||||
"text": (text or "")[:120],
|
||||
},
|
||||
"timestamp": int(_time.time() * 1000),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
# #endregion
|
||||
|
||||
snap = extract_inquiry_snapshot(
|
||||
text,
|
||||
|
||||
@@ -931,12 +931,13 @@ def apply_product_quote(
|
||||
sess.collab_facts = merged
|
||||
line = str(view.get("business_line") or view.get("businessLine") or "").strip().upper()
|
||||
base = _current_quote(view, sess)
|
||||
baseline_ccy = _quote_currency_hint(base)
|
||||
quote = extract_product_quote(
|
||||
text,
|
||||
injected=injected_quote,
|
||||
business_line=line,
|
||||
transport_mode="陆运" if line == "LAND" else ("海运" if line == "SEA" else ""),
|
||||
baseline_currency=_quote_currency_hint(base),
|
||||
baseline_currency=baseline_ccy,
|
||||
)
|
||||
if not quote:
|
||||
logger.info("产品报价未识别 wo=%s text=%s", no, (text or "")[:200])
|
||||
@@ -1644,9 +1645,13 @@ def _merged_total(
|
||||
总销售价:有 TMS 原合计就按改价差额加减,不能只加产品改的那几行。
|
||||
|
||||
美元行已含在 TMS 人民币合计里,不擅自换汇;只调整和合计同币种的差额。
|
||||
产品正文已点币种(如 RMB→CNY)时,表头跟产品,禁止 TMS HKD 再盖回去(WO040)。
|
||||
"""
|
||||
src = dict(base or {})
|
||||
header_ccy = str(src.get("currency") or incoming.get("currency") or "").strip()
|
||||
# 产品显式币种优先:否则「1RMB/KG」合并后表头仍停在 TMS HKD。
|
||||
header_ccy = str(incoming.get("currency") or src.get("currency") or "").strip()
|
||||
if header_ccy.upper() == "RMB":
|
||||
header_ccy = "CNY"
|
||||
base_total = _parse_fee_amount(src.get("total"))
|
||||
if base_total is not None:
|
||||
total_ccy = (header_ccy or "CNY").upper()
|
||||
|
||||
@@ -103,6 +103,41 @@ _WRITE_KEYS = {
|
||||
# 建单前要看的当前字段。没写进这几项、但原话里出现过的,靠 sensitive_hold 记住。
|
||||
_SCAN_KEYS = ("品名", "货物品名", "起运港", "起运地", "始发站", "备注")
|
||||
|
||||
# 新开一票的说法。命中且本句没有违禁词时,丢掉上一句没改口的 hold。
|
||||
_FRESH_INQUIRY = re.compile(
|
||||
r"(查一下|帮我查|询一下|询个价|询价|请报价|报一下价|报个价|费用)"
|
||||
)
|
||||
# 只补量尺、件数、包装:不算新询价,旧 hold 继续有效。
|
||||
_MEASURE_SUPPLEMENT = re.compile(
|
||||
r"(?:"
|
||||
r"重量|毛重|体积|件数|货量|包装(?:方式|类型)?"
|
||||
r")\s*[::]?\s*\d"
|
||||
r"|\d+\s*(?:kgs?|kg|公斤|千克|吨|t|cbm|方|件|箱|托)",
|
||||
re.I,
|
||||
)
|
||||
|
||||
|
||||
def looks_like_fresh_inquiry(text: str) -> bool:
|
||||
"""
|
||||
当前句是不是另开一票,而不是接着补上一票的字段。
|
||||
|
||||
「查一下目的地吉隆坡的费用」算新开;「重量100KGS」不算。
|
||||
用来在本句已无违禁词时清掉 sticky hold,避免正常询价被上一句炸弹拖死。
|
||||
"""
|
||||
body = (text or "").strip()
|
||||
if not body:
|
||||
return False
|
||||
if _MEASURE_SUPPLEMENT.search(body) and not _FRESH_INQUIRY.search(body):
|
||||
return False
|
||||
return bool(_FRESH_INQUIRY.search(body))
|
||||
|
||||
|
||||
def scrub_blocked_scan_fields(facts: dict[str, Any]) -> None:
|
||||
"""新开一票时,把品名/起运/备注里仍带着违禁词的旧值清掉,避免 field_hit 再拦。"""
|
||||
for key in _SCAN_KEYS:
|
||||
if contains_blocked(str((facts or {}).get(key) or "")):
|
||||
facts[key] = ""
|
||||
|
||||
|
||||
def contains_blocked(text: str) -> bool:
|
||||
"""
|
||||
@@ -203,6 +238,7 @@ def judge(*, text: str, facts: dict[str, Any], hold: str) -> tuple[bool, str]:
|
||||
当前句已经把品名说成普通货(运送衣服、改成衣服、换成衣服):前面的违禁原话作废。
|
||||
同一句里先提到旧词再改口,按改口后的品名放行。
|
||||
只补重量、件数:旧原话继续有效。
|
||||
本句无违禁词且像另开一票(查一下…费用):旧 hold 作废,并清掉字段里残留的违禁词。
|
||||
品名改干净之后,起运地或备注里还留着违禁词,仍然拦截。
|
||||
会就地更新 facts 里本句写明的品名、起运地、备注。
|
||||
第三个返回值是本句写下的键,多段用来写回每一段,避免把别的字段盖掉。
|
||||
@@ -210,14 +246,23 @@ def judge(*, text: str, facts: dict[str, Any], hold: str) -> tuple[bool, str]:
|
||||
edits = apply_field_edits(text, facts)
|
||||
replaced = bool(spoken_cargo(text)) or is_cargo_rename(text)
|
||||
message_hit = contains_blocked(text)
|
||||
fresh = False
|
||||
if replaced and not contains_blocked(str(facts.get("品名") or "")):
|
||||
# 已经说出新的普通货品名。旧词留在这句里只是在改口,不再记住。
|
||||
new_hold = ""
|
||||
message_hit = False
|
||||
elif message_hit:
|
||||
new_hold = (text or "").strip() or (hold or "")
|
||||
elif hold and looks_like_fresh_inquiry(text):
|
||||
# 本句没有违禁词,且是另开一票(查一下…费用),上一句违禁原话作废。
|
||||
# 只补重量/件数不走这里,旧 hold 继续拦。
|
||||
fresh = True
|
||||
new_hold = ""
|
||||
scrub_blocked_scan_fields(facts)
|
||||
else:
|
||||
new_hold = hold or ""
|
||||
field_text = "\n".join(str((facts or {}).get(key) or "") for key in _SCAN_KEYS)
|
||||
blocked = message_hit or contains_blocked(new_hold) or contains_blocked(field_text)
|
||||
hold_hit = contains_blocked(new_hold)
|
||||
field_hit = contains_blocked(field_text)
|
||||
blocked = message_hit or hold_hit or field_hit
|
||||
return blocked, new_hold, edits
|
||||
|
||||
Reference in New Issue
Block a user