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.
@@ -72,6 +72,16 @@ function debugLog(message) {
72
72
  const timestamp = new Date().toISOString();
73
73
  console.error(`[${timestamp}] ${message}`);
74
74
  }
75
+ // 包版本真相源:package.json(dist/mcp-server.js 的上一级目录)。读不到时兜底 0.0.0。
76
+ const PKG_VERSION = (() => {
77
+ try {
78
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
79
+ return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
80
+ }
81
+ catch {
82
+ return '0.0.0';
83
+ }
84
+ })();
75
85
  /**
76
86
  * MCP Feedback Server
77
87
  */
@@ -95,11 +105,14 @@ class McpFeedbackServer {
95
105
  /** 面板在超时空窗内提交的反馈:暂存到下一轮 pending 注册时立即兑现 */
96
106
  this.panelStash = null;
97
107
  /**
98
- * 忙时消息队列:AI 正在干活(该项目空间没有等待中的反馈请求)时用户发来的飞书消息
108
+ * 忙时消息队列:AI 正在干活(该项目空间没有等待中的反馈请求)时用户发来的消息
99
109
  * 不再丢弃,而是按项目空间排队;等 AI 下一轮调 interactive_feedback 时合并送达,
100
- * 并附「任务期间追加」提示头。开关见 FeishuBridge.queueWhenBusy(面板 / FEISHU_QUEUE)。
110
+ * 并附「任务期间追加」提示头。飞书与面板消息共用同一个队列(按到达顺序),
111
+ * 飞书侧开关见 FeishuBridge.queueWhenBusy(面板 / FEISHU_QUEUE);面板排队恒可用。
101
112
  */
102
113
  this.queuedInbound = [];
114
+ /** 队列项 id 自增序号(配合时间戳保证进程内唯一) */
115
+ this.queueSeq = 0;
103
116
  /** 暂存过期提示定时器:到点仍未被认领则「回复」那条消息告知没送到,避免静默丢弃 */
104
117
  this.stashExpiryTimer = null;
105
118
  // 最近一次被飞书回复 resolve 的请求:供插件端精确区分「飞书回复」与「超时」,
@@ -139,7 +152,7 @@ class McpFeedbackServer {
139
152
  this.basePort = port;
140
153
  this.server = new index_js_1.Server({
141
154
  name: 'cursor-feedback-server',
142
- version: '0.0.1',
155
+ version: PKG_VERSION,
143
156
  }, {
144
157
  capabilities: {
145
158
  tools: {},
@@ -933,13 +946,43 @@ class McpFeedbackServer {
933
946
  }
934
947
  return false;
935
948
  }
936
- /** 队列里是否有属于某项目空间的排队消息 */
949
+ /** 队列里是否有属于某项目空间的排队消息(不看渠道开关:面板消息不受飞书队列开关约束) */
937
950
  hasQueuedFor(projectDir) {
938
- if (!this.feishu.isQueueWhenBusy())
939
- return false;
940
951
  const owner = this.normalizePath(projectDir);
941
952
  return this.queuedInbound.some((q) => this.pathsRelated(this.normalizePath(q.forProjectDir), owner));
942
953
  }
954
+ /** 某项目空间的排队消息快照(随 /api/feedback/current 下发给面板展示队列列表) */
955
+ queuedSnapshotFor(normalizedWs) {
956
+ if (!normalizedWs)
957
+ return [];
958
+ return this.queuedInbound
959
+ .filter((q) => this.pathsRelated(this.normalizePath(q.forProjectDir), normalizedWs))
960
+ .map((q) => ({
961
+ id: q.id,
962
+ at: q.at,
963
+ source: q.source,
964
+ text: q.text,
965
+ images: q.images.length,
966
+ files: q.files.length,
967
+ }));
968
+ }
969
+ /**
970
+ * 撤回一条排队消息(面板队列列表的小叉触发)。
971
+ * 飞书来源的消息同步回执「已撤回」,两边状态一致;面板消息撤掉后列表随轮询消失即回执。
972
+ * 返回 false = 没找到(可能已被消费或过期),面板下一秒轮询自然对齐,无需特殊处理。
973
+ */
974
+ removeQueuedById(id) {
975
+ const idx = this.queuedInbound.findIndex((q) => q.id === id);
976
+ if (idx < 0)
977
+ return false;
978
+ const [item] = this.queuedInbound.splice(idx, 1);
979
+ clearTimeout(item.expiryTimer);
980
+ debugLog(`Queued message recalled (id=${id}, source=${item.source}, queueSize=${this.queuedInbound.length})`);
981
+ if (item.source === 'feishu') {
982
+ this.feishu.replyToMessage(item.messageId || undefined, item.chatId, '🗑️ 这条排队消息已在插件面板被撤回,不会送达 AI。');
983
+ }
984
+ return true;
985
+ }
943
986
  /**
944
987
  * 用户回复了一张「已结束」的卡片(不在超时续接窗口内)→ 忙时排队:
945
988
  * - 该项目已有新一轮在等 → 引导回复最新卡片(避免旧回复窜入错误轮次),视为已处理;
@@ -962,23 +1005,29 @@ class McpFeedbackServer {
962
1005
  /**
963
1006
  * 消息入队 + 回执用户「已排队」。每条消息带 60 分钟过期兜底:
964
1007
  * 到点仍未被 AI 读取(对话可能已结束)→ 引用回复告知未送达,绝不静默丢弃。
1008
+ * 飞书与面板消息共用同一个队列(严格按到达顺序),但回执链路分渠道:
1009
+ * 飞书走引用回复/表情,面板消息没有推送通道,靠轮询下发的队列列表反映在/不在。
965
1010
  */
966
- enqueueInbound(text, chatId, images, files, messageId, forProjectDir) {
1011
+ enqueueInbound(text, chatId, images, files, messageId, forProjectDir, source = 'feishu') {
967
1012
  // 上限保护:挤出最老的一条并告知未送达
968
1013
  while (this.queuedInbound.length >= McpFeedbackServer.QUEUE_MAX) {
969
1014
  const dropped = this.queuedInbound.shift();
970
1015
  if (!dropped)
971
1016
  break;
972
1017
  clearTimeout(dropped.expiryTimer);
973
- this.feishu.replyToMessage(dropped.messageId || undefined, dropped.chatId, '⚠️ 排队消息过多,这条消息已被挤出队列、未送达 AI,请稍后重发。');
1018
+ if (dropped.source === 'feishu') {
1019
+ this.feishu.replyToMessage(dropped.messageId || undefined, dropped.chatId, '⚠️ 排队消息过多,这条消息已被挤出队列、未送达 AI,请稍后重发。');
1020
+ }
974
1021
  }
975
1022
  const item = {
1023
+ id: `q${Date.now()}_${++this.queueSeq}`,
976
1024
  text,
977
1025
  chatId,
978
1026
  images,
979
1027
  files,
980
1028
  at: Date.now(),
981
1029
  messageId,
1030
+ source,
982
1031
  forProjectDir,
983
1032
  expiryTimer: undefined,
984
1033
  };
@@ -987,12 +1036,16 @@ class McpFeedbackServer {
987
1036
  if (idx < 0)
988
1037
  return; // 已被消费
989
1038
  this.queuedInbound.splice(idx, 1);
990
- this.feishu.replyToMessage(item.messageId || undefined, item.chatId, '⚠️ 这条排队消息等了 60 分钟仍未被 AI 读取(对话可能已结束),未能送达。需要的话请在 AI 下次询问时重发。');
1039
+ if (item.source === 'feishu') {
1040
+ this.feishu.replyToMessage(item.messageId || undefined, item.chatId, '⚠️ 这条排队消息等了 60 分钟仍未被 AI 读取(对话可能已结束),未能送达。需要的话请在 AI 下次询问时重发。');
1041
+ }
991
1042
  }, McpFeedbackServer.QUEUE_TTL_MS);
992
1043
  item.expiryTimer.unref?.();
993
1044
  this.queuedInbound.push(item);
994
- debugLog(`Inbound queued for busy AI (project=${forProjectDir}, queueSize=${this.queuedInbound.length})`);
995
- this.feishu.replyToMessage(messageId || undefined, chatId, `🤖 AI 正在工作中,这条消息已排队,将在「${this.projectName(forProjectDir)}」当前任务完成后自动读取。`);
1045
+ debugLog(`Inbound queued for busy AI (source=${source}, project=${forProjectDir}, queueSize=${this.queuedInbound.length})`);
1046
+ if (source === 'feishu') {
1047
+ this.feishu.replyToMessage(messageId || undefined, chatId, `🤖 AI 正在工作中,这条消息已排队,将在「${this.projectName(forProjectDir)}」当前任务完成后自动读取。`);
1048
+ }
996
1049
  }
997
1050
  /**
998
1051
  * 队列兑现:新一轮 pending 注册时,把属于该项目空间的所有排队消息合并成一次反馈送达,
@@ -1022,7 +1075,8 @@ class McpFeedbackServer {
1022
1075
  });
1023
1076
  const text = '[QUEUED_MESSAGES] 以下是用户在你执行上一轮任务期间「追加」发送的消息(当时你正忙,消息已排队暂存)。' +
1024
1077
  '注意:这些内容不是对你最新一轮工作摘要的回复,请结合任务上下文阅读并处理;' +
1025
- '如与你摘要中的询问冲突,以用户追加内容为准。处理完后照常调用 interactive_feedback 继续对话。\n\n' +
1078
+ '如与你摘要中的询问冲突,以用户追加内容为准。本次的 summary 用户并没有收到,如有必要可在下次带上。' +
1079
+ '处理完后照常调用 interactive_feedback 继续对话。\n\n' +
1026
1080
  `=== 追加消息(${matched.length} 条)===\n` +
1027
1081
  lines.join('\n\n');
1028
1082
  debugLog(`Consuming ${matched.length} queued message(s) for request: ${requestId}`);
@@ -1037,9 +1091,12 @@ class McpFeedbackServer {
1037
1091
  this.feishu.clearPending(requestId);
1038
1092
  // 标记为「飞书渠道 resolve」:插件面板据此重置,避免对着已消失的请求提交
1039
1093
  this.lastFeishuResolved = { id: requestId, at: Date.now() };
1040
- // 送达回执:给每条排队消息补表情
1094
+ // 送达回执:给每条飞书排队消息补表情(面板消息没有回执通道,
1095
+ // 其送达状态由轮询下发的队列列表体现——消费后列表随即清空)
1041
1096
  for (const m of matched) {
1042
- this.feishu.reactDone(m.messageId || undefined, m.chatId);
1097
+ if (m.source === 'feishu') {
1098
+ this.feishu.reactDone(m.messageId || undefined, m.chatId);
1099
+ }
1043
1100
  }
1044
1101
  }
1045
1102
  /** 「我的窗口」是否仍活着(无插件 host 无从判定,视为活跃) */
@@ -1293,9 +1350,66 @@ class McpFeedbackServer {
1293
1350
  feishu: this.feishu.getStatus(),
1294
1351
  feishuResolvedId: (this.lastFeishuResolved && Date.now() - this.lastFeishuResolved.at < 30000) ? this.lastFeishuResolved.id : null,
1295
1352
  pause: this.getPauseStateFor(chosen?.id),
1353
+ // 该窗口的忙时排队消息快照(飞书 + 面板同队列),供面板展示队列列表
1354
+ queued: this.queuedSnapshotFor(pollWs),
1296
1355
  }));
1297
1356
  return;
1298
1357
  }
1358
+ // 面板忙时排队:AI 正忙(该项目无等待中的请求)时,把面板消息排进忙时队列,
1359
+ // 与飞书消息共用同一队列(按到达顺序),下一轮 interactive_feedback 合并送达
1360
+ if (req.method === 'POST' && req.url === '/api/feedback/enqueue') {
1361
+ let body = '';
1362
+ req.on('data', (chunk) => { body += chunk.toString(); });
1363
+ req.on('end', () => {
1364
+ try {
1365
+ const { text, images, attachedFiles, projectDir } = JSON.parse(body);
1366
+ if (!projectDir) {
1367
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1368
+ res.end(JSON.stringify({ queued: false, reason: 'projectDir required' }));
1369
+ return;
1370
+ }
1371
+ // 忙时排队是全局开关(面板 UI 已隐藏排队入口,这里兜底防竞态)
1372
+ if (!this.feishu.isQueueWhenBusy()) {
1373
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1374
+ res.end(JSON.stringify({ queued: false, reason: 'disabled' }));
1375
+ return;
1376
+ }
1377
+ // 该项目有等待中的请求 → 不该排队,面板应直接提交(正常轮询下一秒就会显示该请求)
1378
+ if (this.hasPendingForProject(projectDir)) {
1379
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1380
+ res.end(JSON.stringify({ queued: false, reason: 'pending' }));
1381
+ return;
1382
+ }
1383
+ this.enqueueInbound(text || '', '', images || [], attachedFiles || [], '', projectDir, 'panel');
1384
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1385
+ res.end(JSON.stringify({ queued: true }));
1386
+ }
1387
+ catch {
1388
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1389
+ res.end(JSON.stringify({ queued: false, reason: 'invalid body' }));
1390
+ }
1391
+ });
1392
+ return;
1393
+ }
1394
+ // 撤回排队消息:面板队列列表的删除按钮触发,按队列项 id 定位。
1395
+ // 飞书来源的消息由 removeQueuedById 同步回执,保证两边状态一致
1396
+ if (req.method === 'POST' && req.url === '/api/feedback/queue/remove') {
1397
+ let body = '';
1398
+ req.on('data', (chunk) => { body += chunk.toString(); });
1399
+ req.on('end', () => {
1400
+ try {
1401
+ const { id } = JSON.parse(body);
1402
+ const removed = !!id && this.removeQueuedById(id);
1403
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1404
+ res.end(JSON.stringify({ removed }));
1405
+ }
1406
+ catch {
1407
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1408
+ res.end(JSON.stringify({ removed: false }));
1409
+ }
1410
+ });
1411
+ return;
1412
+ }
1299
1413
  // 提交反馈
1300
1414
  if (req.method === 'POST' && req.url === '/api/feedback/submit') {
1301
1415
  let body = '';
@@ -1342,6 +1456,30 @@ class McpFeedbackServer {
1342
1456
  return;
1343
1457
  }
1344
1458
  // 配置飞书(来自插件 UI;凭证未变只补 chatId,变了则重建长连接)
1459
+ // 扫码一键创建飞书应用:启动 Device Grant 流程并返回待扫码链接(等二维码就绪才响应)
1460
+ if (req.method === 'POST' && req.url === '/api/feishu/register/start') {
1461
+ this.feishu.startRegister().then((state) => {
1462
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1463
+ res.end(JSON.stringify(state));
1464
+ }).catch(() => {
1465
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1466
+ res.end(JSON.stringify({ status: 'error', error: 'internal' }));
1467
+ });
1468
+ return;
1469
+ }
1470
+ // 扫码创建流程状态(插件轮询直到 success / error)
1471
+ if (req.method === 'GET' && req.url === '/api/feishu/register/status') {
1472
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1473
+ res.end(JSON.stringify(this.feishu.getRegisterState()));
1474
+ return;
1475
+ }
1476
+ // 取消进行中的扫码创建流程(用户关闭设置弹窗)
1477
+ if (req.method === 'POST' && req.url === '/api/feishu/register/cancel') {
1478
+ this.feishu.cancelRegister();
1479
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1480
+ res.end(JSON.stringify({ ok: true }));
1481
+ return;
1482
+ }
1345
1483
  if (req.method === 'POST' && req.url === '/api/feishu/config') {
1346
1484
  let body = '';
1347
1485
  req.on('data', (chunk) => { body += chunk.toString(); });
@@ -1541,7 +1679,7 @@ class McpFeedbackServer {
1541
1679
  res.writeHead(200, { 'Content-Type': 'application/json' });
1542
1680
  res.end(JSON.stringify({
1543
1681
  status: 'ok',
1544
- version: '0.0.1',
1682
+ version: PKG_VERSION,
1545
1683
  hasCurrentRequest: this.currentRequest !== null,
1546
1684
  pid: process.pid,
1547
1685
  }));
@@ -20,7 +20,7 @@
20
20
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/></svg>
21
21
  </button>
22
22
  <button id="autoRetryBtn" class="iconbtn" type="button" aria-label="Timeout keep-waiting / 超时续期" data-tip="Timeout keep-waiting / 超时续期">
23
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M3 21v-5h5"/></svg>
23
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 22h14"/><path d="M5 2h14"/><path d="M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22"/><path d="M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2"/></svg>
24
24
  </button>
25
25
  <button id="themeAccentBtn" class="iconbtn" type="button" aria-label="Accent color / 主题色" data-tip="Accent color / 主题色">
26
26
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.555C21.965 6.012 17.461 2 12 2z"/></svg>
@@ -37,13 +37,20 @@
37
37
  </div>
38
38
  </header>
39
39
 
40
- <!-- 等待态 -->
40
+ <!-- 等待态(排队模式下排队消息列表内嵌在此卡片中,整体高度由分隔条控制) -->
41
41
  <section id="waitingStatus" class="empty">
42
42
  <div class="empty__icon" aria-hidden="true">
43
43
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
44
44
  </div>
45
45
  <p class="empty__title">{{i18n.waitingForAI}}</p>
46
46
  <p class="empty__hint">{{i18n.waitingHint}}</p>
47
+ <div id="queueCard" class="waiting-queue hidden">
48
+ <div class="waiting-queue__head">
49
+ <span class="waiting-queue__title">{{i18n.queuedTitle}}</span>
50
+ <span id="queueCount" class="queue-count"></span>
51
+ </div>
52
+ <div id="queueList" class="queue-list"></div>
53
+ </div>
47
54
  </section>
48
55
 
49
56
  <!-- 反馈表单 -->
@@ -55,6 +62,16 @@
55
62
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .962 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.962 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/></svg>
56
63
  </span>
57
64
  <h2 class="card__title">{{i18n.aiSummary}}</h2>
65
+ <!-- 历史摘要导航:存过 2 条以上时显示,可回看之前轮次的 AI 摘要 -->
66
+ <div id="summaryNav" class="summary-nav" hidden>
67
+ <button id="summaryPrevBtn" class="iconbtn summary-nav__btn" type="button" aria-label="{{i18n.summaryPrevTip}}" data-tip="{{i18n.summaryPrevTip}}">
68
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
69
+ </button>
70
+ <span id="summaryNavPos" class="summary-nav__pos"></span>
71
+ <button id="summaryNextBtn" class="iconbtn summary-nav__btn" type="button" aria-label="{{i18n.summaryNextTip}}" data-tip="{{i18n.summaryNextTip}}">
72
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>
73
+ </button>
74
+ </div>
58
75
  </div>
59
76
  <div id="summaryContent" class="summary markdown-body"></div>
60
77
  <div id="projectInfo" class="project-chip"></div>
@@ -183,7 +200,17 @@
183
200
  </div>
184
201
  </div>
185
202
 
186
- <!-- 区块②:飞书通知(推送开关 + 凭证 + 绑定状态) -->
203
+ <!-- 区块②:忙时消息排队(全局开关:飞书与面板消息共用忙时队列) -->
204
+ <div class="feishu-section">
205
+ <label class="feishu-switch">
206
+ <span class="feishu-switch__label feishu-switch__label--strong">{{i18n.queueWhenBusyLabel}}</span>
207
+ <input id="queueWhenBusyToggle" type="checkbox" class="feishu-switch__input">
208
+ <span class="feishu-switch__track"><span class="feishu-switch__thumb"></span></span>
209
+ </label>
210
+ <p class="feishu-modal__desc">{{i18n.queueWhenBusyDesc}}</p>
211
+ </div>
212
+
213
+ <!-- 区块③:飞书通知(推送开关 + 凭证 + 绑定状态) -->
187
214
  <div class="feishu-section">
188
215
  <label class="feishu-switch">
189
216
  <span class="feishu-switch__label feishu-switch__label--strong">{{i18n.feishuSettings}}</span>
@@ -199,14 +226,6 @@
199
226
  </label>
200
227
  <p class="feishu-modal__desc feishu-modal__desc--sub">{{i18n.feishuAckDesc}}</p>
201
228
  </div>
202
- <div id="feishuQueueSub" class="feishu-subitem">
203
- <label class="feishu-switch">
204
- <span class="feishu-switch__label">{{i18n.feishuQueueLabel}}</span>
205
- <input id="feishuQueueToggle" type="checkbox" class="feishu-switch__input">
206
- <span class="feishu-switch__track"><span class="feishu-switch__thumb"></span></span>
207
- </label>
208
- <p class="feishu-modal__desc feishu-modal__desc--sub">{{i18n.feishuQueueDesc}}</p>
209
- </div>
210
229
  <label class="feishu-field">
211
230
  <span class="feishu-field__label">{{i18n.feishuAppId}}</span>
212
231
  <input id="feishuAppId" class="feishu-input" type="text" placeholder="{{i18n.feishuAppIdPlaceholder}}" autocomplete="off" spellcheck="false">
@@ -226,7 +245,16 @@
226
245
  <span id="feishuStatusText" class="feishu-status__text">{{i18n.feishuStatusUnconfigured}}</span>
227
246
  </div>
228
247
  <div class="feishu-modal__actions">
229
- <button id="feishuGuideBtn" class="feishu-link" type="button">{{i18n.feishuGuide}}</button>
248
+ <button id="feishuRegisterBtn" class="feishu-link feishu-link--primary" type="button">{{i18n.feishuRegisterBtn}}</button>
249
+ </div>
250
+ <!-- 扫码一键创建应用:点击上方按钮后展开,展示验证二维码与流程状态 -->
251
+ <div id="feishuRegisterPanel" class="feishu-register hidden">
252
+ <img id="feishuRegisterQr" class="feishu-register__qr hidden" alt="QR code">
253
+ <p id="feishuRegisterHint" class="feishu-register__hint"></p>
254
+ <div class="feishu-register__actions">
255
+ <button id="feishuRegisterOpenBtn" class="feishu-link hidden" type="button">{{i18n.registerOpenLink}}</button>
256
+ <button id="feishuRegisterRetryBtn" class="feishu-link hidden" type="button">{{i18n.registerRetry}}</button>
257
+ </div>
230
258
  </div>
231
259
  <p class="feishu-modal__hint">{{i18n.feishuHint}}</p>
232
260
  </div>