Files
zyt/server/app/mcp/cli/probe.php
T
2026-09-24 09:45:44 +08:00

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));
}