9 Commits

Author SHA1 Message Date
npc0-hue d6fe1858db fix: React Server Components拒绝服务漏洞 (CVE-2025-55184) 和 CVE-2025-54880 2025-12-15 15:44:23 +08:00
npc0-hue 2e70cb143b fix: 修复 cve-2025-55182 2025-12-06 11:14:49 +08:00
npc0-hue bcd2ff4a45 fix: 修复text-to-audio 记得添加 api_token
fix: 修复监控界面刷新每次会先显示半秒余额15后才切换为真实值
fix: 添加使用统计
2025-12-06 11:14:49 +08:00
FamousMai 52e99cf180 Update README.md
更新群二维码
2025-10-30 18:02:22 +08:00
npc0-hue 88fd431534 fix: 修改镜像版本,解决部分用户拉取到旧的镜像 2025-10-28 18:03:08 +08:00
npc0-hue e1a1b1f799 feat: - 移除后台用户mail唯一索引 2025-10-28 11:53:05 +08:00
npc0-hue 316fa371f2 feat: - 非权限用户不显示密钥按钮
- 用户id没有自增解决方案
2025-10-28 11:53:05 +08:00
npc0-hue c507fc2675 feat:-皮肤处理和oauth快捷登录
- 同步至应用模板
- 取消同步至应用模板
- 计费唯一索引
2025-10-28 11:53:05 +08:00
FamousMai 2186654be7 fix: 修复初始化用户后,登录失败后报密码错误
Removed user existence check and password update logic.
2025-10-19 16:44:58 +08:00
31 changed files with 1273 additions and 984 deletions
+2 -1
View File
@@ -135,7 +135,8 @@ Dify-Plus,该名字不是说比 Dify 项目牛的意思,意思是想说比 D
- Dify-plus官方交流群3(已满)
- Dify-plus&Coze开源交流群4
<img width="200" height="583" alt="image" src="https://github.com/user-attachments/assets/e467e5ec-d493-46e7-8b6d-8d984a304508" />
<img width="200" height="583" alt="image" src="https://github.com/user-attachments/assets/3ac4598c-7255-4ade-99aa-8bc0f039797e" />
+1 -1
View File
@@ -7,7 +7,7 @@ import (
)
type GVA_MODEL struct {
ID uint `gorm:"primarykey" json:"ID"` // 主键ID
ID uint `gorm:"primarykey;autoIncrement" json:"ID"` // 主键ID
CreatedAt time.Time // 创建时间
UpdatedAt time.Time // 更新时间
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 删除时间
+1
View File
@@ -35,6 +35,7 @@ func (e *ensureTables) MigrateTable(ctx context.Context) (context.Context, error
if !ok {
return ctx, system.ErrMissingDBContext
}
tables := []interface{}{
sysModel.SysApi{},
sysModel.SysUser{},
+79
View File
@@ -1,6 +1,8 @@
package initialize
import (
"fmt"
"log"
"os"
"github.com/flipped-aurora/gin-vue-admin/server/global"
@@ -37,6 +39,7 @@ func Gorm() *gorm.DB {
func RegisterTables() {
db := global.GVA_DB
err := db.AutoMigrate(
system.SysApi{},
system.SysIgnoreApi{},
@@ -78,6 +81,11 @@ func RegisterTables() {
os.Exit(0)
}
//// 如果是PostgreSQL数据库,创建必要的序列
//if global.GVA_CONFIG.System.DbType == "pgsql" {
// createPostgreSQLSequences(db)
//}
err = bizModel()
if err != nil {
@@ -86,3 +94,74 @@ func RegisterTables() {
}
global.GVA_LOG.Info("register table success")
}
// createPostgreSQLSequences 为PostgreSQL数据库创建必要的序列
func createPostgreSQLSequences(db *gorm.DB) {
// 需要创建序列的表列表
tables := []string{
"sys_users",
"sys_apis",
"sys_base_menus",
"sys_authorities",
"sys_dictionaries",
"sys_operation_records",
"sys_auto_code_histories",
"sys_dictionary_details",
"sys_base_menu_parameters",
"sys_base_menu_btns",
"sys_authority_btns",
"sys_auto_code_packages",
"sys_export_templates",
"conditions",
"join_templates",
"sys_params",
"exa_files",
"exa_customers",
"exa_file_chunks",
"exa_file_upload_and_downloads",
"account_ding_talk_extends",
"app_request_test_batches",
"app_request_tests",
"system_integrations",
"forwarding_extends",
"batch_workflows",
"batch_workflow_tasks",
"sys_user_global_codes",
}
for _, table := range tables {
sequenceName := fmt.Sprintf("%s_id_seq", table)
// 检查序列是否已存在
var exists bool
checkSQL := "SELECT EXISTS (SELECT 1 FROM pg_sequences WHERE sequencename = ?)"
if err := db.Raw(checkSQL, sequenceName).Scan(&exists).Error; err != nil {
log.Printf("检查序列 %s 是否存在时出错: %v", sequenceName, err)
continue
}
if !exists {
// 创建序列
createSQL := fmt.Sprintf("CREATE SEQUENCE IF NOT EXISTS %s START 1 INCREMENT 1", sequenceName)
if err := db.Exec(createSQL).Error; err != nil {
log.Printf("创建序列 %s 时出错: %v", sequenceName, err)
continue
}
// 将序列设置为表的默认值
alterSQL := fmt.Sprintf("ALTER TABLE %s ALTER COLUMN id SET DEFAULT nextval('%s')", table, sequenceName)
if err := db.Exec(alterSQL).Error; err != nil {
log.Printf("设置表 %s 的ID默认值时出错: %v", table, err)
continue
}
// 更新序列的当前值(如果表中已有数据)
updateSQL := fmt.Sprintf("SELECT setval('%s', COALESCE((SELECT MAX(id) FROM %s), 1), true)", sequenceName, table)
if err := db.Exec(updateSQL).Error; err != nil {
log.Printf("更新序列 %s 的当前值时出错: %v", sequenceName, err)
}
log.Printf("成功为表 %s 创建序列 %s", table, sequenceName)
}
}
}
+8
View File
@@ -26,6 +26,10 @@ func GormPgSql() *gorm.DB {
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(p.MaxIdleConns)
sqlDB.SetMaxOpenConns(p.MaxOpenConns)
// 为PostgreSQL创建必要的序列
//createPostgreSQLSequences(db)
return db
}
}
@@ -45,6 +49,10 @@ func GormPgSqlByConfig(p config.Pgsql) *gorm.DB {
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(p.MaxIdleConns)
sqlDB.SetMaxOpenConns(p.MaxOpenConns)
// 为PostgreSQL创建必要的序列
//createPostgreSQLSequences(db)
return db
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ type SysUser struct {
Authority SysAuthority `json:"authority" gorm:"foreignKey:AuthorityId;references:AuthorityId;comment:用户角色"` // 用户角色
Authorities []SysAuthority `json:"authorities" gorm:"many2many:sys_user_authority;"` // 多用户角色
Phone string `json:"phone" gorm:"comment:用户手机号"` // 用户手机号
Email string `json:"email" gorm:"comment:用户邮箱"` // 用户邮箱
Email string `json:"email" gorm:"index;comment:用户邮箱"` // 用户邮箱
Enable int `json:"enable" gorm:"default:1;comment:用户是否被冻结 1正常 2冻结"` //用户是否被冻结 1正常 2冻结
OriginSetting common.JSONMap `json:"originSetting" form:"originSetting" gorm:"type:text;default:null;column:origin_setting;comment:配置;"` //配置
}
-6
View File
@@ -128,12 +128,6 @@ func RegisterUser(u system.SysUser, token string) (err error) {
global.GVA_LOG.Debug("注册用户信息:", zap.Any("1", 1))
var acc gaia.Account
if err = global.GVA_DB.Where("email=?", u.Email).First(&acc).Error; err == nil {
// 用户已存在,更新密码
global.GVA_LOG.Info(fmt.Sprintf("account %s already exists, updating password", acc.Name))
global.GVA_DB.Model(&acc).Updates(&map[string]interface{}{
"password": passwordHashed,
"password_salt": salt,
})
return nil
}
// 默认以root执行
+22 -17
View File
@@ -1,7 +1,5 @@
package utils
import "math"
// InArray @author: [Fantasia](https://www.npc0.com)
// @function: InArray
// @description: 判断是否在数组中
@@ -52,20 +50,27 @@ func InStringArray(value string, array []string) (isIn bool) {
// @description: 字符串加星号
// @return: err error, conf config.Server
func AddAsteriskToString(s string) string {
// 计算要插入的星号数量
num := 0
stars := ""
// 计算插入位置
insertPos := len(s) / 2
numStars := int(math.Ceil(float64(len(s)) / 5))
for i := 0; i < numStars; i++ {
if num > 8 {
continue
}
stars += "*"
num += 1
// 处理空字符串或长度不足的情况
if len(s) == 0 {
return ""
}
// 插入星号
result := s[:insertPos] + stars + s[insertPos:]
return result
if len(s) == 1 {
return "*"
}
if len(s) == 2 {
return s[:1] + "*"
}
if len(s) <= 4 {
return s[:1] + "***" + s[len(s)-1:]
}
if len(s) <= 8 {
return s[:2] + "***" + s[len(s)-2:]
}
// 保留前6个字符和后6个字符,中间用星号替换
prefix := s[:6]
suffix := s[len(s)-6:]
middle := "********"
return prefix + middle + suffix
}
@@ -198,7 +198,7 @@ const config = ref({
user_name_field: "",
user_email_field: "",
user_id_field: "",
scope: "",
scope: "openid profile email",
token_auth_method: "client_secret_post",
redirect_uri: "",
discovery_url: "",
@@ -249,10 +249,11 @@ const testConnection = async () => {
const base = config.value.server_url || ''
const authPath = config.value.authorize_url || ''
let authorizeUrl = authPath.startsWith('http://') || authPath.startsWith('https://') ? authPath : `${base}${authPath}`
let redirectUriRaw = config.value.redirect_uri || `${location.protocol}//${location.host}/api/base/auth2/callback`
let redirectUriRaw = config.value.redirect_uri || `${location.protocol}//${location.host}/admin/api/base/auth2/callback`
let redirectUri = encodeURIComponent(redirectUriRaw)
let authorizeLike = authorizeUrl.includes('?') ? '&' : '?'
let scope = encodeURIComponent(config.value.scope || 'openid profile email')
window.open(`${authorizeUrl}?client_id=${encodeURIComponent(config.value.app_id)}&response_type=code&scope=${scope}&redirect_uri=${redirectUri}`)
window.open(`${authorizeUrl}${authorizeLike}&client_id=${encodeURIComponent(config.value.app_id)}&response_type=code&scope=${scope}&redirect_uri=${redirectUri}`)
}
const initForm = async() => {
+34 -5
View File
@@ -73,6 +73,8 @@ class OAuthCallback(Resource):
if not oauth_provider:
return {"error": "Invalid provider"}, 400
# extend: 兼容casdoor
id_token = None
code = request.args.get("code")
state = request.args.get("state")
# Fallback: some providers may return tokens directly in query (implicit/hybrid flow)
@@ -95,12 +97,38 @@ class OAuthCallback(Resource):
invite_token = state
try:
token = token_from_query or oauth_provider.get_access_token(code) # type: ignore[arg-type]
if token_from_query is not None:
token = token_from_query
else:
# Extend: Start 兼容casdoor
response_json = oauth_provider.get_access_token(code)
token = response_json.get("access_token")
if not token:
return {"error": f"Error in OAuth: {response_json}"}, 502
id_token = response_json.get("id_token")
# Extend: Stop 兼容casdoor
# 检查token是否有效
if not token or token.strip() == "":
logger.error("OAuth2 access token is empty for provider %s", provider)
return {"error": "OAuth2 access token is empty or invalid"}, 400
user_info = oauth_provider.get_user_info(token)
except requests.RequestException as e:
error_text = e.response.text if e.response else str(e)
logger.exception("An error occurred during the OAuth process with %s: %s", provider, error_text)
return {"error": "OAuth process failed"}, 400
error_status = e.response.status_code if e.response else "Unknown"
logger.exception("An error occurred during the OAuth process with %s (Status: %s): %s",
provider, error_status, error_text)
# 提供更具体的错误信息
if error_status == 401:
return {"error": f"OAuth2 authentication failed (401 Unauthorized): {error_text}"}, 400
elif error_status == 400:
return {"error": f"OAuth2 bad request (400): {error_text}"}, 400
else:
return {"error": f"OAuth process failed (Status: {error_status}): {error_text}"}, 400
except ValueError as e:
logger.exception("OAuth2 configuration error for provider %s: %s", provider, str(e))
return {"error": f"OAuth2 configuration error: {str(e)}"}, 400
if invite_token and RegisterService.is_valid_invite_token(invite_token):
invitation = RegisterService._get_invitation_by_token(token=invite_token)
@@ -146,9 +174,10 @@ class OAuthCallback(Resource):
account=account,
ip_address=extract_remote_ip(request),
)
# extend: 兼容casdoor
return redirect(
f"{dify_config.CONSOLE_WEB_URL}?access_token={token_pair.access_token}&refresh_token={token_pair.refresh_token}"
f"{dify_config.CONSOLE_WEB_URL}?access_token={token_pair.access_token}"
f"&refresh_token={token_pair.refresh_token}&id_token={id_token}"
)
+1 -1
View File
@@ -106,7 +106,7 @@ class TextApi(Resource):
}
)
@validate_app_token(fetch_user_arg=FetchUserArg(fetch_from=WhereisUserArg.JSON))
def post(self, app_model: App, end_user: EndUser):
def post(self, app_model: App, end_user: EndUser, api_token: ApiToken): # 二开部分End - 密钥额度限制,新增api_token
"""Convert text to audio using text-to-speech.
Converts the provided text to audio using the specified voice.
@@ -30,6 +30,7 @@ from libs import helper
from libs.helper import uuid_value
from models.model import ApiToken, App, AppMode, EndUser # 二开部分End - 密钥额度限制,新增ApiToken
from services.app_generate_service import AppGenerateService
from services.app_generate_service_extend import AppGenerateServiceExtend
from services.errors.app import IsDraftWorkflowError, WorkflowIdFormatError, WorkflowNotFoundError
from services.errors.llm import InvokeRateLimitError
@@ -204,6 +205,13 @@ class ChatApi(Resource):
# # ------------------- 二开部分End - 密钥额度限制 -------------------
try:
# ------------------- 二开部分Begin - 添加使用统计 -------------------
AppGenerateServiceExtend.calculate_cumulative_usage(
app_model=app_model,
args=args,
) # Extend: App
# ------------------- 二开部分End - 添加使用统计 -------------------
response = AppGenerateService.generate(
app_model=app_model, user=end_user, args=args, invoke_from=InvokeFrom.SERVICE_API, streaming=streaming
)
@@ -673,10 +673,10 @@ class WorkflowAppGenerateTaskPipeline:
if isinstance(
event,
(
QueueNodeFailedEvent,
QueueNodeInIterationFailedEvent,
QueueNodeInLoopFailedEvent,
QueueNodeExceptionEvent,
QueueNodeFailedEvent,
QueueNodeInIterationFailedEvent,
QueueNodeInLoopFailedEvent,
QueueNodeExceptionEvent,
),
):
yield from self._handle_node_failed_events(
+4 -5
View File
@@ -36,7 +36,6 @@ from tasks.extend.update_account_money_when_workflow_node_execution_created_exte
update_account_money_when_workflow_node_execution_created_extend,
)
# 二开部分End - 密钥额度限制
@dataclass
@@ -215,9 +214,9 @@ class WorkflowCycleManager:
self,
*,
event: QueueNodeFailedEvent
| QueueNodeInIterationFailedEvent
| QueueNodeInLoopFailedEvent
| QueueNodeExceptionEvent,
| QueueNodeInIterationFailedEvent
| QueueNodeInLoopFailedEvent
| QueueNodeExceptionEvent,
) -> WorkflowNodeExecution:
"""
Workflow node execution failed
@@ -361,7 +360,7 @@ class WorkflowCycleManager:
node_exec
for node_exec in self._node_execution_cache.values()
if node_exec.workflow_execution_id == workflow_execution_id
and node_exec.status == WorkflowNodeExecutionStatus.RUNNING
and node_exec.status == WorkflowNodeExecutionStatus.RUNNING
]
for node_execution in running_node_executions:
+43 -31
View File
@@ -66,13 +66,7 @@ class GitHubOAuth(OAuth):
headers = {"Accept": "application/json"}
response = requests.post(self._TOKEN_URL, data=data, headers=headers)
response_json = response.json()
access_token = response_json.get("access_token")
if not access_token:
raise ValueError(f"Error in GitHub OAuth: {response_json}")
return access_token
return response.json()
def get_raw_user_info(self, token: str):
headers = {"Authorization": f"token {token}"}
@@ -120,13 +114,7 @@ class GoogleOAuth(OAuth):
headers = {"Accept": "application/json"}
response = requests.post(self._TOKEN_URL, data=data, headers=headers)
response_json = response.json()
access_token = response_json.get("access_token")
if not access_token:
raise ValueError(f"Error in Google OAuth: {response_json}")
return access_token
return response.json()
def get_raw_user_info(self, token: str):
headers = {"Authorization": f"Bearer {token}"}
@@ -239,9 +227,7 @@ class OaOAuth(OAuth):
'response_type': 'code',
'redirect_uri': dify_config.CONSOLE_API_URL + "/console/api/oauth/authorize/oauth2",
'client_id': integration.app_id,
# 重要:未设置 scope 时,Casdoor /api/userinfo 仅返回 openid 最小字段
# 从配置读取 scope,默认请求更完整的信息
'scope': (config.get('scope') if isinstance(config, dict) and config.get('scope') else 'openid profile email'),
'scope': config.get('scope'),
}
if invite_token:
params['state'] = invite_token
@@ -249,7 +235,7 @@ class OaOAuth(OAuth):
endpoints = self._resolve_endpoints(config)
auth_url = endpoints.get('authorize_url')
return f"{auth_url}?{query_string}"
return f"{auth_url}{'&' if "?" in auth_url else '?'}{query_string}"
def get_access_token(self, code: str):
auto2_conf = self.get_auto2_conf()
@@ -281,17 +267,12 @@ class OaOAuth(OAuth):
if not code:
return ""
response = requests.post(token_url, data=data, headers=headers, auth=auth)
response = requests.post(token_url, data=data, headers=headers, auth=auth, timeout=30)
response.encoding = "utf-8"
if response.status_code != 200:
return ""
try:
response_json = response.json()
except:
return ""
access_token = response_json.get("access_token")
return access_token
return response.json()
def get_raw_user_info(self, token: str):
auto2_conf = self.get_auto2_conf()
@@ -299,14 +280,45 @@ class OaOAuth(OAuth):
return ""
config = auto2_conf.get('config')
endpoints = self._resolve_endpoints(config)
headers = {"Authorization": f"Bearer {token}"}
userinfo_url = endpoints.get('userinfo_url')
response = requests.get(userinfo_url, headers=headers)
response.raise_for_status()
return response.json()
# 检查token是否为空
if not token or token.strip() == "":
raise ValueError("OAuth2 access token is empty or invalid")
# 尝试不同的Authorization header格式
auth_formats = [
f"Bearer {token}",
f"Token {token}",
token
]
last_error = None
for auth_header in auth_formats:
try:
headers = {"Authorization": auth_header}
response = requests.get(f"{endpoints.get('userinfo_url')}", headers=headers, timeout=30)
if response.status_code == 200:
return response.json()
elif response.status_code == 401:
last_error = f"401 Unauthorized: {response.text}"
continue
else:
last_error = f"HTTP {response.status_code}: {response.text}"
continue
except requests.RequestException as e:
last_error = str(e)
continue
# 如果所有格式都失败,抛出最后一个错误
if last_error:
raise requests.RequestException(f"All authentication formats failed. Last error: {last_error}")
else:
raise requests.RequestException("Failed to get user info with any authentication format")
def _transform_user_info(self, raw_info: dict) -> OAuthUserInfo:
# 检查 raw_info 是否为空或为 None
auto2_conf = self.get_auto2_conf()
if not raw_info or not isinstance(raw_info, dict) or auto2_conf.get('integration') is None:
@@ -0,0 +1,62 @@
"""add_account_money_extend_unique_constraint
Revision ID: 012_account_money_extend_unique
Revises: 011_system_integration_fields
Create Date: 2025-10-21 18:00:00.000000
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.engine.reflection import Inspector
# revision identifiers, used by Alembic.
revision = '012_account_money_extend_unique'
down_revision = '011_system_integration_fields'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
conn = op.get_bind()
inspector = Inspector.from_engine(conn)
tables = inspector.get_table_names()
if 'account_money_extend' in tables:
# 首先删除重复数据,只保留每个account_id中updated_at最大的记录
conn.execute(sa.text("""
DELETE FROM account_money_extend
WHERE id NOT IN (
SELECT DISTINCT ON (account_id) id
FROM account_money_extend
ORDER BY account_id, updated_at DESC
)
"""))
# 删除现有的普通索引
with op.batch_alter_table('account_money_extend', schema=None) as batch_op:
try:
batch_op.drop_index('idx_account_money_account_id')
except Exception:
# 如果索引不存在,忽略错误
pass
# 创建唯一约束
with op.batch_alter_table('account_money_extend', schema=None) as batch_op:
batch_op.create_unique_constraint('idx_account_money_account_id_unique', ['account_id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
if 'account_money_extend' in tables:
with op.batch_alter_table('account_money_extend', schema=None) as batch_op:
try:
batch_op.drop_constraint('idx_account_money_account_id_unique', type_='unique')
except Exception:
# 如果约束不存在,忽略错误
pass
# 重新创建普通索引
batch_op.create_index('idx_account_money_account_id', ['account_id'], unique=False)
# ### end Alembic commands ###
+1 -1
View File
@@ -7,7 +7,7 @@ class AccountMoneyExtend(db.Model):
__tablename__ = "account_money_extend"
__table_args__ = (
db.PrimaryKeyConstraint("id", name="account_money_pkey"),
db.Index("idx_account_money_account_id", "account_id"),
db.UniqueConstraint("account_id", name="idx_account_money_account_id_unique"),
)
id = db.Column(StringUUID, server_default=db.text("uuid_generate_v4()"))
@@ -88,7 +88,7 @@ class DatabaseRecommendAppRetrieval(RecommendAppRetrievalBase):
category = class_dick[classId]
recommended_app_result = {
"id": recommended_app.id,
"app": recommended_app.app,
"app": recommended_app.app,
"app_id": recommended_app.app_id,
"description": description,
"copyright": site.copyright,
+2 -3
View File
@@ -1,4 +1,3 @@
from flask_login import current_user
from configs import dify_config
@@ -37,8 +36,8 @@ class WorkspaceService:
tenant_extend_service = TenantExtendService
super_admin_id = tenant_extend_service.get_super_admin_id().id
super_admin_tenant_id = tenant_extend_service.get_super_admin_tenant_id().id
tenant_info["admin_extend"] = (super_admin_id == current_user.id)
tenant_info["tenant_extend"] = (super_admin_tenant_id == tenant.id)
tenant_info["admin_extend"] = super_admin_id == current_user.id
tenant_info["tenant_extend"] = super_admin_tenant_id == tenant.id
# ----------------------- 二开部分Stop 添加用户权限 - ----------------------
can_replace_logo = FeatureService.get_features(tenant.id).can_replace_logo
+8 -8
View File
@@ -582,7 +582,7 @@ x-shared-env: &shared-api-worker-env
services:
# API service
api:
image: ccr.ccs.tencentyun.com/yfgaia/dify-plus-api:1.8.1
image: ccr.ccs.tencentyun.com/yfgaia/dify-plus-api:1.8.1.fix
restart: always
environment:
# Use the shared environment variables.
@@ -611,7 +611,7 @@ services:
# worker service
# The Celery worker for processing the queue.
worker:
image: ccr.ccs.tencentyun.com/yfgaia/dify-plus-api:1.8.1
image: ccr.ccs.tencentyun.com/yfgaia/dify-plus-api:1.8.1.fix
restart: always
environment:
# Use the shared environment variables.
@@ -636,7 +636,7 @@ services:
# worker-gaia service
# The Celery worker-gaia for processing the queue.
worker-gaia:
image: ccr.ccs.tencentyun.com/yfgaia/dify-plus-api:1.8.1
image: ccr.ccs.tencentyun.com/yfgaia/dify-plus-api:1.8.1.fix
restart: always
environment:
# Use the shared environment variables.
@@ -661,7 +661,7 @@ services:
# worker-dataset service
# The Celery worker-dataset for processing the queue.
worker-dataset:
image: ccr.ccs.tencentyun.com/yfgaia/dify-plus-api:1.8.1
image: ccr.ccs.tencentyun.com/yfgaia/dify-plus-api:1.8.1.fix
restart: always
environment:
# Use the shared environment variables.
@@ -686,7 +686,7 @@ services:
# beat service
# The Celery worker for schedule tasks.
beat:
image: ccr.ccs.tencentyun.com/yfgaia/dify-plus-api:1.8.1
image: ccr.ccs.tencentyun.com/yfgaia/dify-plus-api:1.8.1.fix
restart: always
environment:
# Use the shared environment variables.
@@ -710,7 +710,7 @@ services:
# Frontend web application.
web:
image: ccr.ccs.tencentyun.com/yfgaia/dify-plus-web:1.8.1
image: ccr.ccs.tencentyun.com/yfgaia/dify-plus-web:1.8.1.fix.3
restart: always
environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@@ -1375,7 +1375,7 @@ services:
# Extend - admin-web
admin-web:
image: ccr.ccs.tencentyun.com/yfgaia/dify-plus-admin-web:1.8.1
image: ccr.ccs.tencentyun.com/yfgaia/dify-plus-admin-web:1.8.1.fix
restart: always
ports:
- '8081:8081'
@@ -1387,7 +1387,7 @@ services:
# Extend - admin-server
admin-server:
image: ccr.ccs.tencentyun.com/yfgaia/dify-plus-admin-server:1.8.1
image: ccr.ccs.tencentyun.com/yfgaia/dify-plus-admin-server:1.8.1.fix
restart: always
environment:
# JWT signing key must match API's SECRET_KEY for token compatibility
+3 -3
View File
@@ -348,7 +348,9 @@ const AppCard = ({ app, onRefresh, onApp }: AppCardProps) => {
<button className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickInstalledApp}>
<span className='system-sm-regular text-text-secondary'>{t('app.openInExplore')}</span>
</button>
{/* <>------start SyncToAppTemplate-------</> */}
</>
}
{/* <>------start SyncToAppTemplate-------</> */}
{(userProfile.admin_extend && userProfile.tenant_extend && !onApp) && (
<>
<Divider className="!my-1"/>
@@ -372,8 +374,6 @@ const AppCard = ({ app, onRefresh, onApp }: AppCardProps) => {
</>
)}
{/* <>------start SyncToAppTemplate-------</> */}
</>
}
<Divider className="my-1" />
{
systemFeatures.webapp_auth.enabled && isCurrentWorkspaceEditor && <>
+18 -1
View File
@@ -122,6 +122,22 @@ const List = () => {
{ value: 'completion', text: t('app.types.completion'), icon: <RiFile4Line className='mr-1 h-[14px] w-[14px]' /> },
]
// Extend: start 取消同步至应用模板
const recommendedApps = data?.at(-1)?.recommended_apps ?? [] // app recommended apps[]string
useEffect(() => {
const hasMore = data?.at(-1)?.has_more ?? true
let observer: IntersectionObserver | undefined
if (anchorRef.current) {
observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !isLoading && hasMore)
setSize((size: number) => size + 1)
}, { rootMargin: '100px' })
observer.observe(anchorRef.current)
}
return () => observer?.disconnect()
}, [isLoading, setSize, anchorRef, mutate, data])
// Extend: stop 取消同步至应用模板
useEffect(() => {
if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
@@ -213,7 +229,8 @@ const List = () => {
{isCurrentWorkspaceEditor
&& <NewAppCard ref={newAppCardRef} onSuccess={mutate} />}
{data.map(({ data: apps }) => apps.map(app => (
<AppCard key={app.id} app={app} onRefresh={mutate} />
// Extend: 取消同步至应用模板
<AppCard key={app.id} app={app} onRefresh={mutate} onApp={recommendedApps.includes(app.id)} />
)))}
</div>
: <div className='relative grid grow grid-cols-1 content-start gap-4 overflow-hidden px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6'>
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
import { RiKey2Line } from '@remixicon/react'
import Button from '@/app/components/base/button'
import SecretKeyModal from '@/app/components/develop/secret-key/secret-key-modal'
import {useAppContext} from "@/context/app-context";
type ISecretKeyButtonProps = {
className?: string
@@ -12,8 +13,11 @@ type ISecretKeyButtonProps = {
}
const SecretKeyButton = ({ className, appId, textCls }: ISecretKeyButtonProps) => {
const { isCurrentWorkspaceManager } = useAppContext()
const [isVisible, setVisible] = useState(false)
const { t } = useTranslation()
if (!isCurrentWorkspaceManager)
return ""
return (
<>
<Button
@@ -40,8 +40,8 @@ const AppCard = ({
}
return (
<div className={cn('group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-lg shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg')}>
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
<div className={cn('group relative col-span-1 flex cursor-pointer flex-col overflow-hidden rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pb-2 shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg')}>
<div className='flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pb-3 pt-[14px]'>
<div className='relative shrink-0'>
<AppIcon
size='large'
@@ -50,7 +50,7 @@ const AppCard = ({
background={app.app.icon_background}
imageUrl={app.app.icon_url}
/>
<span className='absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'>
<span className='absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'>
{appBasicInfo.mode === 'advanced-chat' && (
<ChatBot className='w-3 h-3 text-[#1570EF]' />
)}
@@ -69,7 +69,7 @@ const AppCard = ({
</span>
</div>
<div className='grow w-0 py-[1px]'>
<div className='flex items-center text-sm leading-5 font-semibold text-gray-800'>
<div className='flex items-center text-sm font-semibold leading-5 text-text-secondary'>
<div className='truncate' title={appBasicInfo.name}>{appBasicInfo.name}</div>
</div>
<div className='flex items-center text-[10px] leading-[18px] text-gray-500 font-medium'>
@@ -186,8 +186,8 @@ const Apps = ({
return (
<div className={cn(
'flex flex-col',
pageType === PageType.EXPLORE ? 'h-full border-l border-gray-200' : 'h-[calc(100%-56px)]',
'flex flex-col border-divider-regular',
pageType === PageType.EXPLORE ? 'h-full border-l' : 'h-[calc(100%-56px)]',
)}>
{pageType === PageType.EXPLORE && (
<div className='shrink-0 pt-6 px-12'>
@@ -225,7 +225,7 @@ const Apps = ({
{/* extend: Application Center Search Stop */}
</div>
<div className={cn(
'relative flex flex-1 pb-6 flex-col overflow-auto bg-gray-100 shrink-0 grow',
'relative flex flex-1 pb-6 flex-col overflow-auto shrink-0 grow',
pageType === PageType.EXPLORE ? 'mt-6' : 'mt-0 pt-2',
)}
>
@@ -68,11 +68,15 @@ export default function AppSelector() {
if (localStorage?.getItem('conversationIdInfo'))
localStorage.removeItem('conversationIdInfo')
// 二开部分 - End 解决切换账号对话记录不存在问题
let id_token = null
if (localStorage) {
id_token = localStorage.getItem('logout_id_token')
}
// Start: Automatic login/logout Extend
console.log(systemFeatures, 2344)
if (window.location !== undefined && `${systemFeatures.is_custom_auth2_logout}` !== '' && systemFeatures.is_custom_auth2_logout !== undefined)
window.location.href = `${systemFeatures.is_custom_auth2_logout}&redirect_url=${window.location.href}`
let logout_url = systemFeatures.is_custom_auth2_logout
let is_connector = logout_url.includes('?') ? '&' : '?'
if (window.location !== undefined && logout_url !== '')
window.location.href = `${logout_url}${is_connector}id_token_hint=${id_token}&redirect_url=${window.location.href}`
// Stop: Automatic login/logout Extend
router.push('/signin')
}
@@ -5,7 +5,7 @@ import type { UserMoney } from '@/models/common-extend'
import cn from 'classnames'
const AccountMoneyExtend = () => {
const [userMoney, setUserMoney] = useState<UserMoney>({ used_quota: 0, total_quota: 15 })
const [userMoney, setUserMoney] = useState<UserMoney>({ used_quota: 0, total_quota: 0 })
const [isFetched, setIsFetched] = useState(false)
const exchangeRate = 7.26 // 美元转人民币固定汇率
@@ -24,9 +24,13 @@ const AccountMoneyExtend = () => {
// 计算额度(确保使用数字类型)
const usedQuota = Number(userMoney.used_quota) || 0
const totalQuota = Number(userMoney.total_quota) || 15
const totalQuota = Number(userMoney.total_quota) || 0
const remainingQuota = totalQuota - usedQuota
// 当总额度为0时不显示
if (totalQuota === 0)
return null
// 转换为人民币并保留2位小数
const usedRMB = (usedQuota * exchangeRate).toFixed(2)
const totalRMB = (totalQuota * exchangeRate).toFixed(2)
+7
View File
@@ -19,6 +19,13 @@ const SwrInitializer = ({
}: SwrInitializerProps) => {
const router = useRouter()
const searchParams = useSearchParams()
// extend: start 兼容casdoor 退出登录
const tokenId = searchParams.get('id_token')
if (tokenId && tokenId !== 'None') {
if (window.location !== undefined)
localStorage?.setItem('logout_id_token', tokenId)
}
// extend: stop 兼容casdoor 退出登录
const consoleToken = decodeURIComponent(searchParams.get('access_token') || '')
const refreshToken = decodeURIComponent(searchParams.get('refresh_token') || '')
// Extend Start DingTalk login compatible
+8
View File
@@ -2,6 +2,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import useSWR from 'swr'
import { fetchAppList } from '@/service/apps'
import { createContext, useContext, useContextSelector } from 'use-context-selector'
import type { FC, ReactNode } from 'react'
import { fetchCurrentWorkspace, fetchLangGeniusVersion, fetchUserProfile } from '@/service/common'
@@ -10,6 +11,7 @@ import MaintenanceNotice from '@/app/components/header/maintenance-notice'
import { noop } from 'lodash-es'
export type AppContextValue = {
mutateApps: VoidFunction
userProfile: UserProfileResponse
mutateUserProfile: VoidFunction
currentWorkspace: ICurrentWorkspace
@@ -57,6 +59,7 @@ const initialWorkspaceInfo: ICurrentWorkspace = {
}
const AppContext = createContext<AppContextValue>({
mutateApps: noop,
userProfile: userProfilePlaceholder,
currentWorkspace: initialWorkspaceInfo,
isCurrentWorkspaceManager: false,
@@ -79,6 +82,7 @@ export type AppContextProviderProps = {
}
export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => {
const { mutate: mutateApps } = useSWR({ url: '/apps', params: { } }, fetchAppList)
const { data: userProfileResponse, mutate: mutateUserProfile, error: userProfileError } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile)
const { data: currentWorkspaceResponse, mutate: mutateCurrentWorkspace, isLoading: isLoadingCurrentWorkspace } = useSWR({ url: '/workspaces/current', params: {} }, fetchCurrentWorkspace)
@@ -123,8 +127,12 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
setCurrentWorkspace(currentWorkspaceResponse)
}, [currentWorkspaceResponse])
if (!userProfile)
return <Loading type='app' />
return (
<AppContext.Provider value={{
mutateApps,
userProfile,
mutateUserProfile,
langGeniusVersionInfo,
+11 -11
View File
@@ -101,19 +101,19 @@
"lexical": "^0.30.0",
"line-clamp": "^1.0.0",
"lodash-es": "^4.17.21",
"mermaid": "11.4.1",
"mermaid": "11.10.0",
"mime": "^4.0.4",
"mitt": "^3.0.1",
"negotiator": "^0.6.3",
"next": "15.5.0",
"next": "~15.5.9",
"next-themes": "^0.4.3",
"papaparse": "^5.5.3",
"pinyin-pro": "^3.25.0",
"qrcode.react": "^4.2.0",
"qs": "^6.13.0",
"react": "19.1.1",
"react": "19.2.3",
"react-18-input-autosize": "^3.0.0",
"react-dom": "19.1.1",
"react-dom": "19.2.3",
"react-easy-crop": "^5.1.0",
"react-error-boundary": "^4.1.2",
"react-headless-pagination": "^1.1.6",
@@ -164,9 +164,9 @@
"@happy-dom/jest-environment": "^17.4.4",
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@next/bundle-analyzer": "15.5.0",
"@next/eslint-plugin-next": "15.5.0",
"@next/mdx": "15.5.0",
"@next/bundle-analyzer": "15.5.9",
"@next/eslint-plugin-next": "15.5.9",
"@next/mdx": "15.5.9",
"@rgrove/parse-xml": "^4.1.0",
"@storybook/addon-essentials": "8.5.0",
"@storybook/addon-interactions": "8.5.0",
@@ -188,8 +188,8 @@
"@types/negotiator": "^0.6.3",
"@types/node": "18.15.0",
"@types/qs": "^6.9.16",
"@types/react": "19.1.11",
"@types/react-dom": "19.1.7",
"@types/react": "~19.2.7",
"@types/react-dom": "~19.2.3",
"@types/react-slider": "^1.3.6",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-window": "^1.8.8",
@@ -226,8 +226,8 @@
"uglify-js": "^3.19.3"
},
"resolutions": {
"@types/react": "19.1.11",
"@types/react-dom": "19.1.7",
"@types/react": "~19.2.7",
"@types/react-dom": "~19.2.3",
"string-width": "4.2.3"
},
"lint-staged": {
+915 -868
View File
File diff suppressed because it is too large Load Diff