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

80 lines
3.3 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"(全角 / 也认,大小写不敏感)。
- 光出现还不够,必须是命令形状,二者居一:
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)