更新
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* AI 数据目录:数据表覆盖检查。逐张表判断它能否通过 AI 查到:
|
||||
* endpoint 后台接口(adminapi)用到这张表,由目录里的接口资源覆盖
|
||||
* table 目录里有针对这张表的“数据表资源”(后台没有页面的表)
|
||||
* system 凭据、会话、配置、日志、队列等系统表,不对 AI 开放
|
||||
* uncovered 以上都不是:需要补一个数据表资源或写明不开放
|
||||
*
|
||||
* 用法(在 server 目录下,连接预发/测试库):
|
||||
* php app/mcp/cli/coverage.php 打印覆盖报告
|
||||
* php app/mcp/cli/coverage.php --write-tables 为 uncovered 的表生成 review/tables.php(仅超级管理员可查,去掉凭据列)
|
||||
*/
|
||||
|
||||
use app\mcp\service\Catalog;
|
||||
use think\App;
|
||||
use think\facade\Db;
|
||||
use think\helper\Str;
|
||||
|
||||
$root = dirname(__DIR__, 3) . DIRECTORY_SEPARATOR;
|
||||
require $root . 'vendor/autoload.php';
|
||||
(new App($root))->initialize();
|
||||
|
||||
const SYSTEM_TABLE = '/(session|token|^config$|_config$|^dev_|generate|crontab|^jobs|migration|install|^ai_grant$|^ai_access_log$|operation_log|^system_|^decorate|^notice_setting|^sms_log|file_cate|^file$|^article|^hot_search|^dict_|^iam_|^admin_role$|^admin_dept$|^admin_jobs$|^jobs$|pay_config|pay_way|^refund_log$|^recharge_order$|^user_auth$|^asset_|_cursor$|provider_state|_inbox$|^prescription_ai_(attempt|limit|request)$|query_log$|patient_trtc|click_log$|allocator$)/';
|
||||
const CREDENTIAL_COLUMN = '/(password|salt|secret|token|cipher|session_key|private_key|api_key|app_key|access_key|aes_key|signature|sign_key|user_?sig|cookie|credential|ticket)/i';
|
||||
|
||||
$prefix = (string) config('database.connections.' . config('database.default') . '.prefix');
|
||||
$tables = [];
|
||||
foreach (Db::query('SHOW TABLES') as $row) {
|
||||
$name = (string) array_values($row)[0];
|
||||
if ($prefix === '' || str_starts_with($name, $prefix)) {
|
||||
$tables[] = substr($name, strlen($prefix));
|
||||
}
|
||||
}
|
||||
sort($tables);
|
||||
|
||||
// 后台接口涉及的表:adminapi 代码里 Db::name/table 直接写的表名,以及 use 的模型类对应的表
|
||||
$modelTable = static function (string $class) use ($prefix): ?string {
|
||||
if (!class_exists($class)) {
|
||||
return null;
|
||||
}
|
||||
$ref = new ReflectionClass($class);
|
||||
if ($ref->isAbstract() || !$ref->isSubclassOf(\think\Model::class)) {
|
||||
return null;
|
||||
}
|
||||
$defaults = $ref->getDefaultProperties();
|
||||
if (!empty($defaults['table'])) {
|
||||
return preg_replace('/^' . preg_quote($prefix, '/') . '/', '', (string) $defaults['table']);
|
||||
}
|
||||
return !empty($defaults['name']) ? (string) $defaults['name'] : Str::snake($ref->getShortName());
|
||||
};
|
||||
$reachable = [];
|
||||
$files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root . 'app' . DIRECTORY_SEPARATOR . 'adminapi', FilesystemIterator::SKIP_DOTS));
|
||||
foreach ($files as $file) {
|
||||
if ($file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
$source = (string) file_get_contents($file->getPathname());
|
||||
if (preg_match_all('/(?:Db::|->)(?:name|table)\(\s*[\'"](\w+)/', $source, $m)) {
|
||||
foreach ($m[1] as $table) {
|
||||
$reachable[preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $table)] = true;
|
||||
}
|
||||
}
|
||||
if (preg_match_all('/^use\s+(app\\\\common\\\\model\\\\[\w\\\\]+);/m', $source, $m)) {
|
||||
foreach ($m[1] as $class) {
|
||||
if ($table = $modelTable($class)) {
|
||||
$reachable[$table] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$tableResources = [];
|
||||
foreach (Catalog::all() as $key => $resource) {
|
||||
if (!empty($resource['handler']['table'])) {
|
||||
$tableResources[$resource['handler']['table']] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
$report = [];
|
||||
foreach ($tables as $table) {
|
||||
$report[$table] = isset($tableResources[$table]) ? 'table' : (preg_match(SYSTEM_TABLE, $table) ? 'system' : (isset($reachable[$table]) ? 'endpoint' : 'uncovered'));
|
||||
}
|
||||
$counts = array_count_values($report);
|
||||
ksort($counts);
|
||||
echo 'tables: ' . count($tables) . ' ' . json_encode($counts) . PHP_EOL;
|
||||
foreach ($report as $table => $status) {
|
||||
if ($status === 'uncovered' || in_array('--verbose', $argv, true)) {
|
||||
echo str_pad($status, 10) . $table . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array('--write-tables', $argv, true)) {
|
||||
$entries = [];
|
||||
foreach ($report as $table => $status) {
|
||||
if ($status !== 'uncovered' && $status !== 'table') {
|
||||
continue;
|
||||
}
|
||||
$columns = [];
|
||||
$types = [];
|
||||
foreach (Db::query('SHOW COLUMNS FROM `' . $prefix . $table . '`') as $column) {
|
||||
$types[$column['Field']] = strtolower((string) $column['Type']);
|
||||
if (!preg_match(CREDENTIAL_COLUMN, (string) $column['Field'])) {
|
||||
$columns[] = $column['Field'];
|
||||
}
|
||||
}
|
||||
$filters = [];
|
||||
foreach ($columns as $column) {
|
||||
if ($column === 'id' || str_ends_with($column, '_id') || in_array($column, ['status', 'type'], true)) {
|
||||
$filters[$column] = '=';
|
||||
}
|
||||
}
|
||||
$date = isset($types['create_time']) ? 'create_time' : null;
|
||||
$entries['table.' . $table . '/lists'] = [
|
||||
'status' => 'open',
|
||||
'kind' => 'table',
|
||||
'perm' => 'ai.mcp/tables',
|
||||
'name' => '数据表 ' . $table,
|
||||
'domain' => '其他数据表',
|
||||
'note' => '后台没有页面的数据表:目前仅超级管理员可查;如需给其他角色开放,把 scope 改为属主列(如 [\'owner\' => [\'doctor_id\']])并审核',
|
||||
'handler' => array_filter([
|
||||
'table' => $table,
|
||||
'columns' => $columns,
|
||||
'filters' => $filters,
|
||||
'date' => $date,
|
||||
'date_type' => $date && str_contains($types[$date], 'int') ? 'int' : ($date ? 'datetime' : null),
|
||||
'soft_delete' => isset($types['delete_time']) ? 'delete_time' : null,
|
||||
'order' => in_array('id', $columns, true) ? 'id desc' : null,
|
||||
'scope' => 'root',
|
||||
], static fn ($v) => $v !== null),
|
||||
];
|
||||
}
|
||||
$target = $root . 'app' . DIRECTORY_SEPARATOR . 'mcp' . DIRECTORY_SEPARATOR . 'catalog' . DIRECTORY_SEPARATOR . 'review' . DIRECTORY_SEPARATOR . 'tables.php';
|
||||
$header = "<?php\n// 后台没有页面的数据表(php app/mcp/cli/coverage.php --write-tables 生成,可手工调整 scope/columns)。\n// 生成时间:" . date('Y-m-d H:i:s') . "\nreturn ";
|
||||
file_put_contents($target, $header . var_export($entries, true) . ";\n");
|
||||
echo 'written ' . count($entries) . ' table resources: ' . $target . PHP_EOL;
|
||||
}
|
||||
Reference in New Issue
Block a user