Commit 9cdf04ef by luoqi

chore: 入库会话前既有的工作区改动

会话前已存在、本次一并提交:cold-import.service / sync-incremental scheduler /
clinical-gap potential-treatment-gap SQL / cold-import manifest.schema /
dw-lag-monitor / canonical-codes / ingestion·monitoring 文档 / jvs-dw manifest.yaml /
package.json / pnpm-lock / .gitignore(next-env.d.ts)/ docs/friday-saas-mapping-temp。
非本次会话产出,随此次部署一并落地。
parent 1cf137ac
Pipeline #3340 failed in 0 seconds
...@@ -35,3 +35,6 @@ docs/.obsidian/ ...@@ -35,3 +35,6 @@ docs/.obsidian/
# 本地 IDE / Claude 配置 # 本地 IDE / Claude 配置
.claude/ .claude/
# Next.js 自动生成(dev/build 间路径翻转,build 每次重生成)
apps/pac-web/next-env.d.ts
...@@ -60,13 +60,18 @@ SELECT count(*) AS multi_active FROM (SELECT subject_id,host_id,tenant_id FROM p ...@@ -60,13 +60,18 @@ SELECT count(*) AS multi_active FROM (SELECT subject_id,host_id,tenant_id FROM p
## 二、日级增量 ## 二、日级增量
`SyncIncrementalSchedulerService` 的 `@Cron`(`PAC_INCREMENTAL_CRON`)随服务启动注册,走与全量同一套 `importDirectory`: `SyncIncrementalSchedulerService` 启动时**为每个 `auto_sync: true` 的 host 各注册一个 CronJob**,走与全量同一套 `importDirectory`。cron 时间取自该 host `manifest.incremental_cron`(按 manifest `timezone` 解释),缺省回退全局 `PAC_INCREMENTAL_CRON` —— 各 host 在自己上游刷新窗口后跑,不再挤一个全局时刻:
```yaml
# data/<host>/manifest.yaml
incremental_cron: '15 8 * * *' # 该 host 数据源当日刷新之后
``` ```
每天 08:15(PAC_INCREMENTAL_CRON,须设在数据源当日刷新之后)
```
到点(每 host 各自 cron)
importDirectory({ incremental: true }) importDirectory({ incremental: true })
- 并发锁:sync_logs partial UNIQUE (host_id) WHERE status='running' - 并发锁:sync_logs partial UNIQUE (host_id) WHERE status='running' AND direction='pull'
同 host 已在跑 → 跳过,等下次 cron 同 host 已在跑 → 跳过,等下次 cron
- 读上次 cursor_after → SQL WHERE updated_date > (cursor − 回看窗 48h) - 读上次 cursor_after → SQL WHERE updated_date > (cursor − 回看窗 48h)
(回看重叠靠 source_event_id 去重;增量数据量小,通常 1 批) (回看重叠靠 source_event_id 去重;增量数据量小,通常 1 批)
......
...@@ -22,7 +22,19 @@ PAC 的运行态监控分四块:数据源滞后、每日健康日报、队列健 ...@@ -22,7 +22,19 @@ PAC 的运行态监控分四块:数据源滞后、每日健康日报、队列健
🔴 > 48h critical log + 告警推送(严重滞后,事件可能漏召) 🔴 > 48h critical log + 告警推送(严重滞后,事件可能漏召)
``` ```
阈值由 `PAC_LAG_WARN_HOURS` / `PAC_LAG_ERROR_HOURS` 控制。未配 `ALERT_WEBHOOK_URL` 时仅落 log,值班查 `journalctl -u pac-service | grep dw-lag`。 阈值默认由 `PAC_LAG_WARN_HOURS` / `PAC_LAG_ERROR_HOURS` 控制,**每个 host 可在 `manifest.monitoring` 覆盖**(上游节奏不同,一刀切会误报/漏报):
```yaml
# data/<host>/manifest.yaml
monitoring:
dw_lag_warn_hours: 24
dw_lag_error_hours: 48
push_lag_error_hours: 26 # push host:超此小时数无推送 → 告警(实时推设小,批量推设大)
```
未配 `ALERT_WEBHOOK_URL` 时仅落 log,值班查 `journalctl -u pac-service | grep dw-lag`。
**push 断流监控**:同一 `DwLagMonitorService` 对**曾推过**的 host 检查最近一次 push 距今时长,超 `push_lag_error_hours`(默认 26h)→ critical 告警;未上线 push 的 host 不检查。
--- ---
......
...@@ -42,6 +42,12 @@ host_name: jvs-dw ...@@ -42,6 +42,12 @@ host_name: jvs-dw
display_name: 瑞尔集团 DW(单 host 多 brand) display_name: 瑞尔集团 DW(单 host 多 brand)
auto_sync: true # 纳入每日自动增量同步(scheduler 自动发现;取代 PAC_INCREMENTAL_HOSTS env) auto_sync: true # 纳入每日自动增量同步(scheduler 自动发现;取代 PAC_INCREMENTAL_HOSTS env)
# 每宿主运维配置(多宿主:上游节奏/SLA 各异,不用全局 env 一刀切)。
incremental_cron: '15 8 * * *' # 瑞尔 DW 每日 08:00(沪)落库,其后跑;按下方 timezone 解释
monitoring:
dw_lag_warn_hours: 24 # 增量游标滞后 >24h 提醒
dw_lag_error_hours: 48 # >48h 告警(事件可能漏召)
# ── 集团模型(tenant=集团 slug;品牌降为 source_unit 命名空间)── # ── 集团模型(tenant=集团 slug;品牌降为 source_unit 命名空间)──
# tenant=集团:稳定 slug(各环境一致,不再写死 DW UUID);所有行归此集团。 # tenant=集团:稳定 slug(各环境一致,不再写死 DW UUID);所有行归此集团。
# 品牌(瑞尔/瑞泰)= patients.source_unit,经 identity_namespace_field 从 brand 列填, # 品牌(瑞尔/瑞泰)= patients.source_unit,经 identity_namespace_field 从 brand 列填,
......
...@@ -65,6 +65,7 @@ ...@@ -65,6 +65,7 @@
"@nestjs/swagger": "^11.4.2", "@nestjs/swagger": "^11.4.2",
"@nestjs/websockets": "^11.1.24", "@nestjs/websockets": "^11.1.24",
"@pac/types": "workspace:*", "@pac/types": "workspace:*",
"cron": "^4.4.0",
"@pac/utils": "workspace:*", "@pac/utils": "workspace:*",
"@prisma/client": "^6.19.2", "@prisma/client": "^6.19.2",
"@scalar/nestjs-api-reference": "^1.1.14", "@scalar/nestjs-api-reference": "^1.1.14",
......
...@@ -4,6 +4,7 @@ import { ...@@ -4,6 +4,7 @@ import {
EXTERNAL_TREATMENT_VISIT_NEG_RE, EXTERNAL_TREATMENT_VISIT_NEG_RE,
NO_RESTORATION_GAP_EXAM_PATTERNS, NO_RESTORATION_GAP_EXAM_PATTERNS,
TREATMENT_REFUSAL_SUBTYPE_PATTERNS, TREATMENT_REFUSAL_SUBTYPE_PATTERNS,
TREATMENT_REFUSAL_IMAGING_EXCLUDE_RE,
RESTORATION_INELIGIBLE_DX_NAMES, RESTORATION_INELIGIBLE_DX_NAMES,
STRUCTURAL_DX_CODE_LIST, STRUCTURAL_DX_CODE_LIST,
type DxTreatmentRule, type DxTreatmentRule,
...@@ -139,6 +140,11 @@ export function buildGapCore(input: GapCoreInput): GapCorePieces { ...@@ -139,6 +140,11 @@ export function buildGapCore(input: GapCoreInput): GapCorePieces {
const afterDxFragRtx = afterDxFor('rtx'); const afterDxFragRtx = afterDxFor('rtx');
const noRestorMsgRe = NO_RESTORATION_GAP_EXAM_PATTERNS.join('|'); const noRestorMsgRe = NO_RESTORATION_GAP_EXAM_PATTERNS.join('|');
const refusalRe = TREATMENT_REFUSAL_SUBTYPE_PATTERNS.join('|'); const refusalRe = TREATMENT_REFUSAL_SUBTYPE_PATTERNS.join('|');
/// 判 refusal 前先抹掉"拒绝拍片"类影像拒绝(≠ 拒绝治疗),避免误压召回。
/// regexp_replace 而非直接 !~:混合记录("拒绝拍片…拒绝正畸")抹掉影像拒绝后仍保留真治疗拒绝。
const refusalImagingRe = TREATMENT_REFUSAL_IMAGING_EXCLUDE_RE;
const refusalSubtypeMatch = (alias: string): Prisma.Sql =>
Prisma.sql`regexp_replace(${Prisma.raw(alias)}.content->>'subtype', ${refusalImagingRe}, '', 'g') ~ ${refusalRe}`;
/// 外院已治疗(回访记录 result)患者级排除:POS 紧邻命中 ∧ 非 NEG 意图 ∧ 回访日 ≥ 本次诊断 → 排除。 /// 外院已治疗(回访记录 result)患者级排除:POS 紧邻命中 ∧ 非 NEG 意图 ∧ 回访日 ≥ 本次诊断 → 排除。
/// 患者级(不看牙位/类目);时间方向同治疗排除(新诊断更晚 → 不排)。只读 patient_return_visits.result, /// 患者级(不看牙位/类目);时间方向同治疗排除(新诊断更晚 → 不排)。只读 patient_return_visits.result,
/// 不影响已治疗事实。一线反馈②(李姝妤 回访"已在外院矫正中")。 /// 不影响已治疗事实。一线反馈②(李姝妤 回访"已在外院矫正中")。
...@@ -276,7 +282,7 @@ export function buildGapCore(input: GapCoreInput): GapCorePieces { ...@@ -276,7 +282,7 @@ export function buildGapCore(input: GapCoreInput): GapCorePieces {
CROSS JOIN unnest(${toothArrSql(Prisma.sql`rfx.content->>'tooth_position'`)}) AS rft CROSS JOIN unnest(${toothArrSql(Prisma.sql`rfx.content->>'tooth_position'`)}) AS rft
WHERE rfx.patient_id = p.id WHERE rfx.patient_id = p.id
AND rfx.type = 'treatment_record' AND rfx.status IN ('active', 'fulfilled') AND rfx.type = 'treatment_record' AND rfx.status IN ('active', 'fulfilled')
AND rfx.content->>'subtype' ~ ${Prisma.sql`${refusalRe}`} AND ${refusalSubtypeMatch('rfx')}
AND rfx.content->>'category' = ANY(${resolverCats}::text[]) AND rfx.content->>'category' = ANY(${resolverCats}::text[])
${refusalAfterDx} ${refusalAfterDx}
UNION UNION
...@@ -341,7 +347,7 @@ export function buildGapCore(input: GapCoreInput): GapCorePieces { ...@@ -341,7 +347,7 @@ export function buildGapCore(input: GapCoreInput): GapCorePieces {
WHERE rfx.patient_id = p.id WHERE rfx.patient_id = p.id
AND rfx.type = 'treatment_record' AND rfx.type = 'treatment_record'
AND rfx.status IN ('active', 'fulfilled') AND rfx.status IN ('active', 'fulfilled')
AND rfx.content->>'subtype' ~ ${Prisma.sql`${refusalRe}`} AND ${refusalSubtypeMatch('rfx')}
AND rfx.content->>'category' = ANY(${resolverCats}::text[]) AND rfx.content->>'category' = ANY(${resolverCats}::text[])
${refusalAfterDx} ${refusalAfterDx}
) )
......
...@@ -495,6 +495,38 @@ export class ColdImportService { ...@@ -495,6 +495,38 @@ export class ColdImportService {
} }
} }
/**
* 每宿主运维配置(调度 cron + 监控阈值),从 manifest 读。多宿主:各 host 上游节奏/SLA 不同,
* 这些不该用全局 env 一刀切。缺失字段由调用方回退全局默认。无 manifest → 全 null。
*/
getHostOpsConfig(hostName: string): {
incrementalCron: string | null;
timezone: string | null;
dwLagWarnHours: number | null;
dwLagErrorHours: number | null;
pushLagErrorHours: number | null;
} {
const empty = {
incrementalCron: null,
timezone: null,
dwLagWarnHours: null,
dwLagErrorHours: null,
pushLagErrorHours: null,
};
try {
const m = this.readManifest(this.resolveHostDir(hostName));
return {
incrementalCron: m.incremental_cron ?? null,
timezone: m.timezone ?? null,
dwLagWarnHours: m.monitoring?.dw_lag_warn_hours ?? null,
dwLagErrorHours: m.monitoring?.dw_lag_error_hours ?? null,
pushLagErrorHours: m.monitoring?.push_lag_error_hours ?? null,
};
} catch {
return empty;
}
}
/// host → 配置目录(= sync.service.resolveDataDir 同约定:env PAC_INCREMENTAL_DATA_DIR ?? cwd/data,再拼 hostName) /// host → 配置目录(= sync.service.resolveDataDir 同约定:env PAC_INCREMENTAL_DATA_DIR ?? cwd/data,再拼 hostName)
private resolveHostDir(hostName: string): string { private resolveHostDir(hostName: string): string {
const base = process.env.PAC_INCREMENTAL_DATA_DIR ?? path.resolve(process.cwd(), 'data'); const base = process.env.PAC_INCREMENTAL_DATA_DIR ?? path.resolve(process.cwd(), 'data');
......
...@@ -114,6 +114,24 @@ export const ColdImportManifestSchema = z ...@@ -114,6 +114,24 @@ export const ColdImportManifestSchema = z
///(接入新 host 忘了改 env 导致静默不同步的坑)。env 仍可作显式 override(见 scheduler)。 ///(接入新 host 忘了改 env 导致静默不同步的坑)。env 仍可作显式 override(见 scheduler)。
auto_sync: z.boolean().optional().default(false), auto_sync: z.boolean().optional().default(false),
/// **每宿主增量 cron**(可选)—— 上游 DW 刷新窗口因 host 而异,该 host 在自己 DW 就绪后跑。
/// 缺省回退全局 `PAC_INCREMENTAL_CRON`。cron 按本 manifest 的 `timezone` 解释(不是服务器 UTC)。
/// 例:'30 2 * * *'(DW 02:00 刷新后)。仅 auto_sync=true 时生效。
incremental_cron: z.string().min(1).optional(),
/// **每宿主监控阈值**(可选)—— 不同 host 推送/刷新节奏不同,一刀切阈值要么误报要么漏报。
/// 缺省回退全局 env(见 dw-lag-monitor)。
monitoring: z
.object({
/// DW 增量游标滞后告警(pull host):warn / error 小时阈值
dw_lag_warn_hours: z.number().positive().optional(),
dw_lag_error_hours: z.number().positive().optional(),
/// push 断流告警(push host):超过此小时数无 push → critical。
/// 实时推的 host 设小(如 3),每晚批量推的设大(如 26)。
push_lag_error_hours: z.number().positive().optional(),
})
.optional(),
/// **单 tenant 场景**:静态 tenant_id,所有行共用一个 tenant /// **单 tenant 场景**:静态 tenant_id,所有行共用一个 tenant
tenant_id: z.string().min(1).optional(), tenant_id: z.string().min(1).optional(),
......
...@@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; ...@@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule'; import { Cron } from '@nestjs/schedule';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { AlertService } from '../common/alerting/alert.service'; import { AlertService } from '../common/alerting/alert.service';
import { ColdImportService } from '../modules/sync/cold-import/cold-import.service';
/** /**
* DwLagMonitorService — 每小时检查 DW 增量数据滞后(W4 末) * DwLagMonitorService — 每小时检查 DW 增量数据滞后(W4 末)
...@@ -25,6 +26,7 @@ export class DwLagMonitorService { ...@@ -25,6 +26,7 @@ export class DwLagMonitorService {
constructor( constructor(
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly alerter: AlertService, private readonly alerter: AlertService,
private readonly coldImport: ColdImportService,
) {} ) {}
/// DW 数据滞后告警 cron — env 驱动 /// DW 数据滞后告警 cron — env 驱动
...@@ -35,12 +37,16 @@ export class DwLagMonitorService { ...@@ -35,12 +37,16 @@ export class DwLagMonitorService {
name: 'dw-lag-monitor', name: 'dw-lag-monitor',
}) })
async checkLag(): Promise<void> { async checkLag(): Promise<void> {
const warnH = Number(process.env.PAC_LAG_WARN_HOURS ?? '24'); // 全局默认(env);每宿主可在 manifest.monitoring 覆盖(上游节奏不同,一刀切会误报/漏报)
const errorH = Number(process.env.PAC_LAG_ERROR_HOURS ?? '48'); const gWarnH = Number(process.env.PAC_LAG_WARN_HOURS ?? '24');
const gErrorH = Number(process.env.PAC_LAG_ERROR_HOURS ?? '48');
const now = Date.now(); const now = Date.now();
const hosts = await this.prisma.host.findMany({ select: { id: true, name: true } }); const hosts = await this.prisma.host.findMany({ select: { id: true, name: true } });
for (const host of hosts) { for (const host of hosts) {
const ops = this.coldImport.getHostOpsConfig(host.name);
const warnH = ops.dwLagWarnHours ?? gWarnH;
const errorH = ops.dwLagErrorHours ?? gErrorH;
const last = await this.prisma.syncLog.findFirst({ const last = await this.prisma.syncLog.findFirst({
where: { where: {
hostId: host.id, hostId: host.id,
...@@ -105,8 +111,10 @@ export class DwLagMonitorService { ...@@ -105,8 +111,10 @@ export class DwLagMonitorService {
hosts: Array<{ id: string; name: string }>, hosts: Array<{ id: string; name: string }>,
now: number, now: number,
): Promise<void> { ): Promise<void> {
const errorH = Number(process.env.PAC_PUSH_LAG_ERROR_HOURS ?? '26'); const gErrorH = Number(process.env.PAC_PUSH_LAG_ERROR_HOURS ?? '26');
for (const host of hosts) { for (const host of hosts) {
// 每宿主阈值:实时推的 host 设小(断几小时就该报),批量推的设大;缺省回退全局
const errorH = this.coldImport.getHostOpsConfig(host.name).pushLagErrorHours ?? gErrorH;
const lastPush = await this.prisma.syncLog.findFirst({ const lastPush = await this.prisma.syncLog.findFirst({
where: { hostId: host.id, direction: 'push' }, where: { hostId: host.id, direction: 'push' },
orderBy: { startedAt: 'desc' }, orderBy: { startedAt: 'desc' },
......
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { Cron } from '@nestjs/schedule'; import { SchedulerRegistry } from '@nestjs/schedule';
import { CronJob } from 'cron';
import * as path from 'path'; import * as path from 'path';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { import {
...@@ -11,27 +12,24 @@ import { PersonaService } from '../modules/persona/persona.service'; ...@@ -11,27 +12,24 @@ import { PersonaService } from '../modules/persona/persona.service';
import { PlanEngineService } from '../modules/plan/engine/plan-engine.service'; import { PlanEngineService } from '../modules/plan/engine/plan-engine.service';
/** /**
* SyncIncrementalSchedulerService — W4 末:DW 直连增量自动跑(每天 02:30) * SyncIncrementalSchedulerService — DW 直连增量自动跑(**每宿主独立 cron**)
* *
* 上游:DW 团队每日凌晨 ~02:00 完成全量刷新 * 每天在各 host 自己的 DW 刷新窗口后触发:
* 我们 02:30 触发: * 1. ColdImportService.importDirectory({ incremental: true })(游标增量 + stub 补齐)
* 1. ColdImportService.importDirectory({ incremental: true }) * 2. Persona 重算(本次 sync 写入的 distinct patient_id)
* - 读 sync_logs 上次 cursor_after → SQL 注入 WHERE updated_date > '...' * 3. org-tree 预热 + Plan 重算(per tenant)
* - 反向拉主档(C 方案)+ stub auto-create(A 方案)→ 无数据丢失
* - 写新 cursor 到 sync_logs
* 2. Persona 重算(affected patient — 本次 sync 写入的 distinct patient_id)
* 3. Plan 重算(per tenant,scenario SQL + 6 因子打分)
* *
* 多 host:默认自动发现 data/<host>/manifest.yaml 中 `auto_sync: true` 的 host; * **多 host 调度**(启动时动态注册,取代单一全局 @Cron):
* PAC_INCREMENTAL_HOSTS env(逗号分隔)若设则作显式 override(escape hatch)。 * - 发现 data/<host>/manifest.yaml 中 auto_sync=true 的 host;
* 每个 host 期望 data/<host>/manifest.yaml 存在 * - 每个 host 用**自己 manifest 的 incremental_cron + timezone**注册独立 CronJob
* —— 上游 DW 刷新窗口因 host 而异,各自在就绪后跑,不再挤在一个全局时刻;
* - manifest 没配 cron → 回退全局 PAC_INCREMENTAL_CRON(按 host manifest 时区解释)。
* - PAC_INCREMENTAL_HOSTS env(逗号分隔)仍作显式 override 逃生口(用全局 cron)。
* *
* 跑失败: * 跑失败:cursor 不前进 → 下次自动 catchup;log ERROR 不抛。
* - cursor 不前进(ColdImportService 内 transaction)→ 下次自动 catchup
* - log ERROR + 不抛(让 cron 下次继续)
*/ */
@Injectable() @Injectable()
export class SyncIncrementalSchedulerService { export class SyncIncrementalSchedulerService implements OnModuleInit {
private readonly logger = new Logger(SyncIncrementalSchedulerService.name); private readonly logger = new Logger(SyncIncrementalSchedulerService.name);
constructor( constructor(
...@@ -40,56 +38,69 @@ export class SyncIncrementalSchedulerService { ...@@ -40,56 +38,69 @@ export class SyncIncrementalSchedulerService {
private readonly orgTree: OrgTreeService, private readonly orgTree: OrgTreeService,
private readonly persona: PersonaService, private readonly persona: PersonaService,
private readonly planEngine: PlanEngineService, private readonly planEngine: PlanEngineService,
private readonly scheduler: SchedulerRegistry,
) {} ) {}
/// DW 增量同步 cron — env 驱动 onModuleInit(): void {
/// PAC_INCREMENTAL_CRON: const globalCron = process.env.PAC_INCREMENTAL_CRON;
/// 不设 → 不跑(local 默认,避免开发期自动消耗 DW 流量) const dataDir = this.dataDir();
/// '30 2 * * *' → 生产推荐(DW 02:00 全量刷新后) // env override(逗号分隔)优先;否则自动发现 auto_sync=true 的 host
/// '30 3 * * *' → staging 错峰
/// 详见 .env.example
@Cron(process.env.PAC_INCREMENTAL_CRON || '0 0 31 12 *' /* never — env not set */, {
name: 'sync-incremental-daily',
// cron 表达式按北京时间解释(容器为 UTC;不加则实际 +8h —— 03:30 会变 11:30)
timeZone: 'Asia/Shanghai',
})
async runDaily(): Promise<void> {
const dataDir = process.env.PAC_INCREMENTAL_DATA_DIR ?? path.resolve(__dirname, '../../data');
// host 来源:env 显式 override(逗号分隔)优先;未设 → 自动发现 manifest.auto_sync=true。
// 自动发现取代旧 `PAC_INCREMENTAL_HOSTS=jvs-dw` 默认值,接入新 host 只改其 manifest,不碰 env(防 drift)。
const envHosts = (process.env.PAC_INCREMENTAL_HOSTS ?? '') const envHosts = (process.env.PAC_INCREMENTAL_HOSTS ?? '')
.split(',') .split(',')
.map((h) => h.trim()) .map((h) => h.trim())
.filter(Boolean); .filter(Boolean);
const hosts = envHosts.length > 0 ? envHosts : this.coldImport.discoverAutoSyncHostDirs(dataDir); const hosts = envHosts.length > 0 ? envHosts : this.coldImport.discoverAutoSyncHostDirs(dataDir);
const source = envHosts.length > 0 ? 'env-override' : 'manifest auto_sync';
if (hosts.length === 0) { if (hosts.length === 0) {
this.logger.warn( this.logger.log('sync-incremental: 无 auto_sync host,不注册 cron');
`sync-incremental: 无 host 可同步(env PAC_INCREMENTAL_HOSTS 未设,且 ${dataDir} 下无 manifest.auto_sync=true);skip`,
);
return; return;
} }
this.logger.log(
`sync-incremental: START hosts=[${hosts.join(',')}] (source=${source}) dataDir=${dataDir}`,
);
for (const host of hosts) { for (const host of hosts) {
try { const ops = this.coldImport.getHostOpsConfig(host);
await this.runOne(path.join(dataDir, host)); // 每宿主 cron:manifest.incremental_cron 优先,否则全局 env;都没有 → 不注册(该 host 不自动同步)
} catch (err) { const cronExpr = ops.incrementalCron ?? globalCron;
if (err instanceof SyncAlreadyRunningError) { if (!cronExpr) {
// 并发拦截 — 已有手动跑或上次 cron 还没完。下次 cron 自然 retry。 this.logger.warn(
this.logger.warn( `sync-incremental: host=${host} 无 cron(manifest.incremental_cron 与 PAC_INCREMENTAL_CRON 都未设),跳过注册`,
`sync-incremental: host=${host} skip(并发锁拦截):${err.message}`, );
); continue;
} else { }
this.logger.error( const timeZone = ops.timezone ?? 'Asia/Shanghai';
`sync-incremental: host=${host} failed: ${(err as Error).message}`, const jobName = `sync-incremental:${host}`;
); const job = new CronJob(
} cronExpr,
// 不抛 — 下个 host 继续;cursor 没前进自然下次 catchup () => {
void this.runHostSafe(host);
},
null,
false,
timeZone,
);
this.scheduler.addCronJob(jobName, job as unknown as Parameters<SchedulerRegistry['addCronJob']>[1]);
job.start();
this.logger.log(
`sync-incremental: 注册 host=${host} cron='${cronExpr}' tz=${timeZone}` +
`${ops.incrementalCron ? '' : '(全局默认)'}`,
);
}
}
private dataDir(): string {
return process.env.PAC_INCREMENTAL_DATA_DIR ?? path.resolve(__dirname, '../../data');
}
/// 单 host 跑一轮(cron 回调用,吞异常不影响该 host 下次 / 别的 host)
private async runHostSafe(host: string): Promise<void> {
try {
await this.runOne(path.join(this.dataDir(), host));
} catch (err) {
if (err instanceof SyncAlreadyRunningError) {
this.logger.warn(`sync-incremental: host=${host} skip(并发锁拦截):${err.message}`);
} else {
this.logger.error(`sync-incremental: host=${host} failed: ${(err as Error).message}`);
} }
} }
this.logger.log('sync-incremental: ALL DONE');
} }
/// 跑单个 host 的完整链路(可手动调,debug 用) /// 跑单个 host 的完整链路(可手动调,debug 用)
......
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
# FRIDAY SaaS 标准化字段临时映射
## 1. 患者一句话总结
患者主档需要宿主提供 `customer_basic_info``customer_contacts``customer_label``customer_description`
## 1.1 患者宿主需提供表
| 表 | 用途 |
| --- | --- |
| `customer_basic_info` | 患者主档、品牌、获客来源 |
| `customer_contacts` | 患者默认手机号 |
| `customer_label` | 患者标签 |
| `customer_description` | 患者备注 |
## 1.2 PAC patients
| PAC 字段 | FRIDAY 候选 |
| --- | --- |
| `id` | PAC 生成 UUID |
| `host_id` | PAC `hosts.id` |
| `tenant_id` | 固定集团 slug |
| `source_unit` | 品牌 ID |
| `external_id` | `customer_basic_info.id` |
| `name` | `customer_basic_info.name` |
| `phone` | `customer_contacts.contacts_tel` where `contacts_type=1` and `is_default=1` |
| `gender` | `customer_basic_info.sex` |
| `birth_date` | `customer_basic_info.birthday` |
| `medical_record_number` | `customer_basic_info.file_number` |
| `preferences` | |
| `active` | |
| `created_at` | PAC 写入时间 |
| `updated_at` | PAC 写入时间 |
## 1.3 PAC patient_profiles
| PAC 字段 | FRIDAY 候选 |
| --- | --- |
| `patient_id` | PAC `patients.id` |
| `do_not_contact` | |
| `do_not_contact_reason` | |
| `do_not_contact_at` | |
| `deceased` | |
| `deceased_at` | |
| `tags` | `customer_label.label_name[]` |
| `notes` | `customer_description.description` |
| `acquisition_channel` | `customer_basic_info.informed_first_name` |
| `acquisition_sub` | `customer_basic_info.informed_second_name` |
| `referral_count` | TODO |
| `referral_amount_cents` | TODO |
| `created_at` | PAC 写入时间 |
| `updated_at` | PAC 写入时间 |
## 2. 预约一句话总结
预约数据需要宿主提供 `appointment_base``appointment_complaint``appointment_cancel`
## 2.1 预约宿主需提供表
| 表 | 用途 |
| ------------------------ | --------- |
| `appointment_base` | 预约主记录 |
| `appointment_complaint` | 预约主诉 |
| `appointment_cancel` | 取消 / 改约原因 |
## 2.2 PAC appointment_record content
| PAC content 字段 | FRIDAY 候选 |
| ------------------------- | --------------------------------------------------------------------------------- |
| `appointment_external_id` | `appointment_base.id` |
| `scheduled_at` | `appointment_base.appointment_date` + `appointment_base.appointment_start` |
| `arrived_at` | `appointment_base.in_time` |
| `doctor_id` | `appointment_base.doctor_user_id` |
| `complaint_category` | `appointment_complaint.complaint_category_name` |
| `complaint_text` | `appointment_complaint.complaint_name` |
| `duration_minutes` | `appointment_base.appointment_time_length` |
| `cancellation_reason` | `appointment_cancel.cancel_cause_name` / `appointment_cancel.cancel_cause_custom` |
| `staff_notes` | `appointment_base.remark` |
| `status` | `appointment_base.appointment_status` |
## 3. 接诊一句话总结
接诊数据需要宿主提供 `visit_info`
## 3.1 接诊宿主需提供表
| 表 | 用途 |
| --------------- | ------------------------- |
| `visit_info` | 接诊主记录、预约关联、医生、就诊状态、就诊起止时间 |
## 3.2 PAC encounter_record content
| PAC content 字段 | FRIDAY 候选 |
| ----------------------- | ------------------------ |
| `encounter_external_id` | `visit_info.id` |
| `doctor_id` | `visit_info.doctor_id` |
| `doctor_name` | `visit_info.doctor_name` |
| `chief_complaint` | |
| `notes` | |
## 4. EMR 一句话总结
EMR 数据需要宿主提供 `med_emr``med_emr_info`,通过 `visit_info` 回连接诊。
## 4.1 EMR 宿主需提供表
| 表 | 用途 |
| --- | --- |
| `med_emr` | 病历索引、患者、预约、医生、病历状态 |
| `med_emr_info` | 病历正文 |
| `visit_info` | 回连 PAC encounter |
## 4.2 PAC emr_record content
| PAC content 字段 | FRIDAY 候选 |
| ----------------------- | --------------------------------------------------------------------------- |
| `emr_external_id` | `med_emr.emr_sub_id` |
| `encounter_external_id` | `visit_info.id` where `visit_info.registration_id = med_emr.appointment_id` |
| `doctor_id` | `med_emr.user_id` |
| `doctor_name` | `med_emr.user_name` |
| `illness_desc` | `med_emr.illness_desc` |
| `pre_illness` | `med_emr_info.pre_illness` |
| `past_history` | `med_emr_info.past_hist` |
| `general_condition` | `med_emr_info.gen_cond` |
| `exam_findings` | `med_emr_info.examine` / `med_emr_info.auxi_exam` |
| `diagnosis_text` | `med_emr_info.diag` |
| `treatment_plan` | `med_emr_info.treat` |
| `disposal` | `med_emr_info.dispose` |
| `doctor_advice` | `med_emr_info.doc_order` |
| `quality_check` | `med_emr_info.status` |
## 5. 诊断一句话总结
诊断数据需要宿主提供 `med_emr_info.diag[]`,可选补充 `std_diag`
## 5.1 诊断宿主需提供表
| 表 | 用途 |
| --- | --- |
| `med_emr_info` | 病历正文 Mongo collection,`diag[]` 保存患者诊断 |
| `med_emr` | 病历索引、医生、患者、预约 |
| `visit_info` | 回连 PAC encounter |
| `std_diag` | 标准诊断词典 |
## 5.2 PAC diagnosis_record content
| PAC content 字段 | FRIDAY 候选 |
| ------------------------------ | --------------------------------------------------------------------------- |
| `code` | `med_emr_info.diag[].link_code` / `std_diag.std_code` |
| `code_source` | `std_code` |
| `name_zh` | `med_emr_info.diag[].value` / `std_diag.diag_name` |
| `tooth_position` | `med_emr_info.diag[].tooth_position` |
| `severity` | |
| `onset_date` | |
| `doctor_id` | `med_emr.user_id` |
| `doctor_name` | `med_emr.user_name` |
| `source_encounter_external_id` | `visit_info.id` where `visit_info.registration_id = med_emr.appointment_id` |
## 6. 治疗一句话总结
治疗数据需要宿主提供 `therapy_plan``visit_therapy_order`,可选补充 `med_emr_info.treat[]``med_emr_info.dispose[]``std_treat`
## 6.1 治疗宿主需提供表
| 表 | 用途 |
| --- | --- |
| `therapy_plan` | 患者治疗计划、状态、医生、接诊关联 |
| `visit_therapy_order` | 治疗服务项、数量、单位、执行状态 |
| `visit_info` | 回连 PAC encounter |
| `med_emr_info` | 病历正文 Mongo collection,`treat[]` / `dispose[]` 保存病历治疗文本 |
| `std_treat` | 标准治疗词典 |
## 6.2 PAC treatment_record content
| PAC content 字段 | FRIDAY 候选 |
| --- | --- |
| `category` | `visit_therapy_order.cure_info_name` / `visit_therapy_order.service_project_name` / `med_emr_info.treat[].value` |
| `subtype` | `visit_therapy_order.spec_name` / `visit_therapy_order.subject_class_name` |
| `tooth_position` | `med_emr_info.treat[].tooth_position` / `med_emr_info.dispose[].tooth_position` |
| `status` | `therapy_plan.plan_status` / `visit_therapy_order.status` |
| `started_at` | `therapy_plan.created_gmt_at` |
| `completed_at` | `therapy_plan.updated_gmt_at` when `therapy_plan.plan_status=3` |
| `doctor_id` | `therapy_plan.doctor_id` / `visit_therapy_order.doctor_id` |
| `doctor_name` | `therapy_plan.doctor_name` |
| `quantity` | `visit_therapy_order.number` |
| `unit_name` | `visit_therapy_order.unit_name` |
| `treat_stages` | |
| `source_encounter_external_id` | `therapy_plan.visit_id` |
| `related_diagnosis_subject_id` | |
| `review_window_days` | |
## 7. 影像和影像分析一句话总结
影像数据需要宿主提供 `med_check``med_emr_check`;影像分析 FRIDAY 代码未见独立表,PAC 落 `agent_invocations.output` / `agent_invocations.output_text`
## 7.1 影像宿主需提供表
| 表 | 用途 |
| --- | --- |
| `med_check` | 影像/检查文件主记录 |
| `med_emr_check` | 病历与影像关联 |
| `med_emr` | 回连预约、医生、患者 |
| `visit_info` | 回连 PAC encounter |
| `std_check_class` | 影像/检查分类词典 |
## 7.2 PAC image_record content
| PAC content 字段 | FRIDAY 候选 |
| --- | --- |
| `image_external_id` | `med_check.id` |
| `encounter_external_id` | `visit_info.id` where `visit_info.registration_id = med_emr.appointment_id` |
| `modality` | `med_check.file_type` / `med_check.class_code` / `std_check_class.class_name` |
| `tooth_positions` | |
| `finding` | `med_check.file_desc` |
| `captured_at` | `med_check.shooting_time` |
| `doctor_id` | `med_check.user_id` / `med_emr.user_id` |
| `doctor_name` | `med_check.user_name` / `med_emr.user_name` |
## 8. 收款一句话总结
收款数据需要宿主提供 `patient_settlement`,通过 `patient_settlement_spec` 补项目/折扣,通过 `settlement_modes` 补支付渠道/卡券。
## 8.1 收款宿主需提供表
| 表 | 用途 |
| --- | --- |
| `patient_settlement` | 结算单主表 |
| `patient_settlement_spec` | 结算单收费项目明细 |
| `settlement_modes` | 结算支付方式/卡券/储值 |
| `visit_info` | 通过 `patient_settlement.registration_id` 回连 PAC encounter |
## 8.2 PAC payment_record content
| PAC content 字段 | FRIDAY 候选 |
| --- | --- |
| `payment_external_id` | `patient_settlement.uuid` |
| `amount_cents` | `patient_settlement.net_receipts_this` / sum(`settlement_modes.money`) |
| `channel` | `settlement_modes.modes_name` / `settlement_modes.modes` |
| `insurance_name` | `patient_settlement.insurance_name` |
| `card_type_name` | |
| `card_name` | `settlement_modes.card_name` / `patient_settlement.welfare_cards` / `patient_settlement_spec.used_welfare_name` |
| `discount_cents` | `patient_settlement.hospital_receipts` + `patient_settlement.custom_company_receipts` + `patient_settlement.special_welfare_receipts` |
| `settlement_project` | `patient_settlement_spec.service_project_name` / `patient_settlement_spec.cure_name` / `patient_settlement_spec.classify_name` |
| `doctor_id` | `patient_settlement.doctor_id` / `patient_settlement_spec.doctor_id` |
| `encounter_external_id` | `visit_info.id` where `visit_info.registration_id = patient_settlement.registration_id` |
| `related_order_external_id` | `patient_settlement.visit_therapy_id` / `patient_settlement_spec.order_pricing_id` |
## 9. 退款一句话总结
退款数据需要宿主提供 `patient_settlement` 退款行,`ref_settlement_id` 关联原结算单;退款原因可从 `patient_problem_list` 补。
## 9.1 退款宿主需提供表
| 表 | 用途 |
| --- | --- |
| `patient_settlement` | 退款结算单主记录 |
| `patient_settlement_spec` | 退款收费项目明细 |
| `settlement_modes` | 退款支付方式/卡券/储值回滚 |
| `patient_problem_list` | 退款申请、原因、协议 |
| `refund_audit` | 退费审核信息 |
| `refund_audit_auditing_reason` | 退费审核失败原因 |
## 9.2 PAC refund_record content
| PAC content 字段 | FRIDAY 候选 |
| --- | --- |
| `refund_external_id` | `patient_settlement.uuid` |
| `amount_cents` | abs(`patient_settlement.net_receipts_this`) / abs(sum(`settlement_modes.money`)) |
| `reason` | `patient_problem_list.reason` / `patient_problem_list.refuse_reason` / `refund_audit_auditing_reason.auditing_reason` |
| `related_payment_external_id` | `patient_settlement.ref_settlement_id` |
## 10. 充值/储值/福利卡一句话总结
充值/储值数据需要宿主提供 `transaction_records`,账户信息用 `patient_pre_deposit`,支付方式用 `pay_method`,福利卡权益用 `old_welfare` / `welfare_activity` / `welfare_activity_detail`
## 10.1 充值/储值/福利卡宿主需提供表
| 表 | 用途 |
| --- | --- |
| `transaction_records` | 预存款/储值交易流水 |
| `patient_pre_deposit` | 患者预存款账户 |
| `pay_method` | 充值支付方式 |
| `pay_records` | 预存款消费/退费关联 |
| `old_welfare` | 结算单绑定福利卡/储值卡 |
| `welfare_activity` | 福利活动 |
| `welfare_activity_detail` | 福利活动规则明细 |
## 10.2 PAC recharge_record content
| PAC content 字段 | FRIDAY 候选 |
| --- | --- |
| `recharge_external_id` | `transaction_records.id` |
| `amount_cents` | `transaction_records.principal_amount` + `transaction_records.donation_amount` |
| `card_type` | `transaction_records.pre_deposit_activity_name` / `old_welfare.card_type_name` / `old_welfare.card_name` |
## 11. 投诉一句话总结
投诉数据需要宿主提供 `complain_patient_handling`,跟进记录可补 `complain_follow_records`,处理人/权限可补 `complain_user_relationship`
## 11.1 投诉宿主需提供表
| 表 | 用途 |
| --- | --- |
| `complain_patient_handling` | 投诉主表 |
| `complain_follow_records` | 投诉跟进记录 |
| `complain_user_relationship` | 投诉单处理人/人员关系 |
| `complain_type_info` | 投诉分类/渠道/对象枚举 |
## 11.2 PAC complaint_record content
| PAC content 字段 | FRIDAY 候选 |
| --- | --- |
| `complaint_external_id` | `complain_patient_handling.id` / `complain_patient_handling.complain_number` |
| `host_status` | `complain_patient_handling.complain_type` |
| `category` | `complain_patient_handling.complain_classification` / `complain_patient_handling.complain_concerned` |
| `content` | `complain_patient_handling.complain_content` |
| `related_treatment_subject_id` | `complain_patient_handling.settlement_number` / `complain_patient_handling.complain_treatment` |
| `resolved_at` | `complain_follow_records.follow_date` where `follow_type=4` |
## 12. 咨询一句话总结
咨询数据需要宿主提供 `customer_consult`,治疗方案补 `customer_treatment_schedule`,预约咨询师/会诊时间补 `appointment_consultation`
## 12.1 咨询宿主需提供表
| 表 | 用途 |
| --- | --- |
| `customer_consult` | 客户咨询主体 |
| `customer_treatment_schedule` | 咨询治疗方案/预算 |
| `appointment_consultation` | 预约咨询师/会诊时间 |
| `appointment_base` | 回连预约与到诊 |
## 12.2 PAC consultation_record content
| PAC content 字段 | FRIDAY 候选 |
| --- | --- |
| `consultation_external_id` | `customer_consult.id` |
| `consult_date` | `customer_consult.created_gmt_at` / `appointment_consultation.consultation_date` |
| `source_channel` | `customer_consult.data_sources` |
| `intents` | `customer_consult.potential_treatment` / `customer_treatment_schedule.schedule_content` |
| `intent_categories` | `customer_consult.potential_treatment` / `customer_treatment_schedule.schedule_content` |
| `is_first_visit` | |
| `task_director` | `customer_consult.task_director` / `appointment_consultation.consultation_resource_name` |
| `doctor_id` | `customer_consult.task_director_id` / `appointment_consultation.consultation_resource_id` |
| `appointment_external_id` | `appointment_consultation.appointment_id` / `customer_consult.task_id` |
| `intent` | `customer_consult.potential_treatment` |
| `content` | `customer_consult.treatment_plan` / `customer_consult.other_conditions` / `customer_consult.unsettled_reason_description` |
## 13. 回访一句话总结
回访数据需要宿主提供 `customer_task` + `customer_return_visit`,计划型诊后回访可补 `return_visit_plan``return_visit_plan_patient``return_visit_plan_record`
## 13.1 回访宿主需提供表
| 表 | 用途 |
| --- | --- |
| `customer_task` | 回访任务主体/任务日期 |
| `customer_return_visit` | 回访内容/类型/结果 |
| `return_visit_plan` | 计划型诊后回访方案 |
| `return_visit_plan_patient` | 计划型回访患者 |
| `return_visit_plan_record` | 计划型回访执行记录 |
## 13.2 PAC patient_return_visit
| PAC 字段 | FRIDAY 候选 |
| --- | --- |
| `externalId` | `customer_return_visit.id` |
| `patientExternalId` | `customer_return_visit.customer_id` / `customer_task.customer_id` |
| `clinicId` | `customer_return_visit.organization_id` / `customer_task.organization_id` |
| `taskDate` | `customer_task.task_date` |
| `type` | `customer_return_visit.return_visit_type` |
| `status` | `customer_return_visit.return_visit_status` |
| `taskStatus` | `customer_task.task_status` |
| `treatmentItems` | `customer_return_visit.treatment_items` + `customer_return_visit.treatment_items_two` |
| `followContent` | `customer_return_visit.follow_content` |
| `result` | `customer_return_visit.return_visit_result` / `customer_task.remark` |
## 14. 主体关注表汇总
| 主体 | 需要关注的表 |
| --------- | ------------------------------------------------------------------------------------------------------------------- |
| 患者 | `customer_basic_info``customer_contacts``customer_label``customer_description` |
| 预约 | `appointment_base``appointment_complaint``appointment_cancel` |
| 接诊 | `visit_info` |
| EMR | `med_emr``med_emr_info``visit_info` |
| 诊断 | `med_emr_info``med_emr``visit_info``std_diag` |
| 治疗 | `therapy_plan``visit_therapy_order``visit_info``med_emr_info``std_treat` |
| 影像/影像分析 | `med_check``med_emr_check``med_emr``visit_info``std_check_class` |
| 收款 | `patient_settlement``patient_settlement_spec``settlement_modes``visit_info` |
| 退款 | `patient_settlement``patient_settlement_spec``settlement_modes``patient_problem_list` |
| 充值/储值/福利卡 | `transaction_records``patient_pre_deposit``pay_method``old_welfare``welfare_activity``welfare_activity_detail` |
| 投诉 | `complain_patient_handling``complain_follow_records``complain_user_relationship` |
| 咨询 | `customer_consult``customer_treatment_schedule``appointment_consultation``appointment_base` |
| 回访 | `customer_task``customer_return_visit``return_visit_plan``return_visit_plan_patient``return_visit_plan_record` |
...@@ -316,6 +316,13 @@ export function reconcileDiagnosisCode(code: string | null, nameZh: string | nul ...@@ -316,6 +316,13 @@ export function reconcileDiagnosisCode(code: string | null, nameZh: string | nul
/// 全库 treatment_record subtype 命中 1225 患者(正畸434/牙周267/种植103…)。 /// 全库 treatment_record subtype 命中 1225 患者(正畸434/牙周267/种植103…)。
export const TREATMENT_REFUSAL_SUBTYPE_PATTERNS = ['无意愿', '不愿', '拒绝'] as const; export const TREATMENT_REFUSAL_SUBTYPE_PATTERNS = ['无意愿', '不愿', '拒绝'] as const;
/// 影像拒绝(拒绝拍片 / 拒绝拍全景片 / 拒绝影像…)= 拒绝【拍片】≠ 拒绝【治疗】。
/// 判定 refusal 前先把这类影像拒绝短语抹掉再匹配 —— 否则"拒绝拍片但做了洁治"会被 `拒绝` 误当成
/// "拒绝该类治疗"从而错误压制召回(2026-07 数据核查:review 235 无害 + perio/种植/正畸等约 30 人真误伤)。
/// `拒绝` + ≤6 个非标点字符 + 影像名词(片/影像/X光/CBCT/全景/放射/曲面断层)。
export const TREATMENT_REFUSAL_IMAGING_EXCLUDE_RE =
'拒绝[^,。;;、]{0,6}(片|影像|X光|CBCT|全景|放射|曲面断层)';
export const NO_RESTORATION_GAP_EXAM_PATTERNS = [ export const NO_RESTORATION_GAP_EXAM_PATTERNS = [
'间隙关闭', '间隙关闭',
'间隙已关闭', '间隙已关闭',
......
...@@ -190,6 +190,9 @@ importers: ...@@ -190,6 +190,9 @@ importers:
class-validator: class-validator:
specifier: ^0.15.1 specifier: ^0.15.1
version: 0.15.1 version: 0.15.1
cron:
specifier: ^4.4.0
version: 4.4.0
csv-parse: csv-parse:
specifier: ^6.2.1 specifier: ^6.2.1
version: 6.2.1 version: 6.2.1
...@@ -12054,8 +12057,8 @@ snapshots: ...@@ -12054,8 +12057,8 @@ snapshots:
'@next/eslint-plugin-next': 16.2.4 '@next/eslint-plugin-next': 16.2.4
eslint: 9.39.4(jiti@2.7.0) eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10 eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0))
eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0))
...@@ -12077,7 +12080,7 @@ snapshots: ...@@ -12077,7 +12080,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)): eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)):
dependencies: dependencies:
'@nolyfill/is-core-module': 1.0.39 '@nolyfill/is-core-module': 1.0.39
debug: 4.4.3 debug: 4.4.3
...@@ -12088,22 +12091,22 @@ snapshots: ...@@ -12088,22 +12091,22 @@ snapshots:
tinyglobby: 0.2.16 tinyglobby: 0.2.16
unrs-resolver: 1.11.1 unrs-resolver: 1.11.1
optionalDependencies: optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)):
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
eslint: 9.39.4(jiti@2.7.0) eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10 eslint-import-resolver-node: 0.3.10
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)) eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)):
dependencies: dependencies:
'@rtsao/scc': 1.1.0 '@rtsao/scc': 1.1.0
array-includes: 3.1.9 array-includes: 3.1.9
...@@ -12114,7 +12117,7 @@ snapshots: ...@@ -12114,7 +12117,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 9.39.4(jiti@2.7.0) eslint: 9.39.4(jiti@2.7.0)
eslint-import-resolver-node: 0.3.10 eslint-import-resolver-node: 0.3.10
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))
hasown: 2.0.4 hasown: 2.0.4
is-core-module: 2.16.2 is-core-module: 2.16.2
is-glob: 4.0.3 is-glob: 4.0.3
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment