254 lines
20 KiB
PHP
254 lines
20 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
/**
|
||
* AI 助手(MCP)物流查询测试:zyt_logistics_order / zyt_logistics_patient。
|
||
* 覆盖:只查得到「处方业务订单」列表范围内的订单(医助只看自己创建的);已同步轨迹(状态、签收时间、最新在前、来源和更新时间);
|
||
* 收件人电话、患者电话、轨迹文字里的手机号脱敏,不返回详细地址;没有「物流轨迹」权限时不给轨迹;未填单号;
|
||
* 同名患者分开列出、按患者ID给最近一单完整轨迹;库里没有轨迹时的实时查询(本测试关闭了快递100 / 京东接口,
|
||
* 断言不写库、不外呼)及每小时次数上限;参数校验;工具中文名;审计记录。
|
||
*
|
||
* 需要一次性测试库(库名以 _test 结尾)和指向它的运行实例;运行实例请关闭物流外部接口,避免测试外呼:
|
||
* PHP_LOGISTICS_KUAIDI100_DISABLE=true PHP_JD_LOGISTICS_DISABLE=true php -S 127.0.0.1:8099 -t public public/router.php
|
||
* AI_MCP_TEST_MYSQL=1 AI_MCP_TEST_BASE_URL=http://127.0.0.1:8099 php server/tests/AiMcpLogisticsTest.php
|
||
* 夹具 ID 段:账号 93101-93103、角色 297-298、诊单 95101-95104、订单号 LOGT*、运单号 *LOGT*(2031 年 3 月)。
|
||
*/
|
||
|
||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||
|
||
use think\App;
|
||
use think\cache\driver\File as FileCache;
|
||
use think\facade\Db;
|
||
|
||
function aiMcpLogisticsExpect(bool $condition, string $message): void
|
||
{
|
||
if (!$condition) {
|
||
fwrite(STDERR, "FAIL: {$message}\n");
|
||
exit(1);
|
||
}
|
||
}
|
||
|
||
$base = rtrim((string) getenv('AI_MCP_TEST_BASE_URL'), '/');
|
||
if (getenv('AI_MCP_TEST_MYSQL') !== '1' || $base === '') {
|
||
echo "AiMcpLogisticsTest SKIP (set AI_MCP_TEST_MYSQL=1, AI_MCP_TEST_BASE_URL and PHP_DATABASE_* for a disposable *_test database)\n";
|
||
exit(0);
|
||
}
|
||
|
||
$app = new App(dirname(__DIR__) . DIRECTORY_SEPARATOR);
|
||
$app->initialize();
|
||
$database = (string) config('database.connections.' . config('database.default') . '.database');
|
||
aiMcpLogisticsExpect(str_ends_with($database, '_test'), "refusing to run on database '{$database}' (name must end with _test)");
|
||
aiMcpLogisticsExpect((int) Db::name('system_menu')->where('perms', 'ai.mcp/access')->count() === 1, 'run 2026_09_24_ai_mcp.sql on the test database first');
|
||
|
||
// ---------------------------------------------------------------- 夹具
|
||
$now = time();
|
||
$pwd = create_password('Test@123456', (string) config('project.unique_identification'));
|
||
$roles = [297 => ['物流医助', ['ai.mcp/access', 'tcm.prescriptionOrder/lists', 'tcm.prescriptionOrder/logisticsTrace']],
|
||
298 => ['物流无轨迹权限', ['ai.mcp/access', 'tcm.prescriptionOrder/lists']]];
|
||
Db::name('system_role')->whereIn('id', array_keys($roles))->delete();
|
||
Db::name('system_role_menu')->whereIn('role_id', array_keys($roles))->delete();
|
||
foreach ($roles as $id => [$name, $perms]) {
|
||
// data_scope 4:仅本人;角色 ID 不在 order_edit_all_roles 里,只看自己创建的订单
|
||
Db::name('system_role')->insert(['id' => $id, 'name' => $name, 'desc' => 'ai-mcp-logistics-test', 'sort' => 0, 'data_scope' => 4, 'create_time' => $now, 'update_time' => $now]);
|
||
foreach ($perms as $perm) {
|
||
Db::name('system_role_menu')->insert(['role_id' => $id, 'menu_id' => (int) Db::name('system_menu')->where('perms', $perm)->value('id')]);
|
||
}
|
||
}
|
||
$admins = [93101 => ['l_asst', 297], 93102 => ['l_other', 297], 93103 => ['l_notrace', 298]];
|
||
Db::name('admin')->whereIn('id', array_keys($admins))->delete();
|
||
Db::name('admin_role')->whereIn('admin_id', array_keys($admins))->delete();
|
||
Db::name('ai_grant')->whereIn('admin_id', array_keys($admins))->delete();
|
||
Db::name('ai_access_log')->whereIn('admin_id', array_keys($admins))->delete();
|
||
foreach ($admins as $id => [$account, $role]) {
|
||
Db::name('admin')->insert(['id' => $id, 'root' => 0, 'name' => $account, 'avatar' => '', 'account' => $account, 'password' => $pwd, 'multipoint_login' => 1,
|
||
'is_paw' => 1, 'work_wechat_userid' => '', 'disable' => 0, 'phone' => '1360000' . substr((string) $id, -4), 'create_time' => $now, 'update_time' => $now]);
|
||
Db::name('admin_role')->insert(['admin_id' => $id, 'role_id' => $role]);
|
||
}
|
||
Db::name('tcm_diagnosis')->whereIn('id', [95101, 95102, 95103, 95104])->delete();
|
||
foreach ([95101 => ['物流甲', '13911110001', 93101], 95102 => ['物流甲', '13911110002', 93101], 95103 => ['物流乙', '13911110003', 93102],
|
||
95104 => ['物流丙', '13911110004', 93103]] as $id => [$name, $phone, $assistant]) {
|
||
Db::name('tcm_diagnosis')->insert(['id' => $id, 'patient_id' => $id + 1000, 'patient_name' => $name, 'phone' => $phone, 'id_card' => '', 'gender' => 1, 'age' => 50,
|
||
'status' => 1, 'assistant_id' => $assistant, 'create_time' => $now, 'update_time' => $now]);
|
||
}
|
||
Db::name('tcm_prescription_order')->whereLike('order_no', 'LOGT%')->delete();
|
||
$orders = [];
|
||
foreach ([
|
||
// 订单号 => [创建人, 诊单, 履约状态, 快递公司, 运单号, 创建时间]
|
||
'LOGT1' => [93101, 95101, 6, 'jd', 'JDVELOGT0001', '2031-03-01 10:00:00'],
|
||
'LOGT2' => [93101, 95101, 5, 'sf', 'SFLOGT0002', '2031-03-05 10:00:00'],
|
||
'LOGT3' => [93101, 95102, 2, '', '', '2031-03-06 10:00:00'],
|
||
'LOGT4' => [93101, 95101, 5, 'yt', 'YTLOGT0004', '2031-03-07 10:00:00'], // 库里没有轨迹 → 实时查询
|
||
'LOGT5' => [93102, 95103, 5, 'jd', 'JDVELOGT0005', '2031-03-02 10:00:00'], // 别人的订单
|
||
'LOGT6' => [93103, 95104, 5, 'jd', 'JDVELOGT0006', '2031-03-03 10:00:00'], // 没有物流轨迹权限的账号
|
||
] as $no => [$creator, $diag, $status, $company, $number, $time]) {
|
||
$orders[$no] = (int) Db::name('tcm_prescription_order')->insertGetId(['order_no' => $no, 'prescription_id' => 0, 'diagnosis_id' => $diag, 'creator_id' => $creator,
|
||
'amount' => 300, 'fulfillment_status' => $status, 'refund_amount' => 0, 'express_company' => $company, 'tracking_number' => $number,
|
||
'recipient_name' => '收件人' . $no, 'recipient_phone' => '1391111' . sprintf('%04d', $diag - 95100), 'shipping_address' => '河南省洛阳市洛龙区开元大道 88 号 3 栋',
|
||
'shipping_province' => '河南省', 'shipping_city' => '洛阳市', 'shipping_district' => '洛龙区', 'create_time' => strtotime($time)]);
|
||
}
|
||
$numbers = ['JDVELOGT0001', 'SFLOGT0002', 'YTLOGT0004', 'JDVELOGT0005', 'JDVELOGT0006'];
|
||
$old = Db::name('express_tracking')->whereIn('tracking_number', $numbers)->column('id');
|
||
Db::name('express_trace')->whereIn('tracking_id', $old ?: [0])->delete();
|
||
Db::name('express_tracking')->whereIn('tracking_number', $numbers)->delete();
|
||
Db::name('express_query_log')->whereIn('tracking_number', $numbers)->delete();
|
||
foreach ([
|
||
'JDVELOGT0001' => ['LOGT1', 'jd', '京东快递', '3', '已签收', 'jd_official', '2031-03-03 10:00:00', [
|
||
['2031-03-01 18:00:00', '您的快件已由京东快递揽收'], ['2031-03-02 08:00:00', '快件已到达洛阳分拣中心,快递员王师傅 13812345678 正在派送'],
|
||
['2031-03-03 09:30:00', '您的快件已签收,签收人:本人']]],
|
||
'SFLOGT0002' => ['LOGT2', 'sf', '顺丰速运', '5', '派件中', 'kuaidi100', '2031-03-06 12:00:00', [
|
||
['2031-03-05 20:00:00', '顺丰速运 已收取快件'], ['2031-03-06 11:00:00', '快件派送中,派件员 13900001111']]],
|
||
'JDVELOGT0005' => ['LOGT5', 'jd', '京东快递', '0', '在途', 'jd_official', '2031-03-02 12:00:00', [['2031-03-02 11:00:00', '别人订单的轨迹']]],
|
||
'JDVELOGT0006' => ['LOGT6', 'jd', '京东快递', '0', '在途', 'jd_official', '2031-03-03 12:00:00', [['2031-03-03 11:00:00', '无权限账号订单的轨迹']]],
|
||
] as $number => [$no, $company, $companyName, $state, $stateText, $source, $queried, $traces]) {
|
||
$last = end($traces);
|
||
$trackingId = (int) Db::name('express_tracking')->insertGetId(['order_id' => $orders[$no], 'order_type' => 'prescription', 'tracking_number' => $number,
|
||
'express_company' => $company, 'express_company_name' => $companyName, 'recipient_phone' => '13911119999', 'recipient_name' => '收件人',
|
||
'recipient_address' => '河南省洛阳市', 'current_state' => $state, 'current_state_text' => $stateText, 'is_signed' => $state === '3' ? 1 : 0,
|
||
'latest_trace_time' => $last[0], 'latest_trace_context' => $last[1], 'latest_location' => '洛阳', 'last_query_time' => strtotime($queried), 'query_count' => 3,
|
||
'data_source' => $source, 'auto_update' => 1, 'create_time' => $now, 'update_time' => $now]);
|
||
foreach ($traces as [$time, $context]) {
|
||
Db::name('express_trace')->insert(['tracking_id' => $trackingId, 'tracking_number' => $number, 'trace_time' => $time, 'trace_time_stamp' => strtotime($time),
|
||
'trace_context' => $context, 'status' => '', 'status_code' => '', 'location' => '', 'create_time' => $now]);
|
||
}
|
||
}
|
||
// 实时查询的每小时计数在 mcp 应用自己的缓存目录里,清掉本小时的桶,让次数上限的断言可重复
|
||
$cacheOptions = (array) config('cache.stores.file');
|
||
$cacheOptions['path'] = app()->getRootPath() . 'runtime' . DIRECTORY_SEPARATOR . 'mcp' . DIRECTORY_SEPARATOR . 'cache';
|
||
foreach (array_keys($admins) as $adminId) {
|
||
(new FileCache(app(), $cacheOptions))->delete('ai_mcp_rl_logistics_live_' . $adminId . '_' . intdiv(time(), 3600));
|
||
}
|
||
\think\facade\Cache::clear();
|
||
|
||
// ---------------------------------------------------------------- HTTP 工具
|
||
function aiMcpLogisticsHttp(string $url, array $body, array $headers): array
|
||
{
|
||
$ch = curl_init($url);
|
||
$lines = ['Content-Type: application/json'];
|
||
foreach ($headers as $k => $v) {
|
||
$lines[] = $k . ': ' . $v;
|
||
}
|
||
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $lines, CURLOPT_TIMEOUT => 120,
|
||
CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_UNICODE)]);
|
||
$raw = (string) curl_exec($ch);
|
||
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||
curl_close($ch);
|
||
return [$status, json_decode($raw, true)];
|
||
}
|
||
|
||
$grant = static function (string $account) use ($base): string {
|
||
[, $body] = aiMcpLogisticsHttp($base . '/mcp/auth/grant', ['account' => $account, 'password' => 'Test@123456', 'client' => 'xingzhi', 'client_instance' => 'logistics-test'], []);
|
||
aiMcpLogisticsExpect(($body['code'] ?? null) === 1, "grant for {$account}: " . json_encode($body, JSON_UNESCAPED_UNICODE));
|
||
return (string) $body['data']['token'];
|
||
};
|
||
$rpc = static function (string $token, string $method, array $params = []) use ($base): array {
|
||
static $id = 0;
|
||
[$status, $body] = aiMcpLogisticsHttp($base . '/mcp', ['jsonrpc' => '2.0', 'id' => ++$id, 'method' => $method, 'params' => $params],
|
||
['Authorization' => 'Bearer ' . $token, 'Accept' => 'application/json, text/event-stream', 'X-Xingzhi-Task-Id' => 'logistics-task']);
|
||
aiMcpLogisticsExpect($status === 200 && isset($body['result']), "{$method} should return a result, got HTTP {$status}: " . json_encode($body, JSON_UNESCAPED_UNICODE));
|
||
return $body['result'];
|
||
};
|
||
$call = static fn (string $token, string $name, array $args): array => $rpc($token, 'tools/call', ['name' => $name, 'arguments' => $args]);
|
||
$ok = static function (string $token, string $name, array $args) use ($call): array {
|
||
$result = $call($token, $name, $args);
|
||
aiMcpLogisticsExpect(empty($result['isError']), "{$name} " . json_encode($args, JSON_UNESCAPED_UNICODE) . ' should succeed: ' . ($result['content'][0]['text'] ?? ''));
|
||
return [$result['content'][0]['text'] ?? '', (array) ($result['structuredContent'] ?? [])];
|
||
};
|
||
|
||
$asst = $grant('l_asst');
|
||
$noTrace = $grant('l_notrace');
|
||
|
||
// ---------------------------------------------------------------- 工具与中文名
|
||
$tools = array_column($rpc($asst, 'tools/list')['tools'] ?? [], null, 'name');
|
||
aiMcpLogisticsExpect(isset($tools['zyt_logistics_order'], $tools['zyt_logistics_patient']), 'logistics tools are listed for accounts that can see prescription orders');
|
||
aiMcpLogisticsExpect(($tools['zyt_logistics_order']['title'] ?? '') === '订单物流查询' && ($tools['zyt_logistics_order']['annotations']['title'] ?? '') === '订单物流查询'
|
||
&& ($tools['zyt_logistics_order']['annotations']['readOnlyHint'] ?? false) === true, 'tools carry a Chinese display title and stay read-only');
|
||
aiMcpLogisticsExpect(($tools['zyt_query']['annotations']['title'] ?? '') === '查询业务数据', 'every tool has a display title');
|
||
|
||
// ---------------------------------------------------------------- 按订单号:已同步的轨迹
|
||
[$text, $data] = $ok($asst, 'zyt_logistics_order', ['order_no' => 'LOGT1']);
|
||
$order = $data['orders'][0] ?? [];
|
||
$logistics = $order['logistics'] ?? [];
|
||
aiMcpLogisticsExpect(($data['found'] ?? 0) === 1 && ($order['order_no'] ?? '') === 'LOGT1' && ($order['fulfillment_text'] ?? '') === '已签收'
|
||
&& ($order['express_company'] ?? '') === '京东快递' && ($order['tracking_number'] ?? '') === 'JDVELOGT0001', 'order found with carrier and waybill: ' . json_encode($order, JSON_UNESCAPED_UNICODE));
|
||
aiMcpLogisticsExpect(($logistics['status'] ?? '') === '已签收' && ($logistics['delivered'] ?? null) === true && ($logistics['signed_at'] ?? '') === '2031-03-03 09:30:00'
|
||
&& ($logistics['trace_count'] ?? 0) === 3 && ($logistics['traces'][0]['time'] ?? '') === '2031-03-03 09:30:00', 'stored trace, newest first, sign time: ' . json_encode($logistics, JSON_UNESCAPED_UNICODE));
|
||
aiMcpLogisticsExpect(($logistics['source'] ?? '') === '京东物流官方接口' && ($logistics['updated_at'] ?? '') === '2031-03-03 10:00', 'source and last sync time are reported');
|
||
aiMcpLogisticsExpect(str_contains($text, '签收时间 2031-03-03 09:30:00') && str_contains($text, '数据更新于 2031-03-03 10:00'), 'summary line: ' . $text);
|
||
$all = $text . json_encode($data, JSON_UNESCAPED_UNICODE);
|
||
aiMcpLogisticsExpect(!str_contains($all, '13812345678') && str_contains($all, '138****5678'), 'phone numbers inside trace text are masked');
|
||
aiMcpLogisticsExpect(($order['recipient_phone'] ?? '') === '139****0001' && ($order['recipient_region'] ?? '') === '河南省洛阳市洛龙区' && !str_contains($all, '开元大道'),
|
||
'recipient phone masked; only the region, never the street address');
|
||
aiMcpLogisticsExpect(str_contains((string) ($order['tracking_page'] ?? ''), 'jdl.com') && str_contains((string) ($order['tracking_page'] ?? ''), 'JDVELOGT0001'), 'official tracking page link');
|
||
|
||
// ---------------------------------------------------------------- 按快递单号;范围之外;未填单号;参数校验
|
||
[, $data] = $ok($asst, 'zyt_logistics_order', ['tracking_no' => 'SFLOGT0002', 'trace_limit' => 1]);
|
||
$logistics = $data['orders'][0]['logistics'] ?? [];
|
||
aiMcpLogisticsExpect(($data['orders'][0]['order_no'] ?? '') === 'LOGT2' && ($logistics['status'] ?? '') === '派件中' && ($logistics['delivered'] ?? null) === false
|
||
&& count($logistics['traces'] ?? []) === 1 && ($logistics['trace_count'] ?? 0) === 2, 'lookup by waybill, trace_limit respected: ' . json_encode($logistics, JSON_UNESCAPED_UNICODE));
|
||
foreach ([['order_no' => 'LOGT5'], ['tracking_no' => 'JDVELOGT0005']] as $args) {
|
||
[$text, $data] = $ok($asst, 'zyt_logistics_order', $args);
|
||
aiMcpLogisticsExpect(($data['found'] ?? -1) === 0 && str_contains($text, '可见范围') && !str_contains($text, '别人订单的轨迹'), 'orders outside the list scope are not found: ' . $text);
|
||
}
|
||
[, $data] = $ok($asst, 'zyt_logistics_order', ['order_no' => 'LOGT3']);
|
||
aiMcpLogisticsExpect(($data['orders'][0]['logistics']['status'] ?? '') === '还没有快递单号', 'order without a waybill');
|
||
foreach ([[], ['order_no' => "LOGT1' OR 1=1"]] as $args) {
|
||
$result = $call($asst, 'zyt_logistics_order', $args);
|
||
aiMcpLogisticsExpect(!empty($result['isError']), 'bad arguments are refused: ' . json_encode($args, JSON_UNESCAPED_UNICODE));
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 没有「物流轨迹」权限
|
||
[$text, $data] = $ok($noTrace, 'zyt_logistics_order', ['order_no' => 'LOGT6']);
|
||
aiMcpLogisticsExpect(($data['orders'][0]['logistics']['status'] ?? '') === '无权查看物流轨迹' && !isset($data['orders'][0]['logistics']['traces'])
|
||
&& !str_contains($text . json_encode($data, JSON_UNESCAPED_UNICODE), '无权限账号订单的轨迹'), 'no traces without the logistics-trace permission');
|
||
|
||
// ---------------------------------------------------------------- 库里没有轨迹:实时查询(外部接口已关闭)不写库
|
||
$liveCalls = 0;
|
||
[, $data] = $ok($asst, 'zyt_logistics_order', ['order_no' => 'LOGT4', 'live' => false]);
|
||
aiMcpLogisticsExpect(($data['orders'][0]['logistics']['status'] ?? '') === '系统里暂无这张运单的物流记录', 'live=false keeps to stored data');
|
||
[, $data] = $ok($asst, 'zyt_logistics_order', ['order_no' => 'LOGT4']);
|
||
$liveCalls++;
|
||
$logistics = $data['orders'][0]['logistics'] ?? [];
|
||
aiMcpLogisticsExpect(($logistics['status'] ?? '') === '快递公司暂无轨迹' && ($logistics['source'] ?? '') === '实时查询' && str_contains((string) ($logistics['note'] ?? ''), '快递100'),
|
||
'live query path ran (carrier APIs disabled in this test): ' . json_encode($logistics, JSON_UNESCAPED_UNICODE));
|
||
aiMcpLogisticsExpect(Db::name('express_tracking')->where('tracking_number', 'YTLOGT0004')->count() === 0
|
||
&& Db::name('express_query_log')->where('tracking_number', 'YTLOGT0004')->count() === 0, 'the live query writes nothing');
|
||
|
||
// ---------------------------------------------------------------- 按患者
|
||
[$text, $data] = $ok($asst, 'zyt_logistics_patient', ['patient' => '物流甲']);
|
||
aiMcpLogisticsExpect(count($data['patients'] ?? []) === 2 && !isset($data['latest_shipment']) && str_contains($text, '有 2 位患者匹配'), 'same-name patients are listed separately: ' . $text);
|
||
aiMcpLogisticsExpect(!str_contains($text . json_encode($data, JSON_UNESCAPED_UNICODE), '物流乙'), 'other assistants\' patients stay out');
|
||
[$text, $data] = $ok($asst, 'zyt_logistics_patient', ['patient_id' => 96101]);
|
||
$group = $data['patients'][0] ?? [];
|
||
aiMcpLogisticsExpect(count($data['patients'] ?? []) === 1 && ($group['patient_name'] ?? '') === '物流甲' && ($group['phone'] ?? '') === '139****0001'
|
||
&& array_column($group['orders'] ?? [], 'order_no') === ['LOGT4', 'LOGT2', 'LOGT1'], 'one patient, newest orders first, phone masked: ' . json_encode($group, JSON_UNESCAPED_UNICODE));
|
||
$byNo = array_column($group['orders'], null, 'order_no');
|
||
aiMcpLogisticsExpect(($byNo['LOGT2']['logistics']['status'] ?? '') === '派件中' && str_contains((string) ($byNo['LOGT2']['logistics']['latest'] ?? ''), '139****1111')
|
||
&& ($byNo['LOGT1']['logistics']['delivered'] ?? null) === true, 'each order carries its latest stored event');
|
||
aiMcpLogisticsExpect(($data['latest_shipment']['order_no'] ?? '') === 'LOGT4' && ($data['latest_shipment']['logistics']['source'] ?? '') === '实时查询'
|
||
&& str_contains($text, '最近一单的物流'), 'full trace for the latest shipped order');
|
||
$liveCalls++;
|
||
[, $data] = $ok($asst, 'zyt_logistics_patient', ['patient' => '物流甲', 'start_date' => '2031-03-05', 'end_date' => '2031-03-31']);
|
||
$inRange = array_column(array_merge(...array_column($data['patients'] ?? [], 'orders')), 'order_no');
|
||
sort($inRange);
|
||
aiMcpLogisticsExpect($inRange === ['LOGT2', 'LOGT3', 'LOGT4'], 'date range filters by order creation date: ' . json_encode($inRange));
|
||
$result = $call($asst, 'zyt_logistics_patient', ['patient' => '物流甲', 'start_date' => '2031/03/05']);
|
||
aiMcpLogisticsExpect(!empty($result['isError']), 'dates must be YYYY-MM-DD');
|
||
|
||
// ---------------------------------------------------------------- 实时查询每小时上限(默认 30 次)
|
||
$limited = false;
|
||
for ($i = $liveCalls; $i <= 31; $i++) {
|
||
[, $data] = $ok($asst, 'zyt_logistics_order', ['order_no' => 'LOGT4']);
|
||
if (str_contains((string) ($data['orders'][0]['logistics']['note'] ?? ''), '次数已用完')) {
|
||
$limited = $i === 30;
|
||
break;
|
||
}
|
||
}
|
||
aiMcpLogisticsExpect($limited, 'the 31st live query in an hour is refused');
|
||
|
||
// ---------------------------------------------------------------- 审计
|
||
$log = Db::name('ai_access_log')->where(['admin_id' => 93101, 'tool' => 'zyt_logistics_order'])->where('record_ids', (string) $orders['LOGT1'])->find();
|
||
aiMcpLogisticsExpect($log && $log['status'] === 'ok' && $log['resource'] === 'tcm.prescriptionOrder/lists' && $log['client_task_id'] === 'logistics-task', 'audited with the order id and 行知 task id');
|
||
|
||
echo "AiMcpLogisticsTest OK\n";
|