Compare commits

...

15 Commits

Author SHA1 Message Date
archer
a290369fc6 fix: tiktoken memory 2023-06-02 13:10:34 +08:00
archer
8817e4d2db feat: qa must pay 2023-06-01 11:51:00 +08:00
archer
c9ee6fabe4 fix: sql 2023-05-31 11:37:39 +08:00
archer
24319fe860 fix: quote len 2023-05-30 23:43:08 +08:00
archer
87c5cb6bca feat: quote source 2023-05-30 23:38:16 +08:00
archer
746b9af2de feat: kb data source 2023-05-30 23:26:29 +08:00
archer
176c5a4d79 fix: prompts filter 2023-05-30 21:27:09 +08:00
archer
0cde9a10a8 feat: use last quote 2023-05-30 21:18:08 +08:00
archer
59ddf09b94 fix: save chat 2023-05-30 00:40:03 +08:00
archer
cdee91bec1 fix: ts 2023-05-29 23:49:45 +08:00
archer
d36a7cb177 perf: avatar 2023-05-29 23:40:22 +08:00
archer
2fc31a706d feat: update model use time 2023-05-29 22:51:09 +08:00
archer
2fce76202a feat: custom title and set history top 2023-05-29 22:24:49 +08:00
Textcat
7fe39c2515 feat:自定义历史聊天标题 (#41)
* feat:自定义历史聊天标题

* Update chat.ts

* perf:自定义聊天标题

* feat: google auth

* perf:将修改标题移入右键菜单

* perf:updatetitle

---------

Co-authored-by: archer <545436317@qq.com>
2023-05-29 20:36:28 +08:00
archer
e818cb037f fix: openai error 2023-05-28 21:41:13 +08:00
59 changed files with 659 additions and 587 deletions

View File

@@ -8,6 +8,7 @@ CREATE TABLE IF NOT EXISTS modelData (
vector VECTOR(1536) NOT NULL,
user_id VARCHAR(50) NOT NULL,
kb_id VARCHAR(50) NOT NULL,
source VARCHAR(100),
q TEXT NOT NULL,
a TEXT NOT NULL
);

View File

@@ -55,7 +55,6 @@
"remark-math": "^5.1.1",
"request-ip": "^3.3.0",
"sass": "^1.58.3",
"sharp": "^0.31.3",
"tunnel": "^0.0.6",
"wxpay-v3": "^3.0.2",
"zustand": "^4.3.5"

253
pnpm-lock.yaml generated
View File

@@ -64,7 +64,6 @@ specifiers:
remark-math: ^5.1.1
request-ip: ^3.3.0
sass: ^1.58.3
sharp: ^0.31.3
tunnel: ^0.0.6
typescript: 4.9.5
wxpay-v3: ^3.0.2
@@ -115,7 +114,6 @@ dependencies:
remark-math: registry.npmmirror.com/remark-math/5.1.1
request-ip: 3.3.0
sass: registry.npmmirror.com/sass/1.58.3
sharp: registry.npmmirror.com/sharp/0.31.3
tunnel: registry.npmmirror.com/tunnel/0.0.6
wxpay-v3: registry.npmmirror.com/wxpay-v3/3.0.2
zustand: registry.npmmirror.com/zustand/4.3.5_immer@9.0.19+react@18.2.0
@@ -5827,16 +5825,6 @@ packages:
engines: {node: '>=8'}
dev: false
registry.npmmirror.com/bl/4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz}
name: bl
version: 4.1.0
dependencies:
buffer: registry.npmmirror.com/buffer/5.7.1
inherits: registry.npmmirror.com/inherits/2.0.4
readable-stream: registry.npmmirror.com/readable-stream/3.6.1
dev: false
registry.npmmirror.com/bluebird/3.4.7:
resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/bluebird/-/bluebird-3.4.7.tgz}
name: bluebird
@@ -6017,12 +6005,6 @@ packages:
fsevents: 2.3.2
dev: false
registry.npmmirror.com/chownr/1.1.4:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/chownr/-/chownr-1.1.4.tgz}
name: chownr
version: 1.1.4
dev: false
registry.npmmirror.com/clean-stack/2.2.0:
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/clean-stack/-/clean-stack-2.2.0.tgz}
name: clean-stack
@@ -6079,6 +6061,7 @@ packages:
engines: {node: '>=7.0.0'}
dependencies:
color-name: registry.npmmirror.com/color-name/1.1.4
dev: true
registry.npmmirror.com/color-name/1.1.3:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/color-name/-/color-name-1.1.3.tgz}
@@ -6089,25 +6072,7 @@ packages:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz}
name: color-name
version: 1.1.4
registry.npmmirror.com/color-string/1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz}
name: color-string
version: 1.9.1
dependencies:
color-name: registry.npmmirror.com/color-name/1.1.4
simple-swizzle: registry.npmmirror.com/simple-swizzle/0.2.2
dev: false
registry.npmmirror.com/color/4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/color/-/color-4.2.3.tgz}
name: color
version: 4.2.3
engines: {node: '>=12.5.0'}
dependencies:
color-convert: registry.npmmirror.com/color-convert/2.0.1
color-string: registry.npmmirror.com/color-string/1.9.1
dev: false
dev: true
registry.npmmirror.com/color2k/2.0.2:
resolution: {integrity: sha512-kJhwH5nAwb34tmyuqq/lgjEKzlFXn1U99NlnB6Ws4qVaERcRUYeYP1cBw6BJ4vxaWStAUEef4WMr7WjOCnBt8w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/color2k/-/color2k-2.0.2.tgz}
@@ -6362,15 +6327,6 @@ packages:
character-entities: registry.npmmirror.com/character-entities/2.0.2
dev: false
registry.npmmirror.com/decompress-response/6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz}
name: decompress-response
version: 6.0.0
engines: {node: '>=10'}
dependencies:
mimic-response: registry.npmmirror.com/mimic-response/3.1.0
dev: false
registry.npmmirror.com/deep-equal/2.2.0:
resolution: {integrity: sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/deep-equal/-/deep-equal-2.2.0.tgz}
name: deep-equal
@@ -6395,13 +6351,6 @@ packages:
which-typed-array: registry.npmmirror.com/which-typed-array/1.1.9
dev: true
registry.npmmirror.com/deep-extend/0.6.0:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/deep-extend/-/deep-extend-0.6.0.tgz}
name: deep-extend
version: 0.6.0
engines: {node: '>=4.0.0'}
dev: false
registry.npmmirror.com/deep-is/0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz}
name: deep-is
@@ -6480,13 +6429,6 @@ packages:
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dev: false
registry.npmmirror.com/detect-libc/2.0.1:
resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/detect-libc/-/detect-libc-2.0.1.tgz}
name: detect-libc
version: 2.0.1
engines: {node: '>=8'}
dev: false
registry.npmmirror.com/detect-node-es/1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz}
name: detect-node-es
@@ -7171,13 +7113,6 @@ packages:
strip-final-newline: registry.npmmirror.com/strip-final-newline/3.0.0
dev: true
registry.npmmirror.com/expand-template/2.0.3:
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/expand-template/-/expand-template-2.0.3.tgz}
name: expand-template
version: 2.0.3
engines: {node: '>=6'}
dev: false
registry.npmmirror.com/extend-shallow/2.0.1:
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/extend-shallow/-/extend-shallow-2.0.1.tgz}
name: extend-shallow
@@ -7399,12 +7334,6 @@ packages:
tslib: registry.npmmirror.com/tslib/2.4.0
dev: false
registry.npmmirror.com/fs-constants/1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz}
name: fs-constants
version: 1.0.0
dev: false
registry.npmmirror.com/fs-extra/8.1.0:
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/fs-extra/-/fs-extra-8.1.0.tgz}
name: fs-extra
@@ -7517,12 +7446,6 @@ packages:
- supports-color
dev: false
registry.npmmirror.com/github-from-package/0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/github-from-package/-/github-from-package-0.0.0.tgz}
name: github-from-package
version: 0.0.0
dev: false
registry.npmmirror.com/glob-parent/5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz}
name: glob-parent
@@ -8062,12 +7985,6 @@ packages:
name: is-arrayish
version: 0.2.1
registry.npmmirror.com/is-arrayish/0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.2.tgz}
name: is-arrayish
version: 0.3.2
dev: false
registry.npmmirror.com/is-bigint/1.0.4:
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/is-bigint/-/is-bigint-1.0.4.tgz}
name: is-bigint
@@ -9291,13 +9208,6 @@ packages:
engines: {node: '>=12'}
dev: true
registry.npmmirror.com/mimic-response/3.1.0:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz}
name: mimic-response
version: 3.1.0
engines: {node: '>=10'}
dev: false
registry.npmmirror.com/minimatch/3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz}
name: minimatch
@@ -9311,12 +9221,6 @@ packages:
name: minimist
version: 1.2.8
registry.npmmirror.com/mkdirp-classic/0.5.3:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz}
name: mkdirp-classic
version: 0.5.3
dev: false
registry.npmmirror.com/mkdirp/0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz}
name: mkdirp
@@ -9436,12 +9340,6 @@ packages:
hasBin: true
dev: false
registry.npmmirror.com/napi-build-utils/1.0.2:
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz}
name: napi-build-utils
version: 1.0.2
dev: false
registry.npmmirror.com/natural-compare/1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz}
name: natural-compare
@@ -9503,21 +9401,6 @@ packages:
- babel-plugin-macros
dev: false
registry.npmmirror.com/node-abi/3.33.0:
resolution: {integrity: sha512-7GGVawqyHF4pfd0YFybhv/eM9JwTtPqx0mAanQ146O3FlSh3pA24zf9IRQTOsfTSqXTNzPSP5iagAJ94jjuVog==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/node-abi/-/node-abi-3.33.0.tgz}
name: node-abi
version: 3.33.0
engines: {node: '>=10'}
dependencies:
semver: registry.npmmirror.com/semver/7.3.8
dev: false
registry.npmmirror.com/node-addon-api/5.1.0:
resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/node-addon-api/-/node-addon-api-5.1.0.tgz}
name: node-addon-api
version: 5.1.0
dev: false
registry.npmmirror.com/node-releases/2.0.10:
resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/node-releases/-/node-releases-2.0.10.tgz}
name: node-releases
@@ -10027,27 +9910,6 @@ packages:
dependencies:
xtend: registry.npmmirror.com/xtend/4.0.2
registry.npmmirror.com/prebuild-install/7.1.1:
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.1.tgz}
name: prebuild-install
version: 7.1.1
engines: {node: '>=10'}
hasBin: true
dependencies:
detect-libc: registry.npmmirror.com/detect-libc/2.0.1
expand-template: registry.npmmirror.com/expand-template/2.0.3
github-from-package: registry.npmmirror.com/github-from-package/0.0.0
minimist: registry.npmmirror.com/minimist/1.2.8
mkdirp-classic: registry.npmmirror.com/mkdirp-classic/0.5.3
napi-build-utils: registry.npmmirror.com/napi-build-utils/1.0.2
node-abi: registry.npmmirror.com/node-abi/3.33.0
pump: registry.npmmirror.com/pump/3.0.0
rc: registry.npmmirror.com/rc/1.2.8
simple-get: registry.npmmirror.com/simple-get/4.0.1
tar-fs: registry.npmmirror.com/tar-fs/2.1.1
tunnel-agent: registry.npmmirror.com/tunnel-agent/0.6.0
dev: false
registry.npmmirror.com/prelude-ls/1.1.2:
resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.1.2.tgz}
name: prelude-ls
@@ -10194,18 +10056,6 @@ packages:
unpipe: registry.npmmirror.com/unpipe/1.0.0
dev: false
registry.npmmirror.com/rc/1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/rc/-/rc-1.2.8.tgz}
name: rc
version: 1.2.8
hasBin: true
dependencies:
deep-extend: registry.npmmirror.com/deep-extend/0.6.0
ini: registry.npmmirror.com/ini/1.3.8
minimist: registry.npmmirror.com/minimist/1.2.8
strip-json-comments: registry.npmmirror.com/strip-json-comments/2.0.1
dev: false
registry.npmmirror.com/react-clientside-effect/1.2.6_react@18.2.0:
resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz}
id: registry.npmmirror.com/react-clientside-effect/1.2.6
@@ -10429,17 +10279,6 @@ packages:
util-deprecate: registry.npmmirror.com/util-deprecate/1.0.2
dev: false
registry.npmmirror.com/readable-stream/3.6.1:
resolution: {integrity: sha512-+rQmrWMYGA90yenhTYsLWAsLsqVC8osOw6PKE1HDYiO0gdPeKe/xDHNzIAIn4C91YQ6oenEhfYqqc1883qHbjQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.1.tgz}
name: readable-stream
version: 3.6.1
engines: {node: '>= 6'}
dependencies:
inherits: registry.npmmirror.com/inherits/2.0.4
string_decoder: registry.npmmirror.com/string_decoder/1.3.0
util-deprecate: registry.npmmirror.com/util-deprecate/1.0.2
dev: false
registry.npmmirror.com/readdirp/3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz}
name: readdirp
@@ -10780,23 +10619,6 @@ packages:
version: 1.2.0
dev: false
registry.npmmirror.com/sharp/0.31.3:
resolution: {integrity: sha512-XcR4+FCLBFKw1bdB+GEhnUNXNXvnt0tDo4WsBsraKymuo/IAuPuCBVAL2wIkUw2r/dwFW5Q5+g66Kwl2dgDFVg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/sharp/-/sharp-0.31.3.tgz}
name: sharp
version: 0.31.3
engines: {node: '>=14.15.0'}
requiresBuild: true
dependencies:
color: registry.npmmirror.com/color/4.2.3
detect-libc: registry.npmmirror.com/detect-libc/2.0.1
node-addon-api: registry.npmmirror.com/node-addon-api/5.1.0
prebuild-install: registry.npmmirror.com/prebuild-install/7.1.1
semver: registry.npmmirror.com/semver/7.3.8
simple-get: registry.npmmirror.com/simple-get/4.0.1
tar-fs: registry.npmmirror.com/tar-fs/2.1.1
tunnel-agent: registry.npmmirror.com/tunnel-agent/0.6.0
dev: false
registry.npmmirror.com/shebang-command/2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz}
name: shebang-command
@@ -10834,30 +10656,6 @@ packages:
version: 3.0.7
dev: true
registry.npmmirror.com/simple-concat/1.0.1:
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz}
name: simple-concat
version: 1.0.1
dev: false
registry.npmmirror.com/simple-get/4.0.1:
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/simple-get/-/simple-get-4.0.1.tgz}
name: simple-get
version: 4.0.1
dependencies:
decompress-response: registry.npmmirror.com/decompress-response/6.0.0
once: registry.npmmirror.com/once/1.4.0
simple-concat: registry.npmmirror.com/simple-concat/1.0.1
dev: false
registry.npmmirror.com/simple-swizzle/0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz}
name: simple-swizzle
version: 0.2.2
dependencies:
is-arrayish: registry.npmmirror.com/is-arrayish/0.3.2
dev: false
registry.npmmirror.com/slash/3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz}
name: slash
@@ -11103,14 +10901,6 @@ packages:
safe-buffer: registry.npmmirror.com/safe-buffer/5.1.2
dev: false
registry.npmmirror.com/string_decoder/1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz}
name: string_decoder
version: 1.3.0
dependencies:
safe-buffer: registry.npmmirror.com/safe-buffer/5.2.1
dev: false
registry.npmmirror.com/strip-ansi/6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz}
name: strip-ansi
@@ -11143,13 +10933,6 @@ packages:
engines: {node: '>=12'}
dev: true
registry.npmmirror.com/strip-json-comments/2.0.1:
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz}
name: strip-json-comments
version: 2.0.1
engines: {node: '>=0.10.0'}
dev: false
registry.npmmirror.com/strip-json-comments/3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz}
name: strip-json-comments
@@ -11260,30 +11043,6 @@ packages:
engines: {node: '>=6'}
dev: true
registry.npmmirror.com/tar-fs/2.1.1:
resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.1.tgz}
name: tar-fs
version: 2.1.1
dependencies:
chownr: registry.npmmirror.com/chownr/1.1.4
mkdirp-classic: registry.npmmirror.com/mkdirp-classic/0.5.3
pump: registry.npmmirror.com/pump/3.0.0
tar-stream: registry.npmmirror.com/tar-stream/2.2.0
dev: false
registry.npmmirror.com/tar-stream/2.2.0:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz}
name: tar-stream
version: 2.2.0
engines: {node: '>=6'}
dependencies:
bl: registry.npmmirror.com/bl/4.1.0
end-of-stream: registry.npmmirror.com/end-of-stream/1.4.4
fs-constants: registry.npmmirror.com/fs-constants/1.0.0
inherits: registry.npmmirror.com/inherits/2.0.4
readable-stream: registry.npmmirror.com/readable-stream/3.6.1
dev: false
registry.npmmirror.com/text-table/0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz}
name: text-table
@@ -11424,14 +11183,6 @@ packages:
tslib: registry.npmmirror.com/tslib/1.14.1
dev: false
registry.npmmirror.com/tunnel-agent/0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz}
name: tunnel-agent
version: 0.6.0
dependencies:
safe-buffer: registry.npmmirror.com/safe-buffer/5.2.1
dev: false
registry.npmmirror.com/tunnel/0.0.6:
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/tunnel/-/tunnel-0.0.6.tgz}
name: tunnel

View File

@@ -14,14 +14,13 @@
如果使用了自己的 Api Key不会计费。可以在账号页看到详细账单。
| 计费项 | 价格: 元/ 1K tokens包含上下文|
| --- | --- |
| claude - 对话 | 免费 |
| 知识库 - 索引 | 免费 |
| chatgpt - 对话 | 0.025 |
| gpt4 - 对话 | 0.5 |
| 文件拆分 | 0.025 |
**其他问题**
请 WX 联系: fastgpt123
请 WX 联系: YNyiqi
| 交流群 | 小助手 |
| ----------------------- | -------------------- |
| ![](https://otnvvf-imgs.oss.laf.run/wxqun300.jpg) | ![](https://otnvvf-imgs.oss.laf.run/wx300.jpg) |

View File

@@ -18,7 +18,6 @@ FastGpt 项目完全开源,可随意私有化部署,去除平台风险忧虑
如果使用了自己的 Api Key不会计费。可以在账号页看到详细账单。
| 计费项 | 价格: 元/ 1K tokens包含上下文|
| --- | --- |
| claude - 对话 | 免费 |
| 知识库 - 索引 | 免费 |
| chatgpt - 对话 | 0.025 |
| gpt4 - 对话 | 0.5 |
@@ -27,7 +26,7 @@ FastGpt 项目完全开源,可随意私有化部署,去除平台风险忧虑
### 交流群/问题反馈
如果群满了,可加个小助手,定时拉
wx 号: fastgpt123
wx 号: YNyiqi
| 交流群 | 小助手 |
| ------------------------------------------------- | ---------------------------------------------- |

View File

@@ -1,4 +1,10 @@
### Fast GPT V3.8
### Fast GPT V3.8.1
- 新增 - 知识库引用反馈
- 新增 - 知识库与 AI 助手对多对关系,一个知识库可以被多个 AI 助手关联,一个 AI 助手可以关联多个知识库。
1. 新增 - 自定义历史记录标题
2. 新增 - 置顶历史记录。
3. 新增 - 自动置顶最近聊天的模型。
4. 优化 - 索引和 QA 队列,支持多节点和并发(目前线上大概 2500 条/分钟)
5. 优化 - 随机任务,不再按线性等待。
6. 优化 - 内容分段导入和进度查看,不再担心大文件无法导入
7. 优化 - 导出的 csv 大小。最大支持 100M 导出。
8. 知识库数量说明,由于线上数据太多,没法创建索引,所以目前如果超过 5w 组数据,大概率会失败。

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -1,11 +1,12 @@
import { GET, POST, DELETE } from './request';
import { GET, POST, DELETE, PUT } from './request';
import type { HistoryItemType } from '@/types/chat';
import type { InitChatResponse, InitShareChatResponse } from './response/chat';
import { RequestPaging } from '../types/index';
import type { ShareChatSchema } from '@/types/mongoSchema';
import type { ShareChatEditType } from '@/types/model';
import { Obj2Query } from '@/utils/tools';
import { QuoteItemType } from '@/pages/api/openapi/kb/appKbSearch';
import type { QuoteItemType } from '@/pages/api/openapi/kb/appKbSearch';
import type { Props as UpdateHistoryProps } from '@/pages/api/chat/history/updateChatHistory';
/**
* 获取初始化聊天内容
@@ -17,7 +18,7 @@ export const getInitChatSiteInfo = (modelId: '' | string, chatId: '' | string) =
* 获取历史记录
*/
export const getChatHistory = (data: RequestPaging) =>
POST<HistoryItemType[]>('/chat/getHistory', data);
POST<HistoryItemType[]>('/chat/history/getHistory', data);
/**
* 删除一条历史记录
@@ -28,7 +29,7 @@ export const delChatHistoryById = (id: string) => GET(`/chat/removeHistory?id=${
* get history quotes
*/
export const getHistoryQuote = (params: { chatId: string; historyId: string }) =>
GET<(QuoteItemType & { _id: string })[]>(`/chat/getHistoryQuote`, params);
GET<(QuoteItemType & { _id: string })[]>(`/chat/history/getHistoryQuote`, params);
/**
* update history quote status
@@ -37,7 +38,7 @@ export const updateHistoryQuote = (params: {
chatId: string;
historyId: string;
quoteId: string;
}) => GET(`/chat/updateHistoryQuote`, params);
}) => GET(`/chat/history/updateHistoryQuote`, params);
/**
* 删除一句对话
@@ -45,6 +46,12 @@ export const updateHistoryQuote = (params: {
export const delChatRecordByIndex = (chatId: string, contentId: string) =>
DELETE(`/chat/delChatRecordByContentId?chatId=${chatId}&contentId=${contentId}`);
/**
* 修改历史记录: 标题/置顶
*/
export const putChatHistory = (data: UpdateHistoryProps) =>
PUT('/chat/history/updateChatHistory', data);
/**
* create a shareChat
*/
@@ -55,7 +62,7 @@ export const createShareChat = (
) => POST<string>(`/chat/shareChat/create`, data);
/**
* get shareChat
* get shareChat
*/
export const getShareChatList = (modelId: string) =>
GET<ShareChatSchema[]>(`/chat/shareChat/list?modelId=${modelId}`);

View File

@@ -50,7 +50,7 @@ export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchPr
read();
} catch (err: any) {
if (err?.message === 'The user aborted a request.') {
return resolve({ responseText, newChatId, quoteLen: 0, systemPrompt: '' });
return resolve({ responseText, newChatId, quoteLen, systemPrompt: '' });
}
reject(typeof err === 'string' ? err : err?.message || '请求异常');
}

View File

@@ -16,6 +16,7 @@ export interface InitChatResponse {
export interface InitShareChatResponse {
maxContext: number;
userAvatar: string;
model: {
name: string;
avatar: string;

View File

@@ -1,16 +1,18 @@
import React from 'react';
import { Image } from '@chakra-ui/react';
import type { ImageProps } from '@chakra-ui/react';
import { LOGO_ICON } from '@/constants/chat';
const Avatar = ({ w = '30px', ...props }: ImageProps) => {
return (
<Image
fallbackSrc="/icon/logo.png"
fallbackSrc={LOGO_ICON}
borderRadius={'50%'}
objectFit={'cover'}
alt=""
w={w}
h={w}
p={'1px'}
{...props}
/>
);

View File

@@ -5,6 +5,7 @@ import MyIcon from '../Icon';
import { useUserStore } from '@/store/user';
import { useChatStore } from '@/store/chat';
import Avatar from '../Avatar';
import { HUMAN_ICON } from '@/constants/chat';
export enum NavbarTypeEnum {
normal = 'normal',
@@ -83,7 +84,7 @@ const Navbar = () => {
cursor={'pointer'}
onClick={() => router.push('/number')}
>
<Avatar w={'36px'} h={'36px'} src={userInfo?.avatar} fallbackSrc={'/icon/human.png'} />
<Avatar w={'36px'} h={'36px'} src={userInfo?.avatar} fallbackSrc={HUMAN_ICON} />
</Box>
{/* 导航列表 */}
<Box flex={1}>

View File

@@ -31,7 +31,7 @@ const WxConcat = ({ onClose }: { onClose: () => void }) => {
<Box mt={2}>
:
<Box as={'span'} userSelect={'all'}>
fastgpt123
YNyiqi
</Box>
</Box>
</ModalBody>

File diff suppressed because one or more lines are too long

91
src/hooks/useEditInfo.tsx Normal file
View File

@@ -0,0 +1,91 @@
import React, { useCallback, useRef } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalFooter,
ModalBody,
ModalCloseButton,
Input,
useDisclosure,
Button
} from '@chakra-ui/react';
export const useEditInfo = ({
title,
placeholder = ''
}: {
title: string;
placeholder?: string;
}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const inputRef = useRef<HTMLInputElement | null>(null);
const onSuccessCb = useRef<(content: string) => void | Promise<void>>();
const onErrorCb = useRef<(err: any) => void>();
const defaultValue = useRef('');
const onOpenModal = useCallback(
({
defaultVal,
onSuccess,
onError
}: {
defaultVal: string;
onSuccess: (content: string) => any;
onError?: (err: any) => void;
}) => {
onOpen();
onSuccessCb.current = onSuccess;
onErrorCb.current = onError;
defaultValue.current = defaultVal;
},
[onOpen]
);
const onclickConfirm = useCallback(async () => {
if (!inputRef.current) return;
try {
const val = inputRef.current.value;
await onSuccessCb.current?.(val);
onClose();
} catch (err) {
onErrorCb.current?.(err);
}
}, [onClose]);
// eslint-disable-next-line react/display-name
const EditModal = useCallback(
() => (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>{title}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Input
ref={inputRef}
defaultValue={defaultValue.current}
placeholder={placeholder}
autoFocus
maxLength={20}
/>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'outline'} onClick={onClose}>
</Button>
<Button onClick={onclickConfirm}></Button>
</ModalFooter>
</ModalContent>
</Modal>
),
[isOpen, onClose, onclickConfirm, placeholder, title]
);
return {
onOpenModal,
EditModal
};
};

View File

@@ -50,6 +50,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// 读取对话内容
const prompts = [...content, prompt[0]];
const {
code = 200,
systemPrompts = [],
@@ -61,7 +62,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { code, searchPrompts, rawSearch, guidePrompt } = await appKbSearch({
model,
userId,
prompts,
fixedQuote: content[content.length - 1]?.quote || [],
prompt: prompt[0],
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity
});
@@ -114,7 +116,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.end(response);
}
prompts.splice(prompts.length - 3, 0, ...systemPrompts);
prompts.unshift(...systemPrompts);
// content check
await sensitiveCheck({
@@ -127,7 +129,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
);
// 发出 chat 请求
const { streamResponse } = await modelServiceToolMap[model.chat.chatModel].chatCompletion({
const { streamResponse, responseMessages } = await modelServiceToolMap[
model.chat.chatModel
].chatCompletion({
apiKey: userOpenAiKey || systemAuthKey,
temperature: +temperature,
messages: prompts,
@@ -145,7 +149,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
model: model.chat.chatModel,
res,
chatResponse: streamResponse,
prompts
prompts: responseMessages
});
// save chat

View File

@@ -0,0 +1,39 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import type { HistoryItemType } from '@/types/chat';
/* 获取历史记录 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
const data = await Chat.find(
{
userId
},
'_id title top customTitle modelId updateTime latestChat'
)
.sort({ top: -1, updateTime: -1 })
.limit(20);
jsonRes<HistoryItemType[]>(res, {
data: data.map((item) => ({
_id: item._id,
updateTime: item.updateTime,
modelId: item.modelId,
title: item.customTitle || item.title,
latestChat: item.latestChat,
top: item.top
}))
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -3,25 +3,32 @@ import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
/* 获取历史记录 */
export type Props = {
chatId: '' | string;
customTitle?: string;
top?: boolean;
};
/* 更新聊天标题 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { chatId, customTitle, top } = req.body as Props;
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
const data = await Chat.find(
await Chat.findOneAndUpdate(
{
_id: chatId,
userId
},
'_id title modelId updateTime latestChat'
)
.sort({ updateTime: -1 })
.limit(20);
jsonRes(res, {
data
});
{
...(customTitle ? { customTitle } : {}),
...(top ? { top } : { top: null })
}
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,

View File

@@ -27,7 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
{
$set: {
'content.$.quote.$[quoteElem].isEdit': true
'content.$.quote.$[quoteElem].source': '手动修改'
}
},
{

View File

@@ -1,7 +1,7 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { ChatItemType } from '@/types/chat';
import { connectToDatabase, Chat } from '@/service/mongo';
import { connectToDatabase, Chat, Model } from '@/service/mongo';
import { authModel } from '@/service/utils/auth';
import { authUser } from '@/service/utils/auth';
import mongoose from 'mongoose';
@@ -51,42 +51,51 @@ export async function saveChat({
userId
}: Props & { userId: string }) {
await connectToDatabase();
await authModel({ modelId, userId, authOwner: false });
const { model } = await authModel({ modelId, userId, authOwner: false });
const content = prompts.map((item) => ({
_id: item._id ? new mongoose.Types.ObjectId(item._id) : undefined,
obj: item.obj,
value: item.value,
systemPrompt: item.systemPrompt,
quote:
item.quote?.map((item) => ({
...item,
isEdit: false
})) || []
quote: item.quote || []
}));
// 没有 chatId, 创建一个对话
if (!chatId) {
const { _id } = await Chat.create({
_id: newChatId ? new mongoose.Types.ObjectId(newChatId) : undefined,
userId,
modelId,
content,
title: content[0].value.slice(0, 20),
latestChat: content[1].value
});
return _id;
} else {
// 已经有记录,追加入库
await Chat.findByIdAndUpdate(chatId, {
$push: {
content: {
$each: content
}
},
title: content[0].value.slice(0, 20),
latestChat: content[1].value,
updateTime: new Date()
});
}
const [id] = await Promise.all([
...(chatId // update chat
? [
Chat.findByIdAndUpdate(chatId, {
$push: {
content: {
$each: content
}
},
title: content[0].value.slice(0, 20),
latestChat: content[1].value,
updateTime: new Date()
}).then(() => '')
]
: [
Chat.create({
_id: newChatId ? new mongoose.Types.ObjectId(newChatId) : undefined,
userId,
modelId,
content,
title: content[0].value.slice(0, 20),
latestChat: content[1].value
}).then((res) => res._id)
]),
// update model
...(String(model.userId) === userId
? [
Model.findByIdAndUpdate(modelId, {
updateTime: new Date()
})
]
: [])
]);
return {
id
};
}

View File

@@ -47,7 +47,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const { code, searchPrompts } = await appKbSearch({
model,
userId,
prompts,
fixedQuote: [],
prompt: prompts[prompts.length - 1],
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity
});
@@ -74,7 +75,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.send(systemPrompts[0]?.value);
}
prompts.splice(prompts.length - 3, 0, ...systemPrompts);
prompts.unshift(...systemPrompts);
// content check
await sensitiveCheck({
@@ -87,7 +88,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
);
// 发出请求
const { streamResponse } = await modelServiceToolMap[model.chat.chatModel].chatCompletion({
const { streamResponse, responseMessages } = await modelServiceToolMap[
model.chat.chatModel
].chatCompletion({
apiKey: userOpenAiKey || systemAuthKey,
temperature: +temperature,
messages: prompts,
@@ -105,7 +108,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
model: model.chat.chatModel,
res,
chatResponse: streamResponse,
prompts
prompts: responseMessages
});
res.end();

View File

@@ -1,9 +1,10 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, ShareChat } from '@/service/mongo';
import { connectToDatabase, ShareChat, User } from '@/service/mongo';
import type { InitShareChatResponse } from '@/api/response/chat';
import { authModel } from '@/service/utils/auth';
import { hashPassword } from '@/service/utils/tools';
import { HUMAN_ICON } from '@/constants/chat';
/* 初始化我的聊天框,需要身份验证 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -40,9 +41,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
authOwner: false
});
const user = await User.findById(shareChat.userId, 'avatar');
jsonRes<InitShareChatResponse>(res, {
data: {
maxContext: shareChat.maxContext,
userAvatar: user?.avatar || HUMAN_ICON,
model: {
name: model.name,
avatar: model.avatar,

View File

@@ -20,7 +20,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
},
'_id avatar name chat.systemPrompt'
).sort({
_id: -1
updateTime: -1
}),
Collection.find({ userId })
.populate({

View File

@@ -75,10 +75,11 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
// 使用了知识库搜索
if (model.chat.relatedKbs.length > 0) {
const { code, searchPrompts } = await appKbSearch({
prompts,
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity,
model,
userId
userId,
fixedQuote: [],
prompt: prompts[prompts.length - 1],
similarity: ModelVectorSearchModeMap[model.chat.searchMode]?.similarity
});
// search result is empty
@@ -101,7 +102,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
];
}
prompts.splice(prompts.length - 3, 0, ...systemPrompts);
prompts.unshift(...systemPrompts);
// content check
await sensitiveCheck({
@@ -139,7 +140,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
model: model.chat.chatModel,
res,
chatResponse: streamResponse,
prompts
prompts: responseMessages
});
res.end();
return {

View File

@@ -12,7 +12,7 @@ import { ChatRoleEnum } from '@/constants/chat';
import { openaiEmbedding } from '../plugin/openaiEmbedding';
import { modelToolMap } from '@/utils/plugin';
export type QuoteItemType = { id: string; q: string; a: string; isEdit: boolean };
export type QuoteItemType = { id: string; q: string; a: string; source?: string };
type Props = {
prompts: ChatItemSimpleType[];
similarity: number;
@@ -49,10 +49,11 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
});
const result = await appKbSearch({
model,
userId,
prompts,
similarity,
model
fixedQuote: [],
prompt: prompts[prompts.length - 1],
similarity
});
jsonRes<Response>(res, {
@@ -70,67 +71,53 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
export async function appKbSearch({
model,
userId,
prompts,
fixedQuote,
prompt,
similarity
}: {
userId: string;
prompts: ChatItemSimpleType[];
similarity: number;
model: ModelSchema;
userId: string;
fixedQuote: QuoteItemType[];
prompt: ChatItemSimpleType;
similarity: number;
}): Promise<Response> {
const modelConstantsData = ChatModelMap[model.chat.chatModel];
// search two times.
const userPrompts = prompts.filter((item) => item.obj === 'Human');
const input: string[] = [
userPrompts[userPrompts.length - 1].value,
userPrompts[userPrompts.length - 2]?.value
].filter((item) => item);
// get vector
const promptVectors = await openaiEmbedding({
const promptVector = await openaiEmbedding({
userId,
input,
input: [prompt.value],
type: 'chat'
});
// search kb
const searchRes = await Promise.all(
promptVectors.map((promptVector) =>
PgClient.select<QuoteItemType>('modelData', {
fields: ['id', 'q', 'a'],
where: [
`kb_id IN (${model.chat.relatedKbs.map((item) => `'${item}'`).join(',')})`,
'AND',
`vector <=> '[${promptVector}]' < ${similarity}`
],
order: [{ field: 'vector', mode: `<=> '[${promptVector}]'` }],
limit: promptVectors.length === 1 ? 15 : 10
}).then((res) => res.rows)
)
);
const { rows: searchRes } = await PgClient.select<QuoteItemType>('modelData', {
fields: ['id', 'q', 'a', 'source'],
where: [
`kb_id IN (${model.chat.relatedKbs.map((item) => `'${item}'`).join(',')})`,
'AND',
`vector <=> '[${promptVector[0]}]' < ${similarity}`
],
order: [{ field: 'vector', mode: `<=> '[${promptVector[0]}]'` }],
limit: 8
});
// filter same search result
const idSet = new Set<string>();
const filterSearch = searchRes.map((search) =>
search.filter((item) => {
if (idSet.has(item.id)) {
return false;
}
idSet.add(item.id);
return true;
})
);
const filterSearch = [
...searchRes.slice(0, 3),
...fixedQuote.slice(0, 2),
...searchRes.slice(3),
...fixedQuote.slice(2, 5)
].filter((item) => {
if (idSet.has(item.id)) {
return false;
}
idSet.add(item.id);
return true;
});
// slice search result by rate.
const sliceRateMap: Record<number, number[]> = {
1: [1],
2: [0.7, 0.3]
};
const sliceRate = sliceRateMap[searchRes.length] || sliceRateMap[0];
// 计算固定提示词的 token 数量
const guidePrompt = model.chat.systemPrompt // user system prompt
? {
obj: ChatRoleEnum.System,
@@ -154,24 +141,21 @@ export async function appKbSearch({
const fixedSystemTokens = modelToolMap[model.chat.chatModel].countTokens({
messages: [guidePrompt]
});
const maxTokens = modelConstantsData.systemMaxToken - fixedSystemTokens;
const sliceResult = sliceRate.map((rate, i) =>
modelToolMap[model.chat.chatModel]
.tokenSlice({
maxToken: Math.round(maxTokens * rate),
messages: filterSearch[i].map((item) => ({
obj: ChatRoleEnum.System,
value: `${item.q}\n${item.a}`
}))
})
.map((item) => item.value)
);
const sliceResult = modelToolMap[model.chat.chatModel]
.tokenSlice({
maxToken: modelConstantsData.systemMaxToken - fixedSystemTokens,
messages: filterSearch.map((item) => ({
obj: ChatRoleEnum.System,
value: `${item.q}\n${item.a}`
}))
})
.map((item) => item.value);
// slice filterSearch
const sliceSearch = filterSearch.map((item, i) => item.slice(0, sliceResult[i].length)).flat();
const rawSearch = filterSearch.slice(0, sliceResult.length);
// system prompt
const systemPrompt = sliceResult.flat().join('\n').trim();
const systemPrompt = sliceResult.join('\n').trim();
/* 高相似度+不回复 */
if (!systemPrompt && model.chat.searchMode === appVectorSearchModeEnum.hightSimilarity) {
@@ -206,7 +190,7 @@ export async function appKbSearch({
return {
code: 200,
rawSearch: sliceSearch,
rawSearch,
guidePrompt: guidePrompt.value || '',
searchPrompts: [
{

View File

@@ -1,5 +1,4 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { KbDataItemType } from '@/types/plugin';
import { jsonRes } from '@/service/response';
import { connectToDatabase, TrainingData } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
@@ -9,9 +8,11 @@ import { TrainingModeEnum } from '@/constants/plugin';
import { startQueue } from '@/service/utils/tools';
import { PgClient } from '@/service/pg';
type DateItemType = { a: string; q: string; source?: string };
export type Props = {
kbId: string;
data: { a: KbDataItemType['a']; q: KbDataItemType['q'] }[];
data: DateItemType[];
mode: `${TrainingModeEnum}`;
prompt?: string;
};
@@ -63,10 +64,7 @@ export async function pushDataToKb({
// 过滤重复的 qa 内容
const set = new Set();
const filterData: {
a: string;
q: string;
}[] = [];
const filterData: DateItemType[] = [];
data.forEach((item) => {
const text = item.q + item.a;
@@ -79,11 +77,12 @@ export async function pushDataToKb({
// 数据库去重
const insertData = (
await Promise.allSettled(
filterData.map(async ({ q, a = '' }) => {
filterData.map(async ({ q, a = '', source }) => {
if (mode !== TrainingModeEnum.index) {
return Promise.resolve({
q,
a
a,
source
});
}
@@ -112,19 +111,21 @@ export async function pushDataToKb({
}
return Promise.resolve({
q,
a
a,
source
});
})
)
)
.filter((item) => item.status === 'fulfilled')
.map<{ q: string; a: string }>((item: any) => item.value);
.map<DateItemType>((item: any) => item.value);
// 插入记录
await TrainingData.insertMany(
insertData.map((item) => ({
q: item.q,
a: item.a,
source: item.source,
userId,
kbId,
mode,

View File

@@ -32,6 +32,7 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
await PgClient.update('modelData', {
where: [['id', dataId], 'AND', ['user_id', userId]],
values: [
{ key: 'source', value: '手动修改' },
{ key: 'a', value: a.replace(/'/g, '"') },
...(q
? [

View File

@@ -3,7 +3,7 @@ import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
import type { PgKBDataItemType } from '@/types/pg';
import type { KbDataItemType } from '@/types/plugin';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -21,8 +21,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
const where: any = [['user_id', userId], 'AND', ['id', dataId]];
const searchRes = await PgClient.select<PgKBDataItemType>('modelData', {
fields: ['id', 'q', 'a'],
const searchRes = await PgClient.select<KbDataItemType>('modelData', {
fields: ['id', 'q', 'a', 'source'],
where,
limit: 1
});

View File

@@ -3,7 +3,7 @@ import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
import type { PgKBDataItemType } from '@/types/pg';
import type { KbDataItemType } from '@/types/plugin';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -31,11 +31,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
['user_id', userId],
'AND',
['kb_id', kbId],
...(searchText ? ['AND', `(q LIKE '%${searchText}%' OR a LIKE '%${searchText}%')`] : [])
...(searchText
? [
'AND',
`(q LIKE '%${searchText}%' OR a LIKE '%${searchText}%' OR source LIKE '%${searchText}%')`
]
: [])
];
const searchRes = await PgClient.select<PgKBDataItemType>('modelData', {
fields: ['id', 'q', 'a'],
const searchRes = await PgClient.select<KbDataItemType>('modelData', {
fields: ['id', 'q', 'a', 'source'],
where,
order: [{ field: 'id', mode: 'DESC' }],
limit: pageSize,

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { Card, Box, Flex } from '@chakra-ui/react';
import { useMarkdown } from '@/hooks/useMarkdown';
import Markdown from '@/components/Markdown';
import { LOGO_ICON } from '@/constants/chat';
import Avatar from '@/components/Avatar';
const Empty = ({

View File

@@ -16,14 +16,16 @@ import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import { useLoading } from '@/hooks/useLoading';
import { useUserStore } from '@/store/user';
import { formatTimeToChatTime } from '@/utils/tools';
import MyIcon from '@/components/Icon';
import type { HistoryItemType, ExportChatType } from '@/types/chat';
import { useChatStore } from '@/store/chat';
import ModelList from './ModelList';
import { useGlobalStore } from '@/store/global';
import styles from '../index.module.scss';
import { useEditInfo } from '@/hooks/useEditInfo';
import { putChatHistory } from '@/api/chat';
import { useToast } from '@/hooks/useToast';
import { formatTimeToChatTime, getErrText } from '@/utils/tools';
const PcSliderBar = ({
onclickDelHistory,
@@ -33,12 +35,13 @@ const PcSliderBar = ({
onclickExportChat: (type: ExportChatType) => void;
}) => {
const router = useRouter();
const { toast } = useToast();
const { modelId = '', chatId = '' } = router.query as { modelId: string; chatId: string };
const ContextMenuRef = useRef(null);
const theme = useTheme();
const { isPc } = useGlobalStore();
const ContextMenuRef = useRef(null);
const { Loading, setIsLoading } = useLoading();
const [contextMenuData, setContextMenuData] = useState<{
left: number;
@@ -52,7 +55,12 @@ const PcSliderBar = ({
() => [...myModels, ...myCollectionModels],
[myCollectionModels, myModels]
);
useQuery(['loadModels'], () => loadMyModels(false));
// custom title edit
const { onOpenModal, EditModal: EditTitleModal } = useEditInfo({
title: '自定义历史记录标题',
placeholder: '如果设置为空,会自动跟随聊天记录。'
});
// close contextMenu
useOutsideClick({
@@ -60,13 +68,9 @@ const PcSliderBar = ({
handler: () =>
setTimeout(() => {
setContextMenuData(undefined);
})
}, 10)
});
const { isLoading: isLoadingHistory } = useQuery(['loadingHistory'], () =>
loadHistory({ pageNum: 1 })
);
const onclickContextMenu = useCallback(
(e: MouseEvent<HTMLDivElement>, history: HistoryItemType) => {
e.preventDefault(); // 阻止默认右键菜单
@@ -82,6 +86,12 @@ const PcSliderBar = ({
[isPc]
);
useQuery(['loadModels'], () => loadMyModels(false));
const { isLoading: isLoadingHistory } = useQuery(['loadingHistory'], () =>
loadHistory({ pageNum: 1 })
);
return (
<Flex
position={'relative'}
@@ -149,14 +159,16 @@ const PcSliderBar = ({
borderLeft={['none', '5px solid transparent']}
userSelect={'none'}
_hover={{
backgroundColor: ['', '#dee0e3']
bg: ['', '#dee0e3']
}}
{...(item._id === chatId
? {
backgroundColor: '#eff0f1',
bg: 'myGray.100 !important',
borderLeftColor: 'myBlue.600 !important'
}
: {})}
: {
bg: item.top ? 'myBlue.200' : ''
})}
onClick={() => {
if (item._id === chatId) return;
if (isPc) {
@@ -216,6 +228,19 @@ const PcSliderBar = ({
<Box ref={ContextMenuRef}></Box>
<Menu isOpen>
<MenuList>
<MenuItem
onClick={async () => {
try {
await putChatHistory({
chatId: contextMenuData.history._id,
top: !contextMenuData.history.top
});
loadHistory({ pageNum: 1, init: true });
} catch (error) {}
}}
>
{contextMenuData.history.top ? '取消置顶' : '置顶'}
</MenuItem>
<MenuItem
onClick={async () => {
setIsLoading(true);
@@ -232,6 +257,33 @@ const PcSliderBar = ({
>
</MenuItem>
<MenuItem
onClick={() =>
onOpenModal({
defaultVal: contextMenuData.history.title,
onSuccess: async (val: string) => {
await putChatHistory({
chatId: contextMenuData.history._id,
customTitle: val,
top: contextMenuData.history.top
});
toast({
title: '自定义标题成功',
status: 'success'
});
loadHistory({ pageNum: 1, init: true });
},
onError(err) {
toast({
title: getErrText(err),
status: 'error'
});
}
})
}
>
</MenuItem>
<MenuItem onClick={() => onclickExportChat('html')}>HTML格式</MenuItem>
<MenuItem onClick={() => onclickExportChat('pdf')}>PDF格式</MenuItem>
<MenuItem onClick={() => onclickExportChat('md')}>Markdown格式</MenuItem>
@@ -239,7 +291,7 @@ const PcSliderBar = ({
</Menu>
</Box>
)}
<EditTitleModal />
<Loading loading={isLoadingHistory} fixed={false} />
</Flex>
);

View File

@@ -126,7 +126,7 @@ const QuoteModal = ({
position={'relative'}
_hover={{ '& .edit': { display: 'flex' } }}
>
{item.isEdit && <Box color={'myGray.600'}>()</Box>}
{item.source && <Box color={'myGray.600'}>({item.source})</Box>}
<Box>{item.q}</Box>
<Box>{item.a}</Box>
<Box

View File

@@ -36,7 +36,6 @@ import { streamFetch } from '@/api/fetch';
import MyIcon from '@/components/Icon';
import { throttle } from 'lodash';
import { Types } from 'mongoose';
import { LOGO_ICON } from '@/constants/chat';
import { ChatModelMap } from '@/constants/model';
import { useChatStore } from '@/store/chat';
import { useLoading } from '@/hooks/useLoading';
@@ -49,6 +48,7 @@ import SideBar from '@/components/SideBar';
import Avatar from '@/components/Avatar';
import Empty from './components/Empty';
import QuoteModal from './components/QuoteModal';
import { HUMAN_ICON } from '@/constants/chat';
const PhoneSliderBar = dynamic(() => import('./components/PhoneSliderBar'), {
ssr: false
@@ -105,7 +105,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
const { copyData } = useCopyData();
const { isPc } = useGlobalStore();
const { Loading, setIsLoading } = useLoading();
const { userInfo } = useUserStore();
const { userInfo, loadMyModels } = useUserStore();
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
// close contextMenu
@@ -211,8 +211,6 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
router.replace(`/chat?modelId=${modelId}&chatId=${newChatId}`);
}
abortSignal.signal.aborted && (await delay(600));
// 设置聊天内容为完成状态
setChatData((state) => ({
...state,
@@ -228,13 +226,21 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
})
}));
// refresh history
setTimeout(() => {
loadHistory({ pageNum: 1, init: true });
generatingMessage();
}, 100);
// refresh data
generatingMessage();
loadHistory({ pageNum: 1, init: true });
loadMyModels(true);
},
[chatId, setForbidLoadChatData, generatingMessage, loadHistory, modelId, router, setChatData]
[
chatId,
modelId,
setChatData,
loadHistory,
loadMyModels,
generatingMessage,
setForbidLoadChatData,
router
]
);
/**
@@ -458,14 +464,14 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
async ({
modelId,
chatId,
isLoading = false
loading = false
}: {
modelId: string;
chatId: string;
isLoading?: boolean;
loading?: boolean;
}) => {
isLoading && setIsLoading(true);
try {
loading && setIsLoading(true);
const res = await getInitChatSiteInfo(modelId, chatId);
setChatData({
@@ -500,18 +506,18 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
return null;
},
[
router,
loadHistory,
setForbidLoadChatData,
scrollToBottom,
setChatData,
setIsLoading,
setChatData,
scrollToBottom,
setForbidLoadChatData,
router,
setLastChatModelId,
setLastChatId,
setLastChatModelId
loadHistory
]
);
// 初始化聊天框
const { isLoading } = useQuery(['init', modelId, chatId], () => {
useQuery(['init', modelId, chatId], () => {
// pc: redirect to latest model chat
if (!modelId && lastChatModelId) {
router.replace(`/chat?modelId=${lastChatModelId}&chatId=${lastChatId}`);
@@ -529,7 +535,8 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
return loadChatInfo({
modelId,
chatId
chatId,
loading: true
});
});
@@ -704,8 +711,8 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
className="avatar"
src={
item.obj === 'Human'
? userInfo?.avatar || '/icon/human.png'
: chatData.model.avatar || LOGO_ICON
? userInfo?.avatar || HUMAN_ICON
: chatData.model.avatar
}
w={['20px', '34px']}
h={['20px', '34px']}
@@ -874,7 +881,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
</Box>
)}
<Loading loading={isLoading} fixed={false} />
<Loading fixed={false} />
</Flex>
)}

View File

@@ -38,7 +38,6 @@ import { streamFetch } from '@/api/fetch';
import MyIcon from '@/components/Icon';
import { throttle } from 'lodash';
import { Types } from 'mongoose';
import { LOGO_ICON } from '@/constants/chat';
import { useChatStore } from '@/store/chat';
import { useLoading } from '@/hooks/useLoading';
import { fileDownload } from '@/utils/file';
@@ -49,6 +48,7 @@ import Markdown from '@/components/Markdown';
import SideBar from '@/components/SideBar';
import Avatar from '@/components/Avatar';
import Empty from './components/Empty';
import { HUMAN_ICON } from '@/constants/chat';
const ShareHistory = dynamic(() => import('./components/ShareHistory'), {
loading: () => <Loading fixed={false} />,
@@ -101,7 +101,6 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
const { copyData } = useCopyData();
const { isPc } = useGlobalStore();
const { Loading, setIsLoading } = useLoading();
const { userInfo } = useUserStore();
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: onOpenSlider } = useDisclosure();
const {
isOpen: isOpenPassword,
@@ -628,8 +627,8 @@ const Chat = ({ shareId, historyId }: { shareId: string; historyId: string }) =>
<Avatar
src={
item.obj === 'Human'
? userInfo?.avatar || '/icon/human.png'
: shareChatData.model.avatar || LOGO_ICON
? shareChatData.userAvatar || HUMAN_ICON
: shareChatData.model.avatar
}
w={['20px', '34px']}
h={['20px', '34px']}

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useState, useRef } from 'react';
import React, { useCallback, useState, useRef, useEffect } from 'react';
import {
Box,
TableContainer,
@@ -56,7 +56,7 @@ const DataCard = ({ kbId }: { kbId: string }) => {
const { toast } = useToast();
const {
data: modelDataList,
data: kbDataList,
isLoading,
Pagination,
total,
@@ -72,11 +72,6 @@ const DataCard = ({ kbId }: { kbId: string }) => {
defaultRequest: false
});
useQuery(['getKbData', kbId], () => {
getData(1);
return null;
});
const [editInputData, setEditInputData] = useState<InputDataType>();
const {
@@ -101,20 +96,14 @@ const DataCard = ({ kbId }: { kbId: string }) => {
);
const refetchData = useCallback(
(num = 1) => {
(num = pageNum) => {
getData(num);
refetch();
return null;
},
[getData, refetch]
[getData, pageNum, refetch]
);
// interval get data
useQuery(['refetchData'], () => refetchData(pageNum), {
refetchInterval: 5000,
enabled: qaListLen > 0 || vectorListLen > 0
});
// get al data and export csv
const { mutate: onclickExport, isLoading: isLoadingExport = false } = useMutation({
mutationFn: () => getExportDataList(kbId),
@@ -148,6 +137,17 @@ const DataCard = ({ kbId }: { kbId: string }) => {
}
});
// interval get data
useQuery(['refetchData'], () => refetchData(1), {
refetchInterval: 5000,
enabled: qaListLen > 0 || vectorListLen > 0
});
useQuery(['getKbData', kbId], () => {
setSearchText('');
getData(1);
return null;
});
return (
<Box position={'relative'}>
<Flex>
@@ -205,10 +205,10 @@ const DataCard = ({ kbId }: { kbId: string }) => {
)}
<Box flex={1} />
<Input
maxW={'240px'}
maxW={['90%', '300px']}
size={'sm'}
value={searchText}
placeholder="搜索相关问题和答案,回车确认"
placeholder="搜索匹配知识,补充知识和来源,回车确认"
onChange={(e) => setSearchText(e.target.value)}
onBlur={() => {
if (searchText === lastSearch.current) return;
@@ -239,18 +239,22 @@ const DataCard = ({ kbId }: { kbId: string }) => {
</Tooltip>
</Th>
<Th></Th>
<Th></Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{modelDataList.map((item) => (
<Tr key={item.id}>
{kbDataList.map((item) => (
<Tr key={item.id} fontSize={'sm'}>
<Td>
<Box {...tdStyles.current}>{item.q}</Box>
</Td>
<Td>
<Box {...tdStyles.current}>{item.a || '-'}</Box>
</Td>
<Td maxW={'15%'} whiteSpace={'pre-wrap'} userSelect={'all'}>
{item.source?.trim() || '-'}
</Td>
<Td>
<IconButton
mr={5}

View File

@@ -56,13 +56,14 @@ const InputDataModal = ({
try {
const { insertLen } = await postKbDataFromList({
kbId,
mode: TrainingModeEnum.index,
data: [
{
a: e.a,
q: e.q
q: e.q,
source: '手动录入'
}
],
mode: TrainingModeEnum.index
]
});
if (insertLen === 0) {

View File

@@ -37,6 +37,7 @@ const SelectJsonModal = ({
const { toast } = useToast();
const { File, onOpen } = useSelectFile({ fileType: '.csv', multiple: false });
const [fileData, setFileData] = useState<{ q: string; a: string }[]>([]);
const [fileName, setFileName] = useState('');
const [successData, setSuccessData] = useState(0);
const { openConfirm, ConfirmChild } = useConfirm({
content: '确认导入该数据集?'
@@ -46,6 +47,7 @@ const SelectJsonModal = ({
async (e: File[]) => {
const file = e[0];
setSelecting(true);
setFileName(file.name);
try {
const { header, data } = await readCsvContent(file);
if (header[0] !== 'question' || header[1] !== 'answer') {
@@ -75,11 +77,14 @@ const SelectJsonModal = ({
let success = 0;
// subsection import
const step = 50;
const step = 100;
for (let i = 0; i < fileData.length; i += step) {
const { insertLen } = await postKbDataFromList({
kbId,
data: fileData.slice(i, i + step),
data: fileData.slice(i, i + step).map((item) => ({
...item,
source: fileName
})),
mode: TrainingModeEnum.index
});
success += insertLen || 0;
@@ -129,13 +134,14 @@ const SelectJsonModal = ({
>
csv模板
</Box>
<Flex alignItems={'center'}>
<Box>
<Button isLoading={selecting} isDisabled={uploading} onClick={onOpen}>
csv
</Button>
<Box ml={4}> {fileData.length} 100</Box>
</Flex>
<Box mt={4}>
{fileName} {fileData.length} 100
</Box>
</Box>
</Box>
<Box flex={'3 0 0'} h={'100%'} overflow={'auto'} p={2} backgroundColor={'blackAlpha.50'}>
{fileData.slice(0, 100).map((item, index) => (

View File

@@ -1,4 +1,4 @@
import React, { useState, useCallback, useMemo } from 'react';
import React, { useState, useCallback } from 'react';
import {
Box,
Flex,
@@ -54,18 +54,20 @@ const SelectFileModal = ({
const [prompt, setPrompt] = useState('');
const { File, onOpen } = useSelectFile({ fileType: fileExtension, multiple: true });
const [mode, setMode] = useState<`${TrainingModeEnum}`>(TrainingModeEnum.index);
const [fileTextArr, setFileTextArr] = useState<string[]>(['']);
const [files, setFiles] = useState<{ filename: string; text: string }[]>([
{ filename: '文本1', text: '' }
]);
const [splitRes, setSplitRes] = useState<{
tokens: number;
chunks: string[];
chunks: { filename: string; value: string }[];
successChunks: number;
}>({
tokens: 0,
chunks: [],
successChunks: 0
successChunks: 0,
chunks: []
});
const { openConfirm, ConfirmChild } = useConfirm({
content: `确认导入该文件,需要一定时间进行拆解,该任务无法终止!如果余额不足,未完成的任务会被直接清除。一共 ${
content: `确认导入该文件,需要一定时间进行拆解,该任务无法终止!QA 拆分仅能使用余额,如果余额不足,未完成的任务会被直接清除。一共 ${
splitRes.chunks.length
} 组。${splitRes.tokens ? `大约 ${splitRes.tokens} 个tokens。` : ''}`
});
@@ -78,21 +80,21 @@ const SelectFileModal = ({
files.forEach((file) => {
promise = promise.then(async () => {
const extension = file?.name?.split('.')?.pop()?.toLowerCase();
let text = '';
switch (extension) {
case 'txt':
case 'md':
text = await readTxtContent(file);
break;
case 'pdf':
text = await readPdfContent(file);
break;
case 'doc':
case 'docx':
text = await readDocContent(file);
break;
}
text && setFileTextArr((state) => [text].concat(state));
const text = await (async () => {
switch (extension) {
case 'txt':
case 'md':
return readTxtContent(file);
case 'pdf':
return readPdfContent(file);
case 'doc':
case 'docx':
return readDocContent(file);
}
return '';
})();
text && setFiles((state) => [{ filename: file.name, text }].concat(state));
return;
});
});
@@ -115,11 +117,13 @@ const SelectFileModal = ({
// subsection import
let success = 0;
const step = 50;
const step = 100;
for (let i = 0; i < splitRes.chunks.length; i += step) {
const { insertLen } = await postKbDataFromList({
kbId,
data: splitRes.chunks.slice(i, i + step).map((text) => ({ q: text, a: '' })),
data: splitRes.chunks
.slice(i, i + step)
.map((item) => ({ q: item.value, a: '', source: item.filename })),
prompt: `下面是"${prompt || '一段长文本'}"`,
mode
});
@@ -149,26 +153,32 @@ const SelectFileModal = ({
const onclickImport = useCallback(async () => {
setBtnLoading(true);
try {
let promise = Promise.resolve();
const splitRes = await Promise.all(
fileTextArr
.filter((item) => item)
.map((item) =>
splitText_token({
text: item,
...modeMap[mode]
})
)
);
const splitRes = files
.map((item) =>
splitText_token({
text: item.text,
...modeMap[mode]
})
)
.map((item, i) => ({
...item,
filename: files[i].filename
}))
.filter((item) => item.tokens > 0);
setSplitRes({
tokens: splitRes.reduce((sum, item) => sum + item.tokens, 0),
chunks: splitRes.map((item) => item.chunks).flat(),
chunks: splitRes
.map((item) =>
item.chunks.map((chunk) => ({
filename: item.filename,
value: chunk
}))
)
.flat(),
successChunks: 0
});
await promise;
openConfirm(mutate)();
} catch (error) {
toast({
@@ -177,7 +187,7 @@ const SelectFileModal = ({
});
}
setBtnLoading(false);
}, [fileTextArr, mode, mutate, openConfirm, toast]);
}, [files, mode, mutate, openConfirm, toast]);
return (
<Modal isOpen={true} onClose={onClose} isCentered>
@@ -204,7 +214,7 @@ const SelectFileModal = ({
>
<Box mt={2} px={5} maxW={['100%', '70%']} textAlign={'justify'} color={'blackAlpha.600'}>
{fileExtension} Gpt会自动对文本进行 QA
tokens{fileTextArr.length}
tokens{files.length}
</Box>
{/* 拆分模式 */}
<Flex w={'100%'} px={5} alignItems={'center'} mt={4}>
@@ -235,26 +245,26 @@ const SelectFileModal = ({
)}
{/* 文本内容 */}
<Box flex={'1 0 0'} px={5} h={0} w={'100%'} overflowY={'auto'} mt={4}>
{fileTextArr.slice(0, 100).map((item, i) => (
{files.slice(0, 100).map((item, i) => (
<Box key={i} mb={5}>
<Box mb={1}>{i + 1}</Box>
<Box mb={1}>{item.filename}</Box>
<Textarea
placeholder="文件内容,空内容会自动忽略"
maxLength={-1}
rows={10}
fontSize={'xs'}
whiteSpace={'pre-wrap'}
value={item}
value={item.text}
onChange={(e) => {
setFileTextArr([
...fileTextArr.slice(0, i),
e.target.value,
...fileTextArr.slice(i + 1)
setFiles([
...files.slice(0, i),
{ ...item, text: e.target.value },
...files.slice(i + 1)
]);
}}
onBlur={(e) => {
if (fileTextArr.length > 1 && e.target.value === '') {
setFileTextArr((state) => [...state.slice(0, i), ...state.slice(i + 1)]);
if (files.length > 1 && e.target.value === '') {
setFiles((state) => [...state.slice(0, i), ...state.slice(i + 1)]);
}
}}
/>
@@ -272,7 +282,7 @@ const SelectFileModal = ({
</Button>
<Button
isDisabled={uploading || btnLoading || fileTextArr[0] === ''}
isDisabled={uploading || btnLoading || files[0]?.text === ''}
onClick={onclickImport}
>
{uploading ? (

View File

@@ -136,7 +136,7 @@ const NumberSetting = () => {
</Button>
</Flex>
<Box fontSize={'xs'} color={'blackAlpha.500'}>
openai
openai openai
</Box>
</Box>
<Flex mt={6} alignItems={'center'}>

View File

@@ -27,7 +27,8 @@ export const openaiError: Record<string, string> = {
export const openaiAccountError: Record<string, string> = {
insufficient_quota: 'API 余额不足',
invalid_api_key: 'openai 账号异常',
account_deactivated: '账号已停用'
account_deactivated: '账号已停用',
invalid_request_error: '无效请求'
};
export const proxyError: Record<string, boolean> = {
ECONNABORTED: true,

View File

@@ -61,7 +61,8 @@ export async function generateQA(): Promise<any> {
userId: 1,
kbId: 1,
prompt: 1,
q: 1
q: 1,
source: 1
});
// task preemption
@@ -75,10 +76,11 @@ export async function generateQA(): Promise<any> {
const kbId = String(data.kbId);
// 余额校验并获取 openapi Key
const { userOpenAiKey, systemAuthKey } = await getApiKey({
const { systemAuthKey } = await getApiKey({
model: OpenAiChatEnum.GPT35,
userId,
type: 'training'
type: 'training',
mustPay: true
});
const startTime = Date.now();
@@ -88,7 +90,7 @@ export async function generateQA(): Promise<any> {
[data.q].map((text) =>
modelServiceToolMap[OpenAiChatEnum.GPT35]
.chatCompletion({
apiKey: userOpenAiKey || systemAuthKey,
apiKey: systemAuthKey,
temperature: 0.8,
messages: [
{
@@ -113,7 +115,7 @@ A2:
console.log(`split result length: `, result.length);
// 计费
pushSplitDataBill({
isPay: !userOpenAiKey && result.length > 0,
isPay: result.length > 0,
userId: data.userId,
type: BillTypeEnum.QA,
textLen: responseMessages.map((item) => item.value).join('').length,
@@ -137,7 +139,10 @@ A2:
// 创建 向量生成 队列
await pushDataToKb({
kbId,
data: responseList,
data: responseList.map((item) => ({
...item,
source: data.source
})),
userId,
mode: TrainingModeEnum.index
});

View File

@@ -57,7 +57,8 @@ export async function generateVector(): Promise<any> {
userId: 1,
kbId: 1,
q: 1,
a: 1
a: 1,
source: 1
});
// task preemption
@@ -91,6 +92,7 @@ export async function generateVector(): Promise<any> {
data: vectors.map((vector, i) => ({
q: dataItems[i].q,
a: dataItems[i].a,
source: data.source,
vector
}))
});
@@ -110,6 +112,7 @@ export async function generateVector(): Promise<any> {
} else {
console.log('生成向量错误:', err);
}
// message error or openai account error
if (
err?.message === 'invalid message format' ||

View File

@@ -31,10 +31,17 @@ const ChatSchema = new Schema({
type: String,
default: '历史记录'
},
customTitle: {
type: String,
default: ''
},
latestChat: {
type: String,
default: ''
},
top: {
type: Boolean
},
content: {
type: [
{
@@ -53,7 +60,7 @@ const ChatSchema = new Schema({
id: String,
q: String,
a: String,
isEdit: Boolean
source: String
}
],
default: []

View File

@@ -38,8 +38,9 @@ const TrainingDataSchema = new Schema({
type: String,
default: ''
},
vectorList: {
type: Object
source: {
type: String,
default: ''
}
});

View File

@@ -2,6 +2,7 @@ import { Schema, model, models, Model } from 'mongoose';
import { hashPassword } from '@/service/utils/tools';
import { PRICE_SCALE } from '@/constants/common';
import { UserModelSchema } from '@/types/mongoSchema';
const UserSchema = new Schema({
username: {
// 可以是手机/邮箱,新的验证都只用手机

View File

@@ -37,6 +37,7 @@ export async function connectToDatabase(): Promise<void> {
});
}
// 初始化队列
global.qaQueueLen = 0;
global.vectorQueueLen = 0;

View File

@@ -6,13 +6,14 @@ export const connectPg = async () => {
return global.pgClient;
}
const maxLink = Number(process.env.VECTOR_MAX_PROCESS || 10);
global.pgClient = new Pool({
host: process.env.PG_HOST,
port: process.env.PG_PORT ? +process.env.PG_PORT : 5432,
user: process.env.PG_USER,
password: process.env.PG_PASSWORD,
database: process.env.PG_DB_NAME,
max: 20,
max: maxLink,
idleTimeoutMillis: 60000,
connectionTimeoutMillis: 20000
});
@@ -171,12 +172,14 @@ export const insertKbItem = ({
vector: number[];
q: string;
a: string;
source?: string;
}[];
}) => {
return PgClient.insert('modelData', {
values: data.map((item) => [
{ key: 'user_id', value: userId },
{ key: 'kb_id', value: kbId },
{ key: 'source', value: item.source?.slice(0, 30)?.trim() || '' },
{ key: 'q', value: item.q.replace(/'/g, '"') },
{ key: 'a', value: item.a.replace(/'/g, '"') },
{ key: 'vector', value: `[${item.vector}]` }

View File

@@ -280,7 +280,8 @@ export const authChat = async ({
{
$project: {
obj: '$content.obj',
value: '$content.value'
value: '$content.value',
quote: '$content.quote'
}
}
]);

View File

@@ -89,39 +89,55 @@ export const ChatContextFilter = ({
prompts: ChatItemSimpleType[];
maxTokens: number;
}) => {
const systemPrompts: ChatItemSimpleType[] = [];
const chatPrompts: ChatItemSimpleType[] = [];
let rawTextLen = 0;
const formatPrompts = prompts.map<ChatItemSimpleType>((item) => {
prompts.forEach((item) => {
const val = simplifyStr(item.value);
rawTextLen += val.length;
return {
const data = {
obj: item.obj,
value: val
};
if (item.obj === ChatRoleEnum.System) {
systemPrompts.push(data);
} else {
chatPrompts.push(data);
}
});
// 长度太小时,不需要进行 token 截断
if (formatPrompts.length <= 2 || rawTextLen < maxTokens * 0.5) {
return formatPrompts;
if (rawTextLen < maxTokens * 0.5) {
return [...systemPrompts, ...chatPrompts];
}
// 去掉 system 的 token
maxTokens -= modelToolMap[model].countTokens({
messages: systemPrompts
});
// 根据 tokens 截断内容
const chats: ChatItemSimpleType[] = [];
// 从后往前截取对话内容
for (let i = formatPrompts.length - 1; i >= 0; i--) {
chats.unshift(formatPrompts[i]);
for (let i = chatPrompts.length - 1; i >= 0; i--) {
chats.unshift(chatPrompts[i]);
const tokens = modelToolMap[model].countTokens({
messages: chats
});
/* 整体 tokens 超出范围, system必须保留 */
if (tokens >= maxTokens && formatPrompts[i].obj !== ChatRoleEnum.System) {
return chats.slice(1);
if (tokens >= maxTokens) {
chats.shift();
break;
}
}
return chats;
return [...systemPrompts, ...chats];
};
/* stream response */

View File

@@ -104,6 +104,7 @@ export const openAiStreamResponse = async ({
obj: ChatRoleEnum.AI,
value: responseContent
});
const totalTokens = modelToolMap[model].countTokens({
messages: finishMessages
});

View File

@@ -11,6 +11,7 @@ import {
ShareChatType
} from '@/types/chat';
import { getChatHistory } from '@/api/chat';
import { HUMAN_ICON } from '@/constants/chat';
type SetShareChatHistoryItem = {
historyId: string;
@@ -57,6 +58,7 @@ const defaultChatData = {
};
const defaultShareChatData: ShareChatType = {
maxContext: 5,
userAvatar: HUMAN_ICON,
model: {
name: '',
avatar: '/icon/logo.png',

1
src/types/chat.d.ts vendored
View File

@@ -33,6 +33,7 @@ export type HistoryItemType = {
modelId: string;
title: string;
latestChat: string;
top: boolean;
};
export type ShareChatHistoryItemType = {

View File

@@ -1,6 +1,7 @@
import type { Mongoose } from 'mongoose';
import type { Agent } from 'http';
import type { Pool } from 'pg';
import type { Tiktoken } from '@dqbd/tiktoken';
declare global {
var mongodb: Mongoose | string | null;
@@ -11,6 +12,7 @@ declare global {
var QRCode: any;
var qaQueueLen: number;
var vectorQueueLen: number;
var OpenAiEncMap: Record<string, Tiktoken>;
interface Window {
['pdfjs-dist/build/pdf']: any;

View File

@@ -78,6 +78,7 @@ export interface TrainingDataSchema {
prompt: string;
q: string;
a: string;
source: string;
}
export interface ChatSchema {
@@ -85,8 +86,11 @@ export interface ChatSchema {
userId: string;
modelId: string;
expiredTime: number;
loadAmount: number;
updateTime: Date;
title: string;
customTitle: string;
latestChat: string;
top: boolean;
content: ChatItemType[];
}
export interface ChatPopulate extends ChatSchema {

7
src/types/pg.d.ts vendored
View File

@@ -1,7 +0,0 @@
export interface PgKBDataItemType {
id: string;
q: string;
a: string;
user_id: string;
kb_id: string;
}

View File

@@ -10,8 +10,7 @@ export interface KbDataItemType {
id: string;
q: string; // 提问词
a: string; // 原文
kbId: string;
userId: string;
source: string;
}
export type TextPluginRequestParams = {

View File

@@ -1,6 +1,7 @@
import mammoth from 'mammoth';
import Papa from 'papaparse';
import { getOpenAiEncMap } from './plugin/openai';
import { getErrText } from './tools';
/**
* 读取 txt 文件内容
@@ -145,7 +146,7 @@ export const fileDownload = ({
* slideLen - The size of the before and after Text
* maxLen > slideLen
*/
export const splitText_token = async ({
export const splitText_token = ({
text,
maxLen,
slideLen
@@ -184,8 +185,8 @@ export const splitText_token = async ({
chunks,
tokens
};
} catch (error) {
return Promise.reject(error);
} catch (err) {
throw new Error(getErrText(err));
}
};

View File

@@ -8,6 +8,66 @@ import Graphemer from 'graphemer';
const textDecoder = new TextDecoder();
const graphemer = new Graphemer();
export const getOpenAiEncMap = () => {
if (typeof window !== 'undefined') {
window.OpenAiEncMap = window.OpenAiEncMap || {
'gpt-3.5-turbo': encoding_for_model('gpt-3.5-turbo', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4': encoding_for_model('gpt-4', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4-32k': encoding_for_model('gpt-4-32k', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
})
};
return window.OpenAiEncMap;
}
if (typeof global !== 'undefined') {
global.OpenAiEncMap = global.OpenAiEncMap || {
'gpt-3.5-turbo': encoding_for_model('gpt-3.5-turbo', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4': encoding_for_model('gpt-4', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4-32k': encoding_for_model('gpt-4-32k', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
})
};
return global.OpenAiEncMap;
}
return {
'gpt-3.5-turbo': encoding_for_model('gpt-3.5-turbo', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4': encoding_for_model('gpt-4', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4-32k': encoding_for_model('gpt-4-32k', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
})
};
};
export const adaptChatItem_openAI = ({
messages
}: {
@@ -24,29 +84,6 @@ export const adaptChatItem_openAI = ({
}));
};
/* count openai chat token*/
let OpenAiEncMap: Record<string, Tiktoken>;
export const getOpenAiEncMap = () => {
if (OpenAiEncMap) return OpenAiEncMap;
OpenAiEncMap = {
'gpt-3.5-turbo': encoding_for_model('gpt-3.5-turbo', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4': encoding_for_model('gpt-4', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
}),
'gpt-4-32k': encoding_for_model('gpt-4-32k', {
'<|im_start|>': 100264,
'<|im_end|>': 100265,
'<|im_sep|>': 100266
})
};
return OpenAiEncMap;
};
export function countOpenAIToken({
messages,
model