@xcanwin/manyoyo 5.8.10 → 5.8.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -352,6 +352,20 @@ textarea:focus-visible {
352
352
  white-space: pre-wrap;
353
353
  }
354
354
 
355
+ .link-confirm-url {
356
+ margin-top: 2px;
357
+ padding: 12px 14px;
358
+ border: 1px solid rgba(146, 100, 42, 0.18);
359
+ border-radius: 10px;
360
+ background: #fff8ef;
361
+ color: var(--text);
362
+ font-family: var(--font-mono);
363
+ font-size: 12px;
364
+ line-height: 1.6;
365
+ word-break: break-all;
366
+ user-select: text;
367
+ }
368
+
355
369
  .config-editor {
356
370
  width: 100%;
357
371
  min-height: 340px;
@@ -275,6 +275,22 @@
275
275
  </footer>
276
276
  </section>
277
277
  </div>
278
+
279
+ <div id="externalLinkModal" class="modal-backdrop" hidden>
280
+ <section class="modal" role="dialog" aria-modal="true" aria-labelledby="externalLinkTitle">
281
+ <header class="modal-header">
282
+ <h2 id="externalLinkTitle">打开外部链接</h2>
283
+ <button type="button" id="externalLinkCancelBtn" class="secondary">取消</button>
284
+ </header>
285
+ <div class="modal-body">
286
+ <div class="modal-tip">即将新标签页打开外部页面。请确认完整链接可信后再继续。</div>
287
+ <div id="externalLinkUrl" class="link-confirm-url"></div>
288
+ </div>
289
+ <footer class="modal-footer">
290
+ <button type="button" id="externalLinkOpenBtn">确认并新标签打开</button>
291
+ </footer>
292
+ </section>
293
+ </div>
278
294
  </div>
279
295
 
280
296
  <script src="/app/vendor/xterm.js"></script>
@@ -51,6 +51,7 @@
51
51
  configModalOpen: false,
52
52
  createModalOpen: false,
53
53
  agentTemplateModalOpen: false,
54
+ externalLinkModalOpen: false,
54
55
  configLoading: false,
55
56
  configSaving: false,
56
57
  configSaveMessage: '',
@@ -105,7 +106,8 @@
105
106
  lastSentRows: 0,
106
107
  ctrlMode: false,
107
108
  altMode: false
108
- }
109
+ },
110
+ externalLinkUrl: ''
109
111
  };
110
112
 
111
113
  const sidebarNode = document.querySelector('.sidebar');
@@ -196,6 +198,10 @@
196
198
  const agentTemplateCancelBtn = document.getElementById('agentTemplateCancelBtn');
197
199
  const agentTemplateResetBtn = document.getElementById('agentTemplateResetBtn');
198
200
  const agentTemplateSaveBtn = document.getElementById('agentTemplateSaveBtn');
201
+ const externalLinkModal = document.getElementById('externalLinkModal');
202
+ const externalLinkUrl = document.getElementById('externalLinkUrl');
203
+ const externalLinkCancelBtn = document.getElementById('externalLinkCancelBtn');
204
+ const externalLinkOpenBtn = document.getElementById('externalLinkOpenBtn');
199
205
  const refreshBtn = document.getElementById('refreshBtn');
200
206
  const removeBtn = document.getElementById('removeBtn');
201
207
  const removeAllBtn = document.getElementById('removeAllBtn');
@@ -740,6 +746,45 @@
740
746
  configStatus.textContent = text;
741
747
  }
742
748
 
749
+ function openExternalLinkModalView(url) {
750
+ const text = String(url || '').trim();
751
+ if (!text) {
752
+ return;
753
+ }
754
+ state.externalLinkUrl = text;
755
+ state.externalLinkModalOpen = true;
756
+ if (externalLinkUrl) {
757
+ externalLinkUrl.textContent = text;
758
+ }
759
+ setModalVisible(externalLinkModal, true);
760
+ }
761
+
762
+ function closeExternalLinkModalView() {
763
+ state.externalLinkModalOpen = false;
764
+ state.externalLinkUrl = '';
765
+ if (externalLinkUrl) {
766
+ externalLinkUrl.textContent = '';
767
+ }
768
+ setModalVisible(externalLinkModal, false);
769
+ }
770
+
771
+ function confirmExternalLinkOpen() {
772
+ const targetUrl = String(state.externalLinkUrl || '').trim();
773
+ if (!targetUrl) {
774
+ closeExternalLinkModalView();
775
+ return;
776
+ }
777
+ if (markdownRenderer && typeof markdownRenderer.openExternalLink === 'function') {
778
+ markdownRenderer.openExternalLink(targetUrl);
779
+ } else {
780
+ const popup = window.open(targetUrl, '_blank', 'noopener,noreferrer');
781
+ if (popup) {
782
+ popup.opener = null;
783
+ }
784
+ }
785
+ closeExternalLinkModalView();
786
+ }
787
+
743
788
  function showDirectoryPickerError(message) {
744
789
  if (!directoryPickerError) return;
745
790
  const text = String(message || '').trim();
@@ -2095,6 +2140,9 @@
2095
2140
  if (agentTemplateModal) {
2096
2141
  agentTemplateModal.hidden = !state.agentTemplateModalOpen;
2097
2142
  }
2143
+ if (externalLinkModal) {
2144
+ externalLinkModal.hidden = !state.externalLinkModalOpen;
2145
+ }
2098
2146
  if (agentTemplateSaveBtn) {
2099
2147
  agentTemplateSaveBtn.disabled = state.agentTemplateSaving || !state.active;
2100
2148
  }
@@ -2118,7 +2166,7 @@
2118
2166
  }
2119
2167
  document.body.classList.toggle(
2120
2168
  'modal-open',
2121
- state.configModalOpen || state.createModalOpen || state.directoryPicker.open || state.agentTemplateModalOpen
2169
+ state.configModalOpen || state.createModalOpen || state.directoryPicker.open || state.agentTemplateModalOpen || state.externalLinkModalOpen
2122
2170
  );
2123
2171
  if (!state.active) {
2124
2172
  sendState.textContent = '未选择会话';
@@ -4030,6 +4078,15 @@
4030
4078
  });
4031
4079
  }
4032
4080
 
4081
+ if (externalLinkModal) {
4082
+ externalLinkModal.addEventListener('click', function (event) {
4083
+ if (event.target === externalLinkModal) {
4084
+ closeExternalLinkModalView();
4085
+ syncUi();
4086
+ }
4087
+ });
4088
+ }
4089
+
4033
4090
  window.addEventListener('keydown', function (event) {
4034
4091
  if (event.key === 'Escape' && state.configModalOpen) {
4035
4092
  closeConfigModal();
@@ -4046,6 +4103,10 @@
4046
4103
  closeAgentTemplateModal();
4047
4104
  syncUi();
4048
4105
  }
4106
+ if (event.key === 'Escape' && state.externalLinkModalOpen) {
4107
+ closeExternalLinkModalView();
4108
+ syncUi();
4109
+ }
4049
4110
  if (event.key === 'Escape' && state.mobileSidebarOpen) {
4050
4111
  closeMobileSessionPanel();
4051
4112
  }
@@ -4128,6 +4189,27 @@
4128
4189
  }
4129
4190
  });
4130
4191
 
4192
+ if (externalLinkCancelBtn) {
4193
+ externalLinkCancelBtn.addEventListener('click', function () {
4194
+ closeExternalLinkModalView();
4195
+ syncUi();
4196
+ });
4197
+ }
4198
+
4199
+ if (externalLinkOpenBtn) {
4200
+ externalLinkOpenBtn.addEventListener('click', function () {
4201
+ confirmExternalLinkOpen();
4202
+ syncUi();
4203
+ });
4204
+ }
4205
+
4206
+ if (markdownRenderer && typeof markdownRenderer.setLinkOpenHandler === 'function') {
4207
+ markdownRenderer.setLinkOpenHandler(function (url) {
4208
+ openExternalLinkModalView(url);
4209
+ syncUi();
4210
+ });
4211
+ }
4212
+
4131
4213
  window.addEventListener('beforeunload', function () {
4132
4214
  disconnectTerminal('', true);
4133
4215
  });
@@ -1,8 +1,11 @@
1
1
  (function () {
2
- const MARKDOWN_URL_PROTOCOL_PATTERN = /^(https?:|mailto:|tel:)/i;
2
+ const MARKDOWN_LINK_PROTOCOL_PATTERN = /^(https?:|mailto:|tel:)/i;
3
+ const MARKDOWN_IMAGE_PROTOCOL_PATTERN = /^(https?:)/i;
3
4
  const runtime = {
4
5
  configured: false,
5
- available: false
6
+ available: false,
7
+ linkGuardBound: false,
8
+ linkOpenHandler: null
6
9
  };
7
10
 
8
11
  function escapeHtml(value) {
@@ -14,26 +17,175 @@
14
17
  .replace(/'/g, '&#39;');
15
18
  }
16
19
 
17
- function sanitizeMarkdownUrl(value) {
20
+ function normalizeRendererValue(value) {
21
+ if (value && typeof value === 'object') {
22
+ if (typeof value.href === 'string') {
23
+ return value.href;
24
+ }
25
+ if (typeof value.text === 'string') {
26
+ return value.text;
27
+ }
28
+ }
29
+ return String(value == null ? '' : value);
30
+ }
31
+
32
+ function sanitizeMarkdownLinkUrl(value) {
18
33
  const raw = String(value == null ? '' : value).trim();
19
34
  if (!raw) {
20
35
  return '';
21
36
  }
22
- if (raw[0] === '#') {
37
+ if (MARKDOWN_LINK_PROTOCOL_PATTERN.test(raw)) {
23
38
  return raw;
24
39
  }
40
+ return '';
41
+ }
42
+
43
+ function sanitizeMarkdownImageUrl(value) {
44
+ const raw = String(value == null ? '' : value).trim();
45
+ if (!raw) {
46
+ return '';
47
+ }
25
48
  if (raw[0] === '/') {
26
49
  return raw.startsWith('//') ? '' : raw;
27
50
  }
28
51
  if (raw.startsWith('./') || raw.startsWith('../')) {
29
52
  return raw;
30
53
  }
31
- if (MARKDOWN_URL_PROTOCOL_PATTERN.test(raw)) {
54
+ if (MARKDOWN_IMAGE_PROTOCOL_PATTERN.test(raw)) {
32
55
  return raw;
33
56
  }
34
57
  return '';
35
58
  }
36
59
 
60
+ function getRendererToken(value) {
61
+ return value && typeof value === 'object' ? value : null;
62
+ }
63
+
64
+ function renderInlineTokens(rendererContext, token, fallbackText) {
65
+ if (
66
+ token
67
+ && Array.isArray(token.tokens)
68
+ && rendererContext
69
+ && rendererContext.parser
70
+ && typeof rendererContext.parser.parseInline === 'function'
71
+ ) {
72
+ try {
73
+ return String(rendererContext.parser.parseInline(token.tokens) || '');
74
+ } catch (e) {
75
+ // ignore and fallback to plain text below
76
+ }
77
+ }
78
+ if (token && typeof token.text === 'string') {
79
+ return escapeHtml(token.text);
80
+ }
81
+ return escapeHtml(fallbackText || '');
82
+ }
83
+
84
+ function buildSafeAnchorHtml(href, title, content, extraAttrs) {
85
+ let output = '<a href="' + escapeHtml(href) + '" target="_blank" rel="noopener noreferrer"'
86
+ + ' referrerpolicy="no-referrer" data-safe-external-link="true"'
87
+ + ' data-safe-href="' + escapeHtml(href) + '"';
88
+ if (title) {
89
+ output += ' title="' + escapeHtml(title) + '"';
90
+ }
91
+ if (extraAttrs) {
92
+ output += extraAttrs;
93
+ }
94
+ output += '>' + content + '</a>';
95
+ return output;
96
+ }
97
+
98
+ function openExternalLinkWithNoReferrer(href) {
99
+ const url = String(href || '').trim();
100
+ if (!url || typeof document === 'undefined' || !document || typeof document.createElement !== 'function') {
101
+ if (typeof window.open === 'function') {
102
+ const opened = window.open(url, '_blank', 'noopener,noreferrer');
103
+ if (opened) {
104
+ opened.opener = null;
105
+ }
106
+ }
107
+ return;
108
+ }
109
+
110
+ const anchor = document.createElement('a');
111
+ anchor.href = url;
112
+ anchor.target = '_blank';
113
+ anchor.rel = 'noopener noreferrer';
114
+ anchor.referrerPolicy = 'no-referrer';
115
+ anchor.style.display = 'none';
116
+ document.body.appendChild(anchor);
117
+ try {
118
+ if (typeof anchor.click === 'function') {
119
+ anchor.click();
120
+ } else if (typeof window.open === 'function') {
121
+ const opened = window.open(url, '_blank', 'noopener,noreferrer');
122
+ if (opened) {
123
+ opened.opener = null;
124
+ }
125
+ }
126
+ } finally {
127
+ if (anchor.parentNode && typeof anchor.parentNode.removeChild === 'function') {
128
+ anchor.parentNode.removeChild(anchor);
129
+ }
130
+ }
131
+ }
132
+
133
+ function requestExternalLinkOpen(href) {
134
+ const url = String(href || '').trim();
135
+ if (!url) {
136
+ return;
137
+ }
138
+ if (typeof runtime.linkOpenHandler === 'function') {
139
+ runtime.linkOpenHandler(url);
140
+ return;
141
+ }
142
+
143
+ const shouldOpen = typeof window.confirm === 'function'
144
+ ? window.confirm('即将打开外部链接:\n' + url + '\n\n确认继续打开?')
145
+ : true;
146
+ if (!shouldOpen) {
147
+ return;
148
+ }
149
+
150
+ openExternalLinkWithNoReferrer(url);
151
+ }
152
+
153
+ function bindDocumentLinkGuard() {
154
+ if (runtime.linkGuardBound || typeof document === 'undefined' || !document || typeof document.addEventListener !== 'function') {
155
+ return;
156
+ }
157
+
158
+ document.addEventListener('click', function (event) {
159
+ const target = event && event.target;
160
+ if (!target || typeof target.closest !== 'function') {
161
+ return;
162
+ }
163
+ const link = target.closest('a[data-safe-external-link="true"]');
164
+ if (!link) {
165
+ return;
166
+ }
167
+ if (event.preventDefault) {
168
+ event.preventDefault();
169
+ }
170
+ if (event.stopPropagation) {
171
+ event.stopPropagation();
172
+ }
173
+
174
+ const href = String(
175
+ (typeof link.getAttribute === 'function' && (link.getAttribute('data-safe-href') || link.getAttribute('href')))
176
+ || link.href
177
+ || ''
178
+ ).trim();
179
+ if (!href) {
180
+ return;
181
+ }
182
+
183
+ requestExternalLinkOpen(href);
184
+ });
185
+
186
+ runtime.linkGuardBound = true;
187
+ }
188
+
37
189
  function getMarkedApi() {
38
190
  const api = window.marked;
39
191
  if (!api || typeof api.parse !== 'function') {
@@ -57,45 +209,48 @@
57
209
  try {
58
210
  const renderer = new markedApi.Renderer();
59
211
  renderer.html = function (html) {
60
- return escapeHtml(html);
212
+ const token = getRendererToken(html);
213
+ return escapeHtml(token ? token.text : html);
61
214
  };
62
215
  renderer.link = function (href, title, text) {
63
- const safeHref = sanitizeMarkdownUrl(href);
216
+ const token = getRendererToken(href);
217
+ const rawHref = token ? token.href : normalizeRendererValue(href);
218
+ const rawTitle = token ? token.title : title;
219
+ const safeHref = sanitizeMarkdownLinkUrl(rawHref);
220
+ const safeText = renderInlineTokens(this, token, text)
221
+ // [P1-02] 移除 marked 已渲染链接文本中的 on* 事件属性,防止内联 HTML 注入 XSS
222
+ .replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, '');
64
223
  if (!safeHref) {
65
- return escapeHtml(text || '');
66
- }
67
- // [P1-02] 移除 marked 已渲染链接文本中的 on* 事件属性,防止内联 HTML 注入 XSS
68
- const safeText = String(text || '').replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, '');
69
- let output = '<a href="' + escapeHtml(safeHref) + '" target="_blank" rel="noopener noreferrer"';
70
- if (title) {
71
- output += ' title="' + escapeHtml(title) + '"';
224
+ return safeText || escapeHtml(token ? token.text : text || '');
72
225
  }
73
- output += '>' + safeText + '</a>';
74
- return output;
226
+ return buildSafeAnchorHtml(safeHref, rawTitle, safeText || escapeHtml(safeHref));
75
227
  };
76
228
  // [P1-01] 重写 image 渲染器:
77
229
  // - 外部 http/https 图片转为可点击链接,避免浏览器自动发起外部请求(追踪像素风险)
78
230
  // - 相对路径图片正常渲染为 <img>
79
231
  // - 危险协议(javascript:/data: 等)降级为纯文本
80
232
  renderer.image = function (href, title, text) {
81
- const safeHref = sanitizeMarkdownUrl(href);
233
+ const token = getRendererToken(href);
234
+ const rawHref = token ? token.href : normalizeRendererValue(href);
235
+ const rawTitle = token ? token.title : title;
236
+ const safeHref = sanitizeMarkdownImageUrl(rawHref);
237
+ const safeText = renderInlineTokens(this, token, text)
238
+ .replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, '');
82
239
  if (!safeHref) {
83
- return escapeHtml(text || '');
240
+ return safeText || escapeHtml(token ? token.text : text || '');
84
241
  }
85
242
  // 外部绝对 URL:转为链接,用户主动决定是否访问
86
243
  if (/^https?:/i.test(safeHref)) {
87
- const safeText = String(text || '').replace(/\s+on\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi, '')
88
- || escapeHtml(safeHref);
89
- let output = '<a href="' + escapeHtml(safeHref) + '" target="_blank" rel="noopener noreferrer"';
90
- if (title) {
91
- output += ' title="' + escapeHtml(title) + '"';
92
- }
93
- return output + '>[\uD83D\uDDBC\uFE0F点击查看图片:' + safeText + ']</a>';
244
+ return buildSafeAnchorHtml(
245
+ safeHref,
246
+ rawTitle,
247
+ '[\uD83D\uDDBC\uFE0F点击查看图片:' + (safeText || escapeHtml(safeHref)) + ']'
248
+ );
94
249
  }
95
250
  // 相对路径:正常渲染为图片
96
251
  let output = '<img src="' + escapeHtml(safeHref) + '" alt="' + escapeHtml(text || '') + '"';
97
- if (title) {
98
- output += ' title="' + escapeHtml(title) + '"';
252
+ if (rawTitle) {
253
+ output += ' title="' + escapeHtml(rawTitle) + '"';
99
254
  }
100
255
  return output + '>';
101
256
  };
@@ -138,6 +293,12 @@
138
293
 
139
294
  window.ManyoyoMarkdown = {
140
295
  shouldRenderMessage,
141
- render
296
+ render,
297
+ openExternalLink: openExternalLinkWithNoReferrer,
298
+ setLinkOpenHandler: function (handler) {
299
+ runtime.linkOpenHandler = typeof handler === 'function' ? handler : null;
300
+ }
142
301
  };
302
+
303
+ bindDocumentLinkGuard();
143
304
  }());
@@ -72,5 +72,13 @@
72
72
 
73
73
  .bubble .md-content a {
74
74
  color: #8a4f17;
75
- text-decoration-color: rgba(138, 79, 23, 0.45);
75
+ text-decoration-line: underline;
76
+ text-decoration-thickness: 2px;
77
+ text-underline-offset: 0.16em;
78
+ text-decoration-color: rgba(138, 79, 23, 0.8);
79
+ }
80
+
81
+ .bubble .md-content a:hover,
82
+ .bubble .md-content a:focus-visible {
83
+ text-decoration-color: #8a4f17;
76
84
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.8.10",
3
+ "version": "5.8.11",
4
4
  "imageVersion": "1.9.0-common",
5
5
  "playwrightCliVersion": "0.1.1",
6
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",