@workflow-code/cli 0.2.4-20261011001.codex-cli-web → 0.2.4-20261011003.codex-cli-web
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/dist/index.js +1 -1
- package/dist/web/worker.mjs +13 -5
- package/dist/web/workflow-ui.js +3 -0
- package/dist/web/workflow-workspace.css +2379 -0
- package/dist/web/workflow-workspace.js +8692 -0
- package/dist/{web-TJ2SME4G.js → web-O6FZ5URK.js} +963 -172
- package/package.json +5 -4
|
@@ -17,15 +17,14 @@ import "./chunk-NSPRIPOP.js";
|
|
|
17
17
|
|
|
18
18
|
// src/web.ts
|
|
19
19
|
import { spawn as spawn3 } from "child_process";
|
|
20
|
-
import
|
|
20
|
+
import path5 from "path";
|
|
21
21
|
|
|
22
22
|
// src/web/server.ts
|
|
23
23
|
import { createServer } from "http";
|
|
24
24
|
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
25
|
-
import { mkdir, readFile, rename, writeFile, realpath } from "fs/promises";
|
|
26
|
-
import
|
|
25
|
+
import { mkdir, readFile as readFile2, rename, writeFile, realpath as realpath2 } from "fs/promises";
|
|
26
|
+
import path3 from "path";
|
|
27
27
|
import os from "os";
|
|
28
|
-
import { pathToFileURL } from "url";
|
|
29
28
|
import { spawn as spawn2 } from "child_process";
|
|
30
29
|
|
|
31
30
|
// src/web/workflow-types.ts
|
|
@@ -668,28 +667,818 @@ var ConversationController = class {
|
|
|
668
667
|
}
|
|
669
668
|
};
|
|
670
669
|
|
|
670
|
+
// src/web/workflow-workspace.ts
|
|
671
|
+
function parseWorkflowArgs(text) {
|
|
672
|
+
const value = JSON.parse(text || "[]");
|
|
673
|
+
if (!Array.isArray(value) || !value.every((arg) => typeof arg === "string")) {
|
|
674
|
+
throw new Error("Run arguments must be a JSON string array.");
|
|
675
|
+
}
|
|
676
|
+
return value;
|
|
677
|
+
}
|
|
678
|
+
function migrateWorkflowHistory(sessions, activeId) {
|
|
679
|
+
const workspace = { version: 1, runs: [], drafts: {}, migratedConversations: true };
|
|
680
|
+
for (const session of sessions) {
|
|
681
|
+
let user;
|
|
682
|
+
for (const [index, message] of session.messages.entries()) {
|
|
683
|
+
if (message.role === "user") {
|
|
684
|
+
user = message;
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
const report = message.report;
|
|
688
|
+
const createdAt = message.createdAt ?? session.createdAt ?? 0;
|
|
689
|
+
workspace.runs.push({
|
|
690
|
+
id: message.id ?? `legacy-${session.id}-${index}`,
|
|
691
|
+
scopeId: session.id,
|
|
692
|
+
title: session.title,
|
|
693
|
+
entrypointId: message.entrypointId ?? user?.entrypointId,
|
|
694
|
+
createdAt,
|
|
695
|
+
updatedAt: session.updatedAt ?? createdAt,
|
|
696
|
+
status: message.status ?? report?.status ?? "success",
|
|
697
|
+
args: message.args ?? user?.args ?? [],
|
|
698
|
+
params: user?.input?.params ?? {},
|
|
699
|
+
policies: session.toolPermissionPolicies ?? {},
|
|
700
|
+
items: report?.output?.items ?? message.items ?? (message.content ? [{ content: message.content, contentType: "markdown" }] : []),
|
|
701
|
+
events: message.events ?? [],
|
|
702
|
+
nodes: report?.nodes ?? [],
|
|
703
|
+
logs: report?.logs ?? "",
|
|
704
|
+
files: [...user?.attachments ?? [], ...message.files ?? [], ...report?.files ?? []],
|
|
705
|
+
report,
|
|
706
|
+
pendingInput: message.pendingInput ?? report?.pendingUserInput,
|
|
707
|
+
resolvedInputs: message.resolvedInputs ?? report?.resolvedUserInputs ?? [],
|
|
708
|
+
error: message.error,
|
|
709
|
+
durationMs: message.durationMs,
|
|
710
|
+
tokenUsage: message.tokenUsage,
|
|
711
|
+
timers: message.timers,
|
|
712
|
+
migratedFrom: { sessionId: session.id, messageId: message.id }
|
|
713
|
+
});
|
|
714
|
+
if (session.id === activeId) {
|
|
715
|
+
workspace.selectedId = workspace.runs.at(-1).id;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
for (const [entrypoint, values] of Object.entries(session.parameterValues ?? {})) {
|
|
719
|
+
workspace.drafts[entrypoint] = { scopeId: session.id, values, rawArgs: "[]", inputMode: "form", policies: session.toolPermissionPolicies ?? {} };
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
workspace.runs.sort((a, b) => b.createdAt - a.createdAt);
|
|
723
|
+
return workspace;
|
|
724
|
+
}
|
|
725
|
+
function workflowStorageReferences(value) {
|
|
726
|
+
const files = /* @__PURE__ */ new Set(), scopes = /* @__PURE__ */ new Set();
|
|
727
|
+
const visit = (item) => {
|
|
728
|
+
if (typeof item === "string") {
|
|
729
|
+
for (const id of item.match(/wf_[a-f0-9]{32}/g) ?? []) {
|
|
730
|
+
files.add(id);
|
|
731
|
+
}
|
|
732
|
+
if (/^[\[{]/.test(item.trim())) {
|
|
733
|
+
try {
|
|
734
|
+
visit(JSON.parse(item));
|
|
735
|
+
} catch {
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
} else if (Array.isArray(item)) {
|
|
739
|
+
item.forEach(visit);
|
|
740
|
+
} else if (item && typeof item === "object") {
|
|
741
|
+
const record2 = item;
|
|
742
|
+
if (typeof record2.scopeId === "string") {
|
|
743
|
+
scopes.add(record2.scopeId);
|
|
744
|
+
}
|
|
745
|
+
if (typeof record2.conversationId === "string") {
|
|
746
|
+
scopes.add(record2.conversationId);
|
|
747
|
+
}
|
|
748
|
+
Object.values(record2).forEach(visit);
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
visit(value);
|
|
752
|
+
return { files, scopes };
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/web/workflow-controller.ts
|
|
756
|
+
var WorkflowController = class {
|
|
757
|
+
constructor(project, runtime, saved, options) {
|
|
758
|
+
this.project = project;
|
|
759
|
+
this.runtime = runtime;
|
|
760
|
+
this.options = options;
|
|
761
|
+
this.workspace = { version: 1, runs: [], drafts: {}, ...saved };
|
|
762
|
+
for (const record2 of this.workspace.runs) {
|
|
763
|
+
delete record2.submittingInput;
|
|
764
|
+
if (record2.status === "running" || record2.status === "waiting_for_input") {
|
|
765
|
+
record2.status = "interrupted";
|
|
766
|
+
record2.error = "\u8FD0\u884C\u5DF2\u4E2D\u65AD";
|
|
767
|
+
this.clearTimers(record2);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
this.workspace.occupiedId = this.workspace.runs.find((record2) => record2.id === saved.occupiedId && record2.pendingInput)?.id ?? this.workspace.runs.find((record2) => !!record2.pendingInput)?.id;
|
|
771
|
+
if (!this.workspace.runs.some((record2) => record2.id === this.workspace.selectedId)) {
|
|
772
|
+
delete this.workspace.selectedId;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
project;
|
|
776
|
+
runtime;
|
|
777
|
+
options;
|
|
778
|
+
workspace;
|
|
779
|
+
task;
|
|
780
|
+
disposed = false;
|
|
781
|
+
eventSave;
|
|
782
|
+
get runs() {
|
|
783
|
+
return this.workspace.runs;
|
|
784
|
+
}
|
|
785
|
+
get current() {
|
|
786
|
+
return this.runs.find((record2) => record2.id === this.workspace.selectedId);
|
|
787
|
+
}
|
|
788
|
+
get running() {
|
|
789
|
+
return !!this.workspace.occupiedId;
|
|
790
|
+
}
|
|
791
|
+
get occupied() {
|
|
792
|
+
return this.runs.find((record2) => record2.id === this.workspace.occupiedId);
|
|
793
|
+
}
|
|
794
|
+
draft(entrypointId) {
|
|
795
|
+
return this.workspace.drafts[entrypointId ?? ""] ??= { scopeId: globalThis.crypto.randomUUID(), values: {}, rawArgs: "[]", inputMode: "form", policies: {} };
|
|
796
|
+
}
|
|
797
|
+
async persist() {
|
|
798
|
+
await this.options.save(JSON.parse(JSON.stringify({ ...this.workspace, runs: this.runs.map(({ submittingInput: _, ...record2 }) => record2) })));
|
|
799
|
+
}
|
|
800
|
+
changed() {
|
|
801
|
+
if (!this.disposed) {
|
|
802
|
+
this.options.changed();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
scheduleSave() {
|
|
806
|
+
if (this.eventSave) {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
this.eventSave = setTimeout(() => {
|
|
810
|
+
this.eventSave = void 0;
|
|
811
|
+
void this.persist().catch((error) => this.options.log(String(error)));
|
|
812
|
+
}, 100);
|
|
813
|
+
}
|
|
814
|
+
async select(id) {
|
|
815
|
+
if (id && !this.runs.some((record2) => record2.id === id)) {
|
|
816
|
+
throw new Error("Run no longer exists.");
|
|
817
|
+
}
|
|
818
|
+
this.workspace.selectedId = id;
|
|
819
|
+
this.changed();
|
|
820
|
+
await this.persist();
|
|
821
|
+
}
|
|
822
|
+
async updateDraft(entrypointId, draft) {
|
|
823
|
+
Object.assign(this.draft(entrypointId), draft);
|
|
824
|
+
this.changed();
|
|
825
|
+
await this.persist();
|
|
826
|
+
}
|
|
827
|
+
assertAvailable() {
|
|
828
|
+
if (this.disposed) {
|
|
829
|
+
throw new Error("Workflow workspace has closed.");
|
|
830
|
+
}
|
|
831
|
+
if (this.running || this.task) {
|
|
832
|
+
throw new Error("A workflow is already running or waiting for input.");
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
async start(input) {
|
|
836
|
+
this.assertAvailable();
|
|
837
|
+
const parameters = workflowParameters(this.project, input.entrypointId);
|
|
838
|
+
const args = input.inputMode === "raw" ? parseWorkflowArgs(input.rawArgs ?? "[]") : (validateWorkflowValues(parameters.fields, input.params, input.states), workflowParamArgs(parameters.fields, input.params, input.states));
|
|
839
|
+
const files = await this.inputFiles([input.params, args]);
|
|
840
|
+
this.assertAvailable();
|
|
841
|
+
const draft = this.draft(input.entrypointId);
|
|
842
|
+
const id = globalThis.crypto.randomUUID(), now = Date.now();
|
|
843
|
+
const record2 = {
|
|
844
|
+
id,
|
|
845
|
+
scopeId: id,
|
|
846
|
+
entrypointId: input.entrypointId,
|
|
847
|
+
title: this.project.entrypoints.find((entry) => entry.id === input.entrypointId)?.title ?? this.project.name,
|
|
848
|
+
createdAt: now,
|
|
849
|
+
updatedAt: now,
|
|
850
|
+
status: "running",
|
|
851
|
+
args: [...args],
|
|
852
|
+
params: { ...input.params },
|
|
853
|
+
policies: structuredClone(draft.policies),
|
|
854
|
+
items: [],
|
|
855
|
+
events: [],
|
|
856
|
+
files,
|
|
857
|
+
nodes: [],
|
|
858
|
+
logs: "",
|
|
859
|
+
resolvedInputs: []
|
|
860
|
+
};
|
|
861
|
+
this.runs.unshift(record2);
|
|
862
|
+
this.workspace.selectedId = id;
|
|
863
|
+
draft.values = { ...input.params };
|
|
864
|
+
draft.rawArgs = input.rawArgs ?? draft.rawArgs;
|
|
865
|
+
draft.inputMode = input.inputMode ?? "form";
|
|
866
|
+
this.launch(record2);
|
|
867
|
+
await this.persist();
|
|
868
|
+
return record2;
|
|
869
|
+
}
|
|
870
|
+
async rerun(previousId) {
|
|
871
|
+
this.assertAvailable();
|
|
872
|
+
const previous = this.runs.find((record3) => record3.id === previousId);
|
|
873
|
+
if (!previous) {
|
|
874
|
+
throw new Error("Run no longer exists.");
|
|
875
|
+
}
|
|
876
|
+
const files = await this.inputFiles([previous.params, previous.args]);
|
|
877
|
+
this.assertAvailable();
|
|
878
|
+
const id = globalThis.crypto.randomUUID(), now = Date.now();
|
|
879
|
+
const record2 = {
|
|
880
|
+
id,
|
|
881
|
+
scopeId: id,
|
|
882
|
+
title: previous.title,
|
|
883
|
+
entrypointId: previous.entrypointId,
|
|
884
|
+
createdAt: now,
|
|
885
|
+
updatedAt: now,
|
|
886
|
+
status: "running",
|
|
887
|
+
args: [...previous.args],
|
|
888
|
+
params: structuredClone(previous.params),
|
|
889
|
+
policies: structuredClone(previous.policies),
|
|
890
|
+
items: [],
|
|
891
|
+
events: [],
|
|
892
|
+
files,
|
|
893
|
+
nodes: [],
|
|
894
|
+
logs: "",
|
|
895
|
+
resolvedInputs: []
|
|
896
|
+
};
|
|
897
|
+
this.runs.unshift(record2);
|
|
898
|
+
this.workspace.selectedId = id;
|
|
899
|
+
this.launch(record2);
|
|
900
|
+
await this.persist();
|
|
901
|
+
return record2;
|
|
902
|
+
}
|
|
903
|
+
async inputFiles(value) {
|
|
904
|
+
return Promise.all([...workflowStorageReferences(value).files].map(async (id) => (await this.runtime.readFile(this.project, id)).reference));
|
|
905
|
+
}
|
|
906
|
+
launch(record2) {
|
|
907
|
+
record2.status = "running";
|
|
908
|
+
record2.error = void 0;
|
|
909
|
+
record2.updatedAt = Date.now();
|
|
910
|
+
this.workspace.occupiedId = record2.id;
|
|
911
|
+
const task = { record: record2, cancelled: false, finished: Promise.resolve() };
|
|
912
|
+
this.task = task;
|
|
913
|
+
this.changed();
|
|
914
|
+
task.finished = this.execute(task).catch((error) => this.options.log(`Workflow persistence: ${String(error)}`));
|
|
915
|
+
}
|
|
916
|
+
async execute(task) {
|
|
917
|
+
const record2 = task.record, pending = record2.pendingInput;
|
|
918
|
+
try {
|
|
919
|
+
await this.persist();
|
|
920
|
+
if (task.cancelled || this.disposed) {
|
|
921
|
+
throw new Error("Stopped.");
|
|
922
|
+
}
|
|
923
|
+
const report = await this.runtime.run({
|
|
924
|
+
project: this.project,
|
|
925
|
+
conversationId: record2.scopeId,
|
|
926
|
+
args: record2.args,
|
|
927
|
+
entrypointId: record2.entrypointId,
|
|
928
|
+
runId: record2.id,
|
|
929
|
+
toolPermissionPolicies: record2.policies,
|
|
930
|
+
resolvedInputs: [...record2.resolvedInputs, ...record2.submittingInput ? [record2.submittingInput] : []],
|
|
931
|
+
resumeAfterRequestId: record2.submittingInput?.requestId
|
|
932
|
+
}, (event) => {
|
|
933
|
+
if (task.cancelled || this.disposed || this.task !== task) {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
record2.events.push(event);
|
|
937
|
+
if (event.type === "node_chunk") {
|
|
938
|
+
applyOutputChunk(record2.items, event.chunk);
|
|
939
|
+
}
|
|
940
|
+
if (event.type === "node_started" || event.type === "node_finished") {
|
|
941
|
+
const index = record2.nodes.findIndex((node2) => node2.executionId === event.executionId);
|
|
942
|
+
const node = { ...index >= 0 ? record2.nodes[index] : {}, ...event, name: event.nodeName, status: event.status ?? "running" };
|
|
943
|
+
if (index >= 0) {
|
|
944
|
+
record2.nodes[index] = node;
|
|
945
|
+
} else {
|
|
946
|
+
record2.nodes.push(node);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (event.type === "runtime_log" && typeof event.text === "string") {
|
|
950
|
+
record2.logs = (record2.logs + event.text).slice(-1e5);
|
|
951
|
+
}
|
|
952
|
+
if (event.type === "user_input_requested") {
|
|
953
|
+
record2.pendingInput = event;
|
|
954
|
+
}
|
|
955
|
+
if (event.type === "user_input_resolved") {
|
|
956
|
+
this.confirmInput(record2, event);
|
|
957
|
+
if (record2.pendingInput?.requestId === event.requestId) {
|
|
958
|
+
delete record2.pendingInput;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (event.type === "token_usage_updated") {
|
|
962
|
+
record2.tokenUsage = { enabled: true, display: workflowParameters(this.project, record2.entrypointId).tokenUsage?.display !== false, run: event.usage };
|
|
963
|
+
}
|
|
964
|
+
if (event.type === "file_created") {
|
|
965
|
+
record2.files.push(event.file);
|
|
966
|
+
}
|
|
967
|
+
if (event.type === "timer_started" || event.type === "timer_finished") {
|
|
968
|
+
const timer = event.timer;
|
|
969
|
+
record2.timers = [...(record2.timers ?? []).filter((entry) => entry.timerId !== timer.timerId), timer];
|
|
970
|
+
}
|
|
971
|
+
record2.updatedAt = Date.now();
|
|
972
|
+
this.changed();
|
|
973
|
+
this.scheduleSave();
|
|
974
|
+
});
|
|
975
|
+
record2.report = report;
|
|
976
|
+
record2.durationMs = report.durationMs;
|
|
977
|
+
record2.status = report.status;
|
|
978
|
+
if (report.output) {
|
|
979
|
+
record2.items = report.output.items ?? [];
|
|
980
|
+
}
|
|
981
|
+
record2.nodes = report.nodes ?? record2.nodes;
|
|
982
|
+
record2.logs = report.logs ?? record2.logs;
|
|
983
|
+
record2.tokenUsage = report.tokenUsage ?? record2.tokenUsage;
|
|
984
|
+
record2.timers = report.timers ?? record2.timers;
|
|
985
|
+
record2.files = [...new Map([...record2.files, ...report.files ?? []].map((file) => [file.id, file])).values()];
|
|
986
|
+
record2.pendingInput = report.status === "waiting_for_input" ? report.pendingUserInput : void 0;
|
|
987
|
+
for (const resolved of report.resolvedUserInputs ?? []) {
|
|
988
|
+
this.confirmInput(record2, resolved);
|
|
989
|
+
}
|
|
990
|
+
if (record2.submittingInput && report.status === "success") {
|
|
991
|
+
this.confirmInput(record2, record2.submittingInput);
|
|
992
|
+
}
|
|
993
|
+
if (report.status === "failed") {
|
|
994
|
+
record2.error = report.result?.error?.message ?? "Workflow failed.";
|
|
995
|
+
}
|
|
996
|
+
if (record2.submittingInput && pending && report.status === "failed") {
|
|
997
|
+
record2.status = "waiting_for_input";
|
|
998
|
+
record2.pendingInput = pending;
|
|
999
|
+
}
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
record2.error = error instanceof Error ? error.message : String(error);
|
|
1002
|
+
record2.status = task.cancelled ? this.disposed ? "interrupted" : "aborted" : record2.submittingInput && pending ? "waiting_for_input" : "failed";
|
|
1003
|
+
if (record2.status === "waiting_for_input") {
|
|
1004
|
+
record2.pendingInput = pending;
|
|
1005
|
+
} else if (this.disposed) {
|
|
1006
|
+
record2.pendingInput ??= pending;
|
|
1007
|
+
} else {
|
|
1008
|
+
delete record2.pendingInput;
|
|
1009
|
+
}
|
|
1010
|
+
} finally {
|
|
1011
|
+
if (task.cancelled) {
|
|
1012
|
+
record2.status = this.disposed ? "interrupted" : "aborted";
|
|
1013
|
+
if (!this.disposed) {
|
|
1014
|
+
delete record2.pendingInput;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
delete record2.submittingInput;
|
|
1018
|
+
if (!record2.pendingInput || this.disposed) {
|
|
1019
|
+
delete this.workspace.occupiedId;
|
|
1020
|
+
}
|
|
1021
|
+
if (record2.status !== "running") {
|
|
1022
|
+
this.clearTimers(record2);
|
|
1023
|
+
}
|
|
1024
|
+
record2.updatedAt = Date.now();
|
|
1025
|
+
this.task = void 0;
|
|
1026
|
+
this.changed();
|
|
1027
|
+
await this.persist();
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
clearTimers(record2) {
|
|
1031
|
+
record2.timers = record2.timers?.map((timer) => timer.status === "running" ? { ...timer, status: "cleared", endedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: Math.max(0, Date.now() - Date.parse(timer.startedAt)) } : timer);
|
|
1032
|
+
}
|
|
1033
|
+
confirmInput(record2, resolved) {
|
|
1034
|
+
const previous = record2.resolvedInputs.find((input) => input.requestId === resolved.requestId) ?? (record2.submittingInput?.requestId === resolved.requestId ? record2.submittingInput : void 0);
|
|
1035
|
+
record2.resolvedInputs = [
|
|
1036
|
+
...record2.resolvedInputs.filter((input) => input.requestId !== resolved.requestId),
|
|
1037
|
+
{ ...previous, ...resolved, precedingOutputItemIds: previous?.precedingOutputItemIds ?? resolved.precedingOutputItemIds }
|
|
1038
|
+
];
|
|
1039
|
+
if (record2.submittingInput?.requestId === resolved.requestId) {
|
|
1040
|
+
delete record2.submittingInput;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
async resume(id, requestId, values) {
|
|
1044
|
+
if (this.task || this.disposed) {
|
|
1045
|
+
throw new Error("Workflow is already running.");
|
|
1046
|
+
}
|
|
1047
|
+
const record2 = this.runs.find((record3) => record3.id === id && record3.pendingInput?.requestId === requestId);
|
|
1048
|
+
if (!record2?.pendingInput) {
|
|
1049
|
+
throw new Error("Input request is no longer available.");
|
|
1050
|
+
}
|
|
1051
|
+
if (this.workspace.occupiedId && this.workspace.occupiedId !== id) {
|
|
1052
|
+
throw new Error("Another workflow is waiting for input.");
|
|
1053
|
+
}
|
|
1054
|
+
const request = record2.pendingInput;
|
|
1055
|
+
const form = Object.fromEntries(request.params.map((field) => [field.name, workflowFormValue(field, values[field.name] ?? request.defaultValues?.[field.name])]));
|
|
1056
|
+
record2.submittingInput = {
|
|
1057
|
+
requestId,
|
|
1058
|
+
nodeName: request.nodeName,
|
|
1059
|
+
values: resolvedWorkflowValues(request.params, form),
|
|
1060
|
+
submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1061
|
+
request,
|
|
1062
|
+
precedingOutputItemIds: record2.items.map((item, index) => item.id ?? String(index))
|
|
1063
|
+
};
|
|
1064
|
+
this.launch(record2);
|
|
1065
|
+
await this.persist();
|
|
1066
|
+
}
|
|
1067
|
+
async stop(reason = "Stopped.") {
|
|
1068
|
+
const record2 = this.occupied;
|
|
1069
|
+
if (!record2) {
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
if (this.task) {
|
|
1073
|
+
this.task.cancelled = true;
|
|
1074
|
+
this.runtime.cancel(record2.id);
|
|
1075
|
+
await this.task.finished;
|
|
1076
|
+
}
|
|
1077
|
+
record2.status = "aborted";
|
|
1078
|
+
record2.error = reason;
|
|
1079
|
+
delete record2.pendingInput;
|
|
1080
|
+
delete this.workspace.occupiedId;
|
|
1081
|
+
this.clearTimers(record2);
|
|
1082
|
+
this.changed();
|
|
1083
|
+
await this.persist();
|
|
1084
|
+
}
|
|
1085
|
+
async remove(id) {
|
|
1086
|
+
if (this.workspace.occupiedId === id || this.task?.record.id === id) {
|
|
1087
|
+
throw new Error("Stop the workflow before deleting its record.");
|
|
1088
|
+
}
|
|
1089
|
+
const record2 = this.runs.find((record3) => record3.id === id);
|
|
1090
|
+
if (!record2) {
|
|
1091
|
+
throw new Error("Run no longer exists.");
|
|
1092
|
+
}
|
|
1093
|
+
const other = workflowStorageReferences([this.runs.filter((item) => item !== record2), this.workspace.drafts, this.options.legacy ?? []]);
|
|
1094
|
+
const own = workflowStorageReferences(record2);
|
|
1095
|
+
for (const id2 of other.files) {
|
|
1096
|
+
const file = await this.runtime.readFile(this.project, id2).catch(() => void 0);
|
|
1097
|
+
if (file?.reference.conversationId) {
|
|
1098
|
+
other.scopes.add(file.reference.conversationId);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
const owners = /* @__PURE__ */ new Map();
|
|
1102
|
+
for (const fileId of own.files) {
|
|
1103
|
+
if (other.files.has(fileId)) {
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
const file = await this.runtime.readFile(this.project, fileId).catch(() => void 0);
|
|
1107
|
+
if (file?.reference.conversationId) {
|
|
1108
|
+
const ids = owners.get(file.reference.conversationId) ?? [];
|
|
1109
|
+
ids.push(fileId);
|
|
1110
|
+
owners.set(file.reference.conversationId, ids);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
for (const [scope, ids] of owners) {
|
|
1114
|
+
await this.runtime.deleteFiles(this.project, scope, ids);
|
|
1115
|
+
}
|
|
1116
|
+
if (!other.scopes.has(record2.scopeId)) {
|
|
1117
|
+
await this.runtime.deleteConversation(this.project, record2.scopeId);
|
|
1118
|
+
}
|
|
1119
|
+
this.runs.splice(this.runs.indexOf(record2), 1);
|
|
1120
|
+
if (this.workspace.selectedId === id) {
|
|
1121
|
+
delete this.workspace.selectedId;
|
|
1122
|
+
}
|
|
1123
|
+
this.changed();
|
|
1124
|
+
await this.persist();
|
|
1125
|
+
}
|
|
1126
|
+
async idle() {
|
|
1127
|
+
if (this.task) {
|
|
1128
|
+
await this.task.finished;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
dispose() {
|
|
1132
|
+
this.disposed = true;
|
|
1133
|
+
clearTimeout(this.eventSave);
|
|
1134
|
+
if (this.task) {
|
|
1135
|
+
this.task.cancelled = true;
|
|
1136
|
+
this.runtime.cancel(this.task.record.id);
|
|
1137
|
+
} else if (this.occupied) {
|
|
1138
|
+
this.occupied.status = "interrupted";
|
|
1139
|
+
this.occupied.error = "Host closed.";
|
|
1140
|
+
this.clearTimers(this.occupied);
|
|
1141
|
+
delete this.workspace.occupiedId;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
// src/web/resource-cards.ts
|
|
1147
|
+
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1148
|
+
var string = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
1149
|
+
function conversationResourceCards(items, projectId, conversationId) {
|
|
1150
|
+
const cards = /* @__PURE__ */ new Map();
|
|
1151
|
+
for (const [index, item] of items.entries()) {
|
|
1152
|
+
if (!["agent-part", "opencode-part"].includes(String(item.metadata?.kind)) || item.metadata?.partType !== "tool") {
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
const content = record(item.content);
|
|
1156
|
+
if (content?.status !== "success") {
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
let output = record(content.output);
|
|
1160
|
+
if (Array.isArray(output?.content) && output.content.length === 1) {
|
|
1161
|
+
try {
|
|
1162
|
+
output = record(JSON.parse(String(record(output.content[0])?.text)));
|
|
1163
|
+
} catch {
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
let descriptor = record(record(output?.details)?.resourceCard);
|
|
1167
|
+
const input = record(content.input) ?? {};
|
|
1168
|
+
const tool = string(content.tool) ?? "";
|
|
1169
|
+
if (tool === "workflow-ui_present_card") {
|
|
1170
|
+
const presentation = output?.action ? output : input;
|
|
1171
|
+
if (presentation.action === "remove") {
|
|
1172
|
+
cards.delete(String(presentation.cardId));
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
if (presentation.action !== "upsert") {
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
descriptor = presentation;
|
|
1179
|
+
}
|
|
1180
|
+
if (!descriptor) {
|
|
1181
|
+
const target2 = { source: "local", projectId, conversationId };
|
|
1182
|
+
let kind2;
|
|
1183
|
+
if (tool === "workflow-kv_kv_store" || /workflow-desktop_(?:read|list|write)_project_kv/.test(tool)) {
|
|
1184
|
+
kind2 = "kv";
|
|
1185
|
+
Object.assign(target2, { projectId: input.projectId ?? projectId, key: input.key, scope: input.scope ?? "project", conversationId: input.conversationId ?? conversationId });
|
|
1186
|
+
} else if (tool === "workflow-knowledge_knowledge_documents" || /workflow-desktop_(?:query|mutate)_project_knowledge/.test(tool)) {
|
|
1187
|
+
kind2 = "knowledge";
|
|
1188
|
+
Object.assign(target2, { projectId: input.projectId ?? projectId, documentId: input.documentId ?? record(output?.document)?.id });
|
|
1189
|
+
} else if (tool === "read" || tool === "workflow-desktop_open_dock_tool" && input.tool === "files") {
|
|
1190
|
+
kind2 = "file";
|
|
1191
|
+
target2.filePath = input.filePath ?? input.path;
|
|
1192
|
+
} else if (tool === "workflow-desktop_open_project" && input.kind === "project") {
|
|
1193
|
+
kind2 = "project";
|
|
1194
|
+
target2.projectId = input.targetId;
|
|
1195
|
+
} else if (tool === "workflow-desktop_open_dock_tool") {
|
|
1196
|
+
if (input.tool === "browser") {
|
|
1197
|
+
kind2 = "browser";
|
|
1198
|
+
target2.url = input.url;
|
|
1199
|
+
}
|
|
1200
|
+
if (input.tool === "preview") {
|
|
1201
|
+
kind2 = "kanban";
|
|
1202
|
+
}
|
|
1203
|
+
if (input.tool === "open-project" && input.openedProjectKind === "project") {
|
|
1204
|
+
kind2 = "project";
|
|
1205
|
+
target2.projectId = input.openedProjectId;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
if (!kind2) {
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
descriptor = { kind: kind2, target: target2, title: record(output?.document)?.title };
|
|
1212
|
+
}
|
|
1213
|
+
const kind = descriptor.kind;
|
|
1214
|
+
if (!["kv", "knowledge", "project", "browser", "file", "kanban"].includes(kind)) {
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
const target = Object.fromEntries(Object.entries(record(descriptor.target) ?? {}).filter((entry) => typeof entry[1] === "string" && entry[1].length < 8192));
|
|
1218
|
+
if (target.source === "current") {
|
|
1219
|
+
target.source = "local";
|
|
1220
|
+
target.projectId = projectId;
|
|
1221
|
+
}
|
|
1222
|
+
if (target.source && !["local", "server"].includes(target.source)) {
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
if (kind === "browser") {
|
|
1226
|
+
try {
|
|
1227
|
+
const url = new URL(target.url);
|
|
1228
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
} catch {
|
|
1232
|
+
continue;
|
|
1233
|
+
}
|
|
1234
|
+
} else {
|
|
1235
|
+
target.source ??= "local";
|
|
1236
|
+
target.projectId ??= projectId;
|
|
1237
|
+
if (target.scope === "conversation") {
|
|
1238
|
+
target.conversationId ??= conversationId;
|
|
1239
|
+
}
|
|
1240
|
+
if (kind === "file" && (!target.filePath || /^(?:[\\/]|[a-z]:)|(?:^|[\\/])\.\.(?:[\\/]|$)/i.test(target.filePath))) {
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
const id = string(descriptor.cardId) ?? `${kind}:${JSON.stringify(target)}`;
|
|
1245
|
+
const title = (string(descriptor.title) ?? target.key ?? target.documentId ?? target.filePath ?? target.url ?? target.projectId ?? kind).slice(0, 200);
|
|
1246
|
+
cards.set(id, { id, kind, title, target, itemId: item.id ?? String(index) });
|
|
1247
|
+
}
|
|
1248
|
+
return [...cards.values()];
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// src/web/workflow-actions.ts
|
|
1252
|
+
import { readFile, realpath } from "fs/promises";
|
|
1253
|
+
import path from "path";
|
|
1254
|
+
function createWorkflowActions(controller, runtime, project, revision, changed) {
|
|
1255
|
+
const states = /* @__PURE__ */ new Map();
|
|
1256
|
+
const epochs = /* @__PURE__ */ new Map();
|
|
1257
|
+
let limit = 30, query = "";
|
|
1258
|
+
const entry = (id) => {
|
|
1259
|
+
const value = typeof id === "string" && id ? id : project.defaultEntrypoint ?? project.entrypoints[0]?.id;
|
|
1260
|
+
if (value && !project.entrypoints.some((item) => item.id === value)) {
|
|
1261
|
+
throw new Error("Unknown entrypoint.");
|
|
1262
|
+
}
|
|
1263
|
+
return value;
|
|
1264
|
+
};
|
|
1265
|
+
const values = (supplied, id) => {
|
|
1266
|
+
if (!supplied || typeof supplied !== "object" || Array.isArray(supplied) || Object.values(supplied).some((value) => typeof value !== "string" && !(Array.isArray(value) && value.every((item) => typeof item === "string")))) {
|
|
1267
|
+
throw new Error("Invalid parameters.");
|
|
1268
|
+
}
|
|
1269
|
+
return Object.fromEntries(workflowParameters(project, id).fields.map((field) => [
|
|
1270
|
+
field.name,
|
|
1271
|
+
workflowParamValue(field, supplied[field.name] ?? field.defaultValue ?? (field.type === "boolean" ? "false" : ""))
|
|
1272
|
+
]));
|
|
1273
|
+
};
|
|
1274
|
+
const resolve = async (message, id) => {
|
|
1275
|
+
const key = id ?? "", parameters = workflowParameters(project, id);
|
|
1276
|
+
const params = values(message.params ?? controller.draft(id).values, id);
|
|
1277
|
+
const mode = message.inputMode === "raw" ? "raw" : "form";
|
|
1278
|
+
const epoch = (epochs.get(key) ?? 0) + 1;
|
|
1279
|
+
epochs.set(key, epoch);
|
|
1280
|
+
const auth = revision();
|
|
1281
|
+
await controller.updateDraft(id, { values: params, rawArgs: String(message.rawArgs ?? controller.draft(id).rawArgs).slice(0, 1e5), inputMode: mode });
|
|
1282
|
+
const args = mode === "raw" ? parseWorkflowArgs(String(message.rawArgs ?? "[]")) : workflowParamArgs(parameters.fields, params, states.get(key));
|
|
1283
|
+
const trigger = message.trigger?.kind === "param" && parameters.fields.some((field) => field.name === message.trigger.paramName) ? { kind: "param", paramName: String(message.trigger.paramName) } : { kind: message.trigger?.kind === "input" ? "input" : "init" };
|
|
1284
|
+
const result = parameters.paramResolver?.enabled ? await runtime.resolveParams(project, { args, inputMode: mode, draftText: "", trigger }, id) : { params: {} };
|
|
1285
|
+
if (epoch !== epochs.get(key) || auth !== revision()) {
|
|
1286
|
+
throw new Error("Parameters changed. Try again.");
|
|
1287
|
+
}
|
|
1288
|
+
const next = { ...states.get(key) ?? {} };
|
|
1289
|
+
for (const [name, patch] of Object.entries(result.params ?? {})) {
|
|
1290
|
+
if (!parameters.fields.some((field) => field.name === name)) {
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
next[name] = { ...next[name], ...patch, options: { ...next[name]?.options, ...patch.options } };
|
|
1294
|
+
if (patch.value !== void 0) {
|
|
1295
|
+
params[name] = patch.value === null ? workflowParamValue(parameters.fields.find((field) => field.name === name)) : patch.value;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
states.set(key, next);
|
|
1299
|
+
await controller.updateDraft(id, { values: params, rawArgs: String(message.rawArgs ?? controller.draft(id).rawArgs).slice(0, 1e5), inputMode: mode });
|
|
1300
|
+
return { params, states: next };
|
|
1301
|
+
};
|
|
1302
|
+
const state = () => {
|
|
1303
|
+
const filtered = controller.runs.filter((record2) => !query || `${record2.title} ${record2.entrypointId ?? ""} ${record2.args.join(" ")}`.toLocaleLowerCase().includes(query.toLocaleLowerCase()));
|
|
1304
|
+
const runs = filtered.slice(0, limit).map(({ id, title, entrypointId, status, createdAt, updatedAt, durationMs }) => ({ id, title, entrypointId, status, createdAt, updatedAt, durationMs }));
|
|
1305
|
+
return {
|
|
1306
|
+
runs,
|
|
1307
|
+
selectedId: controller.workspace.selectedId,
|
|
1308
|
+
run: controller.current,
|
|
1309
|
+
occupiedId: controller.workspace.occupiedId,
|
|
1310
|
+
occupiedRun: controller.occupied,
|
|
1311
|
+
running: controller.running,
|
|
1312
|
+
drafts: controller.workspace.drafts,
|
|
1313
|
+
history: { pageSize: 30, total: filtered.length, hasMore: filtered.length > limit, query },
|
|
1314
|
+
migratedConversations: controller.workspace.migratedConversations
|
|
1315
|
+
};
|
|
1316
|
+
};
|
|
1317
|
+
return {
|
|
1318
|
+
state,
|
|
1319
|
+
invalidate: () => {
|
|
1320
|
+
states.clear();
|
|
1321
|
+
for (const [key, value] of epochs) {
|
|
1322
|
+
epochs.set(key, value + 1);
|
|
1323
|
+
}
|
|
1324
|
+
},
|
|
1325
|
+
async handle(message) {
|
|
1326
|
+
const replies = [];
|
|
1327
|
+
const id = entry(message.entrypointId);
|
|
1328
|
+
if (message.projectUri && message.projectUri !== project.uri) {
|
|
1329
|
+
throw new Error("Project changed.");
|
|
1330
|
+
}
|
|
1331
|
+
switch (message.type) {
|
|
1332
|
+
case "ready":
|
|
1333
|
+
break;
|
|
1334
|
+
case "workflow-history": {
|
|
1335
|
+
const nextQuery = String(message.query ?? "").slice(0, 200);
|
|
1336
|
+
if (query !== nextQuery || !message.more) {
|
|
1337
|
+
limit = 30;
|
|
1338
|
+
} else {
|
|
1339
|
+
limit = Math.min(limit + 30, 3e4);
|
|
1340
|
+
}
|
|
1341
|
+
query = nextQuery;
|
|
1342
|
+
changed();
|
|
1343
|
+
break;
|
|
1344
|
+
}
|
|
1345
|
+
case "workflow-select":
|
|
1346
|
+
await controller.select(message.id ? String(message.id) : void 0);
|
|
1347
|
+
break;
|
|
1348
|
+
case "workflow-draft": {
|
|
1349
|
+
const draft = { values: values(message.params ?? {}, id) };
|
|
1350
|
+
if (message.inputMode !== void 0) {
|
|
1351
|
+
draft.inputMode = message.inputMode === "raw" ? "raw" : "form";
|
|
1352
|
+
}
|
|
1353
|
+
if (message.rawArgs !== void 0) {
|
|
1354
|
+
draft.rawArgs = String(message.rawArgs).slice(0, 1e5);
|
|
1355
|
+
}
|
|
1356
|
+
epochs.set(id ?? "", (epochs.get(id ?? "") ?? 0) + 1);
|
|
1357
|
+
await controller.updateDraft(id, draft);
|
|
1358
|
+
break;
|
|
1359
|
+
}
|
|
1360
|
+
case "resolve": {
|
|
1361
|
+
try {
|
|
1362
|
+
const result = await resolve(message, id);
|
|
1363
|
+
replies.push({ type: "params", projectUri: project.uri, entrypointId: id, requestId: message.requestId, params: result.states, values: result.params });
|
|
1364
|
+
} catch (error) {
|
|
1365
|
+
replies.push({ type: "params", projectUri: project.uri, entrypointId: id, requestId: message.requestId, error: error.message });
|
|
1366
|
+
}
|
|
1367
|
+
break;
|
|
1368
|
+
}
|
|
1369
|
+
case "workflow-run": {
|
|
1370
|
+
if (controller.running) {
|
|
1371
|
+
throw new Error("A workflow is already running or waiting for input.");
|
|
1372
|
+
}
|
|
1373
|
+
const resolved = await resolve(message, id);
|
|
1374
|
+
const record2 = await controller.start({
|
|
1375
|
+
entrypointId: id,
|
|
1376
|
+
params: resolved.params,
|
|
1377
|
+
states: resolved.states,
|
|
1378
|
+
inputMode: message.inputMode === "raw" ? "raw" : "form",
|
|
1379
|
+
rawArgs: String(message.rawArgs ?? "[]")
|
|
1380
|
+
});
|
|
1381
|
+
replies.push({ type: "workflow-started", id: record2.id });
|
|
1382
|
+
break;
|
|
1383
|
+
}
|
|
1384
|
+
case "workflow-stop":
|
|
1385
|
+
await controller.stop();
|
|
1386
|
+
break;
|
|
1387
|
+
case "workflow-resume":
|
|
1388
|
+
await controller.resume(String(message.id), String(message.requestId), message.values ?? {});
|
|
1389
|
+
break;
|
|
1390
|
+
case "workflow-rerun":
|
|
1391
|
+
await controller.rerun(String(message.id));
|
|
1392
|
+
break;
|
|
1393
|
+
case "workflow-delete":
|
|
1394
|
+
await controller.remove(String(message.id));
|
|
1395
|
+
break;
|
|
1396
|
+
case "workflow-export": {
|
|
1397
|
+
const record2 = controller.runs.find((record3) => record3.id === message.id);
|
|
1398
|
+
if (!record2) {
|
|
1399
|
+
throw new Error("Run no longer exists.");
|
|
1400
|
+
}
|
|
1401
|
+
replies.push({ type: "download", name: `workflow-${record2.id}.json`, content: JSON.stringify(redactWorkflowData(record2), null, 2) });
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
case "tool-policy": {
|
|
1405
|
+
const tools = workflowParameters(project, id).tools ?? [];
|
|
1406
|
+
const policies = Object.fromEntries(tools.map((tool) => [tool.name, {
|
|
1407
|
+
enabled: typeof message.policies?.[tool.name]?.enabled === "boolean" ? message.policies[tool.name].enabled : tool.enabled,
|
|
1408
|
+
autoApprove: typeof message.policies?.[tool.name]?.autoApprove === "boolean" ? message.policies[tool.name].autoApprove : tool.autoApprove
|
|
1409
|
+
}]));
|
|
1410
|
+
await controller.updateDraft(id, { policies });
|
|
1411
|
+
break;
|
|
1412
|
+
}
|
|
1413
|
+
case "upload": {
|
|
1414
|
+
if (!Array.isArray(message.files) || message.files.length > 32 || message.files.some((file) => typeof file?.name !== "string" || typeof file.data !== "string" || file.path !== void 0)) {
|
|
1415
|
+
throw new Error("Invalid upload.");
|
|
1416
|
+
}
|
|
1417
|
+
const record2 = message.runId ? controller.runs.find((record3) => record3.id === message.runId) : void 0;
|
|
1418
|
+
if (message.runId && (!record2?.pendingInput || record2.pendingInput.requestId !== message.requestId || controller.workspace.occupiedId !== record2.id)) {
|
|
1419
|
+
throw new Error("Input request changed.");
|
|
1420
|
+
}
|
|
1421
|
+
const files = await runtime.uploadFiles(project, record2?.scopeId ?? controller.draft(id).scopeId, message.files);
|
|
1422
|
+
if (record2) {
|
|
1423
|
+
record2.files.push(...files);
|
|
1424
|
+
await controller.persist();
|
|
1425
|
+
changed();
|
|
1426
|
+
}
|
|
1427
|
+
replies.push({ type: "attachments", entrypointId: id, runId: message.runId, requestId: message.requestId, paramName: message.paramName, files });
|
|
1428
|
+
break;
|
|
1429
|
+
}
|
|
1430
|
+
case "resource": {
|
|
1431
|
+
const run = controller.runs.find((record2) => record2.id === message.runId);
|
|
1432
|
+
if (!run) {
|
|
1433
|
+
throw new Error("Run no longer exists.");
|
|
1434
|
+
}
|
|
1435
|
+
const card = conversationResourceCards(run.items, project.id, run.scopeId).find((card2) => card2.id === message.cardId);
|
|
1436
|
+
if (!card || card.target.projectId && card.target.projectId !== project.id || card.target.source === "server") {
|
|
1437
|
+
throw new Error("Resource is outside this project.");
|
|
1438
|
+
}
|
|
1439
|
+
if (card.kind === "kv" || card.kind === "knowledge") {
|
|
1440
|
+
replies.push({ type: "preview", title: card.title, content: await runtime.readResource(project, run.scopeId, card.kind, card.target) });
|
|
1441
|
+
} else if (card.kind === "file") {
|
|
1442
|
+
const root = await realpath(project.folderPath), filename = await realpath(path.resolve(root, card.target.filePath)), relative = path.relative(root, filename);
|
|
1443
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
1444
|
+
throw new Error("File is outside this project.");
|
|
1445
|
+
}
|
|
1446
|
+
replies.push({ type: "preview", title: card.title, content: await readFile(filename, "utf8") });
|
|
1447
|
+
} else {
|
|
1448
|
+
throw new Error("This resource requires an editor host.");
|
|
1449
|
+
}
|
|
1450
|
+
break;
|
|
1451
|
+
}
|
|
1452
|
+
default:
|
|
1453
|
+
throw new Error(`Unsupported workflow action: ${message.type}`);
|
|
1454
|
+
}
|
|
1455
|
+
return replies;
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
|
|
671
1460
|
// src/web/runtime.ts
|
|
672
1461
|
import { spawn } from "child_process";
|
|
673
1462
|
import { existsSync, readFileSync } from "fs";
|
|
674
|
-
import
|
|
1463
|
+
import path2 from "path";
|
|
675
1464
|
import { fileURLToPath } from "url";
|
|
676
1465
|
function cliRoot() {
|
|
677
|
-
let directory =
|
|
1466
|
+
let directory = path2.dirname(fileURLToPath(import.meta.url));
|
|
678
1467
|
for (let depth = 0; depth < 8; depth++) {
|
|
679
1468
|
try {
|
|
680
|
-
if (JSON.parse(readFileSync(
|
|
1469
|
+
if (JSON.parse(readFileSync(path2.join(directory, "package.json"), "utf8")).name === "@workflow-code/cli") {
|
|
681
1470
|
return directory;
|
|
682
1471
|
}
|
|
683
1472
|
} catch {
|
|
684
1473
|
}
|
|
685
|
-
directory =
|
|
1474
|
+
directory = path2.dirname(directory);
|
|
686
1475
|
}
|
|
687
1476
|
throw new Error("Cannot locate the CLI installation.");
|
|
688
1477
|
}
|
|
689
1478
|
function createWebRuntime(projectDir, storageDir, connection) {
|
|
690
1479
|
const root = cliRoot();
|
|
691
|
-
const builtWorker =
|
|
692
|
-
const worker = existsSync(builtWorker) ? builtWorker :
|
|
1480
|
+
const builtWorker = path2.join(root, "dist/web/worker.mjs");
|
|
1481
|
+
const worker = existsSync(builtWorker) ? builtWorker : path2.join(root, "src/web/worker.mjs");
|
|
693
1482
|
const children = /* @__PURE__ */ new Set();
|
|
694
1483
|
const active = /* @__PURE__ */ new Map();
|
|
695
1484
|
let disposed = false;
|
|
@@ -711,7 +1500,7 @@ function createWebRuntime(projectDir, storageDir, connection) {
|
|
|
711
1500
|
WORKFLOW_SERVER_TOKEN: "",
|
|
712
1501
|
WORKFLOW_LLM_SERVER_URL: auth?.serverUrl ?? "",
|
|
713
1502
|
WORKFLOW_LLM_SERVER_TOKEN: auth?.accessToken ?? "",
|
|
714
|
-
WORKFLOW_CLI_AUTH_DIR:
|
|
1503
|
+
WORKFLOW_CLI_AUTH_DIR: path2.join(storageDir, "runtime-auth")
|
|
715
1504
|
};
|
|
716
1505
|
for (const key of [
|
|
717
1506
|
"TSX_TSCONFIG_PATH",
|
|
@@ -742,7 +1531,8 @@ function createWebRuntime(projectDir, storageDir, connection) {
|
|
|
742
1531
|
child.stdout.setEncoding("utf8");
|
|
743
1532
|
child.stderr.setEncoding("utf8");
|
|
744
1533
|
child.stderr.on("data", (chunk) => {
|
|
745
|
-
stderr = (stderr + chunk).slice(-
|
|
1534
|
+
stderr = (stderr + chunk).slice(-1e5);
|
|
1535
|
+
onEvent?.({ type: "runtime_log", text: redactWorkflowData(chunk, [auth?.accessToken ?? ""]), runId: request.runId });
|
|
746
1536
|
});
|
|
747
1537
|
child.stdout.on("data", (chunk) => {
|
|
748
1538
|
buffer += chunk;
|
|
@@ -795,7 +1585,7 @@ function createWebRuntime(projectDir, storageDir, connection) {
|
|
|
795
1585
|
}
|
|
796
1586
|
});
|
|
797
1587
|
child.stdin.on("error", () => void 0);
|
|
798
|
-
child.stdin.end(JSON.stringify({ ...request, storageDir }));
|
|
1588
|
+
child.stdin.end(JSON.stringify({ ...request, storageDir, runtimeProjectDir: projectDir }));
|
|
799
1589
|
});
|
|
800
1590
|
return {
|
|
801
1591
|
available: true,
|
|
@@ -832,111 +1622,6 @@ function createWebRuntime(projectDir, storageDir, connection) {
|
|
|
832
1622
|
};
|
|
833
1623
|
}
|
|
834
1624
|
|
|
835
|
-
// src/web/resource-cards.ts
|
|
836
|
-
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
837
|
-
var string = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
838
|
-
function conversationResourceCards(items, projectId, conversationId) {
|
|
839
|
-
const cards = /* @__PURE__ */ new Map();
|
|
840
|
-
for (const [index, item] of items.entries()) {
|
|
841
|
-
if (!["agent-part", "opencode-part"].includes(String(item.metadata?.kind)) || item.metadata?.partType !== "tool") {
|
|
842
|
-
continue;
|
|
843
|
-
}
|
|
844
|
-
const content = record(item.content);
|
|
845
|
-
if (content?.status !== "success") {
|
|
846
|
-
continue;
|
|
847
|
-
}
|
|
848
|
-
let output = record(content.output);
|
|
849
|
-
if (Array.isArray(output?.content) && output.content.length === 1) {
|
|
850
|
-
try {
|
|
851
|
-
output = record(JSON.parse(String(record(output.content[0])?.text)));
|
|
852
|
-
} catch {
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
let descriptor = record(record(output?.details)?.resourceCard);
|
|
856
|
-
const input = record(content.input) ?? {};
|
|
857
|
-
const tool = string(content.tool) ?? "";
|
|
858
|
-
if (tool === "workflow-ui_present_card") {
|
|
859
|
-
const presentation = output?.action ? output : input;
|
|
860
|
-
if (presentation.action === "remove") {
|
|
861
|
-
cards.delete(String(presentation.cardId));
|
|
862
|
-
continue;
|
|
863
|
-
}
|
|
864
|
-
if (presentation.action !== "upsert") {
|
|
865
|
-
continue;
|
|
866
|
-
}
|
|
867
|
-
descriptor = presentation;
|
|
868
|
-
}
|
|
869
|
-
if (!descriptor) {
|
|
870
|
-
const target2 = { source: "local", projectId, conversationId };
|
|
871
|
-
let kind2;
|
|
872
|
-
if (tool === "workflow-kv_kv_store" || /workflow-desktop_(?:read|list|write)_project_kv/.test(tool)) {
|
|
873
|
-
kind2 = "kv";
|
|
874
|
-
Object.assign(target2, { projectId: input.projectId ?? projectId, key: input.key, scope: input.scope ?? "project", conversationId: input.conversationId ?? conversationId });
|
|
875
|
-
} else if (tool === "workflow-knowledge_knowledge_documents" || /workflow-desktop_(?:query|mutate)_project_knowledge/.test(tool)) {
|
|
876
|
-
kind2 = "knowledge";
|
|
877
|
-
Object.assign(target2, { projectId: input.projectId ?? projectId, documentId: input.documentId ?? record(output?.document)?.id });
|
|
878
|
-
} else if (tool === "read" || tool === "workflow-desktop_open_dock_tool" && input.tool === "files") {
|
|
879
|
-
kind2 = "file";
|
|
880
|
-
target2.filePath = input.filePath ?? input.path;
|
|
881
|
-
} else if (tool === "workflow-desktop_open_project" && input.kind === "project") {
|
|
882
|
-
kind2 = "project";
|
|
883
|
-
target2.projectId = input.targetId;
|
|
884
|
-
} else if (tool === "workflow-desktop_open_dock_tool") {
|
|
885
|
-
if (input.tool === "browser") {
|
|
886
|
-
kind2 = "browser";
|
|
887
|
-
target2.url = input.url;
|
|
888
|
-
}
|
|
889
|
-
if (input.tool === "preview") {
|
|
890
|
-
kind2 = "kanban";
|
|
891
|
-
}
|
|
892
|
-
if (input.tool === "open-project" && input.openedProjectKind === "project") {
|
|
893
|
-
kind2 = "project";
|
|
894
|
-
target2.projectId = input.openedProjectId;
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
if (!kind2) {
|
|
898
|
-
continue;
|
|
899
|
-
}
|
|
900
|
-
descriptor = { kind: kind2, target: target2, title: record(output?.document)?.title };
|
|
901
|
-
}
|
|
902
|
-
const kind = descriptor.kind;
|
|
903
|
-
if (!["kv", "knowledge", "project", "browser", "file", "kanban"].includes(kind)) {
|
|
904
|
-
continue;
|
|
905
|
-
}
|
|
906
|
-
const target = Object.fromEntries(Object.entries(record(descriptor.target) ?? {}).filter((entry) => typeof entry[1] === "string" && entry[1].length < 8192));
|
|
907
|
-
if (target.source === "current") {
|
|
908
|
-
target.source = "local";
|
|
909
|
-
target.projectId = projectId;
|
|
910
|
-
}
|
|
911
|
-
if (target.source && !["local", "server"].includes(target.source)) {
|
|
912
|
-
continue;
|
|
913
|
-
}
|
|
914
|
-
if (kind === "browser") {
|
|
915
|
-
try {
|
|
916
|
-
const url = new URL(target.url);
|
|
917
|
-
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
|
|
918
|
-
continue;
|
|
919
|
-
}
|
|
920
|
-
} catch {
|
|
921
|
-
continue;
|
|
922
|
-
}
|
|
923
|
-
} else {
|
|
924
|
-
target.source ??= "local";
|
|
925
|
-
target.projectId ??= projectId;
|
|
926
|
-
if (target.scope === "conversation") {
|
|
927
|
-
target.conversationId ??= conversationId;
|
|
928
|
-
}
|
|
929
|
-
if (kind === "file" && (!target.filePath || /^(?:[\\/]|[a-z]:)|(?:^|[\\/])\.\.(?:[\\/]|$)/i.test(target.filePath))) {
|
|
930
|
-
continue;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
const id = string(descriptor.cardId) ?? `${kind}:${JSON.stringify(target)}`;
|
|
934
|
-
const title = (string(descriptor.title) ?? target.key ?? target.documentId ?? target.filePath ?? target.url ?? target.projectId ?? kind).slice(0, 200);
|
|
935
|
-
cards.set(id, { id, kind, title, target, itemId: item.id ?? String(index) });
|
|
936
|
-
}
|
|
937
|
-
return [...cards.values()];
|
|
938
|
-
}
|
|
939
|
-
|
|
940
1625
|
// src/web/account-auth.ts
|
|
941
1626
|
async function loginWebAccount(serverUrl, email, password) {
|
|
942
1627
|
if (!email.trim() || !password) {
|
|
@@ -983,12 +1668,47 @@ async function loginWebAccount(serverUrl, email, password) {
|
|
|
983
1668
|
}
|
|
984
1669
|
}
|
|
985
1670
|
|
|
1671
|
+
// src/web/assistant-project.ts
|
|
1672
|
+
import { pathToFileURL } from "url";
|
|
1673
|
+
var ASSISTANT_PROTOCOL_VERSION = 1;
|
|
1674
|
+
var assistantTools = /* @__PURE__ */ new Set([
|
|
1675
|
+
"read",
|
|
1676
|
+
"write",
|
|
1677
|
+
"edit",
|
|
1678
|
+
"bash",
|
|
1679
|
+
"grep",
|
|
1680
|
+
"find",
|
|
1681
|
+
"ls",
|
|
1682
|
+
"skill",
|
|
1683
|
+
"workflow-memory_global_memory",
|
|
1684
|
+
"workflow-ui_present_card"
|
|
1685
|
+
]);
|
|
1686
|
+
async function inspectWebProject(runtime, projectDir, host) {
|
|
1687
|
+
if (host?.assistant && !host.assistantProjectDir) {
|
|
1688
|
+
throw new Error("A dedicated assistant project is required.");
|
|
1689
|
+
}
|
|
1690
|
+
const inspection = await runtime.inspect([host?.assistantProjectDir ?? projectDir]);
|
|
1691
|
+
return {
|
|
1692
|
+
...inspection,
|
|
1693
|
+
assistantProtocolVersion: host?.assistant ? ASSISTANT_PROTOCOL_VERSION : void 0,
|
|
1694
|
+
projects: inspection.projects.map((descriptor) => {
|
|
1695
|
+
const project = { ...descriptor, uri: pathToFileURL(descriptor.folderPath).href };
|
|
1696
|
+
if (host?.assistant) {
|
|
1697
|
+
project.assistant = host.assistant;
|
|
1698
|
+
project.uri = `studio-assistant:${encodeURIComponent(project.assistant.workdir)}`;
|
|
1699
|
+
project.requiresLogin = true;
|
|
1700
|
+
project.fields = project.fields.map((field) => field.name === "permissionPolicy" ? { ...field, defaultValue: "workspace" } : field);
|
|
1701
|
+
project.tools = project.tools?.filter((tool) => assistantTools.has(tool.name));
|
|
1702
|
+
}
|
|
1703
|
+
return project;
|
|
1704
|
+
})
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
|
|
986
1708
|
// src/web/server.ts
|
|
987
1709
|
async function startWebServer(options) {
|
|
988
|
-
const projectDir = await
|
|
989
|
-
const
|
|
990
|
-
const storageDir = path2.resolve(options.dataDir ?? path2.join(os.homedir(), ".workflow-code", "web"));
|
|
991
|
-
const historyPath = path2.join(storageDir, key, "conversations.json");
|
|
1710
|
+
const projectDir = await realpath2(path3.resolve(options.projectDir));
|
|
1711
|
+
const storageDir = path3.resolve(options.dataDir ?? path3.join(os.homedir(), ".workflow-code", "web"));
|
|
992
1712
|
const clients = /* @__PURE__ */ new Set();
|
|
993
1713
|
let connection;
|
|
994
1714
|
let account = { status: "signed-out", serverUrl: "http://localhost:7126", revision: 0 };
|
|
@@ -999,12 +1719,13 @@ async function startWebServer(options) {
|
|
|
999
1719
|
let hostConnection = options.host?.connection;
|
|
1000
1720
|
let hostManaged = !!hostConnection;
|
|
1001
1721
|
let authEpoch = 0;
|
|
1722
|
+
let authenticationInitialized = false;
|
|
1002
1723
|
let authCheck;
|
|
1003
1724
|
const sensitiveTokens = /* @__PURE__ */ new Set();
|
|
1004
1725
|
const runtime = options.runtime ?? createWebRuntime(projectDir, storageDir, () => connection);
|
|
1005
1726
|
let inspected;
|
|
1006
1727
|
try {
|
|
1007
|
-
inspected = await runtime.
|
|
1728
|
+
inspected = await inspectWebProject(runtime, projectDir, options.host);
|
|
1008
1729
|
} catch (error) {
|
|
1009
1730
|
runtime.dispose();
|
|
1010
1731
|
throw error;
|
|
@@ -1021,18 +1742,12 @@ async function startWebServer(options) {
|
|
|
1021
1742
|
runtime.dispose();
|
|
1022
1743
|
throw new Error("workflow-code web currently supports Conversation and Workflow projects.");
|
|
1023
1744
|
}
|
|
1024
|
-
const project =
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
project.uri = `studio-assistant:${encodeURIComponent(project.assistant.workdir)}`;
|
|
1028
|
-
project.requiresLogin = true;
|
|
1029
|
-
project.fields = project.fields.map((field) => field.name === "permissionPolicy" ? { ...field, defaultValue: "workspace" } : field);
|
|
1030
|
-
const supported = /* @__PURE__ */ new Set(["read", "write", "edit", "bash", "grep", "find", "ls", "skill", "workflow-memory_global_memory", "workflow-ui_present_card"]);
|
|
1031
|
-
project.tools = project.tools?.filter((tool) => supported.has(tool.name));
|
|
1032
|
-
}
|
|
1745
|
+
const project = descriptor;
|
|
1746
|
+
const key = createHash("sha256").update(project.assistant ? `assistant:${project.id}:${project.assistant.workdir}` : projectDir).digest("hex").slice(0, 24);
|
|
1747
|
+
const historyPath = path3.join(storageDir, key, "conversations.json");
|
|
1033
1748
|
let saved = {};
|
|
1034
1749
|
try {
|
|
1035
|
-
saved = JSON.parse(await
|
|
1750
|
+
saved = JSON.parse(await readFile2(historyPath, "utf8"));
|
|
1036
1751
|
} catch (error) {
|
|
1037
1752
|
if (error.code !== "ENOENT") {
|
|
1038
1753
|
runtime.dispose();
|
|
@@ -1042,6 +1757,7 @@ async function startWebServer(options) {
|
|
|
1042
1757
|
let pendingSave = Promise.resolve();
|
|
1043
1758
|
let broadcastTimer;
|
|
1044
1759
|
let authRevision = 0;
|
|
1760
|
+
const isWorkflow = project.projectType === "workflow";
|
|
1045
1761
|
const parameterStates = /* @__PURE__ */ new Map();
|
|
1046
1762
|
const parameterEpochs = /* @__PURE__ */ new Map();
|
|
1047
1763
|
const publicFile = (file) => ({ ...file, previewUrl: `/api/file/${encodeURIComponent(file.id)}` });
|
|
@@ -1058,7 +1774,13 @@ async function startWebServer(options) {
|
|
|
1058
1774
|
}
|
|
1059
1775
|
return value;
|
|
1060
1776
|
};
|
|
1061
|
-
const state = () => publicValue(redactWorkflowData({
|
|
1777
|
+
const state = () => publicValue(redactWorkflowData({
|
|
1778
|
+
type: "state",
|
|
1779
|
+
project,
|
|
1780
|
+
...isWorkflow ? workflowActions.state() : { sessions: controller.sessions, activeId: controller.activeId, running: controller.running, parameterValues: controller.current.parameterValues ?? {} },
|
|
1781
|
+
account,
|
|
1782
|
+
authenticationRevision: authRevision
|
|
1783
|
+
}, [...sensitiveTokens]));
|
|
1062
1784
|
const broadcast = () => {
|
|
1063
1785
|
if (broadcastTimer) {
|
|
1064
1786
|
return;
|
|
@@ -1073,14 +1795,14 @@ async function startWebServer(options) {
|
|
|
1073
1795
|
}
|
|
1074
1796
|
}, 30);
|
|
1075
1797
|
};
|
|
1076
|
-
const
|
|
1798
|
+
const conversationController = isWorkflow ? void 0 : new ConversationController(project, runtime, saved, {
|
|
1077
1799
|
changed: broadcast,
|
|
1078
1800
|
log: (message) => process.stderr.write(`${message}
|
|
1079
1801
|
`),
|
|
1080
1802
|
save: (sessions, activeId) => {
|
|
1081
1803
|
const content = JSON.stringify({ sessions, activeId });
|
|
1082
1804
|
pendingSave = pendingSave.catch(() => void 0).then(async () => {
|
|
1083
|
-
await mkdir(
|
|
1805
|
+
await mkdir(path3.dirname(historyPath), { recursive: true });
|
|
1084
1806
|
const temp = `${historyPath}.${process.pid}.tmp`;
|
|
1085
1807
|
await writeFile(temp, content, { mode: 384 });
|
|
1086
1808
|
await rename(temp, historyPath);
|
|
@@ -1088,6 +1810,51 @@ async function startWebServer(options) {
|
|
|
1088
1810
|
return pendingSave;
|
|
1089
1811
|
}
|
|
1090
1812
|
});
|
|
1813
|
+
const controller = conversationController;
|
|
1814
|
+
const runsPath = path3.join(storageDir, key, "runs.json");
|
|
1815
|
+
let workspace;
|
|
1816
|
+
if (isWorkflow) {
|
|
1817
|
+
try {
|
|
1818
|
+
workspace = JSON.parse(await readFile2(runsPath, "utf8"));
|
|
1819
|
+
if (workspace?.version !== 1 || !Array.isArray(workspace.runs) || !workspace.drafts || typeof workspace.drafts !== "object") {
|
|
1820
|
+
throw new Error("Invalid Workflow history.");
|
|
1821
|
+
}
|
|
1822
|
+
} catch (error) {
|
|
1823
|
+
if (error.code !== "ENOENT") {
|
|
1824
|
+
runtime.dispose();
|
|
1825
|
+
throw error;
|
|
1826
|
+
}
|
|
1827
|
+
workspace = migrateWorkflowHistory(saved.sessions ?? [], saved.activeId);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
const workflowController = isWorkflow ? new WorkflowController(project, runtime, workspace, {
|
|
1831
|
+
changed: broadcast,
|
|
1832
|
+
legacy: saved.sessions,
|
|
1833
|
+
log: (message) => process.stderr.write(`${message}
|
|
1834
|
+
`),
|
|
1835
|
+
save: (workspace2) => {
|
|
1836
|
+
const content = JSON.stringify(workspace2);
|
|
1837
|
+
pendingSave = pendingSave.catch(() => void 0).then(async () => {
|
|
1838
|
+
await mkdir(path3.dirname(runsPath), { recursive: true });
|
|
1839
|
+
const temp = `${runsPath}.${process.pid}.tmp`;
|
|
1840
|
+
await writeFile(temp, content, { mode: 384 });
|
|
1841
|
+
await rename(temp, runsPath);
|
|
1842
|
+
});
|
|
1843
|
+
return pendingSave;
|
|
1844
|
+
}
|
|
1845
|
+
}) : void 0;
|
|
1846
|
+
const workflowActions = workflowController ? createWorkflowActions(workflowController, runtime, project, () => authRevision, broadcast) : void 0;
|
|
1847
|
+
if (workflowController) {
|
|
1848
|
+
await workflowController.persist();
|
|
1849
|
+
}
|
|
1850
|
+
const stopForAuthentication = async () => {
|
|
1851
|
+
for (const session of conversationController?.sessions ?? []) {
|
|
1852
|
+
conversationController.stop(session);
|
|
1853
|
+
}
|
|
1854
|
+
if (workflowController) {
|
|
1855
|
+
await workflowController.stop("Workflow login changed.");
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1091
1858
|
const refreshAuth = async (force = false) => {
|
|
1092
1859
|
if ((passwordLogin || login) && !force) {
|
|
1093
1860
|
return;
|
|
@@ -1118,10 +1885,12 @@ ${token}`;
|
|
|
1118
1885
|
connection = void 0;
|
|
1119
1886
|
authRevision++;
|
|
1120
1887
|
parameterStates.clear();
|
|
1121
|
-
|
|
1122
|
-
|
|
1888
|
+
workflowActions?.invalidate();
|
|
1889
|
+
if (authenticationInitialized) {
|
|
1890
|
+
await stopForAuthentication();
|
|
1123
1891
|
}
|
|
1124
1892
|
}
|
|
1893
|
+
authenticationInitialized = true;
|
|
1125
1894
|
account = { status: candidate ? "checking" : "signed-out", serverUrl: serverUrl || "http://localhost:7126", user: candidate ? changed ? hostManaged ? void 0 : config.user : account.user : void 0, revision: account.revision + 1 };
|
|
1126
1895
|
if (candidate) {
|
|
1127
1896
|
let unauthorized = false;
|
|
@@ -1139,9 +1908,11 @@ ${token}`;
|
|
|
1139
1908
|
if (!connection) {
|
|
1140
1909
|
authRevision++;
|
|
1141
1910
|
parameterStates.clear();
|
|
1911
|
+
workflowActions?.invalidate();
|
|
1142
1912
|
}
|
|
1143
1913
|
connection = candidate;
|
|
1144
|
-
|
|
1914
|
+
const savedConnection = !hostManaged && candidate.serverUrl === config.serverUrl.replace(/\/+$/, "") && candidate.accessToken === config.apiKey;
|
|
1915
|
+
account = { ...account, status: "authenticated", user: profile, expiresAt: result.data.principal?.expiresAt ?? (savedConnection ? config.expiresAt : void 0) };
|
|
1145
1916
|
} catch (error) {
|
|
1146
1917
|
if (epoch !== authEpoch) {
|
|
1147
1918
|
return;
|
|
@@ -1150,9 +1921,8 @@ ${token}`;
|
|
|
1150
1921
|
if (!changed) {
|
|
1151
1922
|
authRevision++;
|
|
1152
1923
|
parameterStates.clear();
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1924
|
+
workflowActions?.invalidate();
|
|
1925
|
+
await stopForAuthentication();
|
|
1156
1926
|
}
|
|
1157
1927
|
account = { ...account, status: unauthorized ? "expired" : "error", error: String(error.message) };
|
|
1158
1928
|
}
|
|
@@ -1214,11 +1984,14 @@ ${token}`;
|
|
|
1214
1984
|
if (message.projectUri && message.projectUri !== project.uri) {
|
|
1215
1985
|
throw new Error("Project changed.");
|
|
1216
1986
|
}
|
|
1217
|
-
if (
|
|
1987
|
+
if (workflowActions && !["ready", "login", "verify", "logout", "usage"].includes(message.type)) {
|
|
1988
|
+
return publicValue(await workflowActions.handle(message));
|
|
1989
|
+
}
|
|
1990
|
+
if (message.sessionId && message.sessionId !== conversationController?.activeId) {
|
|
1218
1991
|
throw new Error("Conversation changed.");
|
|
1219
1992
|
}
|
|
1220
1993
|
const replies = [];
|
|
1221
|
-
const current =
|
|
1994
|
+
const current = conversationController?.current;
|
|
1222
1995
|
const entrypointId = typeof message.entrypointId === "string" && message.entrypointId ? message.entrypointId : project.defaultEntrypoint;
|
|
1223
1996
|
if (entrypointId && !project.entrypoints.some((entry) => entry.id === entrypointId)) {
|
|
1224
1997
|
throw new Error("Unknown entrypoint.");
|
|
@@ -1379,12 +2152,13 @@ ${token}`;
|
|
|
1379
2152
|
if (card.kind === "kv" || card.kind === "knowledge") {
|
|
1380
2153
|
replies.push({ type: "preview", title: card.title, content: await runtime.readResource(project, current.id, card.kind, card.target) });
|
|
1381
2154
|
} else if (card.kind === "file") {
|
|
1382
|
-
const
|
|
1383
|
-
const
|
|
1384
|
-
|
|
2155
|
+
const root = await realpath2(project.assistant?.workdir ?? projectDir);
|
|
2156
|
+
const filename = await realpath2(path3.resolve(root, card.target.filePath));
|
|
2157
|
+
const relative = path3.relative(root, filename);
|
|
2158
|
+
if (relative === ".." || relative.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative)) {
|
|
1385
2159
|
throw new Error("File is outside this project.");
|
|
1386
2160
|
}
|
|
1387
|
-
replies.push({ type: "preview", title: card.title, content: await
|
|
2161
|
+
replies.push({ type: "preview", title: card.title, content: await readFile2(filename, "utf8") });
|
|
1388
2162
|
} else {
|
|
1389
2163
|
throw new Error("This resource requires an editor host.");
|
|
1390
2164
|
}
|
|
@@ -1399,7 +2173,7 @@ ${token}`;
|
|
|
1399
2173
|
if (!response.ok || result.errCode !== 0) {
|
|
1400
2174
|
throw new Error(result.errMessage || "Cannot load usage.");
|
|
1401
2175
|
}
|
|
1402
|
-
replies.push({ type: "usage", sessionId: current
|
|
2176
|
+
replies.push({ type: "usage", sessionId: current?.id, usage: result.data });
|
|
1403
2177
|
break;
|
|
1404
2178
|
}
|
|
1405
2179
|
case "login": {
|
|
@@ -1441,7 +2215,7 @@ ${token}`;
|
|
|
1441
2215
|
await refreshAuth(true);
|
|
1442
2216
|
break;
|
|
1443
2217
|
}
|
|
1444
|
-
login = spawn2(process.execPath, [
|
|
2218
|
+
login = spawn2(process.execPath, [path3.join(cliRoot(), "dist/index.js"), "workspace", "login", "--server", url.toString().replace(/\/$/, "")], { shell: false, stdio: ["ignore", "ignore", "pipe"] });
|
|
1445
2219
|
login.stderr?.on("data", (chunk) => process.stderr.write(chunk));
|
|
1446
2220
|
login.once("error", (error) => {
|
|
1447
2221
|
login = void 0;
|
|
@@ -1538,7 +2312,7 @@ ${token}`;
|
|
|
1538
2312
|
}
|
|
1539
2313
|
if (url.pathname.startsWith("/api/file/") && request.method === "GET") {
|
|
1540
2314
|
const file = await runtime.readFile(project, decodeURIComponent(url.pathname.slice("/api/file/".length)));
|
|
1541
|
-
const bytes = await
|
|
2315
|
+
const bytes = await readFile2(file.path);
|
|
1542
2316
|
const safeType = /^(?:image\/(?:png|jpeg|gif|webp|avif)|audio\/[a-z0-9.+-]+|video\/[a-z0-9.+-]+)$/.test(file.reference.mimeType);
|
|
1543
2317
|
response.writeHead(200, { "Content-Type": safeType ? file.reference.mimeType : "application/octet-stream", "Content-Disposition": `${safeType ? "inline" : "attachment"}; filename*=UTF-8''${encodeURIComponent(file.reference.name)}`, "Cache-Control": "no-store" });
|
|
1544
2318
|
response.end(bytes);
|
|
@@ -1559,15 +2333,18 @@ ${token}`;
|
|
|
1559
2333
|
if (url.pathname === "/") {
|
|
1560
2334
|
const ancestors = options.host ? "'self' vscode-webview: vscode-file: https://*.vscode-cdn.net https://*.vscode-webview.net http://127.0.0.1:* http://localhost:*" : "'self' vscode-webview:";
|
|
1561
2335
|
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Content-Security-Policy": `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: http:; font-src 'self'; connect-src 'self'; media-src 'self' blob:; object-src 'none'; base-uri 'none'; frame-ancestors ${ancestors}; form-action 'self'` });
|
|
1562
|
-
|
|
2336
|
+
const ui = isWorkflow ? "workflow-workspace" : "workflow-ui";
|
|
2337
|
+
response.end(`<!doctype html><html lang="zh-CN" data-theme="${options.theme ?? "auto"}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="workflow-project" content="${key}"><title>Workflow</title><link rel="stylesheet" href="/assets/codicons/codicon.css"><link rel="stylesheet" href="/assets/${ui}.css"><link rel="stylesheet" href="/assets/theme.css"></head><body data-surface="${options.host?.surface ?? "editor"}"><div id="app"></div><script src="/assets/${ui}.js" defer></script></body></html>`);
|
|
1563
2338
|
return;
|
|
1564
2339
|
}
|
|
1565
2340
|
const assets = { "/assets/workflow-ui.js": "application/javascript", "/assets/workflow-ui.css": "text/css", "/assets/theme.css": "text/css", "/assets/codicons/codicon.css": "text/css", "/assets/codicons/codicon.ttf": "font/ttf" };
|
|
2341
|
+
assets["/assets/workflow-workspace.js"] = "application/javascript";
|
|
2342
|
+
assets["/assets/workflow-workspace.css"] = "text/css";
|
|
1566
2343
|
if (!assets[url.pathname]) {
|
|
1567
2344
|
json(response, 404, { message: "Not found." });
|
|
1568
2345
|
return;
|
|
1569
2346
|
}
|
|
1570
|
-
const content = await
|
|
2347
|
+
const content = await readFile2(path3.join(cliRoot(), "dist/web", url.pathname.slice("/assets/".length)));
|
|
1571
2348
|
response.writeHead(200, { "Content-Type": assets[url.pathname], "Cache-Control": "no-cache" });
|
|
1572
2349
|
response.end(content);
|
|
1573
2350
|
})().catch((error) => {
|
|
@@ -1588,7 +2365,8 @@ ${token}`;
|
|
|
1588
2365
|
});
|
|
1589
2366
|
} catch (error) {
|
|
1590
2367
|
clearInterval(authPoll);
|
|
1591
|
-
|
|
2368
|
+
conversationController?.dispose();
|
|
2369
|
+
workflowController?.dispose();
|
|
1592
2370
|
runtime.dispose();
|
|
1593
2371
|
throw error;
|
|
1594
2372
|
}
|
|
@@ -1599,6 +2377,7 @@ ${token}`;
|
|
|
1599
2377
|
url: `${origin}/#session=${capability}`,
|
|
1600
2378
|
project,
|
|
1601
2379
|
controller,
|
|
2380
|
+
workflowController,
|
|
1602
2381
|
setHostConnection: async (value) => {
|
|
1603
2382
|
authEpoch++;
|
|
1604
2383
|
authCheck?.abort();
|
|
@@ -1615,14 +2394,17 @@ ${token}`;
|
|
|
1615
2394
|
clearTimeout(broadcastTimer);
|
|
1616
2395
|
}
|
|
1617
2396
|
login?.kill();
|
|
1618
|
-
|
|
2397
|
+
conversationController?.dispose();
|
|
2398
|
+
workflowController?.dispose();
|
|
1619
2399
|
runtime.dispose();
|
|
1620
2400
|
for (const client of clients) {
|
|
1621
2401
|
client.end();
|
|
1622
2402
|
}
|
|
1623
2403
|
await new Promise((resolve) => server.close(() => resolve()));
|
|
1624
|
-
await
|
|
1625
|
-
await
|
|
2404
|
+
await conversationController?.idle();
|
|
2405
|
+
await conversationController?.persist();
|
|
2406
|
+
await workflowController?.idle();
|
|
2407
|
+
await workflowController?.persist();
|
|
1626
2408
|
await pendingSave;
|
|
1627
2409
|
})()
|
|
1628
2410
|
};
|
|
@@ -1632,7 +2414,7 @@ ${token}`;
|
|
|
1632
2414
|
import { createInterface } from "readline";
|
|
1633
2415
|
|
|
1634
2416
|
// src/web/host-config.ts
|
|
1635
|
-
import
|
|
2417
|
+
import path4 from "path";
|
|
1636
2418
|
function parseHostConnection(value) {
|
|
1637
2419
|
if (value === null || value === void 0) {
|
|
1638
2420
|
return;
|
|
@@ -1656,10 +2438,17 @@ function parseHostConfiguration(value) {
|
|
|
1656
2438
|
throw new Error("Invalid web host configuration.");
|
|
1657
2439
|
}
|
|
1658
2440
|
const assistant = configuration.assistant;
|
|
1659
|
-
if (assistant && (typeof assistant.workdir !== "string" || !
|
|
2441
|
+
if (assistant && (typeof assistant.workdir !== "string" || !path4.isAbsolute(assistant.workdir) || !Array.isArray(assistant.roots) || !assistant.roots.length || assistant.roots.length > 100 || assistant.standalone !== void 0 && typeof assistant.standalone !== "boolean" || assistant.roots.some((root) => typeof root.uri !== "string" || typeof root.name !== "string" || typeof root.folderPath !== "string" || !path4.isAbsolute(root.folderPath)) || !assistant.roots.some((root) => root.folderPath === assistant.workdir))) {
|
|
1660
2442
|
throw new Error("Invalid assistant workspace.");
|
|
1661
2443
|
}
|
|
1662
|
-
|
|
2444
|
+
const assistantProjectDir = configuration.assistantProjectDir;
|
|
2445
|
+
if (configuration.surface === "assistant" && (!assistant || typeof assistantProjectDir !== "string" || !path4.isAbsolute(assistantProjectDir))) {
|
|
2446
|
+
throw new Error("A dedicated assistant project and its workspace are required.");
|
|
2447
|
+
}
|
|
2448
|
+
if (configuration.surface !== "assistant" && (assistant || assistantProjectDir !== void 0)) {
|
|
2449
|
+
throw new Error("Assistant configuration requires the assistant surface.");
|
|
2450
|
+
}
|
|
2451
|
+
return { surface: configuration.surface, assistant, assistantProjectDir, connection: parseHostConnection(configuration.connection) };
|
|
1663
2452
|
}
|
|
1664
2453
|
|
|
1665
2454
|
// src/web.ts
|
|
@@ -1694,12 +2483,12 @@ function parseWebArgs(argv) {
|
|
|
1694
2483
|
}
|
|
1695
2484
|
options.theme = value;
|
|
1696
2485
|
} else {
|
|
1697
|
-
options.dataDir =
|
|
2486
|
+
options.dataDir = path5.resolve(value);
|
|
1698
2487
|
}
|
|
1699
2488
|
} else if (arg.startsWith("-") || projectSet) {
|
|
1700
2489
|
throw new Error(`Unknown web argument: ${arg}`);
|
|
1701
2490
|
} else {
|
|
1702
|
-
options.projectDir =
|
|
2491
|
+
options.projectDir = path5.resolve(arg);
|
|
1703
2492
|
projectSet = true;
|
|
1704
2493
|
}
|
|
1705
2494
|
}
|
|
@@ -1711,15 +2500,6 @@ async function main(argv) {
|
|
|
1711
2500
|
console.log(i18n.language.startsWith("zh") ? "workflow-code web [\u9879\u76EE\u76EE\u5F55]\n --port <0-65535> \u76D1\u542C\u7AEF\u53E3\uFF0C0 \u81EA\u52A8\u5206\u914D\n --no-open \u4E0D\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\n --theme <auto|light|dark>\n --data-dir <\u76EE\u5F55> \u5BF9\u8BDD\u548C\u6587\u4EF6\u5B58\u50A8\u4F4D\u7F6E\n --json \u8F93\u51FA\u7ED3\u6784\u5316\u542F\u52A8\u4FE1\u606F" : "workflow-code web [project-directory]\n --port <0-65535> Listen port; 0 selects an available port\n --no-open Do not open a browser\n --theme <auto|light|dark>\n --data-dir <path> Conversation and file storage directory\n --json Print structured startup information");
|
|
1712
2501
|
return 0;
|
|
1713
2502
|
}
|
|
1714
|
-
if (options.inspect) {
|
|
1715
|
-
const runtime = createWebRuntime(options.projectDir, options.dataDir ?? "", () => void 0);
|
|
1716
|
-
try {
|
|
1717
|
-
console.log(JSON.stringify(await runtime.inspect([options.projectDir])));
|
|
1718
|
-
} finally {
|
|
1719
|
-
runtime.dispose();
|
|
1720
|
-
}
|
|
1721
|
-
return 0;
|
|
1722
|
-
}
|
|
1723
2503
|
const input = options.hostStdio ? createInterface({ input: process.stdin, crlfDelay: Infinity }) : void 0;
|
|
1724
2504
|
const lines = input?.[Symbol.asyncIterator]();
|
|
1725
2505
|
let host;
|
|
@@ -1735,6 +2515,17 @@ async function main(argv) {
|
|
|
1735
2515
|
input?.close();
|
|
1736
2516
|
throw error;
|
|
1737
2517
|
}
|
|
2518
|
+
if (options.inspect) {
|
|
2519
|
+
const runtime = createWebRuntime(options.projectDir, options.dataDir ?? "", () => void 0);
|
|
2520
|
+
try {
|
|
2521
|
+
console.log(JSON.stringify(await inspectWebProject(runtime, options.projectDir, host)));
|
|
2522
|
+
} finally {
|
|
2523
|
+
runtime.dispose();
|
|
2524
|
+
input?.close();
|
|
2525
|
+
process.stdin.pause();
|
|
2526
|
+
}
|
|
2527
|
+
return 0;
|
|
2528
|
+
}
|
|
1738
2529
|
const app = await startWebServer({ ...options, host }).catch((error) => {
|
|
1739
2530
|
input?.close();
|
|
1740
2531
|
throw error;
|
|
@@ -1751,7 +2542,7 @@ async function main(argv) {
|
|
|
1751
2542
|
};
|
|
1752
2543
|
process.once("SIGINT", close);
|
|
1753
2544
|
process.once("SIGTERM", close);
|
|
1754
|
-
console.log(options.json ? JSON.stringify({ type: "ready", protocolVersion: 1, hostProtocolVersion: 1, url: app.url, project: { id: app.project.id, name: app.project.name, projectType: app.project.projectType }, runtime: app.project.runtime }) : `Workflow Web: ${app.url}`);
|
|
2545
|
+
console.log(options.json ? JSON.stringify({ type: "ready", protocolVersion: 1, hostProtocolVersion: 1, assistantProtocolVersion: host?.assistant ? ASSISTANT_PROTOCOL_VERSION : void 0, url: app.url, project: { id: app.project.id, name: app.project.name, projectType: app.project.projectType }, runtime: app.project.runtime }) : `Workflow Web: ${app.url}`);
|
|
1755
2546
|
if (lines) {
|
|
1756
2547
|
void (async () => {
|
|
1757
2548
|
try {
|