Compare commits
19 Commits
master
...
c13e01c902
| Author | SHA1 | Date | |
|---|---|---|---|
| c13e01c902 | |||
| c8ad78ab44 | |||
| 04a8912307 | |||
| 495fde42ca | |||
| 5363613a7d | |||
| fc54bcd3d6 | |||
| 4a8bc3cb7a | |||
| 367cd98018 | |||
| ee8ba0a5d9 | |||
| 56e1517743 | |||
| eea5874dbf | |||
| 6ef063ac09 | |||
| c37710af3d | |||
| c52d31c16e | |||
| 48a4636a81 | |||
| 9c03013c44 | |||
| 05a0e35891 | |||
| 937d1e9373 | |||
| af3f0678b8 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# Vue language service / accidental transpile artifacts
|
||||
src/**/*.vue.js
|
||||
|
||||
|
||||
|
||||
119
CLAUDE.md
119
CLAUDE.md
@@ -92,6 +92,125 @@ This is the **GFast UI** project (`gfast-ui`), a Vue 3 admin management system b
|
||||
|
||||
5. **Keep-alive**: Route components can be cached via keep-alive, managed by the `useKeepALiveNames` store.
|
||||
|
||||
## Repository Collaboration Rules
|
||||
|
||||
These rules capture long-term repository preferences confirmed by the user and should be applied in future sessions unless the user explicitly overrides them.
|
||||
|
||||
### Pagination Rules
|
||||
|
||||
- Prefer the project global `pagination` component by default.
|
||||
- Pagination should be fixed and visible as part of the page or dialog layout, not conditionally hidden only because the dataset is small.
|
||||
- In single-page scenarios, the UI should still show total count together with the pagination area.
|
||||
- Avoid designs where pagination only appears after data exceeds one page.
|
||||
|
||||
### Dialog Layout Rules
|
||||
|
||||
- For dialogs that contain searchable tabular data, prefer this structure:
|
||||
1. search area
|
||||
2. table/content area
|
||||
3. pagination area placed under the table inside the dialog body
|
||||
4. footer action buttons
|
||||
- The pagination area should remain visually stable and should not be omitted just because the current result set is small.
|
||||
|
||||
### Theme and Style Rules
|
||||
|
||||
- When functionality and existing theme style conflict, prioritize making the feature clearly visible and usable first.
|
||||
- After ensuring usability, align the implementation with the repository's existing theme and style system as much as possible.
|
||||
- Be cautious about global theme styles that may affect local pagination, dialog, or table layouts.
|
||||
|
||||
### Problem-solving Workflow
|
||||
|
||||
- For repeated UI issues, analyze in this order:
|
||||
1. interface/request data and returned structure
|
||||
2. component logic and render conditions
|
||||
3. theme and style coverage/conflicts
|
||||
4. then decide whether a rewrite is necessary
|
||||
- Do not jump straight to a rewrite before checking interface behavior and style interference.
|
||||
|
||||
### Execution Workflow Rules
|
||||
|
||||
- When modifying a feature, locate it in this order: usage location, component definition, API definition, then at least one similar implementation in the project.
|
||||
- For common UI features such as pagination, dialogs, and tables, align with an existing similar implementation before making changes.
|
||||
- Prefer componentization. If a page or Vue SFC is already large, or a block has a clear independent responsibility, split it into components instead of continuing to extend the same file.
|
||||
- For very long files such as complex page-level `index.vue` files, treat further component splitting as the default direction rather than the exception.
|
||||
- Before editing large files or complex Vue SFCs, first read the target context; if a replacement fails once, do not repeat the same replacement blindly, and instead expand the context or switch to smaller targeted edits.
|
||||
- After UI or interaction changes, verify not only lint status for edited files but also render conditions, interface fields, and possible style impact.
|
||||
- If the same issue is not resolved after one attempt, pause and ask the user before continuing with more speculative changes.
|
||||
- Do not proactively rewrite a component unless the user explicitly asks for a rewrite.
|
||||
- At the start of a new conversation, read `CLAUDE.md` before making changes.
|
||||
- When code changes appear not to take effect, check in this order: compile/HMR status, render conditions, API data, then style overrides.
|
||||
- Turn repeated low-level mistakes into explicit workflow checks and follow those checks in future edits.
|
||||
|
||||
### Environment and Tool Selection Rules
|
||||
|
||||
- Before running environment-dependent commands, first follow the known runtime context already provided by the IDE, such as OS and default shell.
|
||||
- On Windows projects with PowerShell as the active shell, prefer PowerShell-native commands and scripts by default.
|
||||
- Do not assume Python, bash, sed, awk, or other non-default tooling is installed locally unless the user or repository explicitly indicates that they are available.
|
||||
- For reading and editing repository files, prefer dedicated file tools first; use shell-based text replacement only when file tools are not suitable for the change.
|
||||
- When a replacement fails once, do not keep retrying the same method blindly. Re-read the exact target content and switch to a smaller or more reliable edit strategy that matches the current environment.
|
||||
- Avoid trial-and-error probing for basic tooling when the available environment is already known from context.
|
||||
|
||||
### Componentization and Structure Rules
|
||||
|
||||
- If a view block has an independent responsibility, prefer extracting it into a component instead of keeping it inside a large page file.
|
||||
- When a file is already long, or a new feature would further expand responsibilities, proactively split it into components.
|
||||
- Child components may keep their own local logic when that logic belongs clearly to the child’s responsibility; only shared state and necessary events should stay in the parent.
|
||||
- For complex pages, small-scale components can stay in `component/`, while larger feature areas may be grouped by responsibility in subdirectories.
|
||||
- For very long page files, prefer keeping the parent focused on composition, state orchestration, and data flow, while moving feature UI and local behavior into child components.
|
||||
- When extracting new child components from a large file, move their related local styles with them to avoid continued growth of the parent file.
|
||||
|
||||
### API and Data Handling Rules
|
||||
|
||||
- Follow the project’s common response structure by default, but add moderate component-side compatibility handling when equivalent APIs return slightly inconsistent fields.
|
||||
- For page-level error handling, use global handling by default, but use page-level or silent handling when the interaction requires local control.
|
||||
- Prefer a unified parameter-building entry for request params instead of assembling the same parameters across multiple functions.
|
||||
|
||||
### Form and Interaction Rules
|
||||
|
||||
- Follow the project’s existing form behavior style: required fields should be clearly validated on submit, while avoiding unnecessarily disruptive validation during input.
|
||||
- For shared create/edit forms, initialize, patch, and reset form state through a unified initialization path to avoid stale state.
|
||||
- Search interactions should behave consistently across button click, Enter key, and clear actions, and should reset to the first page by default.
|
||||
|
||||
### Dialog and Selector Rules
|
||||
|
||||
- Dialogs should clear temporary internal state on close by default; any state that needs to persist should be explicitly provided back by the parent.
|
||||
- Selector components should follow a consistent pattern: `v-model` for visibility, a default value prop, and a `confirm` event for returning the selected result.
|
||||
- For temporary selectors and lightweight dialogs, prefer `destroy-on-close`; for more complex stateful forms, decide deliberately rather than applying it blindly.
|
||||
|
||||
### Styling Rules
|
||||
|
||||
- Prefer local scoped style changes first; only modify global theme styles after confirming the issue is truly shared across multiple places.
|
||||
- When overriding Element Plus internals, prefer local `:deep()` overrides before considering global style changes.
|
||||
- Use semantic, area-based class names instead of short or overly generic names.
|
||||
|
||||
### Debugging and Validation Rules
|
||||
|
||||
- Minimal temporary debugging is allowed when necessary, but all temporary debugging code must be removed before final delivery.
|
||||
- Temporary `console` output is acceptable only when debugging is genuinely needed and must be removed before final delivery.
|
||||
- For UI and interaction changes, verify lint, key render conditions, interface fields, and style impact before considering the task complete.
|
||||
|
||||
### Change Scope and Consistency Rules
|
||||
|
||||
- Prefer the smallest necessary change set, but do not pretend a local-only fix is sufficient when the real root cause is elsewhere in the chain.
|
||||
- Only fix nearby issues when they are strongly related to the current task; otherwise ask the user before expanding the scope.
|
||||
- Within the current modification scope, move code toward repository rules when practical, but avoid broad unrelated refactors.
|
||||
|
||||
### Communication and Risk Rules
|
||||
|
||||
- Before changing code, confirm with the user when a change involves a rewrite, global side effects, unclear requirements, or a second attempt that still has not solved the issue.
|
||||
- After changes, explain what changed, why it was changed that way, and any remaining risks or points worth confirming.
|
||||
- When a requirement still depends on a meaningful assumption, ask the user first instead of silently choosing a direction.
|
||||
|
||||
### Repository Memory Rules
|
||||
|
||||
- When collaboration reveals a repeated problem pattern, proactively suggest turning it into an explicit repository rule in `CLAUDE.md`.
|
||||
- Project-specific experience or pitfall notes may be added when they become necessary, but do not expand them prematurely without clear recurring value.
|
||||
|
||||
### Rule Documentation Style
|
||||
|
||||
- Keep long-term repository rules concise and practical.
|
||||
- Prefer short rule lists with a small amount of explanation over overly long documentation.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `.env.development` - Development environment settings
|
||||
|
||||
@@ -37,6 +37,8 @@ export interface NodeLibraryItem {
|
||||
nodeName: string;
|
||||
modelType: number;
|
||||
skillOption: boolean;
|
||||
promptOption: boolean;
|
||||
isSaveFile: boolean;
|
||||
formConfig: NodeLibraryFormItem[];
|
||||
modelConfig: NodeLibraryModelConfig[];
|
||||
}
|
||||
@@ -86,6 +88,7 @@ export interface ExecutionItem {
|
||||
timestamp: string;
|
||||
content: string;
|
||||
label: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface ExecutionFlowItem {
|
||||
|
||||
@@ -119,18 +119,22 @@ export interface ModelModuleItem {
|
||||
/** 会话开关状态(列表接口返回,0 关 1 开;会话开关接口就绪后生效) */
|
||||
chatSessionEnabled?: number;
|
||||
enabled: number;
|
||||
/** 调用模式:0-同步 1-异步 2-流式 */
|
||||
callMode?: number;
|
||||
maxConcurrency: number;
|
||||
queueLimit: number;
|
||||
queueLimit?: number;
|
||||
timeoutMs?: number;
|
||||
timeoutSeconds?: number;
|
||||
expectedSeconds?: number;
|
||||
retryTimes: number;
|
||||
retryQueueMaxSeconds: number;
|
||||
retryQueueMaxSeconds?: number;
|
||||
autoCleanSeconds: number;
|
||||
remark?: string;
|
||||
headMsg?: string;
|
||||
headMsg?: string | Record<string, string>;
|
||||
form?: ModelFormEntry[] | Record<string, { value: string }>;
|
||||
requestMapping?: Record<string, unknown>;
|
||||
requiredFields?: string[];
|
||||
firstFrame?: string;
|
||||
lastFrame?: string;
|
||||
responseMapping?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@@ -150,27 +154,32 @@ export interface CreateModelParams {
|
||||
operatorName?: string;
|
||||
baseUrl: string;
|
||||
httpMethod?: string;
|
||||
headMsg?: string;
|
||||
headMsg?: Record<string, string>;
|
||||
isPrivate: number;
|
||||
enabled: number;
|
||||
isChatModel: number;
|
||||
/** 调用模式:0-同步 1-异步 2-流式,默认0 */
|
||||
callMode: number;
|
||||
apiKey?: string;
|
||||
form: ModelFormEntry[];
|
||||
requestMapping?: Record<string, unknown>;
|
||||
requiredFields?: string[];
|
||||
firstFrame?: string;
|
||||
lastFrame?: string;
|
||||
responseMapping?: Record<string, unknown>;
|
||||
responseBody?: Record<string, unknown>;
|
||||
responseBody?: string;
|
||||
extendMapping?: Record<string, unknown>;
|
||||
responseTokenField?: string;
|
||||
tokenConfig?: Record<string, unknown>;
|
||||
queryConfig?: Record<string, unknown>;
|
||||
streamConfig?: Record<string, unknown>;
|
||||
maxConcurrency?: number;
|
||||
queueLimit?: number;
|
||||
timeoutSeconds: number;
|
||||
expectedSeconds: number;
|
||||
expectedSeconds?: number;
|
||||
retryTimes?: number;
|
||||
retryQueueMaxSeconds: number;
|
||||
retryQueueMaxSeconds?: number;
|
||||
autoCleanSeconds: number;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface ModelConfigTypeItem {
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface PromptListParams {
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
keyword?: string;
|
||||
nodeType?: string;
|
||||
}
|
||||
|
||||
export interface CreatePromptParams {
|
||||
@@ -65,6 +66,17 @@ export function getNodeLibraryList() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取提示词列表(根据节点类型)
|
||||
*/
|
||||
export function getPromptList(params: PromptListParams) {
|
||||
return request<PromptListResponse>({
|
||||
url: '/ai-agent/node/prompt/list',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户提示词列表
|
||||
*/
|
||||
|
||||
@@ -118,15 +118,22 @@ interface ModelItem {
|
||||
apiKey?: string;
|
||||
isPrivate?: number;
|
||||
isChatModel?: number;
|
||||
headMsg?: string;
|
||||
headMsg?: string | Record<string, string>;
|
||||
operatorName?: string;
|
||||
responseBody?: Record<string, unknown>;
|
||||
responseTokenField?: string;
|
||||
tokenConfig?: Record<string, unknown> | string;
|
||||
extendMapping?: Record<string, unknown> | string;
|
||||
queryConfig?: Record<string, unknown>;
|
||||
streamConfig?: Record<string, unknown>;
|
||||
form?: ModelFormEntry[] | Record<string, unknown>;
|
||||
requestMapping?: Record<string, unknown>;
|
||||
requiredFields?: string[];
|
||||
firstFrame?: string;
|
||||
lastFrame?: string;
|
||||
responseMapping?: Record<string, unknown>;
|
||||
callMode?: number;
|
||||
isAsync?: number;
|
||||
maxConcurrency?: number;
|
||||
queueLimit?: number;
|
||||
timeoutSeconds?: number;
|
||||
@@ -271,74 +278,71 @@ const fieldsToUnknownObject = (fields: Array<{ key: string; value: string }>) =>
|
||||
return obj;
|
||||
};
|
||||
|
||||
const flattenNestedObject = (obj: Record<string, unknown>, prefix = ''): Array<{ key: string; value: string }> => {
|
||||
const rows: Array<{ key: string; value: string }> = [];
|
||||
Object.entries(obj || {}).forEach(([k, v]) => {
|
||||
const fk = prefix ? `${prefix}.${k}` : k;
|
||||
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
||||
rows.push(...flattenNestedObject(v as Record<string, unknown>, fk));
|
||||
return;
|
||||
}
|
||||
rows.push({ key: fk, value: String(v ?? '') });
|
||||
});
|
||||
return rows;
|
||||
};
|
||||
|
||||
const nestFieldsToObject = (fields: Array<{ key: string; value: string }>) => {
|
||||
const root: Record<string, unknown> = {};
|
||||
fields.forEach((f) => {
|
||||
const path = String(f.key || '').trim();
|
||||
if (!path) return;
|
||||
const parts = path
|
||||
.split('.')
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean);
|
||||
if (!parts.length) return;
|
||||
let cur: Record<string, unknown> = root;
|
||||
parts.forEach((part, idx) => {
|
||||
if (idx === parts.length - 1) {
|
||||
cur[part] = String(f.value ?? '');
|
||||
return;
|
||||
}
|
||||
if (!cur[part] || typeof cur[part] !== 'object' || Array.isArray(cur[part])) {
|
||||
cur[part] = {};
|
||||
}
|
||||
cur = cur[part] as Record<string, unknown>;
|
||||
});
|
||||
});
|
||||
return root;
|
||||
};
|
||||
|
||||
const buildQueryConfigFromRaw = (rawQc: Record<string, unknown> | null): Record<string, unknown> => {
|
||||
if (!rawQc) return { responseType: 'sync', callbackUrl: '' };
|
||||
const rt = String(rawQc.responseType || 'sync');
|
||||
if (rt === 'callback') return { responseType: 'callback', callbackUrl: String(rawQc.callbackUrl || '') };
|
||||
if (rt === 'pull') {
|
||||
const hFields = Object.entries((rawQc.headers as Record<string, unknown>) || {}).map(([k, v]) => ({ key: k, value: String(v ?? '') }));
|
||||
const bFields = flattenNestedObject((rawQc.body as Record<string, unknown>) || {});
|
||||
return {
|
||||
responseType: 'pull',
|
||||
callbackUrl: '',
|
||||
method: String(rawQc.method || 'GET'),
|
||||
url: String(rawQc.url || ''),
|
||||
headers: fieldsToUnknownObject(hFields),
|
||||
body: nestFieldsToObject(bFields),
|
||||
response: ((rawQc.response as unknown[]) || [])
|
||||
.map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
return { value: item, isTokenField: false, isMainBody: false };
|
||||
}
|
||||
const row = item as Record<string, unknown>;
|
||||
return {
|
||||
value: String(row.value ?? ''),
|
||||
isTokenField: Boolean(row.isTokenField),
|
||||
isMainBody: Boolean(row.isMainBody),
|
||||
};
|
||||
})
|
||||
.filter((row) => row.value !== ''),
|
||||
};
|
||||
const parseFormEntries = (raw: unknown): ModelFormEntry[] => {
|
||||
if (Array.isArray(raw)) {
|
||||
return (raw as ModelFormEntry[])
|
||||
.filter((item) => item && item.key !== undefined)
|
||||
.map((item) => ({
|
||||
key: String(item.key ?? '').trim(),
|
||||
value: String(item.value ?? ''),
|
||||
}))
|
||||
.filter((item) => item.key !== '');
|
||||
}
|
||||
return { responseType: 'sync', callbackUrl: '' };
|
||||
|
||||
if (raw && typeof raw === 'object') {
|
||||
return Object.entries(raw as Record<string, unknown>).map(([key, value]) => {
|
||||
let nextValue = value;
|
||||
if (nextValue && typeof nextValue === 'object' && !Array.isArray(nextValue) && 'value' in nextValue) {
|
||||
nextValue = (nextValue as { value: unknown }).value;
|
||||
}
|
||||
return {
|
||||
key: String(key).trim(),
|
||||
value: String(nextValue ?? ''),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const normalizeQueryConfig = (raw: unknown): Record<string, unknown> => {
|
||||
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
||||
return raw as Record<string, unknown>;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const parseHeadMsgRecord = (raw: ModelItem['headMsg']) => {
|
||||
const headMsgRecord: Record<string, string> = {};
|
||||
if (typeof raw === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
Object.entries(parsed).forEach(([k, v]) => {
|
||||
headMsgRecord[k] = String(v);
|
||||
});
|
||||
return headMsgRecord;
|
||||
}
|
||||
} catch {
|
||||
const pairs = raw.split(',');
|
||||
pairs.forEach((pair) => {
|
||||
const idx = pair.indexOf(':');
|
||||
if (idx === -1) return;
|
||||
const key = pair.slice(0, idx).trim();
|
||||
const value = pair.slice(idx + 1).trim();
|
||||
if (key) {
|
||||
headMsgRecord[key] = value;
|
||||
}
|
||||
});
|
||||
return headMsgRecord;
|
||||
}
|
||||
}
|
||||
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
||||
Object.entries(raw).forEach(([k, v]) => {
|
||||
headMsgRecord[k] = String(v);
|
||||
});
|
||||
}
|
||||
return headMsgRecord;
|
||||
};
|
||||
|
||||
const fetchModelList = async () => {
|
||||
@@ -396,25 +400,25 @@ const handleCreatePrivateModel = async () => {
|
||||
creatingModel.value = true;
|
||||
|
||||
const builtInModel = builtInModelToClone.value;
|
||||
const formList: ModelFormEntry[] = Array.isArray(builtInModel.form)
|
||||
? (builtInModel.form as ModelFormEntry[])
|
||||
: Object.entries((builtInModel.form as Record<string, unknown>) || {}).map(([key, value]) => ({
|
||||
key: String(key),
|
||||
value: String(value ?? ''),
|
||||
}));
|
||||
const formList = parseFormEntries(builtInModel.form);
|
||||
|
||||
const createParams: CreateModelParams = {
|
||||
modelName: apiKeyForm.modelName,
|
||||
modelType: builtInModel.modelType,
|
||||
operatorName: builtInModel.operatorName || '',
|
||||
baseUrl: builtInModel.baseUrl,
|
||||
httpMethod: builtInModel.httpMethod || 'POST',
|
||||
headMsg: builtInModel.headMsg || '',
|
||||
headMsg: parseHeadMsgRecord(builtInModel.headMsg),
|
||||
isPrivate: builtInModel.isPrivate ?? 1,
|
||||
enabled: builtInModel.enabled ?? 1,
|
||||
isChatModel: builtInModel.isChatModel || 0,
|
||||
callMode: builtInModel.callMode ?? builtInModel.isAsync ?? 0,
|
||||
apiKey: apiKeyForm.apiKey,
|
||||
form: formList,
|
||||
requestMapping: (builtInModel.requestMapping as Record<string, unknown>) || {},
|
||||
requiredFields: Array.isArray(builtInModel.requiredFields) ? builtInModel.requiredFields : [],
|
||||
firstFrame: String(builtInModel.firstFrame || ''),
|
||||
lastFrame: String(builtInModel.lastFrame || ''),
|
||||
responseMapping: (builtInModel.responseMapping as Record<string, unknown>) || {},
|
||||
responseBody: builtInModel.responseBody || {},
|
||||
maxConcurrency: builtInModel.maxConcurrency || 10,
|
||||
@@ -424,19 +428,20 @@ const handleCreatePrivateModel = async () => {
|
||||
retryTimes: builtInModel.retryTimes || 3,
|
||||
retryQueueMaxSeconds: builtInModel.retryQueueMaxSeconds || 60,
|
||||
autoCleanSeconds: builtInModel.autoCleanSeconds || 300,
|
||||
remark: builtInModel.remark || '',
|
||||
|
||||
extendMapping: fieldsToUnknownObject(
|
||||
Object.entries(parseJsonObjectField(builtInModel.extendMapping)).map(([k, v]) => ({ key: k, value: String(v ?? '') }))
|
||||
),
|
||||
tokenConfig: fieldsToUnknownObject(
|
||||
Object.entries(parseJsonObjectField(builtInModel.tokenConfig)).map(([k, v]) => ({ key: k, value: String(v ?? '') }))
|
||||
),
|
||||
queryConfig: buildQueryConfigFromRaw(
|
||||
builtInModel.queryConfig && typeof builtInModel.queryConfig === 'object' && !Array.isArray(builtInModel.queryConfig)
|
||||
? (builtInModel.queryConfig as Record<string, unknown>)
|
||||
: null
|
||||
),
|
||||
queryConfig: normalizeQueryConfig(builtInModel.queryConfig),
|
||||
responseTokenField: String(builtInModel.responseTokenField || ''),
|
||||
streamConfig:
|
||||
Number(builtInModel.callMode ?? builtInModel.isAsync ?? 0) === 2
|
||||
? builtInModel.streamConfig && typeof builtInModel.streamConfig === 'object' && !Array.isArray(builtInModel.streamConfig)
|
||||
? (builtInModel.streamConfig as Record<string, unknown>)
|
||||
: {}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
const res: any = await addModelModule(createParams);
|
||||
|
||||
@@ -34,6 +34,23 @@ export const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes: staticRoutes,
|
||||
});
|
||||
const hasValidResolvedRoute = (to: any) => {
|
||||
const resolved = router.resolve(to.fullPath);
|
||||
const matched = resolved.matched || [];
|
||||
|
||||
const isStaticFallbackRoute = matched.length === 1 && matched[0]?.path === '/:pathMatch(.*)*';
|
||||
|
||||
const isRedirectTo404Route = matched.length > 0 && matched.every((item) => item.redirect === '/404');
|
||||
|
||||
return matched.length > 0 && !isStaticFallbackRoute && !isRedirectTo404Route;
|
||||
};
|
||||
const goResolvedOr404AfterInit = (to: any, next: any) => {
|
||||
if (hasValidResolvedRoute(to)) {
|
||||
next({ ...to, replace: true });
|
||||
} else {
|
||||
next('/404');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 路由多级嵌套数组处理成一维数组
|
||||
@@ -106,18 +123,18 @@ router.beforeEach(async (to, from, next) => {
|
||||
const { routesList } = storeToRefs(storesRoutesList);
|
||||
if (routesList.value.length === 0) {
|
||||
if (isRequestRoutes) {
|
||||
// 后端控制路由:路由数据初始化,防止刷新时丢失
|
||||
await initBackEndControlRoutes();
|
||||
// 动态添加路由:防止非首页刷新时跳转回首页的问题
|
||||
// 确保 addRoute() 时动态添加的路由已经被完全加载上去
|
||||
next({ ...to, replace: true });
|
||||
goResolvedOr404AfterInit(to, next);
|
||||
} else {
|
||||
// https://gitee.com/lyt-top/vue-next-admin/issues/I5F1HP
|
||||
await initFrontEndControlRoutes();
|
||||
next({ ...to, replace: true });
|
||||
goResolvedOr404AfterInit(to, next);
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
if (hasValidResolvedRoute(to)) {
|
||||
next();
|
||||
} else {
|
||||
next('/404');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
/**
|
||||
* 路由meta对象参数说明
|
||||
@@ -69,9 +69,9 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
|
||||
|
||||
/**
|
||||
* 盘点管理路由配置(供后端菜单配置参考)
|
||||
*
|
||||
*
|
||||
* 父级菜单: 库存作业 (/assets/operation)
|
||||
*
|
||||
*
|
||||
* 盘点菜单配置:
|
||||
* - 路由路径: /assets/operation/count
|
||||
* - 组件路径: assets/operation/count/index
|
||||
@@ -1077,8 +1077,8 @@ export const demoRoutes: Array<RouteRecordRaw> = [
|
||||
*/
|
||||
export const notFoundAndNoPower = [
|
||||
{
|
||||
path: '/:path(.*)*',
|
||||
name: 'notFound',
|
||||
path: '/404',
|
||||
name: 'notFoundPage',
|
||||
component: () => import('/@/views/error/404.vue'),
|
||||
meta: {
|
||||
title: 'message.staticRoutes.notFound',
|
||||
@@ -1094,6 +1094,15 @@ export const notFoundAndNoPower = [
|
||||
isHide: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/:path(.*)*',
|
||||
name: 'notFound',
|
||||
redirect: '/404',
|
||||
meta: {
|
||||
title: 'message.staticRoutes.notFound',
|
||||
isHide: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
242
src/views/settings/creation/component/PromptSelector.vue
Normal file
242
src/views/settings/creation/component/PromptSelector.vue
Normal file
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择提示词"
|
||||
width="900px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="prompt-selector-dialog">
|
||||
<div class="prompt-header">
|
||||
<div class="search-bar">
|
||||
<el-input v-model="searchParams.keyword" placeholder="搜索提示词内容" clearable @clear="handleSearch" @keyup.enter="handleSearch">
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="prompt-content" v-loading="loading">
|
||||
<el-empty v-if="!loading && promptList.length === 0" description="暂无提示词数据" :image-size="100" />
|
||||
<el-table v-else :data="promptList" height="360" border stripe style="width: 100%" @row-click="handleSelectPrompt">
|
||||
<el-table-column label="节点类型" width="120" prop="nodeType" />
|
||||
<el-table-column label="提示词内容" prop="prompt" show-overflow-tooltip />
|
||||
<el-table-column label="来源" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.sourceType === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.sourceType === 1 ? '公共' : '自定义' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="选择" width="60" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-icon v-if="selectedPrompt?.id === row.id" class="check-icon" color="#67c23a">
|
||||
<CircleCheck />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div v-if="shouldRenderPagination" class="pagination-panel">
|
||||
<div class="pagination-summary">共 {{ displayTotal }} 条</div>
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.pageNum"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="displayTotal"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="sizes, prev, pager, next, jumper"
|
||||
background
|
||||
:hide-on-single-page="false"
|
||||
@current-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedPrompt" @click="handleConfirm">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { Search, CircleCheck } from '@element-plus/icons-vue';
|
||||
import { getPromptList, type PromptItem } from '/@/api/settings/promptManager';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
defaultPrompt?: PromptItem | null;
|
||||
nodeType?: string;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
(e: 'confirm', prompt: PromptItem): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
defaultPrompt: null,
|
||||
nodeType: '',
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const promptList = ref<PromptItem[]>([]);
|
||||
const selectedPrompt = ref<PromptItem | null>(null);
|
||||
const searchParams = reactive({ keyword: '' });
|
||||
const pagination = reactive({ pageNum: 1, pageSize: 10, total: 0 });
|
||||
|
||||
const displayTotal = computed(() => {
|
||||
if (pagination.total > 0) return pagination.total;
|
||||
if (promptList.value.length > 0) return promptList.value.length;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const shouldRenderPagination = computed(() => displayTotal.value > 0);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
visible.value = val;
|
||||
if (val) {
|
||||
selectedPrompt.value = props.defaultPrompt || null;
|
||||
pagination.pageNum = 1;
|
||||
fetchPromptList();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.nodeType,
|
||||
() => {
|
||||
if (visible.value) {
|
||||
pagination.pageNum = 1;
|
||||
fetchPromptList();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(visible, (val) => {
|
||||
if (!val) {
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
});
|
||||
|
||||
const resolvePromptList = (payload: any): PromptItem[] => {
|
||||
if (Array.isArray(payload?.data?.list)) return payload.data.list;
|
||||
if (Array.isArray(payload?.list)) return payload.list;
|
||||
if (Array.isArray(payload?.rows)) return payload.rows;
|
||||
return [];
|
||||
};
|
||||
|
||||
const resolvePromptTotal = (payload: any, list: PromptItem[]) => {
|
||||
const totalCandidates = [payload?.data?.total, payload?.total, payload?.data?.count, payload?.count];
|
||||
const validTotal = totalCandidates.find((item) => typeof item === 'number' && !Number.isNaN(item));
|
||||
if (typeof validTotal === 'number') return validTotal;
|
||||
return list.length;
|
||||
};
|
||||
|
||||
const fetchPromptList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
pageNum: pagination.pageNum,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchParams.keyword || undefined,
|
||||
nodeType: props.nodeType || undefined,
|
||||
};
|
||||
const res = await getPromptList(params);
|
||||
const list = resolvePromptList(res);
|
||||
promptList.value = list;
|
||||
pagination.total = resolvePromptTotal(res, list);
|
||||
} catch (error) {
|
||||
promptList.value = [];
|
||||
pagination.total = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.pageNum = 1;
|
||||
fetchPromptList();
|
||||
};
|
||||
|
||||
const handlePageChange = () => {
|
||||
fetchPromptList();
|
||||
};
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size;
|
||||
pagination.pageNum = 1;
|
||||
fetchPromptList();
|
||||
};
|
||||
|
||||
const handleSelectPrompt = (prompt: PromptItem) => {
|
||||
selectedPrompt.value = prompt;
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!selectedPrompt.value) return;
|
||||
emit('confirm', selectedPrompt.value);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
selectedPrompt.value = null;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.prompt-selector-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.prompt-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-bar :deep(.el-input) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.prompt-content {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.pagination-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.pagination-summary {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pagination-panel :deep(.el-pagination) {
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -240,18 +240,54 @@ const handleCreatePrivateModelAndSetChat = async () => {
|
||||
|
||||
// 基于内置模型创建新模型(继承原模型的所有配置,只替换 apiKey)
|
||||
const builtInModel = builtInModelToClone.value;
|
||||
|
||||
// Parse headMsg to Record<string, string> - it might be stored as string or already as object
|
||||
let headMsgRecord: Record<string, string> = {};
|
||||
if (builtInModel.headMsg && typeof builtInModel.headMsg === 'string') {
|
||||
// Try to parse as JSON first (new format stored as string)
|
||||
try {
|
||||
const parsed = JSON.parse(builtInModel.headMsg);
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
Object.entries(parsed).forEach(([k, v]) => {
|
||||
headMsgRecord[k] = String(v);
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// If JSON parse fails, parse as old format "key1:value1,key2:value2"
|
||||
const pairs = builtInModel.headMsg.split(',');
|
||||
pairs.forEach((pair: string) => {
|
||||
const idx = pair.indexOf(':');
|
||||
if (idx === -1) return;
|
||||
const key = pair.slice(0, idx).trim();
|
||||
const value = pair.slice(idx + 1).trim();
|
||||
if (key) {
|
||||
headMsgRecord[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (builtInModel.headMsg && typeof builtInModel.headMsg === 'object' && !Array.isArray(builtInModel.headMsg)) {
|
||||
// Already an object
|
||||
Object.entries(builtInModel.headMsg).forEach(([k, v]) => {
|
||||
headMsgRecord[k] = String(v);
|
||||
});
|
||||
}
|
||||
|
||||
const createParams = {
|
||||
modelName: apiKeyForm.modelName,
|
||||
modelType: builtInModel.modelType,
|
||||
baseUrl: builtInModel.baseUrl,
|
||||
httpMethod: builtInModel.httpMethod || 'POST',
|
||||
headMsg: builtInModel.headMsg || '',
|
||||
headMsg: headMsgRecord,
|
||||
isPrivate: builtInModel.isPrivate ?? 1,
|
||||
enabled: builtInModel.enabled ?? 1,
|
||||
isChatModel: 1, // 设置为会话模型
|
||||
callMode: builtInModel.callMode ?? builtInModel.isAsync ?? 0,
|
||||
apiKey: apiKeyForm.apiKey,
|
||||
form: builtInModel.form || {},
|
||||
requestMapping: builtInModel.requestMapping || {},
|
||||
requiredFields: Array.isArray(builtInModel.requiredFields) ? builtInModel.requiredFields : [],
|
||||
firstFrame: String(builtInModel.firstFrame || ''),
|
||||
lastFrame: String(builtInModel.lastFrame || ''),
|
||||
responseMapping: builtInModel.responseMapping || {},
|
||||
responseBody: builtInModel.responseBody || {},
|
||||
tokenMapping: builtInModel.tokenMapping || '',
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
<template>
|
||||
<div class="system-model-type-container layout-padding">
|
||||
<el-card shadow="hover" class="layout-padding-auto">
|
||||
<div class="system-model-type-search mb15">
|
||||
<el-input v-model="state.tableData.param.keyword" size="default" placeholder="请输入模型类型名称" style="max-width: 180px" clearable> </el-input>
|
||||
<el-button size="default" type="primary" class="ml10" @click="getTableData">
|
||||
<el-icon>
|
||||
<ele-Search />
|
||||
</el-icon>
|
||||
查询
|
||||
</el-button>
|
||||
<el-button size="default" type="success" class="ml10" @click="onOpenAddType('add')">
|
||||
<el-icon>
|
||||
<ele-FolderAdd />
|
||||
</el-icon>
|
||||
新增模型类型
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="state.tableData.data" v-loading="state.tableData.loading" style="width: 100%">
|
||||
<el-table-column type="index" label="序号" width="60" />
|
||||
<el-table-column prop="typeName" label="类型名称" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="typeCode" label="类型编码" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="description" label="描述" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">{{ scope.row.status === 1 ? '启用' : '禁用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" show-overflow-tooltip></el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="scope">
|
||||
<el-button size="small" text type="primary" @click="onOpenEditType('edit', scope.row)">修改</el-button>
|
||||
<el-button size="small" text type="primary" @click="onRowDel(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
@size-change="onHandleSizeChange"
|
||||
@current-change="onHandleCurrentChange"
|
||||
class="mt15"
|
||||
:pager-count="5"
|
||||
:page-sizes="[10, 20, 30]"
|
||||
v-model:current-page="state.tableData.param.pageNum"
|
||||
background
|
||||
v-model:page-size="state.tableData.param.pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="state.tableData.total"
|
||||
>
|
||||
</el-pagination>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="digitalHumanModelType">
|
||||
import { reactive, onMounted } from 'vue';
|
||||
import { ElMessageBox, ElMessage } from 'element-plus';
|
||||
|
||||
const state = reactive({
|
||||
tableData: {
|
||||
data: [],
|
||||
total: 0,
|
||||
loading: false,
|
||||
param: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
keyword: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 初始化表格数据
|
||||
const getTableData = () => {
|
||||
state.tableData.loading = true;
|
||||
// TODO: 调用API获取数据
|
||||
setTimeout(() => {
|
||||
state.tableData.data = [];
|
||||
state.tableData.total = 0;
|
||||
state.tableData.loading = false;
|
||||
}, 500);
|
||||
};
|
||||
|
||||
// 打开新增模型类型弹窗
|
||||
const onOpenAddType = (type: string) => {
|
||||
ElMessage.info('功能开发中...');
|
||||
};
|
||||
|
||||
// 打开修改模型类型弹窗
|
||||
const onOpenEditType = (type: string, row: any) => {
|
||||
ElMessage.info('功能开发中...');
|
||||
};
|
||||
|
||||
// 删除模型类型
|
||||
const onRowDel = (row: any) => {
|
||||
ElMessage.info('功能开发中...');
|
||||
};
|
||||
|
||||
// 分页改变
|
||||
const onHandleSizeChange = (val: number) => {
|
||||
state.tableData.param.pageSize = val;
|
||||
getTableData();
|
||||
};
|
||||
|
||||
// 分页改变
|
||||
const onHandleCurrentChange = (val: number) => {
|
||||
state.tableData.param.pageNum = val;
|
||||
getTableData();
|
||||
};
|
||||
|
||||
// 页面加载时
|
||||
onMounted(() => {
|
||||
getTableData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.system-model-type-container {
|
||||
:deep(.el-card__body) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
.el-table {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -43,11 +43,6 @@
|
||||
<span class="ml10 text-muted">天(0表示永不过期)</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="禁止重复使用次数" prop="historyLimit">
|
||||
<el-input-number v-model="ruleForm.historyLimit" :min="0" :max="24" placeholder="请输入禁止重复使用次数" />
|
||||
<span class="ml10 text-muted">次(0表示不限制)</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="登录失败锁定次数" prop="maxRetryCount">
|
||||
<el-input-number v-model="ruleForm.maxRetryCount" :min="0" :max="10" placeholder="请输入登录失败锁定次数" />
|
||||
<span class="ml10 text-muted">次(0表示不锁定)</span>
|
||||
@@ -85,7 +80,6 @@ interface RuleFormState {
|
||||
requireDigit: boolean;
|
||||
requireSpecialChar: boolean;
|
||||
expireDays: number;
|
||||
historyLimit: number;
|
||||
maxRetryCount: number;
|
||||
lockTimeMinutes: number;
|
||||
remark: string;
|
||||
@@ -112,7 +106,6 @@ export default defineComponent({
|
||||
requireDigit: true,
|
||||
requireSpecialChar: false,
|
||||
expireDays: 90,
|
||||
historyLimit: 5,
|
||||
maxRetryCount: 5,
|
||||
lockTimeMinutes: 30,
|
||||
remark: '',
|
||||
@@ -127,9 +120,6 @@ export default defineComponent({
|
||||
expireDays: [
|
||||
{ required: true, message: '请输入密码过期天数', trigger: 'blur' }
|
||||
],
|
||||
historyLimit: [
|
||||
{ required: true, message: '请输入禁止重复使用次数', trigger: 'blur' }
|
||||
],
|
||||
maxRetryCount: [
|
||||
{ required: true, message: '请输入登录失败锁定次数', trigger: 'blur' }
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user