mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-16 01:43:22 +08:00
Merge remote-tracking branch 'origin/main' into codex/pr94-updated
# Conflicts: # CHANGELOG.md
This commit is contained in:
commit
29aa138b3b
11 changed files with 245 additions and 202 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -11,6 +11,18 @@ This file records the notable changes in each dsh-im release. Its format follows
|
||||||
- 飞书机器人启动时调用 `app_slash_commands` OpenAPI,把常用命令注册为原生 Slash Command,使飞书单聊输入框输入 `/` 弹出命令面板;命令清单由 dsh-im 持有并推送注册,不依赖 dsh/Harness 后端。需要应用开通 `application:app_slash_command:read` / `write` 并发布版本,注册失败不影响消息收发。
|
- 飞书机器人启动时调用 `app_slash_commands` OpenAPI,把常用命令注册为原生 Slash Command,使飞书单聊输入框输入 `/` 弹出命令面板;命令清单由 dsh-im 持有并推送注册,不依赖 dsh/Harness 后端。需要应用开通 `application:app_slash_command:read` / `write` 并发布版本,注册失败不影响消息收发。
|
||||||
On startup the Feishu bot registers its common commands as native Slash Commands via the `app_slash_commands` OpenAPI, so the `/` panel appears in Feishu direct-message input. The command list is owned and pushed by dsh-im and does not depend on the dsh/Harness backend. Requires the app to grant `application:app_slash_command:read` / `write` and publish a version; registration failure does not affect messaging.
|
On startup the Feishu bot registers its common commands as native Slash Commands via the `app_slash_commands` OpenAPI, so the `/` panel appears in Feishu direct-message input. The command list is owned and pushed by dsh-im and does not depend on the dsh/Harness backend. Requires the app to grant `application:app_slash_command:read` / `write` and publish a version; registration failure does not affect messaging.
|
||||||
|
|
||||||
|
## [4.1.1] - 2026-08-31
|
||||||
|
|
||||||
|
### Fixed / 修复
|
||||||
|
|
||||||
|
- QQ 扫码绑定的机器人现在会响应群内任意成员对机器人的 @ 消息,同时继续只接受扫码者的私聊;群聊仍不会响应未 @ 机器人的普通消息。
|
||||||
|
QQ bots connected by QR code now respond when any group member mentions the bot, while private chats remain restricted to the scanner. Ordinary group messages without a mention remain ignored.
|
||||||
|
|
||||||
|
### Documentation / 文档
|
||||||
|
|
||||||
|
- 中英文 README 新增上下文增强界面截图和企业微信群入口,方便查看设置效果并加入用户社区。
|
||||||
|
Added context-enhancement screenshots and the WeCom community-group entry to the Chinese and English READMEs, making the settings easier to preview and the user community easier to join.
|
||||||
|
|
||||||
## [4.1.0] - 2026-08-30
|
## [4.1.0] - 2026-08-30
|
||||||
|
|
||||||
### Added / 新增
|
### Added / 新增
|
||||||
|
|
@ -493,7 +505,8 @@ This file records the notable changes in each dsh-im release. Its format follows
|
||||||
- 改进 npm 发布包结构,保留 CLI 入口并避免安装脚本拦截。
|
- 改进 npm 发布包结构,保留 CLI 入口并避免安装脚本拦截。
|
||||||
Improved npm package contents to preserve the CLI entry point and avoid install-script blocking.
|
Improved npm package contents to preserve the CLI entry point and avoid install-script blocking.
|
||||||
|
|
||||||
[Unreleased]: https://github.com/xmanrui/dsh-im/compare/v4.1.0...HEAD
|
[Unreleased]: https://github.com/xmanrui/dsh-im/compare/v4.1.1...HEAD
|
||||||
|
[4.1.1]: https://github.com/xmanrui/dsh-im/compare/v4.1.0...v4.1.1
|
||||||
[4.1.0]: https://github.com/xmanrui/dsh-im/compare/v4.0.1...v4.1.0
|
[4.1.0]: https://github.com/xmanrui/dsh-im/compare/v4.0.1...v4.1.0
|
||||||
[4.0.1]: https://github.com/xmanrui/dsh-im/compare/v4.0.0...v4.0.1
|
[4.0.1]: https://github.com/xmanrui/dsh-im/compare/v4.0.0...v4.0.1
|
||||||
[4.0.0]: https://github.com/xmanrui/dsh-im/compare/v3.2.0...v4.0.0
|
[4.0.0]: https://github.com/xmanrui/dsh-im/compare/v3.2.0...v4.0.0
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ Connect IM bots to DeepSeek Harness by scanning a QR code, using an App Manifest
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Built-in channels
|
## Built-in channels
|
||||||
|
|
||||||
| Channel | Setup | Messaging and replies |
|
| Channel | Setup | Messaging and replies |
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,9 @@ Connect IM bots to DeepSeek Harness by scanning a QR code, using an App Manifest
|
||||||
|
|
||||||
## 界面
|
## 界面
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 当前内置渠道
|
## 当前内置渠道
|
||||||
|
|
||||||
|
|
|
||||||
BIN
docs/images/Context_enhancement.png
Normal file
BIN
docs/images/Context_enhancement.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 255 KiB |
BIN
docs/images/Context_enhancement_en.png
Normal file
BIN
docs/images/Context_enhancement_en.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 266 KiB |
|
|
@ -571,7 +571,7 @@ var React23 = __toESM(require("react"), 1);
|
||||||
// package.json
|
// package.json
|
||||||
var package_default = {
|
var package_default = {
|
||||||
name: "@xmanrui/dsh-im",
|
name: "@xmanrui/dsh-im",
|
||||||
version: "4.1.0",
|
version: "4.1.1",
|
||||||
description: "\u628A\u4E5D\u79CD IM \u673A\u5668\u4EBA\u548C\u516C\u7F51 AI Office \u63A5\u5165\u672C\u673A DeepSeek Harness\u3002 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
|
description: "\u628A\u4E5D\u79CD IM \u673A\u5668\u4EBA\u548C\u516C\u7F51 AI Office \u63A5\u5165\u672C\u673A DeepSeek Harness\u3002 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
|
||||||
keywords: [
|
keywords: [
|
||||||
"deepseek-harness",
|
"deepseek-harness",
|
||||||
|
|
|
||||||
374
lib/index.js
374
lib/index.js
File diff suppressed because one or more lines are too long
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@xmanrui/dsh-im",
|
"name": "@xmanrui/dsh-im",
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@xmanrui/dsh-im",
|
"name": "@xmanrui/dsh-im",
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tencent-connect/qqbot-connector": "1.2.0",
|
"@tencent-connect/qqbot-connector": "1.2.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@xmanrui/dsh-im",
|
"name": "@xmanrui/dsh-im",
|
||||||
"version": "4.1.0",
|
"version": "4.1.1",
|
||||||
"description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
|
"description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"deepseek-harness",
|
"deepseek-harness",
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,15 @@ function conversationKey(message) {
|
||||||
return `${message.kind}:${message.kind === 'group' ? message.groupOpenid : message.senderId}`;
|
return `${message.kind}:${message.kind === 'group' ? message.groupOpenid : message.senderId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function senderAllowed(message, ownerUserOpenid) {
|
||||||
|
// QR binding yields a C2C user_openid, while group events identify senders
|
||||||
|
// with a group-scoped member_openid. Treat group membership plus @mention as
|
||||||
|
// the access boundary, and keep the scanner restriction for private chats.
|
||||||
|
return message?.kind === 'group'
|
||||||
|
|| ownerUserOpenid === '*'
|
||||||
|
|| message?.senderId === ownerUserOpenid;
|
||||||
|
}
|
||||||
|
|
||||||
function safeText(message) {
|
function safeText(message) {
|
||||||
return typeof message?.content === 'string' ? message.content.trim() : '';
|
return typeof message?.content === 'string' ? message.content.trim() : '';
|
||||||
}
|
}
|
||||||
|
|
@ -442,7 +451,7 @@ export class QqHarnessBridge {
|
||||||
}
|
}
|
||||||
const pending = this.#pendingInteractions.get(key);
|
const pending = this.#pendingInteractions.get(key);
|
||||||
const commandText = safeText(message);
|
const commandText = safeText(message);
|
||||||
const allowed = this.#ownerUserOpenid === '*' || sender === this.#ownerUserOpenid;
|
const allowed = senderAllowed(message, this.#ownerUserOpenid);
|
||||||
const addressed = message.kind !== 'group'
|
const addressed = message.kind !== 'group'
|
||||||
|| message.rawEventType === 'GROUP_AT_MESSAGE_CREATE';
|
|| message.rawEventType === 'GROUP_AT_MESSAGE_CREATE';
|
||||||
const batchCommand = isBatchInputCommand(commandText);
|
const batchCommand = isBatchInputCommand(commandText);
|
||||||
|
|
@ -569,7 +578,7 @@ export class QqHarnessBridge {
|
||||||
alreadyRecorded = false,
|
alreadyRecorded = false,
|
||||||
batchSubmission = null,
|
batchSubmission = null,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const allowed = this.#ownerUserOpenid === '*' || message.senderId === this.#ownerUserOpenid;
|
const allowed = senderAllowed(message, this.#ownerUserOpenid);
|
||||||
const addressed = message.kind !== 'group'
|
const addressed = message.kind !== 'group'
|
||||||
|| message.rawEventType === 'GROUP_AT_MESSAGE_CREATE';
|
|| message.rawEventType === 'GROUP_AT_MESSAGE_CREATE';
|
||||||
const preparedMessage = allowed && addressed
|
const preparedMessage = allowed && addressed
|
||||||
|
|
@ -721,7 +730,7 @@ export class QqHarnessBridge {
|
||||||
await this.#state.markSeen(messageId);
|
await this.#state.markSeen(messageId);
|
||||||
messageRecorded = true;
|
messageRecorded = true;
|
||||||
};
|
};
|
||||||
if (this.#ownerUserOpenid !== '*' && sender !== this.#ownerUserOpenid) {
|
if (!senderAllowed(message, this.#ownerUserOpenid)) {
|
||||||
this.#status.messagesRejected += 1;
|
this.#status.messagesRejected += 1;
|
||||||
this.#status.lastRejectedAt = new Date().toISOString();
|
this.#status.lastRejectedAt = new Date().toISOString();
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -789,6 +789,7 @@ test('QQ remembers any authorized private inbound as a connection-test target',
|
||||||
await bridge.accept(message({
|
await bridge.accept(message({
|
||||||
kind: 'group',
|
kind: 'group',
|
||||||
rawEventType: 'GROUP_AT_MESSAGE_CREATE',
|
rawEventType: 'GROUP_AT_MESSAGE_CREATE',
|
||||||
|
senderId: 'group-member-openid',
|
||||||
groupOpenid: 'group-1',
|
groupOpenid: 'group-1',
|
||||||
messageId: 'help-group',
|
messageId: 'help-group',
|
||||||
content: '/help',
|
content: '/help',
|
||||||
|
|
@ -1089,8 +1090,8 @@ test('QQ keeps a stopped turn terminal when its notice cannot be sent', async ()
|
||||||
assert.equal(fixture.seen.has('qq-stopped-stream-fallback'), true);
|
assert.equal(fixture.seen.has('qq-stopped-stream-fallback'), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('QQ bridge accepts only the scanner and requires an at-message event in groups', async () => {
|
test('QQ bridge keeps private chats scanner-only and accepts any mentioned group member', async () => {
|
||||||
let asks = 0;
|
const asks = [];
|
||||||
const state = {
|
const state = {
|
||||||
hasSeen: () => false,
|
hasSeen: () => false,
|
||||||
markSeen: async () => {},
|
markSeen: async () => {},
|
||||||
|
|
@ -1102,15 +1103,31 @@ test('QQ bridge accepts only the scanner and requires an at-message event in gro
|
||||||
const bridge = new QqHarnessBridge({
|
const bridge = new QqHarnessBridge({
|
||||||
bot: { sendText: async () => {} },
|
bot: { sendText: async () => {} },
|
||||||
ownerUserOpenid: 'owner-openid',
|
ownerUserOpenid: 'owner-openid',
|
||||||
harness: { sessionExists: async () => true, ask: async () => { asks += 1; return 'ok'; } },
|
harness: {
|
||||||
|
sessionExists: async () => true,
|
||||||
|
ask: async (sessionId, text) => { asks.push({ sessionId, text }); return 'ok'; },
|
||||||
|
},
|
||||||
state,
|
state,
|
||||||
});
|
});
|
||||||
await bridge.accept(message({ messageId: 'other', senderId: 'other-openid' }));
|
await bridge.accept(message({ messageId: 'other', senderId: 'other-openid' }));
|
||||||
await bridge.accept(message({
|
await bridge.accept(message({
|
||||||
messageId: 'group', kind: 'group', groupOpenid: 'group-1', rawEventType: 'GROUP_MESSAGE_CREATE',
|
messageId: 'group-unmentioned',
|
||||||
replyTarget: { scope: 'group', targetId: 'group-1', msgId: 'group' },
|
kind: 'group',
|
||||||
|
senderId: 'other-member-openid',
|
||||||
|
groupOpenid: 'group-1',
|
||||||
|
rawEventType: 'GROUP_MESSAGE_CREATE',
|
||||||
|
replyTarget: { scope: 'group', targetId: 'group-1', msgId: 'group-unmentioned' },
|
||||||
}));
|
}));
|
||||||
assert.equal(asks, 0);
|
await bridge.accept(message({
|
||||||
|
messageId: 'group-mentioned',
|
||||||
|
kind: 'group',
|
||||||
|
senderId: 'other-member-openid',
|
||||||
|
groupOpenid: 'group-1',
|
||||||
|
rawEventType: 'GROUP_AT_MESSAGE_CREATE',
|
||||||
|
content: '群成员的问题',
|
||||||
|
replyTarget: { scope: 'group', targetId: 'group-1', msgId: 'group-mentioned' },
|
||||||
|
}));
|
||||||
|
assert.deepEqual(asks, [{ sessionId: 'session', text: '群成员的问题' }]);
|
||||||
assert.equal(bridge.status.messagesRejected, 1);
|
assert.equal(bridge.status.messagesRejected, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue