flash-story-app 1.0.0 → 1.1.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/README.md CHANGED
@@ -36,8 +36,10 @@ Environment:
36
36
  | `FLASHSTORY_OPENAI_MODELS` | = MODEL | **fallback** selectable model list (comma/space separated), used only when the endpoint's live `/v1/models` probe fails |
37
37
  | `FLASHSTORY_SERVER` | `http://127.0.0.1:8787` | CLI client target (flag `--server` wins) |
38
38
 
39
- Serve-mode CLI flags (override env / .env): `--port <n>` `--host <h>`
40
- `--data-dir <dir>` `--open` (open the editor in the default browser after boot).
39
+ Serve-mode flags (override env / .env):
40
+ `--port <n>` `--host <h>` `--data-dir <dir>` `--open`. The editor opens in the
41
+ default browser automatically after boot; pass `--no-open` to keep it
42
+ headless (server still prints its URL).
41
43
 
42
44
  ### .env support
43
45
 
@@ -72,6 +74,16 @@ The settings dialog reads the model list from the server: a live probe of the
72
74
  endpoint's `/v1/models` (cached 60s with a 2.5s timeout), falling back to
73
75
  `FLASHSTORY_OPENAI_MODELS` (or the default model) when the probe fails.
74
76
 
77
+ ### Custom endpoint / key (settings-AI)
78
+
79
+ The settings dialog's AI tab can set a custom **API Base URL** and **API Key**
80
+ that override the environment. They are stored ON THE SERVER
81
+ (`~/.flashstory/llm-override.json` — behaving like a `.env` overlay: applied
82
+ globally for every browser and the CLI, hot without a restart, and "clear"
83
+ removes the override so the env config wins again). The browser never persists
84
+ the key, and no server response ever returns it. The "test connection" button
85
+ probes whatever the draft fields hold before you commit them.
86
+
75
87
  ## Dev mode (vite)
76
88
 
77
89
  In dev the frontend calls the backend **directly** (`http://127.0.0.1:8787` by
package/dist/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{spawn as P}from"node:child_process";import{mkdirSync as F,writeFileSync as O,readFileSync as R}from"node:fs";import{join as S}from"node:path";import{homedir as C}from"node:os";import{l as q}from"./env.js";class w extends Error{}async function N(e,t,r,i){const d=await fetch(`${e}${r}`,{method:t,headers:i!==void 0?{"content-type":"application/json"}:void 0,body:i!==void 0?JSON.stringify(i):void 0}),c=await d.json().catch(()=>null);return{status:d.status,data:c}}async function A(e,t,r){const i=await fetch(`${e}${r}`,{method:t});return{status:i.status,text:await i.text()}}async function D(e,t){const r=await fetch(`${e}${t}`);return{status:r.status,bytes:new Uint8Array(await r.arrayBuffer())}}function k(e,t){if(e.status>=200&&e.status<300)return e.data;const r=e.data?.detail??e.data?.error??`HTTP ${e.status}`;throw new w(`${t} failed: ${r}`)}const G={get:N,post:N,binary:D,text:A};function H(e){const t=[],r={};for(let i=0;i<e.length;i++){const d=e[i];if(d.startsWith("--")){const c=d.slice(2),s=e[i+1];s!==void 0&&!s.startsWith("--")?(r[c]=s,i+=1):r[c]=!0}else t.push(d)}return{positionals:t,flags:r}}function o(e,t){const r=e[t];return typeof r=="string"?r:void 0}function J(e){const t=o(e,"project");if(!t)throw new w("--project <id> is required");return t}function g(e,t){if(!e)throw new w(`--json <${t}> is required`);try{return JSON.parse(e)}catch{throw new w(`--json <${t}> is not valid JSON`)}}async function h(e,t,r,i,d=!1){const c=await e.post(t,"POST",d?"/api/tools/batch":"/api/tools/execute",{projectId:r,...d?{actions:i}:{action:i}});if(c.status===404)return{data:{error:"project not found"},exit:1};const s=c.data;return s.ok?{data:{ok:!0,rev:s.rev},exit:0}:{data:{ok:!1,error:s.error??"rejected"},exit:1}}const y=e=>e().catch(t=>({data:{error:t instanceof Error?t.message:String(t)},exit:1}));async function B(e,t,r=G){const[i,...d]=e,{positionals:c,flags:s}=H(d),l=()=>J(s);switch(i){case"health":return y(async()=>{const u=await r.get(t,"GET","/health");return{data:k(u,"health"),exit:0}});case"scene":{const u=c[0];if(u==="list")return y(async()=>{const n=await r.get(t,"GET","/api/projects");return{data:k(n,"list projects"),exit:0}});if(u==="get")return y(async()=>{const n=await r.get(t,"GET",`/api/projects/${l()}`);if(n.status===404)throw new w("project not found");return{data:n.data,exit:0}});throw f("flashstory scene list|get --project <id>")}case"entity":{const u=c[0],n=l(),a=o(s,"id"),m=o(s,"name");switch(u){case"add":return h(r,t,n,{type:"addEntity",entity:{...a?{id:a}:{},name:m??"未命名",kind:o(s,"kind")??"character",...o(s,"color")?{color:o(s,"color")}:{}}});case"remove":if(!a)throw f("--id is required");return h(r,t,n,{type:"removeEntity",id:a});case"update":{if(!a)throw f("--id is required");const x={};return m&&(x.name=m),o(s,"color")&&(x.color=o(s,"color")),o(s,"heightCm")&&(x.heightCm=Number(o(s,"heightCm"))),h(r,t,n,{type:"updateEntity",id:a,patch:x})}default:throw f("flashstory entity add|remove|update --project <id> [--id] [--name]")}}case"transform":if(c[0]!=="set"||!o(s,"id"))throw f("flashstory transform set --project --id --json <keys>");return h(r,t,l(),{type:"setTransformTrack",id:o(s,"id"),keys:g(o(s,"json"),"keys")});case"pose":{const u=c[0];if(u==="list")return y(async()=>{const n=await r.get(t,"GET",`/api/projects/${l()}`);if(n.status===404)throw new w("project not found");return{data:{customPoses:n.data.doc.customPoses??{}},exit:0}});if(u==="set-keys"){if(!o(s,"id"))throw f("--id is required");return h(r,t,l(),{type:"setPoseTrack",id:o(s,"id"),keys:g(o(s,"json"),"keys")})}throw f("flashstory pose list|set-keys --project --id --json")}case"camera":if(c[0]!=="set-keys")throw f("flashstory camera set-keys --project --json");return h(r,t,l(),{type:"setCameraTrack",keys:g(o(s,"json"),"keys")});case"settings":return h(r,t,l(),{type:"setSettings",patch:g(o(s,"json"),"patch")});case"marker":{const u=Number(o(s,"t")??NaN);if(c[0]!=="add"||!Number.isFinite(u))throw f("flashstory marker add --project --t <sec> --label <text>");return h(r,t,l(),{type:"addMarker",t:u,label:o(s,"label")??""})}case"batch":return h(r,t,l(),g(o(s,"json"),"actions"),!0);case"screenshot":return y(async()=>{const u=await r.post(t,"POST","/api/tools/screenshot",{projectId:l(),t:Number(o(s,"t")??0),camera:o(s,"camera")??"edit"}),n=k(u,"screenshot"),a=o(s,"out");if(n.kind==="image"&&a){const m=n.dataUrl?.split(",")[1];return m&&O(a,Buffer.from(m,"base64")),{data:{kind:"image",saved:a},exit:0}}return{data:n,exit:0}});case"export":{const u=c[0],n=c[1];switch(u){case"submit":return y(async()=>{const a=await r.post(t,"POST","/api/export",{projectId:l(),includeVideo:s["no-video"]!==!0,includeStills:s.stills===!0});return{data:k(a,"export submit"),exit:0}});case"status":if(!n)throw f("usage: flashstory export status <exportId>");return y(async()=>{const a=await r.get(t,"GET",`/api/export/${n}`);if(a.status===404)throw new w("export not found");return{data:a.data,exit:0}});case"result":if(!n)throw f("usage: flashstory export result <exportId> --out <dir>");return y(async()=>{const a=o(s,"out")??"exports";F(a,{recursive:!0});const x=((await r.get(t,"GET",`/api/export/${n}/files`)).data?.files??[]).filter(Boolean),$=[];for(const j of x){const E=await r.binary(t,`/api/export/${n}/files/${encodeURIComponent(j)}`);E.status===200&&(O(S(a,j),E.bytes),$.push(S(a,j)))}return{data:{exportId:n,downloaded:$},exit:0}});case"cancel":if(!n)throw f("usage: flashstory export cancel <exportId>");return y(async()=>({data:(await r.post(t,"POST",`/api/export/${n}/cancel`,{})).data,exit:0}));default:throw f("flashstory export submit|status|result|cancel")}}default:throw f(`unknown command: ${i}`)}}function f(e){return new w(e)}const L="http://127.0.0.1:8787";function U(){return S(C(),".flashstory","config.json")}function _(){try{const e=R(U(),"utf8"),t=JSON.parse(e);return typeof t.server=="string"&&t.server?t.server:void 0}catch{return}}function V(e){return e??process.env.FLASHSTORY_SERVER??_()??L}q();const v=process.argv.slice(2),p={},T=[];for(let e=0;e<v.length;e++){const t=v[e];switch(t){case"--server":p.server=v[e+1],e+=1;break;case"--port":p.port=Number(v[e+1]),e+=1;break;case"--host":p.host=v[e+1],e+=1;break;case"--data-dir":p.dataDir=v[e+1],e+=1;break;case"--open":p.open=!0;break;default:T.push(t)}}const b=T[0];if(!b||b==="serve"){const{startServer:e}=await import("./serve.js"),t={};if(p.port!==void 0&&Number.isFinite(p.port)&&(t.port=p.port),p.host&&(t.hostname=p.host),p.dataDir&&(t.dataDir=p.dataDir),await e(t),p.open){const r=`http://${t.hostname??process.env.FLASHSTORY_HOST??"127.0.0.1"}:${t.port??process.env.FLASHSTORY_PORT??8787}`;Y(r)}}else{const e=V(p.server),{data:t,exit:r}=await B(T,e);console.log(JSON.stringify(t)),process.exit(r)}function Y(e){const t=process.platform,d=P(t==="darwin"?"open":t==="win32"?"cmd":"xdg-open",t==="win32"?["/c","start","",e]:[e],{stdio:"ignore",detached:!0});d.on("error",()=>{}),d.unref?.()}
2
+ import{spawn as N}from"node:child_process";import{mkdirSync as P,writeFileSync as $,readFileSync as F}from"node:fs";import{join as j}from"node:path";import{homedir as R}from"node:os";import{l as C}from"./env.js";class w extends Error{}async function E(e,t,r,o){const c=await fetch(`${e}${r}`,{method:t,headers:o!==void 0?{"content-type":"application/json"}:void 0,body:o!==void 0?JSON.stringify(o):void 0}),u=await c.json().catch(()=>null);return{status:c.status,data:u}}async function q(e,t,r){const o=await fetch(`${e}${r}`,{method:t});return{status:o.status,text:await o.text()}}async function A(e,t){const r=await fetch(`${e}${t}`);return{status:r.status,bytes:new Uint8Array(await r.arrayBuffer())}}function v(e,t){if(e.status>=200&&e.status<300)return e.data;const r=e.data?.detail??e.data?.error??`HTTP ${e.status}`;throw new w(`${t} failed: ${r}`)}const B={get:E,post:E,binary:A,text:q};function D(e){const t=[],r={};for(let o=0;o<e.length;o++){const c=e[o];if(c.startsWith("--")){const u=c.slice(2),s=e[o+1];s!==void 0&&!s.startsWith("--")?(r[u]=s,o+=1):r[u]=!0}else t.push(c)}return{positionals:t,flags:r}}function n(e,t){const r=e[t];return typeof r=="string"?r:void 0}function G(e){const t=n(e,"project");if(!t)throw new w("--project <id> is required");return t}function g(e,t){if(!e)throw new w(`--json <${t}> is required`);try{return JSON.parse(e)}catch{throw new w(`--json <${t}> is not valid JSON`)}}async function l(e,t,r,o,c=!1){const u=await e.post(t,"POST",c?"/api/tools/batch":"/api/tools/execute",{projectId:r,...c?{actions:o}:{action:o}});if(u.status===404)return{data:{error:"project not found"},exit:1};const s=u.data;return s.ok?{data:{ok:!0,rev:s.rev},exit:0}:{data:{ok:!1,error:s.error??"rejected"},exit:1}}const h=e=>e().catch(t=>({data:{error:t instanceof Error?t.message:String(t)},exit:1}));async function H(e,t,r=B){const[o,...c]=e,{positionals:u,flags:s}=D(c),p=()=>G(s);switch(o){case"health":return h(async()=>{const d=await r.get(t,"GET","/health");return{data:v(d,"health"),exit:0}});case"scene":{const d=u[0];if(d==="list")return h(async()=>{const a=await r.get(t,"GET","/api/projects");return{data:v(a,"list projects"),exit:0}});if(d==="get")return h(async()=>{const a=await r.get(t,"GET",`/api/projects/${p()}`);if(a.status===404)throw new w("project not found");return{data:a.data,exit:0}});throw f("flashstory scene list|get --project <id>")}case"entity":{const d=u[0],a=p(),i=n(s,"id"),m=n(s,"name");switch(d){case"add":return l(r,t,a,{type:"addEntity",entity:{...i?{id:i}:{},name:m??"未命名",kind:n(s,"kind")??"character",...n(s,"color")?{color:n(s,"color")}:{}}});case"remove":if(!i)throw f("--id is required");return l(r,t,a,{type:"removeEntity",id:i});case"update":{if(!i)throw f("--id is required");const x={};return m&&(x.name=m),n(s,"color")&&(x.color=n(s,"color")),n(s,"heightCm")&&(x.heightCm=Number(n(s,"heightCm"))),l(r,t,a,{type:"updateEntity",id:i,patch:x})}default:throw f("flashstory entity add|remove|update --project <id> [--id] [--name]")}}case"transform":if(u[0]!=="set"||!n(s,"id"))throw f("flashstory transform set --project --id --json <keys>");return l(r,t,p(),{type:"setTransformTrack",id:n(s,"id"),keys:g(n(s,"json"),"keys")});case"pose":{const d=u[0];if(d==="list")return h(async()=>{const a=await r.get(t,"GET",`/api/projects/${p()}`);if(a.status===404)throw new w("project not found");return{data:{customPoses:a.data.doc.customPoses??{}},exit:0}});if(d==="set-keys"){if(!n(s,"id"))throw f("--id is required");return l(r,t,p(),{type:"setPoseTrack",id:n(s,"id"),keys:g(n(s,"json"),"keys")})}throw f("flashstory pose list|set-keys --project --id --json")}case"camera":if(u[0]!=="set-keys")throw f("flashstory camera set-keys --project --json");return l(r,t,p(),{type:"setCameraTrack",keys:g(n(s,"json"),"keys")});case"settings":return l(r,t,p(),{type:"setSettings",patch:g(n(s,"json"),"patch")});case"marker":{const d=Number(n(s,"t")??NaN);if(u[0]!=="add"||!Number.isFinite(d))throw f("flashstory marker add --project --t <sec> --label <text>");return l(r,t,p(),{type:"addMarker",t:d,label:n(s,"label")??""})}case"batch":return l(r,t,p(),g(n(s,"json"),"actions"),!0);case"screenshot":return h(async()=>{const d=await r.post(t,"POST","/api/tools/screenshot",{projectId:p(),t:Number(n(s,"t")??0),camera:n(s,"camera")??"edit"}),a=v(d,"screenshot"),i=n(s,"out");if(a.kind==="image"&&i){const m=a.dataUrl?.split(",")[1];return m&&$(i,Buffer.from(m,"base64")),{data:{kind:"image",saved:i},exit:0}}return{data:a,exit:0}});case"export":{const d=u[0],a=u[1];switch(d){case"submit":return h(async()=>{const i=await r.post(t,"POST","/api/export",{projectId:p(),includeVideo:s["no-video"]!==!0,includeStills:s.stills===!0});return{data:v(i,"export submit"),exit:0}});case"status":if(!a)throw f("usage: flashstory export status <exportId>");return h(async()=>{const i=await r.get(t,"GET",`/api/export/${a}`);if(i.status===404)throw new w("export not found");return{data:i.data,exit:0}});case"result":if(!a)throw f("usage: flashstory export result <exportId> --out <dir>");return h(async()=>{const i=n(s,"out")??"exports";P(i,{recursive:!0});const x=((await r.get(t,"GET",`/api/export/${a}/files`)).data?.files??[]).filter(Boolean),S=[];for(const k of x){const T=await r.binary(t,`/api/export/${a}/files/${encodeURIComponent(k)}`);T.status===200&&($(j(i,k),T.bytes),S.push(j(i,k)))}return{data:{exportId:a,downloaded:S},exit:0}});case"cancel":if(!a)throw f("usage: flashstory export cancel <exportId>");return h(async()=>({data:(await r.post(t,"POST",`/api/export/${a}/cancel`,{})).data,exit:0}));default:throw f("flashstory export submit|status|result|cancel")}}default:throw f(`unknown command: ${o}`)}}function f(e){return new w(e)}const J="http://127.0.0.1:8787";function L(){return j(R(),".flashstory","config.json")}function U(){try{const e=F(L(),"utf8"),t=JSON.parse(e);return typeof t.server=="string"&&t.server?t.server:void 0}catch{return}}function _(e){return e??process.env.FLASHSTORY_SERVER??U()??J}function V(e){const t={},r=[];for(let o=0;o<e.length;o++){const c=e[o];switch(c){case"--server":t.server=e[o+1],o+=1;break;case"--port":t.port=Number(e[o+1]),o+=1;break;case"--host":t.host=e[o+1],o+=1;break;case"--data-dir":t.dataDir=e[o+1],o+=1;break;case"--open":t.open=!0;break;case"--no-open":t.noOpen=!0;break;default:r.push(c)}}return{flags:t,args:r}}function Y(e){return e.noOpen===!0?!1:e.open??!0}C();const{flags:y,args:b}=V(process.argv.slice(2)),O=b[0];if(!O||O==="serve"){const{startServer:e}=await import("./serve.js"),t={};if(y.port!==void 0&&Number.isFinite(y.port)&&(t.port=y.port),y.host&&(t.hostname=y.host),y.dataDir&&(t.dataDir=y.dataDir),await e(t),Y(y)){const r=`http://${t.hostname??process.env.FLASHSTORY_HOST??"127.0.0.1"}:${t.port??process.env.FLASHSTORY_PORT??8787}/`;W(r)}}else{const e=_(y.server),{data:t,exit:r}=await H(b,e);console.log(JSON.stringify(t)),process.exit(r)}function W(e){const t=process.platform,c=N(t==="darwin"?"open":t==="win32"?"cmd":"xdg-open",t==="win32"?["/c","start","",e]:[e],{stdio:"ignore",detached:!0});c.on("error",()=>{}),c.unref?.()}
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{startServer as r}from"./serve.js";import{l as o}from"./env.js";import"@hono/node-server";import"node:module";import"hono";import"@hono/node-server/serve-static";import"node:fs";import"node:fs/promises";import"node:path";import"node:url";import"hono/streaming";import"stream";import"events";import"buffer";import"util";import"ws";import"node:os";o();await r();
1
+ import{startServer as r}from"./serve.js";import{l as o}from"./env.js";import"@hono/node-server";import"node:module";import"hono";import"@hono/node-server/serve-static";import"node:fs";import"node:fs/promises";import"node:path";import"node:url";import"node:os";import"hono/streaming";import"stream";import"events";import"buffer";import"util";import"ws";o();await r();