plugin-file-preview-auth 1.3.3 → 1.3.6
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-v2.d.ts +2 -0
- package/client-v2.js +1 -0
- package/dist/client/713.79a55458f5b67f39.js +30 -0
- package/dist/client/823.8b0ab22c181d4523.js +10 -0
- package/dist/client/828.ae8e47a2e7a3bc9e.js +49 -0
- package/dist/client/892.a568eb42fd6f0047.js +10 -0
- package/dist/client/index.js +1 -1
- package/dist/client-v2/index.js +10 -0
- package/dist/externalVersion.js +8 -7
- package/dist/node_modules/@aws-sdk/client-s3/dist-cjs/index.js +3086 -3725
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/.bin/fxparser +16 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/.bin/fxparser.cmd +17 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/.bin/fxparser.ps1 +28 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-cjs/index.js +110 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-es/SignatureV4MultiRegion.js +66 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-es/index.js +2 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-es/signature-v4-crt-container.js +3 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-types/SignatureV4MultiRegion.d.ts +30 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-types/index.d.ts +5 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-types/signature-v4-crt-container.d.ts +28 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-types/ts3.4/SignatureV4MultiRegion.d.ts +40 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-types/ts3.4/index.d.ts +2 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/dist-types/ts3.4/signature-v4-crt-container.d.ts +20 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region/package.json +57 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/AdaptiveRetryStrategy.js +1 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/ConfiguredRetryStrategy.js +1 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/DefaultRateLimiter.js +1 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/StandardRetryStrategy.js +1 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/config.js +1 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/constants.js +1 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/defaultRetryBackoffStrategy.js +1 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/defaultRetryToken.js +1 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/index.js +358 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-cjs/types.js +1 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/AdaptiveRetryStrategy.js +24 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/ConfiguredRetryStrategy.js +18 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/DefaultRateLimiter.js +100 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/StandardRetryStrategy.js +65 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/config.js +7 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/constants.js +9 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/defaultRetryBackoffStrategy.js +14 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/defaultRetryToken.js +11 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/index.js +7 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-es/types.js +1 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/AdaptiveRetryStrategy.d.ts +33 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ConfiguredRetryStrategy.d.ts +32 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/DefaultRateLimiter.d.ts +49 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/StandardRetryStrategy.d.ts +26 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/config.d.ts +20 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/constants.d.ts +59 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/defaultRetryBackoffStrategy.d.ts +5 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/defaultRetryToken.d.ts +9 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/index.d.ts +7 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/AdaptiveRetryStrategy.d.ts +33 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/ConfiguredRetryStrategy.d.ts +32 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/DefaultRateLimiter.d.ts +49 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/StandardRetryStrategy.d.ts +26 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/config.d.ts +20 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/constants.d.ts +59 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/defaultRetryBackoffStrategy.d.ts +5 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/defaultRetryToken.d.ts +9 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/index.d.ts +7 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/ts3.4/types.d.ts +19 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/dist-types/types.d.ts +19 -0
- package/dist/node_modules/@aws-sdk/client-s3/node_modules/@smithy/util-retry/package.json +68 -0
- package/dist/node_modules/@aws-sdk/client-s3/package.json +1 -1
- package/dist/node_modules/xlsx/package.json +1 -1
- package/dist/server/ocr/tesseract-runner.js +3 -1
- package/dist/server/plugin.js +22 -4
- package/package.json +57 -45
- package/src/client/AIFilePreviewAction.tsx +282 -0
- package/src/client/__tests__/ocr-utils.test.ts +85 -0
- package/src/client/client.d.ts +258 -0
- package/src/client/index.tsx +1807 -0
- package/src/client/locale.ts +21 -0
- package/src/client-v2/index.tsx +1 -0
- package/src/client-v2/plugin.tsx +7 -0
- package/{dist/index.d.ts → src/index.ts} +11 -10
- package/src/locale/en-US.json +14 -0
- package/src/locale/vi-VN.json +14 -0
- package/src/locale/zh-CN.json +14 -0
- package/src/server/__tests__/smoke.test.ts +17 -0
- package/src/server/collections/attachment-ocr-results.ts +40 -0
- package/{dist/server/collections/file-preview-auth.d.ts → src/server/collections/file-preview-auth.ts} +15 -14
- package/src/server/excel-parser-handler.ts +128 -0
- package/{dist/server/index.d.ts → src/server/index.ts} +10 -9
- package/src/server/migrations/20260528000000-move-ocr-fields-out-of-attachments.ts +39 -0
- package/src/server/ocr/tesseract-runner.ts +389 -0
- package/src/server/ocr/tesseract-worker.ts +235 -0
- package/src/server/plugin.ts +1470 -0
- package/dist/client/166.17caa11c2ba40313.js +0 -10
- package/dist/client/351.0f0ce45c92425c8f.js +0 -10
- package/dist/client/374.96762d13b15e7467.js +0 -30
- package/dist/client/514.2a8b6aa0d2fcd4b2.js +0 -49
- package/dist/client/AIFilePreviewAction.d.ts +0 -42
- package/dist/client/index.d.ts +0 -14
- package/dist/client/locale.d.ts +0 -10
- package/dist/node_modules/xlsx/node_modules/.bin/crc32 +0 -15
- package/dist/node_modules/xlsx/node_modules/.bin/crc32.cmd +0 -7
- package/dist/server/collections/attachment-ocr-results.d.ts +0 -2
- package/dist/server/excel-parser-handler.d.ts +0 -60
- package/dist/server/migrations/20260528000000-move-ocr-fields-out-of-attachments.d.ts +0 -5
- package/dist/server/ocr/tesseract-runner.d.ts +0 -34
- package/dist/server/ocr/tesseract-worker.d.ts +0 -27
- package/dist/server/plugin.d.ts +0 -54
package/dist/server/plugin.js
CHANGED
|
@@ -325,10 +325,14 @@ class PluginFilePreviewAuthServer extends import_server.Plugin {
|
|
|
325
325
|
const params = ctx.action.params || {};
|
|
326
326
|
const reqBody = ctx.request.body || {};
|
|
327
327
|
const values = params.values || {};
|
|
328
|
-
const
|
|
329
|
-
if (!
|
|
328
|
+
const rawAttachmentId = values.attachmentId || reqBody.attachmentId;
|
|
329
|
+
if (!rawAttachmentId) {
|
|
330
330
|
ctx.throw(400, "attachmentId is required");
|
|
331
331
|
}
|
|
332
|
+
const attachmentId = normalizeOcrAttachmentId(rawAttachmentId);
|
|
333
|
+
if (!attachmentId) {
|
|
334
|
+
ctx.throw(400, "attachmentId must be a numeric attachment record id");
|
|
335
|
+
}
|
|
332
336
|
this.assertAuthenticated(ctx);
|
|
333
337
|
const repo = ctx.db.getRepository("attachments");
|
|
334
338
|
const attachment = await repo.findOne({ filterByTk: attachmentId });
|
|
@@ -352,10 +356,14 @@ class PluginFilePreviewAuthServer extends import_server.Plugin {
|
|
|
352
356
|
const reqQuery = ctx.request.query || {};
|
|
353
357
|
const reqBody = ctx.request.body || {};
|
|
354
358
|
const values = params.values || {};
|
|
355
|
-
const
|
|
356
|
-
if (!
|
|
359
|
+
const rawAttachmentId = values.attachmentId || params.attachmentId || reqQuery.attachmentId || reqBody.attachmentId;
|
|
360
|
+
if (!rawAttachmentId) {
|
|
357
361
|
ctx.throw(400, "attachmentId is required");
|
|
358
362
|
}
|
|
363
|
+
const attachmentId = normalizeOcrAttachmentId(rawAttachmentId);
|
|
364
|
+
if (!attachmentId) {
|
|
365
|
+
ctx.throw(400, "attachmentId must be a numeric attachment record id");
|
|
366
|
+
}
|
|
359
367
|
this.assertAuthenticated(ctx);
|
|
360
368
|
const repo = ctx.db.getRepository("attachments");
|
|
361
369
|
const attachment = await repo.findOne({ filterByTk: attachmentId });
|
|
@@ -1160,6 +1168,16 @@ function isLikelyRecordId(value) {
|
|
|
1160
1168
|
const text = String(value);
|
|
1161
1169
|
return !text.includes("/") && !text.startsWith("http://") && !text.startsWith("https://");
|
|
1162
1170
|
}
|
|
1171
|
+
function normalizeOcrAttachmentId(value) {
|
|
1172
|
+
if (typeof value === "number") {
|
|
1173
|
+
return Number.isInteger(value) && value > 0 ? value : null;
|
|
1174
|
+
}
|
|
1175
|
+
if (typeof value === "string") {
|
|
1176
|
+
const trimmed = value.trim();
|
|
1177
|
+
return /^\d+$/.test(trimmed) ? trimmed : null;
|
|
1178
|
+
}
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1163
1181
|
function getUrlCandidates(value) {
|
|
1164
1182
|
if (!value) return [];
|
|
1165
1183
|
const original = decodePossiblyEncodedUrl(String(value));
|
package/package.json
CHANGED
|
@@ -1,45 +1,57 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "plugin-file-preview-auth",
|
|
3
|
-
"displayName": "Authenticated File Previewer",
|
|
4
|
-
"displayName.vi-VN": "Xem trước file có xác thực",
|
|
5
|
-
"displayName.zh-CN": "认证文件预览",
|
|
6
|
-
"description": "Preview PDF, image, and text files with Bearer token authentication via blob URLs.",
|
|
7
|
-
"description.vi-VN": "Xem trước file PDF, hình ảnh và văn bản với xác thực Bearer token qua blob URL.",
|
|
8
|
-
"description.zh-CN": "通过 Bearer 令牌认证和 Blob URL 预览 PDF、图片和文本文件。",
|
|
9
|
-
"version": "1.3.
|
|
10
|
-
"main": "dist/server/index.js",
|
|
11
|
-
"files": [
|
|
12
|
-
"dist",
|
|
13
|
-
"public",
|
|
14
|
-
"client.js",
|
|
15
|
-
"client.d.ts",
|
|
16
|
-
"server.js",
|
|
17
|
-
"server.d.ts",
|
|
18
|
-
"README.md"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"@
|
|
25
|
-
"@nocobase/
|
|
26
|
-
"@nocobase/
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"@nocobase/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "plugin-file-preview-auth",
|
|
3
|
+
"displayName": "Authenticated File Previewer",
|
|
4
|
+
"displayName.vi-VN": "Xem trước file có xác thực",
|
|
5
|
+
"displayName.zh-CN": "认证文件预览",
|
|
6
|
+
"description": "Preview PDF, image, and text files with Bearer token authentication via blob URLs.",
|
|
7
|
+
"description.vi-VN": "Xem trước file PDF, hình ảnh và văn bản với xác thực Bearer token qua blob URL.",
|
|
8
|
+
"description.zh-CN": "通过 Bearer 令牌认证和 Blob URL 预览 PDF、图片和文本文件。",
|
|
9
|
+
"version": "1.3.6",
|
|
10
|
+
"main": "dist/server/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"public",
|
|
14
|
+
"client.js",
|
|
15
|
+
"client.d.ts",
|
|
16
|
+
"server.js",
|
|
17
|
+
"server.d.ts",
|
|
18
|
+
"README.md",
|
|
19
|
+
"client-v2.js",
|
|
20
|
+
"client-v2.d.ts",
|
|
21
|
+
"src"
|
|
22
|
+
],
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@aws-sdk/client-s3": "*",
|
|
25
|
+
"@nocobase/client": "2.x",
|
|
26
|
+
"@nocobase/database": "2.x",
|
|
27
|
+
"@nocobase/plugin-ai": "2.x",
|
|
28
|
+
"@nocobase/plugin-file-manager": "2.x",
|
|
29
|
+
"@nocobase/server": "2.x",
|
|
30
|
+
"@nocobase/utils": "2.x",
|
|
31
|
+
"@nocobase/client-v2": "2.x",
|
|
32
|
+
"@nocobase/flow-engine": "2.x"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"@nocobase/plugin-ai": {
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"File manager",
|
|
42
|
+
"File preview",
|
|
43
|
+
"Authentication"
|
|
44
|
+
],
|
|
45
|
+
"license": "Apache-2.0",
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"docx-preview": "^0.3.3",
|
|
48
|
+
"react-pptx-preview-kit": "^0.1.9",
|
|
49
|
+
"xlsx": "^0.18.5"
|
|
50
|
+
},
|
|
51
|
+
"nocobase": {
|
|
52
|
+
"supportedVersions": [
|
|
53
|
+
"2.x"
|
|
54
|
+
],
|
|
55
|
+
"editionLevel": 0
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
11
|
+
import { FileTextOutlined, RobotOutlined } from '@ant-design/icons';
|
|
12
|
+
import { Button, Dropdown, Space, Tooltip, message } from 'antd';
|
|
13
|
+
import type { MenuProps } from 'antd';
|
|
14
|
+
import type { Application } from '@nocobase/client';
|
|
15
|
+
import { useChatBoxActions, useAIConfigRepository, type AIEmployee } from '@nocobase/plugin-ai/client';
|
|
16
|
+
import { useT } from './locale';
|
|
17
|
+
|
|
18
|
+
export const FILE_PREVIEW_WORK_CONTEXT_TYPE = 'file-preview';
|
|
19
|
+
|
|
20
|
+
const AI_EMPLOYEE_STORAGE_KEY = 'plugin-file-preview-auth.aiEmployee';
|
|
21
|
+
|
|
22
|
+
function getFileDisplayName(file: any): string {
|
|
23
|
+
if (!file) return 'file';
|
|
24
|
+
if (file.title && file.extname) return `${file.title}${file.extname}`;
|
|
25
|
+
return file.filename || file.name || file.title || 'file';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getFileContextUid(file: any): string {
|
|
29
|
+
const stableValue = file?.id ?? file?.uid ?? file?.url ?? file?.path ?? getFileDisplayName(file);
|
|
30
|
+
return `file-preview:${String(stableValue)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizePreviewFile(file: any) {
|
|
34
|
+
return {
|
|
35
|
+
id: file?.id,
|
|
36
|
+
uid: file?.uid,
|
|
37
|
+
url: file?.url,
|
|
38
|
+
preview: file?.preview,
|
|
39
|
+
filename: file?.filename || file?.name,
|
|
40
|
+
name: file?.name || file?.filename,
|
|
41
|
+
title: file?.title,
|
|
42
|
+
extname: file?.extname,
|
|
43
|
+
mimetype: file?.mimetype,
|
|
44
|
+
size: file?.size,
|
|
45
|
+
path: file?.path,
|
|
46
|
+
storageId: file?.storageId ?? file?.storage_id ?? file?.storage?.id,
|
|
47
|
+
storage_id: file?.storage_id,
|
|
48
|
+
storageType: file?.storageType || file?.storage?.type,
|
|
49
|
+
storageName: file?.storageName || file?.storage?.name,
|
|
50
|
+
storage: file?.storage,
|
|
51
|
+
collectionName: file?.collectionName,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createFilePreviewWorkContext(file: any) {
|
|
56
|
+
return {
|
|
57
|
+
type: FILE_PREVIEW_WORK_CONTEXT_TYPE,
|
|
58
|
+
uid: getFileContextUid(file),
|
|
59
|
+
title: getFileDisplayName(file),
|
|
60
|
+
content: {
|
|
61
|
+
source: 'plugin-file-preview-auth',
|
|
62
|
+
file: normalizePreviewFile(file),
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getStoredAIEmployeeUsername() {
|
|
68
|
+
try {
|
|
69
|
+
return window.localStorage.getItem(AI_EMPLOYEE_STORAGE_KEY) || '';
|
|
70
|
+
} catch {
|
|
71
|
+
return '';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function setStoredAIEmployeeUsername(username: string) {
|
|
76
|
+
try {
|
|
77
|
+
window.localStorage.setItem(AI_EMPLOYEE_STORAGE_KEY, username);
|
|
78
|
+
} catch {
|
|
79
|
+
// Ignore storage restrictions in embedded/sandboxed clients.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getEmployeeLabel(employee: AIEmployee) {
|
|
84
|
+
return employee?.nickname || employee?.username || '';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class AIFilePreviewActionBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> {
|
|
88
|
+
constructor(props: { children: React.ReactNode }) {
|
|
89
|
+
super(props);
|
|
90
|
+
this.state = { hasError: false };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
static getDerivedStateFromError() {
|
|
94
|
+
return { hasError: true };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
render() {
|
|
98
|
+
if (this.state.hasError) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return this.props.children;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const AIFilePreviewActionInner: React.FC<{ file: any }> = ({ file }) => {
|
|
106
|
+
const t = useT();
|
|
107
|
+
const aiConfigRepository = useAIConfigRepository();
|
|
108
|
+
const { triggerTask } = useChatBoxActions();
|
|
109
|
+
const [employees, setEmployees] = useState<AIEmployee[]>([]);
|
|
110
|
+
const [loading, setLoading] = useState(false);
|
|
111
|
+
const [asking, setAsking] = useState(false);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
let cancelled = false;
|
|
115
|
+
if (!aiConfigRepository?.getAIEmployees) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const cached = aiConfigRepository.aiEmployees || [];
|
|
120
|
+
if (cached.length) {
|
|
121
|
+
setEmployees([...cached]);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
setLoading(true);
|
|
126
|
+
aiConfigRepository
|
|
127
|
+
.getAIEmployees()
|
|
128
|
+
.then((list) => {
|
|
129
|
+
if (!cancelled) {
|
|
130
|
+
setEmployees([...(list || [])]);
|
|
131
|
+
setLoading(false);
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
.catch(() => {
|
|
135
|
+
if (!cancelled) {
|
|
136
|
+
setEmployees([]);
|
|
137
|
+
setLoading(false);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return () => {
|
|
142
|
+
cancelled = true;
|
|
143
|
+
};
|
|
144
|
+
}, [aiConfigRepository]);
|
|
145
|
+
|
|
146
|
+
const orderedEmployees = useMemo(() => {
|
|
147
|
+
const selected = getStoredAIEmployeeUsername();
|
|
148
|
+
if (!selected) {
|
|
149
|
+
return employees;
|
|
150
|
+
}
|
|
151
|
+
return [...employees].sort((a, b) => {
|
|
152
|
+
if (a.username === selected) return -1;
|
|
153
|
+
if (b.username === selected) return 1;
|
|
154
|
+
return 0;
|
|
155
|
+
});
|
|
156
|
+
}, [employees]);
|
|
157
|
+
|
|
158
|
+
const openAIChat = useCallback(
|
|
159
|
+
async (employee: AIEmployee) => {
|
|
160
|
+
if (!employee || !file) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
setAsking(true);
|
|
165
|
+
try {
|
|
166
|
+
setStoredAIEmployeeUsername(employee.username);
|
|
167
|
+
await triggerTask({
|
|
168
|
+
aiEmployee: employee,
|
|
169
|
+
tasks: [
|
|
170
|
+
{
|
|
171
|
+
title: getFileDisplayName(file),
|
|
172
|
+
message: {
|
|
173
|
+
user: t('Please help me analyze the file currently open in preview.'),
|
|
174
|
+
workContext: [createFilePreviewWorkContext(file)],
|
|
175
|
+
},
|
|
176
|
+
autoSend: false,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
});
|
|
180
|
+
} catch {
|
|
181
|
+
message.error(t('Failed to open AI chat'));
|
|
182
|
+
} finally {
|
|
183
|
+
setAsking(false);
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
[file, t, triggerTask],
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const menuItems: MenuProps['items'] = orderedEmployees.map((employee) => ({
|
|
190
|
+
key: employee.username,
|
|
191
|
+
label: getEmployeeLabel(employee),
|
|
192
|
+
onClick: () => openAIChat(employee),
|
|
193
|
+
}));
|
|
194
|
+
|
|
195
|
+
if (!loading && !orderedEmployees.length) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (orderedEmployees.length === 1) {
|
|
200
|
+
return (
|
|
201
|
+
<Tooltip title={t('Ask AI')}>
|
|
202
|
+
<Button
|
|
203
|
+
type="text"
|
|
204
|
+
size="small"
|
|
205
|
+
icon={<RobotOutlined />}
|
|
206
|
+
loading={asking || loading}
|
|
207
|
+
onClick={(event) => {
|
|
208
|
+
event.stopPropagation();
|
|
209
|
+
openAIChat(orderedEmployees[0]);
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
{t('Ask AI')}
|
|
213
|
+
</Button>
|
|
214
|
+
</Tooltip>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<Tooltip title={t('Ask AI')}>
|
|
220
|
+
<Dropdown menu={{ items: menuItems }} trigger={['click']} placement="bottomRight" disabled={asking || loading}>
|
|
221
|
+
<Button
|
|
222
|
+
type="text"
|
|
223
|
+
size="small"
|
|
224
|
+
icon={<RobotOutlined />}
|
|
225
|
+
loading={asking || loading}
|
|
226
|
+
onClick={(event) => event.stopPropagation()}
|
|
227
|
+
>
|
|
228
|
+
{t('Ask AI')}
|
|
229
|
+
</Button>
|
|
230
|
+
</Dropdown>
|
|
231
|
+
</Tooltip>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export const AIFilePreviewAction: React.FC<{ file: any }> = ({ file }) => {
|
|
236
|
+
return (
|
|
237
|
+
<AIFilePreviewActionBoundary>
|
|
238
|
+
<AIFilePreviewActionInner file={file} />
|
|
239
|
+
</AIFilePreviewActionBoundary>
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export function registerFilePreviewAIWorkContext(app: Application) {
|
|
244
|
+
let aiPlugin: any;
|
|
245
|
+
try {
|
|
246
|
+
aiPlugin = app.pm.get('ai') as any;
|
|
247
|
+
} catch {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const aiManager = aiPlugin?.aiManager;
|
|
251
|
+
if (!aiManager?.registerWorkContext) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const options = {
|
|
256
|
+
name: FILE_PREVIEW_WORK_CONTEXT_TYPE,
|
|
257
|
+
tag: {
|
|
258
|
+
Component: ({ item }: { item: any }) => (
|
|
259
|
+
<Space>
|
|
260
|
+
<FileTextOutlined />
|
|
261
|
+
<span>{item?.title || ''}</span>
|
|
262
|
+
</Space>
|
|
263
|
+
),
|
|
264
|
+
},
|
|
265
|
+
chatbox: {
|
|
266
|
+
Component: ({ item }: { item: any }) => (
|
|
267
|
+
<Space>
|
|
268
|
+
<FileTextOutlined />
|
|
269
|
+
<span>{item?.title || ''}</span>
|
|
270
|
+
</Space>
|
|
271
|
+
),
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
if (!aiManager.getWorkContext?.(FILE_PREVIEW_WORK_CONTEXT_TYPE)) {
|
|
277
|
+
aiManager.registerWorkContext(FILE_PREVIEW_WORK_CONTEXT_TYPE, options);
|
|
278
|
+
}
|
|
279
|
+
} catch {
|
|
280
|
+
// Duplicate registration can happen during hot reload. It is harmless.
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { extractOcrStatusRecord, getOcrAttachmentId, isOcrCompleteStatus, normalizeOcrAttachmentId } from '../index';
|
|
2
|
+
|
|
3
|
+
describe('file preview OCR client utils', () => {
|
|
4
|
+
describe('normalizeOcrAttachmentId', () => {
|
|
5
|
+
it('accepts numeric attachment ids', () => {
|
|
6
|
+
expect(normalizeOcrAttachmentId(12)).toBe(12);
|
|
7
|
+
expect(normalizeOcrAttachmentId('12')).toBe('12');
|
|
8
|
+
expect(normalizeOcrAttachmentId(' 12 ')).toBe('12');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('rejects URLs and upload-only ids', () => {
|
|
12
|
+
expect(normalizeOcrAttachmentId('http://localhost/storage/uploads/test.pdf')).toBeNull();
|
|
13
|
+
expect(normalizeOcrAttachmentId('/storage/uploads/test.pdf')).toBeNull();
|
|
14
|
+
expect(normalizeOcrAttachmentId('rc-upload-1710000000000-1')).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('getOcrAttachmentId', () => {
|
|
19
|
+
it('uses response.id when file.id is a storage URL', () => {
|
|
20
|
+
expect(
|
|
21
|
+
getOcrAttachmentId({
|
|
22
|
+
id: 'http://localhost/storage/uploads/yyyy-test%20(1)-lcqadd.pdf',
|
|
23
|
+
uid: 'rc-upload-1',
|
|
24
|
+
response: {
|
|
25
|
+
id: 42,
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
).toBe(42);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('falls back to a numeric uid for persisted attachment records', () => {
|
|
32
|
+
expect(
|
|
33
|
+
getOcrAttachmentId({
|
|
34
|
+
id: '/storage/uploads/yyyy-test.pdf',
|
|
35
|
+
uid: '88',
|
|
36
|
+
}),
|
|
37
|
+
).toBe('88');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('isOcrCompleteStatus', () => {
|
|
42
|
+
it('only treats result statuses as completed', () => {
|
|
43
|
+
expect(isOcrCompleteStatus('waiting-verify')).toBe(true);
|
|
44
|
+
expect(isOcrCompleteStatus('success')).toBe(true);
|
|
45
|
+
expect(isOcrCompleteStatus('no-ocr')).toBe(false);
|
|
46
|
+
expect(isOcrCompleteStatus('pending-ocr')).toBe(false);
|
|
47
|
+
expect(isOcrCompleteStatus('failed')).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('extractOcrStatusRecord', () => {
|
|
52
|
+
it('unwraps NocoBase double data responses', () => {
|
|
53
|
+
const record = extractOcrStatusRecord({
|
|
54
|
+
data: {
|
|
55
|
+
data: {
|
|
56
|
+
id: 4,
|
|
57
|
+
attachmentId: 57,
|
|
58
|
+
status: 'waiting-verify',
|
|
59
|
+
data: { pages: [] },
|
|
60
|
+
error: null,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(record?.id).toBe(4);
|
|
66
|
+
expect(record?.attachmentId).toBe(57);
|
|
67
|
+
expect(record?.status).toBe('waiting-verify');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('unwraps action responses with ok and data', () => {
|
|
71
|
+
const record = extractOcrStatusRecord({
|
|
72
|
+
data: {
|
|
73
|
+
ok: true,
|
|
74
|
+
data: {
|
|
75
|
+
id: 5,
|
|
76
|
+
status: 'pending-ocr',
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(record?.id).toBe(5);
|
|
82
|
+
expect(record?.status).toBe('pending-ocr');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|