Skip to content

Commit 735656b

Browse files
authored
Merge pull request #41 from visduo/main
适配SolonCode Studio
2 parents cadf772 + a816c35 commit 735656b

4 files changed

Lines changed: 311 additions & 15 deletions

File tree

soloncode-cli/src/main/resources/static/js/app-history.js

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
/* 自定义 composing 标志,替代 e.isComposing(macOS 输入法组合态下 Enter 时序问题) */
99
var composing = false;
1010

11+
function isInputComposing(event) {
12+
return composing || !!(event && (event.isComposing || event.keyCode === 229));
13+
}
14+
1115
var ACTIVE_SESSION_KEY = 'soloncode-active-session';
1216
function rememberActiveSession(sessionId) {
1317
try { if (sessionId) localStorage.setItem(ACTIVE_SESSION_KEY, sessionId); } catch (e) {}
@@ -374,11 +378,11 @@ function applyCmdSelection(inputEl, completeEl) {
374378
if (cmdActiveIndex >= 0 && cmdActiveIndex < cmdVisibleItems.length) {
375379
var cmd = cmdVisibleItems[cmdActiveIndex];
376380
var trigger = cmdTrigger || '/';
377-
381+
378382
// 找到当前输入框中的命令前缀位置
379383
var val = inputEl.value;
380384
var prefixPos = -1;
381-
385+
382386
// 查找最近的命令前缀(/、@ 或 $)
383387
for (var i = val.length - 1; i >= 0; i--) {
384388
var ch = val.charAt(i);
@@ -387,22 +391,22 @@ function applyCmdSelection(inputEl, completeEl) {
387391
break;
388392
}
389393
}
390-
394+
391395
if (prefixPos >= 0) {
392396
// 替换前缀及其后面的内容
393397
var textBefore = val.substring(0, prefixPos);
394398
var textAfter = val.substring(prefixPos);
395-
399+
396400
// 找到前缀后面的空格位置(如果有)
397401
var spaceIndex = textAfter.indexOf(' ');
398402
var argsStr = '';
399403
if (spaceIndex >= 0) {
400404
argsStr = textAfter.substring(spaceIndex);
401405
}
402-
406+
403407
// 构建新的值(命令/技能/子代理名称后追加空格)
404408
inputEl.value = textBefore + trigger + cmd.name + ' ' + argsStr;
405-
409+
406410
// 更新光标位置到命令和空格后面
407411
var newCursorPos = textBefore.length + trigger.length + cmd.name.length + 1;
408412
inputEl.setSelectionRange(newCursorPos, newCursorPos);
@@ -411,7 +415,7 @@ function applyCmdSelection(inputEl, completeEl) {
411415
inputEl.value = trigger + cmd.name + ' ' + val;
412416
inputEl.setSelectionRange(trigger.length + cmd.name.length + 1, trigger.length + cmd.name.length + 1);
413417
}
414-
418+
415419
autoResize(inputEl);
416420
}
417421
hideCmdComplete();
@@ -497,14 +501,14 @@ function triggerCmdComplete(inputEl, completeEl, prefix) {
497501
var cursorPos = inputEl.selectionStart;
498502
var textBefore = inputEl.value.substring(0, cursorPos);
499503
var textAfter = inputEl.value.substring(cursorPos);
500-
504+
501505
// 在光标位置插入前缀(命令/子代理/技能符号后追加空格)
502506
inputEl.value = textBefore + prefix + ' ' + textAfter;
503-
507+
504508
// 更新光标位置到前缀和空格后面
505509
var newCursorPos = cursorPos + prefix.length + 1;
506510
inputEl.setSelectionRange(newCursorPos, newCursorPos);
507-
511+
508512
inputEl.focus();
509513
showCmdComplete(inputEl, completeEl, prefix);
510514
}
@@ -539,14 +543,14 @@ $(chatInput).on('compositionend', function() { composing = false; });
539543
// Keyboard navigation for command completion
540544
$(welcomeInput).on('keydown', function(e) {
541545
// 输入法正在组合中(如拼音选词),不触发发送
542-
if (composing) return;
546+
if (isInputComposing(e)) return;
543547
var handled = navigateCmdComplete(e, welcomeInput, $welcomeCmdComplete[0]);
544548
if (handled) return;
545549
if (e.key === 'Enter' && !e.shiftKey && !e.altKey) { e.preventDefault(); sendMessage(); }
546550
});
547551
$(chatInput).on('keydown', function(e) {
548552
// 输入法正在组合中(如拼音选词),不触发发送
549-
if (composing) return;
553+
if (isInputComposing(e)) return;
550554
// 优先级1:命令补全导航
551555
var handled = navigateCmdComplete(e, chatInput, $chatCmdComplete[0]);
552556
if (handled) return;

soloncode-cli/src/main/resources/static/js/app-ui.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,32 @@ $('#chatImageInput').on('change', function(e) {
251251
});
252252

253253
/* ===== Marked ===== */
254-
if (typeof marked !== 'undefined') { marked.setOptions({ breaks: true, gfm: true }); }
254+
function escapeHtmlAttr(value) {
255+
return String(value == null ? '' : value)
256+
.replace(/&/g, '&amp;')
257+
.replace(/"/g, '&quot;')
258+
.replace(/</g, '&lt;')
259+
.replace(/>/g, '&gt;');
260+
}
261+
262+
function createMarkdownRenderer() {
263+
var renderer = new marked.Renderer();
264+
265+
renderer.link = function (token) {
266+
var href = token && typeof token === 'object' ? token.href : token;
267+
var title = token && typeof token === 'object' ? token.title : '';
268+
var text = token && typeof token === 'object' ? token.text : '';
269+
var safeHref = href || '';
270+
var safeTitle = title ? ' title="' + escapeHtmlAttr(title) + '"' : '';
271+
var safeText = text || '';
272+
273+
return '<a href="' + escapeHtmlAttr(safeHref) + '" target="_blank" rel="noopener noreferrer"' + safeTitle + '>' + safeText + '</a>';
274+
};
275+
276+
return renderer;
277+
}
278+
279+
if (typeof marked !== 'undefined') { marked.setOptions({ breaks: true, gfm: true, renderer: createMarkdownRenderer() }); }
255280
var _mdCache = new Map();
256281
var _MD_CACHE_MAX = 100;
257282
function renderMd(text) {
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
(function () {
2+
if (window.__studioNavigationGuardInstalled) {
3+
return;
4+
}
5+
window.__studioNavigationGuardInstalled = true;
6+
7+
var nativeOpen = window.open;
8+
var allowedSchemes = ["http:", "https:", "mailto:", "tel:"];
9+
var studioFlag = "studio";
10+
var isStudioPageEnabled = false;
11+
12+
try {
13+
isStudioPageEnabled = new URL(window.location.href).searchParams.get(studioFlag) === "true";
14+
} catch (e) {
15+
isStudioPageEnabled = false;
16+
}
17+
18+
function dispatchStudioNavigationBlocked(payload) {
19+
var eventName = "studio-blocked-navigation";
20+
21+
try {
22+
var customEvent = new CustomEvent(eventName, { detail: payload });
23+
window.dispatchEvent(customEvent);
24+
} catch (e) {
25+
// ignore custom event failures
26+
}
27+
28+
try {
29+
if (window.top && window.top !== window) {
30+
var topEvent = new CustomEvent(eventName, { detail: payload });
31+
window.top.dispatchEvent(topEvent);
32+
}
33+
} catch (e) {
34+
// ignore top window dispatch failures
35+
}
36+
37+
if (window.parent && window.parent !== window) {
38+
try {
39+
window.parent.postMessage(
40+
{
41+
type: eventName,
42+
payload: payload
43+
},
44+
"*"
45+
);
46+
} catch (e) {
47+
// ignore cross-window failures
48+
}
49+
}
50+
}
51+
52+
function bindStudioNavigationBlockedListener() {
53+
try {
54+
window.addEventListener("message", function (event) {
55+
console.log("[studio] raw message:", event.origin, event.data);
56+
});
57+
} catch (e) {
58+
// ignore listener setup failures
59+
}
60+
61+
try {
62+
window.addEventListener("studio-blocked-navigation", function (event) {
63+
var payload = event && event.detail ? event.detail : null;
64+
if (!payload || typeof payload.url !== "string") {
65+
return;
66+
}
67+
68+
console.log("[studio] blocked navigation:", payload.source, payload.url);
69+
});
70+
} catch (e) {
71+
// ignore listener setup failures
72+
}
73+
}
74+
75+
bindStudioNavigationBlockedListener();
76+
77+
if (!isStudioPageEnabled) {
78+
return;
79+
}
80+
81+
function isAllowedUrl(url) {
82+
if (!url) {
83+
return false;
84+
}
85+
86+
var value = String(url).trim();
87+
if (!value) {
88+
return false;
89+
}
90+
91+
if (value.charAt(0) === "#") {
92+
return true;
93+
}
94+
95+
if (value.indexOf("javascript:") === 0 || value.indexOf("data:") === 0) {
96+
return false;
97+
}
98+
99+
try {
100+
var parsed = new URL(value, window.location.href);
101+
return allowedSchemes.indexOf(parsed.protocol) >= 0 || parsed.origin === window.location.origin;
102+
} catch (e) {
103+
return false;
104+
}
105+
}
106+
107+
function shouldBlockAnchor(anchor) {
108+
if (!anchor || !anchor.getAttribute) {
109+
return false;
110+
}
111+
112+
var href = anchor.getAttribute("href");
113+
if (!href) {
114+
return false;
115+
}
116+
117+
if (isAllowedUrl(href) && (anchor.getAttribute("target") || "").toLowerCase() === "_blank") {
118+
return true;
119+
}
120+
121+
return false;
122+
}
123+
124+
function updateAnchorInterception(anchor) {
125+
if (!anchor || !anchor.getAttribute) {
126+
return;
127+
}
128+
129+
var href = anchor.getAttribute("href");
130+
if (!href) {
131+
return;
132+
}
133+
134+
if (isAllowedUrl(href) && (anchor.getAttribute("target") || "").toLowerCase() === "_blank") {
135+
anchor.setAttribute("data-studio-block-navigation", "true");
136+
} else {
137+
anchor.removeAttribute("data-studio-block-navigation");
138+
}
139+
}
140+
141+
function scanAnchors(root) {
142+
if (!root || !root.querySelectorAll) {
143+
return;
144+
}
145+
146+
var anchors = root.querySelectorAll("a[href]");
147+
for (var i = 0; i < anchors.length; i += 1) {
148+
updateAnchorInterception(anchors[i]);
149+
}
150+
}
151+
152+
function bindAnchorObserver() {
153+
try {
154+
scanAnchors(document);
155+
156+
if (!window.MutationObserver) {
157+
return;
158+
}
159+
160+
var observer = new MutationObserver(function (mutations) {
161+
for (var i = 0; i < mutations.length; i += 1) {
162+
var mutation = mutations[i];
163+
for (var j = 0; j < mutation.addedNodes.length; j += 1) {
164+
var node = mutation.addedNodes[j];
165+
if (!node || node.nodeType !== 1) {
166+
continue;
167+
}
168+
169+
if (node.tagName === "A") {
170+
updateAnchorInterception(node);
171+
}
172+
173+
scanAnchors(node);
174+
}
175+
}
176+
});
177+
178+
observer.observe(document.documentElement || document.body, {
179+
childList: true,
180+
subtree: true
181+
});
182+
} catch (e) {
183+
// ignore observer setup failures
184+
}
185+
}
186+
187+
function handleBlockedNavigation(url, source) {
188+
var payload = {
189+
source: source,
190+
url: url,
191+
timestamp: Date.now()
192+
};
193+
194+
dispatchStudioNavigationBlocked(payload);
195+
console.log("[studio] blocked navigation:", source, url);
196+
197+
if (typeof window.onStudioNavigationBlocked === "function") {
198+
window.onStudioNavigationBlocked(url, source);
199+
}
200+
}
201+
202+
window.open = function (url, target, features) {
203+
if (!isAllowedUrl(url)) {
204+
return nativeOpen.apply(window, arguments);
205+
}
206+
207+
if (typeof target === "string" && target.toLowerCase() === "_blank") {
208+
handleBlockedNavigation(url, "window.open");
209+
return null;
210+
}
211+
212+
return nativeOpen.apply(window, arguments);
213+
};
214+
215+
document.addEventListener(
216+
"click",
217+
function (event) {
218+
var node = event.target;
219+
while (node && node !== document) {
220+
if (node.tagName === "A") {
221+
if (shouldBlockAnchor(node)) {
222+
event.preventDefault();
223+
event.stopPropagation();
224+
handleBlockedNavigation(node.href || node.getAttribute("href"), "anchor-click");
225+
}
226+
return;
227+
}
228+
node = node.parentNode;
229+
}
230+
},
231+
true
232+
);
233+
234+
document.addEventListener(
235+
"submit",
236+
function (event) {
237+
var form = event.target;
238+
if (!form || form.tagName !== "FORM") {
239+
return;
240+
}
241+
242+
var action = form.getAttribute("action") || window.location.href;
243+
if (!isAllowedUrl(action)) {
244+
return;
245+
}
246+
247+
if ((action || "").indexOf("#") === 0) {
248+
return;
249+
}
250+
251+
if (new URL(action, window.location.href).origin !== window.location.origin) {
252+
return;
253+
}
254+
255+
if ((window.location.search || "").indexOf(studioFlag + "=true") >= 0) {
256+
event.preventDefault();
257+
event.stopPropagation();
258+
handleBlockedNavigation(action, "form-submit");
259+
}
260+
},
261+
true
262+
);
263+
264+
bindStudioNavigationBlockedListener();
265+
bindAnchorObserver();
266+
})();

0 commit comments

Comments
 (0)