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. Gavin Liang Gavin Liang

      API密钥存在数据库里安全吗?有没有考虑过加密存储?

    2. Fiona Gu Fiona Gu

      能不能让AI助手出现在侧边栏?这样不占用文章内容区域,设计上更灵活。

    3. Evan Tang Evan Tang

      文章里提到要测试不同Typecho版本,确实很重要。我在1.1.4版本上运行正常,但在1.0.0上有一些小问题。

    4. Daisy Ruan Daisy Ruan

      前端代码用了fetch,兼容性很好,IE都能用(虽然现在没什么人用IE了)。

    5. Carter Su Carter Su

      希望能记录每个用户的对话历史,这样用户下次访问可以看到之前的聊天记录。