Merge branch '18.0' of github.com:guohuadeng/app-odoo into 18.0
4
app_ai_bard/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# from . import controllers
|
||||||
|
from . import models
|
||||||
58
app_ai_bard/__manifest__.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Created on 2023-02-016
|
||||||
|
# author: 欧度智能,https://www.odooai.cn
|
||||||
|
# email: 300883@qq.com
|
||||||
|
# resource of odooai
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
{
|
||||||
|
'name': 'Google Bard Ai for odoo ai center, 谷歌Ai支持',
|
||||||
|
'version': '24.11.06',
|
||||||
|
'author': 'odooai.cn',
|
||||||
|
'company': 'odooai.cn',
|
||||||
|
'maintainer': 'odooai.cn',
|
||||||
|
'category': 'Website/Website',
|
||||||
|
'website': 'https://www.odooai.cn',
|
||||||
|
'live_test_url': 'https://demo.odooapp.cn',
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
'sequence': 10,
|
||||||
|
'images': ['static/description/banner.gif'],
|
||||||
|
'summary': '''
|
||||||
|
Google Bard Ai for Odoo AI Center. Ai Aigc Center including Google Bard Ai, Azure Ai, Baidu Ai.
|
||||||
|
Support chatgpt 4 image. DALLE, Integration All ChatGpt Api and Azure OpenAI Service.
|
||||||
|
Easy Chat channel with several ChatGPT Robots and train.
|
||||||
|
''',
|
||||||
|
'description': '''
|
||||||
|
Chat with google bard ai with odoo.
|
||||||
|
Allows the application to leverage the capabilities of the GPT language model to generate human-like responses,
|
||||||
|
providing a more natural and intuitive user experience.
|
||||||
|
odoo bard connector.
|
||||||
|
1. Multi ChatGpt openAI robot Connector. Chat and train.
|
||||||
|
2. Multi Ai support including Google Bard Ai, Azure Ai, Chatgpt 4, Chatgpt 3.5 Turbo, Chatgpt 3 Davinci, Chatgpt 2 Code Optimized, 'Dall-E Image.
|
||||||
|
3. Bind ChatGpt Api to user. So we can chat to robot user or use ChatGpt Channel for Group Chat.
|
||||||
|
4. White and black List for ChatGpt.
|
||||||
|
5. Setup Demo Chat time for every new user.
|
||||||
|
6. Easy Start and Stop ChatGpt.
|
||||||
|
7. Evaluation the ai robot to make better response. This training.
|
||||||
|
8. Add api support Connect the Microsoft Azure OpenAI Service.
|
||||||
|
9. Can set Synchronous or Asynchronous mode for Ai response.
|
||||||
|
10.Filter Sensitive Words Setup.
|
||||||
|
11. Multi-language Support. Multi-Company Support.
|
||||||
|
12. Support Odoo 18,17,16,15,14,13,12, Enterprise and Community and odoo.sh Edition.
|
||||||
|
13. Full Open Source.
|
||||||
|
''',
|
||||||
|
'depends': [
|
||||||
|
'app_chatgpt',
|
||||||
|
],
|
||||||
|
'data': [
|
||||||
|
'data/ai_robot_data.xml',
|
||||||
|
'data/user_partner_data.xml',
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
},
|
||||||
|
'external_dependencies': {'python': ['bardapi']},
|
||||||
|
'installable': True,
|
||||||
|
'application': True,
|
||||||
|
'auto_install': False,
|
||||||
|
}
|
||||||
3
app_ai_bard/controllers/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import main
|
||||||
10
app_ai_bard/controllers/main.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import http
|
||||||
|
|
||||||
|
|
||||||
|
class ChatgptController(http.Controller):
|
||||||
|
@http.route(['/chatgpt_form'], type='http', auth="public", csrf=False,
|
||||||
|
website=True)
|
||||||
|
def question_submit(self):
|
||||||
|
return http.request.render('app_chatgpt.connector')
|
||||||
13
app_ai_bard/data/ai_robot_data.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="robot_google_bard" model="ai.robot">
|
||||||
|
<field name="name">Google Bard</field>
|
||||||
|
<field name="provider">google</field>
|
||||||
|
<field name="ai_model">google-bard</field>
|
||||||
|
<field name="api_version" eval="False"/>
|
||||||
|
<field name="endpoint">https://api.bard.ai/v1/text/generate</field>
|
||||||
|
<field name="sequence">9</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
19
app_ai_bard/data/user_partner_data.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="partner_google_bard" model="res.partner">
|
||||||
|
<field name="name">Google Bard</field>
|
||||||
|
<field name="image_1920" type="base64" file="app_ai_bard/static/src/img/bard.gif"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="user_google_bard" model="res.users">
|
||||||
|
<field name="login">ai_bard@example.com</field>
|
||||||
|
<field name="email">ai_bard@example.com</field>
|
||||||
|
<field name="partner_id" ref="partner_google_bard"/>
|
||||||
|
<field name="gpt_id" ref="robot_google_bard"/>
|
||||||
|
<field name="company_id" ref="base.main_company"/>
|
||||||
|
<field name="company_ids" eval="[Command.link(ref('base.main_company'))]"/>
|
||||||
|
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
0
app_ai_bard/i18n/zh_CN.po
Normal file
3
app_ai_bard/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import ai_robot
|
||||||
59
app_ai_bard/models/ai_robot.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import requests, json
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
# todo: 暂时直接 requests
|
||||||
|
# from bardapi import Bard
|
||||||
|
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AiRobot(models.Model):
|
||||||
|
_inherit = 'ai.robot'
|
||||||
|
|
||||||
|
provider = fields.Selection(
|
||||||
|
selection_add=[('google', 'Google Ai')],
|
||||||
|
ondelete={'google': 'set default'}
|
||||||
|
)
|
||||||
|
set_ai_model = fields.Selection(
|
||||||
|
selection_add=[('google-bard', 'Google Bard')],
|
||||||
|
ondelete={'google-bard': 'set default'})
|
||||||
|
|
||||||
|
@api.onchange('provider')
|
||||||
|
def _onchange_provider(self):
|
||||||
|
if self.provider == 'google':
|
||||||
|
self.endpoint = 'https://api.bard.ai/v1/text/generate'
|
||||||
|
return super()._onchange_provider()
|
||||||
|
|
||||||
|
def get_google(self, data, author_id, answer_id, param={}):
|
||||||
|
self.ensure_one()
|
||||||
|
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {self.openapi_api_key}"}
|
||||||
|
R_TIMEOUT = self.ai_timeout or 120
|
||||||
|
o_url = self.endpoint or "https://api.bard.ai/v1/text/generate"
|
||||||
|
|
||||||
|
# todo: 更多参数如 prompt, max_length
|
||||||
|
max_tokens = param.get('max_tokens') if param.get('max_tokens') else self.max_tokens
|
||||||
|
temperature = param.get('temperature') if param.get('temperature') else self.temperature
|
||||||
|
pdata = {
|
||||||
|
"text": data,
|
||||||
|
"max_length": max_tokens,
|
||||||
|
"temperature": temperature,
|
||||||
|
}
|
||||||
|
response = requests.post(o_url, data=json.dumps(pdata), headers=headers, timeout=R_TIMEOUT)
|
||||||
|
response.raise_for_status()
|
||||||
|
try:
|
||||||
|
res = response.json()['text']
|
||||||
|
return res
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning("Get Response Json failed: %s", e)
|
||||||
|
else:
|
||||||
|
_logger.warning('=====================Openai output data: %s' % response.json())
|
||||||
|
|
||||||
|
def get_google_post(self, res, author_id=False, answer_id=False, param={}):
|
||||||
|
if self.provider == 'google':
|
||||||
|
content = res['text']
|
||||||
|
return content, False, True
|
||||||
0
app_ai_bard/security/odooai.cn
Normal file
BIN
app_ai_bard/static/description/banner.gif
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
app_ai_bard/static/description/banner.png
Normal file
|
After Width: | Height: | Size: 217 KiB |
BIN
app_ai_bard/static/description/bard.gif
Normal file
|
After Width: | Height: | Size: 271 KiB |
BIN
app_ai_bard/static/description/chatgpt.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
app_ai_bard/static/description/chatgpt4_azure.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
app_ai_bard/static/description/chatgpt_blue.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
app_ai_bard/static/description/chatgpt_green.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
app_ai_bard/static/description/demo01.jpg
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
app_ai_bard/static/description/demo02.jpg
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
app_ai_bard/static/description/demo03.jpg
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
app_ai_bard/static/description/demo04.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
app_ai_bard/static/description/demo1.jpg
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
app_ai_bard/static/description/demo2.jpg
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
app_ai_bard/static/description/demo3.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
app_ai_bard/static/description/demo4.jpg
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
app_ai_bard/static/description/demo5.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
app_ai_bard/static/description/demo6.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
app_ai_bard/static/description/demo7.jpg
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
app_ai_bard/static/description/demo71.jpg
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
app_ai_bard/static/description/demo8.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
app_ai_bard/static/description/demo81.jpg
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
app_ai_bard/static/description/demo9.jpg
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
app_ai_bard/static/description/demo91.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
app_ai_bard/static/description/demoa.jpg
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
app_ai_bard/static/description/demob.jpg
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
app_ai_bard/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
263
app_ai_bard/static/description/index.html
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced" >
|
||||||
|
<div class="row">
|
||||||
|
<h2 class="oe_slogan"> Google Bard Ai for odoo ai center</h2>
|
||||||
|
<h3 class="oe_slogan"> Ai center addons. all aigc in one. </h3>
|
||||||
|
<div class="oe_row">
|
||||||
|
<h3>Latest update: v18.24.11.06</h3>
|
||||||
|
<div class="row">
|
||||||
|
<div class="row">
|
||||||
|
<img class="" src="bard.gif">
|
||||||
|
Add google bard support, update chatgpt api</div>
|
||||||
|
<img class="oe_demo oe_screenshot img img-fluid" src="demo02.jpg">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<img class="oe_demo oe_screenshot img img-fluid" style="max-height: 100%;" src="banner.png">
|
||||||
|
</div>
|
||||||
|
<div class="oe_span12 oe_spaced">
|
||||||
|
<div class="alert alert-info" style="padding:8px;font-weight: 300; font-size: 20px;">
|
||||||
|
<i class="fa fa-hand-o-right"></i><b> Key features: </b>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
1. Multi ChatGpt openAI robot Connector. Chat and train.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
2. Multi Ai support including Google Bard Ai, Azure Ai, Chatgpt 4, Chatgpt 3.5 Turbo, Chatgpt 3 Davinci, Chatgpt 2 Code Optimized, 'Dall-E Image.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
3. Bind ChatGpt Api to user. So we can chat to robot user or use ChatGpt Channel for Group Chat.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
4. White and black List for ChatGpt.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
5. Setup Demo Chat time for every new user.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
6. Easy Start and Stop ChatGpt.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
7. Evaluation the ai robot to make better response. This training.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
8. Add api support Connect the Microsoft Azure OpenAI Service.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
9. Can set Synchronous or Asynchronous mode for Ai response.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
10.Filter Sensitive Words Setup.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
11. Multi-language Support. Multi-Company Support.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
12. Support Odoo 18,17,16,15,14,13,12, Enterprise and Community and odoo.sh Edition.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
13. Full Open Source.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h2 class="bg-warning text-center pt8 pb8 mt16 mb16">Add more Ai support like google bard, chatgpt 4, baidu china</h2>
|
||||||
|
<h4 class="oe_slogan"> Need to navigate to odoo app store to buy and install addons</h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo01.jpg"/>
|
||||||
|
</div>
|
||||||
|
<h4 class="oe_slogan">Please apply for the bard api first from google</h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo03.jpg"/>
|
||||||
|
</div>
|
||||||
|
<h4 class="oe_slogan">Setup for your own key</h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo04.jpg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h2 class="bg-warning text-center pt8 pb8 mt16 mb16">Easy to use Ai Robot with multi Provider. Easy chat, easy help</h2>
|
||||||
|
<h4 class="oe_slogan"> Open Ai for more smart. Microsoft Azure chatgpt for china user.</h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demob.jpg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h2 class="bg-warning text-center pt8 pb8 mt16 mb16">1. Multi ChatGpt openAI robot Connector. Chat and train.</h2>
|
||||||
|
<h4 class="oe_slogan"> Goto Setting--> GPT Robot to setup your robot api. </h4>
|
||||||
|
<p> Input your api key, And Select the api model you need to use.</p>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo1.jpg"/>
|
||||||
|
</div>
|
||||||
|
<p> You can set the Temperature higer for more creative answer.</p>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo2.jpg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h2 class="bg-warning text-center pt8 pb8 mt16 mb16">2. Multi Api support, Chatgpt 3.5 Turbo, Chatgpt 3 Davinci, Chatgpt 2 Code Optimized, 'Dall-E Image.</h2>
|
||||||
|
<h4 class="oe_slogan"> Choose the model you want to use</h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo2.jpg"/>
|
||||||
|
</div>
|
||||||
|
<p> You can set the Temperature higer for more creative answer.</p>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo3.jpg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h2 class="bg-warning text-center pt8 pb8 mt16 mb16">3. Bind ChatGpt Api to user. So we can chat to robot user or use ChatGpt Channel for Group Chat.</h2>
|
||||||
|
<h4 class="oe_slogan"> Go Settings ->users, bind chatgpt to some user.</h4>
|
||||||
|
<img src="demo4.jpg"/>
|
||||||
|
</div>
|
||||||
|
<h4 class="oe_slogan"> So you can have many user, and many chatgpt robot. This provide you an Ai pool.</h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo5.jpg"/>
|
||||||
|
</div>
|
||||||
|
<h4 class="oe_slogan"> You can set the blacklist to this chatgpt robot to limit request. Also you can setup Demo time for every normal user..</h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo6.jpg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h2 class="bg-warning text-center pt8 pb8 mt16 mb16">4. White and black List for ChatGpt.</h2>
|
||||||
|
<h2 class="bg-warning text-center pt8 pb8 mt16 mb16">5. Setup Demo Chat time for every new user.</h2>
|
||||||
|
<h4 class="oe_slogan"> You can set the blacklist to this chatgpt robot to limit request. Also you can setup Demo time for every normal user..</h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo6.jpg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h2 class="bg-warning text-center pt8 pb8 mt16 mb16">6. Easy Start and Stop ChatGpt..</h2>
|
||||||
|
<h4 class="oe_slogan"> You can easy chat with the apt robot with odoo IM</h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo7.jpg"/>
|
||||||
|
</div>
|
||||||
|
<h4 class="oe_slogan"> You can chat with several robot in the same time</h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo8.jpg"/>
|
||||||
|
</div>
|
||||||
|
<h4 class="oe_slogan"> If you have more than 1 robot in the group. you can @ the specify robot.</h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo9.jpg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h2 class="bg-warning text-center pt8 pb8 mt16 mb16">7. Evaluation the ai robot to make better response. This training.</h2>
|
||||||
|
<h4 class="oe_slogan"> You can Evaluation chatgpt's answer. Mark as good for good answer. Mark as back for bad answer.</h4>
|
||||||
|
<p> With Evaluation, you can make your ai robot more smart.
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo71.jpg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h2 class="bg-warning text-center pt8 pb8 mt16 mb16">8. Add api support Connect the Microsoft Azure OpenAI Service.</h2>
|
||||||
|
<h4 class="oe_slogan"> Azure openai add. It is for china and other country which no chatgpt service.</h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo81.jpg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h2 class="bg-warning text-center pt8 pb8 mt16 mb16">9. Can set Synchronous or Asynchronous mode for Ai response.</h2>
|
||||||
|
<h4 class="oe_slogan"> Synchronous(default) mode can get response then ask question again. Asynchronous mode would make you do other thing when waiting for response.</h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo91.jpg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h2 class="bg-warning text-center pt8 pb8 mt16 mb16">Multi-language Support..</h2>
|
||||||
|
<h4 class="oe_slogan"> </h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="cnreadme.jpg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="container oe_dark">
|
||||||
|
<div class="oe_row oe_spaced text-center">
|
||||||
|
<div class="row">
|
||||||
|
<h2 class="oe_slogan">Technical Help & Support</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 pad0">
|
||||||
|
<div class="oe_mt16">
|
||||||
|
<p><h4>
|
||||||
|
For any type of technical help & support requests, Feel free to contact us</h4></p>
|
||||||
|
<a style="background: #002e5a none repeat scroll 0% 0%; color: rgb(255, 255, 255);position: relative; overflow: hidden;"
|
||||||
|
class="btn btn-warning btn-lg" rel="nofollow" href="mailto:odoo@china.com"><span
|
||||||
|
style="height: 354px; width: 354px; top: -147.433px; left: -6.93335px;" class="o_ripple"></span>
|
||||||
|
<i class="fa fa-envelope"></i> odoo@china.com</a>
|
||||||
|
<p><h4>
|
||||||
|
Via QQ: 300883 (App user would not get QQ or any other IM support. Only for odoo project customize.)</h4></p>
|
||||||
|
<a style="background: #002e5a none repeat scroll 0% 0%; color: rgb(255, 255, 255);position: relative; overflow: hidden;"
|
||||||
|
class="btn btn-warning btn-lg" rel="nofollow" href="mailto:300883@qq.com"><span
|
||||||
|
style="height: 354px; width: 354px; top: -147.433px; left: -6.93335px;" class="o_ripple"></span>
|
||||||
|
<i class="fa fa-envelope"></i> 300883@qq.com</a>
|
||||||
|
</div>
|
||||||
|
<div class="oe_mt16">
|
||||||
|
<p><h4>
|
||||||
|
Visit our website for more support.</h4></p>
|
||||||
|
<a style="background: #002e5a none repeat scroll 0% 0%; color: rgb(255, 255, 255);position: relative; overflow: hidden;"
|
||||||
|
class="btn btn-warning btn-lg" rel="nofollow" href="https://www.odooai.cn" target="_blank"><span
|
||||||
|
style="height: 354px; width: 354px; top: -147.433px; left: -6.93335px;" class="o_ripple"></span>
|
||||||
|
<i class="fa fa-web"></i>https://www.odooai.cn</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="oe_row oe_spaced text-center">
|
||||||
|
<h2>More Powerful addons, Make your odoo very easy to use, easy customize:
|
||||||
|
<a class="btn btn-primary mb16" href="http://www.odoo.com/apps/modules/browse?author=odooai.cn">odooai.cn Odoo Addons</a>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
BIN
app_ai_bard/static/src/img/bard.gif
Normal file
|
After Width: | Height: | Size: 271 KiB |
BIN
app_ai_bard/static/src/img/google.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
0
app_ai_bard/static/src/odooai.cn
Normal file
100
app_ai_bard/views/ai_robot_views.xml
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="ai_robot_tree_view" model="ir.ui.view">
|
||||||
|
<field name="name">ai.robot.tree</field>
|
||||||
|
<field name="model">ai.robot</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="sequence" widget="handle"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="provider" optional="hide"/>
|
||||||
|
<field name="ai_model" optional="show"/>
|
||||||
|
<field name="openapi_api_key" password="True"/>
|
||||||
|
<field name="max_tokens" optional="show"/>
|
||||||
|
<field name="temperature"/>
|
||||||
|
<field name="max_send_char"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="ai_robot_form_view" model="ir.ui.view">
|
||||||
|
<field name="name">ai.robot.form</field>
|
||||||
|
<field name="model">ai.robot</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<header>
|
||||||
|
<button string="Get List Model" type="object" name="get_ai_list_model"/>
|
||||||
|
<button string="Get Model Info" type="object" name="get_ai_model_info"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<label for="name"/>
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="Robot Name" required="1"/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="openapi_api_key" password="True" required="True"/>
|
||||||
|
<field name="temperature"/>
|
||||||
|
<field name="top_p"/>
|
||||||
|
<field name="frequency_penalty"/>
|
||||||
|
<field name="presence_penalty"/>
|
||||||
|
<field name="sys_content" placeholder="Role-playing and scene setting.Give the model instructions about how it should behave and any context it should reference when generating a response."/>
|
||||||
|
<field name="max_send_char"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="ai_model"/>
|
||||||
|
<label class="o_form_label" for="provider">
|
||||||
|
OpenAI Document
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<field name="provider"/>
|
||||||
|
<a href="https://platform.openai.com/docs/introduction" title="OpenAI Document" class="o_doc_link" target="_blank"></a>
|
||||||
|
</div>
|
||||||
|
<field name="max_tokens"/>
|
||||||
|
<field name="engine"/>
|
||||||
|
<field name="endpoint"/>
|
||||||
|
<field name="api_version"/>
|
||||||
|
<field name="ai_timeout"/>
|
||||||
|
<field name="sequence"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="is_filtering"/>
|
||||||
|
<field name="sensitive_words" attrs="{'invisible': [('is_filtering', '=', False)]}"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_ai_robot" model="ir.actions.act_window">
|
||||||
|
<field name="name">GPT Robot</field>
|
||||||
|
<field name="res_model">ai.robot</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Let's create a GPT Robot.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="model_ai_robot_action_disconnect" model="ir.actions.server">
|
||||||
|
<field name="name">Disconnect</field>
|
||||||
|
<field name="model_id" ref="app_chatgpt.model_ai_robot"/>
|
||||||
|
<field name="binding_model_id" ref="app_chatgpt.model_ai_robot"/>
|
||||||
|
<field name="binding_view_types">list,form</field>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">action = records.action_disconnect()</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
id="menu_ai_robot"
|
||||||
|
name="GPT Robot"
|
||||||
|
parent="base.menu_users"
|
||||||
|
sequence="2"
|
||||||
|
action="action_ai_robot"
|
||||||
|
groups="base.group_system"/>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
94
app_ai_bard/views/res_partner_ai_use_views.xml
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="res_partner_ai_use_tree_view" model="ir.ui.view">
|
||||||
|
<field name="name">res.partner.ai.use.tree</field>
|
||||||
|
<field name="model">res.partner.ai.use</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="ai_user_id" optional="show"/>
|
||||||
|
<field name="first_ask_time" optional="show"/>
|
||||||
|
<field name="latest_ask_time" optional="show"/>
|
||||||
|
<field name="service_start_date" optional="show"/>
|
||||||
|
<field name="service_end_date" optional="show"/>
|
||||||
|
<field name="used_number" sum="Total" optional="hide"/>
|
||||||
|
<field name="max_number" sum="Total" optional="hide"/>
|
||||||
|
<field name="human_prompt_tokens" sum="Total" optional="show"/>
|
||||||
|
<field name="ai_completion_tokens" sum="Total" optional="show"/>
|
||||||
|
<field name="tokens_total" sum="Total" optional="show"/>
|
||||||
|
<field name="token_balance" sum="Total" optional="show"/>
|
||||||
|
<field name="token_allow" sum="Total" optional="show"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="res_partner_ai_use_form_view" model="ir.ui.view">
|
||||||
|
<field name="name">res.partner.ai.use.form</field>
|
||||||
|
<field name="model">res.partner.ai.use</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<label for="name"/>
|
||||||
|
<h1>
|
||||||
|
<field name="name"/>
|
||||||
|
</h1>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="ai_user_id"/>
|
||||||
|
<field name="first_ask_time"/>
|
||||||
|
<field name="latest_ask_time"/>
|
||||||
|
<field name="service_start_date"/>
|
||||||
|
<field name="service_end_date"/>
|
||||||
|
<field name="used_number" readonly="True"/>
|
||||||
|
<field name="max_number" readonly="True"/>
|
||||||
|
<field name="token_balance" readonly="True"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="human_prompt_tokens" readonly="True"/>
|
||||||
|
<field name="ai_completion_tokens" readonly="True"/>
|
||||||
|
<field name="tokens_total" readonly="True"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="res_partner_ai_use_search_view" model="ir.ui.view">
|
||||||
|
<field name="name">res.partner.ai.use.search</field>
|
||||||
|
<field name="model">res.partner.ai.use</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="ai_user_id"/>
|
||||||
|
<searchpanel>
|
||||||
|
<field name="ai_user_id"/>
|
||||||
|
</searchpanel>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_res_partner_ai_use" model="ir.actions.act_window">
|
||||||
|
<field name="name">Partner Ai Use</field>
|
||||||
|
<field name="res_model">res.partner.ai.use</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="context">{'create': 0, 'delete': 0}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
<record id="action_res_users_2_partner_ai_use" model="ir.actions.act_window">
|
||||||
|
<field name="name">Partner Ai Use</field>
|
||||||
|
<field name="res_model">res.partner.ai.use</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="domain">[('ai_user_id', 'in', active_ids)]</field>
|
||||||
|
<field name="context">{'default_ai_user_id':active_id,}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem
|
||||||
|
id="menu_res_partner_ai_use"
|
||||||
|
name="Partner Ai Use"
|
||||||
|
parent="base.menu_users"
|
||||||
|
sequence="3"
|
||||||
|
action="action_res_partner_ai_use"
|
||||||
|
groups="base.group_system"/>
|
||||||
|
</odoo>
|
||||||
39
app_ai_bard/views/res_users_views.xml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="app_chatgpt_res_users_form" model="ir.ui.view">
|
||||||
|
<field name="name">app.chatgpt.res.users.form</field>
|
||||||
|
<field name="model">res.users</field>
|
||||||
|
<field name="inherit_id" ref="base.view_users_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//div[@name='button_box']" position="inside">
|
||||||
|
<button name="%(app_chatgpt.action_res_users_2_partner_ai_use)d" type="action" string="Partner Ai Use" icon="fa-comments">
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//page[@name='preferences']" position="after">
|
||||||
|
<page name="page_chatgpt" string="ChatGPT">
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="gpt_id"/>
|
||||||
|
<field name="gpt_policy"/>
|
||||||
|
<field name="gpt_wl_partners" widget="many2many_tags" attrs="{'invisible': [('gpt_policy', '=', 'all')]}"/>
|
||||||
|
<field name="gpt_demo_time"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<!-- search-->
|
||||||
|
<record id="app_view_users_search" model="ir.ui.view">
|
||||||
|
<field name="name">app.res.users.search</field>
|
||||||
|
<field name="model">res.users</field>
|
||||||
|
<field name="inherit_id" ref="base.view_users_search"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//filter[@name='filter_no_share']" position="before">
|
||||||
|
<filter name="is_robot" string="Ai User" domain="[('gpt_id','!=',False)]"/>
|
||||||
|
<filter name="not_robot" string="Not Ai" domain="[('gpt_id','=',False)]"/>
|
||||||
|
<separator/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
4
app_auto_backup/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import controllers
|
||||||
|
from . import models
|
||||||
60
app_auto_backup/__manifest__.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# 'author': "Yenthe Van Ginneken",
|
||||||
|
# 'website': "http://www.odoo.yenthevg.com",
|
||||||
|
# 'author': "guohuadeng@hotmail.com",
|
||||||
|
# 'website': "https://www.odooai.cn",
|
||||||
|
|
||||||
|
{
|
||||||
|
'name': "Database auto backup and Download,数据库自动备份",
|
||||||
|
'version': '24.11.06',
|
||||||
|
|
||||||
|
'summary': 'Automated and odoo database backups. easy download and manage database file. optimized from auto_backup of oca Yenthe Van Ginneken',
|
||||||
|
|
||||||
|
'description': """
|
||||||
|
1. Easy schedule database backup
|
||||||
|
2. Set remote backup and cron schedule
|
||||||
|
3. Manual backup database in one click
|
||||||
|
4. Easy download backup file or remove file for System user
|
||||||
|
11. Multi-language Support. Multi-Company Support.
|
||||||
|
12. Support Odoo 18,17,16,15,14,13,12, Enterprise and Community and odoo.sh Edition.
|
||||||
|
13. Full Open Source.
|
||||||
|
The Database Auto-Backup module enables the user to make configurations for the automatic backup of the database.
|
||||||
|
Backups can be taken on the local system or on a remote server, through SFTP.
|
||||||
|
You only have to specify the hostname, port, backup location and databasename (all will be pre-filled by default with correct data.
|
||||||
|
If you want to write to an external server with SFTP you will need to provide the IP, username and password for the remote backups.
|
||||||
|
The base of this module is taken from Odoo SA V6.1 (https://www.odoo.com/apps/modules/6.0/auto_backup/) and then upgraded and heavily expanded.
|
||||||
|
This module is made and provided by Yenthe Van Ginneken (Oocademy).
|
||||||
|
Automatic backup for all such configured databases can then be scheduled as follows:
|
||||||
|
|
||||||
|
1) Go to Settings / Technical / Automation / Scheduled actions.
|
||||||
|
2) Search the action 'Backup scheduler'.
|
||||||
|
3) Set it active and choose how often you wish to take backups.
|
||||||
|
4) If you want to write backups to a remote location you should fill in the SFTP details.
|
||||||
|
""",
|
||||||
|
|
||||||
|
'author': 'odooai.cn',
|
||||||
|
'website': "http://www.odooai.cn",
|
||||||
|
'category': 'Extra tools',
|
||||||
|
'installable': True,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
'price': 38.00,
|
||||||
|
'currency': 'EUR',
|
||||||
|
|
||||||
|
# any module necessary for this one to work correctly
|
||||||
|
'depends': [
|
||||||
|
'base',
|
||||||
|
'app_odoo_customize'
|
||||||
|
],
|
||||||
|
'external_dependencies': {
|
||||||
|
'python': ['paramiko'],
|
||||||
|
},
|
||||||
|
|
||||||
|
# always loaded
|
||||||
|
'data': [
|
||||||
|
'security/user_groups.xml',
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/backup_view.xml',
|
||||||
|
'data/backup_data.xml',
|
||||||
|
'views/db_backup_details.xml',
|
||||||
|
],
|
||||||
|
}
|
||||||
3
app_auto_backup/controllers/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import main
|
||||||
33
app_auto_backup/controllers/main.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from odoo import http, _
|
||||||
|
from odoo.http import request, content_disposition
|
||||||
|
from odoo.exceptions import AccessError, UserError
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AppAutoBackup(http.Controller):
|
||||||
|
|
||||||
|
@http.route("/dbbackup/download/<path:file_path>", type="http", auth="user")
|
||||||
|
def download_backupfile(self, file_path, **kw):
|
||||||
|
_logger.warning('download_backupfile: %s', file_path)
|
||||||
|
if not self.env.user.has_group('base.group_system'):
|
||||||
|
raise UserError(_('File not found for user.'))
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
try:
|
||||||
|
with open(file_path, 'rb') as file:
|
||||||
|
file_content = file.read()
|
||||||
|
file_name = file_path.split("/")[-1]
|
||||||
|
headers = [
|
||||||
|
('Content-Type', 'application/octet-stream'),
|
||||||
|
('Content-Disposition', content_disposition(file_name)),
|
||||||
|
]
|
||||||
|
return request.make_response(file_content, headers)
|
||||||
|
except Exception as e:
|
||||||
|
raise UserError(e)
|
||||||
|
else:
|
||||||
|
return 'File not found'
|
||||||
15
app_auto_backup/data/backup_data.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" ?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="backup_scheduler" model="ir.cron">
|
||||||
|
<field name="interval_type">hours</field>
|
||||||
|
<field name="name">Backup scheduler</field>
|
||||||
|
<field name="priority">5</field>
|
||||||
|
<field name="active">True</field>
|
||||||
|
<field name="interval_number">12</field>
|
||||||
|
<field name="model_id" ref="model_db_backup"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model.schedule_backup()</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
449
app_auto_backup/i18n/zh_CN.po
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
# Translation of Odoo Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * app_auto_backup
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Odoo Server 16.0+e-20231112\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2024-07-23 08:20+0000\n"
|
||||||
|
"PO-Revision-Date: 2024-07-23 08:20+0000\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Plural-Forms: \n"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid ""
|
||||||
|
"<b>Warning:</b>\n"
|
||||||
|
" Use SFTP with caution! This writes files to external servers under the path you specify."
|
||||||
|
msgstr ""
|
||||||
|
"<b>警告:</b>\n"
|
||||||
|
" 使用 SFTP 要注意! 写入至外部Server的文件要在你指定的路径下。"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,help:app_auto_backup.field_db_backup__folder
|
||||||
|
msgid "Absolute path for storing the backups"
|
||||||
|
msgstr "备份绝对路径"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.module.category,name:app_auto_backup.module_management
|
||||||
|
msgid "Auto backup access"
|
||||||
|
msgstr "备份的权限"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__send_mail_sftp_fail
|
||||||
|
msgid "Auto. E-mail on backup fail"
|
||||||
|
msgstr "FTP备份失败自动邮件通知你"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__autoremove
|
||||||
|
msgid "Auto. Remove Backups"
|
||||||
|
msgstr "自动删除备份"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid "Back-up view"
|
||||||
|
msgstr "备份视图"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.ui.menu,name:app_auto_backup.auto_backup_menu
|
||||||
|
msgid "Back-ups"
|
||||||
|
msgstr "备份"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__backup_details_ids
|
||||||
|
msgid "Backup Details"
|
||||||
|
msgstr "备份明细"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__folder
|
||||||
|
msgid "Backup Directory"
|
||||||
|
msgstr "备份目录"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__backup_type
|
||||||
|
msgid "Backup Type"
|
||||||
|
msgstr "备份类型"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model,name:app_auto_backup.model_db_backup
|
||||||
|
msgid "Backup configuration record"
|
||||||
|
msgstr "备份配置记录"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid "Backup records"
|
||||||
|
msgstr "备份记录"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.actions.server,name:app_auto_backup.backup_scheduler_ir_actions_server
|
||||||
|
#: model:ir.cron,cron_name:app_auto_backup.backup_scheduler
|
||||||
|
msgid "Backup scheduler"
|
||||||
|
msgstr "数据库备份计划"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_tree
|
||||||
|
msgid "Backups"
|
||||||
|
msgstr "备份"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,help:app_auto_backup.field_db_backup__days_to_keep_sftp
|
||||||
|
msgid ""
|
||||||
|
"Choose after how many days the backup should be deleted from the FTP server. For example:\n"
|
||||||
|
"If you fill in 5 the backups will be removed after 5 days from the FTP server."
|
||||||
|
msgstr ""
|
||||||
|
"选择后多少天备份应被删除从 FTP 服务器。例如: \n"
|
||||||
|
"如果你填写 5, 将5 天后 从FTP 服务器 删除备份文件。"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,help:app_auto_backup.field_db_backup__days_to_keep
|
||||||
|
msgid ""
|
||||||
|
"Choose after how many days the backup should be deleted. For example:\n"
|
||||||
|
"If you fill in 5 the backups will be removed after 5 days."
|
||||||
|
msgstr ""
|
||||||
|
"选择多少天后将会删除历史备份文件。如:\n"
|
||||||
|
"填入 5 ,则5天前的备份文件将自动删除。"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.actions.act_window,name:app_auto_backup.action_backup
|
||||||
|
#: model:ir.ui.menu,name:app_auto_backup.backup_conf_menu
|
||||||
|
msgid "Configure backups"
|
||||||
|
msgstr "配置备份"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/app_auto_backup/models/db_backup.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid "Connection Test Failed!"
|
||||||
|
msgstr "连接测试失败!"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/app_auto_backup/models/db_backup.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"Connection Test Succeeded!\n"
|
||||||
|
"Everything seems properly set up for FTP back-ups!"
|
||||||
|
msgstr ""
|
||||||
|
"连接测试成功!\n"
|
||||||
|
"所有的FTP备份设置正常"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid "Contact odooai.cn!"
|
||||||
|
msgstr "联系 odooai.cn"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__create_uid
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup_details__create_uid
|
||||||
|
msgid "Created by"
|
||||||
|
msgstr "创建者"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__create_date
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup_details__create_date
|
||||||
|
msgid "Created on"
|
||||||
|
msgstr "创建时间"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__name
|
||||||
|
msgid "Database"
|
||||||
|
msgstr "数据库"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup_details__db_backup_id
|
||||||
|
msgid "Database Backup"
|
||||||
|
msgstr "数据库备份"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model,name:app_auto_backup.model_db_backup_details
|
||||||
|
msgid "Database Backup Details"
|
||||||
|
msgstr "数据库备份明细"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.actions.act_window,name:app_auto_backup.action_db_backup_details
|
||||||
|
#: model:ir.ui.menu,name:app_auto_backup.menu_action_db_backup_details
|
||||||
|
msgid "Database backups"
|
||||||
|
msgstr "数据库备份管理"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,help:app_auto_backup.field_db_backup__name
|
||||||
|
msgid "Database you want to schedule backups for"
|
||||||
|
msgstr "计划备份的数据库"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__display_name
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup_details__display_name
|
||||||
|
msgid "Display Name"
|
||||||
|
msgstr "显示名称"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.db_backup_details_tree_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid "Download File"
|
||||||
|
msgstr "下载文件"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields.selection,name:app_auto_backup.selection__db_backup__backup_type__dump
|
||||||
|
msgid "Dump"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__email_to_notify
|
||||||
|
msgid "E-mail to notify"
|
||||||
|
msgstr "提醒E-mail地址"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup_details__file_path
|
||||||
|
msgid "File Path"
|
||||||
|
msgstr "文件路径"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/app_auto_backup/models/db_backup_details.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid "File Path or URL not found."
|
||||||
|
msgstr "无法找到该文件路径或URL"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,help:app_auto_backup.field_db_backup__email_to_notify
|
||||||
|
msgid ""
|
||||||
|
"Fill in the e-mail where you want to be notified that the backup failed on "
|
||||||
|
"the FTP."
|
||||||
|
msgstr "FTP备份失败时,邮件通知你详细信息"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid "For example: /odoo/backups/"
|
||||||
|
msgstr "如:/odoo/backups/"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid "Go to Settings / Technical / Automation / Scheduled Actions."
|
||||||
|
msgstr "点击 设置 / 技术 / 自动化 / 计划的动作"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid "Help"
|
||||||
|
msgstr "帮助"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#. odoo-python
|
||||||
|
#: code:addons/app_auto_backup/models/db_backup.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid "Here is what we got instead:\n"
|
||||||
|
msgstr "这里是我们 is what we got instead:\n"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__host
|
||||||
|
msgid "Host"
|
||||||
|
msgstr "服务器"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__id
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup_details__id
|
||||||
|
msgid "ID"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__sftp_host
|
||||||
|
msgid "IP Address SFTP Server"
|
||||||
|
msgstr " SFTP 服务器 IP 地址"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,help:app_auto_backup.field_db_backup__send_mail_sftp_fail
|
||||||
|
msgid ""
|
||||||
|
"If you check this option you can choose to automaticly get e-mailed when the"
|
||||||
|
" backup to the external server failed."
|
||||||
|
msgstr "如果您选中此选项,您可以选择自动收到通过邮件发送到外部服务器备份失败的信息。"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,help:app_auto_backup.field_db_backup__autoremove
|
||||||
|
msgid ""
|
||||||
|
"If you check this option you can choose to automaticly remove the backup "
|
||||||
|
"after xx days"
|
||||||
|
msgstr "如果您选中此选项,您可以指定需要写入 sftp 的远程服务器的详细信息。"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,help:app_auto_backup.field_db_backup__sftp_write
|
||||||
|
msgid ""
|
||||||
|
"If you check this option you can specify the details needed to write to a "
|
||||||
|
"remote server with SFTP."
|
||||||
|
msgstr "如果勾选此项,您可以进行远程 SFTP 备份。"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup____last_update
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup_details____last_update
|
||||||
|
msgid "Last Modified on"
|
||||||
|
msgstr "最后更新时间"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__write_uid
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup_details__write_uid
|
||||||
|
msgid "Last Updated by"
|
||||||
|
msgstr "最后更新者"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__write_date
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup_details__write_date
|
||||||
|
msgid "Last Updated on"
|
||||||
|
msgstr "最后更新日期"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid "Local backup configuration"
|
||||||
|
msgstr "本地备份配置"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:res.groups,name:app_auto_backup.group_manager
|
||||||
|
msgid "Manager"
|
||||||
|
msgstr "备份管理员"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup_details__name
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "文件名"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid "Need more help?"
|
||||||
|
msgstr "需要更多帮助吗?"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__sftp_password
|
||||||
|
msgid "Password User SFTP Server"
|
||||||
|
msgstr " SFTP服务器密码"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__sftp_path
|
||||||
|
msgid "Path external server"
|
||||||
|
msgstr "服务器目录"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__port
|
||||||
|
msgid "Port"
|
||||||
|
msgstr "端口"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.db_backup_details_tree_view
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid "Remove File"
|
||||||
|
msgstr "删除文件"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__days_to_keep_sftp
|
||||||
|
msgid "Remove SFTP after x days"
|
||||||
|
msgstr "多少天后从服务器删除"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__days_to_keep
|
||||||
|
msgid "Remove after x days"
|
||||||
|
msgstr "多少天后删除"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid "Run Backup"
|
||||||
|
msgstr "执行备份"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid "SFTP"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__sftp_port
|
||||||
|
msgid "SFTP Port"
|
||||||
|
msgstr "SFTP 端口"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid "Search the action named 'Backup scheduler'."
|
||||||
|
msgstr "搜索计划备份调度程序“备份计划”。"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid ""
|
||||||
|
"Set the scheduler to active and fill in how often you want backups "
|
||||||
|
"generated."
|
||||||
|
msgstr "设置计划动作为有效,并填写备份间隔时间,间隔时间单位,间隔次数,执行时间等数据库具体备份方案。"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid "Test SFTP Connection"
|
||||||
|
msgstr "测试 SFTP 连接"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,help:app_auto_backup.field_db_backup__sftp_host
|
||||||
|
msgid "The IP address from your remote server. For example 192.168.0.1"
|
||||||
|
msgstr "SFTP服务器的 IP 地址。例如: 192.168.0.1"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,help:app_auto_backup.field_db_backup__sftp_path
|
||||||
|
msgid ""
|
||||||
|
"The location to the folder where the dumps should be written to. For example /odoo/backups/.\n"
|
||||||
|
"Files will then be written to /odoo/backups/ on your remote server."
|
||||||
|
msgstr ""
|
||||||
|
"转储应将写入的文件夹位置。例如 /odoo/backups/远程服务器上,然后将写入 /odoo/backups/.\n"
|
||||||
|
"Files。"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,help:app_auto_backup.field_db_backup__sftp_password
|
||||||
|
msgid ""
|
||||||
|
"The password from the user where the SFTP connection should be made with. "
|
||||||
|
"This is the password from the user on the external server."
|
||||||
|
msgstr "从 SFTP 服务器连接该用户的密码。这是SFTP服务器上的用户密码。"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,help:app_auto_backup.field_db_backup__sftp_port
|
||||||
|
msgid "The port on the FTP server that accepts SSH/SFTP calls."
|
||||||
|
msgstr "接受 SSH/SFTP 使用的FTP 服务器上的端口。"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,help:app_auto_backup.field_db_backup__sftp_user
|
||||||
|
msgid ""
|
||||||
|
"The username where the SFTP connection should be made with. This is the user"
|
||||||
|
" on the external server."
|
||||||
|
msgstr "SFTP 连接使用该用户名。这是在SFTP服务器上的用户。"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid ""
|
||||||
|
"This configures the scheduler for automatic backup of the given database running on given host\n"
|
||||||
|
" at given port on regular intervals.\n"
|
||||||
|
" <br/>\n"
|
||||||
|
" Automatic backups of the database can be scheduled as follows:"
|
||||||
|
msgstr "配置适用指定数据库备份 在设置服务器端口定期运行"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup_details__url
|
||||||
|
msgid "URL"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.module.category,description:app_auto_backup.module_management
|
||||||
|
msgid "User access level for this module"
|
||||||
|
msgstr "本模块用户权限"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__sftp_user
|
||||||
|
msgid "Username SFTP Server"
|
||||||
|
msgstr "SFTP服务器用户名"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model_terms:ir.ui.view,arch_db:app_auto_backup.view_backup_config_form
|
||||||
|
msgid "View Cron"
|
||||||
|
msgstr "查看定时任务"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields,field_description:app_auto_backup.field_db_backup__sftp_write
|
||||||
|
msgid "Write to external server with sftp"
|
||||||
|
msgstr "SFtp备份至远程服务器:"
|
||||||
|
|
||||||
|
#. module: app_auto_backup
|
||||||
|
#: model:ir.model.fields.selection,name:app_auto_backup.selection__db_backup__backup_type__zip
|
||||||
|
msgid "Zip"
|
||||||
|
msgstr "Zip压缩包"
|
||||||
4
app_auto_backup/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import db_backup
|
||||||
|
from . import db_backup_details
|
||||||
360
app_auto_backup/models/db_backup.py
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import datetime
|
||||||
|
import time
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import odoo
|
||||||
|
from odoo import models, fields, api, tools, _
|
||||||
|
from odoo.exceptions import ValidationError, AccessDenied
|
||||||
|
from odoo.tools.misc import exec_pg_environ, find_pg_tool
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import paramiko
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError(
|
||||||
|
'This module needs paramiko to automatically write backups to the FTP through SFTP. '
|
||||||
|
'Please install paramiko on your system. (sudo pip3 install paramiko)')
|
||||||
|
|
||||||
|
|
||||||
|
class DbBackup(models.Model):
|
||||||
|
_name = 'db.backup'
|
||||||
|
_description = 'Backup configuration record'
|
||||||
|
|
||||||
|
def _get_db_name(self):
|
||||||
|
dbName = self._cr.dbname
|
||||||
|
return dbName
|
||||||
|
|
||||||
|
# Columns for local server configuration
|
||||||
|
host = fields.Char('Host', required=True, default='localhost')
|
||||||
|
port = fields.Char('Port', required=True, default=8069)
|
||||||
|
name = fields.Char('Database', required=True, help='Database you want to schedule backups for',
|
||||||
|
default=_get_db_name)
|
||||||
|
folder = fields.Char('Backup Directory', help='Absolute path for storing the backups', required=True,
|
||||||
|
default='/usr/lib/python3/dist-packages/odoo/backups')
|
||||||
|
backup_type = fields.Selection([('zip', 'Zip'), ('dump', 'Dump')], 'Backup Type', required=True, default='zip')
|
||||||
|
autoremove = fields.Boolean('Auto. Remove Backups',
|
||||||
|
help='If you check this option you can choose to automaticly remove the backup '
|
||||||
|
'after xx days')
|
||||||
|
days_to_keep = fields.Integer('Remove after x days',
|
||||||
|
help="Choose after how many days the backup should be deleted. For example:\n"
|
||||||
|
"If you fill in 5 the backups will be removed after 5 days.",
|
||||||
|
required=True)
|
||||||
|
|
||||||
|
# Columns for external server (SFTP)
|
||||||
|
sftp_write = fields.Boolean('Write to external server with sftp',
|
||||||
|
help="If you check this option you can specify the details needed to write to a remote "
|
||||||
|
"server with SFTP.")
|
||||||
|
sftp_path = fields.Char('Path external server',
|
||||||
|
help='The location to the folder where the dumps should be written to. For example '
|
||||||
|
'/odoo/backups/.\nFiles will then be written to /odoo/backups/ on your remote server.')
|
||||||
|
sftp_host = fields.Char('IP Address SFTP Server',
|
||||||
|
help='The IP address from your remote server. For example 192.168.0.1')
|
||||||
|
sftp_port = fields.Integer('SFTP Port', help='The port on the FTP server that accepts SSH/SFTP calls.', default=22)
|
||||||
|
sftp_user = fields.Char('Username SFTP Server',
|
||||||
|
help='The username where the SFTP connection should be made with. This is the user on the '
|
||||||
|
'external server.')
|
||||||
|
sftp_password = fields.Char('Password User SFTP Server',
|
||||||
|
help='The password from the user where the SFTP connection should be made with. This '
|
||||||
|
'is the password from the user on the external server.')
|
||||||
|
days_to_keep_sftp = fields.Integer('Remove SFTP after x days',
|
||||||
|
help='Choose after how many days the backup should be deleted from the FTP '
|
||||||
|
'server. For example:\nIf you fill in 5 the backups will be removed after '
|
||||||
|
'5 days from the FTP server.',
|
||||||
|
default=30)
|
||||||
|
send_mail_sftp_fail = fields.Boolean('Auto. E-mail on backup fail',
|
||||||
|
help='If you check this option you can choose to automaticly get e-mailed '
|
||||||
|
'when the backup to the external server failed.')
|
||||||
|
email_to_notify = fields.Char('E-mail to notify',
|
||||||
|
help='Fill in the e-mail where you want to be notified that the backup failed on '
|
||||||
|
'the FTP.')
|
||||||
|
backup_details_ids = fields.One2many('db.backup.details', 'db_backup_id', 'Backup Details')
|
||||||
|
|
||||||
|
def test_sftp_connection(self, context=None):
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
# Check if there is a success or fail and write messages
|
||||||
|
message_title = ""
|
||||||
|
message_content = ""
|
||||||
|
error = ""
|
||||||
|
has_failed = False
|
||||||
|
|
||||||
|
for rec in self:
|
||||||
|
path_to_write_to = rec.sftp_path
|
||||||
|
ip_host = rec.sftp_host
|
||||||
|
port_host = rec.sftp_port
|
||||||
|
username_login = rec.sftp_user
|
||||||
|
password_login = rec.sftp_password
|
||||||
|
|
||||||
|
# Connect with external server over SFTP, so we know sure that everything works.
|
||||||
|
try:
|
||||||
|
s = paramiko.SSHClient()
|
||||||
|
s.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
s.connect(ip_host, port_host, username_login, password_login, timeout=10)
|
||||||
|
sftp = s.open_sftp()
|
||||||
|
sftp.close()
|
||||||
|
message_title = _("Connection Test Succeeded!\nEverything seems properly set up for FTP back-ups!")
|
||||||
|
except Exception as e:
|
||||||
|
_logger.critical('There was a problem connecting to the remote ftp: %s', str(e))
|
||||||
|
error += str(e)
|
||||||
|
has_failed = True
|
||||||
|
message_title = _("Connection Test Failed!")
|
||||||
|
if len(rec.sftp_host) < 8:
|
||||||
|
message_content += "\nYour IP address seems to be too short.\n"
|
||||||
|
message_content += _("Here is what we got instead:\n")
|
||||||
|
finally:
|
||||||
|
if s:
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
if has_failed:
|
||||||
|
raise ValidationError(message_title + '\n\n' + message_content + "%s" % str(error))
|
||||||
|
else:
|
||||||
|
raise ValidationError(message_title + '\n\n' + message_content)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def schedule_backup(self):
|
||||||
|
conf_ids = self.search([])
|
||||||
|
for rec in conf_ids:
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not os.path.isdir(rec.folder):
|
||||||
|
os.makedirs(rec.folder)
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
# Create name for dumpfile.
|
||||||
|
bkp_file = '%s_%s.%s' % (time.strftime('%Y_%m_%d_%H_%M_%S'), rec.name, rec.backup_type)
|
||||||
|
file_path = os.path.join(rec.folder, bkp_file)
|
||||||
|
uri = 'http://' + rec.host + ':' + rec.port
|
||||||
|
bkp = ''
|
||||||
|
fp = open(file_path, 'wb')
|
||||||
|
try:
|
||||||
|
# try to backup database and write it away
|
||||||
|
fp = open(file_path, 'wb')
|
||||||
|
self._take_dump(rec.name, fp, 'db.backup', rec.backup_type)
|
||||||
|
fp.close()
|
||||||
|
rec.backup_details_ids.create({
|
||||||
|
'name': bkp_file,
|
||||||
|
'file_path': file_path,
|
||||||
|
'url': '/dbbackup/download%s' % file_path,
|
||||||
|
'db_backup_id': rec.id,
|
||||||
|
})
|
||||||
|
except Exception as error:
|
||||||
|
_logger.warning(
|
||||||
|
"Couldn't backup database %s. Bad database administrator password for server running at "
|
||||||
|
"http://%s:%s" % (rec.name, rec.host, rec.port))
|
||||||
|
_logger.warning("Exact error from the exception: %s", str(error))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if user wants to write to SFTP or not.
|
||||||
|
if rec.sftp_write is True:
|
||||||
|
try:
|
||||||
|
# Store all values in variables
|
||||||
|
dir = rec.folder
|
||||||
|
path_to_write_to = rec.sftp_path
|
||||||
|
ip_host = rec.sftp_host
|
||||||
|
port_host = rec.sftp_port
|
||||||
|
username_login = rec.sftp_user
|
||||||
|
password_login = rec.sftp_password
|
||||||
|
_logger.debug('sftp remote path: %s' % path_to_write_to)
|
||||||
|
|
||||||
|
try:
|
||||||
|
s = paramiko.SSHClient()
|
||||||
|
s.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
s.connect(ip_host, port_host, username_login, password_login, timeout=20)
|
||||||
|
sftp = s.open_sftp()
|
||||||
|
except Exception as error:
|
||||||
|
_logger.critical('Error connecting to remote server! Error: %s', str(error))
|
||||||
|
|
||||||
|
try:
|
||||||
|
sftp.chdir(path_to_write_to)
|
||||||
|
except IOError:
|
||||||
|
# Create directory and subdirs if they do not exist.
|
||||||
|
current_directory = ''
|
||||||
|
for dirElement in path_to_write_to.split('/'):
|
||||||
|
current_directory += dirElement + '/'
|
||||||
|
try:
|
||||||
|
sftp.chdir(current_directory)
|
||||||
|
except:
|
||||||
|
_logger.info('(Part of the) path didn\'t exist. Creating it now at %s', current_directory)
|
||||||
|
# Make directory and then navigate into it
|
||||||
|
sftp.mkdir(current_directory, 777)
|
||||||
|
sftp.chdir(current_directory)
|
||||||
|
pass
|
||||||
|
sftp.chdir(path_to_write_to)
|
||||||
|
# Loop over all files in the directory.
|
||||||
|
for f in os.listdir(dir):
|
||||||
|
if rec.name in f:
|
||||||
|
fullpath = os.path.join(dir, f)
|
||||||
|
if os.path.isfile(fullpath):
|
||||||
|
try:
|
||||||
|
sftp.stat(os.path.join(path_to_write_to, f))
|
||||||
|
_logger.debug(
|
||||||
|
'File %s already exists on the remote FTP Server ------ skipped' % fullpath)
|
||||||
|
# This means the file does not exist (remote) yet!
|
||||||
|
except IOError:
|
||||||
|
try:
|
||||||
|
# sftp.put(fullpath, path_to_write_to)
|
||||||
|
sftp.put(fullpath, os.path.join(path_to_write_to, f))
|
||||||
|
_logger.info('Copying File % s------ success' % fullpath)
|
||||||
|
except Exception as err:
|
||||||
|
_logger.critical(
|
||||||
|
'We couldn\'t write the file to the remote server. Error: ' + str(err))
|
||||||
|
|
||||||
|
# Navigate in to the correct folder.
|
||||||
|
sftp.chdir(path_to_write_to)
|
||||||
|
|
||||||
|
# Loop over all files in the directory from the back-ups.
|
||||||
|
# We will check the creation date of every back-up.
|
||||||
|
for file in sftp.listdir(path_to_write_to):
|
||||||
|
if rec.name in file:
|
||||||
|
# Get the full path
|
||||||
|
fullpath = os.path.join(path_to_write_to, file)
|
||||||
|
# Get the timestamp from the file on the external server
|
||||||
|
timestamp = sftp.stat(fullpath).st_mtime
|
||||||
|
createtime = datetime.datetime.fromtimestamp(timestamp)
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
delta = now - createtime
|
||||||
|
# If the file is older than the days_to_keep_sftp (the days to keep that the user filled in
|
||||||
|
# on the Odoo form it will be removed.
|
||||||
|
if delta.days >= rec.days_to_keep_sftp:
|
||||||
|
# Only delete files, no directories!
|
||||||
|
if ".dump" in file or '.zip' in file:
|
||||||
|
_logger.info("Delete too old file from SFTP servers: %s", file)
|
||||||
|
sftp.unlink(file)
|
||||||
|
# Close the SFTP session.
|
||||||
|
sftp.close()
|
||||||
|
except Exception as e:
|
||||||
|
_logger.debug('Exception! We couldn\'t back up to the FTP server..')
|
||||||
|
# At this point the SFTP backup failed. We will now check if the user wants
|
||||||
|
# an e-mail notification about this.
|
||||||
|
if rec.send_mail_sftp_fail:
|
||||||
|
try:
|
||||||
|
ir_mail_server = self.env['ir.mail_server'].search([], order='sequence asc', limit=1)
|
||||||
|
message = "Dear,\n\nThe backup for the server " + rec.host + " (IP: " + rec.sftp_host + \
|
||||||
|
") failed. Please check the following details:\n\nIP address SFTP server: " + \
|
||||||
|
rec.sftp_host + "\nUsername: " + rec.sftp_user + \
|
||||||
|
"\n\nError details: " + tools.ustr(e) + \
|
||||||
|
"\n\nWith kind regards"
|
||||||
|
catch_all_domain = self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain")
|
||||||
|
response_mail = "auto_backup@%s" % catch_all_domain if catch_all_domain else self.env.user.partner_id.email
|
||||||
|
msg = ir_mail_server.build_email(response_mail, [rec.email_to_notify],
|
||||||
|
"Backup from " + rec.host + "(" + rec.sftp_host +
|
||||||
|
") failed",
|
||||||
|
message)
|
||||||
|
ir_mail_server.send_email(msg)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
"""
|
||||||
|
Remove all old files (on local server) in case this is configured..
|
||||||
|
"""
|
||||||
|
if rec.autoremove:
|
||||||
|
directory = rec.folder
|
||||||
|
# Loop over all files in the directory.
|
||||||
|
for f in os.listdir(directory):
|
||||||
|
fullpath = os.path.join(directory, f)
|
||||||
|
# Only delete the ones wich are from the current database
|
||||||
|
# (Makes it possible to save different databases in the same folder)
|
||||||
|
if rec.name in fullpath:
|
||||||
|
timestamp = os.stat(fullpath).st_ctime
|
||||||
|
createtime = datetime.datetime.fromtimestamp(timestamp)
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
delta = now - createtime
|
||||||
|
if delta.days >= rec.days_to_keep:
|
||||||
|
# Only delete files (which are .dump and .zip), no directories.
|
||||||
|
if os.path.isfile(fullpath) and (".dump" in f or '.zip' in f):
|
||||||
|
_logger.info("Delete local out-of-date file: %s", fullpath)
|
||||||
|
backup_details_id = self.env['db.backup.details'].search([('file_path', '=', fullpath)])
|
||||||
|
if backup_details_id:
|
||||||
|
backup_details_id.unlink()
|
||||||
|
else:
|
||||||
|
os.remove(fullpath)
|
||||||
|
|
||||||
|
# This is more or less the same as the default Odoo function at
|
||||||
|
# https://github.com/odoo/odoo/blob/e649200ab44718b8faefc11c2f8a9d11f2db7753/odoo/service/db.py#L209
|
||||||
|
# The main difference is that we do not do have a wrapper for the function check_db_management_enabled here and
|
||||||
|
# that we authenticate based on the cron its user id and by checking if we have 'db.backup' defined in the function
|
||||||
|
# call. Since this function is called from the cron and since we have these security checks on model and on user_id
|
||||||
|
# its pretty impossible to hack any way to take a backup. This allows us to disable the Odoo database manager
|
||||||
|
# which is a MUCH safer way
|
||||||
|
def _take_dump(self, db_name, stream, model, backup_format='zip'):
|
||||||
|
"""Dump database `db` into file-like object `stream` if stream is None
|
||||||
|
return a file object with the dump """
|
||||||
|
|
||||||
|
cron_user_id = self.env.ref('app_auto_backup.backup_scheduler').user_id.id
|
||||||
|
if self._name != 'db.backup' or cron_user_id != self.env.user.id:
|
||||||
|
_logger.error('Unauthorized database operation. Backups should only be available from the cron job.')
|
||||||
|
raise AccessDenied()
|
||||||
|
|
||||||
|
_logger.info('DUMP DB: %s format %s', db_name, backup_format)
|
||||||
|
|
||||||
|
cmd = [find_pg_tool('pg_dump'), '--no-owner', db_name]
|
||||||
|
env = exec_pg_environ()
|
||||||
|
if backup_format == 'zip':
|
||||||
|
with tempfile.TemporaryDirectory() as dump_dir:
|
||||||
|
filestore = odoo.tools.config.filestore(db_name)
|
||||||
|
if os.path.exists(filestore):
|
||||||
|
shutil.copytree(filestore, os.path.join(dump_dir, 'filestore'))
|
||||||
|
with open(os.path.join(dump_dir, 'manifest.json'), 'w') as fh:
|
||||||
|
db = odoo.sql_db.db_connect(db_name)
|
||||||
|
with db.cursor() as cr:
|
||||||
|
json.dump(self._dump_db_manifest(cr), fh, indent=4)
|
||||||
|
cmd.insert(-1, '--file=' + os.path.join(dump_dir, 'dump.sql'))
|
||||||
|
subprocess.run(cmd, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, check=True)
|
||||||
|
if stream:
|
||||||
|
odoo.tools.osutil.zip_dir(dump_dir, stream, include_dir=False, fnct_sort=lambda file_name: file_name != 'dump.sql')
|
||||||
|
else:
|
||||||
|
t=tempfile.TemporaryFile()
|
||||||
|
odoo.tools.osutil.zip_dir(dump_dir, t, include_dir=False, fnct_sort=lambda file_name: file_name != 'dump.sql')
|
||||||
|
t.seek(0)
|
||||||
|
return t
|
||||||
|
else:
|
||||||
|
cmd.insert(-1, '--format=c')
|
||||||
|
process = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
stdout, _ = process.communicate()
|
||||||
|
if stream:
|
||||||
|
# shutil.copyfileobj(stdout, stream)
|
||||||
|
stream.write(stdout)
|
||||||
|
else:
|
||||||
|
return stdout
|
||||||
|
|
||||||
|
def _dump_db_manifest(self, cr):
|
||||||
|
pg_version = "%d.%d" % divmod(cr._obj.connection.server_version / 100, 100)
|
||||||
|
cr.execute("SELECT name, latest_version FROM ir_module_module WHERE state = 'installed'")
|
||||||
|
modules = dict(cr.fetchall())
|
||||||
|
manifest = {
|
||||||
|
'odoo_dump': '1',
|
||||||
|
'db_name': cr.dbname,
|
||||||
|
'version': odoo.release.version,
|
||||||
|
'version_info': odoo.release.version_info,
|
||||||
|
'major_version': odoo.release.major_version,
|
||||||
|
'pg_version': pg_version,
|
||||||
|
'modules': modules,
|
||||||
|
}
|
||||||
|
return manifest
|
||||||
|
|
||||||
|
def action_view_cron(self):
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
action = self.env.ref('base.ir_cron_act', False).sudo().read()[0]
|
||||||
|
cron = self.env.ref('app_auto_backup.backup_scheduler', False)
|
||||||
|
if action and cron:
|
||||||
|
action['views'] = [(self.env.ref('base.ir_cron_view_form').id, 'form')]
|
||||||
|
action['res_id'] = cron.id
|
||||||
|
return action
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def action_run_cron(self):
|
||||||
|
self.ensure_one()
|
||||||
|
cron = self.env.ref('app_auto_backup.backup_scheduler', False)
|
||||||
|
if cron:
|
||||||
|
cron.method_direct_trigger()
|
||||||
|
return True
|
||||||
41
app_auto_backup/models/db_backup_details.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from odoo import api, fields, models, _
|
||||||
|
from odoo.exceptions import AccessError, UserError
|
||||||
|
|
||||||
|
|
||||||
|
class DbBackupDetails(models.Model):
|
||||||
|
_name = 'db.backup.details'
|
||||||
|
_description = 'Database Backup Details'
|
||||||
|
|
||||||
|
name = fields.Char(string='Name')
|
||||||
|
file_path = fields.Char(string="File Path")
|
||||||
|
url = fields.Char(string='URL')
|
||||||
|
db_backup_id = fields.Many2one('db.backup', 'Database Backup')
|
||||||
|
|
||||||
|
def action_download_file(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.file_path or not self.url:
|
||||||
|
raise UserError(_("File Path or URL not found."))
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_url',
|
||||||
|
'url': self.url,
|
||||||
|
'target': 'new',
|
||||||
|
}
|
||||||
|
|
||||||
|
def unlink(self):
|
||||||
|
for rec in self:
|
||||||
|
try:
|
||||||
|
if rec.file_path:
|
||||||
|
if os.path.exists(rec.file_path):
|
||||||
|
os.remove(rec.file_path)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
return super(DbBackupDetails, self).unlink()
|
||||||
|
|
||||||
|
def action_remove_file(self):
|
||||||
|
self.ensure_one()
|
||||||
|
self.unlink()
|
||||||
4
app_auto_backup/security/ir.model.access.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
admin_access, db_backup admin access,model_db_backup,base.group_no_one,1,1,1,1
|
||||||
|
admin_security_rule, Model db_backup admin access,model_db_backup,app_auto_backup.group_manager,1,1,1,1
|
||||||
|
admin_db_backup_details, Model db_backup_details admin access,model_db_backup_details,app_auto_backup.group_manager,1,1,1,1
|
||||||
|
19
app_auto_backup/security/user_groups.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<record model="ir.module.category" id="module_management">
|
||||||
|
<field name="name">Auto backup access</field>
|
||||||
|
<field name="description">User access level for this module</field>
|
||||||
|
<field name="sequence">3</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="group_manager" model="res.groups">
|
||||||
|
<field name="name">Manager</field>
|
||||||
|
<field name="category_id" ref="app_auto_backup.module_management"/>
|
||||||
|
</record>
|
||||||
|
<!-- group_system 系统用户自动拥有备份管理员 -->
|
||||||
|
<record id="base.group_system" model="res.groups">
|
||||||
|
<field name="implied_ids" eval="[(4, ref('app_auto_backup.group_manager'))]"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
BIN
app_auto_backup/static/description/banner.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
app_auto_backup/static/description/banner1.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
app_auto_backup/static/description/demo1.jpg
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
app_auto_backup/static/description/demo2.jpg
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
app_auto_backup/static/description/demo3.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
app_auto_backup/static/description/demo4.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
app_auto_backup/static/description/emailnotification.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
app_auto_backup/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
267
app_auto_backup/static/description/index.html
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
##############################################################################
|
||||||
|
# Copyright (C) 2009~2024 odooAi.cn
|
||||||
|
##############################################################################
|
||||||
|
-->
|
||||||
|
<html>
|
||||||
|
<!-- begin title-->
|
||||||
|
<section class="oe_container container o_cc o_cc2">
|
||||||
|
<h2 class="text-center bg-warning text-white pt24 pb24">Database auto backup. Easy schedule backup and Download </h2>
|
||||||
|
<h3 class="text-center">数据库自动备份及快速下载</h3>
|
||||||
|
</section>
|
||||||
|
<!-- end title-->
|
||||||
|
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced" >
|
||||||
|
<div class="row">
|
||||||
|
<div class="oe_row">
|
||||||
|
<h3>Latest update: v18.24.11.06</h3>
|
||||||
|
<div class="row">
|
||||||
|
<img class="oe_demo oe_screenshot img img-fluid" style="max-height: 100%;" src="banner.png">
|
||||||
|
</div>
|
||||||
|
<div class="oe_span12 oe_spaced">
|
||||||
|
<div class="alert alert-info" style="padding:8px;font-weight: 300; font-size: 20px;">
|
||||||
|
<i class="fa fa-hand-o-right"></i><b> Key features: </b>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
1. Easy schedule database backup
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
2. Set remote backup and cron schedule
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
3. Manual backup database in one click
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
4. Easy download backup file or remove file for System user
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
11. Multi-language Support. Multi-Company Support.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
12. Support Odoo 18,17,16,15,14,13,12, Enterprise and Community and odoo.sh Edition.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="fa fa-check-square-o text-primary"></i>
|
||||||
|
13. Full Open Source.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- quick demo-->
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h2 class="bg-warning text-center pt8 pb8 mt16 mb16">1. Easy schedule database backup</h2>
|
||||||
|
<h4 class="oe_slogan">快速按日程自动备份,方便的直接下载 </h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo1.jpg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h2 class="bg-warning text-center pt8 pb8 mt16 mb16">2. Set remote backup and cron schedule</h2>
|
||||||
|
<h4 class="oe_slogan">支持远程备份 </h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo2.jpg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h2 class="bg-warning text-center pt8 pb8 mt16 mb16">3. Manual backup database in one click</h2>
|
||||||
|
<h4 class="oe_slogan">一键手动备份 </h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo3.jpg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h2 class="bg-warning text-center pt8 pb8 mt16 mb16">4. Easy download backup file or remove file for System user</h2>
|
||||||
|
<h4 class="oe_slogan">系统用户可以直接下载所有数据库备份文件,进行备份管理</h4>
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="demo4.jpg"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- end quick demo-->
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<div class="row">
|
||||||
|
<h2 class="oe_slogan">Database Automated backups</h2>
|
||||||
|
<p>optimized from auto_backup of Yenthe Van Ginneken</p>
|
||||||
|
<h3 class="oe_slogan">A tool for all your back-ups, internal and external!</h3>
|
||||||
|
</div>
|
||||||
|
<div class="oe_span6">
|
||||||
|
<div class="oe_demo oe_picture oe_screenshot img img-fluid">
|
||||||
|
<img src="overview.png">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="oe_span6">
|
||||||
|
<p class="oe_mt32">
|
||||||
|
Keep your Odoo data safe with this module. Take automated back-ups, remove them automatically
|
||||||
|
and even write them to an external server through an encrypted tunnel.
|
||||||
|
You can even specify how long local backups and external backups should be kept, automatically!
|
||||||
|
</p>
|
||||||
|
<div class="oe_centeralign oe_websiteonly">
|
||||||
|
<a href="http://www.openerp.com/start?app=mail" class="oe_button oe_big oe_tacky">Start your <span class="oe_emph">free</span> trial</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Second block -->
|
||||||
|
<section class="container oe_dark">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h2 class="oe_slogan">Connect with an FTP Server</h2>
|
||||||
|
<h3 class="oe_slogan">Keep your data safe, through an SSH tunnel!</h3>
|
||||||
|
<div class="oe_span6">
|
||||||
|
<p class="oe_mt32">
|
||||||
|
Want to go even further and write your backups to an external server?
|
||||||
|
You can with this module! Specify the credentials to the server, specify a path and everything will be backed up automatically. This is done through an SSH (encrypted) tunnel, thanks to pysftp, so your data is safe!
|
||||||
|
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="oe_span6">
|
||||||
|
<div class="oe_row_img oe_centered">
|
||||||
|
<img class="oe_picture oe_screenshot img img-fluid" src="terminalssh.png">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!--Third block -->
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<div class="row">
|
||||||
|
<h2 class="oe_slogan">Test connection</h2>
|
||||||
|
<h3 class="oe_slogan">Checks your credentials in one click</h3>
|
||||||
|
</div>
|
||||||
|
<div class="oe_span6">
|
||||||
|
<div class="oe_demo oe_picture oe_screenshot img img-fluid">
|
||||||
|
<img src="testconnection.png">
|
||||||
|
<img src="testconnectionfailed.png">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="oe_span6">
|
||||||
|
<p class="oe_mt32">
|
||||||
|
Want to make sure if the connection details are correct and if Odoo can automatically write them to the remote server? Simply click on the 'Test SFTP Connection' button and you will get message telling you if everything is OK, or what is wrong!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- Fourth block -->
|
||||||
|
<section class="container oe_dark">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h2 class="oe_slogan">E-mail on backup failure</h2>
|
||||||
|
<h3 class="oe_slogan">Stay informed of problems, automatically!</h3>
|
||||||
|
<div class="oe_span6">
|
||||||
|
<p class="oe_mt32">
|
||||||
|
Do you want to know if the database backup failed? Check the checkbox 'Auto. E-mail on backup fail' and fill in your e-mail.
|
||||||
|
Every time a backup fails you will get an e-mail in your mailbox with technical details.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="oe_span6">
|
||||||
|
<div class="oe_row_img oe_centered">
|
||||||
|
<img class="oe_picture oe_screenshot img img-fluid" src="emailnotification.png">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!--Fifth block -->
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<div class="row">
|
||||||
|
<h2 class="oe_slogan">Contact / Support</h2>
|
||||||
|
<h3 class="oe_slogan">Need help or want extra features?</h3>
|
||||||
|
</div>
|
||||||
|
<div class="oe_span6">
|
||||||
|
<p class="oe_mt32">
|
||||||
|
Need help with the configuration or want this module to have more functionalities?
|
||||||
|
Please create a bug report <a href="https://github.com/Yenthe666/auto_backup/issues">on the Github issue tracker</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- begin free-->
|
||||||
|
<!-- end free-->
|
||||||
|
|
||||||
|
<!-- begin howto-->
|
||||||
|
<section class="oe_container container s_text_block o_colored_level pt16 pb16">
|
||||||
|
<h2 class="text-center bg-info text-white pt16 pb16">- How to setup and use -</h2>
|
||||||
|
<h3>This app need no extra module. The price already included</h3>
|
||||||
|
</section>
|
||||||
|
<section class="oe_container container">
|
||||||
|
<div class="oe_row oe_spaced">
|
||||||
|
<h4 class="pt16">
|
||||||
|
1. Buy and Install
|
||||||
|
</h4>
|
||||||
|
<h4 class="pt16">
|
||||||
|
2. Read the app description for user guide
|
||||||
|
</h4>
|
||||||
|
<h4 class="pt16">
|
||||||
|
3. Enjoy and easy use
|
||||||
|
</h4>
|
||||||
|
<div class="row">
|
||||||
|
<div class="oe_demo oe_screenshot img img-fluid">
|
||||||
|
<img src="banner.png">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h4 class="pt16">4. More information in our FAQ</h4>
|
||||||
|
<div class="row">
|
||||||
|
https://www.odooai.cn/faq
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- end howto-->
|
||||||
|
|
||||||
|
<!-- begin upsell-->
|
||||||
|
<!-- end upsell-->
|
||||||
|
|
||||||
|
<!-- begin support-->
|
||||||
|
<section class="container oe_dark">
|
||||||
|
<div class="oe_row oe_spaced text-center">
|
||||||
|
<div class="row">
|
||||||
|
<h2 class="oe_slogan">Technical Help & Support</h2>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 pad0">
|
||||||
|
<div class="oe_mt16">
|
||||||
|
<p><h4 class="pt16">
|
||||||
|
For any type of technical help & support requests, Feel free to contact us</h4></p>
|
||||||
|
<a style="background: #002e5a none repeat scroll 0% 0%; color: rgb(255, 255, 255);position: relative; overflow: hidden;"
|
||||||
|
class="btn btn-warning btn-lg" rel="nofollow" href="mailto:odoo@china.com"><span
|
||||||
|
style="height: 354px; width: 354px; top: -147.433px; left: -6.93335px;" class="o_ripple"></span>
|
||||||
|
<i class="fa fa-envelope"></i> odoo@china.com</a>
|
||||||
|
<p><h4 class="pt16">
|
||||||
|
Via QQ: 300883 (App user would not get QQ or any other IM support. Only for odoo project customize.)</h4></p>
|
||||||
|
<a style="background: #002e5a none repeat scroll 0% 0%; color: rgb(255, 255, 255);position: relative; overflow: hidden;"
|
||||||
|
class="btn btn-warning btn-lg" rel="nofollow" href="mailto:300883@qq.com"><span
|
||||||
|
style="height: 354px; width: 354px; top: -147.433px; left: -6.93335px;" class="o_ripple"></span>
|
||||||
|
<i class="fa fa-envelope"></i> 300883@qq.com</a>
|
||||||
|
</div>
|
||||||
|
<div class="oe_mt16">
|
||||||
|
<h4 class="pt16">Visit our website for more support.</h4>
|
||||||
|
<h4 class="pt16">https://www.odooai.cn</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<!-- end support-->
|
||||||
|
</html>
|
||||||
BIN
app_auto_backup/static/description/overview.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
app_auto_backup/static/description/terminalssh.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
app_auto_backup/static/description/testconnection.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
app_auto_backup/static/description/testconnectionfailed.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
112
app_auto_backup/views/backup_view.xml
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<record id="view_backup_config_form" model="ir.ui.view">
|
||||||
|
<field name="name">db.backup.form</field>
|
||||||
|
<field name="model">db.backup</field>
|
||||||
|
<field name="type">form</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Back-up view">
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_button_box" name="button_box">
|
||||||
|
<button name="action_view_cron" string="View Cron" type="object" class="oe_stat_button" icon="fa-clock-o"/>
|
||||||
|
<button name="action_run_cron" string="Run Backup" type="object" class="oe_stat_button" icon="fa-play-circle"/>
|
||||||
|
</div>
|
||||||
|
<group col="4" colspan="4">
|
||||||
|
<separator col="2" string="Local backup configuration"/>
|
||||||
|
</group>
|
||||||
|
<group name="configuration">
|
||||||
|
<field name="host" colspan="2"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="port"/>
|
||||||
|
<field name="backup_type"/>
|
||||||
|
<field name="folder"/>
|
||||||
|
<field name="autoremove"/>
|
||||||
|
<field name="days_to_keep" invisible="not autoremove"/>
|
||||||
|
</group>
|
||||||
|
<group name="allow_stfp" col="4" colspan="4">
|
||||||
|
<separator col="2" string="SFTP"/>
|
||||||
|
</group>
|
||||||
|
<div style="width:50%;border-radius:10px;margin: 10px 0px;padding:15px 10px 15px 10px;
|
||||||
|
background-repeat: no-repeat;background-position: 10px center;color: #9F6000;background-color: #FEEFB3;" invisible="not sftp_write">
|
||||||
|
<b>Warning:</b>
|
||||||
|
Use SFTP with caution! This writes files to external servers under the path you specify.
|
||||||
|
</div>
|
||||||
|
<group name="sftp_configuration">
|
||||||
|
<field name="sftp_write"/>
|
||||||
|
<field name="sftp_host" invisible="not sftp_write" required="sftp_write"/>
|
||||||
|
<field name="sftp_port" invisible="not sftp_write" required="sftp_write"/>
|
||||||
|
<field name="sftp_user" invisible="not sftp_write" required="sftp_write"/>
|
||||||
|
<field name="sftp_password" invisible="not sftp_write" required="sftp_write" password="True"/>
|
||||||
|
<field name="sftp_path" invisible="not sftp_write" required="sftp_write" placeholder="For example: /odoo/backups/"/>
|
||||||
|
<field name="days_to_keep_sftp" invisible="not sftp_write" required="sftp_write"/>
|
||||||
|
<field name="send_mail_sftp_fail" invisible="not sftp_write"/>
|
||||||
|
<field name="email_to_notify" invisible="not send_mail_sftp_fail or not sftp_write" required="send_mail_sftp_fail"/>
|
||||||
|
<button name="test_sftp_connection" type="object" invisible="not sftp_write" string="Test SFTP Connection"/>
|
||||||
|
</group>
|
||||||
|
<separator string="Help" colspan="2"/>
|
||||||
|
<div name="configuration_details">
|
||||||
|
This configures the scheduler for automatic backup of the given database running on given host
|
||||||
|
at given port on regular intervals.
|
||||||
|
<br/>
|
||||||
|
Automatic backups of the database can be scheduled as follows:
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
Go to Settings / Technical / Automation / Scheduled Actions.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Search the action named 'Backup scheduler'.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Set the scheduler to active and fill in how often you want backups generated.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<p style="font-size:18px;">
|
||||||
|
Need more help?
|
||||||
|
<a href="https://www.odooai.cn">Contact odooai.cn!</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<notebook>
|
||||||
|
<page name="backup_details" string="Backup records">
|
||||||
|
<field name="backup_details_ids" readonly="1">
|
||||||
|
<list>
|
||||||
|
<field name="name" optional="hide"/>
|
||||||
|
<field name="file_path"/>
|
||||||
|
<field name="url" optional="hide"/>
|
||||||
|
<button name="action_download_file" type="object" title="Download File" class="fa fa-download"/>
|
||||||
|
<button name="action_remove_file" type="object" title="Remove File" class="fa fa-trash"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_backup_config_tree" model="ir.ui.view">
|
||||||
|
<field name="name">db.backup.tree</field>
|
||||||
|
<field name="model">db.backup</field>
|
||||||
|
<field name="type">list</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Backups">
|
||||||
|
<field name='host'/>
|
||||||
|
<field name='port'/>
|
||||||
|
<field name='name'/>
|
||||||
|
<field name='folder'/>
|
||||||
|
<field name="autoremove"/>
|
||||||
|
<field name="sftp_host"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_backup" model="ir.actions.act_window">
|
||||||
|
<field name="name">Configure backups</field>
|
||||||
|
<field name="res_model">db.backup</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="view_id" ref="view_backup_config_tree"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="auto_backup_menu" name="Back-ups" sequence="9" parent="app_odoo_customize.menu_app_group"/>
|
||||||
|
<menuitem parent="auto_backup_menu" action="action_backup" id="backup_conf_menu" sequence="1"/>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
24
app_auto_backup/views/db_backup_details.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="db_backup_details_tree_view" model="ir.ui.view">
|
||||||
|
<field name="name">db.backup.details.tree</field>
|
||||||
|
<field name="model">db.backup.details</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="file_path"/>
|
||||||
|
<field name="url" invisible="1"/>
|
||||||
|
<button name="action_download_file" type="object" title="Download File" class="fa fa-download"/>
|
||||||
|
<button name="action_remove_file" type="object" title="Remove File" class="fa fa-trash"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_db_backup_details" model="ir.actions.act_window">
|
||||||
|
<field name="name">Database backups</field>
|
||||||
|
<field name="res_model">db.backup.details</field>
|
||||||
|
<field name="view_mode">list</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_action_db_backup_details" action="action_db_backup_details" parent="auto_backup_menu" sequence="3"/>
|
||||||
|
</odoo>
|
||||||
4
app_chatgpt/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# from . import controllers
|
||||||
|
from . import models
|
||||||
76
app_chatgpt/__manifest__.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Created on 2023-02-016
|
||||||
|
# author: 欧度智能,https://www.odooai.cn
|
||||||
|
# email: 300883@qq.com
|
||||||
|
# resource of odooai
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
# Copyright (c) 2020-Present InTechual Solutions. (<https://intechualsolutions.com/>)
|
||||||
|
|
||||||
|
{
|
||||||
|
'name': 'ChatGPT4, China Ali,AiGC Center.Ai服务中心,聚合全网Ai',
|
||||||
|
'version': '24.11.06',
|
||||||
|
'author': 'odooai.cn',
|
||||||
|
'company': 'odooai.cn',
|
||||||
|
'maintainer': 'odooai.cn',
|
||||||
|
'category': 'Website/Website',
|
||||||
|
'website': 'https://www.odooai.cn',
|
||||||
|
'live_test_url': 'https://demo.odooapp.cn',
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
'sequence': 10,
|
||||||
|
'images': ['static/description/banner.gif'],
|
||||||
|
'summary': '''
|
||||||
|
ChatGpt Odoo AI Center. Multi Ai aigc support with Ali Qwen Ai, Azure Ai, Baidu Ai,etc..
|
||||||
|
Support chatgpt 4 32k, Integration All ChatGpt Api and Azure OpenAI.
|
||||||
|
Easy Chat channel with several ChatGPT Robots and train.
|
||||||
|
''',
|
||||||
|
'description': '''
|
||||||
|
Allows the application to leverage the capabilities of the GPT language model to generate human-like responses,
|
||||||
|
providing a more natural and intuitive user experience.
|
||||||
|
Base on is_chatgpt_integration from InTechual Solutions.
|
||||||
|
1. Multi ChatGpt openAI robot Connector. Chat and train.
|
||||||
|
2. Multi Ai support including Azure Ai, Alibaba Ai, Baidu Ai, Chatgpt 4, Chatgpt 3.5 Turbo, Chatgpt 3 Davinci.
|
||||||
|
3. Bind ChatGpt Api to user. So we can chat to robot user or use ChatGpt Channel for Group Chat.
|
||||||
|
4. White and black List for ChatGpt.
|
||||||
|
5. Setup Demo Chat time for every new user.
|
||||||
|
6. Easy Start and Stop ChatGpt.
|
||||||
|
7. Evaluation the ai robot to make better response. This training.
|
||||||
|
8. Add api support Connect the Microsoft Azure OpenAI Service.
|
||||||
|
9. Can set Synchronous or Asynchronous mode for Ai response.
|
||||||
|
10.Filter Sensitive Words Setup.
|
||||||
|
11. Multi-language Support. Multi-Company Support.
|
||||||
|
12. Support Odoo 18,17,16,15,14,13,12, Enterprise and Community and odoo.sh Edition.
|
||||||
|
13. Full Open Source.
|
||||||
|
''',
|
||||||
|
'depends': [
|
||||||
|
'base',
|
||||||
|
'app_odoo_customize',
|
||||||
|
'base_setup',
|
||||||
|
'mail',
|
||||||
|
# 'queue_job',
|
||||||
|
],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'security/ir_rules.xml',
|
||||||
|
'data/discuss_channel_data.xml',
|
||||||
|
'data/ai_robot_data.xml',
|
||||||
|
'data/user_partner_data.xml',
|
||||||
|
'data/ir_config_parameter.xml',
|
||||||
|
'views/ai_robot_views.xml',
|
||||||
|
'views/res_partner_ai_use_views.xml',
|
||||||
|
'views/res_users_views.xml',
|
||||||
|
'views/discuss_channel_views.xml',
|
||||||
|
],
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'app_chatgpt/static/src/models/message_model.js',
|
||||||
|
'app_chatgpt/static/src/components/message/message.js',
|
||||||
|
'app_chatgpt/static/src/components/*/*.xml',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'external_dependencies': {'python': ['typing_extensions', 'openai']},
|
||||||
|
'installable': True,
|
||||||
|
'application': True,
|
||||||
|
'auto_install': False,
|
||||||
|
}
|
||||||
3
app_chatgpt/controllers/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import main
|
||||||
10
app_chatgpt/controllers/main.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import http
|
||||||
|
|
||||||
|
|
||||||
|
class ChatgptController(http.Controller):
|
||||||
|
@http.route(['/chatgpt_form'], type='http', auth="public", csrf=False,
|
||||||
|
website=True)
|
||||||
|
def question_submit(self):
|
||||||
|
return http.request.render('app_chatgpt.connector')
|
||||||
53
app_chatgpt/data/ai_robot_data.xml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<record id="chatgpt_robot" model="ai.robot">
|
||||||
|
<field name="name">ChatGPT odoo</field>
|
||||||
|
<field name="provider">openai</field>
|
||||||
|
<field name="ai_model">gpt-4o</field>
|
||||||
|
<field name="endpoint">https://api.openai.com/v1/chat/completions</field>
|
||||||
|
<field name="sequence">1</field>
|
||||||
|
<field name="image_avatar" type="base64" file="app_chatgpt/static/description/src/openai.png"/>
|
||||||
|
</record>
|
||||||
|
<record id="chatgpt_robot1" model="ai.robot">
|
||||||
|
<field name="name">ChatGPT Coding</field>
|
||||||
|
<field name="provider">openai</field>
|
||||||
|
<field name="endpoint">https://api.openai.com/v1/chat/completions</field>
|
||||||
|
<field name="sequence">6</field>
|
||||||
|
<field name="image_avatar" type="base64" file="app_chatgpt/static/description/src/openai.png"/>
|
||||||
|
</record>
|
||||||
|
<record id="chatgpt_robot2" model="ai.robot">
|
||||||
|
<field name="name">ChatGPT Finance</field>
|
||||||
|
<field name="provider">openai</field>
|
||||||
|
<field name="endpoint">https://api.openai.com/v1/chat/completions</field>
|
||||||
|
<field name="sequence">7</field>
|
||||||
|
<field name="image_avatar" type="base64" file="app_chatgpt/static/description/src/openai.png"/>
|
||||||
|
</record>
|
||||||
|
<record id="chatgpt3_azure" model="ai.robot">
|
||||||
|
<field name="name">ChatGPT Azure</field>
|
||||||
|
<field name="provider">azure</field>
|
||||||
|
<field name="endpoint">https://my.openai.azure.com</field>
|
||||||
|
<field name="engine">gpt35</field>
|
||||||
|
<field name="api_version">2024-03-01-preview</field>
|
||||||
|
<field name="sequence">8</field>
|
||||||
|
<field name="image_avatar" type="base64" file="app_chatgpt/static/description/src/chatgpt35_azure.png"/>
|
||||||
|
</record>
|
||||||
|
<!-- 注意,在azure中,只需做1个部署即可,然后指定不同模型名。 因为其同时只能部署一个endpoint-->
|
||||||
|
<record id="chatgpt4_azure" model="ai.robot">
|
||||||
|
<field name="name">ChatGPT4 Azure</field>
|
||||||
|
<field name="provider">azure</field>
|
||||||
|
<field name="endpoint">https://my.openai.azure.com</field>
|
||||||
|
<field name="engine">gpt4</field>
|
||||||
|
<field name="api_version">2024-03-01-preview</field>
|
||||||
|
<field name="sequence">9</field>
|
||||||
|
<field name="image_avatar" type="base64" file="app_chatgpt/static/description/src/chatgpt4_azure.png"/>
|
||||||
|
</record>
|
||||||
|
<record id="chatgpt4_32k_azure" model="ai.robot">
|
||||||
|
<field name="name">ChatGPT4-32k Azure</field>
|
||||||
|
<field name="provider">azure</field>
|
||||||
|
<field name="endpoint">https://my.openai.azure.com</field>
|
||||||
|
<field name="engine">gpt4-32k</field>
|
||||||
|
<field name="api_version">2024-03-01-preview</field>
|
||||||
|
<field name="sequence">10</field>
|
||||||
|
<field name="image_avatar" type="base64" file="app_chatgpt/static/description/src/chatgpt4_azure.png"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
24
app_chatgpt/data/discuss_channel_data.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record model="discuss.channel" id="channel_chatgpt">
|
||||||
|
<field name="name">ChatGPT Group Chat</field>
|
||||||
|
<field name="description">ChatGPT话题</field>
|
||||||
|
<field name="image_128" type="base64" file="app_chatgpt/static/description/chatgpt.png"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="mail.message" id="module_install_notification">
|
||||||
|
<field name="model">discuss.channel</field>
|
||||||
|
<field name="res_id" ref="app_chatgpt.channel_chatgpt"/>
|
||||||
|
<field name="message_type">email</field>
|
||||||
|
<field name="subtype_id" ref="mail.mt_comment"/>
|
||||||
|
<field name="subject">Welcome to ChatGPT Group Chat</field>
|
||||||
|
<field name="body"><![CDATA[<p>Welcome to ChatGPT Group Chat.</p>
|
||||||
|
<p>Please ask me any question.</p>]]></field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="discuss.channel" id="app_chatgpt.channel_chatgpt">
|
||||||
|
<field name="group_ids" eval="[Command.link(ref('base.group_user'))]"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
7
app_chatgpt/data/ir_config_parameter.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<function model="ir.config_parameter" name="set_param" eval="('app_chatgpt.openapi_context_timeout', '300')"/>
|
||||||
|
<function model="ir.config_parameter" name="set_param" eval="('app_chatgpt.openai_sync_config', 'sync')"/>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
90
app_chatgpt/data/user_partner_data.xml
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="partner_chatgpt" model="res.partner">
|
||||||
|
<field name="name">ChatGPT odoo</field>
|
||||||
|
<field name="image_1920" type="base64" file="app_chatgpt/static/description/chatgpt.png"/>
|
||||||
|
</record>
|
||||||
|
<record id="user_chatgpt" model="res.users">
|
||||||
|
<field name="login">chatgpt@example.com</field>
|
||||||
|
<field name="partner_id" ref="app_chatgpt.partner_chatgpt"/>
|
||||||
|
<field name="gpt_id" ref="app_chatgpt.chatgpt_robot"/>
|
||||||
|
<field name="company_id" ref="base.main_company"/>
|
||||||
|
<field name="company_ids" eval="[Command.link(ref('base.main_company'))]"/>
|
||||||
|
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="partner_chatgpt1" model="res.partner">
|
||||||
|
<field name="name">ChatGPT Coding</field>
|
||||||
|
<field name="image_1920" type="base64" file="app_chatgpt/static/description/chatgpt.png"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="user_chatgpt1" model="res.users">
|
||||||
|
<field name="login">chatgpt1@example.com</field>
|
||||||
|
<field name="email">chatgpt1@example.com</field>
|
||||||
|
<field name="partner_id" ref="app_chatgpt.partner_chatgpt1"/>
|
||||||
|
<field name="gpt_id" ref="app_chatgpt.chatgpt_robot1"/>
|
||||||
|
<field name="company_id" ref="base.main_company"/>
|
||||||
|
<field name="company_ids" eval="[Command.link(ref('base.main_company'))]"/>
|
||||||
|
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="partner_chatgpt2" model="res.partner">
|
||||||
|
<field name="name">ChatGPT Finance</field>
|
||||||
|
<field name="image_1920" type="base64" file="app_chatgpt/static/description/chatgpt.png"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="user_chatgpt2" model="res.users">
|
||||||
|
<field name="login">chatgpt2@example.com</field>
|
||||||
|
<field name="email">chatgpt2@example.com</field>
|
||||||
|
<field name="partner_id" ref="app_chatgpt.partner_chatgpt2"/>
|
||||||
|
<field name="gpt_id" ref="app_chatgpt.chatgpt_robot2"/>
|
||||||
|
<field name="company_id" ref="base.main_company"/>
|
||||||
|
<field name="company_ids" eval="[Command.link(ref('base.main_company'))]"/>
|
||||||
|
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="partner_chatgpt3_azure" model="res.partner">
|
||||||
|
<field name="name">ChatGPT3 Azure</field>
|
||||||
|
<field name="image_1920" type="base64" file="app_chatgpt/static/description/src/chatgpt_blue.png"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="user_chatgpt3_azure" model="res.users">
|
||||||
|
<field name="login">chatgpt3_azure@example.com</field>
|
||||||
|
<field name="email">chatgpt3_azure@example.com</field>
|
||||||
|
<field name="partner_id" ref="app_chatgpt.partner_chatgpt3_azure"/>
|
||||||
|
<field name="gpt_id" ref="app_chatgpt.chatgpt3_azure"/>
|
||||||
|
<field name="company_id" ref="base.main_company"/>
|
||||||
|
<field name="company_ids" eval="[Command.link(ref('base.main_company'))]"/>
|
||||||
|
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
|
||||||
|
</record>
|
||||||
|
<record id="partner_chatgpt4_azure" model="res.partner">
|
||||||
|
<field name="name">ChatGPT4 Azure</field>
|
||||||
|
<field name="image_1920" type="base64" file="app_chatgpt/static/description/src/chatgpt4_azure.png"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="user_chatgpt4_azure" model="res.users">
|
||||||
|
<field name="login">chatgpt4_azure@example.com</field>
|
||||||
|
<field name="email">chatgpt4_azure@example.com</field>
|
||||||
|
<field name="partner_id" ref="app_chatgpt.partner_chatgpt4_azure"/>
|
||||||
|
<field name="gpt_id" ref="app_chatgpt.chatgpt4_azure"/>
|
||||||
|
<field name="company_id" ref="base.main_company"/>
|
||||||
|
<field name="company_ids" eval="[Command.link(ref('base.main_company'))]"/>
|
||||||
|
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
|
||||||
|
</record>
|
||||||
|
<record id="partner_chatgpt4_32k_azure" model="res.partner">
|
||||||
|
<field name="name">ChatGPT4-32k Azure</field>
|
||||||
|
<field name="image_1920" type="base64" file="app_chatgpt/static/description/src/chatgpt4_32k_azure.png"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="user_chatgpt4_32k_azure" model="res.users">
|
||||||
|
<field name="login">chatgpt4_32k_azure@example.com</field>
|
||||||
|
<field name="email">chatgpt4_32k_azure@example.com</field>
|
||||||
|
<field name="partner_id" ref="app_chatgpt.partner_chatgpt4_32k_azure"/>
|
||||||
|
<field name="gpt_id" ref="app_chatgpt.chatgpt4_32k_azure"/>
|
||||||
|
<field name="company_id" ref="base.main_company"/>
|
||||||
|
<field name="company_ids" eval="[Command.link(ref('base.main_company'))]"/>
|
||||||
|
<field name="groups_id" eval="[Command.link(ref('base.group_user'))]"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
27879
app_chatgpt/i18n/zh_CN.po
Normal file
10
app_chatgpt/models/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import res_partner
|
||||||
|
from . import discuss_channel
|
||||||
|
from . import res_config_settings
|
||||||
|
from . import ai_robot
|
||||||
|
from . import res_partner_ai_use
|
||||||
|
from . import res_users
|
||||||
|
from . import mail_message
|
||||||
|
from . import mail_thread
|
||||||
381
app_chatgpt/models/ai_robot.py
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
from openai import OpenAI
|
||||||
|
from openai import AzureOpenAI
|
||||||
|
import requests, json
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from odoo import api, fields, models, modules, tools, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
import logging
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AiRobot(models.Model):
|
||||||
|
_name = 'ai.robot'
|
||||||
|
_description = 'Ai Robot'
|
||||||
|
_order = 'sequence, name'
|
||||||
|
|
||||||
|
name = fields.Char(string='Name', translate=True, required=True)
|
||||||
|
provider = fields.Selection(string="AI Provider", selection=[('openai', 'OpenAI'), ('azure', 'Azure')],
|
||||||
|
required=True, default='openai', change_default=True)
|
||||||
|
# update ai_robot set ai_model=set_ai_model
|
||||||
|
ai_model = fields.Char(string="Ai Model", required=True, default='auto', help='Customize input')
|
||||||
|
set_ai_model = fields.Selection(string="Quick Set Model", selection=[
|
||||||
|
('gpt-3.5-turbo-0125', 'gpt-3.5-turbo-0125(Default and Latest)'),
|
||||||
|
('gpt-3.5-turbo-0613', 'gpt-3.5-turbo-0613'),
|
||||||
|
('gpt-3.5-turbo-0125', 'gpt-3.5-turbo-0125'),
|
||||||
|
('gpt-3.5-turbo-16k-0613', 'gpt-3.5-turbo-16k-0613(Big text)'),
|
||||||
|
('gpt-4o', 'GPT-4o'),
|
||||||
|
('gpt-4-turbo', 'GPT-4 Turbo'),
|
||||||
|
('gpt-4', 'Chatgpt 4'),
|
||||||
|
('gpt-4-32k', 'Chatgpt 4 32k'),
|
||||||
|
('gpt-3.5-turbo', 'Chatgpt 3.5 Turbo'),
|
||||||
|
('gpt-3.5-turbo-0301', 'Chatgpt 3.5 Turbo on 20230301'),
|
||||||
|
('text-davinci-003', 'Chatgpt 3 Davinci'),
|
||||||
|
('code-davinci-002', 'Chatgpt 2 Code Optimized'),
|
||||||
|
('text-davinci-002', 'Chatgpt 2 Davinci'),
|
||||||
|
('dall-e2', 'Dall-E Image'),
|
||||||
|
], default='gpt-3.5-turbo-0125',
|
||||||
|
help="""
|
||||||
|
GPT-4: Can understand Image, generate natural language or code.
|
||||||
|
GPT-3.5: A set of models that improve on GPT-3 and can understand as well as generate natural language or code
|
||||||
|
DALL·E: A model that can generate and edit images given a natural language prompt
|
||||||
|
Whisper: A model that can convert audio into text
|
||||||
|
Embeddings: A set of models that can convert text into a numerical form
|
||||||
|
CodexLimited: A set of models that can understand and generate code, including translating natural language to code
|
||||||
|
Moderation: A fine-tuned model that can detect whether text may be sensitive or unsafe
|
||||||
|
GPT-3 A set of models that can understand and generate natural language
|
||||||
|
""")
|
||||||
|
openapi_api_key = fields.Char(string="API Key", help="Provide the API key here")
|
||||||
|
# begin gpt 参数
|
||||||
|
# 1. stop:表示聊天机器人停止生成回复的条件,可以是一段文本或者一个列表,当聊天机器人生成的回复中包含了这个条件,就会停止继续生成回复。
|
||||||
|
# 2. temperature:0-2,控制回复的“新颖度”,值越高,聊天机器人生成的回复越不确定和随机,值越低,聊天机器人生成的回复会更加可预测和常规化。
|
||||||
|
# 3. top_p:0-1,语言连贯性,与temperature有些类似,也是控制回复的“新颖度”。不同的是,top_p控制的是回复中概率最高的几个可能性的累计概率之和,值越小,生成的回复越保守,值越大,生成的回复越新颖。
|
||||||
|
# 4. frequency_penalty:-2~2,用于控制聊天机器人回复中出现频率过高的词汇的惩罚程度。聊天机器人会尝试避免在回复中使用频率较高的词汇,以提高回复的多样性和新颖度。
|
||||||
|
# 5. presence_penalty:-2~2与frequency_penalty相对,用于控制聊天机器人回复中出现频率较低的词汇的惩罚程度。聊天机器人会尝试在回复中使用频率较低的词汇,以提高回复的多样性和新颖度。
|
||||||
|
max_tokens = fields.Integer('Max Response', default=600,
|
||||||
|
help="""
|
||||||
|
Set a limit on the number of tokens per model response.
|
||||||
|
The API supports a maximum of 4000 tokens shared between the prompt
|
||||||
|
(including system message, examples, message history, and user query) and the model's response.
|
||||||
|
One token is roughly 4 characters for typical English text.
|
||||||
|
""")
|
||||||
|
temperature = fields.Float(string='Temperature', default=1,
|
||||||
|
help="""
|
||||||
|
Controls randomness. Lowering the temperature means that the model will produce
|
||||||
|
more repetitive and deterministic responses.
|
||||||
|
Increasing the temperature will result in more unexpected or creative responses.
|
||||||
|
Try adjusting temperature or Top P but not both.
|
||||||
|
""")
|
||||||
|
top_p = fields.Float('Top Probabilities', default=0.6,
|
||||||
|
help="""
|
||||||
|
Similar to temperature, this controls randomness but uses a different method.
|
||||||
|
Lowering Top P will narrow the model’s token selection to likelier tokens.
|
||||||
|
Increasing Top P will let the model choose from tokens with both high and low likelihood.
|
||||||
|
Try adjusting temperature or Top P but not both
|
||||||
|
""")
|
||||||
|
# 避免使用常用词
|
||||||
|
frequency_penalty = fields.Float('Frequency Penalty', default=1,
|
||||||
|
help="""
|
||||||
|
Reduce the chance of repeating a token proportionally based on how often it has appeared in the text so far.
|
||||||
|
This decreases the likelihood of repeating the exact same text in a response.
|
||||||
|
""")
|
||||||
|
# 越大模型就趋向于生成更新的话题,惩罚已经出现过的文本
|
||||||
|
presence_penalty = fields.Float('Presence penalty', default=1,
|
||||||
|
help="""
|
||||||
|
Reduce the chance of repeating any token that has appeared in the text at all so far.
|
||||||
|
This increases the likelihood of introducing new topics in a response.
|
||||||
|
""")
|
||||||
|
# 停止回复的关键词
|
||||||
|
stop = fields.Char('Stop sequences',
|
||||||
|
help="""
|
||||||
|
Use , to separate the stop key word.
|
||||||
|
Make responses stop at a desired point, such as the end of a sentence or list.
|
||||||
|
Specify up to four sequences where the model will stop generating further tokens in a response.
|
||||||
|
The returned text will not contain the stop sequence.
|
||||||
|
""")
|
||||||
|
# 角色设定
|
||||||
|
sys_content = fields.Char('System message',
|
||||||
|
help="""
|
||||||
|
Give the model instructions about how it should behave and any context it should reference when generating a response.
|
||||||
|
You can describe the assistant’s personality,
|
||||||
|
tell it what it should and shouldn’t answer, and tell it how to format responses.
|
||||||
|
There’s no token limit for this section, but it will be included with every API call,
|
||||||
|
so it counts against the overall token limit.
|
||||||
|
""")
|
||||||
|
# end gpt 参数
|
||||||
|
endpoint = fields.Char('End Point', default='https://api.openai.com/v1/chat/completions')
|
||||||
|
engine = fields.Char('Engine', help='If use Azure, Please input the Model deployment name.')
|
||||||
|
api_version = fields.Char('API Version', default='2022-12-01')
|
||||||
|
ai_timeout = fields.Integer('Timeout(seconds)', help="Connect timeout for Ai response", default=120)
|
||||||
|
sequence = fields.Integer('Sequence', help="Determine the display order", default=10)
|
||||||
|
sensitive_words = fields.Text('Sensitive Words Plus', help='Sensitive word filtering. Separate keywords with a carriage return.')
|
||||||
|
is_filtering = fields.Boolean('Filter Sensitive Words', default=False, help='Use base Filter in dir models/lib/sensi_words.txt')
|
||||||
|
|
||||||
|
max_send_char = fields.Integer('Max Send Char', help='Max Send Prompt Length', default=8000)
|
||||||
|
image_avatar = fields.Image('Avatar')
|
||||||
|
partner_ids = fields.One2many('res.partner', 'gpt_id', string='Partner')
|
||||||
|
partner_count = fields.Integer('#Partner', compute='_compute_partner_count', store=False)
|
||||||
|
active = fields.Boolean('Active', default=True)
|
||||||
|
|
||||||
|
def _compute_partner_count(self):
|
||||||
|
for rec in self:
|
||||||
|
rec.partner_count = len(rec.partner_ids)
|
||||||
|
|
||||||
|
def action_disconnect(self):
|
||||||
|
requests.delete('https://chatgpt.com/v1/disconnect')
|
||||||
|
|
||||||
|
def get_ai_pre(self, data, author_id=False, answer_id=False, param={}):
|
||||||
|
# hook,都正常
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_ai(self, data, author_id=False, answer_id=False, param={}):
|
||||||
|
# 通用方法
|
||||||
|
# author_id: 请求的 partner_id 对象
|
||||||
|
# answer_id: 回答的 partner_id 对象
|
||||||
|
# param,dict 形式的参数
|
||||||
|
# 调整输出为2个参数:res_post详细内容,is_ai是否ai的响应
|
||||||
|
|
||||||
|
self.ensure_one()
|
||||||
|
# 前置勾子,一般返回 False,有问题返回响应内容,用于处理敏感词等
|
||||||
|
res_pre = self.get_ai_pre(data, author_id, answer_id, param)
|
||||||
|
if res_pre:
|
||||||
|
# 有错误内容,则返回上级内容及 is_ai为假
|
||||||
|
return res_pre, {}, False
|
||||||
|
if not hasattr(self, 'get_%s' % self.provider):
|
||||||
|
res = _('No robot provider found')
|
||||||
|
return res, {}, False
|
||||||
|
|
||||||
|
res = getattr(self, 'get_%s' % self.provider)(data, author_id, answer_id, param)
|
||||||
|
# 后置勾子,返回处理后的内容
|
||||||
|
res_post, usage, is_ai = self.get_ai_post(res, author_id, answer_id, param)
|
||||||
|
return res_post, usage, is_ai
|
||||||
|
|
||||||
|
def get_ai_origin(self, data, author_id=False, answer_id=False, param={}):
|
||||||
|
# 通用方法
|
||||||
|
# author_id: 请求的 partner_id 对象
|
||||||
|
# answer_id: 回答的 partner_id 对象
|
||||||
|
# param,dict 形式的参数
|
||||||
|
# 调整输出为2个参数:res_post详细内容,is_ai是否ai的响应
|
||||||
|
|
||||||
|
self.ensure_one()
|
||||||
|
# 前置勾子,一般返回 False,有问题返回响应内容,用于处理敏感词等
|
||||||
|
res_pre = self.get_ai_pre(data, author_id, answer_id, param)
|
||||||
|
if res_pre:
|
||||||
|
# 有错误内容,则返回上级内容及 is_ai为假
|
||||||
|
return res_pre, {}, False
|
||||||
|
if not hasattr(self, 'get_%s' % self.provider):
|
||||||
|
res = _('No robot provider found')
|
||||||
|
return res, {}, False
|
||||||
|
|
||||||
|
res = getattr(self, 'get_%s' % self.provider)(data, author_id, answer_id, param)
|
||||||
|
# 后置勾子,返回处理后的内容
|
||||||
|
res_post, usage, is_ai = self.get_ai_post(res, author_id, answer_id, param)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def get_ai_post(self, res, author_id=False, answer_id=False, param=None):
|
||||||
|
# hook,高级版要替代
|
||||||
|
if param is None:
|
||||||
|
param = {}
|
||||||
|
if not res or not author_id or (not isinstance(res, list) and not isinstance(res, dict)):
|
||||||
|
return res, False, False
|
||||||
|
# 返回是个对象,那么就是ai
|
||||||
|
usage = content = data = None
|
||||||
|
try:
|
||||||
|
if self.provider == 'openai':
|
||||||
|
# openai 格式处理
|
||||||
|
usage = res['usage']
|
||||||
|
content = res['choices'][0]['message']['content']
|
||||||
|
# _logger.warning('===========Ai响应:%s' % content)
|
||||||
|
elif self.provider == 'azure':
|
||||||
|
# azure 格式
|
||||||
|
usage = res['usage']
|
||||||
|
content = res['choices'][0]['message']['content']
|
||||||
|
else:
|
||||||
|
usage = False
|
||||||
|
content = res
|
||||||
|
data = content.replace(' .', '.').strip()
|
||||||
|
return data, usage, True
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error('==========app_chatgpt get_ai_post Error: %s' % e)
|
||||||
|
return res, False, False
|
||||||
|
|
||||||
|
def get_ai_system(self, content=None):
|
||||||
|
# 获取基础ai角色设定, role system
|
||||||
|
sys_content = content or self.sys_content
|
||||||
|
if sys_content:
|
||||||
|
return {"role": "system", "content": sys_content}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_ai_model_info(self):
|
||||||
|
self.ensure_one()
|
||||||
|
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {self.openapi_api_key}"}
|
||||||
|
R_TIMEOUT = self.ai_timeout or 120
|
||||||
|
o_url = "https://api.openai.com/v1/models/%s" % self.ai_model
|
||||||
|
if self.endpoint:
|
||||||
|
o_url = self.endpoint.replace("/chat/completions", "") + "/models/%s" % self.ai_model
|
||||||
|
|
||||||
|
response = requests.get(o_url, headers=headers, timeout=R_TIMEOUT)
|
||||||
|
response.close()
|
||||||
|
if response:
|
||||||
|
res = response.json()
|
||||||
|
r_text = json.dumps(res, indent=2)
|
||||||
|
else:
|
||||||
|
r_text = 'No response.'
|
||||||
|
raise UserError(r_text)
|
||||||
|
|
||||||
|
def get_ai_list_model(self):
|
||||||
|
self.ensure_one()
|
||||||
|
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {self.openapi_api_key}"}
|
||||||
|
R_TIMEOUT = self.ai_timeout or 120
|
||||||
|
o_url = "https://api.openai.com/v1/models"
|
||||||
|
if self.endpoint:
|
||||||
|
o_url = self.endpoint.replace("/chat/completions", "") + "/models"
|
||||||
|
response = requests.get(o_url, headers=headers, timeout=R_TIMEOUT)
|
||||||
|
response.close()
|
||||||
|
if response:
|
||||||
|
res = response.json()
|
||||||
|
r_text = json.dumps(res, indent=2)
|
||||||
|
else:
|
||||||
|
r_text = 'No response.'
|
||||||
|
raise UserError(r_text)
|
||||||
|
|
||||||
|
def get_openai(self, data, author_id, answer_id, param={}):
|
||||||
|
self.ensure_one()
|
||||||
|
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {self.openapi_api_key}"}
|
||||||
|
R_TIMEOUT = self.ai_timeout or 120
|
||||||
|
o_url = self.endpoint or "https://api.openai.com/v1/chat/completions"
|
||||||
|
|
||||||
|
# 处理传参,传过来的优先于 robot 默认的
|
||||||
|
max_tokens = param.get('max_tokens') if param.get('max_tokens') else self.max_tokens
|
||||||
|
temperature = param.get('temperature') if param.get('temperature') else self.temperature
|
||||||
|
top_p = param.get('top_p') if param.get('top_p') else self.top_p
|
||||||
|
frequency_penalty = param.get('frequency_penalty') if param.get('frequency_penalty') else self.frequency_penalty
|
||||||
|
presence_penalty = param.get('presence_penalty') if param.get('presence_penalty') else self.presence_penalty
|
||||||
|
request_timeout = param.get('request_timeout') if param.get('request_timeout') else self.ai_timeout
|
||||||
|
|
||||||
|
if self.stop:
|
||||||
|
stop = self.stop.split(',')
|
||||||
|
else:
|
||||||
|
stop = ["Human:", "AI:"]
|
||||||
|
# 以下处理 open ai
|
||||||
|
if self.ai_model == 'dall-e2':
|
||||||
|
# todo: 处理 图像引擎,主要是返回参数到聊天中
|
||||||
|
# image_url = response['data'][0]['url']
|
||||||
|
# https://platform.openai.com/docs/guides/images/introduction
|
||||||
|
pdata = {
|
||||||
|
"prompt": data,
|
||||||
|
"n": 3,
|
||||||
|
"size": "1024x1024",
|
||||||
|
}
|
||||||
|
return '建设中'
|
||||||
|
else:
|
||||||
|
pdata = {
|
||||||
|
"model": self.ai_model,
|
||||||
|
"prompt": data,
|
||||||
|
"temperature": 1,
|
||||||
|
"max_tokens": max_tokens,
|
||||||
|
"top_p": 0.6,
|
||||||
|
"frequency_penalty": 0.1,
|
||||||
|
"presence_penalty": 0.1,
|
||||||
|
"stop": stop
|
||||||
|
}
|
||||||
|
client = OpenAI(
|
||||||
|
api_key=self.openapi_api_key,
|
||||||
|
timeout=R_TIMEOUT
|
||||||
|
)
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
messages=data,
|
||||||
|
model=self.ai_model,
|
||||||
|
)
|
||||||
|
res = response.model_dump()
|
||||||
|
if 'choices' in res:
|
||||||
|
return res
|
||||||
|
else:
|
||||||
|
_logger.warning('=====================openai output data: %s' % response.json())
|
||||||
|
|
||||||
|
return _("Response Timeout, please speak again.")
|
||||||
|
|
||||||
|
def get_azure(self, data, author_id, answer_id, param={}):
|
||||||
|
self.ensure_one()
|
||||||
|
# only for azure
|
||||||
|
if not self.endpoint:
|
||||||
|
raise UserError(_("Please Set your AI robot's endpoint first."))
|
||||||
|
|
||||||
|
if not self.api_version:
|
||||||
|
raise UserError(_("Please Set your AI robot's API Version first."))
|
||||||
|
|
||||||
|
if self.stop:
|
||||||
|
stop = self.stop.split(',')
|
||||||
|
else:
|
||||||
|
stop = ["Human:", "AI:"]
|
||||||
|
if isinstance(data, list):
|
||||||
|
messages = data
|
||||||
|
else:
|
||||||
|
messages = [{"role": "user", "content": data}]
|
||||||
|
|
||||||
|
# 处理传参,传过来的优先于 robot 默认的
|
||||||
|
max_tokens = param.get('max_tokens') if param.get('max_tokens') else self.max_tokens
|
||||||
|
temperature = param.get('temperature') if param.get('temperature') else self.temperature
|
||||||
|
top_p = param.get('top_p') if param.get('top_p') else self.top_p
|
||||||
|
frequency_penalty = param.get('frequency_penalty') if param.get('frequency_penalty') else self.frequency_penalty
|
||||||
|
presence_penalty = param.get('presence_penalty') if param.get('presence_penalty') else self.presence_penalty
|
||||||
|
request_timeout= param.get('request_timeout') if param.get('request_timeout') else self.ai_timeout
|
||||||
|
|
||||||
|
# Ai角色设定,如果没设定则再处理
|
||||||
|
if messages[0].get('role') != 'system':
|
||||||
|
sys_content = self.get_ai_system(param.get('sys_content'))
|
||||||
|
if sys_content:
|
||||||
|
messages.insert(0, sys_content)
|
||||||
|
# 暂时不变
|
||||||
|
|
||||||
|
client = AzureOpenAI(
|
||||||
|
api_version=self.api_version,
|
||||||
|
azure_endpoint=self.endpoint,
|
||||||
|
api_key=self.openapi_api_key,
|
||||||
|
timeout=request_timeout
|
||||||
|
)
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model=self.engine,
|
||||||
|
messages=messages,
|
||||||
|
# 返回的回答数量
|
||||||
|
n=1,
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
temperature=temperature,
|
||||||
|
top_p=top_p,
|
||||||
|
frequency_penalty=frequency_penalty,
|
||||||
|
presence_penalty=presence_penalty,
|
||||||
|
stop=None,
|
||||||
|
)
|
||||||
|
res = response.model_dump()
|
||||||
|
if 'choices' in res:
|
||||||
|
return res
|
||||||
|
else:
|
||||||
|
_logger.warning('=====================azure output data: %s' % response.json())
|
||||||
|
return _("Response Timeout, please speak again.")
|
||||||
|
|
||||||
|
@api.onchange('provider')
|
||||||
|
def _onchange_provider(self):
|
||||||
|
if self.provider == 'openai':
|
||||||
|
self.endpoint = 'https://api.openai.com/v1/chat/completions'
|
||||||
|
elif self.provider == 'azure':
|
||||||
|
self.endpoint = 'https://odoo.openai.azure.com'
|
||||||
|
|
||||||
|
if self.provider:
|
||||||
|
# 取头像
|
||||||
|
module_path = modules.get_module_path('app_chatgpt', display_warning=False)
|
||||||
|
if module_path:
|
||||||
|
path = modules.check_resource_path(module_path, ('static/description/src/%s.png' % self.provider))
|
||||||
|
if path:
|
||||||
|
image_file = tools.file_open(path, 'rb')
|
||||||
|
self.image_avatar = base64.b64encode(image_file.read())
|
||||||
|
|
||||||
|
@api.onchange('set_ai_model')
|
||||||
|
def _onchange_set_ai_model(self):
|
||||||
|
if self.set_ai_model:
|
||||||
|
self.ai_model = self.set_ai_model
|
||||||
|
else:
|
||||||
|
self.ai_model = None
|
||||||
358
app_chatgpt/models/discuss_channel.py
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import openai
|
||||||
|
import requests, json
|
||||||
|
import datetime
|
||||||
|
# from transformers import TextDavinciTokenizer, TextDavinciModel
|
||||||
|
from odoo import api, fields, models, tools, _
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
from odoo.osv import expression
|
||||||
|
from odoo.addons.app_common.models.base import get_ua_type
|
||||||
|
|
||||||
|
import logging
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Channel(models.Model):
|
||||||
|
_inherit = 'discuss.channel'
|
||||||
|
|
||||||
|
is_private = fields.Boolean(string="Private", default=False, help="Check to set Private, Can only use by user, not Public")
|
||||||
|
# 因为 channel_member_ids 不好处理,在此增加此字段
|
||||||
|
# 主Ai
|
||||||
|
ai_partner_id = fields.Many2one(comodel_name="res.partner", string="Main Ai", required=False,
|
||||||
|
domain=[('gpt_id', '!=', None), ('is_chat_private', '=', True)],
|
||||||
|
default=lambda self: self._app_get_m2o_default('ai_partner_id'),
|
||||||
|
help="Main Ai is the robot help you default.")
|
||||||
|
ext_ai_partner_id = fields.Many2one(comodel_name="res.partner", string="Secondary Ai",
|
||||||
|
domain=[('gpt_id', '!=', None), ('is_chat_private', '=', True)])
|
||||||
|
description = fields.Text('Ai Character', help="Ai would help you act as the Character set.")
|
||||||
|
set_max_tokens = fields.Selection([
|
||||||
|
('300', 'Short'),
|
||||||
|
('600', 'Standard'),
|
||||||
|
('1000', 'Medium'),
|
||||||
|
('2000', 'Long'),
|
||||||
|
('3000', 'Overlength'),
|
||||||
|
('32000', '32K'),
|
||||||
|
], string='Max Response', default='600', help="越大返回内容越多,计费也越多")
|
||||||
|
set_chat_count = fields.Selection([
|
||||||
|
('none', 'Ai Auto'),
|
||||||
|
('1', '1标准'),
|
||||||
|
('3', '3强关联'),
|
||||||
|
('5', '5超强关联'),
|
||||||
|
], string="History Count", default='1', help="0-5,设定后,会将最近n次对话发给Ai,有助于他更好的回答,但太大费用也高")
|
||||||
|
set_temperature = fields.Selection([
|
||||||
|
('2', '天马行空'),
|
||||||
|
('1.5', '创造性'),
|
||||||
|
('1', '标准'),
|
||||||
|
('0.6', '理性'),
|
||||||
|
('0.1', '保守'),
|
||||||
|
], string="Set Temperature", default='1', help="0-21,值越大越富有想像力,越小则越保守")
|
||||||
|
set_top_p = fields.Selection([
|
||||||
|
('0.9', '严谨惯性思维'),
|
||||||
|
('0.6', '标准推理'),
|
||||||
|
('0.4', '跳跃性'),
|
||||||
|
('0.1', '随便'),
|
||||||
|
], string="Top Probabilities", default='0.6', help="0-1,值越大越倾向大众化的连贯思维")
|
||||||
|
# 避免使用常用词
|
||||||
|
set_frequency_penalty = fields.Selection([
|
||||||
|
('2', '老学究-晦涩难懂'),
|
||||||
|
('1.5', '学院派-较多高级词'),
|
||||||
|
('1', '标准'),
|
||||||
|
('0.1', '少常用词'),
|
||||||
|
('-1', '通俗易懂'),
|
||||||
|
('-2', '大白话'),
|
||||||
|
], string='Frequency Penalty', default='1', help="-2~2,值越大越少使用常用词")
|
||||||
|
set_presence_penalty = fields.Selection([
|
||||||
|
('2', '多样强迫症'),
|
||||||
|
('1.5', '新颖化'),
|
||||||
|
('1', '标准'),
|
||||||
|
('0.1', '允许常规重复'),
|
||||||
|
('-1', '允许较多重复'),
|
||||||
|
('-2', '更多强调重复'),
|
||||||
|
], string='Presence penalty', default='1', help="-2~2,值越大越少重复词")
|
||||||
|
|
||||||
|
# todo: 这里用 compute?
|
||||||
|
max_tokens = fields.Integer('最长响应Token', default=600, help="越大返回内容越多,计费也越多")
|
||||||
|
chat_count = fields.Integer(string="上下文数量", default=0, help="0~3,设定后,会将最近n次对话发给Ai,有助于他更好的回答")
|
||||||
|
temperature = fields.Float(string="创造性值", default=1, help="0~2,值越大越富有想像力,越小则越保守")
|
||||||
|
top_p = fields.Float(string="连贯性值", default=0.6, help="0~1,值越大越富有想像力,越小则越保守")
|
||||||
|
frequency_penalty = fields.Float('避免常用词值', default=1, help="-2~2,值越大越少使用常用词")
|
||||||
|
presence_penalty = fields.Float('避免重复词值', default=1, help="-2~2,值越大越少重复词")
|
||||||
|
|
||||||
|
is_current_channel = fields.Boolean('是否当前用户默认频道', compute='_compute_is_current_channel', help='是否当前用户默认微信对话频道')
|
||||||
|
|
||||||
|
def name_get(self):
|
||||||
|
result = []
|
||||||
|
for c in self:
|
||||||
|
pre = '[私]' if c.channel_type == 'channel' and c.is_private else ''
|
||||||
|
result.append((c.id, f"{pre}{c.name or ''}"))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_openai_context(self, channel_id, author_id, answer_id, minutes=60, chat_count=0):
|
||||||
|
# 上下文处理,要处理群的方式,以及独聊的方式
|
||||||
|
# azure新api 处理
|
||||||
|
context_history = []
|
||||||
|
afterTime = fields.Datetime.now() - datetime.timedelta(minutes=minutes)
|
||||||
|
message_model = self.env['mail.message'].sudo()
|
||||||
|
# 处理消息: 取最新问题 + 上 chat_count=1次的交互,将之前的交互按时间顺序拼接。
|
||||||
|
# 注意: ai 每一次回复都有 parent_id 来处理连续性
|
||||||
|
# 私聊处理
|
||||||
|
|
||||||
|
# todo: 更好的处理方式
|
||||||
|
domain = [('res_id', '=', channel_id),
|
||||||
|
('model', '=', 'discuss.channel'),
|
||||||
|
('message_type', '!=', 'user_notification'),
|
||||||
|
('parent_id', '!=', False),
|
||||||
|
('is_ai', '=', True),
|
||||||
|
('body', '!=', '<p>%s</p>' % _('Response Timeout, please speak again.')),
|
||||||
|
('body', '!=', _('温馨提示:您发送的内容含有敏感词,请修改内容后再向我发送。'))]
|
||||||
|
|
||||||
|
if self.channel_type in ['group', 'channel']:
|
||||||
|
# 群聊增加时间限制,当前找所有人,不限制 author_id
|
||||||
|
domain = expression.AND([domain, [('date', '>=', afterTime)]])
|
||||||
|
else:
|
||||||
|
domain = expression.AND([domain, [('author_id', '=', answer_id.id)]])
|
||||||
|
if chat_count == 0:
|
||||||
|
ai_msg_list = []
|
||||||
|
else:
|
||||||
|
ai_msg_list = message_model.with_context(tz='UTC').search(domain, order="id desc", limit=chat_count)
|
||||||
|
for ai_msg in ai_msg_list:
|
||||||
|
# 判断这个 ai_msg 是不是ai发,有才 insert。 判断 user_msg 是不是 user发的,有才 insert
|
||||||
|
user_msg = ai_msg.parent_id.sudo()
|
||||||
|
if ai_msg.author_id.sudo().gpt_id and answer_id.sudo().gpt_id and ai_msg.author_id.sudo().gpt_id == answer_id.sudo().gpt_id:
|
||||||
|
ai_content = str(ai_msg.body).replace("<p>", "").replace("</p>", "").replace("<p>", "")
|
||||||
|
context_history.insert(0, {
|
||||||
|
'role': 'assistant',
|
||||||
|
'content': ai_content,
|
||||||
|
})
|
||||||
|
if not user_msg.author_id.gpt_id:
|
||||||
|
user_content = user_msg.description.replace("<p>", "").replace("</p>", "").replace('@%s' % answer_id.name, '').lstrip()
|
||||||
|
context_history.insert(0, {
|
||||||
|
'role': 'user',
|
||||||
|
'content': user_content,
|
||||||
|
})
|
||||||
|
return context_history
|
||||||
|
|
||||||
|
def get_ai_config(self, ai):
|
||||||
|
# 勾子,用于取ai 配置
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_ai_response(self, ai, messages, channel, user_id, message):
|
||||||
|
author_id = message.create_uid.partner_id
|
||||||
|
answer_id = user_id.partner_id
|
||||||
|
# todo: 只有个人配置的群聊才给配置
|
||||||
|
param = self.get_ai_config(ai)
|
||||||
|
res, usage, is_ai = ai.get_ai(messages, author_id, answer_id, param)
|
||||||
|
if res:
|
||||||
|
if get_ua_type() != 'wxweb':
|
||||||
|
# 处理当微信语音返回时,是直接回文本信息,不需要转换回车
|
||||||
|
res = res.replace('\n', '<br/>')
|
||||||
|
new_msg = channel.with_user(user_id).message_post(body=res, message_type='comment', subtype_xmlid='mail.mt_comment', parent_id=message.id)
|
||||||
|
if usage:
|
||||||
|
if ai.provider == 'ali':
|
||||||
|
prompt_tokens = usage['input_tokens']
|
||||||
|
completion_tokens = usage['output_tokens']
|
||||||
|
total_tokens = usage['input_tokens'] + usage['output_tokens']
|
||||||
|
else:
|
||||||
|
prompt_tokens = usage['prompt_tokens']
|
||||||
|
completion_tokens = usage['completion_tokens']
|
||||||
|
total_tokens = usage['total_tokens']
|
||||||
|
new_msg.write({
|
||||||
|
'human_prompt_tokens': prompt_tokens,
|
||||||
|
'ai_completion_tokens': completion_tokens,
|
||||||
|
'cost_tokens': total_tokens,
|
||||||
|
})
|
||||||
|
|
||||||
|
def _notify_thread(self, message, msg_vals=False, **kwargs):
|
||||||
|
rdata = super(Channel, self)._notify_thread(message, msg_vals=msg_vals, **kwargs)
|
||||||
|
# print(f'rdata:{rdata}')
|
||||||
|
answer_id = self.env['res.partner']
|
||||||
|
user_id = self.env['res.users']
|
||||||
|
author_id = msg_vals.get('author_id')
|
||||||
|
ai = self.env['ai.robot'].sudo()
|
||||||
|
channel = self.env['discuss.channel']
|
||||||
|
channel_type = self.channel_type
|
||||||
|
messages = []
|
||||||
|
|
||||||
|
# 不处理 一般notify,但处理欢迎
|
||||||
|
if '<div class="o_mail_notification' in message.body and message.body != _('<div class="o_mail_notification">joined the channel</div>'):
|
||||||
|
return rdata
|
||||||
|
if 'o_odoobot_command' in message.body:
|
||||||
|
return rdata
|
||||||
|
|
||||||
|
if channel_type == 'chat':
|
||||||
|
channel_partner_ids = self.channel_partner_ids
|
||||||
|
answer_id = channel_partner_ids - message.author_id
|
||||||
|
user_id = answer_id.mapped('user_ids').sudo().filtered(lambda r: r.gpt_id)[:1]
|
||||||
|
if user_id and answer_id.gpt_id:
|
||||||
|
gpt_policy = user_id.gpt_policy
|
||||||
|
gpt_wl_partners = user_id.gpt_wl_partners
|
||||||
|
is_allow = message.author_id.id in gpt_wl_partners.ids
|
||||||
|
if gpt_policy == 'all' or (gpt_policy == 'limit' and is_allow):
|
||||||
|
ai = answer_id.sudo().gpt_id
|
||||||
|
|
||||||
|
elif channel_type in ['group', 'channel']:
|
||||||
|
# partner_ids = @ ids
|
||||||
|
partner_ids = list(msg_vals.get('partner_ids'))
|
||||||
|
if hasattr(self, 'ai_partner_id') and self.ai_partner_id:
|
||||||
|
# 当有主id时,使用主id
|
||||||
|
if self.ai_partner_id.id in partner_ids:
|
||||||
|
partner_ids = [self.ai_partner_id.id]
|
||||||
|
if partner_ids:
|
||||||
|
# 常规群聊 @
|
||||||
|
partners = self.env['res.partner'].search([('id', 'in', partner_ids)])
|
||||||
|
# user_id = user, who has binded gpt robot
|
||||||
|
user_id = partners.mapped('user_ids').sudo().filtered(lambda r: r.gpt_id)[:1]
|
||||||
|
elif message.body == _('<div class="o_mail_notification">joined the channel</div>'):
|
||||||
|
# 欢迎的情况
|
||||||
|
partners = self.channel_partner_ids.sudo().filtered(lambda r: r.gpt_id)[:1]
|
||||||
|
user_id = partners.mapped('user_ids')[:1]
|
||||||
|
elif self.member_count == 2:
|
||||||
|
# 处理独聊频道
|
||||||
|
if hasattr(self, 'is_private') and not self.is_private:
|
||||||
|
# 2个人的非私有频道不处理
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
partners = self.channel_partner_ids.sudo().filtered(lambda r: r.gpt_id and r != message.author_id)[:1]
|
||||||
|
user_id = partners.mapped('user_ids')[:1]
|
||||||
|
elif not message.author_id.gpt_id:
|
||||||
|
# 没有@时,默认第一个robot
|
||||||
|
# robot = self.env.ref('app_chatgpt.chatgpt_robot')
|
||||||
|
# 临时用azure
|
||||||
|
if hasattr(self, 'ai_partner_id') and self.ai_partner_id:
|
||||||
|
# 当有主id时,使用主id
|
||||||
|
user_id = self.ai_partner_id.mapped('user_ids')[:1]
|
||||||
|
else:
|
||||||
|
# 使用群里的第一个robot
|
||||||
|
partners = self.channel_partner_ids.sudo().filtered(lambda r: r.gpt_id)[:1]
|
||||||
|
user_id = partners.mapped('user_ids')[:1]
|
||||||
|
if user_id:
|
||||||
|
ai = user_id.sudo().gpt_id
|
||||||
|
# 此处理不判断,将此处逻辑迁移至 get_ai_pre, 非ai回复的直接内容注意设置为 is_ai=false
|
||||||
|
# gpt_policy = user_id.gpt_policy
|
||||||
|
# gpt_wl_partners = user_id.gpt_wl_partners
|
||||||
|
# is_allow = message.author_id.id in gpt_wl_partners.ids
|
||||||
|
# answer_id = user_id.partner_id
|
||||||
|
# if gpt_policy == 'all' or (gpt_policy == 'limit' and is_allow):
|
||||||
|
# ai = user_id.sudo().gpt_id
|
||||||
|
# elif user_id.gpt_id and not is_allow:
|
||||||
|
# # 暂时有限用户的Ai
|
||||||
|
# raise UserError(_('此Ai暂时未开放,请联系管理员。'))
|
||||||
|
if hasattr(ai, 'is_translator') and ai.is_translator and ai.ai_model == 'translator':
|
||||||
|
return rdata
|
||||||
|
chatgpt_channel_id = self.env.ref('app_chatgpt.channel_chatgpt')
|
||||||
|
|
||||||
|
if message.body == _('<div class="o_mail_notification">joined the channel</div>'):
|
||||||
|
msg = _("Please warmly welcome our new partner %s and send him the best wishes.") % message.author_id.name
|
||||||
|
else:
|
||||||
|
# 不能用 preview, 如果用 : 提示词则 preview信息丢失
|
||||||
|
plaintext_ct = tools.html_to_inner_content(message.body)
|
||||||
|
msg = plaintext_ct.replace('@%s' % answer_id.name, '').lstrip()
|
||||||
|
|
||||||
|
if not msg:
|
||||||
|
return rdata
|
||||||
|
|
||||||
|
if self._context.get('app_ai_sync_config') and self._context.get('app_ai_sync_config') in ['sync', 'async']:
|
||||||
|
sync_config = self._context.get('app_ai_sync_config')
|
||||||
|
else:
|
||||||
|
sync_config = self.env['ir.config_parameter'].sudo().get_param('app_chatgpt.openai_sync_config')
|
||||||
|
# api_key = self.env['ir.config_parameter'].sudo().get_param('app_chatgpt.openapi_api_key')
|
||||||
|
# ai处理,不要自问自答
|
||||||
|
if ai and answer_id != message.author_id:
|
||||||
|
api_key = ai.openapi_api_key
|
||||||
|
if not api_key:
|
||||||
|
_logger.warning(_("ChatGPT Robot【%s】have not set open api key."))
|
||||||
|
return rdata
|
||||||
|
try:
|
||||||
|
openapi_context_timeout = int(self.env['ir.config_parameter'].sudo().get_param('app_chatgpt.openapi_context_timeout')) or 60
|
||||||
|
except:
|
||||||
|
openapi_context_timeout = 60
|
||||||
|
openai.api_key = api_key
|
||||||
|
# 非4版本,取0次。其它取3 次历史
|
||||||
|
chat_count = 3
|
||||||
|
if '4' in ai.ai_model or '4' in ai.name:
|
||||||
|
chat_count = 1
|
||||||
|
if hasattr(self, 'chat_count'):
|
||||||
|
if self.chat_count > 0:
|
||||||
|
chat_count = 1
|
||||||
|
else:
|
||||||
|
chat_count = chat_count
|
||||||
|
|
||||||
|
if author_id != answer_id.id and self.channel_type == 'chat':
|
||||||
|
# 私聊
|
||||||
|
_logger.info(f'私聊:author_id:{author_id},partner_chatgpt.id:{answer_id.id}')
|
||||||
|
channel = self.env[msg_vals.get('model')].browse(msg_vals.get('res_id'))
|
||||||
|
elif author_id != answer_id.id and msg_vals.get('model', '') == 'discuss.channel' and msg_vals.get('res_id', 0) == chatgpt_channel_id.id:
|
||||||
|
# todo: 公开的群聊,当前只开1个,后续更多
|
||||||
|
_logger.info(f'频道群聊:author_id:{author_id},partner_chatgpt.id:{answer_id.id}')
|
||||||
|
channel = chatgpt_channel_id
|
||||||
|
elif author_id != answer_id.id and msg_vals.get('model', '') == 'discuss.channel' and self.channel_type in ['group', 'channel']:
|
||||||
|
# 高级用户自建的话题
|
||||||
|
channel = self.env[msg_vals.get('model')].browse(msg_vals.get('res_id'))
|
||||||
|
if hasattr(channel, 'is_private') and channel.description:
|
||||||
|
messages.append({"role": "system", "content": channel.description})
|
||||||
|
|
||||||
|
try:
|
||||||
|
c_history = self.get_openai_context(channel.id, author_id, answer_id, openapi_context_timeout, chat_count)
|
||||||
|
if c_history:
|
||||||
|
messages += c_history
|
||||||
|
messages.append({"role": "user", "content": msg})
|
||||||
|
msg_len = sum(len(str(m)) for m in messages)
|
||||||
|
# 接口最大接收 8430 Token
|
||||||
|
if msg_len * 2 > ai.max_send_char:
|
||||||
|
messages = []
|
||||||
|
if hasattr(channel, 'is_private') and channel.description:
|
||||||
|
messages.append({"role": "system", "content": channel.description})
|
||||||
|
messages.append({"role": "user", "content": msg})
|
||||||
|
msg_len = sum(len(str(m)) for m in messages)
|
||||||
|
if msg_len * 2 > ai.max_send_char:
|
||||||
|
new_msg = channel.with_user(user_id).message_post(body=_('您所发送的提示词已超长。'), message_type='comment',
|
||||||
|
subtype_xmlid='mail.mt_comment',
|
||||||
|
parent_id=message.id)
|
||||||
|
|
||||||
|
# if msg_len * 2 >= 8000:
|
||||||
|
# messages = [{"role": "user", "content": msg}]
|
||||||
|
self.get_ai_response(ai, messages, channel, user_id, message)
|
||||||
|
# if sync_config == 'sync':
|
||||||
|
# self.get_ai_response(ai, messages, channel, user_id, message)
|
||||||
|
# else:
|
||||||
|
# self.with_delay().get_ai_response(ai, messages, channel, user_id, message)
|
||||||
|
except Exception as e:
|
||||||
|
raise UserError(_(e))
|
||||||
|
|
||||||
|
return rdata
|
||||||
|
|
||||||
|
def _message_post_after_hook(self, message, msg_vals):
|
||||||
|
if message.author_id.gpt_id:
|
||||||
|
if msg_vals['body'] not in [_('Response Timeout, please speak again.'), _('温馨提示:您发送的内容含有敏感词,请修改内容后再向我发送。'),
|
||||||
|
_('此Ai暂时未开放,请联系管理员。'), _('您所发送的提示词已超长。')]:
|
||||||
|
message.is_ai = True
|
||||||
|
return super(Channel, self)._message_post_after_hook(message, msg_vals)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_my_last_cid(self):
|
||||||
|
# 获取当前用户最后一次进入的channel,返回该channel的id
|
||||||
|
# todo: 优化,每次聊天进入时就 write
|
||||||
|
user = self.env.user
|
||||||
|
msgs = self.env['mail.message'].sudo().search([
|
||||||
|
('model', '=', 'discuss.channel'),
|
||||||
|
('author_id', '=', user.partner_id.id),
|
||||||
|
], limit=3, order='id desc')
|
||||||
|
c_id = 0
|
||||||
|
c = self
|
||||||
|
for m in msgs:
|
||||||
|
c = self.browse(m.res_id)
|
||||||
|
if c.is_member:
|
||||||
|
c_id = c.id
|
||||||
|
break
|
||||||
|
if not c_id:
|
||||||
|
c = self.env.ref('app_chatgpt.channel_chatgpt', raise_if_not_found=False)
|
||||||
|
c_id = c.id or False
|
||||||
|
if c and not c.is_member:
|
||||||
|
c.sudo().add_members([user.partner_id.id])
|
||||||
|
return c_id
|
||||||
|
|
||||||
|
@api.onchange('ai_partner_id')
|
||||||
|
def _onchange_ai_partner_id(self):
|
||||||
|
if self.ai_partner_id and self.ai_partner_id.image_1920:
|
||||||
|
self.image_128 = self.ai_partner_id.avatar_128
|
||||||
32
app_chatgpt/models/mail_message.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class Message(models.Model):
|
||||||
|
_inherit = "mail.message"
|
||||||
|
|
||||||
|
human_prompt_tokens = fields.Integer('Human Prompt Tokens')
|
||||||
|
ai_completion_tokens = fields.Integer('AI Completion Tokens')
|
||||||
|
cost_tokens = fields.Integer('Cost Tokens')
|
||||||
|
# 是否ai回复
|
||||||
|
is_ai = fields.Boolean('Is Ai', default=False)
|
||||||
|
# 得到 ai 响应后,需要特殊处理ai的
|
||||||
|
ai2model = fields.Char('Ai Response model')
|
||||||
|
ai2id = fields.Integer('Ai Response id')
|
||||||
|
|
||||||
|
def _message_add_reaction(self, content):
|
||||||
|
super(Message, self)._message_add_reaction(content)
|
||||||
|
if self.create_uid.gpt_id:
|
||||||
|
# 处理反馈
|
||||||
|
pass
|
||||||
|
|
||||||
|
def message_format(self, format_reply=True, msg_vals=None):
|
||||||
|
message_values = super(Message, self).message_format(format_reply=format_reply, msg_vals=msg_vals)
|
||||||
|
for message in message_values:
|
||||||
|
message_sudo = self.browse(message['id']).sudo().with_prefetch(self.ids)
|
||||||
|
message['human_prompt_tokens'] = message_sudo.human_prompt_tokens
|
||||||
|
message['ai_completion_tokens'] = message_sudo.ai_completion_tokens
|
||||||
|
message['cost_tokens'] = message_sudo.cost_tokens
|
||||||
|
message['is_ai'] = message_sudo.is_ai
|
||||||
|
return message_values
|
||||||
7
app_chatgpt/models/mail_thread.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import fields, models, api, _
|
||||||
|
|
||||||
|
|
||||||
|
class MailThread(models.AbstractModel):
|
||||||
|
_inherit = "mail.thread"
|
||||||
21
app_chatgpt/models/res.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
'id': 'chatcmpl-747IRWr2Ij3HA6NVTWp4ZTnEA2grW',
|
||||||
|
'object': 'chat.completion',
|
||||||
|
'created': 1681215715,
|
||||||
|
'model': 'gpt-3.5-turbo-0301',
|
||||||
|
'usage': {
|
||||||
|
'prompt_tokens': 17,
|
||||||
|
'completion_tokens': 38,
|
||||||
|
'total_tokens': 55
|
||||||
|
},
|
||||||
|
'choices': [
|
||||||
|
{
|
||||||
|
'message': {
|
||||||
|
'role': 'assistant',
|
||||||
|
'content': '非常抱歉,我不太理解您在说什么。 可以提供更多背景信息或上下文吗?'
|
||||||
|
},
|
||||||
|
'finish_reason': 'stop',
|
||||||
|
'index': 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
13
app_chatgpt/models/res_config_settings.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
_inherit = "res.config.settings"
|
||||||
|
|
||||||
|
openapi_context_timeout = fields.Integer(string="Connect Timout", help="群聊中多少分钟以内的聊天信息作为上下文继续", config_parameter="app_chatgpt.openapi_context_timeout")
|
||||||
|
openai_sync_config = fields.Selection([
|
||||||
|
('sync', 'Synchronous'),
|
||||||
|
('async', 'Asynchronous')
|
||||||
|
], string='Sync Config', default='sync', config_parameter="app_chatgpt.openai_sync_config")
|
||||||
61
app_chatgpt/models/res_partner.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import fields, models, api
|
||||||
|
from odoo.addons.mail.tools.discuss import Store
|
||||||
|
|
||||||
|
|
||||||
|
class ResPartner(models.Model):
|
||||||
|
_inherit = "res.partner"
|
||||||
|
|
||||||
|
gpt_id = fields.Many2one('ai.robot', string='Bind to Ai', ondelete='set null')
|
||||||
|
|
||||||
|
is_chat_private = fields.Boolean('Allow Chat Private', default=False)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def im_search(self, name, limit=20, excluded_ids=None):
|
||||||
|
users = self.env['res.users'].search([
|
||||||
|
('id', '!=', self.env.user.id),
|
||||||
|
('name', 'ilike', name),
|
||||||
|
('active', '=', True),
|
||||||
|
('share', '=', False),
|
||||||
|
('is_chat_private', '=', True)
|
||||||
|
], order='gpt_id, name, id', limit=limit)
|
||||||
|
# return list(users.partner_id.mail_partner_format().values())
|
||||||
|
return Store(users.partner_id).get_result()
|
||||||
|
|
||||||
|
def mail_partner_format(self, fields=None):
|
||||||
|
# TODO: remove !!!
|
||||||
|
# 直接覆盖原生,增加 gpt_id 字段
|
||||||
|
partners_format = dict()
|
||||||
|
if not fields:
|
||||||
|
fields = {'id': True, 'name': True, 'email': True, 'active': True, 'im_status': True, 'gpt_id': 0, 'user': {}}
|
||||||
|
for partner in self:
|
||||||
|
data = {}
|
||||||
|
if 'id' in fields:
|
||||||
|
data['id'] = partner.id
|
||||||
|
if 'name' in fields:
|
||||||
|
name = partner.name
|
||||||
|
# 英文不好分,暂时不隐名
|
||||||
|
# if not partner.related_user_id.gpt_id:
|
||||||
|
# name = partner.name[0] + '*' * (len(partner.name) - 1)
|
||||||
|
data['name'] = name
|
||||||
|
if 'email' in fields:
|
||||||
|
data['email'] = partner.email
|
||||||
|
if 'active' in fields:
|
||||||
|
data['active'] = partner.active
|
||||||
|
if 'im_status' in fields:
|
||||||
|
data['im_status'] = partner.im_status
|
||||||
|
if 'gpt_id' in fields:
|
||||||
|
data['gpt_id'] = partner.gpt_id.id if partner.gpt_id else 0
|
||||||
|
if 'user' in fields:
|
||||||
|
internal_users = partner.user_ids - partner.user_ids.filtered('share')
|
||||||
|
main_user = internal_users[0] if len(internal_users) > 0 else partner.user_ids[0] if len(partner.user_ids) > 0 else self.env['res.users']
|
||||||
|
data['user'] = {
|
||||||
|
"id": main_user.id,
|
||||||
|
"isInternalUser": not main_user.share,
|
||||||
|
} if main_user else [('clear',)]
|
||||||
|
# if 'guest' in self.env.context or not self.env.user.has_group('base.group_erp_manager'):
|
||||||
|
# 完全不显示 邮箱
|
||||||
|
data.pop('email', None)
|
||||||
|
partners_format[partner] = data
|
||||||
|
return partners_format
|
||||||
23
app_chatgpt/models/res_partner_ai_use.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class ResPartnerAiUse(models.Model):
|
||||||
|
_name = "res.partner.ai.use"
|
||||||
|
_description = '消费者Ai使用情况'
|
||||||
|
|
||||||
|
name = fields.Many2one('res.partner', 'Partner')
|
||||||
|
ai_user_id = fields.Many2one('res.users', 'Ai User', domain=[('gpt_id', '!=', False)])
|
||||||
|
first_ask_time = fields.Datetime('First Ask Time')
|
||||||
|
latest_ask_time = fields.Datetime('Latest Ask Time')
|
||||||
|
service_start_date = fields.Datetime('Service Start Date')
|
||||||
|
service_end_date = fields.Datetime('Service End Date')
|
||||||
|
used_number = fields.Integer('Number of Used')
|
||||||
|
max_number = fields.Integer('Max Number of Call')
|
||||||
|
human_prompt_tokens = fields.Integer('Human Prompt Tokens')
|
||||||
|
ai_completion_tokens = fields.Integer('AI Completion Tokens')
|
||||||
|
tokens_total = fields.Integer('Total Tokens')
|
||||||
|
token_balance = fields.Integer('Token Balance')
|
||||||
|
# balance = allow - total
|
||||||
|
token_allow = fields.Integer('Token Allow')
|
||||||
17
app_chatgpt/models/res_users.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class ResUsers(models.Model):
|
||||||
|
_inherit = "res.users"
|
||||||
|
|
||||||
|
# 改为在 partner中设置,用户处绑定
|
||||||
|
gpt_id = fields.Many2one('ai.robot', string='Bind to Ai', related='partner_id.gpt_id', inherited=True, readonly=False)
|
||||||
|
gpt_policy = fields.Selection([
|
||||||
|
('all', 'All Users'),
|
||||||
|
('limit', 'Selected Users')
|
||||||
|
], string='Allowed Conversation Mode', default='all', ondelete='set default')
|
||||||
|
gpt_wl_partners = fields.Many2many('res.partner', 'res_partner_ai_use', 'ai_user_id', 'name', string='Allowed Partners')
|
||||||
|
gpt_demo_time = fields.Integer('Default Demo Time', default=0)
|
||||||
|
is_chat_private = fields.Boolean('Allow Chat Private', default=False, related='partner_id.is_chat_private', inherited=True, readonly=False)
|
||||||
1270
app_chatgpt/models/z_list_model.json
Normal file
23
app_chatgpt/models/z_model_info.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"id": "gpt-3.5-turbo",
|
||||||
|
"object": "model",
|
||||||
|
"created": 1677610602,
|
||||||
|
"owned_by": "openai",
|
||||||
|
"permission": [
|
||||||
|
{
|
||||||
|
"object": "model_permission",
|
||||||
|
"created": 1677691854,
|
||||||
|
"allow_create_engine": false,
|
||||||
|
"allow_sampling": true,
|
||||||
|
"allow_logprobs": true,
|
||||||
|
"allow_search_indices": false,
|
||||||
|
"allow_view": true,
|
||||||
|
"allow_fine_tuning": false,
|
||||||
|
"organization": "*",
|
||||||
|
"group": null,
|
||||||
|
"is_blocking": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"root": "gpt-3.5-turbo",
|
||||||
|
"parent": null
|
||||||
|
}
|
||||||
5
app_chatgpt/security/ir.model.access.csv
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_gpt_robt_user,access_gpt_robt_user,model_ai_robot,base.group_user,1,0,0,0
|
||||||
|
access_gpt_robt_manager,access_gpt_robt_manager,model_ai_robot,base.group_erp_manager,1,1,1,1
|
||||||
|
access_res_partner_ai_use_user,access_res_partner_ai_use_user,model_res_partner_ai_use,base.group_user,1,0,0,0
|
||||||
|
access_res_partner_ai_use_manager,access_res_partner_ai_use_manager,model_res_partner_ai_use,base.group_erp_manager,1,1,1,1
|
||||||
|
16
app_chatgpt/security/ir_rules.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo noupdate="1">
|
||||||
|
<record id="res_partner_ai_use_personal_rule" model="ir.rule">
|
||||||
|
<field name="name">Personal AI Use</field>
|
||||||
|
<field ref="model_res_partner_ai_use" name="model_id"/>
|
||||||
|
<field name="domain_force">[('name','=',user.partner_id.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="res_partner_ai_use_see_all" model="ir.rule">
|
||||||
|
<field name="name">All AI Use</field>
|
||||||
|
<field ref="model_res_partner_ai_use" name="model_id"/>
|
||||||
|
<field name="domain_force">[(1,'=',1)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_erp_manager'))]"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
BIN
app_chatgpt/static/description/ai_chat.mp4
Normal file
BIN
app_chatgpt/static/description/app_ai_seo.gif
Normal file
|
After Width: | Height: | Size: 9.0 MiB |
BIN
app_chatgpt/static/description/banner.gif
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
app_chatgpt/static/description/banner.png
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
app_chatgpt/static/description/bard.gif
Normal file
|
After Width: | Height: | Size: 271 KiB |
BIN
app_chatgpt/static/description/chatgpt.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
app_chatgpt/static/description/chatgpt_blue.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
app_chatgpt/static/description/chatgpt_green.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |