@xmanrui/dsh-im 2.3.0 → 2.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -265,7 +265,7 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
265
265
  ? h("img", {
266
266
  src: qrSource,
267
267
  alt: repairing
268
- ? `用于修复${botName}卡片按钮的一次性授权二维码`
268
+ ? `用于为${botName}补全权限与回调的一次性授权二维码`
269
269
  : grantingGroupMessages
270
270
  ? `用于为${botName}开通群消息权限的一次性授权二维码`
271
271
  : "用于新增 DeepSeek Harness 飞书机器人的一次性授权二维码",
@@ -293,31 +293,31 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
293
293
  h("div", { className: "bxf-stateLabel dim-stateLabel" },
294
294
  h("span", { className: "bxf-dot dim-stateDot", "data-tone": "warning" }),
295
295
  h("span", null, repairing
296
- ? `正在修复「${botName}」`
296
+ ? `正在为「${botName}」补全权限与回调`
297
297
  : grantingGroupMessages
298
298
  ? `正在为「${botName}」开通群消息权限`
299
299
  : "正在添加新机器人")),
300
300
  h("h3", null, expired
301
301
  ? "刷新二维码后继续"
302
302
  : repairing
303
- ? "使用飞书扫码修复卡片按钮"
303
+ ? "使用飞书扫码补全权限"
304
304
  : grantingGroupMessages
305
305
  ? "使用飞书确认群消息权限"
306
306
  : "使用飞书扫码创建机器人"),
307
307
  h("p", null, repairing
308
- ? "扫码会更新现有飞书应用,只增量补充卡片按钮回调;不会创建新应用。确认后此机器人会短暂重连,其他机器人不受影响。"
308
+ ? "扫码会更新现有飞书应用,最多增量补充卡片按钮回调、读取用户消息内图片或文件所需的 im:message:readonly(飞书显示为“获取单聊、群组消息”),以及上传机器人图片或文件所需的 im:resource;不会创建新应用。确认页只显示当前缺少项,完成后此机器人会短暂重连,其他机器人不受影响。"
309
309
  : grantingGroupMessages
310
310
  ? "扫码会更新现有飞书应用,只增量开通“获取群组中所有消息”权限;不会创建新应用。确认后会自动启用“响应所有群消息”,其他机器人不受影响。"
311
311
  : "扫码只会新增一个机器人,已接入的机器人会继续正常收发消息。"),
312
312
  h("ol", { className: "bxf-steps dim-steps" },
313
313
  h("li", null, "打开飞书移动端,使用扫一扫读取二维码"),
314
314
  h("li", null, repairing
315
- ? "核对现有应用名称,并确认只新增卡片回调"
315
+ ? "核对现有应用名称,并确认只新增当前缺少的上述配置"
316
316
  : grantingGroupMessages
317
317
  ? "核对现有应用,并确认“获取群组中所有消息”权限"
318
318
  : "核对应用名称与权限范围,并确认创建"),
319
319
  h("li", null, repairing
320
- ? "保持本页打开,等待卡片按钮修复完成"
320
+ ? "保持本页打开,等待权限与回调补全完成"
321
321
  : grantingGroupMessages
322
322
  ? "保持本页打开,等待权限生效并自动切换响应方式"
323
323
  : "保持本页打开,等待新机器人的长连接就绪")),
@@ -336,7 +336,7 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
336
336
  ? h(Button, { onClick: onRefresh, disabled: busy }, "换一个二维码")
337
337
  : null,
338
338
  h(Button, { onClick: onCancel, disabled: busy }, repairing
339
- ? "取消修复"
339
+ ? "取消补全"
340
340
  : grantingGroupMessages ? "取消授权" : "取消添加")),
341
341
  ),
342
342
  ),
@@ -354,16 +354,16 @@ function ProvisionProgress({ phase, provision, onCancel, busy }) {
354
354
  h("div", { className: "dim-spinner", "aria-hidden": "true" }),
355
355
  h("h3", null, connecting
356
356
  ? repairing
357
- ? "已确认,正在完成卡片按钮修复"
357
+ ? "已确认,正在完成权限与回调配置"
358
358
  : grantingGroupMessages
359
359
  ? "已确认,正在启用全部消息模式"
360
360
  : "已确认,正在连接新机器人"
361
361
  : repairing
362
- ? "正在准备修复二维码"
362
+ ? "正在准备权限补全二维码"
363
363
  : grantingGroupMessages ? "正在准备权限授权二维码" : "正在准备授权二维码"),
364
364
  h("p", null, connecting
365
365
  ? repairing
366
- ? "配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。"
366
+ ? "配置已提交,正在保存权限、验证卡片回调并重连此机器人;此阶段无法取消,其他机器人不会中断。"
367
367
  : grantingGroupMessages
368
368
  ? "权限配置已提交,正在保存设置并重连此机器人;此阶段无法取消,其他机器人不会中断。"
369
369
  : "正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。"
@@ -375,7 +375,7 @@ function ProvisionProgress({ phase, provision, onCancel, busy }) {
375
375
  connecting && onCancel
376
376
  ? h("div", { className: "bxf-actions dim-viewActions", style: { justifyContent: "center" } },
377
377
  h(Button, { onClick: onCancel, disabled: busy }, repairing
378
- ? "取消修复"
378
+ ? "取消补全"
379
379
  : grantingGroupMessages ? "取消授权" : "取消添加"))
380
380
  : null,
381
381
  );
@@ -388,7 +388,7 @@ function ProvisionError({ error, provision, onRetry, onCancel, busy }) {
388
388
  h("div", { className: "bxf-inlineError dim-inlineError", role: "alert" },
389
389
  h("div", null,
390
390
  h("h3", null, repairing
391
- ? "卡片按钮没有修复完成"
391
+ ? "权限与回调没有补全完成"
392
392
  : grantingGroupMessages ? "群消息权限没有开通完成" : "新机器人没有添加完成"),
393
393
  h("p", null, error.message),
394
394
  error.code ? h("span", { className: "bxf-errorCode" }, error.code) : null,
@@ -564,6 +564,7 @@ export function BotCard({
564
564
  removeButtonRef,
565
565
  }) {
566
566
  const { bot, health, state, connected } = connection;
567
+ const repairTooltipId = React.useId();
567
568
  const stateForDisplay = busy === "reconnect"
568
569
  ? "connecting"
569
570
  : state;
@@ -637,13 +638,22 @@ export function BotCard({
637
638
  disabled: Boolean(busy), "aria-busy": busy === "reconnect" ? "true" : undefined,
638
639
  "aria-label": `${connected ? "检查连接" : "重试连接"}${bot.name}`,
639
640
  }, busy === "reconnect" ? (connected ? "检查中…" : "正在连接…") : connected ? "检查连接" : "重试连接"),
640
- h(Button, {
641
- className: "bxf-repairButton dim-cardAction",
642
- onClick: onRepairCallback,
643
- disabled: Boolean(busy) || repairDisabled,
644
- "aria-busy": busy === "callback-repair" ? "true" : undefined,
645
- "aria-label": `修复${bot.name}的卡片按钮`,
646
- }, busy === "callback-repair" ? "等待扫码…" : "修复卡片按钮"),
641
+ h("span", { className: "bxf-repairAction" },
642
+ h(Button, {
643
+ className: "bxf-repairButton dim-cardAction",
644
+ onClick: onRepairCallback,
645
+ disabled: Boolean(busy) || repairDisabled,
646
+ "aria-busy": busy === "callback-repair" ? "true" : undefined,
647
+ "aria-label": `为${bot.name}补全权限与回调`,
648
+ "aria-describedby": repairTooltipId,
649
+ }, busy === "callback-repair" ? "等待扫码…" : "补全权限"),
650
+ h("span", {
651
+ id: repairTooltipId,
652
+ className: "bxf-repairTooltip",
653
+ role: "tooltip",
654
+ },
655
+ h("strong", null, "补全范围"),
656
+ h("span", null, "最多增量添加卡片回调 card.action.trigger、读取消息内图片或文件所需的 im:message:readonly(飞书显示为“获取单聊、群组消息”),以及上传机器人图片或文件所需的 im:resource;确认页只显示当前缺少项,不会创建新应用。"))),
647
657
  h(Button, {
648
658
  className: "dim-cardAction", kind: "danger", onClick: onRequestRemove,
649
659
  disabled: Boolean(busy), ref: removeButtonRef,
@@ -933,7 +943,7 @@ export function FeishuSettingsTab({ rpcCall }) {
933
943
  && (provision.operation !== operation || provision.botId !== botId)) {
934
944
  throw new Error(grantingGroupMessages
935
945
  ? "飞书服务返回了不匹配的群消息权限二维码"
936
- : "飞书服务返回了不匹配的卡片修复二维码");
946
+ : "飞书服务返回了不匹配的权限补全二维码");
937
947
  }
938
948
  const timestamp = Date.now();
939
949
  setNow(timestamp);
@@ -948,7 +958,7 @@ export function FeishuSettingsTab({ rpcCall }) {
948
958
  },
949
959
  }));
950
960
  announce(repairing
951
- ? `${botName ?? "机器人"}的修复二维码已生成,请使用飞书扫码。`
961
+ ? `${botName ?? "机器人"}的权限补全二维码已生成,请使用飞书扫码。`
952
962
  : grantingGroupMessages
953
963
  ? `${botName ?? "机器人"}的群消息权限二维码已生成,请使用飞书确认。`
954
964
  : "授权二维码已生成,请使用飞书扫码。");
@@ -1032,7 +1042,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1032
1042
  : current);
1033
1043
  announce(grantingGroupMessages
1034
1044
  ? "权限配置已提交,正在启用全部消息模式并重连此机器人;此阶段无法取消,其他机器人不会中断。"
1035
- : "配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。");
1045
+ : "配置已提交,正在保存权限、验证卡片回调并重连此机器人;此阶段无法取消,其他机器人不会中断。");
1036
1046
  return;
1037
1047
  }
1038
1048
  if (result.status === "connected") {
@@ -1040,7 +1050,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1040
1050
  setModel((current) => ({ ...current, provisioning: null }));
1041
1051
  announce(grantingGroupMessages
1042
1052
  ? `${targetBotName}已开通群消息权限,并启用“响应所有群消息”。`
1043
- : `${targetBotName}的卡片按钮已修复。`);
1053
+ : `${targetBotName}的权限与回调已补全。`);
1044
1054
  if (activeProvision.botId) setFocusBotId(activeProvision.botId);
1045
1055
  await loadStatus({ silent: true, restoreProvisioning: false });
1046
1056
  return;
@@ -1048,7 +1058,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1048
1058
  }
1049
1059
  setModel((current) => ({ ...current, provisioning: null }));
1050
1060
  announce(repairing
1051
- ? "已取消卡片按钮修复。"
1061
+ ? "已取消补全权限与回调。"
1052
1062
  : grantingGroupMessages ? "已取消群消息权限授权。" : "已取消添加机器人。");
1053
1063
  await loadStatus({ silent: true, restoreProvisioning: false });
1054
1064
  scheduleAnimationFrame(() => {
@@ -1117,7 +1127,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1117
1127
  const targetBot = snapshot?.bots.find((bot) => bot.botId === result.botId);
1118
1128
  if (!snapshot) {
1119
1129
  throw new Error(isCallbackRepair(provision)
1120
- ? "卡片按钮已更新,但暂时无法确认机器人连接状态"
1130
+ ? "权限与回调已更新,但暂时无法确认机器人连接状态"
1121
1131
  : isGroupMessagePermission(provision)
1122
1132
  ? "群消息权限已更新,但暂时无法确认机器人连接状态"
1123
1133
  : "机器人已经创建,但暂时无法确认连接状态");
@@ -1130,7 +1140,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1130
1140
  }
1131
1141
  setModel((current) => ({ ...current, provisioning: null }));
1132
1142
  announce(isCallbackRepair(provision)
1133
- ? `${targetBot.bot.name}的卡片按钮已修复。`
1143
+ ? `${targetBot.bot.name}的权限与回调已补全。`
1134
1144
  : isGroupMessagePermission(provision)
1135
1145
  ? `${targetBot.bot.name}已开通群消息权限,并启用“响应所有群消息”。`
1136
1146
  : targetBot
@@ -1142,7 +1152,7 @@ export function FeishuSettingsTab({ rpcCall }) {
1142
1152
  if (result.status === "failed") {
1143
1153
  const error = new Error(result.message
1144
1154
  ?? (isCallbackRepair(provision)
1145
- ? "飞书卡片按钮修复失败"
1155
+ ? "飞书权限与回调补全失败"
1146
1156
  : isGroupMessagePermission(provision)
1147
1157
  ? "飞书群消息权限开通失败"
1148
1158
  : "飞书应用创建失败"));
@@ -450,10 +450,35 @@ const CSS = String.raw`
450
450
  .bxf-connectedFooter { display: flex; align-items: center; justify-content: space-between; gap: 15px; margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--dsw-alias-border-l1, #eef0f3); }
451
451
  .bxf-healthSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; }
452
452
  .bxf-healthSummary[data-error="true"] { color: var(--bxf-error); }
453
- .bxf-botActions { flex: none; width: 100%; flex-wrap: wrap; gap: 8px; margin-top: 0; justify-content: flex-end; }
453
+ .bxf-botActions { position: relative; flex: none; width: 100%; flex-wrap: wrap; gap: 8px; margin-top: 0; justify-content: flex-end; }
454
454
  .bxf-botActions .bxf-button { flex: none; white-space: nowrap; }
455
455
  .bxf-botActions .bxf-repairButton { color: var(--bxf-accent); border-color: color-mix(in srgb, var(--bxf-accent) 35%, var(--dsw-alias-border-l2, #dee0e3)); }
456
456
  .bxf-botActions .bxf-repairButton:hover:not(:disabled) { background: color-mix(in srgb, var(--bxf-accent) 7%, transparent); }
457
+ .bxf-repairAction { display: inline-flex; }
458
+ .bxf-repairTooltip {
459
+ position: absolute;
460
+ right: 0;
461
+ bottom: calc(100% + 8px);
462
+ z-index: 40;
463
+ width: min(330px, 100%);
464
+ display: grid;
465
+ gap: 3px;
466
+ padding: 9px 10px;
467
+ border: 1px solid var(--dsw-alias-border-l2, #dfe1e5);
468
+ border-radius: 8px;
469
+ color: var(--dsw-alias-label-primary, #1f2329);
470
+ background: var(--dsw-alias-bg-layer-3, #fff);
471
+ box-shadow: 0 10px 28px rgb(31 35 41 / 16%);
472
+ opacity: 0;
473
+ visibility: hidden;
474
+ transform: translateY(3px);
475
+ pointer-events: none;
476
+ transition: opacity .15s ease, transform .15s ease, visibility .15s ease;
477
+ }
478
+ .bxf-repairTooltip strong { font-size: 12px; line-height: 17px; font-weight: 650; }
479
+ .bxf-repairTooltip > span { color: var(--dsw-alias-label-secondary, #646a73); font-size: 11px; line-height: 17px; font-weight: 400; overflow-wrap: anywhere; }
480
+ .bxf-repairAction:hover .bxf-repairTooltip,
481
+ .bxf-repairAction:focus-within .bxf-repairTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
457
482
 
458
483
  .bxf-confirm {
459
484
  border-top: 1px solid var(--dsw-alias-border-l2, #dee0e3);
@@ -231,7 +231,7 @@ const EN = Object.freeze({
231
231
  '保持本页打开,等待新机器人的长连接就绪': 'Keep this page open until the bot connection is ready',
232
232
  '在飞书中打开': 'Open in Feishu',
233
233
  '取消添加': 'Cancel',
234
- '使用飞书扫码修复卡片按钮': 'Scan with Feishu to repair card buttons',
234
+ '使用飞书扫码补全权限': 'Scan with Feishu to complete permissions',
235
235
  '使用飞书确认群消息权限': 'Confirm group-message permission with Feishu',
236
236
  '扫码会更新现有飞书应用,只增量开通“获取群组中所有消息”权限;不会创建新应用。确认后会自动启用“响应所有群消息”,其他机器人不受影响。': 'Scanning updates the existing Feishu app with only the “Read all messages in associated group chat” scope. It does not create a new app. After confirmation, “Respond to all group messages” is enabled automatically; other bots are unaffected.',
237
237
  '核对现有应用,并确认“获取群组中所有消息”权限': 'Review the existing app and confirm the “Read all messages in associated group chat” permission',
@@ -242,18 +242,20 @@ const EN = Object.freeze({
242
242
  '权限配置已提交,正在启用全部消息模式并重连此机器人;此阶段无法取消,其他机器人不会中断。': 'The permission update was submitted. Enabling all-message mode and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.',
243
243
  '正在为现有飞书应用申请群消息权限二维码,请稍候。': 'Requesting a group-message permission QR code for the existing Feishu app…',
244
244
  '群消息权限没有开通完成': 'Group-message permission was not granted',
245
- '扫码会更新现有飞书应用,只增量补充卡片按钮回调;不会创建新应用。确认后此机器人会短暂重连,其他机器人不受影响。': 'Scanning updates the existing Feishu app with only the card-button callback. It does not create a new app. This bot reconnects briefly after confirmation; other bots are not affected.',
246
- '核对现有应用名称,并确认只新增卡片回调': 'Review the existing app name and confirm that only the card callback is added',
247
- '保持本页打开,等待卡片按钮修复完成': 'Keep this page open until card-button repair finishes',
248
- '取消修复': 'Cancel repair',
249
- '已确认,正在完成卡片按钮修复': 'Confirmed. Finishing card-button repair',
250
- '正在准备修复二维码': 'Preparing the repair QR code',
251
- '配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。': 'The update was submitted. Verifying the card callback and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.',
245
+ '扫码会更新现有飞书应用,最多增量补充卡片按钮回调、读取用户消息内图片或文件所需的 im:message:readonly(飞书显示为“获取单聊、群组消息”),以及上传机器人图片或文件所需的 im:resource;不会创建新应用。确认页只显示当前缺少项,完成后此机器人会短暂重连,其他机器人不受影响。': 'Scanning updates the existing Feishu app with up to three missing items: the card-button callback, im:message:readonly for reading images or files in user messages (shown by Feishu as “Read direct and group messages”), and im:resource for uploading images or files sent by the bot. It does not create a new app. The confirmation page shows only missing items; this bot reconnects briefly afterward, while other bots are unaffected.',
246
+ '核对现有应用名称,并确认只新增当前缺少的上述配置': 'Review the existing app name and confirm that only the missing items described above are added',
247
+ '保持本页打开,等待权限与回调补全完成': 'Keep this page open until permissions and the callback are complete',
248
+ '取消补全': 'Cancel setup',
249
+ '已确认,正在完成权限与回调配置': 'Confirmed. Completing permissions and callback setup',
250
+ '正在准备权限补全二维码': 'Preparing the permission-completion QR code',
251
+ '配置已提交,正在保存权限、验证卡片回调并重连此机器人;此阶段无法取消,其他机器人不会中断。': 'The update was submitted. Saving permissions, verifying the card callback, and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.',
252
252
  '正在为现有飞书应用申请一次性更新二维码,请稍候。': 'Requesting a one-time update QR code for the existing Feishu app…',
253
- '卡片按钮没有修复完成': 'Card-button repair did not finish',
254
- '修复卡片按钮': 'Repair card buttons',
253
+ '权限与回调没有补全完成': 'Permissions and callback setup did not finish',
254
+ '补全权限': 'Complete permissions',
255
+ '补全范围': 'Completion scope',
256
+ '最多增量添加卡片回调 card.action.trigger、读取消息内图片或文件所需的 im:message:readonly(飞书显示为“获取单聊、群组消息”),以及上传机器人图片或文件所需的 im:resource;确认页只显示当前缺少项,不会创建新应用。': 'Adds up to the missing card callback card.action.trigger, im:message:readonly for reading images or files in messages (shown by Feishu as “Read direct and group messages”), and im:resource for uploading images or files sent by the bot. The confirmation page shows only missing items; no new app is created.',
255
257
  '等待扫码…': 'Waiting for scan…',
256
- '飞书服务返回了不匹配的卡片修复二维码': 'Feishu returned a repair QR code for a different bot',
258
+ '飞书服务返回了不匹配的权限补全二维码': 'Feishu returned a permission-completion QR code for a different bot',
257
259
  '飞书服务返回了不匹配的群消息权限二维码': 'Feishu returned a group-message permission QR code for a different bot',
258
260
  '飞书服务返回的修复信息缺少 botId': 'Feishu repair status is missing the bot ID',
259
261
  '飞书服务返回的应用更新信息缺少 botId': 'Feishu app-update status is missing the bot ID',
@@ -261,16 +263,16 @@ const EN = Object.freeze({
261
263
  '此机器人': 'this bot',
262
264
  '用于为${botName}开通群消息权限的一次性授权二维码': 'One-time QR code for granting group-message permission to ${botName}',
263
265
  '正在为「${botName}」开通群消息权限': 'Granting group-message permission to “${botName}”',
264
- '${botName ?? "机器人"}的修复二维码已生成,请使用飞书扫码。': 'Repair QR code generated for ${botName ?? "bot"}. Scan it with Feishu.',
266
+ '${botName ?? "机器人"}的权限补全二维码已生成,请使用飞书扫码。': 'Permission-completion QR code generated for ${botName ?? "bot"}. Scan it with Feishu.',
265
267
  '${botName ?? "机器人"}的群消息权限二维码已生成,请使用飞书确认。': 'Group-message permission QR code generated for ${botName ?? "bot"}. Confirm it with Feishu.',
266
268
  '${targetBotName}已开通群消息权限,并启用“响应所有群消息”。': '${targetBotName} now has group-message permission and “Respond to all group messages” is enabled.',
267
269
  '${targetBot.bot.name}已开通群消息权限,并启用“响应所有群消息”。': '${targetBot.bot.name} now has group-message permission and “Respond to all group messages” is enabled.',
268
270
  '${targetBot.bot.name}已连接,可以在飞书中开始聊天。': '${targetBot.bot.name} is connected and ready to chat in Feishu.',
269
- '已取消卡片按钮修复。': 'Card-button repair was cancelled.',
271
+ '已取消补全权限与回调。': 'Completing permissions and the callback was cancelled.',
270
272
  '已取消群消息权限授权。': 'Group-message permission authorization was cancelled.',
271
- '卡片按钮已更新,但暂时无法确认机器人连接状态': 'The card callback was updated, but the bot connection could not be confirmed yet',
273
+ '权限与回调已更新,但暂时无法确认机器人连接状态': 'Permissions and the callback were updated, but the bot connection could not be confirmed yet',
272
274
  '群消息权限已更新,但暂时无法确认机器人连接状态': 'The group-message permission was updated, but the bot connection could not be confirmed yet',
273
- '飞书卡片按钮修复失败': 'Could not repair the Feishu card buttons',
275
+ '飞书权限与回调补全失败': 'Could not complete Feishu permissions and callback setup',
274
276
  '飞书群消息权限开通失败': 'Could not grant the Feishu group-message permission',
275
277
  '请先完成当前飞书授权操作,再开通群消息权限。': 'Finish the current Feishu authorization before granting group-message permission.',
276
278
  '已确认,正在连接新机器人': 'Confirmed. Connecting the new bot',
@@ -598,22 +600,22 @@ function translateDynamic(text) {
598
600
  if (match) return `Remove ${match[1]} from DeepSeek Harness`;
599
601
  match = /^(.+)的飞书授权流程$/.exec(text);
600
602
  if (match) return `Feishu authorization flow for ${match[1]}`;
601
- match = /^用于修复(.+)卡片按钮的一次性授权二维码$/.exec(text);
602
- if (match) return `One-time QR code for repairing card buttons for ${match[1]}`;
603
+ match = /^用于为(.+)补全权限与回调的一次性授权二维码$/.exec(text);
604
+ if (match) return `One-time QR code for completing permissions and the callback for ${match[1]}`;
603
605
  match = /^用于为(.+)开通群消息权限的一次性授权二维码$/.exec(text);
604
606
  if (match) return `One-time QR code for granting group-message permission to ${match[1]}`;
605
- match = /^正在修复「(.+)」$/.exec(text);
606
- if (match) return `Repairing “${match[1]}”`;
607
+ match = /^正在为「(.+)」补全权限与回调$/.exec(text);
608
+ if (match) return `Completing permissions and the callback for “${match[1]}”`;
607
609
  match = /^正在为「(.+)」开通群消息权限$/.exec(text);
608
610
  if (match) return `Granting group-message permission to “${match[1]}”`;
609
- match = /^修复(.+)的卡片按钮$/.exec(text);
610
- if (match) return `Repair card buttons for ${match[1]}`;
611
- match = /^(.+)的修复二维码已生成,请使用飞书扫码。$/.exec(text);
612
- if (match) return `Repair QR code generated for ${match[1]}. Scan it with Feishu.`;
611
+ match = /^为(.+)补全权限与回调$/.exec(text);
612
+ if (match) return `Complete permissions and the callback for ${match[1]}`;
613
+ match = /^(.+)的权限补全二维码已生成,请使用飞书扫码。$/.exec(text);
614
+ if (match) return `Permission-completion QR code generated for ${match[1]}. Scan it with Feishu.`;
613
615
  match = /^(.+)的群消息权限二维码已生成,请使用飞书确认。$/.exec(text);
614
616
  if (match) return `Group-message permission QR code generated for ${match[1]}. Confirm it with Feishu.`;
615
- match = /^(.+)的卡片按钮已修复。$/.exec(text);
616
- if (match) return `Card buttons repaired for ${match[1]}.`;
617
+ match = /^(.+)的权限与回调已补全。$/.exec(text);
618
+ if (match) return `Permissions and the callback completed for ${match[1]}.`;
617
619
  match = /^(.+)已开通群消息权限,并启用“响应所有群消息”。$/.exec(text);
618
620
  if (match) return `${match[1]} now has group-message permission and “Respond to all group messages” is enabled.`;
619
621
  match = /^(检查连接|重试连接)(.+)$/.exec(text);
@@ -77,6 +77,7 @@ const HELP_TEXT_LINES = [
77
77
  '/send 提交当前批次',
78
78
  '/cancel 取消当前批次',
79
79
  '/status 检查连接状态',
80
+ '/version 查看插件版本',
80
81
  '/help 显示本帮助',
81
82
  ];
82
83
 
@@ -106,9 +106,10 @@ const CARD_OVERLOAD_NOTICE_COOLDOWN_MS = 5_000;
106
106
  const CARD_DATA_TIMEOUT_MS = 5_000;
107
107
  const REPAIR_LINK_WAIT_MS = 15_000;
108
108
  const REPAIR_POLL_INTERVAL_MS = 1_000;
109
- const REPAIR_ACTIVE_STATES = new Set([
110
- 'starting', 'qr_ready', 'polling', 'slow_down', 'domain_switched', 'saving',
109
+ const REPAIR_AUTHORIZATION_STATES = new Set([
110
+ 'starting', 'qr_ready', 'polling', 'slow_down', 'domain_switched',
111
111
  ]);
112
+ const REPAIR_ACTIVE_STATES = new Set([...REPAIR_AUTHORIZATION_STATES, 'saving']);
112
113
  const REPAIR_TERMINAL_STATES = new Set([
113
114
  'succeeded', 'expired', 'cancelled', 'error',
114
115
  ]);
@@ -150,7 +151,7 @@ function artifactFailureText(fileName, error) {
150
151
  const name = String(fileName ?? t('结果文件')).replace(/[\r\n]+/g, ' ').trim() || t('结果文件');
151
152
  switch (error?.code) {
152
153
  case 'artifact-permission-required':
153
- return t('结果文件「{name}」已生成,但机器人缺少飞书文件上传权限。请为应用添加 im:resource 并完成必要审批后重试。', { name });
154
+ return t('结果文件「{name}」已生成,但机器人缺少飞书文件上传权限 im:resource。请私聊机器人执行 /repair 命令,或者在插件页面点击“补全权限”按钮并扫码。完成飞书要求的发布审批后重试。', { name });
154
155
  case 'artifact-too-large':
155
156
  return t('结果文件「{name}」超过飞书 30 MB 上限,未发送。', { name });
156
157
  case 'artifact-empty':
@@ -410,7 +411,6 @@ export class FeishuHarnessBridge {
410
411
  #botOpenId;
411
412
  #groupResponseMode;
412
413
  #repair;
413
- #repairOwnerOpenIds;
414
414
  #repairAttempt = null;
415
415
  #repairMonitorVersion = 0;
416
416
  #repairPollIntervalMs;
@@ -443,7 +443,6 @@ export class FeishuHarnessBridge {
443
443
  botOpenId,
444
444
  groupResponseMode = FEISHU_GROUP_RESPONSE_MODES.ALL,
445
445
  repair,
446
- repairOwnerOpenIds,
447
446
  repairPollIntervalMs = REPAIR_POLL_INTERVAL_MS,
448
447
  repairLinkWaitMs = REPAIR_LINK_WAIT_MS,
449
448
  cardDataTimeoutMs = CARD_DATA_TIMEOUT_MS,
@@ -480,10 +479,6 @@ export class FeishuHarnessBridge {
480
479
  this.#botOpenId = nonEmptyString(botOpenId);
481
480
  this.#groupResponseMode = normalizeFeishuGroupResponseMode(groupResponseMode);
482
481
  this.#repair = repair ?? null;
483
- const repairOwners = repairOwnerOpenIds ?? allowedSenderOpenIds;
484
- this.#repairOwnerOpenIds = new Set(
485
- [...(repairOwners ?? [])].filter((value) => typeof value === 'string' && value && value !== '*'),
486
- );
487
482
  this.#repairPollIntervalMs = repairPollIntervalMs;
488
483
  this.#repairLinkWaitMs = repairLinkWaitMs;
489
484
  this.#cardDataTimeoutMs = cardDataTimeoutMs;
@@ -1075,13 +1070,8 @@ export class FeishuHarnessBridge {
1075
1070
  return;
1076
1071
  }
1077
1072
  const actorOpenId = strictSenderOpenId(event);
1078
- if (!actorOpenId || !this.#repairOwnerOpenIds.has(actorOpenId)) {
1079
- await this.#send(
1080
- event.message.chat_id,
1081
- this.#repairOwnerOpenIds.size === 0
1082
- ? t('当前机器人没有可验证的接入者身份,不能从聊天发起修复;请先在插件页设置管理员。')
1083
- : t('此操作只能由机器人接入者在私聊中发起,未进行任何修改。'),
1084
- );
1073
+ if (!actorOpenId) {
1074
+ await this.#send(event.message.chat_id, t('无法识别当前发送者,未发起修复。'));
1085
1075
  return;
1086
1076
  }
1087
1077
  if (!this.#repair) {
@@ -1110,7 +1100,7 @@ export class FeishuHarnessBridge {
1110
1100
  return;
1111
1101
  }
1112
1102
  if (attempt.actorOpenId !== actorOpenId) {
1113
- await this.#send(chatId, t('另一位管理员正在修复该机器人,本次不会显示其授权信息。'));
1103
+ await this.#send(chatId, t('另一位用户正在修复该机器人,本次不会显示其授权信息。'));
1114
1104
  return;
1115
1105
  }
1116
1106
  if (operation === 'cancel') {
@@ -1162,21 +1152,53 @@ export class FeishuHarnessBridge {
1162
1152
 
1163
1153
  async #startRepair({ actorOpenId, chatId }) {
1164
1154
  const previous = this.#repairAttempt;
1155
+ let restarted = false;
1165
1156
  if (previous && REPAIR_ACTIVE_STATES.has(previous.snapshot.state)) {
1166
1157
  if (previous.actorOpenId !== actorOpenId) {
1167
- await this.#send(chatId, t('另一位管理员正在修复该机器人,本次不会显示其授权信息。'));
1158
+ await this.#send(chatId, t('另一位用户正在修复该机器人,本次不会显示其授权信息。'));
1168
1159
  return;
1169
1160
  }
1161
+ let current;
1170
1162
  try {
1171
- const current = await this.#refreshRepairAttempt(previous);
1172
- if (REPAIR_ACTIVE_STATES.has(current.state) && previous.verificationUrl) {
1173
- await this.#sendRepairLink(chatId, previous.verificationUrl, current, { existing: true });
1174
- return;
1175
- }
1163
+ current = await this.#refreshRepairAttempt(previous);
1176
1164
  } catch {
1177
1165
  await this.#send(chatId, t('暂时无法查询修复状态,请稍后重试。'));
1178
1166
  return;
1179
1167
  }
1168
+ // Once Feishu has accepted the update, starting another attempt could
1169
+ // race the credential swap and callback probe. Status commands remain
1170
+ // available while that non-cancellable convergence is in progress.
1171
+ if (current.state === 'saving') {
1172
+ await this.#send(chatId, this.#repairStatusText(current));
1173
+ return;
1174
+ }
1175
+ // Feishu launcher links carry one-time user codes. Opening one with the
1176
+ // wrong Open Platform account can consume it even though authorization
1177
+ // did not succeed, so a fresh bare /repair must replace a still-waiting
1178
+ // attempt instead of redisplaying the same unusable URL.
1179
+ if (REPAIR_AUTHORIZATION_STATES.has(current.state)) {
1180
+ let cancelled;
1181
+ try {
1182
+ cancelled = repairSnapshot(
1183
+ await this.#repair.cancel(this.#repairArgs(previous)),
1184
+ { botId: this.#botId },
1185
+ );
1186
+ previous.snapshot = cancelled;
1187
+ } catch {
1188
+ await this.#send(chatId, t('暂时无法取消旧修复任务,未生成新链接;请稍后重试。'));
1189
+ return;
1190
+ }
1191
+ if (!REPAIR_TERMINAL_STATES.has(cancelled.state)) {
1192
+ await this.#send(chatId, this.#repairStatusText(cancelled));
1193
+ return;
1194
+ }
1195
+ previous.stopped = true;
1196
+ this.#repairMonitorVersion += 1;
1197
+ restarted = cancelled.state === 'cancelled';
1198
+ } else if (REPAIR_TERMINAL_STATES.has(current.state)) {
1199
+ previous.stopped = true;
1200
+ this.#repairMonitorVersion += 1;
1201
+ }
1180
1202
  }
1181
1203
 
1182
1204
  let snapshot;
@@ -1225,7 +1247,7 @@ export class FeishuHarnessBridge {
1225
1247
  await this.#send(chatId, t('飞书未返回授权链接,已中止本次修复。'));
1226
1248
  return;
1227
1249
  }
1228
- await this.#sendRepairLink(chatId, attempt.verificationUrl, snapshot);
1250
+ await this.#sendRepairLink(chatId, attempt.verificationUrl, snapshot, { restarted });
1229
1251
  this.#monitorRepair(attempt);
1230
1252
  }
1231
1253
 
@@ -1303,15 +1325,15 @@ export class FeishuHarnessBridge {
1303
1325
  });
1304
1326
  }
1305
1327
 
1306
- async #sendRepairLink(chatId, url, snapshot, { existing = false } = {}) {
1328
+ async #sendRepairLink(chatId, url, snapshot, { restarted = false } = {}) {
1307
1329
  const remaining = snapshot.remainingSeconds
1308
1330
  ?? (snapshot.expiresAt ? Math.max(0, Math.ceil((snapshot.expiresAt - Date.now()) / 1000)) : null);
1309
1331
  const expiry = remaining === null
1310
1332
  ? t('链接为短期有效')
1311
1333
  : t('链接约 {minutes} 分钟后过期', { minutes: Math.max(1, Math.ceil(remaining / 60)) });
1312
1334
  await this.#send(chatId, [
1313
- existing ? t('已有一个修复任务在等待授权。') : t('🔧 准备修复卡片按钮。'),
1314
- t('本次只会增量添加 card.action.trigger。请核对确认页只显示这一项;若出现其他权限或事件,请取消。'),
1335
+ restarted ? t('旧授权链接已作废,已生成新的修复链接。') : t('🔧 准备补全权限与回调。'),
1336
+ t('本次最多增量添加三项:卡片回调 card.action.trigger;飞书显示为“获取单聊、群组消息”的租户权限 im:message:readonly(用于读取用户消息中的图片或文件);以及 im:resource(用于上传机器人发送的图片或文件)。确认页只会显示当前缺少的项;若出现上述范围之外的配置,请取消。'),
1315
1337
  '',
1316
1338
  t('当前设备直接打开:'),
1317
1339
  url,
@@ -124,7 +124,7 @@ function backButton() {
124
124
  * - presetCatalog: object|null (预设目录, {items,defaultId,_currentId})
125
125
  * - modelCatalog: object|null (模型目录, {groups,current})
126
126
  * Number fallback: 1=续写 2=新会话 3=会话列表 4=状态
127
- * 5=修复 6=帮助.
127
+ * 5=补全权限 6=帮助.
128
128
  */
129
129
  export function menuCard(ctx) {
130
130
  const {
@@ -323,7 +323,7 @@ export function menuCard(ctx) {
323
323
 
324
324
  // 命令与数字兜底说明
325
325
  elements.push({ tag: 'div', text: markdown(t(
326
- '**数字兜底**\n**1**工作区列表 · **2**新会话 · **3**会话列表 · **4**状态\n**5**🔧修复 · **6**帮助',
326
+ '**数字兜底**\n**1**工作区列表 · **2**新会话 · **3**会话列表 · **4**状态\n**5**🔧补全权限 · **6**帮助',
327
327
  )) });
328
328
  return cardWith(t('🤖 助手中心'), elements);
329
329
  }
@@ -495,6 +495,7 @@ export function menuHelpText() {
495
495
  '',
496
496
  '📊 状态 / 压缩',
497
497
  '/status 连接状态',
498
+ '/version 查看插件版本',
498
499
  '/compact 压缩当前会话上下文',
499
500
  '/archived on/off 会话列表显示/隐藏归档',
500
501
  '',
@@ -521,7 +522,7 @@ export function menuHelpText() {
521
522
  '🎮 任务控制',
522
523
  '/stop 停止当前任务',
523
524
  '/steer 指令 给 Agent 补充指令',
524
- '/repair 修复卡片按钮回调',
525
+ '/repair 补全飞书权限与卡片回调',
525
526
  ].map((line) => t(line)).join('\n');
526
527
  }
527
528
 
@@ -572,14 +573,14 @@ const HELP_TEXT_COMMANDS = [
572
573
  '`/batch` — 开启批量输入(仅私聊,最多 10 条文字)',
573
574
  '`/send` — 提交当前批次',
574
575
  '`/cancel` — 取消当前批次',
575
- '`/repair` — 修复卡片按钮',
576
+ '`/repair` — 补全飞书权限与卡片回调',
576
577
  ].join('\n');
577
578
 
578
579
  const HELP_NUMBER_FALLBACK = [
579
580
  '**💡 数字兜底**',
580
581
  '回复数字快速操作:',
581
582
  '**1**工作区列表 · **2**新会话 · **3**会话/关注',
582
- '**4**状态 · **5**修复 · **6**帮助',
583
+ '**4**状态 · **5**补全权限 · **6**帮助',
583
584
  ].join('\n');
584
585
 
585
586
  export function helpCard(extraTextLines = []) {
@@ -589,7 +590,10 @@ export function helpCard(extraTextLines = []) {
589
590
  const elements = [
590
591
  { tag: 'div', text: markdown(t(HELP_CARD_FEATURES)) },
591
592
  { tag: 'hr' },
592
- { tag: 'div', text: markdown(t(HELP_TEXT_COMMANDS) + extraText) },
593
+ { tag: 'div', text: markdown([
594
+ t(HELP_TEXT_COMMANDS),
595
+ t('`/version` — 查看插件版本'),
596
+ ].join('\n') + extraText) },
593
597
  { tag: 'hr' },
594
598
  { tag: 'div', text: markdown(t(HELP_NUMBER_FALLBACK)) },
595
599
  { tag: 'hr' },
@@ -267,7 +267,6 @@ export class FeishuRuntime {
267
267
  botOpenId: this.#botOpenId,
268
268
  groupResponseMode: this.#groupResponseMode,
269
269
  repair: this.#repair,
270
- repairOwnerOpenIds: new Set(this.#ownerOpenIds.filter((value) => value !== '*')),
271
270
  replyTimeoutMs: this.#replyTimeoutMs,
272
271
  signal,
273
272
  logger: this.#logger,
@@ -5,7 +5,7 @@ const FEISHU_MISSING_MESSAGE_SCOPE_CODE = 99991672;
5
5
  const FEISHU_ERROR_BODY_LIMIT = 64 * 1024;
6
6
  const FEISHU_ERROR_BODY_TIMEOUT_MS = 1_000;
7
7
  const FEISHU_IMAGE_PERMISSION_MESSAGE =
8
- '飞书机器人缺少图片读取权限。请在飞书开放平台为该应用添加 im:message:readonly,发布新版本并完成必要的管理员审批后,再重新发送图片。';
8
+ '飞书机器人缺少图片读取权限 im:message:readonly(飞书显示为“获取单聊、群组消息”)。请私聊机器人执行 /repair 命令,或者在插件页面点击“补全权限”按钮并扫码。按飞书提示发布新版本、完成必要审批后,再重新发送图片。';
9
9
 
10
10
  export function conversationKey(event) {
11
11
  const chatType = event?.message?.chat_type;
@@ -869,22 +869,6 @@ export class MultiBotDshFeishuController {
869
869
  'Feishu returned credentials for a different account domain.',
870
870
  );
871
871
  }
872
- if (record.initiator.actorOpenId && record.initiator.actorOpenId !== ownerOpenId) {
873
- throw this.#callbackRepairError(
874
- record,
875
- 'repair_owner_mismatch',
876
- 'The Feishu repair was confirmed by a different operator.',
877
- );
878
- }
879
- if (!target.ownerOpenIds.includes(ALL_VISIBLE_SENDERS)
880
- && !target.ownerOpenIds.includes(ownerOpenId)) {
881
- throw this.#callbackRepairError(
882
- record,
883
- 'repair_owner_mismatch',
884
- 'The Feishu repair operator is not an owner of this configured bot.',
885
- );
886
- }
887
-
888
872
  record.stage = 'verifying_identity';
889
873
  let verified;
890
874
  try {
@@ -1008,7 +992,7 @@ export class MultiBotDshFeishuController {
1008
992
  record.stage = 'awaiting_callback';
1009
993
  try {
1010
994
  const proof = await runtime.beginCardActionProbe({
1011
- expectedOperatorOpenId: ownerOpenId,
995
+ expectedOperatorOpenId: record.initiator.actorOpenId ?? ownerOpenId,
1012
996
  timeoutMs: this.#callbackProbeTimeoutMs,
1013
997
  ...(record.initiator.chatId ? { chatId: record.initiator.chatId } : {}),
1014
998
  });