Commit f0a34afd by luoqi

refactor(admin): host 管理只走 CLI — 删跨 host 裸 API + 清理自管页冗余字段

删 HostsAdminController(/admin/hosts 列表/改任意 host 的裸 API,无 UI 在用):host 管理只走
pnpm pac:host CLI + 自管页 /admin/host/self(按 token hostId 自动 scope,碰不到别家)→ 多 host
天然隔离、免拆权限。自管页去掉 Timezone/Amount Unit/Pull endpoints(真实配置在 manifest.yaml,
DB pullConfig 这几项无人消费、会误导),保留凭据轮换/激活停用/运行状态/最近事件。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent e13e526f
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { HostsAdminController } from './hosts.controller'; // 跨 host 的 /admin/hosts 裸 API 已删:host 管理只走 CLI(pnpm pac:host)+ 自管页(/admin/host/self,
// 按 token 的 hostId 自动 scope,碰不到别家)。HostsService 仍由 CLI / host-self 复用。
import { HostSelfAdminController } from './host-self.controller'; import { HostSelfAdminController } from './host-self.controller';
import { AiInvocationsAdminController } from './ai-invocations.controller'; import { AiInvocationsAdminController } from './ai-invocations.controller';
import { MappingMissAdminController } from './mapping-miss.controller'; import { MappingMissAdminController } from './mapping-miss.controller';
...@@ -9,7 +10,6 @@ import { MappingMissService } from './mapping-miss.service'; ...@@ -9,7 +10,6 @@ import { MappingMissService } from './mapping-miss.service';
@Module({ @Module({
controllers: [ controllers: [
HostsAdminController,
HostSelfAdminController, HostSelfAdminController,
AiInvocationsAdminController, AiInvocationsAdminController,
MappingMissAdminController, MappingMissAdminController,
......
import {
Body,
Controller,
Get,
Param,
Patch,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { ZodResponse } from 'nestjs-zod';
import { Permission } from '@pac/types';
import { RequirePermission } from '../../common/decorators/permissions.decorator';
import {
ListHostsResponseDto,
HostSummaryDto,
UpdateHostRequestDto,
} from './dto/admin.dto';
import { HostsService } from './hosts.service';
/**
* Admin Platforms — **只读 + 受限更新**(write 操作走 CLI)。
*
* 设计:敏感写操作(create / rotate-secret / rotate-push / deactivate)只走
* `pnpm pac:host <cmd>` CLI(SSH 到 PAC 服务器 + DB 直连)。
*
* 这里只保留:
* - GET /admin/hosts 列表(将来 web dashboard 查看)
* - GET /admin/hosts/:id 详情
* - PATCH /admin/hosts/:id 受限更新(actionUrls / pullConfig / active 软停)
*
* 不保留(已迁移到 CLI):
* - POST /admin/hosts 注册新 host
* - POST /admin/hosts/:id/rotate-secret
* - POST /admin/hosts/:id/rotate-push-secret
* - POST /admin/hosts/:id/deactivate(active=false 通过 PATCH 也可走,这里也提供)
*/
@ApiTags('admin')
@ApiBearerAuth('accessToken')
@Controller('admin/hosts')
@RequirePermission(Permission.PLATFORM_MANAGE)
export class HostsAdminController {
constructor(private readonly platforms: HostsService) {}
@Get()
@ZodResponse({ status: 200, type: ListHostsResponseDto })
list() {
return this.platforms.list();
}
@Get(':id')
@ZodResponse({ status: 200, type: HostSummaryDto })
get(@Param('id') id: string) {
return this.platforms.get(id);
}
@Patch(':id')
@ZodResponse({ status: 200, type: HostSummaryDto })
@ApiOperation({
summary: 'Update host metadata / config (PATCH — actionUrls / pullConfig / active 软停)',
description:
'敏感操作(create / rotate / deactivate)走 `pnpm pac:host` CLI;' +
'本接口只支持改 actionUrls / pullConfig / active 三类非密钥字段。',
})
update(@Param('id') id: string, @Body() dto: UpdateHostRequestDto) {
return this.platforms.update(id, dto);
}
}
...@@ -7,7 +7,6 @@ import { ...@@ -7,7 +7,6 @@ import {
HostActionKey, HostActionKey,
HOST_ACTION_META, HOST_ACTION_META,
type HostDetail, type HostDetail,
type PullConfig,
type ActionUrlsConfig, type ActionUrlsConfig,
} from '@pac/types'; } from '@pac/types';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
...@@ -264,17 +263,6 @@ export function HostAdminApp({ data, onRefresh }: { data: HostDetail; onRefresh: ...@@ -264,17 +263,6 @@ export function HostAdminApp({ data, onRefresh }: { data: HostDetail; onRefresh:
</CardContent> </CardContent>
</Card> </Card>
{/* Pull 配置 */}
<PullConfigCard
config={data.pullConfig}
busy={busy}
confirm={confirm}
onSaved={() => {
onRefresh();
toast.success('Pull 配置已保存');
}}
onError={(msg) => toast.error('保存失败', { description: msg })}
/>
{/* Action URLs */} {/* Action URLs */}
<ActionUrlsCard <ActionUrlsCard
...@@ -391,181 +379,6 @@ function StatsCard({ data }: { data: HostDetail }) { ...@@ -391,181 +379,6 @@ function StatsCard({ data }: { data: HostDetail }) {
); );
} }
function PullConfigCard({
config,
busy,
confirm,
onSaved,
onError,
}: {
config: PullConfig | null;
busy: boolean;
confirm: (o: { title: string; description?: React.ReactNode; confirmText?: string; danger?: boolean }) => Promise<boolean>;
onSaved: () => void;
onError: (msg: string) => void;
}) {
const defaultConfig: PullConfig = {
endpoints: {},
amountUnit: 'yuan',
timezone: 'Asia/Shanghai',
intervalMinutes: 5,
};
const [draft, setDraft] = useState<PullConfig>(config ?? defaultConfig);
const [saving, setSaving] = useState(false);
const dirty = JSON.stringify(draft) !== JSON.stringify(config);
const dangerousChanged =
!!config &&
(draft.amountUnit !== config.amountUnit || draft.timezone !== config.timezone);
const save = async () => {
if (dangerousChanged) {
const ok = await confirm({
title: '修改将影响历史数据解读',
description: (
<>
你修改了 <code>amountUnit</code><code>timezone</code>
<br />
已入库的金额/时间按旧值已固化,前后不一致会让 LTV / 时间窗等指标失真。
<br />
仅在宿主真切换了这两个语义时操作。
</>
),
confirmText: '我已确认,继续保存',
danger: true,
});
if (!ok) return;
}
setSaving(true);
try {
await hostApi.update({ pullConfig: draft });
onSaved();
} catch (e) {
onError(e instanceof Error ? e.message : String(e));
} finally {
setSaving(false);
}
};
return (
<Card>
<CardHeader>
<CardTitle>Pull 配置</CardTitle>
<CardDescription>PAC 主动调宿主 API 拉数据的配置;空 = 不启用 pull</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-3 gap-3">
<Field label="Amount Unit ⚠️">
<Select
value={draft.amountUnit}
onValueChange={(v) => setDraft({ ...draft, amountUnit: v as 'yuan' | 'fen' })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="yuan">yuan</SelectItem>
<SelectItem value="fen">fen</SelectItem>
</SelectContent>
</Select>
</Field>
<Field label="Timezone ⚠️">
<Input
value={draft.timezone}
onChange={(e) => setDraft({ ...draft, timezone: e.target.value })}
className="font-mono"
/>
</Field>
<Field label="Interval (min)">
<Input
type="number"
min={1}
max={1440}
value={draft.intervalMinutes}
onChange={(e) =>
setDraft({ ...draft, intervalMinutes: Math.max(1, Number(e.target.value) || 5) })
}
className="nums"
/>
</Field>
</div>
<div className="text-[10.5px] text-amber-600">
⚠️ 修改 Amount Unit / Timezone 会**改变历史数据解读**,非必要不改(保存时二次确认)。
</div>
<div>
<div className="text-xs text-muted-foreground mb-1">Endpoints</div>
<EndpointEditor
endpoints={draft.endpoints}
onChange={(endpoints) => setDraft({ ...draft, endpoints })}
/>
</div>
<div className="flex justify-end gap-2 pt-2 border-t">
<Button
variant="ghost"
size="sm"
onClick={() => setDraft(config ?? defaultConfig)}
disabled={!dirty || saving || busy}
>
重置
</Button>
<Button onClick={save} disabled={!dirty || saving || busy} size="sm">
{saving ? '保存中…' : '保存'}
</Button>
</div>
</CardContent>
</Card>
);
}
const ENDPOINT_KEYS: Array<keyof PullConfig['endpoints']> = [
'patients',
'appointments',
'consultations',
'visit_registrations',
'visit_receptions',
'clinical_encounters',
'emr_documents',
'charge_orders',
'payments',
'refunds',
'image_studies',
'users',
];
function EndpointEditor({
endpoints,
onChange,
}: {
endpoints: PullConfig['endpoints'];
onChange: (next: PullConfig['endpoints']) => void;
}) {
return (
<div className="space-y-1">
{ENDPOINT_KEYS.map((k) => (
<div key={k} className="flex items-center gap-2">
<div className="w-36 text-xs text-muted-foreground font-mono">{k}</div>
<Input
compact
value={endpoints[k] ?? ''}
placeholder={`https://host.example.com/api/${k}`}
onChange={(e) => {
const v = e.target.value.trim();
const next = { ...endpoints };
if (v) (next as Record<string, string>)[k] = v;
else delete (next as Record<string, string | undefined>)[k];
onChange(next);
}}
className="font-mono"
/>
</div>
))}
</div>
);
}
function ActionUrlsCard({ function ActionUrlsCard({
urls, urls,
...@@ -703,14 +516,6 @@ function ActionUrlsCard({ ...@@ -703,14 +516,6 @@ function ActionUrlsCard({
); );
} }
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div>
<div className="text-xs text-muted-foreground mb-1">{label}</div>
{children}
</div>
);
}
function KindPill({ kind }: { kind: string }) { function KindPill({ kind }: { kind: string }) {
const tones: Record<string, string> = { const tones: Record<string, string> = {
......
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