@fastgpt-plugin/cli 0.1.0-beta.9 → 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/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.1.0-beta.9";
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 = [
@@ -977,63 +1255,6 @@ var StreamData = class StreamData {
977
1255
  }
978
1256
  };
979
1257
 
980
- //#endregion
981
- //#region ../../packages/domain/src/value-objects/system-var.vo.ts
982
- const SystemVarSchema = z.object({
983
- app: z.object({
984
- id: z.string(),
985
- name: z.string()
986
- }),
987
- chat: z.object({
988
- chatId: z.string(),
989
- uid: z.string().optional()
990
- }),
991
- invokeToken: z.string(),
992
- time: z.string()
993
- });
994
-
995
- //#endregion
996
- //#region ../../packages/domain/src/value-objects/tool.vo.ts
997
- const ToolHandlerReturnSchema = z.record(z.string(), z.unknown());
998
- const StreamMessageTypeSchema = z.enum([
999
- "response",
1000
- "error",
1001
- "stream"
1002
- ]);
1003
- const StreamMessageTypeEnum = StreamMessageTypeSchema.enum;
1004
- const StreamDataAnswerTypeSchema = z.enum(["answer", "fastAnswer"]);
1005
- const StreamDataAnswerTypeEnum = StreamDataAnswerTypeSchema.enum;
1006
- const ToolAnswerSchema = z.object({
1007
- type: StreamDataAnswerTypeSchema,
1008
- content: z.string()
1009
- });
1010
- const ToolStreamMessageSchema = z.discriminatedUnion("type", [
1011
- z.object({
1012
- type: z.literal(StreamMessageTypeEnum.response),
1013
- data: ToolHandlerReturnSchema
1014
- }),
1015
- z.object({
1016
- type: z.literal(StreamMessageTypeEnum.stream),
1017
- data: ToolAnswerSchema
1018
- }),
1019
- z.object({
1020
- type: z.literal(StreamMessageTypeEnum.error),
1021
- data: z.string()
1022
- })
1023
- ]);
1024
- const ToolRunInputSchema = z.object({
1025
- pluginId: z.string(),
1026
- version: z.preprocess((value) => {
1027
- if (value === "") return void 0;
1028
- return value;
1029
- }, z.string().optional()),
1030
- source: PluginSourceSchema.optional(),
1031
- childId: z.string().optional(),
1032
- input: z.record(z.string(), z.unknown()),
1033
- secrets: z.record(z.string(), z.unknown()).optional(),
1034
- systemVar: SystemVarSchema
1035
- });
1036
-
1037
1258
  //#endregion
1038
1259
  //#region ../../packages/infrastructure/src/plugin/plugin-runtime/ports/channel/event/client.ts
1039
1260
  /**
@@ -1171,7 +1392,9 @@ const PluginChannelMessageIdSchema = z.union([z.string(), z.number()]);
1171
1392
  const PluginChannelErrorSchema = z.object({
1172
1393
  code: z.string(),
1173
1394
  message: z.string(),
1174
- data: z.unknown().optional()
1395
+ reason: I18nStringSchema.optional(),
1396
+ data: z.unknown().optional(),
1397
+ cause: z.lazy(() => PluginChannelErrorSchema).optional()
1175
1398
  });
1176
1399
  /**
1177
1400
  * request message:需要对端返回 success/error。
@@ -1467,7 +1690,7 @@ function createIncomingStream({ source, streamName, streamId, traceId, meta }) {
1467
1690
  };
1468
1691
  }
1469
1692
  function toStreamData(source) {
1470
- if (source instanceof StreamData) return source;
1693
+ if (isStreamDataLike$1(source)) return source;
1471
1694
  const output = StreamData.create();
1472
1695
  (async () => {
1473
1696
  try {
@@ -1479,6 +1702,9 @@ function toStreamData(source) {
1479
1702
  })();
1480
1703
  return output;
1481
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
+ }
1482
1708
 
1483
1709
  //#endregion
1484
1710
  //#region src/debug/session.ts
@@ -1643,10 +1869,13 @@ function createVirtualHostHandler(uploadDir) {
1643
1869
  async function readSourceToBuffer(source) {
1644
1870
  if (!source) return Buffer.alloc(0);
1645
1871
  const chunks = [];
1646
- const iterable = source instanceof StreamData ? source.values() : source;
1872
+ const iterable = isStreamDataLike(source) ? source.values() : source;
1647
1873
  for await (const chunk of iterable) chunks.push(toBuffer(chunk));
1648
1874
  return Buffer.concat(chunks);
1649
1875
  }
1876
+ function isStreamDataLike(source) {
1877
+ return source instanceof StreamData || typeof source === "object" && source !== null && typeof source.values === "function" && typeof source.consume === "function";
1878
+ }
1650
1879
  function toBuffer(value) {
1651
1880
  if (Buffer.isBuffer(value)) return value;
1652
1881
  if (value instanceof Uint8Array) return Buffer.from(value);
@@ -1714,22 +1943,960 @@ async function assertPathExists(targetPath, message) {
1714
1943
  }
1715
1944
  }
1716
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
+
1717
2862
  //#endregion
1718
2863
  //#region src/commands/debug.ts
1719
2864
  var DebugCommand = class extends BaseCommand {
1720
2865
  register(parent) {
1721
- parent.command("debug <entry>").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 的输出目录").action(async (entry, opts) => {
1722
- await this.run(entry, opts);
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);
1723
2868
  });
1724
2869
  }
1725
- async run(entry, options) {
1726
- const entryDir = path.resolve(entry);
1727
- const uploadDir = path.resolve(options.uploadDir ?? path.join(entryDir, ".fastgpt-plugin-debug/uploads"));
1728
- const session = await loadDebugSession({
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({
1729
2886
  entryDir,
1730
- 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);
1731
2896
  });
1732
- this.printSnapshot(session.snapshot, uploadDir);
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;
1733
2900
  if (!options.run) return;
1734
2901
  const input = await this.readJsonOption(options.input, options.inputFile, "input");
1735
2902
  const secrets = await this.readJsonOption(options.secrets, options.secretsFile, "secrets");
@@ -1810,6 +2977,11 @@ var DebugCommand = class extends BaseCommand {
1810
2977
  JSON.stringify(this.createInputExample(tool.inputSchema))
1811
2978
  ].map(quoteShellArg).join(" ");
1812
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
+ }
1813
2985
  createInputExample(schema) {
1814
2986
  if (Array.isArray(schema.enum) && schema.enum.length > 0) return schema.enum[0];
1815
2987
  if ("const" in schema) return schema.const;
@@ -1853,6 +3025,38 @@ function quoteShellArg(value) {
1853
3025
  if (/^[a-zA-Z0-9_./:@-]+$/.test(value)) return value;
1854
3026
  return `'${value.replace(/'/g, `'"'"'`)}'`;
1855
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
+ };
1856
3060
 
1857
3061
  //#endregion
1858
3062
  //#region src/commands/pack.ts
@@ -1942,6 +3146,7 @@ function createProgram() {
1942
3146
  new CheckCommand().register(program);
1943
3147
  new CreateCommand().register(program);
1944
3148
  new DebugCommand().register(program);
3149
+ new DevCommand().register(program);
1945
3150
  new PackCommand().register(program);
1946
3151
  return program;
1947
3152
  }