OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)

This commit is contained in:
wangruiguo
2026-09-03 18:49:20 +08:00
commit d647428529
3501 changed files with 1988906 additions and 0 deletions

34
.dev/dev-apache-site.conf Normal file
View File

@@ -0,0 +1,34 @@
<VirtualHost *:8080>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html/public
# Pass the Authorization header through to PHP so $request->bearerToken() works.
# Without this, Apache strips it (it only lands in HTTP_AUTHORIZATION), Laravel's
# Sanctum guard never sees the Bearer token, and the test env silently exercises a
# different auth path than production — which is how the 3.9.x Bearer role regression
# slipped past CI. With it, BearerApiCest goes through the real Sanctum-guard path.
<Directory /var/www/html/public>
CGIPassAuth On
</Directory>
ErrorLog /var/www/html/storage/logs/apacheError.log
CustomLog /var/www/html/storage/logs/access.log combined
</VirtualHost>
<VirtualHost *:443>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html/public
SSLEngine on
SSLCertificateFile /etc/ssl/certs/cert.pem
SSLCertificateKeyFile /etc/ssl/private/key.pem
SetEnv HTTPS "on"
<Directory /var/www/html/public>
CGIPassAuth On
</Directory>
ErrorLog /var/www/html/storage/logs/apacheError.log
CustomLog /var/www/html/storage/logs/access.log combined
</VirtualHost>

225
.dev/dev-apache2.conf Normal file
View File

@@ -0,0 +1,225 @@
# This is the main Apache server configuration file. It contains the
# configuration directives that give the server its instructions.
# See http://httpd.apache.org/docs/2.4/ for detailed information about
# the directives and /usr/share/doc/apache2/README.Debian about Debian specific
# hints.
#
#
# Summary of how the Apache 2 configuration works in Debian:
# The Apache 2 web server configuration in Debian is quite different to
# upstream's suggested way to configure the web server. This is because Debian's
# default Apache2 installation attempts to make adding and removing modules,
# virtual hosts, and extra configuration directives as flexible as possible, in
# order to make automating the changes and administering the server as easy as
# possible.
# It is split into several files forming the configuration hierarchy outlined
# below, all located in the /etc/apache2/ directory:
#
# /etc/apache2/
# |-- apache2.conf
# | `-- ports.conf
# |-- mods-enabled
# | |-- *.load
# | `-- *.conf
# |-- conf-enabled
# | `-- *.conf
# `-- sites-enabled
# `-- *.conf
#
#
# * apache2.conf is the main configuration file (this file). It puts the pieces
# together by including all remaining configuration files when starting up the
# web server.
#
# * ports.conf is always included from the main configuration file. It is
# supposed to determine listening ports for incoming connections which can be
# customized anytime.
#
# * Configuration files in the mods-enabled/, conf-enabled/ and sites-enabled/
# directories contain particular configuration snippets which manage modules,
# global configuration fragments, or virtual host configurations,
# respectively.
#
# They are activated by symlinking available configuration files from their
# respective *-available/ counterparts. These should be managed by using our
# helpers a2enmod/a2dismod, a2ensite/a2dissite and a2enconf/a2disconf. See
# their respective man pages for detailed information.
#
# * The binary is called apache2. Due to the use of environment variables, in
# the default configuration, apache2 needs to be started/stopped with
# /etc/init.d/apache2 or apache2ctl. Calling /usr/bin/apache2 directly will not
# work with the default configuration.
# Global configuration
#
#
# ServerRoot: The top of the directory tree under which the server's
# configuration, error, and log files are kept.
#
# NOTE! If you intend to place this on an NFS (or otherwise network)
# mounted filesystem then please read the Mutex documentation (available
# at <URL:http://httpd.apache.org/docs/2.4/mod/core.html#mutex>);
# you will save yourself a lot of trouble.
#
# Do NOT add a slash at the end of the directory path.
#
#ServerRoot "/etc/apache2"
#
# The accept serialization lock file MUST BE STORED ON A LOCAL DISK.
#
#Mutex file:${APACHE_LOCK_DIR} default
#
# The directory where shm and other runtime files will be stored.
#
DefaultRuntimeDir ${APACHE_RUN_DIR}
#
# PidFile: The file in which the server should record its process
# identification number when it starts.
# This needs to be set in /etc/apache2/envvars
#
PidFile ${APACHE_PID_FILE}
#
# Timeout: The number of seconds before receives and sends time out.
#
Timeout 300
#
# KeepAlive: Whether or not to allow persistent connections (more than
# one request per connection). Set to "Off" to deactivate.
#
KeepAlive On
#
# MaxKeepAliveRequests: The maximum number of requests to allow
# during a persistent connection. Set to 0 to allow an unlimited amount.
# We recommend you leave this number high, for maximum performance.
#
MaxKeepAliveRequests 100
#
# KeepAliveTimeout: Number of seconds to wait for the next request from the
# same client on the same connection.
#
KeepAliveTimeout 5
# These need to be set in /etc/apache2/envvars
User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}
#
# HostnameLookups: Log the names of clients or just their IP addresses
# e.g., www.apache.org (on) or 204.62.129.132 (off).
# The default is off because it'd be overall better for the net if people
# had to knowingly turn this feature on, since enabling it means that
# each client request will result in AT LEAST one lookup request to the
# nameserver.
#
HostnameLookups Off
# ErrorLog: The location of the error log file.
# If you do not specify an ErrorLog directive within a <VirtualHost>
# container, error messages relating to that virtual host will be
# logged here. If you *do* define an error logfile for a <VirtualHost>
# container, that host's errors will be logged there and not here.
#
ErrorLog /var/www/html/storage/logs/apacheError.log
#
# LogLevel: Control the severity of messages logged to the error_log.
# Available values: trace8, ..., trace1, debug, info, notice, warn,
# error, crit, alert, emerg.
# It is also possible to configure the log level for particular modules, e.g.
# "LogLevel info ssl:warn"
#
LogLevel warn
# Include module configuration:
IncludeOptional mods-enabled/*.load
IncludeOptional mods-enabled/*.conf
# Include list of ports to listen on
Include ports.conf
# Sets the default security model of the Apache2 HTTPD server. It does
# not allow access to the root filesystem outside of /usr/share and /var/www.
# The former is used by web applications packaged in Debian,
# the latter may be used for local directories served by the web server. If
# your system is serving content from a sub-directory in /srv you must allow
# access here, or in any related virtual host.
<Directory />
Options FollowSymLinks
AllowOverride None
Require all denied
</Directory>
<Directory /usr/share>
AllowOverride None
Require all granted
</Directory>
<Directory /var/www/>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
#<Directory /srv/>
# Options Indexes FollowSymLinks
# AllowOverride None
# Require all granted
#</Directory>
# AccessFileName: The name of the file to look for in each directory
# for additional configuration directives. See also the AllowOverride
# directive.
#
AccessFileName .htaccess
#
# The following lines prevent .htaccess and .htpasswd files from being
# viewed by Web clients.
#
<FilesMatch "^\.ht">
Require all denied
</FilesMatch>
#
# The following directives define some format nicknames for use with
# a CustomLog directive.
#
# These deviate from the Common Log Format definitions in that they use %O
# (the actual bytes sent including headers) instead of %b (the size of the
# requested file), because the latter makes it impossible to detect partial
# requests.
#
# Note that the use of %{X-Forwarded-For}i instead of %h is not recommended.
# Use mod_remoteip instead.
#
LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %O" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
# Include of directories ignores editors' and dpkg's backup files,
# see README.Debian for details.
# Include generic snippets of statements
IncludeOptional conf-enabled/*.conf
# Include the virtual host configurations:
IncludeOptional sites-enabled/*.conf
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

15
.dev/dev-ports.conf Normal file
View File

@@ -0,0 +1,15 @@
# If you just change the port or add more ports here, you will likely also
# have to change the VirtualHost statement in
# /etc/apache2/sites-enabled/000-default.conf
Listen 8080
<IfModule ssl_module>
Listen 443
</IfModule>
<IfModule mod_gnutls.c>
Listen 443
</IfModule>
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

View File

@@ -0,0 +1,27 @@
#
# This docker compose file is used to build the environment for our testing framework in codeception
# It is not to be used for development
#
services:
leantime-dev:
volumes:
- "../:/var/www/html"
- "./xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini"
- "./error_reporting.ini:/usr/local/etc/php/conf.d/error_reporting.ini"
- "./test.env:/var/www/html/config/.env"
ports:
- "8002:8080"
- "44302:443"
environment:
LEAN_ENV: 'testing'
LEAN_DB_HOST: 'leantime-db'
LEAN_DB_DATABASE: 'leantime_test'
env_file:
- test.env
depends_on:
leantime-db:
condition: service_healthy
leantime-db:
environment:
MYSQL_DATABASE: leantime_test

110
.dev/docker-compose.yaml Normal file
View File

@@ -0,0 +1,110 @@
services:
leantime-dev:
container_name: leantime-dev
build: .
ports:
- "5080:8080" #Using ports that are hopefully available. format is: 80XX
- "5443:443" #Using ports that are hopefully available. format is: 443XX (available range is 4430044399)
extra_hosts:
- "host.docker.internal:host-gateway"
# - "authentik.docker.local:172.28.0.2"
volumes:
- "../:/var/www/html"
- "./xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini"
- "./error_reporting.ini:/usr/local/etc/php/conf.d/error_reporting.ini"
environment:
LEAN_DB_HOST: 'leantime-db'
LEAN_DB_USER: 'leantime'
LEAN_DB_PASSWORD: 'leantime'
LEAN_DB_DATABASE: 'leantime'
LEAN_DB_PORT: '3306'
depends_on:
leantime-db:
condition: service_healthy
networks:
- leantime_internal
- leantime
# - traefik
# labels:
# - "traefik.enable=true"
# - "traefik.priority=2"
# - "traefik.docker.network=traefik"
#
# # HTTP Router
# - "traefik.http.routers.leantime.rule=Host(`leantime.docker.local`)"
# - "traefik.http.services.leantime.loadbalancer.server.port=8080"
# - "traefik.http.routers.leantime.tls=true"
leantime-redis:
container_name: leantime-redis
image: redis:4.0
ports:
- "6379"
networks:
- leantime_internal
leantime-db:
image: mysql:8.4
ports:
- "3306"
environment:
MYSQL_ROOT_PASSWORD: 'leantime'
MYSQL_USER: 'leantime'
MYSQL_PASSWORD: 'leantime'
MYSQL_DATABASE: 'leantime'
networks:
- leantime_internal
volumes:
- leantime-mysql:/var/lib/mysql:rw
healthcheck:
test: "mysql -u$$MYSQL_USER -p$$MYSQL_PASSWORD -e 'SELECT 1'"
interval: 5s
timeout: 5s
retries: 20
selenium:
image: selenium/standalone-chromium
shm_size: 2gb
environment:
- SCREEN_WIDTH=2560
- SCREEN_HEIGHT=1440
ports:
- "4444"
- "7900"
networks:
- leantime_internal
maildev:
image: maildev/maildev
environment:
- MAILDEV_SMTP_PORT=465
- MAILDEV_WEB_PORT=8081
ports:
- "8081"
networks:
- leantime_internal
s3ninja:
image: scireum/s3-ninja
ports:
- "8083"
networks:
- leantime_internal
volumes:
- s3ninja-data:/home/sirius/data
networks:
leantime_internal:
driver: bridge
name: leantime_internal
leantime:
driver: bridge
# traefik:
# external: true
# name: traefik
volumes:
leantime-mysql:
s3ninja-data:

68
.dev/dockerfile Normal file
View File

@@ -0,0 +1,68 @@
FROM php:8.2-apache
COPY ./dev-apache2.conf /etc/apache2/apache2.conf
COPY ./dev-ports.conf /etc/apache2/ports.conf
# Ensure packages are available.
RUN apt-get update
RUN DEBIAN_FRONTEND=noninteractive \
apt install -fqy \
libonig-dev \
libcurl4-openssl-dev \
libxml2-dev \
libxslt1-dev \
libzip-dev \
libjson-c-dev \
libldap-dev \
libargon2-dev \
libfreetype6-dev \
libjpeg62-turbo-dev \
libpng-dev \
libpq-dev \
apt-utils \
vim \
curl \
sqlite3 \
openssl \
default-mysql-client \
iputils-ping \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# xdebug (coverage) + redis (cache) are optional for the test/dev image — the test env runs on
# file cache/session (see .dev/test.env). Don't let an intermittent pecl.php.net outage
# ("No releases available for ...") fail the whole image build and block all CI.
RUN pecl install xdebug redis || echo "pecl unavailable — building without xdebug/redis"
RUN docker-php-ext-configure gd --enable-gd --with-jpeg=/usr/include/ --with-freetype --with-jpeg
RUN docker-php-ext-install \
gd \
mysqli \
pdo_mysql \
pdo_pgsql \
mbstring \
exif \
pcntl \
pdo \
bcmath \
opcache \
ldap \
zip
# zip is always installed above; xdebug/redis only if pecl was reachable. Enable each one
# independently and best-effort, so a missing extension (partial pecl outage) doesn't prevent
# enabling the one that IS present.
RUN docker-php-ext-enable zip \
&& (docker-php-ext-enable xdebug || echo "xdebug not installed, skipping enable") \
&& (docker-php-ext-enable redis || echo "redis not installed, skipping enable")
COPY ./dev-apache-site.conf /etc/apache2/sites-enabled/000-default.conf
RUN openssl req -x509 -newkey rsa:4096 -keyout /etc/ssl/private/key.pem -out /etc/ssl/certs/cert.pem -days 365 -nodes -subj "/C=US/ST=NY/L=NY/O=ACME/OU=CD/CN=AcmeWPDeveloper"
RUN a2enmod ssl
RUN a2enmod rewrite
VOLUME /var/www/uploads
RUN mkdir -p /var/www/uploads \
&& chown -R www-data:www-data /var/www/uploads

3
.dev/error_reporting.ini Normal file
View File

@@ -0,0 +1,3 @@
error_reporting=E_ALL
display_errors=On
phar.readonly=Off

View File

@@ -0,0 +1,583 @@
<?php
namespace Leantime\Core\Configuration;
use Leantime\Core\Configuration\Attributes\LaravelConfig;
/**
* Default Configuration Class
*/
class DefaultConfig
{
// General =====================================================================================
/**
* @var string Name of your site, can be changed later
*/
#[LaravelConfig('app.name')]
public string $sitename = 'OneBot';
/**
* @var string Default language
*/
#[LaravelConfig('app.locale')]
public string $language = 'en-US';
/**
* @var string Default logo path, can be changed later
*/
public string $logoPath = '/dist/images/logo.svg';
/**
* @var string Default logo URL use for printing (must be jpg or png format)
*/
public string $printLogoURL = '/dist/images/logo.jpg';
/**
* @var string Base URL, trailing slash not needed
*/
#[LaravelConfig('app.url')]
public string $appUrl = '';
/**
* @var string Base of application withotu trailing slash (used for cookies), e.g, /leantime
*/
public string $appDir = '';
/**
* @var bool Send anonymous data <a href='https://docs.leantime.io/#/using-leantime/company-settings?id=telemetry' target='_blank'>More Info</a>.
* No personally identifiable data will be sent and it will be impossible for us to track individual users.
*/
public bool $allowTelemetry = true;
/**
* @var string Default theme
*/
public string $defaultTheme = 'default';
/**
* @var string Primary Theme color
*/
public string $primarycolor;
/**
* @var string Secondary Theme Color
*/
public string $secondarycolor;
/**
* @var string Default timezone
*/
#[LaravelConfig('app.timezone')]
public string $defaultTimezone = 'America/Los_Angeles';
/**
* @var bool Enable to specifiy menu on a project by project basis
*/
public bool $enableMenuType = false;
/**
* @var bool|int Debug flag
*/
#[LaravelConfig('app.debug')]
public int|bool $debug = 0;
/**
* @var bool When true, a denied #[RequiresPermission] attribute blocks the request
* (403 web / JSON-RPC -32001). When false the central enforcer only LOGS the
* would-be denial (audit mode), so permission coverage can be rolled out and
* observed before being enforced. In-method $this->authorize() calls always
* enforce regardless of this flag.
*/
#[LaravelConfig('permissions.enforce')]
public bool $permissionsEnforce = true;
/**
* @var string editor used for code editing
*/
public string $editor = 'phpstorm';
/**
* @var string Application environment
*/
#[LaravelConfig('app.env')]
public string $env = 'production';
/**
* @var string Log Path
*/
public string $logPath = APP_ROOT.'/storage/logs/error.log';
/**
* @var bool Whether or not to enable the Poor Man's Cron fallback
*/
public bool $poorMansCron = true;
/**
* @var bool Don't show user/pass form on login?
*/
public bool $disableLoginForm = false;
// Database ====================================================================================
/**
* @var string Database host
*/
public string $dbHost = 'localhost';
/**
* @var string Database username
*/
public string $dbUser = '';
/**
* @var string Database password
*/
public string $dbPassword = '';
/**
* @var string Database name
*/
public string $dbDatabase = '';
/**
* @var string Database port
*/
public string $dbPort = '3306';
// Fileupload ==================================================================================
/**
* @var string Local relative path to store uploaded files (if not using S3)
*/
public string $userFilePath = 'userfiles/';
/**
* @var string Local relative path to store backup files, need permission to write
*/
public string $dbBackupPath = 'userfiles/';
// S3 configuration ============================================================================
/**
* @var bool Set to true if you want to use S3 instead of local files
*/
public bool $useS3 = false;
/**
* @var string S3 Key
*/
public string $s3Key = '';
/**
* @var string S3 Secret
*/
public string $s3Secret = '';
/**
* @var string S3 Bucket
*/
public string $s3Bucket = '';
/**
* @var bool false => https://[bucket].[endpoint] ; true => https://[endpoint]/[bucket]
*/
public bool $s3UsePathStyleEndpoint = false;
/**
* @var string S3 Region
*/
public string $s3Region = '';
/**
* @var string S3 Foldername within S3 (can be empty)
*/
public string $s3FolderName = '';
/**
* @var string|null S3 EndPoint S3 Compatible
*
* @see https://sfo2.digitaloceanspaces.com
*/
public ?string $s3EndPoint = null;
// Sessions ====================================================================================
/**
* @var string Salting sessions. Replace with a strong password
*/
#[LaravelConfig('app.key')]
public string $sessionPassword = '3evBlq9zdUEuzKvVJHWWx3QzsQhturBApxwcws2m';
/**
* @var int How many minutes after inactivity should we logout? 480min = 8hours
*/
public int $sessionExpiration = 480;
/**
* @var bool. Sets whether the cookie should only be served via https
*/
#[LaravelConfig('session.secure')]
public bool $sessionSecure = false;
// Email =======================================================================================
/**
* @var string Return email address
*/
public string $email = '';
/**
* @var bool Use SMTP? If set to false, the default php mail() function will be used
*/
public bool $useSMTP = false;
/**
* @var string SMTP host
*/
public string $smtpHosts = '';
/**
* @var bool SMTP use user/password authentication
*/
public bool $smtpAuth = true;
/**
* @var string SMTP username
*/
public string $smtpUsername = '';
/**
* @var string SMTP password
*/
public string $smtpPassword = '';
/**
* @var bool SMTP Enable TLS encryption automatically if a server supports it
*/
public bool $smtpAutoTLS = true;
/**
* @var string SMTP Security protocol (usually one of: TLS, SSL, STARTTLS)
*/
public string $smtpSecure = '';
/**
* @var bool SMTP Allow insecure SSL: Don't verify certificate, accept self-signed, etc.
*/
public bool $smtpSSLNoverify = false;
/**
* @var int SMTP Port (usually one of 25, 465, 587, 2526)
*/
public int $smtpPort = 587;
// ldap default settings (can be changed in company settings) ==================================
/**
* @var bool Set to true if you want to use LDAP
*/
public bool $useLdap = false;
/**
* @var string Select the correct directory type. Currently Supported: OL - OpenLdap, AD - Active Directory
*/
public string $ldapType = 'OL';
/**
* @var string LDAP host (FQDN)
*/
public string $ldapHost = '';
/**
* @var int LDAP port
*/
public int $ldapPort = 389;
/**
* @var string LDAP domain
*/
public string $ldapDomain = '';
/**
* @var string LDAP base DN
*/
public string $ldapUri = '';
/**
* @var string Location of users, example: CN=users,DC=example,DC=com
*/
public string $ldapDn = '';
/**
* @var string Default LDAP keys in your directory. Works for OL
*/
public string $ldapKeys = '{
"username":"uid",
"groups":"memberof",
"email":"mail",
"firstname":"displayname",
"lastname":"",
"phone":"",
"jobTitle":"title",
"jobLevel":"level",
"department":"department"
}';
// For AD use
/*
public $ldapKeys = '{
"username":"cn",
"groups":"memberof",
"email":"mail",
"firstname":"givenname",
"lastname":"sn",
"phone":"telephoneNumber",
"jobTitle":"title",
"jobLevel":"level",
"department":"department"
}';
*/
/**
* @var bool Create users
* Create user if not exists
*/
public bool $ldapCreateUser = false;
/**
* @var string Default role assignments upon first login. (Optional) Can be updated in user settings for each user
*/
public string $ldapLtGroupAssignments = '{
"5": {
"ltRole":"readonly",
"ldapRole":""
},
"10": {
"ltRole":"commenter",
"ldapRole":""
},
"20": {
"ltRole":"editor",
"ldapRole":""
},
"30": {
"ltRole":"manager",
"ldapRole":""
},
"40": {
"ltRole":"admin",
"ldapRole":""
},
"50": {
"ltRole":"owner",
"ldapRole":"administrators"
}
}';
// Default Leantime Role on creation. (set to editor)
/**
* @var int Default Leantime Role on creation. (set to editor)
*/
public int $ldapDefaultRoleKey = 20;
// Plugin Settings ==============================================================================
/**
* @var string Comma separated list of plugins that will always be loaded
*/
public string $plugins = '';
/**
* @var string The Url of the Marketplace
**/
public string $marketplaceUrl = 'https://marketplace.leantime.io/';
// OIDC Settings ================================================================================
/**
* @var bool Set to true if you want to use OIDC
*/
public bool $oidcEnable = false;
/**
* @var string OIDC Provider URL
*/
public string $oidcProviderUrl = '';
/**
* @var string OIDC Client ID
*/
public string $oidcClientId = '';
/**
* @var string OIDC Client Secret
*/
public string $oidcClientSecret = '';
/**
* @var string Custom Auto discover URL
*/
public string $oidcAutoDiscoverUrl = '';
/**
* @var string OIDC Auth URL
*/
public string $oidcAuthUrl = '';
/**
* @var string OIDC Token URL
*/
public string $oidcTokenUrl = '';
/**
* @var string OIDC JWKS URL
*/
public string $oidcJwksUrl = '';
/**
* @var string OIDC User Info URL
*/
public string $oidcUserInfoUrl = '';
/**
* @var string OIDC Certificate String
*/
public string $oidcCertificateString = '';
/**
* @var string OIDC Certificate File
*/
public string $oidcCertificateFile = '';
/**
* @var string OIDC Scopes
*/
public string $oidcScopes = 'openid profile email';
/**
* @var bool create user
*
* Create user if not exists
*/
public bool $oidcCreateUser = false;
/**
* @var int OIDC
*
* Default Role for new users
*/
public int $oidcDefaultRole = 20;
/**
* @var string OIDC Field Email
*/
public string $oidcFieldEmail = 'email';
/**
* @var string OIDC Field First Name
*/
public string $oidcFieldFirstName = 'given_name';
/**
* @var string OIDC Field Last Name
*/
public string $oidcFieldLastName = 'family_name';
/**
* @var string OIDC Field Phone
*/
public string $oidcFieldPhone = '';
/**
* @var string OIDC Field Job Title
*/
public string $oidcFieldJobtitle = '';
/**
* @var string OIDC Field Job Level
*/
public string $oidcFieldJoblevel = '';
/**
* @var string OIDC Field Department
*/
public string $oidcFieldDepartment = '';
// Redis Settings ===============================================================================
/**
* @var bool Set to true if you want to use Redis
*/
public bool $useRedis = false;
/**
* @var bool Set to true if you want to use a redis cluster
*/
public bool $useCluster = false;
/**
* @var string Redis URL
*/
#[LaravelConfig('redis.default.url')]
public string $redisUrl = '';
/**
* @var string Redis Host
*/
#[LaravelConfig('redis.default.host')]
public string $redisHost = '127.0.0.1';
/**
* @var string Redis Port
*/
#[LaravelConfig('redis.default.port')]
public string $redisPort = '6379';
/**
* @var string Redis Password
*/
#[LaravelConfig('redis.default.password')]
public string $redisPassword = '';
/**
* @var string Redis Password
*/
#[LaravelConfig('redis.default.tls')]
public string $redisScheme = 'tls';
// Security/Rate Limiting Settings ===============================================================================
/**
* @var string trusted Proxies
*/
public string $trustedProxies = '127.0.0.1,REMOTE_ADDR';
/**
* @var int rate limit on all requests
*/
public int $ratelimitGeneral = 2000;
/**
* @var int rate limit on API requests (per user+IP per minute). 120 = 2 req/s sustained —
* enough for mobile-app sync bursts and integration polling while still catching
* runaway scripts; in line with comparable tools (GitHub ~83/min, Jira ~100/min).
*/
public int $ratelimitApi = 120;
/**
* @var int rate limit on auth requests
*/
public int $ratelimitAuth = 20;
/**
* @var int rate limit on MCP endpoint requests (per user+IP per minute). Higher than the API
* limit because agentic LLM clients burst many parallel tool calls per turn.
*/
public int $ratelimitMcp = 300;
/**
* @var int rate limit on signup + user-invite POSTs (per IP per minute). These endpoints
* send email and provision resources, so they get a tight budget (invite-spam abuse).
*/
public int $ratelimitSignup = 5;
/**
* @var int maximum user invites per inviting user per hour
*/
public int $ratelimitInvitesUser = 10;
/**
* @var int maximum user invites per installation per day. Raise for legitimate bulk
* onboarding (CSV/directory imports).
*/
public int $ratelimitInvitesTenant = 30;
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Leantime\Views\Composers;
use Leantime\Core\UI\Composer;
use Leantime\Core\UI\Theme;
class Entry extends Composer
{
public static array $views = [
'global::layouts.entry',
];
private Theme $themeCore;
public function init(Theme $themeCore): void
{
$this->themeCore = $themeCore;
}
public function with(): array
{
$this->themeCore->getActive();
$logoUrl = $this->themeCore->getLogoUrl();
return [
'logoPath' => $logoUrl ?: BASE_URL.'/dist/images/logo.svg',
];
}
}

View File

@@ -0,0 +1,328 @@
<?php
namespace Leantime\Core;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Support\Facades\Log;
use Leantime\Core\Configuration\Environment;
use Leantime\Core\Events\DispatchesEvents;
use Leantime\Core\Support\NameSanitizer;
use PHPMailer\PHPMailer\Exception;
use PHPMailer\PHPMailer\PHPMailer;
/**
* Mail class - mails with php mail()
*
* @version 1.0
*
* @license GNU/AGPL-3.0, see license.txt
*/
class Mailer
{
use DispatchesEvents;
public string $cc;
public string $bcc;
public string $text = '';
public string $subject;
public string $context;
private PHPMailer $mailAgent;
/**
* @var string
*/
private mixed $emailDomain;
private Language $language;
private string $logo;
private string $companyColor;
private string $html;
private bool $hideWrapper = false;
public bool $nl2br = true;
/**
* __construct - get configurations
*
* @return void
*/
public function __construct(Environment $config, Language $language)
{
if ($config->email != '') {
$this->emailDomain = $config->email;
} else {
$host = $_SERVER['HTTP_HOST'] ?? 'leantime';
$this->emailDomain = 'no-reply@'.$host;
}
$this->emailDomain = self::dispatch_filter('fromEmail', $this->emailDomain, $this);
// PHPMailer
$this->mailAgent = new PHPMailer(false);
$this->mailAgent->CharSet = 'UTF-8'; // Ensure UTF-8 is used for emails
// Use SMTP or php mail().
if (filter_var($config->useSMTP, FILTER_VALIDATE_BOOLEAN) === true) {
if ($config->debug) {
$this->mailAgent->SMTPDebug = 4; // ensure all aspects (connection, TLS, SMTP, etc) are covered
$this->mailAgent->Debugoutput = function ($str, $level) {
Log::debug($level.' '.$str);
};
} else {
$this->mailAgent->SMTPDebug = 0;
}
$this->mailAgent->Timeout = 20;
$this->mailAgent->isSMTP(); // Set mailer to use SMTP
$this->mailAgent->Host = $config->smtpHosts; // Specify main and backup SMTP servers
if (isset($config->smtpAuth)) {
$this->mailAgent->SMTPAuth = filter_var($config->smtpAuth, FILTER_VALIDATE_BOOLEAN); // Enable SMTP user/password authentication
} else {
$this->mailAgent->SMTPAuth = true;
}
$this->mailAgent->Username = $config->smtpUsername; // SMTP username
$this->mailAgent->Password = $config->smtpPassword; // SMTP password
$this->mailAgent->SMTPAutoTLS = $config->smtpAutoTLS ?? true; // Enable TLS encryption automatically if a server supports it
$this->mailAgent->SMTPSecure = $config->smtpSecure; // Enable TLS encryption, `ssl` also accepted
$this->mailAgent->Port = (int) $config->smtpPort; // TCP port to connect to
if (isset($config->smtpSSLNoverify) && filter_var($config->smtpSSLNoverify, FILTER_VALIDATE_BOOLEAN) === true) { // If enabled, don't verify certifcates: accept self-signed or expired certs.
$this->mailAgent->SMTPOptions = [
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true,
],
];
}
} else {
$this->mailAgent->isMail();
}
$this->logo = ! session()->has('companysettings.logoPath') ? '/dist/images/logo_blue.png' : session('companysettings.logoPath');
$this->companyColor = ! session()->has('companysettings.primarycolor') ? '#006c9e' : session('companysettings.primarycolor');
$this->language = $language;
}
/**
* setContext - sets the context for the mailing
* (used for filters & events)
*/
public function setContext($context): void
{
$this->context = $context;
}
/**
* setText - sets the mailtext
*/
public function setText($text): void
{
$this->text = $text;
}
/**
* setHTML - set Mail html (no function yet)
*/
public function setHtml($html, bool $hideWrapper = false): void
{
$this->hideWrapper = $hideWrapper;
$this->html = $html;
}
/**
* setSubject - set mail subject
*/
public function setSubject($subject): void
{
$this->subject = $subject;
}
/**
* dispatchMailerEvent - dispatches a mailer event
*/
private function dispatchMailerEvent($hookname, $payload, array $additional_params = []): void
{
$this->dispatchMailerHook('event', $hookname, $payload, $additional_params);
}
/**
* dispatchMailerFilter - dispatches a mailer filter
*/
private function dispatchMailerFilter($hookname, $payload, array $additional_params = []): mixed
{
return $this->dispatchMailerHook('filter', $hookname, $payload, $additional_params);
}
/**
* dispatchMailerHook - dispatches a mailer hook
*
* @throws BindingResolutionException
*/
private function dispatchMailerHook($type, $hookname, $payload, array $additional_params = []): mixed
{
if ($type !== 'filter' && $type !== 'event') {
return false;
}
$hooks = [$hookname];
if (! empty($this->context)) {
$hooks[] = "$hookname.{$this->context}";
}
$filteredValue = null;
foreach ($hooks as $hook) {
if ($type == 'filter') {
$filteredValue = self::dispatch_filter($hook, $payload, $additional_params);
} elseif ($type == 'event') {
self::dispatch_event($hook, $payload);
}
}
if ($type == 'filter') {
return $filteredValue;
}
return null;
}
/**
* sendMail - send the mail with mail()
*
* @throws Exception
*/
public function sendMail(array $to, $from): void
{
$this->dispatchMailerEvent('beforeSendMail', []);
$to = $this->dispatchMailerFilter('sendMailTo', $to, []);
$from = $this->dispatchMailerFilter('sendMailFrom', $from, []);
$this->mailAgent->isHTML(true); // Set email format to HTML
// The From display name must never carry user-controlled content (invite-spam abuse
// used attacker firstnames here). Callers pass fixed labels; sanitize regardless.
$fromName = NameSanitizer::clean($from);
$fromDisplay = ($fromName === '' || strcasecmp($fromName, 'Leantime') === 0)
? 'Leantime'
: $fromName.' (Leantime)';
$this->mailAgent->setFrom($this->emailDomain, $fromDisplay);
$this->mailAgent->Subject = $this->subject;
if (str_contains($this->logo, 'images/logo.svg')) {
$this->logo = '/dist/images/logo_blue.png';
}
$logoParts = parse_url($this->logo);
if (isset($logoParts['scheme'])) {
// Logo is URL
$inlineLogoContent = $this->logo;
} else {
if (file_exists(ROOT.''.$this->logo) && $this->logo != '' && is_file(ROOT.''.$this->logo)) {
// Logo comes from local file system
$this->mailAgent->addEmbeddedImage(ROOT.''.$this->logo, 'companylogo');
} else {
$this->mailAgent->addEmbeddedImage(ROOT.'/dist/images/logo_blue.png', 'companylogo');
}
$inlineLogoContent = 'cid:companylogo';
}
$mailBody = $this->hideWrapper ? $this->html : app('blade.compiler')::render(
$this->dispatchMailerFilter('bodyTemplate', '<table width="100%" style="background:#fefefe; padding:15px; ">
<tr>
<td align="center" valign="top">
<table width="600" style="width:600px; background-color:#ffffff; border:1px solid #ccc; border-radius:5px;">
<tr>
<td style="padding:20px 10px; text-align:center;">
<img alt="Logo" src="{!! $inlineLogoContent !!}" width="150" style="width:150px;">
</td>
</tr>
<tr>
<td style=\'padding:10px; font-family:"Lato","Helvetica Neue",helvetica,sans-serif; color:#666; font-size:16px; line-height:1.7;\'>
{!! $headline !!}
<br/>
{!! $content !!}
<br/><br/>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center" style=\'padding:10px; font-family:"Lato","Helvetica Neue",helvetica,sans-serif; color:#666; font-size:14px; line-height:1.7;\'>
{!! $unsub_link !!}
</td>
</tr>
</table>'),
$this->dispatchMailerFilter(
'mailBodyParams',
[
'inlineLogoContent' => $inlineLogoContent,
'headline' => $this->language->__('email_notifications.hi'),
'content' => $this->nl2br ? nl2br($this->html) : $this->html,
'unsub_link' => sprintf($this->language->__('email_notifications.unsubscribe'), BASE_URL.'/users/editOwn/'),
]
)
);
$mailBody = $this->dispatchMailerFilter(
'bodyContent',
$mailBody,
[
[
'companyColor' => $this->companyColor,
'logoUrl' => $inlineLogoContent,
'languageHiText' => $this->language->__('email_notifications.hi'),
'emailContentsHtml' => nl2br($this->html),
'unsubLink' => sprintf($this->language->__('email_notifications.unsubscribe'), BASE_URL.'/users/editOwn/'),
],
]
);
$this->mailAgent->Body = $mailBody;
$altBody = $this->dispatchMailerFilter(
'altBody',
$this->text,
[]
);
$this->mailAgent->AltBody = $altBody;
if (is_array($to)) {
$to = array_unique($to);
foreach ($to as $recip) {
try {
$this->mailAgent->addAddress($recip);
$this->mailAgent->send();
} catch (Exception $e) {
Log::error($this->mailAgent->ErrorInfo);
Log::error($e);
}
$this->mailAgent->clearAllRecipients();
}
}
$this->dispatchMailerEvent('afterSendMail', $to);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html dir="{{ __('language.direction') }}" lang="{{ __('language.code') }}">
<head>
@include('global::sections.header')
@stack('styles')
</head>
<body class="" hx-ext="preload" hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'>
@include('global::sections.appAnnouncement')
<div class="mainwrapper menu{{ session("menuState") ?? "closed" }}">
<div class="header">
<div class="headerinner">
<a class="btnmenu" href="javascript:void(0);"></a>
<a class="barmenu" href="javascript:void(0);">
<span class="fa fa-bars"></span>
</a>
<div class="logo">
<a
href="{{ BASE_URL }}"
style="background-image: url('{{ BASE_URL }}/dist/images/logo.svg')"
>&nbsp;</a>
</div>
@include('menu::headMenu')
</div><!-- headerinner -->
</div><!-- header -->
<div class="overlay" style="position: relative">
<div class="leftpanel">
<div class="leftmenu">
@include('menu::menu')
</div><!-- leftmenu -->
</div>
<div class="rightpanel {{ $section }}">
<div class="primaryContent">
@isset($action, $module)
@include("$module::$action")
@else
@yield('content')
@endisset
<div class="clearfix"></div>
@include('global::sections.footer')
</div>
</div>
</div><!-- rightpanel -->
<div class="menu-backdrop" aria-hidden="true"></div>
</div><!-- mainwrapper -->
@include('global::sections.pageBottom')
@stack('scripts')
@include('help::helpermodal')
</body>
</html>

View File

@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html dir="{{ __('language.direction') }}" lang="{{ __('language.code') }}">
<head>
@include('global::sections.header')
<style>
.leantimeLogo { position: fixed; bottom: 10px; right: 10px; }
</style>
@stack('styles')
</head>
<body class="loginpage" style="height:100%;" hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'>
<div class="header hidden-gt-sm tw-p-[10px]" style="background:var(--header-gradient)">
<a href="{!! BASE_URL !!}" target="_blank">
<img src="{{ BASE_URL }}/dist/images/logo.svg" class="tw-h-full "/>
</a>
</div>
<div class="row" style="min-height:100vh; max-width: 98vw; height: auto;">
<div class="col-md-4 hidden-phone regLeft">
<div class="logo">
<a href="{!! BASE_URL !!}" target="_blank">
<img src="{{ BASE_URL }}/dist/images/logo.svg" />
</a>
</div>
<div class="welcomeContent">
@dispatchFilter('welcomeText', '<h1 class="mainWelcome">'.$language->__("headlines.welcome_back").'</h1>')
</div>
@dispatchFilter('belowWelcomeText', '')
</div>
<div class="col-md-8 col-sm-12 regRight">
<div class="regpanel">
<div class="regpanelinner">
@if($logoPath != '')
<a href="{!! BASE_URL !!}" target="_blank">
@if(!str_ends_with($logoPath, "dist/images/logo.svg" ))
<img src="{{ $logoPath }}" class="tw-h-full "/>
@endif
</a>
@endif
@isset($action, $module)
@include("$module::$action")
@else
@yield('content')
@endisset
</div>
</div>
</div>
<div class="leantimeLogo">
<img style="height: 25px;" src="{!! BASE_URL !!}/dist/images/logo-powered-by-leantime.png">
</div>
</div>
@include('global::sections.pageBottom')
@stack('scripts')
</body>
</html>

View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html dir="{{ __('language.direction') }}" lang="{{ __('language.code') }}">
<head>
@include('global::sections.header')
<style>
.leantimeLogo { position: fixed; bottom: 10px; right: 10px; }
</style>
@stack('styles')
</head>
<body class="loginpage" style="height:100%;" hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'>
<div class="header hidden-gt-sm tw-p-[10px]" style="background:var(--header-gradient)">
<a href="{!! BASE_URL !!}" target="_blank">
<img src="{{ BASE_URL }}/dist/images/logo.svg" class="tw-h-full "/>
</a>
</div>
<div class="row " style="height:100%; width: 99%;">
<div class="col-md-4 hidden-phone regLeft">
<div class="logo">
<a href="{!! BASE_URL !!}" target="_blank"><img src="{{ BASE_URL }}/dist/images/logo.svg" /></a>
</div>
<div class="welcomeContent">
<h1 class="mainWelcome">
Oops, something is off.
</h1>
</div>
</div>
<div class="col-md-8 col-sm-12 regRight">
<div class="regpanel">
<div class="regpanelinner">
@isset($action, $module)
@include("$module::$action")
@else
@yield('content')
@endisset
</div>
</div>
</div>
<div class="leantimeLogo">
<img style="height: 25px;" src="{!! BASE_URL !!}/dist/images/logo-powered-by-leantime.png">
</div>
</div>
@include('global::sections.pageBottom')
@stack('scripts')
</body>
</html>

74
.dev/onebot-bom-i18n.py Normal file
View File

@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
"""追加 BOM 模块的语言翻译 keyzh-CN 与 en-US"""
import os
root = r"d:\开发\leantime-master\leantime-master"
zh = '''
# BOM (OneBot)
menu.bom = "BOM"
menu.bom_tooltip = "BOM 物料清单"
text.bom_subtitle = "物料清单BOM管理"
notification.bom_not_found = "BOM 不存在"
label.new_bom = "新建 BOM"
text.no_boms = "暂无 BOM点击“新建 BOM”开始。"
label.bom_no = "BOM编号"
label.product_name = "产品名称"
label.specification = "规格型号"
label.drawing_no = "图号"
label.column_key = "列标识(英文或中文,用于存储)"
label.column_label = "列显示名"
label.add_row = "新增行"
label.add_column = "增加列"
label.teable_sources = "Teable 数据源"
text.no_sources = "未配置数据源"
label.source_name = "表名/别名"
text.teable_import_confirm = "从 Teable 拉取最新数据并导入为明细?"
text.imported = "已导入"
label.rows = ""
text.no_items = "暂无明细"
links.open = "打开"
links.back = "返回"
links.add = "添加"
'''
en = '''
# BOM (OneBot)
menu.bom = "BOM"
menu.bom_tooltip = "Bill of Materials"
text.bom_subtitle = "Bill of Materials (BOM) management"
notification.bom_not_found = "BOM not found"
label.new_bom = "New BOM"
text.no_boms = "No BOM yet. Click \"New BOM\" to start."
label.bom_no = "BOM No."
label.product_name = "Product Name"
label.specification = "Specification"
label.drawing_no = "Drawing No."
label.column_key = "Column key (used for storage)"
label.column_label = "Column label"
label.add_row = "Add Row"
label.add_column = "Add Column"
label.teable_sources = "Teable Sources"
text.no_sources = "No source configured"
label.source_name = "Table name / alias"
text.teable_import_confirm = "Fetch latest data from Teable and import as items?"
text.imported = "Imported"
label.rows = "rows"
text.no_items = "No items"
links.open = "Open"
links.back = "Back"
links.add = "Add"
'''
for code, block in (("zh-CN", zh), ("en-US", en)):
p = os.path.join(root, "app", "Language", code + ".ini")
with open(p, "r", encoding="utf-8") as f:
content = f.read()
if not content.endswith("\n"):
content += "\n"
content += block
with open(p, "w", encoding="utf-8") as f:
f.write(content)
print(code, "appended")

111
.dev/onebot-bom-lint.py Normal file
View File

@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
"""对改动过的 PHP/Blade 文件做静态检查(无 php CLI 时的替代):
跳过 /* */ 与 // 与 # 注释,再检查括号/引号平衡。
"""
import os
import re
root = r"d:\开发\leantime-master\leantime-master"
files = [
r"app\Domain\Bom\Repositories\Bom.php",
r"app\Domain\Bom\Services\Bom.php",
r"app\Domain\Bom\Services\Teable.php",
r"app\Domain\Bom\Services\Excel.php",
r"app\Domain\Bom\Permissions\BomPermissions.php",
r"app\Domain\Bom\Controllers\Show.php",
r"app\Domain\Bom\Controllers\Api.php",
r"app\Domain\Bom\routes.php",
r"app\Domain\Bom\Templates\show.blade.php",
r"app\Domain\Bom\Templates\detail.blade.php",
r"app\Domain\Install\Repositories\Install.php",
r"app\Domain\Install\Services\SchemaBuilder.php",
r"app\Domain\Menu\Repositories\Menu.php",
r"app\Views\Templates\components\pdfPreview.blade.php",
r"app\Views\Templates\sections\header.blade.php",
]
def strip_comments(src):
# 去掉 /* ... */ 和 // 行注释(保留换行结构便于定位)
out = []
i = 0
n = len(src)
while i < n:
c = src[i]
nxt = src[i + 1] if i + 1 < n else ''
if c == '/' and nxt == '*':
j = src.find('*/', i + 2)
out.append(' ' * (n if j == -1 else (j + 2 - i)))
i = n if j == -1 else j + 2
elif c == '/' and nxt == '/':
j = src.find('\n', i)
out.append(' ' * (n if j == -1 else (j - i)))
i = n if j == -1 else j
elif c == '#':
j = src.find('\n', i)
out.append(' ' * (n if j == -1 else (j - i)))
i = n if j == -1 else j
else:
out.append(c)
i += 1
return ''.join(out)
def check_balance(s):
pairs = {')': '(', '}': '{', ']': '['}
stack = []
in_s = in_d = False
i = 0
esc = False
while i < len(s):
ch = s[i]
if esc:
esc = False
i += 1
continue
if ch == '\\' and (in_s or in_d):
esc = True
i += 1
continue
if ch == "'" and not in_d:
in_s = not in_s
i += 1
continue
if ch == '"' and not in_s:
in_d = not in_d
i += 1
continue
if not in_s and not in_d:
if ch in '({[':
stack.append(ch)
elif ch in ')}]':
if not stack or stack[-1] != pairs[ch]:
return f"不匹配 '{ch}' 于位置 {i}"
stack.pop()
i += 1
if in_s:
return "单引号未闭合"
if in_d:
return "双引号未闭合"
if stack:
return f"未闭合 {stack[-1]}"
return None
all_ok = True
for f in files:
p = os.path.join(root, f)
if not os.path.exists(p):
print(f"[SKIP] {f} (不存在)")
continue
with open(p, encoding="utf-8") as fh:
src = fh.read()
stripped = strip_comments(src)
bal = check_balance(stripped)
if bal:
print(f"[FAIL] {f} <- {bal}")
all_ok = False
else:
print(f"[OK] {f}")
print("\n总判定:", "PASS" if all_ok else "存在需人工复核的项")

63
.dev/onebot-rebrand.py Normal file
View File

@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
"""OneBot rebrand helper: logo 生成 + 语言文件品牌名替换"""
import base64
import re
import glob
import os
import shutil
root = r"d:\开发\leantime-master\leantime-master"
img_dir = os.path.join(root, "public", "assets", "images")
bt = os.path.join(root, "bt.png")
# 1. 生成内嵌 bt.png 的 logo.svg / logo_blue.svg保持现有 .svg 引用不变,最小改动)
with open(bt, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="108" height="98" '
'viewBox="0 0 108 98"><image width="108" height="98" '
'href="data:image/png;base64,{}"/></svg>').format(b64)
for name in ("logo.svg", "logo_blue.svg"):
with open(os.path.join(img_dir, name), "w", encoding="utf-8") as f:
f.write(svg)
# printLogoURL 指向 logo.jpg原本缺失复制 bt.png 补齐
shutil.copyfile(bt, os.path.join(img_dir, "logo.jpg"))
# 2. 语言文件:仅替换 value 中的 Leantime/leantime保留翻译 key 与 *.leantime.io 域名
def repl(m):
s = m.group(0)
return "OneBot" if s[0].isupper() else "onebot"
# 匹配 Leantime不区分大小写排除后面紧跟 .io域名或字母更大单词
pat = re.compile(r"(?i)leantime(?!(\.io|[a-z]))")
changed_files = 0
changed_lines = 0
for ini in glob.glob(os.path.join(root, "app", "Language", "*.ini")):
with open(ini, "r", encoding="utf-8") as f:
lines = f.readlines()
out = []
file_changed = False
for line in lines:
if line.lstrip().startswith(("#", ";")):
out.append(line)
continue
if "=" not in line:
out.append(line)
continue
idx = line.index("=")
key, val = line[:idx], line[idx:]
new_val, n = pat.subn(repl, val)
if n:
file_changed = True
changed_lines += n
out.append(key + new_val)
if file_changed:
with open(ini, "w", encoding="utf-8") as f:
f.writelines(out)
changed_files += 1
print("logo.svg/logo_blue.svg/logo.jpg written")
print("language files changed:", changed_files, "| line replacements:", changed_lines)

View File

@@ -0,0 +1,56 @@
#!/bin/bash
set -e
APP_DIR=/var/www/onebot
SRC_TAR=/home/rui/onebot-src.tar.gz
echo "=== 1. 解压源码 ==="
sudo rm -rf "$APP_DIR"
sudo mkdir -p "$APP_DIR"
sudo tar -xzf "$SRC_TAR" -C "$APP_DIR" --strip-components=1
sudo chown -R "$USER":www-data "$APP_DIR"
echo "解压完成,目录内容:"
ls "$APP_DIR" | head -20
echo "=== 2. 初始化 MySQL 数据库 ==="
DB_NAME=onebot
DB_USER=onebot
DB_PASS='OneBot_Db_7xKpQ9z'
sudo mysql <<SQL
CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
FLUSH PRIVILEGES;
SQL
echo "数据库 ${DB_NAME} 创建完成,验证连接:"
mysql -u"$DB_USER" -p"$DB_PASS" -e "SELECT VERSION();" "$DB_NAME" 2>/dev/null && echo "DB_CONNECT_OK"
echo "=== 3. 创建 .env ==="
cat > "$APP_DIR/.env" <<ENV
APP_ENV=production
APP_DEBUG=false
APP_URL=http://119.29.53.45:8081
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=${DB_NAME}
DB_USERNAME=${DB_USER}
DB_PASSWORD=${DB_PASS}
LEAN_DB_HOST=127.0.0.1
LEAN_DB_USER=${DB_USER}
LEAN_DB_PASSWORD=${DB_PASS}
LEAN_DB_DATABASE=${DB_NAME}
LEAN_APP_URL=http://119.29.53.45:8081
LEAN_SITENAME='OneBot'
LEAN_LANGUAGE='zh-CN'
LEAN_SESSION_PASSWORD='OneBot_Session_4tHwLq8z'
ENV
echo ".env 写入完成:"
cat "$APP_DIR/.env"
echo "=== 全部完成 ==="

View File

@@ -0,0 +1,18 @@
#!/bin/bash
set -e
echo "=== 检查 mysql-server 包是否可用 ==="
apt-cache policy mysql-server 2>&1 | head -5
echo "=== 安装 mysql-server ==="
export DEBIAN_FRONTEND=noninteractive
apt-get install -y mysql-server 2>&1 | tail -30
echo "=== 检查安装结果 ==="
which mysql mysqld 2>&1 || echo "MYSQL_NOT_FOUND"
dpkg -l 2>/dev/null | grep -E 'mysql-server|mysql-common' | head
echo "=== 启动 mysql ==="
systemctl start mysql 2>&1 || true
sleep 3
systemctl is-active mysql 2>&1

View File

@@ -0,0 +1,21 @@
#!/bin/bash
set -e
DB_NAME=onebot
DB_USER=onebot
DB_PASS='OneBot_Db_7xKpQ9z'
echo "=== 创建数据库与用户 ==="
sudo mysql <<SQL
CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
ALTER USER '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}';
GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO '${DB_USER}'@'localhost';
FLUSH PRIVILEGES;
SQL
echo "=== 验证连接 ==="
mysql -u"$DB_USER" -p"$DB_PASS" -h127.0.0.1 -e "SELECT VERSION();" "$DB_NAME" && echo "DB_CONNECT_OK"
echo "=== 确认 .env 存在 ==="
ls -la /var/www/onebot/.env

View File

@@ -0,0 +1,48 @@
#!/bin/bash
set -e
APP_DIR=/var/www/onebot
DB_NAME=onebot
DB_USER=onebot
DB_PASS='OneBot_Db_7xKpQ9z'
echo "=== 写入 .env ==="
cat > "$APP_DIR/.env" <<ENV
APP_ENV=production
APP_DEBUG=false
APP_URL=http://119.29.53.45:8081
LEAN_APP_URL=http://119.29.53.45:8081
LEAN_APP_DIR=
LEAN_DEBUG=0
LEAN_DB_HOST=127.0.0.1
LEAN_DB_PORT=3306
LEAN_DB_USER=${DB_USER}
LEAN_DB_PASSWORD=${DB_PASS}
LEAN_DB_DATABASE=${DB_NAME}
LEAN_SESSION_PASSWORD='OneBot_Session_4tHwLq8z'
LEAN_SESSION_EXPIRATION=480
LEAN_SITENAME='OneBot'
LEAN_LANGUAGE='zh-CN'
LEAN_DEFAULT_TIMEZONE='Asia/Shanghai'
LEAN_LOGO_PATH='/dist/images/logo.svg'
LEAN_PRINT_LOGO_URL='/dist/images/logo.png'
LEAN_EMAIL_RETURN=''
LEAN_USE_S3=false
LEAN_USER_FILE_PATH='userfiles/'
LEAN_DB_BACKUP_PATH='backupdb/'
LEAN_USE_REDIS=false
LEAN_NEWS_ENABLED=false
ENV
echo "=== .env 内容 ==="
cat "$APP_DIR/.env"
echo "=== 权限 ==="
sudo chown -R rui:www-data "$APP_DIR"
sudo chmod -R 775 "$APP_DIR/storage" "$APP_DIR/bootstrap/cache" 2>/dev/null || true
echo "DONE"

View File

@@ -0,0 +1,14 @@
#!/bin/bash
set -e
APP_DIR=/var/www/onebot
LOG=/tmp/composer_install.log
echo "=== composer install 开始 ==="
cd "$APP_DIR"
sudo -u rui composer install --no-dev --no-interaction --optimize-autoloader > "$LOG" 2>&1
echo "COMPOSER_EXIT=$?"
echo "=== 日志尾部 ==="
tail -20 "$LOG"
echo "=== vendor 目录大小 ==="
du -sh "$APP_DIR/vendor" 2>/dev/null

View File

@@ -0,0 +1,13 @@
#!/bin/bash
set -e
APP_DIR=/var/www/onebot
LOG=/tmp/npm_install.log
export PATH="/home/rui/.nvm/versions/node/v24.12.0/bin:$PATH"
cd "$APP_DIR"
echo "npm install 开始 $(date)" > "$LOG"
node -v >> "$LOG" 2>&1
npm -v >> "$LOG" 2>&1
npm install --no-audit --no-fund >> "$LOG" 2>&1
echo "NPM_EXIT=$?" >> "$LOG"
echo "完成 $(date)" >> "$LOG"

View File

@@ -0,0 +1,8 @@
#!/bin/bash
set -e
cp /home/rui/onebot-nginx.conf /etc/nginx/sites-available/onebot
ln -sf /etc/nginx/sites-available/onebot /etc/nginx/sites-enabled/onebot
nginx -t
systemctl reload nginx
echo "NGINX_OK"

View File

@@ -0,0 +1,19 @@
#!/bin/bash
set -e
APP_DIR=/var/www/onebot
LOG=/tmp/npm_build.log
export PATH="/home/rui/.nvm/versions/node/v24.12.0/bin:$PATH"
cd "$APP_DIR"
echo "=== 前端构建开始 $(date) ===" > "$LOG"
node -v >> "$LOG" 2>&1
# laravel-mix 生产构建
npx mix --production >> "$LOG" 2>&1
echo "BUILD_EXIT=$?" >> "$LOG"
echo "=== 构建完成 $(date) ===" >> "$LOG"
echo "=== dist 目录 ==="
ls -la "$APP_DIR/public/dist/" 2>/dev/null | head -20
echo "=== dist/images logo ==="
ls -la "$APP_DIR/public/dist/images/" 2>/dev/null | grep -i logo || echo "NO_LOGO_IN_DIST"

View File

@@ -0,0 +1,19 @@
#!/bin/bash
set -e
APP_DIR=/var/www/onebot
echo "=== 重建 storage 目录结构 ==="
mkdir -p "$APP_DIR/storage/app"
mkdir -p "$APP_DIR/storage/framework/cache"
mkdir -p "$APP_DIR/storage/framework/sessions"
mkdir -p "$APP_DIR/storage/framework/views"
mkdir -p "$APP_DIR/storage/logs"
mkdir -p "$APP_DIR/storage/backupdb"
mkdir -p "$APP_DIR/userfiles"
chown -R rui:www-data "$APP_DIR/storage" "$APP_DIR/userfiles"
chmod -R 775 "$APP_DIR/storage" "$APP_DIR/userfiles"
echo "=== storage 目录 ==="
ls -la "$APP_DIR/storage/"
echo "DONE"

View File

@@ -0,0 +1,17 @@
#!/bin/bash
echo "=== HTTP 状态 ==="
curl -s -o /dev/null -w "HTTP=%{http_code}\n" http://127.0.0.1:8081/
echo "=== 页面标题 ==="
curl -s http://127.0.0.1:8081/ | grep -oE "<title>[^<]*</title>" | head -1
echo "=== 检查品牌关键词 ==="
BODY=$(curl -s http://127.0.0.1:8081/)
echo "$BODY" | grep -qi "onebot" && echo "含 OneBot" || echo "无 OneBot"
echo "$BODY" | grep -qi "leantime" && echo "含 Leantime" || echo "无 Leantime"
echo "=== 检查数据库表是否已建 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SHOW TABLES LIKE 'zp_bom%';" 2>/dev/null || echo "DB 查询失败"
echo "=== 检查 BOM 相关表 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SHOW TABLES;" 2>/dev/null | grep -iE "zp_bom|zp_" | head -30

View File

@@ -0,0 +1,12 @@
#!/bin/bash
echo "=== laravel.log 尾部 ==="
tail -50 /var/www/onebot/storage/logs/laravel.log 2>/dev/null || echo "无 laravel.log"
echo ""
echo "=== .env 是否有 APP_KEY ==="
grep -E "APP_KEY" /var/www/onebot/.env || echo "无 APP_KEY"
echo ""
echo "=== 用 artisan 直接看报错 ==="
cd /var/www/onebot
sudo -u www-data php artisan about 2>&1 | head -40

View File

@@ -0,0 +1,27 @@
#!/bin/bash
echo "=== /var/www/onebot 完整文件列表(根目录)==="
ls -la /var/www/onebot/
echo ""
echo "=== 是否有 artisan ==="
ls -la /var/www/onebot/artisan 2>&1
echo ""
echo "=== index.php 内容 ==="
cat /var/www/onebot/index.php
echo ""
echo "=== config 目录 ==="
ls -la /var/www/onebot/config/
echo ""
echo "=== config/app.php 里 APP_KEY 相关 ==="
grep -n "key\|APP_KEY\|cipher" /var/www/onebot/config/app.php 2>/dev/null | head -20 || echo "无 config/app.php"
echo ""
echo "=== .env 里是否有 APP_KEY ==="
grep -n "APP_KEY" /var/www/onebot/.env || echo "无 APP_KEY"
echo ""
echo "=== bin 目录 ==="
ls -la /var/www/onebot/bin/ 2>&1

View File

@@ -0,0 +1,22 @@
#!/bin/bash
set -e
APP_DIR=/var/www/onebot
# 打开 debug让 Laravel 直接渲染真实错误
sed -i 's/^APP_DEBUG=.*/APP_DEBUG=true/' "$APP_DIR/.env"
sed -i 's/^LEAN_DEBUG=.*/LEAN_DEBUG=1/' "$APP_DIR/.env"
grep -E "APP_DEBUG|LEAN_DEBUG" "$APP_DIR/.env"
# 清空旧日志,重新请求
rm -f "$APP_DIR/storage/logs/laravel.log"
echo "=== 直接 curl 看错误 ==="
curl -s http://127.0.0.1:8081/ | grep -oE "(Fatal error|Parse error|Exception|Error|message)[^<]*" | head -20
echo ""
echo "=== php-fpm 错误日志 ==="
tail -30 /var/log/php8.3-fpm.log 2>/dev/null || echo "无 php-fpm.log"
echo ""
echo "=== laravel.log ==="
tail -40 "$APP_DIR/storage/logs/laravel.log" 2>/dev/null || echo "仍无 laravel.log"

View File

@@ -0,0 +1,17 @@
#!/bin/bash
echo "=== nginx onebot.error.log ==="
tail -50 /var/log/nginx/onebot.error.log 2>/dev/null || echo "无 onebot.error.log"
echo ""
echo "=== nginx error.log (默认) ==="
tail -30 /var/log/nginx/error.log 2>/dev/null || echo "无默认 error.log"
echo ""
echo "=== 直接 CLI 方式执行 index.php 看真实错误 ==="
cd /var/www/onebot/public
sudo -u www-data php -d display_errors=1 -d error_reporting=E_ALL index.php 2>&1 | head -40
echo ""
echo "=== 用 php 直接 require 入口 ==="
cd /var/www/onebot
sudo -u www-data php -r '$_SERVER["REQUEST_URI"]="/"; $_SERVER["REQUEST_METHOD"]="GET"; $_SERVER["SCRIPT_NAME"]="/index.php"; $_SERVER["DOCUMENT_ROOT"]="/var/www/onebot/public"; require "/var/www/onebot/public/index.php";' 2>&1 | head -60

View File

@@ -0,0 +1,20 @@
#!/bin/bash
# 清空 Laravel 缓存,避免旧的语言缓存/配置缓存导致错误
cd /var/www/onebot
rm -rf storage/framework/cache/data/* 2>/dev/null
rm -rf storage/framework/views/* 2>/dev/null
rm -f bootstrap/cache/config.php bootstrap/cache/routes*.php 2>/dev/null
echo "=== HTTP 状态 ==="
curl -s -o /dev/null -w "HTTP=%{http_code}\n" http://127.0.0.1:8081/
echo "=== 页面标题 ==="
curl -s http://127.0.0.1:8081/ | grep -oE "<title>[^<]*</title>" | head -1
echo "=== 品牌关键词 ==="
BODY=$(curl -s http://127.0.0.1:8081/)
echo "$BODY" | grep -qi "onebot" && echo "含 OneBot" || echo "无 OneBot"
echo "$BODY" | grep -qi "leantime" && echo "含 Leantime" || echo "无 Leantime"
echo "=== 数据库表 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SHOW TABLES;" 2>/dev/null | grep -iE "zp_bom|zp_" | head -30

View File

@@ -0,0 +1,27 @@
#!/bin/bash
echo "=== 用 PHP 直接测试语言文件解析 ==="
for f in en-US zh-CN; do
echo "--- $f.ini ---"
sudo -u www-data php -r "
\$r = @parse_ini_file('/var/www/onebot/app/Language/$f.ini', false, INI_SCANNER_RAW);
if (\$r === false) {
echo \"PARSE_FAILED\n\";
} else {
echo \"PARSE_OK, keys=\".count(\$r).\"\n\";
}
" 2>&1
done
echo ""
echo "=== 完整错误(不抑制)==="
sudo -u www-data php -d display_errors=1 -d error_reporting=E_ALL -r "
parse_ini_file('/var/www/onebot/app/Language/en-US.ini', false, INI_SCANNER_RAW);
echo 'en-US OK\n';
parse_ini_file('/var/www/onebot/app/Language/zh-CN.ini', false, INI_SCANNER_RAW);
echo 'zh-CN OK\n';
" 2>&1
echo ""
echo "=== installation 缓存位置 ==="
ls -la /var/www/onebot/storage/framework/cache/ 2>/dev/null
find /var/www/onebot/storage/framework/cache -type f 2>/dev/null | head -20

View File

@@ -0,0 +1,24 @@
#!/bin/bash
cd /var/www/onebot
echo "=== 清空所有缓存 ==="
rm -rf storage/framework/cache/* 2>/dev/null
rm -rf storage/framework/views/* 2>/dev/null
rm -f bootstrap/cache/*.php 2>/dev/null
echo "缓存已清空"
echo ""
echo "=== 重新用 PHP 验证语言文件解析 ==="
sudo -u www-data php -r "
\$r = @parse_ini_file('/var/www/onebot/app/Language/en-US.ini', false, INI_SCANNER_RAW);
echo 'en-US: ' . (\$r === false ? 'FAIL' : 'OK('.count(\$r).')') . PHP_EOL;
\$r2 = @parse_ini_file('/var/www/onebot/app/Language/zh-CN.ini', false, INI_SCANNER_RAW);
echo 'zh-CN: ' . (\$r2 === false ? 'FAIL' : 'OK('.count(\$r2).')') . PHP_EOL;
" 2>&1
echo ""
echo "=== HTTP 状态 ==="
curl -s -o /dev/null -w "HTTP=%{http_code}\n" http://127.0.0.1:8081/
echo "=== 页面标题 ==="
curl -s http://127.0.0.1:8081/ | grep -oE "<title>[^<]*</title>" | head -1

View File

@@ -0,0 +1,9 @@
#!/bin/bash
cd /var/www/onebot
echo "=== 可用 CLI 命令 ==="
sudo -u www-data php /var/www/onebot/public/index.php list 2>&1 | head -50
echo ""
echo "=== /install 页面表单 ==="
curl -s http://127.0.0.1:8081/install 2>&1 | grep -oE "(name=\"[^\"]*\"|placeholder=\"[^\"]*\"|公司|Company|Email|First|Last|Password)" | head -40

View File

@@ -0,0 +1,9 @@
#!/bin/bash
cd /var/www/onebot
echo "=== 完整 CLI 命令列表 ==="
sudo -u www-data php /var/www/onebot/public/index.php list 2>&1 | grep -iE "install|db:|migrate|seed"
echo ""
echo "=== /install 表单完整字段(含 type=password==="
curl -s http://127.0.0.1:8081/install 2>&1 | grep -oE "<input[^>]*>" | grep -iE "password|name=" | head -40

View File

@@ -0,0 +1,16 @@
#!/bin/bash
cd /var/www/onebot
echo "=== 提交安装表单 ==="
RESP=$(curl -s -i -X POST http://127.0.0.1:8081/install \
-d "email=admin@onebot.local" \
-d "firstname=Admin" \
-d "lastname=OneBot" \
-d "company=OneBot" \
-d "install=Install")
echo "$RESP" | head -20
echo ""
echo "=== 检查数据库表是否已建 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SHOW TABLES;" 2>/dev/null | head -50

View File

@@ -0,0 +1,13 @@
#!/bin/bash
cd /var/www/onebot
echo "=== laravel.log 尾部 60 行 ==="
tail -60 storage/logs/laravel.log 2>/dev/null || echo "无 laravel.log"
echo ""
echo "=== 当前数据库表 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SHOW TABLES;" 2>/dev/null
echo ""
echo "=== 是否有 zp_user 表 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT COUNT(*) AS cnt FROM zp_user;" 2>/dev/null || echo "无 zp_user 表"

View File

@@ -0,0 +1,26 @@
#!/bin/bash
cd /var/www/onebot
echo "=== storage/logs 权限 ==="
ls -la storage/ storage/logs/ 2>&1
echo ""
echo "=== 检查日志配置 ==="
grep -rE "LOG_CHANNEL|LEAN_LOG|LOG_PATH" .env config/ app/Core/Configuration/ 2>/dev/null | head -20
echo ""
echo "=== 直接 CLI 调用 setupDB 捕获异常 ==="
sudo -u www-data php -d display_errors=1 -d error_reporting=E_ALL -r '
require "/var/www/onebot/vendor/autoload.php";
$app = require "/var/www/onebot/bootstrap/app.php";
\Leantime\Core\Bootloader::getInstance()->boot($app);
$repo = app()->make(\Leantime\Domain\Install\Repositories\Install::class);
try {
$result = $repo->setupDB(["email"=>"admin@onebot.local","firstname"=>"Admin","lastname"=>"OneBot","company"=>"OneBot"]);
echo "setupDB result: " . var_export($result, true) . PHP_EOL;
} catch (\Throwable $e) {
echo "EXCEPTION: " . $e->getMessage() . PHP_EOL;
echo "FILE: " . $e->getFile() . ":" . $e->getLine() . PHP_EOL;
echo $e->getTraceAsString() . PHP_EOL;
}
' 2>&1 | head -60

View File

@@ -0,0 +1,13 @@
#!/bin/bash
cd /var/www/onebot
echo "=== 日志文件列表 ==="
ls -la storage/logs/
echo ""
echo "=== leantime 日志尾部 80 行 ==="
tail -80 storage/logs/leantime-2026-08-31.log 2>/dev/null
echo ""
echo "=== 搜索 Error/Exception 关键词 ==="
grep -nE "Error|Exception|error|failed|SQLSTATE|Access denied" storage/logs/leantime-2026-08-31.log 2>/dev/null | tail -40

View File

@@ -0,0 +1,26 @@
#!/bin/bash
set -e
APP_DIR=/var/www/onebot
echo "=== 移动 .env 到 config/.env ==="
mv "$APP_DIR/.env" "$APP_DIR/config/.env" 2>/dev/null || cp "$APP_DIR/.env" "$APP_DIR/config/.env"
chown rui:www-data "$APP_DIR/config/.env"
chmod 664 "$APP_DIR/config/.env"
echo "=== 确认位置 ==="
ls -la "$APP_DIR/.env" 2>/dev/null && echo "根目录 .env 仍存在(需删除)" || echo "根目录 .env 已移除"
ls -la "$APP_DIR/config/.env" && echo "config/.env 已就位"
echo ""
echo "=== 清缓存 ==="
rm -rf "$APP_DIR/storage/framework/cache/"* 2>/dev/null
rm -f "$APP_DIR/bootstrap/cache/"*.php 2>/dev/null
echo "缓存已清"
echo ""
echo "=== 验证数据库连接配置 ==="
grep -E "LEAN_DB_|DB_" "$APP_DIR/config/.env"
echo ""
echo "=== HTTP 测试 ==="
curl -s -o /dev/null -w "HTTP=%{http_code}\n" http://127.0.0.1:8081/

View File

@@ -0,0 +1,12 @@
#!/bin/bash
echo "=== 总表数 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='onebot';" 2>/dev/null
echo "=== BOM 表 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SHOW TABLES LIKE 'zp_bom%';" 2>/dev/null
echo "=== zp_user ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT id, username, status, role FROM zp_user;" 2>/dev/null
echo "=== db-version ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT * FROM zp_settings WHERE \`key\`='db-version';" 2>/dev/null

View File

@@ -0,0 +1,11 @@
#!/bin/bash
cd /var/www/onebot
echo "=== 生成密码哈希并激活管理员 ==="
HASH=$(php -r "echo password_hash('OneBot@2026', PASSWORD_DEFAULT);")
echo "哈希: ${HASH:0:20}..."
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "UPDATE zp_user SET password='${HASH}', status='a', forcePwReset=0 WHERE id=1;" 2>/dev/null
echo "=== 验证用户状态 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT id, username, status, role FROM zp_user WHERE id=1;" 2>/dev/null

View File

@@ -0,0 +1,18 @@
#!/bin/bash
echo "=== 首页 HTTP 状态 ==="
curl -s -o /dev/null -w "HTTP=%{http_code}\n" http://127.0.0.1:8081/
echo "=== 首页标题与品牌 ==="
BODY=$(curl -s http://127.0.0.1:8081/)
echo "$BODY" | grep -oE "<title>[^<]*</title>" | head -1
echo "$BODY" | grep -qi "OneBot" && echo "✅ 含 OneBot 品牌" || echo "❌ 无 OneBot"
echo "$BODY" | grep -qi "leantime" && echo "⚠️ 仍含 leantime" || echo "✅ 无 leantime 残留"
echo "=== logo 文件可达 ==="
curl -s -o /dev/null -w "logo.svg HTTP=%{http_code} size=%{size_download}\n" http://127.0.0.1:8081/dist/images/logo.svg
echo "=== PDF 预览组件挂载 ==="
echo "$BODY" | grep -q "ob-pdf" && echo "✅ pdfPreview 组件已挂载" || echo "⚠️ 未在首页检测到 pdfPreview登录页可能不加载"
echo "=== 登录页面字段 ==="
echo "$BODY" | grep -oE "(name=\"(username|password|email)\")" | sort -u

View File

@@ -0,0 +1,33 @@
#!/bin/bash
cd /var/www/onebot
JAR=/tmp/onebot_cookies.txt
rm -f "$JAR"
echo "=== 登录页品牌 ==="
BODY=$(curl -s -c "$JAR" http://127.0.0.1:8081/auth/login)
echo "$BODY" | grep -oE "<title>[^<]*</title>" | head -1
echo "$BODY" | grep -qi "OneBot" && echo "✅ 登录页含 OneBot" || echo "⚠️ 登录页无 OneBot 文案(可能是标题/翻译)"
echo "$BODY" | grep -qi "leantime" && echo "⚠️ 登录页仍含 leantime" || echo "✅ 登录页无 leantime 残留"
echo ""
echo "=== 提取 CSRF token ==="
CSRF=$(echo "$BODY" | grep -oE 'name="csrf-token" content="[^"]*"' | sed 's/.*content="//;s/"//')
echo "CSRF: ${CSRF:0:20}..."
echo ""
echo "=== 尝试登录 ==="
LOGIN=$(curl -s -i -b "$JAR" -c "$JAR" -X POST http://127.0.0.1:8081/auth/login \
-d "username=admin@onebot.local" \
-d "password=OneBot@2026" \
-d "_token=$CSRF" \
-H "X-CSRF-TOKEN: $CSRF" \
-H "X-Requested-With: XMLHttpRequest")
echo "$LOGIN" | head -10
echo ""
echo "=== 登录后访问首页 ==="
curl -s -b "$JAR" -o /dev/null -w "dashboard HTTP=%{http_code}\n" http://127.0.0.1:8081/dashboard/show
echo ""
echo "=== 访问 BOM 页面 ==="
curl -s -b "$JAR" -o /dev/null -w "bom/show HTTP=%{http_code}\n" http://127.0.0.1:8081/bom/show

View File

@@ -0,0 +1,34 @@
#!/bin/bash
cd /var/www/onebot
JAR=/tmp/onebot_cookies2.txt
rm -f "$JAR"
# 重新登录拿会话
BODY=$(curl -s -c "$JAR" http://127.0.0.1:8081/auth/login)
CSRF=$(echo "$BODY" | grep -oE 'name="csrf-token" content="[^"]*"' | sed 's/.*content="//;s/"//')
curl -s -b "$JAR" -c "$JAR" -X POST http://127.0.0.1:8081/auth/login \
-d "username=admin@onebot.local" -d "password=OneBot@2026" \
-d "_token=$CSRF" -H "X-CSRF-TOKEN: $CSRF" -o /dev/null
echo "=== BOM 页面内容检查 ==="
BOM=$(curl -s -b "$JAR" http://127.0.0.1:8081/bom/show)
echo "$BOM" | grep -oE "<title>[^<]*</title>" | head -1
echo "$BOM" | grep -qi "BOM" && echo "✅ 页面含 BOM 内容" || echo "❌ 无 BOM"
echo "$BOM" | grep -qi "新建 BOM" && echo "✅ 含新建 BOM 按钮" || echo "⚠️ 无新建 BOM语言可能是 en-US"
echo "$BOM" | grep -qi "New BOM" && echo "✅ 含 New BOM 按钮" || echo "⚠️ 无 New BOM"
echo ""
echo "=== 左侧菜单是否含 BOM 项 ==="
echo "$BOM" | grep -oE 'href="[^"]*bom[^"]*"' | head -5
echo "$BOM" | grep -qi "menu.bom\|BOM" && echo "✅ 菜单区含 BOM" || echo "⚠️ 菜单区未见 BOM 文案"
echo ""
echo "=== dashboard 品牌检查 ==="
DASH=$(curl -s -b "$JAR" http://127.0.0.1:8081/dashboard/show)
echo "$DASH" | grep -oE "<title>[^<]*</title>" | head -1
echo "$DASH" | grep -qi "OneBot" && echo "✅ dashboard 含 OneBot" || echo "⚠️ dashboard 无 OneBot 文案"
echo "$DASH" | grep -qi "leantime" && echo "⚠️ dashboard 仍含 leantime检查上下文" || echo "✅ dashboard 无 leantime"
echo ""
echo "=== 登录页 leantime 残留定位 ==="
curl -s http://127.0.0.1:8081/auth/login | grep -oiE ".{40}leantime.{40}" | head -5

View File

@@ -0,0 +1,17 @@
#!/bin/bash
cd /var/www/onebot
JAR=/tmp/onebot_cookies3.txt
rm -f "$JAR"
BODY=$(curl -s -c "$JAR" http://127.0.0.1:8081/auth/login)
CSRF=$(echo "$BODY" | grep -oE 'name="csrf-token" content="[^"]*"' | sed 's/.*content="//;s/"//')
curl -s -b "$JAR" -c "$JAR" -X POST http://127.0.0.1:8081/auth/login \
-d "username=admin@onebot.local" -d "password=OneBot@2026" \
-d "_token=$CSRF" -H "X-CSRF-TOKEN: $CSRF" -o /dev/null
echo "=== dashboard 页面 leantime 出现上下文 ==="
curl -s -b "$JAR" http://127.0.0.1:8081/dashboard/show | grep -oiE ".{30}leantime.{30}" | head -10
echo ""
echo "=== 可见文本(去标签)是否含 Leantime ==="
curl -s -b "$JAR" http://127.0.0.1:8081/dashboard/show | sed 's/<[^>]*>//g' | grep -i "leantime" | head -5 || echo "✅ 可见文本无 Leantime"

View File

@@ -0,0 +1,11 @@
#!/bin/bash
echo "=== 本机网卡 IP ==="
ip -4 addr show 2>/dev/null | grep -E "inet " | awk '{print $2, $NF}'
echo ""
echo "=== 192.168.1.44 是否可达(本机自测)==="
curl -s -o /dev/null -w "HTTP=%{http_code}\n" --connect-timeout 5 http://192.168.1.44:8081/ || echo "8081 从本机 192.168.1.44 访问失败"
echo ""
echo "=== nginx 监听 ==="
ss -tlnp 2>/dev/null | grep -E ":8081|:80"

View File

@@ -0,0 +1,17 @@
#!/bin/bash
set -e
APP_DIR=/var/www/onebot
echo "=== 修改站点 URL 为内网 IP ==="
sed -i 's|http://119.29.53.45:8081|http://192.168.1.44:8081|g' "$APP_DIR/config/.env"
grep -E "APP_URL|LEAN_APP_URL" "$APP_DIR/config/.env"
echo ""
echo "=== 清缓存 ==="
rm -rf "$APP_DIR/storage/framework/cache/"* 2>/dev/null
rm -f "$APP_DIR/bootstrap/cache/"*.php 2>/dev/null
echo "缓存已清"
echo ""
echo "=== 验证重定向地址 ==="
curl -s -i http://192.168.1.44:8081/ | grep -iE "^HTTP|^Location" | head -3

View File

@@ -0,0 +1,15 @@
#!/bin/bash
echo "=== ufw 状态 ==="
sudo ufw status 2>/dev/null || echo "ufw 未启用/未安装"
echo ""
echo "=== ufw 8081 放行 ==="
sudo ufw allow 8081/tcp 2>/dev/null && echo "已放行 8081" || true
echo ""
echo "=== 最终验证:内网 IP 访问 ==="
curl -s -o /dev/null -w "192.168.1.44:8081 HTTP=%{http_code}\n" --connect-timeout 5 http://192.168.1.44:8081/
echo ""
echo "=== 登录页品牌最终确认 ==="
curl -s http://192.168.1.44:8081/auth/login | grep -oE "<title>[^<]*</title>" | head -1

View File

@@ -0,0 +1,12 @@
#!/bin/bash
cd /var/www/onebot
echo "=== 生成新密码哈希 ==="
HASH=$(php -r "echo password_hash('wrg521113..', PASSWORD_DEFAULT);")
echo "哈希: ${HASH:0:20}..."
echo "=== 更新管理员账号 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "UPDATE zp_user SET username='ruiguo_wang@163.com', password='${HASH}', status='a', forcePwReset=0 WHERE id=1;" 2>/dev/null
echo "=== 验证 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT id, username, status, role FROM zp_user WHERE id=1;" 2>/dev/null

View File

@@ -0,0 +1,23 @@
#!/bin/bash
cd /var/www/onebot
JAR=/tmp/onebot_cookies_final.txt
rm -f "$JAR"
BODY=$(curl -s -c "$JAR" http://127.0.0.1:8081/auth/login)
CSRF=$(echo "$BODY" | grep -oE 'name="csrf-token" content="[^"]*"' | sed 's/.*content="//;s/"//')
echo "=== 用新账号登录 ==="
LOGIN=$(curl -s -i -b "$JAR" -c "$JAR" -X POST http://127.0.0.1:8081/auth/login \
-d "username=ruiguo_wang@163.com" \
-d "password=wrg521113.." \
-d "_token=$CSRF" \
-H "X-CSRF-TOKEN: $CSRF")
echo "$LOGIN" | grep -iE "^HTTP|^Location" | head -3
echo ""
echo "=== 登录后 BOM 页面 ==="
curl -s -b "$JAR" -o /dev/null -w "bom/show HTTP=%{http_code}\n" http://127.0.0.1:8081/bom/show
echo ""
echo "=== dashboard 标题 ==="
curl -s -b "$JAR" http://127.0.0.1:8081/dashboard/show | grep -oE "<title>[^<]*</title>" | head -1

View File

@@ -0,0 +1,85 @@
#!/bin/bash
set -u
cd /var/www/onebot
echo "=== PHP 语法检查 ==="
for f in app/Domain/Bom/Services/Teable.php app/Domain/Bom/Services/Bom.php app/Domain/Bom/Controllers/Api.php app/Domain/Bom/routes.php; do
php -l "$f" 2>&1
done
echo ""
echo "=== 清缓存 + 重载 fpm ==="
rm -rf storage/framework/views/* storage/framework/cache/* bootstrap/cache/*.php 2>/dev/null
echo "wrg521113.." | sudo -S systemctl reload php8.3-fpm >/dev/null 2>&1
echo "done"
echo ""
echo "=== 登录 ==="
JAR=/tmp/onebot_paste_cookies.txt
rm -f "$JAR"
BODY=$(curl -s -c "$JAR" http://127.0.0.1:8081/auth/login)
CSRF=$(echo "$BODY" | grep -oE 'name="csrf-token" content="[^"]*"' | sed 's/.*content="//;s/"//')
curl -s -b "$JAR" -c "$JAR" -X POST http://127.0.0.1:8081/auth/login \
-d "username=ruiguo_wang@163.com" \
-d "password=wrg521113.." \
-d "_token=$CSRF" \
-H "X-CSRF-TOKEN: $CSRF" -o /dev/null -w "login HTTP=%{http_code}\n"
echo ""
echo "=== 粘贴解析接口测试 ==="
cat > /tmp/paste_sample.txt <<'PASTEEOF'
# Table: UAGP350B
## API Operations
### 1. Read Records (GET)
curl -X GET "https://table.universal-onebot.com/api/table/tblGHwQc8eAGFZcZ71A/record?fieldKeyType=name" \
-H "Authorization: Bearer teable_accaWtlglDjnwkieJXE_KaafZue9411p5G+QmP0r4zRsC6GDpUTuRDVfh2f8eco="
PASTEEOF
php -r '$t=file_get_contents("/tmp/paste_sample.txt"); echo json_encode(["text"=>$t], JSON_UNESCAPED_SLASHES);' > /tmp/paste_payload.json
curl -s -b "$JAR" -X POST http://127.0.0.1:8081/bom/api/teable/parse \
-H "Content-Type: application/json" \
-H "X-CSRF-TOKEN: $CSRF" \
--data-binary @/tmp/paste_payload.json
echo ""
echo ""
echo "=== Teable API 连通性(远程拉 1 条记录) ==="
curl -s -m 20 "https://table.universal-onebot.com/api/table/tblGHwQc8eAGFZcZ71A/record?fieldKeyType=name&take=1" \
-H "Authorization: Bearer teable_accaWtlglDjnwkieJXE_KaafZue9411p5G+QmP0r4zRsC6GDpUTuRDVfh2f8eco=" | head -c 400
echo ""
echo ""
echo "=== 端到端:新建测试 BOM -> 粘贴导入 -> 验证 -> 删除 ==="
PROJECT_ID=$(mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -N -e "SELECT id FROM zp_projects LIMIT 1;" 2>/dev/null)
echo "projectId=$PROJECT_ID"
CREATE_RESP=$(curl -s -b "$JAR" -X POST http://127.0.0.1:8081/bom/api \
-H "Content-Type: application/json" \
-H "X-CSRF-TOKEN: $CSRF" \
-d "{\"bomNo\":\"TEST-PASTE-1\",\"productName\":\"UAGP350B paste test\",\"projectId\":$PROJECT_ID}")
echo "create: $CREATE_RESP"
BOM_ID=$(echo "$CREATE_RESP" | grep -oE '"id":[0-9]+' | head -1 | sed 's/"id"://')
echo "bomId=$BOM_ID"
if [ -n "$BOM_ID" ] && [ "$BOM_ID" -gt 0 ] 2>/dev/null; then
echo ""
echo "--- 粘贴导入(真实拉取 Teable 数据) ---"
curl -s -b "$JAR" -X POST "http://127.0.0.1:8081/bom/api/$BOM_ID/import/teable-paste" \
-H "Content-Type: application/json" \
-H "X-CSRF-TOKEN: $CSRF" \
--data-binary @/tmp/paste_payload.json
echo ""
echo "--- 验证行数 ---"
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -N -e "SELECT COUNT(*) FROM zp_bom_item WHERE bomId=$BOM_ID;" 2>/dev/null
echo "--- 清理测试 BOM ---"
curl -s -b "$JAR" -X DELETE "http://127.0.0.1:8081/bom/api/$BOM_ID" \
-H "X-CSRF-TOKEN: $CSRF" -w "delete HTTP=%{http_code}\n"
fi
echo ""
echo "=== 全部完成 ==="

View File

@@ -0,0 +1,33 @@
#!/bin/bash
set -u
cd /var/www/onebot
echo "=== 1. BOM 列表 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT id, bomNo, productName, projectId FROM zp_bom ORDER BY id DESC LIMIT 5;" 2>/dev/null
echo ""
echo "=== 2. 每个 BOM 的行数 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT bomId, COUNT(*) cnt FROM zp_bom_item GROUP BY bomId;" 2>/dev/null
echo ""
echo "=== 3. 动态列 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT id, bomId, colKey, label FROM zp_bom_column ORDER BY bomId, sortOrder LIMIT 40;" 2>/dev/null
echo ""
echo "=== 4. 最新 BOM 的一行明细extra 原始 JSON ==="
LATEST=$(mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -N -e "SELECT MAX(bomId) FROM zp_bom_item;" 2>/dev/null)
echo "latest bomId with items = $LATEST"
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT id, seq, partNo, partName, extra FROM zp_bom_item WHERE bomId=$LATEST LIMIT 2\G" 2>/dev/null | head -60
echo ""
echo "=== 5. Teable field 接口原始返回(前 3 个字段对象) ==="
curl -s -m 20 "https://table.universal-onebot.com/api/table/tblGHwQc8eAGFZcZ71A/field" \
-H "Authorization: Bearer teable_accaWtlglDjnwkieJXE_KaafZue9411p5G+QmP0r4zRsC6GDpUTuRDVfh2f8eco=" \
| php -r '$d=json_decode(stream_get_contents(STDIN),true); echo json_encode(array_slice($d,0,3), JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);'
echo ""
echo "=== 6. Teable 记录原始返回(第 1 条) ==="
curl -s -m 20 "https://table.universal-onebot.com/api/table/tblGHwQc8eAGFZcZ71A/record?fieldKeyType=name&take=1" \
-H "Authorization: Bearer teable_accaWtlglDjnwkieJXE_KaafZue9411p5G+QmP0r4zRsC6GDpUTuRDVfh2f8eco=" \
| head -c 1200
echo ""

View File

@@ -0,0 +1,39 @@
#!/bin/bash
set -u
cd /var/www/onebot
echo "=== 1. zp_bom_column 全部行(正确列名) ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT id, bomId, \`key\`, label, sortOrder FROM zp_bom_column ORDER BY bomId, sortOrder;" 2>&1 | grep -v "Warning"
echo ""
echo "=== 2. zp_bom_source 全部行 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT id, bomId, name, tableId FROM zp_bom_source;" 2>&1 | grep -v "Warning"
echo ""
echo "=== 3. 远程文件 md5 ==="
md5sum app/Domain/Bom/Services/Bom.php app/Domain/Bom/Services/Teable.php app/Domain/Bom/Templates/detail.blade.php
echo ""
echo "=== 4. 远程 Bom.php importSource 字段循环395-425 行) ==="
sed -n '395,425p' app/Domain/Bom/Services/Bom.php
echo ""
echo "=== 5. BOM 3 详情页渲染检查 ==="
JAR=/tmp/ob38.txt
rm -f $JAR
BODY=$(curl -s -c $JAR http://127.0.0.1:8081/auth/login)
CSRF=$(echo "$BODY" | grep -oE 'name="csrf-token" content="[^"]*"' | sed 's/.*content="//;s/"//')
curl -s -b $JAR -c $JAR -X POST http://127.0.0.1:8081/auth/login \
-d "username=ruiguo_wang@163.com" -d "password=wrg521113.." -d "_token=$CSRF" \
-H "X-CSRF-TOKEN: $CSRF" -o /dev/null
curl -s -b $JAR "http://127.0.0.1:8081/bom/show/3" -o /tmp/ob38_detail.html -w "detail HTTP=%{http_code}\n"
echo "--- 页面 th 数量及 data-key ---"
grep -oE '<th data-key="[^"]*"' /tmp/ob38_detail.html | sed 's/<th data-key="//;s/"//' | tr '\n' ' '
echo ""
echo "--- 页面里的动态列标签th 里的中文) ---"
grep -oE '<th[^>]*>[^<]*' /tmp/ob38_detail.html | head -40
echo ""
echo "=== 6. BOM 3 明细接口 JSON 前 300 字符 ==="
curl -s -b $JAR "http://127.0.0.1:8081/bom/api/3" | head -c 300
echo ""

View File

@@ -0,0 +1,40 @@
#!/bin/bash
cd /var/www/onebot
echo "=== BOM1 前2行 extra ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT id, seq, partNo, LEFT(extra,200) e FROM zp_bom_item WHERE bomId=1 ORDER BY id LIMIT 2\G" 2>/dev/null
echo ""
echo "=== zp_bom_item 表结构 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SHOW CREATE TABLE zp_bom_item\G" 2>/dev/null | grep -E "CREATE|\`seq\`|\`partNo\`|\`extra\`"
echo ""
echo "=== 登录并拉取两个 BOM 的详情页 ==="
JAR=/tmp/ob38b.txt
rm -f $JAR
BODY=$(curl -s -c $JAR http://127.0.0.1:8081/auth/login)
CSRF=$(echo "$BODY" | grep -oE 'name="csrf-token" content="[^"]*"' | sed 's/.*content="//;s/"//')
curl -s -b $JAR -c $JAR -X POST http://127.0.0.1:8081/auth/login \
-d "username=ruiguo_wang@163.com" -d "password=wrg521113.." -d "_token=$CSRF" \
-H "X-CSRF-TOKEN: $CSRF" -o /dev/null
curl -s -b $JAR http://127.0.0.1:8081/bom/show/1 -o /tmp/ob38b_1.html
curl -s -b $JAR http://127.0.0.1:8081/bom/show/3 -o /tmp/ob38b_3.html
cat > /tmp/check_row.php <<'PHPEOF'
<?php
foreach ([1, 3] as $bid) {
$h = file_get_contents("/tmp/ob38b_$bid.html");
echo "=== BOM $bid 页面渲染的第一行非空单元格 ===\n";
if (! preg_match('/<tbody>(.*?)<\/tbody>/s', $h, $m)) { echo "no tbody\n"; continue; }
if (! preg_match_all('/<tr data-item-id="(\d+)"(.*?)<\/tr>/s', $m[1], $rows)) { echo "no rows\n"; continue; }
$n = 0;
foreach ($rows[2] as $i => $body) {
preg_match_all('/data-key="([^"]*)"[^>]*value="([^"]*)"/', $body, $mm, PREG_SET_ORDER);
$filled = [];
foreach ($mm as $x) { if ($x[2] !== '') { $filled[] = $x[1].'='.mb_substr($x[2], 0, 24); } }
echo "row ".$rows[1][$i].": ".implode(' | ', $filled)."\n";
if (++$n >= 2) break;
}
}
PHPEOF
php /tmp/check_row.php

View File

@@ -0,0 +1,37 @@
#!/bin/bash
set -u
cd /var/www/onebot
echo "=== 1. PHP 语法检查 ==="
for f in \
app/Domain/Bom/Services/Teable.php \
app/Domain/Bom/Services/Bom.php \
app/Domain/Bom/Services/Excel.php \
app/Domain/Bom/Controllers/Api.php \
app/Domain/Bom/routes.php \
app/Domain/Bom/Repositories/Bom.php \
app/Domain/Install/Repositories/Install.php \
app/Domain/Install/Services/SchemaBuilder.php \
app/Core/Configuration/AppSettings.php ; do
php -l "$f" 2>&1 | grep -v "No syntax errors" || true
done
echo "语法检查完成"
echo ""
echo "=== 2. 清缓存 ==="
rm -rf storage/framework/views/* storage/framework/cache/* bootstrap/cache/*.php 2>/dev/null
echo "done"
echo ""
echo "=== 3. 执行数据库迁移updateDB ==="
php public/index.php db:update 2>&1 | tail -30 || echo "db:update 命令可能不存在,尝试其他方式"
echo ""
echo "=== 4. 检查 db-version 和 hiddenColumns 列 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT \`key\`,\`value\` FROM zp_settings WHERE \`key\`='db-version';" 2>/dev/null
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SHOW COLUMNS FROM zp_bom LIKE 'hiddenColumns';" 2>/dev/null
echo ""
echo "=== 5. 重载 fpm ==="
echo "wrg521113.." | sudo -S systemctl reload php8.3-fpm >/dev/null 2>&1
echo "done"

View File

@@ -0,0 +1,57 @@
#!/bin/bash
set -u
cd /var/www/onebot
JAR=/tmp/ob40.txt
rm -f $JAR
echo "=== 登录 ==="
BODY=$(curl -s -c $JAR http://127.0.0.1:8081/auth/login)
CSRF=$(echo "$BODY" | grep -oE 'name="csrf-token" content="[^"]*"' | sed 's/.*content="//;s/"//')
curl -s -b $JAR -c $JAR -X POST http://127.0.0.1:8081/auth/login \
-d "username=ruiguo_wang@163.com" -d "password=wrg521113.." -d "_token=$CSRF" \
-H "X-CSRF-TOKEN: $CSRF" -o /dev/null -w "login HTTP=%{http_code}\n"
echo ""
echo "=== 详情页 HTTP 状态 ==="
curl -s -b $JAR -o /tmp/ob40_detail.html -w "bom/show/3 HTTP=%{http_code}\n" http://127.0.0.1:8081/bom/show/3
echo ""
echo "=== 页面关键元素检查 ==="
echo -n "横向滚动容器: "; grep -c 'overflow-x:auto' /tmp/ob40_detail.html
echo -n "列设置按钮: "; grep -c 'btnColumnSettings' /tmp/ob40_detail.html
echo -n "Teable下拉: "; grep -c 'btnTeablePaste' /tmp/ob40_detail.html
echo -n "Excel模板导出: "; grep -c 'btnExcelTemplate' /tmp/ob40_detail.html
echo -n "Excel导出xlsx: "; grep -c 'btnExcelExport' /tmp/ob40_detail.html
echo -n "粘贴手动补齐输入框(pasteBaseUrl): "; grep -c 'pasteBaseUrl' /tmp/ob40_detail.html
echo -n "缩略图列显示友好(非JSON, 看是否有image.png): "; grep -c 'image.png' /tmp/ob40_detail.html
echo ""
echo "=== 列显隐 API 测试(隐藏 seq + 项目) ==="
curl -s -b $JAR -X POST http://127.0.0.1:8081/bom/api/3/column-visibility \
-H "Content-Type: application/json" \
-H "X-CSRF-TOKEN: $CSRF" \
-d '{"keys":["seq","项目"]}' -w "\nvisibility HTTP=%{http_code}\n"
echo ""
echo "=== 验证隐藏列已持久化 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT hiddenColumns FROM zp_bom WHERE id=3;" 2>/dev/null
echo ""
echo "=== 导出 xlsx ==="
curl -s -b $JAR -o /tmp/ob40_export.xlsx -w "export xlsx HTTP=%{http_code} size=%{size_download}\n" "http://127.0.0.1:8081/bom/api/3/export?format=xlsx"
echo -n "xlsx 文件头(应为 PK): "; head -c 2 /tmp/ob40_export.xlsx; echo ""
echo ""
echo "=== 导出模板 ==="
curl -s -b $JAR -o /tmp/ob40_tpl.xlsx -w "export-template HTTP=%{http_code} size=%{size_download}\n" "http://127.0.0.1:8081/bom/api/3/export-template"
echo ""
echo "=== 恢复列显隐(清空) ==="
curl -s -b $JAR -X POST http://127.0.0.1:8081/bom/api/3/column-visibility \
-H "Content-Type: application/json" \
-H "X-CSRF-TOKEN: $CSRF" \
-d '{"keys":[]}' -w "reset HTTP=%{http_code}\n"
echo ""
echo "=== 全部完成 ==="

View File

@@ -0,0 +1,26 @@
#!/bin/bash
cd /var/www/onebot
echo "=== 1. 取一个附件字段原始值 ==="
curl -s -m 20 "https://table.universal-onebot.com/api/table/tblGHwQc8eAGFZcZ71A/record?fieldKeyType=name&take=1" \
-H "Authorization: Bearer teable_accaWtlglDjnwkieJXE_KaafZue9411p5G+QmP0r4zRsC6GDpUTuRDVfh2f8eco=" \
| php -r '$d=json_decode(stream_get_contents(STDIN),true); $f=$d["records"][0]["fields"]["缩略图"][0] ?? []; echo json_encode($f, JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);'
echo ""
echo "=== 2. 提取 smThumbnailUrl 并测试直连(不带 Bearer ==="
URL=$(curl -s -m 20 "https://table.universal-onebot.com/api/table/tblGHwQc8eAGFZcZ71A/record?fieldKeyType=name&take=1" \
-H "Authorization: Bearer teable_accaWtlglDjnwkieJXE_KaafZue9411p5G+QmP0r4zRsC6GDpUTuRDVfh2f8eco=" \
| php -r '$d=json_decode(stream_get_contents(STDIN),true); $f=$d["records"][0]["fields"]["缩略图"][0] ?? []; echo $f["smThumbnailUrl"] ?? "";')
echo "smThumbnailUrl=$URL"
if [ -n "$URL" ]; then
FULL="https://table.universal-onebot.com$URL"
echo "--- 直连(无 Bearer---"
curl -s -m 15 -o /dev/null -w "HTTP=%{http_code} type=%{content_type} size=%{size_download}\n" "$FULL"
echo "--- 带 Bearer ---"
curl -s -m 15 -o /dev/null -w "HTTP=%{http_code} type=%{content_type} size=%{size_download}\n" -H "Authorization: Bearer teable_accaWtlglDjnwkieJXE_KaafZue9411p5G+QmP0r4zRsC6GDpUTuRDVfh2f8eco=" "$FULL"
fi
echo ""
echo "=== 3. CDN 可达性测试Handsontable ==="
curl -s -m 10 -o /dev/null -w "jsdelivr HTTP=%{http_code}\n" "https://cdn.jsdelivr.net/npm/handsontable/dist/handsontable.full.min.js" 2>&1 || echo "jsdelivr 不可达"
curl -s -m 10 -o /dev/null -w "unpkg HTTP=%{http_code}\n" "https://unpkg.com/handsontable/dist/handsontable.full.min.js" 2>&1 || echo "unpkg 不可达"

View File

@@ -0,0 +1,20 @@
#!/bin/bash
set -u
cd /var/www/onebot
DIR=public/dist/handsontable
mkdir -p "$DIR"
BASE="https://cdn.jsdelivr.net/npm/handsontable@14.6.2/dist"
echo "=== 下载 Handsontable 14.6.2 ==="
curl -s -m 60 -o "$DIR/handsontable.full.min.js" "$BASE/handsontable.full.min.js" && echo "js ok $(wc -c < "$DIR/handsontable.full.min.js") bytes"
curl -s -m 60 -o "$DIR/handsontable.full.min.css" "$BASE/handsontable.full.min.css" && echo "css ok $(wc -c < "$DIR/handsontable.full.min.css") bytes"
echo ""
echo "=== 校验js 应有 Handsontable 关键字css 应有 .handsontable ==="
head -c 60 "$DIR/handsontable.full.min.js"; echo ""
grep -c "handsontable" "$DIR/handsontable.full.min.css" 2>/dev/null || echo "css 校验失败"
echo ""
echo "=== 通过 web 验证可访问 ==="
curl -s -o /dev/null -w "js HTTP=%{http_code}\n" "http://127.0.0.1:8081/dist/handsontable/handsontable.full.min.js"
curl -s -o /dev/null -w "css HTTP=%{http_code}\n" "http://127.0.0.1:8081/dist/handsontable/handsontable.full.min.css"

View File

@@ -0,0 +1,49 @@
#!/bin/bash
set -u
cd /var/www/onebot
JAR=/tmp/ob43.txt
rm -f $JAR
echo "=== 登录 ==="
BODY=$(curl -s -c $JAR http://127.0.0.1:8081/auth/login)
CSRF=$(echo "$BODY" | grep -oE 'name="csrf-token" content="[^"]*"' | sed 's/.*content="//;s/"//')
curl -s -b $JAR -c $JAR -X POST http://127.0.0.1:8081/auth/login \
-d "username=ruiguo_wang@163.com" -d "password=wrg521113.." -d "_token=$CSRF" \
-H "X-CSRF-TOKEN: $CSRF" -o /dev/null -w "login HTTP=%{http_code}\n"
echo ""
echo "=== 详情页渲染 ==="
curl -s -b $JAR -o /tmp/ob43.html -w "bom/show/3 HTTP=%{http_code}\n" http://127.0.0.1:8081/bom/show/3
echo ""
echo "=== 关键元素检查 ==="
echo -n "Handsontable js 引入: "; grep -c 'handsontable.full.min.js' /tmp/ob43.html
echo -n "Handsontable css 引入: "; grep -c 'handsontable.full.min.css' /tmp/ob43.html
echo -n "bomHot 容器: "; grep -c 'id="bomHot"' /tmp/ob43.html
echo -n "imageRenderer: "; grep -c 'imageRenderer' /tmp/ob43.html
echo -n "fileRenderer: "; grep -c 'fileRenderer' /tmp/ob43.html
echo -n "new Handsontable: "; grep -c 'new Handsontable' /tmp/ob43.html
echo -n "附件 URL (attachments/read): "; grep -c 'attachments/read' /tmp/ob43.html
echo -n "图片缩略图 img 标签: "; grep -c '<img' /tmp/ob43.html
echo ""
echo "=== 明细 API 返回的附件字段(应为 JSON 数组含 url ==="
curl -s -b $JAR "http://127.0.0.1:8081/bom/api/3" | php -r '
$d=json_decode(stream_get_contents(STDIN),true);
$cols=$d["data"]["columns"] ?? [];
$img=$d["data"]["imageColumns"] ?? [];
$file=$d["data"]["fileColumns"] ?? [];
echo "imageColumns: ".json_encode($img,JSON_UNESCAPED_UNICODE)."\n";
echo "fileColumns: ".json_encode($file,JSON_UNESCAPED_UNICODE)."\n";
$it=$d["data"]["items"][0] ?? [];
foreach($img as $k){ echo "首行 $k = ".substr((string)($it[$k]??""),0,120)."\n"; }
'
echo ""
echo "=== Handsontable 静态资源可访问 ==="
curl -s -o /dev/null -w "js HTTP=%{http_code}\n" http://127.0.0.1:8081/dist/handsontable/handsontable.full.min.js
curl -s -o /dev/null -w "css HTTP=%{http_code}\n" http://127.0.0.1:8081/dist/handsontable/handsontable.full.min.css
echo ""
echo "=== 完成 ==="

View File

@@ -0,0 +1,22 @@
#!/bin/bash
set -u
cd /var/www/onebot
JAR=/tmp/ob44.txt
rm -f $JAR
BODY=$(curl -s -c $JAR http://127.0.0.1:8081/auth/login)
CSRF=$(echo "$BODY" | grep -oE 'name="csrf-token" content="[^"]*"' | sed 's/.*content="//;s/"//')
curl -s -b $JAR -c $JAR -X POST http://127.0.0.1:8081/auth/login \
-d "username=ruiguo_wang@163.com" -d "password=wrg521113.." -d "_token=$CSRF" \
-H "X-CSRF-TOKEN: $CSRF" -o /dev/null
curl -s -b $JAR -o /tmp/ob44.html -w "detail HTTP=%{http_code}\n" http://127.0.0.1:8081/bom/show/3
echo "=== 关键改动检查 ==="
echo -n "filters 筛选插件: "; grep -c 'filters: true' /tmp/ob44.html
echo -n "dropdownMenu 列头下拉: "; grep -c 'dropdownMenu: true' /tmp/ob44.html
echo -n "search 搜索插件: "; grep -c 'search: true' /tmp/ob44.html
echo -n "stretchH 铺满: "; grep -c "stretchH: 'all'" /tmp/ob44.html
echo -n "calcHeight 动态高度: "; grep -c 'function calcHeight' /tmp/ob44.html
echo -n "window resize 重算: "; grep -c "'resize'" /tmp/ob44.html
echo -n "数据源管理按钮: "; grep -c 'btnSourceManager' /tmp/ob44.html
echo -n "数据源管理弹窗: "; grep -c 'sourceManagerModal' /tmp/ob44.html
echo -n "顶部源配置面板已移除(应为0): "; grep -c 'Teable 数据源</div>' /tmp/ob44.html

View File

@@ -0,0 +1,59 @@
#!/bin/bash
set -u
cd /var/www/onebot
JAR=/tmp/ob45.txt
rm -f $JAR
BODY=$(curl -s -c $JAR http://127.0.0.1:8081/auth/login)
CSRF=$(echo "$BODY" | grep -oE 'name="csrf-token" content="[^"]*"' | sed 's/.*content="//;s/"//')
curl -s -b $JAR -c $JAR -X POST http://127.0.0.1:8081/auth/login \
-d "username=ruiguo_wang@163.com" -d "password=wrg521113.." -d "_token=$CSRF" \
-H "X-CSRF-TOKEN: $CSRF" -o /dev/null -w "login HTTP=%{http_code}\n"
MASTER_ID=$(mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -N -e "SELECT id FROM zp_bom WHERE bomNo='1' ORDER BY id DESC LIMIT 1;" 2>/dev/null)
echo "masterId=$MASTER_ID"
echo ""
echo "=== 1. 项目关联主数据 ==="
LINK=$(curl -s -b $JAR -X POST http://127.0.0.1:8081/bom/api/ref/link \
-H "Content-Type: application/json" -H "X-CSRF-TOKEN: $CSRF" \
-d "{\"projectId\":1,\"masterId\":$MASTER_ID}")
echo "$LINK"
REF_ID=$(echo "$LINK" | grep -oE '"refId":[0-9]+' | head -1 | sed 's/"refId"://')
echo "refId=$REF_ID"
echo ""
echo "=== 2. 项目级追加列(采购数量) ==="
COL=$(curl -s -b $JAR -X POST "http://127.0.0.1:8081/bom/api/ref/$REF_ID/column" \
-H "Content-Type: application/json" -H "X-CSRF-TOKEN: $CSRF" \
-d '{"key":"采购数量","label":"采购数量"}')
echo "$COL"
COL_ID=$(echo "$COL" | grep -oE '"columnId":[0-9]+' | head -1 | sed 's/"columnId"://')
echo "columnId=$COL_ID"
echo ""
echo "=== 3. 项目级存值itemId 用第一条明细) ==="
ITEM_ID=$(mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -N -e "SELECT id FROM zp_bom_item WHERE bomId=$MASTER_ID ORDER BY id LIMIT 1;" 2>/dev/null)
echo "itemId=$ITEM_ID"
curl -s -b $JAR -X POST "http://127.0.0.1:8081/bom/api/ref/$REF_ID/value" \
-H "Content-Type: application/json" -H "X-CSRF-TOKEN: $CSRF" \
-d "{\"columnId\":$COL_ID,\"itemId\":$ITEM_ID,\"value\":\"999\"}" -w " save HTTP=%{http_code}\n"
echo ""
echo "=== 4. 验证:项目级值已写入引用层 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT * FROM zp_master_ref_value WHERE refId=$REF_ID;" 2>/dev/null
echo ""
echo "=== 5. 验证全局主数据行未被修改extra 不含采购数量) ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT id, extra LIKE '%采购数量%' AS has_col FROM zp_bom_item WHERE id=$ITEM_ID;" 2>/dev/null
echo ""
echo "=== 6. 引用详情接口 ==="
curl -s -b $JAR "http://127.0.0.1:8081/bom/api/ref/$REF_ID" | head -c 400
echo ""
echo ""
echo "=== 7. 清理测试数据 ==="
curl -s -b $JAR -X DELETE "http://127.0.0.1:8081/bom/api/ref/$REF_ID" \
-H "X-CSRF-TOKEN: $CSRF" -w "unlink HTTP=%{http_code}\n"
echo ""
echo "=== 完成 ==="

View File

@@ -0,0 +1,41 @@
#!/bin/bash
set -u
cd /var/www/onebot
JAR=/tmp/ob48.txt
rm -f $JAR
BODY=$(curl -s -c $JAR http://127.0.0.1:8081/auth/login)
CSRF=$(echo "$BODY" | grep -oE 'name="csrf-token" content="[^"]*"' | sed 's/.*content="//;s/"//')
curl -s -b $JAR -c $JAR -X POST http://127.0.0.1:8081/auth/login \
-d "username=ruiguo_wang@163.com" -d "password=wrg521113.." -d "_token=$CSRF" \
-H "X-CSRF-TOKEN: $CSRF" -o /dev/null -w "login HTTP=%{http_code}\n"
echo ""
echo "=== 1. 引用列表页 /bom/refs ==="
curl -s -b $JAR -o /tmp/ob48_refs.html -w "refs HTTP=%{http_code}\n" http://127.0.0.1:8081/bom/refs
grep -oE "项目引用的主数据|尚未关联" /tmp/ob48_refs.html | head -1
echo ""
echo "=== 2. 先关联一个主数据 ==="
MASTER_ID=$(mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -N -e "SELECT id FROM zp_bom WHERE bomNo='1' ORDER BY id DESC LIMIT 1;" 2>/dev/null)
echo "masterId=$MASTER_ID"
LINK=$(curl -s -b $JAR -X POST http://127.0.0.1:8081/bom/api/ref/link \
-H "Content-Type: application/json" -H "X-CSRF-TOKEN: $CSRF" \
-d "{\"projectId\":1,\"masterId\":$MASTER_ID}")
echo "$LINK"
REF_ID=$(echo "$LINK" | grep -oE '"refId":[0-9]+' | head -1 | sed 's/"refId"://')
echo "refId=$REF_ID"
echo ""
echo "=== 3. 引用详情页 /bom/refs/$REF_ID ==="
curl -s -b $JAR -o /tmp/ob48_refdetail.html -w "refDetail HTTP=%{http_code}\n" "http://127.0.0.1:8081/bom/refs/$REF_ID"
echo -n "标题含'引用视图': "; grep -c "引用视图" /tmp/ob48_refdetail.html
echo -n "增加项目列按钮: "; grep -c "btnAddRefColumn" /tmp/ob48_refdetail.html
echo -n "refVal 输入框: "; grep -c "refVal" /tmp/ob48_refdetail.html
echo ""
echo "=== 4. 清理测试引用 ==="
curl -s -b $JAR -X DELETE "http://127.0.0.1:8081/bom/api/ref/$REF_ID" \
-H "X-CSRF-TOKEN: $CSRF" -w "unlink HTTP=%{http_code}\n"
echo ""
echo "=== 完成 ==="

View File

@@ -0,0 +1,32 @@
#!/bin/bash
set -u
cd /var/www/onebot
JAR=/tmp/ob51.txt
rm -f $JAR
BODY=$(curl -s -c $JAR http://127.0.0.1:8081/auth/login)
CSRF=$(echo "$BODY" | grep -oE 'name="csrf-token" content="[^"]*"' | sed 's/.*content="//;s/"//')
curl -s -b $JAR -c $JAR -X POST http://127.0.0.1:8081/auth/login \
-d "username=ruiguo_wang@163.com" -d "password=wrg521113.." -d "_token=$CSRF" \
-H "X-CSRF-TOKEN: $CSRF" -o /dev/null -w "login HTTP=%{http_code}\n"
echo ""
echo "=== 1. 主数据列表(全局,与项目无关) ==="
curl -s -b $JAR "http://127.0.0.1:8081/bom/show?type=bom" -o /tmp/ob51_bom.html -w "bom list HTTP=%{http_code}\n"
echo -n "列表含 PRUAGP215: "; grep -c "PRUAGP215" /tmp/ob51_bom.html
echo -n "列表含 PR215: "; grep -c "PR215" /tmp/ob51_bom.html
echo ""
echo "=== 2. 主数据详情(全局 projectId=0鉴权用全局角色 ==="
curl -s -b $JAR "http://127.0.0.1:8081/bom/api/1" -o /tmp/ob51_detail.json -w "detail HTTP=%{http_code}\n"
echo -n "详情返回 projectId=0: "; grep -oE '"projectId":0' /tmp/ob51_detail.json | head -1
echo ""
echo "=== 3. 数据室菜单含主数据 + 图标 ==="
curl -s -b $JAR "http://127.0.0.1:8081/dashboard/show" -o /tmp/ob51_dash.html
echo -n "菜单含 BOM 图标 fa-list-check: "; grep -c "fa-list-check" /tmp/ob51_dash.html
echo -n "菜单含工艺文件: "; grep -c "工艺文件" /tmp/ob51_dash.html
echo -n "菜单含工具清单: "; grep -c "工具清单" /tmp/ob51_dash.html
echo -n "make分组不再含BOM(应为0): "; grep -c 'id="submenu-materialize"' /tmp/ob51_dash.html
echo ""
echo "=== 完成 ==="

View File

@@ -0,0 +1,33 @@
#!/bin/bash
cd /var/www/onebot
JAR=/tmp/ob64.txt
rm -f $JAR
BODY=$(curl -s -c $JAR http://127.0.0.1:8081/auth/login)
CSRF=$(echo "$BODY" | grep -oE 'name="csrf-token" content="[^"]*"' | sed 's/.*content="//;s/"//')
curl -s -b $JAR -c $JAR -X POST http://127.0.0.1:8081/auth/login \
-d "username=ruiguo_wang@163.com" -d "password=wrg521113.." -d "_token=$CSRF" \
-H "X-CSRF-TOKEN: $CSRF" -o /dev/null
curl -s -b $JAR "http://127.0.0.1:8081/bom/show/1" -o /tmp/ob64.html
php -r '
$h = file_get_contents("/tmp/ob64.html");
if (preg_match("/var RAW_ITEMS = (\[.*?\]);/s", $h, $m)) {
$items = json_decode($m[1], true);
echo "RAW_ITEMS 元素数: " . (is_array($items) ? count($items) : "解析失败") . "\n";
if (is_array($items) && isset($items[0])) {
echo "第1行完整: " . json_encode($items[0], JSON_UNESCAPED_UNICODE) . "\n";
echo "第1行字段数: " . count($items[0]) . "\n";
}
} else {
echo "未匹配 RAW_ITEMS\n";
}
if (preg_match("/var COLUMNS = (\[.*?\]);/s", $h, $m)) {
$cols = json_decode($m[1], true);
echo "COLUMNS 数: " . (is_array($cols) ? count($cols) : "解析失败") . "\n";
if (is_array($cols)) echo "COLUMNS keys: " . implode(",", array_column($cols, "key")) . "\n";
}
if (preg_match("/var FIXED = (\[.*?\]);/s", $h, $m)) {
$fixed = json_decode($m[1], true);
echo "FIXED keys: " . implode(",", $fixed) . "\n";
}
'

View File

@@ -0,0 +1,8 @@
#!/bin/bash
cd /var/www/onebot
echo "=== BOM3 第一条缩略图完整值 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -N -e "SELECT extra FROM zp_bom_item WHERE bomId=3 ORDER BY id LIMIT 1;" 2>/dev/null > /tmp/extra3.txt
php -r '$d=json_decode(file_get_contents("/tmp/extra3.txt"),true); echo "缩略图原始值: ".$d["缩略图"]."\n\n"; $t=json_decode($d["缩略图"]??"[]",true); if(is_array($t)&&isset($t[0])){ echo "=== 第一项字段 ===\n"; foreach($t[0] as $k=>$v){ if(is_string($v)&&strlen($v)>80){ echo $k.": ".substr($v,0,80)."... (len ".strlen($v).")\n"; } else { echo $k.": ".var_export($v,true)."\n"; } } }'
echo ""
echo "=== BOM3 数据源 baseUrl ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT id, name, baseUrl, tableId FROM zp_bom_source WHERE bomId=3;" 2>/dev/null

View File

@@ -0,0 +1,21 @@
#!/bin/bash
cd /var/www/onebot
JAR=/tmp/ob80.txt
rm -f $JAR
BODY=$(curl -s -c $JAR http://127.0.0.1:8081/auth/login)
CSRF=$(echo "$BODY" | grep -oE 'name="csrf-token" content="[^"]*"' | sed 's/.*content="//;s/"//')
curl -s -b $JAR -c $JAR -X POST http://127.0.0.1:8081/auth/login \
-d "username=ruiguo_wang@163.com" -d "password=wrg521113.." -d "_token=$CSRF" \
-H "X-CSRF-TOKEN: $CSRF" -o /dev/null
echo "=== 测试保存 BOM1 第一条明细itemId=251改 partNo 为测试值) ==="
curl -s -b $JAR -X POST http://127.0.0.1:8081/bom/api/1/item \
-H "Content-Type: application/json" -H "X-CSRF-TOKEN: $CSRF" \
-d '{"itemId":251,"seq":0,"partNo":"TEST-SAVE-1","partName":"测试保存","material":"","spec":"","qtyPerUnit":"","unit":"","process":"","remark":"","项目":"1","零件代号":"PR4391-"}'
echo ""
echo "=== 验证数据库是否写入 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "SELECT id, partNo, partName FROM zp_bom_item WHERE id=251;" 2>/dev/null
echo "=== 恢复原值 ==="
mysql -uonebot -p'OneBot_Db_7xKpQ9z' -h127.0.0.1 onebot -e "UPDATE zp_bom_item SET partNo='', partName='' WHERE id=251;" 2>/dev/null

View File

@@ -0,0 +1,52 @@
server {
listen 8081;
listen [::]:8081;
server_name _;
set $base /var/www/onebot;
root $base/public;
access_log /var/log/nginx/onebot.access.log;
error_log /var/log/nginx/onebot.error.log warn;
index index.php;
client_max_body_size 64M;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
try_files $fastcgi_script_name =404;
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
fastcgi_buffers 8 16k;
fastcgi_buffer_size 32k;
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
}
location = /favicon.ico {
log_not_found off;
access_log off;
}
location = /robots.txt {
log_not_found off;
access_log off;
}
location ^~ /dist/ {
expires 7d;
access_log off;
}
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/javascript;
gzip_disable "MSIE [1-6]\.";
}

142
.dev/test.env Normal file
View File

@@ -0,0 +1,142 @@
#
# This environment file is used for our testing framework
# It is not to be used for development
#
## Minimum Configuration, these are required for installation
APP_ENV=testing
APP_DEBUG=true
APP_KEY=base64:testing123testing123testing123testing123=
APP_URL=https://leantime-dev
LEAN_APP_URL = 'https://leantime-dev' # Base URL, only needed for subfolder installation
LEAN_APP_DIR = '' # Base of application without trailing slash (used for cookies), e.g, /leantime
LEAN_DEBUG = true # Debug flag
# Database
LEAN_DB_HOST=leantime-db
LEAN_DB_USER=root
LEAN_DB_PASSWORD=leantime
LEAN_DB_DATABASE=leantime_test
LEAN_DB_PORT=3306
## Optional Configuraiton, you may ommit these from your .env file
## Default Settings
LEAN_SITENAME = 'Leantime' # Name of your site, can be changed later
LEAN_LANGUAGE = 'en-US' # Default language
LEAN_DEFAULT_TIMEZONE = 'America/Los_Angeles' # Set default timezone
LEAN_ENABLE_MENU_TYPE = false # Enable to specifiy menu on aproject by project basis
LEAN_SESSION_PASSWORD = '3evBlq9zdUEuzKvVJHWWx3QzsQhturBApxwcws2m' #Salting sessions. Replace with a strong password
LEAN_SESSION_EXPIRATION = 28800 # How many seconds after inactivity should we logout? 28800seconds = 8hours
LEAN_LOG_PATH = '' # Default Log Path (including filename), if not set /logs/error.log will be used
## Look & Feel, these settings are available in the UI and can be overwritten there.
LEAN_LOGO_PATH = '/dist/images/logo.svg' # Default logo path, can be changed later
LEAN_PRINT_LOGO_URL = '/dist/images/logo.jpg' # Default logo URL use for printing (must be jpg or png format)
LEAN_DEFAULT_THEME = 'default' # Default theme
LEAN_PRIMARY_COLOR = '#006d9f' # Primary Theme color
LEAN_SECONDARY_COLOR = '#81B1A8' # Secondary Theme Color
LEAN_KEEP_THEME = true # Keep theme and language from previous user for login screen
## Fileuploads
# Local File Uploads
LEAN_USER_FILE_PATH = 'userfiles/' # Local relative path to store uploaded files (if not using S3)
LEAN_DB_BACKUP_PATH = 'backupdb/' # Local relative path to store backup files, need permission to write
# S3 File Uploads
LEAN_USE_S3 = false # Set to true if you want to use S3 instead of local files
LEAN_S3_KEY = 'AKIAIOSFODNN7EXAMPLE' # S3 Key, hardcoded in s3ninja
LEAN_S3_SECRET = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' # S3 Secret, hardcoded in s3ninja
LEAN_S3_BUCKET = 'leantime' # Your S3 bucket
LEAN_S3_USE_PATH_STYLE_ENDPOINT = true # Sets the endpoint style: false => https://[bucket].[endpoint] ; true => https://[endpoint]/[bucket]
LEAN_S3_REGION = 'eu-west-1' # S3 region
LEAN_S3_FOLDER_NAME = '' # Foldername within S3 (can be emtpy)
LEAN_S3_END_POINT = "http://s3ninja:9000" # S3 EndPoint S3 Compatible (https://sfo2.digitaloceanspaces.com)
## Email
LEAN_EMAIL_RETURN = 'leantime@test.local' # Return email address, needs to be valid email address format
LEAN_EMAIL_USE_SMTP = true # Use SMTP? If set to false, the default php mail() function will be used
LEAN_EMAIL_SMTP_HOSTS = 'maildev' # SMTP host
LEAN_EMAIL_SMTP_AUTH = false # SMTP authentication required
LEAN_EMAIL_SMTP_USERNAME = '' # SMTP username
LEAN_EMAIL_SMTP_PASSWORD = '' # SMTP password
LEAN_EMAIL_SMTP_AUTO_TLS = true # SMTP Enable TLS encryption automatically if a server supports it
LEAN_EMAIL_SMTP_SECURE = '' # SMTP Security protocol (usually one of: TLS, SSL, STARTTLS)
LEAN_EMAIL_SMTP_SSLNOVERIFY = true # SMTP Allow insecure SSL: Don't verify certificate, accept self-signed, etc.
LEAN_EMAIL_SMTP_PORT = '465' # Port (usually one of 25, 465, 587, 2526)
## Ldap
LEAN_LDAP_USE_LDAP = false # Set to true if you want to use LDAP
LEAN_LDAP_LDAP_TYPE = 'OL' # Select the correct directory type. Currently Supported: OL - OpenLdap, AD - Active Directory
LEAN_LDAP_HOST = '' # FQDN
LEAN_LDAP_PORT = 389 # Default Port
LEAN_LDAP_DN = '' # Location of users, example: CN=users,DC=example,DC=com
# Leantime->Ldap attribute mapping
LEAN_LDAP_KEYS = "{\"username\":\"uid\",\"groups\":\"memberOf\",\"email\":\"mail\",\"firstname\":\"displayname\",\"lastname\":\"\",\"phonenumber\":\"telephoneNumber\"}"
# For AD use these default attributes
# LEAN_LDAP_KEYS = "{
# \"username\":\"cn\",
# \"groups\":\"memberOf\",
# \"email\":\"mail\",
# \"firstname\":\"givenName\",
# \"lastname\":\"sn\",
# \"phonenumber\":\"telephoneNumber\"
# }"
LEAN_LDAP_DEFAULT_ROLE_KEY = 20; # Default Leantime Role on creation. (set to editor)
# Default role assignments upon first login.
# (Optional) Can be updated later in user settings for each user
LEAN_LDAP_GROUP_ASSIGNMENT = "{\"5\": {\"ltRole\":\"readonly\",\"ldapRole\":\"readonly\"},\"10\": {\"ltRole\":\"commenter\",\"ldapRole\":\"commenter\"},\"20\": {\"ltRole\":\"editor\",\"ldapRole\":\"editor\"},\"30\": {\"ltRole\":\"manager\",\"ldapRole\":\"manager\"},\"40\": {\"ltRole\":\"admin\",\"ldapRole\":\"administrators\"},\"50\": {\"ltRole\":\"owner\",\"ldapRole\":\"administrators\"}}"
## OpenID Connect
# required
LEAN_OIDC_ENABLE = true
LEAN_OIDC_CLIEND_ID =
LEAN_OIDC_CLIEND_SECRET =
# required - the url for your provider (examples down below)
#LEAN_OIDC_PROVIDER_URL =
# optional - these will be read from the well-known configuration if possible
#LEAN_OIDC_AUTH_URL_OVERRIDE =
#LEAN_OIDC_TOKEN_URL_OVERRIDE =
#LEAN_OIDC_JWKS_URL_OVERRIDE =
#LEAN_OIDC_USERINFO_URL_OVERRIDE =
# optional - override the public key for RSA validation
#LEAN_OIDC_CERTIFICATE_STRING =
#LEAN_OIDC_CERTIFICATE_FILE =
# optional - override the requested scopes
#LEAN_OIDC_SCOPES =
# optional - override the keys used for these fields
#LEAN_OIDC_FIELD_EMAIL =
#LEAN_OIDC_FIELD_FIRSTNAME =
#LEAN_OIDC_FIELD_LASTNAME =
## OpenID Connect setting for github
#LEAN_OIDC_PROVIDER_URL = https://token.actions.githubusercontent.com/
#LEAN_OIDC_AUTH_URL_OVERRIDE = https://github.com/login/oauth/authorize
#LEAN_OIDC_TOKEN_URL_OVERRIDE = https://github.com/login/oauth/access_token
#LEAN_OIDC_USERINFO_URL_OVERRIDE = https://api.github.com/user,https://api.github.com/user/emails
#LEAN_OIDC_SCOPES = user:email
#LEAN_OIDC_FIELD_EMAIL = 0.email
LEAN_RATELIMIT_GENERAL = 50000
LEAN_RATELIMIT_AUTH = 50000
LEAN_RATELIMIT_API = 50000
# Cache Configuration for testing (use file cache instead of Redis)
LEAN_USE_REDIS=false
CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_CONNECTION=sync

10
.dev/xdebug.ini Normal file
View File

@@ -0,0 +1,10 @@
zend_extension=xdebug
[xdebug]
xdebug.mode=debug
xdebug.discover_client_host=1
xdebug.client_host=host.docker.internal; do not touch
xdebug.start_with_request=yes
xdebug.client_port=9003
xdebug.idekey="PHPSTORM"
xdebug.log=/var/www/html/storage/logs/xdebug.log