@corgelabs/corge 0.0.1 → 1.0.0
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/dist/ai/index.d.ts +3 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/index.js +3 -0
- package/dist/ai/index.js.map +1 -0
- package/dist/ai/schema.d.ts +83 -0
- package/dist/ai/schema.d.ts.map +1 -0
- package/dist/ai/schema.js +19 -0
- package/dist/ai/schema.js.map +1 -0
- package/dist/ai/service.d.ts +14 -0
- package/dist/ai/service.d.ts.map +1 -0
- package/dist/ai/service.js +266 -0
- package/dist/ai/service.js.map +1 -0
- package/dist/ai/utils.d.ts +10 -0
- package/dist/ai/utils.d.ts.map +1 -0
- package/dist/ai/utils.js +27 -0
- package/dist/ai/utils.js.map +1 -0
- package/dist/attachments/index.d.ts +4 -0
- package/dist/attachments/index.d.ts.map +1 -0
- package/dist/attachments/index.js +4 -0
- package/dist/attachments/index.js.map +1 -0
- package/dist/attachments/routes.d.ts +75 -0
- package/dist/attachments/routes.d.ts.map +1 -0
- package/dist/attachments/routes.js +18 -0
- package/dist/attachments/routes.js.map +1 -0
- package/dist/attachments/schema.d.ts +61 -0
- package/dist/attachments/schema.d.ts.map +1 -0
- package/dist/attachments/schema.js +30 -0
- package/dist/attachments/schema.js.map +1 -0
- package/dist/attachments/service.d.ts +27 -0
- package/dist/attachments/service.d.ts.map +1 -0
- package/dist/attachments/service.js +224 -0
- package/dist/attachments/service.js.map +1 -0
- package/dist/attachments/utils.d.ts +9 -0
- package/dist/attachments/utils.d.ts.map +1 -0
- package/dist/attachments/utils.js +80 -0
- package/dist/attachments/utils.js.map +1 -0
- package/dist/auth/index.d.ts +4 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +4 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/oidc.d.ts +25 -0
- package/dist/auth/oidc.d.ts.map +1 -0
- package/dist/auth/oidc.js +298 -0
- package/dist/auth/oidc.js.map +1 -0
- package/dist/auth/recovery.d.ts +41 -0
- package/dist/auth/recovery.d.ts.map +1 -0
- package/dist/auth/recovery.js +250 -0
- package/dist/auth/recovery.js.map +1 -0
- package/dist/auth/routes.d.ts +226 -0
- package/dist/auth/routes.d.ts.map +1 -0
- package/dist/auth/routes.js +96 -0
- package/dist/auth/routes.js.map +1 -0
- package/dist/auth/schema.d.ts +158 -0
- package/dist/auth/schema.d.ts.map +1 -0
- package/dist/auth/schema.js +82 -0
- package/dist/auth/schema.js.map +1 -0
- package/dist/auth/service.d.ts +96 -0
- package/dist/auth/service.d.ts.map +1 -0
- package/dist/auth/service.js +129 -0
- package/dist/auth/service.js.map +1 -0
- package/dist/auth/session.d.ts +51 -0
- package/dist/auth/session.d.ts.map +1 -0
- package/dist/auth/session.js +344 -0
- package/dist/auth/session.js.map +1 -0
- package/dist/auth/utils.d.ts +17 -0
- package/dist/auth/utils.d.ts.map +1 -0
- package/dist/auth/utils.js +96 -0
- package/dist/auth/utils.js.map +1 -0
- package/dist/bin/corge.js +547 -0
- package/dist/chat/context.d.ts +10 -0
- package/dist/chat/context.d.ts.map +1 -0
- package/dist/chat/context.js +221 -0
- package/dist/chat/context.js.map +1 -0
- package/dist/chat/index.d.ts +4 -0
- package/dist/chat/index.d.ts.map +1 -0
- package/dist/chat/index.js +4 -0
- package/dist/chat/index.js.map +1 -0
- package/dist/chat/routes.d.ts +160 -0
- package/dist/chat/routes.d.ts.map +1 -0
- package/dist/chat/routes.js +210 -0
- package/dist/chat/routes.js.map +1 -0
- package/dist/chat/schema.d.ts +285 -0
- package/dist/chat/schema.d.ts.map +1 -0
- package/dist/chat/schema.js +74 -0
- package/dist/chat/schema.js.map +1 -0
- package/dist/chat/service.d.ts +64 -0
- package/dist/chat/service.d.ts.map +1 -0
- package/dist/chat/service.js +279 -0
- package/dist/chat/service.js.map +1 -0
- package/dist/chat/stream.d.ts +53 -0
- package/dist/chat/stream.d.ts.map +1 -0
- package/dist/chat/stream.js +339 -0
- package/dist/chat/stream.js.map +1 -0
- package/dist/chat/utils.d.ts +61 -0
- package/dist/chat/utils.d.ts.map +1 -0
- package/dist/chat/utils.js +285 -0
- package/dist/chat/utils.js.map +1 -0
- package/dist/cli/builder.d.ts +6 -0
- package/dist/cli/builder.d.ts.map +1 -0
- package/dist/cli/builder.js +91 -0
- package/dist/cli/builder.js.map +1 -0
- package/dist/cli/deployer.d.ts +2 -0
- package/dist/cli/deployer.d.ts.map +1 -0
- package/dist/cli/deployer.js +152 -0
- package/dist/cli/deployer.js.map +1 -0
- package/dist/cli/dev-server.d.ts +2 -0
- package/dist/cli/dev-server.d.ts.map +1 -0
- package/dist/cli/dev-server.js +70 -0
- package/dist/cli/dev-server.js.map +1 -0
- package/dist/cli/env.d.ts +4 -0
- package/dist/cli/env.d.ts.map +1 -0
- package/dist/cli/env.js +68 -0
- package/dist/cli/env.js.map +1 -0
- package/dist/cli/init.d.ts +6 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +103 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/config.d.ts +262 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +212 -0
- package/dist/config.js.map +1 -0
- package/dist/db/index.d.ts +3 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +3 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema.d.ts +410 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +294 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/db/service.d.ts +42 -0
- package/dist/db/service.d.ts.map +1 -0
- package/dist/db/service.js +372 -0
- package/dist/db/service.js.map +1 -0
- package/dist/db/utils.d.ts +25 -0
- package/dist/db/utils.d.ts.map +1 -0
- package/dist/db/utils.js +188 -0
- package/dist/db/utils.js.map +1 -0
- package/dist/emails/index.d.ts +4 -0
- package/dist/emails/index.d.ts.map +1 -0
- package/dist/emails/index.js +4 -0
- package/dist/emails/index.js.map +1 -0
- package/dist/emails/routes.d.ts +191 -0
- package/dist/emails/routes.d.ts.map +1 -0
- package/dist/emails/routes.js +39 -0
- package/dist/emails/routes.js.map +1 -0
- package/dist/emails/schema.d.ts +237 -0
- package/dist/emails/schema.d.ts.map +1 -0
- package/dist/emails/schema.js +98 -0
- package/dist/emails/schema.js.map +1 -0
- package/dist/emails/service.d.ts +22 -0
- package/dist/emails/service.d.ts.map +1 -0
- package/dist/emails/service.js +337 -0
- package/dist/emails/service.js.map +1 -0
- package/dist/emails/utils.d.ts +49 -0
- package/dist/emails/utils.d.ts.map +1 -0
- package/dist/emails/utils.js +341 -0
- package/dist/emails/utils.js.map +1 -0
- package/dist/files/index.d.ts +4 -0
- package/dist/files/index.d.ts.map +1 -0
- package/dist/files/index.js +4 -0
- package/dist/files/index.js.map +1 -0
- package/dist/files/routes.d.ts +72 -0
- package/dist/files/routes.d.ts.map +1 -0
- package/dist/files/routes.js +38 -0
- package/dist/files/routes.js.map +1 -0
- package/dist/files/schema.d.ts +57 -0
- package/dist/files/schema.d.ts.map +1 -0
- package/dist/files/schema.js +39 -0
- package/dist/files/schema.js.map +1 -0
- package/dist/files/service.d.ts +27 -0
- package/dist/files/service.d.ts.map +1 -0
- package/dist/files/service.js +175 -0
- package/dist/files/service.js.map +1 -0
- package/dist/files/utils.d.ts +3 -0
- package/dist/files/utils.d.ts.map +1 -0
- package/dist/files/utils.js +63 -0
- package/dist/files/utils.js.map +1 -0
- package/dist/index.d.ts +875 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9919 -0
- package/dist/index.js.map +7 -0
- package/dist/payments/billing.d.ts +29 -0
- package/dist/payments/billing.d.ts.map +1 -0
- package/dist/payments/billing.js +378 -0
- package/dist/payments/billing.js.map +1 -0
- package/dist/payments/index.d.ts +4 -0
- package/dist/payments/index.d.ts.map +1 -0
- package/dist/payments/index.js +4 -0
- package/dist/payments/index.js.map +1 -0
- package/dist/payments/routes.d.ts +340 -0
- package/dist/payments/routes.d.ts.map +1 -0
- package/dist/payments/routes.js +56 -0
- package/dist/payments/routes.js.map +1 -0
- package/dist/payments/schema.d.ts +412 -0
- package/dist/payments/schema.d.ts.map +1 -0
- package/dist/payments/schema.js +260 -0
- package/dist/payments/schema.js.map +1 -0
- package/dist/payments/service.d.ts +31 -0
- package/dist/payments/service.d.ts.map +1 -0
- package/dist/payments/service.js +322 -0
- package/dist/payments/service.js.map +1 -0
- package/dist/payments/utils.d.ts +61 -0
- package/dist/payments/utils.d.ts.map +1 -0
- package/dist/payments/utils.js +369 -0
- package/dist/payments/utils.js.map +1 -0
- package/dist/router/guard.d.ts +19 -0
- package/dist/router/guard.d.ts.map +1 -0
- package/dist/router/guard.js +243 -0
- package/dist/router/guard.js.map +1 -0
- package/dist/router/index.d.ts +3 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +3 -0
- package/dist/router/index.js.map +1 -0
- package/dist/router/schema.d.ts +103 -0
- package/dist/router/schema.d.ts.map +1 -0
- package/dist/router/schema.js +282 -0
- package/dist/router/schema.js.map +1 -0
- package/dist/router/service.d.ts +25 -0
- package/dist/router/service.d.ts.map +1 -0
- package/dist/router/service.js +339 -0
- package/dist/router/service.js.map +1 -0
- package/dist/tasks/index.d.ts +4 -0
- package/dist/tasks/index.d.ts.map +1 -0
- package/dist/tasks/index.js +4 -0
- package/dist/tasks/index.js.map +1 -0
- package/dist/tasks/routes.d.ts +54 -0
- package/dist/tasks/routes.d.ts.map +1 -0
- package/dist/tasks/routes.js +195 -0
- package/dist/tasks/routes.js.map +1 -0
- package/dist/tasks/schema.d.ts +261 -0
- package/dist/tasks/schema.d.ts.map +1 -0
- package/dist/tasks/schema.js +35 -0
- package/dist/tasks/schema.js.map +1 -0
- package/dist/tasks/service.d.ts +32 -0
- package/dist/tasks/service.d.ts.map +1 -0
- package/dist/tasks/service.js +201 -0
- package/dist/tasks/service.js.map +1 -0
- package/dist/tasks/steps.d.ts +18 -0
- package/dist/tasks/steps.d.ts.map +1 -0
- package/dist/tasks/steps.js +365 -0
- package/dist/tasks/steps.js.map +1 -0
- package/dist/tasks/utils.d.ts +75 -0
- package/dist/tasks/utils.d.ts.map +1 -0
- package/dist/tasks/utils.js +311 -0
- package/dist/tasks/utils.js.map +1 -0
- package/dist/tasks/watcher.d.ts +13 -0
- package/dist/tasks/watcher.d.ts.map +1 -0
- package/dist/tasks/watcher.js +255 -0
- package/dist/tasks/watcher.js.map +1 -0
- package/dist/tasks/worker.d.ts +28 -0
- package/dist/tasks/worker.d.ts.map +1 -0
- package/dist/tasks/worker.js +326 -0
- package/dist/tasks/worker.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/schema.d.ts +79 -0
- package/dist/utils/schema.d.ts.map +1 -0
- package/dist/utils/schema.js +41 -0
- package/dist/utils/schema.js.map +1 -0
- package/dist/utils/service.d.ts +32 -0
- package/dist/utils/service.d.ts.map +1 -0
- package/dist/utils/service.js +384 -0
- package/dist/utils/service.js.map +1 -0
- package/docs/SDK_SPEC.md +1645 -0
- package/infra/firestore.base.indexes.json +138 -0
- package/infra/firestore.base.rules +152 -0
- package/infra/storage.base.rules +8 -0
- package/package.json +59 -13
- package/src/ai/index.ts +2 -0
- package/src/ai/schema.ts +64 -0
- package/src/ai/service.ts +317 -0
- package/src/ai/utils.ts +38 -0
- package/src/attachments/index.ts +3 -0
- package/src/attachments/routes.ts +28 -0
- package/src/attachments/schema.ts +45 -0
- package/src/attachments/service.ts +315 -0
- package/src/attachments/utils.ts +115 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/oidc.ts +419 -0
- package/src/auth/recovery.ts +365 -0
- package/src/auth/routes.ts +142 -0
- package/src/auth/schema.ts +181 -0
- package/src/auth/service.ts +183 -0
- package/src/auth/session.ts +414 -0
- package/src/auth/utils.ts +106 -0
- package/src/chat/context.ts +310 -0
- package/src/chat/index.ts +3 -0
- package/src/chat/routes.ts +264 -0
- package/src/chat/schema.ts +295 -0
- package/src/chat/service.ts +360 -0
- package/src/chat/stream.ts +481 -0
- package/src/chat/utils.ts +370 -0
- package/src/cli/builder.ts +93 -0
- package/src/cli/deployer.ts +184 -0
- package/src/cli/dev-server.ts +77 -0
- package/src/cli/env.ts +89 -0
- package/src/cli/init.ts +125 -0
- package/src/config.ts +447 -0
- package/src/db/index.ts +2 -0
- package/src/db/schema.ts +409 -0
- package/src/db/service.ts +477 -0
- package/src/db/utils.ts +250 -0
- package/src/emails/index.ts +3 -0
- package/src/emails/routes.ts +64 -0
- package/src/emails/schema.ts +239 -0
- package/src/emails/service.ts +488 -0
- package/src/emails/utils.ts +449 -0
- package/src/files/index.ts +3 -0
- package/src/files/routes.ts +44 -0
- package/src/files/schema.ts +76 -0
- package/src/files/service.ts +238 -0
- package/src/files/utils.ts +77 -0
- package/src/index.ts +425 -0
- package/src/payments/billing.ts +488 -0
- package/src/payments/index.ts +3 -0
- package/src/payments/routes.ts +92 -0
- package/src/payments/schema.ts +401 -0
- package/src/payments/service.ts +445 -0
- package/src/payments/utils.ts +472 -0
- package/src/router/guard.ts +319 -0
- package/src/router/index.ts +2 -0
- package/src/router/schema.ts +458 -0
- package/src/router/service.ts +453 -0
- package/src/tasks/index.ts +11 -0
- package/src/tasks/routes.ts +230 -0
- package/src/tasks/schema.ts +310 -0
- package/src/tasks/service.ts +254 -0
- package/src/tasks/steps.ts +472 -0
- package/src/tasks/utils.ts +445 -0
- package/src/tasks/watcher.ts +307 -0
- package/src/tasks/worker.ts +461 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/schema.ts +69 -0
- package/src/utils/service.ts +461 -0
- package/tests/e2e.test.ts +2531 -0
- package/tsconfig.base.json +12 -0
- package/README.md +0 -2
- package/index.d.ts +0 -1
- package/index.js +0 -1
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import type { JevQuestion } from '../ai'
|
|
2
|
+
import type { SelectModel } from '../db'
|
|
3
|
+
import { fileScan, fileScanError, type FileRef, type FileScan, type FileScanError } from '../files'
|
|
4
|
+
import type { Core } from '../index'
|
|
5
|
+
import type { StepRunner } from '../tasks'
|
|
6
|
+
import {
|
|
7
|
+
getRegisteredAttachmentTypes,
|
|
8
|
+
processAttachmentsOutput,
|
|
9
|
+
scanAttachmentInput,
|
|
10
|
+
scanAttachmentOutput,
|
|
11
|
+
type AttachmentTypesMap,
|
|
12
|
+
type ProcessedAttachment,
|
|
13
|
+
type ProcessAttachmentsInput,
|
|
14
|
+
type ProcessAttachmentsOutput,
|
|
15
|
+
type ScanAttachmentInput,
|
|
16
|
+
type ScanAttachmentOutput
|
|
17
|
+
} from './schema'
|
|
18
|
+
import {
|
|
19
|
+
buildScanError,
|
|
20
|
+
classifyPermanentScanDefect,
|
|
21
|
+
extractAttachmentTasks,
|
|
22
|
+
parseFields,
|
|
23
|
+
schemaFor
|
|
24
|
+
} from './utils'
|
|
25
|
+
|
|
26
|
+
export class AttachmentsService {
|
|
27
|
+
readonly #core: Core
|
|
28
|
+
|
|
29
|
+
constructor(coreClient: Core) {
|
|
30
|
+
this.#core = coreClient
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#wrapScanStep = async <T>(fn: () => Promise<T>, onDefect: (defect: FileScanError) => void): Promise<T> => {
|
|
34
|
+
try {
|
|
35
|
+
return await fn()
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const defect = classifyPermanentScanDefect(err)
|
|
38
|
+
if (defect) {
|
|
39
|
+
onDefect(defect)
|
|
40
|
+
return this.#core.errors.nonRetryableError(defect.agentHint, defect.reason)
|
|
41
|
+
}
|
|
42
|
+
throw err
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#findCrossUserByShaSum = async (
|
|
47
|
+
ref: FileRef,
|
|
48
|
+
shaSum: string,
|
|
49
|
+
requestedType?: string
|
|
50
|
+
): Promise<{ scan?: FileScan; scanError?: FileScanError } | undefined> => {
|
|
51
|
+
if (!shaSum || shaSum.length < 24) return undefined
|
|
52
|
+
try {
|
|
53
|
+
await this.#core.internals.resilience.acquireLimit('firestore')
|
|
54
|
+
const snap = await this.#core.db.firestore().collection('file').where('shaSum', '==', shaSum).limit(5).get()
|
|
55
|
+
for (const doc of snap.docs) {
|
|
56
|
+
if (doc.id === ref) continue
|
|
57
|
+
const data = doc.data()
|
|
58
|
+
if (data.deleted === true) continue
|
|
59
|
+
if (data.scan) {
|
|
60
|
+
const parsedScan = fileScan.safeParse(data.scan)
|
|
61
|
+
if (parsedScan.success && (!requestedType || parsedScan.data.type === requestedType)) {
|
|
62
|
+
return { scan: parsedScan.data }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (data.scanError) {
|
|
66
|
+
const parsedErr = fileScanError.safeParse(data.scanError)
|
|
67
|
+
if (parsedErr.success) {
|
|
68
|
+
return { scanError: parsedErr.data }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch {}
|
|
73
|
+
return undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
classify = async (
|
|
77
|
+
attachmentContent: string,
|
|
78
|
+
types: AttachmentTypesMap = getRegisteredAttachmentTypes()
|
|
79
|
+
): Promise<{ attachmentType: string; confidence: number }> => {
|
|
80
|
+
const entries = Object.entries(types)
|
|
81
|
+
if (!entries.length) return { attachmentType: 'other', confidence: 1 }
|
|
82
|
+
const questions: Record<string, JevQuestion> = {
|
|
83
|
+
documentType: {
|
|
84
|
+
type: 'choice',
|
|
85
|
+
instructions:
|
|
86
|
+
'Classify the attached document (its full text is the state) into exactly one type. Choose "other" when it matches none of the specific types.',
|
|
87
|
+
criteria: {
|
|
88
|
+
...Object.fromEntries(entries.map(([k, v]) => [k, v.description])),
|
|
89
|
+
other: 'Unrecognized document, plain image/diagram, or none of the specific attachment types'
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const { answers } = await this.#core.ai.decide({ document: attachmentContent.slice(0, 80_000) }, questions)
|
|
94
|
+
const ans = answers.documentType
|
|
95
|
+
return ans?.type === 'choice'
|
|
96
|
+
? { attachmentType: ans.choice, confidence: ans.confidence }
|
|
97
|
+
: { attachmentType: 'other', confidence: 0 }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#executeScanOcrAndClassify = async (opts: {
|
|
101
|
+
ref: FileRef
|
|
102
|
+
contentType: string
|
|
103
|
+
initialType?: string
|
|
104
|
+
types: AttachmentTypesMap
|
|
105
|
+
step: StepRunner
|
|
106
|
+
onDefect: (d: FileScanError) => void
|
|
107
|
+
}): Promise<FileScan> => {
|
|
108
|
+
const { ref, contentType, initialType, types, step, onDefect } = opts
|
|
109
|
+
const isImage = contentType.startsWith('image/')
|
|
110
|
+
let dataUrlP: Promise<string> | undefined
|
|
111
|
+
const getDataUrl = () =>
|
|
112
|
+
(dataUrlP ??= (async () =>
|
|
113
|
+
`data:${contentType};base64,${(await this.#core.files.download(ref)).toString('base64')}`)())
|
|
114
|
+
|
|
115
|
+
const hasTypes = Object.keys(types).length > 0
|
|
116
|
+
let resolvedType: string | undefined = hasTypes ? initialType : 'other'
|
|
117
|
+
let confidence = 1
|
|
118
|
+
let markdown = ''
|
|
119
|
+
|
|
120
|
+
if (!resolvedType || resolvedType === 'other') {
|
|
121
|
+
const ocrRes = await step('ocr', ({ signal }) =>
|
|
122
|
+
this.#wrapScanStep(async () => this.#core.ai.vision(await getDataUrl(), { signal }), onDefect)
|
|
123
|
+
)
|
|
124
|
+
markdown = ocrRes.markdown
|
|
125
|
+
if (!markdown) {
|
|
126
|
+
markdown = await step('vision', ({ signal }) =>
|
|
127
|
+
this.#wrapScanStep(async () => this.#core.ai.describeImage(await getDataUrl(), signal), onDefect)
|
|
128
|
+
)
|
|
129
|
+
resolvedType = 'other'
|
|
130
|
+
} else {
|
|
131
|
+
if (!resolvedType) {
|
|
132
|
+
const classified = await step('classify', () =>
|
|
133
|
+
this.#wrapScanStep(() => this.classify(markdown, types), onDefect)
|
|
134
|
+
)
|
|
135
|
+
resolvedType = classified.attachmentType
|
|
136
|
+
confidence = classified.confidence
|
|
137
|
+
}
|
|
138
|
+
if (hasTypes && resolvedType === 'other' && isImage) {
|
|
139
|
+
const visionDesc = await step('vision', ({ signal }) =>
|
|
140
|
+
this.#wrapScanStep(async () => this.#core.ai.describeImage(await getDataUrl(), signal), onDefect)
|
|
141
|
+
)
|
|
142
|
+
markdown = `${markdown}\n\n${visionDesc}`.trim()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let fields: Record<string, unknown> = {}
|
|
148
|
+
const schema = resolvedType && resolvedType !== 'other' ? schemaFor(resolvedType, types) : undefined
|
|
149
|
+
if (schema && resolvedType) {
|
|
150
|
+
const extract = await step('extract', ({ signal }) =>
|
|
151
|
+
this.#wrapScanStep(async () => this.#core.ai.vision(await getDataUrl(), { schema, signal }), onDefect)
|
|
152
|
+
)
|
|
153
|
+
fields = parseFields(resolvedType, extract.annotation, types)
|
|
154
|
+
if (!markdown) markdown = extract.markdown
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (markdown.trim().length < 5 && Object.keys(fields).length === 0) {
|
|
158
|
+
const defect = buildScanError(
|
|
159
|
+
'EMPTY_OR_UNREADABLE',
|
|
160
|
+
'The uploaded document appears blank or unreadable (fewer than 5 characters detected). Please upload a clear, legible scan or photo of the document.'
|
|
161
|
+
)
|
|
162
|
+
onDefect(defect)
|
|
163
|
+
return this.#core.errors.nonRetryableError(defect.agentHint, defect.reason)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { type: resolvedType ?? 'other', confidence, fields, markdown }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
scan = async (
|
|
170
|
+
raw: ScanAttachmentInput,
|
|
171
|
+
opts?: { types?: AttachmentTypesMap; step?: StepRunner; preloadedFile?: SelectModel<'file'> }
|
|
172
|
+
): Promise<ScanAttachmentOutput> => {
|
|
173
|
+
const step = opts?.step ?? this.#core.step
|
|
174
|
+
const types = opts?.types ?? getRegisteredAttachmentTypes()
|
|
175
|
+
const preloadedFile = opts?.preloadedFile
|
|
176
|
+
const { ref, type } = scanAttachmentInput.parse(raw)
|
|
177
|
+
if (type && type !== 'other' && Object.keys(types).length > 0 && !(type in types)) {
|
|
178
|
+
throw new Error(`Invalid attachment type: ${type}`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return this.#core.internals.resilience.singleflight(
|
|
182
|
+
`scan:${this.#core.env.isDev ? 'dev' : 'prod'}:${ref}:${type ?? ''}`,
|
|
183
|
+
async () => {
|
|
184
|
+
const entry =
|
|
185
|
+
preloadedFile ?? (await step('read-meta', () => this.#core.db.read('file', ref, false, { bypassRbac: true })))
|
|
186
|
+
if (!entry) throw new Error(`File not found: ${ref}`)
|
|
187
|
+
if (entry.scan) return scanAttachmentOutput.parse(entry.scan)
|
|
188
|
+
if (entry.scanError) {
|
|
189
|
+
return this.#core.errors.nonRetryableError(entry.scanError.agentHint, entry.scanError.reason)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const shaSum = entry.shaSum || ref.slice(-24)
|
|
193
|
+
const crossUserMatch = await this.#findCrossUserByShaSum(ref, shaSum, type)
|
|
194
|
+
if (crossUserMatch?.scan) {
|
|
195
|
+
await step('persist', () =>
|
|
196
|
+
this.#core.db.update('file', ref, { scan: crossUserMatch.scan!, scanError: null }, { bypassRbac: true })
|
|
197
|
+
)
|
|
198
|
+
return scanAttachmentOutput.parse(crossUserMatch.scan)
|
|
199
|
+
}
|
|
200
|
+
if (crossUserMatch?.scanError) {
|
|
201
|
+
await this.#core.db
|
|
202
|
+
.update('file', ref, { scanError: crossUserMatch.scanError, scan: null }, { bypassRbac: true })
|
|
203
|
+
.catch(() => {})
|
|
204
|
+
return this.#core.errors.nonRetryableError(
|
|
205
|
+
crossUserMatch.scanError.agentHint,
|
|
206
|
+
crossUserMatch.scanError.reason
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let capturedDefect: FileScanError | undefined
|
|
211
|
+
try {
|
|
212
|
+
const scan = await this.#executeScanOcrAndClassify({
|
|
213
|
+
ref,
|
|
214
|
+
contentType: entry.contentType || 'application/pdf',
|
|
215
|
+
initialType: type,
|
|
216
|
+
types,
|
|
217
|
+
step,
|
|
218
|
+
onDefect: (d) => {
|
|
219
|
+
capturedDefect = d
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
await step('persist', () =>
|
|
223
|
+
this.#core.db.update('file', ref, { scan, scanError: null }, { bypassRbac: true })
|
|
224
|
+
)
|
|
225
|
+
return scanAttachmentOutput.parse(scan)
|
|
226
|
+
} catch (err) {
|
|
227
|
+
const defect = capturedDefect ?? classifyPermanentScanDefect(err)
|
|
228
|
+
if (defect) {
|
|
229
|
+
await this.#core.db
|
|
230
|
+
.update('file', ref, { scanError: defect, scan: null }, { bypassRbac: true })
|
|
231
|
+
.catch(() => {})
|
|
232
|
+
if (err instanceof this.#core.errors.NonRetryableError) throw err
|
|
233
|
+
return this.#core.errors.nonRetryableError(defect.agentHint, defect.reason)
|
|
234
|
+
}
|
|
235
|
+
throw err
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#scanShortCircuit = async ({ input }: { input: ScanAttachmentInput }): Promise<ScanAttachmentOutput | undefined> => {
|
|
242
|
+
const entry = await this.#core.db.read('file', input.ref, false, { bypassRbac: true })
|
|
243
|
+
if (entry?.scan) return scanAttachmentOutput.parse(entry.scan)
|
|
244
|
+
if (entry?.scanError) {
|
|
245
|
+
return this.#core.errors.nonRetryableError(entry.scanError.agentHint, entry.scanError.reason)
|
|
246
|
+
}
|
|
247
|
+
return undefined
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#scanTask = async ({
|
|
251
|
+
input,
|
|
252
|
+
step,
|
|
253
|
+
types,
|
|
254
|
+
preloadedFile
|
|
255
|
+
}: {
|
|
256
|
+
input: ScanAttachmentInput
|
|
257
|
+
step: StepRunner
|
|
258
|
+
types?: AttachmentTypesMap
|
|
259
|
+
preloadedFile?: SelectModel<'file'>
|
|
260
|
+
}): Promise<ProcessedAttachment> => {
|
|
261
|
+
try {
|
|
262
|
+
return { ref: input.ref, scan: await this.scan(input, { step, types, preloadedFile }) }
|
|
263
|
+
} catch (err) {
|
|
264
|
+
if (this.#core.errors.isNonRetryableError(err)) {
|
|
265
|
+
return { ref: input.ref, error: err instanceof Error ? err.message : String(err) }
|
|
266
|
+
}
|
|
267
|
+
return { ref: input.ref, error: 'Temporary scanning service unavailability. Please retry.' }
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
process = async (
|
|
272
|
+
raw: ProcessAttachmentsInput,
|
|
273
|
+
opts?: { types?: AttachmentTypesMap; step?: StepRunner }
|
|
274
|
+
): Promise<ProcessAttachmentsOutput> => {
|
|
275
|
+
const tasks = extractAttachmentTasks(raw)
|
|
276
|
+
const step = opts?.step ?? this.#core.step
|
|
277
|
+
|
|
278
|
+
const uniqueRefs = [...new Set(tasks.map((t) => t.ref))]
|
|
279
|
+
const existingFiles = await this.#core.db.list('file', { ids: uniqueRefs }, { bypassRbac: true })
|
|
280
|
+
const fileMap = new Map<FileRef, SelectModel<'file'>>(existingFiles.map((f) => [f.id, f]))
|
|
281
|
+
|
|
282
|
+
const attachments = await step.map(
|
|
283
|
+
'process-attachments',
|
|
284
|
+
tasks,
|
|
285
|
+
(taskInput) =>
|
|
286
|
+
this.#scanTask({
|
|
287
|
+
input: taskInput,
|
|
288
|
+
step: this.#core.internals.workflows.createStepRunner('workflow'),
|
|
289
|
+
types: opts?.types,
|
|
290
|
+
preloadedFile: fileMap.get(taskInput.ref)
|
|
291
|
+
}),
|
|
292
|
+
{
|
|
293
|
+
timeout: `${this.#core.config.tasks.scanDeadlineSeconds}s`,
|
|
294
|
+
shortCircuit: (taskInput) => {
|
|
295
|
+
const fileDoc = fileMap.get(taskInput.ref)
|
|
296
|
+
if (fileDoc?.scan) {
|
|
297
|
+
return { ref: taskInput.ref, scan: scanAttachmentOutput.parse(fileDoc.scan) }
|
|
298
|
+
}
|
|
299
|
+
if (fileDoc?.scanError) {
|
|
300
|
+
return { ref: taskInput.ref, error: fileDoc.scanError.agentHint }
|
|
301
|
+
}
|
|
302
|
+
return undefined
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return processAttachmentsOutput.parse({ attachments })
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
get internals() {
|
|
311
|
+
return {
|
|
312
|
+
scanShortCircuit: this.#scanShortCircuit
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import type { FileScanError, UnparseableReason } from '../files'
|
|
3
|
+
import {
|
|
4
|
+
getRegisteredAttachmentTypes,
|
|
5
|
+
processAttachmentsInput,
|
|
6
|
+
type AttachmentTypesMap,
|
|
7
|
+
type ProcessAttachmentsInput,
|
|
8
|
+
type ScanAttachmentInput
|
|
9
|
+
} from './schema'
|
|
10
|
+
|
|
11
|
+
export const schemaFor = (
|
|
12
|
+
type: string,
|
|
13
|
+
types: AttachmentTypesMap = getRegisteredAttachmentTypes()
|
|
14
|
+
): Record<string, unknown> | undefined =>
|
|
15
|
+
type in types ? (z.toJSONSchema(types[type]!.schema, { io: 'output' }) as Record<string, unknown>) : undefined
|
|
16
|
+
|
|
17
|
+
export const parseFields = (
|
|
18
|
+
type: string,
|
|
19
|
+
fields: Record<string, unknown>,
|
|
20
|
+
types: AttachmentTypesMap = getRegisteredAttachmentTypes()
|
|
21
|
+
): Record<string, unknown> => (type in types ? (types[type]!.schema.parse(fields) as Record<string, unknown>) : {})
|
|
22
|
+
|
|
23
|
+
export const extractAttachmentTasks = (input: ProcessAttachmentsInput): ScanAttachmentInput[] =>
|
|
24
|
+
processAttachmentsInput.parse(input).refs.map((item) => (typeof item === 'string' ? { ref: item } : item))
|
|
25
|
+
|
|
26
|
+
export const buildScanError = (
|
|
27
|
+
reason: UnparseableReason,
|
|
28
|
+
agentHint: string,
|
|
29
|
+
diagnosticMessage?: string
|
|
30
|
+
): FileScanError => ({
|
|
31
|
+
status: 'unparseable',
|
|
32
|
+
reason,
|
|
33
|
+
agentHint,
|
|
34
|
+
...(diagnosticMessage ? { diagnosticMessage: diagnosticMessage.slice(0, 500) } : {}),
|
|
35
|
+
failedAt: new Date().toISOString()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export const extractHttpStatus = (err: unknown): number | undefined => {
|
|
39
|
+
if (!err || typeof err !== 'object') return undefined
|
|
40
|
+
const rec = err as Record<string, unknown>
|
|
41
|
+
if (typeof rec.statusCode === 'number') return rec.statusCode
|
|
42
|
+
if (typeof rec.status === 'number') return rec.status
|
|
43
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
44
|
+
const match = /\b(?:status(?:\s+code)?|http)\s*[:=]?\s*(4\d\d|5\d\d)\b/i.exec(msg)
|
|
45
|
+
return match?.[1] ? Number(match[1]) : undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const TRANSIENT_TOKENS = ['aborted', 'timed out', 'econnreset', 'etimedout', 'socket hang up', 'rate limit']
|
|
49
|
+
|
|
50
|
+
const DEFECT_RULES: ReadonlyArray<{
|
|
51
|
+
reason: UnparseableReason
|
|
52
|
+
hint: string
|
|
53
|
+
match: (status: number | undefined, lower: string) => boolean
|
|
54
|
+
}> = [
|
|
55
|
+
{
|
|
56
|
+
reason: 'PASSWORD_PROTECTED',
|
|
57
|
+
hint: 'This document is password-protected or encrypted and cannot be read. Please upload an unlocked copy of the document.',
|
|
58
|
+
match: (_, lower) => lower.includes('password') || lower.includes('encrypted')
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
reason: 'UNSUPPORTED_FORMAT',
|
|
62
|
+
hint: 'This file format is not supported by the document scanner. Please upload a valid PDF or standard image (JPEG, PNG, WebP).',
|
|
63
|
+
match: (status, lower) =>
|
|
64
|
+
status === 415 || lower.includes('unsupported media') || lower.includes('unsupported format')
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
reason: 'EMPTY_OR_UNREADABLE',
|
|
68
|
+
hint: 'The uploaded document appears blank or unreadable. Please upload a clear, legible scan or photo of the document.',
|
|
69
|
+
match: (_, lower) =>
|
|
70
|
+
lower.includes('empty description') ||
|
|
71
|
+
lower.includes('empty_or_unreadable') ||
|
|
72
|
+
lower.includes('no readable text')
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
reason: 'CLASSIFICATION_REJECTED',
|
|
76
|
+
hint: 'The document could not be classified into a valid document type. Please upload a clear, complete document.',
|
|
77
|
+
match: (_, lower) => lower.includes('classification_rejected')
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
reason: 'CORRUPTED_FILE',
|
|
81
|
+
hint: 'This file appears corrupted or malformed and could not be parsed. Please export or re-upload a valid PDF or image file.',
|
|
82
|
+
match: (status, lower) =>
|
|
83
|
+
status === 400 ||
|
|
84
|
+
status === 422 ||
|
|
85
|
+
lower.includes('corrupt') ||
|
|
86
|
+
lower.includes('malformed') ||
|
|
87
|
+
lower.includes('invalid pdf') ||
|
|
88
|
+
lower.includes('invalid image') ||
|
|
89
|
+
lower.includes('failed to process document') ||
|
|
90
|
+
lower.includes('could not parse')
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
export const classifyPermanentScanDefect = (err: unknown): FileScanError | null => {
|
|
95
|
+
if (!err) return null
|
|
96
|
+
const bodyStr =
|
|
97
|
+
typeof err === 'object' && 'body' in err && typeof (err as { body?: unknown }).body === 'string'
|
|
98
|
+
? (err as { body: string }).body
|
|
99
|
+
: ''
|
|
100
|
+
const msg = `${err instanceof Error ? err.message : String(err)} ${bodyStr}`.trim()
|
|
101
|
+
const lower = msg.toLowerCase()
|
|
102
|
+
const status = extractHttpStatus(err)
|
|
103
|
+
|
|
104
|
+
if (
|
|
105
|
+
status === 408 ||
|
|
106
|
+
status === 429 ||
|
|
107
|
+
(status !== undefined && status >= 500) ||
|
|
108
|
+
TRANSIENT_TOKENS.some((t) => lower.includes(t))
|
|
109
|
+
) {
|
|
110
|
+
return null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const matched = DEFECT_RULES.find((rule) => rule.match(status, lower))
|
|
114
|
+
return matched ? buildScanError(matched.reason, matched.hint, msg) : null
|
|
115
|
+
}
|