mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-17 00:53:22 +08:00
feat(feishu): add group message permission authorization
This commit is contained in:
parent
7b892c02f5
commit
7bf12628ce
17 changed files with 1581 additions and 258 deletions
292
lib/client.js
292
lib/client.js
|
|
@ -260,10 +260,22 @@ var EN = Object.freeze({
|
||||||
"\u98DE\u4E66\u673A\u5668\u4EBA\u8BBE\u7F6E": "Feishu bot settings",
|
"\u98DE\u4E66\u673A\u5668\u4EBA\u8BBE\u7F6E": "Feishu bot settings",
|
||||||
"\u7FA4\u804A\u54CD\u5E94\u65B9\u5F0F": "Group response mode",
|
"\u7FA4\u804A\u54CD\u5E94\u65B9\u5F0F": "Group response mode",
|
||||||
"\u4EC5\u5728 @\u673A\u5668\u4EBA\u65F6\u54CD\u5E94\uFF08\u63A8\u8350\uFF09": "Only respond when @mentioned (recommended)",
|
"\u4EC5\u5728 @\u673A\u5668\u4EBA\u65F6\u54CD\u5E94\uFF08\u63A8\u8350\uFF09": "Only respond when @mentioned (recommended)",
|
||||||
|
"\u54CD\u5E94\u6240\u6709\u7FA4\u6D88\u606F": "Respond to all group messages",
|
||||||
"\u54CD\u5E94\u6240\u6709\u7FA4\u6D88\u606F\uFF08\u9700\u98DE\u4E66\u654F\u611F\u6743\u9650\uFF09": "Respond to all group messages (requires a sensitive Feishu scope)",
|
"\u54CD\u5E94\u6240\u6709\u7FA4\u6D88\u606F\uFF08\u9700\u98DE\u4E66\u654F\u611F\u6743\u9650\uFF09": "Respond to all group messages (requires a sensitive Feishu scope)",
|
||||||
|
"\u91CD\u65B0\u6388\u6743": "Reauthorize",
|
||||||
|
"\u53BB\u6388\u6743": "Authorize",
|
||||||
|
"\u91CD\u65B0\u6388\u6743\u7FA4\u6D88\u606F\u6743\u9650": "Reauthorize group-message permission",
|
||||||
|
"\u6388\u6743\u7FA4\u6D88\u606F\u6743\u9650": "Authorize group-message permission",
|
||||||
|
"\u6B63\u5728\u51C6\u5907\u6388\u6743\u2026": "Preparing authorization\u2026",
|
||||||
|
"\u6B63\u5728\u51C6\u5907\u2026": "Preparing\u2026",
|
||||||
|
"\u79C1\u804A\u59CB\u7EC8\u54CD\u5E94\uFF1B\u7FA4\u804A\u4EC5\u5904\u7406\u660E\u786E @\u5F53\u524D\u673A\u5668\u4EBA\u7684\u6D88\u606F\u3002\u7FA4\u6D88\u606F\u6743\u9650\u5DF2\u5F00\u901A\uFF0C\u518D\u6B21\u5207\u6362\u65E0\u9700\u6388\u6743\u3002": "Direct messages always work; group chats require an explicit @mention of this bot. The group-message permission is already granted, so switching again needs no authorization.",
|
||||||
|
"\u79C1\u804A\u59CB\u7EC8\u54CD\u5E94\uFF1B\u7FA4\u804A\u4EC5\u5904\u7406\u660E\u786E @\u5F53\u524D\u673A\u5668\u4EBA\u7684\u6D88\u606F\u3002\u9009\u62E9\u5168\u90E8\u6D88\u606F\u540E\u4F1A\u6253\u5F00\u98DE\u4E66\u5B98\u65B9\u6388\u6743\u6D41\u7A0B\u3002": "Direct messages always work; group chats require an explicit @mention of this bot. Selecting all messages opens the official Feishu authorization flow.",
|
||||||
|
"\u5DF2\u5F00\u901A\u201C\u83B7\u53D6\u7FA4\u7EC4\u4E2D\u6240\u6709\u6D88\u606F\u201D\u6743\u9650\uFF08im:message.group_msg\uFF09\uFF1B\u673A\u5668\u4EBA\u4F1A\u5904\u7406\u7FA4\u804A\u4E2D\u7684\u6240\u6709\u53EF\u89C1\u6D88\u606F\u3002": "The \u201CRead all messages in associated group chat\u201D scope (im:message.group_msg) is granted; the bot processes every visible group message.",
|
||||||
|
"\u5C1A\u672A\u786E\u8BA4\u201C\u83B7\u53D6\u7FA4\u7EC4\u4E2D\u6240\u6709\u6D88\u606F\u201D\u6743\u9650\uFF0C\u8BF7\u5B8C\u6210\u98DE\u4E66\u6388\u6743\u3002": "The \u201CRead all messages in associated group chat\u201D scope has not been confirmed. Complete Feishu authorization.",
|
||||||
"\u79C1\u804A\u59CB\u7EC8\u54CD\u5E94\uFF1B\u7FA4\u804A\u4EC5\u5904\u7406\u660E\u786E @\u5F53\u524D\u673A\u5668\u4EBA\u7684\u6D88\u606F\u3002": "Direct messages always work; group chats require an explicit @mention of this bot.",
|
"\u79C1\u804A\u59CB\u7EC8\u54CD\u5E94\uFF1B\u7FA4\u804A\u4EC5\u5904\u7406\u660E\u786E @\u5F53\u524D\u673A\u5668\u4EBA\u7684\u6D88\u606F\u3002": "Direct messages always work; group chats require an explicit @mention of this bot.",
|
||||||
"\u9700\u5728\u98DE\u4E66\u4E3A\u8BE5\u673A\u5668\u4EBA\u5F00\u901A\u201C\u83B7\u53D6\u7FA4\u7EC4\u4E2D\u6240\u6709\u6D88\u606F\u201D\u6743\u9650\uFF08im:message.group_msg\uFF09\uFF1B\u5F00\u901A\u540E\uFF0C\u673A\u5668\u4EBA\u4F1A\u5904\u7406\u7FA4\u804A\u4E2D\u7684\u6240\u6709\u53EF\u89C1\u6D88\u606F\u3002": "Grant this bot the \u201CRead all messages in associated group chat\u201D Feishu scope (im:message.group_msg); once granted, it will process every visible group message.",
|
"\u9700\u5728\u98DE\u4E66\u4E3A\u8BE5\u673A\u5668\u4EBA\u5F00\u901A\u201C\u83B7\u53D6\u7FA4\u7EC4\u4E2D\u6240\u6709\u6D88\u606F\u201D\u6743\u9650\uFF08im:message.group_msg\uFF09\uFF1B\u5F00\u901A\u540E\uFF0C\u673A\u5668\u4EBA\u4F1A\u5904\u7406\u7FA4\u804A\u4E2D\u7684\u6240\u6709\u53EF\u89C1\u6D88\u606F\u3002": "Grant this bot the \u201CRead all messages in associated group chat\u201D Feishu scope (im:message.group_msg); once granted, it will process every visible group message.",
|
||||||
"\u7FA4\u804A\u54CD\u5E94\u65B9\u5F0F\u4FEE\u6539\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002": "Could not update the group response mode. Try again.",
|
"\u7FA4\u804A\u54CD\u5E94\u65B9\u5F0F\u4FEE\u6539\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002": "Could not update the group response mode. Try again.",
|
||||||
|
"\u7FA4\u6D88\u606F\u6743\u9650\u6388\u6743\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002": "Could not authorize group-message permission. Try again.",
|
||||||
"\u9489\u9489\u8BBE\u7F6E": "DingTalk settings",
|
"\u9489\u9489\u8BBE\u7F6E": "DingTalk settings",
|
||||||
"\u4F01\u4E1A\u5FAE\u4FE1\u8BBE\u7F6E": "WeCom settings",
|
"\u4F01\u4E1A\u5FAE\u4FE1\u8BBE\u7F6E": "WeCom settings",
|
||||||
"\u626B\u7801\u63A5\u5165\u673A\u5668\u4EBA": "Scan QR code",
|
"\u626B\u7801\u63A5\u5165\u673A\u5668\u4EBA": "Scan QR code",
|
||||||
|
|
@ -386,6 +398,7 @@ var EN = Object.freeze({
|
||||||
"\u751F\u6210 QQ \u4E8C\u7EF4\u7801": "Generate QQ QR code",
|
"\u751F\u6210 QQ \u4E8C\u7EF4\u7801": "Generate QQ QR code",
|
||||||
"\u6B63\u5728\u751F\u6210\u4E8C\u7EF4\u7801\u2026": "Generating QR code\u2026",
|
"\u6B63\u5728\u751F\u6210\u4E8C\u7EF4\u7801\u2026": "Generating QR code\u2026",
|
||||||
"\u6B63\u5728\u51C6\u5907\u6388\u6743\u4E8C\u7EF4\u7801": "Preparing authorization QR code",
|
"\u6B63\u5728\u51C6\u5907\u6388\u6743\u4E8C\u7EF4\u7801": "Preparing authorization QR code",
|
||||||
|
"\u6B63\u5728\u51C6\u5907\u6743\u9650\u6388\u6743\u4E8C\u7EF4\u7801": "Preparing permission authorization QR code",
|
||||||
"\u6B63\u5728\u51C6\u5907\u5FAE\u4FE1\u4E8C\u7EF4\u7801": "Preparing WeChat QR code",
|
"\u6B63\u5728\u51C6\u5907\u5FAE\u4FE1\u4E8C\u7EF4\u7801": "Preparing WeChat QR code",
|
||||||
"\u6B63\u5728\u6DFB\u52A0\u65B0\u673A\u5668\u4EBA": "Adding a new bot",
|
"\u6B63\u5728\u6DFB\u52A0\u65B0\u673A\u5668\u4EBA": "Adding a new bot",
|
||||||
"\u6B63\u5728\u7533\u8BF7\u9489\u9489\u6388\u6743\u4E8C\u7EF4\u7801\u2026": "Requesting DingTalk authorization QR code\u2026",
|
"\u6B63\u5728\u7533\u8BF7\u9489\u9489\u6388\u6743\u4E8C\u7EF4\u7801\u2026": "Requesting DingTalk authorization QR code\u2026",
|
||||||
|
|
@ -403,6 +416,16 @@ var EN = Object.freeze({
|
||||||
"\u5728\u98DE\u4E66\u4E2D\u6253\u5F00": "Open in Feishu",
|
"\u5728\u98DE\u4E66\u4E2D\u6253\u5F00": "Open in Feishu",
|
||||||
"\u53D6\u6D88\u6DFB\u52A0": "Cancel",
|
"\u53D6\u6D88\u6DFB\u52A0": "Cancel",
|
||||||
"\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u4FEE\u590D\u5361\u7247\u6309\u94AE": "Scan with Feishu to repair card buttons",
|
"\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u4FEE\u590D\u5361\u7247\u6309\u94AE": "Scan with Feishu to repair card buttons",
|
||||||
|
"\u4F7F\u7528\u98DE\u4E66\u786E\u8BA4\u7FA4\u6D88\u606F\u6743\u9650": "Confirm group-message permission with Feishu",
|
||||||
|
"\u626B\u7801\u4F1A\u66F4\u65B0\u73B0\u6709\u98DE\u4E66\u5E94\u7528\uFF0C\u53EA\u589E\u91CF\u5F00\u901A\u201C\u83B7\u53D6\u7FA4\u7EC4\u4E2D\u6240\u6709\u6D88\u606F\u201D\u6743\u9650\uFF1B\u4E0D\u4F1A\u521B\u5EFA\u65B0\u5E94\u7528\u3002\u786E\u8BA4\u540E\u4F1A\u81EA\u52A8\u542F\u7528\u201C\u54CD\u5E94\u6240\u6709\u7FA4\u6D88\u606F\u201D\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u53D7\u5F71\u54CD\u3002": "Scanning updates the existing Feishu app with only the \u201CRead all messages in associated group chat\u201D scope. It does not create a new app. After confirmation, \u201CRespond to all group messages\u201D is enabled automatically; other bots are unaffected.",
|
||||||
|
"\u6838\u5BF9\u73B0\u6709\u5E94\u7528\uFF0C\u5E76\u786E\u8BA4\u201C\u83B7\u53D6\u7FA4\u7EC4\u4E2D\u6240\u6709\u6D88\u606F\u201D\u6743\u9650": "Review the existing app and confirm the \u201CRead all messages in associated group chat\u201D permission",
|
||||||
|
"\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u6743\u9650\u751F\u6548\u5E76\u81EA\u52A8\u5207\u6362\u54CD\u5E94\u65B9\u5F0F": "Keep this page open while the permission takes effect and the response mode switches automatically",
|
||||||
|
"\u53D6\u6D88\u6388\u6743": "Cancel authorization",
|
||||||
|
"\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u542F\u7528\u5168\u90E8\u6D88\u606F\u6A21\u5F0F": "Confirmed. Enabling all-message mode",
|
||||||
|
"\u6743\u9650\u914D\u7F6E\u5DF2\u63D0\u4EA4\uFF0C\u6B63\u5728\u4FDD\u5B58\u8BBE\u7F6E\u5E76\u91CD\u8FDE\u6B64\u673A\u5668\u4EBA\uFF1B\u6B64\u9636\u6BB5\u65E0\u6CD5\u53D6\u6D88\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002": "The permission update was submitted. Saving the setting and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.",
|
||||||
|
"\u6743\u9650\u914D\u7F6E\u5DF2\u63D0\u4EA4\uFF0C\u6B63\u5728\u542F\u7528\u5168\u90E8\u6D88\u606F\u6A21\u5F0F\u5E76\u91CD\u8FDE\u6B64\u673A\u5668\u4EBA\uFF1B\u6B64\u9636\u6BB5\u65E0\u6CD5\u53D6\u6D88\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002": "The permission update was submitted. Enabling all-message mode and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.",
|
||||||
|
"\u6B63\u5728\u4E3A\u73B0\u6709\u98DE\u4E66\u5E94\u7528\u7533\u8BF7\u7FA4\u6D88\u606F\u6743\u9650\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u5019\u3002": "Requesting a group-message permission QR code for the existing Feishu app\u2026",
|
||||||
|
"\u7FA4\u6D88\u606F\u6743\u9650\u6CA1\u6709\u5F00\u901A\u5B8C\u6210": "Group-message permission was not granted",
|
||||||
"\u626B\u7801\u4F1A\u66F4\u65B0\u73B0\u6709\u98DE\u4E66\u5E94\u7528\uFF0C\u53EA\u589E\u91CF\u8865\u5145\u5361\u7247\u6309\u94AE\u56DE\u8C03\uFF1B\u4E0D\u4F1A\u521B\u5EFA\u65B0\u5E94\u7528\u3002\u786E\u8BA4\u540E\u6B64\u673A\u5668\u4EBA\u4F1A\u77ED\u6682\u91CD\u8FDE\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u53D7\u5F71\u54CD\u3002": "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.",
|
"\u626B\u7801\u4F1A\u66F4\u65B0\u73B0\u6709\u98DE\u4E66\u5E94\u7528\uFF0C\u53EA\u589E\u91CF\u8865\u5145\u5361\u7247\u6309\u94AE\u56DE\u8C03\uFF1B\u4E0D\u4F1A\u521B\u5EFA\u65B0\u5E94\u7528\u3002\u786E\u8BA4\u540E\u6B64\u673A\u5668\u4EBA\u4F1A\u77ED\u6682\u91CD\u8FDE\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u53D7\u5F71\u54CD\u3002": "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.",
|
||||||
"\u6838\u5BF9\u73B0\u6709\u5E94\u7528\u540D\u79F0\uFF0C\u5E76\u786E\u8BA4\u53EA\u65B0\u589E\u5361\u7247\u56DE\u8C03": "Review the existing app name and confirm that only the card callback is added",
|
"\u6838\u5BF9\u73B0\u6709\u5E94\u7528\u540D\u79F0\uFF0C\u5E76\u786E\u8BA4\u53EA\u65B0\u589E\u5361\u7247\u56DE\u8C03": "Review the existing app name and confirm that only the card callback is added",
|
||||||
"\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u5361\u7247\u6309\u94AE\u4FEE\u590D\u5B8C\u6210": "Keep this page open until card-button repair finishes",
|
"\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u5361\u7247\u6309\u94AE\u4FEE\u590D\u5B8C\u6210": "Keep this page open until card-button repair finishes",
|
||||||
|
|
@ -415,14 +438,25 @@ var EN = Object.freeze({
|
||||||
"\u4FEE\u590D\u5361\u7247\u6309\u94AE": "Repair card buttons",
|
"\u4FEE\u590D\u5361\u7247\u6309\u94AE": "Repair card buttons",
|
||||||
"\u7B49\u5F85\u626B\u7801\u2026": "Waiting for scan\u2026",
|
"\u7B49\u5F85\u626B\u7801\u2026": "Waiting for scan\u2026",
|
||||||
"\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u5339\u914D\u7684\u5361\u7247\u4FEE\u590D\u4E8C\u7EF4\u7801": "Feishu returned a repair QR code for a different bot",
|
"\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u5339\u914D\u7684\u5361\u7247\u4FEE\u590D\u4E8C\u7EF4\u7801": "Feishu returned a repair QR code for a different bot",
|
||||||
|
"\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u5339\u914D\u7684\u7FA4\u6D88\u606F\u6743\u9650\u4E8C\u7EF4\u7801": "Feishu returned a group-message permission QR code for a different bot",
|
||||||
"\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u7684\u4FEE\u590D\u4FE1\u606F\u7F3A\u5C11 botId": "Feishu repair status is missing the bot ID",
|
"\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u7684\u4FEE\u590D\u4FE1\u606F\u7F3A\u5C11 botId": "Feishu repair status is missing the bot ID",
|
||||||
|
"\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u7684\u5E94\u7528\u66F4\u65B0\u4FE1\u606F\u7F3A\u5C11 botId": "Feishu app-update status is missing the bot ID",
|
||||||
"\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u5339\u914D\u7684\u6CE8\u518C\u8FDB\u5EA6": "Feishu returned registration progress for a different operation",
|
"\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u5339\u914D\u7684\u6CE8\u518C\u8FDB\u5EA6": "Feishu returned registration progress for a different operation",
|
||||||
"\u6B64\u673A\u5668\u4EBA": "this bot",
|
"\u6B64\u673A\u5668\u4EBA": "this bot",
|
||||||
|
"\u7528\u4E8E\u4E3A${botName}\u5F00\u901A\u7FA4\u6D88\u606F\u6743\u9650\u7684\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801": "One-time QR code for granting group-message permission to ${botName}",
|
||||||
|
"\u6B63\u5728\u4E3A\u300C${botName}\u300D\u5F00\u901A\u7FA4\u6D88\u606F\u6743\u9650": "Granting group-message permission to \u201C${botName}\u201D",
|
||||||
'${botName ?? "\u673A\u5668\u4EBA"}\u7684\u4FEE\u590D\u4E8C\u7EF4\u7801\u5DF2\u751F\u6210\uFF0C\u8BF7\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u3002': 'Repair QR code generated for ${botName ?? "bot"}. Scan it with Feishu.',
|
'${botName ?? "\u673A\u5668\u4EBA"}\u7684\u4FEE\u590D\u4E8C\u7EF4\u7801\u5DF2\u751F\u6210\uFF0C\u8BF7\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u3002': 'Repair QR code generated for ${botName ?? "bot"}. Scan it with Feishu.',
|
||||||
|
'${botName ?? "\u673A\u5668\u4EBA"}\u7684\u7FA4\u6D88\u606F\u6743\u9650\u4E8C\u7EF4\u7801\u5DF2\u751F\u6210\uFF0C\u8BF7\u4F7F\u7528\u98DE\u4E66\u786E\u8BA4\u3002': 'Group-message permission QR code generated for ${botName ?? "bot"}. Confirm it with Feishu.',
|
||||||
|
"${targetBotName}\u5DF2\u5F00\u901A\u7FA4\u6D88\u606F\u6743\u9650\uFF0C\u5E76\u542F\u7528\u201C\u54CD\u5E94\u6240\u6709\u7FA4\u6D88\u606F\u201D\u3002": "${targetBotName} now has group-message permission and \u201CRespond to all group messages\u201D is enabled.",
|
||||||
|
"${targetBot.bot.name}\u5DF2\u5F00\u901A\u7FA4\u6D88\u606F\u6743\u9650\uFF0C\u5E76\u542F\u7528\u201C\u54CD\u5E94\u6240\u6709\u7FA4\u6D88\u606F\u201D\u3002": "${targetBot.bot.name} now has group-message permission and \u201CRespond to all group messages\u201D is enabled.",
|
||||||
"${targetBot.bot.name}\u5DF2\u8FDE\u63A5\uFF0C\u53EF\u4EE5\u5728\u98DE\u4E66\u4E2D\u5F00\u59CB\u804A\u5929\u3002": "${targetBot.bot.name} is connected and ready to chat in Feishu.",
|
"${targetBot.bot.name}\u5DF2\u8FDE\u63A5\uFF0C\u53EF\u4EE5\u5728\u98DE\u4E66\u4E2D\u5F00\u59CB\u804A\u5929\u3002": "${targetBot.bot.name} is connected and ready to chat in Feishu.",
|
||||||
"\u5DF2\u53D6\u6D88\u5361\u7247\u6309\u94AE\u4FEE\u590D\u3002": "Card-button repair was cancelled.",
|
"\u5DF2\u53D6\u6D88\u5361\u7247\u6309\u94AE\u4FEE\u590D\u3002": "Card-button repair was cancelled.",
|
||||||
|
"\u5DF2\u53D6\u6D88\u7FA4\u6D88\u606F\u6743\u9650\u6388\u6743\u3002": "Group-message permission authorization was cancelled.",
|
||||||
"\u5361\u7247\u6309\u94AE\u5DF2\u66F4\u65B0\uFF0C\u4F46\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u673A\u5668\u4EBA\u8FDE\u63A5\u72B6\u6001": "The card callback was updated, but the bot connection could not be confirmed yet",
|
"\u5361\u7247\u6309\u94AE\u5DF2\u66F4\u65B0\uFF0C\u4F46\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u673A\u5668\u4EBA\u8FDE\u63A5\u72B6\u6001": "The card callback was updated, but the bot connection could not be confirmed yet",
|
||||||
|
"\u7FA4\u6D88\u606F\u6743\u9650\u5DF2\u66F4\u65B0\uFF0C\u4F46\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u673A\u5668\u4EBA\u8FDE\u63A5\u72B6\u6001": "The group-message permission was updated, but the bot connection could not be confirmed yet",
|
||||||
"\u98DE\u4E66\u5361\u7247\u6309\u94AE\u4FEE\u590D\u5931\u8D25": "Could not repair the Feishu card buttons",
|
"\u98DE\u4E66\u5361\u7247\u6309\u94AE\u4FEE\u590D\u5931\u8D25": "Could not repair the Feishu card buttons",
|
||||||
|
"\u98DE\u4E66\u7FA4\u6D88\u606F\u6743\u9650\u5F00\u901A\u5931\u8D25": "Could not grant the Feishu group-message permission",
|
||||||
|
"\u8BF7\u5148\u5B8C\u6210\u5F53\u524D\u98DE\u4E66\u6388\u6743\u64CD\u4F5C\uFF0C\u518D\u5F00\u901A\u7FA4\u6D88\u606F\u6743\u9650\u3002": "Finish the current Feishu authorization before granting group-message permission.",
|
||||||
"\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u8FDE\u63A5\u65B0\u673A\u5668\u4EBA": "Confirmed. Connecting the new bot",
|
"\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u8FDE\u63A5\u65B0\u673A\u5668\u4EBA": "Confirmed. Connecting the new bot",
|
||||||
"\u6B63\u5728\u5B89\u5168\u4FDD\u5B58\u51ED\u636E\u5E76\u68C0\u67E5\u65B0\u673A\u5668\u4EBA\u7684\u6D88\u606F\u901A\u9053\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002": "Saving credentials and checking the new bot connection. Existing bots will not be interrupted.",
|
"\u6B63\u5728\u5B89\u5168\u4FDD\u5B58\u51ED\u636E\u5E76\u68C0\u67E5\u65B0\u673A\u5668\u4EBA\u7684\u6D88\u606F\u901A\u9053\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002": "Saving credentials and checking the new bot connection. Existing bots will not be interrupted.",
|
||||||
"\u6B63\u5728\u5411\u98DE\u4E66\u7533\u8BF7\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u5019\u3002": "Requesting a one-time authorization QR code from Feishu\u2026",
|
"\u6B63\u5728\u5411\u98DE\u4E66\u7533\u8BF7\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u5019\u3002": "Requesting a one-time authorization QR code from Feishu\u2026",
|
||||||
|
|
@ -715,16 +749,26 @@ function translateDynamic(text5) {
|
||||||
if (match) return `Remove \u201C${match[1]}\u201D from DeepSeek Harness?`;
|
if (match) return `Remove \u201C${match[1]}\u201D from DeepSeek Harness?`;
|
||||||
match = /^从 DeepSeek Harness 移除(.+)$/.exec(text5);
|
match = /^从 DeepSeek Harness 移除(.+)$/.exec(text5);
|
||||||
if (match) return `Remove ${match[1]} from DeepSeek Harness`;
|
if (match) return `Remove ${match[1]} from DeepSeek Harness`;
|
||||||
|
match = /^(.+)的飞书授权流程$/.exec(text5);
|
||||||
|
if (match) return `Feishu authorization flow for ${match[1]}`;
|
||||||
match = /^用于修复(.+)卡片按钮的一次性授权二维码$/.exec(text5);
|
match = /^用于修复(.+)卡片按钮的一次性授权二维码$/.exec(text5);
|
||||||
if (match) return `One-time QR code for repairing card buttons for ${match[1]}`;
|
if (match) return `One-time QR code for repairing card buttons for ${match[1]}`;
|
||||||
|
match = /^用于为(.+)开通群消息权限的一次性授权二维码$/.exec(text5);
|
||||||
|
if (match) return `One-time QR code for granting group-message permission to ${match[1]}`;
|
||||||
match = /^正在修复「(.+)」$/.exec(text5);
|
match = /^正在修复「(.+)」$/.exec(text5);
|
||||||
if (match) return `Repairing \u201C${match[1]}\u201D`;
|
if (match) return `Repairing \u201C${match[1]}\u201D`;
|
||||||
|
match = /^正在为「(.+)」开通群消息权限$/.exec(text5);
|
||||||
|
if (match) return `Granting group-message permission to \u201C${match[1]}\u201D`;
|
||||||
match = /^修复(.+)的卡片按钮$/.exec(text5);
|
match = /^修复(.+)的卡片按钮$/.exec(text5);
|
||||||
if (match) return `Repair card buttons for ${match[1]}`;
|
if (match) return `Repair card buttons for ${match[1]}`;
|
||||||
match = /^(.+)的修复二维码已生成,请使用飞书扫码。$/.exec(text5);
|
match = /^(.+)的修复二维码已生成,请使用飞书扫码。$/.exec(text5);
|
||||||
if (match) return `Repair QR code generated for ${match[1]}. Scan it with Feishu.`;
|
if (match) return `Repair QR code generated for ${match[1]}. Scan it with Feishu.`;
|
||||||
|
match = /^(.+)的群消息权限二维码已生成,请使用飞书确认。$/.exec(text5);
|
||||||
|
if (match) return `Group-message permission QR code generated for ${match[1]}. Confirm it with Feishu.`;
|
||||||
match = /^(.+)的卡片按钮已修复。$/.exec(text5);
|
match = /^(.+)的卡片按钮已修复。$/.exec(text5);
|
||||||
if (match) return `Card buttons repaired for ${match[1]}.`;
|
if (match) return `Card buttons repaired for ${match[1]}.`;
|
||||||
|
match = /^(.+)已开通群消息权限,并启用“响应所有群消息”。$/.exec(text5);
|
||||||
|
if (match) return `${match[1]} now has group-message permission and \u201CRespond to all group messages\u201D is enabled.`;
|
||||||
match = /^(检查连接|重试连接)(.+)$/.exec(text5);
|
match = /^(检查连接|重试连接)(.+)$/.exec(text5);
|
||||||
if (match) return `${localizeText(match[1])} ${match[2]}`;
|
if (match) return `${localizeText(match[1])} ${match[2]}`;
|
||||||
match = /^移除(.+)$/.exec(text5);
|
match = /^移除(.+)$/.exec(text5);
|
||||||
|
|
@ -3284,6 +3328,7 @@ var FEISHU_ENDPOINTS = Object.freeze({
|
||||||
status: "connection.status",
|
status: "connection.status",
|
||||||
beginProvisioning: "provision.begin",
|
beginProvisioning: "provision.begin",
|
||||||
beginCallbackRepair: "bot.callback-repair.begin",
|
beginCallbackRepair: "bot.callback-repair.begin",
|
||||||
|
beginGroupMessagePermission: "bot.group-message-permission.begin",
|
||||||
pollProvisioning: "provision.poll",
|
pollProvisioning: "provision.poll",
|
||||||
cancelProvisioning: "provision.cancel",
|
cancelProvisioning: "provision.cancel",
|
||||||
bindCredentials: "bot.bind-credentials",
|
bindCredentials: "bot.bind-credentials",
|
||||||
|
|
@ -3299,7 +3344,8 @@ var FEISHU_ENDPOINTS = Object.freeze({
|
||||||
});
|
});
|
||||||
var FEISHU_REGISTRATION_OPERATIONS = Object.freeze({
|
var FEISHU_REGISTRATION_OPERATIONS = Object.freeze({
|
||||||
PROVISION: "provision",
|
PROVISION: "provision",
|
||||||
CALLBACK_REPAIR: "callback_repair"
|
CALLBACK_REPAIR: "callback_repair",
|
||||||
|
GROUP_MESSAGE_PERMISSION: "group_message_permission"
|
||||||
});
|
});
|
||||||
var CONNECTION_STATES = /* @__PURE__ */ new Set([
|
var CONNECTION_STATES = /* @__PURE__ */ new Set([
|
||||||
"disconnected",
|
"disconnected",
|
||||||
|
|
@ -3339,7 +3385,16 @@ function clamp2(value, min, max, fallback) {
|
||||||
return typeof value === "number" && Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;
|
return typeof value === "number" && Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;
|
||||||
}
|
}
|
||||||
function normalizeRegistrationOperation(value) {
|
function normalizeRegistrationOperation(value) {
|
||||||
return value === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR ? FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR : FEISHU_REGISTRATION_OPERATIONS.PROVISION;
|
if (value === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR) {
|
||||||
|
return FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR;
|
||||||
|
}
|
||||||
|
if (value === FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION) {
|
||||||
|
return FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION;
|
||||||
|
}
|
||||||
|
return FEISHU_REGISTRATION_OPERATIONS.PROVISION;
|
||||||
|
}
|
||||||
|
function isTargetedAppUpdate(operation) {
|
||||||
|
return operation === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR || operation === FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION;
|
||||||
}
|
}
|
||||||
function unwrapRpcResult3(result) {
|
function unwrapRpcResult3(result) {
|
||||||
if (!isRecord3(result) || typeof result.ok !== "boolean") {
|
if (!isRecord3(result) || typeof result.ok !== "boolean") {
|
||||||
|
|
@ -3367,8 +3422,8 @@ function normalizeProvisioning2(value, now = Date.now()) {
|
||||||
const expireIn = clamp2(source.expireIn, 1, 60 * 60, 5 * 60);
|
const expireIn = clamp2(source.expireIn, 1, 60 * 60, 5 * 60);
|
||||||
const operation = normalizeRegistrationOperation(source.operation);
|
const operation = normalizeRegistrationOperation(source.operation);
|
||||||
const botId = optionalString2(source.botId);
|
const botId = optionalString2(source.botId);
|
||||||
if (operation === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR && !botId) {
|
if (isTargetedAppUpdate(operation) && !botId) {
|
||||||
throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u7684\u4FEE\u590D\u4FE1\u606F\u7F3A\u5C11 botId");
|
throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u7684\u5E94\u7528\u66F4\u65B0\u4FE1\u606F\u7F3A\u5C11 botId");
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
attemptId,
|
attemptId,
|
||||||
|
|
@ -3431,6 +3486,7 @@ function normalizeBotConnection(value, fallbackBotId) {
|
||||||
workspace: optionalString2(value.workspace)?.slice(0, 4096) ?? "",
|
workspace: optionalString2(value.workspace)?.slice(0, 4096) ?? "",
|
||||||
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
||||||
groupResponseMode: normalizeGroupResponseMode(value.groupResponseMode),
|
groupResponseMode: normalizeGroupResponseMode(value.groupResponseMode),
|
||||||
|
groupMessagePermissionGranted: value.groupMessagePermissionGranted === true,
|
||||||
bot: normalizeBot2(value.bot),
|
bot: normalizeBot2(value.bot),
|
||||||
health: normalizeHealth(value.health, connected),
|
health: normalizeHealth(value.health, connected),
|
||||||
error: normalizeError2(value.error)
|
error: normalizeError2(value.error)
|
||||||
|
|
@ -4020,7 +4076,40 @@ var CSS3 = String.raw`
|
||||||
.bxf-responseModeSelect:focus-visible { outline: none; border-color: var(--bxf-accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--bxf-accent) 16%, transparent); }
|
.bxf-responseModeSelect:focus-visible { outline: none; border-color: var(--bxf-accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--bxf-accent) 16%, transparent); }
|
||||||
.bxf-responseModeSelect:disabled { cursor: not-allowed; opacity: .55; }
|
.bxf-responseModeSelect:disabled { cursor: not-allowed; opacity: .55; }
|
||||||
.bxf-responseModeHelp { grid-column: 1 / -1; grid-row: 3; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 1.45; }
|
.bxf-responseModeHelp { grid-column: 1 / -1; grid-row: 3; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 1.45; }
|
||||||
.bxf-responseModeError { grid-column: 1 / -1; grid-row: 4; color: var(--bxf-error); font-size: 12px; line-height: 1.4; margin: 0; }
|
.bxf-responseModePermissionAction { grid-column: 1 / -1; grid-row: 4; display: flex; justify-content: flex-start; margin-top: 2px; }
|
||||||
|
.bxf-responseModePermissionButton { min-height: 28px; padding: 3px 9px; color: var(--bxf-accent); border-color: color-mix(in srgb, var(--bxf-accent) 30%, var(--dsw-alias-border-l2, #dfe1e5)); background: var(--dsw-alias-bg-layer-1, #fff); }
|
||||||
|
.bxf-responseModePermissionButton:hover:not(:disabled) { background: color-mix(in srgb, var(--bxf-accent) 7%, transparent); }
|
||||||
|
.bxf-responseModeError { grid-column: 1 / -1; grid-row: 5; color: var(--bxf-error); font-size: 12px; line-height: 1.4; margin: 0; }
|
||||||
|
|
||||||
|
.bxf-botProvision {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 14px;
|
||||||
|
scroll-margin-block: 20px;
|
||||||
|
animation: bxf-revealProvision .2s var(--ds-ease-out, ease-out) both;
|
||||||
|
}
|
||||||
|
.bxf-botProvision:focus { outline: none; }
|
||||||
|
.bxf-botProvision:focus-visible {
|
||||||
|
outline: 2px solid color-mix(in srgb, var(--bxf-accent) 75%, transparent);
|
||||||
|
outline-offset: 3px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.bxf-botProvision > .bxf-provisionCard {
|
||||||
|
border-radius: 11px;
|
||||||
|
box-shadow: none;
|
||||||
|
background: color-mix(in srgb, var(--bxf-accent) 2.5%, var(--dsw-alias-bg-layer-3, #fff));
|
||||||
|
}
|
||||||
|
.bxf-botProvision .bxf-cardBody { padding: 18px; }
|
||||||
|
.bxf-botProvision .bxf-qrLayout {
|
||||||
|
grid-template-columns: 184px minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.bxf-botProvision .bxf-qrFrame { width: 176px; height: 176px; padding: 10px; border-radius: 11px; }
|
||||||
|
.bxf-botProvision .bxf-countdown { width: 176px; }
|
||||||
|
.bxf-botProvision .bxf-qrCopy h3 { font-size: 18px; line-height: 26px; }
|
||||||
|
.bxf-botProvision .bxf-steps { gap: 8px; margin-top: 14px; }
|
||||||
|
.bxf-botProvision .bxf-actions { margin-top: 16px; }
|
||||||
|
.bxf-botProvision .bxf-inlineError { min-height: 160px; padding: 22px; }
|
||||||
|
|
||||||
.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); }
|
.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); }
|
||||||
.bxf-healthSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; }
|
.bxf-healthSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; }
|
||||||
|
|
@ -4074,6 +4163,7 @@ var CSS3 = String.raw`
|
||||||
@keyframes bxf-rotate { to { transform: rotate(360deg); } }
|
@keyframes bxf-rotate { to { transform: rotate(360deg); } }
|
||||||
@keyframes bxf-pulse { 0%, 100% { transform: scale(.9); opacity: .45; } 50% { transform: scale(1.08); opacity: 1; } }
|
@keyframes bxf-pulse { 0%, 100% { transform: scale(.9); opacity: .45; } 50% { transform: scale(1.08); opacity: 1; } }
|
||||||
@keyframes bxf-shimmer { to { background-position: -220% 0; } }
|
@keyframes bxf-shimmer { to { background-position: -220% 0; } }
|
||||||
|
@keyframes bxf-revealProvision { from { opacity: 0; transform: translateY(-5px); } }
|
||||||
|
|
||||||
@container (max-width: 620px) {
|
@container (max-width: 620px) {
|
||||||
.bxf-headingTools { gap: 6px; }
|
.bxf-headingTools { gap: 6px; }
|
||||||
|
|
@ -4085,6 +4175,7 @@ var CSS3 = String.raw`
|
||||||
.bxf-intro { grid-template-columns: minmax(0, 1fr); }
|
.bxf-intro { grid-template-columns: minmax(0, 1fr); }
|
||||||
.bxf-markStage { display: none; }
|
.bxf-markStage { display: none; }
|
||||||
.bxf-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }
|
.bxf-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }
|
||||||
|
.bxf-botProvision .bxf-qrLayout { grid-template-columns: minmax(0, 1fr); }
|
||||||
.bxf-qrCopy { width: 100%; }
|
.bxf-qrCopy { width: 100%; }
|
||||||
.bxf-statusGrid { grid-template-columns: minmax(0, 1fr); }
|
.bxf-statusGrid { grid-template-columns: minmax(0, 1fr); }
|
||||||
.bxf-connectedTop { align-items: flex-start; flex-direction: column; }
|
.bxf-connectedTop { align-items: flex-start; flex-direction: column; }
|
||||||
|
|
@ -4121,9 +4212,16 @@ function installFeishuStyles() {
|
||||||
|
|
||||||
// plugin-src/client/channels/feishu/index.js
|
// plugin-src/client/channels/feishu/index.js
|
||||||
var CALLBACK_REPAIR_OPERATION = FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR;
|
var CALLBACK_REPAIR_OPERATION = FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR;
|
||||||
|
var GROUP_MESSAGE_PERMISSION_OPERATION = FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION;
|
||||||
function isCallbackRepair(value) {
|
function isCallbackRepair(value) {
|
||||||
return value?.operation === CALLBACK_REPAIR_OPERATION;
|
return value?.operation === CALLBACK_REPAIR_OPERATION;
|
||||||
}
|
}
|
||||||
|
function isGroupMessagePermission(value) {
|
||||||
|
return value?.operation === GROUP_MESSAGE_PERMISSION_OPERATION;
|
||||||
|
}
|
||||||
|
function isTargetedAppUpdate2(value) {
|
||||||
|
return isCallbackRepair(value) || isGroupMessagePermission(value);
|
||||||
|
}
|
||||||
function SvgIcon({ children, size = 18, className, viewBox = "0 0 24 24" }) {
|
function SvgIcon({ children, size = 18, className, viewBox = "0 0 24 24" }) {
|
||||||
return h2("svg", {
|
return h2("svg", {
|
||||||
width: size,
|
width: size,
|
||||||
|
|
@ -4305,6 +4403,7 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
||||||
const expired = provision.expired === true || remaining === 0;
|
const expired = provision.expired === true || remaining === 0;
|
||||||
const progress = Math.min(1, remaining / Math.max(1, provision.durationMs ?? remaining));
|
const progress = Math.min(1, remaining / Math.max(1, provision.durationMs ?? remaining));
|
||||||
const repairing = isCallbackRepair(provision);
|
const repairing = isCallbackRepair(provision);
|
||||||
|
const grantingGroupMessages = isGroupMessagePermission(provision);
|
||||||
const botName = provision.botName ?? "\u6B64\u673A\u5668\u4EBA";
|
const botName = provision.botName ?? "\u6B64\u673A\u5668\u4EBA";
|
||||||
React11.useEffect(() => setImageFailed(false), [qrSource]);
|
React11.useEffect(() => setImageFailed(false), [qrSource]);
|
||||||
return h2(
|
return h2(
|
||||||
|
|
@ -4321,7 +4420,7 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
||||||
{ className: "bxf-qrFrame dim-qrFrame" },
|
{ className: "bxf-qrFrame dim-qrFrame" },
|
||||||
qrSource && !imageFailed ? h2("img", {
|
qrSource && !imageFailed ? h2("img", {
|
||||||
src: qrSource,
|
src: qrSource,
|
||||||
alt: repairing ? `\u7528\u4E8E\u4FEE\u590D${botName}\u5361\u7247\u6309\u94AE\u7684\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801` : "\u7528\u4E8E\u65B0\u589E DeepSeek Harness \u98DE\u4E66\u673A\u5668\u4EBA\u7684\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801",
|
alt: repairing ? `\u7528\u4E8E\u4FEE\u590D${botName}\u5361\u7247\u6309\u94AE\u7684\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801` : grantingGroupMessages ? `\u7528\u4E8E\u4E3A${botName}\u5F00\u901A\u7FA4\u6D88\u606F\u6743\u9650\u7684\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801` : "\u7528\u4E8E\u65B0\u589E DeepSeek Harness \u98DE\u4E66\u673A\u5668\u4EBA\u7684\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801",
|
||||||
onError: () => setImageFailed(true)
|
onError: () => setImageFailed(true)
|
||||||
}) : h2(
|
}) : h2(
|
||||||
"div",
|
"div",
|
||||||
|
|
@ -4360,16 +4459,16 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
||||||
"div",
|
"div",
|
||||||
{ className: "bxf-stateLabel dim-stateLabel" },
|
{ className: "bxf-stateLabel dim-stateLabel" },
|
||||||
h2("span", { className: "bxf-dot dim-stateDot", "data-tone": "warning" }),
|
h2("span", { className: "bxf-dot dim-stateDot", "data-tone": "warning" }),
|
||||||
h2("span", null, repairing ? `\u6B63\u5728\u4FEE\u590D\u300C${botName}\u300D` : "\u6B63\u5728\u6DFB\u52A0\u65B0\u673A\u5668\u4EBA")
|
h2("span", null, repairing ? `\u6B63\u5728\u4FEE\u590D\u300C${botName}\u300D` : grantingGroupMessages ? `\u6B63\u5728\u4E3A\u300C${botName}\u300D\u5F00\u901A\u7FA4\u6D88\u606F\u6743\u9650` : "\u6B63\u5728\u6DFB\u52A0\u65B0\u673A\u5668\u4EBA")
|
||||||
),
|
),
|
||||||
h2("h3", null, expired ? "\u5237\u65B0\u4E8C\u7EF4\u7801\u540E\u7EE7\u7EED" : repairing ? "\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u4FEE\u590D\u5361\u7247\u6309\u94AE" : "\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u521B\u5EFA\u673A\u5668\u4EBA"),
|
h2("h3", null, expired ? "\u5237\u65B0\u4E8C\u7EF4\u7801\u540E\u7EE7\u7EED" : repairing ? "\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u4FEE\u590D\u5361\u7247\u6309\u94AE" : grantingGroupMessages ? "\u4F7F\u7528\u98DE\u4E66\u786E\u8BA4\u7FA4\u6D88\u606F\u6743\u9650" : "\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u521B\u5EFA\u673A\u5668\u4EBA"),
|
||||||
h2("p", null, repairing ? "\u626B\u7801\u4F1A\u66F4\u65B0\u73B0\u6709\u98DE\u4E66\u5E94\u7528\uFF0C\u53EA\u589E\u91CF\u8865\u5145\u5361\u7247\u6309\u94AE\u56DE\u8C03\uFF1B\u4E0D\u4F1A\u521B\u5EFA\u65B0\u5E94\u7528\u3002\u786E\u8BA4\u540E\u6B64\u673A\u5668\u4EBA\u4F1A\u77ED\u6682\u91CD\u8FDE\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u53D7\u5F71\u54CD\u3002" : "\u626B\u7801\u53EA\u4F1A\u65B0\u589E\u4E00\u4E2A\u673A\u5668\u4EBA\uFF0C\u5DF2\u63A5\u5165\u7684\u673A\u5668\u4EBA\u4F1A\u7EE7\u7EED\u6B63\u5E38\u6536\u53D1\u6D88\u606F\u3002"),
|
h2("p", null, repairing ? "\u626B\u7801\u4F1A\u66F4\u65B0\u73B0\u6709\u98DE\u4E66\u5E94\u7528\uFF0C\u53EA\u589E\u91CF\u8865\u5145\u5361\u7247\u6309\u94AE\u56DE\u8C03\uFF1B\u4E0D\u4F1A\u521B\u5EFA\u65B0\u5E94\u7528\u3002\u786E\u8BA4\u540E\u6B64\u673A\u5668\u4EBA\u4F1A\u77ED\u6682\u91CD\u8FDE\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u53D7\u5F71\u54CD\u3002" : grantingGroupMessages ? "\u626B\u7801\u4F1A\u66F4\u65B0\u73B0\u6709\u98DE\u4E66\u5E94\u7528\uFF0C\u53EA\u589E\u91CF\u5F00\u901A\u201C\u83B7\u53D6\u7FA4\u7EC4\u4E2D\u6240\u6709\u6D88\u606F\u201D\u6743\u9650\uFF1B\u4E0D\u4F1A\u521B\u5EFA\u65B0\u5E94\u7528\u3002\u786E\u8BA4\u540E\u4F1A\u81EA\u52A8\u542F\u7528\u201C\u54CD\u5E94\u6240\u6709\u7FA4\u6D88\u606F\u201D\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u53D7\u5F71\u54CD\u3002" : "\u626B\u7801\u53EA\u4F1A\u65B0\u589E\u4E00\u4E2A\u673A\u5668\u4EBA\uFF0C\u5DF2\u63A5\u5165\u7684\u673A\u5668\u4EBA\u4F1A\u7EE7\u7EED\u6B63\u5E38\u6536\u53D1\u6D88\u606F\u3002"),
|
||||||
h2(
|
h2(
|
||||||
"ol",
|
"ol",
|
||||||
{ className: "bxf-steps dim-steps" },
|
{ className: "bxf-steps dim-steps" },
|
||||||
h2("li", null, "\u6253\u5F00\u98DE\u4E66\u79FB\u52A8\u7AEF\uFF0C\u4F7F\u7528\u626B\u4E00\u626B\u8BFB\u53D6\u4E8C\u7EF4\u7801"),
|
h2("li", null, "\u6253\u5F00\u98DE\u4E66\u79FB\u52A8\u7AEF\uFF0C\u4F7F\u7528\u626B\u4E00\u626B\u8BFB\u53D6\u4E8C\u7EF4\u7801"),
|
||||||
h2("li", null, repairing ? "\u6838\u5BF9\u73B0\u6709\u5E94\u7528\u540D\u79F0\uFF0C\u5E76\u786E\u8BA4\u53EA\u65B0\u589E\u5361\u7247\u56DE\u8C03" : "\u6838\u5BF9\u5E94\u7528\u540D\u79F0\u4E0E\u6743\u9650\u8303\u56F4\uFF0C\u5E76\u786E\u8BA4\u521B\u5EFA"),
|
h2("li", null, repairing ? "\u6838\u5BF9\u73B0\u6709\u5E94\u7528\u540D\u79F0\uFF0C\u5E76\u786E\u8BA4\u53EA\u65B0\u589E\u5361\u7247\u56DE\u8C03" : grantingGroupMessages ? "\u6838\u5BF9\u73B0\u6709\u5E94\u7528\uFF0C\u5E76\u786E\u8BA4\u201C\u83B7\u53D6\u7FA4\u7EC4\u4E2D\u6240\u6709\u6D88\u606F\u201D\u6743\u9650" : "\u6838\u5BF9\u5E94\u7528\u540D\u79F0\u4E0E\u6743\u9650\u8303\u56F4\uFF0C\u5E76\u786E\u8BA4\u521B\u5EFA"),
|
||||||
h2("li", null, repairing ? "\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u5361\u7247\u6309\u94AE\u4FEE\u590D\u5B8C\u6210" : "\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u65B0\u673A\u5668\u4EBA\u7684\u957F\u8FDE\u63A5\u5C31\u7EEA")
|
h2("li", null, repairing ? "\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u5361\u7247\u6309\u94AE\u4FEE\u590D\u5B8C\u6210" : grantingGroupMessages ? "\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u6743\u9650\u751F\u6548\u5E76\u81EA\u52A8\u5207\u6362\u54CD\u5E94\u65B9\u5F0F" : "\u4FDD\u6301\u672C\u9875\u6253\u5F00\uFF0C\u7B49\u5F85\u65B0\u673A\u5668\u4EBA\u7684\u957F\u8FDE\u63A5\u5C31\u7EEA")
|
||||||
),
|
),
|
||||||
h2(
|
h2(
|
||||||
"div",
|
"div",
|
||||||
|
|
@ -4386,7 +4485,7 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
||||||
rel: "noopener noreferrer"
|
rel: "noopener noreferrer"
|
||||||
}, h2("span", null, "\u5728\u98DE\u4E66\u4E2D\u6253\u5F00")) : null,
|
}, h2("span", null, "\u5728\u98DE\u4E66\u4E2D\u6253\u5F00")) : null,
|
||||||
!expired ? h2(Button5, { onClick: onRefresh, disabled: busy }, "\u6362\u4E00\u4E2A\u4E8C\u7EF4\u7801") : null,
|
!expired ? h2(Button5, { onClick: onRefresh, disabled: busy }, "\u6362\u4E00\u4E2A\u4E8C\u7EF4\u7801") : null,
|
||||||
h2(Button5, { onClick: onCancel, disabled: busy }, repairing ? "\u53D6\u6D88\u4FEE\u590D" : "\u53D6\u6D88\u6DFB\u52A0")
|
h2(Button5, { onClick: onCancel, disabled: busy }, repairing ? "\u53D6\u6D88\u4FEE\u590D" : grantingGroupMessages ? "\u53D6\u6D88\u6388\u6743" : "\u53D6\u6D88\u6DFB\u52A0")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -4395,6 +4494,7 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
||||||
function ProvisionProgress({ phase, provision, onCancel, busy }) {
|
function ProvisionProgress({ phase, provision, onCancel, busy }) {
|
||||||
const connecting = phase === "connecting";
|
const connecting = phase === "connecting";
|
||||||
const repairing = isCallbackRepair(provision);
|
const repairing = isCallbackRepair(provision);
|
||||||
|
const grantingGroupMessages = isGroupMessagePermission(provision);
|
||||||
return h2(
|
return h2(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
|
|
@ -4402,17 +4502,18 @@ function ProvisionProgress({ phase, provision, onCancel, busy }) {
|
||||||
"aria-busy": "true"
|
"aria-busy": "true"
|
||||||
},
|
},
|
||||||
h2("div", { className: "dim-spinner", "aria-hidden": "true" }),
|
h2("div", { className: "dim-spinner", "aria-hidden": "true" }),
|
||||||
h2("h3", null, connecting ? repairing ? "\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u5B8C\u6210\u5361\u7247\u6309\u94AE\u4FEE\u590D" : "\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u8FDE\u63A5\u65B0\u673A\u5668\u4EBA" : repairing ? "\u6B63\u5728\u51C6\u5907\u4FEE\u590D\u4E8C\u7EF4\u7801" : "\u6B63\u5728\u51C6\u5907\u6388\u6743\u4E8C\u7EF4\u7801"),
|
h2("h3", null, connecting ? repairing ? "\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u5B8C\u6210\u5361\u7247\u6309\u94AE\u4FEE\u590D" : grantingGroupMessages ? "\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u542F\u7528\u5168\u90E8\u6D88\u606F\u6A21\u5F0F" : "\u5DF2\u786E\u8BA4\uFF0C\u6B63\u5728\u8FDE\u63A5\u65B0\u673A\u5668\u4EBA" : repairing ? "\u6B63\u5728\u51C6\u5907\u4FEE\u590D\u4E8C\u7EF4\u7801" : grantingGroupMessages ? "\u6B63\u5728\u51C6\u5907\u6743\u9650\u6388\u6743\u4E8C\u7EF4\u7801" : "\u6B63\u5728\u51C6\u5907\u6388\u6743\u4E8C\u7EF4\u7801"),
|
||||||
h2("p", null, connecting ? repairing ? "\u914D\u7F6E\u5DF2\u63D0\u4EA4\uFF0C\u6B63\u5728\u9A8C\u8BC1\u5361\u7247\u6309\u94AE\u56DE\u8C03\u5E76\u91CD\u8FDE\u6B64\u673A\u5668\u4EBA\uFF1B\u6B64\u9636\u6BB5\u65E0\u6CD5\u53D6\u6D88\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002" : "\u6B63\u5728\u5B89\u5168\u4FDD\u5B58\u51ED\u636E\u5E76\u68C0\u67E5\u65B0\u673A\u5668\u4EBA\u7684\u6D88\u606F\u901A\u9053\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002" : repairing ? "\u6B63\u5728\u4E3A\u73B0\u6709\u98DE\u4E66\u5E94\u7528\u7533\u8BF7\u4E00\u6B21\u6027\u66F4\u65B0\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u5019\u3002" : "\u6B63\u5728\u5411\u98DE\u4E66\u7533\u8BF7\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u5019\u3002"),
|
h2("p", null, connecting ? repairing ? "\u914D\u7F6E\u5DF2\u63D0\u4EA4\uFF0C\u6B63\u5728\u9A8C\u8BC1\u5361\u7247\u6309\u94AE\u56DE\u8C03\u5E76\u91CD\u8FDE\u6B64\u673A\u5668\u4EBA\uFF1B\u6B64\u9636\u6BB5\u65E0\u6CD5\u53D6\u6D88\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002" : grantingGroupMessages ? "\u6743\u9650\u914D\u7F6E\u5DF2\u63D0\u4EA4\uFF0C\u6B63\u5728\u4FDD\u5B58\u8BBE\u7F6E\u5E76\u91CD\u8FDE\u6B64\u673A\u5668\u4EBA\uFF1B\u6B64\u9636\u6BB5\u65E0\u6CD5\u53D6\u6D88\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002" : "\u6B63\u5728\u5B89\u5168\u4FDD\u5B58\u51ED\u636E\u5E76\u68C0\u67E5\u65B0\u673A\u5668\u4EBA\u7684\u6D88\u606F\u901A\u9053\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002" : repairing ? "\u6B63\u5728\u4E3A\u73B0\u6709\u98DE\u4E66\u5E94\u7528\u7533\u8BF7\u4E00\u6B21\u6027\u66F4\u65B0\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u5019\u3002" : grantingGroupMessages ? "\u6B63\u5728\u4E3A\u73B0\u6709\u98DE\u4E66\u5E94\u7528\u7533\u8BF7\u7FA4\u6D88\u606F\u6743\u9650\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u5019\u3002" : "\u6B63\u5728\u5411\u98DE\u4E66\u7533\u8BF7\u4E00\u6B21\u6027\u6388\u6743\u4E8C\u7EF4\u7801\uFF0C\u8BF7\u7A0D\u5019\u3002"),
|
||||||
connecting && onCancel ? h2(
|
connecting && onCancel ? h2(
|
||||||
"div",
|
"div",
|
||||||
{ className: "bxf-actions dim-viewActions", style: { justifyContent: "center" } },
|
{ className: "bxf-actions dim-viewActions", style: { justifyContent: "center" } },
|
||||||
h2(Button5, { onClick: onCancel, disabled: busy }, repairing ? "\u53D6\u6D88\u4FEE\u590D" : "\u53D6\u6D88\u6DFB\u52A0")
|
h2(Button5, { onClick: onCancel, disabled: busy }, repairing ? "\u53D6\u6D88\u4FEE\u590D" : grantingGroupMessages ? "\u53D6\u6D88\u6388\u6743" : "\u53D6\u6D88\u6DFB\u52A0")
|
||||||
) : null
|
) : null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function ProvisionError2({ error, provision, onRetry, onCancel, busy }) {
|
function ProvisionError2({ error, provision, onRetry, onCancel, busy }) {
|
||||||
const repairing = isCallbackRepair(provision);
|
const repairing = isCallbackRepair(provision);
|
||||||
|
const grantingGroupMessages = isGroupMessagePermission(provision);
|
||||||
return h2(
|
return h2(
|
||||||
"div",
|
"div",
|
||||||
{ className: "bxf-card bxf-provisionCard dim-surfaceCard" },
|
{ className: "bxf-card bxf-provisionCard dim-surfaceCard" },
|
||||||
|
|
@ -4422,7 +4523,7 @@ function ProvisionError2({ error, provision, onRetry, onCancel, busy }) {
|
||||||
h2(
|
h2(
|
||||||
"div",
|
"div",
|
||||||
null,
|
null,
|
||||||
h2("h3", null, repairing ? "\u5361\u7247\u6309\u94AE\u6CA1\u6709\u4FEE\u590D\u5B8C\u6210" : "\u65B0\u673A\u5668\u4EBA\u6CA1\u6709\u6DFB\u52A0\u5B8C\u6210"),
|
h2("h3", null, repairing ? "\u5361\u7247\u6309\u94AE\u6CA1\u6709\u4FEE\u590D\u5B8C\u6210" : grantingGroupMessages ? "\u7FA4\u6D88\u606F\u6743\u9650\u6CA1\u6709\u5F00\u901A\u5B8C\u6210" : "\u65B0\u673A\u5668\u4EBA\u6CA1\u6709\u6DFB\u52A0\u5B8C\u6210"),
|
||||||
h2("p", null, error.message),
|
h2("p", null, error.message),
|
||||||
error.code ? h2("span", { className: "bxf-errorCode" }, error.code) : null,
|
error.code ? h2("span", { className: "bxf-errorCode" }, error.code) : null,
|
||||||
h2(
|
h2(
|
||||||
|
|
@ -4504,9 +4605,17 @@ function RemoveConfirmation2({ bot, busy, onConfirm, onCancel }) {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function GroupResponseModeEditor({ value, disabled = false, onSave }) {
|
function GroupResponseModeEditor({
|
||||||
|
value,
|
||||||
|
permissionGranted = false,
|
||||||
|
disabled = false,
|
||||||
|
authorizationDisabled = false,
|
||||||
|
onSave,
|
||||||
|
onAuthorize
|
||||||
|
}) {
|
||||||
const current = normalizeGroupResponseMode(value);
|
const current = normalizeGroupResponseMode(value);
|
||||||
const [saving, setSaving] = React11.useState(false);
|
const [saving, setSaving] = React11.useState(false);
|
||||||
|
const [authorizing, setAuthorizing] = React11.useState(false);
|
||||||
const [error, setError] = React11.useState(null);
|
const [error, setError] = React11.useState(null);
|
||||||
const change = async (event) => {
|
const change = async (event) => {
|
||||||
const next = normalizeGroupResponseMode(event.target.value);
|
const next = normalizeGroupResponseMode(event.target.value);
|
||||||
|
|
@ -4521,6 +4630,18 @@ function GroupResponseModeEditor({ value, disabled = false, onSave }) {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const authorize = async () => {
|
||||||
|
if (current !== "all" || saving || authorizing || disabled || authorizationDisabled) return;
|
||||||
|
setAuthorizing(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await onAuthorize?.();
|
||||||
|
} catch (cause) {
|
||||||
|
setError(cause?.message ?? "\u7FA4\u6D88\u606F\u6743\u9650\u6388\u6743\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\u3002");
|
||||||
|
} finally {
|
||||||
|
setAuthorizing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
return h2(
|
return h2(
|
||||||
"div",
|
"div",
|
||||||
{ className: "bxf-responseMode dim-responseMode" },
|
{ className: "bxf-responseMode dim-responseMode" },
|
||||||
|
|
@ -4528,7 +4649,11 @@ function GroupResponseModeEditor({ value, disabled = false, onSave }) {
|
||||||
"div",
|
"div",
|
||||||
{ className: "bxf-responseModeHeader dim-responseModeHeader" },
|
{ className: "bxf-responseModeHeader dim-responseModeHeader" },
|
||||||
h2("span", null, "\u7FA4\u804A\u54CD\u5E94\u65B9\u5F0F"),
|
h2("span", null, "\u7FA4\u804A\u54CD\u5E94\u65B9\u5F0F"),
|
||||||
saving ? h2("span", { className: "bxf-responseModeStatus dim-responseModeStatus" }, "\u4FDD\u5B58\u4E2D\u2026") : null
|
saving || authorizing ? h2(
|
||||||
|
"span",
|
||||||
|
{ className: "bxf-responseModeStatus dim-responseModeStatus" },
|
||||||
|
saving ? "\u4FDD\u5B58\u4E2D\u2026" : "\u6B63\u5728\u51C6\u5907\u6388\u6743\u2026"
|
||||||
|
) : null
|
||||||
),
|
),
|
||||||
h2(
|
h2(
|
||||||
"select",
|
"select",
|
||||||
|
|
@ -4542,13 +4667,27 @@ function GroupResponseModeEditor({ value, disabled = false, onSave }) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
h2("option", { value: "mention" }, "\u4EC5\u5728 @\u673A\u5668\u4EBA\u65F6\u54CD\u5E94\uFF08\u63A8\u8350\uFF09"),
|
h2("option", { value: "mention" }, "\u4EC5\u5728 @\u673A\u5668\u4EBA\u65F6\u54CD\u5E94\uFF08\u63A8\u8350\uFF09"),
|
||||||
h2("option", { value: "all" }, "\u54CD\u5E94\u6240\u6709\u7FA4\u6D88\u606F\uFF08\u9700\u98DE\u4E66\u654F\u611F\u6743\u9650\uFF09")
|
h2("option", { value: "all" }, "\u54CD\u5E94\u6240\u6709\u7FA4\u6D88\u606F")
|
||||||
),
|
),
|
||||||
h2(
|
h2(
|
||||||
"small",
|
"small",
|
||||||
{ className: "bxf-responseModeHelp dim-responseModeHelp" },
|
{ className: "bxf-responseModeHelp dim-responseModeHelp" },
|
||||||
current === "mention" ? "\u79C1\u804A\u59CB\u7EC8\u54CD\u5E94\uFF1B\u7FA4\u804A\u4EC5\u5904\u7406\u660E\u786E @\u5F53\u524D\u673A\u5668\u4EBA\u7684\u6D88\u606F\u3002" : "\u9700\u5728\u98DE\u4E66\u4E3A\u8BE5\u673A\u5668\u4EBA\u5F00\u901A\u201C\u83B7\u53D6\u7FA4\u7EC4\u4E2D\u6240\u6709\u6D88\u606F\u201D\u6743\u9650\uFF08im:message.group_msg\uFF09\uFF1B\u5F00\u901A\u540E\uFF0C\u673A\u5668\u4EBA\u4F1A\u5904\u7406\u7FA4\u804A\u4E2D\u7684\u6240\u6709\u53EF\u89C1\u6D88\u606F\u3002"
|
current === "mention" ? permissionGranted ? "\u79C1\u804A\u59CB\u7EC8\u54CD\u5E94\uFF1B\u7FA4\u804A\u4EC5\u5904\u7406\u660E\u786E @\u5F53\u524D\u673A\u5668\u4EBA\u7684\u6D88\u606F\u3002\u7FA4\u6D88\u606F\u6743\u9650\u5DF2\u5F00\u901A\uFF0C\u518D\u6B21\u5207\u6362\u65E0\u9700\u6388\u6743\u3002" : "\u79C1\u804A\u59CB\u7EC8\u54CD\u5E94\uFF1B\u7FA4\u804A\u4EC5\u5904\u7406\u660E\u786E @\u5F53\u524D\u673A\u5668\u4EBA\u7684\u6D88\u606F\u3002\u9009\u62E9\u5168\u90E8\u6D88\u606F\u540E\u4F1A\u6253\u5F00\u98DE\u4E66\u5B98\u65B9\u6388\u6743\u6D41\u7A0B\u3002" : permissionGranted ? "\u5DF2\u5F00\u901A\u201C\u83B7\u53D6\u7FA4\u7EC4\u4E2D\u6240\u6709\u6D88\u606F\u201D\u6743\u9650\uFF08im:message.group_msg\uFF09\uFF1B\u673A\u5668\u4EBA\u4F1A\u5904\u7406\u7FA4\u804A\u4E2D\u7684\u6240\u6709\u53EF\u89C1\u6D88\u606F\u3002" : "\u5C1A\u672A\u786E\u8BA4\u201C\u83B7\u53D6\u7FA4\u7EC4\u4E2D\u6240\u6709\u6D88\u606F\u201D\u6743\u9650\uFF0C\u8BF7\u5B8C\u6210\u98DE\u4E66\u6388\u6743\u3002"
|
||||||
),
|
),
|
||||||
|
current === "all" ? h2(
|
||||||
|
"div",
|
||||||
|
{ className: "bxf-responseModePermissionAction dim-responseModePermissionAction" },
|
||||||
|
h2(Button5, {
|
||||||
|
className: "bxf-responseModePermissionButton",
|
||||||
|
size: "small",
|
||||||
|
disabled: disabled || authorizationDisabled || saving || authorizing,
|
||||||
|
"aria-busy": authorizing ? "true" : void 0,
|
||||||
|
"aria-label": permissionGranted ? "\u91CD\u65B0\u6388\u6743\u7FA4\u6D88\u606F\u6743\u9650" : "\u6388\u6743\u7FA4\u6D88\u606F\u6743\u9650",
|
||||||
|
onClick: () => {
|
||||||
|
void authorize();
|
||||||
|
}
|
||||||
|
}, authorizing ? "\u6B63\u5728\u51C6\u5907\u2026" : permissionGranted ? "\u91CD\u65B0\u6388\u6743" : "\u53BB\u6388\u6743")
|
||||||
|
) : null,
|
||||||
error ? h2("p", {
|
error ? h2("p", {
|
||||||
className: "bxf-responseModeError dim-responseModeError",
|
className: "bxf-responseModeError dim-responseModeError",
|
||||||
role: "alert"
|
role: "alert"
|
||||||
|
|
@ -4559,6 +4698,8 @@ function BotCard({
|
||||||
connection,
|
connection,
|
||||||
busy,
|
busy,
|
||||||
repairDisabled,
|
repairDisabled,
|
||||||
|
provisionContent,
|
||||||
|
provisionRef,
|
||||||
actionError,
|
actionError,
|
||||||
testNotice,
|
testNotice,
|
||||||
removing,
|
removing,
|
||||||
|
|
@ -4567,6 +4708,7 @@ function BotCard({
|
||||||
onWorkspaceSave,
|
onWorkspaceSave,
|
||||||
onAgentPresetSave,
|
onAgentPresetSave,
|
||||||
onGroupResponseModeSave,
|
onGroupResponseModeSave,
|
||||||
|
onGroupMessagePermissionAuthorize,
|
||||||
onRequestRemove,
|
onRequestRemove,
|
||||||
onConfirmRemove,
|
onConfirmRemove,
|
||||||
onCancelRemove,
|
onCancelRemove,
|
||||||
|
|
@ -4643,9 +4785,19 @@ function BotCard({
|
||||||
}),
|
}),
|
||||||
h2(GroupResponseModeEditor, {
|
h2(GroupResponseModeEditor, {
|
||||||
value: connection.groupResponseMode,
|
value: connection.groupResponseMode,
|
||||||
|
permissionGranted: connection.groupMessagePermissionGranted,
|
||||||
disabled: Boolean(busy),
|
disabled: Boolean(busy),
|
||||||
onSave: onGroupResponseModeSave
|
authorizationDisabled: repairDisabled,
|
||||||
|
onSave: onGroupResponseModeSave,
|
||||||
|
onAuthorize: onGroupMessagePermissionAuthorize
|
||||||
}),
|
}),
|
||||||
|
provisionContent ? h2("section", {
|
||||||
|
className: "bxf-botProvision dim-botProvision",
|
||||||
|
"aria-label": `${bot.name}\u7684\u98DE\u4E66\u6388\u6743\u6D41\u7A0B`,
|
||||||
|
"data-provision-for": connection.botId,
|
||||||
|
ref: provisionRef,
|
||||||
|
tabIndex: -1
|
||||||
|
}, provisionContent) : null,
|
||||||
h2(
|
h2(
|
||||||
"div",
|
"div",
|
||||||
{ className: "bxf-connectedFooter dim-cardFooter" },
|
{ className: "bxf-connectedFooter dim-cardFooter" },
|
||||||
|
|
@ -4711,8 +4863,10 @@ function BotList(props) {
|
||||||
{ key: bot.botId },
|
{ key: bot.botId },
|
||||||
h2(BotCard, {
|
h2(BotCard, {
|
||||||
connection: bot,
|
connection: bot,
|
||||||
busy: props.busyByBot[bot.botId] ?? (isCallbackRepair(props.provisioning) && props.provisioning.botId === bot.botId ? "callback-repair" : void 0),
|
busy: props.busyByBot[bot.botId] ?? (isTargetedAppUpdate2(props.provisioning) && props.provisioning.botId === bot.botId ? props.provisioning.operation : void 0),
|
||||||
repairDisabled: Boolean(props.provisioning),
|
repairDisabled: Boolean(props.provisioning),
|
||||||
|
provisionContent: isTargetedAppUpdate2(props.provisioning) && props.provisioning.botId === bot.botId ? props.provisionContent : null,
|
||||||
|
provisionRef: props.provisionRef,
|
||||||
actionError: props.errorsByBot[bot.botId],
|
actionError: props.errorsByBot[bot.botId],
|
||||||
testNotice: props.testNoticesByBot[bot.botId],
|
testNotice: props.testNoticesByBot[bot.botId],
|
||||||
removing: props.removeTargetId === bot.botId,
|
removing: props.removeTargetId === bot.botId,
|
||||||
|
|
@ -4721,6 +4875,7 @@ function BotList(props) {
|
||||||
onWorkspaceSave: (workspace) => props.onWorkspaceSave(bot, workspace),
|
onWorkspaceSave: (workspace) => props.onWorkspaceSave(bot, workspace),
|
||||||
onAgentPresetSave: (agentPreset) => props.onAgentPresetSave(bot, agentPreset),
|
onAgentPresetSave: (agentPreset) => props.onAgentPresetSave(bot, agentPreset),
|
||||||
onGroupResponseModeSave: (groupResponseMode) => props.onGroupResponseModeSave(bot, groupResponseMode),
|
onGroupResponseModeSave: (groupResponseMode) => props.onGroupResponseModeSave(bot, groupResponseMode),
|
||||||
|
onGroupMessagePermissionAuthorize: () => props.onGroupMessagePermissionAuthorize(bot),
|
||||||
onRequestRemove: () => props.onRequestRemove(bot),
|
onRequestRemove: () => props.onRequestRemove(bot),
|
||||||
onConfirmRemove: () => props.onConfirmRemove(bot),
|
onConfirmRemove: () => props.onConfirmRemove(bot),
|
||||||
onCancelRemove: props.onCancelRemove,
|
onCancelRemove: props.onCancelRemove,
|
||||||
|
|
@ -4807,6 +4962,7 @@ function FeishuSettingsTab({ rpcCall }) {
|
||||||
const [focusBotId, setFocusBotId] = React11.useState(null);
|
const [focusBotId, setFocusBotId] = React11.useState(null);
|
||||||
const cardRefs = React11.useRef(/* @__PURE__ */ new Map());
|
const cardRefs = React11.useRef(/* @__PURE__ */ new Map());
|
||||||
const removeButtonRefs = React11.useRef(/* @__PURE__ */ new Map());
|
const removeButtonRefs = React11.useRef(/* @__PURE__ */ new Map());
|
||||||
|
const targetedProvisionRef = React11.useRef(null);
|
||||||
const addButtonRef = React11.useRef(null);
|
const addButtonRef = React11.useRef(null);
|
||||||
const mountedRef = React11.useRef(true);
|
const mountedRef = React11.useRef(true);
|
||||||
const workspaceFence = useWorkspaceSnapshotFence();
|
const workspaceFence = useWorkspaceSnapshotFence();
|
||||||
|
|
@ -4884,15 +5040,27 @@ function FeishuSettingsTab({ rpcCall }) {
|
||||||
node.focus({ preventScroll: true });
|
node.focus({ preventScroll: true });
|
||||||
setFocusBotId(null);
|
setFocusBotId(null);
|
||||||
}, [focusBotId, model.bots]);
|
}, [focusBotId, model.bots]);
|
||||||
|
const targetedProvisionFocusKey = isTargetedAppUpdate2(model.provisioning) ? `${model.provisioning.botId}:${model.provisioning.attemptId ?? "preparing"}:${model.provisioning.phase}` : null;
|
||||||
|
React11.useEffect(() => {
|
||||||
|
if (!targetedProvisionFocusKey) return;
|
||||||
|
scheduleAnimationFrame(() => {
|
||||||
|
const node = targetedProvisionRef.current;
|
||||||
|
if (!node) return;
|
||||||
|
node.scrollIntoView?.({ block: "nearest", behavior: "smooth" });
|
||||||
|
node.focus?.({ preventScroll: true });
|
||||||
|
}, "targeted-provision-focus");
|
||||||
|
}, [scheduleAnimationFrame, targetedProvisionFocusKey]);
|
||||||
const startProvisioning = React11.useCallback(async ({
|
const startProvisioning = React11.useCallback(async ({
|
||||||
replace = false,
|
replace = false,
|
||||||
operation = FEISHU_REGISTRATION_OPERATIONS.PROVISION,
|
operation = FEISHU_REGISTRATION_OPERATIONS.PROVISION,
|
||||||
bot
|
bot
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const repairing = operation === CALLBACK_REPAIR_OPERATION;
|
const repairing = operation === CALLBACK_REPAIR_OPERATION;
|
||||||
const botId = repairing ? bot?.botId ?? model.provisioning?.botId : void 0;
|
const grantingGroupMessages = operation === GROUP_MESSAGE_PERMISSION_OPERATION;
|
||||||
const botName = repairing ? bot?.bot?.name ?? model.provisioning?.botName : void 0;
|
const targetedUpdate = repairing || grantingGroupMessages;
|
||||||
if (repairing && !botId) return;
|
const botId = targetedUpdate ? bot?.botId ?? model.provisioning?.botId : void 0;
|
||||||
|
const botName = targetedUpdate ? bot?.bot?.name ?? model.provisioning?.botName : void 0;
|
||||||
|
if (targetedUpdate && !botId) return;
|
||||||
setCredentialOpen(false);
|
setCredentialOpen(false);
|
||||||
setCredentialError(null);
|
setCredentialError(null);
|
||||||
setProvisionBusy(true);
|
setProvisionBusy(true);
|
||||||
|
|
@ -4915,12 +5083,13 @@ function FeishuSettingsTab({ rpcCall }) {
|
||||||
} catch {
|
} catch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const endpoint = repairing ? FEISHU_ENDPOINTS.beginCallbackRepair : grantingGroupMessages ? FEISHU_ENDPOINTS.beginGroupMessagePermission : FEISHU_ENDPOINTS.beginProvisioning;
|
||||||
const provision2 = normalizeProvisioning2(await invoke(
|
const provision2 = normalizeProvisioning2(await invoke(
|
||||||
repairing ? FEISHU_ENDPOINTS.beginCallbackRepair : FEISHU_ENDPOINTS.beginProvisioning,
|
endpoint,
|
||||||
repairing ? { botId } : { locale: "zh-CN" }
|
targetedUpdate ? { botId } : { locale: "zh-CN" }
|
||||||
));
|
));
|
||||||
if (repairing && (provision2.operation !== CALLBACK_REPAIR_OPERATION || provision2.botId !== botId)) {
|
if (targetedUpdate && (provision2.operation !== operation || provision2.botId !== botId)) {
|
||||||
throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u5339\u914D\u7684\u5361\u7247\u4FEE\u590D\u4E8C\u7EF4\u7801");
|
throw new Error(grantingGroupMessages ? "\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u5339\u914D\u7684\u7FA4\u6D88\u606F\u6743\u9650\u4E8C\u7EF4\u7801" : "\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u5339\u914D\u7684\u5361\u7247\u4FEE\u590D\u4E8C\u7EF4\u7801");
|
||||||
}
|
}
|
||||||
const timestamp7 = Date.now();
|
const timestamp7 = Date.now();
|
||||||
setNow(timestamp7);
|
setNow(timestamp7);
|
||||||
|
|
@ -4934,7 +5103,7 @@ function FeishuSettingsTab({ rpcCall }) {
|
||||||
expired: false
|
expired: false
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
announce(repairing ? `${botName ?? "\u673A\u5668\u4EBA"}\u7684\u4FEE\u590D\u4E8C\u7EF4\u7801\u5DF2\u751F\u6210\uFF0C\u8BF7\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u3002` : "\u6388\u6743\u4E8C\u7EF4\u7801\u5DF2\u751F\u6210\uFF0C\u8BF7\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u3002");
|
announce(repairing ? `${botName ?? "\u673A\u5668\u4EBA"}\u7684\u4FEE\u590D\u4E8C\u7EF4\u7801\u5DF2\u751F\u6210\uFF0C\u8BF7\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u3002` : grantingGroupMessages ? `${botName ?? "\u673A\u5668\u4EBA"}\u7684\u7FA4\u6D88\u606F\u6743\u9650\u4E8C\u7EF4\u7801\u5DF2\u751F\u6210\uFF0C\u8BF7\u4F7F\u7528\u98DE\u4E66\u786E\u8BA4\u3002` : "\u6388\u6743\u4E8C\u7EF4\u7801\u5DF2\u751F\u6210\uFF0C\u8BF7\u4F7F\u7528\u98DE\u4E66\u626B\u7801\u3002");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setModel((current) => ({
|
setModel((current) => ({
|
||||||
...current,
|
...current,
|
||||||
|
|
@ -4983,12 +5152,14 @@ function FeishuSettingsTab({ rpcCall }) {
|
||||||
const activeProvision = model.provisioning;
|
const activeProvision = model.provisioning;
|
||||||
const attemptId = activeProvision?.attemptId;
|
const attemptId = activeProvision?.attemptId;
|
||||||
const repairing = isCallbackRepair(activeProvision);
|
const repairing = isCallbackRepair(activeProvision);
|
||||||
const targetBot = repairing ? model.bots.find((bot) => bot.botId === activeProvision?.botId) : void 0;
|
const grantingGroupMessages = isGroupMessagePermission(activeProvision);
|
||||||
|
const targetedUpdate = isTargetedAppUpdate2(activeProvision);
|
||||||
|
const targetBot = targetedUpdate ? model.bots.find((bot) => bot.botId === activeProvision?.botId) : void 0;
|
||||||
setProvisionBusy(true);
|
setProvisionBusy(true);
|
||||||
try {
|
try {
|
||||||
const result = attemptId ? normalizePollResult(await invoke(FEISHU_ENDPOINTS.cancelProvisioning, { attemptId })) : null;
|
const result = attemptId ? normalizePollResult(await invoke(FEISHU_ENDPOINTS.cancelProvisioning, { attemptId })) : null;
|
||||||
if (repairing && result) {
|
if (targetedUpdate && result) {
|
||||||
if (result.operation !== CALLBACK_REPAIR_OPERATION || result.botId !== activeProvision.botId) {
|
if (result.operation !== activeProvision.operation || result.botId !== activeProvision.botId) {
|
||||||
throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u5339\u914D\u7684\u6CE8\u518C\u8FDB\u5EA6");
|
throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u5339\u914D\u7684\u6CE8\u518C\u8FDB\u5EA6");
|
||||||
}
|
}
|
||||||
if (result.status === "connecting") {
|
if (result.status === "connecting") {
|
||||||
|
|
@ -5002,23 +5173,23 @@ function FeishuSettingsTab({ rpcCall }) {
|
||||||
expired: false
|
expired: false
|
||||||
}
|
}
|
||||||
} : current);
|
} : current);
|
||||||
announce("\u914D\u7F6E\u5DF2\u63D0\u4EA4\uFF0C\u6B63\u5728\u9A8C\u8BC1\u5361\u7247\u6309\u94AE\u56DE\u8C03\u5E76\u91CD\u8FDE\u6B64\u673A\u5668\u4EBA\uFF1B\u6B64\u9636\u6BB5\u65E0\u6CD5\u53D6\u6D88\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002");
|
announce(grantingGroupMessages ? "\u6743\u9650\u914D\u7F6E\u5DF2\u63D0\u4EA4\uFF0C\u6B63\u5728\u542F\u7528\u5168\u90E8\u6D88\u606F\u6A21\u5F0F\u5E76\u91CD\u8FDE\u6B64\u673A\u5668\u4EBA\uFF1B\u6B64\u9636\u6BB5\u65E0\u6CD5\u53D6\u6D88\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002" : "\u914D\u7F6E\u5DF2\u63D0\u4EA4\uFF0C\u6B63\u5728\u9A8C\u8BC1\u5361\u7247\u6309\u94AE\u56DE\u8C03\u5E76\u91CD\u8FDE\u6B64\u673A\u5668\u4EBA\uFF1B\u6B64\u9636\u6BB5\u65E0\u6CD5\u53D6\u6D88\uFF0C\u5176\u4ED6\u673A\u5668\u4EBA\u4E0D\u4F1A\u4E2D\u65AD\u3002");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.status === "connected") {
|
if (result.status === "connected") {
|
||||||
const targetBotName = targetBot?.bot.name ?? activeProvision.botName ?? "\u673A\u5668\u4EBA";
|
const targetBotName = targetBot?.bot.name ?? activeProvision.botName ?? "\u673A\u5668\u4EBA";
|
||||||
setModel((current) => ({ ...current, provisioning: null }));
|
setModel((current) => ({ ...current, provisioning: null }));
|
||||||
announce(`${targetBotName}\u7684\u5361\u7247\u6309\u94AE\u5DF2\u4FEE\u590D\u3002`);
|
announce(grantingGroupMessages ? `${targetBotName}\u5DF2\u5F00\u901A\u7FA4\u6D88\u606F\u6743\u9650\uFF0C\u5E76\u542F\u7528\u201C\u54CD\u5E94\u6240\u6709\u7FA4\u6D88\u606F\u201D\u3002` : `${targetBotName}\u7684\u5361\u7247\u6309\u94AE\u5DF2\u4FEE\u590D\u3002`);
|
||||||
if (activeProvision.botId) setFocusBotId(activeProvision.botId);
|
if (activeProvision.botId) setFocusBotId(activeProvision.botId);
|
||||||
await loadStatus({ silent: true, restoreProvisioning: false });
|
await loadStatus({ silent: true, restoreProvisioning: false });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setModel((current) => ({ ...current, provisioning: null }));
|
setModel((current) => ({ ...current, provisioning: null }));
|
||||||
announce(repairing ? "\u5DF2\u53D6\u6D88\u5361\u7247\u6309\u94AE\u4FEE\u590D\u3002" : "\u5DF2\u53D6\u6D88\u6DFB\u52A0\u673A\u5668\u4EBA\u3002");
|
announce(repairing ? "\u5DF2\u53D6\u6D88\u5361\u7247\u6309\u94AE\u4FEE\u590D\u3002" : grantingGroupMessages ? "\u5DF2\u53D6\u6D88\u7FA4\u6D88\u606F\u6743\u9650\u6388\u6743\u3002" : "\u5DF2\u53D6\u6D88\u6DFB\u52A0\u673A\u5668\u4EBA\u3002");
|
||||||
await loadStatus({ silent: true, restoreProvisioning: false });
|
await loadStatus({ silent: true, restoreProvisioning: false });
|
||||||
scheduleAnimationFrame(() => {
|
scheduleAnimationFrame(() => {
|
||||||
if (repairing && activeProvision.botId) {
|
if (targetedUpdate && activeProvision.botId) {
|
||||||
cardRefs.current.get(activeProvision.botId)?.focus();
|
cardRefs.current.get(activeProvision.botId)?.focus();
|
||||||
} else {
|
} else {
|
||||||
addButtonRef.current?.focus();
|
addButtonRef.current?.focus();
|
||||||
|
|
@ -5066,26 +5237,26 @@ function FeishuSettingsTab({ rpcCall }) {
|
||||||
{ attemptId: provision2.attemptId },
|
{ attemptId: provision2.attemptId },
|
||||||
controller.signal
|
controller.signal
|
||||||
));
|
));
|
||||||
if (result.operation !== provision2.operation || isCallbackRepair(provision2) && result.botId !== provision2.botId) {
|
if (result.operation !== provision2.operation || isTargetedAppUpdate2(provision2) && result.botId !== provision2.botId) {
|
||||||
throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u5339\u914D\u7684\u6CE8\u518C\u8FDB\u5EA6");
|
throw new Error("\u98DE\u4E66\u670D\u52A1\u8FD4\u56DE\u4E86\u4E0D\u5339\u914D\u7684\u6CE8\u518C\u8FDB\u5EA6");
|
||||||
}
|
}
|
||||||
if (result.status === "connected") {
|
if (result.status === "connected") {
|
||||||
const snapshot = await loadStatus({ signal: controller.signal, silent: true, restoreProvisioning: false });
|
const snapshot = await loadStatus({ signal: controller.signal, silent: true, restoreProvisioning: false });
|
||||||
const targetBot = snapshot?.bots.find((bot) => bot.botId === result.botId);
|
const targetBot = snapshot?.bots.find((bot) => bot.botId === result.botId);
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
throw new Error(isCallbackRepair(provision2) ? "\u5361\u7247\u6309\u94AE\u5DF2\u66F4\u65B0\uFF0C\u4F46\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u673A\u5668\u4EBA\u8FDE\u63A5\u72B6\u6001" : "\u673A\u5668\u4EBA\u5DF2\u7ECF\u521B\u5EFA\uFF0C\u4F46\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u8FDE\u63A5\u72B6\u6001");
|
throw new Error(isCallbackRepair(provision2) ? "\u5361\u7247\u6309\u94AE\u5DF2\u66F4\u65B0\uFF0C\u4F46\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u673A\u5668\u4EBA\u8FDE\u63A5\u72B6\u6001" : isGroupMessagePermission(provision2) ? "\u7FA4\u6D88\u606F\u6743\u9650\u5DF2\u66F4\u65B0\uFF0C\u4F46\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u673A\u5668\u4EBA\u8FDE\u63A5\u72B6\u6001" : "\u673A\u5668\u4EBA\u5DF2\u7ECF\u521B\u5EFA\uFF0C\u4F46\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u8FDE\u63A5\u72B6\u6001");
|
||||||
}
|
}
|
||||||
if (!targetBot?.connected) {
|
if (!targetBot?.connected) {
|
||||||
setModel((current) => current.provisioning?.attemptId === provision2.attemptId ? { ...current, provisioning: { ...current.provisioning, phase: "connecting" } } : current);
|
setModel((current) => current.provisioning?.attemptId === provision2.attemptId ? { ...current, provisioning: { ...current.provisioning, phase: "connecting" } } : current);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setModel((current) => ({ ...current, provisioning: null }));
|
setModel((current) => ({ ...current, provisioning: null }));
|
||||||
announce(isCallbackRepair(provision2) ? `${targetBot.bot.name}\u7684\u5361\u7247\u6309\u94AE\u5DF2\u4FEE\u590D\u3002` : targetBot ? `${targetBot.bot.name}\u5DF2\u8FDE\u63A5\uFF0C\u53EF\u4EE5\u5728\u98DE\u4E66\u4E2D\u5F00\u59CB\u804A\u5929\u3002` : "\u65B0\u98DE\u4E66\u673A\u5668\u4EBA\u5DF2\u8FDE\u63A5\uFF0C\u53EF\u4EE5\u5F00\u59CB\u804A\u5929\u3002");
|
announce(isCallbackRepair(provision2) ? `${targetBot.bot.name}\u7684\u5361\u7247\u6309\u94AE\u5DF2\u4FEE\u590D\u3002` : isGroupMessagePermission(provision2) ? `${targetBot.bot.name}\u5DF2\u5F00\u901A\u7FA4\u6D88\u606F\u6743\u9650\uFF0C\u5E76\u542F\u7528\u201C\u54CD\u5E94\u6240\u6709\u7FA4\u6D88\u606F\u201D\u3002` : targetBot ? `${targetBot.bot.name}\u5DF2\u8FDE\u63A5\uFF0C\u53EF\u4EE5\u5728\u98DE\u4E66\u4E2D\u5F00\u59CB\u804A\u5929\u3002` : "\u65B0\u98DE\u4E66\u673A\u5668\u4EBA\u5DF2\u8FDE\u63A5\uFF0C\u53EF\u4EE5\u5F00\u59CB\u804A\u5929\u3002");
|
||||||
if (result.botId) setFocusBotId(result.botId);
|
if (result.botId) setFocusBotId(result.botId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.status === "failed") {
|
if (result.status === "failed") {
|
||||||
const error = new Error(result.message ?? (isCallbackRepair(provision2) ? "\u98DE\u4E66\u5361\u7247\u6309\u94AE\u4FEE\u590D\u5931\u8D25" : "\u98DE\u4E66\u5E94\u7528\u521B\u5EFA\u5931\u8D25"));
|
const error = new Error(result.message ?? (isCallbackRepair(provision2) ? "\u98DE\u4E66\u5361\u7247\u6309\u94AE\u4FEE\u590D\u5931\u8D25" : isGroupMessagePermission(provision2) ? "\u98DE\u4E66\u7FA4\u6D88\u606F\u6743\u9650\u5F00\u901A\u5931\u8D25" : "\u98DE\u4E66\u5E94\u7528\u521B\u5EFA\u5931\u8D25"));
|
||||||
error.code = "FEISHU_PROVISION_FAILED";
|
error.code = "FEISHU_PROVISION_FAILED";
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -5231,8 +5402,29 @@ function FeishuSettingsTab({ rpcCall }) {
|
||||||
if (mountedRef.current) setBotBusy(botId, null);
|
if (mountedRef.current) setBotBusy(botId, null);
|
||||||
}
|
}
|
||||||
}, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
|
}, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
|
||||||
|
const authorizeGroupMessages = React11.useCallback(async (connection) => {
|
||||||
|
const { botId } = connection;
|
||||||
|
if (model.provisioning) {
|
||||||
|
throw new Error("\u8BF7\u5148\u5B8C\u6210\u5F53\u524D\u98DE\u4E66\u6388\u6743\u64CD\u4F5C\uFF0C\u518D\u5F00\u901A\u7FA4\u6D88\u606F\u6743\u9650\u3002");
|
||||||
|
}
|
||||||
|
setRemoveTargetId(null);
|
||||||
|
setBotError(botId, null);
|
||||||
|
setTestNoticesByBot((current) => {
|
||||||
|
const next = { ...current };
|
||||||
|
delete next[botId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
await startProvisioning({
|
||||||
|
operation: GROUP_MESSAGE_PERMISSION_OPERATION,
|
||||||
|
bot: connection
|
||||||
|
});
|
||||||
|
}, [model.provisioning, setBotError, startProvisioning]);
|
||||||
const saveGroupResponseMode = React11.useCallback(async (connection, groupResponseMode) => {
|
const saveGroupResponseMode = React11.useCallback(async (connection, groupResponseMode) => {
|
||||||
const { botId } = connection;
|
const { botId } = connection;
|
||||||
|
if (groupResponseMode === "all" && connection.groupMessagePermissionGranted !== true) {
|
||||||
|
await authorizeGroupMessages(connection);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const snapshotVersion = workspaceFence.beginMutation();
|
const snapshotVersion = workspaceFence.beginMutation();
|
||||||
setBotBusy(botId, "group-response-mode");
|
setBotBusy(botId, "group-response-mode");
|
||||||
setBotError(botId, null);
|
setBotError(botId, null);
|
||||||
|
|
@ -5249,7 +5441,15 @@ function FeishuSettingsTab({ rpcCall }) {
|
||||||
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
||||||
if (mountedRef.current) setBotBusy(botId, null);
|
if (mountedRef.current) setBotBusy(botId, null);
|
||||||
}
|
}
|
||||||
}, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
|
}, [
|
||||||
|
invoke,
|
||||||
|
authorizeGroupMessages,
|
||||||
|
loadStatus,
|
||||||
|
mergeSnapshot,
|
||||||
|
setBotBusy,
|
||||||
|
setBotError,
|
||||||
|
workspaceFence
|
||||||
|
]);
|
||||||
const requestRemove = React11.useCallback((connection) => {
|
const requestRemove = React11.useCallback((connection) => {
|
||||||
setRemoveTargetId(connection.botId);
|
setRemoveTargetId(connection.botId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -5284,6 +5484,7 @@ function FeishuSettingsTab({ rpcCall }) {
|
||||||
}
|
}
|
||||||
}, [announce, invoke, loadStatus, mergeSnapshot, scheduleAnimationFrame, setBotBusy, setBotError, workspaceFence]);
|
}, [announce, invoke, loadStatus, mergeSnapshot, scheduleAnimationFrame, setBotBusy, setBotError, workspaceFence]);
|
||||||
const provision = model.provisioning;
|
const provision = model.provisioning;
|
||||||
|
const targetedProvisioning = isTargetedAppUpdate2(provision);
|
||||||
const provisionBot = provision?.botId ? model.bots.find((bot) => bot.botId === provision.botId) ?? { botId: provision.botId, bot: { name: provision.botName ?? "\u6B64\u673A\u5668\u4EBA" } } : void 0;
|
const provisionBot = provision?.botId ? model.bots.find((bot) => bot.botId === provision.botId) ?? { botId: provision.botId, bot: { name: provision.botName ?? "\u6B64\u673A\u5668\u4EBA" } } : void 0;
|
||||||
const restartProvisioning = ({ replace = false } = {}) => startProvisioning({
|
const restartProvisioning = ({ replace = false } = {}) => startProvisioning({
|
||||||
replace,
|
replace,
|
||||||
|
|
@ -5309,7 +5510,7 @@ function FeishuSettingsTab({ rpcCall }) {
|
||||||
provisionContent = h2(ProvisionProgress, {
|
provisionContent = h2(ProvisionProgress, {
|
||||||
phase: "connecting",
|
phase: "connecting",
|
||||||
provision,
|
provision,
|
||||||
onCancel: isCallbackRepair(provision) ? void 0 : () => void cancelProvisioning(),
|
onCancel: isTargetedAppUpdate2(provision) ? void 0 : () => void cancelProvisioning(),
|
||||||
busy: provisionBusy
|
busy: provisionBusy
|
||||||
});
|
});
|
||||||
} else if (provision?.phase === "error") {
|
} else if (provision?.phase === "error") {
|
||||||
|
|
@ -5389,7 +5590,7 @@ function FeishuSettingsTab({ rpcCall }) {
|
||||||
React11.Fragment,
|
React11.Fragment,
|
||||||
null,
|
null,
|
||||||
credentialContent,
|
credentialContent,
|
||||||
provisionContent,
|
targetedProvisioning ? null : provisionContent,
|
||||||
model.bots.length === 0 && !provision && !credentialOpen ? h2(EmptyView2, { onStart: () => void startProvisioning(), busy: provisionBusy }) : null,
|
model.bots.length === 0 && !provision && !credentialOpen ? h2(EmptyView2, { onStart: () => void startProvisioning(), busy: provisionBusy }) : null,
|
||||||
model.bots.length > 0 ? h2(BotList, {
|
model.bots.length > 0 ? h2(BotList, {
|
||||||
bots: model.bots,
|
bots: model.bots,
|
||||||
|
|
@ -5398,11 +5599,14 @@ function FeishuSettingsTab({ rpcCall }) {
|
||||||
testNoticesByBot,
|
testNoticesByBot,
|
||||||
removeTargetId,
|
removeTargetId,
|
||||||
provisioning: provision,
|
provisioning: provision,
|
||||||
|
provisionContent,
|
||||||
|
provisionRef: targetedProvisionRef,
|
||||||
onReconnect: (bot) => void reconnectOneBot(bot),
|
onReconnect: (bot) => void reconnectOneBot(bot),
|
||||||
onRepairCallback: repairCallback,
|
onRepairCallback: repairCallback,
|
||||||
onWorkspaceSave: saveWorkspace,
|
onWorkspaceSave: saveWorkspace,
|
||||||
onAgentPresetSave: saveAgentPreset,
|
onAgentPresetSave: saveAgentPreset,
|
||||||
onGroupResponseModeSave: saveGroupResponseMode,
|
onGroupResponseModeSave: saveGroupResponseMode,
|
||||||
|
onGroupMessagePermissionAuthorize: authorizeGroupMessages,
|
||||||
onRequestRemove: requestRemove,
|
onRequestRemove: requestRemove,
|
||||||
onConfirmRemove: (bot) => void confirmRemove(bot),
|
onConfirmRemove: (bot) => void confirmRemove(bot),
|
||||||
onCancelRemove: cancelRemove,
|
onCancelRemove: cancelRemove,
|
||||||
|
|
|
||||||
282
lib/index.js
282
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -14,6 +14,7 @@ export const FEISHU_ENDPOINTS = Object.freeze({
|
||||||
status: "connection.status",
|
status: "connection.status",
|
||||||
beginProvisioning: "provision.begin",
|
beginProvisioning: "provision.begin",
|
||||||
beginCallbackRepair: "bot.callback-repair.begin",
|
beginCallbackRepair: "bot.callback-repair.begin",
|
||||||
|
beginGroupMessagePermission: "bot.group-message-permission.begin",
|
||||||
pollProvisioning: "provision.poll",
|
pollProvisioning: "provision.poll",
|
||||||
cancelProvisioning: "provision.cancel",
|
cancelProvisioning: "provision.cancel",
|
||||||
bindCredentials: "bot.bind-credentials",
|
bindCredentials: "bot.bind-credentials",
|
||||||
|
|
@ -31,6 +32,7 @@ export const FEISHU_ENDPOINTS = Object.freeze({
|
||||||
export const FEISHU_REGISTRATION_OPERATIONS = Object.freeze({
|
export const FEISHU_REGISTRATION_OPERATIONS = Object.freeze({
|
||||||
PROVISION: "provision",
|
PROVISION: "provision",
|
||||||
CALLBACK_REPAIR: "callback_repair",
|
CALLBACK_REPAIR: "callback_repair",
|
||||||
|
GROUP_MESSAGE_PERMISSION: "group_message_permission",
|
||||||
});
|
});
|
||||||
|
|
||||||
const CONNECTION_STATES = new Set([
|
const CONNECTION_STATES = new Set([
|
||||||
|
|
@ -82,9 +84,18 @@ function clamp(value, min, max, fallback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeRegistrationOperation(value) {
|
function normalizeRegistrationOperation(value) {
|
||||||
return value === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR
|
if (value === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR) {
|
||||||
? FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR
|
return FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR;
|
||||||
: FEISHU_REGISTRATION_OPERATIONS.PROVISION;
|
}
|
||||||
|
if (value === FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION) {
|
||||||
|
return FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION;
|
||||||
|
}
|
||||||
|
return FEISHU_REGISTRATION_OPERATIONS.PROVISION;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTargetedAppUpdate(operation) {
|
||||||
|
return operation === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR
|
||||||
|
|| operation === FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unwrapRpcResult(result) {
|
export function unwrapRpcResult(result) {
|
||||||
|
|
@ -117,8 +128,8 @@ export function normalizeProvisioning(value, now = Date.now()) {
|
||||||
const expireIn = clamp(source.expireIn, 1, 60 * 60, 5 * 60);
|
const expireIn = clamp(source.expireIn, 1, 60 * 60, 5 * 60);
|
||||||
const operation = normalizeRegistrationOperation(source.operation);
|
const operation = normalizeRegistrationOperation(source.operation);
|
||||||
const botId = optionalString(source.botId);
|
const botId = optionalString(source.botId);
|
||||||
if (operation === FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR && !botId) {
|
if (isTargetedAppUpdate(operation) && !botId) {
|
||||||
throw new Error("飞书服务返回的修复信息缺少 botId");
|
throw new Error("飞书服务返回的应用更新信息缺少 botId");
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
attemptId,
|
attemptId,
|
||||||
|
|
@ -192,6 +203,7 @@ export function normalizeBotConnection(value, fallbackBotId) {
|
||||||
workspace: optionalString(value.workspace)?.slice(0, 4_096) ?? "",
|
workspace: optionalString(value.workspace)?.slice(0, 4_096) ?? "",
|
||||||
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
agentPreset: normalizeAgentPresetId(value.agentPreset),
|
||||||
groupResponseMode: normalizeGroupResponseMode(value.groupResponseMode),
|
groupResponseMode: normalizeGroupResponseMode(value.groupResponseMode),
|
||||||
|
groupMessagePermissionGranted: value.groupMessagePermissionGranted === true,
|
||||||
bot: normalizeBot(value.bot),
|
bot: normalizeBot(value.bot),
|
||||||
health: normalizeHealth(value.health, connected),
|
health: normalizeHealth(value.health, connected),
|
||||||
error: normalizeError(value.error),
|
error: normalizeError(value.error),
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,20 @@ export const name = "feishu-settings";
|
||||||
export const inject = ["slots", "connection"];
|
export const inject = ["slots", "connection"];
|
||||||
|
|
||||||
const CALLBACK_REPAIR_OPERATION = FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR;
|
const CALLBACK_REPAIR_OPERATION = FEISHU_REGISTRATION_OPERATIONS.CALLBACK_REPAIR;
|
||||||
|
const GROUP_MESSAGE_PERMISSION_OPERATION = FEISHU_REGISTRATION_OPERATIONS.GROUP_MESSAGE_PERMISSION;
|
||||||
|
|
||||||
function isCallbackRepair(value) {
|
function isCallbackRepair(value) {
|
||||||
return value?.operation === CALLBACK_REPAIR_OPERATION;
|
return value?.operation === CALLBACK_REPAIR_OPERATION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isGroupMessagePermission(value) {
|
||||||
|
return value?.operation === GROUP_MESSAGE_PERMISSION_OPERATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTargetedAppUpdate(value) {
|
||||||
|
return isCallbackRepair(value) || isGroupMessagePermission(value);
|
||||||
|
}
|
||||||
|
|
||||||
function SvgIcon({ children, size = 18, className, viewBox = "0 0 24 24" }) {
|
function SvgIcon({ children, size = 18, className, viewBox = "0 0 24 24" }) {
|
||||||
return h("svg", {
|
return h("svg", {
|
||||||
width: size,
|
width: size,
|
||||||
|
|
@ -242,6 +251,7 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
||||||
const expired = provision.expired === true || remaining === 0;
|
const expired = provision.expired === true || remaining === 0;
|
||||||
const progress = Math.min(1, remaining / Math.max(1, provision.durationMs ?? remaining));
|
const progress = Math.min(1, remaining / Math.max(1, provision.durationMs ?? remaining));
|
||||||
const repairing = isCallbackRepair(provision);
|
const repairing = isCallbackRepair(provision);
|
||||||
|
const grantingGroupMessages = isGroupMessagePermission(provision);
|
||||||
const botName = provision.botName ?? "此机器人";
|
const botName = provision.botName ?? "此机器人";
|
||||||
|
|
||||||
React.useEffect(() => setImageFailed(false), [qrSource]);
|
React.useEffect(() => setImageFailed(false), [qrSource]);
|
||||||
|
|
@ -255,7 +265,9 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
||||||
src: qrSource,
|
src: qrSource,
|
||||||
alt: repairing
|
alt: repairing
|
||||||
? `用于修复${botName}卡片按钮的一次性授权二维码`
|
? `用于修复${botName}卡片按钮的一次性授权二维码`
|
||||||
: "用于新增 DeepSeek Harness 飞书机器人的一次性授权二维码",
|
: grantingGroupMessages
|
||||||
|
? `用于为${botName}开通群消息权限的一次性授权二维码`
|
||||||
|
: "用于新增 DeepSeek Harness 飞书机器人的一次性授权二维码",
|
||||||
onError: () => setImageFailed(true),
|
onError: () => setImageFailed(true),
|
||||||
})
|
})
|
||||||
: h("div", { className: "bxf-qrFallback dim-qrFallback" },
|
: h("div", { className: "bxf-qrFallback dim-qrFallback" },
|
||||||
|
|
@ -279,21 +291,35 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
||||||
h("div", { className: "bxf-qrCopy dim-qrCopy" },
|
h("div", { className: "bxf-qrCopy dim-qrCopy" },
|
||||||
h("div", { className: "bxf-stateLabel dim-stateLabel" },
|
h("div", { className: "bxf-stateLabel dim-stateLabel" },
|
||||||
h("span", { className: "bxf-dot dim-stateDot", "data-tone": "warning" }),
|
h("span", { className: "bxf-dot dim-stateDot", "data-tone": "warning" }),
|
||||||
h("span", null, repairing ? `正在修复「${botName}」` : "正在添加新机器人")),
|
h("span", null, repairing
|
||||||
|
? `正在修复「${botName}」`
|
||||||
|
: grantingGroupMessages
|
||||||
|
? `正在为「${botName}」开通群消息权限`
|
||||||
|
: "正在添加新机器人")),
|
||||||
h("h3", null, expired
|
h("h3", null, expired
|
||||||
? "刷新二维码后继续"
|
? "刷新二维码后继续"
|
||||||
: repairing ? "使用飞书扫码修复卡片按钮" : "使用飞书扫码创建机器人"),
|
: repairing
|
||||||
|
? "使用飞书扫码修复卡片按钮"
|
||||||
|
: grantingGroupMessages
|
||||||
|
? "使用飞书确认群消息权限"
|
||||||
|
: "使用飞书扫码创建机器人"),
|
||||||
h("p", null, repairing
|
h("p", null, repairing
|
||||||
? "扫码会更新现有飞书应用,只增量补充卡片按钮回调;不会创建新应用。确认后此机器人会短暂重连,其他机器人不受影响。"
|
? "扫码会更新现有飞书应用,只增量补充卡片按钮回调;不会创建新应用。确认后此机器人会短暂重连,其他机器人不受影响。"
|
||||||
: "扫码只会新增一个机器人,已接入的机器人会继续正常收发消息。"),
|
: grantingGroupMessages
|
||||||
|
? "扫码会更新现有飞书应用,只增量开通“获取群组中所有消息”权限;不会创建新应用。确认后会自动启用“响应所有群消息”,其他机器人不受影响。"
|
||||||
|
: "扫码只会新增一个机器人,已接入的机器人会继续正常收发消息。"),
|
||||||
h("ol", { className: "bxf-steps dim-steps" },
|
h("ol", { className: "bxf-steps dim-steps" },
|
||||||
h("li", null, "打开飞书移动端,使用扫一扫读取二维码"),
|
h("li", null, "打开飞书移动端,使用扫一扫读取二维码"),
|
||||||
h("li", null, repairing
|
h("li", null, repairing
|
||||||
? "核对现有应用名称,并确认只新增卡片回调"
|
? "核对现有应用名称,并确认只新增卡片回调"
|
||||||
: "核对应用名称与权限范围,并确认创建"),
|
: grantingGroupMessages
|
||||||
|
? "核对现有应用,并确认“获取群组中所有消息”权限"
|
||||||
|
: "核对应用名称与权限范围,并确认创建"),
|
||||||
h("li", null, repairing
|
h("li", null, repairing
|
||||||
? "保持本页打开,等待卡片按钮修复完成"
|
? "保持本页打开,等待卡片按钮修复完成"
|
||||||
: "保持本页打开,等待新机器人的长连接就绪")),
|
: grantingGroupMessages
|
||||||
|
? "保持本页打开,等待权限生效并自动切换响应方式"
|
||||||
|
: "保持本页打开,等待新机器人的长连接就绪")),
|
||||||
h("div", { className: "bxf-actions dim-viewActions" },
|
h("div", { className: "bxf-actions dim-viewActions" },
|
||||||
expired
|
expired
|
||||||
? h(Button, {
|
? h(Button, {
|
||||||
|
|
@ -308,7 +334,9 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
||||||
!expired
|
!expired
|
||||||
? h(Button, { onClick: onRefresh, disabled: busy }, "换一个二维码")
|
? h(Button, { onClick: onRefresh, disabled: busy }, "换一个二维码")
|
||||||
: null,
|
: null,
|
||||||
h(Button, { onClick: onCancel, disabled: busy }, repairing ? "取消修复" : "取消添加")),
|
h(Button, { onClick: onCancel, disabled: busy }, repairing
|
||||||
|
? "取消修复"
|
||||||
|
: grantingGroupMessages ? "取消授权" : "取消添加")),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -317,34 +345,50 @@ function QrPane({ provision, now, onRefresh, onCancel, busy }) {
|
||||||
function ProvisionProgress({ phase, provision, onCancel, busy }) {
|
function ProvisionProgress({ phase, provision, onCancel, busy }) {
|
||||||
const connecting = phase === "connecting";
|
const connecting = phase === "connecting";
|
||||||
const repairing = isCallbackRepair(provision);
|
const repairing = isCallbackRepair(provision);
|
||||||
|
const grantingGroupMessages = isGroupMessagePermission(provision);
|
||||||
return h("div", {
|
return h("div", {
|
||||||
className: "bxf-card bxf-provisionCard dim-surfaceCard dim-loadingView",
|
className: "bxf-card bxf-provisionCard dim-surfaceCard dim-loadingView",
|
||||||
"aria-busy": "true",
|
"aria-busy": "true",
|
||||||
},
|
},
|
||||||
h("div", { className: "dim-spinner", "aria-hidden": "true" }),
|
h("div", { className: "dim-spinner", "aria-hidden": "true" }),
|
||||||
h("h3", null, connecting
|
h("h3", null, connecting
|
||||||
? repairing ? "已确认,正在完成卡片按钮修复" : "已确认,正在连接新机器人"
|
? repairing
|
||||||
: repairing ? "正在准备修复二维码" : "正在准备授权二维码"),
|
? "已确认,正在完成卡片按钮修复"
|
||||||
|
: grantingGroupMessages
|
||||||
|
? "已确认,正在启用全部消息模式"
|
||||||
|
: "已确认,正在连接新机器人"
|
||||||
|
: repairing
|
||||||
|
? "正在准备修复二维码"
|
||||||
|
: grantingGroupMessages ? "正在准备权限授权二维码" : "正在准备授权二维码"),
|
||||||
h("p", null, connecting
|
h("p", null, connecting
|
||||||
? repairing
|
? repairing
|
||||||
? "配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。"
|
? "配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。"
|
||||||
: "正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。"
|
: grantingGroupMessages
|
||||||
|
? "权限配置已提交,正在保存设置并重连此机器人;此阶段无法取消,其他机器人不会中断。"
|
||||||
|
: "正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。"
|
||||||
: repairing
|
: repairing
|
||||||
? "正在为现有飞书应用申请一次性更新二维码,请稍候。"
|
? "正在为现有飞书应用申请一次性更新二维码,请稍候。"
|
||||||
: "正在向飞书申请一次性授权二维码,请稍候。"),
|
: grantingGroupMessages
|
||||||
|
? "正在为现有飞书应用申请群消息权限二维码,请稍候。"
|
||||||
|
: "正在向飞书申请一次性授权二维码,请稍候。"),
|
||||||
connecting && onCancel
|
connecting && onCancel
|
||||||
? h("div", { className: "bxf-actions dim-viewActions", style: { justifyContent: "center" } },
|
? h("div", { className: "bxf-actions dim-viewActions", style: { justifyContent: "center" } },
|
||||||
h(Button, { onClick: onCancel, disabled: busy }, repairing ? "取消修复" : "取消添加"))
|
h(Button, { onClick: onCancel, disabled: busy }, repairing
|
||||||
|
? "取消修复"
|
||||||
|
: grantingGroupMessages ? "取消授权" : "取消添加"))
|
||||||
: null,
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProvisionError({ error, provision, onRetry, onCancel, busy }) {
|
function ProvisionError({ error, provision, onRetry, onCancel, busy }) {
|
||||||
const repairing = isCallbackRepair(provision);
|
const repairing = isCallbackRepair(provision);
|
||||||
|
const grantingGroupMessages = isGroupMessagePermission(provision);
|
||||||
return h("div", { className: "bxf-card bxf-provisionCard dim-surfaceCard" },
|
return h("div", { className: "bxf-card bxf-provisionCard dim-surfaceCard" },
|
||||||
h("div", { className: "bxf-inlineError dim-inlineError", role: "alert" },
|
h("div", { className: "bxf-inlineError dim-inlineError", role: "alert" },
|
||||||
h("div", null,
|
h("div", null,
|
||||||
h("h3", null, repairing ? "卡片按钮没有修复完成" : "新机器人没有添加完成"),
|
h("h3", null, repairing
|
||||||
|
? "卡片按钮没有修复完成"
|
||||||
|
: grantingGroupMessages ? "群消息权限没有开通完成" : "新机器人没有添加完成"),
|
||||||
h("p", null, error.message),
|
h("p", null, error.message),
|
||||||
error.code ? h("span", { className: "bxf-errorCode" }, error.code) : null,
|
error.code ? h("span", { className: "bxf-errorCode" }, error.code) : null,
|
||||||
h("div", { className: "bxf-actions dim-viewActions" },
|
h("div", { className: "bxf-actions dim-viewActions" },
|
||||||
|
|
@ -414,9 +458,17 @@ function RemoveConfirmation({ bot, busy, onConfirm, onCancel }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function GroupResponseModeEditor({ value, disabled = false, onSave }) {
|
function GroupResponseModeEditor({
|
||||||
|
value,
|
||||||
|
permissionGranted = false,
|
||||||
|
disabled = false,
|
||||||
|
authorizationDisabled = false,
|
||||||
|
onSave,
|
||||||
|
onAuthorize,
|
||||||
|
}) {
|
||||||
const current = normalizeGroupResponseMode(value);
|
const current = normalizeGroupResponseMode(value);
|
||||||
const [saving, setSaving] = React.useState(false);
|
const [saving, setSaving] = React.useState(false);
|
||||||
|
const [authorizing, setAuthorizing] = React.useState(false);
|
||||||
const [error, setError] = React.useState(null);
|
const [error, setError] = React.useState(null);
|
||||||
|
|
||||||
const change = async (event) => {
|
const change = async (event) => {
|
||||||
|
|
@ -433,10 +485,26 @@ function GroupResponseModeEditor({ value, disabled = false, onSave }) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const authorize = async () => {
|
||||||
|
if (current !== "all" || saving || authorizing || disabled || authorizationDisabled) return;
|
||||||
|
setAuthorizing(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await onAuthorize?.();
|
||||||
|
} catch (cause) {
|
||||||
|
setError(cause?.message ?? "群消息权限授权失败,请重试。");
|
||||||
|
} finally {
|
||||||
|
setAuthorizing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return h("div", { className: "bxf-responseMode dim-responseMode" },
|
return h("div", { className: "bxf-responseMode dim-responseMode" },
|
||||||
h("div", { className: "bxf-responseModeHeader dim-responseModeHeader" },
|
h("div", { className: "bxf-responseModeHeader dim-responseModeHeader" },
|
||||||
h("span", null, "群聊响应方式"),
|
h("span", null, "群聊响应方式"),
|
||||||
saving ? h("span", { className: "bxf-responseModeStatus dim-responseModeStatus" }, "保存中…") : null),
|
saving || authorizing
|
||||||
|
? h("span", { className: "bxf-responseModeStatus dim-responseModeStatus" },
|
||||||
|
saving ? "保存中…" : "正在准备授权…")
|
||||||
|
: null),
|
||||||
h("select", {
|
h("select", {
|
||||||
className: "bxf-responseModeSelect dim-responseModeSelect",
|
className: "bxf-responseModeSelect dim-responseModeSelect",
|
||||||
value: current,
|
value: current,
|
||||||
|
|
@ -445,12 +513,27 @@ function GroupResponseModeEditor({ value, disabled = false, onSave }) {
|
||||||
onChange: (event) => { void change(event); },
|
onChange: (event) => { void change(event); },
|
||||||
},
|
},
|
||||||
h("option", { value: "mention" }, "仅在 @机器人时响应(推荐)"),
|
h("option", { value: "mention" }, "仅在 @机器人时响应(推荐)"),
|
||||||
h("option", { value: "all" }, "响应所有群消息(需飞书敏感权限)"),
|
h("option", { value: "all" }, "响应所有群消息"),
|
||||||
),
|
),
|
||||||
h("small", { className: "bxf-responseModeHelp dim-responseModeHelp" },
|
h("small", { className: "bxf-responseModeHelp dim-responseModeHelp" },
|
||||||
current === "mention"
|
current === "mention"
|
||||||
? "私聊始终响应;群聊仅处理明确 @当前机器人的消息。"
|
? permissionGranted
|
||||||
: "需在飞书为该机器人开通“获取群组中所有消息”权限(im:message.group_msg);开通后,机器人会处理群聊中的所有可见消息。"),
|
? "私聊始终响应;群聊仅处理明确 @当前机器人的消息。群消息权限已开通,再次切换无需授权。"
|
||||||
|
: "私聊始终响应;群聊仅处理明确 @当前机器人的消息。选择全部消息后会打开飞书官方授权流程。"
|
||||||
|
: permissionGranted
|
||||||
|
? "已开通“获取群组中所有消息”权限(im:message.group_msg);机器人会处理群聊中的所有可见消息。"
|
||||||
|
: "尚未确认“获取群组中所有消息”权限,请完成飞书授权。"),
|
||||||
|
current === "all"
|
||||||
|
? h("div", { className: "bxf-responseModePermissionAction dim-responseModePermissionAction" },
|
||||||
|
h(Button, {
|
||||||
|
className: "bxf-responseModePermissionButton",
|
||||||
|
size: "small",
|
||||||
|
disabled: disabled || authorizationDisabled || saving || authorizing,
|
||||||
|
"aria-busy": authorizing ? "true" : undefined,
|
||||||
|
"aria-label": permissionGranted ? "重新授权群消息权限" : "授权群消息权限",
|
||||||
|
onClick: () => { void authorize(); },
|
||||||
|
}, authorizing ? "正在准备…" : permissionGranted ? "重新授权" : "去授权"))
|
||||||
|
: null,
|
||||||
error ? h("p", {
|
error ? h("p", {
|
||||||
className: "bxf-responseModeError dim-responseModeError",
|
className: "bxf-responseModeError dim-responseModeError",
|
||||||
role: "alert",
|
role: "alert",
|
||||||
|
|
@ -462,6 +545,8 @@ export function BotCard({
|
||||||
connection,
|
connection,
|
||||||
busy,
|
busy,
|
||||||
repairDisabled,
|
repairDisabled,
|
||||||
|
provisionContent,
|
||||||
|
provisionRef,
|
||||||
actionError,
|
actionError,
|
||||||
testNotice,
|
testNotice,
|
||||||
removing,
|
removing,
|
||||||
|
|
@ -470,6 +555,7 @@ export function BotCard({
|
||||||
onWorkspaceSave,
|
onWorkspaceSave,
|
||||||
onAgentPresetSave,
|
onAgentPresetSave,
|
||||||
onGroupResponseModeSave,
|
onGroupResponseModeSave,
|
||||||
|
onGroupMessagePermissionAuthorize,
|
||||||
onRequestRemove,
|
onRequestRemove,
|
||||||
onConfirmRemove,
|
onConfirmRemove,
|
||||||
onCancelRemove,
|
onCancelRemove,
|
||||||
|
|
@ -527,9 +613,21 @@ export function BotCard({
|
||||||
}),
|
}),
|
||||||
h(GroupResponseModeEditor, {
|
h(GroupResponseModeEditor, {
|
||||||
value: connection.groupResponseMode,
|
value: connection.groupResponseMode,
|
||||||
|
permissionGranted: connection.groupMessagePermissionGranted,
|
||||||
disabled: Boolean(busy),
|
disabled: Boolean(busy),
|
||||||
|
authorizationDisabled: repairDisabled,
|
||||||
onSave: onGroupResponseModeSave,
|
onSave: onGroupResponseModeSave,
|
||||||
|
onAuthorize: onGroupMessagePermissionAuthorize,
|
||||||
}),
|
}),
|
||||||
|
provisionContent
|
||||||
|
? h("section", {
|
||||||
|
className: "bxf-botProvision dim-botProvision",
|
||||||
|
"aria-label": `${bot.name}的飞书授权流程`,
|
||||||
|
"data-provision-for": connection.botId,
|
||||||
|
ref: provisionRef,
|
||||||
|
tabIndex: -1,
|
||||||
|
}, provisionContent)
|
||||||
|
: null,
|
||||||
h("div", { className: "bxf-connectedFooter dim-cardFooter" },
|
h("div", { className: "bxf-connectedFooter dim-cardFooter" },
|
||||||
summary ? h("div", { className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : undefined },
|
summary ? h("div", { className: "bxf-healthSummary dim-cardSummary", "data-error": actionError || connection.error ? "true" : undefined },
|
||||||
summary) : null,
|
summary) : null,
|
||||||
|
|
@ -577,9 +675,14 @@ function BotList(props) {
|
||||||
h(BotCard, {
|
h(BotCard, {
|
||||||
connection: bot,
|
connection: bot,
|
||||||
busy: props.busyByBot[bot.botId]
|
busy: props.busyByBot[bot.botId]
|
||||||
?? (isCallbackRepair(props.provisioning)
|
?? (isTargetedAppUpdate(props.provisioning)
|
||||||
&& props.provisioning.botId === bot.botId ? "callback-repair" : undefined),
|
&& props.provisioning.botId === bot.botId ? props.provisioning.operation : undefined),
|
||||||
repairDisabled: Boolean(props.provisioning),
|
repairDisabled: Boolean(props.provisioning),
|
||||||
|
provisionContent: isTargetedAppUpdate(props.provisioning)
|
||||||
|
&& props.provisioning.botId === bot.botId
|
||||||
|
? props.provisionContent
|
||||||
|
: null,
|
||||||
|
provisionRef: props.provisionRef,
|
||||||
actionError: props.errorsByBot[bot.botId],
|
actionError: props.errorsByBot[bot.botId],
|
||||||
testNotice: props.testNoticesByBot[bot.botId],
|
testNotice: props.testNoticesByBot[bot.botId],
|
||||||
removing: props.removeTargetId === bot.botId,
|
removing: props.removeTargetId === bot.botId,
|
||||||
|
|
@ -588,6 +691,7 @@ function BotList(props) {
|
||||||
onWorkspaceSave: (workspace) => props.onWorkspaceSave(bot, workspace),
|
onWorkspaceSave: (workspace) => props.onWorkspaceSave(bot, workspace),
|
||||||
onAgentPresetSave: (agentPreset) => props.onAgentPresetSave(bot, agentPreset),
|
onAgentPresetSave: (agentPreset) => props.onAgentPresetSave(bot, agentPreset),
|
||||||
onGroupResponseModeSave: (groupResponseMode) => props.onGroupResponseModeSave(bot, groupResponseMode),
|
onGroupResponseModeSave: (groupResponseMode) => props.onGroupResponseModeSave(bot, groupResponseMode),
|
||||||
|
onGroupMessagePermissionAuthorize: () => props.onGroupMessagePermissionAuthorize(bot),
|
||||||
onRequestRemove: () => props.onRequestRemove(bot),
|
onRequestRemove: () => props.onRequestRemove(bot),
|
||||||
onConfirmRemove: () => props.onConfirmRemove(bot),
|
onConfirmRemove: () => props.onConfirmRemove(bot),
|
||||||
onCancelRemove: props.onCancelRemove,
|
onCancelRemove: props.onCancelRemove,
|
||||||
|
|
@ -668,6 +772,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
const [focusBotId, setFocusBotId] = React.useState(null);
|
const [focusBotId, setFocusBotId] = React.useState(null);
|
||||||
const cardRefs = React.useRef(new Map());
|
const cardRefs = React.useRef(new Map());
|
||||||
const removeButtonRefs = React.useRef(new Map());
|
const removeButtonRefs = React.useRef(new Map());
|
||||||
|
const targetedProvisionRef = React.useRef(null);
|
||||||
const addButtonRef = React.useRef(null);
|
const addButtonRef = React.useRef(null);
|
||||||
const mountedRef = React.useRef(true);
|
const mountedRef = React.useRef(true);
|
||||||
const workspaceFence = useWorkspaceSnapshotFence();
|
const workspaceFence = useWorkspaceSnapshotFence();
|
||||||
|
|
@ -758,15 +863,30 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
setFocusBotId(null);
|
setFocusBotId(null);
|
||||||
}, [focusBotId, model.bots]);
|
}, [focusBotId, model.bots]);
|
||||||
|
|
||||||
|
const targetedProvisionFocusKey = isTargetedAppUpdate(model.provisioning)
|
||||||
|
? `${model.provisioning.botId}:${model.provisioning.attemptId ?? "preparing"}:${model.provisioning.phase}`
|
||||||
|
: null;
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!targetedProvisionFocusKey) return;
|
||||||
|
scheduleAnimationFrame(() => {
|
||||||
|
const node = targetedProvisionRef.current;
|
||||||
|
if (!node) return;
|
||||||
|
node.scrollIntoView?.({ block: "nearest", behavior: "smooth" });
|
||||||
|
node.focus?.({ preventScroll: true });
|
||||||
|
}, "targeted-provision-focus");
|
||||||
|
}, [scheduleAnimationFrame, targetedProvisionFocusKey]);
|
||||||
|
|
||||||
const startProvisioning = React.useCallback(async ({
|
const startProvisioning = React.useCallback(async ({
|
||||||
replace = false,
|
replace = false,
|
||||||
operation = FEISHU_REGISTRATION_OPERATIONS.PROVISION,
|
operation = FEISHU_REGISTRATION_OPERATIONS.PROVISION,
|
||||||
bot,
|
bot,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const repairing = operation === CALLBACK_REPAIR_OPERATION;
|
const repairing = operation === CALLBACK_REPAIR_OPERATION;
|
||||||
const botId = repairing ? bot?.botId ?? model.provisioning?.botId : undefined;
|
const grantingGroupMessages = operation === GROUP_MESSAGE_PERMISSION_OPERATION;
|
||||||
const botName = repairing ? bot?.bot?.name ?? model.provisioning?.botName : undefined;
|
const targetedUpdate = repairing || grantingGroupMessages;
|
||||||
if (repairing && !botId) return;
|
const botId = targetedUpdate ? bot?.botId ?? model.provisioning?.botId : undefined;
|
||||||
|
const botName = targetedUpdate ? bot?.bot?.name ?? model.provisioning?.botName : undefined;
|
||||||
|
if (targetedUpdate && !botId) return;
|
||||||
setCredentialOpen(false);
|
setCredentialOpen(false);
|
||||||
setCredentialError(null);
|
setCredentialError(null);
|
||||||
setProvisionBusy(true);
|
setProvisionBusy(true);
|
||||||
|
|
@ -794,13 +914,20 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
// Continue with begin. It is the source of truth for the new attempt.
|
// Continue with begin. It is the source of truth for the new attempt.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const endpoint = repairing
|
||||||
|
? FEISHU_ENDPOINTS.beginCallbackRepair
|
||||||
|
: grantingGroupMessages
|
||||||
|
? FEISHU_ENDPOINTS.beginGroupMessagePermission
|
||||||
|
: FEISHU_ENDPOINTS.beginProvisioning;
|
||||||
const provision = normalizeProvisioning(await invoke(
|
const provision = normalizeProvisioning(await invoke(
|
||||||
repairing ? FEISHU_ENDPOINTS.beginCallbackRepair : FEISHU_ENDPOINTS.beginProvisioning,
|
endpoint,
|
||||||
repairing ? { botId } : { locale: "zh-CN" },
|
targetedUpdate ? { botId } : { locale: "zh-CN" },
|
||||||
));
|
));
|
||||||
if (repairing
|
if (targetedUpdate
|
||||||
&& (provision.operation !== CALLBACK_REPAIR_OPERATION || provision.botId !== botId)) {
|
&& (provision.operation !== operation || provision.botId !== botId)) {
|
||||||
throw new Error("飞书服务返回了不匹配的卡片修复二维码");
|
throw new Error(grantingGroupMessages
|
||||||
|
? "飞书服务返回了不匹配的群消息权限二维码"
|
||||||
|
: "飞书服务返回了不匹配的卡片修复二维码");
|
||||||
}
|
}
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
setNow(timestamp);
|
setNow(timestamp);
|
||||||
|
|
@ -816,7 +943,9 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
}));
|
}));
|
||||||
announce(repairing
|
announce(repairing
|
||||||
? `${botName ?? "机器人"}的修复二维码已生成,请使用飞书扫码。`
|
? `${botName ?? "机器人"}的修复二维码已生成,请使用飞书扫码。`
|
||||||
: "授权二维码已生成,请使用飞书扫码。");
|
: grantingGroupMessages
|
||||||
|
? `${botName ?? "机器人"}的群消息权限二维码已生成,请使用飞书确认。`
|
||||||
|
: "授权二维码已生成,请使用飞书扫码。");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setModel((current) => ({
|
setModel((current) => ({
|
||||||
...current,
|
...current,
|
||||||
|
|
@ -867,7 +996,9 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
const activeProvision = model.provisioning;
|
const activeProvision = model.provisioning;
|
||||||
const attemptId = activeProvision?.attemptId;
|
const attemptId = activeProvision?.attemptId;
|
||||||
const repairing = isCallbackRepair(activeProvision);
|
const repairing = isCallbackRepair(activeProvision);
|
||||||
const targetBot = repairing
|
const grantingGroupMessages = isGroupMessagePermission(activeProvision);
|
||||||
|
const targetedUpdate = isTargetedAppUpdate(activeProvision);
|
||||||
|
const targetBot = targetedUpdate
|
||||||
? model.bots.find((bot) => bot.botId === activeProvision?.botId)
|
? model.bots.find((bot) => bot.botId === activeProvision?.botId)
|
||||||
: undefined;
|
: undefined;
|
||||||
setProvisionBusy(true);
|
setProvisionBusy(true);
|
||||||
|
|
@ -875,8 +1006,8 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
const result = attemptId
|
const result = attemptId
|
||||||
? normalizePollResult(await invoke(FEISHU_ENDPOINTS.cancelProvisioning, { attemptId }))
|
? normalizePollResult(await invoke(FEISHU_ENDPOINTS.cancelProvisioning, { attemptId }))
|
||||||
: null;
|
: null;
|
||||||
if (repairing && result) {
|
if (targetedUpdate && result) {
|
||||||
if (result.operation !== CALLBACK_REPAIR_OPERATION
|
if (result.operation !== activeProvision.operation
|
||||||
|| result.botId !== activeProvision.botId) {
|
|| result.botId !== activeProvision.botId) {
|
||||||
throw new Error("飞书服务返回了不匹配的注册进度");
|
throw new Error("飞书服务返回了不匹配的注册进度");
|
||||||
}
|
}
|
||||||
|
|
@ -893,23 +1024,29 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: current);
|
: current);
|
||||||
announce("配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。");
|
announce(grantingGroupMessages
|
||||||
|
? "权限配置已提交,正在启用全部消息模式并重连此机器人;此阶段无法取消,其他机器人不会中断。"
|
||||||
|
: "配置已提交,正在验证卡片按钮回调并重连此机器人;此阶段无法取消,其他机器人不会中断。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.status === "connected") {
|
if (result.status === "connected") {
|
||||||
const targetBotName = targetBot?.bot.name ?? activeProvision.botName ?? "机器人";
|
const targetBotName = targetBot?.bot.name ?? activeProvision.botName ?? "机器人";
|
||||||
setModel((current) => ({ ...current, provisioning: null }));
|
setModel((current) => ({ ...current, provisioning: null }));
|
||||||
announce(`${targetBotName}的卡片按钮已修复。`);
|
announce(grantingGroupMessages
|
||||||
|
? `${targetBotName}已开通群消息权限,并启用“响应所有群消息”。`
|
||||||
|
: `${targetBotName}的卡片按钮已修复。`);
|
||||||
if (activeProvision.botId) setFocusBotId(activeProvision.botId);
|
if (activeProvision.botId) setFocusBotId(activeProvision.botId);
|
||||||
await loadStatus({ silent: true, restoreProvisioning: false });
|
await loadStatus({ silent: true, restoreProvisioning: false });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setModel((current) => ({ ...current, provisioning: null }));
|
setModel((current) => ({ ...current, provisioning: null }));
|
||||||
announce(repairing ? "已取消卡片按钮修复。" : "已取消添加机器人。");
|
announce(repairing
|
||||||
|
? "已取消卡片按钮修复。"
|
||||||
|
: grantingGroupMessages ? "已取消群消息权限授权。" : "已取消添加机器人。");
|
||||||
await loadStatus({ silent: true, restoreProvisioning: false });
|
await loadStatus({ silent: true, restoreProvisioning: false });
|
||||||
scheduleAnimationFrame(() => {
|
scheduleAnimationFrame(() => {
|
||||||
if (repairing && activeProvision.botId) {
|
if (targetedUpdate && activeProvision.botId) {
|
||||||
cardRefs.current.get(activeProvision.botId)?.focus();
|
cardRefs.current.get(activeProvision.botId)?.focus();
|
||||||
} else {
|
} else {
|
||||||
addButtonRef.current?.focus();
|
addButtonRef.current?.focus();
|
||||||
|
|
@ -966,7 +1103,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
controller.signal,
|
controller.signal,
|
||||||
));
|
));
|
||||||
if (result.operation !== provision.operation
|
if (result.operation !== provision.operation
|
||||||
|| (isCallbackRepair(provision) && result.botId !== provision.botId)) {
|
|| (isTargetedAppUpdate(provision) && result.botId !== provision.botId)) {
|
||||||
throw new Error("飞书服务返回了不匹配的注册进度");
|
throw new Error("飞书服务返回了不匹配的注册进度");
|
||||||
}
|
}
|
||||||
if (result.status === "connected") {
|
if (result.status === "connected") {
|
||||||
|
|
@ -975,7 +1112,9 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
if (!snapshot) {
|
if (!snapshot) {
|
||||||
throw new Error(isCallbackRepair(provision)
|
throw new Error(isCallbackRepair(provision)
|
||||||
? "卡片按钮已更新,但暂时无法确认机器人连接状态"
|
? "卡片按钮已更新,但暂时无法确认机器人连接状态"
|
||||||
: "机器人已经创建,但暂时无法确认连接状态");
|
: isGroupMessagePermission(provision)
|
||||||
|
? "群消息权限已更新,但暂时无法确认机器人连接状态"
|
||||||
|
: "机器人已经创建,但暂时无法确认连接状态");
|
||||||
}
|
}
|
||||||
if (!targetBot?.connected) {
|
if (!targetBot?.connected) {
|
||||||
setModel((current) => current.provisioning?.attemptId === provision.attemptId
|
setModel((current) => current.provisioning?.attemptId === provision.attemptId
|
||||||
|
|
@ -986,15 +1125,21 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
setModel((current) => ({ ...current, provisioning: null }));
|
setModel((current) => ({ ...current, provisioning: null }));
|
||||||
announce(isCallbackRepair(provision)
|
announce(isCallbackRepair(provision)
|
||||||
? `${targetBot.bot.name}的卡片按钮已修复。`
|
? `${targetBot.bot.name}的卡片按钮已修复。`
|
||||||
: targetBot
|
: isGroupMessagePermission(provision)
|
||||||
? `${targetBot.bot.name}已连接,可以在飞书中开始聊天。`
|
? `${targetBot.bot.name}已开通群消息权限,并启用“响应所有群消息”。`
|
||||||
: "新飞书机器人已连接,可以开始聊天。");
|
: targetBot
|
||||||
|
? `${targetBot.bot.name}已连接,可以在飞书中开始聊天。`
|
||||||
|
: "新飞书机器人已连接,可以开始聊天。");
|
||||||
if (result.botId) setFocusBotId(result.botId);
|
if (result.botId) setFocusBotId(result.botId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.status === "failed") {
|
if (result.status === "failed") {
|
||||||
const error = new Error(result.message
|
const error = new Error(result.message
|
||||||
?? (isCallbackRepair(provision) ? "飞书卡片按钮修复失败" : "飞书应用创建失败"));
|
?? (isCallbackRepair(provision)
|
||||||
|
? "飞书卡片按钮修复失败"
|
||||||
|
: isGroupMessagePermission(provision)
|
||||||
|
? "飞书群消息权限开通失败"
|
||||||
|
: "飞书应用创建失败"));
|
||||||
error.code = "FEISHU_PROVISION_FAILED";
|
error.code = "FEISHU_PROVISION_FAILED";
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -1153,8 +1298,30 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
}
|
}
|
||||||
}, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
|
}, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
|
||||||
|
|
||||||
|
const authorizeGroupMessages = React.useCallback(async (connection) => {
|
||||||
|
const { botId } = connection;
|
||||||
|
if (model.provisioning) {
|
||||||
|
throw new Error("请先完成当前飞书授权操作,再开通群消息权限。");
|
||||||
|
}
|
||||||
|
setRemoveTargetId(null);
|
||||||
|
setBotError(botId, null);
|
||||||
|
setTestNoticesByBot((current) => {
|
||||||
|
const next = { ...current };
|
||||||
|
delete next[botId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
await startProvisioning({
|
||||||
|
operation: GROUP_MESSAGE_PERMISSION_OPERATION,
|
||||||
|
bot: connection,
|
||||||
|
});
|
||||||
|
}, [model.provisioning, setBotError, startProvisioning]);
|
||||||
|
|
||||||
const saveGroupResponseMode = React.useCallback(async (connection, groupResponseMode) => {
|
const saveGroupResponseMode = React.useCallback(async (connection, groupResponseMode) => {
|
||||||
const { botId } = connection;
|
const { botId } = connection;
|
||||||
|
if (groupResponseMode === "all" && connection.groupMessagePermissionGranted !== true) {
|
||||||
|
await authorizeGroupMessages(connection);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const snapshotVersion = workspaceFence.beginMutation();
|
const snapshotVersion = workspaceFence.beginMutation();
|
||||||
setBotBusy(botId, "group-response-mode");
|
setBotBusy(botId, "group-response-mode");
|
||||||
setBotError(botId, null);
|
setBotError(botId, null);
|
||||||
|
|
@ -1171,7 +1338,15 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
if (shouldRefresh && mountedRef.current) void loadStatus({ silent: true });
|
||||||
if (mountedRef.current) setBotBusy(botId, null);
|
if (mountedRef.current) setBotBusy(botId, null);
|
||||||
}
|
}
|
||||||
}, [invoke, loadStatus, mergeSnapshot, setBotBusy, setBotError, workspaceFence]);
|
}, [
|
||||||
|
invoke,
|
||||||
|
authorizeGroupMessages,
|
||||||
|
loadStatus,
|
||||||
|
mergeSnapshot,
|
||||||
|
setBotBusy,
|
||||||
|
setBotError,
|
||||||
|
workspaceFence,
|
||||||
|
]);
|
||||||
|
|
||||||
const requestRemove = React.useCallback((connection) => {
|
const requestRemove = React.useCallback((connection) => {
|
||||||
setRemoveTargetId(connection.botId);
|
setRemoveTargetId(connection.botId);
|
||||||
|
|
@ -1210,6 +1385,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
}, [announce, invoke, loadStatus, mergeSnapshot, scheduleAnimationFrame, setBotBusy, setBotError, workspaceFence]);
|
}, [announce, invoke, loadStatus, mergeSnapshot, scheduleAnimationFrame, setBotBusy, setBotError, workspaceFence]);
|
||||||
|
|
||||||
const provision = model.provisioning;
|
const provision = model.provisioning;
|
||||||
|
const targetedProvisioning = isTargetedAppUpdate(provision);
|
||||||
const provisionBot = provision?.botId
|
const provisionBot = provision?.botId
|
||||||
? model.bots.find((bot) => bot.botId === provision.botId)
|
? model.bots.find((bot) => bot.botId === provision.botId)
|
||||||
?? { botId: provision.botId, bot: { name: provision.botName ?? "此机器人" } }
|
?? { botId: provision.botId, bot: { name: provision.botName ?? "此机器人" } }
|
||||||
|
|
@ -1235,7 +1411,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
provisionContent = h(ProvisionProgress, {
|
provisionContent = h(ProvisionProgress, {
|
||||||
phase: "connecting",
|
phase: "connecting",
|
||||||
provision,
|
provision,
|
||||||
onCancel: isCallbackRepair(provision) ? undefined : () => void cancelProvisioning(),
|
onCancel: isTargetedAppUpdate(provision) ? undefined : () => void cancelProvisioning(),
|
||||||
busy: provisionBusy,
|
busy: provisionBusy,
|
||||||
});
|
});
|
||||||
} else if (provision?.phase === "error") {
|
} else if (provision?.phase === "error") {
|
||||||
|
|
@ -1310,7 +1486,7 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
})
|
})
|
||||||
: h(React.Fragment, null,
|
: h(React.Fragment, null,
|
||||||
credentialContent,
|
credentialContent,
|
||||||
provisionContent,
|
targetedProvisioning ? null : provisionContent,
|
||||||
model.bots.length === 0 && !provision && !credentialOpen
|
model.bots.length === 0 && !provision && !credentialOpen
|
||||||
? h(EmptyView, { onStart: () => void startProvisioning(), busy: provisionBusy })
|
? h(EmptyView, { onStart: () => void startProvisioning(), busy: provisionBusy })
|
||||||
: null,
|
: null,
|
||||||
|
|
@ -1322,11 +1498,14 @@ export function FeishuSettingsTab({ rpcCall }) {
|
||||||
testNoticesByBot,
|
testNoticesByBot,
|
||||||
removeTargetId,
|
removeTargetId,
|
||||||
provisioning: provision,
|
provisioning: provision,
|
||||||
|
provisionContent,
|
||||||
|
provisionRef: targetedProvisionRef,
|
||||||
onReconnect: (bot) => void reconnectOneBot(bot),
|
onReconnect: (bot) => void reconnectOneBot(bot),
|
||||||
onRepairCallback: repairCallback,
|
onRepairCallback: repairCallback,
|
||||||
onWorkspaceSave: saveWorkspace,
|
onWorkspaceSave: saveWorkspace,
|
||||||
onAgentPresetSave: saveAgentPreset,
|
onAgentPresetSave: saveAgentPreset,
|
||||||
onGroupResponseModeSave: saveGroupResponseMode,
|
onGroupResponseModeSave: saveGroupResponseMode,
|
||||||
|
onGroupMessagePermissionAuthorize: authorizeGroupMessages,
|
||||||
onRequestRemove: requestRemove,
|
onRequestRemove: requestRemove,
|
||||||
onConfirmRemove: (bot) => void confirmRemove(bot),
|
onConfirmRemove: (bot) => void confirmRemove(bot),
|
||||||
onCancelRemove: cancelRemove,
|
onCancelRemove: cancelRemove,
|
||||||
|
|
|
||||||
|
|
@ -416,7 +416,40 @@ const CSS = String.raw`
|
||||||
.bxf-responseModeSelect:focus-visible { outline: none; border-color: var(--bxf-accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--bxf-accent) 16%, transparent); }
|
.bxf-responseModeSelect:focus-visible { outline: none; border-color: var(--bxf-accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--bxf-accent) 16%, transparent); }
|
||||||
.bxf-responseModeSelect:disabled { cursor: not-allowed; opacity: .55; }
|
.bxf-responseModeSelect:disabled { cursor: not-allowed; opacity: .55; }
|
||||||
.bxf-responseModeHelp { grid-column: 1 / -1; grid-row: 3; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 1.45; }
|
.bxf-responseModeHelp { grid-column: 1 / -1; grid-row: 3; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 11px; line-height: 1.45; }
|
||||||
.bxf-responseModeError { grid-column: 1 / -1; grid-row: 4; color: var(--bxf-error); font-size: 12px; line-height: 1.4; margin: 0; }
|
.bxf-responseModePermissionAction { grid-column: 1 / -1; grid-row: 4; display: flex; justify-content: flex-start; margin-top: 2px; }
|
||||||
|
.bxf-responseModePermissionButton { min-height: 28px; padding: 3px 9px; color: var(--bxf-accent); border-color: color-mix(in srgb, var(--bxf-accent) 30%, var(--dsw-alias-border-l2, #dfe1e5)); background: var(--dsw-alias-bg-layer-1, #fff); }
|
||||||
|
.bxf-responseModePermissionButton:hover:not(:disabled) { background: color-mix(in srgb, var(--bxf-accent) 7%, transparent); }
|
||||||
|
.bxf-responseModeError { grid-column: 1 / -1; grid-row: 5; color: var(--bxf-error); font-size: 12px; line-height: 1.4; margin: 0; }
|
||||||
|
|
||||||
|
.bxf-botProvision {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 14px;
|
||||||
|
scroll-margin-block: 20px;
|
||||||
|
animation: bxf-revealProvision .2s var(--ds-ease-out, ease-out) both;
|
||||||
|
}
|
||||||
|
.bxf-botProvision:focus { outline: none; }
|
||||||
|
.bxf-botProvision:focus-visible {
|
||||||
|
outline: 2px solid color-mix(in srgb, var(--bxf-accent) 75%, transparent);
|
||||||
|
outline-offset: 3px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.bxf-botProvision > .bxf-provisionCard {
|
||||||
|
border-radius: 11px;
|
||||||
|
box-shadow: none;
|
||||||
|
background: color-mix(in srgb, var(--bxf-accent) 2.5%, var(--dsw-alias-bg-layer-3, #fff));
|
||||||
|
}
|
||||||
|
.bxf-botProvision .bxf-cardBody { padding: 18px; }
|
||||||
|
.bxf-botProvision .bxf-qrLayout {
|
||||||
|
grid-template-columns: 184px minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.bxf-botProvision .bxf-qrFrame { width: 176px; height: 176px; padding: 10px; border-radius: 11px; }
|
||||||
|
.bxf-botProvision .bxf-countdown { width: 176px; }
|
||||||
|
.bxf-botProvision .bxf-qrCopy h3 { font-size: 18px; line-height: 26px; }
|
||||||
|
.bxf-botProvision .bxf-steps { gap: 8px; margin-top: 14px; }
|
||||||
|
.bxf-botProvision .bxf-actions { margin-top: 16px; }
|
||||||
|
.bxf-botProvision .bxf-inlineError { min-height: 160px; padding: 22px; }
|
||||||
|
|
||||||
.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); }
|
.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); }
|
||||||
.bxf-healthSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; }
|
.bxf-healthSummary { min-width: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; }
|
||||||
|
|
@ -470,6 +503,7 @@ const CSS = String.raw`
|
||||||
@keyframes bxf-rotate { to { transform: rotate(360deg); } }
|
@keyframes bxf-rotate { to { transform: rotate(360deg); } }
|
||||||
@keyframes bxf-pulse { 0%, 100% { transform: scale(.9); opacity: .45; } 50% { transform: scale(1.08); opacity: 1; } }
|
@keyframes bxf-pulse { 0%, 100% { transform: scale(.9); opacity: .45; } 50% { transform: scale(1.08); opacity: 1; } }
|
||||||
@keyframes bxf-shimmer { to { background-position: -220% 0; } }
|
@keyframes bxf-shimmer { to { background-position: -220% 0; } }
|
||||||
|
@keyframes bxf-revealProvision { from { opacity: 0; transform: translateY(-5px); } }
|
||||||
|
|
||||||
@container (max-width: 620px) {
|
@container (max-width: 620px) {
|
||||||
.bxf-headingTools { gap: 6px; }
|
.bxf-headingTools { gap: 6px; }
|
||||||
|
|
@ -481,6 +515,7 @@ const CSS = String.raw`
|
||||||
.bxf-intro { grid-template-columns: minmax(0, 1fr); }
|
.bxf-intro { grid-template-columns: minmax(0, 1fr); }
|
||||||
.bxf-markStage { display: none; }
|
.bxf-markStage { display: none; }
|
||||||
.bxf-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }
|
.bxf-qrLayout { grid-template-columns: minmax(0, 1fr); justify-items: center; }
|
||||||
|
.bxf-botProvision .bxf-qrLayout { grid-template-columns: minmax(0, 1fr); }
|
||||||
.bxf-qrCopy { width: 100%; }
|
.bxf-qrCopy { width: 100%; }
|
||||||
.bxf-statusGrid { grid-template-columns: minmax(0, 1fr); }
|
.bxf-statusGrid { grid-template-columns: minmax(0, 1fr); }
|
||||||
.bxf-connectedTop { align-items: flex-start; flex-direction: column; }
|
.bxf-connectedTop { align-items: flex-start; flex-direction: column; }
|
||||||
|
|
|
||||||
|
|
@ -70,10 +70,22 @@ const EN = Object.freeze({
|
||||||
'飞书机器人设置': 'Feishu bot settings',
|
'飞书机器人设置': 'Feishu bot settings',
|
||||||
'群聊响应方式': 'Group response mode',
|
'群聊响应方式': 'Group response mode',
|
||||||
'仅在 @机器人时响应(推荐)': 'Only respond when @mentioned (recommended)',
|
'仅在 @机器人时响应(推荐)': 'Only respond when @mentioned (recommended)',
|
||||||
|
'响应所有群消息': 'Respond to all group messages',
|
||||||
'响应所有群消息(需飞书敏感权限)': 'Respond to all group messages (requires a sensitive Feishu scope)',
|
'响应所有群消息(需飞书敏感权限)': 'Respond to all group messages (requires a sensitive Feishu scope)',
|
||||||
|
'重新授权': 'Reauthorize',
|
||||||
|
'去授权': 'Authorize',
|
||||||
|
'重新授权群消息权限': 'Reauthorize group-message permission',
|
||||||
|
'授权群消息权限': 'Authorize group-message permission',
|
||||||
|
'正在准备授权…': 'Preparing authorization…',
|
||||||
|
'正在准备…': 'Preparing…',
|
||||||
|
'私聊始终响应;群聊仅处理明确 @当前机器人的消息。群消息权限已开通,再次切换无需授权。': 'Direct messages always work; group chats require an explicit @mention of this bot. The group-message permission is already granted, so switching again needs no authorization.',
|
||||||
|
'私聊始终响应;群聊仅处理明确 @当前机器人的消息。选择全部消息后会打开飞书官方授权流程。': 'Direct messages always work; group chats require an explicit @mention of this bot. Selecting all messages opens the official Feishu authorization flow.',
|
||||||
|
'已开通“获取群组中所有消息”权限(im:message.group_msg);机器人会处理群聊中的所有可见消息。': 'The “Read all messages in associated group chat” scope (im:message.group_msg) is granted; the bot processes every visible group message.',
|
||||||
|
'尚未确认“获取群组中所有消息”权限,请完成飞书授权。': 'The “Read all messages in associated group chat” scope has not been confirmed. Complete Feishu authorization.',
|
||||||
'私聊始终响应;群聊仅处理明确 @当前机器人的消息。': 'Direct messages always work; group chats require an explicit @mention of this bot.',
|
'私聊始终响应;群聊仅处理明确 @当前机器人的消息。': 'Direct messages always work; group chats require an explicit @mention of this bot.',
|
||||||
'需在飞书为该机器人开通“获取群组中所有消息”权限(im:message.group_msg);开通后,机器人会处理群聊中的所有可见消息。': 'Grant this bot the “Read all messages in associated group chat” Feishu scope (im:message.group_msg); once granted, it will process every visible group message.',
|
'需在飞书为该机器人开通“获取群组中所有消息”权限(im:message.group_msg);开通后,机器人会处理群聊中的所有可见消息。': 'Grant this bot the “Read all messages in associated group chat” Feishu scope (im:message.group_msg); once granted, it will process every visible group message.',
|
||||||
'群聊响应方式修改失败,请重试。': 'Could not update the group response mode. Try again.',
|
'群聊响应方式修改失败,请重试。': 'Could not update the group response mode. Try again.',
|
||||||
|
'群消息权限授权失败,请重试。': 'Could not authorize group-message permission. Try again.',
|
||||||
'钉钉设置': 'DingTalk settings',
|
'钉钉设置': 'DingTalk settings',
|
||||||
'企业微信设置': 'WeCom settings',
|
'企业微信设置': 'WeCom settings',
|
||||||
'扫码接入机器人': 'Scan QR code',
|
'扫码接入机器人': 'Scan QR code',
|
||||||
|
|
@ -196,6 +208,7 @@ const EN = Object.freeze({
|
||||||
'生成 QQ 二维码': 'Generate QQ QR code',
|
'生成 QQ 二维码': 'Generate QQ QR code',
|
||||||
'正在生成二维码…': 'Generating QR code…',
|
'正在生成二维码…': 'Generating QR code…',
|
||||||
'正在准备授权二维码': 'Preparing authorization QR code',
|
'正在准备授权二维码': 'Preparing authorization QR code',
|
||||||
|
'正在准备权限授权二维码': 'Preparing permission authorization QR code',
|
||||||
'正在准备微信二维码': 'Preparing WeChat QR code',
|
'正在准备微信二维码': 'Preparing WeChat QR code',
|
||||||
'正在添加新机器人': 'Adding a new bot',
|
'正在添加新机器人': 'Adding a new bot',
|
||||||
'正在申请钉钉授权二维码…': 'Requesting DingTalk authorization QR code…',
|
'正在申请钉钉授权二维码…': 'Requesting DingTalk authorization QR code…',
|
||||||
|
|
@ -213,6 +226,16 @@ const EN = Object.freeze({
|
||||||
'在飞书中打开': 'Open in Feishu',
|
'在飞书中打开': 'Open in Feishu',
|
||||||
'取消添加': 'Cancel',
|
'取消添加': 'Cancel',
|
||||||
'使用飞书扫码修复卡片按钮': 'Scan with Feishu to repair card buttons',
|
'使用飞书扫码修复卡片按钮': 'Scan with Feishu to repair card buttons',
|
||||||
|
'使用飞书确认群消息权限': 'Confirm group-message permission with Feishu',
|
||||||
|
'扫码会更新现有飞书应用,只增量开通“获取群组中所有消息”权限;不会创建新应用。确认后会自动启用“响应所有群消息”,其他机器人不受影响。': '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.',
|
||||||
|
'核对现有应用,并确认“获取群组中所有消息”权限': 'Review the existing app and confirm the “Read all messages in associated group chat” permission',
|
||||||
|
'保持本页打开,等待权限生效并自动切换响应方式': 'Keep this page open while the permission takes effect and the response mode switches automatically',
|
||||||
|
'取消授权': 'Cancel authorization',
|
||||||
|
'已确认,正在启用全部消息模式': 'Confirmed. Enabling all-message mode',
|
||||||
|
'权限配置已提交,正在保存设置并重连此机器人;此阶段无法取消,其他机器人不会中断。': 'The permission update was submitted. Saving the setting and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.',
|
||||||
|
'权限配置已提交,正在启用全部消息模式并重连此机器人;此阶段无法取消,其他机器人不会中断。': 'The permission update was submitted. Enabling all-message mode and reconnecting this bot. This stage cannot be cancelled; other bots will not be interrupted.',
|
||||||
|
'正在为现有飞书应用申请群消息权限二维码,请稍候。': 'Requesting a group-message permission QR code for the existing Feishu app…',
|
||||||
|
'群消息权限没有开通完成': 'Group-message permission was not granted',
|
||||||
'扫码会更新现有飞书应用,只增量补充卡片按钮回调;不会创建新应用。确认后此机器人会短暂重连,其他机器人不受影响。': '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.',
|
'扫码会更新现有飞书应用,只增量补充卡片按钮回调;不会创建新应用。确认后此机器人会短暂重连,其他机器人不受影响。': '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.',
|
||||||
'核对现有应用名称,并确认只新增卡片回调': 'Review the existing app name and confirm that only the card callback is added',
|
'核对现有应用名称,并确认只新增卡片回调': 'Review the existing app name and confirm that only the card callback is added',
|
||||||
'保持本页打开,等待卡片按钮修复完成': 'Keep this page open until card-button repair finishes',
|
'保持本页打开,等待卡片按钮修复完成': 'Keep this page open until card-button repair finishes',
|
||||||
|
|
@ -225,14 +248,25 @@ const EN = Object.freeze({
|
||||||
'修复卡片按钮': 'Repair card buttons',
|
'修复卡片按钮': 'Repair card buttons',
|
||||||
'等待扫码…': 'Waiting for scan…',
|
'等待扫码…': 'Waiting for scan…',
|
||||||
'飞书服务返回了不匹配的卡片修复二维码': 'Feishu returned a repair QR code for a different bot',
|
'飞书服务返回了不匹配的卡片修复二维码': 'Feishu returned a repair QR code for a different bot',
|
||||||
|
'飞书服务返回了不匹配的群消息权限二维码': 'Feishu returned a group-message permission QR code for a different bot',
|
||||||
'飞书服务返回的修复信息缺少 botId': 'Feishu repair status is missing the bot ID',
|
'飞书服务返回的修复信息缺少 botId': 'Feishu repair status is missing the bot ID',
|
||||||
|
'飞书服务返回的应用更新信息缺少 botId': 'Feishu app-update status is missing the bot ID',
|
||||||
'飞书服务返回了不匹配的注册进度': 'Feishu returned registration progress for a different operation',
|
'飞书服务返回了不匹配的注册进度': 'Feishu returned registration progress for a different operation',
|
||||||
'此机器人': 'this bot',
|
'此机器人': 'this bot',
|
||||||
|
'用于为${botName}开通群消息权限的一次性授权二维码': 'One-time QR code for granting group-message permission to ${botName}',
|
||||||
|
'正在为「${botName}」开通群消息权限': 'Granting group-message permission to “${botName}”',
|
||||||
'${botName ?? "机器人"}的修复二维码已生成,请使用飞书扫码。': 'Repair QR code generated for ${botName ?? "bot"}. Scan it with Feishu.',
|
'${botName ?? "机器人"}的修复二维码已生成,请使用飞书扫码。': 'Repair QR code generated for ${botName ?? "bot"}. Scan it with Feishu.',
|
||||||
|
'${botName ?? "机器人"}的群消息权限二维码已生成,请使用飞书确认。': 'Group-message permission QR code generated for ${botName ?? "bot"}. Confirm it with Feishu.',
|
||||||
|
'${targetBotName}已开通群消息权限,并启用“响应所有群消息”。': '${targetBotName} now has group-message permission and “Respond to all group messages” is enabled.',
|
||||||
|
'${targetBot.bot.name}已开通群消息权限,并启用“响应所有群消息”。': '${targetBot.bot.name} now has group-message permission and “Respond to all group messages” is enabled.',
|
||||||
'${targetBot.bot.name}已连接,可以在飞书中开始聊天。': '${targetBot.bot.name} is connected and ready to chat in Feishu.',
|
'${targetBot.bot.name}已连接,可以在飞书中开始聊天。': '${targetBot.bot.name} is connected and ready to chat in Feishu.',
|
||||||
'已取消卡片按钮修复。': 'Card-button repair was cancelled.',
|
'已取消卡片按钮修复。': 'Card-button repair was cancelled.',
|
||||||
|
'已取消群消息权限授权。': 'Group-message permission authorization was cancelled.',
|
||||||
'卡片按钮已更新,但暂时无法确认机器人连接状态': 'The card callback was updated, but the bot connection could not be confirmed yet',
|
'卡片按钮已更新,但暂时无法确认机器人连接状态': 'The card callback was updated, but the bot connection could not be confirmed yet',
|
||||||
|
'群消息权限已更新,但暂时无法确认机器人连接状态': 'The group-message permission was updated, but the bot connection could not be confirmed yet',
|
||||||
'飞书卡片按钮修复失败': 'Could not repair the Feishu card buttons',
|
'飞书卡片按钮修复失败': 'Could not repair the Feishu card buttons',
|
||||||
|
'飞书群消息权限开通失败': 'Could not grant the Feishu group-message permission',
|
||||||
|
'请先完成当前飞书授权操作,再开通群消息权限。': 'Finish the current Feishu authorization before granting group-message permission.',
|
||||||
'已确认,正在连接新机器人': 'Confirmed. Connecting the new bot',
|
'已确认,正在连接新机器人': 'Confirmed. Connecting the new bot',
|
||||||
'正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。': 'Saving credentials and checking the new bot connection. Existing bots will not be interrupted.',
|
'正在安全保存凭据并检查新机器人的消息通道,其他机器人不会中断。': 'Saving credentials and checking the new bot connection. Existing bots will not be interrupted.',
|
||||||
'正在向飞书申请一次性授权二维码,请稍候。': 'Requesting a one-time authorization QR code from Feishu…',
|
'正在向飞书申请一次性授权二维码,请稍候。': 'Requesting a one-time authorization QR code from Feishu…',
|
||||||
|
|
@ -531,16 +565,26 @@ function translateDynamic(text) {
|
||||||
if (match) return `Remove “${match[1]}” from DeepSeek Harness?`;
|
if (match) return `Remove “${match[1]}” from DeepSeek Harness?`;
|
||||||
match = /^从 DeepSeek Harness 移除(.+)$/.exec(text);
|
match = /^从 DeepSeek Harness 移除(.+)$/.exec(text);
|
||||||
if (match) return `Remove ${match[1]} from DeepSeek Harness`;
|
if (match) return `Remove ${match[1]} from DeepSeek Harness`;
|
||||||
|
match = /^(.+)的飞书授权流程$/.exec(text);
|
||||||
|
if (match) return `Feishu authorization flow for ${match[1]}`;
|
||||||
match = /^用于修复(.+)卡片按钮的一次性授权二维码$/.exec(text);
|
match = /^用于修复(.+)卡片按钮的一次性授权二维码$/.exec(text);
|
||||||
if (match) return `One-time QR code for repairing card buttons for ${match[1]}`;
|
if (match) return `One-time QR code for repairing card buttons for ${match[1]}`;
|
||||||
|
match = /^用于为(.+)开通群消息权限的一次性授权二维码$/.exec(text);
|
||||||
|
if (match) return `One-time QR code for granting group-message permission to ${match[1]}`;
|
||||||
match = /^正在修复「(.+)」$/.exec(text);
|
match = /^正在修复「(.+)」$/.exec(text);
|
||||||
if (match) return `Repairing “${match[1]}”`;
|
if (match) return `Repairing “${match[1]}”`;
|
||||||
|
match = /^正在为「(.+)」开通群消息权限$/.exec(text);
|
||||||
|
if (match) return `Granting group-message permission to “${match[1]}”`;
|
||||||
match = /^修复(.+)的卡片按钮$/.exec(text);
|
match = /^修复(.+)的卡片按钮$/.exec(text);
|
||||||
if (match) return `Repair card buttons for ${match[1]}`;
|
if (match) return `Repair card buttons for ${match[1]}`;
|
||||||
match = /^(.+)的修复二维码已生成,请使用飞书扫码。$/.exec(text);
|
match = /^(.+)的修复二维码已生成,请使用飞书扫码。$/.exec(text);
|
||||||
if (match) return `Repair QR code generated for ${match[1]}. Scan it with Feishu.`;
|
if (match) return `Repair QR code generated for ${match[1]}. Scan it with Feishu.`;
|
||||||
|
match = /^(.+)的群消息权限二维码已生成,请使用飞书确认。$/.exec(text);
|
||||||
|
if (match) return `Group-message permission QR code generated for ${match[1]}. Confirm it with Feishu.`;
|
||||||
match = /^(.+)的卡片按钮已修复。$/.exec(text);
|
match = /^(.+)的卡片按钮已修复。$/.exec(text);
|
||||||
if (match) return `Card buttons repaired for ${match[1]}.`;
|
if (match) return `Card buttons repaired for ${match[1]}.`;
|
||||||
|
match = /^(.+)已开通群消息权限,并启用“响应所有群消息”。$/.exec(text);
|
||||||
|
if (match) return `${match[1]} now has group-message permission and “Respond to all group messages” is enabled.`;
|
||||||
match = /^(检查连接|重试连接)(.+)$/.exec(text);
|
match = /^(检查连接|重试连接)(.+)$/.exec(text);
|
||||||
if (match) return `${localizeText(match[1])} ${match[2]}`;
|
if (match) return `${localizeText(match[1])} ${match[2]}`;
|
||||||
match = /^移除(.+)$/.exec(text);
|
match = /^移除(.+)$/.exec(text);
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,17 @@ const REGISTRATION_STATES = new Set([
|
||||||
'idle', 'starting', 'qr_ready', 'polling', 'slow_down',
|
'idle', 'starting', 'qr_ready', 'polling', 'slow_down',
|
||||||
'domain_switched', 'saving', 'succeeded', 'expired', 'cancelled', 'error',
|
'domain_switched', 'saving', 'succeeded', 'expired', 'cancelled', 'error',
|
||||||
]);
|
]);
|
||||||
const REGISTRATION_OPERATIONS = new Set(['provision', 'callback_repair']);
|
const REGISTRATION_OPERATIONS = new Set([
|
||||||
|
'provision',
|
||||||
|
'callback_repair',
|
||||||
|
'group_message_permission',
|
||||||
|
]);
|
||||||
const CALLBACK_REPAIR_OPERATION = 'callback_repair';
|
const CALLBACK_REPAIR_OPERATION = 'callback_repair';
|
||||||
|
const GROUP_MESSAGE_PERMISSION_OPERATION = 'group_message_permission';
|
||||||
|
const TARGETED_APP_UPDATE_OPERATIONS = new Set([
|
||||||
|
CALLBACK_REPAIR_OPERATION,
|
||||||
|
GROUP_MESSAGE_PERMISSION_OPERATION,
|
||||||
|
]);
|
||||||
const OFFICIAL_REGISTRATION_HOSTS = new Set([
|
const OFFICIAL_REGISTRATION_HOSTS = new Set([
|
||||||
'accounts.feishu.cn',
|
'accounts.feishu.cn',
|
||||||
'accounts.larksuite.com',
|
'accounts.larksuite.com',
|
||||||
|
|
@ -63,6 +72,9 @@ const PUBLIC_ERROR_MESSAGES = Object.freeze({
|
||||||
credential_update_failed: 'Unable to store the repaired Feishu credentials.',
|
credential_update_failed: 'Unable to store the repaired Feishu credentials.',
|
||||||
credential_state_unknown: 'The repaired Feishu credentials could not be confirmed after saving.',
|
credential_state_unknown: 'The repaired Feishu credentials could not be confirmed after saving.',
|
||||||
repair_connection_failed: 'The callback update was accepted, but the selected bot could not reconnect.',
|
repair_connection_failed: 'The callback update was accepted, but the selected bot could not reconnect.',
|
||||||
|
group_message_permission_required: 'Authorize access to all group messages before enabling this mode.',
|
||||||
|
group_message_permission_save_failed: 'Feishu granted the permission, but all-message mode could not be saved.',
|
||||||
|
group_message_permission_connection_failed: 'Feishu granted the permission, but the selected bot could not reconnect.',
|
||||||
card_action_probe_unavailable: 'The selected bot is not connected, so its card button cannot be verified.',
|
card_action_probe_unavailable: 'The selected bot is not connected, so its card button cannot be verified.',
|
||||||
card_action_probe_send_failed: 'The callback update was accepted, but the verification card could not be sent.',
|
card_action_probe_send_failed: 'The callback update was accepted, but the verification card could not be sent.',
|
||||||
card_action_probe_timeout: 'Feishu accepted the update, but the card button was not verified in time. Start the repair again and click the test button within two minutes.',
|
card_action_probe_timeout: 'Feishu accepted the update, but the card button was not verified in time. Start the repair again and click the test button within two minutes.',
|
||||||
|
|
@ -142,7 +154,7 @@ function safeRegistrationUrl(value, operation, expectedHost) {
|
||||||
|| url.port
|
|| url.port
|
||||||
|| url.username
|
|| url.username
|
||||||
|| url.password) return undefined;
|
|| url.password) return undefined;
|
||||||
if (operation === CALLBACK_REPAIR_OPERATION) {
|
if (TARGETED_APP_UPDATE_OPERATIONS.has(operation)) {
|
||||||
const clientIds = url.searchParams.getAll('clientID');
|
const clientIds = url.searchParams.getAll('clientID');
|
||||||
const transportKinds = url.searchParams.getAll('tp');
|
const transportKinds = url.searchParams.getAll('tp');
|
||||||
const addons = url.searchParams.getAll('addons');
|
const addons = url.searchParams.getAll('addons');
|
||||||
|
|
@ -244,7 +256,7 @@ async function publicProvisioning(registration, encodeQr, expectedHost) {
|
||||||
// remote update is committed. Keep only the opaque attempt identity so a
|
// remote update is committed. Keep only the opaque attempt identity so a
|
||||||
// browser reload can resume polling the non-cancellable verification phase.
|
// browser reload can resume polling the non-cancellable verification phase.
|
||||||
if (registration.state === 'saving'
|
if (registration.state === 'saving'
|
||||||
&& registration.operation === CALLBACK_REPAIR_OPERATION
|
&& TARGETED_APP_UPDATE_OPERATIONS.has(registration.operation)
|
||||||
&& registration.botId) {
|
&& registration.botId) {
|
||||||
return { ...projection, submitted: true };
|
return { ...projection, submitted: true };
|
||||||
}
|
}
|
||||||
|
|
@ -264,6 +276,7 @@ function publicBotEntry(entry) {
|
||||||
configured: source.configured === true,
|
configured: source.configured === true,
|
||||||
agentPreset: normalizeAgentPresetId(source.agentPreset),
|
agentPreset: normalizeAgentPresetId(source.agentPreset),
|
||||||
groupResponseMode: normalizeFeishuGroupResponseMode(source.groupResponseMode),
|
groupResponseMode: normalizeFeishuGroupResponseMode(source.groupResponseMode),
|
||||||
|
groupMessagePermissionGranted: source.groupMessagePermissionGranted === true,
|
||||||
bot: publicBot(source.bot),
|
bot: publicBot(source.bot),
|
||||||
health: publicHealth(source, connected),
|
health: publicHealth(source, connected),
|
||||||
};
|
};
|
||||||
|
|
@ -279,12 +292,12 @@ export async function toPublicFeishuStatus(status, { encodeQr = qrCodeDataUrl }
|
||||||
const registration = publicRegistration(source.registration);
|
const registration = publicRegistration(source.registration);
|
||||||
const facts = connectionFacts(source.connection);
|
const facts = connectionFacts(source.connection);
|
||||||
const connected = source.connected === true || facts.connected;
|
const connected = source.connected === true || facts.connected;
|
||||||
const repairTarget = registration.operation === CALLBACK_REPAIR_OPERATION
|
const appUpdateTarget = TARGETED_APP_UPDATE_OPERATIONS.has(registration.operation)
|
||||||
&& registration.botId
|
&& registration.botId
|
||||||
&& Array.isArray(source.bots)
|
&& Array.isArray(source.bots)
|
||||||
? source.bots.find((entry) => entry?.botId === registration.botId)
|
? source.bots.find((entry) => entry?.botId === registration.botId)
|
||||||
: undefined;
|
: undefined;
|
||||||
const expectedRegistrationHost = repairTarget?.bot?.domain === 'lark'
|
const expectedRegistrationHost = appUpdateTarget?.bot?.domain === 'lark'
|
||||||
? 'open.larksuite.com'
|
? 'open.larksuite.com'
|
||||||
: 'open.feishu.cn';
|
: 'open.feishu.cn';
|
||||||
const provisioning = await publicProvisioning(
|
const provisioning = await publicProvisioning(
|
||||||
|
|
@ -350,6 +363,11 @@ function validPayload(endpoint, payload) {
|
||||||
? null
|
? null
|
||||||
: 'Callback repair requires a single valid botId.';
|
: 'Callback repair requires a single valid botId.';
|
||||||
}
|
}
|
||||||
|
if (endpoint === FEISHU_ENDPOINTS.beginGroupMessagePermission) {
|
||||||
|
return hasOnlyKeys(payload, new Set(['botId'])) && safeOpaqueId(payload.botId)
|
||||||
|
? null
|
||||||
|
: 'Group message permission update requires a single valid botId.';
|
||||||
|
}
|
||||||
if (endpoint === FEISHU_ENDPOINTS.bindCredentials) {
|
if (endpoint === FEISHU_ENDPOINTS.bindCredentials) {
|
||||||
return hasOnlyKeys(payload, new Set(['appId', 'appSecret']))
|
return hasOnlyKeys(payload, new Set(['appId', 'appSecret']))
|
||||||
&& validCredential(payload.appId, 256)
|
&& validCredential(payload.appId, 256)
|
||||||
|
|
@ -520,6 +538,20 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
|
||||||
throw new Error('Callback repair did not produce a safe QR code.');
|
throw new Error('Callback repair did not produce a safe QR code.');
|
||||||
}
|
}
|
||||||
attemptQr.set(attemptId, value.verificationUrl);
|
attemptQr.set(attemptId, value.verificationUrl);
|
||||||
|
} else if (endpoint === FEISHU_ENDPOINTS.beginGroupMessagePermission) {
|
||||||
|
if (typeof controller.startGroupMessagePermission !== 'function') {
|
||||||
|
throw new Error('Feishu group message permission update is unavailable.');
|
||||||
|
}
|
||||||
|
const started = await controller.startGroupMessagePermission(payload.botId);
|
||||||
|
const attemptId = String(publicRegistration(started?.registration).attempt);
|
||||||
|
const ready = await waitForQr(controller, started, attemptId, signal);
|
||||||
|
value = (await toPublicFeishuStatus(ready, { encodeQr: cachedEncodeQr })).provisioning;
|
||||||
|
if (!value
|
||||||
|
|| value.operation !== GROUP_MESSAGE_PERMISSION_OPERATION
|
||||||
|
|| value.botId !== payload.botId) {
|
||||||
|
throw new Error('Group message permission update did not produce a safe QR code.');
|
||||||
|
}
|
||||||
|
attemptQr.set(attemptId, value.verificationUrl);
|
||||||
} else if (endpoint === FEISHU_ENDPOINTS.pollProvisioning) {
|
} else if (endpoint === FEISHU_ENDPOINTS.pollProvisioning) {
|
||||||
const current = await statusForRegistration(controller, payload.attemptId);
|
const current = await statusForRegistration(controller, payload.attemptId);
|
||||||
if (!current || !sameAttempt(current, payload.attemptId)) {
|
if (!current || !sameAttempt(current, payload.attemptId)) {
|
||||||
|
|
@ -554,14 +586,16 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
|
||||||
after = await controller.cancelRegistration(payload.attemptId);
|
after = await controller.cancelRegistration(payload.attemptId);
|
||||||
}
|
}
|
||||||
const afterRegistration = publicRegistration(after?.registration);
|
const afterRegistration = publicRegistration(after?.registration);
|
||||||
if (registration.operation === CALLBACK_REPAIR_OPERATION
|
if (TARGETED_APP_UPDATE_OPERATIONS.has(registration.operation)
|
||||||
&& registration.state === 'saving'
|
&& registration.state === 'saving'
|
||||||
&& ['saving', 'succeeded'].includes(afterRegistration.state)) {
|
&& ['saving', 'succeeded'].includes(afterRegistration.state)) {
|
||||||
value = {
|
value = {
|
||||||
status: pollStatus(after),
|
status: pollStatus(after),
|
||||||
operation: afterRegistration.operation,
|
operation: afterRegistration.operation,
|
||||||
...(afterRegistration.botId ? { botId: afterRegistration.botId } : {}),
|
...(afterRegistration.botId ? { botId: afterRegistration.botId } : {}),
|
||||||
message: 'Callback repair was already submitted and is still being verified.',
|
message: registration.operation === GROUP_MESSAGE_PERMISSION_OPERATION
|
||||||
|
? 'The permission update was already submitted and is still being applied.'
|
||||||
|
: 'Callback repair was already submitted and is still being verified.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (value?.status !== 'connecting') {
|
if (value?.status !== 'connecting') {
|
||||||
|
|
|
||||||
71
src/channels/feishu/group-message-permission-manager.mjs
Normal file
71
src/channels/feishu/group-message-permission-manager.mjs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { RegistrationManager } from './registration-manager.mjs';
|
||||||
|
import { assertTargetedAppUpdateUrl } from './repair-manager.mjs';
|
||||||
|
|
||||||
|
export const FEISHU_GROUP_MESSAGE_SCOPE = 'im:message.group_msg';
|
||||||
|
export const GROUP_MESSAGE_PERMISSION_OPERATION = 'group_message_permission';
|
||||||
|
|
||||||
|
function accountsDomain(domain) {
|
||||||
|
return domain === 'lark' ? 'accounts.larksuite.com' : 'accounts.feishu.cn';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertGroupMessagePermissionUrl(value, expectedAppId, domain = 'feishu') {
|
||||||
|
return assertTargetedAppUpdateUrl(
|
||||||
|
value,
|
||||||
|
expectedAppId,
|
||||||
|
domain,
|
||||||
|
'Feishu group message permission update',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Incrementally grants the one sensitive tenant scope needed by all-message
|
||||||
|
* mode to an existing app. The fixed manifest prevents this UI action from
|
||||||
|
* creating another app or silently adding unrelated capabilities.
|
||||||
|
*/
|
||||||
|
export class GroupMessagePermissionManager {
|
||||||
|
#manager;
|
||||||
|
#appId;
|
||||||
|
#domain;
|
||||||
|
|
||||||
|
constructor({ registerApp, onCredentials, appId, domain = 'feishu' } = {}) {
|
||||||
|
if (typeof registerApp !== 'function') throw new TypeError('registerApp is required');
|
||||||
|
if (typeof onCredentials !== 'function') throw new TypeError('onCredentials is required');
|
||||||
|
if (typeof appId !== 'string' || !appId.trim()) throw new TypeError('appId is required');
|
||||||
|
if (domain !== 'feishu' && domain !== 'lark') throw new TypeError('domain is invalid');
|
||||||
|
|
||||||
|
this.#appId = appId.trim();
|
||||||
|
this.#domain = domain;
|
||||||
|
this.#manager = new RegistrationManager({
|
||||||
|
registerApp: (options) => registerApp({
|
||||||
|
...options,
|
||||||
|
onQRCodeReady: (info) => {
|
||||||
|
assertGroupMessagePermissionUrl(info?.url, this.#appId, this.#domain);
|
||||||
|
options.onQRCodeReady(info);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
onCredentials,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
return this.#manager.start({
|
||||||
|
source: 'deepseek-harness-group-message-permission',
|
||||||
|
domain: accountsDomain(this.#domain),
|
||||||
|
appId: this.#appId,
|
||||||
|
addons: {
|
||||||
|
preset: false,
|
||||||
|
scopes: { tenant: [FEISHU_GROUP_MESSAGE_SCOPE] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
return this.#manager.status();
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
return this.#manager.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GroupMessagePermissionManager;
|
||||||
|
|
@ -5,6 +5,10 @@ import {
|
||||||
CALLBACK_REPAIR_OPERATION,
|
CALLBACK_REPAIR_OPERATION,
|
||||||
CallbackRepairManager,
|
CallbackRepairManager,
|
||||||
} from './repair-manager.mjs';
|
} from './repair-manager.mjs';
|
||||||
|
import {
|
||||||
|
GROUP_MESSAGE_PERMISSION_OPERATION,
|
||||||
|
GroupMessagePermissionManager,
|
||||||
|
} from './group-message-permission-manager.mjs';
|
||||||
import { REQUIRED_TENANT_SCOPES } from './plugin-controller.mjs';
|
import { REQUIRED_TENANT_SCOPES } from './plugin-controller.mjs';
|
||||||
import {
|
import {
|
||||||
isFeishuGroupResponseMode,
|
isFeishuGroupResponseMode,
|
||||||
|
|
@ -15,6 +19,10 @@ const ACTIVE_REGISTRATION_STATES = new Set([
|
||||||
'starting', 'qr_ready', 'polling', 'slow_down', 'domain_switched',
|
'starting', 'qr_ready', 'polling', 'slow_down', 'domain_switched',
|
||||||
]);
|
]);
|
||||||
const MUTABLE_REGISTRATION_STATES = new Set([...ACTIVE_REGISTRATION_STATES, 'saving']);
|
const MUTABLE_REGISTRATION_STATES = new Set([...ACTIVE_REGISTRATION_STATES, 'saving']);
|
||||||
|
const TARGETED_APP_UPDATE_OPERATIONS = new Set([
|
||||||
|
CALLBACK_REPAIR_OPERATION,
|
||||||
|
GROUP_MESSAGE_PERMISSION_OPERATION,
|
||||||
|
]);
|
||||||
const ALL_VISIBLE_SENDERS = '*';
|
const ALL_VISIBLE_SENDERS = '*';
|
||||||
const DEFAULT_CALLBACK_PROBE_TIMEOUT_MS = 120_000;
|
const DEFAULT_CALLBACK_PROBE_TIMEOUT_MS = 120_000;
|
||||||
const MAX_CALLBACK_PROBE_TIMEOUT_MS = 600_000;
|
const MAX_CALLBACK_PROBE_TIMEOUT_MS = 600_000;
|
||||||
|
|
@ -81,6 +89,7 @@ function configuredBotFingerprint(config) {
|
||||||
botOpenId: config.botOpenId,
|
botOpenId: config.botOpenId,
|
||||||
activated: config.activated,
|
activated: config.activated,
|
||||||
groupResponseMode: normalizeFeishuGroupResponseMode(config.groupResponseMode),
|
groupResponseMode: normalizeFeishuGroupResponseMode(config.groupResponseMode),
|
||||||
|
groupMessagePermissionGranted: config.groupMessagePermissionGranted === true,
|
||||||
deletionPending: config.deletionPending === true,
|
deletionPending: config.deletionPending === true,
|
||||||
connectedAt: config.connectedAt ?? null,
|
connectedAt: config.connectedAt ?? null,
|
||||||
createdAt: config.createdAt ?? null,
|
createdAt: config.createdAt ?? null,
|
||||||
|
|
@ -108,7 +117,7 @@ export class MultiBotDshFeishuController {
|
||||||
#runtimes = new Map();
|
#runtimes = new Map();
|
||||||
#botErrors = new Map();
|
#botErrors = new Map();
|
||||||
#registrations = new Map();
|
#registrations = new Map();
|
||||||
#activeRepairs = new Map();
|
#activeAppUpdates = new Map();
|
||||||
#botOwnership = new Map();
|
#botOwnership = new Map();
|
||||||
#latestRegistrationId = null;
|
#latestRegistrationId = null;
|
||||||
#configTransition = Promise.resolve();
|
#configTransition = Promise.resolve();
|
||||||
|
|
@ -239,12 +248,15 @@ export class MultiBotDshFeishuController {
|
||||||
const target = this.#requireBot(botId);
|
const target = this.#requireBot(botId);
|
||||||
if (target.deletionPending) throw new Error('Cannot repair a Feishu bot pending deletion');
|
if (target.deletionPending) throw new Error('Cannot repair a Feishu bot pending deletion');
|
||||||
|
|
||||||
const activeId = this.#activeRepairs.get(botId);
|
const activeId = this.#activeAppUpdates.get(botId);
|
||||||
const active = activeId ? this.#registrations.get(activeId) : null;
|
const active = activeId ? this.#registrations.get(activeId) : null;
|
||||||
if (active && MUTABLE_REGISTRATION_STATES.has(active.manager.status().state)) {
|
if (active && MUTABLE_REGISTRATION_STATES.has(active.manager.status().state)) {
|
||||||
|
if (active.operation !== CALLBACK_REPAIR_OPERATION) {
|
||||||
|
throw new Error('Another Feishu app update is already active for this bot');
|
||||||
|
}
|
||||||
return this.registrationStatus(active.id);
|
return this.registrationStatus(active.id);
|
||||||
}
|
}
|
||||||
this.#activeRepairs.delete(botId);
|
this.#activeAppUpdates.delete(botId);
|
||||||
|
|
||||||
const id = this.#createRegistrationId();
|
const id = this.#createRegistrationId();
|
||||||
if (typeof id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(id) || this.#registrations.has(id)) {
|
if (typeof id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(id) || this.#registrations.has(id)) {
|
||||||
|
|
@ -283,7 +295,66 @@ export class MultiBotDshFeishuController {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.#registrations.set(id, record);
|
this.#registrations.set(id, record);
|
||||||
this.#activeRepairs.set(botId, id);
|
this.#activeAppUpdates.set(botId, id);
|
||||||
|
this.#latestRegistrationId = id;
|
||||||
|
this.#trimRegistrations();
|
||||||
|
record.manager.start();
|
||||||
|
this.#touch();
|
||||||
|
return this.registrationStatus(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
startGroupMessagePermission(botId) {
|
||||||
|
this.#assertOpen();
|
||||||
|
const target = this.#requireBot(botId);
|
||||||
|
if (target.deletionPending) {
|
||||||
|
throw new Error('Cannot update permissions for a Feishu bot pending deletion');
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeId = this.#activeAppUpdates.get(botId);
|
||||||
|
const active = activeId ? this.#registrations.get(activeId) : null;
|
||||||
|
if (active && MUTABLE_REGISTRATION_STATES.has(active.manager.status().state)) {
|
||||||
|
if (active.operation !== GROUP_MESSAGE_PERMISSION_OPERATION) {
|
||||||
|
throw new Error('Another Feishu app update is already active for this bot');
|
||||||
|
}
|
||||||
|
return this.registrationStatus(active.id);
|
||||||
|
}
|
||||||
|
this.#activeAppUpdates.delete(botId);
|
||||||
|
|
||||||
|
const id = this.#createRegistrationId();
|
||||||
|
if (typeof id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(id) || this.#registrations.has(id)) {
|
||||||
|
throw new Error('Registration id generator returned an invalid or duplicate id');
|
||||||
|
}
|
||||||
|
const record = {
|
||||||
|
id,
|
||||||
|
operation: GROUP_MESSAGE_PERMISSION_OPERATION,
|
||||||
|
manager: null,
|
||||||
|
botId,
|
||||||
|
createdNew: false,
|
||||||
|
cancelled: false,
|
||||||
|
remoteCommitted: false,
|
||||||
|
processing: null,
|
||||||
|
publicError: null,
|
||||||
|
stage: 'authorizing',
|
||||||
|
target: structuredClone(target),
|
||||||
|
targetFingerprint: configuredBotFingerprint(target),
|
||||||
|
initiator: { actorOpenId: null, chatId: null },
|
||||||
|
};
|
||||||
|
record.manager = new GroupMessagePermissionManager({
|
||||||
|
registerApp: this.#registerApp,
|
||||||
|
appId: target.appId,
|
||||||
|
domain: target.domain,
|
||||||
|
onCredentials: (result) => {
|
||||||
|
record.remoteCommitted = true;
|
||||||
|
const processing = this.#acceptGroupMessagePermission(record, result);
|
||||||
|
const tracked = processing.finally(() => {
|
||||||
|
if (record.processing === tracked) record.processing = null;
|
||||||
|
});
|
||||||
|
record.processing = tracked;
|
||||||
|
return tracked;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.#registrations.set(id, record);
|
||||||
|
this.#activeAppUpdates.set(botId, id);
|
||||||
this.#latestRegistrationId = id;
|
this.#latestRegistrationId = id;
|
||||||
this.#trimRegistrations();
|
this.#trimRegistrations();
|
||||||
record.manager.start();
|
record.manager.start();
|
||||||
|
|
@ -308,7 +379,7 @@ export class MultiBotDshFeishuController {
|
||||||
// Once registerApp has returned, the platform-side update has committed.
|
// Once registerApp has returned, the platform-side update has committed.
|
||||||
// A repair must finish converging the returned credential and callback
|
// A repair must finish converging the returned credential and callback
|
||||||
// probe; cancelling here cannot roll that remote mutation back.
|
// probe; cancelling here cannot roll that remote mutation back.
|
||||||
if (record.operation === CALLBACK_REPAIR_OPERATION && state === 'saving') {
|
if (TARGETED_APP_UPDATE_OPERATIONS.has(record.operation) && state === 'saving') {
|
||||||
return this.registrationStatus(attemptId);
|
return this.registrationStatus(attemptId);
|
||||||
}
|
}
|
||||||
if (!MUTABLE_REGISTRATION_STATES.has(state)) {
|
if (!MUTABLE_REGISTRATION_STATES.has(state)) {
|
||||||
|
|
@ -485,6 +556,11 @@ export class MultiBotDshFeishuController {
|
||||||
}
|
}
|
||||||
return this.#serializeConfig(() => this.#withBotTransition(botId, async () => {
|
return this.#serializeConfig(() => this.#withBotTransition(botId, async () => {
|
||||||
const config = this.#requireBot(botId);
|
const config = this.#requireBot(botId);
|
||||||
|
if (groupResponseMode === 'all' && config.groupMessagePermissionGranted !== true) {
|
||||||
|
const error = new Error('Authorize im:message.group_msg before enabling all group messages');
|
||||||
|
error.code = 'group_message_permission_required';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
const saved = await this.#configStore.saveBot({ ...config, groupResponseMode });
|
const saved = await this.#configStore.saveBot({ ...config, groupResponseMode });
|
||||||
this.#runtimes.get(botId)?.setGroupResponseMode?.(saved.groupResponseMode);
|
this.#runtimes.get(botId)?.setGroupResponseMode?.(saved.groupResponseMode);
|
||||||
this.#touch();
|
this.#touch();
|
||||||
|
|
@ -516,14 +592,14 @@ export class MultiBotDshFeishuController {
|
||||||
async close() {
|
async close() {
|
||||||
if (this.#closed) return;
|
if (this.#closed) return;
|
||||||
this.#closed = true;
|
this.#closed = true;
|
||||||
const repairProcessing = [];
|
const appUpdateProcessing = [];
|
||||||
for (const record of this.#registrations.values()) {
|
for (const record of this.#registrations.values()) {
|
||||||
const state = record.manager.status().state;
|
const state = record.manager.status().state;
|
||||||
if (record.operation === CALLBACK_REPAIR_OPERATION && state === 'saving') {
|
if (TARGETED_APP_UPDATE_OPERATIONS.has(record.operation) && state === 'saving') {
|
||||||
// Stop projecting an in-flight repair, but do not request its local
|
// Stop projecting an in-flight repair, but do not request its local
|
||||||
// credential rollback after the remote update has committed.
|
// credential rollback after the remote update has committed.
|
||||||
record.manager.cancel();
|
record.manager.cancel();
|
||||||
if (record.processing) repairProcessing.push(record.processing);
|
if (record.processing) appUpdateProcessing.push(record.processing);
|
||||||
} else if (MUTABLE_REGISTRATION_STATES.has(state)) {
|
} else if (MUTABLE_REGISTRATION_STATES.has(state)) {
|
||||||
record.cancelled = true;
|
record.cancelled = true;
|
||||||
record.manager.cancel();
|
record.manager.cancel();
|
||||||
|
|
@ -532,7 +608,7 @@ export class MultiBotDshFeishuController {
|
||||||
await this.#configTransition;
|
await this.#configTransition;
|
||||||
await Promise.allSettled([...this.#botTransitions.values()]);
|
await Promise.allSettled([...this.#botTransitions.values()]);
|
||||||
await Promise.allSettled([...this.#runtimes.keys()].map((id) => this.#stopRuntime(id)));
|
await Promise.allSettled([...this.#runtimes.keys()].map((id) => this.#stopRuntime(id)));
|
||||||
await Promise.allSettled(repairProcessing);
|
await Promise.allSettled(appUpdateProcessing);
|
||||||
// A committed repair can be between SDK completion and its serialized
|
// A committed repair can be between SDK completion and its serialized
|
||||||
// credential/runtime transition when close begins. Waiting for the repair
|
// credential/runtime transition when close begins. Waiting for the repair
|
||||||
// can therefore create a replacement runtime after the first drain. Drain
|
// can therefore create a replacement runtime after the first drain. Drain
|
||||||
|
|
@ -554,6 +630,7 @@ export class MultiBotDshFeishuController {
|
||||||
connected,
|
connected,
|
||||||
configured: true,
|
configured: true,
|
||||||
groupResponseMode: normalizeFeishuGroupResponseMode(config.groupResponseMode),
|
groupResponseMode: normalizeFeishuGroupResponseMode(config.groupResponseMode),
|
||||||
|
groupMessagePermissionGranted: config.groupMessagePermissionGranted === true,
|
||||||
bot: publicBot(config),
|
bot: publicBot(config),
|
||||||
connection,
|
connection,
|
||||||
error,
|
error,
|
||||||
|
|
@ -605,6 +682,158 @@ export class MultiBotDshFeishuController {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async #acceptGroupMessagePermission(record, result) {
|
||||||
|
const appId = result.client_id;
|
||||||
|
const appSecret = result.client_secret;
|
||||||
|
const ownerOpenId = optionalNonEmptyString(result.user_info?.open_id);
|
||||||
|
const tenantBrand = result.user_info?.tenant_brand;
|
||||||
|
const target = record.target;
|
||||||
|
|
||||||
|
if (record.cancelled) {
|
||||||
|
throw this.#callbackRepairError(
|
||||||
|
record,
|
||||||
|
'abort',
|
||||||
|
'Group message permission update was cancelled before local activation.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (appId !== target.appId) {
|
||||||
|
throw this.#callbackRepairError(
|
||||||
|
record,
|
||||||
|
'repair_app_mismatch',
|
||||||
|
'Feishu returned credentials for a different app.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!ownerOpenId) {
|
||||||
|
throw this.#callbackRepairError(
|
||||||
|
record,
|
||||||
|
'repair_owner_missing',
|
||||||
|
'Feishu returned no permission operator identity.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (tenantBrand !== undefined && tenantBrand !== target.domain) {
|
||||||
|
throw this.#callbackRepairError(
|
||||||
|
record,
|
||||||
|
'repair_domain_mismatch',
|
||||||
|
'Feishu returned credentials for a different account domain.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!target.ownerOpenIds.includes(ALL_VISIBLE_SENDERS)
|
||||||
|
&& !target.ownerOpenIds.includes(ownerOpenId)) {
|
||||||
|
throw this.#callbackRepairError(
|
||||||
|
record,
|
||||||
|
'repair_owner_mismatch',
|
||||||
|
'The Feishu permission operator is not an owner of this configured bot.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
record.stage = 'verifying_identity';
|
||||||
|
let verified;
|
||||||
|
try {
|
||||||
|
verified = await this.#verifyApp({ appId, appSecret, domain: target.domain });
|
||||||
|
} catch (error) {
|
||||||
|
throw this.#callbackRepairError(
|
||||||
|
record,
|
||||||
|
'repair_credentials_invalid',
|
||||||
|
'Feishu could not verify the updated app credentials.',
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (target.botOpenId && verified?.openId !== target.botOpenId) {
|
||||||
|
throw this.#callbackRepairError(
|
||||||
|
record,
|
||||||
|
'repair_bot_mismatch',
|
||||||
|
'The updated Feishu app belongs to a different bot identity.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.#serializeConfig(() => this.#withBotTransition(record.botId, async () => {
|
||||||
|
const current = this.#configStore.getBot(record.botId);
|
||||||
|
if (!current
|
||||||
|
|| current.deletionPending
|
||||||
|
|| configuredBotFingerprint(current) !== record.targetFingerprint) {
|
||||||
|
throw this.#callbackRepairError(
|
||||||
|
record,
|
||||||
|
'repair_target_changed',
|
||||||
|
'The Feishu bot changed while its permission update was in progress.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let previous;
|
||||||
|
try {
|
||||||
|
previous = await this.#credentials.resolve(current.secretRef);
|
||||||
|
} catch (error) {
|
||||||
|
throw this.#callbackRepairError(
|
||||||
|
record,
|
||||||
|
'credential_update_failed',
|
||||||
|
'Unable to read the current Feishu credential.',
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (previous?.value !== appSecret) {
|
||||||
|
record.stage = 'persisting_secret';
|
||||||
|
try {
|
||||||
|
await this.#credentials.set(current.secretRef, appSecret);
|
||||||
|
} catch (writeError) {
|
||||||
|
const observed = await this.#credentials.resolve(current.secretRef).catch(() => null);
|
||||||
|
if (observed?.value !== appSecret) {
|
||||||
|
throw this.#callbackRepairError(
|
||||||
|
record,
|
||||||
|
'credential_update_failed',
|
||||||
|
'Unable to store the updated Feishu credential.',
|
||||||
|
writeError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const persisted = await this.#credentials.resolve(current.secretRef).catch(() => null);
|
||||||
|
if (persisted?.value !== appSecret) {
|
||||||
|
throw this.#callbackRepairError(
|
||||||
|
record,
|
||||||
|
'credential_state_unknown',
|
||||||
|
'The updated Feishu credential could not be confirmed after writing.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record.stage = 'enabling_all_messages';
|
||||||
|
let saved;
|
||||||
|
try {
|
||||||
|
saved = await this.#configStore.saveBot({
|
||||||
|
...current,
|
||||||
|
groupMessagePermissionGranted: true,
|
||||||
|
groupResponseMode: 'all',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw this.#callbackRepairError(
|
||||||
|
record,
|
||||||
|
'group_message_permission_save_failed',
|
||||||
|
'The permission was accepted, but all-message mode could not be saved.',
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
record.stage = 'restarting';
|
||||||
|
try {
|
||||||
|
await this.#startRuntime(saved, appSecret);
|
||||||
|
} catch (error) {
|
||||||
|
this.#botErrors.set(record.botId, {
|
||||||
|
code: 'connection_failed',
|
||||||
|
message: '群消息权限已开通,但机器人长连接未就绪,请点击重试。',
|
||||||
|
});
|
||||||
|
this.#touch();
|
||||||
|
throw this.#callbackRepairError(
|
||||||
|
record,
|
||||||
|
'group_message_permission_connection_failed',
|
||||||
|
'The permission was accepted, but the Feishu runtime could not restart.',
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.#botErrors.delete(record.botId);
|
||||||
|
record.stage = 'verified';
|
||||||
|
record.publicError = null;
|
||||||
|
this.#touch();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
async #acceptCallbackRepair(record, result) {
|
async #acceptCallbackRepair(record, result) {
|
||||||
const appId = result.client_id;
|
const appId = result.client_id;
|
||||||
const appSecret = result.client_secret;
|
const appSecret = result.client_secret;
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ function normalizeBot(value, { legacy = false } = {}) {
|
||||||
botOpenId: cleanString(value.botOpenId),
|
botOpenId: cleanString(value.botOpenId),
|
||||||
activated: value.activated ?? null,
|
activated: value.activated ?? null,
|
||||||
groupResponseMode: normalizeFeishuGroupResponseMode(value.groupResponseMode),
|
groupResponseMode: normalizeFeishuGroupResponseMode(value.groupResponseMode),
|
||||||
|
groupMessagePermissionGranted: value.groupMessagePermissionGranted === true,
|
||||||
deletionPending: value.deletionPending === true,
|
deletionPending: value.deletionPending === true,
|
||||||
connectedAt: cleanString(value.connectedAt),
|
connectedAt: cleanString(value.connectedAt),
|
||||||
createdAt: cleanString(value.createdAt) ?? cleanString(value.connectedAt),
|
createdAt: cleanString(value.createdAt) ?? cleanString(value.connectedAt),
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,17 @@ function launcherDomain(domain) {
|
||||||
* never fall back to the create-only flow. This catches regressions such as a
|
* never fall back to the create-only flow. This catches regressions such as a
|
||||||
* literal `{{client_id}}` before the broken URL reaches the browser.
|
* literal `{{client_id}}` before the broken URL reaches the browser.
|
||||||
*/
|
*/
|
||||||
export function assertCallbackRepairUrl(value, expectedAppId, domain = 'feishu') {
|
export function assertTargetedAppUpdateUrl(
|
||||||
|
value,
|
||||||
|
expectedAppId,
|
||||||
|
domain = 'feishu',
|
||||||
|
operationLabel = 'Feishu app update',
|
||||||
|
) {
|
||||||
let url;
|
let url;
|
||||||
try {
|
try {
|
||||||
url = new URL(value);
|
url = new URL(value);
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error('Feishu callback repair returned an invalid verification URL');
|
throw new Error(`${operationLabel} returned an invalid verification URL`);
|
||||||
}
|
}
|
||||||
const supportedDomain = domain === 'feishu' || domain === 'lark';
|
const supportedDomain = domain === 'feishu' || domain === 'lark';
|
||||||
const clientIds = url.searchParams.getAll('clientID');
|
const clientIds = url.searchParams.getAll('clientID');
|
||||||
|
|
@ -49,11 +54,20 @@ export function assertCallbackRepairUrl(value, expectedAppId, domain = 'feishu')
|
||||||
|| addons.length !== 1
|
|| addons.length !== 1
|
||||||
|| !addons[0]?.trim()
|
|| !addons[0]?.trim()
|
||||||
|| hasPlaceholder) {
|
|| hasPlaceholder) {
|
||||||
throw new Error('Feishu callback repair returned an unsafe verification URL');
|
throw new Error(`${operationLabel} returned an unsafe verification URL`);
|
||||||
}
|
}
|
||||||
return url.toString();
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function assertCallbackRepairUrl(value, expectedAppId, domain = 'feishu') {
|
||||||
|
return assertTargetedAppUpdateUrl(
|
||||||
|
value,
|
||||||
|
expectedAppId,
|
||||||
|
domain,
|
||||||
|
'Feishu callback repair',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One targeted update attempt for an existing Feishu app. It intentionally
|
* One targeted update attempt for an existing Feishu app. It intentionally
|
||||||
* shares RegistrationManager's polling/state implementation while fixing the
|
* shares RegistrationManager's polling/state implementation while fixing the
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
|
|
||||||
test('multi-bot endpoints are bot-scoped and keep legacy operations separate', () => {
|
test('multi-bot endpoints are bot-scoped and keep legacy operations separate', () => {
|
||||||
assert.equal(FEISHU_ENDPOINTS.beginCallbackRepair, 'bot.callback-repair.begin');
|
assert.equal(FEISHU_ENDPOINTS.beginCallbackRepair, 'bot.callback-repair.begin');
|
||||||
|
assert.equal(FEISHU_ENDPOINTS.beginGroupMessagePermission, 'bot.group-message-permission.begin');
|
||||||
assert.equal(FEISHU_ENDPOINTS.reconnectBot, 'bot.reconnect');
|
assert.equal(FEISHU_ENDPOINTS.reconnectBot, 'bot.reconnect');
|
||||||
assert.equal(FEISHU_ENDPOINTS.disconnectBot, 'bot.disconnect');
|
assert.equal(FEISHU_ENDPOINTS.disconnectBot, 'bot.disconnect');
|
||||||
assert.equal(FEISHU_ENDPOINTS.deleteBot, 'bot.delete');
|
assert.equal(FEISHU_ENDPOINTS.deleteBot, 'bot.delete');
|
||||||
|
|
@ -35,6 +36,7 @@ test('client normalizes multiple independent bots and derives authoritative tota
|
||||||
connected: true,
|
connected: true,
|
||||||
configured: true,
|
configured: true,
|
||||||
groupResponseMode: 'all',
|
groupResponseMode: 'all',
|
||||||
|
groupMessagePermissionGranted: true,
|
||||||
bot: {
|
bot: {
|
||||||
name: '销售助手',
|
name: '销售助手',
|
||||||
appIdMasked: 'cli_aaaa••••1111',
|
appIdMasked: 'cli_aaaa••••1111',
|
||||||
|
|
@ -61,7 +63,9 @@ test('client normalizes multiple independent bots and derives authoritative tota
|
||||||
assert.deepEqual(snapshot.totals, { configured: 2, connected: 1 });
|
assert.deepEqual(snapshot.totals, { configured: 2, connected: 1 });
|
||||||
assert.equal(snapshot.bots[0].state, 'connected');
|
assert.equal(snapshot.bots[0].state, 'connected');
|
||||||
assert.equal(snapshot.bots[0].groupResponseMode, 'all');
|
assert.equal(snapshot.bots[0].groupResponseMode, 'all');
|
||||||
|
assert.equal(snapshot.bots[0].groupMessagePermissionGranted, true);
|
||||||
assert.equal(snapshot.bots[1].groupResponseMode, 'mention');
|
assert.equal(snapshot.bots[1].groupResponseMode, 'mention');
|
||||||
|
assert.equal(snapshot.bots[1].groupMessagePermissionGranted, false);
|
||||||
assert.equal(snapshot.bots[1].state, 'connecting');
|
assert.equal(snapshot.bots[1].state, 'connecting');
|
||||||
assert.equal(snapshot.bots[1].bot.domain, 'lark');
|
assert.equal(snapshot.bots[1].bot.domain, 'lark');
|
||||||
assert.equal(snapshot.bots[1].error.message, '连接失败');
|
assert.equal(snapshot.bots[1].error.message, '连接失败');
|
||||||
|
|
@ -170,6 +174,31 @@ test('client restores a submitted callback repair without requiring an expired Q
|
||||||
assert.equal(provisioning.qrCodeDataUrl, undefined);
|
assert.equal(provisioning.qrCodeDataUrl, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('client preserves group-message permission identity across QR and poll projections', () => {
|
||||||
|
const provisioning = normalizeProvisioning({
|
||||||
|
attemptId: 'reg_group_permission',
|
||||||
|
operation: 'group_message_permission',
|
||||||
|
botId: 'bot_target',
|
||||||
|
verificationUrl: 'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_target&addons=x',
|
||||||
|
qrCodeDataUrl: 'data:image/png;base64,AAAA',
|
||||||
|
});
|
||||||
|
assert.equal(provisioning.operation, 'group_message_permission');
|
||||||
|
assert.equal(provisioning.botId, 'bot_target');
|
||||||
|
|
||||||
|
const poll = normalizePollResult({
|
||||||
|
status: 'connecting',
|
||||||
|
operation: 'group_message_permission',
|
||||||
|
botId: 'bot_target',
|
||||||
|
});
|
||||||
|
assert.equal(poll.operation, 'group_message_permission');
|
||||||
|
assert.equal(poll.botId, 'bot_target');
|
||||||
|
assert.throws(() => normalizeProvisioning({
|
||||||
|
attemptId: 'reg_broken_permission',
|
||||||
|
operation: 'group_message_permission',
|
||||||
|
verificationUrl: 'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_target&addons=x',
|
||||||
|
}), /botId/);
|
||||||
|
});
|
||||||
|
|
||||||
test('client unwraps RpcResult and redacts credential-shaped error text', () => {
|
test('client unwraps RpcResult and redacts credential-shaped error text', () => {
|
||||||
assert.deepEqual(unwrapRpcResult({ ok: true, value: { connected: true } }), {
|
assert.deepEqual(unwrapRpcResult({ ok: true, value: { connected: true } }), {
|
||||||
connected: true,
|
connected: true,
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,8 @@ test('Feishu connection check requests and displays test-message feedback', asyn
|
||||||
assert.match(markup, /aria-label="修复飞书测试机器人的卡片按钮"/);
|
assert.match(markup, /aria-label="修复飞书测试机器人的卡片按钮"/);
|
||||||
assert.match(markup, /<select[^>]*aria-label="群聊响应方式"/);
|
assert.match(markup, /<select[^>]*aria-label="群聊响应方式"/);
|
||||||
assert.match(markup, /仅在 @机器人时响应(推荐)/);
|
assert.match(markup, /仅在 @机器人时响应(推荐)/);
|
||||||
assert.match(markup, /响应所有群消息(需飞书敏感权限)/);
|
assert.match(markup, /响应所有群消息/);
|
||||||
|
assert.match(markup, /选择全部消息后会打开飞书官方授权流程/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Feishu bot card saves group response mode from a dropdown', async () => {
|
test('Feishu bot card saves group response mode from a dropdown', async () => {
|
||||||
|
|
@ -91,6 +92,221 @@ test('Feishu bot card saves group response mode from a dropdown', async () => {
|
||||||
await act(async () => renderer.unmount());
|
await act(async () => renderer.unmount());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Feishu bot card offers authorization recovery for all-message mode', () => {
|
||||||
|
const baseConnection = {
|
||||||
|
botId: 'bot-permission-recovery',
|
||||||
|
state: 'connected',
|
||||||
|
connected: true,
|
||||||
|
groupResponseMode: 'all',
|
||||||
|
bot: { name: '权限恢复机器人', appIdMasked: 'cli_reco••••very' },
|
||||||
|
health: { summary: '长连接运行正常', lastCheckedAt: Date.now() },
|
||||||
|
};
|
||||||
|
const reauthorizeMarkup = renderToStaticMarkup(React.createElement(BotCard, {
|
||||||
|
connection: { ...baseConnection, groupMessagePermissionGranted: true },
|
||||||
|
onReconnect() {},
|
||||||
|
onRequestRemove() {},
|
||||||
|
onConfirmRemove() {},
|
||||||
|
onCancelRemove() {},
|
||||||
|
}));
|
||||||
|
assert.match(reauthorizeMarkup, /aria-label="重新授权群消息权限"/);
|
||||||
|
assert.match(reauthorizeMarkup, />重新授权</);
|
||||||
|
|
||||||
|
const legacyMarkup = renderToStaticMarkup(React.createElement(BotCard, {
|
||||||
|
connection: { ...baseConnection, groupMessagePermissionGranted: false },
|
||||||
|
onReconnect() {},
|
||||||
|
onRequestRemove() {},
|
||||||
|
onConfirmRemove() {},
|
||||||
|
onCancelRemove() {},
|
||||||
|
}));
|
||||||
|
assert.match(legacyMarkup, /尚未确认“获取群组中所有消息”权限/);
|
||||||
|
assert.match(legacyMarkup, />去授权</);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('selecting all group messages opens the official permission flow before saving mode', async (t) => {
|
||||||
|
const previousWindow = globalThis.window;
|
||||||
|
let nextTimer = 0;
|
||||||
|
const frames = new Map();
|
||||||
|
globalThis.window = {
|
||||||
|
setInterval() { return ++nextTimer; },
|
||||||
|
clearInterval() {},
|
||||||
|
setTimeout() { return ++nextTimer; },
|
||||||
|
clearTimeout() {},
|
||||||
|
requestAnimationFrame(callback) {
|
||||||
|
const id = ++nextTimer;
|
||||||
|
frames.set(id, callback);
|
||||||
|
queueMicrotask(() => {
|
||||||
|
const pending = frames.get(id);
|
||||||
|
if (!pending) return;
|
||||||
|
frames.delete(id);
|
||||||
|
pending();
|
||||||
|
});
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
cancelAnimationFrame(id) { frames.delete(id); },
|
||||||
|
};
|
||||||
|
t.after(() => {
|
||||||
|
if (previousWindow === undefined) delete globalThis.window;
|
||||||
|
else globalThis.window = previousWindow;
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshot = {
|
||||||
|
schemaVersion: 2,
|
||||||
|
revision: 1,
|
||||||
|
state: 'connected',
|
||||||
|
bots: [{
|
||||||
|
botId: 'bot_before_permission',
|
||||||
|
state: 'connected',
|
||||||
|
connected: true,
|
||||||
|
configured: true,
|
||||||
|
groupResponseMode: 'mention',
|
||||||
|
groupMessagePermissionGranted: false,
|
||||||
|
bot: { name: '前一个机器人', appIdMasked: 'cli_bef••••ore' },
|
||||||
|
health: { status: 'healthy', summary: '长连接运行正常' },
|
||||||
|
}, {
|
||||||
|
botId: 'bot_group_permission',
|
||||||
|
state: 'connected',
|
||||||
|
connected: true,
|
||||||
|
configured: true,
|
||||||
|
groupResponseMode: 'mention',
|
||||||
|
groupMessagePermissionGranted: false,
|
||||||
|
bot: { name: '权限机器人', appIdMasked: 'cli_per••••sion' },
|
||||||
|
health: { status: 'healthy', summary: '长连接运行正常' },
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
const calls = [];
|
||||||
|
const rpcCall = async (endpoint, payload) => {
|
||||||
|
calls.push({ endpoint, payload });
|
||||||
|
if (endpoint === FEISHU_ENDPOINTS.status) return { ok: true, value: snapshot };
|
||||||
|
if (endpoint === FEISHU_ENDPOINTS.beginGroupMessagePermission) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
attemptId: 'reg_group_permission',
|
||||||
|
operation: 'group_message_permission',
|
||||||
|
botId: 'bot_group_permission',
|
||||||
|
verificationUrl: 'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_permission&addons=encoded',
|
||||||
|
qrCodeDataUrl: 'data:image/png;base64,AAAA',
|
||||||
|
expiresAt: Date.now() + 60_000,
|
||||||
|
pollIntervalMs: 800,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected endpoint: ${endpoint}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
let renderer;
|
||||||
|
await act(async () => {
|
||||||
|
renderer = create(React.createElement(FeishuSettingsTab, { rpcCall }));
|
||||||
|
await flushMicrotasks();
|
||||||
|
});
|
||||||
|
const targetCard = () => renderer.root.findByProps({ 'data-bot-id': 'bot_group_permission' });
|
||||||
|
const select = targetCard().findByProps({ 'aria-label': '群聊响应方式' });
|
||||||
|
await act(async () => {
|
||||||
|
select.props.onChange({ target: { value: 'all' } });
|
||||||
|
await flushMicrotasks();
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.ok(calls.some(({ endpoint, payload }) => (
|
||||||
|
endpoint === FEISHU_ENDPOINTS.beginGroupMessagePermission
|
||||||
|
&& payload.botId === 'bot_group_permission'
|
||||||
|
)));
|
||||||
|
assert.equal(calls.some(({ endpoint }) => endpoint === FEISHU_ENDPOINTS.setGroupResponseMode), false);
|
||||||
|
const permissionPanel = targetCard().findByProps({
|
||||||
|
'data-provision-for': 'bot_group_permission',
|
||||||
|
});
|
||||||
|
assert.equal(permissionPanel.findByType('a').props.href,
|
||||||
|
'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_permission&addons=encoded');
|
||||||
|
assert.match(textOf(permissionPanel), /只增量开通“获取群组中所有消息”权限/);
|
||||||
|
assert.equal(renderer.root.findByProps({ 'data-bot-id': 'bot_before_permission' })
|
||||||
|
.findAllByProps({ 'data-provision-for': 'bot_group_permission' }).length, 0);
|
||||||
|
assert.equal(targetCard().findByProps({ 'aria-label': '群聊响应方式' }).props.value, 'mention');
|
||||||
|
await act(async () => renderer.unmount());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reauthorizing all-message mode starts the same bot-scoped permission flow', async (t) => {
|
||||||
|
const previousWindow = globalThis.window;
|
||||||
|
let nextTimer = 0;
|
||||||
|
const frames = new Map();
|
||||||
|
globalThis.window = {
|
||||||
|
setInterval() { return ++nextTimer; },
|
||||||
|
clearInterval() {},
|
||||||
|
setTimeout() { return ++nextTimer; },
|
||||||
|
clearTimeout() {},
|
||||||
|
requestAnimationFrame(callback) {
|
||||||
|
const id = ++nextTimer;
|
||||||
|
frames.set(id, callback);
|
||||||
|
queueMicrotask(() => {
|
||||||
|
const pending = frames.get(id);
|
||||||
|
if (!pending) return;
|
||||||
|
frames.delete(id);
|
||||||
|
pending();
|
||||||
|
});
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
cancelAnimationFrame(id) { frames.delete(id); },
|
||||||
|
};
|
||||||
|
t.after(() => {
|
||||||
|
if (previousWindow === undefined) delete globalThis.window;
|
||||||
|
else globalThis.window = previousWindow;
|
||||||
|
});
|
||||||
|
|
||||||
|
const snapshot = {
|
||||||
|
schemaVersion: 2,
|
||||||
|
revision: 1,
|
||||||
|
state: 'connected',
|
||||||
|
bots: [{
|
||||||
|
botId: 'bot_reauthorize',
|
||||||
|
state: 'connected',
|
||||||
|
connected: true,
|
||||||
|
configured: true,
|
||||||
|
groupResponseMode: 'all',
|
||||||
|
groupMessagePermissionGranted: true,
|
||||||
|
bot: { name: '重新授权机器人', appIdMasked: 'cli_reau••••thorize' },
|
||||||
|
health: { status: 'healthy', summary: '长连接运行正常' },
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
const calls = [];
|
||||||
|
const rpcCall = async (endpoint, payload) => {
|
||||||
|
calls.push({ endpoint, payload });
|
||||||
|
if (endpoint === FEISHU_ENDPOINTS.status) return { ok: true, value: snapshot };
|
||||||
|
if (endpoint === FEISHU_ENDPOINTS.beginGroupMessagePermission) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
attemptId: 'reg_reauthorize',
|
||||||
|
operation: 'group_message_permission',
|
||||||
|
botId: 'bot_reauthorize',
|
||||||
|
verificationUrl: 'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_reauthorize&addons=encoded',
|
||||||
|
qrCodeDataUrl: 'data:image/png;base64,AAAA',
|
||||||
|
expiresAt: Date.now() + 60_000,
|
||||||
|
pollIntervalMs: 800,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected endpoint: ${endpoint}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
let renderer;
|
||||||
|
await act(async () => {
|
||||||
|
renderer = create(React.createElement(FeishuSettingsTab, { rpcCall }));
|
||||||
|
await flushMicrotasks();
|
||||||
|
});
|
||||||
|
const targetCard = () => renderer.root.findByProps({ 'data-bot-id': 'bot_reauthorize' });
|
||||||
|
await act(async () => {
|
||||||
|
targetCard().findByProps({ 'aria-label': '重新授权群消息权限' }).props.onClick();
|
||||||
|
await flushMicrotasks();
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.ok(calls.some(({ endpoint, payload }) => (
|
||||||
|
endpoint === FEISHU_ENDPOINTS.beginGroupMessagePermission
|
||||||
|
&& payload.botId === 'bot_reauthorize'
|
||||||
|
)));
|
||||||
|
assert.equal(calls.some(({ endpoint }) => endpoint === FEISHU_ENDPOINTS.setGroupResponseMode), false);
|
||||||
|
assert.match(textOf(targetCard().findByProps({ 'data-provision-for': 'bot_reauthorize' })),
|
||||||
|
/正在为「重新授权机器人」开通群消息权限/);
|
||||||
|
await act(async () => renderer.unmount());
|
||||||
|
});
|
||||||
|
|
||||||
test('Feishu callback repair keeps a Host-submitted attempt when a stale QR cancel races saving', async (t) => {
|
test('Feishu callback repair keeps a Host-submitted attempt when a stale QR cancel races saving', async (t) => {
|
||||||
const previousWindow = globalThis.window;
|
const previousWindow = globalThis.window;
|
||||||
let nextTimer = 0;
|
let nextTimer = 0;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import {
|
||||||
|
FEISHU_GROUP_MESSAGE_SCOPE,
|
||||||
|
GroupMessagePermissionManager,
|
||||||
|
assertGroupMessagePermissionUrl,
|
||||||
|
} from '../../../src/channels/feishu/group-message-permission-manager.mjs';
|
||||||
|
|
||||||
|
const flush = () => new Promise((resolve) => setImmediate(resolve));
|
||||||
|
|
||||||
|
async function waitFor(predicate, timeoutMs = 1000) {
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
while (!predicate()) {
|
||||||
|
if (Date.now() >= deadline) throw new Error('condition timed out');
|
||||||
|
await flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('GroupMessagePermissionManager updates one real app with only im:message.group_msg', async () => {
|
||||||
|
let observed;
|
||||||
|
let resolveRegistration;
|
||||||
|
const accepted = [];
|
||||||
|
const manager = new GroupMessagePermissionManager({
|
||||||
|
appId: 'cli_real_app',
|
||||||
|
domain: 'feishu',
|
||||||
|
registerApp(options) {
|
||||||
|
observed = options;
|
||||||
|
return new Promise((resolve) => { resolveRegistration = resolve; });
|
||||||
|
},
|
||||||
|
onCredentials: async (result) => { accepted.push(result); },
|
||||||
|
});
|
||||||
|
|
||||||
|
manager.start();
|
||||||
|
await waitFor(() => observed !== undefined);
|
||||||
|
assert.equal(observed.appId, 'cli_real_app');
|
||||||
|
assert.equal(observed.domain, 'accounts.feishu.cn');
|
||||||
|
assert.equal(Object.hasOwn(observed, 'createOnly'), false);
|
||||||
|
assert.equal(Object.hasOwn(observed, 'appPreset'), false);
|
||||||
|
assert.deepEqual(observed.addons, {
|
||||||
|
preset: false,
|
||||||
|
scopes: { tenant: [FEISHU_GROUP_MESSAGE_SCOPE] },
|
||||||
|
});
|
||||||
|
assert.equal(Object.hasOwn(observed.addons, 'events'), false);
|
||||||
|
assert.equal(Object.hasOwn(observed.addons, 'callbacks'), false);
|
||||||
|
|
||||||
|
observed.onQRCodeReady({
|
||||||
|
url: 'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_real_app&addons=encoded',
|
||||||
|
expireIn: 60,
|
||||||
|
});
|
||||||
|
assert.equal(manager.status().state, 'qr_ready');
|
||||||
|
resolveRegistration({
|
||||||
|
client_id: 'cli_real_app',
|
||||||
|
client_secret: 'private-secret',
|
||||||
|
user_info: { open_id: 'ou_owner', tenant_brand: 'feishu' },
|
||||||
|
});
|
||||||
|
await waitFor(() => manager.status().state === 'succeeded');
|
||||||
|
assert.equal(accepted.length, 1);
|
||||||
|
assert.doesNotMatch(JSON.stringify(manager.status()), /private-secret/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('group-message permission URLs stay on the exact official SDK launcher', () => {
|
||||||
|
assert.equal(
|
||||||
|
assertGroupMessagePermissionUrl(
|
||||||
|
'https://open.larksuite.com/page/launcher?tp=sdk&clientID=cli_real_app&addons=x',
|
||||||
|
'cli_real_app',
|
||||||
|
'lark',
|
||||||
|
),
|
||||||
|
'https://open.larksuite.com/page/launcher?tp=sdk&clientID=cli_real_app&addons=x',
|
||||||
|
);
|
||||||
|
for (const unsafe of [
|
||||||
|
'http://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_real_app&addons=x',
|
||||||
|
'https://open.larksuite.com/page/launcher?tp=sdk&clientID=cli_real_app&addons=x',
|
||||||
|
'https://open.feishu.cn/page/launcher?tp=sdk&clientID=other_app&addons=x',
|
||||||
|
'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_real_app&addons=x&createOnly=true',
|
||||||
|
'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_real_app',
|
||||||
|
]) {
|
||||||
|
assert.throws(
|
||||||
|
() => assertGroupMessagePermissionUrl(unsafe, 'cli_real_app'),
|
||||||
|
/unsafe verification URL/,
|
||||||
|
unsafe,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -191,6 +191,7 @@ test('QR registration separates events from card callbacks', async () => {
|
||||||
|
|
||||||
test('group response mode defaults to mention and updates the live runtime without reconnecting', async () => {
|
test('group response mode defaults to mention and updates the live runtime without reconnecting', async () => {
|
||||||
const existing = bot('bot_response_mode', 'response_mode');
|
const existing = bot('bot_response_mode', 'response_mode');
|
||||||
|
existing.groupMessagePermissionGranted = true;
|
||||||
const fx = fixture({
|
const fx = fixture({
|
||||||
bots: [existing],
|
bots: [existing],
|
||||||
secrets: { [existing.secretRef]: 'stable-secret' },
|
secrets: { [existing.secretRef]: 'stable-secret' },
|
||||||
|
|
@ -198,6 +199,7 @@ test('group response mode defaults to mention and updates the live runtime witho
|
||||||
await fx.controller.initialize();
|
await fx.controller.initialize();
|
||||||
|
|
||||||
assert.equal(fx.controller.status().bots[0].groupResponseMode, 'mention');
|
assert.equal(fx.controller.status().bots[0].groupResponseMode, 'mention');
|
||||||
|
assert.equal(fx.controller.status().bots[0].groupMessagePermissionGranted, true);
|
||||||
const runtime = fx.runtimes.get(existing.id)[0];
|
const runtime = fx.runtimes.get(existing.id)[0];
|
||||||
const updated = await fx.controller.updateGroupResponseMode(existing.id, 'all');
|
const updated = await fx.controller.updateGroupResponseMode(existing.id, 'all');
|
||||||
|
|
||||||
|
|
@ -212,6 +214,96 @@ test('group response mode defaults to mention and updates the live runtime witho
|
||||||
await fx.controller.close();
|
await fx.controller.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('all-message mode requires authorization before direct updates', async () => {
|
||||||
|
const existing = bot('bot_response_permission_required', 'response_permission_required');
|
||||||
|
const fx = fixture({
|
||||||
|
bots: [existing],
|
||||||
|
secrets: { [existing.secretRef]: 'stable-secret' },
|
||||||
|
});
|
||||||
|
await fx.controller.initialize();
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
fx.controller.updateGroupResponseMode(existing.id, 'all'),
|
||||||
|
(error) => error?.code === 'group_message_permission_required',
|
||||||
|
);
|
||||||
|
assert.equal(fx.configStore.getBot(existing.id).groupResponseMode, undefined);
|
||||||
|
assert.equal(fx.controller.status().bots[0].groupResponseMode, 'mention');
|
||||||
|
assert.equal(fx.controller.status().bots[0].groupMessagePermissionGranted, false);
|
||||||
|
await fx.controller.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('group-message authorization grants only its scope, enables all mode, and restarts one bot', async () => {
|
||||||
|
const existing = bot('bot_group_permission', 'group_permission');
|
||||||
|
const fx = fixture({
|
||||||
|
bots: [existing],
|
||||||
|
secrets: { [existing.secretRef]: 'stable-secret' },
|
||||||
|
verifyApp: async () => ({
|
||||||
|
name: existing.botName,
|
||||||
|
openId: existing.botOpenId,
|
||||||
|
activated: existing.activated,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
await fx.controller.initialize();
|
||||||
|
const oldRuntime = fx.runtimes.get(existing.id)[0];
|
||||||
|
|
||||||
|
const started = fx.controller.startGroupMessagePermission(existing.id);
|
||||||
|
const duplicate = fx.controller.startGroupMessagePermission(existing.id);
|
||||||
|
const attemptId = started.registration.attempt;
|
||||||
|
assert.equal(duplicate.registration.attempt, attemptId);
|
||||||
|
assert.equal(started.registration.operation, 'group_message_permission');
|
||||||
|
assert.equal(started.registration.botId, existing.id);
|
||||||
|
|
||||||
|
await waitFor(() => fx.registrationRuns.length === 1);
|
||||||
|
const run = fx.registrationRuns.shift();
|
||||||
|
assert.equal(run.options.appId, existing.appId);
|
||||||
|
assert.equal(Object.hasOwn(run.options, 'createOnly'), false);
|
||||||
|
assert.deepEqual(run.options.addons, {
|
||||||
|
preset: false,
|
||||||
|
scopes: { tenant: ['im:message.group_msg'] },
|
||||||
|
});
|
||||||
|
run.options.onQRCodeReady({
|
||||||
|
url: callbackRepairQrUrl(existing.appId),
|
||||||
|
expireIn: 60,
|
||||||
|
});
|
||||||
|
run.resolve({
|
||||||
|
client_id: existing.appId,
|
||||||
|
client_secret: 'stable-secret',
|
||||||
|
user_info: { open_id: existing.ownerOpenIds[0], tenant_brand: existing.domain },
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => fx.controller.registrationStatus(attemptId).registration.state === 'succeeded');
|
||||||
|
const saved = fx.configStore.getBot(existing.id);
|
||||||
|
assert.equal(saved.groupMessagePermissionGranted, true);
|
||||||
|
assert.equal(saved.groupResponseMode, 'all');
|
||||||
|
assert.equal(oldRuntime.stops, 1);
|
||||||
|
assert.equal(fx.runtimes.get(existing.id).length, 2);
|
||||||
|
assert.equal(fx.runtimes.get(existing.id)[1].config.groupResponseMode, 'all');
|
||||||
|
const status = fx.controller.registrationStatus(attemptId);
|
||||||
|
assert.equal(status.bots[0].groupMessagePermissionGranted, true);
|
||||||
|
assert.equal(status.bots[0].groupResponseMode, 'all');
|
||||||
|
await fx.controller.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cancelling group-message authorization before confirmation preserves mention mode', async () => {
|
||||||
|
const existing = bot('bot_group_permission_cancel', 'group_permission_cancel');
|
||||||
|
const fx = fixture({
|
||||||
|
bots: [existing],
|
||||||
|
secrets: { [existing.secretRef]: 'stable-secret' },
|
||||||
|
});
|
||||||
|
await fx.controller.initialize();
|
||||||
|
|
||||||
|
const started = fx.controller.startGroupMessagePermission(existing.id);
|
||||||
|
const attemptId = started.registration.attempt;
|
||||||
|
await waitFor(() => fx.registrationRuns.length === 1);
|
||||||
|
const cancelled = await fx.controller.cancelRegistration(attemptId);
|
||||||
|
assert.equal(cancelled.registration.state, 'cancelled');
|
||||||
|
const saved = fx.configStore.getBot(existing.id);
|
||||||
|
assert.equal(saved.groupMessagePermissionGranted, undefined);
|
||||||
|
assert.equal(saved.groupResponseMode, undefined);
|
||||||
|
assert.equal(fx.runtimes.get(existing.id).length, 1);
|
||||||
|
await fx.controller.close();
|
||||||
|
});
|
||||||
|
|
||||||
test('callback repair is deduplicated per bot, updates only its secret, and proves the callback', async () => {
|
test('callback repair is deduplicated per bot, updates only its secret, and proves the callback', async () => {
|
||||||
const existing = bot('bot_existing', 'existing');
|
const existing = bot('bot_existing', 'existing');
|
||||||
const fx = fixture({
|
const fx = fixture({
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,16 @@ test('PluginConfigStore persists non-secret onboarding facts', async () => {
|
||||||
assert.equal((await stat(path)).mode & 0o777, 0o600);
|
assert.equal((await stat(path)).mode & 0o777, 0o600);
|
||||||
assert.equal((await new PluginConfigStore(path).load()).get().appId, 'cli_test');
|
assert.equal((await new PluginConfigStore(path).load()).get().appId, 'cli_test');
|
||||||
assert.equal(store.get().groupResponseMode, 'mention');
|
assert.equal(store.get().groupResponseMode, 'mention');
|
||||||
|
assert.equal(store.get().groupMessagePermissionGranted, false);
|
||||||
|
|
||||||
await store.save({ ...store.get(), groupResponseMode: 'all' });
|
await store.save({
|
||||||
assert.equal((await new PluginConfigStore(path).load()).get().groupResponseMode, 'all');
|
...store.get(),
|
||||||
|
groupResponseMode: 'all',
|
||||||
|
groupMessagePermissionGranted: true,
|
||||||
|
});
|
||||||
|
const reloaded = (await new PluginConfigStore(path).load()).get();
|
||||||
|
assert.equal(reloaded.groupResponseMode, 'all');
|
||||||
|
assert.equal(reloaded.groupMessagePermissionGranted, true);
|
||||||
|
|
||||||
await store.clear();
|
await store.clear();
|
||||||
assert.equal(store.get(), null);
|
assert.equal(store.get(), null);
|
||||||
|
|
@ -71,6 +78,7 @@ test('PluginConfigStore migrates a v1 bot atomically without moving its credenti
|
||||||
assert.equal(store.get().ownerOpenId, 'ou_legacy');
|
assert.equal(store.get().ownerOpenId, 'ou_legacy');
|
||||||
assert.equal(store.list()[0].appId, 'cli_legacy');
|
assert.equal(store.list()[0].appId, 'cli_legacy');
|
||||||
assert.equal(store.list()[0].groupResponseMode, 'mention');
|
assert.equal(store.list()[0].groupResponseMode, 'mention');
|
||||||
|
assert.equal(store.list()[0].groupMessagePermissionGranted, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('PluginConfigStore rejects an invalid or duplicate v2 document without dropping entries', async () => {
|
test('PluginConfigStore rejects an invalid or duplicate v2 document without dropping entries', async () => {
|
||||||
|
|
|
||||||
|
|
@ -403,6 +403,78 @@ test('callback repair begins for exactly one bot and returns only a safe officia
|
||||||
await fx.dispose();
|
await fx.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('group-message permission begins for one existing bot and returns a safe official QR projection', async () => {
|
||||||
|
const calls = [];
|
||||||
|
const permission = status({
|
||||||
|
schemaVersion: 2,
|
||||||
|
phase: 'registering',
|
||||||
|
configured: true,
|
||||||
|
registration: {
|
||||||
|
state: 'qr_ready',
|
||||||
|
attempt: 'reg_group_permission',
|
||||||
|
operation: 'group_message_permission',
|
||||||
|
botId: 'bot_target',
|
||||||
|
qrCodeUrl: 'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_target&addons=encoded&user_code=opaque',
|
||||||
|
expiresAt: Date.now() + 60_000,
|
||||||
|
},
|
||||||
|
bots: [{
|
||||||
|
botId: 'bot_target',
|
||||||
|
connected: true,
|
||||||
|
configured: true,
|
||||||
|
groupResponseMode: 'mention',
|
||||||
|
groupMessagePermissionGranted: false,
|
||||||
|
bot: { name: '目标机器人', appIdMasked: 'cli_tar••••rget', domain: 'feishu' },
|
||||||
|
connection: { ready: true, feishuLongConnectionState: 'connected', harnessReachable: true },
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
const controller = {
|
||||||
|
status: async () => permission,
|
||||||
|
registrationStatus: async () => permission,
|
||||||
|
startRegistration: async () => status(),
|
||||||
|
startGroupMessagePermission: async (botId) => { calls.push(botId); return permission; },
|
||||||
|
cancelRegistration: async () => permission,
|
||||||
|
disconnect: async () => status(),
|
||||||
|
};
|
||||||
|
const fx = await rpcFixture(controller);
|
||||||
|
const result = await fx.registration.handler(
|
||||||
|
FEISHU_ENDPOINTS.beginGroupMessagePermission,
|
||||||
|
{ botId: 'bot_target' },
|
||||||
|
signal(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.deepEqual(calls, ['bot_target']);
|
||||||
|
assert.equal(result.value.operation, 'group_message_permission');
|
||||||
|
assert.equal(result.value.botId, 'bot_target');
|
||||||
|
assert.equal(
|
||||||
|
result.value.verificationUrl,
|
||||||
|
'https://open.feishu.cn/page/launcher?tp=sdk&clientID=cli_target&addons=encoded&user_code=opaque',
|
||||||
|
);
|
||||||
|
assert.match(result.value.qrCodeDataUrl, /^data:image\/png;base64,/);
|
||||||
|
assert.doesNotMatch(JSON.stringify(result), /client_secret|appSecret/);
|
||||||
|
|
||||||
|
const restored = await fx.registration.handler(FEISHU_ENDPOINTS.status, {}, signal());
|
||||||
|
assert.equal(restored.value.provisioning.operation, 'group_message_permission');
|
||||||
|
assert.equal(restored.value.provisioning.botId, 'bot_target');
|
||||||
|
assert.equal(restored.value.bots[0].groupMessagePermissionGranted, false);
|
||||||
|
|
||||||
|
for (const payload of [
|
||||||
|
{},
|
||||||
|
{ botId: '../target' },
|
||||||
|
{ botId: 'bot_target', appSecret: 'must-not-leak' },
|
||||||
|
]) {
|
||||||
|
const invalid = await fx.registration.handler(
|
||||||
|
FEISHU_ENDPOINTS.beginGroupMessagePermission,
|
||||||
|
payload,
|
||||||
|
signal(),
|
||||||
|
);
|
||||||
|
assert.equal(invalid.ok, false);
|
||||||
|
assert.equal(invalid.error.code, 'bad-request');
|
||||||
|
assert.doesNotMatch(JSON.stringify(invalid), /must-not-leak|\.\.\/target/);
|
||||||
|
}
|
||||||
|
await fx.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
test('status preserves a submitted callback repair attempt after its QR URL is discarded', async () => {
|
test('status preserves a submitted callback repair attempt after its QR URL is discarded', async () => {
|
||||||
const secret = 'must-never-cross-the-rpc-boundary';
|
const secret = 'must-never-cross-the-rpc-boundary';
|
||||||
const saving = status({
|
const saving = status({
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue