feat: 主线 /goal 命令自动建 goal

发给主线的一句话(含字面 /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 跑通。
This commit is contained in:
host committed 2026-10-30 07:15:05 +08:00
1 parent 3a5c5db9fb
commit 3096e04345
5 files changed
+720 -11

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
"""主线 /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)