| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- 'use strict';
- /**
- * 智能问数智能体 · 入口与运行注册表
- * ----------------------------------------------------------------------------
- * startRun 立即返回运行记录(前端轮询),循环异步执行;成败都折叠进 rec,
- * 绝不向上抛(对齐 ai-fde 委派网关「永不抛出」的契约)。
- * 双模式:quick = 复用 nl-query 两段式(省预算);agent = harness 循环(默认)。
- * 运行态在内存(仿 syncState),终态落盘 data/agent/(trace.js)。
- */
- const trace = require('./trace.js');
- const loop = require('./loop.js');
- const skills = require('./skills.js');
- const { mockReply } = require('./mock-model.js');
- const { pickRow } = require('./tools/lake.js');
- const nlQuery = require('../../nl-query.js');
- const runs = new Map(); // runId → rec
- const MAX_CACHED = 60;
- function isMock() { return process.env.NAPOS_AGENT_MOCK === '1'; }
- /** 快问自动分流:短问句且不含分析类词 → 快问(nl-query 两段式),否则深度分析 */
- function pickMode(q) {
- if (String(q || '').length > 18) return 'agent';
- if (/(为什么|怎么|为何|对比|环比|同比|变化|体检|对标|排名|归因|分析|趋势|影响|跨|哪家|哪些|问题)/.test(String(q || ''))) return 'agent';
- return 'quick';
- }
- function assertChatCfg(cfg) {
- for (const [k, label] of [['chatApiBase', '对话模型地址'], ['chatApiKey', '对话模型 Key'], ['chatModel', '对话模型']]) {
- if (!cfg[k]) {
- const e = new Error(`对话模型未配置(缺少${label}):请到「系统设置 → API 设置 → 对话模型」选择模型后点「保存设置」`);
- e.code = 'ASK_NO_MODEL';
- throw e;
- }
- }
- }
- function trimCache() {
- if (runs.size <= MAX_CACHED) return;
- for (const [id, rec] of runs) {
- if (rec.status !== 'running') runs.delete(id);
- if (runs.size <= MAX_CACHED) break;
- }
- }
- /**
- * 发起一次运行并立即返回记录。
- * @param {object} o { entity, question, mode: 'agent'|'quick', sessionId, cfg, svc }
- */
- function startRun(o) {
- const { cfg, svc } = o;
- const q = String(o.question || '').trim();
- if (!q) { const e = new Error('请输入问题'); e.code = 'ASK_EMPTY'; throw e; }
- if (!isMock()) assertChatCfg(cfg);
- const entity = trace.sanitizeKey(o.entity);
- if (!svc.store.getEntity(entity)) {
- const e = new Error('数据主体不存在,请刷新页面重试');
- e.code = 'ASK_NO_ENTITY';
- throw e;
- }
- const rec = trace.newRunRecord({
- entity,
- entityName: (svc.store.getEntity(entity) || {}).name,
- question: q,
- mode: o.mode,
- sessionId: o.sessionId ? trace.sanitizeKey(o.sessionId) : undefined,
- });
- const skill = skills.get(o.skill);
- if (skill) rec.skill = skill.key;
- // 自动分流:mode='auto'(前端默认档)按规则选快问/深度分析
- if (o.mode === 'auto') {
- rec.mode = pickMode(q);
- rec.autoPicked = true;
- }
- runs.set(rec.runId, rec);
- trimCache();
- trace.upsertSessionRun(entity, {
- sessionId: rec.sessionId, runId: rec.runId, question: q, mode: rec.mode, status: 'running', at: rec.startedAt,
- });
- // 异步执行:路由已返回,前端轮询 GET /api/agent/runs/:id
- (async () => {
- try {
- if (isMock()) {
- // mock 模式:quick/agent 都走脚本化假模型(快问=直接 final);auto 已折算进 rec.mode
- rec.result = rec.mode === 'quick'
- ? { answer: '(mock 模型)快问链路正常。', chart: null, appliedFrom: null, thought: '' }
- : await loop.runAgent({ rec, svc, cfg, mock: { next: (r) => mockReply(r) } });
- } else if (rec.mode === 'quick') {
- const history = trace.loadRecentConclusions(entity, rec.sessionId);
- const result = await nlQuery.ask({ q, entity, cfg, svc, history });
- rec.result = {
- answer: result.reply,
- chart: result.chart && result.chart.type !== 'none' ? result.chart : null,
- appliedFrom: result.applied || null,
- thought: '',
- // 快问同样可一键打开当期 HTML 分析报告(区间取本次查询钳制后的区间)
- reportUrl: result.applied && result.applied.from && result.applied.to
- ? `/api/reports/report-html?${new URLSearchParams({ entity, from: result.applied.from, to: result.applied.to }).toString()}`
- : null,
- // 图表数据源(快问不走工具步骤,行随结果携带;去 raw 控体积)
- chartRows: (result.rows || []).slice(0, 200).map(pickRow),
- };
- } else {
- rec.history = trace.loadRecentConclusions(entity, rec.sessionId);
- rec.result = await loop.runAgent({ rec, svc, cfg, mock: null, cloud: o.cloud || null });
- }
- rec.steps.push({ type: 'final', answer: rec.result.answer });
- rec.status = 'done';
- } catch (e) {
- if (e && e.code === 'AGENT_CANCELLED') rec.status = 'cancelled';
- else {
- rec.status = 'failed';
- rec.error = String((e && e.message) || e).slice(0, 500);
- }
- } finally {
- rec.finishedAt = new Date().toISOString();
- rec.cancelRequested = false;
- trace.pushEvent(rec, 'run.end', { status: rec.status, error: rec.error });
- trace.persistRun(rec);
- trace.upsertSessionRun(entity, {
- sessionId: rec.sessionId, runId: rec.runId, question: q, mode: rec.mode, status: rec.status, at: rec.finishedAt,
- });
- }
- })();
- return rec;
- }
- function cancelRun(runId) {
- const rec = runs.get(String(runId || ''));
- if (rec && rec.status === 'running') rec.cancelRequested = true;
- return rec || null;
- }
- /** 内存优先,落盘兜底(服务重启后仍可回放历史运行) */
- function getRun(entity, runId) {
- const key = trace.sanitizeKey(entity);
- const id = String(runId || '').replace(/[^a-zA-Z0-9_-]/g, '');
- const rec = runs.get(id);
- if (rec && rec.entity === key) return rec;
- const saved = trace.loadRun(key, id);
- return saved && saved.entity === key ? saved : null;
- }
- function listRuns(entity, sessionId, limit = 20) {
- const key = trace.sanitizeKey(entity);
- const session = trace.loadSessions(key).find((s) => s.sessionId === sessionId);
- return session ? session.runs.slice(0, limit) : [];
- }
- function listSessions(entity) {
- const key = trace.sanitizeKey(entity);
- return trace.loadSessions(key).map((s) => {
- const last = (s.runs || [])[0] || {};
- return {
- sessionId: s.sessionId,
- title: s.title,
- createdAt: s.createdAt,
- updatedAt: s.updatedAt,
- runCount: (s.runs || []).length,
- lastQuestion: last.question || '',
- lastAt: last.at || s.updatedAt || s.createdAt || null,
- };
- });
- }
- function deleteSession(entity, sessionId) {
- const key = trace.sanitizeKey(entity);
- const id = String(sessionId || '').replace(/[^a-zA-Z0-9_-]/g, '');
- return trace.deleteSession(key, id);
- }
- module.exports = { startRun, cancelRun, getRun, listRuns, listSessions, deleteSession };
|