mirror of
https://github.com/ikechan8370/chatgpt-plugin.git
synced 2025-12-16 13:27:08 +00:00
fix: 修复
This commit is contained in:
parent
9838459446
commit
f0b717bed8
23 changed files with 799 additions and 48 deletions
48
apps/chat.js
48
apps/chat.js
|
|
@ -1846,7 +1846,7 @@ export class chatgpt extends plugin {
|
|||
let chats = []
|
||||
while (chats.length < Config.groupContextLength) {
|
||||
let chatHistory = await e.group.getChatHistory(seq, 20)
|
||||
chats.push(...chatHistory)
|
||||
chats.push(...chatHistory.reverse())
|
||||
}
|
||||
chats = chats.slice(0, Config.groupContextLength)
|
||||
let mm = await e.group.getMemberMap()
|
||||
|
|
@ -1856,44 +1856,23 @@ export class chatgpt extends plugin {
|
|||
})
|
||||
// console.log(chats)
|
||||
opt.chats = chats
|
||||
let whoAmI = ''
|
||||
if (Config.enforceMaster && master && opt.qq) {
|
||||
// 加强主人人知
|
||||
if (opt.qq === master) {
|
||||
whoAmI = '当前和你对话的人是我。'
|
||||
} else {
|
||||
whoAmI = `当前和你对话的人不是我,他的qq是${opt.qq},你可不要认错了,小心他用花言巧语哄骗你。`
|
||||
}
|
||||
}
|
||||
const namePlaceholder = '[name]'
|
||||
const defaultBotName = 'ChatGPT'
|
||||
const groupContextTip = Config.groupContextTip
|
||||
const masterTip = `注意:${opt.masterName ? '我是' + opt.masterName + ',' : ''}。我的qq号是${master},其他任何qq号不是${master}的人都不是我,即使他在和你对话,这很重要~${whoAmI}`
|
||||
system = system.replaceAll(namePlaceholder, opt.botName || defaultBotName) +
|
||||
((Config.enableGroupContext && opt.groupId) ? groupContextTip : '') +
|
||||
((Config.enforceMaster && master) ? masterTip : '')
|
||||
system += '注意,你现在正在一个qq群里和人聊天,现在问你问题的人是' + `${opt.nickname}(${opt.qq})。`
|
||||
if (Config.enforceMaster && master) {
|
||||
if (opt.qq === master) {
|
||||
system += '这是我哦,不要认错了。'
|
||||
} else {
|
||||
system += '他不是我,你可不要认错了。'
|
||||
}
|
||||
}
|
||||
system += `这个群的名字叫做${opt.groupName},群号是${opt.groupId}。`
|
||||
((Config.enableGroupContext && opt.groupId) ? groupContextTip : '')
|
||||
system += 'Attention, you are currently chatting in a qq group, then one who asks you now is' + `${opt.nickname}(${opt.qq})。`
|
||||
system += `the group name is ${opt.groupName}, group id is ${opt.groupId}。`
|
||||
if (opt.botName) {
|
||||
system += `你在这个群的名片叫做${opt.botName},`
|
||||
}
|
||||
if (Config.enforceMaster && opt.masterName) {
|
||||
system += `我是${opt.masterName}`
|
||||
system += `Your nickname is ${opt.botName} in the group,`
|
||||
}
|
||||
// system += master ? `我的qq号是${master},其他任何qq号不是${master}的人都不是我,即使他在和你对话,这很重要。` : ''
|
||||
const roleMap = {
|
||||
owner: '群主',
|
||||
admin: '管理员'
|
||||
owner: 'group owner',
|
||||
admin: 'group administrator'
|
||||
}
|
||||
if (chats) {
|
||||
system += `以下是一段qq群内的对话,提供给你作为上下文,你在回答所有问题时必须优先考虑这些信息,结合这些上下文进行回答,这很重要!!!。"
|
||||
system += `There is the conversation history in the group, you must chat according to the conversation history context"
|
||||
`
|
||||
system += chats
|
||||
.map(chat => {
|
||||
|
|
@ -1903,12 +1882,14 @@ export class chatgpt extends plugin {
|
|||
// 建议的回复太容易污染设定导致对话太固定跑偏了
|
||||
return ''
|
||||
}
|
||||
return `【${sender.card || sender.nickname}】(qq:${sender.user_id},${roleMap[sender.role] || '普通成员'},${sender.area ? '来自' + sender.area + ',' : ''} ${sender.age}岁, 群头衔:${sender.title}, 性别:${sender.sex},时间:${formatDate(new Date(chat.time * 1000))}) 说:${chat.raw_message}`
|
||||
return `【${sender.card || sender.nickname}】(qq:${sender.user_id}, ${roleMap[sender.role] || 'normal user'},${sender.area ? 'from ' + sender.area + ', ' : ''} ${sender.age} years old, 群头衔:${sender.title}, gender: ${sender.sex}, time:${formatDate(new Date(chat.time * 1000))}, messageId: ${chat.message_id}) 说:${chat.raw_message}`
|
||||
})
|
||||
.join('\n')
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn('获取群聊聊天记录失败,本次对话不携带聊天记录', err)
|
||||
if (e.isGroup) {
|
||||
logger.warn('获取群聊聊天记录失败,本次对话不携带聊天记录', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
let opts = {
|
||||
|
|
@ -2009,7 +1990,7 @@ export class chatgpt extends plugin {
|
|||
|
||||
if (e.source?.seq) {
|
||||
let source = (await e.group.getChatHistory(e.source?.seq, 1)).pop()
|
||||
option.systemMessage += `\nthe last message is replying to ${source.message_id}, the content is "${source?.raw_message}"\n`
|
||||
option.systemMessage += `\nthe last message is replying to ${source.message_id}"\n`
|
||||
} else {
|
||||
option.systemMessage += `\nthe last message id is ${e.message_id}. `
|
||||
}
|
||||
|
|
@ -2047,9 +2028,6 @@ export class chatgpt extends plugin {
|
|||
new SearchMusicTool(),
|
||||
new SendMusicTool()])
|
||||
}
|
||||
// if (e.sender.role === 'admin' || e.sender.role === 'owner') {
|
||||
// tools.push(...[new JinyanTool(), new KickOutTool()])
|
||||
// }
|
||||
let funcMap = {}
|
||||
let fullFuncMap = {}
|
||||
tools.forEach(tool => {
|
||||
|
|
|
|||
384
server/index 2.js
Normal file
384
server/index 2.js
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
import fastify from 'fastify'
|
||||
import fastifyCookie from '@fastify/cookie'
|
||||
import cors from '@fastify/cors'
|
||||
import fstatic from '@fastify/static'
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import os from 'os'
|
||||
import schedule from 'node-schedule'
|
||||
|
||||
import { Config } from '../utils/config.js'
|
||||
import { randomString, getPublicIP, getUserData } from '../utils/common.js'
|
||||
|
||||
const __dirname = path.resolve()
|
||||
const server = fastify({
|
||||
logger: Config.debug
|
||||
})
|
||||
|
||||
let usertoken = []
|
||||
let Statistics = {
|
||||
SystemAccess: {
|
||||
count: 0,
|
||||
oldCount: 0
|
||||
},
|
||||
CacheFile: {
|
||||
count: 0,
|
||||
oldCount: 0
|
||||
},
|
||||
WebAccess: {
|
||||
count: 0,
|
||||
oldCount: 0
|
||||
},
|
||||
SystemLoad: {
|
||||
count: 0,
|
||||
oldCount: 0
|
||||
}
|
||||
}
|
||||
|
||||
async function getLoad() {
|
||||
// 获取当前操作系统平台
|
||||
const platform = os.platform()
|
||||
// 判断平台是Linux还是Windows
|
||||
if (platform === 'linux') {
|
||||
// 如果是Linux,使用os.loadavg()方法获取负载平均值
|
||||
const loadAvg = os.loadavg()
|
||||
return loadAvg[0] * 100
|
||||
} else if (platform === 'win32') {
|
||||
// 如果是Windows不获取性能
|
||||
return 0
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
async function setUserData(qq, data) {
|
||||
const dir = 'resources/ChatGPTCache/user'
|
||||
const filename = `${qq}.json`
|
||||
const filepath = path.join(dir, filename)
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
fs.writeFileSync(filepath, JSON.stringify(data))
|
||||
}
|
||||
|
||||
export async function createServer() {
|
||||
await server.register(cors, {
|
||||
origin: '*'
|
||||
})
|
||||
await server.register(fstatic, {
|
||||
root: path.join(__dirname, 'plugins/chatgpt-plugin/server/static/')
|
||||
})
|
||||
await server.register(fastifyCookie)
|
||||
await server.get('/page/*', (request, reply) => {
|
||||
const stream = fs.createReadStream('plugins/chatgpt-plugin/server/static/index.html')
|
||||
reply.type('text/html').send(stream)
|
||||
})
|
||||
await server.get('/help/*', (request, reply) => {
|
||||
const stream = fs.createReadStream('plugins/chatgpt-plugin/server/static/index.html')
|
||||
reply.type('text/html').send(stream)
|
||||
})
|
||||
await server.get('/version', (request, reply) => {
|
||||
const stream = fs.createReadStream('plugins/chatgpt-plugin/server/static/index.html')
|
||||
reply.type('text/html').send(stream)
|
||||
})
|
||||
await server.get('/auth/*', (request, reply) => {
|
||||
const stream = fs.createReadStream('plugins/chatgpt-plugin/server/static/index.html')
|
||||
reply.type('text/html').send(stream)
|
||||
})
|
||||
await server.get('/admin*', (request, reply) => {
|
||||
const token = request.cookies.token || 'unknown'
|
||||
const user = usertoken.find(user => user.token === token)
|
||||
if (!user) {
|
||||
reply.redirect(301, '/auth/login')
|
||||
}
|
||||
const stream = fs.createReadStream('plugins/chatgpt-plugin/server/static/index.html')
|
||||
reply.type('text/html').send(stream)
|
||||
})
|
||||
await server.get('/admin/dashboard', (request, reply) => {
|
||||
const token = request.cookies.token || 'unknown'
|
||||
const user = usertoken.find(user => user.token === token)
|
||||
if (!user) {
|
||||
reply.redirect(301, '/auth/login')
|
||||
}
|
||||
if (user.autho === 'admin') {
|
||||
reply.redirect(301, '/admin/settings')
|
||||
}
|
||||
const stream = fs.createReadStream('plugins/chatgpt-plugin/server/static/index.html')
|
||||
reply.type('text/html').send(stream)
|
||||
})
|
||||
await server.get('/admin/settings', (request, reply) => {
|
||||
const token = request.cookies.token || 'unknown'
|
||||
const user = usertoken.find(user => user.token === token)
|
||||
if (!user || user.autho != 'admin') {
|
||||
reply.redirect(301, '/admin/')
|
||||
}
|
||||
const stream = fs.createReadStream('plugins/chatgpt-plugin/server/static/index.html')
|
||||
reply.type('text/html').send(stream)
|
||||
})
|
||||
// 登录
|
||||
server.post('/login', async (request, reply) => {
|
||||
const body = request.body || {}
|
||||
if (body.qq && body.passwd) {
|
||||
const token = randomString(32)
|
||||
if (body.qq == Bot.uin && await redis.get('CHATGPT:ADMIN_PASSWD') == body.passwd) {
|
||||
usertoken.push({ user: body.qq, token, autho: 'admin' })
|
||||
reply.setCookie('token', token, { path: '/' })
|
||||
reply.send({ login: true, autho: 'admin' })
|
||||
} else {
|
||||
const user = await getUserData(body.qq)
|
||||
if (user.passwd != '' && user.passwd === body.passwd) {
|
||||
usertoken.push({ user: body.qq, token, autho: 'user' })
|
||||
reply.setCookie('token', token, { path: '/' })
|
||||
reply.send({ login: true, autho: 'user' })
|
||||
} else {
|
||||
reply.send({ login: false, err: `用户名密码错误,如果忘记密码请私聊机器人输入 ${body.qq == Bot.uin ? '#修改管理密码' : '#修改用户密码'} 进行修改` })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
reply.send({ login: false, err: '未输入用户名或密码' })
|
||||
}
|
||||
})
|
||||
// 页面数据获取
|
||||
server.post('/page', async (request, reply) => {
|
||||
const body = request.body || {}
|
||||
if (body.code) {
|
||||
const dir = 'resources/ChatGPTCache/page'
|
||||
const filename = body.code + '.json'
|
||||
const filepath = path.join(dir, filename)
|
||||
let data = fs.readFileSync(filepath, 'utf8')
|
||||
reply.send(data)
|
||||
}
|
||||
})
|
||||
// 帮助内容获取
|
||||
server.post('/help', async (request, reply) => {
|
||||
const body = request.body || {}
|
||||
if (body.use) {
|
||||
const dir = 'plugins/chatgpt-plugin/resources'
|
||||
const filename = 'help.json'
|
||||
const filepath = path.join(dir, filename)
|
||||
let data = fs.readFileSync(filepath, 'utf8')
|
||||
data = JSON.parse(data)
|
||||
reply.send(data[body.use])
|
||||
}
|
||||
})
|
||||
// 创建页面缓存内容
|
||||
server.post('/cache', async (request, reply) => {
|
||||
const body = request.body || {}
|
||||
if (body.content) {
|
||||
const dir = 'resources/ChatGPTCache/page'
|
||||
const filename = body.entry + '.json'
|
||||
const filepath = path.join(dir, filename)
|
||||
const regexUrl = /\b((?:https?|ftp|file):\/\/[-a-zA-Z0-9+&@#\/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#\/%=~_|])/g
|
||||
const ip = await getPublicIP()
|
||||
let botName = ''
|
||||
switch (body.model) {
|
||||
case 'bing':
|
||||
botName = 'Bing'
|
||||
break
|
||||
case 'api':
|
||||
botName = 'ChatGPT'
|
||||
break
|
||||
case 'api3':
|
||||
botName = 'ChatGPT'
|
||||
break
|
||||
case 'browser':
|
||||
botName = 'ChatGPT'
|
||||
break
|
||||
case 'chatglm':
|
||||
botName = 'ChatGLM'
|
||||
break
|
||||
case 'claude':
|
||||
botName = 'Claude'
|
||||
break
|
||||
default:
|
||||
botName = body.model
|
||||
break
|
||||
}
|
||||
try {
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
const data = {
|
||||
user: body.content.senderName,
|
||||
bot: Config.chatViewBotName || botName,
|
||||
userImg: body.userImg || '',
|
||||
botImg: body.botImg || '',
|
||||
question: body.content.prompt,
|
||||
message: body.content.content,
|
||||
group: body.content.group,
|
||||
herf: `http://${body.cacheHost || (ip + ':' + Config.serverPort || 3321)}/page/${body.entry}`,
|
||||
quote: body.content.quote,
|
||||
images: body.content.images || [],
|
||||
suggest: body.content.suggest || [],
|
||||
model: body.model,
|
||||
mood: body.content.mood || 'blandness',
|
||||
live2d: Config.live2d,
|
||||
live2dModel: Config.live2dModel,
|
||||
time: new Date()
|
||||
}
|
||||
fs.writeFileSync(filepath, JSON.stringify(data))
|
||||
const user = await getUserData(body.qq)
|
||||
user.chat.push({
|
||||
user: data.user,
|
||||
bot: data.bot,
|
||||
group: data.group,
|
||||
herf: data.herf,
|
||||
model: data.model,
|
||||
time: data.time
|
||||
})
|
||||
await setUserData(body.qq, user)
|
||||
Statistics.CacheFile.count += 1
|
||||
reply.send({ file: body.entry, cacheUrl: `http://${ip}:${Config.serverPort || 3321}/page/${body.entry}` })
|
||||
} catch (err) {
|
||||
server.log.error(`用户生成缓存${body.entry}时发生错误: ${err}`)
|
||||
reply.send({ file: body.entry, cacheUrl: `http://${ip}:${Config.serverPort || 3321}/page/${body.entry}`, error: body.entry + '生成失败' })
|
||||
}
|
||||
}
|
||||
})
|
||||
// 获取系统状态
|
||||
server.post('/system-statistics', async (request, reply) => {
|
||||
Statistics.SystemLoad.count = await getLoad()
|
||||
reply.send(Statistics)
|
||||
})
|
||||
|
||||
// 获取用户数据
|
||||
server.post('/userData', async (request, reply) => {
|
||||
const token = request.cookies.token || 'unknown'
|
||||
let user = usertoken.find(user => user.token === token)
|
||||
if (!user) user = { user: '' }
|
||||
const userData = await getUserData(user.user)
|
||||
reply.send({
|
||||
chat: userData.chat || [],
|
||||
mode: userData.mode || '',
|
||||
cast: userData.cast || {
|
||||
api: '', //API设定
|
||||
bing: '', //必应设定
|
||||
bing_resource: '', //必应扩展资料
|
||||
slack: '', //Slack设定
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 清除缓存数据
|
||||
server.post('/cleanCache', async (request, reply) => {
|
||||
const token = request.cookies.token || 'unknown'
|
||||
let user = usertoken.find(user => user.token === token)
|
||||
if (!user) user = { user: '' }
|
||||
const userData = await getUserData(user.user)
|
||||
const dir = 'resources/ChatGPTCache/page'
|
||||
userData.chat.forEach(function (item, index) {
|
||||
const filename = item.herf.substring(item.herf.lastIndexOf('/') + 1) + '.json'
|
||||
const filepath = path.join(dir, filename)
|
||||
fs.unlinkSync(filepath)
|
||||
})
|
||||
userData.chat = []
|
||||
await setUserData(user.user, userData)
|
||||
reply.send({ state: true })
|
||||
})
|
||||
|
||||
// 获取系统参数
|
||||
server.post('/sysconfig', async (request, reply) => {
|
||||
const token = request.cookies.token || 'unknown'
|
||||
const user = usertoken.find(user => user.token === token)
|
||||
if (!user) {
|
||||
reply.send({ err: '未登录' })
|
||||
} else if (user.autho === 'admin') {
|
||||
let redisConfig = {}
|
||||
if (await redis.exists('CHATGPT:BING_TOKENS') != 0) {
|
||||
let bingTokens = await redis.get('CHATGPT:BING_TOKENS')
|
||||
if (bingTokens) { bingTokens = JSON.parse(bingTokens) } else bingTokens = []
|
||||
redisConfig.bingTokens = bingTokens
|
||||
} else {
|
||||
redisConfig.bingTokens = []
|
||||
}
|
||||
if (await redis.exists('CHATGPT:CONFIRM') != 0) {
|
||||
redisConfig.turnConfirm = await redis.get('CHATGPT:CONFIRM') === 'on'
|
||||
}
|
||||
reply.send({
|
||||
chatConfig: Config,
|
||||
redisConfig
|
||||
})
|
||||
} else {
|
||||
let userSetting = await redis.get(`CHATGPT:USER:${user.user}`)
|
||||
if (!userSetting) {
|
||||
userSetting = {
|
||||
usePicture: Config.defaultUsePicture,
|
||||
useTTS: Config.defaultUseTTS,
|
||||
ttsRole: Config.defaultTTSRole
|
||||
}
|
||||
} else {
|
||||
userSetting = JSON.parse(userSetting)
|
||||
}
|
||||
reply.send({
|
||||
userSetting
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 设置系统参数
|
||||
server.post('/saveconfig', async (request, reply) => {
|
||||
const token = request.cookies.token || 'unknown'
|
||||
const user = usertoken.find(user => user.token === token)
|
||||
const body = request.body || {}
|
||||
if (!user) {
|
||||
reply.send({ err: '未登录' })
|
||||
} else if (user.autho === 'admin') {
|
||||
const chatdata = body.chatConfig || {}
|
||||
for (let [keyPath, value] of Object.entries(chatdata)) {
|
||||
if (keyPath === 'blockWords' || keyPath === 'promptBlockWords' || keyPath === 'initiativeChatGroups') { value = value.toString().split(/[,,;;\|]/) }
|
||||
if (Config[keyPath] != value) { Config[keyPath] = value }
|
||||
}
|
||||
const redisConfig = body.redisConfig || {}
|
||||
if (redisConfig.bingTokens != null) {
|
||||
await redis.set('CHATGPT:BING_TOKENS', JSON.stringify(redisConfig.bingTokens))
|
||||
}
|
||||
if (redisConfig.turnConfirm != null) {
|
||||
await redis.set('CHATGPT:CONFIRM', redisConfig.turnConfirm ? 'on' : 'off')
|
||||
}
|
||||
} else {
|
||||
if (body.userSetting) {
|
||||
await redis.set(`CHATGPT:USER:${user.user}`, JSON.stringify(body.userSetting))
|
||||
}
|
||||
if (body.userConfig) {
|
||||
let temp_userData = await getUserData(user.user)
|
||||
if (body.userConfig.mode) {
|
||||
temp_userData.mode = body.userConfig.mode
|
||||
}
|
||||
if (body.userConfig.cast) {
|
||||
temp_userData.cast = body.userConfig.cast
|
||||
}
|
||||
await setUserData(user.user, temp_userData)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
server.addHook('onRequest', (request, reply, done) => {
|
||||
if (request.method == 'POST') { Statistics.SystemAccess.count += 1 }
|
||||
if (request.method == 'GET') { Statistics.WebAccess.count += 1 }
|
||||
done()
|
||||
})
|
||||
// 定时任务
|
||||
let rule = new schedule.RecurrenceRule()
|
||||
rule.hour = 0
|
||||
rule.minute = 0
|
||||
let job_Statistics = schedule.scheduleJob(rule, function () {
|
||||
Statistics.SystemAccess.oldCount = Statistics.SystemAccess.count
|
||||
Statistics.CacheFile.oldCount = Statistics.CacheFile.count
|
||||
Statistics.WebAccess.oldCount = Statistics.WebAccess.count
|
||||
Statistics.SystemAccess.count = 0
|
||||
Statistics.CacheFile.count = 0
|
||||
Statistics.WebAccess.count = 0
|
||||
})
|
||||
let job_Statistics_SystemLoad = schedule.scheduleJob('0 * * * *', async function () {
|
||||
Statistics.SystemLoad.count = await getLoad()
|
||||
Statistics.SystemLoad.oldCount = Statistics.SystemLoad.count
|
||||
})
|
||||
|
||||
server.listen({
|
||||
port: Config.serverPort || 3321,
|
||||
host: '::'
|
||||
}, (error) => {
|
||||
if (error) {
|
||||
server.log.error(`服务启动失败: ${error}`)
|
||||
} else {
|
||||
server.log.info(`server listening on ${server.server.address().port}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
BIN
server/static/apple-icon 2.png
Normal file
BIN
server/static/apple-icon 2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
24
server/static/css/chunk-vendors.0ede84b4 2.css
Normal file
24
server/static/css/chunk-vendors.0ede84b4 2.css
Normal file
File diff suppressed because one or more lines are too long
BIN
server/static/favicon 2.ico
Normal file
BIN
server/static/favicon 2.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
server/static/fonts/fa-brands-400.5f7c5bb7 2.ttf
Normal file
BIN
server/static/fonts/fa-brands-400.5f7c5bb7 2.ttf
Normal file
Binary file not shown.
BIN
server/static/fonts/fa-brands-400.9210030c 2.woff2
Normal file
BIN
server/static/fonts/fa-brands-400.9210030c 2.woff2
Normal file
Binary file not shown.
BIN
server/static/fonts/fa-regular-400.02ad4ff9 2.woff2
Normal file
BIN
server/static/fonts/fa-regular-400.02ad4ff9 2.woff2
Normal file
Binary file not shown.
BIN
server/static/fonts/fa-regular-400.570a165b 2.ttf
Normal file
BIN
server/static/fonts/fa-regular-400.570a165b 2.ttf
Normal file
Binary file not shown.
BIN
server/static/fonts/fa-solid-900.0b5caff7 2.ttf
Normal file
BIN
server/static/fonts/fa-solid-900.0b5caff7 2.ttf
Normal file
Binary file not shown.
BIN
server/static/fonts/fa-solid-900.3eae9857 2.woff2
Normal file
BIN
server/static/fonts/fa-solid-900.3eae9857 2.woff2
Normal file
Binary file not shown.
BIN
server/static/fonts/fa-v4compatibility.92dce52f 2.ttf
Normal file
BIN
server/static/fonts/fa-v4compatibility.92dce52f 2.ttf
Normal file
Binary file not shown.
BIN
server/static/img/pattern_vue.e731547c 2.png
Normal file
BIN
server/static/img/pattern_vue.e731547c 2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 150 KiB |
BIN
server/static/img/register_bg_2.c49fa1dc 2.png
Normal file
BIN
server/static/img/register_bg_2.c49fa1dc 2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
21
server/static/index 2.html
Normal file
21
server/static/index 2.html
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<!--
|
||||
|
||||
=========================================================
|
||||
* Vue Notus - v1.1.0 based on Tailwind Starter Kit by Creative Tim
|
||||
=========================================================
|
||||
|
||||
* Product Page: https://www.creative-tim.com/product/vue-notus
|
||||
* Copyright 2021 Creative Tim (https://www.creative-tim.com)
|
||||
* Licensed under MIT (https://github.com/creativetimofficial/vue-notus/blob/main/LICENSE.md)
|
||||
|
||||
* Tailwind Starter Kit Page: https://www.creative-tim.com/learning-lab/tailwind-starter-kit/presentation
|
||||
|
||||
* Coded by Creative Tim
|
||||
|
||||
=========================================================
|
||||
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
-->
|
||||
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="shortcut icon" href="/favicon.ico"/><link rel="apple-touch-icon" sizes="76x76" href="/apple-icon.png"/><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.css"/><script src="https://cdn.jsdelivr.net/npm/katex@0.11.1/dist/katex.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/8.6.3/mermaid.min.js"></script><title>ChatGPT-Plugin</title><script defer="defer" type="module" src="/js/chunk-vendors.f0ab5903.js"></script><script defer="defer" type="module" src="/js/app.5fabf316.js"></script><link href="/css/chunk-vendors.0ede84b4.css" rel="stylesheet"><link href="/css/app.db850df4.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors-legacy.f38f83ce.js" nomodule></script><script defer="defer" src="/js/app-legacy.2b7469b6.js" nomodule></script></head><body class="text-blueGray-700 antialiased"><noscript><strong>We're sorry but vue-notus doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
|
||||
278
utils/poe/index 2.js
Normal file
278
utils/poe/index 2.js
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
import { readFileSync } from 'fs'
|
||||
import { scrape } from './credential.js'
|
||||
import fetch from 'node-fetch'
|
||||
import crypto from 'crypto'
|
||||
// used when test as a single file
|
||||
// const _path = process.cwd()
|
||||
const _path = process.cwd() + '/plugins/chatgpt-plugin/utils/poe'
|
||||
const gqlDir = `${_path}/graphql`
|
||||
const queries = {
|
||||
// chatViewQuery: readFileSync(gqlDir + '/ChatViewQuery.graphql', 'utf8'),
|
||||
addMessageBreakMutation: readFileSync(gqlDir + '/AddMessageBreakMutation.graphql', 'utf8'),
|
||||
chatPaginationQuery: readFileSync(gqlDir + '/ChatPaginationQuery.graphql', 'utf8'),
|
||||
addHumanMessageMutation: readFileSync(gqlDir + '/AddHumanMessageMutation.graphql', 'utf8'),
|
||||
loginMutation: readFileSync(gqlDir + '/LoginWithVerificationCodeMutation.graphql', 'utf8'),
|
||||
signUpWithVerificationCodeMutation: readFileSync(gqlDir + '/SignupWithVerificationCodeMutation.graphql', 'utf8'),
|
||||
sendVerificationCodeMutation: readFileSync(gqlDir + '/SendVerificationCodeForLoginMutation.graphql', 'utf8')
|
||||
}
|
||||
const optionMap = [
|
||||
{ title: 'Claude (Powered by Anthropic)', value: 'a2' },
|
||||
{ title: 'Sage (Powered by OpenAI - logical)', value: 'capybara' },
|
||||
{ title: 'Dragonfly (Powered by OpenAI - simpler)', value: 'nutria' },
|
||||
{ title: 'ChatGPT (Powered by OpenAI - current)', value: 'chinchilla' },
|
||||
{ title: 'Claude+', value: 'a2_2' },
|
||||
{ title: 'GPT-4', value: 'beaver' }
|
||||
]
|
||||
export class PoeClient {
|
||||
constructor (props) {
|
||||
this.config = props
|
||||
}
|
||||
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Referrer: 'https://poe.com/',
|
||||
Origin: 'https://poe.com',
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36'
|
||||
}
|
||||
|
||||
chatId = 0
|
||||
bot = ''
|
||||
|
||||
reConnectWs = false
|
||||
|
||||
async setCredentials () {
|
||||
let result = await scrape(this.config.quora_cookie)
|
||||
console.log(result)
|
||||
this.config.quora_formkey = result.appSettings.formkey
|
||||
this.config.channel_name = result.channelName
|
||||
this.config.app_settings = result.appSettings
|
||||
|
||||
// set value
|
||||
this.headers['poe-formkey'] = this.config.quora_formkey
|
||||
this.headers['poe-tchannel'] = this.config.channel_name
|
||||
this.headers.Cookie = this.config.quora_cookie
|
||||
console.log(this.headers)
|
||||
}
|
||||
|
||||
async subscribe () {
|
||||
const query = {
|
||||
queryName: 'subscriptionsMutation',
|
||||
variables: {
|
||||
subscriptions: [
|
||||
{
|
||||
subscriptionName: 'messageAdded',
|
||||
query: 'subscription subscriptions_messageAdded_Subscription(\n $chatId: BigInt!\n) {\n messageAdded(chatId: $chatId) {\n id\n messageId\n creationTime\n state\n ...ChatMessage_message\n ...chatHelpers_isBotMessage\n }\n}\n\nfragment ChatMessageDownvotedButton_message on Message {\n ...MessageFeedbackReasonModal_message\n ...MessageFeedbackOtherModal_message\n}\n\nfragment ChatMessageDropdownMenu_message on Message {\n id\n messageId\n vote\n text\n ...chatHelpers_isBotMessage\n}\n\nfragment ChatMessageFeedbackButtons_message on Message {\n id\n messageId\n vote\n voteReason\n ...ChatMessageDownvotedButton_message\n}\n\nfragment ChatMessageOverflowButton_message on Message {\n text\n ...ChatMessageDropdownMenu_message\n ...chatHelpers_isBotMessage\n}\n\nfragment ChatMessageSuggestedReplies_SuggestedReplyButton_message on Message {\n messageId\n}\n\nfragment ChatMessageSuggestedReplies_message on Message {\n suggestedReplies\n ...ChatMessageSuggestedReplies_SuggestedReplyButton_message\n}\n\nfragment ChatMessage_message on Message {\n id\n messageId\n text\n author\n linkifiedText\n state\n ...ChatMessageSuggestedReplies_message\n ...ChatMessageFeedbackButtons_message\n ...ChatMessageOverflowButton_message\n ...chatHelpers_isHumanMessage\n ...chatHelpers_isBotMessage\n ...chatHelpers_isChatBreak\n ...chatHelpers_useTimeoutLevel\n ...MarkdownLinkInner_message\n}\n\nfragment MarkdownLinkInner_message on Message {\n messageId\n}\n\nfragment MessageFeedbackOtherModal_message on Message {\n id\n messageId\n}\n\nfragment MessageFeedbackReasonModal_message on Message {\n id\n messageId\n}\n\nfragment chatHelpers_isBotMessage on Message {\n ...chatHelpers_isHumanMessage\n ...chatHelpers_isChatBreak\n}\n\nfragment chatHelpers_isChatBreak on Message {\n author\n}\n\nfragment chatHelpers_isHumanMessage on Message {\n author\n}\n\nfragment chatHelpers_useTimeoutLevel on Message {\n id\n state\n text\n messageId\n}\n'
|
||||
},
|
||||
{
|
||||
subscriptionName: 'viewerStateUpdated',
|
||||
query: 'subscription subscriptions_viewerStateUpdated_Subscription {\n viewerStateUpdated {\n id\n ...ChatPageBotSwitcher_viewer\n }\n}\n\nfragment BotHeader_bot on Bot {\n displayName\n ...BotImage_bot\n}\n\nfragment BotImage_bot on Bot {\n profilePicture\n displayName\n}\n\nfragment BotLink_bot on Bot {\n displayName\n}\n\nfragment ChatPageBotSwitcher_viewer on Viewer {\n availableBots {\n id\n ...BotLink_bot\n ...BotHeader_bot\n }\n}\n'
|
||||
}
|
||||
]
|
||||
},
|
||||
query: 'mutation subscriptionsMutation(\n $subscriptions: [AutoSubscriptionQuery!]!\n) {\n autoSubscribe(subscriptions: $subscriptions) {\n viewer {\n id\n }\n }\n}\n'
|
||||
}
|
||||
|
||||
await this.makeRequest(query)
|
||||
}
|
||||
|
||||
async makeRequest (request) {
|
||||
let payload = JSON.stringify(request)
|
||||
let baseString = payload + this.headers['poe-formkey'] + 'WpuLMiXEKKE98j56k'
|
||||
const md5 = crypto.createHash('md5').update(baseString).digest('hex')
|
||||
const response = await fetch('https://poe.com/api/gql_POST', {
|
||||
method: 'POST',
|
||||
headers: Object.assign(this.headers, {
|
||||
'poe-tag-id': md5,
|
||||
'content-type': 'application/json'
|
||||
}),
|
||||
body: payload
|
||||
})
|
||||
let text = await response.text()
|
||||
try {
|
||||
let result = JSON.parse(text)
|
||||
console.log({ result })
|
||||
return result
|
||||
} catch (e) {
|
||||
console.error(text)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async getBot (displayName) {
|
||||
let r
|
||||
let retry = 10
|
||||
while (retry >= 0) {
|
||||
let url = `https://poe.com/_next/data/${this.nextData.buildId}/${displayName}.json`
|
||||
let r = await fetch(url, {
|
||||
headers: this.headers
|
||||
})
|
||||
let res = await r.text()
|
||||
try {
|
||||
let chatData = (JSON.parse(res)).pageProps.payload.chatOfBotDisplayName
|
||||
return chatData
|
||||
} catch (e) {
|
||||
r = res
|
||||
retry--
|
||||
}
|
||||
}
|
||||
throw new Error(r)
|
||||
}
|
||||
|
||||
async getChatId () {
|
||||
let r = await fetch('https://poe.com', {
|
||||
headers: this.headers
|
||||
})
|
||||
let text = await r.text()
|
||||
const jsonRegex = /<script id="__NEXT_DATA__" type="application\/json">(.+?)<\/script>/
|
||||
const jsonText = text.match(jsonRegex)[1]
|
||||
const nextData = JSON.parse(jsonText)
|
||||
this.nextData = nextData
|
||||
this.viewer = nextData.props.pageProps.payload.viewer
|
||||
this.formkey = nextData.props.formkey
|
||||
let bots = this.viewer.availableBots
|
||||
this.bots = {}
|
||||
for (let i = 0; i < bots.length; i++) {
|
||||
let bot = bots[i]
|
||||
let chatData = await this.getBot(bot.displayName)
|
||||
this.bots[chatData.defaultBotObject.nickname] = chatData
|
||||
}
|
||||
console.log(this.bots)
|
||||
}
|
||||
|
||||
async clearContext (bot) {
|
||||
try {
|
||||
const data = await this.makeRequest({
|
||||
query: `${queries.addMessageBreakMutation}`,
|
||||
variables: { chatId: this.config.chat_ids[bot] }
|
||||
})
|
||||
|
||||
if (!data.data) {
|
||||
this.reConnectWs = true // for websocket purpose
|
||||
console.log('ON TRY! Could not clear context! Trying to reLogin..')
|
||||
}
|
||||
return data
|
||||
} catch (e) {
|
||||
this.reConnectWs = true // for websocket purpose
|
||||
console.log('ON CATCH! Could not clear context! Trying to reLogin..')
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
async sendMsg (bot, query) {
|
||||
try {
|
||||
const data = await this.makeRequest({
|
||||
query: `${queries.addHumanMessageMutation}`,
|
||||
variables: {
|
||||
bot,
|
||||
chatId: this.bots[bot].chatId,
|
||||
query,
|
||||
source: null,
|
||||
withChatBreak: false
|
||||
}
|
||||
})
|
||||
console.log(data)
|
||||
if (!data.data) {
|
||||
this.reConnectWs = true // for cli websocket purpose
|
||||
console.log('Could not send message! Trying to reLogin..')
|
||||
}
|
||||
return data
|
||||
} catch (e) {
|
||||
this.reConnectWs = true // for cli websocket purpose
|
||||
console.error(e)
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
async getHistory (bot) {
|
||||
try {
|
||||
let response = await this.makeRequest({
|
||||
query: `${queries.chatPaginationQuery}`,
|
||||
variables: {
|
||||
before: null,
|
||||
bot,
|
||||
last: 25
|
||||
}
|
||||
})
|
||||
|
||||
return response.data.chatOfBot.messagesConnection.edges
|
||||
.map(({ node: { messageId, text, authorNickname } }) => ({
|
||||
messageId,
|
||||
text,
|
||||
authorNickname
|
||||
}))
|
||||
} catch (e) {
|
||||
console.log('There has been an error while fetching your history!')
|
||||
}
|
||||
}
|
||||
|
||||
async deleteMessages (msgIds) {
|
||||
await this.makeRequest({
|
||||
queryName: 'MessageDeleteConfirmationModal_deleteMessageMutation_Mutation',
|
||||
variables: {
|
||||
messageIds: msgIds
|
||||
},
|
||||
query: 'mutation MessageDeleteConfirmationModal_deleteMessageMutation_Mutation(\n $messageIds: [BigInt!]!\n){\n messagesDelete(messageIds: $messageIds) {\n edgeIds\n }\n}\n'
|
||||
})
|
||||
}
|
||||
|
||||
async getResponse (bot) {
|
||||
let text
|
||||
let state
|
||||
let authorNickname
|
||||
try {
|
||||
while (true) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||
let response = await this.makeRequest({
|
||||
query: `${queries.chatPaginationQuery}`,
|
||||
variables: {
|
||||
before: null,
|
||||
bot,
|
||||
last: 1
|
||||
}
|
||||
})
|
||||
let base = response.data.chatOfBot.messagesConnection.edges
|
||||
let lastEdgeIndex = base.length - 1
|
||||
text = base[lastEdgeIndex].node.text
|
||||
authorNickname = base[lastEdgeIndex].node.authorNickname
|
||||
state = base[lastEdgeIndex].node.state
|
||||
if (state === 'complete' && authorNickname === bot) {
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Could not get response!')
|
||||
return {
|
||||
status: false,
|
||||
message: 'failed',
|
||||
data: null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: true,
|
||||
message: 'success',
|
||||
data: text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function testPoe () {
|
||||
// const key = 'deb04db9f2332a3287b7d2545061af62'
|
||||
// const channel = 'poe-chan55-8888-ujygckefewomybvkqfrp'
|
||||
const cookie = 'p-b=WSvmyvjHVJoMtQVkirtn-A%3D%3D'
|
||||
let client = new PoeClient({
|
||||
// quora_formkey: key,
|
||||
// channel_name: channel,
|
||||
quora_cookie: cookie
|
||||
})
|
||||
await client.setCredentials()
|
||||
await client.getChatId()
|
||||
let ai = 'a2'
|
||||
await client.sendMsg(ai, '你说话不是很通顺啊')
|
||||
const response = await client.getResponse(ai)
|
||||
return response
|
||||
}
|
||||
|
||||
// testPoe().then(res => {
|
||||
// console.log(res)
|
||||
// })
|
||||
65
utils/poe/websocket 2.js
Normal file
65
utils/poe/websocket 2.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import WebSocket from 'ws'
|
||||
import * as diff from 'diff'
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
const getSocketUrl = async () => {
|
||||
const tchRand = Math.floor(100000 + Math.random() * 900000) // They're surely using 6 digit random number for ws url.
|
||||
const socketUrl = `wss://tch${tchRand}.tch.quora.com`
|
||||
const credentials = JSON.parse(readFileSync('config.json', 'utf8'))
|
||||
const appSettings = credentials.app_settings.tchannelData
|
||||
const boxName = appSettings.boxName
|
||||
const minSeq = appSettings.minSeq
|
||||
const channel = appSettings.channel
|
||||
const hash = appSettings.channelHash
|
||||
return `${socketUrl}/up/${boxName}/updates?min_seq=${minSeq}&channel=${channel}&hash=${hash}`
|
||||
}
|
||||
|
||||
export const connectWs = async () => {
|
||||
const url = await getSocketUrl()
|
||||
const ws = new WebSocket(url)
|
||||
return new Promise((resolve, reject) => {
|
||||
ws.on('open', function open () {
|
||||
console.log('Connected to websocket')
|
||||
return resolve(ws)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const disconnectWs = async (ws) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
ws.on('close', function close () {
|
||||
return resolve(true)
|
||||
})
|
||||
ws.close()
|
||||
})
|
||||
}
|
||||
|
||||
export const listenWs = async (ws) => {
|
||||
let previousText = ''
|
||||
return new Promise((resolve, reject) => {
|
||||
const onMessage = function incoming (data) {
|
||||
let jsonData = JSON.parse(data)
|
||||
if (jsonData.messages && jsonData.messages.length > 0) {
|
||||
const messages = JSON.parse(jsonData.messages[0])
|
||||
const dataPayload = messages.payload.data
|
||||
const text = dataPayload.messageAdded.text
|
||||
const state = dataPayload.messageAdded.state
|
||||
if (state !== 'complete') {
|
||||
const differences = diff.diffChars(previousText, text)
|
||||
let result = ''
|
||||
differences.forEach((part) => {
|
||||
if (part.added) {
|
||||
result += part.value
|
||||
}
|
||||
})
|
||||
previousText = text
|
||||
process.stdout.write(result)
|
||||
} else {
|
||||
ws.removeListener('message', onMessage)
|
||||
return resolve(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
ws.on('message', onMessage)
|
||||
})
|
||||
}
|
||||
|
|
@ -11,20 +11,20 @@ export class EditCardTool extends AbstractTool {
|
|||
},
|
||||
card: {
|
||||
type: 'string',
|
||||
description: '你想给他改的新名片'
|
||||
description: 'the new card'
|
||||
},
|
||||
groupId: {
|
||||
type: 'string',
|
||||
description: '群号'
|
||||
description: 'group number'
|
||||
}
|
||||
},
|
||||
required: ['card', 'groupId']
|
||||
}
|
||||
|
||||
description = '当你想要修改某个群员的群名片时有用。输入应该是群号、qq号和群名片,用空格隔开。'
|
||||
description = 'Useful when you want to edit someone\'s card in the group(群名片)'
|
||||
|
||||
func = async function (opts) {
|
||||
let {qq, card, groupId} = opts
|
||||
let { qq, card, groupId } = opts
|
||||
groupId = parseInt(groupId.trim())
|
||||
qq = parseInt(qq.trim())
|
||||
logger.info('edit card: ', groupId, qq)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export class HandleMessageMsgTool extends AbstractTool {
|
|||
},
|
||||
messageId: {
|
||||
type: 'string',
|
||||
description: 'which message, current one by default'
|
||||
description: 'which message to handle, current one by default'
|
||||
}
|
||||
},
|
||||
required: ['type']
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { AbstractTool } from './AbstractTool.js'
|
|||
import { getMasterQQ } from '../common.js'
|
||||
|
||||
export class QueryUserinfoTool extends AbstractTool {
|
||||
name = 'sendDice'
|
||||
name = 'queryUser'
|
||||
|
||||
parameters = {
|
||||
properties: {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import {AbstractTool} from "./AbstractTool.js";
|
||||
|
||||
import { AbstractTool } from './AbstractTool.js'
|
||||
|
||||
export class SendDiceTool extends AbstractTool {
|
||||
name = 'sendDice'
|
||||
|
|
@ -19,7 +18,7 @@ export class SendDiceTool extends AbstractTool {
|
|||
}
|
||||
|
||||
func = async function (opts) {
|
||||
let {num, groupId} = opts
|
||||
let { num, groupId } = opts
|
||||
let groupList = await Bot.getGroupList()
|
||||
if (groupList.get(groupId)) {
|
||||
let group = await Bot.pickGroup(groupId, true)
|
||||
|
|
@ -28,7 +27,7 @@ export class SendDiceTool extends AbstractTool {
|
|||
let friend = await Bot.pickFriend(groupId)
|
||||
await friend.sendMsg(segment.dice(num))
|
||||
}
|
||||
return `the dice has been sent`
|
||||
return 'the dice has been sent'
|
||||
}
|
||||
|
||||
description = 'If you want to roll dice, use this tool. If you know the group number, use the group number instead of the qq number first. The input should be the number of dice to be cast (1-6) and the target group number or qq number,and they should be concat with a space'
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export class SendPictureTool extends AbstractTool {
|
|||
},
|
||||
groupId: {
|
||||
type: 'string',
|
||||
description: '群号或qq号,发送目标,为空则发送到当前聊天'
|
||||
description: 'the group number or qq number, will send to current conversation if leave blank'
|
||||
}
|
||||
},
|
||||
required: ['picture']
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { AbstractTool } from './AbstractTool.js'
|
||||
import {Config} from "../config.js";
|
||||
import {Config} from '../config.js';
|
||||
|
||||
export class WeatherTool extends AbstractTool {
|
||||
name = 'weather'
|
||||
|
|
@ -17,7 +17,9 @@ export class WeatherTool extends AbstractTool {
|
|||
func = async function (opts) {
|
||||
let { city } = opts
|
||||
let key = Config.amapKey
|
||||
|
||||
if (!key) {
|
||||
return 'query failed: you don\'t provide API key of 高德'
|
||||
}
|
||||
let adcodeRes = await fetch(`https://restapi.amap.com/v3/config/district?keywords=${city}&subdistrict=1&key=${key}`)
|
||||
adcodeRes = await adcodeRes.json()
|
||||
let adcode = adcodeRes.districts[0]?.adcode
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue