@n2world/orchestrator 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-os-rd.d.ts +100 -0
- package/dist/agent-os-rd.js +258 -0
- package/dist/audit-store.d.ts +14 -0
- package/dist/audit-store.js +107 -0
- package/dist/beta-runner.d.ts +95 -0
- package/dist/beta-runner.js +251 -0
- package/dist/beta.d.ts +102 -0
- package/dist/beta.js +180 -0
- package/dist/browser-agent.d.ts +90 -0
- package/dist/browser-agent.js +223 -0
- package/dist/channel-gateway.d.ts +74 -0
- package/dist/channel-gateway.js +270 -0
- package/dist/channels.d.ts +120 -0
- package/dist/channels.js +432 -0
- package/dist/chat-store.d.ts +29 -0
- package/dist/chat-store.js +120 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +607 -0
- package/dist/command-screen.d.ts +12 -0
- package/dist/command-screen.js +44 -0
- package/dist/commit-gate.d.ts +98 -0
- package/dist/commit-gate.js +258 -0
- package/dist/companion-api.d.ts +37 -0
- package/dist/companion-api.js +101 -0
- package/dist/conversation-graph.d.ts +39 -0
- package/dist/conversation-graph.js +92 -0
- package/dist/cost-estimator.d.ts +27 -0
- package/dist/cost-estimator.js +42 -0
- package/dist/cron-runner.d.ts +31 -0
- package/dist/cron-runner.js +46 -0
- package/dist/dashboard/chat.html +326 -0
- package/dist/dashboard/dental.html +58 -0
- package/dist/dashboard/freebie.png +0 -0
- package/dist/dashboard/icon-192.png +0 -0
- package/dist/dashboard/index.html +892 -0
- package/dist/dashboard/manifest.json +15 -0
- package/dist/dashboard/service-worker.js +28 -0
- package/dist/dashboard-server.d.ts +37 -0
- package/dist/dashboard-server.js +457 -0
- package/dist/dental-intake-service.d.ts +37 -0
- package/dist/dental-intake-service.js +61 -0
- package/dist/dental-metrics.d.ts +25 -0
- package/dist/dental-metrics.js +37 -0
- package/dist/docking.d.ts +36 -0
- package/dist/docking.js +73 -0
- package/dist/finance-mcts-candidate.d.ts +37 -0
- package/dist/finance-mcts-candidate.js +106 -0
- package/dist/finance-regulation-kr.d.ts +33 -0
- package/dist/finance-regulation-kr.js +104 -0
- package/dist/finance-workflow.d.ts +135 -0
- package/dist/finance-workflow.js +242 -0
- package/dist/gateway.d.ts +18 -0
- package/dist/gateway.js +123 -0
- package/dist/governance.d.ts +39 -0
- package/dist/governance.js +48 -0
- package/dist/governed-executor.d.ts +31 -0
- package/dist/governed-executor.js +63 -0
- package/dist/governed-llm.d.ts +41 -0
- package/dist/governed-llm.js +83 -0
- package/dist/gpu-bridge.d.ts +16 -0
- package/dist/gpu-bridge.js +53 -0
- package/dist/health.d.ts +47 -0
- package/dist/health.js +66 -0
- package/dist/identity-link.d.ts +32 -0
- package/dist/identity-link.js +98 -0
- package/dist/index.d.ts +184 -0
- package/dist/index.js +417 -0
- package/dist/integrations/emr-adapter.d.ts +41 -0
- package/dist/integrations/emr-adapter.js +63 -0
- package/dist/kakao-oauth.d.ts +16 -0
- package/dist/kakao-oauth.js +87 -0
- package/dist/knowledge-graph.d.ts +53 -0
- package/dist/knowledge-graph.js +156 -0
- package/dist/llm.d.ts +65 -0
- package/dist/llm.js +357 -0
- package/dist/mcp-client-guard.d.ts +32 -0
- package/dist/mcp-client-guard.js +179 -0
- package/dist/mcp-macaroon.d.ts +75 -0
- package/dist/mcp-macaroon.js +161 -0
- package/dist/mcts-kernel-bridge.d.ts +36 -0
- package/dist/mcts-kernel-bridge.js +99 -0
- package/dist/mcts-prior.d.ts +79 -0
- package/dist/mcts-prior.js +170 -0
- package/dist/model-router.d.ts +51 -0
- package/dist/model-router.js +75 -0
- package/dist/multi-axis-lift.d.ts +43 -0
- package/dist/multi-axis-lift.js +141 -0
- package/dist/net-guard.d.ts +39 -0
- package/dist/net-guard.js +141 -0
- package/dist/onboarding.d.ts +38 -0
- package/dist/onboarding.js +94 -0
- package/dist/oracle-anchored-search.d.ts +25 -0
- package/dist/oracle-anchored-search.js +50 -0
- package/dist/oracle.d.ts +22 -0
- package/dist/oracle.js +116 -0
- package/dist/p6-governance.d.ts +150 -0
- package/dist/p6-governance.js +252 -0
- package/dist/pairing.d.ts +22 -0
- package/dist/pairing.js +81 -0
- package/dist/personalization.d.ts +35 -0
- package/dist/personalization.js +73 -0
- package/dist/pglite-hnsw-bridge.d.ts +118 -0
- package/dist/pglite-hnsw-bridge.js +311 -0
- package/dist/pglite-store.d.ts +59 -0
- package/dist/pglite-store.js +180 -0
- package/dist/playbook.d.ts +79 -0
- package/dist/playbook.js +83 -0
- package/dist/playbooks/dental-intake.d.ts +20 -0
- package/dist/playbooks/dental-intake.js +112 -0
- package/dist/predictive-agent.d.ts +157 -0
- package/dist/predictive-agent.js +535 -0
- package/dist/prompt-optimizer.d.ts +18 -0
- package/dist/prompt-optimizer.js +104 -0
- package/dist/rate-limiter.d.ts +25 -0
- package/dist/rate-limiter.js +75 -0
- package/dist/safety-anneal.d.ts +83 -0
- package/dist/safety-anneal.js +153 -0
- package/dist/sandbox-controller.d.ts +12 -0
- package/dist/sandbox-controller.js +95 -0
- package/dist/satisfaction-metrics.d.ts +26 -0
- package/dist/satisfaction-metrics.js +61 -0
- package/dist/sensor-bridge.d.ts +53 -0
- package/dist/sensor-bridge.js +133 -0
- package/dist/session-repair.d.ts +27 -0
- package/dist/session-repair.js +66 -0
- package/dist/slack-finance-intake.d.ts +42 -0
- package/dist/slack-finance-intake.js +122 -0
- package/dist/symbolic-dynamics.d.ts +113 -0
- package/dist/symbolic-dynamics.js +420 -0
- package/dist/telemetry.d.ts +19 -0
- package/dist/telemetry.js +68 -0
- package/dist/text-embedding.d.ts +6 -0
- package/dist/text-embedding.js +42 -0
- package/dist/tier-classifier.d.ts +20 -0
- package/dist/tier-classifier.js +58 -0
- package/dist/tier-guard.d.ts +36 -0
- package/dist/tier-guard.js +56 -0
- package/dist/tui.d.ts +9 -0
- package/dist/tui.js +214 -0
- package/dist/update-security.d.ts +31 -0
- package/dist/update-security.js +112 -0
- package/dist/v-calibration.d.ts +16 -0
- package/dist/v-calibration.js +42 -0
- package/dist/value-calibration.d.ts +41 -0
- package/dist/value-calibration.js +133 -0
- package/dist/value-head.d.ts +20 -0
- package/dist/value-head.js +91 -0
- package/dist/wal-buffer.d.ts +23 -0
- package/dist/wal-buffer.js +144 -0
- package/dist/wiki-synthesizer.d.ts +80 -0
- package/dist/wiki-synthesizer.js +0 -0
- package/dist/worker-agent.d.ts +10 -0
- package/dist/worker-agent.js +19 -0
- package/package.json +65 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n2world Agent Dashboard",
|
|
3
|
+
"short_name": "n2world",
|
|
4
|
+
"start_url": "/index.html",
|
|
5
|
+
"display": "standalone",
|
|
6
|
+
"background_color": "#f4f7fa",
|
|
7
|
+
"theme_color": "#42a5f5",
|
|
8
|
+
"icons": [
|
|
9
|
+
{
|
|
10
|
+
"src": "icon-192.png",
|
|
11
|
+
"sizes": "192x192",
|
|
12
|
+
"type": "image/png"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const CACHE_NAME = 'n2world-dashboard-v1';
|
|
2
|
+
const ASSETS = [
|
|
3
|
+
'/',
|
|
4
|
+
'/index.html',
|
|
5
|
+
'/manifest.json',
|
|
6
|
+
'/icon-192.png',
|
|
7
|
+
'/freebie.png'
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
self.addEventListener('install', (e) => {
|
|
11
|
+
e.waitUntil(
|
|
12
|
+
caches.open(CACHE_NAME).then((cache) => {
|
|
13
|
+
return cache.addAll(ASSETS);
|
|
14
|
+
})
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
self.addEventListener('fetch', (e) => {
|
|
19
|
+
// Allow normal streaming fetch for SSE APIs
|
|
20
|
+
if (e.request.url.includes('/api/')) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
e.respondWith(
|
|
24
|
+
caches.match(e.request).then((cachedResponse) => {
|
|
25
|
+
return cachedResponse || fetch(e.request);
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { GovernanceController } from './governance';
|
|
2
|
+
import { GovernanceEventBus, ConstitutionalRollback, QuietTimeGate } from './p6-governance';
|
|
3
|
+
export declare class DashboardServer {
|
|
4
|
+
private port;
|
|
5
|
+
private server;
|
|
6
|
+
private sandbox;
|
|
7
|
+
private worker;
|
|
8
|
+
private telemetry;
|
|
9
|
+
/** 외부에서 거버넌스(킬스위치·예산·감사)를 주입해 상태 노출/제어 가능. */
|
|
10
|
+
governance: GovernanceController;
|
|
11
|
+
/** P6: 위반/차단 이벤트 버스(실데이터). MCP 인가·commit-gate·tier-egress 이벤트를 publish. */
|
|
12
|
+
governanceEvents: GovernanceEventBus;
|
|
13
|
+
/** P6: 헌법 롤백 — 이벤트 버스 구독, 위반 시 즉시 중단(killswitch). */
|
|
14
|
+
rollback: ConstitutionalRollback;
|
|
15
|
+
/** P6: 정온 시간 게이트 — 휴식 시간 백그라운드 능동성 억제. */
|
|
16
|
+
quietGate: QuietTimeGate;
|
|
17
|
+
/** 자연어 대화 히스토리 — 디스크 영속(다운돼도 보존·재시작 시 복원). CLI `chat` 과 같은 보존소. */
|
|
18
|
+
private chat;
|
|
19
|
+
/** P2 — 치과 접수 환자 원장(대조용, 로컬 전용·세션 지속). */
|
|
20
|
+
private dentalStore;
|
|
21
|
+
/**
|
|
22
|
+
* C1 — /api/* 접근 토큰. 비루프백(LAN) 접근 시 필수. 루프백(로컬 UI)은 불요.
|
|
23
|
+
* 환경변수 DASHBOARD_TOKEN 우선, 없으면 1회성 무작위 발급(시작 시 콘솔 표시).
|
|
24
|
+
*/
|
|
25
|
+
private authToken;
|
|
26
|
+
/** H3 — 컴패니언 로컬 API(루프백+토큰 *전송계층* 강제). 대시보드 토큰을 공유. */
|
|
27
|
+
private companion;
|
|
28
|
+
constructor(port: number);
|
|
29
|
+
/** P6: v2.x 완료 정의 종합 점검(실측 이벤트 기반, 베타 미가동 항목은 정직 unmet). */
|
|
30
|
+
dodReport(): import("./p6-governance").DoDReport;
|
|
31
|
+
start(): Promise<void>;
|
|
32
|
+
/** 실제 바인딩된 포트(port 0 으로 시작했으면 OS가 할당한 포트). */
|
|
33
|
+
getPort(): number;
|
|
34
|
+
/** C1 — /api/* 접근 토큰(비루프백 접근용). 운영자 본인에게만 노출. */
|
|
35
|
+
accessToken(): string;
|
|
36
|
+
stop(): Promise<void>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DashboardServer = void 0;
|
|
37
|
+
const http = __importStar(require("http"));
|
|
38
|
+
const fs = __importStar(require("fs/promises"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const crypto = __importStar(require("crypto"));
|
|
41
|
+
const net_guard_1 = require("./net-guard");
|
|
42
|
+
const companion_api_1 = require("./companion-api");
|
|
43
|
+
const index_1 = require("./index");
|
|
44
|
+
const worker_agent_1 = require("./worker-agent");
|
|
45
|
+
const llm_1 = require("./llm");
|
|
46
|
+
const governed_llm_1 = require("./governed-llm");
|
|
47
|
+
const dental_intake_service_1 = require("./dental-intake-service");
|
|
48
|
+
const dental_intake_1 = require("./playbooks/dental-intake");
|
|
49
|
+
const chat_store_1 = require("./chat-store");
|
|
50
|
+
const conversation_graph_1 = require("./conversation-graph");
|
|
51
|
+
const telemetry_1 = require("./telemetry");
|
|
52
|
+
const governance_1 = require("./governance");
|
|
53
|
+
const health_1 = require("./health");
|
|
54
|
+
const p6_governance_1 = require("./p6-governance");
|
|
55
|
+
class DashboardServer {
|
|
56
|
+
port;
|
|
57
|
+
server = null;
|
|
58
|
+
sandbox = null;
|
|
59
|
+
worker = null;
|
|
60
|
+
telemetry = new telemetry_1.TelemetrySource();
|
|
61
|
+
/** 외부에서 거버넌스(킬스위치·예산·감사)를 주입해 상태 노출/제어 가능. */
|
|
62
|
+
governance = new governance_1.GovernanceController();
|
|
63
|
+
/** P6: 위반/차단 이벤트 버스(실데이터). MCP 인가·commit-gate·tier-egress 이벤트를 publish. */
|
|
64
|
+
governanceEvents = new p6_governance_1.GovernanceEventBus();
|
|
65
|
+
/** P6: 헌법 롤백 — 이벤트 버스 구독, 위반 시 즉시 중단(killswitch). */
|
|
66
|
+
rollback = new p6_governance_1.ConstitutionalRollback(this.governance);
|
|
67
|
+
/** P6: 정온 시간 게이트 — 휴식 시간 백그라운드 능동성 억제. */
|
|
68
|
+
quietGate = new p6_governance_1.QuietTimeGate();
|
|
69
|
+
/** 자연어 대화 히스토리 — 디스크 영속(다운돼도 보존·재시작 시 복원). CLI `chat` 과 같은 보존소. */
|
|
70
|
+
chat = new chat_store_1.ChatStore();
|
|
71
|
+
/** P2 — 치과 접수 환자 원장(대조용, 로컬 전용·세션 지속). */
|
|
72
|
+
dentalStore = new dental_intake_1.PatientRecordStore();
|
|
73
|
+
/**
|
|
74
|
+
* C1 — /api/* 접근 토큰. 비루프백(LAN) 접근 시 필수. 루프백(로컬 UI)은 불요.
|
|
75
|
+
* 환경변수 DASHBOARD_TOKEN 우선, 없으면 1회성 무작위 발급(시작 시 콘솔 표시).
|
|
76
|
+
*/
|
|
77
|
+
authToken = process.env.DASHBOARD_TOKEN || crypto.randomBytes(24).toString('base64url');
|
|
78
|
+
/** H3 — 컴패니언 로컬 API(루프백+토큰 *전송계층* 강제). 대시보드 토큰을 공유. */
|
|
79
|
+
companion = new companion_api_1.CompanionApi(this.governance, this.authToken);
|
|
80
|
+
constructor(port) {
|
|
81
|
+
this.port = port;
|
|
82
|
+
this.rollback.attach(this.governanceEvents); // 위반 이벤트 구독
|
|
83
|
+
this.chat.load(); // 이전 대화 복원(다운 후 재시작해도 이어짐)
|
|
84
|
+
}
|
|
85
|
+
/** P6: v2.x 완료 정의 종합 점검(실측 이벤트 기반, 베타 미가동 항목은 정직 unmet). */
|
|
86
|
+
dodReport() {
|
|
87
|
+
const c = this.governanceEvents.counts();
|
|
88
|
+
return (0, p6_governance_1.v2DoDReport)({
|
|
89
|
+
betaUsersDelivered: 0, // 실사용자 인도 0 — 베타 미가동(정직)
|
|
90
|
+
realUserLiftMeasured: false, // 실사용자 라벨 부재 → (목표;미측정)
|
|
91
|
+
safetySeparatedReported: true,
|
|
92
|
+
tier0ExfilObserved: c.tier0ExfilEscaped, // 실측(0 이어야 함)
|
|
93
|
+
honestyHeld: true,
|
|
94
|
+
complexityWithinBudget: true,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async start() {
|
|
98
|
+
// Initialize sandbox and worker for actual command execution
|
|
99
|
+
this.sandbox = new index_1.LocalSandbox();
|
|
100
|
+
await this.sandbox.init();
|
|
101
|
+
this.worker = new worker_agent_1.WorkerAgent(this.sandbox);
|
|
102
|
+
// C1 — CORS: 기본은 *미설정*(same-origin). 교차출처(모바일 컴패니언)는 DASHBOARD_CORS_ORIGIN
|
|
103
|
+
// 으로 특정 오리진을 명시할 때만 허용. '*' 와일드카드 기본값 제거(무인증 교차출처 차단).
|
|
104
|
+
const corsOrigin = process.env.DASHBOARD_CORS_ORIGIN; // 미설정이면 ACAO 헤더를 보내지 않음
|
|
105
|
+
this.server = http.createServer(async (req, res) => {
|
|
106
|
+
if (corsOrigin) {
|
|
107
|
+
res.setHeader('Access-Control-Allow-Origin', corsOrigin);
|
|
108
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
109
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Dashboard-Token');
|
|
110
|
+
res.setHeader('Vary', 'Origin');
|
|
111
|
+
}
|
|
112
|
+
if (req.method === 'OPTIONS') {
|
|
113
|
+
res.writeHead(204);
|
|
114
|
+
res.end();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Direct root requests to index.html
|
|
118
|
+
const reqUrl = req.url || '/index.html';
|
|
119
|
+
// 쿼리스트링 제거 후 경로만(라우팅·정적파일 안전).
|
|
120
|
+
const reqPathOnly = reqUrl.split('?')[0];
|
|
121
|
+
const urlPath = reqPathOnly === '/' || reqPathOnly === '' ? '/index.html' : reqPathOnly;
|
|
122
|
+
// C1 — /api/* 접근 통제: 루프백(로컬 UI)은 허용, 비루프백(LAN/원격)은 토큰 필수.
|
|
123
|
+
// 명령실행(/api/run)·킬스위치(/api/governance)·PII(dental)·대화이력 모두 보호.
|
|
124
|
+
if (urlPath.startsWith('/api/')) {
|
|
125
|
+
const access = (0, net_guard_1.decideAccess)(req, this.authToken);
|
|
126
|
+
if (!access.ok) {
|
|
127
|
+
res.writeHead(401, {
|
|
128
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
129
|
+
'WWW-Authenticate': 'Bearer realm="n2world-dashboard"',
|
|
130
|
+
});
|
|
131
|
+
res.end(JSON.stringify({ error: 'unauthorized', reason: access.reason }));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Handle SSE API Endpoint
|
|
136
|
+
if (urlPath === '/api/telemetry') {
|
|
137
|
+
res.writeHead(200, {
|
|
138
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
139
|
+
'Cache-Control': 'no-cache',
|
|
140
|
+
'Connection': 'keep-alive'
|
|
141
|
+
});
|
|
142
|
+
const interval = setInterval(() => {
|
|
143
|
+
// D1: 진짜 시스템 메트릭(가짜 Math.random 제거).
|
|
144
|
+
const t = this.telemetry.sample();
|
|
145
|
+
const telemetryData = JSON.stringify({
|
|
146
|
+
cpu: t.cpuPercent,
|
|
147
|
+
memory: t.memUsedMB + 'MB',
|
|
148
|
+
memPercent: t.memPercent,
|
|
149
|
+
heap: t.heapUsedMB + 'MB',
|
|
150
|
+
load: t.loadAvg1,
|
|
151
|
+
tasks: t.activeTasks,
|
|
152
|
+
uptime: t.uptimeSec,
|
|
153
|
+
});
|
|
154
|
+
res.write(`data: ${telemetryData}\n\n`);
|
|
155
|
+
}, 500);
|
|
156
|
+
req.on('close', () => {
|
|
157
|
+
clearInterval(interval);
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// D2: 거버넌스 상태/제어 (GET 상태, POST kill/reset)
|
|
162
|
+
if (urlPath === '/api/governance') {
|
|
163
|
+
if (req.method === 'POST') {
|
|
164
|
+
let body = '';
|
|
165
|
+
req.on('data', (c) => { body += c; });
|
|
166
|
+
req.on('end', () => {
|
|
167
|
+
try {
|
|
168
|
+
const action = JSON.parse(body || '{}').action;
|
|
169
|
+
if (action === 'kill')
|
|
170
|
+
this.governance.kill('dashboard');
|
|
171
|
+
else if (action === 'reset')
|
|
172
|
+
this.governance.reset();
|
|
173
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
174
|
+
res.end(JSON.stringify(this.governance.status()));
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
178
|
+
res.end(JSON.stringify({ error: 'bad request' }));
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
184
|
+
res.end(JSON.stringify({ ...this.governance.status(), telemetry: this.telemetry.sample() }));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// P6: 위반/차단 이벤트 집계·최근 + 롤백 상태(실데이터 — Math.random 없음)
|
|
188
|
+
if (urlPath === '/api/governance-events') {
|
|
189
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
190
|
+
res.end(JSON.stringify({
|
|
191
|
+
counts: this.governanceEvents.counts(),
|
|
192
|
+
recent: this.governanceEvents.recent(50),
|
|
193
|
+
rollback: this.rollback.status(),
|
|
194
|
+
quiet: { config: this.quietGate.config(), now: this.quietGate.allowBackgroundAction(new Date(), 'normal') },
|
|
195
|
+
}));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// P6: v2.x 완료 정의 종합 점검(미충족은 정직 표기)
|
|
199
|
+
if (urlPath === '/api/dod') {
|
|
200
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
201
|
+
res.end(JSON.stringify(this.dodReport()));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// H3 — 컴패니언 로컬 API: loopback 은 *소켓 주소*로 판정(위조 불가), 토큰은 헤더/쿼리에서.
|
|
205
|
+
// (이미 위의 /api/* 게이트를 통과했지만, CompanionApi 가 한 번 더 루프백+토큰을 강제한다.)
|
|
206
|
+
if (urlPath === '/api/companion/status') {
|
|
207
|
+
const r = this.companion.status((0, net_guard_1.authContextFromRequest)(req));
|
|
208
|
+
res.writeHead(r.ok ? 200 : 401, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
209
|
+
res.end(JSON.stringify(r));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (urlPath === '/api/companion/kill' && req.method === 'POST') {
|
|
213
|
+
let body = '';
|
|
214
|
+
req.on('data', (c) => { body += c.toString(); });
|
|
215
|
+
req.on('end', () => {
|
|
216
|
+
let kill = true;
|
|
217
|
+
try {
|
|
218
|
+
kill = JSON.parse(body || '{}').kill !== false;
|
|
219
|
+
}
|
|
220
|
+
catch { /* 기본 kill */ }
|
|
221
|
+
const r = this.companion.setKill((0, net_guard_1.authContextFromRequest)(req), kill);
|
|
222
|
+
res.writeHead(r.ok ? 200 : 401, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
223
|
+
res.end(JSON.stringify(r));
|
|
224
|
+
});
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// 컴패니언 페어링 토큰 표시 — *루프백 전용*(원격은 토큰조차 못 받게 차단).
|
|
228
|
+
if (urlPath === '/api/companion/pairing-token') {
|
|
229
|
+
if (!(0, net_guard_1.isLoopbackRequest)(req)) {
|
|
230
|
+
res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
231
|
+
res.end(JSON.stringify({ error: 'forbidden', reason: '페어링 토큰은 루프백에서만 조회 가능' }));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
235
|
+
res.end(JSON.stringify({ token: this.companion.pairingToken() }));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
// P2 — 치과 접수·대조 폼 UI(/dental → dental.html).
|
|
239
|
+
if (urlPath === '/dental') {
|
|
240
|
+
try {
|
|
241
|
+
const content = await fs.readFile(path.join(__dirname, 'dashboard', 'dental.html'));
|
|
242
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
243
|
+
res.end(content);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
247
|
+
res.end('dental.html 을 찾을 수 없습니다(빌드 필요).');
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// 전용 대화 웹 UI(/chat → chat.html). 저장된 대화를 불러와 이어서 대화.
|
|
252
|
+
if (urlPath === '/chat') {
|
|
253
|
+
try {
|
|
254
|
+
const content = await fs.readFile(path.join(__dirname, 'dashboard', 'chat.html'));
|
|
255
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
256
|
+
res.end(content);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
260
|
+
res.end('chat.html 을 찾을 수 없습니다(빌드 필요).');
|
|
261
|
+
}
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
// 대화 복구 — 영속된 대화 히스토리 조회(다운 후에도 복원·복사 가능)
|
|
265
|
+
if (urlPath === '/api/chat-history') {
|
|
266
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
267
|
+
res.end(JSON.stringify({ count: this.chat.count(), turns: this.chat.history(), path: this.chat.path_() }));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// 사고 과정 그래프 — 대화에서 도출한 의미 그래프(실데이터, Graphify 계열)
|
|
271
|
+
if (urlPath === '/api/graph') {
|
|
272
|
+
const graph = (0, conversation_graph_1.buildConversationGraph)(this.chat.history());
|
|
273
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
274
|
+
res.end(JSON.stringify(graph));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
// D4: 헬스체크 (status: healthy/degraded/unhealthy + 실 텔레메트리)
|
|
278
|
+
if (urlPath === '/api/health') {
|
|
279
|
+
const health = new health_1.HealthCheck(this.telemetry, this.governance).check();
|
|
280
|
+
const code = health.status === 'unhealthy' ? 503 : 200;
|
|
281
|
+
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
282
|
+
res.end(JSON.stringify(health));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// Handle Run command API Endpoint (Real Execution)
|
|
286
|
+
// P2 — 치과 접수·대조 플레이북 실행(PII=로컬 강제). 운영자 본인 세션 → consent=true.
|
|
287
|
+
if (urlPath === '/api/playbook/dental/intake' && req.method === 'POST') {
|
|
288
|
+
let body = '';
|
|
289
|
+
req.on('data', (c) => { body += c.toString(); });
|
|
290
|
+
req.on('end', async () => {
|
|
291
|
+
try {
|
|
292
|
+
const payload = JSON.parse(body || '{}');
|
|
293
|
+
const runShell = async (cmd) => {
|
|
294
|
+
if (!this.worker)
|
|
295
|
+
return '';
|
|
296
|
+
const r = await this.worker.executeTask(cmd);
|
|
297
|
+
return `${r.stdout}\n${r.stderr}`;
|
|
298
|
+
};
|
|
299
|
+
const gov = new governed_llm_1.GovernedAgent((0, governed_llm_1.defaultGovernedBackends)(runShell));
|
|
300
|
+
const svc = new dental_intake_service_1.DentalIntakeService(gov, this.dentalStore);
|
|
301
|
+
const result = await svc.intake(payload, { consent: true });
|
|
302
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
303
|
+
res.end(JSON.stringify(result));
|
|
304
|
+
}
|
|
305
|
+
catch (e) {
|
|
306
|
+
// M3 — 상세는 서버 로그로만. 클라이언트엔 일반 메시지.
|
|
307
|
+
console.error('[dashboard] dental intake 처리 오류:', e);
|
|
308
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
309
|
+
res.end(JSON.stringify({ error: '요청 처리 실패(형식 확인)' }));
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (urlPath === '/api/run' && req.method === 'POST') {
|
|
315
|
+
let body = '';
|
|
316
|
+
req.on('data', chunk => {
|
|
317
|
+
body += chunk.toString();
|
|
318
|
+
});
|
|
319
|
+
req.on('end', async () => {
|
|
320
|
+
try {
|
|
321
|
+
const payload = JSON.parse(body);
|
|
322
|
+
const cmd = payload.command;
|
|
323
|
+
if (!cmd) {
|
|
324
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
325
|
+
res.end(JSON.stringify({ error: 'Missing command parameter' }));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (!this.worker) {
|
|
329
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
330
|
+
res.end(JSON.stringify({ error: 'Worker agent not initialized' }));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const normalizedCmd = cmd.trim();
|
|
334
|
+
const isNaturalLanguage = /^[ㄱ-ㅎㅏ-ㅣ가-힣\s\?\!\.₩]+$/.test(normalizedCmd) ||
|
|
335
|
+
(!normalizedCmd.includes(' ') && !/^[a-zA-Z0-9_-]+$/.test(normalizedCmd)) ||
|
|
336
|
+
(normalizedCmd.split(' ').length > 1 && !/^(dir|ls|cd|echo|n2world|node|npm|git|type|cat|mkdir|rm|del|copy|xcopy|move|ping|ipconfig|netstat)/i.test(normalizedCmd));
|
|
337
|
+
if (isNaturalLanguage) {
|
|
338
|
+
// 멀티턴 대화 — 이전 맥락을 기억하고 매 턴 디스크에 영속(다운돼도 보존).
|
|
339
|
+
this.chat.append({ role: 'user', content: normalizedCmd }); // 입력 즉시 저장
|
|
340
|
+
const runShell = async (shellCmd) => {
|
|
341
|
+
const res = await this.worker.executeTask(shellCmd);
|
|
342
|
+
return `[Command: ${shellCmd}]\nStdout:\n${res.stdout}\nStderr:\n${res.stderr}\nSuccess: ${res.success}\n`;
|
|
343
|
+
};
|
|
344
|
+
// H2 — Tier 거버넌스 *기본 ON*(보안 하드닝). 민감=Tier-0 로컬 강제, PII 외부 송신 차단.
|
|
345
|
+
// 명시적 비활성화만 우회 허용: N2WORLD_TIER_GUARD=0|false|off (운영자 책임).
|
|
346
|
+
let agentResponse;
|
|
347
|
+
const guardOff = /^(0|false|off)$/i.test(process.env.N2WORLD_TIER_GUARD || '');
|
|
348
|
+
if (!guardOff) {
|
|
349
|
+
const gov = new governed_llm_1.GovernedAgent((0, governed_llm_1.defaultGovernedBackends)(runShell), { baselineTier: 1 });
|
|
350
|
+
agentResponse = (await gov.respond(this.chat.history(), { consent: true })).text;
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
agentResponse = await (0, llm_1.askAgentChat)(this.chat.history(), runShell);
|
|
354
|
+
}
|
|
355
|
+
this.chat.append({ role: 'assistant', content: agentResponse }); // 응답 즉시 저장
|
|
356
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
357
|
+
res.end(JSON.stringify({
|
|
358
|
+
stdout: agentResponse,
|
|
359
|
+
stderr: '',
|
|
360
|
+
success: true
|
|
361
|
+
}));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const result = await this.worker.executeTask(cmd);
|
|
365
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
366
|
+
res.end(JSON.stringify({
|
|
367
|
+
stdout: result.stdout,
|
|
368
|
+
stderr: result.stderr,
|
|
369
|
+
success: result.success
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
// M3 — 내부 상세(스택·경로)는 서버 로그로만. 클라이언트엔 일반 메시지.
|
|
374
|
+
console.error('[dashboard] /api/run 처리 오류:', err);
|
|
375
|
+
res.writeHead(500, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
376
|
+
res.end(JSON.stringify({ error: '내부 처리 오류' }));
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
// C1 — 경로 순회(LFI) 방지: dashboard 디렉터리 밖으로 못 나가게 정규화 후 경계 검사.
|
|
382
|
+
const dashDir = path.join(__dirname, 'dashboard');
|
|
383
|
+
const resolvedPath = path.resolve(dashDir, '.' + path.posix.normalize('/' + urlPath));
|
|
384
|
+
if (resolvedPath !== dashDir && !resolvedPath.startsWith(dashDir + path.sep)) {
|
|
385
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
386
|
+
res.end('잘못된 경로 요청.');
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
const content = await fs.readFile(resolvedPath);
|
|
391
|
+
let contentType = 'text/html; charset=utf-8';
|
|
392
|
+
if (urlPath.endsWith('.js')) {
|
|
393
|
+
contentType = 'application/javascript; charset=utf-8';
|
|
394
|
+
}
|
|
395
|
+
else if (urlPath.endsWith('.css')) {
|
|
396
|
+
contentType = 'text/css; charset=utf-8';
|
|
397
|
+
}
|
|
398
|
+
else if (urlPath.endsWith('.png')) {
|
|
399
|
+
contentType = 'image/png';
|
|
400
|
+
}
|
|
401
|
+
else if (urlPath.endsWith('.json')) {
|
|
402
|
+
contentType = 'application/json; charset=utf-8';
|
|
403
|
+
}
|
|
404
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
405
|
+
res.end(content);
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
409
|
+
res.end('지정한 대시보드 리소스를 찾을 수 없습니다.');
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
// C1 — 기본 바인딩 127.0.0.1(로컬 전용). LAN 노출은 DASHBOARD_HOST=0.0.0.0 로 *명시적* 선택.
|
|
413
|
+
const host = process.env.DASHBOARD_HOST || '127.0.0.1';
|
|
414
|
+
const loopbackOnly = host === '127.0.0.1' || host === '::1' || host === 'localhost';
|
|
415
|
+
return new Promise((resolve, reject) => {
|
|
416
|
+
const srv = this.server;
|
|
417
|
+
const onError = (err) => { srv.removeListener('error', onError); reject(err); };
|
|
418
|
+
srv.once('error', onError); // EADDRINUSE 등을 정직히 reject(미처리 throw 방지)
|
|
419
|
+
srv.listen(this.port, host, () => {
|
|
420
|
+
srv.removeListener('error', onError);
|
|
421
|
+
const addr = srv.address();
|
|
422
|
+
if (addr && typeof addr === 'object' && addr.port)
|
|
423
|
+
this.port = addr.port; // port 0 → 실제 에페메럴 포트
|
|
424
|
+
if (!loopbackOnly) {
|
|
425
|
+
// LAN 노출 시: 비루프백 접근은 토큰 필수임을 운영자에게 고지(자동발급 토큰 표시).
|
|
426
|
+
console.warn(`[dashboard] ⚠ 비루프백(${host}) 바인딩 — 원격 /api/* 접근은 토큰 필수.`);
|
|
427
|
+
if (!process.env.DASHBOARD_TOKEN) {
|
|
428
|
+
console.warn(`[dashboard] 접근 토큰(이번 세션): ${this.authToken}`);
|
|
429
|
+
console.warn(`[dashboard] 사용: Authorization: Bearer <토큰> 또는 ?token=<토큰>`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
resolve();
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
/** 실제 바인딩된 포트(port 0 으로 시작했으면 OS가 할당한 포트). */
|
|
437
|
+
getPort() {
|
|
438
|
+
return this.port;
|
|
439
|
+
}
|
|
440
|
+
/** C1 — /api/* 접근 토큰(비루프백 접근용). 운영자 본인에게만 노출. */
|
|
441
|
+
accessToken() {
|
|
442
|
+
return this.authToken;
|
|
443
|
+
}
|
|
444
|
+
async stop() {
|
|
445
|
+
if (this.server) {
|
|
446
|
+
await new Promise((resolve) => {
|
|
447
|
+
this.server.close(() => {
|
|
448
|
+
resolve();
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
if (this.sandbox) {
|
|
453
|
+
await this.sandbox.destroy();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
exports.DashboardServer = DashboardServer;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { PatientRecordStore, DentalIntake } from './playbooks/dental-intake';
|
|
2
|
+
import { GovernedAgent } from './governed-llm';
|
|
3
|
+
export interface IntakeStepView {
|
|
4
|
+
id: string;
|
|
5
|
+
provider: 'local' | 'external' | 'none';
|
|
6
|
+
text: string;
|
|
7
|
+
tier: number;
|
|
8
|
+
}
|
|
9
|
+
export interface IntakeResponse {
|
|
10
|
+
ok: boolean;
|
|
11
|
+
tier: number;
|
|
12
|
+
missingFields: string[];
|
|
13
|
+
flags: {
|
|
14
|
+
id: string;
|
|
15
|
+
flag: string;
|
|
16
|
+
severity: 'block' | 'warn';
|
|
17
|
+
}[];
|
|
18
|
+
steps: IntakeStepView[];
|
|
19
|
+
humanInLoopRequired: boolean;
|
|
20
|
+
piiKeptLocal: boolean;
|
|
21
|
+
tier0ExternalAllowed: number;
|
|
22
|
+
metrics: {
|
|
23
|
+
ms: number;
|
|
24
|
+
llmCalls: number;
|
|
25
|
+
};
|
|
26
|
+
notice: string;
|
|
27
|
+
}
|
|
28
|
+
export declare class DentalIntakeService {
|
|
29
|
+
private gov;
|
|
30
|
+
private store;
|
|
31
|
+
private rt;
|
|
32
|
+
constructor(gov: GovernedAgent, store?: PatientRecordStore);
|
|
33
|
+
intake(body: Partial<DentalIntake>, opts?: {
|
|
34
|
+
consent?: boolean;
|
|
35
|
+
}): Promise<IntakeResponse>;
|
|
36
|
+
recordCount(): number;
|
|
37
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// P2 — 치과 접수·대조 서비스 (앱 v1.2 §4.2) — 실행 UI의 백엔드 핵심
|
|
4
|
+
// ----------------------------------------------------------------------------
|
|
5
|
+
// 이미 구현된 PlaybookRuntime + dental-intake 플레이북에 *요청/응답 어댑터*를 결선한다.
|
|
6
|
+
// UI(폼/결과 카드)가 소비할 JSON을 만든다. PII는 코어 TierGuard 가 로컬로 강제한다.
|
|
7
|
+
//
|
|
8
|
+
// 정직 고지(제1계명): 런타임/대조 로직 재구현 금지(소비만). LLM 단계는 GovernedAgent
|
|
9
|
+
// (주입형/실 백엔드)로 라우팅 — PII 외부 송신 0/N. 가짜 성공 없음.
|
|
10
|
+
// ============================================================================
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DentalIntakeService = void 0;
|
|
13
|
+
const playbook_1 = require("./playbook");
|
|
14
|
+
const dental_intake_1 = require("./playbooks/dental-intake");
|
|
15
|
+
function normalize(body) {
|
|
16
|
+
const s = (v) => (v == null ? '' : String(v).trim());
|
|
17
|
+
return {
|
|
18
|
+
name: s(body.name), dob: s(body.dob), phone: s(body.phone),
|
|
19
|
+
insuranceId: body.insuranceId ? s(body.insuranceId) : undefined,
|
|
20
|
+
appointmentDate: body.appointmentDate ? s(body.appointmentDate) : undefined,
|
|
21
|
+
provider: body.provider ? s(body.provider) : undefined,
|
|
22
|
+
chiefComplaint: body.chiefComplaint ? s(body.chiefComplaint) : undefined,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
class DentalIntakeService {
|
|
26
|
+
gov;
|
|
27
|
+
store;
|
|
28
|
+
rt;
|
|
29
|
+
constructor(gov, store = new dental_intake_1.PatientRecordStore()) {
|
|
30
|
+
this.gov = gov;
|
|
31
|
+
this.store = store;
|
|
32
|
+
this.rt = new playbook_1.PlaybookRuntime(gov);
|
|
33
|
+
}
|
|
34
|
+
async intake(body, opts = {}) {
|
|
35
|
+
const rec = normalize(body);
|
|
36
|
+
const r = await this.rt.run(dental_intake_1.dentalIntakePlaybook, rec, this.store, opts);
|
|
37
|
+
let notice;
|
|
38
|
+
if (r.missingFields.length > 0)
|
|
39
|
+
notice = `필수 정보가 비어 있습니다: ${r.missingFields.join(', ')}`;
|
|
40
|
+
else if (r.humanInLoopRequired)
|
|
41
|
+
notice = '⚠ 임상 관련 내용 — 치과의 확인이 필요합니다(자동 처리 보류).';
|
|
42
|
+
else if (r.flags.some((f) => f.severity === 'block'))
|
|
43
|
+
notice = '대조 결과 차단 항목이 있습니다.';
|
|
44
|
+
else
|
|
45
|
+
notice = '접수·대조 완료. 직원 요약/환자 안내가 생성되었습니다.';
|
|
46
|
+
return {
|
|
47
|
+
ok: r.ok,
|
|
48
|
+
tier: r.overallTier,
|
|
49
|
+
missingFields: r.missingFields,
|
|
50
|
+
flags: r.flags,
|
|
51
|
+
steps: r.steps.map((s) => ({ id: s.id, provider: s.provider, text: s.text, tier: s.tier })),
|
|
52
|
+
humanInLoopRequired: r.humanInLoopRequired,
|
|
53
|
+
piiKeptLocal: r.blockedExternalForPii,
|
|
54
|
+
tier0ExternalAllowed: this.gov.guardRef().tier0ExternalAllowed(),
|
|
55
|
+
metrics: r.metrics,
|
|
56
|
+
notice,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
recordCount() { return this.store.count(); }
|
|
60
|
+
}
|
|
61
|
+
exports.DentalIntakeService = DentalIntakeService;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface IntakeEvent {
|
|
2
|
+
baselineMs: number;
|
|
3
|
+
actualMs: number;
|
|
4
|
+
completed: boolean;
|
|
5
|
+
flagHit: boolean;
|
|
6
|
+
satisfaction?: 1 | 2 | 3 | 4 | 5;
|
|
7
|
+
}
|
|
8
|
+
export interface BenefitReport {
|
|
9
|
+
n: number;
|
|
10
|
+
timeSavedMsMean: number;
|
|
11
|
+
timeSavedPct: number;
|
|
12
|
+
completionRate: number;
|
|
13
|
+
flagHitRate: number;
|
|
14
|
+
satisfactionMean: number | null;
|
|
15
|
+
}
|
|
16
|
+
export declare class DentalMetrics {
|
|
17
|
+
private events;
|
|
18
|
+
record(e: IntakeEvent): void;
|
|
19
|
+
report(): BenefitReport;
|
|
20
|
+
}
|
|
21
|
+
/** 유닛 이코노믹스 게이트: LTV/CAC ≥ 3 인가(파일럿 검증). */
|
|
22
|
+
export declare function ltvCacGate(ltv: number, cac: number, threshold?: number): {
|
|
23
|
+
pass: boolean;
|
|
24
|
+
ratio: number;
|
|
25
|
+
};
|