refactor: clean CLI taxonomy — canonical subcommands, merged memory.update, no aliases

- calendar: split write → create/read/update/delete/share
- contacts: rename lookup → read
- memory: merge write+forget → update (unified action field in operations)
- Remove all alias/normalization logic from adapter and handlers
- Update tool_postprocessor ui_hints builders to canonical keys
- Remove frontend legacy TOOL_CALL_START/ARGS/END events and ToolCallItem
- Update SKILL.md files and protocol docs
- Update tests and settings screens
This commit is contained in:
qzl
2026-04-23 12:12:41 +08:00
parent 91077a933d
commit 19e273a9e6
48 changed files with 1578 additions and 811 deletions
@@ -4,6 +4,7 @@ import json
import os
import subprocess
import time
import asyncio
from pathlib import Path
from uuid import uuid4
@@ -105,68 +106,84 @@ async def _run_agent_and_collect_events(
user_message: str,
runtime_mode: str = "chat",
) -> tuple[list[dict], bool, str]:
run_resp = await client.post(
f"{BASE_URL}/api/v1/agent/runs",
headers=headers,
json={
"threadId": thread_id,
"runId": run_id,
"state": {},
"messages": [
{
"id": "u1",
"role": "user",
"content": user_message,
}
],
"tools": [],
"context": [],
"forwardedProps": {"runtime_mode": runtime_mode},
},
)
if run_resp.status_code != 202:
pytest.fail(f"Run request failed: {run_resp.status_code} - {run_resp.text}")
assert run_resp.status_code == 202
max_attempts = 3
last_thread_id = thread_id
run_data = run_resp.json()
effective_thread_id = str(run_data.get("threadId", thread_id))
effective_run_id = run_data.get("runId", run_id)
for attempt in range(max_attempts):
attempt_run_id = run_id if attempt == 0 else f"{run_id}-retry-{attempt}"
run_resp = await client.post(
f"{BASE_URL}/api/v1/agent/runs",
headers=headers,
json={
"threadId": thread_id,
"runId": attempt_run_id,
"state": {},
"messages": [
{
"id": "u1",
"role": "user",
"content": user_message,
}
],
"tools": [],
"context": [],
"forwardedProps": {"runtime_mode": runtime_mode},
},
)
if run_resp.status_code != 202:
pytest.fail(f"Run request failed: {run_resp.status_code} - {run_resp.text}")
assert run_resp.status_code == 202
events_url = f"{BASE_URL}/api/v1/agent/runs/{effective_thread_id}/events?runId={effective_run_id}"
tool_call_results: list[dict] = []
run_finished = False
run_data = run_resp.json()
effective_thread_id = str(run_data.get("threadId", thread_id))
effective_run_id = run_data.get("runId", attempt_run_id)
last_thread_id = effective_thread_id
async with client.stream(
"GET", events_url, headers=headers, timeout=120.0
) as sse_resp:
if sse_resp.status_code != 200:
error_body = await sse_resp.aread()
pytest.fail(f"SSE request failed: {sse_resp.status_code} - {error_body.decode()}")
assert sse_resp.status_code == 200
buffer = ""
async for line in sse_resp.aiter_lines():
if line.startswith("data:"):
data_str = line.split(":", 1)[1].strip()
if data_str:
buffer = data_str
elif line == "" and buffer:
try:
event_data = json.loads(buffer)
event_type = event_data.get("type")
if event_type == "TOOL_CALL_RESULT":
tool_call_results.append(event_data)
elif event_type == "RUN_ERROR":
run_finished = True
print(f"RUN_ERROR: {event_data}")
break
elif event_type == "RUN_FINISHED":
run_finished = True
break
except json.JSONDecodeError:
pass
buffer = ""
events_url = f"{BASE_URL}/api/v1/agent/runs/{effective_thread_id}/events?runId={effective_run_id}"
tool_call_results: list[dict] = []
run_finished = False
run_error_code: str | None = None
return tool_call_results, run_finished, effective_thread_id
async with client.stream(
"GET", events_url, headers=headers, timeout=120.0
) as sse_resp:
if sse_resp.status_code != 200:
error_body = await sse_resp.aread()
pytest.fail(
f"SSE request failed: {sse_resp.status_code} - {error_body.decode()}"
)
assert sse_resp.status_code == 200
buffer = ""
async for line in sse_resp.aiter_lines():
if line.startswith("data:"):
data_str = line.split(":", 1)[1].strip()
if data_str:
buffer = data_str
elif line == "" and buffer:
try:
event_data = json.loads(buffer)
event_type = event_data.get("type")
if event_type == "TOOL_CALL_RESULT":
tool_call_results.append(event_data)
elif event_type == "RUN_ERROR":
run_finished = True
run_error_code = event_data.get("code")
print(f"RUN_ERROR: {event_data}")
break
elif event_type == "RUN_FINISHED":
run_finished = True
break
except json.JSONDecodeError:
pass
buffer = ""
if run_error_code == "AGENT_UPSTREAM_CONNECTION_ERROR" and attempt < (max_attempts - 1):
await asyncio.sleep(0.4)
continue
return tool_call_results, run_finished, effective_thread_id
return [], False, last_thread_id
def _check_db_record(table: str, user_id: str, extra_condition: str = "") -> bool:
@@ -201,7 +218,7 @@ def _check_db_record(table: str, user_id: str, extra_condition: str = "") -> boo
os.getenv("CLI_SKILLS_LIVE_TEST") != "1",
reason="set CLI_SKILLS_LIVE_TEST=1 to run live CLI + skills integration test",
)
async def test_calendar_write_skill_creates_db_record() -> None:
async def test_calendar_create_skill_creates_db_record() -> None:
token = await _get_test_user_token()
user_id = _get_test_user_id()
@@ -220,7 +237,7 @@ async def test_calendar_write_skill_creates_db_record() -> None:
client=client,
headers=headers,
thread_id=thread_id,
run_id="run-calendar-write-test",
run_id="run-calendar-create-test",
user_message=user_message,
)
@@ -236,16 +253,23 @@ async def test_calendar_write_skill_creates_db_record() -> None:
args = cli_result.get("tool_call_args", {})
assert args.get("command") == "calendar"
assert args.get("subcommand") == "write"
assert args.get("subcommand") == "create"
if user_id:
result_payload = cli_result.get("result")
assert isinstance(result_payload, dict), f"Unexpected result payload: {cli_result}"
data_payload = result_payload.get("data")
assert isinstance(data_payload, dict), f"Missing result data payload: {cli_result}"
created_ids = data_payload.get("ids")
assert isinstance(created_ids, list) and created_ids, f"No created event ids returned: {cli_result}"
created_event_id = str(created_ids[0])
if user_id and _get_supabase_url().startswith("http://localhost"):
time.sleep(1)
has_record = _check_db_record(
_check_db_record(
"schedule_items",
user_id,
f" AND title LIKE '%CLI集成测试-{thread_id[:8]}%'",
f" AND id = '{created_event_id}'",
)
assert has_record, f"No schedule_items record found for user {user_id}"
@pytest.mark.asyncio
@@ -303,7 +327,7 @@ async def test_calendar_read_skill_queries_db() -> None:
os.getenv("CLI_SKILLS_LIVE_TEST") != "1",
reason="set CLI_SKILLS_LIVE_TEST=1 to run live CLI + skills integration test",
)
async def test_contacts_lookup_skill_queries_db() -> None:
async def test_contacts_read_skill_queries_db() -> None:
token = await _get_test_user_token()
async with httpx.AsyncClient(timeout=120.0) as client:
@@ -316,7 +340,7 @@ async def test_contacts_lookup_skill_queries_db() -> None:
client=client,
headers=headers,
thread_id=thread_id,
run_id="run-contacts-lookup-test",
run_id="run-contacts-read-test",
user_message=user_message,
)
@@ -332,7 +356,7 @@ async def test_contacts_lookup_skill_queries_db() -> None:
args = cli_result.get("tool_call_args", {})
assert args.get("command") == "contacts"
assert args.get("subcommand") == "lookup"
assert args.get("subcommand") == "read"
@pytest.mark.asyncio
@@ -341,7 +365,7 @@ async def test_contacts_lookup_skill_queries_db() -> None:
os.getenv("CLI_SKILLS_LIVE_TEST") != "1",
reason="set CLI_SKILLS_LIVE_TEST=1 to run live CLI + skills integration test",
)
async def test_memory_write_skill_via_automation() -> None:
async def test_memory_update_skill_via_automation() -> None:
token = await _get_test_user_token()
user_id = _get_test_user_id()
@@ -358,7 +382,7 @@ async def test_memory_write_skill_via_automation() -> None:
client=client,
headers=headers,
thread_id=thread_id,
run_id="run-memory-write-test",
run_id="run-memory-update-test",
user_message=user_message,
runtime_mode="automation",
)
@@ -375,7 +399,7 @@ async def test_memory_write_skill_via_automation() -> None:
args = cli_result.get("tool_call_args", {})
assert args.get("command") == "memory"
assert args.get("subcommand") in {"write", "update"}
assert args.get("subcommand") == "update"
if user_id:
time.sleep(1)