Files
Neo-ZQYY/apps/admin-web/src/pages/AITriggers.tsx
Neo caf179a5da feat: 2026-04-15~05-02 累积变更基线 — AI 重构 + Runtime Context + DWS 修复
涵盖(每条对应已存的审计记录):
- AI 模块拆分:apps/backend/app/ai/apps -> prompts/(8 个 APP + app2a 派生)
  audit: 2026-04-20__ai-module-complete.md
- admin-web AI 管理套件:AIDashboard / AIOperations / AIRunLogs / AITriggers / TriggerManager
  audit: 2026-04-21__admin-web-ai-management-suite.md
- App2 财务洞察 prompt v3 -> v5.1 + 小程序 AI 接入(chat / board-finance)
  audit: 2026-04-22__app2_prompt_v5_1_and_miniprogram_ai_insight.md
- App2 prewarm 全过滤器 + AI 触发器 cron reschedule
  audit: 2026-04-21__app2-finance-prewarm-all-filters.md
  migration: 20260420_ai_trigger_jobs_and_app2_prewarm.sql / 20260421_app2_prewarm_cron_reschedule.sql
- AppType 联合类型对齐 + adminAiAppTypes.test.ts
  audit: 2026-04-30__admin_web_ai_app_type_alignment.md
- DashScope tokens_used 提取修复
  audit: 2026-04-30__backend_dashscope_tokens_used_extraction.md
- App3 线索完整详情 prompt
  audit: 2026-05-01__backend_app3_full_detail_prompt.md
- Runtime Context 沙箱(5-1~5-2 主线):
  - 后端 schema/service + admin_runtime_context / xcx_runtime_clock 两个 router
  - admin-web RuntimeContext.tsx + miniprogram runtime-clock.ts
  - migration: 20260501__runtime_context_sandbox.sql
  - tools/db/verify_admin_web_sandbox.py + verify_sandbox_end_to_end.py
  - database/changes: 7 份 sandbox_* 验证报告
- 飞球 DWS 修复:finance_area_daily 区域汇总 + task_engine 调整
  + RLS 视图业务日上界(migration 20260502 + scripts/ops/gen_rls_business_date_migration.py)

合规:
- .gitignore 启用 tmp/ 排除
- 不入仓:apps/etl/connectors/feiqiu/.env(API_TOKEN secret,本地修改保留)

待验证清单:
- docs/audit/changes/2026-05-04__cumulative_baseline_pending_verification.md
  每个主题的功能完整性 / 上线验证几乎都未收口,按优先级 P0~P3 逐一处理
2026-05-04 02:30:19 +08:00

244 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AI 触发器设置页面。
*
* 管理 biz.trigger_jobs 表中 job_type='ai_*' 的所有触发器,支持:
* - 启用/禁用
* - 修改 cron 表达式(仅 cron 类型)
* - 修改描述
* - 查看事件名、最近运行、下次运行、最后错误
*/
import React, { useCallback, useEffect, useState } from "react";
import {
Card, Table, Tag, Button, Space, Modal, Input, Switch,
message, Typography, Tooltip, Descriptions,
} from "antd";
import { ReloadOutlined, EditOutlined } from "@ant-design/icons";
import type { ColumnsType } from "antd/es/table";
import {
listTriggers, updateTrigger,
type TriggerItem,
} from "../api/adminAI";
const { Title, Paragraph } = Typography;
const STATUS_COLOR: Record<string, string> = {
enabled: "success",
disabled: "default",
};
const CONDITION_COLOR: Record<string, string> = {
event: "processing",
cron: "warning",
interval: "cyan",
};
function fmtTime(raw: string | null): string {
if (!raw) return "—";
const d = new Date(raw);
return Number.isNaN(d.getTime()) ? raw : d.toLocaleString("zh-CN");
}
function cronExpr(item: TriggerItem): string {
const cfg = item.trigger_config || {};
return String(cfg.cron_expression || cfg.event_name || "—");
}
const AITriggers: React.FC = () => {
const [items, setItems] = useState<TriggerItem[]>([]);
const [loading, setLoading] = useState(false);
const [editing, setEditing] = useState<TriggerItem | null>(null);
const [editCron, setEditCron] = useState("");
const [editDesc, setEditDesc] = useState("");
const [saving, setSaving] = useState(false);
const load = useCallback(async () => {
setLoading(true);
try {
const res = await listTriggers();
setItems(res);
} catch {
message.error("加载触发器列表失败");
} finally {
setLoading(false);
}
}, []);
useEffect(() => { load(); }, [load]);
const handleToggle = async (item: TriggerItem, next: boolean) => {
try {
await updateTrigger(item.id, { status: next ? "enabled" : "disabled" });
message.success(next ? "已启用" : "已禁用");
load();
} catch {
message.error("状态切换失败");
}
};
const openEdit = (item: TriggerItem) => {
setEditing(item);
setEditCron(String(item.trigger_config?.cron_expression || ""));
setEditDesc(item.description || "");
};
const handleSave = async () => {
if (!editing) return;
setSaving(true);
try {
const body: { cron_expression?: string; description?: string } = {};
if (editing.trigger_condition === "cron" && editCron !== editing.trigger_config?.cron_expression) {
body.cron_expression = editCron;
}
if (editDesc !== (editing.description || "")) {
body.description = editDesc;
}
if (Object.keys(body).length === 0) {
message.info("无变更");
setEditing(null);
return;
}
await updateTrigger(editing.id, body);
message.success("已保存");
setEditing(null);
load();
} catch (err) {
const msg = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail;
message.error(`保存失败${msg ? `${msg}` : ""}`);
} finally {
setSaving(false);
}
};
const columns: ColumnsType<TriggerItem> = [
{ title: "ID", dataIndex: "id", key: "id", width: 60 },
{
title: "触发器名", dataIndex: "job_name", key: "job_name", width: 200,
render: (v: string, r) => (
<div>
<div style={{ fontWeight: 500 }}>{v}</div>
{r.description && (
<div style={{ fontSize: 12, color: "#888", marginTop: 2 }}>{r.description}</div>
)}
</div>
),
},
{
title: "类型", dataIndex: "trigger_condition", key: "trigger_condition", width: 80,
render: (v: string) => <Tag color={CONDITION_COLOR[v] ?? "default"}>{v}</Tag>,
},
{
title: "表达式 / 事件", key: "expr", width: 240,
render: (_: unknown, r) => (
<code style={{ fontSize: 12 }}>{cronExpr(r)}</code>
),
},
{
title: "状态", dataIndex: "status", key: "status", width: 100,
render: (v: string, r) => (
<Space>
<Switch
size="small"
checked={v === "enabled"}
onChange={(c) => handleToggle(r, c)}
/>
<Tag color={STATUS_COLOR[v] ?? "default"}>{v}</Tag>
</Space>
),
},
{ title: "最近运行", dataIndex: "last_run_at", key: "last_run_at", width: 160, render: fmtTime },
{ title: "下次运行", dataIndex: "next_run_at", key: "next_run_at", width: 160, render: fmtTime },
{
title: "最后错误", dataIndex: "last_error", key: "last_error", ellipsis: true,
render: (v: string | null) => v ? (
<Tooltip title={v}><span style={{ color: "#d46b08" }}>{v.slice(0, 40)}</span></Tooltip>
) : "—",
},
{
title: "操作", key: "action", width: 100, fixed: "right",
render: (_: unknown, r) => (
<Button size="small" icon={<EditOutlined />} onClick={() => openEdit(r)}></Button>
),
},
];
return (
<div>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 16 }}>
<div>
<Title level={4} style={{ margin: 0 }}>AI </Title>
<Paragraph type="secondary" style={{ margin: 0, fontSize: 13 }}>
<code>biz.trigger_jobs</code> AI cron
</Paragraph>
</div>
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}></Button>
</div>
<Card size="small">
<Table<TriggerItem>
columns={columns}
dataSource={items}
rowKey="id"
loading={loading}
pagination={false}
size="small"
scroll={{ x: 1200 }}
/>
</Card>
<Modal
title={editing ? `编辑触发器 #${editing.id}` : ""}
open={!!editing}
onCancel={() => setEditing(null)}
onOk={handleSave}
confirmLoading={saving}
okText="保存" cancelText="取消"
width={600}
>
{editing && (
<>
<Descriptions size="small" column={1} bordered style={{ marginBottom: 16 }}>
<Descriptions.Item label="触发器名">{editing.job_name}</Descriptions.Item>
<Descriptions.Item label="类型">
<Tag color={CONDITION_COLOR[editing.trigger_condition] ?? "default"}>
{editing.trigger_condition}
</Tag>
</Descriptions.Item>
{editing.trigger_condition === "event" && (
<Descriptions.Item label="事件名">
<code>{String(editing.trigger_config?.event_name || "—")}</code>
</Descriptions.Item>
)}
</Descriptions>
{editing.trigger_condition === "cron" && (
<div style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 4, fontWeight: 500 }}>Cron </div>
<Input
value={editCron}
onChange={(e) => setEditCron(e.target.value)}
placeholder="标准 5 段 cron例如 0 10 * * *"
/>
<Paragraph type="secondary" style={{ fontSize: 12, margin: "4px 0 0 0" }}>
<code> </code><code>0 10 * * *</code> 10:00<code>*/30 * * * *</code> 30
</Paragraph>
</div>
)}
<div>
<div style={{ marginBottom: 4, fontWeight: 500 }}></div>
<Input.TextArea
value={editDesc}
onChange={(e) => setEditDesc(e.target.value)}
rows={3}
/>
</div>
</>
)}
</Modal>
</div>
);
};
export default AITriggers;