feat: 同步今日小程序与后台迭代版本
集中提交今日 API、管理端、小程序与部署文档调整,确保 Gitea 主分支与本地最新开发版本一致。 Made-with: Cursor
This commit is contained in:
75
api/scripts/check_ai_chat_ready.php
Normal file
75
api/scripts/check_ai_chat_ready.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* 神仙 AI 对话上线自检(在服务器 api 目录执行)
|
||||
* php scripts/check_ai_chat_ready.php
|
||||
*
|
||||
* 检查:ai_chat_jobs 表是否存在、是否有已启用的 AI 服务商、InternalPushHook 路由是否可达(仅提示)
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
use app\model\AiProvider as AiProviderModel;
|
||||
use think\App;
|
||||
use think\facade\Db;
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
$app = new App();
|
||||
$app->initialize();
|
||||
|
||||
$errors = [];
|
||||
$ok = [];
|
||||
|
||||
$prefix = (string) config('database.connections.mysql.prefix', '');
|
||||
$tableFull = $prefix . 'ai_chat_jobs';
|
||||
|
||||
try {
|
||||
Db::name('ai_chat_jobs')->limit(1)->select();
|
||||
$ok[] = '数据表 ' . $tableFull . ' 可访问';
|
||||
} catch (\Throwable $e) {
|
||||
$errors[] = '数据表 ai_chat_jobs 不可用(请执行 database/migrations/add_ai_chat_jobs.sql,前缀与 database.prefix 一致): ' . $e->getMessage();
|
||||
}
|
||||
|
||||
try {
|
||||
$n = AiProviderModel::where('enabled', 1)
|
||||
->whereRaw('(apiKey IS NOT NULL AND LENGTH(TRIM(apiKey)) > 0)')
|
||||
->count();
|
||||
if ($n > 0) {
|
||||
$ok[] = '已启用且含 apiKey 的 AI 服务商: ' . $n . ' 条';
|
||||
} else {
|
||||
$errors[] = '无可用 AI 服务商:请在超管启用至少一条 provider 并填写 apiKey';
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$errors[] = '读取 ai_providers 失败: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
try {
|
||||
$cfg = config('database.connections.mysql');
|
||||
$dbName = (string) ($cfg['database'] ?? '');
|
||||
$msgTable = $prefix . 'ai_messages';
|
||||
if ($dbName !== '') {
|
||||
$hit = Db::query(
|
||||
'SELECT COUNT(*) AS c FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?',
|
||||
[$dbName, $msgTable, 'isDegraded']
|
||||
);
|
||||
$c = (int) (($hit[0]['c'] ?? $hit[0]['C'] ?? 0));
|
||||
if ($c > 0) {
|
||||
$ok[] = '数据表 ' . $msgTable . ' 含 isDegraded 字段(与 AiChat 写入一致)';
|
||||
} else {
|
||||
$errors[] = '数据表 ' . $msgTable . ' 缺少 isDegraded 列:请执行 database/migrations/add_ai_chat_and_soul_articles.sql 或 patch_ai_messages_isdegraded.sql(前缀与 database.prefix 一致),否则助手消息无法落库';
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$errors[] = '检查 ai_messages.isDegraded 失败: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
$ok[] = '异步投递:确保已部署 InternalPushHook.php,且 POST /api/internal/outbound-push/dispatch 不被 Nginx 拦截';
|
||||
$ok[] = '小程序:上传含 ai-chat 轮询与超时逻辑的最新代码包;request 合法域名包含 API 域名';
|
||||
|
||||
foreach ($ok as $line) {
|
||||
echo '[OK] ' . $line . "\n";
|
||||
}
|
||||
foreach ($errors as $line) {
|
||||
echo '[!!] ' . $line . "\n";
|
||||
}
|
||||
|
||||
exit($errors ? 1 : 0);
|
||||
30
api/scripts/mint_jwt_for_smoke.js
Normal file
30
api/scripts/mint_jwt_for_smoke.js
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 生成与 JwtService::generateToken 同格式的 token(签名段为 hex),仅供本机冒烟。
|
||||
* 读取 api/.env 中的 JWT_SECRET。
|
||||
*/
|
||||
const crypto = require('crypto')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const envPath = path.join(__dirname, '..', '.env')
|
||||
let secret = 'mbti_jwt_secret_key_2024_change_in_production'
|
||||
try {
|
||||
const raw = fs.readFileSync(envPath, 'utf8')
|
||||
const m = raw.match(/JWT_SECRET\s*=\s*(\S+)/)
|
||||
if (m) secret = m[1].trim()
|
||||
} catch (_) {}
|
||||
|
||||
const header = Buffer.from(JSON.stringify({ typ: 'JWT', alg: 'HS256' })).toString('base64')
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const payload = Buffer.from(
|
||||
JSON.stringify({
|
||||
userId: 1,
|
||||
user_id: 1,
|
||||
source: 'wechat',
|
||||
exp: now + 3600,
|
||||
iat: now,
|
||||
})
|
||||
).toString('base64')
|
||||
const sig = crypto.createHmac('sha256', secret).update(header + '.' + payload).digest('hex')
|
||||
process.stdout.write(header + '.' + payload + '.' + sig + '\n')
|
||||
64
api/scripts/run_sql_file_pdo.php
Normal file
64
api/scripts/run_sql_file_pdo.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
/**
|
||||
* 用 PDO 执行 SQL 文件(去掉行注释 -- 后整段 exec,适配多行 UPDATE)
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
if ($argc < 2) {
|
||||
fwrite(STDERR, "用法: php scripts/run_sql_file_pdo.php <相对api根目录的sql路径>\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
$sqlPath = $root . '/' . ltrim($argv[1], '/');
|
||||
if (!is_readable($sqlPath)) {
|
||||
fwrite(STDERR, "找不到文件: {$sqlPath}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$envFile = $root . '/.env';
|
||||
$env = [];
|
||||
if (is_readable($envFile)) {
|
||||
foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
|
||||
$line = trim(str_replace("\r", '', $line));
|
||||
if ($line === '' || (isset($line[0]) && $line[0] === '#')) {
|
||||
continue;
|
||||
}
|
||||
if (!preg_match('/^([A-Za-z0-9_]+)\s*=\s*(.*)$/', $line, $m)) {
|
||||
continue;
|
||||
}
|
||||
$env[$m[1]] = trim($m[2], " \t\"'");
|
||||
}
|
||||
}
|
||||
|
||||
$host = $env['DATABASE_HOSTNAME'] ?? '127.0.0.1';
|
||||
$port = (int) ($env['DATABASE_HOSTPORT'] ?? 3306);
|
||||
$db = $env['DATABASE_DATABASE'] ?? '';
|
||||
$user = $env['DATABASE_USERNAME'] ?? '';
|
||||
$pass = $env['DATABASE_PASSWORD'] ?? '';
|
||||
if ($db === '' || $user === '') {
|
||||
fwrite(STDERR, ".env 缺少 DATABASE_DATABASE / DATABASE_USERNAME\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$raw = file_get_contents($sqlPath);
|
||||
if ($raw === false || trim($raw) === '') {
|
||||
fwrite(STDERR, "SQL 为空\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// 去掉整行 -- 注释(本仓库迁移文件常用)
|
||||
$sql = preg_replace('/^\s*--.*$/m', '', $raw);
|
||||
$sql = trim(preg_replace("/\n{3,}/", "\n\n", $sql));
|
||||
if ($sql === '') {
|
||||
fwrite(STDERR, "去掉注释后无 SQL\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$dsn = "mysql:host={$host};port={$port};dbname={$db};charset=utf8mb4";
|
||||
$pdo = new PDO($dsn, $user, $pass, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
$pdo->exec($sql);
|
||||
|
||||
echo "OK\n";
|
||||
62
api/scripts/smoke_ai_provider.php
Normal file
62
api/scripts/smoke_ai_provider.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/**
|
||||
* 神仙 AI 冒烟:与线上 AiChat 相同调用链(AiCallService::chat → OpenAI 兼容 /chat/completions 或 Anthropic)。
|
||||
* 用法:在 api 目录下执行 php scripts/smoke_ai_provider.php
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
use app\common\service\AiCallService;
|
||||
use app\model\AiProvider as AiProviderModel;
|
||||
use think\App;
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
$app = new App();
|
||||
$app->initialize();
|
||||
|
||||
$row = AiProviderModel::where('enabled', 1)
|
||||
->whereRaw('(visible IS NULL OR visible = 1)')
|
||||
->whereRaw('(apiKey IS NOT NULL AND LENGTH(TRIM(apiKey)) > 0)')
|
||||
->order('sortWeight', 'asc')
|
||||
->order('id', 'asc')
|
||||
->find();
|
||||
|
||||
$providerHint = $row ? [
|
||||
'id' => (int) $row->id,
|
||||
'providerId' => (string) ($row->providerId ?? ''),
|
||||
'endpoint' => trim((string) ($row->apiEndpoint ?? '')) ?: '(默认内置)',
|
||||
'model' => (string) ($row->model ?? ''),
|
||||
] : null;
|
||||
|
||||
$messages = [
|
||||
['role' => 'system', 'content' => AiCallService::buildSystemPrompt([
|
||||
'mbtiType' => 'ENTJ',
|
||||
'summary' => '',
|
||||
'nickname' => '冒烟测试',
|
||||
'testAppendix' => '',
|
||||
])],
|
||||
[
|
||||
'role' => 'user',
|
||||
'content' => '用两三句话回答:ENTJ 常见的一个优势和一个盲点分别是什么?不要以#或@开头,不要人设签名。',
|
||||
],
|
||||
];
|
||||
|
||||
$r = AiCallService::chat($messages, ['temperature' => 0.45, 'maxTokens' => 512]);
|
||||
|
||||
$content = trim((string) ($r['content'] ?? ''));
|
||||
$ok = $content !== '' && empty($r['isDegraded']);
|
||||
|
||||
$payload = [
|
||||
'first_provider' => $providerHint,
|
||||
'api_path' => strtolower((string) ($row->providerId ?? '')) === 'anthropic'
|
||||
? 'POST {endpoint}/v1/messages (Anthropic)'
|
||||
: 'POST {endpoint}/chat/completions (OpenAI 兼容)',
|
||||
'isDegraded' => !empty($r['isDegraded']),
|
||||
'providerId' => (string) ($r['providerId'] ?? ''),
|
||||
'content_preview'=> $ok ? mb_substr($content, 0, 280) . (mb_strlen($content) > 280 ? '…' : '') : $content,
|
||||
'ok' => $ok,
|
||||
];
|
||||
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), "\n";
|
||||
|
||||
exit($ok ? 0 : 1);
|
||||
Reference in New Issue
Block a user