@kafca/agentdock 0.1.33 → 0.1.35
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 +67 -0
- package/dist/renderer/assets/{Badge-VeSLCPPV.js → Badge-BM4WCcua.js} +1 -1
- package/dist/renderer/assets/{Chat-DWTQeTgn.js → Chat-DoyBHOsx.js} +1 -1
- package/dist/renderer/assets/{ChatMarkdown-C9gSQIVb.js → ChatMarkdown-BWj04kgW.js} +1 -1
- package/dist/renderer/assets/{Config-CTA-pZNR.js → Config-A95OuazQ.js} +1 -1
- package/dist/renderer/assets/{CronList-BngBCnFY.js → CronList-DsPU4zGT.js} +1 -1
- package/dist/renderer/assets/{Dashboard-D4pGvDpc.js → Dashboard-D8Vpj97x.js} +2 -2
- package/dist/renderer/assets/{EmptyState-DMf8rzDs.js → EmptyState-x1uVoSxZ.js} +1 -1
- package/dist/renderer/assets/{KnowledgeDetail-C1Ym_cHk.js → KnowledgeDetail-C9TbcyNJ.js} +1 -1
- package/dist/renderer/assets/{KnowledgeHome-DPRiR72G.js → KnowledgeHome-Cg9Fb41Q.js} +1 -1
- package/dist/renderer/assets/Logs-oWaKA60u.js +1 -0
- package/dist/renderer/assets/{Modal-BDhhPv2l.js → Modal-24hTXXHK.js} +1 -1
- package/dist/renderer/assets/{Page-DdDZN7AE.js → Page-YRtQonKZ.js} +1 -1
- package/dist/renderer/assets/{ProjectDetail-DCsr3LkW.js → ProjectDetail-DScsxJO-.js} +1 -1
- package/dist/renderer/assets/{ProjectList-Cnm-iLvg.js → ProjectList-8L5GD7PN.js} +1 -1
- package/dist/renderer/assets/{Select-Dw4FCXhJ.js → Select-BI8Uw_DF.js} +1 -1
- package/dist/renderer/assets/{SessionChat-O-6abK8x.js → SessionChat-BIoCWdDD.js} +1 -1
- package/dist/renderer/assets/{SessionList-5yptvVL9.js → SessionList-BsEJeDxI.js} +1 -1
- package/dist/renderer/assets/{ThreadChat-CIM_o2HO.js → ThreadChat-DJi0UBta.js} +1 -1
- package/dist/renderer/assets/{Workspace-D9UeUCNr.js → Workspace-tlJxrgC5.js} +1 -1
- package/dist/renderer/assets/{arrow-left-CKfxNOXE.js → arrow-left-pzaBhi9y.js} +1 -1
- package/dist/renderer/assets/{book-open-CQddlENV.js → book-open-CNhlVyqh.js} +1 -1
- package/dist/renderer/assets/{check-CZYvOmIS.js → check-Cg2LLCdl.js} +1 -1
- package/dist/renderer/assets/{chevron-down-CVIWFoXF.js → chevron-down-Dgqg07wf.js} +1 -1
- package/dist/renderer/assets/{circle-DSfdQM7k.js → circle-BBd_jIS2.js} +1 -1
- package/dist/renderer/assets/{desktop-B9KUgeOV.js → desktop-C0S-Yv8G.js} +1 -1
- package/dist/renderer/assets/{funnel-pB5NegZB.js → funnel-DwSh-SrX.js} +1 -1
- package/dist/renderer/assets/{heart-BT5laZ_b.js → heart-CWVc-KhZ.js} +1 -1
- package/dist/renderer/assets/{index-DFiJR0vv.css → index-CaQZSRbg.css} +1 -1
- package/dist/renderer/assets/{index-BYyDk28K.js → index-DZbeAYVk.js} +2 -2
- package/dist/renderer/assets/{index-zTUUYax3.js → index-HYmDNQ9k.js} +1 -1
- package/dist/renderer/assets/{pencil-C0thxLi6.js → pencil-tI4tCLz0.js} +1 -1
- package/dist/renderer/assets/{play-CmZ0kWtt.js → play-Coi2HA1w.js} +1 -1
- package/dist/renderer/assets/{plug-CeDQatBe.js → plug-D4TCyW9b.js} +1 -1
- package/dist/renderer/assets/{plus-Di__xFOZ.js → plus-p62AgpoB.js} +1 -1
- package/dist/renderer/assets/projects-CTDuVxeQ.js +1 -0
- package/dist/renderer/assets/{save-BDatCjfO.js → save-fpMhkVV0.js} +1 -1
- package/dist/renderer/assets/{search-DnTOjP0C.js → search-BHKIStdA.js} +1 -1
- package/dist/renderer/assets/{server-Ey_iHY6m.js → server-0oTWed8U.js} +1 -1
- package/dist/renderer/assets/{session-utils-DWaM7Rkr.js → session-utils-D0Qv0I0-.js} +1 -1
- package/dist/renderer/assets/{sessions-CYYjLD5I.js → sessions-C8ApKQgC.js} +1 -1
- package/dist/renderer/assets/{shield-check-Br8BQzcn.js → shield-check-BAJI9jzC.js} +1 -1
- package/dist/renderer/assets/{trash-2-7YD616fw.js → trash-2-B-Ygm-x6.js} +1 -1
- package/dist/renderer/assets/{user-BR_5jmr9.js → user-Cv8rzw8l.js} +1 -1
- package/dist/renderer/assets/{wifi-off-By9QdREz.js → wifi-off-BImS3nkm.js} +1 -1
- package/dist/renderer/index.html +2 -2
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-backend.js +3 -5
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-session-coordinator.js +23 -6
- package/dist-electron/services/local-ai-core/src/cli/lac.js +7 -1
- package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +9 -19
- package/dist-electron/services/local-ai-core/src/kernel/rotating-logger.js +232 -0
- package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +12 -44
- package/dist-electron/services/local-ai-core/src/runtime/local-core-runtime-state.js +40 -84
- package/dist-electron/services/local-ai-core/src/runtime/server-routes.js +3 -0
- package/dist-electron/services/local-ai-core/src/runtime/server.js +14 -0
- package/dist-electron/services/local-ai-core/src/scheduler/lark-schedule-adapter.js +6 -5
- package/dist-electron/services/local-ai-core/src/scheduler/scheduled-conversation-executor.js +22 -0
- package/dist-electron/services/local-ai-core/src/scheduler/scheduled-job-application-service.js +84 -0
- package/dist-electron/services/local-ai-core/src/scheduler/scheduled-job-route.js +35 -2
- package/dist-electron/services/local-ai-core/src/scheduler/weixin-schedule-adapter.js +5 -5
- package/dist-electron/src/api/client.js +1 -1
- package/dist-electron/tests/electron/lac-cli.test.js +50 -0
- package/dist-electron/tests/electron/plugin-kernel.test.js +162 -44
- package/dist-electron/tests/electron/workspace-task-store.test.js +123 -0
- package/dist-electron/tests/integration/local-core-refactor.test.js +13 -3
- package/package.json +1 -1
- package/dist/renderer/assets/Logs-CRHecmzO.js +0 -1
- package/dist/renderer/assets/projects-B-pbhhtH.js +0 -1
|
@@ -4,7 +4,6 @@ exports.LocalCoreController = void 0;
|
|
|
4
4
|
const node_events_1 = require("node:events");
|
|
5
5
|
const desktop_js_1 = require("../../../../shared/desktop.js");
|
|
6
6
|
const bootstrap_js_1 = require("../kernel/bootstrap.js");
|
|
7
|
-
const scheduled_job_route_js_1 = require("../scheduler/scheduled-job-route.js");
|
|
8
7
|
const runtime_detection_service_js_1 = require("./runtime-detection-service.js");
|
|
9
8
|
class LocalCoreController extends node_events_1.EventEmitter {
|
|
10
9
|
userDataPath;
|
|
@@ -12,7 +11,7 @@ class LocalCoreController extends node_events_1.EventEmitter {
|
|
|
12
11
|
workspaceRouter;
|
|
13
12
|
knowledgeProvider;
|
|
14
13
|
channelRuntimes;
|
|
15
|
-
|
|
14
|
+
scheduledJobs;
|
|
16
15
|
kernel;
|
|
17
16
|
runtime;
|
|
18
17
|
runtimeDetection;
|
|
@@ -33,7 +32,7 @@ class LocalCoreController extends node_events_1.EventEmitter {
|
|
|
33
32
|
this.workspaceRouter = this.runtime.workspaceRouter;
|
|
34
33
|
const runtimeChannels = this.runtime.channelRuntimes || [this.runtime.channelRuntime, this.runtime.weixinChannelRuntime];
|
|
35
34
|
this.channelRuntimes = new Map(runtimeChannels.filter(Boolean).map((runtime) => [runtime.platform, runtime]));
|
|
36
|
-
this.
|
|
35
|
+
this.scheduledJobs = this.runtime.scheduledJobs;
|
|
37
36
|
this.runtimeDetection = new runtime_detection_service_js_1.RuntimeDetectionService({
|
|
38
37
|
userDataPath,
|
|
39
38
|
readConfig: async () => (await this.readConfigFile()).parsed,
|
|
@@ -106,6 +105,9 @@ class LocalCoreController extends node_events_1.EventEmitter {
|
|
|
106
105
|
getLogs(limit = 200) {
|
|
107
106
|
return this.state.getLogs(limit);
|
|
108
107
|
}
|
|
108
|
+
getLogEntries(level = 'sys', limit = 200) {
|
|
109
|
+
return this.state.getLogEntries(level, limit);
|
|
110
|
+
}
|
|
109
111
|
async readConfigFile() {
|
|
110
112
|
return this.state.readConfigFile();
|
|
111
113
|
}
|
|
@@ -182,63 +184,29 @@ class LocalCoreController extends node_events_1.EventEmitter {
|
|
|
182
184
|
return this.workspaceRouter.listAuditEvents(query);
|
|
183
185
|
}
|
|
184
186
|
async listScheduledJobs(workspaceId) {
|
|
185
|
-
return this.
|
|
187
|
+
return this.scheduledJobs.listJobs(workspaceId);
|
|
186
188
|
}
|
|
187
189
|
async getScheduledJob(jobId) {
|
|
188
|
-
const job = this.
|
|
190
|
+
const job = this.scheduledJobs.getJob(jobId);
|
|
189
191
|
if (!job) {
|
|
190
192
|
throw new Error(`Scheduled job not found: ${jobId}`);
|
|
191
193
|
}
|
|
192
194
|
return job;
|
|
193
195
|
}
|
|
194
196
|
async createScheduledJob(input) {
|
|
195
|
-
return this.
|
|
196
|
-
}
|
|
197
|
-
resolveScheduledJobCreateInput(input) {
|
|
198
|
-
if (input.platform && input.route) {
|
|
199
|
-
return {
|
|
200
|
-
...input,
|
|
201
|
-
route: (0, scheduled_job_route_js_1.withoutThreadRoute)(input.route),
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
const threadId = String(input.threadId || input.route?.threadId || '').trim();
|
|
205
|
-
if (threadId) {
|
|
206
|
-
const binding = this.runtime.store.getPlatformThreadBindingByThreadId(threadId);
|
|
207
|
-
if (binding && binding.workspace_id === input.workspaceId) {
|
|
208
|
-
return {
|
|
209
|
-
...input,
|
|
210
|
-
platform: binding.platform,
|
|
211
|
-
route: {
|
|
212
|
-
type: 'channel.chat',
|
|
213
|
-
channelId: binding.chat_id,
|
|
214
|
-
participantId: binding.platform_user_id,
|
|
215
|
-
},
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return {
|
|
220
|
-
...input,
|
|
221
|
-
platform: 'local',
|
|
222
|
-
route: {
|
|
223
|
-
type: 'local.thread',
|
|
224
|
-
channelId: input.workspaceId,
|
|
225
|
-
},
|
|
226
|
-
};
|
|
197
|
+
return this.scheduledJobs.createJob(input);
|
|
227
198
|
}
|
|
228
199
|
async updateScheduledJob(jobId, input) {
|
|
229
|
-
return this.
|
|
230
|
-
...input,
|
|
231
|
-
...(input.route ? { route: (0, scheduled_job_route_js_1.withoutThreadRoute)(input.route) } : {}),
|
|
232
|
-
});
|
|
200
|
+
return this.scheduledJobs.updateJob(jobId, input);
|
|
233
201
|
}
|
|
234
202
|
async deleteScheduledJob(jobId) {
|
|
235
|
-
return this.
|
|
203
|
+
return this.scheduledJobs.deleteJob(jobId);
|
|
236
204
|
}
|
|
237
205
|
async runScheduledJob(jobId) {
|
|
238
|
-
return this.
|
|
206
|
+
return this.scheduledJobs.runJobNow(jobId);
|
|
239
207
|
}
|
|
240
208
|
async listScheduledJobRuns(jobId) {
|
|
241
|
-
return this.
|
|
209
|
+
return this.scheduledJobs.listJobRuns(jobId);
|
|
242
210
|
}
|
|
243
211
|
async listThreads(workspaceId) {
|
|
244
212
|
return this.workspaceRouter.listThreads(workspaceId);
|
|
@@ -37,13 +37,10 @@ exports.createLocalCoreRuntimeState = createLocalCoreRuntimeState;
|
|
|
37
37
|
const node_fs_1 = require("node:fs");
|
|
38
38
|
const node_path_1 = require("node:path");
|
|
39
39
|
const TOML = __importStar(require("@iarna/toml"));
|
|
40
|
+
const rotating_logger_js_1 = require("../kernel/rotating-logger.js");
|
|
40
41
|
const DEFAULT_CONFIG = `# Managed by Local AI Core
|
|
41
42
|
# Add [[projects]] entries from the workspace page before starting a conversation.
|
|
42
43
|
`;
|
|
43
|
-
const LOG_FILE_NAME = 'local-core.log';
|
|
44
|
-
const LOG_FILE_MAX_BYTES = 2 * 1024 * 1024;
|
|
45
|
-
const LOG_TAIL_CHUNK_BYTES = 64 * 1024;
|
|
46
|
-
const LOG_MAX_RETURN_LINES = 5000;
|
|
47
44
|
class FileBackedLocalCoreRuntimeState {
|
|
48
45
|
userDataPath;
|
|
49
46
|
onLog;
|
|
@@ -53,12 +50,14 @@ class FileBackedLocalCoreRuntimeState {
|
|
|
53
50
|
logPath;
|
|
54
51
|
settings;
|
|
55
52
|
logs = [];
|
|
53
|
+
logger;
|
|
56
54
|
constructor(userDataPath, onLog) {
|
|
57
55
|
this.userDataPath = userDataPath;
|
|
58
56
|
this.onLog = onLog;
|
|
59
57
|
this.runtimeDir = (0, node_path_1.join)(userDataPath, 'runtime');
|
|
60
58
|
this.settingsPath = (0, node_path_1.join)(this.runtimeDir, 'local-core-settings.json');
|
|
61
|
-
this.
|
|
59
|
+
this.logger = new rotating_logger_js_1.AgentDockRotatingLogger({ scope: 'local-ai-core' });
|
|
60
|
+
this.logPath = this.logger.sysLogPath;
|
|
62
61
|
(0, node_fs_1.mkdirSync)(this.runtimeDir, { recursive: true });
|
|
63
62
|
this.cliBinDir = this.ensureCliWrapper();
|
|
64
63
|
this.settings = this.loadSettings();
|
|
@@ -114,43 +113,25 @@ class FileBackedLocalCoreRuntimeState {
|
|
|
114
113
|
return this.settings.knowledge;
|
|
115
114
|
}
|
|
116
115
|
getLogs(limit = 200) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
116
|
+
return this.logger.tailSysLog(limit);
|
|
117
|
+
}
|
|
118
|
+
getLogEntries(level = 'sys', limit = 200) {
|
|
119
|
+
const normalizedLevel = normalizeLogFile(level);
|
|
120
|
+
return this.logger.tail(normalizedLevel, limit)
|
|
121
|
+
.map(parseLogEntry)
|
|
122
|
+
.filter((entry) => Boolean(entry));
|
|
124
123
|
}
|
|
125
124
|
pushLog(message) {
|
|
126
125
|
if (!message) {
|
|
127
126
|
return;
|
|
128
127
|
}
|
|
129
128
|
this.logs.push(message);
|
|
130
|
-
this.
|
|
129
|
+
this.logger.write((0, rotating_logger_js_1.inferLogLevel)(message), message);
|
|
131
130
|
this.onLog?.(message);
|
|
132
131
|
if (this.logs.length > 400) {
|
|
133
132
|
this.logs.splice(0, this.logs.length - 400);
|
|
134
133
|
}
|
|
135
134
|
}
|
|
136
|
-
appendLogFile(message) {
|
|
137
|
-
try {
|
|
138
|
-
if ((0, node_fs_1.existsSync)(this.logPath) && (0, node_fs_1.statSync)(this.logPath).size > LOG_FILE_MAX_BYTES) {
|
|
139
|
-
const rotatedPath = `${this.logPath}.1`;
|
|
140
|
-
try {
|
|
141
|
-
(0, node_fs_1.renameSync)(this.logPath, rotatedPath);
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
// Keep runtime logging best-effort; stdout/UI logs still work.
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
const line = `${new Date().toISOString()} ${message.replace(/\r?\n/g, '\\n')}\n`;
|
|
148
|
-
(0, node_fs_1.appendFileSync)(this.logPath, line, 'utf-8');
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
// File logging must never break Local AI Core runtime behavior.
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
135
|
async readConfigFile() {
|
|
155
136
|
const path = this.settings.configPath;
|
|
156
137
|
if (!(0, node_fs_1.existsSync)(path)) {
|
|
@@ -262,6 +243,34 @@ class FileBackedLocalCoreRuntimeState {
|
|
|
262
243
|
return binDir;
|
|
263
244
|
}
|
|
264
245
|
}
|
|
246
|
+
function normalizeLogFile(level) {
|
|
247
|
+
if (level === 'debug' || level === 'info' || level === 'warn' || level === 'error') {
|
|
248
|
+
return level;
|
|
249
|
+
}
|
|
250
|
+
return 'sys';
|
|
251
|
+
}
|
|
252
|
+
function parseLogEntry(line) {
|
|
253
|
+
try {
|
|
254
|
+
const parsed = JSON.parse(line);
|
|
255
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
const level = normalizeLogFile(String(parsed.level || 'info'));
|
|
259
|
+
if (level === 'sys') {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
ts: String(parsed.ts || ''),
|
|
264
|
+
level,
|
|
265
|
+
scope: String(parsed.scope || ''),
|
|
266
|
+
message: String(parsed.message || ''),
|
|
267
|
+
...(parsed.meta && typeof parsed.meta === 'object' ? { meta: parsed.meta } : {}),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
265
274
|
function normalizePluginSettings(input) {
|
|
266
275
|
if (!input || typeof input !== 'object') {
|
|
267
276
|
return {};
|
|
@@ -281,59 +290,6 @@ function normalizePluginSettings(input) {
|
|
|
281
290
|
}
|
|
282
291
|
return output;
|
|
283
292
|
}
|
|
284
|
-
function normalizeLogLimit(limit) {
|
|
285
|
-
if (!Number.isFinite(limit)) {
|
|
286
|
-
return 200;
|
|
287
|
-
}
|
|
288
|
-
return Math.min(Math.max(Math.floor(limit), 1), LOG_MAX_RETURN_LINES);
|
|
289
|
-
}
|
|
290
|
-
function tailLogFile(path, limit) {
|
|
291
|
-
if (limit <= 0 || !(0, node_fs_1.existsSync)(path)) {
|
|
292
|
-
return [];
|
|
293
|
-
}
|
|
294
|
-
let fd = -1;
|
|
295
|
-
try {
|
|
296
|
-
const size = (0, node_fs_1.statSync)(path).size;
|
|
297
|
-
if (size <= 0) {
|
|
298
|
-
return [];
|
|
299
|
-
}
|
|
300
|
-
fd = (0, node_fs_1.openSync)(path, 'r');
|
|
301
|
-
const chunks = [];
|
|
302
|
-
let position = size;
|
|
303
|
-
let newlineCount = 0;
|
|
304
|
-
while (position > 0 && newlineCount <= limit) {
|
|
305
|
-
const bytesToRead = Math.min(LOG_TAIL_CHUNK_BYTES, position);
|
|
306
|
-
position -= bytesToRead;
|
|
307
|
-
const chunk = Buffer.allocUnsafe(bytesToRead);
|
|
308
|
-
const bytesRead = (0, node_fs_1.readSync)(fd, chunk, 0, bytesToRead, position);
|
|
309
|
-
const slice = bytesRead === bytesToRead ? chunk : chunk.subarray(0, bytesRead);
|
|
310
|
-
chunks.unshift(slice);
|
|
311
|
-
for (let index = 0; index < slice.length; index += 1) {
|
|
312
|
-
if (slice[index] === 10) {
|
|
313
|
-
newlineCount += 1;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
return Buffer.concat(chunks)
|
|
318
|
-
.toString('utf8')
|
|
319
|
-
.split(/\r?\n/)
|
|
320
|
-
.filter((line) => line.length > 0)
|
|
321
|
-
.slice(-limit);
|
|
322
|
-
}
|
|
323
|
-
catch {
|
|
324
|
-
return [];
|
|
325
|
-
}
|
|
326
|
-
finally {
|
|
327
|
-
if (fd >= 0) {
|
|
328
|
-
try {
|
|
329
|
-
(0, node_fs_1.closeSync)(fd);
|
|
330
|
-
}
|
|
331
|
-
catch {
|
|
332
|
-
// Log reads are best-effort; failed closes cannot be surfaced to callers.
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
293
|
function createLocalCoreRuntimeState(options) {
|
|
338
294
|
return new FileBackedLocalCoreRuntimeState(options.userDataPath, options.onLog);
|
|
339
295
|
}
|
|
@@ -23,6 +23,9 @@ function parseLocalAiCoreRoute(method, path) {
|
|
|
23
23
|
if (normalizedMethod === 'GET' && relativePath === '/runtime/logs') {
|
|
24
24
|
return { name: 'runtime.logs' };
|
|
25
25
|
}
|
|
26
|
+
if (normalizedMethod === 'GET' && relativePath === '/logs') {
|
|
27
|
+
return { name: 'logs.list' };
|
|
28
|
+
}
|
|
26
29
|
if (normalizedMethod === 'GET' && relativePath === '/runtime/agent-runtimes') {
|
|
27
30
|
return { name: 'runtime.agent-runtimes' };
|
|
28
31
|
}
|
|
@@ -143,6 +143,20 @@ class LocalAiCoreServer {
|
|
|
143
143
|
json(res, 200, this.bindings.getLogs(limit));
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
|
+
case 'logs.list': {
|
|
147
|
+
const level = url.searchParams.get('level') || 'sys';
|
|
148
|
+
const limit = Number(url.searchParams.get('limit') || '200');
|
|
149
|
+
json(res, 200, {
|
|
150
|
+
entries: this.bindings.getLogEntries(level, limit).map((entry) => ({
|
|
151
|
+
time: entry.ts,
|
|
152
|
+
level: entry.level,
|
|
153
|
+
scope: entry.scope,
|
|
154
|
+
message: entry.message,
|
|
155
|
+
meta: entry.meta,
|
|
156
|
+
})),
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
146
160
|
case 'runtime.agent-runtimes':
|
|
147
161
|
case 'runtimes.list':
|
|
148
162
|
json(res, 200, await this.runtimeDetectionResponse());
|
|
@@ -16,7 +16,7 @@ class LarkScheduleAdapter {
|
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
supports(job) {
|
|
19
|
-
return job.platform
|
|
19
|
+
return (0, scheduled_job_route_js_1.platformMatches)(job.platform, 'lark') && (job.route.type === 'channel.chat' || job.route.type === 'lark_chat');
|
|
20
20
|
}
|
|
21
21
|
async execute(context) {
|
|
22
22
|
const { job } = context;
|
|
@@ -32,7 +32,7 @@ class LarkScheduleAdapter {
|
|
|
32
32
|
if (!channelRuntime.sendScheduledMessage) {
|
|
33
33
|
throw new Error('Lark channel runtime does not support scheduled delivery.');
|
|
34
34
|
}
|
|
35
|
-
platformMessageId = await channelRuntime.sendScheduledMessage(job.workspaceId, (0, scheduled_job_route_js_1.withoutThreadRoute)(job.route), execution.replyText);
|
|
35
|
+
platformMessageId = await channelRuntime.sendScheduledMessage(job.workspaceId, (0, scheduled_job_route_js_1.routeWithPlatformInstance)((0, scheduled_job_route_js_1.withoutThreadRoute)(job.route), job.platform), execution.replyText);
|
|
36
36
|
if (!platformMessageId) {
|
|
37
37
|
throw new Error('Lark gateway did not return a message id for scheduled delivery.');
|
|
38
38
|
}
|
|
@@ -49,7 +49,7 @@ class LarkScheduleAdapter {
|
|
|
49
49
|
const route = job.route;
|
|
50
50
|
const channelId = route.channelId;
|
|
51
51
|
const participantId = route.participantId || '';
|
|
52
|
-
const binding = this.options.store.getPlatformThreadBinding(job.workspaceId, channelId, participantId,
|
|
52
|
+
const binding = this.options.store.getPlatformThreadBinding(job.workspaceId, channelId, participantId, job.platform);
|
|
53
53
|
if (binding?.thread_id && await this.threadExists(binding.thread_id)) {
|
|
54
54
|
return binding.thread_id;
|
|
55
55
|
}
|
|
@@ -60,6 +60,7 @@ class LarkScheduleAdapter {
|
|
|
60
60
|
const now = new Date().toISOString();
|
|
61
61
|
this.options.store.upsertPlatformThreadBinding({
|
|
62
62
|
workspace_id: job.workspaceId,
|
|
63
|
+
platform: job.platform,
|
|
63
64
|
chat_id: channelId,
|
|
64
65
|
platform_user_id: participantId,
|
|
65
66
|
thread_id: thread.id,
|
|
@@ -67,9 +68,9 @@ class LarkScheduleAdapter {
|
|
|
67
68
|
created_at: now,
|
|
68
69
|
updated_at: now,
|
|
69
70
|
});
|
|
70
|
-
const authorized = this.options.store.getAuthorizedUser(job.workspaceId, participantId);
|
|
71
|
+
const authorized = this.options.store.getAuthorizedUser(job.workspaceId, participantId, job.platform);
|
|
71
72
|
if (authorized) {
|
|
72
|
-
this.options.store.updateAuthorizedUserThread(job.workspaceId, participantId, thread.id);
|
|
73
|
+
this.options.store.updateAuthorizedUserThread(job.workspaceId, participantId, thread.id, job.platform);
|
|
73
74
|
}
|
|
74
75
|
return thread.id;
|
|
75
76
|
}
|
package/dist-electron/services/local-ai-core/src/scheduler/scheduled-conversation-executor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ScheduledConversationExecutor = void 0;
|
|
4
|
+
const scheduled_job_route_js_1 = require("./scheduled-job-route.js");
|
|
4
5
|
const TERMINAL_RUN_STATES = new Set(['completed', 'failed', 'interrupted']);
|
|
5
6
|
const SCHEDULED_RUN_PERMISSION_MODE = 'bypassPermissions';
|
|
6
7
|
class ScheduledConversationExecutor {
|
|
@@ -14,6 +15,7 @@ class ScheduledConversationExecutor {
|
|
|
14
15
|
try {
|
|
15
16
|
const sendResult = await this.options.workspaceRouter.sendThreadMessage(target.threadId, prompt, {
|
|
16
17
|
permissionMode: SCHEDULED_RUN_PERMISSION_MODE,
|
|
18
|
+
runtimeEnv: this.buildScheduledRuntimeEnv(target),
|
|
17
19
|
});
|
|
18
20
|
await this.waitForRun(sendResult.runId, timeoutMs);
|
|
19
21
|
const thread = await this.options.workspaceRouter.getThread(target.threadId);
|
|
@@ -31,6 +33,26 @@ class ScheduledConversationExecutor {
|
|
|
31
33
|
await policy.afterExecute?.(target, job);
|
|
32
34
|
}
|
|
33
35
|
}
|
|
36
|
+
buildScheduledRuntimeEnv(target) {
|
|
37
|
+
const route = target.route;
|
|
38
|
+
const basePlatform = (0, scheduled_job_route_js_1.getChannelPlatformBase)(target.platform);
|
|
39
|
+
const env = {};
|
|
40
|
+
if (basePlatform && basePlatform !== 'local') {
|
|
41
|
+
env.LOCAL_AI_PLATFORM = basePlatform;
|
|
42
|
+
env.LOCAL_AI_ROUTE_TYPE = (0, scheduled_job_route_js_1.routeTypeForPlatform)(target.platform);
|
|
43
|
+
}
|
|
44
|
+
const instanceId = route.instanceId || (0, scheduled_job_route_js_1.getChannelPlatformInstanceId)(target.platform);
|
|
45
|
+
if (instanceId) {
|
|
46
|
+
env.LOCAL_AI_PLATFORM_INSTANCE_ID = instanceId;
|
|
47
|
+
}
|
|
48
|
+
if (route.channelId) {
|
|
49
|
+
env.LOCAL_AI_CHAT_ID = route.channelId;
|
|
50
|
+
}
|
|
51
|
+
if (route.participantId) {
|
|
52
|
+
env.LOCAL_AI_PLATFORM_USER_ID = route.participantId;
|
|
53
|
+
}
|
|
54
|
+
return env;
|
|
55
|
+
}
|
|
34
56
|
async waitForRun(runId, timeoutMs) {
|
|
35
57
|
const startedAt = Date.now();
|
|
36
58
|
while (Date.now() - startedAt < timeoutMs) {
|
package/dist-electron/services/local-ai-core/src/scheduler/scheduled-job-application-service.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ScheduledJobApplicationService = void 0;
|
|
4
|
+
const scheduled_job_route_js_1 = require("./scheduled-job-route.js");
|
|
5
|
+
class ScheduledJobApplicationService {
|
|
6
|
+
options;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
listJobs(workspaceId) {
|
|
11
|
+
return this.options.scheduler.listJobs(workspaceId);
|
|
12
|
+
}
|
|
13
|
+
getJob(jobId) {
|
|
14
|
+
return this.options.scheduler.getJob(jobId);
|
|
15
|
+
}
|
|
16
|
+
createJob(input) {
|
|
17
|
+
return this.options.scheduler.createJob(this.resolveCreateInput(input));
|
|
18
|
+
}
|
|
19
|
+
createCronJob(input) {
|
|
20
|
+
return this.options.scheduler.createJob({
|
|
21
|
+
workspaceId: input.workspaceId,
|
|
22
|
+
platform: input.platform,
|
|
23
|
+
route: this.resolveExplicitRoute(input.platform, input.route),
|
|
24
|
+
triggerType: 'cron',
|
|
25
|
+
cronExpr: input.schedule,
|
|
26
|
+
promptTemplate: input.message,
|
|
27
|
+
description: `${input.name} · ${input.scheduleDescription}`,
|
|
28
|
+
enabled: true,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
updateJob(jobId, input) {
|
|
32
|
+
return this.options.scheduler.updateJob(jobId, {
|
|
33
|
+
...input,
|
|
34
|
+
...(input.route ? { route: (0, scheduled_job_route_js_1.withoutThreadRoute)(input.route) } : {}),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
deleteJob(jobId) {
|
|
38
|
+
return this.options.scheduler.deleteJob(jobId);
|
|
39
|
+
}
|
|
40
|
+
runJobNow(jobId) {
|
|
41
|
+
return this.options.scheduler.runJobNow(jobId);
|
|
42
|
+
}
|
|
43
|
+
listJobRuns(jobId) {
|
|
44
|
+
return this.options.scheduler.listJobRuns(jobId);
|
|
45
|
+
}
|
|
46
|
+
listJobsForThread(threadId) {
|
|
47
|
+
const binding = this.options.store.getPlatformThreadBindingByThreadId(threadId);
|
|
48
|
+
return this.options.scheduler
|
|
49
|
+
.listJobs()
|
|
50
|
+
.filter((job) => job.route.threadId === threadId ||
|
|
51
|
+
(binding ? (0, scheduled_job_route_js_1.scheduledJobMatchesPlatformBinding)(job, binding) : false));
|
|
52
|
+
}
|
|
53
|
+
resolveCreateInput(input) {
|
|
54
|
+
if (input.platform && input.route) {
|
|
55
|
+
return {
|
|
56
|
+
...input,
|
|
57
|
+
route: this.resolveExplicitRoute(input.platform, input.route),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const threadId = String(input.threadId || input.route?.threadId || '').trim();
|
|
61
|
+
if (threadId) {
|
|
62
|
+
const binding = this.options.store.getPlatformThreadBindingByThreadId(threadId);
|
|
63
|
+
if (binding && binding.workspace_id === input.workspaceId) {
|
|
64
|
+
return {
|
|
65
|
+
...input,
|
|
66
|
+
platform: binding.platform,
|
|
67
|
+
route: (0, scheduled_job_route_js_1.routeFromPlatformThreadBinding)(binding),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
...input,
|
|
73
|
+
platform: 'local',
|
|
74
|
+
route: {
|
|
75
|
+
type: 'local.thread',
|
|
76
|
+
channelId: input.workspaceId,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
resolveExplicitRoute(platform, route) {
|
|
81
|
+
return (0, scheduled_job_route_js_1.routeWithPlatformInstance)((0, scheduled_job_route_js_1.withoutThreadRoute)(route), platform);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
exports.ScheduledJobApplicationService = ScheduledJobApplicationService;
|
|
@@ -1,15 +1,48 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.withoutThreadRoute = withoutThreadRoute;
|
|
4
|
+
exports.getChannelPlatformBase = getChannelPlatformBase;
|
|
5
|
+
exports.getChannelPlatformInstanceId = getChannelPlatformInstanceId;
|
|
6
|
+
exports.platformMatches = platformMatches;
|
|
7
|
+
exports.routeWithPlatformInstance = routeWithPlatformInstance;
|
|
8
|
+
exports.routeTypeForPlatform = routeTypeForPlatform;
|
|
9
|
+
exports.routeFromPlatformThreadBinding = routeFromPlatformThreadBinding;
|
|
4
10
|
exports.scheduledJobMatchesPlatformBinding = scheduledJobMatchesPlatformBinding;
|
|
5
11
|
exports.scheduledJobMatchesCliContext = scheduledJobMatchesCliContext;
|
|
6
12
|
function withoutThreadRoute(route) {
|
|
7
13
|
const { threadId: _threadId, ...deliveryRoute } = route;
|
|
8
14
|
return deliveryRoute;
|
|
9
15
|
}
|
|
16
|
+
function getChannelPlatformBase(platform) {
|
|
17
|
+
return String(platform || '').trim().toLowerCase().split(':', 1)[0] || '';
|
|
18
|
+
}
|
|
19
|
+
function getChannelPlatformInstanceId(platform) {
|
|
20
|
+
const normalized = String(platform || '').trim().toLowerCase();
|
|
21
|
+
const separator = normalized.indexOf(':');
|
|
22
|
+
return separator >= 0 ? normalized.slice(separator + 1) : '';
|
|
23
|
+
}
|
|
24
|
+
function platformMatches(candidate, expectedBase) {
|
|
25
|
+
return getChannelPlatformBase(candidate) === expectedBase;
|
|
26
|
+
}
|
|
27
|
+
function routeWithPlatformInstance(route, platform) {
|
|
28
|
+
const instanceId = route.instanceId || getChannelPlatformInstanceId(platform);
|
|
29
|
+
return instanceId ? { ...route, instanceId } : route;
|
|
30
|
+
}
|
|
31
|
+
function routeTypeForPlatform(platform) {
|
|
32
|
+
const base = getChannelPlatformBase(platform);
|
|
33
|
+
return base === 'lark' || base === 'weixin' ? 'channel.chat' : base;
|
|
34
|
+
}
|
|
35
|
+
function routeFromPlatformThreadBinding(binding) {
|
|
36
|
+
return {
|
|
37
|
+
type: routeTypeForPlatform(binding.platform),
|
|
38
|
+
channelId: binding.chat_id,
|
|
39
|
+
instanceId: getChannelPlatformInstanceId(binding.platform) || undefined,
|
|
40
|
+
participantId: binding.platform_user_id,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
10
43
|
function scheduledJobMatchesPlatformBinding(job, binding) {
|
|
11
44
|
return (job.workspaceId === binding.workspace_id &&
|
|
12
|
-
job.platform === binding.platform &&
|
|
45
|
+
getChannelPlatformBase(job.platform) === getChannelPlatformBase(binding.platform) &&
|
|
13
46
|
job.route.channelId === binding.chat_id &&
|
|
14
47
|
String(job.route.participantId || '') === String(binding.platform_user_id || ''));
|
|
15
48
|
}
|
|
@@ -18,7 +51,7 @@ function scheduledJobMatchesCliContext(job, context) {
|
|
|
18
51
|
return false;
|
|
19
52
|
}
|
|
20
53
|
return (job.workspaceId === context.workspaceId &&
|
|
21
|
-
job.platform === context.platform &&
|
|
54
|
+
getChannelPlatformBase(job.platform) === getChannelPlatformBase(context.platform) &&
|
|
22
55
|
job.route.channelId === context.chatId &&
|
|
23
56
|
String(job.route.participantId || '') === String(context.platformUserId || ''));
|
|
24
57
|
}
|
|
@@ -16,7 +16,7 @@ class WeixinScheduleAdapter {
|
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
supports(job) {
|
|
19
|
-
return job.platform
|
|
19
|
+
return (0, scheduled_job_route_js_1.platformMatches)(job.platform, 'weixin') && (job.route.type === 'channel.chat' || job.route.type === 'weixin_chat');
|
|
20
20
|
}
|
|
21
21
|
async execute(context) {
|
|
22
22
|
const { job } = context;
|
|
@@ -32,7 +32,7 @@ class WeixinScheduleAdapter {
|
|
|
32
32
|
if (!channelRuntime.sendScheduledMessage) {
|
|
33
33
|
throw new Error('WeChat channel runtime does not support scheduled delivery.');
|
|
34
34
|
}
|
|
35
|
-
platformMessageId = await channelRuntime.sendScheduledMessage(job.workspaceId, (0, scheduled_job_route_js_1.withoutThreadRoute)(job.route), execution.replyText);
|
|
35
|
+
platformMessageId = await channelRuntime.sendScheduledMessage(job.workspaceId, (0, scheduled_job_route_js_1.routeWithPlatformInstance)((0, scheduled_job_route_js_1.withoutThreadRoute)(job.route), job.platform), execution.replyText);
|
|
36
36
|
if (!platformMessageId) {
|
|
37
37
|
throw new Error('WeChat gateway did not return a message id for scheduled delivery.');
|
|
38
38
|
}
|
|
@@ -49,7 +49,7 @@ class WeixinScheduleAdapter {
|
|
|
49
49
|
const route = job.route;
|
|
50
50
|
const channelId = route.channelId;
|
|
51
51
|
const participantId = route.participantId || '';
|
|
52
|
-
const binding = this.options.store.getPlatformThreadBinding(job.workspaceId, channelId, participantId,
|
|
52
|
+
const binding = this.options.store.getPlatformThreadBinding(job.workspaceId, channelId, participantId, job.platform);
|
|
53
53
|
if (binding?.thread_id && await this.threadExists(binding.thread_id)) {
|
|
54
54
|
return binding.thread_id;
|
|
55
55
|
}
|
|
@@ -68,9 +68,9 @@ class WeixinScheduleAdapter {
|
|
|
68
68
|
created_at: now,
|
|
69
69
|
updated_at: now,
|
|
70
70
|
});
|
|
71
|
-
const authorized = this.options.store.getAuthorizedUser(job.workspaceId, participantId,
|
|
71
|
+
const authorized = this.options.store.getAuthorizedUser(job.workspaceId, participantId, job.platform);
|
|
72
72
|
if (authorized) {
|
|
73
|
-
this.options.store.updateAuthorizedUserThread(job.workspaceId, participantId, thread.id,
|
|
73
|
+
this.options.store.updateAuthorizedUserThread(job.workspaceId, participantId, thread.id, job.platform);
|
|
74
74
|
}
|
|
75
75
|
return thread.id;
|
|
76
76
|
}
|
|
@@ -14,7 +14,7 @@ class ApiClient {
|
|
|
14
14
|
this.baseUrl = DEFAULT_API_BASE;
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
|
-
this.baseUrl = trimmed.endsWith('/api/v1')
|
|
17
|
+
this.baseUrl = trimmed.endsWith('/api/v1') || trimmed.endsWith('/api/local/v1')
|
|
18
18
|
? trimmed
|
|
19
19
|
: `${trimmed.replace(/\/+$/, '')}/api/v1`;
|
|
20
20
|
}
|
|
@@ -214,6 +214,56 @@ function createIo() {
|
|
|
214
214
|
});
|
|
215
215
|
strict_1.default.match(read().stdout, /Sent file out\.pdf to chat-1: msg-file-1/);
|
|
216
216
|
});
|
|
217
|
+
(0, node_test_1.default)('lac channel send-file normalizes instance-qualified platform env into route instance', async () => {
|
|
218
|
+
let capturedUrl = '';
|
|
219
|
+
let capturedBody = null;
|
|
220
|
+
const server = (0, node_http_1.createServer)(async (req, res) => {
|
|
221
|
+
if (req.method === 'POST') {
|
|
222
|
+
capturedUrl = req.url || '';
|
|
223
|
+
const chunks = [];
|
|
224
|
+
for await (const chunk of req) {
|
|
225
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
226
|
+
}
|
|
227
|
+
capturedBody = Buffer.concat(chunks).toString('utf8');
|
|
228
|
+
res.setHeader('content-type', 'application/json');
|
|
229
|
+
res.end(JSON.stringify({
|
|
230
|
+
ok: true,
|
|
231
|
+
data: {
|
|
232
|
+
platform: 'lark',
|
|
233
|
+
workspaceId: '知识库',
|
|
234
|
+
channelId: 'chat-1',
|
|
235
|
+
messageIds: ['msg-file-1'],
|
|
236
|
+
attachments: [{ kind: 'file', fileName: 'out.pdf', fileSize: 123 }],
|
|
237
|
+
},
|
|
238
|
+
}));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
res.statusCode = 404;
|
|
242
|
+
res.end(JSON.stringify({ ok: false, error: 'not found' }));
|
|
243
|
+
});
|
|
244
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
245
|
+
const address = server.address();
|
|
246
|
+
(0, strict_1.default)(address && typeof address === 'object');
|
|
247
|
+
const { io } = createIo();
|
|
248
|
+
const exitCode = await (0, lac_js_1.runCli)(['channel', 'send-file', '--path', 'reports/out.pdf'], {
|
|
249
|
+
LOCAL_AI_CORE_BASE: `http://127.0.0.1:${address.port}/api/local/v1`,
|
|
250
|
+
LOCAL_AI_WORKSPACE_ID: '知识库',
|
|
251
|
+
LOCAL_AI_WORKSPACE_PATH: '/workspace/project',
|
|
252
|
+
LOCAL_AI_PLATFORM: 'lark:lark-1',
|
|
253
|
+
LOCAL_AI_CHAT_ID: 'chat-1',
|
|
254
|
+
LOCAL_AI_PLATFORM_USER_ID: 'user-1',
|
|
255
|
+
}, io);
|
|
256
|
+
server.close();
|
|
257
|
+
strict_1.default.equal(exitCode, 0);
|
|
258
|
+
strict_1.default.equal(capturedUrl, '/api/local/v1/platforms/lark/%E7%9F%A5%E8%AF%86%E5%BA%93/messages');
|
|
259
|
+
(0, strict_1.default)(capturedBody);
|
|
260
|
+
strict_1.default.deepEqual(JSON.parse(capturedBody).route, {
|
|
261
|
+
type: 'channel.chat',
|
|
262
|
+
channelId: 'chat-1',
|
|
263
|
+
instanceId: 'lark-1',
|
|
264
|
+
participantId: 'user-1',
|
|
265
|
+
});
|
|
266
|
+
});
|
|
217
267
|
(0, node_test_1.default)('lac channel send-file posts allowed absolute paths through the same outbound message path', async () => {
|
|
218
268
|
let capturedBody = null;
|
|
219
269
|
const server = (0, node_http_1.createServer)(async (req, res) => {
|