Compare commits

..

121 commits

Author SHA1 Message Date
f21f4b405a chore(deps): add renovate.json 2026-02-20 18:15:48 +00:00
LinuxJoy
1b52474f3d feat(license): support WENPAI_LICENSE_URL constant override
Allow overriding the license server URL via wp-config.php constant
for local testing and staging environments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-20 11:28:10 +08:00
LinuxJoy
d733b8e7fc feat(license): add WenPai License SDK + test infrastructure
- WenPai_License PHP SDK: verify/activate/deactivate with 24h cache + 7-day grace period
- wpmind_license() global helper, settings page license tab with AJAX activate/deactivate
- composer.json with yoast/wp-test-utils, PHPUnit 9.6 + BrainMonkey test setup
- 6 unit tests for WenPai_License (get_key, set_key, verify cache, plan fallback)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-20 01:20:05 +08:00
LinuxJoy
3434080dfd feat(i18n): add .pot translation template and auto-generate in deploy
Generate wpmind.pot via wp-cli during deploy, enabling translation
pipeline and i18n coverage testing by translate and elementary VMs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-19 23:15:25 +08:00
LinuxJoy
57c301d234 feat(updater): integrate WenPai update checker with popup fix
Add Update URI header and WenPai_Updater v1.1.0 for self-hosted update
support via updates.wenpai.net. The updater includes three key fixes over
v1.0.0: external flag to prevent broken wp.org links in popup, API error
response validation, and local plugin header fallback when API unavailable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-19 22:02:28 +08:00
LinuxJoy
c61b539e4e feat(ci): sync wp-release.yml with changelog generation from ci-workflows 2026-02-19 11:35:42 +08:00
LinuxJoy
59acf3501f fix(ci): avoid SIGPIPE in verify tools step 2026-02-18 16:57:22 +08:00
LinuxJoy
8e4d9a98fd chore: restore local tracking of internal docs
Sensitive files tracked locally but excluded from remote push
via push-feicode.sh + .git/push-exclude mechanism.
2026-02-18 16:44:26 +08:00
LinuxJoy
419f18a4bf chore: remove roadmap and deployment docs from tracking 2026-02-18 16:32:53 +08:00
LinuxJoy
b89c1773aa chore: add Forgejo CI/CD workflow and reset version to 0.11.3
- Add .forgejo/workflows/release.yml (tag-triggered auto release)
- Reset version from 3.11.3 to 0.11.3 (pre-release stage)
- Update version references in docs
2026-02-18 16:11:33 +08:00
LinuxJoy
85e90e0a50 style: 设置页 header 贴边布局重构
- 模块 header 移到 panel 外层,突破 tab-pane padding 实现贴边显示
- 统一 header 样式:padding、border-bottom、space-between 布局
- 各模块模板添加 wpmind-tab-pane-body 包裹内容区域
- Tab 列表改为 flex-wrap 并添加左右 padding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-16 01:02:23 +08:00
LinuxJoy
3db7b58c5b fix: 移除 init 钩子中的 provider 连通性调试循环
register_wpmind_providers() 在 init 钩子中对每个已启用的 AI provider
调用 isProviderConfigured(),该方法会发送真实 API 请求测试连通性。
导致每次页面加载都产生 N 个 AI API 请求(N = 已启用 provider 数量),
浪费 API 配额并拖慢页面加载。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-16 00:53:33 +08:00
LinuxJoy
6ee99661c5 fix(geo): add tabbed Schema preview (Article/Breadcrumb/WebSite)
Enhance the JSON-LD preview section with three switchable tabs
for Article, BreadcrumbList, and WebSite schema types. Update
property table with BreadcrumbList and WebSite entries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-10 16:31:43 +08:00
LinuxJoy
c8da39d98e feat(geo): add WebSite/BreadcrumbList Schema, Author sameAs, JSON-LD preview
- BrandEntity: output WebSite + SearchAction JSON-LD on front page
- SchemaGenerator: output BreadcrumbList (Home → Category → Post) on
  singular pages, enhance author schema with sameAs from user profile
  social fields (Twitter, LinkedIn, GitHub, Weibo, Facebook)
- Settings: add live JSON-LD preview in 结构化数据 tab right sidebar
- Version bump 3.11.2 → 3.11.3

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-10 13:24:24 +08:00
LinuxJoy
63c73ff120 refactor(geo): move entity linker to brand tab, enhance schema info
Move "实体关联" from 结构化数据 tab to 品牌实体 tab as "文章实体关联",
consolidating all entity settings in one place. Replace schema tab
right sidebar with AI-focused explanation and output property table
showing all 12 auto-generated JSON-LD attributes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-10 12:42:27 +08:00
LinuxJoy
69b0d95002 fix(geo): unify brand entity form layout and input widths
Group related fields into single cards instead of one card per field.
Add wpmind-brand-fields CSS for consistent full-width inputs and
uniform label/field spacing across all brand entity sections.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-10 11:58:41 +08:00
LinuxJoy
5672ef0888 feat(geo): add Brand Entity sub-tab for Organization Schema
Add BrandEntity component that enriches article publisher Schema
with sameAs, description, contactPoint and outputs standalone
Organization JSON-LD on the front page. Includes settings UI with
org type, social profiles, contact info and Knowledge Graph links.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 21:26:26 +08:00
LinuxJoy
5ababf0e18 docs: add v3.11.1 CSS refactoring and v3.11.0 Media Intelligence changelog
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 19:31:08 +08:00
LinuxJoy
64a17aa203 refactor(css): extract shared module layout to components/module-layout.css
Unified CSS architecture for all module settings pages:
- Created assets/css/components/module-layout.css with wpmind-module-* naming
- Replaced scattered wpmind-geo-*, wpmind-mi-* shared classes across modules
- Scoped all module JS selectors to their panel containers (prevents cross-module interference)
- Each module CSS now contains only module-specific styles
- Bumped version to 3.11.1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 19:11:45 +08:00
LinuxJoy
9f1bca77d5 fix(media-intelligence): scope sub-tab selectors to prevent cross-module interference
Media Intelligence JS used global selectors (.wpmind-mi-subtab, .wpmind-mi-tab-panel)
that removed active class from all modules' panels, causing blank sub-tab content when
switching main tabs. Scoped all selectors to .wpmind-media-panel container.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 18:47:02 +08:00
LinuxJoy
ee1205d3e8 fix(auto-meta): self-contained CSS, no dependency on geo/media-intelligence
Subtab navigation and option/actions styles are now scoped to
.wpmind-auto-meta-panel, ensuring correct rendering even when
GEO or Media Intelligence modules are disabled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 18:08:55 +08:00
LinuxJoy
ade7336d9d docs: add Auto-Meta v3.11.0 changelog entry
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 13:50:18 +08:00
LinuxJoy
60b9e37c09 fix(auto-meta): address Codex review findings
- High: add preflight check to skip AI call when no features enabled
- Medium: allow retry on update when initial generation failed (source empty)
- Medium: inject_faq_schema now respects the FAQ toggle setting
- Low: manual generation returns clear error for non-published posts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 13:14:19 +08:00
LinuxJoy
fb874ff463 feat: Auto-Meta 模块 — 发布时自动生成摘要、标签、分类、FAQ Schema、SEO 描述
- 新增 modules/auto-meta/ 模块(MetaGenerator + AutoMetaAjaxController)
- transition_post_status + post_updated 触发,WP-Cron 异步执行
- 单次 wpmind_structured() 调用生成全部元数据
- FAQ Schema 通过 wpmind_article_schema filter 注入 GEO 模块
- 设置页双子标签:功能开关 + 手动生成
- 版本号 3.10.9 → 3.11.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 13:02:25 +08:00
LinuxJoy
bb5e58282c fix: Vision API failover 限制为支持视觉的 Provider
ChatService.chat() 新增 failover_providers 选项,允许调用方约束
故障转移链。PublicAPI.vision() 传入 VisionHelper 的已配置视觉
Provider 列表,避免 failover 尝试不支持图片输入的 Provider
(如 DeepSeek/Moonshot/Doubao 等),减少无效重试。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 12:24:16 +08:00
LinuxJoy
b5093f0c3a refactor: Media Intelligence 设置页重构为子标签页导航
将平铺布局拆分为三个 pill 子标签页(基本设置/批量处理/内容安全),
与 GEO 模块视觉风格一致,类名独立避免 JS 冲突。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 12:14:38 +08:00
LinuxJoy
7f8503e3ad feat: Media Intelligence 模块 — AI 图片 alt text/标题/描述自动生成
新增 Media Intelligence 模块 (v4.3),复用 chat API 多模态能力实现:
- wpmind_vision() 全局函数 + VisionHelper 静态类
- 上传时异步生成 alt text、标题、caption
- NSFW 内容检测(可选)
- 批量处理已有无 alt text 图片(防无限循环)
- 设置管理界面 + 统计面板

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 11:48:06 +08:00
LinuxJoy
8356f6ae15 refactor: 拆分 admin.css 模块专属样式到独立文件
将 admin.css 从 2274 行精简至 1520 行(-33%),提取 3 个模块专属
样式到 assets/css/pages/ 下独立文件,由各模块自行 enqueue 加载:
- geo.css (350行): subtabs/sections/crawlers/options
- api-gateway.css (383行): subtabs/keys table/audit log/docs
- exact-cache.css (43行): trend chart/cache panel badge

共享设计系统组件(geo-header/stat-card/badge)保留在 admin.css。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 10:33:24 +08:00
LinuxJoy
cd30c961e1 fix: 移除 services.php 重复缓存设置,缓存管理按钮移至标题栏右对齐
- 删除文本服务标签中重复的 Exact Cache 设置项(启用/TTL/容量上限)
- 清空缓存和重置统计按钮从底部移至标题栏,与数据统计风格一致
- badge 添加 margin-right:auto 实现按钮右对齐
- 清理废弃的 .wpmind-cache-actions CSS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 03:24:20 +08:00
LinuxJoy
563bf8e2c3 feat: Exact Cache 模块 — AI 请求精确缓存管理界面
新增 Exact Cache 模块,为已有的 ExactCache 核心类提供完整的管理界面和统计功能:

- 模块骨架:module.json + ExactCacheModule(实现 ModuleInterface 全部 7 个方法)
- AJAX 控制器:4 个 handler(保存设置/清空缓存/重置统计/获取统计),含安全三要素
- 成本估算器:基于 UsageTracker 历史数据估算缓存节省金额
- 日统计:7 天滚动统计,shutdown 批量写入,retry-once transient lock
- 管理界面:统计卡片 + Chart.js 7 天趋势图 + 设置表单 + 缓存管理
- settings-page.php:添加精确缓存 tab(与 GEO/API Gateway 相同模式)
- ExactCache.php:仅添加 get_entries() 只读方法,零核心影响
- uninstall.php:添加 option + transient 批量清理

经过 3 轮 Codex 架构审查(v1.0→v2.0→v2.1→v2.2),所有 P0/P1 问题已解决。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 03:09:57 +08:00
LinuxJoy
a4b2a90074 refactor: MCP Ability slug 统一为 mind/ 前缀
wpmind/chat → mind/chat
wpmind/get-providers → mind/get-providers
wpmind/get-usage-stats → mind/get-usage-stats
wpmind/get-budget-status → mind/get-budget-status
wpmind/switch-strategy → mind/switch-strategy

与 REST API namespace mind/v1 保持一致。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 01:58:50 +08:00
LinuxJoy
ef6fa2def6 style: 全标签页设计审查 — 统一标题区域和 Remixicon 图标
- API Gateway: 新增标题区域(ri-server-line) + 描述 + 统计卡片/子标签图标全部换为 Remixicon
- 服务配置: 新增标题区域(ri-cloud-line) + 描述
- 图片生成: 新增标题区域(ri-image-line) + 描述
- 模块管理: 标题添加图标(ri-puzzle-line)
- 新增 docs/api-gateway-use-cases.md 应用场景文档

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 01:49:43 +08:00
LinuxJoy
49b25c58d1 fix: admin.css var() 语法错误导致 Gateway 样式失效
admin.css:1729 缺少右括号 var(--wpmind-text-xs → var(--wpmind-text-xs)
导致浏览器 CSS 解析器中断,后续所有 .wpmind-gw-* 样式不生效。
版本号 3.10.5 → 3.10.6

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 01:29:56 +08:00
LinuxJoy
36b69d300d security: API Gateway 全量代码审计修复 (59 issues)
Codex 审计发现 124 个问题,修复 59 个 P0-P2 级别问题:

P0 (CRITICAL):
- TOCTOU 竞态: TransientRateStore/SseConcurrencyGuard 改用 wp_cache_add 原子锁
- REST 路由: __return_true → check_bearer_present 轻量鉴权
- Schema 验证: messages/tools/tool_choice 添加完整 validate_callback
- SQL 列名不匹配: upsert 查询对齐 SchemaManager 定义
- SQL 表名插值: 统一使用 %i 占位符

P1 (HIGH):
- IP hash 加盐 (HMAC) + IP 解析统一 (context 共享)
- Header 注入防护 (X-Request-Id/响应头 \r\n 过滤)
- 数值参数范围校验 (temperature/top_p/penalties/max_tokens)
- insert_key 错误处理 + revoke_key 审计日志
- Pipeline finally 保护 + RateLimiter rollback 双 store
- wp_unslash 补全 + strtotime 返回值检查
- RedisRateStore $rid 冒号验证
- UpstreamStreamClient catch \Throwable

P2 (MEDIUM):
- AuditLogRepository DRY (提取 build_where_clause)
- Token 估算 CJK 优化 (mb_strlen)
- ResponseTransformMiddleware null 检查
- 嵌入响应 usage 数据修复
- SchemaManager autoload 优化

25 files changed, ~530 insertions, ~190 deletions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 01:14:12 +08:00
LinuxJoy
6b89e4dd57 feat: API Gateway 设置界面改版 — 统一设计系统 + 功能增强
- 重写 settings.php:4 个子标签(基础设置/Key 管理/接入文档/请求日志)
- 统一使用 CSS 变量和 Gutenberg 直角风格,移除内联样式
- 新增 Key 编辑功能(行内展开式编辑面板)
- 新增审计日志查看(分页 + 过滤)
- 新增 API 接入文档面板(端点列表 + curl 示例)
- 新增 AuditLogRepository 和 update_key() 方法

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 00:42:23 +08:00
LinuxJoy
7d780574bc chore: 版本号 3.10.2 → 3.10.3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 00:09:12 +08:00
LinuxJoy
82c9e71e60 fix: API Gateway AJAX nonce 时序问题 — 创建 Key 失败
根因:内联 JS 在 body 中执行时 wpmindData(footer 注入)尚未加载,
nonce 被设为空字符串,导致所有 AJAX 请求 check_ajax_referer 失败返回 0。

修复:直接用 PHP 输出 ajaxurl 和 nonce,不依赖 wpmindData 加载时序。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-09 00:05:41 +08:00
LinuxJoy
07721ad840 fix: API Gateway Admin JS 防御性编程 — 修复 AJAX 回调空值崩溃
- 保存设置: r.data.message → (r.data && r.data.message) || '保存失败'
- 创建 Key: r.data.message → (r.data && r.data.message) || '创建失败'
- 吊销 Key: r.data.message → (r.data && r.data.message) || '吊销失败'
- 加载列表: r.data.keys.length → r.data && r.data.keys && r.data.keys.length

修复 admin.php?page=wpmind:4220 TypeError: Cannot read properties of undefined

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 23:48:36 +08:00
LinuxJoy
8b1d5a7796 fix: API Gateway 部署验证修复 — 3 个遗漏问题
- RequestTransformer: 添加 model_detail/status 到 match 分支(之前落入 default 报错)
- RouteMiddleware: wp_request() → rest_request()(方法名不存在导致 internal_error)
- AuthMiddleware: status 操作加入 API_OPERATION_PREFIXES(之前走管理端认证)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 23:45:47 +08:00
LinuxJoy
db75102584 fix: API Gateway Codex 审查修复 — 6 个问题
HIGH:
1. generate_key_id: 改用固定字母表循环,保证始终 12 字符
2. Redis TPM: Lua 脚本改为遍历 member 求和 cost,而非 ZCARD 计数
3. IP 头安全: 仅在 REMOTE_ADDR 匹配可信代理时才读取 X-Forwarded-For

MEDIUM:
4. SSE 审计: 挂载 wpmind_gateway_sse_complete 回调写入审计日志+用量统计
5. /models/{id}: 新增 model_detail 操作,返回单模型或 404
6. ErrorMapper: 添加 sse_concurrency_exceeded/sse_lock_timeout/unsupported_operation 映射

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 23:12:38 +08:00
LinuxJoy
59da0bdfab docs: 更新 CHANGELOG — API Gateway v3.10.2 完成记录
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 22:15:44 +08:00
LinuxJoy
3e1bd784e7 fix: API Gateway 部署修复 — key_id 长度 + 设置页标签页
- generate_key_id: random_bytes(8)→(12) 确保 key_id 始终 12 字符
- settings-page.php: 添加 API Gateway 标签页到硬编码导航
- 版本号升级: 3.10.1 → 3.10.2

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 22:10:57 +08:00
LinuxJoy
f263e38b04 feat: API Gateway Phase 9 — Admin UI 设置页 + API Key 管理
- GatewayAjaxController: 4 个 AJAX handler (save/create/list/revoke)
- ApiKeyRepository: 新增 list_all_with_usage / count 查询方法
- settings.php: 完整设置页 (状态卡片 + 基础设置 + Key 管理)
- ApiGatewayModule: 集成 AJAX 控制器注册

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 21:32:27 +08:00
LinuxJoy
253b3cadd8 feat: API Gateway Phase 8 — REST 控制器 + 端点注册 (核心功能完成)
- RestController: 6 个 OpenAI 兼容端点注册
  POST /mind/v1/chat/completions
  POST /mind/v1/embeddings
  POST /mind/v1/responses
  GET  /mind/v1/models
  GET  /mind/v1/models/{model_id}
  GET  /mind/v1/status
- 8 阶段 Pipeline 完整串联
- RouteMiddleware: 添加 status + responses 操作处理
- ApiGatewayModule: 接入 RestController

API Gateway 核心功能 Phase 0-8 全部完成 (35 PHP 文件)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 20:41:13 +08:00
LinuxJoy
29459364a8 feat: API Gateway Phase 6+7 — SSE 流式处理 + 错误处理 + 审计日志
Phase 6 (SSE 流式处理):
- CancellationToken: 客户端断开检测
- SseConcurrencyGuard: 全局+Key 并发槽位管理
- UpstreamStreamClient: 上游 Provider 流式代理
- SseStreamController: SSE 生命周期编排
- RouteMiddleware: 集成 SSE 流式分支

Phase 7 (错误处理+审计):
- ErrorMapper: WP_Error → OpenAI 错误格式映射 (14种)
- ErrorMiddleware: 异常捕获 + OpenAI 兼容错误响应
- LogMiddleware: 审计日志写入 + 用量统计 upsert

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 20:37:10 +08:00
LinuxJoy
493033ec70 feat: API Gateway Phase 4+5 — 中间件 + 路由转换层
Phase 4 (认证+预算+限流):
- AuthMiddleware: Bearer Key (API) / Cookie+Nonce (管理端)
- BudgetMiddleware: 月度预算检查 (api_key_usage 表)
- QuotaMiddleware: RPM/TPM 双维度限流
- RateLimit 子系统: Redis 滑动窗口 + Transient 降级

Phase 5 (路由+转换):
- ModelMapper: 18 模型映射 + 别名 + auto 路由
- RequestTransformer: OpenAI → WPMind (体积/token 上限)
- ResponseTransformer: WPMind → OpenAI 格式
- RouteMiddleware: 分发到 PublicAPI
- RequestTransformMiddleware / ResponseTransformMiddleware

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 20:31:39 +08:00
LinuxJoy
b381a1295b feat: API Gateway Phase 0-3 — 模块骨架 + Auth 子系统 + 中间件框架
- Phase 0: 框架对齐 (module.json, ModuleInterface 契约)
- Phase 1: 模块入口 + SchemaManager (3张DB表)
- Phase 2: API Key 系统 (Hasher/Repository/Manager/AuthResult)
- Phase 3: 请求上下文 + 8阶段中间件 Pipeline
- uninstall.php: 添加 API Gateway 表清理

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 20:20:59 +08:00
LinuxJoy
b8bfb55dad feat: Phase 4.0 — ExactCache + MCP Gateway + API Gateway 设计方案
核心功能:
- ExactCache: 请求级精确缓存,批量写入优化 (shutdown hook)
- MCP Gateway: WordPress Abilities API 集成 (5 个能力)
- AbstractService: cache_ttl 三级语义 (负数禁用/零自动/正数指定)

API Gateway 模块设计 (Codex 三轮评审通过):
- 完整设计方案 + 5 个 P0 解决方案 (API Key/Pipeline/SSE/限流/错误映射)
- 11 阶段实施计划 (Phase 0-10),8 阶段中间件 Pipeline
- 3 张 DB 表设计 (api_keys/api_key_usage/api_audit_log)

其他:
- Provider 测试文档 (Qwen/Doubao 兼容性矩阵)
- GEO 设置页 badge 文本修正
- 版本号升级到 3.10.1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 19:06:30 +08:00
LinuxJoy
b64063c9d7 feat: GEO 设置页各子标签添加知识块
每个子标签页右栏添加对应的概念讲解卡片:
- 基础设置:什么是 GEO?(补充知识库链接)
- 内容输出:AI 内容分发
- 结构化数据:语义理解与实体消歧
- AI 索引:AI 索引权限控制
- 爬虫管理:AI 爬虫生态

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 16:11:25 +08:00
LinuxJoy
1760669c4b fix: 右栏结构重构 + 中国 AI 爬虫补充
- 右栏(爬虫统计+GEO说明)移入基础设置 tab 内部 grid,其他 tab 自然全宽
- 移除 JS sidebar show/hide 逻辑,改为纯 HTML 结构控制
- robots.txt 管理新增: 百度/搜狗/360/神马/DuckAssistBot
- CrawlerTracker 同步新增中国搜索引擎爬虫识别

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 15:09:05 +08:00
LinuxJoy
c7108ed376 feat: GEO 模块 v3.10 — 3 个新功能 + 设置页子导航重构
新增组件:
- RobotsTxtManager: robots.txt AI 爬虫管理 (15 个爬虫 Allow/Disallow)
- AiSummaryManager: AI 摘要编辑器字段 + meta 标签 + Schema.org abstract
- EntityLinker: Wikidata 实体关联 + Schema.org about.sameAs

设置页重构:
- 平铺 section 改为 5 个 pill 子导航 (基础/内容/结构化数据/AI索引/爬虫管理)
- 右栏爬虫统计仅在基础设置 tab 显示,其他 tab 全宽布局
- 子 tab 状态通过 sessionStorage 保持

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 15:02:58 +08:00
LinuxJoy
abac5095b4 fix: Codex 审查修复 — checkbox 值判断 + rewrite flush + 子设置保护
1. checkbox 开关改用值比较替代 isset(),修复无法关闭的 bug
2. rewrite flush 改为延迟到下次 admin_init 执行,确保路由已注册
3. AI 索引/Sitemap 子设置仅在对应功能启用时才保存,避免覆盖

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 12:02:13 +08:00
LinuxJoy
ec51446624 feat: AI Sitemap (/ai-sitemap.xml) — AI 爬虫专属站点地图
新增 AiSitemapGenerator,为 AI 爬虫提供带元数据的 XML Sitemap:
- 每个 URL 包含 ai:declaration(内容声明)和 ai:summary(摘要)
- 自动排除标记 noai 的内容(集成 AiIndexingManager)
- Transient 缓存 + 内容变更自动失效
- 后台可配置开关和最大条目数

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 11:49:45 +08:00
LinuxJoy
ba4bdf20a9 feat: AI 索引指令功能 (noai/nollm meta + X-Robots-Tag + 内容声明)
新增 AiIndexingManager 类,支持全局和单篇控制 AI 爬虫索引权限:
- wp_head 输出 noai/nollm meta robots 和 ai-content-declaration meta
- send_headers 输出 X-Robots-Tag HTTP 头
- 编辑器侧边栏 metabox 支持单篇覆盖全局设置
- GEO 设置页新增 AI 索引指令 section(开关、默认声明、按 post type 排除)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 11:29:18 +08:00
LinuxJoy
22786bf996 style: 后台 UI 持续优化 — CSS 统一 + 概览页增强 + 版本号修正
统一全页面设计风格,优化概览页模板结构,修正版本号为 3.9.26。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 10:50:56 +08:00
LinuxJoy
cbe047fa2a style: 全页面设计统一 — 单色调 + GEO 风格标准化
以 GEO 页面为参考标准,统一所有页面的设计语言:
- 页面标题 14px + 18px primary 图标,section 标题 13px + 图标
- 卡片统一 gray-50 背景 + border,移除 box-shadow
- 统计卡片移除多色图标背景,统一 primary 单色
- 概览页 Hero 改为左侧强调线风格,移除大色块和水印图标
- 按钮基础样式统一(inline-flex + icon 对齐)
- 部署规则:每次 deploy 必须递增版本号

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 03:29:14 +08:00
LinuxJoy
d5144a205c refactor: Tab 标签重命名统一 + 排序优化 + CSS 变量修复
Tab 重命名:
- dashboard → analytics(ID + 标签"数据分析")
- "模块" → "模块管理"(导航标签统一)

Tab 重排序(功能 → 监控 → 设置):
  概览 → 文本服务 → 图像服务 → 智能路由
  → 数据分析 → 预算管理 → GEO 优化 → 模块管理

CSS 变量修复:
- 添加缺失的 --wpmind-space-7: 28px
- 移除 overview.css 中的 fallback 值

涉及文件: settings-page.php, admin-boot.js,
admin-analytics.js, AnalyticsModule.php, module.json,
admin.css, overview.css

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 00:59:00 +08:00
LinuxJoy
19967d2dfa feat: 概览页 UX 优化 — Hero 按钮 + 空状态引导 + 相对时间
- Hero 区块新增"添加 Provider"和"查看文档"操作按钮
- 移除渐变背景,改为纯色
- 移除卡片顶部彩色边线
- Provider 空状态:图标 + 引导文案 + "去配置"链接
- 活动空状态:图标 + 提示文案
- 最近活动时间改为相对时间(human_time_diff)
- 修复两处 HTML 多余字符
- 版本号升级到 v3.9.2

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 00:44:45 +08:00
LinuxJoy
193a145f5b feat: 概览页视觉增强 — Hero 区块 + 卡片图标背景
- 新增 Hero 品牌区块(渐变背景/标题/副标题/元信息)
- 本月摘要改为 2x2 网格布局,每项带彩色图标
- 最近活动加 Provider 图标 + 行 hover 效果
- 卡片顶部彩色边线(蓝/绿)区分类型
- 4 张卡片添加右下角半透明装饰图标
- 版本号升级到 v3.9.1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 00:30:39 +08:00
LinuxJoy
4e79f392ec feat: 概览页增强 — 本月摘要 + 最近活动 + 底部链接
- 新增本月摘要卡片(最常用模型/响应时间/可用率/总Tokens)
- 新增最近活动卡片(最近5条API调用记录)
- 新增底部链接栏(文派社区/使用文档/版本号)
- 修复今日Tokens统计使用不存在的key
- 版本号升级到 v3.9.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-08 00:22:48 +08:00
LinuxJoy
18d6246e37 style: JS 文件全面应用 WordPress JS 编码规范
- 括号内加空格: $( selector ), if ( condition ), function( arg )
- Yoda 条件: 'undefined' === typeof x
- 否定运算符后加空格: ! condition
- $.ajax( { ... } ) 花括号空格
- 数组下标空格: array[ index ]

涉及文件: admin-analytics, admin-budget, admin-endpoints,
admin-geo, admin-modules, admin-routing, admin-ui

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 23:48:57 +08:00
LinuxJoy
e495787463 style: 全局缩进统一为 Tab + 修复概览快捷链接
- 所有 JS/CSS/PHP 模板文件缩进从 4 空格转换为 Tab
- admin-boot.js 完整 WordPress JS 编码规范化
  - 括号内空格: $( '.selector' ), if ( condition )
  - Yoda 条件: 'dashboard' === tabId
- 修复概览页快捷链接点击无效
  - 委托事件改为直接绑定 .wpmind-tab-link
  - .attr('data-tab-link') 替代 .data('tabLink')
  - return false 双重保险

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 23:41:42 +08:00
LinuxJoy
40b349c3b0 feat: 概览标签页 — 始终可用的插件首页
- 新增 overview.php 模板:统计卡片 + Provider/模块状态 + 快捷入口
- 概览页始终作为默认首页,不依赖任何可选模块
- 修复 admin-boot.js 空白页 bug:fallback 到第一个可用 Tab
- 新增 overview.css:Gutenberg 设计语言,响应式布局

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 21:59:21 +08:00
LinuxJoy
277701c85b fix: 核心模块 UI 优化 — 锁图标 + 徽章 + 状态提示
- 不可禁用模块显示蓝色"核心"徽章(带 tooltip)
- toggle 开关替换为锁图标(带 tooltip 说明原因)
- 底部状态显示"核心模块"(蓝色盾牌)替代普通"已启用"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 21:46:06 +08:00
LinuxJoy
559883eb4b docs: CHANGELOG v3.8.0 兼容层清理 + 扩展点增强
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 20:23:41 +08:00
LinuxJoy
0ab7d3582d refactor: 清理兼容层 + Provider 懒加载 + 路由策略可插拔
- 删除 10 个兼容层文件 (~55KB),调用方直接引用模块类
- cost-control 模块改为不可禁用,ModuleLoader 强制启用
- ProviderRegistrar 改用字符串 FQCN,添加 wpmind_provider_map filter
- IntelligentRouter 添加 wpmind_register_routing_strategies action
- routing.php 添加 class_exists() 守卫处理 analytics 模块禁用
- AjaxController analytics 端点添加 class_exists() 守卫
- 更新集成测试移除已删除文件引用

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 20:19:35 +08:00
LinuxJoy
f0488aa735 docs: 文档清理 + CHANGELOG v3.7.0 + ROADMAP 状态同步
- 归档 5 个过时文档到 docs/_archive/
  (FEATURE-ROADMAP, DEVELOPMENT-PLAN, public-api-design,
   ai-gateway-design, target-plugins)
- CHANGELOG: 添加 v3.7.0 记录 (Facade 拆分 + 安全加固)
- ROADMAP: Phase 3 GEO 标记已完成,新增 Phase 3.5 架构优化,
  更新技术架构图反映 Services 拆分

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 17:06:57 +08:00
LinuxJoy
6c36d563e4 security: Codex 审计问题修复 + WordPress 插件开发指导文档
安全修复(Codex 审计 11 项):
- transcribe() SSRF 防护: wp_http_validate_url + 协议白名单
- transcribe() 本地文件读取限制: realpath + uploads 目录校验
- transcribe() 文件大小上限 25MB + 扩展名白名单
- ErrorHandler 响应体截断到 500 字符,防止信息泄露
- SDKAdapter getTokenUsage() 空值保护
- SDKAdapter 异常消息脱敏,仅 WP_DEBUG 记录详情
- stream() 添加 allow_url_fopen 检测
- embed() JSON 解码显式校验
- speech() file_put_contents 返回值检查

新增文档:
- WordPress 插件开发指导手册 v1.1.0 (1225 行)
- 覆盖项目准备、编码规范、架构设计、开发流程等 10 章
- 基于 Codex 审计反馈补充安全基线和 WP 生态兼容性

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 12:41:21 +08:00
LinuxJoy
b0b5032907 refactor: PublicAPI 拆分为 Facade + 6 个 Service 类
将 PublicAPI.php (2124行/76KB) 拆分为 Facade 模式架构:
- PublicAPI.php: 瘦 Facade (398行),保留单例、递归保护、状态方法
- Services/AbstractService.php: 共享基础设施 (provider解析/failover/缓存)
- Services/ChatService.php: chat + stream + SDK路由 + HTTP请求
- Services/TextProcessingService.php: translate + summarize + moderate
- Services/StructuredOutputService.php: structured + batch + schema验证
- Services/EmbeddingService.php: embed
- Services/AudioService.php: transcribe + speech
- Services/ImageService.php: generate_image (委托ImageRouter)

所有 15 个公共方法签名不变,wpmind_*() 全局函数兼容,
递归保护留在 Facade 层,Service 内部互调不触发递归检查。
已通过 7 项回归测试验证。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 12:32:36 +08:00
LinuxJoy
f5b2daf722 feat: 执行层统一到 WP AI Client SDK v3.6.0 (Phase C)
- C1: 新增 SDKAdapter 类,封装 SDK 调用和格式转换
- C2: execute_chat_request() 增加 SDK 优先路径 + HTTP 回退
- C3: should_use_sdk() 能力 gate,默认对 Anthropic/Google 启用

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 11:24:10 +08:00
LinuxJoy
af33502949 feat: 模型重选 + 路由统一 v3.5.0 (Phase B)
- B1: failover 循环内动态模型重选,auto 下移 + 显式模型回退标注
- B2: stream() 接入 wpmind_select_provider + FailoverManager 故障转移
- B3: embed() 接入路由和故障转移,embed model 动态选择
- B4: transcribe/speech 接入路由,能力过滤不支持 audio API 的 provider

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 10:58:30 +08:00
LinuxJoy
d4aedd40dd fix: AI 请求链路可靠性修复 v3.4.0 (Phase A)
- A1: 缓存键加入 provider/model,避免跨 Provider 缓存污染
- A2: 非 JSON 响应防护,json_decode 失败返回 WP_Error
- A3: stream() 默认 provider 与 chat() 统一
- A4: per-provider 重试逻辑,激活 ErrorHandler 死代码

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 10:46:04 +08:00
LinuxJoy
960bfb8ba4 refactor: admin modularization and cleanup 2026-02-07 04:27:30 +08:00
LinuxJoy
222f11495f chore: 版本号升至 3.3.0 (Phase 1 完成)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 03:28:51 +08:00
LinuxJoy
74db7a20af refactor: 方法名 camelCase → snake_case (39文件, WordPress 编码规范)
将项目自有代码的方法名从 camelCase 统一为 snake_case,
符合 WordPress PHP 编码规范。涉及 Routing、Failover、Budget、
Analytics、Usage、ErrorHandler 等模块。外部库接口方法和
Image Provider 子系统保持不变。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 03:21:31 +08:00
LinuxJoy
817b7a6ba5 chore: 版本号升至 3.2.1 (Phase 0 完成)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 00:51:05 +08:00
LinuxJoy
183a5c79c6 fix: Phase 0 - Chart.js 本地化、Tab 修复、可观测性
- Chart.js 4.5.0 本地化,移除 CDN 依赖 (cloudflare 国内被墙)
- 移除 admin.js 对 chartjs 的硬依赖,防止级联故障
- Chart.js 延迟加载支持:轮询重试 + 超时友好提示
- 修复 Tab CSS class 不匹配 (active → wpmind-tab-pane-active)
- 添加 JS 健康检查:body class + console.log + wpmindData 调试字段
- 更新 .gitignore 允许 assets/js/vendor/ 目录提交
- 新增后台重构方案文档 (ADMIN-REFRACTOR-PLAN.md)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-07 00:50:06 +08:00
LinuxJoy
2441306a37 revert: 回滚 camelCase→snake_case 重构及相关修补 (7个提交)
回滚 b0fe6a6..8c00d7d 的所有改动,恢复到 02cb582 的稳定状态。
重构导致大量遗漏和前端功能异常,需要重新规划后再实施。

回滚的提交:
- b0fe6a6 refactor: 方法名 camelCase → snake_case
- d3f9bd2 fix: UsageTracker 静态调用遗漏
- af8864a fix: getCurrency/getProviderStatus/getAllHealth 遗漏
- d3c2e70 fix: private/internal 方法遗漏
- d15d601 fix: Chart.js 本地化 + Tab 切换修复
- ee52ef9 fix: 移除 chartjs 硬依赖
- 8c00d7d fix: 再次移除 chartjs 硬依赖

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-06 22:38:34 +08:00
LinuxJoy
8c00d7d977 fix: 再次移除 admin.js 对 chartjs 的硬依赖
Codex 修改时误将 chartjs 加回了依赖列表。
Chart.js CDN 被墙时会阻止 admin.js 加载,导致标签切换等全部 JS 失效。
ensureChartJs() 已处理动态加载,无需 wp_enqueue_script 依赖。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-06 18:56:46 +08:00
LinuxJoy
ee52ef9259 fix: 移除 admin.js 对 chartjs 的硬依赖,防止 CDN 失败阻塞全部 JS
Chart.js 改为 ensureChartJs() 动态加载,不再作为 wp_enqueue_script 依赖。
CDN 被墙时不会阻止 admin.js 加载,标签切换等基础功能不受影响。
版本号升级到 3.2.2。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-06 17:39:38 +08:00
LinuxJoy
d15d601053 fix: Chart.js 本地化 + CDN fallback + Tab 切换修复
- Chart.js 优先本地加载,CDN 失败时自动 fallback (jsdelivr → unpkg → cdnjs)
- 修复 Tab 切换:Chart.js CDN 被墙时阻塞 admin.js 加载导致所有 JS 失效
- 修复底部初始化检查错误的 CSS 类名 (active → wpmind-tab-pane-active)
- 添加 resolveTabId 安全校验,防止无效 hash 导致 JS 错误
- 版本号升级到 3.2.1

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-06 16:24:11 +08:00
LinuxJoy
d3c2e70aab fix: 补全所有遗漏的 private/internal 方法 camelCase → snake_case
ProviderHealthTracker、CircuitBreaker、IntelligentRouter、RoutingHooks、
CompositeStrategy、LoadBalancedStrategy、BudgetAlert、BudgetChecker、
BudgetManager、AnalyticsManager 共 16 文件 151 处。
全项目零残留 camelCase 方法定义。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-06 16:02:46 +08:00
LinuxJoy
af8864aae3 fix: 修复最后5处遗漏 getCurrency/getProviderStatus/getAllHealth
全项目扫描确认零残留 camelCase 调用。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-06 15:46:31 +08:00
LinuxJoy
d3f9bd2a16 fix: 修复遗漏的 UsageTracker 静态调用 camelCase → snake_case
getStats/getHistory/formatTokens/formatCost/formatCostByCurrency/calculateCost
在模板和调用方中未同步重命名,导致 __callStatic 找不到方法。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-06 15:36:39 +08:00
LinuxJoy
b0fe6a6f1e refactor: 方法名 camelCase → snake_case (39文件, 107个方法)
Routing/Failover/Budget/Usage/Analytics 全模块方法定义和调用方统一重命名,
符合 WordPress 编码规范。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-06 15:28:40 +08:00
LinuxJoy
02cb582d6c chore: 补全 strict_types 声明 + CHANGELOG 2.5.0-3.2.1
- 29 个 PHP 文件添加 declare(strict_types=1)
- CHANGELOG 补全 2.5.0、3.0.0、3.1.0、3.2.0、3.2.1 版本记录

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-06 13:09:28 +08:00
LinuxJoy
e18a999ff8 refactor: 模块依赖排序 + 定价数据去重 + GEO 设置去重
P1 修复:
- ModuleLoader 添加依赖排序 (resolve_load_order),确保 cost-control 先于 analytics 加载
- 提取共享 Pricing.php 类,消除 UsageTracker 两份 PRICING 常量重复 (~160行)
- Fallback 补全 baidu/minimax 定价数据

P2 修复:
- 从 wpmind.php 移除 5 个 GEO register_setting 重复注册,由 GeoModule 统一管理

净减少 112 行代码 (84+, 196-)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-06 12:59:32 +08:00
LinuxJoy
69c9942533 fix: 全面审查修复 - 15个安全/功能/质量问题
P0 紧急 (4):
- AnalyticsModule 私有构造函数导致 Fatal Error,移除单例模式
- 版本号统一为 3.2.0 (插件头部 + CLAUDE.md)
- 删除 wpmind.php 残留的损坏 PHPDoc 注释块
- 设置链接 URL 从 options-general.php 修正为 admin.php

P1 高 (7):
- ImageRouter 命名空间从 Routing\ 修正为 Providers\Image\
- AnalyticsModule nonce 名称从 wpmind_admin_nonce 修正为 wpmind_ajax
- 移除 wpmind_clear_usage_stats 重复 AJAX 注册
- uninstall.php 补充 GEO/模块状态等 13 个选项清理 + $wpdb->prepare()
- 测试端点添加 nonce 验证,移除 nopriv 未认证访问
- PublicAPI speech() 添加 uploads 目录路径验证防止路径遍历

P2 中 (4):
- admin.js errorCode 添加 escapeHtml() 防止 XSS
- GeoModule AJAX 使用 wp_unslash() 替代直接 $_POST 访问
- ModuleLoader 添加 WPMind\ 命名空间前缀验证
- stripslashes() 统一替换为 wp_unslash()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-06 12:47:30 +08:00
LinuxJoy
2a7c45aada fix: AnalyticsModule 实现缺失的接口方法
- 添加 check_dependencies() 方法检查模块依赖
- 添加 get_settings_tab() 方法返回设置标签页

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 12:27:47 +08:00
LinuxJoy
0aea0a17f0 fix: 修复模块管理设置项缺失问题
- 修复 Analytics 模块 module.json 格式(slug -> id)
- ModuleLoader 添加 settings_tab、requires、features 字段解析
- 统一模块配置格式

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 12:27:02 +08:00
LinuxJoy
839e5151b8 feat: 智能路由集成到真实请求链路
- 新增 RoutingHooks 类,将 IntelligentRouter 连接到 wpmind_select_provider filter
- 支持 auto 模式下的智能路由选择
- 支持指定 Provider 时的可用性检查和自动回退
- 根据请求上下文自动推断模型类型(chat/embedding/vision/completion)
- 添加路由决策和回退的 action hooks 用于日志记录
- 更新模块化计划文档,标记阶段 3 为已完成

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 12:23:41 +08:00
LinuxJoy
a8d7b5cf83 feat: 实施 Analytics 模块化
- 创建 modules/analytics/ 模块结构
- 迁移 AnalyticsManager 到模块
- 添加 AnalyticsManagerFallback 兼容层
- 更新设置页面支持模块状态检查
- 模块禁用时隐藏仪表板标签页
- 更新模块化计划文档

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 12:17:33 +08:00
LinuxJoy
144bab83bf fix: 修复熔断器状态转换 bug
问题:当熔断器处于 open 状态且恢复时间已过时,
recordSuccess/recordFailure 没有正确触发状态转换到 half_open,
导致熔断器一直显示"熔断中"即使有成功请求。

修复:在 recordSuccess 和 recordFailure 方法开始时检查
是否应该从 open 转换到 half_open 状态。

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 10:57:31 +08:00
LinuxJoy
d49ea59f8f fix: 添加缺失的 getProviderColor() 方法
模块 UsageTracker 缺少 getProviderColor() 方法,
导致 dashboard.php 调用时报错

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 10:43:52 +08:00
LinuxJoy
3c74dddea4 fix: 模块禁用时隐藏预算管理标签页
问题:Cost Control 模块停用后,预算管理标签页仍然显示

修复:
- settings-page.php: 添加 $cost_control_enabled 检查
- settings-page.php: 预算管理标签页和内容区域添加条件判断
- wpmind.php: ajax_save_budget_settings() 添加模块状态检查
- wpmind.php: ajax_get_budget_status() 添加模块状态检查

与 GEO 模块保持一致的实现方式

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 10:40:36 +08:00
LinuxJoy
907e5730fb fix: 添加缺失的 getWeekStats() 方法
- 模块 UsageTracker: 添加 getWeekStats() 方法
- Fallback UsageTracker: 同步添加 getWeekStats() 方法
- 修复 dashboard.php 调用时的 undefined method 错误

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 10:34:01 +08:00
LinuxJoy
d69fd55206 feat: 实施 Cost Control 模块 (高质量重构)
模块化架构:
- 创建 modules/cost-control/ 目录结构
- CostControlModule 实现 ModuleInterface
- 迁移 UsageTracker, BudgetManager, BudgetChecker, BudgetAlert

关键修复:
- BudgetManager: 实现 recursiveMerge() 替代 wp_parse_args()
  解决嵌套数组合并问题
- 模板: 添加 class_exists() 检查防止类未加载错误
- 模板: 使用 ?? 运算符安全访问数组键

事件驱动架构:
- wpmind.php 触发 wpmind_usage_record action
- CostControlModule 监听并处理用量记录
- 触发 wpmind_usage_recorded 防止重复记录

向后兼容:
- 兼容层委托给模块类
- Fallback 文件在模块未加载时提供回退实现
- 保持原有命名空间可用

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 10:28:10 +08:00
LinuxJoy
2aec4935e6 refactor: 清理 GEO 双实现,统一使用 modules/geo
- 删除旧的 includes/GEO/ 目录
- 更新 templates/tabs/geo.php 使用新命名空间 WPMind\Modules\Geo
- 更新测试文件使用新命名空间
- 更新 modules/geo/includes @package 注释
- 添加模块化方案文档 docs/MODULARIZATION-PLAN.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 23:31:28 +08:00
LinuxJoy
a54be49534 fix: 修复 llms.txt rewrite rules 刷新问题
- 模块启用/禁用时自动刷新 rewrite rules
- 插件激活时延迟刷新 rewrite rules (通过 admin_init/wp_loaded)
- 修复 wpmind_llms_txt_enabled 选项检查,支持字符串和布尔值
- 插件停用时清理 rewrite rules

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 22:57:58 +08:00
LinuxJoy
56f1563574 fix: 移除错误的 \n 字面字符串 2026-02-05 22:37:16 +08:00
LinuxJoy
152519e1a0 fix: 模块禁用后隐藏对应的标签页
改进交互逻辑:
- GEO 模块禁用时,隐藏 GEO 优化标签页
- 避免用户点击到不可用的功能
- 简化代码,移除冗余的模块状态检查

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 22:35:43 +08:00
LinuxJoy
85a9b7664d fix: 修复模块禁用后标签页无法点击的问题
问题原因:
当 GEO 模块被禁用时,模块类文件不会被加载,
但 settings-page.php 仍尝试加载 GEO 设置模板,
导致 PHP 致命错误(类不存在),页面渲染中断。

修复方案:
在加载 GEO 设置模板之前检查模块是否启用,
如果模块未启用,显示友好提示并提供跳转链接。

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 22:32:53 +08:00
LinuxJoy
95ba43c06f fix: 修复模块无法禁用的问题
根本原因:
WordPress update_option() 对 boolean false 值处理不一致,
可能导致 option 存储失败或被删除,使模块无法被禁用。

修复方案:
- 使用字符串 '1'/'0' 代替 boolean true/false
- is_module_enabled() 兼容旧格式 (boolean/integer/string)
- 所有设置保存使用字符串格式
- JavaScript 发送 '1'/'0' 代替 boolean

修改文件:
- includes/Core/ModuleLoader.php
- modules/geo/GeoModule.php
- modules/geo/templates/settings.php
- templates/tabs/modules.php
- templates/tabs/geo.php
- includes/GEO/MarkdownProcessor.php
- includes/GEO/MarkdownEnhancer.php

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 22:29:17 +08:00
LinuxJoy
2c2134ce85 feat: v3.2.0 模块化架构 - GEO 独立为可选模块
新增架构:
- includes/Core/ModuleInterface.php: 模块接口定义
- includes/Core/ModuleLoader.php: 模块发现、加载、管理
- modules/geo/: GEO 优化模块(独立目录)
  - module.json: 模块元数据
  - GeoModule.php: 模块入口类
  - includes/: 模块代码(从 includes/GEO 迁移)
  - templates/settings.php: 模块设置界面

新增功能:
- 模块管理界面 (templates/tabs/modules.php)
- 模块启用/禁用 AJAX 接口
- 模块自动发现机制

架构优势:
- GEO 功能可独立启用/禁用
- 为未来拆分为独立插件做准备
- 模块化设计,易于扩展

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 22:16:46 +08:00
LinuxJoy
ac91c8ac29 feat(geo): v3.1.0 GEO 增强 - 统一核心管线、llms.txt、Schema.org
新增功能:
- MarkdownProcessor: 统一核心处理管线,支持幂等性
- ProcessOptions: 可配置的处理选项
- LlmsTxtGenerator: 符合官方规范的 llms.txt 生成器
- SchemaGenerator: Article Schema.org 结构化数据,支持 SEO 插件兼容模式
- GEO 设置界面: llms.txt 和 Schema.org 配置选项

改进:
- MarkdownFeed/MarkdownEnhancer 重构使用统一 MarkdownProcessor
- 中文阅读时间计算优化 (400字/分钟)
- 多站点缓存键支持
- Schema inLanguage 属性

文档:
- V31-TASK-PLAN.md: 完整的 v3.1 任务计划
- GEO-EVALUATION-REPORT.md: GEO 模块评估报告

Codex 评审: 8.5/10 通过

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 22:03:44 +08:00
LinuxJoy
a11567e031 feat: 添加 GEO 模块 - Markdown Feeds 支持 (v3.0.0)
新增功能:
- Markdown Feeds 支持 (/?feed=markdown, .md 后缀)
- 中文内容优化器 (中英文空格、标点规范化)
- GEO 信号注入 (权威性声明、引用格式)
- AI 爬虫追踪 (GPTBot, ClaudeBot 等)
- HTML 到 Markdown 转换器

技术实现:
- 双模式架构:增强模式 (官方插件) + 独立模式
- 与官方 PR #194 兼容的 Filter Hook
- PHP 7.4+ 兼容
- Codex 代码审查通过

文件:
- includes/GEO/ - 6 个核心类
- tests/GEO/ - 3 个测试文件
- docs/GEO-MARKDOWN-FEEDS.md - 设计文档

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 15:37:37 +08:00
LinuxJoy
2d14c5d746 feat: 对齐官方 WordPress AI 插件 filter hook
- 更新 filter hook 从 ai_experiments_preferred_models 到
  ai_experiments_preferred_models_for_text_generation
- 与官方 WordPress AI 插件保持兼容

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 14:50:48 +08:00
LinuxJoy
68744cb57d chore: 更新版本号到 2.5.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 16:15:05 +08:00
LinuxJoy
cc87f005d9 fix: 调整 Toast 通知位置到 wpmind-title 下方
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 15:00:49 +08:00
LinuxJoy
14bc422737 feat: Phase 2.5 UI 错误反馈优化
- 改进 AJAX 连接测试错误消息 (#25)
  - 显示 HTTP 状态码和重试信息
  - 延长错误消息显示时间到 10 秒
  - 添加 Toast 错误通知
  - 改进超时和网络错误提示

- UI 层面错误反馈优化 (#28)
  - Toast 通知添加图标支持
  - 错误消息显示时间延长到 8 秒
  - 添加 Toast 和测试结果的 CSS 样式
  - 改进视觉反馈效果

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 12:23:36 +08:00
LinuxJoy
6e568938b2 feat: Phase 2.5 稳定性增强 - 自动跳过不健康 Provider 和手动优先级设置
- 自动跳过不健康 Provider (#32)
  - PublicAPI::do_chat() 使用 FailoverManager 获取故障转移链
  - 自动尝试链中的下一个健康 Provider

- 手动设置 Provider 优先级 (#33)
  - IntelligentRouter 添加 getManualPriority/setManualPriority 方法
  - FailoverManager 优先使用手动优先级排序
  - 新增 AJAX handler: wpmind_set_provider_priority
  - 路由页面添加拖拽排序 UI
  - 支持保存和清除手动优先级

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 12:19:03 +08:00
LinuxJoy
0f9b5e2b90 fix: 修复 Codex 二次审计发现的问题
- 修复 PublicAPI::get_status 选项键名错误 (wpmind_endpoints → wpmind_custom_endpoints)
- 修复预算邮箱字段名不一致 (alert_email → email_address)
- 完善卸载脚本:添加 wpmind_budget_notices、wpmind_round_robin_index 清理
- 添加动态 transient 清理 (wpmind_cb_* 熔断器状态)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 11:57:24 +08:00
LinuxJoy
4fab0b94ac security: 修复 Codex 审计发现的安全和代码质量问题
- 修复测试端点安全漏洞:nopriv 端点需要 WP_DEBUG + WPMIND_DEV_MODE 双重条件
- 修复选项键名不一致:统一使用 custom_base_url
- 修复 XSS 风险:添加 escapeHtml() 函数转义服务器消息
- 完善卸载脚本:清理所有 9 个选项和 2 个 transient

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 11:45:39 +08:00
LinuxJoy
d61c075c2f fix: 确保 ErrorHandler 在 PublicAPI 之前加载
在 load_public_api() 中添加 ErrorHandler 的 require_once,
确保 PublicAPI 使用 ErrorHandler 时类已经存在
2026-02-02 12:39:38 +08:00
LinuxJoy
0fcb38e74d feat: API 健壮性改进 - Phase 1-3 完成
Phase 1: 核心安全改进
- 添加全局循环调用保护 (call_stack 追踪)
- chat() 和 translate() 方法添加循环检测
- 使用 try-finally 确保调用栈正确清理

Phase 3: 错误处理标准化
- 新增 ErrorHandler 类
- 定义标准错误代码常量
- 实现快捷错误创建方法
- 添加错误日志记录和请求追踪

Phase 5: 测试
- 创建集成测试脚本
- 测试循环保护、翻译、拼音、缓存功能

文档
- 更新 API 改进计划
- 标记已完成任务
2026-02-02 12:34:29 +08:00
LinuxJoy
34d67c6195 feat: 添加语义化拼音功能 (wpmind_pinyin)
- 新增 wpmind_pinyin() 公共函数
- 在 translate prompt 中添加 format=pinyin 支持
- AI 按词语分隔而非按字分隔('你好世界' → 'nihao-shijie')
- 更新 wpslug 集成文档
2026-02-02 11:59:44 +08:00
LinuxJoy
8cf2f29e6c perf: 添加 is_available() 静态缓存优化
每个 HTTP 请求只检查一次端点可用性,避免重复遍历端点配置
2026-02-02 11:51:34 +08:00
LinuxJoy
e0b39e49bc fix: translate 方法使用 sanitize_title_with_dashes 避免循环调用
使用 sanitize_title_with_dashes() 替代 sanitize_title()
- sanitize_title() 会触发 filter,可能被其他插件拦截
- 这会导致无限递归(如 WPSlug 插件)
- sanitize_title_with_dashes() 直接处理字符串,不触发任何 filter
2026-02-02 11:21:30 +08:00
LinuxJoy
54d092af09 feat: 实现 WPMind 公共 API v2.7.0
新增 API:
- wpmind_summarize() - 文本摘要(paragraph/bullet/title)
- wpmind_moderate() - 内容审核
- wpmind_transcribe() - 音频转录(语音转文字)
- wpmind_speech() - 文本转语音(TTS)

增强 chat() 支持:
- tools 参数 - Function Calling 工具定义
- tool_choice 参数 - 工具选择策略
- 响应包含 tool_calls 和 finish_reason

技术特性:
- 摘要支持多种风格和语言
- 审核使用结构化输出保证格式
- 转录支持文件路径和 URL
- 语音合成自动上传到媒体库
2026-02-02 00:34:48 +08:00
LinuxJoy
a820278954 feat: 实现 WPMind 公共 API v2.6.0 增强功能
新增 API:
- wpmind_stream() - 流式输出
- wpmind_structured() - 结构化输出(支持 JSON Schema + 自动重试)
- wpmind_batch() - 批量处理
- wpmind_embed() - 文本嵌入向量
- wpmind_count_tokens() - Token 计数(估算)

技术特性:
- 流式使用 PHP stream context
- 结构化支持自动重试和 Schema 验证
- 批量支持延迟控制和错误处理
- 嵌入支持多服务商自动模型选择
2026-02-02 00:28:23 +08:00
LinuxJoy
f007de8d20 fix: 修复公共 API 中的类引用和模型获取逻辑
修复:
- WPMind 类命名空间引用
- UsageTracker 静态方法调用
- is_available() 使用 WPMind 实例
- 模型获取从 models 数组取第一个

测试通过:
- wpmind_is_available(): true
- wpmind_get_status(): 正确返回状态
- wpmind_chat(): 1+1=2 (1433ms)
- wpmind_translate(): 你好世界 -> Hello world (1226ms)
2026-02-02 00:22:40 +08:00
23 changed files with 3602 additions and 2418 deletions

685
WPMIND-ROADMAP.md Normal file
View file

@ -0,0 +1,685 @@
# WPMind 插件战略规划

> 让 WordPress 用户零门槛使用国产 AI

*文档版本: 3.7.0 | 创建日期: 2026-01-26 | 更新日期: 2026-02-07*

---

## 产品定位

### 核心使命

**从"技术桥接"升级为"用户赋能"**

```
传统模式:
用户 → 申请 API Key → 配置 → 使用 AI
(复杂、门槛高、成本不透明)

WPMind 模式:
用户 → 安装插件 → 立即使用 AI
(零配置、免费起步、智能路由)
```

### 产品架构

```
┌─────────────────────────────────────────────────────┐
│ WordPress 用户 │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ WPMind 插件 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 内容创作助手 │ │ 智能运营 │ │ GEO 优化 │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ 文派心思 AI 统一接入 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 智能路由引擎 │ │
│ │ • 任务类型识别 → 最优模型选择 │ │
│ │ • 成本优化 → 简单任务用便宜模型 │ │
│ │ • 故障转移 → 自动切换备用 │ │
│ │ • 负载均衡 → 高峰期分流 │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ 国产大模型集群 │
│ DeepSeek │ 通义千问 │ 智谱 │ Moonshot │ 豆包 │ ... │
└─────────────────────────────────────────────────────┘
```

### 核心价值主张

| 用户痛点 | WPMind 解决方案 |
|---------|----------------|
| 需要申请多个 API Key | 文派心思 AI 统一接入,零配置 |
| 不知道选哪个模型 | 智能路由自动选择最优模型 |
| API 成本不透明 | 免费版够用,专业版固定价 |
| 功能分散难上手 | 一站式内容创作助手 |
| 国际 API 访问不稳定 | 国产模型本地化服务 |

### 插件功能设计决策

> **2026-02-07 确立,所有后续功能设计必须遵守。**

**原则WPMind 保持纯 AI 能力插件,争议性功能移至 WPCY。**

凡涉及以下特征的功能,**优先移至文派叶子 WPCYslug: wp-china-yes插件**实现:

| 特征 | 说明 | 示例 |
|------|------|------|
| **请求拦截** | 拦截/修改其他插件的 HTTP 请求 | OpenAI API 代理、商业插件 AI 请求屏蔽 |
| **UI 替换** | 隐藏/替换其他插件的界面元素 | Block+Hide+Inject 商业插件 AI 面板 |
| **地域特供** | 仅特定地区用户需要的功能 | 国内镜像加速、GFW 绕过 |
| **商业争议** | 可能与商业插件产生利益冲突 | 绕过付费 AI 服务、模拟商业 API 响应 |
| **ToS 灰区** | 可能违反第三方服务条款 | 逆向工程商业 API 格式 |

**WPMind 只做:**
- 提供 AI APIchat/translate/embed/vision/...
- 提供功能模块Content Assistant/GEO/Analytics/...
- 通过标准 WordPress API 集成post meta/SlotFill/Hooks
- 面向全球用户,不含地域限制逻辑

**WPCY 负责:**
- 中国网络环境适配(镜像/加速/API 路由)
- 请求拦截和转发Layer 1 + Layer 3
- 检测 WPMind 安装状态,协同提供 AI 能力
- 地域特供的 UI 适配和开关

**协同机制:**
```
WPCY 检测 WPMind:
├── 已安装 → 调用 WPMind API路由/模型选择/failover
└── 未安装 → 内置简单 DeepSeek 转发(降级方案)
```

---

## 商业模式

### 版本定价

| 版本 | 价格 | 目标用户 | 核心功能 |
|------|------|---------|---------|
| **免费版** | ¥0 | 个人博客、小站 | 基础 AI 功能 + 每日额度 |
| **专业版** | ¥99/月 | 企业站、内容站 | 无限额度 + 高级功能 + GEO |
| **企业版** | 定制 | 大型客户 | 私有部署 + 自定义模型 |

### 免费版功能

| 功能 | 每日额度 | 说明 |
|------|---------|------|
| 标题生成 | 50 次 | 每篇文章 5 个候选 |
| 摘要生成 | 30 次 | 自动提取文章摘要 |
| 内容润色 | 20 次 | 选中文字优化 |
| 错别字检查 | 无限 | 基础质量保障 |

### 专业版增值

| 功能 | 描述 |
|------|------|
| 无限 AI 调用 | 不限次数 |
| 智能模型路由 | 自动选择最优模型 |
| GEO 优化套件 | AI 搜索引擎优化 |
| 批量内容生成 | 新闻源自动化 |
| 评论智能管理 | 自动回复/审核 |
| 多语言翻译 | 一键全文翻译 |
| 优先技术支持 | 工单响应 |

---

## 开发路线图

### Phase 1: 基础架构 (v1.0-1.3) ✅ 已完成

- [x] Provider 架构设计与实现
- [x] 6 个国产 AI Provider
- [x] WordPress AI Client SDK 集成
- [x] 原生 AI 功能支持(标题生成已验证)
- [x] 设置页面与 API Key 管理

**技术实现亮点:**
- `AuthenticatedProviderAvailability` 解决认证传递
- `prepareGenerateTextParams()` 强制 `n=1` 适配国内 API
- `pre_option_` filter 合并凭据到 AI Client

### Phase 2: 核心功能 (v1.5-2.0) ✅ 已完成

- [x] 用量统计系统 (`includes/Usage/`)
- [x] 预算管理系统 (`includes/Budget/`)
- [x] 分析仪表板 (`includes/Analytics/`)
- [x] Gutenberg 风格设计系统
- [x] Tab 导航 UI
- [x] 响应式适配
- [x] Chart.js 图表集成

### Phase 2.5: 稳定性增强 (v2.0-2.5) ✅ 已完成

- [x] **智能路由系统** (`includes/Routing/`)
- [x] 成本优先策略
- [x] 延迟优先策略
- [x] 可用性优先策略
- [x] 负载均衡策略
- [x] 复合策略(平衡/性能/经济)
- [x] **故障转移机制** (`includes/Failover/`)
- [x] Provider 健康检查
- [x] 自动跳过不健康 Provider
- [x] 手动优先级设置
- [x] **官方 Filter 集成**
- [x] 对齐 `ai_experiments_preferred_models_for_text_generation`
- [x] 更多 Provider 支持
- [x] 百度文心 (`includes/Providers/Baidu/`)
- [x] MiniMax (`includes/Providers/MiniMax/`)
- [x] 图像生成 (`includes/Providers/Image/`)
- [x] UI 错误反馈优化
- [x] Toast 通知系统

### Phase 3: GEO 解决方案 (v3.0-3.2) ✅ 已完成

- [x] AI 搜索引擎可见性优化
- [x] 结构化数据增强Schema.org
- [x] 内容权威性信号优化
- [x] AI 引用友好格式生成
- [x] 中文内容优化 Prompt 模板
- [x] 批量内容生成(适配新闻源场景)
- [x] **集成官方 Markdown Feeds** (#194)
- [x] 模块化架构GEO/Cost Control/Analytics 独立模块)

**SEO 插件扩展(独立扩展包):** 📋 规划中
- [ ] WPMind GEO for Rank Math
- [ ] WPMind GEO for Yoast SEO
- [ ] WPMind GEO for Slim SEO
- [ ] WPMind GEO for AIOSEO
- [ ] WPMind GEO for SEOPress

### Phase 3.5: 架构优化 (v3.3-3.7) ✅ 已完成

- [x] 编码规范化snake_case 重命名 (v3.3)
- [x] AI 请求链路审计 Phase A: 缓存键/JSON 防护/重试逻辑 (v3.4)
- [x] AI 请求链路审计 Phase B: 模型重选/路由统一 (v3.5)
- [x] AI 请求链路审计 Phase C: WP AI Client SDK 集成 (v3.6)
- [x] PublicAPI Facade 拆分为 6 个 Service 类 (v3.7)
- [x] Codex 安全审计修复SSRF 防护/文件校验等 9 项)(v3.7)

### Phase 4: 用户功能 + 生态扩展 (v4.0+) 📋 规划中

**新增 AI 能力API 层):**

| 能力 | 函数 | 说明 |
|------|------|------|
| 视觉理解 | `wpmind_vision()` | 图片描述/alt text/NSFW 检测,复用多模态 Provider |
| 向量存储 | `wpmind_store()` / `wpmind_search()` | RAG、语义搜索、相关文章 |
| 重排序 | `wpmind_rerank()` | 搜索结果按相关性重排 |

**新增模块(按优先级):**

| 阶段 | 模块 | 复用 API | 核心功能 |
|------|------|---------|---------|
| v4.0 | **Content Assistant** | chat/stream/structured/summarize | Gutenberg AI 面板、标题/摘要/大纲生成、改写/续写 |
| v4.1 | **Auto-Meta** | structured/batch/summarize | 发布时自动生成摘要/标签/FAQ/关键词 |
| v4.2 | **Comment Intelligence** | moderate/chat/structured | 评论审核/情感分析/AI 自动回复 |
| v4.3 | **Media Intelligence** | vision/generate_image | 图片 alt text/描述自动生成、AI 特色图片 |
| v4.4 | **Semantic Search** | embed + 向量存储 | 相关文章/语义搜索/RAG |
| v4.5 | **Translation** | translate/batch | 一键翻译/批量翻译/翻译记忆 |

**Post-Meta BridgeSEO 插件集成):**
- [ ] `PluginDocumentSettingPanel` 注册 WPMind AI 面板
- [ ] SEO 内容生成(标题/描述/关键词)
- [ ] 写入 Rank Math meta (`rank_math_title` 等)
- [ ] 写入 Yoast meta (`_yoast_wpseo_title` 等)
- [ ] 自动检测已安装的 SEO 插件

**AI Gateway 拆分决策2026-02-07 Claude + Codex 联合分析):**

> AI 拦截类功能全部移至 **文派叶子 WPCY** 插件实现。
> 理由WPCY 已有中国本地化拦截基础设施WordPress.org/Gravatar
> 用户群完全匹配(中国用户),无商业冲突(目标服务在中国本就不可用)。
> WPMind 保持纯 AI 能力插件定位,不做任何请求拦截或 UI 替换。
> 两个插件协同WPCY 负责拦截路由WPMind 负责 AI 能力。

| 层级 | 方案 | 归属 | 决策 |
|------|------|------|------|
| Layer 1 | OpenAI 兼容代理 | → **WPCY** | ✅ WPCY 可选功能 |
| Layer 2 | Post-Meta Bridge | **WPMind** | ✅ WPMind Gutenberg 面板 |
| Layer 3 | Block+Hide+Inject | → **WPCY** | ✅ WPCY 可选功能(实验性) |
| ~~原 Layer 2~~ | ~~覆盖转发~~ | — | ❌ 不做 |

**WPCY + WPMind 协同模式:**

```
WPCY 设置(中国本地化):
☑ WordPress 核心加速(默认开启)
☑ Gravatar 替换(默认开启)
☐ AI 服务本地化(可选)
→ 拦截 OpenAI/Anthropic/Google API 请求
→ 检测 WPMind → 使用 WPMind 路由引擎
→ 未安装 WPMind → 内置 DeepSeek 转发
☐ AI UI 替换(可选,实验性)
→ 隐藏商业插件 AI 面板
→ 注入 WPMind AI 面板(需安装 WPMind
→ ⚠️ 插件更新后可能需要重新适配
```

**生态扩展:**

- [ ] MCP Server 适配(让 Claude/ChatGPT 管理 WordPress
- [ ] Abilities API 扩展(注册自定义能力)
- [ ] AI 工作流自动化
- [ ] WooCommerce 集成
- [ ] 多站点支持
- [ ] API 开放
- [ ] **集成官方 Service Account** (#211)

---

## 模块化架构规划

> 2026-02-07 确立。核心层保持最小必要集,所有新功能作为模块实现。

### 架构分层原则

```
核心层(不可拆分,插件运行必需):
├── API/ PublicAPI Facade + 6 个 Service15 个全局函数)
├── Providers/ AI 服务商注册和管理
├── Routing/ 智能路由器 + 策略引擎
├── Failover/ 熔断器 + 健康追踪
├── SDK/ WP AI Client SDK 适配器
├── Admin/ 管理界面框架
└── Core/ 模块加载器

模块层(可选功能,用户可启用/禁用):
├── modules/geo/ ✅ 已有 — GEO 优化
├── modules/cost-control/ ✅ 已有 — 成本控制
├── modules/analytics/ ✅ 已有 — 分析面板
├── modules/content-assistant/ 🆕 v4.0 — 内容创作助手
├── modules/auto-meta/ 🆕 v4.1 — 自动元数据
├── modules/comment-intelligence/ 🆕 v4.2 — 评论智能
├── modules/media-intelligence/ 🆕 v4.3 — 媒体智能
├── modules/semantic-search/ 🆕 v4.4 — 语义搜索
└── modules/translation/ 🆕 v4.5 — 翻译管理
```

### 模块判断标准

```
✅ 做成模块: 可选 + 自包含 + 用户可见 + 可独立测试 + 可独立更新
❌ 留在核心: 插件必需 / 被其他模块依赖 / 需早于模块加载 / 无用户开关
```

### 核心层优化任务v3.8

| 任务 | 说明 | 影响范围 |
|------|------|---------|
| **清理兼容层** | 移除 `includes/Usage/`、`Budget/`、`Analytics/` 兼容层 | 已 deprecated 自 v3.3 |
| **Provider 懒加载** | 只加载用户启用的 Provider减少内存占用 | `includes/Providers/register.php` |
| **路由策略可插拔** | 开放 `wpmind_routing_strategies` filter允许模块注册自定义策略 | `includes/Routing/` |

### 新模块结构参考(以 GEO 模块为标准)

```
modules/{module-name}/
├── module.json # 元数据id/name/version/requires/features
├── {ModuleName}Module.php # 主类,实现 ModuleInterface
├── includes/ # 内部组件
│ ├── Component1.php
│ └── Component2.php
├── assets/ # 前端资源(可选)
│ ├── js/
│ └── css/
└── templates/
└── settings.php # 设置页面模板
```

---

## 文派心思 AI 智能路由

### 路由策略 (已实现)

```
用户请求
任务分析器 (识别任务类型)
┌─────────────────────────────────────┐
│ 路由决策引擎 │
├─────────────────────────────────────┤
│ 成本优先 (CostStrategy) │
│ → 选择成本最低的 Provider │
├─────────────────────────────────────┤
│ 延迟优先 (LatencyStrategy) │
│ → 选择响应最快的 Provider │
├─────────────────────────────────────┤
│ 可用性优先 (AvailabilityStrategy) │
│ → 选择健康分数最高的 Provider │
├─────────────────────────────────────┤
│ 负载均衡 (LoadBalancedStrategy) │
│ → 在多个 Provider 之间分散请求 │
├─────────────────────────────────────┤
│ 复合策略 (CompositeStrategy) │
│ → 平衡/性能优先/经济策略 │
└─────────────────────────────────────┘
```

### 模型能力矩阵

| 模型 | 优势场景 | 成本 | 上下文 |
|------|---------|------|--------|
| DeepSeek | 推理、代码、通用 | ⭐ 极低 | 64K |
| 通义千问 | 中文理解、多模态 | ⭐⭐ 中等 | 128K |
| 智谱 AI | 知识问答、Agent | ⭐⭐ 中等 | 128K |
| Moonshot | 超长上下文 | ⭐⭐⭐ 较高 | 128K |
| 豆包 | 对话、创意写作 | ⭐ 低 | 128K |

---

## 技术架构

### Provider 架构

```
WPMind\\Providers\\
├── AbstractOpenAiCompatibleProvider.php
├── AbstractOpenAiCompatibleTextGenerationModel.php
├── AbstractOpenAiCompatibleModelMetadataDirectory.php
├── AuthenticatedProviderAvailability.php
├── ProviderRegistrar.php
├── register.php
├── DeepSeek/
├── Qwen/
├── Zhipu/
├── Moonshot/
├── Doubao/
├── SiliconFlow/
├── Baidu/ # v2.0 新增
├── MiniMax/ # v2.0 新增
└── Image/ # v2.0 新增
```

### 功能模块

```
WPMind\\
├── API/ # Public API (Facade + Services)
│ └── Services/ # ChatService, TextProcessing, Embedding, Audio, Image, StructuredOutput
├── Analytics/ # 分析仪表板
├── Budget/ # 预算管理
├── Failover/ # 故障转移
├── Routing/ # 智能路由
│ └── Strategies/ # 路由策略
├── SDK/ # WP AI Client SDK 适配
└── Usage/ # 用量统计
```

### 支持的 AI 服务

| 服务 | Provider ID | 模型 | 状态 |
|------|-------------|------|------|
| OpenAI | `openai` | gpt-4o, gpt-4o-mini | ✅ |
| Anthropic | `anthropic` | claude-3-5-sonnet | ✅ |
| Google | `google` | gemini-2.0-flash | ✅ |
| DeepSeek | `deepseek` | deepseek-chat, deepseek-reasoner | ✅ |
| 通义千问 | `qwen` | qwen-turbo, qwen-plus, qwen-max | ✅ |
| 智谱 AI | `zhipu` | glm-4, glm-4-flash, glm-4-plus | ✅ |
| Moonshot | `moonshot` | moonshot-v1-8k/32k/128k | ✅ |
| 豆包 | `doubao` | doubao-pro-4k/32k/128k | ✅ |
| 硅基流动 | `siliconflow` | DeepSeek-V3, Qwen2.5-72B | ✅ |
| 百度文心 | `baidu` | ernie-4.0, ernie-3.5 | ✅ |
| MiniMax | `minimax` | abab6.5s-chat | ✅ |

---

## WordPress AI 生态背景

### 官方发布时间线

| 版本 | 时间 | 关键内容 |
|------|------|----------|
| WordPress 6.8 | 2025-04-15 | PHP 依赖现代化准备 |
| WordPress 6.9 | 2025-12-02 | PHP AI Client + MCP Adapter |
| **WordPress 7.0** | **2026-03/04** | **WP AI Client 合并到核心** |

### 官方四大技术支柱

1. **PHP AI Client SDK** - 统一的 LLM 抽象层
2. **Abilities API** - WordPress 能力注册表
3. **MCP Adapter** - 连接外部 AI 助手
4. **AI Experiments** - 功能实验室

### WPMind 与官方的关系

```
官方 AI Building Blocks (WordPress Core)
WPMind (增强层)
• 国产模型支持
• 智能路由
• 用户友好功能
文派心思 AI (统一接入)
国产大模型集群
```

### 官方 Filter 集成策略

WPMind 通过官方提供的 Filter Hooks 无缝注入国产模型支持:

```php
// 注入国产模型到官方首选列表 (已实现)
add_filter( 'ai_experiments_preferred_models_for_text_generation', function( $models ) {
return array_merge(
array(
array( 'deepseek', 'deepseek-chat' ),
array( 'qwen', 'qwen-turbo' ),
array( 'zhipu', 'glm-4-flash' ),
array( 'moonshot', 'moonshot-v1-8k' ),
array( 'doubao', 'doubao-pro-4k' ),
),
$models
);
});
```

### 官方可用 Filter Hooks

| Hook | 用途 | WPMind 应用 |
|------|------|-------------|
| `ai_experiments_preferred_models_for_text_generation` | 文本生成模型偏好 | ✅ 已实现 |
| `ai_experiments_preferred_image_models` | 图像生成模型偏好 | ✅ 已实现 |
| `ai_experiments_pre_has_valid_credentials_check` | 凭据验证前置 | 待实现 |
| `ai_experiments_pre_normalize_content` | 内容预处理 | 待实现 |
| `ai_experiments_normalize_content` | 内容后处理 | 待实现 |

---

## 官方仓库跟踪

> **重要**: 持续跟踪 WordPress/ai 仓库动态,确保 WPMind 与官方保持兼容和协同。

### 跟踪仓库

| 仓库 | 用途 | 跟踪频率 |
|------|------|----------|
| [WordPress/ai](https://github.com/WordPress/ai) | AI Experiments 插件 | 每周 |
| [WordPress/php-ai-client](https://github.com/WordPress/php-ai-client) | PHP AI Client SDK | 每月 |
| [WordPress/mcp-adapter](https://github.com/WordPress/mcp-adapter) | MCP 适配器 | 每月 |

### 官方版本路线图

| 版本 | 状态 | 关键功能 | 对 WPMind 影响 |
|------|------|----------|----------------|
| 0.1.1 | ✅ 已发布 | WP AI Client 0.2.0 | 基础依赖 |
| 0.2.0 | ✅ 已发布 | 基础实验功能 | 参考实现 |
| **0.3.0** | 🚧 开发中 | Service Account, Markdown Feeds | **需要关注** |
| 0.4.0 | 📋 规划中 | Ability Table 扩展 | 可能影响 |

### 重点跟踪 PR

| # | 标题 | 状态 | 与 WPMind 关系 | 最后检查 |
|---|------|------|----------------|----------|
| **#211** | Add Service Account experiment | Draft | ⚠️ 文派心思 AI 可利用 | 2026-02-05 |
| **#194** | Add Markdown Feeds experiment | Open | ✅ GEO 优化可集成 | 2026-02-05 |

### 重点跟踪 Issues

| # | 标题 | 里程碑 | 与 WPMind 关系 | 最后检查 |
|---|------|--------|----------------|----------|
| **#27** | Pre-configured AI providers | 0.3.0 | ⚠️ 影响 Provider 设计 | 2026-02-05 |
| **#21** | Support hundreds of abilities | 0.3.0 | 参考 Layered Tool Pattern | 2026-02-05 |
| **#192** | Custom prompt templates | Future | 参考扩展点设计 | 2026-02-05 |
| **#190** | Site-wide AI content insights | Future | ⚠️ 潜在功能重叠 | 2026-02-05 |

### 差异化优势分析

| 领域 | 官方 AI 插件 | WPMind | 优势 |
|------|-------------|--------|------|
| **Provider 支持** | Anthropic, Google, OpenAI | 11 个 Provider (含国产) | ✅ 独占 |
| **智能路由** | 无 | 5 种策略 + 复合策略 | ✅ 独占 |
| **商业模式** | 无 | 免费版/专业版 | ✅ 独占 |
| **统一接入** | 需用户配置 API Key | 文派心思零配置 | ✅ 独占 |
| **中文优化** | 无 | 中文内容处理优化 | ✅ 独占 |
| **GEO 优化** | Markdown Feeds (基础) | 完整 GEO 套件 | ✅ 增强 |
| **预算管理** | 无 | 支出护栏 + 告警 | ✅ 独占 |
| **分析仪表板** | 无 | Chart.js 可视化 | ✅ 独占 |

### 官方首选模型(无国产)

```php
// 官方 helpers.php - get_preferred_models_for_text_generation()
$preferred_models = array(
array( 'anthropic', 'claude-haiku-4-5' ),
array( 'google', 'gemini-2.5-flash' ),
array( 'openai', 'gpt-4o-mini' ),
array( 'openai', 'gpt-4.1' ),
);
// ⚠️ 完全没有国产模型!这是 WPMind 的核心差异化优势
```

---

## GEO 解决方案

### 什么是 GEO

**Generative Engine Optimization (GEO)** - 生成式引擎优化

- **传统 SEO**: 优化 Google/Bing 搜索结果排名
- **GEO**: 优化 AI 搜索引擎ChatGPT、Perplexity、Google AI Overview的引用

### GEO 核心策略

1. **引用优化** - 让 AI 更容易引用你的内容
2. **权威性信号** - 增强内容可信度
3. **结构化数据** - 帮助 AI 理解内容
4. **问答格式** - 适配 AI 对话式搜索
5. **实体关联** - 建立知识图谱连接

### SEO 插件扩展

```
WPMind Core (核心插件)
├── WPMind GEO for Rank Math
├── WPMind GEO for Yoast SEO
├── WPMind GEO for Slim SEO
├── WPMind GEO for AIOSEO
└── WPMind GEO for SEOPress
```

---

## 市场机会

| 指标 | 数据 |
|------|------|
| WordPress AI 插件市场 | 2025年 $5亿 → 2033年 $25亿 |
| 年复合增长率 | 25% CAGR |
| 2026年 AI 采用率 | 60%+ WordPress 站点 |
| 内容生产效率提升 | 3小时 → 90分钟 |

---

## 开发环境

| 项目 | 路径 |
|------|------|
| 开发仓库 | `~/Projects/wpmind/` |
| 部署目录 | `/www/wwwroot/wpcy.com/wp-content/plugins/wpmind/` |
| 测试站点 | `wpcy.com` |
| 部署脚本 | `./deploy.sh` |

---

## 相关资源

- [WordPress AI Building Blocks](https://make.wordpress.org/ai/2025/07/17/ai-building-blocks/)
- [AI Experiments Plugin](https://wordpress.org/plugins/ai/)
- [PHP AI Client SDK](https://github.com/WordPress/php-ai-client)
- [MCP Adapter](https://github.com/WordPress/mcp-adapter)
- [Abilities API 文档](https://make.wordpress.org/core/2025/11/10/abilities-api-in-wordpress-6-9/)

---

## 更新日志

### v3.0.0 (2026-02-05) - 文档同步

**文档更新:**
- 同步开发进度到 v2.5.0
- 合并官方仓库跟踪研究
- 更新开发路线图状态
- 修正开发环境路径
- 更新支持的 AI 服务列表11 个)

**已完成功能确认:**
- Phase 1: 基础架构 ✅
- Phase 2: 核心功能 ✅
- Phase 2.5: 稳定性增强 ✅
- 智能路由系统 ✅
- 故障转移机制 ✅
- 官方 Filter Hook 对齐 ✅

### v2.1.0 (2026-02-05) - 官方仓库跟踪

**新增内容:**
- 添加官方仓库跟踪章节
- 记录重点 PR (#211 Service Account, #194 Markdown Feeds)
- 记录重点 Issues (#27, #21, #192, #190)
- 添加官方 Filter Hooks 集成策略
- 差异化优势分析

**关键发现:**
- 官方完全没有国产模型支持(核心差异化优势)
- 官方提供 Filter Hooks 可无缝注入国产模型
- Service Account (#211) 可用于文派心思 AI 统一接入
- Markdown Feeds (#194) 可集成到 GEO 优化套件

### v2.0.0 (2026-01-26) - 战略升级

**产品定位:**
- 从"技术桥接"升级为"用户赋能"
- 引入文派心思 AI 统一接入
- 智能路由引擎设计
- 免费版/专业版商业模式

### v1.3.0 (2026-01-26) - Phase 1 完成

**新增功能:**
- 完整的 Provider 架构实现
- 6 个国内 AI 服务 Provider
- WordPress AI 原生功能支持

**技术改进:**
- `AuthenticatedProviderAvailability` - 解决认证传递
- `prepareGenerateTextParams()` - 强制 `n=1`
- `pre_option_` filter - 合并凭据

---

*最后更新: 2026-02-07 13:00*

22
composer.json Normal file
View file

@ -0,0 +1,22 @@
{
"name": "wenpai/wpmind",
"description": "WPMind - WordPress AI 自定义端点扩展",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"require": {
"php": ">=8.1"
},
"require-dev": {
"yoast/wp-test-utils": "^1.2"
},
"autoload": {
"psr-4": {
"WPMind\\": "includes/"
}
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
}
}

2205
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -68,6 +68,7 @@ final class AjaxController {
add_action( 'wp_ajax_wpmind_route_request', [ $this, 'ajax_route_request' ] );
add_action( 'wp_ajax_wpmind_set_provider_priority', [ $this, 'ajax_set_provider_priority' ] );
// GEO settings are handled by GeoModule.
add_action( 'wp_ajax_wpmind_license', [ $this, 'ajax_license' ] );
}

/**
@ -610,4 +611,41 @@ final class AjaxController {
'message' => '连接失败 (HTTP ' . $status_code . ')',
];
}

/**
* AJAX 授权操作(激活/停用)。
*
* @since 4.0.0
*/
public function ajax_license(): void {
check_ajax_referer( 'wpmind_license_action' );

if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( [ 'message' => '权限不足' ] );
}

$action = sanitize_text_field( $_POST['license_action'] ?? '' );
$key = sanitize_text_field( $_POST['license_key'] ?? '' );

$license = \WPMind\wpmind_license();

if ( $action === 'activate' ) {
if ( $key === '' ) {
wp_send_json_error( [ 'message' => '请输入 License Key' ] );
}
$license->set_key( $key );
$result = $license->activate();
if ( $result ) {
wp_send_json_success( [ 'message' => '授权激活成功' ] );
} else {
wp_send_json_error( [ 'message' => '激活失败,请检查 License Key 是否正确' ] );
}
} elseif ( $action === 'deactivate' ) {
$license->deactivate();
$license->set_key( '' );
wp_send_json_success( [ 'message' => '授权已停用' ] );
} else {
wp_send_json_error( [ 'message' => '未知操作' ] );
}
}
}

View file

@ -0,0 +1,311 @@
<?php
/**
* WenPai License Client
*
* Handles license verification, activation, and deactivation
* for WenPai commercial plugins.
*
* @package WPMind
* @since 4.0.0
*/

declare( strict_types=1 );

if ( ! defined( 'ABSPATH' ) ) {
exit;
}

/**
* WenPai License client.
*
* @since 4.0.0
*/
class WenPai_License {

/**
* License server API URL.
*
* @var string
*/
private string $api_url = 'https://license.wenpai.net';

/**
* Product slug.
*
* @var string
*/
private string $product_slug;

/**
* WordPress option key for the license key.
*
* @var string
*/
private string $option_key;

/**
* Transient key for cached verification.
*
* @var string
*/
private string $cache_key;

/**
* Constructor.
*
* @param string $product_slug Product identifier.
* @param string $api_url Optional custom API URL.
*/
public function __construct( string $product_slug, string $api_url = '' ) {
$this->product_slug = $product_slug;
$this->option_key = 'wenpai_license_key_' . $product_slug;
$this->cache_key = 'wenpai_license_' . $product_slug;

if ( $api_url !== '' ) {
$this->api_url = rtrim( $api_url, '/' );
} elseif ( defined( 'WENPAI_LICENSE_URL' ) ) {
$this->api_url = rtrim( WENPAI_LICENSE_URL, '/' );
}
}

/**
* Get the stored license key.
*
* @return string
*/
public function get_key(): string {
return (string) get_option( $this->option_key, '' );
}

/**
* Save a license key.
*
* @param string $key License key.
*/
public function set_key( string $key ): void {
update_option( $this->option_key, sanitize_text_field( $key ) );
delete_transient( $this->cache_key );
}

/**
* Verify the license remotely. Results are cached for 24 hours.
*
* @param bool $force_refresh Skip cache.
* @return array{valid: bool, plan: string, expires_at: string|null, features: array, cache_ttl: int}
*/
public function verify( bool $force_refresh = false ): array {
$key = $this->get_key();
if ( $key === '' ) {
return $this->free_response();
}

if ( ! $force_refresh ) {
$cached = get_transient( $this->cache_key );
if ( is_array( $cached ) ) {
return $cached;
}
}

$response = $this->api_request( '/api/v1/license/verify', [
'license_key' => $key,
'site_url' => home_url(),
] );

if ( is_wp_error( $response ) ) {
// Grace period: use last known good state for up to 7 days.
$grace = get_option( $this->cache_key . '_grace' );
if ( is_array( $grace ) ) {
$grace_expires = (int) ( $grace['_grace_until'] ?? 0 );
if ( $grace_expires > time() ) {
return $grace;
}
}
return $this->free_response();
}

$data = $this->parse_response( $response );

// Cache the result.
$ttl = (int) ( $data['cache_ttl'] ?? 86400 );
set_transient( $this->cache_key, $data, $ttl );

// Store grace period copy (7 days).
$data['_grace_until'] = time() + ( 7 * DAY_IN_SECONDS );
update_option( $this->cache_key . '_grace', $data );

unset( $data['_grace_until'] );
return $data;
}

/**
* Activate the current site.
*
* @return bool True on success.
*/
public function activate(): bool {
$key = $this->get_key();
if ( $key === '' ) {
return false;
}

$response = $this->api_request( '/api/v1/license/activate', [
'license_key' => $key,
'site_url' => home_url(),
] );

if ( is_wp_error( $response ) ) {
return false;
}

$data = $this->parse_response( $response );

// Refresh cache with activation result.
if ( ! empty( $data['valid'] ) ) {
$ttl = (int) ( $data['cache_ttl'] ?? 86400 );
set_transient( $this->cache_key, $data, $ttl );
}

return ! empty( $data['valid'] );
}

/**
* Deactivate the current site.
*
* @return bool True on success.
*/
public function deactivate(): bool {
$key = $this->get_key();
if ( $key === '' ) {
return false;
}

$response = $this->api_request( '/api/v1/license/deactivate', [
'license_key' => $key,
'site_url' => home_url(),
] );

delete_transient( $this->cache_key );
delete_option( $this->cache_key . '_grace' );

return ! is_wp_error( $response );
}

/**
* Get the current plan name.
*
* @return string free|pro|enterprise
*/
public function plan(): string {
$data = $this->verify();
if ( empty( $data['valid'] ) ) {
return 'free';
}
return $data['plan'] ?? 'free';
}

/**
* Check if a specific feature is available.
*
* @param string $feature Feature key.
* @return bool
*/
public function can( string $feature ): bool {
$data = $this->verify();
if ( empty( $data['valid'] ) || empty( $data['features'] ) ) {
return false;
}
return isset( $data['features'][ $feature ] ) && $data['features'][ $feature ] !== 0;
}

/**
* Get the quota for a feature. Returns -1 for unlimited.
*
* @param string $feature Feature key.
* @return int
*/
public function quota( string $feature ): int {
$data = $this->verify();
if ( empty( $data['valid'] ) || empty( $data['features'] ) ) {
$free = $this->free_features();
return $free[ $feature ] ?? 0;
}
return (int) ( $data['features'][ $feature ] ?? 0 );
}

/**
* Check if the license is valid (includes 7-day grace period).
*
* @return bool
*/
public function is_valid(): bool {
$data = $this->verify();
return ! empty( $data['valid'] );
}

/**
* Make an API request to the license server.
*
* @param string $endpoint API path.
* @param array $body Request body.
* @return array|\WP_Error
*/
private function api_request( string $endpoint, array $body ) {
$url = $this->api_url . $endpoint;

$response = wp_remote_post( $url, [
'timeout' => 15,
'headers' => [ 'Content-Type' => 'application/json' ],
'body' => wp_json_encode( $body ),
] );

if ( is_wp_error( $response ) ) {
return $response;
}

$code = wp_remote_retrieve_response_code( $response );
if ( $code < 200 || $code >= 300 ) {
return new \WP_Error( 'license_api_error', 'License API returned ' . $code );
}

return $response;
}

/**
* Parse the JSON response body.
*
* @param array $response wp_remote_post response.
* @return array
*/
private function parse_response( $response ): array {
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
return is_array( $data ) ? $data : [];
}

/**
* Default response for free/unlicensed users.
*
* @return array
*/
private function free_response(): array {
return [
'valid' => false,
'plan' => 'free',
'features' => $this->free_features(),
'cache_ttl' => 3600,
];
}

/**
* Free tier feature limits.
*
* @return array<string, int>
*/
private function free_features(): array {
return [
'analytics_days' => 7,
'cache_limit' => 100,
'auto_meta_daily' => 10,
];
}
}

View file

@ -0,0 +1,162 @@
# API Gateway - Deployment Guide

WPMind API Gateway 模块部署指南。

## Prerequisites

| 要求 | 最低版本 |
|------|----------|
| PHP | 8.1+ |
| WordPress | 6.0+ |
| WPMind 插件 | 3.6.0+ (已激活) |
| MySQL/MariaDB | 5.7+ / 10.3+ |

可选依赖:
- **Redis** - 用于高性能速率限制 (推荐生产环境)
- **OpenSSL** - API Key 哈希 (PHP 默认已包含)

## Database Setup

数据库表在模块激活时由 `SchemaManager` 自动创建,无需手动操作。

自动创建的表:
- `{prefix}wpmind_api_keys` - API 密钥存储
- `{prefix}wpmind_gateway_logs` - 请求审计日志

如需手动触发升级:
```php
\WPMind\Modules\ApiGateway\SchemaManager::maybe_upgrade();
```

## Nginx Configuration

在站点 Nginx 配置中添加以下规则,确保 SSE 流式输出正常工作:

```nginx
# API Gateway - SSE streaming support
location ~ ^/wp-json/mind/v1/ {
proxy_buffering off;
proxy_cache off;

proxy_read_timeout 300s;
proxy_send_timeout 300s;

# SSE headers
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding on;

# Allow large request bodies (embeddings)
client_max_body_size 10m;

# Pass to PHP-FPM
try_files $uri $uri/ /index.php?$args;
}
```

> **宝塔面板**: 在站点设置 > 配置文件中添加上述 location 块,放在其他 location 规则之前。

## Apache Configuration

在 WordPress 根目录的 `.htaccess` 中添加:

```apache
# API Gateway - SSE support
<IfModule mod_headers.c>
<LocationMatch "^/wp-json/mind/v1/">
Header set Cache-Control "no-cache, no-store"
Header set X-Accel-Buffering "no"
</LocationMatch>
</IfModule>

# Increase timeout for streaming endpoints
<IfModule mod_reqtimeout.c>
RequestReadTimeout body=300
</IfModule>

# Allow large request bodies
LimitRequestBody 10485760
```

## Cloudflare Notes

Cloudflare 默认会缓冲 SSE 响应,需要特殊配置:

1. **Page Rules** (推荐): 为 `your-site.com/wp-json/mind/v1/*` 创建规则:
- Cache Level: Bypass
- Disable Performance (关闭 Rocket Loader, Minification)

2. **Response Header**: 模块已自动发送 `X-Accel-Buffering: no`Cloudflare 会识别此头部

3. **Timeout**: Cloudflare Free 计划最大超时 100 秒。如果流式响应超过此限制:
- 升级到 Pro 计划 (300 秒)
- 或在客户端设置 `stream: false` 使用非流式模式

## Redis (Optional)

速率限制默认使用 WordPress Transients (数据库)。生产环境推荐使用 Redis:

```php
// wp-config.php
define( 'WP_REDIS_HOST', '127.0.0.1' );
define( 'WP_REDIS_PORT', 6379 );
```

模块会自动检测 Redis 可用性:
- Redis 可用 -> `RedisRateStore` (高性能,原子操作)
- Redis 不可用 -> `TransientRateStore` (兼容模式,使用数据库)

## First Steps After Deployment

### 1. 启用网关

进入 WordPress 后台 > WPMind > API Gateway 设置页面,开启网关。

### 2. 创建 API Key

在设置页面点击「创建 API Key」记录生成的密钥 (仅显示一次)。

### 3. 测试连接

```bash
# 非流式请求
curl -X POST https://your-site.com/wp-json/mind/v1/chat/completions \
-H "Authorization: Bearer sk_mind_xxx" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Hello"}]
}'

# 流式请求 (SSE)
curl -X POST https://your-site.com/wp-json/mind/v1/chat/completions \
-H "Authorization: Bearer sk_mind_xxx" \
-H "Content-Type: application/json" \
-N \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "Hello"}],
"stream": true
}'

# 查看可用模型
curl https://your-site.com/wp-json/mind/v1/models \
-H "Authorization: Bearer sk_mind_xxx"

# 网关状态
curl https://your-site.com/wp-json/mind/v1/status \
-H "Authorization: Bearer sk_mind_xxx"
```

## Troubleshooting

| 问题 | 原因 | 解决方案 |
|------|------|----------|
| 404 on endpoints | 固定链接未刷新 | 后台 > 设置 > 固定链接,点击保存 |
| 413 Request Entity Too Large | Nginx body size 限制 | 添加 `client_max_body_size 10m;` |
| SSE 不工作 (一次性返回) | 代理缓冲未关闭 | 添加 `proxy_buffering off;` |
| 504 Gateway Timeout | 上游响应超时 | 增加 `proxy_read_timeout 300s;` |
| 401 Unauthorized | API Key 无效或已撤销 | 检查 Key 状态,重新创建 |
| 429 Too Many Requests | 触发速率限制 | 等待窗口重置或调整限制 |
| 数据库表不存在 | 模块未正确激活 | 停用后重新激活模块 |
| Redis 连接失败 | Redis 未运行 | 检查 Redis 服务,或忽略 (自动降级到 Transients) |

22
phpunit.xml.dist Normal file
View file

@ -0,0 +1,22 @@
<?xml version="1.0"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
bootstrap="tests/bootstrap.php"
colors="true"
beStrictAboutTestsThatDoNotTestAnything="true"
>
<testsuites>
<testsuite name="unit">
<directory suffix="Test.php">tests/Unit</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory suffix=".php">includes</directory>
</include>
<exclude>
<file>includes/class-wenpai-updater.php</file>
</exclude>
</coverage>
</phpunit>

View file

@ -107,6 +107,9 @@ defined("ABSPATH") || exit(); ?>
<a href="#modules" class="wpmind-tab" data-tab="modules">
<?php esc_html_e("模块管理", "wpmind"); ?>
</a>
<a href="#license" class="wpmind-tab" data-tab="license">
<?php esc_html_e("授权", "wpmind"); ?>
</a>
</nav>

<!-- 主内容区 -->
@ -189,6 +192,9 @@ defined("ABSPATH") || exit(); ?>
<div id="modules" class="wpmind-tab-pane">
<?php include WPMIND_PLUGIN_DIR . "templates/tabs/modules.php"; ?>
</div>
<div id="license" class="wpmind-tab-pane">
<?php include WPMIND_PLUGIN_DIR . "templates/tabs/license.php"; ?>
</div>
</div>
</div>
</div>

134
templates/tabs/license.php Normal file
View file

@ -0,0 +1,134 @@
<?php
/**
* WPMind 授权设置 Tab
*
* @package WPMind
* @since 4.0.0
*/

defined( 'ABSPATH' ) || exit();

$license = \WPMind\wpmind_license();
$key = $license->get_key();
$data = $license->verify();
$is_valid = ! empty( $data['valid'] );
$plan = $data['plan'] ?? 'free';
?>

<div class="wpmind-license-settings">
<h2><?php esc_html_e( '授权管理', 'wpmind' ); ?></h2>
<p class="description">
<?php esc_html_e( '输入您的授权密钥以解锁 Pro/Enterprise 功能。', 'wpmind' ); ?>
</p>

<!-- 状态卡片 -->
<div class="wpmind-license-status" style="margin: 16px 0; padding: 16px; background: <?php echo $is_valid ? '#f0fdf4' : '#fefce8'; ?>; border-left: 4px solid <?php echo $is_valid ? '#22c55e' : '#eab308'; ?>; border-radius: 4px;">
<strong><?php esc_html_e( '当前状态', 'wpmind' ); ?>:</strong>
<?php if ( $is_valid ) : ?>
<span style="color: #16a34a;">
<?php
printf(
/* translators: %s: plan name */
esc_html__( '已激活 — %s', 'wpmind' ),
esc_html( strtoupper( $plan ) )
);
?>
</span>
<?php if ( ! empty( $data['expires_at'] ) ) : ?>
<br><small>
<?php
printf(
/* translators: %s: expiration date */
esc_html__( '到期时间: %s', 'wpmind' ),
esc_html( wp_date( 'Y-m-d', strtotime( $data['expires_at'] ) ) )
);
?>
</small>
<?php endif; ?>
<?php else : ?>
<span style="color: #ca8a04;">
<?php esc_html_e( '免费版', 'wpmind' ); ?>
</span>
<?php endif; ?>
</div>

<!-- License Key 输入 -->
<table class="form-table">
<tr>
<th scope="row">
<label for="wpmind-license-key"><?php esc_html_e( 'License Key', 'wpmind' ); ?></label>
</th>
<td>
<input type="text" id="wpmind-license-key" class="regular-text"
value="<?php echo esc_attr( $key ); ?>"
placeholder="wenpai_wpmind_pro_xxxxxxxx"
<?php echo $is_valid ? 'readonly' : ''; ?>
/>
<p class="description">
<?php esc_html_e( '在 wenpai.net 购买后获取授权密钥。', 'wpmind' ); ?>
</p>
</td>
</tr>
</table>

<!-- 操作按钮 -->
<p class="submit">
<?php if ( $is_valid ) : ?>
<button type="button" id="wpmind-license-deactivate" class="button button-secondary">
<?php esc_html_e( '停用授权', 'wpmind' ); ?>
</button>
<?php else : ?>
<button type="button" id="wpmind-license-activate" class="button button-primary">
<?php esc_html_e( '激活授权', 'wpmind' ); ?>
</button>
<?php endif; ?>
<span id="wpmind-license-spinner" class="spinner" style="float: none;"></span>
<span id="wpmind-license-message" style="margin-left: 8px;"></span>
</p>

<?php wp_nonce_field( 'wpmind_license_action', 'wpmind_license_nonce' ); ?>
</div>

<script>
(function() {
const activateBtn = document.getElementById('wpmind-license-activate');
const deactivateBtn = document.getElementById('wpmind-license-deactivate');
const keyInput = document.getElementById('wpmind-license-key');
const spinner = document.getElementById('wpmind-license-spinner');
const message = document.getElementById('wpmind-license-message');
const nonce = document.getElementById('wpmind_license_nonce').value;

function doAction(action) {
spinner.classList.add('is-active');
message.textContent = '';

const data = new FormData();
data.append('action', 'wpmind_license');
data.append('license_action', action);
data.append('license_key', keyInput.value);
data.append('_wpnonce', nonce);

fetch(ajaxurl, { method: 'POST', body: data })
.then(r => r.json())
.then(res => {
spinner.classList.remove('is-active');
if (res.success) {
message.style.color = '#16a34a';
message.textContent = res.data.message || 'OK';
setTimeout(() => location.reload(), 1000);
} else {
message.style.color = '#dc2626';
message.textContent = res.data.message || 'Error';
}
})
.catch(() => {
spinner.classList.remove('is-active');
message.style.color = '#dc2626';
message.textContent = '<?php esc_html_e( '请求失败', 'wpmind' ); ?>';
});
}

if (activateBtn) activateBtn.addEventListener('click', () => doAction('activate'));
if (deactivateBtn) deactivateBtn.addEventListener('click', () => doAction('deactivate'));
})();
</script>

View file

@ -1,120 +0,0 @@
<?php
/**
* Tests for ApiKeyHasher
*
* @package WPMind\Tests\ApiGateway\Auth
*/

declare(strict_types=1);

namespace WPMind\Tests\ApiGateway\Auth;

require_once __DIR__ . '/../../../modules/api-gateway/includes/Auth/ApiKeyHasher.php';

use WPMind\Modules\ApiGateway\Auth\ApiKeyHasher;
use PHPUnit\Framework\TestCase;

/**
* Test class for ApiKeyHasher cryptographic utilities.
*/
class ApiKeyHasherTest extends TestCase {

/**
* Test make_salt_hex returns a 32-character hex string.
*/
public function test_make_salt_hex_returns_32_char_hex(): void {
$salt = ApiKeyHasher::make_salt_hex();

$this->assertSame( 32, strlen( $salt ) );
$this->assertMatchesRegularExpression( '/^[0-9a-f]{32}$/', $salt );
}

/**
* Test make_salt_hex produces different values on each call.
*/
public function test_make_salt_hex_is_random(): void {
$salt_a = ApiKeyHasher::make_salt_hex();
$salt_b = ApiKeyHasher::make_salt_hex();

$this->assertNotSame( $salt_a, $salt_b );
}

/**
* Test hash_secret returns a 64-character hex string (SHA-256).
*/
public function test_hash_secret_returns_64_char_hex(): void {
$hash = ApiKeyHasher::hash_secret( 'my-secret', 'abcdef0123456789abcdef0123456789' );

$this->assertSame( 64, strlen( $hash ) );
$this->assertMatchesRegularExpression( '/^[0-9a-f]{64}$/', $hash );
}

/**
* Test hash_secret is deterministic: same input produces same output.
*/
public function test_hash_secret_is_deterministic(): void {
$salt = 'abcdef0123456789abcdef0123456789';
$secret = 'my-secret';

$hash_a = ApiKeyHasher::hash_secret( $secret, $salt );
$hash_b = ApiKeyHasher::hash_secret( $secret, $salt );

$this->assertSame( $hash_a, $hash_b );
}

/**
* Test hash_secret produces different hashes with different salts.
*/
public function test_hash_secret_differs_with_different_salt(): void {
$secret = 'my-secret';
$salt_a = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1';
$salt_b = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2';

$hash_a = ApiKeyHasher::hash_secret( $secret, $salt_a );
$hash_b = ApiKeyHasher::hash_secret( $secret, $salt_b );

$this->assertNotSame( $hash_a, $hash_b );
}

/**
* Test constant_time_verify returns true for a correct secret.
*/
public function test_constant_time_verify_returns_true_for_correct_secret(): void {
$secret = 'test-secret-key';
$salt = ApiKeyHasher::make_salt_hex();
$hash = ApiKeyHasher::hash_secret( $secret, $salt );

$this->assertTrue( ApiKeyHasher::constant_time_verify( $secret, $salt, $hash ) );
}

/**
* Test constant_time_verify returns false for a wrong secret.
*/
public function test_constant_time_verify_returns_false_for_wrong_secret(): void {
$salt = ApiKeyHasher::make_salt_hex();
$hash = ApiKeyHasher::hash_secret( 'correct-secret', $salt );

$this->assertFalse( ApiKeyHasher::constant_time_verify( 'wrong-secret', $salt, $hash ) );
}

/**
* Test constant_time_verify returns false against DUMMY_HASH_HEX.
*/
public function test_constant_time_verify_returns_false_against_dummy_hash(): void {
$result = ApiKeyHasher::constant_time_verify(
'any-secret',
ApiKeyHasher::DUMMY_SALT_HEX,
ApiKeyHasher::DUMMY_HASH_HEX
);

$this->assertFalse( $result );
}

/**
* Test dummy constants have the correct lengths.
*/
public function test_dummy_constants_have_correct_length(): void {
$this->assertSame( 32, strlen( ApiKeyHasher::DUMMY_SALT_HEX ) );
$this->assertSame( 64, strlen( ApiKeyHasher::DUMMY_HASH_HEX ) );
}
}

View file

@ -1,115 +0,0 @@
<?php
/**
* Tests for ErrorMapper
*
* @package WPMind\Tests\ApiGateway\Error
*/

declare(strict_types=1);

namespace WPMind\Tests\ApiGateway\Error;

require_once __DIR__ . '/../../../modules/api-gateway/includes/Error/ErrorMapper.php';

use WPMind\Modules\ApiGateway\Error\ErrorMapper;
use PHPUnit\Framework\TestCase;

/**
* Test class for ErrorMapper error code mapping.
*/
class ErrorMapperTest extends TestCase {

/**
* Test map returns 401 for authentication error codes.
*/
public function test_map_returns_correct_status_for_auth_errors(): void {
$auth_codes = [
'missing_auth_header',
'invalid_auth_header',
'invalid_api_key_format',
'api_key_not_found',
'api_key_invalid_secret',
'not_authenticated',
];

foreach ( $auth_codes as $code ) {
$result = ErrorMapper::map( $code );
$this->assertSame( 401, $result['status'], "Expected 401 for code: {$code}" );
}
}

/**
* Test map returns 403 for inactive, expired, and IP-denied keys.
*/
public function test_map_returns_403_for_inactive_and_expired(): void {
$forbidden_codes = [
'api_key_inactive',
'api_key_expired',
'api_key_ip_denied',
];

foreach ( $forbidden_codes as $code ) {
$result = ErrorMapper::map( $code );
$this->assertSame( 403, $result['status'], "Expected 403 for code: {$code}" );
}
}

/**
* Test map returns 429 for rate limit and quota errors.
*/
public function test_map_returns_429_for_rate_limit(): void {
$rate_codes = [
'insufficient_quota',
'rate_limit_exceeded',
];

foreach ( $rate_codes as $code ) {
$result = ErrorMapper::map( $code );
$this->assertSame( 429, $result['status'], "Expected 429 for code: {$code}" );
}
}

/**
* Test map returns 400 for model_not_found.
*/
public function test_map_returns_400_for_model_not_found(): void {
$result = ErrorMapper::map( 'model_not_found' );

$this->assertSame( 400, $result['status'] );
$this->assertSame( 'model_not_found', $result['code'] );
}

/**
* Test map returns 413 for request_too_large.
*/
public function test_map_returns_413_for_request_too_large(): void {
$result = ErrorMapper::map( 'request_too_large' );

$this->assertSame( 413, $result['status'] );
$this->assertSame( 'request_too_large', $result['code'] );
}

/**
* Test map returns 500 for unknown error codes.
*/
public function test_map_returns_500_for_unknown_code(): void {
$result = ErrorMapper::map( 'totally_unknown_error_code' );

$this->assertSame( 500, $result['status'] );
$this->assertSame( 'server_error', $result['type'] );
$this->assertSame( 'internal_error', $result['code'] );
}

/**
* Test format_openai_error returns the correct JSON structure.
*/
public function test_format_openai_error_structure(): void {
$result = ErrorMapper::format_openai_error( 'Something went wrong', 'invalid_request_error', 'invalid_api_key' );

$this->assertArrayHasKey( 'error', $result );
$this->assertSame( 'Something went wrong', $result['error']['message'] );
$this->assertSame( 'invalid_request_error', $result['error']['type'] );
$this->assertNull( $result['error']['param'] );
$this->assertSame( 'invalid_api_key', $result['error']['code'] );
}
}

View file

@ -1,137 +0,0 @@
<?php
/**
* Tests for RateLimiter
*
* @package WPMind\Tests\ApiGateway\RateLimit
*/

declare(strict_types=1);

namespace WPMind\Tests\ApiGateway\RateLimit;

require_once __DIR__ . '/../../../modules/api-gateway/includes/RateLimit/RateStoreResult.php';
require_once __DIR__ . '/../../../modules/api-gateway/includes/RateLimit/RateStoreInterface.php';
require_once __DIR__ . '/../../../modules/api-gateway/includes/RateLimit/RateLimiter.php';

use WPMind\Modules\ApiGateway\RateLimit\RateLimiter;
use WPMind\Modules\ApiGateway\RateLimit\RateStoreInterface;
use WPMind\Modules\ApiGateway\RateLimit\RateStoreResult;
use PHPUnit\Framework\TestCase;

/**
* Test class for RateLimiter orchestration logic.
*/
class RateLimiterTest extends TestCase {

/**
* Test unlimited limits (rpm=0, tpm=0) always return allowed.
*/
public function test_unlimited_returns_allowed(): void {
$primary = $this->createMock( RateStoreInterface::class );
$fallback = $this->createMock( RateStoreInterface::class );

$primary->expects( $this->never() )->method( 'consume' );
$fallback->expects( $this->never() )->method( 'consume' );

$limiter = new RateLimiter( $primary, $fallback );
$result = $limiter->check_and_consume( 'key-1', 'req-1', 0, 0, 100 );

$this->assertTrue( $result->allowed );
}

/**
* Test RPM allowed returns the result from the primary store.
*/
public function test_rpm_allowed_returns_result(): void {
$expected = new RateStoreResult( true, 9, time() + 60 );

$primary = $this->createMock( RateStoreInterface::class );
$primary->method( 'consume' )->willReturn( $expected );

$fallback = $this->createMock( RateStoreInterface::class );

$limiter = new RateLimiter( $primary, $fallback );
$result = $limiter->check_and_consume( 'key-1', 'req-1', 10, 0, 0 );

$this->assertTrue( $result->allowed );
$this->assertSame( 9, $result->remaining );
}

/**
* Test RPM denied returns denied result immediately.
*/
public function test_rpm_denied_returns_denied(): void {
$denied = new RateStoreResult( false, 0, time() + 60 );

$primary = $this->createMock( RateStoreInterface::class );
$primary->method( 'consume' )->willReturn( $denied );

$fallback = $this->createMock( RateStoreInterface::class );

$limiter = new RateLimiter( $primary, $fallback );
$result = $limiter->check_and_consume( 'key-1', 'req-1', 10, 1000, 50 );

$this->assertFalse( $result->allowed );
}

/**
* Test TPM denied triggers RPM rollback.
*/
public function test_tpm_denied_rolls_back_rpm(): void {
$rpm_ok = new RateStoreResult( true, 9, time() + 60 );
$tpm_bad = new RateStoreResult( false, 0, time() + 60 );

$primary = $this->createMock( RateStoreInterface::class );
$primary->method( 'consume' )
->willReturnOnConsecutiveCalls( $rpm_ok, $tpm_bad );

$primary->expects( $this->once() )->method( 'rollback' );

$fallback = $this->createMock( RateStoreInterface::class );

$limiter = new RateLimiter( $primary, $fallback );
$result = $limiter->check_and_consume( 'key-1', 'req-1', 10, 1000, 500 );

$this->assertFalse( $result->allowed );
}

/**
* Test fallback store is used when primary throws an exception.
*/
public function test_fallback_used_when_primary_throws(): void {
$fallback_result = new RateStoreResult( true, 5, time() + 60 );

$primary = $this->createMock( RateStoreInterface::class );
$primary->method( 'consume' )
->willThrowException( new \RuntimeException( 'Redis down' ) );

$fallback = $this->createMock( RateStoreInterface::class );
$fallback->method( 'consume' )->willReturn( $fallback_result );

$limiter = new RateLimiter( $primary, $fallback );
$result = $limiter->check_and_consume( 'key-1', 'req-1', 10, 0, 0 );

$this->assertTrue( $result->allowed );
$this->assertSame( 5, $result->remaining );
}

/**
* Test most restrictive result is returned when both RPM and TPM pass.
*/
public function test_most_restrictive_result_returned(): void {
$rpm_result = new RateStoreResult( true, 8, time() + 60 );
$tpm_result = new RateStoreResult( true, 3, time() + 60 );

$primary = $this->createMock( RateStoreInterface::class );
$primary->method( 'consume' )
->willReturnOnConsecutiveCalls( $rpm_result, $tpm_result );

$fallback = $this->createMock( RateStoreInterface::class );

$limiter = new RateLimiter( $primary, $fallback );
$result = $limiter->check_and_consume( 'key-1', 'req-1', 10, 1000, 100 );

$this->assertTrue( $result->allowed );
$this->assertSame( 3, $result->remaining );
}
}

View file

@ -1,110 +0,0 @@
<?php
/**
* Tests for ModelMapper
*
* @package WPMind\Tests\ApiGateway\Transform
*/

declare(strict_types=1);

// Stub WordPress get_option in the ModelMapper namespace for unit testing.
namespace WPMind\Modules\ApiGateway\Transform {
if ( ! function_exists( 'WPMind\Modules\ApiGateway\Transform\get_option' ) ) {
function get_option( string $key, $default = false ) {
global $wpmind_test_options;
return $wpmind_test_options[ $key ] ?? $default;
}
}
}

namespace WPMind\Tests\ApiGateway\Transform {

require_once __DIR__ . '/../../../modules/api-gateway/includes/Transform/ModelMapper.php';

use WPMind\Modules\ApiGateway\Transform\ModelMapper;
use PHPUnit\Framework\TestCase;

/**
* Test class for ModelMapper model resolution.
*/
class ModelMapperTest extends TestCase {

/**
* Reset global test options before each test.
*/
protected function setUp(): void {
global $wpmind_test_options;
$wpmind_test_options = [];
}

/**
* Test resolve returns correct provider/model for a known model.
*/
public function test_resolve_known_model(): void {
$result = ModelMapper::resolve( 'gpt-4o' );

$this->assertNotNull( $result );
$this->assertSame( 'openai', $result['provider'] );
$this->assertSame( 'gpt-4o', $result['model'] );
}

/**
* Test resolve returns null for an unknown model.
*/
public function test_resolve_unknown_model_returns_null(): void {
$result = ModelMapper::resolve( 'nonexistent-model' );

$this->assertNull( $result );
}

/**
* Test resolve returns auto provider/model for 'auto'.
*/
public function test_resolve_auto_returns_auto(): void {
$result = ModelMapper::resolve( 'auto' );

$this->assertNotNull( $result );
$this->assertSame( 'auto', $result['provider'] );
$this->assertSame( 'auto', $result['model'] );
}

/**
* Test resolve uses alias over default mapping.
*/
public function test_resolve_uses_alias_over_default(): void {
global $wpmind_test_options;
$wpmind_test_options['wpmind_gateway_model_aliases'] = [
'gpt-4o' => [
'provider' => 'custom-provider',
'model' => 'custom-model',
],
];

$result = ModelMapper::resolve( 'gpt-4o' );

$this->assertNotNull( $result );
$this->assertSame( 'custom-provider', $result['provider'] );
$this->assertSame( 'custom-model', $result['model'] );
}

/**
* Test get_available_models includes 'auto'.
*/
public function test_get_available_models_includes_auto(): void {
$models = ModelMapper::get_available_models();

$this->assertContains( 'auto', $models );
}

/**
* Test get_available_models includes default models.
*/
public function test_get_available_models_includes_defaults(): void {
$models = ModelMapper::get_available_models();

$this->assertContains( 'gpt-4o', $models );
$this->assertContains( 'deepseek-chat', $models );
$this->assertContains( 'qwen-max', $models );
}
}
}

View file

@ -1,149 +0,0 @@
<?php
/**
* Tests for ResponseTransformer
*
* @package WPMind\Tests\ApiGateway\Transform
*/

declare(strict_types=1);

namespace WPMind\Tests\ApiGateway\Transform;

require_once __DIR__ . '/../../../modules/api-gateway/includes/Transform/ResponseTransformer.php';

use WPMind\Modules\ApiGateway\Transform\ResponseTransformer;
use PHPUnit\Framework\TestCase;

/**
* Test class for ResponseTransformer format conversion.
*/
class ResponseTransformerTest extends TestCase {

/**
* @var ResponseTransformer
*/
private ResponseTransformer $transformer;

/**
* Set up test fixtures.
*/
protected function setUp(): void {
$this->transformer = new ResponseTransformer();
}

/**
* Test transform_chat with a plain string result.
*/
public function test_transform_chat_with_string_result(): void {
$result = $this->transformer->transform_chat( 'Hello world', 'gpt-4o', 'req-001' );

$this->assertSame( 'Hello world', $result['choices'][0]['message']['content'] );
$this->assertSame( 'assistant', $result['choices'][0]['message']['role'] );
}

/**
* Test transform_chat with array content format.
*/
public function test_transform_chat_with_array_content(): void {
$input = [ 'content' => 'Array content text' ];
$result = $this->transformer->transform_chat( $input, 'gpt-4o', 'req-002' );

$this->assertSame( 'Array content text', $result['choices'][0]['message']['content'] );
}

/**
* Test transform_chat with nested choices format.
*/
public function test_transform_chat_with_choices_format(): void {
$input = [
'choices' => [
[
'message' => [
'content' => 'Choices content text',
],
],
],
];

$result = $this->transformer->transform_chat( $input, 'gpt-4o', 'req-003' );

$this->assertSame( 'Choices content text', $result['choices'][0]['message']['content'] );
}

/**
* Test transform_chat output structure has all required keys.
*/
public function test_transform_chat_structure(): void {
$result = $this->transformer->transform_chat( 'test', 'gpt-4o', 'req-004' );

$this->assertArrayHasKey( 'id', $result );
$this->assertArrayHasKey( 'object', $result );
$this->assertArrayHasKey( 'created', $result );
$this->assertArrayHasKey( 'model', $result );
$this->assertArrayHasKey( 'choices', $result );
$this->assertArrayHasKey( 'usage', $result );

$this->assertSame( 'wpmind-req-004', $result['id'] );
$this->assertSame( 'chat.completion', $result['object'] );
$this->assertSame( 'gpt-4o', $result['model'] );
$this->assertSame( 'stop', $result['choices'][0]['finish_reason'] );
$this->assertSame( 0, $result['choices'][0]['index'] );
}

/**
* Test transform_chat extracts usage data from result.
*/
public function test_transform_chat_usage_extraction(): void {
$input = [
'content' => 'text',
'usage' => [
'prompt_tokens' => 10,
'completion_tokens' => 20,
'total_tokens' => 30,
],
];

$result = $this->transformer->transform_chat( $input, 'gpt-4o', 'req-005' );

$this->assertSame( 10, $result['usage']['prompt_tokens'] );
$this->assertSame( 20, $result['usage']['completion_tokens'] );
$this->assertSame( 30, $result['usage']['total_tokens'] );
}

/**
* Test transform_embedding output structure.
*/
public function test_transform_embedding_structure(): void {
$input = [ [ 0.1, 0.2, 0.3 ] ];
$result = $this->transformer->transform_embedding( $input, 'text-embedding-ada-002', 'req-006' );

$this->assertSame( 'list', $result['object'] );
$this->assertCount( 1, $result['data'] );
$this->assertSame( 'embedding', $result['data'][0]['object'] );
$this->assertSame( 0, $result['data'][0]['index'] );
$this->assertSame( [ 0.1, 0.2, 0.3 ], $result['data'][0]['embedding'] );
}

/**
* Test transform_models output structure.
*/
public function test_transform_models_structure(): void {
$result = $this->transformer->transform_models( [ 'gpt-4o', 'deepseek-chat' ] );

$this->assertSame( 'list', $result['object'] );
$this->assertCount( 2, $result['data'] );
$this->assertSame( 'gpt-4o', $result['data'][0]['id'] );
$this->assertSame( 'model', $result['data'][0]['object'] );
}

/**
* Test transform_models sets owned_by to wpmind.
*/
public function test_transform_models_owned_by_wpmind(): void {
$result = $this->transformer->transform_models( [ 'gpt-4o', 'deepseek-chat', 'auto' ] );

foreach ( $result['data'] as $model ) {
$this->assertSame( 'wpmind', $model['owned_by'], "Model {$model['id']} should be owned by wpmind" );
}
}
}

View file

@ -1,131 +0,0 @@
<?php
/**
* Tests for ChineseOptimizer
*
* @package WPMind\Tests\GEO
*/

namespace WPMind\Tests\GEO;

use WPMind\Modules\Geo\ChineseOptimizer;
use PHPUnit\Framework\TestCase;

/**
* Test class for ChineseOptimizer.
*/
class ChineseOptimizerTest extends TestCase {

/**
* Test Chinese-English spacing.
*/
public function test_adds_space_between_chinese_and_english(): void {
$optimizer = new ChineseOptimizer();
$sections = array( 'content' => '这是WordPress插件' );
$result = $optimizer->optimize( $sections );

$this->assertEquals( '这是 WordPress 插件', $result['content'] );
}

/**
* Test Chinese-number spacing.
*/
public function test_adds_space_between_chinese_and_numbers(): void {
$optimizer = new ChineseOptimizer();
$sections = array( 'content' => '版本号是2.0' );
$result = $optimizer->optimize( $sections );

$this->assertEquals( '版本号是 2.0', $result['content'] );
}

/**
* Test recursive array processing.
*/
public function test_handles_nested_arrays(): void {
$optimizer = new ChineseOptimizer();
$sections = array(
'title' => '标题Title',
'nested' => array(
'content' => '内容Content',
'deep' => array(
'text' => '深层Deep',
),
),
);

$result = $optimizer->optimize( $sections );

$this->assertEquals( '标题 Title', $result['title'] );
$this->assertEquals( '内容 Content', $result['nested']['content'] );
$this->assertEquals( '深层 Deep', $result['nested']['deep']['text'] );
}

/**
* Test non-string values are preserved.
*/
public function test_preserves_non_string_values(): void {
$optimizer = new ChineseOptimizer();
$sections = array(
'count' => 42,
'enabled' => true,
'content' => '测试Test',
);

$result = $optimizer->optimize( $sections );

$this->assertEquals( 42, $result['count'] );
$this->assertTrue( $result['enabled'] );
$this->assertEquals( '测试 Test', $result['content'] );
}

/**
* Test code content is not modified.
*/
public function test_skips_code_content(): void {
$optimizer = new ChineseOptimizer();
$sections = array(
'code' => '```php
function test() {
$var = "value";
}
```',
);

$result = $optimizer->optimize( $sections );

// Code should not be modified.
$this->assertStringContainsString( '$var', $result['code'] );
}

/**
* Test punctuation normalization when enabled.
*/
public function test_punctuation_normalization(): void {
$optimizer = new ChineseOptimizer( true ); // Enable punctuation normalization.
$sections = array( 'content' => '你好,世界!' );
$result = $optimizer->optimize( $sections );

$this->assertEquals( '你好, 世界! ', $result['content'] );
}

/**
* Test empty string handling.
*/
public function test_handles_empty_strings(): void {
$optimizer = new ChineseOptimizer();
$sections = array( 'content' => '' );
$result = $optimizer->optimize( $sections );

$this->assertEquals( '', $result['content'] );
}

/**
* Test pure English text is not modified.
*/
public function test_pure_english_unchanged(): void {
$optimizer = new ChineseOptimizer();
$sections = array( 'content' => 'Hello World 123' );
$result = $optimizer->optimize( $sections );

$this->assertEquals( 'Hello World 123', $result['content'] );
}
}

View file

@ -1,121 +0,0 @@
<?php
/**
* Tests for GeoSignalInjector
*
* @package WPMind\Tests\GEO
*/

namespace WPMind\Tests\GEO;

use WPMind\Modules\Geo\GeoSignalInjector;
use PHPUnit\Framework\TestCase;

/**
* Test class for GeoSignalInjector.
*
* Note: These tests require WordPress test framework for full functionality.
* Basic structure tests can run without WordPress.
*/
class GeoSignalInjectorTest extends TestCase {

/**
* Test inject preserves associative array keys.
*/
public function test_inject_preserves_array_keys(): void {
// Skip if WordPress functions not available.
if ( ! function_exists( 'get_the_author_meta' ) ) {
$this->markTestSkipped( 'WordPress functions not available.' );
}

$injector = new GeoSignalInjector();
$sections = array(
'title' => '# Test Title',
'content' => 'Test content here.',
);

// Create mock post.
$post = $this->createMock( \WP_Post::class );
$post->ID = 1;
$post->post_author = 1;

$result = $injector->inject( $sections, $post );

// Check that original keys are preserved.
$this->assertArrayHasKey( 'title', $result );
$this->assertArrayHasKey( 'content', $result );

// Check that new keys are added.
$this->assertArrayHasKey( 'wpmind_authority', $result );
$this->assertArrayHasKey( 'wpmind_citation', $result );
}

/**
* Test authority signal format.
*/
public function test_authority_signal_contains_required_fields(): void {
if ( ! function_exists( 'get_the_author_meta' ) ) {
$this->markTestSkipped( 'WordPress functions not available.' );
}

$injector = new GeoSignalInjector();
$sections = array( 'content' => 'Test' );

$post = $this->createMock( \WP_Post::class );
$post->ID = 1;
$post->post_author = 1;

$result = $injector->inject( $sections, $post );

// Authority signal should contain YAML front matter.
$this->assertStringContainsString( '---', $result['wpmind_authority'] );
$this->assertStringContainsString( '作者:', $result['wpmind_authority'] );
$this->assertStringContainsString( '发布日期:', $result['wpmind_authority'] );
$this->assertStringContainsString( '最后更新:', $result['wpmind_authority'] );
}

/**
* Test citation format.
*/
public function test_citation_contains_required_elements(): void {
if ( ! function_exists( 'get_the_title' ) ) {
$this->markTestSkipped( 'WordPress functions not available.' );
}

$injector = new GeoSignalInjector();
$sections = array( 'content' => 'Test' );

$post = $this->createMock( \WP_Post::class );
$post->ID = 1;
$post->post_author = 1;

$result = $injector->inject( $sections, $post );

// Citation should contain required elements.
$this->assertStringContainsString( '引用本文', $result['wpmind_citation'] );
$this->assertStringContainsString( 'APA', $result['wpmind_citation'] );
}

/**
* Test structured data generation.
*/
public function test_get_structured_data_returns_valid_schema(): void {
if ( ! function_exists( 'get_the_title' ) ) {
$this->markTestSkipped( 'WordPress functions not available.' );
}

$injector = new GeoSignalInjector();

$post = $this->createMock( \WP_Post::class );
$post->ID = 1;
$post->post_author = 1;

$data = $injector->get_structured_data( $post );

// Check required Schema.org fields.
$this->assertEquals( 'Article', $data['@type'] );
$this->assertArrayHasKey( 'headline', $data );
$this->assertArrayHasKey( 'author', $data );
$this->assertArrayHasKey( 'datePublished', $data );
$this->assertArrayHasKey( 'publisher', $data );
}
}

View file

@ -1,196 +0,0 @@
<?php
/**
* Tests for HtmlToMarkdown
*
* @package WPMind\Tests\GEO
*/

namespace WPMind\Tests\GEO;

use WPMind\Modules\Geo\HtmlToMarkdown;
use PHPUnit\Framework\TestCase;

/**
* Test class for HtmlToMarkdown converter.
*/
class HtmlToMarkdownTest extends TestCase {

/**
* @var HtmlToMarkdown
*/
private HtmlToMarkdown $converter;

/**
* Set up test fixtures.
*/
protected function setUp(): void {
$this->converter = new HtmlToMarkdown();
}

/**
* Test heading conversion.
*/
public function test_converts_headings(): void {
$html = '<h1>Title</h1><h2>Subtitle</h2><h3>Section</h3>';
$md = $this->converter->convert( $html );

$this->assertStringContainsString( '# Title', $md );
$this->assertStringContainsString( '## Subtitle', $md );
$this->assertStringContainsString( '### Section', $md );
}

/**
* Test link conversion.
*/
public function test_converts_links(): void {
$html = '<a href="https://example.com">Example</a>';
$md = $this->converter->convert( $html );

$this->assertStringContainsString( '[Example](https://example.com)', $md );
}

/**
* Test image conversion.
*/
public function test_converts_images(): void {
$html = '<img src="image.jpg" alt="Test Image">';
$md = $this->converter->convert( $html );

$this->assertStringContainsString( '![Test Image](image.jpg)', $md );
}

/**
* Test image without alt text.
*/
public function test_converts_images_without_alt(): void {
$html = '<img src="image.jpg">';
$md = $this->converter->convert( $html );

$this->assertStringContainsString( '![](image.jpg)', $md );
}

/**
* Test unordered list conversion.
*/
public function test_converts_unordered_lists(): void {
$html = '<ul><li>Item 1</li><li>Item 2</li></ul>';
$md = $this->converter->convert( $html );

$this->assertStringContainsString( '- Item 1', $md );
$this->assertStringContainsString( '- Item 2', $md );
}

/**
* Test code block conversion.
*/
public function test_converts_code_blocks(): void {
$html = '<pre><code class="language-php">echo "Hello";</code></pre>';
$md = $this->converter->convert( $html );

$this->assertStringContainsString( '```php', $md );
$this->assertStringContainsString( 'echo "Hello";', $md );
$this->assertStringContainsString( '```', $md );
}

/**
* Test inline code conversion.
*/
public function test_converts_inline_code(): void {
$html = 'Use <code>$variable</code> here.';
$md = $this->converter->convert( $html );

$this->assertStringContainsString( '`$variable`', $md );
}

/**
* Test blockquote conversion.
*/
public function test_converts_blockquotes(): void {
$html = '<blockquote>This is a quote.</blockquote>';
$md = $this->converter->convert( $html );

$this->assertStringContainsString( '> This is a quote.', $md );
}

/**
* Test bold text conversion.
*/
public function test_converts_bold(): void {
$html = '<strong>Bold text</strong>';
$md = $this->converter->convert( $html );

$this->assertStringContainsString( '**Bold text**', $md );
}

/**
* Test italic text conversion.
*/
public function test_converts_italic(): void {
$html = '<em>Italic text</em>';
$md = $this->converter->convert( $html );

$this->assertStringContainsString( '*Italic text*', $md );
}

/**
* Test strikethrough conversion.
*/
public function test_converts_strikethrough(): void {
$html = '<del>Deleted text</del>';
$md = $this->converter->convert( $html );

$this->assertStringContainsString( '~~Deleted text~~', $md );
}

/**
* Test paragraph conversion.
*/
public function test_converts_paragraphs(): void {
$html = '<p>First paragraph.</p><p>Second paragraph.</p>';
$md = $this->converter->convert( $html );

$this->assertStringContainsString( 'First paragraph.', $md );
$this->assertStringContainsString( 'Second paragraph.', $md );
}

/**
* Test WordPress block comment removal.
*/
public function test_removes_wordpress_block_comments(): void {
$html = '<!-- wp:paragraph --><p>Content</p><!-- /wp:paragraph -->';
$md = $this->converter->convert( $html );

$this->assertStringNotContainsString( 'wp:paragraph', $md );
$this->assertStringContainsString( 'Content', $md );
}

/**
* Test HTML entity decoding.
*/
public function test_decodes_html_entities(): void {
$html = '<p>&amp; &lt; &gt; &quot;</p>';
$md = $this->converter->convert( $html );

$this->assertStringContainsString( '&', $md );
$this->assertStringContainsString( '<', $md );
$this->assertStringContainsString( '>', $md );
}

/**
* Test empty input.
*/
public function test_handles_empty_input(): void {
$md = $this->converter->convert( '' );
$this->assertEquals( '', $md );
}

/**
* Test complex nested HTML.
*/
public function test_handles_nested_html(): void {
$html = '<p>Text with <strong>bold and <em>italic</em></strong> content.</p>';
$md = $this->converter->convert( $html );

$this->assertStringContainsString( '**bold and *italic***', $md );
}
}

View file

@ -1,231 +0,0 @@
<?php
/**
* WPMind 公共 API 测试 - AJAX 测试端点
*
* 通过访问 /wp-admin/admin-ajax.php?action=wpmind_test_api 触发测试
*
* @package WPMind
* @since 2.5.0
*/

// 确保在 WordPress 环境中运行
if (!defined('ABSPATH')) {
exit;
}

/**
* 注册 AJAX 测试端点
*
* 安全说明:
* - wp_ajax_ 端点仅限已登录用户
* - 需要管理员权限和 nonce 验证
*/
add_action('wp_ajax_wpmind_test_api', 'wpmind_run_api_test');

function wpmind_run_api_test() {
// Nonce 验证
check_ajax_referer('wpmind_ajax', 'nonce');

// 权限检查:必须是管理员
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => '权限不足']);
}

header('Content-Type: application/json; charset=utf-8');
$results = [];

// 测试 1: wpmind_is_available()
$results['is_available'] = [
'name' => 'wpmind_is_available()',
'exists' => function_exists('wpmind_is_available'),
'result' => function_exists('wpmind_is_available') ? wpmind_is_available() : null,
];

// 测试 2: wpmind_get_status()
$results['get_status'] = [
'name' => 'wpmind_get_status()',
'exists' => function_exists('wpmind_get_status'),
'result' => function_exists('wpmind_get_status') ? wpmind_get_status() : null,
];

// 测试 3: wpmind_chat() - 简单模式
if (function_exists('wpmind_chat')) {
$start = microtime(true);
$chat_result = wpmind_chat('你好请用一句话回答1+1=?', [
'context' => 'api_test',
'max_tokens' => 50,
]);
$duration = round((microtime(true) - $start) * 1000);
$results['chat_simple'] = [
'name' => 'wpmind_chat() - 简单模式',
'exists' => true,
'success' => !is_wp_error($chat_result),
'duration_ms' => $duration,
'result' => is_wp_error($chat_result) ? [
'error' => $chat_result->get_error_message(),
] : $chat_result,
];
} else {
$results['chat_simple'] = [
'name' => 'wpmind_chat() - 简单模式',
'exists' => false,
];
}

// 测试 4: wpmind_translate()
if (function_exists('wpmind_translate')) {
$start = microtime(true);
$translate_result = wpmind_translate('你好世界', 'zh', 'en', [
'context' => 'api_test',
'cache_ttl' => 0,
]);
$duration = round((microtime(true) - $start) * 1000);
$results['translate'] = [
'name' => 'wpmind_translate()',
'exists' => true,
'success' => !is_wp_error($translate_result),
'duration_ms' => $duration,
'result' => is_wp_error($translate_result) ? [
'error' => $translate_result->get_error_message(),
] : $translate_result,
];
} else {
$results['translate'] = [
'name' => 'wpmind_translate()',
'exists' => false,
];
}

// ============================================
// v2.6.0 增强 API 测试
// ============================================

// 测试 5: wpmind_count_tokens()
if (function_exists('wpmind_count_tokens')) {
$test_text = '这是一段测试文本,用于测试 token 计数功能。This is a test.';
$tokens = wpmind_count_tokens($test_text);
$results['count_tokens'] = [
'name' => 'wpmind_count_tokens()',
'exists' => true,
'success' => $tokens > 0,
'result' => [
'text_length' => mb_strlen($test_text),
'estimated_tokens' => $tokens,
],
];
}

// 测试 6: wpmind_structured()
if (function_exists('wpmind_structured')) {
$start = microtime(true);
$structured_result = wpmind_structured('北京是中国首都人口超过2000万', [
'type' => 'object',
'required' => ['city', 'country'],
'properties' => [
'city' => ['type' => 'string'],
'country' => ['type' => 'string'],
'population' => ['type' => 'string'],
],
], [
'retries' => 2,
]);
$duration = round((microtime(true) - $start) * 1000);
$results['structured'] = [
'name' => 'wpmind_structured()',
'exists' => true,
'success' => !is_wp_error($structured_result),
'duration_ms' => $duration,
'result' => is_wp_error($structured_result) ? [
'error' => $structured_result->get_error_message(),
] : $structured_result,
];
}

// 测试 7: wpmind_batch() - 只测试 2 个项目
if (function_exists('wpmind_batch')) {
$start = microtime(true);
$batch_result = wpmind_batch(
['苹果', '香蕉'],
'用英文回答:{{item}} 的英文是什么?只回答单词。',
[
'max_tokens' => 20,
'delay_ms' => 50,
]
);
$duration = round((microtime(true) - $start) * 1000);
$results['batch'] = [
'name' => 'wpmind_batch()',
'exists' => true,
'success' => !is_wp_error($batch_result) && ($batch_result['success_count'] ?? 0) > 0,
'duration_ms' => $duration,
'result' => is_wp_error($batch_result) ? [
'error' => $batch_result->get_error_message(),
] : [
'success_count' => $batch_result['success_count'],
'error_count' => $batch_result['error_count'],
'total_tokens' => $batch_result['total_tokens'],
],
];
}

// ============================================
// v2.7.0 专用 API 测试
// ============================================

// 测试 8: wpmind_summarize()
if (function_exists('wpmind_summarize')) {
$start = microtime(true);
$long_text = '人工智能Artificial Intelligence简称AI是计算机科学的一个分支它企图了解智能的实质并生产出一种新的能以人类智能相似的方式做出反应的智能机器。人工智能从诞生以来理论和技术日益成熟应用领域也在不断扩大。';
$summarize_result = wpmind_summarize($long_text, [
'style' => 'title',
'cache_ttl' => 0,
]);
$duration = round((microtime(true) - $start) * 1000);
$results['summarize'] = [
'name' => 'wpmind_summarize()',
'exists' => true,
'success' => !is_wp_error($summarize_result),
'duration_ms' => $duration,
'result' => is_wp_error($summarize_result) ? [
'error' => $summarize_result->get_error_message(),
] : $summarize_result,
];
}

// 测试 9: wpmind_moderate()
if (function_exists('wpmind_moderate')) {
$start = microtime(true);
$moderate_result = wpmind_moderate('这是一段正常的文本内容,用于测试内容审核功能。', [
'categories' => ['spam', 'adult'],
'cache_ttl' => 0,
]);
$duration = round((microtime(true) - $start) * 1000);
$results['moderate'] = [
'name' => 'wpmind_moderate()',
'exists' => true,
'success' => !is_wp_error($moderate_result),
'duration_ms' => $duration,
'result' => is_wp_error($moderate_result) ? [
'error' => $moderate_result->get_error_message(),
] : [
'safe' => $moderate_result['safe'],
'summary' => $moderate_result['summary'] ?? '',
],
];
}

wp_send_json_success([
'message' => 'WPMind API 测试完成 (v2.5.0 + v2.6.0 + v2.7.0)',
'version' => '2.7.0',
'tests' => $results,
]);
}

View file

@ -1,315 +0,0 @@
<?php
/**
* WPMind API 集成测试
*
* 在 WordPress CLI 中运行: wp eval-file tests/integration-test.php
*
* @package WPMind
* @since 2.5.0
*/

// 确保在 WordPress 环境中运行
if (!defined('ABSPATH')) {
echo "请在 WordPress 环境中运行此测试\n";
echo "使用: wp eval-file tests/integration-test.php\n";
exit(1);
}

// 测试结果统计
$tests_passed = 0;
$tests_failed = 0;
$test_results = [];

/**
* 断言函数
*/
function assert_true($condition, $test_name) {
global $tests_passed, $tests_failed, $test_results;
if ($condition) {
$tests_passed++;
$test_results[] = ['name' => $test_name, 'status' => 'PASS', 'message' => ''];
echo "✅ $test_name\n";
return true;
} else {
$tests_failed++;
$test_results[] = ['name' => $test_name, 'status' => 'FAIL', 'message' => 'Condition is false'];
echo "❌ $test_name\n";
return false;
}
}

function assert_equals($expected, $actual, $test_name) {
global $tests_passed, $tests_failed, $test_results;
if ($expected === $actual) {
$tests_passed++;
$test_results[] = ['name' => $test_name, 'status' => 'PASS', 'message' => ''];
echo "✅ $test_name\n";
return true;
} else {
$tests_failed++;
$message = "Expected: " . print_r($expected, true) . ", Got: " . print_r($actual, true);
$test_results[] = ['name' => $test_name, 'status' => 'FAIL', 'message' => $message];
echo "❌ $test_name\n";
echo " Expected: " . print_r($expected, true) . "\n";
echo " Got: " . print_r($actual, true) . "\n";
return false;
}
}

function assert_is_wp_error($value, $test_name) {
global $tests_passed, $tests_failed, $test_results;
if (is_wp_error($value)) {
$tests_passed++;
$test_results[] = ['name' => $test_name, 'status' => 'PASS', 'message' => ''];
echo "✅ $test_name\n";
return true;
} else {
$tests_failed++;
$test_results[] = ['name' => $test_name, 'status' => 'FAIL', 'message' => 'Expected WP_Error'];
echo "❌ $test_name\n";
return false;
}
}

function assert_not_wp_error($value, $test_name) {
global $tests_passed, $tests_failed, $test_results;
if (!is_wp_error($value)) {
$tests_passed++;
$test_results[] = ['name' => $test_name, 'status' => 'PASS', 'message' => ''];
echo "✅ $test_name\n";
return true;
} else {
$tests_failed++;
$message = 'Got WP_Error: ' . $value->get_error_message();
$test_results[] = ['name' => $test_name, 'status' => 'FAIL', 'message' => $message];
echo "❌ $test_name: " . $value->get_error_message() . "\n";
return false;
}
}

/**
* 测试套件
*/

echo "\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
echo " WPMind API 集成测试\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n";

// ==================
// 1. 基础可用性测试
// ==================
echo "1. 基础可用性测试\n";
echo "─────────────────────────────────────\n";

assert_true(function_exists('wpmind_is_available'), '函数 wpmind_is_available 存在');
assert_true(function_exists('wpmind_chat'), '函数 wpmind_chat 存在');
assert_true(function_exists('wpmind_translate'), '函数 wpmind_translate 存在');
assert_true(function_exists('wpmind_pinyin'), '函数 wpmind_pinyin 存在');
assert_true(function_exists('wpmind_get_status'), '函数 wpmind_get_status 存在');

$is_available = wpmind_is_available();
assert_true(is_bool($is_available), 'wpmind_is_available 返回布尔值');

echo "\n";

// ==================
// 2. ErrorHandler 测试
// ==================
echo "2. ErrorHandler 测试\n";
echo "─────────────────────────────────────\n";

use WPMind\API\ErrorHandler;

// 测试错误创建
$error = ErrorHandler::create(ErrorHandler::ERROR_NOT_AVAILABLE);
assert_is_wp_error($error, 'ErrorHandler::create 返回 WP_Error');
assert_equals('wpmind_not_available', $error->get_error_code(), '错误代码正确');

// 测试快捷方法
$error = ErrorHandler::not_available();
assert_equals('wpmind_not_available', $error->get_error_code(), 'not_available 错误代码正确');

$error = ErrorHandler::recursive_call('chat', 'test123');
assert_equals('wpmind_recursive_call', $error->get_error_code(), 'recursive_call 错误代码正确');

$error = ErrorHandler::call_depth_exceeded('chat', 3, 3);
assert_equals('wpmind_call_depth_exceeded', $error->get_error_code(), 'call_depth_exceeded 错误代码正确');

// 测试可重试检查
$timeout_error = ErrorHandler::create(ErrorHandler::ERROR_API_TIMEOUT);
assert_true(ErrorHandler::is_retryable($timeout_error), '超时错误应该可重试');

$auth_error = ErrorHandler::create(ErrorHandler::ERROR_API_AUTH);
assert_true(!ErrorHandler::is_retryable($auth_error), '认证错误不应该可重试');

echo "\n";

// ==================
// 3. 循环调用保护测试
// ==================
echo "3. 循环调用保护测试\n";
echo "─────────────────────────────────────\n";

// 如果 WPMind 可用,进行 API 测试
if (wpmind_is_available()) {
// 正常调用应该成功
$result = wpmind_chat('Say "test passed"', [
'max_tokens' => 10,
'cache_ttl' => 0,
]);
assert_not_wp_error($result, '正常 chat 调用成功');
// 模拟循环调用场景(通过 filter
$recursive_test_passed = true;
add_filter('wpmind_chat_response', function($response, $messages, $context) use (&$recursive_test_passed) {
if ($context === 'recursive_test') {
// 在响应过滤器中再次调用 chat应该被保护
$nested_result = wpmind_chat('Nested call', [
'context' => 'recursive_test', // 相同 context
'max_tokens' => 10,
]);
// 如果返回 WP_Error 且是循环调用错误,测试通过
if (!is_wp_error($nested_result)) {
$recursive_test_passed = false;
}
}
return $response;
}, 10, 3);
// 触发可能的循环调用
$result = wpmind_chat('Test recursive protection', [
'context' => 'recursive_test',
'max_tokens' => 10,
'cache_ttl' => 0,
]);
echo " (循环保护测试完成)\n";
} else {
echo " ⚠️ WPMind 未配置,跳过 API 调用测试\n";
}

echo "\n";

// ==================
// 4. 翻译 API 测试
// ==================
echo "4. 翻译 API 测试\n";
echo "─────────────────────────────────────\n";

if (wpmind_is_available()) {
// 测试基本翻译
$result = wpmind_translate('你好', 'zh', 'en', ['cache_ttl' => 0]);
assert_not_wp_error($result, 'translate 调用成功');
if (!is_wp_error($result)) {
assert_true(is_string($result), 'translate 返回字符串');
assert_true(strlen($result) > 0, 'translate 结果不为空');
echo " 翻译结果: $result\n";
}
// 测试 slug 格式
$result = wpmind_translate('你好世界', 'zh', 'en', [
'format' => 'slug',
'cache_ttl' => 0,
]);
assert_not_wp_error($result, 'translate(format=slug) 调用成功');
if (!is_wp_error($result)) {
// slug 应该只包含小写字母、数字和连字符
assert_true(preg_match('/^[a-z0-9\-]+$/', $result) === 1, 'Slug 格式正确');
echo " Slug 结果: $result\n";
}
} else {
echo " ⚠️ WPMind 未配置,跳过翻译测试\n";
}

echo "\n";

// ==================
// 5. 语义化拼音测试
// ==================
echo "5. 语义化拼音测试\n";
echo "─────────────────────────────────────\n";

if (wpmind_is_available()) {
$result = wpmind_pinyin('你好世界', ['cache_ttl' => 0]);
assert_not_wp_error($result, 'pinyin 调用成功');
if (!is_wp_error($result)) {
assert_true(is_string($result), 'pinyin 返回字符串');
assert_true(strlen($result) > 0, 'pinyin 结果不为空');
echo " 拼音结果: $result\n";
// 检查是否按词分隔(不应该是 ni-hao-shi-jie 这样按字分隔)
$words = explode('-', $result);
if (count($words) < 4) {
echo " ✓ 按词分隔($result不是按字分隔\n";
}
}
} else {
echo " ⚠️ WPMind 未配置,跳过拼音测试\n";
}

echo "\n";

// ==================
// 6. 缓存测试
// ==================
echo "6. 缓存测试\n";
echo "─────────────────────────────────────\n";

if (wpmind_is_available()) {
$test_text = '缓存测试' . time(); // 唯一文本避免命中现有缓存
// 第一次调用(无缓存)
$start = microtime(true);
$result1 = wpmind_translate($test_text, 'zh', 'en', ['cache_ttl' => 3600]);
$time1 = round((microtime(true) - $start) * 1000);
// 第二次调用(应该命中缓存)
$start = microtime(true);
$result2 = wpmind_translate($test_text, 'zh', 'en', ['cache_ttl' => 3600]);
$time2 = round((microtime(true) - $start) * 1000);
echo " 第一次调用: {$time1}ms\n";
echo " 第二次调用: {$time2}ms\n";
assert_equals($result1, $result2, '缓存结果一致');
// 缓存应该显著快于首次调用
if ($time1 > 100 && $time2 < $time1 / 2) {
echo " ✓ 缓存生效(第二次调用快 " . round(($time1 - $time2) / $time1 * 100) . "%\n";
}
// 清理测试缓存
delete_transient('wpmind_translate_' . md5(serialize([
'text' => $test_text,
'from' => 'zh',
'to' => 'en',
'options' => ['context' => 'translation', 'format' => 'text', 'hint' => '', 'cache_ttl' => 3600]
])));
} else {
echo " ⚠️ WPMind 未配置,跳过缓存测试\n";
}

echo "\n";

// ==================
// 测试总结
// ==================
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
echo " 测试结果\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
echo " 通过: $tests_passed\n";
echo " 失败: $tests_failed\n";
echo " 总计: " . ($tests_passed + $tests_failed) . "\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n";

// 返回退出码
exit($tests_failed > 0 ? 1 : 0);

View file

@ -1,379 +0,0 @@
#!/usr/bin/env php
<?php
/**
* API Gateway 集成测试脚本
*
* 验证 API Gateway 模块的文件完整性、语法、命名空间、接口实现和安全性。
*
* Usage: php tests/test-api-gateway.php
*
* @package WPMind
* @since 3.6.0
*/

declare(strict_types=1);

error_reporting(E_ALL);
ini_set('display_errors', '1');

// 自动检测插件路径
$plugin_path = dirname(__DIR__) . '/';
if (!file_exists($plugin_path . 'wpmind.php')) {
$plugin_path = '/www/wwwroot/wpcy.com/wp-content/plugins/wpmind/';
}

$module_path = $plugin_path . 'modules/api-gateway/';

echo "=== API Gateway 集成测试 ===\n";
echo "模块路径: $module_path\n\n";

$errors = [];
$warnings = [];

// ---------------------------------------------------------------------------
// 1. 文件存在性检查
// ---------------------------------------------------------------------------
echo "--- 1. 文件存在性检查 ---\n";

$required_files = [
'ApiGatewayModule.php',
'module.json',
'includes/Admin/GatewayAjaxController.php',
'includes/Auth/ApiKeyAuthResult.php',
'includes/Auth/ApiKeyHasher.php',
'includes/Auth/ApiKeyManager.php',
'includes/Auth/ApiKeyRepository.php',
'includes/Error/ErrorMapper.php',
'includes/Pipeline/AuthMiddleware.php',
'includes/Pipeline/BudgetMiddleware.php',
'includes/Pipeline/ErrorMiddleware.php',
'includes/Pipeline/GatewayPipeline.php',
'includes/Pipeline/GatewayRequestContext.php',
'includes/Pipeline/GatewayStageInterface.php',
'includes/Pipeline/LogMiddleware.php',
'includes/Pipeline/QuotaMiddleware.php',
'includes/Pipeline/RequestTransformMiddleware.php',
'includes/Pipeline/ResponseTransformMiddleware.php',
'includes/Pipeline/RouteMiddleware.php',
'includes/RateLimit/RateLimiter.php',
'includes/RateLimit/RateStoreInterface.php',
'includes/RateLimit/RateStoreResult.php',
'includes/RateLimit/RedisRateStore.php',
'includes/RateLimit/TransientRateStore.php',
'includes/Stream/CancellationToken.php',
'includes/Stream/SseConcurrencyGuard.php',
'includes/Stream/SseSlot.php',
'includes/Stream/SseStreamController.php',
'includes/Stream/StreamResult.php',
'includes/Stream/UpstreamStreamClient.php',
'includes/Transform/ModelMapper.php',
'includes/Transform/RequestTransformer.php',
'includes/Transform/ResponseTransformer.php',
'includes/GatewayRequestSchema.php',
'includes/RestController.php',
'includes/SchemaManager.php',
'templates/settings.php',
];

$file_count = count($required_files);
$found = 0;

foreach ($required_files as $file) {
$full_path = $module_path . $file;
if (file_exists($full_path)) {
echo " ✅ $file\n";
$found++;
} else {
echo " ❌ $file (不存在)\n";
$errors[] = "文件不存在: $file";
}
}

echo " 文件统计: $found / $file_count\n";

// ---------------------------------------------------------------------------
// 2. PHP 语法检查
// ---------------------------------------------------------------------------
echo "\n--- 2. PHP 语法检查 ---\n";

$php_files = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($module_path, RecursiveDirectoryIterator::SKIP_DOTS)
);

foreach ($iterator as $file_info) {
if ($file_info->getExtension() === 'php') {
$php_files[] = $file_info->getPathname();
}
}

sort($php_files);
$syntax_errors = 0;

foreach ($php_files as $file) {
$output = [];
$return_var = 0;
exec("php -l " . escapeshellarg($file) . " 2>&1", $output, $return_var);
if ($return_var !== 0) {
$relative = str_replace($module_path, '', $file);
echo " ❌ $relative: " . implode("\n", $output) . "\n";
$errors[] = "语法错误: $relative";
$syntax_errors++;
}
}

if ($syntax_errors === 0) {
echo " ✅ 所有 " . count($php_files) . " 个 PHP 文件语法正确\n";
}

// ---------------------------------------------------------------------------
// 3. 命名空间一致性检查
// ---------------------------------------------------------------------------
echo "\n--- 3. 命名空间一致性检查 ---\n";

$namespace_errors = 0;

foreach ($php_files as $file) {
$relative = str_replace($module_path, '', $file);

// 跳过 templates/ 目录
if (str_starts_with($relative, 'templates/')) {
continue;
}

$content = file_get_contents($file);
if (preg_match('/^namespace\s+(.+?);/m', $content, $matches)) {
$ns = $matches[1];
if (!str_starts_with($ns, 'WPMind\\Modules\\ApiGateway')) {
echo " ❌ $relative: 命名空间 '$ns' 不以 WPMind\\Modules\\ApiGateway 开头\n";
$errors[] = "命名空间错误: $relative ($ns)";
$namespace_errors++;
}
} else {
echo " ❌ $relative: 未声明命名空间\n";
$errors[] = "缺少命名空间: $relative";
$namespace_errors++;
}
}

if ($namespace_errors === 0) {
echo " ✅ 所有非模板 PHP 文件命名空间正确\n";
}

// ---------------------------------------------------------------------------
// 4. 接口实现检查
// ---------------------------------------------------------------------------
echo "\n--- 4. 接口实现检查 ---\n";

/**
* 检查文件内容是否包含指定字符串
*/
function file_contains(string $path, string $needle): bool {
if (!file_exists($path)) {
return false;
}
return str_contains(file_get_contents($path), $needle);
}

// ApiGatewayModule implements ModuleInterface
$module_file = $module_path . 'ApiGatewayModule.php';
if (file_contains($module_file, 'implements ModuleInterface')) {
echo " ✅ ApiGatewayModule implements ModuleInterface\n";
} else {
echo " ❌ ApiGatewayModule 未实现 ModuleInterface\n";
$errors[] = "ApiGatewayModule 未实现 ModuleInterface";
}

// Pipeline middleware files implement GatewayStageInterface
$middleware_files = [
'AuthMiddleware',
'BudgetMiddleware',
'ErrorMiddleware',
'LogMiddleware',
'QuotaMiddleware',
'RequestTransformMiddleware',
'ResponseTransformMiddleware',
'RouteMiddleware',
];

foreach ($middleware_files as $mw) {
$mw_path = $module_path . "includes/Pipeline/$mw.php";
if (file_contains($mw_path, 'implements GatewayStageInterface')) {
echo " ✅ $mw implements GatewayStageInterface\n";
} else {
echo " ❌ $mw 未实现 GatewayStageInterface\n";
$errors[] = "$mw 未实现 GatewayStageInterface";
}
}

// RateStoreInterface is declared as interface
$rsi_path = $module_path . 'includes/RateLimit/RateStoreInterface.php';
if (file_contains($rsi_path, 'interface RateStoreInterface')) {
echo " ✅ RateStoreInterface 声明为 interface\n";
} else {
echo " ❌ RateStoreInterface 未声明为 interface\n";
$errors[] = "RateStoreInterface 未声明为 interface";
}

// RedisRateStore and TransientRateStore implement RateStoreInterface
foreach (['RedisRateStore', 'TransientRateStore'] as $store) {
$store_path = $module_path . "includes/RateLimit/$store.php";
if (file_contains($store_path, 'implements RateStoreInterface')) {
echo " ✅ $store implements RateStoreInterface\n";
} else {
echo " ❌ $store 未实现 RateStoreInterface\n";
$errors[] = "$store 未实现 RateStoreInterface";
}
}

// ---------------------------------------------------------------------------
// 5. 安全检查
// ---------------------------------------------------------------------------
echo "\n--- 5. 安全检查 ---\n";

$ajax_file = $module_path . 'includes/Admin/GatewayAjaxController.php';
$hasher_file = $module_path . 'includes/Auth/ApiKeyHasher.php';

// check_ajax_referer
if (file_contains($ajax_file, 'check_ajax_referer')) {
echo " ✅ GatewayAjaxController 包含 check_ajax_referer (CSRF 防护)\n";
} else {
echo " ❌ GatewayAjaxController 缺少 check_ajax_referer\n";
$errors[] = "安全: GatewayAjaxController 缺少 CSRF 防护";
}

// current_user_can
if (file_contains($ajax_file, 'current_user_can')) {
echo " ✅ GatewayAjaxController 包含 current_user_can (权限检查)\n";
} else {
echo " ❌ GatewayAjaxController 缺少 current_user_can\n";
$errors[] = "安全: GatewayAjaxController 缺少权限检查";
}

// hash_equals (constant-time comparison)
if (file_contains($hasher_file, 'hash_equals')) {
echo " ✅ ApiKeyHasher 包含 hash_equals (常量时间比较)\n";
} else {
echo " ❌ ApiKeyHasher 缺少 hash_equals\n";
$errors[] = "安全: ApiKeyHasher 缺少常量时间比较";
}

// ---------------------------------------------------------------------------
// 6. REST 端点注册检查
// ---------------------------------------------------------------------------
echo "\n--- 6. REST 端点注册检查 ---\n";

$rest_file = $module_path . 'includes/RestController.php';
$rest_content = file_exists($rest_file) ? file_get_contents($rest_file) : '';

$expected_routes = [
'chat/completions' => "'/chat/completions'",
'embeddings' => "'/embeddings'",
'responses' => "'/responses'",
'models' => "'/models'",
'status' => "'/status'",
];

foreach ($expected_routes as $label => $pattern) {
if (str_contains($rest_content, $pattern)) {
echo " ✅ 路由已注册: $label\n";
} else {
echo " ❌ 路由未注册: $label\n";
$errors[] = "REST 路由缺失: $label";
}
}

// ---------------------------------------------------------------------------
// 7. module.json 检查
// ---------------------------------------------------------------------------
echo "\n--- 7. module.json 检查 ---\n";

$json_path = $module_path . 'module.json';

if (!file_exists($json_path)) {
echo " ❌ module.json 不存在\n";
$errors[] = "module.json 不存在";
} else {
$json_raw = file_get_contents($json_path);
$json_data = json_decode($json_raw, true);

if (json_last_error() !== JSON_ERROR_NONE) {
echo " ❌ module.json 不是有效 JSON: " . json_last_error_msg() . "\n";
$errors[] = "module.json 解析失败: " . json_last_error_msg();
} else {
echo " ✅ module.json 是有效 JSON\n";

$required_keys = ['id', 'name', 'version'];
foreach ($required_keys as $key) {
if (isset($json_data[$key])) {
echo " ✅ 包含必需字段: $key = " . $json_data[$key] . "\n";
} else {
echo " ❌ 缺少必需字段: $key\n";
$errors[] = "module.json 缺少字段: $key";
}
}
}
}

// ---------------------------------------------------------------------------
// 8. require_once 完整性检查
// ---------------------------------------------------------------------------
echo "\n--- 8. require_once 完整性检查 ---\n";

$module_content = file_exists($module_file) ? file_get_contents($module_file) : '';

preg_match_all(
"/require_once\s+__DIR__\s*\.\s*'([^']+)'/",
$module_content,
$req_matches
);

$require_files = $req_matches[1] ?? [];
$require_errors = 0;

if (empty($require_files)) {
echo " ⚠️ 未找到 require_once 语句\n";
$warnings[] = "ApiGatewayModule.php 中未找到 require_once";
} else {
foreach ($require_files as $req_file) {
// $req_file 以 / 开头,如 /includes/SchemaManager.php
$full = $module_path . ltrim($req_file, '/');
if (file_exists($full)) {
echo " ✅ $req_file\n";
} else {
echo " ❌ $req_file (文件不存在)\n";
$errors[] = "require_once 引用的文件不存在: $req_file";
$require_errors++;
}
}

if ($require_errors === 0) {
echo " require_once 统计: " . count($require_files) . " 个文件全部存在\n";
}
}

// ---------------------------------------------------------------------------
// 结果汇总
// ---------------------------------------------------------------------------
echo "\n=== 测试结果汇总 ===\n";

if (empty($errors) && empty($warnings)) {
echo "✅ 所有测试通过\n";
exit(0);
}

if (!empty($errors)) {
echo "❌ 发现 " . count($errors) . " 个错误:\n";
foreach ($errors as $error) {
echo " - $error\n";
}
}

if (!empty($warnings)) {
echo "⚠️ 发现 " . count($warnings) . " 个警告:\n";
foreach ($warnings as $warning) {
echo " - $warning\n";
}
}

exit(empty($errors) ? 0 : 1);

View file

@ -1,196 +0,0 @@
#!/usr/bin/env php
<?php
/**
* WPMind 集成测试脚本
*
* 用于部署后验证所有模板依赖的方法是否存在
*
* 使用方法:
* php tests/test-integration.php
* 或部署后: php /www/wwwroot/wpcy.com/wp-content/plugins/wpmind/tests/test-integration.php
*
* @package WPMind
* @since 3.2.0
*/

error_reporting(E_ALL);
ini_set('display_errors', 1);

// 自动检测插件路径
$plugin_path = dirname(__DIR__) . '/';
if (!file_exists($plugin_path . 'wpmind.php')) {
$plugin_path = '/www/wwwroot/wpcy.com/wp-content/plugins/wpmind/';
}

echo "=== WPMind 集成测试 ===\n";
echo "插件路径: $plugin_path\n\n";

$errors = [];
$warnings = [];

/**
* 检查文件中是否存在指定方法
*/
function check_method(string $file, string $method): bool {
if (!file_exists($file)) {
return false;
}
$content = file_get_contents($file);
return (bool) preg_match('/function\s+' . preg_quote($method, '/') . '\s*\(/', $content);
}

/**
* 从文件中提取调用的静态方法
*/
function extract_static_calls(string $file, string $class): array {
if (!file_exists($file)) {
return [];
}
$content = file_get_contents($file);
preg_match_all('/' . preg_quote($class, '/') . '::([a-zA-Z_]+)\(/', $content, $matches);
return array_unique($matches[1] ?? []);
}

// 1. 文件存在性检查
echo "--- 文件存在性检查 ---\n";
$required_files = [
'wpmind.php',
'modules/cost-control/CostControlModule.php',
'modules/cost-control/includes/UsageTracker.php',
'modules/cost-control/includes/BudgetManager.php',
'modules/cost-control/includes/BudgetChecker.php',
'modules/cost-control/includes/BudgetAlert.php',
'modules/cost-control/templates/settings.php',
'modules/geo/GeoModule.php',
'modules/geo/includes/CrawlerTracker.php',
'modules/geo/templates/settings.php',
'includes/Usage/Pricing.php',
'templates/tabs/dashboard.php',
'templates/tabs/budget.php',
'templates/settings-page.php',
];

foreach ($required_files as $file) {
$full_path = $plugin_path . $file;
if (file_exists($full_path)) {
echo "✅ $file\n";
} else {
echo "❌ $file (不存在)\n";
$errors[] = "文件不存在: $file";
}
}

// 2. 语法检查
echo "\n--- PHP 语法检查 ---\n";
$php_files = glob($plugin_path . '{*.php,**/*.php,**/**/*.php}', GLOB_BRACE);
$syntax_errors = 0;
foreach ($php_files as $file) {
$output = [];
$return_var = 0;
exec("php -l '$file' 2>&1", $output, $return_var);
if ($return_var !== 0) {
$relative = str_replace($plugin_path, '', $file);
echo "❌ $relative: " . implode("\n", $output) . "\n";
$errors[] = "语法错误: $relative";
$syntax_errors++;
}
}
if ($syntax_errors === 0) {
echo "✅ 所有 PHP 文件语法正确\n";
}

// 3. dashboard.php 依赖检查
echo "\n--- dashboard.php 依赖检查 ---\n";
$tracker_file = $plugin_path . 'modules/cost-control/includes/UsageTracker.php';
$dashboard_methods = extract_static_calls($plugin_path . 'templates/tabs/dashboard.php', 'UsageTracker');

foreach ($dashboard_methods as $method) {
if (check_method($tracker_file, $method)) {
echo "✅ UsageTracker::$method()\n";
} else {
echo "❌ UsageTracker::$method() 缺失\n";
$errors[] = "方法缺失: UsageTracker::$method()";
}
}

// 4. budget.php 依赖检查
echo "\n--- budget.php 依赖检查 ---\n";
$manager_file = $plugin_path . 'modules/cost-control/includes/BudgetManager.php';
$checker_file = $plugin_path . 'modules/cost-control/includes/BudgetChecker.php';

if (check_method($manager_file, 'get_settings')) {
echo "✅ BudgetManager::get_settings()\n";
} else {
echo "❌ BudgetManager::get_settings() 缺失\n";
$errors[] = "方法缺失: BudgetManager::get_settings()";
}

if (check_method($checker_file, 'get_summary')) {
echo "✅ BudgetChecker::get_summary()\n";
} else {
echo "❌ BudgetChecker::get_summary() 缺失\n";
$errors[] = "方法缺失: BudgetChecker::get_summary()";
}

// 5. Cost Control 模块设置页面依赖检查
echo "\n--- cost-control/templates/settings.php 依赖检查 ---\n";
$cc_settings = $plugin_path . 'modules/cost-control/templates/settings.php';
if (file_exists($cc_settings)) {
$cc_tracker_methods = extract_static_calls($cc_settings, 'UsageTracker');
foreach ($cc_tracker_methods as $method) {
if (check_method($tracker_file, $method)) {
echo "✅ UsageTracker::$method()\n";
} else {
echo "❌ UsageTracker::$method() 缺失\n";
$errors[] = "方法缺失: UsageTracker::$method()";
}
}

$cc_manager_methods = extract_static_calls($cc_settings, 'BudgetManager');
foreach ($cc_manager_methods as $method) {
if (check_method($manager_file, $method)) {
echo "✅ BudgetManager::$method()\n";
} else {
echo "❌ BudgetManager::$method() 缺失\n";
$errors[] = "方法缺失: BudgetManager::$method()";
}
}
}

// 6. GEO 模块设置页面依赖检查
echo "\n--- geo/templates/settings.php 依赖检查 ---\n";
$geo_settings = $plugin_path . 'modules/geo/templates/settings.php';
$crawler_file = $plugin_path . 'modules/geo/includes/CrawlerTracker.php';
if (file_exists($geo_settings) && file_exists($crawler_file)) {
echo "✅ GEO 模块文件完整\n";
}

// 7. Cost Control 模块完整性检查
echo "\n--- Cost Control 模块完整性检查 ---\n";
$cost_control_tracker = $plugin_path . 'modules/cost-control/includes/UsageTracker.php';

foreach ($dashboard_methods as $method) {
if (!check_method($cost_control_tracker, $method)) {
echo "⚠️ UsageTracker::$method() 缺失\n";
$warnings[] = "模块方法缺失: UsageTracker::$method()";
}
}

// 结果汇总
echo "\n=== 测试结果汇总 ===\n";
if (empty($errors)) {
echo "✅ 所有测试通过\n";
exit(0);
} else {
echo "❌ 发现 " . count($errors) . " 个错误:\n";
foreach ($errors as $error) {
echo " - $error\n";
}
if (!empty($warnings)) {
echo "\n⚠ 发现 " . count($warnings) . " 个警告:\n";
foreach ($warnings as $warning) {
echo " - $warning\n";
}
}
exit(1);
}

View file

@ -1,218 +0,0 @@
<?php
/**
* WPMind 公共 API 测试脚本
*
* 使用方法:
* 1. 通过 WP-CLI: wp eval-file tests/test-public-api.php
* 2. 或者在主题 functions.php 中临时调用
*
* @package WPMind
* @since 2.5.0
*/

// 确保在 WordPress 环境中运行
if (!defined('ABSPATH')) {
echo "请在 WordPress 环境中运行此脚本\n";
exit(1);
}

echo "\n";
echo "================================================\n";
echo " WPMind 公共 API 测试\n";
echo "================================================\n\n";

// 测试 1: wpmind_is_available()
echo "【测试 1】wpmind_is_available()\n";
echo "─────────────────────────────────────────────────\n";

if (function_exists('wpmind_is_available')) {
$available = wpmind_is_available();
echo "结果: " . ($available ? "✅ 可用" : "❌ 不可用") . "\n";
} else {
echo "❌ 函数未定义\n";
}
echo "\n";

// 测试 2: wpmind_get_status()
echo "【测试 2】wpmind_get_status()\n";
echo "─────────────────────────────────────────────────\n";

if (function_exists('wpmind_get_status')) {
$status = wpmind_get_status();
echo "可用状态: " . ($status['available'] ? "是" : "否") . "\n";
echo "当前服务商: " . ($status['provider'] ?: "未设置") . "\n";
echo "当前模型: " . ($status['model'] ?: "未设置") . "\n";
echo "今日用量: " . $status['usage']['today'] . " tokens\n";
echo "本月用量: " . $status['usage']['month'] . " tokens\n";
} else {
echo "❌ 函数未定义\n";
}
echo "\n";

// 测试 3: wpmind_chat() - 简单模式
echo "【测试 3】wpmind_chat() - 简单模式\n";
echo "─────────────────────────────────────────────────\n";

if (function_exists('wpmind_chat')) {
echo "发送: \"你好,请用一句话介绍你自己\"\n";
echo "请求中...\n";
$start = microtime(true);
$result = wpmind_chat('你好,请用一句话介绍你自己', [
'context' => 'test_simple',
'max_tokens' => 100,
]);
$duration = round((microtime(true) - $start) * 1000);
if (is_wp_error($result)) {
echo "❌ 错误: " . $result->get_error_message() . "\n";
} else {
echo "✅ 响应 ({$duration}ms):\n";
echo " 内容: " . mb_substr($result['content'], 0, 100) . "...\n";
echo " 服务商: " . $result['provider'] . "\n";
echo " 模型: " . $result['model'] . "\n";
echo " Token: " . $result['usage']['total_tokens'] . "\n";
}
} else {
echo "❌ 函数未定义\n";
}
echo "\n";

// 测试 4: wpmind_chat() - 多轮对话
echo "【测试 4】wpmind_chat() - 多轮对话模式\n";
echo "─────────────────────────────────────────────────\n";

if (function_exists('wpmind_chat')) {
$messages = [
['role' => 'system', 'content' => '你是一个 WordPress 专家'],
['role' => 'user', 'content' => '什么是 Hook用一句话回答'],
];
echo "发送多轮对话...\n";
$start = microtime(true);
$result = wpmind_chat($messages, [
'context' => 'test_multiround',
'max_tokens' => 100,
]);
$duration = round((microtime(true) - $start) * 1000);
if (is_wp_error($result)) {
echo "❌ 错误: " . $result->get_error_message() . "\n";
} else {
echo "✅ 响应 ({$duration}ms):\n";
echo " 内容: " . $result['content'] . "\n";
}
} else {
echo "❌ 函数未定义\n";
}
echo "\n";

// 测试 5: wpmind_translate()
echo "【测试 5】wpmind_translate()\n";
echo "─────────────────────────────────────────────────\n";

if (function_exists('wpmind_translate')) {
echo "翻译: \"WordPress 性能优化指南\" -> 英文\n";
$start = microtime(true);
$result = wpmind_translate('WordPress 性能优化指南', 'zh', 'en', [
'context' => 'test_translate',
'cache_ttl' => 0, // 测试时不缓存
]);
$duration = round((microtime(true) - $start) * 1000);
if (is_wp_error($result)) {
echo "❌ 错误: " . $result->get_error_message() . "\n";
} else {
echo "✅ 翻译结果 ({$duration}ms): " . $result . "\n";
}
// 测试 slug 格式
echo "\n翻译为 Slug 格式...\n";
$start = microtime(true);
$result = wpmind_translate('WordPress 性能优化指南', 'zh', 'en', [
'format' => 'slug',
'cache_ttl' => 0,
]);
$duration = round((microtime(true) - $start) * 1000);
if (is_wp_error($result)) {
echo "❌ 错误: " . $result->get_error_message() . "\n";
} else {
echo "✅ Slug 结果 ({$duration}ms): " . $result . "\n";
}
} else {
echo "❌ 函数未定义\n";
}
echo "\n";

// 测试 6: 缓存功能
echo "【测试 6】缓存功能\n";
echo "─────────────────────────────────────────────────\n";

if (function_exists('wpmind_chat')) {
$prompt = '1+1等于几只回答数字';
echo "第一次请求 (无缓存)...\n";
$start = microtime(true);
$result1 = wpmind_chat($prompt, [
'context' => 'test_cache',
'cache_ttl' => 60, // 缓存 60 秒
]);
$duration1 = round((microtime(true) - $start) * 1000);
if (!is_wp_error($result1)) {
echo " 耗时: {$duration1}ms\n";
echo "第二次请求 (有缓存)...\n";
$start = microtime(true);
$result2 = wpmind_chat($prompt, [
'context' => 'test_cache',
'cache_ttl' => 60,
]);
$duration2 = round((microtime(true) - $start) * 1000);
echo " 耗时: {$duration2}ms\n";
if ($duration2 < $duration1 / 2) {
echo "✅ 缓存生效 (第二次快于第一次)\n";
} else {
echo "⚠️ 缓存可能未生效\n";
}
// 清理测试缓存
$cache_key = 'wpmind_chat_' . md5(serialize(['messages' => [['role' => 'user', 'content' => $prompt]], 'max_tokens' => 1000, 'temperature' => 0.7, 'json_mode' => false]));
delete_transient($cache_key);
} else {
echo "❌ 错误: " . $result1->get_error_message() . "\n";
}
}
echo "\n";

// 测试 7: Hooks
echo "【测试 7】Hooks 测试\n";
echo "─────────────────────────────────────────────────\n";

$hook_triggered = false;

// 注册测试 Hook
add_action('wpmind_before_request', function($type, $args, $context) use (&$hook_triggered) {
$hook_triggered = true;
echo " ✅ wpmind_before_request 触发 (type: {$type}, context: {$context})\n";
}, 10, 3);

if (function_exists('wpmind_chat')) {
wpmind_chat('测试 Hook', [
'context' => 'test_hooks',
'max_tokens' => 10,
]);
if (!$hook_triggered) {
echo " ⚠️ Hook 未触发\n";
}
}
echo "\n";

echo "================================================\n";
echo " 测试完成\n";
echo "================================================\n\n";

View file

@ -744,5 +744,22 @@ spl_autoload_register( function ( string $class ): void {
require_once WPMIND_PLUGIN_DIR . 'includes/class-wenpai-updater.php';
new \WenPai_Updater( WPMIND_PLUGIN_BASENAME, WPMIND_VERSION );

// 加载 WenPai 授权客户端
require_once WPMIND_PLUGIN_DIR . 'includes/class-wenpai-license.php';

/**
* 获取 WPMind 授权实例。
*
* @since 4.0.0
* @return \WenPai_License
*/
function wpmind_license(): \WenPai_License {
static $instance = null;
if ( null === $instance ) {
$instance = new \WenPai_License( 'wpmind' );
}
return $instance;
}

// 加载 Provider 注册模块
require_once WPMIND_PLUGIN_DIR . 'includes/Providers/register.php';