Forge
csharpdeeb25a2
1using System.Text.Json;
2
3namespace CascadeIDE.Features.WebAiPortal.Application;
4
5/// <summary>
6/// Вытащить последнюю команду (<c>command_id</c> + args) со страницы чата вниз по порядку сбора.
7/// Учитывает: fenced ```json-cascade; голый языковой маркер <c>json-cascade</c> + перевод строки + JSON (частый вывод Gemini без backticks); <c>pre</c>/<code>;
8/// дополнительно тот же поиск по <see cref="innerText"/> корня документа (блок может жить не в pre/code).
9/// </summary>
10public static class WebAiPortalLastCommandDomProbe
11{
12 /// <summary>IIFE возвращает JSON-строку вида один объект или пустую строку (результат WebView сериализуется).</summary>
13 internal const string LastPayloadProbeScriptJavaScript =
14 """
15 (() => {
16 function extractFirstJsonObject(text) {
17 const i = text.indexOf('{');
18 if (i < 0) return null;
19 let depth = 0;
20 let inStr = false;
21 let esc = false;
22 for (let j = i; j < text.length; j++) {
23 const c = text[j];
24 if (inStr) {
25 if (esc) { esc = false; continue; }
26 if (c === '\\') { esc = true; continue; }
27 if (c === '"') { inStr = false; continue; }
28 continue;
29 }
30 if (c === '"') { inStr = true; continue; }
31 if (c === '{') depth++;
32 else if (c === '}') {
33 depth--;
34 if (depth === 0) return text.slice(i, j + 1);
35 }
36 }
37 return null;
38 }
39
40 function tryPushCommandJson(raw, arr) {
41 if (!raw) return;
42 let s = String(raw).trim();
43
44 const fm = s.match(/```json-cascade\s*\r?\n([\s\S]*?)```/);
45 if (fm) s = fm[1].trim();
46 else if (/^json-cascade\s*\r?\n/i.test(s))
47 s = s.replace(/^json-cascade\s*\r?\n/i, '').trim();
48
49 if (!s.startsWith('{')) return;
50 if (s.indexOf('"command_id"') < 0) return;
51 try {
52 const cand = extractFirstJsonObject(s) ?? s;
53 const o = JSON.parse(cand);
54 if (typeof o.command_id === 'string' && o.command_id)
55 arr.push(JSON.stringify(o));
56 } catch (e) {}
57 }
58
59 function collectDomElements(arr) {
60 document.querySelectorAll('pre').forEach(el =>
61 tryPushCommandJson(el.textContent, arr));
62 document.querySelectorAll('code').forEach(el => {
63 if (!el.closest('pre'))
64 tryPushCommandJson(el.textContent, arr);
65 });
66 }
67
68 function collectFromPlainText(chunk, arr) {
69 if (!chunk) return;
70 const reFence = /```json-cascade\s*\r?\n?([\s\S]*?)```/gi;
71 let m;
72 while ((m = reFence.exec(chunk)) !== null)
73 tryPushCommandJson(m[1], arr);
74
75 const needle = 'json-cascade';
76 const lower = chunk;
77 const low = lower.toLowerCase();
78 for (let i = 0; i < low.length;) {
79 const idx = low.indexOf(needle, i);
80 if (idx < 0) break;
81 const before = idx === 0 ? ' ' : lower[idx - 1];
82 const afterPos = idx + needle.length;
83 const after = afterPos < lower.length ? lower[afterPos] : ' ';
84 const wordish = /[0-9A-Za-z_]/;
85 if (!wordish.test(before) && !wordish.test(after))
86 tryPushCommandJson(lower.slice(idx), arr);
87 i = idx + 1;
88 }
89 }
90
91 const payloads = [];
92 collectDomElements(payloads);
93 const bodyText = document.body ? document.body.innerText : '';
94 collectFromPlainText(bodyText, payloads);
95
96 return payloads.length ? payloads[payloads.length - 1] : '';
97 })()
98 """;
99
100 /// <summary>Снять одну оболочку JSON-string с результата <see cref="Avalonia.Controls.NativeWebView.InvokeScript"/> (WebView2).</summary>
101 public static string? UnwrapWrappedJsonString(string? invokeScriptResult)
102 {
103 if (string.IsNullOrWhiteSpace(invokeScriptResult))
104 return invokeScriptResult?.Trim();
105 var t = invokeScriptResult.Trim();
106 if (t.Length >= 2 && t[0] == '"' && t[^1] == '"')
107 {
108 try
109 {
110 return JsonSerializer.Deserialize<string>(t);
111 }
112 catch (JsonException)
113 {
114 return t;
115 }
116 }
117
118 return t;
119 }
120}
121
View only · write via MCP/CIDE