@fieldwangai/agentflow 0.1.165 → 0.1.167

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.
@@ -67,6 +67,7 @@ import {
67
67
  publishFlowSnippet,
68
68
  publishNodeFromInstance,
69
69
  } from "./marketplace.mjs";
70
+ import { markRepositoryIndexDirty } from "./repository-index.mjs";
70
71
  import { runGit } from "./git-worktree.mjs";
71
72
  import {
72
73
  authSetupRequired,
@@ -93,6 +94,13 @@ import {
93
94
  renderCliAuthorizationResult,
94
95
  } from "./cli-auth-page.mjs";
95
96
  import { readGlobalEnvRows, readUserEnvRows, writeGlobalEnvRows, writeUserEnvRows } from "./user-env.mjs";
97
+ import { runCursorAgentWithPrompt } from "./agent-runners.mjs";
98
+ import {
99
+ clearCursorApiKeyCooldown,
100
+ getCursorApiKeyModelSelection,
101
+ getCursorApiKeyPoolStatuses,
102
+ parseCursorApiKeyRecords,
103
+ } from "./cursor-api-key-pool.mjs";
96
104
  import {
97
105
  readAdminBuiltinPipelineConfig,
98
106
  updateAdminBuiltinPipelineConfig,
@@ -208,8 +216,84 @@ const MIME = {
208
216
  const ADMIN_ONLY_USER_ENV_KEYS = new Set([
209
217
  "CURSOR_API_KEYS",
210
218
  "AGENTFLOW_CURSOR_API_KEY_COOLDOWN_MINUTES",
219
+ "AGENTFLOW_CURSOR_API_KEY_RESOURCE_EXHAUSTED_COOLDOWN_MINUTES",
211
220
  "CURSOR_API_KEY_COOLDOWN_MINUTES",
212
221
  ]);
222
+ const activeCursorApiKeyTests = new Set();
223
+
224
+ function cursorApiKeyRowsForScope(userCtx = {}, scope = "global") {
225
+ const rows = scope === "user" ? readUserEnvRows(userCtx.userId) : readGlobalEnvRows();
226
+ const poolRow = rows.find((row) => String(row?.key || "").trim() === "CURSOR_API_KEYS");
227
+ return parseCursorApiKeyRecords(poolRow?.value || "");
228
+ }
229
+
230
+ function maskCursorApiKeyForAdmin(key) {
231
+ const value = String(key || "").trim();
232
+ if (!value) return "";
233
+ if (value.length <= 10) return `${"•".repeat(Math.max(0, value.length - 4))}${value.slice(-4)}`;
234
+ return `${value.slice(0, 4)}${"•".repeat(Math.min(18, value.length - 8))}${value.slice(-4)}`;
235
+ }
236
+
237
+ function cursorApiKeyAdminMetadata(userCtx = {}, scope = "global") {
238
+ const records = cursorApiKeyRowsForScope(userCtx, scope);
239
+ const statuses = new Map(getCursorApiKeyPoolStatuses(records).map((item) => [item.id, item]));
240
+ return records.map((record) => ({
241
+ id: record.id,
242
+ name: record.name,
243
+ maskedKey: maskCursorApiKeyForAdmin(record.key),
244
+ createdAt: record.createdAt || "",
245
+ ...(statuses.get(record.id) || {}),
246
+ }));
247
+ }
248
+
249
+ async function probeCursorApiKey(root, record) {
250
+ const startedAt = Date.now();
251
+ const modelSelection = getCursorApiKeyModelSelection(record.id) || { lane: "auto", modelId: "auto", modelName: "Auto" };
252
+ const selection = { ...record, index: 0, total: 1, modelSelection };
253
+ let replyPreview = "";
254
+ let timedOut = false;
255
+ const handle = runCursorAgentWithPrompt(root, "你好。这是连通性测试,请不要调用任何工具,只回复“你好”。", {
256
+ env: { CURSOR_API_KEYS: JSON.stringify([record]), AGENTFLOW_USER_ID: "" },
257
+ _agentflowCursorApiKeyAttempts: [selection],
258
+ mode: "ask",
259
+ force: false,
260
+ sandboxDisabled: false,
261
+ approveMcps: false,
262
+ onStreamEvent(event) {
263
+ if (event?.type !== "natural" || !event?.text) return;
264
+ if (event.kind === "result" || !replyPreview) replyPreview = String(event.text).trim().slice(0, 500);
265
+ },
266
+ });
267
+ const timeout = setTimeout(() => {
268
+ timedOut = true;
269
+ try { handle.child.kill("SIGTERM"); } catch (_) {}
270
+ }, 30_000);
271
+ try {
272
+ await handle.finished;
273
+ if (timedOut) throw new Error("Cursor API Key 测试超时(30 秒)");
274
+ return {
275
+ success: true,
276
+ testedAt: new Date().toISOString(),
277
+ durationMs: Date.now() - startedAt,
278
+ replyPreview,
279
+ modelId: modelSelection.modelId,
280
+ modelName: modelSelection.modelName,
281
+ modelLane: modelSelection.lane,
282
+ };
283
+ } catch (error) {
284
+ return {
285
+ success: false,
286
+ testedAt: new Date().toISOString(),
287
+ durationMs: Date.now() - startedAt,
288
+ errorPreview: timedOut ? "Cursor API Key 测试超时(30 秒)" : String(error?.message || error).slice(0, 500),
289
+ modelId: modelSelection.modelId,
290
+ modelName: modelSelection.modelName,
291
+ modelLane: modelSelection.lane,
292
+ };
293
+ } finally {
294
+ clearTimeout(timeout);
295
+ }
296
+ }
213
297
 
214
298
  const UI_SERVER_STARTED_AT = new Date().toISOString();
215
299
  const UI_SERVER_APP_VERSION = (() => {
@@ -2183,6 +2267,9 @@ export function startUiServer({
2183
2267
  for (const item of summary.needsDecision) log.warn(`[storage-migration] needs decision: ${item}`);
2184
2268
  for (const item of summary.failed) log.warn(`[storage-migration] failed: ${item}`);
2185
2269
  }
2270
+ // Repository discovery is derived data. Reconcile it off the request path so a missing or stale
2271
+ // index delays startup work, not the first person opening the repository page.
2272
+ markRepositoryIndexDirty(root);
2186
2273
  const server = http.createServer(async (req, res) => {
2187
2274
  const url = new URL(req.url || "/", "http://127.0.0.1");
2188
2275
  const reqStart = Date.now();
@@ -3680,6 +3767,84 @@ export function startUiServer({
3680
3767
  return;
3681
3768
  }
3682
3769
 
3770
+ if (req.method === "GET" && url.pathname === "/api/admin/cursor-api-keys/status") {
3771
+ if (!authUser?.isAdmin) {
3772
+ json(res, 403, { error: "Admin permission required" });
3773
+ return;
3774
+ }
3775
+ try {
3776
+ const scope = url.searchParams.get("scope") === "user" ? "user" : "global";
3777
+ json(res, 200, { scope, keys: cursorApiKeyAdminMetadata(userCtx, scope) });
3778
+ } catch (e) {
3779
+ json(res, 500, { error: (e && e.message) || String(e) });
3780
+ }
3781
+ return;
3782
+ }
3783
+
3784
+ if (req.method === "PATCH" && url.pathname === "/api/admin/cursor-api-keys/cooldown") {
3785
+ if (!authUser?.isAdmin) {
3786
+ json(res, 403, { error: "Admin permission required" });
3787
+ return;
3788
+ }
3789
+ let payload;
3790
+ try {
3791
+ payload = JSON.parse(await readBody(req));
3792
+ } catch {
3793
+ json(res, 400, { error: "Invalid JSON body" });
3794
+ return;
3795
+ }
3796
+ const scope = payload?.scope === "user" ? "user" : "global";
3797
+ const id = String(payload?.id || "").trim();
3798
+ const record = cursorApiKeyRowsForScope(userCtx, scope).find((item) => item.id === id);
3799
+ if (!record) {
3800
+ json(res, 404, { error: "Cursor API Key not found" });
3801
+ return;
3802
+ }
3803
+ clearCursorApiKeyCooldown(record);
3804
+ json(res, 200, { success: true, scope, keys: cursorApiKeyAdminMetadata(userCtx, scope) });
3805
+ return;
3806
+ }
3807
+
3808
+ if (req.method === "POST" && url.pathname === "/api/admin/cursor-api-keys/test") {
3809
+ if (!authUser?.isAdmin) {
3810
+ json(res, 403, { error: "Admin permission required" });
3811
+ return;
3812
+ }
3813
+ let payload;
3814
+ try {
3815
+ payload = JSON.parse(await readBody(req));
3816
+ } catch {
3817
+ json(res, 400, { error: "Invalid JSON body" });
3818
+ return;
3819
+ }
3820
+ const scope = payload?.scope === "user" ? "user" : "global";
3821
+ const id = String(payload?.id || "").trim();
3822
+ const record = cursorApiKeyRowsForScope(userCtx, scope).find((item) => item.id === id);
3823
+ if (!record) {
3824
+ json(res, 404, { error: "Cursor API Key not found" });
3825
+ return;
3826
+ }
3827
+ const testId = `${scope}:${record.id}`;
3828
+ if (activeCursorApiKeyTests.has(testId)) {
3829
+ json(res, 409, { error: "该 Cursor API Key 正在测试中" });
3830
+ return;
3831
+ }
3832
+ activeCursorApiKeyTests.add(testId);
3833
+ try {
3834
+ const result = await probeCursorApiKey(root, record);
3835
+ json(res, result.success ? 200 : 502, {
3836
+ result,
3837
+ scope,
3838
+ keys: cursorApiKeyAdminMetadata(userCtx, scope),
3839
+ });
3840
+ } catch (e) {
3841
+ json(res, 500, { error: (e && e.message) || String(e) });
3842
+ } finally {
3843
+ activeCursorApiKeyTests.delete(testId);
3844
+ }
3845
+ return;
3846
+ }
3847
+
3683
3848
  if (req.method === "POST" && url.pathname === "/api/user-env") {
3684
3849
  let payload;
3685
3850
  try {
@@ -3919,6 +4084,7 @@ export function startUiServer({
3919
4084
  }
3920
4085
  try {
3921
4086
  const result = deleteMarketplaceNodePackage(root, id, version, userCtx);
4087
+ if (result.ok) markRepositoryIndexDirty(root);
3922
4088
  json(res, result.ok ? 200 : 400, result);
3923
4089
  } catch (e) {
3924
4090
  json(res, 500, { ok: false, error: (e && e.message) || String(e) });
@@ -3959,6 +4125,7 @@ export function startUiServer({
3959
4125
  if (!resolved.error && resolved.flowDir) flowDir = resolved.flowDir;
3960
4126
  }
3961
4127
  const result = publishNodeFromInstance(root, payload || {}, { flowDir, ...userCtx });
4128
+ if (result.ok) markRepositoryIndexDirty(root);
3962
4129
  json(res, result.ok ? 200 : 400, result);
3963
4130
  } catch (e) {
3964
4131
  json(res, 500, { ok: false, error: (e && e.message) || String(e) });