From a848b3f8f5a820007809e050e18bfcafdf6ebce1 Mon Sep 17 00:00:00 2001 From: ULookup Date: Fri, 19 Jun 2026 13:02:12 +0000 Subject: [PATCH] feat: add POST /api/worldbuilding/:wid/timeline for manual event recording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unlike POST /timeline/advance, this endpoint does not enforce forward-only — supports flashbacks, recollections, and backfill. 124 total endpoints (96 WB). --- docs/api-reference.md | 40 ++++++++++++++++- .../merak/worldbuilding_http_handler.hpp | 1 + libs/http/src/worldbuilding_http_handler.cpp | 44 +++++++++++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index acdc4f07..712fd499 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -3205,6 +3205,41 @@ Agent 在执行创建操作时,先生成预览供用户确认,确认后存 --- +#### `POST /api/worldbuilding/:wid/timeline` — 手动记录时间线事件 + +记录任意世界时间点的事件,不校验"只能向前"——支持回忆/闪回/补录。 + +**路径参数**:`wid` — 世界 ID + +**请求体**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `world_time` | string | 是 | 事件发生的世界时间(如 `"第2日午"`) | +| `description` | string | 是 | 事件描述 | +| `recorded_by` | string | 否 | 记录者,默认 `"user"` | +| `affected_character_ids` | string[] | 否 | 受影响的角色 ID 列表 | +| `related_scene_ids` | string[] | 否 | 关联场景 ID 列表 | + +**响应 `201`**: + +| 字段 | 类型 | 说明 | +|------|------|------| +| `ok` | boolean | 操作是否成功 | +| `event.id` | string | 事件 ID | +| `event.world_time` | string | 世界时间 | +| `event.description` | string | 事件描述 | +| `event.recorded_by` | string | 记录者 | +| `event.affected_character_ids` | string[] | 受影响的角色 ID | +| `event.related_scene_ids` | string[] | 关联场景 ID | + +**`400`**:`invalid_request` — 缺少必填字段或 JSON 解析失败 +**`404`**:`world_not_found` + +> **与 `POST /timeline/advance` 的区别**:`/timeline` 不校验时间先后,可记录过去/未来的任意事件;`/timeline/advance` 要求新时间严格晚于当前世界时间。 + +--- + #### `POST /api/worldbuilding/:wid/timeline/advance` — 推进世界时间 推进世界时间并可选记录时间线事件。替代旧的 `/api/worldbuilding/:wid/time/advance` 端点。 @@ -3304,7 +3339,7 @@ Agent 在执行创建操作时,先生成预览供用户确认,确认后存 | PUT | `/api/workspace/files/content` | 保存文件内容 | | POST | `/api/workspace/open` | 打开文件/目录 | -### 世界观(95 个) +### 世界观(96 个) | 方法 | 路径 | 分类 | |------|------|------| @@ -3403,8 +3438,9 @@ Agent 在执行创建操作时,先生成预览供用户确认,确认后存 | GET | `/api/worldbuilding/:wid/timeline` | Timeline | | GET | `/api/worldbuilding/:wid/timeline/events/:eid` | Timeline | | POST | `/api/worldbuilding/:wid/timeline/advance` | Timeline | +| POST | `/api/worldbuilding/:wid/timeline` | Timeline | -**总计:123 个 API 端点**(28 core + 95 worldbuilding),加 1 个 SSE 流端点,加 1 个静态文件挂载(`/`)。 +**总计:124 个 API 端点**(28 core + 96 worldbuilding),加 1 个 SSE 流端点,加 1 个静态文件挂载(`/`)。 --- diff --git a/libs/http/include/merak/worldbuilding_http_handler.hpp b/libs/http/include/merak/worldbuilding_http_handler.hpp index ba02b182..eb7660ec 100644 --- a/libs/http/include/merak/worldbuilding_http_handler.hpp +++ b/libs/http/include/merak/worldbuilding_http_handler.hpp @@ -70,6 +70,7 @@ class WorldbuildingHttpHandler { void handle_timeline_list(const httplib::Request&, httplib::Response&); void handle_timeline_event_get(const httplib::Request&, httplib::Response&); void handle_timeline_advance(const httplib::Request&, httplib::Response&); + void handle_timeline_add_event(const httplib::Request&, httplib::Response&); // Foreshadowing void handle_foreshadow_list(const httplib::Request&, httplib::Response&); diff --git a/libs/http/src/worldbuilding_http_handler.cpp b/libs/http/src/worldbuilding_http_handler.cpp index 3bf7ebdd..7f063a58 100644 --- a/libs/http/src/worldbuilding_http_handler.cpp +++ b/libs/http/src/worldbuilding_http_handler.cpp @@ -450,6 +450,8 @@ void WorldbuildingHttpHandler::install_routes(httplib::Server& server) { [this](const auto& req, auto& res) { handle_timeline_event_get(req, res); }); server.Get(R"(/api/worldbuilding/([^/]+)/timeline)", [this](const auto& req, auto& res) { handle_timeline_list(req, res); }); + server.Post(R"(/api/worldbuilding/([^/]+)/timeline)", + [this](const auto& req, auto& res) { handle_timeline_add_event(req, res); }); server.Post(R"(/api/worldbuilding/([^/]+)/timeline/advance)", [this](const auto& req, auto& res) { handle_timeline_advance(req, res); }); @@ -1522,6 +1524,48 @@ void WorldbuildingHttpHandler::handle_timeline_advance(const httplib::Request& r } } +void WorldbuildingHttpHandler::handle_timeline_add_event(const httplib::Request& req, httplib::Response& res) { + try { + std::string wid = req.matches[1]; + auto body = nlohmann::json::parse(req.body); + + auto world = service_->worlds().get_world(wid); + if (!world) { + error_response(res, "World not found", 404, "world_not_found"); + return; + } + + worldbuilding::TimelineEvent event; + event.world_time = body.at("world_time"); + event.description = body.at("description"); + event.recorded_by = body.value("recorded_by", "user"); + if (body.contains("affected_character_ids") && body["affected_character_ids"].is_array()) { + for (const auto& id : body["affected_character_ids"]) + event.affected_character_ids.push_back(id.get()); + } + if (body.contains("related_scene_ids") && body["related_scene_ids"].is_array()) { + for (const auto& id : body["related_scene_ids"]) + event.related_scene_ids.push_back(id.get()); + } + + auto recorded = service_->narrative().record_timeline_event(wid, std::move(event)); + + json_response(res, { + {"ok", true}, + {"event", { + {"id", recorded.id}, + {"world_time", recorded.world_time}, + {"description", recorded.description}, + {"recorded_by", recorded.recorded_by}, + {"affected_character_ids", recorded.affected_character_ids}, + {"related_scene_ids", recorded.related_scene_ids} + }} + }, 201); + } catch (const std::exception& e) { + error_response(res, e.what(), 400); + } +} + // --- Foreshadowing handlers ---