cursor-feedback 1.2.0 → 2.0.0
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.
- package/CHANGELOG.md +7 -0
- package/README.md +40 -7
- package/README_CN.md +40 -7
- package/dist/extension.js +480 -53
- package/dist/feishu.js +667 -0
- package/dist/i18n/en.json +49 -7
- package/dist/i18n/index.js +46 -4
- package/dist/i18n/zh-CN.json +49 -7
- package/dist/mcp-server.js +514 -20
- package/dist/webview/index.html +193 -43
- package/dist/webview/script.js +795 -145
- package/dist/webview/styles.css +921 -298
- package/package.json +29 -2
- package/.github/workflows/release-please.yml +0 -19
- package/.husky/pre-commit +0 -2
- package/.husky/pre-push +0 -1
- package/.versionrc.json +0 -15
- package/.vscode/launch.json +0 -28
- package/.vscode/tasks.json +0 -22
- package/.vscodeignore +0 -11
- package/demo.gif +0 -0
- package/dist/extension.js.map +0 -1
- package/dist/i18n/index.js.map +0 -1
- package/dist/mcp-server.js.map +0 -1
- package/icon.png +0 -0
- package/mcp-install-dark.png +0 -0
- package/resources/icon.svg +0 -4
- package/resources/vendor/marked.min.js +0 -69
- package/scripts/check-changelog.js +0 -42
- package/src/extension.ts +0 -759
- package/src/i18n/en.json +0 -34
- package/src/i18n/index.ts +0 -131
- package/src/i18n/zh-CN.json +0 -34
- package/src/mcp-server.ts +0 -768
- package/src/webview/index.html +0 -69
- package/src/webview/script.js +0 -318
- package/src/webview/styles.css +0 -422
- package/tsconfig.json +0 -17
package/dist/extension.js
CHANGED
|
@@ -42,12 +42,14 @@ const path = __importStar(require("path"));
|
|
|
42
42
|
const child_process_1 = require("child_process");
|
|
43
43
|
const i18n_1 = require("./i18n");
|
|
44
44
|
let feedbackViewProvider = null;
|
|
45
|
-
|
|
45
|
+
/** 规范化路径:统一分隔符、去尾斜杠、转小写,用于工作区匹配 */
|
|
46
|
+
function normalizePath(p) {
|
|
47
|
+
return (p || '').replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
48
|
+
}
|
|
46
49
|
function activate(context) {
|
|
47
|
-
console.log('Cursor Feedback extension is now active!');
|
|
48
50
|
// 注册侧边栏 WebView(端口从 61927 开始自动扫描)
|
|
49
|
-
feedbackViewProvider = new FeedbackViewProvider(context.extensionUri, 61927);
|
|
50
|
-
context.subscriptions.push(vscode.window.registerWebviewViewProvider('cursorFeedback.feedbackView', feedbackViewProvider));
|
|
51
|
+
feedbackViewProvider = new FeedbackViewProvider(context.extensionUri, 61927, context.globalState);
|
|
52
|
+
context.subscriptions.push(vscode.window.registerWebviewViewProvider('cursorFeedback.feedbackView', feedbackViewProvider, { webviewOptions: { retainContextWhenHidden: true } }));
|
|
51
53
|
// 注册命令:显示反馈面板
|
|
52
54
|
context.subscriptions.push(vscode.commands.registerCommand('cursorFeedback.showPanel', () => {
|
|
53
55
|
vscode.commands.executeCommand('cursorFeedback.feedbackView.focus');
|
|
@@ -56,14 +58,25 @@ function activate(context) {
|
|
|
56
58
|
context.subscriptions.push(vscode.commands.registerCommand('cursorFeedback.startPolling', () => {
|
|
57
59
|
if (feedbackViewProvider) {
|
|
58
60
|
feedbackViewProvider.startPolling();
|
|
59
|
-
vscode.window.showInformationMessage(feedbackViewProvider.getMessage('startListening'));
|
|
60
61
|
}
|
|
61
62
|
}));
|
|
62
63
|
// 注册命令:停止轮询
|
|
63
64
|
context.subscriptions.push(vscode.commands.registerCommand('cursorFeedback.stopPolling', () => {
|
|
64
65
|
if (feedbackViewProvider) {
|
|
65
66
|
feedbackViewProvider.stopPolling();
|
|
66
|
-
|
|
67
|
+
}
|
|
68
|
+
}));
|
|
69
|
+
// 注册命令:带入编辑器选中代码到反馈输入框
|
|
70
|
+
context.subscriptions.push(vscode.commands.registerCommand('cursorFeedback.insertSelection', () => {
|
|
71
|
+
feedbackViewProvider?.insertSelectionToFeedback();
|
|
72
|
+
}));
|
|
73
|
+
// 缓存最近的活动编辑器(焦点在 feedback webview 时 activeTextEditor 会变 undefined)
|
|
74
|
+
if (vscode.window.activeTextEditor) {
|
|
75
|
+
feedbackViewProvider.setLastActiveEditor(vscode.window.activeTextEditor);
|
|
76
|
+
}
|
|
77
|
+
context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(editor => {
|
|
78
|
+
if (editor) {
|
|
79
|
+
feedbackViewProvider?.setLastActiveEditor(editor);
|
|
67
80
|
}
|
|
68
81
|
}));
|
|
69
82
|
// 自动开始轮询
|
|
@@ -94,8 +107,7 @@ function getWorkspacePaths() {
|
|
|
94
107
|
function isPathInWorkspace(targetPath) {
|
|
95
108
|
const workspacePaths = getWorkspacePaths();
|
|
96
109
|
// 规范化路径(去除末尾斜杠,统一分隔符,小写)
|
|
97
|
-
const
|
|
98
|
-
const normalizedTarget = normalize(targetPath);
|
|
110
|
+
const normalizedTarget = normalizePath(targetPath);
|
|
99
111
|
// 检查 targetPath 是否为空或默认值
|
|
100
112
|
const isEmptyPath = !targetPath || targetPath === '.' || normalizedTarget === '' || normalizedTarget === '.';
|
|
101
113
|
if (workspacePaths.length === 0) {
|
|
@@ -107,7 +119,7 @@ function isPathInWorkspace(targetPath) {
|
|
|
107
119
|
return false;
|
|
108
120
|
}
|
|
109
121
|
for (const wsPath of workspacePaths) {
|
|
110
|
-
const normalizedWs =
|
|
122
|
+
const normalizedWs = normalizePath(wsPath);
|
|
111
123
|
// 精确匹配:只匹配完全相同的路径
|
|
112
124
|
if (normalizedTarget === normalizedWs) {
|
|
113
125
|
return true;
|
|
@@ -119,13 +131,29 @@ function isPathInWorkspace(targetPath) {
|
|
|
119
131
|
* 侧边栏 WebView Provider
|
|
120
132
|
*/
|
|
121
133
|
class FeedbackViewProvider {
|
|
122
|
-
constructor(_extensionUri, port) {
|
|
134
|
+
constructor(_extensionUri, port, _memento) {
|
|
123
135
|
this._extensionUri = _extensionUri;
|
|
124
|
-
this.
|
|
136
|
+
this._memento = _memento;
|
|
137
|
+
this._polling = false;
|
|
138
|
+
this._pollTimer = null;
|
|
139
|
+
this._pollDelay = 1000;
|
|
125
140
|
this._currentRequest = null;
|
|
141
|
+
// webview 脚本是否就绪;插入上下文消息需等就绪后再发,否则 focus 唤起重建时会丢消息
|
|
142
|
+
this._webviewReady = false;
|
|
143
|
+
this._pendingInsert = null;
|
|
126
144
|
this._activePort = null;
|
|
127
145
|
this._portScanRange = 20; // 扫描端口范围
|
|
128
146
|
this._seenRequestIds = new Set(); // 已处理过的请求 ID
|
|
147
|
+
// 超时续期开关(由侧边栏按钮切换,随轮询同步给 MCP server)
|
|
148
|
+
this._autoRetry = true;
|
|
149
|
+
// 飞书配置(持久化在 globalState;secret 会回显给 webview,前端用小眼睛切换明文/掩码)
|
|
150
|
+
this._feishuConfig = { appId: '', appSecret: '' };
|
|
151
|
+
// 飞书绑定状态(运行时,来自轮询 server;绑定本身由 server 端磁盘持久化、多进程共享)
|
|
152
|
+
this._feishuBound = false;
|
|
153
|
+
// 飞书通知开关(用户可关:即使配置了凭证也不推飞书)
|
|
154
|
+
this._feishuEnabled = true;
|
|
155
|
+
// Get 表情回执子开关(飞书通知子项;关掉后用户飞书回复不加 Get 表情、也不发文字兜底)
|
|
156
|
+
this._feishuAck = true;
|
|
129
157
|
this._debugInfo = {
|
|
130
158
|
portRange: '',
|
|
131
159
|
workspacePath: '',
|
|
@@ -137,6 +165,12 @@ class FeedbackViewProvider {
|
|
|
137
165
|
this._debugInfo.portRange = `${port}-${port + this._portScanRange - 1}`;
|
|
138
166
|
this._i18n = (0, i18n_1.loadMessages)(this._extensionUri.fsPath);
|
|
139
167
|
this._debugInfo.lastStatus = this._i18n.checkingConnection;
|
|
168
|
+
this._autoRetry = this._memento?.get('autoRetry', true) ?? true;
|
|
169
|
+
// 凭证真相源在 server 端磁盘;这里从 globalState 读上次的值仅作首屏占位,poll 一到即被 server 覆盖。
|
|
170
|
+
const fc = this._memento?.get('feishuConfig', {}) ?? {};
|
|
171
|
+
this._feishuConfig = { appId: fc.appId || '', appSecret: fc.appSecret || '' };
|
|
172
|
+
this._feishuEnabled = this._memento?.get('feishuEnabled', true) ?? true;
|
|
173
|
+
this._feishuAck = this._memento?.get('feishuAck', true) ?? true;
|
|
140
174
|
}
|
|
141
175
|
/**
|
|
142
176
|
* 获取翻译消息
|
|
@@ -144,8 +178,19 @@ class FeedbackViewProvider {
|
|
|
144
178
|
getMessage(key) {
|
|
145
179
|
return this._i18n[key] || key;
|
|
146
180
|
}
|
|
181
|
+
/** 翻译并替换 {占位符} 参数(用于带变量的状态文案) */
|
|
182
|
+
_t(key, params) {
|
|
183
|
+
let s = this._i18n[key] || key;
|
|
184
|
+
if (params) {
|
|
185
|
+
for (const k of Object.keys(params)) {
|
|
186
|
+
s = s.replace(`{${k}}`, String(params[k]));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return s;
|
|
190
|
+
}
|
|
147
191
|
resolveWebviewView(webviewView, _context, _token) {
|
|
148
192
|
this._view = webviewView;
|
|
193
|
+
this._webviewReady = false;
|
|
149
194
|
webviewView.webview.options = {
|
|
150
195
|
enableScripts: true,
|
|
151
196
|
localResourceRoots: [this._extensionUri]
|
|
@@ -158,11 +203,17 @@ class FeedbackViewProvider {
|
|
|
158
203
|
await this._handleFeedbackSubmit(data.payload);
|
|
159
204
|
break;
|
|
160
205
|
case 'ready':
|
|
161
|
-
|
|
206
|
+
this._webviewReady = true;
|
|
207
|
+
// 同步超时续期开关状态到 UI
|
|
208
|
+
this._postAutoRetryState();
|
|
209
|
+
this._postFeishuState();
|
|
162
210
|
// WebView 准备就绪后,检查是否有待处理的请求
|
|
163
|
-
|
|
211
|
+
// (插件通知关闭时保持静默,不显示缓存的请求)
|
|
212
|
+
if (this._currentRequest && this._isPluginNotifyEnabled()) {
|
|
164
213
|
this._showFeedbackRequest(this._currentRequest);
|
|
165
214
|
}
|
|
215
|
+
// 补发就绪前排队的「插入上下文」消息
|
|
216
|
+
this._flushPendingInsert();
|
|
166
217
|
break;
|
|
167
218
|
case 'checkServer':
|
|
168
219
|
await this._checkServerHealth();
|
|
@@ -173,36 +224,78 @@ class FeedbackViewProvider {
|
|
|
173
224
|
case 'switchLanguage':
|
|
174
225
|
await this._handleSwitchLanguage();
|
|
175
226
|
break;
|
|
227
|
+
case 'toggleAutoRetry':
|
|
228
|
+
await this._handleToggleAutoRetry();
|
|
229
|
+
break;
|
|
230
|
+
case 'searchFiles':
|
|
231
|
+
await this._handleSearchFiles();
|
|
232
|
+
break;
|
|
233
|
+
case 'requestSelection':
|
|
234
|
+
await this.insertSelectionToFeedback();
|
|
235
|
+
break;
|
|
236
|
+
case 'saveFeishuConfig':
|
|
237
|
+
await this._handleSaveFeishuConfig(data.payload);
|
|
238
|
+
break;
|
|
239
|
+
case 'openFeishuGuide':
|
|
240
|
+
await this._handleOpenFeishuGuide();
|
|
241
|
+
break;
|
|
242
|
+
case 'toggleFeishuEnabled':
|
|
243
|
+
await this._handleToggleFeishuEnabled(!!data.payload?.enabled);
|
|
244
|
+
break;
|
|
245
|
+
case 'toggleSystemNotification':
|
|
246
|
+
await this._handleToggleSystemNotification(!!data.payload?.enabled);
|
|
247
|
+
break;
|
|
248
|
+
case 'toggleOsNotification':
|
|
249
|
+
await this._handleToggleOsNotification(!!data.payload?.enabled);
|
|
250
|
+
break;
|
|
251
|
+
case 'toggleFeishuAck':
|
|
252
|
+
await this._handleToggleFeishuAck(!!data.payload?.enabled);
|
|
253
|
+
break;
|
|
176
254
|
}
|
|
177
255
|
});
|
|
178
256
|
// 当 view 变为可见时,检查当前请求
|
|
257
|
+
// (插件通知关闭时保持静默:即使用户从侧边栏切回面板也不显示缓存的请求)
|
|
179
258
|
webviewView.onDidChangeVisibility(() => {
|
|
180
|
-
if (webviewView.visible && this._currentRequest) {
|
|
259
|
+
if (webviewView.visible && this._currentRequest && this._isPluginNotifyEnabled()) {
|
|
181
260
|
this._showFeedbackRequest(this._currentRequest);
|
|
182
261
|
}
|
|
183
262
|
});
|
|
184
263
|
}
|
|
185
264
|
/**
|
|
186
265
|
* 开始轮询 MCP Server
|
|
266
|
+
*
|
|
267
|
+
* 自适应间隔:连上 server 时每秒轮询(兼作 server watchdog 心跳,必须 < 30s 空闲阈值);
|
|
268
|
+
* 完全找不到 server 时指数退避到最多 5s,降低空闲 CPU / 电池开销。
|
|
269
|
+
* 一旦发现任意 server 立即恢复 1s。
|
|
187
270
|
*/
|
|
188
271
|
startPolling() {
|
|
189
|
-
if (this.
|
|
272
|
+
if (this._polling) {
|
|
190
273
|
return;
|
|
191
274
|
}
|
|
192
|
-
|
|
193
|
-
this.
|
|
275
|
+
this._polling = true;
|
|
276
|
+
this._pollDelay = 1000;
|
|
277
|
+
const loop = async () => {
|
|
278
|
+
if (!this._polling)
|
|
279
|
+
return;
|
|
194
280
|
await this._pollForFeedbackRequest();
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
281
|
+
if (!this._polling)
|
|
282
|
+
return;
|
|
283
|
+
// 没连上任何 server → 退避;连上了 → 保持 1s 心跳
|
|
284
|
+
this._pollDelay = this._debugInfo.connectedPorts.length === 0
|
|
285
|
+
? Math.min(this._pollDelay + 1000, 5000)
|
|
286
|
+
: 1000;
|
|
287
|
+
this._pollTimer = setTimeout(loop, this._pollDelay);
|
|
288
|
+
};
|
|
289
|
+
loop();
|
|
198
290
|
}
|
|
199
291
|
/**
|
|
200
292
|
* 停止轮询
|
|
201
293
|
*/
|
|
202
294
|
stopPolling() {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
this.
|
|
295
|
+
this._polling = false;
|
|
296
|
+
if (this._pollTimer) {
|
|
297
|
+
clearTimeout(this._pollTimer);
|
|
298
|
+
this._pollTimer = null;
|
|
206
299
|
}
|
|
207
300
|
}
|
|
208
301
|
/**
|
|
@@ -213,29 +306,34 @@ class FeedbackViewProvider {
|
|
|
213
306
|
try {
|
|
214
307
|
// 更新工作区路径
|
|
215
308
|
const workspacePaths = getWorkspacePaths();
|
|
216
|
-
this._debugInfo.workspacePath = workspacePaths.length > 0 ? workspacePaths[0] : '
|
|
309
|
+
this._debugInfo.workspacePath = workspacePaths.length > 0 ? workspacePaths[0] : this._t('statusNoWorkspace');
|
|
217
310
|
const currentWorkspace = workspacePaths[0] || '';
|
|
218
|
-
const
|
|
219
|
-
const normalizedCurrentWorkspace = normalize(currentWorkspace);
|
|
311
|
+
const normalizedCurrentWorkspace = normalizePath(currentWorkspace);
|
|
220
312
|
// 如果有活跃端口,先尝试只轮询该端口
|
|
221
313
|
if (this._activePort) {
|
|
222
314
|
const result = await this._checkPortForRequest(this._activePort);
|
|
223
315
|
// 检查是否仍然是我们的 Server
|
|
224
316
|
if (result.connected) {
|
|
225
|
-
const serverOwner = result.ownerWorkspace ?
|
|
317
|
+
const serverOwner = result.ownerWorkspace ? normalizePath(result.ownerWorkspace) : '';
|
|
226
318
|
const isMyServer = !serverOwner || serverOwner === normalizedCurrentWorkspace;
|
|
227
319
|
if (isMyServer) {
|
|
228
320
|
// 端口仍然有效,保持使用
|
|
229
321
|
this._debugInfo.connectedPorts = [this._activePort];
|
|
230
322
|
this._debugInfo.activePort = this._activePort;
|
|
231
323
|
if (result.request && !this._seenRequestIds.has(result.request.id)) {
|
|
232
|
-
this._debugInfo.lastStatus =
|
|
324
|
+
this._debugInfo.lastStatus = this._t('statusListening', { port: this._activePort });
|
|
233
325
|
this._handleNewRequest(result.request, this._activePort);
|
|
234
326
|
this._updateDebugInfo();
|
|
235
327
|
return;
|
|
236
328
|
}
|
|
329
|
+
// 正在显示的请求被「飞书回复」结束 → 重置面板,避免提交到已消失的请求。
|
|
330
|
+
// 仅当 server 明确标记该请求是被飞书 resolve 的才重置——超时续期时 request 也会短暂为 null,
|
|
331
|
+
// 但 feishuResolvedId 不会命中,于是保持原有等待逻辑,不误重置、不丢用户草稿。
|
|
332
|
+
if (this._currentRequest && result.feishuResolvedId === this._currentRequest.id) {
|
|
333
|
+
this._handleExternalResolve();
|
|
334
|
+
}
|
|
237
335
|
// 端口有效但无新请求,继续保持连接
|
|
238
|
-
this._debugInfo.lastStatus =
|
|
336
|
+
this._debugInfo.lastStatus = this._t('statusListening', { port: this._activePort });
|
|
239
337
|
this._updateDebugInfo();
|
|
240
338
|
return;
|
|
241
339
|
}
|
|
@@ -258,7 +356,7 @@ class FeedbackViewProvider {
|
|
|
258
356
|
if (!r.request || this._seenRequestIds.has(r.request.id)) {
|
|
259
357
|
return false;
|
|
260
358
|
}
|
|
261
|
-
const serverOwner = r.ownerWorkspace ?
|
|
359
|
+
const serverOwner = r.ownerWorkspace ? normalizePath(r.ownerWorkspace) : '';
|
|
262
360
|
return !serverOwner || serverOwner === normalizedCurrentWorkspace;
|
|
263
361
|
}).sort((a, b) => b.request.timestamp - a.request.timestamp);
|
|
264
362
|
// 处理最新的请求
|
|
@@ -266,7 +364,7 @@ class FeedbackViewProvider {
|
|
|
266
364
|
const newest = myRequests[0];
|
|
267
365
|
this._activePort = newest.port;
|
|
268
366
|
this._debugInfo.activePort = newest.port;
|
|
269
|
-
this._debugInfo.lastStatus =
|
|
367
|
+
this._debugInfo.lastStatus = this._t('statusFound', { port: newest.port });
|
|
270
368
|
this._handleNewRequest(newest.request, newest.port);
|
|
271
369
|
this._updateDebugInfo();
|
|
272
370
|
return;
|
|
@@ -274,22 +372,22 @@ class FeedbackViewProvider {
|
|
|
274
372
|
// 没有新请求,检查是否有当前请求
|
|
275
373
|
if (this._currentRequest && this._activePort) {
|
|
276
374
|
this._debugInfo.activePort = this._activePort;
|
|
277
|
-
this._debugInfo.lastStatus =
|
|
375
|
+
this._debugInfo.lastStatus = this._t('statusListening', { port: this._activePort });
|
|
278
376
|
this._updateDebugInfo();
|
|
279
377
|
return;
|
|
280
378
|
}
|
|
281
379
|
// 没有任何请求
|
|
282
380
|
this._debugInfo.activePort = null;
|
|
283
381
|
if (this._debugInfo.connectedPorts.length === 0) {
|
|
284
|
-
this._debugInfo.lastStatus = '
|
|
382
|
+
this._debugInfo.lastStatus = this._t('statusNoServer');
|
|
285
383
|
}
|
|
286
384
|
else {
|
|
287
|
-
this._debugInfo.lastStatus =
|
|
385
|
+
this._debugInfo.lastStatus = this._t('statusConnected', { count: this._debugInfo.connectedPorts.length });
|
|
288
386
|
}
|
|
289
387
|
this._updateDebugInfo();
|
|
290
388
|
}
|
|
291
389
|
catch (error) {
|
|
292
|
-
this._debugInfo.lastStatus =
|
|
390
|
+
this._debugInfo.lastStatus = this._t('statusPollError', { error: String(error) });
|
|
293
391
|
this._updateDebugInfo();
|
|
294
392
|
}
|
|
295
393
|
}
|
|
@@ -309,12 +407,14 @@ class FeedbackViewProvider {
|
|
|
309
407
|
// 旧格式: FeedbackRequest | null
|
|
310
408
|
let request;
|
|
311
409
|
let ownerWorkspace = null;
|
|
312
|
-
let
|
|
410
|
+
let feishuResolvedId = null;
|
|
313
411
|
if (parsed && typeof parsed === 'object' && 'startTime' in parsed) {
|
|
314
412
|
// 新格式
|
|
315
413
|
request = parsed.request;
|
|
316
414
|
ownerWorkspace = parsed.ownerWorkspace;
|
|
317
|
-
|
|
415
|
+
feishuResolvedId = parsed.feishuResolvedId ?? null;
|
|
416
|
+
this._maybeSyncFeishu(port, parsed.feishu);
|
|
417
|
+
this._maybeSyncAutoRetry(parsed.autoRetry);
|
|
318
418
|
}
|
|
319
419
|
else {
|
|
320
420
|
// 旧格式(兼容 npm 上的旧版本)
|
|
@@ -325,10 +425,10 @@ class FeedbackViewProvider {
|
|
|
325
425
|
const isMatch = isPathInWorkspace(request.projectDir);
|
|
326
426
|
if (!isMatch) {
|
|
327
427
|
// 请求不属于当前工作区,返回特殊标记
|
|
328
|
-
return { connected: true, request: null, port, mismatch: true, ownerWorkspace
|
|
428
|
+
return { connected: true, request: null, port, mismatch: true, ownerWorkspace };
|
|
329
429
|
}
|
|
330
430
|
}
|
|
331
|
-
return { connected: true, request, port, ownerWorkspace,
|
|
431
|
+
return { connected: true, request, port, ownerWorkspace, feishuResolvedId };
|
|
332
432
|
}
|
|
333
433
|
catch {
|
|
334
434
|
return { connected: false, request: null, port };
|
|
@@ -345,7 +445,6 @@ class FeedbackViewProvider {
|
|
|
345
445
|
// 判断是否为"新鲜"请求:创建后 10 秒内被发现
|
|
346
446
|
const requestAge = Date.now() - request.timestamp;
|
|
347
447
|
const isFreshRequest = requestAge < 10000; // 10秒内
|
|
348
|
-
console.log(`Feedback request on port ${port}:`, request.id, `age: ${requestAge}ms, isFresh: ${isFreshRequest}`);
|
|
349
448
|
// 标记为已见过
|
|
350
449
|
this._seenRequestIds.add(request.id);
|
|
351
450
|
// 清理旧的请求 ID(保留最近 100 个)
|
|
@@ -356,12 +455,18 @@ class FeedbackViewProvider {
|
|
|
356
455
|
if (!this._currentRequest || request.id !== this._currentRequest.id) {
|
|
357
456
|
this._currentRequest = request;
|
|
358
457
|
this._activePort = port;
|
|
359
|
-
//
|
|
458
|
+
// 「插件通知」主开关(配置 key 历史原因仍叫 systemNotification):关掉后本窗口完全静默——
|
|
459
|
+
// 不推送内容、不弹面板、不抢焦点、不发系统通知;连用户之后主动切回 / 打开面板也不显示
|
|
460
|
+
// (见 resolveWebviewView 里 ready 与 onDidChangeVisibility 的同款判断)。
|
|
461
|
+
// 请求仍记录在 _currentRequest,仅用于去重与外部渠道(飞书 / 超时)resolve 关联。
|
|
462
|
+
if (!this._isPluginNotifyEnabled()) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
// 开启:推送内容并显示面板
|
|
360
466
|
this._showFeedbackRequest(request);
|
|
361
|
-
//
|
|
467
|
+
// 只对新鲜请求做主动提醒(聚焦面板 + IDE 提示 + 失焦系统通知)
|
|
362
468
|
if (isFreshRequest) {
|
|
363
469
|
vscode.commands.executeCommand('cursorFeedback.feedbackView.focus');
|
|
364
|
-
vscode.window.showInformationMessage(this._i18n.aiWaitingFeedback);
|
|
365
470
|
this._sendSystemNotification(request);
|
|
366
471
|
}
|
|
367
472
|
}
|
|
@@ -380,6 +485,10 @@ class FeedbackViewProvider {
|
|
|
380
485
|
if (!config.get('systemNotification', true)) {
|
|
381
486
|
return;
|
|
382
487
|
}
|
|
488
|
+
// 子开关「失焦时系统提示」:关掉后即使窗口失焦也不弹系统通知
|
|
489
|
+
if (!config.get('osNotification', true)) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
383
492
|
if (vscode.window.state.focused) {
|
|
384
493
|
return;
|
|
385
494
|
}
|
|
@@ -455,6 +564,15 @@ class FeedbackViewProvider {
|
|
|
455
564
|
payload: { connected: false }
|
|
456
565
|
});
|
|
457
566
|
}
|
|
567
|
+
/**
|
|
568
|
+
* 「插件通知」主开关是否开启(配置 key 历史原因仍叫 systemNotification)。
|
|
569
|
+
* 关闭即本窗口完全静默:不主动推送,被动切回 / ready 时也不显示缓存的请求。
|
|
570
|
+
*/
|
|
571
|
+
_isPluginNotifyEnabled() {
|
|
572
|
+
return vscode.workspace
|
|
573
|
+
.getConfiguration('cursorFeedback')
|
|
574
|
+
.get('systemNotification', true);
|
|
575
|
+
}
|
|
458
576
|
/**
|
|
459
577
|
* 显示反馈请求
|
|
460
578
|
*/
|
|
@@ -465,7 +583,7 @@ class FeedbackViewProvider {
|
|
|
465
583
|
type: 'showFeedbackRequest',
|
|
466
584
|
payload: {
|
|
467
585
|
requestId: request.id,
|
|
468
|
-
summary: request.summary,
|
|
586
|
+
summary: this._inlineLocalImages(request.summary),
|
|
469
587
|
projectDir: request.projectDir,
|
|
470
588
|
timeout: request.timeout,
|
|
471
589
|
timestamp: request.timestamp
|
|
@@ -483,6 +601,18 @@ class FeedbackViewProvider {
|
|
|
483
601
|
});
|
|
484
602
|
}
|
|
485
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* 当前请求在 server 端被外部渠道结束(如飞书回复 / 超时)→ 重置面板,
|
|
606
|
+
* 防止用户对着已消失的请求提交,导致 "Request not found"。
|
|
607
|
+
*/
|
|
608
|
+
_handleExternalResolve() {
|
|
609
|
+
if (!this._currentRequest)
|
|
610
|
+
return;
|
|
611
|
+
this._seenRequestIds.add(this._currentRequest.id);
|
|
612
|
+
this._currentRequest = null;
|
|
613
|
+
this._showWaitingState();
|
|
614
|
+
this._view?.webview.postMessage({ type: 'externalResolved' });
|
|
615
|
+
}
|
|
486
616
|
/**
|
|
487
617
|
* 更新调试信息到 WebView
|
|
488
618
|
*/
|
|
@@ -512,7 +642,6 @@ class FeedbackViewProvider {
|
|
|
512
642
|
}));
|
|
513
643
|
const result = JSON.parse(response);
|
|
514
644
|
if (result.success) {
|
|
515
|
-
vscode.window.showInformationMessage(this._i18n.feedbackSubmitted);
|
|
516
645
|
this._currentRequest = null;
|
|
517
646
|
this._showWaitingState();
|
|
518
647
|
}
|
|
@@ -542,6 +671,72 @@ class FeedbackViewProvider {
|
|
|
542
671
|
});
|
|
543
672
|
}
|
|
544
673
|
}
|
|
674
|
+
setLastActiveEditor(editor) {
|
|
675
|
+
this._lastActiveEditor = editor;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* 带入最近活动编辑器的选中代码(或整个文件)到反馈输入框
|
|
679
|
+
*/
|
|
680
|
+
async insertSelectionToFeedback() {
|
|
681
|
+
const editor = this._lastActiveEditor || vscode.window.activeTextEditor;
|
|
682
|
+
await vscode.commands.executeCommand('cursorFeedback.feedbackView.focus');
|
|
683
|
+
if (!editor) {
|
|
684
|
+
this._postOrQueue({ type: 'insertContextEmpty' });
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const doc = editor.document;
|
|
688
|
+
const sel = editor.selection;
|
|
689
|
+
const filePath = doc.uri.fsPath;
|
|
690
|
+
if (sel && !sel.isEmpty) {
|
|
691
|
+
this._postOrQueue({
|
|
692
|
+
type: 'insertContext',
|
|
693
|
+
payload: {
|
|
694
|
+
filePath,
|
|
695
|
+
lang: doc.languageId,
|
|
696
|
+
code: doc.getText(sel),
|
|
697
|
+
startLine: sel.start.line + 1,
|
|
698
|
+
endLine: sel.end.line + 1
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
this._postOrQueue({
|
|
704
|
+
type: 'filesSelected',
|
|
705
|
+
payload: { paths: [filePath] }
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
/** 发送插入消息;若 webview 尚未就绪(focus 触发重建),先排队,待 ready 后补发 */
|
|
710
|
+
_postOrQueue(msg) {
|
|
711
|
+
this._pendingInsert = msg;
|
|
712
|
+
if (this._webviewReady) {
|
|
713
|
+
this._flushPendingInsert();
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
_flushPendingInsert() {
|
|
717
|
+
if (this._pendingInsert && this._view && this._webviewReady) {
|
|
718
|
+
this._view.webview.postMessage(this._pendingInsert);
|
|
719
|
+
this._pendingInsert = null;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* 搜索工作区文件,回传给 webview 做 @ 引用选择
|
|
724
|
+
*/
|
|
725
|
+
async _handleSearchFiles() {
|
|
726
|
+
try {
|
|
727
|
+
const uris = await vscode.workspace.findFiles('**/*', '**/{node_modules,.git,dist,out,build,.next,.cache,coverage}/**', 1000);
|
|
728
|
+
const files = uris.map(u => ({
|
|
729
|
+
path: u.fsPath,
|
|
730
|
+
name: u.path.split('/').pop() || u.fsPath,
|
|
731
|
+
rel: vscode.workspace.asRelativePath(u)
|
|
732
|
+
}));
|
|
733
|
+
files.sort((a, b) => a.rel.length - b.rel.length);
|
|
734
|
+
this._view?.webview.postMessage({ type: 'fileSearchResults', payload: { files } });
|
|
735
|
+
}
|
|
736
|
+
catch {
|
|
737
|
+
this._view?.webview.postMessage({ type: 'fileSearchResults', payload: { files: [] } });
|
|
738
|
+
}
|
|
739
|
+
}
|
|
545
740
|
/**
|
|
546
741
|
* 处理语言切换
|
|
547
742
|
*/
|
|
@@ -569,12 +764,188 @@ class FeedbackViewProvider {
|
|
|
569
764
|
if (this._view) {
|
|
570
765
|
this._view.webview.html = this._getHtmlForWebview(this._view.webview);
|
|
571
766
|
}
|
|
572
|
-
const effectiveLang = (0, i18n_1.getLanguage)();
|
|
573
|
-
vscode.window.showInformationMessage(effectiveLang === 'en'
|
|
574
|
-
? 'Language changed to English'
|
|
575
|
-
: '语言已切换为简体中文');
|
|
576
767
|
}
|
|
577
768
|
}
|
|
769
|
+
/**
|
|
770
|
+
* 切换超时续期开关(AI 超时后是否继续等待用户)
|
|
771
|
+
*/
|
|
772
|
+
async _handleToggleAutoRetry() {
|
|
773
|
+
this._autoRetry = !this._autoRetry;
|
|
774
|
+
await this._memento?.update('autoRetry', this._autoRetry);
|
|
775
|
+
this._postAutoRetryState();
|
|
776
|
+
this._broadcastAutoRetry();
|
|
777
|
+
}
|
|
778
|
+
/** 把续期开关 POST 给所有已连接 server(server 写同一份磁盘、全局一致、重启保留) */
|
|
779
|
+
_broadcastAutoRetry() {
|
|
780
|
+
const ports = new Set(this._debugInfo.connectedPorts || []);
|
|
781
|
+
if (this._activePort)
|
|
782
|
+
ports.add(this._activePort);
|
|
783
|
+
const body = JSON.stringify({ autoRetry: this._autoRetry });
|
|
784
|
+
for (const port of ports) {
|
|
785
|
+
this._httpPost(`http://127.0.0.1:${port}/api/settings/autoRetry`, body).catch(() => { });
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* 把超时续期开关状态推给 WebView
|
|
790
|
+
*/
|
|
791
|
+
_postAutoRetryState() {
|
|
792
|
+
this._view?.webview.postMessage({
|
|
793
|
+
type: 'autoRetryState',
|
|
794
|
+
payload: { enabled: this._autoRetry }
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
/** 从 server poll 回读续期开关并回显到 UI(server 为真相源,单向同步,避免多窗口拉锯) */
|
|
798
|
+
_maybeSyncAutoRetry(v) {
|
|
799
|
+
if (typeof v !== 'boolean' || v === this._autoRetry)
|
|
800
|
+
return;
|
|
801
|
+
this._autoRetry = v;
|
|
802
|
+
this._memento?.update('autoRetry', v);
|
|
803
|
+
this._postAutoRetryState();
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* 把飞书配置状态推给 WebView(含 secret 明文,前端用小眼睛切换显示/隐藏)
|
|
807
|
+
*/
|
|
808
|
+
_postFeishuState() {
|
|
809
|
+
const { appId, appSecret } = this._feishuConfig;
|
|
810
|
+
const cfg = vscode.workspace.getConfiguration('cursorFeedback');
|
|
811
|
+
const systemNotification = cfg.get('systemNotification', true);
|
|
812
|
+
const osNotification = cfg.get('osNotification', true);
|
|
813
|
+
this._view?.webview.postMessage({
|
|
814
|
+
type: 'feishuState',
|
|
815
|
+
payload: {
|
|
816
|
+
appId,
|
|
817
|
+
appSecret,
|
|
818
|
+
hasSecret: !!appSecret,
|
|
819
|
+
configured: !!(appId && appSecret),
|
|
820
|
+
bound: this._feishuBound,
|
|
821
|
+
feishuEnabled: this._feishuEnabled,
|
|
822
|
+
feishuAck: this._feishuAck,
|
|
823
|
+
systemNotification: !!systemNotification,
|
|
824
|
+
osNotification: !!osNotification
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* 处理保存飞书配置(来自 webview)
|
|
830
|
+
* - 所见即生效:appId/secret 直接用输入框的值,删空就是删空(半填 = 凭证不全 = server 关闭)。
|
|
831
|
+
* - appId 变化:本地先解绑(真相仍以 server 磁盘 / poll 回读为准)。
|
|
832
|
+
*/
|
|
833
|
+
async _handleSaveFeishuConfig(payload) {
|
|
834
|
+
const appId = (payload?.appId || '').trim();
|
|
835
|
+
const appSecret = (payload?.appSecret || '').trim();
|
|
836
|
+
if (appId !== this._feishuConfig.appId)
|
|
837
|
+
this._feishuBound = false;
|
|
838
|
+
this._feishuConfig = { appId, appSecret };
|
|
839
|
+
// 占位缓存(真相在 server 磁盘,poll 会回读覆盖)
|
|
840
|
+
await this._memento?.update('feishuConfig', this._feishuConfig);
|
|
841
|
+
// 下发给所有端口:server 写磁盘(全局真相源)+ configure;清空则下发空 → server 关闭 / 清空。
|
|
842
|
+
this._broadcastFeishuConfig();
|
|
843
|
+
this._postFeishuState();
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* 轮询时把 server 的飞书状态同步到本地用于回显。
|
|
847
|
+
* 凭证真相源在 server 端磁盘(跨窗口共享):插件只读不回写,从根上消除多窗口「A 删 B 又补回」的拉锯。
|
|
848
|
+
* 同步凭证 / 开关 / Get 表情 / 绑定,有变化才刷新面板。
|
|
849
|
+
*/
|
|
850
|
+
_maybeSyncFeishu(_port, feishuStatus) {
|
|
851
|
+
if (!feishuStatus)
|
|
852
|
+
return;
|
|
853
|
+
const appId = feishuStatus.appId || '';
|
|
854
|
+
const appSecret = feishuStatus.appSecret || '';
|
|
855
|
+
const enabled = feishuStatus.enabled !== false;
|
|
856
|
+
const ack = feishuStatus.ackReaction !== false;
|
|
857
|
+
const bound = !!feishuStatus.boundChatId;
|
|
858
|
+
let changed = false;
|
|
859
|
+
if (appId !== this._feishuConfig.appId || appSecret !== this._feishuConfig.appSecret) {
|
|
860
|
+
this._feishuConfig = { appId, appSecret };
|
|
861
|
+
// 落一份到 globalState 作首屏占位缓存(真相仍以 server 为准)
|
|
862
|
+
this._memento?.update('feishuConfig', this._feishuConfig);
|
|
863
|
+
changed = true;
|
|
864
|
+
}
|
|
865
|
+
if (enabled !== this._feishuEnabled) {
|
|
866
|
+
this._feishuEnabled = enabled;
|
|
867
|
+
changed = true;
|
|
868
|
+
}
|
|
869
|
+
if (ack !== this._feishuAck) {
|
|
870
|
+
this._feishuAck = ack;
|
|
871
|
+
changed = true;
|
|
872
|
+
}
|
|
873
|
+
if (bound !== this._feishuBound) {
|
|
874
|
+
this._feishuBound = bound;
|
|
875
|
+
changed = true;
|
|
876
|
+
}
|
|
877
|
+
if (changed)
|
|
878
|
+
this._postFeishuState();
|
|
879
|
+
}
|
|
880
|
+
/** 把当前飞书配置 POST 给所有已连接的 server(server 端写同一份磁盘、全局一致;清空即下发空以关闭) */
|
|
881
|
+
_broadcastFeishuConfig() {
|
|
882
|
+
const ports = new Set(this._debugInfo.connectedPorts || []);
|
|
883
|
+
if (this._activePort)
|
|
884
|
+
ports.add(this._activePort);
|
|
885
|
+
const { appId, appSecret } = this._feishuConfig;
|
|
886
|
+
const body = JSON.stringify({
|
|
887
|
+
appId,
|
|
888
|
+
appSecret,
|
|
889
|
+
enabled: this._feishuEnabled,
|
|
890
|
+
ackReaction: this._feishuAck,
|
|
891
|
+
});
|
|
892
|
+
for (const port of ports) {
|
|
893
|
+
this._httpPost(`http://127.0.0.1:${port}/api/feishu/config`, body).catch(() => { });
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* 打开「如何配置飞书机器人」指引
|
|
898
|
+
*/
|
|
899
|
+
async _handleOpenFeishuGuide() {
|
|
900
|
+
const guideUri = vscode.Uri.joinPath(this._extensionUri, 'docs', 'feishu-setup.md');
|
|
901
|
+
try {
|
|
902
|
+
await vscode.commands.executeCommand('markdown.showPreview', guideUri);
|
|
903
|
+
}
|
|
904
|
+
catch {
|
|
905
|
+
try {
|
|
906
|
+
await vscode.env.openExternal(guideUri);
|
|
907
|
+
}
|
|
908
|
+
catch {
|
|
909
|
+
// ignore
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* 切换飞书通知开关(关了即使已配置也不推飞书;长连接仍保留以便绑定/回复)
|
|
915
|
+
*/
|
|
916
|
+
async _handleToggleFeishuEnabled(enabled) {
|
|
917
|
+
this._feishuEnabled = enabled;
|
|
918
|
+
await this._memento?.update('feishuEnabled', enabled);
|
|
919
|
+
this._broadcastFeishuConfig();
|
|
920
|
+
this._postFeishuState();
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* 切换系统通知开关(写入 VSCode 配置,_sendSystemNotification 会读取)
|
|
924
|
+
*/
|
|
925
|
+
async _handleToggleSystemNotification(enabled) {
|
|
926
|
+
await vscode.workspace
|
|
927
|
+
.getConfiguration('cursorFeedback')
|
|
928
|
+
.update('systemNotification', enabled, vscode.ConfigurationTarget.Global);
|
|
929
|
+
this._postFeishuState();
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* 切换「失焦时系统提示」子开关(插件通知的子项,写入 VSCode 配置)
|
|
933
|
+
*/
|
|
934
|
+
async _handleToggleOsNotification(enabled) {
|
|
935
|
+
await vscode.workspace
|
|
936
|
+
.getConfiguration('cursorFeedback')
|
|
937
|
+
.update('osNotification', enabled, vscode.ConfigurationTarget.Global);
|
|
938
|
+
this._postFeishuState();
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* 切换「Get 表情回执」子开关(飞书通知的子项;透传到 server 端 feishu bridge)
|
|
942
|
+
*/
|
|
943
|
+
async _handleToggleFeishuAck(enabled) {
|
|
944
|
+
this._feishuAck = enabled;
|
|
945
|
+
await this._memento?.update('feishuAck', enabled);
|
|
946
|
+
this._broadcastFeishuConfig();
|
|
947
|
+
this._postFeishuState();
|
|
948
|
+
}
|
|
578
949
|
/**
|
|
579
950
|
* HTTP GET 请求
|
|
580
951
|
*/
|
|
@@ -623,29 +994,85 @@ class FeedbackViewProvider {
|
|
|
623
994
|
req.end();
|
|
624
995
|
});
|
|
625
996
|
}
|
|
997
|
+
/**
|
|
998
|
+
* 把 summary 里的本地图片路径内联成 base64 data:,
|
|
999
|
+
* 这样 webview 无需 file:// 访问权限即可显示 AI 发来的本地图片。
|
|
1000
|
+
* 网络图(http/https)和已有的 data:/blob: 不处理,交给 CSP 放行。
|
|
1001
|
+
*/
|
|
1002
|
+
_inlineLocalImages(md) {
|
|
1003
|
+
if (!md)
|
|
1004
|
+
return md;
|
|
1005
|
+
return md.replace(/!\[([^\]]*)\]\(\s*([^)\s]+)(\s+"[^"]*")?\s*\)/g, (full, alt, src, title) => {
|
|
1006
|
+
try {
|
|
1007
|
+
let p = String(src).trim().replace(/^["']|["']$/g, '');
|
|
1008
|
+
if (/^(https?:|data:|blob:)/i.test(p))
|
|
1009
|
+
return full;
|
|
1010
|
+
if (p.startsWith('file://'))
|
|
1011
|
+
p = decodeURIComponent(p.replace(/^file:\/\//, ''));
|
|
1012
|
+
if (!path.isAbsolute(p) || !fs.existsSync(p))
|
|
1013
|
+
return full;
|
|
1014
|
+
const mime = this._imageMime(p);
|
|
1015
|
+
if (!mime)
|
|
1016
|
+
return full;
|
|
1017
|
+
const b64 = fs.readFileSync(p).toString('base64');
|
|
1018
|
+
return ``;
|
|
1019
|
+
}
|
|
1020
|
+
catch {
|
|
1021
|
+
return full;
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
_imageMime(p) {
|
|
1026
|
+
switch (path.extname(p).toLowerCase()) {
|
|
1027
|
+
case '.png': return 'image/png';
|
|
1028
|
+
case '.jpg':
|
|
1029
|
+
case '.jpeg': return 'image/jpeg';
|
|
1030
|
+
case '.gif': return 'image/gif';
|
|
1031
|
+
case '.webp': return 'image/webp';
|
|
1032
|
+
case '.svg': return 'image/svg+xml';
|
|
1033
|
+
case '.bmp': return 'image/bmp';
|
|
1034
|
+
default: return null;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
626
1037
|
_getHtmlForWebview(webview) {
|
|
627
1038
|
// 获取资源文件的 URI
|
|
628
1039
|
const markedJsUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'resources', 'vendor', 'marked.min.js'));
|
|
1040
|
+
const highlightJsUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'resources', 'vendor', 'highlight.min.js'));
|
|
629
1041
|
const stylesCssUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist', 'webview', 'styles.css'));
|
|
630
1042
|
const scriptJsUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist', 'webview', 'script.js'));
|
|
631
1043
|
// 读取 HTML 模板
|
|
632
1044
|
const htmlTemplatePath = path.join(this._extensionUri.fsPath, 'dist', 'webview', 'index.html');
|
|
633
1045
|
let htmlTemplate = fs.readFileSync(htmlTemplatePath, 'utf-8');
|
|
634
|
-
// CSP
|
|
635
|
-
const csp = `default-src 'none'; style-src ${webview.cspSource}; script-src 'unsafe-inline' ${webview.cspSource}; img-src data:;`;
|
|
1046
|
+
// CSP 策略:放宽 img-src 以支持 AI 在 summary 里发来的网络图片 / vscode 资源 / base64
|
|
1047
|
+
const csp = `default-src 'none'; style-src ${webview.cspSource}; script-src 'unsafe-inline' ${webview.cspSource}; img-src ${webview.cspSource} https: data: blob:;`;
|
|
636
1048
|
// 获取语言设置
|
|
637
1049
|
const language = (0, i18n_1.getLanguage)();
|
|
638
1050
|
const langCode = language === 'zh-TW' ? 'zh-TW' : (language === 'en' ? 'en' : 'zh-CN');
|
|
1051
|
+
// 按平台解析快捷键占位符(mac vs win/linux),未知占位符保持原样
|
|
1052
|
+
const isMac = process.platform === 'darwin';
|
|
1053
|
+
const placeholders = {
|
|
1054
|
+
shortcut: isMac ? '⇧⌘\'' : 'Ctrl+Shift+\'',
|
|
1055
|
+
ctrlEnter: isMac ? '⌘Enter' : 'Ctrl+Enter',
|
|
1056
|
+
shiftEnter: isMac ? '⇧Enter' : 'Shift+Enter',
|
|
1057
|
+
};
|
|
1058
|
+
const i18nResolved = {};
|
|
1059
|
+
for (const k of Object.keys(this._i18n)) {
|
|
1060
|
+
const v = this._i18n[k];
|
|
1061
|
+
i18nResolved[k] = typeof v === 'string'
|
|
1062
|
+
? v.replace(/\{(\w+)\}/g, (m, name) => placeholders[name] ?? m)
|
|
1063
|
+
: v;
|
|
1064
|
+
}
|
|
639
1065
|
// 替换占位符
|
|
640
1066
|
htmlTemplate = htmlTemplate
|
|
641
1067
|
.replace(/\{\{CSP\}\}/g, csp)
|
|
642
1068
|
.replace(/\{\{LANG\}\}/g, langCode)
|
|
643
1069
|
.replace(/\{\{MARKED_JS_URI\}\}/g, markedJsUri.toString())
|
|
1070
|
+
.replace(/\{\{HIGHLIGHT_JS_URI\}\}/g, highlightJsUri.toString())
|
|
644
1071
|
.replace(/\{\{STYLES_CSS_URI\}\}/g, stylesCssUri.toString())
|
|
645
1072
|
.replace(/\{\{SCRIPT_JS_URI\}\}/g, scriptJsUri.toString())
|
|
646
|
-
.replace(/\{\{I18N_JSON\}\}/g, JSON.stringify(
|
|
1073
|
+
.replace(/\{\{I18N_JSON\}\}/g, JSON.stringify(i18nResolved))
|
|
647
1074
|
.replace(/\{\{i18n\.(\w+)\}\}/g, (_, key) => {
|
|
648
|
-
return
|
|
1075
|
+
return i18nResolved[key] || key;
|
|
649
1076
|
});
|
|
650
1077
|
return htmlTemplate;
|
|
651
1078
|
}
|