cursor-feedback 2.2.0 → 2.3.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/dist/extension.js +228 -18
- package/dist/feishu.js +113 -0
- package/dist/i18n/en.json +23 -3
- package/dist/i18n/index.js +23 -1
- package/dist/i18n/zh-CN.json +23 -3
- package/dist/mcp-server.js +153 -15
- package/dist/webview/index.html +40 -12
- package/dist/webview/script.js +409 -28
- package/dist/webview/styles.css +215 -19
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [2.3.0](https://github.com/jianger666/cursor-feedback-extension/compare/v2.2.0...v2.3.0) (2026-07-04)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* 面板消息排队与撤回、扫码建应用、摘要历史与多项 UI 体验优化 ([ffe731c](https://github.com/jianger666/cursor-feedback-extension/commit/ffe731c767975f2045fc4b8701500b6a60f48373))
|
|
11
|
+
|
|
5
12
|
## [2.2.0](https://github.com/jianger666/cursor-feedback-extension/compare/v2.1.2...v2.2.0) (2026-07-03)
|
|
6
13
|
|
|
7
14
|
|
package/dist/extension.js
CHANGED
|
@@ -179,6 +179,9 @@ class FeedbackViewProvider {
|
|
|
179
179
|
this._feishuAck = true;
|
|
180
180
|
// 忙时消息排队子开关(飞书通知子项;AI 正忙时用户消息入队,下一轮 feedback 自动送达)
|
|
181
181
|
this._feishuQueue = true;
|
|
182
|
+
// 最近一次下发给 webview 的队列快照(签名去重避免每秒刷屏;webview 重建后凭此恢复列表)
|
|
183
|
+
this._lastQueueSig = '';
|
|
184
|
+
this._lastQueueItems = [];
|
|
182
185
|
this._debugInfo = {
|
|
183
186
|
portRange: '',
|
|
184
187
|
workspacePath: '',
|
|
@@ -186,6 +189,9 @@ class FeedbackViewProvider {
|
|
|
186
189
|
activePort: null,
|
|
187
190
|
lastStatus: ''
|
|
188
191
|
};
|
|
192
|
+
// ---------- 扫码一键创建飞书应用 ----------
|
|
193
|
+
this._registerPollTimer = null;
|
|
194
|
+
this._registerPort = null;
|
|
189
195
|
this._basePort = port;
|
|
190
196
|
this._debugInfo.portRange = `${port}-${port + this._portScanRange - 1}`;
|
|
191
197
|
this._i18n = (0, i18n_1.loadMessages)(this._extensionUri.fsPath);
|
|
@@ -228,11 +234,22 @@ class FeedbackViewProvider {
|
|
|
228
234
|
case 'submitFeedback':
|
|
229
235
|
await this._handleFeedbackSubmit(data.payload);
|
|
230
236
|
break;
|
|
237
|
+
case 'queueMessage':
|
|
238
|
+
await this._handleQueueMessage(data.payload);
|
|
239
|
+
break;
|
|
240
|
+
case 'removeQueued':
|
|
241
|
+
await this._handleRemoveQueued(data.payload);
|
|
242
|
+
break;
|
|
231
243
|
case 'ready':
|
|
232
244
|
this._webviewReady = true;
|
|
233
245
|
// 同步超时续期开关状态到 UI
|
|
234
246
|
this._postAutoRetryState();
|
|
235
247
|
this._postFeishuState();
|
|
248
|
+
// 重建后的 webview 恢复队列列表显示(poll 之后每秒还会校准)
|
|
249
|
+
this._view?.webview.postMessage({
|
|
250
|
+
type: 'queueState',
|
|
251
|
+
payload: { items: this._lastQueueItems }
|
|
252
|
+
});
|
|
236
253
|
// WebView 准备就绪后,检查是否有待处理的请求
|
|
237
254
|
// (插件通知关闭时保持静默,不显示缓存的请求)
|
|
238
255
|
if (this._currentRequest && this._isPluginNotifyEnabled()) {
|
|
@@ -265,9 +282,6 @@ class FeedbackViewProvider {
|
|
|
265
282
|
case 'saveFeishuConfig':
|
|
266
283
|
await this._handleSaveFeishuConfig(data.payload);
|
|
267
284
|
break;
|
|
268
|
-
case 'openFeishuGuide':
|
|
269
|
-
await this._handleOpenFeishuGuide();
|
|
270
|
-
break;
|
|
271
285
|
case 'toggleFeishuEnabled':
|
|
272
286
|
await this._handleToggleFeishuEnabled(!!data.payload?.enabled);
|
|
273
287
|
break;
|
|
@@ -283,6 +297,15 @@ class FeedbackViewProvider {
|
|
|
283
297
|
case 'toggleFeishuQueue':
|
|
284
298
|
await this._handleToggleFeishuQueue(!!data.payload?.enabled);
|
|
285
299
|
break;
|
|
300
|
+
case 'feishuRegisterStart':
|
|
301
|
+
await this._handleFeishuRegisterStart();
|
|
302
|
+
break;
|
|
303
|
+
case 'feishuRegisterCancel':
|
|
304
|
+
this._handleFeishuRegisterCancel();
|
|
305
|
+
break;
|
|
306
|
+
case 'openLink':
|
|
307
|
+
this._handleOpenLink(data.payload?.url);
|
|
308
|
+
break;
|
|
286
309
|
case 'testNotification':
|
|
287
310
|
this._sendTestNotification();
|
|
288
311
|
break;
|
|
@@ -358,6 +381,8 @@ class FeedbackViewProvider {
|
|
|
358
381
|
// 端口仍然有效,保持使用
|
|
359
382
|
this._debugInfo.connectedPorts = [this._activePort];
|
|
360
383
|
this._debugInfo.activePort = this._activePort;
|
|
384
|
+
// 同步忙时队列快照到面板(签名去重,无变化不发);打上端口标,撤回时按它路由
|
|
385
|
+
this._postQueueState((result.queued || []).map(q => ({ ...q, port: this._activePort })));
|
|
361
386
|
// 只挡「已提交/已结束」的请求;_handleNewRequest 内部按当前显示态去重,重复调用无害
|
|
362
387
|
if (result.request && !this._resolvedRequestIds.has(result.request.id)) {
|
|
363
388
|
this._debugInfo.lastStatus = this._t('statusListening', { port: this._activePort });
|
|
@@ -390,6 +415,12 @@ class FeedbackViewProvider {
|
|
|
390
415
|
const results = await Promise.all(ports.map(port => this._checkPortForRequest(port)));
|
|
391
416
|
// 更新已连接的端口列表
|
|
392
417
|
this._debugInfo.connectedPorts = results.filter(r => r.connected).map(r => r.port);
|
|
418
|
+
// 汇总各 server 返回的本工作区忙时队列(正常只有归属本窗口的 server 有内容),按时间排序下发;
|
|
419
|
+
// 打上各自 server 的端口标,撤回时按它路由
|
|
420
|
+
const queuedAll = results
|
|
421
|
+
.flatMap(r => (r.connected && Array.isArray(r.queued) ? r.queued.map(q => ({ ...q, port: r.port })) : []))
|
|
422
|
+
.sort((a, b) => a.at - b.at);
|
|
423
|
+
this._postQueueState(queuedAll);
|
|
393
424
|
// 找出属于当前工作区的请求(只排除已提交/已结束的;「见过但没提交」的仍要能回到面板)。
|
|
394
425
|
// 不再用 server 进程级 ownerWorkspace 二次过滤:request 已在 _checkPortForRequest
|
|
395
426
|
// 按本窗口工作区过滤过(isPathInWorkspace);多窗口共用一个 MCP 进程时 owner 只反映
|
|
@@ -457,11 +488,14 @@ class FeedbackViewProvider {
|
|
|
457
488
|
let request;
|
|
458
489
|
let ownerWorkspace = null;
|
|
459
490
|
let feishuResolvedId = null;
|
|
491
|
+
let queued = [];
|
|
460
492
|
if (parsed && typeof parsed === 'object' && 'startTime' in parsed) {
|
|
461
493
|
// 新格式
|
|
462
494
|
request = parsed.request;
|
|
463
495
|
ownerWorkspace = parsed.ownerWorkspace;
|
|
464
496
|
feishuResolvedId = parsed.feishuResolvedId ?? null;
|
|
497
|
+
if (Array.isArray(parsed.queued))
|
|
498
|
+
queued = parsed.queued;
|
|
465
499
|
this._maybeSyncFeishu(port, parsed.feishu);
|
|
466
500
|
this._maybeSyncAutoRetry(parsed.autoRetry);
|
|
467
501
|
this._maybeSyncPause(parsed.pause);
|
|
@@ -475,10 +509,10 @@ class FeedbackViewProvider {
|
|
|
475
509
|
const isMatch = isPathInWorkspace(request.projectDir);
|
|
476
510
|
if (!isMatch) {
|
|
477
511
|
// 请求不属于当前工作区,返回特殊标记
|
|
478
|
-
return { connected: true, request: null, port, mismatch: true, ownerWorkspace };
|
|
512
|
+
return { connected: true, request: null, port, mismatch: true, ownerWorkspace, queued };
|
|
479
513
|
}
|
|
480
514
|
}
|
|
481
|
-
return { connected: true, request, port, ownerWorkspace, feishuResolvedId };
|
|
515
|
+
return { connected: true, request, port, ownerWorkspace, feishuResolvedId, queued };
|
|
482
516
|
}
|
|
483
517
|
catch {
|
|
484
518
|
return { connected: false, request: null, port };
|
|
@@ -766,6 +800,103 @@ class FeedbackViewProvider {
|
|
|
766
800
|
vscode.window.showErrorMessage(this._i18n.submitFailed + ': ' + this._i18n.cannotConnectMCP);
|
|
767
801
|
}
|
|
768
802
|
}
|
|
803
|
+
/**
|
|
804
|
+
* 把忙时队列快照推给 webview(签名去重:队列无变化时每秒轮询不重复发)
|
|
805
|
+
*/
|
|
806
|
+
_postQueueState(items) {
|
|
807
|
+
const sig = JSON.stringify(items);
|
|
808
|
+
if (sig === this._lastQueueSig)
|
|
809
|
+
return;
|
|
810
|
+
this._lastQueueSig = sig;
|
|
811
|
+
this._lastQueueItems = items;
|
|
812
|
+
this._view?.webview.postMessage({ type: 'queueState', payload: { items } });
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* 面板忙时排队:AI 正忙(没有等待中的反馈请求)时用户在面板点了「排队发送」。
|
|
816
|
+
* 先定位归属本工作区的 server(AI 的下一轮 interactive_feedback 会走同一个 MCP 进程,
|
|
817
|
+
* 队列必须排在那里才能被消费),再 POST 入队。找不到归属 server = AI 没在本项目工作,
|
|
818
|
+
* 明确告知失败,绝不静默吞消息。
|
|
819
|
+
*/
|
|
820
|
+
async _handleQueueMessage(payload) {
|
|
821
|
+
const workspacePaths = getWorkspacePaths();
|
|
822
|
+
const workspacePath = workspacePaths.length > 0 ? workspacePaths[0] : '';
|
|
823
|
+
const fail = (reason) => {
|
|
824
|
+
this._view?.webview.postMessage({ type: 'queueSubmitted', payload: { success: false, reason } });
|
|
825
|
+
};
|
|
826
|
+
if (!workspacePath) {
|
|
827
|
+
fail('no-server');
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const port = await this._findOwnerPort(workspacePath);
|
|
831
|
+
if (!port) {
|
|
832
|
+
fail('no-server');
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
const response = await this._httpPost(`http://127.0.0.1:${port}/api/feedback/enqueue`, JSON.stringify({
|
|
837
|
+
text: payload.interactive_feedback,
|
|
838
|
+
images: payload.images || [],
|
|
839
|
+
attachedFiles: payload.attachedFiles || [],
|
|
840
|
+
projectDir: workspacePath
|
|
841
|
+
}));
|
|
842
|
+
const result = JSON.parse(response);
|
|
843
|
+
if (result.queued) {
|
|
844
|
+
this._view?.webview.postMessage({ type: 'queueSubmitted', payload: { success: true } });
|
|
845
|
+
}
|
|
846
|
+
else {
|
|
847
|
+
fail(result.reason === 'pending' || result.reason === 'disabled' ? result.reason : 'no-server');
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
catch {
|
|
851
|
+
fail('no-server');
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* 撤回一条排队消息:按队列项携带的端口路由到所在 server。
|
|
856
|
+
* webview 已做乐观移除;失败也无需提示——下一秒轮询会把仍在队列里的消息补回列表。
|
|
857
|
+
*/
|
|
858
|
+
async _handleRemoveQueued(payload) {
|
|
859
|
+
if (!payload || !payload.id || typeof payload.port !== 'number')
|
|
860
|
+
return;
|
|
861
|
+
try {
|
|
862
|
+
await this._httpPost(`http://127.0.0.1:${payload.port}/api/feedback/queue/remove`, JSON.stringify({ id: payload.id }));
|
|
863
|
+
}
|
|
864
|
+
catch {
|
|
865
|
+
// 静默:轮询会校准列表
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* 定位归属指定工作区的 MCP server 端口(ownerWorkspace 与工作区路径互为前缀)。
|
|
870
|
+
* 优先试当前已知端口,避免全端口扫描;都不中再并行扫描全范围。
|
|
871
|
+
*/
|
|
872
|
+
async _findOwnerPort(workspacePath) {
|
|
873
|
+
const normalizedWs = normalizePath(workspacePath);
|
|
874
|
+
const isOwner = async (port) => {
|
|
875
|
+
try {
|
|
876
|
+
const response = await this._httpGet(`http://127.0.0.1:${port}/api/feedback/current?workspace=${encodeURIComponent(workspacePath)}`);
|
|
877
|
+
const parsed = JSON.parse(response);
|
|
878
|
+
const owner = parsed && parsed.ownerWorkspace ? normalizePath(parsed.ownerWorkspace) : '';
|
|
879
|
+
return !!owner && pathsRelated(owner, normalizedWs);
|
|
880
|
+
}
|
|
881
|
+
catch {
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
const preferred = [this._currentRequestPort, this._activePort]
|
|
886
|
+
.filter((p) => typeof p === 'number');
|
|
887
|
+
for (const p of preferred) {
|
|
888
|
+
if (await isOwner(p))
|
|
889
|
+
return p;
|
|
890
|
+
}
|
|
891
|
+
const ports = [];
|
|
892
|
+
for (let i = 0; i < this._portScanRange; i++) {
|
|
893
|
+
const p = this._basePort + i;
|
|
894
|
+
if (!preferred.includes(p))
|
|
895
|
+
ports.push(p);
|
|
896
|
+
}
|
|
897
|
+
const hits = await Promise.all(ports.map(async (p) => ((await isOwner(p)) ? p : null)));
|
|
898
|
+
return hits.find((p) => p !== null) ?? null;
|
|
899
|
+
}
|
|
769
900
|
/**
|
|
770
901
|
* 处理选择文件/文件夹
|
|
771
902
|
*/
|
|
@@ -1059,24 +1190,103 @@ class FeedbackViewProvider {
|
|
|
1059
1190
|
this._httpPost(`http://127.0.0.1:${port}/api/feishu/config`, body).catch(() => { });
|
|
1060
1191
|
}
|
|
1061
1192
|
}
|
|
1193
|
+
_postRegisterState(payload) {
|
|
1194
|
+
this._view?.webview.postMessage({ type: 'feishuRegisterState', payload });
|
|
1195
|
+
}
|
|
1062
1196
|
/**
|
|
1063
|
-
*
|
|
1197
|
+
* 发起扫码创建:找一个活跃的 MCP server(凭证写磁盘全局共享,任一进程均可),
|
|
1198
|
+
* 拿到验证链接后本地生成二维码 dataURL 推给面板,随后轮询直到成功/失败。
|
|
1064
1199
|
*/
|
|
1065
|
-
async
|
|
1066
|
-
|
|
1067
|
-
const
|
|
1200
|
+
async _handleFeishuRegisterStart() {
|
|
1201
|
+
this._stopRegisterPolling();
|
|
1202
|
+
const port = await this._findAnyServerPort();
|
|
1203
|
+
if (!port) {
|
|
1204
|
+
this._postRegisterState({ status: 'error', error: this._i18n.registerNoServer });
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
this._registerPort = port;
|
|
1068
1208
|
try {
|
|
1069
|
-
//
|
|
1070
|
-
//
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1209
|
+
// register/start 要等二维码就绪(服务端最多 10s)才响应,超时给足余量。
|
|
1210
|
+
// 二维码 dataURL 由 server 生成(插件 VSIX 不带 node_modules,装不进二维码库)
|
|
1211
|
+
const raw = await this._httpPost(`http://127.0.0.1:${port}/api/feishu/register/start`, '{}', 15000);
|
|
1212
|
+
const state = JSON.parse(raw);
|
|
1213
|
+
if (state.status === 'waiting' && state.url) {
|
|
1214
|
+
this._postRegisterState({ status: 'waiting', url: state.url, expireIn: state.expireIn, qr: state.qr });
|
|
1215
|
+
this._startRegisterPolling(port);
|
|
1216
|
+
}
|
|
1217
|
+
else {
|
|
1218
|
+
this._postRegisterState({ status: 'error', error: state.error || 'failed' });
|
|
1219
|
+
}
|
|
1074
1220
|
}
|
|
1075
1221
|
catch {
|
|
1076
|
-
|
|
1077
|
-
await vscode.env.openExternal(vscode.Uri.parse(onlineUrl));
|
|
1222
|
+
this._postRegisterState({ status: 'error', error: this._i18n.registerNoServer });
|
|
1078
1223
|
}
|
|
1079
1224
|
}
|
|
1225
|
+
_handleFeishuRegisterCancel() {
|
|
1226
|
+
this._stopRegisterPolling();
|
|
1227
|
+
if (this._registerPort) {
|
|
1228
|
+
this._httpPost(`http://127.0.0.1:${this._registerPort}/api/feishu/register/cancel`, '{}').catch(() => { });
|
|
1229
|
+
this._registerPort = null;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
_startRegisterPolling(port) {
|
|
1233
|
+
this._stopRegisterPolling();
|
|
1234
|
+
this._registerPollTimer = setInterval(async () => {
|
|
1235
|
+
try {
|
|
1236
|
+
const raw = await this._httpGet(`http://127.0.0.1:${port}/api/feishu/register/status`);
|
|
1237
|
+
const st = JSON.parse(raw);
|
|
1238
|
+
if (st.status === 'success') {
|
|
1239
|
+
this._stopRegisterPolling();
|
|
1240
|
+
this._postRegisterState({ status: 'success', appId: st.appId });
|
|
1241
|
+
}
|
|
1242
|
+
else if (st.status === 'error' || st.status === 'idle') {
|
|
1243
|
+
this._stopRegisterPolling();
|
|
1244
|
+
this._postRegisterState({ status: 'error', error: st.error || 'failed' });
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
catch {
|
|
1248
|
+
// server 暂时不可达:下一轮再试
|
|
1249
|
+
}
|
|
1250
|
+
}, 2000);
|
|
1251
|
+
}
|
|
1252
|
+
_stopRegisterPolling() {
|
|
1253
|
+
if (this._registerPollTimer) {
|
|
1254
|
+
clearInterval(this._registerPollTimer);
|
|
1255
|
+
this._registerPollTimer = null;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
/** 找任意一个可达的 MCP server 端口(优先当前活跃端口,避免全端口扫描) */
|
|
1259
|
+
async _findAnyServerPort() {
|
|
1260
|
+
const alive = async (port) => {
|
|
1261
|
+
try {
|
|
1262
|
+
const raw = await this._httpGet(`http://127.0.0.1:${port}/api/health`);
|
|
1263
|
+
return JSON.parse(raw).status === 'ok';
|
|
1264
|
+
}
|
|
1265
|
+
catch {
|
|
1266
|
+
return false;
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
const preferred = [this._activePort, this._currentRequestPort, ...(this._debugInfo.connectedPorts || [])]
|
|
1270
|
+
.filter((p) => typeof p === 'number');
|
|
1271
|
+
for (const p of preferred) {
|
|
1272
|
+
if (await alive(p))
|
|
1273
|
+
return p;
|
|
1274
|
+
}
|
|
1275
|
+
const ports = [];
|
|
1276
|
+
for (let i = 0; i < this._portScanRange; i++) {
|
|
1277
|
+
const p = this._basePort + i;
|
|
1278
|
+
if (!preferred.includes(p))
|
|
1279
|
+
ports.push(p);
|
|
1280
|
+
}
|
|
1281
|
+
const hits = await Promise.all(ports.map(async (p) => ((await alive(p)) ? p : null)));
|
|
1282
|
+
return hits.find((p) => p !== null) ?? null;
|
|
1283
|
+
}
|
|
1284
|
+
/** 打开外部链接(webview 内点「打开链接」时用;仅放行 http/https) */
|
|
1285
|
+
_handleOpenLink(url) {
|
|
1286
|
+
if (!url || !/^https?:\/\//i.test(url))
|
|
1287
|
+
return;
|
|
1288
|
+
vscode.env.openExternal(vscode.Uri.parse(url));
|
|
1289
|
+
}
|
|
1080
1290
|
/**
|
|
1081
1291
|
* 切换飞书通知开关(关了即使已配置也不推飞书;长连接仍保留以便绑定/回复)
|
|
1082
1292
|
*/
|
|
@@ -1142,7 +1352,7 @@ class FeedbackViewProvider {
|
|
|
1142
1352
|
/**
|
|
1143
1353
|
* HTTP POST 请求
|
|
1144
1354
|
*/
|
|
1145
|
-
_httpPost(url, body) {
|
|
1355
|
+
_httpPost(url, body, timeoutMs = 5000) {
|
|
1146
1356
|
return new Promise((resolve, reject) => {
|
|
1147
1357
|
const urlObj = new URL(url);
|
|
1148
1358
|
const options = {
|
|
@@ -1154,7 +1364,7 @@ class FeedbackViewProvider {
|
|
|
1154
1364
|
'Content-Type': 'application/json',
|
|
1155
1365
|
'Content-Length': Buffer.byteLength(body)
|
|
1156
1366
|
},
|
|
1157
|
-
timeout:
|
|
1367
|
+
timeout: timeoutMs
|
|
1158
1368
|
};
|
|
1159
1369
|
const req = http.request(options, (res) => {
|
|
1160
1370
|
let data = '';
|
package/dist/feishu.js
CHANGED
|
@@ -82,6 +82,9 @@ class FeishuBridge {
|
|
|
82
82
|
this.onReply = null;
|
|
83
83
|
/** 绑定的 chat_id 变化时的回调(由 mcp-server 注入,用于通知 extension 持久化) */
|
|
84
84
|
this.onBindChange = null;
|
|
85
|
+
// ---------- 扫码一键创建应用(registerApp / Device Grant) ----------
|
|
86
|
+
this.registerState = { status: 'idle' };
|
|
87
|
+
this.registerAbort = null;
|
|
85
88
|
/** 按 chatId 暂存待合并的入站消息 */
|
|
86
89
|
this.inboundBuffer = new Map();
|
|
87
90
|
}
|
|
@@ -109,6 +112,116 @@ class FeishuBridge {
|
|
|
109
112
|
isConfigured() {
|
|
110
113
|
return !!this.config;
|
|
111
114
|
}
|
|
115
|
+
getRegisterState() {
|
|
116
|
+
return this.registerState;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 启动「扫码一键创建应用」:返回待扫码的验证链接(等 onQRCodeReady 就绪后才返回)。
|
|
120
|
+
* 用户在飞书里确认后 SDK resolve 出凭证 → 与手动在面板保存等价(写磁盘 touched + configure 立即生效)。
|
|
121
|
+
* 已有等待中的流程直接复用当前二维码,不重复发起。
|
|
122
|
+
*/
|
|
123
|
+
async startRegister() {
|
|
124
|
+
if (this.registerState.status === 'waiting' && this.registerAbort) {
|
|
125
|
+
return this.registerState;
|
|
126
|
+
}
|
|
127
|
+
if (!this.lark) {
|
|
128
|
+
try {
|
|
129
|
+
this.lark = await Promise.resolve().then(() => __importStar(require('@larksuiteoapi/node-sdk')));
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
flog('飞书 SDK 加载失败: ' + e);
|
|
133
|
+
this.registerState = { status: 'error', error: 'sdk_load_failed' };
|
|
134
|
+
return this.registerState;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (typeof this.lark.registerApp !== 'function') {
|
|
138
|
+
this.registerState = { status: 'error', error: 'sdk_too_old' };
|
|
139
|
+
return this.registerState;
|
|
140
|
+
}
|
|
141
|
+
const abort = new AbortController();
|
|
142
|
+
this.registerAbort = abort;
|
|
143
|
+
const state = { status: 'waiting' };
|
|
144
|
+
this.registerState = state;
|
|
145
|
+
let urlResolve = () => { };
|
|
146
|
+
const urlReady = new Promise((resolve) => {
|
|
147
|
+
urlResolve = resolve;
|
|
148
|
+
setTimeout(resolve, 10000); // 兜底:10s 拿不到二维码就按错误返回
|
|
149
|
+
});
|
|
150
|
+
this.lark
|
|
151
|
+
.registerApp({
|
|
152
|
+
signal: abort.signal,
|
|
153
|
+
onQRCodeReady: (info) => {
|
|
154
|
+
state.url = info.url;
|
|
155
|
+
state.expireIn = info.expireIn;
|
|
156
|
+
urlResolve();
|
|
157
|
+
},
|
|
158
|
+
appPreset: {
|
|
159
|
+
name: 'Cursor Feedback',
|
|
160
|
+
desc: 'Cursor AI 交互反馈通知机器人(由 cursor-feedback 插件扫码创建)',
|
|
161
|
+
},
|
|
162
|
+
// 平台基础模板已含收发消息 / reaction / 长连接事件;这里增量补齐本插件用到的资源权限。
|
|
163
|
+
// 平台不认识的名字会在确认页被静默丢弃,不会导致流程失败。
|
|
164
|
+
addons: {
|
|
165
|
+
scopes: { tenant: ['im:message', 'im:message:send_as_bot', 'im:resource'] },
|
|
166
|
+
events: { items: { tenant: ['im.message.receive_v1'] } },
|
|
167
|
+
},
|
|
168
|
+
// 只允许创建新应用:避免用户误选已有应用、其配置被覆盖
|
|
169
|
+
createOnly: true,
|
|
170
|
+
})
|
|
171
|
+
.then(async (result) => {
|
|
172
|
+
if (this.registerState !== state)
|
|
173
|
+
return; // 已被取消/新流程覆盖
|
|
174
|
+
const cfg = {
|
|
175
|
+
appId: result.client_id,
|
|
176
|
+
appSecret: result.client_secret,
|
|
177
|
+
enabled: this.enabled,
|
|
178
|
+
ackReaction: this.ackReaction,
|
|
179
|
+
queueWhenBusy: this.queueWhenBusy,
|
|
180
|
+
};
|
|
181
|
+
this.writePersistedConfig({ ...cfg, touched: true });
|
|
182
|
+
await this.configure(cfg);
|
|
183
|
+
state.status = 'success';
|
|
184
|
+
state.appId = result.client_id;
|
|
185
|
+
state.url = undefined;
|
|
186
|
+
flog(`扫码创建应用成功: ${result.client_id}`);
|
|
187
|
+
})
|
|
188
|
+
.catch((e) => {
|
|
189
|
+
if (this.registerState !== state)
|
|
190
|
+
return;
|
|
191
|
+
state.status = 'error';
|
|
192
|
+
state.url = undefined;
|
|
193
|
+
state.error = [e?.code, e?.description].filter(Boolean).join(': ') || String(e);
|
|
194
|
+
flog(`扫码创建应用失败: ${state.error}`);
|
|
195
|
+
});
|
|
196
|
+
await urlReady;
|
|
197
|
+
if (state.status === 'waiting' && !state.url) {
|
|
198
|
+
abort.abort();
|
|
199
|
+
this.registerAbort = null;
|
|
200
|
+
this.registerState = { status: 'error', error: 'qr_timeout' };
|
|
201
|
+
return this.registerState;
|
|
202
|
+
}
|
|
203
|
+
// 二维码在 server 侧生成(插件 VSIX 不带 node_modules);失败则面板退化为只显示链接按钮
|
|
204
|
+
if (state.status === 'waiting' && state.url) {
|
|
205
|
+
try {
|
|
206
|
+
const QRCode = await Promise.resolve().then(() => __importStar(require('qrcode')));
|
|
207
|
+
state.qr = await QRCode.toDataURL(state.url, { margin: 1, width: 220 });
|
|
208
|
+
}
|
|
209
|
+
catch (e) {
|
|
210
|
+
flog('二维码生成失败(退化为链接): ' + e);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return this.registerState;
|
|
214
|
+
}
|
|
215
|
+
/** 取消进行中的扫码创建流程(用户关闭弹窗 / 主动取消) */
|
|
216
|
+
cancelRegister() {
|
|
217
|
+
if (this.registerAbort) {
|
|
218
|
+
this.registerAbort.abort();
|
|
219
|
+
this.registerAbort = null;
|
|
220
|
+
}
|
|
221
|
+
if (this.registerState.status === 'waiting') {
|
|
222
|
+
this.registerState = { status: 'idle' };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
112
225
|
/** 读取当前 appId 对应的绑定 chat_id(来自磁盘,跨进程共享) */
|
|
113
226
|
getBoundChatId() {
|
|
114
227
|
if (!this.config?.appId)
|
package/dist/i18n/en.json
CHANGED
|
@@ -58,7 +58,6 @@
|
|
|
58
58
|
"feishuAppSecret": "App Secret",
|
|
59
59
|
"feishuAppIdPlaceholder": "cli_xxxxxxxx",
|
|
60
60
|
"feishuSecretPlaceholder": "Enter App Secret",
|
|
61
|
-
"feishuGuide": "How to set up a Feishu bot?",
|
|
62
61
|
"feishuClose": "Close",
|
|
63
62
|
"feishuStatusUnconfigured": "Not configured",
|
|
64
63
|
"feishuStatusConfigured": "Configured · send your bot a message to bind",
|
|
@@ -73,8 +72,29 @@
|
|
|
73
72
|
"osNotifyDesc": "Sends a system notification when the IDE is in the background.",
|
|
74
73
|
"feishuAckLabel": "Get emoji acknowledgement",
|
|
75
74
|
"feishuAckDesc": "After you reply in Feishu, the bot adds a Get emoji as acknowledgement.",
|
|
76
|
-
"
|
|
77
|
-
"
|
|
75
|
+
"queueWhenBusyLabel": "Queue messages while AI is busy",
|
|
76
|
+
"queueWhenBusyDesc": "While the AI is busy, messages from the panel and Feishu are queued and auto-delivered in order on its next feedback round, marked as appended during the task.",
|
|
77
|
+
"queueSend": "Queue Message",
|
|
78
|
+
"queuedTitle": "Queued messages",
|
|
79
|
+
"queueModeHint": "AI is busy — messages sent now are queued and delivered when it next asks for feedback",
|
|
80
|
+
"toastPanelQueued": "✓ Queued — the AI will receive it after finishing this task",
|
|
81
|
+
"queueFailedPending": "AI is waiting for your feedback — submit it directly instead",
|
|
82
|
+
"queueFailedDisabled": "Busy-time queuing is turned off in settings — the message was not queued",
|
|
83
|
+
"queueFailedNoServer": "No active AI session found — the message was not queued",
|
|
84
|
+
"sourceFeishu": "Feishu",
|
|
85
|
+
"sourcePanel": "Panel",
|
|
86
|
+
"queueMetaImages": "img",
|
|
87
|
+
"queueMetaFiles": "file",
|
|
88
|
+
"summaryPrevTip": "Older summary",
|
|
89
|
+
"summaryNextTip": "Newer summary",
|
|
90
|
+
"feishuRegisterBtn": "Create app by QR code",
|
|
91
|
+
"registerLoading": "Getting QR code…",
|
|
92
|
+
"registerScanHint": "Scan with Feishu to confirm — permissions are preset and credentials fill in automatically",
|
|
93
|
+
"registerOpenLink": "Don't want to scan? Create via link",
|
|
94
|
+
"registerRetry": "Retry",
|
|
95
|
+
"registerSuccess": "Created! Credentials configured automatically",
|
|
96
|
+
"registerFailed": "Creation failed",
|
|
97
|
+
"registerNoServer": "MCP Server is not running — cannot start creation",
|
|
78
98
|
"quickReplyDefault1": "Keep waiting for my feedback",
|
|
79
99
|
"quickReplyDefault2": "End the task",
|
|
80
100
|
"quickReplyEdit": "Edit quick replies",
|
package/dist/i18n/index.js
CHANGED
|
@@ -149,7 +149,6 @@ function getDefaultMessages() {
|
|
|
149
149
|
feishuAppSecret: "App Secret",
|
|
150
150
|
feishuAppIdPlaceholder: "cli_xxxxxxxx",
|
|
151
151
|
feishuSecretPlaceholder: "Enter App Secret",
|
|
152
|
-
feishuGuide: "How to set up a Feishu bot?",
|
|
153
152
|
feishuClose: "Close",
|
|
154
153
|
feishuStatusUnconfigured: "Not configured",
|
|
155
154
|
feishuStatusConfigured: "Configured · send your bot a message to bind",
|
|
@@ -164,6 +163,29 @@ function getDefaultMessages() {
|
|
|
164
163
|
osNotifyDesc: "Sends a system notification when the IDE is in the background.",
|
|
165
164
|
feishuAckLabel: "Get emoji acknowledgement",
|
|
166
165
|
feishuAckDesc: "After you reply in Feishu, the bot adds a Get emoji as acknowledgement.",
|
|
166
|
+
queueWhenBusyLabel: "Queue messages while AI is busy",
|
|
167
|
+
queueWhenBusyDesc: "While the AI is busy, messages from the panel and Feishu are queued and auto-delivered in order on its next feedback round, marked as appended during the task.",
|
|
168
|
+
queueSend: "Queue Message",
|
|
169
|
+
queuedTitle: "Queued messages",
|
|
170
|
+
queueModeHint: "AI is busy — messages sent now are queued and delivered when it next asks for feedback",
|
|
171
|
+
toastPanelQueued: "✓ Queued — the AI will receive it after finishing this task",
|
|
172
|
+
queueFailedPending: "AI is waiting for your feedback — submit it directly instead",
|
|
173
|
+
queueFailedDisabled: "Busy-time queuing is turned off in settings — the message was not queued",
|
|
174
|
+
queueFailedNoServer: "No active AI session found — the message was not queued",
|
|
175
|
+
sourceFeishu: "Feishu",
|
|
176
|
+
sourcePanel: "Panel",
|
|
177
|
+
queueMetaImages: "img",
|
|
178
|
+
queueMetaFiles: "file",
|
|
179
|
+
summaryPrevTip: "Older summary",
|
|
180
|
+
summaryNextTip: "Newer summary",
|
|
181
|
+
feishuRegisterBtn: "Create app by QR code",
|
|
182
|
+
registerLoading: "Getting QR code…",
|
|
183
|
+
registerScanHint: "Scan with Feishu to confirm — permissions are preset and credentials fill in automatically",
|
|
184
|
+
registerOpenLink: "Don't want to scan? Create via link",
|
|
185
|
+
registerRetry: "Retry",
|
|
186
|
+
registerSuccess: "Created! Credentials configured automatically",
|
|
187
|
+
registerFailed: "Creation failed",
|
|
188
|
+
registerNoServer: "MCP Server is not running — cannot start creation",
|
|
167
189
|
quickReplyDefault1: "Keep waiting for my feedback",
|
|
168
190
|
quickReplyDefault2: "End the task",
|
|
169
191
|
quickReplyEdit: "Edit quick replies",
|
package/dist/i18n/zh-CN.json
CHANGED
|
@@ -58,7 +58,6 @@
|
|
|
58
58
|
"feishuAppSecret": "App Secret",
|
|
59
59
|
"feishuAppIdPlaceholder": "cli_xxxxxxxx",
|
|
60
60
|
"feishuSecretPlaceholder": "请输入 App Secret",
|
|
61
|
-
"feishuGuide": "如何配置飞书机器人?",
|
|
62
61
|
"feishuClose": "关闭",
|
|
63
62
|
"feishuStatusUnconfigured": "未配置",
|
|
64
63
|
"feishuStatusConfigured": "已配置 · 请给机器人发条消息以绑定接收方",
|
|
@@ -73,8 +72,29 @@
|
|
|
73
72
|
"osNotifyDesc": "IDE 切到后台时,用系统通知提醒你。",
|
|
74
73
|
"feishuAckLabel": "Get 表情回执",
|
|
75
74
|
"feishuAckDesc": "你在飞书回复后,机器人加个 Get 表情表示「已收到」。",
|
|
76
|
-
"
|
|
77
|
-
"
|
|
75
|
+
"queueWhenBusyLabel": "忙时消息排队",
|
|
76
|
+
"queueWhenBusyDesc": "AI 正忙时,面板与飞书发送的消息先排队,等它下一轮询问时按序自动送达,并附「任务期间追加」提示。",
|
|
77
|
+
"queueSend": "排队发送",
|
|
78
|
+
"queuedTitle": "排队中的消息",
|
|
79
|
+
"queueModeHint": "AI 正忙,现在发送的消息会排队,等它下一轮询问时自动送达",
|
|
80
|
+
"toastPanelQueued": "✓ 已排队,AI 完成当前任务后自动收到",
|
|
81
|
+
"queueFailedPending": "AI 正在等待你的反馈,请直接提交",
|
|
82
|
+
"queueFailedDisabled": "忙时排队已在设置中关闭,消息未排队",
|
|
83
|
+
"queueFailedNoServer": "没有找到正在工作的 AI 会话,消息未排队",
|
|
84
|
+
"sourceFeishu": "飞书",
|
|
85
|
+
"sourcePanel": "面板",
|
|
86
|
+
"queueMetaImages": "图片",
|
|
87
|
+
"queueMetaFiles": "文件",
|
|
88
|
+
"summaryPrevTip": "更早的摘要",
|
|
89
|
+
"summaryNextTip": "更新的摘要",
|
|
90
|
+
"feishuRegisterBtn": "扫码快速创建应用",
|
|
91
|
+
"registerLoading": "正在获取二维码…",
|
|
92
|
+
"registerScanHint": "用飞书「扫一扫」确认创建,权限已预置,凭证会自动填好",
|
|
93
|
+
"registerOpenLink": "不想扫码?点击链接创建",
|
|
94
|
+
"registerRetry": "重试",
|
|
95
|
+
"registerSuccess": "创建成功!凭证已自动配置",
|
|
96
|
+
"registerFailed": "创建失败",
|
|
97
|
+
"registerNoServer": "MCP Server 未运行,无法发起创建",
|
|
78
98
|
"quickReplyDefault1": "继续等待我的反馈",
|
|
79
99
|
"quickReplyDefault2": "结束任务",
|
|
80
100
|
"quickReplyEdit": "编辑快捷短语",
|