Typecho AI DeepSeek 助手插件开发方案


🧩 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', '请求失败,请稍后重试');
    });
});

✅ 五、关键注意事项

  1. 安全性

    • 建议开启“仅登录用户可用”和“提问间隔限制”,防止 API 被恶意刷量导致欠费 。
    • 对所有用户输入进行过滤和转义 。
    • 使用 CSRF token 保护表单提交 。
  2. API 费用

    • DeepSeek API 是付费服务,请在插件设置中提醒用户注意用量 。
  3. 错误处理

    • API 调用失败时应返回友好提示,避免暴露敏感信息 。
  4. 兼容性

    • 测试不同 Typecho 版本和 PHP 环境 。
  5. 扩展性

    • 可参考 AutoTags 插件的思路 ,为文章编辑页增加“AI 生成摘要”或“AI 推荐标签”的功能。

📦 六、安装与发布

  1. 将整个 AIDeepSeek 文件夹上传到 /usr/plugins/
  2. 进入 Typecho 后台 → 控制台 → 插件,找到“AI DeepSeek”并激活。
  3. 点击“设置”填写 DeepSeek API 密钥和其他选项。
  4. 访问任意文章页,底部应出现 AI 助手窗口。

已有 65 条评论

    1. Bella Feng Bella Feng

      今天测试了一下,发现如果问题中包含特殊字符会导致API报错,建议在发送前做一下转义处理。

    2. Landon Xia Landon Xia

      这个插件的思路可以扩展到其他AI服务,比如OpenAI、Claude等,做成一个统一的AI助手框架。

    3. Aria Zheng Aria Zheng

      能不能增加一个“清空对话”的按钮?有时候聊着聊着想重新开始。

    4. Adam Guo Adam Guo

      后台设置页面的链接最好用路由生成,而不是硬编码,这样更符合Typecho的开发规范。

    5. Lucy Yao Lucy Yao

      文章里的代码可以直接复制使用吗?我看到有版权声明,不太确定是否可以商用。