触发词从仅 /goal 放宽到 /goal 或裸 goal(大小写不敏感, 单数 only,goals/mygoal 不算):goal 收xxx、goal,目标是 收xxx、发goal 收xxx 均同款触发,新建与主线转换入口共用 同一解析。讨论句式仍不触发:goal 的用法、goal 是什么、 讨论goal的用法、/goal的用法 均返回 None。 单测补 4 项:裸 goal 触发、裸 goal 讨论不触发、followup 裸 goal 转换、新建裸 goal。
108 lines
5.0 KiB
Python
108 lines
5.0 KiB
Python
"""主线 /goal 命令解析:发给主线会话的一句话里自动建 goal。
|
||
|
||
背景:此前主线不会因为收到“请在这条主线开启一个持久 /goal”
|
||
这样的自然语言就自己变成 goal 任务——goal 只在建任务时由
|
||
调用方用 goal_mode=1 指定,或在 /continue 时显式勾 goal 模式。
|
||
本模块把“/goal”当成显式命令标记,让发给主线的消息本身就能
|
||
触发转换,调用方不用再知道任务当前是进行中还是已终态。
|
||
|
||
解析规则(刻意收紧,防误触发):
|
||
- 触发词是 "/goal" 或裸 "goal"(斜杠不用,全角 / 也认,
|
||
大小写不敏感,单数 only——"goals" 不算)。
|
||
- 光出现还不够,必须是命令形状,二者居一:
|
||
1. 文本以触发词开头(最显式的命令),例如“goal 收xxx”;
|
||
2. 触发词后面紧跟分隔符(空格、,、:、、)或“目标”标记,
|
||
例如“/goal 收xxx”“goal,目标是收xxx”;
|
||
3. 触发词前面有开启/发送类动词(开启/建立/创建/添加/设为/
|
||
设置/开一个/发/发送),例如“请开启一个持久/goal收xxx”、
|
||
“发goal 收xxx”。
|
||
只是在讨论里提到“/goal 的用法”“goal 的用法”“goal 是什么”
|
||
这种不算命令:触发词后面紧跟“的/是/吗/什么/怎么/如何/
|
||
模式/用法/意思/功能”时一律不当命令。
|
||
- 目标取触发词之后的部分:先去分隔符,再去“目标是/目标为/
|
||
目标:/目标:”或“持久目标是/为/:/:”这类前缀,最后去尾句号。
|
||
- 返回值:不是命令返回 None;是命令但没给目标返回 ""(调用方
|
||
据此回 400“目标不能为空”,而不是当成普通消息悄悄排队);
|
||
否则返回目标文本。
|
||
|
||
本模块只做纯文本解析,不碰数据库、不发请求,server 与 worker
|
||
共用同一份解析,避免两端对“是不是 /goal 命令”判断不一致。
|
||
"""
|
||
|
||
import re
|
||
|
||
_GOAL_RE = re.compile(r"/goal(?![A-Za-z])", re.IGNORECASE)
|
||
_BARE_GOAL_RE = re.compile(r"(?<![A-Za-z])goal(?![A-Za-z])", re.IGNORECASE)
|
||
_SEPARATORS = " \t\r\n::,,、"
|
||
_TARGET_MARKERS = (
|
||
"持久目标是", "持久目标为", "持久目标:", "持久目标:",
|
||
"目标是", "目标为", "目标:", "目标:",
|
||
)
|
||
_OPEN_VERBS = ("开启", "建立", "创建", "添加", "设为", "设置", "开一个",
|
||
"发", "发送")
|
||
# 触发词后紧跟这些词头时是在讨论 goal 本身,不是下命令。
|
||
_DISCUSSION_PREFIXES = ("的", "是", "吗", "什么", "怎么", "如何",
|
||
"模式", "用法", "意思", "功能")
|
||
|
||
|
||
def _strip_target(rest):
|
||
"""去掉目标前的分隔符与“目标是”类前缀、尾句号。"""
|
||
rest = rest.lstrip(_SEPARATORS)
|
||
for marker in _TARGET_MARKERS:
|
||
if rest.startswith(marker):
|
||
rest = rest[len(marker):].lstrip(_SEPARATORS)
|
||
break
|
||
rest = rest.strip()
|
||
while rest.endswith("。"):
|
||
rest = rest[:-1].rstrip()
|
||
return rest
|
||
|
||
|
||
def _rest_is_command(rest):
|
||
"""触发词后的部分是不是命令形状(讨论词头除外)。"""
|
||
if not rest:
|
||
# 单独一句“/goal”/“goal”:是命令,但没目标,调用方回 400。
|
||
return True
|
||
if rest[0] in _SEPARATORS:
|
||
return True
|
||
if rest.startswith("目标"):
|
||
return True
|
||
if rest.startswith(_DISCUSSION_PREFIXES):
|
||
return False
|
||
return True
|
||
|
||
|
||
def parse_goal_command(text):
|
||
"""解析主线消息里的 /goal 命令,返回目标或 None/""(见模块说明)。"""
|
||
if not isinstance(text, str):
|
||
return None
|
||
s = text.strip().replace("/", "/")
|
||
if not s:
|
||
return None
|
||
# 先认带斜杠的:是命令直接返回;不是命令(如讨论里提到)
|
||
# 也不急着否掉,后面裸 goal 的位置还可能是一条真命令。
|
||
match = _GOAL_RE.search(s)
|
||
if match is not None:
|
||
before = s[:match.start()]
|
||
rest = s[match.end():]
|
||
# 斜杠本身已是显式标记,句首与否不再单算:命令与否
|
||
# 只看前面有没有动词、后面是不是讨论词头——这样
|
||
# “/goal的用法”也不会被误当成命令。
|
||
if (any(verb in before for verb in _OPEN_VERBS)
|
||
or _rest_is_command(rest)):
|
||
return _strip_target(rest)
|
||
# 再认不带斜杠的 goal:位置必须在句首,或前面是分隔符/
|
||
# 开启·发送类动词,否则“讨论goal的用法”这类也会误触发。
|
||
for bare in _BARE_GOAL_RE.finditer(s):
|
||
if bare.start() > 0 and s[bare.start() - 1] == "/":
|
||
continue # 斜杠那处已在上面判过
|
||
before = s[:bare.start()]
|
||
rest = s[bare.end():]
|
||
at_start = not before.strip()
|
||
before_ok = at_start or (
|
||
before.rstrip() and before.rstrip()[-1] in _SEPARATORS) or (
|
||
any(verb in before for verb in _OPEN_VERBS))
|
||
if before_ok and _rest_is_command(rest):
|
||
return _strip_target(rest)
|
||
return None
|