diff --git a/cloud/README.md b/cloud/README.md index 51004d6..fbd6c31 100644 --- a/cloud/README.md +++ b/cloud/README.md @@ -535,10 +535,13 @@ goal 模式的继续:server 把任务转成新 goal 语义(goal=新消息、 ## 主线 /goal 命令(发给主线一句话就自动建 goal) 不用先知道主线是进行中还是已终态,也不用勾选项:给主线发 -一条含字面 `/goal` 的消息即可,例如“请在这条主线开启一个 -持久 /goal,目标是收xxx”,或直接写“/goal 收xxx”。解析在 +一条含触发词的消息即可,例如“请在这条主线开启一个 +持久 /goal,目标是收xxx”,或直接写“/goal 收xxx”。斜杠 +不用:单发 `goal` 也同款触发,例如“goal 收xxx”、 +“goal,目标是收xxx”。解析在 `cloud/goal_command.py`(server 与 worker 共用同一份,防两端 -判断不一致);只是在讨论里提到“/goal 的用法”不算命令。 +判断不一致);只是在讨论里提到“/goal 的用法”“goal 是什么” +这种不算命令。 新建目标也同款:`POST /tasks` 的 prompt 里直接写 `/goal 收xxx`(或上面那句整话)即新建 goal 任务,prompt 与 goal 都存解析出的目标,不用勾 goal 选项;空目标、plan 冲突、 diff --git a/cloud/goal_command.py b/cloud/goal_command.py index 62fe6a8..0423a23 100644 --- a/cloud/goal_command.py +++ b/cloud/goal_command.py @@ -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"(? 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 diff --git a/cloud/tests/test_mainline_goal_command.py b/cloud/tests/test_mainline_goal_command.py index d81ad5f..d7b9f1a 100644 --- a/cloud/tests/test_mainline_goal_command.py +++ b/cloud/tests/test_mainline_goal_command.py @@ -56,6 +56,22 @@ class ParserTestCase(unittest.TestCase): self.assertIsNone(parse_goal_command("请帮我完成目标收xxx")) self.assertIsNone(parse_goal_command("hello world")) + def test_bare_goal_without_slash(self): + # 不用斜杠:用户单发 goal 也触发同款目标。 + self.assertEqual(parse_goal_command("goal 收xxx"), "收xxx") + self.assertEqual(parse_goal_command("goal,目标是收xxx"), "收xxx") + self.assertEqual(parse_goal_command("goal目标是收xxx"), "收xxx") + self.assertEqual(parse_goal_command("goal收xxx"), "收xxx") + self.assertEqual(parse_goal_command("发goal 收xxx"), "收xxx") + self.assertEqual(parse_goal_command("goal"), "") + + def test_bare_goal_discussion_still_not_a_command(self): + self.assertIsNone(parse_goal_command("goal的用法")) + self.assertIsNone(parse_goal_command("goal是什么意思")) + self.assertIsNone(parse_goal_command("讨论goal的用法")) + self.assertIsNone(parse_goal_command("请问goal是什么意思")) + self.assertIsNone(parse_goal_command("/goal的用法")) + class ServerTestCase(unittest.TestCase): def setUp(self): @@ -248,6 +264,20 @@ class ServerTestCase(unittest.TestCase): task = self._create(prompt="讨论/goal的用法") self.assertEqual(task["goal_mode"], 0) + def test_followup_bare_goal_converts(self): + task = self._create() + status, body = self._req( + "POST", "/tasks/%s/followups" % task["id"], + {"body": "goal 收xxx"}, self.owner_key) + self.assertEqual(status, 200, body) + self.assertEqual(body["delivery"], "goal_converted") + self.assertEqual(body["goal"], "收xxx") + + def test_create_task_bare_goal(self): + task = self._create(prompt="goal 收xxx") + self.assertEqual(task["goal_mode"], 1) + self.assertEqual(task["goal"], "收xxx") + def test_continue_goal_command(self): task = self._create() self._set_status(task["id"], "completed")