feat: 模型配置支持删除,岗位行动规划对接任务系统字典与批量新增 Skill
模型管理新增 DELETE 接口与前端删除能力(支持含斜杠的模型名);行动规划改为 9 字段 JSON,prepare 传入 taskId/字典并绑定 position-roundtable-action-create Skill。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
af509eb670
commit
c9c2dc7ea8
12 files changed
+697
-95
No files matched your search
@@ -3,6 +3,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import importlib.util
|
||||
import json
|
||||
import zipfile
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
@@ -65,6 +68,120 @@ def test_position_intent_agent_is_a_seeded_functional_agent():
|
||||
assert POSITION_INTENT_AGENT_ID in set(required_agent_ids())
|
||||
|
||||
|
||||
def test_action_plan_seed_binds_the_batch_create_skill_and_nine_field_contract():
|
||||
"""New deployments must not fall back to the retired subtask JSON contract."""
|
||||
action_assets = roundtable_seed._ASSETS_DIR / roundtable_seed.ACTION_PLAN_AGENT_ID
|
||||
soul = (action_assets / "SOUL.md").read_text(encoding="utf-8")
|
||||
config = (action_assets / "config.yaml").read_text(encoding="utf-8")
|
||||
|
||||
assert "position-roundtable-action-create" in config
|
||||
for field in (
|
||||
"actionName",
|
||||
"actionDescription",
|
||||
"actionType",
|
||||
"publishChannel",
|
||||
"infoFormat",
|
||||
"responsiblePosition",
|
||||
"startTime",
|
||||
"endTime",
|
||||
"taskId",
|
||||
):
|
||||
assert field in soul
|
||||
assert "/task/action/add" in soul
|
||||
assert "批量新增" in soul
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_action_plan_prompt_receives_frontend_task_id_and_dictionary_values():
|
||||
"""The action planner must see exact dictValue choices, not frontend labels only."""
|
||||
dictionaries = position_roundtable_router.ActionPlanDictionaries(
|
||||
publish_channels=[{"label": "微信公众号", "value": "11"}],
|
||||
action_types=[{"label": "宣传", "value": "21"}],
|
||||
info_formats=[{"label": "图文", "value": "31"}],
|
||||
responsible_positions=[{"label": "规划岗", "value": "41"}],
|
||||
)
|
||||
prompt = await position_roundtable_router._build_builtin_prompt(
|
||||
kind="action_plan",
|
||||
session={
|
||||
"task_snapshot": {"title": "任务 A", "description": "生成行动"},
|
||||
"summary_snapshot": {"status": "done", "content": "方案总结正文"},
|
||||
},
|
||||
nodes=[],
|
||||
action_task_id=1001,
|
||||
action_dictionaries=dictionaries,
|
||||
)
|
||||
|
||||
assert "当前 taskId:1001" in prompt
|
||||
assert "微信公众号 → 11" in prompt
|
||||
assert "宣传 → 21" in prompt
|
||||
assert "图文 → 31" in prompt
|
||||
assert "规划岗 → 41" in prompt
|
||||
assert "id、title、goal" in prompt
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_action_plan_prompt_degrades_when_task_dictionaries_are_unavailable():
|
||||
"""A 404 from the task-system dictionaries must not block local planning."""
|
||||
prompt = await position_roundtable_router._build_builtin_prompt(
|
||||
kind="action_plan",
|
||||
session={
|
||||
"task_snapshot": {"title": "本地演示任务"},
|
||||
"summary_snapshot": {"status": "done", "content": "方案总结正文"},
|
||||
},
|
||||
nodes=[],
|
||||
action_task_id="abc123",
|
||||
action_dictionaries=position_roundtable_router.ActionPlanDictionaries(),
|
||||
)
|
||||
|
||||
assert "字典未获取完整" in prompt
|
||||
assert "不得调用 Skill" in prompt
|
||||
assert "0 占位" in prompt
|
||||
|
||||
|
||||
def test_action_plan_prepare_keeps_a_local_mock_task_id_without_numeric_coercion():
|
||||
"""A local demo id must not stop the action-plan conversation at prepare time."""
|
||||
body = position_roundtable_router.PrepareBuiltinRunRequest(task_id="abc123")
|
||||
|
||||
assert body.task_id == "abc123"
|
||||
|
||||
|
||||
def test_action_batch_skill_accepts_only_the_nine_field_array(tmp_path):
|
||||
"""The batch Skill must reject old task-subdivision fields before any HTTP call."""
|
||||
archive_path = roundtable_seed._ASSETS_DIR.parents[3] / "skill-packages" / "position-roundtable-action-create.skill"
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
archive.extractall(tmp_path / "skill-package")
|
||||
script_path = tmp_path / "skill-package" / "position-roundtable-action-create" / "scripts" / "batch_add_actions.py"
|
||||
spec = importlib.util.spec_from_file_location("position_action_batch_skill", script_path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
valid = [{
|
||||
"actionName": "发布内容",
|
||||
"actionDescription": "向目标渠道发布审核通过的信息。",
|
||||
"actionType": "1",
|
||||
"publishChannel": "2",
|
||||
"infoFormat": "3",
|
||||
"responsiblePosition": "4",
|
||||
"startTime": "2026-08-12T09:00:00+08:00",
|
||||
"endTime": "2026-08-12T18:00:00+08:00",
|
||||
"taskId": 1001,
|
||||
}]
|
||||
source = tmp_path / "action-plan-subtasks.json"
|
||||
source.write_text(json.dumps(valid, ensure_ascii=False), encoding="utf-8")
|
||||
assert module.read_actions(source, 1001) == [{
|
||||
**valid[0],
|
||||
"actionType": 1,
|
||||
"publishChannel": 2,
|
||||
"infoFormat": 3,
|
||||
"responsiblePosition": 4,
|
||||
}]
|
||||
|
||||
source.write_text(json.dumps([{**valid[0], "id": "legacy"}], ensure_ascii=False), encoding="utf-8")
|
||||
with pytest.raises(module.ActionInputError, match="多余字段"):
|
||||
module.read_actions(source, 1001)
|
||||
|
||||
|
||||
def test_position_intent_prompt_keeps_the_original_step_one_guardrails():
|
||||
"""Dedicated schema must not regress into solutioning or gatekeeping."""
|
||||
prompt = (
|
||||
@@ -165,6 +282,29 @@ def test_position_intent_seed_repairs_its_contract_without_touching_other_agents
|
||||
assert generic_soul.read_text(encoding="utf-8") == "deployed generic tune"
|
||||
|
||||
|
||||
def test_existing_action_planner_only_merges_the_owned_skill(tmp_path, monkeypatch):
|
||||
"""A code upgrade may add the Skill without overwriting a user-tuned SOUL."""
|
||||
agents_dir = tmp_path / "agents"
|
||||
monkeypatch.setattr(
|
||||
roundtable_seed,
|
||||
"get_paths",
|
||||
lambda: SimpleNamespace(agent_dir=lambda agent_id: agents_dir / agent_id),
|
||||
)
|
||||
action_dir = agents_dir / roundtable_seed.ACTION_PLAN_AGENT_ID
|
||||
action_dir.mkdir(parents=True)
|
||||
(action_dir / "SOUL.md").write_text("deployed action-plan tune", encoding="utf-8")
|
||||
(action_dir / "config.yaml").write_text(
|
||||
"id: position-action-planner\nskills:\n - existing-skill\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert roundtable_seed._ensure_one(roundtable_seed.ACTION_PLAN_AGENT_ID) is True
|
||||
config = (action_dir / "config.yaml").read_text(encoding="utf-8")
|
||||
assert "existing-skill" in config
|
||||
assert "position-roundtable-action-create" in config
|
||||
assert (action_dir / "SOUL.md").read_text(encoding="utf-8") == "deployed action-plan tune"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_intent_init_creates_thread_directly_without_loopback(monkeypatch):
|
||||
"""Step 1 initialization must not depend on the deployed Gateway port."""
|
||||
|
||||
Reference in new issue
Block a user