commands.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. import datetime
  2. import math
  3. import random
  4. import string
  5. import time
  6. import click
  7. from flask import current_app
  8. from werkzeug.exceptions import NotFound
  9. from core.index.index import IndexBuilder
  10. from core.model_providers.providers.hosted import hosted_model_providers
  11. from libs.password import password_pattern, valid_password, hash_password
  12. from libs.helper import email as email_validate
  13. from extensions.ext_database import db
  14. from libs.rsa import generate_key_pair
  15. from models.account import InvitationCode, Tenant
  16. from models.dataset import Dataset, DatasetQuery, Document
  17. from models.model import Account
  18. import secrets
  19. import base64
  20. from models.provider import Provider, ProviderType, ProviderQuotaType, ProviderModel
  21. @click.command('reset-password', help='Reset the account password.')
  22. @click.option('--email', prompt=True, help='The email address of the account whose password you need to reset')
  23. @click.option('--new-password', prompt=True, help='the new password.')
  24. @click.option('--password-confirm', prompt=True, help='the new password confirm.')
  25. def reset_password(email, new_password, password_confirm):
  26. if str(new_password).strip() != str(password_confirm).strip():
  27. click.echo(click.style('sorry. The two passwords do not match.', fg='red'))
  28. return
  29. account = db.session.query(Account). \
  30. filter(Account.email == email). \
  31. one_or_none()
  32. if not account:
  33. click.echo(click.style('sorry. the account: [{}] not exist .'.format(email), fg='red'))
  34. return
  35. try:
  36. valid_password(new_password)
  37. except:
  38. click.echo(
  39. click.style('sorry. The passwords must match {} '.format(password_pattern), fg='red'))
  40. return
  41. # generate password salt
  42. salt = secrets.token_bytes(16)
  43. base64_salt = base64.b64encode(salt).decode()
  44. # encrypt password with salt
  45. password_hashed = hash_password(new_password, salt)
  46. base64_password_hashed = base64.b64encode(password_hashed).decode()
  47. account.password = base64_password_hashed
  48. account.password_salt = base64_salt
  49. db.session.commit()
  50. click.echo(click.style('Congratulations!, password has been reset.', fg='green'))
  51. @click.command('reset-email', help='Reset the account email.')
  52. @click.option('--email', prompt=True, help='The old email address of the account whose email you need to reset')
  53. @click.option('--new-email', prompt=True, help='the new email.')
  54. @click.option('--email-confirm', prompt=True, help='the new email confirm.')
  55. def reset_email(email, new_email, email_confirm):
  56. if str(new_email).strip() != str(email_confirm).strip():
  57. click.echo(click.style('Sorry, new email and confirm email do not match.', fg='red'))
  58. return
  59. account = db.session.query(Account). \
  60. filter(Account.email == email). \
  61. one_or_none()
  62. if not account:
  63. click.echo(click.style('sorry. the account: [{}] not exist .'.format(email), fg='red'))
  64. return
  65. try:
  66. email_validate(new_email)
  67. except:
  68. click.echo(
  69. click.style('sorry. {} is not a valid email. '.format(email), fg='red'))
  70. return
  71. account.email = new_email
  72. db.session.commit()
  73. click.echo(click.style('Congratulations!, email has been reset.', fg='green'))
  74. @click.command('reset-encrypt-key-pair', help='Reset the asymmetric key pair of workspace for encrypt LLM credentials. '
  75. 'After the reset, all LLM credentials will become invalid, '
  76. 'requiring re-entry.'
  77. 'Only support SELF_HOSTED mode.')
  78. @click.confirmation_option(prompt=click.style('Are you sure you want to reset encrypt key pair?'
  79. ' this operation cannot be rolled back!', fg='red'))
  80. def reset_encrypt_key_pair():
  81. if current_app.config['EDITION'] != 'SELF_HOSTED':
  82. click.echo(click.style('Sorry, only support SELF_HOSTED mode.', fg='red'))
  83. return
  84. tenant = db.session.query(Tenant).first()
  85. if not tenant:
  86. click.echo(click.style('Sorry, no workspace found. Please enter /install to initialize.', fg='red'))
  87. return
  88. tenant.encrypt_public_key = generate_key_pair(tenant.id)
  89. db.session.query(Provider).filter(Provider.provider_type == 'custom').delete()
  90. db.session.query(ProviderModel).delete()
  91. db.session.commit()
  92. click.echo(click.style('Congratulations! '
  93. 'the asymmetric key pair of workspace {} has been reset.'.format(tenant.id), fg='green'))
  94. @click.command('generate-invitation-codes', help='Generate invitation codes.')
  95. @click.option('--batch', help='The batch of invitation codes.')
  96. @click.option('--count', prompt=True, help='Invitation codes count.')
  97. def generate_invitation_codes(batch, count):
  98. if not batch:
  99. now = datetime.datetime.now()
  100. batch = now.strftime('%Y%m%d%H%M%S')
  101. if not count or int(count) <= 0:
  102. click.echo(click.style('sorry. the count must be greater than 0.', fg='red'))
  103. return
  104. count = int(count)
  105. click.echo('Start generate {} invitation codes for batch {}.'.format(count, batch))
  106. codes = ''
  107. for i in range(count):
  108. code = generate_invitation_code()
  109. invitation_code = InvitationCode(
  110. code=code,
  111. batch=batch
  112. )
  113. db.session.add(invitation_code)
  114. click.echo(code)
  115. codes += code + "\n"
  116. db.session.commit()
  117. filename = 'storage/invitation-codes-{}.txt'.format(batch)
  118. with open(filename, 'w') as f:
  119. f.write(codes)
  120. click.echo(click.style(
  121. 'Congratulations! Generated {} invitation codes for batch {} and saved to the file \'{}\''.format(count, batch,
  122. filename),
  123. fg='green'))
  124. def generate_invitation_code():
  125. code = generate_upper_string()
  126. while db.session.query(InvitationCode).filter(InvitationCode.code == code).count() > 0:
  127. code = generate_upper_string()
  128. return code
  129. def generate_upper_string():
  130. letters_digits = string.ascii_uppercase + string.digits
  131. result = ""
  132. for i in range(8):
  133. result += random.choice(letters_digits)
  134. return result
  135. @click.command('recreate-all-dataset-indexes', help='Recreate all dataset indexes.')
  136. def recreate_all_dataset_indexes():
  137. click.echo(click.style('Start recreate all dataset indexes.', fg='green'))
  138. recreate_count = 0
  139. page = 1
  140. while True:
  141. try:
  142. datasets = db.session.query(Dataset).filter(Dataset.indexing_technique == 'high_quality') \
  143. .order_by(Dataset.created_at.desc()).paginate(page=page, per_page=50)
  144. except NotFound:
  145. break
  146. page += 1
  147. for dataset in datasets:
  148. try:
  149. click.echo('Recreating dataset index: {}'.format(dataset.id))
  150. index = IndexBuilder.get_index(dataset, 'high_quality')
  151. if index and index._is_origin():
  152. index.recreate_dataset(dataset)
  153. recreate_count += 1
  154. else:
  155. click.echo('passed.')
  156. except Exception as e:
  157. click.echo(
  158. click.style('Recreate dataset index error: {} {}'.format(e.__class__.__name__, str(e)), fg='red'))
  159. continue
  160. click.echo(click.style('Congratulations! Recreate {} dataset indexes.'.format(recreate_count), fg='green'))
  161. @click.command('clean-unused-dataset-indexes', help='Clean unused dataset indexes.')
  162. def clean_unused_dataset_indexes():
  163. click.echo(click.style('Start clean unused dataset indexes.', fg='green'))
  164. clean_days = int(current_app.config.get('CLEAN_DAY_SETTING'))
  165. start_at = time.perf_counter()
  166. thirty_days_ago = datetime.datetime.now() - datetime.timedelta(days=clean_days)
  167. page = 1
  168. while True:
  169. try:
  170. datasets = db.session.query(Dataset).filter(Dataset.created_at < thirty_days_ago) \
  171. .order_by(Dataset.created_at.desc()).paginate(page=page, per_page=50)
  172. except NotFound:
  173. break
  174. page += 1
  175. for dataset in datasets:
  176. dataset_query = db.session.query(DatasetQuery).filter(
  177. DatasetQuery.created_at > thirty_days_ago,
  178. DatasetQuery.dataset_id == dataset.id
  179. ).all()
  180. if not dataset_query or len(dataset_query) == 0:
  181. documents = db.session.query(Document).filter(
  182. Document.dataset_id == dataset.id,
  183. Document.indexing_status == 'completed',
  184. Document.enabled == True,
  185. Document.archived == False,
  186. Document.updated_at > thirty_days_ago
  187. ).all()
  188. if not documents or len(documents) == 0:
  189. try:
  190. # remove index
  191. vector_index = IndexBuilder.get_index(dataset, 'high_quality')
  192. kw_index = IndexBuilder.get_index(dataset, 'economy')
  193. # delete from vector index
  194. if vector_index:
  195. vector_index.delete()
  196. kw_index.delete()
  197. # update document
  198. update_params = {
  199. Document.enabled: False
  200. }
  201. Document.query.filter_by(dataset_id=dataset.id).update(update_params)
  202. db.session.commit()
  203. click.echo(click.style('Cleaned unused dataset {} from db success!'.format(dataset.id),
  204. fg='green'))
  205. except Exception as e:
  206. click.echo(
  207. click.style('clean dataset index error: {} {}'.format(e.__class__.__name__, str(e)),
  208. fg='red'))
  209. end_at = time.perf_counter()
  210. click.echo(click.style('Cleaned unused dataset from db success latency: {}'.format(end_at - start_at), fg='green'))
  211. @click.command('sync-anthropic-hosted-providers', help='Sync anthropic hosted providers.')
  212. def sync_anthropic_hosted_providers():
  213. if not hosted_model_providers.anthropic:
  214. click.echo(click.style('Anthropic hosted provider is not configured.', fg='red'))
  215. return
  216. click.echo(click.style('Start sync anthropic hosted providers.', fg='green'))
  217. count = 0
  218. new_quota_limit = hosted_model_providers.anthropic.quota_limit
  219. page = 1
  220. while True:
  221. try:
  222. providers = db.session.query(Provider).filter(
  223. Provider.provider_name == 'anthropic',
  224. Provider.provider_type == ProviderType.SYSTEM.value,
  225. Provider.quota_type == ProviderQuotaType.TRIAL.value,
  226. Provider.quota_limit != new_quota_limit
  227. ).order_by(Provider.created_at.desc()).paginate(page=page, per_page=100)
  228. except NotFound:
  229. break
  230. page += 1
  231. for provider in providers:
  232. try:
  233. click.echo('Syncing tenant anthropic hosted provider: {}, origin: limit {}, used {}'
  234. .format(provider.tenant_id, provider.quota_limit, provider.quota_used))
  235. original_quota_limit = provider.quota_limit
  236. division = math.ceil(new_quota_limit / 1000)
  237. provider.quota_limit = new_quota_limit if original_quota_limit == 1000 \
  238. else original_quota_limit * division
  239. provider.quota_used = division * provider.quota_used
  240. db.session.commit()
  241. count += 1
  242. except Exception as e:
  243. click.echo(click.style(
  244. 'Sync tenant anthropic hosted provider error: {} {}'.format(e.__class__.__name__, str(e)),
  245. fg='red'))
  246. continue
  247. click.echo(click.style('Congratulations! Synced {} anthropic hosted providers.'.format(count), fg='green'))
  248. def register_commands(app):
  249. app.cli.add_command(reset_password)
  250. app.cli.add_command(reset_email)
  251. app.cli.add_command(generate_invitation_codes)
  252. app.cli.add_command(reset_encrypt_key_pair)
  253. app.cli.add_command(recreate_all_dataset_indexes)
  254. app.cli.add_command(sync_anthropic_hosted_providers)
  255. app.cli.add_command(clean_unused_dataset_indexes)