This commit is contained in:
Archer
2023-12-18 16:24:50 +08:00
committed by GitHub
parent d33c99f564
commit 703583fff7
130 changed files with 3418 additions and 2579 deletions

View File

@@ -1,57 +1,81 @@
# Install dependencies only when needed
FROM node:18.15-alpine AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat && npm install -g pnpm
# --------- install dependence -----------
FROM node:18.17-alpine AS mainDeps
WORKDIR /app
ARG name
ARG proxy
RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk add --no-cache libc6-compat && npm install -g pnpm@8.6.0
# if proxy exists, set proxy
RUN [ -z "$proxy" ] || pnpm config set registry https://registry.npm.taobao.org
# copy packages and one project
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
COPY ./packages ./packages
COPY ./projects/$name/package.json ./projects/$name/package.json
RUN [ -f pnpm-lock.yaml ] || (echo "Lockfile not found." && exit 1)
RUN pnpm install
RUN pnpm i
# Rebuild the source code only when needed
FROM node:18.15-alpine AS builder
# --------- install dependence -----------
FROM node:18.17-alpine AS workerDeps
WORKDIR /app
ARG proxy
RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk add --no-cache libc6-compat && npm install -g pnpm@8.6.0
# if proxy exists, set proxy
RUN [ -z "$proxy" ] || pnpm config set registry https://registry.npm.taobao.org
COPY ./worker /app/worker
RUN cd /app/worker && pnpm i --production --ignore-workspace
# --------- builder -----------
FROM node:18.17-alpine AS builder
WORKDIR /app
ARG name
ARG proxy
# copy common node_modules and one project node_modules
COPY package.json pnpm-workspace.yaml ./
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/packages ./packages
COPY --from=mainDeps /app/node_modules ./node_modules
COPY --from=mainDeps /app/packages ./packages
COPY ./projects/$name ./projects/$name
COPY --from=deps /app/projects/$name/node_modules ./projects/$name/node_modules
COPY --from=mainDeps /app/projects/$name/node_modules ./projects/$name/node_modules
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm install -g pnpm
RUN pnpm --filter=$name run build
RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
FROM node:18.15-alpine AS runner
RUN apk add --no-cache libc6-compat && npm install -g pnpm@8.6.0
RUN pnpm --filter=$name build
# --------- runner -----------
FROM node:18.17-alpine AS runner
WORKDIR /app
ARG name
ARG proxy
# create user and use it
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN [ -z "$proxy" ] || sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
RUN apk add --no-cache curl ca-certificates \
&& update-ca-certificates
# copy running files
COPY --from=builder /app/projects/$name/public ./projects/$name/public
COPY --from=builder /app/projects/$name/next.config.js ./projects/$name/next.config.js
COPY --from=builder --chown=nextjs:nodejs /app/projects/$name/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/projects/$name/.next/static ./projects/$name/.next/static
COPY --from=builder /app/projects/$name/public /app/projects/$name/public
COPY --from=builder /app/projects/$name/next.config.js /app/projects/$name/next.config.js
COPY --from=builder --chown=nextjs:nodejs /app/projects/$name/.next/standalone /app/
COPY --from=builder --chown=nextjs:nodejs /app/projects/$name/.next/static /app/projects/$name/.next/static
# copy package.json to version file
COPY --from=builder /app/projects/$name/package.json ./package.json
# copy woker
COPY --from=workerDeps /app/worker /app/worker
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 599 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 162 KiB

View File

@@ -0,0 +1,35 @@
---
title: "自定义反馈"
description: "自定义反馈模块介绍"
icon: "feedback"
draft: false
toc: true
weight: 354
---
该模块为临时模块,后续会针对该模块进行更全面的设计。
## 特点
- 可重复添加
- 无外部输入
- 自动执行
| | |
| --------------------- | --------------------- |
| ![](/imgs/customfeedback1.png) | ![](/imgs/customfeedback2.png) |
| ![](/imgs/customfeedback3.png) | ![](/imgs/customfeedback4.png) |
## 介绍
自定义反馈模块,可以为你的对话增加一个反馈标记,从而方便在后台更好的分析对话的数据。
在调试模式下,不会记录反馈内容,而是直接提示: `自动反馈测试: 反馈内容`
在对话模式(对话、分享窗口、带 chatId 的 API 调用会将反馈内容记录到对话日志中。会延迟60s记录
## 作用
自定义反馈模块的功能类似于程序开发的`埋点`,便于你观测的对话中的数据。

View File

@@ -1,19 +0,0 @@
---
title: "历史记录"
description: "FastGPT 历史记录模块介绍"
icon: "history"
draft: false
toc: true
weight: 354
---
# 特点
- 可重复添加(防止复杂编排时线太乱,重复添加可以更美观)
- 无外部输入
- 流程入口
- 自动执行
每次对话时,会从数据库取最多 n 条聊天记录作为上下文。注意,不是指本轮对话最多 n 条上下文,本轮对话还包括:提示词、限定词、引用内容和问题。
![](/imgs/history.png)

View File

@@ -1,5 +1,5 @@
---
title: "HTTP 模块"
title: "HTTP 模块"
description: "FastGPT HTTP 模块介绍"
icon: "http"
draft: false
@@ -19,86 +19,233 @@ weight: 355
## 介绍
HTTP 模块会向对应的地址发送一个 POST 请求Body 中携带 JSON 类型的参数,具体的参数可自定义),并接收一个 JSON 响应值,字段也是自定义。如上图中,我们定义了一个入参:「提取的字段」(定义的 key 为 appointment类型为 string和一个出参「提取结果」定义的 key 为 response类型为 string
HTTP 模块会向对应的地址发送一个 `POST/GET` 请求,携带部分`系统参数``自定义参数`,并接收一个 JSON 响应值,字段也是自定义
那么,这个请求的命令为:
- 你还可以通过 JSON 传入自定义的请求头。
- POST 请求中,数据会被放置在 `body` 中。
- GET 请求中,数据会被放置在 `query` 中。
- 在出入参数中,你都可以通过 xxx.xxx 来代表嵌套的对象。
```bash
curl --location --request POST 'https://xxxx.laf.dev/appointment-lab' \
--header 'Content-Type: application/json' \
--data-raw '{
"appointment":"{\"name\":\"小明\",\"time\":\"2023/08/16 15:00\",\"labname\":\"子良A323\"}"
}'
```
## 参数结构
响应为:
### 系统参数说明
- appId: 应用的ID
- chatId: 当前对话的ID测试模式下不存在。
- responseChatItemId: 当前对话中响应的消息ID测试模式下不存在。
- variables: 当前对话的全局变量。
- data: 自定义传递的参数。
### 嵌套对象使用
**入参**
假设我们设计了`3个`输入。
- user.name (string)
- user.age (number)
- type (string)
最终组成的对象为:
```json
{
"response": "您已经有一个预约记录了,每人仅能同时预约一个实验室:\n 姓名:小明\n 时间: 2023/08/15 15:00\n 实验室: 子良A323\n "
"user": {
"name": "",
"age": ""
},
"type": ""
}
```
**出参**
假设接口的输出结构为:
```json
{
"message": "测试",
"data":{
"name": "name",
"age": 10
}
}
```
那么,自定出参的`key`可以设置为:
- message (string)
- data.name (string)
- data.age (number)
## POST 示例
**自定义入参**
- user.name (string)
- user.age (number)
- type (string)
**自定义出参**
- message (string)
- data.name (string)
- data.age (number)
那么,这个模块发出的请求则是:
{{< tabs tabTotal="2" >}}
{{< tab tabName="POST 请求示例" >}}
{{< markdownify >}}
```bash
curl --location --request POST 'http://xxxx.com' \
--header 'Content-Type: application/json' \
--data-raw '{
"appId": "65782f7ffae5f7854ed4498b",
"chatId": "xxxx",
"responseChatItemId": "xxxx",
"variables": {
"cTime": "2023-12-18 13:45:46"
},
"data": {
"user": {
"name": "",
"age": ""
},
"type": ""
}
}'
```
{{< /markdownify >}}
{{< /tab >}}
{{< tab tabName="POST响应" >}}
{{< markdownify >}}
```json
{
"message": "message",
"data": {
"name": "name",
"age": 10
}
}
```
{{< /markdownify >}}
{{< /tab >}}
{{< /tabs >}}
## GET 示例
GET 中不推荐使用嵌套参数否则会出现奇怪的问题。此外GET 请求中FastGPT 会将参数扁平化,不会将自定义参单独抽到 data 中,同时全局变量也会扁平化,因此需要注意字段 key 是否冲突。
**自定义入参**
- name (string)
- age (number)
- type (string)
**自定义出参**
- message (string)
- name (string)
- age (number)
那么,这个模块发出的请求则是:
{{< tabs tabTotal="2" >}}
{{< tab tabName="GET 请求示例" >}}
{{< markdownify >}}
```bash
curl --location --request GET 'http://xxx.com/test?name&age&type&appId=65782f7ffae5f7854ed4498b&chatId=xxxx&responseChatItemId=xxxx&cTime=2023-12-18 13:45:46'
```
{{< /markdownify >}}
{{< /tab >}}
{{< tab tabName="GET 响应" >}}
{{< markdownify >}}
```json
{
"message": "message",
"data": {
"name": "name",
"age": 10
}
}
```
{{< /markdownify >}}
{{< /tab >}}
{{< /tabs >}}
## laf 对接 HTTP 示例
{{% alert context="warning" %}}
如果你不想额外部署服务,可以使用 [Laf](https://laf.dev/) 来快速开发上线接口,即写即发,无需部署。
下面是在 Laf 上编写的一个请求示例:
{{% /alert %}}
下面是在 Laf 编写的 POST 请求示例:
```ts
import cloud from '@lafjs/cloud';
const db = cloud.database();
import cloud from '@lafjs/cloud'
const db = cloud.database()
type RequestType = {
appId: string;
data: {
appointment: string;
action: 'post' | 'delete' | 'put' | 'get'
}
}
export default async function (ctx: FunctionContext) {
const { appointment } = ctx.body;
const { name, time, labname } = JSON.parse(appointment);
try {
// 从 body 中获取参数
const { appId, data: { appointment, action } } = ctx.body as RequestType
const parseBody = JSON.parse(appointment)
if (action === 'get') {
return await getRecord(parseBody)
}
if (action === 'post') {
return await createRecord(parseBody)
}
if (action === 'put') {
return await putRecord(parseBody)
}
if (action === 'delete') {
return await removeRecord(parseBody)
}
const missData = [];
if (!name) missData.push('你的姓名');
if (!time) missData.push('需要预约的时间');
if (!labname) missData.push('实验室名称');
if (missData.length > 0) {
return {
response: `请提供: ${missData.join('、')}`
};
}
const { data: record } = await db
.collection('LabAppointment')
.where({
name,
status: 'unStart'
})
.getOne();
if (record) {
response: "异常"
}
} catch (err) {
return {
response: `您已经有一个预约记录了,每人仅能同时预约一个实验室:
姓名:${record.name}
时间: ${record.time}
实验室: ${record.labname}
`
};
response: "异常"
}
}
await db.collection('LabAppointment').add({
name,
time,
labname,
status: 'unStart'
});
return {
response: `预约成功。
姓名:${name}
时间: ${time}
实验室: ${labname}
`
};
}
```
## 作用
基于 HTTP 模块可以无限扩展,比如操作数据库、执行联网搜索、发送邮箱等等。如果你有有趣的案例,欢迎提交 PR 到 [编排案例](/docs/workflow/examples)
通过 HTTP 模块可以无限扩展,比如
- 操作数据库
- 调用外部数据源
- 执行联网搜索
- 发送邮箱
- ....
## 相关示例
- [谷歌搜索](/docs/workflow/examples/google_search/)
- [实验室预约(操作数据库)](/docs/workflow/examples/lab_appointment/)

View File

@@ -5,27 +5,24 @@
"scripts": {
"prepare": "husky install",
"format-code": "prettier --config \"./.prettierrc.js\" --write \"./**/src/**/*.{ts,tsx,scss}\"",
"format-doc": "zhlint --dir ./docSite *.md --fix"
"format-doc": "zhlint --dir ./docSite *.md --fix",
"postinstall": "sh ./scripts/postinstall.sh"
},
"devDependencies": {
"@types/multer": "^1.4.10",
"husky": "^8.0.3",
"i18next": "^22.5.1",
"lint-staged": "^13.2.1",
"next-i18next": "^13.3.0",
"prettier": "^3.0.3",
"react-i18next": "^12.3.1",
"zhlint": "^0.7.1"
"zhlint": "^0.7.1",
"i18next": "^22.5.1",
"next-i18next": "^13.3.0",
"react-i18next": "^12.3.1"
},
"lint-staged": {
"./**/**/*.{ts,tsx,scss}": "npm run format-code",
"./**/**/*.md": "npm run format-doc"
},
"engines": {
"node": ">=18.0.0"
},
"dependencies": {
"multer": "1.4.5-lts.1",
"openai": "4.16.1"
"node": ">=18.0.0",
"pnpm": ">=8.6.0"
}
}

View File

@@ -1,8 +1,3 @@
import axios from 'axios';
import { UrlFetchParams, UrlFetchResponse } from './api.d';
import { htmlToMarkdown } from '../string/markdown';
import * as cheerio from 'cheerio';
export const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B';
@@ -12,91 +7,3 @@ export const formatFileSize = (bytes: number): string => {
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
export const cheerioToHtml = ({
fetchUrl,
$,
selector
}: {
fetchUrl: string;
$: cheerio.CheerioAPI;
selector?: string;
}) => {
// get origin url
const originUrl = new URL(fetchUrl).origin;
// remove i element
$('i,script').remove();
// remove empty a element
$('a')
.filter((i, el) => {
return $(el).text().trim() === '' && $(el).children().length === 0;
})
.remove();
// if link,img startWith /, add origin url
$('a').each((i, el) => {
const href = $(el).attr('href');
if (href && href.startsWith('/')) {
$(el).attr('href', originUrl + href);
}
});
$('img').each((i, el) => {
const src = $(el).attr('src');
if (src && src.startsWith('/')) {
$(el).attr('src', originUrl + src);
}
});
const html = $(selector || 'body')
.map((item, dom) => {
return $(dom).html();
})
.get()
.join('\n');
return html;
};
export const urlsFetch = async ({
urlList,
selector
}: UrlFetchParams): Promise<UrlFetchResponse> => {
urlList = urlList.filter((url) => /^(http|https):\/\/[^ "]+$/.test(url));
const response = (
await Promise.all(
urlList.map(async (url) => {
try {
const fetchRes = await axios.get(url, {
timeout: 30000
});
const $ = cheerio.load(fetchRes.data);
const md = htmlToMarkdown(
cheerioToHtml({
fetchUrl: url,
$,
selector
})
);
return {
url,
content: md
};
} catch (error) {
console.log(error, 'fetch error');
return {
url,
content: ''
};
}
})
)
).filter((item) => item.content);
return response;
};

View File

@@ -1,5 +1,4 @@
import { simpleText } from './tools';
import { NodeHtmlMarkdown } from 'node-html-markdown';
/* Delete redundant text in markdown */
export const simpleMarkdownText = (rawText: string) => {
@@ -27,75 +26,11 @@ export const simpleMarkdownText = (rawText: string) => {
// Remove headings and code blocks front spaces
['####', '###', '##', '#', '```', '~~~'].forEach((item, i) => {
const isMarkdown = i <= 3;
const reg = new RegExp(`\\n\\s*${item}`, 'g');
if (reg.test(rawText)) {
rawText = rawText.replace(
new RegExp(`(\\n)\\s*(${item})`, 'g'),
isMarkdown ? '\n$1$2' : '$1$2'
);
rawText = rawText.replace(new RegExp(`(\\n)\\s*(${item})`, 'g'), '$1$2');
}
});
return rawText.trim();
};
/* html string to markdown */
export const htmlToMarkdown = (html?: string | null) => {
if (!html) return '';
const surround = (source: string, surroundStr: string) => `${surroundStr}${source}${surroundStr}`;
const nhm = new NodeHtmlMarkdown(
{
codeFence: '```',
codeBlockStyle: 'fenced',
ignore: ['i', 'script']
},
{
code: ({ node, parent, options: { codeFence, codeBlockStyle }, visitor }) => {
const isCodeBlock = ['PRE', 'WRAPPED-PRE'].includes(parent?.tagName!);
if (!isCodeBlock) {
return {
spaceIfRepeatingChar: true,
noEscape: true,
postprocess: ({ content }) => {
// Find longest occurring sequence of running backticks and add one more (so content is escaped)
const delimiter =
'`' + (content.match(/`+/g)?.sort((a, b) => b.length - a.length)?.[0] || '');
const padding = delimiter.length > 1 ? ' ' : '';
return surround(surround(content, padding), delimiter);
}
};
}
/* Handle code block */
if (codeBlockStyle === 'fenced') {
const language =
node.getAttribute('class')?.match(/language-(\S+)/)?.[1] ||
parent?.getAttribute('class')?.match(/language-(\S+)/)?.[1] ||
'';
return {
noEscape: true,
prefix: `${codeFence}${language}\n`,
postfix: `\n${codeFence}\n`,
childTranslators: visitor.instance.codeBlockTranslators
};
}
return {
noEscape: true,
postprocess: ({ content }) => content.replace(/^/gm, ' '),
childTranslators: visitor.instance.codeBlockTranslators
};
}
}
);
const markdown = nhm.translate(html).trim();
return simpleMarkdownText(markdown);
};

View File

@@ -13,12 +13,13 @@ export const splitText2Chunks = (props: {
chunkLen: number;
overlapRatio?: number;
customReg?: string[];
countTokens?: boolean;
}): {
chunks: string[];
tokens: number;
overlapRatio?: number;
} => {
let { text = '', chunkLen, overlapRatio = 0.2, customReg = [] } = props;
let { text = '', chunkLen, overlapRatio = 0.2, customReg = [], countTokens = true } = props;
const splitMarker = 'SPLIT_HERE_SPLIT_HERE';
const codeBlockMarker = 'CODE_BLOCK_LINE_MARKER';
const overlapLen = Math.round(chunkLen * overlapRatio);
@@ -233,7 +234,9 @@ export const splitText2Chunks = (props: {
mdTitle: ''
}).map((chunk) => chunk.replaceAll(codeBlockMarker, '\n')); // restore code block
const tokens = chunks.reduce((sum, chunk) => sum + countPromptTokens(chunk, 'system'), 0);
const tokens = countTokens
? chunks.reduce((sum, chunk) => sum + countPromptTokens(chunk, 'system'), 0)
: 0;
return {
chunks,

View File

@@ -40,7 +40,7 @@ export type ChatItemSchema = {
value: string;
userGoodFeedback?: string;
userBadFeedback?: string;
robotBadFeedback?: string;
customFeedbacks?: string[];
adminFeedback?: AdminFbkType;
[ModuleOutputKeyEnum.responseData]?: ChatHistoryItemResType[];
};
@@ -60,6 +60,7 @@ export type ChatItemType = {
value: any;
userGoodFeedback?: string;
userBadFeedback?: string;
customFeedbacks?: ChatItemSchema['customFeedbacks'];
adminFeedback?: ChatItemSchema['feedback'];
[ModuleOutputKeyEnum.responseData]?: ChatHistoryItemResType[];
};

View File

@@ -5,12 +5,14 @@ export type SelectedDatasetType = { datasetId: string; vectorModel: VectorModelI
export type HttpBodyType<T = any> = {
appId: string;
chatId?: string;
responseChatItemId?: string;
variables: Record<string, any>;
data: T;
};
export type HttpQueryType = {
appId: string;
chatId?: string;
responseChatItemId?: string;
variables: Record<string, any>;
[key: string]: any;
};

View File

@@ -23,5 +23,6 @@ export type PluginTemplateType = {
name: string;
avatar: string;
intro: string;
showStatus?: boolean;
modules: ModuleItemType[];
};

View File

@@ -2,17 +2,14 @@
"name": "@fastgpt/global",
"version": "1.0.0",
"dependencies": {
"axios": "^1.5.1",
"cheerio": "1.0.0-rc.12",
"dayjs": "^1.11.7",
"openai": "4.23.0",
"encoding": "^0.1.13",
"js-tiktoken": "^1.0.7",
"node-html-markdown": "^1.3.0",
"openai": "^4.20.1",
"axios": "^1.5.1",
"timezones-list": "^3.0.2"
},
"devDependencies": {
"@types/node": "^20.8.5",
"@types/turndown": "^5.0.4"
"@types/node": "^20.8.5"
}
}

View File

@@ -3,6 +3,8 @@
"version": "1.0.0",
"dependencies": {},
"devDependencies": {
"@types/node": "^20.8.5"
"@types/node": "^20.8.5",
"@fastgpt/global": "workspace:*",
"@fastgpt/service": "workspace:*"
}
}

View File

@@ -71,7 +71,7 @@ instance.interceptors.response.use(responseSuccess, (err) => Promise.reject(err)
export function request(url: string, data: any, config: ConfigType, method: Method): any {
if (!global.systemEnv || !global.systemEnv?.pluginBaseUrl) {
console.log('未部署商业版接口');
console.log('未部署商业版接口', url);
return Promise.reject('The The request was denied...');
}

View File

@@ -0,0 +1,95 @@
import { UrlFetchParams, UrlFetchResponse } from '@fastgpt/global/common/file/api';
import * as cheerio from 'cheerio';
import axios from 'axios';
import { htmlToMarkdown } from './markdown';
export const cheerioToHtml = ({
fetchUrl,
$,
selector
}: {
fetchUrl: string;
$: cheerio.CheerioAPI;
selector?: string;
}) => {
// get origin url
const originUrl = new URL(fetchUrl).origin;
const selectDom = $(selector || 'body');
// remove i element
selectDom.find('i,script').remove();
// remove empty a element
selectDom
.find('a')
.filter((i, el) => {
return $(el).text().trim() === '' && $(el).children().length === 0;
})
.remove();
// if link,img startWith /, add origin url
selectDom.find('a').each((i, el) => {
const href = $(el).attr('href');
if (href && href.startsWith('/')) {
$(el).attr('href', originUrl + href);
}
});
selectDom.find('img').each((i, el) => {
const src = $(el).attr('src');
if (src && src.startsWith('/')) {
$(el).attr('src', originUrl + src);
}
});
const html = selectDom
.map((item, dom) => {
return $(dom).html();
})
.get()
.join('\n');
return html;
};
export const urlsFetch = async ({
urlList,
selector
}: UrlFetchParams): Promise<UrlFetchResponse> => {
urlList = urlList.filter((url) => /^(http|https):\/\/[^ "]+$/.test(url));
const response = (
await Promise.all(
urlList.map(async (url) => {
try {
const fetchRes = await axios.get(url, {
timeout: 30000
});
const $ = cheerio.load(fetchRes.data);
const md = await htmlToMarkdown(
cheerioToHtml({
fetchUrl: url,
$,
selector
})
);
return {
url,
content: md
};
} catch (error) {
console.log(error, 'fetch error');
return {
url,
content: ''
};
}
})
)
).filter((item) => item.content);
return response;
};

View File

@@ -0,0 +1,23 @@
import { simpleMarkdownText } from '@fastgpt/global/common/string/markdown';
import { Worker } from 'worker_threads';
import { getWorkerPath } from './utils';
/* html string to markdown */
export const htmlToMarkdown = (html?: string | null) =>
new Promise<string>((resolve, reject) => {
if (!html) return resolve('');
const start = Date.now();
// worker
const worker = new Worker(getWorkerPath('html2md'));
worker.on('message', (md: string) => {
resolve(simpleMarkdownText(md));
});
worker.on('error', (err) => {
reject(err);
});
worker.postMessage(html);
});

View File

@@ -0,0 +1,9 @@
export const getWorkerPath = (name: string) => {
// @ts-ignore
const isSubModule = !!global?.systemConfig;
const isProd = process.env.NODE_ENV === 'production';
return isProd
? `/app/worker/${name}.js`
: `../../${isSubModule ? 'FastGPT/' : ''}/worker/${name}.js`;
};

View File

@@ -61,8 +61,8 @@ const ChatItemSchema = new Schema({
userBadFeedback: {
type: String
},
robotBadFeedback: {
type: String
customFeedbacks: {
type: [String]
},
adminFeedback: {
type: {
@@ -86,7 +86,7 @@ try {
ChatItemSchema.index({ chatId: 1 });
ChatItemSchema.index({ userGoodFeedback: 1 });
ChatItemSchema.index({ userBadFeedback: 1 });
ChatItemSchema.index({ robotBadFeedback: 1 });
ChatItemSchema.index({ customFeedbacks: 1 });
ChatItemSchema.index({ adminFeedback: 1 });
} catch (error) {
console.log(error);

View File

@@ -1,5 +1,6 @@
import type { ChatItemType } from '@fastgpt/global/core/chat/type';
import { MongoChatItem } from './chatItemSchema';
import { addLog } from '../../common/system/log';
export async function getChatItems({
chatId,
@@ -20,3 +21,29 @@ export async function getChatItems({
return { history };
}
export const addCustomFeedbacks = async ({
chatId,
chatItemId,
feedbacks
}: {
chatId?: string;
chatItemId?: string;
feedbacks: string[];
}) => {
if (!chatId || !chatItemId) return;
try {
await MongoChatItem.findOneAndUpdate(
{
chatId,
dataId: chatItemId
},
{
$push: { customFeedbacks: { $each: feedbacks } }
}
);
} catch (error) {
addLog.error('addCustomFeedbacks error', error);
}
};

View File

@@ -4,7 +4,7 @@ import type { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
import { splitText2Chunks } from '@fastgpt/global/common/string/textSplitter';
import { MongoDatasetTraining } from '../training/schema';
import { urlsFetch } from '@fastgpt/global/common/file/tools';
import { urlsFetch } from '../../../common/string/cheerio';
import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constant';
/**
@@ -105,7 +105,8 @@ export const loadingOneChunkCollection = async ({
// split data
const { chunks } = splitText2Chunks({
text: newRawText,
chunkLen: collection.chunkSize || 512
chunkLen: collection.chunkSize || 512,
countTokens: false
});
// insert to training queue

View File

@@ -44,6 +44,7 @@ const getPluginTemplateById = async (id: string): Promise<PluginTemplateType> =>
name: item.name,
avatar: item.avatar,
intro: item.intro,
showStatus: true,
source: PluginSourceEnum.personal,
modules: item.modules,
templateType: ModuleTemplateTypeEnum.personalPlugin
@@ -67,7 +68,7 @@ export async function getPluginPreviewModule({
avatar: plugin.avatar,
name: plugin.name,
intro: plugin.intro,
showStatus: true,
showStatus: plugin.showStatus,
...plugin2ModuleIO(plugin.id, plugin.modules)
};
}

View File

@@ -3,22 +3,24 @@
"version": "1.0.0",
"dependencies": {
"@fastgpt/global": "workspace:*",
"axios": "^1.5.1",
"cookie": "^0.5.0",
"encoding": "^0.1.13",
"jsonwebtoken": "^9.0.2",
"mongoose": "^7.0.2",
"nanoid": "^4.0.1",
"dayjs": "^1.11.7",
"next": "13.5.2",
"multer": "1.4.5-lts.1",
"axios": "^1.5.1",
"cheerio": "1.0.0-rc.12",
"nextjs-cors": "^2.1.2",
"pg": "^8.10.0",
"tunnel": "^0.0.6",
"dayjs": "^1.11.7"
"tunnel": "^0.0.6"
},
"devDependencies": {
"@types/cookie": "^0.5.2",
"@types/jsonwebtoken": "^9.0.3",
"@types/node": "^20.8.5",
"@types/multer": "^1.4.10",
"@types/pg": "^8.6.6",
"@types/tunnel": "^0.0.4"
}

View File

@@ -31,3 +31,11 @@ export async function authCertOrShareId({
canWrite: false
};
}
/* auth the request from local service */
export const authRequestFromLocal = ({ req }: AuthModeType) => {
const host = `${process.env.HOSTNAME || 'localhost'}:${process.env.PORT || 3000}`;
if (host !== req.headers.host) {
return Promise.reject('Invalid request');
}
};

View File

@@ -1,8 +1,6 @@
{
"name": "@fastgpt/web",
"version": "1.0.0",
"dependencies": {
"axios": "^1.5.1"
},
"dependencies": {},
"devDependencies": {}
}

3819
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,10 +6,10 @@
module.exports = {
i18n: {
defaultLocale: 'zh',
locales: ['en', 'zh', 'zh-Hans', 'zh-CN'],
localeDetection: false
locales: ['en', 'zh'],
localeDetection: true
},
localePath:
typeof window === 'undefined' ? require('path').resolve('./public/locales') : '/locales',
typeof window === 'undefined' ? require('path').resolve('./public/locales') : '/public/locales',
reloadOnPrerender: process.env.NODE_ENV === 'development'
};

View File

@@ -45,7 +45,7 @@ const nextConfig = {
},
transpilePackages: ['@fastgpt/*'],
experimental: {
serverComponentsExternalPackages: ['mongoose', 'winston', 'winston-mongodb', 'pg'],
serverComponentsExternalPackages: ['mongoose', 'pg'],
outputFileTracingRoot: path.join(__dirname, '../../')
}
};

View File

@@ -10,12 +10,13 @@
},
"dependencies": {
"@chakra-ui/anatomy": "^2.2.1",
"@chakra-ui/icons": "^2.0.17",
"@chakra-ui/react": "^2.7.0",
"@chakra-ui/icons": "^2.1.1",
"@chakra-ui/next-js": "^2.1.5",
"@chakra-ui/react": "^2.8.1",
"@chakra-ui/styled-system": "^2.9.1",
"@chakra-ui/system": "^2.5.8",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@chakra-ui/system": "^2.6.1",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@fastgpt/plugins": "workspace:*",
"@fastgpt/global": "workspace:*",
"@fastgpt/service": "workspace:*",
@@ -23,33 +24,25 @@
"@node-rs/jieba": "^1.7.2",
"@tanstack/react-query": "^4.24.10",
"@types/nprogress": "^0.2.0",
"axios": "^1.5.1",
"date-fns": "^2.30.0",
"dayjs": "^1.11.7",
"downloadjs": "^1.4.7",
"echarts": "^5.4.1",
"next": "13.5.2",
"echarts-gl": "^2.0.9",
"formidable": "^2.1.1",
"framer-motion": "^9.0.6",
"hyperdown": "^2.4.29",
"i18next": "^22.5.1",
"immer": "^9.0.19",
"jschardet": "^3.0.0",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"mammoth": "^1.6.0",
"mermaid": "^10.2.3",
"multer": "1.4.5-lts.1",
"nanoid": "^4.0.1",
"next": "13.5.2",
"next-i18next": "^13.3.0",
"nprogress": "^0.2.0",
"papaparse": "^5.4.1",
"react": "18.2.0",
"react-day-picker": "^8.7.1",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.1",
"react-i18next": "^12.3.1",
"react-markdown": "^8.0.7",
"react-syntax-highlighter": "^15.5.0",
"reactflow": "^11.7.4",
@@ -59,28 +52,28 @@
"remark-math": "^5.1.1",
"request-ip": "^3.3.0",
"sass": "^1.58.3",
"zustand": "^4.3.5"
"zustand": "^4.3.5",
"i18next": "^22.5.1",
"next-i18next": "^13.3.0",
"react-i18next": "^12.3.1",
"axios": "^1.5.1",
"nanoid": "^4.0.1",
"dayjs": "^1.11.7"
},
"devDependencies": {
"@svgr/webpack": "^6.5.1",
"@types/downloadjs": "^1.4.3",
"@types/formidable": "^2.0.5",
"@types/js-cookie": "^3.0.3",
"@types/jsonwebtoken": "^9.0.3",
"@types/lodash": "^4.14.191",
"@types/multer": "^1.4.10",
"@types/node": "^20.8.5",
"@types/papaparse": "^5.3.7",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@types/react": "18.2.0",
"@types/react-dom": "18.2.0",
"@types/react-syntax-highlighter": "^15.5.6",
"@types/request-ip": "^0.0.37",
"eslint": "8.34.0",
"eslint-config-next": "13.1.6",
"typescript": "4.9.5"
},
"engines": {
"node": ">=18.0.0",
"pnpm": ">=8.6.0"
}
}

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1702637232008" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6146" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M261.688889 194.218667A67.470222 67.470222 0 1 1 194.218667 261.688889 67.584 67.584 0 0 1 261.688889 194.218667M261.688889 136.533333a125.155556 125.155556 0 1 0 125.155555 125.155556 125.155556 125.155556 0 0 0-125.155555-125.155556zM614.4 444.529778A67.470222 67.470222 0 1 1 547.271111 512a67.584 67.584 0 0 1 67.128889-67.470222M614.4 386.844444a125.155556 125.155556 0 1 0 125.155556 125.155556 125.155556 125.155556 0 0 0-125.155556-125.155556zM408.120889 694.840889A67.470222 67.470222 0 1 1 340.650667 762.311111a67.470222 67.470222 0 0 1 67.470222-67.470222m0-57.685333a125.155556 125.155556 0 1 0 125.155555 125.155555 125.155556 125.155556 0 0 0-125.155555-125.155555z" fill="#1DCCA1" p-id="6147"></path><path d="M489.016889 736.142222h375.011555v52.337778H489.016889zM325.632 788.48h-42.894222C171.804444 788.48 113.777778 711.452444 113.777778 635.335111a143.36 143.36 0 0 1 43.235555-103.651555c30.833778-30.037333 74.296889-45.511111 125.724445-45.511112h242.119111v52.337778H282.737778c-80.554667 0-116.622222 48.810667-116.622222 97.166222s36.522667 100.807111 116.622222 100.807112h42.894222zM743.537778 538.168889h-40.618667v-52.337778h40.618667c78.620444 0 114.346667-51.541333 114.346666-99.555555s-35.384889-98.417778-114.346666-98.417778H337.123556v-52.337778h406.414222C853.333333 235.52 910.222222 311.409778 910.222222 386.844444a147.911111 147.911111 0 0 1-42.780444 104.903112 168.732444 168.732444 0 0 1-123.904 46.421333z" fill="#1DCCA1" p-id="6148"></path><path d="M910.222222 762.311111l-136.533333 102.4V659.911111l136.533333 102.4z" fill="#1DCCA1" p-id="6149"></path></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -247,6 +247,10 @@
"Save and out": "Save out",
"UnSave": "UnSave"
},
"feedback": {
"Custom feedback": "Custom feedback",
"close custom feedback": "Close Feedback"
},
"logs": {
"Source And Time": "Source & Time"
},

View File

@@ -247,6 +247,10 @@
"Save and out": "保存并退出",
"UnSave": "不保存"
},
"feedback": {
"Custom feedback": "自定义反馈",
"close custom feedback": "关闭反馈"
},
"logs": {
"Source And Time": "来源 & 时间"
},

View File

@@ -18,7 +18,8 @@ type pluginType = {
name: string;
avatar: string;
intro: string;
modules: 直接从高级编排导出配置复制过来;
showStatus?: boolean; // 是否需要展示组件运行状态
modules: []; //直接从高级编排导出配置复制过来;
};
```

View File

@@ -0,0 +1,319 @@
{
"author": "FastGPT Team",
"templateType": "other",
"name": "自定义反馈",
"avatar": "/imgs/module/customFeedback.svg",
"intro": "该模块被触发时,会给当前的对话记录增加一条反馈。可用于自动记录对话效果等。",
"showStatus": false,
"modules": [
{
"moduleId": "w90mfp",
"name": "定义插件输入",
"avatar": "/imgs/module/input.png",
"flowType": "pluginInput",
"showStatus": false,
"position": {
"x": 515.1887815471657,
"y": -169.04905809653783
},
"inputs": [
{
"key": "defaultFeedback",
"valueType": "string",
"label": "默认反馈内容",
"type": "textarea",
"required": false,
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true,
"inputType": true
},
"connected": true
},
{
"key": "customFeedback",
"valueType": "string",
"label": "自定义反馈内容",
"type": "target",
"required": false,
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true,
"inputType": true
},
"connected": true
}
],
"outputs": [
{
"key": "defaultFeedback",
"valueType": "string",
"label": "默认反馈内容",
"type": "source",
"edit": true,
"targets": [
{
"moduleId": "49de3g",
"key": "defaultFeedback"
}
]
},
{
"key": "customFeedback",
"valueType": "string",
"label": "自定义反馈内容",
"type": "source",
"edit": true,
"targets": [
{
"moduleId": "49de3g",
"key": "customFeedback"
}
]
}
]
},
{
"moduleId": "49de3g",
"name": "HTTP模块",
"avatar": "/imgs/module/http.png",
"flowType": "httpRequest",
"showStatus": true,
"position": {
"x": 1086.8929621216014,
"y": -451.7550009773506
},
"inputs": [
{
"key": "switch",
"type": "target",
"label": "core.module.input.label.switch",
"valueType": "any",
"showTargetInApp": true,
"showTargetInPlugin": true,
"connected": false
},
{
"key": "system_httpMethod",
"type": "select",
"valueType": "string",
"label": "core.module.input.label.Http Request Method",
"value": "POST",
"list": [
{
"label": "GET",
"value": "GET"
},
{
"label": "POST",
"value": "POST"
}
],
"required": true,
"showTargetInApp": false,
"showTargetInPlugin": false,
"connected": false
},
{
"key": "system_httpReqUrl",
"type": "input",
"valueType": "string",
"label": "core.module.input.label.Http Request Url",
"description": "core.module.input.description.Http Request Url",
"placeholder": "https://api.ai.com/getInventory",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"value": "/api/plugins/customFeedback",
"connected": false
},
{
"key": "system_httpHeader",
"type": "textarea",
"valueType": "string",
"label": "core.module.input.label.Http Request Header",
"description": "core.module.input.description.Http Request Header",
"placeholder": "core.module.input.description.Http Request Header",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"value": "",
"connected": false
},
{
"key": "DYNAMIC_INPUT_KEY",
"type": "target",
"valueType": "any",
"label": "core.module.inputType.dynamicTargetInput",
"description": "core.module.input.description.dynamic input",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": true,
"hideInApp": true,
"connected": false
},
{
"valueType": "string",
"label": "defaultFeedback",
"type": "target",
"required": true,
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
},
"connected": true,
"key": "defaultFeedback"
},
{
"key": "customFeedback",
"valueType": "string",
"label": "customFeedback",
"type": "target",
"required": true,
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
},
"connected": true
},
{
"key": "system_addInputParam",
"type": "addInputParam",
"valueType": "any",
"label": "",
"required": false,
"showTargetInApp": false,
"showTargetInPlugin": false,
"editField": {
"key": true,
"name": true,
"description": true,
"required": true,
"dataType": true
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"inputType": "target",
"valueType": "string",
"required": true
},
"connected": false
}
],
"outputs": [
{
"key": "finish",
"label": "core.module.output.label.running done",
"description": "core.module.output.description.running done",
"valueType": "boolean",
"type": "source",
"targets": []
},
{
"key": "system_addOutputParam",
"type": "addOutputParam",
"valueType": "any",
"label": "",
"targets": [],
"editField": {
"key": true,
"name": true,
"description": true,
"dataType": true
},
"defaultEditField": {
"label": "",
"key": "",
"description": "",
"outputType": "source",
"valueType": "string"
}
},
{
"type": "source",
"valueType": "string",
"label": "response",
"description": "",
"edit": true,
"editField": {
"key": true,
"name": true,
"description": true,
"dataType": true
},
"targets": [
{
"moduleId": "s15f3v",
"key": "text"
}
],
"key": "response"
}
]
},
{
"moduleId": "s15f3v",
"name": "指定回复",
"avatar": "/imgs/module/reply.png",
"flowType": "answerNode",
"position": {
"x": 1705.6337348182756,
"y": -37.53826066726282
},
"inputs": [
{
"key": "switch",
"type": "target",
"label": "core.module.input.label.switch",
"valueType": "any",
"showTargetInApp": true,
"showTargetInPlugin": true,
"connected": false
},
{
"key": "text",
"type": "textarea",
"valueType": "any",
"label": "回复的内容",
"description": "可以使用 \\n 来实现连续换行。\n可以通过外部模块输入实现回复外部模块输入时会覆盖当前填写的内容。\n如传入非字符串类型数据将会自动转成字符串",
"placeholder": "可以使用 \\n 来实现连续换行。\n可以通过外部模块输入实现回复外部模块输入时会覆盖当前填写的内容。\n如传入非字符串类型数据将会自动转成字符串",
"showTargetInApp": true,
"showTargetInPlugin": true,
"connected": true
}
],
"outputs": [
{
"key": "finish",
"label": "core.module.output.label.running done",
"description": "core.module.output.description.running done",
"valueType": "boolean",
"type": "source",
"targets": []
}
]
}
]
}

View File

@@ -4,6 +4,7 @@
"name": "core.module.template.textEditor",
"avatar": "/imgs/module/textEditor.svg",
"intro": "core.module.template.textEditor intro",
"showStatus": false,
"modules": [
{
"moduleId": "w90mfp",

View File

@@ -4,6 +4,7 @@
"name": "core.module.template.TFSwitch",
"avatar": "/imgs/module/tfSwitch.svg",
"intro": "core.module.template.TFSwitch intro",
"showStatus": false,
"modules": [
{
"moduleId": "w90mfp",

View File

@@ -2,7 +2,7 @@ import { useSpeech } from '@/web/common/hooks/useSpeech';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { Box, Flex, Image, Spinner, Textarea } from '@chakra-ui/react';
import React, { useRef, useEffect, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
import MyTooltip from '../MyTooltip';
import MyIcon from '../Icon';
import styles from './index.module.scss';
@@ -216,7 +216,7 @@ ${images.map((img) => JSON.stringify({ src: img.src })).join('\n')}
pl={5}
alignItems={'center'}
bg={'white'}
color={'myBlue.600'}
color={'blue.500'}
visibility={isSpeaking && isTransCription ? 'visible' : 'hidden'}
>
<Spinner size={'sm'} mr={4} />
@@ -244,7 +244,7 @@ ${images.map((img) => JSON.stringify({ src: img.src })).join('\n')}
alignItems={'center'}
justifyContent={'center'}
rounded={'md'}
color={'myBlue.600'}
color={'blue.500'}
top={0}
left={0}
bottom={0}
@@ -260,7 +260,7 @@ ${images.map((img) => JSON.stringify({ src: img.src })).join('\n')}
h={'16px'}
color={'myGray.700'}
cursor={'pointer'}
_hover={{ color: 'myBlue.600' }}
_hover={{ color: 'blue.500' }}
position={'absolute'}
bg={'white'}
right={'-8px'}
@@ -396,7 +396,7 @@ ${images.map((img) => JSON.stringify({ src: img.src })).join('\n')}
name={isSpeaking ? 'core/chat/stopSpeechFill' : 'core/chat/recordFill'}
width={['20px', '22px']}
height={['20px', '22px']}
color={'myBlue.600'}
color={'blue.500'}
/>
</MyTooltip>
</Flex>
@@ -415,7 +415,7 @@ ${images.map((img) => JSON.stringify({ src: img.src })).join('\n')}
h={['28px', '32px']}
w={['28px', '32px']}
borderRadius={'md'}
bg={isSpeaking || isChatting ? '' : !havInput ? '#E5E5E5' : 'myBlue.600'}
bg={isSpeaking || isChatting ? '' : !havInput ? '#E5E5E5' : 'blue.500'}
cursor={havInput ? 'pointer' : 'not-allowed'}
lineHeight={1}
onClick={() => {

View File

@@ -105,7 +105,7 @@ const QuoteModal = ({
className="hover-data"
display={'none'}
alignItems={'center'}
color={'myBlue.600'}
color={'blue.500'}
href={`/dataset/detail?datasetId=${item.datasetId}&currentTab=dataCard&collectionId=${item.collectionId}`}
>
{t('core.dataset.Go Dataset')}
@@ -164,7 +164,7 @@ const QuoteModal = ({
cursor={'pointer'}
color={'myGray.600'}
_hover={{
color: 'myBlue.700'
color: 'blue.600'
}}
onClick={() => onclickEdit(item)}
/>

View File

@@ -147,7 +147,7 @@ const ResponseTags = ({
name="common/routePushLight"
w={'14px'}
cursor={'pointer'}
_hover={{ color: 'myBlue.600' }}
_hover={{ color: 'blue.500' }}
onClick={async (e) => {
e.stopPropagation();

View File

@@ -35,7 +35,7 @@ const SelectMarkCollection = ({
const theme = useTheme();
const [selectedDatasetId, setSelectedDatasetId] = useState<string>();
const [selectedDatasetCollectionIds, setSelectedDatasetCollectionIds] = useState<string[]>([]);
const { paths, parentId, setParentId, datasets, isFetching } = useDatasetSelect();
const { paths, setParentId, datasets, isFetching } = useDatasetSelect();
return (
<>
@@ -70,7 +70,7 @@ const SelectMarkCollection = ({
}}
{...(selected
? {
bg: 'myBlue.300'
bg: 'blue.200'
}
: {})}
onClick={() => {

View File

@@ -27,7 +27,8 @@ import {
BoxProps,
FlexProps,
Image,
Textarea
Textarea,
Checkbox
} from '@chakra-ui/react';
import { feConfigs } from '@/web/common/system/staticData';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
@@ -43,7 +44,11 @@ import { useRouter } from 'next/router';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next';
import { customAlphabet } from 'nanoid';
import { updateChatAdminFeedback, updateChatUserFeedback } from '@/web/core/chat/api';
import {
closeCustomFeedback,
updateChatAdminFeedback,
updateChatUserFeedback
} from '@/web/core/chat/api';
import type { AdminMarkType } from './SelectMarkCollection';
import MyIcon from '@/components/Icon';
@@ -63,6 +68,7 @@ import { splitGuideModule } from '@fastgpt/global/core/module/utils';
import type { AppTTSConfigType } from '@fastgpt/global/core/module/type.d';
import MessageInput from './MessageInput';
import { ModuleOutputKeyEnum } from '@fastgpt/global/core/module/constants';
import ChatBoxDivider from '../core/chat/Divider';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 24);
@@ -492,7 +498,7 @@ const ChatBox = (
const colorMap = {
loading: 'myGray.700',
running: '#67c13b',
finish: 'myBlue.600'
finish: 'blue.500'
};
if (!isChatting) return;
const chatContent = chatHistory[chatHistory.length - 1];
@@ -660,7 +666,7 @@ const ChatBox = (
<Card
className="markdown"
{...MessageCardStyle}
bg={'myBlue.300'}
bg={'blue.200'}
borderRadius={'8px 0 8px 8px'}
textAlign={'left'}
>
@@ -853,16 +859,56 @@ const ChatBox = (
<ResponseTags responseData={item.responseData} isShare={!!shareId} />
{/* custom feedback */}
{item.customFeedbacks && item.customFeedbacks.length > 0 && (
<Box>
<ChatBoxDivider
icon={'core/app/customFeedback'}
text={t('core.app.feedback.Custom feedback')}
/>
{item.customFeedbacks.map((text, i) => (
<Box key={`${text}${i}`}>
<MyTooltip label={t('core.app.feedback.close custom feedback')}>
<Checkbox
onChange={(e) => {
if (e.target.checked && appId && chatId && item.dataId) {
closeCustomFeedback({
appId,
chatId,
chatItemId: item.dataId,
index: i
});
// update dom
setChatHistory((state) =>
state.map((chatItem) =>
chatItem.dataId === item.dataId
? {
...chatItem,
customFeedbacks: chatItem.customFeedbacks?.filter(
(item, index) => index !== i
)
}
: chatItem
)
);
}
console.log(e);
}}
>
{text}
</Checkbox>
</MyTooltip>
</Box>
))}
</Box>
)}
{/* admin mark content */}
{showMarkIcon && item.adminFeedback && (
<Box>
<Flex alignItems={'center'} py={2}>
<MyIcon name={'core/app/markLight'} w={'14px'} color={'myGray.900'} />
<Box ml={2} color={'myGray.500'}>
{t('chat.Admin Mark Content')}
</Box>
<Box h={'1px'} bg={'myGray.300'} flex={'1'} />
</Flex>
<ChatBoxDivider
icon="core/app/markLight"
text={t('chat.Admin Mark Content')}
/>
<Box whiteSpace={'pre'}>{`${item.adminFeedback.q || ''}${
item.adminFeedback.a ? `\n${item.adminFeedback.a}` : ''
}`}</Box>
@@ -942,7 +988,10 @@ const ChatBox = (
setAdminMarkData={(e) => setAdminMarkData({ ...e, chatItemId: adminMarkData.chatItemId })}
onClose={() => setAdminMarkData(undefined)}
onSuccess={(adminFeedback) => {
if (!appId || !chatId || !adminMarkData.chatItemId) return;
updateChatAdminFeedback({
appId,
chatId,
chatItemId: adminMarkData.chatItemId,
...adminFeedback
});
@@ -1089,7 +1138,7 @@ function ChatAvatar({ src, type }: { src?: string; type: 'Human' | 'AI' }) {
borderRadius={'lg'}
border={theme.borders.base}
boxShadow={'0 0 5px rgba(0,0,0,0.1)'}
bg={type === 'Human' ? 'white' : 'myBlue.100'}
bg={type === 'Human' ? 'white' : 'blue.50'}
>
<Avatar src={src} w={'100%'} h={'100%'} />
</Box>
@@ -1170,7 +1219,7 @@ function ChatController({
<MyIcon
{...controlIconStyle}
name={'copy'}
_hover={{ color: 'myBlue.700' }}
_hover={{ color: 'blue.600' }}
onClick={() => copyData(chat.value)}
/>
</MyTooltip>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1702637232008"
class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6146"
xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128">
<path
d="M261.688889 194.218667A67.470222 67.470222 0 1 1 194.218667 261.688889 67.584 67.584 0 0 1 261.688889 194.218667M261.688889 136.533333a125.155556 125.155556 0 1 0 125.155555 125.155556 125.155556 125.155556 0 0 0-125.155555-125.155556zM614.4 444.529778A67.470222 67.470222 0 1 1 547.271111 512a67.584 67.584 0 0 1 67.128889-67.470222M614.4 386.844444a125.155556 125.155556 0 1 0 125.155556 125.155556 125.155556 125.155556 0 0 0-125.155556-125.155556zM408.120889 694.840889A67.470222 67.470222 0 1 1 340.650667 762.311111a67.470222 67.470222 0 0 1 67.470222-67.470222m0-57.685333a125.155556 125.155556 0 1 0 125.155555 125.155555 125.155556 125.155556 0 0 0-125.155555-125.155555z"
fill="#1DCCA1" p-id="6147"></path>
<path
d="M489.016889 736.142222h375.011555v52.337778H489.016889zM325.632 788.48h-42.894222C171.804444 788.48 113.777778 711.452444 113.777778 635.335111a143.36 143.36 0 0 1 43.235555-103.651555c30.833778-30.037333 74.296889-45.511111 125.724445-45.511112h242.119111v52.337778H282.737778c-80.554667 0-116.622222 48.810667-116.622222 97.166222s36.522667 100.807111 116.622222 100.807112h42.894222zM743.537778 538.168889h-40.618667v-52.337778h40.618667c78.620444 0 114.346667-51.541333 114.346666-99.555555s-35.384889-98.417778-114.346666-98.417778H337.123556v-52.337778h406.414222C853.333333 235.52 910.222222 311.409778 910.222222 386.844444a147.911111 147.911111 0 0 1-42.780444 104.903112 168.732444 168.732444 0 0 1-123.904 46.421333z"
fill="#1DCCA1" p-id="6148"></path>
<path d="M910.222222 762.311111l-136.533333 102.4V659.911111l136.533333 102.4z" fill="#1DCCA1" p-id="6149"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -124,7 +124,8 @@ const iconPaths = {
'common/confirm/deleteTip': () => import('./icons/common/confirm/deleteTip.svg'),
'common/confirm/commonTip': () => import('./icons/common/confirm/commonTip.svg'),
'common/routePushLight': () => import('./icons/common/routePushLight.svg'),
'common/viewLight': () => import('./icons/common/viewLight.svg')
'common/viewLight': () => import('./icons/common/viewLight.svg'),
'core/app/customFeedback': () => import('./icons/core/app/customFeedback.svg')
};
export type IconName = keyof typeof iconPaths;

View File

@@ -129,7 +129,7 @@ const Navbar = ({ unread }: { unread: number }) => {
{...itemStyles}
{...(item.activeLink.includes(router.pathname)
? {
color: 'myBlue.700',
color: 'blue.600',
bg: 'white !important',
boxShadow: '1px 1px 10px rgba(0,0,0,0.2)'
}

View File

@@ -25,9 +25,9 @@ const Loading = ({
justifyContent={'center'}
flexDirection={'column'}
>
<Spinner thickness="4px" speed="0.65s" emptyColor="myGray.100" color="myBlue.600" size="xl" />
<Spinner thickness="4px" speed="0.65s" emptyColor="myGray.100" color="blue.500" size="xl" />
{text && (
<Box mt={2} color="myBlue.700" fontWeight={'bold'}>
<Box mt={2} color="blue.600" fontWeight={'bold'}>
{text}
</Box>
)}

View File

@@ -23,7 +23,7 @@ function MyLink(e: any) {
<Box as={'li'} mb={1}>
<Box
as={'span'}
color={'blue.600'}
color={'blue.500'}
textDecoration={'underline'}
cursor={'pointer'}
onClick={() => {

View File

@@ -77,7 +77,7 @@ const QuestionGuide = ({ text }: { text: string }) => {
name={'core/chat/sendLight'}
w={'14px'}
cursor={'pointer'}
_hover={{ color: 'myBlue.600' }}
_hover={{ color: 'blue.500' }}
onClick={() => eventBus.emit(EventNameEnum.sendQuestion, { text })}
/>
</MyTooltip>

View File

@@ -123,7 +123,7 @@ const MermaidBlock = ({ code }: { code: string }) => {
position={'absolute'}
color={'myGray.600'}
_hover={{
color: 'myBlue.700'
color: 'blue.600'
}}
right={0}
top={0}

View File

@@ -1,20 +1,23 @@
.waitingAnimation::after {
.waitingAnimation > :last-child::after {
display: inline-block;
content: '';
width: 4px;
width: 3px;
height: 14px;
transform: translate(4px, 2px) scaleY(1.3);
background-color: var(--chakra-colors-chakra-body-text);
background-color: var(--chakra-colors-blue-700);
animation: blink 0.6s infinite;
}
.animation {
> :last-child::after {
height: 20px;
&::after {
display: inline-block;
content: '';
width: 4px;
width: 3px;
height: 14px;
transform: translate(4px, 2px) scaleY(1.3);
background-color: var(--chakra-colors-chakra-body-text);
background-color: var(--chakra-colors-blue-700);
animation: blink 0.6s infinite;
}
}

View File

@@ -96,10 +96,10 @@ function A({ children, ...props }: any) {
name={'core/chat/quoteSign'}
transform={'translateY(-2px)'}
w={'18px'}
color={'myBlue.600'}
color={'blue.500'}
cursor={'pointer'}
_hover={{
color: 'myBlue.800'
color: 'blue.700'
}}
onClick={() => getFileAndOpen(props.href)}
/>
@@ -131,7 +131,7 @@ const Markdown = ({ source, isChatting = false }: { source: string; isChatting?:
return (
<ReactMarkdown
className={`markdown ${styles.markdown}
${isChatting ? (source === '' ? styles.waitingAnimation : styles.animation) : ''}
${isChatting ? `${formatSource ? styles.waitingAnimation : styles.animation}` : ''}
`}
remarkPlugins={[RemarkGfm, RemarkMath, RemarkBreaks]}
rehypePlugins={[RehypeKatex]}

View File

@@ -41,7 +41,7 @@ const MyMenu = ({ width, offset = [0, 10], Button, menuList }: Props) => {
e.stopPropagation();
item.onClick && item.onClick();
}}
color={item.isActive ? 'myBlue.600' : ''}
color={item.isActive ? 'blue.500' : ''}
whiteSpace={'pre-wrap'}
>
{item.child}

View File

@@ -32,7 +32,7 @@ const PromptTemplate = ({
cursor={'pointer'}
{...(item.title === selectTemplateTitle?.title
? {
bg: 'myBlue.100'
bg: 'blue.50'
}
: {})}
onClick={() => setSelectTemplateTitle(item)}

View File

@@ -62,7 +62,7 @@ const MySelect = (
{...(isOpen
? {
boxShadow: '0px 0px 4px #A8DBFF',
borderColor: 'myBlue.600'
borderColor: 'blue.500'
}
: {})}
{...props}
@@ -93,7 +93,7 @@ const MySelect = (
{...menuItemStyles}
{...(value === item.value
? {
color: 'myBlue.600',
color: 'blue.500',
bg: 'myWhite.300'
}
: {})}

View File

@@ -44,9 +44,9 @@ const SideTabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) =>
alignItems={'center'}
{...(activeId === item.id
? {
bg: ' myBlue.300 !important',
bg: ' blue.200 !important',
fontWeight: 'bold',
color: 'myBlue.700 ',
color: 'blue.600 ',
cursor: 'default'
}
: {

View File

@@ -69,7 +69,7 @@ const MySlider = ({
<SliderMark
value={value}
textAlign="center"
bg="myBlue.600"
bg="blue.500"
color="white"
px={1}
minW={'18px'}
@@ -95,9 +95,9 @@ const MySlider = ({
right: '-3px'
}}
>
<SliderFilledTrack bg={'myBlue.600'} />
<SliderFilledTrack bg={'blue.500'} />
</SliderTrack>
<SliderThumb border={'3px solid'} borderColor={'myBlue.600'}></SliderThumb>
<SliderThumb border={'3px solid'} borderColor={'blue.500'}></SliderThumb>
</Slider>
);
};

View File

@@ -55,10 +55,10 @@ const Tabs = ({ list, size = 'md', activeId, onChange, ...props }: Props) => {
whiteSpace={'nowrap'}
{...(activeId === item.id
? {
color: 'myBlue.700',
color: 'blue.600',
cursor: 'default',
fontWeight: 'bold',
borderBottomColor: 'myBlue.700'
borderBottomColor: 'blue.600'
}
: {
cursor: 'pointer'

View File

@@ -10,9 +10,9 @@ const Tag = ({ children, colorSchema = 'blue', ...props }: Props) => {
const theme = useMemo(() => {
const map = {
blue: {
borderColor: 'myBlue.600',
borderColor: 'blue.500',
bg: '#F2FBFF',
color: 'myBlue.700'
color: 'blue.600'
},
green: {
borderColor: '#67c13b',

View File

@@ -43,13 +43,13 @@ const MyRadio = ({
position={'relative'}
{...(value === item.value
? {
borderColor: 'myBlue.500',
bg: 'myBlue.100'
borderColor: 'blue.400',
bg: 'blue.50'
}
: {
bg: 'myWhite.300',
_hover: {
borderColor: 'myBlue.500'
borderColor: 'blue.400'
}
})}
_after={{
@@ -66,7 +66,7 @@ const MyRadio = ({
...(value === item.value
? {
border: '5px solid',
borderColor: 'myBlue.700'
borderColor: 'blue.600'
}
: {
border: '2px solid',

View File

@@ -52,7 +52,7 @@ const TagTextarea = ({ defaultValues, onUpdate, ...props }: Props) => {
bg={'myWhite.600'}
{...(focus && {
boxShadow: '0px 0px 4px #A8DBFF',
borderColor: 'myBlue.600'
borderColor: 'blue.500'
})}
{...props}
onClick={() => {

View File

@@ -3,8 +3,7 @@ import MyModal from '@/components/MyModal';
import { useQuery } from '@tanstack/react-query';
import React, { Dispatch, useMemo, useState } from 'react';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { Box, Flex } from '@chakra-ui/react';
import { Box } from '@chakra-ui/react';
import ParentPaths from '@/components/common/ParentPaths';
type PathItemType = {
@@ -53,13 +52,12 @@ const DatasetSelectContainer = ({
}
isOpen={isOpen}
onClose={onClose}
h={'80vh'}
w={'100%'}
maxW={['90vw', '900px']}
isCentered
>
<Flex flexDirection={'column'} h={'90vh'}>
<Box flex={'1 0 0'}>{children}</Box>
</Flex>
{children}
</MyModal>
);
};

View File

@@ -66,7 +66,7 @@ const AIChatSettingsModal = ({
fontSize: ['sm', 'md']
};
const selectTemplateBtn: BoxProps = {
color: 'myBlue.600',
color: 'blue.500',
cursor: 'pointer'
};

View File

@@ -82,7 +82,7 @@ export const DatasetSelectModal = ({
p={3}
border={theme.borders.base}
boxShadow={'sm'}
bg={'myBlue.300'}
bg={'blue.200'}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['24px', '28px']}></Avatar>

View File

@@ -12,10 +12,7 @@ import type {
FlowModuleItemType,
FlowModuleTemplateType
} from '@fastgpt/global/core/module/type.d';
import type {
FlowNodeOutputTargetItemType,
FlowNodeChangeProps
} from '@fastgpt/global/core/module/node/type';
import type { FlowNodeChangeProps } from '@fastgpt/global/core/module/node/type';
import React, {
type SetStateAction,
type Dispatch,

View File

@@ -60,7 +60,7 @@ const SelectAppModal = ({
cursor={'pointer'}
{...(selectedApps.includes(app._id)
? {
bg: 'myBlue.200',
bg: 'blue.100',
onClick: () => {
setSelectedApps(selectedApps.filter((e) => e !== app._id));
}

View File

@@ -36,7 +36,7 @@ const ButtonEdge = (
targetPosition
});
const edgeStyle = {
const edgeStyle: React.CSSProperties = {
...style,
...(selected
? {
@@ -63,6 +63,7 @@ const ButtonEdge = (
color={'black'}
cursor={'pointer'}
border={'1px solid #fff'}
zIndex={selected ? 1000 : 0}
_hover={{
boxShadow: '0 0 6px 2px rgba(0, 0, 0, 0.08)'
}}
@@ -71,7 +72,7 @@ const ButtonEdge = (
<MyIcon
name="closeSolid"
w={'100%'}
color={selected ? 'myBlue.800' : 'myGray.500'}
color={selected ? 'blue.700' : 'myGray.500'}
></MyIcon>
</Flex>
</EdgeLabelRenderer>

View File

@@ -65,7 +65,7 @@ const NodePluginInput = ({ data }: NodeProps<FlowModuleItemType>) => {
w={'14px'}
cursor={'pointer'}
mr={3}
_hover={{ color: 'myBlue.600' }}
_hover={{ color: 'blue.500' }}
onClick={() =>
setEditField({
inputType: item.type,

View File

@@ -84,7 +84,7 @@ const NodePluginOutput = ({ data }: NodeProps<FlowModuleItemType>) => {
w={'14px'}
cursor={'pointer'}
ml={3}
_hover={{ color: 'myBlue.600' }}
_hover={{ color: 'blue.500' }}
onClick={() =>
setEditField({
inputType: item.type,

View File

@@ -1,6 +1,6 @@
import { EditNodeFieldType, FlowNodeInputItemType } from '@fastgpt/global/core/module/node/type';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
import {
onChangeNode,
useFlowProviderStore,
@@ -81,7 +81,7 @@ const InputLabel = ({
w={'14px'}
cursor={'pointer'}
ml={3}
_hover={{ color: 'myBlue.600' }}
_hover={{ color: 'blue.500' }}
onClick={() =>
setEditField({
inputType: type,

View File

@@ -41,7 +41,7 @@ const OutputLabel = ({
w={'14px'}
cursor={'pointer'}
mr={3}
_hover={{ color: 'myBlue.600' }}
_hover={{ color: 'blue.500' }}
onClick={() =>
setEditField({
key: outputKey,

View File

@@ -96,8 +96,10 @@ const Container = React.memo(function Container(props: Props) {
minZoom={0.1}
maxZoom={1.5}
defaultEdgeOptions={{
animated: true
animated: true,
zIndex: 0
}}
elevateEdgesOnSelect
connectionLineStyle={{ strokeWidth: 2, stroke: '#5A646Es' }}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}

View File

@@ -88,7 +88,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
href={feConfigs.openAPIDocUrl || getDocPath('/docs/development/openapi')}
target={'_blank'}
ml={1}
color={'myBlue.600'}
color={'blue.500'}
>
</Link>

View File

@@ -156,7 +156,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
<MyIcon
name={'common/addCircleLight'}
w={['16px', '18px']}
color={'myBlue.600'}
color={'blue.500'}
cursor={'pointer'}
/>
}
@@ -177,7 +177,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
gap={3}
{...(userInfo?.team?.teamId === team.teamId
? {
bg: 'myBlue.300'
bg: 'blue.200'
}
: {
_hover: {
@@ -198,7 +198,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
{team.teamName}
</Box>
{userInfo?.team?.teamId === team.teamId ? (
<MyIcon name={'common/tickFill'} w={'16px'} color={'myBlue.600'} />
<MyIcon name={'common/tickFill'} w={'16px'} color={'blue.500'} />
) : (
<Button size={'xs'} variant={'base'} onClick={() => onSwitchTeam(team.teamId)}>
{t('user.team.Check Team')}
@@ -235,7 +235,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
ml={2}
cursor={'pointer'}
_hover={{
color: 'myBlue.600'
color: 'blue.500'
}}
onClick={() => {
if (!userInfo?.team) return;
@@ -260,7 +260,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
size="sm"
borderRadius={'md'}
ml={3}
leftIcon={<MyIcon name={'common/inviteLight'} w={'14px'} color={'myBlue.600'} />}
leftIcon={<MyIcon name={'common/inviteLight'} w={'14px'} color={'blue.500'} />}
onClick={() => {
if (userInfo.team.maxSize <= members.length) {
toast({
@@ -283,11 +283,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
borderRadius={'md'}
ml={3}
leftIcon={
<MyIcon
name={'support/account/loginoutLight'}
w={'14px'}
color={'myBlue.600'}
/>
<MyIcon name={'support/account/loginoutLight'} w={'14px'} color={'blue.500'} />
}
onClick={() => {
openLeaveConfirm(() => onLeaveTeam(userInfo?.team?.teamId))();
@@ -339,7 +335,7 @@ const TeamManageModal = ({ onClose }: { onClose: () => void }) => {
name={'edit'}
cursor={'pointer'}
w="14px"
_hover={{ color: 'myBlue.600' }}
_hover={{ color: 'blue.500' }}
/>
</MenuButton>
}

View File

@@ -12,6 +12,7 @@ export type GetChatSpeechProps = {
export type InitChatProps = {
appId?: string;
chatId?: string;
loadCustomFeedbacks?: boolean;
};
export type InitOutLinkChatProps = {
chatId?: string;
@@ -74,5 +75,14 @@ export type DeleteChatItemProps = {
};
export type AdminUpdateFeedbackParams = AdminFbkType & {
appId: string;
chatId: string;
chatItemId: string;
};
export type CloseCustomFeedbackParams = {
appId: string;
chatId: string;
chatItemId: string;
index: number;
};

View File

@@ -288,7 +288,7 @@ async function initPgData() {
const limit = 10;
// add column
try {
await Promise.all([
await Promise.allSettled([
PgClient.query(`ALTER TABLE ${PgDatasetTableName} ADD COLUMN team_id VARCHAR(50);`),
PgClient.query(`ALTER TABLE ${PgDatasetTableName} ADD COLUMN tmb_id VARCHAR(50);`),
PgClient.query(`ALTER TABLE ${PgDatasetTableName} ALTER COLUMN user_id DROP NOT NULL;`)

View File

@@ -2,79 +2,15 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { customAlphabet } from 'nanoid';
import multer from 'multer';
import path from 'path';
import { uploadFile } from '@fastgpt/service/common/file/gridfs/controller';
import { BucketNameEnum } from '@fastgpt/global/common/file/constants';
const nanoid = customAlphabet('1234567890abcdef', 12);
type FileType = {
fieldname: string;
originalname: string;
encoding: string;
mimetype: string;
filename: string;
path: string;
size: number;
};
import { getUploadModel } from '@fastgpt/service/common/file/upload/multer';
/**
* Creates the multer uploader
*/
const maxSize = 500 * 1024 * 1024;
class UploadModel {
uploader = multer({
limits: {
fieldSize: maxSize
},
preservePath: true,
storage: multer.diskStorage({
filename: (_req, file, cb) => {
const { ext } = path.parse(decodeURIComponent(file.originalname));
cb(null, nanoid() + ext);
}
})
}).any();
async doUpload(req: NextApiRequest, res: NextApiResponse) {
return new Promise<{
files: FileType[];
bucketName: `${BucketNameEnum}`;
metadata: Record<string, any>;
}>((resolve, reject) => {
// @ts-ignore
this.uploader(req, res, (error) => {
if (error) {
return reject(error);
}
resolve({
...req.body,
files:
// @ts-ignore
req.files?.map((file) => ({
...file,
originalname: decodeURIComponent(file.originalname)
})) || [],
metadata: (() => {
if (!req.body?.metadata) return {};
try {
return JSON.parse(req.body.metadata);
} catch (error) {
console.log(error);
return {};
}
})()
});
});
});
}
}
const upload = new UploadModel();
const upload = getUploadModel({
maxSize: 500 * 1024 * 1024
});
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -82,6 +18,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
const { userId, teamId, tmbId } = await authCert({ req, authToken: true });
const { files, bucketName, metadata } = await upload.doUpload(req, res);
if (!bucketName) {
throw new Error('bucketName is empty');
}
const upLoadResults = await Promise.all(
files.map((file) =>
uploadFile({

View File

@@ -4,7 +4,7 @@ import { jsonRes } from '@fastgpt/service/common/response';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { connectToDatabase } from '@/service/mongo';
import { UrlFetchParams, UrlFetchResponse } from '@fastgpt/global/common/file/api.d';
import { urlsFetch } from '@fastgpt/global/common/file/tools';
import { urlsFetch } from '@fastgpt/service/common/string/cheerio';
const fetchContent = async (req: NextApiRequest, res: NextApiResponse) => {
try {

View File

@@ -53,7 +53,7 @@ function simpleChatTemplate({
key: 'userChatInput',
type: 'systemInput',
label: '用户问题',
connected: true
connected: false
}
],
outputs: [
@@ -95,7 +95,7 @@ function simpleChatTemplate({
label: '对话模型',
required: true,
value: formData.aiSettings.model,
connected: true
connected: false
},
{
key: 'temperature',
@@ -115,7 +115,7 @@ function simpleChatTemplate({
value: 10
}
],
connected: true
connected: false
},
{
key: 'maxToken',
@@ -135,7 +135,7 @@ function simpleChatTemplate({
value: 4000
}
],
connected: true
connected: false
},
{
key: 'isResponseAnswerText',
@@ -143,21 +143,21 @@ function simpleChatTemplate({
label: '返回AI内容',
valueType: 'boolean',
value: true,
connected: true
connected: false
},
{
key: 'quoteTemplate',
type: 'hidden',
label: '引用内容模板',
valueType: 'string',
connected: true
connected: false
},
{
key: 'quotePrompt',
type: 'hidden',
label: '引用内容提示词',
valueType: 'string',
connected: true
connected: false
},
{
key: 'aiSettings',
@@ -176,7 +176,7 @@ function simpleChatTemplate({
placeholder:
'模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。可使用变量,例如 {{language}}',
value: formData.aiSettings.systemPrompt,
connected: true
connected: false
},
{
key: 'quoteQA',
@@ -191,7 +191,7 @@ function simpleChatTemplate({
type: 'target',
label: 'core.module.input.label.chat history',
valueType: 'chatHistory',
connected: true,
connected: false,
value: 8
},
{
@@ -292,21 +292,21 @@ function datasetTemplate({
value: formData.dataset.datasets,
type: FlowNodeInputTypeEnum.custom,
label: '关联的知识库',
connected: true
connected: false
},
{
key: 'similarity',
value: 0.1,
type: FlowNodeInputTypeEnum.slider,
label: '相关度',
connected: true
connected: false
},
{
key: 'limit',
value: 2000,
type: FlowNodeInputTypeEnum.slider,
label: '单次搜索上限',
connected: true
connected: false
},
{
key: 'switch',
@@ -403,7 +403,7 @@ function datasetTemplate({
label: '对话模型',
required: true,
value: formData.aiSettings.model,
connected: true
connected: false
},
{
key: 'temperature',
@@ -423,7 +423,7 @@ function datasetTemplate({
value: 10
}
],
connected: true
connected: false
},
{
key: 'maxToken',
@@ -443,7 +443,7 @@ function datasetTemplate({
value: 4000
}
],
connected: true
connected: false
},
{
key: 'isResponseAnswerText',
@@ -451,21 +451,21 @@ function datasetTemplate({
label: '返回AI内容',
valueType: 'boolean',
value: true,
connected: true
connected: false
},
{
key: 'quoteTemplate',
type: 'hidden',
label: '引用内容模板',
valueType: 'string',
connected: true
connected: false
},
{
key: 'quotePrompt',
type: 'hidden',
label: '引用内容提示词',
valueType: 'string',
connected: true
connected: false
},
{
key: 'aiSettings',
@@ -484,7 +484,7 @@ function datasetTemplate({
placeholder:
'模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。可使用变量,例如 {{language}}',
value: formData.aiSettings.systemPrompt,
connected: true
connected: false
},
{
key: 'quoteQA',
@@ -499,7 +499,7 @@ function datasetTemplate({
type: 'target',
label: 'core.module.input.label.chat history',
valueType: 'chatHistory',
connected: true,
connected: false,
value: 8
},
{

View File

@@ -39,49 +39,49 @@ function chatModelInput(formData: AppSimpleEditFormType): FlowNodeInputItemType[
value: formData.aiSettings.model,
type: 'custom',
label: '对话模型',
connected: true
connected: false
},
{
key: 'temperature',
value: formData.aiSettings.temperature,
type: 'slider',
label: '温度',
connected: true
connected: false
},
{
key: 'maxToken',
value: formData.aiSettings.maxToken,
type: 'custom',
label: '回复上限',
connected: true
connected: false
},
{
key: 'systemPrompt',
value: formData.aiSettings.systemPrompt || '',
type: 'textarea',
label: '系统提示词',
connected: true
connected: false
},
{
key: ModuleInputKeyEnum.aiChatIsResponseText,
value: true,
type: 'hidden',
label: '返回AI内容',
connected: true
connected: false
},
{
key: 'quoteTemplate',
value: formData.aiSettings.quoteTemplate || '',
type: 'hidden',
label: '引用内容模板',
connected: true
connected: false
},
{
key: 'quotePrompt',
value: formData.aiSettings.quotePrompt || '',
type: 'hidden',
label: '引用内容提示词',
connected: true
connected: false
},
{
key: 'switch',
@@ -93,7 +93,7 @@ function chatModelInput(formData: AppSimpleEditFormType): FlowNodeInputItemType[
key: 'history',
type: 'target',
label: 'core.module.input.label.chat history',
connected: true,
connected: false,
value: 6
},
{
@@ -118,9 +118,9 @@ function simpleChatTemplate(formData: AppSimpleEditFormType): ModuleItemType[] {
inputs: [
{
key: 'userChatInput',
connected: true,
connected: false,
label: '用户问题',
type: 'target'
type: 'systemInput'
}
],
outputs: [
@@ -179,8 +179,8 @@ function datasetTemplate(formData: AppSimpleEditFormType): ModuleItemType[] {
{
key: 'userChatInput',
label: '用户问题',
type: 'target',
connected: true
type: 'systemInput',
connected: false
}
],
outputs: [
@@ -319,7 +319,7 @@ function datasetTemplate(formData: AppSimpleEditFormType): ModuleItemType[] {
type: FlowNodeInputTypeEnum.textarea,
valueType: ModuleIOValueTypeEnum.string,
label: '回复的内容',
connected: true
connected: false
}
],
outputs: [],

View File

@@ -78,12 +78,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
}
},
robotBadFeedbackCount: {
customFeedbacksCount: {
$size: {
$filter: {
input: '$chatitems',
as: 'item',
cond: { $ifNull: ['$$item.robotBadFeedback', false] }
cond: { $gt: [{ $size: { $ifNull: ['$$item.customFeedbacks', []] } }, 0] }
}
}
},
@@ -102,7 +102,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
$sort: {
userBadFeedbackCount: -1,
userGoodFeedbackCount: -1,
robotBadFeedbackCount: -1,
customFeedbacksCount: -1,
updateTime: -1
}
},
@@ -118,7 +118,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
messageCount: { $size: '$chatitems' },
userGoodFeedbackCount: 1,
userBadFeedbackCount: 1,
robotBadFeedbackCount: 1,
customFeedbacksCount: 1,
markCount: 1
}
}

View File

@@ -4,22 +4,29 @@ import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import type { AdminUpdateFeedbackParams } from '@/global/core/chat/api.d';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { autChatCrud } from '@/service/support/permission/auth/chat';
/* 初始化我的聊天框,需要身份验证 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { chatItemId, datasetId, dataId, q, a } = req.body as AdminUpdateFeedbackParams;
const { appId, chatId, chatItemId, datasetId, dataId, q, a } =
req.body as AdminUpdateFeedbackParams;
if (!chatItemId || !datasetId || !dataId || !q) {
throw new Error('missing parameter');
}
const { userId } = await authCert({ req, authToken: true });
await autChatCrud({
req,
authToken: true,
appId,
chatId,
per: 'r'
});
await MongoChatItem.findOneAndUpdate(
{
userId,
dataId: chatItemId
},
{

View File

@@ -0,0 +1,47 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import type {
AdminUpdateFeedbackParams,
CloseCustomFeedbackParams
} from '@/global/core/chat/api.d';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { autChatCrud } from '@/service/support/permission/auth/chat';
/* 初始化我的聊天框,需要身份验证 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const { appId, chatId, chatItemId, index } = req.body as CloseCustomFeedbackParams;
if (!chatItemId || !appId || !chatId || !chatItemId) {
throw new Error('missing parameter');
}
await autChatCrud({
req,
authToken: true,
appId,
chatId,
per: 'r'
});
await authCert({ req, authToken: true });
await MongoChatItem.findOneAndUpdate(
{ dataId: chatItemId },
{ $unset: { [`customFeedbacks.${index}`]: 1 } }
);
await MongoChatItem.findOneAndUpdate(
{ dataId: chatItemId },
{ $pull: { customFeedbacks: null } }
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -14,7 +14,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
try {
await connectToDatabase();
let { appId, chatId } = req.query as InitChatProps;
let { appId, chatId, loadCustomFeedbacks } = req.query as InitChatProps;
if (!appId) {
return jsonRes(res, {
@@ -43,7 +43,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { history } = await getChatItems({
chatId,
limit: 30,
field: `dataId obj value adminFeedback userBadFeedback userGoodFeedback robotBadFeedback ${ModuleOutputKeyEnum.responseData}`
field: `dataId obj value adminFeedback userBadFeedback userGoodFeedback ${
ModuleOutputKeyEnum.responseData
} ${loadCustomFeedbacks ? 'customFeedbacks' : ''}`
});
jsonRes<InitChatResponse>(res, {

View File

@@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { HttpBodyType } from '@fastgpt/global/core/module/api.d';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { authRequestFromLocal } from '@fastgpt/service/support/permission/auth/common';
type Props = HttpBodyType<{
input: string;
@@ -13,6 +14,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
data: { input, rule = '' }
} = req.body as Props;
await authRequestFromLocal({ req });
const result = (() => {
if (typeof input === 'string') {
const defaultReg: any[] = [

View File

@@ -0,0 +1,52 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { HttpBodyType } from '@fastgpt/global/core/module/api.d';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { addCustomFeedbacks } from '@fastgpt/service/core/chat/controller';
import { authRequestFromLocal } from '@fastgpt/service/support/permission/auth/common';
type Props = HttpBodyType<{
defaultFeedback: string;
customFeedback: string;
}>;
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const {
chatId,
responseChatItemId: chatItemId,
data: { defaultFeedback, customFeedback }
} = req.body as Props;
await authRequestFromLocal({ req });
const feedback = customFeedback || defaultFeedback;
if (!feedback) {
return res.json({
response: ''
});
}
// wait the chat finish
setTimeout(() => {
addCustomFeedbacks({
chatId,
chatItemId,
feedbacks: [feedback]
});
}, 60000);
if (!chatId || !chatItemId) {
return res.json({
response: `\\n\\n**自动反馈调试**: ${feedback}\\n\\n`
});
}
return res.json({
response: ''
});
} catch (err) {
console.log(err);
res.status(500).send(getErrText(err));
}
}

View File

@@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import type { HttpBodyType } from '@fastgpt/global/core/module/api.d';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { replaceVariable } from '@fastgpt/global/common/string/tools';
import { authRequestFromLocal } from '@fastgpt/service/support/permission/auth/common';
type Props = HttpBodyType<{
text: string;
@@ -14,6 +15,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
data: { text, ...obj }
} = req.body as Props;
await authRequestFromLocal({ req });
const textResult = replaceVariable(text, obj);
res.json({

View File

@@ -195,12 +195,14 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
// get and concat history
const { history } = await getChatItems({ chatId, limit: 30, field: `dataId obj value` });
const concatHistories = history.concat(chatMessages);
const responseChatItemId: string | undefined = messages[messages.length - 1].dataId;
/* start flow controller */
const { responseData, answerText } = await dispatchModules({
res,
appId: String(app._id),
chatId,
responseChatItemId,
modules: app.modules,
user,
teamId: user.team.teamId,
@@ -237,7 +239,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
content: [
question,
{
dataId: messages[messages.length - 1].dataId,
dataId: responseChatItemId,
obj: ChatRoleEnum.AI,
value: answerText,
responseData

View File

@@ -29,6 +29,7 @@ import Tag from '@/components/Tag';
import MyModal from '@/components/MyModal';
import DateRangePicker, { type DateRangeType } from '@/components/DateRangePicker';
import { addDays } from 'date-fns';
import MyBox from '@/components/common/MyBox';
const Logs = ({ appId }: { appId: string }) => {
const { t } = useTranslation();
@@ -96,6 +97,7 @@ const Logs = ({ appId }: { appId: string }) => {
<Th>{t('app.Logs Title')}</Th>
<Th>{t('app.Logs Message Total')}</Th>
<Th>{t('app.Feedback Count')}</Th>
<Th>{t('core.app.feedback.Custom feedback')}</Th>
<Th>{t('app.Mark Count')}</Th>
</Tr>
</Thead>
@@ -158,7 +160,9 @@ const Logs = ({ appId }: { appId: string }) => {
{item.userBadFeedbackCount}
</Flex>
)}
{!item?.userGoodFeedbackCount && !item?.userBadFeedbackCount && <>-</>}
</Td>
<Td>{item.customFeedbacksCount || '-'}</Td>
<Td>{item.markCount}</Td>
</Tr>
))}
@@ -208,7 +212,7 @@ const Logs = ({ appId }: { appId: string }) => {
export default React.memo(Logs);
function DetailLogsModal({
const DetailLogsModal = ({
appId,
chatId,
onClose
@@ -216,14 +220,14 @@ function DetailLogsModal({
appId: string;
chatId: string;
onClose: () => void;
}) {
}) => {
const ChatBoxRef = useRef<ComponentRef>(null);
const { isPc } = useSystemStore();
const theme = useTheme();
const { data: chat } = useQuery(
const { data: chat, isFetching } = useQuery(
['getChatDetail', chatId],
() => getInitChatInfo({ appId, chatId }),
() => getInitChatInfo({ appId, chatId, loadCustomFeedbacks: true }),
{
onSuccess(res) {
const history = res.history.map((item) => ({
@@ -249,9 +253,11 @@ function DetailLogsModal({
return (
<>
<Flex
zIndex={3}
<MyBox
isLoading={isFetching}
display={'flex'}
flexDirection={'column'}
zIndex={3}
position={['fixed', 'absolute']}
top={[0, '2%']}
right={0}
@@ -325,8 +331,8 @@ function DetailLogsModal({
chatId={chatId}
/>
</Box>
</Flex>
</MyBox>
<Box zIndex={2} position={'fixed'} top={0} left={0} bottom={0} right={0} onClick={onClose} />
</>
);
}
};

View File

@@ -189,7 +189,7 @@ const SelectUsingWayModal = ({ share, onClose }: { share: OutLinkSchema; onClose
w={'16px'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{ color: 'myBlue.600' }}
_hover={{ color: 'blue.500' }}
onClick={() => {
copyData(wayMap[getValues('usingWay')].code);
}}

View File

@@ -77,7 +77,7 @@ const Share = ({ appId }: { appId: string }) => {
</Box>
<Button
variant={'base'}
colorScheme={'myBlue'}
colorScheme={'blue'}
size={['sm', 'md']}
{...(shareChatList.length >= 10
? {

View File

@@ -548,7 +548,7 @@ function Settings({ appId }: { appId: string }) {
mt={2}
px={5}
py={4}
bg={'myBlue.100'}
bg={'blue.50'}
position={'relative'}
>
<Flex alignItems={'center'} py={2}>

View File

@@ -139,7 +139,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
>
<IconButton
mr={3}
icon={<MyIcon name={'backFill'} w={'18px'} color={'myBlue.600'} />}
icon={<MyIcon name={'backFill'} w={'18px'} color={'blue.500'} />}
bg={'white'}
boxShadow={'1px 1px 9px rgba(0,0,0,0.15)'}
h={'28px'}

View File

@@ -61,7 +61,7 @@ const ShareModelList = ({
<Flex
alignItems={'center'}
cursor={'pointer'}
color={model.isCollection ? 'myBlue.700' : 'blackAlpha.700'}
color={model.isCollection ? 'blue.600' : 'blackAlpha.700'}
onClick={() => onclickCollection(model._id)}
>
<MyIcon

View File

@@ -154,7 +154,7 @@ const ChatHistorySlider = ({
variant={'base'}
flex={1}
h={'100%'}
color={'myBlue.700'}
color={'blue.600'}
borderRadius={'xl'}
leftIcon={<MyIcon name={'chat'} w={'16px'} />}
overflow={'hidden'}
@@ -201,8 +201,8 @@ const ChatHistorySlider = ({
bg={item.top ? '#E6F6F6 !important' : ''}
{...(item.id === activeChatId
? {
backgroundColor: 'myBlue.100 !important',
color: 'myBlue.700'
backgroundColor: 'blue.50 !important',
color: 'blue.600'
}
: {
onClick: () => {
@@ -290,8 +290,8 @@ const ChatHistorySlider = ({
alignItems={'center'}
{...(item._id === appId
? {
backgroundColor: 'myBlue.100 !important',
color: 'myBlue.700'
backgroundColor: 'blue.50 !important',
color: 'blue.600'
}
: {
onClick: () => {
@@ -325,7 +325,7 @@ const ChatHistorySlider = ({
>
<IconButton
mr={3}
icon={<MyIcon name={'backFill'} w={'18px'} color={'myBlue.600'} />}
icon={<MyIcon name={'backFill'} w={'18px'} color={'blue.500'} />}
bg={'white'}
boxShadow={'1px 1px 9px rgba(0,0,0,0.15)'}
h={'28px'}

View File

@@ -28,7 +28,7 @@ const SliderApps = ({ appId }: { appId: string }) => {
>
<IconButton
mr={3}
icon={<MyIcon name={'backFill'} w={'18px'} color={'myBlue.600'} />}
icon={<MyIcon name={'backFill'} w={'18px'} color={'blue.500'} />}
bg={'white'}
boxShadow={'1px 1px 9px rgba(0,0,0,0.15)'}
h={'28px'}

View File

@@ -311,7 +311,7 @@ const CollectionCard = () => {
target="_blank"
mr={2}
textDecoration={'underline'}
color={'myBlue.700'}
color={'blue.600'}
>
{datasetDetail.websiteConfig.url}
</Link>
@@ -371,7 +371,7 @@ const CollectionCard = () => {
Button={
<MenuButton
_hover={{
color: 'myBlue.600'
color: 'blue.500'
}}
fontSize={['sm', 'md']}
>
@@ -381,7 +381,7 @@ const CollectionCard = () => {
py={2}
borderRadius={'md'}
cursor={'pointer'}
bg={'myBlue.600'}
bg={'blue.500'}
overflow={'hidden'}
color={'white'}
h={['28px', '35px']}
@@ -489,7 +489,7 @@ const CollectionCard = () => {
data-drag-id={
collection.type === DatasetCollectionTypeEnum.folder ? collection._id : undefined
}
bg={dragTargetId === collection._id ? 'myBlue.200' : ''}
bg={dragTargetId === collection._id ? 'blue.100' : ''}
userSelect={'none'}
onDragStart={(e) => {
setDragStartId(collection._id);
@@ -579,7 +579,7 @@ const CollectionCard = () => {
h={'22px'}
borderRadius={'md'}
_hover={{
color: 'myBlue.600',
color: 'blue.500',
'& .icon': {
bg: 'myGray.100'
}

Some files were not shown because too many files have changed in this diff Show More