From f3bd65c171b38ee2c5cd5888d3628271ac11f8d0 Mon Sep 17 00:00:00 2001 From: host Date: Thu, 29 Oct 2026 05:21:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=BB=BA=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E7=9A=84=20prompt=20=E9=87=8C=E5=86=99=20/goal=20=E4=B9=9F?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8F=98=20goal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一提交只覆盖了已有主线的转换,新建任务的 prompt 里写 /goal 时仍会当成普通一轮任务跑。本提交补上 POST /tasks 入口:prompt 经 cloud/goal_command.py 解析后,目标即新任 务的 prompt 与 goal,goal_mode 强制为 1(命令优先于 body 里的 goal_mode=0),plan/验收命令冲突仍按新建规则 400, 空目标 400 不建任务;讨论里提到 /goal 不触发。 单测补 5 项:/goal 直写、整句、空目标、plan/验收冲突、 讨论 prompt 不变。 --- cloud/README.md | 5 ++- cloud/server.py | 16 ++++++++- cloud/tests/test_mainline_goal_command.py | 40 +++++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/cloud/README.md b/cloud/README.md index e481baf..51004d6 100644 --- a/cloud/README.md +++ b/cloud/README.md @@ -539,7 +539,10 @@ goal 模式的继续:server 把任务转成新 goal 语义(goal=新消息、 持久 /goal,目标是收xxx”,或直接写“/goal 收xxx”。解析在 `cloud/goal_command.py`(server 与 worker 共用同一份,防两端 判断不一致);只是在讨论里提到“/goal 的用法”不算命令。 -入口三处同款:`POST /tasks/{id}/followups`、 +新建目标也同款:`POST /tasks` 的 prompt 里直接写 `/goal +收xxx`(或上面那句整话)即新建 goal 任务,prompt 与 goal +都存解析出的目标,不用勾 goal 选项;空目标、plan 冲突、 +带验收命令各 400。入口三处同款:`POST /tasks/{id}/followups`、 `POST /tasks/{id}/thread/message`、`POST /tasks/{id}/continue` (message 字段);响应里 `delivery` 说明实际走了哪条路: queued 主线原地转 goal(`goal_converted`)、已终态复活并转 diff --git a/cloud/server.py b/cloud/server.py index adf2dbb..15f6986 100644 --- a/cloud/server.py +++ b/cloud/server.py @@ -4244,6 +4244,17 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler): if not prompt or not isinstance(prompt, str): self._send_json(400, {"error": "prompt required"}) return + # 新建目标:prompt 里直接写 /goal 也算新建 goal,不必 + # 先勾选项——例如 prompt="/goal 收xxx" 或“请开启一个 + # 持久 /goal,目标是收xxx”。解析在 cloud/goal_command.py, + # 与主线转换同款(讨论里提到 /goal 不算)。目标即新 + # 任务的 prompt 与 goal;空目标 400,不建普通任务。 + prompt_goal_target = _parse_goal_command(prompt) + if prompt_goal_target is not None and not prompt_goal_target: + self._send_json(400, {"error": "goal 目标不能为空"}) + return + if prompt_goal_target: + prompt = prompt_goal_target depends_on = body.get("depends_on") or [] if isinstance(depends_on, str): depends_on = [depends_on] @@ -4327,7 +4338,10 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler): # verify_command 是语义冲突,400。旧语义(verify_command # 非空)保留:goal 文本缺省用 prompt,轮次缺省 3,非法值 # 400 且不建任务;老客户端与已建的旧 goal 任务继续可用。 - goal_mode = 1 if body.get("goal_mode") in (True, 1) else 0 + # /goal 命令优先于 body 里的 goal_mode=0(prompt 里的 + # 命令比选项更具体),plan/验收命令冲突仍在下面 400。 + goal_mode = 1 if body.get("goal_mode") in (True, 1) \ + or prompt_goal_target else 0 if goal_mode: if plan: self._send_json(400, { diff --git a/cloud/tests/test_mainline_goal_command.py b/cloud/tests/test_mainline_goal_command.py index 7a55fc9..d81ad5f 100644 --- a/cloud/tests/test_mainline_goal_command.py +++ b/cloud/tests/test_mainline_goal_command.py @@ -208,6 +208,46 @@ class ServerTestCase(unittest.TestCase): after = self._get_task(task["id"]) self.assertEqual(after["goal_mode"], 0) + def test_create_task_prompt_goal_command(self): + task = self._create(prompt="/goal 收xxx") + self.assertEqual(task["goal_mode"], 1) + self.assertEqual(task["goal"], "收xxx") + self.assertEqual(task["prompt"], "收xxx") + self.assertIsNone(task["verify_command"]) + self.assertEqual(task["goal_max_rounds"], 0) + + def test_create_task_prompt_goal_user_sentence(self): + task = self._create( + prompt="请在这条主线开启一个持久 /goal,目标是收xxx") + self.assertEqual(task["goal_mode"], 1) + self.assertEqual(task["goal"], "收xxx") + self.assertEqual(task["prompt"], "收xxx") + + def test_create_task_prompt_goal_empty_400(self): + status, body = self._req( + "POST", "/tasks", + {"project_id": self.project["id"], "prompt": "/goal"}, + self.owner_key) + self.assertEqual(status, 400, body) + + def test_create_task_prompt_goal_conflicts_400(self): + status, _body = self._req( + "POST", "/tasks", + {"project_id": self.project["id"], "prompt": "/goal 收xxx", + "plan": 1}, + self.owner_key) + self.assertEqual(status, 400) + status, _body = self._req( + "POST", "/tasks", + {"project_id": self.project["id"], "prompt": "/goal 收xxx", + "verify_command": "true"}, + self.owner_key) + self.assertEqual(status, 400) + + def test_create_task_discussion_prompt_unchanged(self): + task = self._create(prompt="讨论/goal的用法") + self.assertEqual(task["goal_mode"], 0) + def test_continue_goal_command(self): task = self._create() self._set_status(task["id"], "completed")