@things-factory/ai-assistant 10.1.178 → 10.1.179
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/components/assistant-chat.ts +19 -12
- package/client/utils/attachment-policy.ts +41 -32
- package/dist-client/components/assistant-chat.d.ts +1 -0
- package/dist-client/components/assistant-chat.js +14 -11
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/attachment-policy.d.ts +10 -6
- package/dist-client/utils/attachment-policy.js +40 -39
- package/dist-client/utils/attachment-policy.js.map +1 -1
- package/dist-server/service/chat-session/attachment-policy.js +8 -2
- package/dist-server/service/chat-session/attachment-policy.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -9
- package/server/service/chat-session/attachment-policy.test.ts +1 -1
- package/server/service/chat-session/attachment-policy.ts +7 -2
- package/test/assistant-dock.test.ts +1 -1
- package/test/attachment-one-rule.test.ts +46 -0
- package/test/attachment-policy-agree.test.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/ai-assistant",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.179",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"things-factory": true,
|
|
6
6
|
"author": "heartyoh",
|
|
@@ -29,18 +29,19 @@
|
|
|
29
29
|
"@material/web": "^2.0.0",
|
|
30
30
|
"@operato/graphql": "^10.0.0",
|
|
31
31
|
"@operato/i18n": "^10.0.0",
|
|
32
|
-
"@operato/input": "^10.
|
|
32
|
+
"@operato/input": "^10.29.37",
|
|
33
33
|
"@operato/layout": "^10.20.0",
|
|
34
34
|
"@operato/popup": "^10.16.0",
|
|
35
35
|
"@operato/shell": "^10.0.0",
|
|
36
36
|
"@operato/styles": "^10.0.0",
|
|
37
|
-
"@
|
|
38
|
-
"@things-factory/
|
|
39
|
-
"@things-factory/
|
|
40
|
-
"@things-factory/
|
|
37
|
+
"@operato/utils": "^10.29.38",
|
|
38
|
+
"@things-factory/ai-client-base": "^10.1.179",
|
|
39
|
+
"@things-factory/apptool-base": "^10.1.179",
|
|
40
|
+
"@things-factory/attachment-base": "^10.1.179",
|
|
41
|
+
"@things-factory/auth-base": "^10.1.179",
|
|
41
42
|
"@things-factory/env": "^10.1.149",
|
|
42
|
-
"@things-factory/setting-base": "^10.1.
|
|
43
|
-
"@things-factory/shell": "^10.1.
|
|
43
|
+
"@things-factory/setting-base": "^10.1.179",
|
|
44
|
+
"@things-factory/shell": "^10.1.179",
|
|
44
45
|
"dompurify": "^3.0.0",
|
|
45
46
|
"exceljs": "4.2.0",
|
|
46
47
|
"graphql-scalars": "^1.22.0",
|
|
@@ -54,5 +55,5 @@
|
|
|
54
55
|
"copyfiles": "^2.4.1",
|
|
55
56
|
"rimraf": "^5.0.0"
|
|
56
57
|
},
|
|
57
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "4c8263b08e06b2b15f11dc0575c61a280a0d9e27"
|
|
58
59
|
}
|
|
@@ -23,7 +23,7 @@ describe('attachment policy', () => {
|
|
|
23
23
|
it('each limit refuses by name, before sending', () => {
|
|
24
24
|
expect(refuseAttachments(policy, [{ name: 'a.pdf', type: 'application/pdf', size: 1 }])).toMatchObject({ code: 'attachment-kind-not-accepted', name: 'a.pdf' })
|
|
25
25
|
expect(refuseAttachments(policy, [{ name: 'big.png', type: 'image/png', size: 1001 }])).toMatchObject({ code: 'attachment-too-large', name: 'big.png', maxBytes: 1000 })
|
|
26
|
-
expect(refuseAttachments(policy, [{}, {}, {}]
|
|
26
|
+
expect(refuseAttachments(policy, [{ name: 'a.png', type: 'image/png', size: 1 }, { name: 'b.png', type: 'image/png', size: 1 }, { name: 'c.png', type: 'image/png', size: 1 }])).toEqual({ code: 'too-many-attachments', maxFiles: 2 })
|
|
27
27
|
})
|
|
28
28
|
|
|
29
29
|
it('a kind outside the declared ones is refused', () => {
|
|
@@ -64,16 +64,21 @@ export function refuseAttachments(
|
|
|
64
64
|
if (!files.length) return null
|
|
65
65
|
if (!policy || !policy.kinds?.length) return { code: 'attachments-not-accepted' }
|
|
66
66
|
|
|
67
|
+
/*
|
|
68
|
+
* File by file, in the order sent: its kind, then its size, then whether it is one too many. The screen's picking
|
|
69
|
+
* rule (@operato/utils sortPickedFiles) says them in this order, so the same batch hears the same "no" from both.
|
|
70
|
+
*/
|
|
67
71
|
const maxFiles = policy.maxFiles ?? DEFAULT_MAX_FILES
|
|
68
|
-
if (files.length > maxFiles) return { code: 'too-many-attachments', maxFiles }
|
|
69
|
-
|
|
70
72
|
const maxBytes = policy.maxBytes ?? DEFAULT_MAX_BYTES
|
|
73
|
+
let taken = 0
|
|
71
74
|
for (const file of files) {
|
|
72
75
|
const kind = attachmentKindOf(file)
|
|
73
76
|
if (!kind || !takes(policy, kind)) {
|
|
74
77
|
return { code: 'attachment-kind-not-accepted', name: file.name || '', kinds: policy.kinds }
|
|
75
78
|
}
|
|
76
79
|
if ((file.size ?? 0) > maxBytes) return { code: 'attachment-too-large', name: file.name || '', maxBytes }
|
|
80
|
+
if (taken >= maxFiles) return { code: 'too-many-attachments', maxFiles }
|
|
81
|
+
taken++
|
|
77
82
|
}
|
|
78
83
|
return null
|
|
79
84
|
}
|
|
@@ -139,7 +139,7 @@ describe('attachments in the chat follow the mode', () => {
|
|
|
139
139
|
})
|
|
140
140
|
|
|
141
141
|
it('said before sending, then uploaded to the conversation and sent by id', () => {
|
|
142
|
-
assert.match(chat, /refuseAttachments\(this\.modeView\?\.attachments/)
|
|
142
|
+
assert.match(chat, /refuseAttachments\(\s*this\.modeView\?\.attachments/)
|
|
143
143
|
assert.match(chat, /mutation: ATTACH_TO_CONVERSATION_MUTATION/)
|
|
144
144
|
/* Without it the client sends the File as {} and the server refuses the upload (found on :3700, 2026-09-25). */
|
|
145
145
|
assert.match(chat, /context: \{ hasUpload: true \}/)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { readFileSync } from 'fs'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
|
|
4
|
+
import * as screen from '../client/utils/attachment-policy'
|
|
5
|
+
import * as server from '../server/service/chat-session/attachment-policy'
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* A conversation's attachments are refused on the screen by the one file-picking rule (@operato/utils
|
|
9
|
+
* sortPickedFiles, ADR-0061 2026-10-17); the mode's policy is a limit, narrowed by the server's upload limit. The
|
|
10
|
+
* screen's "no" is only so the person hears it early - the door still refuses by the mode's policy on its own.
|
|
11
|
+
*/
|
|
12
|
+
const read = (file: string) => readFileSync(join(__dirname, '..', file), 'utf8')
|
|
13
|
+
|
|
14
|
+
describe('one rule for a conversation attachment', () => {
|
|
15
|
+
const policy = { kinds: ['image'], maxBytes: 100, maxFiles: 5 } as any
|
|
16
|
+
|
|
17
|
+
it('★ the screen applies the tighter of the mode policy and the server upload limit', () => {
|
|
18
|
+
const file = [{ name: 'a.png', type: 'image/png', size: 60 }]
|
|
19
|
+
expect(screen.refuseAttachments(policy, file)).toBeNull()
|
|
20
|
+
expect(screen.refuseAttachments(policy, file, { maxFileSize: 50 })).toEqual({ code: 'attachment-too-large', name: 'a.png', maxBytes: 50 })
|
|
21
|
+
expect(screen.refuseAttachments(policy, file, { maxFileSize: 1000 })).toBeNull()
|
|
22
|
+
expect(screen.refuseAttachments(policy, [{ name: 'b.png', type: 'image/png', size: 200 }], { maxFileSize: 1000 })).toMatchObject({ maxBytes: 100 })
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('★ the screen judges with the shared rule, not a copy of its own', () => {
|
|
26
|
+
const source = read('client/utils/attachment-policy.ts')
|
|
27
|
+
expect(source).toMatch(/import \{ narrowLimits, sortPickedFiles, type FileUploadLimits \} from '@operato\/utils\/file-acceptance\.js'/)
|
|
28
|
+
expect(source).not.toMatch(/function takes\(|export function attachmentKindOf/)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('★ the door still refuses by the mode policy itself, before and after saving', () => {
|
|
32
|
+
expect(server.refuseAttachments(policy, [{ name: 'a.pdf', type: 'application/pdf', size: 1 }])).toMatchObject({ code: 'attachment-kind-not-accepted' })
|
|
33
|
+
const door = read('server/service/chat-session-resolver.ts')
|
|
34
|
+
expect(door).toMatch(/import \{ refuseAttachments, type AttachmentRefusal \} from '\.\/chat-session\/attachment-policy\.js'/)
|
|
35
|
+
expect(door.match(/refuseAttachments\(policy, /g)?.length).toBe(2)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('★ the attach button opens the one picker; paste and drop take the same rule', () => {
|
|
39
|
+
const chat = read('client/components/assistant-chat.ts')
|
|
40
|
+
expect(chat).not.toMatch(/type="file"/)
|
|
41
|
+
expect(chat).toMatch(/<ox-input-file\s+id="chat-file-input"\s+quiet\s+outside-trigger/)
|
|
42
|
+
expect(chat).toMatch(/\(this\.renderRoot\.querySelector\('#chat-file-input'\) as any\)\?\.pick\(\)/)
|
|
43
|
+
expect(chat).toMatch(/this\.onAttachFiles\(e\.dataTransfer\.files\)/)
|
|
44
|
+
expect(chat).toMatch(/await fileUploadLimits\(\)\s*\)/)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -13,7 +13,10 @@ const fileSets = [
|
|
|
13
13
|
[{ name: 'report.CSV', type: '', size: 10 }],
|
|
14
14
|
[{ name: 'a.pdf', type: 'application/pdf', size: 10 }],
|
|
15
15
|
[{ name: 'big.png', type: 'image/png', size: 101 }],
|
|
16
|
-
[{ name: 'a.png', type: 'image/png', size: 1 }, { name: 'b.png', type: 'image/png', size: 1 }, { name: 'c.png', type: 'image/png', size: 1 }]
|
|
16
|
+
[{ name: 'a.png', type: 'image/png', size: 1 }, { name: 'b.png', type: 'image/png', size: 1 }, { name: 'c.png', type: 'image/png', size: 1 }],
|
|
17
|
+
/* Mixed batches: the same "no" in the same order - kind, then size, then one too many, file by file. */
|
|
18
|
+
[{ name: 'a.png', type: 'image/png', size: 1 }, { name: 'b.png', type: 'image/png', size: 1 }, { name: 'c.pdf', type: 'application/pdf', size: 1 }],
|
|
19
|
+
[{ name: 'a.png', type: 'image/png', size: 1 }, { name: 'b.png', type: 'image/png', size: 1 }, { name: 'c.png', type: 'image/png', size: 101 }]
|
|
17
20
|
]
|
|
18
21
|
|
|
19
22
|
describe('the screen and the door refuse the same things', () => {
|