Compare commits
8
Commits
27fbef9321
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbf474ddd7 | ||
|
|
bbe3e870a1 | ||
|
|
caab2cabe3 | ||
|
|
3c7b88bb15 | ||
|
|
36975c6c1b | ||
|
|
6325ba88ff | ||
|
|
f8b6196205 | ||
|
|
aa0d22bbe2 |
@@ -467,7 +467,7 @@ export function prescriptionOrderEditTime(params: { id: number; create_time: str
|
||||
return request.post({ url: '/tcm.prescriptionOrder/editTime', params })
|
||||
}
|
||||
|
||||
/** 仅修改业务订单的承运商与快递单号;所有履约状态均可使用 */
|
||||
/** 仅修改业务订单的承运商与快递单号;处方和支付单均审核通过后,所有履约状态均可使用 */
|
||||
export function prescriptionOrderDdcode(params: {
|
||||
id: number
|
||||
express_company: string
|
||||
|
||||
@@ -45,3 +45,16 @@ export function unbindWorkWechat() {
|
||||
export function changeFirstPassword(params: { password: string; password_confirm: string }) {
|
||||
return request.post({ url: '/login/changeFirstPassword', params })
|
||||
}
|
||||
|
||||
// 统一账号登录开关和服务器生成的固定登录入口
|
||||
export function getIamConfig() {
|
||||
return request.get({ url: '/iam/config' }, { withToken: false })
|
||||
}
|
||||
|
||||
// 浏览器绑定的一次性兑换码;不重试,也不将旧业务 token 带入认证
|
||||
export function iamLogin(ticket: string) {
|
||||
return request.post(
|
||||
{ url: '/iam/exchange', params: { ticket, terminal: config.terminal }, withCredentials: true },
|
||||
{ withToken: false, isOpenRetry: false }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
import { getUserInfo, login, logout, workWechatLogin } from '@/api/user'
|
||||
import { getUserInfo, iamLogin, login, logout, workWechatLogin } from '@/api/user'
|
||||
import { TOKEN_KEY } from '@/enums/cacheEnums'
|
||||
import { PageEnum } from '@/enums/pageEnum'
|
||||
import router, { filterAsyncRoutes } from '@/router'
|
||||
@@ -83,6 +83,13 @@ const useUserStore = defineStore({
|
||||
})
|
||||
})
|
||||
},
|
||||
async iamLogin(ticket: string) {
|
||||
const data = await iamLogin(ticket)
|
||||
this.token = data.token
|
||||
this.isPaw = data.is_paw ?? 1
|
||||
cache.set(TOKEN_KEY, data.token)
|
||||
return data
|
||||
},
|
||||
getUserInfo() {
|
||||
return new Promise((resolve, reject) => {
|
||||
getUserInfo()
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
<div class="text-center text-3xl font-medium mb-8">{{ config.web_name }}</div>
|
||||
|
||||
<!-- 企业微信自动授权中 -->
|
||||
<div v-if="wxWorkAutoLogin" class="text-center py-10">
|
||||
<div v-if="wxWorkAutoLogin || iamLoading" class="text-center py-10">
|
||||
<el-icon class="is-loading mb-4" :size="40" color="var(--el-color-primary)">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<div class="text-gray-500">企业微信授权登录中...</div>
|
||||
<div class="text-gray-500">{{ iamLoading ? '统一账号登录中...' : '企业微信授权登录中...' }}</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
@@ -78,6 +78,20 @@
|
||||
请使用企业微信扫描二维码登录
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<section v-if="iamEnabled" class="iam-login-alternative" aria-label="其他登录方式">
|
||||
<div class="iam-login-divider" aria-hidden="true">其他登录方式</div>
|
||||
<el-button class="iam-login-entry" size="large" @click="handleIamLogin">
|
||||
<span class="iam-login-entry__content">
|
||||
<icon name="local-icon-anquan" size="22" />
|
||||
<span>统一账号快捷登录</span>
|
||||
</span>
|
||||
<span class="iam-login-entry__arrow" aria-hidden="true">
|
||||
<icon name="el-icon-ArrowRight" size="16" />
|
||||
</span>
|
||||
</el-button>
|
||||
<p class="iam-login-hint">使用统一身份平台账号登录</p>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -99,7 +113,7 @@ import LayoutFooter from '@/layout/components/footer.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import cache from '@/utils/cache'
|
||||
import { getWorkWechatConfig } from '@/api/user'
|
||||
import { getIamConfig, getWorkWechatConfig } from '@/api/user'
|
||||
|
||||
const passwordRef = shallowRef<InputInstance>()
|
||||
const formRef = shallowRef<FormInstance>()
|
||||
@@ -118,6 +132,66 @@ const rules = {
|
||||
password: [{ required: true, message: '请输入密码', trigger: ['blur'] }]
|
||||
}
|
||||
|
||||
// 统一账号登录是可选入口,不取代账号密码或企业微信登录。
|
||||
const iamEnabled = ref(false)
|
||||
const iamLoginUrl = ref('')
|
||||
const iamLoading = ref(false)
|
||||
let iamCallbackHandled = false
|
||||
|
||||
const loadIamConfig = async () => {
|
||||
try {
|
||||
const result = await getIamConfig()
|
||||
const url = new URL(result?.loginUrl || '', window.location.origin)
|
||||
if (result?.enabled === true && url.protocol === 'https:') {
|
||||
iamLoginUrl.value = url.href
|
||||
iamEnabled.value = true
|
||||
}
|
||||
} catch {
|
||||
// IAM 不可用时,原有登录入口保持可用。
|
||||
}
|
||||
}
|
||||
|
||||
const handleIamLogin = () => {
|
||||
if (iamEnabled.value && iamLoginUrl.value) {
|
||||
window.location.assign(iamLoginUrl.value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleIamCallback = async (ticket: string | null, error: string | null) => {
|
||||
if (iamCallbackHandled) return
|
||||
iamCallbackHandled = true
|
||||
// 在任何 await 和兑换前移除票据,刷新不会重复兑换;保留其他 query/hash。
|
||||
const cleanUrl = new URL(window.location.href)
|
||||
cleanUrl.searchParams.delete('iam_ticket')
|
||||
cleanUrl.searchParams.delete('iam_error')
|
||||
// 同一回调不得随后被识别为企业微信授权。
|
||||
cleanUrl.searchParams.delete('code')
|
||||
cleanUrl.searchParams.delete('state')
|
||||
window.history.replaceState(window.history.state, '', cleanUrl.pathname + cleanUrl.search + cleanUrl.hash)
|
||||
if (error || !ticket) {
|
||||
ElMessage.error(error || '统一账号登录凭证无效,请重新登录')
|
||||
return
|
||||
}
|
||||
iamLoading.value = true
|
||||
try {
|
||||
const result = await userStore.iamLogin(ticket)
|
||||
if (result.is_paw === 0) {
|
||||
await router.push('/change-password')
|
||||
return
|
||||
}
|
||||
if (result.need_bind_work_wechat) {
|
||||
await router.push('/bind-work-wechat')
|
||||
return
|
||||
}
|
||||
redirectAfterLogin()
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error?.msg || error?.message || '统一账号登录失败,请重新登录或使用账号密码')
|
||||
loginMode.value = 'account'
|
||||
} finally {
|
||||
iamLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 企业微信相关
|
||||
const loginMode = ref<'account' | 'wxwork'>('account')
|
||||
const wxWorkEnabled = ref(false)
|
||||
@@ -243,7 +317,12 @@ onMounted(async () => {
|
||||
|
||||
// 检查 URL 中是否有企业微信回调 code
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const wxCode = urlParams.get('code')
|
||||
void loadIamConfig()
|
||||
const hasIamCallback = urlParams.has('iam_ticket') || urlParams.has('iam_error')
|
||||
if (hasIamCallback) {
|
||||
await handleIamCallback(urlParams.get('iam_ticket'), urlParams.get('iam_error'))
|
||||
}
|
||||
const wxCode = hasIamCallback ? null : urlParams.get('code')
|
||||
const wxState = urlParams.get('state')
|
||||
|
||||
if (wxCode && wxState === 'admin_login') {
|
||||
@@ -263,7 +342,7 @@ onMounted(async () => {
|
||||
wxWorkConfig.value = { corp_id: res.corp_id, agent_id: res.agent_id }
|
||||
|
||||
// 在企业微信内:自动跳转 OAuth 授权
|
||||
if (isInWxWork()) {
|
||||
if (isInWxWork() && !hasIamCallback) {
|
||||
const redirectUri = encodeURIComponent(getRedirectUri())
|
||||
const authUrl =
|
||||
`https://open.weixin.qq.com/connect/oauth2/authorize` +
|
||||
@@ -286,6 +365,82 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.iam-login-alternative {
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.iam-login-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 18px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--el-border-color-light);
|
||||
}
|
||||
}
|
||||
|
||||
.iam-login-entry.el-button {
|
||||
--el-button-bg-color: var(--el-color-primary-light-9);
|
||||
--el-button-text-color: var(--el-color-primary);
|
||||
--el-button-border-color: var(--el-color-primary-light-5);
|
||||
--el-button-hover-bg-color: var(--el-color-primary-light-8);
|
||||
--el-button-hover-text-color: var(--el-color-primary);
|
||||
--el-button-hover-border-color: var(--el-color-primary);
|
||||
--el-button-active-bg-color: var(--el-color-primary-light-8);
|
||||
--el-button-active-border-color: var(--el-color-primary);
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
padding: 12px 34px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
transition: background-color 150ms ease, border-color 150ms ease;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--el-color-primary);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.iam-login-entry__content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.iam-login-entry__arrow {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.iam-login-hint {
|
||||
margin: 14px 0 0;
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.iam-login-entry.el-button {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.login {
|
||||
background-image: url('./images/login_bg.png');
|
||||
@apply min-h-screen bg-no-repeat bg-center bg-cover;
|
||||
|
||||
@@ -871,8 +871,17 @@
|
||||
<el-option label="京东快递" value="jd" />
|
||||
<el-option label="极兔速递" value="jt" />
|
||||
</el-select>
|
||||
<el-input v-model="editForm.tracking_number" maxlength="80" placeholder="快递单号" class="flex-1 min-w-0" />
|
||||
</div>
|
||||
<el-input
|
||||
v-model="editForm.tracking_number"
|
||||
disabled
|
||||
maxlength="80"
|
||||
placeholder="快递单号"
|
||||
class="flex-1 min-w-0"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
编辑订单后需重新审核;双审通过后,请通过列表的单号操作填写或修改
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -1067,8 +1076,17 @@
|
||||
<el-option label="京东快递" value="jd" />
|
||||
<el-option label="极兔速递" value="jt" />
|
||||
</el-select>
|
||||
<el-input v-model="editForm.tracking_number" maxlength="80" placeholder="快递单号" class="flex-1 min-w-0" />
|
||||
</div>
|
||||
<el-input
|
||||
v-model="editForm.tracking_number"
|
||||
disabled
|
||||
maxlength="80"
|
||||
placeholder="快递单号"
|
||||
class="flex-1 min-w-0"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
编辑订单后需重新审核;双审通过后,请通过列表的单号操作填写或修改
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
@@ -2975,9 +2993,13 @@ function canWithdrawRow(row: { fulfillment_status?: number }) {
|
||||
return Number(row.fulfillment_status) === 1
|
||||
}
|
||||
|
||||
function canShipRow(row: { fulfillment_status?: number }) {
|
||||
// 履约中(2) 可发货
|
||||
return Number(row.fulfillment_status) === 2
|
||||
function canShipRow(row: {
|
||||
fulfillment_status?: number
|
||||
prescription_audit_status?: number
|
||||
payment_slip_audit_status?: number
|
||||
}) {
|
||||
// 履约中(2) 且处方、支付单均审核通过才可发货
|
||||
return Number(row.fulfillment_status) === 2 && isDualAuditPassed(row)
|
||||
}
|
||||
|
||||
type ShipMode = 'gancao' | 'direct'
|
||||
@@ -3073,8 +3095,11 @@ function canRefundRow(row: { fulfillment_status?: number; payment_slip_audit_sta
|
||||
return (fs === 5 || fs === 6 || fs === 3 || fs === 9) && Number(row.payment_slip_audit_status) === 1
|
||||
}
|
||||
|
||||
function canQuickTrackRow(_row: { fulfillment_status?: number }) {
|
||||
return true
|
||||
function canQuickTrackRow(row: {
|
||||
prescription_audit_status?: number
|
||||
payment_slip_audit_status?: number
|
||||
}) {
|
||||
return isDualAuditPassed(row)
|
||||
}
|
||||
|
||||
function canUploadPharmacyRow(row: {
|
||||
@@ -3270,7 +3295,7 @@ const editSaving = ref(false)
|
||||
/** 编辑订单分步:0 收货 / 1 服务与支付单 / 2 金额与确认(与处方列表「创建业务订单」一致) */
|
||||
const editOrderStep = ref(0)
|
||||
/** 顶部「关联处方」卡片数据(来自业务订单详情中的 prescription) */
|
||||
const editOrderPrescription = ref<Record<string, any> | null>(null)
|
||||
const editOrderPrescription = ref<Record<string, any> | null>(null)
|
||||
/** 甘草 SCM 已提交:弹窗仅展示并提交快递单号与承运商 */
|
||||
const editGancaoLogisticsOnlyMode = ref(false)
|
||||
/** 用于提示文案展示甘草处方单号 */
|
||||
@@ -3455,7 +3480,7 @@ const editRules = computed<FormRules>(() => {
|
||||
return rules
|
||||
})
|
||||
|
||||
function resetEditOrderDialog() {
|
||||
function resetEditOrderDialog() {
|
||||
editOrderStep.value = 0
|
||||
editOrderPrescription.value = null
|
||||
editGancaoLogisticsOnlyMode.value = false
|
||||
@@ -3507,8 +3532,8 @@ async function openEdit(row: {
|
||||
}
|
||||
editOrderStep.value = 0
|
||||
editOrderPrescription.value = null
|
||||
editVisible.value = true
|
||||
editDialogLoading.value = true
|
||||
editVisible.value = true
|
||||
editDialogLoading.value = true
|
||||
try {
|
||||
const res: any = await prescriptionOrderDetail({ id: row.id })
|
||||
const d = res?.data ?? res
|
||||
@@ -3531,7 +3556,7 @@ async function openEdit(row: {
|
||||
editOrderPrescription.value = null
|
||||
}
|
||||
|
||||
editForm.id = d.id
|
||||
editForm.id = d.id
|
||||
editForm.prescription_id = Number(d.prescription_id) || 0
|
||||
editForm.diagnosis_id = Number(d.diagnosis_id) || 0
|
||||
editForm.assistant_id = Number(d.assistant_id) || 0
|
||||
@@ -3627,7 +3652,6 @@ async function submitEdit() {
|
||||
service_channel: editForm.service_channel || '',
|
||||
service_package: Array.isArray(editForm.service_package) ? editForm.service_package.join(',') : '',
|
||||
express_company: editForm.express_company || 'auto',
|
||||
tracking_number: editForm.tracking_number || '',
|
||||
fee_type: editForm.fee_type,
|
||||
amount: editForm.amount,
|
||||
remark_extra: editForm.remark_extra || '',
|
||||
@@ -3915,7 +3939,17 @@ const quickTrackForm = reactive({
|
||||
tracking_number: ''
|
||||
})
|
||||
|
||||
function openQuickTrack(row: { id: number; express_company?: unknown; tracking_number?: unknown }) {
|
||||
function openQuickTrack(row: {
|
||||
id: number
|
||||
express_company?: unknown
|
||||
tracking_number?: unknown
|
||||
prescription_audit_status?: number
|
||||
payment_slip_audit_status?: number
|
||||
}) {
|
||||
if (!canQuickTrackRow(row)) {
|
||||
feedback.msgWarning('处方审核和支付单审核均通过后,才可填写或修改快递单号')
|
||||
return
|
||||
}
|
||||
quickTrackRowId.value = row.id
|
||||
quickTrackForm.express_company = String(row.express_company || 'auto') || 'auto'
|
||||
quickTrackForm.tracking_number = String(row.tracking_number || '')
|
||||
@@ -3963,7 +3997,19 @@ function resolveShipModeForRow(row: { id: number; ship_mode?: unknown }) {
|
||||
return normalizeShipMode(row.ship_mode)
|
||||
}
|
||||
|
||||
function openShip(row: { id: number; express_company?: unknown; tracking_number?: unknown; ship_mode?: unknown }) {
|
||||
function openShip(row: {
|
||||
id: number
|
||||
express_company?: unknown
|
||||
tracking_number?: unknown
|
||||
ship_mode?: unknown
|
||||
fulfillment_status?: number
|
||||
prescription_audit_status?: number
|
||||
payment_slip_audit_status?: number
|
||||
}) {
|
||||
if (!canShipRow(row)) {
|
||||
feedback.msgWarning('仅处方审核和支付单审核均通过的履约中订单可填写单号并发货')
|
||||
return
|
||||
}
|
||||
shipRowId.value = Number(row.id)
|
||||
shipForm.ship_mode = resolveShipModeForRow(row)
|
||||
shipForm.express_company = String(row.express_company || 'auto') || 'auto'
|
||||
|
||||
@@ -1624,8 +1624,17 @@
|
||||
<el-option label="京东快递" value="jd" />
|
||||
<el-option label="极兔速递" value="jt" />
|
||||
</el-select>
|
||||
<el-input v-model="editForm.tracking_number" maxlength="80" placeholder="快递单号" class="flex-1 min-w-0" />
|
||||
</div>
|
||||
<el-input
|
||||
v-model="editForm.tracking_number"
|
||||
disabled
|
||||
maxlength="80"
|
||||
placeholder="快递单号"
|
||||
class="flex-1 min-w-0"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 mt-1">
|
||||
编辑订单后需重新审核;双审通过后,请通过列表的单号操作填写或修改
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
@@ -3422,9 +3431,13 @@ function canWithdrawRow(row: { fulfillment_status?: number }) {
|
||||
return Number(row.fulfillment_status) === 1
|
||||
}
|
||||
|
||||
function canShipRow(row: { fulfillment_status?: number }) {
|
||||
// 履约中(2) 可发货
|
||||
return Number(row.fulfillment_status) === 2
|
||||
function canShipRow(row: {
|
||||
fulfillment_status?: number
|
||||
prescription_audit_status?: number
|
||||
payment_slip_audit_status?: number
|
||||
}) {
|
||||
// 履约中(2) 且处方、支付单均审核通过才可发货
|
||||
return Number(row.fulfillment_status) === 2 && isDualAuditPassed(row)
|
||||
}
|
||||
|
||||
function canAddPayOrderRow(row: { fulfillment_status?: number }) {
|
||||
@@ -3444,8 +3457,11 @@ function canRefundRow(row: { fulfillment_status?: number; payment_slip_audit_sta
|
||||
return (fs === 5 || fs === 6 || fs === 3 || fs === 9) && Number(row.payment_slip_audit_status) === 1
|
||||
}
|
||||
|
||||
function canQuickTrackRow(_row: { fulfillment_status?: number }) {
|
||||
return true
|
||||
function canQuickTrackRow(row: {
|
||||
prescription_audit_status?: number
|
||||
payment_slip_audit_status?: number
|
||||
}) {
|
||||
return isDualAuditPassed(row)
|
||||
}
|
||||
|
||||
function canUploadPharmacyRow(row: {
|
||||
@@ -4181,7 +4197,7 @@ const editSaving = ref(false)
|
||||
/** 编辑订单分步:0 收货 / 1 服务与支付单 / 2 金额与确认(与处方列表「创建业务订单」一致) */
|
||||
const editOrderStep = ref(0)
|
||||
/** 顶部「关联处方」卡片数据(来自业务订单详情中的 prescription) */
|
||||
const editOrderPrescription = ref<Record<string, any> | null>(null)
|
||||
const editOrderPrescription = ref<Record<string, any> | null>(null)
|
||||
|
||||
const editOrderStepLead = computed(() => {
|
||||
const texts = [
|
||||
@@ -4381,7 +4397,7 @@ const editRules = computed<FormRules>(() => {
|
||||
return rules
|
||||
})
|
||||
|
||||
function resetEditOrderDialog() {
|
||||
function resetEditOrderDialog() {
|
||||
editOrderStep.value = 0
|
||||
editOrderPrescription.value = null
|
||||
editFormRef.value?.clearValidate()
|
||||
@@ -4431,8 +4447,8 @@ async function openEdit(row: {
|
||||
}
|
||||
editOrderStep.value = 0
|
||||
editOrderPrescription.value = null
|
||||
editVisible.value = true
|
||||
editDialogLoading.value = true
|
||||
editVisible.value = true
|
||||
editDialogLoading.value = true
|
||||
try {
|
||||
const res: any = await prescriptionOrderDetail({ id: row.id })
|
||||
const d = res?.data ?? res
|
||||
@@ -4455,7 +4471,7 @@ async function openEdit(row: {
|
||||
editOrderPrescription.value = null
|
||||
}
|
||||
|
||||
editForm.id = d.id
|
||||
editForm.id = d.id
|
||||
editForm.prescription_id = Number(d.prescription_id) || 0
|
||||
editForm.diagnosis_id = Number(d.diagnosis_id) || 0
|
||||
editForm.assistant_id = Number(d.assistant_id) || 0
|
||||
@@ -4540,7 +4556,6 @@ async function submitEdit() {
|
||||
service_channel: editForm.service_channel || '',
|
||||
service_package: Array.isArray(editForm.service_package) ? editForm.service_package.join(',') : '',
|
||||
express_company: editForm.express_company || 'auto',
|
||||
tracking_number: editForm.tracking_number || '',
|
||||
fee_type: editForm.fee_type,
|
||||
amount: editForm.amount,
|
||||
remark_extra: editForm.remark_extra || '',
|
||||
@@ -4788,7 +4803,17 @@ const quickTrackForm = reactive({
|
||||
tracking_number: ''
|
||||
})
|
||||
|
||||
function openQuickTrack(row: { id: number; express_company?: unknown; tracking_number?: unknown }) {
|
||||
function openQuickTrack(row: {
|
||||
id: number
|
||||
express_company?: unknown
|
||||
tracking_number?: unknown
|
||||
prescription_audit_status?: number
|
||||
payment_slip_audit_status?: number
|
||||
}) {
|
||||
if (!canQuickTrackRow(row)) {
|
||||
feedback.msgWarning('处方审核和支付单审核均通过后,才可填写或修改快递单号')
|
||||
return
|
||||
}
|
||||
quickTrackRowId.value = row.id
|
||||
quickTrackForm.express_company = String(row.express_company || 'auto') || 'auto'
|
||||
quickTrackForm.tracking_number = String(row.tracking_number || '')
|
||||
@@ -4840,7 +4865,19 @@ const shipDialogModeDisplay = computed(() =>
|
||||
shipForm.ship_mode === 'direct' ? '洛阳药房直发' : '甘草药房直发'
|
||||
)
|
||||
|
||||
function openShip(row: { id: number; express_company?: unknown; tracking_number?: unknown; ship_mode?: unknown }) {
|
||||
function openShip(row: {
|
||||
id: number
|
||||
express_company?: unknown
|
||||
tracking_number?: unknown
|
||||
ship_mode?: unknown
|
||||
fulfillment_status?: number
|
||||
prescription_audit_status?: number
|
||||
payment_slip_audit_status?: number
|
||||
}) {
|
||||
if (!canShipRow(row)) {
|
||||
feedback.msgWarning('仅处方审核和支付单审核均通过的履约中订单可填写单号并发货')
|
||||
return
|
||||
}
|
||||
shipRowId.value = row.id
|
||||
shipForm.ship_mode = String(row.ship_mode || 'gancao') || 'gancao'
|
||||
shipForm.express_company = String(row.express_company || 'auto') || 'auto'
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const test = require('node:test')
|
||||
const ts = require('typescript')
|
||||
const vue = require('vue')
|
||||
const { parse, compileScript, compileTemplate } = require('@vue/compiler-sfc')
|
||||
|
||||
const handlers = [
|
||||
'isDualAuditPassed', 'canQuickTrackRow', 'canShipRow', 'openQuickTrack',
|
||||
'submitQuickTrack', 'openShip', 'openEdit', 'submitEdit', 'resetEditOrderDialog'
|
||||
]
|
||||
const stateNames = [
|
||||
'quickTrackVisible', 'quickTrackSaving', 'quickTrackRowId', 'quickTrackForm',
|
||||
'shipVisible', 'shipRowId', 'shipForm', 'editVisible', 'editDialogLoading',
|
||||
'editSaving', 'editOrderStep', 'editOrderPrescription',
|
||||
'editFormRef', 'editForm', 'editDepositMin'
|
||||
]
|
||||
|
||||
for (const page of ['order_list.vue', 'order_list_h5.vue']) {
|
||||
const filename = path.join(__dirname, '../src/views/consumer/prescription', page)
|
||||
const { descriptor, errors } = parse(fs.readFileSync(filename, 'utf8'), { filename })
|
||||
assert.deepEqual(errors, [])
|
||||
const script = compileScript(descriptor, { id: page })
|
||||
const ast = ts.createSourceFile(filename + '.ts', descriptor.scriptSetup.content, ts.ScriptTarget.Latest, true)
|
||||
// Execute the actual order handlers with only network and unrelated UI dependencies stubbed.
|
||||
const declarations = new Map()
|
||||
for (const node of ast.statements) {
|
||||
if (ts.isFunctionDeclaration(node) && node.name) declarations.set(node.name.text, node.getText(ast))
|
||||
if (ts.isVariableStatement(node)) {
|
||||
for (const declaration of node.declarationList.declarations) {
|
||||
declarations.set(declaration.name.getText(ast), `const ${declaration.getText(ast)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
const selected = [...handlers, ...stateNames]
|
||||
for (const name of selected) assert.ok(declarations.has(name), `${page}: missing ${name}`)
|
||||
const compiled = ts.transpileModule(selected.map(name => declarations.get(name)).join('\n'), {
|
||||
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
|
||||
}).outputText
|
||||
|
||||
function instance(detail = {}, saveTracking = async () => {}) {
|
||||
const warnings = []
|
||||
const errors = []
|
||||
const edits = []
|
||||
const tracks = []
|
||||
const globals = {
|
||||
ref: vue.ref, reactive: vue.reactive, nextTick: vue.nextTick,
|
||||
feedback: { msgWarning: message => warnings.push(message), msgError: message => errors.push(message), msgSuccess() {} },
|
||||
prescriptionOrderDetail: async () => ({ data: detail }),
|
||||
prescriptionOrderEdit: async payload => edits.push(payload),
|
||||
prescriptionOrderDdcode: async payload => { tracks.push(payload); await saveTracking(payload) },
|
||||
canEditRow: () => true,
|
||||
parseServicePackageValues: () => [],
|
||||
loadEditPaidOrders: async () => {},
|
||||
resolveShipModeForRow: row => row.ship_mode || 'gancao',
|
||||
editGancaoLogisticsOnlyMode: vue.ref(false),
|
||||
editGancaoDisplayNo: vue.ref(''),
|
||||
detailDrawerRef: vue.ref(null),
|
||||
detailVisible: vue.ref(false),
|
||||
getLists() {}
|
||||
}
|
||||
const state = new Function(...Object.keys(globals), `${compiled}\nreturn { ${selected.join(', ')} }`)(...Object.values(globals))
|
||||
state.editFormRef.value = { validate: async () => {}, clearValidate() {} }
|
||||
return { state, warnings, errors, edits, tracks }
|
||||
}
|
||||
|
||||
test(`${page}: only two approved audits unlock tracking, including legacy string statuses`, () => {
|
||||
const { state } = instance()
|
||||
for (const rx of [undefined, null, 0, 1, 2, '0', '1', '2']) {
|
||||
for (const pay of [undefined, null, 0, 1, 2, '0', '1', '2']) {
|
||||
const row = { prescription_audit_status: rx, payment_slip_audit_status: pay, fulfillment_status: 2 }
|
||||
const allowed = [1, '1'].includes(rx) && [1, '1'].includes(pay)
|
||||
assert.equal(state.canQuickTrackRow(row), allowed, `tracking: ${rx}/${pay}`)
|
||||
assert.equal(state.canShipRow(row), allowed, `shipping: ${rx}/${pay}`)
|
||||
}
|
||||
}
|
||||
for (const status of [1, 3, 4, 5, 6, 9]) {
|
||||
assert.equal(state.canShipRow({ prescription_audit_status: 1, payment_slip_audit_status: 1, fulfillment_status: status }), false)
|
||||
}
|
||||
})
|
||||
|
||||
test(`${page}: direct handler calls cannot open tracking or shipping before both approvals`, () => {
|
||||
for (const [rx, pay] of [[0, 0], [1, 0], [0, 1], [2, 1], [1, 2]]) {
|
||||
const { state, warnings } = instance()
|
||||
const row = { id: 42, prescription_audit_status: rx, payment_slip_audit_status: pay, fulfillment_status: 2 }
|
||||
state.openQuickTrack(row)
|
||||
state.openShip(row)
|
||||
assert.equal(state.quickTrackVisible.value, false)
|
||||
assert.equal(state.shipVisible.value, false)
|
||||
assert.equal(state.quickTrackRowId.value, 0)
|
||||
assert.equal(state.shipRowId.value, 0)
|
||||
assert.equal(warnings.length, 2)
|
||||
}
|
||||
})
|
||||
|
||||
test(`${page}: approved orders can fill or replace the number and preserve the carrier`, async () => {
|
||||
for (const oldNumber of ['', 'SF-OLD']) {
|
||||
const { state, tracks } = instance()
|
||||
const row = { id: 42, prescription_audit_status: 1, payment_slip_audit_status: 1, fulfillment_status: 2, express_company: 'sf', tracking_number: oldNumber }
|
||||
state.openQuickTrack(row)
|
||||
assert.equal(state.quickTrackVisible.value, true)
|
||||
assert.equal(state.quickTrackForm.tracking_number, oldNumber)
|
||||
state.quickTrackForm.tracking_number = ' SF-NEW '
|
||||
await state.submitQuickTrack()
|
||||
assert.deepEqual(tracks, [{ id: 42, express_company: 'sf', tracking_number: 'SF-NEW' }])
|
||||
assert.equal(state.quickTrackVisible.value, false)
|
||||
state.openShip(row)
|
||||
assert.equal(state.shipVisible.value, true)
|
||||
}
|
||||
})
|
||||
|
||||
test(`${page}: a server rejection after audit revocation retains the dialog and entered number`, async () => {
|
||||
const { state } = instance({}, async () => { throw new Error('audit revoked') })
|
||||
state.openQuickTrack({ id: 42, prescription_audit_status: 1, payment_slip_audit_status: 1 })
|
||||
state.quickTrackForm.tracking_number = 'SF-NEW'
|
||||
await state.submitQuickTrack()
|
||||
assert.equal(state.quickTrackVisible.value, true)
|
||||
assert.equal(state.quickTrackSaving.value, false)
|
||||
assert.equal(state.quickTrackForm.tracking_number, 'SF-NEW')
|
||||
})
|
||||
|
||||
test(`${page}: editing preserves the existing number while saving other fields before approval`, async () => {
|
||||
const { state, edits, errors } = instance({ id: 42, prescription_audit_status: 1, payment_slip_audit_status: 0, tracking_number: 'SF-EXISTING' })
|
||||
await state.openEdit({ id: 42, prescription_audit_status: 1, payment_slip_audit_status: 1 })
|
||||
assert.deepEqual(errors, [])
|
||||
assert.equal(state.editForm.tracking_number, 'SF-EXISTING')
|
||||
state.editForm.tracking_number = 'FORGED'
|
||||
state.editForm.recipient_name = '修改后的收货人'
|
||||
state.editOrderStep.value = 2
|
||||
await state.submitEdit()
|
||||
assert.equal(edits.length, 1)
|
||||
assert.equal(edits[0].recipient_name, '修改后的收货人')
|
||||
assert.equal(Object.hasOwn(edits[0], 'tracking_number'), false)
|
||||
})
|
||||
|
||||
test(`${page}: ordinary editing cannot change tracking while resetting an approved payment audit`, async () => {
|
||||
const { state, edits, errors } = instance({ id: 42, prescription_audit_status: '1', payment_slip_audit_status: '1' })
|
||||
await state.openEdit({ id: 42 })
|
||||
assert.deepEqual(errors, [])
|
||||
state.editForm.tracking_number = 'SF-NEW'
|
||||
state.editOrderStep.value = 2
|
||||
await state.submitEdit()
|
||||
assert.equal(edits.length, 1)
|
||||
assert.equal(Object.hasOwn(edits[0], 'tracking_number'), false)
|
||||
})
|
||||
|
||||
test(`${page}: template compiles with tracking controls bound to audit restrictions`, () => {
|
||||
const template = compileTemplate({ source: descriptor.template.content, filename, id: page, compilerOptions: { bindingMetadata: script.bindings } })
|
||||
assert.deepEqual(template.errors, [])
|
||||
const inputs = [...descriptor.template.content.matchAll(/<el-input\b[^>]*v-model="editForm\.tracking_number"[^>]*>/g)]
|
||||
assert.equal(inputs.length, page === 'order_list.vue' ? 2 : 1)
|
||||
for (const [input] of inputs) assert.match(input, /\sdisabled(?:\s|\/?>)/)
|
||||
assert.match(descriptor.template.content, /v-if="canQuickTrackRow\(row\)"/)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# 已开处方 → AI 界面改造
|
||||
|
||||
## 功能分析
|
||||
|
||||
这是基于已保存资料快照的双模型分析与医生复核工作台。主流程是确认患者和批次、查看模型完成情况、对照原方差异、核查资料缺口、记录所选模型的复核意见。
|
||||
|
||||
保留六个入口:对比总览、完整报告、候选与逐味、资料与缺口、处理进度、历史与趋势。历史批次切换、刷新、重新分析、单模型重试、独立复核草稿及保存继续使用现有服务和权限逻辑。医生原方及支持报告仍可访问。
|
||||
|
||||
## 设计决定
|
||||
|
||||
- 主程序 `shell.py` 将 prescriptions 等业务页列为科技蓝页面。因此继续复用 `reception_style.TECH_BLUE`:主色 #1769E8,背景 #F3F7FD,白色面板,分割线 #DBE5F2。没有采用主程序其他页面的靛蓝令牌,也没有修改全局主题。
|
||||
- 深蓝渐变横幅改为白色标题工具栏。批次状态放在顶行,患者及原方信息单独成行,当前导航用浅蓝背景与蓝色底线标识。
|
||||
- 缩小一致度圆环和数字,保留共同药味、候选药味与药味重合的计算依据;修复指标区样式误把分隔线应用到每个子标签的问题。
|
||||
- 总览由三栏改成剂量差异主区与固定复核侧栏。三方交集和附件覆盖改为可展开的摘要,减少初始屏幕空白和对主图的挤压。
|
||||
- 复核模型和复核状态并排显示,意见输入和保存按钮放在一起。顶部保存动作显示当前模型名,切换时同步更新。
|
||||
- 短窗口收起次要信息,保留剂量主图和复核输入;展开图表时剂量滚动区域可让出高度。列表内容保持最小高度,避免刷新后条目重叠。
|
||||
- 保留未知值与零值区分、不可比/历史状态处理、模型各自失败重试以及现有一致度说明。
|
||||
|
||||
## 验证
|
||||
|
||||
- 处方 AI 五组回归测试覆盖数据处理、权限、异步读取、逐味比较、页面、模型复核与布局。
|
||||
- 新增八种窗口/展开状态组合的控件边界检查,以及展开/收起不改变比较数据和复核清单的检查。
|
||||
- 离线模拟数据预览覆盖 1440×940、1280×860、1024×700、940×640,各内容页面、展开图表、运行中、失败、历史及统计窗口。
|
||||
- 预览输出:`app/artifacts/issued_prescription_redesign/`。模拟数据仅供 UI 验证,不对应真实患者。
|
||||
|
||||
本次改动在现有未提交工作区基础上完成,仅调整桌面呈现与相关回归检查;没有重新打包或发布桌面安装程序。
|
||||
@@ -0,0 +1,281 @@
|
||||
"""Render the prescription comparison window with synthetic, offline data."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from doctor_workstation.ui.dialogs import issued_prescription_ai as ai
|
||||
from doctor_workstation.ui.theme import apply_theme
|
||||
|
||||
|
||||
def example_batch() -> dict[str, Any]:
|
||||
"""Three prescriptions that actually differ, so every state of the page has something to draw."""
|
||||
|
||||
doctor = {"生地黄": 16, "天花粉": 15, "干石斛": 20, "醋五味子": 6,
|
||||
"生麦冬": 12, "茯苓": 10, "红参片": 6, "生牡丹皮": 10}
|
||||
candidates = {
|
||||
"qwen": {"生地黄": 15, "干石斛": 12, "醋五味子": 6, "生麦冬": 12, "茯苓": 15, "麸炒白术": 12, "丹参": 15},
|
||||
"openai": {"生地黄": 12, "醋五味子": 6, "生麦冬": 10, "茯苓": 12, "生白芍": 5, "炒酸枣仁": 6},
|
||||
}
|
||||
reports = {
|
||||
"qwen": {"summary": "界面演示数据:对照药味组成、剂量及用法,辅助医生逐项复核。",
|
||||
"diagnosis": "消渴病,气阴两虚兼血热。兼证需结合舌脉资料核实。",
|
||||
"analysis": "阅读药方对比页,查看同名药材的剂量差异、仅医生方药味与仅候选方药味。",
|
||||
"risk_assessment": [{"level": "high", "label": "血压数据缺失,影响补气药安全性评估"},
|
||||
{"level": "medium", "label": "肝肾功能具体指标未提供"}],
|
||||
"missing_information": ["甲状腺功能及眼底检查报告缺失", "舌象与脉诊仅有附件,无文本记录"]},
|
||||
"openai": {"summary": "界面演示数据:两份候选方独立生成,引用编号可回查原始资料。",
|
||||
"diagnosis": "已记录气阴两虚证;辨证依据需补充。",
|
||||
"analysis": "候选方以益气养阴为主,安神药味为本模型新增,需要医师确认。",
|
||||
"risk_assessment": [{"level": "high", "label": "缺少当前用药记录,无法排除配伍风险"}],
|
||||
"missing_information": ["舌象与脉诊仅有附件,无文本记录", "近期体重变化及 BMI 数据缺失"]},
|
||||
}
|
||||
models = {}
|
||||
for key, doses in candidates.items():
|
||||
herbs, rows = [], []
|
||||
for name in sorted(set(doctor) | set(doses), key=lambda item: (item not in doses, item)):
|
||||
common = {"name": name, "unit": "g", "dose_basis": "per_dose", "formula_type": "主方"}
|
||||
left, right = doctor.get(name), doses.get(name)
|
||||
if right is not None:
|
||||
herbs.append({**common, "dosage": right})
|
||||
rows.append({
|
||||
**common, "key": name,
|
||||
"doctor": {**common, "dosage": left} if left is not None else None,
|
||||
"candidate": {**common, "dosage": right, "source_rows": [len(herbs) - 1]} if right is not None else None,
|
||||
"doctor_dosage": left, "candidate_dosage": right,
|
||||
"match_type": "candidate_only" if left is None else "doctor_only" if right is None else "matched",
|
||||
"contribution": min(left, right) / max(left, right) if left and right else None,
|
||||
})
|
||||
matched = sum(row["match_type"] == "matched" for row in rows)
|
||||
denominator = len(doctor) + len(herbs)
|
||||
models[key] = {
|
||||
"status": "success", "report_id": 10 if key == "qwen" else 11,
|
||||
"candidate": {"status": "available_for_review", "prescription_name": "候选药方 · 界面示例",
|
||||
"herbs": herbs, "dose_basis": "per_dose", "prescription_type": "浓缩水丸",
|
||||
"usage_instruction": "服法由医生复核后确认。", "usage_days": 7, "times_per_day": 2,
|
||||
"rationale": "这是用于检查界面排版的模拟药方,不对应真实患者。",
|
||||
"risk_warnings": ["药味与剂量差异需要逐项复核。"]},
|
||||
"comparison": {"status": "comparable",
|
||||
"score": 200 * sum(row["contribution"] or 0 for row in rows) / denominator,
|
||||
"herb_score": 200 * matched / denominator, "matched_count": matched,
|
||||
"doctor_count": len(doctor), "candidate_count": len(herbs), "rows": rows,
|
||||
"reason": "药味与剂量可比;服法与疗程需单独复核。", "usage_differences": []},
|
||||
"report": reports[key],
|
||||
"coverage": {"status": "incomplete", "complete": False,
|
||||
"files": [{"file_id": f"a{index:04d}", "type": "image" if index % 3 else "document",
|
||||
"status": "processed", "transmitted": True, "version_verified": True}
|
||||
for index in range(20 if key == "qwen" else 17)]
|
||||
+ [{"file_id": f"z{index:04d}", "type": "document", "status": "restricted",
|
||||
"transmitted": False, "version_verified": False,
|
||||
"reason": "FILE_UNAVAILABLE_OR_UNSUPPORTED"} for index in range(2)]},
|
||||
"progress": {"stage_label": "处理完成", "elapsed_seconds": 135 if key == "qwen" else 591,
|
||||
"attempt": 1},
|
||||
"usage": {"total_calls": 3, "calls": [
|
||||
{"stage": "text:0", "ok": True, "latency_ms": 3120, "file_count": 0,
|
||||
"usage": {"completion_tokens": 980}, "error_code": ""},
|
||||
{"stage": "files:0", "ok": True, "latency_ms": 22400,
|
||||
"file_count": 20 if key == "qwen" else 17,
|
||||
"usage": {"completion_tokens": 2140}, "error_code": ""},
|
||||
{"stage": "final", "ok": True, "latency_ms": 18800, "file_count": 0,
|
||||
"usage": {"completion_tokens": 5617}, "error_code": ""}]},
|
||||
"review": {"status": "viewed", "comment": ""},
|
||||
"algorithm_version": "prescription-soft-dice-v1.1.0",
|
||||
"prompt_version": "manual-prescription-required-candidate-v4",
|
||||
}
|
||||
return {"id": 40, "prescription_id": 7556, "patient_id": 1391, "diagnosis_id": 1391,
|
||||
"prescription_revision": 1, "status": "success", "validity": "current",
|
||||
"comparison_type": "non_independent", "models": models, "coverage_status": "partial",
|
||||
"created_at": "2026-09-10 15:29:00", "cutoff_at": "2026-09-10 15:29:00",
|
||||
"source_summary": {"diagnoses_count": 2, "attachment_count": 22, "video_calls_count": 4,
|
||||
"chat_messages_count": 137, "source_record_count": 31},
|
||||
"doctor_snapshot": {"patient": {"name": "张卫君", "gender": 2, "gender_label": "女", "age": 58},
|
||||
"diagnosis": {"clinical_diagnosis": "2型糖尿病 消渴病 · 气阴两虚兼血热",
|
||||
"chief_complaint": "咳嗽反复1月余"},
|
||||
"prescription": {"herbs": [{"name": name, "dosage": dose, "unit": "g",
|
||||
"dose_basis": "per_dose", "formula_type": "主方"}
|
||||
for name, dose in doctor.items()],
|
||||
"prescription_type": "浓缩水丸", "dose_count": 1,
|
||||
"usage_instruction": "每日1剂,水煎分服。"}},
|
||||
"missing": [{"code": "TRANSCRIPT_NOT_VERIFIED_COMPLETE", "critical": True},
|
||||
{"code": "TRANSCRIPT_NOT_VERIFIED_COMPLETE", "critical": True},
|
||||
{"code": "ARCHIVE_SYNC_WATERMARK_UNAVAILABLE", "critical": False}]}
|
||||
|
||||
|
||||
def example_statistics() -> dict[str, Any]:
|
||||
"""Doctor-level shape the statistics window renders; synthetic, but structurally complete."""
|
||||
|
||||
def doctor(identifier: int, name: str, totals: tuple[int, int, int],
|
||||
qwen: tuple[int, float | None], openai: tuple[int, float | None],
|
||||
review: tuple[int, int]) -> dict[str, Any]:
|
||||
total, patients, paired = totals
|
||||
models = {}
|
||||
for key, (eligible, mean) in (("qwen", qwen), ("openai", openai)):
|
||||
share = (0.34, 0.38, 0.18, 0.07, 0.03)
|
||||
models[key] = {"eligible_count": eligible, "mean": mean,
|
||||
"median": None if mean is None else round(mean - 1.4, 1),
|
||||
"excluded_reasons": {"SOURCE_HISTORY_VERSIONS_UNAVAILABLE": max(0, total - eligible - 2),
|
||||
"incomplete_coverage": min(2, max(0, total - eligible))},
|
||||
"distribution": {name: round(eligible * fraction)
|
||||
for name, fraction in zip(
|
||||
("[0,20)", "[20,40)", "[40,60)", "[60,80)", "[80,100]"),
|
||||
share, strict=True)}}
|
||||
evaluated, qualified = review
|
||||
return {"doctor_id": identifier, "doctor_name": name, "total_count": total,
|
||||
"patient_count": patients, "paired_count": paired, "models": models,
|
||||
"review": {"evaluated_count": evaluated, "qualified_count": qualified,
|
||||
"qualified_rate": None if not evaluated else round(100 * qualified / evaluated, 1)}}
|
||||
|
||||
doctors = [doctor(26, "何医生", (18, 14, 9), (12, 21.4), (9, 18.9), (6, 4)),
|
||||
doctor(31, "李医生", (11, 9, 4), (7, 26.8), (5, 24.1), (3, 1)),
|
||||
doctor(44, "王医生", (6, 5, 1), (2, 15.2), (0, None), (0, 0))]
|
||||
return {"total_count": sum(item["total_count"] for item in doctors),
|
||||
"patient_count": sum(item["patient_count"] for item in doctors), "doctors": doctors}
|
||||
|
||||
|
||||
class PreviewRepository:
|
||||
def __init__(self) -> None:
|
||||
self.batch = example_batch()
|
||||
|
||||
def list_prescription_ai_reports(self, **_params: Any) -> dict[str, Any]:
|
||||
return {"enabled": True, "lists": deepcopy(self.history()), "count": len(self.history())}
|
||||
|
||||
def history(self) -> list[dict[str, Any]]:
|
||||
"""The current batch plus the earlier ones it supersedes, newest first."""
|
||||
|
||||
older = [
|
||||
{"id": 39, "created_at": "2026-09-10 15:18:00", "status": "success", "validity": "superseded",
|
||||
"comparison_type": "non_independent",
|
||||
"models": {"qwen": {"comparison": {"status": "comparable", "score": 48.9},
|
||||
"algorithm_version": "prescription-soft-dice-v1.0.1", "prompt_version": "v3"},
|
||||
"openai": {"comparison": {"status": "comparable", "score": 41.7},
|
||||
"algorithm_version": "prescription-soft-dice-v1.0.1", "prompt_version": "v3"}}},
|
||||
{"id": 38, "created_at": "2026-09-10 14:05:00", "status": "success", "validity": "superseded",
|
||||
"models": {"qwen": {"comparison": {"status": "comparable", "score": 33.4},
|
||||
"algorithm_version": "prescription-soft-dice-v1.0.1", "prompt_version": "v3"},
|
||||
"openai": {"comparison": {"status": "comparable", "score": 31.1},
|
||||
"algorithm_version": "prescription-soft-dice-v1.0.1", "prompt_version": "v3"}}},
|
||||
{"id": 37, "created_at": "2026-09-10 13:25:00", "status": "failed", "validity": "superseded",
|
||||
"models": {"qwen": {"error_message": "模型返回未通过校验",
|
||||
"algorithm_version": "prescription-soft-dice-v1.0.1"},
|
||||
"openai": {"comparison": {"status": "not_comparable"}}}},
|
||||
{"id": 36, "created_at": "2026-09-10 12:34:00", "status": "success", "validity": "superseded",
|
||||
"models": {"qwen": {"comparison": {"status": "not_comparable"},
|
||||
"algorithm_version": "prescription-soft-dice-v1.0.0"},
|
||||
"openai": {"comparison": {"status": "not_comparable"}}}},
|
||||
]
|
||||
return [self.batch, *older]
|
||||
|
||||
def get_prescription_ai_report(self, _batch_id: int) -> dict[str, Any]:
|
||||
return deepcopy(self.batch)
|
||||
|
||||
def prescription_ai_statistics(self, *_args: Any, **_params: Any) -> dict[str, Any]:
|
||||
return deepcopy(example_statistics())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
application = QApplication.instance() or QApplication([])
|
||||
apply_theme(application)
|
||||
output = Path(__file__).resolve().parents[1] / "artifacts" / "issued_prescription_redesign"
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def immediate(function: Any, **callbacks: Any) -> None:
|
||||
result = function()
|
||||
if callbacks.get("on_success"):
|
||||
callbacks["on_success"](result)
|
||||
if callbacks.get("on_finished"):
|
||||
callbacks["on_finished"]()
|
||||
|
||||
ai.run_async = immediate
|
||||
repository = PreviewRepository()
|
||||
dialog = ai.IssuedPrescriptionAiDialog(repository, ["*"], prescription_id=7556,
|
||||
current_user={"name": "张医生"})
|
||||
dialog.show()
|
||||
names = {dialog.tabs.tabText(index): index for index in range(dialog.tabs.count())}
|
||||
for filename, width, height, tab in (
|
||||
("prescription-1440.png", 1440, 940, "对比总览"),
|
||||
("prescription-1280.png", 1280, 860, "对比总览"),
|
||||
("prescription-1024.png", 1024, 700, "对比总览"),
|
||||
("prescription-940.png", 940, 640, "对比总览"),
|
||||
("original-1280.png", 1280, 860, "原方记录"),
|
||||
("analysis-1280.png", 1280, 860, "完整报告"),
|
||||
("candidate-1280.png", 1280, 860, "方义与用法"),
|
||||
("per-herb-1440.png", 1440, 940, "候选与逐味"),
|
||||
("sources-1440.png", 1440, 940, "资料与缺口"),
|
||||
("history-1440.png", 1440, 940, "历史与趋势"),
|
||||
("pipeline-1440.png", 1440, 940, "处理进度"),
|
||||
):
|
||||
dialog.resize(width, height)
|
||||
dialog.tabs.setCurrentIndex(names[tab])
|
||||
for _ in range(4):
|
||||
application.processEvents()
|
||||
assert (dialog.width(), dialog.height()) == (width, height), (filename, dialog.size())
|
||||
assert dialog.grab().save(str(output / filename))
|
||||
print(filename, "window", width, height, "workspace", dialog.tabs.width(), dialog.tabs.height())
|
||||
dialog.tabs.setCurrentIndex(names["完整报告"])
|
||||
dialog._set_report_mode("differences")
|
||||
application.processEvents()
|
||||
assert dialog.grab().save(str(output / "report-differences-1440.png"))
|
||||
dialog._set_report_mode("both")
|
||||
dialog.tabs.setCurrentIndex(names["对比总览"])
|
||||
dialog.resize(1280, 860)
|
||||
for _ in range(4):
|
||||
application.processEvents()
|
||||
dialog.review_model.setCurrentIndex(1)
|
||||
application.processEvents()
|
||||
assert dialog.grab().save(str(output / "openai-1280.png"))
|
||||
dialog.review_model.setCurrentIndex(0)
|
||||
repository.batch["status"] = "running"
|
||||
repository.batch["models"]["openai"].update(status="running", candidate=None, comparison=None, report=None)
|
||||
dialog.refresh()
|
||||
application.processEvents()
|
||||
assert dialog.grab().save(str(output / "partial-1280.png"))
|
||||
repository.batch.update(validity="superseded", status="success")
|
||||
dialog.refresh()
|
||||
application.processEvents()
|
||||
assert dialog.grab().save(str(output / "historical-1280.png"))
|
||||
repository.batch.update(validity="current", status="running")
|
||||
repository.batch["models"]["qwen"].update(status="running", candidate=None, comparison=None, report=None)
|
||||
dialog.refresh()
|
||||
application.processEvents()
|
||||
assert dialog.grab().save(str(output / "pending-1280.png"))
|
||||
# A failed model must state the reason and offer its own retry on the card itself.
|
||||
repository.batch.update(validity="current", status="partial")
|
||||
repository.batch["models"]["qwen"].update(
|
||||
status="failed", error_code="upstream_timeout", error_message="上游模型超时,未返回结果",
|
||||
candidate=None, comparison=None, report=None, retry_count=1, max_retry=3)
|
||||
repository.batch["models"]["openai"] = deepcopy(example_batch()["models"]["openai"])
|
||||
dialog.refresh()
|
||||
application.processEvents()
|
||||
assert dialog.grab().save(str(output / "failed-1280.png"))
|
||||
# The light theme is the same window with the other palette; capture it once.
|
||||
dialog.resize(1440, 940)
|
||||
dialog._switch_theme()
|
||||
for _ in range(12):
|
||||
application.processEvents()
|
||||
dialog.repaint()
|
||||
assert dialog.grab().save(str(output / "light-1440.png"))
|
||||
dialog._switch_theme()
|
||||
for _ in range(4):
|
||||
application.processEvents()
|
||||
dialog.close()
|
||||
|
||||
statistics = ai.PrescriptionAiStatisticsDialog(repository, ["*"])
|
||||
statistics.resize(1240, 880)
|
||||
statistics.show()
|
||||
for _ in range(4):
|
||||
application.processEvents()
|
||||
assert statistics.grab().save(str(output / "statistics-1240.png"))
|
||||
statistics.close()
|
||||
print("statistics-1240.png", statistics.panel.doctors.rowCount(), "doctors")
|
||||
print(output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -3,9 +3,9 @@
|
||||
__all__ = ["DEBUG_MODE", "ONLINE_API_BASE_URL", "__version__"]
|
||||
|
||||
# Single source of truth for runtime, package, installer, and executable versions.
|
||||
__version__ = "1.4.2"
|
||||
__version__ = "1.4.5"
|
||||
|
||||
# 调试模式开启时,登录页显示“演示模式”和“服务器设置”。
|
||||
# 正式发布请保持 False;此时程序只使用下面配置的线上域名。
|
||||
DEBUG_MODE = True
|
||||
DEBUG_MODE = False
|
||||
ONLINE_API_BASE_URL = "https://admin.zhenyangtang.com.cn"
|
||||
|
||||
@@ -233,8 +233,29 @@ class DoctorRepository(Protocol):
|
||||
) -> dict[str, Any]:
|
||||
"""Append one model-specific AI diagnosis snapshot for one patient."""
|
||||
|
||||
def get_prescription(self, prescription_id: int) -> Prescription:
|
||||
"""Return one issued prescription."""
|
||||
def get_prescription(self, prescription_id: int) -> Prescription:
|
||||
"""Return one issued prescription."""
|
||||
|
||||
def list_prescription_ai_statuses(self, ids: list[int]) -> dict[str, Any]:
|
||||
"""Read cached dual-model states for up to 100 prescriptions."""
|
||||
|
||||
def list_prescription_ai_reports(self, *, prescription_id: int = 0, diagnosis_id: int = 0, page_no: int = 1, page_size: int = 20) -> dict[str, Any]:
|
||||
"""Read immutable analysis batch history in the authorized scope."""
|
||||
|
||||
def get_prescription_ai_report(self, batch_id: int) -> dict[str, Any]:
|
||||
"""Read one analysis batch without starting model work."""
|
||||
|
||||
def regenerate_prescription_ai(self, prescription_id: int, reason: str) -> dict[str, Any]:
|
||||
"""Explicitly request a new analysis batch with a reason."""
|
||||
|
||||
def retry_prescription_ai(self, batch_id: int, model_key: str) -> dict[str, Any]:
|
||||
"""Retry only the requested failed model."""
|
||||
|
||||
def review_prescription_ai(self, batch_id: int, model_key: str, status: str, comment: str) -> dict[str, Any]:
|
||||
"""Save a doctor's review separately from immutable AI output."""
|
||||
|
||||
def prescription_ai_statistics(self, date_from: str, date_to: str, doctor_id: int | None = None) -> dict[str, Any]:
|
||||
"""Read baseline agreement statistics, never diagnostic accuracy."""
|
||||
|
||||
def create_prescription(
|
||||
self,
|
||||
@@ -1574,14 +1595,55 @@ class RemoteDoctorRepository:
|
||||
payload, Prescription.from_dict, page_no=page_no, page_size=page_size
|
||||
)
|
||||
|
||||
def get_prescription(self, prescription_id: int) -> Prescription:
|
||||
"""Load one issued prescription using ``tcm.prescription/detail``."""
|
||||
def get_prescription(self, prescription_id: int) -> Prescription:
|
||||
"""Load one issued prescription using ``tcm.prescription/detail``."""
|
||||
|
||||
result = _require_mapping(
|
||||
self.client.get("tcm.prescription/detail", {"id": prescription_id}),
|
||||
"tcm.prescription/detail",
|
||||
)
|
||||
return Prescription.from_dict(result)
|
||||
return Prescription.from_dict(result)
|
||||
|
||||
def _prescription_ai_request(self, action: str, params: dict[str, Any], *, mutation: bool = False) -> dict[str, Any]:
|
||||
endpoint = f"tcm.prescriptionAi/{action}"
|
||||
payload = _client_request(self.client, "post" if mutation else "get", endpoint, params, timeout=30.0)
|
||||
return dict(_require_mapping(payload, endpoint))
|
||||
|
||||
def list_prescription_ai_statuses(self, ids: list[int]) -> dict[str, Any]:
|
||||
if len(ids) > 100 or any(int(value) <= 0 for value in ids):
|
||||
raise ValueError("statuses requires at most 100 positive prescription IDs")
|
||||
return self._prescription_ai_request("statuses", {"ids": ",".join(str(int(value)) for value in dict.fromkeys(ids))})
|
||||
|
||||
def list_prescription_ai_reports(self, *, prescription_id: int = 0, diagnosis_id: int = 0, page_no: int = 1, page_size: int = 20) -> dict[str, Any]:
|
||||
if bool(prescription_id) == bool(diagnosis_id):
|
||||
raise ValueError("Exactly one prescription_id or diagnosis_id is required")
|
||||
params = {"prescription_id": prescription_id} if prescription_id else {"diagnosis_id": diagnosis_id}
|
||||
params.update(page_no=page_no, page_size=page_size)
|
||||
return self._prescription_ai_request("reports", params)
|
||||
|
||||
def get_prescription_ai_report(self, batch_id: int) -> dict[str, Any]:
|
||||
return self._prescription_ai_request("detail", {"batch_id": batch_id})
|
||||
|
||||
def regenerate_prescription_ai(self, prescription_id: int, reason: str) -> dict[str, Any]:
|
||||
if not reason.strip():
|
||||
raise ValueError("重新分析需要填写原因")
|
||||
return self._prescription_ai_request("regenerate", {"prescription_id": prescription_id, "reason": reason.strip()}, mutation=True)
|
||||
|
||||
def retry_prescription_ai(self, batch_id: int, model_key: str) -> dict[str, Any]:
|
||||
if model_key not in {"qwen", "openai"}:
|
||||
raise ValueError("Unknown model_key")
|
||||
return self._prescription_ai_request("retry", {"batch_id": batch_id, "model_key": model_key}, mutation=True)
|
||||
|
||||
def review_prescription_ai(self, batch_id: int, model_key: str, status: str, comment: str) -> dict[str, Any]:
|
||||
if model_key not in {"qwen", "openai"} or status not in {"viewed", "needs_information", "not_adopted", "reviewed"}:
|
||||
raise ValueError("Invalid review state")
|
||||
return self._prescription_ai_request("review", {"batch_id": batch_id, "model_key": model_key, "status": status, "comment": comment}, mutation=True)
|
||||
|
||||
def prescription_ai_statistics(self, date_from: str, date_to: str, doctor_id: int | None = None) -> dict[str, Any]:
|
||||
params: dict[str, Any] = {"date_from": date_from, "date_to": date_to}
|
||||
if doctor_id is not None:
|
||||
params["doctor_id"] = doctor_id
|
||||
return self._prescription_ai_request("statistics", params)
|
||||
|
||||
def create_prescription(
|
||||
self,
|
||||
|
||||
@@ -5729,6 +5729,7 @@ class AiConsultDialog(QDialog):
|
||||
"usage_notes",
|
||||
}
|
||||
seed.update({key: deepcopy(value) for key, value in draft.items() if key in allowed})
|
||||
seed["ai_assisted"] = True
|
||||
dialog = PrescriptionEditorDialog(
|
||||
self.repository,
|
||||
seed,
|
||||
|
||||
@@ -78,6 +78,7 @@ from ..widgets import (
|
||||
page_total,
|
||||
run_async,
|
||||
)
|
||||
from .issued_prescription_ai import can_open_issued_ai, present_issued_prescription_ai
|
||||
from .local_audio_queue import LocalAudioQueueDialog
|
||||
from .prescription_ai import can_open_diagnosis_ai_report, present_diagnosis_ai_report
|
||||
|
||||
@@ -1067,6 +1068,12 @@ class DiagnosisDialog(QDialog):
|
||||
self.readonly_ai_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.readonly_ai_button.clicked.connect(self._open_ai_report)
|
||||
heading_row.addWidget(self.readonly_ai_button, 0)
|
||||
self.readonly_prescription_ai_button = QPushButton("处方 AI 对照", card)
|
||||
self.readonly_prescription_ai_button.setProperty("variant", "secondary")
|
||||
self.readonly_prescription_ai_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.readonly_prescription_ai_button.setVisible(can_open_issued_ai(self.permissions) and callable(getattr(self.repository, "list_prescription_ai_reports", None)))
|
||||
self.readonly_prescription_ai_button.clicked.connect(self._open_prescription_ai_report)
|
||||
heading_row.addWidget(self.readonly_prescription_ai_button, 0)
|
||||
layout.addLayout(heading_row)
|
||||
patient_hero = QFrame()
|
||||
patient_hero.setObjectName("DiagnosisReadonlyPatientHero")
|
||||
@@ -1709,6 +1716,9 @@ class DiagnosisDialog(QDialog):
|
||||
if hasattr(self, "readonly_ai_button"):
|
||||
self.readonly_ai_button.setVisible(can_open_diagnosis_ai_report(self.permissions))
|
||||
self.readonly_ai_button.setEnabled(self._diagnosis_id > 0)
|
||||
if hasattr(self, "readonly_prescription_ai_button"):
|
||||
self.readonly_prescription_ai_button.setVisible(can_open_issued_ai(self.permissions) and callable(getattr(self.repository, "list_prescription_ai_reports", None)))
|
||||
self.readonly_prescription_ai_button.setEnabled(self._diagnosis_id > 0)
|
||||
previous_key = self._current_tab_key()
|
||||
allowed_tabs = [
|
||||
(key, label) for key, label, codes in _TAB_DEFINITIONS if self._tab_allowed(codes)
|
||||
@@ -1960,6 +1970,10 @@ class DiagnosisDialog(QDialog):
|
||||
)
|
||||
present_diagnosis_ai_report(self.repository, self.permissions, self, row)
|
||||
|
||||
def _open_prescription_ai_report(self) -> None:
|
||||
if self._diagnosis_id > 0:
|
||||
present_issued_prescription_ai(self.repository, self.permissions, self, diagnosis_id=self._diagnosis_id)
|
||||
|
||||
def open_view_only(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,375 @@
|
||||
"""Painted charts for the prescription analysis window, in the workstation's tech blue.
|
||||
|
||||
Every widget draws only what the saved report contains: a value that is missing stays visibly
|
||||
absent instead of being drawn as zero, and no chart implies a medical judgement by colour.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from PySide6.QtCore import QPointF, QRectF, QSize, Qt
|
||||
from PySide6.QtGui import QColor, QFont, QPainter, QPaintEvent
|
||||
from PySide6.QtWidgets import QSizePolicy, QWidget
|
||||
|
||||
from .issued_prescription_ai_theme import CONSOLE as TECH_BLUE
|
||||
|
||||
# Colour names resolve through the active palette at paint time, so a theme switch needs no
|
||||
# rebuild here: the next repaint already draws in the new colours.
|
||||
|
||||
|
||||
class _Palette:
|
||||
"""Attribute access into the live palette: ``COLOUR.qwen`` is always the current hue."""
|
||||
|
||||
__slots__ = ("_keys",)
|
||||
|
||||
def __init__(self, **keys: str) -> None:
|
||||
object.__setattr__(self, "_keys", dict(keys))
|
||||
|
||||
def __getattr__(self, name: str) -> str:
|
||||
return TECH_BLUE[self._keys[name]]
|
||||
|
||||
|
||||
COLOUR = _Palette(doctor="muted", qwen="qwen", openai="openai", track="raised",
|
||||
limited="amber", ink="heading", muted="muted")
|
||||
|
||||
|
||||
def _mapping(value: Any) -> dict[str, Any]:
|
||||
return dict(value) if isinstance(value, Mapping) else {}
|
||||
|
||||
|
||||
def _count(value: Any) -> int:
|
||||
if value is None or isinstance(value, bool):
|
||||
return 0
|
||||
try:
|
||||
number = int(float(value))
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
return max(0, number)
|
||||
|
||||
|
||||
class DonutGauge(QWidget):
|
||||
"""A ring reading one percentage. An unavailable value leaves the track empty, never zero."""
|
||||
|
||||
def __init__(self, accent: str, parent: QWidget | None = None, *, diameter: int = 88,
|
||||
thickness: int = 11, track: str = COLOUR.track) -> None:
|
||||
super().__init__(parent)
|
||||
self._accent, self._track = accent, track
|
||||
self._thickness = thickness
|
||||
self._value: float | None = None
|
||||
self.setFixedSize(diameter, diameter)
|
||||
self.setAccessibleName("一致度环形图")
|
||||
self.set_value(None)
|
||||
|
||||
def set_value(self, value: Any) -> None:
|
||||
parsed = None
|
||||
try:
|
||||
parsed = None if value is None or isinstance(value, bool) else float(value)
|
||||
except (TypeError, ValueError):
|
||||
parsed = None
|
||||
self._value = None if parsed is None or parsed < 0 or parsed > 100 else parsed
|
||||
self.setAccessibleDescription("暂无可比结果" if self._value is None else f"{self._value:.1f}%")
|
||||
self.update()
|
||||
|
||||
def value(self) -> float | None:
|
||||
return self._value
|
||||
|
||||
def paintEvent(self, event: QPaintEvent) -> None: # noqa: N802 - Qt virtual
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
inset = self._thickness / 2 + 1
|
||||
box = QRectF(inset, inset, self.width() - 2 * inset, self.height() - 2 * inset)
|
||||
pen = painter.pen()
|
||||
pen.setWidthF(self._thickness)
|
||||
pen.setCapStyle(Qt.PenCapStyle.FlatCap)
|
||||
pen.setColor(QColor(self._track))
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
painter.drawEllipse(box)
|
||||
if self._value:
|
||||
pen.setColor(QColor(self._accent))
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
painter.setPen(pen)
|
||||
# Qt angles are sixteenths of a degree; start at twelve o'clock and run clockwise.
|
||||
painter.drawArc(box, 90 * 16, -int(360 * 16 * self._value / 100))
|
||||
painter.end()
|
||||
|
||||
|
||||
class VennChart(QWidget):
|
||||
"""Doctor / model-A / model-B herb sets with their real intersection counts."""
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._sets: dict[str, int] = {}
|
||||
self._labels = ("医生原方", "千问", "OpenAI")
|
||||
self.setMinimumHeight(96)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
|
||||
def set_counts(self, counts: Mapping[str, int], labels: tuple[str, str, str] | None = None) -> None:
|
||||
"""Regions: doctor_only, qwen_only, openai_only, doctor_qwen, doctor_openai, qwen_openai, all."""
|
||||
|
||||
self._sets = {key: _count(value) for key, value in _mapping(counts).items()}
|
||||
if labels:
|
||||
self._labels = labels
|
||||
total = sum(self._sets.values())
|
||||
self.setAccessibleDescription(
|
||||
"三方用药交集:" + " · ".join(f"{key} {value}" for key, value in self._sets.items()) if total
|
||||
else "尚无可比较的候选药方")
|
||||
self.update()
|
||||
|
||||
def has_data(self) -> bool:
|
||||
return any(self._sets.values())
|
||||
|
||||
def paintEvent(self, event: QPaintEvent) -> None: # noqa: N802 - Qt virtual
|
||||
if not self.has_data():
|
||||
return
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
# The three set names sit above and below the circles, so their bands are reserved first.
|
||||
top_band, bottom_band = 18.0, 18.0
|
||||
available = max(40.0, self.height() - top_band - bottom_band)
|
||||
side = min(self.width() * 0.62, available / 1.34)
|
||||
radius = side / 2
|
||||
offset = radius * 0.52
|
||||
centre_x = self.width() / 2
|
||||
centre_y = top_band + available / 2 - offset * 0.1
|
||||
circles = (
|
||||
(centre_x - offset, centre_y - offset * 0.55, COLOUR.doctor),
|
||||
(centre_x + offset, centre_y - offset * 0.55, COLOUR.qwen),
|
||||
(centre_x, centre_y + offset * 0.75, COLOUR.openai),
|
||||
)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
for x, y, colour in circles:
|
||||
fill = QColor(colour)
|
||||
fill.setAlpha(52)
|
||||
painter.setBrush(fill)
|
||||
painter.drawEllipse(QPointF(x, y), radius, radius)
|
||||
|
||||
font = QFont(self.font())
|
||||
font.setPixelSize(13)
|
||||
font.setBold(True)
|
||||
painter.setFont(font)
|
||||
regions = (
|
||||
("doctor_only", centre_x - offset * 1.5, centre_y - offset * 0.75, COLOUR.doctor),
|
||||
("qwen_only", centre_x + offset * 1.5, centre_y - offset * 0.75, COLOUR.qwen),
|
||||
("openai_only", centre_x, centre_y + offset * 1.45, COLOUR.openai),
|
||||
("doctor_qwen", centre_x, centre_y - offset * 0.95, COLOUR.ink),
|
||||
("doctor_openai", centre_x - offset * 0.85, centre_y + offset * 0.55, COLOUR.ink),
|
||||
("qwen_openai", centre_x + offset * 0.85, centre_y + offset * 0.55, COLOUR.ink),
|
||||
("all", centre_x, centre_y + offset * 0.1, COLOUR.ink),
|
||||
)
|
||||
for key, x, y, colour in regions:
|
||||
value = self._sets.get(key, 0)
|
||||
if not value:
|
||||
continue
|
||||
painter.setPen(QColor(colour))
|
||||
painter.drawText(QRectF(x - 22, y - 10, 44, 20), Qt.AlignmentFlag.AlignCenter, str(value))
|
||||
|
||||
font.setPixelSize(11)
|
||||
font.setBold(False)
|
||||
painter.setFont(font)
|
||||
painter.setPen(QColor(COLOUR.muted))
|
||||
top = centre_y - offset * 0.55 - radius - 17
|
||||
painter.setPen(QColor(COLOUR.doctor))
|
||||
painter.drawText(QRectF(0, top, centre_x - 6, 16),
|
||||
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter, self._labels[0])
|
||||
painter.setPen(QColor(COLOUR.qwen))
|
||||
painter.drawText(QRectF(centre_x + 6, top, centre_x - 6, 16),
|
||||
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, self._labels[1])
|
||||
painter.setPen(QColor(COLOUR.openai))
|
||||
painter.drawText(QRectF(0, centre_y + offset * 0.75 + radius + 1, self.width(), 16),
|
||||
Qt.AlignmentFlag.AlignCenter, self._labels[2])
|
||||
painter.end()
|
||||
|
||||
def minimumSizeHint(self) -> QSize:
|
||||
return QSize(200, 96)
|
||||
|
||||
|
||||
class DivergingDoses(QWidget):
|
||||
"""Per-herb dose difference against the doctor's prescription, one row per herb."""
|
||||
|
||||
ROW = 30
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._rows: list[dict[str, Any]] = []
|
||||
self._span = 1.0
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
||||
|
||||
def set_rows(self, rows: list[Mapping[str, Any]]) -> None:
|
||||
"""Each row: name, doctor, qwen, openai (floats or None), unit."""
|
||||
|
||||
self._rows = []
|
||||
for row in rows:
|
||||
value = _mapping(row)
|
||||
entry = {"name": str(value.get("name") or ""), "unit": str(value.get("unit") or "")}
|
||||
for key in ("doctor", "qwen", "openai"):
|
||||
raw = value.get(key)
|
||||
entry[key] = None if raw is None or isinstance(raw, bool) else float(raw)
|
||||
self._rows.append(entry)
|
||||
deltas = [abs((row[key] or 0) - (row["doctor"] or 0))
|
||||
for row in self._rows for key in ("qwen", "openai")
|
||||
if row[key] is not None and row["doctor"] is not None]
|
||||
self._span = max(1.0, max(deltas, default=1.0))
|
||||
self.setAccessibleDescription("剂量差异:" + " · ".join(
|
||||
f"{row['name']} 千问 {self._delta_text(row, 'qwen')} OpenAI {self._delta_text(row, 'openai')}"
|
||||
for row in self._rows) if self._rows else "暂无可比药味")
|
||||
self.setMinimumHeight(self.ROW * max(1, len(self._rows)) + 18)
|
||||
self.updateGeometry()
|
||||
self.update()
|
||||
|
||||
def _delta_text(self, row: Mapping[str, Any], key: str) -> str:
|
||||
if row.get(key) is None or row.get("doctor") is None:
|
||||
return "—"
|
||||
delta = row[key] - row["doctor"]
|
||||
return "一致" if abs(delta) < 1e-9 else f"{delta:+g}{row.get('unit') or ''}"
|
||||
|
||||
def paintEvent(self, event: QPaintEvent) -> None: # noqa: N802 - Qt virtual
|
||||
if not self._rows:
|
||||
return
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
label_width, value_width = 96, 96
|
||||
left = label_width + 10
|
||||
right = self.width() - value_width - 10
|
||||
middle = (left + right) / 2
|
||||
scale = max(1.0, (right - left) / 2) / self._span
|
||||
font = QFont(self.font())
|
||||
font.setPixelSize(12)
|
||||
painter.setFont(font)
|
||||
for index, row in enumerate(self._rows):
|
||||
top = index * self.ROW + 4
|
||||
painter.setPen(QColor(COLOUR.ink))
|
||||
painter.drawText(QRectF(0, top, label_width, self.ROW - 8),
|
||||
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter, row["name"])
|
||||
painter.setPen(QColor(COLOUR.track))
|
||||
painter.drawLine(QPointF(middle, top), QPointF(middle, top + self.ROW - 10))
|
||||
for offset, key, colour in ((0, "qwen", COLOUR.qwen), (10, "openai", COLOUR.openai)):
|
||||
if row[key] is None or row["doctor"] is None:
|
||||
continue
|
||||
delta = (row[key] - row["doctor"]) * scale
|
||||
bar = QRectF(min(middle, middle + delta), top + offset, max(abs(delta), 2.0), 8)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.setBrush(QColor(colour))
|
||||
painter.drawRoundedRect(bar, 3, 3)
|
||||
painter.setPen(QColor(COLOUR.muted))
|
||||
painter.drawText(QRectF(right + 8, top, value_width - 8, self.ROW - 8),
|
||||
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
|
||||
f"{self._delta_text(row, 'qwen')} / {self._delta_text(row, 'openai')}")
|
||||
painter.end()
|
||||
|
||||
|
||||
class WaffleCoverage(QWidget):
|
||||
"""One square per attachment: read, limited, or not delivered."""
|
||||
|
||||
def __init__(self, parent: QWidget | None = None, *, columns: int = 11) -> None:
|
||||
super().__init__(parent)
|
||||
self._columns = max(4, columns)
|
||||
self._read = self._limited = self._total = 0
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
||||
|
||||
def set_counts(self, read: int, limited: int, total: int | None = None) -> None:
|
||||
self._read, self._limited = _count(read), _count(limited)
|
||||
self._total = max(_count(total), self._read + self._limited)
|
||||
self.setAccessibleDescription(
|
||||
f"附件 {self._total} 个:已读 {self._read},受限或不支持 {self._limited}" if self._total else "本次没有附件")
|
||||
rows = max(1, -(-self._total // self._columns)) if self._total else 0
|
||||
self.setMinimumHeight(rows * 16 + max(0, rows - 1) * 4)
|
||||
self.updateGeometry()
|
||||
self.update()
|
||||
|
||||
def has_data(self) -> bool:
|
||||
return self._total > 0
|
||||
|
||||
def paintEvent(self, event: QPaintEvent) -> None: # noqa: N802 - Qt virtual
|
||||
if not self._total:
|
||||
return
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
gap = 4
|
||||
size = max(8.0, min(16.0, (self.width() - gap * (self._columns - 1)) / self._columns))
|
||||
for index in range(self._total):
|
||||
column, row = index % self._columns, index // self._columns
|
||||
colour = COLOUR.qwen if index < self._read else (COLOUR.limited if index < self._read + self._limited else COLOUR.track)
|
||||
painter.setBrush(QColor(colour))
|
||||
painter.drawRoundedRect(QRectF(column * (size + gap), row * (size + gap), size, size), 4, 4)
|
||||
painter.end()
|
||||
|
||||
|
||||
class TrendBars(QWidget):
|
||||
"""Grouped bars per batch: one column per model, oldest batch first."""
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._points: list[dict[str, Any]] = []
|
||||
self.setMinimumHeight(170)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
||||
|
||||
def set_points(self, points: list[Mapping[str, Any]]) -> None:
|
||||
"""Each point: label plus qwen/openai scores; None keeps the slot visibly empty."""
|
||||
|
||||
self._points = []
|
||||
for point in points:
|
||||
value = _mapping(point)
|
||||
entry = {"label": str(value.get("label") or "")}
|
||||
for key in ("qwen", "openai"):
|
||||
raw = value.get(key)
|
||||
entry[key] = None if raw is None or isinstance(raw, bool) else float(raw)
|
||||
self._points.append(entry)
|
||||
described = [f"{entry['label']} 千问 {self._text(entry['qwen'])} OpenAI {self._text(entry['openai'])}"
|
||||
for entry in self._points]
|
||||
self.setAccessibleDescription("一致度趋势:" + (" · ".join(described) or "暂无批次"))
|
||||
self.setToolTip("\n".join(described) or "暂无批次")
|
||||
self.update()
|
||||
|
||||
@staticmethod
|
||||
def _text(value: float | None) -> str:
|
||||
return "—" if value is None else f"{value:.1f}%"
|
||||
|
||||
def has_data(self) -> bool:
|
||||
return any(point.get(key) is not None for point in self._points for key in ("qwen", "openai"))
|
||||
|
||||
def paintEvent(self, event: QPaintEvent) -> None: # noqa: N802 - Qt virtual
|
||||
if not self._points:
|
||||
return
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
# The top band is left free so each bar can print its own figure above it.
|
||||
left, right, top, bottom = 44.0, self.width() - 10.0, 24.0, self.height() - 24.0
|
||||
values = [point[key] for point in self._points for key in ("qwen", "openai") if point[key] is not None]
|
||||
ceiling = max(10.0, max(values, default=10.0))
|
||||
font = QFont(self.font())
|
||||
font.setPixelSize(10)
|
||||
painter.setFont(font)
|
||||
for fraction in (0.0, 0.5, 1.0):
|
||||
y = bottom - (bottom - top) * fraction
|
||||
painter.setPen(QColor(COLOUR.track))
|
||||
painter.drawLine(QPointF(left, y), QPointF(right, y))
|
||||
painter.setPen(QColor(COLOUR.muted))
|
||||
painter.drawText(QRectF(0, y - 8, left - 6, 16),
|
||||
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
|
||||
f"{ceiling * fraction:.0f}%")
|
||||
slot = (right - left) / max(1, len(self._points))
|
||||
width = min(26.0, slot / 3.6)
|
||||
# The two bars of a batch sit side by side, far enough apart for each figure to fit above.
|
||||
gap = width / 2 + 4
|
||||
for index, point in enumerate(self._points):
|
||||
centre = left + slot * (index + 0.5)
|
||||
for offset, key, colour in ((-gap, "qwen", COLOUR.qwen), (gap, "openai", COLOUR.openai)):
|
||||
value = point[key]
|
||||
if value is None:
|
||||
continue
|
||||
height = (bottom - top) * min(1.0, value / ceiling)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.setBrush(QColor(colour))
|
||||
painter.drawRoundedRect(QRectF(centre + offset - width / 2, bottom - height, width, height), 3, 3)
|
||||
# The design prints each figure above its bar, in that model's own colour.
|
||||
painter.setPen(QColor(colour))
|
||||
painter.drawText(QRectF(centre + offset - 21, bottom - height - 17, 42, 14),
|
||||
Qt.AlignmentFlag.AlignCenter, f"{value:.1f}%")
|
||||
painter.setPen(QColor(COLOUR.muted))
|
||||
painter.drawText(QRectF(centre - slot / 2, bottom + 4, slot, 16),
|
||||
Qt.AlignmentFlag.AlignCenter, point["label"])
|
||||
painter.end()
|
||||
@@ -0,0 +1,644 @@
|
||||
"""Native, read-only prescription comparison using saved report snapshots only."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass, replace
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import Any
|
||||
|
||||
from PySide6.QtCore import QRectF, QSize, Qt
|
||||
from PySide6.QtGui import QColor, QFont, QPainter, QPaintEvent
|
||||
from PySide6.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QButtonGroup,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from .issued_prescription_ai_labels import EXTRA_FIELD_LABELS, SYSTEM_LABELS, system_text
|
||||
from .issued_prescription_ai_progress import ACTIVE_STATES, SUCCESS_STATES
|
||||
|
||||
MODEL_NAMES = {"qwen": "千问", "openai": "OpenAI"}
|
||||
MODEL_COLORS = {"qwen": "#3676C8", "openai": "#268578"}
|
||||
DOCTOR_COLOR = "#526479"
|
||||
INK, MUTED, LINE = "#23384C", "#758395", "#E6EDF3"
|
||||
_UNITS = {
|
||||
"g": "克", "克": "克", "mg": "毫克", "毫克": "毫克", "kg": "千克", "千克": "千克",
|
||||
"ml": "毫升", "毫升": "毫升", "l": "升", "升": "升", "iu": "国际单位", "国际单位": "国际单位",
|
||||
**{unit: unit for unit in ("片", "丸", "粒", "袋", "包", "支", "滴", "枚", "个", "条", "付", "帖", "两", "钱")},
|
||||
}
|
||||
_BASES = {"per_dose": "每剂", "每剂": "每剂", "per_day": "每日", "每日": "每日", "每天": "每日"}
|
||||
_FORMULAS = {"main": "主方", "1": "主方", "主方": "主方", "aux": "辅方", "auxiliary": "辅方", "2": "辅方", "辅方": "辅方"}
|
||||
_STALE = {
|
||||
"stale": "处方已变更", "superseded": "处方已变更", "prescription_changed": "处方已变更",
|
||||
"source_updated": "资料已更新", "invalid": "报告已失效", "voided": "处方已作废",
|
||||
"deleted": "处方已删除", "revoked": "资料权限已变更",
|
||||
}
|
||||
|
||||
|
||||
def _mapping(value: Any) -> dict[str, Any]:
|
||||
return dict(value) if isinstance(value, Mapping) else {}
|
||||
|
||||
|
||||
def _text(value: Any) -> str:
|
||||
"""Never stringify a structured payload, which could expose internal identifiers."""
|
||||
return str(value).strip() if isinstance(value, (str, int, float, Decimal)) and not isinstance(value, bool) else ""
|
||||
|
||||
|
||||
def _reason(value: Any) -> str:
|
||||
if isinstance(value, Mapping):
|
||||
return _reason(value.get("reason") or value.get("message") or value.get("code"))
|
||||
if isinstance(value, list):
|
||||
return ";".join(filter(None, (_reason(item) for item in value)))
|
||||
return system_text(value, SYSTEM_LABELS, EXTRA_FIELD_LABELS, strict=True) if _text(value) else ""
|
||||
|
||||
|
||||
def _number(value: Any) -> Decimal | None:
|
||||
if not _text(value):
|
||||
return None
|
||||
try:
|
||||
number = Decimal(str(value))
|
||||
except (InvalidOperation, ValueError):
|
||||
return None
|
||||
return number if number.is_finite() and number >= 0 else None
|
||||
|
||||
|
||||
def _formula(value: Any) -> str:
|
||||
return _FORMULAS.get(_text(value), "主辅方未注明")
|
||||
|
||||
|
||||
def _metadata(value: Any) -> str:
|
||||
return system_text(value, SYSTEM_LABELS, EXTRA_FIELD_LABELS, strict=True) if _text(value) else ""
|
||||
|
||||
|
||||
def _join_instructions(values: list[str]) -> str:
|
||||
parts = [part.strip() for value in values for part in value.split(";")]
|
||||
return ";".join(dict.fromkeys(part for part in parts if part and part.lower() not in {"无", "明确无", "none"}))
|
||||
|
||||
|
||||
def _instructions(snapshot: dict[str, Any]) -> str:
|
||||
"""Read both original herb fields and the service's normalized usage snapshot."""
|
||||
fields = ("instructions", "decoction_instruction", "special_usage", "usage_instruction", "usage_time", "usage_way")
|
||||
usage = _mapping(snapshot.get("usage"))
|
||||
values = [_text(owner.get(field)) for owner in (snapshot, usage) for field in fields]
|
||||
if not usage:
|
||||
values.append(_text(snapshot.get("usage")))
|
||||
return _join_instructions(values)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _Dose:
|
||||
raw: Any
|
||||
unit: str
|
||||
basis: str
|
||||
formula: str
|
||||
processing: str
|
||||
route: str
|
||||
group: str
|
||||
instructions: str = ""
|
||||
|
||||
@property
|
||||
def number(self) -> Decimal | None:
|
||||
return _number(self.raw)
|
||||
|
||||
@property
|
||||
def scale(self) -> tuple[str, str] | None:
|
||||
unit, basis = _UNITS.get(self.unit.lower()), _BASES.get(self.basis)
|
||||
return (unit, basis) if unit and basis else None
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
amount = _text(self.raw)
|
||||
if not amount:
|
||||
return "—"
|
||||
unit = _UNITS.get(self.unit.lower()) or _metadata(self.unit) or "单位未注明"
|
||||
basis = _BASES.get(self.basis) or "基准未确认"
|
||||
return f"{amount} {unit} / {basis}"
|
||||
|
||||
@property
|
||||
def identity(self) -> tuple[str, str, str, str]:
|
||||
# Unknown enum values must remain distinct even when their display label is generic.
|
||||
return (_FORMULAS.get(self.formula, self.formula), SYSTEM_LABELS.get(self.processing, self.processing), SYSTEM_LABELS.get(self.route, self.route), self.group)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _Row:
|
||||
name: str
|
||||
context: str
|
||||
doctor: _Dose | None
|
||||
candidate: _Dose | None
|
||||
match_type: str
|
||||
origin: str = "comparison"
|
||||
source_details: str = ""
|
||||
source_names: tuple[str, ...] = ()
|
||||
|
||||
@property
|
||||
def scale(self) -> tuple[str, str] | None:
|
||||
if self.origin != "comparison":
|
||||
return None
|
||||
doses = [dose for dose in (self.doctor, self.candidate) if dose is not None]
|
||||
if not doses or any(dose.scale is None for dose in doses):
|
||||
return None
|
||||
if len({dose.scale for dose in doses}) != 1 or len({dose.identity for dose in doses}) != 1:
|
||||
return None
|
||||
return doses[0].scale
|
||||
|
||||
@property
|
||||
def incompatibility(self) -> str:
|
||||
if self.origin == "uncompared":
|
||||
return "未纳入对比,仅保留候选原方;医生剂量未知"
|
||||
if self.origin == "original":
|
||||
return "候选原方附列;对应关系未保存,不推断同药"
|
||||
if self.doctor is not None and self.candidate is not None:
|
||||
if self.doctor.identity != self.candidate.identity:
|
||||
return "主辅方、炮制或给药分组不同,不作条形比较"
|
||||
if self.doctor.scale and self.candidate.scale and self.doctor.scale != self.candidate.scale:
|
||||
return "单位或剂量基准不同,不作条形比较"
|
||||
return "单位或每剂/每日基准未明确,不作条形比较"
|
||||
|
||||
def usage_description(self, model_name: str) -> str:
|
||||
return ";".join(filter(None, ("医生:" + self.doctor.instructions if self.doctor and self.doctor.instructions else "", model_name + ":" + self.candidate.instructions if self.candidate and self.candidate.instructions else "")))
|
||||
|
||||
def description(self, model_name: str) -> str:
|
||||
dosage = f"{self.name},{self.context};医生:{self.doctor.label if self.doctor else '—'};{model_name}:{self.candidate.label if self.candidate else '—'}"
|
||||
return ";".join(filter(None, (dosage, self.usage_description(model_name), self.source_details)))
|
||||
|
||||
|
||||
def _dose(row: dict[str, Any], side: str) -> _Dose | None:
|
||||
# An explicit null snapshot takes precedence over contradictory legacy flat fields.
|
||||
if side in row and row[side] is None:
|
||||
return None
|
||||
nested = _mapping(row.get(side))
|
||||
keys = ("doctor_dosage", "doctor_dose") if side == "doctor" else ("candidate_dosage", "candidate_dose", "ai_dose")
|
||||
raw = nested.get("dosage") if "dosage" in nested else next((row[key] for key in keys if key in row), None)
|
||||
if not nested and raw is None:
|
||||
return None
|
||||
values = {key: _text(nested[key] if key in nested else row.get(key)) for key in ("unit", "dose_basis", "formula_type", "processing", "administration_route", "group")}
|
||||
instructions = _instructions(nested) or _instructions(row)
|
||||
return _Dose(raw, values["unit"], values["dose_basis"], values["formula_type"], values["processing"], values["administration_route"], values["group"], instructions)
|
||||
|
||||
|
||||
def _row(value: dict[str, Any]) -> _Row:
|
||||
doctor, candidate = _dose(value, "doctor"), _dose(value, "candidate")
|
||||
details = []
|
||||
for dose in (doctor, candidate):
|
||||
if dose is not None:
|
||||
detail = " · ".join(filter(None, (_formula(dose.formula), _metadata(dose.processing), _metadata(dose.route), _metadata(dose.group))))
|
||||
if detail not in details:
|
||||
details.append(detail)
|
||||
context = " / ".join(details) or _formula(value.get("formula_type"))
|
||||
if len(details) > 1:
|
||||
context = "医生与模型:" + context
|
||||
return _Row(_text(value.get("name") or value.get("canonical_name") or value.get("herb_name")) or "药名未保存", context, doctor, candidate, _text(value.get("match_type")))
|
||||
|
||||
|
||||
def _original_row(herb: Any, *, origin: str) -> _Row:
|
||||
saved = dict(herb) if isinstance(herb, Mapping) else {"name": _text(herb) or "药材记录需核对"}
|
||||
row = _row({"name": saved.get("name"), "doctor": None, "candidate": saved, "match_type": "candidate_only"})
|
||||
note = "未纳入对比" if origin == "uncompared" else "候选原方附列 · 对应关系未保存"
|
||||
return replace(row, origin=origin, context=note + " · " + row.context)
|
||||
|
||||
|
||||
def _saved_rows(candidate: dict[str, Any], comparison: dict[str, Any]) -> list[_Row]:
|
||||
saved = comparison.get("rows")
|
||||
comparison_rows = [dict(value) for value in saved if isinstance(value, Mapping)] if isinstance(saved, list) else []
|
||||
herbs = candidate.get("herbs")
|
||||
herbs = herbs if isinstance(herbs, list) else []
|
||||
result: list[_Row] = []
|
||||
covered: set[int] = set()
|
||||
missing_correspondence = False
|
||||
for value in comparison_rows:
|
||||
row = _row(value)
|
||||
if row.candidate is not None and herbs:
|
||||
indices = _mapping(value.get("candidate")).get("source_rows")
|
||||
# The service uses zero-based array_values indices; never infer correspondence
|
||||
# from names, doses, order, or the number of normalized rows.
|
||||
valid_trace = isinstance(indices, list) and bool(indices) and all(isinstance(index, int) and not isinstance(index, bool) and 0 <= index < len(herbs) for index in indices)
|
||||
if valid_trace:
|
||||
indices = list(dict.fromkeys(indices))
|
||||
covered.update(indices)
|
||||
originals = [_original_row(herbs[index], origin="original") for index in indices]
|
||||
details = ";".join(f"第 {index + 1} 项 {original.name} {original.candidate.label if original.candidate else '—'}" + ("," + original.candidate.instructions if original.candidate and original.candidate.instructions else "") for index, original in zip(indices, originals, strict=True))
|
||||
instructions = _join_instructions([row.candidate.instructions, *[original.candidate.instructions for original in originals if original.candidate]])
|
||||
row = replace(row, candidate=replace(row.candidate, instructions=instructions), source_details="候选原方记录:" + details, source_names=tuple(original.name for original in originals))
|
||||
else:
|
||||
missing_correspondence = True
|
||||
result.append(row)
|
||||
# Normalization can omit unknown names, processing conflicts, or incompatible duplicate
|
||||
# entries. Keep every unaccounted original, but do not claim its doctor counterpart.
|
||||
origin = "original" if missing_correspondence else "uncompared"
|
||||
result.extend(_original_row(herb, origin=origin) for index, herb in enumerate(herbs) if index not in covered)
|
||||
return result
|
||||
|
||||
|
||||
class _DoseChart(QWidget):
|
||||
"""A scrollable painted chart; the adjacent table provides native accessibility."""
|
||||
|
||||
ROW_HEIGHT = 80
|
||||
GROUP_HEIGHT = 38
|
||||
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
super().__init__(parent)
|
||||
self.setObjectName("PrescriptionDoseChart")
|
||||
self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Preferred)
|
||||
self.setAccessibleName("药方逐味剂量对比图")
|
||||
self.rows: list[_Row] = []
|
||||
self.groups: list[tuple[tuple[str, str] | None, list[_Row], Decimal]] = []
|
||||
self.model_key = "qwen"
|
||||
self.message = "暂无药方数据"
|
||||
self.bars_enabled = False
|
||||
|
||||
def set_rows(self, rows: list[_Row], model_key: str, *, bars_enabled: bool, message: str) -> None:
|
||||
self.rows, self.model_key, self.bars_enabled, self.message = rows, model_key, bars_enabled, message
|
||||
grouped: dict[tuple[str, str] | None, list[_Row]] = {}
|
||||
for row in rows:
|
||||
grouped.setdefault(row.scale if bars_enabled else None, []).append(row)
|
||||
self.groups = []
|
||||
for scale, members in grouped.items():
|
||||
numbers = [dose.number for row in members for dose in (row.doctor, row.candidate) if dose is not None and dose.number is not None]
|
||||
self.groups.append((scale, members, max(numbers, default=Decimal(0))))
|
||||
color_name = "蓝色" if model_key == "qwen" else "青绿色"
|
||||
descriptions = [message, f"医生为深灰蓝;{MODEL_NAMES[model_key]}为{color_name}。各单位与基准组独立标尺,组间长度不可比较。缺失值为—,不按零计算。"]
|
||||
for row in rows:
|
||||
descriptions.append(row.description(MODEL_NAMES[model_key]))
|
||||
if bars_enabled and row.scale is None:
|
||||
descriptions.append(row.incompatibility)
|
||||
if any(dose is not None and _text(dose.raw) and dose.number is None for dose in (row.doctor, row.candidate)):
|
||||
descriptions.append("非数值、负数及非有限剂量保留原值,未绘制条形。")
|
||||
self.setAccessibleDescription("\n".join(descriptions))
|
||||
height = 16 + sum(self.GROUP_HEIGHT + len(members) * self.ROW_HEIGHT for _, members, _ in self.groups)
|
||||
self.setMinimumHeight(max(120, height))
|
||||
self.updateGeometry()
|
||||
self.update()
|
||||
|
||||
def sizeHint(self) -> QSize:
|
||||
return QSize(440, self.minimumHeight())
|
||||
|
||||
def minimumSizeHint(self) -> QSize:
|
||||
return QSize(0, 0)
|
||||
|
||||
def paintEvent(self, event: QPaintEvent) -> None:
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
painter.fillRect(self.rect(), QColor("#FFFFFF"))
|
||||
painter.setFont(self.font())
|
||||
if not self.rows:
|
||||
painter.setPen(QColor(MUTED))
|
||||
painter.drawText(self.rect().adjusted(24, 12, -24, -12), Qt.AlignmentFlag.AlignCenter | Qt.TextFlag.TextWordWrap, self.message)
|
||||
return
|
||||
width, y = max(0, self.width() - 32), 8
|
||||
normal = QFont(self.font())
|
||||
bold = QFont(normal)
|
||||
bold.setBold(True)
|
||||
for scale, members, maximum in self.groups:
|
||||
painter.fillRect(QRectF(16, y, width, self.GROUP_HEIGHT - 8), QColor("#F3F6F9"))
|
||||
painter.setFont(bold)
|
||||
painter.setPen(QColor(INK))
|
||||
heading = f"{scale[0]} · {scale[1]} 独立标尺 0–{maximum}" if scale else "原始剂量 · 不作条形比较"
|
||||
painter.drawText(QRectF(24, y, max(0, width - 16), self.GROUP_HEIGHT - 8), Qt.AlignmentFlag.AlignVCenter, painter.fontMetrics().elidedText(heading, Qt.TextElideMode.ElideRight, max(0, width - 16)))
|
||||
y += self.GROUP_HEIGHT
|
||||
for row in members:
|
||||
if y + self.ROW_HEIGHT >= event.rect().top() and y <= event.rect().bottom():
|
||||
painter.setFont(bold)
|
||||
painter.setPen(QColor(INK))
|
||||
title = f"{row.name} · {row.context}"
|
||||
painter.drawText(QRectF(16, y, width, 21), Qt.AlignmentFlag.AlignVCenter, painter.fontMetrics().elidedText(title, Qt.TextElideMode.ElideRight, width))
|
||||
painter.setFont(normal)
|
||||
if scale is None:
|
||||
painter.setPen(QColor(MUTED))
|
||||
detail = ";".join(("医生 " + (row.doctor.label if row.doctor else "—"), MODEL_NAMES[self.model_key] + " " + (row.candidate.label if row.candidate else "—")))
|
||||
painter.drawText(QRectF(16, y + 24, width, 22), Qt.AlignmentFlag.AlignVCenter, painter.fontMetrics().elidedText(detail, Qt.TextElideMode.ElideRight, width))
|
||||
note = row.incompatibility if self.bars_enabled else "按已保存原值列示,详见左侧药材表"
|
||||
painter.drawText(QRectF(16, y + 49, width, 22), Qt.AlignmentFlag.AlignVCenter, painter.fontMetrics().elidedText(note, Qt.TextElideMode.ElideRight, width))
|
||||
else:
|
||||
for index, (dose, color, name) in enumerate(((row.doctor, DOCTOR_COLOR, "医生"), (row.candidate, MODEL_COLORS[self.model_key], MODEL_NAMES[self.model_key]))):
|
||||
bar_y = y + 24 + index * 20
|
||||
painter.setPen(QColor(color))
|
||||
painter.drawText(QRectF(16, bar_y - 5, 54, 22), Qt.AlignmentFlag.AlignVCenter, name)
|
||||
value = dose.label if dose else "—"
|
||||
label_width = min(max(118, painter.fontMetrics().horizontalAdvance(value) + 8), max(118, width // 2))
|
||||
bar_x, bar_width = 76, max(8, width - 66 - label_width - 12)
|
||||
if dose is not None and dose.number is not None:
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.setBrush(QColor("#F0F3F7"))
|
||||
painter.drawRoundedRect(QRectF(bar_x, bar_y, bar_width, 9), 3, 3)
|
||||
fraction = float(dose.number / maximum) if maximum else 0.0
|
||||
if fraction > 0:
|
||||
painter.setBrush(QColor(color))
|
||||
painter.drawRoundedRect(QRectF(bar_x, bar_y, bar_width * fraction, 9), 3, 3)
|
||||
painter.setPen(QColor(INK))
|
||||
painter.drawText(QRectF(bar_x + bar_width + 10, bar_y - 5, label_width, 22), Qt.AlignmentFlag.AlignVCenter, painter.fontMetrics().elidedText(value, Qt.TextElideMode.ElideRight, int(label_width)))
|
||||
painter.setPen(QColor(LINE))
|
||||
painter.drawLine(16, y + self.ROW_HEIGHT - 6, self.width() - 16, y + self.ROW_HEIGHT - 6)
|
||||
y += self.ROW_HEIGHT
|
||||
|
||||
|
||||
class PrescriptionComparisonPanel(QWidget):
|
||||
"""Switch between saved candidate prescriptions without issuing any requests."""
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setObjectName("PrescriptionComparisonPanel")
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
self._batch: dict[str, Any] = {}
|
||||
self._rows: list[_Row] = []
|
||||
self._model_key = "qwen"
|
||||
self._bars_enabled = False
|
||||
self._chart_message = "暂无药方数据"
|
||||
self._candidate_count: int | None = None
|
||||
self._updating_rows = False
|
||||
self.setStyleSheet(f"""
|
||||
QWidget#PrescriptionComparisonPanel {{ background: transparent; }}
|
||||
QFrame#PrescriptionPane {{ background: white; border: 1px solid {LINE}; border-radius: 9px; }}
|
||||
QLabel {{ color: {INK}; background: transparent; }}
|
||||
QLabel#PrescriptionTitle {{ font-size: 20px; font-weight: 700; }}
|
||||
QLabel#PrescriptionCaption {{ color: {MUTED}; font-size: 12px; }}
|
||||
QLabel#PrescriptionStatus {{ color: #5F7287; font-size: 12px; }}
|
||||
QPushButton#PrescriptionModel {{ border: 1px solid #DAE4EE; background: white; color: #607388;
|
||||
border-radius: 6px; padding: 6px 19px; font-weight: 600; }}
|
||||
QPushButton#PrescriptionModel[model="qwen"]:checked {{ background: #EDF4FE; color: #3676C8; border-color: #AAC7EB; }}
|
||||
QPushButton#PrescriptionModel[model="openai"]:checked {{ background: #EDF7F4; color: #268578; border-color: #A5D2C6; }}
|
||||
QLineEdit#PrescriptionSearch {{ background: white; border: 1px solid #DAE4EE; border-radius: 6px; padding: 6px 10px; }}
|
||||
QTableWidget#PrescriptionHerbs {{ background: white; border: 0; color: {INK}; gridline-color: {LINE}; }}
|
||||
QTableWidget#PrescriptionHerbs::item {{ padding: 4px 7px; border-bottom: 1px solid #EDF1F5; }}
|
||||
QTableWidget#PrescriptionHerbs::item:selected {{ background: #EDF4FC; color: {INK}; }}
|
||||
QHeaderView::section {{ background: #F4F7FA; color: #687C91; border: 0; padding: 7px; font-weight: 600; }}
|
||||
QScrollArea {{ background: white; border: 0; }}
|
||||
""")
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(10, 8, 10, 8)
|
||||
layout.setSpacing(10)
|
||||
toolbar = QHBoxLayout()
|
||||
toolbar.setSpacing(7)
|
||||
self.model_buttons: dict[str, QPushButton] = {}
|
||||
self.model_group = QButtonGroup(self)
|
||||
for model_key, name in MODEL_NAMES.items():
|
||||
button = QPushButton(name, self)
|
||||
button.setObjectName("PrescriptionModel")
|
||||
button.setProperty("model", model_key)
|
||||
button.setCheckable(True)
|
||||
button.setChecked(model_key == self._model_key)
|
||||
button.setAccessibleName(f"查看{name}候选方与医生方对比")
|
||||
button.clicked.connect(lambda _checked=False, key=model_key: self._select_model(key))
|
||||
self.model_group.addButton(button)
|
||||
self.model_buttons[model_key] = button
|
||||
toolbar.addWidget(button)
|
||||
toolbar.addStretch(1)
|
||||
self.search = QLineEdit(self)
|
||||
self.search.setObjectName("PrescriptionSearch")
|
||||
self.search.setPlaceholderText("搜索药名")
|
||||
self.search.setAccessibleName("搜索药名,同时筛选药材表和图表")
|
||||
self.search.setClearButtonEnabled(True)
|
||||
self.search.setMaximumWidth(240)
|
||||
self.search.textChanged.connect(self._filter_rows)
|
||||
toolbar.addWidget(self.search)
|
||||
layout.addLayout(toolbar)
|
||||
panes = QHBoxLayout()
|
||||
panes.setSpacing(12)
|
||||
left = QFrame(self)
|
||||
left.setObjectName("PrescriptionPane")
|
||||
left.setMinimumWidth(0)
|
||||
left.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Expanding)
|
||||
left_layout = QVBoxLayout(left)
|
||||
left_layout.setContentsMargins(15, 13, 15, 10)
|
||||
left_layout.setSpacing(6)
|
||||
self.caption_label = self._label("千问 · 候选药方", left, "PrescriptionCaption")
|
||||
self.name_label = self._label("暂无候选药方", left, "PrescriptionTitle")
|
||||
self.name_label.setWordWrap(True)
|
||||
self.name_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Preferred)
|
||||
self.usage_label = self._label("", left, "PrescriptionCaption")
|
||||
self.usage_label.setWordWrap(True)
|
||||
left_layout.addWidget(self.caption_label)
|
||||
left_layout.addWidget(self.name_label)
|
||||
left_layout.addWidget(self.usage_label)
|
||||
self.herb_table = QTableWidget(0, 3, left)
|
||||
self.herb_table.setObjectName("PrescriptionHerbs")
|
||||
self.herb_table.setAccessibleName("候选药方药材与医生剂量对照表")
|
||||
self.herb_table.setHorizontalHeaderLabels(["药材 / 主辅方", "医生剂量", "千问剂量"])
|
||||
self.herb_table.verticalHeader().hide()
|
||||
self.herb_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||
self.herb_table.horizontalHeader().setMinimumSectionSize(36)
|
||||
self.herb_table.setShowGrid(False)
|
||||
self.herb_table.setWordWrap(True)
|
||||
self.herb_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
self.herb_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.herb_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||
self.herb_table.itemSelectionChanged.connect(self._focus_chart_row)
|
||||
self.herb_table.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
||||
self.herb_table.setMinimumSize(0, 0)
|
||||
self.herb_table.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Expanding)
|
||||
left_layout.addWidget(self.herb_table, 1)
|
||||
self.empty_label = self._label("选择报告后显示已保存的候选药方", left, "PrescriptionCaption")
|
||||
self.empty_label.setWordWrap(True)
|
||||
left_layout.addWidget(self.empty_label)
|
||||
self.count_label = self._label("", left, "PrescriptionCaption")
|
||||
self.count_label.setWordWrap(True)
|
||||
left_layout.addWidget(self.count_label)
|
||||
panes.addWidget(left, 5)
|
||||
right = QFrame(self)
|
||||
right.setObjectName("PrescriptionPane")
|
||||
right.setMinimumWidth(0)
|
||||
right.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Expanding)
|
||||
right_layout = QVBoxLayout(right)
|
||||
right_layout.setContentsMargins(15, 13, 15, 10)
|
||||
right_layout.setSpacing(6)
|
||||
title = self._label("逐味剂量对比", right)
|
||||
title.setStyleSheet("font-size: 15px; font-weight: 700;")
|
||||
legend_row = QHBoxLayout()
|
||||
legend_row.addWidget(title)
|
||||
legend_row.addStretch()
|
||||
self.legend = self._label("● 医生 ● 千问", right, "PrescriptionCaption")
|
||||
self.legend.setTextFormat(Qt.TextFormat.RichText)
|
||||
legend_row.addWidget(self.legend)
|
||||
right_layout.addLayout(legend_row)
|
||||
self.status_label = self._label("", right, "PrescriptionStatus")
|
||||
self.status_label.setWordWrap(True)
|
||||
self.status_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Preferred)
|
||||
right_layout.addWidget(self.status_label)
|
||||
self.chart_scroll = QScrollArea(right)
|
||||
self.chart_scroll.setWidgetResizable(True)
|
||||
self.chart_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.chart_scroll.setMinimumSize(0, 0)
|
||||
self.chart_scroll.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Expanding)
|
||||
self.chart_scroll.setAccessibleName("可滚动查看全部药味的剂量图")
|
||||
self.chart = _DoseChart(self.chart_scroll)
|
||||
self.chart_scroll.setWidget(self.chart)
|
||||
right_layout.addWidget(self.chart_scroll, 1)
|
||||
footnote = self._label("同组同标尺,组间勿比较;剂量差异不代表医疗优劣。", right, "PrescriptionCaption")
|
||||
footnote.setWordWrap(True)
|
||||
right_layout.addWidget(footnote)
|
||||
panes.addWidget(right, 5)
|
||||
layout.addLayout(panes, 1)
|
||||
self._render()
|
||||
|
||||
@staticmethod
|
||||
def _label(text: str, parent: QWidget, name: str = "") -> QLabel:
|
||||
label = QLabel(text, parent)
|
||||
label.setTextFormat(Qt.TextFormat.PlainText)
|
||||
label.setObjectName(name)
|
||||
return label
|
||||
|
||||
@property
|
||||
def selected_model(self) -> str:
|
||||
return self._model_key
|
||||
|
||||
def minimumSizeHint(self) -> QSize:
|
||||
return QSize(0, 0)
|
||||
|
||||
def set_batch(self, batch: dict) -> None:
|
||||
data = _mapping(batch)
|
||||
if data == self._batch:
|
||||
return
|
||||
self._batch = deepcopy(data)
|
||||
self._render()
|
||||
|
||||
def _select_model(self, model_key: str) -> None:
|
||||
if model_key == self._model_key:
|
||||
return
|
||||
self._model_key = model_key
|
||||
self._render()
|
||||
|
||||
def _state(self, model: dict[str, Any], candidate: dict[str, Any], comparison: dict[str, Any]) -> tuple[bool, str]:
|
||||
if not self._batch:
|
||||
return False, "选择一份报告后,查看已保存的候选药方与剂量对比。"
|
||||
validity = _text(self._batch.get("validity"))
|
||||
if validity and validity not in {"current", "valid"}:
|
||||
return False, (_STALE.get(validity) or "报告有效性未确认") + ";仅查看历史原值,暂停条形比较。"
|
||||
status = _text(model.get("status"))
|
||||
candidate_status = _text(candidate.get("status"))
|
||||
if status in ACTIVE_STATES or candidate_status in ACTIVE_STATES:
|
||||
return False, f"{MODEL_NAMES[self._model_key]}正在生成候选方,完成后显示剂量对比。"
|
||||
if status in _STALE:
|
||||
return False, _STALE[status] + ";仅查看历史原值。"
|
||||
if status in {"failed", "cancelled", "canceled", "blocked"}:
|
||||
return False, "候选方分析未完成;已保存内容仅供查看。"
|
||||
if candidate_status in {"insufficient_data", "withheld_for_risk", "no_medication", "no_medication_recommended"}:
|
||||
headline = {"insufficient_data": "资料不足,暂未提供候选药方", "withheld_for_risk": "因风险暂缓候选用药", "no_medication": "建议暂不使用药物", "no_medication_recommended": "建议暂不使用药物"}[candidate_status]
|
||||
return False, headline + (";" + _reason(candidate.get("reason")) if candidate.get("reason") else "。")
|
||||
if status and status not in SUCCESS_STATES | {"partial"}:
|
||||
return False, "模型状态未确认;仅列示已保存原值。"
|
||||
if candidate_status and candidate_status not in {"available_for_review", "success", "succeeded", "completed"}:
|
||||
return False, "候选方状态未确认;仅列示已保存原值。"
|
||||
if comparison.get("status") == "not_comparable":
|
||||
return False, "本报告不可比:" + (_reason(comparison.get("reason") or comparison.get("reason_code")) or "未通过单位、剂量或资料完整性核验。")
|
||||
saved_rows = comparison.get("rows")
|
||||
if not isinstance(saved_rows, list) or not any(isinstance(row, Mapping) for row in saved_rows):
|
||||
return False, "尚无已保存的逐味对比;医生剂量以—表示,不推算历史处方。"
|
||||
if comparison.get("status") != "comparable":
|
||||
return False, "可比状态未确认;仅列示已保存原值。"
|
||||
prefix = "报告有效性未注明;" if not validity else ""
|
||||
return True, prefix + "按明确单位与每剂/每日基准分组;缺失剂量不按零计算。"
|
||||
|
||||
def _render(self) -> None:
|
||||
model = _mapping(_mapping(self._batch.get("models")).get(self._model_key))
|
||||
candidate, comparison = _mapping(model.get("candidate")), _mapping(model.get("comparison"))
|
||||
self._rows = _saved_rows(candidate, comparison)
|
||||
self._bars_enabled, self._chart_message = self._state(model, candidate, comparison)
|
||||
name = MODEL_NAMES[self._model_key]
|
||||
self.caption_label.setText(f"{name} · 候选药方")
|
||||
candidate_herbs = candidate.get("herbs")
|
||||
self._candidate_count = len(candidate_herbs) if isinstance(candidate_herbs, list) else None
|
||||
has_herbs = isinstance(candidate_herbs, list) and bool(candidate_herbs)
|
||||
if any(row.origin == "original" for row in self._rows):
|
||||
self._chart_message += " 候选原方另行附列;历史对应关系未保存,不推断同药。"
|
||||
elif comparison.get("rows") and any(row.origin == "uncompared" for row in self._rows):
|
||||
self._chart_message += " 未纳入对比的候选药材已补列原值。"
|
||||
empty_title = "候选方生成中" if model.get("status") in ACTIVE_STATES else "暂无候选药方"
|
||||
self.name_label.setText(_text(candidate.get("prescription_name")) or ("已保存候选方" if has_herbs else empty_title))
|
||||
self.name_label.setToolTip(self.name_label.text())
|
||||
usage = []
|
||||
if _text(candidate.get("usage_instruction")):
|
||||
usage.append(_text(candidate["usage_instruction"]))
|
||||
if _number(candidate.get("times_per_day")) is not None:
|
||||
usage.append(f"每日 {candidate['times_per_day']} 次")
|
||||
if _number(candidate.get("usage_days")) is not None:
|
||||
usage.append(f"共 {candidate['usage_days']} 天")
|
||||
usage_text = " · ".join(usage)
|
||||
self.usage_label.setText(usage_text[:100] + ("…" if len(usage_text) > 100 else ""))
|
||||
self.usage_label.setToolTip(usage_text)
|
||||
self.usage_label.setVisible(bool(usage_text))
|
||||
self.status_label.setText(self._chart_message)
|
||||
self.legend.setText(f'<span style="color:{DOCTOR_COLOR}">● 医生</span> <span style="color:{MODEL_COLORS[self._model_key]}">● {name}</span>')
|
||||
self.herb_table.setHorizontalHeaderLabels(["药材 / 主辅方", "医生剂量", f"{name}剂量"])
|
||||
self._filter_rows()
|
||||
|
||||
def _filter_rows(self) -> None:
|
||||
self._updating_rows = True
|
||||
query = self.search.text().strip().casefold()
|
||||
rows = [row for row in self._rows if any(query in name.casefold() for name in (row.name, *row.source_names))]
|
||||
table_scroll = self.herb_table.verticalScrollBar().value()
|
||||
chart_scroll = self.chart_scroll.verticalScrollBar().value()
|
||||
selected_row = self.herb_table.currentRow()
|
||||
selected_name = self.herb_table.item(selected_row, 0).text() if selected_row >= 0 and self.herb_table.item(selected_row, 0) else ""
|
||||
self.herb_table.setRowCount(len(rows))
|
||||
for index, row in enumerate(rows):
|
||||
name_text = row.name + "\n" + row.context
|
||||
instructions = row.usage_description(MODEL_NAMES[self._model_key])
|
||||
if instructions:
|
||||
name_text += "\n" + instructions
|
||||
values = (name_text, row.doctor.label if row.doctor else "—", row.candidate.label if row.candidate else "—")
|
||||
for column, value in enumerate(values):
|
||||
item = QTableWidgetItem(value)
|
||||
item.setToolTip(row.description(MODEL_NAMES[self._model_key]))
|
||||
item.setData(Qt.ItemDataRole.AccessibleTextRole, value)
|
||||
if column == 0:
|
||||
item.setData(Qt.ItemDataRole.UserRole, row)
|
||||
item.setData(Qt.ItemDataRole.AccessibleDescriptionRole, row.description(MODEL_NAMES[self._model_key]))
|
||||
item.setTextAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||
if column == 1:
|
||||
item.setForeground(QColor(DOCTOR_COLOR))
|
||||
if column == 2:
|
||||
item.setForeground(QColor(MODEL_COLORS[self._model_key]))
|
||||
self.herb_table.setItem(index, column, item)
|
||||
self.herb_table.setRowHeight(index, max(48, self.herb_table.fontMetrics().height() * (3 if instructions else 2) + 12))
|
||||
if name_text == selected_name:
|
||||
self.herb_table.selectRow(index)
|
||||
self.herb_table.verticalScrollBar().setValue(table_scroll)
|
||||
self.herb_table.setVisible(bool(rows))
|
||||
self.empty_label.setText("没有匹配的药名,请调整搜索。" if self._rows and not rows else self._chart_message)
|
||||
self.empty_label.setVisible(not rows)
|
||||
comparison_count = sum(row.origin == "comparison" for row in self._rows)
|
||||
uncompared_count = sum(row.origin == "uncompared" for row in self._rows)
|
||||
original_count = sum(row.origin == "original" for row in self._rows)
|
||||
counts = [f"候选原方 {self._candidate_count} 项" if self._candidate_count is not None else "候选原方未保存", f"对比 {comparison_count} 项"]
|
||||
if uncompared_count:
|
||||
counts.append(f"未纳入 {uncompared_count} 项")
|
||||
if original_count:
|
||||
counts.append(f"原方附列 {original_count} 项")
|
||||
if query:
|
||||
counts.append(f"搜索显示 {len(rows)} 项")
|
||||
self.count_label.setText(" · ".join(counts))
|
||||
self.count_label.setToolTip("原方项数来自候选药材完整清单;对比项数来自已保存的标准化记录。附列药材不推断与对比记录的对应关系;— 表示该侧未保存剂量。")
|
||||
self.count_label.setVisible(bool(self._rows))
|
||||
empty_message = "没有匹配的药名" if self._rows and not rows else self._chart_message
|
||||
self.chart.set_rows(rows, self._model_key, bars_enabled=self._bars_enabled, message=empty_message)
|
||||
self.chart_scroll.verticalScrollBar().setValue(chart_scroll)
|
||||
self._updating_rows = False
|
||||
|
||||
def _focus_chart_row(self) -> None:
|
||||
if self._updating_rows:
|
||||
return
|
||||
item = self.herb_table.item(self.herb_table.currentRow(), 0)
|
||||
if item is None:
|
||||
return
|
||||
target = item.data(Qt.ItemDataRole.UserRole)
|
||||
y = 8
|
||||
for _scale, members, _maximum in self.chart.groups:
|
||||
y += self.chart.GROUP_HEIGHT
|
||||
for row in members:
|
||||
if row is target:
|
||||
self.chart_scroll.verticalScrollBar().setValue(y)
|
||||
return
|
||||
y += self.chart.ROW_HEIGHT
|
||||
@@ -0,0 +1,558 @@
|
||||
"""The console chrome: the agreement comparison bar and the numbered step rail.
|
||||
|
||||
Both widgets read only what the saved batch carries. A model without a comparable candidate keeps
|
||||
an empty track instead of a zero-length bar, and the rail's counters stay blank until the batch
|
||||
actually reports them.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from PySide6.QtCore import QPointF, QRectF, QSize, Qt, Signal
|
||||
from PySide6.QtGui import (
|
||||
QBrush,
|
||||
QColor,
|
||||
QFont,
|
||||
QFontMetricsF,
|
||||
QLinearGradient,
|
||||
QPainter,
|
||||
QPainterPath,
|
||||
QPaintEvent,
|
||||
QPalette,
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QProgressBar,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from .issued_prescription_ai_theme import CONSOLE, MODEL_HUE, MODEL_TEXT, num_font
|
||||
|
||||
MODEL_NAMES = {"qwen": "千问", "openai": "OpenAI"}
|
||||
|
||||
|
||||
def _number(value: Any) -> float | None:
|
||||
if value is None or isinstance(value, bool):
|
||||
return None
|
||||
try:
|
||||
parsed = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return parsed
|
||||
|
||||
|
||||
def _mapping(value: Any) -> dict[str, Any]:
|
||||
return dict(value) if isinstance(value, Mapping) else {}
|
||||
|
||||
|
||||
def _score(model: Mapping[str, Any]) -> float | None:
|
||||
comparison = _mapping(_mapping(model).get("comparison"))
|
||||
status = comparison.get("status") or _mapping(model).get("comparison_status")
|
||||
if status != "comparable":
|
||||
return None
|
||||
value = _number(comparison.get("score", _mapping(model).get("score")))
|
||||
return None if value is None or value < 0 or value > 100 else value
|
||||
|
||||
|
||||
class ScaleTrack(QWidget):
|
||||
"""A 0–100 track: the model's own fill, and a grey mark where the other model stands."""
|
||||
|
||||
# The design's scale: a 12px band for the rival's label, a 13px rail, then the axis row.
|
||||
LABEL_BAND = 13.0
|
||||
RAIL_TOP = 16.0
|
||||
RAIL_HEIGHT = 13.0
|
||||
AXIS_TOP = 31.0
|
||||
|
||||
def __init__(self, model_key: str, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.model_key = model_key
|
||||
self._value: float | None = None
|
||||
self._other: float | None = None
|
||||
self._other_label = ""
|
||||
self.setFixedHeight(44)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
||||
|
||||
def set_values(self, value: float | None, other: float | None, other_label: str) -> None:
|
||||
self._value, self._other, self._other_label = value, other, other_label
|
||||
self.setAccessibleDescription("暂无可比结果" if value is None else f"{value:.1f}%")
|
||||
self.setToolTip("" if other is None else f"{other_label} 在 {other:.1f}%")
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event: QPaintEvent) -> None: # noqa: N802 - Qt virtual
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
top, height = self.RAIL_TOP, self.RAIL_HEIGHT
|
||||
radius = height / 2
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.setBrush(QColor(CONSOLE["raised"]))
|
||||
painter.drawRoundedRect(QRectF(0, top, self.width(), height), radius, radius)
|
||||
if self._value:
|
||||
filled = self.width() * self._value / 100
|
||||
hue = QColor(MODEL_HUE[self.model_key])
|
||||
faded = QColor(hue)
|
||||
faded.setAlphaF(0.45)
|
||||
gradient = QLinearGradient(QPointF(0, top), QPointF(filled, top))
|
||||
gradient.setColorAt(0.0, faded)
|
||||
gradient.setColorAt(1.0, hue)
|
||||
painter.setBrush(QBrush(gradient))
|
||||
painter.drawRoundedRect(QRectF(0, top, filled, height), radius, radius)
|
||||
# Quarter dividers sit on the rail itself, as the design draws them.
|
||||
painter.setPen(QColor(CONSOLE["grid_line"]))
|
||||
for fraction in (0.25, 0.5, 0.75):
|
||||
x = self.width() * fraction
|
||||
painter.drawLine(QPointF(x, top), QPointF(x, top + height))
|
||||
axis = QFont(self.font())
|
||||
axis.setPixelSize(9)
|
||||
painter.setFont(axis)
|
||||
painter.setPen(QColor(CONSOLE["faint"]))
|
||||
for fraction in (0.0, 0.25, 0.5, 0.75, 1.0):
|
||||
label = f"{int(fraction * 100)}%"
|
||||
width = 44.0
|
||||
left = self.width() * fraction - width / 2
|
||||
align = Qt.AlignmentFlag.AlignCenter
|
||||
if fraction == 0.0:
|
||||
left, align = 0.0, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
|
||||
elif fraction == 1.0:
|
||||
left, align = self.width() - width, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||
painter.drawText(QRectF(left, self.AXIS_TOP, width, 12), align, label)
|
||||
if self._other is not None:
|
||||
x = self.width() * self._other / 100
|
||||
marker = QColor(CONSOLE["muted"])
|
||||
marker.setAlphaF(0.8)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.setBrush(marker)
|
||||
painter.drawRoundedRect(QRectF(x - 1, top - 4, 2, height + 8), 1, 1)
|
||||
painter.setPen(QColor(CONSOLE["faint"]))
|
||||
painter.drawText(QRectF(max(0.0, min(x - 60, self.width() - 120)), 0, 120, self.LABEL_BAND),
|
||||
Qt.AlignmentFlag.AlignCenter, f"{self._other_label} 在此")
|
||||
painter.end()
|
||||
|
||||
|
||||
class ScoreLabel(QLabel):
|
||||
"""The big figure with its unit set small and raised, the way the design prints it.
|
||||
|
||||
``text()`` still returns the whole string, so callers and tests read one plain value.
|
||||
"""
|
||||
|
||||
SIZE = 27
|
||||
UNIT_SIZE = 13
|
||||
|
||||
def _parts(self) -> tuple[str, str]:
|
||||
text = self.text()
|
||||
for unit in ("%", "pt"):
|
||||
if text.endswith(unit) and len(text) > len(unit):
|
||||
return text[: -len(unit)], unit
|
||||
return text, ""
|
||||
|
||||
def sizeHint(self) -> QSize: # noqa: N802 - Qt virtual
|
||||
figure, unit = self._parts()
|
||||
width = QFontMetricsF(num_font(self.SIZE, weight=QFont.Weight.DemiBold)).horizontalAdvance(figure)
|
||||
if unit:
|
||||
width += 2 + QFontMetricsF(num_font(self.UNIT_SIZE)).horizontalAdvance(unit)
|
||||
return QSize(int(width) + 1, int(self.SIZE * 1.1) + 1)
|
||||
|
||||
def minimumSizeHint(self) -> QSize: # noqa: N802 - Qt virtual
|
||||
return self.sizeHint()
|
||||
|
||||
def paintEvent(self, event: QPaintEvent) -> None: # noqa: N802 - Qt virtual
|
||||
figure, unit = self._parts()
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
figure_font = num_font(self.SIZE, weight=QFont.Weight.DemiBold)
|
||||
painter.setFont(figure_font)
|
||||
painter.setPen(self.palette().color(QPalette.ColorRole.WindowText))
|
||||
metrics = QFontMetricsF(figure_font)
|
||||
baseline = (self.height() + metrics.capHeight()) / 2
|
||||
painter.drawText(QPointF(0, baseline), figure)
|
||||
if unit:
|
||||
unit_font = num_font(self.UNIT_SIZE)
|
||||
painter.setFont(unit_font)
|
||||
painter.setPen(QColor(CONSOLE["muted"]))
|
||||
painter.drawText(QPointF(metrics.horizontalAdvance(figure) + 2,
|
||||
baseline - metrics.capHeight() + QFontMetricsF(unit_font).capHeight()),
|
||||
unit)
|
||||
painter.end()
|
||||
|
||||
|
||||
class AgreementBar(QFrame):
|
||||
"""Both models' agreement on one scale, with the gap between them stated in points."""
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setObjectName("AiAgreement")
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(22, 16, 22, 16)
|
||||
layout.setSpacing(24)
|
||||
self.blocks: dict[str, dict[str, Any]] = {}
|
||||
for index, key in enumerate(("qwen", "openai")):
|
||||
if index:
|
||||
divider = QFrame(self)
|
||||
divider.setObjectName("AiFactDivider")
|
||||
divider.setFixedWidth(1)
|
||||
layout.addWidget(divider)
|
||||
layout.addWidget(self._delta_block())
|
||||
divider = QFrame(self)
|
||||
divider.setObjectName("AiFactDivider")
|
||||
divider.setFixedWidth(1)
|
||||
layout.addWidget(divider)
|
||||
# Both heads line up at the top, so a model that failed does not slide its column down.
|
||||
layout.addWidget(self._model_block(key), 1, Qt.AlignmentFlag.AlignTop)
|
||||
|
||||
def paintEvent(self, event: QPaintEvent) -> None: # noqa: N802 - Qt virtual
|
||||
"""The design runs a blue-to-violet bar down the card's left edge, inside its rounding."""
|
||||
|
||||
super().paintEvent(event)
|
||||
card = QPainterPath()
|
||||
card.addRoundedRect(QRectF(1, 1, self.width() - 2, self.height() - 2), 9, 9)
|
||||
strip = QPainterPath()
|
||||
strip.addRect(QRectF(0, 0, 4, self.height()))
|
||||
gradient = QLinearGradient(QPointF(0, 0), QPointF(0, self.height()))
|
||||
gradient.setColorAt(0.0, QColor(CONSOLE["accent"]))
|
||||
gradient.setColorAt(1.0, QColor(CONSOLE["openai"]))
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.fillPath(card.intersected(strip), QBrush(gradient))
|
||||
painter.end()
|
||||
|
||||
def _model_block(self, key: str) -> QWidget:
|
||||
holder = QWidget(self)
|
||||
column = QVBoxLayout(holder)
|
||||
column.setContentsMargins(0, 0, 0, 0)
|
||||
column.setSpacing(9)
|
||||
head = QHBoxLayout()
|
||||
head.setSpacing(9)
|
||||
dot = QLabel(holder)
|
||||
dot.setFixedSize(9, 9)
|
||||
dot.setStyleSheet(f"background: {MODEL_HUE[key]}; border-radius: 3px;")
|
||||
head.addWidget(dot)
|
||||
name = QLabel(MODEL_NAMES[key], holder)
|
||||
name.setStyleSheet(f"color: {CONSOLE['heading']}; font-size: 13.8px; font-weight: 600;")
|
||||
head.addWidget(name)
|
||||
status = QLabel("尚无报告", holder)
|
||||
status.setTextFormat(Qt.TextFormat.PlainText)
|
||||
head.addWidget(status)
|
||||
coverage = QLabel("", holder)
|
||||
coverage.setTextFormat(Qt.TextFormat.PlainText)
|
||||
coverage.setStyleSheet(
|
||||
f"background: {CONSOLE['amber_dim']}; color: {CONSOLE['amber_text']};"
|
||||
f" border: 1px solid {CONSOLE['amber']}; border-radius: 9px; padding: 1px 8px; font-size: 9.9px;")
|
||||
head.addWidget(coverage)
|
||||
elapsed = QLabel("", holder)
|
||||
elapsed.setTextFormat(Qt.TextFormat.PlainText)
|
||||
elapsed.setStyleSheet(
|
||||
f"background: transparent; color: {CONSOLE['faint']};"
|
||||
f" border: 1px solid {CONSOLE['line']}; border-radius: 9px; padding: 1px 8px; font-size: 9.9px;")
|
||||
head.addWidget(elapsed)
|
||||
head.addStretch(1)
|
||||
column.addLayout(head)
|
||||
score = ScoreLabel("—", holder)
|
||||
score.setObjectName(f"AiAgreementScore{key.capitalize()}")
|
||||
score.setTextFormat(Qt.TextFormat.PlainText)
|
||||
column.addWidget(score)
|
||||
caption = QLabel("药味与剂量一致率", holder)
|
||||
caption.setStyleSheet(f"color: {CONSOLE['faint']}; font-size: 10.5px;")
|
||||
column.addWidget(caption)
|
||||
track = ScaleTrack(key, holder)
|
||||
column.addWidget(track)
|
||||
|
||||
# A failed model states why it stopped and offers its retry on its own block, which is
|
||||
# the only place in the console that belongs to that model alone.
|
||||
failure = QWidget(holder)
|
||||
failure_row = QHBoxLayout(failure)
|
||||
failure_row.setContentsMargins(0, 2, 0, 0)
|
||||
failure_row.setSpacing(8)
|
||||
failure_text = QLabel("", failure)
|
||||
failure_text.setTextFormat(Qt.TextFormat.PlainText)
|
||||
failure_text.setWordWrap(True)
|
||||
failure_text.setStyleSheet(f"color: {CONSOLE['rose_text']}; font-size: 12px;")
|
||||
failure_row.addWidget(failure_text, 1)
|
||||
retry_slot = QHBoxLayout()
|
||||
retry_slot.setContentsMargins(0, 0, 0, 0)
|
||||
retry_slot.setSpacing(6)
|
||||
failure_row.addLayout(retry_slot)
|
||||
failure.setVisible(False)
|
||||
column.addWidget(failure)
|
||||
|
||||
stage = QLabel("等待处理进度", holder)
|
||||
stage.setTextFormat(Qt.TextFormat.PlainText)
|
||||
stage.setWordWrap(True)
|
||||
stage.setStyleSheet(f"color: {MODEL_TEXT[key]}; font-size: 12px;")
|
||||
stage.hide()
|
||||
# The plain bar stays as a value carrier for callers and accessibility tools; the painted
|
||||
# track above is the visual, so it is never added to the layout.
|
||||
carrier = QProgressBar(holder)
|
||||
carrier.setRange(0, 1000)
|
||||
carrier.setTextVisible(False)
|
||||
carrier.setFixedHeight(4)
|
||||
carrier.setAccessibleName(f"{MODEL_NAMES[key]}与医生方的药味及剂量一致度")
|
||||
carrier.setVisible(False)
|
||||
self.blocks[key] = {"score": score, "coverage": coverage, "elapsed": elapsed, "track": track,
|
||||
"status": status, "failure": failure, "failure_text": failure_text,
|
||||
"retry_slot": retry_slot, "stage": stage, "carrier": carrier}
|
||||
return holder
|
||||
|
||||
def views(self, key: str) -> dict[str, Any]:
|
||||
"""Widget map kept stable for the dialog and its regression tests."""
|
||||
|
||||
block = self.blocks[key]
|
||||
return {"card": self, "model_label": block["score"], "status_chip": block["status"],
|
||||
"coverage_chip": block["coverage"], "score": block["score"], "elapsed": block["elapsed"],
|
||||
"agreement_bar": block["carrier"], "gauge": block["track"], "stage": block["stage"],
|
||||
"failure": block["failure"], "failure_text": block["failure_text"]}
|
||||
|
||||
def attach_action(self, key: str, button: QWidget) -> None:
|
||||
"""Host a dialog-owned action (retry) inside that model's failure row."""
|
||||
|
||||
self.blocks[key]["retry_slot"].addWidget(button)
|
||||
|
||||
def set_failure(self, key: str, message: str, retryable: bool) -> None:
|
||||
block = self.blocks[key]
|
||||
block["failure_text"].setText(message)
|
||||
block["failure"].setVisible(bool(message))
|
||||
slot = block["retry_slot"]
|
||||
for index in range(slot.count()):
|
||||
widget = slot.itemAt(index).widget()
|
||||
if widget is not None:
|
||||
widget.setVisible(retryable)
|
||||
|
||||
def _delta_block(self) -> QWidget:
|
||||
holder = QWidget(self)
|
||||
holder.setFixedWidth(180)
|
||||
column = QVBoxLayout(holder)
|
||||
column.setContentsMargins(0, 6, 0, 0)
|
||||
column.setSpacing(2)
|
||||
column.addStretch(1)
|
||||
self.delta = QLabel("—", holder)
|
||||
self.delta.setTextFormat(Qt.TextFormat.PlainText)
|
||||
self.delta.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.delta.setFont(num_font(20, weight=QFont.Weight.DemiBold))
|
||||
self.delta.setStyleSheet(f"color: {CONSOLE['accent_text']}; font-size: 19.5px; font-weight: 600;")
|
||||
column.addWidget(self.delta)
|
||||
self.delta_note = QLabel("等待两个模型", holder)
|
||||
self.delta_note.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.delta_note.setStyleSheet(f"color: {CONSOLE['faint']}; font-size: 10.2px;")
|
||||
column.addWidget(self.delta_note)
|
||||
self.overlap = QLabel("", holder)
|
||||
self.overlap.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.overlap.setStyleSheet(f"color: {CONSOLE['faint']}; font-size: 10.2px;")
|
||||
column.addWidget(self.overlap)
|
||||
column.addStretch(1)
|
||||
return holder
|
||||
|
||||
def apply(self, batch: Mapping[str, Any] | None, *, coverage: Mapping[str, str] | None = None,
|
||||
elapsed: Mapping[str, str] | None = None) -> None:
|
||||
data = _mapping(batch)
|
||||
models = _mapping(data.get("models"))
|
||||
scores = {key: _score(_mapping(models.get(key))) for key in ("qwen", "openai")}
|
||||
for key in ("qwen", "openai"):
|
||||
block = self.blocks[key]
|
||||
value = scores[key]
|
||||
block["score"].setText("—" if value is None else f"{value:.1f}%")
|
||||
other = scores["openai" if key == "qwen" else "qwen"]
|
||||
block["track"].set_values(value, other, MODEL_NAMES["openai" if key == "qwen" else "qwen"])
|
||||
text = _mapping(coverage).get(key, "")
|
||||
block["coverage"].setText(text)
|
||||
block["coverage"].setVisible(bool(text))
|
||||
spent = _mapping(elapsed).get(key, "")
|
||||
block["elapsed"].setText(spent)
|
||||
block["elapsed"].setVisible(bool(spent))
|
||||
if scores["qwen"] is None or scores["openai"] is None:
|
||||
self.delta.setText("—")
|
||||
self.delta_note.setText("两个模型都可比后才给差值")
|
||||
self.overlap.setText("")
|
||||
return
|
||||
gap = scores["qwen"] - scores["openai"]
|
||||
leader = MODEL_NAMES["qwen"] if gap >= 0 else MODEL_NAMES["openai"]
|
||||
self.delta.setText(f"{'+' if gap >= 0 else '−'}{abs(gap):.1f}pt")
|
||||
self.delta_note.setText(f"{leader}领先" if gap else "两模型持平")
|
||||
overlaps = []
|
||||
for key in ("qwen", "openai"):
|
||||
herb = _number(_mapping(_mapping(models.get(key)).get("comparison")).get("herb_score"))
|
||||
overlaps.append("—" if herb is None else f"{herb:.1f}%")
|
||||
self.overlap.setText("药味重合 " + " / ".join(overlaps))
|
||||
|
||||
|
||||
class _StepButton(QPushButton):
|
||||
"""A step row; the design marks the selected one with a rounded bar down its left edge."""
|
||||
|
||||
BAR_INSET = 9
|
||||
BAR_WIDTH = 3
|
||||
|
||||
def paintEvent(self, event: Any) -> None: # noqa: N802 - Qt virtual
|
||||
super().paintEvent(event)
|
||||
if not self.isChecked():
|
||||
return
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.setBrush(QColor(CONSOLE["accent"]))
|
||||
painter.drawRoundedRect(
|
||||
QRectF(0, self.BAR_INSET, self.BAR_WIDTH, self.height() - self.BAR_INSET * 2), 1.5, 1.5)
|
||||
painter.end()
|
||||
|
||||
|
||||
class StepRail(QFrame):
|
||||
"""The six destinations as numbered steps, with what this batch needs looked at below them."""
|
||||
|
||||
selected = Signal(str)
|
||||
save_requested = Signal()
|
||||
|
||||
FOCUS_ROWS = (("differences", "剂量差异", "项", "amber_text"),
|
||||
("critical", "关键缺口", "项", "rose_text"),
|
||||
("restricted", "附件受限", "个", "text"),
|
||||
("consensus", "三方共识", "味", "accent_text"))
|
||||
|
||||
def __init__(self, steps: tuple[tuple[str, str], ...], parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setObjectName("AiRail")
|
||||
self.setFixedWidth(232)
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(12)
|
||||
|
||||
nav = QFrame(self)
|
||||
nav.setObjectName("AiPanel")
|
||||
nav_layout = QVBoxLayout(nav)
|
||||
nav_layout.setContentsMargins(6, 6, 6, 6)
|
||||
nav_layout.setSpacing(0)
|
||||
self._tones: dict[str, str] = {}
|
||||
self.buttons: dict[str, QPushButton] = {}
|
||||
self.badges: dict[str, QLabel] = {}
|
||||
self.numbers: dict[str, QLabel] = {}
|
||||
self.names: dict[str, QLabel] = {}
|
||||
for index, (key, text) in enumerate(steps, start=1):
|
||||
button = _StepButton(nav)
|
||||
button.setObjectName("AiStep")
|
||||
button.setCheckable(True)
|
||||
button.setChecked(index == 1)
|
||||
button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
button.setFixedHeight(42)
|
||||
button.clicked.connect(lambda _checked=False, target=key: self.selected.emit(target))
|
||||
row = QHBoxLayout(button)
|
||||
row.setContentsMargins(11, 0, 11, 0)
|
||||
row.setSpacing(10)
|
||||
number = QLabel(f"{index:02d}", button)
|
||||
number.setObjectName("AiStepNumber")
|
||||
number.setFixedSize(22, 22)
|
||||
number.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
number.setFont(num_font(10, weight=QFont.Weight.Bold))
|
||||
row.addWidget(number)
|
||||
name = QLabel(text, button)
|
||||
name.setObjectName("AiStepName")
|
||||
row.addWidget(name, 1)
|
||||
badge = QLabel("", button)
|
||||
badge.setObjectName("AiStepBadge")
|
||||
badge.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
badge.setMinimumWidth(20)
|
||||
badge.setFixedHeight(17)
|
||||
badge.setFont(num_font(10))
|
||||
row.addWidget(badge, 0, Qt.AlignmentFlag.AlignVCenter)
|
||||
self.buttons[key] = button
|
||||
self.badges[key] = badge
|
||||
self.numbers[key] = number
|
||||
self.names[key] = name
|
||||
nav_layout.addWidget(button)
|
||||
layout.addWidget(nav)
|
||||
self.set_current(steps[0][0] if steps else "")
|
||||
|
||||
focus = QFrame(self)
|
||||
focus.setObjectName("AiPanel")
|
||||
focus_layout = QVBoxLayout(focus)
|
||||
focus_layout.setContentsMargins(12, 13, 12, 14)
|
||||
focus_layout.setSpacing(7)
|
||||
caption = QLabel("本次关注", focus)
|
||||
caption.setStyleSheet(f"color: {CONSOLE['faint']}; font-size: 10.2px; letter-spacing: 1.4px;"
|
||||
" padding-left: 4px;")
|
||||
focus_layout.addWidget(caption)
|
||||
self.focus_values: dict[str, QLabel] = {}
|
||||
for key, text, unit, tone in self.FOCUS_ROWS:
|
||||
row = QFrame(focus)
|
||||
row.setObjectName("AiFocusRow")
|
||||
row_layout = QHBoxLayout(row)
|
||||
row_layout.setContentsMargins(10, 8, 10, 8)
|
||||
row_layout.setSpacing(8)
|
||||
name = QLabel(text, row)
|
||||
name.setStyleSheet(f"color: {CONSOLE['muted']}; font-size: 11.25px;")
|
||||
row_layout.addWidget(name)
|
||||
row_layout.addStretch(1)
|
||||
value = QLabel(f"— {unit}", row)
|
||||
value.setTextFormat(Qt.TextFormat.PlainText)
|
||||
value.setFont(num_font(14))
|
||||
value.setStyleSheet(f"color: {CONSOLE[tone]};")
|
||||
row_layout.addWidget(value)
|
||||
self.focus_values[key] = value
|
||||
focus_layout.addWidget(row)
|
||||
self.save_button = QPushButton("保存本次复核", focus)
|
||||
self.save_button.setObjectName("AiPrimaryAction")
|
||||
self.save_button.setFixedHeight(34)
|
||||
self.save_button.clicked.connect(self.save_requested.emit)
|
||||
focus_layout.addWidget(self.save_button)
|
||||
layout.addWidget(focus)
|
||||
layout.addStretch(1)
|
||||
|
||||
BADGE_TONES = {"hot": ("rose_dim", "rose_text"), "warn": ("amber_dim", "amber_text")}
|
||||
|
||||
def set_current(self, key: str) -> None:
|
||||
"""The selected step is a tinted row with a blue index chip, not a solid blue button."""
|
||||
|
||||
for target, button in self.buttons.items():
|
||||
selected = target == key
|
||||
button.setChecked(selected)
|
||||
self.numbers[target].setStyleSheet(
|
||||
f"background: {CONSOLE['accent'] if selected else CONSOLE['raised']};"
|
||||
f" color: {'#F2F7FF' if selected else CONSOLE['faint']}; border-radius: 6px;")
|
||||
self.names[target].setStyleSheet(
|
||||
f"color: {CONSOLE['heading'] if selected else CONSOLE['muted']}; font-size: 12.6px;"
|
||||
+ (" font-weight: 600;" if selected else ""))
|
||||
self._paint_badge(target)
|
||||
|
||||
def _paint_badge(self, key: str) -> None:
|
||||
background, colour = self.BADGE_TONES.get(self._tones.get(key, ""), ("raised", "faint"))
|
||||
self.badges[key].setStyleSheet(
|
||||
f"background: {CONSOLE[background]}; color: {CONSOLE[colour]};"
|
||||
" border-radius: 8px; padding: 0 6px;")
|
||||
|
||||
def set_badges(self, counts: Mapping[str, Any], tones: Mapping[str, str] | None = None) -> None:
|
||||
"""Counts, and the severity that decides whether one reads as rose, amber or quiet."""
|
||||
|
||||
self._tones = dict(tones or {})
|
||||
for key, badge in self.badges.items():
|
||||
value = counts.get(key)
|
||||
badge.setText("" if value in (None, "") else str(value))
|
||||
badge.setVisible(bool(badge.text()))
|
||||
self._paint_badge(key)
|
||||
|
||||
def set_focus(self, counts: Mapping[str, Any]) -> None:
|
||||
for key, _text, unit, _tone in self.FOCUS_ROWS:
|
||||
value = counts.get(key)
|
||||
self.focus_values[key].setText(f"{'—' if value is None else value} {unit}")
|
||||
|
||||
|
||||
def rail_qss() -> str:
|
||||
"""The rail and agreement styles for the palette that is active right now."""
|
||||
|
||||
return f"""
|
||||
QFrame#AiRail {{ background: transparent; border: 0; }}
|
||||
QFrame#AiFocusRow {{ background: {CONSOLE['surface_2']}; border: 0; border-radius: 7px; }}
|
||||
QPushButton#AiStep {{ background: transparent; border: 1px solid transparent; border-radius: 7px;
|
||||
text-align: left; }}
|
||||
QPushButton#AiStep:hover {{ background: {CONSOLE['surface_2']}; }}
|
||||
QPushButton#AiStep:checked {{ background: {CONSOLE['selection']};
|
||||
border-color: {CONSOLE['selection_line']}; }}
|
||||
QLabel#AiStepNumber {{ color: {CONSOLE['faint']}; }}
|
||||
QLabel#AiStepName {{ color: {CONSOLE['muted']}; font-size: 12.6px; }}
|
||||
QLabel#AiStepBadge {{ color: {CONSOLE['faint']}; background: {CONSOLE['raised']};
|
||||
border-radius: 8px; padding: 0 6px; }}
|
||||
QFrame#AiAgreement {{ background: {CONSOLE['surface']}; border: 1px solid {CONSOLE['line_soft']};
|
||||
border-radius: 10px; }}
|
||||
QLabel#AiAgreementScoreQwen {{ color: {MODEL_TEXT['qwen']}; font-size: 27px; font-weight: 600; }}
|
||||
QLabel#AiAgreementScoreOpenai {{ color: {MODEL_TEXT['openai']}; font-size: 27px; font-weight: 600; }}
|
||||
"""
|
||||
@@ -0,0 +1,254 @@
|
||||
"""Painted glyphs for the prescription analysis window.
|
||||
|
||||
The window ships no image assets, so every icon in the design is drawn here with QPainter at the
|
||||
size it is used. Each glyph is line art on a transparent background; the colour is supplied by the
|
||||
caller, which keeps a glyph usable on a card, inside a tinted tile, or on the navigation bar.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import QPointF, QRectF, QSize, Qt
|
||||
from PySide6.QtGui import (
|
||||
QBrush,
|
||||
QColor,
|
||||
QIcon,
|
||||
QLinearGradient,
|
||||
QPainter,
|
||||
QPaintEvent,
|
||||
QPen,
|
||||
QPixmap,
|
||||
QPolygonF,
|
||||
)
|
||||
from PySide6.QtWidgets import QSizePolicy, QWidget
|
||||
|
||||
from .issued_prescription_ai_theme import CONSOLE as TECH_BLUE
|
||||
|
||||
|
||||
def _pen(painter: QPainter, colour: str, width: float) -> QPen:
|
||||
pen = QPen(QColor(colour))
|
||||
pen.setWidthF(width)
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
return pen
|
||||
|
||||
|
||||
def paint_glyph(painter: QPainter, kind: str, box: QRectF, colour: str) -> None:
|
||||
"""Draw one glyph inside ``box``. Unknown names draw nothing rather than a placeholder."""
|
||||
|
||||
painter.save()
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
x, y, w, h = box.x(), box.y(), box.width(), box.height()
|
||||
stroke = max(1.2, min(w, h) / 11)
|
||||
if kind == "bars":
|
||||
_pen(painter, colour, stroke)
|
||||
for index, height in enumerate((0.45, 0.75, 0.6)):
|
||||
left = x + w * (0.22 + index * 0.28)
|
||||
painter.drawLine(QPointF(left, y + h * 0.82), QPointF(left, y + h * (0.82 - height)))
|
||||
elif kind == "doc":
|
||||
_pen(painter, colour, stroke)
|
||||
painter.drawRoundedRect(QRectF(x + w * 0.22, y + h * 0.12, w * 0.56, h * 0.76), w * 0.08, w * 0.08)
|
||||
for index in range(2):
|
||||
top = y + h * (0.38 + index * 0.2)
|
||||
painter.drawLine(QPointF(x + w * 0.34, top), QPointF(x + w * 0.66, top))
|
||||
elif kind == "box":
|
||||
_pen(painter, colour, stroke)
|
||||
top, bottom, middle = y + h * 0.2, y + h * 0.8, y + h * 0.5
|
||||
left, right = x + w * 0.16, x + w * 0.84
|
||||
painter.drawPolygon(QPolygonF([QPointF(x + w * 0.5, top), QPointF(right, middle * 0.75 + top * 0.25),
|
||||
QPointF(right, bottom - h * 0.12), QPointF(x + w * 0.5, bottom),
|
||||
QPointF(left, bottom - h * 0.12), QPointF(left, middle * 0.75 + top * 0.25)]))
|
||||
painter.drawLine(QPointF(x + w * 0.5, y + h * 0.5), QPointF(x + w * 0.5, bottom))
|
||||
elif kind == "image":
|
||||
_pen(painter, colour, stroke)
|
||||
painter.drawRoundedRect(QRectF(x + w * 0.16, y + h * 0.22, w * 0.68, h * 0.56), w * 0.08, w * 0.08)
|
||||
painter.drawPolyline(QPolygonF([QPointF(x + w * 0.24, y + h * 0.7), QPointF(x + w * 0.42, y + h * 0.48),
|
||||
QPointF(x + w * 0.58, y + h * 0.66), QPointF(x + w * 0.68, y + h * 0.56)]))
|
||||
painter.setBrush(QColor(colour))
|
||||
painter.drawEllipse(QPointF(x + w * 0.64, y + h * 0.36), stroke * 0.9, stroke * 0.9)
|
||||
elif kind == "clock":
|
||||
_pen(painter, colour, stroke)
|
||||
painter.drawEllipse(QRectF(x + w * 0.16, y + h * 0.16, w * 0.68, h * 0.68))
|
||||
centre = QPointF(x + w * 0.5, y + h * 0.5)
|
||||
painter.drawLine(centre, QPointF(x + w * 0.5, y + h * 0.3))
|
||||
painter.drawLine(centre, QPointF(x + w * 0.66, y + h * 0.58))
|
||||
elif kind == "trend":
|
||||
_pen(painter, colour, stroke)
|
||||
painter.drawPolyline(QPolygonF([QPointF(x + w * 0.18, y + h * 0.68), QPointF(x + w * 0.4, y + h * 0.46),
|
||||
QPointF(x + w * 0.56, y + h * 0.58), QPointF(x + w * 0.82, y + h * 0.28)]))
|
||||
painter.drawPolyline(QPolygonF([QPointF(x + w * 0.62, y + h * 0.28), QPointF(x + w * 0.82, y + h * 0.28),
|
||||
QPointF(x + w * 0.82, y + h * 0.48)]))
|
||||
elif kind == "bell":
|
||||
_pen(painter, colour, stroke)
|
||||
painter.drawPolyline(QPolygonF([
|
||||
QPointF(x + w * 0.24, y + h * 0.68), QPointF(x + w * 0.3, y + h * 0.58),
|
||||
QPointF(x + w * 0.3, y + h * 0.42), QPointF(x + w * 0.5, y + h * 0.2),
|
||||
QPointF(x + w * 0.7, y + h * 0.42), QPointF(x + w * 0.7, y + h * 0.58),
|
||||
QPointF(x + w * 0.76, y + h * 0.68), QPointF(x + w * 0.24, y + h * 0.68)]))
|
||||
painter.drawArc(QRectF(x + w * 0.4, y + h * 0.66, w * 0.2, h * 0.18), 0, -180 * 16)
|
||||
elif kind == "clipboard":
|
||||
_pen(painter, colour, stroke)
|
||||
painter.drawRoundedRect(QRectF(x + w * 0.22, y + h * 0.2, w * 0.56, h * 0.66), w * 0.08, w * 0.08)
|
||||
painter.drawLine(QPointF(x + w * 0.36, y + h * 0.48), QPointF(x + w * 0.64, y + h * 0.48))
|
||||
painter.drawLine(QPointF(x + w * 0.36, y + h * 0.64), QPointF(x + w * 0.56, y + h * 0.64))
|
||||
elif kind == "pencil":
|
||||
_pen(painter, colour, stroke)
|
||||
painter.drawPolyline(QPolygonF([QPointF(x + w * 0.24, y + h * 0.76), QPointF(x + w * 0.28, y + h * 0.6),
|
||||
QPointF(x + w * 0.64, y + h * 0.24), QPointF(x + w * 0.78, y + h * 0.38),
|
||||
QPointF(x + w * 0.42, y + h * 0.74), QPointF(x + w * 0.24, y + h * 0.76)]))
|
||||
elif kind == "bulb":
|
||||
_pen(painter, colour, stroke)
|
||||
painter.drawArc(QRectF(x + w * 0.28, y + h * 0.18, w * 0.44, h * 0.46), 0, 180 * 16)
|
||||
painter.drawLine(QPointF(x + w * 0.28, y + h * 0.41), QPointF(x + w * 0.38, y + h * 0.62))
|
||||
painter.drawLine(QPointF(x + w * 0.72, y + h * 0.41), QPointF(x + w * 0.62, y + h * 0.62))
|
||||
painter.drawLine(QPointF(x + w * 0.38, y + h * 0.66), QPointF(x + w * 0.62, y + h * 0.66))
|
||||
painter.drawLine(QPointF(x + w * 0.42, y + h * 0.78), QPointF(x + w * 0.58, y + h * 0.78))
|
||||
elif kind == "flask":
|
||||
_pen(painter, colour, stroke)
|
||||
painter.drawLine(QPointF(x + w * 0.38, y + h * 0.2), QPointF(x + w * 0.62, y + h * 0.2))
|
||||
painter.drawPolyline(QPolygonF([QPointF(x + w * 0.44, y + h * 0.2), QPointF(x + w * 0.44, y + h * 0.44),
|
||||
QPointF(x + w * 0.24, y + h * 0.78), QPointF(x + w * 0.76, y + h * 0.78),
|
||||
QPointF(x + w * 0.56, y + h * 0.44), QPointF(x + w * 0.56, y + h * 0.2)]))
|
||||
elif kind == "alert":
|
||||
_pen(painter, colour, stroke)
|
||||
painter.drawEllipse(QRectF(x + w * 0.16, y + h * 0.16, w * 0.68, h * 0.68))
|
||||
painter.drawLine(QPointF(x + w * 0.5, y + h * 0.33), QPointF(x + w * 0.5, y + h * 0.56))
|
||||
painter.setBrush(QColor(colour))
|
||||
painter.drawEllipse(QPointF(x + w * 0.5, y + h * 0.68), stroke * 0.7, stroke * 0.7)
|
||||
elif kind == "paperclip":
|
||||
_pen(painter, colour, stroke)
|
||||
painter.drawRoundedRect(QRectF(x + w * 0.18, y + h * 0.22, w * 0.64, h * 0.5), w * 0.1, w * 0.1)
|
||||
painter.drawLine(QPointF(x + w * 0.3, y + h * 0.72), QPointF(x + w * 0.3, y + h * 0.84))
|
||||
elif kind == "info":
|
||||
_pen(painter, colour, stroke)
|
||||
painter.drawEllipse(QRectF(x + w * 0.16, y + h * 0.16, w * 0.68, h * 0.68))
|
||||
painter.drawLine(QPointF(x + w * 0.5, y + h * 0.46), QPointF(x + w * 0.5, y + h * 0.68))
|
||||
painter.setBrush(QColor(colour))
|
||||
painter.drawEllipse(QPointF(x + w * 0.5, y + h * 0.34), stroke * 0.7, stroke * 0.7)
|
||||
elif kind == "refresh":
|
||||
_pen(painter, colour, stroke)
|
||||
painter.drawArc(QRectF(x + w * 0.2, y + h * 0.2, w * 0.6, h * 0.6), 40 * 16, 280 * 16)
|
||||
painter.setBrush(QColor(colour))
|
||||
painter.drawPolygon(QPolygonF([QPointF(x + w * 0.72, y + h * 0.12), QPointF(x + w * 0.86, y + h * 0.34),
|
||||
QPointF(x + w * 0.6, y + h * 0.32)]))
|
||||
elif kind == "save":
|
||||
_pen(painter, colour, stroke)
|
||||
painter.drawRoundedRect(QRectF(x + w * 0.2, y + h * 0.2, w * 0.6, h * 0.6), w * 0.08, w * 0.08)
|
||||
painter.drawLine(QPointF(x + w * 0.36, y + h * 0.2), QPointF(x + w * 0.36, y + h * 0.42))
|
||||
painter.drawLine(QPointF(x + w * 0.36, y + h * 0.42), QPointF(x + w * 0.64, y + h * 0.42))
|
||||
painter.drawLine(QPointF(x + w * 0.64, y + h * 0.42), QPointF(x + w * 0.64, y + h * 0.2))
|
||||
painter.drawRect(QRectF(x + w * 0.36, y + h * 0.56, w * 0.28, h * 0.24))
|
||||
elif kind == "chevron":
|
||||
_pen(painter, colour, stroke)
|
||||
painter.drawPolyline(QPolygonF([QPointF(x + w * 0.4, y + h * 0.28), QPointF(x + w * 0.62, y + h * 0.5),
|
||||
QPointF(x + w * 0.4, y + h * 0.72)]))
|
||||
elif kind == "shield":
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.setBrush(QColor(colour))
|
||||
painter.drawPolygon(QPolygonF([QPointF(x + w * 0.5, y + h * 0.16), QPointF(x + w * 0.82, y + h * 0.3),
|
||||
QPointF(x + w * 0.82, y + h * 0.56), QPointF(x + w * 0.5, y + h * 0.84),
|
||||
QPointF(x + w * 0.18, y + h * 0.56), QPointF(x + w * 0.18, y + h * 0.3)]))
|
||||
elif kind == "check":
|
||||
_pen(painter, colour, stroke * 1.2)
|
||||
painter.drawPolyline(QPolygonF([QPointF(x + w * 0.32, y + h * 0.52), QPointF(x + w * 0.45, y + h * 0.65),
|
||||
QPointF(x + w * 0.7, y + h * 0.37)]))
|
||||
elif kind == "spark":
|
||||
# The 千问 mark: three crossing strokes forming a six-pointed star.
|
||||
_pen(painter, colour, stroke * 1.1)
|
||||
centre = QPointF(x + w * 0.5, y + h * 0.5)
|
||||
radius = min(w, h) * 0.3
|
||||
for angle in (90, 30, -30):
|
||||
from math import cos, radians, sin
|
||||
dx, dy = cos(radians(angle)) * radius, -sin(radians(angle)) * radius
|
||||
painter.drawLine(QPointF(centre.x() - dx, centre.y() - dy), QPointF(centre.x() + dx, centre.y() + dy))
|
||||
elif kind == "knot":
|
||||
# The OpenAI mark, reduced to the interlocking hexagon it is built from.
|
||||
_pen(painter, colour, stroke)
|
||||
from math import cos, radians, sin
|
||||
centre = QPointF(x + w * 0.5, y + h * 0.5)
|
||||
radius = min(w, h) * 0.3
|
||||
points = [QPointF(centre.x() + cos(radians(angle)) * radius, centre.y() + sin(radians(angle)) * radius)
|
||||
for angle in range(0, 360, 60)]
|
||||
painter.drawPolygon(QPolygonF(points))
|
||||
painter.drawLine(points[0], points[3])
|
||||
painter.restore()
|
||||
|
||||
|
||||
class Glyph(QWidget):
|
||||
"""A single painted icon at a fixed size."""
|
||||
|
||||
def __init__(self, kind: str, colour: str, size: int = 18, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.kind, self.colour = kind, colour
|
||||
self.setFixedSize(size, size)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
||||
|
||||
def set_colour(self, colour: str) -> None:
|
||||
if colour != self.colour:
|
||||
self.colour = colour
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event: QPaintEvent) -> None: # noqa: N802 - Qt virtual
|
||||
painter = QPainter(self)
|
||||
paint_glyph(painter, self.kind, QRectF(0, 0, self.width(), self.height()), self.colour)
|
||||
painter.end()
|
||||
|
||||
|
||||
class LogoTile(QWidget):
|
||||
"""A rounded tile with a glyph on it: the window mark and the two model marks."""
|
||||
|
||||
def __init__(self, kind: str, *, start: str, end: str, glyph: str = "#FFFFFF",
|
||||
size: int = 34, radius: float = 10.0, circle: bool = False,
|
||||
parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.kind, self.start, self.end, self.glyph_colour = kind, start, end, glyph
|
||||
self.radius, self.circle = radius, circle
|
||||
self.setFixedSize(size, size)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
|
||||
|
||||
def paintEvent(self, event: QPaintEvent) -> None: # noqa: N802 - Qt virtual
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
||||
box = QRectF(0, 0, self.width(), self.height())
|
||||
gradient = QLinearGradient(box.topLeft(), box.bottomRight())
|
||||
gradient.setColorAt(0.0, QColor(self.start))
|
||||
gradient.setColorAt(1.0, QColor(self.end))
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
painter.setBrush(QBrush(gradient))
|
||||
if self.circle:
|
||||
painter.drawEllipse(box)
|
||||
else:
|
||||
painter.drawRoundedRect(box, self.radius, self.radius)
|
||||
paint_glyph(painter, self.kind, box, self.glyph_colour)
|
||||
painter.end()
|
||||
|
||||
def minimumSizeHint(self) -> QSize:
|
||||
return QSize(self.width(), self.height())
|
||||
|
||||
|
||||
def window_mark(parent: QWidget | None = None, size: int = 38) -> LogoTile:
|
||||
tile = LogoTile("check", start=TECH_BLUE["accent"], end=TECH_BLUE["accent_pressed"],
|
||||
size=size, radius=11, parent=parent)
|
||||
tile.setAccessibleName("诊断与药方对照")
|
||||
return tile
|
||||
|
||||
|
||||
def model_mark(model_key: str, parent: QWidget | None = None, size: int = 30) -> LogoTile:
|
||||
if model_key == "openai":
|
||||
return LogoTile("knot", start=TECH_BLUE["openai_dim"], end=TECH_BLUE["surface_2"],
|
||||
glyph=TECH_BLUE["openai_text"], size=size, circle=True, parent=parent)
|
||||
return LogoTile("spark", start=TECH_BLUE["qwen_dim"], end=TECH_BLUE["surface_2"],
|
||||
glyph=TECH_BLUE["qwen_text"], size=size, radius=9, parent=parent)
|
||||
|
||||
|
||||
def glyph_icon(kind: str, colour: str, size: int = 16) -> QIcon:
|
||||
"""The same line art as a QIcon, for buttons that place their own icon and label."""
|
||||
|
||||
pixmap = QPixmap(size, size)
|
||||
pixmap.fill(Qt.GlobalColor.transparent)
|
||||
painter = QPainter(pixmap)
|
||||
paint_glyph(painter, kind, QRectF(0, 0, size, size), colour)
|
||||
painter.end()
|
||||
return QIcon(pixmap)
|
||||
@@ -0,0 +1,343 @@
|
||||
"""Presentation-only Chinese labels for saved prescription analysis payloads.
|
||||
|
||||
Keep source identifiers, enum keys and statistics untouched in the repository data.
|
||||
Only explicitly structured metadata is localized; clinical prose is not translated.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
SOURCE_LABELS = {
|
||||
"diagnoses": "诊单", "doctor_notes": "医生笔记", "tracking_notes": "随访记录",
|
||||
"prescriptions": "历史处方", "call_records": "问诊通话", "video_calls": "视频问诊",
|
||||
"transcript_segments": "转写片段", "chat_records": "聊天记录", "daily_records": "日常记录",
|
||||
"blood_records": "血糖血压记录", "blood_glucose_pressure": "血糖血压记录",
|
||||
"diet_records": "饮食记录", "diet": "饮食记录", "exercise_records": "运动记录", "exercise": "运动记录",
|
||||
"im_messages": "即时聊天记录", "tencent_im": "即时聊天记录",
|
||||
"wechat_messages": "企业微信聊天记录", "wechat_work": "企业微信聊天记录",
|
||||
"target_plan": "本次处方方案", "clinical": "临床资料", "file": "附件",
|
||||
"tongue": "舌象", "tongue_image": "舌象图片", "tongue_images": "舌象图片",
|
||||
"clinical_attachment": "临床附件", "patient": "患者资料",
|
||||
}
|
||||
|
||||
SYSTEM_LABELS = {
|
||||
"SOURCE_HISTORY_VERSIONS_UNAVAILABLE": "来源历史版本无法核验",
|
||||
"ARCHIVE_SYNC_WATERMARK_UNAVAILABLE": "归档同步完整性尚未核验",
|
||||
"TRANSCRIPT_NOT_VERIFIED_COMPLETE": "问诊转写完整性尚未核验",
|
||||
"TRANSCRIPT_NOT_FINAL": "问诊转写尚未完整归档",
|
||||
"TRANSCRIPT_PARTIAL": "问诊转写仅部分完成", "TRANSCRIPT_FAILED": "问诊转写失败",
|
||||
"TRANSCRIPT_RUNNING": "问诊转写进行中", "TRANSCRIPT_PENDING": "等待问诊转写",
|
||||
"SOURCE_AUTHORIZATION_LINK_UNAVAILABLE": "来源缺少可核验的授权关联",
|
||||
"SOURCE_PATIENT_CONFLICT": "来源的患者关联存在冲突",
|
||||
"SOURCE_ACCESS_RESTRICTED": "来源访问受限",
|
||||
"UNLINKED_SOURCE_REQUIRES_AUTHORIZATION": "未关联来源需核验访问权限",
|
||||
"SOURCE_READ_UNAVAILABLE": "来源暂时无法读取",
|
||||
"ATTACHMENT_TARGET_PLAN_LEAKAGE_UNVERIFIED": "附件可能包含本次处方,独立性未核验",
|
||||
"UNSTRUCTURED_TARGET_PLAN_LEAKAGE_UNVERIFIED": "非结构化资料可能包含本次处方,独立性未核验",
|
||||
"TARGET_PLAN_COPY_ISOLATED": "已隔离资料中复制的本次处方内容",
|
||||
"CRITICAL_CLINICAL_FACT_MISSING": "关键临床资料缺失",
|
||||
"FILE_STORAGE_AUTHORIZATION_UNVERIFIED": "附件存储访问权限尚未核验",
|
||||
"FILE_CONTENT_VERSION_UNVERIFIED": "附件内容版本尚未核验",
|
||||
"PATIENT_BINDING_REQUIRED": "需完善患者与诊单关联",
|
||||
"PATIENT_BINDING_OR_PERMISSION_REQUIRED": "需核验患者关联与资料访问权限",
|
||||
"ACCESS_REVOKED": "资料访问权限已变更", "SOURCE_CHANGED": "资料或处方已更新,请查看新版本",
|
||||
"BUDGET_PAUSED": "已达到分析预算,等待额度恢复",
|
||||
"CONFIG_INVALID": "模型配置无效,请联系管理员", "CONFIG_DISABLED": "模型分析尚未启用",
|
||||
"UPSTREAM_AUTH_FAILED": "模型服务认证失败,请联系管理员",
|
||||
"UPSTREAM_TIMEOUT": "模型响应超时,可稍后重试", "UPSTREAM_BUSY": "模型服务繁忙,可稍后重试",
|
||||
"UPSTREAM_UNAVAILABLE": "模型服务暂不可用", "UPSTREAM_REJECTED": "模型服务未接受本次请求",
|
||||
"UPSTREAM_FAILED": "模型服务处理失败", "EMPTY_RESPONSE": "模型未返回内容",
|
||||
"INCOMPLETE_RESPONSE": "模型返回内容不完整", "INVALID_RESPONSE": "模型返回内容未通过校验",
|
||||
"RESPONSE_INVALID": "模型返回内容未通过校验", "INVALID_REPORT_OUTPUT": "模型报告未通过格式校验",
|
||||
"INVALID_EVIDENCE_OUTPUT": "模型证据来源未通过校验",
|
||||
"INVALID_FILE_EVIDENCE_OUTPUT": "模型附件证据未通过校验",
|
||||
"CONTEXT_TOO_LARGE": "资料超过本次处理预算",
|
||||
"INPUT_TOKEN_BUDGET_EXCEEDED": "输入资料超过本次处理预算",
|
||||
"SYNTHESIS_BUDGET_EXCEEDED": "资料汇总超过本次处理预算",
|
||||
"FINAL_CONTEXT_EXCEEDS_BUDGET": "来源与缺口说明超过汇总预算,请联系管理员",
|
||||
"TOTAL_CALL_BUDGET_EXCEEDED": "模型调用次数达到本次上限",
|
||||
"SOURCE_UNIT_EXCEEDS_BUDGET": "单条来源资料超过处理预算",
|
||||
"RESPONSE_SIZE_EXCEEDED": "模型返回内容超过长度上限",
|
||||
"LEASE_EXPIRED": "工作进程中断,任务等待恢复",
|
||||
"SOURCE_PREPARATION_FAILED": "来源资料准备失败", "INTERNAL_ERROR": "分析处理异常,可稍后重试",
|
||||
"GENERATION_FAILED": "报告生成失败,可稍后重试", "CHECKPOINT_REJECTED": "分析进度保存未通过校验",
|
||||
"INVALID_PROFILE": "模型配置未通过校验", "INVALID_FROZEN_CONTEXT": "冻结资料未通过校验",
|
||||
"INVALID_FILE_MANIFEST": "附件清单未通过校验", "SOURCE_GAP": "来源资料存在缺口",
|
||||
"FILE_CAPABILITY_DISABLED": "模型附件处理能力尚未启用",
|
||||
"FILE_UNAVAILABLE_OR_UNSUPPORTED": "附件不可用或格式不受支持",
|
||||
"FILE_TYPE_UNSUPPORTED": "附件格式不受支持",
|
||||
"STRICT_FILES_INVALID_OR_LIMIT": "附件校验未通过或超过处理上限",
|
||||
"FILE_DELIVERY_UNVERIFIED": "附件送达情况尚未核验",
|
||||
"MODEL_REPORTED_UNREADABLE": "模型无法读取附件", "MODEL_REPORTED_UNSUPPORTED": "模型不支持此附件",
|
||||
"MODEL_FILE_OUTPUT_INVALID": "模型未能正确解析这组附件",
|
||||
"AI_ANALYSIS_CIPHER_INVALID": "报告加密数据无法读取",
|
||||
"AI_ANALYSIS_ENCRYPTION_FAILED": "报告加密保存失败",
|
||||
"AI_ANALYSIS_KEY_INVALID": "报告加密配置无效", "AI_ANALYSIS_KEY_UNAVAILABLE": "报告加密配置不可用",
|
||||
"formulation_mismatch": "剂型不同,未提供经确认的换算规则",
|
||||
"dose_basis_mismatch": "剂量基准不同", "unit_mismatch": "剂量单位不同",
|
||||
"unknown_formulation": "剂型缺失或不受支持", "empty_prescription": "处方为空或药味结构无效",
|
||||
"catalog_unavailable": "缺少可用的药材字典", "invalid_herb": "药味结构无效",
|
||||
"ambiguous_herb_name": "药名存在多种字典匹配", "unknown_herb_name": "药名未匹配药材字典",
|
||||
"doctor_identity_mismatch": "医生药材编号与规范药名不一致",
|
||||
"processing_conflict": "炮制信息与药材字典冲突或无效",
|
||||
"ambiguous_herb_role": "主辅方、给药途径或分组不明确",
|
||||
"missing_or_unknown_unit": "剂量单位缺失或不受支持",
|
||||
"missing_or_unknown_dose_basis": "每剂或每日剂量基准不明确",
|
||||
"invalid_dosage": "剂量数值无效", "invalid_herb_usage": "药味煎服说明格式无效",
|
||||
"duplicate_semantics_conflict": "重复药项的单位、剂量基准或煎服说明不一致",
|
||||
"no_medication": "建议暂不使用药物", "no_medication_recommended": "建议暂不使用药物",
|
||||
"baseline_ineligible": "不符合独立基线统计条件", "incomplete_coverage": "资料覆盖不完整",
|
||||
"missing_result": "缺少模型结果", "invalid_score": "一致度分值无效",
|
||||
"missing_algorithm_version": "缺少算法版本", "transcript_not_final": "问诊转写尚未完整归档",
|
||||
"event_patient_conflict": "开方事件的患者关联冲突", "duplicate_baseline_conflict": "重复基线结果存在冲突",
|
||||
"review_conflict": "复核记录存在冲突", "review_not_completed": "复核尚未完成",
|
||||
"review_not_independent": "复核不具独立性", "review_disputed": "复核存在争议",
|
||||
"review_not_evaluable": "复核不可评价", "invalid_review_outcome": "复核结论无效",
|
||||
"unknown_review_sampling": "复核抽样方式未确认", "start_at_required": "需设置分析起始时间",
|
||||
"processed": "已处理", "restricted": "访问受限", "error": "处理失败", "timeout": "处理超时",
|
||||
"delivered": "已送达", "unreadable": "不可读", "unsupported": "不支持", "parsed": "已解析",
|
||||
"ok": "可比条件已通过", "doctor": "医生方", "candidate": "候选方", "both": "双方",
|
||||
"low": "低风险", "medium": "中风险", "high": "高风险", "none": "无",
|
||||
"main": "主方", "oral": "口服", "external": "外用",
|
||||
"raw": "生品", "image": "图片", "document": "文档", "remote_url": "远程附件",
|
||||
"stratified_versions": "按版本分层统计", "single_version": "单一版本", "no_valid_samples": "无有效样本",
|
||||
"no_samples": "未建立复核样本", "recorded": "已记录", "conflict": "存在冲突",
|
||||
"not_evaluable": "不可评价", "qualified": "合格", "needs_revision": "需修订", "unqualified": "不合格",
|
||||
"random": "随机抽样", "stratified": "分层抽样", "risk_directed": "按风险抽样",
|
||||
"stratified_sampling": "按抽样方式分层统计",
|
||||
"spelling_aliases_only_no_quantity_conversion": "仅规范单位写法,不进行剂量换算",
|
||||
"g": "克", "mg": "毫克", "kg": "千克", "ml": "毫升", "mL": "毫升", "l": "升", "L": "升",
|
||||
"qwen": "千问", "openai": "OpenAI",
|
||||
}
|
||||
|
||||
EXTRA_FIELD_LABELS = {
|
||||
"source": "来源", "code": "原因说明", "reason_code": "原因说明", "error_code": "错误原因",
|
||||
"error_message": "错误说明", "message": "说明", "detail": "详情", "details": "详细记录",
|
||||
"reason_message": "原因说明", "field": "字段", "value": "记录值", "side": "所属处方",
|
||||
"row": "药项序号", "rows": "逐味记录", "key": "药项标识", "id": "编号", "medicine_id": "药材编号",
|
||||
"prescription_id": "处方编号", "diagnosis_id": "诊单编号", "patient_id": "患者编号",
|
||||
"doctor_id": "医生编号", "doctor_name": "医生姓名", "model_key": "模型", "model_name": "模型名称",
|
||||
"configured_model_name": "配置的模型名称", "comparison_status": "可比状态", "coverage_status": "资料覆盖状态",
|
||||
"comparison_type": "比较类型", "validity": "报告有效性", "version_verified": "内容版本已核验",
|
||||
"source_record_count": "来源记录总数", "missing_count": "资料缺口数", "snapshot_complete": "资料快照完整",
|
||||
"may_be_truncated": "资料可能截断", "history_versioning": "历史版本核验", "archive_sync_verified": "归档同步已核验",
|
||||
"file_ids": "附件编号", "evidence_file_ids": "证据附件编号", "covered_source_ids": "已覆盖来源编号",
|
||||
"source_kind": "来源类型", "kind": "来源类型", "type": "类型", "purpose": "用途",
|
||||
"transfer_method": "附件传递方式", "content_hash": "内容指纹", "dictionary_hash": "药材字典指纹",
|
||||
"source_hash": "来源指纹", "url": "附件地址", "uri": "附件地址", "path": "附件路径",
|
||||
"age": "年龄", "gender": "性别", "gender_label": "性别说明", "allergy_history": "过敏史",
|
||||
"pregnancy_history": "妊娠与哺乳情况", "current_medications": "当前用药",
|
||||
"allergy_history_text": "过敏史正文", "allergy_history_desc": "过敏史说明",
|
||||
"pregnancy_history_text": "妊娠与哺乳正文", "pregnancy_history_desc": "妊娠与哺乳说明",
|
||||
"current_medicine": "当前用药", "current_medication": "当前用药",
|
||||
"prescription": "处方", "prescription_opinion": "处方意见", "prescription_advice": "处方建议",
|
||||
"treatment_principle": "治则", "doctor_advice": "医嘱", "prescription_date": "开方日期",
|
||||
"issues": "需核对问题", "formulation": "剂型", "items": "药项", "bases": "剂量基准",
|
||||
"merges": "重复药项合并", "defaults": "默认值记录", "identity_complete": "药项身份核验完整",
|
||||
"raw_herb_count": "原始药项数", "source_rows": "原始药项序号", "source_names": "原始药名",
|
||||
"original_dosages": "原始剂量", "usage": "煎服说明", "doctor": "医生方", "candidate": "候选方",
|
||||
"dictionary_versions": "药材字典版本", "unit_policy": "单位处理规则", "denominator": "一致度计算分母",
|
||||
"matched_contribution_sum": "共同药项贡献合计", "special_usage": "特殊用法",
|
||||
"decoction_instruction": "煎药说明", "usage_note": "服法备注", "before": "处理前", "after": "处理后",
|
||||
"dosage_amount": "每次用量", "dosage_unit": "每次用量单位", "dosage_bag_count": "每次袋数", "aux_usage": "辅助用法",
|
||||
"score": "药味剂量一致度", "herb_score": "纯药味重合度", "model": "模型",
|
||||
"schema_version": "资料格式版本", "decision_at": "处方决策时间", "created_at": "建立时间", "updated_at": "更新时间",
|
||||
"source_diagnosis_ids": "来源诊单编号", "redaction_manifest": "处方内容隔离清单",
|
||||
"total_count": "开方事件数", "total_events": "开方事件数", "patient_count": "患者数",
|
||||
"eligible_count": "有效比较数", "valid_count": "有效比较数", "excluded_count": "排除样本数",
|
||||
"coverage_rate": "覆盖率", "coverage_percent": "覆盖率", "paired_count": "双模型共同有效样本数",
|
||||
"excluded_reasons": "排除原因及数量", "exclusion_reasons": "排除原因及数量", "exclusion_reason": "排除原因",
|
||||
"aggregation_status": "统计汇总方式", "paired_strata": "双模型版本分层", "models": "模型统计",
|
||||
"review": "专家复核", "reviews": "专家复核", "evaluated_count": "可评价样本数", "evaluable_count": "可评价样本数",
|
||||
"qualified_count": "合格样本数", "qualified_rate": "合格率", "qualification_rate": "合格率",
|
||||
"reviewed_events": "已复核事件数", "unreviewed_events": "未复核事件数", "sampling_method": "抽样方式",
|
||||
"sampling_groups": "抽样分组", "sampling_coverage_percent": "抽样覆盖率", "outcomes": "复核结论分布",
|
||||
"outcome": "复核结论", "independent": "独立复核", "disputed": "存在争议",
|
||||
"confidence_interval": "置信区间", "confidence_interval_reason": "置信区间说明",
|
||||
"unknown_patient_events": "患者身份未确认事件数", "repeated_patient_events": "重复患者事件数",
|
||||
"invalid_row_count": "无效记录数", "metric": "统计指标",
|
||||
}
|
||||
|
||||
ENUM_FIELDS = {
|
||||
"status", "comparison_status", "coverage_status", "comparison_type", "validity", "dose_basis", "bases",
|
||||
"match_type", "match", "side", "level", "kind", "source_kind", "type", "purpose", "transfer_method",
|
||||
"sample_status", "aggregation_status", "history_versioning", "sampling_method", "outcome", "unit_policy",
|
||||
"model_key", "model", "unit", "formula_type",
|
||||
}
|
||||
REASON_FIELDS = {
|
||||
"reason", "reason_message", "reason_code", "error_code", "error_message", "code", "message",
|
||||
"missing", "missing_information", "baseline_exclusion_reasons", "excluded_reasons", "exclusion_reasons", "exclusion_reason",
|
||||
}
|
||||
SOURCE_FIELDS = {
|
||||
"source", "source_id", "source_ids", "file_id", "file_ids", "evidence_file_ids", "covered_source_ids",
|
||||
"evidence_references", "redaction_manifest",
|
||||
}
|
||||
|
||||
_MACHINE_KEY = re.compile(r"[A-Za-z][A-Za-z0-9]*(?:_[A-Za-z0-9]+)+\Z")
|
||||
_UPPER_CODE = re.compile(r"(?<![A-Za-z0-9_])[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+(?![A-Za-z0-9_])")
|
||||
_SOURCE_ID = re.compile(r"([a-z][a-z0-9_]*)[::]([^\s,,;;:<>]+)\Z")
|
||||
_KNOWN_SOURCE_IN_TEXT = re.compile(r"(?<![A-Za-z0-9_])(" + "|".join(SOURCE_LABELS) + r")[::]([A-Za-z0-9]+)(?![A-Za-z0-9_])")
|
||||
|
||||
|
||||
def plain_text(value: Any) -> str:
|
||||
if value is None or value == "":
|
||||
return "—"
|
||||
if isinstance(value, bool):
|
||||
return "是" if value else "否"
|
||||
return str(value)
|
||||
|
||||
|
||||
def source_text(value: Any, fields: Mapping[str, str]) -> str:
|
||||
"""Localize a source prefix, preserving its entire numeric or opaque ID."""
|
||||
text = plain_text(value)
|
||||
if text in SOURCE_LABELS:
|
||||
return SOURCE_LABELS[text]
|
||||
if text.startswith("clinical."):
|
||||
field = text.removeprefix("clinical.")
|
||||
return "临床资料 · " + fields.get(field, "待核对项目")
|
||||
match = _SOURCE_ID.fullmatch(text)
|
||||
if match:
|
||||
prefix, identifier = match.groups()
|
||||
# Redaction manifests append a field name after the numeric source ID.
|
||||
if ":" in identifier:
|
||||
identifier, field = identifier.split(":", 1)
|
||||
return f"{SOURCE_LABELS.get(prefix, '其他来源')}(编号:{identifier}) · {fields.get(field, '待核对字段')}"
|
||||
return f"{SOURCE_LABELS.get(prefix, '其他来源')}(编号:{identifier})"
|
||||
if _MACHINE_KEY.fullmatch(text) or re.fullmatch(r"[A-Za-z][A-Za-z0-9_-]*", text):
|
||||
return "来源类型待核对"
|
||||
return _KNOWN_SOURCE_IN_TEXT.sub(lambda match: source_text(f"{match[1]}:{match[2]}", fields), text)
|
||||
|
||||
|
||||
def system_text(value: Any, labels: Mapping[str, str], fields: Mapping[str, str], *, strict: bool = False) -> str:
|
||||
"""Translate metadata, including historical ``CODE: source:id`` gap strings."""
|
||||
text = plain_text(value)
|
||||
if "\n" in text:
|
||||
return "\n".join(system_text(line, labels, fields, strict=strict) for line in text.split("\n"))
|
||||
if text in labels:
|
||||
return labels[text]
|
||||
if text in SOURCE_LABELS:
|
||||
return SOURCE_LABELS[text]
|
||||
if text in fields:
|
||||
return fields[text]
|
||||
# Only a technical prefix permits parsing the rest as a source identifier.
|
||||
compound = re.fullmatch(r"([A-Za-z][A-Za-z0-9_]+)\s*[::]\s*(.*)", text, re.DOTALL)
|
||||
if compound and (compound[1] in labels or _MACHINE_KEY.fullmatch(compound[1])):
|
||||
prefix = labels.get(compound[1], "未识别的系统原因,请联系管理员核对")
|
||||
return prefix + ":" + source_text(compound[2], fields)
|
||||
if _MACHINE_KEY.fullmatch(text):
|
||||
return "未识别的系统标识,请联系管理员核对"
|
||||
# Historic reports can include a known source/code inside a Chinese gap explanation.
|
||||
text = _UPPER_CODE.sub(lambda match: labels.get(match[0], "未识别的系统原因,请联系管理员核对"), text)
|
||||
text = _KNOWN_SOURCE_IN_TEXT.sub(lambda match: source_text(f"{match[1]}:{match[2]}", fields), text)
|
||||
if strict and re.fullmatch(r"[A-Za-z][A-Za-z0-9 ._-]*", text):
|
||||
return "未识别的系统状态,请联系管理员核对"
|
||||
return text
|
||||
|
||||
|
||||
def field_text(key: Any, fields: Mapping[str, str], labels: Mapping[str, str]) -> str:
|
||||
text = str(key)
|
||||
if text in fields:
|
||||
return fields[text]
|
||||
if text.endswith("_count") and text.removesuffix("_count") in SOURCE_LABELS:
|
||||
return SOURCE_LABELS[text.removesuffix("_count")] + "数"
|
||||
translated = system_text(text, labels, fields)
|
||||
if translated != text:
|
||||
return translated
|
||||
if text in SOURCE_LABELS or text.startswith("clinical.") or _SOURCE_ID.fullmatch(text):
|
||||
return source_text(text, fields)
|
||||
return "其他字段(待核对)" if re.search(r"[A-Za-z]", text) and not re.search(r"[\u4e00-\u9fff]", text) else text
|
||||
|
||||
|
||||
def value_text(value: Any, field: str, labels: Mapping[str, str], fields: Mapping[str, str]) -> str:
|
||||
if field in SOURCE_FIELDS:
|
||||
return source_text(value, fields)
|
||||
if field == "field":
|
||||
return field_text(value, fields, labels)
|
||||
if field == "coverage_status":
|
||||
return {"partial": "资料不全", "pending": "资料待核对", "unavailable": "暂无覆盖信息"}.get(str(value)) or system_text(value, labels, fields, strict=True)
|
||||
if field == "history_versioning" and value == "unavailable":
|
||||
return "历史版本无法核验"
|
||||
if field == "formula_type" and value == "auxiliary":
|
||||
return "辅方"
|
||||
if field.endswith("version") or field.endswith("versions"):
|
||||
text = plain_text(value)
|
||||
for prefix, name in (
|
||||
("manual-prescription-independent-v", "手动处方独立分析"),
|
||||
("manual-prescription-available-evidence-v", "手动处方已读资料分析"),
|
||||
("prescription-soft-dice-v", "处方药味剂量一致度算法"),
|
||||
("prescription-evidence-v", "处方证据资料格式"),
|
||||
("prescription-source-access-v", "处方来源权限格式"),
|
||||
):
|
||||
if text.startswith(prefix) and re.fullmatch(r"\d+(?:\.\d+)*", text.removeprefix(prefix)):
|
||||
return f"{name} · 第 {text.removeprefix(prefix)} 版"
|
||||
return text
|
||||
if field in ENUM_FIELDS:
|
||||
return system_text(value, labels, fields, strict=True)
|
||||
if field in REASON_FIELDS:
|
||||
return system_text(value, labels, fields, strict=field in {"code", "error_code", "reason_code"})
|
||||
if field.endswith(("_status", "_state", "_code")):
|
||||
return system_text(value, labels, fields, strict=True)
|
||||
if field and field not in fields:
|
||||
return system_text(value, labels, fields)
|
||||
return plain_text(value)
|
||||
|
||||
|
||||
STATE_LABELS = {
|
||||
"blank": "尚未开方", "not_generated": "尚无分析记录", "unavailable": "暂无可比结果",
|
||||
"not_applicable": "尚未开方", "not_started": "尚未分析", "pending": "待分析",
|
||||
"preparing": "准备资料", "waiting_sources": "等待转写/资料", "retry_wait": "等待重试", "blocked": "需完善资料关联",
|
||||
"queued": "待分析", "waiting": "等待资料", "waiting_transcript": "等待转写",
|
||||
"waiting_transcription": "等待转写", "running": "分析中", "processing": "分析中",
|
||||
"retrying": "重试中", "succeeded": "已完成", "completed": "已完成", "success": "已完成",
|
||||
"partial": "部分完成", "failed": "需重试", "cancelled": "已取消", "canceled": "已取消",
|
||||
"stale": "处方已变更", "superseded": "处方已变更", "invalid": "已失效",
|
||||
"prescription_changed": "处方已变更", "source_updated": "资料已更新", "voided": "处方已作废", "deleted": "处方已删除", "revoked": "权限已撤销",
|
||||
"current": "当前版本", "valid": "当前有效", "complete": "资料清单完整",
|
||||
"incomplete": "资料不全", "missing": "资料缺失", "unknown": "未确认",
|
||||
"needs_patient_link": "需完善患者关联", "patient_unlinked": "需完善患者关联",
|
||||
"independent_baseline": "独立基线", "baseline": "独立基线",
|
||||
"latest_context": "最新资料对照", "supplemental": "最新资料对照",
|
||||
"assisted_revision": "AI 辅助后修订", "ai_assisted": "AI 辅助后修订",
|
||||
"non_independent": "非独立对照", "auxiliary": "辅助复核",
|
||||
"comparable": "可比", "not_comparable": "不可比",
|
||||
"available_for_review": "供医生复核", "insufficient_data": "资料不足,暂不提供候选用药",
|
||||
"withheld_for_risk": "因风险暂缓候选用药", "viewed": "已查看", "needs_information": "需补充资料",
|
||||
"not_adopted": "不采纳", "reviewed": "已复核",
|
||||
"per_dose": "每剂", "per_day": "每日", "matched": "共同药味", "doctor_only": "仅医生方", "candidate_only": "仅模型方",
|
||||
"insufficient_sample": "样本不足", "descriptive_only": "仅作描述性统计",
|
||||
}
|
||||
FIELD_LABELS = {
|
||||
"summary": "概要", "timeline": "病程", "analysis": "综合分析", "tcm_analysis": "中医辨证",
|
||||
"diagnosis": "辨证分析", "treatment_advice": "治疗与随访建议", "risk_assessment": "需复核风险",
|
||||
"evidence_references": "证据来源编号", "missing_information": "待补充资料", "level": "风险等级", "label": "说明",
|
||||
"risk_warnings": "需复核风险", "risks": "风险", "follow_up": "随访建议", "evidence": "依据",
|
||||
"sources": "来源", "manifest": "来源清单", "missing": "资料缺口", "status": "状态",
|
||||
"reason": "原因", "name": "药名", "herb_name": "规范药名", "canonical_name": "规范药名",
|
||||
"processing": "炮制", "dosage": "剂量", "dose": "剂量", "unit": "单位", "dose_basis": "剂量基准",
|
||||
"formula_type": "主辅方", "doctor_dosage": "医生剂量", "candidate_dosage": "模型剂量",
|
||||
"doctor_dose": "医生剂量", "candidate_dose": "模型剂量", "ai_dose": "模型剂量",
|
||||
"contribution": "匹配贡献", "ratio": "匹配贡献", "match_ratio": "匹配贡献", "match": "匹配情况",
|
||||
"prescription_name": "候选方名称", "prescription_type": "剂型", "herbs": "药味",
|
||||
"dose_count": "剂数", "usage_days": "疗程(天)", "times_per_day": "每日服次",
|
||||
"usage_instruction": "服法", "usage_time": "服药时间", "usage_way": "给药途径",
|
||||
"rationale": "方义与依据", "usage_differences": "用法、疗程与风险差异", "normalization": "规范化记录",
|
||||
"algorithm_version": "算法版本", "dictionary_version": "药材字典版本", "model_version": "模型版本",
|
||||
"prompt_version": "提示词版本", "doctor_count": "医生药项数", "candidate_count": "候选药项数",
|
||||
"matched_count": "共同药项数", "coverage": "模型资料覆盖", "source_summary": "来源汇总",
|
||||
"cutoff_at": "资料截止时间", "generated_at": "报告生成时间", "comment": "复核意见",
|
||||
"match_type": "增减药项", "administration_route": "给药途径", "group": "用药组",
|
||||
"delivered": "已送达", "unreadable": "不可读", "unsupported": "不支持", "parsed": "已解析",
|
||||
"diagnosis_count": "病历数", "prescription_count": "历史处方数", "chat_count": "聊天记录数",
|
||||
"daily_record_count": "日常记录数", "transcript_count": "转写数", "attachment_count": "附件数",
|
||||
"files": "附件处理清单", "source_ids": "已读取来源编号", "source_id": "来源编号", "file_id": "附件编号",
|
||||
"complete": "资料清单完整", "source_complete": "文字来源齐全", "transmitted": "附件已送达", "critical": "关键资料缺口",
|
||||
"baseline_eligible": "独立基线统计资格", "baseline_exclusion_reasons": "基线排除原因", "instructions": "特殊煎服说明",
|
||||
"versions": "版本信息", "strata": "按版本分层", "count": "样本数", "mean": "均值", "median": "中位数",
|
||||
"distribution": "一致度分布", "sample_status": "样本说明",
|
||||
}
|
||||
STATE_LABELS.update(SYSTEM_LABELS)
|
||||
FIELD_LABELS.update(EXTRA_FIELD_LABELS)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,135 @@
|
||||
"""Honest presentation of server checkpoints, without fabricated totals or ETA."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
ACTIVE_STATES = {"pending", "preparing", "queued", "waiting", "waiting_sources", "waiting_transcript", "waiting_transcription", "running", "processing", "retrying", "retry_wait"}
|
||||
SUCCESS_STATES = {"succeeded", "completed", "success"}
|
||||
TERMINAL_STATES = SUCCESS_STATES | {"partial", "failed", "cancelled", "canceled", "blocked", "stale", "superseded", "invalid", "revoked", "deleted", "voided"}
|
||||
STAGES = {
|
||||
"preparing": "准备资料", "waiting_sources": "等待转写与资料", "queued": "排队等待处理",
|
||||
"text": "分析文字资料", "files": "分析附件", "reduce": "汇总资料要点",
|
||||
"final": "生成完整报告", "validating": "校验报告", "comparing": "计算用药对照",
|
||||
"completed": "已完成", "retry_wait": "等待重试", "failed": "处理失败", "cancelled": "已取消",
|
||||
"unknown": "等待阶段详情",
|
||||
}
|
||||
COMPACT_STAGES = {"preparing": "准备资料", "waiting_sources": "等待资料", "queued": "排队中", "text": "文字",
|
||||
"files": "附件", "reduce": "汇总", "final": "生成报告", "validating": "校验", "comparing": "用药对照",
|
||||
"completed": "已完成", "retry_wait": "待重试", "failed": "失败", "cancelled": "已取消", "unknown": "处理中"}
|
||||
STATUS_STAGE = {"pending": "queued", "waiting": "waiting_sources", "waiting_transcript": "waiting_sources",
|
||||
"waiting_transcription": "waiting_sources", "retrying": "retry_wait",
|
||||
**{state: "completed" for state in SUCCESS_STATES}, "canceled": "cancelled", "partial": "completed", "blocked": "failed"}
|
||||
|
||||
|
||||
def _mapping(value: Any) -> dict[str, Any]:
|
||||
return dict(value) if isinstance(value, Mapping) else {}
|
||||
|
||||
|
||||
def _integer(value: Any) -> int | None:
|
||||
# Counts/timestamps are integer contract fields, not arbitrary numeric text.
|
||||
return value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else None
|
||||
|
||||
|
||||
def duration(seconds: int) -> str:
|
||||
seconds = max(0, seconds)
|
||||
if seconds < 60:
|
||||
return f"{seconds} 秒"
|
||||
if seconds < 3600:
|
||||
return f"{seconds // 60} 分 {seconds % 60:02d} 秒"
|
||||
return f"{seconds // 3600} 小时 {seconds % 3600 // 60:02d} 分"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProgressView:
|
||||
stage: str
|
||||
headline: str
|
||||
detail: str
|
||||
completed: int | None = None
|
||||
total: int | None = None
|
||||
busy: bool = False
|
||||
|
||||
|
||||
def progress_view(owner: Any, *, fallback_status: str = "", seconds: int = 0, live: bool = True) -> ProgressView:
|
||||
data = _mapping(owner)
|
||||
progress = _mapping(data.get("progress"))
|
||||
status = str(data.get("status") or fallback_status)
|
||||
stage = str(progress.get("stage") or STATUS_STAGE.get(status, status))
|
||||
if stage not in STAGES:
|
||||
stage = "unknown"
|
||||
terminal = status in TERMINAL_STATES
|
||||
if terminal:
|
||||
stage = STATUS_STAGE.get(status, status)
|
||||
if stage not in STAGES:
|
||||
stage = "unknown"
|
||||
active = status in ACTIVE_STATES and not terminal and stage not in {"completed", "failed", "cancelled"}
|
||||
advance = max(0, seconds) if active else 0
|
||||
headline = STAGES[stage]
|
||||
if status == "partial":
|
||||
headline = "部分模型已完成"
|
||||
if stage == "unknown":
|
||||
headline = "正在处理,等待阶段详情" if active else "暂无处理进度"
|
||||
completed = _integer(progress.get("completed_units"))
|
||||
total = _integer(progress.get("total_units"))
|
||||
if stage not in {"text", "files", "reduce"} or total is None or not 0 < total <= 1_000_000 or completed is None or completed > total:
|
||||
completed = total = None
|
||||
if total is not None:
|
||||
headline += f" · 本阶段 {completed}/{total} 组"
|
||||
|
||||
details = []
|
||||
attempt = _integer(progress.get("attempt"))
|
||||
if attempt is not None and 0 < attempt <= 1_000_000:
|
||||
details.append(f"第 {attempt} 次尝试")
|
||||
elapsed = _integer(progress.get("elapsed_seconds"))
|
||||
stage_elapsed = _integer(progress.get("stage_elapsed_seconds"))
|
||||
if elapsed is not None:
|
||||
# Retry metadata freezes the previous attempt's duration. Only its
|
||||
# scheduling countdown and update age continue between server polls.
|
||||
details.append("已用时 " + duration(elapsed + (0 if stage == "retry_wait" else advance)))
|
||||
if stage_elapsed is not None and active and stage != "retry_wait":
|
||||
details.append("本阶段 " + duration(stage_elapsed + advance))
|
||||
remaining = _integer(progress.get("wait_remaining_seconds"))
|
||||
if stage in {"preparing", "waiting_sources", "retry_wait"} and remaining is not None and active:
|
||||
remaining = max(0, remaining - advance)
|
||||
details.append("资料等待窗口剩余 " + duration(remaining) if remaining else "等待窗口已到,等待服务端确认")
|
||||
if stage == "retry_wait":
|
||||
details[-1] = "距下次重试 " + duration(remaining) if remaining else "重试时间已到,等待服务端确认"
|
||||
server_time = _integer(progress.get("server_time"))
|
||||
updated_at = _integer(progress.get("updated_at"))
|
||||
if active and server_time is not None and updated_at is not None and updated_at > 0:
|
||||
details.append("阶段更新于 " + duration(max(0, server_time - updated_at) + advance) + "前")
|
||||
if not progress and active:
|
||||
details.append("服务端暂未提供分阶段进度")
|
||||
notice = progress.get("notice")
|
||||
terminal_notice = stage in {"failed", "cancelled"} and progress.get("stage") == stage and progress.get("phase") == "failed"
|
||||
if isinstance(notice, str) and notice.strip() and (not terminal or terminal_notice):
|
||||
details.append(notice.strip())
|
||||
elif stage in {"final", "text", "files", "reduce"} and active:
|
||||
details.append("等待模型响应;耗时取决于资料量与模型服务")
|
||||
return ProgressView(stage, headline, " · ".join(details), completed, total, live and active)
|
||||
|
||||
|
||||
def flow_text(batch: Any) -> str:
|
||||
data = _mapping(batch)
|
||||
status = data.get("status")
|
||||
if not data:
|
||||
return "准备资料 → 双模型分析 → 用药对照 → 完成"
|
||||
if data.get("validity") not in (None, "", "current", "valid"):
|
||||
return "此批次已失效 · 以下为最后保存的处理记录"
|
||||
models = [_mapping(_mapping(data.get("models")).get(key)) for key in ("qwen", "openai")]
|
||||
complete = sum(model.get("status") in SUCCESS_STATES for model in models)
|
||||
failed = sum(model.get("status") == "failed" for model in models)
|
||||
stages = [progress_view(model).stage for model in models]
|
||||
batch_stage = progress_view(data).stage
|
||||
if status in {"failed", "cancelled", "canceled", "blocked"}:
|
||||
return f"处理已停止 · {complete}/2 个模型完成" + (f" · {failed} 个模型失败" if failed else "")
|
||||
if batch_stage in {"preparing", "waiting_sources"}:
|
||||
return "准备资料:进行中 → 双模型分析:待开始 → 用药对照:待开始 → 完成:待处理"
|
||||
prepared = "已完成" if any(models) else "待确认"
|
||||
comparisons = sum(model.get("status") in SUCCESS_STATES and bool(model.get("comparison")) for model in models)
|
||||
compare_state = f"{comparisons}/2 已处理" if comparisons else "进行中" if "comparing" in stages else "待处理"
|
||||
analysis = f"{complete}/2 完成" + (f",{failed} 个失败" if failed else ",进行中" if status not in TERMINAL_STATES else "")
|
||||
end = "已完成" if complete == 2 else "部分完成" if status == "partial" else "待处理"
|
||||
return f"准备资料:{prepared} → 双模型分析:{analysis} → 用药对照:{compare_state} → 完成:{end}"
|
||||
@@ -0,0 +1,210 @@
|
||||
"""The analysis console's palette, and the switch between its dark and light themes.
|
||||
|
||||
The prescription analysis window has its own theme, separate from the workstation's light chrome:
|
||||
tech blue for the interface itself, and two model hues (cyan for 千问, violet for OpenAI) that stay
|
||||
apart from the interface colour and from each other. Severity keeps amber and rose, so a reading
|
||||
never depends on hue alone.
|
||||
|
||||
``CONSOLE`` is the active palette and is mutated in place by :func:`use_theme`, so every module
|
||||
that imported it sees the new values. Anything derived from it — a stylesheet string, a colour
|
||||
constant — must be rebuilt in a callback registered through :func:`on_theme_changed`.
|
||||
|
||||
Key names mirror ``reception_style.TECH_BLUE`` so a widget can take either palette.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from PySide6.QtGui import QFont
|
||||
|
||||
DARK: dict[str, str] = {
|
||||
# ground and cards
|
||||
"canvas": "#080D18",
|
||||
"canvas_soft": "#0C1322",
|
||||
"surface": "#0F1726",
|
||||
"surface_2": "#131D2E",
|
||||
"raised": "#1A2637",
|
||||
"line": "#22304A",
|
||||
"line_soft": "#18233A",
|
||||
# type
|
||||
"heading": "#E8EFFB",
|
||||
"text": "#B6C5DA",
|
||||
"muted": "#8195AF",
|
||||
"faint": "#6A7F99",
|
||||
"selected_text": "#E8EFFB",
|
||||
# interface colour
|
||||
"accent": "#2E7BF6",
|
||||
"accent_text": "#6BA5FF",
|
||||
"accent_pressed": "#1A5FD0",
|
||||
"selection": "#142645",
|
||||
"selection_line": "#1B3E77",
|
||||
# model hues
|
||||
"qwen": "#17BFDD",
|
||||
"qwen_text": "#55D8EE",
|
||||
"qwen_dim": "#10303C",
|
||||
"openai": "#9B7BFF",
|
||||
"openai_text": "#B69FFF",
|
||||
"openai_dim": "#241F48",
|
||||
# severity
|
||||
"amber": "#F5B942",
|
||||
"amber_text": "#F8C661",
|
||||
"amber_dim": "#2C2415",
|
||||
"rose": "#F4697A",
|
||||
"rose_text": "#F88694",
|
||||
"rose_dim": "#2C1620",
|
||||
"ok": "#2E7BF6",
|
||||
"zebra": "#0E1626",
|
||||
"grid_line": "#191F2C",
|
||||
}
|
||||
|
||||
LIGHT: dict[str, str] = {
|
||||
"canvas": "#F1F5FC",
|
||||
"canvas_soft": "#E7EEFA",
|
||||
"surface": "#FFFFFF",
|
||||
"surface_2": "#F5F8FE",
|
||||
"raised": "#E8EFFA",
|
||||
"line": "#D5E1F2",
|
||||
"line_soft": "#E3EBF8",
|
||||
"heading": "#0A182E",
|
||||
"text": "#2A3C56",
|
||||
"muted": "#54697F",
|
||||
"faint": "#7A8DA3",
|
||||
"selected_text": "#0A182E",
|
||||
"accent": "#1A5FD0",
|
||||
"accent_text": "#124DAE",
|
||||
"accent_pressed": "#0E3F90",
|
||||
"selection": "#E8EFFA",
|
||||
"selection_line": "#AEC7EE",
|
||||
"qwen": "#0C89AC",
|
||||
"qwen_text": "#086C88",
|
||||
"qwen_dim": "#E2F5FA",
|
||||
"openai": "#6B4AE0",
|
||||
"openai_text": "#5537C6",
|
||||
"openai_dim": "#EDE8FE",
|
||||
"amber": "#B4791A",
|
||||
"amber_text": "#8F5F0B",
|
||||
"amber_dim": "#FCF2DF",
|
||||
"rose": "#D0435A",
|
||||
"rose_text": "#AE3149",
|
||||
"rose_dim": "#FCE9EC",
|
||||
"ok": "#1A5FD0",
|
||||
"zebra": "#FAFBFE",
|
||||
"grid_line": "#DDE4EE",
|
||||
}
|
||||
|
||||
CONSOLE: dict[str, str] = dict(DARK)
|
||||
_THEME = "dark"
|
||||
_LISTENERS: list[Callable[[], None]] = []
|
||||
|
||||
|
||||
def current_theme() -> str:
|
||||
return _THEME
|
||||
|
||||
|
||||
def on_theme_changed(callback: Callable[[], None]) -> Callable[[], None]:
|
||||
"""Register a rebuild for anything derived from the palette; returns the callback."""
|
||||
|
||||
_LISTENERS.append(callback)
|
||||
return callback
|
||||
|
||||
|
||||
def use_theme(name: str) -> str:
|
||||
"""Switch the active palette in place and let every derived value rebuild itself."""
|
||||
|
||||
global _THEME
|
||||
palette = LIGHT if name == "light" else DARK
|
||||
_THEME = "light" if name == "light" else "dark"
|
||||
CONSOLE.clear()
|
||||
CONSOLE.update(palette)
|
||||
_rebuild()
|
||||
return _THEME
|
||||
|
||||
|
||||
def toggle_theme() -> str:
|
||||
return use_theme("light" if _THEME == "dark" else "dark")
|
||||
|
||||
|
||||
def _rebuild() -> None:
|
||||
for callback in list(_LISTENERS):
|
||||
callback()
|
||||
|
||||
|
||||
MODEL_HUE = {"qwen": CONSOLE["qwen"], "openai": CONSOLE["openai"]}
|
||||
MODEL_TEXT = {"qwen": CONSOLE["qwen_text"], "openai": CONSOLE["openai_text"]}
|
||||
MODEL_DIM = {"qwen": CONSOLE["qwen_dim"], "openai": CONSOLE["openai_dim"]}
|
||||
|
||||
|
||||
@on_theme_changed
|
||||
def _rebuild_model_hues() -> None:
|
||||
MODEL_HUE.update({"qwen": CONSOLE["qwen"], "openai": CONSOLE["openai"]})
|
||||
MODEL_TEXT.update({"qwen": CONSOLE["qwen_text"], "openai": CONSOLE["openai_text"]})
|
||||
MODEL_DIM.update({"qwen": CONSOLE["qwen_dim"], "openai": CONSOLE["openai_dim"]})
|
||||
|
||||
|
||||
RADIUS = 10
|
||||
RADIUS_SM = 7
|
||||
|
||||
# The design sets numbers in a condensed face so columns of figures line up; the fallbacks keep
|
||||
# the same tabular behaviour when Bahnschrift is missing.
|
||||
NUM_FAMILIES = ("Bahnschrift", "Segoe UI", "Microsoft YaHei UI", "Microsoft YaHei")
|
||||
|
||||
|
||||
def num_font(size: int, *, weight: QFont.Weight | None = None) -> QFont:
|
||||
"""A tabular face for figures, at the pixel size the design states."""
|
||||
|
||||
font = QFont()
|
||||
font.setFamilies(list(NUM_FAMILIES))
|
||||
font.setPixelSize(size)
|
||||
if weight is not None:
|
||||
font.setWeight(weight)
|
||||
font.setStyleStrategy(QFont.StyleStrategy.PreferAntialias)
|
||||
return font
|
||||
|
||||
|
||||
# Windows draws the title bar itself, so the console's dark ground stops at the frame unless the
|
||||
# window asks DWM for a matching caption. Windows 11 (build 22000+) honours these attributes;
|
||||
# anywhere else the call fails and the native bar is left as it is.
|
||||
_DWMWA_USE_IMMERSIVE_DARK_MODE = 20
|
||||
_DWMWA_BORDER_COLOR = 34
|
||||
_DWMWA_CAPTION_COLOR = 35
|
||||
_DWMWA_TEXT_COLOR = 36
|
||||
|
||||
|
||||
def _colorref(value: str) -> int:
|
||||
"""A ``#RRGGBB`` string as the ``0x00BBGGRR`` integer DWM expects."""
|
||||
|
||||
colour = value.lstrip("#")
|
||||
red, green, blue = (int(colour[index:index + 2], 16) for index in (0, 2, 4))
|
||||
return (blue << 16) | (green << 8) | red
|
||||
|
||||
|
||||
def apply_window_chrome(widget: Any) -> bool:
|
||||
"""Paint the native title bar in the palette that is active right now.
|
||||
|
||||
Returns whether DWM accepted the change, so a caller can tell "not Windows 11" from "done".
|
||||
"""
|
||||
|
||||
if sys.platform != "win32":
|
||||
return False
|
||||
handle = int(widget.winId())
|
||||
if not handle:
|
||||
return False
|
||||
try:
|
||||
dwm = ctypes.windll.dwmapi
|
||||
except (AttributeError, OSError): # pragma: no cover - not Windows
|
||||
return False
|
||||
dark = ctypes.c_int(1 if _THEME == "dark" else 0)
|
||||
caption = ctypes.c_uint(_colorref(CONSOLE["canvas"]))
|
||||
text = ctypes.c_uint(_colorref(CONSOLE["heading"]))
|
||||
border = ctypes.c_uint(_colorref(CONSOLE["line"]))
|
||||
applied = False
|
||||
for attribute, value in ((_DWMWA_USE_IMMERSIVE_DARK_MODE, dark), (_DWMWA_CAPTION_COLOR, caption),
|
||||
(_DWMWA_TEXT_COLOR, text), (_DWMWA_BORDER_COLOR, border)):
|
||||
result = dwm.DwmSetWindowAttribute(ctypes.c_void_p(handle), ctypes.c_int(attribute),
|
||||
ctypes.byref(value), ctypes.sizeof(value))
|
||||
applied = applied or result == 0
|
||||
return applied
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@ from datetime import date
|
||||
from pathlib import Path
|
||||
from time import monotonic
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from PySide6.QtCore import (
|
||||
QBuffer,
|
||||
@@ -2658,6 +2659,8 @@ class PrescriptionEditorDialog(QDialog):
|
||||
else None
|
||||
)
|
||||
self._source = _mapping(prescription)
|
||||
self._save_fingerprint = ""
|
||||
self._save_request_key = ""
|
||||
self._loading_data = False
|
||||
self._linked_order_generation = 0
|
||||
self._diagnosis_view: _StructuredDiagnosisDialog | None = None
|
||||
@@ -3786,6 +3789,7 @@ class PrescriptionEditorDialog(QDialog):
|
||||
"audit_remark",
|
||||
"business_prescription_audit_rejected",
|
||||
"business_prescription_audit_remark",
|
||||
"ai_assisted",
|
||||
)
|
||||
result = {key: self._source.get(key) for key in hidden_keys if key in self._source}
|
||||
if self.mode == "add":
|
||||
@@ -3838,6 +3842,20 @@ class PrescriptionEditorDialog(QDialog):
|
||||
result["dosage_amount"] = dosage_amount
|
||||
else:
|
||||
result.pop("dosage_amount", None)
|
||||
# This editor has no independent-assistance attestation control. Only
|
||||
# retain positive evidence of AI exposure; a seed's false is not a new
|
||||
# doctor-confirmed statement that this submission was unassisted.
|
||||
if result.get("ai_assisted") in (True, 1, "1", "true"):
|
||||
result["ai_assisted"] = True
|
||||
else:
|
||||
result.pop("ai_assisted", None)
|
||||
# A repeated submit of unchanged editor content keeps its idempotency key.
|
||||
# Unknown AI exposure is omitted, rather than falsely asserted as False.
|
||||
fingerprint = json.dumps(result, ensure_ascii=False, sort_keys=True, default=str)
|
||||
if fingerprint != self._save_fingerprint:
|
||||
self._save_fingerprint = fingerprint
|
||||
self._save_request_key = str(uuid4())
|
||||
result["request_key"] = self._save_request_key
|
||||
return result
|
||||
|
||||
def _herb_validation_label(self, global_index: int) -> str:
|
||||
|
||||
@@ -8,7 +8,7 @@ from html import escape
|
||||
from math import ceil
|
||||
from typing import Any
|
||||
|
||||
from PySide6.QtCore import QDateTime, QModelIndex, QRectF, QSize, Qt
|
||||
from PySide6.QtCore import QDateTime, QEvent, QModelIndex, QRectF, QSize, Qt, QTimer
|
||||
from PySide6.QtGui import (
|
||||
QColor,
|
||||
QFont,
|
||||
@@ -33,7 +33,8 @@ from PySide6.QtWidgets import (
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QListWidget,
|
||||
QListWidgetItem,
|
||||
QListWidgetItem,
|
||||
QMenu,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
@@ -48,7 +49,16 @@ from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from .. import icons, motion
|
||||
from .. import icons, motion
|
||||
from ..dialogs.issued_prescription_ai import (
|
||||
PrescriptionAiStatisticsDialog,
|
||||
agreement_text,
|
||||
batch_running,
|
||||
can_open_issued_ai,
|
||||
present_issued_prescription_ai,
|
||||
state_text,
|
||||
status_tooltip,
|
||||
)
|
||||
from ..dialogs.prescription import (
|
||||
AuditPrescriptionDialog,
|
||||
DiagnosisDetailDialog,
|
||||
@@ -435,7 +445,22 @@ def _formula(value: Any) -> str:
|
||||
return "辅方" if text in {"2", "aux", "auxiliary", "辅方"} else "主方"
|
||||
|
||||
|
||||
def _order_warnings(row: Any) -> list[str]:
|
||||
def _is_blank_prescription(row: Any) -> bool:
|
||||
if row is None or _truthy(get_value(row, "is_system_auto")):
|
||||
return True
|
||||
raw = getattr(row, "raw", None)
|
||||
source = raw if isinstance(raw, Mapping) and raw else row
|
||||
missing = object()
|
||||
herbs = get_value(source, "herbs", missing)
|
||||
# Compact historical rows may omit herbs; omission is not an empty prescription.
|
||||
if herbs is missing:
|
||||
return False
|
||||
return not isinstance(herbs, (list, tuple)) or not any(
|
||||
str(get_value(herb, "name", "") or "").strip() for herb in herbs
|
||||
)
|
||||
|
||||
|
||||
def _order_warnings(row: Any) -> list[str]:
|
||||
"""Match the PC list's linked-order checks, including blank herb rows."""
|
||||
|
||||
raw = getattr(row, "raw", None)
|
||||
@@ -877,7 +902,7 @@ class _PrescriptionInfoDelegate(QStyledItemDelegate):
|
||||
painter.setPen(QColor(foreground))
|
||||
painter.drawText(pill, Qt.AlignmentFlag.AlignCenter, label)
|
||||
elif column != 2:
|
||||
lines = text.rsplit(" · ", 2) if column == 5 else text.rsplit(" ", 1) if column == 10 else [text]
|
||||
lines = text.rsplit(" · ", 2) if column == 5 else text.rsplit(" ", 1) if column == 10 else text.splitlines() if column == 12 else [text]
|
||||
if len(lines) > 1:
|
||||
primary, secondary = lines[0], " · ".join(lines[1:]) if column == 5 else lines[1]
|
||||
if column == 5:
|
||||
@@ -979,7 +1004,18 @@ class PrescriptionsPage(QWidget):
|
||||
self._detail_target = 0
|
||||
self._diagnosis_detail_generation = 0
|
||||
self._diagnosis_detail_target = 0
|
||||
self._mutation_pending = False
|
||||
self._mutation_pending = False
|
||||
self._ai_statuses: dict[int, dict[str, Any]] = {}
|
||||
self._ai_enabled = False
|
||||
self._ai_request_token = 0
|
||||
self._ai_pending = False
|
||||
self._ai_timer = QTimer(self)
|
||||
self._ai_timer.setInterval(5000)
|
||||
self._ai_timer.timeout.connect(self._load_ai_statuses)
|
||||
self._ai_scroll_timer = QTimer(self)
|
||||
self._ai_scroll_timer.setSingleShot(True)
|
||||
self._ai_scroll_timer.setInterval(180)
|
||||
self._ai_scroll_timer.timeout.connect(self._load_ai_statuses)
|
||||
|
||||
outer = QVBoxLayout(self)
|
||||
outer.setContentsMargins(28, 16, 26, 8)
|
||||
@@ -1010,7 +1046,11 @@ class PrescriptionsPage(QWidget):
|
||||
self.orders_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.orders_button.setVisible(has_permission(permissions, "tcm.prescriptionOrder/lists"))
|
||||
self.orders_button.clicked.connect(lambda: self._open_orders())
|
||||
header.add_action(self.orders_button)
|
||||
header.add_action(self.orders_button)
|
||||
self.ai_statistics_button = QPushButton("AI 一致度统计", header)
|
||||
self.ai_statistics_button.setVisible(has_permission(permissions, "tcm.prescriptionAi/statistics", default=False) and callable(getattr(repository, "prescription_ai_statistics", None)))
|
||||
self.ai_statistics_button.clicked.connect(self._open_ai_statistics)
|
||||
header.add_action(self.ai_statistics_button)
|
||||
self.add_button = QPushButton("新增处方", header)
|
||||
self.add_button.setObjectName("PrescriptionAddButton")
|
||||
self.add_button.setMinimumWidth(122)
|
||||
@@ -1156,7 +1196,10 @@ class PrescriptionsPage(QWidget):
|
||||
toolbar.addWidget(self.count_badge)
|
||||
toolbar.addStretch(1)
|
||||
self.view_button = self._action_button("查看", "cf.prescription/read", self._view_selected)
|
||||
toolbar.addWidget(self.view_button)
|
||||
toolbar.addWidget(self.view_button)
|
||||
self.ai_report_button = self._action_button("AI 报告", "tcm.prescriptionAi/reports", self._open_ai_report)
|
||||
self.ai_report_button.hide()
|
||||
toolbar.addWidget(self.ai_report_button)
|
||||
self.patch_button = self._action_button(
|
||||
"修改患者", "tcm.prescription/patchPatient", self._patch_selected
|
||||
)
|
||||
@@ -1181,9 +1224,17 @@ class PrescriptionsPage(QWidget):
|
||||
refresh.setIcon(_blue_prescription_icon("refresh", "#5D6B80"))
|
||||
refresh.setIconSize(QSize(15, 15))
|
||||
refresh.clicked.connect(self.refresh)
|
||||
toolbar.addWidget(refresh)
|
||||
layout.addWidget(toolbar_scroll)
|
||||
self.stack = QStackedWidget()
|
||||
toolbar.addWidget(refresh)
|
||||
layout.addWidget(toolbar_scroll)
|
||||
self.ai_status_notice = QLabel("AI 分析:正在检查服务状态。", card)
|
||||
self.ai_status_notice.setObjectName("PrescriptionAiStatusNotice")
|
||||
self.ai_status_notice.setProperty("role", "muted")
|
||||
self.ai_status_notice.setTextFormat(Qt.TextFormat.PlainText)
|
||||
self.ai_status_notice.setWordWrap(True)
|
||||
self.ai_status_notice.setMargin(12)
|
||||
self.ai_status_notice.setAccessibleName("AI 分析状态")
|
||||
layout.addWidget(self.ai_status_notice)
|
||||
self.stack = QStackedWidget()
|
||||
table_host = QWidget()
|
||||
table_layout = QVBoxLayout(table_host)
|
||||
table_layout.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -1200,7 +1251,9 @@ class PrescriptionsPage(QWidget):
|
||||
TableColumn("void_status", "作废", 72, _void_cell),
|
||||
TableColumn("doctor_name", "医生信息", 180, _doctor_cell),
|
||||
TableColumn("assistant_name", "医助", 125),
|
||||
TableColumn("create_time", "创建时间", 180, _create_time_cell),
|
||||
TableColumn("create_time", "创建时间", 180, _create_time_cell),
|
||||
TableColumn("__ai_status__", "AI 分析", 144, lambda _value, _row: "—"),
|
||||
TableColumn("__ai_agreement__", "与 AI 一致度", 130, lambda _value, _row: "—"),
|
||||
]
|
||||
)
|
||||
self.table.setObjectName("PrescriptionTable")
|
||||
@@ -1213,8 +1266,13 @@ class PrescriptionsPage(QWidget):
|
||||
self.table.horizontalHeader().setDefaultAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
||||
self.table.horizontalHeader().moveSection(2, 10)
|
||||
for column, width in enumerate((42, 192, 144, 108, 94, 144, 108, 80, 137, 83, 128)):
|
||||
self.table.setColumnWidth(column, width)
|
||||
for column, width in enumerate((42, 192, 144, 108, 94, 144, 108, 80, 137, 83, 128)):
|
||||
self.table.setColumnWidth(column, width)
|
||||
self.table.setColumnHidden(11, True)
|
||||
self.table.setColumnHidden(12, True)
|
||||
self.table.horizontalHeaderItem(12).setToolTip("分别显示千问、OpenAI 的药味与剂量一致度;点击查看逐味贡献。")
|
||||
self.table.horizontalHeader().viewport().installEventFilter(self)
|
||||
self.table.verticalScrollBar().valueChanged.connect(lambda _value: self._schedule_ai_statuses())
|
||||
self.table.setWordWrap(False)
|
||||
# Rows are not uniform: the number column's delegate grows a row that
|
||||
# carries an order warning so the warning text stays readable without a
|
||||
@@ -1227,7 +1285,10 @@ class PrescriptionsPage(QWidget):
|
||||
self.table.horizontalHeaderItem(0).setIcon(_blue_prescription_icon("checkbox", "#8A97A9", 14))
|
||||
self.table.horizontalHeaderItem(0).setTextAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.table.itemSelectionChanged.connect(self._selection_changed)
|
||||
self.table.itemDoubleClicked.connect(lambda _item: self._view_selected())
|
||||
self.table.itemDoubleClicked.connect(lambda _item: self._view_selected())
|
||||
self.table.itemClicked.connect(lambda item: self._open_ai_report() if item.column() in {11, 12} else None)
|
||||
self.table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.table.customContextMenuRequested.connect(self._open_ai_context_menu)
|
||||
table_layout.addWidget(self.table, 1)
|
||||
self.pager = InfiniteList(self._page_size)
|
||||
self.pager.bind(self.table)
|
||||
@@ -1271,9 +1332,22 @@ class PrescriptionsPage(QWidget):
|
||||
self.filter_grid.setColumnStretch(column, stretch)
|
||||
self.filter_card.setFixedHeight(200 if compact else 144)
|
||||
|
||||
def _number_column_resized(self, column: int, _old_size: int, _new_size: int) -> None:
|
||||
def _number_column_resized(self, column: int, _old_size: int, _new_size: int) -> None:
|
||||
if column == 1:
|
||||
self.table.resizeRowsToContents()
|
||||
self.table.resizeRowsToContents()
|
||||
|
||||
def eventFilter(self, watched: Any, event: Any) -> bool:
|
||||
if hasattr(self, "table") and watched is self.table.horizontalHeader().viewport() and event.type() in {QEvent.Type.MouseButtonPress, QEvent.Type.MouseButtonRelease, QEvent.Type.MouseButtonDblClick}:
|
||||
header = self.table.horizontalHeader()
|
||||
position = event.position().toPoint()
|
||||
column = header.logicalIndexAt(position)
|
||||
if column in {11, 12} and event.button() == Qt.MouseButton.LeftButton:
|
||||
relative = position.x() - header.sectionViewportPosition(column)
|
||||
# Keep native column resizing, but never rank mixed-model display
|
||||
# text or treat unavailable values as numeric zero through sorting.
|
||||
if 5 < relative < header.sectionSize(column) - 5:
|
||||
return True
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
def _action_button(
|
||||
self,
|
||||
@@ -1354,7 +1428,11 @@ class PrescriptionsPage(QWidget):
|
||||
self.doctor_filter.clear()
|
||||
self._search()
|
||||
|
||||
def refresh(self) -> None:
|
||||
def refresh(self) -> None:
|
||||
self._ai_timer.stop()
|
||||
self._ai_request_token += 1
|
||||
self._ai_pending = False
|
||||
self._ai_statuses.clear()
|
||||
self._loading = True
|
||||
self._refresh_pending = False
|
||||
self._generation += 1
|
||||
@@ -1392,8 +1470,9 @@ class PrescriptionsPage(QWidget):
|
||||
if generation != self._generation:
|
||||
return
|
||||
rows = page_items(result)
|
||||
self.table.set_rows(rows)
|
||||
self._decorate_rows()
|
||||
self.table.set_rows(rows)
|
||||
self._decorate_rows()
|
||||
self._render_ai_statuses()
|
||||
total = page_total(result, len(rows))
|
||||
self.pager.update_state(requested_page, total)
|
||||
self.count_badge.setText(f"共 {total} 条")
|
||||
@@ -1415,7 +1494,8 @@ class PrescriptionsPage(QWidget):
|
||||
self.banner.clear()
|
||||
if rows and self.table.currentRow() < 0:
|
||||
self.table.selectRow(0)
|
||||
self._selection_changed()
|
||||
self._selection_changed()
|
||||
self._load_ai_statuses()
|
||||
|
||||
def _decorate_rows(self) -> None:
|
||||
"""Apply the reference table's tags, checkbox, avatar, and row actions."""
|
||||
@@ -1489,7 +1569,7 @@ class PrescriptionsPage(QWidget):
|
||||
actions.setContentsMargins(3, 0, 3, 0)
|
||||
actions.setSpacing(3)
|
||||
actions.addStretch(1)
|
||||
if has_permission(self.permissions, "cf.prescription/read"):
|
||||
if has_permission(self.permissions, "cf.prescription/read"):
|
||||
actions.addWidget(
|
||||
_row_action_button(
|
||||
"eye",
|
||||
@@ -1499,7 +1579,15 @@ class PrescriptionsPage(QWidget):
|
||||
),
|
||||
actions_host,
|
||||
)
|
||||
)
|
||||
)
|
||||
if self._ai_enabled and can_open_issued_ai(self.permissions) and not _is_blank_prescription(row):
|
||||
actions.addWidget(
|
||||
_row_action_button(
|
||||
"eye", "AI 报告",
|
||||
lambda _checked=False, target=row: self._run_row_action(target, self._open_ai_report),
|
||||
actions_host, label="AI",
|
||||
)
|
||||
)
|
||||
if has_permission(self.permissions, "cf.prescription/edit"):
|
||||
actions.addWidget(
|
||||
_row_action_button(
|
||||
@@ -1531,7 +1619,179 @@ class PrescriptionsPage(QWidget):
|
||||
actions.addStretch(1)
|
||||
self.table.setCellWidget(row_index, 2, actions_host)
|
||||
self._sync_row_mutation_actions()
|
||||
self.table.resizeRowsToContents()
|
||||
self.table.resizeRowsToContents()
|
||||
|
||||
def _visible_ai_ids(self) -> list[int]:
|
||||
if not self.isVisible() or not self.table.isVisible():
|
||||
return []
|
||||
ids = []
|
||||
viewport = self.table.viewport().rect()
|
||||
for index in range(self.table.rowCount()):
|
||||
item = self.table.item(index, 0)
|
||||
# Column 0 may be horizontally off-screen; use row geometry only.
|
||||
top = self.table.rowViewportPosition(index)
|
||||
if item is None or top + self.table.rowHeight(index) <= 0 or top >= viewport.height():
|
||||
continue
|
||||
row = item.data(Qt.ItemDataRole.UserRole)
|
||||
if _is_blank_prescription(row):
|
||||
continue
|
||||
value = _int(first_value(row, "id", "prescription_id"), 0)
|
||||
if value > 0:
|
||||
ids.append(value)
|
||||
return list(dict.fromkeys(ids))[:100]
|
||||
|
||||
def _schedule_ai_statuses(self) -> None:
|
||||
if self.isVisible():
|
||||
self._ai_scroll_timer.start()
|
||||
|
||||
def _load_ai_statuses(self) -> None:
|
||||
only_blank = self.table.rowCount() > 0 and all(
|
||||
_is_blank_prescription(self.table.item(index, 0).data(Qt.ItemDataRole.UserRole))
|
||||
for index in range(self.table.rowCount())
|
||||
)
|
||||
self.ai_status_notice.setVisible(not only_blank)
|
||||
if only_blank:
|
||||
self._ai_timer.stop()
|
||||
self._set_ai_columns(False)
|
||||
return
|
||||
method = getattr(self.repository, "list_prescription_ai_statuses", None)
|
||||
if not has_permission(self.permissions, "tcm.prescriptionAi/statuses", default=False):
|
||||
self.ai_status_notice.setText("AI 分析:当前账号没有查看分析状态的权限,请联系管理员授权。")
|
||||
return
|
||||
if not callable(method):
|
||||
self.ai_status_notice.setText("AI 分析:当前数据模式暂不支持此功能。")
|
||||
return
|
||||
if self._ai_pending:
|
||||
return
|
||||
ids = self._visible_ai_ids()
|
||||
ids = [value for value in ids if value not in self._ai_statuses or batch_running(self._ai_statuses[value])]
|
||||
if not ids:
|
||||
self._ai_timer.stop()
|
||||
if self.table.rowCount() == 0:
|
||||
self.ai_status_notice.setText("AI 分析:当前列表没有处方,暂无可查看的分析结果。")
|
||||
return
|
||||
self._ai_pending = True
|
||||
self._ai_request_token += 1
|
||||
token, generation = self._ai_request_token, self._generation
|
||||
run_async(
|
||||
lambda: method(ids),
|
||||
on_success=lambda result: self._ai_statuses_ready(result, ids, token, generation),
|
||||
on_error=lambda error: self._ai_statuses_error(error, token, generation),
|
||||
on_finished=lambda: self._ai_statuses_finished(token),
|
||||
)
|
||||
|
||||
def _ai_statuses_finished(self, token: int) -> None:
|
||||
if token == self._ai_request_token:
|
||||
self._ai_pending = False
|
||||
|
||||
def _set_ai_columns(self, enabled: bool) -> None:
|
||||
changed = enabled != self._ai_enabled
|
||||
self._ai_enabled = enabled
|
||||
for column in (11, 12):
|
||||
self.table.setColumnHidden(column, not enabled)
|
||||
if enabled:
|
||||
header = self.table.horizontalHeader()
|
||||
header.moveSection(header.visualIndex(11), header.visualIndex(4) + 1)
|
||||
header.moveSection(header.visualIndex(12), header.visualIndex(11) + 1)
|
||||
self.table.setColumnWidth(11, 144)
|
||||
self.table.setColumnWidth(12, 130)
|
||||
self.table.setColumnWidth(2, 178)
|
||||
if changed:
|
||||
self._decorate_rows()
|
||||
|
||||
def _ai_statuses_ready(self, result: Any, ids: list[int], token: int, generation: int) -> None:
|
||||
if token != self._ai_request_token or generation != self._generation or not self.isVisible():
|
||||
return
|
||||
data = _row_mapping(result)
|
||||
self._set_ai_columns(data.get("enabled") is True)
|
||||
if not self._ai_enabled:
|
||||
self._ai_timer.stop()
|
||||
notice = "AI 分析未启用:暂不生成报告或一致度,请联系管理员启用。"
|
||||
if can_open_issued_ai(self.permissions):
|
||||
notice += "已有报告仍可从“AI 报告”查看。"
|
||||
self.ai_status_notice.setText(notice)
|
||||
self.ai_status_notice.setToolTip("")
|
||||
self.ai_report_button.setToolTip("自动分析未启用;仍可查看已保存的历史报告。")
|
||||
return
|
||||
self.ai_status_notice.setText("AI 分析已启用:保存手工处方后自动生成两份报告,并显示药味与剂量一致度。")
|
||||
self.ai_status_notice.setToolTip("")
|
||||
self.ai_report_button.setToolTip("查看两份 AI 报告、候选处方和逐味对照。")
|
||||
for value in ids:
|
||||
self._ai_statuses[value] = {}
|
||||
for batch in data.get("items") or []:
|
||||
value = _int(get_value(batch, "prescription_id"), 0)
|
||||
if value in ids:
|
||||
self._ai_statuses[value] = _row_mapping(batch)
|
||||
self._render_ai_statuses()
|
||||
if any(batch_running(self._ai_statuses.get(value)) for value in self._visible_ai_ids()):
|
||||
self._ai_timer.start()
|
||||
else:
|
||||
self._ai_timer.stop()
|
||||
|
||||
def _render_ai_statuses(self) -> None:
|
||||
sorting = self.table.isSortingEnabled()
|
||||
self.table.setSortingEnabled(False)
|
||||
changed = False
|
||||
try:
|
||||
for index in range(self.table.rowCount()):
|
||||
row = self.table.item(index, 0).data(Qt.ItemDataRole.UserRole)
|
||||
value = _int(first_value(row, "id", "prescription_id"), 0)
|
||||
batch = self._ai_statuses.get(value)
|
||||
blank = _is_blank_prescription(row)
|
||||
if batch is None and not blank:
|
||||
continue
|
||||
state = "" if blank else (state_text(batch) if batch else "尚无分析记录")
|
||||
agreement = "" if blank else agreement_text(batch)
|
||||
for column, text in ((11, state), (12, agreement)):
|
||||
item = self.table.item(index, column)
|
||||
if item.text() != text:
|
||||
item.setText(text)
|
||||
changed = True
|
||||
item.setToolTip("" if blank else status_tooltip(batch))
|
||||
item.setData(Qt.ItemDataRole.AccessibleTextRole, text)
|
||||
if changed:
|
||||
self.table.resizeRowsToContents()
|
||||
finally:
|
||||
self.table.setSortingEnabled(sorting)
|
||||
|
||||
def _ai_statuses_error(self, error: Exception, token: int, generation: int) -> None:
|
||||
if token == self._ai_request_token and generation == self._generation:
|
||||
self._ai_timer.stop()
|
||||
self._set_ai_columns(False)
|
||||
self.ai_status_notice.setText("AI 分析暂不可用:请稍后刷新;持续无法使用时,请联系管理员检查服务。")
|
||||
self.ai_status_notice.setToolTip(friendly_error(error))
|
||||
self.ai_report_button.setToolTip("AI 状态暂不可用:" + friendly_error(error))
|
||||
|
||||
def _open_ai_report(self) -> None:
|
||||
row = self._selected()
|
||||
if _is_blank_prescription(row):
|
||||
return
|
||||
value = _int(first_value(row, "id", "prescription_id"), 0)
|
||||
if value > 0:
|
||||
present_issued_prescription_ai(self.repository, self.permissions, self, prescription_id=value)
|
||||
|
||||
def _open_ai_context_menu(self, position: Any) -> None:
|
||||
if not can_open_issued_ai(self.permissions) or not callable(getattr(self.repository, "list_prescription_ai_reports", None)):
|
||||
return
|
||||
item = self.table.itemAt(position)
|
||||
if item is None:
|
||||
return
|
||||
self.table.selectRow(item.row())
|
||||
row = self._selected()
|
||||
if _is_blank_prescription(row):
|
||||
return
|
||||
menu = QMenu(self.table)
|
||||
action = menu.addAction("AI 报告 / 逐味对照")
|
||||
action.triggered.connect(lambda: self._run_row_action(row, self._open_ai_report))
|
||||
self._ai_context_menu = menu
|
||||
menu.popup(self.table.viewport().mapToGlobal(position))
|
||||
|
||||
def _open_ai_statistics(self) -> None:
|
||||
if not has_permission(self.permissions, "tcm.prescriptionAi/statistics", default=False):
|
||||
return
|
||||
dialog = PrescriptionAiStatisticsDialog(self.repository, self.permissions, self)
|
||||
dialog.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
||||
dialog.show()
|
||||
|
||||
def _run_row_action(self, row: Any, callback: Callable[[], None]) -> None:
|
||||
target_id = _int(first_value(row, "id", "prescription_id", default=None), 0)
|
||||
@@ -1566,7 +1826,14 @@ class PrescriptionsPage(QWidget):
|
||||
def _selection_changed(self) -> None:
|
||||
row = self.table.current_data()
|
||||
active = not self._mutation_pending
|
||||
self.view_button.setEnabled(active and row is not None)
|
||||
self.view_button.setEnabled(active and row is not None)
|
||||
ai_available = (
|
||||
not _is_blank_prescription(row)
|
||||
and can_open_issued_ai(self.permissions)
|
||||
and callable(getattr(self.repository, "list_prescription_ai_reports", None))
|
||||
)
|
||||
self.ai_report_button.setVisible(ai_available)
|
||||
self.ai_report_button.setEnabled(ai_available)
|
||||
self.patch_button.setEnabled(active and can_patch_patient(row))
|
||||
self.create_order_button.setEnabled(active and can_create_order(row))
|
||||
self.edit_button.setEnabled(active and can_edit_or_delete(row))
|
||||
@@ -1935,10 +2202,19 @@ class PrescriptionsPage(QWidget):
|
||||
self.banner.show_message(message, "danger")
|
||||
show_toast(self, message, "danger", 5000)
|
||||
|
||||
def showEvent(self, event: Any) -> None:
|
||||
def showEvent(self, event: Any) -> None:
|
||||
super().showEvent(event)
|
||||
if self.table.rowCount() == 0 and not self._loading:
|
||||
self.refresh()
|
||||
if self.table.rowCount() == 0 and not self._loading:
|
||||
self.refresh()
|
||||
elif not self._loading:
|
||||
self._load_ai_statuses()
|
||||
|
||||
def hideEvent(self, event: Any) -> None:
|
||||
self._ai_timer.stop()
|
||||
self._ai_scroll_timer.stop()
|
||||
self._ai_request_token += 1
|
||||
self._ai_pending = False
|
||||
super().hideEvent(event)
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -127,6 +127,13 @@ def format_record_time(value: Any, default: str = "—") -> str:
|
||||
raw = str(value).strip()
|
||||
if not raw:
|
||||
return default
|
||||
# The API sends 0 for "not set yet" (a snapshot cutoff that has not been frozen, an
|
||||
# unfinished task); rendering it as a bare 0 or as 1970 would read as a real time.
|
||||
try:
|
||||
if float(raw) <= 0:
|
||||
return default
|
||||
except ValueError:
|
||||
pass
|
||||
if _UNIX_TIMESTAMP_RE.fullmatch(raw):
|
||||
stamp = float(raw)
|
||||
if stamp >= 10_000_000_000:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,399 @@
|
||||
"""Offline rendering and data-integrity checks for the prescription workspace."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import socket
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
import pytest
|
||||
from PySide6.QtCore import QEvent, QObject, Qt
|
||||
from PySide6.QtTest import QTest
|
||||
from PySide6.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
from doctor_workstation.ui.dialogs.issued_prescription_ai_comparison import (
|
||||
PrescriptionComparisonPanel,
|
||||
)
|
||||
|
||||
|
||||
def saved_row(name: str = "黄芪", *, doctor: Any = 30, candidate: Any = 15, unit: str = "g", basis: str = "per_dose", formula: str = "主方", processing: str = "生品") -> dict[str, Any]:
|
||||
identity = {"name": name, "processing": processing, "formula_type": formula, "administration_route": "口服", "group": "", "unit": unit, "dose_basis": basis}
|
||||
return {**identity, "key": "a1b2c3d4" * 8, "doctor": {**identity, "dosage": doctor}, "candidate": {**identity, "dosage": candidate}, "doctor_dosage": doctor, "candidate_dosage": candidate, "match_type": "matched"}
|
||||
|
||||
|
||||
def saved_batch(rows: list[dict[str, Any]] | None = None) -> dict[str, Any]:
|
||||
rows = rows if rows is not None else [saved_row(), saved_row("白术", doctor=9, candidate=12)]
|
||||
herbs = []
|
||||
for row in rows:
|
||||
if row.get("candidate"):
|
||||
original = deepcopy(row["candidate"])
|
||||
original.pop("source_rows", None)
|
||||
row["candidate"].setdefault("source_rows", [len(herbs)])
|
||||
herbs.append(original)
|
||||
candidate = {"status": "available_for_review", "prescription_name": "补中益气汤加减", "herbs": herbs, "usage_instruction": "水煎温服", "times_per_day": 2, "usage_days": 7}
|
||||
qwen = {"status": "succeeded", "candidate": candidate, "comparison": {"status": "comparable", "score": 68.5, "herb_score": 100, "rows": rows}}
|
||||
openai = deepcopy(qwen)
|
||||
openai["candidate"]["prescription_name"] = "益气健脾方"
|
||||
return {"id": 44, "validity": "current", "status": "completed", "models": {"qwen": qwen, "openai": openai}}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def application() -> QApplication:
|
||||
return QApplication.instance() or QApplication([])
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def no_network(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def denied(*_args: Any, **_kwargs: Any) -> None:
|
||||
pytest.fail("The comparison panel must never contact external systems")
|
||||
monkeypatch.setattr(socket.socket, "connect", denied)
|
||||
monkeypatch.setattr(socket.socket, "connect_ex", denied)
|
||||
monkeypatch.setattr(socket, "create_connection", denied)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def panel(application: QApplication):
|
||||
host = QWidget()
|
||||
host.resize(1120, 400)
|
||||
layout = QVBoxLayout(host)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
widget = PrescriptionComparisonPanel(host)
|
||||
layout.addWidget(widget)
|
||||
host.show()
|
||||
application.processEvents()
|
||||
yield widget
|
||||
host.close()
|
||||
host.deleteLater()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_saved_snapshot_is_readable_and_model_switch_is_explicit(panel: PrescriptionComparisonPanel, application: QApplication) -> None:
|
||||
data = saved_batch()
|
||||
before = deepcopy(data)
|
||||
panel.set_batch(data)
|
||||
application.processEvents()
|
||||
assert data == before
|
||||
assert panel.selected_model == "qwen"
|
||||
assert panel.model_buttons["qwen"].isChecked()
|
||||
assert panel.name_label.text() == "补中益气汤加减"
|
||||
assert panel.herb_table.rowCount() == 2
|
||||
assert panel.herb_table.item(0, 1).text() == "30 克 / 每剂"
|
||||
assert panel.herb_table.item(0, 2).text() == "15 克 / 每剂"
|
||||
assert "主方" in panel.herb_table.item(0, 0).text()
|
||||
assert "a1b2c3d4" not in panel.chart.accessibleDescription()
|
||||
assert "药味剂量一致度" not in panel.chart.accessibleDescription()
|
||||
assert "水煎温服" in panel.usage_label.text()
|
||||
assert panel.chart.bars_enabled
|
||||
assert panel.chart.groups[0][0] == ("克", "每剂")
|
||||
panel.model_buttons["openai"].click()
|
||||
assert panel.selected_model == "openai"
|
||||
assert panel.name_label.text() == "益气健脾方"
|
||||
assert panel.herb_table.horizontalHeaderItem(2).text() == "OpenAI剂量"
|
||||
assert panel.chart.model_key == "openai"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("candidate_key", ["candidate_dose", "candidate_dosage", "ai_dose"])
|
||||
def test_legacy_flat_doses_remain_supported(panel: PrescriptionComparisonPanel, candidate_key: str) -> None:
|
||||
data = saved_batch()
|
||||
data["models"]["qwen"]["comparison"]["rows"] = [{"name": "黄芪", "doctor_dose": 0, candidate_key: "12.50", "unit": "g", "dose_basis": "每剂", "formula_type": "主方"}]
|
||||
panel.set_batch(data)
|
||||
assert panel.herb_table.item(0, 1).text() == "0 克 / 每剂"
|
||||
assert panel.herb_table.item(0, 2).text() == "12.50 克 / 每剂"
|
||||
assert panel.chart.rows[0].doctor.number == 0
|
||||
|
||||
|
||||
def test_absent_historical_doctor_is_never_replaced_with_current_prescription(panel: PrescriptionComparisonPanel) -> None:
|
||||
data = saved_batch()
|
||||
data["models"]["qwen"]["comparison"]["rows"] = []
|
||||
data["prescription"] = {"herbs": [{"name": "黄芪", "dosage": 99, "unit": "g", "dose_basis": "per_dose"}]}
|
||||
panel.set_batch(data)
|
||||
assert panel.herb_table.rowCount() == 2
|
||||
assert panel.herb_table.item(0, 1).text() == "—"
|
||||
assert panel.herb_table.item(0, 2).text() == "15 克 / 每剂"
|
||||
assert not panel.chart.bars_enabled
|
||||
assert "历史处方" in panel.status_label.text()
|
||||
assert "99" not in panel.chart.accessibleDescription()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rows", [None, {}, "unavailable"])
|
||||
def test_malformed_comparison_keeps_candidate_list_without_claiming_a_snapshot(panel: PrescriptionComparisonPanel, rows: Any) -> None:
|
||||
data = saved_batch()
|
||||
data["models"]["qwen"]["comparison"]["rows"] = rows
|
||||
panel.set_batch(data)
|
||||
assert panel.herb_table.rowCount() == 2
|
||||
assert panel.herb_table.item(0, 1).text() == "—"
|
||||
assert not panel.chart.bars_enabled
|
||||
|
||||
|
||||
def test_explicit_missing_snapshot_and_nested_unknown_unit_override_flat_values(panel: PrescriptionComparisonPanel) -> None:
|
||||
row = saved_row()
|
||||
row["doctor"] = None
|
||||
row["candidate"]["unit"] = None
|
||||
panel.set_batch(saved_batch([row]))
|
||||
assert panel.herb_table.item(0, 1).text() == "—"
|
||||
assert "单位未注明" in panel.herb_table.item(0, 2).text()
|
||||
assert panel.chart.groups[0][0] is None
|
||||
assert "未明确" in panel.chart.accessibleDescription()
|
||||
|
||||
|
||||
def test_incomparable_report_retains_original_doses_without_bars(panel: PrescriptionComparisonPanel) -> None:
|
||||
data = saved_batch()
|
||||
data["models"]["qwen"]["comparison"].update(status="not_comparable", score=99, reason="dose_basis_mismatch")
|
||||
panel.set_batch(data)
|
||||
assert "剂量基准不同" in panel.status_label.text()
|
||||
assert not panel.chart.bars_enabled
|
||||
assert panel.herb_table.item(0, 1).text() == "30 克 / 每剂"
|
||||
assert all(scale is None for scale, _rows, _maximum in panel.chart.groups)
|
||||
assert "99" not in panel.chart.accessibleDescription()
|
||||
|
||||
|
||||
def test_units_and_bases_have_independent_scales_and_mismatched_rows_are_excluded(panel: PrescriptionComparisonPanel) -> None:
|
||||
rows = [saved_row("黄芪"), saved_row("白术", unit="mg", doctor=1000), saved_row("茯苓", basis="per_day", doctor=60), saved_row("甘草")]
|
||||
rows[-1]["candidate"]["dose_basis"] = "per_day"
|
||||
panel.set_batch(saved_batch(rows))
|
||||
assert [scale for scale, _members, _maximum in panel.chart.groups] == [("克", "每剂"), ("毫克", "每剂"), ("克", "每日"), None]
|
||||
assert panel.chart.groups[0][2] == 30
|
||||
assert panel.chart.groups[1][2] == 1000
|
||||
assert "单位或剂量基准不同" in panel.chart.accessibleDescription()
|
||||
assert "每日" in panel.herb_table.item(3, 2).text()
|
||||
|
||||
|
||||
def test_main_auxiliary_and_processing_rows_never_merge(panel: PrescriptionComparisonPanel) -> None:
|
||||
rows = [saved_row("甘草", formula="主方"), saved_row("甘草", formula="辅方"), saved_row("甘草", processing="炙品")]
|
||||
rows[2]["candidate"]["processing"] = "生品"
|
||||
panel.set_batch(saved_batch(rows))
|
||||
assert panel.herb_table.rowCount() == 3
|
||||
assert "主方" in panel.herb_table.item(0, 0).text()
|
||||
assert "辅方" in panel.herb_table.item(1, 0).text()
|
||||
assert panel.chart.rows[2].scale is None
|
||||
assert "炮制" in panel.chart.accessibleDescription()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("skipped_index", [0, 1])
|
||||
def test_zero_based_trace_preserves_candidate_herbs_skipped_during_normalization(panel: PrescriptionComparisonPanel, skipped_index: int) -> None:
|
||||
normalized = saved_row("黄芪")
|
||||
normalized["candidate"]["source_rows"] = [1 - skipped_index]
|
||||
data = saved_batch([normalized])
|
||||
original = deepcopy(data["models"]["qwen"]["candidate"]["herbs"][0])
|
||||
excluded = {**original, "name": "黄芪", "processing": "炮制待核对", "dosage": 8, "instructions": "先煎 30 分钟"}
|
||||
herbs = [original]
|
||||
herbs.insert(skipped_index, excluded)
|
||||
data["models"]["qwen"]["candidate"]["herbs"] = herbs
|
||||
# Even a contradictory comparable status must not grant the omitted raw herb a bar.
|
||||
panel.set_batch(data)
|
||||
assert panel.herb_table.rowCount() == 2
|
||||
assert panel.herb_table.item(0, 2).text() == "15 克 / 每剂"
|
||||
assert "未纳入对比" in panel.herb_table.item(1, 0).text()
|
||||
assert "炮制待核对" in panel.herb_table.item(1, 0).text()
|
||||
assert panel.herb_table.item(1, 1).text() == "—"
|
||||
assert panel.herb_table.item(1, 2).text() == "8 克 / 每剂"
|
||||
assert panel.chart.rows[1].scale is None
|
||||
assert "先煎 30 分钟" in panel.chart.accessibleDescription()
|
||||
assert "原方 2 项" in panel.count_label.text() and "未纳入 1 项" in panel.count_label.text()
|
||||
|
||||
|
||||
def test_merged_source_rows_cover_each_original_once_and_keep_original_doses(panel: PrescriptionComparisonPanel) -> None:
|
||||
merged = saved_row("黄芪", candidate=10)
|
||||
merged["candidate"]["source_rows"] = [0, 1]
|
||||
data = saved_batch([merged])
|
||||
original = data["models"]["qwen"]["candidate"]["herbs"][0]
|
||||
data["models"]["qwen"]["candidate"]["herbs"] = [{**original, "name": "北芪", "dosage": 4}, {**original, "dosage": 6}, {**original, "name": "未识别药材", "dosage": 5}]
|
||||
panel.set_batch(data)
|
||||
assert panel.herb_table.rowCount() == 2
|
||||
assert panel.herb_table.item(0, 2).text() == "10 克 / 每剂"
|
||||
tooltip = panel.herb_table.item(0, 0).toolTip()
|
||||
assert "第 1 项 北芪 4 克 / 每剂" in tooltip
|
||||
assert "第 2 项 黄芪 6 克 / 每剂" in tooltip
|
||||
assert "未识别药材" in panel.herb_table.item(1, 0).text()
|
||||
assert "未纳入对比" in panel.herb_table.item(1, 0).text()
|
||||
assert "原方 3 项" in panel.count_label.text() and "对比 1 项" in panel.count_label.text()
|
||||
panel.search.setText("北芪")
|
||||
assert panel.herb_table.rowCount() == 1
|
||||
assert "黄芪" in panel.herb_table.item(0, 0).text()
|
||||
|
||||
|
||||
def test_legacy_missing_trace_retains_full_original_separately_without_name_matching(panel: PrescriptionComparisonPanel) -> None:
|
||||
data = saved_batch([saved_row("黄芪")])
|
||||
model = data["models"]["qwen"]
|
||||
del model["comparison"]["rows"][0]["candidate"]["source_rows"]
|
||||
original = model["candidate"]["herbs"][0]
|
||||
model["candidate"]["herbs"] = [{**original, "dosage": 4}, {**original, "name": "炮制待核对药材", "dosage": 6}]
|
||||
panel.set_batch(data)
|
||||
assert panel.herb_table.rowCount() == 3
|
||||
assert panel.herb_table.item(0, 2).text() == "15 克 / 每剂"
|
||||
for index, dose in ((1, "4 克 / 每剂"), (2, "6 克 / 每剂")):
|
||||
assert "原方附列" in panel.herb_table.item(index, 0).text()
|
||||
assert panel.herb_table.item(index, 1).text() == "—"
|
||||
assert panel.herb_table.item(index, 2).text() == dose
|
||||
assert panel.chart.rows[index].scale is None
|
||||
assert "对应关系未保存" in panel.status_label.text()
|
||||
assert "原方附列 2 项" in panel.count_label.text()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("trace", [[True], [1], [-1], ["0"], []])
|
||||
def test_invalid_source_trace_never_hides_an_original_herb(panel: PrescriptionComparisonPanel, trace: list[Any]) -> None:
|
||||
data = saved_batch([saved_row("黄芪")])
|
||||
data["models"]["qwen"]["comparison"]["rows"][0]["candidate"]["source_rows"] = trace
|
||||
panel.set_batch(data)
|
||||
assert panel.herb_table.rowCount() == 2
|
||||
assert "原方附列" in panel.herb_table.item(1, 0).text()
|
||||
assert panel.chart.rows[1].scale is None
|
||||
|
||||
|
||||
def test_real_normalized_usage_keeps_all_special_instructions_visible_and_accessible(panel: PrescriptionComparisonPanel) -> None:
|
||||
row = saved_row("石膏")
|
||||
row["doctor"]["usage"] = {"decoction_instruction": "先煎 30 分钟", "special_usage": "布包煎", "usage_time": "饭后", "usage_way": "温服"}
|
||||
row["candidate"]["instructions"] = "后下 5 分钟"
|
||||
row["candidate"]["usage"] = {"instructions": "后下 5 分钟", "decoction_instruction": "另煎", "special_usage": "分次兑服", "usage_instruction": "服前摇匀", "usage_time": "睡前", "usage_way": "温服"}
|
||||
panel.set_batch(saved_batch([row]))
|
||||
name_item = panel.herb_table.item(0, 0)
|
||||
for instruction in ("先煎 30 分钟", "布包煎", "饭后", "温服", "后下 5 分钟", "另煎", "分次兑服", "服前摇匀", "睡前"):
|
||||
assert instruction in name_item.toolTip()
|
||||
assert instruction in name_item.data(Qt.ItemDataRole.AccessibleDescriptionRole)
|
||||
assert instruction in panel.chart.accessibleDescription()
|
||||
assert "先煎 30 分钟" in name_item.text() and "后下 5 分钟" in name_item.text()
|
||||
assert panel.chart.rows[0].candidate.instructions.count("后下 5 分钟") == 1
|
||||
assert panel.herb_table.rowHeight(0) >= panel.herb_table.fontMetrics().height() * 3 + 12
|
||||
|
||||
|
||||
def test_clicking_a_table_row_scrolls_to_its_chart_group(panel: PrescriptionComparisonPanel, application: QApplication) -> None:
|
||||
rows = [saved_row(f"药材{index}", unit="g" if index % 2 == 0 else "mg") for index in range(20)]
|
||||
panel.set_batch(saved_batch(rows))
|
||||
application.processEvents()
|
||||
item = panel.herb_table.item(11, 0)
|
||||
panel.herb_table.scrollToItem(item)
|
||||
application.processEvents()
|
||||
panel.chart_scroll.verticalScrollBar().setValue(0)
|
||||
QTest.mouseClick(panel.herb_table.viewport(), Qt.MouseButton.LeftButton, pos=panel.herb_table.visualItemRect(item).center())
|
||||
application.processEvents()
|
||||
assert panel.herb_table.currentRow() == 11
|
||||
# Ten gram rows precede the milligram group; the clicked row is its sixth member.
|
||||
expected_y = 8 + panel.chart.GROUP_HEIGHT * 2 + panel.chart.ROW_HEIGHT * 15
|
||||
assert panel.chart_scroll.verticalScrollBar().value() == expected_y
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["processing", "formula_type", "administration_route", "group"])
|
||||
def test_unrecognized_identity_values_do_not_become_equal_after_localization(panel: PrescriptionComparisonPanel, field: str) -> None:
|
||||
row = saved_row()
|
||||
row["doctor"][field] = "unknown_first"
|
||||
row["candidate"][field] = "unknown_second"
|
||||
panel.set_batch(saved_batch([row]))
|
||||
assert panel.chart.rows[0].scale is None
|
||||
assert "unknown_first" not in panel.chart.accessibleDescription()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [None, "适量", -3, float("nan"), float("inf"), True])
|
||||
def test_invalid_or_missing_doses_never_produce_numeric_bars(panel: PrescriptionComparisonPanel, application: QApplication, value: Any) -> None:
|
||||
panel.set_batch(saved_batch([saved_row(doctor=value)]))
|
||||
assert panel.chart.rows[0].doctor.number is None
|
||||
assert panel.chart.groups[0][2] == 15
|
||||
application.processEvents()
|
||||
assert not panel.chart.grab().isNull()
|
||||
if value is None or value is True:
|
||||
assert panel.herb_table.item(0, 1).text() == "—"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("status", "expected"), [("running", "正在生成"), ("failed", "未完成"), ("future_state", "状态未确认")])
|
||||
def test_processing_failed_and_unknown_model_states_are_explicit(panel: PrescriptionComparisonPanel, status: str, expected: str) -> None:
|
||||
data = saved_batch()
|
||||
data["models"]["qwen"].update(status=status, candidate=None, comparison=None)
|
||||
panel.set_batch(data)
|
||||
assert expected in panel.status_label.text()
|
||||
assert panel.empty_label.isVisible()
|
||||
assert panel.herb_table.rowCount() == 0
|
||||
assert not panel.chart.bars_enabled
|
||||
|
||||
|
||||
@pytest.mark.parametrize("validity", ["stale", "source_updated", "future_validity"])
|
||||
def test_outdated_and_unknown_validity_only_show_saved_original_values(panel: PrescriptionComparisonPanel, validity: str) -> None:
|
||||
data = saved_batch()
|
||||
data["validity"] = validity
|
||||
panel.set_batch(data)
|
||||
assert not panel.chart.bars_enabled
|
||||
assert "历史原值" in panel.status_label.text()
|
||||
assert panel.herb_table.rowCount() == 2
|
||||
|
||||
|
||||
def test_refresh_preserves_search_model_selection_and_both_scroll_positions(panel: PrescriptionComparisonPanel, application: QApplication) -> None:
|
||||
data = saved_batch([saved_row(f"黄芪{index}") for index in range(40)])
|
||||
panel.set_batch(data)
|
||||
panel.model_buttons["openai"].click()
|
||||
panel.search.setText("黄芪")
|
||||
panel.herb_table.selectRow(10)
|
||||
application.processEvents()
|
||||
panel.herb_table.verticalScrollBar().setValue(180)
|
||||
panel.chart_scroll.verticalScrollBar().setValue(250)
|
||||
before = (panel.herb_table.verticalScrollBar().value(), panel.chart_scroll.verticalScrollBar().value())
|
||||
for _ in range(3):
|
||||
panel.set_batch(deepcopy(data))
|
||||
application.processEvents()
|
||||
assert panel.search.text() == "黄芪"
|
||||
assert panel.selected_model == "openai"
|
||||
assert panel.herb_table.currentRow() == 10
|
||||
assert before == (panel.herb_table.verticalScrollBar().value(), panel.chart_scroll.verticalScrollBar().value())
|
||||
changed = deepcopy(data)
|
||||
changed["models"]["qwen"]["progress"] = {"stage": "completed"}
|
||||
panel.set_batch(changed)
|
||||
application.processEvents()
|
||||
assert panel.search.text() == "黄芪" and panel.selected_model == "openai"
|
||||
assert before == (panel.herb_table.verticalScrollBar().value(), panel.chart_scroll.verticalScrollBar().value())
|
||||
panel.search.setText("黄芪39")
|
||||
assert panel.herb_table.rowCount() == 1
|
||||
assert len(panel.chart.rows) == 1
|
||||
panel.search.setText("未匹配")
|
||||
assert "没有匹配" in panel.empty_label.text()
|
||||
|
||||
|
||||
def test_parent_owned_controls_do_not_flash_windows_on_update(application: QApplication) -> None:
|
||||
shown_windows = []
|
||||
|
||||
class WindowObserver(QObject):
|
||||
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
|
||||
if event.type() == QEvent.Type.Show and isinstance(watched, QWidget) and watched.isWindow():
|
||||
shown_windows.append(watched)
|
||||
return False
|
||||
|
||||
observer = WindowObserver()
|
||||
application.installEventFilter(observer)
|
||||
host = QWidget()
|
||||
try:
|
||||
layout = QVBoxLayout(host)
|
||||
widget = PrescriptionComparisonPanel(host)
|
||||
layout.addWidget(widget)
|
||||
widget.set_batch(saved_batch())
|
||||
host.show()
|
||||
application.processEvents()
|
||||
assert shown_windows == [host]
|
||||
shown_windows.clear()
|
||||
widget.set_batch(saved_batch([saved_row("当归")]))
|
||||
widget.model_buttons["openai"].click()
|
||||
widget.set_batch({})
|
||||
application.processEvents()
|
||||
assert shown_windows == []
|
||||
assert all(child.parentWidget() is not None for child in widget.findChildren(QWidget))
|
||||
assert widget.herb_table.rowCount() == 0
|
||||
assert widget.chart.rows == []
|
||||
finally:
|
||||
application.removeEventFilter(observer)
|
||||
host.close()
|
||||
host.deleteLater()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", [(1120, 400), (940, 340)])
|
||||
def test_compact_sizes_keep_table_and_chart_scrollable(panel: PrescriptionComparisonPanel, application: QApplication, size: tuple[int, int]) -> None:
|
||||
panel.parentWidget().resize(*size)
|
||||
panel.set_batch(saved_batch([saved_row(f"药材{index}") for index in range(60)]))
|
||||
application.processEvents()
|
||||
assert panel.width() <= size[0] and panel.height() <= size[1]
|
||||
assert panel.herb_table.viewport().height() >= 65
|
||||
assert panel.herb_table.viewport().width() >= 320
|
||||
assert panel.chart_scroll.viewport().height() >= 90
|
||||
assert panel.herb_table.verticalScrollBar().maximum() > 0
|
||||
assert panel.chart_scroll.verticalScrollBar().maximum() > 0
|
||||
assert not panel.grab().isNull()
|
||||
assert panel.herb_table.item(0, 2).data(Qt.ItemDataRole.AccessibleTextRole) == "15 克 / 每剂"
|
||||
assert "药材59" in panel.chart.accessibleDescription()
|
||||
@@ -0,0 +1,478 @@
|
||||
"""The composed pages must show the saved batch exactly, including what is missing from it."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from PySide6.QtWidgets import QApplication, QLabel
|
||||
|
||||
from doctor_workstation.ui.dialogs import issued_prescription_ai_pages as pages
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def application() -> QApplication:
|
||||
return QApplication.instance() or QApplication([])
|
||||
|
||||
|
||||
def comparison_rows(model: str) -> list[dict[str, Any]]:
|
||||
doses = {"qwen": {"生地黄": 15, "生麦冬": 12, "麸炒白术": 12}, "openai": {"生地黄": 12, "茯苓": 8}}[model]
|
||||
contributions = {"qwen": {"生地黄": 0.94, "生麦冬": 1.0}, "openai": {"生地黄": 0.75}}[model]
|
||||
doctor = {"生地黄": 16, "生麦冬": 12, "红参片": 6, "茯苓": 10}
|
||||
rows = []
|
||||
for name in sorted(set(doses) | set(doctor)):
|
||||
rows.append({
|
||||
"name": name, "unit": "g",
|
||||
"doctor_dosage": doctor.get(name),
|
||||
"candidate_dosage": doses.get(name),
|
||||
"contribution": contributions.get(name),
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def batch(**overrides: Any) -> dict[str, Any]:
|
||||
value = {
|
||||
"id": 13, "status": "success", "coverage_status": "partial", "comparison_type": "latest_context",
|
||||
"source_summary": {"attachment_count": 4, "diagnoses_count": 1, "video_calls_count": 3,
|
||||
"source_record_count": 9},
|
||||
"missing": [{"code": "TRANSCRIPT_NOT_VERIFIED_COMPLETE", "critical": True},
|
||||
{"code": "TRANSCRIPT_NOT_VERIFIED_COMPLETE", "critical": True},
|
||||
{"code": "ARCHIVE_SYNC_WATERMARK_UNAVAILABLE", "critical": False}],
|
||||
"models": {
|
||||
"qwen": {"status": "success", "algorithm_version": "prescription-soft-dice-v1.1.0",
|
||||
"prompt_version": "manual-prescription-required-candidate-v4",
|
||||
"comparison": {"status": "comparable", "rows": comparison_rows("qwen")},
|
||||
"coverage": {"files": [{"status": "processed"}, {"status": "processed"},
|
||||
{"status": "processed"}, {"status": "restricted"}]},
|
||||
"progress": {"stage_label": "处理完成", "elapsed_seconds": 135, "attempt": 1},
|
||||
"usage": {"total_calls": 2, "calls": [
|
||||
{"stage": "text:0", "ok": True, "latency_ms": 3400, "file_count": 0,
|
||||
"usage": {"completion_tokens": 1020}, "error_code": ""},
|
||||
{"stage": "final", "ok": False, "latency_ms": 14400, "file_count": 0,
|
||||
"usage": {"completion_tokens": 5617}, "error_code": "INVALID_REPORT_OUTPUT"}]}},
|
||||
"openai": {"status": "success", "comparison": {"status": "comparable", "rows": comparison_rows("openai")},
|
||||
"progress": {"stage_label": "处理完成", "elapsed_seconds": 593, "attempt": 1},
|
||||
"usage": {"calls": []}},
|
||||
},
|
||||
}
|
||||
value.update(overrides)
|
||||
return value
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- candidates page
|
||||
@pytest.fixture
|
||||
def per_herb(application: QApplication) -> pages.CandidatesPage:
|
||||
widget = pages.CandidatesPage()
|
||||
widget.resize(1200, 700)
|
||||
widget.show()
|
||||
application.processEvents()
|
||||
yield widget
|
||||
widget.close()
|
||||
|
||||
|
||||
def _dose_cells(page: pages.CandidatesPage, row: int) -> tuple[str, str]:
|
||||
return (page.table.cellWidget(row, 1).dose, page.table.cellWidget(row, 2).dose)
|
||||
|
||||
|
||||
def _verdict(page: pages.CandidatesPage, row: int) -> str:
|
||||
return page.table.cellWidget(row, 4).findChildren(QLabel)[0].text()
|
||||
|
||||
|
||||
def test_per_herb_merges_both_models_into_one_row(per_herb: pages.CandidatesPage) -> None:
|
||||
per_herb.set_batch(batch())
|
||||
names = [per_herb.table.item(row, 0).text() for row in range(per_herb.table.rowCount())]
|
||||
assert names.count("生地黄") == 1
|
||||
row = names.index("生地黄")
|
||||
assert _dose_cells(per_herb, row) == ("15 g", "12 g")
|
||||
assert per_herb.table.item(row, 3).text() == "16 g"
|
||||
assert per_herb.table.cellWidget(row, 1).contribution == 0.94
|
||||
assert per_herb.table.cellWidget(row, 2).contribution == 0.75
|
||||
|
||||
|
||||
def test_per_herb_marks_absence_without_inventing_a_dose(per_herb: pages.CandidatesPage) -> None:
|
||||
per_herb.set_batch(batch())
|
||||
names = [per_herb.table.item(row, 0).text() for row in range(per_herb.table.rowCount())]
|
||||
only_doctor = names.index("红参片")
|
||||
assert _dose_cells(per_herb, only_doctor) == ("—", "—")
|
||||
assert per_herb.table.cellWidget(only_doctor, 1).contribution is None
|
||||
assert _verdict(per_herb, only_doctor) == "仅医方使用"
|
||||
added = names.index("麸炒白术")
|
||||
assert per_herb.table.item(added, 3).text() == "未收录"
|
||||
assert _verdict(per_herb, added) == "仅 千问 收录"
|
||||
|
||||
|
||||
def test_per_herb_conclusion_states_a_dose_gap_over_the_threshold(per_herb: pages.CandidatesPage) -> None:
|
||||
per_herb.set_batch(batch())
|
||||
names = [per_herb.table.item(row, 0).text() for row in range(per_herb.table.rowCount())]
|
||||
row = names.index("生地黄")
|
||||
assert _verdict(per_herb, row) == "两模型均收录" # 医方 16,两侧差 1 与 4,未过 5 克阈值
|
||||
source = batch()
|
||||
for model in source["models"].values():
|
||||
for entry in model["comparison"]["rows"]:
|
||||
if entry["name"] == "生地黄":
|
||||
entry["candidate_dosage"] = 9
|
||||
per_herb.set_batch(source)
|
||||
names = [per_herb.table.item(index, 0).text() for index in range(per_herb.table.rowCount())]
|
||||
assert _verdict(per_herb, names.index("生地黄")) == "剂量分歧 7 g"
|
||||
|
||||
|
||||
def test_per_herb_filters_count_and_narrow_the_table(
|
||||
per_herb: pages.CandidatesPage, application: QApplication) -> None:
|
||||
per_herb.set_batch(batch())
|
||||
total = per_herb.table.rowCount()
|
||||
assert per_herb.filter_buttons["all"].text() == f"全部 {total}"
|
||||
per_herb.search.setText("生地黄")
|
||||
application.processEvents()
|
||||
assert per_herb.table.rowCount() == 1
|
||||
per_herb.search.clear()
|
||||
per_herb.filter_buttons["doctor"].click()
|
||||
application.processEvents()
|
||||
assert 0 < per_herb.table.rowCount() < total
|
||||
assert all(_verdict(per_herb, row) in {"仅医方使用", "两模型均未收录"}
|
||||
for row in range(per_herb.table.rowCount()))
|
||||
per_herb.filter_buttons["single"].click()
|
||||
application.processEvents()
|
||||
assert all("仅 " in _verdict(per_herb, row) for row in range(per_herb.table.rowCount()))
|
||||
|
||||
|
||||
def test_per_herb_cards_show_each_saved_prescription(per_herb: pages.CandidatesPage) -> None:
|
||||
per_herb.set_batch(batch(doctor_snapshot={"prescription": {
|
||||
"herbs": [{"name": "生地黄", "dosage": 16, "unit": "g"}, {"name": "红参片", "dosage": 6, "unit": "g"}],
|
||||
"prescription_type": "浓缩水丸", "dose_count": 1, "usage_instruction": "每日1剂,水煎分服。"}}))
|
||||
doctor = per_herb.cards["doctor"]
|
||||
assert doctor.summary.text() == "2 味 · 浓缩水丸"
|
||||
assert "生地黄" in doctor.herbs.text() and "16 g" in doctor.herbs.text()
|
||||
assert "每日1剂" in doctor.usage.text()
|
||||
assert per_herb.cards["qwen"].summary.text() == "尚无候选方"
|
||||
|
||||
|
||||
def test_per_herb_card_grows_for_a_long_usage_note(per_herb: pages.CandidatesPage,
|
||||
application: QApplication) -> None:
|
||||
"""A card must not cap itself and cut the herb list or the 方义 in half."""
|
||||
|
||||
note = "方中生黄芪益气固表,生地黄、生麦冬滋阴清热。" * 8
|
||||
per_herb.set_batch(batch(doctor_snapshot={"prescription": {
|
||||
"herbs": [{"name": f"药{index}", "dosage": 15, "unit": "g"} for index in range(20)],
|
||||
"prescription_type": "浓缩水丸", "dose_count": 1, "usage_instruction": note}}))
|
||||
per_herb.resize(1100, 700)
|
||||
application.processEvents()
|
||||
card = per_herb.cards["doctor"]
|
||||
assert "另有 15 味" in card.herbs.text()
|
||||
assert card.usage.height() >= card.usage.heightForWidth(card.usage.width())
|
||||
assert card.height() >= card.herbs.height() + card.usage.height()
|
||||
|
||||
|
||||
def test_per_herb_says_when_no_prescription_was_saved(per_herb: pages.CandidatesPage) -> None:
|
||||
per_herb.set_batch({})
|
||||
assert per_herb.cards["doctor"].summary.text() == "未保存原方"
|
||||
assert per_herb.cards["doctor"].herbs.text() == "尚未保存药味"
|
||||
assert per_herb.table.rowCount() == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- sources page
|
||||
@pytest.fixture
|
||||
def sources(application: QApplication) -> pages.SourcesPage:
|
||||
widget = pages.SourcesPage()
|
||||
widget.resize(900, 600)
|
||||
widget.show()
|
||||
application.processEvents()
|
||||
yield widget
|
||||
widget.close()
|
||||
|
||||
|
||||
def test_sources_groups_gaps_by_type_and_keeps_criticality(sources: pages.SourcesPage) -> None:
|
||||
sources.set_batch(batch())
|
||||
rows = {sources.gaps.item(row, 0).text(): (sources.gaps.item(row, 1).text(), sources.gaps.item(row, 2).text())
|
||||
for row in range(sources.gaps.rowCount())}
|
||||
transcript = next(key for key in rows if "转写" in key)
|
||||
assert rows[transcript] == ("关键", "2")
|
||||
archive = next(key for key in rows if "归档" in key)
|
||||
assert rows[archive][0] == "一般"
|
||||
|
||||
|
||||
def _composition(sources: pages.SourcesPage) -> dict[str, str]:
|
||||
rows = {}
|
||||
for index in range(sources.composition_layout.count()):
|
||||
widget = sources.composition_layout.itemAt(index).widget()
|
||||
labels = widget.findChildren(QLabel)
|
||||
rows[labels[0].text()] = labels[-1].text()
|
||||
return rows
|
||||
|
||||
|
||||
def test_sources_reports_attachment_reading_and_composition(sources: pages.SourcesPage) -> None:
|
||||
sources.set_batch(batch())
|
||||
assert "模型实际读取 3 个" in sources.attachment_note.text()
|
||||
assert sources.waffle.accessibleDescription() == "附件 4 个:已读 3,受限或不支持 1"
|
||||
assert _composition(sources)["问诊通话"] == "3"
|
||||
assert "soft-dice" in sources.meta.text()
|
||||
|
||||
|
||||
def test_sources_reports_what_each_model_managed_to_read(sources: pages.SourcesPage) -> None:
|
||||
sources.set_batch(batch())
|
||||
rows = {sources.per_model.item(row, 0).text(): (sources.per_model.item(row, 1).text(),
|
||||
sources.per_model.item(row, 3).text())
|
||||
for row in range(sources.per_model.rowCount())}
|
||||
assert rows["千问"] == ("3", "75%")
|
||||
assert "已读取 3" in sources.attachment_legend.text()
|
||||
|
||||
|
||||
def test_sources_meta_states_the_cutoff_and_reads_codes_in_chinese(sources: pages.SourcesPage) -> None:
|
||||
sources.set_batch(batch(cutoff_at="2026-09-10 15:29"))
|
||||
text = sources.meta.text()
|
||||
assert "资料截止:2026-09-10 15:29" in text
|
||||
assert "覆盖状态:部分资料缺失" in text # partial 在覆盖语境里说的是资料,不是进度
|
||||
assert "对照类型:最新资料对照" in text
|
||||
sources.set_batch(batch())
|
||||
assert "资料截止:—" in sources.meta.text()
|
||||
|
||||
|
||||
def test_sources_stays_empty_without_a_batch(sources: pages.SourcesPage) -> None:
|
||||
sources.set_batch({})
|
||||
assert _composition(sources) == {}
|
||||
assert sources.gaps.rowCount() == 0
|
||||
assert sources.attachment_note.text() == "本次没有附件"
|
||||
assert not sources.gap_note.isVisible()
|
||||
|
||||
|
||||
def test_sources_puts_critical_gaps_first_and_counts_them(sources: pages.SourcesPage) -> None:
|
||||
sources.set_batch(batch())
|
||||
assert "关键" in sources.gaps.item(0, 1).text()
|
||||
assert "3 项 · 关键 2" in sources.gap_title.findChildren(QLabel)[-1].text()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- progress page
|
||||
@pytest.fixture
|
||||
def progress(application: QApplication) -> pages.ProgressPage:
|
||||
widget = pages.ProgressPage()
|
||||
widget.resize(900, 600)
|
||||
widget.show()
|
||||
application.processEvents()
|
||||
yield widget
|
||||
widget.close()
|
||||
|
||||
|
||||
def test_progress_lists_every_call_with_its_outcome(progress: pages.ProgressPage) -> None:
|
||||
progress.set_batch(batch())
|
||||
assert progress.calls.rowCount() == 2
|
||||
assert progress.calls.item(0, 0).text() == "千问"
|
||||
assert progress.calls.item(0, 1).text() == "文字资料 1" # 阶段键不直接露出
|
||||
assert progress.calls.item(1, 1).text() == "生成候选与报告"
|
||||
assert progress.calls.item(0, 3).text() == "3.4 s"
|
||||
assert progress.calls.item(0, 6).text() == "通过"
|
||||
assert progress.calls.item(1, 5).text() == "5617"
|
||||
# 最慢的一次调用占满耗时分布条,其余按比例
|
||||
assert progress.calls.cellWidget(1, 2)._fraction == 1.0
|
||||
assert progress.calls.cellWidget(0, 2)._fraction < 0.3
|
||||
assert "INVALID_REPORT_OUTPUT" not in progress.calls.item(1, 5).text()
|
||||
|
||||
|
||||
def test_progress_shows_each_model_stage_and_attempt(progress: pages.ProgressPage) -> None:
|
||||
progress.set_batch(batch())
|
||||
assert "处理完成" in progress.stage_labels["qwen"].text()
|
||||
assert "第 1 次尝试" in progress.stage_labels["qwen"].text()
|
||||
assert "已用时 2 分 15 秒" in progress.stage_labels["qwen"].text()
|
||||
assert "已用时 9 分 53 秒" in progress.stage_labels["openai"].text()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- history page
|
||||
@pytest.fixture
|
||||
def history(application: QApplication) -> pages.HistoryPage:
|
||||
widget = pages.HistoryPage()
|
||||
widget.resize(900, 600)
|
||||
widget.show()
|
||||
application.processEvents()
|
||||
yield widget
|
||||
widget.close()
|
||||
|
||||
|
||||
def test_history_orders_by_time_and_keeps_uncomparable_slots_empty(history: pages.HistoryPage) -> None:
|
||||
history.set_history([
|
||||
{"id": 13, "created_at": 300, "status": "success", "comparison_type": "latest_context",
|
||||
"models": {"qwen": {"score": 18.9, "comparison_status": "comparable",
|
||||
"algorithm_version": "prescription-soft-dice-v1.1.0"},
|
||||
"openai": {"score": 16.7, "comparison_status": "comparable"}}},
|
||||
{"id": 11, "created_at": 100, "status": "failed",
|
||||
"models": {"qwen": {"score": None, "comparison_status": "not_comparable"},
|
||||
"openai": {"score": None, "comparison_status": "not_comparable"}}},
|
||||
])
|
||||
description = history.chart.accessibleDescription()
|
||||
assert description.index("11") < description.index("13") # oldest first on the chart
|
||||
assert "11 千问 — OpenAI —" in description
|
||||
assert history.table.item(0, 0).text() == "#13" # newest first in the list
|
||||
assert history.table.item(0, 3).text() == "18.9%"
|
||||
assert history.table.item(1, 3).text() == "—"
|
||||
assert history.table.item(0, 5).text() == "v1.1.0"
|
||||
assert history.chart.has_data()
|
||||
|
||||
|
||||
def test_history_reads_the_score_from_either_payload_shape(history: pages.HistoryPage) -> None:
|
||||
"""列表接口把分数摊平,详情接口留在 comparison 里,两种都要认。"""
|
||||
history.set_history([
|
||||
{"id": 40, "created_at": "2026-09-10 15:29:00", "status": "success", "comparison_type": "non_independent",
|
||||
"models": {"qwen": {"comparison": {"status": "comparable", "score": 94.44}},
|
||||
"openai": {"comparison": {"status": "not_comparable", "score": 93.3}}}},
|
||||
{"id": 39, "created_at": "2026-09-09 09:00:00", "status": "success",
|
||||
"models": {"qwen": {"score": 42.9, "comparison_status": "comparable"}}},
|
||||
])
|
||||
assert history.table.item(0, 0).text() == "#40"
|
||||
assert history.table.item(0, 3).text() == "94.4%"
|
||||
assert history.table.item(0, 4).text() == "—" # 不可比就不给分,哪怕载荷里带着数字
|
||||
assert "分层" in history.strata.text() or "同一套" in history.strata.text()
|
||||
assert history.table.item(1, 3).text() == "42.9%"
|
||||
description = history.chart.accessibleDescription()
|
||||
assert description.index("39") < description.index("40")
|
||||
|
||||
|
||||
def test_history_without_any_comparable_batch_draws_nothing(history: pages.HistoryPage) -> None:
|
||||
history.set_history([{"id": 1, "created_at": 1, "models": {"qwen": {"comparison_status": "not_comparable"}}}])
|
||||
assert not history.chart.has_data()
|
||||
assert history.table.rowCount() == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- statistics panel
|
||||
@pytest.fixture
|
||||
def statistics(application: QApplication) -> pages.StatisticsPanel:
|
||||
widget = pages.StatisticsPanel()
|
||||
widget.resize(1000, 600)
|
||||
widget.show()
|
||||
application.processEvents()
|
||||
yield widget
|
||||
widget.close()
|
||||
|
||||
|
||||
def statistics_payload() -> dict[str, Any]:
|
||||
return {
|
||||
"total_count": 10, "patient_count": 8,
|
||||
"doctors": [
|
||||
{"doctor_id": 26, "doctor_name": "何医生", "total_count": 6, "patient_count": 5, "paired_count": 3,
|
||||
"models": {"qwen": {"eligible_count": 4, "mean": 18.4, "median": 16.9,
|
||||
"excluded_reasons": {"SOURCE_HISTORY_VERSIONS_UNAVAILABLE": 2}},
|
||||
"openai": {"eligible_count": 3, "mean": 21.0, "median": 19.6,
|
||||
"excluded_reasons": {"SOURCE_HISTORY_VERSIONS_UNAVAILABLE": 3}}},
|
||||
"review": {"evaluated_count": 0, "qualified_count": 0, "qualified_rate": None}},
|
||||
{"doctor_id": 31, "doctor_name": "李医生", "total_count": 4, "patient_count": 3, "paired_count": 1,
|
||||
"models": {"qwen": {"eligible_count": 2, "mean": 25.0, "excluded_reasons": {"incomplete_coverage": 2}},
|
||||
"openai": {"eligible_count": 0, "mean": None, "excluded_reasons": {"incomplete_coverage": 4}}},
|
||||
"review": {"evaluated_count": 2, "qualified_count": 1, "qualified_rate": 50.0}},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_statistics_headline_counts_and_coverage(statistics: pages.StatisticsPanel) -> None:
|
||||
statistics.set_statistics(statistics_payload())
|
||||
assert statistics.kpi_values["events"].text() == "10"
|
||||
assert "涉及患者 8 人" in statistics.kpi_notes["events"].text()
|
||||
assert statistics.kpi_values["qwen"].text() == "6"
|
||||
assert "覆盖率 60.0%" in statistics.kpi_notes["qwen"].text()
|
||||
assert statistics.kpi_values["openai"].text() == "3"
|
||||
|
||||
|
||||
def test_statistics_review_rate_needs_a_review_sample(statistics: pages.StatisticsPanel) -> None:
|
||||
payload = statistics_payload()
|
||||
for doctor in payload["doctors"]:
|
||||
doctor["review"] = {"evaluated_count": 0, "qualified_count": 0, "qualified_rate": None}
|
||||
statistics.set_statistics(payload)
|
||||
assert statistics.kpi_values["review"].text() == "—"
|
||||
assert "尚未建立复核样本" in statistics.kpi_notes["review"].text()
|
||||
statistics.set_statistics(statistics_payload())
|
||||
assert statistics.kpi_values["review"].text() == "50.0%"
|
||||
|
||||
|
||||
def _bars(layout) -> list[str]:
|
||||
return [layout.itemAt(index).widget().accessibleDescription() for index in range(layout.count())]
|
||||
|
||||
|
||||
def test_statistics_funnel_and_exclusions_are_aggregated(statistics: pages.StatisticsPanel) -> None:
|
||||
statistics.set_statistics(statistics_payload())
|
||||
funnel = _bars(statistics.funnel_layout)
|
||||
assert "范围内开方事件 10" in funnel
|
||||
assert "千问 有效基线比较 6" in funnel
|
||||
assert "两模型配对共同样本 4" in funnel
|
||||
reasons = _bars(statistics.exclusion_layout)
|
||||
assert any(text.endswith(" 5") for text in reasons) # 来源历史版本无法重建 2 + 3
|
||||
assert all("SOURCE_HISTORY" not in text for text in reasons)
|
||||
|
||||
|
||||
def test_statistics_distribution_sums_the_saved_bins(statistics: pages.StatisticsPanel) -> None:
|
||||
payload = statistics_payload()
|
||||
payload["doctors"][0]["models"]["qwen"]["distribution"] = {"[0,20)": 3, "[20,40)": 1}
|
||||
payload["doctors"][1]["models"]["qwen"]["distribution"] = {"[0,20)": 2}
|
||||
statistics.set_statistics(payload)
|
||||
assert statistics.distribution.has_data()
|
||||
assert "千问 5/1/0/0/0" in statistics.distribution.accessibleDescription()
|
||||
assert statistics.distribution.isVisible()
|
||||
|
||||
|
||||
def test_statistics_draws_no_distribution_without_samples(statistics: pages.StatisticsPanel) -> None:
|
||||
statistics.set_statistics(statistics_payload())
|
||||
assert not statistics.distribution.has_data()
|
||||
assert statistics.chart_empty.isVisible()
|
||||
|
||||
|
||||
def test_statistics_summary_row_pairs_mean_with_median(statistics: pages.StatisticsPanel) -> None:
|
||||
statistics.set_statistics(statistics_payload())
|
||||
assert statistics.summary_values["qwen"].text() == "21.7% / 16.9%"
|
||||
assert statistics.summary_values["paired"].text() == "4 例"
|
||||
|
||||
|
||||
def test_statistics_lists_each_doctor_without_ranking(statistics: pages.StatisticsPanel) -> None:
|
||||
statistics.set_statistics(statistics_payload())
|
||||
assert statistics.doctors.rowCount() == 2
|
||||
assert statistics.doctors.item(0, 0).text() == "何医生"
|
||||
assert statistics.doctors.item(0, 3).text() == "4 / 18.4%"
|
||||
assert statistics.doctors.item(1, 4).text() == "0 / —"
|
||||
assert statistics.doctors.item(1, 6).text() == "50.0%"
|
||||
assert "不是医生准确率" in statistics.footnote.text()
|
||||
|
||||
|
||||
def test_progress_counts_the_batch_in_the_stat_row(progress: pages.ProgressPage) -> None:
|
||||
progress.set_batch(batch())
|
||||
assert progress.stat_values["calls"].text() == "2 次"
|
||||
assert progress.stat_values["failures"].text() == "1 次"
|
||||
assert progress.stat_values["repairs"].text() == "0 次"
|
||||
assert progress.stat_values["elapsed"].text() == "9 分 53 秒"
|
||||
|
||||
|
||||
def test_progress_derives_its_stages_from_the_saved_calls(progress: pages.ProgressPage) -> None:
|
||||
progress.set_batch(batch())
|
||||
names = []
|
||||
layout = progress.stage_lists["qwen"]
|
||||
for index in range(layout.count()):
|
||||
widget = layout.itemAt(index).widget()
|
||||
names.append(widget.findChildren(QLabel)[1].text())
|
||||
assert names == ["文字资料分析", "生成候选与报告"]
|
||||
assert progress.stage_lists["openai"].count() == 0 # 没有调用记录就不编造阶段
|
||||
|
||||
|
||||
def test_history_names_the_version_change_between_two_batches(history: pages.HistoryPage) -> None:
|
||||
history.set_history([
|
||||
{"id": 5, "created_at": "2026-09-10 15:29:00", "status": "success",
|
||||
"models": {"qwen": {"comparison": {"status": "comparable", "score": 15.3},
|
||||
"algorithm_version": "prescription-soft-dice-v1.1.0",
|
||||
"prompt_version": "v4"}}},
|
||||
{"id": 4, "created_at": "2026-09-10 15:18:00", "status": "success",
|
||||
"models": {"qwen": {"comparison": {"status": "comparable", "score": 18.9},
|
||||
"algorithm_version": "prescription-soft-dice-v1.0.1",
|
||||
"prompt_version": "v3"}}},
|
||||
])
|
||||
text = history.strata.text()
|
||||
assert "比较算法 v1.0.1 → v1.1.0" in text
|
||||
assert "提示词 v3 → v4" in text
|
||||
assert "不能直接相减" in text
|
||||
|
||||
|
||||
def test_history_says_when_every_batch_shares_one_version(history: pages.HistoryPage) -> None:
|
||||
history.set_history([
|
||||
{"id": 2, "created_at": "2026-09-10 15:29:00",
|
||||
"models": {"qwen": {"algorithm_version": "prescription-soft-dice-v1.1.0"}}},
|
||||
{"id": 1, "created_at": "2026-09-10 14:29:00",
|
||||
"models": {"qwen": {"algorithm_version": "prescription-soft-dice-v1.1.0"}}},
|
||||
])
|
||||
assert "全部批次使用同一套算法" in history.strata.text()
|
||||
|
||||
|
||||
def test_history_shows_why_a_batch_has_no_score(history: pages.HistoryPage) -> None:
|
||||
history.set_history([{"id": 2, "created_at": "2026-09-10 13:25:00", "status": "failed",
|
||||
"models": {"qwen": {"error_message": "模型返回未通过校验"}}}])
|
||||
assert "模型返回未通过校验" in history.table.item(0, 2).text()
|
||||
assert history.table.item(0, 3).text() == "—"
|
||||
@@ -0,0 +1,337 @@
|
||||
"""Data provenance and native UI checks for the prescription overview page."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from copy import deepcopy
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
import pytest
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
|
||||
|
||||
from doctor_workstation.ui.dialogs.issued_prescription_ai_workspace import (
|
||||
PrescriptionReviewWorkspace,
|
||||
checklist_items,
|
||||
diagnosis_text,
|
||||
dose_deltas,
|
||||
gap_counts,
|
||||
review_rows,
|
||||
)
|
||||
|
||||
|
||||
def batch(count: int = 3) -> dict:
|
||||
rows = []
|
||||
herbs = []
|
||||
for index in range(count):
|
||||
herb = {"name": f"药材{index}", "dosage": "15.00", "unit": "g", "dose_basis": "per_dose", "formula_type": "主方", "processing": "生品"}
|
||||
rows.append({"key": f"saved-identity-{index}", "name": herb["name"], "doctor": {**herb, "dosage": "30.00"}, "candidate": {**herb, "source_rows": [index]}, "match_type": "matched"})
|
||||
herbs.append(herb)
|
||||
model = {"status": "succeeded", "report": {"diagnosis": "已保存的辨证意见", "summary": "不能当作诊断的摘要", "missing_information": ["缺少舌脉记录"]}, "candidate": {"status": "available_for_review", "herbs": herbs}, "comparison": {"status": "comparable", "rows": rows}}
|
||||
return {"id": 4, "validity": "current", "models": {"qwen": deepcopy(model), "openai": deepcopy(model)}, "doctor_snapshot": {"patient": {"name": "测试患者", "gender": "male", "age": 50}, "diagnosis": {"western_diagnosis": "已记录西医诊断", "tcm_diagnosis": "已记录中医诊断", "syndrome": "已记录证候"}, "prescription": {"herbs": deepcopy(herbs), "usage_instruction": "水煎服", "usage_days": 7, "times_per_day": 2}}}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def application():
|
||||
return QApplication.instance() or QApplication([])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def workspace(application):
|
||||
host = QWidget()
|
||||
host.resize(1024, 760)
|
||||
layout = QVBoxLayout(host)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
widget = PrescriptionReviewWorkspace(host)
|
||||
layout.addWidget(widget)
|
||||
host.show()
|
||||
application.processEvents()
|
||||
yield widget
|
||||
host.close()
|
||||
host.deleteLater()
|
||||
application.processEvents()
|
||||
|
||||
|
||||
def test_three_series_need_saved_unique_identity_and_same_baseline():
|
||||
source = batch()
|
||||
before = deepcopy(source)
|
||||
rows, _ = review_rows(source)
|
||||
assert source == before
|
||||
assert len(rows) == 3
|
||||
assert all(set(row.doses) == {"doctor", "qwen", "openai"} for row in rows)
|
||||
assert rows[0].scale == ("克", "每剂")
|
||||
assert rows[0].changed
|
||||
assert "15.00 克 / 每剂" in rows[0].description
|
||||
assert "候选原方记录:第 1 项 药材0" in rows[0].description
|
||||
assert "saved-identity" not in rows[0].description
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mutation", ["no_keys", "different_baseline", "duplicate_keys", "different_units", "different_identity"])
|
||||
def test_no_guessed_cross_model_join(mutation):
|
||||
source = batch(1)
|
||||
target = source["models"]["openai"]["comparison"]["rows"][0]
|
||||
if mutation == "no_keys":
|
||||
for model in source["models"].values():
|
||||
model["comparison"]["rows"][0].pop("key")
|
||||
elif mutation == "different_baseline":
|
||||
target["doctor"]["dosage"] = 31
|
||||
elif mutation == "duplicate_keys":
|
||||
source["models"]["openai"]["comparison"]["rows"].append(deepcopy(target))
|
||||
elif mutation == "different_units":
|
||||
target["candidate"]["unit"] = "mg"
|
||||
else:
|
||||
target["candidate"]["processing"] = "炙品"
|
||||
rows, _ = review_rows(source)
|
||||
assert len(rows) >= 2
|
||||
assert all(len(row.entries) == 1 for row in rows)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["nan", "-3", "1/2", "Infinity", None])
|
||||
def test_invalid_numbers_and_missing_are_never_zero(value):
|
||||
source = batch(1)
|
||||
for model in source["models"].values():
|
||||
model["comparison"]["rows"][0]["candidate"]["dosage"] = value
|
||||
rows, _ = review_rows(source)
|
||||
assert rows[0].scale is None
|
||||
assert not rows[0].changed
|
||||
if value is None:
|
||||
assert rows[0].entries["qwen"].candidate.label == "—"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field,value", [("validity", "stale"), ("validity", "source_updated"), ("validity", None), ("status", "running"), ("status", "failed"), ("comparison", "not_comparable"), ("candidate", "withheld_for_risk")])
|
||||
def test_pending_historical_and_error_are_text_only(field, value):
|
||||
source = batch()
|
||||
if field == "validity":
|
||||
source[field] = value
|
||||
else:
|
||||
for model in source["models"].values():
|
||||
if field == "status":
|
||||
model[field] = value
|
||||
else:
|
||||
model[field]["status"] = value
|
||||
rows, _ = review_rows(source)
|
||||
assert rows
|
||||
assert all(row.scale is None for row in rows)
|
||||
assert all(not row.changed for row in rows)
|
||||
|
||||
|
||||
def test_unaccounted_candidates_and_missing_trace_survive():
|
||||
source = batch(1)
|
||||
for model in source["models"].values():
|
||||
model["candidate"]["herbs"].append({"name": "未规范炮制药", "dosage": "2.5", "unit": "g"})
|
||||
model["comparison"]["rows"][0]["candidate"].pop("source_rows")
|
||||
rows, _ = review_rows(source)
|
||||
assert len(rows) == 5
|
||||
originals = [row for row in rows if next(iter(row.entries.values())).origin != "comparison"]
|
||||
assert len(originals) == 4
|
||||
assert all(row.scale is None for row in originals)
|
||||
assert sum(row.name == "未规范炮制药" for row in originals) == 2
|
||||
|
||||
|
||||
def test_diagnosis_never_fabricated_from_summary():
|
||||
assert diagnosis_text({"report": {"summary": "摘要疾病"}}) == "诊断意见未保存"
|
||||
assert diagnosis_text({"report": {"diagnosis": "辨证原文"}}) == "辨证原文"
|
||||
assert diagnosis_text({"report": {"diagnosis": {"western_diagnosis": "诊断原值"}}}) == "西医诊断:诊断原值"
|
||||
|
||||
|
||||
def test_missing_side_is_not_a_zero_dose_difference():
|
||||
source = batch(1)
|
||||
for model in source["models"].values():
|
||||
model["comparison"]["rows"][0]["doctor"] = None
|
||||
rows, _ = review_rows(source)
|
||||
assert rows[0].doctor is None
|
||||
assert not rows[0].changed
|
||||
|
||||
|
||||
def test_historical_report_states_itself_instead_of_reporting_zero_differences(workspace):
|
||||
source = batch()
|
||||
source["validity"] = "stale"
|
||||
workspace.set_batch(source)
|
||||
assert "历史或失效报告" in workspace.summary_text
|
||||
assert workspace.doses.rows == [] # 不可比就不画,不是画成 0
|
||||
assert workspace.doses.empty.isVisible()
|
||||
|
||||
|
||||
def test_每味药与原方的距离按共同标尺画出(workspace):
|
||||
workspace.set_batch(batch())
|
||||
assert [delta.name for delta in workspace.doses.rows] == ["药材0", "药材1", "药材2"]
|
||||
assert all(delta.deltas["qwen"] == -15 for delta in workspace.doses.rows)
|
||||
assert "3 项剂量差异" in workspace.summary_text
|
||||
axis = workspace.doses.body.findChildren(QWidget)
|
||||
described = [widget.accessibleDescription() for widget in axis if widget.accessibleName() == "剂量差异条"]
|
||||
assert "药材0 千问 −15克 OpenAI −15克" in described
|
||||
|
||||
|
||||
def test_a_herb_one_model_never_listed_is_marked_absent_not_zero(workspace):
|
||||
source = batch(1)
|
||||
source["models"]["openai"]["comparison"]["rows"][0]["candidate"] = None
|
||||
source["models"]["openai"]["candidate"]["herbs"] = []
|
||||
workspace.set_batch(source)
|
||||
delta = workspace.doses.rows[0]
|
||||
assert delta.deltas["openai"] is None
|
||||
assert delta.badge == "OpenAI 未收录"
|
||||
assert delta.deltas["qwen"] == -15
|
||||
|
||||
|
||||
def test_an_unreadable_saved_dose_takes_the_whole_row_off_the_chart(workspace):
|
||||
source = batch(1)
|
||||
source["models"]["openai"]["comparison"]["rows"][0]["candidate"]["dosage"] = "nan"
|
||||
workspace.set_batch(source)
|
||||
assert workspace.doses.rows == []
|
||||
|
||||
|
||||
def test_an_incomparable_basis_keeps_the_row_off_the_chart(workspace):
|
||||
source = batch(1)
|
||||
for model in source["models"].values():
|
||||
model["comparison"]["rows"][0]["doctor"]["dose_basis"] = "per_day"
|
||||
workspace.set_batch(source)
|
||||
assert workspace.doses.rows == []
|
||||
|
||||
|
||||
def test_a_pending_model_adds_no_row_and_no_difference(workspace):
|
||||
source = batch(1)
|
||||
source["models"]["openai"] = {"status": "running"}
|
||||
workspace.set_batch(source)
|
||||
assert len(workspace.doses.rows) == 1
|
||||
assert workspace.doses.rows[0].deltas["openai"] is None
|
||||
|
||||
|
||||
def test_new_herbs_carry_their_full_dose_and_say_which_model_added_them(workspace):
|
||||
source = batch(1)
|
||||
for model in source["models"].values():
|
||||
model["comparison"]["rows"][0]["doctor"] = None
|
||||
workspace.set_batch(source)
|
||||
delta = workspace.doses.rows[0]
|
||||
assert delta.doctor is None
|
||||
assert delta.deltas["qwen"] == 15
|
||||
|
||||
|
||||
def test_gap_counts_split_the_saved_gaps_into_three_buckets() -> None:
|
||||
source = batch(1)
|
||||
source["missing"] = [{"code": "TRANSCRIPT_NOT_VERIFIED_COMPLETE", "critical": True},
|
||||
{"code": "ATTACHMENT_STORAGE_RESTRICTED"},
|
||||
{"code": "ARCHIVE_SYNC_WATERMARK_UNAVAILABLE"}]
|
||||
assert gap_counts(source) == {"critical": 1, "attachment": 1, "other": 1}
|
||||
assert gap_counts(batch(1)) == {"critical": 0, "attachment": 0, "other": 0}
|
||||
|
||||
|
||||
def _conclusion_texts(workspace) -> list[str]:
|
||||
"""The numbered sentences, skipping the hairlines the design puts between them."""
|
||||
|
||||
texts = []
|
||||
for index in range(workspace.conclusions.body_layout.count()):
|
||||
widget = workspace.conclusions.body_layout.itemAt(index).widget()
|
||||
labels = widget.findChildren(QLabel) if widget is not None else []
|
||||
if labels:
|
||||
texts.append(labels[-1].text())
|
||||
return texts
|
||||
|
||||
|
||||
def test_conclusions_state_the_counts_they_were_built_from(workspace) -> None:
|
||||
workspace.set_batch(batch(3))
|
||||
texts = _conclusion_texts(workspace)
|
||||
assert any("两个模型都给出了候选方" in text for text in texts)
|
||||
assert any("剂量偏差共" in text for text in texts)
|
||||
assert any("没有记录资料缺口" in text for text in texts)
|
||||
|
||||
|
||||
def test_conclusions_never_invent_a_score_comparison(workspace) -> None:
|
||||
source = batch(1)
|
||||
source["models"]["openai"]["comparison"]["status"] = "not_comparable"
|
||||
workspace.set_batch(source)
|
||||
texts = _conclusion_texts(workspace)
|
||||
assert any("只有可比的一侧有分数" in text for text in texts)
|
||||
|
||||
|
||||
def test_attribution_groups_every_herb_exactly_once(workspace) -> None:
|
||||
source = batch(2)
|
||||
source["models"]["openai"]["candidate"]["herbs"] = [{"name": "药材0", "dosage": "15.00", "unit": "g"}]
|
||||
workspace.set_batch(source)
|
||||
assert workspace.attribution.rows["all"]["count"].text() == "1 味"
|
||||
assert workspace.attribution.rows["doctor_only"]["count"].text() == "0 味"
|
||||
assert workspace.attribution.rows["openai_only"]["count"].text() == "0 味"
|
||||
# 药材1 只有医方与千问共用,四个分组都不含它,标题必须说明这一点
|
||||
assert "合计 2 味" in workspace.attribution.total_note.text()
|
||||
assert "另 1 味为医方与单一模型共用" in workspace.attribution.total_note.text()
|
||||
|
||||
|
||||
def test_risk_panel_flags_only_large_changes(workspace) -> None:
|
||||
small = batch(1)
|
||||
for model in small["models"].values():
|
||||
model["comparison"]["rows"][0]["candidate"]["dosage"] = "29.00"
|
||||
workspace.set_batch(small)
|
||||
assert workspace.risks.empty.isVisible() # 差 1 克不进清单
|
||||
large = batch(1)
|
||||
for model in large["models"].values():
|
||||
model["comparison"]["rows"][0]["candidate"]["dosage"] = "12.00"
|
||||
workspace.set_batch(large)
|
||||
assert workspace.risks.body.isVisible()
|
||||
assert "2 项" in workspace.risks.hint.text()
|
||||
|
||||
|
||||
def test_checklist_merges_the_models_and_keeps_the_severe_items_first():
|
||||
source = batch(1)
|
||||
source["models"]["qwen"]["report"]["risk_assessment"] = [{"level": "high", "label": "血压数据缺失"}]
|
||||
source["missing"] = [{"code": "TRANSCRIPT_NOT_VERIFIED_COMPLETE", "critical": True}]
|
||||
items = checklist_items(source)
|
||||
assert items[0][0] == "血压数据缺失"
|
||||
assert items[0][1] == "千问 关键"
|
||||
shared = next(item for item in items if item[0] == "缺少舌脉记录")
|
||||
assert shared[1] == "千问、OpenAI"
|
||||
assert any("转写" in item[0] for item in items)
|
||||
|
||||
|
||||
def test_checklist_shows_the_saved_items_and_says_so_when_empty(workspace):
|
||||
workspace.set_batch(batch())
|
||||
assert "1 条 · 按严重度排序" in workspace.checklist.count_note.text()
|
||||
assert not workspace.checklist.empty.isVisible()
|
||||
workspace.set_batch({"id": 9, "validity": "current", "models": {}})
|
||||
assert workspace.checklist.empty.isVisible()
|
||||
assert workspace.checklist.count_note.text() == "暂无待确认项"
|
||||
|
||||
|
||||
def test_rerendering_leaves_no_stale_row_widgets(workspace, application):
|
||||
workspace.set_batch(batch(20))
|
||||
application.processEvents()
|
||||
assert len(_axes(workspace)) == 20
|
||||
workspace.set_batch(batch(3))
|
||||
application.processEvents()
|
||||
assert len(_axes(workspace)) == 3
|
||||
|
||||
|
||||
def _axes(workspace):
|
||||
return [widget for widget in workspace.doses.body.findChildren(QWidget)
|
||||
if widget.accessibleName() == "剂量差异条" and widget.parentWidget() is not None]
|
||||
|
||||
|
||||
def test_page_fits_a_1024_window_without_horizontal_scrolling(workspace, application):
|
||||
workspace.set_batch(batch())
|
||||
application.processEvents()
|
||||
assert workspace.minimumSizeHint().width() <= 900
|
||||
assert workspace.width() == 1024
|
||||
assert workspace.doses.scroll.horizontalScrollBarPolicy() == Qt.ScrollBarPolicy.ScrollBarAlwaysOff
|
||||
assert workspace.checklist.scroll.horizontalScrollBarPolicy() == Qt.ScrollBarPolicy.ScrollBarAlwaysOff
|
||||
|
||||
|
||||
def test_dose_deltas_ignore_rows_without_a_shared_scale():
|
||||
source = batch(1)
|
||||
for model in source["models"].values():
|
||||
model["comparison"]["rows"][0]["candidate"]["unit"] = "mg"
|
||||
rows, _states = review_rows(source)
|
||||
assert dose_deltas(rows) == []
|
||||
|
||||
|
||||
def test_review_slot_has_no_visible_parentless_widget(workspace, application):
|
||||
before = {widget for widget in application.topLevelWidgets() if widget.isVisible()}
|
||||
first = QLabel("第一模型复核", workspace)
|
||||
second = QLabel("双模型复核", workspace)
|
||||
workspace.set_review_widget(first)
|
||||
workspace.set_review_widget(second)
|
||||
application.processEvents()
|
||||
assert second.parentWidget() is workspace.checklist.review_slot
|
||||
assert not first.isVisible()
|
||||
assert first.parentWidget() is workspace.checklist.review_slot
|
||||
assert {widget for widget in application.topLevelWidgets() if widget.isVisible()} == before
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ from doctor_workstation.ui.pages import prescription_library as library_module
|
||||
from doctor_workstation.ui.pages import prescriptions as prescriptions_module
|
||||
from doctor_workstation.ui.pages.prescription_library import PrescriptionLibraryPage
|
||||
from doctor_workstation.ui.pages.prescriptions import PrescriptionsPage
|
||||
from doctor_workstation.ui.widgets import BusinessPager
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
|
||||
@@ -303,7 +303,8 @@ def test_issued_page_sends_exact_filter_dto_and_row_guards(
|
||||
page.table.selectRow(0)
|
||||
page._selection_changed()
|
||||
|
||||
assert page.table.columnCount() == 11
|
||||
assert page.table.columnCount() == 13
|
||||
assert page.table.isColumnHidden(11) and page.table.isColumnHidden(12)
|
||||
assert page.table.horizontalHeaderItem(2).text() == "操作"
|
||||
assert page.table.cellWidget(0, 2) is not None
|
||||
# 处方类型与审核状态由 _RowDecorationDelegate 绘制标签,不再为每行每列
|
||||
|
||||
@@ -121,7 +121,8 @@ def test_shell_preserves_all_columns_filters_and_reachable_pager(
|
||||
table = page.table
|
||||
assert [column.key for column in table.columns] == [
|
||||
"__selected__", "sn", "__actions__", "prescription_type", "is_system_auto",
|
||||
"patient_name", "audit_status", "void_status", "doctor_name", "assistant_name", "create_time",
|
||||
"patient_name", "audit_status", "void_status", "doctor_name", "assistant_name", "create_time",
|
||||
"__ai_status__", "__ai_agreement__",
|
||||
]
|
||||
assert [table.horizontalHeader().logicalIndex(index) for index in range(11)] == [
|
||||
0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 2,
|
||||
|
||||
@@ -46,6 +46,15 @@ def test_format_record_time_with_datetime_returns_minute_precision() -> None:
|
||||
assert format_record_time(date(2026, 8, 20)) == "2026-08-20"
|
||||
|
||||
|
||||
def test_format_record_time_with_unset_epoch_returns_default() -> None:
|
||||
# The API sends 0 (and occasionally "0") for a snapshot cutoff or finish time that does
|
||||
# not exist yet; it must not be rendered as a bare 0 or as 1970.
|
||||
assert format_record_time(0) == "—"
|
||||
assert format_record_time("0") == "—"
|
||||
assert format_record_time(-1) == "—"
|
||||
assert format_record_time(0, default="未开始") == "未开始"
|
||||
|
||||
|
||||
def test_format_record_time_with_garbage_returns_raw_value() -> None:
|
||||
assert format_record_time("not-a-timestamp") == "not-a-timestamp"
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
// Local-only additive schema upgrade. Never prints connection secrets or patient content.
|
||||
$root = dirname(__DIR__, 2) . '/server/';
|
||||
chdir($root);
|
||||
require $root . 'vendor/autoload.php';
|
||||
$app = new \think\App($root);
|
||||
$app->initialize();
|
||||
use think\facade\Db;
|
||||
try {
|
||||
$connection = (string) config('database.default');
|
||||
$cfg = (array) config('database.connections.' . $connection);
|
||||
if (!in_array((string) ($cfg['hostname'] ?? ''), ['127.0.0.1', 'localhost', '::1'], true)
|
||||
|| ($cfg['prefix'] ?? '') !== 'zyt_') {
|
||||
throw new RuntimeException('Local database/prefix guard rejected');
|
||||
}
|
||||
$active = Db::name('prescription_ai_task')->whereIn('status', ['queued', 'running', 'retry_wait'])->count()
|
||||
+ Db::name('prescription_ai_batch')->where('validity', 'current')->whereIn('status', ['preparing', 'waiting_sources', 'queued', 'running', 'retry_wait'])->count();
|
||||
if ($active > 0) { throw new RuntimeException('Tasks must finish before this local upgrade'); }
|
||||
$snapshot = static function (): array {
|
||||
return [
|
||||
'batches' => Db::name('prescription_ai_batch')->count(),
|
||||
'results' => Db::name('prescription_ai_result')->count(),
|
||||
'attempts' => Db::name('prescription_ai_attempt')->count(),
|
||||
'tasks_hash' => hash('sha256', json_encode(Db::name('prescription_ai_task')->field('id,status,attempts,total_attempts,manual_retries,result_id')->order('id')->select()->toArray())),
|
||||
];
|
||||
};
|
||||
$before = $snapshot();
|
||||
$present = isset(Db::name('prescription_ai_task')->getFields()['progress_json']);
|
||||
$sqlFile = $root . 'database/migrations/2026_09_10_prescription_ai_progress.sql';
|
||||
$result = ['time' => date('c'), 'local_database' => true, 'column_previously_present' => $present,
|
||||
'migration_sha256' => hash_file('sha256', $sqlFile), 'applied' => false];
|
||||
if (in_array('--apply', $argv, true)) {
|
||||
Db::execute('SET SESSION lock_wait_timeout = 5');
|
||||
foreach (explode(';', preg_replace('/^--.*$/m', '', file_get_contents($sqlFile))) as $statement) {
|
||||
if (trim($statement) !== '') { Db::execute($statement); }
|
||||
}
|
||||
$columns = Db::query("SHOW COLUMNS FROM `zyt_prescription_ai_task` LIKE 'progress_json'");
|
||||
if (count($columns) !== 1 || $columns[0]['Type'] !== 'varchar(2048)' || $columns[0]['Null'] !== 'YES') {
|
||||
throw new RuntimeException('Unexpected progress column definition');
|
||||
}
|
||||
$result['applied'] = true;
|
||||
$result['business_state_unchanged'] = $before === $snapshot();
|
||||
if (!$result['business_state_unchanged']) { throw new RuntimeException('Business task state changed during upgrade'); }
|
||||
}
|
||||
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), PHP_EOL;
|
||||
} catch (Throwable $e) {
|
||||
echo json_encode(['error' => get_class($e), 'code' => $e->getCode()]), PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"lane": "qwen",
|
||||
"old_pid": 36592,
|
||||
"new_pid": 7512,
|
||||
"started_at": "2026-09-10T09:56:47.4495394+08:00",
|
||||
"policy": "manual-prescription-available-evidence-v2",
|
||||
"historical_tasks_retried": false
|
||||
},
|
||||
{
|
||||
"lane": "openai",
|
||||
"old_pid": 4796,
|
||||
"new_pid": 9292,
|
||||
"started_at": "2026-09-10T09:56:47.8478717+08:00",
|
||||
"policy": "manual-prescription-available-evidence-v2",
|
||||
"historical_tasks_retried": false
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// Read-only comparison diagnostic. Prints dosage units and structural fields only,
|
||||
// never herb names, report text, patient data, URLs or credentials.
|
||||
$serverRoot = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'server' . DIRECTORY_SEPARATOR;
|
||||
chdir($serverRoot);
|
||||
require $serverRoot . 'vendor/autoload.php';
|
||||
$app = new \think\App($serverRoot);
|
||||
$app->initialize();
|
||||
|
||||
use app\common\service\prescriptionai\PrescriptionAiCipher;
|
||||
use app\common\service\prescriptionai\PrescriptionAiPolicy;
|
||||
use think\facade\Db;
|
||||
|
||||
$batchId = (int) ($argv[1] ?? 5);
|
||||
$cipher = new PrescriptionAiCipher();
|
||||
$batch = Db::name('prescription_ai_batch')->where('id', $batchId)->find();
|
||||
$doctor = $cipher->decrypt((string) $batch['prescription_cipher'], 'prescription');
|
||||
$herbs = PrescriptionAiPolicy::decode($doctor['herbs'] ?? []);
|
||||
|
||||
$shape = static function (array $herb): array {
|
||||
return [
|
||||
'has_name' => trim((string) ($herb['name'] ?? '')) !== '',
|
||||
'dosage' => $herb['dosage'] ?? null,
|
||||
'unit' => $herb['unit'] ?? null,
|
||||
'dose_basis' => $herb['dose_basis'] ?? null,
|
||||
'processing' => $herb['processing'] ?? null,
|
||||
'formula_type' => $herb['formula_type'] ?? null,
|
||||
'keys' => array_keys($herb),
|
||||
];
|
||||
};
|
||||
|
||||
$out = ['batch_id' => $batchId, 'doctor' => [
|
||||
'dose_unit' => $doctor['dose_unit'] ?? null,
|
||||
'dose_basis' => $doctor['dose_basis'] ?? null,
|
||||
'prescription_type' => $doctor['prescription_type'] ?? null,
|
||||
'herb_count' => count($herbs),
|
||||
'herbs' => array_map($shape, array_slice($herbs, 0, 5)),
|
||||
]];
|
||||
|
||||
foreach (Db::name('prescription_ai_result')->where('batch_id', $batchId)->select()->toArray() as $row) {
|
||||
$body = $cipher->decrypt((string) $row['body_cipher'], 'result:' . $row['batch_id'] . ':' . $row['model_key']);
|
||||
$candidate = (array) ($body['candidate'] ?? []);
|
||||
$candidateHerbs = (array) ($candidate['herbs'] ?? []);
|
||||
$out['results'][] = [
|
||||
'result_id' => (int) $row['id'], 'model_key' => $row['model_key'],
|
||||
'candidate_status' => $candidate['status'] ?? null,
|
||||
'dose_basis' => $candidate['dose_basis'] ?? null,
|
||||
'prescription_type' => $candidate['prescription_type'] ?? null,
|
||||
'times_per_day' => $candidate['times_per_day'] ?? null,
|
||||
'usage_days' => $candidate['usage_days'] ?? null,
|
||||
'herb_count' => count($candidateHerbs),
|
||||
'herbs' => array_map($shape, array_slice($candidateHerbs, 0, 5)),
|
||||
'comparison_status' => $row['comparison_status'], 'comparison_reason_code' => $row['comparison_reason_code'],
|
||||
];
|
||||
}
|
||||
|
||||
echo json_encode($out, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
$serverRoot = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'server' . DIRECTORY_SEPARATOR;
|
||||
chdir($serverRoot);
|
||||
require $serverRoot . 'vendor/autoload.php';
|
||||
$app = new \think\App($serverRoot);
|
||||
$app->initialize();
|
||||
try {
|
||||
$secret = (string) config('prescription_analysis.encryption_key', '');
|
||||
if ($secret === '') { $secret = trim((string) file_get_contents(root_path('runtime') . 'prescription_ai_private/snapshot.key')); }
|
||||
$cipher = new \app\common\service\prescriptionai\PrescriptionAiCipher($secret);
|
||||
$out = [];
|
||||
foreach (\think\facade\Db::name('prescription_ai_task')->where('batch_id', (int) \think\facade\Db::name('prescription_ai_subject')->where('prescription_id', 7556)->value('latest_batch_id'))->select()->toArray() as $task) {
|
||||
if (empty($task['progress_cipher'])) {
|
||||
$out[] = ['model' => $task['model_key'], 'status' => $task['status'], 'result_id' => $task['result_id']];
|
||||
continue;
|
||||
}
|
||||
$p = $cipher->decrypt($task['progress_cipher'], 'progress:' . $task['id']);
|
||||
$out[] = ['model' => $task['model_key'], 'status' => $task['status'], 'attempts' => $task['attempts'], 'manual_retries' => $task['manual_retries'],
|
||||
'error_code' => $task['error_code'], 'stage' => $p['stage'], 'calls' => $p['usage']['total_calls'],
|
||||
'next_run' => date('c', (int) $task['next_run_at']), 'updated' => date('c', (int) $task['updated_at']),
|
||||
'steps' => array_keys($p['steps']),
|
||||
'recent_calls' => array_map(static fn (array $call): array => array_intersect_key($call, array_flip(['stage', 'latency_ms', 'ok', 'file_count', 'error_code'])), array_slice($p['usage']['calls'], -4))];
|
||||
}
|
||||
echo json_encode(['time' => date('c'), 'tasks' => $out], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), PHP_EOL;
|
||||
} catch (Throwable $e) { echo json_encode(['error' => get_class($e), 'code' => $e->getCode(), 'line' => $e->getLine(), 'file' => basename($e->getFile())]), PHP_EOL; exit(1); }
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
// Read-only API validation; clinical bodies remain in memory and are never printed.
|
||||
$serverRoot = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'server' . DIRECTORY_SEPARATOR;
|
||||
chdir($serverRoot);
|
||||
require $serverRoot . 'vendor/autoload.php';
|
||||
$app = new \think\App($serverRoot);
|
||||
$app->initialize();
|
||||
try {
|
||||
$batch = \think\facade\Db::name('prescription_ai_batch')->where('id', 1)->where('prescription_id', 7556)->find();
|
||||
$actor = (int) $batch['actor_id'];
|
||||
$info = \app\common\service\prescriptionai\PrescriptionAiAccess::actor($actor);
|
||||
$detail = \app\adminapi\logic\tcm\PrescriptionAiLogic::detail(1, $actor, $info);
|
||||
$models = [];
|
||||
foreach ($detail['models'] ?? [] as $key => $model) {
|
||||
$coverage = $model['coverage'] ?? [];
|
||||
$fileStatuses = [];
|
||||
$fileReasons = [];
|
||||
foreach ($coverage['files'] ?? [] as $file) {
|
||||
$status = (string) ($file['status'] ?? 'unknown');
|
||||
$fileStatuses[$status] = ($fileStatuses[$status] ?? 0) + 1;
|
||||
$reason = (string) ($file['reason'] ?? '');
|
||||
if ($reason !== '') { $fileReasons[$reason] = ($fileReasons[$reason] ?? 0) + 1; }
|
||||
}
|
||||
$models[$key] = ['status' => $model['status'] ?? null,
|
||||
'report_id' => $model['report_id'] ?? null, 'has_report' => !empty($model['report']),
|
||||
'candidate_status' => $model['candidate']['status'] ?? null,
|
||||
'coverage_complete' => $coverage['complete'] ?? null,
|
||||
'files' => $fileStatuses, 'file_reasons' => $fileReasons,
|
||||
'comparison_status' => $model['comparison']['status'] ?? null,
|
||||
'error_code' => $model['error_code'] ?? null];
|
||||
}
|
||||
$clinicalMissing = array_values(array_map(static fn (array $gap): string => (string) $gap['source_id'],
|
||||
array_filter($detail['missing'] ?? [], static fn (array $gap): bool => ($gap['code'] ?? '') === 'CRITICAL_CLINICAL_FACT_MISSING')));
|
||||
echo json_encode(['time' => date('c'), 'batch_id' => $detail['id'] ?? null, 'status' => $detail['status'] ?? null,
|
||||
'clinical_missing_fields' => $clinicalMissing, 'models' => $models], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), PHP_EOL;
|
||||
} catch (Throwable $e) {
|
||||
echo json_encode(['error' => get_class($e), 'code' => $e->getCode()]), PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require __DIR__ . '/check_queue.php';
|
||||
|
||||
use app\common\service\prescriptionai\PrescriptionAiCipher;
|
||||
use app\common\service\prescriptionai\PrescriptionAiGenerator;
|
||||
use think\facade\Db;
|
||||
|
||||
$secret = (string) config('prescription_analysis.encryption_key', '');
|
||||
if ($secret === '') {
|
||||
$secret = trim((string) file_get_contents(root_path('runtime') . 'prescription_ai_private/snapshot.key'));
|
||||
}
|
||||
$cipher = new PrescriptionAiCipher($secret);
|
||||
$batch = Db::name('prescription_ai_batch')->where('prescription_id', 7556)->order('id', 'desc')->find();
|
||||
$context = $cipher->decrypt((string) $batch['context_cipher'], 'context');
|
||||
$budget = max(6000, min(200000, (int) config('prescription_ai.manual_analysis.input_token_budget', 24000))) - 3500;
|
||||
$units = (new ReflectionMethod(PrescriptionAiGenerator::class, 'sourceUnits'))->invoke(null, $context['source']['records'], $budget);
|
||||
$chunks = (new ReflectionMethod(PrescriptionAiGenerator::class, 'pack'))->invoke(null, $units, $budget);
|
||||
$textPromptBytes = [];
|
||||
foreach ($chunks as $chunk) {
|
||||
$ids = array_values(array_unique(array_column($chunk, 'source_id')));
|
||||
$prompt = (new ReflectionMethod(PrescriptionAiGenerator::class, 'evidencePrompt'))->invoke(null, 'text', $ids,
|
||||
['patient' => $context['source']['patient'] ?? [], 'clinical_field_semantics' => $context['source']['clinical_field_semantics'] ?? [], 'records' => $chunk]);
|
||||
$textPromptBytes[] = strlen($prompt);
|
||||
}
|
||||
$attachmentStatuses = array_count_values(array_column($context['files'] ?? [], 'status'));
|
||||
$result = [];
|
||||
foreach (Db::name('prescription_ai_task')->where('batch_id', $batch['id'])->select()->toArray() as $task) {
|
||||
if (empty($task['progress_cipher'])) { continue; }
|
||||
$progress = $cipher->decrypt($task['progress_cipher'], 'progress:' . $task['id']);
|
||||
$item = ['model' => $task['model_key'], 'stage' => $progress['stage'], 'calls' => $progress['usage']['total_calls'], 'steps' => []];
|
||||
foreach ($progress['steps'] ?? [] as $key => $step) {
|
||||
$content = (string) ($step['value']['content'] ?? '');
|
||||
$decoded = json_decode(trim($content), true);
|
||||
$jsonError = json_last_error_msg();
|
||||
$fenced = preg_match('/\A```(?:json)?\s*(.*?)\s*```\z/s', trim($content), $match) === 1;
|
||||
$wrapped = $fenced ? json_decode($match[1], true) : null;
|
||||
$summary = ['step' => $key, 'bytes' => strlen($content), 'json_error' => $jsonError,
|
||||
'has_think_tag' => str_contains($content, '<think>'), 'whole_json_fence' => $fenced,
|
||||
'json_keys' => is_array($decoded) ? array_keys($decoded) : null,
|
||||
'fenced_json_keys' => is_array($wrapped) ? array_keys($wrapped) : null];
|
||||
if (str_starts_with($key, 'text:')) {
|
||||
$expected = array_values(array_unique(array_column($chunks[(int) substr($key, 5)], 'source_id')));
|
||||
$summary['expected_source_count'] = count($expected);
|
||||
$summary['raw_valid'] = (new ReflectionMethod(PrescriptionAiGenerator::class, 'parseEvidence'))->invoke(null, $content, $expected) !== null;
|
||||
$summary['fenced_valid'] = $fenced && (new ReflectionMethod(PrescriptionAiGenerator::class, 'parseEvidence'))->invoke(null, $match[1], $expected) !== null;
|
||||
$value = is_array($decoded) ? $decoded : (is_array($wrapped) ? $wrapped : []);
|
||||
$summary['field_types'] = array_map('get_debug_type', $value);
|
||||
if (is_array($value['covered_source_ids'] ?? null)) {
|
||||
$summary['covered_source_count'] = count($value['covered_source_ids']);
|
||||
$summary['missing_source_count'] = count(array_diff($expected, $value['covered_source_ids']));
|
||||
$summary['extra_source_count'] = count(array_diff($value['covered_source_ids'], $expected));
|
||||
}
|
||||
}
|
||||
if (str_starts_with($key, 'files:')) {
|
||||
$sendable = array_values(array_filter($context['files'], static fn (array $f): bool => $f['status'] !== 'restricted' && !empty($f['url'])));
|
||||
$fileBatch = array_chunk($sendable, 3)[(int) substr($key, 6)];
|
||||
$known = array_values(array_unique(array_merge(array_column($context['source']['records'], 'source_id'), array_column($context['files'], 'file_id'))));
|
||||
$value = (new ReflectionMethod(PrescriptionAiGenerator::class, 'object'))->invoke(null, $content);
|
||||
$summary['file_output_valid'] = (new ReflectionMethod(PrescriptionAiGenerator::class, 'parseFiles'))->invoke(null, $content, $fileBatch, $known) !== null;
|
||||
$summary['returned_count'] = is_array($value['files'] ?? null) ? count($value['files']) : null;
|
||||
$summary['expected_count'] = count($fileBatch);
|
||||
$summary['file_shapes'] = [];
|
||||
foreach ($value['files'] ?? [] as $file) {
|
||||
$refs = $file['evidence_references'] ?? null;
|
||||
$summary['file_shapes'][] = ['keys' => array_keys($file), 'types' => array_map('get_debug_type', $file),
|
||||
'status' => in_array($file['status'] ?? '', ['processed', 'unreadable', 'unsupported'], true) ? $file['status'] : 'INVALID',
|
||||
'expected_id' => in_array($file['file_id'] ?? null, array_column($fileBatch, 'file_id'), true),
|
||||
'findings_bytes' => is_string($file['findings'] ?? null) ? strlen($file['findings']) : null,
|
||||
'refs_count' => is_array($refs) ? count($refs) : null,
|
||||
'unknown_refs_count' => is_array($refs) && count(array_filter($refs, 'is_string')) === count($refs) ? count(array_diff($refs, $known)) : null];
|
||||
}
|
||||
}
|
||||
$item['steps'][] = $summary;
|
||||
}
|
||||
$result[] = $item;
|
||||
}
|
||||
echo json_encode(['text_chunks' => count($chunks), 'text_prompt_bytes' => $textPromptBytes, 'input_budget' => $budget + 3500,
|
||||
'attachments' => count($context['files'] ?? []), 'attachment_statuses' => $attachmentStatuses,
|
||||
'progress_shape_only' => $result], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), PHP_EOL;
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
// Read-only: exercise actual permission-aware endpoints, print only public progress metadata.
|
||||
$root = dirname(__DIR__, 2) . '/server/';
|
||||
chdir($root);
|
||||
require $root . 'vendor/autoload.php';
|
||||
$app = new \think\App($root);
|
||||
$app->initialize();
|
||||
use think\facade\Db;
|
||||
use app\adminapi\logic\tcm\PrescriptionAiLogic as Api;
|
||||
use app\common\service\prescriptionai\PrescriptionAiAccess as Access;
|
||||
try {
|
||||
$batchId = (int) Db::name('prescription_ai_subject')->where('prescription_id', 7556)->value('latest_batch_id');
|
||||
$batch = Db::name('prescription_ai_batch')->where('id', $batchId)->find();
|
||||
$actor = (int) $batch['actor_id'];
|
||||
$info = Access::actor($actor);
|
||||
$started = microtime(true);
|
||||
$statuses = Api::statuses([7556], $actor, $info);
|
||||
$statusMs = round((microtime(true) - $started) * 1000, 1);
|
||||
$started = microtime(true);
|
||||
$detail = Api::detail($batchId, $actor, $info);
|
||||
$detailMs = round((microtime(true) - $started) * 1000, 1);
|
||||
$models = [];
|
||||
foreach ($detail['models'] ?? [] as $key => $model) {
|
||||
$models[$key] = ['status' => $model['status'], 'error_code' => $model['error_code'], 'progress' => $model['progress'] ?? null];
|
||||
}
|
||||
$out = ['time' => date('c'), 'batch_id' => $batchId, 'batch_status' => $detail['status'], 'models' => $models,
|
||||
'list_has_progress' => isset($statuses['items'][0]['progress']),
|
||||
'list_milliseconds' => $statusMs, 'detail_milliseconds' => $detailMs];
|
||||
echo json_encode($out, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), PHP_EOL;
|
||||
} catch (Throwable $e) {
|
||||
echo json_encode(['error' => get_class($e), 'code' => $e->getCode()]), PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
// Read-only provider parameters. Does not submit patient data or generate a report.
|
||||
$serverRoot = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'server' . DIRECTORY_SEPARATOR;
|
||||
chdir($serverRoot);
|
||||
require $serverRoot . 'vendor/autoload.php';
|
||||
$app = new \think\App($serverRoot);
|
||||
$app->initialize();
|
||||
$cfg = (array) config('prescription_ai');
|
||||
$endpoint = (new ReflectionMethod(\app\common\service\DifyChatService::class, 'buildEndpoint'))->invoke(null, $cfg['base_url'], 'parameters');
|
||||
$out = [];
|
||||
foreach (['qwen', 'openai'] as $model) {
|
||||
$curl = curl_init($endpoint);
|
||||
curl_setopt_array($curl, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $cfg['models'][$model]['api_key']]]);
|
||||
$body = curl_exec($curl);
|
||||
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
$decoded = is_string($body) ? json_decode($body, true) : [];
|
||||
$files = (array) ($decoded['file_upload'] ?? []);
|
||||
$image = (array) ($files['image'] ?? []);
|
||||
$out[$model] = ['http_code' => $status, 'curl_errno' => curl_errno($curl),
|
||||
'files' => array_intersect_key($files, array_flip(['enabled', 'number_limits', 'allowed_file_types', 'allowed_file_extensions', 'allowed_file_upload_methods'])),
|
||||
'image' => array_intersect_key($image, array_flip(['enabled', 'number_limits', 'transfer_methods']))];
|
||||
curl_close($curl);
|
||||
}
|
||||
echo json_encode($out, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), PHP_EOL;
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// Read-only queue metadata diagnostic. Never print credentials or clinical content.
|
||||
$serverRoot = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'server' . DIRECTORY_SEPARATOR;
|
||||
chdir($serverRoot);
|
||||
require $serverRoot . 'vendor/autoload.php';
|
||||
$app = new \think\App($serverRoot);
|
||||
$app->initialize();
|
||||
|
||||
try {
|
||||
$out = [
|
||||
'php_now' => time(), 'php_local_time' => date('c'),
|
||||
'enabled' => (bool) config('prescription_analysis.enabled', false),
|
||||
'start_at' => (int) config('prescription_analysis.start_at', 0),
|
||||
'configured_key_present' => (string) config('prescription_analysis.encryption_key', '') !== '',
|
||||
'local_key_present' => is_file(root_path('runtime') . 'prescription_ai_private/snapshot.key'),
|
||||
'db_now' => \think\facade\Db::query('SELECT UNIX_TIMESTAMP() AS now')[0]['now'],
|
||||
];
|
||||
$fields = 'id,prescription_id,status,validity,trigger_type,wait_until,next_run_at,lock_until,prepare_attempts,error_code,cutoff_at,created_at,updated_at';
|
||||
$out['prescription_batches'] = \think\facade\Db::name('prescription_ai_batch')
|
||||
->where('prescription_id', 7556)->field($fields)->order('id', 'desc')->limit(5)->select()->toArray();
|
||||
$ids = array_column($out['prescription_batches'], 'id');
|
||||
$out['prescription_tasks'] = $ids ? \think\facade\Db::name('prescription_ai_task')->whereIn('batch_id', $ids)
|
||||
->field('id,batch_id,model_key,status,attempts,total_attempts,next_run_at,lock_until,error_code,result_id,started_at,finished_at,updated_at')
|
||||
->select()->toArray() : [];
|
||||
$out['batch_counts'] = \think\facade\Db::name('prescription_ai_batch')
|
||||
->field('status,COUNT(*) AS total,MIN(id) AS first_id,MAX(id) AS last_id')->group('status')->select()->toArray();
|
||||
$out['model_counts'] = \think\facade\Db::name('prescription_ai_task')
|
||||
->field('model_key,status,COUNT(*) AS total')->group('model_key,status')->select()->toArray();
|
||||
$out['active_preparation'] = \think\facade\Db::name('prescription_ai_batch')
|
||||
->where('lock_until', '>', time())->field($fields)->limit(10)->select()->toArray();
|
||||
echo json_encode($out, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), PHP_EOL;
|
||||
} catch (\Throwable $error) {
|
||||
echo json_encode(['diagnostic_error' => get_class($error), 'code' => $error->getCode()]), PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require __DIR__ . '/check_queue.php';
|
||||
$secret = (string) config('prescription_analysis.encryption_key', '');
|
||||
if ($secret === '') {
|
||||
$secret = trim((string) file_get_contents(root_path('runtime') . 'prescription_ai_private/snapshot.key'));
|
||||
}
|
||||
$cipher = new \app\common\service\prescriptionai\PrescriptionAiCipher($secret);
|
||||
$batch = \think\facade\Db::name('prescription_ai_batch')->where('id', 1)->where('prescription_id', 7556)->find();
|
||||
$context = $cipher->decrypt($batch['context_cipher'], 'context');
|
||||
$types = $hosts = $missing = [];
|
||||
$sendable = [];
|
||||
foreach ($context['files'] as $file) {
|
||||
$type = (string) ($file['type'] ?? 'unknown');
|
||||
$types[$type] = ($types[$type] ?? 0) + 1;
|
||||
$host = (string) (parse_url((string) ($file['url'] ?? ''), PHP_URL_HOST) ?: '(none)');
|
||||
$hosts[$host] = ($hosts[$host] ?? 0) + 1;
|
||||
if ($file['status'] !== 'restricted' && !empty($file['url'])) { $sendable[] = $file; }
|
||||
}
|
||||
$normalization = [];
|
||||
foreach (array_chunk($sendable, 3) as $files) {
|
||||
$wire = array_map(static fn (array $f): array => ['type' => $f['type'], 'url' => $f['url'], 'transfer_method' => 'remote_url'], $files);
|
||||
$value = (new ReflectionMethod(\app\common\service\DifyChatService::class, 'normalizeFiles'))->invoke(null, $wire, 3);
|
||||
$normalization[] = ['input' => count($wire), 'kept' => count($value['kept']), 'over_limit' => count($value['dropped'])];
|
||||
}
|
||||
foreach ($context['missing'] ?? [] as $item) {
|
||||
$code = (string) ($item['code'] ?? 'unknown');
|
||||
$missing[$code] = ($missing[$code] ?? 0) + 1;
|
||||
}
|
||||
echo json_encode(['source_shape_only' => [
|
||||
'attachment_types' => $types, 'attachment_hosts' => $hosts, 'missing_codes' => $missing,
|
||||
'attachment_batch_normalization' => $normalization,
|
||||
'max_files' => config('prescription_ai.max_files'), 'timeout' => config('prescription_ai.timeout'),
|
||||
]], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), PHP_EOL;
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
$serverRoot = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'server' . DIRECTORY_SEPARATOR;
|
||||
chdir($serverRoot);
|
||||
require $serverRoot . 'vendor/autoload.php';
|
||||
$app = new \think\App($serverRoot);
|
||||
$app->initialize();
|
||||
try {
|
||||
$secret = (string) config('prescription_analysis.encryption_key', '');
|
||||
if ($secret === '') { $secret = trim((string) file_get_contents(root_path('runtime') . 'prescription_ai_private/snapshot.key')); }
|
||||
$cipher = new \app\common\service\prescriptionai\PrescriptionAiCipher($secret);
|
||||
$out = [];
|
||||
foreach (\think\facade\Db::name('prescription_ai_task')->where('batch_id', 1)->select()->toArray() as $task) {
|
||||
if (empty($task['progress_cipher'])) {
|
||||
$out[] = ['model' => $task['model_key'], 'status' => $task['status'], 'result_id' => $task['result_id']];
|
||||
continue;
|
||||
}
|
||||
$p = $cipher->decrypt($task['progress_cipher'], 'progress:' . $task['id']);
|
||||
$out[] = ['model' => $task['model_key'], 'status' => $task['status'], 'attempts' => $task['attempts'], 'manual_retries' => $task['manual_retries'],
|
||||
'error_code' => $task['error_code'], 'stage' => $p['stage'], 'calls' => $p['usage']['total_calls'],
|
||||
'next_run' => date('c', (int) $task['next_run_at']), 'updated' => date('c', (int) $task['updated_at']),
|
||||
'steps' => array_keys($p['steps']),
|
||||
'recent_calls' => array_map(static fn (array $call): array => array_intersect_key($call, array_flip(['stage', 'latency_ms', 'ok', 'file_count', 'error_code'])), array_slice($p['usage']['calls'], -4))];
|
||||
}
|
||||
echo json_encode(['time' => date('c'), 'tasks' => $out], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), PHP_EOL;
|
||||
} catch (Throwable $e) { echo json_encode(['error' => get_class($e), 'code' => $e->getCode(), 'line' => $e->getLine(), 'file' => basename($e->getFile())]), PHP_EOL; exit(1); }
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// Read-only diagnostic for preparation wait and model timing. Metadata and counts only;
|
||||
// never prints clinical content, attachment URLs, prompts or credentials.
|
||||
$serverRoot = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'server' . DIRECTORY_SEPARATOR;
|
||||
chdir($serverRoot);
|
||||
require $serverRoot . 'vendor/autoload.php';
|
||||
$app = new \think\App($serverRoot);
|
||||
$app->initialize();
|
||||
|
||||
use think\facade\Db;
|
||||
|
||||
$rxId = (int) ($argv[1] ?? 7556);
|
||||
$out = ['now' => time(), 'prescription_id' => $rxId];
|
||||
|
||||
$rx = Db::name('tcm_prescription')->where('id', $rxId)->field('id,diagnosis_id,appointment_id,patient_id,is_system_auto,update_time')->find();
|
||||
$out['prescription'] = $rx;
|
||||
$diagnosisId = (int) ($rx['diagnosis_id'] ?? 0);
|
||||
|
||||
$out['call_records'] = Db::name('tcm_call_record')->where('diagnosis_id', $diagnosisId)
|
||||
->field('id,diagnosis_id,status,transcription_status,transcription_session_id,transcription_segment_count,start_time,end_time,create_time,update_time,transcription_started_at,transcription_finished_at')
|
||||
->order('id', 'desc')->limit(10)->select()->toArray();
|
||||
foreach ($out['call_records'] as &$call) {
|
||||
$call['segment_total'] = (int) Db::name('tcm_call_transcript_segment')->where('call_record_id', $call['id'])->count();
|
||||
$call['segment_current_session'] = (string) $call['transcription_session_id'] !== ''
|
||||
? (int) Db::name('tcm_call_transcript_segment')->where('call_record_id', $call['id'])
|
||||
->where('transcription_session_id', $call['transcription_session_id'])->count()
|
||||
: 0;
|
||||
}
|
||||
unset($call);
|
||||
|
||||
$batches = Db::name('prescription_ai_batch')->where('prescription_id', $rxId)
|
||||
->field('id,status,validity,cutoff_at,decision_at,wait_until,next_run_at,created_at,updated_at,coverage_status,source_summary_json,missing_json,error_code')
|
||||
->order('id', 'desc')->limit(5)->select()->toArray();
|
||||
foreach ($batches as &$batch) {
|
||||
$batch['prepare_seconds'] = (int) $batch['cutoff_at'] > 0 ? (int) $batch['cutoff_at'] - (int) $batch['created_at'] : null;
|
||||
$batch['missing_codes'] = array_count_values(array_column(json_decode((string) $batch['missing_json'], true) ?: [], 'code'));
|
||||
unset($batch['missing_json']);
|
||||
}
|
||||
unset($batch);
|
||||
$out['batches'] = $batches;
|
||||
|
||||
$ids = array_column($batches, 'id');
|
||||
$out['tasks'] = $ids ? Db::name('prescription_ai_task')->whereIn('batch_id', $ids)
|
||||
->field('id,batch_id,model_key,status,attempts,total_attempts,error_code,started_at,finished_at,updated_at')
|
||||
->order('id')->select()->toArray() : [];
|
||||
foreach ($out['tasks'] as &$task) {
|
||||
$task['run_seconds'] = (int) $task['finished_at'] > 0 && (int) $task['started_at'] > 0
|
||||
? (int) $task['finished_at'] - (int) $task['started_at'] : null;
|
||||
}
|
||||
unset($task);
|
||||
|
||||
$taskIds = array_column($out['tasks'], 'id');
|
||||
$out['attempts'] = $taskIds ? Db::name('prescription_ai_attempt')->whereIn('task_id', $taskIds)
|
||||
->field('id,task_id,attempt_no,status,error_code,started_at,finished_at')->order('id')->limit(40)->select()->toArray() : [];
|
||||
|
||||
echo json_encode($out, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
|
||||
@@ -0,0 +1,42 @@
|
||||
........................................................................ [ 5%]
|
||||
........................................................................ [ 11%]
|
||||
.....................................................................F.. [ 16%]
|
||||
........................................................................ [ 22%]
|
||||
...................................Windows fatal exception: code 0xc0000374
|
||||
|
||||
Thread 0x00006580 (most recent call first):
|
||||
File "D:\web\zyt\app\tests\test_diagnosis_drawer_visual.py", line 610 in _open_dialog
|
||||
File "D:\web\zyt\app\tests\test_diagnosis_drawer_visual.py", line 643 in test_readonly_scroll_keeps_close_controls_reachable
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\_pytest\python.py", line 157 in pytest_pyfunc_call
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\pluggy\_callers.py", line 121 in _multicall
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\pluggy\_manager.py", line 120 in _hookexec
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\pluggy\_hooks.py", line 512 in __call__
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\_pytest\python.py", line 1671 in runtest
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\_pytest\runner.py", line 178 in pytest_runtest_call
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\pluggy\_callers.py", line 121 in _multicall
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\pluggy\_manager.py", line 120 in _hookexec
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\pluggy\_hooks.py", line 512 in __call__
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\_pytest\runner.py", line 246 in <lambda>
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\_pytest\runner.py", line 344 in from_call
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\_pytest\runner.py", line 245 in call_and_report
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\_pytest\runner.py", line 136 in runtestprotocol
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\_pytest\runner.py", line 117 in pytest_runtest_protocol
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\pluggy\_callers.py", line 121 in _multicall
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\pluggy\_manager.py", line 120 in _hookexec
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\pluggy\_hooks.py", line 512 in __call__
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\_pytest\main.py", line 367 in pytest_runtestloop
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\pluggy\_callers.py", line 121 in _multicall
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\pluggy\_manager.py", line 120 in _hookexec
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\pluggy\_hooks.py", line 512 in __call__
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\_pytest\main.py", line 343 in _main
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\_pytest\main.py", line 289 in wrap_session
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\_pytest\main.py", line 336 in pytest_cmdline_main
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\pluggy\_callers.py", line 121 in _multicall
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\pluggy\_manager.py", line 120 in _hookexec
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\pluggy\_hooks.py", line 512 in __call__
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\_pytest\config\__init__.py", line 175 in main
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\_pytest\config\__init__.py", line 201 in console_main
|
||||
File "D:\web\zyt\app\.venv\Lib\site-packages\pytest\__main__.py", line 9 in <module>
|
||||
File "<frozen runpy>", line 88 in _run_code
|
||||
File "<frozen runpy>", line 198 in _run_module_as_main
|
||||
.
|
||||
@@ -0,0 +1,35 @@
|
||||
[
|
||||
{
|
||||
"lane": "prepare",
|
||||
"old_pids": [
|
||||
25796
|
||||
],
|
||||
"new_pid": 16964,
|
||||
"started_at": "2026-09-10T11:55:23.8552926+08:00",
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\prepare-fixes-20260910-115522.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\prepare-fixes-20260910-115522.stderr.log",
|
||||
"loaded": "transcript-wait-fix, request_timeout=240s, one format repair per stage"
|
||||
},
|
||||
{
|
||||
"lane": "qwen",
|
||||
"old_pids": [
|
||||
47796
|
||||
],
|
||||
"new_pid": 40500,
|
||||
"started_at": "2026-09-10T11:55:24.5947659+08:00",
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-fixes-20260910-115522.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-fixes-20260910-115522.stderr.log",
|
||||
"loaded": "transcript-wait-fix, request_timeout=240s, one format repair per stage"
|
||||
},
|
||||
{
|
||||
"lane": "openai",
|
||||
"old_pids": [
|
||||
19152
|
||||
],
|
||||
"new_pid": 33772,
|
||||
"started_at": "2026-09-10T11:55:25.3312733+08:00",
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\openai-fixes-20260910-115522.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\openai-fixes-20260910-115522.stderr.log",
|
||||
"loaded": "transcript-wait-fix, request_timeout=240s, one format repair per stage"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
[
|
||||
{
|
||||
"lane": "prepare",
|
||||
"old_pids": [
|
||||
16964
|
||||
],
|
||||
"new_pid": 19548,
|
||||
"started_at": "2026-09-10T12:24:55.5424202+08:00",
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\prepare-fixes-20260910-122454.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\prepare-fixes-20260910-122454.stderr.log",
|
||||
"loaded": "transcript-wait-fix, request_timeout=240s, one format repair per stage"
|
||||
},
|
||||
{
|
||||
"lane": "qwen",
|
||||
"old_pids": [
|
||||
40500
|
||||
],
|
||||
"new_pid": 5836,
|
||||
"started_at": "2026-09-10T12:24:56.2231420+08:00",
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-fixes-20260910-122454.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-fixes-20260910-122454.stderr.log",
|
||||
"loaded": "transcript-wait-fix, request_timeout=240s, one format repair per stage"
|
||||
},
|
||||
{
|
||||
"lane": "openai",
|
||||
"old_pids": [
|
||||
33772
|
||||
],
|
||||
"new_pid": 25176,
|
||||
"started_at": "2026-09-10T12:24:56.9236012+08:00",
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\openai-fixes-20260910-122454.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\openai-fixes-20260910-122454.stderr.log",
|
||||
"loaded": "transcript-wait-fix, request_timeout=240s, one format repair per stage"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
[
|
||||
{
|
||||
"lane": "prepare",
|
||||
"old_pids": [
|
||||
19548
|
||||
],
|
||||
"new_pid": 22872,
|
||||
"started_at": "2026-09-10T13:22:16.1041976+08:00",
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\prepare-fixes-20260910-132215.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\prepare-fixes-20260910-132215.stderr.log",
|
||||
"loaded": "transcript-wait-fix, request_timeout=240s, one format repair per stage"
|
||||
},
|
||||
{
|
||||
"lane": "qwen",
|
||||
"old_pids": [
|
||||
5836
|
||||
],
|
||||
"new_pid": 6872,
|
||||
"started_at": "2026-09-10T13:22:16.7639660+08:00",
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-fixes-20260910-132215.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-fixes-20260910-132215.stderr.log",
|
||||
"loaded": "transcript-wait-fix, request_timeout=240s, one format repair per stage"
|
||||
},
|
||||
{
|
||||
"lane": "openai",
|
||||
"old_pids": [
|
||||
36332
|
||||
],
|
||||
"new_pid": 33836,
|
||||
"started_at": "2026-09-10T13:22:17.4568700+08:00",
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\openai-fixes-20260910-132215.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\openai-fixes-20260910-132215.stderr.log",
|
||||
"loaded": "transcript-wait-fix, request_timeout=240s, one format repair per stage"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
$root = dirname(__DIR__, 2) . '/server/';
|
||||
chdir($root);
|
||||
require $root . 'vendor/autoload.php';
|
||||
$app = new \think\App($root);
|
||||
$app->initialize();
|
||||
try {
|
||||
$connection = (string) config('database.default');
|
||||
$cfg = (array) config('database.connections.' . $connection);
|
||||
$safe = [
|
||||
'local_database' => in_array((string) ($cfg['hostname'] ?? ''), ['127.0.0.1', 'localhost', '::1'], true),
|
||||
'table_prefix_valid' => preg_match('/^[a-zA-Z0-9_]*$/D', (string) ($cfg['prefix'] ?? '')) === 1,
|
||||
'progress_column_present' => isset(\think\facade\Db::name('prescription_ai_task')->getFields()['progress_json']),
|
||||
'running_models' => \think\facade\Db::name('prescription_ai_task')->where('status', 'running')->field('model_key,COUNT(*) AS count')->group('model_key')->select()->toArray(),
|
||||
'active_preparation_count' => \think\facade\Db::name('prescription_ai_batch')->where('lock_until', '>', time())->count(),
|
||||
'active_batch_count' => \think\facade\Db::name('prescription_ai_batch')->where('validity', 'current')->whereIn('status', ['preparing', 'waiting_sources', 'queued', 'running', 'retry_wait'])->count(),
|
||||
'active_task_count' => \think\facade\Db::name('prescription_ai_task')->whereIn('status', ['queued', 'running', 'retry_wait'])->count(),
|
||||
];
|
||||
echo json_encode($safe, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), PHP_EOL;
|
||||
} catch (\Throwable $e) {
|
||||
echo json_encode(['error' => get_class($e)]), PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// Row-level comparison diagnostic: prints normalized identity keys and match types so a
|
||||
// scoring mismatch can be traced. Herb identities only; no patient data or report text.
|
||||
$serverRoot = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . 'server' . DIRECTORY_SEPARATOR;
|
||||
chdir($serverRoot);
|
||||
require $serverRoot . 'vendor/autoload.php';
|
||||
$app = new \think\App($serverRoot);
|
||||
$app->initialize();
|
||||
|
||||
use app\common\model\doctor\Medicine;
|
||||
use app\common\service\prescriptionai\PrescriptionAiCipher;
|
||||
use app\common\service\prescriptionai\PrescriptionAiComparison;
|
||||
use app\common\service\prescriptionai\PrescriptionAiPolicy;
|
||||
use think\facade\Db;
|
||||
|
||||
$batchId = (int) ($argv[1] ?? 6);
|
||||
$model = (string) ($argv[2] ?? 'qwen');
|
||||
$cipher = new PrescriptionAiCipher();
|
||||
$batch = Db::name('prescription_ai_batch')->where('id', $batchId)->find();
|
||||
$doctor = $cipher->decrypt((string) $batch['prescription_cipher'], 'prescription');
|
||||
$doctor['herbs'] = PrescriptionAiPolicy::decode($doctor['herbs'] ?? []);
|
||||
$doctor['aux_usage'] = PrescriptionAiPolicy::decode($doctor['aux_usage'] ?? []);
|
||||
$row = Db::name('prescription_ai_result')->where('batch_id', $batchId)->where('model_key', $model)->find();
|
||||
$body = $cipher->decrypt((string) $row['body_cipher'], 'result:' . $row['batch_id'] . ':' . $row['model_key']);
|
||||
$catalog = Medicine::where('status', 1)->whereNull('delete_time')->field(['id', 'name', 'unit'])->order('id')->select()->toArray();
|
||||
$comparison = PrescriptionAiComparison::compare($doctor, (array) ($body['candidate'] ?? []), $catalog);
|
||||
|
||||
$rows = array_map(static fn (array $r): array => [
|
||||
'key' => $r['key'], 'name' => $r['name'], 'role' => $r['formula_type'], 'match' => $r['match_type'],
|
||||
'doctor' => $r['doctor_dosage'], 'candidate' => $r['candidate_dosage'], 'contribution' => $r['contribution'],
|
||||
], $comparison['rows']);
|
||||
echo json_encode([
|
||||
'batch' => $batchId, 'model' => $model, 'score' => $comparison['score'], 'herb_score' => $comparison['herb_score'],
|
||||
'doctor_count' => $comparison['doctor_count'], 'candidate_count' => $comparison['candidate_count'],
|
||||
'matched' => $comparison['matched_count'],
|
||||
'doctor_defaults' => count($comparison['normalization']['doctor']['defaults']),
|
||||
'doctor_merges' => $comparison['normalization']['doctor']['merges'],
|
||||
'rows' => $rows,
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
|
||||
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"lane": "qwen",
|
||||
"old_pid": 22252,
|
||||
"new_pid": 37984,
|
||||
"started_at": "2026-09-10T09:33:31.3573771+08:00",
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-20260910-093330.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-20260910-093330.stderr.log"
|
||||
},
|
||||
{
|
||||
"lane": "openai",
|
||||
"old_pid": 22300,
|
||||
"new_pid": 43480,
|
||||
"started_at": "2026-09-10T09:33:31.7827284+08:00",
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\openai-20260910-093330.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\openai-20260910-093330.stderr.log"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"lane": "qwen",
|
||||
"old_pid": 37984,
|
||||
"new_pid": 36592,
|
||||
"started_at": "2026-09-10T09:39:54.9335396+08:00",
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-20260910-093954.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-20260910-093954.stderr.log"
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[40001]
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
@@ -0,0 +1 @@
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
@@ -0,0 +1,172 @@
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
@@ -0,0 +1,2 @@
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
@@ -0,0 +1,172 @@
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
@@ -0,0 +1,172 @@
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836 SQLSTATE[HY000] [2002]
|
||||
@@ -0,0 +1,558 @@
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
PRESCRIPTION_AI storage_or_configuration_error think\db\exception\PDOException@PDOConnection.php:836
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"lane": "openai",
|
||||
"old_pid": 43480,
|
||||
"new_pid": 4796,
|
||||
"started_at": "2026-09-10T09:42:12.6466672+08:00",
|
||||
"reason": "Load validated cache and attachment batching fixes; no task retried"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
@@ -0,0 +1,830 @@
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
PRESCRIPTION_AI storage_or_configuration_error
|
||||
@@ -0,0 +1,3 @@
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
@@ -0,0 +1,3 @@
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
@@ -0,0 +1,3 @@
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
@@ -0,0 +1,2 @@
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
@@ -0,0 +1,3 @@
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
PRESCRIPTION_AI {"lane":"openai","enabled":true,"processed":true}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"time": "2026-09-10T09:42:39+08:00",
|
||||
"batch_id": 1,
|
||||
"status": "partial",
|
||||
"models": {
|
||||
"openai": {
|
||||
"status": "success",
|
||||
"report_id": 1,
|
||||
"has_report": true,
|
||||
"candidate_status": "insufficient_data",
|
||||
"coverage_complete": false,
|
||||
"files": {
|
||||
"restricted": 2,
|
||||
"unsupported": 20
|
||||
},
|
||||
"file_reasons": {
|
||||
"FILE_UNAVAILABLE_OR_UNSUPPORTED": 2,
|
||||
"UPSTREAM_REJECTED": 20
|
||||
},
|
||||
"comparison_status": "not_comparable",
|
||||
"error_code": ""
|
||||
},
|
||||
"qwen": {
|
||||
"status": "failed",
|
||||
"report_id": 0,
|
||||
"has_report": false,
|
||||
"candidate_status": null,
|
||||
"coverage_complete": null,
|
||||
"files": [],
|
||||
"file_reasons": [],
|
||||
"comparison_status": null,
|
||||
"error_code": "INVALID_FILE_EVIDENCE_OUTPUT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"lane": "prepare",
|
||||
"instance": 1,
|
||||
"pid": 23076,
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\prepare-1-parallel-20260910-144827.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\prepare-1-parallel-20260910-144827.stderr.log"
|
||||
},
|
||||
{
|
||||
"lane": "qwen",
|
||||
"instance": 1,
|
||||
"pid": 19592,
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-1-parallel-20260910-144827.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-1-parallel-20260910-144827.stderr.log"
|
||||
},
|
||||
{
|
||||
"lane": "qwen",
|
||||
"instance": 2,
|
||||
"pid": 24824,
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-2-parallel-20260910-144827.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-2-parallel-20260910-144827.stderr.log"
|
||||
},
|
||||
{
|
||||
"lane": "openai",
|
||||
"instance": 1,
|
||||
"pid": 19924,
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\openai-1-parallel-20260910-144827.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\openai-1-parallel-20260910-144827.stderr.log"
|
||||
},
|
||||
{
|
||||
"lane": "openai",
|
||||
"instance": 2,
|
||||
"pid": 31592,
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\openai-2-parallel-20260910-144827.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\openai-2-parallel-20260910-144827.stderr.log"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"lane": "prepare",
|
||||
"instance": 1,
|
||||
"pid": 1160,
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\prepare-1-parallel-20260910-171432.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\prepare-1-parallel-20260910-171432.stderr.log"
|
||||
},
|
||||
{
|
||||
"lane": "qwen",
|
||||
"instance": 1,
|
||||
"pid": 25112,
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-1-parallel-20260910-171432.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-1-parallel-20260910-171432.stderr.log"
|
||||
},
|
||||
{
|
||||
"lane": "qwen",
|
||||
"instance": 2,
|
||||
"pid": 25352,
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-2-parallel-20260910-171432.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\qwen-2-parallel-20260910-171432.stderr.log"
|
||||
},
|
||||
{
|
||||
"lane": "openai",
|
||||
"instance": 1,
|
||||
"pid": 25156,
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\openai-1-parallel-20260910-171432.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\openai-1-parallel-20260910-171432.stderr.log"
|
||||
},
|
||||
{
|
||||
"lane": "openai",
|
||||
"instance": 2,
|
||||
"pid": 25188,
|
||||
"stdout": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\openai-2-parallel-20260910-171432.stdout.log",
|
||||
"stderr": "D:\\web\\zyt\\artifacts\\prescription-ai-runtime\\openai-2-parallel-20260910-171432.stderr.log"
|
||||
}
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user