载入中...
搜索中...
未找到
EditorAutomationProvider.cpp
浏览该文件的文档.
2
6#include "common/Capability.h"
7#include "rx/Rx.h"
8
9#include <algorithm>
10#include <cstdint>
11#include <optional>
12#include <string>
13#include <utility>
14#include <vector>
15
16namespace eve::editor {
17
18namespace {
19
20const char* statusName(EditorStatus status) {
21 switch (status) {
22 case EditorStatus::Ok: return "ok";
23 case EditorStatus::Applied: return "applied";
24 case EditorStatus::Pending: return "pending";
25 case EditorStatus::NoOp: return "no-op";
26 case EditorStatus::Rejected: return "rejected";
27 case EditorStatus::Conflict: return "conflict";
28 case EditorStatus::NotFound: return "not-found";
29 case EditorStatus::Unsupported: return "unsupported";
30 case EditorStatus::Cancelled: return "cancelled";
31 case EditorStatus::Failed: return "failed";
32 }
33 return "failed";
34}
35
36const char* transactionStateName(TransactionState state) {
37 switch (state) {
38 case TransactionState::Planning: return "planning";
39 case TransactionState::Previewing: return "previewing";
40 case TransactionState::PendingAuthority: return "pending-authority";
41 case TransactionState::Committed: return "committed";
42 case TransactionState::RolledBack: return "rolled-back";
43 case TransactionState::Rejected: return "rejected";
44 case TransactionState::Conflicted: return "conflicted";
45 case TransactionState::Failed: return "failed";
46 }
47 return "failed";
48}
49
50EditorValue diagnosticsValue(const std::vector<EditorDiagnostic>& diagnostics) {
52 for (const EditorDiagnostic& diagnostic : diagnostics) {
54 value["rule"] = editing::diagnosticRule(diagnostic).value();
55 value["severity"] = static_cast<std::int64_t>(diagnostic.severity());
56 value["message"] = diagnostic.message();
57 values.emplace_back(std::move(value));
58 }
59 return EditorValue(std::move(values));
60}
61
62EditorValue resultValue(EditorStatus status, const std::vector<EditorDiagnostic>& diagnostics) {
64 result["status"] = statusName(status);
65 result["accepted"] = status == EditorStatus::Applied || status == EditorStatus::Pending ||
66 status == EditorStatus::NoOp;
67 result["diagnostics"] = diagnosticsValue(diagnostics);
68 return EditorValue(std::move(result));
69}
70
71EditorValue receiptValue(const Result<TransactionReceipt>& result) {
72 EditorValue response = resultValue(result.code(), result.diagnostics());
73 if (result.ok()) {
74 auto* object = response.getIf<EditorValue::Object>();
75 (*object)["transactionId"] = result.value().id.value();
76 (*object)["state"] = transactionStateName(result.value().state);
77 (*object)["beforeRevision"] = static_cast<std::int64_t>(result.value().beforeRevision);
78 (*object)["afterRevision"] = static_cast<std::int64_t>(result.value().afterRevision);
79 }
80 return response;
81}
82
83const EditorValue* findField(const EditorValue::Object& request, const char* key) {
84 const auto found = request.find(key);
85 return found == request.end() ? nullptr : &found->second;
86}
87
88std::string stringField(const EditorValue::Object& request, const char* key) {
89 const EditorValue* value = findField(request, key);
90 const auto* text = value ? value->getIf<std::string>() : nullptr;
91 return text ? *text : std::string{};
92}
93
94const std::int64_t* integerField(const EditorValue::Object& request, const char* key) {
95 const EditorValue* value = findField(request, key);
96 return value ? value->getIf<std::int64_t>() : nullptr;
97}
98
99EditorValue valueField(const EditorValue::Object& request, const char* key) {
100 const EditorValue* value = findField(request, key);
101 return value ? *value : EditorValue{};
102}
103
104std::string errorJson(EditorStatus status, const char* rule, std::string message) {
105 return editorValueToJson(resultValue(
107 DiagnosticSeverity::Error, std::move(message))}));
108}
109
110} // namespace
111
119
122 : commands_(&commands), targets_(&targets) {
123 session_.setCommandService(commands_);
124 session_.setSessionId(SessionId("editor.mcp"));
125}
126
128
130 if (session_.boundTargetId() != target) return;
131 session_.clearRetainedPlans();
132 session_.clearTarget();
133}
134
135std::string EditorAutomationProvider::invoke(const std::string& operation, const std::string& requestJson) {
136 refreshProfile();
137 Result<EditorValue> parsed = editorValueFromJson(requestJson.empty() ? "{}" : requestJson);
138 if (!parsed.ok() || parsed.value().type() != EditorValue::Type::Object)
139 return errorJson(EditorStatus::Rejected, "editor.automation.invalid-json", "Request must be a JSON object");
140 const auto& request = *parsed.value().getIf<EditorValue::Object>();
141 if (operation == "commands") return commandsJson();
142 if (operation == "target-list") return targetList();
143 if (operation == "target-create") return createTarget(request);
144 if (operation == "target-close") return closeTarget(request);
145 if (operation == "observe-start") return startObservation(request);
146 if (operation == "observe-describe") return describeObservation(request);
147 if (operation == "observe-publish") return publishObservation(request);
148 if (operation == "observe-poll") return pollObservation(request);
149 if (operation == "observe-close") return closeObservation(request);
150 if (operation == "inspect") {
151 auto result = targets_->inspect(TargetId(stringField(request, "target")));
152 lastDiagnostics_ = result.diagnostics();
153 EditorValue response = resultValue(result.code(), result.diagnostics());
154 if (result.ok()) (*response.getIf<EditorValue::Object>())["target"] = result.value();
155 return editorValueToJson(response);
156 }
157 if (operation == "plan" || operation == "execute") {
158 auto bound = bindRequestedTarget(request);
159 if (!bound.ok()) return editorValueToJson(resultValue(bound.code(), bound.diagnostics()));
160 }
161 if (operation == "plan") {
162 const std::string command = stringField(request, "command");
163 std::optional<Revision> expected;
164 if (const auto* value = integerField(request, "expectedRevision")) expected = static_cast<Revision>(*value);
165 auto result = session_.retainPlan(CommandId(command), valueField(request, "payload"),
166 CommandSource::Automation, expected);
167 lastDiagnostics_ = result.diagnostics();
168 EditorValue response = resultValue(result.code(), result.diagnostics());
169 if (result.ok()) (*response.getIf<EditorValue::Object>())["planId"] = result.value().value();
170 return editorValueToJson(response);
171 }
172 if (operation == "commit") {
173 auto result = session_.executeRetainedPlan(PlanId(stringField(request, "planId")), CommandSource::Automation);
174 lastDiagnostics_ = result.diagnostics();
175 return editorValueToJson(receiptValue(result));
176 }
177 if (operation == "execute") {
178 const CommandId command(stringField(request, "command"));
179 std::optional<Result<TransactionReceipt>> result;
180 if (commands_->supportsPlanning(command)) {
181 auto planned = session_.planCommand(command, valueField(request, "payload"), CommandSource::Automation);
182 if (!planned.ok()) {
183 result.emplace(Result<TransactionReceipt>::failure(planned.status()));
184 } else {
185 result.emplace(session_.executePlan(planned.value(), valueField(request, "payload"),
186 CommandSource::Automation));
187 }
188 } else {
189 result.emplace(session_.executeCommandReceipt(command, valueField(request, "payload"),
190 CommandSource::Automation));
191 }
192 lastDiagnostics_ = result->diagnostics();
193 return editorValueToJson(receiptValue(*result));
194 }
195 if (operation == "cancel") {
196 auto result = session_.cancelRetainedPlan(PlanId(stringField(request, "planId")));
197 lastDiagnostics_ = result.diagnostics();
198 return editorValueToJson(resultValue(result.code(), result.diagnostics()));
199 }
200 if (operation == "undo" || operation == "redo") {
201 const TargetId target(stringField(request, "target"));
202 if (!target.empty()) {
203 auto result = operation == "undo" ? targets_->undo(target) : targets_->redo(target);
204 lastDiagnostics_ = result.diagnostics();
205 return editorValueToJson(receiptValue(result));
206 }
207 const bool changed = operation == "undo" ? session_.transactions().undo() : session_.transactions().redo();
208 EditorValue response = resultValue(changed ? EditorStatus::Applied : EditorStatus::NoOp, {});
209 (*response.getIf<EditorValue::Object>())["changed"] = changed;
210 return editorValueToJson(response);
211 }
212 if (operation == "diagnostics")
213 return editorValueToJson(resultValue(EditorStatus::Applied, lastDiagnostics_));
214 return errorJson(EditorStatus::Unsupported, "editor.automation.unsupported-operation",
215 "Unsupported editor automation operation: " + operation);
216}
217
218std::string EditorAutomationProvider::startObservation(const EditorValue::Object& request) {
219 const EditorValue* descriptorValue = findField(request, "descriptor");
220 const EditorValue* eventValue = findField(request, "event");
221 const auto* descriptor = descriptorValue ? descriptorValue->getIf<EditorValue::Object>() : nullptr;
222 const auto* event = eventValue ? eventValue->getIf<EditorValue::Object>() : nullptr;
223 if (!descriptor || !event)
224 return errorJson(EditorStatus::Rejected, "editor.automation.observe-start",
225 "Observation descriptor and initial event must be JSON objects");
226
227 const std::string id = "editor.observe." + std::to_string(nextObservationSession_++);
228 auto session = std::make_unique<ObservationSession>();
229 session->descriptor = *descriptor;
230 session->distinct.reset(session->source.distinctUntilChanged());
231 ObservationSession* observed = session.get();
232 session->subscription = session->distinct->subscribe(
233 [observed](const std::string& value) { observed->pending.push_back(value); });
234 session->source.onNext(editorValueToJson(*eventValue));
235 // startObservation returns the initial event directly. Retain it only as
236 // the distinct baseline so the first poll does not deliver it twice.
237 session->pending.clear();
238 observationSessions_.emplace(id, std::move(session));
239
240 EditorValue response = resultValue(EditorStatus::Applied, {});
241 auto* object = response.getIf<EditorValue::Object>();
242 (*object)["sessionId"] = id;
243 (*object)["event"] = *eventValue;
244 return editorValueToJson(response);
245}
246
247std::string EditorAutomationProvider::describeObservation(const EditorValue::Object& request) const {
248 const std::string id = stringField(request, "sessionId");
249 const auto found = observationSessions_.find(id);
250 if (found == observationSessions_.end())
251 return errorJson(EditorStatus::NotFound, "editor.automation.observe-session", "Observation session not found");
252 EditorValue response = resultValue(EditorStatus::Applied, {});
253 (*response.getIf<EditorValue::Object>())["descriptor"] = found->second->descriptor;
254 return editorValueToJson(response);
255}
256
257std::string EditorAutomationProvider::publishObservation(const EditorValue::Object& request) {
258 const std::string id = stringField(request, "sessionId");
259 const auto found = observationSessions_.find(id);
260 if (found == observationSessions_.end())
261 return errorJson(EditorStatus::NotFound, "editor.automation.observe-session", "Observation session not found");
262 const EditorValue* event = findField(request, "event");
263 if (!event || event->type() != EditorValue::Type::Object)
264 return errorJson(EditorStatus::Rejected, "editor.automation.observe-event", "Observation event must be an object");
265 found->second->source.onNext(editorValueToJson(*event));
266 return editorValueToJson(resultValue(EditorStatus::Applied, {}));
267}
268
269std::string EditorAutomationProvider::pollObservation(const EditorValue::Object& request) {
270 const std::string id = stringField(request, "sessionId");
271 const auto found = observationSessions_.find(id);
272 if (found == observationSessions_.end())
273 return errorJson(EditorStatus::NotFound, "editor.automation.observe-session", "Observation session not found");
275 for (const std::string& encoded : found->second->pending) {
276 Result<EditorValue> parsed = editorValueFromJson(encoded);
277 if (parsed.ok()) events.push_back(std::move(parsed).takeValue());
278 }
279 found->second->pending.clear();
280 EditorValue response = resultValue(EditorStatus::Applied, {});
281 auto* object = response.getIf<EditorValue::Object>();
282 (*object)["sessionId"] = id;
283 (*object)["events"] = std::move(events);
284 return editorValueToJson(response);
285}
286
287std::string EditorAutomationProvider::closeObservation(const EditorValue::Object& request) {
288 const std::string id = stringField(request, "sessionId");
289 const auto found = observationSessions_.find(id);
290 if (found == observationSessions_.end())
291 return errorJson(EditorStatus::NotFound, "editor.automation.observe-session", "Observation session not found");
292 found->second->subscription.dispose();
293 observationSessions_.erase(found);
294 EditorValue response = resultValue(EditorStatus::Applied, {});
295 (*response.getIf<EditorValue::Object>())["sessionId"] = id;
296 return editorValueToJson(response);
297}
298
299std::string EditorAutomationProvider::targetList() const {
300 EditorValue response = resultValue(EditorStatus::Applied, {});
301 auto* value = response.getIf<EditorValue::Object>();
302 if (!value) return errorJson(EditorStatus::Failed, "editor.automation.target-list", "Could not build response");
303
304 const TargetId bound = session_.boundTargetId();
306 for (const auto& summary : targets_->targets()) {
308 item["id"] = summary.id;
309 item["bound"] = summary.id == bound.value();
310 item["owned"] = ownedTargets_.contains(TargetId(summary.id));
311 item["revision"] = static_cast<std::int64_t>(summary.revision);
312 item["generation"] = static_cast<std::int64_t>(summary.generation);
313 if (!summary.type.empty()) item["type"] = summary.type;
314 entries.emplace_back(std::move(item));
315 }
316 (*value)["targets"] = std::move(entries);
317 if (!bound.empty()) (*value)["boundTarget"] = bound.value();
318
319 // Type discovery: the accepted names come from the loaded factories, so the
320 // advertised list cannot drift from what create actually accepts.
321 std::vector<std::string> types;
322 eve::cap::forEach<IEditorAutomationTargetFactory>([&](IEditorAutomationTargetFactory* factory) {
323 if (!factory) return;
324 for (const std::string_view type : factory->types()) types.emplace_back(type);
325 });
326 std::sort(types.begin(), types.end());
327 types.erase(std::unique(types.begin(), types.end()), types.end());
328 EditorValue::Array advertised;
329 advertised.reserve(types.size());
330 for (auto& type : types) advertised.emplace_back(std::move(type));
331 (*value)["supportedTypes"] = std::move(advertised);
332
333 return editorValueToJson(response);
334}
335
336std::string EditorAutomationProvider::createTarget(const EditorValue::Object& request) {
337 const TargetId target(stringField(request, "target"));
338 const std::string type = stringField(request, "type");
339 if (target.empty())
340 return errorJson(EditorStatus::Rejected, "editor.automation.target-id", "Target id must not be empty");
341 if (ownedTargets_.contains(target))
342 return errorJson(EditorStatus::Conflict, "editor.automation.target-exists", "Target already exists");
343
344 std::optional<Result<AutomationOwnedTarget>> created;
345 const bool handled = eve::cap::forEachUntil<IEditorAutomationTargetFactory>(
346 [&](IEditorAutomationTargetFactory* factory) {
347 if (!factory->supports(type)) return false;
348 created.emplace(factory->create(target, type, request));
349 return true;
350 });
351 if (!handled) {
352 return errorJson(EditorStatus::Unsupported, "editor.automation.target-type",
353 "No loaded editor adapter supports target type: " + type);
354 }
355 lastDiagnostics_ = created->diagnostics();
356 if (!created->ok() || !created->value().target)
357 return editorValueToJson(resultValue(created->code(), created->diagnostics()));
358
359 auto registered = targets_->registerTarget(*created->value().target);
360 lastDiagnostics_ = registered.diagnostics();
361 if (!registered.ok()) return editorValueToJson(resultValue(registered.code(), registered.diagnostics()));
362 ownedTargets_.emplace(target, std::move(*created).takeValue());
363 EditorValue response = resultValue(EditorStatus::Applied, {});
364 auto* value = response.getIf<EditorValue::Object>();
365 (*value)["target"] = target.value();
366 (*value)["type"] = type;
367 return editorValueToJson(response);
368}
369
370std::string EditorAutomationProvider::closeTarget(const EditorValue::Object& request) {
371 const TargetId target(stringField(request, "target"));
372 auto found = ownedTargets_.find(target);
373 if (found == ownedTargets_.end())
374 return errorJson(EditorStatus::NotFound, "editor.automation.target-not-found",
375 "Owned automation target does not exist");
376 auto unregistered = targets_->unregisterTarget(target);
377 lastDiagnostics_ = unregistered.diagnostics();
378 if (!unregistered.ok())
379 return editorValueToJson(resultValue(unregistered.code(), unregistered.diagnostics()));
381 ownedTargets_.erase(found);
382 return editorValueToJson(resultValue(EditorStatus::Applied, {}));
383}
384
385void EditorAutomationProvider::refreshProfile() {
386 HostProfile profile = HostProfile::automation();
387 if (commands_)
388 for (const CommandDescriptor& command : commands_->commands(HostProfile::developer()))
389 if (command.automationAllowed && profile.hasFeatures(command.requiredFeatures))
390 profile.allowCommand(command.id);
391 session_.setHostProfile(std::move(profile));
392}
393
394std::string EditorAutomationProvider::commandsJson() {
395 EditorValue::Array descriptors;
396 for (const CommandDescriptor& command : session_.availableCommands()) {
398 descriptor["id"] = command.id.value();
399 descriptor["displayName"] = command.displayName;
400 descriptor["category"] = command.category;
401 descriptor["owner"] = command.ownerModule;
402 descriptor["planned"] = commands_->supportsPlanning(command.id);
403 descriptors.emplace_back(std::move(descriptor));
404 }
405 EditorValue response = resultValue(EditorStatus::Applied, {});
406 (*response.getIf<EditorValue::Object>())["commands"] = EditorValue(std::move(descriptors));
407 return editorValueToJson(response);
408}
409
410Result<void> EditorAutomationProvider::bindRequestedTarget(const EditorValue::Object& request) {
411 const std::string target = stringField(request, "target");
412 if (target.empty()) return eve::editing::applied<void>();
413 return targets_->bind(session_, TargetId(target));
414}
415
416} // namespace eve::editor
LogicalId target
ActionParameterOperation operation
double value
std::string descriptor
std::vector< QuestEvent > pending
std::unordered_map< std::string, QuestRuntime > entries
std::map< std::string, Var > values
std::string message
const GltfImportRequest & request
std::uint32_t key
wgpu::PopErrorScopeStatus status
std::int32_t second
std::string text
std::string id
Definition PlayHost.cpp:108
bool found
int created
Battle::Events events
std::optional< InteractionSession > session
Definition Tactics.cpp:39
std::vector< VegetationPresetCommand > commands
Move-only operation result carrying either a value or Status.
Definition Result.h:155
const T & value() const &
Borrow the value from a const lvalue after checking success.
Definition Result.h:308
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Definition Result.h:255
UUID-backed identifier adapter for legacy textual boundaries.
Definition Identity.h:314
const std::string & value() const noexcept
Returns the exact legacy or canonical compatibility spelling.
Definition Identity.h:362
Deterministic owning value tree shared by authoring hosts.
std::vector< Value > Array
std::map< std::string, Value > Object
const T * getIf() const
Return an immediately borrowed typed value.
~EditorAutomationProvider() override
Releases EditorAutomationProvider resources.
EditorAutomationProvider(EditorCommandService &commands, EditorTargetCoordinator &targets)
Bind borrowed services that outlive this provider.
void targetUnregistered(const TargetId &target)
Clear automation state that still borrows an unregistered target.
std::string invoke(const std::string &operation, const std::string &requestJson) override
Invoke discovery, inspect, execute, plan, commit, undo or redo.
Registry and execution gate for UI-, script- and automation-neutral commands.
bool supportsPlanning(const CommandId &id) const
Return whether the registered command requires plan/commit execution.
void clearTarget()
Clear the active target and invalidate retained plans derived from it.
void setCommandService(EditorCommandService *service)
Compatibility-only nullable pointer facade over bindCommandService() and clearCommandService().
EditorTransactions & transactions()
Transactions.
Result< void > cancelRetainedPlan(const PlanId &id)
Discard one retained plan without executing it.
Result< TransactionReceipt > executeCommandReceipt(const CommandId &id, const EditorValue &payload={}, CommandSource source=CommandSource::Api)
Execute a legacy-compatible command and return a V2 transaction receipt.
void setSessionId(SessionId id)
Assign the stable identity included in asynchronous context snapshots.
void clearRetainedPlans()
Discard all retained plans, for example after replacing a document or target.
Result< CommandPlan > planCommand(const CommandId &id, const EditorValue &payload={}, CommandSource source=CommandSource::Api, std::optional< Revision > expectedRevision=std::nullopt) const
Plan a registered command without applying target mutations.
void setHostProfile(HostProfile profile)
Set the host capability boundary used for command discovery and execution.
Result< TransactionReceipt > executeRetainedPlan(const PlanId &id, CommandSource source=CommandSource::Api)
Execute a retained plan. Successful plans are consumed exactly once.
const TargetId & boundTargetId() const noexcept
Return the identity of the current binding without dereferencing the target.
Result< TransactionReceipt > executePlan(const CommandPlan &plan, const EditorValue &payload={}, CommandSource source=CommandSource::Api)
Execute a previously returned plan through its registered executor.
Result< PlanId > retainPlan(const CommandId &id, const EditorValue &payload={}, CommandSource source=CommandSource::Api, std::optional< Revision > expectedRevision=std::nullopt)
Plan a command and retain its immutable payload under the returned plan id.
Coordinates editable targets, per-target histories and domain commands.
Result< TransactionReceipt > redo(const TargetId &target)
Redo.
Result< void > unregisterTarget(const TargetId &target)
Unregisters target.
Result< EditorValue > inspect(const TargetId &target) const
Inspect.
Result< void > bind(EditorSession &session, const TargetId &target)
Binds .
Result< TransactionReceipt > undo(const TargetId &target)
Undo.
Result< void > registerTarget(IEditableTarget &target)
Registers target.
bool undo()
Compatibility-only boolean projection of undoTransaction().
bool redo()
Compatibility-only boolean projection of redoTransaction().
static HostProfile automation()
Headless profile that denies commands unless allow-listed.
Multicast push-based stream: both an Observable and a push source. Thread-safe: onNext/onError/onComp...
Definition Rx.h:399
RAII-style dispose handle for an active stream subscription. Disposing unsubscribes from the source; ...
Definition Rx.h:93
constexpr eve::DiagnosticCode diagnosticCodeForStatus(Status status) noexcept
Coarse default diagnostic category for a failure status.
std::string editorValueToJson(const EditorValue &value)
Serialize an EditorValue to deterministic compact JSON.
Result< EditorValue > editorValueFromJson(const std::string &json)
Parse JSON into the pointer-free EditorValue protocol tree.
RuleId diagnosticRule(const Diagnostic &diagnostic)
Return the projected editing rule, or an empty RuleId when absent.
Diagnostic ruleDiagnostic(eve::DiagnosticCode code, RuleId rule, DiagnosticSeverity severity, std::string message)
Build a common diagnostic carrying an open editing rule identity.
eve::editing::Value EditorValue
Compatibility alias for the UI-independent authoring value tree.
Definition EditorValue.h:8
eve::editing::PlanId PlanId
Definition EditorIds.h:35
eve::editing::SessionId SessionId
Definition EditorIds.h:28
eve::editing::TransactionState TransactionState
eve::editing::CommandId CommandId
Definition EditorIds.h:30
eve::editing::TargetId TargetId
Definition EditorIds.h:29
eve::Diagnostic EditorDiagnostic
eve::editing::RuleId RuleId
Definition EditorIds.h:32
eve::StatusCode EditorStatus
Definition EditorResult.h:8
eve::editing::Revision Revision
std::string_view statusName(BattleStatus status) noexcept
Return the stable protocol spelling of a battle status.
std::unique_ptr< rx::Observable< std::string > > distinct