载入中...
搜索中...
未找到
RenderVision.cpp
浏览该文件的文档.
3
5
6#include <Poco/Base64Encoder.h>
7#include <Poco/Exception.h>
8#include <Poco/JSON/Array.h>
9#include <Poco/JSON/Object.h>
10#include <Poco/JSON/Parser.h>
11#include <Poco/JSON/Stringifier.h>
12#include <Poco/Net/HTTPClientSession.h>
13#include <Poco/Net/HTTPRequest.h>
14#include <Poco/Net/HTTPResponse.h>
15#include <Poco/Net/NetException.h>
16#include <Poco/Net/SocketAddress.h>
17#include <Poco/Timespan.h>
18
19#include <cstdlib>
20#include <iterator>
21#include <sstream>
22
23namespace eve::dev {
24
25namespace {
26
27const char* kSystemPrompt =
28 "You are a rendering debug assistant integrated with the EVEngine game engine. "
29 "You receive a screenshot of the current rendered frame plus a block of engine "
30 "render parameters. Respond in concise English prose aimed at another LLM agent. "
31 "Cover: (1) what is visibly rendered (scene content, objects, colors, lighting, "
32 "UI overlays, any artifacts or anomalies); (2) how the provided render parameters "
33 "relate to the visible result (resolution, whether a 3D scene is active, readback "
34 "state, render-pipeline event counts); (3) any anomalies (black screen, missing "
35 "geometry, wrong colors, flicker) and the parameter likely responsible. Be specific "
36 "and reference the parameter names verbatim.";
37
38// Splits "http://host[:port][/basepath]" into host, port and base path.
39void splitBaseUrl(const std::string& url, std::string& host, Poco::UInt16& port,
40 std::string& basePath) {
41 host = "127.0.0.1";
42 port = 80;
43 basePath = "";
44 std::string rest = url;
45 const auto scheme = rest.find("://");
46 if (scheme != std::string::npos) rest = rest.substr(scheme + 3);
47 const auto slash = rest.find('/');
48 std::string authority = (slash == std::string::npos) ? rest : rest.substr(0, slash);
49 if (slash != std::string::npos) basePath = rest.substr(slash);
50 // Strip trailing slash from basePath.
51 while (basePath.size() > 1 && basePath.back() == '/') basePath.pop_back();
52 const auto colon = authority.rfind(':');
53 if (colon != std::string::npos) {
54 host = authority.substr(0, colon);
55 try {
56 port = static_cast<Poco::UInt16>(std::stoi(authority.substr(colon + 1)));
57 } catch (...) {
58 port = 80;
59 }
60 } else {
61 host = authority;
62 }
63 if (host.empty()) host = "127.0.0.1";
64}
65
66} // namespace
67
69 // Process-immortal singleton; see devtools/Immortal.hpp.
71}
72
73void RenderVision::ensureEnvLocked() {
74 if (envLoaded_) return;
75 envLoaded_ = true;
76 if (const char* v = std::getenv("EVE_VISION_BASE_URL")) baseUrl_ = v;
77 if (const char* v = std::getenv("EVE_VISION_API_KEY")) apiKey_ = v;
78 if (const char* v = std::getenv("EVE_VISION_MODEL")) model_ = v;
79 if (const char* v = std::getenv("EVE_VISION_PATH")) path_ = v;
80 if (const char* v = std::getenv("EVE_VISION_TIMEOUT_MS")) {
81 try {
82 timeoutMs_ = std::stoi(v);
83 } catch (...) {
84 }
85 }
86}
87
88void RenderVision::setBaseUrl(std::string url) {
89 std::lock_guard<std::mutex> lock(mu_);
90 ensureEnvLocked();
91 baseUrl_ = std::move(url);
92}
93void RenderVision::setApiKey(std::string key) {
94 std::lock_guard<std::mutex> lock(mu_);
95 ensureEnvLocked();
96 apiKey_ = std::move(key);
97}
98void RenderVision::setModel(std::string model) {
99 std::lock_guard<std::mutex> lock(mu_);
100 ensureEnvLocked();
101 model_ = std::move(model);
102}
103void RenderVision::setPath(std::string path) {
104 std::lock_guard<std::mutex> lock(mu_);
105 ensureEnvLocked();
106 if (path.empty()) path = "/chat/completions";
107 if (path.front() != '/') path.insert(path.begin(), '/');
108 path_ = std::move(path);
109}
111 std::lock_guard<std::mutex> lock(mu_);
112 ensureEnvLocked();
113 if (ms > 0) timeoutMs_ = ms;
114}
115
117 std::lock_guard<std::mutex> lock(mu_);
118 ensureEnvLocked();
119 return !baseUrl_.empty() && !model_.empty();
120}
121
123 std::lock_guard<std::mutex> lock(mu_);
124 ensureEnvLocked();
125 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
126 o->set("baseUrl", baseUrl_);
127 o->set("model", model_);
128 o->set("path", path_);
129 o->set("timeoutMs", timeoutMs_);
130 o->set("apiKeySet", !apiKey_.empty());
131 std::ostringstream oss;
132 Poco::JSON::Stringifier::stringify(Poco::Dynamic::Var(o), oss, 0, 0);
133 return oss.str();
134}
135
136std::string RenderVision::latest() const {
137 std::lock_guard<std::mutex> lock(mu_);
138 return latest_;
139}
140
141std::string RenderVision::lastError() const {
142 std::lock_guard<std::mutex> lock(mu_);
143 return lastError_;
144}
145
146void RenderVision::notifyPending(const std::string& reason, const std::string& source, int line) {
147 std::lock_guard<std::mutex> lock(mu_);
148 pendingReason_ = reason.empty() ? "breakpoint" : reason;
149 pendingLoc_ = source + ":" + std::to_string(line);
150 pending_.store(true);
151}
152
153bool RenderVision::pending() const { return pending_.load(); }
154
155std::string RenderVision::pendingReason() const {
156 std::lock_guard<std::mutex> lock(mu_);
157 return pendingReason_;
158}
159
160std::string RenderVision::describe(eve::IRenderCapture* cap, const std::string& renderDataJson, bool fresh,
161 const std::string& reason) {
162 {
163 std::lock_guard<std::mutex> lock(mu_);
164 ensureEnvLocked();
165 if (!fresh && !latest_.empty()) return latest_;
166 }
167 if (!cap) return "error: Graphics module not available for vision describe";
168 return doDescribe(cap, renderDataJson, reason);
169}
170
171void RenderVision::pollPending(eve::IRenderCapture* cap, const std::string& renderDataJson) {
172 if (!pending_.load() || !cap) return;
173 std::string reason;
174 {
175 std::lock_guard<std::mutex> lock(mu_);
176 reason = pendingReason_;
177 }
178 // clear first so a failed dump does not loop; doDescribe caches result/error.
179 pending_.store(false);
180 doDescribe(cap, renderDataJson, reason);
181}
182
183std::string RenderVision::doDescribe(eve::IRenderCapture* cap, const std::string& renderDataJson,
184 const std::string& reason) {
185 std::string baseUrl, apiKey, model, path;
186 int timeoutMs = timeoutMs_;
187 {
188 std::lock_guard<std::mutex> lock(mu_);
189 ensureEnvLocked();
190 baseUrl = baseUrl_;
191 apiKey = apiKey_;
192 model = model_;
193 path = path_;
194 timeoutMs = timeoutMs_;
195 }
196 if (baseUrl.empty() || model.empty()) {
197 const std::string err = "error: RenderVision not configured (set EVE_VISION_BASE_URL/MODEL or call eve_render_vision_config)";
198 std::lock_guard<std::mutex> lock(mu_);
199 lastError_ = err;
200 return err;
201 }
202
203 // --- Capture frame to PNG bytes in memory (via the render-capture interface) ---
204 const std::string dataUrl = cap->capturePngDataUrl();
205 if (dataUrl.empty()) {
206 const std::string err = "error: frame readback returned no image";
207 std::lock_guard<std::mutex> lock(mu_);
208 lastError_ = err;
209 return err;
210 }
211
212 // --- Build OpenAI-compatible chat/completions body ---
213 std::string context = reason.empty() ? "Engine render parameters:" : "Engine render parameters (dump reason: " + reason + "):";
214 if (!renderDataJson.empty()) context += "\n" + renderDataJson;
215
216 Poco::JSON::Object::Ptr root = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
217 root->set("model", model);
218 Poco::JSON::Array::Ptr messages = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
219
220 Poco::JSON::Object::Ptr sys = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
221 sys->set("role", "system");
222 sys->set("content", kSystemPrompt);
223 messages->add(sys);
224
225 Poco::JSON::Object::Ptr user = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
226 user->set("role", "user");
227 Poco::JSON::Array::Ptr uc = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
228
229 Poco::JSON::Object::Ptr textPart = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
230 textPart->set("type", "text");
231 textPart->set("text", context);
232 uc->add(textPart);
233
234 Poco::JSON::Object::Ptr imgPart = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
235 imgPart->set("type", "image_url");
236 Poco::JSON::Object::Ptr iu = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
237 iu->set("url", dataUrl);
238 imgPart->set("image_url", iu);
239 uc->add(imgPart);
240
241 user->set("content", uc);
242 messages->add(user);
243 root->set("messages", messages);
244 root->set("max_tokens", 1024);
245
246 std::ostringstream bodyStream;
247 Poco::JSON::Stringifier::stringify(Poco::Dynamic::Var(root), bodyStream, 0, 0);
248 const std::string body = bodyStream.str();
249
250 // --- HTTP POST ---
251 std::string host;
252 Poco::UInt16 port = 80;
253 std::string basePath;
254 splitBaseUrl(baseUrl, host, port, basePath);
255 std::string endpoint = basePath + path;
256 if (endpoint.empty()) endpoint = "/chat/completions";
257
258 std::string responseBody;
259 try {
260 Poco::Net::HTTPClientSession session(host, port);
261 session.setTimeout(
262 Poco::Timespan(static_cast<long long>(timeoutMs) * 1000));
263 session.setKeepAlive(false);
264 Poco::Net::HTTPRequest req(Poco::Net::HTTPRequest::HTTP_POST, endpoint,
265 Poco::Net::HTTPMessage::HTTP_1_1);
266 req.setContentType("application/json");
267 req.setChunkedTransferEncoding(false);
268 req.setContentLength(static_cast<int>(body.size()));
269 if (!apiKey.empty()) req.set("Authorization", "Bearer " + apiKey);
270 std::ostream& os = session.sendRequest(req);
271 os << body;
272 os.flush();
273 Poco::Net::HTTPResponse res;
274 std::istream& is = session.receiveResponse(res);
275 responseBody.assign(std::istreambuf_iterator<char>(is), std::istreambuf_iterator<char>());
276 if (res.getStatus() != Poco::Net::HTTPResponse::HTTP_OK) {
277 std::string snippet = responseBody.size() > 300 ? responseBody.substr(0, 300) : responseBody;
278 const std::string err = "error: vision HTTP " + std::to_string(res.getStatus()) + ": " + snippet;
279 std::lock_guard<std::mutex> lock(mu_);
280 lastError_ = err;
281 return err;
282 }
283 } catch (const Poco::Exception& e) {
284 const std::string err = std::string("error: vision request failed: ") + e.displayText();
285 std::lock_guard<std::mutex> lock(mu_);
286 lastError_ = err;
287 return err;
288 } catch (const std::exception& e) {
289 const std::string err = std::string("error: vision request failed: ") + e.what();
290 std::lock_guard<std::mutex> lock(mu_);
291 lastError_ = err;
292 return err;
293 }
294
295 // --- Parse response ---
296 try {
297 Poco::JSON::Parser parser;
298 auto var = parser.parse(responseBody);
299 auto obj = var.extract<Poco::JSON::Object::Ptr>();
300 auto choices = obj ? obj->getArray("choices") : nullptr;
301 if (!choices || choices->size() == 0) {
302 const std::string err = "error: vision response had no choices";
303 std::lock_guard<std::mutex> lock(mu_);
304 lastError_ = err;
305 return err;
306 }
307 auto c0 = choices->getObject(0);
308 auto msg = c0 ? c0->getObject("message") : nullptr;
309 std::string content = msg ? msg->optValue<std::string>("content", "") : "";
310 if (content.empty()) {
311 const std::string err = "error: vision response had empty content";
312 std::lock_guard<std::mutex> lock(mu_);
313 lastError_ = err;
314 return err;
315 }
316 std::lock_guard<std::mutex> lock(mu_);
317 latest_ = content;
318 lastError_.clear();
319 return content;
320 } catch (const Poco::Exception& e) {
321 const std::string err = std::string("error: vision response parse failed: ") + e.displayText();
322 std::lock_guard<std::mutex> lock(mu_);
323 lastError_ = err;
324 return err;
325 } catch (const std::exception& e) {
326 const std::string err = std::string("error: vision response parse failed: ") + e.what();
327 std::lock_guard<std::mutex> lock(mu_);
328 lastError_ = err;
329 return err;
330 }
331}
332
333} // namespace eve::dev
int root
Definition AnimSmr.cpp:119
std::uint32_t key
float v
std::set< std::string > choices
std::string path
Definition PlayHost.cpp:110
glm::mat4 model
std::optional< InteractionSession > session
Definition Tactics.cpp:39
UIHostHandle host
std::string body
const UnitySourceAsset & source
const VegetationPresetContext & context
Frame capture + camera + visible-entity inspection (graphics).
virtual std::string capturePngDataUrl()=0
Capture the last presented frame as a base64 PNG data URL.
Vision-model bridge for rendered-frame review (desktop / devtools only).
void notifyPending(const std::string &reason, const std::string &source, int line)
Record that a breakpoint / critical site wants a vision dump.
std::string configJson()
JSON description of current config (API key masked).
void setApiKey(std::string key)
std::string latest() const
void setModel(std::string model)
static RenderVision & instance()
std::string lastError() const
void pollPending(eve::IRenderCapture *cap, const std::string &renderDataJson)
McpServer::poll hook: performs one pending dump when safe, clears flag.
void setPath(std::string path)
std::string pendingReason() const
std::string describe(eve::IRenderCapture *cap, const std::string &renderDataJson, bool fresh, const std::string &reason={})
Main-thread capture + vision describe. Returns the description text, or a string starting with "error...
void setBaseUrl(std::string url)
static T & get()
Returns the process-lifetime instance.
Definition Immortal.hpp:21