Skip to content

[v2] chat:接真实接口(会话 CRUD、SSE、快照映射) - #236

Merged
EricWvi merged 18 commits into
ora-space:mainfrom
ObsisMc:feat/787-v2-chat-api
Oct 30, 2026
Merged

EricWvi merged 18 commits into
ora-space:mainfrom
ObsisMc:feat/787-v2-chat-api

Conversation

@ObsisMc

@ObsisMc ObsisMc commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

对话页从前端 store 和占位数据改为走 v2-server。v2-server 新增 chat 领域、HTTP 和 SSE 事件流;chat 通过统一注入点 server.Options.Execution / Catalog 取执行服务,不改 main.go。真实执行服务装配后,页面侧不需要再改代码。

v2-server

  • internal/chat:对话、对话与执行 session 的一一对应、项目顺序。一个对话只对应一个 session:停止或服务重启后对话只读,再发消息返回 409 conversation_ended;上一轮未结束返回 409 turn_running,由页面排队。Idempotency-Key 即该轮的 attempt ID,同 key 重试不会重复执行;结果未知返回 503 outcome_unknown;首条消息被拒绝时不留空对话。
  • internal/server/chat.go:会话列表、创建、改名、删除、发消息、停止、执行器清单、项目顺序,以及 GET …/chat/conversations/{cid}/events(Server-Sent Events,按 Last-Event-ID 续传)。契约之外的候选接口定义在 v2-server/chat.openapi.json,并登记在 v2-server/README.md。
  • internal/store:对话表和项目顺序表。
  • 未配置执行服务(当前 main 注入的 executiontest.Stub)时,执行器清单、创建和发消息返回 503 execution_unavailable,历史仍可读。

frontend-v2

  • 用 orval 生成 chat 客户端(orval.chat.config.ts);项目列表读 v2-server 的候选接口 GET /api/v2/tenants/{tid}/projects,按 nextCursor 读完所有页。
  • history_snapshot 整份替换后重建轮次(同位置替换、settled 优先),没能完整读取的回答显示为失败。
  • store 改走后端:新对话由第一条消息创建并只选项目;顶栏标"沙盒";session 结束后线程末尾标出原因,输入框换成"新对话";只给在看或在等回复的对话开事件流,再打开时从上次的序号续读。
  • 首版不提供的能力(附件、@ 插件、会话内切换模型、新建项目)留空。
  • npm run dev:demo / task v2:demo:浏览器内的演示后端,按 v2-server 的 chat 规则回放脚本回复,界面右上角标"演示模式"。
  • e2e:chat.spec.ts 验证无运行时时的提示、示例回放和左栏调宽;workflow-runs.spec.ts 因项目改由后端提供,用路由替身给一个项目和执行器。

验证

  • v2-server:gofmt、go vet ./...,真实 PostgreSQL 上 go test -race -count=1 ./... 全部通过。
  • frontend-v2:三个 api:check:*、tsc -b、oxlint --deny-warnings、vite build 通过;单元测试 67 个文件 490 个用例全部通过。
  • e2e:本地全量 playwright test 18 个用例中 16 个通过;失败的 chat 与 workflow-runs 两个修正后单独重跑通过。
  • 执行服务只对着 executiontest.Stub 验证;真实 Agent 链路的 e2e 和截图等执行服务装配进 main 后再补。

已知限制

main 尚未注入项目和执行服务,运行中的页面只有"示例"分组,项目的 + 菜单(新建对话、运行工作流)要等装配后才出现。

v2,不涉及 specs。

Refs ora-space/circus#787

A conversation owns its title, target and executor choice and maps to
one or more execution sessions: stopping, idling, deleting or a restart
ends the current session and the next prompt starts a new one in the
same workspace. Idempotency keys are the turns' attempt IDs, so retries
never run a prompt twice, and a refused first prompt leaves no empty
conversation.
The chat routes live outside api/openapi.json in chat.openapi.json.
Session events stream as Server-Sent Events keyed by session sequence,
so a reconnect resumes with Last-Event-ID. Without an execution runtime
every chat route answers 503 execution_unavailable.
Generate the chat client from v2-server/chat.openapi.json and rebuild
each session's turns from its latest snapshot through the reducer the
samples use, so snapshots replace rather than append. An answer that
was not read back in full shows as failed, and a marker says where an
ended session stops so the next one reads as a fresh Agent.
An idle live session keeps its workspace until End, and stopping only
cancels a running turn. POST .../end ends the session so a busy
workspace can be handed to another conversation; the next prompt
starts a new session.
The history column now lists the backend's projects, worktrees and
conversations next to the samples. A conversation is created by its
first prompt, followed over its sessions' event streams and rebuilt
from each snapshot; a sent prompt shows until the history has it.
A busy workspace names the conversation holding it and offers to end
its session. The input sends text only and keeps a conversation on its
executor, and new projects and worktrees stay hidden until the backend
can create them. Samples still replay in the browser.
The session_ended event carries no reason, so a page learns it from the
conversation. Chat now writes the reason before asking the port to end
the session and withdraws it if ending fails, so the reason is readable
as soon as the event is: an idle or released session no longer reads as
interrupted.
A conversation nobody is watching and that waits on nothing stops
following its sessions, since browsers allow six connections per
origin over HTTP/1.1; following again resumes after the last event
seen, so nothing is applied twice. A session that ended without this
page acting reads its reason from the conversation. Prompts queued
while a first prompt is admitted carry over to the conversation.
npm run dev:demo (task v2:demo) serves the chat routes and their event
streams from the browser with v2-server's chat rules, made-up projects
and a scripted Agent that writes fixed-Node history: busy workspaces,
stopping, idle ends, failed answers and preview-to-settled replies can
all be shown without an execution runtime. The page says it is a demo.
Each conversation now runs in its own directory, so conversations never
contend for a workspace. Stopping a turn ends the fixed Node session, and
the conversation then stays read-only instead of opening a new session.

- A conversation binds one execution session; the store refuses a second.
- Sending to an ended conversation returns 409 conversation_ended, and an
  Append refused after a restart records the session as interrupted.
- A prompt sent while the previous turn runs returns 409 turn_running for
  the page to queue.
- Remove idle reclaim, POST .../end, busy occupant hints and workspace
  selection; events move to GET .../conversations/{cid}/events.
Follows the backend's one-session-per-conversation rules on the chat page.

- A new chat picks only its project: no workspace tab, and the project's
  "+" starts a chat directly. Conversations list straight under their
  project with an "own directory" badge.
- Once the session ends (stopped, or the server restarted) the thread
  ends with a marker and the input becomes a "new chat" button; nothing
  is sent and no new session is started.
- A prompt the backend refuses with turn_running goes back to the front
  of the queue and waits for the running turn's end event.
- Drop the busy-workspace notice, release action and worktree loading;
  follow events per conversation.
- The demo backend gives every conversation its own session and seeds a
  live, a stopped and a restart-ended conversation.
Every conversation gets a new worktree from its project, so the input
shows a "Worktree" label, as desktop marks worktree mode, with a tooltip
explaining it; the conversation header shows the same label in place of
the workspace badge.

The worktree sample no longer sits under a branch in the history, since
no conversation is grouped by worktree any more.
Every conversation runs in a sandbox of its own, its code cloned from the
project's repository, so the chat page no longer knows any workspace kind:
sessions and new chats drop the main/isolated/new-worktree workspace, the
sidebar lists a project's conversations directly, and the unused worktree
picker, naming dialog and branch-name helper go. The Worktree label becomes
a 沙盒 label whose tooltip says the code is cloned and never written back.
A project's + or 新建对话 used to do nothing visible when the new chat view
already showed that project. Each request for the new chat view now puts
the cursor in its input, so the click always answers.
Chat no longer takes its own Options.Chat service. server.New builds the
chat service from the store and the injected Options.Execution and Catalog,
as every page does, so main.go stays untouched and the real service arrives
with #774's assembly. Until then the injected unavailable Stub keeps the
history readable and answers executors, create and send with 503
execution_unavailable; tests inject an executiontest.Stub the same way.
The project's + is a menu again, holding 新建对话 next to 运行工作流, and
the chat page reads its projects and tenant from the backend, so the
workflow-run flow renders with a query client and the chat backend fake.
v2-server does not serve the formal v1 project list; the chat page now
uses the candidate GET /api/v2/tenants/{tid}/projects through its own
generated client and follows nextCursor so no project is left out. The
test and demo backends answer the new route, the test one paged so the
page has to follow the cursor.
The chat page reads projects from v2-server, which serves none until the
execution runtime is wired, so the workflow-run e2e gives it one project
and executor to hang the run on, and returns to a sample session at the
end. The chat e2e picks its notice out of dnd-kit's live region.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants