Compare commits
No commits in common. "v1.0.13" and "main" have entirely different histories.
9 changed files with 528 additions and 44 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
# 文派统一插件发布 CI Workflow
|
# 文派统一插件发布 CI Workflow
|
||||||
# 触发:push tag v*
|
# 触发:push tag v*
|
||||||
# 运行环境:forgejo-ci-php:latest (Alpine + php-cli + git + rsync + zip + node)
|
# 运行环境:forgejo-ci-php:latest (Alpine + php-cli + git + rsync + zip + node)
|
||||||
|
# 基于 ci-workflows/wp-release.yml 2026-02-18 版本
|
||||||
name: Release Plugin
|
name: Release Plugin
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|
@ -17,17 +18,18 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Verify tools
|
- name: Verify tools
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
php -v | head -1 || true
|
||||||
php -v | head -1
|
|
||||||
git --version
|
git --version
|
||||||
rsync --version | head -1
|
rsync --version | head -1 || true
|
||||||
zip --version | head -2
|
zip --version | head -2 || true
|
||||||
jq --version
|
jq --version
|
||||||
curl --version | head -1
|
curl --version | head -1 || true
|
||||||
|
|
||||||
- name: Extract version from tag
|
- name: Extract version from tag
|
||||||
id: version
|
id: version
|
||||||
|
|
@ -74,6 +76,58 @@ jobs:
|
||||||
fi
|
fi
|
||||||
echo "Version consistency check passed: $VERSION"
|
echo "Version consistency check passed: $VERSION"
|
||||||
|
|
||||||
|
- name: Generate Changelog
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TAG="${GITHUB_REF#refs/tags/}"
|
||||||
|
|
||||||
|
PREV_TAG=$(git tag --sort=-version:refname | grep -E '^v' | grep -v "^${TAG}$" | head -1)
|
||||||
|
|
||||||
|
if [ -n "$PREV_TAG" ]; then
|
||||||
|
echo "Changelog: ${PREV_TAG}..${TAG}"
|
||||||
|
COMMITS=$(git log "${PREV_TAG}..${TAG}" --no-merges --pretty=format:"%s (%h)" 2>/dev/null || true)
|
||||||
|
else
|
||||||
|
echo "Changelog: first release, using all commits"
|
||||||
|
COMMITS=$(git log --no-merges --pretty=format:"%s (%h)" 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$COMMITS" ]; then
|
||||||
|
: > /tmp/changelog.md
|
||||||
|
echo "No commits found for changelog"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
FEAT="" FIX="" OTHER=""
|
||||||
|
while IFS= read -r line; do
|
||||||
|
case "$line" in
|
||||||
|
feat:*|feat\(*) FEAT="${FEAT}\n- ${line}" ;;
|
||||||
|
fix:*|fix\(*) FIX="${FIX}\n- ${line}" ;;
|
||||||
|
*) OTHER="${OTHER}\n- ${line}" ;;
|
||||||
|
esac
|
||||||
|
done <<< "$COMMITS"
|
||||||
|
|
||||||
|
CHANGELOG=""
|
||||||
|
HAS_CATEGORIES=false
|
||||||
|
if [ -n "$FEAT" ]; then
|
||||||
|
CHANGELOG="${CHANGELOG}#### New Features${FEAT}\n\n"
|
||||||
|
HAS_CATEGORIES=true
|
||||||
|
fi
|
||||||
|
if [ -n "$FIX" ]; then
|
||||||
|
CHANGELOG="${CHANGELOG}#### Bug Fixes${FIX}\n\n"
|
||||||
|
HAS_CATEGORIES=true
|
||||||
|
fi
|
||||||
|
if [ -n "$OTHER" ]; then
|
||||||
|
if [ "$HAS_CATEGORIES" = true ]; then
|
||||||
|
CHANGELOG="${CHANGELOG}#### Other Changes${OTHER}\n\n"
|
||||||
|
else
|
||||||
|
CHANGELOG="${OTHER#\\n}\n\n"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%b' "$CHANGELOG" > /tmp/changelog.md
|
||||||
|
echo "Changelog generated ($(echo "$COMMITS" | wc -l) commits)"
|
||||||
|
|
||||||
- name: PHP Lint
|
- name: PHP Lint
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -113,8 +167,13 @@ jobs:
|
||||||
--exclude=".gitattributes" \
|
--exclude=".gitattributes" \
|
||||||
--exclude=".editorconfig" \
|
--exclude=".editorconfig" \
|
||||||
--exclude=".env*" \
|
--exclude=".env*" \
|
||||||
|
--exclude=".agent" \
|
||||||
|
--exclude=".vscode" \
|
||||||
--exclude="node_modules" \
|
--exclude="node_modules" \
|
||||||
|
--exclude="vendor" \
|
||||||
--exclude="tests" \
|
--exclude="tests" \
|
||||||
|
--exclude="docs" \
|
||||||
|
--exclude="lib" \
|
||||||
--exclude="phpunit.xml*" \
|
--exclude="phpunit.xml*" \
|
||||||
--exclude="phpcs.xml*" \
|
--exclude="phpcs.xml*" \
|
||||||
--exclude="phpstan.neon*" \
|
--exclude="phpstan.neon*" \
|
||||||
|
|
@ -127,7 +186,6 @@ jobs:
|
||||||
--exclude="*.md" \
|
--exclude="*.md" \
|
||||||
--exclude="LICENSE" \
|
--exclude="LICENSE" \
|
||||||
--exclude="Makefile" \
|
--exclude="Makefile" \
|
||||||
--exclude="lib" \
|
|
||||||
./ "$BUILD_DIR/"
|
./ "$BUILD_DIR/"
|
||||||
|
|
||||||
(
|
(
|
||||||
|
|
@ -161,8 +219,9 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
if [ -z "${RELEASE_TOKEN:-}" ]; then
|
AUTH_TOKEN="${RELEASE_TOKEN:-${GITHUB_TOKEN:-}}"
|
||||||
echo "::error::Missing RELEASE_TOKEN secret"
|
if [ -z "$AUTH_TOKEN" ]; then
|
||||||
|
echo "::error::Missing auth token: set RELEASE_TOKEN or use default GITHUB_TOKEN"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -172,8 +231,24 @@ jobs:
|
||||||
ZIP_NAME="${{ steps.build.outputs.zip_name }}"
|
ZIP_NAME="${{ steps.build.outputs.zip_name }}"
|
||||||
SHA256="${{ steps.checksum.outputs.sha256 }}"
|
SHA256="${{ steps.checksum.outputs.sha256 }}"
|
||||||
API_URL="${GITHUB_SERVER_URL%/}/api/v1/repos/${GITHUB_REPOSITORY}"
|
API_URL="${GITHUB_SERVER_URL%/}/api/v1/repos/${GITHUB_REPOSITORY}"
|
||||||
AUTH_HEADER="Authorization: token ${RELEASE_TOKEN}"
|
AUTH_HEADER="Authorization: token ${AUTH_TOKEN}"
|
||||||
printf -v RELEASE_NOTES '## %s %s\n\n### Checksums\n\n| File | SHA-256 |\n|------|---------|\n| %s | %s |\n' "$SLUG" "$TAG" "$ZIP_NAME" "$SHA256"
|
|
||||||
|
{
|
||||||
|
echo "## ${SLUG} ${TAG}"
|
||||||
|
echo ""
|
||||||
|
CHANGELOG_FILE="/tmp/changelog.md"
|
||||||
|
if [ -f "$CHANGELOG_FILE" ] && [ -s "$CHANGELOG_FILE" ]; then
|
||||||
|
echo "### What's Changed"
|
||||||
|
echo ""
|
||||||
|
cat "$CHANGELOG_FILE"
|
||||||
|
fi
|
||||||
|
echo "### Checksums"
|
||||||
|
echo ""
|
||||||
|
echo "| File | SHA-256 |"
|
||||||
|
echo "|------|---------|"
|
||||||
|
echo "| ${ZIP_NAME} | ${SHA256} |"
|
||||||
|
} > /tmp/release-notes.md
|
||||||
|
RELEASE_NOTES=$(cat /tmp/release-notes.md)
|
||||||
|
|
||||||
echo ">>> Resolving release ${TAG}"
|
echo ">>> Resolving release ${TAG}"
|
||||||
STATUS=$(curl -sS -o /tmp/release.json -w "%{http_code}" \
|
STATUS=$(curl -sS -o /tmp/release.json -w "%{http_code}" \
|
||||||
|
|
@ -243,3 +318,119 @@ jobs:
|
||||||
echo ">>> Release ${TAG} published successfully"
|
echo ">>> Release ${TAG} published successfully"
|
||||||
echo "Release URL: ${GITHUB_SERVER_URL%/}/${GITHUB_REPOSITORY}/releases/tag/${TAG}"
|
echo "Release URL: ${GITHUB_SERVER_URL%/}/${GITHUB_REPOSITORY}/releases/tag/${TAG}"
|
||||||
|
|
||||||
|
- name: Run post-release composer repair on target site (optional)
|
||||||
|
if: ${{ secrets.DEPLOY_HOST != '' && secrets.DEPLOY_SSH_KEY != '' }}
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||||
|
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
|
||||||
|
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||||
|
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
|
DEPLOY_COMMAND_MODE: ${{ secrets.DEPLOY_COMMAND_MODE }}
|
||||||
|
DEPLOY_SITE_PATH: ${{ secrets.DEPLOY_SITE_PATH }}
|
||||||
|
DEPLOY_WP_USER: ${{ secrets.DEPLOY_WP_USER }}
|
||||||
|
DEPLOY_COMPOSER_SCRIPT: ${{ secrets.DEPLOY_COMPOSER_SCRIPT }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${DEPLOY_HOST:?DEPLOY_HOST is required when this step is enabled}"
|
||||||
|
DEPLOY_PORT="${DEPLOY_PORT:-22}"
|
||||||
|
DEPLOY_USER="${DEPLOY_USER:-wpdeploy}"
|
||||||
|
DEPLOY_COMMAND_MODE="${DEPLOY_COMMAND_MODE:-runner}"
|
||||||
|
SLUG="${{ env.PLUGIN_SLUG }}"
|
||||||
|
|
||||||
|
if ! command -v ssh >/dev/null 2>&1; then
|
||||||
|
if command -v apk >/dev/null 2>&1; then
|
||||||
|
apk add --no-cache openssh-client >/dev/null
|
||||||
|
else
|
||||||
|
echo "::error::ssh client not found in runner image"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -m 700 -d ~/.ssh
|
||||||
|
KEY_PATH="$HOME/.ssh/deploy_key"
|
||||||
|
printf '%s\n' "$DEPLOY_SSH_KEY" > "$KEY_PATH"
|
||||||
|
chmod 600 "$KEY_PATH"
|
||||||
|
ssh-keyscan -t rsa,ecdsa,ed25519 -p "$DEPLOY_PORT" -H "$DEPLOY_HOST" >> "$HOME/.ssh/known_hosts" 2>/dev/null || true
|
||||||
|
|
||||||
|
if [[ "$DEPLOY_COMMAND_MODE" == "runner" ]]; then
|
||||||
|
DEPLOY_COMPOSER_SCRIPT="${DEPLOY_COMPOSER_SCRIPT:-/usr/local/sbin/wp-post-release-composer-runner}"
|
||||||
|
ssh -i "$KEY_PATH" -p "$DEPLOY_PORT" -o BatchMode=yes -o StrictHostKeyChecking=yes "${DEPLOY_USER}@${DEPLOY_HOST}" \
|
||||||
|
"sudo '$DEPLOY_COMPOSER_SCRIPT' '$SLUG'"
|
||||||
|
else
|
||||||
|
DEPLOY_SITE_PATH="${DEPLOY_SITE_PATH:-/www/wwwroot/wptea.com}"
|
||||||
|
DEPLOY_WP_USER="${DEPLOY_WP_USER:-www}"
|
||||||
|
DEPLOY_COMPOSER_SCRIPT="${DEPLOY_COMPOSER_SCRIPT:-/opt/wenpai-infra/ops/wp-post-release-composer.sh}"
|
||||||
|
ssh -i "$KEY_PATH" -p "$DEPLOY_PORT" -o BatchMode=yes -o StrictHostKeyChecking=yes "${DEPLOY_USER}@${DEPLOY_HOST}" \
|
||||||
|
"bash '$DEPLOY_COMPOSER_SCRIPT' --site '$DEPLOY_SITE_PATH' --wp-user '$DEPLOY_WP_USER' --plugin '$SLUG' --strict"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Verify update API metadata (optional)
|
||||||
|
if: ${{ secrets.UPDATE_API_BASE != '' }}
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
UPDATE_API_BASE: ${{ secrets.UPDATE_API_BASE }}
|
||||||
|
VERIFY_FROM_VERSION: ${{ secrets.VERIFY_FROM_VERSION }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
API_BASE="${UPDATE_API_BASE%/}"
|
||||||
|
FROM_VERSION="${VERIFY_FROM_VERSION:-0.0.0}"
|
||||||
|
SLUG="${{ env.PLUGIN_SLUG }}"
|
||||||
|
MAIN_FILE="${{ steps.detect.outputs.main_file }}"
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
TAG="${{ steps.version.outputs.tag }}"
|
||||||
|
PLUGIN_FILE="${SLUG}/${MAIN_FILE}"
|
||||||
|
|
||||||
|
echo "Verify update-check: ${PLUGIN_FILE} from ${FROM_VERSION} -> ${VERSION}"
|
||||||
|
REQUEST_JSON=$(jq -cn --arg plugin_file "$PLUGIN_FILE" --arg from "$FROM_VERSION" '{plugins: {($plugin_file): {Version: $from}}}')
|
||||||
|
|
||||||
|
UPDATE_OK=0
|
||||||
|
for attempt in $(seq 1 36); do
|
||||||
|
curl -sS -f -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
--data "$REQUEST_JSON" \
|
||||||
|
"${API_BASE}/api/v1/update-check" > /tmp/update-check.json
|
||||||
|
|
||||||
|
API_VERSION=$(jq -r --arg plugin_file "$PLUGIN_FILE" '.plugins[$plugin_file].version // empty' /tmp/update-check.json)
|
||||||
|
API_PACKAGE=$(jq -r --arg plugin_file "$PLUGIN_FILE" '.plugins[$plugin_file].package // empty' /tmp/update-check.json)
|
||||||
|
|
||||||
|
if [[ "$API_VERSION" == "$VERSION" && "$API_PACKAGE" == *"/releases/download/${TAG}/"* ]]; then
|
||||||
|
UPDATE_OK=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[attempt ${attempt}/36] update-check not ready, version=${API_VERSION:-<empty>} package=${API_PACKAGE:-<empty>}"
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$UPDATE_OK" -ne 1 ]]; then
|
||||||
|
echo "::error::update-check verification timeout"
|
||||||
|
cat /tmp/update-check.json
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Verify plugin info: ${SLUG}"
|
||||||
|
INFO_OK=0
|
||||||
|
for attempt in $(seq 1 36); do
|
||||||
|
curl -sS -f "${API_BASE}/api/v1/plugins/${SLUG}/info" > /tmp/plugin-info.json
|
||||||
|
INFO_VERSION=$(jq -r '.version // empty' /tmp/plugin-info.json)
|
||||||
|
INFO_PACKAGE=$(jq -r '.download_link // empty' /tmp/plugin-info.json)
|
||||||
|
|
||||||
|
if [[ "$INFO_VERSION" == "$VERSION" && "$INFO_PACKAGE" == *"/releases/download/${TAG}/"* ]]; then
|
||||||
|
INFO_OK=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[attempt ${attempt}/36] plugin-info not ready, version=${INFO_VERSION:-<empty>} download=${INFO_PACKAGE:-<empty>}"
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$INFO_OK" -ne 1 ]]; then
|
||||||
|
echo "::error::plugin-info verification timeout"
|
||||||
|
cat /tmp/plugin-info.json
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Update API verification passed"
|
||||||
|
|
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
vendor/
|
||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
|
@ -31,7 +31,7 @@ class WenPai_Updater {
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
const VERSION = '1.0.0';
|
const VERSION = '1.1.0';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 云桥 API 地址。
|
* 云桥 API 地址。
|
||||||
|
|
@ -168,24 +168,50 @@ class WenPai_Updater {
|
||||||
|
|
||||||
$response = $this->api_request( "plugins/{$this->slug}/info" );
|
$response = $this->api_request( "plugins/{$this->slug}/info" );
|
||||||
|
|
||||||
if ( is_wp_error( $response ) ) {
|
if ( ! is_wp_error( $response ) && ! isset( $response['error'] ) && ! empty( $response['name'] ) ) {
|
||||||
|
$info = new stdClass();
|
||||||
|
$info->name = $response['name'];
|
||||||
|
$info->slug = $response['slug'] ?? $this->slug;
|
||||||
|
$info->version = $response['version'] ?? '';
|
||||||
|
$info->author = $response['author'] ?? '';
|
||||||
|
$info->homepage = $response['homepage'] ?? '';
|
||||||
|
$info->download_link = $response['download_link'] ?? '';
|
||||||
|
$info->requires = $response['requires'] ?? '';
|
||||||
|
$info->tested = $response['tested'] ?? '';
|
||||||
|
$info->requires_php = $response['requires_php'] ?? '';
|
||||||
|
$info->last_updated = $response['last_updated'] ?? '';
|
||||||
|
$info->icons = $response['icons'] ?? [];
|
||||||
|
$info->banners = $response['banners'] ?? [];
|
||||||
|
$info->sections = $response['sections'] ?? [];
|
||||||
|
$info->external = true;
|
||||||
|
|
||||||
|
return $info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 不可用或插件未注册时,用本地插件头信息兜底
|
||||||
|
$plugin_path = WP_PLUGIN_DIR . '/' . $this->plugin_file;
|
||||||
|
if ( ! file_exists( $plugin_path ) ) {
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
$info = new stdClass();
|
if ( ! function_exists( 'get_plugin_data' ) ) {
|
||||||
$info->name = $response['name'] ?? '';
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
$info->slug = $response['slug'] ?? $this->slug;
|
}
|
||||||
$info->version = $response['version'] ?? '';
|
|
||||||
$info->author = $response['author'] ?? '';
|
$plugin_data = get_plugin_data( $plugin_path );
|
||||||
$info->homepage = $response['homepage'] ?? '';
|
|
||||||
$info->download_link = $response['download_link'] ?? '';
|
$info = new stdClass();
|
||||||
$info->requires = $response['requires'] ?? '';
|
$info->name = $plugin_data['Name'] ?? $this->slug;
|
||||||
$info->tested = $response['tested'] ?? '';
|
$info->slug = $this->slug;
|
||||||
$info->requires_php = $response['requires_php'] ?? '';
|
$info->version = $this->version;
|
||||||
$info->last_updated = $response['last_updated'] ?? '';
|
$info->author = $plugin_data['AuthorName'] ?? '';
|
||||||
$info->icons = $response['icons'] ?? [];
|
$info->homepage = $plugin_data['PluginURI'] ?? '';
|
||||||
$info->banners = $response['banners'] ?? [];
|
$info->requires = $plugin_data['RequiresWP'] ?? '';
|
||||||
$info->sections = $response['sections'] ?? [];
|
$info->requires_php = $plugin_data['RequiresPHP'] ?? '';
|
||||||
|
$info->sections = [
|
||||||
|
'description' => $plugin_data['Description'] ?? '',
|
||||||
|
];
|
||||||
|
$info->external = true;
|
||||||
|
|
||||||
return $info;
|
return $info;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,10 +127,10 @@ class WPSlug_Admin
|
||||||
<span style="font-size: 13px; padding-left: 10px;">
|
<span style="font-size: 13px; padding-left: 10px;">
|
||||||
<?php printf( esc_html__( 'Version: %s', 'wpslug' ), esc_html( WPSLUG_VERSION ) ); ?>
|
<?php printf( esc_html__( 'Version: %s', 'wpslug' ), esc_html( WPSLUG_VERSION ) ); ?>
|
||||||
</span>
|
</span>
|
||||||
<a href="https://wpslug.com/document" target="_blank" class="button button-secondary" style="margin-left: 10px;">
|
<a href="https://wpcy.com/slug/" target="_blank" class="button button-secondary" style="margin-left: 10px;">
|
||||||
<?php esc_html_e( 'Documentation', 'wpslug' ); ?>
|
<?php esc_html_e( 'Documentation', 'wpslug' ); ?>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://sharecms.com/forums/" target="_blank" class="button button-secondary">
|
<a href="https://wpcy.com/c/wpslug/" target="_blank" class="button button-secondary">
|
||||||
<?php esc_html_e( 'Support', 'wpslug' ); ?>
|
<?php esc_html_e( 'Support', 'wpslug' ); ?>
|
||||||
</a>
|
</a>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
@ -740,6 +740,35 @@ class WPSlug_Admin
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="wpslug-api-section" data-service="wpmind">
|
||||||
|
<h4><?php _e("WPMind AI Translation", "wpslug"); ?></h4>
|
||||||
|
<div class="wpslug-wpmind-status">
|
||||||
|
<?php if (function_exists('wpmind_is_available') && wpmind_is_available()): ?>
|
||||||
|
<p class="description" style="color: #2e7d32;">
|
||||||
|
<span class="dashicons dashicons-yes-alt"></span>
|
||||||
|
<?php _e("WPMind is active and configured. No additional settings required.", "wpslug"); ?>
|
||||||
|
</p>
|
||||||
|
<p class="description">
|
||||||
|
<?php _e("WPMind uses AI to translate titles to SEO-friendly slugs. It provides more accurate and context-aware translations compared to traditional translation services.", "wpslug"); ?>
|
||||||
|
</p>
|
||||||
|
<p class="description">
|
||||||
|
<a href="<?php echo admin_url('options-general.php?page=wpmind'); ?>">
|
||||||
|
<?php _e("Configure WPMind Settings", "wpslug"); ?> →
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="description" style="color: #d32f2f;">
|
||||||
|
<span class="dashicons dashicons-warning"></span>
|
||||||
|
<?php _e("WPMind plugin is not active or not configured.", "wpslug"); ?>
|
||||||
|
</p>
|
||||||
|
<p class="description">
|
||||||
|
<?php _e("Please install and activate the WPMind plugin to use AI-powered translation.", "wpslug"); ?>
|
||||||
|
<a href="https://wpcy.com/mind/" target="_blank"><?php _e("Learn more", "wpslug"); ?></a>
|
||||||
|
</p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ class WPSlug_Converter {
|
||||||
case 'pinyin':
|
case 'pinyin':
|
||||||
$result = $this->convertPinyin($text, $options);
|
$result = $this->convertPinyin($text, $options);
|
||||||
break;
|
break;
|
||||||
|
case 'semantic_pinyin':
|
||||||
|
$result = $this->convertSemanticPinyin($text, $options);
|
||||||
|
break;
|
||||||
case 'transliteration':
|
case 'transliteration':
|
||||||
$result = $this->convertTransliteration($text, $options);
|
$result = $this->convertTransliteration($text, $options);
|
||||||
break;
|
break;
|
||||||
|
|
@ -71,6 +74,89 @@ class WPSlug_Converter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语义化拼音转换(使用 WPMind AI)
|
||||||
|
*
|
||||||
|
* 与普通拼音不同,这个方法会按词语分隔而非按字分隔
|
||||||
|
* 例如 "你好世界" → "nihao-shijie" 而非 "ni-hao-shi-jie"
|
||||||
|
*/
|
||||||
|
private function convertSemanticPinyin($text, $options) {
|
||||||
|
$debug_mode = isset($options['debug_mode']) && $options['debug_mode'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 检查是否是中文
|
||||||
|
if ($this->detectLanguage($text) !== 'zh') {
|
||||||
|
return $this->cleanBasicSlug($text, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查 WPMind 是否可用
|
||||||
|
if (!function_exists('wpmind_pinyin') || !function_exists('wpmind_is_available') || !wpmind_is_available()) {
|
||||||
|
if ($debug_mode) {
|
||||||
|
error_log('[WPSlug] WPMind not available for semantic pinyin, falling back to regular pinyin');
|
||||||
|
}
|
||||||
|
return $this->convertPinyin($text, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 文本长度限制(避免超时)
|
||||||
|
if (mb_strlen($text) > 200) {
|
||||||
|
if ($debug_mode) {
|
||||||
|
error_log('[WPSlug] Text too long for semantic pinyin, using regular pinyin');
|
||||||
|
}
|
||||||
|
return $this->convertPinyin($text, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 调用 WPMind AI 进行语义化拼音转换
|
||||||
|
$result = wpmind_pinyin($text, [
|
||||||
|
'context' => 'wpslug_semantic_pinyin',
|
||||||
|
'cache_ttl' => 604800, // 7天
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 5. 处理结果
|
||||||
|
if (is_wp_error($result)) {
|
||||||
|
if ($debug_mode) {
|
||||||
|
error_log('[WPSlug] WPMind pinyin error: ' . $result->get_error_message());
|
||||||
|
}
|
||||||
|
return $this->convertPinyin($text, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 清理结果
|
||||||
|
$slug = $this->cleanSemanticPinyinResult($result, $options);
|
||||||
|
|
||||||
|
if (empty($slug)) {
|
||||||
|
if ($debug_mode) {
|
||||||
|
error_log('[WPSlug] WPMind returned empty pinyin, using regular pinyin');
|
||||||
|
}
|
||||||
|
return $this->convertPinyin($text, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($debug_mode) {
|
||||||
|
error_log('[WPSlug] Semantic pinyin: "' . $text . '" → "' . $slug . '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $slug;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->settings->logError('Semantic pinyin error: ' . $e->getMessage());
|
||||||
|
return $this->convertPinyin($text, $options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理语义化拼音结果
|
||||||
|
*/
|
||||||
|
private function cleanSemanticPinyinResult($text, $options) {
|
||||||
|
// 移除可能的引号和空格
|
||||||
|
$text = trim($text, " \t\n\r\0\x0B\"'");
|
||||||
|
|
||||||
|
// 确保只有小写字母、数字和连字符
|
||||||
|
$text = preg_replace('/[^a-zA-Z0-9\-]/', '-', $text);
|
||||||
|
$text = strtolower($text);
|
||||||
|
$text = preg_replace('/-+/', '-', $text);
|
||||||
|
$text = trim($text, '-');
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
private function convertTransliteration($text, $options) {
|
private function convertTransliteration($text, $options) {
|
||||||
try {
|
try {
|
||||||
$detected_lang = $this->detectLanguage($text);
|
$detected_lang = $this->detectLanguage($text);
|
||||||
|
|
@ -163,7 +249,7 @@ class WPSlug_Converter {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSupportedModes() {
|
public function getSupportedModes() {
|
||||||
return array('pinyin', 'transliteration', 'translation');
|
return array('pinyin', 'semantic_pinyin', 'transliteration', 'translation');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isModeSupported($mode) {
|
public function isModeSupported($mode) {
|
||||||
|
|
|
||||||
|
|
@ -237,14 +237,24 @@ class WPSlug_Settings
|
||||||
|
|
||||||
public function getConversionModes()
|
public function getConversionModes()
|
||||||
{
|
{
|
||||||
return [
|
$modes = [
|
||||||
"pinyin" => __("Chinese Pinyin Conversion", "wpslug"),
|
"pinyin" => __("Chinese Pinyin Conversion", "wpslug"),
|
||||||
"transliteration" => __(
|
|
||||||
"Foreign Language Transliteration",
|
|
||||||
"wpslug"
|
|
||||||
),
|
|
||||||
"translation" => __("Multi-language Translation", "wpslug"),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 如果 WPMind 可用,添加语义化拼音选项
|
||||||
|
if (function_exists('wpmind_is_available') && wpmind_is_available()) {
|
||||||
|
$modes["semantic_pinyin"] = __("Semantic Pinyin (WPMind AI)", "wpslug");
|
||||||
|
} elseif (class_exists('\\WPMind\\WPMind')) {
|
||||||
|
$modes["semantic_pinyin"] = __("Semantic Pinyin (Requires WPMind)", "wpslug");
|
||||||
|
}
|
||||||
|
|
||||||
|
$modes["transliteration"] = __(
|
||||||
|
"Foreign Language Transliteration",
|
||||||
|
"wpslug"
|
||||||
|
);
|
||||||
|
$modes["translation"] = __("Multi-language Translation", "wpslug");
|
||||||
|
|
||||||
|
return $modes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTransliterationMethods()
|
public function getTransliterationMethods()
|
||||||
|
|
@ -266,11 +276,25 @@ class WPSlug_Settings
|
||||||
|
|
||||||
public function getTranslationServices()
|
public function getTranslationServices()
|
||||||
{
|
{
|
||||||
return [
|
$services = [
|
||||||
"none" => __("None", "wpslug"),
|
"none" => __("None", "wpslug"),
|
||||||
"google" => __("Google Translate", "wpslug"),
|
"google" => __("Google Translate", "wpslug"),
|
||||||
"baidu" => __("Baidu Translate", "wpslug"),
|
"baidu" => __("Baidu Translate", "wpslug"),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 动态检测 WPMind 是否可用
|
||||||
|
if (function_exists('wpmind_is_available') && wpmind_is_available()) {
|
||||||
|
// WPMind 可用,添加到服务列表顶部(推荐)
|
||||||
|
$services = array_merge(
|
||||||
|
["wpmind" => __("WPMind AI (Recommended)", "wpslug")],
|
||||||
|
$services
|
||||||
|
);
|
||||||
|
} elseif (class_exists('\\WPMind\\WPMind')) {
|
||||||
|
// WPMind 已安装但未配置
|
||||||
|
$services["wpmind"] = __("WPMind AI (Not Configured)", "wpslug");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLanguages()
|
public function getLanguages()
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,15 @@ if (!defined('ABSPATH')) {
|
||||||
class WPSlug_Translator {
|
class WPSlug_Translator {
|
||||||
private $converter;
|
private $converter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防止循环调用的静态标志
|
||||||
|
* 当使用 WPMind 翻译时,WPMind 内部可能触发 sanitize_title filter
|
||||||
|
* 如果没有保护,会导致无限递归
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private static $is_translating = false;
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
$this->converter = null;
|
$this->converter = null;
|
||||||
}
|
}
|
||||||
|
|
@ -16,9 +25,23 @@ class WPSlug_Translator {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 防止循环调用:如果正在翻译中,直接回退到拼音
|
||||||
|
if (self::$is_translating) {
|
||||||
|
return $this->fallbackTranslate($text, $options);
|
||||||
|
}
|
||||||
|
|
||||||
$service = isset($options['translation_service']) ? $options['translation_service'] : 'none';
|
$service = isset($options['translation_service']) ? $options['translation_service'] : 'none';
|
||||||
|
|
||||||
switch ($service) {
|
switch ($service) {
|
||||||
|
case 'wpmind':
|
||||||
|
// WPMind 服务需要循环保护
|
||||||
|
self::$is_translating = true;
|
||||||
|
try {
|
||||||
|
$result = $this->translateWPMind($text, $options);
|
||||||
|
} finally {
|
||||||
|
self::$is_translating = false;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
case 'google':
|
case 'google':
|
||||||
return $this->translateGoogle($text, $options);
|
return $this->translateGoogle($text, $options);
|
||||||
case 'baidu':
|
case 'baidu':
|
||||||
|
|
@ -166,6 +189,96 @@ class WPSlug_Translator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用 WPMind AI 翻译
|
||||||
|
*
|
||||||
|
* @param string $text 要翻译的文本
|
||||||
|
* @param array $options 选项
|
||||||
|
* @return string 翻译后的文本(slug 格式)
|
||||||
|
*/
|
||||||
|
private function translateWPMind($text, $options) {
|
||||||
|
$debug_mode = isset($options['debug_mode']) && $options['debug_mode'];
|
||||||
|
|
||||||
|
// 获取语言设置(提前获取以便用于缓存键)
|
||||||
|
$source_lang = isset($options['translation_source_lang']) ? $options['translation_source_lang'] : 'zh';
|
||||||
|
$target_lang = isset($options['translation_target_lang']) ? $options['translation_target_lang'] : 'en';
|
||||||
|
|
||||||
|
// 1. 先检查本地缓存(缓存键包含语言设置)
|
||||||
|
$cache_key = 'wpslug_wpmind_' . md5($text . '_' . $source_lang . '_' . $target_lang);
|
||||||
|
$cached = get_transient($cache_key);
|
||||||
|
if ($cached !== false) {
|
||||||
|
if ($debug_mode) {
|
||||||
|
error_log('[WPSlug] WPMind cache hit for: ' . $text . ' (' . $source_lang . ' -> ' . $target_lang . ')');
|
||||||
|
}
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查 WPMind 是否可用
|
||||||
|
if (!function_exists('wpmind_is_available') || !wpmind_is_available()) {
|
||||||
|
if ($debug_mode) {
|
||||||
|
error_log('[WPSlug] WPMind not available, falling back to pinyin');
|
||||||
|
}
|
||||||
|
return $this->fallbackTranslate($text, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 文本长度限制(避免超时)
|
||||||
|
if (mb_strlen($text) > 200) {
|
||||||
|
if ($debug_mode) {
|
||||||
|
error_log('[WPSlug] Text too long (' . mb_strlen($text) . ' chars), using pinyin');
|
||||||
|
}
|
||||||
|
return $this->fallbackTranslate($text, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 中文字符数限制
|
||||||
|
$chinese_count = preg_match_all('/[\x{4e00}-\x{9fff}]/u', $text);
|
||||||
|
if ($chinese_count > 50) {
|
||||||
|
if ($debug_mode) {
|
||||||
|
error_log('[WPSlug] Too many Chinese characters (' . $chinese_count . '), using pinyin');
|
||||||
|
}
|
||||||
|
return $this->fallbackTranslate($text, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 调用 WPMind API(语言设置已在前面获取)
|
||||||
|
$start_time = microtime(true);
|
||||||
|
|
||||||
|
$result = wpmind_translate($text, $source_lang, $target_lang, [
|
||||||
|
'context' => 'wpslug_translation',
|
||||||
|
'format' => 'slug',
|
||||||
|
'cache_ttl' => 86400, // WPMind 内部缓存 1 天
|
||||||
|
'max_tokens' => 100,
|
||||||
|
'temperature' => 0.3,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$elapsed_time = round((microtime(true) - $start_time) * 1000);
|
||||||
|
|
||||||
|
// 7. 处理结果
|
||||||
|
if (is_wp_error($result)) {
|
||||||
|
if ($debug_mode) {
|
||||||
|
error_log('[WPSlug] WPMind error: ' . $result->get_error_message() . ' (took ' . $elapsed_time . 'ms)');
|
||||||
|
}
|
||||||
|
return $this->fallbackTranslate($text, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
$slug = $this->cleanTranslatedText($result);
|
||||||
|
|
||||||
|
// 8. 验证结果有效性
|
||||||
|
if (empty($slug)) {
|
||||||
|
if ($debug_mode) {
|
||||||
|
error_log('[WPSlug] WPMind returned empty result, using pinyin');
|
||||||
|
}
|
||||||
|
return $this->fallbackTranslate($text, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 缓存结果(7 天)
|
||||||
|
set_transient($cache_key, $slug, 7 * DAY_IN_SECONDS);
|
||||||
|
|
||||||
|
if ($debug_mode) {
|
||||||
|
error_log('[WPSlug] WPMind translated "' . $text . '" to "' . $slug . '" in ' . $elapsed_time . 'ms');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $slug;
|
||||||
|
}
|
||||||
|
|
||||||
private function fallbackTranslate($text, $options) {
|
private function fallbackTranslate($text, $options) {
|
||||||
if ($this->converter === null) {
|
if ($this->converter === null) {
|
||||||
$this->converter = new WPSlug_Converter();
|
$this->converter = new WPSlug_Converter();
|
||||||
|
|
|
||||||
|
|
@ -130,12 +130,13 @@ class WPSlug_Validator {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function validateConversionMode($mode) {
|
public static function validateConversionMode($mode) {
|
||||||
$valid_modes = array('pinyin', 'transliteration', 'translation');
|
$valid_modes = array('pinyin', 'semantic_pinyin', 'transliteration', 'translation');
|
||||||
return self::validateSelect($mode, $valid_modes, 'pinyin');
|
return self::validateSelect($mode, $valid_modes, 'pinyin');
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function validateTranslationService($service) {
|
public static function validateTranslationService($service) {
|
||||||
$valid_services = array('none', 'google', 'baidu');
|
// 注意:wpmind 服务需要在此列表中才能保存
|
||||||
|
$valid_services = array('none', 'google', 'baidu', 'wpmind');
|
||||||
return self::validateSelect($service, $valid_services, 'none');
|
return self::validateSelect($service, $valid_services, 'none');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,6 +158,15 @@ class WPSlug_Validator {
|
||||||
if ($service === 'baidu' && (empty($options['baidu_app_id']) || empty($options['baidu_secret_key']))) {
|
if ($service === 'baidu' && (empty($options['baidu_app_id']) || empty($options['baidu_secret_key']))) {
|
||||||
$errors[] = __('Baidu App ID and Secret Key are required for Baidu Translate service.', 'wpslug');
|
$errors[] = __('Baidu App ID and Secret Key are required for Baidu Translate service.', 'wpslug');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WPMind 需要已配置
|
||||||
|
if ($service === 'wpmind') {
|
||||||
|
if (!function_exists('wpmind_is_available')) {
|
||||||
|
$errors[] = __('WPMind plugin is not installed.', 'wpslug');
|
||||||
|
} elseif (!wpmind_is_available()) {
|
||||||
|
$errors[] = __('WPMind is not configured. Please configure it in WPMind settings.', 'wpslug');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return empty($errors) ? true : $errors;
|
return empty($errors) ? true : $errors;
|
||||||
|
|
|
||||||
12
wpslug.php
12
wpslug.php
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Plugin Name: WPSlug
|
Plugin Name: WPSlug
|
||||||
Plugin URI: https://wpslug.com
|
Plugin URI: https://wpcy.com/slug
|
||||||
Description: Advanced slug management plugin with Chinese Pinyin support and SEO optimization.
|
Description: Advanced slug management plugin with Chinese Pinyin support and SEO optimization.
|
||||||
Version: 1.0.13
|
Version: 1.1.0
|
||||||
Author: WPSlug.com
|
Author: WPSlug
|
||||||
Author URI: https://wpslug.com
|
Author URI: https://wpcy.com/slug
|
||||||
License: GPL2
|
License: GPL2
|
||||||
Text Domain: wpslug
|
Text Domain: wpslug
|
||||||
Domain Path: /languages
|
Domain Path: /languages
|
||||||
|
|
@ -19,7 +19,7 @@ if (!defined("ABSPATH")) {
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
define("WPSLUG_VERSION", "1.0.13");
|
define("WPSLUG_VERSION", "1.1.0");
|
||||||
define("WPSLUG_PLUGIN_DIR", plugin_dir_path(__FILE__));
|
define("WPSLUG_PLUGIN_DIR", plugin_dir_path(__FILE__));
|
||||||
define("WPSLUG_PLUGIN_URL", plugin_dir_url(__FILE__));
|
define("WPSLUG_PLUGIN_URL", plugin_dir_url(__FILE__));
|
||||||
define("WPSLUG_PLUGIN_BASENAME", plugin_basename(__FILE__));
|
define("WPSLUG_PLUGIN_BASENAME", plugin_basename(__FILE__));
|
||||||
|
|
@ -230,4 +230,4 @@ if (is_admin()) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
wpslug();
|
wpslug();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue