292 lines
9.4 KiB
TypeScript
292 lines
9.4 KiB
TypeScript
// @ts-nocheck
|
||
|
||
|
||
import { inferTimetableQuery as sharedInfer, normalizeTimetableQuery as sharedNormalize } from './timetable-query.js';
|
||
import * as sessionContext from '../core/session-context.js';
|
||
|
||
export function inferTimetableQuery(text) {
|
||
return sharedInfer(text);
|
||
}
|
||
|
||
export function normalizeTimetableQuery(raw) {
|
||
return sharedNormalize(raw);
|
||
}
|
||
|
||
const DAY_TYPE_TO_QUERY = {
|
||
WORKDAY: '工作日',
|
||
WORK_DAY: '工作日',
|
||
HOLIDAY: '节假日',
|
||
WEEKEND: '周末',
|
||
workday: '工作日',
|
||
holiday: '节假日',
|
||
};
|
||
|
||
function resolveTimetableQuery(args, userInput) {
|
||
const aliases = ['fileName', 'name', 'timetableName'];
|
||
let q = '';
|
||
for (const k of ['query', ...aliases]) {
|
||
const v = String(args[k] ?? '').trim();
|
||
if (v) { q = v; break; }
|
||
}
|
||
if (!q && args.dayType != null) {
|
||
const key = String(args.dayType).trim();
|
||
q = DAY_TYPE_TO_QUERY[key] || DAY_TYPE_TO_QUERY[key.toUpperCase()] || '';
|
||
}
|
||
if (!q && userInput) q = inferTimetableQuery(userInput);
|
||
if (q) q = normalizeTimetableQuery(q);
|
||
return q;
|
||
}
|
||
|
||
const EXTRACTORS = {
|
||
|
||
timetableQuery: {
|
||
infer: inferTimetableQuery,
|
||
normalize: normalizeTimetableQuery,
|
||
resolve: resolveTimetableQuery,
|
||
},
|
||
|
||
userInputAsIs: {
|
||
infer: (text) => String(text || '').trim(),
|
||
},
|
||
};
|
||
|
||
const TOOL_PARAM_RULES = {
|
||
metro_load_timetable_full: [{
|
||
field: 'query',
|
||
aliases: ['fileName', 'name', 'timetableName'],
|
||
extract: 'timetableQuery',
|
||
}],
|
||
metro_search_tools: [{
|
||
field: 'query',
|
||
extract: 'userInputAsIs',
|
||
onlyIfEmpty: true,
|
||
}],
|
||
};
|
||
|
||
function readFieldValue(args, field, aliases = []) {
|
||
const keys = [field, ...aliases];
|
||
for (const k of keys) {
|
||
const v = String(args[k] ?? '').trim();
|
||
if (v) return v;
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function applyExtractor(name, value, userInput, args) {
|
||
const ext = EXTRACTORS[name];
|
||
if (!ext) return value;
|
||
if (ext.resolve) return ext.resolve(args || {}, userInput);
|
||
let v = value;
|
||
if (!v && userInput && ext.infer) v = ext.infer(userInput);
|
||
if (v && ext.normalize) v = ext.normalize(v);
|
||
return v;
|
||
}
|
||
|
||
const SETUP_CONFIRM_TOOLS = new Set(['metro_setup_rest_rules', 'metro_setup_handover_rules']);
|
||
const RULES_CONFIRM_RE = /^(确认|可以|没问题|执行|好的|同意|按这个|就这样|确认执行|确认设置)([。.!!\s]*)$/;
|
||
const RULES_CONFIRM_LOOSE_RE = /确认(执行|设置|应用|写入)?|按(这个|此)?(清单|方案|规则)?(执行|设置|应用)?/;
|
||
|
||
function findLastProposedRules(messages) {
|
||
if (!Array.isArray(messages)) return null;
|
||
for (let i = messages.length - 1; i >= 0; i--) {
|
||
const m = messages[i];
|
||
if (m?.role !== 'tool') continue;
|
||
try {
|
||
const parsed = typeof m.content === 'string' ? JSON.parse(m.content) : m.content;
|
||
const result = parsed?.result || parsed;
|
||
if (Array.isArray(result?.proposedRules) && result.proposedRules.length) {
|
||
return result.proposedRules;
|
||
}
|
||
} catch (_) { }
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function applySetupConfirmFromDialog(out, currentUserInput, messages) {
|
||
const ui = String(currentUserInput || '').trim();
|
||
if (!ui) return;
|
||
|
||
const wantsConfirm = RULES_CONFIRM_RE.test(ui) || RULES_CONFIRM_LOOSE_RE.test(ui);
|
||
if (!wantsConfirm) return;
|
||
|
||
out.stage = 'write';
|
||
out.confirmed = true;
|
||
if (!Array.isArray(out.rules) || out.rules.length === 0) {
|
||
const pending = findLastProposedRules(messages);
|
||
if (pending?.length) out.rules = pending;
|
||
}
|
||
}
|
||
|
||
export function enrichToolArguments(toolName, args, userInput, context = {}) {
|
||
const out = { ...(args || {}) };
|
||
const ui = String(userInput || '').trim();
|
||
const dialog = [context.recentDialog, ui].filter(Boolean).join('\n');
|
||
if (ui) out._userContext = ui;
|
||
|
||
|
||
|
||
|
||
const ctx = sessionContext.getState();
|
||
const sessionSnapshot = {};
|
||
if (ctx.currentEmployeeId) sessionSnapshot.currentEmployeeId = ctx.currentEmployeeId;
|
||
if (ctx.currentEmployeeName) sessionSnapshot.currentEmployeeName = ctx.currentEmployeeName;
|
||
if (ctx.currentTeam) sessionSnapshot.currentTeam = ctx.currentTeam;
|
||
if (ctx.currentMonth) sessionSnapshot.currentMonth = ctx.currentMonth;
|
||
if (ctx.lastQueryType) sessionSnapshot.lastQueryType = ctx.lastQueryType;
|
||
if (Object.keys(sessionSnapshot).length > 0) {
|
||
out._sessionContext = sessionSnapshot;
|
||
}
|
||
|
||
|
||
sessionContext.updateFromUserInput(ui);
|
||
|
||
const rules = TOOL_PARAM_RULES[toolName];
|
||
if (rules) {
|
||
for (const rule of rules) {
|
||
let val = readFieldValue(out, rule.field, rule.aliases);
|
||
if (rule.onlyIfEmpty && val) {
|
||
out[rule.field] = val;
|
||
continue;
|
||
}
|
||
val = applyExtractor(rule.extract, val, ui, out);
|
||
if (val) out[rule.field] = val;
|
||
}
|
||
}
|
||
|
||
if (toolName === 'metro_load_timetable_full') {
|
||
if (out.query && out.dayType) delete out.dayType;
|
||
applyAutoConfirmFromDialog(out, dialog, ui);
|
||
}
|
||
|
||
if (SETUP_CONFIRM_TOOLS.has(toolName)) {
|
||
applySetupConfirmFromDialog(out, ui, context.messages);
|
||
}
|
||
|
||
|
||
const userYearText = buildUserYearSourceText(ui, context.messages);
|
||
stripInventedProfileYear(toolName, out, userYearText);
|
||
|
||
|
||
normalizeProfileEmployeeArgs(toolName, out, ui, dialog);
|
||
|
||
|
||
if (Array.isArray(out.rules)) {
|
||
out.rules = out.rules.map(normalizeRuleFields);
|
||
}
|
||
|
||
return out;
|
||
}
|
||
|
||
function normalizeProfileEmployeeArgs(toolName, out, userInput, dialog) {
|
||
if (!String(toolName || '').startsWith('profile_')) return;
|
||
const id = out.employeeId;
|
||
if (id != null && String(id).trim() && /[\u4e00-\u9fff]/.test(String(id))) {
|
||
if (!out.employeeName) out.employeeName = String(id).trim();
|
||
delete out.employeeId;
|
||
}
|
||
if (!out.employeeName && userInput) {
|
||
|
||
const m = String(dialog || userInput).match(
|
||
/(?:查看|查一下|查询|生成.*?|帮我看|看看)?\s*([一-龥·]{2,4})\s*(?:的|老师傅|师傅)?(?:数据|画像|报告|评估)?/
|
||
);
|
||
|
||
const m2 = String(userInput).match(/([一-龥·]{2,4})的(?:数据|画像|报告|评估|考试|公里)/);
|
||
if (m2) out.employeeName = m2[1];
|
||
}
|
||
|
||
if (out.month === 0 || out.month === '0') delete out.month;
|
||
}
|
||
|
||
function buildUserYearSourceText(userInput, messages) {
|
||
const parts = [];
|
||
if (Array.isArray(messages)) {
|
||
for (const m of messages) {
|
||
if (m?.role === 'user' && typeof m.content === 'string' && m.content.trim()) {
|
||
parts.push(m.content.trim());
|
||
}
|
||
}
|
||
}
|
||
const ui = String(userInput || '').trim();
|
||
if (ui && !parts.includes(ui)) parts.push(ui);
|
||
return parts.join('\n');
|
||
}
|
||
|
||
function stripInventedProfileYear(toolName, out, userText) {
|
||
if (!String(toolName || '').startsWith('profile_')) return;
|
||
if (out.year == null || out.year === '') return;
|
||
const y = Number(out.year);
|
||
if (!Number.isFinite(y)) {
|
||
delete out.year;
|
||
return;
|
||
}
|
||
const allowed = extractExplicitYears(userText);
|
||
if (!allowed.includes(y)) {
|
||
delete out.year;
|
||
}
|
||
}
|
||
|
||
export function extractExplicitYears(text) {
|
||
const years = [];
|
||
const s = String(text || '');
|
||
const re = /(20\d{2})\s*年(?:度)?/g;
|
||
let m;
|
||
while ((m = re.exec(s))) {
|
||
const y = parseInt(m[1], 10);
|
||
if (!years.includes(y)) years.push(y);
|
||
}
|
||
return years;
|
||
}
|
||
|
||
const FIELD_ALIASES = {
|
||
stationName: ['stationName', 'siteId', 'siteName', 'stationId', 'station', 'name'],
|
||
count: ['count', 'num', 'number', 'people', 'persons', 'amount'],
|
||
startTime: ['startTime', 'start', 'begin', 'from', 'start_time'],
|
||
endTime: ['endTime', 'end', 'to', 'until', 'end_time'],
|
||
reason: ['reason', 'type', 'category'],
|
||
shift: ['shift', 'shiftType', '班制'],
|
||
};
|
||
|
||
function normalizeRuleFields(rule) {
|
||
if (!rule || typeof rule !== 'object') return rule;
|
||
const out = {};
|
||
for (const [canonical, aliases] of Object.entries(FIELD_ALIASES)) {
|
||
for (const alias of aliases) {
|
||
if (rule[alias] !== undefined) {
|
||
out[canonical] = rule[alias];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
for (const [k, v] of Object.entries(rule)) {
|
||
if (!Object.values(FIELD_ALIASES).flat().includes(k)) {
|
||
out[k] = v;
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
const AUTO_CONFIRM_DENY_RE = /不要删|不删|保留(?:部分)?|取消删除|别删|无需删除/;
|
||
const AUTO_CONFIRM_YES_RE = /自动(?:确认)?删除|直接(?:帮您)?(?:确认)?删除|不用问|自己删除|自动处理调试车|确认删除调试车|加载完成(?:后|时).{0,16}删(?:除)?调试车|会直接.{0,12}删(?:除)?调试车|直接帮您确认删除/i;
|
||
|
||
function applyAutoConfirmFromDialog(out, dialog, currentUserInput) {
|
||
if (out.autoConfirmDebugTrain === true) return;
|
||
const deny = AUTO_CONFIRM_DENY_RE.test(String(currentUserInput || ''));
|
||
if (deny) {
|
||
out.autoConfirmDebugTrain = false;
|
||
return;
|
||
}
|
||
if (AUTO_CONFIRM_YES_RE.test(String(dialog || ''))) {
|
||
out.autoConfirmDebugTrain = true;
|
||
}
|
||
}
|
||
|
||
export function buildRecentDialogText(messages, maxItems = 14) {
|
||
return (messages || [])
|
||
.filter(m => m.role === 'user' || m.role === 'assistant')
|
||
.slice(-maxItems)
|
||
.map(m => String(m.content || '').trim())
|
||
.filter(Boolean)
|
||
.join('\n');
|
||
}
|