plugin-document-parser 1.0.1
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.d.ts +2 -0
- package/client.js +1 -0
- package/dist/client/01b8a5798a872638.js +10 -0
- package/dist/client/022be20abc96fdb4.js +10 -0
- package/dist/client/12e97e7a84d900e0.js +10 -0
- package/dist/client/index.js +10 -0
- package/dist/externalVersion.js +20 -0
- package/dist/index.js +48 -0
- package/dist/locale/en-US.json +54 -0
- package/dist/locale/vi-VN.json +54 -0
- package/dist/node_modules/form-data/License +19 -0
- package/dist/node_modules/form-data/index.d.ts +62 -0
- package/dist/node_modules/form-data/lib/browser.js +4 -0
- package/dist/node_modules/form-data/lib/form_data.js +14 -0
- package/dist/node_modules/form-data/lib/populate.js +10 -0
- package/dist/node_modules/form-data/package.json +1 -0
- package/dist/server/collections/doc-parser-providers.js +137 -0
- package/dist/server/collections/doc-parser-settings.js +85 -0
- package/dist/server/index.js +51 -0
- package/dist/server/plugin.js +181 -0
- package/dist/server/resource/docParserProviders.js +91 -0
- package/dist/server/services/builtin-ai-handler.js +63 -0
- package/dist/server/services/external-ocr-client.js +189 -0
- package/dist/server/services/internal-parser-registry.js +82 -0
- package/dist/server/services/parse-router.js +273 -0
- package/package.json +33 -0
- package/server.d.ts +2 -0
- package/server.js +1 -0
- package/src/client/components/GlobalSettings.tsx +151 -0
- package/src/client/components/ProviderForm.tsx +266 -0
- package/src/client/components/ProviderList.tsx +193 -0
- package/src/client/components/SettingsPage.tsx +43 -0
- package/src/client/index.tsx +2 -0
- package/src/client/locale.ts +12 -0
- package/src/client/plugin.tsx +34 -0
- package/src/index.ts +2 -0
- package/src/locale/en-US.json +54 -0
- package/src/locale/vi-VN.json +54 -0
- package/src/server/collections/doc-parser-providers.ts +107 -0
- package/src/server/collections/doc-parser-settings.ts +59 -0
- package/src/server/index.ts +10 -0
- package/src/server/plugin.ts +172 -0
- package/src/server/resource/docParserProviders.ts +72 -0
- package/src/server/services/builtin-ai-handler.ts +49 -0
- package/src/server/services/external-ocr-client.ts +233 -0
- package/src/server/services/internal-parser-registry.ts +126 -0
- package/src/server/services/parse-router.ts +357 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Card, Form, Radio, Switch, Select, Button, message, Spin, Divider, Alert, Space, Typography } from 'antd';
|
|
3
|
+
import { SaveOutlined } from '@ant-design/icons';
|
|
4
|
+
import { useAPIClient } from '@nocobase/client';
|
|
5
|
+
import { useDocParserTranslation } from '../locale';
|
|
6
|
+
|
|
7
|
+
const { Text } = Typography;
|
|
8
|
+
|
|
9
|
+
type Settings = {
|
|
10
|
+
id?: number;
|
|
11
|
+
mode: 'default' | 'internal' | 'external';
|
|
12
|
+
activeProviderId?: number | null;
|
|
13
|
+
fallbackToDefault: boolean;
|
|
14
|
+
imagePassThrough: boolean;
|
|
15
|
+
includedExtnames: string[];
|
|
16
|
+
useDocpixie: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type Provider = {
|
|
20
|
+
id: number;
|
|
21
|
+
title: string;
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const GlobalSettings: React.FC = () => {
|
|
26
|
+
const { t } = useDocParserTranslation();
|
|
27
|
+
const api = useAPIClient();
|
|
28
|
+
const [form] = Form.useForm<Settings>();
|
|
29
|
+
const [loading, setLoading] = useState(true);
|
|
30
|
+
const [saving, setSaving] = useState(false);
|
|
31
|
+
const [providers, setProviders] = useState<Provider[]>([]);
|
|
32
|
+
const [mode, setMode] = useState<'default' | 'internal' | 'external'>('default');
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
Promise.all([
|
|
36
|
+
api.request({ url: 'docParserSettings:get' }),
|
|
37
|
+
api.request({ url: 'docParserProviders:list', params: { pageSize: 200 } }),
|
|
38
|
+
]).then(([settingsRes, providersRes]) => {
|
|
39
|
+
const settings: Settings = settingsRes?.data?.data ?? {
|
|
40
|
+
mode: 'default',
|
|
41
|
+
fallbackToDefault: true,
|
|
42
|
+
imagePassThrough: true,
|
|
43
|
+
includedExtnames: [],
|
|
44
|
+
};
|
|
45
|
+
form.setFieldsValue(settings);
|
|
46
|
+
setMode(settings.mode);
|
|
47
|
+
|
|
48
|
+
const list: Provider[] = providersRes?.data?.data ?? [];
|
|
49
|
+
setProviders(list);
|
|
50
|
+
}).finally(() => setLoading(false));
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const handleSave = async () => {
|
|
54
|
+
const values = await form.validateFields();
|
|
55
|
+
setSaving(true);
|
|
56
|
+
try {
|
|
57
|
+
await api.request({ url: 'docParserSettings:save', method: 'POST', data: values });
|
|
58
|
+
message.success(t('Settings saved'));
|
|
59
|
+
} catch (err: any) {
|
|
60
|
+
message.error(err?.message ?? 'Save failed');
|
|
61
|
+
} finally {
|
|
62
|
+
setSaving(false);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (loading) return <Spin style={{ display: 'block', margin: '40px auto' }} />;
|
|
67
|
+
|
|
68
|
+
const modeDescriptions: Record<string, string> = {
|
|
69
|
+
default: t('mode_default_desc'),
|
|
70
|
+
internal: t('mode_internal_desc'),
|
|
71
|
+
external: t('mode_external_desc'),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Card bordered={false}>
|
|
76
|
+
<Form form={form} layout="vertical" onValuesChange={(changed) => {
|
|
77
|
+
if (changed.mode) setMode(changed.mode);
|
|
78
|
+
}}>
|
|
79
|
+
|
|
80
|
+
<Form.Item name="mode" label={t('Processing Mode')}>
|
|
81
|
+
<Radio.Group style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
82
|
+
<Radio value="default">
|
|
83
|
+
<Space direction="vertical" size={0}>
|
|
84
|
+
<Text strong>{t('Default (plugin-ai built-in)')}</Text>
|
|
85
|
+
<Text type="secondary" style={{ fontSize: 12 }}>{t('mode_default_desc')}</Text>
|
|
86
|
+
</Space>
|
|
87
|
+
</Radio>
|
|
88
|
+
<Radio value="internal">
|
|
89
|
+
<Space direction="vertical" size={0}>
|
|
90
|
+
<Text strong>{t('Internal (built-in document loaders)')}</Text>
|
|
91
|
+
<Text type="secondary" style={{ fontSize: 12 }}>{t('mode_internal_desc')}</Text>
|
|
92
|
+
</Space>
|
|
93
|
+
</Radio>
|
|
94
|
+
<Radio value="external">
|
|
95
|
+
<Space direction="vertical" size={0}>
|
|
96
|
+
<Text strong>{t('External (OCR API provider)')}</Text>
|
|
97
|
+
<Text type="secondary" style={{ fontSize: 12 }}>{t('mode_external_desc')}</Text>
|
|
98
|
+
</Space>
|
|
99
|
+
</Radio>
|
|
100
|
+
</Radio.Group>
|
|
101
|
+
</Form.Item>
|
|
102
|
+
|
|
103
|
+
{mode === 'external' && (
|
|
104
|
+
<Form.Item
|
|
105
|
+
name="activeProviderId"
|
|
106
|
+
label={t('Active Provider')}
|
|
107
|
+
rules={[{ required: true, message: t('Please select a provider') }]}
|
|
108
|
+
>
|
|
109
|
+
<Select
|
|
110
|
+
placeholder={t('Please select a provider')}
|
|
111
|
+
options={providers.filter((p) => p.enabled).map((p) => ({
|
|
112
|
+
label: p.title,
|
|
113
|
+
value: p.id,
|
|
114
|
+
}))}
|
|
115
|
+
style={{ maxWidth: 400 }}
|
|
116
|
+
/>
|
|
117
|
+
</Form.Item>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
<Divider />
|
|
121
|
+
|
|
122
|
+
<Form.Item name="fallbackToDefault" valuePropName="checked" label={t('Fallback to default on error')}>
|
|
123
|
+
<Switch />
|
|
124
|
+
</Form.Item>
|
|
125
|
+
|
|
126
|
+
<Form.Item name="imagePassThrough" valuePropName="checked" label={t('Pass images through to default')}>
|
|
127
|
+
<Switch />
|
|
128
|
+
</Form.Item>
|
|
129
|
+
|
|
130
|
+
<Divider />
|
|
131
|
+
|
|
132
|
+
<Form.Item
|
|
133
|
+
name="useDocpixie"
|
|
134
|
+
valuePropName="checked"
|
|
135
|
+
label={t('Index with DocPixie (when available)')}
|
|
136
|
+
help={<Text type="secondary">{t('docpixie_mode_desc')}</Text>}
|
|
137
|
+
>
|
|
138
|
+
<Switch />
|
|
139
|
+
</Form.Item>
|
|
140
|
+
|
|
141
|
+
<Divider />
|
|
142
|
+
|
|
143
|
+
<Form.Item>
|
|
144
|
+
<Button type="primary" icon={<SaveOutlined />} onClick={handleSave} loading={saving}>
|
|
145
|
+
{t('Settings saved').replace('saved', 'Save')}
|
|
146
|
+
</Button>
|
|
147
|
+
</Form.Item>
|
|
148
|
+
</Form>
|
|
149
|
+
</Card>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Form, Input, InputNumber, Select, Switch, Button, Space, Divider,
|
|
4
|
+
Typography, Collapse, Row, Col,
|
|
5
|
+
} from 'antd';
|
|
6
|
+
import { useDocParserTranslation } from '../locale';
|
|
7
|
+
|
|
8
|
+
const { Text } = Typography;
|
|
9
|
+
const { Panel } = Collapse;
|
|
10
|
+
|
|
11
|
+
export type ProviderFormValues = {
|
|
12
|
+
title: string;
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
apiEndpoint: string;
|
|
15
|
+
authType: 'bearer' | 'api-key-header' | 'basic' | 'custom-headers' | 'none';
|
|
16
|
+
apiKey?: string;
|
|
17
|
+
authConfig?: {
|
|
18
|
+
headerName?: string;
|
|
19
|
+
username?: string;
|
|
20
|
+
password?: string;
|
|
21
|
+
customHeaders?: string; // JSON string in UI, parsed on save
|
|
22
|
+
};
|
|
23
|
+
requestFormat: 'multipart' | 'json-base64' | 'url';
|
|
24
|
+
requestConfig?: {
|
|
25
|
+
fileFieldName?: string;
|
|
26
|
+
filenameFieldName?: string;
|
|
27
|
+
mimetypeFieldName?: string;
|
|
28
|
+
extraFields?: string; // JSON string
|
|
29
|
+
base64FieldPath?: string;
|
|
30
|
+
filenameFieldPath?: string;
|
|
31
|
+
mimetypeFieldPath?: string;
|
|
32
|
+
extraBody?: string; // JSON string
|
|
33
|
+
urlFieldPath?: string;
|
|
34
|
+
};
|
|
35
|
+
responseTextPath?: string;
|
|
36
|
+
timeout?: number;
|
|
37
|
+
supportedMimetypes?: string[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type Props = {
|
|
41
|
+
initialValues?: Partial<ProviderFormValues>;
|
|
42
|
+
onSubmit: (values: ProviderFormValues) => Promise<void>;
|
|
43
|
+
onCancel: () => void;
|
|
44
|
+
submitting?: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const ProviderForm: React.FC<Props> = ({ initialValues, onSubmit, onCancel, submitting }) => {
|
|
48
|
+
const { t } = useDocParserTranslation();
|
|
49
|
+
const [form] = Form.useForm<ProviderFormValues>();
|
|
50
|
+
const authType = Form.useWatch('authType', form);
|
|
51
|
+
const requestFormat = Form.useWatch('requestFormat', form);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (initialValues) {
|
|
55
|
+
form.setFieldsValue(initialValues);
|
|
56
|
+
}
|
|
57
|
+
}, [initialValues]);
|
|
58
|
+
|
|
59
|
+
const handleSubmit = async () => {
|
|
60
|
+
const values = await form.validateFields();
|
|
61
|
+
await onSubmit(values);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Form form={form} layout="vertical" initialValues={{
|
|
66
|
+
enabled: true,
|
|
67
|
+
authType: 'bearer',
|
|
68
|
+
requestFormat: 'multipart',
|
|
69
|
+
responseTextPath: 'text',
|
|
70
|
+
timeout: 60000,
|
|
71
|
+
requestConfig: { fileFieldName: 'file' },
|
|
72
|
+
...initialValues,
|
|
73
|
+
}}>
|
|
74
|
+
|
|
75
|
+
{/* ── Basic ──────────────────────────────────────────── */}
|
|
76
|
+
<Row gutter={16}>
|
|
77
|
+
<Col span={18}>
|
|
78
|
+
<Form.Item name="title" label={t('Provider Title')} rules={[{ required: true }]}>
|
|
79
|
+
<Input />
|
|
80
|
+
</Form.Item>
|
|
81
|
+
</Col>
|
|
82
|
+
<Col span={6}>
|
|
83
|
+
<Form.Item name="enabled" label={t('Enabled')} valuePropName="checked">
|
|
84
|
+
<Switch />
|
|
85
|
+
</Form.Item>
|
|
86
|
+
</Col>
|
|
87
|
+
</Row>
|
|
88
|
+
|
|
89
|
+
<Form.Item name="apiEndpoint" label={t('API Endpoint')} rules={[{ required: true }]}>
|
|
90
|
+
<Input placeholder="https://api.ocr-provider.com/v1/parse" />
|
|
91
|
+
</Form.Item>
|
|
92
|
+
|
|
93
|
+
{/* ── Auth ───────────────────────────────────────────── */}
|
|
94
|
+
<Divider orientation="left" plain>{t('Auth Type')}</Divider>
|
|
95
|
+
|
|
96
|
+
<Form.Item name="authType" label={t('Auth Type')}>
|
|
97
|
+
<Select options={[
|
|
98
|
+
{ label: 'None', value: 'none' },
|
|
99
|
+
{ label: 'Bearer Token', value: 'bearer' },
|
|
100
|
+
{ label: 'API Key Header', value: 'api-key-header' },
|
|
101
|
+
{ label: 'Basic Auth', value: 'basic' },
|
|
102
|
+
{ label: 'Custom Headers', value: 'custom-headers' },
|
|
103
|
+
]} style={{ maxWidth: 260 }} />
|
|
104
|
+
</Form.Item>
|
|
105
|
+
|
|
106
|
+
{(authType === 'bearer' || authType === 'api-key-header') && (
|
|
107
|
+
<Form.Item name="apiKey" label={t('API Key')}>
|
|
108
|
+
<Input.Password />
|
|
109
|
+
</Form.Item>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{authType === 'api-key-header' && (
|
|
113
|
+
<Form.Item name={['authConfig', 'headerName']} label={t('Header Name')}>
|
|
114
|
+
<Input placeholder="X-Api-Key" style={{ maxWidth: 300 }} />
|
|
115
|
+
</Form.Item>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{authType === 'basic' && (
|
|
119
|
+
<Row gutter={16}>
|
|
120
|
+
<Col span={12}>
|
|
121
|
+
<Form.Item name={['authConfig', 'username']} label={t('Username')}>
|
|
122
|
+
<Input />
|
|
123
|
+
</Form.Item>
|
|
124
|
+
</Col>
|
|
125
|
+
<Col span={12}>
|
|
126
|
+
<Form.Item name={['authConfig', 'password']} label={t('Password')}>
|
|
127
|
+
<Input.Password />
|
|
128
|
+
</Form.Item>
|
|
129
|
+
</Col>
|
|
130
|
+
</Row>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
{authType === 'custom-headers' && (
|
|
134
|
+
<Form.Item
|
|
135
|
+
name={['authConfig', 'customHeaders']}
|
|
136
|
+
label={t('Custom Headers')}
|
|
137
|
+
help={<Text type="secondary">JSON: {'{"X-Tenant": "abc", "X-Secret": "xxx"}'}</Text>}
|
|
138
|
+
>
|
|
139
|
+
<Input.TextArea rows={3} placeholder='{"X-Tenant": "abc"}' />
|
|
140
|
+
</Form.Item>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
{/* ── Request format ─────────────────────────────────── */}
|
|
144
|
+
<Divider orientation="left" plain>{t('Request Format')}</Divider>
|
|
145
|
+
|
|
146
|
+
<Form.Item name="requestFormat" label={t('Request Format')}>
|
|
147
|
+
<Select options={[
|
|
148
|
+
{ label: t('Multipart Form Data'), value: 'multipart' },
|
|
149
|
+
{ label: t('JSON Base64'), value: 'json-base64' },
|
|
150
|
+
{ label: 'URL (provider fetches)', value: 'url' },
|
|
151
|
+
]} style={{ maxWidth: 260 }} />
|
|
152
|
+
</Form.Item>
|
|
153
|
+
|
|
154
|
+
{requestFormat === 'multipart' && (
|
|
155
|
+
<Row gutter={16}>
|
|
156
|
+
<Col span={8}>
|
|
157
|
+
<Form.Item name={['requestConfig', 'fileFieldName']} label={t('Form Field Name')}>
|
|
158
|
+
<Input placeholder="file" />
|
|
159
|
+
</Form.Item>
|
|
160
|
+
</Col>
|
|
161
|
+
<Col span={8}>
|
|
162
|
+
<Form.Item name={['requestConfig', 'filenameFieldName']} label={t('Filename Field Path')}>
|
|
163
|
+
<Input placeholder="(optional)" />
|
|
164
|
+
</Form.Item>
|
|
165
|
+
</Col>
|
|
166
|
+
<Col span={8}>
|
|
167
|
+
<Form.Item name={['requestConfig', 'mimetypeFieldName']} label={t('Mimetype Field Path')}>
|
|
168
|
+
<Input placeholder="(optional)" />
|
|
169
|
+
</Form.Item>
|
|
170
|
+
</Col>
|
|
171
|
+
</Row>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{requestFormat === 'json-base64' && (
|
|
175
|
+
<Row gutter={16}>
|
|
176
|
+
<Col span={8}>
|
|
177
|
+
<Form.Item name={['requestConfig', 'base64FieldPath']} label={t('Base64 Field Path')}>
|
|
178
|
+
<Input placeholder="file" />
|
|
179
|
+
</Form.Item>
|
|
180
|
+
</Col>
|
|
181
|
+
<Col span={8}>
|
|
182
|
+
<Form.Item name={['requestConfig', 'filenameFieldPath']} label={t('Filename Field Path')}>
|
|
183
|
+
<Input placeholder="filename" />
|
|
184
|
+
</Form.Item>
|
|
185
|
+
</Col>
|
|
186
|
+
<Col span={8}>
|
|
187
|
+
<Form.Item name={['requestConfig', 'mimetypeFieldPath']} label={t('Mimetype Field Path')}>
|
|
188
|
+
<Input placeholder="mimetype" />
|
|
189
|
+
</Form.Item>
|
|
190
|
+
</Col>
|
|
191
|
+
</Row>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{requestFormat === 'url' && (
|
|
195
|
+
<Form.Item name={['requestConfig', 'urlFieldPath']} label={t('Base64 Field Path').replace('Base64', 'URL')}>
|
|
196
|
+
<Input placeholder="url" style={{ maxWidth: 300 }} />
|
|
197
|
+
</Form.Item>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
<Collapse ghost>
|
|
201
|
+
<Panel header={t('Extra Request Body')} key="extra">
|
|
202
|
+
<Form.Item
|
|
203
|
+
name={['requestConfig', requestFormat === 'multipart' ? 'extraFields' : 'extraBody']}
|
|
204
|
+
help={<Text type="secondary">JSON object — merged into request body/fields</Text>}
|
|
205
|
+
>
|
|
206
|
+
<Input.TextArea rows={4} placeholder='{"lang": "vie+eng"}' />
|
|
207
|
+
</Form.Item>
|
|
208
|
+
</Panel>
|
|
209
|
+
</Collapse>
|
|
210
|
+
|
|
211
|
+
{/* ── Response ───────────────────────────────────────── */}
|
|
212
|
+
<Divider orientation="left" plain>Response</Divider>
|
|
213
|
+
|
|
214
|
+
<Row gutter={16}>
|
|
215
|
+
<Col span={16}>
|
|
216
|
+
<Form.Item
|
|
217
|
+
name="responseTextPath"
|
|
218
|
+
label={t('Response Text Path')}
|
|
219
|
+
help={<Text type="secondary">Dot-path, e.g. "data.text" or "result.pages.0.content"</Text>}
|
|
220
|
+
>
|
|
221
|
+
<Input placeholder="text" />
|
|
222
|
+
</Form.Item>
|
|
223
|
+
</Col>
|
|
224
|
+
<Col span={8}>
|
|
225
|
+
<Form.Item name="timeout" label={t('Timeout (ms)')}>
|
|
226
|
+
<InputNumber min={1000} step={5000} style={{ width: '100%' }} />
|
|
227
|
+
</Form.Item>
|
|
228
|
+
</Col>
|
|
229
|
+
</Row>
|
|
230
|
+
|
|
231
|
+
{/* ── Scope ──────────────────────────────────────────── */}
|
|
232
|
+
<Divider orientation="left" plain>Scope</Divider>
|
|
233
|
+
|
|
234
|
+
<Form.Item
|
|
235
|
+
name="supportedMimetypes"
|
|
236
|
+
label={t('Supported MIME Types')}
|
|
237
|
+
help={t('Leave empty to handle all non-image types')}
|
|
238
|
+
>
|
|
239
|
+
<Select
|
|
240
|
+
mode="tags"
|
|
241
|
+
tokenSeparators={[',']}
|
|
242
|
+
placeholder="application/pdf, application/vnd.openxmlformats..."
|
|
243
|
+
style={{ width: '100%' }}
|
|
244
|
+
options={[
|
|
245
|
+
{ label: 'PDF', value: 'application/pdf' },
|
|
246
|
+
{ label: 'DOCX', value: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' },
|
|
247
|
+
{ label: 'XLSX', value: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
|
|
248
|
+
{ label: 'PPTX', value: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' },
|
|
249
|
+
{ label: 'Plain text', value: 'text/plain' },
|
|
250
|
+
{ label: 'HTML', value: 'text/html' },
|
|
251
|
+
]}
|
|
252
|
+
/>
|
|
253
|
+
</Form.Item>
|
|
254
|
+
|
|
255
|
+
{/* ── Actions ────────────────────────────────────────── */}
|
|
256
|
+
<Form.Item style={{ marginBottom: 0, marginTop: 24 }}>
|
|
257
|
+
<Space>
|
|
258
|
+
<Button type="primary" onClick={handleSubmit} loading={submitting}>
|
|
259
|
+
Save
|
|
260
|
+
</Button>
|
|
261
|
+
<Button onClick={onCancel}>Cancel</Button>
|
|
262
|
+
</Space>
|
|
263
|
+
</Form.Item>
|
|
264
|
+
</Form>
|
|
265
|
+
);
|
|
266
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Button, Table, Tag, Space, Popconfirm, Modal, message, Tooltip, Badge,
|
|
4
|
+
} from 'antd';
|
|
5
|
+
import { PlusOutlined, EditOutlined, DeleteOutlined, ApiOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
|
6
|
+
import { useAPIClient } from '@nocobase/client';
|
|
7
|
+
import { ProviderForm, ProviderFormValues } from './ProviderForm';
|
|
8
|
+
import { useDocParserTranslation } from '../locale';
|
|
9
|
+
|
|
10
|
+
type Provider = ProviderFormValues & { id: number };
|
|
11
|
+
|
|
12
|
+
export const ProviderList: React.FC = () => {
|
|
13
|
+
const { t } = useDocParserTranslation();
|
|
14
|
+
const api = useAPIClient();
|
|
15
|
+
const [providers, setProviders] = useState<Provider[]>([]);
|
|
16
|
+
const [loading, setLoading] = useState(true);
|
|
17
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
18
|
+
const [editing, setEditing] = useState<Provider | null>(null);
|
|
19
|
+
const [submitting, setSubmitting] = useState(false);
|
|
20
|
+
const [testingId, setTestingId] = useState<number | null>(null);
|
|
21
|
+
|
|
22
|
+
const load = async () => {
|
|
23
|
+
setLoading(true);
|
|
24
|
+
try {
|
|
25
|
+
const res = await api.request({ url: 'docParserProviders:list', params: { pageSize: 200 } });
|
|
26
|
+
setProviders(res?.data?.data ?? []);
|
|
27
|
+
} finally {
|
|
28
|
+
setLoading(false);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
useEffect(() => { load(); }, []);
|
|
33
|
+
|
|
34
|
+
const openCreate = () => {
|
|
35
|
+
setEditing(null);
|
|
36
|
+
setModalOpen(true);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const openEdit = (record: Provider) => {
|
|
40
|
+
setEditing(record);
|
|
41
|
+
setModalOpen(true);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleDelete = async (id: number) => {
|
|
45
|
+
await api.request({ url: `docParserProviders:destroy`, method: 'DELETE', params: { filterByTk: id } });
|
|
46
|
+
message.success(t('Provider deleted'));
|
|
47
|
+
load();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleSubmit = async (values: ProviderFormValues) => {
|
|
51
|
+
setSubmitting(true);
|
|
52
|
+
try {
|
|
53
|
+
if (editing) {
|
|
54
|
+
await api.request({
|
|
55
|
+
url: 'docParserProviders:update',
|
|
56
|
+
method: 'POST',
|
|
57
|
+
params: { filterByTk: editing.id },
|
|
58
|
+
data: values,
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
await api.request({ url: 'docParserProviders:create', method: 'POST', data: values });
|
|
62
|
+
}
|
|
63
|
+
message.success(t('Provider saved'));
|
|
64
|
+
setModalOpen(false);
|
|
65
|
+
load();
|
|
66
|
+
} finally {
|
|
67
|
+
setSubmitting(false);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleTest = async (record: Provider) => {
|
|
72
|
+
setTestingId(record.id);
|
|
73
|
+
try {
|
|
74
|
+
const res = await api.request({
|
|
75
|
+
url: 'docParserProviders:testConnection',
|
|
76
|
+
params: { filterByTk: record.id },
|
|
77
|
+
});
|
|
78
|
+
const result = res?.data?.data;
|
|
79
|
+
if (result?.ok) {
|
|
80
|
+
message.success(`${t('Connection successful')}${result.status ? ` (HTTP ${result.status})` : ''}`);
|
|
81
|
+
} else {
|
|
82
|
+
message.error(`${t('Connection failed')}: ${result?.message ?? 'Unknown error'}`);
|
|
83
|
+
}
|
|
84
|
+
} catch (err: any) {
|
|
85
|
+
message.error(`${t('Connection failed')}: ${err?.message}`);
|
|
86
|
+
} finally {
|
|
87
|
+
setTestingId(null);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const columns = [
|
|
92
|
+
{
|
|
93
|
+
title: t('Provider Title'),
|
|
94
|
+
dataIndex: 'title',
|
|
95
|
+
key: 'title',
|
|
96
|
+
render: (title: string, record: Provider) => (
|
|
97
|
+
<Space>
|
|
98
|
+
<Badge status={record.enabled ? 'success' : 'default'} />
|
|
99
|
+
{title}
|
|
100
|
+
</Space>
|
|
101
|
+
),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
title: t('API Endpoint'),
|
|
105
|
+
dataIndex: 'apiEndpoint',
|
|
106
|
+
key: 'apiEndpoint',
|
|
107
|
+
ellipsis: true,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
title: t('Auth Type'),
|
|
111
|
+
dataIndex: 'authType',
|
|
112
|
+
key: 'authType',
|
|
113
|
+
render: (v: string) => <Tag>{v}</Tag>,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
title: t('Request Format'),
|
|
117
|
+
dataIndex: 'requestFormat',
|
|
118
|
+
key: 'requestFormat',
|
|
119
|
+
render: (v: string) => <Tag color="blue">{v}</Tag>,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
title: t('Enabled'),
|
|
123
|
+
dataIndex: 'enabled',
|
|
124
|
+
key: 'enabled',
|
|
125
|
+
render: (v: boolean) =>
|
|
126
|
+
v ? <CheckCircleOutlined style={{ color: '#52c41a' }} /> : <CloseCircleOutlined style={{ color: '#ccc' }} />,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
title: 'Actions',
|
|
130
|
+
key: 'actions',
|
|
131
|
+
render: (_: any, record: Provider) => (
|
|
132
|
+
<Space>
|
|
133
|
+
<Tooltip title={t('Test Connection')}>
|
|
134
|
+
<Button
|
|
135
|
+
size="small"
|
|
136
|
+
icon={<ApiOutlined />}
|
|
137
|
+
loading={testingId === record.id}
|
|
138
|
+
onClick={() => handleTest(record)}
|
|
139
|
+
/>
|
|
140
|
+
</Tooltip>
|
|
141
|
+
<Tooltip title={t('Edit Provider')}>
|
|
142
|
+
<Button size="small" icon={<EditOutlined />} onClick={() => openEdit(record)} />
|
|
143
|
+
</Tooltip>
|
|
144
|
+
<Popconfirm
|
|
145
|
+
title={t('Delete Provider')}
|
|
146
|
+
description="Are you sure you want to delete this provider?"
|
|
147
|
+
onConfirm={() => handleDelete(record.id)}
|
|
148
|
+
>
|
|
149
|
+
<Tooltip title={t('Delete Provider')}>
|
|
150
|
+
<Button size="small" danger icon={<DeleteOutlined />} />
|
|
151
|
+
</Tooltip>
|
|
152
|
+
</Popconfirm>
|
|
153
|
+
</Space>
|
|
154
|
+
),
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<>
|
|
160
|
+
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'flex-end' }}>
|
|
161
|
+
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
|
162
|
+
{t('Add Provider')}
|
|
163
|
+
</Button>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<Table
|
|
167
|
+
rowKey="id"
|
|
168
|
+
dataSource={providers}
|
|
169
|
+
columns={columns}
|
|
170
|
+
loading={loading}
|
|
171
|
+
pagination={false}
|
|
172
|
+
locale={{ emptyText: t('No providers configured') }}
|
|
173
|
+
size="middle"
|
|
174
|
+
/>
|
|
175
|
+
|
|
176
|
+
<Modal
|
|
177
|
+
open={modalOpen}
|
|
178
|
+
title={editing ? t('Edit Provider') : t('Add Provider')}
|
|
179
|
+
onCancel={() => setModalOpen(false)}
|
|
180
|
+
footer={null}
|
|
181
|
+
width={720}
|
|
182
|
+
destroyOnClose
|
|
183
|
+
>
|
|
184
|
+
<ProviderForm
|
|
185
|
+
initialValues={editing ?? undefined}
|
|
186
|
+
onSubmit={handleSubmit}
|
|
187
|
+
onCancel={() => setModalOpen(false)}
|
|
188
|
+
submitting={submitting}
|
|
189
|
+
/>
|
|
190
|
+
</Modal>
|
|
191
|
+
</>
|
|
192
|
+
);
|
|
193
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Tabs } from 'antd';
|
|
3
|
+
import { SettingOutlined, ApiOutlined } from '@ant-design/icons';
|
|
4
|
+
import { GlobalSettings } from './GlobalSettings';
|
|
5
|
+
import { ProviderList } from './ProviderList';
|
|
6
|
+
import { useDocParserTranslation } from '../locale';
|
|
7
|
+
|
|
8
|
+
export const SettingsPage: React.FC = () => {
|
|
9
|
+
const { t } = useDocParserTranslation();
|
|
10
|
+
|
|
11
|
+
const items = [
|
|
12
|
+
{
|
|
13
|
+
key: 'global',
|
|
14
|
+
label: (
|
|
15
|
+
<span>
|
|
16
|
+
<SettingOutlined />
|
|
17
|
+
{t('Global Settings')}
|
|
18
|
+
</span>
|
|
19
|
+
),
|
|
20
|
+
children: <GlobalSettings />,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: 'providers',
|
|
24
|
+
label: (
|
|
25
|
+
<span>
|
|
26
|
+
<ApiOutlined />
|
|
27
|
+
{t('OCR Providers')}
|
|
28
|
+
</span>
|
|
29
|
+
),
|
|
30
|
+
children: (
|
|
31
|
+
<div style={{ padding: '8px 0' }}>
|
|
32
|
+
<ProviderList />
|
|
33
|
+
</div>
|
|
34
|
+
),
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div style={{ padding: 24 }}>
|
|
40
|
+
<Tabs items={items} defaultActiveKey="global" />
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { i18n } from '@nocobase/client';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
|
|
4
|
+
export const NAMESPACE = 'plugin-document-parser';
|
|
5
|
+
|
|
6
|
+
export function lang(key: string, options?: any) {
|
|
7
|
+
return i18n.t(key, { ns: NAMESPACE, ...options });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useDocParserTranslation() {
|
|
11
|
+
return useTranslation(NAMESPACE);
|
|
12
|
+
}
|