mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-15 10:00:47 +08:00
fix: remember direct recipients for connection tests
This commit is contained in:
parent
fb61c4235f
commit
3ac70a7c65
34 changed files with 397 additions and 178 deletions
|
|
@ -354,7 +354,7 @@ function normalizeSnapshot(value) {
|
||||||
function connectionTestFeedback(result) {
|
function connectionTestFeedback(result) {
|
||||||
if (result?.sent === true) return "\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002";
|
if (result?.sent === true) return "\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002";
|
||||||
if (result?.code === "test-target-unavailable") {
|
if (result?.code === "test-target-unavailable") {
|
||||||
return "\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002";
|
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u673A\u5668\u4EBA\u5C1A\u672A\u6536\u5230\u53EF\u7528\u4E8E\u6D4B\u8BD5\u7684\u79C1\u804A\u6D88\u606F\u3002";
|
||||||
}
|
}
|
||||||
return result ? "\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null;
|
return result ? "\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null;
|
||||||
}
|
}
|
||||||
|
|
@ -453,20 +453,16 @@ var EN = Object.freeze({
|
||||||
"\u68C0\u67E5\u4E2D\u2026": "Checking\u2026",
|
"\u68C0\u67E5\u4E2D\u2026": "Checking\u2026",
|
||||||
"\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002": "Connection check failed. Try again later.",
|
"\u8FDE\u63A5\u68C0\u67E5\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002": "Connection check failed. Try again later.",
|
||||||
"\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u5BF9\u5E94\u673A\u5668\u4EBA\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002": "Test message sent. Check the matching bot conversation.",
|
"\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u5BF9\u5E94\u673A\u5668\u4EBA\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002": "Test message sent. Check the matching bot conversation.",
|
||||||
"\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002": "Connection check completed. Send /status to the bot in a direct message, then check again.",
|
"\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u673A\u5668\u4EBA\u5C1A\u672A\u6536\u5230\u53EF\u7528\u4E8E\u6D4B\u8BD5\u7684\u79C1\u804A\u6D88\u606F\u3002": "Connection check completed. The bot has not received a direct message it can use for testing.",
|
||||||
"\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002": "Connection check completed, but the test message could not be sent.",
|
"\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002": "Connection check completed, but the test message could not be sent.",
|
||||||
"\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u98DE\u4E66\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002": "Test message sent. Check the Feishu conversation.",
|
"\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u98DE\u4E66\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002": "Test message sent. Check the Feishu conversation.",
|
||||||
"\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u7684\u6D4B\u8BD5\u6D88\u606F\u63A5\u6536\u4EBA\u3002": "Connection check completed, but no test-message recipient is available.",
|
|
||||||
"\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230 WhatsApp \u81EA\u804A\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002": "Test message sent. Check the WhatsApp self-chat.",
|
"\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230 WhatsApp \u81EA\u804A\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002": "Test message sent. Check the WhatsApp self-chat.",
|
||||||
"\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u7684 WhatsApp \u81EA\u804A\u76EE\u6807\u3002": "Connection check completed, but no WhatsApp self-chat target is available.",
|
"\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u7684 WhatsApp \u81EA\u804A\u76EE\u6807\u3002": "Connection check completed, but no WhatsApp self-chat target is available.",
|
||||||
"\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002": "DingTalk connection check completed and the test message was sent.",
|
"\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002": "DingTalk connection check completed and the test message was sent.",
|
||||||
"\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002": "DingTalk connection check completed. Send /status to the bot in a direct message, then check again.",
|
|
||||||
"\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002": "DingTalk connection check completed, but the test message could not be sent.",
|
"\u9489\u9489\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002": "DingTalk connection check completed, but the test message could not be sent.",
|
||||||
"\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002": "WeChat connection check completed and the test message was sent.",
|
"\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002": "WeChat connection check completed and the test message was sent.",
|
||||||
"\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002": "WeChat connection check completed. Send /status to the bot in a direct message, then check again.",
|
|
||||||
"\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002": "WeChat connection check completed, but the test message could not be sent.",
|
"\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002": "WeChat connection check completed, but the test message could not be sent.",
|
||||||
"\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002": "WeCom connection check completed and the test message was sent.",
|
"\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002": "WeCom connection check completed and the test message was sent.",
|
||||||
"\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002": "WeCom connection check completed. Send /status to the bot in a direct message, then check again.",
|
|
||||||
"\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002": "WeCom connection check completed, but the test message could not be sent.",
|
"\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002": "WeCom connection check completed, but the test message could not be sent.",
|
||||||
"\u91CD\u8BD5\u8FDE\u63A5": "Reconnect",
|
"\u91CD\u8BD5\u8FDE\u63A5": "Reconnect",
|
||||||
"\u91CD\u8BD5\u4E2D\u2026": "Retrying\u2026",
|
"\u91CD\u8BD5\u4E2D\u2026": "Retrying\u2026",
|
||||||
|
|
@ -2501,7 +2497,7 @@ function checkedTime2(value) {
|
||||||
function connectionTestNotice(value) {
|
function connectionTestNotice(value) {
|
||||||
if (value?.testMessage?.sent === true) return "\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u5BF9\u5E94\u673A\u5668\u4EBA\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002";
|
if (value?.testMessage?.sent === true) return "\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u5BF9\u5E94\u673A\u5668\u4EBA\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002";
|
||||||
if (value?.testMessage?.code === "test-target-unavailable") {
|
if (value?.testMessage?.code === "test-target-unavailable") {
|
||||||
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002";
|
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u673A\u5668\u4EBA\u5C1A\u672A\u6536\u5230\u53EF\u7528\u4E8E\u6D4B\u8BD5\u7684\u79C1\u804A\u6D88\u606F\u3002";
|
||||||
}
|
}
|
||||||
return value?.testMessage ? "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null;
|
return value?.testMessage ? "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null;
|
||||||
}
|
}
|
||||||
|
|
@ -4063,7 +4059,7 @@ function connectionTestNotice2(value) {
|
||||||
return "\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u98DE\u4E66\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002";
|
return "\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u98DE\u4E66\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002";
|
||||||
}
|
}
|
||||||
if (value?.testMessage?.code === "test-target-unavailable") {
|
if (value?.testMessage?.code === "test-target-unavailable") {
|
||||||
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u5F53\u524D\u6CA1\u6709\u53EF\u7528\u7684\u6D4B\u8BD5\u6D88\u606F\u63A5\u6536\u4EBA\u3002";
|
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u673A\u5668\u4EBA\u5C1A\u672A\u6536\u5230\u53EF\u7528\u4E8E\u6D4B\u8BD5\u7684\u79C1\u804A\u6D88\u606F\u3002";
|
||||||
}
|
}
|
||||||
return value?.testMessage ? "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null;
|
return value?.testMessage ? "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null;
|
||||||
}
|
}
|
||||||
|
|
@ -4892,7 +4888,7 @@ function normalizeSnapshot3(value) {
|
||||||
function connectionTestFeedback2(result) {
|
function connectionTestFeedback2(result) {
|
||||||
if (result?.sent === true) return "\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u5BF9\u5E94\u673A\u5668\u4EBA\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002";
|
if (result?.sent === true) return "\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u8BF7\u5230\u5BF9\u5E94\u673A\u5668\u4EBA\u4F1A\u8BDD\u4E2D\u786E\u8BA4\u3002";
|
||||||
if (result?.code === "test-target-unavailable") {
|
if (result?.code === "test-target-unavailable") {
|
||||||
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002";
|
return "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u673A\u5668\u4EBA\u5C1A\u672A\u6536\u5230\u53EF\u7528\u4E8E\u6D4B\u8BD5\u7684\u79C1\u804A\u6D88\u606F\u3002";
|
||||||
}
|
}
|
||||||
return result ? "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null;
|
return result ? "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002" : null;
|
||||||
}
|
}
|
||||||
|
|
@ -6397,7 +6393,7 @@ function WecomSettingsTab({ rpcCall }) {
|
||||||
} else if (snapshot.testMessage?.sent) {
|
} else if (snapshot.testMessage?.sent) {
|
||||||
feedback = "\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002";
|
feedback = "\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002";
|
||||||
} else if (snapshot.testMessage?.code === "test-target-unavailable") {
|
} else if (snapshot.testMessage?.code === "test-target-unavailable") {
|
||||||
feedback = "\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002";
|
feedback = "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u673A\u5668\u4EBA\u5C1A\u672A\u6536\u5230\u53EF\u7528\u4E8E\u6D4B\u8BD5\u7684\u79C1\u804A\u6D88\u606F\u3002";
|
||||||
} else if (snapshot.testMessage) {
|
} else if (snapshot.testMessage) {
|
||||||
feedback = "\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002";
|
feedback = "\u4F01\u4E1A\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002";
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -7371,7 +7367,7 @@ function WeixinSettingsTab({ rpcCall }) {
|
||||||
} else if (snapshot.testMessage?.sent) {
|
} else if (snapshot.testMessage?.sent) {
|
||||||
feedback = "\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002";
|
feedback = "\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u6D4B\u8BD5\u6D88\u606F\u5DF2\u53D1\u9001\u3002";
|
||||||
} else if (snapshot.testMessage?.code === "test-target-unavailable") {
|
} else if (snapshot.testMessage?.code === "test-target-unavailable") {
|
||||||
feedback = "\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u8BF7\u5148\u79C1\u804A\u673A\u5668\u4EBA\u53D1\u9001 /status\uFF0C\u518D\u91CD\u65B0\u68C0\u67E5\u3002";
|
feedback = "\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\u3002\u673A\u5668\u4EBA\u5C1A\u672A\u6536\u5230\u53EF\u7528\u4E8E\u6D4B\u8BD5\u7684\u79C1\u804A\u6D88\u606F\u3002";
|
||||||
} else if (snapshot.testMessage) {
|
} else if (snapshot.testMessage) {
|
||||||
feedback = "\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002";
|
feedback = "\u5FAE\u4FE1\u8FDE\u63A5\u68C0\u67E5\u5B8C\u6210\uFF0C\u4F46\u6D4B\u8BD5\u6D88\u606F\u53D1\u9001\u5931\u8D25\u3002";
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
148
lib/index.js
148
lib/index.js
File diff suppressed because one or more lines are too long
|
|
@ -206,7 +206,7 @@ export function normalizeSnapshot(value) {
|
||||||
export function connectionTestFeedback(result) {
|
export function connectionTestFeedback(result) {
|
||||||
if (result?.sent === true) return '钉钉连接检查完成,测试消息已发送。';
|
if (result?.sent === true) return '钉钉连接检查完成,测试消息已发送。';
|
||||||
if (result?.code === 'test-target-unavailable') {
|
if (result?.code === 'test-target-unavailable') {
|
||||||
return '钉钉连接检查完成。请先私聊机器人发送 /status,再重新检查。';
|
return '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
|
||||||
}
|
}
|
||||||
return result ? '钉钉连接检查完成,但测试消息发送失败。' : null;
|
return result ? '钉钉连接检查完成,但测试消息发送失败。' : null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -335,7 +335,7 @@ function connectionTestNotice(value) {
|
||||||
return '测试消息已发送,请到飞书会话中确认。';
|
return '测试消息已发送,请到飞书会话中确认。';
|
||||||
}
|
}
|
||||||
if (value?.testMessage?.code === 'test-target-unavailable') {
|
if (value?.testMessage?.code === 'test-target-unavailable') {
|
||||||
return '连接检查完成,但当前没有可用的测试消息接收人。';
|
return '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
|
||||||
}
|
}
|
||||||
return value?.testMessage ? '连接检查完成,但测试消息发送失败。' : null;
|
return value?.testMessage ? '连接检查完成,但测试消息发送失败。' : null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ export function normalizeSnapshot(value) {
|
||||||
export function connectionTestFeedback(result) {
|
export function connectionTestFeedback(result) {
|
||||||
if (result?.sent === true) return '测试消息已发送,请到对应机器人会话中确认。';
|
if (result?.sent === true) return '测试消息已发送,请到对应机器人会话中确认。';
|
||||||
if (result?.code === 'test-target-unavailable') {
|
if (result?.code === 'test-target-unavailable') {
|
||||||
return '连接检查完成。请先私聊机器人发送 /status,再重新检查。';
|
return '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
|
||||||
}
|
}
|
||||||
return result ? '连接检查完成,但测试消息发送失败。' : null;
|
return result ? '连接检查完成,但测试消息发送失败。' : null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ function checkedTime(value) {
|
||||||
function connectionTestNotice(value) {
|
function connectionTestNotice(value) {
|
||||||
if (value?.testMessage?.sent === true) return '测试消息已发送,请到对应机器人会话中确认。';
|
if (value?.testMessage?.sent === true) return '测试消息已发送,请到对应机器人会话中确认。';
|
||||||
if (value?.testMessage?.code === 'test-target-unavailable') {
|
if (value?.testMessage?.code === 'test-target-unavailable') {
|
||||||
return '连接检查完成。请先私聊机器人发送 /status,再重新检查。';
|
return '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
|
||||||
}
|
}
|
||||||
return value?.testMessage ? '连接检查完成,但测试消息发送失败。' : null;
|
return value?.testMessage ? '连接检查完成,但测试消息发送失败。' : null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -410,7 +410,7 @@ export function WecomSettingsTab({ rpcCall }) {
|
||||||
} else if (snapshot.testMessage?.sent) {
|
} else if (snapshot.testMessage?.sent) {
|
||||||
feedback = '企业微信连接检查完成,测试消息已发送。';
|
feedback = '企业微信连接检查完成,测试消息已发送。';
|
||||||
} else if (snapshot.testMessage?.code === 'test-target-unavailable') {
|
} else if (snapshot.testMessage?.code === 'test-target-unavailable') {
|
||||||
feedback = '企业微信连接检查完成。请先私聊机器人发送 /status,再重新检查。';
|
feedback = '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
|
||||||
} else if (snapshot.testMessage) {
|
} else if (snapshot.testMessage) {
|
||||||
feedback = '企业微信连接检查完成,但测试消息发送失败。';
|
feedback = '企业微信连接检查完成,但测试消息发送失败。';
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -524,7 +524,7 @@ export function WeixinSettingsTab({ rpcCall }) {
|
||||||
} else if (snapshot.testMessage?.sent) {
|
} else if (snapshot.testMessage?.sent) {
|
||||||
feedback = '微信连接检查完成,测试消息已发送。';
|
feedback = '微信连接检查完成,测试消息已发送。';
|
||||||
} else if (snapshot.testMessage?.code === 'test-target-unavailable') {
|
} else if (snapshot.testMessage?.code === 'test-target-unavailable') {
|
||||||
feedback = '微信连接检查完成。请先私聊机器人发送 /status,再重新检查。';
|
feedback = '连接检查完成。机器人尚未收到可用于测试的私聊消息。';
|
||||||
} else if (snapshot.testMessage) {
|
} else if (snapshot.testMessage) {
|
||||||
feedback = '微信连接检查完成,但测试消息发送失败。';
|
feedback = '微信连接检查完成,但测试消息发送失败。';
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -77,20 +77,16 @@ const EN = Object.freeze({
|
||||||
'检查中…': 'Checking…',
|
'检查中…': 'Checking…',
|
||||||
'连接检查失败,请稍后重试。': 'Connection check failed. Try again later.',
|
'连接检查失败,请稍后重试。': 'Connection check failed. Try again later.',
|
||||||
'测试消息已发送,请到对应机器人会话中确认。': 'Test message sent. Check the matching bot conversation.',
|
'测试消息已发送,请到对应机器人会话中确认。': 'Test message sent. Check the matching bot conversation.',
|
||||||
'连接检查完成。请先私聊机器人发送 /status,再重新检查。': 'Connection check completed. Send /status to the bot in a direct message, then check again.',
|
'连接检查完成。机器人尚未收到可用于测试的私聊消息。': 'Connection check completed. The bot has not received a direct message it can use for testing.',
|
||||||
'连接检查完成,但测试消息发送失败。': 'Connection check completed, but the test message could not be sent.',
|
'连接检查完成,但测试消息发送失败。': 'Connection check completed, but the test message could not be sent.',
|
||||||
'测试消息已发送,请到飞书会话中确认。': 'Test message sent. Check the Feishu conversation.',
|
'测试消息已发送,请到飞书会话中确认。': 'Test message sent. Check the Feishu conversation.',
|
||||||
'连接检查完成,但当前没有可用的测试消息接收人。': 'Connection check completed, but no test-message recipient is available.',
|
|
||||||
'测试消息已发送,请到 WhatsApp 自聊会话中确认。': 'Test message sent. Check the WhatsApp self-chat.',
|
'测试消息已发送,请到 WhatsApp 自聊会话中确认。': 'Test message sent. Check the WhatsApp self-chat.',
|
||||||
'连接检查完成,但当前没有可用的 WhatsApp 自聊目标。': 'Connection check completed, but no WhatsApp self-chat target is available.',
|
'连接检查完成,但当前没有可用的 WhatsApp 自聊目标。': 'Connection check completed, but no WhatsApp self-chat target is available.',
|
||||||
'钉钉连接检查完成,测试消息已发送。': 'DingTalk connection check completed and the test message was sent.',
|
'钉钉连接检查完成,测试消息已发送。': 'DingTalk connection check completed and the test message was sent.',
|
||||||
'钉钉连接检查完成。请先私聊机器人发送 /status,再重新检查。': 'DingTalk connection check completed. Send /status to the bot in a direct message, then check again.',
|
|
||||||
'钉钉连接检查完成,但测试消息发送失败。': 'DingTalk connection check completed, but the test message could not be sent.',
|
'钉钉连接检查完成,但测试消息发送失败。': 'DingTalk connection check completed, but the test message could not be sent.',
|
||||||
'微信连接检查完成,测试消息已发送。': 'WeChat connection check completed and the test message was sent.',
|
'微信连接检查完成,测试消息已发送。': 'WeChat connection check completed and the test message was sent.',
|
||||||
'微信连接检查完成。请先私聊机器人发送 /status,再重新检查。': 'WeChat connection check completed. Send /status to the bot in a direct message, then check again.',
|
|
||||||
'微信连接检查完成,但测试消息发送失败。': 'WeChat connection check completed, but the test message could not be sent.',
|
'微信连接检查完成,但测试消息发送失败。': 'WeChat connection check completed, but the test message could not be sent.',
|
||||||
'企业微信连接检查完成,测试消息已发送。': 'WeCom connection check completed and the test message was sent.',
|
'企业微信连接检查完成,测试消息已发送。': 'WeCom connection check completed and the test message was sent.',
|
||||||
'企业微信连接检查完成。请先私聊机器人发送 /status,再重新检查。': 'WeCom connection check completed. Send /status to the bot in a direct message, then check again.',
|
|
||||||
'企业微信连接检查完成,但测试消息发送失败。': 'WeCom connection check completed, but the test message could not be sent.',
|
'企业微信连接检查完成,但测试消息发送失败。': 'WeCom connection check completed, but the test message could not be sent.',
|
||||||
'重试连接': 'Reconnect',
|
'重试连接': 'Reconnect',
|
||||||
'重试中…': 'Retrying…',
|
'重试中…': 'Retrying…',
|
||||||
|
|
|
||||||
|
|
@ -310,6 +310,9 @@ export class DingtalkHarnessBridge {
|
||||||
} catch {
|
} catch {
|
||||||
// An unsafe reply route must never be able to submit an approval.
|
// An unsafe reply route must never be able to submit an approval.
|
||||||
}
|
}
|
||||||
|
if (sessionWebhook && String(message.conversationType) !== '2') {
|
||||||
|
rememberConnectionTestTarget(this.#state, { sessionWebhook });
|
||||||
|
}
|
||||||
const pending = this.#pendingInteractions.get(key);
|
const pending = this.#pendingInteractions.get(key);
|
||||||
const approvalReply = this.#approvals.claimReply({
|
const approvalReply = this.#approvals.claimReply({
|
||||||
key,
|
key,
|
||||||
|
|
@ -460,9 +463,6 @@ export class DingtalkHarnessBridge {
|
||||||
if (isPlainText && !hasImages && command === '/status') {
|
if (isPlainText && !hasImages && command === '/status') {
|
||||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||||
await this.#send(sessionWebhook, '钉钉机器人与 DeepSeek Harness 连接正常。');
|
await this.#send(sessionWebhook, '钉钉机器人与 DeepSeek Harness 连接正常。');
|
||||||
if (String(message.conversationType) !== '2') {
|
|
||||||
rememberConnectionTestTarget(this.#state, { sessionWebhook });
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isPlainText && !hasImages && command === '/new') {
|
if (isPlainText && !hasImages && command === '/new') {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
} from '../shared/harness-question.mjs';
|
} from '../shared/harness-question.mjs';
|
||||||
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
||||||
import { runCompactCommand } from '../shared/compact-command.mjs';
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
||||||
|
import { rememberConnectionTestTarget } from '../shared/connection-test.mjs';
|
||||||
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
||||||
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
||||||
|
|
||||||
|
|
@ -133,6 +134,11 @@ export class FeishuHarnessBridge {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.message.chat_type === 'p2p') {
|
||||||
|
const chatId = nonEmptyString(event.message.chat_id);
|
||||||
|
if (chatId) rememberConnectionTestTarget(this.#state, { chatId });
|
||||||
|
}
|
||||||
|
|
||||||
this.#acceptedMessageIds.add(messageId);
|
this.#acceptedMessageIds.add(messageId);
|
||||||
const processingReaction = this.#addReaction(messageId, 'OnIt');
|
const processingReaction = this.#addReaction(messageId, 'OnIt');
|
||||||
if (this.#isResolvedQuestionReply(event, key)) {
|
if (this.#isResolvedQuestionReply(event, key)) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
import { FeishuHarnessBridge } from './bridge.mjs';
|
import { FeishuHarnessBridge } from './bridge.mjs';
|
||||||
import { VerifiedFeishuChannel } from './feishu-channel.mjs';
|
import { VerifiedFeishuChannel } from './feishu-channel.mjs';
|
||||||
|
import {
|
||||||
|
connectionTestTargetUnavailable,
|
||||||
|
sendRememberedConnectionTest,
|
||||||
|
} from '../shared/connection-test.mjs';
|
||||||
|
|
||||||
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
const DEFAULT_REQUEST_TIMEOUT_MS = 15_000;
|
||||||
|
|
||||||
|
|
@ -246,27 +250,39 @@ export class FeishuRuntime {
|
||||||
error.code = 'test-target-unavailable';
|
error.code = 'test-target-unavailable';
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
const ownerOpenId = this.#ownerOpenIds[0];
|
|
||||||
if (!ownerOpenId || ownerOpenId === '*') {
|
|
||||||
const error = new Error('飞书机器人没有可用的测试消息接收人');
|
|
||||||
error.code = 'test-target-unavailable';
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
if (typeof text !== 'string' || !text.trim()) {
|
if (typeof text !== 'string' || !text.trim()) {
|
||||||
throw new TypeError('Feishu connection test text is required');
|
throw new TypeError('Feishu connection test text is required');
|
||||||
}
|
}
|
||||||
const response = await this.#client.im.v1.message.create({
|
const send = async (receiveIdType, receiveId, content) => {
|
||||||
params: { receive_id_type: 'open_id' },
|
const response = await this.#client.im.v1.message.create({
|
||||||
data: {
|
params: { receive_id_type: receiveIdType },
|
||||||
receive_id: ownerOpenId,
|
data: {
|
||||||
msg_type: 'text',
|
receive_id: receiveId,
|
||||||
content: JSON.stringify({ text }),
|
msg_type: 'text',
|
||||||
|
content: JSON.stringify({ text: content }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (response?.code && response.code !== 0) {
|
||||||
|
throw new Error(`Feishu connection test failed: ${response.msg || response.code}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ownerOpenId = this.#ownerOpenIds.find((value) => value !== '*');
|
||||||
|
if (ownerOpenId) {
|
||||||
|
await send('open_id', ownerOpenId, text);
|
||||||
|
return { sent: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return sendRememberedConnectionTest({
|
||||||
|
state: this.#state,
|
||||||
|
text,
|
||||||
|
channelLabel: '飞书机器人',
|
||||||
|
send: async (target, content) => {
|
||||||
|
const chatId = typeof target?.chatId === 'string' ? target.chatId.trim() : '';
|
||||||
|
if (!chatId) throw connectionTestTargetUnavailable('飞书机器人');
|
||||||
|
await send('chat_id', chatId, content);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (response?.code && response.code !== 0) {
|
|
||||||
throw new Error(`Feishu connection test failed: ${response.msg || response.code}`);
|
|
||||||
}
|
|
||||||
return { sent: true };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async stop({ preserveError = false } = {}) {
|
async stop({ preserveError = false } = {}) {
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,12 @@ export class QqHarnessBridge {
|
||||||
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
|
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
|
||||||
const key = conversationKey(message);
|
const key = conversationKey(message);
|
||||||
this.#acceptedMessageIds.add(messageId);
|
this.#acceptedMessageIds.add(messageId);
|
||||||
|
if (message.kind === 'c2c'
|
||||||
|
&& (this.#ownerUserOpenid === '*' || sender === this.#ownerUserOpenid)
|
||||||
|
&& message.replyTarget?.scope === 'c2c'
|
||||||
|
&& nonEmptyString(message.replyTarget.targetId) === sender) {
|
||||||
|
rememberConnectionTestTarget(this.#state, message.replyTarget);
|
||||||
|
}
|
||||||
const pending = this.#pendingInteractions.get(key);
|
const pending = this.#pendingInteractions.get(key);
|
||||||
const approval = this.#approvals.claimReply({
|
const approval = this.#approvals.claimReply({
|
||||||
key,
|
key,
|
||||||
|
|
@ -296,11 +302,6 @@ export class QqHarnessBridge {
|
||||||
}
|
}
|
||||||
if (!hasImages && command === '/status') {
|
if (!hasImages && command === '/status') {
|
||||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||||
if (message.kind === 'c2c'
|
|
||||||
&& target?.scope === 'c2c'
|
|
||||||
&& nonEmptyString(target.targetId) === sender) {
|
|
||||||
rememberConnectionTestTarget(this.#state, target);
|
|
||||||
}
|
|
||||||
await this.#bot.sendText(target, 'QQ 机器人与 DeepSeek Harness 连接正常。');
|
await this.#bot.sendText(target, 'QQ 机器人与 DeepSeek Harness 连接正常。');
|
||||||
await this.#state.markSeen(messageId);
|
await this.#state.markSeen(messageId);
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export function connectionTestMessage(botName, channelLabel = '机器人') {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function connectionTestTargetUnavailable(channelLabel = '机器人') {
|
export function connectionTestTargetUnavailable(channelLabel = '机器人') {
|
||||||
const error = new Error(`请先私聊${channelLabel}发送 /status,再点击“检查连接”。`);
|
const error = new Error(`${channelLabel}尚未收到可用于测试的私聊消息。`);
|
||||||
error.code = 'test-target-unavailable';
|
error.code = 'test-target-unavailable';
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,12 @@ export class TextHarnessBridge {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
this.#acceptedMessageIds.add(messageId);
|
this.#acceptedMessageIds.add(messageId);
|
||||||
|
if (normalized.kind === 'direct') {
|
||||||
|
rememberConnectionTestTarget(
|
||||||
|
this.#state,
|
||||||
|
normalized.connectionTestTarget ?? normalized.replyTarget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const key = `${kind}:${conversationId}`;
|
const key = `${kind}:${conversationId}`;
|
||||||
const pending = this.#pendingInteractions.get(key);
|
const pending = this.#pendingInteractions.get(key);
|
||||||
|
|
@ -252,9 +258,6 @@ export class TextHarnessBridge {
|
||||||
if (!hasImages && command === '/status') {
|
if (!hasImages && command === '/status') {
|
||||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||||
await this.#bot.sendText(target, `${this.#descriptor.label}机器人与 DeepSeek Harness 连接正常。`);
|
await this.#bot.sendText(target, `${this.#descriptor.label}机器人与 DeepSeek Harness 连接正常。`);
|
||||||
if (message.kind === 'direct') {
|
|
||||||
rememberConnectionTestTarget(this.#state, message.connectionTestTarget ?? target);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const workspaceCommand = !hasImages
|
const workspaceCommand = !hasImages
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,9 @@ export class WecomHarnessBridge {
|
||||||
|
|
||||||
const key = conversationKey(frame);
|
const key = conversationKey(frame);
|
||||||
this.#acceptedMessageIds.add(messageId);
|
this.#acceptedMessageIds.add(messageId);
|
||||||
|
if (body.chattype === 'single') {
|
||||||
|
rememberConnectionTestTarget(this.#state, { chatId });
|
||||||
|
}
|
||||||
const pending = this.#pendingInteractions.get(key);
|
const pending = this.#pendingInteractions.get(key);
|
||||||
const approval = this.#approvals.claimReply({
|
const approval = this.#approvals.claimReply({
|
||||||
key,
|
key,
|
||||||
|
|
@ -431,9 +434,6 @@ export class WecomHarnessBridge {
|
||||||
if (!hasImages && command === '/status') {
|
if (!hasImages && command === '/status') {
|
||||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||||
await this.#sendImmediate(frame, chatId, '企业微信机器人与 DeepSeek Harness 连接正常。');
|
await this.#sendImmediate(frame, chatId, '企业微信机器人与 DeepSeek Harness 连接正常。');
|
||||||
if (body.chattype === 'single') {
|
|
||||||
rememberConnectionTestTarget(this.#state, { chatId });
|
|
||||||
}
|
|
||||||
await this.#state.markSeen(messageId);
|
await this.#state.markSeen(messageId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,9 @@ export class WeixinHarnessBridge {
|
||||||
if (!messageId || !sender || this.#state.hasSeen(messageId)
|
if (!messageId || !sender || this.#state.hasSeen(messageId)
|
||||||
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
|
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
|
||||||
this.#acceptedMessageIds.add(messageId);
|
this.#acceptedMessageIds.add(messageId);
|
||||||
|
if (sender === this.#ownerUserId) {
|
||||||
|
rememberConnectionTestTarget(this.#state, { toUserId: sender });
|
||||||
|
}
|
||||||
const key = conversationKey(sender);
|
const key = conversationKey(sender);
|
||||||
const contextToken = nonEmptyString(message?.context_token) ?? undefined;
|
const contextToken = nonEmptyString(message?.context_token) ?? undefined;
|
||||||
const runId = nonEmptyString(message?.run_id) ?? undefined;
|
const runId = nonEmptyString(message?.run_id) ?? undefined;
|
||||||
|
|
@ -251,7 +254,6 @@ export class WeixinHarnessBridge {
|
||||||
if (!hasImages && command === '/status') {
|
if (!hasImages && command === '/status') {
|
||||||
await this.#harness.ensureRunning({ signal: this.#signal });
|
await this.#harness.ensureRunning({ signal: this.#signal });
|
||||||
await this.#send(sender, '微信与 DeepSeek Harness 连接正常。', contextToken, runId);
|
await this.#send(sender, '微信与 DeepSeek Harness 连接正常。', contextToken, runId);
|
||||||
rememberConnectionTestTarget(this.#state, { toUserId: sender });
|
|
||||||
await this.#state.markSeen(messageId);
|
await this.#state.markSeen(messageId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ test('connection-test feedback uses fixed client-owned messages', () => {
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
connectionTestFeedback({ sent: false, code: 'test-target-unavailable' }),
|
connectionTestFeedback({ sent: false, code: 'test-target-unavailable' }),
|
||||||
'钉钉连接检查完成。请先私聊机器人发送 /status,再重新检查。',
|
'连接检查完成。机器人尚未收到可用于测试的私聊消息。',
|
||||||
);
|
);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
connectionTestFeedback({ sent: false, code: 'test-message-failed' }),
|
connectionTestFeedback({ sent: false, code: 'test-message-failed' }),
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ const PNG_BYTES = Buffer.from([
|
||||||
0x01, 0x02, 0x03,
|
0x01, 0x02, 0x03,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
test('DingTalk remembers only a private /status session webhook for connection tests', async () => {
|
test('DingTalk remembers any private inbound session webhook for connection tests', async () => {
|
||||||
const privateFixture = stateFixture();
|
const privateFixture = stateFixture();
|
||||||
const privateSent = [];
|
const privateSent = [];
|
||||||
const privateBridge = new DingtalkHarnessBridge({
|
const privateBridge = new DingtalkHarnessBridge({
|
||||||
|
|
@ -92,11 +92,11 @@ test('DingTalk remembers only a private /status session webhook for connection t
|
||||||
harness: { ensureRunning: async () => true },
|
harness: { ensureRunning: async () => true },
|
||||||
state: privateFixture.state,
|
state: privateFixture.state,
|
||||||
});
|
});
|
||||||
await privateBridge.accept(message('status-private', '/status'));
|
await privateBridge.accept(message('help-private', '/help'));
|
||||||
assert.deepEqual(connectionTestTarget(privateFixture.state), {
|
assert.deepEqual(connectionTestTarget(privateFixture.state), {
|
||||||
sessionWebhook: 'https://oapi.dingtalk.com/robot/reply?ticket=status-private',
|
sessionWebhook: 'https://oapi.dingtalk.com/robot/reply?ticket=help-private',
|
||||||
});
|
});
|
||||||
assert.equal(privateSent.at(-1).text, '钉钉机器人与 DeepSeek Harness 连接正常。');
|
assert.match(privateSent.at(-1).text, /\/help/);
|
||||||
|
|
||||||
const groupFixture = stateFixture();
|
const groupFixture = stateFixture();
|
||||||
const groupBridge = new DingtalkHarnessBridge({
|
const groupBridge = new DingtalkHarnessBridge({
|
||||||
|
|
@ -106,9 +106,9 @@ test('DingTalk remembers only a private /status session webhook for connection t
|
||||||
harness: { ensureRunning: async () => true },
|
harness: { ensureRunning: async () => true },
|
||||||
state: groupFixture.state,
|
state: groupFixture.state,
|
||||||
});
|
});
|
||||||
await groupBridge.accept(message('status-group', '/status', {
|
await groupBridge.accept(message('help-group', '/help', {
|
||||||
conversationType: '2',
|
conversationType: '2',
|
||||||
conversationId: 'group-status',
|
conversationId: 'group-help',
|
||||||
isInAtList: true,
|
isInAtList: true,
|
||||||
}));
|
}));
|
||||||
assert.equal(connectionTestTarget(groupFixture.state), null);
|
assert.equal(connectionTestTarget(groupFixture.state), null);
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ function stateFixture() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('runtime sends a DingTalk connection test only through the remembered private status webhook', async () => {
|
test('runtime sends a DingTalk connection test only through the remembered private webhook', async () => {
|
||||||
const state = stateFixture();
|
const state = stateFixture();
|
||||||
const sends = [];
|
const sends = [];
|
||||||
const client = {
|
const client = {
|
||||||
|
|
@ -70,13 +70,13 @@ test('runtime sends a DingTalk connection test only through the remembered priva
|
||||||
code: 'test-target-unavailable',
|
code: 'test-target-unavailable',
|
||||||
});
|
});
|
||||||
rememberConnectionTestTarget(state, {
|
rememberConnectionTestTarget(state, {
|
||||||
sessionWebhook: 'https://oapi.dingtalk.com/robot/reply?ticket=status-private',
|
sessionWebhook: 'https://oapi.dingtalk.com/robot/reply?ticket=inbound-private',
|
||||||
});
|
});
|
||||||
assert.deepEqual(await runtime.sendConnectionTest('连接测试'), { sent: true });
|
assert.deepEqual(await runtime.sendConnectionTest('连接测试'), { sent: true });
|
||||||
assert.equal(sends.length, 1);
|
assert.equal(sends.length, 1);
|
||||||
assert.equal(sends[0].clientId, 'ding-client');
|
assert.equal(sends[0].clientId, 'ding-client');
|
||||||
assert.equal(sends[0].clientSecret, 'host-secret');
|
assert.equal(sends[0].clientSecret, 'host-secret');
|
||||||
assert.equal(sends[0].sessionWebhook, 'https://oapi.dingtalk.com/robot/reply?ticket=status-private');
|
assert.equal(sends[0].sessionWebhook, 'https://oapi.dingtalk.com/robot/reply?ticket=inbound-private');
|
||||||
assert.equal(sends[0].text, '连接测试');
|
assert.equal(sends[0].text, '连接测试');
|
||||||
await runtime.stop();
|
await runtime.stop();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
|
||||||
import { Readable } from 'node:stream';
|
import { Readable } from 'node:stream';
|
||||||
import { FeishuHarnessBridge } from '../../../src/channels/feishu/bridge.mjs';
|
import { FeishuHarnessBridge } from '../../../src/channels/feishu/bridge.mjs';
|
||||||
import { DEFAULT_IMAGE_PROMPT } from '../../../src/channels/shared/image-prompt.mjs';
|
import { DEFAULT_IMAGE_PROMPT } from '../../../src/channels/shared/image-prompt.mjs';
|
||||||
|
import { connectionTestTarget } from '../../../src/channels/shared/connection-test.mjs';
|
||||||
|
|
||||||
const PNG_1X1 = Buffer.from(
|
const PNG_1X1 = Buffer.from(
|
||||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
||||||
|
|
@ -86,6 +87,54 @@ function textClient(sendText) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('Feishu remembers any authorized private inbound message as a connection-test target', async () => {
|
||||||
|
const groupFixture = stateFixture();
|
||||||
|
const groupBridge = new FeishuHarnessBridge({
|
||||||
|
client: textClient(async () => {}),
|
||||||
|
channel: {},
|
||||||
|
harness: { ensureRunning: async () => true },
|
||||||
|
state: groupFixture.state,
|
||||||
|
status: bridgeStatus(),
|
||||||
|
allowedSenderOpenIds: new Set(['ou_user']),
|
||||||
|
});
|
||||||
|
await groupBridge.accept(event('target-group', '/help', {
|
||||||
|
chat_type: 'group',
|
||||||
|
chat_id: 'oc_group',
|
||||||
|
}));
|
||||||
|
await groupBridge.waitForIdle();
|
||||||
|
assert.equal(connectionTestTarget(groupFixture.state), null);
|
||||||
|
|
||||||
|
const rejectedFixture = stateFixture();
|
||||||
|
const rejectedBridge = new FeishuHarnessBridge({
|
||||||
|
client: textClient(async () => {}),
|
||||||
|
channel: {},
|
||||||
|
harness: { ensureRunning: async () => true },
|
||||||
|
state: rejectedFixture.state,
|
||||||
|
status: bridgeStatus(),
|
||||||
|
allowedSenderOpenIds: new Set(['ou_owner']),
|
||||||
|
});
|
||||||
|
await rejectedBridge.accept(event('target-rejected', '/help', {
|
||||||
|
senderOpenId: 'ou_other',
|
||||||
|
}));
|
||||||
|
await rejectedBridge.waitForIdle();
|
||||||
|
assert.equal(connectionTestTarget(rejectedFixture.state), null);
|
||||||
|
|
||||||
|
const privateFixture = stateFixture();
|
||||||
|
const privateBridge = new FeishuHarnessBridge({
|
||||||
|
client: textClient(async () => {}),
|
||||||
|
channel: {},
|
||||||
|
harness: { ensureRunning: async () => true },
|
||||||
|
state: privateFixture.state,
|
||||||
|
status: bridgeStatus(),
|
||||||
|
allowedSenderOpenIds: new Set(['*']),
|
||||||
|
});
|
||||||
|
await privateBridge.accept(event('target-private', '/help', {
|
||||||
|
chat_id: 'oc_private',
|
||||||
|
}));
|
||||||
|
await privateBridge.waitForIdle();
|
||||||
|
assert.deepEqual(connectionTestTarget(privateFixture.state), { chatId: 'oc_private' });
|
||||||
|
});
|
||||||
|
|
||||||
test('Feishu executes /compact for the bound Session without prompting the model', async () => {
|
test('Feishu executes /compact for the bound Session without prompting the model', async () => {
|
||||||
const fixture = stateFixture([['p2p:ou_user', 'session-compact']]);
|
const fixture = stateFixture([['p2p:ou_user', 'session-compact']]);
|
||||||
const sent = [];
|
const sent = [];
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ test('Feishu connection check requests and displays test-message feedback', asyn
|
||||||
import.meta.url,
|
import.meta.url,
|
||||||
), 'utf8');
|
), 'utf8');
|
||||||
assert.match(source, /FEISHU_ENDPOINTS\.reconnectBot, \{ botId, sendTest: true \}/);
|
assert.match(source, /FEISHU_ENDPOINTS\.reconnectBot, \{ botId, sendTest: true \}/);
|
||||||
|
assert.match(source, /机器人尚未收到可用于测试的私聊消息/);
|
||||||
|
assert.doesNotMatch(source, /请先私聊机器人发送 \/status/);
|
||||||
|
|
||||||
const markup = renderToStaticMarkup(React.createElement(BotCard, {
|
const markup = renderToStaticMarkup(React.createElement(BotCard, {
|
||||||
connection: {
|
connection: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { FeishuRuntime } from '../../../src/channels/feishu/feishu-runtime.mjs';
|
import { FeishuRuntime } from '../../../src/channels/feishu/feishu-runtime.mjs';
|
||||||
|
import { rememberConnectionTestTarget } from '../../../src/channels/shared/connection-test.mjs';
|
||||||
|
|
||||||
class FakeClient {
|
class FakeClient {
|
||||||
static instances = [];
|
static instances = [];
|
||||||
|
|
@ -86,7 +87,7 @@ test('FeishuRuntime becomes chat-ready only after Harness and Feishu are connect
|
||||||
lark: fakeLark(),
|
lark: fakeLark(),
|
||||||
appId: 'cli_test',
|
appId: 'cli_test',
|
||||||
appSecret: 'secret',
|
appSecret: 'secret',
|
||||||
ownerOpenId: 'ou_owner',
|
ownerOpenIds: ['*', 'ou_owner'],
|
||||||
harness: {
|
harness: {
|
||||||
async ensureRunning(options) {
|
async ensureRunning(options) {
|
||||||
harnessChecks += 1;
|
harnessChecks += 1;
|
||||||
|
|
@ -133,6 +134,40 @@ test('FeishuRuntime becomes chat-ready only after Harness and Feishu are connect
|
||||||
assert.equal(harnessSignal.aborted, true);
|
assert.equal(harnessSignal.aborted, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('FeishuRuntime uses a remembered private target for wildcard-only manual bots', async () => {
|
||||||
|
const state = { hasSeen: () => false };
|
||||||
|
const runtime = new FeishuRuntime({
|
||||||
|
lark: fakeLark(),
|
||||||
|
appId: 'cli_manual',
|
||||||
|
appSecret: 'secret',
|
||||||
|
ownerOpenIds: ['*'],
|
||||||
|
harness: { async ensureRunning() {} },
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
|
||||||
|
const starting = runtime.start();
|
||||||
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
|
FakeWSClient.instances[0].becomeReady();
|
||||||
|
await starting;
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
runtime.sendConnectionTest('连接测试'),
|
||||||
|
(error) => error?.code === 'test-target-unavailable',
|
||||||
|
);
|
||||||
|
rememberConnectionTestTarget(state, { chatId: 'oc_manual_private' });
|
||||||
|
assert.deepEqual(await runtime.sendConnectionTest('连接测试'), { sent: true });
|
||||||
|
assert.deepEqual(FakeClient.sent, [{
|
||||||
|
params: { receive_id_type: 'chat_id' },
|
||||||
|
data: {
|
||||||
|
receive_id: 'oc_manual_private',
|
||||||
|
msg_type: 'text',
|
||||||
|
content: JSON.stringify({ text: '连接测试' }),
|
||||||
|
},
|
||||||
|
}]);
|
||||||
|
|
||||||
|
await runtime.stop();
|
||||||
|
});
|
||||||
|
|
||||||
test('FeishuRuntime fails closed when the initial WebSocket handshake times out', async () => {
|
test('FeishuRuntime fails closed when the initial WebSocket handshake times out', async () => {
|
||||||
const runtime = new FeishuRuntime({
|
const runtime = new FeishuRuntime({
|
||||||
lark: fakeLark(),
|
lark: fakeLark(),
|
||||||
|
|
|
||||||
|
|
@ -259,7 +259,7 @@ test('QQ executes /compact for the bound Session without prompting the model', a
|
||||||
assert.equal(fixture.seen.has('compact-qq'), true);
|
assert.equal(fixture.seen.has('compact-qq'), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('QQ remembers a connection-test target only for an authorized private /status', async () => {
|
test('QQ remembers any authorized private inbound as a connection-test target', async () => {
|
||||||
const fixture = stateFixture();
|
const fixture = stateFixture();
|
||||||
const sent = [];
|
const sent = [];
|
||||||
const bridge = new QqHarnessBridge({
|
const bridge = new QqHarnessBridge({
|
||||||
|
|
@ -270,32 +270,32 @@ test('QQ remembers a connection-test target only for an authorized private /stat
|
||||||
});
|
});
|
||||||
|
|
||||||
await bridge.accept(message({
|
await bridge.accept(message({
|
||||||
messageId: 'status-rejected',
|
messageId: 'help-rejected',
|
||||||
senderId: 'other-openid',
|
senderId: 'other-openid',
|
||||||
content: '/status',
|
content: '/help',
|
||||||
replyTarget: { scope: 'c2c', targetId: 'other-openid', msgId: 'status-rejected' },
|
replyTarget: { scope: 'c2c', targetId: 'other-openid', msgId: 'help-rejected' },
|
||||||
}));
|
}));
|
||||||
await bridge.accept(message({
|
await bridge.accept(message({
|
||||||
kind: 'group',
|
kind: 'group',
|
||||||
rawEventType: 'GROUP_AT_MESSAGE_CREATE',
|
rawEventType: 'GROUP_AT_MESSAGE_CREATE',
|
||||||
groupOpenid: 'group-1',
|
groupOpenid: 'group-1',
|
||||||
messageId: 'status-group',
|
messageId: 'help-group',
|
||||||
content: '/status',
|
content: '/help',
|
||||||
replyTarget: { scope: 'group', targetId: 'group-1', msgId: 'status-group' },
|
replyTarget: { scope: 'group', targetId: 'group-1', msgId: 'help-group' },
|
||||||
}));
|
}));
|
||||||
assert.equal(connectionTestTarget(fixture.state), null);
|
assert.equal(connectionTestTarget(fixture.state), null);
|
||||||
|
|
||||||
const privateTarget = {
|
const privateTarget = {
|
||||||
scope: 'c2c', targetId: 'owner-openid', msgId: 'status-private',
|
scope: 'c2c', targetId: 'owner-openid', msgId: 'help-private',
|
||||||
};
|
};
|
||||||
await bridge.accept(message({
|
await bridge.accept(message({
|
||||||
messageId: 'status-private',
|
messageId: 'help-private',
|
||||||
content: '/status',
|
content: '/help',
|
||||||
replyTarget: privateTarget,
|
replyTarget: privateTarget,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
assert.deepEqual(connectionTestTarget(fixture.state), privateTarget);
|
assert.deepEqual(connectionTestTarget(fixture.state), privateTarget);
|
||||||
assert.equal(sent.filter(({ text }) => text.includes('连接正常')).length, 2);
|
assert.equal(sent.length, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('QQ private messages stream Harness snapshots and finalize once', async () => {
|
test('QQ private messages stream Harness snapshots and finalize once', async () => {
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,10 @@ test('QQ client normalizes and presents connection-test outcomes', () => {
|
||||||
assert.deepEqual(unavailable.testMessage, {
|
assert.deepEqual(unavailable.testMessage, {
|
||||||
sent: false, code: 'test-target-unavailable',
|
sent: false, code: 'test-target-unavailable',
|
||||||
});
|
});
|
||||||
assert.match(connectionTestFeedback(unavailable.testMessage), /\/status/);
|
assert.equal(
|
||||||
|
connectionTestFeedback(unavailable.testMessage),
|
||||||
|
'连接检查完成。机器人尚未收到可用于测试的私聊消息。',
|
||||||
|
);
|
||||||
|
|
||||||
const invalid = normalizeSnapshot({
|
const invalid = normalizeSnapshot({
|
||||||
bots: [], testMessage: { sent: false, code: 'private-provider-error' },
|
bots: [], testMessage: { sent: false, code: 'private-provider-error' },
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ test('QQ runtime sends a proactive connection test to the explicit owner fallbac
|
||||||
await runtime.stop();
|
await runtime.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('QQ runtime requires /status for wildcard owners and strips the remembered reply id', async () => {
|
test('QQ runtime requires a remembered private target for wildcard owners and strips the reply id', async () => {
|
||||||
const bot = new FakeBot();
|
const bot = new FakeBot();
|
||||||
const state = {};
|
const state = {};
|
||||||
const runtime = new QqRuntime({
|
const runtime = new QqRuntime({
|
||||||
|
|
@ -119,7 +119,7 @@ test('QQ runtime requires /status for wildcard owners and strips the remembered
|
||||||
(error) => error?.code === 'test-target-unavailable',
|
(error) => error?.code === 'test-target-unavailable',
|
||||||
);
|
);
|
||||||
rememberConnectionTestTarget(state, {
|
rememberConnectionTestTarget(state, {
|
||||||
scope: 'c2c', targetId: 'recent-user', msgId: 'old-status-message',
|
scope: 'c2c', targetId: 'recent-user', msgId: 'old-inbound-message',
|
||||||
});
|
});
|
||||||
await runtime.sendConnectionTest('remembered-target');
|
await runtime.sendConnectionTest('remembered-target');
|
||||||
assert.deepEqual(bot.sent, [{
|
assert.deepEqual(bot.sent, [{
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
|
||||||
import { DiscordHarnessBridge } from '../../../src/channels/discord/discord-bridge.mjs';
|
import { DiscordHarnessBridge } from '../../../src/channels/discord/discord-bridge.mjs';
|
||||||
|
import { connectionTestTarget } from '../../../src/channels/shared/connection-test.mjs';
|
||||||
import { TextHarnessBridge } from '../../../src/channels/shared/text-harness-bridge.mjs';
|
import { TextHarnessBridge } from '../../../src/channels/shared/text-harness-bridge.mjs';
|
||||||
import { SlackHarnessBridge } from '../../../src/channels/slack/slack-bridge.mjs';
|
import { SlackHarnessBridge } from '../../../src/channels/slack/slack-bridge.mjs';
|
||||||
import { TelegramHarnessBridge } from '../../../src/channels/telegram/telegram-bridge.mjs';
|
import { TelegramHarnessBridge } from '../../../src/channels/telegram/telegram-bridge.mjs';
|
||||||
|
|
@ -149,6 +150,78 @@ test('all four shared text channels execute /compact outside the model prompt pa
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Slack, Telegram, and Discord remember any valid direct message per bot', async () => {
|
||||||
|
for (const [name, Bridge] of [
|
||||||
|
['slack', SlackHarnessBridge],
|
||||||
|
['telegram', TelegramHarnessBridge],
|
||||||
|
['discord', DiscordHarnessBridge],
|
||||||
|
]) {
|
||||||
|
let sessionSequence = 0;
|
||||||
|
const harness = {
|
||||||
|
createSession: async () => `${name}-session-${++sessionSequence}`,
|
||||||
|
ask: async () => `${name} reply`,
|
||||||
|
};
|
||||||
|
const first = stateFixture();
|
||||||
|
const second = stateFixture();
|
||||||
|
const firstSent = [];
|
||||||
|
const secondSent = [];
|
||||||
|
const firstBot = {
|
||||||
|
sendText: async (target, text) => firstSent.push({ target, text }),
|
||||||
|
};
|
||||||
|
const secondBot = {
|
||||||
|
sendText: async (target, text) => secondSent.push({ target, text }),
|
||||||
|
};
|
||||||
|
const firstBridge = new Bridge({ bot: firstBot, harness, state: first.state });
|
||||||
|
const secondBridge = new Bridge({ bot: secondBot, harness, state: second.state });
|
||||||
|
const firstTarget = { channelId: `${name}-private-a` };
|
||||||
|
const secondTarget = { channelId: `${name}-private-b` };
|
||||||
|
|
||||||
|
await firstBridge.accept(message(`${name}-direct-a`, 'hello', {
|
||||||
|
conversationId: `${name}-private-a`,
|
||||||
|
replyTarget: { ...firstTarget, replyToMessageId: 'reply-a' },
|
||||||
|
connectionTestTarget: firstTarget,
|
||||||
|
}));
|
||||||
|
assert.deepEqual(connectionTestTarget(first.state), firstTarget, name);
|
||||||
|
|
||||||
|
await firstBridge.accept(message(`${name}-group`, 'hello group', {
|
||||||
|
kind: 'group',
|
||||||
|
conversationId: `${name}-group`,
|
||||||
|
addressed: true,
|
||||||
|
replyTarget: { channelId: `${name}-group` },
|
||||||
|
connectionTestTarget: { channelId: `${name}-group` },
|
||||||
|
}));
|
||||||
|
assert.deepEqual(connectionTestTarget(first.state), firstTarget, `${name} group`);
|
||||||
|
|
||||||
|
await firstBridge.accept(message(`${name}-direct-a`, 'duplicate', {
|
||||||
|
conversationId: `${name}-private-replay`,
|
||||||
|
replyTarget: { channelId: `${name}-private-replay` },
|
||||||
|
connectionTestTarget: { channelId: `${name}-private-replay` },
|
||||||
|
}));
|
||||||
|
assert.deepEqual(connectionTestTarget(first.state), firstTarget, `${name} duplicate`);
|
||||||
|
|
||||||
|
await secondBridge.accept(message(`${name}-direct-b`, 'hello', {
|
||||||
|
conversationId: `${name}-private-b`,
|
||||||
|
replyTarget: { ...secondTarget, replyToMessageId: 'reply-b' },
|
||||||
|
connectionTestTarget: secondTarget,
|
||||||
|
}));
|
||||||
|
assert.deepEqual(connectionTestTarget(second.state), secondTarget, `${name} second bot`);
|
||||||
|
|
||||||
|
const reconnectedFirstBridge = new Bridge({ bot: firstBot, harness, state: first.state });
|
||||||
|
await reconnectedFirstBridge.sendConnectionTest(`${name} first test`);
|
||||||
|
await secondBridge.sendConnectionTest(`${name} second test`);
|
||||||
|
assert.deepEqual(
|
||||||
|
firstSent.find(({ text }) => text === `${name} first test`)?.target,
|
||||||
|
firstTarget,
|
||||||
|
`${name} reconnect`,
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
secondSent.find(({ text }) => text === `${name} second test`)?.target,
|
||||||
|
secondTarget,
|
||||||
|
`${name} bot isolation`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('answers a multi-question interaction on the fast lane with canonical values', async () => {
|
test('answers a multi-question interaction on the fast lane with canonical values', async () => {
|
||||||
const fixture = stateFixture();
|
const fixture = stateFixture();
|
||||||
const sent = [];
|
const sent = [];
|
||||||
|
|
|
||||||
|
|
@ -454,26 +454,15 @@ test('Telegram bridge ignores unaddressed groups and streams direct replies', as
|
||||||
assert.equal(askCount, 0);
|
assert.equal(askCount, 0);
|
||||||
await bridge.accept({
|
await bridge.accept({
|
||||||
messageId: '2', senderId: 'u1', kind: 'direct', conversationId: 'u1', content: 'hello',
|
messageId: '2', senderId: 'u1', kind: 'direct', conversationId: 'u1', content: 'hello',
|
||||||
addressed: true, replyTarget: {},
|
addressed: true,
|
||||||
|
replyTarget: { chatId: 88, replyToMessageId: 7 },
|
||||||
|
connectionTestTarget: { chatId: 88 },
|
||||||
});
|
});
|
||||||
assert.equal(askCount, 1);
|
assert.equal(askCount, 1);
|
||||||
assert.deepEqual(updates, ['正在使用搜索…', '处理中']);
|
assert.deepEqual(updates, ['正在使用搜索…', '处理中']);
|
||||||
assert.deepEqual(sent, ['完成']);
|
assert.deepEqual(sent, ['完成']);
|
||||||
await assert.rejects(
|
|
||||||
() => bridge.sendConnectionTest('card test'),
|
|
||||||
(error) => error?.code === 'test-target-unavailable',
|
|
||||||
);
|
|
||||||
|
|
||||||
const statusTarget = { chatId: 88, replyToMessageId: 7 };
|
|
||||||
await bridge.accept({
|
|
||||||
messageId: '3', senderId: 'u1', kind: 'direct', conversationId: 'u1', content: '/status',
|
|
||||||
addressed: true, replyTarget: statusTarget, connectionTestTarget: { chatId: 88 },
|
|
||||||
});
|
|
||||||
await bridge.sendConnectionTest('card test');
|
await bridge.sendConnectionTest('card test');
|
||||||
assert.deepEqual(sent.slice(-2), [
|
assert.equal(sent.at(-1), 'card test');
|
||||||
'Telegram机器人与 DeepSeek Harness 连接正常。',
|
|
||||||
'card test',
|
|
||||||
]);
|
|
||||||
assert.deepEqual(sentTargets.at(-1), { chatId: 88 });
|
assert.deepEqual(sentTargets.at(-1), { chatId: 88 });
|
||||||
const reconnectedBridge = new TelegramHarnessBridge({ bot, harness, state });
|
const reconnectedBridge = new TelegramHarnessBridge({ bot, harness, state });
|
||||||
await reconnectedBridge.sendConnectionTest('after reconnect');
|
await reconnectedBridge.sendConnectionTest('after reconnect');
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ function questionInteraction({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('Enterprise WeChat remembers only a private /status chat as a connection-test target', async () => {
|
test('Enterprise WeChat remembers any private inbound as a connection-test target', async () => {
|
||||||
const privateState = state();
|
const privateState = state();
|
||||||
const privateTransport = testClient();
|
const privateTransport = testClient();
|
||||||
const privateBridge = new WecomHarnessBridge({
|
const privateBridge = new WecomHarnessBridge({
|
||||||
|
|
@ -109,7 +109,7 @@ test('Enterprise WeChat remembers only a private /status chat as a connection-te
|
||||||
harness: { ensureRunning: async () => true },
|
harness: { ensureRunning: async () => true },
|
||||||
state: privateState,
|
state: privateState,
|
||||||
});
|
});
|
||||||
await privateBridge.accept(frame({ msgid: 'status-private', text: { content: '/status' } }));
|
await privateBridge.accept(frame({ msgid: 'help-private', text: { content: '/help' } }));
|
||||||
assert.deepEqual(connectionTestTarget(privateState), { chatId: 'member-1' });
|
assert.deepEqual(connectionTestTarget(privateState), { chatId: 'member-1' });
|
||||||
|
|
||||||
const groupState = state();
|
const groupState = state();
|
||||||
|
|
@ -120,10 +120,10 @@ test('Enterprise WeChat remembers only a private /status chat as a connection-te
|
||||||
state: groupState,
|
state: groupState,
|
||||||
});
|
});
|
||||||
await groupBridge.accept(frame({
|
await groupBridge.accept(frame({
|
||||||
msgid: 'status-group',
|
msgid: 'help-group',
|
||||||
chattype: 'group',
|
chattype: 'group',
|
||||||
chatid: 'group-1',
|
chatid: 'group-1',
|
||||||
text: { content: '@机器人 /status' },
|
text: { content: '@机器人 /help' },
|
||||||
}));
|
}));
|
||||||
assert.equal(connectionTestTarget(groupState), null);
|
assert.equal(connectionTestTarget(groupState), null);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -134,5 +134,7 @@ test('Enterprise WeChat connection feedback is scoped to the checked bot', async
|
||||||
test('Enterprise WeChat reconnect failure uses fixed translatable copy', async () => {
|
test('Enterprise WeChat reconnect failure uses fixed translatable copy', async () => {
|
||||||
const source = await readFile(CLIENT_URL, 'utf8');
|
const source = await readFile(CLIENT_URL, 'utf8');
|
||||||
assert.match(source, /'连接检查失败,请稍后重试。'/);
|
assert.match(source, /'连接检查失败,请稍后重试。'/);
|
||||||
|
assert.match(source, /'连接检查完成。机器人尚未收到可用于测试的私聊消息。'/);
|
||||||
|
assert.doesNotMatch(source, /请先私聊机器人发送 \/status/);
|
||||||
assert.doesNotMatch(source, /连接检查失败:\$\{presentError\(error\)\.message\}/);
|
assert.doesNotMatch(source, /连接检查失败:\$\{presentError\(error\)\.message\}/);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ class FakeClient extends EventEmitter {
|
||||||
async sendMessage(chatId, body) { this.sent.push({ chatId, body }); }
|
async sendMessage(chatId, body) { this.sent.push({ chatId, body }); }
|
||||||
}
|
}
|
||||||
|
|
||||||
test('Enterprise WeChat runtime sends a connection test only to the remembered private status target', async () => {
|
test('Enterprise WeChat runtime sends a connection test only to the remembered private target', async () => {
|
||||||
const client = new FakeClient();
|
const client = new FakeClient();
|
||||||
const state = {};
|
const state = {};
|
||||||
const runtime = new WecomRuntime({
|
const runtime = new WecomRuntime({
|
||||||
|
|
|
||||||
|
|
@ -127,5 +127,7 @@ test('Weixin connection feedback is scoped to the checked bot', async (t) => {
|
||||||
test('Weixin reconnect failure uses fixed translatable copy', async () => {
|
test('Weixin reconnect failure uses fixed translatable copy', async () => {
|
||||||
const source = await readFile(CLIENT_URL, 'utf8');
|
const source = await readFile(CLIENT_URL, 'utf8');
|
||||||
assert.match(source, /'连接检查失败,请稍后重试。'/);
|
assert.match(source, /'连接检查失败,请稍后重试。'/);
|
||||||
|
assert.match(source, /'连接检查完成。机器人尚未收到可用于测试的私聊消息。'/);
|
||||||
|
assert.doesNotMatch(source, /请先私聊机器人发送 \/status/);
|
||||||
assert.doesNotMatch(source, /连接检查失败:\$\{presentError\(error\)\.message\}/);
|
assert.doesNotMatch(source, /连接检查失败:\$\{presentError\(error\)\.message\}/);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ const PNG_BYTES = Buffer.from([
|
||||||
0x01, 0x02, 0x03,
|
0x01, 0x02, 0x03,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
test('Weixin remembers only the authorized owner /status as a connection-test target', async () => {
|
test('Weixin remembers any authorized private inbound as a connection-test target', async () => {
|
||||||
const fixture = stateFixture();
|
const fixture = stateFixture();
|
||||||
const sent = [];
|
const sent = [];
|
||||||
const bridge = new WeixinHarnessBridge({
|
const bridge = new WeixinHarnessBridge({
|
||||||
|
|
@ -81,20 +81,20 @@ test('Weixin remembers only the authorized owner /status as a connection-test ta
|
||||||
state: fixture.state,
|
state: fixture.state,
|
||||||
});
|
});
|
||||||
|
|
||||||
await bridge.accept(message('status-owner', '/status'));
|
await bridge.accept(message('help-owner', '/help'));
|
||||||
assert.deepEqual(connectionTestTarget(fixture.state), { toUserId: 'owner-user' });
|
assert.deepEqual(connectionTestTarget(fixture.state), { toUserId: 'owner-user' });
|
||||||
assert.equal(sent.at(-1).text, '微信与 DeepSeek Harness 连接正常。');
|
assert.match(sent.at(-1).text, /\/help/);
|
||||||
|
|
||||||
const rejectedFixture = stateFixture();
|
const rejectedFixture = stateFixture();
|
||||||
const rejectedBridge = new WeixinHarnessBridge({
|
const rejectedBridge = new WeixinHarnessBridge({
|
||||||
api: { sendText: async () => assert.fail('unauthorized status must not be answered') },
|
api: { sendText: async () => assert.fail('unauthorized message must not be answered') },
|
||||||
baseUrl: 'https://ilinkai.weixin.qq.com/',
|
baseUrl: 'https://ilinkai.weixin.qq.com/',
|
||||||
token: 'host-token',
|
token: 'host-token',
|
||||||
ownerUserId: 'owner-user',
|
ownerUserId: 'owner-user',
|
||||||
harness: { ensureRunning: async () => true },
|
harness: { ensureRunning: async () => true },
|
||||||
state: rejectedFixture.state,
|
state: rejectedFixture.state,
|
||||||
});
|
});
|
||||||
await rejectedBridge.accept(message('status-other', '/status', { from_user_id: 'other-user' }));
|
await rejectedBridge.accept(message('help-other', '/help', { from_user_id: 'other-user' }));
|
||||||
assert.equal(connectionTestTarget(rejectedFixture.state), null);
|
assert.equal(connectionTestTarget(rejectedFixture.state), null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -532,6 +532,50 @@ test('connection check requests a test message and shows its delivery result', a
|
||||||
await act(async () => { renderer.unmount(); });
|
await act(async () => { renderer.unmount(); });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('shared token target-unavailable feedback asks for any direct message', async (t) => {
|
||||||
|
const previousWindow = globalThis.window;
|
||||||
|
globalThis.window = { setInterval() { return 1; }, clearInterval() {} };
|
||||||
|
setImTranslator((key) => en[key] ?? key);
|
||||||
|
t.after(() => {
|
||||||
|
setImTranslator(null);
|
||||||
|
if (previousWindow === undefined) delete globalThis.window;
|
||||||
|
else globalThis.window = previousWindow;
|
||||||
|
});
|
||||||
|
const snapshot = discordSnapshot('/workspace/current');
|
||||||
|
const rpcCall = async (endpoint) => {
|
||||||
|
if (endpoint === 'connection.status') return { ok: true, value: snapshot };
|
||||||
|
if (endpoint === 'bot.reconnect') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
value: {
|
||||||
|
...snapshot,
|
||||||
|
testMessage: { sent: false, code: 'test-target-unavailable' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error(`Unexpected endpoint: ${endpoint}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
let renderer;
|
||||||
|
await act(async () => {
|
||||||
|
renderer = create(React.createElement(DiscordSettingsTab, { rpcCall }));
|
||||||
|
await flushMicrotasks();
|
||||||
|
});
|
||||||
|
const card = renderer.root.findByProps({ 'data-bot-id': 'discord_test' });
|
||||||
|
await act(async () => {
|
||||||
|
buttonNamed(card, 'Check connection').props.onClick();
|
||||||
|
await flushMicrotasks();
|
||||||
|
});
|
||||||
|
|
||||||
|
const notice = textOf(renderer.root.findByProps({ role: 'status' }));
|
||||||
|
assert.equal(
|
||||||
|
notice,
|
||||||
|
'Connection check completed. The bot has not received a direct message it can use for testing.',
|
||||||
|
);
|
||||||
|
assert.doesNotMatch(notice, /\/status|[\p{Script=Han}]/u);
|
||||||
|
await act(async () => { renderer.unmount(); });
|
||||||
|
});
|
||||||
|
|
||||||
test('shared token connection failures render a fixed English-safe notice', async (t) => {
|
test('shared token connection failures render a fixed English-safe notice', async (t) => {
|
||||||
const previousWindow = globalThis.window;
|
const previousWindow = globalThis.window;
|
||||||
globalThis.window = { setInterval() { return 1; }, clearInterval() {} };
|
globalThis.window = { setInterval() { return 1; }, clearInterval() {} };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue