cursor-feedback 1.1.1 → 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 +14 -0
- package/README.md +48 -4
- package/README_CN.md +48 -4
- package/dist/extension.js +546 -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 +38 -1
- 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 -692
- 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
|
@@ -39,14 +39,17 @@ const vscode = __importStar(require("vscode"));
|
|
|
39
39
|
const http = __importStar(require("http"));
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
42
43
|
const i18n_1 = require("./i18n");
|
|
43
44
|
let feedbackViewProvider = null;
|
|
44
|
-
|
|
45
|
+
/** 规范化路径:统一分隔符、去尾斜杠、转小写,用于工作区匹配 */
|
|
46
|
+
function normalizePath(p) {
|
|
47
|
+
return (p || '').replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
48
|
+
}
|
|
45
49
|
function activate(context) {
|
|
46
|
-
console.log('Cursor Feedback extension is now active!');
|
|
47
50
|
// 注册侧边栏 WebView(端口从 61927 开始自动扫描)
|
|
48
|
-
feedbackViewProvider = new FeedbackViewProvider(context.extensionUri, 61927);
|
|
49
|
-
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 } }));
|
|
50
53
|
// 注册命令:显示反馈面板
|
|
51
54
|
context.subscriptions.push(vscode.commands.registerCommand('cursorFeedback.showPanel', () => {
|
|
52
55
|
vscode.commands.executeCommand('cursorFeedback.feedbackView.focus');
|
|
@@ -55,14 +58,25 @@ function activate(context) {
|
|
|
55
58
|
context.subscriptions.push(vscode.commands.registerCommand('cursorFeedback.startPolling', () => {
|
|
56
59
|
if (feedbackViewProvider) {
|
|
57
60
|
feedbackViewProvider.startPolling();
|
|
58
|
-
vscode.window.showInformationMessage(feedbackViewProvider.getMessage('startListening'));
|
|
59
61
|
}
|
|
60
62
|
}));
|
|
61
63
|
// 注册命令:停止轮询
|
|
62
64
|
context.subscriptions.push(vscode.commands.registerCommand('cursorFeedback.stopPolling', () => {
|
|
63
65
|
if (feedbackViewProvider) {
|
|
64
66
|
feedbackViewProvider.stopPolling();
|
|
65
|
-
|
|
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);
|
|
66
80
|
}
|
|
67
81
|
}));
|
|
68
82
|
// 自动开始轮询
|
|
@@ -93,8 +107,7 @@ function getWorkspacePaths() {
|
|
|
93
107
|
function isPathInWorkspace(targetPath) {
|
|
94
108
|
const workspacePaths = getWorkspacePaths();
|
|
95
109
|
// 规范化路径(去除末尾斜杠,统一分隔符,小写)
|
|
96
|
-
const
|
|
97
|
-
const normalizedTarget = normalize(targetPath);
|
|
110
|
+
const normalizedTarget = normalizePath(targetPath);
|
|
98
111
|
// 检查 targetPath 是否为空或默认值
|
|
99
112
|
const isEmptyPath = !targetPath || targetPath === '.' || normalizedTarget === '' || normalizedTarget === '.';
|
|
100
113
|
if (workspacePaths.length === 0) {
|
|
@@ -106,7 +119,7 @@ function isPathInWorkspace(targetPath) {
|
|
|
106
119
|
return false;
|
|
107
120
|
}
|
|
108
121
|
for (const wsPath of workspacePaths) {
|
|
109
|
-
const normalizedWs =
|
|
122
|
+
const normalizedWs = normalizePath(wsPath);
|
|
110
123
|
// 精确匹配:只匹配完全相同的路径
|
|
111
124
|
if (normalizedTarget === normalizedWs) {
|
|
112
125
|
return true;
|
|
@@ -118,13 +131,29 @@ function isPathInWorkspace(targetPath) {
|
|
|
118
131
|
* 侧边栏 WebView Provider
|
|
119
132
|
*/
|
|
120
133
|
class FeedbackViewProvider {
|
|
121
|
-
constructor(_extensionUri, port) {
|
|
134
|
+
constructor(_extensionUri, port, _memento) {
|
|
122
135
|
this._extensionUri = _extensionUri;
|
|
123
|
-
this.
|
|
136
|
+
this._memento = _memento;
|
|
137
|
+
this._polling = false;
|
|
138
|
+
this._pollTimer = null;
|
|
139
|
+
this._pollDelay = 1000;
|
|
124
140
|
this._currentRequest = null;
|
|
141
|
+
// webview 脚本是否就绪;插入上下文消息需等就绪后再发,否则 focus 唤起重建时会丢消息
|
|
142
|
+
this._webviewReady = false;
|
|
143
|
+
this._pendingInsert = null;
|
|
125
144
|
this._activePort = null;
|
|
126
145
|
this._portScanRange = 20; // 扫描端口范围
|
|
127
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;
|
|
128
157
|
this._debugInfo = {
|
|
129
158
|
portRange: '',
|
|
130
159
|
workspacePath: '',
|
|
@@ -136,6 +165,12 @@ class FeedbackViewProvider {
|
|
|
136
165
|
this._debugInfo.portRange = `${port}-${port + this._portScanRange - 1}`;
|
|
137
166
|
this._i18n = (0, i18n_1.loadMessages)(this._extensionUri.fsPath);
|
|
138
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;
|
|
139
174
|
}
|
|
140
175
|
/**
|
|
141
176
|
* 获取翻译消息
|
|
@@ -143,8 +178,19 @@ class FeedbackViewProvider {
|
|
|
143
178
|
getMessage(key) {
|
|
144
179
|
return this._i18n[key] || key;
|
|
145
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
|
+
}
|
|
146
191
|
resolveWebviewView(webviewView, _context, _token) {
|
|
147
192
|
this._view = webviewView;
|
|
193
|
+
this._webviewReady = false;
|
|
148
194
|
webviewView.webview.options = {
|
|
149
195
|
enableScripts: true,
|
|
150
196
|
localResourceRoots: [this._extensionUri]
|
|
@@ -157,11 +203,17 @@ class FeedbackViewProvider {
|
|
|
157
203
|
await this._handleFeedbackSubmit(data.payload);
|
|
158
204
|
break;
|
|
159
205
|
case 'ready':
|
|
160
|
-
|
|
206
|
+
this._webviewReady = true;
|
|
207
|
+
// 同步超时续期开关状态到 UI
|
|
208
|
+
this._postAutoRetryState();
|
|
209
|
+
this._postFeishuState();
|
|
161
210
|
// WebView 准备就绪后,检查是否有待处理的请求
|
|
162
|
-
|
|
211
|
+
// (插件通知关闭时保持静默,不显示缓存的请求)
|
|
212
|
+
if (this._currentRequest && this._isPluginNotifyEnabled()) {
|
|
163
213
|
this._showFeedbackRequest(this._currentRequest);
|
|
164
214
|
}
|
|
215
|
+
// 补发就绪前排队的「插入上下文」消息
|
|
216
|
+
this._flushPendingInsert();
|
|
165
217
|
break;
|
|
166
218
|
case 'checkServer':
|
|
167
219
|
await this._checkServerHealth();
|
|
@@ -172,36 +224,78 @@ class FeedbackViewProvider {
|
|
|
172
224
|
case 'switchLanguage':
|
|
173
225
|
await this._handleSwitchLanguage();
|
|
174
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;
|
|
175
254
|
}
|
|
176
255
|
});
|
|
177
256
|
// 当 view 变为可见时,检查当前请求
|
|
257
|
+
// (插件通知关闭时保持静默:即使用户从侧边栏切回面板也不显示缓存的请求)
|
|
178
258
|
webviewView.onDidChangeVisibility(() => {
|
|
179
|
-
if (webviewView.visible && this._currentRequest) {
|
|
259
|
+
if (webviewView.visible && this._currentRequest && this._isPluginNotifyEnabled()) {
|
|
180
260
|
this._showFeedbackRequest(this._currentRequest);
|
|
181
261
|
}
|
|
182
262
|
});
|
|
183
263
|
}
|
|
184
264
|
/**
|
|
185
265
|
* 开始轮询 MCP Server
|
|
266
|
+
*
|
|
267
|
+
* 自适应间隔:连上 server 时每秒轮询(兼作 server watchdog 心跳,必须 < 30s 空闲阈值);
|
|
268
|
+
* 完全找不到 server 时指数退避到最多 5s,降低空闲 CPU / 电池开销。
|
|
269
|
+
* 一旦发现任意 server 立即恢复 1s。
|
|
186
270
|
*/
|
|
187
271
|
startPolling() {
|
|
188
|
-
if (this.
|
|
272
|
+
if (this._polling) {
|
|
189
273
|
return;
|
|
190
274
|
}
|
|
191
|
-
|
|
192
|
-
this.
|
|
275
|
+
this._polling = true;
|
|
276
|
+
this._pollDelay = 1000;
|
|
277
|
+
const loop = async () => {
|
|
278
|
+
if (!this._polling)
|
|
279
|
+
return;
|
|
193
280
|
await this._pollForFeedbackRequest();
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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();
|
|
197
290
|
}
|
|
198
291
|
/**
|
|
199
292
|
* 停止轮询
|
|
200
293
|
*/
|
|
201
294
|
stopPolling() {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
this.
|
|
295
|
+
this._polling = false;
|
|
296
|
+
if (this._pollTimer) {
|
|
297
|
+
clearTimeout(this._pollTimer);
|
|
298
|
+
this._pollTimer = null;
|
|
205
299
|
}
|
|
206
300
|
}
|
|
207
301
|
/**
|
|
@@ -212,29 +306,34 @@ class FeedbackViewProvider {
|
|
|
212
306
|
try {
|
|
213
307
|
// 更新工作区路径
|
|
214
308
|
const workspacePaths = getWorkspacePaths();
|
|
215
|
-
this._debugInfo.workspacePath = workspacePaths.length > 0 ? workspacePaths[0] : '
|
|
309
|
+
this._debugInfo.workspacePath = workspacePaths.length > 0 ? workspacePaths[0] : this._t('statusNoWorkspace');
|
|
216
310
|
const currentWorkspace = workspacePaths[0] || '';
|
|
217
|
-
const
|
|
218
|
-
const normalizedCurrentWorkspace = normalize(currentWorkspace);
|
|
311
|
+
const normalizedCurrentWorkspace = normalizePath(currentWorkspace);
|
|
219
312
|
// 如果有活跃端口,先尝试只轮询该端口
|
|
220
313
|
if (this._activePort) {
|
|
221
314
|
const result = await this._checkPortForRequest(this._activePort);
|
|
222
315
|
// 检查是否仍然是我们的 Server
|
|
223
316
|
if (result.connected) {
|
|
224
|
-
const serverOwner = result.ownerWorkspace ?
|
|
317
|
+
const serverOwner = result.ownerWorkspace ? normalizePath(result.ownerWorkspace) : '';
|
|
225
318
|
const isMyServer = !serverOwner || serverOwner === normalizedCurrentWorkspace;
|
|
226
319
|
if (isMyServer) {
|
|
227
320
|
// 端口仍然有效,保持使用
|
|
228
321
|
this._debugInfo.connectedPorts = [this._activePort];
|
|
229
322
|
this._debugInfo.activePort = this._activePort;
|
|
230
323
|
if (result.request && !this._seenRequestIds.has(result.request.id)) {
|
|
231
|
-
this._debugInfo.lastStatus =
|
|
324
|
+
this._debugInfo.lastStatus = this._t('statusListening', { port: this._activePort });
|
|
232
325
|
this._handleNewRequest(result.request, this._activePort);
|
|
233
326
|
this._updateDebugInfo();
|
|
234
327
|
return;
|
|
235
328
|
}
|
|
329
|
+
// 正在显示的请求被「飞书回复」结束 → 重置面板,避免提交到已消失的请求。
|
|
330
|
+
// 仅当 server 明确标记该请求是被飞书 resolve 的才重置——超时续期时 request 也会短暂为 null,
|
|
331
|
+
// 但 feishuResolvedId 不会命中,于是保持原有等待逻辑,不误重置、不丢用户草稿。
|
|
332
|
+
if (this._currentRequest && result.feishuResolvedId === this._currentRequest.id) {
|
|
333
|
+
this._handleExternalResolve();
|
|
334
|
+
}
|
|
236
335
|
// 端口有效但无新请求,继续保持连接
|
|
237
|
-
this._debugInfo.lastStatus =
|
|
336
|
+
this._debugInfo.lastStatus = this._t('statusListening', { port: this._activePort });
|
|
238
337
|
this._updateDebugInfo();
|
|
239
338
|
return;
|
|
240
339
|
}
|
|
@@ -257,7 +356,7 @@ class FeedbackViewProvider {
|
|
|
257
356
|
if (!r.request || this._seenRequestIds.has(r.request.id)) {
|
|
258
357
|
return false;
|
|
259
358
|
}
|
|
260
|
-
const serverOwner = r.ownerWorkspace ?
|
|
359
|
+
const serverOwner = r.ownerWorkspace ? normalizePath(r.ownerWorkspace) : '';
|
|
261
360
|
return !serverOwner || serverOwner === normalizedCurrentWorkspace;
|
|
262
361
|
}).sort((a, b) => b.request.timestamp - a.request.timestamp);
|
|
263
362
|
// 处理最新的请求
|
|
@@ -265,7 +364,7 @@ class FeedbackViewProvider {
|
|
|
265
364
|
const newest = myRequests[0];
|
|
266
365
|
this._activePort = newest.port;
|
|
267
366
|
this._debugInfo.activePort = newest.port;
|
|
268
|
-
this._debugInfo.lastStatus =
|
|
367
|
+
this._debugInfo.lastStatus = this._t('statusFound', { port: newest.port });
|
|
269
368
|
this._handleNewRequest(newest.request, newest.port);
|
|
270
369
|
this._updateDebugInfo();
|
|
271
370
|
return;
|
|
@@ -273,22 +372,22 @@ class FeedbackViewProvider {
|
|
|
273
372
|
// 没有新请求,检查是否有当前请求
|
|
274
373
|
if (this._currentRequest && this._activePort) {
|
|
275
374
|
this._debugInfo.activePort = this._activePort;
|
|
276
|
-
this._debugInfo.lastStatus =
|
|
375
|
+
this._debugInfo.lastStatus = this._t('statusListening', { port: this._activePort });
|
|
277
376
|
this._updateDebugInfo();
|
|
278
377
|
return;
|
|
279
378
|
}
|
|
280
379
|
// 没有任何请求
|
|
281
380
|
this._debugInfo.activePort = null;
|
|
282
381
|
if (this._debugInfo.connectedPorts.length === 0) {
|
|
283
|
-
this._debugInfo.lastStatus = '
|
|
382
|
+
this._debugInfo.lastStatus = this._t('statusNoServer');
|
|
284
383
|
}
|
|
285
384
|
else {
|
|
286
|
-
this._debugInfo.lastStatus =
|
|
385
|
+
this._debugInfo.lastStatus = this._t('statusConnected', { count: this._debugInfo.connectedPorts.length });
|
|
287
386
|
}
|
|
288
387
|
this._updateDebugInfo();
|
|
289
388
|
}
|
|
290
389
|
catch (error) {
|
|
291
|
-
this._debugInfo.lastStatus =
|
|
390
|
+
this._debugInfo.lastStatus = this._t('statusPollError', { error: String(error) });
|
|
292
391
|
this._updateDebugInfo();
|
|
293
392
|
}
|
|
294
393
|
}
|
|
@@ -308,12 +407,14 @@ class FeedbackViewProvider {
|
|
|
308
407
|
// 旧格式: FeedbackRequest | null
|
|
309
408
|
let request;
|
|
310
409
|
let ownerWorkspace = null;
|
|
311
|
-
let
|
|
410
|
+
let feishuResolvedId = null;
|
|
312
411
|
if (parsed && typeof parsed === 'object' && 'startTime' in parsed) {
|
|
313
412
|
// 新格式
|
|
314
413
|
request = parsed.request;
|
|
315
414
|
ownerWorkspace = parsed.ownerWorkspace;
|
|
316
|
-
|
|
415
|
+
feishuResolvedId = parsed.feishuResolvedId ?? null;
|
|
416
|
+
this._maybeSyncFeishu(port, parsed.feishu);
|
|
417
|
+
this._maybeSyncAutoRetry(parsed.autoRetry);
|
|
317
418
|
}
|
|
318
419
|
else {
|
|
319
420
|
// 旧格式(兼容 npm 上的旧版本)
|
|
@@ -324,10 +425,10 @@ class FeedbackViewProvider {
|
|
|
324
425
|
const isMatch = isPathInWorkspace(request.projectDir);
|
|
325
426
|
if (!isMatch) {
|
|
326
427
|
// 请求不属于当前工作区,返回特殊标记
|
|
327
|
-
return { connected: true, request: null, port, mismatch: true, ownerWorkspace
|
|
428
|
+
return { connected: true, request: null, port, mismatch: true, ownerWorkspace };
|
|
328
429
|
}
|
|
329
430
|
}
|
|
330
|
-
return { connected: true, request, port, ownerWorkspace,
|
|
431
|
+
return { connected: true, request, port, ownerWorkspace, feishuResolvedId };
|
|
331
432
|
}
|
|
332
433
|
catch {
|
|
333
434
|
return { connected: false, request: null, port };
|
|
@@ -344,7 +445,6 @@ class FeedbackViewProvider {
|
|
|
344
445
|
// 判断是否为"新鲜"请求:创建后 10 秒内被发现
|
|
345
446
|
const requestAge = Date.now() - request.timestamp;
|
|
346
447
|
const isFreshRequest = requestAge < 10000; // 10秒内
|
|
347
|
-
console.log(`Feedback request on port ${port}:`, request.id, `age: ${requestAge}ms, isFresh: ${isFreshRequest}`);
|
|
348
448
|
// 标记为已见过
|
|
349
449
|
this._seenRequestIds.add(request.id);
|
|
350
450
|
// 清理旧的请求 ID(保留最近 100 个)
|
|
@@ -355,15 +455,90 @@ class FeedbackViewProvider {
|
|
|
355
455
|
if (!this._currentRequest || request.id !== this._currentRequest.id) {
|
|
356
456
|
this._currentRequest = request;
|
|
357
457
|
this._activePort = port;
|
|
358
|
-
//
|
|
458
|
+
// 「插件通知」主开关(配置 key 历史原因仍叫 systemNotification):关掉后本窗口完全静默——
|
|
459
|
+
// 不推送内容、不弹面板、不抢焦点、不发系统通知;连用户之后主动切回 / 打开面板也不显示
|
|
460
|
+
// (见 resolveWebviewView 里 ready 与 onDidChangeVisibility 的同款判断)。
|
|
461
|
+
// 请求仍记录在 _currentRequest,仅用于去重与外部渠道(飞书 / 超时)resolve 关联。
|
|
462
|
+
if (!this._isPluginNotifyEnabled()) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
// 开启:推送内容并显示面板
|
|
359
466
|
this._showFeedbackRequest(request);
|
|
360
|
-
//
|
|
467
|
+
// 只对新鲜请求做主动提醒(聚焦面板 + IDE 提示 + 失焦系统通知)
|
|
361
468
|
if (isFreshRequest) {
|
|
362
469
|
vscode.commands.executeCommand('cursorFeedback.feedbackView.focus');
|
|
363
|
-
|
|
470
|
+
this._sendSystemNotification(request);
|
|
364
471
|
}
|
|
365
472
|
}
|
|
366
473
|
}
|
|
474
|
+
/**
|
|
475
|
+
* 发送系统级通知(macOS / Windows / Linux)
|
|
476
|
+
*
|
|
477
|
+
* 场景:AI 运行较久时用户可能切去做别的事,IDE 内部的提示看不到,
|
|
478
|
+
* 等回来时反馈请求已超时。系统通知可以在 IDE 失焦时及时提醒用户回来。
|
|
479
|
+
*
|
|
480
|
+
* 仅在 IDE 窗口未聚焦时发送——窗口聚焦时 IDE 内部提示已足够,
|
|
481
|
+
* 避免每轮对话都弹系统通知造成打扰。
|
|
482
|
+
*/
|
|
483
|
+
_sendSystemNotification(request) {
|
|
484
|
+
const config = vscode.workspace.getConfiguration('cursorFeedback');
|
|
485
|
+
if (!config.get('systemNotification', true)) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
// 子开关「失焦时系统提示」:关掉后即使窗口失焦也不弹系统通知
|
|
489
|
+
if (!config.get('osNotification', true)) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (vscode.window.state.focused) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const withSound = config.get('notificationSound', true);
|
|
496
|
+
const projectName = path.basename(request.projectDir || '')
|
|
497
|
+
|| vscode.workspace.workspaceFolders?.[0]?.name
|
|
498
|
+
|| '';
|
|
499
|
+
const title = projectName ? `Cursor Feedback · ${projectName}` : 'Cursor Feedback';
|
|
500
|
+
const body = this._i18n.aiWaitingFeedback;
|
|
501
|
+
try {
|
|
502
|
+
if (process.platform === 'darwin') {
|
|
503
|
+
// AppleScript 字符串转义(execFile 不经过 shell,无注入风险)
|
|
504
|
+
const esc = (s) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
505
|
+
let script = `display notification "${esc(body)}" with title "${esc(title)}"`;
|
|
506
|
+
if (withSound) {
|
|
507
|
+
script += ' sound name "Glass"';
|
|
508
|
+
}
|
|
509
|
+
(0, child_process_1.execFile)('osascript', ['-e', script], () => { });
|
|
510
|
+
}
|
|
511
|
+
else if (process.platform === 'win32') {
|
|
512
|
+
// PowerShell 单引号字符串转义
|
|
513
|
+
const esc = (s) => s.replace(/'/g, "''");
|
|
514
|
+
// 使用 PowerShell 已注册的 AppId,未注册的 AppId 在 Win10/11 上会静默失败
|
|
515
|
+
const appId = '{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe';
|
|
516
|
+
const psScript = [
|
|
517
|
+
'$null = [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime];',
|
|
518
|
+
'$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02);',
|
|
519
|
+
"$texts = $template.GetElementsByTagName('text');",
|
|
520
|
+
`$null = $texts.Item(0).AppendChild($template.CreateTextNode('${esc(title)}'));`,
|
|
521
|
+
`$null = $texts.Item(1).AppendChild($template.CreateTextNode('${esc(body)}'));`,
|
|
522
|
+
...(withSound ? [] : [
|
|
523
|
+
"$audio = $template.CreateElement('audio');",
|
|
524
|
+
"$audio.SetAttribute('silent', 'true');",
|
|
525
|
+
'$null = $template.DocumentElement.AppendChild($audio);'
|
|
526
|
+
]),
|
|
527
|
+
'$toast = [Windows.UI.Notifications.ToastNotification]::new($template);',
|
|
528
|
+
`[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${esc(appId)}').Show($toast);`
|
|
529
|
+
].join(' ');
|
|
530
|
+
(0, child_process_1.execFile)('powershell.exe', ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', psScript], () => { });
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
// Linux:依赖 notify-send(GNOME/KDE 等主流桌面均自带),缺失时静默忽略
|
|
534
|
+
(0, child_process_1.execFile)('notify-send', ['-u', 'normal', title, body], () => { });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
// 系统通知失败不影响主流程
|
|
539
|
+
console.error('Failed to send system notification:', error);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
367
542
|
/**
|
|
368
543
|
* 检查服务器健康状态
|
|
369
544
|
*/
|
|
@@ -389,6 +564,15 @@ class FeedbackViewProvider {
|
|
|
389
564
|
payload: { connected: false }
|
|
390
565
|
});
|
|
391
566
|
}
|
|
567
|
+
/**
|
|
568
|
+
* 「插件通知」主开关是否开启(配置 key 历史原因仍叫 systemNotification)。
|
|
569
|
+
* 关闭即本窗口完全静默:不主动推送,被动切回 / ready 时也不显示缓存的请求。
|
|
570
|
+
*/
|
|
571
|
+
_isPluginNotifyEnabled() {
|
|
572
|
+
return vscode.workspace
|
|
573
|
+
.getConfiguration('cursorFeedback')
|
|
574
|
+
.get('systemNotification', true);
|
|
575
|
+
}
|
|
392
576
|
/**
|
|
393
577
|
* 显示反馈请求
|
|
394
578
|
*/
|
|
@@ -399,7 +583,7 @@ class FeedbackViewProvider {
|
|
|
399
583
|
type: 'showFeedbackRequest',
|
|
400
584
|
payload: {
|
|
401
585
|
requestId: request.id,
|
|
402
|
-
summary: request.summary,
|
|
586
|
+
summary: this._inlineLocalImages(request.summary),
|
|
403
587
|
projectDir: request.projectDir,
|
|
404
588
|
timeout: request.timeout,
|
|
405
589
|
timestamp: request.timestamp
|
|
@@ -417,6 +601,18 @@ class FeedbackViewProvider {
|
|
|
417
601
|
});
|
|
418
602
|
}
|
|
419
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
|
+
}
|
|
420
616
|
/**
|
|
421
617
|
* 更新调试信息到 WebView
|
|
422
618
|
*/
|
|
@@ -446,7 +642,6 @@ class FeedbackViewProvider {
|
|
|
446
642
|
}));
|
|
447
643
|
const result = JSON.parse(response);
|
|
448
644
|
if (result.success) {
|
|
449
|
-
vscode.window.showInformationMessage(this._i18n.feedbackSubmitted);
|
|
450
645
|
this._currentRequest = null;
|
|
451
646
|
this._showWaitingState();
|
|
452
647
|
}
|
|
@@ -476,6 +671,72 @@ class FeedbackViewProvider {
|
|
|
476
671
|
});
|
|
477
672
|
}
|
|
478
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
|
+
}
|
|
479
740
|
/**
|
|
480
741
|
* 处理语言切换
|
|
481
742
|
*/
|
|
@@ -503,12 +764,188 @@ class FeedbackViewProvider {
|
|
|
503
764
|
if (this._view) {
|
|
504
765
|
this._view.webview.html = this._getHtmlForWebview(this._view.webview);
|
|
505
766
|
}
|
|
506
|
-
const effectiveLang = (0, i18n_1.getLanguage)();
|
|
507
|
-
vscode.window.showInformationMessage(effectiveLang === 'en'
|
|
508
|
-
? 'Language changed to English'
|
|
509
|
-
: '语言已切换为简体中文');
|
|
510
767
|
}
|
|
511
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
|
+
}
|
|
512
949
|
/**
|
|
513
950
|
* HTTP GET 请求
|
|
514
951
|
*/
|
|
@@ -557,29 +994,85 @@ class FeedbackViewProvider {
|
|
|
557
994
|
req.end();
|
|
558
995
|
});
|
|
559
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
|
+
}
|
|
560
1037
|
_getHtmlForWebview(webview) {
|
|
561
1038
|
// 获取资源文件的 URI
|
|
562
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'));
|
|
563
1041
|
const stylesCssUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist', 'webview', 'styles.css'));
|
|
564
1042
|
const scriptJsUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist', 'webview', 'script.js'));
|
|
565
1043
|
// 读取 HTML 模板
|
|
566
1044
|
const htmlTemplatePath = path.join(this._extensionUri.fsPath, 'dist', 'webview', 'index.html');
|
|
567
1045
|
let htmlTemplate = fs.readFileSync(htmlTemplatePath, 'utf-8');
|
|
568
|
-
// CSP
|
|
569
|
-
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:;`;
|
|
570
1048
|
// 获取语言设置
|
|
571
1049
|
const language = (0, i18n_1.getLanguage)();
|
|
572
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
|
+
}
|
|
573
1065
|
// 替换占位符
|
|
574
1066
|
htmlTemplate = htmlTemplate
|
|
575
1067
|
.replace(/\{\{CSP\}\}/g, csp)
|
|
576
1068
|
.replace(/\{\{LANG\}\}/g, langCode)
|
|
577
1069
|
.replace(/\{\{MARKED_JS_URI\}\}/g, markedJsUri.toString())
|
|
1070
|
+
.replace(/\{\{HIGHLIGHT_JS_URI\}\}/g, highlightJsUri.toString())
|
|
578
1071
|
.replace(/\{\{STYLES_CSS_URI\}\}/g, stylesCssUri.toString())
|
|
579
1072
|
.replace(/\{\{SCRIPT_JS_URI\}\}/g, scriptJsUri.toString())
|
|
580
|
-
.replace(/\{\{I18N_JSON\}\}/g, JSON.stringify(
|
|
1073
|
+
.replace(/\{\{I18N_JSON\}\}/g, JSON.stringify(i18nResolved))
|
|
581
1074
|
.replace(/\{\{i18n\.(\w+)\}\}/g, (_, key) => {
|
|
582
|
-
return
|
|
1075
|
+
return i18nResolved[key] || key;
|
|
583
1076
|
});
|
|
584
1077
|
return htmlTemplate;
|
|
585
1078
|
}
|