feat: update command for add admin cmd

pull/21891/head
ytqh 1 year ago
parent acced21108
commit bf9ef068a1

@ -26,9 +26,22 @@ from libs.password import hash_password, password_pattern, valid_password
from libs.rsa import generate_key_pair from libs.rsa import generate_key_pair
from models import Account, Tenant, TenantAccountJoin from models import Account, Tenant, TenantAccountJoin
from models.account import TenantAccountJoinRole from models.account import TenantAccountJoinRole
from models.dataset import Dataset, DatasetCollectionBinding, DatasetMetadata, DatasetMetadataBinding, DocumentSegment from models.dataset import (
Dataset,
DatasetCollectionBinding,
DatasetMetadata,
DatasetMetadataBinding,
DocumentSegment,
)
from models.dataset import Document as DatasetDocument from models.dataset import Document as DatasetDocument
from models.model import App, AppAnnotationSetting, AppMode, Conversation, MessageAnnotation, UploadFile from models.model import (
App,
AppAnnotationSetting,
AppMode,
Conversation,
MessageAnnotation,
UploadFile,
)
from models.provider import Provider, ProviderModel from models.provider import Provider, ProviderModel
from services.account_service import RegisterService, TenantService from services.account_service import RegisterService, TenantService
from services.clear_free_plan_tenant_expired_logs import ClearFreePlanTenantExpiredLogs from services.clear_free_plan_tenant_expired_logs import ClearFreePlanTenantExpiredLogs
@ -52,13 +65,19 @@ def reset_password(email, new_password, password_confirm):
account = db.session.query(Account).filter(Account.email == email).one_or_none() account = db.session.query(Account).filter(Account.email == email).one_or_none()
if not account: if not account:
click.echo(click.style("Account not found for email: {}".format(email), fg="red")) click.echo(
click.style("Account not found for email: {}".format(email), fg="red")
)
return return
try: try:
valid_password(new_password) valid_password(new_password)
except: except:
click.echo(click.style("Invalid password. Must match {}".format(password_pattern), fg="red")) click.echo(
click.style(
"Invalid password. Must match {}".format(password_pattern), fg="red"
)
)
return return
# generate password salt # generate password salt
@ -90,7 +109,9 @@ def reset_email(email, new_email, email_confirm):
account = db.session.query(Account).filter(Account.email == email).one_or_none() account = db.session.query(Account).filter(Account.email == email).one_or_none()
if not account: if not account:
click.echo(click.style("Account not found for email: {}".format(email), fg="red")) click.echo(
click.style("Account not found for email: {}".format(email), fg="red")
)
return return
try: try:
@ -124,24 +145,34 @@ def reset_encrypt_key_pair():
Only support SELF_HOSTED mode. Only support SELF_HOSTED mode.
""" """
if dify_config.EDITION != "SELF_HOSTED": if dify_config.EDITION != "SELF_HOSTED":
click.echo(click.style("This command is only for SELF_HOSTED installations.", fg="red")) click.echo(
click.style("This command is only for SELF_HOSTED installations.", fg="red")
)
return return
tenants = db.session.query(Tenant).all() tenants = db.session.query(Tenant).all()
for tenant in tenants: for tenant in tenants:
if not tenant: if not tenant:
click.echo(click.style("No workspaces found. Run /install first.", fg="red")) click.echo(
click.style("No workspaces found. Run /install first.", fg="red")
)
return return
tenant.encrypt_public_key = generate_key_pair(tenant.id) tenant.encrypt_public_key = generate_key_pair(tenant.id)
db.session.query(Provider).filter(Provider.provider_type == "custom", Provider.tenant_id == tenant.id).delete() db.session.query(Provider).filter(
db.session.query(ProviderModel).filter(ProviderModel.tenant_id == tenant.id).delete() Provider.provider_type == "custom", Provider.tenant_id == tenant.id
).delete()
db.session.query(ProviderModel).filter(
ProviderModel.tenant_id == tenant.id
).delete()
db.session.commit() db.session.commit()
click.echo( click.echo(
click.style( click.style(
"Congratulations! The asymmetric key pair of workspace {} has been reset.".format(tenant.id), "Congratulations! The asymmetric key pair of workspace {} has been reset.".format(
tenant.id
),
fg="green", fg="green",
) )
) )
@ -191,12 +222,15 @@ def migrate_annotation_vector_database():
for app in apps: for app in apps:
total_count = total_count + 1 total_count = total_count + 1
click.echo( click.echo(
f"Processing the {total_count} app {app.id}. " + f"{create_count} created, {skipped_count} skipped." f"Processing the {total_count} app {app.id}. "
+ f"{create_count} created, {skipped_count} skipped."
) )
try: try:
click.echo("Creating app annotation index: {}".format(app.id)) click.echo("Creating app annotation index: {}".format(app.id))
app_annotation_setting = ( app_annotation_setting = (
db.session.query(AppAnnotationSetting).filter(AppAnnotationSetting.app_id == app.id).first() db.session.query(AppAnnotationSetting)
.filter(AppAnnotationSetting.app_id == app.id)
.first()
) )
if not app_annotation_setting: if not app_annotation_setting:
@ -206,13 +240,22 @@ def migrate_annotation_vector_database():
# get dataset_collection_binding info # get dataset_collection_binding info
dataset_collection_binding = ( dataset_collection_binding = (
db.session.query(DatasetCollectionBinding) db.session.query(DatasetCollectionBinding)
.filter(DatasetCollectionBinding.id == app_annotation_setting.collection_binding_id) .filter(
DatasetCollectionBinding.id
== app_annotation_setting.collection_binding_id
)
.first() .first()
) )
if not dataset_collection_binding: if not dataset_collection_binding:
click.echo("App annotation collection binding not found: {}".format(app.id)) click.echo(
"App annotation collection binding not found: {}".format(app.id)
)
continue continue
annotations = db.session.query(MessageAnnotation).filter(MessageAnnotation.app_id == app.id).all() annotations = (
db.session.query(MessageAnnotation)
.filter(MessageAnnotation.app_id == app.id)
.all()
)
dataset = Dataset( dataset = Dataset(
id=app.id, id=app.id,
tenant_id=app.tenant_id, tenant_id=app.tenant_id,
@ -234,14 +277,24 @@ def migrate_annotation_vector_database():
) )
documents.append(document) documents.append(document)
vector = Vector(dataset, attributes=["doc_id", "annotation_id", "app_id"]) vector = Vector(
dataset, attributes=["doc_id", "annotation_id", "app_id"]
)
click.echo(f"Migrating annotations for app: {app.id}.") click.echo(f"Migrating annotations for app: {app.id}.")
try: try:
vector.delete() vector.delete()
click.echo(click.style(f"Deleted vector index for app {app.id}.", fg="green")) click.echo(
click.style(
f"Deleted vector index for app {app.id}.", fg="green"
)
)
except Exception as e: except Exception as e:
click.echo(click.style(f"Failed to delete vector index for app {app.id}.", fg="red")) click.echo(
click.style(
f"Failed to delete vector index for app {app.id}.", fg="red"
)
)
raise e raise e
if documents: if documents:
try: try:
@ -252,7 +305,11 @@ def migrate_annotation_vector_database():
) )
) )
vector.create(documents) vector.create(documents)
click.echo(click.style(f"Created vector index for app {app.id}.", fg="green")) click.echo(
click.style(
f"Created vector index for app {app.id}.", fg="green"
)
)
except Exception as e: except Exception as e:
click.echo( click.echo(
click.style( click.style(
@ -266,7 +323,9 @@ def migrate_annotation_vector_database():
except Exception as e: except Exception as e:
click.echo( click.echo(
click.style( click.style(
"Error creating app annotation index: {} {}".format(e.__class__.__name__, str(e)), "Error creating app annotation index: {} {}".format(
e.__class__.__name__, str(e)
),
fg="red", fg="red",
) )
) )
@ -332,7 +391,9 @@ def migrate_knowledge_vector_database():
f"Processing the {total_count} dataset {dataset.id}. {create_count} created, {skipped_count} skipped." f"Processing the {total_count} dataset {dataset.id}. {create_count} created, {skipped_count} skipped."
) )
try: try:
click.echo("Creating dataset vector database index: {}".format(dataset.id)) click.echo(
"Creating dataset vector database index: {}".format(dataset.id)
)
if dataset.index_struct_dict: if dataset.index_struct_dict:
if dataset.index_struct_dict["type"] == vector_type: if dataset.index_struct_dict["type"] == vector_type:
skipped_count = skipped_count + 1 skipped_count = skipped_count + 1
@ -345,7 +406,10 @@ def migrate_knowledge_vector_database():
if dataset.collection_binding_id: if dataset.collection_binding_id:
dataset_collection_binding = ( dataset_collection_binding = (
db.session.query(DatasetCollectionBinding) db.session.query(DatasetCollectionBinding)
.filter(DatasetCollectionBinding.id == dataset.collection_binding_id) .filter(
DatasetCollectionBinding.id
== dataset.collection_binding_id
)
.one_or_none() .one_or_none()
) )
if dataset_collection_binding: if dataset_collection_binding:
@ -356,7 +420,9 @@ def migrate_knowledge_vector_database():
collection_name = Dataset.gen_collection_name_by_id(dataset_id) collection_name = Dataset.gen_collection_name_by_id(dataset_id)
elif vector_type in lower_collection_vector_types: elif vector_type in lower_collection_vector_types:
collection_name = Dataset.gen_collection_name_by_id(dataset_id).lower() collection_name = Dataset.gen_collection_name_by_id(
dataset_id
).lower()
else: else:
raise ValueError(f"Vector store {vector_type} is not supported.") raise ValueError(f"Vector store {vector_type} is not supported.")
@ -455,7 +521,9 @@ def migrate_knowledge_vector_database():
db.session.rollback() db.session.rollback()
click.echo( click.echo(
click.style( click.style(
"Error creating dataset index: {} {}".format(e.__class__.__name__, str(e)), "Error creating dataset index: {} {}".format(
e.__class__.__name__, str(e)
),
fg="red", fg="red",
) )
) )
@ -517,9 +585,9 @@ def convert_to_agent_apps():
db.session.commit() db.session.commit()
# update conversation mode to agent # update conversation mode to agent
db.session.query(Conversation).filter(Conversation.app_id == app.id).update( db.session.query(Conversation).filter(
{Conversation.mode: AppMode.AGENT_CHAT.value} Conversation.app_id == app.id
) ).update({Conversation.mode: AppMode.AGENT_CHAT.value})
db.session.commit() db.session.commit()
click.echo(click.style("Converted app: {}".format(app.id), fg="green")) click.echo(click.style("Converted app: {}".format(app.id), fg="green"))
@ -533,7 +601,9 @@ def convert_to_agent_apps():
click.echo( click.echo(
click.style( click.style(
"Conversion complete. Converted {} agent apps.".format(len(proceeded_app_ids)), "Conversion complete. Converted {} agent apps.".format(
len(proceeded_app_ids)
),
fg="green", fg="green",
) )
) )
@ -666,11 +736,15 @@ def old_metadata_migration():
) )
db.session.add(dataset_metadata_binding) db.session.add(dataset_metadata_binding)
else: else:
dataset_metadata_binding = DatasetMetadataBinding.query.filter( dataset_metadata_binding = (
DatasetMetadataBinding.dataset_id == document.dataset_id, DatasetMetadataBinding.query.filter(
DatasetMetadataBinding.dataset_id
== document.dataset_id,
DatasetMetadataBinding.document_id == document.id, DatasetMetadataBinding.document_id == document.id,
DatasetMetadataBinding.metadata_id == dataset_metadata.id, DatasetMetadataBinding.metadata_id
== dataset_metadata.id,
).first() ).first()
)
if not dataset_metadata_binding: if not dataset_metadata_binding:
dataset_metadata_binding = DatasetMetadataBinding( dataset_metadata_binding = DatasetMetadataBinding(
tenant_id=document.tenant_id, tenant_id=document.tenant_id,
@ -689,7 +763,9 @@ def old_metadata_migration():
@click.option("--email", prompt=True, help="Tenant account email.") @click.option("--email", prompt=True, help="Tenant account email.")
@click.option("--name", prompt=True, help="Workspace name.") @click.option("--name", prompt=True, help="Workspace name.")
@click.option("--language", prompt=True, help="Account language, default: en-US.") @click.option("--language", prompt=True, help="Account language, default: en-US.")
def create_tenant(email: str, language: Optional[str] = None, name: Optional[str] = None): def create_tenant(
email: str, language: Optional[str] = None, name: Optional[str] = None
):
""" """
Create tenant account Create tenant account
""" """
@ -727,7 +803,9 @@ def create_tenant(email: str, language: Optional[str] = None, name: Optional[str
click.echo( click.echo(
click.style( click.style(
"Account and tenant created.\nAccount: {}\nPassword: {}".format(email, new_password), "Account and tenant created.\nAccount: {}\nPassword: {}".format(
email, new_password
),
fg="green", fg="green",
) )
) )
@ -802,64 +880,115 @@ where sites.id is null limit 1000"""
fg="red", fg="red",
) )
) )
logging.exception(f"Failed to fix app related site missing issue, app_id: {app_id}") logging.exception(
f"Failed to fix app related site missing issue, app_id: {app_id}"
)
continue continue
if not processed_count: if not processed_count:
break break
click.echo(click.style("Fix for missing app-related sites completed successfully!", fg="green")) click.echo(
click.style(
"Fix for missing app-related sites completed successfully!", fg="green"
)
)
@click.command( @click.command(
"create-admin-with-phone", "create-admin-account",
help="Create or update an admin account for an organization with a phone number.", help="Create or update an admin account for an organization with a phone number or email.",
) )
@click.option("--name", prompt=True, help="Admin account name") @click.option("--name", prompt=True, help="Admin account name")
@click.option("--phone", prompt=True, help="Admin account phone number") @click.option("--login-id", prompt=True, help="Admin account phone number or email")
@click.option(
"--login-id-type",
prompt=True,
type=click.Choice(["phone", "email"]),
help="Type of login ID (phone or email)",
)
@click.option("--organization-id", required=True, help="Organization ID") @click.option("--organization-id", required=True, help="Organization ID")
def create_admin_with_phone(name: str, phone: str, organization_id: str): def create_admin_account(
name: str, login_id: str, login_id_type: str, organization_id: str
):
""" """
Create or update an admin account with a phone number for a specific organization. Create or update an admin account with a phone number or email for a specific organization.
This command will create a new account if the phone doesn't exist, This command will create a new account if the login ID doesn't exist,
or update an existing account with the specified admin role. or update an existing account with the specified admin role.
""" """
try: try:
# Check if organization exists # Check if organization exists
from models.organization import Organization, OrganizationMember, OrganizationRole from models.organization import (
Organization,
OrganizationMember,
OrganizationRole,
)
organization = db.session.query(Organization).filter(Organization.id == organization_id).first() organization = (
db.session.query(Organization)
.filter(Organization.id == organization_id)
.first()
)
if not organization: if not organization:
click.echo(click.style(f"Organization with ID {organization_id} not found.", fg="red")) click.echo(
click.style(
f"Organization with ID {organization_id} not found.", fg="red"
)
)
return return
# Get tenant from organization # Get tenant from organization
tenant = db.session.query(Tenant).filter(Tenant.id == organization.tenant_id).first() tenant = (
db.session.query(Tenant).filter(Tenant.id == organization.tenant_id).first()
)
if not tenant: if not tenant:
click.echo(click.style(f"Tenant for organization {organization_id} not found.", fg="red")) click.echo(
click.style(
f"Tenant for organization {organization_id} not found.", fg="red"
)
)
return return
# Check if account exists with this phone number # Check if account exists with this login ID
account = db.session.query(Account).filter(Account.phone == phone).first() account = None
if login_id_type == "phone":
account = (
db.session.query(Account).filter(Account.phone == login_id).first()
)
else: # email
account = (
db.session.query(Account).filter(Account.email == login_id).first()
)
if account: if account:
click.echo(f"Account with phone {phone} already exists. Updating account...") click.echo(
f"Account with {login_id_type} {login_id} already exists. Updating account..."
)
# Update account # Update account
account.name = name account.name = name
account.current_organization_id = organization_id account.current_organization_id = organization_id
db.session.commit() db.session.commit()
else: else:
click.echo(f"Creating new account with phone {phone}...") click.echo(f"Creating new account with {login_id_type} {login_id}...")
# Create new account with phone # Create new account
if login_id_type == "phone":
account = Account(
name=name,
email=f"{login_id}@qingsu.chat", # Use phone as part of email
phone=login_id,
interface_language=languages[0],
status="active",
current_organization_id=organization_id,
)
else: # email
account = Account( account = Account(
name=name, name=name,
email=f"{phone}@qingsu.chat", # Use organization code in email email=login_id,
phone=phone,
interface_language=languages[0], interface_language=languages[0],
status="active", status="active",
current_organization_id=organization_id, # Set current organization current_organization_id=organization_id,
) )
db.session.add(account) db.session.add(account)
@ -898,7 +1027,9 @@ def create_admin_with_phone(name: str, phone: str, organization_id: str):
if org_member: if org_member:
# Update role to admin # Update role to admin
org_member.role = OrganizationRole.ADMIN org_member.role = OrganizationRole.ADMIN
click.echo(f"Updated account role to {OrganizationRole.ADMIN} in organization {organization.name}") click.echo(
f"Updated account role to {OrganizationRole.ADMIN} in organization {organization.name}"
)
else: else:
# Add account to organization with admin role # Add account to organization with admin role
org_member = OrganizationMember( org_member = OrganizationMember(
@ -909,18 +1040,20 @@ def create_admin_with_phone(name: str, phone: str, organization_id: str):
created_by=account.id, created_by=account.id,
) )
db.session.add(org_member) db.session.add(org_member)
click.echo(f"Added account to organization {organization.name} with role {OrganizationRole.ADMIN}") click.echo(
f"Added account to organization {organization.name} with role {OrganizationRole.ADMIN}"
)
db.session.commit() db.session.commit()
click.echo( click.echo(
click.style( click.style(
f"Successfully {'updated' if account else 'created'} admin account with phone number.", f"Successfully {'updated' if account else 'created'} admin account with {login_id_type}.",
fg="green", fg="green",
) )
) )
click.echo(f"Name: {name}") click.echo(f"Name: {name}")
click.echo(f"Phone: {phone}") click.echo(f"{login_id_type.capitalize()}: {login_id}")
click.echo(f"Organization: {organization.name} (ID: {organization.id})") click.echo(f"Organization: {organization.name} (ID: {organization.id})")
except Exception as e: except Exception as e:
@ -928,8 +1061,12 @@ def create_admin_with_phone(name: str, phone: str, organization_id: str):
click.echo(click.style(f"Error: {str(e)}", fg="red")) click.echo(click.style(f"Error: {str(e)}", fg="red"))
@click.command("create-organization", help="Create a new organization for multi-school support.") @click.command(
@click.option("--tenant-id", required=True, help="ID of the tenant that owns this organization") "create-organization", help="Create a new organization for multi-school support."
)
@click.option(
"--tenant-id", required=True, help="ID of the tenant that owns this organization"
)
@click.option("--name", required=True, help="Name of the organization") @click.option("--name", required=True, help="Name of the organization")
@click.option("--code", required=True, help="Unique code for the organization") @click.option("--code", required=True, help="Unique code for the organization")
@click.option( @click.option(
@ -940,15 +1077,21 @@ def create_admin_with_phone(name: str, phone: str, organization_id: str):
help="Type of organization", help="Type of organization",
) )
@click.option("--description", default="", help="Description of the organization") @click.option("--description", default="", help="Description of the organization")
@click.option("--email-domains", default="", help="Comma-separated list of allowed email domains") @click.option(
"--email-domains", default="", help="Comma-separated list of allowed email domains"
)
@click.option("--created-by", required=True, help="Account ID of the creator") @click.option("--created-by", required=True, help="Account ID of the creator")
def create_organization_cmd(tenant_id, name, code, org_type, description, email_domains, created_by): def create_organization_cmd(
tenant_id, name, code, org_type, description, email_domains, created_by
):
"""Create a new organization under a tenant for multi-school support""" """Create a new organization under a tenant for multi-school support"""
try: try:
# Check if code already exists # Check if code already exists
from models.organization import Organization from models.organization import Organization
existing = db.session.query(Organization).filter(Organization.code == code).first() existing = (
db.session.query(Organization).filter(Organization.code == code).first()
)
if existing: if existing:
click.echo(f"Error: Organization with code '{code}' already exists") click.echo(f"Error: Organization with code '{code}' already exists")
return return
@ -980,7 +1123,9 @@ def create_organization_cmd(tenant_id, name, code, org_type, description, email_
db.session.add(organization) db.session.add(organization)
db.session.commit() db.session.commit()
click.echo(f"Organization '{name}' (ID: {organization.id}) created successfully") click.echo(
f"Organization '{name}' (ID: {organization.id}) created successfully"
)
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
@ -992,13 +1137,17 @@ def create_organization_cmd(tenant_id, name, code, org_type, description, email_
@click.option("--name", help="New name for the organization") @click.option("--name", help="New name for the organization")
@click.option("--description", help="New description") @click.option("--description", help="New description")
@click.option("--email-domains", help="Comma-separated list of allowed email domains") @click.option("--email-domains", help="Comma-separated list of allowed email domains")
@click.option("--status", type=click.Choice(["active", "inactive"]), help="Organization status") @click.option(
"--status", type=click.Choice(["active", "inactive"]), help="Organization status"
)
def update_organization_cmd(org_id, name, description, email_domains, status): def update_organization_cmd(org_id, name, description, email_domains, status):
"""Update an existing organization's configuration""" """Update an existing organization's configuration"""
try: try:
from models.organization import Organization from models.organization import Organization
organization = db.session.query(Organization).filter(Organization.id == org_id).first() organization = (
db.session.query(Organization).filter(Organization.id == org_id).first()
)
if not organization: if not organization:
click.echo(f"Error: Organization with ID '{org_id}' not found") click.echo(f"Error: Organization with ID '{org_id}' not found")
return return
@ -1031,7 +1180,11 @@ def update_organization_cmd(org_id, name, description, email_domains, status):
def list_organizations_cmd(tenant_id): def list_organizations_cmd(tenant_id):
"""List all organizations with optional tenant filtering""" """List all organizations with optional tenant filtering"""
try: try:
from models.organization import Organization, OrganizationMember, OrganizationRole from models.organization import (
Organization,
OrganizationMember,
OrganizationRole,
)
query = db.session.query(Organization) query = db.session.query(Organization)
@ -1093,7 +1246,9 @@ def show_organization_cmd(org_id):
try: try:
from models.organization import Organization from models.organization import Organization
organization = db.session.query(Organization).filter(Organization.id == org_id).first() organization = (
db.session.query(Organization).filter(Organization.id == org_id).first()
)
if not organization: if not organization:
click.echo(f"Error: Organization with ID '{org_id}' not found") click.echo(f"Error: Organization with ID '{org_id}' not found")
@ -1123,19 +1278,27 @@ def show_organization_cmd(org_id):
@click.option( @click.option(
"--role", "--role",
required=True, required=True,
type=click.Choice(["admin", "teacher", "student", "staff", "manager", "employee", "guest"]), type=click.Choice(
["admin", "teacher", "student", "staff", "manager", "employee", "guest"]
),
help="Role in the organization", help="Role in the organization",
) )
@click.option("--department", help="Department within the organization") @click.option("--department", help="Department within the organization")
@click.option("--title", help="Job title or position") @click.option("--title", help="Job title or position")
@click.option("--is-default", is_flag=True, help="Set as the account's default organization") @click.option(
def add_account_to_organization_cmd(org_id, account_id, role, department, title, is_default): "--is-default", is_flag=True, help="Set as the account's default organization"
)
def add_account_to_organization_cmd(
org_id, account_id, role, department, title, is_default
):
"""Add an account to an organization with appropriate role and metadata""" """Add an account to an organization with appropriate role and metadata"""
try: try:
from models.organization import Organization, OrganizationMember from models.organization import Organization, OrganizationMember
# Check if organization exists # Check if organization exists
organization = db.session.query(Organization).filter(Organization.id == org_id).first() organization = (
db.session.query(Organization).filter(Organization.id == org_id).first()
)
if not organization: if not organization:
click.echo(f"Error: Organization with ID '{org_id}' not found") click.echo(f"Error: Organization with ID '{org_id}' not found")
return return
@ -1157,7 +1320,9 @@ def add_account_to_organization_cmd(org_id, account_id, role, department, title,
) )
if existing: if existing:
click.echo("Account is already a member of this organization. Updating role and metadata.") click.echo(
"Account is already a member of this organization. Updating role and metadata."
)
existing.role = role existing.role = role
existing.department = department existing.department = department
existing.title = title existing.title = title
@ -1230,7 +1395,9 @@ def upload_private_key_file_cloud_storage(tenant_id: Optional[str] = None):
) )
file_key = f"privkeys/{tenant_id}/private.pem" file_key = f"privkeys/{tenant_id}/private.pem"
file_content = Path(f"{os.environ.get('STORAGE_LOCAL_PATH', 'storage')}/{file_key}").read_bytes() file_content = Path(
f"{os.environ.get('STORAGE_LOCAL_PATH', 'storage')}/{file_key}"
).read_bytes()
storage.save(filename=file_key, data=file_content) storage.save(filename=file_key, data=file_content)
click.echo( click.echo(
click.style( click.style(
@ -1240,7 +1407,9 @@ def upload_private_key_file_cloud_storage(tenant_id: Optional[str] = None):
) )
@click.command("upload-local-files-to-cloud-storage", help="upload local files to cloud storage") @click.command(
"upload-local-files-to-cloud-storage", help="upload local files to cloud storage"
)
def upload_local_files_to_cloud_storage(): def upload_local_files_to_cloud_storage():
""" """
upload local files to cloud storage upload local files to cloud storage
@ -1258,10 +1427,14 @@ def upload_local_files_to_cloud_storage():
batch_size = 100 batch_size = 100
processed_count = 0 processed_count = 0
while processed_count < total_count: while processed_count < total_count:
files: list[UploadFile] = UploadFile.query.filter_by(storage_type="local").limit(batch_size).all() files: list[UploadFile] = (
UploadFile.query.filter_by(storage_type="local").limit(batch_size).all()
)
for file in files: for file in files:
target_filepath = f"{os.environ.get('STORAGE_LOCAL_PATH', 'storage')}/{file.key}" target_filepath = (
f"{os.environ.get('STORAGE_LOCAL_PATH', 'storage')}/{file.key}"
)
# if the file exists # if the file exists
if not os.path.exists(target_filepath): if not os.path.exists(target_filepath):
@ -1307,7 +1480,11 @@ def upload_local_files_to_cloud_storage():
processed_count += 1 processed_count += 1
if processed_count % 10 == 0 or processed_count == total_count: if processed_count % 10 == 0 or processed_count == total_count:
click.echo(click.style(f"Processed {processed_count}/{total_count} files\n", fg="blue")) click.echo(
click.style(
f"Processed {processed_count}/{total_count} files\n", fg="blue"
)
)
time.sleep(3) time.sleep(3)
click.echo( click.echo(
@ -1408,7 +1585,9 @@ def install_plugins(input_file: str, output_file: str, workers: int):
click.echo(click.style("Install plugins completed.", fg="green")) click.echo(click.style("Install plugins completed.", fg="green"))
@click.command("clear-free-plan-tenant-expired-logs", help="Clear free plan tenant expired logs.") @click.command(
"clear-free-plan-tenant-expired-logs", help="Clear free plan tenant expired logs."
)
@click.option( @click.option(
"--days", "--days",
prompt=True, prompt=True,
@ -1435,7 +1614,9 @@ def clear_free_plan_tenant_expired_logs(days: int, batch: int, tenant_ids: list[
ClearFreePlanTenantExpiredLogs.process(days, batch, tenant_ids) ClearFreePlanTenantExpiredLogs.process(days, batch, tenant_ids)
click.echo(click.style("Clear free plan tenant expired logs completed.", fg="green")) click.echo(
click.style("Clear free plan tenant expired logs completed.", fg="green")
)
@click.option( @click.option(
@ -1491,7 +1672,9 @@ def clear_orphaned_file_records(force: bool):
) )
) )
for ids_table in ids_tables: for ids_table in ids_tables:
click.echo(click.style(f"- {ids_table['table']} ({ids_table['column']})", fg="yellow")) click.echo(
click.style(f"- {ids_table['table']} ({ids_table['column']})", fg="yellow")
)
click.echo("") click.echo("")
click.echo(click.style("!!! USE WITH CAUTION !!!", fg="red")) click.echo(click.style("!!! USE WITH CAUTION !!!", fg="red"))
@ -1542,7 +1725,9 @@ def clear_orphaned_file_records(force: bool):
with db.engine.begin() as conn: with db.engine.begin() as conn:
rs = conn.execute(db.text(query)) rs = conn.execute(db.text(query))
for i in rs: for i in rs:
orphaned_message_files.append({"id": str(i[0]), "message_id": str(i[1])}) orphaned_message_files.append(
{"id": str(i[0]), "message_id": str(i[1])}
)
if orphaned_message_files: if orphaned_message_files:
click.echo( click.echo(
@ -1568,7 +1753,9 @@ def clear_orphaned_file_records(force: bool):
abort=True, abort=True,
) )
click.echo(click.style("- Deleting orphaned message_files records", fg="white")) click.echo(
click.style("- Deleting orphaned message_files records", fg="white")
)
query = "DELETE FROM message_files WHERE id IN :ids" query = "DELETE FROM message_files WHERE id IN :ids"
with db.engine.begin() as conn: with db.engine.begin() as conn:
conn.execute( conn.execute(
@ -1589,7 +1776,11 @@ def clear_orphaned_file_records(force: bool):
) )
) )
except Exception as e: except Exception as e:
click.echo(click.style(f"Error deleting orphaned message_files records: {str(e)}", fg="red")) click.echo(
click.style(
f"Error deleting orphaned message_files records: {str(e)}", fg="red"
)
)
# clean up the orphaned records in the rest of the *_files tables # clean up the orphaned records in the rest of the *_files tables
try: try:
@ -1606,8 +1797,14 @@ def clear_orphaned_file_records(force: bool):
with db.engine.begin() as conn: with db.engine.begin() as conn:
rs = conn.execute(db.text(query)) rs = conn.execute(db.text(query))
for i in rs: for i in rs:
all_files_in_tables.append({"table": files_table["table"], "id": str(i[0]), "key": i[1]}) all_files_in_tables.append(
click.echo(click.style(f"Found {len(all_files_in_tables)} files in tables.", fg="white")) {"table": files_table["table"], "id": str(i[0]), "key": i[1]}
)
click.echo(
click.style(
f"Found {len(all_files_in_tables)} files in tables.", fg="white"
)
)
# fetch referred table and columns # fetch referred table and columns
guid_regexp = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" guid_regexp = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
@ -1622,12 +1819,15 @@ def clear_orphaned_file_records(force: bool):
) )
) )
query = ( query = (
f"SELECT {ids_table['column']} FROM {ids_table['table']} WHERE {ids_table['column']} IS NOT NULL" f"SELECT {ids_table['column']} FROM {ids_table['table']} "
f"WHERE {ids_table['column']} IS NOT NULL"
) )
with db.engine.begin() as conn: with db.engine.begin() as conn:
rs = conn.execute(db.text(query)) rs = conn.execute(db.text(query))
for i in rs: for i in rs:
all_ids_in_tables.append({"table": ids_table["table"], "id": str(i[0])}) all_ids_in_tables.append(
{"table": ids_table["table"], "id": str(i[0])}
)
elif ids_table["type"] == "text": elif ids_table["type"] == "text":
click.echo( click.echo(
click.style( click.style(
@ -1663,7 +1863,11 @@ def clear_orphaned_file_records(force: bool):
for i in rs: for i in rs:
for j in i[0]: for j in i[0]:
all_ids_in_tables.append({"table": ids_table["table"], "id": j}) all_ids_in_tables.append({"table": ids_table["table"], "id": j})
click.echo(click.style(f"Found {len(all_ids_in_tables)} file ids in tables.", fg="white")) click.echo(
click.style(
f"Found {len(all_ids_in_tables)} file ids in tables.", fg="white"
)
)
except Exception as e: except Exception as e:
click.echo(click.style(f"Error fetching keys: {str(e)}", fg="red")) click.echo(click.style(f"Error fetching keys: {str(e)}", fg="red"))
@ -1681,7 +1885,9 @@ def clear_orphaned_file_records(force: bool):
) )
) )
return return
click.echo(click.style(f"Found {len(orphaned_files)} orphaned file records.", fg="white")) click.echo(
click.style(f"Found {len(orphaned_files)} orphaned file records.", fg="white")
)
for file in orphaned_files: for file in orphaned_files:
click.echo(click.style(f"- orphaned file id: {file}", fg="black")) click.echo(click.style(f"- orphaned file id: {file}", fg="black"))
if not force: if not force:
@ -1703,9 +1909,13 @@ def clear_orphaned_file_records(force: bool):
with db.engine.begin() as conn: with db.engine.begin() as conn:
conn.execute(db.text(query), {"ids": tuple(orphaned_files)}) conn.execute(db.text(query), {"ids": tuple(orphaned_files)})
except Exception as e: except Exception as e:
click.echo(click.style(f"Error deleting orphaned file records: {str(e)}", fg="red")) click.echo(
click.style(f"Error deleting orphaned file records: {str(e)}", fg="red")
)
return return
click.echo(click.style(f"Removed {len(orphaned_files)} orphaned file records.", fg="green")) click.echo(
click.style(f"Removed {len(orphaned_files)} orphaned file records.", fg="green")
)
@click.option( @click.option(
@ -1714,7 +1924,9 @@ def clear_orphaned_file_records(force: bool):
is_flag=True, is_flag=True,
help="Skip user confirmation and force the command to execute.", help="Skip user confirmation and force the command to execute.",
) )
@click.command("remove-orphaned-files-on-storage", help="Remove orphaned files on the storage.") @click.command(
"remove-orphaned-files-on-storage", help="Remove orphaned files on the storage."
)
def remove_orphaned_files_on_storage(force: bool): def remove_orphaned_files_on_storage(force: bool):
""" """
Remove orphaned files on the storage. Remove orphaned files on the storage.
@ -1790,20 +2002,32 @@ def remove_orphaned_files_on_storage(force: bool):
all_files_in_tables = [] all_files_in_tables = []
try: try:
for files_table in files_tables: for files_table in files_tables:
click.echo(click.style(f"- Listing files from table {files_table['table']}", fg="white")) click.echo(
click.style(
f"- Listing files from table {files_table['table']}", fg="white"
)
)
query = f"SELECT {files_table['key_column']} FROM {files_table['table']}" query = f"SELECT {files_table['key_column']} FROM {files_table['table']}"
with db.engine.begin() as conn: with db.engine.begin() as conn:
rs = conn.execute(db.text(query)) rs = conn.execute(db.text(query))
for i in rs: for i in rs:
all_files_in_tables.append(str(i[0])) all_files_in_tables.append(str(i[0]))
click.echo(click.style(f"Found {len(all_files_in_tables)} files in tables.", fg="white")) click.echo(
click.style(
f"Found {len(all_files_in_tables)} files in tables.", fg="white"
)
)
except Exception as e: except Exception as e:
click.echo(click.style(f"Error fetching keys: {str(e)}", fg="red")) click.echo(click.style(f"Error fetching keys: {str(e)}", fg="red"))
all_files_on_storage = [] all_files_on_storage = []
for storage_path in storage_paths: for storage_path in storage_paths:
try: try:
click.echo(click.style(f"- Scanning files on storage path {storage_path}", fg="white")) click.echo(
click.style(
f"- Scanning files on storage path {storage_path}", fg="white"
)
)
files = storage.scan(path=storage_path, files=True, directories=False) files = storage.scan(path=storage_path, files=True, directories=False)
all_files_on_storage.extend(files) all_files_on_storage.extend(files)
except FileNotFoundError as e: except FileNotFoundError as e:
@ -1822,12 +2046,18 @@ def remove_orphaned_files_on_storage(force: bool):
) )
) )
continue continue
click.echo(click.style(f"Found {len(all_files_on_storage)} files on storage.", fg="white")) click.echo(
click.style(f"Found {len(all_files_on_storage)} files on storage.", fg="white")
)
# find orphaned files # find orphaned files
orphaned_files = list(set(all_files_on_storage) - set(all_files_in_tables)) orphaned_files = list(set(all_files_on_storage) - set(all_files_in_tables))
if not orphaned_files: if not orphaned_files:
click.echo(click.style("No orphaned files found. There is nothing to remove.", fg="green")) click.echo(
click.style(
"No orphaned files found. There is nothing to remove.", fg="green"
)
)
return return
click.echo(click.style(f"Found {len(orphaned_files)} orphaned files.", fg="white")) click.echo(click.style(f"Found {len(orphaned_files)} orphaned files.", fg="white"))
for file in orphaned_files: for file in orphaned_files:
@ -1848,10 +2078,18 @@ def remove_orphaned_files_on_storage(force: bool):
click.echo(click.style(f"- Removing orphaned file: {file}", fg="white")) click.echo(click.style(f"- Removing orphaned file: {file}", fg="white"))
except Exception as e: except Exception as e:
error_files += 1 error_files += 1
click.echo(click.style(f"- Error deleting orphaned file {file}: {str(e)}", fg="red")) click.echo(
click.style(
f"- Error deleting orphaned file {file}: {str(e)}", fg="red"
)
)
continue continue
if error_files == 0: if error_files == 0:
click.echo(click.style(f"Removed {removed_files} orphaned files without errors.", fg="green")) click.echo(
click.style(
f"Removed {removed_files} orphaned files without errors.", fg="green"
)
)
else: else:
click.echo( click.echo(
click.style( click.style(
@ -1859,3 +2097,20 @@ def remove_orphaned_files_on_storage(force: bool):
fg="yellow", fg="yellow",
) )
) )
# Keep the original function for backward compatibility
@click.command(
"create-admin-with-phone",
help="Create or update an admin account for an organization with a phone number.",
)
@click.option("--name", prompt=True, help="Admin account name")
@click.option("--phone", prompt=True, help="Admin account phone number")
@click.option("--organization-id", required=True, help="Organization ID")
def create_admin_with_phone(name: str, phone: str, organization_id: str):
"""
Create or update an admin account with a phone number for a specific organization.
This command will create a new account if the phone doesn't exist,
or update an existing account with the specified admin role.
"""
return create_admin_account(name, phone, "phone", organization_id)

@ -8,6 +8,7 @@ def init_app(app: DifyApp):
clear_free_plan_tenant_expired_logs, clear_free_plan_tenant_expired_logs,
clear_orphaned_file_records, clear_orphaned_file_records,
convert_to_agent_apps, convert_to_agent_apps,
create_admin_account,
create_admin_with_phone, create_admin_with_phone,
create_organization_cmd, create_organization_cmd,
create_tenant, create_tenant,
@ -40,6 +41,7 @@ def init_app(app: DifyApp):
create_tenant, create_tenant,
upgrade_db, upgrade_db,
fix_app_site_missing, fix_app_site_missing,
create_admin_account,
create_admin_with_phone, create_admin_with_phone,
create_organization_cmd, create_organization_cmd,
add_account_to_organization_cmd, add_account_to_organization_cmd,

Loading…
Cancel
Save