|
|
@@ -0,0 +1,184 @@
|
|
|
+/**
|
|
|
+ * 分析模板库:把高频的提问方式沉淀成可复用模板(AI 对话输入框的「模板」抽屉用)。
|
|
|
+ *
|
|
|
+ * 从旧版 Codex 工作台({@code codex/utils/codexTemplateLibrary})迁移而来,两处差异:
|
|
|
+ * <ul>
|
|
|
+ * <li>自定义模板沿用<b>同一 localStorage 键</b>(codex.templates.v1)——用户在旧版里
|
|
|
+ * 存过的模板在新对话框里继续可用,两边共享同一份;</li>
|
|
|
+ * <li>预置模板按当前 AI 研判 agent 的口径重写:说清要查什么、结果怎么展示即可,
|
|
|
+ * 不再教 ```table / ```echarts 这类 Codex 围栏协议(新对话用 Markdown 表格与产物)。</li>
|
|
|
+ * </ul>
|
|
|
+ */
|
|
|
+
|
|
|
+export interface PromptTemplate {
|
|
|
+ id: string;
|
|
|
+ title: string;
|
|
|
+ prompt: string;
|
|
|
+ /** 分组名,用于抽屉里的分节 */
|
|
|
+ group: string;
|
|
|
+ /** true = 内置,不可删除/编辑 */
|
|
|
+ builtin: boolean;
|
|
|
+}
|
|
|
+
|
|
|
+/** 与旧版 Codex 同键:升级后用户已存的自定义模板不丢 */
|
|
|
+const CUSTOM_KEY = 'codex.templates.v1';
|
|
|
+const MAX_CUSTOM = 100;
|
|
|
+const MAX_PROMPT_LENGTH = 8000;
|
|
|
+
|
|
|
+/** 预置模板:贴合案件数据研判场景(与欢迎页示例同口径) */
|
|
|
+export const BUILTIN_TEMPLATES: PromptTemplate[] = [
|
|
|
+ {
|
|
|
+ id: 'builtin-overview',
|
|
|
+ title: '数据概览',
|
|
|
+ group: '摸底',
|
|
|
+ builtin: true,
|
|
|
+ prompt: '当前案件的数据空间里有哪些表?分别有哪些字段?先给出表清单,并说明每张表能回答什么问题。',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'builtin-profile',
|
|
|
+ title: '字段画像与空值排查',
|
|
|
+ group: '摸底',
|
|
|
+ builtin: true,
|
|
|
+ prompt:
|
|
|
+ '对「<填写表名>」做字段画像:每个字段的类型、非空率、取值分布与最小/最大值(或高频取值),' +
|
|
|
+ '用表格列出,并把可疑字段(空值占比高、取值异常)单独标出来。',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'builtin-topn',
|
|
|
+ title: 'TopN 排行',
|
|
|
+ group: '统计',
|
|
|
+ builtin: true,
|
|
|
+ prompt:
|
|
|
+ '统计「<填写字段>」出现次数最多的前 20 项,列出次数与占比(表格),' + '并指出明显异常、值得进一步核查的对象。',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'builtin-trend',
|
|
|
+ title: '时序趋势',
|
|
|
+ group: '统计',
|
|
|
+ builtin: true,
|
|
|
+ prompt:
|
|
|
+ '按「<填写时间字段>」做时间聚合,统计每天的记录数变化趋势,用图表展示并指出峰值时段与突变点,' +
|
|
|
+ '说明可能的业务含义,原始明细用表格附在后面。',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'builtin-money',
|
|
|
+ title: '资金流向链路',
|
|
|
+ group: '关联',
|
|
|
+ builtin: true,
|
|
|
+ prompt:
|
|
|
+ '围绕「<填写账号/卡号>」梳理资金流向:找出与其有交易往来的对象、交易笔数与金额合计,' +
|
|
|
+ '按金额从高到低列出,并总结最值得关注的资金链路与可疑点。',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'builtin-spacetime',
|
|
|
+ title: '时空碰撞',
|
|
|
+ group: '关联',
|
|
|
+ builtin: true,
|
|
|
+ prompt:
|
|
|
+ '找出「<填写对象A>」与「<填写对象B>」在同一地点(基站/卡口/住宿)同一时段内同时出现的记录,' +
|
|
|
+ '按时间排序列出(含地点、起止时间、重合时长),并说明重合次数最多的地点。',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'builtin-outlier',
|
|
|
+ title: '数据质量排查',
|
|
|
+ group: '质检',
|
|
|
+ builtin: true,
|
|
|
+ prompt:
|
|
|
+ '检查当前案件数据的质量问题:字段缺失、时间异常(早于 2000 年等)、金额异常(负数等)、' +
|
|
|
+ '重复记录、编码格式异常。用表格列出问题清单(问题类型、涉及字段、样例行、影响行数),并给出处理建议。',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ id: 'builtin-report',
|
|
|
+ title: '结论报告成稿',
|
|
|
+ group: '输出',
|
|
|
+ builtin: true,
|
|
|
+ prompt:
|
|
|
+ '把本次会话前面的分析结论整理成一份可直接交付的报告:数据来源与时间范围、分析口径、' +
|
|
|
+ '主要发现(每条都要有数据支撑)、可疑点与建议的下一步核查方向。',
|
|
|
+ },
|
|
|
+];
|
|
|
+
|
|
|
+/* ------------------------------------------------------------------ *
|
|
|
+ * 自定义模板(localStorage)
|
|
|
+ * ------------------------------------------------------------------ */
|
|
|
+
|
|
|
+function sanitize(list: unknown): PromptTemplate[] {
|
|
|
+ if (!Array.isArray(list)) return [];
|
|
|
+ return list
|
|
|
+ .map((item: any) => ({
|
|
|
+ id: String(item?.id ?? ''),
|
|
|
+ title: String(item?.title ?? '').slice(0, 60),
|
|
|
+ prompt: String(item?.prompt ?? '').slice(0, MAX_PROMPT_LENGTH),
|
|
|
+ group: String(item?.group ?? '我的模板').slice(0, 20) || '我的模板',
|
|
|
+ builtin: false,
|
|
|
+ }))
|
|
|
+ .filter((item) => item.id && item.title && item.prompt);
|
|
|
+}
|
|
|
+
|
|
|
+export function readCustomTemplates(): PromptTemplate[] {
|
|
|
+ try {
|
|
|
+ const raw = window.localStorage.getItem(CUSTOM_KEY);
|
|
|
+ if (!raw) return [];
|
|
|
+ return sanitize(JSON.parse(raw));
|
|
|
+ } catch {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function writeCustomTemplates(list: PromptTemplate[]): PromptTemplate[] {
|
|
|
+ const trimmed = list.slice(-MAX_CUSTOM);
|
|
|
+ try {
|
|
|
+ window.localStorage.setItem(CUSTOM_KEY, JSON.stringify(trimmed));
|
|
|
+ } catch {
|
|
|
+ // 超配额:只影响持久化
|
|
|
+ }
|
|
|
+ return trimmed;
|
|
|
+}
|
|
|
+
|
|
|
+/** 新增自定义模板;标题重复时覆盖同名条目 */
|
|
|
+export function saveTemplate(input: { title: string; prompt: string; group?: string }): PromptTemplate[] {
|
|
|
+ const title = input.title.trim().slice(0, 60);
|
|
|
+ const prompt = input.prompt.trim().slice(0, MAX_PROMPT_LENGTH);
|
|
|
+ if (!title || !prompt) return readCustomTemplates();
|
|
|
+
|
|
|
+ const existing = readCustomTemplates();
|
|
|
+ const hit = existing.find((item) => item.title === title);
|
|
|
+ const record: PromptTemplate = {
|
|
|
+ id: hit?.id ?? `custom-${timestampTag()}-${Math.random().toString(36).slice(2, 7)}`,
|
|
|
+ title,
|
|
|
+ prompt,
|
|
|
+ group: (input.group ?? '我的模板').trim().slice(0, 20) || '我的模板',
|
|
|
+ builtin: false,
|
|
|
+ };
|
|
|
+ const next = hit ? existing.map((item) => (item.id === hit.id ? record : item)) : [...existing, record];
|
|
|
+ return writeCustomTemplates(next);
|
|
|
+}
|
|
|
+
|
|
|
+export function removeTemplate(id: string): PromptTemplate[] {
|
|
|
+ return writeCustomTemplates(readCustomTemplates().filter((item) => item.id !== id));
|
|
|
+}
|
|
|
+
|
|
|
+/** 预置 + 自定义,按分组聚合 */
|
|
|
+export function groupTemplates(
|
|
|
+ custom: PromptTemplate[],
|
|
|
+ keyword = '',
|
|
|
+): Array<{ group: string; items: PromptTemplate[] }> {
|
|
|
+ const key = keyword.trim().toLowerCase();
|
|
|
+ const all = [...BUILTIN_TEMPLATES, ...custom].filter((item) =>
|
|
|
+ key ? `${item.title} ${item.prompt} ${item.group}`.toLowerCase().includes(key) : true,
|
|
|
+ );
|
|
|
+ const groups: string[] = [];
|
|
|
+ for (const item of all) {
|
|
|
+ if (!groups.includes(item.group)) groups.push(item.group);
|
|
|
+ }
|
|
|
+ return groups.map((group) => ({ group, items: all.filter((item) => item.group === group) }));
|
|
|
+}
|
|
|
+
|
|
|
+/** 导出文件名用的时间标签:YYYYMMDD-HHmmss */
|
|
|
+export function timestampTag(date = new Date()): string {
|
|
|
+ const p = (n: number) => String(n).padStart(2, '0');
|
|
|
+ return (
|
|
|
+ `${date.getFullYear()}${p(date.getMonth() + 1)}${p(date.getDate())}-` +
|
|
|
+ `${p(date.getHours())}${p(date.getMinutes())}${p(date.getSeconds())}`
|
|
|
+ );
|
|
|
+}
|