Compare commits
121 commits
671f4ce16a
...
f21f4b405a
| Author | SHA1 | Date | |
|---|---|---|---|
| f21f4b405a | |||
|
|
1b52474f3d | ||
|
|
d733b8e7fc | ||
|
|
3434080dfd | ||
|
|
57c301d234 | ||
|
|
c61b539e4e | ||
|
|
59acf3501f | ||
|
|
8e4d9a98fd | ||
|
|
419f18a4bf | ||
|
|
b89c1773aa | ||
|
|
85e90e0a50 | ||
|
|
3db7b58c5b | ||
|
|
6ee99661c5 | ||
|
|
c8da39d98e | ||
|
|
63c73ff120 | ||
|
|
69b0d95002 | ||
|
|
5672ef0888 | ||
|
|
5ababf0e18 | ||
|
|
64a17aa203 | ||
|
|
9f1bca77d5 | ||
|
|
ee1205d3e8 | ||
|
|
ade7336d9d | ||
|
|
60b9e37c09 | ||
|
|
fb874ff463 | ||
|
|
bb5e58282c | ||
|
|
b5093f0c3a | ||
|
|
7f8503e3ad | ||
|
|
8356f6ae15 | ||
|
|
cd30c961e1 | ||
|
|
563bf8e2c3 | ||
|
|
a4b2a90074 | ||
|
|
ef6fa2def6 | ||
|
|
49b25c58d1 | ||
|
|
36b69d300d | ||
|
|
6b89e4dd57 | ||
|
|
7d780574bc | ||
|
|
82c9e71e60 | ||
|
|
07721ad840 | ||
|
|
8b1d5a7796 | ||
|
|
db75102584 | ||
|
|
59da0bdfab | ||
|
|
3e1bd784e7 | ||
|
|
f263e38b04 | ||
|
|
253b3cadd8 | ||
|
|
29459364a8 | ||
|
|
493033ec70 | ||
|
|
b381a1295b | ||
|
|
b8bfb55dad | ||
|
|
b64063c9d7 | ||
|
|
1760669c4b | ||
|
|
c7108ed376 | ||
|
|
abac5095b4 | ||
|
|
ec51446624 | ||
|
|
ba4bdf20a9 | ||
|
|
22786bf996 | ||
|
|
cbe047fa2a | ||
|
|
d5144a205c | ||
|
|
19967d2dfa | ||
|
|
193a145f5b | ||
|
|
4e79f392ec | ||
|
|
18d6246e37 | ||
|
|
e495787463 | ||
|
|
40b349c3b0 | ||
|
|
277701c85b | ||
|
|
559883eb4b | ||
|
|
0ab7d3582d | ||
|
|
f0488aa735 | ||
|
|
6c36d563e4 | ||
|
|
b0b5032907 | ||
|
|
f5b2daf722 | ||
|
|
af33502949 | ||
|
|
d4aedd40dd | ||
|
|
960bfb8ba4 | ||
|
|
222f11495f | ||
|
|
74db7a20af | ||
|
|
817b7a6ba5 | ||
|
|
183a5c79c6 | ||
|
|
2441306a37 | ||
|
|
8c00d7d977 | ||
|
|
ee52ef9259 | ||
|
|
d15d601053 | ||
|
|
d3c2e70aab | ||
|
|
af8864aae3 | ||
|
|
d3f9bd2a16 | ||
|
|
b0fe6a6f1e | ||
|
|
02cb582d6c | ||
|
|
e18a999ff8 | ||
|
|
69c9942533 | ||
|
|
2a7c45aada | ||
|
|
0aea0a17f0 | ||
|
|
839e5151b8 | ||
|
|
a8d7b5cf83 | ||
|
|
144bab83bf | ||
|
|
d49ea59f8f | ||
|
|
3c74dddea4 | ||
|
|
907e5730fb | ||
|
|
d69fd55206 | ||
|
|
2aec4935e6 | ||
|
|
a54be49534 | ||
|
|
56f1563574 | ||
|
|
152519e1a0 | ||
|
|
85a9b7664d | ||
|
|
95ba43c06f | ||
|
|
2c2134ce85 | ||
|
|
ac91c8ac29 | ||
|
|
a11567e031 | ||
|
|
2d14c5d746 | ||
|
|
68744cb57d | ||
|
|
cc87f005d9 | ||
|
|
14bc422737 | ||
|
|
6e568938b2 | ||
|
|
0f9b5e2b90 | ||
|
|
4fab0b94ac | ||
|
|
d61c075c2f | ||
|
|
0fcb38e74d | ||
|
|
34d67c6195 | ||
|
|
8cf2f29e6c | ||
|
|
e0b39e49bc | ||
|
|
54d092af09 | ||
|
|
a820278954 | ||
|
|
f007de8d20 |
23 changed files with 3602 additions and 2418 deletions
685
WPMIND-ROADMAP.md
Normal file
685
WPMIND-ROADMAP.md
Normal 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。**
|
||||
|
||||
凡涉及以下特征的功能,**优先移至文派叶子 WPCY(slug: wp-china-yes)插件**实现:
|
||||
|
||||
| 特征 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| **请求拦截** | 拦截/修改其他插件的 HTTP 请求 | OpenAI API 代理、商业插件 AI 请求屏蔽 |
|
||||
| **UI 替换** | 隐藏/替换其他插件的界面元素 | Block+Hide+Inject 商业插件 AI 面板 |
|
||||
| **地域特供** | 仅特定地区用户需要的功能 | 国内镜像加速、GFW 绕过 |
|
||||
| **商业争议** | 可能与商业插件产生利益冲突 | 绕过付费 AI 服务、模拟商业 API 响应 |
|
||||
| **ToS 灰区** | 可能违反第三方服务条款 | 逆向工程商业 API 格式 |
|
||||
|
||||
**WPMind 只做:**
|
||||
- 提供 AI API(chat/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 Bridge(SEO 插件集成):**
|
||||
- [ ] `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 个 Service(15 个全局函数)
|
||||
├── 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
22
composer.json
Normal 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
2205
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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' => '未知操作' ] );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
311
includes/class-wenpai-license.php
Normal file
311
includes/class-wenpai-license.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
162
modules/api-gateway/DEPLOYMENT.md
Normal file
162
modules/api-gateway/DEPLOYMENT.md
Normal 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
22
phpunit.xml.dist
Normal 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>
|
||||
|
|
@ -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
134
templates/tabs/license.php
Normal 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>
|
||||
|
|
@ -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 ) );
|
||||
}
|
||||
}
|
||||
|
|
@ -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'] );
|
||||
}
|
||||
}
|
||||
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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'] );
|
||||
}
|
||||
}
|
||||
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
|
@ -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( '', $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( '', $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>& < > "</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 );
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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";
|
||||
17
wpmind.php
17
wpmind.php
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue