feat: add unified IM access policies

This commit is contained in:
xmanrui 2026-09-01 10:45:12 +08:00
parent 075d2713c1
commit 840e5aa857
89 changed files with 5573 additions and 1676 deletions

View file

@ -17,6 +17,10 @@ import {
OutboundArtifactRegistry,
createOutboundArtifactTool,
} from '../../../src/channels/shared/semantic/artifact.mjs';
import {
COMMAND_PERMISSION_DENIED_MESSAGE,
directAccessPolicy,
} from '../access-policy-fixture.mjs';
function deferred() {
let resolve;
@ -475,6 +479,74 @@ test('QQ checks sender and group mention before downloading image attachments',
assert.equal(asks, 0);
});
test('QQ applies the unified access policy before attachments or Harness work', async () => {
const fixture = stateFixture([['c2c:member-openid', 'session-member']]);
let downloads = 0;
const harnessCalls = [];
const sent = [];
const accessPolicy = directAccessPolicy({
users: [{ id: 'member-openid', canExecuteCommands: false }],
privilegedIds: ['owner-openid'],
});
const bridge = new QqHarnessBridge({
bot: {
sendText: async (_target, text) => {
sent.push(text);
return { id: `qq-policy-${sent.length}` };
},
},
ownerUserOpenid: 'owner-openid',
accessPolicy,
harness: {
sessionExists: async (sessionId) => {
harnessCalls.push(['sessionExists', sessionId]);
return true;
},
ask: async (sessionId, prompt) => {
harnessCalls.push(['ask', sessionId, prompt]);
return '白名单消息已处理';
},
},
state: fixture.state,
fetchImpl: async () => {
downloads += 1;
return new Response(PNG_BYTES, { headers: { 'content-type': 'image/png' } });
},
});
const directMessage = (messageId, senderId, content, overrides = {}) => message({
messageId,
senderId,
content,
replyTarget: { scope: 'c2c', targetId: senderId, msgId: messageId },
...overrides,
});
await bridge.accept(directMessage('policy-blocked-image', 'blocked-openid', '', {
attachments: [{
content_type: 'image/png',
filename: 'blocked.png',
url: 'https://multimedia.nt.qq.com.cn/download/blocked',
}],
}));
assert.equal(downloads, 0);
assert.deepEqual(harnessCalls, []);
assert.deepEqual(sent, []);
await bridge.accept(directMessage('policy-member-text', 'member-openid', '普通消息'));
assert.equal(harnessCalls.some(([operation]) => operation === 'ask'), true);
assert.deepEqual(sent, ['白名单消息已处理']);
const callsBeforeDeniedCommand = harnessCalls.length;
const repliesBeforeDeniedCommand = sent.length;
await bridge.accept(directMessage('policy-member-command', 'member-openid', '/help'));
assert.equal(harnessCalls.length, callsBeforeDeniedCommand);
assert.deepEqual(sent.slice(repliesBeforeDeniedCommand), [COMMAND_PERMISSION_DENIED_MESSAGE]);
accessPolicy.getSettings().direct.allowlist.users = [];
await bridge.accept(directMessage('policy-owner-command', 'owner-openid', '/help'));
assert.match(sent.at(-1), /\/help/);
});
test('QQ rejects non-platform image URLs without fetching and returns a retryable image error', async () => {
const fixture = stateFixture([['c2c:owner-openid', 'session-image']]);
const sent = [];