mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
feat(lark): 支持商店应用机器人 (#1855)
* feat(lark): 支持商店应用机器人 * feat(lark): app_type改成select模式,修复select配置无效,按照copilot建议隐藏log敏感信息 * fix: KeyError for backward compatibility --------- Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.github
|
||||||
|
.venv
|
||||||
|
.vscode
|
||||||
|
.data
|
||||||
|
.temp
|
||||||
|
web/.next
|
||||||
|
web/node_modules
|
||||||
|
web/.env
|
||||||
@@ -9,6 +9,7 @@ import re
|
|||||||
import base64
|
import base64
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
@@ -19,6 +20,8 @@ import quart
|
|||||||
from lark_oapi.api.im.v1 import *
|
from lark_oapi.api.im.v1 import *
|
||||||
import pydantic
|
import pydantic
|
||||||
from lark_oapi.api.cardkit.v1 import *
|
from lark_oapi.api.cardkit.v1 import *
|
||||||
|
from lark_oapi.api.auth.v3 import *
|
||||||
|
from lark_oapi.core.model import *
|
||||||
|
|
||||||
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
||||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
||||||
@@ -384,6 +387,7 @@ class LarkEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
|||||||
),
|
),
|
||||||
message_chain=message_chain,
|
message_chain=message_chain,
|
||||||
time=event.event.message.create_time,
|
time=event.event.message.create_time,
|
||||||
|
source_platform_object=event,
|
||||||
)
|
)
|
||||||
elif event.event.message.chat_type == 'group':
|
elif event.event.message.chat_type == 'group':
|
||||||
return platform_events.GroupMessage(
|
return platform_events.GroupMessage(
|
||||||
@@ -400,6 +404,7 @@ class LarkEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
|||||||
),
|
),
|
||||||
message_chain=message_chain,
|
message_chain=message_chain,
|
||||||
time=event.event.message.create_time,
|
time=event.event.message.create_time,
|
||||||
|
source_platform_object=event,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -429,6 +434,10 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
|
|
||||||
seq: int # 用于在发送卡片消息中识别消息顺序,直接以seq作为标识
|
seq: int # 用于在发送卡片消息中识别消息顺序,直接以seq作为标识
|
||||||
bot_uuid: str = None # 机器人UUID
|
bot_uuid: str = None # 机器人UUID
|
||||||
|
app_ticket: str = None # 商店应用用到
|
||||||
|
app_access_token: str = None # 商店应用用到
|
||||||
|
app_access_token_expire_at: int = None
|
||||||
|
tenant_access_tokens: dict[str, dict[str, str]] = {} # 租户access_token映射
|
||||||
|
|
||||||
def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger, **kwargs):
|
def __init__(self, config: dict, logger: abstract_platform_logger.AbstractEventLogger, **kwargs):
|
||||||
quart_app = quart.Quart(__name__)
|
quart_app = quart.Quart(__name__)
|
||||||
@@ -448,8 +457,9 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
bot_account_id = config['bot_name']
|
bot_account_id = config['bot_name']
|
||||||
|
|
||||||
bot = lark_oapi.ws.Client(config['app_id'], config['app_secret'], event_handler=event_handler)
|
bot = lark_oapi.ws.Client(config['app_id'], config['app_secret'], event_handler=event_handler)
|
||||||
api_client = lark_oapi.Client.builder().app_id(config['app_id']).app_secret(config['app_secret']).build()
|
api_client = self.build_api_client(config)
|
||||||
cipher = AESCipher(config.get('encrypt-key', ''))
|
cipher = AESCipher(config.get('encrypt-key', ''))
|
||||||
|
self.request_app_ticket(api_client, config)
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
config=config,
|
config=config,
|
||||||
@@ -466,6 +476,101 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def request_app_ticket(self, api_client, config):
|
||||||
|
app_id = config['app_id']
|
||||||
|
app_secret = config['app_secret']
|
||||||
|
print(f'Requesting app ticket for app_id: {app_id[:3]}***{app_id[-3:]}')
|
||||||
|
if 'isv' == config.get('app_type', 'self'):
|
||||||
|
request: ResendAppTicketRequest = (
|
||||||
|
ResendAppTicketRequest.builder()
|
||||||
|
.request_body(ResendAppTicketRequestBody.builder().app_id(app_id).app_secret(app_secret).build())
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
response: ResendAppTicketResponse = api_client.auth.v3.app_ticket.resend(request)
|
||||||
|
if not response.success():
|
||||||
|
raise Exception(
|
||||||
|
f'client.auth.v3.auth.app_ticket_resend failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
||||||
|
)
|
||||||
|
|
||||||
|
def request_app_access_token(self):
|
||||||
|
app_id = self.config['app_id']
|
||||||
|
app_secret = self.config['app_secret']
|
||||||
|
if 'isv' == self.config.get('app_type', 'self'):
|
||||||
|
request: CreateAppAccessTokenRequest = (
|
||||||
|
CreateAppAccessTokenRequest.builder()
|
||||||
|
.request_body(
|
||||||
|
CreateAppAccessTokenRequestBody.builder()
|
||||||
|
.app_id(app_id)
|
||||||
|
.app_secret(app_secret)
|
||||||
|
.app_ticket(self.app_ticket)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
response: CreateAppAccessTokenResponse = self.api_client.auth.v3.app_access_token.create(request)
|
||||||
|
if not response.success():
|
||||||
|
raise Exception(
|
||||||
|
f'client.auth.v3.auth.app_access_token failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
||||||
|
)
|
||||||
|
content = json.loads(response.raw.content)
|
||||||
|
self.app_access_token = content['app_access_token']
|
||||||
|
self.app_access_token_expire_at = int(time.time()) + content['expire'] - 300
|
||||||
|
|
||||||
|
def get_app_access_token(self):
|
||||||
|
if 'isv' != self.config.get('app_type', 'self'):
|
||||||
|
return None
|
||||||
|
if (
|
||||||
|
self.app_access_token is None
|
||||||
|
or self.app_access_token_expire_at is None
|
||||||
|
or int(time.time()) >= self.app_access_token_expire_at
|
||||||
|
):
|
||||||
|
self.request_app_access_token()
|
||||||
|
return self.app_access_token
|
||||||
|
|
||||||
|
def request_tenant_access_token(self, tenant_key: str):
|
||||||
|
app_access_token = self.get_app_access_token()
|
||||||
|
if 'isv' == self.config.get('app_type', 'self'):
|
||||||
|
request: CreateTenantAccessTokenRequest = (
|
||||||
|
CreateTenantAccessTokenRequest.builder()
|
||||||
|
.request_body(
|
||||||
|
CreateTenantAccessTokenRequestBody.builder()
|
||||||
|
.app_access_token(app_access_token)
|
||||||
|
.tenant_key(tenant_key)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
response: CreateTenantAccessTokenResponse = self.api_client.auth.v3.tenant_access_token.create(request)
|
||||||
|
if not response.success():
|
||||||
|
raise Exception(
|
||||||
|
f'client.auth.v3.auth.tenant_access_token failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
||||||
|
)
|
||||||
|
content = json.loads(response.raw.content)
|
||||||
|
tenant_access_token = content['tenant_access_token']
|
||||||
|
expire = content['expire']
|
||||||
|
self.tenant_access_tokens[tenant_key] = {
|
||||||
|
'token': tenant_access_token,
|
||||||
|
'expire_at': int(time.time()) + expire - 300,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_tenant_access_token(self, tenant_key: str):
|
||||||
|
if tenant_key is None or 'isv' != self.config.get('app_type', 'self'):
|
||||||
|
return None
|
||||||
|
tenant_access_token = self.tenant_access_tokens.get(tenant_key)
|
||||||
|
if tenant_access_token is None or int(time.time()) >= tenant_access_token['expire_at']:
|
||||||
|
self.request_tenant_access_token(tenant_key)
|
||||||
|
return self.tenant_access_tokens.get(tenant_key)['token'] if self.tenant_access_tokens.get(tenant_key) else None
|
||||||
|
|
||||||
|
def build_api_client(self, config):
|
||||||
|
app_id = config['app_id']
|
||||||
|
app_secret = config['app_secret']
|
||||||
|
api_client = lark_oapi.Client.builder().app_id(app_id).app_secret(app_secret).build()
|
||||||
|
if 'isv' == config.get('app_type', 'self'):
|
||||||
|
api_client = (
|
||||||
|
lark_oapi.Client.builder().app_id(app_id).app_secret(app_secret).app_type(lark_oapi.AppType.ISV).build()
|
||||||
|
)
|
||||||
|
return api_client
|
||||||
|
|
||||||
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
async def send_message(self, target_type: str, target_id: str, message: platform_message.MessageChain):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -693,9 +798,19 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
tenant_key = event.source_platform_object.header.tenant_key if event.source_platform_object else None
|
||||||
|
app_access_token = self.get_app_access_token()
|
||||||
|
tenant_access_token = self.get_tenant_access_token(tenant_key)
|
||||||
|
req_opt: RequestOption = (
|
||||||
|
RequestOption.builder()
|
||||||
|
.app_ticket(self.app_ticket)
|
||||||
|
.tenant_key(tenant_key)
|
||||||
|
.app_access_token(app_access_token)
|
||||||
|
.tenant_access_token(tenant_access_token)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
# 发起请求
|
# 发起请求
|
||||||
response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(request)
|
response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(request, req_opt)
|
||||||
|
|
||||||
# 处理失败返回
|
# 处理失败返回
|
||||||
if not response.success():
|
if not response.success():
|
||||||
@@ -722,7 +837,6 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
'content': text_elements,
|
'content': text_elements,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
request: ReplyMessageRequest = (
|
request: ReplyMessageRequest = (
|
||||||
ReplyMessageRequest.builder()
|
ReplyMessageRequest.builder()
|
||||||
.message_id(message_source.message_chain.message_id)
|
.message_id(message_source.message_chain.message_id)
|
||||||
@@ -737,7 +851,22 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
|
||||||
response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(request)
|
tenant_key = (
|
||||||
|
message_source.source_platform_object.header.tenant_key
|
||||||
|
if message_source.source_platform_object
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
app_access_token = self.get_app_access_token()
|
||||||
|
tenant_access_token = self.get_tenant_access_token(tenant_key)
|
||||||
|
req_opt: RequestOption = (
|
||||||
|
RequestOption.builder()
|
||||||
|
.app_ticket(self.app_ticket)
|
||||||
|
.tenant_key(tenant_key)
|
||||||
|
.app_access_token(app_access_token)
|
||||||
|
.tenant_access_token(tenant_access_token)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(request, req_opt)
|
||||||
|
|
||||||
if not response.success():
|
if not response.success():
|
||||||
raise Exception(
|
raise Exception(
|
||||||
@@ -762,7 +891,22 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
|
||||||
response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(request)
|
tenant_key = (
|
||||||
|
message_source.source_platform_object.header.tenant_key
|
||||||
|
if message_source.source_platform_object
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
app_access_token = self.get_app_access_token()
|
||||||
|
tenant_access_token = self.get_tenant_access_token(tenant_key)
|
||||||
|
req_opt: RequestOption = (
|
||||||
|
RequestOption.builder()
|
||||||
|
.app_ticket(self.app_ticket)
|
||||||
|
.tenant_key(tenant_key)
|
||||||
|
.app_access_token(app_access_token)
|
||||||
|
.tenant_access_token(tenant_access_token)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
response: ReplyMessageResponse = await self.api_client.im.v1.message.areply(request, req_opt)
|
||||||
|
|
||||||
if not response.success():
|
if not response.success():
|
||||||
raise Exception(
|
raise Exception(
|
||||||
@@ -816,8 +960,24 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
if is_final and bot_message.tool_calls is None:
|
if is_final and bot_message.tool_calls is None:
|
||||||
# self.seq = 1 # 消息回复结束之后重置seq
|
# self.seq = 1 # 消息回复结束之后重置seq
|
||||||
self.card_id_dict.pop(message_id) # 清理已经使用过的卡片
|
self.card_id_dict.pop(message_id) # 清理已经使用过的卡片
|
||||||
|
|
||||||
|
tenant_key = (
|
||||||
|
message_source.source_platform_object.header.tenant_key
|
||||||
|
if message_source.source_platform_object
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
app_access_token = self.get_app_access_token()
|
||||||
|
tenant_access_token = self.get_tenant_access_token(tenant_key)
|
||||||
|
req_opt: RequestOption = (
|
||||||
|
RequestOption.builder()
|
||||||
|
.app_ticket(self.app_ticket)
|
||||||
|
.tenant_key(tenant_key)
|
||||||
|
.app_access_token(app_access_token)
|
||||||
|
.tenant_access_token(tenant_access_token)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
# 发起请求
|
# 发起请求
|
||||||
response: ContentCardElementResponse = self.api_client.cardkit.v1.card_element.content(request)
|
response: ContentCardElementResponse = self.api_client.cardkit.v1.card_element.content(request, req_opt)
|
||||||
|
|
||||||
# 处理失败返回
|
# 处理失败返回
|
||||||
if not response.success():
|
if not response.success():
|
||||||
@@ -851,6 +1011,17 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
"""设置 bot UUID(用于生成 webhook URL)"""
|
"""设置 bot UUID(用于生成 webhook URL)"""
|
||||||
self.bot_uuid = bot_uuid
|
self.bot_uuid = bot_uuid
|
||||||
|
|
||||||
|
def get_event_type(self, data):
|
||||||
|
schema = '1.0'
|
||||||
|
if 'schema' in data:
|
||||||
|
schema = data['schema']
|
||||||
|
if '2.0' == schema:
|
||||||
|
return data['header']['event_type']
|
||||||
|
elif 'event' in data:
|
||||||
|
return data['event']['type']
|
||||||
|
else:
|
||||||
|
return data['type']
|
||||||
|
|
||||||
async def handle_unified_webhook(self, bot_uuid: str, path: str, request):
|
async def handle_unified_webhook(self, bot_uuid: str, path: str, request):
|
||||||
"""处理统一 webhook 请求。
|
"""处理统一 webhook 请求。
|
||||||
Args:
|
Args:
|
||||||
@@ -866,21 +1037,18 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
if 'encrypt' in data:
|
if 'encrypt' in data:
|
||||||
data = self.cipher.decrypt_string(data['encrypt'])
|
data = self.cipher.decrypt_string(data['encrypt'])
|
||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
type = data.get('type')
|
type = self.get_event_type(data)
|
||||||
if type is None:
|
context = EventContext(data)
|
||||||
context = EventContext(data)
|
|
||||||
type = context.header.event_type
|
|
||||||
|
|
||||||
if 'url_verification' == type:
|
if 'url_verification' == type:
|
||||||
# todo 验证verification token
|
# todo 验证verification token
|
||||||
return {'challenge': data.get('challenge')}
|
return {'challenge': data.get('challenge')}
|
||||||
context = EventContext(data)
|
elif 'app_ticket' == type:
|
||||||
type = context.header.event_type
|
self.app_ticket = context.event['app_ticket']
|
||||||
p2v1 = P2ImMessageReceiveV1()
|
elif 'im.message.receive_v1' == type:
|
||||||
p2v1.header = context.header
|
|
||||||
event = P2ImMessageReceiveV1Data()
|
|
||||||
if 'im.message.receive_v1' == type:
|
|
||||||
try:
|
try:
|
||||||
|
p2v1 = P2ImMessageReceiveV1()
|
||||||
|
p2v1.header = context.header
|
||||||
|
event = P2ImMessageReceiveV1Data()
|
||||||
event.message = EventMessage(context.event['message'])
|
event.message = EventMessage(context.event['message'])
|
||||||
event.sender = EventSender(context.event['sender'])
|
event.sender = EventSender(context.event['sender'])
|
||||||
p2v1.event = event
|
p2v1.event = event
|
||||||
@@ -898,7 +1066,7 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
final_content = {
|
final_content = {
|
||||||
'zh_Hans': {
|
'zh_Hans': {
|
||||||
'title': '',
|
'title': '',
|
||||||
'content': bot_added_welcome_msg,
|
'content': [[{'tag': 'md', 'text': bot_added_welcome_msg}]],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
chat_id = context.event['chat_id']
|
chat_id = context.event['chat_id']
|
||||||
@@ -915,17 +1083,30 @@ class LarkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
|||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
response: CreateMessageResponse = self.api_client.im.v1.message.create(request)
|
tenant_key = context.header.tenant_key if context.header else None
|
||||||
|
app_access_token = self.get_app_access_token()
|
||||||
|
tenant_access_token = self.get_tenant_access_token(tenant_key)
|
||||||
|
req_opt: RequestOption = (
|
||||||
|
RequestOption.builder()
|
||||||
|
.app_ticket(self.app_ticket)
|
||||||
|
.tenant_key(tenant_key)
|
||||||
|
.app_access_token(app_access_token)
|
||||||
|
.tenant_access_token(tenant_access_token)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
response: CreateMessageResponse = self.api_client.im.v1.message.create(request, req_opt)
|
||||||
|
|
||||||
if not response.success():
|
if not response.success():
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f'client.im.v1.message.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
f'client.im.v1.message.create failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}'
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
print(f'im.chat.member.bot.added_v1: {e}')
|
||||||
await self.logger.error(f'Error in lark callback: {traceback.format_exc()}')
|
await self.logger.error(f'Error in lark callback: {traceback.format_exc()}')
|
||||||
|
|
||||||
return {'code': 200, 'message': 'ok'}
|
return {'code': 200, 'message': 'ok'}
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
print(f'Error in lark callback: {e}')
|
||||||
await self.logger.error(f'Error in lark callback: {traceback.format_exc()}')
|
await self.logger.error(f'Error in lark callback: {traceback.format_exc()}')
|
||||||
return {'code': 500, 'message': 'error'}
|
return {'code': 500, 'message': 'error'}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,25 @@ spec:
|
|||||||
type: boolean
|
type: boolean
|
||||||
required: true
|
required: true
|
||||||
default: false
|
default: false
|
||||||
|
- name: app_type
|
||||||
|
label:
|
||||||
|
en_US: App Type
|
||||||
|
zh_Hans: 应用类型
|
||||||
|
description:
|
||||||
|
en_US: Default to self-built application, refer to https://open.feishu.cn/document/platform-overveiw/overview
|
||||||
|
zh_Hans: 默认为企业自建应用,参考 https://open.feishu.cn/document/platform-overveiw/overview
|
||||||
|
type: select
|
||||||
|
options:
|
||||||
|
- name: self
|
||||||
|
label:
|
||||||
|
en_US: Self-built Application
|
||||||
|
zh_Hans: 自建应用
|
||||||
|
- name: isv
|
||||||
|
label:
|
||||||
|
en_US: Store Application
|
||||||
|
zh_Hans: 商店应用
|
||||||
|
required: false
|
||||||
|
default: self
|
||||||
- name: bot_added_welcome
|
- name: bot_added_welcome
|
||||||
label:
|
label:
|
||||||
en_US: Bot Welcome Message
|
en_US: Bot Welcome Message
|
||||||
|
|||||||
@@ -310,6 +310,7 @@ export default function BotForm({
|
|||||||
name: item.name,
|
name: item.name,
|
||||||
required: item.required,
|
required: item.required,
|
||||||
type: parseDynamicFormItemType(item.type),
|
type: parseDynamicFormItemType(item.type),
|
||||||
|
options: item.options,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user