载入中...
搜索中...
未找到
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, unsigned& 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<unsigned>(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
66std::string base64Encode(const void* data, size_t size) {
67 std::ostringstream oss;
68 {
69 Poco::Base64Encoder enc(oss);
70 enc.write(static_cast<const char*>(data), static_cast<std::streamsize>(size));
71 enc.close();
72 }
73 return oss.str();
74}
75
76} // namespace
77
79 // Process-immortal singleton; see devtools/Immortal.hpp.
81}
82
83void RenderVision::ensureEnvLocked() {
84 if (envLoaded_) return;
85 envLoaded_ = true;
86 if (const char* v = std::getenv("EVE_VISION_BASE_URL")) baseUrl_ = v;
87 if (const char* v = std::getenv("EVE_VISION_API_KEY")) apiKey_ = v;
88 if (const char* v = std::getenv("EVE_VISION_MODEL")) model_ = v;
89 if (const char* v = std::getenv("EVE_VISION_PATH")) path_ = v;
90 if (const char* v = std::getenv("EVE_VISION_TIMEOUT_MS")) {
91 try {
92 timeoutMs_ = std::stoi(v);
93 } catch (...) {
94 }
95 }
96}
97
98void RenderVision::setBaseUrl(std::string url) {
99 std::lock_guard<std::mutex> lock(mu_);
100 ensureEnvLocked();
101 baseUrl_ = std::move(url);
102}
103void RenderVision::setApiKey(std::string key) {
104 std::lock_guard<std::mutex> lock(mu_);
105 ensureEnvLocked();
106 apiKey_ = std::move(key);
107}
108void RenderVision::setModel(std::string model) {
109 std::lock_guard<std::mutex> lock(mu_);
110 ensureEnvLocked();
111 model_ = std::move(model);
112}
113void RenderVision::setPath(std::string path) {
114 std::lock_guard<std::mutex> lock(mu_);
115 ensureEnvLocked();
116 if (path.empty()) path = "/chat/completions";
117 if (path.front() != '/') path.insert(path.begin(), '/');
118 path_ = std::move(path);
119}
121 std::lock_guard<std::mutex> lock(mu_);
122 ensureEnvLocked();
123 if (ms > 0) timeoutMs_ = ms;
124}
125
127 std::lock_guard<std::mutex> lock(mu_);
128 ensureEnvLocked();
129 return !baseUrl_.empty() && !model_.empty();
130}
131
133 std::lock_guard<std::mutex> lock(mu_);
134 ensureEnvLocked();
135 Poco::JSON::Object::Ptr o = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
136 o->set("baseUrl", baseUrl_);
137 o->set("model", model_);
138 o->set("path", path_);
139 o->set("timeoutMs", timeoutMs_);
140 o->set("apiKeySet", !apiKey_.empty());
141 std::ostringstream oss;
142 Poco::JSON::Stringifier::stringify(Poco::Dynamic::Var(o), oss, 0, 0);
143 return oss.str();
144}
145
146std::string RenderVision::latest() const {
147 std::lock_guard<std::mutex> lock(mu_);
148 return latest_;
149}
150
151std::string RenderVision::lastError() const {
152 std::lock_guard<std::mutex> lock(mu_);
153 return lastError_;
154}
155
156void RenderVision::notifyPending(const std::string& reason, const std::string& source, int line) {
157 std::lock_guard<std::mutex> lock(mu_);
158 pendingReason_ = reason.empty() ? "breakpoint" : reason;
159 pendingLoc_ = source + ":" + std::to_string(line);
160 pending_.store(true);
161}
162
163bool RenderVision::pending() const { return pending_.load(); }
164
165std::string RenderVision::pendingReason() const {
166 std::lock_guard<std::mutex> lock(mu_);
167 return pendingReason_;
168}
169
170std::string RenderVision::describe(eve::IRenderCapture* cap, const std::string& renderDataJson, bool fresh,
171 const std::string& reason) {
172 {
173 std::lock_guard<std::mutex> lock(mu_);
174 ensureEnvLocked();
175 if (!fresh && !latest_.empty()) return latest_;
176 }
177 if (!cap) return "error: Graphics module not available for vision describe";
178 return doDescribe(cap, renderDataJson, reason);
179}
180
181void RenderVision::pollPending(eve::IRenderCapture* cap, const std::string& renderDataJson) {
182 if (!pending_.load() || !cap) return;
183 std::string reason;
184 {
185 std::lock_guard<std::mutex> lock(mu_);
186 reason = pendingReason_;
187 }
188 // clear first so a failed dump does not loop; doDescribe caches result/error.
189 pending_.store(false);
190 doDescribe(cap, renderDataJson, reason);
191}
192
193std::string RenderVision::doDescribe(eve::IRenderCapture* cap, const std::string& renderDataJson,
194 const std::string& reason) {
195 std::string baseUrl, apiKey, model, path;
196 int timeoutMs = timeoutMs_;
197 {
198 std::lock_guard<std::mutex> lock(mu_);
199 ensureEnvLocked();
200 baseUrl = baseUrl_;
201 apiKey = apiKey_;
202 model = model_;
203 path = path_;
204 timeoutMs = timeoutMs_;
205 }
206 if (baseUrl.empty() || model.empty()) {
207 const std::string err = "error: RenderVision not configured (set EVE_VISION_BASE_URL/MODEL or call eve_render_vision_config)";
208 std::lock_guard<std::mutex> lock(mu_);
209 lastError_ = err;
210 return err;
211 }
212
213 // --- Capture frame to PNG bytes in memory (via the render-capture interface) ---
214 const std::string dataUrl = cap->capturePngDataUrl();
215 if (dataUrl.empty()) {
216 const std::string err = "error: frame readback returned no image";
217 std::lock_guard<std::mutex> lock(mu_);
218 lastError_ = err;
219 return err;
220 }
221
222 // --- Build OpenAI-compatible chat/completions body ---
223 std::string context = reason.empty() ? "Engine render parameters:" : "Engine render parameters (dump reason: " + reason + "):";
224 if (!renderDataJson.empty()) context += "\n" + renderDataJson;
225
226 Poco::JSON::Object::Ptr root = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
227 root->set("model", model);
228 Poco::JSON::Array::Ptr messages = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
229
230 Poco::JSON::Object::Ptr sys = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
231 sys->set("role", "system");
232 sys->set("content", kSystemPrompt);
233 messages->add(sys);
234
235 Poco::JSON::Object::Ptr user = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
236 user->set("role", "user");
237 Poco::JSON::Array::Ptr uc = Poco::JSON::Array::Ptr(new Poco::JSON::Array());
238
239 Poco::JSON::Object::Ptr textPart = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
240 textPart->set("type", "text");
241 textPart->set("text", context);
242 uc->add(textPart);
243
244 Poco::JSON::Object::Ptr imgPart = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
245 imgPart->set("type", "image_url");
246 Poco::JSON::Object::Ptr iu = Poco::JSON::Object::Ptr(new Poco::JSON::Object());
247 iu->set("url", dataUrl);
248 imgPart->set("image_url", iu);
249 uc->add(imgPart);
250
251 user->set("content", uc);
252 messages->add(user);
253 root->set("messages", messages);
254 root->set("max_tokens", 1024);
255
256 std::ostringstream bodyStream;
257 Poco::JSON::Stringifier::stringify(Poco::Dynamic::Var(root), bodyStream, 0, 0);
258 const std::string body = bodyStream.str();
259
260 // --- HTTP POST ---
261 std::string host;
262 unsigned port = 80;
263 std::string basePath;
264 splitBaseUrl(baseUrl, host, port, basePath);
265 std::string endpoint = basePath + path;
266 if (endpoint.empty()) endpoint = "/chat/completions";
267
268 std::string responseBody;
269 try {
270 Poco::Net::HTTPClientSession session(host, port);
271 session.setTimeout(Poco::Timespan(timeoutMs / 1000, (timeoutMs % 1000) * 1000));
272 session.setKeepAlive(false);
273 Poco::Net::HTTPRequest req(Poco::Net::HTTPRequest::HTTP_POST, endpoint,
274 Poco::Net::HTTPMessage::HTTP_1_1);
275 req.setContentType("application/json");
276 req.setChunkedTransferEncoding(false);
277 req.setContentLength(static_cast<int>(body.size()));
278 if (!apiKey.empty()) req.set("Authorization", "Bearer " + apiKey);
279 std::ostream& os = session.sendRequest(req);
280 os << body;
281 os.flush();
282 Poco::Net::HTTPResponse res;
283 std::istream& is = session.receiveResponse(res);
284 responseBody.assign(std::istreambuf_iterator<char>(is), std::istreambuf_iterator<char>());
285 if (res.getStatus() != Poco::Net::HTTPResponse::HTTP_OK) {
286 std::string snippet = responseBody.size() > 300 ? responseBody.substr(0, 300) : responseBody;
287 const std::string err = "error: vision HTTP " + std::to_string(res.getStatus()) + ": " + snippet;
288 std::lock_guard<std::mutex> lock(mu_);
289 lastError_ = err;
290 return err;
291 }
292 } catch (const Poco::Exception& e) {
293 const std::string err = std::string("error: vision request failed: ") + e.displayText();
294 std::lock_guard<std::mutex> lock(mu_);
295 lastError_ = err;
296 return err;
297 } catch (const std::exception& e) {
298 const std::string err = std::string("error: vision request failed: ") + e.what();
299 std::lock_guard<std::mutex> lock(mu_);
300 lastError_ = err;
301 return err;
302 }
303
304 // --- Parse response ---
305 try {
306 Poco::JSON::Parser parser;
307 auto var = parser.parse(responseBody);
308 auto obj = var.extract<Poco::JSON::Object::Ptr>();
309 auto choices = obj ? obj->getArray("choices") : nullptr;
310 if (!choices || choices->size() == 0) {
311 const std::string err = "error: vision response had no choices";
312 std::lock_guard<std::mutex> lock(mu_);
313 lastError_ = err;
314 return err;
315 }
316 auto c0 = choices->getObject(0);
317 auto msg = c0 ? c0->getObject("message") : nullptr;
318 std::string content = msg ? msg->optValue<std::string>("content", "") : "";
319 if (content.empty()) {
320 const std::string err = "error: vision response had empty content";
321 std::lock_guard<std::mutex> lock(mu_);
322 lastError_ = err;
323 return err;
324 }
325 std::lock_guard<std::mutex> lock(mu_);
326 latest_ = content;
327 lastError_.clear();
328 return content;
329 } catch (const Poco::Exception& e) {
330 const std::string err = std::string("error: vision response parse failed: ") + e.displayText();
331 std::lock_guard<std::mutex> lock(mu_);
332 lastError_ = err;
333 return err;
334 } catch (const std::exception& e) {
335 const std::string err = std::string("error: vision response parse failed: ") + e.what();
336 std::lock_guard<std::mutex> lock(mu_);
337 lastError_ = err;
338 return err;
339 }
340}
341
342} // namespace eve::dev
int line
JobFunc body
std::ostringstream & os
glm::mat4 model
int v
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