tide-commander 1.216.0 → 1.217.0
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/CHANGELOG.md +18 -0
- package/dist/assets/{AudioPlayer-Chu8I0tg.js → AudioPlayer-wYzUPbI7.js} +1 -1
- package/dist/assets/{BossLogsModal-DR0ejza7.js → BossLogsModal-CmZNDIpu.js} +1 -1
- package/dist/assets/{BossSpawnModal-LqXBwD2F.js → BossSpawnModal-DtVfSG5G.js} +1 -1
- package/dist/assets/{ControlsModal-BItvxY7x.js → ControlsModal-C0tQf4xy.js} +1 -1
- package/dist/assets/{DockerLogsModal-DfR1XadQ.js → DockerLogsModal-DL_eRO-m.js} +1 -1
- package/dist/assets/{EmbeddedEditor-dMOVrrnl.js → EmbeddedEditor-DAr7_QUH.js} +1 -1
- package/dist/assets/{FcStdViewer-CXDb_hmK.js → FcStdViewer-MDGJiz2T.js} +1 -1
- package/dist/assets/{GcodeViewer-BJGaDLF8.js → GcodeViewer-BHVydtrp.js} +1 -1
- package/dist/assets/{GlbViewer-CrLi0LWd.js → GlbViewer-Bwr_Tayz.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-CzinZ6aZ.js → GmailOAuthSetup-D_uF2cyB.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-DhVaIj11.js → GoogleOAuthSetup-BCmzQl5e.js} +1 -1
- package/dist/assets/{IframeModal-NJJxvrBg.js → IframeModal-CQmCu_P4.js} +1 -1
- package/dist/assets/{IntegrationsPanel-DDht5-zS.js → IntegrationsPanel-BmRLC7dz.js} +2 -2
- package/dist/assets/{LogViewerModal-DobAgsRk.js → LogViewerModal-CrnaI78u.js} +1 -1
- package/dist/assets/{MonitoringModal-ugppuuGW.js → MonitoringModal-C6J4g3Jf.js} +1 -1
- package/dist/assets/{PM2LogsModal-C28LrhXo.js → PM2LogsModal-Ds3dRnuE.js} +1 -1
- package/dist/assets/{PluginsPanel-BrllpLPF.js → PluginsPanel-BWG2NKdJ.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-DUjvqOIA.js → RestoreArchivedAreaModal-BNccBE4Z.js} +1 -1
- package/dist/assets/{Scene2DCanvas-IHlXkbK5.js → Scene2DCanvas-CPvMvm9m.js} +1 -1
- package/dist/assets/{SceneManager-BzRqBmgj.js → SceneManager-lb8uiOCO.js} +1 -1
- package/dist/assets/{SkillsPanel-B7ePWL4w.js → SkillsPanel-BTO2Ow7t.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-bpmQ5yCA.js → SlackMultiInstanceSetup-Da_GWe5W.js} +1 -1
- package/dist/assets/{SpawnModal-Bu7udoPb.js → SpawnModal-JbFRYxZa.js} +1 -1
- package/dist/assets/{StatisticsModal-teyupFGW.js → StatisticsModal-ChSnL5yu.js} +1 -1
- package/dist/assets/{StlViewer-DTuw4IoN.js → StlViewer-BFr-I14v.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-De7mzS6q.js → SubordinateAssignmentModal-DchdwFLE.js} +1 -1
- package/dist/assets/{ThreeAreaSelector-B4FdXGTQ.js → ThreeAreaSelector-D18G4udc.js} +1 -1
- package/dist/assets/{ThreeViewShortcuts-C50gwWcg.js → ThreeViewShortcuts-vNiMNFPJ.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-NhdLnKIE.js → TriggerManagerPanel-DjpLl7cf.js} +1 -1
- package/dist/assets/{VideoPlayer-DR13OKH9.js → VideoPlayer-Drj9r9Dx.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-HR2fDtcS.js → WorkflowEditorPanel-DLwkYQ8M.js} +1 -1
- package/dist/assets/{abnfDiagram-VRR7QNED-Q73b9mfz.js → abnfDiagram-VRR7QNED-BVuUyMTx.js} +1 -1
- package/dist/assets/{arc-CVKFmkgP.js → arc-_phD079o.js} +1 -1
- package/dist/assets/{architectureDiagram-ZJ3FMSHR-Bbf_Qcjn.js → architectureDiagram-ZJ3FMSHR-B7fAK4WF.js} +1 -1
- package/dist/assets/{area-logos-gYv8s8T3.js → area-logos-RCA-q_Vf.js} +1 -1
- package/dist/assets/{blockDiagram-677ZJIJ3-DaheCa8j.js → blockDiagram-677ZJIJ3-C-OlY0-3.js} +1 -1
- package/dist/assets/{c4Diagram-LMCZKHZV-UZ1L_YBs.js → c4Diagram-LMCZKHZV-DrwXTZgK.js} +1 -1
- package/dist/assets/channel-BWg1ErwG.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-D8YOZZA-.js → chunk-2Q5K7J3B-DCcJNg3d.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-BQmKdeF2.js → chunk-32BRIVSS-CZZkOSIs.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-CT5m9OBR.js → chunk-5VM5RSS4-BCFrP-q5.js} +1 -1
- package/dist/assets/{chunk-EX3LRPZG-BwQ_dYOl.js → chunk-EX3LRPZG-Bjnq5732.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DVPlE4Zc.js → chunk-JWPE2WC7-CPjRN_WZ.js} +1 -1
- package/dist/assets/{chunk-MOJQB5TN-DGrSLKap.js → chunk-MOJQB5TN-B6jVWpSe.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-BGpC2Lvw.js → chunk-RYQCIY6F-0QrHhHTV.js} +1 -1
- package/dist/assets/{chunk-V7JOEXUC-T5kHrm0m.js → chunk-V7JOEXUC-DwLSEIop.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-DsjgCLci.js → chunk-VR4S4FIN-R-sa6MjG.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CeisNHDz.js → chunk-XXDRQBXY-FuYmcHPd.js} +1 -1
- package/dist/assets/classDiagram-OUVF2IWQ-Cdy8Clus.js +1 -0
- package/dist/assets/classDiagram-v2-EOCWNBFH-Cdy8Clus.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-DGc7pmnL.js → cose-bilkent-JH36ORCC-CvD5NdQ_.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-B4WOzgxr.js → cynefin-VYW2F7L2-C8_VnooS.js} +1 -1
- package/dist/assets/{cynefinDiagram-TSTJHNR4-C_DBldqW.js → cynefinDiagram-TSTJHNR4-SD_F8ul6.js} +1 -1
- package/dist/assets/{dagre-VKFMJZFB-CLbdjSVi.js → dagre-VKFMJZFB-Cpeo3cpw.js} +1 -1
- package/dist/assets/{diagram-FQU43EPY-rFdDLhQC.js → diagram-FQU43EPY-DsBnlRvM.js} +1 -1
- package/dist/assets/{diagram-G47NLZAW-B7IY4xJj.js → diagram-G47NLZAW-BHMRe5db.js} +1 -1
- package/dist/assets/{diagram-NH7WQ7WH-Dffo88fG.js → diagram-NH7WQ7WH-D8Rm6qlg.js} +1 -1
- package/dist/assets/{diagram-OA4YK3LP-CI24r0oP.js → diagram-OA4YK3LP-C4Svb0Zw.js} +1 -1
- package/dist/assets/{diagram-WEI45ONY-CpYaSMOz.js → diagram-WEI45ONY-BbFXrI2b.js} +1 -1
- package/dist/assets/{ebnfDiagram-CCIWWBDH-CVE6IpE1.js → ebnfDiagram-CCIWWBDH-CCBonTYH.js} +1 -1
- package/dist/assets/{erDiagram-Q63AITRT-BfedFNid.js → erDiagram-Q63AITRT-CGGw9uIx.js} +1 -1
- package/dist/assets/{flowDiagram-23GEKE2U-BMSNxq_P.js → flowDiagram-23GEKE2U-DWxHXzyo.js} +1 -1
- package/dist/assets/{ganttDiagram-NO4QXBWP-CFmNsZuP.js → ganttDiagram-NO4QXBWP-D74TSIvX.js} +1 -1
- package/dist/assets/{gitGraphDiagram-IHSO6WYX-BPZCZyak.js → gitGraphDiagram-IHSO6WYX-Cwv5rBxw.js} +1 -1
- package/dist/assets/{index-CE74eZqz.js → index-6lLWOBQK.js} +1 -1
- package/dist/assets/{index-DAKI-8ry.js → index-B9rURqfo.js} +1 -1
- package/dist/assets/{index-DYtuoGp_.js → index-BMXP1Yd8.js} +3 -3
- package/dist/assets/{index-DbOvHwxP.js → index-BZztJf0b.js} +1 -1
- package/dist/assets/{index-BZPSXTAd.js → index-D-5PhYjN.js} +1 -1
- package/dist/assets/{index-CHl4bmrT.js → index-DOcVAWbZ.js} +1 -1
- package/dist/assets/{index-DjYjfA28.js → index-DW1gJ5Cs.js} +1 -1
- package/dist/assets/{index-DKoidOEn.js → index-DrIXfMcD.js} +1 -1
- package/dist/assets/{index-BRdYSHCt.js → index-s33Ke8oj.js} +2 -2
- package/dist/assets/{infoDiagram-FWYZ7A6U-i4zJwolP.js → infoDiagram-FWYZ7A6U-VSfMHDx9.js} +1 -1
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-Mx_Omxks.js → ishikawaDiagram-FXEZZL3T-CgWr16Qr.js} +1 -1
- package/dist/assets/{journeyDiagram-5HDEW3XC-3Rokfnmf.js → journeyDiagram-5HDEW3XC-Bv4ioJyG.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-CEPx7BrA.js → kanban-definition-HUTT4EX6-DsUoZOQz.js} +1 -1
- package/dist/assets/{linear-DNXvsxYb.js → linear-_3oDVafz.js} +1 -1
- package/dist/assets/{main-BBvM-_Ia.js → main-BKRg0DBA.js} +160 -160
- package/dist/assets/{mermaid.core-0qDPsI0T.js → mermaid.core-spKo10MG.js} +4 -4
- package/dist/assets/{mindmap-definition-LN4V7U3C-DwCHykip.js → mindmap-definition-LN4V7U3C-C6uCJ4IN.js} +1 -1
- package/dist/assets/{pegDiagram-2B236MQR-DO7OR5bN.js → pegDiagram-2B236MQR-BTIBB-Uw.js} +1 -1
- package/dist/assets/{pieDiagram-ENE6RG2P--1xKfxMr.js → pieDiagram-ENE6RG2P-D7oP-ve7.js} +1 -1
- package/dist/assets/{quadrantDiagram-ABIIQ3AL-rBNhtAik.js → quadrantDiagram-ABIIQ3AL-BfmcdARc.js} +1 -1
- package/dist/assets/{railroadDiagram-RFXS5EU6-Bh0AsgK_.js → railroadDiagram-RFXS5EU6-CplgjUu4.js} +1 -1
- package/dist/assets/{requirementDiagram-TGXJPOKE-2oc4ih2c.js → requirementDiagram-TGXJPOKE-BiRTuy5B.js} +1 -1
- package/dist/assets/{sankeyDiagram-HTMAVEWB-B-ysEqL4.js → sankeyDiagram-HTMAVEWB-C81HtBjy.js} +1 -1
- package/dist/assets/{sequenceDiagram-DBY2YBRQ-6kNR0UNq.js → sequenceDiagram-DBY2YBRQ-DFhPtqpN.js} +1 -1
- package/dist/assets/{sizeCapture-X5ZJPWSS-C6PbumMD.js → sizeCapture-X5ZJPWSS-BkT4qJkY.js} +1 -1
- package/dist/assets/{stateDiagram-2N3HPSRC-CvLiUbMc.js → stateDiagram-2N3HPSRC-CKvn1pfo.js} +1 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-blzdlH4v.js +1 -0
- package/dist/assets/{swimlanes-5IMT3BWC-dAgZbHOX.js → swimlanes-5IMT3BWC-D5WhCwkM.js} +2 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-BiOWb3s9.js +8 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-B4uBF1kH.js → timeline-definition-FHXFAJF6-B-UTX_B8.js} +1 -1
- package/dist/assets/{vennDiagram-L72KCM5P-DmICWssw.js → vennDiagram-L72KCM5P-DelObiCK.js} +1 -1
- package/dist/assets/{wardleyDiagram-EHGQE667-CmKrH6Q5.js → wardleyDiagram-EHGQE667-DCWJdXij.js} +1 -1
- package/dist/assets/{web-DC2SNPaM.js → web-BOFBvPGv.js} +1 -1
- package/dist/assets/{web-eQSHkDHy.js → web-C2DocoQz.js} +1 -1
- package/dist/assets/{web-CnrzUv_G.js → web-qtcj3i0F.js} +1 -1
- package/dist/assets/{xychartDiagram-FW5EYKEG-B9wmI4CY.js → xychartDiagram-FW5EYKEG-CzbE79ZQ.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/index.html +1 -1
- package/dist/src/packages/server/data/builtin-skills/image-generation.js +64 -0
- package/dist/src/packages/server/data/builtin-skills/index.js +2 -0
- package/dist/src/packages/server/integrations/slack/slack-client.js +3 -0
- package/dist/src/packages/server/integrations/slack/slack-instance.js +12 -0
- package/dist/src/packages/server/integrations/slack/slack-routes.js +19 -0
- package/dist/src/packages/server/integrations/slack/slack-skill.js +13 -1
- package/dist/src/packages/server/routes/files.js +4 -0
- package/dist/src/packages/server/routes/images.js +53 -0
- package/dist/src/packages/server/routes/index.js +2 -0
- package/dist/src/packages/server/services/image-service.js +364 -0
- package/dist/src/packages/server/services/index.js +1 -0
- package/dist/src/packages/shared/image-types.js +11 -0
- package/dist/src/packages/shared/types.js +1 -0
- package/package.json +1 -1
- package/dist/assets/channel-BgIK1LRk.js +0 -1
- package/dist/assets/classDiagram-OUVF2IWQ-DqGhAOej.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-DqGhAOej.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-Ds464XsG.js +0 -1
- package/dist/assets/swimlanesDiagram-G3AALYLV-B1Qwmr49.js +0 -8
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Generation Service
|
|
3
|
+
*
|
|
4
|
+
* Wraps the OpenAI Images API (`/v1/images/generations`) so agents can turn a
|
|
5
|
+
* prompt into real files on disk through `/api/images/generate` instead of
|
|
6
|
+
* hand-rolling a curl with a raw API key in their shell history.
|
|
7
|
+
*
|
|
8
|
+
* Design constraints:
|
|
9
|
+
* - THE KEY NEVER LEAVES THE SERVER. It is resolved here (secrets store →
|
|
10
|
+
* environment → key file) and never accepted as a request field, so it
|
|
11
|
+
* cannot end up in an agent transcript, a log line or the exec history.
|
|
12
|
+
* - NEVER THROWS on a generation failure. Model refusals, HTTP errors and
|
|
13
|
+
* timeouts come back as `{ ok: false, error }` so the caller can report
|
|
14
|
+
* them as a normal result. Only programmer errors (bad arguments) throw.
|
|
15
|
+
* - Writes stay inside home / cwd / tmp — same containment rule the .http
|
|
16
|
+
* runner uses for the folders it will touch.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as fsp from 'fs/promises';
|
|
20
|
+
import * as os from 'os';
|
|
21
|
+
import * as path from 'path';
|
|
22
|
+
import { getDataDir } from '../data/index.js';
|
|
23
|
+
import { createLogger } from '../utils/logger.js';
|
|
24
|
+
import { getSecretByKey } from './secrets-service.js';
|
|
25
|
+
import { DEFAULT_IMAGE_MODEL, OPENAI_API_KEY_SECRET, } from '../../shared/image-types.js';
|
|
26
|
+
const log = createLogger('ImageService');
|
|
27
|
+
const OPENAI_IMAGES_URL = 'https://api.openai.com/v1/images/generations';
|
|
28
|
+
const DEFAULT_TIMEOUT_MS = 180_000;
|
|
29
|
+
const MAX_TIMEOUT_MS = 600_000;
|
|
30
|
+
const MAX_IMAGES = 10;
|
|
31
|
+
const MAX_PROMPT_CHARS = 32_000;
|
|
32
|
+
const FORMAT_EXTENSIONS = {
|
|
33
|
+
png: '.png',
|
|
34
|
+
jpeg: '.jpg',
|
|
35
|
+
webp: '.webp',
|
|
36
|
+
};
|
|
37
|
+
/** Path of the key file consulted as the last fallback. */
|
|
38
|
+
export function getKeyFilePath() {
|
|
39
|
+
return process.env.TIDE_OPENAI_KEY_FILE || path.join(os.homedir(), '.tide_openai_key');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Find the OpenAI API key. Secrets first (that's the one the user configured
|
|
43
|
+
* in the UI, so it should win over whatever the server process inherited),
|
|
44
|
+
* then `OPENAI_API_KEY`, then the key file.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveApiKey() {
|
|
47
|
+
const fromSecret = getSecretByKey(OPENAI_API_KEY_SECRET)?.value?.trim();
|
|
48
|
+
if (fromSecret)
|
|
49
|
+
return { key: fromSecret, source: 'secret' };
|
|
50
|
+
const fromEnv = process.env.OPENAI_API_KEY?.trim();
|
|
51
|
+
if (fromEnv)
|
|
52
|
+
return { key: fromEnv, source: 'env' };
|
|
53
|
+
try {
|
|
54
|
+
const fromFile = fs.readFileSync(getKeyFilePath(), 'utf-8').trim();
|
|
55
|
+
if (fromFile)
|
|
56
|
+
return { key: fromFile, source: 'file' };
|
|
57
|
+
}
|
|
58
|
+
catch { /* no key file — not an error, just unconfigured */ }
|
|
59
|
+
return { key: null, source: null };
|
|
60
|
+
}
|
|
61
|
+
/** `sk-proj-…f3a9` — enough to tell two keys apart, useless if leaked. */
|
|
62
|
+
export function maskKey(key) {
|
|
63
|
+
if (key.length <= 12)
|
|
64
|
+
return `${key.slice(0, 3)}…`;
|
|
65
|
+
return `${key.slice(0, 8)}…${key.slice(-4)}`;
|
|
66
|
+
}
|
|
67
|
+
/** Directory used when the caller does not pass `outputDir`. */
|
|
68
|
+
export function getDefaultOutputDir() {
|
|
69
|
+
return path.join(getDataDir(), 'generated-images');
|
|
70
|
+
}
|
|
71
|
+
export function getStatus() {
|
|
72
|
+
const { key, source } = resolveApiKey();
|
|
73
|
+
return {
|
|
74
|
+
configured: Boolean(key),
|
|
75
|
+
source: source ?? undefined,
|
|
76
|
+
maskedKey: key ? maskKey(key) : undefined,
|
|
77
|
+
defaultModel: DEFAULT_IMAGE_MODEL,
|
|
78
|
+
defaultOutputDir: getDefaultOutputDir(),
|
|
79
|
+
keyFilePath: getKeyFilePath(),
|
|
80
|
+
secretKey: OPENAI_API_KEY_SECRET,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// Output paths
|
|
85
|
+
// ============================================================================
|
|
86
|
+
function realpathOrResolve(target) {
|
|
87
|
+
try {
|
|
88
|
+
return fs.realpathSync(target);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return path.resolve(target);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve `dir` through symlinks as far as it exists, keeping the
|
|
96
|
+
* not-yet-created tail. Without this a brand-new output directory could never
|
|
97
|
+
* be checked against the allowed roots (realpath throws on missing paths), and
|
|
98
|
+
* `/home/user/link-to-elsewhere/new-dir` would slip past the containment test.
|
|
99
|
+
*/
|
|
100
|
+
function resolveThroughExistingAncestor(dir) {
|
|
101
|
+
const absolute = path.resolve(dir);
|
|
102
|
+
const tail = [];
|
|
103
|
+
let current = absolute;
|
|
104
|
+
while (!fs.existsSync(current)) {
|
|
105
|
+
const parent = path.dirname(current);
|
|
106
|
+
if (parent === current)
|
|
107
|
+
return absolute; // hit the fs root, nothing exists
|
|
108
|
+
tail.unshift(path.basename(current));
|
|
109
|
+
current = parent;
|
|
110
|
+
}
|
|
111
|
+
return path.join(realpathOrResolve(current), ...tail);
|
|
112
|
+
}
|
|
113
|
+
function containmentRoots() {
|
|
114
|
+
return [process.cwd(), os.homedir(), os.tmpdir()].filter(Boolean).map(realpathOrResolve);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* True when `dir` sits inside the working directory, the home directory or the
|
|
118
|
+
* system temp dir — the only places we will write generated images.
|
|
119
|
+
*/
|
|
120
|
+
export function isSafeOutputDir(dir) {
|
|
121
|
+
if (!dir)
|
|
122
|
+
return false;
|
|
123
|
+
const resolved = resolveThroughExistingAncestor(dir);
|
|
124
|
+
return containmentRoots().some((root) => {
|
|
125
|
+
const rel = path.relative(root, resolved);
|
|
126
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/** Lowercase, dash-separated stem derived from the prompt. */
|
|
130
|
+
export function slugifyPrompt(prompt, maxLength = 48) {
|
|
131
|
+
const slug = prompt
|
|
132
|
+
.toLowerCase()
|
|
133
|
+
.normalize('NFD')
|
|
134
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
135
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
136
|
+
.replace(/^-+|-+$/g, '')
|
|
137
|
+
.slice(0, maxLength)
|
|
138
|
+
.replace(/-+$/g, '');
|
|
139
|
+
return slug || 'image';
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Turn a caller-supplied filename into a safe basename stem: directory
|
|
143
|
+
* components, traversal and shell-hostile characters are stripped, and any
|
|
144
|
+
* image extension is dropped (the format decides the real one).
|
|
145
|
+
*/
|
|
146
|
+
export function sanitizeFilenameStem(filename) {
|
|
147
|
+
const base = path.basename(filename.trim())
|
|
148
|
+
.replace(/\.(png|jpe?g|webp)$/i, '')
|
|
149
|
+
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
150
|
+
.replace(/^[-.]+|-+$/g, '')
|
|
151
|
+
.slice(0, 120);
|
|
152
|
+
return base || 'image';
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Build the file names for one run. A single image keeps the bare stem; a
|
|
156
|
+
* batch gets `-1`, `-2`, … so nothing overwrites itself. Existing files are
|
|
157
|
+
* never clobbered — a numeric suffix is appended instead.
|
|
158
|
+
*/
|
|
159
|
+
export function buildOutputPaths(outputDir, stem, count, format, exists = fs.existsSync) {
|
|
160
|
+
const ext = FORMAT_EXTENSIONS[format];
|
|
161
|
+
const paths = [];
|
|
162
|
+
for (let i = 0; i < count; i++) {
|
|
163
|
+
const base = count > 1 ? `${stem}-${i + 1}` : stem;
|
|
164
|
+
let candidate = path.join(outputDir, `${base}${ext}`);
|
|
165
|
+
let dedupe = 2;
|
|
166
|
+
while (exists(candidate) || paths.includes(candidate)) {
|
|
167
|
+
candidate = path.join(outputDir, `${base}-${dedupe}${ext}`);
|
|
168
|
+
dedupe++;
|
|
169
|
+
}
|
|
170
|
+
paths.push(candidate);
|
|
171
|
+
}
|
|
172
|
+
return paths;
|
|
173
|
+
}
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// Request payload
|
|
176
|
+
// ============================================================================
|
|
177
|
+
/** DALL·E models take a different parameter vocabulary than the gpt-image family. */
|
|
178
|
+
function isDalleModel(model) {
|
|
179
|
+
return model.startsWith('dall-e');
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Translate our request body into the OpenAI payload for the target model.
|
|
183
|
+
* Exported for tests — the gpt-image / DALL·E split is easy to get wrong and
|
|
184
|
+
* the API rejects the whole call on one unknown field.
|
|
185
|
+
*/
|
|
186
|
+
export function buildRequestPayload(opts) {
|
|
187
|
+
const model = opts.model?.trim() || DEFAULT_IMAGE_MODEL;
|
|
188
|
+
const n = Math.min(Math.max(Math.trunc(opts.n ?? 1) || 1, 1), MAX_IMAGES);
|
|
189
|
+
const payload = { model, prompt: opts.prompt, n };
|
|
190
|
+
if (opts.size)
|
|
191
|
+
payload.size = opts.size;
|
|
192
|
+
if (isDalleModel(model)) {
|
|
193
|
+
// DALL·E only knows standard/hd, and only returns base64 when asked to.
|
|
194
|
+
if (opts.quality) {
|
|
195
|
+
payload.quality = opts.quality === 'high' || opts.quality === 'hd' ? 'hd' : 'standard';
|
|
196
|
+
}
|
|
197
|
+
payload.response_format = 'b64_json';
|
|
198
|
+
return payload;
|
|
199
|
+
}
|
|
200
|
+
if (opts.quality)
|
|
201
|
+
payload.quality = opts.quality;
|
|
202
|
+
if (opts.background)
|
|
203
|
+
payload.background = opts.background;
|
|
204
|
+
if (opts.format)
|
|
205
|
+
payload.output_format = opts.format;
|
|
206
|
+
if (typeof opts.compression === 'number' && (opts.format === 'jpeg' || opts.format === 'webp')) {
|
|
207
|
+
payload.output_compression = Math.min(Math.max(Math.trunc(opts.compression), 0), 100);
|
|
208
|
+
}
|
|
209
|
+
return payload;
|
|
210
|
+
}
|
|
211
|
+
/** Readable one-liner for an OpenAI error body / HTTP status. */
|
|
212
|
+
function describeApiError(status, body) {
|
|
213
|
+
let message = '';
|
|
214
|
+
try {
|
|
215
|
+
const parsed = JSON.parse(body);
|
|
216
|
+
message = parsed.error?.message || '';
|
|
217
|
+
}
|
|
218
|
+
catch { /* non-JSON error body (proxy/gateway) — fall back to the text */ }
|
|
219
|
+
const detail = message || body.slice(0, 400).trim() || 'no response body';
|
|
220
|
+
if (status === 401)
|
|
221
|
+
return `OpenAI rejected the API key (401): ${detail}`;
|
|
222
|
+
if (status === 403)
|
|
223
|
+
return `OpenAI denied access (403) — the org may not be verified for this model: ${detail}`;
|
|
224
|
+
if (status === 429)
|
|
225
|
+
return `OpenAI rate limit or quota exceeded (429): ${detail}`;
|
|
226
|
+
return `OpenAI images API failed (${status}): ${detail}`;
|
|
227
|
+
}
|
|
228
|
+
async function fetchImageBytes(entry) {
|
|
229
|
+
if (entry.b64_json)
|
|
230
|
+
return Buffer.from(entry.b64_json, 'base64');
|
|
231
|
+
if (entry.url) {
|
|
232
|
+
// DALL·E can hand back a short-lived CDN URL instead of base64.
|
|
233
|
+
const res = await fetch(entry.url);
|
|
234
|
+
if (!res.ok)
|
|
235
|
+
return null;
|
|
236
|
+
return Buffer.from(await res.arrayBuffer());
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Generate one or more images and write them to disk. Resolves with
|
|
242
|
+
* `ok: false` and a human-readable `error` for every failure mode except
|
|
243
|
+
* invalid arguments, which reject.
|
|
244
|
+
*/
|
|
245
|
+
export async function generateImages(opts) {
|
|
246
|
+
const startedAt = Date.now();
|
|
247
|
+
const prompt = (opts.prompt || '').trim();
|
|
248
|
+
const model = opts.model?.trim() || DEFAULT_IMAGE_MODEL;
|
|
249
|
+
const format = opts.format || 'png';
|
|
250
|
+
const outputDir = path.resolve(opts.outputDir?.trim() || getDefaultOutputDir());
|
|
251
|
+
const n = Math.min(Math.max(Math.trunc(opts.n ?? 1) || 1, 1), MAX_IMAGES);
|
|
252
|
+
const fail = (error) => ({
|
|
253
|
+
ok: false,
|
|
254
|
+
model,
|
|
255
|
+
prompt,
|
|
256
|
+
outputDir,
|
|
257
|
+
images: [],
|
|
258
|
+
timeMs: Date.now() - startedAt,
|
|
259
|
+
size: opts.size,
|
|
260
|
+
quality: opts.quality,
|
|
261
|
+
error,
|
|
262
|
+
agentId: opts.agentId,
|
|
263
|
+
});
|
|
264
|
+
if (!prompt)
|
|
265
|
+
throw new Error('Missing required field: prompt');
|
|
266
|
+
if (prompt.length > MAX_PROMPT_CHARS) {
|
|
267
|
+
throw new Error(`Prompt is too long (${prompt.length} chars, max ${MAX_PROMPT_CHARS})`);
|
|
268
|
+
}
|
|
269
|
+
if (!FORMAT_EXTENSIONS[format])
|
|
270
|
+
throw new Error(`Unsupported format: ${format}`);
|
|
271
|
+
if (!isSafeOutputDir(outputDir)) {
|
|
272
|
+
throw new Error('Refusing to write images outside the home, working or temp directory.');
|
|
273
|
+
}
|
|
274
|
+
const { key } = resolveApiKey();
|
|
275
|
+
if (!key) {
|
|
276
|
+
return fail(`No OpenAI API key configured. Add a secret named ${OPENAI_API_KEY_SECRET} in Settings → Secrets, ` +
|
|
277
|
+
`set OPENAI_API_KEY in the server environment, or write the key to ${getKeyFilePath()}.`);
|
|
278
|
+
}
|
|
279
|
+
const timeoutMs = Math.min(Math.max(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS, 1_000), MAX_TIMEOUT_MS);
|
|
280
|
+
const controller = new AbortController();
|
|
281
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
282
|
+
let parsed;
|
|
283
|
+
try {
|
|
284
|
+
const response = await fetch(OPENAI_IMAGES_URL, {
|
|
285
|
+
method: 'POST',
|
|
286
|
+
headers: {
|
|
287
|
+
Authorization: `Bearer ${key}`,
|
|
288
|
+
'Content-Type': 'application/json',
|
|
289
|
+
},
|
|
290
|
+
body: JSON.stringify(buildRequestPayload({ ...opts, prompt, model, n, format })),
|
|
291
|
+
signal: controller.signal,
|
|
292
|
+
});
|
|
293
|
+
const text = await response.text();
|
|
294
|
+
if (!response.ok)
|
|
295
|
+
return fail(describeApiError(response.status, text));
|
|
296
|
+
parsed = JSON.parse(text);
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
300
|
+
if (controller.signal.aborted) {
|
|
301
|
+
return fail(`Image generation timed out after ${timeoutMs}ms`);
|
|
302
|
+
}
|
|
303
|
+
return fail(`Image generation request failed: ${message}`);
|
|
304
|
+
}
|
|
305
|
+
finally {
|
|
306
|
+
clearTimeout(timer);
|
|
307
|
+
}
|
|
308
|
+
const entries = parsed.data ?? [];
|
|
309
|
+
if (entries.length === 0)
|
|
310
|
+
return fail('OpenAI returned no images');
|
|
311
|
+
try {
|
|
312
|
+
await fsp.mkdir(outputDir, { recursive: true });
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
return fail(`Could not create output directory ${outputDir}: ${err instanceof Error ? err.message : String(err)}`);
|
|
316
|
+
}
|
|
317
|
+
const stem = opts.filename ? sanitizeFilenameStem(opts.filename) : slugifyPrompt(prompt);
|
|
318
|
+
const targets = buildOutputPaths(outputDir, stem, entries.length, format);
|
|
319
|
+
const images = [];
|
|
320
|
+
for (let i = 0; i < entries.length; i++) {
|
|
321
|
+
let bytes;
|
|
322
|
+
try {
|
|
323
|
+
bytes = await fetchImageBytes(entries[i]);
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
return fail(`Could not download generated image ${i + 1}: ${err instanceof Error ? err.message : String(err)}`);
|
|
327
|
+
}
|
|
328
|
+
if (!bytes || bytes.length === 0)
|
|
329
|
+
return fail(`OpenAI returned an empty image (index ${i})`);
|
|
330
|
+
try {
|
|
331
|
+
await fsp.writeFile(targets[i], bytes);
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
return fail(`Could not write ${targets[i]}: ${err instanceof Error ? err.message : String(err)}`);
|
|
335
|
+
}
|
|
336
|
+
images.push({
|
|
337
|
+
path: targets[i],
|
|
338
|
+
filename: path.basename(targets[i]),
|
|
339
|
+
bytes: bytes.length,
|
|
340
|
+
format,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
const timeMs = Date.now() - startedAt;
|
|
344
|
+
log.log(`Generated ${images.length} image(s) with ${model} in ${timeMs}ms → ${outputDir}`);
|
|
345
|
+
return {
|
|
346
|
+
ok: true,
|
|
347
|
+
model,
|
|
348
|
+
prompt,
|
|
349
|
+
revisedPrompt: entries[0]?.revised_prompt,
|
|
350
|
+
outputDir,
|
|
351
|
+
images,
|
|
352
|
+
timeMs,
|
|
353
|
+
size: opts.size,
|
|
354
|
+
quality: opts.quality,
|
|
355
|
+
usage: parsed.usage
|
|
356
|
+
? {
|
|
357
|
+
totalTokens: parsed.usage.total_tokens,
|
|
358
|
+
inputTokens: parsed.usage.input_tokens,
|
|
359
|
+
outputTokens: parsed.usage.output_tokens,
|
|
360
|
+
}
|
|
361
|
+
: undefined,
|
|
362
|
+
agentId: opts.agentId,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
@@ -27,4 +27,5 @@ export * as workflowChatService from './workflow-chat-service.js';
|
|
|
27
27
|
export * as whatsappNotificationConfigService from './whatsapp-notification-config-service.js';
|
|
28
28
|
export * as testRunnerService from './test-runner-service.js';
|
|
29
29
|
export * as httpRequestsService from './http-requests-service.js';
|
|
30
|
+
export * as imageService from './image-service.js';
|
|
30
31
|
export { cadService } from './cad-service.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Image Generation (OpenAI Images API)
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Shared types for `/api/images/*`: agents describe an image in a prompt and
|
|
5
|
+
// Tide Commander writes the rendered PNG/JPEG/WebP files into a directory on
|
|
6
|
+
// disk. The API key never travels over this API — it is resolved server-side
|
|
7
|
+
// from the secrets store, the environment, or a key file.
|
|
8
|
+
/** Secret key (Settings → Secrets) the server reads the OpenAI key from. */
|
|
9
|
+
export const OPENAI_API_KEY_SECRET = 'OPENAI_API_KEY';
|
|
10
|
+
/** Image model used when the caller does not pick one. */
|
|
11
|
+
export const DEFAULT_IMAGE_MODEL = 'gpt-image-1';
|
|
@@ -12,6 +12,7 @@ export * from './database-types.js';
|
|
|
12
12
|
export * from './common-types.js';
|
|
13
13
|
export * from './test-runner-types.js';
|
|
14
14
|
export * from './http-requests-types.js';
|
|
15
|
+
export * from './image-types.js';
|
|
15
16
|
export * from './plugin-types.js';
|
|
16
17
|
export * from './websocket-messages.js';
|
|
17
18
|
export * from './trigger-types.js';
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{U as a,C as n}from"./mermaid.core-0qDPsI0T.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-V7JOEXUC-T5kHrm0m.js";import{_ as i}from"./mermaid.core-0qDPsI0T.js";import"./chunk-5VM5RSS4-CT5m9OBR.js";import"./chunk-XXDRQBXY-CeisNHDz.js";import"./chunk-VR4S4FIN-DsjgCLci.js";import"./chunk-32BRIVSS-BQmKdeF2.js";import"./main-BBvM-_Ia.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-DSCAlQQW.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-V7JOEXUC-T5kHrm0m.js";import{_ as i}from"./mermaid.core-0qDPsI0T.js";import"./chunk-5VM5RSS4-CT5m9OBR.js";import"./chunk-XXDRQBXY-CeisNHDz.js";import"./chunk-VR4S4FIN-DsjgCLci.js";import"./chunk-32BRIVSS-BQmKdeF2.js";import"./main-BBvM-_Ia.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-DSCAlQQW.js";var b={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{b as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as r,b as e,a,S as s}from"./chunk-EX3LRPZG-BwQ_dYOl.js";import{_ as i}from"./mermaid.core-0qDPsI0T.js";import"./chunk-XXDRQBXY-CeisNHDz.js";import"./chunk-VR4S4FIN-DsjgCLci.js";import"./chunk-32BRIVSS-BQmKdeF2.js";import"./main-BBvM-_Ia.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-DSCAlQQW.js";var b={parser:a,get db(){return new s(2)},renderer:e,styles:r,init:i(t=>{t.state||(t.state={}),t.state.arrowMarkerAbsolute=t.arrowMarkerAbsolute},"init")};export{b as diagram};
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import{c as r,s as e}from"./flowDiagram-23GEKE2U-BMSNxq_P.js";import{_ as a}from"./mermaid.core-0qDPsI0T.js";import"./chunk-5VM5RSS4-CT5m9OBR.js";import"./chunk-XXDRQBXY-CeisNHDz.js";import"./chunk-VR4S4FIN-DsjgCLci.js";import"./chunk-32BRIVSS-BQmKdeF2.js";import"./channel-BgIK1LRk.js";import"./main-BBvM-_Ia.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-DSCAlQQW.js";var o=a(t=>`${e(t)}
|
|
2
|
-
.swimlane.cluster rect {
|
|
3
|
-
stroke: ${t.clusterBorder} !important;
|
|
4
|
-
}
|
|
5
|
-
[data-look="neo"].cluster rect {
|
|
6
|
-
filter: none;
|
|
7
|
-
}
|
|
8
|
-
`,"getStyles"),m=o,_=r({defaultLayout:"swimlane",styles:m});export{_ as diagram};
|