接入可配置文档编辑与智能取数服务

This commit is contained in:
baiyanyun
2026-07-13 14:53:14 +08:00
parent ab4e84004f
commit 474a4866d2
16 changed files with 443 additions and 9 deletions

View File

@@ -185,6 +185,12 @@ eco-market:
mcp:
proxy-base-url: ${MCP_PROXY_URL:http://localhost:8020}
intelligent-data:
service-url: ${INTELLIGENT_DATA_SERVICE_URL:http://192.168.2.106:3000/test-console}
document-editor:
service-url: ${DOCUMENT_EDITOR_SERVICE_URL:}
knife4j:
# 开启增强配置
enable: true

View File

@@ -185,6 +185,12 @@ eco-market:
mcp:
proxy-base-url: ${MCP_PROXY_URL:http://localhost:18020}
intelligent-data:
service-url: ${INTELLIGENT_DATA_SERVICE_URL:http://192.168.2.106:3000/test-console}
document-editor:
service-url: ${DOCUMENT_EDITOR_SERVICE_URL:}
knife4j:
# 开启增强配置
enable: true

View File

@@ -182,6 +182,12 @@ eco-market:
mcp:
proxy-base-url: ${MCP_PROXY_URL:http://localhost:8020}
intelligent-data:
service-url: ${INTELLIGENT_DATA_SERVICE_URL:http://192.168.2.106:3000/test-console}
document-editor:
service-url: ${DOCUMENT_EDITOR_SERVICE_URL:}
knife4j:
# 开启增强配置
enable: true

View File

@@ -182,6 +182,12 @@ eco-market:
mcp:
proxy-base-url: ${MCP_PROXY_URL:http://localhost:8020}
intelligent-data:
service-url: ${INTELLIGENT_DATA_SERVICE_URL:http://192.168.2.106:3000/test-console}
document-editor:
service-url: ${DOCUMENT_EDITOR_SERVICE_URL:}
knife4j:
# 开启增强配置
enable: true

View File

@@ -179,6 +179,12 @@ public class TenantConfigDto implements Serializable {
@Schema(description = "模型API根地址")
private String baseModelApiUrl;
@Schema(description = "智能取数服务地址")
private String intelligentDataServiceUrl;
@Schema(description = "文档编辑服务地址")
private String documentEditorServiceUrl;
public enum AuthTypeEnum {
PHONE(1, "手机"),
CAS(2, "CAS"),

View File

@@ -72,6 +72,30 @@ public class TenantConfigServiceImpl extends ServiceImpl<TenantConfigMapper, Ten
.required(true)
.sort(1)
.build());
tenantConfigDefaultList.add(TenantConfig.builder()
.name("intelligentDataServiceUrl")
.description("智能取数服务地址")
.value("http://192.168.2.106:3000/test-console")
.notice("客户端点击智能取数时加载的 HTTP 或 HTTPS 服务地址")
.placeholder("请输入智能取数服务地址")
.category(TenantConfig.ConfigCategory.BaseConfig)
.inputType(TenantConfig.InputType.Input)
.dataType(TenantConfig.DataType.String)
.required(true)
.sort(2)
.build());
tenantConfigDefaultList.add(TenantConfig.builder()
.name("documentEditorServiceUrl")
.description("文档编辑服务地址")
.value("")
.notice("客户端点击文档编辑时加载的 HTTP 或 HTTPS 服务地址")
.placeholder("请输入文档编辑服务地址")
.category(TenantConfig.ConfigCategory.BaseConfig)
.inputType(TenantConfig.InputType.Input)
.dataType(TenantConfig.DataType.String)
.required(false)
.sort(3)
.build());
tenantConfigDefaultList.add(TenantConfig.builder()
.name("siteDescription")
.description("站点介绍")

View File

@@ -38,6 +38,12 @@ public class TenantController {
@Value("${model-api-proxy.base-api-url:}")
private String baseApiUrl;
@Value("${intelligent-data.service-url:http://192.168.2.106:3000/test-console}")
private String intelligentDataServiceUrl;
@Value("${document-editor.service-url:}")
private String documentEditorServiceUrl;
@Operation(summary = "租户配置信息查询接口")
@RequestMapping(path = "/config", method = RequestMethod.GET)
public ReqResult<TenantConfigDto> getConfig() {
@@ -69,6 +75,12 @@ public class TenantController {
tenantConfigDto.setFaviconUrl(iFileAccessService.getFileUrlWithAk(tenantConfigDto.getFaviconUrl(), true));
tenantConfigDto.setSquareBanner(iFileAccessService.getFileUrlWithAk(tenantConfigDto.getSquareBanner(), true));
tenantConfigDto.setMaxFileSize(maxFileSize);
if (StringUtils.isBlank(tenantConfigDto.getIntelligentDataServiceUrl())) {
tenantConfigDto.setIntelligentDataServiceUrl(intelligentDataServiceUrl);
}
if (StringUtils.isBlank(tenantConfigDto.getDocumentEditorServiceUrl())) {
tenantConfigDto.setDocumentEditorServiceUrl(documentEditorServiceUrl);
}
if (StringUtils.isBlank(tenantConfigDto.getTemplateConfig()) || tenantConfigDto.getTemplateConfig().equals("{}")) {
tenantConfigDto.setTemplateConfig("{\"primaryColor\":\"#5147ff\",\"backgroundId\":\"bg-variant-1\",\"antdTheme\":\"light\",\"layoutStyle\":\"light\",\"navigationStyle\":\"style1\",\"timestamp\":1757425328082}");
}

View File

@@ -126,6 +126,24 @@ export default function BaseFormItem({
},
],
};
const serviceUrlFieldNames = [
'intelligentDataServiceUrl',
'documentEditorServiceUrl',
];
const fieldRules: Rule[] | undefined = serviceUrlFieldNames.includes(
props.name,
)
? [
{
required: props.name === 'intelligentDataServiceUrl',
message: `请输入${props.description}`,
},
{
type: 'url',
message: '请输入有效的 HTTP 或 HTTPS 地址',
},
]
: rules[currentTab];
// 处理 Select 类型的初始值:-1 表示未选择,应转换为 undefined
const getInitialValue = () => {
if (['Select'].includes(props.inputType) && Number(props.value) === -1) {
@@ -181,7 +199,7 @@ export default function BaseFormItem({
key={props.name}
name={props.name}
initialValue={getInitialValue()}
rules={rules[currentTab]}
rules={fieldRules}
{...(props.inputType === 'MultiInput'
? {
label: <MultiInputLabel />,

View File

@@ -369,6 +369,10 @@ export interface TenantConfigDto {
siteName?: string;
/** 站点描述 */
siteDescription?: string;
/** 智能取数服务地址 */
intelligentDataServiceUrl?: string;
/** 文档编辑服务地址 */
documentEditorServiceUrl?: string;
/** 站点LOGO为空使用默认值 */
siteLogo?: string;
/** 登录页banner */

View File

@@ -50,6 +50,8 @@ import SkillsMarketplacePage from "./components/pages/SkillsMarketplacePage";
import AgentMarketplacePage from "./components/pages/AgentMarketplacePage";
import McpMarketplacePage from "./components/pages/McpMarketplacePage";
import WorkflowMarketplacePage from "./components/pages/WorkflowMarketplacePage";
import DataFetchPage from "./components/pages/DataFetchPage";
import DocumentEditorPage from "./components/pages/DocumentEditorPage";
import MCPSettings from "./components/settings/MCPSettings";
import AppSidebar, {
NavKey,
@@ -106,6 +108,8 @@ type TabKey =
| "legacySessions"
| "automation"
| "knowledge"
| "document"
| "dataFetch"
| "skills"
| "agents"
| "mcpLibrary"
@@ -1080,6 +1084,16 @@ function App() {
return;
}
if (key === "document") {
setActiveTab("document");
return;
}
if (key === "dataFetch") {
setActiveTab("dataFetch");
return;
}
if (key === "toolIntegration" || key === "systemSettings") {
setActiveTab("settings");
return;
@@ -1337,7 +1351,9 @@ function App() {
{/* 主内容区flex 子撑满,便于日志等页占满高度 */}
<div
className={
webviewActions
webviewActions ||
activeTab === "document" ||
activeTab === "dataFetch"
? "app-content app-content-fullwidth"
: "app-content"
}
@@ -1385,6 +1401,8 @@ function App() {
)}
{activeTab === "automation" && <AutomationPage />}
{activeTab === "knowledge" && <KnowledgePage />}
{activeTab === "document" && <DocumentEditorPage />}
{activeTab === "dataFetch" && <DataFetchPage />}
{activeTab === "skills" && <SkillsMarketplacePage />}
{activeTab === "agents" && (
<AgentMarketplacePage onStartChat={handleStartAgentChat} />

View File

@@ -0,0 +1,144 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Alert, Button, Spin } from "antd";
import { ReloadOutlined } from "@ant-design/icons";
import styles from "../../styles/components/ConfiguredServicePage.module.css";
interface ConfiguredServicePageProps {
title: string;
loadServiceUrl: () => Promise<string>;
}
function ConfiguredServicePage({
title,
loadServiceUrl,
}: ConfiguredServicePageProps) {
const webviewRef = useRef<HTMLElement | null>(null);
const [serviceUrl, setServiceUrl] = useState<string | null>(null);
const [loadingConfig, setLoadingConfig] = useState(true);
const [loadingPage, setLoadingPage] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadService = useCallback(async () => {
setLoadingConfig(true);
setError(null);
try {
setServiceUrl(await loadServiceUrl());
} catch (cause) {
setServiceUrl(null);
setError(
cause instanceof Error ? cause.message : `读取${title}服务配置失败`,
);
} finally {
setLoadingConfig(false);
}
}, [loadServiceUrl, title]);
useEffect(() => {
void loadService();
}, [loadService]);
useEffect(() => {
const element = webviewRef.current as any;
if (!element || !serviceUrl) return;
const handleStart = () => {
setLoadingPage(true);
setError(null);
};
const handleStop = () => setLoadingPage(false);
const handleFailure = (event: any) => {
if (event.errorCode !== -3) {
setLoadingPage(false);
setError(
`${title}页面加载失败:${event.errorDescription || "网络连接失败"}`,
);
}
};
element.addEventListener("did-start-loading", handleStart);
element.addEventListener("did-stop-loading", handleStop);
element.addEventListener("did-fail-load", handleFailure);
return () => {
element.removeEventListener("did-start-loading", handleStart);
element.removeEventListener("did-stop-loading", handleStop);
element.removeEventListener("did-fail-load", handleFailure);
};
}, [serviceUrl, title]);
const handleReload = useCallback(() => {
const element = webviewRef.current as any;
if (element?.reload) {
element.reload();
return;
}
void loadService();
}, [loadService]);
if (loadingConfig) {
return (
<div className={styles.state}>
<Spin tip={`正在加载${title}`} />
</div>
);
}
if (!serviceUrl) {
return (
<div className={styles.state}>
<Alert
type="error"
showIcon
message={error || `${title}服务地址未配置`}
action={
<Button
size="small"
icon={<ReloadOutlined />}
onClick={loadService}
>
</Button>
}
/>
</div>
);
}
return (
<div className={styles.container}>
<div className={styles.toolbar}>
<span className={styles.title}>{title}</span>
<span className={styles.url} title={serviceUrl}>
{serviceUrl}
</span>
<Button
size="small"
icon={<ReloadOutlined />}
loading={loadingPage}
onClick={handleReload}
title={`刷新${title}页面`}
>
</Button>
</div>
{error && (
<Alert
className={styles.alert}
type="error"
showIcon
closable
message={error}
onClose={() => setError(null)}
/>
)}
<webview
key={serviceUrl}
ref={webviewRef as any}
src={serviceUrl}
allowpopups={"true" as any}
className={styles.webview}
/>
</div>
);
}
export default ConfiguredServicePage;

View File

@@ -0,0 +1,14 @@
import React from "react";
import { fetchIntelligentDataServiceUrl } from "../../services/core/servicePages";
import ConfiguredServicePage from "./ConfiguredServicePage";
function DataFetchPage() {
return (
<ConfiguredServicePage
title="智能取数"
loadServiceUrl={fetchIntelligentDataServiceUrl}
/>
);
}
export default DataFetchPage;

View File

@@ -0,0 +1,14 @@
import React from "react";
import { fetchDocumentEditorServiceUrl } from "../../services/core/servicePages";
import ConfiguredServicePage from "./ConfiguredServicePage";
function DocumentEditorPage() {
return (
<ConfiguredServicePage
title="文档编辑"
loadServiceUrl={fetchDocumentEditorServiceUrl}
/>
);
}
export default DocumentEditorPage;

View File

@@ -2,7 +2,7 @@ import { message } from "antd";
import {
AUTH_KEYS,
DEFAULT_API_TIMEOUT,
DEFAULT_SERVER_HOST,
STORAGE_KEYS,
} from "@shared/constants";
import { getDomainTokenKey } from "@shared/utils/domain";
import { normalizeServerHost } from "./auth";
@@ -179,11 +179,15 @@ export interface AutomationTemplate {
}
async function getBaseUrl(): Promise<string> {
const config = (await window.electronAPI?.settings.get(
AUTH_KEYS.STEP1_CONFIG,
)) as { serverHost?: string } | null;
return normalizeServerHost(config?.serverHost || DEFAULT_SERVER_HOST);
const [userInfo, config] = (await Promise.all([
window.electronAPI?.settings.get(AUTH_KEYS.USER_INFO),
window.electronAPI?.settings.get(STORAGE_KEYS.STEP1_CONFIG),
])) as [{ currentDomain?: string } | null, { serverHost?: string } | null];
const loginServerHost = userInfo?.currentDomain || config?.serverHost || "";
if (!loginServerHost) {
throw new Error("未找到登录站点地址,请重新登录");
}
return normalizeServerHost(loginServerHost);
}
async function getAuthToken(domain: string): Promise<string | null> {

View File

@@ -0,0 +1,94 @@
import {
AUTH_KEYS,
DEFAULT_API_TIMEOUT,
STORAGE_KEYS,
} from "@shared/constants";
import { getDomainTokenKey } from "@shared/utils/domain";
import { normalizeServerHost } from "./auth";
const SUCCESS_CODE = "0000";
const INTELLIGENT_DATA_FALLBACK_URL = "http://192.168.2.106:3000/test-console";
interface TenantConfigResponse {
code: string;
message?: string;
data?: {
intelligentDataServiceUrl?: string;
documentEditorServiceUrl?: string;
};
}
async function getLoginServerHost(): Promise<string> {
const [userInfo, config] = (await Promise.all([
window.electronAPI?.settings.get(AUTH_KEYS.USER_INFO),
window.electronAPI?.settings.get(STORAGE_KEYS.STEP1_CONFIG),
])) as [{ currentDomain?: string } | null, { serverHost?: string } | null];
const loginServerHost = userInfo?.currentDomain || config?.serverHost || "";
if (!loginServerHost) {
throw new Error("未找到登录站点地址,请重新登录");
}
return normalizeServerHost(loginServerHost);
}
async function getToken(serverHost: string): Promise<string | null> {
const oneShotToken = (await window.electronAPI?.settings.get(
AUTH_KEYS.AUTH_TOKEN,
)) as string | null;
if (oneShotToken) return oneShotToken;
return (await window.electronAPI?.settings.get(
getDomainTokenKey(serverHost),
)) as string | null;
}
function normalizeServiceUrl(value: string, serviceName: string): string {
const candidate = value.trim();
if (!candidate) {
throw new Error(`${serviceName}服务地址未配置,请在系统设置中配置`);
}
const url = new URL(candidate);
if (!/^https?:$/.test(url.protocol)) {
throw new Error(`${serviceName}服务地址必须使用 HTTP 或 HTTPS`);
}
return url.toString();
}
async function fetchTenantConfig(): Promise<TenantConfigResponse["data"]> {
const serverHost = await getLoginServerHost();
const token = await getToken(serverHost);
const response = await fetch(`${serverHost}/api/tenant/config`, {
method: "GET",
credentials: "include",
cache: "no-store",
headers: {
Accept: "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
signal: AbortSignal.timeout(DEFAULT_API_TIMEOUT),
});
if (!response.ok) {
throw new Error(`读取服务配置失败HTTP ${response.status}`);
}
const result = (await response.json()) as TenantConfigResponse;
if (result.code !== SUCCESS_CODE) {
throw new Error(result.message || "读取服务配置失败");
}
return result.data;
}
export async function fetchIntelligentDataServiceUrl(): Promise<string> {
const config = await fetchTenantConfig();
return normalizeServiceUrl(
config?.intelligentDataServiceUrl || INTELLIGENT_DATA_FALLBACK_URL,
"智能取数",
);
}
export async function fetchDocumentEditorServiceUrl(): Promise<string> {
const config = await fetchTenantConfig();
return normalizeServiceUrl(
config?.documentEditorServiceUrl || "",
"文档编辑",
);
}

View File

@@ -0,0 +1,58 @@
.container {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
overflow: hidden;
background: var(--color-bg-container);
}
.toolbar {
display: flex;
align-items: center;
gap: 10px;
min-height: 44px;
padding: 7px 16px;
border-bottom: 1px solid var(--color-border);
background: var(--color-bg-section-header);
flex-shrink: 0;
}
.title {
color: var(--color-text);
font-size: 14px;
font-weight: 600;
flex-shrink: 0;
}
.url {
min-width: 0;
flex: 1;
overflow: hidden;
color: var(--color-text-tertiary);
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.alert {
margin: 8px 12px 0;
flex-shrink: 0;
}
.webview {
width: 100%;
min-height: 0;
flex: 1;
border: 0;
background: #fff;
}
.state {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
min-height: 240px;
padding: 24px;
}