Files
codex-app-server/cloud/goal_command.py
T
host f1aab5415c 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。
2026-10-30 07:15:05 +08:00

108 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""主线 /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