发给主线的一句话(含字面 /goal)即可自动建 goal,不用先 知道主线是进行中还是已终态,也不用勾选项: - cloud/goal_command.py:server 与 worker 共用的纯文本解析, 只认命令形状(/goal 开头、分隔符/目标标记、开启类动词), 讨论里提到 /goal 不误触发;空目标返回空串供调用方 400。 - server followup/thread/message/continue 三入口同款:queued 原地转 goal,已终态复活并转 goal,running 排队后由 worker 在轮次边界转 goal;已是 goal 409、plan 400、merged 409。 - worker 排空:/goal 不当普通轮次跑,CAS 把 running 主线 改回 queued 并写 goal/resume_message,下次认领走 goal-resume 在同一 thread 上开新目标循环;取消/回收时 CAS 失败则把 followup 放回 pending,绝不把取消过的任务改回 queued。 - 单测 14 项:解析、三态转换、拒绝口径、普通消息不受影响、 continue/thread 同款、worker 轮次边界转换 + 下次 goal 跑通。
80 lines
3.3 KiB
Python
80 lines
3.3 KiB
Python
"""主线 /goal 命令解析:发给主线会话的一句话里自动建 goal。
|
||
|
||
背景:此前主线不会因为收到“请在这条主线开启一个持久 /goal”
|
||
这样的自然语言就自己变成 goal 任务——goal 只在建任务时由
|
||
调用方用 goal_mode=1 指定,或在 /continue 时显式勾 goal 模式。
|
||
本模块把“/goal”当成显式命令标记,让发给主线的消息本身就能
|
||
触发转换,调用方不用再知道任务当前是进行中还是已终态。
|
||
|
||
解析规则(刻意收紧,防误触发):
|
||
- 文本里必须出现字面 "/goal"(全角 / 也认,大小写不敏感)。
|
||
- 光出现还不够,必须是命令形状,二者居一:
|
||
1. 文本以 /goal 开头(最显式的命令);
|
||
2. /goal 后面紧跟分隔符(空格、,、:、、)或“目标”标记,
|
||
例如“/goal 收xxx”“/goal,目标是收xxx”;
|
||
3. /goal 前面有开启类动词(开启/建立/创建/添加/设为/设置/
|
||
开一个),例如“请开启一个持久/goal收xxx”。
|
||
只是在讨论里提到“/goal 的用法”这种不算命令。
|
||
- 目标取 /goal 之后的部分:先去分隔符,再去“目标是/目标为/
|
||
目标:/目标:”或“持久目标是/为/:/:”这类前缀,最后去尾句号。
|
||
- 返回值:不是命令返回 None;是命令但没给目标返回 ""(调用方
|
||
据此回 400“目标不能为空”,而不是当成普通消息悄悄排队);
|
||
否则返回目标文本。
|
||
|
||
本模块只做纯文本解析,不碰数据库、不发请求,server 与 worker
|
||
共用同一份解析,避免两端对“是不是 /goal 命令”判断不一致。
|
||
"""
|
||
|
||
import re
|
||
|
||
_GOAL_RE = re.compile(r"/goal(?![A-Za-z])", re.IGNORECASE)
|
||
_SEPARATORS = " \t\r\n::,,、"
|
||
_TARGET_MARKERS = (
|
||
"持久目标是", "持久目标为", "持久目标:", "持久目标:",
|
||
"目标是", "目标为", "目标:", "目标:",
|
||
)
|
||
_OPEN_VERBS = ("开启", "建立", "创建", "添加", "设为", "设置", "开一个")
|
||
|
||
|
||
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 parse_goal_command(text):
|
||
"""解析主线消息里的 /goal 命令,返回目标或 None/""(见模块说明)。"""
|
||
if not isinstance(text, str):
|
||
return None
|
||
s = text.strip().replace("/", "/")
|
||
if not s:
|
||
return None
|
||
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)
|