This commit is contained in:
Your Name
2026-10-08 15:49:53 +08:00
parent 1c9b093e08
commit 67c374d899
52 changed files with 51959 additions and 75 deletions
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace app\common\service {
function config(string $name): array
{
return $GLOBALS['clinicalChatConfig'];
}
function curl_init(): \stdClass
{
return new \stdClass();
}
function curl_setopt(\stdClass $handle, int $option, mixed $value): bool
{
$handle->options[$option] = $value;
return true;
}
function curl_exec(\stdClass $handle): string|bool
{
$GLOBALS['clinicalChatPayloads'][] = json_decode($handle->options[CURLOPT_POSTFIELDS], true);
if (isset($handle->options[CURLOPT_WRITEFUNCTION])) {
$write = $handle->options[CURLOPT_WRITEFUNCTION];
$write($handle, "data: {\"choices\":[{\"delta\":{\"content\":\"offline stream\"}}]}\n\n");
return true;
}
return '{"choices":[{"message":{"content":"offline reply"}}]}';
}
function curl_errno(\stdClass $handle): int { return 0; }
function curl_error(\stdClass $handle): string { return ''; }
function curl_getinfo(\stdClass $handle, int $option): int { return 200; }
function curl_close(\stdClass $handle): void {}
}
namespace {
require dirname(__DIR__) . '/vendor/autoload.php';
use app\common\service\AiChatService;
use app\common\service\NihaixiaClinicalSkill;
function chatSkillExpect(bool $condition, string $message): void
{
if (!$condition) {
fwrite(STDERR, "FAIL: {$message}\n");
exit(1);
}
}
$clinicalChatConfig = [
'enable' => true,
'api_key' => 'offline-fixture',
'base_url' => 'https://ai.example.test',
'model' => 'offline-model',
];
$clinicalChatPayloads = [];
$messages = [
['role' => 'system', 'content' => '只输出 JSON'],
['role' => 'user', 'content' => '分析该患者症状'],
];
$blocking = AiChatService::chat($messages);
chatSkillExpect($blocking['ok'] === true, 'blocking chat succeeds offline');
$deltas = [];
$streaming = AiChatService::streamChat($messages, static function (string $delta) use (&$deltas): void {
$deltas[] = $delta;
});
chatSkillExpect($streaming['ok'] === true && $deltas === ['offline stream'], 'streaming chat succeeds offline');
chatSkillExpect(count($clinicalChatPayloads) === 2, 'both transports sent one request');
foreach ($clinicalChatPayloads as $payload) {
$wireMessages = $payload['messages'];
chatSkillExpect(str_contains($wireMessages[0]['content'], NihaixiaClinicalSkill::VERSION), 'wire request includes the clinical skill');
chatSkillExpect($wireMessages[1] === $messages[0] && $wireMessages[2] === $messages[1], 'original format and patient question are preserved');
}
echo "AiChat clinical skill transport contract passed.\n";
}
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
use app\common\service\NihaixiaClinicalSkill;
function skillExpect(bool $condition, string $message): void
{
if (!$condition) {
fwrite(STDERR, "FAIL: {$message}\n");
exit(1);
}
}
$reference = NihaixiaClinicalSkill::reference();
skillExpect(str_contains($reference, NihaixiaClinicalSkill::VERSION), 'pinned skill version is present');
skillExpect(str_contains($reference, '六经主证:太阳:'), 'reference is read from the installed six-meridian table');
skillExpect(str_contains($reference, '条件线索:太阳病(脉浮 + 头项强痛 + 恶寒)'), 'clinical clues are read from the installed diagnostic formulas');
skillExpect(str_contains($reference, '辨证步骤:定表里→分阴阳→辨寒热'), 'seven-step method is read from the installed skill');
skillExpect(str_contains($reference, '未满足关键证据时不得强行归经'), 'diagnostic clues require patient evidence');
skillExpect(str_contains($reference, '不得据此建议停用现有治疗'), 'historical cases cannot override treatment safety');
skillExpect(strlen($reference) < 3000, 'reference is bounded for staged clinical prompts');
$question = '请分析当前患者的口渴和高血糖。';
$query = NihaixiaClinicalSkill::augmentQuery($question);
skillExpect(str_contains($query, $question), 'original clinical question is preserved');
skillExpect(substr_count($query, NihaixiaClinicalSkill::VERSION) === 1, 'query receives the skill once');
skillExpect(NihaixiaClinicalSkill::augmentQuery($query) === $query, 'query retries do not duplicate the skill');
$messages = [
['role' => 'system', 'content' => '只输出 JSON'],
['role' => 'user', 'content' => $question],
];
$augmented = NihaixiaClinicalSkill::augmentMessages($messages);
skillExpect(count($augmented) === 3, 'chat messages receive one skill system reference');
skillExpect(($augmented[1]['content'] ?? '') === '只输出 JSON', 'original system format is preserved');
skillExpect(($augmented[2]['content'] ?? '') === $question, 'original user question is preserved');
skillExpect(NihaixiaClinicalSkill::augmentMessages($augmented) === $augmented, 'stream retries do not duplicate the skill');
echo "Nihaixia clinical skill contract passed.\n";
@@ -37,6 +37,7 @@ namespace {
require dirname(__DIR__) . '/vendor/autoload.php';
use app\common\service\DifyChatService;
use app\common\service\NihaixiaClinicalSkill;
function expectSame($expected, $actual, string $message): void
{
@@ -210,6 +211,9 @@ foreach (['dify' => 'chat-messages', 'openai' => 'chat/completions'] as $protoco
expectSame(true, $strictResult['ok'], 'strict ' . $protocol . ' accepts distinct logical attachments sharing a URL');
expectSame(1, count($upstreamTestRequests), 'strict ' . $protocol . ' submits the complete batch once');
$payload = $upstreamTestRequests[0]['payload'];
$wirePrompt = $protocol === 'dify' ? $payload['query'] : $payload['messages'][0]['content'][0]['text'];
expectSame(true, str_contains($wirePrompt, NihaixiaClinicalSkill::VERSION), 'strict ' . $protocol . ' includes the clinical skill on the wire');
expectSame(true, str_contains($wirePrompt, 'offline query'), 'strict ' . $protocol . ' preserves the clinical question');
$wireUrls = $protocol === 'dify' ? array_column($payload['files'], 'url')
: array_column(array_column(array_slice($payload['messages'][0]['content'], 1), 'image_url'), 'url');
expectSame(array_column($duplicateFiles, 'url'), $wireUrls, 'strict ' . $protocol . ' transmits every attachment in manifest order');