99 * Protocol (JSON):
1010 * GET /health
1111 * GET /status
12- * POST /apply/image { "imagePath": "..." }
13- * POST /apply/video { "videoPath": "...", "imagePath"?: "...", "startAt"?: number }
12+ * POST /apply/image { "imagePath": "...", "source"?: "managed"|"local" }
13+ * POST /apply/video { "videoPath": "...", "imagePath"?: "...", "source"?: "managed"|"local", " startAt"?: number }
1414 * POST /apply/clear {}
1515 * POST /reapply {} // republish active background into live sessions
16+ * POST /theme/apply { "name": "...", "input": ApplyInput }
1617 * POST /theme/save { "name": "..." } // keep current image/video
1718 * GET /theme/list
1819 * POST /theme/use { "id": "..." }
@@ -51,6 +52,60 @@ function argValue(name) {
5152 return value && ! value . startsWith ( "--" ) ? value : null ;
5253}
5354
55+ function badRequest ( message ) {
56+ const error = new Error ( message ) ;
57+ error . statusCode = 400 ;
58+ return error ;
59+ }
60+
61+ function parseImportMode ( value ) {
62+ if ( value == null ) return undefined ;
63+ if ( value === "managed" || value === "local" ) return value ;
64+ throw badRequest ( "source 必须是 managed 或 local。" ) ;
65+ }
66+
67+ function parseThemeApplyInput ( value ) {
68+ if ( ! value || typeof value !== "object" || Array . isArray ( value ) ) {
69+ throw badRequest ( "input 必须是图片或视频导入参数。" ) ;
70+ }
71+ if ( value . type === "image" ) {
72+ if ( typeof value . imagePath !== "string" || ! value . imagePath ) {
73+ throw badRequest ( "图片主题必须提供 imagePath。" ) ;
74+ }
75+ const input = {
76+ type : "image" ,
77+ imagePath : path . resolve ( value . imagePath ) ,
78+ source : parseImportMode ( value . source ) ,
79+ } ;
80+ if ( value . effects && typeof value . effects === "object" ) {
81+ input . effects = value . effects ;
82+ }
83+ return input ;
84+ }
85+ if ( value . type === "video" ) {
86+ if ( typeof value . videoPath !== "string" || ! value . videoPath ) {
87+ throw badRequest ( "视频主题必须提供 videoPath。" ) ;
88+ }
89+ const input = {
90+ type : "video" ,
91+ videoPath : path . resolve ( value . videoPath ) ,
92+ source : parseImportMode ( value . source ) ,
93+ } ;
94+ if ( typeof value . imagePath === "string" && value . imagePath ) {
95+ input . imagePath = path . resolve ( value . imagePath ) ;
96+ }
97+ if ( value . startAt != null ) {
98+ const startAt = Number ( value . startAt ) ;
99+ if ( ! Number . isFinite ( startAt ) || startAt < 0 ) {
100+ throw badRequest ( "startAt 必须是非负数字(秒)。" ) ;
101+ }
102+ input . startAt = startAt ;
103+ }
104+ return input ;
105+ }
106+ throw badRequest ( "input.type 必须是 image 或 video。" ) ;
107+ }
108+
54109const hostKind = argValue ( "--host" ) ?? "codex" ;
55110if ( hostKind !== "codex" && hostKind !== "dsh" ) {
56111 console . error ( "--host 必须是 codex 或 dsh。" ) ;
@@ -192,6 +247,9 @@ function publicTheme(theme) {
192247 name : theme . name ,
193248 type : theme . type ,
194249 savedAt : theme . savedAt ,
250+ ...( typeof theme . sourceMode === "string"
251+ ? { sourceMode : theme . sourceMode }
252+ : { } ) ,
195253 ...( theme . bundled ? { bundled : true } : { } ) ,
196254 ...( typeof theme . videoPositionSec === "number"
197255 ? { videoPositionSec : theme . videoPositionSec }
@@ -312,6 +370,7 @@ const server = http.createServer(
312370 type : "image" ,
313371 imagePath : path . resolve ( body . imagePath ) ,
314372 } ;
373+ imageInput . source = parseImportMode ( body . source ) ;
315374 if ( body . effects && typeof body . effects === "object" ) {
316375 imageInput . effects = body . effects ;
317376 }
@@ -332,6 +391,7 @@ const server = http.createServer(
332391 type : "video" ,
333392 videoPath : path . resolve ( body . videoPath ) ,
334393 } ;
394+ videoInput . source = parseImportMode ( body . source ) ;
335395 if ( typeof body . imagePath === "string" && body . imagePath ) {
336396 videoInput . imagePath = path . resolve ( body . imagePath ) ;
337397 }
@@ -357,6 +417,25 @@ const server = http.createServer(
357417 send ( res , result . ok ? 200 : 422 , result ) ;
358418 return ;
359419 }
420+ if ( req . method === "POST" && url === "/theme/apply" ) {
421+ const body = await readBody ( req ) ;
422+ if ( typeof body . name !== "string" || ! body . name . trim ( ) ) {
423+ send ( res , 400 , { ok : false , error : "必须提供主题名称。" } ) ;
424+ return ;
425+ }
426+ if ( typeof session . applyAndSaveTheme !== "function" ) {
427+ send ( res , 501 , { ok : false , error : "当前会话不支持应用并保存主题。" } ) ;
428+ return ;
429+ }
430+ const input = parseThemeApplyInput ( body . input ) ;
431+ const result = await session . applyAndSaveTheme ( input , body . name . trim ( ) ) ;
432+ send (
433+ res ,
434+ result . ok ? 200 : 422 ,
435+ result . ok ? { ...result , theme : publicTheme ( result . theme ) } : result ,
436+ ) ;
437+ return ;
438+ }
360439 if ( req . method === "POST" && url === "/theme/save" ) {
361440 const body = await readBody ( req ) ;
362441 if ( typeof body . name !== "string" || ! body . name . trim ( ) ) {
0 commit comments