personalac 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +249 -0
- package/bin/personalac.js +215 -0
- package/dist/agent/context.d.ts +39 -0
- package/dist/agent/context.d.ts.map +1 -0
- package/dist/agent/context.js +163 -0
- package/dist/agent/context.js.map +1 -0
- package/dist/agent/executors.d.ts +40 -0
- package/dist/agent/executors.d.ts.map +1 -0
- package/dist/agent/executors.js +177 -0
- package/dist/agent/executors.js.map +1 -0
- package/dist/agent/index.d.ts +61 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +907 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/planner.d.ts +15 -0
- package/dist/agent/planner.d.ts.map +1 -0
- package/dist/agent/planner.js +81 -0
- package/dist/agent/planner.js.map +1 -0
- package/dist/agent/scheduler.d.ts +22 -0
- package/dist/agent/scheduler.d.ts.map +1 -0
- package/dist/agent/scheduler.js +178 -0
- package/dist/agent/scheduler.js.map +1 -0
- package/dist/database/index.d.ts +5 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +283 -0
- package/dist/database/index.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +141 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.middleware.d.ts +6 -0
- package/dist/middleware/auth.middleware.d.ts.map +1 -0
- package/dist/middleware/auth.middleware.js +19 -0
- package/dist/middleware/auth.middleware.js.map +1 -0
- package/dist/routes/agent.routes.d.ts +3 -0
- package/dist/routes/agent.routes.d.ts.map +1 -0
- package/dist/routes/agent.routes.js +101 -0
- package/dist/routes/agent.routes.js.map +1 -0
- package/dist/routes/auth.routes.d.ts +3 -0
- package/dist/routes/auth.routes.d.ts.map +1 -0
- package/dist/routes/auth.routes.js +66 -0
- package/dist/routes/auth.routes.js.map +1 -0
- package/dist/routes/chat.routes.d.ts +3 -0
- package/dist/routes/chat.routes.d.ts.map +1 -0
- package/dist/routes/chat.routes.js +80 -0
- package/dist/routes/chat.routes.js.map +1 -0
- package/dist/routes/data.routes.d.ts +3 -0
- package/dist/routes/data.routes.d.ts.map +1 -0
- package/dist/routes/data.routes.js +16 -0
- package/dist/routes/data.routes.js.map +1 -0
- package/dist/routes/email.routes.d.ts +3 -0
- package/dist/routes/email.routes.d.ts.map +1 -0
- package/dist/routes/email.routes.js +18 -0
- package/dist/routes/email.routes.js.map +1 -0
- package/dist/routes/goals.routes.d.ts +3 -0
- package/dist/routes/goals.routes.d.ts.map +1 -0
- package/dist/routes/goals.routes.js +45 -0
- package/dist/routes/goals.routes.js.map +1 -0
- package/dist/routes/plans.routes.d.ts +3 -0
- package/dist/routes/plans.routes.d.ts.map +1 -0
- package/dist/routes/plans.routes.js +19 -0
- package/dist/routes/plans.routes.js.map +1 -0
- package/dist/routes/settings.routes.d.ts +3 -0
- package/dist/routes/settings.routes.d.ts.map +1 -0
- package/dist/routes/settings.routes.js +29 -0
- package/dist/routes/settings.routes.js.map +1 -0
- package/dist/routes/workspace.routes.d.ts +3 -0
- package/dist/routes/workspace.routes.d.ts.map +1 -0
- package/dist/routes/workspace.routes.js +28 -0
- package/dist/routes/workspace.routes.js.map +1 -0
- package/dist/services/auth.service.d.ts +32 -0
- package/dist/services/auth.service.d.ts.map +1 -0
- package/dist/services/auth.service.js +110 -0
- package/dist/services/auth.service.js.map +1 -0
- package/dist/services/data.service.d.ts +47 -0
- package/dist/services/data.service.d.ts.map +1 -0
- package/dist/services/data.service.js +139 -0
- package/dist/services/data.service.js.map +1 -0
- package/dist/services/email.service.d.ts +23 -0
- package/dist/services/email.service.d.ts.map +1 -0
- package/dist/services/email.service.js +316 -0
- package/dist/services/email.service.js.map +1 -0
- package/dist/services/notify.service.d.ts +2 -0
- package/dist/services/notify.service.d.ts.map +1 -0
- package/dist/services/notify.service.js +21 -0
- package/dist/services/notify.service.js.map +1 -0
- package/dist/services/plan.service.d.ts +21 -0
- package/dist/services/plan.service.d.ts.map +1 -0
- package/dist/services/plan.service.js +96 -0
- package/dist/services/plan.service.js.map +1 -0
- package/dist/services/resource.service.d.ts +27 -0
- package/dist/services/resource.service.d.ts.map +1 -0
- package/dist/services/resource.service.js +150 -0
- package/dist/services/resource.service.js.map +1 -0
- package/dist/services/settings.service.d.ts +41 -0
- package/dist/services/settings.service.d.ts.map +1 -0
- package/dist/services/settings.service.js +254 -0
- package/dist/services/settings.service.js.map +1 -0
- package/dist/services/sobriety.service.d.ts +41 -0
- package/dist/services/sobriety.service.d.ts.map +1 -0
- package/dist/services/sobriety.service.js +243 -0
- package/dist/services/sobriety.service.js.map +1 -0
- package/dist/services/workspace.service.d.ts +71 -0
- package/dist/services/workspace.service.d.ts.map +1 -0
- package/dist/services/workspace.service.js +265 -0
- package/dist/services/workspace.service.js.map +1 -0
- package/dist/tools/chat-tools.d.ts +2 -0
- package/dist/tools/chat-tools.d.ts.map +1 -0
- package/dist/tools/chat-tools.js +419 -0
- package/dist/tools/chat-tools.js.map +1 -0
- package/dist/tools/index.d.ts +19 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +27 -0
- package/dist/tools/index.js.map +1 -0
- package/package.json +38 -0
- package/public/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/public/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/public/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/public/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/public/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/public/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/public/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/public/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/public/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/public/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/public/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/public/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/public/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/public/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/public/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/public/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/public/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/public/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/public/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/public/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/public/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/public/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/public/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/public/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/public/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/public/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/public/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/public/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/public/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/public/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/public/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/public/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/public/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/public/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/public/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/public/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/public/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/public/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/public/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/public/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/public/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/public/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/public/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/public/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/public/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/public/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/public/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/public/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/public/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/public/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/public/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/public/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/public/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/public/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/public/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/public/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/public/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/public/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/public/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/public/assets/_baseUniq-DiHtXYsr.js +1 -0
- package/public/assets/arc-Bv1zbXf7.js +1 -0
- package/public/assets/architectureDiagram-Q4EWVU46-EbiUvXBr.js +36 -0
- package/public/assets/blockDiagram-DXYQGD6D-B-4wwQNT.js +132 -0
- package/public/assets/c4Diagram-AHTNJAMY-brLyKMPJ.js +10 -0
- package/public/assets/channel-Me4d6Y1x.js +1 -0
- package/public/assets/chunk-4BX2VUAB-DMh-4Nye.js +1 -0
- package/public/assets/chunk-4TB4RGXK-CSr1z2CQ.js +206 -0
- package/public/assets/chunk-55IACEB6-xN0xmcvD.js +1 -0
- package/public/assets/chunk-EDXVE4YY-CkCM3j5E.js +1 -0
- package/public/assets/chunk-FMBD7UC4-B-mDgsjM.js +15 -0
- package/public/assets/chunk-OYMX7WX6-DK8dg9K4.js +231 -0
- package/public/assets/chunk-QZHKN3VN-BZGbFOdK.js +1 -0
- package/public/assets/chunk-YZCP3GAM-BTmFmMer.js +1 -0
- package/public/assets/classDiagram-6PBFFD2Q-BXgAb5m4.js +1 -0
- package/public/assets/classDiagram-v2-HSJHXN6E-BXgAb5m4.js +1 -0
- package/public/assets/clone-rnECnpTI.js +1 -0
- package/public/assets/cose-bilkent-S5V4N54A-BBqCwo63.js +1 -0
- package/public/assets/cytoscape.esm-BQk4lpUV.js +331 -0
- package/public/assets/dagre-KV5264BT-DLeQYvAA.js +4 -0
- package/public/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/public/assets/diagram-5BDNPKRD-D85z2LaM.js +10 -0
- package/public/assets/diagram-G4DWMVQ6-4AvBmvcP.js +24 -0
- package/public/assets/diagram-MMDJMWI5-DRNEGNZA.js +43 -0
- package/public/assets/diagram-TYMM5635-C_oGwrmX.js +24 -0
- package/public/assets/erDiagram-SMLLAGMA-DDSp8ANa.js +85 -0
- package/public/assets/flowDiagram-DWJPFMVM-B1Wy36da.js +162 -0
- package/public/assets/ganttDiagram-T4ZO3ILL-CQ7VWheo.js +292 -0
- package/public/assets/gitGraphDiagram-UUTBAWPF-BhvM7s41.js +106 -0
- package/public/assets/graph-fatjTp1h.js +1 -0
- package/public/assets/index-1PrEnCZL.js +677 -0
- package/public/assets/index-DcsnItNp.css +1 -0
- package/public/assets/infoDiagram-42DDH7IO-4eawOC2G.js +2 -0
- package/public/assets/init-Gi6I4Gst.js +1 -0
- package/public/assets/ishikawaDiagram-UXIWVN3A-CJWfG1fv.js +70 -0
- package/public/assets/journeyDiagram-VCZTEJTY-C4Zx3dpF.js +139 -0
- package/public/assets/kanban-definition-6JOO6SKY-CV1vDcC8.js +89 -0
- package/public/assets/layout-CX0IDv3q.js +1 -0
- package/public/assets/linear-C8hMcu9f.js +1 -0
- package/public/assets/min-BaxMGW9J.js +1 -0
- package/public/assets/mindmap-definition-QFDTVHPH-KzGyX4Em.js +96 -0
- package/public/assets/ordinal-Cboi1Yqb.js +1 -0
- package/public/assets/pieDiagram-DEJITSTG-Dp2x5kvP.js +30 -0
- package/public/assets/quadrantDiagram-34T5L4WZ-B4yqA6XF.js +7 -0
- package/public/assets/requirementDiagram-MS252O5E-DEjN_2tG.js +84 -0
- package/public/assets/sankeyDiagram-XADWPNL6-BKm_F5FA.js +10 -0
- package/public/assets/sequenceDiagram-FGHM5R23-07BCMKjW.js +157 -0
- package/public/assets/stateDiagram-FHFEXIEX-DVz6GtR8.js +1 -0
- package/public/assets/stateDiagram-v2-QKLJ7IA2-DYiYr0PB.js +1 -0
- package/public/assets/timeline-definition-GMOUNBTQ-C0u67FqY.js +120 -0
- package/public/assets/vennDiagram-DHZGUBPP-BiAlMVLe.js +34 -0
- package/public/assets/wardley-RL74JXVD-BnWyuHzf.js +162 -0
- package/public/assets/wardleyDiagram-NUSXRM2D-DEL2Lx6S.js +20 -0
- package/public/assets/xychartDiagram-5P7HB3ND-ILdmj7ok.js +7 -0
- package/public/index.html +17 -0
|
@@ -0,0 +1,907 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.AgentEngine = void 0;
|
|
40
|
+
exports.getAgentEngine = getAgentEngine;
|
|
41
|
+
const axios_1 = __importDefault(require("axios"));
|
|
42
|
+
const uuid_1 = require("uuid");
|
|
43
|
+
const database_1 = require("../database");
|
|
44
|
+
const context_1 = require("./context");
|
|
45
|
+
const scheduler_1 = require("./scheduler");
|
|
46
|
+
const settings_service_1 = require("../services/settings.service");
|
|
47
|
+
const notify_service_1 = require("../services/notify.service");
|
|
48
|
+
const planner_1 = require("./planner");
|
|
49
|
+
const executors_1 = require("./executors");
|
|
50
|
+
const node_cron_1 = __importDefault(require("node-cron"));
|
|
51
|
+
let agentEngineInstance = null;
|
|
52
|
+
function getAgentEngine() {
|
|
53
|
+
return agentEngineInstance;
|
|
54
|
+
}
|
|
55
|
+
class AgentEngine {
|
|
56
|
+
constructor() {
|
|
57
|
+
this.doNotDisturb = null;
|
|
58
|
+
this.processingStudents = new Set();
|
|
59
|
+
this.dailyCronJob = null;
|
|
60
|
+
this.sobrietyCronJob = null;
|
|
61
|
+
this.context = new context_1.AgentContext();
|
|
62
|
+
this.scheduler = new scheduler_1.AgentScheduler((studentId, scheduleId, description) => {
|
|
63
|
+
this.handleEvent({
|
|
64
|
+
type: 'new_learning_data',
|
|
65
|
+
studentId,
|
|
66
|
+
triggerSource: `schedule:${scheduleId}`,
|
|
67
|
+
description
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
agentEngineInstance = this;
|
|
71
|
+
}
|
|
72
|
+
init() {
|
|
73
|
+
this.scheduler.init();
|
|
74
|
+
// Daily review cron at 20:00
|
|
75
|
+
this.dailyCronJob = node_cron_1.default.schedule('0 20 * * *', () => {
|
|
76
|
+
console.log('Daily review cron triggered at 20:00');
|
|
77
|
+
this.generateDailyReview('superadmin').catch((err) => {
|
|
78
|
+
console.error('Daily review cron error:', err);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
// 自我清醒:每小时刷新一次所有学生快照
|
|
82
|
+
this.sobrietyCronJob = node_cron_1.default.schedule('0 * * * *', () => {
|
|
83
|
+
this.refreshAllSobrietySnapshots().catch((err) => {
|
|
84
|
+
console.error('Sobriety cron error:', err);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
// 启动 5 秒后跑一次冷启动刷新
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
this.refreshAllSobrietySnapshots().catch((err) => {
|
|
90
|
+
console.error('Sobriety initial refresh error:', err);
|
|
91
|
+
});
|
|
92
|
+
}, 5000);
|
|
93
|
+
console.log('AgentEngine initialized');
|
|
94
|
+
}
|
|
95
|
+
// ─────────────────────────────────────────────
|
|
96
|
+
// 自我清醒 — 周期刷新所有学生快照
|
|
97
|
+
// ─────────────────────────────────────────────
|
|
98
|
+
async refreshAllSobrietySnapshots() {
|
|
99
|
+
const db = (0, database_1.getDB)();
|
|
100
|
+
const students = db.prepare(`SELECT id FROM User WHERE role='student' AND delete_flag=0`).all();
|
|
101
|
+
if (students.length === 0)
|
|
102
|
+
return;
|
|
103
|
+
const { refreshSobrietySnapshot, getLastUrgency, markNotified } = await Promise.resolve().then(() => __importStar(require('../services/sobriety.service')));
|
|
104
|
+
for (const s of students) {
|
|
105
|
+
try {
|
|
106
|
+
const prev = getLastUrgency(s.id);
|
|
107
|
+
const snap = refreshSobrietySnapshot(s.id);
|
|
108
|
+
// 紧迫度升级到 urgent → 写一条 AgentLog(避免重复推送)
|
|
109
|
+
if (snap.urgency.level === 'urgent' && prev !== 'urgent' && !this.isDoNotDisturb()) {
|
|
110
|
+
await this.logAction(s.id, 'sobriety_alert', snap.urgency.reasons.join(';'), 'sobriety_cron', null, 'success');
|
|
111
|
+
markNotified(s.id);
|
|
112
|
+
console.log(`[Sobriety] urgent escalation for ${s.id}: ${snap.urgency.reasons.join('; ')}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
console.error(`refreshSobrietySnapshot failed for ${s.id}:`, err);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// ─────────────────────────────────────────────
|
|
121
|
+
// Task persistence helpers
|
|
122
|
+
// ─────────────────────────────────────────────
|
|
123
|
+
createTask(taskType, studentId, triggerType, inputSummary) {
|
|
124
|
+
const db = (0, database_1.getDB)();
|
|
125
|
+
const taskId = (0, uuid_1.v4)();
|
|
126
|
+
const now = new Date().toISOString();
|
|
127
|
+
db.prepare(`
|
|
128
|
+
INSERT INTO AgentTask (id, task_type, student_id, status, trigger_type, input_summary, create_time, update_time, delete_flag)
|
|
129
|
+
VALUES (?, ?, ?, 'pending', ?, ?, ?, ?, 0)
|
|
130
|
+
`).run(taskId, taskType, studentId, triggerType, inputSummary || null, now, now);
|
|
131
|
+
return taskId;
|
|
132
|
+
}
|
|
133
|
+
updateTask(taskId, status, output, error) {
|
|
134
|
+
const db = (0, database_1.getDB)();
|
|
135
|
+
const now = new Date().toISOString();
|
|
136
|
+
if (status === 'running') {
|
|
137
|
+
db.prepare(`
|
|
138
|
+
UPDATE AgentTask SET status = 'running', started_at = ?, update_time = ? WHERE id = ?
|
|
139
|
+
`).run(now, now, taskId);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
db.prepare(`
|
|
143
|
+
UPDATE AgentTask SET status = ?, output = ?, error = ?, completed_at = ?, update_time = ? WHERE id = ?
|
|
144
|
+
`).run(status, output || null, error || null, now, now, taskId);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ─────────────────────────────────────────────
|
|
148
|
+
// AI call
|
|
149
|
+
// ─────────────────────────────────────────────
|
|
150
|
+
async callAI(prompt, model, apiKey, baseUrl) {
|
|
151
|
+
try {
|
|
152
|
+
const configResult = (0, settings_service_1.getAIConfig)();
|
|
153
|
+
const aiConfig = configResult.data;
|
|
154
|
+
const rawModel = model || aiConfig?.modelId || 'gpt-3.5-turbo';
|
|
155
|
+
const finalApiKey = apiKey || aiConfig?.apiKey;
|
|
156
|
+
const finalBaseUrl = (baseUrl || aiConfig?.baseUrl || 'https://api.openai.com/v1').replace(/\/$/, '');
|
|
157
|
+
if (!finalApiKey) {
|
|
158
|
+
console.warn('No API key configured');
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
// models.dev 格式 "provider/model" → 只取模型名
|
|
162
|
+
const finalModel = rawModel.includes('/') ? rawModel.split('/').slice(1).join('/') : rawModel;
|
|
163
|
+
// base URL 含 /anthropic → 走 Anthropic Messages API
|
|
164
|
+
const isAnthropic = finalBaseUrl.includes('/anthropic');
|
|
165
|
+
if (isAnthropic) {
|
|
166
|
+
const endpoint = finalBaseUrl + '/messages';
|
|
167
|
+
const response = await axios_1.default.post(endpoint, {
|
|
168
|
+
model: finalModel,
|
|
169
|
+
max_tokens: 2000,
|
|
170
|
+
messages: [{ role: 'user', content: prompt }]
|
|
171
|
+
}, {
|
|
172
|
+
headers: {
|
|
173
|
+
'x-api-key': finalApiKey,
|
|
174
|
+
'anthropic-version': '2023-06-01',
|
|
175
|
+
'Content-Type': 'application/json'
|
|
176
|
+
},
|
|
177
|
+
timeout: 30000
|
|
178
|
+
});
|
|
179
|
+
// content 可能含 thinking 块,找第一个 type=text 的块
|
|
180
|
+
const textBlock = response.data?.content
|
|
181
|
+
?.find((c) => c.type === 'text');
|
|
182
|
+
return textBlock?.text || null;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const endpoint = finalBaseUrl + '/chat/completions';
|
|
186
|
+
const response = await axios_1.default.post(endpoint, {
|
|
187
|
+
model: finalModel,
|
|
188
|
+
messages: [{ role: 'user', content: prompt }],
|
|
189
|
+
temperature: 0.7,
|
|
190
|
+
max_tokens: 2000
|
|
191
|
+
}, {
|
|
192
|
+
headers: {
|
|
193
|
+
Authorization: `Bearer ${finalApiKey}`,
|
|
194
|
+
'Content-Type': 'application/json'
|
|
195
|
+
},
|
|
196
|
+
timeout: 30000
|
|
197
|
+
});
|
|
198
|
+
return response.data?.choices?.[0]?.message?.content || null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
console.error('callAI error:', err);
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// ─────────────────────────────────────────────
|
|
207
|
+
// Two-stage autonomous cycle: Planner → Executors
|
|
208
|
+
// ─────────────────────────────────────────────
|
|
209
|
+
async runAutonomousCycle(studentId) {
|
|
210
|
+
if (this.processingStudents.has(studentId)) {
|
|
211
|
+
console.log(`Autonomous cycle already running for student ${studentId}, skipping`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (this.isDoNotDisturb()) {
|
|
215
|
+
console.log(`Do-not-disturb active, skipping autonomous cycle for student ${studentId}`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.processingStudents.add(studentId);
|
|
219
|
+
try {
|
|
220
|
+
console.log(`Starting autonomous cycle for student ${studentId}`);
|
|
221
|
+
const configResult = (0, settings_service_1.getAIConfig)();
|
|
222
|
+
if (!configResult.success || !configResult.data?.apiKey) {
|
|
223
|
+
console.warn('AI config not set, skipping autonomous cycle');
|
|
224
|
+
await this.logAction(studentId, 'no_action', 'AI 未配置,跳过', 'autonomous', null, 'success');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const aiConfig = configResult.data;
|
|
228
|
+
// Stage 1: Build context once, shared across all executors
|
|
229
|
+
let contextData;
|
|
230
|
+
try {
|
|
231
|
+
contextData = this.context.buildContext(studentId);
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
console.error('Failed to build context:', err);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
// Stage 2: Planner decides which tasks to run
|
|
238
|
+
const plannerPrompt = (0, planner_1.buildPlannerPrompt)(contextData);
|
|
239
|
+
const plannerResponse = await this.callAI(plannerPrompt, aiConfig.modelId, aiConfig.apiKey, aiConfig.baseUrl);
|
|
240
|
+
if (!plannerResponse) {
|
|
241
|
+
await this.logAction(studentId, 'no_action', 'Planner AI 无响应', 'autonomous', aiConfig.modelId, 'failed');
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const plan = (0, planner_1.parsePlan)(plannerResponse);
|
|
245
|
+
const activeTasks = (plan?.tasks ?? []).filter((t) => t.type !== 'no_action');
|
|
246
|
+
if (activeTasks.length === 0) {
|
|
247
|
+
console.log(`Planner: no tasks needed for student ${studentId}. Thinking: ${plan?.thinking ?? '—'}`);
|
|
248
|
+
await this.logAction(studentId, 'no_action', plan?.thinking || '规划器判断无需操作', 'autonomous', aiConfig.modelId, 'success');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
console.log(`Planner scheduled ${activeTasks.length} tasks for ${studentId}: ${activeTasks.map((t) => t.type).join(', ')}`);
|
|
252
|
+
// Stage 3: Execute all tasks in parallel
|
|
253
|
+
await Promise.all(activeTasks.map((task) => this.runExecutorTask(task, studentId, contextData, aiConfig)));
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
console.error(`Autonomous cycle error for student ${studentId}:`, err);
|
|
257
|
+
await this.logAction(studentId, 'no_action', `自主循环错误: ${err instanceof Error ? err.message : String(err)}`, 'autonomous', null, 'failed');
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
this.processingStudents.delete(studentId);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async runExecutorTask(task, studentId, contextData, aiConfig) {
|
|
264
|
+
const taskId = this.createTask(task.type, studentId, 'autonomous', task.reason);
|
|
265
|
+
this.updateTask(taskId, 'running');
|
|
266
|
+
try {
|
|
267
|
+
const executor = (0, executors_1.getExecutor)(task.type);
|
|
268
|
+
if (!executor) {
|
|
269
|
+
this.updateTask(taskId, 'failed', undefined, `未找到执行器: ${task.type}`);
|
|
270
|
+
console.warn(`No executor registered for task type: ${task.type}`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const result = await executor(task, contextData, aiConfig, this.callAI.bind(this));
|
|
274
|
+
if (result.success && result.output) {
|
|
275
|
+
this.updateTask(taskId, 'completed', JSON.stringify(result.output));
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
this.updateTask(taskId, 'failed', undefined, result.error || '执行失败');
|
|
279
|
+
}
|
|
280
|
+
if (result.sideEffects) {
|
|
281
|
+
for (const effect of result.sideEffects) {
|
|
282
|
+
await this.applySideEffect(effect, studentId);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
await this.logAction(studentId, task.type, task.reason, 'autonomous', aiConfig.modelId, result.success ? 'success' : 'failed', result.error);
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
289
|
+
this.updateTask(taskId, 'failed', undefined, msg);
|
|
290
|
+
console.error(`Executor error for task type "${task.type}":`, err);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async applySideEffect(effect, studentId) {
|
|
294
|
+
try {
|
|
295
|
+
if (effect.type === 'bot_message') {
|
|
296
|
+
await (0, notify_service_1.sendMessage)(effect.userId || studentId, effect.content);
|
|
297
|
+
}
|
|
298
|
+
else if (effect.type === 'set_schedule') {
|
|
299
|
+
this.scheduler.createSchedule(effect.studentId || studentId, effect.cron, effect.description);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
console.error('applySideEffect error:', err);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// ─────────────────────────────────────────────
|
|
307
|
+
// Proactive task methods (direct IPC triggers)
|
|
308
|
+
// These use the same executor logic for consistency.
|
|
309
|
+
// ─────────────────────────────────────────────
|
|
310
|
+
async generateResourceBrief(resourceInfo) {
|
|
311
|
+
const studentId = 'superadmin';
|
|
312
|
+
const summary = resourceInfo.fileName
|
|
313
|
+
? `资源:${resourceInfo.fileName}${resourceInfo.subject ? ' [' + resourceInfo.subject + ']' : ''}`
|
|
314
|
+
: '新资源';
|
|
315
|
+
const taskId = this.createTask('resource_brief', studentId, 'email_resource', summary);
|
|
316
|
+
try {
|
|
317
|
+
this.updateTask(taskId, 'running');
|
|
318
|
+
const configResult = (0, settings_service_1.getAIConfig)();
|
|
319
|
+
if (!configResult.success || !configResult.data?.apiKey) {
|
|
320
|
+
this.updateTask(taskId, 'failed', undefined, 'AI 未配置');
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const aiConfig = configResult.data;
|
|
324
|
+
const contextData = this.context.buildContext(studentId);
|
|
325
|
+
const task = {
|
|
326
|
+
type: 'resource_brief',
|
|
327
|
+
priority: 'high',
|
|
328
|
+
reason: summary,
|
|
329
|
+
params: {
|
|
330
|
+
fileName: resourceInfo.fileName,
|
|
331
|
+
subject: resourceInfo.subject,
|
|
332
|
+
fileType: resourceInfo.fileType
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
const result = await (0, executors_1.resourceBriefExecutor)(task, contextData, aiConfig, this.callAI.bind(this));
|
|
336
|
+
if (result.success && result.output) {
|
|
337
|
+
this.updateTask(taskId, 'completed', JSON.stringify(result.output));
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
this.updateTask(taskId, 'failed', undefined, result.error);
|
|
341
|
+
}
|
|
342
|
+
await this.logAction(studentId, 'resource_brief', summary, 'email_resource', aiConfig.modelId, result.success ? 'success' : 'failed');
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
346
|
+
this.updateTask(taskId, 'failed', undefined, msg);
|
|
347
|
+
console.error('generateResourceBrief error:', err);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async generateDailyReview(studentId) {
|
|
351
|
+
const today = new Date().toLocaleDateString('zh-CN');
|
|
352
|
+
const taskId = this.createTask('daily_review', studentId, 'scheduled', `${today} 每日回顾`);
|
|
353
|
+
try {
|
|
354
|
+
this.updateTask(taskId, 'running');
|
|
355
|
+
const configResult = (0, settings_service_1.getAIConfig)();
|
|
356
|
+
if (!configResult.success || !configResult.data?.apiKey) {
|
|
357
|
+
this.updateTask(taskId, 'failed', undefined, 'AI 未配置');
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const aiConfig = configResult.data;
|
|
361
|
+
const contextData = this.context.buildContext(studentId);
|
|
362
|
+
const task = { type: 'daily_review', priority: 'high', reason: `${today} 每日回顾` };
|
|
363
|
+
const result = await (0, executors_1.dailyReviewExecutor)(task, contextData, aiConfig, this.callAI.bind(this));
|
|
364
|
+
if (result.success && result.output) {
|
|
365
|
+
this.updateTask(taskId, 'completed', JSON.stringify(result.output));
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
this.updateTask(taskId, 'failed', undefined, result.error);
|
|
369
|
+
}
|
|
370
|
+
await this.logAction(studentId, 'daily_review', `${today} 每日回顾`, 'scheduled', aiConfig.modelId, result.success ? 'success' : 'failed');
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
374
|
+
this.updateTask(taskId, 'failed', undefined, msg);
|
|
375
|
+
console.error('generateDailyReview error:', err);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async generateWeaknessQuiz(studentId) {
|
|
379
|
+
const taskId = this.createTask('weakness_quiz', studentId, 'manual', '薄弱点测验');
|
|
380
|
+
try {
|
|
381
|
+
this.updateTask(taskId, 'running');
|
|
382
|
+
const configResult = (0, settings_service_1.getAIConfig)();
|
|
383
|
+
if (!configResult.success || !configResult.data?.apiKey) {
|
|
384
|
+
this.updateTask(taskId, 'failed', undefined, 'AI 未配置');
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const aiConfig = configResult.data;
|
|
388
|
+
const contextData = this.context.buildContext(studentId);
|
|
389
|
+
const task = { type: 'weakness_quiz', priority: 'high', reason: '薄弱点测验' };
|
|
390
|
+
const result = await (0, executors_1.weaknessQuizExecutor)(task, contextData, aiConfig, this.callAI.bind(this));
|
|
391
|
+
if (result.success && result.output) {
|
|
392
|
+
this.updateTask(taskId, 'completed', JSON.stringify(result.output));
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
this.updateTask(taskId, 'failed', undefined, result.error);
|
|
396
|
+
}
|
|
397
|
+
await this.logAction(studentId, 'weakness_quiz', '薄弱点测验', 'manual', aiConfig.modelId, result.success ? 'success' : 'failed');
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
401
|
+
this.updateTask(taskId, 'failed', undefined, msg);
|
|
402
|
+
console.error('generateWeaknessQuiz error:', err);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// ─────────────────────────────────────────────
|
|
406
|
+
// Event handler
|
|
407
|
+
// ─────────────────────────────────────────────
|
|
408
|
+
async handleEvent(event) {
|
|
409
|
+
try {
|
|
410
|
+
console.log(`AgentEngine handling event: ${event.type}`);
|
|
411
|
+
switch (event.type) {
|
|
412
|
+
case 'new_learning_data':
|
|
413
|
+
case 'plan_changed': {
|
|
414
|
+
const studentId = event.studentId;
|
|
415
|
+
if (studentId) {
|
|
416
|
+
await this.runAutonomousCycle(studentId);
|
|
417
|
+
}
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
case 'new_resource':
|
|
421
|
+
case 'workspace_update': {
|
|
422
|
+
await this.generateResourceBrief({
|
|
423
|
+
resourceId: event.resourceId,
|
|
424
|
+
fileName: event.fileName,
|
|
425
|
+
subject: event.subject,
|
|
426
|
+
fileType: event.fileType
|
|
427
|
+
});
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
case 'bot_message': {
|
|
431
|
+
const userId = event.userId;
|
|
432
|
+
const role = event.role;
|
|
433
|
+
const content = event.content;
|
|
434
|
+
if ((role === 'student' || role === 'guardian' || role === 'superadmin') && userId) {
|
|
435
|
+
const response = await this.generateBotResponse(userId, content);
|
|
436
|
+
if (response) {
|
|
437
|
+
await (0, notify_service_1.sendMessage)(userId, response);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
case 'workspace_full': {
|
|
443
|
+
try {
|
|
444
|
+
const { workspaceService } = require('../services/workspace.service');
|
|
445
|
+
const result = await workspaceService.cleanupTemp();
|
|
446
|
+
console.log(`Workspace cleanup: deleted ${result.deleted} files, freed ${result.freedBytes} bytes`);
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
console.error('Workspace cleanup failed:', err);
|
|
450
|
+
}
|
|
451
|
+
break;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
catch (err) {
|
|
456
|
+
console.error('AgentEngine handleEvent error:', err);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// ─────────────────────────────────────────────
|
|
460
|
+
// Bot response (chat reply mode)
|
|
461
|
+
// ─────────────────────────────────────────────
|
|
462
|
+
async generateBotResponse(userId, userMessage) {
|
|
463
|
+
const contextData = this.context.buildContext(userId);
|
|
464
|
+
const planSection = contextData.activePlan
|
|
465
|
+
? `学习方向:${contextData.activePlan.title}(${contextData.activePlan.subjects.join('、')})`
|
|
466
|
+
: '学习方向:未设置';
|
|
467
|
+
const prompt = `你是个性化学习辅助 AI PersonalAC。请根据学生学习情况,给出友好的回复。
|
|
468
|
+
|
|
469
|
+
## 学生状态
|
|
470
|
+
${planSection}
|
|
471
|
+
薄弱知识点:${contextData.weakPoints.slice(0, 3).map((p) => p.topic).join('、') || '暂无'}
|
|
472
|
+
|
|
473
|
+
## 用户消息
|
|
474
|
+
${userMessage}
|
|
475
|
+
|
|
476
|
+
如果是学习问题,提供解答;如果是闲聊,友好回应并引导到学习话题。直接输出回复内容。`;
|
|
477
|
+
return await this.callAI(prompt);
|
|
478
|
+
}
|
|
479
|
+
// ─────────────────────────────────────────────
|
|
480
|
+
// ReAct Chat(Tool Use + Streaming)
|
|
481
|
+
// ─────────────────────────────────────────────
|
|
482
|
+
buildChatConfig() {
|
|
483
|
+
const aiConfig = (0, settings_service_1.getAIConfig)().data;
|
|
484
|
+
const apiKey = aiConfig?.apiKey;
|
|
485
|
+
if (!apiKey)
|
|
486
|
+
return null;
|
|
487
|
+
const baseUrl = (aiConfig?.baseUrl || 'https://api.openai.com/v1').replace(/\/$/, '');
|
|
488
|
+
const rawModel = aiConfig?.modelId || 'gpt-4o';
|
|
489
|
+
const model = rawModel.includes('/') ? rawModel.split('/').slice(1).join('/') : rawModel;
|
|
490
|
+
const isAnthropic = baseUrl.includes('/anthropic') || baseUrl.includes('anthropic.com');
|
|
491
|
+
const isReasoning = /o1|o3|o4[-/]|thinking|reason/i.test(model);
|
|
492
|
+
return { apiKey, baseUrl, model, isAnthropic, isReasoning };
|
|
493
|
+
}
|
|
494
|
+
buildSystemPrompt(loginUserId, studentId) {
|
|
495
|
+
const db = require('../database').getDB();
|
|
496
|
+
const loginUser = db.prepare(`SELECT role, display_name FROM User WHERE id=? AND delete_flag=0`).get(loginUserId);
|
|
497
|
+
const student = db.prepare(`SELECT display_name, student_grade FROM User WHERE id=? AND delete_flag=0`).get(studentId);
|
|
498
|
+
const role = loginUser?.role ?? 'student';
|
|
499
|
+
const studentName = student?.display_name ?? '学生';
|
|
500
|
+
const grade = student?.student_grade ? `(${student.student_grade})` : '';
|
|
501
|
+
const plan = db.prepare(`SELECT subjects FROM Plan WHERE student_id=? AND status='active' AND delete_flag=0 ORDER BY create_time DESC LIMIT 1`).get(studentId);
|
|
502
|
+
const subjects = plan ? JSON.parse(plan.subjects || '[]').join('、') : '未设置';
|
|
503
|
+
const goal = db.prepare(`SELECT exam_type, exam_date, school_progress, guardian_notes FROM StudentGoal WHERE student_id=? AND delete_flag=0 ORDER BY update_time DESC LIMIT 1`).get(studentId);
|
|
504
|
+
let goalSection = '';
|
|
505
|
+
if (goal?.exam_type || goal?.exam_date || goal?.school_progress) {
|
|
506
|
+
const parts = [];
|
|
507
|
+
if (goal.exam_type)
|
|
508
|
+
parts.push(`目标考试:${goal.exam_type}`);
|
|
509
|
+
if (goal.exam_date) {
|
|
510
|
+
const days = Math.ceil((goal.exam_date - Date.now()) / 86400000);
|
|
511
|
+
parts.push(`距考试:${days > 0 ? `${days} 天` : '已过'}`);
|
|
512
|
+
}
|
|
513
|
+
if (goal.school_progress)
|
|
514
|
+
parts.push(`学校进度:${goal.school_progress}`);
|
|
515
|
+
if (goal.guardian_notes)
|
|
516
|
+
parts.push(`监护人备注:${goal.guardian_notes}`);
|
|
517
|
+
goalSection = `\n学习目标:${parts.join(',')}`;
|
|
518
|
+
}
|
|
519
|
+
// 自我清醒注入:每次会话开始前,AI 已经知道局势
|
|
520
|
+
let sobrietySection = '';
|
|
521
|
+
try {
|
|
522
|
+
const { getOrRefreshSnapshot } = require('../services/sobriety.service');
|
|
523
|
+
const snap = getOrRefreshSnapshot(studentId, 30 * 60 * 1000);
|
|
524
|
+
if (snap && snap.today_priority && snap.today_priority !== '暂无紧迫事项') {
|
|
525
|
+
const urgencyTag = snap.urgency.level === 'urgent' ? '【紧迫】'
|
|
526
|
+
: snap.urgency.level === 'attention' ? '【关注】'
|
|
527
|
+
: '';
|
|
528
|
+
sobrietySection = `\n清醒视角${urgencyTag}:${snap.today_priority}`;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
catch (err) {
|
|
532
|
+
// 快照不可用不影响对话
|
|
533
|
+
}
|
|
534
|
+
if (role === 'guardian') {
|
|
535
|
+
const guardianName = loginUser?.display_name ?? '监护人';
|
|
536
|
+
return `你是学习辅助 AI。当前与 ${guardianName} 对话,对方是 ${studentName}${grade} 的监护人。
|
|
537
|
+
|
|
538
|
+
学生科目:${subjects}${goalSection}${sobrietySection}
|
|
539
|
+
|
|
540
|
+
规则:
|
|
541
|
+
- 监护人提供的成绩和试卷数据可信,update_knowledge 用 source="guardian_upload",无需验证
|
|
542
|
+
- 询问学生状态时先调用 get_student_summary
|
|
543
|
+
- 上传图片(试卷/成绩单)→ 逐题分析,调用 update_knowledge 更新知识点
|
|
544
|
+
- 提到学习活动 → 调用 record_learning
|
|
545
|
+
- 设置/修改计划 → 调用 set_plan
|
|
546
|
+
- 当"清醒视角"显示紧迫事项时,主动汇报给监护人
|
|
547
|
+
|
|
548
|
+
回复风格:简洁、数据导向。`;
|
|
549
|
+
}
|
|
550
|
+
return `你是学习辅助 AI。当前与学生 ${studentName}${grade} 对话。
|
|
551
|
+
|
|
552
|
+
科目:${subjects}${goalSection}${sobrietySection}
|
|
553
|
+
|
|
554
|
+
清醒原则(最重要):
|
|
555
|
+
- 上面的"清醒视角"是你在对话开始前已经掌握的状态。学生还没说话,你已经知道今天最该关注什么。
|
|
556
|
+
- 如果"清醒视角"非空,主动用它引导对话方向:开场可以用"我注意到 X 已经有几天没复习了,要不要先看一下?"这类句式
|
|
557
|
+
- 不要被动等待学生提问;如果学生只是闲聊或问无关的事,在合适时机把对话拉回到清醒视角的优先项
|
|
558
|
+
- 如果学生有上次未理清的悬念,主动接续
|
|
559
|
+
|
|
560
|
+
规则:
|
|
561
|
+
- 询问学习状态时先调用 get_student_summary
|
|
562
|
+
- 想看更详细的清醒视角细节时调用 get_sobriety
|
|
563
|
+
- 学生说"我会了"→ 先出题验证,答对后再调用 update_knowledge
|
|
564
|
+
- 上传图片(练习/作业)→ 分析后调用 update_knowledge(source="agent_observed")
|
|
565
|
+
- 提到学习活动 → 调用 record_learning
|
|
566
|
+
- 发现持续性错误 → 填写 error_type 和 root_cause
|
|
567
|
+
|
|
568
|
+
讲解追踪:
|
|
569
|
+
- 解释后学生懂了 → log_explanation(understood=true,记录方法)
|
|
570
|
+
- 没懂 → 先追问"哪一步不清楚",找到根因后 log_explanation(understood=false,填 root_cause),换一种方法重讲
|
|
571
|
+
- 不要用同一种方式解释两遍:公式推导 → 数值例子 → 类比 → 图示 → 反例
|
|
572
|
+
|
|
573
|
+
前置依赖诊断(重要):
|
|
574
|
+
- 换了两种方法学生还是不懂 → 怀疑是前置知识有缺口,调用 check_prerequisites 诊断
|
|
575
|
+
- 发现前置缺口(如学生学不懂"导数"是因为"极限"不稳) → 调用 link_prerequisite 记录这个依赖关系,然后明确告诉学生:"我们先把 X 补一下,X 搞定了 Y 就好理解了",切换讲解目标到前置知识点
|
|
576
|
+
- 前置补完、学生掌握后 → 回到原来的知识点,告知"现在来看之前那道题"
|
|
577
|
+
|
|
578
|
+
回复风格:步骤清晰,鼓励为主。`;
|
|
579
|
+
}
|
|
580
|
+
// 非流式单次 AI 调用(ReAct 工具调用轮次)
|
|
581
|
+
async callOnce(cfg, systemPrompt, messages, tools) {
|
|
582
|
+
if (cfg.isAnthropic) {
|
|
583
|
+
const body = {
|
|
584
|
+
model: cfg.model, max_tokens: 16000, system: systemPrompt, messages, tools
|
|
585
|
+
};
|
|
586
|
+
if (cfg.isReasoning)
|
|
587
|
+
body.thinking = { type: 'enabled', budget_tokens: 8000 };
|
|
588
|
+
const res = await axios_1.default.post(cfg.baseUrl + '/messages', body, { headers: { 'x-api-key': cfg.apiKey, 'anthropic-version': '2023-06-01', 'Content-Type': 'application/json' }, timeout: 120000 });
|
|
589
|
+
const content = res.data?.content ?? [];
|
|
590
|
+
const text = content.find(c => c.type === 'text')?.text ?? null;
|
|
591
|
+
const toolCalls = content
|
|
592
|
+
.filter(c => c.type === 'tool_use')
|
|
593
|
+
.map(c => ({ id: c.id, name: c.name, args: (c.input ?? {}) }));
|
|
594
|
+
return { text, toolCalls };
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
const sysMessages = cfg.isReasoning
|
|
598
|
+
? [{ role: 'user', content: `[System instructions]\n${systemPrompt}` }]
|
|
599
|
+
: [{ role: 'system', content: systemPrompt }];
|
|
600
|
+
const body = {
|
|
601
|
+
model: cfg.model,
|
|
602
|
+
messages: [...sysMessages, ...messages],
|
|
603
|
+
tools, tool_choice: 'auto'
|
|
604
|
+
};
|
|
605
|
+
if (!cfg.isReasoning)
|
|
606
|
+
body.temperature = 0.7;
|
|
607
|
+
else
|
|
608
|
+
body.reasoning_effort = 'medium';
|
|
609
|
+
const res = await axios_1.default.post(cfg.baseUrl + '/chat/completions', body, { headers: { Authorization: `Bearer ${cfg.apiKey}`, 'Content-Type': 'application/json' }, timeout: 120000 });
|
|
610
|
+
const msg = res.data?.choices?.[0]?.message;
|
|
611
|
+
const text = msg?.content ?? null;
|
|
612
|
+
const toolCalls = (msg?.tool_calls ?? []).map((tc) => ({
|
|
613
|
+
id: tc.id, name: tc.function.name,
|
|
614
|
+
args: (() => { try {
|
|
615
|
+
return JSON.parse(tc.function.arguments);
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
return {};
|
|
619
|
+
} })()
|
|
620
|
+
}));
|
|
621
|
+
return { text, toolCalls };
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// 流式输出(最终回复,支持 thinking token)
|
|
625
|
+
async streamText(cfg, systemPrompt, messages, onToken, onDone, onError, onThinking) {
|
|
626
|
+
let done = false;
|
|
627
|
+
const safeDone = () => { if (!done) {
|
|
628
|
+
done = true;
|
|
629
|
+
onDone();
|
|
630
|
+
} };
|
|
631
|
+
try {
|
|
632
|
+
if (cfg.isAnthropic) {
|
|
633
|
+
const body = {
|
|
634
|
+
model: cfg.model, max_tokens: 16000, system: systemPrompt, messages, stream: true
|
|
635
|
+
};
|
|
636
|
+
if (cfg.isReasoning)
|
|
637
|
+
body.thinking = { type: 'enabled', budget_tokens: 8000 };
|
|
638
|
+
const res = await axios_1.default.post(cfg.baseUrl + '/messages', body, { headers: { 'x-api-key': cfg.apiKey, 'anthropic-version': '2023-06-01', 'Content-Type': 'application/json' }, responseType: 'stream', timeout: 180000 });
|
|
639
|
+
let buf = '';
|
|
640
|
+
res.data.on('data', (chunk) => {
|
|
641
|
+
buf += chunk.toString();
|
|
642
|
+
const lines = buf.split('\n');
|
|
643
|
+
buf = lines.pop() || '';
|
|
644
|
+
for (const line of lines) {
|
|
645
|
+
if (!line.startsWith('data: '))
|
|
646
|
+
continue;
|
|
647
|
+
try {
|
|
648
|
+
const p = JSON.parse(line.slice(6).trim());
|
|
649
|
+
if (p.type === 'content_block_delta') {
|
|
650
|
+
if (p.delta?.type === 'text_delta')
|
|
651
|
+
onToken(p.delta.text);
|
|
652
|
+
else if (p.delta?.type === 'thinking_delta')
|
|
653
|
+
onThinking?.(p.delta.thinking);
|
|
654
|
+
}
|
|
655
|
+
else if (p.type === 'message_stop')
|
|
656
|
+
safeDone();
|
|
657
|
+
}
|
|
658
|
+
catch { }
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
res.data.on('end', safeDone);
|
|
662
|
+
res.data.on('error', (e) => { if (!done) {
|
|
663
|
+
done = true;
|
|
664
|
+
onError(e.message);
|
|
665
|
+
} });
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
const sysMessages = cfg.isReasoning
|
|
669
|
+
? [{ role: 'user', content: `[System instructions]\n${systemPrompt}` }]
|
|
670
|
+
: [{ role: 'system', content: systemPrompt }];
|
|
671
|
+
const body = {
|
|
672
|
+
model: cfg.model,
|
|
673
|
+
messages: [...sysMessages, ...messages],
|
|
674
|
+
stream: true
|
|
675
|
+
};
|
|
676
|
+
if (!cfg.isReasoning)
|
|
677
|
+
body.temperature = 0.7;
|
|
678
|
+
else
|
|
679
|
+
body.reasoning_effort = 'medium';
|
|
680
|
+
const res = await axios_1.default.post(cfg.baseUrl + '/chat/completions', body, { headers: { Authorization: `Bearer ${cfg.apiKey}`, 'Content-Type': 'application/json' }, responseType: 'stream', timeout: 180000 });
|
|
681
|
+
let buf = '';
|
|
682
|
+
res.data.on('data', (chunk) => {
|
|
683
|
+
buf += chunk.toString();
|
|
684
|
+
const lines = buf.split('\n');
|
|
685
|
+
buf = lines.pop() || '';
|
|
686
|
+
for (const line of lines) {
|
|
687
|
+
if (!line.startsWith('data: '))
|
|
688
|
+
continue;
|
|
689
|
+
const raw = line.slice(6).trim();
|
|
690
|
+
if (raw === '[DONE]') {
|
|
691
|
+
safeDone();
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
const p = JSON.parse(raw);
|
|
696
|
+
const delta = p.choices?.[0]?.delta;
|
|
697
|
+
if (delta?.content)
|
|
698
|
+
onToken(delta.content);
|
|
699
|
+
else if (delta?.reasoning)
|
|
700
|
+
onThinking?.(delta.reasoning);
|
|
701
|
+
}
|
|
702
|
+
catch { }
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
res.data.on('end', safeDone);
|
|
706
|
+
res.data.on('error', (e) => { if (!done) {
|
|
707
|
+
done = true;
|
|
708
|
+
onError(e.message);
|
|
709
|
+
} });
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
catch (err) {
|
|
713
|
+
onError(err instanceof Error ? err.message : String(err));
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
async streamChatResponse(userId, messages, onToken, onDone, onError, onThinking) {
|
|
717
|
+
try {
|
|
718
|
+
const cfg = this.buildChatConfig();
|
|
719
|
+
if (!cfg) {
|
|
720
|
+
onError('未配置 AI,请前往设置页面填写 API Key');
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
// 确定操作数据用的学生 ID
|
|
724
|
+
const db = require('../database').getDB();
|
|
725
|
+
const loginUser = db.prepare('SELECT role FROM User WHERE id=? AND delete_flag=0').get(userId);
|
|
726
|
+
let studentId = userId;
|
|
727
|
+
if (loginUser?.role === 'guardian') {
|
|
728
|
+
const { getPrimaryStudentId } = require('../services/auth.service');
|
|
729
|
+
studentId = getPrimaryStudentId(userId) ?? userId;
|
|
730
|
+
}
|
|
731
|
+
// 学生开口前先刷新清醒快照(让 buildSystemPrompt 拿到最新状态)
|
|
732
|
+
try {
|
|
733
|
+
const { refreshSobrietySnapshot } = await Promise.resolve().then(() => __importStar(require('../services/sobriety.service')));
|
|
734
|
+
refreshSobrietySnapshot(studentId);
|
|
735
|
+
}
|
|
736
|
+
catch { /* 失败不影响对话 */ }
|
|
737
|
+
const systemPrompt = this.buildSystemPrompt(userId, studentId);
|
|
738
|
+
const { toOpenAITools, toAnthropicTools, getTool } = await Promise.resolve().then(() => __importStar(require('../tools/index')));
|
|
739
|
+
const tools = cfg.isAnthropic ? toAnthropicTools() : toOpenAITools();
|
|
740
|
+
let apiMessages = messages.map(m => {
|
|
741
|
+
if (typeof m.content === 'string')
|
|
742
|
+
return { role: m.role, content: m.content };
|
|
743
|
+
// 多模态:前端传 { type:'text'|'image', text?, data?, mediaType? }[]
|
|
744
|
+
const parts = m.content;
|
|
745
|
+
if (cfg.isAnthropic) {
|
|
746
|
+
return {
|
|
747
|
+
role: m.role,
|
|
748
|
+
content: parts.map(p => p.type === 'image'
|
|
749
|
+
? { type: 'image', source: { type: 'base64', media_type: p.mediaType ?? 'image/jpeg', data: p.data } }
|
|
750
|
+
: { type: 'text', text: p.text ?? '' })
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
return {
|
|
755
|
+
role: m.role,
|
|
756
|
+
content: parts.map(p => p.type === 'image'
|
|
757
|
+
? { type: 'image_url', image_url: { url: `data:${p.mediaType ?? 'image/jpeg'};base64,${p.data}` } }
|
|
758
|
+
: { type: 'text', text: p.text ?? '' })
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
// ── ReAct 循环(最多5轮)────────────────────
|
|
763
|
+
const MAX_ITER = 5;
|
|
764
|
+
for (let i = 0; i < MAX_ITER; i++) {
|
|
765
|
+
const { text, toolCalls } = await this.callOnce(cfg, systemPrompt, apiMessages, tools);
|
|
766
|
+
if (toolCalls.length === 0) {
|
|
767
|
+
// 最终文字响应:用真正流式输出
|
|
768
|
+
await this.streamText(cfg, systemPrompt, apiMessages, onToken, onDone, onError, cfg.isReasoning ? (t) => onThinking?.('thinking', t) : undefined);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
// 有工具调用:执行后追加结果,继续循环
|
|
772
|
+
const assistantContent = cfg.isAnthropic
|
|
773
|
+
? [
|
|
774
|
+
...(text ? [{ type: 'text', text }] : []),
|
|
775
|
+
...toolCalls.map(tc => ({ type: 'tool_use', id: tc.id, name: tc.name, input: tc.args }))
|
|
776
|
+
]
|
|
777
|
+
: text ?? null;
|
|
778
|
+
if (cfg.isAnthropic) {
|
|
779
|
+
apiMessages.push({ role: 'assistant', content: assistantContent });
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
apiMessages.push({
|
|
783
|
+
role: 'assistant',
|
|
784
|
+
content: assistantContent,
|
|
785
|
+
...(toolCalls.length ? {
|
|
786
|
+
tool_calls: toolCalls.map(tc => ({
|
|
787
|
+
id: tc.id, type: 'function',
|
|
788
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.args) }
|
|
789
|
+
}))
|
|
790
|
+
} : {})
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
for (const tc of toolCalls) {
|
|
794
|
+
const toolDef = getTool(tc.name);
|
|
795
|
+
const displayNames = {
|
|
796
|
+
get_student_summary: '正在查询学习状态…',
|
|
797
|
+
update_knowledge: '正在更新知识记录…',
|
|
798
|
+
set_plan: '正在更新学习计划…',
|
|
799
|
+
record_learning: '正在记录学习活动…'
|
|
800
|
+
};
|
|
801
|
+
onThinking?.(tc.name, displayNames[tc.name] ?? `正在调用 ${tc.name}…`);
|
|
802
|
+
let result = '工具未找到';
|
|
803
|
+
if (toolDef) {
|
|
804
|
+
try {
|
|
805
|
+
result = await toolDef.execute(tc.args, { userId, studentId });
|
|
806
|
+
}
|
|
807
|
+
catch (e) {
|
|
808
|
+
result = `执行失败: ${e instanceof Error ? e.message : String(e)}`;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (cfg.isAnthropic) {
|
|
812
|
+
apiMessages.push({ role: 'user', content: [{ type: 'tool_result', tool_use_id: tc.id, content: result }] });
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
apiMessages.push({ role: 'tool', content: result, tool_call_id: tc.id, name: tc.name });
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
onError('工具调用轮次超限');
|
|
820
|
+
}
|
|
821
|
+
catch (err) {
|
|
822
|
+
onError(err instanceof Error ? err.message : String(err));
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
// ─────────────────────────────────────────────
|
|
826
|
+
// Logging
|
|
827
|
+
// ─────────────────────────────────────────────
|
|
828
|
+
async logAction(studentId, actionType, actionDetail, triggerType, modelUsed, status = 'success', errorMessage) {
|
|
829
|
+
try {
|
|
830
|
+
const db = (0, database_1.getDB)();
|
|
831
|
+
const now = Date.now();
|
|
832
|
+
const logId = (0, uuid_1.v4)();
|
|
833
|
+
db.prepare(`
|
|
834
|
+
INSERT INTO AgentLog (id, student_id, action_type, action_detail, trigger_type, model_used, status, error_message, create_time, update_time, delete_flag)
|
|
835
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)
|
|
836
|
+
`).run(logId, studentId, actionType, actionDetail, triggerType, modelUsed, status, errorMessage || null, now, now);
|
|
837
|
+
}
|
|
838
|
+
catch (err) {
|
|
839
|
+
console.error('logAction error:', err);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
getRecentLogs(studentId, limit = 20) {
|
|
843
|
+
try {
|
|
844
|
+
const db = (0, database_1.getDB)();
|
|
845
|
+
return db
|
|
846
|
+
.prepare(`SELECT id, action_type, action_detail, trigger_type, model_used, status, create_time
|
|
847
|
+
FROM AgentLog
|
|
848
|
+
WHERE student_id = ? AND delete_flag = 0
|
|
849
|
+
ORDER BY create_time DESC
|
|
850
|
+
LIMIT ?`)
|
|
851
|
+
.all(studentId, limit);
|
|
852
|
+
}
|
|
853
|
+
catch (err) {
|
|
854
|
+
console.error('getRecentLogs error:', err);
|
|
855
|
+
return [];
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
// ─────────────────────────────────────────────
|
|
859
|
+
// Do-not-disturb
|
|
860
|
+
// ─────────────────────────────────────────────
|
|
861
|
+
setDoNotDisturb(start, end) {
|
|
862
|
+
const timeRegex = /^\d{2}:\d{2}$/;
|
|
863
|
+
if (!timeRegex.test(start) || !timeRegex.test(end)) {
|
|
864
|
+
throw new Error('时间格式应为 HH:MM');
|
|
865
|
+
}
|
|
866
|
+
this.doNotDisturb = { start, end };
|
|
867
|
+
console.log(`Do-not-disturb set: ${start} - ${end}`);
|
|
868
|
+
}
|
|
869
|
+
clearDoNotDisturb() {
|
|
870
|
+
this.doNotDisturb = null;
|
|
871
|
+
console.log('Do-not-disturb cleared');
|
|
872
|
+
}
|
|
873
|
+
isDoNotDisturb() {
|
|
874
|
+
if (!this.doNotDisturb)
|
|
875
|
+
return false;
|
|
876
|
+
const now = new Date();
|
|
877
|
+
const currentMinutes = now.getHours() * 60 + now.getMinutes();
|
|
878
|
+
const [startH, startM] = this.doNotDisturb.start.split(':').map(Number);
|
|
879
|
+
const [endH, endM] = this.doNotDisturb.end.split(':').map(Number);
|
|
880
|
+
const startMinutes = startH * 60 + startM;
|
|
881
|
+
const endMinutes = endH * 60 + endM;
|
|
882
|
+
if (startMinutes <= endMinutes) {
|
|
883
|
+
return currentMinutes >= startMinutes && currentMinutes <= endMinutes;
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
return currentMinutes >= startMinutes || currentMinutes <= endMinutes;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
getScheduler() {
|
|
890
|
+
return this.scheduler;
|
|
891
|
+
}
|
|
892
|
+
destroy() {
|
|
893
|
+
this.scheduler.destroy();
|
|
894
|
+
if (this.dailyCronJob) {
|
|
895
|
+
this.dailyCronJob.stop();
|
|
896
|
+
this.dailyCronJob = null;
|
|
897
|
+
}
|
|
898
|
+
if (this.sobrietyCronJob) {
|
|
899
|
+
this.sobrietyCronJob.stop();
|
|
900
|
+
this.sobrietyCronJob = null;
|
|
901
|
+
}
|
|
902
|
+
agentEngineInstance = null;
|
|
903
|
+
console.log('AgentEngine destroyed');
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
exports.AgentEngine = AgentEngine;
|
|
907
|
+
//# sourceMappingURL=index.js.map
|