| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537 |
- const crypto = require('crypto');
- const fs = require('fs');
- const path = require('path');
- const { outputsRoot } = require('../core/output-paths');
- const { okResult, errorResult } = require('../core/result-envelope');
- const { getConversations, updateCustomerTask } = require('./agent-service');
- const { completeTodoKnowledge } = require('./official-office-knowledge-service');
- const { qiweiGoalUpdateTask } = require('../tools/qiwei-goal-management-run');
- const ACTIVE_STATUSES = new Set(['open', 'in_progress', 'blocked']);
- const LOCAL_STATUSES = new Set(['open', 'in_progress', 'blocked', 'done', 'dismissed']);
- const DIAGNOSIS_OUTCOMES = new Set(['effective', 'partially_effective', 'ineffective', 'unknown']);
- function ensureDir(dirPath) {
- fs.mkdirSync(dirPath, { recursive: true });
- return dirPath;
- }
- function readJson(filePath, fallback) {
- try { return JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '')); }
- catch { return fallback; }
- }
- function writeJsonAtomic(filePath, value) {
- ensureDir(path.dirname(filePath));
- const tempPath = `${filePath}.${process.pid}.tmp`;
- fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
- fs.renameSync(tempPath, filePath);
- }
- function stableId(...parts) {
- return crypto.createHash('sha256').update(parts.map(value => String(value || '')).join('\u0000')).digest('hex').slice(0, 20);
- }
- function normalizeDate(value) {
- const text = String(value || '').trim();
- if (!text) return '';
- const parsed = new Date(text.replace(' ', 'T'));
- return Number.isNaN(parsed.getTime()) ? text : parsed.toISOString();
- }
- function isOverdue(task, now = Date.now()) {
- if (!ACTIVE_STATUSES.has(task.status) || !task.dueAt) return false;
- const timestamp = Date.parse(task.dueAt);
- return Number.isFinite(timestamp) && timestamp < now;
- }
- function sourceMeta(source) {
- return ({
- customer: { label: '客户会话跟进', kind: 'formal', channel: 'local-agent' },
- official: { label: '企微官方待办', kind: 'formal', channel: 'official-cli' },
- goal: { label: '目标计划', kind: 'formal', channel: 'local-goal' },
- local: { label: '本地内部任务', kind: 'formal', channel: 'local-task' },
- 'document-candidate': { label: '文档待办候选', kind: 'candidate', channel: 'ai-analysis' },
- 'meeting-candidate': { label: '会议待办候选', kind: 'candidate', channel: 'ai-analysis' },
- 'diagnosis-candidate': { label: '经营诊断候选', kind: 'candidate', channel: 'business-diagnosis' },
- })[source] || { label: source || '其他来源', kind: 'formal', channel: 'local' };
- }
- function baseTask(input) {
- const meta = sourceMeta(input.source);
- const task = {
- id: String(input.id || ''),
- source: input.source,
- sourceLabel: meta.label,
- sourceKind: meta.kind,
- channel: meta.channel,
- title: String(input.title || '未命名任务'),
- owner: String(input.owner || ''),
- dueAt: normalizeDate(input.dueAt),
- priority: String(input.priority || 'medium'),
- status: String(input.status || 'open'),
- evidence: String(input.evidence || ''),
- sourceTitle: String(input.sourceTitle || ''),
- sourceId: String(input.sourceId || ''),
- updatedAt: String(input.updatedAt || ''),
- audience: input.audience === 'external' ? 'external' : 'internal',
- conversationId: String(input.conversationId || ''),
- contactName: String(input.contactName || ''),
- goalId: String(input.goalId || ''),
- officialTodoId: String(input.officialTodoId || ''),
- officialSyncStatus: String(input.officialSyncStatus || ''),
- confidence: input.confidence === undefined || input.confidence === null ? null : Number(input.confidence),
- promotedTo: String(input.promotedTo || ''),
- sourceRef: input.sourceRef && typeof input.sourceRef === 'object' ? input.sourceRef : null,
- diagnosisOutcome: input.diagnosisOutcome && typeof input.diagnosisOutcome === 'object' ? input.diagnosisOutcome : null,
- actions: input.actions || {},
- };
- task.overdue = isOverdue(task);
- return task;
- }
- function createUnifiedTaskService(dependencies = {}) {
- const root = path.resolve(dependencies.outputsDir || outputsRoot());
- const storeDir = path.resolve(dependencies.storeDir || path.join(root, 'knowledge', 'tasks'));
- const localIndexPath = path.join(storeDir, 'index.json');
- const paths = {
- official: path.resolve(dependencies.officialTodoPath || path.join(root, 'knowledge', 'todos', 'index.json')),
- goals: path.resolve(dependencies.goalPath || path.join(root, 'goals', 'plans.json')),
- documents: path.resolve(dependencies.documentPath || path.join(root, 'knowledge', 'docs', 'index.json')),
- meetings: path.resolve(dependencies.meetingPath || path.join(root, 'knowledge', 'meetings', 'index.json')),
- diagnoses: path.resolve(dependencies.diagnosisPath || path.join(root, 'knowledge', 'diagnoses', 'action-candidates.json')),
- };
- const conversationReader = dependencies.getConversations || getConversations;
- const customerUpdater = dependencies.updateCustomerTask || updateCustomerTask;
- const officialCompleter = dependencies.completeTodoKnowledge || completeTodoKnowledge;
- const goalUpdater = dependencies.qiweiGoalUpdateTask || qiweiGoalUpdateTask;
- function readLocalStore() {
- const store = readJson(localIndexPath, { version: 1, tasks: [], updatedAt: null });
- store.version ||= 1;
- store.tasks = Array.isArray(store.tasks) ? store.tasks : [];
- return store;
- }
- function persistLocalStore(store) {
- store.updatedAt = new Date().toISOString();
- writeJsonAtomic(localIndexPath, store);
- const lines = [
- '# 统一任务中心',
- '',
- '本目录保存经人工确认后形成的本地内部任务。客户会话、企微官方待办、目标计划、文档和会议候选会在运行时统一聚合,不复制凭据或原始敏感消息。',
- '',
- `最近更新:${store.updatedAt}`,
- '',
- ...store.tasks.map(task => `- [${task.status === 'done' ? 'x' : ' '}] ${task.title}(${task.owner || '待分配'})`),
- '',
- ];
- fs.writeFileSync(path.join(storeDir, 'README.md'), lines.join('\n'), 'utf8');
- }
- function readDiagnosisCandidates() {
- const store = readJson(paths.diagnoses, { version: 1, candidates: [], updatedAt: null });
- store.version ||= 1;
- store.candidates = Array.isArray(store.candidates) ? store.candidates : [];
- return store;
- }
- function persistDiagnosisCandidates(store) {
- store.updatedAt = new Date().toISOString();
- writeJsonAtomic(paths.diagnoses, store);
- }
- function appendDiagnosisLifecycle(candidate, event, details = {}) {
- candidate.lifecycle = Array.isArray(candidate.lifecycle) ? candidate.lifecycle : [];
- candidate.lifecycle.push({ event, at: new Date().toISOString(), ...details });
- candidate.lifecycle = candidate.lifecycle.slice(-50);
- }
- function findDiagnosisCandidate(store, sourceKey) {
- return store.candidates.find(item => item.sourceKey === sourceKey);
- }
- function collectCustomerTasks() {
- const result = conversationReader() || {};
- const conversations = result.data?.conversations || [];
- return conversations.flatMap(conversation => (conversation.customerIntelligence?.tasks || []).map(task => baseTask({
- id: task.id,
- source: 'customer',
- title: task.title,
- owner: task.owner,
- dueAt: task.dueAt,
- priority: task.priority,
- status: task.status,
- evidence: task.evidence,
- sourceTitle: `与 ${conversation.displayName || '客户'} 的真实企微会话`,
- sourceId: task.id,
- updatedAt: task.updatedAt,
- audience: 'external',
- conversationId: conversation.id,
- contactName: conversation.displayName,
- officialTodoId: task.officialTodoId,
- officialSyncStatus: task.officialSyncStatus,
- actions: {
- openConversation: true,
- complete: ['open', 'in_progress'].includes(task.status),
- dismiss: ['open', 'in_progress'].includes(task.status),
- syncOfficial: ['open', 'in_progress'].includes(task.status) && !task.officialTodoId,
- },
- })));
- }
- function collectOfficialTasks(linkedOfficialIds) {
- const index = readJson(paths.official, { todos: [] });
- const rows = Array.isArray(index) ? index : index.todos || [];
- return rows.filter(todo => !linkedOfficialIds.has(String(todo.id || ''))).map(todo => baseTask({
- id: String(todo.id || ''),
- source: 'official',
- title: todo.content,
- owner: (todo.followers || []).map(item => item.name || '企微成员').filter(Boolean).join('、'),
- dueAt: todo.endTime,
- status: Number(todo.status) === 0 ? 'done' : 'open',
- sourceTitle: '当前机器人创建并同步的企微官方待办',
- sourceId: todo.id,
- updatedAt: todo.syncedAt,
- officialTodoId: todo.id,
- actions: { complete: Number(todo.status) !== 0 },
- }));
- }
- function collectGoalTasks() {
- const store = readJson(paths.goals, { plans: [] });
- return (store.plans || []).flatMap(plan => (plan.milestones || []).flatMap(milestone => (milestone.tasks || []).map(task => baseTask({
- id: task.id,
- source: 'goal',
- title: task.title,
- owner: task.owner,
- dueAt: task.deadline,
- priority: task.priority,
- status: ({ planned: 'open', in_progress: 'in_progress', blocked: 'blocked', done: 'done', cancelled: 'dismissed' })[task.status] || 'open',
- evidence: task.acceptance || task.note,
- sourceTitle: `${plan.title} · ${milestone.title}`,
- sourceId: task.id,
- goalId: plan.id,
- updatedAt: task.updatedAt || plan.updatedAt,
- actions: { complete: !['done', 'cancelled'].includes(task.status), dismiss: !['done', 'cancelled'].includes(task.status) },
- }))));
- }
- function collectLocalTasks(store) {
- return store.tasks.map(task => baseTask({
- ...task,
- source: 'local',
- sourceId: task.id,
- actions: {
- complete: ACTIVE_STATUSES.has(task.status),
- dismiss: ACTIVE_STATUSES.has(task.status),
- diagnosisFeedback: task.status === 'done' && task.sourceRef?.type === 'diagnosis-candidate',
- },
- }));
- }
- function collectCandidates(localStore) {
- const promoted = new Map(localStore.tasks.filter(task => task.sourceRef?.key).map(task => [task.sourceRef.key, task.id]));
- const candidates = [];
- const documents = readJson(paths.documents, { documents: [] }).documents || [];
- for (const document of documents) {
- for (const [index, item] of (document.analysis?.actionItems || []).entries()) {
- const key = `document:${document.id}:${stableId(item.title, item.evidence, index)}`;
- candidates.push(baseTask({
- id: key,
- source: 'document-candidate',
- title: item.title,
- owner: item.owner,
- dueAt: item.dueDate,
- priority: item.priority,
- status: 'suggested',
- evidence: item.evidence,
- sourceTitle: document.title,
- sourceId: key,
- confidence: item.confidence,
- promotedTo: promoted.get(key),
- updatedAt: document.analysis?.analyzedAt || document.syncedAt,
- actions: { promote: !promoted.has(key) },
- }));
- }
- }
- const meetings = readJson(paths.meetings, { meetings: [] }).meetings || [];
- for (const meeting of meetings) {
- for (const [index, item] of (meeting.analysis?.actionItems || []).entries()) {
- const key = `meeting:${meeting.id}:${stableId(item.title, item.evidence, index)}`;
- candidates.push(baseTask({
- id: key,
- source: 'meeting-candidate',
- title: item.title,
- owner: item.owner,
- dueAt: item.dueDate,
- priority: item.priority,
- status: 'suggested',
- evidence: item.evidence,
- sourceTitle: meeting.title,
- sourceId: key,
- confidence: item.confidence,
- promotedTo: promoted.get(key),
- updatedAt: meeting.analysis?.analyzedAt || meeting.syncedAt,
- actions: { promote: !promoted.has(key) },
- }));
- }
- }
- const diagnosisStore = readDiagnosisCandidates();
- for (const item of diagnosisStore.candidates) {
- const key = String(item.sourceKey || `diagnosis:${item.id}`);
- candidates.push(baseTask({
- id: key,
- source: 'diagnosis-candidate',
- title: item.title,
- priority: item.priority,
- status: 'suggested',
- evidence: item.evidence,
- sourceTitle: item.sourceTitle || '经营诊断',
- sourceId: key,
- confidence: null,
- promotedTo: item.promotedTaskId || promoted.get(key),
- diagnosisOutcome: item.diagnosisOutcome,
- updatedAt: item.updatedAt || item.createdAt,
- actions: { promote: !(item.promotedTaskId || promoted.has(key)) },
- }));
- }
- return candidates;
- }
- function buildHub() {
- ensureDir(storeDir);
- const localStore = readLocalStore();
- const customerTasks = collectCustomerTasks();
- const linkedOfficialIds = new Set(customerTasks.map(task => task.officialTodoId).filter(Boolean));
- const formalTasks = [
- ...customerTasks,
- ...collectOfficialTasks(linkedOfficialIds),
- ...collectGoalTasks(),
- ...collectLocalTasks(localStore),
- ];
- const candidates = collectCandidates(localStore);
- const tasks = [...formalTasks, ...candidates].sort((a, b) => {
- const statusOrder = value => value.overdue ? 0 : ACTIVE_STATUSES.has(value.status) ? 1 : value.status === 'suggested' ? 2 : 3;
- return statusOrder(a) - statusOrder(b) || String(b.updatedAt).localeCompare(String(a.updatedAt));
- });
- const bySource = {};
- for (const task of tasks) bySource[task.source] = (bySource[task.source] || 0) + 1;
- const diagnosisCandidates = readDiagnosisCandidates().candidates;
- const diagnosisTaskBySource = new Map(localStore.tasks.filter(task => task.sourceRef?.type === 'diagnosis-candidate').map(task => [task.sourceRef.key, task]));
- const promotedDiagnosisCandidates = diagnosisCandidates.filter(item => item.promotedTaskId || diagnosisTaskBySource.has(item.sourceKey));
- const completedDiagnosisCandidates = diagnosisCandidates.filter(item => item.taskStatus === 'done' || diagnosisTaskBySource.get(item.sourceKey)?.status === 'done');
- const effectiveDiagnosisCandidates = diagnosisCandidates.filter(item => item.diagnosisOutcome?.value === 'effective');
- const partiallyEffectiveDiagnosisCandidates = diagnosisCandidates.filter(item => item.diagnosisOutcome?.value === 'partially_effective');
- const ineffectiveDiagnosisCandidates = diagnosisCandidates.filter(item => item.diagnosisOutcome?.value === 'ineffective');
- const verifiedDiagnosisCandidates = effectiveDiagnosisCandidates.length + partiallyEffectiveDiagnosisCandidates.length + ineffectiveDiagnosisCandidates.length;
- return okResult({
- assistantMessage: `统一任务中心已汇总 ${formalTasks.length} 条正式任务和 ${candidates.length} 条待人工确认候选。`,
- summary: {
- total: formalTasks.length,
- active: formalTasks.filter(task => ACTIVE_STATUSES.has(task.status)).length,
- completed: formalTasks.filter(task => task.status === 'done').length,
- overdue: formalTasks.filter(task => task.overdue).length,
- candidates: candidates.filter(task => !task.promotedTo).length,
- promotedCandidates: candidates.filter(task => task.promotedTo).length,
- deduplicatedOfficialCount: linkedOfficialIds.size,
- bySource,
- diagnosisFeedback: {
- candidates: diagnosisCandidates.length,
- promoted: promotedDiagnosisCandidates.length,
- completed: completedDiagnosisCandidates.length,
- effective: effectiveDiagnosisCandidates.length,
- partiallyEffective: partiallyEffectiveDiagnosisCandidates.length,
- ineffective: ineffectiveDiagnosisCandidates.length,
- adoptionRate: diagnosisCandidates.length ? promotedDiagnosisCandidates.length / diagnosisCandidates.length : null,
- effectivenessRate: verifiedDiagnosisCandidates ? effectiveDiagnosisCandidates.length / verifiedDiagnosisCandidates : null,
- },
- },
- data: {
- tasks,
- storeDir,
- policy: {
- internalTasksOnly: true,
- externalCustomerSafety: '客户会话任务只能转到智能会话,由 Agent 结合上下文拟定回复;系统不会把内部任务标题直接发送给客户。',
- candidateSafety: '文档、会议和经营诊断中的建议默认只是候选,必须人工确认后才会转为正式内部任务。',
- },
- },
- files: [localIndexPath].filter(fs.existsSync),
- });
- }
- function createLocalTask(input = {}) {
- const title = String(input.title || '').trim();
- if (!title) return errorResult('请输入任务标题。');
- const store = readLocalStore();
- const sourceKey = String(input.sourceKey || input.sourceId || '').trim();
- if (sourceKey) {
- const existing = store.tasks.find(task => task.sourceRef?.key === sourceKey);
- if (existing) return okResult({ assistantMessage: '该候选已转为正式内部任务,无需重复创建。', summary: { created: false }, data: { task: existing } });
- }
- const now = new Date().toISOString();
- const task = {
- id: `local_${crypto.randomUUID()}`,
- title,
- owner: String(input.owner || '').trim(),
- dueAt: normalizeDate(input.dueAt),
- priority: String(input.priority || 'medium'),
- status: 'open',
- evidence: String(input.evidence || '').trim(),
- sourceTitle: String(input.sourceTitle || '人工创建'),
- sourceRef: sourceKey ? { key: sourceKey, type: String(input.source || 'candidate') } : null,
- audience: 'internal',
- createdAt: now,
- updatedAt: now,
- };
- store.tasks.push(task);
- persistLocalStore(store);
- if (sourceKey && task.sourceRef?.type === 'diagnosis-candidate') {
- const diagnosisStore = readDiagnosisCandidates();
- const candidate = findDiagnosisCandidate(diagnosisStore, sourceKey);
- if (candidate) {
- candidate.promotedTaskId = task.id;
- candidate.promotedAt = now;
- candidate.taskStatus = task.status;
- candidate.updatedAt = now;
- appendDiagnosisLifecycle(candidate, 'promoted', { taskId: task.id });
- persistDiagnosisCandidates(diagnosisStore);
- }
- }
- return okResult({ assistantMessage: `已创建内部任务“${title}”。`, summary: { created: true }, data: { task }, files: [localIndexPath, path.join(storeDir, 'README.md')] });
- }
- function createDiagnosisCandidate(input = {}) {
- const question = String(input.question || '').trim().slice(0, 500);
- const actionInput = input.action && typeof input.action === 'object' ? input.action : input;
- const title = String(actionInput.label || actionInput.title || '').trim().slice(0, 200);
- if (!question) return errorResult('缺少经营诊断问题。');
- if (!title) return errorResult('缺少建议动作标题。');
- const actionId = String(actionInput.id || actionInput.actionId || title).trim();
- const scopeDate = String(input.scope?.date || input.date || '').trim();
- const sourceKey = `diagnosis:${stableId(question, actionId, scopeDate)}`;
- const store = readDiagnosisCandidates();
- const existing = store.candidates.find(item => item.sourceKey === sourceKey);
- if (existing) return okResult({ assistantMessage: '该诊断建议已在今日行动候选中,无需重复加入。', summary: { created: false, duplicate: true }, data: { candidate: existing }, files: [paths.diagnoses] });
- const evidenceItems = Array.isArray(input.evidence) ? input.evidence : [];
- const gapItems = Array.isArray(input.dataGaps) ? input.dataGaps : [];
- const evidenceText = evidenceItems.slice(0, 3).map(item => typeof item === 'string' ? item : `${item.label || '指标'} ${item.value ?? ''}`.trim()).filter(Boolean).join(';');
- const gapText = gapItems.slice(0, 2).map(item => item.label || item.code).filter(Boolean).join('、');
- const now = new Date().toISOString();
- const candidate = {
- id: `diagnosis_${crypto.randomUUID()}`,
- sourceKey,
- diagnosisType: String(input.diagnosisType || input.type || 'general'),
- question,
- actionId,
- title,
- description: String(actionInput.description || '').trim().slice(0, 500),
- route: String(actionInput.route || actionInput.hash || '').trim().slice(0, 100),
- priority: ['low', 'medium', 'high', 'urgent'].includes(actionInput.priority) ? actionInput.priority : 'medium',
- evidence: [String(actionInput.description || '').trim(), evidenceText, gapText ? `数据缺口:${gapText}` : ''].filter(Boolean).join(';').slice(0, 1200),
- sourceTitle: `经营诊断 · ${question}`.slice(0, 300),
- taskStatus: 'suggested',
- lifecycle: [{ event: 'created', at: now }],
- createdAt: now,
- updatedAt: now,
- };
- store.candidates.push(candidate);
- store.candidates = store.candidates.slice(-500);
- persistDiagnosisCandidates(store);
- return okResult({
- assistantMessage: `已将“${title}”加入今日行动候选,等待人工确认后转为正式任务。`,
- summary: { created: true, requiresPromotion: true, readOnlyDiagnosisPreserved: true },
- data: { candidate },
- files: [paths.diagnoses],
- nextActions: [{ id: 'review-candidate', label: '前往统一任务中心确认', route: '#knowledge', requiresConfirmation: false }]
- });
- }
- async function update(input = {}) {
- const source = String(input.source || '');
- const taskId = String(input.taskId || input.id || '');
- const status = String(input.status || '');
- if (!taskId || !status) return errorResult('更新任务需要 taskId 和 status。');
- if (source === 'local') {
- if (!LOCAL_STATUSES.has(status)) return errorResult('不支持的本地任务状态。');
- const store = readLocalStore();
- const task = store.tasks.find(item => item.id === taskId);
- if (!task) return errorResult('本地任务不存在。');
- task.status = status;
- task.updatedAt = new Date().toISOString();
- persistLocalStore(store);
- if (task.sourceRef?.type === 'diagnosis-candidate') {
- const diagnosisStore = readDiagnosisCandidates();
- const candidate = findDiagnosisCandidate(diagnosisStore, task.sourceRef.key);
- if (candidate) {
- candidate.promotedTaskId ||= task.id;
- candidate.taskStatus = status;
- candidate.updatedAt = task.updatedAt;
- if (status === 'done') candidate.completedAt = task.updatedAt;
- if (status === 'dismissed') candidate.dismissedAt = task.updatedAt;
- appendDiagnosisLifecycle(candidate, status === 'done' ? 'completed' : status === 'dismissed' ? 'dismissed' : 'status_updated', { taskId: task.id, status });
- persistDiagnosisCandidates(diagnosisStore);
- }
- }
- return okResult({ assistantMessage: `本地任务已更新为 ${status}。`, data: { task } });
- }
- if (source === 'customer') return customerUpdater(taskId, { status });
- if (source === 'official') {
- if (status !== 'done') return errorResult('企微官方待办当前只支持标记完成。');
- return officialCompleter(taskId);
- }
- if (source === 'goal') {
- const mapped = ({ open: 'planned', in_progress: 'in_progress', blocked: 'blocked', done: 'done', dismissed: 'cancelled' })[status];
- if (!mapped) return errorResult('不支持的目标任务状态。');
- return goalUpdater({ goalId: String(input.goalId || ''), taskId, status: mapped });
- }
- return errorResult('该来源的任务不能直接更新;AI 候选需先人工确认。');
- }
- function updateDiagnosisOutcome(input = {}) {
- const taskId = String(input.taskId || input.id || '').trim();
- const outcome = String(input.outcome || input.value || '').trim();
- const note = String(input.note || input.evidence || '').trim().slice(0, 1000);
- const actor = String(input.actor || 'dashboard-user').trim().slice(0, 100);
- if (!taskId) return errorResult('记录诊断行动反馈需要 taskId。');
- if (!DIAGNOSIS_OUTCOMES.has(outcome)) return errorResult('反馈结果必须是 effective、partially_effective、ineffective 或 unknown。');
- const localStore = readLocalStore();
- const task = localStore.tasks.find(item => item.id === taskId);
- if (!task) return errorResult('本地任务不存在。');
- if (task.sourceRef?.type !== 'diagnosis-candidate') return errorResult('只有经营诊断来源的正式任务可以记录效果反馈。');
- if (task.status !== 'done') return errorResult('请先将任务标记为已完成,再记录效果反馈。');
- const diagnosisStore = readDiagnosisCandidates();
- const candidate = findDiagnosisCandidate(diagnosisStore, task.sourceRef.key);
- if (!candidate) return errorResult('未找到该任务对应的经营诊断候选。');
- const recordedAt = new Date().toISOString();
- const diagnosisOutcome = { value: outcome, note, actor, recordedAt };
- task.diagnosisOutcome = diagnosisOutcome;
- task.updatedAt = recordedAt;
- candidate.diagnosisOutcome = diagnosisOutcome;
- candidate.feedbackAt = recordedAt;
- candidate.updatedAt = recordedAt;
- appendDiagnosisLifecycle(candidate, 'outcome_recorded', { taskId, outcome, actor });
- persistLocalStore(localStore);
- persistDiagnosisCandidates(diagnosisStore);
- return okResult({
- assistantMessage: '经营诊断行动效果已记录,仅用于内部复盘。',
- summary: { recorded: true, outcome, internalOnly: true, customerMessageSent: false },
- data: { task, candidate },
- files: [localIndexPath, paths.diagnoses],
- });
- }
- return { hub: buildHub, createLocalTask, createDiagnosisCandidate, updateDiagnosisOutcome, update, readLocalStore, readDiagnosisCandidates };
- }
- const service = createUnifiedTaskService();
- module.exports = {
- createUnifiedTaskService,
- getUnifiedTaskHub: service.hub,
- createUnifiedLocalTask: service.createLocalTask,
- createUnifiedDiagnosisCandidate: service.createDiagnosisCandidate,
- updateUnifiedDiagnosisOutcome: service.updateDiagnosisOutcome,
- updateUnifiedTask: service.update,
- __testing: { baseTask, stableId, isOverdue, sourceMeta },
- };
|