@kafca/agentdock 0.1.23 → 0.1.24
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 +1 -0
- package/dist/renderer/assets/{Badge-C2K5apfV.js → Badge-CXcUMqb-.js} +1 -1
- package/dist/renderer/assets/{Chat-DI2YeVKb.js → Chat-BRxdPzi-.js} +1 -1
- package/dist/renderer/assets/{ChatMarkdown-DMAr15Oi.js → ChatMarkdown-sWGBIfIb.js} +1 -1
- package/dist/renderer/assets/{Config-7rP3EuUf.js → Config-DqxuxIGg.js} +1 -1
- package/dist/renderer/assets/{CronList-81QslYE8.js → CronList-OgHxbti5.js} +1 -1
- package/dist/renderer/assets/{Dashboard-Db2T7rSZ.js → Dashboard-DmklXiH1.js} +2 -2
- package/dist/renderer/assets/{EmptyState-BBJ3cTvB.js → EmptyState-BvDmi8dq.js} +1 -1
- package/dist/renderer/assets/{KnowledgeDetail-Dz_6flG3.js → KnowledgeDetail-Bj59Hdpo.js} +1 -1
- package/dist/renderer/assets/{KnowledgeHome-DIh046mN.js → KnowledgeHome-CigQ-nSo.js} +1 -1
- package/dist/renderer/assets/{Logs-BXBtHJLf.js → Logs-Bfc7gBpY.js} +1 -1
- package/dist/renderer/assets/{Modal-BB2Av_Xs.js → Modal-CfmiFknz.js} +1 -1
- package/dist/renderer/assets/{Page-DPSdvMgU.js → Page-BxtLEw_b.js} +1 -1
- package/dist/renderer/assets/{ProjectDetail-C6EWGvGg.js → ProjectDetail-CcUphxGm.js} +1 -1
- package/dist/renderer/assets/{ProjectList-BBo5ygnS.js → ProjectList-CVkvDIfy.js} +1 -1
- package/dist/renderer/assets/{Select-CNX-Tx_O.js → Select-DgQMZ0MD.js} +1 -1
- package/dist/renderer/assets/{SessionChat-BIn46jqZ.js → SessionChat-CRmGKfBx.js} +1 -1
- package/dist/renderer/assets/{SessionList-CZt6f1Rd.js → SessionList-D0E8XbYw.js} +1 -1
- package/dist/renderer/assets/ThreadChat-DUgqZSBB.js +23 -0
- package/dist/renderer/assets/{Workspace-C_4tdpjX.js → Workspace-Q4g4rlRT.js} +1 -1
- package/dist/renderer/assets/{arrow-left-cHr5wkpq.js → arrow-left-BySN_zhF.js} +1 -1
- package/dist/renderer/assets/{book-open-4nx5C8T-.js → book-open-BGlrnrTc.js} +1 -1
- package/dist/renderer/assets/{check-exTS-R-x.js → check-BiJogEa5.js} +1 -1
- package/dist/renderer/assets/{chevron-down-DSvxXQa0.js → chevron-down-D2fHRJv6.js} +1 -1
- package/dist/renderer/assets/{circle-DyvEovdw.js → circle-CIcjixmI.js} +1 -1
- package/dist/renderer/assets/desktop-DAXY9Bxv.js +4 -0
- package/dist/renderer/assets/{desktop-B-KVT10D.js → desktop-DyA_NYew.js} +1 -1
- package/dist/renderer/assets/{funnel-DA9qVdTy.js → funnel-BUzwb-id.js} +1 -1
- package/dist/renderer/assets/{heart-DuEV0ZRJ.js → heart-DZEQ1z_V.js} +1 -1
- package/dist/renderer/assets/{index-Biz-aexG.js → index-CB_KyLOr.js} +2 -2
- package/dist/renderer/assets/{index-BgflqkCN.js → index-DgsIo4vJ.js} +1 -1
- package/dist/renderer/assets/{pencil-DBDxeNmO.js → pencil-DGL4agcl.js} +1 -1
- package/dist/renderer/assets/{play-IzlP83_7.js → play-DzasCLM4.js} +1 -1
- package/dist/renderer/assets/{plug-CuT-qFnW.js → plug-dubNMe_m.js} +1 -1
- package/dist/renderer/assets/{plus-DEqgkn63.js → plus-DN4kML4k.js} +1 -1
- package/dist/renderer/assets/projects-migPtfRe.js +1 -0
- package/dist/renderer/assets/{save-D4sRo07y.js → save-CNIc9tjl.js} +1 -1
- package/dist/renderer/assets/{search-DbPZLuV8.js → search-C0EGbF3I.js} +1 -1
- package/dist/renderer/assets/{server-D1jUQmwN.js → server-BMxQ4zmc.js} +1 -1
- package/dist/renderer/assets/{session-utils-Aife8ep2.js → session-utils-Bf5O-iMi.js} +1 -1
- package/dist/renderer/assets/{sessions-DaH3H7gC.js → sessions-BMSs6rEI.js} +1 -1
- package/dist/renderer/assets/{shield-check-BGwUFWNr.js → shield-check-i3Tf1TUz.js} +1 -1
- package/dist/renderer/assets/{trash-2-CIFBioBa.js → trash-2-SKrcp18f.js} +1 -1
- package/dist/renderer/assets/{user-CnKcvHdo.js → user-cNUZddjZ.js} +1 -1
- package/dist/renderer/assets/{wifi-off-ClO53WGa.js → wifi-off-Dyb84ANR.js} +1 -1
- package/dist/renderer/index.html +1 -1
- package/dist-electron/electron/channel-content.test.js +29 -0
- package/dist-electron/electron/lac-cli.test.js +49 -4
- package/dist-electron/electron/local-core-acp-content.test.js +39 -0
- package/dist-electron/electron/local-core-contracts.test.js +67 -0
- package/dist-electron/electron/local-core-refactor.test.js +123 -0
- package/dist-electron/electron/workspace-task-store.test.js +124 -0
- package/dist-electron/packages/contracts/src/local-core.js +110 -0
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-content.js +25 -4
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-response-processor.js +3 -2
- package/dist-electron/services/local-ai-core/src/acp/local-core-acp-store.js +44 -28
- package/dist-electron/services/local-ai-core/src/acp/workspace-acp-permissions.js +5 -48
- package/dist-electron/services/local-ai-core/src/channel/lark/local-core-lark-gateway.js +68 -25
- package/dist-electron/services/local-ai-core/src/channel/shared/content.js +23 -0
- package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js +23 -28
- package/dist-electron/services/local-ai-core/src/cli/lac.js +19 -16
- package/dist-electron/services/local-ai-core/src/scheduler/job-id.js +15 -0
- package/dist-electron/services/local-ai-core/src/scheduler/scheduler-service.js +30 -5
- package/dist-electron/shared/desktop.js +27 -5
- package/dist-electron/src/pages/Threads/thread-chat-model.js +57 -0
- package/dist-electron/src/pages/Threads/thread-chat-permission.test.js +70 -0
- package/package.json +1 -1
- package/dist/renderer/assets/ThreadChat-BotD0gKQ.js +0 -23
- package/dist/renderer/assets/desktop-CVibRyAw.js +0 -4
- package/dist/renderer/assets/projects-DDtDN1rc.js +0 -1
|
@@ -39,6 +39,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
39
39
|
const node_fs_1 = require("node:fs");
|
|
40
40
|
const node_path_1 = require("node:path");
|
|
41
41
|
const desktop_js_1 = require("../../../../../shared/desktop.js");
|
|
42
|
+
const content_js_1 = require("../shared/content.js");
|
|
42
43
|
const file_utils_js_1 = require("../shared/file-utils.js");
|
|
43
44
|
const cards_js_1 = require("./cards.js");
|
|
44
45
|
const config_js_1 = require("./config.js");
|
|
@@ -650,7 +651,7 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
650
651
|
}
|
|
651
652
|
this.options.store.clearPlatformThreadMessageId(input.workspaceId, input.chatId, input.platformUserId);
|
|
652
653
|
const wrappedText = (0, desktop_js_1.wrapUserMessageWithSchedulerProtocol)(input.text);
|
|
653
|
-
await router.sendThreadMessage(threadId,
|
|
654
|
+
await router.sendThreadMessage(threadId, (0, content_js_1.createChannelThreadMessageInput)(wrappedText, input.contentParts));
|
|
654
655
|
return { paired: true, threadId };
|
|
655
656
|
}
|
|
656
657
|
close() {
|
|
@@ -857,15 +858,24 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
857
858
|
}
|
|
858
859
|
}
|
|
859
860
|
}
|
|
861
|
+
if (messageType === 'file') {
|
|
862
|
+
const fileKey = String(parsedContent.file_key || '').trim();
|
|
863
|
+
if (fileKey) {
|
|
864
|
+
try {
|
|
865
|
+
contentParts.push(await this.downloadMessageFile(workspaceId, String(message.message_id || ''), fileKey, String(parsedContent.file_name || parsedContent.name || '').trim(), instanceId));
|
|
866
|
+
}
|
|
867
|
+
catch (error) {
|
|
868
|
+
const errorText = `[File download failed: ${error instanceof Error ? error.message : String(error)}]`;
|
|
869
|
+
contentParts.push({ type: 'text', text: errorText });
|
|
870
|
+
this.options.log?.(`localcore-lark file download failed for ${workspaceId}: message=${String(message.message_id || '')} fileKey=${fileKey} error=${error instanceof Error ? error.message : String(error)}`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
860
874
|
if (contentParts.length === 0) {
|
|
861
875
|
this.options.log?.(`localcore-lark ignored unsupported message for ${workspaceId}: type=${String(message.message_type || 'unknown')} contentKeys=${JSON.stringify(Object.keys(parsedContent))}`);
|
|
862
876
|
return;
|
|
863
877
|
}
|
|
864
|
-
const displayText = text || (contentParts
|
|
865
|
-
.filter((part) => part.type === 'text')
|
|
866
|
-
.map((part) => part.text)
|
|
867
|
-
.join('\n\n')
|
|
868
|
-
.trim());
|
|
878
|
+
const displayText = text || this.summarizeInboundContentParts(contentParts);
|
|
869
879
|
const platformUserId = String(sender.sender_id?.user_id || sender.sender_id?.open_id || '').trim();
|
|
870
880
|
const chatId = String(message.chat_id || platformUserId).trim();
|
|
871
881
|
if (!platformUserId || !chatId) {
|
|
@@ -887,25 +897,6 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
887
897
|
contentParts,
|
|
888
898
|
});
|
|
889
899
|
}
|
|
890
|
-
createWrappedContentParts(wrappedText, parts) {
|
|
891
|
-
const nonTextParts = Array.isArray(parts)
|
|
892
|
-
? parts.filter((part) => part.type !== 'text')
|
|
893
|
-
: [];
|
|
894
|
-
return [
|
|
895
|
-
{ type: 'text', text: wrappedText },
|
|
896
|
-
...nonTextParts,
|
|
897
|
-
];
|
|
898
|
-
}
|
|
899
|
-
createThreadMessageInput(wrappedText, parts) {
|
|
900
|
-
const hasNonTextPart = Array.isArray(parts) && parts.some((part) => part.type !== 'text');
|
|
901
|
-
if (!hasNonTextPart) {
|
|
902
|
-
return wrappedText;
|
|
903
|
-
}
|
|
904
|
-
return {
|
|
905
|
-
displayText: wrappedText,
|
|
906
|
-
contentParts: this.createWrappedContentParts(wrappedText, parts),
|
|
907
|
-
};
|
|
908
|
-
}
|
|
909
900
|
async downloadMessageImage(workspaceId, messageId, imageKey, instanceId) {
|
|
910
901
|
const state = this.resolveRuntimeState(workspaceId, instanceId).state;
|
|
911
902
|
if (!state?.client) {
|
|
@@ -935,6 +926,58 @@ class LocalCoreLarkGateway extends node_events_1.EventEmitter {
|
|
|
935
926
|
fileName: `${imageKey}.${this.sniffImageExtension(buffer)}`,
|
|
936
927
|
};
|
|
937
928
|
}
|
|
929
|
+
async downloadMessageFile(workspaceId, messageId, fileKey, fileName, instanceId) {
|
|
930
|
+
const state = this.resolveRuntimeState(workspaceId, instanceId).state;
|
|
931
|
+
if (!state?.client) {
|
|
932
|
+
throw new Error('Lark client is not connected');
|
|
933
|
+
}
|
|
934
|
+
if (!messageId) {
|
|
935
|
+
throw new Error('Lark file message is missing message_id');
|
|
936
|
+
}
|
|
937
|
+
const resource = await state.client.im.messageResource.get({
|
|
938
|
+
path: {
|
|
939
|
+
message_id: messageId,
|
|
940
|
+
file_key: fileKey,
|
|
941
|
+
},
|
|
942
|
+
params: {
|
|
943
|
+
type: 'file',
|
|
944
|
+
},
|
|
945
|
+
});
|
|
946
|
+
const buffer = await this.readLarkResourceBuffer(resource);
|
|
947
|
+
if (buffer.length === 0) {
|
|
948
|
+
throw new Error('Lark file resource is empty');
|
|
949
|
+
}
|
|
950
|
+
return {
|
|
951
|
+
type: 'file',
|
|
952
|
+
data: buffer.toString('base64'),
|
|
953
|
+
mimeType: this.extractHeaderMimeType(resource?.headers) || 'application/octet-stream',
|
|
954
|
+
fileName: fileName || fileKey,
|
|
955
|
+
size: buffer.length,
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
summarizeInboundContentParts(parts) {
|
|
959
|
+
const text = parts
|
|
960
|
+
.filter((part) => part.type === 'text')
|
|
961
|
+
.map((part) => part.text)
|
|
962
|
+
.filter(Boolean)
|
|
963
|
+
.join('\n\n')
|
|
964
|
+
.trim();
|
|
965
|
+
if (text)
|
|
966
|
+
return text;
|
|
967
|
+
const attachmentSummary = parts
|
|
968
|
+
.map((part) => {
|
|
969
|
+
if (part.type === 'image') {
|
|
970
|
+
return '[Image]';
|
|
971
|
+
}
|
|
972
|
+
if (part.type === 'file') {
|
|
973
|
+
return part.fileName ? `[File: ${part.fileName}]` : '[File]';
|
|
974
|
+
}
|
|
975
|
+
return '';
|
|
976
|
+
})
|
|
977
|
+
.filter(Boolean)
|
|
978
|
+
.join('\n');
|
|
979
|
+
return attachmentSummary;
|
|
980
|
+
}
|
|
938
981
|
async readLarkResourceBuffer(resource) {
|
|
939
982
|
const readable = resource?.getReadableStream?.();
|
|
940
983
|
if (!readable || typeof readable.on !== 'function') {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createWrappedChannelContentParts = createWrappedChannelContentParts;
|
|
4
|
+
exports.createChannelThreadMessageInput = createChannelThreadMessageInput;
|
|
5
|
+
function createWrappedChannelContentParts(wrappedText, parts) {
|
|
6
|
+
const nonTextParts = Array.isArray(parts)
|
|
7
|
+
? parts.filter((part) => part.type !== 'text')
|
|
8
|
+
: [];
|
|
9
|
+
return [
|
|
10
|
+
{ type: 'text', text: wrappedText },
|
|
11
|
+
...nonTextParts,
|
|
12
|
+
];
|
|
13
|
+
}
|
|
14
|
+
function createChannelThreadMessageInput(wrappedText, parts) {
|
|
15
|
+
const hasNonTextPart = Array.isArray(parts) && parts.some((part) => part.type !== 'text');
|
|
16
|
+
if (!hasNonTextPart) {
|
|
17
|
+
return wrappedText;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
displayText: wrappedText,
|
|
21
|
+
contentParts: createWrappedChannelContentParts(wrappedText, parts),
|
|
22
|
+
};
|
|
23
|
+
}
|
package/dist-electron/services/local-ai-core/src/channel/weixin/local-core-weixin-gateway.js
CHANGED
|
@@ -4,12 +4,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.LocalCoreWeixinGateway = void 0;
|
|
7
|
+
exports.createWeixinAttachmentContentPart = createWeixinAttachmentContentPart;
|
|
7
8
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
8
9
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
10
11
|
const node_events_1 = require("node:events");
|
|
11
12
|
const node_crypto_2 = require("node:crypto");
|
|
12
13
|
const desktop_js_1 = require("../../../../../shared/desktop.js");
|
|
14
|
+
const content_js_1 = require("../shared/content.js");
|
|
13
15
|
const file_utils_js_1 = require("../shared/file-utils.js");
|
|
14
16
|
const config_js_1 = require("./config.js");
|
|
15
17
|
const transport_js_1 = require("./transport.js");
|
|
@@ -97,6 +99,23 @@ function splitTextByUtf8Bytes(text, maxBytes) {
|
|
|
97
99
|
pushCurrent();
|
|
98
100
|
return chunks;
|
|
99
101
|
}
|
|
102
|
+
function createWeixinAttachmentContentPart(att) {
|
|
103
|
+
if (att.kind === 'image') {
|
|
104
|
+
if (!att.data)
|
|
105
|
+
return null;
|
|
106
|
+
return {
|
|
107
|
+
type: 'image',
|
|
108
|
+
data: att.data,
|
|
109
|
+
mimeType: att.mimeType,
|
|
110
|
+
fileName: att.name,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
type: 'file',
|
|
115
|
+
path: att.path,
|
|
116
|
+
fileName: att.name,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
100
119
|
function truncateTextByUtf8Bytes(text, maxBytes) {
|
|
101
120
|
const normalized = String(text || '').replace(/\r\n/g, '\n').trim();
|
|
102
121
|
if (utf8ByteLength(normalized) <= maxBytes)
|
|
@@ -658,7 +677,7 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
|
|
|
658
677
|
}
|
|
659
678
|
this.options.store.updatePlatformThreadMessageId(input.workspaceId, input.chatId, input.platformUserId, input.contextToken || input.messageId, platformKey);
|
|
660
679
|
const wrappedText = (0, desktop_js_1.wrapUserMessageWithSchedulerProtocol)(input.text);
|
|
661
|
-
await router.sendThreadMessage(threadId,
|
|
680
|
+
await router.sendThreadMessage(threadId, (0, content_js_1.createChannelThreadMessageInput)(wrappedText, input.contentParts));
|
|
662
681
|
return { paired: true, threadId };
|
|
663
682
|
}
|
|
664
683
|
// ==================== Private: Bindings ====================
|
|
@@ -825,14 +844,9 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
|
|
|
825
844
|
if (att) {
|
|
826
845
|
attachmentText += attachmentText ? '\n' : '';
|
|
827
846
|
attachmentText += att.kind === 'image' ? `[Image: ${att.path}]` : `[File "${att.name}": ${att.path}]`;
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
data: att.data,
|
|
832
|
-
mimeType: att.mimeType,
|
|
833
|
-
fileName: att.name,
|
|
834
|
-
});
|
|
835
|
-
}
|
|
847
|
+
const part = createWeixinAttachmentContentPart(att);
|
|
848
|
+
if (part)
|
|
849
|
+
attachmentParts.push(part);
|
|
836
850
|
}
|
|
837
851
|
}
|
|
838
852
|
catch (dlErr) {
|
|
@@ -1017,25 +1031,6 @@ class LocalCoreWeixinGateway extends node_events_1.EventEmitter {
|
|
|
1017
1031
|
mimeType: kind === 'image' ? this.mimeTypeForImageExt(ext) : undefined,
|
|
1018
1032
|
};
|
|
1019
1033
|
}
|
|
1020
|
-
createWrappedContentParts(wrappedText, parts) {
|
|
1021
|
-
const nonTextParts = Array.isArray(parts)
|
|
1022
|
-
? parts.filter((part) => part.type !== 'text')
|
|
1023
|
-
: [];
|
|
1024
|
-
return [
|
|
1025
|
-
{ type: 'text', text: wrappedText },
|
|
1026
|
-
...nonTextParts,
|
|
1027
|
-
];
|
|
1028
|
-
}
|
|
1029
|
-
createThreadMessageInput(wrappedText, parts) {
|
|
1030
|
-
const hasNonTextPart = Array.isArray(parts) && parts.some((part) => part.type !== 'text');
|
|
1031
|
-
if (!hasNonTextPart) {
|
|
1032
|
-
return wrappedText;
|
|
1033
|
-
}
|
|
1034
|
-
return {
|
|
1035
|
-
displayText: wrappedText,
|
|
1036
|
-
contentParts: this.createWrappedContentParts(wrappedText, parts),
|
|
1037
|
-
};
|
|
1038
|
-
}
|
|
1039
1034
|
mimeTypeForImageExt(ext) {
|
|
1040
1035
|
if (ext === '.jpg')
|
|
1041
1036
|
return 'image/jpeg';
|
|
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.runCli = runCli;
|
|
7
7
|
const node_process_1 = __importDefault(require("node:process"));
|
|
8
|
+
const index_js_1 = require("../../../../packages/contracts/src/index.js");
|
|
9
|
+
const job_id_js_1 = require("../scheduler/job-id.js");
|
|
8
10
|
const DEFAULT_BASE_URL = 'http://127.0.0.1:9831/api/local/v1';
|
|
9
11
|
async function runCli(argv, env = node_process_1.default.env, io = node_process_1.default) {
|
|
10
12
|
try {
|
|
@@ -51,7 +53,8 @@ async function runCli(argv, env = node_process_1.default.env, io = node_process_
|
|
|
51
53
|
async function handleChannelSendFile(flags, env, io, json) {
|
|
52
54
|
const context = resolveContext(flags, env);
|
|
53
55
|
const filePath = getRequiredFlag(flags, 'path');
|
|
54
|
-
const
|
|
56
|
+
const rawPlatform = getFlag(flags, 'platform') || context.platform;
|
|
57
|
+
const platform = rawPlatform ? (0, index_js_1.normalizeChannelPlatform)(rawPlatform) : '';
|
|
55
58
|
if (!platform) {
|
|
56
59
|
throw new Error('channel send-file requires a platform. Set LOCAL_AI_PLATFORM or pass --platform.');
|
|
57
60
|
}
|
|
@@ -113,8 +116,8 @@ async function handleAdd(flags, env, io, json) {
|
|
|
113
116
|
description,
|
|
114
117
|
enabled: true,
|
|
115
118
|
});
|
|
116
|
-
print(json, io.stdout, job, [
|
|
117
|
-
`Created scheduler job ${job.id}`,
|
|
119
|
+
print(json, io.stdout, presentJob(job), [
|
|
120
|
+
`Created scheduler job ${(0, job_id_js_1.toPublicScheduledJobId)(job.id)}`,
|
|
118
121
|
`Schedule: ${job.cronExpr || ''}`,
|
|
119
122
|
`Execution mode: ${job.executionMode}`,
|
|
120
123
|
`Description: ${job.description}`,
|
|
@@ -132,7 +135,7 @@ async function handleList(flags, env, io, json) {
|
|
|
132
135
|
const jobs = threadId
|
|
133
136
|
? response.jobs.filter((job) => job.route.threadId === threadId)
|
|
134
137
|
: response.jobs;
|
|
135
|
-
print(json, io.stdout, { jobs }, jobs.length === 0 ? 'No scheduler jobs.' : jobs.map(formatJobLine).join('\n'));
|
|
138
|
+
print(json, io.stdout, { jobs: jobs.map(presentJob) }, jobs.length === 0 ? 'No scheduler jobs.' : jobs.map(formatJobLine).join('\n'));
|
|
136
139
|
return 0;
|
|
137
140
|
}
|
|
138
141
|
async function handleInfo(jobId, flags, env, io, json) {
|
|
@@ -142,7 +145,7 @@ async function handleInfo(jobId, flags, env, io, json) {
|
|
|
142
145
|
const context = resolveContext(flags, env);
|
|
143
146
|
const job = await request(context.baseUrl, 'GET', `/scheduler/jobs/${encodeURIComponent(jobId)}`);
|
|
144
147
|
const runs = await request(context.baseUrl, 'GET', `/scheduler/jobs/${encodeURIComponent(jobId)}/runs`);
|
|
145
|
-
print(json, io.stdout, { job, runs: runs.runs }, formatJobDetails(job, runs.runs[0]));
|
|
148
|
+
print(json, io.stdout, { job: presentJob(job), runs: runs.runs }, formatJobDetails(job, runs.runs[0]));
|
|
146
149
|
return 0;
|
|
147
150
|
}
|
|
148
151
|
async function handleEdit(jobId, flags, env, io, json) {
|
|
@@ -168,14 +171,14 @@ async function handleEdit(jobId, flags, env, io, json) {
|
|
|
168
171
|
input.enabled = enabled;
|
|
169
172
|
}
|
|
170
173
|
if (executionMode) {
|
|
171
|
-
input.executionMode =
|
|
174
|
+
input.executionMode = (0, index_js_1.normalizeScheduledJobExecutionMode)(executionMode);
|
|
172
175
|
}
|
|
173
176
|
if (Object.keys(input).length === 0) {
|
|
174
177
|
throw new Error('scheduler edit requires at least one editable field.');
|
|
175
178
|
}
|
|
176
179
|
const context = resolveContext(flags, env);
|
|
177
180
|
const job = await request(context.baseUrl, 'PATCH', `/scheduler/jobs/${encodeURIComponent(jobId)}`, input);
|
|
178
|
-
print(json, io.stdout, job, `Updated scheduler job ${job.id}`);
|
|
181
|
+
print(json, io.stdout, presentJob(job), `Updated scheduler job ${(0, job_id_js_1.toPublicScheduledJobId)(job.id)}`);
|
|
179
182
|
return 0;
|
|
180
183
|
}
|
|
181
184
|
async function handleDelete(jobId, flags, env, io, json) {
|
|
@@ -284,13 +287,7 @@ function normalizeMaybeBooleanFlag(value) {
|
|
|
284
287
|
return value === 'true' ? '' : value;
|
|
285
288
|
}
|
|
286
289
|
function getExecutionMode(flags) {
|
|
287
|
-
return
|
|
288
|
-
}
|
|
289
|
-
function parseExecutionMode(value) {
|
|
290
|
-
if (value === 'same-thread' || value === 'side-thread') {
|
|
291
|
-
return value;
|
|
292
|
-
}
|
|
293
|
-
throw new Error('Flag --execution-mode must be same-thread or side-thread');
|
|
290
|
+
return (0, index_js_1.normalizeScheduledJobExecutionMode)(getFlag(flags, 'execution-mode'));
|
|
294
291
|
}
|
|
295
292
|
function print(asJson, output, payload, text) {
|
|
296
293
|
output.write(asJson ? `${JSON.stringify(payload, null, 2)}\n` : `${text}\n`);
|
|
@@ -309,11 +306,11 @@ function printUsage(output) {
|
|
|
309
306
|
}
|
|
310
307
|
function formatJobLine(job) {
|
|
311
308
|
const schedule = job.triggerType === 'cron' ? job.cronExpr || '' : job.runAt || '';
|
|
312
|
-
return `${job.id} | ${job.enabled ? 'enabled' : 'disabled'} | ${job.executionMode} | ${schedule} | ${job.description}`;
|
|
309
|
+
return `${(0, job_id_js_1.toPublicScheduledJobId)(job.id)} | ${job.enabled ? 'enabled' : 'disabled'} | ${job.executionMode} | ${schedule} | ${job.description}`;
|
|
313
310
|
}
|
|
314
311
|
function formatJobDetails(job, latestRun) {
|
|
315
312
|
return [
|
|
316
|
-
`Job: ${job.id}`,
|
|
313
|
+
`Job: ${(0, job_id_js_1.toPublicScheduledJobId)(job.id)}`,
|
|
317
314
|
`Workspace: ${job.workspaceId}`,
|
|
318
315
|
`Platform: ${job.platform}`,
|
|
319
316
|
`Thread: ${job.route.threadId || ''}`,
|
|
@@ -325,6 +322,12 @@ function formatJobDetails(job, latestRun) {
|
|
|
325
322
|
latestRun ? `Latest run: ${latestRun.status} @ ${latestRun.triggeredAt}` : 'Latest run: none',
|
|
326
323
|
].join('\n');
|
|
327
324
|
}
|
|
325
|
+
function presentJob(job) {
|
|
326
|
+
return {
|
|
327
|
+
...job,
|
|
328
|
+
id: (0, job_id_js_1.toPublicScheduledJobId)(job.id),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
328
331
|
function formatError(error) {
|
|
329
332
|
return error instanceof Error ? error.message : String(error);
|
|
330
333
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createScheduledJobId = createScheduledJobId;
|
|
4
|
+
exports.toPublicScheduledJobId = toPublicScheduledJobId;
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
function createScheduledJobId() {
|
|
7
|
+
return (0, node_crypto_1.randomUUID)().split('-')[0] || (0, node_crypto_1.randomUUID)();
|
|
8
|
+
}
|
|
9
|
+
function toPublicScheduledJobId(jobId) {
|
|
10
|
+
const normalized = jobId.startsWith('job:') ? jobId.slice(4) : jobId;
|
|
11
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(normalized)) {
|
|
12
|
+
return normalized.split('-')[0] || normalized;
|
|
13
|
+
}
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SchedulerService = void 0;
|
|
4
4
|
const node_events_1 = require("node:events");
|
|
5
|
+
const job_id_js_1 = require("./job-id.js");
|
|
5
6
|
const scheduler_run_lifecycle_js_1 = require("./scheduler-run-lifecycle.js");
|
|
6
7
|
class SchedulerService extends node_events_1.EventEmitter {
|
|
7
8
|
options;
|
|
@@ -34,7 +35,8 @@ class SchedulerService extends node_events_1.EventEmitter {
|
|
|
34
35
|
return this.options.store.listScheduledJobs(workspaceId);
|
|
35
36
|
}
|
|
36
37
|
getJob(jobId) {
|
|
37
|
-
|
|
38
|
+
const resolvedJobId = this.resolveJobId(jobId);
|
|
39
|
+
return resolvedJobId ? this.options.store.getScheduledJob(resolvedJobId) : undefined;
|
|
38
40
|
}
|
|
39
41
|
createJob(input) {
|
|
40
42
|
const job = this.options.store.createScheduledJob(input);
|
|
@@ -43,24 +45,47 @@ class SchedulerService extends node_events_1.EventEmitter {
|
|
|
43
45
|
return job;
|
|
44
46
|
}
|
|
45
47
|
updateJob(jobId, input) {
|
|
46
|
-
const job = this.options.store.updateScheduledJob(jobId, input);
|
|
48
|
+
const job = this.options.store.updateScheduledJob(this.resolveRequiredJobId(jobId), input);
|
|
47
49
|
this.emit('job', job);
|
|
48
50
|
this.options.eventBus.emit({ type: 'scheduler.job.updated', payload: job });
|
|
49
51
|
return job;
|
|
50
52
|
}
|
|
51
53
|
deleteJob(jobId) {
|
|
52
|
-
return this.options.store.deleteScheduledJob(jobId);
|
|
54
|
+
return this.options.store.deleteScheduledJob(this.resolveRequiredJobId(jobId));
|
|
53
55
|
}
|
|
54
56
|
listJobRuns(jobId) {
|
|
55
|
-
return this.options.store.listScheduledJobRuns(jobId);
|
|
57
|
+
return this.options.store.listScheduledJobRuns(this.resolveRequiredJobId(jobId));
|
|
56
58
|
}
|
|
57
59
|
async runJobNow(jobId) {
|
|
58
|
-
const
|
|
60
|
+
const resolvedJobId = this.resolveRequiredJobId(jobId);
|
|
61
|
+
const job = this.options.store.getScheduledJob(resolvedJobId);
|
|
59
62
|
if (!job) {
|
|
60
63
|
throw new Error(`Scheduled job not found: ${jobId}`);
|
|
61
64
|
}
|
|
62
65
|
return this.executeJob(job, new Date().toISOString(), true);
|
|
63
66
|
}
|
|
67
|
+
resolveJobId(jobId) {
|
|
68
|
+
if (this.options.store.getScheduledJob(jobId)) {
|
|
69
|
+
return jobId;
|
|
70
|
+
}
|
|
71
|
+
const matches = this.options.store
|
|
72
|
+
.listScheduledJobs()
|
|
73
|
+
.filter((job) => (0, job_id_js_1.toPublicScheduledJobId)(job.id) === jobId);
|
|
74
|
+
if (matches.length === 0) {
|
|
75
|
+
return '';
|
|
76
|
+
}
|
|
77
|
+
if (matches.length > 1) {
|
|
78
|
+
throw new Error(`Scheduled job id is ambiguous: ${jobId}`);
|
|
79
|
+
}
|
|
80
|
+
return matches[0].id;
|
|
81
|
+
}
|
|
82
|
+
resolveRequiredJobId(jobId) {
|
|
83
|
+
const resolvedJobId = this.resolveJobId(jobId);
|
|
84
|
+
if (!resolvedJobId) {
|
|
85
|
+
throw new Error(`Scheduled job not found: ${jobId}`);
|
|
86
|
+
}
|
|
87
|
+
return resolvedJobId;
|
|
88
|
+
}
|
|
64
89
|
async tick() {
|
|
65
90
|
if (this.pollInFlight) {
|
|
66
91
|
return;
|
|
@@ -62,13 +62,14 @@ exports.SCHEDULER_PROTOCOL_INSTRUCTION = [
|
|
|
62
62
|
'lac scheduler add --cron "<5-field cron>" --message "<exact message to send>" --desc "<short label>" [--execution-mode same-thread|side-thread]',
|
|
63
63
|
'lac scheduler list',
|
|
64
64
|
'lac scheduler list --thread',
|
|
65
|
-
'lac scheduler info <job-id>',
|
|
66
|
-
'lac scheduler edit <job-id> [--cron "<5-field cron>"] [--message "<exact message>"] [--desc "<short label>"] [--enabled true|false] [--execution-mode same-thread|side-thread]',
|
|
67
|
-
'lac scheduler del <job-id>',
|
|
68
|
-
'lac scheduler run <job-id>',
|
|
65
|
+
'lac scheduler info <short-job-id>',
|
|
66
|
+
'lac scheduler edit <short-job-id> [--cron "<5-field cron>"] [--message "<exact message>"] [--desc "<short label>"] [--enabled true|false] [--execution-mode same-thread|side-thread]',
|
|
67
|
+
'lac scheduler del <short-job-id>',
|
|
68
|
+
'lac scheduler run <short-job-id>',
|
|
69
69
|
'Environment variables LOCAL_AI_WORKSPACE_ID, LOCAL_AI_THREAD_ID, LOCAL_AI_PLATFORM, LOCAL_AI_CHAT_ID, and LOCAL_AI_PLATFORM_USER_ID are already set when available.',
|
|
70
70
|
'Prefer relying on those variables instead of inventing your own route or creating session-only cron jobs.',
|
|
71
71
|
'By default, `lac scheduler list` shows all scheduled tasks in the current workspace. Use `lac scheduler list --thread` to show only the current conversation thread.',
|
|
72
|
+
'Use the short job id shown by `lac scheduler list`; do not add a `job:` prefix or expand it to a full UUID.',
|
|
72
73
|
'Use `--execution-mode same-thread` to reuse the current thread, or `--execution-mode side-thread` to run in a dedicated scheduled thread.',
|
|
73
74
|
'Only use the scheduler CLI when the user explicitly asks for scheduled automation.',
|
|
74
75
|
'[/Scheduler Tools]',
|
|
@@ -169,7 +170,28 @@ function normalizePermissionResponse(input) {
|
|
|
169
170
|
if (!input) {
|
|
170
171
|
return null;
|
|
171
172
|
}
|
|
172
|
-
|
|
173
|
+
const normalized = String(input).trim().toLowerCase();
|
|
174
|
+
const mapped = PERMISSION_RESPONSE_MAP[normalized];
|
|
175
|
+
if (mapped) {
|
|
176
|
+
return mapped;
|
|
177
|
+
}
|
|
178
|
+
if (normalized.includes('allow_all') ||
|
|
179
|
+
normalized.includes('allow-always') ||
|
|
180
|
+
normalized.includes('allow_always') ||
|
|
181
|
+
normalized.includes('allow always') ||
|
|
182
|
+
normalized.includes('always allow') ||
|
|
183
|
+
normalized.includes('allow all') ||
|
|
184
|
+
normalized.includes('始终允许') ||
|
|
185
|
+
normalized.includes('永久允许')) {
|
|
186
|
+
return 'allow all';
|
|
187
|
+
}
|
|
188
|
+
if (normalized.startsWith('reject') || normalized.startsWith('deny')) {
|
|
189
|
+
return 'deny';
|
|
190
|
+
}
|
|
191
|
+
if (normalized.startsWith('allow')) {
|
|
192
|
+
return 'allow';
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
173
195
|
}
|
|
174
196
|
function isPermissionButtonOption(option) {
|
|
175
197
|
return Boolean(normalizePermissionResponse(option?.data));
|
|
@@ -5,6 +5,9 @@ exports.isInternalProgressMessage = isInternalProgressMessage;
|
|
|
5
5
|
exports.isAwaitingInputMessage = isAwaitingInputMessage;
|
|
6
6
|
exports.findStreamingPreviewMessage = findStreamingPreviewMessage;
|
|
7
7
|
exports.shouldReplacePreviewWithReply = shouldReplacePreviewWithReply;
|
|
8
|
+
exports.advancePreviewContent = advancePreviewContent;
|
|
9
|
+
exports.settlePreviewMessages = settlePreviewMessages;
|
|
10
|
+
exports.finalizeTurnMessageKinds = finalizeTurnMessageKinds;
|
|
8
11
|
exports.extractVisibleMessageContent = extractVisibleMessageContent;
|
|
9
12
|
exports.sessionMatchesDesktop = sessionMatchesDesktop;
|
|
10
13
|
exports.toMessages = toMessages;
|
|
@@ -65,6 +68,60 @@ function shouldReplacePreviewWithReply(message, replyContent, replyCtx) {
|
|
|
65
68
|
const content = message.streamTargetContent ?? message.content;
|
|
66
69
|
return Boolean(replyContent && content === replyContent && !isInternalProgressMessage(content));
|
|
67
70
|
}
|
|
71
|
+
function advancePreviewContent(content, target) {
|
|
72
|
+
if (!target) {
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
if (!content || !target.startsWith(content)) {
|
|
76
|
+
return target.slice(0, Math.min(target.length, Math.max(1, Math.ceil(target.length / 24))));
|
|
77
|
+
}
|
|
78
|
+
if (content === target) {
|
|
79
|
+
return target;
|
|
80
|
+
}
|
|
81
|
+
const remaining = target.length - content.length;
|
|
82
|
+
const step = remaining > 160 ? 14 : remaining > 80 ? 8 : remaining > 24 ? 4 : 2;
|
|
83
|
+
return target.slice(0, Math.min(target.length, content.length + step));
|
|
84
|
+
}
|
|
85
|
+
function settlePreviewMessages(messages, turnKey) {
|
|
86
|
+
let changed = false;
|
|
87
|
+
const next = messages.map((message) => {
|
|
88
|
+
if (!message.preview) {
|
|
89
|
+
return message;
|
|
90
|
+
}
|
|
91
|
+
if (turnKey && message.turnKey !== turnKey) {
|
|
92
|
+
return message;
|
|
93
|
+
}
|
|
94
|
+
changed = true;
|
|
95
|
+
return {
|
|
96
|
+
...message,
|
|
97
|
+
content: message.streamTargetContent ?? message.content,
|
|
98
|
+
streamTargetContent: undefined,
|
|
99
|
+
preview: false,
|
|
100
|
+
previewPlainText: false,
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
return changed ? next : messages;
|
|
104
|
+
}
|
|
105
|
+
function finalizeTurnMessageKinds(messages, turnKey) {
|
|
106
|
+
if (!turnKey) {
|
|
107
|
+
return messages;
|
|
108
|
+
}
|
|
109
|
+
const candidates = messages.filter((message) => message.turnKey === turnKey && !message.preview);
|
|
110
|
+
if (candidates.length === 0) {
|
|
111
|
+
return messages;
|
|
112
|
+
}
|
|
113
|
+
const finalMessage = [...candidates].reverse().find((message) => !isInternalProgressMessage(message.content));
|
|
114
|
+
const finalId = finalMessage?.id;
|
|
115
|
+
return messages.map((message) => {
|
|
116
|
+
if (message.turnKey !== turnKey || message.preview) {
|
|
117
|
+
return message;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
...message,
|
|
121
|
+
kind: finalId && message.id === finalId ? 'final' : 'progress',
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}
|
|
68
125
|
function extractVisibleMessageContent(content) {
|
|
69
126
|
if (!content) {
|
|
70
127
|
return '';
|
|
@@ -157,3 +157,73 @@ function createMessage(overrides) {
|
|
|
157
157
|
strict_1.default.equal((0, thread_chat_model_1.shouldReplacePreviewWithReply)(thoughtPreview, 'Hi! How can I help you today?', 'run-1'), false);
|
|
158
158
|
strict_1.default.equal((0, thread_chat_model_1.shouldReplacePreviewWithReply)(answerPreview, 'Hi! How can I help you today?', 'run-1'), true);
|
|
159
159
|
});
|
|
160
|
+
(0, node_test_1.default)('finalizeTurnMessageKinds marks only the last non-progress turn message as final', () => {
|
|
161
|
+
const messages = [
|
|
162
|
+
{
|
|
163
|
+
id: 'thought',
|
|
164
|
+
role: 'assistant',
|
|
165
|
+
content: '💭 checking',
|
|
166
|
+
kind: 'final',
|
|
167
|
+
order: 1,
|
|
168
|
+
turnKey: 'run-1',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: 'tool',
|
|
172
|
+
role: 'assistant',
|
|
173
|
+
content: '🔧 Read file',
|
|
174
|
+
kind: 'final',
|
|
175
|
+
order: 2,
|
|
176
|
+
turnKey: 'run-1',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'answer',
|
|
180
|
+
role: 'assistant',
|
|
181
|
+
content: 'Done',
|
|
182
|
+
kind: 'progress',
|
|
183
|
+
order: 3,
|
|
184
|
+
turnKey: 'run-1',
|
|
185
|
+
},
|
|
186
|
+
];
|
|
187
|
+
const finalized = (0, thread_chat_model_1.finalizeTurnMessageKinds)(messages, 'run-1');
|
|
188
|
+
strict_1.default.deepEqual(finalized.map((message) => [message.id, message.kind]), [
|
|
189
|
+
['thought', 'progress'],
|
|
190
|
+
['tool', 'progress'],
|
|
191
|
+
['answer', 'final'],
|
|
192
|
+
]);
|
|
193
|
+
});
|
|
194
|
+
(0, node_test_1.default)('settlePreviewMessages settles only previews for the requested turn', () => {
|
|
195
|
+
const messages = [
|
|
196
|
+
{
|
|
197
|
+
id: 'preview-1',
|
|
198
|
+
role: 'assistant',
|
|
199
|
+
content: 'Hel',
|
|
200
|
+
streamTargetContent: 'Hello',
|
|
201
|
+
kind: 'progress',
|
|
202
|
+
order: 1,
|
|
203
|
+
turnKey: 'run-1',
|
|
204
|
+
preview: true,
|
|
205
|
+
previewPlainText: true,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: 'preview-2',
|
|
209
|
+
role: 'assistant',
|
|
210
|
+
content: 'Wor',
|
|
211
|
+
streamTargetContent: 'World',
|
|
212
|
+
kind: 'progress',
|
|
213
|
+
order: 2,
|
|
214
|
+
turnKey: 'run-2',
|
|
215
|
+
preview: true,
|
|
216
|
+
previewPlainText: true,
|
|
217
|
+
},
|
|
218
|
+
];
|
|
219
|
+
const settled = (0, thread_chat_model_1.settlePreviewMessages)(messages, 'run-1');
|
|
220
|
+
strict_1.default.equal(settled[0]?.content, 'Hello');
|
|
221
|
+
strict_1.default.equal(settled[0]?.preview, false);
|
|
222
|
+
strict_1.default.equal(settled[1]?.content, 'Wor');
|
|
223
|
+
strict_1.default.equal(settled[1]?.preview, true);
|
|
224
|
+
});
|
|
225
|
+
(0, node_test_1.default)('advancePreviewContent moves monotonically toward the target content', () => {
|
|
226
|
+
strict_1.default.equal((0, thread_chat_model_1.advancePreviewContent)('', 'Hello'), 'H');
|
|
227
|
+
strict_1.default.equal((0, thread_chat_model_1.advancePreviewContent)('Hel', 'Hello'), 'Hello');
|
|
228
|
+
strict_1.default.equal((0, thread_chat_model_1.advancePreviewContent)('Mismatch', 'Hello'), 'H');
|
|
229
|
+
});
|