feat: 不用斜杠,单发 goal 也触发 /goal 目标

触发词从仅 /goal 放宽到 /goal 或裸 goal(大小写不敏感,
单数 only,goals/mygoal 不算):goal 收xxx、goal,目标是
收xxx、发goal 收xxx 均同款触发,新建与主线转换入口共用
同一解析。讨论句式仍不触发:goal 的用法、goal 是什么、
讨论goal的用法、/goal的用法 均返回 None。
单测补 4 项:裸 goal 触发、裸 goal 讨论不触发、followup
裸 goal 转换、新建裸 goal。
This commit is contained in:
host committed 2026-10-30 07:15:05 +08:00
1 parent f3bd65c171
commit f1aab5415c
3 files changed
+93 -32

No files matched your search

+57 -29
View File
@@ -7,15 +7,19 @@
触发转换,调用方不用再知道任务当前是进行中还是已终态。
解析规则(刻意收紧,防误触发):
- 文本里必须出现字面 "/goal"(全角 / 也认,大小写不敏感)。
- 触发词是 "/goal" 或裸 "goal"(斜杠不用,全角 / 也认,
大小写不敏感,单数 only——"goals" 不算)。
- 光出现还不够,必须是命令形状,二者居一:
1. 文本以 /goal 开头(最显式的命令);
2. /goal 后面紧跟分隔符(空格、,、:、、)或“目标”标记,
例如“/goal 收xxx”“/goal,目标是收xxx”;
3. /goal 前面有开启类动词(开启/建立/创建/添加/设为/设置/
开一个),例如“请开启一个持久/goal收xxx”。
只是在讨论里提到“/goal 的用法”这种不算命令。
- 目标取 /goal 之后的部分:先去分隔符,再去“目标是/目标为/
1. 文本以触发词开头(最显式的命令),例如“goal 收xxx”;
2. 触发词后面紧跟分隔符(空格、,、:、、)或“目标”标记,
例如“/goal 收xxx”“goal,目标是收xxx”;
3. 触发词前面有开启/发送类动词(开启/建立/创建/添加/设为/
设置/开一个/发/发送),例如“请开启一个持久/goal收xxx”、
“发goal 收xxx”。
只是在讨论里提到“/goal 的用法”“goal 的用法”“goal 是什么”
这种不算命令:触发词后面紧跟“的/是/吗/什么/怎么/如何/
模式/用法/意思/功能”时一律不当命令。
- 目标取触发词之后的部分:先去分隔符,再去“目标是/目标为/
目标:/目标:”或“持久目标是/为/:/:”这类前缀,最后去尾句号。
- 返回值:不是命令返回 None;是命令但没给目标返回 ""(调用方
据此回 400“目标不能为空”,而不是当成普通消息悄悄排队);
@@ -28,12 +32,17 @@
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 = ("开启", "建立", "创建", "添加", "设为", "设置", "开一个")
_OPEN_VERBS = ("开启", "建立", "创建", "添加", "设为", "设置", "开一个",
"发", "发送")
# 触发词后紧跟这些词头时是在讨论 goal 本身,不是下命令。
_DISCUSSION_PREFIXES = ("的", "是", "吗", "什么", "怎么", "如何",
"模式", "用法", "意思", "功能")
def _strip_target(rest):
@@ -49,6 +58,20 @@ def _strip_target(rest):
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):
@@ -56,24 +79,29 @@ def parse_goal_command(text):
s = text.strip().replace("/", "/")
if not s:
return None
# 先认带斜杠的:是命令直接返回;不是命令(如讨论里提到)
# 也不急着否掉,后面裸 goal 的位置还可能是一条真命令。
match = _GOAL_RE.search(s)
if match is None:
return None
before = s[:match.start()]
rest = s[match.end():]
stripped = s.lstrip()
is_command = False
if stripped.lower().startswith("/goal"):
is_command = True
elif rest and rest[0] in _SEPARATORS:
is_command = True
elif rest.startswith("目标"):
is_command = True
elif any(verb in before for verb in _OPEN_VERBS):
is_command = True
elif not rest:
# 单独一句“/goal”:是命令,但没目标,调用方回 400。
is_command = True
if not is_command:
return None
return _strip_target(rest)
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