@fastgpt-plugin/cli 0.1.0-beta.8 → 0.2.0-alpha.1
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.en.md +71 -0
- package/README.md +41 -4
- package/dist/index.js +1311 -95
- package/package.json +6 -3
- package/templates/tool/index.tool.ts +11 -3
- package/templates/tool/index.toolset.ts +39 -10
package/dist/index.js
CHANGED
|
@@ -8,8 +8,12 @@ import { build } from "tsdown";
|
|
|
8
8
|
import z from "zod";
|
|
9
9
|
import { input, select } from "@inquirer/prompts";
|
|
10
10
|
import { kebabCase } from "es-toolkit";
|
|
11
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
12
11
|
import fs$1, { createWriteStream } from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
14
|
+
import { Box, Text, render, useInput } from "ink";
|
|
15
|
+
import React from "react";
|
|
16
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
17
|
import { Readable } from "stream";
|
|
14
18
|
import { Readable as Readable$1 } from "node:stream";
|
|
15
19
|
import { ZipFile } from "yazl";
|
|
@@ -48,27 +52,6 @@ const PluginPermissionEnumSchema = z.enum([
|
|
|
48
52
|
]);
|
|
49
53
|
const PluginPermissionEnum = PluginPermissionEnumSchema.enum;
|
|
50
54
|
|
|
51
|
-
//#endregion
|
|
52
|
-
//#region ../../packages/domain/src/value-objects/plugin.vo.ts
|
|
53
|
-
const PluginUniqueIdSchema = z.object({
|
|
54
|
-
pluginId: z.string(),
|
|
55
|
-
version: z.string(),
|
|
56
|
-
etag: z.string()
|
|
57
|
-
});
|
|
58
|
-
const PluginSourceSchema = z.literal("system").or(z.string());
|
|
59
|
-
const PluginTagListItemSchema = z.object({
|
|
60
|
-
id: z.string(),
|
|
61
|
-
name: I18nStringStrictSchema
|
|
62
|
-
});
|
|
63
|
-
const PluginTagListSchema = z.array(PluginTagListItemSchema);
|
|
64
|
-
const PluginRuntimeModeSchema = z.enum(["localPool", "serverless"]);
|
|
65
|
-
const PluginRuntimeModeEnum = PluginRuntimeModeSchema.enum;
|
|
66
|
-
const UserPluginIdSchema = z.object({
|
|
67
|
-
pluginId: z.string(),
|
|
68
|
-
version: z.string().optional(),
|
|
69
|
-
source: PluginSourceSchema.optional()
|
|
70
|
-
});
|
|
71
|
-
|
|
72
55
|
//#endregion
|
|
73
56
|
//#region ../../packages/domain/src/entities/plugin-base.entity.ts
|
|
74
57
|
const PluginTagSchema = z.enum([
|
|
@@ -543,7 +526,7 @@ var CheckCommand = class extends BaseCommand {
|
|
|
543
526
|
//#endregion
|
|
544
527
|
//#region package.json
|
|
545
528
|
var name = "@fastgpt-plugin/cli";
|
|
546
|
-
var version = "0.
|
|
529
|
+
var version = "0.2.0-alpha.1";
|
|
547
530
|
|
|
548
531
|
//#endregion
|
|
549
532
|
//#region src/constants.ts
|
|
@@ -703,6 +686,301 @@ var CreateCommand = class extends BaseCommand {
|
|
|
703
686
|
}
|
|
704
687
|
};
|
|
705
688
|
|
|
689
|
+
//#endregion
|
|
690
|
+
//#region src/debug/discover.ts
|
|
691
|
+
const IGNORED_DIRS = new Set([
|
|
692
|
+
".build",
|
|
693
|
+
".fastgpt-plugin-debug",
|
|
694
|
+
".git",
|
|
695
|
+
".turbo",
|
|
696
|
+
".vite",
|
|
697
|
+
"coverage",
|
|
698
|
+
"dist",
|
|
699
|
+
"node_modules",
|
|
700
|
+
"__pack_output__"
|
|
701
|
+
]);
|
|
702
|
+
async function discoverDebugEntries(cwd = process.cwd()) {
|
|
703
|
+
const root = path.resolve(cwd);
|
|
704
|
+
if (await hasIndexFile(root)) return [root];
|
|
705
|
+
const children = await fs.readdir(root, { withFileTypes: true });
|
|
706
|
+
const entries = [];
|
|
707
|
+
for (const child of children) {
|
|
708
|
+
if (!child.isDirectory() || IGNORED_DIRS.has(child.name)) continue;
|
|
709
|
+
const candidate = path.join(root, child.name);
|
|
710
|
+
if (await hasIndexFile(candidate)) entries.push(candidate);
|
|
711
|
+
}
|
|
712
|
+
entries.sort();
|
|
713
|
+
if (entries.length === 0) throw new Error(`当前目录未发现可调试插件,请传入插件目录或在当前目录下放置 index.ts。`);
|
|
714
|
+
return entries;
|
|
715
|
+
}
|
|
716
|
+
async function hasIndexFile(dir) {
|
|
717
|
+
try {
|
|
718
|
+
return (await fs.stat(path.join(dir, "index.ts"))).isFile();
|
|
719
|
+
} catch {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
//#endregion
|
|
725
|
+
//#region ../../packages/domain/src/value-objects/connection-gateway.vo.ts
|
|
726
|
+
const ConnectionGatewayConsumerTypeSchema = z.string().min(1).max(64);
|
|
727
|
+
const ConnectionGatewayTransportSchema = z.literal("websocket");
|
|
728
|
+
const ConnectionGatewayCapabilitySchema = z.string().min(1).max(64);
|
|
729
|
+
const ConnectionGatewaySessionScopeSchema = z.object({
|
|
730
|
+
userId: z.string().min(1),
|
|
731
|
+
teamId: z.string().min(1).optional(),
|
|
732
|
+
source: z.string().min(1).optional(),
|
|
733
|
+
sources: z.array(z.string().min(1)).optional()
|
|
734
|
+
});
|
|
735
|
+
const ConnectionGatewayTokenClaimsSchema = z.object({
|
|
736
|
+
consumerType: ConnectionGatewayConsumerTypeSchema,
|
|
737
|
+
subject: z.string().min(1),
|
|
738
|
+
sessionScope: ConnectionGatewaySessionScopeSchema,
|
|
739
|
+
transport: ConnectionGatewayTransportSchema,
|
|
740
|
+
capabilities: z.array(ConnectionGatewayCapabilitySchema).default([]),
|
|
741
|
+
expiresAt: z.number().int().positive(),
|
|
742
|
+
issuedAt: z.number().int().positive().optional(),
|
|
743
|
+
nonce: z.string().min(1).optional()
|
|
744
|
+
});
|
|
745
|
+
const ConnectionGatewayEnvelopeSchema = z.object({
|
|
746
|
+
protocol: z.literal("connection-gateway.v1"),
|
|
747
|
+
sessionId: z.string().min(1),
|
|
748
|
+
generation: z.number().int().nonnegative(),
|
|
749
|
+
requestId: z.string().min(1).optional(),
|
|
750
|
+
type: z.enum([
|
|
751
|
+
"request",
|
|
752
|
+
"response",
|
|
753
|
+
"event",
|
|
754
|
+
"stream"
|
|
755
|
+
]),
|
|
756
|
+
consumerType: ConnectionGatewayConsumerTypeSchema,
|
|
757
|
+
capability: ConnectionGatewayCapabilitySchema.optional(),
|
|
758
|
+
traceId: z.string().optional(),
|
|
759
|
+
payload: z.unknown().optional(),
|
|
760
|
+
createdAt: z.number().int().positive()
|
|
761
|
+
});
|
|
762
|
+
const ConnectionGatewayWsProtocolSchema = z.literal("connection-gateway.ws.v1");
|
|
763
|
+
const ConnectionGatewayWsBindMessageSchema = z.object({
|
|
764
|
+
protocol: ConnectionGatewayWsProtocolSchema,
|
|
765
|
+
type: z.literal("bind"),
|
|
766
|
+
requestId: z.string().min(1),
|
|
767
|
+
token: z.string().min(1),
|
|
768
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
769
|
+
});
|
|
770
|
+
const ConnectionGatewayWsEnvelopeMessageSchema = z.object({
|
|
771
|
+
protocol: ConnectionGatewayWsProtocolSchema,
|
|
772
|
+
type: z.literal("envelope"),
|
|
773
|
+
envelope: ConnectionGatewayEnvelopeSchema
|
|
774
|
+
});
|
|
775
|
+
const ConnectionGatewayWsHeartbeatMessageSchema = z.object({
|
|
776
|
+
protocol: ConnectionGatewayWsProtocolSchema,
|
|
777
|
+
type: z.literal("heartbeat"),
|
|
778
|
+
ts: z.number().int().positive()
|
|
779
|
+
});
|
|
780
|
+
const ConnectionGatewayWsErrorMessageSchema = z.object({
|
|
781
|
+
protocol: ConnectionGatewayWsProtocolSchema,
|
|
782
|
+
type: z.literal("error"),
|
|
783
|
+
requestId: z.string().min(1).optional(),
|
|
784
|
+
code: z.string().min(1),
|
|
785
|
+
message: z.string().min(1)
|
|
786
|
+
});
|
|
787
|
+
const ConnectionGatewayWsBoundMessageSchema = z.object({
|
|
788
|
+
protocol: ConnectionGatewayWsProtocolSchema,
|
|
789
|
+
type: z.literal("bound"),
|
|
790
|
+
requestId: z.string().min(1),
|
|
791
|
+
session: z.lazy(() => ConnectionGatewaySessionSchema)
|
|
792
|
+
});
|
|
793
|
+
const ConnectionGatewayWsClientMessageSchema = z.discriminatedUnion("type", [
|
|
794
|
+
ConnectionGatewayWsBindMessageSchema,
|
|
795
|
+
ConnectionGatewayWsEnvelopeMessageSchema,
|
|
796
|
+
ConnectionGatewayWsHeartbeatMessageSchema
|
|
797
|
+
]);
|
|
798
|
+
const ConnectionGatewayWsServerMessageSchema = z.discriminatedUnion("type", [
|
|
799
|
+
ConnectionGatewayWsBoundMessageSchema,
|
|
800
|
+
ConnectionGatewayWsEnvelopeMessageSchema,
|
|
801
|
+
ConnectionGatewayWsHeartbeatMessageSchema,
|
|
802
|
+
ConnectionGatewayWsErrorMessageSchema
|
|
803
|
+
]);
|
|
804
|
+
const ConnectionGatewaySessionStatusSchema = z.enum([
|
|
805
|
+
"connecting",
|
|
806
|
+
"connected",
|
|
807
|
+
"draining",
|
|
808
|
+
"closed"
|
|
809
|
+
]);
|
|
810
|
+
const ConnectionGatewaySessionSchema = z.object({
|
|
811
|
+
id: z.string().min(1),
|
|
812
|
+
consumerType: ConnectionGatewayConsumerTypeSchema,
|
|
813
|
+
subject: z.string().min(1),
|
|
814
|
+
sessionScope: ConnectionGatewaySessionScopeSchema,
|
|
815
|
+
transport: ConnectionGatewayTransportSchema,
|
|
816
|
+
capabilities: z.array(ConnectionGatewayCapabilitySchema),
|
|
817
|
+
generation: z.number().int().nonnegative(),
|
|
818
|
+
ownerNodeId: z.string().min(1),
|
|
819
|
+
status: ConnectionGatewaySessionStatusSchema,
|
|
820
|
+
connectedAt: z.number().int().positive(),
|
|
821
|
+
lastSeenAt: z.number().int().positive(),
|
|
822
|
+
expiresAt: z.number().int().positive(),
|
|
823
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
824
|
+
});
|
|
825
|
+
const ConnectionGatewaySessionStatusViewSchema = z.object({
|
|
826
|
+
session: ConnectionGatewaySessionSchema.nullable(),
|
|
827
|
+
ownerAlive: z.boolean(),
|
|
828
|
+
mailboxLag: z.number().int().nonnegative(),
|
|
829
|
+
logs: z.array(z.object({
|
|
830
|
+
ts: z.number().int().positive(),
|
|
831
|
+
level: z.enum([
|
|
832
|
+
"debug",
|
|
833
|
+
"info",
|
|
834
|
+
"warn",
|
|
835
|
+
"error"
|
|
836
|
+
]),
|
|
837
|
+
message: z.string(),
|
|
838
|
+
data: z.record(z.string(), z.unknown()).optional()
|
|
839
|
+
}))
|
|
840
|
+
});
|
|
841
|
+
const ConnectionGatewayMetricsSchema = z.object({
|
|
842
|
+
nodeId: z.string(),
|
|
843
|
+
activeConnections: z.number().int().nonnegative(),
|
|
844
|
+
activeSessions: z.number().int().nonnegative(),
|
|
845
|
+
inFlightRequests: z.number().int().nonnegative(),
|
|
846
|
+
streamBufferBytes: z.number().int().nonnegative(),
|
|
847
|
+
slowConsumers: z.number().int().nonnegative(),
|
|
848
|
+
ownerLeaseExpiries: z.number().int().nonnegative(),
|
|
849
|
+
mailbox: z.object({
|
|
850
|
+
lag: z.number().int().nonnegative(),
|
|
851
|
+
redisRoundTripMs: z.number().nonnegative()
|
|
852
|
+
}),
|
|
853
|
+
limits: z.object({
|
|
854
|
+
maxConnections: z.number().int().positive(),
|
|
855
|
+
maxSessionsPerSubject: z.number().int().positive(),
|
|
856
|
+
maxInFlightPerSession: z.number().int().positive(),
|
|
857
|
+
maxEnvelopeBytes: z.number().int().positive()
|
|
858
|
+
})
|
|
859
|
+
});
|
|
860
|
+
const ConnectionGatewayResourceLimitsSchema = z.object({
|
|
861
|
+
maxConnections: z.number().int().positive(),
|
|
862
|
+
maxSessionsPerSubject: z.number().int().positive(),
|
|
863
|
+
maxInFlightPerSession: z.number().int().positive(),
|
|
864
|
+
maxEnvelopeBytes: z.number().int().positive(),
|
|
865
|
+
slowConsumerBufferBytes: z.number().int().positive()
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
//#endregion
|
|
869
|
+
//#region ../../packages/domain/src/value-objects/system-var.vo.ts
|
|
870
|
+
const SystemVarSchema = z.object({
|
|
871
|
+
app: z.object({
|
|
872
|
+
id: z.string(),
|
|
873
|
+
name: z.string()
|
|
874
|
+
}),
|
|
875
|
+
chat: z.object({
|
|
876
|
+
chatId: z.string(),
|
|
877
|
+
uid: z.string().optional()
|
|
878
|
+
}),
|
|
879
|
+
invokeToken: z.string(),
|
|
880
|
+
time: z.string()
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
//#endregion
|
|
884
|
+
//#region ../../packages/domain/src/value-objects/plugin.vo.ts
|
|
885
|
+
const PluginUniqueIdSchema = z.object({
|
|
886
|
+
pluginId: z.string(),
|
|
887
|
+
version: z.string(),
|
|
888
|
+
etag: z.string()
|
|
889
|
+
});
|
|
890
|
+
const PluginSourceSchema = z.literal("system").or(z.string());
|
|
891
|
+
const PluginTagListItemSchema = z.object({
|
|
892
|
+
id: z.string(),
|
|
893
|
+
name: I18nStringStrictSchema
|
|
894
|
+
});
|
|
895
|
+
const PluginTagListSchema = z.array(PluginTagListItemSchema);
|
|
896
|
+
const PluginRuntimeModeSchema = z.enum(["localPool", "serverless"]);
|
|
897
|
+
const PluginRuntimeModeEnum = PluginRuntimeModeSchema.enum;
|
|
898
|
+
const UserPluginIdSchema = z.object({
|
|
899
|
+
pluginId: z.string(),
|
|
900
|
+
version: z.string().optional(),
|
|
901
|
+
source: PluginSourceSchema.optional()
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
//#endregion
|
|
905
|
+
//#region ../../packages/domain/src/value-objects/tool.vo.ts
|
|
906
|
+
const ToolHandlerReturnSchema = z.record(z.string(), z.unknown());
|
|
907
|
+
const StreamMessageTypeSchema = z.enum([
|
|
908
|
+
"response",
|
|
909
|
+
"error",
|
|
910
|
+
"stream"
|
|
911
|
+
]);
|
|
912
|
+
const StreamMessageTypeEnum = StreamMessageTypeSchema.enum;
|
|
913
|
+
const StreamDataAnswerTypeSchema = z.enum(["answer", "fastAnswer"]);
|
|
914
|
+
const StreamDataAnswerTypeEnum = StreamDataAnswerTypeSchema.enum;
|
|
915
|
+
const ToolAnswerSchema = z.object({
|
|
916
|
+
type: StreamDataAnswerTypeSchema,
|
|
917
|
+
content: z.string()
|
|
918
|
+
});
|
|
919
|
+
const ToolStreamMessageSchema = z.discriminatedUnion("type", [
|
|
920
|
+
z.object({
|
|
921
|
+
type: z.literal(StreamMessageTypeEnum.response),
|
|
922
|
+
data: ToolHandlerReturnSchema
|
|
923
|
+
}),
|
|
924
|
+
z.object({
|
|
925
|
+
type: z.literal(StreamMessageTypeEnum.stream),
|
|
926
|
+
data: ToolAnswerSchema
|
|
927
|
+
}),
|
|
928
|
+
z.object({
|
|
929
|
+
type: z.literal(StreamMessageTypeEnum.error),
|
|
930
|
+
data: z.string()
|
|
931
|
+
})
|
|
932
|
+
]);
|
|
933
|
+
const ToolRunInputSchema = z.object({
|
|
934
|
+
pluginId: z.string(),
|
|
935
|
+
version: z.preprocess((value) => {
|
|
936
|
+
if (value === "") return void 0;
|
|
937
|
+
return value;
|
|
938
|
+
}, z.string().optional()),
|
|
939
|
+
source: PluginSourceSchema.optional(),
|
|
940
|
+
childId: z.string().optional(),
|
|
941
|
+
input: z.record(z.string(), z.unknown()),
|
|
942
|
+
secrets: z.record(z.string(), z.unknown()).optional(),
|
|
943
|
+
systemVar: SystemVarSchema
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
//#endregion
|
|
947
|
+
//#region ../../packages/domain/src/value-objects/connection-gateway-debug.vo.ts
|
|
948
|
+
const CONNECTION_GATEWAY_PLUGIN_DEBUG_INVOKE_CAPABILITY = "invoke";
|
|
949
|
+
const ConnectionGatewayPluginDebugRequestPayloadSchema = z.object({
|
|
950
|
+
kind: z.literal("plugin-debug.run"),
|
|
951
|
+
eventName: z.literal("run"),
|
|
952
|
+
payload: z.object({
|
|
953
|
+
pluginId: z.string().min(1).optional(),
|
|
954
|
+
input: z.record(z.string(), z.unknown()),
|
|
955
|
+
secrets: z.record(z.string(), z.unknown()).optional(),
|
|
956
|
+
systemVar: SystemVarSchema,
|
|
957
|
+
source: z.string().optional(),
|
|
958
|
+
childId: z.string().optional()
|
|
959
|
+
})
|
|
960
|
+
});
|
|
961
|
+
const ConnectionGatewayPluginDebugResponsePayloadSchema = z.discriminatedUnion("kind", [z.object({ kind: z.literal("plugin-debug.accepted") }), z.object({
|
|
962
|
+
kind: z.literal("plugin-debug.error"),
|
|
963
|
+
message: z.string(),
|
|
964
|
+
code: z.string().optional(),
|
|
965
|
+
data: z.unknown().optional()
|
|
966
|
+
})]);
|
|
967
|
+
const ConnectionGatewayPluginDebugStreamPayloadSchema = z.discriminatedUnion("event", [
|
|
968
|
+
z.object({
|
|
969
|
+
kind: z.literal("plugin-debug.stream"),
|
|
970
|
+
event: z.literal("chunk"),
|
|
971
|
+
data: ToolStreamMessageSchema
|
|
972
|
+
}),
|
|
973
|
+
z.object({
|
|
974
|
+
kind: z.literal("plugin-debug.stream"),
|
|
975
|
+
event: z.literal("end")
|
|
976
|
+
}),
|
|
977
|
+
z.object({
|
|
978
|
+
kind: z.literal("plugin-debug.stream"),
|
|
979
|
+
event: z.literal("error"),
|
|
980
|
+
message: z.string()
|
|
981
|
+
})
|
|
982
|
+
]);
|
|
983
|
+
|
|
706
984
|
//#endregion
|
|
707
985
|
//#region src/debug/import-hooks.ts
|
|
708
986
|
const SOURCE_EXTENSIONS = [
|
|
@@ -892,7 +1170,10 @@ const InvokeUploadFileInputSchema = z.object({ ...FileCreateSchema.omit({
|
|
|
892
1170
|
overwrite: true,
|
|
893
1171
|
path: true
|
|
894
1172
|
}).shape });
|
|
895
|
-
const InvokeUploadFileOutputSchema = z.object({
|
|
1173
|
+
const InvokeUploadFileOutputSchema = z.object({
|
|
1174
|
+
...FileMetaSchema.omit({ fileKey: true }).partial().shape,
|
|
1175
|
+
accessURL: z.string()
|
|
1176
|
+
});
|
|
896
1177
|
const InvokeUserInfoOutputSchema = z.object({
|
|
897
1178
|
username: z.string(),
|
|
898
1179
|
contact: z.string().nullish(),
|
|
@@ -903,9 +1184,17 @@ const InvokeUserInfoOutputSchema = z.object({
|
|
|
903
1184
|
})),
|
|
904
1185
|
groups: z.array(z.object({ name: z.string() }))
|
|
905
1186
|
});
|
|
906
|
-
const InvokeMethodEnumSchema = z.enum([
|
|
1187
|
+
const InvokeMethodEnumSchema = z.enum([
|
|
1188
|
+
"uploadFile",
|
|
1189
|
+
"userInfo",
|
|
1190
|
+
"wecomCorpToken"
|
|
1191
|
+
]);
|
|
907
1192
|
const InvokeMethodEnum = InvokeMethodEnumSchema.enum;
|
|
908
1193
|
|
|
1194
|
+
//#endregion
|
|
1195
|
+
//#region ../../packages/domain/src/value-objects/result.vo.ts
|
|
1196
|
+
const successResult = (data) => [data, null];
|
|
1197
|
+
|
|
909
1198
|
//#endregion
|
|
910
1199
|
//#region ../../packages/domain/src/value-objects/stream.vo.ts
|
|
911
1200
|
var StreamData = class StreamData {
|
|
@@ -966,63 +1255,6 @@ var StreamData = class StreamData {
|
|
|
966
1255
|
}
|
|
967
1256
|
};
|
|
968
1257
|
|
|
969
|
-
//#endregion
|
|
970
|
-
//#region ../../packages/domain/src/value-objects/system-var.vo.ts
|
|
971
|
-
const SystemVarSchema = z.object({
|
|
972
|
-
app: z.object({
|
|
973
|
-
id: z.string(),
|
|
974
|
-
name: z.string()
|
|
975
|
-
}),
|
|
976
|
-
chat: z.object({
|
|
977
|
-
chatId: z.string(),
|
|
978
|
-
uid: z.string().optional()
|
|
979
|
-
}),
|
|
980
|
-
invokeToken: z.string(),
|
|
981
|
-
time: z.string()
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
//#endregion
|
|
985
|
-
//#region ../../packages/domain/src/value-objects/tool.vo.ts
|
|
986
|
-
const ToolHandlerReturnSchema = z.record(z.string(), z.unknown());
|
|
987
|
-
const StreamMessageTypeSchema = z.enum([
|
|
988
|
-
"response",
|
|
989
|
-
"error",
|
|
990
|
-
"stream"
|
|
991
|
-
]);
|
|
992
|
-
const StreamMessageTypeEnum = StreamMessageTypeSchema.enum;
|
|
993
|
-
const StreamDataAnswerTypeSchema = z.enum(["answer", "fastAnswer"]);
|
|
994
|
-
const StreamDataAnswerTypeEnum = StreamDataAnswerTypeSchema.enum;
|
|
995
|
-
const ToolAnswerSchema = z.object({
|
|
996
|
-
type: StreamDataAnswerTypeSchema,
|
|
997
|
-
content: z.string()
|
|
998
|
-
});
|
|
999
|
-
const ToolStreamMessageSchema = z.discriminatedUnion("type", [
|
|
1000
|
-
z.object({
|
|
1001
|
-
type: z.literal(StreamMessageTypeEnum.response),
|
|
1002
|
-
data: ToolHandlerReturnSchema
|
|
1003
|
-
}),
|
|
1004
|
-
z.object({
|
|
1005
|
-
type: z.literal(StreamMessageTypeEnum.stream),
|
|
1006
|
-
data: ToolAnswerSchema
|
|
1007
|
-
}),
|
|
1008
|
-
z.object({
|
|
1009
|
-
type: z.literal(StreamMessageTypeEnum.error),
|
|
1010
|
-
data: z.string()
|
|
1011
|
-
})
|
|
1012
|
-
]);
|
|
1013
|
-
const ToolRunInputSchema = z.object({
|
|
1014
|
-
pluginId: z.string(),
|
|
1015
|
-
version: z.preprocess((value) => {
|
|
1016
|
-
if (value === "") return void 0;
|
|
1017
|
-
return value;
|
|
1018
|
-
}, z.string().optional()),
|
|
1019
|
-
source: PluginSourceSchema.optional(),
|
|
1020
|
-
childId: z.string().optional(),
|
|
1021
|
-
input: z.record(z.string(), z.unknown()),
|
|
1022
|
-
secrets: z.record(z.string(), z.unknown()).optional(),
|
|
1023
|
-
systemVar: SystemVarSchema
|
|
1024
|
-
});
|
|
1025
|
-
|
|
1026
1258
|
//#endregion
|
|
1027
1259
|
//#region ../../packages/infrastructure/src/plugin/plugin-runtime/ports/channel/event/client.ts
|
|
1028
1260
|
/**
|
|
@@ -1160,7 +1392,9 @@ const PluginChannelMessageIdSchema = z.union([z.string(), z.number()]);
|
|
|
1160
1392
|
const PluginChannelErrorSchema = z.object({
|
|
1161
1393
|
code: z.string(),
|
|
1162
1394
|
message: z.string(),
|
|
1163
|
-
|
|
1395
|
+
reason: I18nStringSchema.optional(),
|
|
1396
|
+
data: z.unknown().optional(),
|
|
1397
|
+
cause: z.lazy(() => PluginChannelErrorSchema).optional()
|
|
1164
1398
|
});
|
|
1165
1399
|
/**
|
|
1166
1400
|
* request message:需要对端返回 success/error。
|
|
@@ -1456,7 +1690,7 @@ function createIncomingStream({ source, streamName, streamId, traceId, meta }) {
|
|
|
1456
1690
|
};
|
|
1457
1691
|
}
|
|
1458
1692
|
function toStreamData(source) {
|
|
1459
|
-
if (source
|
|
1693
|
+
if (isStreamDataLike$1(source)) return source;
|
|
1460
1694
|
const output = StreamData.create();
|
|
1461
1695
|
(async () => {
|
|
1462
1696
|
try {
|
|
@@ -1468,6 +1702,9 @@ function toStreamData(source) {
|
|
|
1468
1702
|
})();
|
|
1469
1703
|
return output;
|
|
1470
1704
|
}
|
|
1705
|
+
function isStreamDataLike$1(source) {
|
|
1706
|
+
return source instanceof StreamData || typeof source === "object" && source !== null && typeof source.values === "function" && typeof source.consume === "function";
|
|
1707
|
+
}
|
|
1471
1708
|
|
|
1472
1709
|
//#endregion
|
|
1473
1710
|
//#region src/debug/session.ts
|
|
@@ -1616,14 +1853,14 @@ function createVirtualHostHandler(uploadDir) {
|
|
|
1616
1853
|
const outputPath = path.join(saveDir, `${Date.now()}-${randomUUID().slice(0, 8)}-${fileName}`);
|
|
1617
1854
|
await fs.mkdir(saveDir, { recursive: true });
|
|
1618
1855
|
await fs.writeFile(outputPath, buffer);
|
|
1619
|
-
return {
|
|
1856
|
+
return successResult({
|
|
1620
1857
|
fileName,
|
|
1621
1858
|
contentType,
|
|
1622
1859
|
size: buffer.length,
|
|
1623
1860
|
etag: createHash("md5").update(buffer).digest("hex"),
|
|
1624
1861
|
createTime: /* @__PURE__ */ new Date(),
|
|
1625
1862
|
accessURL: pathToFileURL(outputPath).href
|
|
1626
|
-
};
|
|
1863
|
+
});
|
|
1627
1864
|
}
|
|
1628
1865
|
default: throw new Error(`本地虚拟环境暂不支持反向调用: ${String(method)}`);
|
|
1629
1866
|
}
|
|
@@ -1632,10 +1869,13 @@ function createVirtualHostHandler(uploadDir) {
|
|
|
1632
1869
|
async function readSourceToBuffer(source) {
|
|
1633
1870
|
if (!source) return Buffer.alloc(0);
|
|
1634
1871
|
const chunks = [];
|
|
1635
|
-
const iterable = source
|
|
1872
|
+
const iterable = isStreamDataLike(source) ? source.values() : source;
|
|
1636
1873
|
for await (const chunk of iterable) chunks.push(toBuffer(chunk));
|
|
1637
1874
|
return Buffer.concat(chunks);
|
|
1638
1875
|
}
|
|
1876
|
+
function isStreamDataLike(source) {
|
|
1877
|
+
return source instanceof StreamData || typeof source === "object" && source !== null && typeof source.values === "function" && typeof source.consume === "function";
|
|
1878
|
+
}
|
|
1639
1879
|
function toBuffer(value) {
|
|
1640
1880
|
if (Buffer.isBuffer(value)) return value;
|
|
1641
1881
|
if (value instanceof Uint8Array) return Buffer.from(value);
|
|
@@ -1703,22 +1943,960 @@ async function assertPathExists(targetPath, message) {
|
|
|
1703
1943
|
}
|
|
1704
1944
|
}
|
|
1705
1945
|
|
|
1946
|
+
//#endregion
|
|
1947
|
+
//#region src/debug/gateway.ts
|
|
1948
|
+
async function connectDebugGateway({ targets, options, onLog }) {
|
|
1949
|
+
if (targets.length === 0) throw new Error("Gateway debug targets cannot be empty");
|
|
1950
|
+
if (options.reconnect) return connectReconnectingDebugGateway({
|
|
1951
|
+
targets,
|
|
1952
|
+
options,
|
|
1953
|
+
onLog
|
|
1954
|
+
});
|
|
1955
|
+
return connectSingleDebugGateway({
|
|
1956
|
+
targets,
|
|
1957
|
+
options,
|
|
1958
|
+
onLog
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
async function connectReconnectingDebugGateway({ targets, options, onLog }) {
|
|
1962
|
+
let closed = false;
|
|
1963
|
+
let current = null;
|
|
1964
|
+
let currentSession = null;
|
|
1965
|
+
let latestTargets = targets;
|
|
1966
|
+
const intervalMs = Math.max(100, options.reconnectIntervalMs ?? 2e3);
|
|
1967
|
+
const closedPromise = (async () => {
|
|
1968
|
+
while (!closed) {
|
|
1969
|
+
try {
|
|
1970
|
+
current = await connectSingleDebugGateway({
|
|
1971
|
+
targets: latestTargets,
|
|
1972
|
+
options,
|
|
1973
|
+
onLog
|
|
1974
|
+
});
|
|
1975
|
+
currentSession = current.session;
|
|
1976
|
+
await current.closed;
|
|
1977
|
+
} catch (error) {
|
|
1978
|
+
if (closed) return;
|
|
1979
|
+
onLog?.(`Connection Gateway 调试通道断开: ${formatErrorMessage(error)}`);
|
|
1980
|
+
} finally {
|
|
1981
|
+
current = null;
|
|
1982
|
+
}
|
|
1983
|
+
if (!closed) {
|
|
1984
|
+
onLog?.(`将在 ${intervalMs}ms 后重连 Connection Gateway。`);
|
|
1985
|
+
await delay(intervalMs);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
})();
|
|
1989
|
+
while (!currentSession && !closed) await delay(10);
|
|
1990
|
+
if (!currentSession) throw new Error("Connection Gateway debug session was closed before connecting");
|
|
1991
|
+
return {
|
|
1992
|
+
session: currentSession,
|
|
1993
|
+
updateTargets(targets) {
|
|
1994
|
+
latestTargets = targets;
|
|
1995
|
+
current?.updateTargets(targets);
|
|
1996
|
+
},
|
|
1997
|
+
close() {
|
|
1998
|
+
closed = true;
|
|
1999
|
+
current?.close();
|
|
2000
|
+
},
|
|
2001
|
+
closed: closedPromise
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
async function connectSingleDebugGateway({ targets, options, onLog }) {
|
|
2005
|
+
const socket = await openWebSocket(options.gatewayUrl);
|
|
2006
|
+
let targetsByPluginId = createTargetsByPluginId(targets);
|
|
2007
|
+
let closed = false;
|
|
2008
|
+
let session = null;
|
|
2009
|
+
let boundResolve;
|
|
2010
|
+
let boundReject;
|
|
2011
|
+
const bound = new Promise((resolve, reject) => {
|
|
2012
|
+
boundResolve = resolve;
|
|
2013
|
+
boundReject = reject;
|
|
2014
|
+
});
|
|
2015
|
+
const closedPromise = new Promise((resolve) => {
|
|
2016
|
+
socket.addEventListener("close", () => {
|
|
2017
|
+
closed = true;
|
|
2018
|
+
resolve();
|
|
2019
|
+
});
|
|
2020
|
+
socket.addEventListener("error", () => {
|
|
2021
|
+
onLog?.("Connection Gateway WebSocket 错误");
|
|
2022
|
+
});
|
|
2023
|
+
});
|
|
2024
|
+
socket.addEventListener("message", (event) => {
|
|
2025
|
+
const message = parseWsServerMessage(event.data);
|
|
2026
|
+
if (message.type === "bound") {
|
|
2027
|
+
session = message.session;
|
|
2028
|
+
boundResolve(message.session);
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
if (message.type === "heartbeat") return;
|
|
2032
|
+
if (message.type === "error") {
|
|
2033
|
+
const error = new Error(message.message);
|
|
2034
|
+
if (!session) boundReject(error);
|
|
2035
|
+
onLog?.(`Connection Gateway 错误: ${message.code} ${message.message}`);
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
2038
|
+
if (message.type === "envelope") {
|
|
2039
|
+
const envelope = message.envelope;
|
|
2040
|
+
if (!session) {
|
|
2041
|
+
boundReject(/* @__PURE__ */ new Error("Gateway envelope received before session bound"));
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
handleGatewayEnvelope({
|
|
2045
|
+
socket,
|
|
2046
|
+
session,
|
|
2047
|
+
targetsByPluginId,
|
|
2048
|
+
envelope,
|
|
2049
|
+
onLog
|
|
2050
|
+
}).catch(async (error) => {
|
|
2051
|
+
if (session) await sendEnvelope(socket, makeErrorEnvelope(session, envelope, error));
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
});
|
|
2055
|
+
await sendMessage(socket, {
|
|
2056
|
+
protocol: "connection-gateway.ws.v1",
|
|
2057
|
+
type: "bind",
|
|
2058
|
+
requestId: randomUUID(),
|
|
2059
|
+
token: options.connectToken,
|
|
2060
|
+
metadata: makePluginDebugMetadata(targets, options.source)
|
|
2061
|
+
}).catch(async (error) => {
|
|
2062
|
+
socket.close();
|
|
2063
|
+
throw error;
|
|
2064
|
+
});
|
|
2065
|
+
const boundSession = await bound;
|
|
2066
|
+
onLog?.(`已连接 Connection Gateway session: ${boundSession.id}`);
|
|
2067
|
+
return {
|
|
2068
|
+
session: boundSession,
|
|
2069
|
+
updateTargets(targets) {
|
|
2070
|
+
targetsByPluginId = createTargetsByPluginId(targets);
|
|
2071
|
+
onLog?.(`已热更新本地调试插件: ${targets.map((target) => target.snapshot.pluginId).join(", ")}`);
|
|
2072
|
+
},
|
|
2073
|
+
close() {
|
|
2074
|
+
if (!closed) socket.close();
|
|
2075
|
+
},
|
|
2076
|
+
closed: closedPromise
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
function createTargetsByPluginId(targets) {
|
|
2080
|
+
return new Map(targets.map((target) => [target.snapshot.pluginId, target]));
|
|
2081
|
+
}
|
|
2082
|
+
async function handleGatewayEnvelope({ socket, session, targetsByPluginId, envelope, onLog }) {
|
|
2083
|
+
if (envelope.type !== "request") return;
|
|
2084
|
+
const request = ConnectionGatewayPluginDebugRequestPayloadSchema.parse(envelope.payload);
|
|
2085
|
+
const source = request.payload.source ?? session.sessionScope.source;
|
|
2086
|
+
if (!source) throw new Error("Gateway debug request missing source");
|
|
2087
|
+
const pluginId = request.payload.pluginId;
|
|
2088
|
+
if (!pluginId) throw new Error("Gateway debug request missing pluginId");
|
|
2089
|
+
const target = targetsByPluginId.get(pluginId);
|
|
2090
|
+
if (!target) throw new Error(`Gateway debug target not found: ${source} ${pluginId}`);
|
|
2091
|
+
onLog?.(`收到远程调试请求: ${source} ${pluginId} ${envelope.requestId ?? "-"}`);
|
|
2092
|
+
await sendEnvelope(socket, {
|
|
2093
|
+
protocol: "connection-gateway.v1",
|
|
2094
|
+
sessionId: session.id,
|
|
2095
|
+
generation: session.generation,
|
|
2096
|
+
requestId: requiredRequestId(envelope),
|
|
2097
|
+
type: "response",
|
|
2098
|
+
consumerType: session.consumerType,
|
|
2099
|
+
capability: CONNECTION_GATEWAY_PLUGIN_DEBUG_INVOKE_CAPABILITY,
|
|
2100
|
+
traceId: envelope.traceId,
|
|
2101
|
+
createdAt: Date.now(),
|
|
2102
|
+
payload: { kind: "plugin-debug.accepted" }
|
|
2103
|
+
});
|
|
2104
|
+
const result = await runDebugTool({
|
|
2105
|
+
runtime: target.runtime,
|
|
2106
|
+
snapshot: target.snapshot,
|
|
2107
|
+
toolId: request.payload.childId,
|
|
2108
|
+
input: request.payload.input,
|
|
2109
|
+
secrets: request.payload.secrets,
|
|
2110
|
+
systemVar: request.payload.systemVar
|
|
2111
|
+
});
|
|
2112
|
+
for (const message of result.streamMessages) await sendStreamChunk(socket, session, envelope, message);
|
|
2113
|
+
await sendEnvelope(socket, {
|
|
2114
|
+
protocol: "connection-gateway.v1",
|
|
2115
|
+
sessionId: session.id,
|
|
2116
|
+
generation: session.generation,
|
|
2117
|
+
requestId: requiredRequestId(envelope),
|
|
2118
|
+
type: "stream",
|
|
2119
|
+
consumerType: session.consumerType,
|
|
2120
|
+
capability: CONNECTION_GATEWAY_PLUGIN_DEBUG_INVOKE_CAPABILITY,
|
|
2121
|
+
traceId: envelope.traceId,
|
|
2122
|
+
createdAt: Date.now(),
|
|
2123
|
+
payload: {
|
|
2124
|
+
kind: "plugin-debug.stream",
|
|
2125
|
+
event: "end"
|
|
2126
|
+
}
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
async function sendStreamChunk(socket, session, request, message) {
|
|
2130
|
+
await sendEnvelope(socket, {
|
|
2131
|
+
protocol: "connection-gateway.v1",
|
|
2132
|
+
sessionId: session.id,
|
|
2133
|
+
generation: session.generation,
|
|
2134
|
+
requestId: requiredRequestId(request),
|
|
2135
|
+
type: "stream",
|
|
2136
|
+
consumerType: session.consumerType,
|
|
2137
|
+
capability: CONNECTION_GATEWAY_PLUGIN_DEBUG_INVOKE_CAPABILITY,
|
|
2138
|
+
traceId: request.traceId,
|
|
2139
|
+
createdAt: Date.now(),
|
|
2140
|
+
payload: {
|
|
2141
|
+
kind: "plugin-debug.stream",
|
|
2142
|
+
event: "chunk",
|
|
2143
|
+
data: ToolStreamMessageSchema.parse(message)
|
|
2144
|
+
}
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
function makeErrorEnvelope(session, request, error) {
|
|
2148
|
+
return {
|
|
2149
|
+
protocol: "connection-gateway.v1",
|
|
2150
|
+
sessionId: session.id,
|
|
2151
|
+
generation: session.generation,
|
|
2152
|
+
requestId: request.requestId,
|
|
2153
|
+
type: "stream",
|
|
2154
|
+
consumerType: session.consumerType,
|
|
2155
|
+
capability: CONNECTION_GATEWAY_PLUGIN_DEBUG_INVOKE_CAPABILITY,
|
|
2156
|
+
traceId: request.traceId,
|
|
2157
|
+
createdAt: Date.now(),
|
|
2158
|
+
payload: {
|
|
2159
|
+
kind: "plugin-debug.stream",
|
|
2160
|
+
event: "error",
|
|
2161
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
function makePluginDebugMetadata(targets, source) {
|
|
2166
|
+
return { pluginDebug: { targets: targets.map((target) => ({
|
|
2167
|
+
source,
|
|
2168
|
+
pluginId: target.snapshot.pluginId,
|
|
2169
|
+
version: target.snapshot.version,
|
|
2170
|
+
name: target.snapshot.name,
|
|
2171
|
+
description: target.snapshot.description,
|
|
2172
|
+
toolDescription: target.snapshot.toolDescription,
|
|
2173
|
+
author: target.snapshot.author,
|
|
2174
|
+
tags: target.snapshot.tags,
|
|
2175
|
+
permissions: target.snapshot.permissions,
|
|
2176
|
+
secretSchema: target.snapshot.secretSchema,
|
|
2177
|
+
isToolSet: target.snapshot.isToolSet,
|
|
2178
|
+
tools: target.snapshot.tools
|
|
2179
|
+
})) } };
|
|
2180
|
+
}
|
|
2181
|
+
async function openWebSocket(url) {
|
|
2182
|
+
return new Promise((resolve, reject) => {
|
|
2183
|
+
const socket = new WebSocket(url);
|
|
2184
|
+
socket.addEventListener("open", () => {
|
|
2185
|
+
resolve(socket);
|
|
2186
|
+
}, { once: true });
|
|
2187
|
+
socket.addEventListener("error", () => {
|
|
2188
|
+
reject(/* @__PURE__ */ new Error(`Connection Gateway WebSocket connect failed: ${url}`));
|
|
2189
|
+
}, { once: true });
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
async function sendEnvelope(socket, envelope) {
|
|
2193
|
+
await sendMessage(socket, {
|
|
2194
|
+
protocol: "connection-gateway.ws.v1",
|
|
2195
|
+
type: "envelope",
|
|
2196
|
+
envelope
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
async function sendMessage(socket, message) {
|
|
2200
|
+
if (socket.readyState !== WebSocket.OPEN) throw new Error("Connection Gateway WebSocket is not open");
|
|
2201
|
+
socket.send(JSON.stringify(message));
|
|
2202
|
+
}
|
|
2203
|
+
function requiredRequestId(envelope) {
|
|
2204
|
+
if (!envelope.requestId) throw new Error("Gateway request envelope missing requestId");
|
|
2205
|
+
return envelope.requestId;
|
|
2206
|
+
}
|
|
2207
|
+
function delay(ms) {
|
|
2208
|
+
return new Promise((resolve) => {
|
|
2209
|
+
setTimeout(resolve, ms);
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
function formatErrorMessage(error) {
|
|
2213
|
+
return error instanceof Error ? error.message : String(error);
|
|
2214
|
+
}
|
|
2215
|
+
function parseWsServerMessage(data) {
|
|
2216
|
+
const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : ArrayBuffer.isView(data) ? Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8") : String(data);
|
|
2217
|
+
return ConnectionGatewayWsServerMessageSchema.parse(JSON.parse(text));
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
//#endregion
|
|
2221
|
+
//#region src/debug/remote-session.ts
|
|
2222
|
+
async function runRemoteDebugSession({ entriesInput, options, commandName, migrationHint }) {
|
|
2223
|
+
if (migrationHint) logger.warn(migrationHint);
|
|
2224
|
+
if (options.watch) {
|
|
2225
|
+
await runWatchRemoteDebugSession({
|
|
2226
|
+
entriesInput,
|
|
2227
|
+
options,
|
|
2228
|
+
commandName
|
|
2229
|
+
});
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
await runRemoteDebugSessionOnce({
|
|
2233
|
+
entriesInput,
|
|
2234
|
+
options,
|
|
2235
|
+
commandName
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
async function runRemoteDebugSessionOnce({ entriesInput, options, commandName }) {
|
|
2239
|
+
const targets = await loadTargets(await resolveDebugEntries(entriesInput), options);
|
|
2240
|
+
await ensureConnectLink({
|
|
2241
|
+
options,
|
|
2242
|
+
commandName,
|
|
2243
|
+
targets
|
|
2244
|
+
});
|
|
2245
|
+
const gatewayOptions = await resolveGatewayOptions({
|
|
2246
|
+
options,
|
|
2247
|
+
commandName,
|
|
2248
|
+
targets
|
|
2249
|
+
});
|
|
2250
|
+
const reporter = createRemoteDebugReporter({
|
|
2251
|
+
commandName,
|
|
2252
|
+
options,
|
|
2253
|
+
gatewayOptions,
|
|
2254
|
+
targets
|
|
2255
|
+
});
|
|
2256
|
+
reporter.start();
|
|
2257
|
+
try {
|
|
2258
|
+
const gateway = await connectDebugGateway({
|
|
2259
|
+
targets,
|
|
2260
|
+
options: gatewayOptions,
|
|
2261
|
+
onLog: (message) => reporter.log(`[gateway] ${message}`)
|
|
2262
|
+
});
|
|
2263
|
+
reporter.attachClose(({ force }) => {
|
|
2264
|
+
gateway.close();
|
|
2265
|
+
if (force) process.exit(130);
|
|
2266
|
+
});
|
|
2267
|
+
const source = gateway.session.sessionScope.source ?? gatewayOptions.source ?? "-";
|
|
2268
|
+
reporter.ready(source);
|
|
2269
|
+
await gateway.closed;
|
|
2270
|
+
} finally {
|
|
2271
|
+
await reporter.end();
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
async function runWatchRemoteDebugSession({ entriesInput, options, commandName }) {
|
|
2275
|
+
const entries = await resolveDebugEntries(entriesInput);
|
|
2276
|
+
const initialTargets = await loadTargets(entries, options);
|
|
2277
|
+
await ensureConnectLink({
|
|
2278
|
+
options,
|
|
2279
|
+
commandName,
|
|
2280
|
+
targets: initialTargets
|
|
2281
|
+
});
|
|
2282
|
+
const gatewayOptions = await resolveGatewayOptions({
|
|
2283
|
+
options,
|
|
2284
|
+
commandName,
|
|
2285
|
+
targets: initialTargets
|
|
2286
|
+
});
|
|
2287
|
+
const reporter = createRemoteDebugReporter({
|
|
2288
|
+
commandName,
|
|
2289
|
+
options,
|
|
2290
|
+
gatewayOptions,
|
|
2291
|
+
targets: initialTargets
|
|
2292
|
+
});
|
|
2293
|
+
let reloadRunning = false;
|
|
2294
|
+
let reloadPending = false;
|
|
2295
|
+
let closed = false;
|
|
2296
|
+
let gateway;
|
|
2297
|
+
const reloadTargets = async () => {
|
|
2298
|
+
if (reloadRunning) {
|
|
2299
|
+
reloadPending = true;
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
reloadRunning = true;
|
|
2303
|
+
try {
|
|
2304
|
+
do {
|
|
2305
|
+
reloadPending = false;
|
|
2306
|
+
try {
|
|
2307
|
+
const nextTargets = await loadTargets(entries, options);
|
|
2308
|
+
gateway?.updateTargets(nextTargets);
|
|
2309
|
+
reporter.log(`检测到插件文件变化,已热更新 ${nextTargets.length} 个本地插件。`);
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
reporter.log(`插件热更新失败: ${formatConnectionKeyExchangeError(error)}`);
|
|
2312
|
+
}
|
|
2313
|
+
} while (reloadPending && !closed);
|
|
2314
|
+
} finally {
|
|
2315
|
+
reloadRunning = false;
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2318
|
+
const watcher = watchDebugEntries(entries, () => {
|
|
2319
|
+
reloadTargets();
|
|
2320
|
+
});
|
|
2321
|
+
reporter.start();
|
|
2322
|
+
try {
|
|
2323
|
+
const connectedGateway = await connectDebugGateway({
|
|
2324
|
+
targets: initialTargets,
|
|
2325
|
+
options: gatewayOptions,
|
|
2326
|
+
onLog: (message) => reporter.log(`[gateway] ${message}`)
|
|
2327
|
+
});
|
|
2328
|
+
gateway = connectedGateway;
|
|
2329
|
+
reporter.attachClose(({ force }) => {
|
|
2330
|
+
closed = true;
|
|
2331
|
+
watcher.close();
|
|
2332
|
+
connectedGateway.close();
|
|
2333
|
+
if (force) process.exit(130);
|
|
2334
|
+
});
|
|
2335
|
+
const source = connectedGateway.session.sessionScope.source ?? gatewayOptions.source ?? "-";
|
|
2336
|
+
reporter.ready(source);
|
|
2337
|
+
await connectedGateway.closed;
|
|
2338
|
+
} finally {
|
|
2339
|
+
closed = true;
|
|
2340
|
+
watcher.close();
|
|
2341
|
+
await reporter.end();
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
async function resolveDebugEntries(entriesInput) {
|
|
2345
|
+
const entries = (Array.isArray(entriesInput) ? entriesInput : [entriesInput]).filter((entry) => entry.trim().length > 0);
|
|
2346
|
+
if (entries.length > 0) return entries.map((entry) => path.resolve(entry));
|
|
2347
|
+
const discovered = await discoverDebugEntries();
|
|
2348
|
+
logger.info([`自动发现 ${discovered.length} 个可调试插件:`, ...discovered.map((entry) => ` - ${entry}`)].join("\n"));
|
|
2349
|
+
return discovered;
|
|
2350
|
+
}
|
|
2351
|
+
async function loadTargets(entries, options) {
|
|
2352
|
+
const targets = [];
|
|
2353
|
+
for (const [index, entryDir] of entries.entries()) {
|
|
2354
|
+
const session = await loadDebugSession({
|
|
2355
|
+
entryDir,
|
|
2356
|
+
uploadDir: resolveUploadDir({
|
|
2357
|
+
entryDir,
|
|
2358
|
+
index,
|
|
2359
|
+
total: entries.length,
|
|
2360
|
+
uploadDir: options.uploadDir
|
|
2361
|
+
})
|
|
2362
|
+
});
|
|
2363
|
+
targets.push({
|
|
2364
|
+
runtime: session.runtime,
|
|
2365
|
+
snapshot: session.snapshot
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
const duplicatePluginIds = findDuplicateValues$1(targets.map((target) => target.snapshot.pluginId));
|
|
2369
|
+
if (duplicatePluginIds.length > 0) throw new Error(`gateway pluginId 重复: ${duplicatePluginIds.join(", ")}`);
|
|
2370
|
+
return targets;
|
|
2371
|
+
}
|
|
2372
|
+
function watchDebugEntries(entries, onChange) {
|
|
2373
|
+
const watchers = entries.map((entry) => fsWatch(entry, {
|
|
2374
|
+
recursive: true,
|
|
2375
|
+
onChange
|
|
2376
|
+
}));
|
|
2377
|
+
return { close() {
|
|
2378
|
+
watchers.forEach((watcher) => watcher.close());
|
|
2379
|
+
} };
|
|
2380
|
+
}
|
|
2381
|
+
function fsWatch(dir, { recursive, onChange }) {
|
|
2382
|
+
let timer;
|
|
2383
|
+
const watcher = fs$1.watch(dir, { recursive }, () => {
|
|
2384
|
+
clearTimeout(timer);
|
|
2385
|
+
timer = setTimeout(onChange, 150);
|
|
2386
|
+
});
|
|
2387
|
+
return { close() {
|
|
2388
|
+
clearTimeout(timer);
|
|
2389
|
+
watcher.close();
|
|
2390
|
+
} };
|
|
2391
|
+
}
|
|
2392
|
+
function createRemoteDebugReporter({ commandName, options, gatewayOptions, targets }) {
|
|
2393
|
+
const summary = {
|
|
2394
|
+
commandName,
|
|
2395
|
+
mode: "FastGPT connection key",
|
|
2396
|
+
gateway: gatewayOptions.gatewayUrl,
|
|
2397
|
+
reconnect: gatewayOptions.reconnect === false ? "off" : "on",
|
|
2398
|
+
targets: targets.map((target) => target.snapshot)
|
|
2399
|
+
};
|
|
2400
|
+
return options.interactive !== false && Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY) ? new TuiRemoteDebugReporter(summary) : new PlainRemoteDebugReporter(summary);
|
|
2401
|
+
}
|
|
2402
|
+
async function ensureConnectLink({ options, commandName, targets }) {
|
|
2403
|
+
if (options.connect) return;
|
|
2404
|
+
const savedConnect = await readSavedConnectionKey();
|
|
2405
|
+
if (savedConnect) {
|
|
2406
|
+
options.connect = savedConnect;
|
|
2407
|
+
options.connectPersistOnSuccess = true;
|
|
2408
|
+
logger.info(`已读取本地 FastGPT connection key 配置: ${getCliConfigPath()}`);
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
if (options.interactive === false || !process.stdin.isTTY || !process.stdout.isTTY) throw new Error("dev 需要 --connect,或在交互式终端中输入 FastGPT connection key。");
|
|
2412
|
+
options.connect = await promptConnectLink({
|
|
2413
|
+
commandName,
|
|
2414
|
+
targets
|
|
2415
|
+
});
|
|
2416
|
+
options.connectPersistOnSuccess = true;
|
|
2417
|
+
}
|
|
2418
|
+
async function promptConnectLink({ commandName, targets, defaultValue, errorMessage }) {
|
|
2419
|
+
process.stdout.write([
|
|
2420
|
+
"\x1B[?25h\x1B[2J\x1B[HFastGPT Plugin Dev",
|
|
2421
|
+
"",
|
|
2422
|
+
`Command fastgpt-plugin ${commandName}`,
|
|
2423
|
+
"Mode waiting for FastGPT connection key",
|
|
2424
|
+
"Status pending",
|
|
2425
|
+
"",
|
|
2426
|
+
"Plugins",
|
|
2427
|
+
...targets.map((target) => ` ${target.snapshot.pluginId}@${target.snapshot.version}`),
|
|
2428
|
+
"",
|
|
2429
|
+
...errorMessage ? [`Last error ${errorMessage}`, ""] : [],
|
|
2430
|
+
"Paste the FastGPT connection key or connect link to start the WSS debug session.",
|
|
2431
|
+
""
|
|
2432
|
+
].join("\n"));
|
|
2433
|
+
return (await input({
|
|
2434
|
+
message: "FastGPT connection key",
|
|
2435
|
+
default: defaultValue,
|
|
2436
|
+
prefill: defaultValue ? "editable" : void 0,
|
|
2437
|
+
validate(value) {
|
|
2438
|
+
return value.trim().length > 0 || "请输入 FastGPT connection key";
|
|
2439
|
+
}
|
|
2440
|
+
})).trim();
|
|
2441
|
+
}
|
|
2442
|
+
async function readSavedConnectionKey() {
|
|
2443
|
+
return (await readCliConfig()).debug?.connectionKey?.trim() || void 0;
|
|
2444
|
+
}
|
|
2445
|
+
async function saveConnectionKey(connectionKey) {
|
|
2446
|
+
const trimmed = connectionKey.trim();
|
|
2447
|
+
if (!trimmed) return;
|
|
2448
|
+
const configPath = getCliConfigPath();
|
|
2449
|
+
const config = await readCliConfig();
|
|
2450
|
+
const nextConfig = {
|
|
2451
|
+
...config,
|
|
2452
|
+
debug: {
|
|
2453
|
+
...config.debug,
|
|
2454
|
+
connectionKey: trimmed
|
|
2455
|
+
}
|
|
2456
|
+
};
|
|
2457
|
+
await fs.mkdir(path.dirname(configPath), {
|
|
2458
|
+
recursive: true,
|
|
2459
|
+
mode: 448
|
|
2460
|
+
});
|
|
2461
|
+
await fs.writeFile(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, {
|
|
2462
|
+
encoding: "utf-8",
|
|
2463
|
+
mode: 384
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
async function readCliConfig() {
|
|
2467
|
+
const configPath = getCliConfigPath();
|
|
2468
|
+
let raw;
|
|
2469
|
+
try {
|
|
2470
|
+
raw = await fs.readFile(configPath, "utf-8");
|
|
2471
|
+
} catch (error) {
|
|
2472
|
+
if (isNodeError(error) && error.code === "ENOENT") return {};
|
|
2473
|
+
throw error;
|
|
2474
|
+
}
|
|
2475
|
+
if (!raw.trim()) return {};
|
|
2476
|
+
return CliConfigSchema.parse(JSON.parse(raw));
|
|
2477
|
+
}
|
|
2478
|
+
function getCliConfigPath() {
|
|
2479
|
+
return path.join(getCliConfigDir(), CLI_CONFIG_FILE_NAME);
|
|
2480
|
+
}
|
|
2481
|
+
function getCliConfigDir() {
|
|
2482
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim();
|
|
2483
|
+
if (xdgConfigHome) return path.join(xdgConfigHome, CLI_CONFIG_DIR_NAME);
|
|
2484
|
+
return path.join(os.homedir(), ".config", CLI_CONFIG_DIR_NAME);
|
|
2485
|
+
}
|
|
2486
|
+
function isNodeError(error) {
|
|
2487
|
+
return error instanceof Error && "code" in error;
|
|
2488
|
+
}
|
|
2489
|
+
function formatTargetLine(snapshot) {
|
|
2490
|
+
const tools = snapshot.tools.map((tool) => tool.id).join(", ") || "-";
|
|
2491
|
+
return ` - ${snapshot.pluginId}@${snapshot.version} tools=[${tools}] entry=${snapshot.entryDir}`;
|
|
2492
|
+
}
|
|
2493
|
+
const CLI_CONFIG_FILE_NAME = "config.json";
|
|
2494
|
+
const CLI_CONFIG_DIR_NAME = "fastgpt-plugin";
|
|
2495
|
+
const CliConfigSchema = z.object({ debug: z.object({ connectionKey: z.string().min(1).optional() }).optional() }).passthrough();
|
|
2496
|
+
var PlainRemoteDebugReporter = class {
|
|
2497
|
+
constructor(summary) {
|
|
2498
|
+
this.summary = summary;
|
|
2499
|
+
}
|
|
2500
|
+
start() {
|
|
2501
|
+
logger.info([
|
|
2502
|
+
"FastGPT 插件开发会话",
|
|
2503
|
+
` command: fastgpt-plugin ${this.summary.commandName}`,
|
|
2504
|
+
` mode: ${this.summary.mode}`,
|
|
2505
|
+
" ui: plain",
|
|
2506
|
+
` gateway: ${this.summary.gateway}`,
|
|
2507
|
+
` reconnect: ${this.summary.reconnect}`,
|
|
2508
|
+
" plugins:",
|
|
2509
|
+
...this.summary.targets.map(formatTargetLine)
|
|
2510
|
+
].join("\n"));
|
|
2511
|
+
}
|
|
2512
|
+
log(message) {
|
|
2513
|
+
logger.info(message);
|
|
2514
|
+
}
|
|
2515
|
+
ready(source) {
|
|
2516
|
+
this.summary.targets.forEach((target) => {
|
|
2517
|
+
logger.success(`远程调试已就绪: ${source} ${target.pluginId}`);
|
|
2518
|
+
});
|
|
2519
|
+
logger.info(`已建立 1 个远程调试通道,挂载 ${this.summary.targets.length} 个插件,按 Ctrl+C 停止。`);
|
|
2520
|
+
}
|
|
2521
|
+
attachClose() {}
|
|
2522
|
+
end() {
|
|
2523
|
+
logger.info("远程调试会话已结束。");
|
|
2524
|
+
}
|
|
2525
|
+
};
|
|
2526
|
+
var TuiRemoteDebugReporter = class {
|
|
2527
|
+
status = "connecting";
|
|
2528
|
+
source = "-";
|
|
2529
|
+
logs = [];
|
|
2530
|
+
instance;
|
|
2531
|
+
closeSession;
|
|
2532
|
+
interruptCount = 0;
|
|
2533
|
+
onSigint = () => {
|
|
2534
|
+
this.requestClose("ctrl-c");
|
|
2535
|
+
};
|
|
2536
|
+
requestClose = (reason) => {
|
|
2537
|
+
if (reason === "q") {
|
|
2538
|
+
this.log("收到退出指令,正在关闭远程调试会话。");
|
|
2539
|
+
this.closeSession?.({ force: false });
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
this.interruptCount += 1;
|
|
2543
|
+
const force = this.interruptCount >= 2;
|
|
2544
|
+
this.status = force ? "closed" : "closing";
|
|
2545
|
+
this.log(force ? "再次收到 Ctrl+C,正在强制退出。" : "收到 Ctrl+C,正在关闭远程调试会话。再次按 Ctrl+C 强制退出。");
|
|
2546
|
+
this.closeSession?.({ force });
|
|
2547
|
+
if (force && !this.closeSession) process.exit(130);
|
|
2548
|
+
};
|
|
2549
|
+
constructor(summary) {
|
|
2550
|
+
this.summary = summary;
|
|
2551
|
+
}
|
|
2552
|
+
start() {
|
|
2553
|
+
process.on("SIGINT", this.onSigint);
|
|
2554
|
+
this.instance = render(this.createApp(), {
|
|
2555
|
+
stdin: process.stdin,
|
|
2556
|
+
stdout: process.stdout,
|
|
2557
|
+
stderr: process.stderr,
|
|
2558
|
+
exitOnCtrlC: false,
|
|
2559
|
+
interactive: true,
|
|
2560
|
+
patchConsole: false
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2563
|
+
log(message) {
|
|
2564
|
+
this.logs.push({
|
|
2565
|
+
at: /* @__PURE__ */ new Date(),
|
|
2566
|
+
message
|
|
2567
|
+
});
|
|
2568
|
+
this.logs = this.logs.slice(-6);
|
|
2569
|
+
this.rerender();
|
|
2570
|
+
}
|
|
2571
|
+
ready(source) {
|
|
2572
|
+
this.status = "connected";
|
|
2573
|
+
this.source = source;
|
|
2574
|
+
this.log(`远程调试已就绪,挂载 ${this.summary.targets.length} 个插件。`);
|
|
2575
|
+
}
|
|
2576
|
+
attachClose(close) {
|
|
2577
|
+
this.closeSession = close;
|
|
2578
|
+
}
|
|
2579
|
+
async end() {
|
|
2580
|
+
process.off("SIGINT", this.onSigint);
|
|
2581
|
+
this.status = "closed";
|
|
2582
|
+
this.rerender();
|
|
2583
|
+
const instance = this.instance;
|
|
2584
|
+
this.instance = void 0;
|
|
2585
|
+
if (instance) {
|
|
2586
|
+
await Promise.race([instance.waitUntilRenderFlush(), setTimeout$1(50)]).catch(() => void 0);
|
|
2587
|
+
instance.unmount();
|
|
2588
|
+
await Promise.race([instance.waitUntilExit(), setTimeout$1(50)]).catch(() => void 0);
|
|
2589
|
+
}
|
|
2590
|
+
logger.info("远程调试会话已结束。");
|
|
2591
|
+
}
|
|
2592
|
+
rerender() {
|
|
2593
|
+
this.instance?.rerender(this.createApp());
|
|
2594
|
+
}
|
|
2595
|
+
createApp() {
|
|
2596
|
+
return React.createElement(RemoteDebugInkApp, {
|
|
2597
|
+
summary: this.summary,
|
|
2598
|
+
status: this.status,
|
|
2599
|
+
source: this.source,
|
|
2600
|
+
logs: this.logs,
|
|
2601
|
+
onQuit: this.requestClose
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
};
|
|
2605
|
+
function RemoteDebugInkApp({ summary, status, source, logs, onQuit }) {
|
|
2606
|
+
useInput((inputValue, key) => {
|
|
2607
|
+
if (key.ctrl && inputValue === "c") {
|
|
2608
|
+
onQuit("ctrl-c");
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
if (inputValue === "q") onQuit("q");
|
|
2612
|
+
});
|
|
2613
|
+
const statusMeta = getTuiStatusMeta(status);
|
|
2614
|
+
const sourceLabel = source === "-" ? "waiting for bind" : source;
|
|
2615
|
+
const rows = [
|
|
2616
|
+
["command", `fastgpt-plugin ${summary.commandName}`],
|
|
2617
|
+
["gateway", summary.gateway],
|
|
2618
|
+
["source", sourceLabel],
|
|
2619
|
+
["reconnect", summary.reconnect === "on" ? "enabled" : "disabled"]
|
|
2620
|
+
];
|
|
2621
|
+
const plugins = summary.targets.map((target) => {
|
|
2622
|
+
const toolCount = target.tools.length;
|
|
2623
|
+
return {
|
|
2624
|
+
id: `${target.pluginId}:${target.version}`,
|
|
2625
|
+
name: `${target.pluginId}@${target.version}`,
|
|
2626
|
+
tools: toolCount === 1 ? "1 tool" : `${toolCount} tools`,
|
|
2627
|
+
entry: target.entryDir
|
|
2628
|
+
};
|
|
2629
|
+
});
|
|
2630
|
+
const activityItems = logs.length > 0 ? logs.map((entry, index) => React.createElement(Box, {
|
|
2631
|
+
key: `${entry.at.getTime()}:${index}`,
|
|
2632
|
+
flexDirection: "row"
|
|
2633
|
+
}, React.createElement(Text, { color: "gray" }, formatTuiTime(entry.at)), React.createElement(Text, { dimColor: true }, " │ "), React.createElement(Text, { wrap: "truncate-end" }, entry.message))) : [React.createElement(Text, {
|
|
2634
|
+
key: "empty-log",
|
|
2635
|
+
dimColor: true
|
|
2636
|
+
}, "waiting for gateway events")];
|
|
2637
|
+
return React.createElement(Box, {
|
|
2638
|
+
flexDirection: "column",
|
|
2639
|
+
paddingX: 1,
|
|
2640
|
+
paddingY: 1
|
|
2641
|
+
}, React.createElement(Box, {
|
|
2642
|
+
borderStyle: "round",
|
|
2643
|
+
borderColor: statusMeta.borderColor,
|
|
2644
|
+
paddingX: 2,
|
|
2645
|
+
paddingY: 1,
|
|
2646
|
+
flexDirection: "column"
|
|
2647
|
+
}, React.createElement(Box, { justifyContent: "space-between" }, React.createElement(Box, null, React.createElement(Text, {
|
|
2648
|
+
bold: true,
|
|
2649
|
+
color: "cyanBright"
|
|
2650
|
+
}, "FastGPT Plugin Dev"), React.createElement(Text, { dimColor: true }, " remote debug")), React.createElement(Box, null, React.createElement(Text, {
|
|
2651
|
+
color: statusMeta.color,
|
|
2652
|
+
bold: true
|
|
2653
|
+
}, statusMeta.label), React.createElement(Text, { dimColor: true }, ` ${plugins.length} plugins`))), React.createElement(Box, { marginTop: 1 }, React.createElement(Text, { color: statusMeta.color }, statusMeta.marker), React.createElement(Text, { dimColor: true }, " "), React.createElement(Text, { wrap: "truncate-end" }, statusMeta.description))), React.createElement(Box, {
|
|
2654
|
+
marginTop: 1,
|
|
2655
|
+
columnGap: 1
|
|
2656
|
+
}, React.createElement(Box, {
|
|
2657
|
+
borderStyle: "round",
|
|
2658
|
+
borderColor: "gray",
|
|
2659
|
+
paddingX: 1,
|
|
2660
|
+
paddingY: 1,
|
|
2661
|
+
flexDirection: "column",
|
|
2662
|
+
width: "50%"
|
|
2663
|
+
}, React.createElement(Text, { bold: true }, "Session"), React.createElement(Box, {
|
|
2664
|
+
flexDirection: "column",
|
|
2665
|
+
marginTop: 1
|
|
2666
|
+
}, ...rows.map(([label, value]) => React.createElement(SessionRow, {
|
|
2667
|
+
key: label,
|
|
2668
|
+
label,
|
|
2669
|
+
value
|
|
2670
|
+
})))), React.createElement(Box, {
|
|
2671
|
+
borderStyle: "round",
|
|
2672
|
+
borderColor: "gray",
|
|
2673
|
+
paddingX: 1,
|
|
2674
|
+
paddingY: 1,
|
|
2675
|
+
flexDirection: "column",
|
|
2676
|
+
width: "50%"
|
|
2677
|
+
}, React.createElement(Box, { justifyContent: "space-between" }, React.createElement(Text, { bold: true }, "Plugins"), React.createElement(Text, { dimColor: true }, `${plugins.length} mounted`)), React.createElement(Box, {
|
|
2678
|
+
flexDirection: "column",
|
|
2679
|
+
marginTop: 1
|
|
2680
|
+
}, ...plugins.map((plugin, index) => React.createElement(Box, {
|
|
2681
|
+
key: plugin.id,
|
|
2682
|
+
flexDirection: "column",
|
|
2683
|
+
marginTop: index === 0 ? 0 : 1
|
|
2684
|
+
}, React.createElement(Box, null, React.createElement(Text, { color: "greenBright" }, "● "), React.createElement(Text, {
|
|
2685
|
+
bold: true,
|
|
2686
|
+
wrap: "truncate-end"
|
|
2687
|
+
}, plugin.name), React.createElement(Text, { dimColor: true }, ` ${plugin.tools}`)), React.createElement(Text, {
|
|
2688
|
+
dimColor: true,
|
|
2689
|
+
wrap: "truncate-end"
|
|
2690
|
+
}, ` ${plugin.entry}`)))))), React.createElement(Box, {
|
|
2691
|
+
borderStyle: "round",
|
|
2692
|
+
borderColor: "gray",
|
|
2693
|
+
paddingX: 1,
|
|
2694
|
+
paddingY: 1,
|
|
2695
|
+
flexDirection: "column",
|
|
2696
|
+
marginTop: 1
|
|
2697
|
+
}, React.createElement(Box, { justifyContent: "space-between" }, React.createElement(Text, { bold: true }, "Activity"), React.createElement(Text, { dimColor: true }, "latest 6 events")), React.createElement(Box, {
|
|
2698
|
+
flexDirection: "column",
|
|
2699
|
+
marginTop: 1
|
|
2700
|
+
}, ...activityItems)), React.createElement(Box, {
|
|
2701
|
+
marginTop: 1,
|
|
2702
|
+
justifyContent: "space-between"
|
|
2703
|
+
}, React.createElement(Text, { dimColor: true }, "q stop"), React.createElement(Text, { dimColor: true }, "Ctrl+C stop · second Ctrl+C force exit")));
|
|
2704
|
+
}
|
|
2705
|
+
function getTuiStatusMeta(status) {
|
|
2706
|
+
if (status === "connected") return {
|
|
2707
|
+
label: "CONNECTED",
|
|
2708
|
+
marker: "●",
|
|
2709
|
+
description: "Gateway channel is live and ready to receive FastGPT invocations.",
|
|
2710
|
+
color: "greenBright",
|
|
2711
|
+
borderColor: "green"
|
|
2712
|
+
};
|
|
2713
|
+
if (status === "closed") return {
|
|
2714
|
+
label: "CLOSED",
|
|
2715
|
+
marker: "■",
|
|
2716
|
+
description: "Remote debug session has been closed.",
|
|
2717
|
+
color: "gray",
|
|
2718
|
+
borderColor: "gray"
|
|
2719
|
+
};
|
|
2720
|
+
if (status === "closing") return {
|
|
2721
|
+
label: "CLOSING",
|
|
2722
|
+
marker: "◆",
|
|
2723
|
+
description: "Closing gateway channel. Press Ctrl+C again to force exit.",
|
|
2724
|
+
color: "yellowBright",
|
|
2725
|
+
borderColor: "yellow"
|
|
2726
|
+
};
|
|
2727
|
+
return {
|
|
2728
|
+
label: "CONNECTING",
|
|
2729
|
+
marker: "◆",
|
|
2730
|
+
description: "Binding local plugins to the FastGPT connection gateway.",
|
|
2731
|
+
color: "yellowBright",
|
|
2732
|
+
borderColor: "yellow"
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
function SessionRow({ label, value }) {
|
|
2736
|
+
return React.createElement(Box, null, React.createElement(Text, { color: "gray" }, label.padEnd(10)), React.createElement(Text, { wrap: "truncate-end" }, value));
|
|
2737
|
+
}
|
|
2738
|
+
function formatTuiTime(value) {
|
|
2739
|
+
return [
|
|
2740
|
+
value.getHours().toString().padStart(2, "0"),
|
|
2741
|
+
value.getMinutes().toString().padStart(2, "0"),
|
|
2742
|
+
value.getSeconds().toString().padStart(2, "0")
|
|
2743
|
+
].join(":");
|
|
2744
|
+
}
|
|
2745
|
+
async function resolveGatewayOptions({ options, commandName, targets }) {
|
|
2746
|
+
if (!options.connect) throw new Error("dev 需要 --connect 来建立远程调试通道。");
|
|
2747
|
+
while (true) try {
|
|
2748
|
+
const gatewayOptions = await resolveConnectGatewayOptions(options);
|
|
2749
|
+
if (options.connectPersistOnSuccess === true) {
|
|
2750
|
+
await saveConnectionKey(options.connect);
|
|
2751
|
+
logger.info(`已保存 FastGPT connection key 到本地配置: ${getCliConfigPath()}`);
|
|
2752
|
+
options.connectPersistOnSuccess = false;
|
|
2753
|
+
}
|
|
2754
|
+
return gatewayOptions;
|
|
2755
|
+
} catch (error) {
|
|
2756
|
+
const errorMessage = formatConnectionKeyExchangeError(error);
|
|
2757
|
+
if (!canPromptForConnectInput(options)) throw error;
|
|
2758
|
+
logger.error(errorMessage);
|
|
2759
|
+
options.connect = await promptConnectLink({
|
|
2760
|
+
commandName,
|
|
2761
|
+
targets,
|
|
2762
|
+
defaultValue: options.connect,
|
|
2763
|
+
errorMessage
|
|
2764
|
+
});
|
|
2765
|
+
options.connectPersistOnSuccess = true;
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
function canPromptForConnectInput(options) {
|
|
2769
|
+
return options.interactive !== false && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
2770
|
+
}
|
|
2771
|
+
function formatConnectionKeyExchangeError(error) {
|
|
2772
|
+
return error instanceof Error ? error.message : String(error);
|
|
2773
|
+
}
|
|
2774
|
+
async function resolveConnectGatewayOptions(options) {
|
|
2775
|
+
const info = await exchangeConnectLink(options.connect);
|
|
2776
|
+
return {
|
|
2777
|
+
gatewayUrl: normalizeGatewayWsUrl(info.gatewayUrl),
|
|
2778
|
+
connectToken: info.connectToken,
|
|
2779
|
+
userId: info.tmbId,
|
|
2780
|
+
source: info.source,
|
|
2781
|
+
tokenTtlMs: Math.max(1, info.expiresAt - Date.now()),
|
|
2782
|
+
reconnect: options.noReconnect ? false : options.reconnect ?? true,
|
|
2783
|
+
reconnectIntervalMs: toPositiveInt(options.reconnectIntervalMs ?? process.env.CONNECTION_GATEWAY_RECONNECT_INTERVAL_MS ?? "2000", "reconnect-interval-ms")
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
function resolveUploadDir({ entryDir, index, total, uploadDir }) {
|
|
2787
|
+
if (!uploadDir) return path.resolve(path.join(entryDir, ".fastgpt-plugin-debug/uploads"));
|
|
2788
|
+
if (total === 1) return path.resolve(uploadDir);
|
|
2789
|
+
return path.resolve(uploadDir, `${index + 1}-${sanitizePathSegment$1(path.basename(entryDir))}`);
|
|
2790
|
+
}
|
|
2791
|
+
const ConnectInfoSchema = z.object({
|
|
2792
|
+
gatewayUrl: z.string().min(1),
|
|
2793
|
+
transport: z.literal("websocket"),
|
|
2794
|
+
source: z.string().min(1),
|
|
2795
|
+
connectToken: z.string().min(1),
|
|
2796
|
+
expiresAt: z.number().int().positive()
|
|
2797
|
+
});
|
|
2798
|
+
async function exchangeConnectLink(connectInput) {
|
|
2799
|
+
const response = isHttpUrl(connectInput) ? await fetch(connectInput) : await fetch(resolveConnectionKeyExchangeUrl(), {
|
|
2800
|
+
method: "POST",
|
|
2801
|
+
headers: { "Content-Type": "application/json" },
|
|
2802
|
+
body: JSON.stringify({ connectionKey: connectInput })
|
|
2803
|
+
});
|
|
2804
|
+
const text = await response.text();
|
|
2805
|
+
if (!response.ok) throw new Error(`connection key 请求失败: ${response.status} ${text}`);
|
|
2806
|
+
const payload = text ? JSON.parse(text) : {};
|
|
2807
|
+
const info = ConnectInfoSchema.parse(payload.data ?? payload);
|
|
2808
|
+
return {
|
|
2809
|
+
...info,
|
|
2810
|
+
tmbId: parseTmbIdFromDebugSource(info.source)
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
function isHttpUrl(value) {
|
|
2814
|
+
try {
|
|
2815
|
+
const parsed = new URL(value);
|
|
2816
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
2817
|
+
} catch {
|
|
2818
|
+
return false;
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
function resolveConnectionKeyExchangeUrl() {
|
|
2822
|
+
const explicit = process.env.FASTGPT_PLUGIN_DEBUG_CONNECT_URL;
|
|
2823
|
+
if (explicit) return explicit;
|
|
2824
|
+
const baseUrl = process.env.FASTGPT_PLUGIN_SERVER_URL ?? process.env.PLUGIN_SERVER_URL;
|
|
2825
|
+
if (!baseUrl) throw new Error("使用裸 connection key 时需要设置 FASTGPT_PLUGIN_DEBUG_CONNECT_URL 或 FASTGPT_PLUGIN_SERVER_URL。");
|
|
2826
|
+
return new URL("/api/plugin/debug-sessions/connection-key:exchange", baseUrl).toString();
|
|
2827
|
+
}
|
|
2828
|
+
function parseTmbIdFromDebugSource(source) {
|
|
2829
|
+
const parts = source.split(":");
|
|
2830
|
+
const index = parts.indexOf("tmbId");
|
|
2831
|
+
const tmbId = index >= 0 ? parts[index + 1] : void 0;
|
|
2832
|
+
if (!tmbId) throw new Error(`debug source 缺少 tmbId: ${source}`);
|
|
2833
|
+
return tmbId;
|
|
2834
|
+
}
|
|
2835
|
+
function normalizeGatewayWsUrl(gatewayUrl) {
|
|
2836
|
+
const parsed = new URL(gatewayUrl);
|
|
2837
|
+
if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") throw new Error("gatewayUrl 必须使用 ws:// 或 wss:// 协议。");
|
|
2838
|
+
if (!parsed.hostname) throw new Error("gatewayUrl 必须包含 host。");
|
|
2839
|
+
return parsed.toString();
|
|
2840
|
+
}
|
|
2841
|
+
function toPositiveInt(value, label) {
|
|
2842
|
+
const parsed = Number(value);
|
|
2843
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new Error(`${label} 必须是正整数。`);
|
|
2844
|
+
return parsed;
|
|
2845
|
+
}
|
|
2846
|
+
function findDuplicateValues$1(values) {
|
|
2847
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2848
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
2849
|
+
values.forEach((value) => {
|
|
2850
|
+
if (seen.has(value)) {
|
|
2851
|
+
duplicates.add(value);
|
|
2852
|
+
return;
|
|
2853
|
+
}
|
|
2854
|
+
seen.add(value);
|
|
2855
|
+
});
|
|
2856
|
+
return [...duplicates];
|
|
2857
|
+
}
|
|
2858
|
+
function sanitizePathSegment$1(value) {
|
|
2859
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "_") || "plugin";
|
|
2860
|
+
}
|
|
2861
|
+
|
|
1706
2862
|
//#endregion
|
|
1707
2863
|
//#region src/commands/debug.ts
|
|
1708
2864
|
var DebugCommand = class extends BaseCommand {
|
|
1709
2865
|
register(parent) {
|
|
1710
|
-
parent.command("debug <
|
|
1711
|
-
await this.run(
|
|
2866
|
+
parent.command("debug <entries...>").description("直接加载本地插件目录并调试工具").option("-t, --tool <childId>", "工具集子工具 ID").option("--run", "立即执行一次工具调试", false).option("-i, --input <json>", "工具输入 JSON 字符串").option("--input-file <path>", "工具输入 JSON 文件路径").option("--secrets <json>", "secrets JSON 字符串").option("--secrets-file <path>", "secrets JSON 文件路径").option("--system-var <json>", "systemVar JSON 字符串").option("--system-var-file <path>", "systemVar JSON 文件路径").option("--upload-dir <path>", "虚拟 uploadFile 的输出目录").option("--connect <keyOrUrl>", "兼容入口:FastGPT debug connection key 或 connect link,建议改用 dev 命令").option("--reconnect", "兼容入口:断线后自动重连", true).option("--no-reconnect", "兼容入口:关闭自动重连").option("--reconnect-interval-ms <ms>", "兼容入口:重连间隔").action(async (entries, opts) => {
|
|
2867
|
+
await this.run(entries, opts);
|
|
1712
2868
|
});
|
|
1713
2869
|
}
|
|
1714
|
-
async run(
|
|
1715
|
-
const
|
|
1716
|
-
const
|
|
1717
|
-
|
|
2870
|
+
async run(entriesInput, options) {
|
|
2871
|
+
const entries = (Array.isArray(entriesInput) ? entriesInput : [entriesInput]).map((entry) => path.resolve(entry));
|
|
2872
|
+
const isMultiEntry = entries.length > 1;
|
|
2873
|
+
if (options.connect) {
|
|
2874
|
+
await runRemoteDebugSession({
|
|
2875
|
+
entriesInput,
|
|
2876
|
+
options,
|
|
2877
|
+
commandName: "dev",
|
|
2878
|
+
migrationHint: "debug --connect 是兼容入口,远程集成调试建议改用 fastgpt-plugin dev。"
|
|
2879
|
+
});
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
if (isMultiEntry && !options.connect) throw new Error("debug 是轻量本地调试入口,一次只支持一个插件;多插件远程调试请使用 dev。");
|
|
2883
|
+
if (isMultiEntry && options.run) throw new Error("--run 只支持单个插件入口。");
|
|
2884
|
+
const sessions = [];
|
|
2885
|
+
for (const [index, entryDir] of entries.entries()) sessions.push(await loadDebugSession({
|
|
1718
2886
|
entryDir,
|
|
1719
|
-
uploadDir
|
|
2887
|
+
uploadDir: this.resolveUploadDir({
|
|
2888
|
+
entryDir,
|
|
2889
|
+
index,
|
|
2890
|
+
total: entries.length,
|
|
2891
|
+
uploadDir: options.uploadDir
|
|
2892
|
+
})
|
|
2893
|
+
}));
|
|
2894
|
+
sessions.forEach((session) => {
|
|
2895
|
+
this.printSnapshot(session.snapshot, session.uploadDir);
|
|
1720
2896
|
});
|
|
1721
|
-
|
|
2897
|
+
const duplicatePluginIds = findDuplicateValues(sessions.map((session) => session.snapshot.pluginId));
|
|
2898
|
+
if (duplicatePluginIds.length > 0) throw new Error(`gateway pluginId 重复: ${duplicatePluginIds.join(", ")}`);
|
|
2899
|
+
const [session] = sessions;
|
|
1722
2900
|
if (!options.run) return;
|
|
1723
2901
|
const input = await this.readJsonOption(options.input, options.inputFile, "input");
|
|
1724
2902
|
const secrets = await this.readJsonOption(options.secrets, options.secretsFile, "secrets");
|
|
@@ -1799,6 +2977,11 @@ var DebugCommand = class extends BaseCommand {
|
|
|
1799
2977
|
JSON.stringify(this.createInputExample(tool.inputSchema))
|
|
1800
2978
|
].map(quoteShellArg).join(" ");
|
|
1801
2979
|
}
|
|
2980
|
+
resolveUploadDir({ entryDir, index, total, uploadDir }) {
|
|
2981
|
+
if (!uploadDir) return path.resolve(path.join(entryDir, ".fastgpt-plugin-debug/uploads"));
|
|
2982
|
+
if (total === 1) return path.resolve(uploadDir);
|
|
2983
|
+
return path.resolve(uploadDir, `${index + 1}-${sanitizePathSegment(path.basename(entryDir))}`);
|
|
2984
|
+
}
|
|
1802
2985
|
createInputExample(schema) {
|
|
1803
2986
|
if (Array.isArray(schema.enum) && schema.enum.length > 0) return schema.enum[0];
|
|
1804
2987
|
if ("const" in schema) return schema.const;
|
|
@@ -1842,6 +3025,38 @@ function quoteShellArg(value) {
|
|
|
1842
3025
|
if (/^[a-zA-Z0-9_./:@-]+$/.test(value)) return value;
|
|
1843
3026
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
1844
3027
|
}
|
|
3028
|
+
function findDuplicateValues(values) {
|
|
3029
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3030
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
3031
|
+
values.forEach((value) => {
|
|
3032
|
+
if (seen.has(value)) {
|
|
3033
|
+
duplicates.add(value);
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
3036
|
+
seen.add(value);
|
|
3037
|
+
});
|
|
3038
|
+
return [...duplicates];
|
|
3039
|
+
}
|
|
3040
|
+
function sanitizePathSegment(value) {
|
|
3041
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "_") || "plugin";
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
//#endregion
|
|
3045
|
+
//#region src/commands/dev.ts
|
|
3046
|
+
var DevCommand = class extends BaseCommand {
|
|
3047
|
+
register(parent) {
|
|
3048
|
+
parent.command("dev [entries...]").description("启动 FastGPT 插件集成开发会话").option("--connect <keyOrUrl>", "可选:FastGPT debug connection key 或 connect link,适合脚本和 Agent").option("--reconnect", "断线后自动重连", true).option("--no-reconnect", "关闭自动重连").option("--reconnect-interval-ms <ms>", "重连间隔").option("--upload-dir <path>", "虚拟 uploadFile 的输出目录").option("--watch", "监听插件文件变化并自动重载远程调试会话", false).option("--no-interactive", "使用纯文本状态输出,适合脚本和 Agent").action(async (entries, opts) => {
|
|
3049
|
+
await this.run(entries, opts);
|
|
3050
|
+
});
|
|
3051
|
+
}
|
|
3052
|
+
async run(entriesInput, options) {
|
|
3053
|
+
await runRemoteDebugSession({
|
|
3054
|
+
entriesInput,
|
|
3055
|
+
options,
|
|
3056
|
+
commandName: "dev"
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
};
|
|
1845
3060
|
|
|
1846
3061
|
//#endregion
|
|
1847
3062
|
//#region src/commands/pack.ts
|
|
@@ -1931,6 +3146,7 @@ function createProgram() {
|
|
|
1931
3146
|
new CheckCommand().register(program);
|
|
1932
3147
|
new CreateCommand().register(program);
|
|
1933
3148
|
new DebugCommand().register(program);
|
|
3149
|
+
new DevCommand().register(program);
|
|
1934
3150
|
new PackCommand().register(program);
|
|
1935
3151
|
return program;
|
|
1936
3152
|
}
|