ima2-gen 1.1.6 → 1.1.8
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/.env.example +5 -0
- package/README.md +3 -0
- package/assets/phase-a-bg-cleanup-test.png +0 -0
- package/config.js +111 -0
- package/docs/FAQ.ko.md +20 -0
- package/docs/FAQ.md +20 -0
- package/docs/README.ko.md +3 -0
- package/docs/README.zh-CN.md +3 -0
- package/integrations/comfyui/ima2_gen_bridge/README.md +88 -0
- package/integrations/comfyui/ima2_gen_bridge/__init__.py +3 -0
- package/integrations/comfyui/ima2_gen_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/integrations/comfyui/ima2_gen_bridge/__pycache__/nodes.cpython-313.pyc +0 -0
- package/integrations/comfyui/ima2_gen_bridge/nodes.py +238 -0
- package/lib/canvasVersionStore.js +181 -0
- package/lib/cardNewsPlannerClient.js +4 -2
- package/lib/comfyBridge.js +214 -0
- package/lib/db.js +14 -0
- package/lib/historyList.js +4 -0
- package/lib/imageMetadata.js +4 -0
- package/lib/imageModels.js +20 -0
- package/lib/localImportStore.js +111 -0
- package/lib/oauthProxy.js +88 -38
- package/lib/pngInfo.js +26 -0
- package/lib/promptImport/curatedSources.js +139 -0
- package/lib/promptImport/discoveryRegistry.js +236 -0
- package/lib/promptImport/errors.js +16 -0
- package/lib/promptImport/githubDiscovery.js +248 -0
- package/lib/promptImport/githubFolder.js +308 -0
- package/lib/promptImport/githubSource.js +239 -0
- package/lib/promptImport/gptImageHints.js +68 -0
- package/lib/promptImport/parsePromptCandidates.js +153 -0
- package/lib/promptImport/promptIndex.js +248 -0
- package/lib/promptImport/rankPromptCandidates.js +49 -0
- package/package.json +3 -2
- package/routes/annotations.js +95 -0
- package/routes/canvasVersions.js +64 -0
- package/routes/comfy.js +39 -0
- package/routes/edit.js +73 -4
- package/routes/generate.js +16 -2
- package/routes/imageImport.js +33 -0
- package/routes/index.js +10 -0
- package/routes/multimode.js +18 -1
- package/routes/nodes.js +25 -3
- package/routes/promptImport.js +354 -0
- package/ui/dist/assets/index-BDffwmLs.css +1 -0
- package/ui/dist/assets/index-D0fdHLkJ.js +31 -0
- package/ui/dist/assets/index-D0fdHLkJ.js.map +1 -0
- package/ui/dist/index.html +6 -3
- package/ui/dist/assets/index-3X-6VjbF.css +0 -1
- package/ui/dist/assets/index-DPSq9qEs.js +0 -31
- package/ui/dist/assets/index-DPSq9qEs.js.map +0 -1
package/routes/generate.js
CHANGED
|
@@ -4,7 +4,7 @@ import { randomBytes } from "crypto";
|
|
|
4
4
|
import { validateAndNormalizeRefs } from "../lib/refs.js";
|
|
5
5
|
import { classifyUpstreamError } from "../lib/errorClassify.js";
|
|
6
6
|
import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
|
|
7
|
-
import { normalizeImageModel } from "../lib/imageModels.js";
|
|
7
|
+
import { normalizeImageModel, normalizeReasoningEffort } from "../lib/imageModels.js";
|
|
8
8
|
import { generateViaOAuth } from "../lib/oauthProxy.js";
|
|
9
9
|
import { isNonRetryableGenerationError, normalizeGenerationFailure } from "../lib/generationErrors.js";
|
|
10
10
|
import { startJob, finishJob } from "../lib/inflight.js";
|
|
@@ -39,6 +39,8 @@ export function registerGenerateRoutes(app, ctx) {
|
|
|
39
39
|
references = [],
|
|
40
40
|
mode: promptMode = "auto",
|
|
41
41
|
model: rawModel,
|
|
42
|
+
reasoningEffort: rawReasoningEffort,
|
|
43
|
+
webSearchEnabled: rawWebSearchEnabled = true,
|
|
42
44
|
} = req.body;
|
|
43
45
|
const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
|
|
44
46
|
const modelCheck = normalizeImageModel(ctx, rawModel);
|
|
@@ -49,6 +51,15 @@ export function registerGenerateRoutes(app, ctx) {
|
|
|
49
51
|
return res.status(modelCheck.status).json({ error: modelCheck.error, code: modelCheck.code });
|
|
50
52
|
}
|
|
51
53
|
const imageModel = modelCheck.model;
|
|
54
|
+
const reasoningCheck = normalizeReasoningEffort(ctx, rawReasoningEffort);
|
|
55
|
+
if (reasoningCheck.error) {
|
|
56
|
+
finishStatus = "error";
|
|
57
|
+
finishHttpStatus = reasoningCheck.status;
|
|
58
|
+
finishErrorCode = reasoningCheck.code;
|
|
59
|
+
return res.status(reasoningCheck.status).json({ error: reasoningCheck.error, code: reasoningCheck.code });
|
|
60
|
+
}
|
|
61
|
+
const reasoningEffort = reasoningCheck.effort;
|
|
62
|
+
const webSearchEnabled = rawWebSearchEnabled !== false;
|
|
52
63
|
const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
|
|
53
64
|
|
|
54
65
|
if (!prompt) return res.status(400).json({ error: "Prompt is required" });
|
|
@@ -106,6 +117,7 @@ export function registerGenerateRoutes(app, ctx) {
|
|
|
106
117
|
clientNodeId,
|
|
107
118
|
promptChars: typeof prompt === "string" ? prompt.length : 0,
|
|
108
119
|
promptMode: normalizedPromptMode,
|
|
120
|
+
webSearchEnabled,
|
|
109
121
|
});
|
|
110
122
|
const startTime = Date.now();
|
|
111
123
|
|
|
@@ -127,7 +139,7 @@ export function registerGenerateRoutes(app, ctx) {
|
|
|
127
139
|
requestId,
|
|
128
140
|
normalizedPromptMode,
|
|
129
141
|
ctx,
|
|
130
|
-
{ model: imageModel },
|
|
142
|
+
{ model: imageModel, reasoningEffort, webSearchEnabled },
|
|
131
143
|
);
|
|
132
144
|
if (r.b64) return r;
|
|
133
145
|
lastErr = new Error("Empty response (safety refusal)");
|
|
@@ -170,6 +182,7 @@ export function registerGenerateRoutes(app, ctx) {
|
|
|
170
182
|
createdAt: Date.now(),
|
|
171
183
|
usage: r.value.usage || null,
|
|
172
184
|
webSearchCalls: r.value.webSearchCalls || 0,
|
|
185
|
+
webSearchEnabled,
|
|
173
186
|
refsCount: refCheck.refs.length,
|
|
174
187
|
};
|
|
175
188
|
const rawBuffer = Buffer.from(r.value.b64, "base64");
|
|
@@ -242,6 +255,7 @@ export function registerGenerateRoutes(app, ctx) {
|
|
|
242
255
|
warnings: qualityWarnings,
|
|
243
256
|
revisedPrompt: firstRevised,
|
|
244
257
|
promptMode: normalizedPromptMode,
|
|
258
|
+
webSearchEnabled,
|
|
245
259
|
};
|
|
246
260
|
|
|
247
261
|
if (count === 1) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { createLocalImport } from "../lib/localImportStore.js";
|
|
3
|
+
|
|
4
|
+
function decodeHeader(value) {
|
|
5
|
+
if (typeof value !== "string" || !value) return null;
|
|
6
|
+
try {
|
|
7
|
+
return decodeURIComponent(value);
|
|
8
|
+
} catch {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function registerImageImportRoutes(app, ctx) {
|
|
14
|
+
const rawImage = express.raw({
|
|
15
|
+
type: ["image/png", "image/jpeg", "image/webp"],
|
|
16
|
+
limit: ctx.config.server.bodyLimit,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
app.post("/api/history/import-local", rawImage, async (req, res) => {
|
|
20
|
+
try {
|
|
21
|
+
const item = await createLocalImport(ctx, {
|
|
22
|
+
buffer: Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0),
|
|
23
|
+
originalFilename: decodeHeader(req.headers["x-ima2-original-filename"]),
|
|
24
|
+
});
|
|
25
|
+
res.status(201).json({ item });
|
|
26
|
+
} catch (err) {
|
|
27
|
+
res.status(err.status || 500).json({
|
|
28
|
+
error: err.message,
|
|
29
|
+
code: err.code || "IMPORT_FAILED",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
package/routes/index.js
CHANGED
|
@@ -9,12 +9,21 @@ import { registerStorageRoutes } from "./storage.js";
|
|
|
9
9
|
import { registerCardNewsRoutes } from "./cardNews.js";
|
|
10
10
|
import { registerMetadataRoutes } from "./metadata.js";
|
|
11
11
|
import { registerPromptRoutes } from "./prompts.js";
|
|
12
|
+
import { registerPromptImportRoutes } from "./promptImport.js";
|
|
13
|
+
import { registerAnnotationRoutes } from "./annotations.js";
|
|
14
|
+
import { registerCanvasVersionRoutes } from "./canvasVersions.js";
|
|
15
|
+
import { registerComfyRoutes } from "./comfy.js";
|
|
16
|
+
import { registerImageImportRoutes } from "./imageImport.js";
|
|
12
17
|
|
|
13
18
|
export function configureRoutes(app, ctx) {
|
|
14
19
|
registerHealthRoutes(app, ctx);
|
|
15
20
|
registerStorageRoutes(app, ctx);
|
|
16
21
|
registerMetadataRoutes(app, ctx);
|
|
17
22
|
registerHistoryRoutes(app, ctx);
|
|
23
|
+
registerAnnotationRoutes(app, ctx);
|
|
24
|
+
registerCanvasVersionRoutes(app, ctx);
|
|
25
|
+
registerImageImportRoutes(app, ctx);
|
|
26
|
+
registerComfyRoutes(app, ctx);
|
|
18
27
|
registerSessionRoutes(app, ctx);
|
|
19
28
|
registerEditRoutes(app, ctx);
|
|
20
29
|
registerNodeRoutes(app, ctx);
|
|
@@ -22,4 +31,5 @@ export function configureRoutes(app, ctx) {
|
|
|
22
31
|
registerMultimodeRoutes(app, ctx);
|
|
23
32
|
registerGenerateRoutes(app, ctx);
|
|
24
33
|
registerPromptRoutes(app, ctx);
|
|
34
|
+
registerPromptImportRoutes(app, ctx);
|
|
25
35
|
}
|
package/routes/multimode.js
CHANGED
|
@@ -4,7 +4,7 @@ import { randomBytes } from "crypto";
|
|
|
4
4
|
import { validateAndNormalizeRefs } from "../lib/refs.js";
|
|
5
5
|
import { classifyUpstreamError } from "../lib/errorClassify.js";
|
|
6
6
|
import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
|
|
7
|
-
import { normalizeImageModel } from "../lib/imageModels.js";
|
|
7
|
+
import { normalizeImageModel, normalizeReasoningEffort } from "../lib/imageModels.js";
|
|
8
8
|
import { generateMultimodeViaOAuth } from "../lib/oauthProxy.js";
|
|
9
9
|
import { startJob, finishJob } from "../lib/inflight.js";
|
|
10
10
|
import { logEvent, logError } from "../lib/logger.js";
|
|
@@ -56,6 +56,8 @@ export function registerMultimodeRoutes(app, ctx) {
|
|
|
56
56
|
references = [],
|
|
57
57
|
mode: promptMode = "auto",
|
|
58
58
|
model: rawModel,
|
|
59
|
+
reasoningEffort: rawReasoningEffort,
|
|
60
|
+
webSearchEnabled: rawWebSearchEnabled = true,
|
|
59
61
|
} = req.body;
|
|
60
62
|
const maxImages = normalizeMaxImages(req.body?.maxImages);
|
|
61
63
|
const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
|
|
@@ -69,6 +71,16 @@ export function registerMultimodeRoutes(app, ctx) {
|
|
|
69
71
|
return;
|
|
70
72
|
}
|
|
71
73
|
const imageModel = modelCheck.model;
|
|
74
|
+
const reasoningCheck = normalizeReasoningEffort(ctx, rawReasoningEffort);
|
|
75
|
+
if (reasoningCheck.error) {
|
|
76
|
+
finishStatus = "error";
|
|
77
|
+
finishHttpStatus = reasoningCheck.status;
|
|
78
|
+
finishErrorCode = reasoningCheck.code;
|
|
79
|
+
sendSse(res, "error", { error: reasoningCheck.error, code: reasoningCheck.code, status: reasoningCheck.status, requestId });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const reasoningEffort = reasoningCheck.effort;
|
|
83
|
+
const webSearchEnabled = rawWebSearchEnabled !== false;
|
|
72
84
|
if (!prompt) {
|
|
73
85
|
finishStatus = "error";
|
|
74
86
|
finishHttpStatus = 400;
|
|
@@ -122,6 +134,7 @@ export function registerMultimodeRoutes(app, ctx) {
|
|
|
122
134
|
maxImages,
|
|
123
135
|
refs: refCheck.refs.length,
|
|
124
136
|
promptChars: typeof prompt === "string" ? prompt.length : 0,
|
|
137
|
+
webSearchEnabled,
|
|
125
138
|
});
|
|
126
139
|
|
|
127
140
|
const startTime = Date.now();
|
|
@@ -143,6 +156,8 @@ export function registerMultimodeRoutes(app, ctx) {
|
|
|
143
156
|
{
|
|
144
157
|
model: imageModel,
|
|
145
158
|
maxImages,
|
|
159
|
+
reasoningEffort,
|
|
160
|
+
webSearchEnabled,
|
|
146
161
|
onPartialImage: (partial) =>
|
|
147
162
|
sendSse(res, "partial", {
|
|
148
163
|
image: `data:${mime};base64,${partial.b64}`,
|
|
@@ -184,6 +199,7 @@ export function registerMultimodeRoutes(app, ctx) {
|
|
|
184
199
|
createdAt: Date.now(),
|
|
185
200
|
usage: generated.usage || null,
|
|
186
201
|
webSearchCalls: generated.webSearchCalls || 0,
|
|
202
|
+
webSearchEnabled,
|
|
187
203
|
refsCount: refCheck.refs.length,
|
|
188
204
|
};
|
|
189
205
|
const rawBuffer = Buffer.from(image.b64, "base64");
|
|
@@ -224,6 +240,7 @@ export function registerMultimodeRoutes(app, ctx) {
|
|
|
224
240
|
model: imageModel,
|
|
225
241
|
usage: generated.usage || null,
|
|
226
242
|
webSearchCalls: generated.webSearchCalls || 0,
|
|
243
|
+
webSearchEnabled,
|
|
227
244
|
warnings: qualityWarnings,
|
|
228
245
|
extraIgnored: generated.extraIgnored || 0,
|
|
229
246
|
promptMode: normalizedPromptMode,
|
package/routes/nodes.js
CHANGED
|
@@ -10,7 +10,7 @@ import { startJob, finishJob } from "../lib/inflight.js";
|
|
|
10
10
|
import { validateAndNormalizeRefs } from "../lib/refs.js";
|
|
11
11
|
import { classifyUpstreamError } from "../lib/errorClassify.js";
|
|
12
12
|
import { normalizeOAuthParams } from "../lib/oauthNormalize.js";
|
|
13
|
-
import { normalizeImageModel } from "../lib/imageModels.js";
|
|
13
|
+
import { normalizeImageModel, normalizeReasoningEffort } from "../lib/imageModels.js";
|
|
14
14
|
import { generateViaOAuth, editViaOAuth } from "../lib/oauthProxy.js";
|
|
15
15
|
import { isNonRetryableGenerationError, normalizeGenerationFailure } from "../lib/generationErrors.js";
|
|
16
16
|
import { logEvent, logError } from "../lib/logger.js";
|
|
@@ -87,6 +87,7 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
87
87
|
contextMode: rawContextMode = "parent-plus-refs",
|
|
88
88
|
searchMode: rawSearchMode = "on",
|
|
89
89
|
model: rawModel,
|
|
90
|
+
reasoningEffort: rawReasoningEffort,
|
|
90
91
|
} = body;
|
|
91
92
|
const { provider = "oauth" } = body;
|
|
92
93
|
const { quality, warnings: qualityWarnings } = normalizeOAuthParams({ provider, quality: rawQuality });
|
|
@@ -101,11 +102,23 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
101
102
|
});
|
|
102
103
|
}
|
|
103
104
|
const imageModel = modelCheck.model;
|
|
105
|
+
const reasoningCheck = normalizeReasoningEffort(ctx, rawReasoningEffort);
|
|
106
|
+
if (reasoningCheck.error) {
|
|
107
|
+
finishStatus = "error";
|
|
108
|
+
finishHttpStatus = reasoningCheck.status;
|
|
109
|
+
finishErrorCode = reasoningCheck.code;
|
|
110
|
+
return res.status(reasoningCheck.status).json({
|
|
111
|
+
error: { code: reasoningCheck.code, message: reasoningCheck.error },
|
|
112
|
+
parentNodeId,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const reasoningEffort = reasoningCheck.effort;
|
|
104
116
|
const normalizedPromptMode = promptMode === "direct" ? "direct" : "auto";
|
|
105
117
|
const contextMode = ["parent-plus-refs", "parent-only", "ancestry"].includes(rawContextMode)
|
|
106
118
|
? rawContextMode
|
|
107
119
|
: "parent-plus-refs";
|
|
108
120
|
const searchMode = ["off", "auto", "on"].includes(rawSearchMode) ? rawSearchMode : "on";
|
|
121
|
+
const webSearchEnabled = body.webSearchEnabled !== false && searchMode !== "off";
|
|
109
122
|
if (contextMode === "ancestry") {
|
|
110
123
|
finishStatus = "error";
|
|
111
124
|
finishHttpStatus = 400;
|
|
@@ -168,7 +181,6 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
168
181
|
const generateReferenceDiagnostics = operation === "generate" ? referenceDiagnostics : [];
|
|
169
182
|
const referenceMismatchCount = generateReferenceDiagnostics.filter((ref) => ref.warnings?.includes("mime_mismatch")).length;
|
|
170
183
|
const refsForRequest = contextMode === "parent-only" ? [] : (refCheck.refDetails || refCheck.refs);
|
|
171
|
-
const webSearchEnabled = true;
|
|
172
184
|
const parentImagePresent = !!parentB64;
|
|
173
185
|
const inputImageCount = (parentImagePresent ? 1 : 0) + refsForRequest.length;
|
|
174
186
|
logEvent("node", "request", {
|
|
@@ -227,7 +239,13 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
227
239
|
webSearchEnabled,
|
|
228
240
|
});
|
|
229
241
|
const r = parentB64
|
|
230
|
-
? await editViaOAuth(prompt, parentB64, quality, size, moderation, normalizedPromptMode, ctx, requestId, {
|
|
242
|
+
? await editViaOAuth(prompt, parentB64, quality, size, moderation, normalizedPromptMode, ctx, requestId, {
|
|
243
|
+
model: imageModel,
|
|
244
|
+
references: refsForRequest,
|
|
245
|
+
searchMode,
|
|
246
|
+
reasoningEffort,
|
|
247
|
+
webSearchEnabled,
|
|
248
|
+
})
|
|
231
249
|
: await generateViaOAuth(
|
|
232
250
|
prompt,
|
|
233
251
|
quality,
|
|
@@ -239,6 +257,8 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
239
257
|
ctx,
|
|
240
258
|
{
|
|
241
259
|
model: imageModel,
|
|
260
|
+
reasoningEffort,
|
|
261
|
+
webSearchEnabled,
|
|
242
262
|
partialImages: streamResponse ? 2 : 0,
|
|
243
263
|
onPartialImage: streamResponse
|
|
244
264
|
? (partial) =>
|
|
@@ -336,6 +356,7 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
336
356
|
elapsed,
|
|
337
357
|
usage: usage || null,
|
|
338
358
|
webSearchCalls,
|
|
359
|
+
webSearchEnabled,
|
|
339
360
|
contextMode,
|
|
340
361
|
searchMode,
|
|
341
362
|
provider: "oauth",
|
|
@@ -375,6 +396,7 @@ export function registerNodeRoutes(app, ctx) {
|
|
|
375
396
|
elapsed,
|
|
376
397
|
usage,
|
|
377
398
|
webSearchCalls,
|
|
399
|
+
webSearchEnabled,
|
|
378
400
|
provider: "oauth",
|
|
379
401
|
model: imageModel,
|
|
380
402
|
size,
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { getDb } from "../lib/db.js";
|
|
2
|
+
import { logError, logEvent } from "../lib/logger.js";
|
|
3
|
+
import { isPromptImportError, promptImportError } from "../lib/promptImport/errors.js";
|
|
4
|
+
import {
|
|
5
|
+
fetchGitHubSourceText,
|
|
6
|
+
isSupportedPromptFileName,
|
|
7
|
+
normalizeGitHubSource,
|
|
8
|
+
} from "../lib/promptImport/githubSource.js";
|
|
9
|
+
import {
|
|
10
|
+
fetchGitHubFolderFiles,
|
|
11
|
+
fetchSelectedGitHubFolderFiles,
|
|
12
|
+
normalizeGitHubFolderSource,
|
|
13
|
+
} from "../lib/promptImport/githubFolder.js";
|
|
14
|
+
import { parsePromptCandidates } from "../lib/promptImport/parsePromptCandidates.js";
|
|
15
|
+
import {
|
|
16
|
+
getPromptImportSources,
|
|
17
|
+
refreshCuratedSource,
|
|
18
|
+
searchCuratedPrompts,
|
|
19
|
+
} from "../lib/promptImport/promptIndex.js";
|
|
20
|
+
import { searchGitHubDiscovery } from "../lib/promptImport/githubDiscovery.js";
|
|
21
|
+
import {
|
|
22
|
+
listDiscoveryCandidates,
|
|
23
|
+
reviewDiscoveryCandidate,
|
|
24
|
+
} from "../lib/promptImport/discoveryRegistry.js";
|
|
25
|
+
|
|
26
|
+
function promptImportLimits(ctx) {
|
|
27
|
+
return {
|
|
28
|
+
maxFileBytesForPreview: ctx.config.limits.promptImportMaxFileBytes,
|
|
29
|
+
maxPromptCandidatesPerFile: ctx.config.limits.promptImportMaxCandidatesPerFile,
|
|
30
|
+
maxPromptCandidatesPerImport: ctx.config.limits.promptImportMaxCandidatesPerImport,
|
|
31
|
+
fetchTimeoutMs: ctx.config.limits.promptImportFetchTimeoutMs,
|
|
32
|
+
maxCandidateChars: ctx.config.limits.promptImportMaxCandidateChars,
|
|
33
|
+
minCandidateChars: ctx.config.limits.promptImportMinCandidateChars,
|
|
34
|
+
maxSourceCharsScanned: ctx.config.limits.promptImportMaxSourceCharsScanned,
|
|
35
|
+
maxRepoIndexFiles: ctx.config.limits.promptImportMaxRepoIndexFiles,
|
|
36
|
+
curatedSearchLimit: ctx.config.limits.promptImportCuratedSearchLimit,
|
|
37
|
+
indexCacheTtlMs: ctx.config.limits.promptImportIndexCacheTtlMs,
|
|
38
|
+
maxFolderFiles: ctx.config.limits.promptImportMaxFolderFiles,
|
|
39
|
+
maxFolderPreviewFiles: ctx.config.limits.promptImportMaxFolderPreviewFiles,
|
|
40
|
+
discoverySearchLimit: ctx.config.limits.promptImportDiscoverySearchLimit,
|
|
41
|
+
discoveryMaxQueries: ctx.config.limits.promptImportDiscoveryMaxQueries,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sendPromptImportError(res, error) {
|
|
46
|
+
const status = isPromptImportError(error) ? error.status : 500;
|
|
47
|
+
const code = isPromptImportError(error) ? error.code : "PROMPT_IMPORT_FAILED";
|
|
48
|
+
const message = error?.message || "Prompt import failed";
|
|
49
|
+
res.status(status).json({ error: { code, message } });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function generateId() {
|
|
53
|
+
return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sourceFilename(source) {
|
|
57
|
+
if (source.kind === "local") return source.filename;
|
|
58
|
+
return source.path.split("/").pop();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeLocalSource(source) {
|
|
62
|
+
const filename = typeof source?.filename === "string" ? source.filename.trim() : "";
|
|
63
|
+
const text = typeof source?.text === "string" ? source.text : "";
|
|
64
|
+
if (!filename || !isSupportedPromptFileName(filename)) {
|
|
65
|
+
throw promptImportError("UNSUPPORTED_EXTENSION", "Only .md, .markdown, and .txt files are supported");
|
|
66
|
+
}
|
|
67
|
+
if (!text.trim()) {
|
|
68
|
+
throw promptImportError("PROMPT_IMPORT_EMPTY", "Prompt source is empty", 422);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
kind: "local",
|
|
72
|
+
filename,
|
|
73
|
+
extension: filename.split(".").pop().toLowerCase(),
|
|
74
|
+
text,
|
|
75
|
+
tags: [`file:${filename}`, `ext:${filename.split(".").pop().toLowerCase()}`],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function buildPreview(req, ctx) {
|
|
80
|
+
const body = req.body || {};
|
|
81
|
+
const rawSource = body.source || body;
|
|
82
|
+
const kind = rawSource.kind === "github" ? "github" : "local";
|
|
83
|
+
const limits = promptImportLimits(ctx);
|
|
84
|
+
let source;
|
|
85
|
+
let text;
|
|
86
|
+
|
|
87
|
+
if (kind === "github") {
|
|
88
|
+
source = normalizeGitHubSource(rawSource.input);
|
|
89
|
+
text = await fetchGitHubSourceText(source, limits);
|
|
90
|
+
} else {
|
|
91
|
+
source = normalizeLocalSource(rawSource);
|
|
92
|
+
text = source.text;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (text.length > limits.maxSourceCharsScanned) {
|
|
96
|
+
text = text.slice(0, limits.maxSourceCharsScanned);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const candidates = parsePromptCandidates({
|
|
100
|
+
text,
|
|
101
|
+
filename: sourceFilename(source),
|
|
102
|
+
source: {
|
|
103
|
+
kind: source.kind,
|
|
104
|
+
owner: source.owner,
|
|
105
|
+
repo: source.repo,
|
|
106
|
+
ref: source.ref,
|
|
107
|
+
path: source.path,
|
|
108
|
+
htmlUrl: source.htmlUrl,
|
|
109
|
+
filename: source.filename,
|
|
110
|
+
},
|
|
111
|
+
tags: source.tags,
|
|
112
|
+
limits,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (candidates.length === 0) {
|
|
116
|
+
throw promptImportError("PROMPT_IMPORT_EMPTY", "No prompt candidates were found", 422);
|
|
117
|
+
}
|
|
118
|
+
return { source, candidates, warnings: [] };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizeFolderInput(body) {
|
|
122
|
+
const input = typeof body?.source?.input === "string" ? body.source.input : body?.input;
|
|
123
|
+
return normalizeGitHubFolderSource(input);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function buildFolderFiles(req, ctx) {
|
|
127
|
+
const limits = promptImportLimits(ctx);
|
|
128
|
+
const source = normalizeFolderInput(req.body || {});
|
|
129
|
+
return fetchGitHubFolderFiles(source, limits);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function buildFolderPreview(req, ctx) {
|
|
133
|
+
const limits = promptImportLimits(ctx);
|
|
134
|
+
const source = normalizeFolderInput(req.body || {});
|
|
135
|
+
const paths = Array.isArray(req.body?.paths) ? req.body.paths : [];
|
|
136
|
+
const selected = await fetchSelectedGitHubFolderFiles(source, paths, limits);
|
|
137
|
+
const candidates = [];
|
|
138
|
+
const warnings = [...selected.warnings];
|
|
139
|
+
|
|
140
|
+
for (const file of selected.files) {
|
|
141
|
+
const text = file.text.length > limits.maxSourceCharsScanned
|
|
142
|
+
? file.text.slice(0, limits.maxSourceCharsScanned)
|
|
143
|
+
: file.text;
|
|
144
|
+
const parsed = parsePromptCandidates({
|
|
145
|
+
text,
|
|
146
|
+
filename: file.path,
|
|
147
|
+
source: {
|
|
148
|
+
kind: "github",
|
|
149
|
+
owner: source.owner,
|
|
150
|
+
repo: source.repo,
|
|
151
|
+
ref: source.ref,
|
|
152
|
+
path: file.path,
|
|
153
|
+
htmlUrl: file.htmlUrl,
|
|
154
|
+
},
|
|
155
|
+
tags: [...source.tags, `file:${file.name}`, `ext:${file.extension}`],
|
|
156
|
+
limits,
|
|
157
|
+
});
|
|
158
|
+
if (parsed.length === 0) warnings.push(`${file.path}: no prompt candidates`);
|
|
159
|
+
candidates.push(...parsed);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (candidates.length === 0) {
|
|
163
|
+
throw promptImportError("PROMPT_IMPORT_EMPTY", "No prompt candidates were found", 422);
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
source,
|
|
167
|
+
files: selected.files.map(({ text, contentHash, ...file }) => file),
|
|
168
|
+
candidates: candidates.slice(0, limits.maxPromptCandidatesPerImport),
|
|
169
|
+
warnings,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function assertCommitCandidateText(text, limits) {
|
|
174
|
+
if (text.length < limits.minCandidateChars) {
|
|
175
|
+
throw promptImportError("PROMPT_IMPORT_EMPTY", "Prompt candidate is too short", 422);
|
|
176
|
+
}
|
|
177
|
+
if (text.length > limits.maxCandidateChars) {
|
|
178
|
+
throw promptImportError("PROMPT_IMPORT_TOO_MANY_CANDIDATES", "Prompt candidate is too large", 413);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function commitCandidates(candidates, folderId, limits) {
|
|
183
|
+
const db = getDb();
|
|
184
|
+
const result = { foldersCreated: 0, promptsImported: 0, duplicatesSkipped: 0 };
|
|
185
|
+
const now = Math.floor(Date.now() / 1000);
|
|
186
|
+
const targetFolder = typeof folderId === "string" && folderId ? folderId : "__root__";
|
|
187
|
+
const folderExists = db.prepare("SELECT 1 FROM prompt_folders WHERE id = ? LIMIT 1").get(targetFolder);
|
|
188
|
+
const resolvedFolderId = folderExists ? targetFolder : "__root__";
|
|
189
|
+
|
|
190
|
+
for (const candidate of candidates) {
|
|
191
|
+
if (!candidate?.text || typeof candidate.text !== "string") continue;
|
|
192
|
+
const text = candidate.text.trim();
|
|
193
|
+
if (!text) continue;
|
|
194
|
+
assertCommitCandidateText(text, limits);
|
|
195
|
+
const dup = db.prepare("SELECT 1 FROM prompts WHERE text = ? AND folder_id = ? LIMIT 1").get(text, resolvedFolderId);
|
|
196
|
+
if (dup) {
|
|
197
|
+
result.duplicatesSkipped++;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const tagsJson = Array.isArray(candidate.tags) ? JSON.stringify([...new Set(candidate.tags)]) : null;
|
|
201
|
+
db.prepare(
|
|
202
|
+
`INSERT INTO prompts (id, folder_id, name, text, tags, mode, is_favorite, created_at, updated_at)
|
|
203
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
204
|
+
).run(
|
|
205
|
+
generateId(),
|
|
206
|
+
resolvedFolderId,
|
|
207
|
+
typeof candidate.name === "string" && candidate.name.trim() ? candidate.name.trim() : text.slice(0, 30),
|
|
208
|
+
text,
|
|
209
|
+
tagsJson,
|
|
210
|
+
candidate.mode === "direct" || candidate.mode === "auto" ? candidate.mode : null,
|
|
211
|
+
0,
|
|
212
|
+
now,
|
|
213
|
+
now,
|
|
214
|
+
);
|
|
215
|
+
result.promptsImported++;
|
|
216
|
+
}
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function registerPromptImportRoutes(app, ctx) {
|
|
221
|
+
app.get("/api/prompts/import/curated-sources", async (req, res) => {
|
|
222
|
+
try {
|
|
223
|
+
res.json(await getPromptImportSources(ctx));
|
|
224
|
+
} catch (error) {
|
|
225
|
+
logError("promptImport", "curated_sources_error", error);
|
|
226
|
+
sendPromptImportError(res, error);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
app.get("/api/prompts/import/discovery", async (req, res) => {
|
|
231
|
+
try {
|
|
232
|
+
const candidates = await listDiscoveryCandidates(ctx, {
|
|
233
|
+
status: typeof req.query?.status === "string" ? req.query.status : undefined,
|
|
234
|
+
});
|
|
235
|
+
res.json({ candidates, warnings: [] });
|
|
236
|
+
} catch (error) {
|
|
237
|
+
logError("promptImport", "discovery_list_error", error);
|
|
238
|
+
sendPromptImportError(res, error);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
app.post("/api/prompts/import/discovery-search", async (req, res) => {
|
|
243
|
+
try {
|
|
244
|
+
const limits = promptImportLimits(ctx);
|
|
245
|
+
const seeds = Array.isArray(req.body?.seeds) ? req.body.seeds : [];
|
|
246
|
+
if (seeds.length > limits.discoveryMaxQueries) {
|
|
247
|
+
throw promptImportError("GITHUB_DISCOVERY_TOO_MANY_QUERIES", "Too many discovery queries", 413);
|
|
248
|
+
}
|
|
249
|
+
const result = await searchGitHubDiscovery(ctx, {
|
|
250
|
+
q: req.body?.q,
|
|
251
|
+
seeds,
|
|
252
|
+
limit: req.body?.limit,
|
|
253
|
+
maxQueries: limits.discoveryMaxQueries,
|
|
254
|
+
});
|
|
255
|
+
res.json(result);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
logError("promptImport", "discovery_search_error", error);
|
|
258
|
+
sendPromptImportError(res, error);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
app.post("/api/prompts/import/discovery-review", async (req, res) => {
|
|
263
|
+
try {
|
|
264
|
+
const result = await reviewDiscoveryCandidate(ctx, {
|
|
265
|
+
repo: req.body?.repo,
|
|
266
|
+
status: req.body?.status,
|
|
267
|
+
reviewNotes: req.body?.reviewNotes,
|
|
268
|
+
allowedPaths: req.body?.allowedPaths,
|
|
269
|
+
defaultSearch: req.body?.defaultSearch,
|
|
270
|
+
});
|
|
271
|
+
res.json(result);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
logError("promptImport", "discovery_review_error", error);
|
|
274
|
+
sendPromptImportError(res, error);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
app.post("/api/prompts/import/curated-search", async (req, res) => {
|
|
279
|
+
try {
|
|
280
|
+
const result = await searchCuratedPrompts(ctx, {
|
|
281
|
+
q: req.body?.q,
|
|
282
|
+
sourceIds: req.body?.sourceIds,
|
|
283
|
+
limit: req.body?.limit,
|
|
284
|
+
});
|
|
285
|
+
res.json(result);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
logError("promptImport", "curated_search_error", error);
|
|
288
|
+
sendPromptImportError(res, error);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
app.post("/api/prompts/import/curated-refresh", async (req, res) => {
|
|
293
|
+
try {
|
|
294
|
+
const sourceId = typeof req.body?.sourceId === "string" ? req.body.sourceId : "";
|
|
295
|
+
if (!sourceId) {
|
|
296
|
+
throw promptImportError("INVALID_GITHUB_SOURCE", "Curated source is required", 400);
|
|
297
|
+
}
|
|
298
|
+
const result = await refreshCuratedSource(ctx, sourceId);
|
|
299
|
+
res.json(result);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
logError("promptImport", "curated_refresh_error", error);
|
|
302
|
+
sendPromptImportError(res, error);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
app.post("/api/prompts/import/folder-files", async (req, res) => {
|
|
307
|
+
try {
|
|
308
|
+
const result = await buildFolderFiles(req, ctx);
|
|
309
|
+
res.json(result);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
logError("promptImport", "folder_files_error", error);
|
|
312
|
+
sendPromptImportError(res, error);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
app.post("/api/prompts/import/folder-preview", async (req, res) => {
|
|
317
|
+
try {
|
|
318
|
+
const result = await buildFolderPreview(req, ctx);
|
|
319
|
+
res.json(result);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
logError("promptImport", "folder_preview_error", error);
|
|
322
|
+
sendPromptImportError(res, error);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
app.post("/api/prompts/import/preview", async (req, res) => {
|
|
327
|
+
try {
|
|
328
|
+
const preview = await buildPreview(req, ctx);
|
|
329
|
+
res.json(preview);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
logError("promptImport", "preview_error", error);
|
|
332
|
+
sendPromptImportError(res, error);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
app.post("/api/prompts/import/commit", async (req, res) => {
|
|
337
|
+
try {
|
|
338
|
+
const limits = promptImportLimits(ctx);
|
|
339
|
+
const candidates = Array.isArray(req.body?.candidates) ? req.body.candidates : [];
|
|
340
|
+
if (candidates.length === 0) {
|
|
341
|
+
throw promptImportError("PROMPT_IMPORT_EMPTY", "Select at least one prompt to import", 422);
|
|
342
|
+
}
|
|
343
|
+
if (candidates.length > limits.maxPromptCandidatesPerImport) {
|
|
344
|
+
throw promptImportError("PROMPT_IMPORT_TOO_MANY_CANDIDATES", "Too many prompt candidates", 413);
|
|
345
|
+
}
|
|
346
|
+
const result = commitCandidates(candidates, req.body?.folderId, limits);
|
|
347
|
+
logEvent("promptImport", "committed", result);
|
|
348
|
+
res.json(result);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
logError("promptImport", "commit_error", error);
|
|
351
|
+
sendPromptImportError(res, error);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|