🧩 Typecho AI DeepSeek 助手插件开发方案
📁 一、插件目录结构
建议采用规范的目录结构,便于维护和扩展 :
AIDeepSeek/ # 插件根目录(与插件名一致)
├── Plugin.php # 插件主文件(核心逻辑)
├── Action.php # 动作处理文件(处理AJAX请求等)
├── README.md # 说明文档
├── assets/ # 静态资源
│ ├── css/
│ │ └── style.css # 前端样式
│ └── js/
│ └── script.js # 前端交互脚本
└── views/ # 视图文件
└── widget.php # 前端显示模板📝 二、插件主文件:Plugin.php
这是插件的核心,负责定义插件信息、激活/禁用逻辑以及配置面板。
1. 头部注释与命名空间
插件信息通过注释定义,Typecho 后台通过解析这些注释来识别插件 :
<?php
namespace TypechoPlugin\AIDeepSeek;
use Typecho\Plugin\PluginInterface;
use Typecho\Widget\Helper\Form;
use Typecho\Widget\Helper\Form\Element\Text;
use Typecho\Widget\Helper\Form\Element\Textarea;
use Typecho\Widget\Helper\Form\Element\Radio;
use Typecho\Db;
use Widget\Options;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* AI DeepSeek 助手 - 为你的博客接入 DeepSeek AI 能力
*
* 通过 DeepSeek API 为博客添加智能问答、文章总结、代码辅助等功能。
* 支持自定义提示词、对话历史记录、安全限制等特性。
*
* @package AI DeepSeek
* @author 你的名字
* @version 1.0.0
* @link https://your-website.com
* @since 1.2.0
*/
class Plugin implements PluginInterface
{
// 插件方法将在这里定义
}2. 激活与禁用方法
激活插件时注册必要的钩子(Hook),禁用时清理资源 :
public static function activate()
{
// 1. 检查依赖(如 cURL 扩展)
if (!extension_loaded('curl')) {
throw new \Typecho\Plugin\Exception(_t('需要 PHP cURL 扩展'));
}
// 2. 注册钩子:在文章页底部显示 AI 助手
\Typecho\Plugin::factory('Widget_Archive')->afterRender = __CLASS__ . '::renderWidget';
// 3. 添加路由(用于处理 AJAX 请求)
\Helper::addRoute('ai_deepseek_ask', '/ai-deepseek/ask', 'AIDeepSeek_Action', 'ask');
return _t('插件已激活,请前往设置 API 密钥');
}
public static function deactivate()
{
// 移除路由
\Helper::removeRoute('ai_deepseek_ask');
return _t('插件已禁用');
}3. 配置面板
提供表单让用户输入 DeepSeek API 密钥和其他参数 :
public static function config(Form $form)
{
// API 密钥(必填)
$apiKey = new Text('api_key', null, null, _t('DeepSeek API 密钥'), _t('请在 DeepSeek 官网申请'));
$apiKey->addRule('required', _t('必须填写 API 密钥'));
$form->addInput($apiKey);
// API 接入点(可选)
$apiUrl = new Text('api_url', null, 'https://api.deepseek.com/v1/chat/completions', _t('API 接入点'), _t('一般无需修改'));
$form->addInput($apiUrl);
// 模型选择
$model = new \Typecho\Widget\Helper\Form\Element\Select('model',
['deepseek-chat' => 'DeepSeek Chat', 'deepseek-coder' => 'DeepSeek Coder'],
'deepseek-chat', _t('选择模型'), _t('根据需求选择对话或代码模型'));
$form->addInput($model);
// 分析内容长度限制
$contentLength = new Text('content_length', null, '500', _t('分析内容长度(字符)'), _t('提取文章前多少字符进行分析,越长消耗越多'));
$contentLength->addRule('isInteger', _t('请填入数字'));
$form->addInput($contentLength);
// 系统提示词(用户可自定义 AI 角色)
$systemPrompt = new Textarea('system_prompt', null, '你是一个专业的博客助手,帮助读者理解文章内容。', _t('系统提示词'), _t('定义 AI 的行为和角色'));
$form->addInput($systemPrompt);
// 访客限制
$guestLimit = new Radio('guest_limit',
['0' => _t('允许所有人使用'), '1' => _t('仅允许登录用户使用')],
'1', _t('访客权限'), _t('开启后可防止匿名用户滥用消耗 API'));
$form->addInput($guestLimit);
// 提问间隔限制
$interval = new Text('ask_interval', null, '30', _t('提问间隔(秒)'), _t('防止同一 IP 短时间内频繁请求'));
$interval->addRule('isInteger', _t('请填入数字'));
$form->addInput($interval);
}4. 渲染前端组件
通过钩子触发的方法,将 AI 助手界面输出到文章页底部 :
public static function renderWidget()
{
// 获取当前文章对象
$archive = \Typecho\Widget::widget('Widget_Archive');
// 只在文章详情页显示
if ($archive->is('single')) {
$options = Options::alloc();
$config = $options->plugin('AIDeepSeek');
// 传递配置到视图
$data = [
'apiConfigured' => !empty($config->api_key),
'guestLimit' => $config->guest_limit,
'userLoggedIn' => \Typecho\Widget::widget('Widget_User')->hasLogin(),
'contentLength' => $config->content_length ?: 500,
'systemPrompt' => $config->system_prompt,
'currentUrl' => $archive->permalink,
'currentTitle' => $archive->title
];
// 加载视图文件
require_once dirname(__FILE__) . '/views/widget.php';
}
}⚙️ 三、动作处理文件:Action.php
用于处理前端 AJAX 请求,调用 DeepSeek API :
<?php
namespace TypechoPlugin\AIDeepSeek;
use Typecho\Widget;
use Typecho\Db;
use Widget\Options;
class Action extends Widget implements Widget\ActionInterface
{
public function action()
{
// 路由分发
$this->on($this->request->is('ask'))->ask();
}
public function execute()
{
// 权限检查(可选)
$this->user->pass('contributor');
}
/**
* 处理提问请求
*/
public function ask()
{
try {
// 1. 获取配置
$config = Options::alloc()->plugin('AIDeepSeek');
// 2. 安全验证
$this->securityCheck($config);
// 3. 获取输入
$question = $this->request->get('question');
$context = $this->request->get('context'); // 文章内容片段
$title = $this->request->get('title');
// 4. 构建请求数据
$messages = $this->buildMessages($config, $title, $context, $question);
// 5. 调用 DeepSeek API
$response = $this->callDeepSeek($config->api_key, $config->api_url, $config->model, $messages);
// 6. 返回结果
$this->response->throwJson(['success' => true, 'data' => $response]);
} catch (\Exception $e) {
$this->response->throwJson(['success' => false, 'message' => $e->getMessage()]);
}
}
/**
* 安全检查:登录状态、频率限制等
*/
private function securityCheck($config)
{
// 检查登录限制
if ($config->guest_limit == '1' && !$this->user->hasLogin()) {
throw new \Exception(_t('请先登录后再提问'));
}
// 检查频率限制(示例:基于 IP 的 Session 存储)
$ip = $this->request->getIp();
$interval = intval($config->ask_interval ?: 30);
session_start();
$lastAsk = $_SESSION['ai_deepseek_last_ask_' . $ip] ?? 0;
if (time() - $lastAsk < $interval) {
throw new \Exception(_t('提问过于频繁,请稍后再试'));
}
$_SESSION['ai_deepseek_last_ask_' . $ip] = time();
session_write_close();
}
/**
* 构建消息数组
*/
private function buildMessages($config, $title, $context, $question)
{
$messages = [];
// 系统提示词
if (!empty($config->system_prompt)) {
$messages[] = ['role' => 'system', 'content' => $config->system_prompt];
}
// 添加上文背景(文章内容)
if ($context) {
$limit = intval($config->content_length ?: 500);
$context = mb_substr($context, 0, $limit, 'utf-8');
$messages[] = ['role' => 'user', 'content' => "我正在阅读文章《{$title}》,以下是部分内容:\n{$context}"];
}
// 当前问题
$messages[] = ['role' => 'user', 'content' => $question];
return $messages;
}
/**
* 调用 DeepSeek API
*/
private function callDeepSeek($apiKey, $apiUrl, $model, $messages)
{
$ch = curl_init($apiUrl);
$data = [
'model' => $model,
'messages' => $messages,
'temperature' => 0.7,
'stream' => false
];
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey
]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$error = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($error) {
throw new \Exception('API 请求失败:' . $error);
}
$result = json_decode($response, true);
if ($httpCode !== 200) {
$errorMsg = $result['error']['message'] ?? '未知错误';
throw new \Exception('API 返回错误:' . $errorMsg);
}
return $result['choices'][0]['message']['content'] ?? '';
}
}🎨 四、前端视图:views/widget.php
设计一个简洁的 AI 助手悬浮窗或文章底部对话框,并嵌入必要的 JS/CSS 。
<?php if ($data['apiConfigured'] && (!$data['guestLimit'] || $data['userLoggedIn'])): ?>
<link rel="stylesheet" href="<?php $options->pluginUrl('AIDeepSeek/assets/css/style.css') ?>">
<div id="ai-deepseek-widget" class="ai-widget">
<div class="ai-widget-header">
<span>🤖 AI 助手</span>
<button class="ai-toggle-btn">−</button>
</div>
<div class="ai-widget-body" style="display: none;">
<div class="ai-messages" id="ai-messages"></div>
<div class="ai-input-area">
<textarea id="ai-question" placeholder="输入你的问题..."></textarea>
<button id="ai-send-btn">发送</button>
</div>
<p class="ai-tip">提问内容将结合当前文章上下文</p>
</div>
</div>
<script>
// 传递配置到 JS
window.AIDeepSeekConfig = {
askUrl: '<?php $options->index('/ai-deepseek/ask') ?>',
title: <?php echo json_encode($data['currentTitle']); ?>,
contentLength: <?php echo $data['contentLength']; ?>
};
</script>
<script src="<?php $options->pluginUrl('AIDeepSeek/assets/js/script.js') ?>"></script>
<?php endif; ?>assets/js/script.js 示例逻辑
document.getElementById('ai-send-btn').addEventListener('click', function() {
const question = document.getElementById('ai-question').value.trim();
if (!question) return;
// 获取页面文章内容片段
const articleElement = document.querySelector('.post-content, .entry-content');
let context = articleElement ? articleElement.innerText.substring(0, window.AIDeepSeekConfig.contentLength) : '';
// 显示用户消息
appendMessage('user', question);
// 发送请求
fetch(window.AIDeepSeekConfig.askUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
question: question,
context: context,
title: window.AIDeepSeekConfig.title
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
appendMessage('assistant', data.data);
} else {
appendMessage('assistant', '错误:' + data.message);
}
})
.catch(() => {
appendMessage('assistant', '请求失败,请稍后重试');
});
});✅ 五、关键注意事项
安全性:
- 建议开启“仅登录用户可用”和“提问间隔限制”,防止 API 被恶意刷量导致欠费 。
- 对所有用户输入进行过滤和转义 。
- 使用 CSRF token 保护表单提交 。
API 费用:
- DeepSeek API 是付费服务,请在插件设置中提醒用户注意用量 。
错误处理:
- API 调用失败时应返回友好提示,避免暴露敏感信息 。
兼容性:
- 测试不同 Typecho 版本和 PHP 环境 。
扩展性:
- 可参考
AutoTags插件的思路 ,为文章编辑页增加“AI 生成摘要”或“AI 推荐标签”的功能。
- 可参考
📦 六、安装与发布
- 将整个
AIDeepSeek文件夹上传到/usr/plugins/。 - 进入 Typecho 后台 → 控制台 → 插件,找到“AI DeepSeek”并激活。
- 点击“设置”填写 DeepSeek API 密钥和其他选项。
- 访问任意文章页,底部应出现 AI 助手窗口。
按照教程一步步来,很顺利就装好了。唯一的问题是路由添加后访问404,后来发现是我没清除缓存。
建议在Action.php里增加一个nonce验证,防止CSRF攻击。虽然Typecho有内置的安全机制,但多一层防护总是好的。
The code handles errors gracefully, which is important for production use. I've seen too many plugins that just break silently.
插件文件夹命名建议和插件名保持一致,这点很重要,我之前就犯过这个错误导致插件无法识别。
我发现如果文章内容特别长,截取前500字可能会丢失重要信息,建议可以智能提取关键段落。