fengmiguoji commited on
Commit
1862940
·
verified ·
1 Parent(s): 27454f8

Upload api\commands.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. api//commands.py +719 -0
api//commands.py ADDED
@@ -0,0 +1,719 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import json
3
+ import logging
4
+ import secrets
5
+ from typing import Optional
6
+
7
+ import click
8
+ from flask import current_app
9
+ from werkzeug.exceptions import NotFound
10
+
11
+ from configs import dify_config
12
+ from constants.languages import languages
13
+ from core.rag.datasource.vdb.vector_factory import Vector
14
+ from core.rag.datasource.vdb.vector_type import VectorType
15
+ from core.rag.models.document import Document
16
+ from events.app_event import app_was_created
17
+ from extensions.ext_database import db
18
+ from extensions.ext_redis import redis_client
19
+ from libs.helper import email as email_validate
20
+ from libs.password import hash_password, password_pattern, valid_password
21
+ from libs.rsa import generate_key_pair
22
+ from models import Tenant
23
+ from models.dataset import Dataset, DatasetCollectionBinding, DocumentSegment
24
+ from models.dataset import Document as DatasetDocument
25
+ from models.model import Account, App, AppAnnotationSetting, AppMode, Conversation, MessageAnnotation
26
+ from models.provider import Provider, ProviderModel
27
+ from services.account_service import RegisterService, TenantService
28
+ from services.plugin.data_migration import PluginDataMigration
29
+ from services.plugin.plugin_migration import PluginMigration
30
+
31
+
32
+ @click.command("reset-password", help="Reset the account password.")
33
+ @click.option("--email", prompt=True, help="Account email to reset password for")
34
+ @click.option("--new-password", prompt=True, help="New password")
35
+ @click.option("--password-confirm", prompt=True, help="Confirm new password")
36
+ def reset_password(email, new_password, password_confirm):
37
+ """
38
+ Reset password of owner account
39
+ Only available in SELF_HOSTED mode
40
+ """
41
+ if str(new_password).strip() != str(password_confirm).strip():
42
+ click.echo(click.style("Passwords do not match.", fg="red"))
43
+ return
44
+
45
+ account = db.session.query(Account).filter(Account.email == email).one_or_none()
46
+
47
+ if not account:
48
+ click.echo(click.style("Account not found for email: {}".format(email), fg="red"))
49
+ return
50
+
51
+ try:
52
+ valid_password(new_password)
53
+ except:
54
+ click.echo(click.style("Invalid password. Must match {}".format(password_pattern), fg="red"))
55
+ return
56
+
57
+ # generate password salt
58
+ salt = secrets.token_bytes(16)
59
+ base64_salt = base64.b64encode(salt).decode()
60
+
61
+ # encrypt password with salt
62
+ password_hashed = hash_password(new_password, salt)
63
+ base64_password_hashed = base64.b64encode(password_hashed).decode()
64
+ account.password = base64_password_hashed
65
+ account.password_salt = base64_salt
66
+ db.session.commit()
67
+ click.echo(click.style("Password reset successfully.", fg="green"))
68
+
69
+
70
+ @click.command("reset-email", help="Reset the account email.")
71
+ @click.option("--email", prompt=True, help="Current account email")
72
+ @click.option("--new-email", prompt=True, help="New email")
73
+ @click.option("--email-confirm", prompt=True, help="Confirm new email")
74
+ def reset_email(email, new_email, email_confirm):
75
+ """
76
+ Replace account email
77
+ :return:
78
+ """
79
+ if str(new_email).strip() != str(email_confirm).strip():
80
+ click.echo(click.style("New emails do not match.", fg="red"))
81
+ return
82
+
83
+ account = db.session.query(Account).filter(Account.email == email).one_or_none()
84
+
85
+ if not account:
86
+ click.echo(click.style("Account not found for email: {}".format(email), fg="red"))
87
+ return
88
+
89
+ try:
90
+ email_validate(new_email)
91
+ except:
92
+ click.echo(click.style("Invalid email: {}".format(new_email), fg="red"))
93
+ return
94
+
95
+ account.email = new_email
96
+ db.session.commit()
97
+ click.echo(click.style("Email updated successfully.", fg="green"))
98
+
99
+
100
+ @click.command(
101
+ "reset-encrypt-key-pair",
102
+ help="Reset the asymmetric key pair of workspace for encrypt LLM credentials. "
103
+ "After the reset, all LLM credentials will become invalid, "
104
+ "requiring re-entry."
105
+ "Only support SELF_HOSTED mode.",
106
+ )
107
+ @click.confirmation_option(
108
+ prompt=click.style(
109
+ "Are you sure you want to reset encrypt key pair? This operation cannot be rolled back!", fg="red"
110
+ )
111
+ )
112
+ def reset_encrypt_key_pair():
113
+ """
114
+ Reset the encrypted key pair of workspace for encrypt LLM credentials.
115
+ After the reset, all LLM credentials will become invalid, requiring re-entry.
116
+ Only support SELF_HOSTED mode.
117
+ """
118
+ if dify_config.EDITION != "SELF_HOSTED":
119
+ click.echo(click.style("This command is only for SELF_HOSTED installations.", fg="red"))
120
+ return
121
+
122
+ tenants = db.session.query(Tenant).all()
123
+ for tenant in tenants:
124
+ if not tenant:
125
+ click.echo(click.style("No workspaces found. Run /install first.", fg="red"))
126
+ return
127
+
128
+ tenant.encrypt_public_key = generate_key_pair(tenant.id)
129
+
130
+ db.session.query(Provider).filter(Provider.provider_type == "custom", Provider.tenant_id == tenant.id).delete()
131
+ db.session.query(ProviderModel).filter(ProviderModel.tenant_id == tenant.id).delete()
132
+ db.session.commit()
133
+
134
+ click.echo(
135
+ click.style(
136
+ "Congratulations! The asymmetric key pair of workspace {} has been reset.".format(tenant.id),
137
+ fg="green",
138
+ )
139
+ )
140
+
141
+
142
+ @click.command("vdb-migrate", help="Migrate vector db.")
143
+ @click.option("--scope", default="all", prompt=False, help="The scope of vector database to migrate, Default is All.")
144
+ def vdb_migrate(scope: str):
145
+ if scope in {"knowledge", "all"}:
146
+ migrate_knowledge_vector_database()
147
+ if scope in {"annotation", "all"}:
148
+ migrate_annotation_vector_database()
149
+
150
+
151
+ def migrate_annotation_vector_database():
152
+ """
153
+ Migrate annotation datas to target vector database .
154
+ """
155
+ click.echo(click.style("Starting annotation data migration.", fg="green"))
156
+ create_count = 0
157
+ skipped_count = 0
158
+ total_count = 0
159
+ page = 1
160
+ while True:
161
+ try:
162
+ # get apps info
163
+ apps = (
164
+ App.query.filter(App.status == "normal")
165
+ .order_by(App.created_at.desc())
166
+ .paginate(page=page, per_page=50)
167
+ )
168
+ except NotFound:
169
+ break
170
+
171
+ page += 1
172
+ for app in apps:
173
+ total_count = total_count + 1
174
+ click.echo(
175
+ f"Processing the {total_count} app {app.id}. " + f"{create_count} created, {skipped_count} skipped."
176
+ )
177
+ try:
178
+ click.echo("Creating app annotation index: {}".format(app.id))
179
+ app_annotation_setting = (
180
+ db.session.query(AppAnnotationSetting).filter(AppAnnotationSetting.app_id == app.id).first()
181
+ )
182
+
183
+ if not app_annotation_setting:
184
+ skipped_count = skipped_count + 1
185
+ click.echo("App annotation setting disabled: {}".format(app.id))
186
+ continue
187
+ # get dataset_collection_binding info
188
+ dataset_collection_binding = (
189
+ db.session.query(DatasetCollectionBinding)
190
+ .filter(DatasetCollectionBinding.id == app_annotation_setting.collection_binding_id)
191
+ .first()
192
+ )
193
+ if not dataset_collection_binding:
194
+ click.echo("App annotation collection binding not found: {}".format(app.id))
195
+ continue
196
+ annotations = db.session.query(MessageAnnotation).filter(MessageAnnotation.app_id == app.id).all()
197
+ dataset = Dataset(
198
+ id=app.id,
199
+ tenant_id=app.tenant_id,
200
+ indexing_technique="high_quality",
201
+ embedding_model_provider=dataset_collection_binding.provider_name,
202
+ embedding_model=dataset_collection_binding.model_name,
203
+ collection_binding_id=dataset_collection_binding.id,
204
+ )
205
+ documents = []
206
+ if annotations:
207
+ for annotation in annotations:
208
+ document = Document(
209
+ page_content=annotation.question,
210
+ metadata={"annotation_id": annotation.id, "app_id": app.id, "doc_id": annotation.id},
211
+ )
212
+ documents.append(document)
213
+
214
+ vector = Vector(dataset, attributes=["doc_id", "annotation_id", "app_id"])
215
+ click.echo(f"Migrating annotations for app: {app.id}.")
216
+
217
+ try:
218
+ vector.delete()
219
+ click.echo(click.style(f"Deleted vector index for app {app.id}.", fg="green"))
220
+ except Exception as e:
221
+ click.echo(click.style(f"Failed to delete vector index for app {app.id}.", fg="red"))
222
+ raise e
223
+ if documents:
224
+ try:
225
+ click.echo(
226
+ click.style(
227
+ f"Creating vector index with {len(documents)} annotations for app {app.id}.",
228
+ fg="green",
229
+ )
230
+ )
231
+ vector.create(documents)
232
+ click.echo(click.style(f"Created vector index for app {app.id}.", fg="green"))
233
+ except Exception as e:
234
+ click.echo(click.style(f"Failed to created vector index for app {app.id}.", fg="red"))
235
+ raise e
236
+ click.echo(f"Successfully migrated app annotation {app.id}.")
237
+ create_count += 1
238
+ except Exception as e:
239
+ click.echo(
240
+ click.style(
241
+ "Error creating app annotation index: {} {}".format(e.__class__.__name__, str(e)), fg="red"
242
+ )
243
+ )
244
+ continue
245
+
246
+ click.echo(
247
+ click.style(
248
+ f"Migration complete. Created {create_count} app annotation indexes. Skipped {skipped_count} apps.",
249
+ fg="green",
250
+ )
251
+ )
252
+
253
+
254
+ def migrate_knowledge_vector_database():
255
+ """
256
+ Migrate vector database datas to target vector database .
257
+ """
258
+ click.echo(click.style("Starting vector database migration.", fg="green"))
259
+ create_count = 0
260
+ skipped_count = 0
261
+ total_count = 0
262
+ vector_type = dify_config.VECTOR_STORE
263
+ upper_collection_vector_types = {
264
+ VectorType.MILVUS,
265
+ VectorType.PGVECTOR,
266
+ VectorType.RELYT,
267
+ VectorType.WEAVIATE,
268
+ VectorType.ORACLE,
269
+ VectorType.ELASTICSEARCH,
270
+ }
271
+ lower_collection_vector_types = {
272
+ VectorType.ANALYTICDB,
273
+ VectorType.CHROMA,
274
+ VectorType.MYSCALE,
275
+ VectorType.PGVECTO_RS,
276
+ VectorType.TIDB_VECTOR,
277
+ VectorType.OPENSEARCH,
278
+ VectorType.TENCENT,
279
+ VectorType.BAIDU,
280
+ VectorType.VIKINGDB,
281
+ VectorType.UPSTASH,
282
+ VectorType.COUCHBASE,
283
+ VectorType.OCEANBASE,
284
+ }
285
+ page = 1
286
+ while True:
287
+ try:
288
+ datasets = (
289
+ Dataset.query.filter(Dataset.indexing_technique == "high_quality")
290
+ .order_by(Dataset.created_at.desc())
291
+ .paginate(page=page, per_page=50)
292
+ )
293
+ except NotFound:
294
+ break
295
+
296
+ page += 1
297
+ for dataset in datasets:
298
+ total_count = total_count + 1
299
+ click.echo(
300
+ f"Processing the {total_count} dataset {dataset.id}. {create_count} created, {skipped_count} skipped."
301
+ )
302
+ try:
303
+ click.echo("Creating dataset vector database index: {}".format(dataset.id))
304
+ if dataset.index_struct_dict:
305
+ if dataset.index_struct_dict["type"] == vector_type:
306
+ skipped_count = skipped_count + 1
307
+ continue
308
+ collection_name = ""
309
+ dataset_id = dataset.id
310
+ if vector_type in upper_collection_vector_types:
311
+ collection_name = Dataset.gen_collection_name_by_id(dataset_id)
312
+ elif vector_type == VectorType.QDRANT:
313
+ if dataset.collection_binding_id:
314
+ dataset_collection_binding = (
315
+ db.session.query(DatasetCollectionBinding)
316
+ .filter(DatasetCollectionBinding.id == dataset.collection_binding_id)
317
+ .one_or_none()
318
+ )
319
+ if dataset_collection_binding:
320
+ collection_name = dataset_collection_binding.collection_name
321
+ else:
322
+ raise ValueError("Dataset Collection Binding not found")
323
+ else:
324
+ collection_name = Dataset.gen_collection_name_by_id(dataset_id)
325
+
326
+ elif vector_type in lower_collection_vector_types:
327
+ collection_name = Dataset.gen_collection_name_by_id(dataset_id).lower()
328
+ else:
329
+ raise ValueError(f"Vector store {vector_type} is not supported.")
330
+
331
+ index_struct_dict = {"type": vector_type, "vector_store": {"class_prefix": collection_name}}
332
+ dataset.index_struct = json.dumps(index_struct_dict)
333
+ vector = Vector(dataset)
334
+ click.echo(f"Migrating dataset {dataset.id}.")
335
+
336
+ try:
337
+ vector.delete()
338
+ click.echo(
339
+ click.style(f"Deleted vector index {collection_name} for dataset {dataset.id}.", fg="green")
340
+ )
341
+ except Exception as e:
342
+ click.echo(
343
+ click.style(
344
+ f"Failed to delete vector index {collection_name} for dataset {dataset.id}.", fg="red"
345
+ )
346
+ )
347
+ raise e
348
+
349
+ dataset_documents = (
350
+ db.session.query(DatasetDocument)
351
+ .filter(
352
+ DatasetDocument.dataset_id == dataset.id,
353
+ DatasetDocument.indexing_status == "completed",
354
+ DatasetDocument.enabled == True,
355
+ DatasetDocument.archived == False,
356
+ )
357
+ .all()
358
+ )
359
+
360
+ documents = []
361
+ segments_count = 0
362
+ for dataset_document in dataset_documents:
363
+ segments = (
364
+ db.session.query(DocumentSegment)
365
+ .filter(
366
+ DocumentSegment.document_id == dataset_document.id,
367
+ DocumentSegment.status == "completed",
368
+ DocumentSegment.enabled == True,
369
+ )
370
+ .all()
371
+ )
372
+
373
+ for segment in segments:
374
+ document = Document(
375
+ page_content=segment.content,
376
+ metadata={
377
+ "doc_id": segment.index_node_id,
378
+ "doc_hash": segment.index_node_hash,
379
+ "document_id": segment.document_id,
380
+ "dataset_id": segment.dataset_id,
381
+ },
382
+ )
383
+
384
+ documents.append(document)
385
+ segments_count = segments_count + 1
386
+
387
+ if documents:
388
+ try:
389
+ click.echo(
390
+ click.style(
391
+ f"Creating vector index with {len(documents)} documents of {segments_count}"
392
+ f" segments for dataset {dataset.id}.",
393
+ fg="green",
394
+ )
395
+ )
396
+ vector.create(documents)
397
+ click.echo(click.style(f"Created vector index for dataset {dataset.id}.", fg="green"))
398
+ except Exception as e:
399
+ click.echo(click.style(f"Failed to created vector index for dataset {dataset.id}.", fg="red"))
400
+ raise e
401
+ db.session.add(dataset)
402
+ db.session.commit()
403
+ click.echo(f"Successfully migrated dataset {dataset.id}.")
404
+ create_count += 1
405
+ except Exception as e:
406
+ db.session.rollback()
407
+ click.echo(
408
+ click.style("Error creating dataset index: {} {}".format(e.__class__.__name__, str(e)), fg="red")
409
+ )
410
+ continue
411
+
412
+ click.echo(
413
+ click.style(
414
+ f"Migration complete. Created {create_count} dataset indexes. Skipped {skipped_count} datasets.", fg="green"
415
+ )
416
+ )
417
+
418
+
419
+ @click.command("convert-to-agent-apps", help="Convert Agent Assistant to Agent App.")
420
+ def convert_to_agent_apps():
421
+ """
422
+ Convert Agent Assistant to Agent App.
423
+ """
424
+ click.echo(click.style("Starting convert to agent apps.", fg="green"))
425
+
426
+ proceeded_app_ids = []
427
+
428
+ while True:
429
+ # fetch first 1000 apps
430
+ sql_query = """SELECT a.id AS id FROM apps a
431
+ INNER JOIN app_model_configs am ON a.app_model_config_id=am.id
432
+ WHERE a.mode = 'chat'
433
+ AND am.agent_mode is not null
434
+ AND (
435
+ am.agent_mode like '%"strategy": "function_call"%'
436
+ OR am.agent_mode like '%"strategy": "react"%'
437
+ )
438
+ AND (
439
+ am.agent_mode like '{"enabled": true%'
440
+ OR am.agent_mode like '{"max_iteration": %'
441
+ ) ORDER BY a.created_at DESC LIMIT 1000
442
+ """
443
+
444
+ with db.engine.begin() as conn:
445
+ rs = conn.execute(db.text(sql_query))
446
+
447
+ apps = []
448
+ for i in rs:
449
+ app_id = str(i.id)
450
+ if app_id not in proceeded_app_ids:
451
+ proceeded_app_ids.append(app_id)
452
+ app = db.session.query(App).filter(App.id == app_id).first()
453
+ if app is not None:
454
+ apps.append(app)
455
+
456
+ if len(apps) == 0:
457
+ break
458
+
459
+ for app in apps:
460
+ click.echo("Converting app: {}".format(app.id))
461
+
462
+ try:
463
+ app.mode = AppMode.AGENT_CHAT.value
464
+ db.session.commit()
465
+
466
+ # update conversation mode to agent
467
+ db.session.query(Conversation).filter(Conversation.app_id == app.id).update(
468
+ {Conversation.mode: AppMode.AGENT_CHAT.value}
469
+ )
470
+
471
+ db.session.commit()
472
+ click.echo(click.style("Converted app: {}".format(app.id), fg="green"))
473
+ except Exception as e:
474
+ click.echo(click.style("Convert app error: {} {}".format(e.__class__.__name__, str(e)), fg="red"))
475
+
476
+ click.echo(click.style("Conversion complete. Converted {} agent apps.".format(len(proceeded_app_ids)), fg="green"))
477
+
478
+
479
+ @click.command("add-qdrant-doc-id-index", help="Add Qdrant doc_id index.")
480
+ @click.option("--field", default="metadata.doc_id", prompt=False, help="Index field , default is metadata.doc_id.")
481
+ def add_qdrant_doc_id_index(field: str):
482
+ click.echo(click.style("Starting Qdrant doc_id index creation.", fg="green"))
483
+ vector_type = dify_config.VECTOR_STORE
484
+ if vector_type != "qdrant":
485
+ click.echo(click.style("This command only supports Qdrant vector store.", fg="red"))
486
+ return
487
+ create_count = 0
488
+
489
+ try:
490
+ bindings = db.session.query(DatasetCollectionBinding).all()
491
+ if not bindings:
492
+ click.echo(click.style("No dataset collection bindings found.", fg="red"))
493
+ return
494
+ import qdrant_client
495
+ from qdrant_client.http.exceptions import UnexpectedResponse
496
+ from qdrant_client.http.models import PayloadSchemaType
497
+
498
+ from core.rag.datasource.vdb.qdrant.qdrant_vector import QdrantConfig
499
+
500
+ for binding in bindings:
501
+ if dify_config.QDRANT_URL is None:
502
+ raise ValueError("Qdrant URL is required.")
503
+ qdrant_config = QdrantConfig(
504
+ endpoint=dify_config.QDRANT_URL,
505
+ api_key=dify_config.QDRANT_API_KEY,
506
+ root_path=current_app.root_path,
507
+ timeout=dify_config.QDRANT_CLIENT_TIMEOUT,
508
+ grpc_port=dify_config.QDRANT_GRPC_PORT,
509
+ prefer_grpc=dify_config.QDRANT_GRPC_ENABLED,
510
+ )
511
+ try:
512
+ client = qdrant_client.QdrantClient(**qdrant_config.to_qdrant_params())
513
+ # create payload index
514
+ client.create_payload_index(binding.collection_name, field, field_schema=PayloadSchemaType.KEYWORD)
515
+ create_count += 1
516
+ except UnexpectedResponse as e:
517
+ # Collection does not exist, so return
518
+ if e.status_code == 404:
519
+ click.echo(click.style(f"Collection not found: {binding.collection_name}.", fg="red"))
520
+ continue
521
+ # Some other error occurred, so re-raise the exception
522
+ else:
523
+ click.echo(
524
+ click.style(
525
+ f"Failed to create Qdrant index for collection: {binding.collection_name}.", fg="red"
526
+ )
527
+ )
528
+
529
+ except Exception:
530
+ click.echo(click.style("Failed to create Qdrant client.", fg="red"))
531
+
532
+ click.echo(click.style(f"Index creation complete. Created {create_count} collection indexes.", fg="green"))
533
+
534
+
535
+ @click.command("create-tenant", help="Create account and tenant.")
536
+ @click.option("--email", prompt=True, help="Tenant account email.")
537
+ @click.option("--name", prompt=True, help="Workspace name.")
538
+ @click.option("--language", prompt=True, help="Account language, default: en-US.")
539
+ def create_tenant(email: str, language: Optional[str] = None, name: Optional[str] = None):
540
+ """
541
+ Create tenant account
542
+ """
543
+ if not email:
544
+ click.echo(click.style("Email is required.", fg="red"))
545
+ return
546
+
547
+ # Create account
548
+ email = email.strip()
549
+
550
+ if "@" not in email:
551
+ click.echo(click.style("Invalid email address.", fg="red"))
552
+ return
553
+
554
+ account_name = email.split("@")[0]
555
+
556
+ if language not in languages:
557
+ language = "en-US"
558
+
559
+ # Validates name encoding for non-Latin characters.
560
+ name = name.strip().encode("utf-8").decode("utf-8") if name else None
561
+
562
+ # generate random password
563
+ new_password = secrets.token_urlsafe(16)
564
+
565
+ # register account
566
+ account = RegisterService.register(
567
+ email=email,
568
+ name=account_name,
569
+ password=new_password,
570
+ language=language,
571
+ create_workspace_required=False,
572
+ )
573
+ TenantService.create_owner_tenant_if_not_exist(account, name)
574
+
575
+ click.echo(
576
+ click.style(
577
+ "Account and tenant created.\nAccount: {}\nPassword: {}".format(email, new_password),
578
+ fg="green",
579
+ )
580
+ )
581
+
582
+
583
+ @click.command("upgrade-db", help="Upgrade the database")
584
+ def upgrade_db():
585
+ click.echo("Preparing database migration...")
586
+ lock = redis_client.lock(name="db_upgrade_lock", timeout=60)
587
+ if lock.acquire(blocking=False):
588
+ try:
589
+ click.echo(click.style("Starting database migration.", fg="green"))
590
+
591
+ # run db migration
592
+ import flask_migrate # type: ignore
593
+
594
+ flask_migrate.upgrade()
595
+
596
+ click.echo(click.style("Database migration successful!", fg="green"))
597
+
598
+ except Exception:
599
+ logging.exception("Failed to execute database migration")
600
+ finally:
601
+ lock.release()
602
+ else:
603
+ click.echo("Database migration skipped")
604
+
605
+
606
+ @click.command("fix-app-site-missing", help="Fix app related site missing issue.")
607
+ def fix_app_site_missing():
608
+ """
609
+ Fix app related site missing issue.
610
+ """
611
+ click.echo(click.style("Starting fix for missing app-related sites.", fg="green"))
612
+
613
+ failed_app_ids = []
614
+ while True:
615
+ sql = """select apps.id as id from apps left join sites on sites.app_id=apps.id
616
+ where sites.id is null limit 1000"""
617
+ with db.engine.begin() as conn:
618
+ rs = conn.execute(db.text(sql))
619
+
620
+ processed_count = 0
621
+ for i in rs:
622
+ processed_count += 1
623
+ app_id = str(i.id)
624
+
625
+ if app_id in failed_app_ids:
626
+ continue
627
+
628
+ try:
629
+ app = db.session.query(App).filter(App.id == app_id).first()
630
+ if not app:
631
+ print(f"App {app_id} not found")
632
+ continue
633
+
634
+ tenant = app.tenant
635
+ if tenant:
636
+ accounts = tenant.get_accounts()
637
+ if not accounts:
638
+ print("Fix failed for app {}".format(app.id))
639
+ continue
640
+
641
+ account = accounts[0]
642
+ print("Fixing missing site for app {}".format(app.id))
643
+ app_was_created.send(app, account=account)
644
+ except Exception:
645
+ failed_app_ids.append(app_id)
646
+ click.echo(click.style("Failed to fix missing site for app {}".format(app_id), fg="red"))
647
+ logging.exception(f"Failed to fix app related site missing issue, app_id: {app_id}")
648
+ continue
649
+
650
+ if not processed_count:
651
+ break
652
+
653
+ click.echo(click.style("Fix for missing app-related sites completed successfully!", fg="green"))
654
+
655
+
656
+ @click.command("migrate-data-for-plugin", help="Migrate data for plugin.")
657
+ def migrate_data_for_plugin():
658
+ """
659
+ Migrate data for plugin.
660
+ """
661
+ click.echo(click.style("Starting migrate data for plugin.", fg="white"))
662
+
663
+ PluginDataMigration.migrate()
664
+
665
+ click.echo(click.style("Migrate data for plugin completed.", fg="green"))
666
+
667
+
668
+ @click.command("extract-plugins", help="Extract plugins.")
669
+ @click.option("--output_file", prompt=True, help="The file to store the extracted plugins.", default="plugins.jsonl")
670
+ @click.option("--workers", prompt=True, help="The number of workers to extract plugins.", default=10)
671
+ def extract_plugins(output_file: str, workers: int):
672
+ """
673
+ Extract plugins.
674
+ """
675
+ click.echo(click.style("Starting extract plugins.", fg="white"))
676
+
677
+ PluginMigration.extract_plugins(output_file, workers)
678
+
679
+ click.echo(click.style("Extract plugins completed.", fg="green"))
680
+
681
+
682
+ @click.command("extract-unique-identifiers", help="Extract unique identifiers.")
683
+ @click.option(
684
+ "--output_file",
685
+ prompt=True,
686
+ help="The file to store the extracted unique identifiers.",
687
+ default="unique_identifiers.json",
688
+ )
689
+ @click.option(
690
+ "--input_file", prompt=True, help="The file to store the extracted unique identifiers.", default="plugins.jsonl"
691
+ )
692
+ def extract_unique_plugins(output_file: str, input_file: str):
693
+ """
694
+ Extract unique plugins.
695
+ """
696
+ click.echo(click.style("Starting extract unique plugins.", fg="white"))
697
+
698
+ PluginMigration.extract_unique_plugins_to_file(input_file, output_file)
699
+
700
+ click.echo(click.style("Extract unique plugins completed.", fg="green"))
701
+
702
+
703
+ @click.command("install-plugins", help="Install plugins.")
704
+ @click.option(
705
+ "--input_file", prompt=True, help="The file to store the extracted unique identifiers.", default="plugins.jsonl"
706
+ )
707
+ @click.option(
708
+ "--output_file", prompt=True, help="The file to store the installed plugins.", default="installed_plugins.jsonl"
709
+ )
710
+ @click.option("--workers", prompt=True, help="The number of workers to install plugins.", default=100)
711
+ def install_plugins(input_file: str, output_file: str, workers: int):
712
+ """
713
+ Install plugins.
714
+ """
715
+ click.echo(click.style("Starting install plugins.", fg="white"))
716
+
717
+ PluginMigration.install_plugins(input_file, output_file, workers)
718
+
719
+ click.echo(click.style("Install plugins completed.", fg="green"))