@fastgpt-plugin/cli 0.2.0-alpha.1 → 0.2.0-alpha.3

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 CHANGED
@@ -46,6 +46,8 @@ fastgpt-plugin dev --no-interactive \
46
46
  --connect "fpg_dbg_..."
47
47
  ```
48
48
 
49
+ After `--connect` starts and connects successfully, it overwrites the persisted connection key in local `config.json`, so later `fastgpt-plugin dev` runs can reuse it. In the TUI, press `c` to enter and save a new connection key. Stop the session with `Ctrl+C`; press `Ctrl+C` again to force exit.
50
+
49
51
  When passing a raw connection key, set `FASTGPT_PLUGIN_DEBUG_CONNECT_URL`, or set `FASTGPT_PLUGIN_SERVER_URL` so the CLI can request `/api/plugin/debug-sessions/connection-key:exchange`. Full connect links remain supported for compatibility. The exchange result returns the gateway WSS endpoint, `debug:tmbId:{tmbId}` source, and scoped connect token. The CLI does not need `CONNECTION_GATEWAY_AUTH_TOKEN` or `JWT_SECRET`.
50
52
 
51
53
  When no plugin directories are passed, `dev` auto-discovers plugins from the current directory. If the current directory has `index.ts`, it is used as the plugin entry; otherwise, the CLI scans one level of child directories for `index.ts`. You can still pass multiple plugin directories explicitly.
package/README.md CHANGED
@@ -46,6 +46,8 @@ fastgpt-plugin dev --no-interactive \
46
46
  --connect "fpg_dbg_..."
47
47
  ```
48
48
 
49
+ `--connect` 启动并成功连接后会覆盖本地持久配置 `config.json` 里的 connection key,后续 `fastgpt-plugin dev` 可直接复用该配置。TUI 运行中按 `c` 可以重新输入并保存 connection key;停止会话使用 `Ctrl+C`,再次按 `Ctrl+C` 强制退出。
50
+
49
51
  使用裸 connection key 时,需要设置 `FASTGPT_PLUGIN_DEBUG_CONNECT_URL`,或设置 `FASTGPT_PLUGIN_SERVER_URL` 让 CLI 默认请求 `/api/plugin/debug-sessions/connection-key:exchange`。兼容场景仍可传入完整 connect link。exchange 结果会返回 gateway WSS 地址、`debug:tmbId:{tmbId}` source 和 scoped connect token。CLI 不需要 `CONNECTION_GATEWAY_AUTH_TOKEN` 或 `JWT_SECRET`。
50
52
 
51
53
  `dev` 未传插件目录时会自动探测当前目录:如果当前目录有 `index.ts`,则把当前目录作为插件;否则扫描当前目录下一层子目录中的 `index.ts`。也可以手动传入多个插件目录。
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { registerHooks, stripTypeScriptTypes } from "node:module";
2
+ import { createRequire, registerHooks, stripTypeScriptTypes } from "node:module";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -526,7 +526,7 @@ var CheckCommand = class extends BaseCommand {
526
526
  //#endregion
527
527
  //#region package.json
528
528
  var name = "@fastgpt-plugin/cli";
529
- var version = "0.2.0-alpha.1";
529
+ var version = "0.2.0-alpha.3";
530
530
 
531
531
  //#endregion
532
532
  //#region src/constants.ts
@@ -686,6 +686,40 @@ var CreateCommand = class extends BaseCommand {
686
686
  }
687
687
  };
688
688
 
689
+ //#endregion
690
+ //#region ../../packages/domain/src/value-objects/plugin-debug-session.vo.ts
691
+ const PLUGIN_DEBUG_SOURCE_KIND = "debug";
692
+ const PLUGIN_DEBUG_SESSION_SOURCE_TMB_ID_KEY = "tmbId";
693
+ const PLUGIN_DEBUG_SOURCE_PREFIX = `${PLUGIN_DEBUG_SOURCE_KIND}:`;
694
+ const PLUGIN_DEBUG_SESSION_SOURCE_FORMAT = `${PLUGIN_DEBUG_SOURCE_KIND}:${PLUGIN_DEBUG_SESSION_SOURCE_TMB_ID_KEY}:{tmbId}`;
695
+ const PluginDebugSessionIdSchema = z.string().min(1);
696
+ const PluginDebugSessionTmbIdSchema = z.string().min(1);
697
+ const PluginDebugSessionSourceSchema = z.string().min(1).refine((source) => parsePluginDebugSessionSource(source) !== null, { message: "Invalid plugin debug session source" });
698
+ const PluginDebugSessionStatusSchema = z.enum([
699
+ "enabled",
700
+ "connected",
701
+ "disconnected",
702
+ "revoked"
703
+ ]);
704
+ const PluginDebugSessionSchema = z.object({
705
+ tmbId: PluginDebugSessionTmbIdSchema,
706
+ source: PluginDebugSessionSourceSchema,
707
+ status: PluginDebugSessionStatusSchema,
708
+ enabled: z.boolean(),
709
+ keyId: z.string().min(1),
710
+ connectionKeyHash: z.string().min(1),
711
+ connectionKey: z.string().min(1).optional(),
712
+ createdAt: z.number().int().positive(),
713
+ updatedAt: z.number().int().positive(),
714
+ refreshedAt: z.number().int().positive().optional(),
715
+ revokedAt: z.number().int().positive().optional()
716
+ });
717
+ function parsePluginDebugSessionSource(source) {
718
+ const parts = source.split(":");
719
+ if (parts.length !== 3 || parts[0] !== PLUGIN_DEBUG_SOURCE_KIND || parts[1] !== PLUGIN_DEBUG_SESSION_SOURCE_TMB_ID_KEY || !parts[2]) return null;
720
+ return { tmbId: parts[2] };
721
+ }
722
+
689
723
  //#endregion
690
724
  //#region src/debug/discover.ts
691
725
  const IGNORED_DIRS = new Set([
@@ -777,6 +811,12 @@ const ConnectionGatewayWsHeartbeatMessageSchema = z.object({
777
811
  type: z.literal("heartbeat"),
778
812
  ts: z.number().int().positive()
779
813
  });
814
+ const ConnectionGatewayWsMetadataMessageSchema = z.object({
815
+ protocol: ConnectionGatewayWsProtocolSchema,
816
+ type: z.literal("metadata"),
817
+ requestId: z.string().min(1).optional(),
818
+ metadata: z.record(z.string(), z.unknown())
819
+ });
780
820
  const ConnectionGatewayWsErrorMessageSchema = z.object({
781
821
  protocol: ConnectionGatewayWsProtocolSchema,
782
822
  type: z.literal("error"),
@@ -793,7 +833,8 @@ const ConnectionGatewayWsBoundMessageSchema = z.object({
793
833
  const ConnectionGatewayWsClientMessageSchema = z.discriminatedUnion("type", [
794
834
  ConnectionGatewayWsBindMessageSchema,
795
835
  ConnectionGatewayWsEnvelopeMessageSchema,
796
- ConnectionGatewayWsHeartbeatMessageSchema
836
+ ConnectionGatewayWsHeartbeatMessageSchema,
837
+ ConnectionGatewayWsMetadataMessageSchema
797
838
  ]);
798
839
  const ConnectionGatewayWsServerMessageSchema = z.discriminatedUnion("type", [
799
840
  ConnectionGatewayWsBoundMessageSchema,
@@ -997,7 +1038,11 @@ const RESOLVE_EXTENSIONS = [
997
1038
  ".cjs",
998
1039
  ".json"
999
1040
  ];
1041
+ const TRANSFORM_PROBE_SOURCE = "const __fastgptPluginTransformProbe__: number = 1;";
1000
1042
  let hooksRegistered = false;
1043
+ let transformModeSupported;
1044
+ let cachedTypeScriptCompiler;
1045
+ const localRequire = createRequire(import.meta.url);
1001
1046
  function ensureDebugImportHooks() {
1002
1047
  if (hooksRegistered) return;
1003
1048
  hooksRegistered = true;
@@ -1021,15 +1066,90 @@ function ensureDebugImportHooks() {
1021
1066
  if (SOURCE_EXTENSIONS.includes(ext)) return {
1022
1067
  format: "module",
1023
1068
  shortCircuit: true,
1024
- source: stripTypeScriptTypes(fs$1.readFileSync(fileURLToPath(parsed), "utf-8"), {
1025
- mode: "transform",
1026
- sourceMap: false
1069
+ source: compileTypeScriptModuleSource(fs$1.readFileSync(fileURLToPath(parsed), "utf-8"), {
1070
+ filePath: fileURLToPath(parsed),
1071
+ workspaceRoot
1027
1072
  })
1028
1073
  };
1029
1074
  return nextLoad(url, context);
1030
1075
  }
1031
1076
  });
1032
1077
  }
1078
+ function compileTypeScriptModuleSource(sourceText, { filePath, workspaceRoot, stripTypeScriptTypesFn = stripTypeScriptTypes, typeScriptCompiler }) {
1079
+ if (supportsTransformMode(stripTypeScriptTypesFn)) try {
1080
+ return stripTypeScriptTypesFn(sourceText, {
1081
+ mode: "transform",
1082
+ sourceMap: false
1083
+ });
1084
+ } catch {}
1085
+ return transpileTypeScriptModuleSource(sourceText, {
1086
+ filePath,
1087
+ workspaceRoot,
1088
+ typeScriptCompiler
1089
+ });
1090
+ }
1091
+ function supportsTransformMode(stripTypeScriptTypesFn) {
1092
+ if (stripTypeScriptTypesFn === stripTypeScriptTypes) {
1093
+ if (transformModeSupported !== void 0) return transformModeSupported;
1094
+ transformModeSupported = canTransformTypeScript(stripTypeScriptTypesFn);
1095
+ return transformModeSupported;
1096
+ }
1097
+ return canTransformTypeScript(stripTypeScriptTypesFn);
1098
+ }
1099
+ function canTransformTypeScript(stripTypeScriptTypesFn) {
1100
+ try {
1101
+ stripTypeScriptTypesFn(TRANSFORM_PROBE_SOURCE, {
1102
+ mode: "transform",
1103
+ sourceMap: false
1104
+ });
1105
+ return true;
1106
+ } catch {
1107
+ return false;
1108
+ }
1109
+ }
1110
+ function transpileTypeScriptModuleSource(sourceText, { filePath, workspaceRoot, typeScriptCompiler }) {
1111
+ const compiler = typeScriptCompiler ?? loadTypeScriptCompiler(filePath, workspaceRoot);
1112
+ const transpiled = compiler.transpileModule(sourceText, {
1113
+ fileName: filePath,
1114
+ reportDiagnostics: true,
1115
+ compilerOptions: {
1116
+ module: compiler.ModuleKind.ESNext,
1117
+ target: compiler.ScriptTarget.ES2022,
1118
+ moduleResolution: compiler.ModuleResolutionKind.Bundler,
1119
+ jsx: compiler.JsxEmit.ReactJSX,
1120
+ sourceMap: false,
1121
+ inlineSourceMap: false,
1122
+ inlineSources: false,
1123
+ isolatedModules: true,
1124
+ esModuleInterop: true,
1125
+ allowImportingTsExtensions: true,
1126
+ verbatimModuleSyntax: true
1127
+ }
1128
+ });
1129
+ const errors = transpiled.diagnostics?.filter((diagnostic) => diagnostic.category === compiler.DiagnosticCategory.Error) ?? [];
1130
+ if (errors.length > 0) {
1131
+ const formatted = errors.map((diagnostic) => formatDiagnosticMessage(compiler, diagnostic)).join("\n");
1132
+ throw new Error(`Failed to transpile TypeScript debug module: ${filePath}\n${formatted}`);
1133
+ }
1134
+ return transpiled.outputText;
1135
+ }
1136
+ function loadTypeScriptCompiler(filePath, workspaceRoot) {
1137
+ if (cachedTypeScriptCompiler) return cachedTypeScriptCompiler;
1138
+ const resolvePaths = [
1139
+ path.dirname(filePath),
1140
+ process.cwd(),
1141
+ workspaceRoot,
1142
+ path.dirname(fileURLToPath(import.meta.url))
1143
+ ].filter((value) => Boolean(value));
1144
+ cachedTypeScriptCompiler = localRequire(localRequire.resolve("typescript", { paths: resolvePaths }));
1145
+ return cachedTypeScriptCompiler;
1146
+ }
1147
+ function formatDiagnosticMessage(compiler, diagnostic) {
1148
+ const message = compiler.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
1149
+ if (!diagnostic.file || diagnostic.start === void 0) return message;
1150
+ const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
1151
+ return `${line + 1}:${character + 1} ${message}`;
1152
+ }
1033
1153
  function resolveSpecifier({ specifier, parentURL, workspaceRoot }) {
1034
1154
  if (specifier.startsWith("node:")) return null;
1035
1155
  if (specifier.startsWith("file:")) return toFileURL(resolveExistingPath(fileURLToPath(new URL(specifier))));
@@ -1191,9 +1311,188 @@ const InvokeMethodEnumSchema = z.enum([
1191
1311
  ]);
1192
1312
  const InvokeMethodEnum = InvokeMethodEnumSchema.enum;
1193
1313
 
1314
+ //#endregion
1315
+ //#region ../../packages/domain/src/value-objects/error.vo.ts
1316
+ const DEFAULT_INTERNAL_REASON = {
1317
+ en: "Internal Server Error",
1318
+ "zh-CN": "服务器内部错误"
1319
+ };
1320
+ var RegisteredError = class extends Error {
1321
+ code;
1322
+ reason;
1323
+ httpStatus;
1324
+ telemetryKind;
1325
+ visibility;
1326
+ severity;
1327
+ data;
1328
+ constructor(definition, options = {}) {
1329
+ super(options.message ?? definition.message, { cause: normalizeErrorCause(options.cause) });
1330
+ this.name = "RegisteredError";
1331
+ this.code = definition.code;
1332
+ this.reason = options.reason ?? definition.reason;
1333
+ this.httpStatus = definition.httpStatus ?? 500;
1334
+ this.telemetryKind = definition.telemetryKind;
1335
+ this.visibility = definition.visibility ?? "public";
1336
+ this.severity = definition.severity ?? "expected";
1337
+ this.data = options.data;
1338
+ }
1339
+ };
1340
+ function createReasonError(reason, options = {}) {
1341
+ const cause = normalizeErrorCause(options.cause);
1342
+ const error = cause === void 0 ? new Error(reason.en) : new Error(reason.en, { cause });
1343
+ return Object.assign(error, { reason });
1344
+ }
1345
+ function normalizeToError(error, fallbackMessage = "Unknown error") {
1346
+ if (error instanceof Error) return error;
1347
+ if (typeof error === "string") return new Error(error);
1348
+ if (isI18nString(error)) return createReasonError(error);
1349
+ if (isResultFailureLike(error)) {
1350
+ const reason = isI18nString(error.reason) ? error.reason : void 0;
1351
+ const cause = normalizeToError(error.error, reason?.en ?? fallbackMessage);
1352
+ if (reason && isSameI18nString(reason, getErrorReason(cause))) return cause;
1353
+ return reason ? createReasonError(reason, { cause }) : new Error(fallbackMessage, { cause });
1354
+ }
1355
+ return new Error(getUnknownErrorMessage(error, fallbackMessage), { cause: error });
1356
+ }
1357
+ function getErrorReason(error, fallback) {
1358
+ if (error instanceof RegisteredError) return error.visibility === "internal" ? DEFAULT_INTERNAL_REASON : error.reason;
1359
+ if (isI18nString(error)) return error;
1360
+ const record = isRecord(error) ? error : void 0;
1361
+ if (record && isI18nString(record.reason)) return record.reason;
1362
+ if (record && isI18nString(record.error)) return record.error;
1363
+ if (fallback) return fallback;
1364
+ if (error instanceof Error) return {
1365
+ en: error.message,
1366
+ "zh-CN": error.message
1367
+ };
1368
+ if (typeof error === "string") return {
1369
+ en: error,
1370
+ "zh-CN": error
1371
+ };
1372
+ return DEFAULT_INTERNAL_REASON;
1373
+ }
1374
+ function serializeError(error, options) {
1375
+ return serializeErrorInner(error, {
1376
+ includeStack: options?.includeStack ?? false,
1377
+ depth: 0,
1378
+ seen: /* @__PURE__ */ new WeakSet()
1379
+ });
1380
+ }
1381
+ function normalizeErrorCause(cause) {
1382
+ if (cause === void 0 || cause instanceof Error) return cause;
1383
+ if (typeof cause === "string") return new Error(cause);
1384
+ if (isI18nString(cause)) return createReasonError(cause);
1385
+ if (isResultFailureLike(cause)) return normalizeToError(cause);
1386
+ return cause;
1387
+ }
1388
+ function serializeErrorInner(error, state) {
1389
+ if (state.depth > 5) return {
1390
+ name: "Error",
1391
+ message: "Error cause depth exceeded"
1392
+ };
1393
+ if (error instanceof RegisteredError) return {
1394
+ name: error.name,
1395
+ code: error.code,
1396
+ message: error.message,
1397
+ reason: error.reason,
1398
+ httpStatus: error.httpStatus,
1399
+ telemetryKind: error.telemetryKind,
1400
+ visibility: error.visibility,
1401
+ data: error.data,
1402
+ ...state.includeStack && error.stack !== void 0 ? { stack: error.stack } : {},
1403
+ ...error.cause !== void 0 ? { cause: serializeErrorInner(error.cause, nextSerializeState(state, error)) } : {}
1404
+ };
1405
+ if (error instanceof Error) {
1406
+ const reason = getI18nStringField(error, "reason");
1407
+ return {
1408
+ name: error.name,
1409
+ code: getStringField(error, "code"),
1410
+ message: error.message,
1411
+ ...reason !== void 0 ? { reason } : {},
1412
+ data: getUnknownField(error, "data"),
1413
+ ...state.includeStack && error.stack !== void 0 ? { stack: error.stack } : {},
1414
+ ...error.cause !== void 0 ? { cause: serializeErrorInner(error.cause, nextSerializeState(state, error)) } : {}
1415
+ };
1416
+ }
1417
+ if (typeof error === "string") return {
1418
+ name: "Error",
1419
+ message: error
1420
+ };
1421
+ return {
1422
+ name: "Error",
1423
+ message: getUnknownErrorMessage(error, "Unknown error")
1424
+ };
1425
+ }
1426
+ function nextSerializeState(state, error) {
1427
+ if (state.seen.has(error)) return {
1428
+ ...state,
1429
+ depth: 6
1430
+ };
1431
+ state.seen.add(error);
1432
+ return {
1433
+ ...state,
1434
+ depth: state.depth + 1
1435
+ };
1436
+ }
1437
+ function getUnknownErrorMessage(error, fallback) {
1438
+ if (error === null || error === void 0) return fallback;
1439
+ if (isRecord(error)) {
1440
+ const message = error.message ?? error.msg;
1441
+ if (typeof message === "string" && message.length > 0) return message;
1442
+ }
1443
+ return String(error);
1444
+ }
1445
+ function getStringField(error, field) {
1446
+ const value = error[field];
1447
+ return typeof value === "string" ? value : void 0;
1448
+ }
1449
+ function getI18nStringField(error, field) {
1450
+ const value = error[field];
1451
+ return isI18nString(value) ? value : void 0;
1452
+ }
1453
+ function getUnknownField(error, field) {
1454
+ return error[field];
1455
+ }
1456
+ function isI18nString(value) {
1457
+ return isRecord(value) && typeof value.en === "string" && (value["zh-CN"] === void 0 || typeof value["zh-CN"] === "string") && (value["zh-Hant"] === void 0 || typeof value["zh-Hant"] === "string");
1458
+ }
1459
+ function isSameI18nString(left, right) {
1460
+ return left.en === right.en && left["zh-CN"] === right["zh-CN"] && left["zh-Hant"] === right["zh-Hant"];
1461
+ }
1462
+ function isRecord(value) {
1463
+ return Boolean(value && typeof value === "object");
1464
+ }
1465
+ function isResultFailureLike(value) {
1466
+ return isRecord(value) && "error" in value && ("reason" in value || isRecord(value.error));
1467
+ }
1468
+
1194
1469
  //#endregion
1195
1470
  //#region ../../packages/domain/src/value-objects/result.vo.ts
1196
1471
  const successResult = (data) => [data, null];
1472
+ function failureResult(reasonOrFailure, error) {
1473
+ if (isResultFailure(reasonOrFailure)) return [null, toResultFailure(reasonOrFailure.reason, reasonOrFailure.error, reasonOrFailure)];
1474
+ if (reasonOrFailure instanceof Error) return [null, toResultFailure(getErrorReason(reasonOrFailure), reasonOrFailure)];
1475
+ return [null, toResultFailure(reasonOrFailure, error === void 0 ? createReasonError(reasonOrFailure) : createLegacyFailureError(reasonOrFailure, error))];
1476
+ }
1477
+ function isResultFailure(value) {
1478
+ return Boolean(value && typeof value === "object" && "reason" in value && "error" in value);
1479
+ }
1480
+ function createLegacyFailureError(reason, cause) {
1481
+ return createReasonError(reason, { cause: normalizeToError(cause, reason.en) });
1482
+ }
1483
+ function toResultFailure(reason, error, existing) {
1484
+ const serialized = serializeError(error);
1485
+ const code = existing?.code ?? serialized.code;
1486
+ const message = existing?.message ?? serialized.message;
1487
+ const data = existing && "data" in existing ? existing.data : serialized.data;
1488
+ return {
1489
+ reason,
1490
+ error,
1491
+ ...code !== void 0 ? { code } : {},
1492
+ ...message !== void 0 ? { message } : {},
1493
+ ...data !== void 0 ? { data } : {}
1494
+ };
1495
+ }
1197
1496
 
1198
1497
  //#endregion
1199
1498
  //#region ../../packages/domain/src/value-objects/stream.vo.ts
@@ -1690,7 +1989,7 @@ function createIncomingStream({ source, streamName, streamId, traceId, meta }) {
1690
1989
  };
1691
1990
  }
1692
1991
  function toStreamData(source) {
1693
- if (isStreamDataLike$1(source)) return source;
1992
+ if (isStreamDataLike$2(source)) return source;
1694
1993
  const output = StreamData.create();
1695
1994
  (async () => {
1696
1995
  try {
@@ -1702,7 +2001,7 @@ function toStreamData(source) {
1702
2001
  })();
1703
2002
  return output;
1704
2003
  }
1705
- function isStreamDataLike$1(source) {
2004
+ function isStreamDataLike$2(source) {
1706
2005
  return source instanceof StreamData || typeof source === "object" && source !== null && typeof source.values === "function" && typeof source.consume === "function";
1707
2006
  }
1708
2007
 
@@ -1737,7 +2036,7 @@ async function loadDebugSession({ entryDir, uploadDir }) {
1737
2036
  else process.env.RUNTIME_MODE = previousRuntimeMode;
1738
2037
  }
1739
2038
  }
1740
- async function runDebugTool({ runtime, snapshot, toolId, input, secrets, systemVar }) {
2039
+ async function runDebugTool({ runtime, snapshot, toolId, input, secrets, systemVar, traceId }) {
1741
2040
  const targetTool = pickTargetTool(snapshot, toolId);
1742
2041
  const mergedSystemVar = createDebugSystemVar(snapshot, targetTool.id, systemVar);
1743
2042
  const response = await runtime.invokePlugin("run", {
@@ -1745,7 +2044,7 @@ async function runDebugTool({ runtime, snapshot, toolId, input, secrets, systemV
1745
2044
  systemVar: mergedSystemVar,
1746
2045
  ...snapshot.isToolSet ? { childId: targetTool.id } : {},
1747
2046
  ...secrets ? { secrets } : {}
1748
- }, { traceId: randomUUID() });
2047
+ }, { traceId: traceId ?? randomUUID() });
1749
2048
  if (!response.output) throw new Error("调试运行未返回输出流。");
1750
2049
  const streamMessages = [];
1751
2050
  let finalResponse;
@@ -1805,7 +2104,7 @@ function createDebugSnapshot(tool, location) {
1805
2104
  author: getOptionalString(userManifest.author),
1806
2105
  tags: toStringArray(userManifest.tags),
1807
2106
  permissions: toStringArray(userManifest.permission),
1808
- secretSchema: ensurePlainObject(secretSchema),
2107
+ secretSchema: ensurePlainObject$1(secretSchema),
1809
2108
  isToolSet: children.length > 0,
1810
2109
  tools
1811
2110
  };
@@ -1818,8 +2117,8 @@ function createSingleToolSnapshot(tool, userManifest) {
1818
2117
  name: pickLocalizedText(userManifest.name),
1819
2118
  description: pickLocalizedText(userManifest.description),
1820
2119
  toolDescription: pickToolDescription(getOptionalString(userManifest.toolDescription), userManifest.description),
1821
- inputSchema: ensurePlainObject(z.toJSONSchema(handler.inputSchema)),
1822
- outputSchema: ensurePlainObject(z.toJSONSchema(handler.outputSchema))
2120
+ inputSchema: ensurePlainObject$1(z.toJSONSchema(handler.inputSchema)),
2121
+ outputSchema: ensurePlainObject$1(z.toJSONSchema(handler.outputSchema))
1823
2122
  };
1824
2123
  }
1825
2124
  function createChildToolSnapshot(tool, child) {
@@ -1830,8 +2129,8 @@ function createChildToolSnapshot(tool, child) {
1830
2129
  name: pickLocalizedText(child.name),
1831
2130
  description: pickLocalizedText(child.description),
1832
2131
  toolDescription: pickToolDescription(child.toolDescription, child.description),
1833
- inputSchema: ensurePlainObject(z.toJSONSchema(handler.inputSchema)),
1834
- outputSchema: ensurePlainObject(z.toJSONSchema(handler.outputSchema))
2132
+ inputSchema: ensurePlainObject$1(z.toJSONSchema(handler.inputSchema)),
2133
+ outputSchema: ensurePlainObject$1(z.toJSONSchema(handler.outputSchema))
1835
2134
  };
1836
2135
  }
1837
2136
  function pickTargetTool(snapshot, toolId) {
@@ -1843,10 +2142,10 @@ function pickTargetTool(snapshot, toolId) {
1843
2142
  }
1844
2143
  function createVirtualHostHandler(uploadDir) {
1845
2144
  return async ({ method, args, input }) => {
1846
- const invokeArgs = ensurePlainObject(args);
2145
+ const invokeArgs = ensurePlainObject$1(args);
1847
2146
  switch (method) {
1848
2147
  case InvokeMethodEnum.uploadFile: {
1849
- const buffer = await readSourceToBuffer(input);
2148
+ const buffer = await readSourceToBuffer$1(input);
1850
2149
  const fileName = sanitizeFileName(typeof invokeArgs.fileName === "string" && invokeArgs.fileName.trim().length > 0 ? invokeArgs.fileName : "debug-upload.bin");
1851
2150
  const contentType = typeof invokeArgs.contentType === "string" && invokeArgs.contentType.trim().length > 0 ? invokeArgs.contentType : "application/octet-stream";
1852
2151
  const saveDir = path.resolve(uploadDir);
@@ -1866,17 +2165,17 @@ function createVirtualHostHandler(uploadDir) {
1866
2165
  }
1867
2166
  };
1868
2167
  }
1869
- async function readSourceToBuffer(source) {
2168
+ async function readSourceToBuffer$1(source) {
1870
2169
  if (!source) return Buffer.alloc(0);
1871
2170
  const chunks = [];
1872
- const iterable = isStreamDataLike(source) ? source.values() : source;
1873
- for await (const chunk of iterable) chunks.push(toBuffer(chunk));
2171
+ const iterable = isStreamDataLike$1(source) ? source.values() : source;
2172
+ for await (const chunk of iterable) chunks.push(toBuffer$1(chunk));
1874
2173
  return Buffer.concat(chunks);
1875
2174
  }
1876
- function isStreamDataLike(source) {
2175
+ function isStreamDataLike$1(source) {
1877
2176
  return source instanceof StreamData || typeof source === "object" && source !== null && typeof source.values === "function" && typeof source.consume === "function";
1878
2177
  }
1879
- function toBuffer(value) {
2178
+ function toBuffer$1(value) {
1880
2179
  if (Buffer.isBuffer(value)) return value;
1881
2180
  if (value instanceof Uint8Array) return Buffer.from(value);
1882
2181
  if (typeof value === "string") return Buffer.from(value);
@@ -1914,7 +2213,7 @@ function toStringArray(value) {
1914
2213
  const items = value.filter((item) => typeof item === "string");
1915
2214
  return items.length > 0 ? items : void 0;
1916
2215
  }
1917
- function ensurePlainObject(value) {
2216
+ function ensurePlainObject$1(value) {
1918
2217
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1919
2218
  }
1920
2219
  function sanitizeFileName(fileName) {
@@ -1945,6 +2244,8 @@ async function assertPathExists(targetPath, message) {
1945
2244
 
1946
2245
  //#endregion
1947
2246
  //#region src/debug/gateway.ts
2247
+ const GATEWAY_DUPLICATE_CONNECTION_CODE = "connection_gateway.session_already_bound";
2248
+ const GATEWAY_HEARTBEAT_INTERVAL_MS = 5e3;
1948
2249
  async function connectDebugGateway({ targets, options, onLog }) {
1949
2250
  if (targets.length === 0) throw new Error("Gateway debug targets cannot be empty");
1950
2251
  if (options.reconnect) return connectReconnectingDebugGateway({
@@ -1963,19 +2264,32 @@ async function connectReconnectingDebugGateway({ targets, options, onLog }) {
1963
2264
  let current = null;
1964
2265
  let currentSession = null;
1965
2266
  let latestTargets = targets;
2267
+ let hasAttemptedConnection = false;
2268
+ let currentOptions = options;
2269
+ const resolveReconnectOptions = options.resolveReconnectOptions;
1966
2270
  const intervalMs = Math.max(100, options.reconnectIntervalMs ?? 2e3);
1967
2271
  const closedPromise = (async () => {
1968
2272
  while (!closed) {
1969
2273
  try {
2274
+ const attemptOptions = hasAttemptedConnection && resolveReconnectOptions ? await resolveReconnectOptions() : currentOptions;
2275
+ hasAttemptedConnection = true;
2276
+ currentOptions = {
2277
+ ...attemptOptions,
2278
+ resolveReconnectOptions
2279
+ };
1970
2280
  current = await connectSingleDebugGateway({
1971
2281
  targets: latestTargets,
1972
- options,
2282
+ options: currentOptions,
1973
2283
  onLog
1974
2284
  });
1975
2285
  currentSession = current.session;
1976
2286
  await current.closed;
1977
2287
  } catch (error) {
1978
2288
  if (closed) return;
2289
+ if (isDuplicateConnectionError(error)) {
2290
+ closed = true;
2291
+ throw error;
2292
+ }
1979
2293
  onLog?.(`Connection Gateway 调试通道断开: ${formatErrorMessage(error)}`);
1980
2294
  } finally {
1981
2295
  current = null;
@@ -1994,6 +2308,9 @@ async function connectReconnectingDebugGateway({ targets, options, onLog }) {
1994
2308
  latestTargets = targets;
1995
2309
  current?.updateTargets(targets);
1996
2310
  },
2311
+ reconnect() {
2312
+ current?.close();
2313
+ },
1997
2314
  close() {
1998
2315
  closed = true;
1999
2316
  current?.close();
@@ -2006,6 +2323,7 @@ async function connectSingleDebugGateway({ targets, options, onLog }) {
2006
2323
  let targetsByPluginId = createTargetsByPluginId(targets);
2007
2324
  let closed = false;
2008
2325
  let session = null;
2326
+ let stopHeartbeat = () => void 0;
2009
2327
  let boundResolve;
2010
2328
  let boundReject;
2011
2329
  const bound = new Promise((resolve, reject) => {
@@ -2015,6 +2333,7 @@ async function connectSingleDebugGateway({ targets, options, onLog }) {
2015
2333
  const closedPromise = new Promise((resolve) => {
2016
2334
  socket.addEventListener("close", () => {
2017
2335
  closed = true;
2336
+ stopHeartbeat();
2018
2337
  resolve();
2019
2338
  });
2020
2339
  socket.addEventListener("error", () => {
@@ -2030,7 +2349,7 @@ async function connectSingleDebugGateway({ targets, options, onLog }) {
2030
2349
  }
2031
2350
  if (message.type === "heartbeat") return;
2032
2351
  if (message.type === "error") {
2033
- const error = new Error(message.message);
2352
+ const error = createGatewayError(message.code, message.message);
2034
2353
  if (!session) boundReject(error);
2035
2354
  onLog?.(`Connection Gateway 错误: ${message.code} ${message.message}`);
2036
2355
  return;
@@ -2063,19 +2382,56 @@ async function connectSingleDebugGateway({ targets, options, onLog }) {
2063
2382
  throw error;
2064
2383
  });
2065
2384
  const boundSession = await bound;
2385
+ stopHeartbeat = startGatewayHeartbeat(socket);
2066
2386
  onLog?.(`已连接 Connection Gateway session: ${boundSession.id}`);
2067
2387
  return {
2068
2388
  session: boundSession,
2069
2389
  updateTargets(targets) {
2070
2390
  targetsByPluginId = createTargetsByPluginId(targets);
2391
+ updateGatewayMetadata({
2392
+ socket,
2393
+ targets,
2394
+ options,
2395
+ onLog
2396
+ });
2071
2397
  onLog?.(`已热更新本地调试插件: ${targets.map((target) => target.snapshot.pluginId).join(", ")}`);
2072
2398
  },
2073
2399
  close() {
2074
- if (!closed) socket.close();
2400
+ if (!closed) {
2401
+ stopHeartbeat();
2402
+ socket.close();
2403
+ }
2075
2404
  },
2076
2405
  closed: closedPromise
2077
2406
  };
2078
2407
  }
2408
+ async function updateGatewayMetadata({ socket, targets, options, onLog }) {
2409
+ try {
2410
+ await sendMessage(socket, {
2411
+ protocol: "connection-gateway.ws.v1",
2412
+ type: "metadata",
2413
+ requestId: randomUUID(),
2414
+ metadata: makePluginDebugMetadata(targets, options.source)
2415
+ });
2416
+ } catch (error) {
2417
+ onLog?.(`Connection Gateway metadata 更新失败: ${formatErrorMessage(error)}`);
2418
+ }
2419
+ }
2420
+ function startGatewayHeartbeat(socket) {
2421
+ const timer = setInterval(() => {
2422
+ sendMessage(socket, {
2423
+ protocol: "connection-gateway.ws.v1",
2424
+ type: "heartbeat",
2425
+ ts: Date.now()
2426
+ }).catch(() => {
2427
+ socket.close();
2428
+ });
2429
+ }, GATEWAY_HEARTBEAT_INTERVAL_MS);
2430
+ if (typeof timer === "object" && timer && "unref" in timer) timer.unref();
2431
+ return () => {
2432
+ clearInterval(timer);
2433
+ };
2434
+ }
2079
2435
  function createTargetsByPluginId(targets) {
2080
2436
  return new Map(targets.map((target) => [target.snapshot.pluginId, target]));
2081
2437
  }
@@ -2088,6 +2444,7 @@ async function handleGatewayEnvelope({ socket, session, targetsByPluginId, envel
2088
2444
  if (!pluginId) throw new Error("Gateway debug request missing pluginId");
2089
2445
  const target = targetsByPluginId.get(pluginId);
2090
2446
  if (!target) throw new Error(`Gateway debug target not found: ${source} ${pluginId}`);
2447
+ const traceId = envelope.traceId ?? requiredRequestId(envelope);
2091
2448
  onLog?.(`收到远程调试请求: ${source} ${pluginId} ${envelope.requestId ?? "-"}`);
2092
2449
  await sendEnvelope(socket, {
2093
2450
  protocol: "connection-gateway.v1",
@@ -2101,15 +2458,21 @@ async function handleGatewayEnvelope({ socket, session, targetsByPluginId, envel
2101
2458
  createdAt: Date.now(),
2102
2459
  payload: { kind: "plugin-debug.accepted" }
2103
2460
  });
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);
2461
+ target.invokeBridge?.bind(traceId, { invokeToken: request.payload.systemVar.invokeToken });
2462
+ try {
2463
+ const result = await runDebugTool({
2464
+ runtime: target.runtime,
2465
+ snapshot: target.snapshot,
2466
+ toolId: request.payload.childId,
2467
+ input: request.payload.input,
2468
+ secrets: request.payload.secrets,
2469
+ systemVar: request.payload.systemVar,
2470
+ traceId
2471
+ });
2472
+ for (const message of result.streamMessages) await sendStreamChunk(socket, session, envelope, message);
2473
+ } finally {
2474
+ target.invokeBridge?.release(traceId);
2475
+ }
2113
2476
  await sendEnvelope(socket, {
2114
2477
  protocol: "connection-gateway.v1",
2115
2478
  sessionId: session.id,
@@ -2212,11 +2575,245 @@ function delay(ms) {
2212
2575
  function formatErrorMessage(error) {
2213
2576
  return error instanceof Error ? error.message : String(error);
2214
2577
  }
2578
+ function createGatewayError(code, message) {
2579
+ const error = new Error(message);
2580
+ Object.assign(error, { code });
2581
+ return error;
2582
+ }
2583
+ function isDuplicateConnectionError(error) {
2584
+ return error instanceof Error && ("code" in error ? error.code === GATEWAY_DUPLICATE_CONNECTION_CODE : false);
2585
+ }
2215
2586
  function parseWsServerMessage(data) {
2216
2587
  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
2588
  return ConnectionGatewayWsServerMessageSchema.parse(JSON.parse(text));
2218
2589
  }
2219
2590
 
2591
+ //#endregion
2592
+ //#region ../../packages/infrastructure/src/plugin/invoke/invoke.impl.ts
2593
+ var InvokeManager = class {
2594
+ constructor(deps) {
2595
+ this.deps = deps;
2596
+ }
2597
+ async getWecomCorpToken() {
2598
+ const [result, responseErr] = await this.requestFastGPT({
2599
+ path: "/api/proApi/support/wecom/getCorpToken",
2600
+ init: { method: "GET" },
2601
+ failureReason: {
2602
+ en: "Get wecom corp token failed",
2603
+ "zh-CN": "获取企业微信企业令牌失败"
2604
+ },
2605
+ invalidJsonReason: {
2606
+ en: "Invalid wecom corp token output",
2607
+ "zh-CN": "获取企业微信企业令牌返回数据格式错误"
2608
+ }
2609
+ });
2610
+ if (responseErr) return failureResult(responseErr);
2611
+ return successResult(result);
2612
+ }
2613
+ async userInfo() {
2614
+ const [result, responseErr] = await this.requestFastGPT({
2615
+ path: "/api/invoke/userInfo",
2616
+ init: { method: "GET" },
2617
+ failureReason: {
2618
+ en: "Invoke user info failed",
2619
+ "zh-CN": "反向调用用户信息失败"
2620
+ },
2621
+ invalidJsonReason: {
2622
+ en: "Invalid invoke user info output",
2623
+ "zh-CN": "反向调用用户信息返回数据格式错误"
2624
+ }
2625
+ });
2626
+ if (responseErr) return failureResult(responseErr);
2627
+ try {
2628
+ return successResult(InvokeUserInfoOutputSchema.parse(result));
2629
+ } catch (error) {
2630
+ return failureResult({
2631
+ en: "Invalid invoke user info output",
2632
+ "zh-CN": "反向调用用户信息返回数据格式错误"
2633
+ }, error);
2634
+ }
2635
+ }
2636
+ async uploadFile(input) {
2637
+ let request;
2638
+ try {
2639
+ request = await this.buildUploadFileRequest(input);
2640
+ } catch (error) {
2641
+ return failureResult({
2642
+ en: "Invoke upload file failed",
2643
+ "zh-CN": "反向调用上传文件失败"
2644
+ }, error);
2645
+ }
2646
+ const [result, responseErr] = await this.requestFastGPT({
2647
+ path: "/api/invoke/fileUpload",
2648
+ init: {
2649
+ method: "POST",
2650
+ body: request.formData
2651
+ },
2652
+ failureReason: {
2653
+ en: "Invoke upload file failed",
2654
+ "zh-CN": "反向调用上传文件失败"
2655
+ },
2656
+ invalidJsonReason: {
2657
+ en: "Invalid invoke upload file output",
2658
+ "zh-CN": "反向调用上传文件返回数据格式错误"
2659
+ }
2660
+ });
2661
+ if (responseErr) return failureResult(responseErr);
2662
+ try {
2663
+ return successResult(InvokeUploadFileOutputSchema.parse({ accessURL: result.url }));
2664
+ } catch (error) {
2665
+ return failureResult({
2666
+ en: "Invalid invoke upload file output",
2667
+ "zh-CN": "反向调用上传文件返回数据格式错误"
2668
+ }, error);
2669
+ }
2670
+ }
2671
+ buildUrl(path) {
2672
+ return new URL(path, this.deps.fastgptBaseUrl).toString();
2673
+ }
2674
+ async buildUploadFileRequest(input) {
2675
+ const formData = new FormData();
2676
+ const fileBuffer = await this.toBuffer(input.file);
2677
+ const fileName = input.fileName ?? randomUUID();
2678
+ const contentType = input.contentType ?? "application/octet-stream";
2679
+ const blobPart = new Uint8Array(fileBuffer.byteLength);
2680
+ blobPart.set(fileBuffer);
2681
+ formData.append("file", new Blob([blobPart], { type: contentType }), fileName);
2682
+ if (input.fileName) formData.append("fileName", input.fileName);
2683
+ if (input.contentType) formData.append("contentType", input.contentType);
2684
+ return {
2685
+ formData,
2686
+ meta: {
2687
+ fileName,
2688
+ contentType
2689
+ }
2690
+ };
2691
+ }
2692
+ async toBuffer(file) {
2693
+ if (Buffer.isBuffer(file)) return file;
2694
+ if (file instanceof Readable$1) {
2695
+ const chunks = [];
2696
+ for await (const chunk of file) chunks.push(this.toBufferChunk(chunk));
2697
+ return Buffer.concat(chunks);
2698
+ }
2699
+ return Buffer.from(file);
2700
+ }
2701
+ toBufferChunk(chunk) {
2702
+ if (Buffer.isBuffer(chunk)) return chunk;
2703
+ if (chunk instanceof Uint8Array) return Buffer.from(chunk);
2704
+ if (chunk instanceof ArrayBuffer) return Buffer.from(chunk);
2705
+ if (ArrayBuffer.isView(chunk)) return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
2706
+ if (typeof chunk === "string") return Buffer.from(chunk);
2707
+ if (typeof chunk === "number") return Buffer.from([chunk]);
2708
+ if (this.isSerializedBufferChunk(chunk)) return Buffer.from(chunk.data);
2709
+ throw new TypeError(`Unsupported upload file stream chunk type: ${Object.prototype.toString.call(chunk)}`);
2710
+ }
2711
+ isSerializedBufferChunk(value) {
2712
+ return this.isRecord(value) && value.type === "Buffer" && Array.isArray(value.data) && value.data.every((item) => Number.isInteger(item) && Number.isSafeInteger(item) && item >= 0 && item <= 255);
2713
+ }
2714
+ async requestFastGPT({ path, init, failureReason, invalidJsonReason }) {
2715
+ const headers = new Headers(init?.headers);
2716
+ headers.set("Authorization", `Bearer ${this.deps.token}`);
2717
+ let response;
2718
+ try {
2719
+ response = await fetch(this.buildUrl(path), {
2720
+ ...init,
2721
+ headers
2722
+ });
2723
+ } catch (error) {
2724
+ return failureResult(failureReason, error);
2725
+ }
2726
+ if (!response.ok) return failureResult(failureReason, await this.readErrorResponse(response));
2727
+ let payload;
2728
+ try {
2729
+ payload = await response.json();
2730
+ } catch (error) {
2731
+ return failureResult(invalidJsonReason, error);
2732
+ }
2733
+ return this.parseResponse(payload, failureReason);
2734
+ }
2735
+ parseResponse(payload, fallbackReason) {
2736
+ if (!this.isRecord(payload)) return successResult(payload);
2737
+ if (payload.success === false) return failureResult({
2738
+ en: this.getResponseMessage(payload) ?? fallbackReason.en,
2739
+ "zh-CN": this.getResponseMessage(payload) ?? fallbackReason["zh-CN"]
2740
+ }, payload);
2741
+ if (typeof payload.code === "number" && payload.code >= 400) return failureResult({
2742
+ en: this.getResponseMessage(payload) ?? fallbackReason.en,
2743
+ "zh-CN": this.getResponseMessage(payload) ?? fallbackReason["zh-CN"]
2744
+ }, payload);
2745
+ return successResult("data" in payload ? payload.data : payload);
2746
+ }
2747
+ async readErrorResponse(response) {
2748
+ return {
2749
+ status: response.status,
2750
+ statusText: response.statusText,
2751
+ body: (await response.text().catch(() => "")).slice(0, 1e3)
2752
+ };
2753
+ }
2754
+ getResponseMessage(payload) {
2755
+ return typeof payload.message === "string" ? payload.message : void 0;
2756
+ }
2757
+ isRecord(value) {
2758
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
2759
+ }
2760
+ };
2761
+
2762
+ //#endregion
2763
+ //#region src/debug/remote-invoke.ts
2764
+ var RemoteDebugInvokeBridge = class {
2765
+ sessions = /* @__PURE__ */ new Map();
2766
+ getFastgptBaseUrl;
2767
+ constructor(fastgptBaseUrl) {
2768
+ this.getFastgptBaseUrl = typeof fastgptBaseUrl === "function" ? fastgptBaseUrl : () => fastgptBaseUrl;
2769
+ }
2770
+ attach(runtime) {
2771
+ runtime.setHostRequestHandler((request) => this.handleHostRequest(request));
2772
+ }
2773
+ bind(traceId, input) {
2774
+ this.sessions.set(traceId, { invokeToken: input.invokeToken });
2775
+ }
2776
+ release(traceId) {
2777
+ this.sessions.delete(traceId);
2778
+ }
2779
+ async handleHostRequest({ method, args, input, traceId }) {
2780
+ if (!traceId) throw new Error("远程调试反向调用缺少 traceId。");
2781
+ const session = this.sessions.get(traceId);
2782
+ if (!session) throw new Error("远程调试反向调用会话不存在或已结束。");
2783
+ const invoke = new InvokeManager({
2784
+ token: session.invokeToken,
2785
+ fastgptBaseUrl: this.getFastgptBaseUrl()
2786
+ });
2787
+ switch (method) {
2788
+ case InvokeMethodEnum.uploadFile: return invoke.uploadFile({
2789
+ ...ensurePlainObject(args),
2790
+ file: input ? await readSourceToBuffer(input) : Buffer.alloc(0)
2791
+ });
2792
+ case InvokeMethodEnum.userInfo: return invoke.userInfo();
2793
+ case InvokeMethodEnum.wecomCorpToken: return invoke.getWecomCorpToken();
2794
+ default: throw new Error(`远程调试宿主暂不支持反向调用: ${String(method)}`);
2795
+ }
2796
+ }
2797
+ };
2798
+ function ensurePlainObject(value) {
2799
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
2800
+ }
2801
+ async function readSourceToBuffer(source) {
2802
+ const chunks = [];
2803
+ const iterable = isStreamDataLike(source) ? source.values() : source;
2804
+ for await (const chunk of iterable) chunks.push(toBuffer(chunk));
2805
+ return Buffer.concat(chunks);
2806
+ }
2807
+ function isStreamDataLike(source) {
2808
+ return source instanceof StreamData || typeof source === "object" && source !== null && typeof source.values === "function" && typeof source.consume === "function";
2809
+ }
2810
+ function toBuffer(value) {
2811
+ if (Buffer.isBuffer(value)) return value;
2812
+ if (value instanceof Uint8Array) return Buffer.from(value);
2813
+ if (typeof value === "string") return Buffer.from(value);
2814
+ return Buffer.from(JSON.stringify(value));
2815
+ }
2816
+
2220
2817
  //#endregion
2221
2818
  //#region src/debug/remote-session.ts
2222
2819
  async function runRemoteDebugSession({ entriesInput, options, commandName, migrationHint }) {
@@ -2247,6 +2844,11 @@ async function runRemoteDebugSessionOnce({ entriesInput, options, commandName })
2247
2844
  commandName,
2248
2845
  targets
2249
2846
  });
2847
+ let currentGatewayOptions = gatewayOptions;
2848
+ gatewayOptions.onResolvedReconnectOptions = (nextOptions) => {
2849
+ currentGatewayOptions = nextOptions;
2850
+ };
2851
+ attachRemoteInvokeBridges(targets, () => currentGatewayOptions.fastgptBaseUrl);
2250
2852
  const reporter = createRemoteDebugReporter({
2251
2853
  commandName,
2252
2854
  options,
@@ -2264,6 +2866,20 @@ async function runRemoteDebugSessionOnce({ entriesInput, options, commandName })
2264
2866
  gateway.close();
2265
2867
  if (force) process.exit(130);
2266
2868
  });
2869
+ reporter.attachConfigureConnectionKey(async () => {
2870
+ await configureConnectionKey({
2871
+ options,
2872
+ commandName,
2873
+ targets,
2874
+ reconnect: () => {
2875
+ if (gateway.reconnect) {
2876
+ gateway.reconnect();
2877
+ return;
2878
+ }
2879
+ gateway.close();
2880
+ }
2881
+ });
2882
+ });
2267
2883
  const source = gateway.session.sessionScope.source ?? gatewayOptions.source ?? "-";
2268
2884
  reporter.ready(source);
2269
2885
  await gateway.closed;
@@ -2284,6 +2900,11 @@ async function runWatchRemoteDebugSession({ entriesInput, options, commandName }
2284
2900
  commandName,
2285
2901
  targets: initialTargets
2286
2902
  });
2903
+ let currentGatewayOptions = gatewayOptions;
2904
+ gatewayOptions.onResolvedReconnectOptions = (nextOptions) => {
2905
+ currentGatewayOptions = nextOptions;
2906
+ };
2907
+ attachRemoteInvokeBridges(initialTargets, () => currentGatewayOptions.fastgptBaseUrl);
2287
2908
  const reporter = createRemoteDebugReporter({
2288
2909
  commandName,
2289
2910
  options,
@@ -2294,6 +2915,7 @@ async function runWatchRemoteDebugSession({ entriesInput, options, commandName }
2294
2915
  let reloadPending = false;
2295
2916
  let closed = false;
2296
2917
  let gateway;
2918
+ let activeTargets = initialTargets;
2297
2919
  const reloadTargets = async () => {
2298
2920
  if (reloadRunning) {
2299
2921
  reloadPending = true;
@@ -2305,6 +2927,8 @@ async function runWatchRemoteDebugSession({ entriesInput, options, commandName }
2305
2927
  reloadPending = false;
2306
2928
  try {
2307
2929
  const nextTargets = await loadTargets(entries, options);
2930
+ attachRemoteInvokeBridges(nextTargets, () => currentGatewayOptions.fastgptBaseUrl);
2931
+ activeTargets = nextTargets;
2308
2932
  gateway?.updateTargets(nextTargets);
2309
2933
  reporter.log(`检测到插件文件变化,已热更新 ${nextTargets.length} 个本地插件。`);
2310
2934
  } catch (error) {
@@ -2332,6 +2956,20 @@ async function runWatchRemoteDebugSession({ entriesInput, options, commandName }
2332
2956
  connectedGateway.close();
2333
2957
  if (force) process.exit(130);
2334
2958
  });
2959
+ reporter.attachConfigureConnectionKey(async () => {
2960
+ await configureConnectionKey({
2961
+ options,
2962
+ commandName,
2963
+ targets: activeTargets,
2964
+ reconnect: () => {
2965
+ if (connectedGateway.reconnect) {
2966
+ connectedGateway.reconnect();
2967
+ return;
2968
+ }
2969
+ connectedGateway.close();
2970
+ }
2971
+ });
2972
+ });
2335
2973
  const source = connectedGateway.session.sessionScope.source ?? gatewayOptions.source ?? "-";
2336
2974
  reporter.ready(source);
2337
2975
  await connectedGateway.closed;
@@ -2369,6 +3007,13 @@ async function loadTargets(entries, options) {
2369
3007
  if (duplicatePluginIds.length > 0) throw new Error(`gateway pluginId 重复: ${duplicatePluginIds.join(", ")}`);
2370
3008
  return targets;
2371
3009
  }
3010
+ function attachRemoteInvokeBridges(targets, getFastgptBaseUrl) {
3011
+ targets.forEach((target) => {
3012
+ const invokeBridge = new RemoteDebugInvokeBridge(getFastgptBaseUrl);
3013
+ invokeBridge.attach(target.runtime);
3014
+ target.invokeBridge = invokeBridge;
3015
+ });
3016
+ }
2372
3017
  function watchDebugEntries(entries, onChange) {
2373
3018
  const watchers = entries.map((entry) => fsWatch(entry, {
2374
3019
  recursive: true,
@@ -2400,7 +3045,10 @@ function createRemoteDebugReporter({ commandName, options, gatewayOptions, targe
2400
3045
  return options.interactive !== false && Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY) ? new TuiRemoteDebugReporter(summary) : new PlainRemoteDebugReporter(summary);
2401
3046
  }
2402
3047
  async function ensureConnectLink({ options, commandName, targets }) {
2403
- if (options.connect) return;
3048
+ if (options.connect) {
3049
+ options.connectPersistOnSuccess = true;
3050
+ return;
3051
+ }
2404
3052
  const savedConnect = await readSavedConnectionKey();
2405
3053
  if (savedConnect) {
2406
3054
  options.connect = savedConnect;
@@ -2428,12 +3076,12 @@ async function promptConnectLink({ commandName, targets, defaultValue, errorMess
2428
3076
  "",
2429
3077
  ...errorMessage ? [`Last error ${errorMessage}`, ""] : [],
2430
3078
  "Paste the FastGPT connection key or connect link to start the WSS debug session.",
3079
+ ...defaultValue ? ["Press Enter to reuse the current value, or type a new one to replace it.", ""] : [],
2431
3080
  ""
2432
3081
  ].join("\n"));
2433
3082
  return (await input({
2434
3083
  message: "FastGPT connection key",
2435
3084
  default: defaultValue,
2436
- prefill: defaultValue ? "editable" : void 0,
2437
3085
  validate(value) {
2438
3086
  return value.trim().length > 0 || "请输入 FastGPT connection key";
2439
3087
  }
@@ -2463,6 +3111,16 @@ async function saveConnectionKey(connectionKey) {
2463
3111
  mode: 384
2464
3112
  });
2465
3113
  }
3114
+ async function configureConnectionKey({ options, commandName, targets, reconnect }) {
3115
+ options.connect = await promptConnectLink({
3116
+ commandName,
3117
+ targets,
3118
+ defaultValue: options.connect
3119
+ });
3120
+ options.connectPersistOnSuccess = true;
3121
+ logger.info("已更新 FastGPT connection key,重连成功后会保存到本地配置。");
3122
+ reconnect();
3123
+ }
2466
3124
  async function readCliConfig() {
2467
3125
  const configPath = getCliConfigPath();
2468
3126
  let raw;
@@ -2494,10 +3152,20 @@ const CLI_CONFIG_FILE_NAME = "config.json";
2494
3152
  const CLI_CONFIG_DIR_NAME = "fastgpt-plugin";
2495
3153
  const CliConfigSchema = z.object({ debug: z.object({ connectionKey: z.string().min(1).optional() }).optional() }).passthrough();
2496
3154
  var PlainRemoteDebugReporter = class {
3155
+ closeSession;
3156
+ interruptCount = 0;
3157
+ onSigint = () => {
3158
+ this.requestClose("SIGINT");
3159
+ };
3160
+ onSigterm = () => {
3161
+ this.requestClose("SIGTERM");
3162
+ };
2497
3163
  constructor(summary) {
2498
3164
  this.summary = summary;
2499
3165
  }
2500
3166
  start() {
3167
+ process.on("SIGINT", this.onSigint);
3168
+ process.on("SIGTERM", this.onSigterm);
2501
3169
  logger.info([
2502
3170
  "FastGPT 插件开发会话",
2503
3171
  ` command: fastgpt-plugin ${this.summary.commandName}`,
@@ -2509,6 +3177,13 @@ var PlainRemoteDebugReporter = class {
2509
3177
  ...this.summary.targets.map(formatTargetLine)
2510
3178
  ].join("\n"));
2511
3179
  }
3180
+ requestClose(signal) {
3181
+ this.interruptCount += 1;
3182
+ const force = this.interruptCount >= 2;
3183
+ this.log(force ? `再次收到 ${signal},正在强制退出。` : `收到 ${signal},正在关闭远程调试会话。再次发送 ${signal} 强制退出。`);
3184
+ this.closeSession?.({ force });
3185
+ if (force && !this.closeSession) process.exit(130);
3186
+ }
2512
3187
  log(message) {
2513
3188
  logger.info(message);
2514
3189
  }
@@ -2518,8 +3193,13 @@ var PlainRemoteDebugReporter = class {
2518
3193
  });
2519
3194
  logger.info(`已建立 1 个远程调试通道,挂载 ${this.summary.targets.length} 个插件,按 Ctrl+C 停止。`);
2520
3195
  }
2521
- attachClose() {}
3196
+ attachClose(close) {
3197
+ this.closeSession = close;
3198
+ }
3199
+ attachConfigureConnectionKey() {}
2522
3200
  end() {
3201
+ process.off("SIGINT", this.onSigint);
3202
+ process.off("SIGTERM", this.onSigterm);
2523
3203
  logger.info("远程调试会话已结束。");
2524
3204
  }
2525
3205
  };
@@ -2529,36 +3209,34 @@ var TuiRemoteDebugReporter = class {
2529
3209
  logs = [];
2530
3210
  instance;
2531
3211
  closeSession;
3212
+ configureConnectionKey;
2532
3213
  interruptCount = 0;
3214
+ configuring = false;
2533
3215
  onSigint = () => {
2534
- this.requestClose("ctrl-c");
3216
+ this.requestClose("SIGINT");
2535
3217
  };
2536
- requestClose = (reason) => {
2537
- if (reason === "q") {
2538
- this.log("收到退出指令,正在关闭远程调试会话。");
2539
- this.closeSession?.({ force: false });
2540
- return;
2541
- }
3218
+ requestClose = (signal) => {
2542
3219
  this.interruptCount += 1;
2543
3220
  const force = this.interruptCount >= 2;
2544
3221
  this.status = force ? "closed" : "closing";
2545
- this.log(force ? "再次收到 Ctrl+C,正在强制退出。" : "收到 Ctrl+C,正在关闭远程调试会话。再次按 Ctrl+C 强制退出。");
3222
+ this.log(force ? `再次收到 ${signal},正在强制退出。` : `收到 ${signal},正在关闭远程调试会话。再次按 Ctrl+C 强制退出。`);
2546
3223
  this.closeSession?.({ force });
2547
3224
  if (force && !this.closeSession) process.exit(130);
2548
3225
  };
3226
+ requestConfigureConnectionKey = () => {
3227
+ if (this.configuring) return;
3228
+ if (!this.configureConnectionKey) {
3229
+ this.log("connection key 配置入口尚未就绪。");
3230
+ return;
3231
+ }
3232
+ this.runConfigureConnectionKey();
3233
+ };
2549
3234
  constructor(summary) {
2550
3235
  this.summary = summary;
2551
3236
  }
2552
3237
  start() {
2553
3238
  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
- });
3239
+ this.mount();
2562
3240
  }
2563
3241
  log(message) {
2564
3242
  this.logs.push({
@@ -2576,39 +3254,78 @@ var TuiRemoteDebugReporter = class {
2576
3254
  attachClose(close) {
2577
3255
  this.closeSession = close;
2578
3256
  }
3257
+ attachConfigureConnectionKey(handler) {
3258
+ this.configureConnectionKey = handler;
3259
+ }
2579
3260
  async end() {
2580
3261
  process.off("SIGINT", this.onSigint);
2581
3262
  this.status = "closed";
2582
3263
  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
- }
3264
+ await this.unmount();
2590
3265
  logger.info("远程调试会话已结束。");
2591
3266
  }
2592
3267
  rerender() {
2593
3268
  this.instance?.rerender(this.createApp());
2594
3269
  }
3270
+ mount() {
3271
+ if (this.instance) return;
3272
+ this.instance = render(this.createApp(), {
3273
+ stdin: process.stdin,
3274
+ stdout: process.stdout,
3275
+ stderr: process.stderr,
3276
+ exitOnCtrlC: false,
3277
+ interactive: true,
3278
+ patchConsole: false
3279
+ });
3280
+ }
3281
+ async unmount() {
3282
+ const instance = this.instance;
3283
+ this.instance = void 0;
3284
+ if (!instance) return;
3285
+ await Promise.race([instance.waitUntilRenderFlush(), setTimeout$1(50)]).catch(() => void 0);
3286
+ instance.unmount();
3287
+ await Promise.race([instance.waitUntilExit(), setTimeout$1(50)]).catch(() => void 0);
3288
+ }
3289
+ async runConfigureConnectionKey() {
3290
+ if (!this.configureConnectionKey) return;
3291
+ const previousStatus = this.status;
3292
+ this.configuring = true;
3293
+ this.status = "configuring";
3294
+ this.rerender();
3295
+ try {
3296
+ await this.unmount();
3297
+ await this.configureConnectionKey();
3298
+ this.status = "connecting";
3299
+ this.log("已更新 connection key,正在重新连接 Connection Gateway。");
3300
+ } catch (error) {
3301
+ this.status = previousStatus;
3302
+ this.log(`connection key 配置失败: ${formatConnectionKeyExchangeError(error)}`);
3303
+ } finally {
3304
+ this.configuring = false;
3305
+ if (this.status !== "closed") {
3306
+ this.mount();
3307
+ this.rerender();
3308
+ }
3309
+ }
3310
+ }
2595
3311
  createApp() {
2596
3312
  return React.createElement(RemoteDebugInkApp, {
2597
3313
  summary: this.summary,
2598
3314
  status: this.status,
2599
3315
  source: this.source,
2600
3316
  logs: this.logs,
2601
- onQuit: this.requestClose
3317
+ onClose: () => this.requestClose("SIGINT"),
3318
+ onConfigureConnectionKey: this.requestConfigureConnectionKey
2602
3319
  });
2603
3320
  }
2604
3321
  };
2605
- function RemoteDebugInkApp({ summary, status, source, logs, onQuit }) {
3322
+ function RemoteDebugInkApp({ summary, status, source, logs, onClose, onConfigureConnectionKey }) {
2606
3323
  useInput((inputValue, key) => {
2607
3324
  if (key.ctrl && inputValue === "c") {
2608
- onQuit("ctrl-c");
3325
+ onClose();
2609
3326
  return;
2610
3327
  }
2611
- if (inputValue === "q") onQuit("q");
3328
+ if (inputValue === "c") onConfigureConnectionKey();
2612
3329
  });
2613
3330
  const statusMeta = getTuiStatusMeta(status);
2614
3331
  const sourceLabel = source === "-" ? "waiting for bind" : source;
@@ -2700,7 +3417,7 @@ function RemoteDebugInkApp({ summary, status, source, logs, onQuit }) {
2700
3417
  }, ...activityItems)), React.createElement(Box, {
2701
3418
  marginTop: 1,
2702
3419
  justifyContent: "space-between"
2703
- }, React.createElement(Text, { dimColor: true }, "q stop"), React.createElement(Text, { dimColor: true }, "Ctrl+C stop · second Ctrl+C force exit")));
3420
+ }, React.createElement(Text, { dimColor: true }, "c configure connection key"), React.createElement(Text, { dimColor: true }, "Ctrl+C stop · second Ctrl+C force exit")));
2704
3421
  }
2705
3422
  function getTuiStatusMeta(status) {
2706
3423
  if (status === "connected") return {
@@ -2717,6 +3434,13 @@ function getTuiStatusMeta(status) {
2717
3434
  color: "gray",
2718
3435
  borderColor: "gray"
2719
3436
  };
3437
+ if (status === "configuring") return {
3438
+ label: "CONFIGURE",
3439
+ marker: "◆",
3440
+ description: "Waiting for a new FastGPT connection key.",
3441
+ color: "cyanBright",
3442
+ borderColor: "cyan"
3443
+ };
2720
3444
  if (status === "closing") return {
2721
3445
  label: "CLOSING",
2722
3446
  marker: "◆",
@@ -2773,15 +3497,28 @@ function formatConnectionKeyExchangeError(error) {
2773
3497
  }
2774
3498
  async function resolveConnectGatewayOptions(options) {
2775
3499
  const info = await exchangeConnectLink(options.connect);
2776
- return {
3500
+ const resolvedOptions = {
2777
3501
  gatewayUrl: normalizeGatewayWsUrl(info.gatewayUrl),
2778
3502
  connectToken: info.connectToken,
2779
3503
  userId: info.tmbId,
2780
3504
  source: info.source,
3505
+ fastgptBaseUrl: info.fastgptBaseUrl,
2781
3506
  tokenTtlMs: Math.max(1, info.expiresAt - Date.now()),
2782
3507
  reconnect: options.noReconnect ? false : options.reconnect ?? true,
2783
3508
  reconnectIntervalMs: toPositiveInt(options.reconnectIntervalMs ?? process.env.CONNECTION_GATEWAY_RECONNECT_INTERVAL_MS ?? "2000", "reconnect-interval-ms")
2784
3509
  };
3510
+ resolvedOptions.resolveReconnectOptions = async () => {
3511
+ const nextOptions = await resolveConnectGatewayOptions(options);
3512
+ if (options.connectPersistOnSuccess === true) {
3513
+ await saveConnectionKey(options.connect);
3514
+ logger.info(`已保存 FastGPT connection key 到本地配置: ${getCliConfigPath()}`);
3515
+ options.connectPersistOnSuccess = false;
3516
+ }
3517
+ resolvedOptions.onResolvedReconnectOptions?.(nextOptions);
3518
+ nextOptions.onResolvedReconnectOptions = resolvedOptions.onResolvedReconnectOptions;
3519
+ return nextOptions;
3520
+ };
3521
+ return resolvedOptions;
2785
3522
  }
2786
3523
  function resolveUploadDir({ entryDir, index, total, uploadDir }) {
2787
3524
  if (!uploadDir) return path.resolve(path.join(entryDir, ".fastgpt-plugin-debug/uploads"));
@@ -2793,10 +3530,12 @@ const ConnectInfoSchema = z.object({
2793
3530
  transport: z.literal("websocket"),
2794
3531
  source: z.string().min(1),
2795
3532
  connectToken: z.string().min(1),
3533
+ fastgptBaseUrl: z.string().min(1).optional(),
2796
3534
  expiresAt: z.number().int().positive()
2797
3535
  });
2798
3536
  async function exchangeConnectLink(connectInput) {
2799
- const response = isHttpUrl(connectInput) ? await fetch(connectInput) : await fetch(resolveConnectionKeyExchangeUrl(), {
3537
+ const exchangeUrl = isHttpUrl(connectInput) ? connectInput : resolveConnectionKeyExchangeUrl();
3538
+ const response = isHttpUrl(connectInput) ? await fetch(exchangeUrl) : await fetch(exchangeUrl, {
2800
3539
  method: "POST",
2801
3540
  headers: { "Content-Type": "application/json" },
2802
3541
  body: JSON.stringify({ connectionKey: connectInput })
@@ -2807,6 +3546,7 @@ async function exchangeConnectLink(connectInput) {
2807
3546
  const info = ConnectInfoSchema.parse(payload.data ?? payload);
2808
3547
  return {
2809
3548
  ...info,
3549
+ fastgptBaseUrl: info.fastgptBaseUrl ?? new URL(exchangeUrl).origin,
2810
3550
  tmbId: parseTmbIdFromDebugSource(info.source)
2811
3551
  };
2812
3552
  }
@@ -2826,11 +3566,9 @@ function resolveConnectionKeyExchangeUrl() {
2826
3566
  return new URL("/api/plugin/debug-sessions/connection-key:exchange", baseUrl).toString();
2827
3567
  }
2828
3568
  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;
3569
+ const parsed = parsePluginDebugSessionSource(source);
3570
+ if (!parsed) throw new Error(`debug source 缺少 tmbId: ${source}`);
3571
+ return parsed.tmbId;
2834
3572
  }
2835
3573
  function normalizeGatewayWsUrl(gatewayUrl) {
2836
3574
  const parsed = new URL(gatewayUrl);
@@ -3160,9 +3898,6 @@ async function run(argv) {
3160
3898
 
3161
3899
  //#endregion
3162
3900
  //#region src/index.ts
3163
- process.on("SIGINT", () => {
3164
- process.exit(130);
3165
- });
3166
3901
  const main = async () => {
3167
3902
  try {
3168
3903
  await run();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastgpt-plugin/cli",
3
- "version": "0.2.0-alpha.1",
3
+ "version": "0.2.0-alpha.3",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "publishConfig": {