@huaqiu/component-gen-server 0.3.6
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/LICENSE +21 -0
- package/lib/index.d.mts +205 -0
- package/lib/index.mjs +636 -0
- package/lib/standalone.mjs +754 -0
- package/package.json +47 -0
- package/src/backend.ts +33 -0
- package/src/history.ts +135 -0
- package/src/index.ts +48 -0
- package/src/jobs.ts +279 -0
- package/src/routes.ts +227 -0
- package/src/standalone.ts +172 -0
- package/src/types.ts +101 -0
package/src/routes.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/component-gen-server` — HTTP routes for the component-gen API.
|
|
3
|
+
*
|
|
4
|
+
* Mounted by the DSH plugin on `ctx.webServer` (kind 'prefix',
|
|
5
|
+
* `/api/v1/huaqiu/component-gen`) and by the standalone server. Route paths
|
|
6
|
+
* are parsed from `req.url` manually (DSH `WebRoute` supports only exact /
|
|
7
|
+
* prefix — no `:param`). CORS-free: everything is same-origin in both hosts.
|
|
8
|
+
*/
|
|
9
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
10
|
+
import type { ComponentGenBackend } from './backend.js'
|
|
11
|
+
import { HistoryStore, newImageId } from './history.js'
|
|
12
|
+
import { JobStore, runGeneration, type JobMeta } from './jobs.js'
|
|
13
|
+
import {
|
|
14
|
+
COMPONENT_GEN_ROUTE_PREFIX, MAX_IMAGE_BYTES,
|
|
15
|
+
type ComponentGenConfig, type HistoryPatch, type HistoryQuery, type JobEvent,
|
|
16
|
+
type JobState, type StartJobRequest,
|
|
17
|
+
} from './types.js'
|
|
18
|
+
|
|
19
|
+
export interface ComponentGenHandlerDeps {
|
|
20
|
+
backend: ComponentGenBackend
|
|
21
|
+
history: HistoryStore
|
|
22
|
+
hostMode?: boolean
|
|
23
|
+
getAccessToken?: () => Promise<string | null>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ComponentGenHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void> | void
|
|
27
|
+
|
|
28
|
+
function sendJson(res: ServerResponse, status: number, body: unknown): void {
|
|
29
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
|
30
|
+
res.end(JSON.stringify(body))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readBody(req: IncomingMessage): Promise<string> {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const chunks: Buffer[] = []
|
|
36
|
+
req.on('data', (c: Buffer) => chunks.push(c))
|
|
37
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
38
|
+
req.on('error', reject)
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function pathnameOf(url: string | undefined): string {
|
|
43
|
+
const u = url ?? ''
|
|
44
|
+
const q = u.indexOf('?')
|
|
45
|
+
return (q >= 0 ? u.slice(0, q) : u).replace(/\/+$/, '')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function jsonBodyOf<T>(text: string): T {
|
|
49
|
+
return JSON.parse(text || '{}') as T
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Write one SSE frame and flush. */
|
|
53
|
+
function sse(res: ServerResponse, event: string, payload: unknown): void {
|
|
54
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isFinal(status: JobState['status']): boolean {
|
|
58
|
+
return status === 'needs_confirmation' || status === 'completed' || status === 'failed' || status === 'cancelled'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Map a current job state to its replay SSE event (or null when queued/running). */
|
|
62
|
+
function replayEventOf(state: JobState): JobEvent | null {
|
|
63
|
+
if (state.status === 'needs_confirmation') {
|
|
64
|
+
return { type: 'needs_confirmation', dimensions: state.dimensions ?? {}, pkgType: state.pkgType ?? null, fileName: state.fileName ?? null, at: state.updatedAt }
|
|
65
|
+
}
|
|
66
|
+
if (state.status === 'completed') return { type: 'completed', job: state, at: state.updatedAt }
|
|
67
|
+
if (state.status === 'failed') return { type: 'failed', error: state.error ?? 'generation failed', result: state.result, at: state.updatedAt }
|
|
68
|
+
if (state.status === 'cancelled') return { type: 'cancelled', at: state.updatedAt }
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createComponentGenHandler(deps: ComponentGenHandlerDeps): ComponentGenHandler {
|
|
73
|
+
const store = new JobStore()
|
|
74
|
+
|
|
75
|
+
return async (req, res) => {
|
|
76
|
+
const raw = pathnameOf(req.url)
|
|
77
|
+
if (!raw.startsWith(COMPONENT_GEN_ROUTE_PREFIX)) {
|
|
78
|
+
sendJson(res, 404, { error: 'not found' })
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
const path = raw.slice(COMPONENT_GEN_ROUTE_PREFIX.length) || '/'
|
|
82
|
+
const method = req.method ?? 'GET'
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// GET /config
|
|
86
|
+
if (method === 'GET' && path === '/config') {
|
|
87
|
+
const cfg: ComponentGenConfig = {
|
|
88
|
+
hostMode: deps.hostMode === true,
|
|
89
|
+
capabilities: { symbol: true, footprint: true },
|
|
90
|
+
limits: { imageBytes: MAX_IMAGE_BYTES },
|
|
91
|
+
}
|
|
92
|
+
sendJson(res, 200, cfg)
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// POST /jobs
|
|
97
|
+
if (method === 'POST' && path === '/jobs') {
|
|
98
|
+
const body = jsonBodyOf<StartJobRequest>(await readBody(req))
|
|
99
|
+
if (!body || (body.kind !== 'symbol' && body.kind !== 'extract-footprint' && body.kind !== 'generate-footprint')) {
|
|
100
|
+
sendJson(res, 400, { error: 'invalid job kind (expected symbol | extract-footprint | generate-footprint)' })
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
const input = body.input ?? {}
|
|
104
|
+
if (input.imageDataUrl && input.imageDataUrl.length > MAX_IMAGE_BYTES) {
|
|
105
|
+
sendJson(res, 413, { error: 'image too large', detail: `max ${MAX_IMAGE_BYTES} bytes` })
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
const meta: JobMeta = {}
|
|
109
|
+
if (input.imageDataUrl) {
|
|
110
|
+
const imageId = newImageId()
|
|
111
|
+
await deps.history.saveImage(imageId, input.imageDataUrl)
|
|
112
|
+
meta.imageId = imageId
|
|
113
|
+
}
|
|
114
|
+
const state = store.create({ kind: body.kind, input }, meta)
|
|
115
|
+
void runGeneration(store, deps.backend, deps.history, state.id, { kind: body.kind, input }, meta).catch((err) => {
|
|
116
|
+
console.warn('[component-gen] background run failed', String((err as Error)?.message || err))
|
|
117
|
+
})
|
|
118
|
+
res.writeHead(202, { 'content-type': 'application/json; charset=utf-8' })
|
|
119
|
+
res.end(JSON.stringify({ jobId: state.id }))
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// GET /jobs/:id
|
|
124
|
+
const jobGet = /^\/jobs\/([^/]+)$/.exec(path)
|
|
125
|
+
if (method === 'GET' && jobGet) {
|
|
126
|
+
const state = store.get(jobGet[1]!)
|
|
127
|
+
if (!state) { sendJson(res, 404, { error: 'job not found' }); return }
|
|
128
|
+
sendJson(res, 200, state)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// GET /jobs/:id/events (SSE)
|
|
133
|
+
const jobEvents = /^\/jobs\/([^/]+)\/events$/.exec(path)
|
|
134
|
+
if (method === 'GET' && jobEvents) {
|
|
135
|
+
const id = jobEvents[1]!
|
|
136
|
+
const state = store.get(id)
|
|
137
|
+
if (!state) { sendJson(res, 404, { error: 'job not found' }); return }
|
|
138
|
+
res.writeHead(200, {
|
|
139
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
140
|
+
'cache-control': 'no-cache',
|
|
141
|
+
connection: 'keep-alive',
|
|
142
|
+
})
|
|
143
|
+
res.write(': ok\n\n')
|
|
144
|
+
|
|
145
|
+
// Replay current state first (covers jobs that finished pre-subscribe).
|
|
146
|
+
const replay = replayEventOf(state)
|
|
147
|
+
if (replay) sse(res, replay.type, replay)
|
|
148
|
+
if (isFinal(state.status)) {
|
|
149
|
+
res.end()
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const unsub = store.subscribe(id, (e) => {
|
|
154
|
+
sse(res, e.type, e)
|
|
155
|
+
if (e.type === 'needs_confirmation' || e.type === 'completed' || e.type === 'failed' || e.type === 'cancelled') {
|
|
156
|
+
unsub?.()
|
|
157
|
+
res.end()
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
req.on('close', () => unsub?.())
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// DELETE /jobs/:id
|
|
165
|
+
const jobDel = /^\/jobs\/([^/]+)$/.exec(path)
|
|
166
|
+
if (method === 'DELETE' && jobDel) {
|
|
167
|
+
const ok = store.abort(jobDel[1]!)
|
|
168
|
+
sendJson(res, ok ? 200 : 404, { ok })
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// GET /history
|
|
173
|
+
if (method === 'GET' && path === '/history') {
|
|
174
|
+
const url = new URL(req.url ?? '/', 'http://localhost')
|
|
175
|
+
const query: HistoryQuery = {
|
|
176
|
+
limit: url.searchParams.has('limit') ? Number(url.searchParams.get('limit')) : undefined,
|
|
177
|
+
cursor: url.searchParams.get('cursor'),
|
|
178
|
+
}
|
|
179
|
+
sendJson(res, 200, await deps.history.list(query))
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// GET /history/:imageId/image
|
|
184
|
+
// `:imageId` is the stored thumbnail id (`img_…`), not a history entry
|
|
185
|
+
// id — the client's `ports.inputImage` sends it directly.
|
|
186
|
+
const histImage = /^\/history\/([^/]+)\/image$/.exec(path)
|
|
187
|
+
if (method === 'GET' && histImage) {
|
|
188
|
+
const img = await deps.history.readImage(histImage[1]!)
|
|
189
|
+
if (!img) { sendJson(res, 404, { error: 'image not found' }); return }
|
|
190
|
+
res.writeHead(200, { 'content-type': img.mime, 'cache-control': 'public, max-age=3600' })
|
|
191
|
+
res.end(Buffer.from(img.bytes))
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// GET /history/:id
|
|
196
|
+
const histGet = /^\/history\/([^/]+)$/.exec(path)
|
|
197
|
+
if (method === 'GET' && histGet) {
|
|
198
|
+
const entry = await deps.history.get(histGet[1]!)
|
|
199
|
+
if (!entry) { sendJson(res, 404, { error: 'history not found' }); return }
|
|
200
|
+
sendJson(res, 200, entry)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// PATCH /history/:id
|
|
205
|
+
const histPatch = /^\/history\/([^/]+)$/.exec(path)
|
|
206
|
+
if (method === 'PATCH' && histPatch) {
|
|
207
|
+
const patch = jsonBodyOf<HistoryPatch>(await readBody(req))
|
|
208
|
+
const entry = await deps.history.patch(histPatch[1]!, patch)
|
|
209
|
+
if (!entry) { sendJson(res, 404, { error: 'history not found' }); return }
|
|
210
|
+
sendJson(res, 200, entry)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// DELETE /history/:id
|
|
215
|
+
const histDel = /^\/history\/([^/]+)$/.exec(path)
|
|
216
|
+
if (method === 'DELETE' && histDel) {
|
|
217
|
+
await deps.history.delete(histDel[1]!)
|
|
218
|
+
sendJson(res, 200, { ok: true })
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
sendJson(res, 404, { error: 'not found' })
|
|
223
|
+
} catch (err) {
|
|
224
|
+
sendJson(res, 500, { error: 'internal error', detail: String(err) })
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/component-gen-server` — standalone server.
|
|
3
|
+
*
|
|
4
|
+
* A self-contained local server (no DSH host needed) that serves:
|
|
5
|
+
* - the `@huaqiu/component-gen-app` static bundle (dist),
|
|
6
|
+
* - the component-gen API (`/api/v1/huaqiu/component-gen/*`),
|
|
7
|
+
* - the `@huaqiu/dsh-auth` session routes (login bridge; the same
|
|
8
|
+
* `InMemoryHuaqiuAuthService` the DSH node half uses),
|
|
9
|
+
* - the `@huaqiu/dsh-artifacts` routes (preview artifact content).
|
|
10
|
+
*
|
|
11
|
+
* The generation backend is the plugin's own `createComponentGenBackend` —
|
|
12
|
+
* same `runGenerate*` functions, no reimplementation. Auth is injected through
|
|
13
|
+
* the dsh-auth public service; the backend never implements the login flow.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* hq-component-gen [--port 8787]
|
|
17
|
+
*/
|
|
18
|
+
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
|
|
19
|
+
import { createRequire } from 'node:module'
|
|
20
|
+
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
21
|
+
import { join, extname, resolve } from 'node:path'
|
|
22
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
|
|
23
|
+
import { InMemoryHuaqiuAuthService, createAuthHandler, AUTH_ROUTE_PREFIX, type HuaqiuAuthService } from '@huaqiu/dsh-auth'
|
|
24
|
+
import { HuaqiuArtifactService, createArtifactsHandler, ARTIFACTS_ROUTE_PREFIX } from '@huaqiu/dsh-artifacts'
|
|
25
|
+
import { runGenerateSymbol, runGenerateFootprintFromImage, runGenerateFootprintFromDimensions, createComponentGenBackend, type SymbolFootprintEnv } from '@huaqiu/dsh-tool-symbol-footprint'
|
|
26
|
+
import { createComponentGenHandler } from './index.js'
|
|
27
|
+
import { HistoryStore } from './history.js'
|
|
28
|
+
import { COMPONENT_GEN_ROUTE_PREFIX } from './types.js'
|
|
29
|
+
|
|
30
|
+
const require = createRequire(import.meta.url)
|
|
31
|
+
|
|
32
|
+
export interface StandaloneServerOptions {
|
|
33
|
+
port?: number
|
|
34
|
+
host?: string
|
|
35
|
+
/** component-gen-app dist dir (default: resolved from the package). */
|
|
36
|
+
appDist?: string
|
|
37
|
+
/** history dir (default `~/.dsh/component-gen/`). */
|
|
38
|
+
historyDir?: string
|
|
39
|
+
/** artifacts dir (default `~/.dsh/artifacts/`). */
|
|
40
|
+
artifactsDir?: string
|
|
41
|
+
authConfig?: Record<string, unknown> | null
|
|
42
|
+
hitlLanguage?: 'zh' | 'en'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface StandaloneServer {
|
|
46
|
+
server: ReturnType<typeof createServer>
|
|
47
|
+
port: number
|
|
48
|
+
auth: HuaqiuAuthService
|
|
49
|
+
history: HistoryStore
|
|
50
|
+
close(): Promise<void>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const MIME: Record<string, string> = {
|
|
54
|
+
'.html': 'text/html; charset=utf-8',
|
|
55
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
56
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
57
|
+
'.css': 'text/css; charset=utf-8',
|
|
58
|
+
'.json': 'application/json; charset=utf-8',
|
|
59
|
+
'.svg': 'image/svg+xml',
|
|
60
|
+
'.png': 'image/png',
|
|
61
|
+
'.jpg': 'image/jpeg',
|
|
62
|
+
'.jpeg': 'image/jpeg',
|
|
63
|
+
'.ico': 'image/x-icon',
|
|
64
|
+
'.woff2': 'font/woff2',
|
|
65
|
+
'.map': 'application/json; charset=utf-8',
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveAppDist(override?: string): string {
|
|
69
|
+
if (override) return resolve(override)
|
|
70
|
+
try {
|
|
71
|
+
const pkgPath = require.resolve('@huaqiu/component-gen-app/package.json')
|
|
72
|
+
return join(pkgPath.replace(/package\.json$/, ''), 'dist')
|
|
73
|
+
} catch {
|
|
74
|
+
// Monorepo fallback (before publish): lib/standalone.mjs → packages/component-gen-app/dist
|
|
75
|
+
const local = resolve(new URL('../../component-gen-app/dist', import.meta.url).pathname)
|
|
76
|
+
return local
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Static file responder with traversal protection. */
|
|
81
|
+
function serveStatic(root: string, urlPath: string, res: ServerResponse): void {
|
|
82
|
+
const decoded = decodeURIComponent(urlPath.split('?')[0] ?? '/')
|
|
83
|
+
let rel = decoded === '/' ? '/index.html' : decoded
|
|
84
|
+
if (rel.startsWith('/')) rel = rel.slice(1)
|
|
85
|
+
const target = resolve(root, rel)
|
|
86
|
+
if (!target.startsWith(resolve(root)) || !target.startsWith(root)) {
|
|
87
|
+
res.writeHead(403); res.end('forbidden'); return
|
|
88
|
+
}
|
|
89
|
+
if (!existsSync(target) || !statSync(target).isFile()) {
|
|
90
|
+
// SPA fallback: serve index.html for unknown non-asset paths.
|
|
91
|
+
const idx = join(root, 'index.html')
|
|
92
|
+
if (existsSync(idx)) {
|
|
93
|
+
res.writeHead(200, { 'content-type': MIME['.html'] })
|
|
94
|
+
res.end(readFileSync(idx))
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
res.writeHead(404); res.end('not found'); return
|
|
98
|
+
}
|
|
99
|
+
res.writeHead(200, { 'content-type': MIME[extname(target).toLowerCase()] ?? 'application/octet-stream' })
|
|
100
|
+
res.end(readFileSync(target))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function createStandaloneServer(options: StandaloneServerOptions = {}): Promise<StandaloneServer> {
|
|
104
|
+
const port = options.port ?? 8787
|
|
105
|
+
const host = options.host ?? '127.0.0.1'
|
|
106
|
+
const appDist = resolveAppDist(options.appDist)
|
|
107
|
+
const history = new HistoryStore(options.historyDir ?? dshHomePath('component-gen'))
|
|
108
|
+
|
|
109
|
+
const auth = new InMemoryHuaqiuAuthService(options.authConfig as never)
|
|
110
|
+
const artifacts = new HuaqiuArtifactService({ baseDir: options.artifactsDir ?? dshHomePath('artifacts') })
|
|
111
|
+
|
|
112
|
+
const env: SymbolFootprintEnv = {
|
|
113
|
+
auth: auth.auth,
|
|
114
|
+
artifacts,
|
|
115
|
+
hitlLanguage: options.hitlLanguage ?? 'zh',
|
|
116
|
+
deps: { processEnv: typeof process !== 'undefined' ? process.env : undefined },
|
|
117
|
+
// The app is the driver: never pause on the native HIL popup.
|
|
118
|
+
getUserQuestions: () => undefined,
|
|
119
|
+
}
|
|
120
|
+
const backend = createComponentGenBackend(env)
|
|
121
|
+
const componentGen = createComponentGenHandler({
|
|
122
|
+
backend,
|
|
123
|
+
history,
|
|
124
|
+
hostMode: auth.hostMode,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const server = createServer(async (req, res) => {
|
|
128
|
+
const urlPath = req.url ?? '/'
|
|
129
|
+
if (urlPath.startsWith(AUTH_ROUTE_PREFIX)) {
|
|
130
|
+
await createAuthHandler(auth)(req, res)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
if (urlPath.startsWith(ARTIFACTS_ROUTE_PREFIX)) {
|
|
134
|
+
await createArtifactsHandler(artifacts)(req, res)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
if (urlPath.startsWith(COMPONENT_GEN_ROUTE_PREFIX)) {
|
|
138
|
+
await componentGen(req, res)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
serveStatic(appDist, urlPath, res)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
await new Promise<void>((resolveListen) => {
|
|
145
|
+
server.listen(port, host, resolveListen)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
server,
|
|
150
|
+
port,
|
|
151
|
+
auth,
|
|
152
|
+
history,
|
|
153
|
+
close: async () => new Promise((resolveClose) => server.close(() => resolveClose())),
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** CLI entry (`hq-component-gen`). */
|
|
158
|
+
async function main(): Promise<void> {
|
|
159
|
+
const args = process.argv.slice(2)
|
|
160
|
+
let port = 8787
|
|
161
|
+
for (let i = 0; i < args.length; i++) {
|
|
162
|
+
if (args[i] === '--port' && args[i + 1]) port = Number(args[i + 1])
|
|
163
|
+
}
|
|
164
|
+
const app = await createStandaloneServer({ port })
|
|
165
|
+
const urls = [`http://localhost:${app.port}/?page=footprint`, `http://localhost:${app.port}/?page=symbol`]
|
|
166
|
+
console.log(`[hq-component-gen] standalone server on ${urls[0]}`)
|
|
167
|
+
console.log(`[hq-component-gen] symbol: ${urls[1]}`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (process.argv[1] && /standalone/.test(process.argv[1])) {
|
|
171
|
+
void main()
|
|
172
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/component-gen-server` — shared domain types.
|
|
3
|
+
*
|
|
4
|
+
* These mirror `@huaqiu/component-gen-app/src/ports.ts` (structurally
|
|
5
|
+
* identical, JSON interop) but are owned by the server as the authority.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type ComponentGenPage = 'symbol' | 'footprint'
|
|
9
|
+
|
|
10
|
+
export type JobKind = 'symbol' | 'extract-footprint' | 'generate-footprint'
|
|
11
|
+
|
|
12
|
+
export type JobStatus =
|
|
13
|
+
| 'queued'
|
|
14
|
+
| 'running'
|
|
15
|
+
| 'needs_confirmation'
|
|
16
|
+
| 'completed'
|
|
17
|
+
| 'failed'
|
|
18
|
+
| 'cancelled'
|
|
19
|
+
|
|
20
|
+
export interface JobInput {
|
|
21
|
+
imageDataUrl?: string
|
|
22
|
+
instruction?: string
|
|
23
|
+
packageType?: string
|
|
24
|
+
dimensions?: Record<string, number>
|
|
25
|
+
fileName?: string
|
|
26
|
+
edited?: Record<string, boolean>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface JobState {
|
|
30
|
+
id: string
|
|
31
|
+
kind: JobKind
|
|
32
|
+
status: JobStatus
|
|
33
|
+
progress?: string
|
|
34
|
+
result?: Record<string, unknown>
|
|
35
|
+
dimensions?: Record<string, unknown>
|
|
36
|
+
pkgType?: string | null
|
|
37
|
+
fileName?: string | null
|
|
38
|
+
error?: string
|
|
39
|
+
createdAt: string
|
|
40
|
+
updatedAt: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type JobEvent =
|
|
44
|
+
| { type: 'progress'; message: string; at: string }
|
|
45
|
+
| { type: 'needs_confirmation'; dimensions: Record<string, unknown>; pkgType?: string | null; fileName?: string | null; at: string }
|
|
46
|
+
| { type: 'completed'; job: JobState; at: string }
|
|
47
|
+
| { type: 'failed'; error: string; result?: Record<string, unknown>; at: string }
|
|
48
|
+
| { type: 'cancelled'; at: string }
|
|
49
|
+
|
|
50
|
+
export interface StartJobRequest {
|
|
51
|
+
kind: JobKind
|
|
52
|
+
input: JobInput
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface HistoryQuery {
|
|
56
|
+
limit?: number
|
|
57
|
+
cursor?: string | null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface HistoryPage {
|
|
61
|
+
entries: HistoryEntry[]
|
|
62
|
+
nextCursor?: string | null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface HistoryEntry {
|
|
66
|
+
id: string
|
|
67
|
+
kind: 'symbol' | 'footprint'
|
|
68
|
+
createdAt: string
|
|
69
|
+
status: 'generated' | 'failed' | 'cancelled'
|
|
70
|
+
input: {
|
|
71
|
+
imageId?: string
|
|
72
|
+
instruction?: string
|
|
73
|
+
packageType?: string
|
|
74
|
+
dimensions?: Record<string, number>
|
|
75
|
+
}
|
|
76
|
+
edited?: Record<string, boolean>
|
|
77
|
+
result?: {
|
|
78
|
+
artifactId: string
|
|
79
|
+
filename: string
|
|
80
|
+
fileUrl?: string
|
|
81
|
+
size?: number
|
|
82
|
+
}
|
|
83
|
+
error?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface HistoryPatch {
|
|
87
|
+
status?: HistoryEntry['status']
|
|
88
|
+
result?: HistoryEntry['result']
|
|
89
|
+
error?: string
|
|
90
|
+
edited?: Record<string, boolean>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ComponentGenConfig {
|
|
94
|
+
hostMode: boolean
|
|
95
|
+
capabilities: { symbol: boolean; footprint: boolean }
|
|
96
|
+
limits: { imageBytes: number }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const COMPONENT_GEN_ROUTE_PREFIX = '/api/v1/huaqiu/component-gen'
|
|
100
|
+
|
|
101
|
+
export const MAX_IMAGE_BYTES = 4 * 1024 * 1024
|