67 lines
3.2 KiB
PHP
67 lines
3.2 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* AI 数据目录探测:以指定后台账号的身份,在只读事务里逐个执行候选资源,报告哪些正常、哪些会写库、哪些报错。
|
|
* 用于人工审核(在测试/预发环境的数据库上运行,不要在生产库上跑)。
|
|
*
|
|
* 用法(在 server 目录下):
|
|
* php app/mcp/cli/probe.php --admin=1 [--only=tcm.] [--external] [--json=runtime/probe.json]
|
|
* --admin 用哪个后台账号(ID)的权限执行,建议用 root 账号看全貌
|
|
* --only 只探测以此开头的资源
|
|
* --external 也探测会调用外部接口的资源(默认跳过)
|
|
* 注意:只读事务只能挡住写库;起进程、写缓存、调外部接口挡不住。所以写操作类、已标记不开放的、
|
|
* 以及扫描出外部调用的资源默认一律不执行。
|
|
*/
|
|
|
|
use app\common\model\auth\Admin;
|
|
use app\mcp\service\Catalog;
|
|
use app\mcp\service\Dispatcher;
|
|
use app\mcp\service\Identity;
|
|
use think\App;
|
|
|
|
$root = dirname(__DIR__, 3) . DIRECTORY_SEPARATOR;
|
|
require $root . 'vendor/autoload.php';
|
|
$app = new App($root);
|
|
$app->initialize();
|
|
|
|
$options = getopt('', ['admin:', 'only:', 'external', 'json:']);
|
|
$admin = Admin::where('id', (int) ($options['admin'] ?? 0))->findOrEmpty();
|
|
if ($admin->isEmpty()) {
|
|
fwrite(STDERR, "请用 --admin=<后台账号ID> 指定执行身份\n");
|
|
exit(1);
|
|
}
|
|
$identity = new Identity(['id' => 0, 'expire_time' => time() + 3600], $admin);
|
|
$only = (string) ($options['only'] ?? '');
|
|
$results = [];
|
|
foreach (Catalog::all() as $key => $resource) {
|
|
if ($only !== '' && !str_starts_with($key, $only)) {
|
|
continue;
|
|
}
|
|
if ($resource['kind'] === 'write' || $resource['http'] === 'POST' || !empty($resource['no_login']) || $resource['status'] === Catalog::EXCLUDED) {
|
|
continue;
|
|
}
|
|
if (!isset($options['external']) && !empty($resource['external'])) {
|
|
continue;
|
|
}
|
|
$params = match ($resource['kind']) {
|
|
'list' => ['page_no' => 1, 'page_size' => 3, 'page_type' => 1],
|
|
'detail' => [(string) ($resource['guard']['param'] ?? $resource['id_param'] ?? 'id') => 1],
|
|
default => [],
|
|
};
|
|
$started = microtime(true);
|
|
$envelope = Dispatcher::call($identity, $resource, array_merge($params, (array) ($resource['force'] ?? [])));
|
|
$ms = (int) round((microtime(true) - $started) * 1000);
|
|
$msg = $envelope['msg'];
|
|
$outcome = $envelope['code'] === 1 ? 'ok' : (str_contains($msg, '只读保护') ? 'writes' : (str_contains($msg, '查询失败') ? 'error' : 'fail'));
|
|
$rows = is_array($envelope['data']['lists'] ?? null) ? count($envelope['data']['lists']) : null;
|
|
$results[$key] = ['status' => $resource['status'], 'kind' => $resource['kind'], 'outcome' => $outcome, 'msg' => mb_substr($msg, 0, 120), 'rows' => $rows, 'ms' => $ms];
|
|
printf("%-8s %-8s %-7s %5dms %s %s\n", $outcome, $resource['status'], $resource['kind'], $ms, $key, $outcome === 'ok' ? '' : mb_substr($msg, 0, 80));
|
|
}
|
|
$summary = array_count_values(array_column($results, 'outcome'));
|
|
ksort($summary);
|
|
echo PHP_EOL . json_encode($summary, JSON_UNESCAPED_UNICODE) . PHP_EOL;
|
|
if (!empty($options['json'])) {
|
|
file_put_contents($root . $options['json'], json_encode($results, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
|
}
|