@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.
Files changed (68) hide show
  1. package/README.md +67 -0
  2. package/dist/renderer/assets/{Badge-VeSLCPPV.js → Badge-BM4WCcua.js} +1 -1
  3. package/dist/renderer/assets/{Chat-DWTQeTgn.js → Chat-DoyBHOsx.js} +1 -1
  4. package/dist/renderer/assets/{ChatMarkdown-C9gSQIVb.js → ChatMarkdown-BWj04kgW.js} +1 -1
  5. package/dist/renderer/assets/{Config-CTA-pZNR.js → Config-A95OuazQ.js} +1 -1
  6. package/dist/renderer/assets/{CronList-BngBCnFY.js → CronList-DsPU4zGT.js} +1 -1
  7. package/dist/renderer/assets/{Dashboard-D4pGvDpc.js → Dashboard-D8Vpj97x.js} +2 -2
  8. package/dist/renderer/assets/{EmptyState-DMf8rzDs.js → EmptyState-x1uVoSxZ.js} +1 -1
  9. package/dist/renderer/assets/{KnowledgeDetail-C1Ym_cHk.js → KnowledgeDetail-C9TbcyNJ.js} +1 -1
  10. package/dist/renderer/assets/{KnowledgeHome-DPRiR72G.js → KnowledgeHome-Cg9Fb41Q.js} +1 -1
  11. package/dist/renderer/assets/Logs-oWaKA60u.js +1 -0
  12. package/dist/renderer/assets/{Modal-BDhhPv2l.js → Modal-24hTXXHK.js} +1 -1
  13. package/dist/renderer/assets/{Page-DdDZN7AE.js → Page-YRtQonKZ.js} +1 -1
  14. package/dist/renderer/assets/{ProjectDetail-DCsr3LkW.js → ProjectDetail-DScsxJO-.js} +1 -1
  15. package/dist/renderer/assets/{ProjectList-Cnm-iLvg.js → ProjectList-8L5GD7PN.js} +1 -1
  16. package/dist/renderer/assets/{Select-Dw4FCXhJ.js → Select-BI8Uw_DF.js} +1 -1
  17. package/dist/renderer/assets/{SessionChat-O-6abK8x.js → SessionChat-BIoCWdDD.js} +1 -1
  18. package/dist/renderer/assets/{SessionList-5yptvVL9.js → SessionList-BsEJeDxI.js} +1 -1
  19. package/dist/renderer/assets/{ThreadChat-CIM_o2HO.js → ThreadChat-DJi0UBta.js} +1 -1
  20. package/dist/renderer/assets/{Workspace-D9UeUCNr.js → Workspace-tlJxrgC5.js} +1 -1
  21. package/dist/renderer/assets/{arrow-left-CKfxNOXE.js → arrow-left-pzaBhi9y.js} +1 -1
  22. package/dist/renderer/assets/{book-open-CQddlENV.js → book-open-CNhlVyqh.js} +1 -1
  23. package/dist/renderer/assets/{check-CZYvOmIS.js → check-Cg2LLCdl.js} +1 -1
  24. package/dist/renderer/assets/{chevron-down-CVIWFoXF.js → chevron-down-Dgqg07wf.js} +1 -1
  25. package/dist/renderer/assets/{circle-DSfdQM7k.js → circle-BBd_jIS2.js} +1 -1
  26. package/dist/renderer/assets/{desktop-B9KUgeOV.js → desktop-C0S-Yv8G.js} +1 -1
  27. package/dist/renderer/assets/{funnel-pB5NegZB.js → funnel-DwSh-SrX.js} +1 -1
  28. package/dist/renderer/assets/{heart-BT5laZ_b.js → heart-CWVc-KhZ.js} +1 -1
  29. package/dist/renderer/assets/{index-DFiJR0vv.css → index-CaQZSRbg.css} +1 -1
  30. package/dist/renderer/assets/{index-BYyDk28K.js → index-DZbeAYVk.js} +2 -2
  31. package/dist/renderer/assets/{index-zTUUYax3.js → index-HYmDNQ9k.js} +1 -1
  32. package/dist/renderer/assets/{pencil-C0thxLi6.js → pencil-tI4tCLz0.js} +1 -1
  33. package/dist/renderer/assets/{play-CmZ0kWtt.js → play-Coi2HA1w.js} +1 -1
  34. package/dist/renderer/assets/{plug-CeDQatBe.js → plug-D4TCyW9b.js} +1 -1
  35. package/dist/renderer/assets/{plus-Di__xFOZ.js → plus-p62AgpoB.js} +1 -1
  36. package/dist/renderer/assets/projects-CTDuVxeQ.js +1 -0
  37. package/dist/renderer/assets/{save-BDatCjfO.js → save-fpMhkVV0.js} +1 -1
  38. package/dist/renderer/assets/{search-DnTOjP0C.js → search-BHKIStdA.js} +1 -1
  39. package/dist/renderer/assets/{server-Ey_iHY6m.js → server-0oTWed8U.js} +1 -1
  40. package/dist/renderer/assets/{session-utils-DWaM7Rkr.js → session-utils-D0Qv0I0-.js} +1 -1
  41. package/dist/renderer/assets/{sessions-CYYjLD5I.js → sessions-C8ApKQgC.js} +1 -1
  42. package/dist/renderer/assets/{shield-check-Br8BQzcn.js → shield-check-BAJI9jzC.js} +1 -1
  43. package/dist/renderer/assets/{trash-2-7YD616fw.js → trash-2-B-Ygm-x6.js} +1 -1
  44. package/dist/renderer/assets/{user-BR_5jmr9.js → user-Cv8rzw8l.js} +1 -1
  45. package/dist/renderer/assets/{wifi-off-By9QdREz.js → wifi-off-BImS3nkm.js} +1 -1
  46. package/dist/renderer/index.html +2 -2
  47. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-backend.js +3 -5
  48. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-session-coordinator.js +23 -6
  49. package/dist-electron/services/local-ai-core/src/cli/lac.js +7 -1
  50. package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +9 -19
  51. package/dist-electron/services/local-ai-core/src/kernel/rotating-logger.js +232 -0
  52. package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +12 -44
  53. package/dist-electron/services/local-ai-core/src/runtime/local-core-runtime-state.js +40 -84
  54. package/dist-electron/services/local-ai-core/src/runtime/server-routes.js +3 -0
  55. package/dist-electron/services/local-ai-core/src/runtime/server.js +14 -0
  56. package/dist-electron/services/local-ai-core/src/scheduler/lark-schedule-adapter.js +6 -5
  57. package/dist-electron/services/local-ai-core/src/scheduler/scheduled-conversation-executor.js +22 -0
  58. package/dist-electron/services/local-ai-core/src/scheduler/scheduled-job-application-service.js +84 -0
  59. package/dist-electron/services/local-ai-core/src/scheduler/scheduled-job-route.js +35 -2
  60. package/dist-electron/services/local-ai-core/src/scheduler/weixin-schedule-adapter.js +5 -5
  61. package/dist-electron/src/api/client.js +1 -1
  62. package/dist-electron/tests/electron/lac-cli.test.js +50 -0
  63. package/dist-electron/tests/electron/plugin-kernel.test.js +162 -44
  64. package/dist-electron/tests/electron/workspace-task-store.test.js +123 -0
  65. package/dist-electron/tests/integration/local-core-refactor.test.js +13 -3
  66. package/package.json +1 -1
  67. package/dist/renderer/assets/Logs-CRHecmzO.js +0 -1
  68. 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
- scheduler;
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.scheduler = this.runtime.scheduler;
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.scheduler.listJobs(workspaceId);
187
+ return this.scheduledJobs.listJobs(workspaceId);
186
188
  }
187
189
  async getScheduledJob(jobId) {
188
- const job = this.scheduler.getJob(jobId);
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.scheduler.createJob(this.resolveScheduledJobCreateInput(input));
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.scheduler.updateJob(jobId, {
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.scheduler.deleteJob(jobId);
203
+ return this.scheduledJobs.deleteJob(jobId);
236
204
  }
237
205
  async runScheduledJob(jobId) {
238
- return this.scheduler.runJobNow(jobId);
206
+ return this.scheduledJobs.runJobNow(jobId);
239
207
  }
240
208
  async listScheduledJobRuns(jobId) {
241
- return this.scheduler.listJobRuns(jobId);
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.logPath = (0, node_path_1.join)(this.runtimeDir, LOG_FILE_NAME);
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
- const normalizedLimit = normalizeLogLimit(limit);
118
- const current = tailLogFile(this.logPath, normalizedLimit);
119
- if (current.length >= normalizedLimit) {
120
- return current.slice(-normalizedLimit);
121
- }
122
- const rotated = tailLogFile(`${this.logPath}.1`, normalizedLimit - current.length);
123
- return [...rotated, ...current].slice(-normalizedLimit);
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.appendLogFile(message);
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 === 'lark' && (job.route.type === 'channel.chat' || job.route.type === 'lark_chat');
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, 'lark');
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
  }
@@ -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) {
@@ -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 === 'weixin' && (job.route.type === 'channel.chat' || job.route.type === 'weixin_chat');
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, 'weixin');
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, 'weixin');
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, 'weixin');
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) => {