Compare commits

...

1677 Commits

Author SHA1 Message Date
134
e2a8f2dda3 merge: preserve server-only production updates 2026-08-11 15:47:02 +08:00
134
731aa9725a fix: preserve main behavior after Claw merge 2026-08-11 15:28:11 +08:00
134
1dd489100d feat(claw): support resumable 2GB file uploads 2026-08-11 15:13:36 +08:00
134
3f2b18d18d merge: sync Claw production updates into main
# Conflicts:
#	app/(admin)/settings/admin-settings-client.tsx
#	app/canvas/[id]/CanvasEditorLegacyPageClient.tsx
#	playwright.helpers.config.ts
2026-08-11 15:11:59 +08:00
134
398b2ec83c feat(batch): sync dedicated image tools 2026-08-11 14:05:38 +08:00
134
bbd0795fdc fix(image-tools): restore dedicated processing fallbacks 2026-08-11 13:48:33 +08:00
134
7d67bd1059 fix(claw): resume durable long-running tasks 2026-08-11 11:58:49 +08:00
134
aebc7edecb fix(claw): preserve archive preview metadata 2026-08-11 10:21:48 +08:00
134
bcff82aeca feat(claw): add secure forms and folder uploads 2026-08-11 10:16:02 +08:00
134
2fee4a4a8c fix: reconcile VIP98K media catalog 2026-08-10 23:20:54 +08:00
134
e225e06f0b fix: stabilize 3d workspace and canvas model controls 2026-08-10 20:54:23 +08:00
134
74ea0ca9fc fix(ai): remove VIP98K text-to-image models only 2026-08-10 16:31:23 +08:00
134
ab306e298b Revert "fix(ai): block costly and text-to-image VIP98K models"
This reverts commit 6b2c2c7b42d8a893b170736c06448f99aa059881.
2026-08-10 15:45:28 +08:00
root
65e9b26ad2 Merge branch 'codex/48-main-sync-20260714-150806' of http://git.1080p.hk/lunanting/ai-chatbot into codex/48-main-sync-20260714-150806 2026-08-10 14:22:27 +08:00
134
6b2c2c7b42 fix(ai): block costly and text-to-image VIP98K models 2026-08-10 14:21:18 +08:00
134
0a17a2842c fix(ai): block costly and text-to-image VIP98K models 2026-08-10 14:18:45 +08:00
134
0a1026c237 fix(ai): keep dedicated 3d provider frontend route 2026-08-09 18:46:56 +08:00
134
9438cf2f36 fix(ai): keep dedicated 3d provider frontend route 2026-08-09 18:45:09 +08:00
134
e1e7e0ccd4 fix(ai): store provider model metadata as jsonb objects 2026-08-09 18:39:07 +08:00
134
8fb4873eac fix(ai): store provider model metadata as jsonb objects 2026-08-09 18:37:05 +08:00
134
5decd202a1 fix(ai): preserve realtime canvas and normalize model types 2026-08-09 18:20:46 +08:00
134
2ad794d6e1 fix(ai): preserve realtime canvas and normalize model types 2026-08-09 18:07:01 +08:00
134
27736f0a5d fix(ai): preserve HiCAD provider integration 2026-08-09 17:30:33 +08:00
134
96c4b76250 fix(ai): preserve HiCAD provider integration 2026-08-09 17:29:01 +08:00
134
578f97b248 fix(model3d): preserve Tripo provider integration 2026-08-09 15:28:57 +08:00
134
c962fff811 fix(model3d): preserve Tripo provider integration 2026-08-09 15:26:30 +08:00
134
4c6ce023eb fix(ai): add VIP98K expansion fallback 2026-08-08 17:57:30 +08:00
134
658c3345f3 fix(ai): add VIP98K expansion fallback 2026-08-08 17:56:18 +08:00
134
4f43c7a8c7 fix(ai): honor VIP98K invocation contracts 2026-08-08 17:35:47 +08:00
134
93158e0b7b fix(ai): honor VIP98K invocation contracts 2026-08-08 17:33:44 +08:00
134
a326cf6935 fix(ai): route VIP98K reference edits through task API 2026-08-08 17:21:03 +08:00
134
2676d9a96f fix(ai): route VIP98K reference edits through task API 2026-08-08 17:18:55 +08:00
134
82088274be fix(ai): allow VIP98K catalog in server scripts 2026-08-08 17:02:15 +08:00
134
f9ded76a38 fix(ai): allow VIP98K catalog in server scripts 2026-08-08 16:59:25 +08:00
134
4a1536515e feat(ai): consolidate providers around VIP98K and FAL 2026-08-08 16:52:14 +08:00
134
725a93331a feat(ai): add VIP98K provider foundation on 48 baseline 2026-08-08 16:52:14 +08:00
134
f414150e73 feat(ai): consolidate providers around VIP98K and FAL 2026-08-08 16:40:18 +08:00
134
311d4a0ec3 feat(ai): add VIP98K provider foundation on 48 baseline 2026-08-08 16:40:01 +08:00
134
30b7ae168f fix(claw): use static thirty-minute route duration 2026-08-07 15:24:17 +08:00
134
54cd02d09d feat: improve Claw workflows and reliability 2026-08-07 14:52:17 +08:00
134
f1b769490a fix(claw): allow ten-minute chat tasks 2026-08-07 12:12:11 +08:00
root
7708b66e8d backup: preserve server changes before OpenClaw deploy 2026-08-07 11:43:46 +08:00
134
2f00645cd4 feat: add user-scoped OpenClaw MCP access 2026-08-07 11:31:00 +08:00
134
a68f9832ed fix(claw): persist and recover background chat tasks 2026-08-06 12:58:01 +08:00
134
c2fcf11280 fix(canvas): raise Claw skill tag limit 2026-08-05 21:36:11 +08:00
134
ee5b5d146d fix(canvas): dedupe Claw table images 2026-08-05 19:18:51 +08:00
134
c6c7c703b5 fix(canvas): handle expired sessions for Claw skills 2026-08-05 00:14:06 +08:00
134
233c7b3308 feat: add Claw skill search 2026-08-05 00:04:36 +08:00
134
d467694aa7 style: lighten homepage promo banner text 2026-08-04 23:38:09 +08:00
134
98c8dd20ee feat: add configurable homepage promo banner 2026-08-04 21:41:38 +08:00
134
31567af44e feat(canvas): allow resizing model selector 2026-08-04 21:41:00 +08:00
root
d3a6497672 fix: normalize WorldLabs VIP98K endpoint 2026-08-04 21:39:59 +08:00
134
e3180c23ac fix: persist Claw spreadsheet attachments 2026-07-28 21:12:18 +08:00
134
fb8d8232dc feat: support video attachments in Claw chat 2026-07-28 15:50:47 +08:00
134
21ba23c4ab fix: reconcile HiCAD manufacturing geometry 2026-07-28 14:26:05 +08:00
134
07aef767b9 fix: preserve HiCAD hole annotations 2026-07-28 13:53:07 +08:00
134
a2c9404a3d fix: stabilize HiCAD manufacturing dimensions 2026-07-28 13:53:07 +08:00
134
0397bf3a88 fix: correct HiCAD BOM geometry 2026-07-28 10:42:30 +08:00
134
8e89bb179e feat: add HiCAD editor project shortcuts 2026-07-27 23:55:51 +08:00
134
016076015e feat: add Claw product lifecycle shortcuts 2026-07-27 23:17:48 +08:00
134
43fd3d258a feat: simplify HiCAD project management 2026-07-27 22:58:10 +08:00
134
1a9888d825 fix: harden canvas image generation flows
(cherry picked from commit 78b0da908c4a0c22bd7eb66b5a661c07fcecb4fc)
2026-07-27 14:41:08 +00:00
134
2c81486862 feat: generate complete front and side part drawings 2026-07-27 22:36:42 +08:00
134
b5b5398b30 fix: render HiCAD exploded fallback and reposition trigger 2026-07-27 22:31:57 +08:00
134
cc113155dd fix: compact HiCAD engineering sheet header 2026-07-27 22:25:51 +08:00
134
5565829c65 test: validate HiCAD hardware production drawings 2026-07-27 22:20:56 +08:00
134
d84f3dc7d8 feat: add HiCAD hole dimension production drawings 2026-07-27 22:04:42 +08:00
134
e40c351f80 fix: render HiCAD orthographic views and Chinese BOM names 2026-07-27 21:48:02 +08:00
134
dc7f58d940 fix: correct HiCAD exploded previews and BOM templates 2026-07-27 21:37:19 +08:00
134
46f1d97593 fix: make HiCAD engineering analysis resilient 2026-07-27 21:06:25 +08:00
134
3b3f9e71ed fix: normalize HiCAD engine responses 2026-07-27 20:54:59 +08:00
134
9c9e462682 feat: restore isolated HiCAD engineering tools 2026-07-27 20:46:18 +08:00
134
4c21fd4844 fix: exclude HiCAD templates from app typecheck 2026-07-27 20:34:20 +08:00
134
623ab0f8b5 fix: isolate latest HiCAD startup 2026-07-27 20:24:53 +08:00
134
bbe27bd555 fix: isolate HiCAD manufacturing extension 2026-07-27 20:09:39 +08:00
134
d93b2e8ea7 feat: brand HiCAD production sheets 2026-07-27 19:58:35 +08:00
134
7435e7b0f0 feat: add HiCAD manufacturing drawings 2026-07-27 19:58:35 +08:00
134
08d2503e11 fix: use site favicon in HiCAD 2026-07-27 19:42:35 +08:00
134
240352362a feat: upgrade embedded HiCAD to latest official source 2026-07-27 19:33:59 +08:00
134
f65b9e0472 fix: frame complete HiCAD models after generation 2026-07-27 19:20:57 +08:00
134
11deb3bba6 perf: streamline HiCAD startup 2026-07-27 18:24:43 +08:00
134
de8b7670b2 fix: support Claw AVIF and TIFF uploads 2026-07-27 17:30:42 +08:00
134
80d05e177a fix: keep sent Claw messages out of composer 2026-07-27 10:22:49 +08:00
134
c889bfc381 feat: add product planning v2 claw skill 2026-07-24 14:10:53 +08:00
134
62c686a26b fix: fail over on upstream image provider errors 2026-07-24 13:51:35 +08:00
134
3096eb6752 feat: add AtlasCloud video model support 2026-07-23 10:52:29 +08:00
134
974a4b1b59 chore: rename hidden model control 2026-07-22 11:42:39 +08:00
134
6ccf04a786 fix: preserve AtlasCloud model aliases 2026-07-22 10:53:19 +08:00
134
1a91b8ada9 fix: hide stale AtlasCloud catalog entries 2026-07-22 10:35:05 +08:00
134
75cecdfcad fix: validate AtlasCloud frontend model requests 2026-07-22 10:32:28 +08:00
134
758be188cf test: preserve image model classification coverage 2026-07-21 12:00:25 +08:00
134
fd47e0ae61 fix: classify media models and open canvas chat 2026-07-21 12:00:09 +08:00
134
e8d1aa25aa feat: add Atlas Cloud image provider 2026-07-21 11:08:13 +08:00
134
31cc75823c chore: ignore staged production builds 2026-07-15 11:35:24 +08:00
134
3c1b84ccd0 fix: prevent white screens during production rebuilds 2026-07-15 11:26:41 +08:00
134
abc6a7266a fix: show canvas enhance processing placeholder 2026-07-14 16:15:49 +08:00
134
1fd9be49c7 fix: route canvas capabilities through local runtime 2026-07-14 16:01:44 +08:00
134
452effecbc fix: classify reference image models correctly 2026-07-14 15:26:46 +08:00
134
f52ab780f1 fix(canvas): render generated image thumbnails first 2026-07-14 14:22:55 +08:00
134
80e9771790 feat: add Visionary image provider 2026-07-14 14:12:27 +08:00
134
1b70c1d678 feat(canvas): add capability runtime and MCP tools 2026-07-14 12:51:10 +08:00
134
5a7121dceb fix: handle complex canvas scale adjustments 2026-07-14 09:03:14 +08:00
134
ede6f080a2 feat: add BananaImage image provider 2026-07-13 23:04:54 +08:00
134
5a16072a3d fix: avoid IPv6 retries for Zoo websockets 2026-07-13 21:08:06 +08:00
134
8e88f02214 fix: keep CAD stream aligned with viewport 2026-07-13 20:44:24 +08:00
134
a9390f0abc fix: keep generated CAD models in frame 2026-07-13 19:47:20 +08:00
134
43837e5048 feat: add canvas product position adjustment 2026-07-13 19:12:53 +08:00
134
8119219c15 backup: preserve verified canvas scale adjustment 2026-07-13 19:12:53 +08:00
134
752d1f99d2 refactor: remove legacy AI-CAD workbench 2026-07-13 19:00:52 +08:00
134
08d8a1d4af fix: version the complete HiCAD asset graph 2026-07-13 18:44:03 +08:00
134
beb6ff4f04 fix: enforce read-only Pascal agent requests 2026-07-13 18:43:19 +08:00
134
42bd2827a6 fix: version HiCAD entry assets 2026-07-13 18:38:46 +08:00
134
2484b5e5a4 fix: open HiCAD as top-level app 2026-07-13 18:21:49 +08:00
134
c6cabe5296 fix: keep Pascal agent running in background 2026-07-13 18:14:23 +08:00
134
59c57e7cf8 perf: precompress HiCAD frontend assets 2026-07-13 17:33:21 +08:00
134
6ca4239f16 fix: resolve Webpack aliases across Bun installs 2026-07-13 17:14:19 +08:00
134
ea27d83ea9 fix: dedupe React Three Fiber in Webpack build 2026-07-13 17:08:31 +08:00
134
00cbfce56d fix: translate HiCAD input and spaced copy 2026-07-13 17:07:30 +08:00
134
f29433a91b fix: prevent stale Pascal chunk failures 2026-07-13 16:49:05 +08:00
134
392c5b2a00 feat: complete HiCAD Chinese welcome copy 2026-07-13 16:46:06 +08:00
134
46e5135592 feat: localize and brand HiCAD workspace 2026-07-13 16:20:30 +08:00
134
ae76c45e27 fix: dismiss HiCAD loading state reliably 2026-07-13 15:55:22 +08:00
134
e308142afd fix: serve versioned HiCAD assets directly 2026-07-13 15:43:19 +08:00
134
96f2999d66 fix: resolve embedded HiCAD websocket URLs 2026-07-13 15:25:28 +08:00
134
663cb7c67a fix: resolve Pascal editor icons under base path 2026-07-13 15:17:53 +08:00
134
9cf84c324e feat: replace ai-cad with official HiCAD workspace 2026-07-13 14:56:27 +08:00
134
07ae16d0ed fix: resolve Pascal HDR assets under base path 2026-07-13 14:53:45 +08:00
134
62a6a4a7f0 fix: use WebGL fallback for Safari scenes 2026-07-13 14:31:23 +08:00
134
47067bf278 fix: serve Pascal static assets without session auth 2026-07-13 13:04:26 +08:00
134
cb47ebf568 fix: restore ai-cad model outputs 2026-07-13 12:00:53 +08:00
134
aedf590e1d fix: prevent Pascal chunk load failures 2026-07-13 11:42:04 +08:00
134
7ccbc87afd test: cover Pascal scene rebuild state 2026-07-13 11:05:52 +08:00
134
f832e08293 fix: restore Pascal 3D navigation and walls 2026-07-13 10:55:31 +08:00
134
603163d6ec fix: keep Pascal agent progress visible 2026-07-13 10:09:55 +08:00
134
e820df7eb6 fix: refresh Pascal scene interactions live 2026-07-13 03:57:43 +08:00
134
64dec38ff3 feat: persist Pascal agent conversations 2026-07-13 03:12:17 +08:00
134
cf1091fc2c fix: increase Pascal reasoning output budget 2026-07-12 10:49:18 +08:00
134
4f5dbb4589 fix: continue Pascal agents until a scene write 2026-07-12 10:43:33 +08:00
134
cc6c4b547c fix: bootstrap empty Pascal scenes for agents 2026-07-12 10:39:05 +08:00
134
16bfd0bd38 fix: constrain Pascal tools after initial inspection 2026-07-12 10:35:55 +08:00
134
ab657330ea fix: require Pascal design agents to mutate scenes 2026-07-12 10:32:20 +08:00
134
3d35858c05 fix: resolve Pascal toolbar icons under base path 2026-07-12 10:23:21 +08:00
134
4c1b97f210 fix: allow proxied Pascal scene creation 2026-07-12 08:48:25 +08:00
134
98dddbcc2e fix: keep Pascal agent bound to active scene 2026-07-12 01:13:45 +08:00
134
28c817429a fix: preserve completed Pascal agent streams 2026-07-12 01:08:45 +08:00
134
9efb9e8237 fix: persist Pascal agent scene after live updates 2026-07-12 01:04:23 +08:00
134
5260502507 feat: deploy Pascal from vendored source 2026-07-12 00:55:11 +08:00
134
40ff3ca851 Merge commit 'c3faeba1c8c1275c156c8739b50b8f0ca9425fd3' as 'services/pascal-editor' 2026-07-12 00:43:59 +08:00
134
c3faeba1c8 Squashed 'services/pascal-editor/' content from commit 312064e6
git-subtree-dir: services/pascal-editor
git-subtree-split: 312064e6e8122f958d2961e28ea85ad8b9db40c5
2026-07-12 00:43:59 +08:00
134
4bf6ec2b38 fix: install build dependencies during deployment 2026-07-12 00:26:10 +08:00
134
279121615b feat: add configurable scene sidebar entry 2026-07-12 00:22:45 +08:00
134
73e00b4d6a fix: default Pascal smoke base path 2026-07-11 23:14:03 +08:00
134
22069ada66 fix: skip incompatible Pascal model providers 2026-07-11 23:10:44 +08:00
134
7bb1f06b11 fix: route Pascal through multimodal model 2026-07-11 22:59:56 +08:00
134
af3d17f4a2 feat: add repeatable Pascal production deployment 2026-07-11 22:43:32 +08:00
134
12ae15ed3a feat: integrate authenticated Pascal service 2026-07-11 16:24:42 +08:00
134
5636b2789e fix: unify settings dark mode and layout 2026-07-10 22:20:28 +08:00
134
2ce64dd49b merge: integrate missing canvas and UI updates 2026-07-10 21:50:29 +08:00
134
363bd1d2fe fix: keep uploaded image attachments visible 2026-07-10 21:23:22 +08:00
134
748273d917 feat: defer generated image archiving 2026-07-10 20:27:09 +08:00
134
103ca18031 perf: trim canvas first-open bundles 2026-07-10 18:52:41 +08:00
134
78f14edbfc perf: lazy load canvas 3d renderers 2026-07-10 18:33:49 +08:00
134
6c83826e1d fix: improve analytics dark mode 2026-07-10 18:16:12 +08:00
134
b50d3eb295 perf: speed up canvas chat entry 2026-07-10 18:09:49 +08:00
134
0d67e2935c feat: update video canvas frontend workflow 2026-07-10 17:59:12 +08:00
134
988e1fefed fix: preserve canvas image context and compact overlays 2026-07-10 14:49:45 +08:00
134
c044ee2c40 fix: prevent local fallbacks in production 2026-07-10 14:10:40 +08:00
134
04c08ff256 fix: keep local test models out of production 2026-07-10 13:55:53 +08:00
134
eca1c70edb fix: route internal access checks over local HTTP 2026-07-10 13:31:56 +08:00
134
48134a1bbe merge: integrate canvas and UI refinements
# Conflicts:
#	app/(chat)/api/files/direct-upload/route.ts
#	app/canvas/[id]/CanvasEditorLegacyPageClient.tsx
#	components/canvas/CanvasChatPanel.tsx
#	components/home/lovart-home.tsx
#	components/home/lovart/lovart-featured-gallery.tsx
#	components/home/lovart/lovart-hero-composer.tsx
#	components/home/lovart/lovart-projects-section.tsx
#	components/home/lovart/lovart-sidebar.tsx
#	components/layout/brand-top-bar.tsx
2026-07-10 13:18:21 +08:00
134
3252cbd2c8 feat: refine canvas workflows and unify application UI 2026-07-10 12:57:01 +08:00
Codex
c52cf9aa90 Apply canvas scale adjust rewrite 2026-07-09 17:25:40 +08:00
134
adbbe4442a Use canvas default for scale adjust model fallback 2026-07-09 15:42:33 +08:00
134
65f5a13f13 Rewrite canvas scale adjust local edit 2026-07-09 14:47:18 +08:00
root
8151e57661 Merge remote-tracking branch 'origin/main' into codex/scale-adjust-main-merge 2026-07-09 14:42:51 +08:00
134
a5995ca0c4 fix: keep image attachment add button visible 2026-07-09 14:41:51 +08:00
134
72b1f8f1cf fix: keep image attachment add button visible 2026-07-09 14:39:38 +08:00
134
c9ed662809 Fix canvas scale adjust local edit build 2026-07-09 12:00:42 +08:00
root
22f314f6ad Merge remote-tracking branch 'origin/main' into codex/scale-adjust-main-merge 2026-07-08 22:43:53 +08:00
134
2c318360b3 fix: avoid route refresh when switching claw history 2026-07-08 22:43:03 +08:00
134
accaaad8f1 fix: avoid route refresh when switching claw history 2026-07-08 22:42:41 +08:00
root
2398930ecb Merge remote-tracking branch 'origin/main' into codex/scale-adjust-main-merge 2026-07-08 22:16:02 +08:00
134
ea3ee3a5d7 feat: allow canceling claw submissions 2026-07-08 22:14:47 +08:00
134
e1f514536b feat: allow canceling claw submissions 2026-07-08 22:14:20 +08:00
134
b49519ec56 improve direct upload stall feedback 2026-07-08 21:28:43 +08:00
134
9575873efb speed up direct upload preparation 2026-07-08 21:18:01 +08:00
134
81a0b29633 timeout direct upload control requests 2026-07-08 21:08:02 +08:00
134
6fe69de8de fix: upload fal edit inputs to fal storage 2026-07-08 20:56:03 +08:00
134
2b875847dd close upload progress before image sizing 2026-07-08 20:54:00 +08:00
134
3aa031a4e4 fix: upload normalized source for fal scale edit 2026-07-08 20:41:19 +08:00
134
d00eb6d0e4 keep upload progress above canvas toolbar 2026-07-08 20:35:06 +08:00
134
ff71e3935c fix: reject scale adjust old footprint remnants 2026-07-08 20:33:10 +08:00
134
7ed0f08269 polish canvas toolbar and upload stall handling 2026-07-08 20:29:46 +08:00
134
05b21707c1 bypass proxy for direct storage images 2026-07-08 20:16:48 +08:00
134
7ac4a25a5f fix: honor requested fal scale adjust model 2026-07-08 20:13:36 +08:00
134
8acf1f60ec show canvas reference upload progress 2026-07-08 20:04:46 +08:00
134
66cdf6d3d1 fix safari direct upload feedback 2026-07-08 19:39:10 +08:00
134
91ce0ff5f8 show image upload progress 2026-07-08 19:26:45 +08:00
134
607491fc9e standardize direct storage uploads 2026-07-08 18:52:50 +08:00
134
fef8947e73 fix agent user-facing image edit preface 2026-07-08 15:52:21 +08:00
134
fd5b483bad fix agent user-facing image edit preface 2026-07-08 15:51:08 +08:00
134
778fc1c0b8 preserve colorful brand logos 2026-07-08 15:23:05 +08:00
134
2bbdd0aa61 preserve colorful brand logos 2026-07-08 15:22:17 +08:00
134
d48e26fe9a allow agent multi image generation 2026-07-08 14:48:29 +08:00
134
77531aa7b5 improve agent planning brief 2026-07-08 14:48:29 +08:00
134
599abc5021 allow agent multi image generation 2026-07-08 14:37:07 +08:00
134
e0c277c76a improve agent planning brief 2026-07-08 14:23:38 +08:00
134
d69346de42 fix: keep product out of scale repair crop prompt 2026-07-08 01:38:51 +08:00
134
2cb6f51a63 fix: repair product locked scale adjust in crop workspace 2026-07-08 01:23:46 +08:00
134
8fad43c597 fix: refine scale adjust product lock repair mask 2026-07-08 01:11:14 +08:00
134
0add599427 fix: use alpha channel for product lock repair mask 2026-07-08 00:55:36 +08:00
134
e67aab87c5 fix: rebase product lock scale composite on source 2026-07-08 00:37:28 +08:00
134
67310e9ae5 fix: timeout scale adjust prep uploads 2026-07-08 00:25:48 +08:00
134
7b73a9c7f5 fix: constrain product lock scale repair to source footprint 2026-07-08 00:09:11 +08:00
134
17f5485522 fix: harden scale adjust product lock uploads 2026-07-07 23:40:34 +08:00
134
30eef41dc4 fix: restore longer s3 upload timeout 2026-07-07 23:09:38 +08:00
134
0405113d52 fix: stabilize scale adjust fal timeout and uploads 2026-07-07 22:53:09 +08:00
134
134411f7f8 perf: improve home and canvas initial load 2026-07-07 22:29:04 +08:00
root
6b25171ce1 Merge remote-tracking branch 'origin/main' into codex/scale-adjust-main-merge 2026-07-07 22:28:09 +08:00
134
6c89df0f8b feat: add global claw chat history 2026-07-07 22:26:47 +08:00
134
ebfc7589a9 feat: add global claw chat history 2026-07-07 22:26:04 +08:00
134
6bc43ffc3f perf: improve home and canvas initial load 2026-07-07 22:16:07 +08:00
134
f692425607 fix: persist claw chat sessions 2026-07-07 22:14:06 +08:00
134
a03ea2cb47 fix: persist claw chat sessions 2026-07-07 22:13:39 +08:00
134
16f23b3f7c fix canvas cutout intent routing 2026-07-07 21:39:56 +08:00
134
51283b81fb fix canvas cutout intent routing 2026-07-07 21:38:24 +08:00
134
dab44f3825 fix canvas agent duplicate user message 2026-07-07 21:21:46 +08:00
134
e2e373ce08 fix canvas agent duplicate user message 2026-07-07 21:21:12 +08:00
134
574c259812 fix canvas agent image generation status 2026-07-07 21:08:18 +08:00
134
4ac4ac26cf fix canvas agent image generation status 2026-07-07 21:07:18 +08:00
134
654829c4fd fix: restore canvas pending feedback and scoped capabilities 2026-07-07 20:36:52 +08:00
134
5923149dd6 fix: restore canvas pending feedback and scoped capabilities 2026-07-07 20:36:18 +08:00
134
f7676716bb fix: keep canvas user message visible while pending 2026-07-07 16:55:29 +08:00
134
2e4cb0cf73 fix: keep canvas user message visible while pending 2026-07-07 16:54:43 +08:00
134
b17779e034 fix: stabilize recent dialogue memory typing 2026-07-07 16:33:21 +08:00
134
8c719c44ac fix: stabilize recent dialogue memory typing 2026-07-07 16:33:03 +08:00
134
c17de9def5 fix: remove agent progress bubble and add dialogue memory 2026-07-07 16:30:56 +08:00
134
8c159a3828 fix: remove agent progress bubble and add dialogue memory 2026-07-07 16:30:03 +08:00
134
6a2b928080 fix: resolve unselected canvas image targets 2026-07-07 16:03:15 +08:00
134
4b9f3a25c9 fix: resolve unselected canvas image targets 2026-07-07 16:02:33 +08:00
134
eab3f0372f feat: add canvas agent progress timeline 2026-07-07 15:43:44 +08:00
134
f88ada378e feat: add canvas agent progress timeline 2026-07-07 15:42:54 +08:00
134
8f679a100c feat: add canvas agent orchestration kernel 2026-07-07 15:19:45 +08:00
134
610d2aed9b feat: add canvas agent orchestration kernel 2026-07-07 15:17:43 +08:00
134
7e1fd15ebd fix: stabilize canvas agent chat feedback 2026-07-07 14:55:24 +08:00
134
15f9e1910a fix: stabilize canvas agent chat feedback 2026-07-07 14:54:58 +08:00
134
2d6289f0fa fix: add agent preface before generation 2026-07-07 14:39:02 +08:00
134
ee7d104734 fix: add agent preface before generation 2026-07-07 14:38:26 +08:00
134
1781977486 fix: keep canvas chat messages ordered 2026-07-07 14:28:36 +08:00
134
57a4840df2 fix: keep canvas chat messages ordered 2026-07-07 14:27:48 +08:00
134
690d04ec71 feat: add downloads for style pro and ai wash results 2026-07-07 14:16:09 +08:00
134
77174f4e21 feat: add downloads for style pro and ai wash results 2026-07-07 14:12:18 +08:00
134
819bb92488 fix: use product lock draft as fal repair input 2026-07-06 06:12:27 +08:00
134
d94f6dea8c fix: send opaque scale adjust masks to fal 2026-07-06 05:54:52 +08:00
134
aad838a8e0 fix: guide product lock background repair with tight mask 2026-07-06 05:29:57 +08:00
134
00a6ac08df fix: tighten product lock scale adjust clip mask 2026-07-06 05:11:23 +08:00
134
fb9f0541fa fix: restrict scale adjust mask failover 2026-07-06 04:49:56 +08:00
134
831ce10a5b fix: use mask edit for product lock scale adjust 2026-07-06 04:28:56 +08:00
134
c79da3e1d0 fix: preserve single-channel scale adjust masks after blur 2026-07-06 03:41:56 +08:00
134
7640e6b5ca fix: clean product lock masks before scale adjust overlay 2026-07-06 03:22:59 +08:00
134
6694846c58 fix: harden scale adjust person occlusion masks 2026-07-06 03:00:28 +08:00
134
f7a5accb61 fix: protect people in product locked scale adjust 2026-07-06 02:29:49 +08:00
134
f6faf6bb44 fix: constrain product lock scale adjust to source patch 2026-07-06 01:30:01 +08:00
134
26aa4fb12f fix: lock scale adjust product over repaired background 2026-07-06 01:08:37 +08:00
134
deee79c5c2 fix: avoid compositing dirty full-frame scale drafts 2026-07-05 22:53:11 +08:00
134
fa5212a33e fix: keep furniture scale drafts mask based 2026-07-05 22:25:56 +08:00
134
14e75197cf fix: composite full-frame scale edits 2026-07-05 21:52:48 +08:00
134
c3d8c5d56c fix: keep scale guide source-first 2026-07-05 14:44:34 +08:00
134
c0f376c4cf fix: erase old footprint in scale guide 2026-07-05 14:23:41 +08:00
134
28080924f8 fix: use source plate for scale target guide 2026-07-05 14:07:49 +08:00
134
0f27bf1b10 fix: clear protected side from scale drafts 2026-07-05 13:34:54 +08:00
134
acdc197b7e fix: fall back when scale reference upload fails 2026-07-05 13:17:03 +08:00
134
20693db8b9 fix: aggressively clear protected draft dark residue 2026-07-05 12:52:44 +08:00
134
729ccec2f8 fix: clear dark artifacts from protected scale drafts 2026-07-05 12:39:07 +08:00
134
5262b085c2 fix: suppress protected-side scale draft artifacts 2026-07-05 12:26:47 +08:00
134
f16c54c3a9 fix: avoid protected person mask rectangle fill 2026-07-05 11:53:40 +08:00
134
4ab60cc7a3 fix: ignore protected person direction in scale drafts 2026-07-05 11:42:50 +08:00
134
c9b6df55b0 fix: reduce scale adjust reference uploads 2026-07-05 11:19:24 +08:00
134
a3a69fed43 fix: use full frame draft for furniture scale adjust 2026-07-05 11:09:07 +08:00
134
dde159ea7c fix: tighten scale adjust patch compositing 2026-07-04 22:06:35 +08:00
134
290f36b44e fix: timeout stalled canvas scale providers 2026-07-04 21:48:09 +08:00
134
8e2c6fea1b fix: timeout stalled s3 uploads 2026-07-04 20:20:21 +08:00
134
4d4507489f fix: use fal edit as final scale adjust failover 2026-07-04 20:05:31 +08:00
134
d724a542bb fix: avoid failed providers during canvas image failover 2026-07-04 19:33:18 +08:00
134
2ae5ceb234 fix: preserve furniture component when clipping protected people 2026-07-04 19:18:23 +08:00
134
19c4367fb8 fix: clip protected person pixels without dropping scale target 2026-07-04 19:05:52 +08:00
134
fa7d9d81ff fix: exclude protected people from furniture scale masks 2026-07-04 18:58:30 +08:00
134
970bd89638 fix: allow scale adjust provider failover 2026-07-04 18:38:27 +08:00
134
739dc091ab fix: prefer pixel structure for furniture scale edits 2026-07-04 18:25:11 +08:00
134
50a59b82af fix: clip scale targets by subject alpha 2026-07-04 18:08:52 +08:00
134
9ee2d2b50b fix: clip scale adjust patch composites 2026-07-04 17:13:39 +08:00
134
40fa7b1e26 fix: filter scale adjust haze in patch composites 2026-07-04 16:46:04 +08:00
134
1a60671255 fix: retry image downloads for scale composites 2026-07-04 16:21:46 +08:00
134
28d73fd042 fix: erase old scale footprint in structure drafts 2026-07-04 15:58:43 +08:00
134
fb11a4becd fix: stabilize snapgen reference downloads 2026-07-04 15:38:51 +08:00
134
fec71038c5 fix: harden scale adjust patch workflow 2026-07-04 15:26:31 +08:00
134
f483fab33a fix: force shrink scale edits through locked patch 2026-07-04 14:11:49 +08:00
134
38961de760 fix: simplify fal scale patch prompt 2026-07-04 13:58:41 +08:00
134
56d319af66 fix: force fal scale shrink through locked patch workflow 2026-07-04 13:43:55 +08:00
134
77b25662b4 fix: send layout drafts to fal scale edits 2026-07-04 13:40:02 +08:00
134
83e911f698 fix: stabilize scale adjust subject resizing
# Conflicts:
#	app/api/canvas/local-edit/route.ts
#	lib/canvas/generate-image.ts
2026-07-04 13:26:58 +08:00
134
2a8b303508 fix: route scale shrink through fal edit fallback 2026-07-04 13:23:05 +08:00
134
2e167d38f0 fix: prefer grsai for scale shrink patch 2026-07-04 13:05:25 +08:00
134
254bd08345 fix: avoid degraded mask models for scale shrink 2026-07-04 12:48:48 +08:00
134
87f85e78c5 fix: prefer mask edit for scale shrink 2026-07-04 12:36:32 +08:00
134
04f440574b fix: use cutout drafts for scale shrink 2026-07-04 12:21:17 +08:00
134
392a03a2ed fix: strengthen scale shrink erase guide 2026-07-04 12:11:59 +08:00
134
ae176cba44 fix: timeout remote image downloads 2026-07-04 12:02:29 +08:00
134
d8e2d65186 fix: soften scale shrink layout wipe 2026-07-04 11:47:52 +08:00
134
360cba0873 fix: avoid false seam rejection on scale shrink 2026-07-04 11:26:29 +08:00
134
6baeb57cde fix: accept visual shrink scale edits 2026-07-04 11:06:21 +08:00
134
48a0f2ee73 fix: use structure locked drafts for scale shrink 2026-07-04 10:44:45 +08:00
134
53be242e89 fix: clamp scale patch alpha to edit regions 2026-07-03 23:19:17 +08:00
134
b9e1671777 fix: apply scale diff masks as alpha 2026-07-03 22:44:18 +08:00
134
b4db755c45 fix: composite scale patches with diff masks 2026-07-03 22:33:27 +08:00
134
026164f86d fix: ignore top background sheets in scale masks 2026-07-03 22:07:46 +08:00
134
bb78cb5d4a fix: disable striped rmbg scale drafts by default 2026-07-03 22:01:35 +08:00
134
88878c3307 fix: refine rmbg alpha for scale drafts 2026-07-03 21:57:26 +08:00
134
5ec43fa8a3 fix: reject banded rmbg scale drafts 2026-07-03 21:50:34 +08:00
134
01e17361c8 fix: avoid rectangular region composite for scale edits 2026-07-03 21:20:53 +08:00
134
0a591456c4 fix: limit scale patch composite to edit regions 2026-07-03 20:58:30 +08:00
134
5a4e087b3d fix: preserve source crop for structure scale edits 2026-07-03 20:35:52 +08:00
134
8ecece64e1 fix: filter cutout matte artifacts 2026-07-03 20:20:54 +08:00
134
2875b02d2a fix: denoise cutout alpha masks 2026-07-03 20:10:52 +08:00
134
27b0098bf9 fix: clean cutout alpha for scale drafts 2026-07-03 20:02:00 +08:00
134
ea52d65971 fix: use cutout drafts for scale adjustment 2026-07-03 19:47:54 +08:00
134
280c356ed2 Revert "fix: allow scale adjust target people"
This reverts commit 4db7cc9ada5b7365f41c746d9847f449ddcc193d.
2026-07-03 18:23:10 +08:00
134
ce4e056f20 fix: improve structure locked scale drafts 2026-07-03 18:12:01 +08:00
134
4db7cc9ada fix: allow scale adjust target people 2026-07-03 17:48:03 +08:00
134
d3cfb4a6b7 fix: allow scale adjust target people 2026-07-03 17:47:34 +08:00
134
dc499d0d6f fix: expose detail scale adjust entry 2026-07-03 17:26:21 +08:00
134
7e98f42462 fix: expose detail scale adjust entry 2026-07-03 17:26:00 +08:00
134
d058ff32e6 fix: support scale adjust provider mask params 2026-07-03 14:04:49 +08:00
134
84d843a8a4 fix: lock scale adjust enlargement structure 2026-07-03 13:59:14 +08:00
134
cf85caeb5a fix: lock scale adjust enlargement structure 2026-07-03 13:55:53 +08:00
134
39ee88bd12 fix: keep enlargement patch single-reference by default 2026-07-03 10:53:40 +08:00
134
97720a55e5 fix: honor requested model for scale enlargement 2026-07-03 10:38:04 +08:00
134
e0e57c6865 fix: preserve people during scale enlargement 2026-07-03 10:24:32 +08:00
134
18035d4272 fix: reorder skill tags while dragging 2026-07-03 00:42:09 +08:00
134
4f5a986162 fix: reorder skill tags while dragging 2026-07-03 00:41:38 +08:00
134
273379b18d fix: keep agent text image routing safe 2026-07-03 00:37:25 +08:00
134
ec1bdb7396 fix: keep agent text image routing safe 2026-07-03 00:36:54 +08:00
134
c698901509 feat: persist claw skill tag order 2026-07-03 00:19:27 +08:00
134
469ae7b02d feat: persist claw skill tag order 2026-07-03 00:18:52 +08:00
134
b21b4cdea0 fix: constrain claw table images 2026-07-03 00:02:31 +08:00
134
c789dd7a69 fix: constrain claw table images 2026-07-03 00:01:47 +08:00
134
2ca0c54de2 fix: accept visible scale shrink footprints 2026-07-02 23:04:43 +08:00
134
8c0d4a0930 fix: preload canvas project on route load 2026-07-02 22:41:02 +08:00
134
c4e4981a24 fix: preload canvas project on route load 2026-07-02 22:40:19 +08:00
134
c38003a382 fix: allow scale shrink footprint tolerance 2026-07-02 22:40:04 +08:00
134
0bab5d1c8e fix: prefer gpt image model for scale adjust 2026-07-02 22:30:12 +08:00
134
da7fbb1284 fix: send only target draft for scale patch 2026-07-02 22:12:18 +08:00
134
0904fdf98a fix: avoid source crop reversion in scale adjust 2026-07-02 22:01:03 +08:00
134
d1231365d0 fix: refine claw markdown composer display 2026-07-02 21:59:41 +08:00
134
71fd765ebc fix: refine claw markdown composer display 2026-07-02 21:59:12 +08:00
134
ed63eb77b7 fix: prioritize scale adjust target draft 2026-07-02 21:46:11 +08:00
134
90a9edcd56 fix: edit scale adjust through ai patch workspace 2026-07-02 21:31:32 +08:00
134
89fca5d654 fix: avoid visual terms in alibaba scale prompt 2026-07-02 21:06:20 +08:00
134
11e6aca724 fix: use text coordinate guide for alibaba scale adjust 2026-07-02 20:57:00 +08:00
134
56240d4a6d fix: use marked guide for alibaba scale adjust 2026-07-02 20:44:14 +08:00
134
7c8ae2fe8b fix: honor requested scale adjust model 2026-07-02 20:31:13 +08:00
134
2f312a01df fix: support mask guided scale adjust 2026-07-02 20:20:01 +08:00
134
7152435a3d fix: add focused crop reference for scale adjust 2026-07-02 19:54:33 +08:00
134
98e987e75b fix: focus scale adjust subject guide 2026-07-02 19:38:24 +08:00
134
956f9e0fed fix: guide scale adjust with layout draft 2026-07-02 18:59:01 +08:00
134
1ae9b5a89b fix: verify scale adjust footprint shrink 2026-07-02 18:40:09 +08:00
134
0515fd3e46 fix: add deterministic scale adjust fallback 2026-07-02 18:27:59 +08:00
134
87d2960713 fix: include scale adjust region prompt 2026-07-02 18:14:51 +08:00
134
a8c5c14656 fix: use marked photo guide for scale adjust models 2026-07-02 18:04:51 +08:00
134
ee403bff11 fix: simplify fal scale adjust guide marks 2026-07-02 17:55:57 +08:00
134
2cccac34ff fix: guide fal scale adjust with marked photo 2026-07-02 17:49:03 +08:00
134
291597a69f fix: use fal-safe scale adjust prompt 2026-07-02 17:36:43 +08:00
134
f5a318beef fix: neutralize scale adjust coordinate prompt 2026-07-02 17:25:47 +08:00
134
b3b4fa8ed2 fix: enlarge scale adjust coordinate map 2026-07-02 17:14:06 +08:00
134
ffd13c0f31 fix: keep scale adjust single model attempt 2026-07-02 17:01:43 +08:00
134
6f0fb9fe4b fix: use coordinate map for scale adjust 2026-07-02 16:56:25 +08:00
134
22b007c2d9 fix: avoid old crop reference for scale adjust 2026-07-02 16:44:09 +08:00
134
2ad8bc0073 fix: strengthen scale adjust semantic guide 2026-07-02 16:26:05 +08:00
134
fb4ef202d8 fix: respect requested scale adjust provider 2026-07-02 16:03:29 +08:00
134
354fa98477 fix: strengthen scale adjust target hint 2026-07-02 15:53:42 +08:00
134
183d47fba0 chore: log rejected scale adjust previews 2026-07-02 15:44:57 +08:00
134
ba1bb165c3 fix: hint old footprint for scale adjust 2026-07-02 15:38:29 +08:00
134
cd19fe0b8d fix: reduce scale adjust guide recompose bias 2026-07-02 15:20:58 +08:00
134
62e3bc4611 fix: constrain scale adjust full-frame guide 2026-07-02 15:06:30 +08:00
134
2241e0f27e fix: guide scale adjust with visual target layout 2026-07-02 14:48:51 +08:00
134
08b439b7f6 fix: stop scale adjust quality failover 2026-07-02 13:24:43 +08:00
134
bf8197d884 fix: avoid extra scale adjust retry cost 2026-07-02 11:59:50 +08:00
134
c0ddd420ba fix: retry weak scale adjust edits with stronger prompt 2026-07-02 11:33:39 +08:00
134
0ec99e8ffb fix: constrain scale adjust failover family 2026-07-02 10:34:41 +08:00
134
d44a5d4060 fix: soft failover weak scale adjust outputs 2026-07-02 00:28:51 +08:00
134
8fd93bfd29 fix: keep fal as scale adjust fallback 2026-07-02 00:15:45 +08:00
134
0c919e851a fix: prefer low-cost scale adjust providers 2026-07-02 00:11:25 +08:00
134
f5a409fdde fix: tune fal scale adjust prompt 2026-07-01 23:41:34 +08:00
134
69f2f7957d feat: support claw quick reply actions 2026-07-01 19:05:10 +08:00
134
e7f3df1f5d fix: strengthen fal scale adjust guide 2026-07-01 18:56:06 +08:00
134
6e5c608df8 fix: clarify fal scale adjust targets 2026-07-01 18:22:32 +08:00
134
e5ddc416e9 fix: stabilize fal scale adjust guidance 2026-07-01 18:06:56 +08:00
134
672139e35a fix: align ai wash compare frame 2026-07-01 01:04:34 +08:00
134
91fe35e3b6 fix: stabilize ai wash model handling 2026-07-01 00:30:56 +08:00
134
7aca7a764d feat: add self-hosted pascal editor and 3d modeling entry 2026-06-30 19:57:52 +08:00
134
b87b44c7b4 fix: align image comparison sliders 2026-06-30 17:27:17 +08:00
134
e7cd3d6e73 fix: recognize BFL reference image support 2026-06-30 14:38:37 +08:00
134
d001dba00c fix: clarify scale adjust guide and retry failed FAL edits 2026-06-30 10:32:46 +08:00
134
d9f63bd00c fix: pass FAL scale adjust system prompt 2026-06-29 20:08:31 +08:00
134
5f5c19f313 fix: preserve claw skill tag settings on upgrades 2026-06-29 18:51:06 +08:00
134
0a792c7e26 fix: simplify FAL scale adjust inputs 2026-06-29 18:09:22 +08:00
134
8a9fba7eab fix: keep clawd submitted text out of composer restore 2026-06-29 18:07:08 +08:00
134
02772bc343 fix: preserve custom canvas skill tags on upgrade 2026-06-29 17:30:50 +08:00
134
164d9f97e7 fix: remove FAL scale adjust mockup guide 2026-06-29 17:03:43 +08:00
134
12c93e1154 feat: update canvas skills and project list rename 2026-06-29 15:59:01 +08:00
134
16a6134c77 fix: improve single-image scale adjustment 2026-06-29 14:38:49 +08:00
134
8ded477edd fix: avoid rejecting final scale adjust candidate 2026-06-29 11:24:27 +08:00
134
be0b398e5c fix: prefer cleaner FAL guide retry 2026-06-28 15:06:17 +08:00
134
d37cb6cffc fix: show claw image upload progress 2026-06-28 14:47:05 +08:00
134
36d575b825 fix: respect FAL edit image count limit 2026-06-28 14:16:36 +08:00
134
471eb3f650 fix: retry FAL scale adjust with guide prompts 2026-06-28 14:05:19 +08:00
134
d519ac3dde fix: keep FAL scale adjust on fixed model 2026-06-28 10:51:29 +08:00
134
3b00c37d30 fix: animate claw media responses 2026-06-28 10:50:02 +08:00
134
619cc2bf5b fix: clarify FAL scale adjust guidance 2026-06-28 10:44:54 +08:00
134
1d0af8c2d8 fix: prioritize selected claw shortcut 2026-06-28 10:35:14 +08:00
134
e76c983e64 fix: replace claw skill tags and isolate shortcut sessions 2026-06-28 10:20:53 +08:00
134
98eef59d97 fix: keep FAL scale adjust candidates 2026-06-26 23:20:27 +08:00
134
e3f5b9720e feat: refine canvas shortcuts and image candidate selection 2026-06-26 19:24:53 +08:00
134
d8af4bb48d feat: add Claw skill shortcuts and isolate mode chats 2026-06-26 18:18:43 +08:00
134
80f6827375 feat: improve Claw shortcut mode and toolbar docking 2026-06-26 17:27:59 +08:00
134
045e36b242 fix: allow public font assets 2026-06-25 23:21:58 +08:00
134
4d6e218e98 feat: use JetBrains Mono sitewide 2026-06-25 23:16:03 +08:00
134
95354eb78d fix: allow table links in chat responses 2026-06-25 22:58:35 +08:00
134
b91c40f6df fix: split wide Claw PDF tables 2026-06-25 22:49:12 +08:00
134
934bc56202 fix: improve Claw PDF table print layout 2026-06-25 21:53:19 +08:00
134
e02dff0b06 fix: guide FAL scale adjust with target mockup 2026-06-25 19:45:33 +08:00
134
186e2cefc2 fix: include user department in Claw payload 2026-06-25 14:14:35 +08:00
134
0100f58891 fix: include username in Claw chat payload 2026-06-25 13:05:25 +08:00
134
acaae5fd1b fix: reduce scale adjust guide artifacts 2026-06-25 00:05:52 +08:00
134
270c9fb61d fix: keep Claw composer cleared while submitting 2026-06-24 22:24:59 +08:00
134
948bed1737 fix: harden canvas edit and save flows 2026-06-24 21:52:45 +08:00
134
28c5965c52 fix: handle SnapGen image task failures 2026-06-24 21:51:12 +08:00
134
4de04234d0 fix: persist Claw composer clear guard 2026-06-24 21:44:22 +08:00
134
754209ee53 fix: clear restored Claw composer text 2026-06-24 21:18:39 +08:00
134
6f8c939be2 Tighten multi-image reference handling 2026-06-24 20:12:24 +08:00
134
f0e17dfffe improve current upload target resolution 2026-06-24 20:08:24 +08:00
134
c7e4553c45 fix canvas chat exports and image upload state 2026-06-24 20:03:25 +08:00
134
d10d638119 Refine multi-image target resolution 2026-06-24 19:02:20 +08:00
134
cc80bd5dac Add multi-image reference context resolution 2026-06-24 18:58:38 +08:00
134
cd90c8074c Fix report PDF placement and table scrolling 2026-06-24 18:51:26 +08:00
134
57f4d08c31 Add Claw report PDF export 2026-06-24 18:33:47 +08:00
134
05387d08f3 Update canvas generation UI and downloads 2026-06-24 17:59:10 +08:00
134
41522cc481 Fix canvas toolbar tone and placeholder scaling 2026-06-24 17:59:10 +08:00
134
1aad39b588 fix: keep SnapGen reference order prompt neutral 2026-06-24 17:59:10 +08:00
134
560491aabb fix: preserve SnapGen reference image order 2026-06-24 17:59:10 +08:00
134
91a855c041 fix: improve scale adjust annotation guide 2026-06-24 17:58:41 +08:00
134
07d081f838 chore: log image input counts for failover 2026-06-24 16:39:49 +08:00
134
5973359197 fix: preserve SnapGen resolution tiers 2026-06-24 16:15:50 +08:00
134
dcd005d369 fix: infer SnapGen auto aspect ratio 2026-06-24 11:35:41 +08:00
134
ed2c64d2a6 fix: poll SnapGen history for final images 2026-06-24 11:29:46 +08:00
134
9b94aae898 fix: drive image params from model config 2026-06-24 11:12:11 +08:00
134
7a58cdd12b fix: improve admin dialog dark mode 2026-06-24 11:08:03 +08:00
134
ddfe911812 feat: add SnapGen image provider 2026-06-24 10:58:41 +08:00
134
1af8a29b58 feat: render OpenClaw chart cards 2026-06-24 10:06:12 +08:00
134
fa10115043 fix: rename Claw mode and type responses 2026-06-24 01:37:04 +08:00
134
d5fed4f2da fix: enable markdown table horizontal scroll 2026-06-24 00:59:29 +08:00
134
fb4915e657 fix: show Clawd processing hint 2026-06-24 00:50:45 +08:00
134
47f996d81a fix: soften Clawd typing dots 2026-06-24 00:42:30 +08:00
134
514dcbb21f fix: restore Clawd composer font size 2026-06-24 00:39:08 +08:00
134
cd8f23e4b3 fix: unify Clawd attachment picker 2026-06-24 00:30:55 +08:00
134
16f734e9c5 feat: support Clawd spreadsheet uploads 2026-06-24 00:20:39 +08:00
134
deacac6e8f feat: route canvas agent local edits 2026-06-24 00:17:03 +08:00
134
96827c760c feat: route canvas agent angle variants 2026-06-23 23:59:24 +08:00
134
4588850572 fix: cap markdown heading sizes 2026-06-23 23:45:52 +08:00
134
0c95829b1e feat: batch direct canvas image edits 2026-06-23 23:42:45 +08:00
134
0afa404b19 feat: route canvas agent watermark removal 2026-06-23 23:31:43 +08:00
134
9151058d79 fix: keep background removal out of canvas deletion 2026-06-23 23:20:26 +08:00
134
d4936a8ca4 fix: isolate canvas mode switches 2026-06-23 23:15:41 +08:00
134
3f8219f415 feat: route canvas agent link operations 2026-06-23 23:11:20 +08:00
134
f2ed508cb4 fix: persist canvas chat session selection 2026-06-23 23:07:47 +08:00
134
8a2483ad7d feat: route canvas agent text updates 2026-06-23 23:04:43 +08:00
134
67dbeeab1a fix: improve Clawd pending and markdown tables 2026-06-23 22:59:03 +08:00
134
cd0037dc47 fix: consume canvas agent pending requests 2026-06-23 22:55:55 +08:00
134
d5ac630d01 fix: tighten canvas operation protocol 2026-06-23 22:49:06 +08:00
134
64ba6a6f29 fix: sync canvas save revisions 2026-06-23 22:43:22 +08:00
134
c82223f8f4 feat: add markdown table download 2026-06-23 22:41:17 +08:00
134
3524a4ea16 fix: guide scale adjust with target annotations 2026-06-23 22:36:38 +08:00
134
23b6855b4b fix: align canvas media batch ids 2026-06-23 22:33:00 +08:00
134
eec59f51fc fix: shorten Clawd labels 2026-06-23 22:24:44 +08:00
134
8c6bd2ad03 fix: preserve selected canvas slot context 2026-06-23 22:22:05 +08:00
134
d548e31063 fix: honor canvas agent view rollout on client 2026-06-23 22:08:01 +08:00
134
d434485987 fix: clear Clawd input on send 2026-06-23 22:05:47 +08:00
134
43e01e7613 feat: expose canvas agent rollout controls 2026-06-23 21:56:05 +08:00
134
fae9d32ffb feat: allow canvas agent rollout settings 2026-06-23 21:43:49 +08:00
134
020a74a6a8 fix: support confirmed slot replacement fills 2026-06-23 21:33:30 +08:00
134
652bc75ed0 fix: enforce canvas agent slot media types 2026-06-23 21:26:02 +08:00
134
5d5e10c83e test: cover canvas save revision fallback 2026-06-23 21:14:00 +08:00
134
7c2e31cbe5 fix: reject mismatched canvas agent project ids 2026-06-23 21:06:55 +08:00
134
ac0456eb97 fix: bypass image billing for canvas view intents 2026-06-23 20:52:42 +08:00
134
1669f4e2a4 fix: verify scale adjust after compositing 2026-06-23 20:51:18 +08:00
134
f23fae7676 fix: preserve canvas context for slot controls 2026-06-23 20:33:58 +08:00
134
d9bb014742 feat: add canvas Clawd mode 2026-06-23 20:23:26 +08:00
134
a752737327 fix: suppress image loading for canvas control prompts 2026-06-23 20:13:35 +08:00
134
d93a0d06d5 fix: widen scale adjust workspace guard 2026-06-23 20:10:45 +08:00
134
89b4a82e62 fix: support canvas view prompts without context payload 2026-06-23 20:07:53 +08:00
134
8913daa2b0 fix: route canvas control prompts before image billing 2026-06-23 20:01:32 +08:00
134
76229dac7b fix: let canvas control prompts bypass image point gate 2026-06-23 19:53:06 +08:00
134
3760f7ff44 fix: honor target boxes in scale adjust guard 2026-06-23 19:47:11 +08:00
134
4d65ba0b79 fix: include canvas revisions in service saves 2026-06-23 19:34:43 +08:00
134
733d6338c2 test: cover confirmed canvas agent moves 2026-06-23 19:28:51 +08:00
134
6d66d126c8 test: cover confirmed canvas agent deletes 2026-06-23 19:24:28 +08:00
134
6d5604a84b fix: filter handled canvas media batches 2026-06-23 19:17:13 +08:00
134
cb44b2237f fix: gate canvas agent view operations on client 2026-06-23 19:09:07 +08:00
134
2e386cb931 fix: defer canvas agent view during interactions 2026-06-23 19:03:47 +08:00
134
a1a095fe0e fix: animate canvas agent viewport changes 2026-06-23 18:56:49 +08:00
134
0f74cbd6ff fix: infer canvas agent image counts 2026-06-23 18:51:21 +08:00
134
c15a68385a fix: reset canvas add image dialog state 2026-06-23 18:41:23 +08:00
134
97563450a6 fix: frame canvas agent source and result 2026-06-23 18:40:07 +08:00
134
d3c921a489 fix: distinguish annotation edits from annotation notes 2026-06-23 18:33:35 +08:00
134
e0dc5a3d77 fix: route explicit canvas annotations 2026-06-23 18:28:53 +08:00
134
cf879c63b6 fix: route explicit canvas slot creation 2026-06-23 18:24:38 +08:00
134
06834699be fix: route explicit canvas agent operations 2026-06-23 18:15:08 +08:00
134
6e1d512021 fix: focus latest canvas agent result by prompt 2026-06-23 18:00:18 +08:00
134
b7ff4e68eb fix: centralize canvas agent pending requests 2026-06-23 17:52:00 +08:00
134
914e09af73 fix: show scale regions in detail dialog sidebar 2026-06-23 17:38:29 +08:00
134
c033e75ba1 fix: open chat for annotation edits 2026-06-23 17:34:35 +08:00
134
5648ad12be fix: open chat when filling agent slots 2026-06-23 17:27:35 +08:00
134
1d7c4816b0 fix: persist canvas agent media provenance 2026-06-23 17:17:13 +08:00
134
9e6cf69b45 fix: treat canvas view centering as focus intent 2026-06-23 17:10:44 +08:00
134
75cfa4c00f fix: drive scale adjust from target box guides 2026-06-23 17:02:48 +08:00
134
b78c77a790 feat: add canvas material image search 2026-06-23 16:55:22 +08:00
134
3a2a7f2490 feat: let canvas agent create text nodes 2026-06-23 16:54:18 +08:00
134
1e41335ebb fix: center canvas agent writebacks without anchors 2026-06-23 16:44:34 +08:00
134
38dba2b1bc feat: let canvas agent update image notes 2026-06-23 16:34:52 +08:00
134
e319b16f48 feat: let canvas agent rename nodes 2026-06-23 16:27:06 +08:00
134
763696e8d0 fix: trace annotation edits in canvas writeback metadata 2026-06-23 16:14:27 +08:00
134
ef0f9bc93d fix: anchor canvas writeback to resolved targets 2026-06-23 16:02:41 +08:00
134
e73c2da84e fix: add canvas agent pan view intent 2026-06-23 15:55:10 +08:00
134
38435f9401 fix: reduce canvas agent media duplicate window 2026-06-23 15:37:26 +08:00
134
50aa93d5f3 fix: route clear canvas requests through confirmation 2026-06-23 15:25:02 +08:00
134
0bb134d3b8 fix: include all visible canvas nodes in agent fit-all view 2026-06-23 15:18:54 +08:00
134
8ee7eb1f0a fix: keep annotation edit target in canvas agent context 2026-06-23 15:09:50 +08:00
134
c0550988e0 fix: keep canvas annotation operations idempotent 2026-06-23 14:49:37 +08:00
134
909f1c506c refactor: extract canvas agent writeback planning 2026-06-23 14:43:14 +08:00
134
16ebbeb6ac fix: allow scoped canvas chat history empty state 2026-06-23 14:36:08 +08:00
134
de457b5dc3 feat: pass canvas slot context to agent planner 2026-06-23 14:03:02 +08:00
134
ac16dc5e64 fix: reject rapid stale canvas autosaves 2026-06-23 13:35:54 +08:00
134
7f3ce72468 test: cover stale canvas autosave conflicts 2026-06-23 13:21:31 +08:00
134
084a6225e7 test: cover canvas agent slot creation mutation 2026-06-22 22:49:25 +08:00
134
183f07be25 fix: scope pending canvas view retry to project 2026-06-22 22:45:03 +08:00
134
dfb8971bcb fix: retry canvas agent view after refresh 2026-06-22 22:42:13 +08:00
134
41acb006cf test: cover canvas agent operation route handling 2026-06-22 22:38:00 +08:00
134
2945528f44 feat: support canvas comparison view intent 2026-06-22 22:33:05 +08:00
134
ad6b09d1b2 fix: focus canvas after agent writeback 2026-06-22 22:24:59 +08:00
134
5e82adb7eb fix: preserve canvas agent media metadata 2026-06-22 22:19:54 +08:00
134
3c7c122b2f feat: label image notes as canvas annotations 2026-06-22 22:17:25 +08:00
134
bcea56c0d9 fix: bypass clarification for selected canvas slots 2026-06-22 22:14:14 +08:00
134
7d99330eb9 refactor: resolve canvas agent view operations 2026-06-22 22:09:47 +08:00
134
640e1b6aa6 fix: show scale target corner arrows 2026-06-22 22:09:26 +08:00
134
6041442662 test: cover canvas agent mutation contracts 2026-06-22 22:04:43 +08:00
134
fa9f09e84e test: cover canvas agent operation requests 2026-06-22 21:58:45 +08:00
134
6d8aa557bf fix: allow deleting detail scale selections 2026-06-22 21:56:39 +08:00
134
e1685b517a feat: add canvas annotation edit prompt entry 2026-06-22 21:52:33 +08:00
134
9bc887eee1 fix: merge detail edit scale modes 2026-06-22 21:47:53 +08:00
134
df0daf4d27 feat: add canvas agent operation protocol 2026-06-22 21:45:23 +08:00
134
12b21d6f3b fix: unify canvas scale edit entry 2026-06-22 21:38:29 +08:00
134
d64d3c9deb fix: widen detail replacement preview 2026-06-22 21:10:10 +08:00
134
088d2c1b74 fix: sync detail scale target flow 2026-06-22 20:08:14 +08:00
134
bf8410a6d4 fix: use target box flow for canvas scale adjust 2026-06-22 19:52:55 +08:00
134
aaf3e08dd8 fix: improve canvas derived generation flows 2026-06-22 19:36:35 +08:00
134
47c7da3a1f fix: align detail scale adjust buttons 2026-06-22 18:30:03 +08:00
134
d34e6c8344 fix: keep scale adjust source image visible 2026-06-22 18:21:24 +08:00
134
c2ef33753a fix: improve detail replacement generation controls 2026-06-22 17:56:58 +08:00
134
c528e22680 docs: update canvas agent upgrade plan 2026-06-22 16:38:19 +08:00
134
038730a24b fix: preserve auto image ratio for agnes edits 2026-06-22 16:29:13 +08:00
134
2b96fdb3e7 fix: improve canvas local edit scale adjustment 2026-06-22 14:47:45 +08:00
134
f7d18f20db fix: keep latest canvas image highlighted 2026-06-22 14:46:25 +08:00
134
b032774e8e fix: improve image generation timeout failover 2026-06-22 14:38:59 +08:00
134
25df2d248b fix: honor custom branding favicon sources 2026-06-22 13:26:54 +08:00
134
7fddbe68bc fix: invert branding in dark mode 2026-06-22 12:00:52 +08:00
134
8519aa70e7 fix: guide scale adjust as localized edit 2026-06-22 11:08:51 +08:00
134
072fac14cc fix: sync canvas placeholder with chat loading 2026-06-22 11:02:41 +08:00
134
ef89ab1aeb fix: restore canvas generation placeholders 2026-06-22 10:36:08 +08:00
134
39b4bd5983 fix: skip scale adjust verification rejection 2026-06-22 02:51:22 +08:00
134
9e8817f89e fix: preserve Agnes image edit aspect ratio 2026-06-22 02:28:47 +08:00
134
eb2153818d fix: prefer detail model for scale adjustments 2026-06-22 01:22:25 +08:00
134
dd65ef46a7 fix: support Agnes image-to-image generations API 2026-06-22 01:04:46 +08:00
134
5b2418701f fix: preserve canvas image generation aspect ratio 2026-06-21 23:27:56 +08:00
134
e5bcf6f403 fix: reduce scale adjust false failures 2026-06-21 23:06:26 +08:00
134
7959dd6b3a fix: reduce scale adjust background distortion 2026-06-21 21:31:04 +08:00
134
2fbcd45035 fix: soften scale adjust composite edges 2026-06-21 19:34:23 +08:00
134
317593a72d fix: protect scale adjust output regions 2026-06-21 18:24:56 +08:00
134
e6d3b20308 fix: extend image task waits and scale adjust health 2026-06-21 15:47:10 +08:00
134
1ad52fd3c3 fix: reduce 3d world movement step 2026-06-21 11:57:06 +08:00
134
618bc2bd97 fix: pass scale adjust crop references 2026-06-20 21:47:00 +08:00
134
eac5bb9296 fix: disable scale adjust synthetic fallback 2026-06-20 18:39:49 +08:00
134
4f06ff7151 fix: reject scale adjust whole-frame changes 2026-06-20 10:30:17 +08:00
134
52cd365925 fix: fail over fixed detail image model first 2026-06-20 01:06:09 +08:00
134
0b1a3a715b fix: fallback scale adjust on provider failure 2026-06-20 00:09:42 +08:00
134
c0e16a354f fix: normalize scale adjust verification samples 2026-06-19 23:15:46 +08:00
134
b6296d4022 fix: use detail model for scale adjust 2026-06-19 22:38:31 +08:00
134
1ddb7928c6 fix: preserve subject aspect in scale adjust 2026-06-19 21:35:31 +08:00
134
e22b6f11b9 fix: hide scale prompts and add compare exit 2026-06-19 02:18:00 +08:00
134
baa924c8d4 fix: persist detail scale adjustments 2026-06-19 01:18:34 +08:00
134
43769c5b58 fix: guide scale adjustment target sizing 2026-06-18 22:07:35 +08:00
134
72e4b926d9 fix: soften local edit composite boundaries 2026-06-18 19:49:26 +08:00
134
f29a26b43f feat: improve canvas local edit scale adjustment 2026-06-18 18:41:17 +08:00
134
07854d7c6a fix: unify detail processing placeholders 2026-06-18 14:45:37 +08:00
134
74ae88edbb fix: improve scale adjust guidance and scan feedback 2026-06-17 21:44:57 +08:00
134
af63ebba9e feat: support target detail uploads and improve scale adjust 2026-06-17 17:37:51 +08:00
134
25af7e5526 feat: add detail image scale adjust and processing scan 2026-06-17 15:35:04 +08:00
134
9fae6c6a62 fix: extend local edit task wait time 2026-06-17 13:27:00 +08:00
134
1f1f3fd276 fix: unify canvas processing placeholders 2026-06-17 11:43:40 +08:00
134
8ec27aa225 feat: support canvas text color editing 2026-06-17 10:26:40 +08:00
134
c88cf6146b fix: persist agent workflow selections immediately 2026-06-16 22:30:05 +08:00
134
2807dcf9e3 fix: complete detail local edit workflow 2026-06-16 21:55:01 +08:00
134
553e5b5373 fix: add edit actions to detail replacement results 2026-06-16 21:19:37 +08:00
134
2f6d0498e2 fix: failover grsai maintenance image models 2026-06-16 14:48:42 +08:00
134
c631f906b1 fix: download canvas images from original source 2026-06-15 20:57:12 +08:00
134
71dcd5026b fix: validate external api image resolution 2026-06-15 18:17:50 +08:00
134
65aac6c14d fix: return original images from external image api 2026-06-15 17:19:54 +08:00
134
95d4bccceb fix: reject empty image results across chat providers 2026-06-15 16:52:23 +08:00
134
ffed16d802 fix: hide internal image failure messages 2026-06-15 15:33:58 +08:00
134
fdff0c2865 fix: reject empty gpt image results 2026-06-15 15:05:14 +08:00
134
010d7d1be5 fix: poll grsai image tasks and return originals 2026-06-15 14:33:31 +08:00
134
801d2c4644 feat: enhance detail result image actions 2026-06-15 13:28:58 +08:00
134
fe18aa0000 fix: run selected ai wash preset only 2026-06-15 00:34:46 +08:00
134
5e0464c522 fix: align canvas action credit costs 2026-06-14 20:22:29 +08:00
134
85930dc1a6 feat: show canvas action credit tooltips 2026-06-14 14:30:19 +08:00
134
898ed98f1a fix: simplify 3d world asset actions 2026-06-14 12:32:00 +08:00
134
85b28885a4 fix: refine 3d world viewer controls 2026-06-14 10:53:04 +08:00
134
e5838f480a fix canvas generation chat and failover behavior 2026-06-14 09:54:47 +08:00
134
cf21c80d24 fix: polish canvas generation workflows 2026-06-14 09:39:49 +08:00
134
a420e4cf33 Cap Comfly GPT image size to stable edit resolution 2026-06-13 22:36:29 +08:00
134
a939e000cf Normalize Comfly GPT image model id 2026-06-13 17:15:39 +08:00
134
382faf6c6d Use Comfly chat image edit endpoint 2026-06-13 14:56:59 +08:00
134
4eac5cf0c0 Normalize image output dimensions 2026-06-13 13:33:02 +08:00
134
a265a77095 Fix Comfly GPT image sizing 2026-06-13 13:11:11 +08:00
134
748c468013 Fix Comfly model size override 2026-06-12 23:38:54 +08:00
134
9e52e1844f Fix Comfly 4K image failover timing 2026-06-12 22:20:50 +08:00
134
686111628e Fix model dropdown name clipping 2026-06-12 21:44:10 +08:00
134
1b79f788b5 Adjust image model failover timing 2026-06-12 17:24:13 +08:00
134
d24eedc578 allow emergency server build without typecheck 2026-06-12 16:00:45 +08:00
134
7963a916cb fix comfly image size resolution 2026-06-12 15:52:25 +08:00
134
88afa0551c improve generation placeholder placement 2026-06-12 14:49:51 +08:00
134
03aee55cb6 improve canvas generation placement 2026-06-12 14:28:56 +08:00
134
016d588559 fix grsai image request and generation preview 2026-06-12 14:06:00 +08:00
134
edae2f83c2 fix image failover eligibility 2026-06-12 11:26:38 +08:00
134
1947aae685 improve canvas generation placeholders 2026-06-12 11:14:18 +08:00
134
36654a6fa3 tune grsai gpt image health handling 2026-06-12 10:43:13 +08:00
134
1aaf904fd3 fix grsai gpt image edit inputs 2026-06-12 10:29:01 +08:00
134
04972684c3 fix image providers and model deletion 2026-06-11 19:35:09 +08:00
134
f80b3e8117 Tune Lovart project text spacing 2026-06-11 18:07:20 +08:00
134
86c09ae375 Refine Lovart home visual spacing 2026-06-11 17:51:18 +08:00
134
4fe91d442e Improve canvas image download fallback 2026-06-11 17:06:21 +08:00
134
6523ea740c Polish Lovart home layout 2026-06-11 16:58:28 +08:00
Your Name
89b0eaaa3d fix: toggle home agent composer mode 2026-06-10 23:50:26 +08:00
Your Name
29033b5757 fix: align canvas loading and composer controls 2026-06-10 23:28:10 +08:00
Your Name
f02d18702a fix: refine Lovart-style canvas and home UI 2026-06-10 22:38:58 +08:00
Your Name
6d7f869e56 fix(external-image): correct request filter log template syntax 2026-06-09 19:29:27 +08:00
Your Name
482e1ef291 fix(external-image): improve grsai failover compatibility 2026-06-09 19:25:42 +08:00
Your Name
8db1afdbc0 fix: allow ruxa image in external api failover filtering 2026-06-09 19:00:25 +08:00
Your Name
79751743f2 fix(external-image): hide model details in API-facing generation errors 2026-06-09 18:44:03 +08:00
Your Name
aebf5e86cc fix: block model override fields in external image generate API 2026-06-09 18:36:16 +08:00
Your Name
23d284b994 Fix external image API failover image validation 2026-06-09 18:12:22 +08:00
Your Name
eb15c7a33b fix(ruxa): use configured task input mode for image models 2026-06-09 17:11:10 +08:00
Your Name
aa6c1f72a6 fix: replace existing world3d node before importing 2026-06-09 15:14:38 +08:00
Your Name
4a3342c795 style: refresh canvas composer UI to mirror Lovart dialog 2026-06-09 13:56:20 +08:00
Your Name
6b90b25d5c fix(worldlabs): increase world 3d api request timeouts 2026-06-09 13:32:24 +08:00
Your Name
74adb68d16 fix(chat): normalize image part fields for ts safety 2026-06-09 13:22:42 +08:00
Your Name
09efe47f17 fix(chat): tolerate file part optional data field in image-media conversion 2026-06-09 13:19:56 +08:00
Your Name
7c398cb4b1 fix(ruxa-image): keep preset sync unhidden and enabled 2026-06-09 13:11:29 +08:00
Your Name
cd817a6336 fix(ruxa-image): normalize runtime model id and enforce nano-banana-2 endpoint 2026-06-09 13:06:05 +08:00
Your Name
609700ab54 Fix RUXA nano-banana model input mode and runtime failover IDs 2026-06-09 11:47:42 +08:00
Your Name
56e68ff5d0 Fix RUXA nano-banana-2 runtime model id sync 2026-06-09 11:30:09 +08:00
Your Name
37e48b7878 Fix Gemini image reference forwarding for grsai 2026-06-09 10:46:36 +08:00
Your Name
0ba1ea521f 更新 2026-06-09 10:36:19 +08:00
Your Name
da9c8671fe fix: keep ruxa image modelId from configuration without remap 2026-06-08 21:22:30 +08:00
Your Name
3d7147d242 fix(ruxa): resolve runtime model ids by configured model 2026-06-08 21:06:33 +08:00
Your Name
6a9beb418a fix(ruxa): resolve runtime model ids from config for image flows 2026-06-08 20:13:12 +08:00
Your Name
5a04304ea9 feat(admin): add ruxa image preset model bootstrap 2026-06-08 19:04:13 +08:00
Your Name
663ff7b41c fix ruxa image resolution mapping 2026-06-06 16:39:08 +08:00
Your Name
e2111add87 perf: smooth canvas panning 2026-06-05 20:46:16 +08:00
Your Name
8943e2900b feat: add watermark removal to detail results 2026-06-05 18:01:45 +08:00
Your Name
c795e00d63 feat: configure selected image toolbar density 2026-06-05 18:01:45 +08:00
Your Name
f34344d53b fix: compact canvas image tools 2026-06-05 18:01:45 +08:00
Your Name
1a194b5527 fix: clarify batch pending state 2026-06-05 16:25:16 +08:00
Your Name
14dbe3efeb fix: poll grsai nano banana tasks asynchronously 2026-06-05 11:52:57 +08:00
Your Name
4a36630b57 fix: use original batch result images for actions 2026-06-04 18:42:17 +08:00
Your Name
8ca072d902 fix: refresh site settings after provider changes 2026-06-04 15:12:42 +08:00
Your Name
e358e0241c fix: persist worldlabs base settings without keys 2026-06-04 14:57:32 +08:00
Your Name
1598722ad0 fix: keep worldlabs provider enabled on runtime errors 2026-06-04 14:46:05 +08:00
Your Name
6674c22cdd fix: preserve psd clip mask layer stacking 2026-06-04 13:52:01 +08:00
Your Name
74f2d052ac fix: keep psb upload with compatibility errors 2026-06-04 13:37:05 +08:00
Your Name
c8073c6c46 fix: fail unsupported psb templates fast 2026-06-04 13:26:14 +08:00
Your Name
faa70a315c fix: require prompt pack for detail scene replacement 2026-06-04 13:16:01 +08:00
Your Name
9da001e3ee feat: add canvas ai wash quick panel 2026-06-04 13:16:01 +08:00
Your Name
b12d61b371 fix: align psd template layer ordering 2026-06-04 12:04:14 +08:00
Your Name
800eb10ba4 fix: stabilize psd template parsing progress 2026-06-04 11:39:53 +08:00
Your Name
37e2b66725 fix: force scene prompt rebuild for detail references 2026-06-04 11:29:40 +08:00
Your Name
0dcc759c80 fix: preserve psd clipped image layer order 2026-06-04 11:22:12 +08:00
Your Name
0808f34021 fix: make detail scene prompt pack explicit 2026-06-04 10:07:00 +08:00
Your Name
2ea6f2e800 fix: apply scene prompts to action detail posters 2026-06-03 22:01:49 +08:00
Your Name
7ac5bdf44f fix: harden detail replacement page routing 2026-06-03 16:47:50 +08:00
Your Name
87f23a0ab6 fix: add progress dialog for batch result exports 2026-06-03 16:02:53 +08:00
Your Name
fb240616e0 fix: route detail replacements by scene intent 2026-06-03 15:41:13 +08:00
Your Name
ce7c2bff35 fix: apply prompt packs to white scene detail replacements 2026-06-03 14:06:52 +08:00
Your Name
a154a28a4a fix: harden detail replacement layout rules 2026-06-03 11:13:19 +08:00
Your Name
1eddc7e296 style: refine ai wash workspace 2026-06-03 10:19:34 +08:00
Your Name
4e6f47d35f fix: remove marketplace brand marks from detail replacement 2026-06-02 23:03:42 +08:00
Your Name
91b1d868d4 fix: lock detail replacement layout slots 2026-06-02 22:29:58 +08:00
Your Name
a17d2da133 fix: apply prompt packs to action detail cards 2026-06-02 21:55:22 +08:00
Your Name
e126355b26 fix: avoid blank detail replacement outputs 2026-06-02 21:27:37 +08:00
Your Name
1a97f56169 fix: stabilize detail replacement page layout 2026-06-02 20:42:59 +08:00
Your Name
29930f3c14 fix: prevent detail prompt pack guard self-trigger 2026-06-02 19:40:02 +08:00
Your Name
5aab184a97 fix: route detail replacement scene prompts correctly 2026-06-02 19:12:58 +08:00
Your Name
5e896b8e56 fix: show psd image loading placeholder 2026-06-02 16:47:47 +08:00
Your Name
6f4a881238 fix: speed up psd editor image preview 2026-06-02 16:35:32 +08:00
Your Name
19dc43049d fix: guard detail replacement prompt routing 2026-06-02 16:34:05 +08:00
Your Name
0b9d41e7e2 fix: force detail replacement generation 2026-06-02 15:39:56 +08:00
Your Name
3e2261b1c7 fix: restore detail market id lookup input 2026-06-02 15:21:24 +08:00
Your Name
fa4b4183cf fix: stabilize detail replacement result images 2026-06-02 14:36:37 +08:00
Your Name
537a9d10c2 fix: update detail replacement and UI behavior 2026-06-02 14:26:01 +08:00
Your Name
37f6ef7e8e fix: polish ai wash layout and sidebar overflow 2026-06-02 14:22:49 +08:00
Your Name
ea970888f8 refactor: move ai wash toolbar right 2026-06-02 14:22:49 +08:00
Your Name
70cf96afaf fix: use spu for detail reference search 2026-06-02 13:50:52 +08:00
Your Name
51e8393f95 fix: resolve detail reference sku to spu 2026-06-02 13:19:15 +08:00
Your Name
5a7f8bc1f1 fix: streamline detail reference template lookup 2026-06-02 13:00:27 +08:00
Your Name
f69e2d07a2 fix: show regenerating state for style batch results 2026-06-02 11:44:04 +08:00
Your Name
caf9eb7290 fix: avoid premature psd parse timeout 2026-06-02 11:27:27 +08:00
Your Name
ea1bc4c78b fix: export canvas images as ps-safe png 2026-06-02 10:53:26 +08:00
Your Name
61bb42a266 fix: reject unchanged text edit results 2026-06-01 21:47:50 +08:00
Your Name
08323571aa fix: align rerun replacement pending cards 2026-06-01 21:40:03 +08:00
Your Name
a78fd8a0d4 fix: keep PSD clipped replacements in target layer order 2026-06-01 20:57:50 +08:00
Your Name
87be5499d5 fix: anchor PSD replacements to clip targets 2026-06-01 20:39:25 +08:00
Your Name
c247093ec9 fix: avoid empty PSD replacement masks 2026-06-01 20:28:11 +08:00
Your Name
cffed9ab50 fix: support PSD clipping mask target layers 2026-06-01 20:03:38 +08:00
Your Name
c00b9e333d fix: enable text edit after ai copy suggestion 2026-06-01 19:45:04 +08:00
Your Name
5461cff1db fix: improve PSD template management and clipping masks 2026-06-01 19:40:57 +08:00
Your Name
db1fc8c3df feat: add PSD force clipping mask option 2026-06-01 18:45:54 +08:00
Your Name
4d2093ef44 Revert "fix: preserve PSD super lock mask editing"
This reverts commit 6d60d53910cf8cb3d65aade75496cf514d3df617.
2026-06-01 18:45:54 +08:00
Your Name
0baa13f7de Revert "fix: clip PSD super lock replacements"
This reverts commit 6e647021cdc52a083cace27801044838f11a5864.
2026-06-01 18:45:54 +08:00
Your Name
5d7e8ce773 fix: allow system setting writes from jobs 2026-06-01 18:39:30 +08:00
Your Name
d82a47c035 fix: clip PSD super lock replacements 2026-06-01 18:23:40 +08:00
Your Name
e2cf152662 fix: preserve PSD super lock mask editing 2026-06-01 18:02:16 +08:00
Your Name
0a001fc5f4 fix: precompile PSD template render segments 2026-06-01 17:53:31 +08:00
Your Name
720652b68a fix: proxy material card previews 2026-06-01 16:49:28 +08:00
Your Name
00ae2d4e59 fix: preserve manual detail cutlines 2026-06-01 15:07:05 +08:00
Your Name
a3c291a1e0 fix: stabilize material grid hook order 2026-06-01 14:52:24 +08:00
Your Name
efc77160c2 fix: stabilize detail text edits and cutline deletion 2026-06-01 14:44:13 +08:00
Your Name
e8796c74a6 fix: guard material search client crashes 2026-06-01 14:25:13 +08:00
Your Name
531a23d15f fix: load material previews from originals 2026-06-01 14:09:18 +08:00
Your Name
291eb9b11c fix: improve material preview loading 2026-06-01 13:57:02 +08:00
Your Name
1c8a709670 fix: avoid canvas redirect shell flash 2026-06-01 13:22:11 +08:00
root
25a1a005a4 merge: sync settings updates into main 2026-06-01 12:05:39 +08:00
Your Name
903cf5459e perf: use external material image urls 2026-06-01 11:48:29 +08:00
root
57261dad71 perf: use external material image urls 2026-06-01 11:46:25 +08:00
Your Name
b1dc367bcf refactor: unify ai wash upload preview 2026-06-01 10:32:59 +08:00
Your Name
afc41c5a42 refactor: simplify ai wash preview layout 2026-06-01 01:54:40 +08:00
Your Name
40190a6034 refactor: align ai wash workspace styling 2026-06-01 01:14:07 +08:00
Your Name
1c7d005088 feat: add configurable ai wash workspace 2026-06-01 00:34:37 +08:00
Your Name
de91bd396f fix: add collapse control to external image api settings 2026-05-31 22:53:08 +08:00
Your Name
163497630d fix: sync browser title with site settings 2026-05-31 22:43:50 +08:00
Your Name
fec4b9d5aa fix: refresh krea image provider integration 2026-05-30 12:53:18 +08:00
Your Name
f31db01b8e feat: add agent workflow snapshot center 2026-05-29 17:22:32 +08:00
Your Name
9adb55c6a7 feat: add agent workflow snapshot center 2026-05-29 17:22:10 +08:00
Your Name
e160d7f217 fix: guard agent workflow config rollback 2026-05-29 16:53:19 +08:00
Your Name
ba46f97f3d fix: guard agent workflow config rollback 2026-05-29 16:52:40 +08:00
Your Name
180ee864d9 refactor: polish analytics and gallery chrome 2026-05-29 16:09:06 +08:00
Your Name
0861a0d87e refactor: polish settings toolbar 2026-05-29 15:41:45 +08:00
Your Name
1abbd740a3 refactor: flatten admin settings actions 2026-05-29 15:32:21 +08:00
Your Name
1307095960 refactor: streamline settings navigation chrome 2026-05-29 15:19:06 +08:00
Your Name
aea1d84c88 refactor: improve analytics dashboard 2026-05-29 15:11:52 +08:00
Your Name
155539693c refactor: flatten admin settings header 2026-05-29 14:51:20 +08:00
Your Name
fbfe3256c8 refactor: unify admin settings toolbar 2026-05-29 14:32:11 +08:00
Your Name
de70a23ff6 refactor: tighten admin settings toolbar 2026-05-29 14:24:27 +08:00
Your Name
ffccdecc90 feat: streamline admin settings layout 2026-05-29 14:11:59 +08:00
Your Name
7fd3feb77c feat: convert heic uploads to jpeg 2026-05-28 22:46:41 +08:00
Your Name
aa63415b1c fix: harden direct upload observability and cleanup 2026-05-28 22:08:06 +08:00
Your Name
3b7eb4a539 fix: harden image upload fallback handling 2026-05-28 21:52:08 +08:00
Your Name
fc04267f74 perf: reduce oss image request costs 2026-05-28 21:40:46 +08:00
Your Name
033b3c298d fix: render auto workflow options without preview images 2026-05-28 21:24:21 +08:00
Your Name
1487531b8c fix: improve stitch cut line dragging 2026-05-28 21:18:49 +08:00
Your Name
1cf4cf3ac0 fix: clarify text edit requires actual changes 2026-05-28 20:45:47 +08:00
Your Name
edcbddb0fa fix: parse text edit image results robustly 2026-05-28 20:43:07 +08:00
Your Name
a15a26ba99 fix: show fallback state for missing workflow previews 2026-05-28 20:40:00 +08:00
Your Name
4f1e0bc60a fix: hide home shell while opening agent workspace 2026-05-28 20:37:19 +08:00
Your Name
dfd55a136a chore: allow longer workflow preview fetch timeout 2026-05-28 20:02:17 +08:00
Your Name
5ccd392b75 fix: rewrite preview urls from current storage fallback 2026-05-28 19:52:41 +08:00
Your Name
38cc12da8e perf: rewrite agent workflow preview urls at 230px 2026-05-28 19:49:01 +08:00
Your Name
4d86b15781 perf: backfill agent workflow preview images at 230px 2026-05-28 19:13:00 +08:00
Your Name
33698bbc9c perf: limit agent preset preview images 2026-05-28 19:05:25 +08:00
Your Name
2eba3d8b5e fix: separate admin api rate limiting 2026-05-28 10:08:30 +08:00
Your Name
4941f09dfa feat: add qiniu kodo storage configuration 2026-05-27 21:37:21 +08:00
Your Name
d2e282030b ci: make github actions manual only 2026-05-27 21:14:52 +08:00
Your Name
85981acb8d fix: restore lovart home client session state
Some checks failed
Production Build / build (push) Has been cancelled
Dependency Audit / audit (push) Has been cancelled
Quality Gates / quality (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-27 18:23:37 +08:00
Your Name
4a976bb749 docs: document remaining dependency audit buckets 2026-05-27 14:47:06 +08:00
Your Name
befbc8e205 chore: include production build in release verification 2026-05-27 14:35:00 +08:00
Your Name
29ef366709 ci: add production build workflow 2026-05-27 14:27:55 +08:00
Your Name
7bcafa50a0 docs: keep delivery audit branch metrics current 2026-05-27 14:21:03 +08:00
Your Name
76bdaa1d1a docs: update delivery audit with build verification 2026-05-27 14:20:07 +08:00
Your Name
e653eaae0f chore: harden production build flow 2026-05-27 14:15:58 +08:00
Your Name
f569261359 docs: add delivery readiness audit 2026-05-27 13:46:58 +08:00
Your Name
a223d931a6 Merge remote-tracking branch 'github/main' into codex/admin-settings-ui-refactor
# Conflicts:
#	app/(admin)/settings/admin-settings-client.tsx
#	lib/db/migrations/meta/_journal.json
#	pnpm-lock.yaml
2026-05-27 13:40:19 +08:00
Your Name
44660bf7e3 chore: add branch readiness check 2026-05-27 13:35:35 +08:00
Your Name
4463bf1fc7 chore: add release verification gate 2026-05-27 13:26:17 +08:00
Your Name
58f6f5f249 chore: govern enhance pro api logging 2026-05-27 13:20:39 +08:00
Your Name
f4d272c7ff chore: govern product planning api logging 2026-05-27 13:16:37 +08:00
Your Name
37ed4b83b2 chore: govern layer split logging 2026-05-27 13:12:05 +08:00
Your Name
8e91c00c74 chore: govern rmbg service logging 2026-05-27 13:06:01 +08:00
Your Name
36f3de8321 chore: govern angle edit api logging 2026-05-27 12:59:30 +08:00
Your Name
0768f84802 ci: add dependency audit workflow 2026-05-27 12:55:19 +08:00
Your Name
574e4e4c70 chore: replace vulnerable xlsx parser dependency 2026-05-27 12:51:04 +08:00
Your Name
7859fa08d4 chore: reduce critical dependency audit baseline 2026-05-27 12:41:50 +08:00
Your Name
543a99bf4a docs: add dependency audit risk register 2026-05-27 12:31:33 +08:00
Your Name
a2ed0467d4 test: add helper gate hygiene check 2026-05-27 12:24:57 +08:00
Your Name
ced001ade2 test: add lightweight helper regression gate 2026-05-27 12:21:09 +08:00
Your Name
503e2d1d54 chore: add package metadata gate 2026-05-27 12:16:43 +08:00
Your Name
d9d9760405 chore: add secret hygiene gate 2026-05-27 12:12:54 +08:00
Your Name
3c1fc15039 chore: add env example hygiene gate 2026-05-27 12:08:44 +08:00
Your Name
06e85f8b0f ci: add workflow hygiene gate 2026-05-27 12:04:09 +08:00
Your Name
8e9669c748 ci: harden workflow defaults 2026-05-27 12:00:40 +08:00
Your Name
499fceb689 chore: add local artifact cleanup script 2026-05-27 11:57:03 +08:00
Your Name
c7966298dc feat: add AI model health ban exclusion
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-27 11:54:17 +08:00
Your Name
ceb8a1f8cb chore: pin ultracite script execution 2026-05-27 11:52:47 +08:00
Your Name
2c5a70c3b3 chore: add public brand hygiene gate 2026-05-27 11:49:23 +08:00
Your Name
8b8c39a495 chore: align repository identity with yedir-ai 2026-05-27 11:45:38 +08:00
Your Name
a3d7ea91b0 docs: add changelog 2026-05-27 11:43:10 +08:00
Your Name
e2bdecbbbf docs: add support policy 2026-05-27 11:39:03 +08:00
Your Name
005b9210c8 docs: add code of conduct 2026-05-27 11:36:03 +08:00
Your Name
f528d33728 docs: add code ownership rules 2026-05-27 11:33:20 +08:00
Your Name
ad9985c36f ci: add dependabot maintenance policy 2026-05-27 11:30:42 +08:00
Your Name
0692d5722c docs: add issue templates 2026-05-27 11:28:09 +08:00
Your Name
8a50d2ef3e docs: add contribution guide 2026-05-27 11:25:26 +08:00
Your Name
f9ce7f713b docs: add security policy 2026-05-27 11:22:52 +08:00
Your Name
d54fbef442 ci: run unified quality gate 2026-05-27 11:20:19 +08:00
Your Name
cb3de4f92f docs: add pull request release checklist 2026-05-27 11:18:16 +08:00
Your Name
f6d908f050 docs: add release readiness checklist 2026-05-27 11:16:05 +08:00
Your Name
869d8b2ce8 chore: unify quality gate commands 2026-05-27 11:13:02 +08:00
Your Name
15a08e6d65 fix: exit direct upload cors script
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-27 11:11:24 +08:00
Your Name
e211e41c25 chore: harden world labs task logging 2026-05-27 11:10:23 +08:00
Your Name
9328efa701 fix: load db after env for direct upload cors
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-27 11:07:55 +08:00
Your Name
f4214bf3a9 chore: harden canvas 3d task status logging 2026-05-27 11:07:28 +08:00
Your Name
c24ccebb18 fix: load local env for direct upload cors script
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-27 11:04:24 +08:00
Your Name
c9d3a3964f chore: harden canvas 3d task logging 2026-05-27 11:04:07 +08:00
Your Name
9c450a3d07 fix: make direct upload cors script standalone
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-27 11:01:21 +08:00
Your Name
ccf316630b chore: harden world labs upload logging 2026-05-27 11:01:20 +08:00
Your Name
9e8aa5a543 chore: harden pick object logging 2026-05-27 10:58:43 +08:00
Your Name
a1d59e0db8 chore: harden dynamic provider logging 2026-05-27 10:55:58 +08:00
Your Name
e1e53713b5 feat: add direct multipart image uploads
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-27 10:54:45 +08:00
Your Name
303535b22a chore: harden video archive logging 2026-05-27 10:52:00 +08:00
Your Name
e5224ff089 chore: add route smoke verification script 2026-05-27 10:49:10 +08:00
Your Name
593b7e3df0 refactor: extract canvas upload validation helpers 2026-05-27 10:45:26 +08:00
Your Name
9821832364 refactor: extract canvas detail slot helpers 2026-05-27 10:41:10 +08:00
Your Name
6a19c43094 refactor: extract canvas detail archive helpers 2026-05-27 10:37:35 +08:00
Your Name
d05eabe258 test: cover canvas detail config helpers 2026-05-27 10:32:04 +08:00
Your Name
31cccf87a1 test: cover canvas chat helper behavior 2026-05-27 10:27:49 +08:00
Your Name
63bafd278b refactor: extract canvas detail markdown helpers 2026-05-27 10:24:22 +08:00
Your Name
db4d0943bf refactor: extract canvas detail configuration 2026-05-27 10:21:03 +08:00
Your Name
ef837f9985 refactor: extract canvas agent stream helpers 2026-05-27 10:17:43 +08:00
Your Name
0f058207bb refactor: extract canvas chat media helpers 2026-05-27 10:15:15 +08:00
Your Name
a8f5dff6f8 refactor: extract canvas chat session helpers 2026-05-27 10:11:21 +08:00
Your Name
36412dee83 refactor: extract image node local edit helpers 2026-05-27 10:07:04 +08:00
Your Name
2bcb975606 refactor: extract image node utility helpers 2026-05-27 10:02:11 +08:00
Your Name
935f07d6ec fix: harden image upload reliability
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-27 09:59:45 +08:00
Your Name
0950e6827b refactor: extract image node crop geometry 2026-05-27 09:57:27 +08:00
Your Name
f0a7cbe2ac refactor: extract image node adjustment panel 2026-05-27 09:51:58 +08:00
Your Name
cc52f414af chore: govern chat api logging 2026-05-27 09:43:43 +08:00
Your Name
f2d4f9e698 refactor: modularize chat route helpers 2026-05-26 22:34:12 +08:00
Your Name
33bfcadf97 refactor: extract chat route error helpers 2026-05-26 22:15:49 +08:00
Your Name
c42dc88ba1 refactor: extract chat route stream helpers 2026-05-26 22:08:43 +08:00
Your Name
fadcbbc206 chore: expand canvas log hygiene coverage 2026-05-26 22:02:17 +08:00
Your Name
13a6e3df83 fix: remove black edges from world splats
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-26 21:50:07 +08:00
Your Name
91a98083a1 refactor: split image node presentation sections 2026-05-26 21:43:58 +08:00
Your Name
d89040e9f1 fix: simplify world controls and camera init
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-26 21:41:03 +08:00
Your Name
b30b093f54 fix: improve high quality world rendering
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-26 21:32:51 +08:00
Your Name
309d8281ca fix: prefer high quality world splats
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-26 21:21:25 +08:00
Your Name
c79724eefa docs: prepare yedir-ai for commercial delivery 2026-05-26 21:21:22 +08:00
Your Name
022eee7b95 fix: keep canvas image labels fixed size
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-26 21:09:13 +08:00
Your Name
286acefe4a chore: harden production logging and redaction 2026-05-26 21:05:42 +08:00
Your Name
501f5cf0d8 refactor: split lovart home into focused sections 2026-05-26 20:52:09 +08:00
Your Name
104187e2fe perf: harden lovart home data loading 2026-05-26 20:10:36 +08:00
Your Name
bac7d6f196 perf: make lovart home shell load first 2026-05-26 19:49:45 +08:00
Your Name
1db4444c7b refactor: remove legacy chat pages and home variant switch 2026-05-26 19:32:37 +08:00
Your Name
77ab711d41 chore: restore quality gates 2026-05-26 19:32:19 +08:00
Your Name
1365a22d5b refactor: modularize admin settings local ai sections
Also removes the legacy home mode selector from the settings form because the affected JSX lives inside the same settings client refactor.
2026-05-26 19:31:57 +08:00
Your Name
83d16bd6b6 chore: remove tracked local artifacts 2026-05-26 19:31:47 +08:00
Your Name
b17010bb35 fix: preserve unselected local edit areas
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-26 14:48:55 +08:00
Your Name
939138a02a fix: preserve world snapshot composition during refinement
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-26 14:15:43 +08:00
Your Name
09800c313f feat: refine world snapshots with image generation
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-26 13:59:39 +08:00
Your Name
c20dc8d3a1 fix: remove world splat movement hints
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-26 10:45:31 +08:00
Your Name
b40fe24b4a fix: aim world splat camera at ground plane
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-25 21:32:27 +08:00
Your Name
161126bd4c fix: match world splat initial camera to reference view
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-25 21:26:12 +08:00
Your Name
446b87443c fix: align world splat initial orientation
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-25 21:15:44 +08:00
Your Name
026bfd0615 fix: allow spark wasm data fetch
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-25 20:57:05 +08:00
Your Name
bfa072b5b8 fix: allow wasm for 3d decoders
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-25 20:52:34 +08:00
Your Name
f1ab6b6255 fix: fallback 3d world when splat stalls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-25 20:47:11 +08:00
Your Name
4cc0977c9f fix: harden 3d world splat loading
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-25 20:35:39 +08:00
Your Name
981a01a191 fix: allow 3d orbit during hotspot placement
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-25 19:48:29 +08:00
Your Name
dd2b06f10f fix: harden worldlabs asset loading
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-25 17:27:25 +08:00
Your Name
48f222998b fix: stabilize canvas uploads and image load
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-23 17:30:16 +08:00
Your Name
6e6acea09e fix: align local edit selection handles
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-23 15:41:04 +08:00
Your Name
f42554e82a fix: make local edit prompt typing immediate
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-23 15:21:54 +08:00
Your Name
d9dea4eb75 fix: handle local edit prompt keyboard input
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-23 15:04:46 +08:00
Your Name
9ebac8b079 fix: use text input for local edit prompts
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-23 14:57:31 +08:00
Your Name
87a1514a0f fix: rely on native local edit textarea input
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-23 14:51:26 +08:00
Your Name
852498b6bb fix: portal local edit prompts outside canvas transforms
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-23 14:43:25 +08:00
Your Name
9ddae7423e fix: render local edit prompts outside scaled image layer
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-23 14:35:41 +08:00
Your Name
15bc64984d fix: handle local edit prompt beforeinput
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-23 14:21:41 +08:00
Your Name
b5c7eeac5a fix: avoid intercepting local edit typing events
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-23 14:14:59 +08:00
Your Name
9cbbc7f065 fix: stabilize local edit prompt typing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-23 14:03:50 +08:00
Your Name
862c0f2937 fix: restore local edit prompt input handling
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-23 13:47:04 +08:00
Your Name
50890de38d fix: support volcano model in style pro realistic stage
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-23 13:28:32 +08:00
Your Name
c1b03278ca Harden public attack surface
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-23 11:36:02 +08:00
Your Name
2070cae477 Remove leaked Real-ESRGAN credentials doc 2026-05-23 11:21:32 +08:00
Your Name
007c1a4138 Remove unsafe repository artifacts 2026-05-23 11:18:50 +08:00
Your Name
6750e85ad4 Harden exposed security-sensitive endpoints 2026-05-23 11:09:03 +08:00
Your Name
adac18ba86 Polish DingTalk callback screen 2026-05-23 10:57:26 +08:00
Your Name
dce4e6c68a fix: protect local edit prompt input
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-22 23:00:04 +08:00
Your Name
fc73057935 fix: prevent world 3d viewer loading forever
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-22 22:15:37 +08:00
Your Name
ccb57c095a fix: improve local edit prompt ime input
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-22 21:59:57 +08:00
Your Name
d066a76f5a fix: maximize style pro result preview
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-22 21:32:41 +08:00
Your Name
279903ebb9 fix: allow typing in local edit prompt
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-22 21:29:43 +08:00
Your Name
cad16e70ba fix: show image size adjusted credit cost
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-22 21:26:52 +08:00
Your Name
b58e0f945e fix: tolerate transient style pro task polling failures
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-22 21:23:20 +08:00
Your Name
4aead3018f feat: add image size credit multipliers
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-22 20:49:14 +08:00
Your Name
a66c3107b9 fix: support Chinese input in local edit prompts
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-22 20:41:45 +08:00
Your Name
1fc37c4a8f fix: improve style pro generation flow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-22 20:37:28 +08:00
Your Name
667ab76e3d fix: preserve psd render base in exports
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-22 11:25:00 +08:00
Your Name
566449becd fix: render psd text layers without distortion
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-21 16:18:57 +08:00
Your Name
5776c852ba fix: show psd render base overlays
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-21 15:27:10 +08:00
Your Name
2226ab34c7 fix: allow workflow saves without previews
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-21 11:05:10 +08:00
Your Name
61da591a5c fix: improve canvas local edit output
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-21 10:25:19 +08:00
Your Name
ce283d287f fix: classify chat send failures
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-20 20:59:26 +08:00
Your Name
4d83a2d001 fix: align 3d world mesh display
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-20 18:20:54 +08:00
Your Name
68eab912d8 perf: optimize agent style rendering
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-20 18:10:43 +08:00
Your Name
d6ab02eb45 fix: anchor local edit prompts to image node
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-20 17:45:53 +08:00
Your Name
a93daefe54 fix: align 3d world entry view
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-20 17:29:04 +08:00
Your Name
0f91db7661 fix: keep local edit prompts aligned while panning
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-20 16:48:42 +08:00
Your Name
65b3e2f191 fix: optimize agent style workspace rendering
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-20 16:05:33 +08:00
Your Name
31a697f491 fix: stabilize canvas local edit controls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-20 15:44:17 +08:00
Your Name
06edd31b87 fix: recover stalled 3d world tasks
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-20 15:40:14 +08:00
Your Name
d72ca0b344 fix: polish style pro model visibility and billing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-20 14:26:24 +08:00
Your Name
71837cc547 fix: record image model failures and broaden failover
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-20 13:47:59 +08:00
Your Name
4c86e01956 feat: add reference images to local edit regions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-20 11:01:36 +08:00
Your Name
e415de06d8 fix: keep local edit annotations out of output
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 21:53:31 +08:00
Your Name
b1f2677f20 fix: use dark local edit toolbar accent
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 21:36:20 +08:00
Your Name
06153cc179 fix: enable local edit region controls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 21:20:12 +08:00
Your Name
f87669c15a fix: stabilize agent psd template uploads
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 20:28:10 +08:00
Your Name
1f766e745c fix: load legacy workflow previews directly
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 18:30:32 +08:00
Your Name
ffcfd08b70 fix: keep local edit prompt focused
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 18:08:51 +08:00
Your Name
576d4a6b9f fix: avoid blank workflow preview cards
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 16:46:21 +08:00
Your Name
d370c68b43 fix: prevent local edit prompt focus loss
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 16:43:10 +08:00
Your Name
7421e158d1 fix: portal local edit prompt inputs
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 15:49:32 +08:00
Your Name
1dd1251880 fix: complete local edit and detail replacement wiring
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 15:24:13 +08:00
Your Name
5239250465 feat: improve agent progress feedback
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 15:16:12 +08:00
Your Name
1db3d108d4 fix: preserve psd static layers on export
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 15:02:24 +08:00
Your Name
7792d1a740 fix: stabilize local edit prompt input
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 14:55:37 +08:00
Your Name
a5b3e35df9 fix: refine local edit controls and workflow validation
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 14:15:10 +08:00
Your Name
c5510e4936 fix: always show canvas local edit entry
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 13:35:33 +08:00
Your Name
2979446337 fix: isolate canvas chat sessions during streaming
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 13:28:35 +08:00
Your Name
2d59e3e3c4 feat: add local edit to infinite canvas
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 13:11:53 +08:00
Your Name
ac70694ef3 fix: hide agent target status card
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 13:10:39 +08:00
Your Name
317633085f fix: persist greeting fast path safely
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 11:41:54 +08:00
Your Name
cb8da8b512 fix: make chat creation idempotent
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 11:30:54 +08:00
Your Name
7f309aad22 fix: hide canvas auto-agent process copy
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 11:10:22 +08:00
Your Name
82876977aa fix: improve canvas agent response rendering
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 10:29:35 +08:00
Your Name
0b5866bb94 feat: add canvas local image edit
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-19 10:10:31 +08:00
Your Name
5d8146ca11 feat: surface image queue status across generators
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-18 23:48:49 +08:00
Your Name
0213047d2d fix: improve scene fusion prompts and failover blacklist
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-18 22:12:12 +08:00
Your Name
aaf5d3cddb fix: load agent workflow panel reliably
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-18 20:03:20 +08:00
Your Name
5b618679af fix: unlock new agent project workspace
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-18 19:39:28 +08:00
Your Name
54c6b4a74e fix: prevent agent workspace loading deadlock
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-18 18:49:12 +08:00
Your Name
bd98282bc5 fix: invalidate cached public site settings
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-18 17:52:16 +08:00
Your Name
33f25f4b21 fix: stabilize agent project startup 2026-05-18 17:50:33 +08:00
Your Name
57a1e25921 fix: prioritize external api img2img providers
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-18 17:08:04 +08:00
Your Name
7a2bd3c71b fix: include hidden models in external image api
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-18 16:39:56 +08:00
Your Name
cc320f6334 feat: default external image api to img2img failover
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-18 16:29:46 +08:00
Your Name
11c2f8f423 fix: allow external image api in proxy
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-18 16:06:00 +08:00
Your Name
b4698be089 feat: add external image generation api
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-18 15:56:08 +08:00
Your Name
040a1f514f fix: improve agent workflow settings loading
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-18 15:32:05 +08:00
Your Name
03cdefca34 fix: hide internal model names in account center
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-18 13:38:45 +08:00
Your Name
c433b714cd feat: customize style pro button colors
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-17 23:38:12 +08:00
Your Name
0869eefee3 feat: add style pro realistic mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-17 17:43:09 +08:00
Your Name
ce5daaea45 fix: charge style pro image generation
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-17 00:58:57 +08:00
Your Name
5ece717e31 fix: hide hidden models in style pro selector
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-17 00:46:37 +08:00
Your Name
39c8489e8f fix: restore style pro toolbar entry
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-17 00:14:16 +08:00
Your Name
e81a022b6f fix: respect style mode toolbar toggle
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-16 22:51:53 +08:00
Your Name
f726c14397 fix: sync style pro toolbar and realtime controls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-16 22:36:05 +08:00
Your Name
e8a3212b35 feat: persist style pro history and model pricing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-16 21:28:00 +08:00
Your Name
0ac796b9ff feat: compact style pro prompt layout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-16 20:33:08 +08:00
Your Name
a188ee963a feat: share brand top bar with style pro
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-16 20:04:59 +08:00
Your Name
fa2f26bf97 feat: align style pro UI with index prototype
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-16 19:34:38 +08:00
Your Name
6a47359d8e feat: seed style pro index presets
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-16 16:24:01 +08:00
Your Name
d6a19ad171 feat: add style pro settings tab
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-16 15:28:45 +08:00
Your Name
5387d6f1d2 feat: add style pro canvas workspace
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-16 15:06:12 +08:00
Your Name
e51de15aac fix: load archived 3d assets without proxy 2026-05-15 09:53:42 +08:00
Your Name
4e49e5e5b5 fix: throttle workflow preview preloading
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-15 09:36:16 +08:00
Your Name
edc24cb956 fix: refine PSD templates and result exports
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 20:29:27 +08:00
Your Name
ca7b709de0 fix: show detail stitch cut lines
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 17:43:40 +08:00
Your Name
591f462136 fix: optimize workflow prompt preview images
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 17:15:03 +08:00
Your Name
90e56b7dde fix: bypass proxy for same-origin 3d assets 2026-05-14 17:12:58 +08:00
Your Name
132af95df9 fix: make model cooldown recovery query safe
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 16:51:44 +08:00
Your Name
8fd5eae96a fix: use public origin for detail stitch images
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 16:48:12 +08:00
Your Name
769862cee9 fix: improve detail stitch and model recovery
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 16:35:16 +08:00
Your Name
359b964033 fix: remove psd layer position controls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 16:22:59 +08:00
Your Name
96d9551e54 fix: restore psd preview layer stacking
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 16:15:32 +08:00
Your Name
5f90dbde67 Fix detail stitch long preview format
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 15:48:48 +08:00
Your Name
a3a41ef6e6 fix: align psd layer hit testing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 15:48:13 +08:00
Your Name
5e7809687c fix: improve psd template layer interactions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 15:35:54 +08:00
Your Name
25d331fa51 Optimize agent image stitching and previews
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 15:28:09 +08:00
Your Name
95572bd476 fix: allow basis transcoder assets for 3d textures
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 15:08:33 +08:00
Your Name
1c935bf7ae perf: upgrade world viewer to spark 2
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 14:57:56 +08:00
Your Name
147f9394c8 feat: support product-only detail replacement
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 14:52:01 +08:00
Your Name
000ac13da4 fix: speed up detail replacement downloads
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 14:36:37 +08:00
Your Name
5864e9f4cf fix: proxy tripo texture assets in 3d viewer
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 14:34:29 +08:00
Your Name
2f5c2fca50 fix: proxy archived 3d assets for browser loading
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 12:04:45 +08:00
Your Name
dee8019efd perf: speed up world viewer loading
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 11:46:16 +08:00
Your Name
89ec01cfb8 fix: restore 3d pbr textures in canvas viewer
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-14 10:48:39 +08:00
Your Name
86b1ee9eeb fix: sync canvas generation results immediately
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-13 20:36:08 +08:00
Your Name
1b1ff65de8 feat: add discounted model pricing and fix realtime 3d media handling
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-13 20:02:52 +08:00
Your Name
d0bb8b7cf7 feat: support batch product id reference fill
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-13 18:37:03 +08:00
Your Name
da6cae9103 feat: tighten agent workflow detail UI
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-13 18:33:43 +08:00
Your Name
f0cb1a4f59 fix: adapt ruxa nano banana 2 runtime
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-13 12:25:16 +08:00
Your Name
40e7ecc7da fix: pass bltcy image aspect params
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-12 17:18:26 +08:00
Your Name
a0e303abcb fix: preserve volcano image aspect ratio
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-12 16:40:21 +08:00
Your Name
4db458a69a feat: improve psd template layer editing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-12 14:44:29 +08:00
Your Name
44d4a8065e fix: preserve grsai gpt image reference ratio
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-12 14:36:56 +08:00
Your Name
2cf2162fba fix: map grsai gpt image sizes
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-11 21:03:53 +08:00
Your Name
dd4426ded7 fix: improve agent image intent handling
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-11 20:31:17 +08:00
Your Name
3ebe7db37d feat: add ai model health recovery pool 2026-05-11 20:27:21 +08:00
Your Name
1ed3184000 fix: add psd layer list scrollbar
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-11 15:01:27 +08:00
Your Name
40848ffdb5 fix: stabilize dingtalk login and image sizing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-11 14:24:34 +08:00
Your Name
510e3dc323 fix: upload scene fusion inputs as urls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-09 17:22:12 +08:00
Your Name
3a4aece308 fix: avoid batch upload storage crash
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-09 17:15:24 +08:00
Your Name
f99ca7e49d fix: improve product detail api compatibility
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-09 14:41:47 +08:00
Your Name
d73836c365 fix: ensure product detail channel is forwarded
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-09 14:37:34 +08:00
Your Name
e6d4c31120 feat: connect product detail image references
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-09 14:10:21 +08:00
Your Name
19970915fd feat: optimize psd template asset backfill
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-09 11:28:38 +08:00
Your Name
11a2babb77 feat: add ai model health failover
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-09 09:33:50 +08:00
Your Name
12671a1e36 fix: refine angle panel and agent workspace scroll 2026-05-08 22:16:33 +08:00
Your Name
1991e5c4cc feat: improve image generation concurrency
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-08 21:23:44 +08:00
Your Name
e157480ec2 fix: refine angle panel proportions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-08 20:04:36 +08:00
Your Name
b82bace466 fix: refine canvas zoom gesture handling
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-08 18:37:28 +08:00
Your Name
21a2254428 feat: expand canvas batch and image workflows
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-08 16:24:28 +08:00
Your Name
46b8c23975 fix: refine angle preview panel styling 2026-05-08 16:15:28 +08:00
Your Name
f7e6958315 fix: restore sidebar toolbox popovers
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-08 15:19:05 +08:00
Your Name
2babdd6a47 fix: harden image download fallbacks
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-08 15:01:03 +08:00
Your Name
2f52589469 fix: sync canvas angle availability
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-08 14:00:30 +08:00
Your Name
1f8e19598c fix: refine canvas angle preview panel
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-08 13:34:53 +08:00
Your Name
b78a2701bb fix: respect canvas toolbar toggles
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-08 12:56:19 +08:00
Your Name
1912bcd25b chore: sync all remaining local workspace changes
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-08 11:26:57 +08:00
Your Name
e3948f4518 feat: add in-canvas image crop panels
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-08 11:23:47 +08:00
Your Name
434d4d0d8d fix: add 3d model feature visibility and refunds
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-08 11:21:15 +08:00
Your Name
b64a512dfb fix: adapt fal image sizing parameters
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-08 11:17:05 +08:00
Your Name
c4c0bd8f64 fix: streamline extend image payload flow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-08 08:48:07 +08:00
Your Name
2814669881 fix: harden scene fusion replacement flow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-07 22:28:35 +08:00
Your Name
965d1f98f3 fix: constrain psd editor side panel overflow 2026-05-07 22:00:46 +08:00
Your Name
4ce8562fd8 fix: allow next inline scripts in csp
Some checks failed
Lint / build (20) (push) Has been cancelled
2026-05-07 21:26:35 +08:00
Your Name
66fd760b9a feat: update enhance pro configuration
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-07 19:54:02 +08:00
Your Name
b8e8b45b6e fix: use proxy entry for next middleware 2026-05-07 19:50:00 +08:00
Your Name
2561c66cb8 feat: update workflow categories and admin notifications
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-07 19:31:14 +08:00
Your Name
c843661da3 Align PSD template pool layout 2026-05-07 17:53:21 +08:00
Your Name
d58b706799 security: implement Phase 1 & partial Phase 2 improvements
Some checks failed
Lint / build (20) (push) Has been cancelled
- Remove hardcoded admin password, use env var or auto-generate
- Fix trustHost to be conditional (Vercel-only by default)
- Add HTTP security headers (CSP, X-Frame-Options, HSTS)
- Create middleware.ts to activate existing rate limiting
- Implement structured logging system
- Rewrite error handling in ai-cad service
- Update security documentation
2026-05-05 22:49:57 +08:00
Your Name
38c10d616b feat: add dingtalk robot update notifications
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-01 01:45:59 +08:00
Your Name
94a22f0cc6 feat: add image batch dispatch pacing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-01 01:14:07 +08:00
Your Name
e7cf10bb28 feat: unify site-wide image concurrency
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-05-01 00:17:25 +08:00
Your Name
9073c3a389 feat: raise per-model image queue concurrency
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-30 23:54:06 +08:00
Your Name
30a541559c fix: suppress auto canvas add success toasts
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-30 22:02:17 +08:00
Your Name
eec475287d fix: make canvas node sync idempotent
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-30 20:28:37 +08:00
Your Name
2d76ec1629 fix: center world splat camera controls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-30 19:46:56 +08:00
Your Name
57b7292269 fix: improve world viewer camera targeting
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-30 17:38:13 +08:00
Your Name
987ab3f6a0 feat: support batch competitor planning lookup
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-30 17:35:25 +08:00
Your Name
40ca6d5eb8 fix: temporarily disable product planning advanced analysis
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-30 15:53:36 +08:00
Your Name
48c6cd0f87 fix: stabilize world 3d asset preview
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-30 14:35:17 +08:00
Your Name
fec1151f70 fix: prevent canvas chat image attachment crashes
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-30 12:40:25 +08:00
Your Name
b630267f33 perf: cache scoped site settings
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-30 01:43:41 +08:00
Your Name
3249b19f56 perf: trim public site settings payloads
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-30 01:31:33 +08:00
Your Name
77a32a4d60 perf: isolate lovart home route
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-30 00:41:37 +08:00
Your Name
dedc62cb79 perf: split classic chat from lovart home
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-30 00:22:17 +08:00
Your Name
d62822bc03 perf: defer noncritical home data
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-29 23:58:07 +08:00
Your Name
aabd492300 perf: lazy load home image editor
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-29 23:46:27 +08:00
Your Name
f7dba9cb41 perf: speed up home and canvas entry
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-29 23:32:21 +08:00
Your Name
a7b9533d72 perf: optimize oss image delivery globally
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-29 22:32:06 +08:00
Your Name
a6083ac409 perf: serve aliyun oss previews directly
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-29 21:42:03 +08:00
Your Name
e1e4c205e1 fix: bypass image proxy for browser image loads
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-29 16:48:30 +08:00
Your Name
166cc184d1 fix: preserve canvas context for chat image results
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-29 16:25:51 +08:00
Your Name
979c960359 feat: add StepFun image provider
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-29 15:51:23 +08:00
Your Name
93cc86aa54 fix: streamline world 3d viewer overlay
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-29 11:42:49 +08:00
Your Name
9837ff0b05 fix: polish canvas send icon and text edit dialog
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-29 11:37:08 +08:00
Your Name
769609ba26 feat: improve replacement text editing workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-28 23:02:21 +08:00
Your Name
dc5ec5f87a fix: refine world 3d node controls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-28 22:21:24 +08:00
Your Name
44dee6314a fix: run fal gpt image edit asynchronously
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-28 22:16:57 +08:00
Your Name
ce03c85811 fix: stabilize world canvas navigation controls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-28 20:51:33 +08:00
Your Name
fca2552412 fix: move passthrough toggle onto detail preview cards
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-28 19:55:05 +08:00
Your Name
ab35298e23 fix: add passthrough action on detail reference cards
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-28 19:34:02 +08:00
Your Name
81cd4d1754 fix: refine detail stitch preview and passthrough flow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-28 18:29:33 +08:00
Your Name
ac58f28612 fix: prefer main image for detail preview cards
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-28 18:00:40 +08:00
Your Name
bfb492babd fix: improve detail replacement passthrough and search
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-28 17:50:01 +08:00
Your Name
7d51b980b9 fix: support all image providers for canvas text edit
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-28 15:23:05 +08:00
Your Name
7bf3363211 fix: support dark mode in realtime canvas
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-28 15:19:41 +08:00
Your Name
1f1ffd2211 feat: support default canvas text edit image model
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-28 15:07:28 +08:00
Your Name
8b9d9d42e0 fix: gate detail references by selected brand
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 23:19:18 +08:00
Your Name
83c274e8d4 fix: lock workflow hidden models
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 23:12:23 +08:00
Your Name
35c4c02579 fix: simplify recharge copy
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 22:57:02 +08:00
Your Name
dfec8d7ee3 fix: prevent psd templates leaking across brands
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 22:49:35 +08:00
Your Name
5fb1b5e652 fix: validate reference brand by goods info
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 22:24:52 +08:00
Your Name
f83dc0aff3 fix: allow hidden models in admin workflow selectors
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 22:15:03 +08:00
Your Name
ce6873f8fe fix: expose psd text color controls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 22:02:36 +08:00
Your Name
00c4e067bf fix: enforce brand matching for references
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 21:53:02 +08:00
Your Name
47a0299090 fix: ignore store filter for detail references
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 21:31:10 +08:00
Your Name
807aba1e85 fix: filter detail references by brand
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 21:08:33 +08:00
Your Name
55bed5087c fix: resolve public dulupay callback origin
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 20:55:37 +08:00
Your Name
7b69e7dcee fix: route fal nano banana edit requests
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 20:54:44 +08:00
Your Name
3f1e4b24c6 fix: make fal provider selection discoverable
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 20:35:24 +08:00
Your Name
ba896cb132 fix: expose fal preset quick add button
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 20:27:51 +08:00
Your Name
7e0f482bad fix: guard dulupay checkout for guests
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 20:24:19 +08:00
Your Name
2108d06715 feat: support cross-provider image failover and workflow model settings
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 20:10:48 +08:00
Your Name
b8a59fe2a4 fix: harden enhance pro task charging
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 18:38:42 +08:00
Your Name
9f17020582 Allow canvas image queue concurrency up to 100
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 17:22:58 +08:00
Your Name
432d315bc6 fix: align canvas billing and 3d task routing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 16:56:40 +08:00
Your Name
1870f87beb Optimize stylization workflow selection performance
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 16:49:02 +08:00
Your Name
4eaab49776 feat: sync video canvas billing and branding 2026-04-27 12:04:37 +08:00
Your Name
9ad5d69bec align grid split with reference workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-27 11:27:53 +08:00
Your Name
40af82a807 Improve video analysis workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-26 22:01:56 +08:00
Your Name
c5c2d6eb93 Add video node toolbar interactions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-26 21:08:19 +08:00
Your Name
2cad27cdcd feat: add connected image-to-video composer
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-26 19:55:36 +08:00
Your Name
7f796a5bfd feat: add split-tile upscale toolbar
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-26 18:48:54 +08:00
Your Name
d622b18ddf fix canvas grid split selection workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-26 17:55:43 +08:00
Your Name
1a74df664c chore: move video canvas demo media to oss
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-26 15:24:26 +08:00
Your Name
00a6f70990 fix: avoid blocking canvas refs on local url checks
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-26 14:40:40 +08:00
Your Name
0b3f1e7c15 fix: prefer generated image as canvas reference
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-26 14:07:22 +08:00
Your Name
0e12d4209c fix: show preset toolbar for generated image result nodes
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-26 13:38:41 +08:00
Your Name
653525cfda feat: 优化详情参考图替换链路
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-25 18:12:55 +08:00
Your Name
7222c5d6dd refactor: remove style template step from workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-24 20:01:45 +08:00
Your Name
63433120e6 fix: keep world 3d assets hydrated
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-24 19:28:31 +08:00
Your Name
c7de64b3f6 perf: optimize canvas image loading
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-24 18:35:11 +08:00
Your Name
314884dc77 fix: stabilize 3d world canvas assets 2026-04-24 18:12:00 +08:00
Your Name
b4529c860c feat: polish canvas and workflow settings
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-24 17:30:11 +08:00
Your Name
f946d9c1f0 feat: add PSD text color and position editing 2026-04-24 17:28:55 +08:00
Your Name
f9b6cfdc0d fix: improve canvas chat dark mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-24 16:49:02 +08:00
Your Name
5b7d6a6e89 feat: add home theme selector and canvas polish 2026-04-24 16:36:06 +08:00
Your Name
560a1999a0 feat: improve lovart home and 3d canvas flows
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-24 12:58:33 +08:00
Your Name
d6d8bf021f feat: add bltcy unified video provider
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-23 18:24:39 +08:00
Your Name
0808d1963b feat: add grsai video provider type
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-23 15:20:58 +08:00
Your Name
e58d37015e fix: route grsai gpt image provider
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-23 14:33:20 +08:00
Your Name
85c0f2c096 fix: allow large workflow exclusion lists
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-23 13:29:47 +08:00
Your Name
3101f5966d feat: enhance canvas agent UI customization
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-23 13:25:05 +08:00
Your Name
ccb62d6f7d Add canvas 3D generation actions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-23 12:38:33 +08:00
Your Name
40ec8c412e fix: apply workflow exclusions to previous selections
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-23 10:42:17 +08:00
Your Name
b438977a68 fix: stabilize workflow settings editing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-22 20:16:31 +08:00
Your Name
28a2531082 fix: show special module toggle on workflow modules
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-22 19:44:34 +08:00
Your Name
885708b2ec feat: add special workflow module exclusions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-22 19:16:02 +08:00
Your Name
aaba40b2d6 feat: add workflow configuration dashboard
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-22 11:19:39 +08:00
Your Name
1103ff957f feat: allow workflow step level adjustment
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-21 19:48:04 +08:00
Your Name
396bbf9d9b fix: reduce workflow settings input lag
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-21 19:25:46 +08:00
Your Name
7a2169576d fix: improve workflow settings typing performance
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-21 18:19:57 +08:00
Your Name
8a722ed849 fix: stabilize enhance preview compare
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-21 01:31:38 +08:00
Your Name
82dda143eb fix: load 3d viewers on client
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-21 01:07:08 +08:00
Your Name
edc3ae59b1 fix: improve canvas mode settings
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-21 00:55:35 +08:00
Your Name
e92e0b24f6 feat: improve 3d canvas controls and points visibility
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-20 21:43:11 +08:00
Your Name
1cda4fa23e fix: settle fallback model billing and worldlabs key pool
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-20 15:45:07 +08:00
Your Name
6628a6b2d0 feat: enhance workflow controls and media settings
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-20 14:39:41 +08:00
Your Name
3775485bdf feat: add custom realtime mode icons
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-20 10:41:29 +08:00
Your Name
7164304451 fix: show direct upload in workflow style step
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-19 21:17:43 +08:00
Your Name
90b6cd442c fix: keep upload access in direct workflow mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-19 16:15:55 +08:00
Your Name
076e4f55e6 feat: add workflow page feature toggles
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-19 15:36:29 +08:00
Your Name
9422c5de73 Wire video canvas model selectors
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-19 14:44:27 +08:00
Your Name
af74e2197b Stop auto opening canvas inspector
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-19 11:52:10 +08:00
Your Name
79fffd88f4 Use configurable video canvas prompts
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-19 11:31:44 +08:00
Your Name
7ee5dbc264 Fix custom grid split menu
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-19 11:03:12 +08:00
Your Name
7a1a478eaf Improve nine grid storyboard generation
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-19 10:52:54 +08:00
Your Name
f3869f5e63 Add canvas generation pending placeholders
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-19 10:22:52 +08:00
Your Name
e21f99eaca Scale image toolbar dropdowns
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-19 10:01:26 +08:00
Your Name
8a3f0bea15 Add pure text resource nodes
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-19 09:50:49 +08:00
Your Name
3a4604286f Normalize video canvas composer scaling
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-19 09:41:30 +08:00
Your Name
193b03d677 Fix text module composer replica
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-19 09:04:55 +08:00
Your Name
37b7e58f2a Refine video canvas function replica
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-19 08:52:38 +08:00
Your Name
a4c1fbca39 Refine video canvas replica details
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-19 00:33:58 +08:00
Your Name
3f1923701b Use site branding on video canvas
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 23:16:01 +08:00
Your Name
df9a28d752 Replica LibTV image toolbar menus
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 22:44:30 +08:00
Your Name
4acf95490d Match LibTV toolbar icons and edge glow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 22:34:43 +08:00
Your Name
78c7395e75 Keep LibTV composer submit inside panel
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 22:30:01 +08:00
Your Name
4838bbe73e Tune LibTV composer size and image dragging
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 22:03:20 +08:00
Your Name
d8517449ac Fix LibTV composer overlay scale
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 21:56:40 +08:00
Your Name
0c12132bb4 Align LibTV image composer overlay
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 21:38:20 +08:00
Your Name
7bc4e768b6 Improve LibTV video canvas replica
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 21:02:39 +08:00
Your Name
3278776e7f Restore LibTV module action workflows
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 18:31:31 +08:00
Your Name
5ba8900438 fix: wire libtv connection controls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 17:46:48 +08:00
Your Name
3d091a665b fix: refine canvas toolbar layout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 17:43:16 +08:00
Your Name
d2199975a5 Match LibTV added module node cards
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 17:27:19 +08:00
Your Name
46cd4a5899 Align LibTV add node modules
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 17:06:27 +08:00
Your Name
d348be844b Add LibTV composer trigger interactions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 16:38:21 +08:00
Your Name
6f64164109 Anchor preset controls to selected media
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 16:27:55 +08:00
Your Name
8784432f23 Scale LibTV preset controls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 16:17:58 +08:00
Your Name
e04288bc88 Refine LibTV preset canvas interactions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 16:06:14 +08:00
Your Name
cfd48b6b3b Load LibTV starter workflows on canvas
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 15:49:06 +08:00
Your Name
c3b4e35997 Match LibTV account and member panels
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 15:25:52 +08:00
Your Name
d7f50984b4 Match LibTV share publish flow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 15:09:48 +08:00
Your Name
e6c8c140d8 Match LibTV settings modals
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 14:57:14 +08:00
Your Name
d04f43c464 fix: use icon-only image params control
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 14:56:33 +08:00
Your Name
b24acff972 Add LibTV canvas command interactions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 14:51:07 +08:00
Your Name
db657b3ea5 Expand LibTV starter interactions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 14:39:21 +08:00
Your Name
4551eee9d1 Match LibTV character preset canvas
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 12:53:54 +08:00
Your Name
ce98b117b7 Refine LibTV video canvas clone
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 12:13:24 +08:00
Your Name
aebdb72564 fix: refine canvas composer controls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 12:10:00 +08:00
Your Name
fd95b9547e feat: show generation point estimates
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 11:58:37 +08:00
Your Name
799dd3007b Replicate target video canvas frontend
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 11:50:04 +08:00
Your Name
0edd9872ea feat: add BLTCY Flux image providers
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-18 10:29:30 +08:00
Your Name
0b1062a31f fix: show workflow draft recovery action
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-16 19:11:01 +08:00
Your Name
8c8d2c07f5 fix: protect workflow settings from overwrites
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-16 19:02:53 +08:00
Your Name
c5a4b4add2 fix: preserve video duration pricing curve
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-16 15:12:56 +08:00
Your Name
c63f163b0e fix: align video and enhance pro point pricing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-16 15:09:27 +08:00
Your Name
169bf2fe4c Fix PSD template preview fallback
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-16 10:32:52 +08:00
Your Name
2a793dcbba fix: hide video canvas entry when disabled
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-15 21:25:24 +08:00
Your Name
71c537fdb7 refactor: simplify psd text assignment checks
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-15 21:17:20 +08:00
Your Name
71997086e1 fix: improve psd template text rendering
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-15 21:15:43 +08:00
Your Name
bc8c3084fe fix: align krea seedance video payload 2026-04-15 20:42:28 +08:00
Your Name
8e51697800 feat: refine krea enhance pro billing 2026-04-15 20:29:07 +08:00
Your Name
f75f77105d fix: preserve selected image context for generation
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-15 18:53:14 +08:00
Your Name
f112a63c75 fix: allow longer workflow prompt fragments
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-14 18:44:28 +08:00
Your Name
672632ffe0 fix: preserve workflow step children on save
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-14 18:15:53 +08:00
Your Name
7d5aabe803 feat: enhance workflow step editor
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-13 17:00:06 +08:00
Your Name
dfc5f704b4 chore: sync latest app updates
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-12 23:45:00 +08:00
Your Name
1b239eb2c7 fix: improve flow board mind tools
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-12 01:37:27 +08:00
Your Name
9a562db15a fix: align flow board floating sidebar
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-12 01:05:57 +08:00
Your Name
3f4d850815 refactor: share floating sidebar shell
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-12 00:23:45 +08:00
Your Name
3c45bdfef9 fix: move flow board toolbar to sidebar
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-12 00:03:21 +08:00
Your Name
0870739a7d fix: stabilize flow board node insertion
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-11 23:36:46 +08:00
Your Name
3ad76b5c54 fix: stabilize video canvas quick connect
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-11 23:14:05 +08:00
Your Name
e054dcabcc feat: update canvas and flow board features
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-11 22:28:18 +08:00
Your Name
d88c28356a feat: add flow board with database drafts
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-11 20:48:46 +08:00
Your Name
36f7eea13b fix: resync realtime canvas after mode switch
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-11 20:16:59 +08:00
Your Name
8757f76b1f chore: rename agent UI to stylized
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-11 19:48:45 +08:00
Your Name
fe5d571190 fix: keep video duration input separators
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-11 19:05:55 +08:00
Your Name
f69cca1da6 feat: add local AI key pools
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-11 16:25:10 +08:00
Your Name
99c2edb1d4 fix: support custom video duration tiers
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-11 13:42:32 +08:00
Your Name
0d268a0e31 fix: make shared overlays opaque
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-11 13:28:46 +08:00
Your Name
3569582b43 fix: clamp canvas image toolbar inside viewport
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-11 12:57:57 +08:00
Your Name
d6149bd05d fix: remove canvas brand capsule shadow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-11 11:56:24 +08:00
Your Name
167f6d4aff fix: show canvas brand capsule background
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-11 11:42:13 +08:00
Your Name
6cdf3d013e fix: scale canvas image controls with zoom
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-11 11:28:22 +08:00
Your Name
0d3d5a9507 fix: polish PSD editor scrolling and dimension styles
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-11 00:08:29 +08:00
Your Name
c9c78f20d4 fix: stabilize PSD render asset downloads
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 23:45:52 +08:00
Your Name
649825072b feat: add async PSD render task pipeline
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 23:29:46 +08:00
Your Name
6648f43a20 feat: queue psd template exports
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 23:04:38 +08:00
Your Name
411bb4a5eb fix: normalize rate limit errors across chat and PSD flows
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 22:49:26 +08:00
Your Name
03f07564e0 feat: customize PSD dimension annotations
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 22:21:31 +08:00
Your Name
df6694de5d perf: speed up PSD template loading
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 22:01:29 +08:00
Your Name
10dc87eaf7 fix: stabilize psd text font rendering
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 21:55:26 +08:00
Your Name
e84bfd94ec feat: split PSD parsing into fast parse and async backfill
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 21:22:36 +08:00
Your Name
8c8b1794e4 fix: stabilize psd template admin and export render
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 21:16:57 +08:00
Your Name
db46934025 fix: stabilize PSD template parsing workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 21:03:59 +08:00
Your Name
e74586c8a0 fix: remove psd runtime source fallback
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 20:59:43 +08:00
Mosijiu
ebbab98a41 多组织选择修复
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 20:38:54 +08:00
Your Name
d450bec1d7 fix: stabilize psd text preview positioning
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 20:35:59 +08:00
Your Name
9b745e5591 fix: improve psd text font recovery
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 20:19:21 +08:00
Your Name
aa885a3d48 fix: persist PSD editor uploads and results
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 20:17:51 +08:00
Your Name
ded97bef00 fix: improve psd image action button labels
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 19:58:56 +08:00
Your Name
0464306cc0 fix: refine PSD template editor interactions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 19:56:00 +08:00
Your Name
0e95d9ec29 fix: speed up psd template dialog load
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 19:55:50 +08:00
Your Name
25132540bc fix: show psd image tools in editor panel
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 19:48:59 +08:00
Your Name
477f215259 feat: update video billing and agent workspace improvements
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 16:25:35 +08:00
Your Name
02f13a045a fix: stabilize psd template export rendering
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 14:28:19 +08:00
Your Name
b6e1a6c657 feat: add enhance pro points pricing rules
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 14:05:23 +08:00
Your Name
240b367acd fix: center world orbit rotation on view
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-10 13:30:16 +08:00
Your Name
27d44b3b77 feat: add configurable 3d model display aliases
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-08 22:09:43 +08:00
Your Name
1a71072497 fix: improve world 3d controls and restore mesh selection
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-08 18:10:36 +08:00
Your Name
59b28461f9 fix: remove rounded corners from canvas images
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-07 21:16:59 +08:00
Your Name
a26e76638e feat: add ai-cad camera orientation cube
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-07 12:14:57 +08:00
Your Name
aede89654f style: simplify realtime enhance ui to monochrome 2026-04-07 12:08:12 +08:00
Your Name
ed23cd3f4a tweak: widen ai-cad chat pane
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-07 00:55:05 +08:00
Your Name
01d3f2f23e refactor: remove ai-cad header chrome
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-07 00:50:28 +08:00
Your Name
6d07b1a915 refactor: simplify ai-cad workbench chrome
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-07 00:36:41 +08:00
Your Name
c74dffdcd6 Add video canvas sidebar setting
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-07 00:30:47 +08:00
Your Name
be8cb630f9 Refine video canvas sidebar UI
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-07 00:20:07 +08:00
Your Name
ca0fbc5fd4 Scale node content when video canvas modules shrink 2026-04-06 23:56:44 +08:00
Your Name
5763ed1c94 feat: add official AI-CAD iteration and modeling tools 2026-04-06 23:55:22 +08:00
Your Name
b542a80f89 Restyle video canvas editor UI
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 23:30:44 +08:00
Your Name
cd54fcf792 refactor: redesign realtime enhance workspace 2026-04-06 23:21:46 +08:00
Your Name
a17a035689 refactor: beautify realtime enhance workspace ui
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 23:01:05 +08:00
Your Name
8fec20b3be fix: stretch AI-CAD workspace to viewport
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 22:32:22 +08:00
Your Name
e8279ead52 refactor: merge AI-CAD toolbar into single row
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 22:23:19 +08:00
Your Name
47085819f6 fix: align AI-CAD panel headers
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 22:17:43 +08:00
Your Name
00464f518f refactor: tighten AI-CAD viewport layout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 22:10:51 +08:00
Your Name
0414983f64 refactor: compact AI-CAD top toolbar
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 22:03:12 +08:00
Your Name
394a9761f1 refactor: replicate official AI-CAD workbench shell
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 21:38:10 +08:00
Your Name
12bc8b1668 refactor: streamline AI-CAD into single-screen workspace
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 21:15:41 +08:00
Your Name
9d222f0d42 feat: simplify AI-CAD into canvas-first workbench
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 20:59:09 +08:00
Your Name
6c7f02c142 refactor: white-label 3d world workspace
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 20:16:50 +08:00
Your Name
7eba6d2fb8 feat: restructure AI-CAD workbench to match CAD tool layout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 18:47:07 +08:00
Your Name
9f5dc2e666 feat: add home back button to ai-cad
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 16:42:46 +08:00
Your Name
f42ade947c fix: remove chat sidebar from ai-cad
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 16:28:48 +08:00
Your Name
450e998d3f fix: give ai-cad a dedicated shell layout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 16:07:26 +08:00
Your Name
9d3ba9b353 fix: compact ai-cad workspace chrome
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 15:37:37 +08:00
Your Name
b353bc8e48 fix: restore ai-cad panel scrolling
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 15:21:20 +08:00
Your Name
3de40dc959 feat: expose ai-cad from home sidebar
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 15:04:30 +08:00
Your Name
413b5ffac1 feat: add AI-CAD workspace and KittyCAD integration
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 14:46:51 +08:00
Your Name
276da822a6 refactor: redesign enhance workspace control sidebar
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 02:31:37 +08:00
Your Name
14009f869e fix: keep world splat loading responsive 2026-04-06 02:31:33 +08:00
Your Name
58b4fc49b9 fix: restore smooth world orbit dragging
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 02:24:19 +08:00
Your Name
e21e3896a2 fix: smooth world splat camera dragging
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 02:15:12 +08:00
Your Name
cd1256d816 fix: stabilize world splat reveal loading
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 02:08:28 +08:00
Your Name
b6b14d739f fix: start world view inside scene
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 01:54:45 +08:00
Your Name
94478e469f refactor: polish realtime enhance workspace layout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 01:50:06 +08:00
Your Name
97f3bf2c20 fix: tune granular splat reveal timing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 01:44:36 +08:00
Your Name
5363a62e0e feat: add granular splat reveal transition
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 01:31:58 +08:00
Your Name
9281df46fe feat: improve real splat world reveal
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 01:04:46 +08:00
Your Name
62b5b75ec9 feat: add 3d splat assembly loading effect
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 00:30:40 +08:00
Your Name
0d5a69e543 feat: add splat loading overlay transition
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-06 00:24:28 +08:00
Your Name
b3b49b4fe2 feat: improve worldlabs admin controls and splat selection
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 23:42:37 +08:00
Your Name
c4aafe9413 feat: store worldlabs import token in db
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 23:15:25 +08:00
Your Name
078a115cfd feat: add worldlabs key pool import api
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 23:08:04 +08:00
Your Name
df1b1a017e style: unify realtime canvas control colors
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 23:00:08 +08:00
Your Name
ed827b629a refactor: float enhance controls over canvas
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 22:34:57 +08:00
Your Name
b0245c5366 refactor: unify realtime canvas enhance panel styling
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 22:26:05 +08:00
Your Name
3100d10dc0 fix: restore light theme for realtime canvas enhance mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 22:09:21 +08:00
Your Name
9c8228b697 fix: compact enhance pro workspace layout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 21:59:05 +08:00
Your Name
5c2ece9fb6 feat: split 3d model and world point costs 2026-04-05 21:54:11 +08:00
Your Name
607fd3bc3e fix: allow public world share pages
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 21:38:05 +08:00
Your Name
39324778f5 fix: keep home sidebar growing downward 2026-04-05 21:23:53 +08:00
Your Name
b6e0a59d5b feat: add world sharing workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 21:23:27 +08:00
Your Name
b3d9b70ed4 feat: move enhance pro into realtime canvas workspace
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 21:12:24 +08:00
Your Name
07bf4a293c fix: correct world splat orientation and movement
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 20:36:58 +08:00
Your Name
021f497970 Simplify video workflow canvas UI
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 09:56:51 +08:00
Your Name
20d0529365 Add in-canvas module delete actions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 03:37:04 +08:00
Your Name
3876a0c403 Add collapsible video workflow sidebar
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 03:32:41 +08:00
Your Name
d0c26e9fab Add inline workflow quick connect bubbles
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 03:24:04 +08:00
Your Name
584579e987 Add video canvas double-click insert menu
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 03:14:33 +08:00
Your Name
e60177c177 Polish video canvas surface consistency
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 03:00:29 +08:00
Your Name
a787a97221 Make video canvas surfaces opaque
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 02:46:07 +08:00
Your Name
b62c97dcde Remove overlapping video canvas badge
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 02:35:52 +08:00
Your Name
f6b9a6f73b Refine video canvas workspace UX
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 02:26:18 +08:00
Your Name
be5b9aef12 chore: sync latest workspace changes
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 02:08:26 +08:00
Your Name
d34e66da25 fix: add missing canvas clarification gate
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 01:30:40 +08:00
Your Name
3a087d3392 fix: stabilize canvas agent mode for kimi
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-05 01:28:28 +08:00
Your Name
2a322feea9 fix: use per-card local ai setting toggles
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-04 23:23:03 +08:00
Your Name
033c4f2fa4 feat: collapse local ai settings by default
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-04 23:03:05 +08:00
Your Name
67602b0af7 feat: add native world3d canvas workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-04 22:59:00 +08:00
Your Name
670a50c4d0 feat: add enhance pro canvas workflow and presets
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-04 22:40:50 +08:00
Your Name
d76b31952f feat: add world labs 3d workspace mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-04 22:06:43 +08:00
Your Name
9b0f762654 fix: simplify workflow prompt composition
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-03 20:40:11 +08:00
Your Name
2eb8d7ad67 fix: preserve agent workflow state and auth prompts
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-03 20:00:15 +08:00
Your Name
9362b25593 fix: 支持追加队列批量上传PSD字体
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-03 19:50:49 +08:00
Your Name
3ee9701a50 feat: 联动PSD品类标签并支持批量上传字体
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-03 19:41:59 +08:00
Your Name
366e0e3e5c feat: enforce single active session per account
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-03 17:43:03 +08:00
Your Name
90458511c1 feat: add workflow appearance recognition controls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-03 17:36:13 +08:00
Your Name
f6d8a4b8ea feat: 支持PSD模板品类标签筛选
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-03 14:30:42 +08:00
Your Name
5037a1a1eb fix: improve agent project previews and step wrapping
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-03 14:18:12 +08:00
Your Name
a197a21337 feat: 支持工作流跨级互斥与开发中卡片态
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-03 13:31:26 +08:00
Your Name
d7d33ebc4a feat: add explicit psd text color switch buttons
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-03 12:30:58 +08:00
Your Name
1cf1ba6bb6 feat: optimize scene workflow prompt composition
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-03 12:24:14 +08:00
Your Name
3af9990311 feat: add SKU appearance constraints via Qwen VL
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-03 11:43:28 +08:00
Your Name
bcb0006a9e fix: stop psd replacements inheriting source masks
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-03 11:36:07 +08:00
Your Name
1963c84d7c feat: add workflow brand tree reset action
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 22:04:35 +08:00
Your Name
427e99b51e style: refine PSD dimension annotation sizing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 21:58:09 +08:00
Your Name
b26f46cc80 fix: use cm dimensions in PSD annotation labels
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 21:47:27 +08:00
Your Name
d53308126d style: match PSD dimension lines to reference style
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 21:33:55 +08:00
Your Name
cc53eee037 feat: support reordering workflow modules
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 21:06:22 +08:00
Your Name
83c31b600c feat: add sku PSD dimension annotations
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 21:01:11 +08:00
Your Name
5ce1e48392 feat: customize workflow category labels
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 20:52:36 +08:00
Your Name
ba539ce236 fix: allow saving imported workflow brand trees
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 19:25:01 +08:00
Your Name
0f1548fdc9 feat: split psd template picker by mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 18:48:48 +08:00
Your Name
11ab9a8e03 feat: organize psd assets by brand and harden template editor
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 18:29:41 +08:00
Your Name
f4d488d104 feat: align psd templates with brand category config
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 17:48:41 +08:00
Your Name
a90e28d115 fix: polish psd editor and settings layout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 15:09:20 +08:00
Your Name
ab0dc8bce3 feat: add brand import and brand-aware psd settings
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 14:42:18 +08:00
Your Name
ea6a9a9d76 feat: extend workflow brand hierarchy to five levels
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 13:59:33 +08:00
Your Name
7eda4720d3 feat: extend workflow category and card controls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 12:35:24 +08:00
Your Name
f0086e26da fix: preserve secondary sku scene references
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 12:06:10 +08:00
Your Name
5e51cd8ca4 fix: simplify psd editor chrome
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 10:51:33 +08:00
Your Name
a63db2af6b fix: streamline psd editor layout and scaling
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 10:40:26 +08:00
Your Name
8c0ce434dc fix: correct psd masked image placement
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 10:14:45 +08:00
Your Name
305dd21dd5 feat: enhance psd template editor workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 09:35:34 +08:00
Your Name
e7cc0c9555 fix: align workflow material upload limits
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 00:24:16 +08:00
Your Name
01748fe501 fix: harden psd text workflow compatibility
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-02 00:08:53 +08:00
Your Name
3e935fae37 feat: add workflow material upload cutout flow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-01 23:36:05 +08:00
Your Name
117621b8cb fix: relax psd text layout bounds
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-01 23:05:56 +08:00
Your Name
bc432ffb44 fix: preserve psd text and layer exports
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-01 22:50:24 +08:00
Your Name
fb6b2170bd fix: harden psd template rendering
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-01 22:36:45 +08:00
Your Name
494a3c6d48 fix: stop psd dialog update loop
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-01 22:04:53 +08:00
Your Name
8853bfae77 fix: harden agent PSD template dialog
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-01 21:52:25 +08:00
Your Name
d02416d1b2 回退不必要的依赖项
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 21:04:56 +08:00
Your Name
b095ed40b8 修复useEffect缺少依赖项
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 21:02:23 +08:00
Your Name
c202dd48a8 feat: redesign psd scale control and fix >100% scaling behavior
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-01 19:41:13 +08:00
Your Name
ae254d7bc5 修复图片缩放偏移问题
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
- 缩放时按比例调整偏移量
- 保持图片中心点不变

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 19:15:36 +08:00
Your Name
e6b6cbf0c7 修复Fragment闭合标签
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 19:12:02 +08:00
Your Name
b188352fda 修复style标签语法错误
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 19:04:50 +08:00
Your Name
80db2de86a 使用CSS样式实现滚动条美化
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
- 改用style标签实现滚动条样式
- 确保样式正确生效

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 19:03:44 +08:00
Your Name
89c9d1455a 优化PSD模板图片缩放和滚动条样式
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
- 修复图片缩放以中心点等比例缩放
- 美化滚动条样式,提升可见性

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 18:52:29 +08:00
Your Name
a2bd22c4e1 修复PSD模板画布自动适配时机问题
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
- 添加延迟执行确保DOM已渲染
- 添加尺寸有效性检查

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 18:39:55 +08:00
Your Name
d4033cc201 修复PSD模板画布自动适配显示
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
- 添加自动缩放逻辑,确保模板完整显示在预览窗口内
- 避免模板超出预览区域,提升用户体验

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 18:36:00 +08:00
Your Name
046fff7f2e fix: harden psd template dialog against malformed payload
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-01 17:44:03 +08:00
Your Name
49b54c2387 fix: guard psd template dialog runtime errors
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-04-01 13:20:29 +08:00
Your Name
89dfc39284 fix: stabilize psd template preview size
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-31 20:58:02 +08:00
Your Name
dceebf0652 feat: add on-canvas zoom controls for psd image slots
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-31 20:47:42 +08:00
Your Name
dc04c1d91b feat: support dragging and scaling psd image slots
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-31 20:40:06 +08:00
Your Name
d1ee239fd6 fix: preserve psd text padding alignment
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-31 20:25:07 +08:00
Your Name
43083ff90c fix: use exact uploaded font face for psd export
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-31 18:56:40 +08:00
Your Name
c8ac5584c3 fix: use uploaded fonts when exporting psd text
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-31 18:46:50 +08:00
Your Name
59e26064e5 fix: add cjk fallback fonts for psd export
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-31 18:25:15 +08:00
Your Name
cc01b539bc fix: move psd text helper badges out of content
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-31 18:18:23 +08:00
Your Name
14cd997a43 fix: stabilize psd text edit styling
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-31 18:08:32 +08:00
Your Name
baad844495 chore: clarify fal realtime rollout messaging
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-31 14:01:42 +08:00
Your Name
cd91094b0f feat: add fal provider for realtime canvas
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-31 13:36:28 +08:00
Your Name
ade5344163 feat: add layer split import to realtime canvas 2026-03-29 14:13:37 +08:00
Your Name
9a1c7a8f3a fix: preserve psd text appearance while editing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-29 10:59:27 +08:00
Your Name
a8e5252241 fix: restore psd text layer inline editing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-29 09:47:21 +08:00
Your Name
6d281e950a fix: improve psd editor dialog layout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-29 09:40:32 +08:00
Your Name
80db6cd48d fix: preserve psd layer stack in editor preview
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-28 20:53:00 +08:00
Your Name
1a82296133 fix: simplify material search status hints
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-28 18:24:25 +08:00
Your Name
f8900a0d72 fix: move material fetch success state into input
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-28 18:00:08 +08:00
Your Name
37b22b583f fix: remove workflow depth badge from timeline
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-28 17:46:36 +08:00
Your Name
9fc4790722 fix: tighten workflow timeline spacing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-28 17:37:37 +08:00
Your Name
c2527027d6 fix: normalize workflow timeline chip alignment
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-28 17:26:55 +08:00
Your Name
d393b5f138 fix: align workflow timeline labels inline
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-28 17:15:47 +08:00
Your Name
2b2b2bd228 feat: add in-canvas PSD template editing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 23:05:45 +08:00
Your Name
449a631db4 fix: use PSD text metrics for template rendering
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 22:00:05 +08:00
Your Name
eb6a0bf7dd fix: upload realtime replicate inputs before inference
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 21:48:49 +08:00
Your Name
9965ead31a fix: preserve PSD layer order in template rendering
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 21:44:53 +08:00
Your Name
510b0c13a6 feat: clarify editable PSD slots in agent dialog
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 21:36:50 +08:00
Your Name
e562e48c13 fix: surface parsed PSD templates in agent dialog
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 21:27:04 +08:00
Your Name
e23912d48f fix: clip PSD overlays to canvas bounds
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 20:50:07 +08:00
Your Name
ed7534a1ff fix: expose PSD parse action in upload panel
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 20:43:41 +08:00
Your Name
bfbad743c4 feat: add manual PSD parsing workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 20:31:04 +08:00
Your Name
93e2c5d892 feat: auto match PSD text fonts
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 19:38:47 +08:00
Your Name
30b0d972e9 fix: backfill missing PSD layer previews
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 19:24:13 +08:00
Your Name
e2698a7a49 feat: improve PSD template previews and deletion
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 19:17:35 +08:00
Your Name
305e1e5161 feat: filter PSD templates by brand
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 16:46:56 +08:00
Your Name
954d708e30 Restore generic agent mode messaging
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 11:56:06 +08:00
Your Name
a219397344 Allow entering agent mode without preset
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 11:48:19 +08:00
Your Name
1f864294c9 Fix stale agent mode fallback and error messaging
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 11:35:49 +08:00
Your Name
25b20e1730 Fix agent workspace chat request validation
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 11:18:11 +08:00
Your Name
d133af4cbe Fix agent mode chat request sanitization
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 11:07:41 +08:00
Your Name
17dcfcd55d feat: add PSD template font rendering workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 11:01:13 +08:00
Your Name
21d3eb0769 fix: show product planning entry on lovart home
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-27 10:24:19 +08:00
Your Name
97d695b476 fix resize ratio popover layering
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 21:41:13 +08:00
Your Name
f32146b3f3 compact resize ratio picker
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 21:28:04 +08:00
Your Name
6c1583a2fa fix resize slider visibility
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 21:16:26 +08:00
Your Name
b088d65a66 improve workflow timeline and resize controls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 21:01:10 +08:00
Your Name
399abb1ed3 stop appending legacy scene preset prompt
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 19:37:55 +08:00
Your Name
a215ec1cbb show agent scene system lock prompts
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 19:33:56 +08:00
Your Name
bd1dbc4406 fix agent scene slot prompt dedupe
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 19:20:34 +08:00
Your Name
c16862bd90 fix agent workflow prompt state sync
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 19:00:37 +08:00
Your Name
243e12cc0e fix agent project persistence recovery
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 18:18:54 +08:00
Your Name
cd44deb8c4 feat: add dedicated model for product planning
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 17:42:38 +08:00
Your Name
1721f53248 feat agent scene result resize dialog
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 17:38:32 +08:00
Your Name
19c76950db feat: add collapsible workflow child settings
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 17:12:16 +08:00
Your Name
478562c312 feat: migrate product planning workspace
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 16:38:48 +08:00
Your Name
5f54531f75 fix agent scene preset prompt merge
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 14:16:31 +08:00
Your Name
04428bf6b2 fix: support global workflow card blocking
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 12:15:53 +08:00
Your Name
1768413eca fix agent model identity guardrails
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 00:31:42 +08:00
Your Name
177f7ec3df strengthen product identity constraints for agent scene generation
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 00:21:24 +08:00
Your Name
8df3173cb2 fix agent workflow freshness and goods info prompts
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-26 00:08:26 +08:00
Your Name
519239bcd3 fix agent image rerun preserving prompt input
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 23:52:04 +08:00
Your Name
80d862258b fix agent generation pending state on failures
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 23:31:10 +08:00
Your Name
54a4e15cf2 fix agent project menu closing after actions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 23:08:22 +08:00
Your Name
2e87ab91d3 fix agent project state sync and workflow prompt freshness
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 23:02:16 +08:00
Your Name
785177bed4 fix agent workflow prompt cleanup and card blocking
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 22:21:09 +08:00
Your Name
fb2f3461c2 fix: dedupe agent workflow scene prompts
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 21:24:14 +08:00
Your Name
23933cfb41 refactor: merge child workflow steps into top navigation
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 19:41:18 +08:00
Your Name
cb0c9ebf95 fix: hide child workflow bar for single level
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 19:33:35 +08:00
Your Name
5407ad751a feat: expand workflow tree depth and child navigation
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 18:47:58 +08:00
Your Name
de142d0be7 fix: preserve ai detail placeholder cards
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 17:54:32 +08:00
Your Name
b62926cf6e fix: prevent ai detail sync render loop
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 17:33:11 +08:00
Your Name
c823910185 fix: restore ai detail default frame count
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 17:14:27 +08:00
Your Name
f97e96c5f9 fix: sync ai detail material slots with workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 15:48:21 +08:00
Your Name
01e124ebb1 fix: mark image stats route as dynamic
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 15:42:07 +08:00
Your Name
3864cf7ff8 fix: relax canvas read api rate limits
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 15:39:54 +08:00
Your Name
23c326b1fb fix: reduce infinite canvas rate limit bursts
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 15:35:04 +08:00
Your Name
b08b98102a fix: reduce canvas request bursts
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 15:24:52 +08:00
Your Name
1269e5f562 fix: prevent canvas agent requests without preset
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 13:35:06 +08:00
Your Name
50d487dbcb fix: fallback to original agent preview images
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 13:23:25 +08:00
Your Name
bbde945bb8 fix: align reference replacement copy
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 13:04:51 +08:00
Your Name
f580a43efd fix: reorder ai detail generation controls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 11:37:56 +08:00
Your Name
e5ef86f089 fix: simplify agent material picker controls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 11:14:32 +08:00
Your Name
66d9031d89 fix: avoid agent detail mode render loop
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-25 09:53:56 +08:00
Your Name
63b77b5507 fix: move home prompt tags above animation
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 23:42:28 +08:00
Your Name
e5a07fa022 fix: refine home animation embed layout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 23:39:18 +08:00
Your Name
5568325a0d feat: add configurable home animation module
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 22:53:16 +08:00
Your Name
900a1c9e38 feat: add agent ai detail workflow mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 21:30:11 +08:00
Your Name
83102c559f feat: support deleting agent result cards
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 17:48:02 +08:00
Your Name
0a33d99c28 style: compact agent project pill
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 17:09:48 +08:00
Your Name
817309ade9 feat: add sqlserver admin connection settings
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 17:01:50 +08:00
Your Name
cfb4a0e329 fix: support agent admin chat entitlements
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 16:53:17 +08:00
Your Name
f18f9733df fix: route image presets through direct image mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 16:42:22 +08:00
Your Name
406bc6418e fix: send explicit agent composer context
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 16:18:57 +08:00
Your Name
bd0928c135 chore: add agent scene generation diagnostics
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 15:21:22 +08:00
Your Name
d73f6d6a29 fix: persist agent scene generation pending state
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 14:29:55 +08:00
Your Name
ae93dfcfda fix: improve agent project UX and placeholder persistence
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 14:06:04 +08:00
Your Name
5b30c2616b fix: keep generation placeholders active during processing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 04:09:50 +08:00
Your Name
bb86d1fb1a fix: place layer split group beside source image
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 04:06:20 +08:00
Your Name
33fcb34900 fix: stack layer split results as full-frame overlays
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 03:55:52 +08:00
Your Name
8af70192f8 fix: use original urls in agent result previews
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 03:53:23 +08:00
Your Name
3e7e5d1177 fix: migrate legacy agent results by mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 03:44:51 +08:00
Your Name
0b01f6620b fix: keep background in layer split group
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 03:40:10 +08:00
Your Name
0f32383d7d feat: organize agent results by generation mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 03:36:30 +08:00
Your Name
d2b1fd22d0 fix: crop layer split overlays by bounds
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 03:26:45 +08:00
Your Name
60c9c57263 fix: pass layer split bounds to canvas overlay
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 03:13:31 +08:00
Your Name
15c57afdfa fix: preserve layer split overlay positions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 03:07:40 +08:00
Your Name
365eabead1 fix: stop canvas reload effect loop
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 02:53:01 +08:00
Your Name
f1ffeed9f2 feat: add canvas layer split workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 02:07:45 +08:00
Your Name
14b6b3ec10 fix: preserve workflow prompt state in agent mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 01:45:20 +08:00
Your Name
328f1f4915 fix: strengthen detail replacement scene rebuild
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-24 00:23:14 +08:00
Your Name
b40df45daa fix: harden detail reference stitch loading
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-23 23:49:00 +08:00
Your Name
4afced3b8d feat: strengthen scene-style constraints for detail replacement
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-23 23:12:55 +08:00
Your Name
ed6ebbf80a fix: limit kimi planning to reference image and extend timeout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-23 22:42:51 +08:00
Your Name
d2aa27b1c2 feat: add scene-aware detail replacement planning
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-23 21:13:20 +08:00
Your Name
53f8cd7052 fix: preserve live agent mode payload in workspace sends
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-23 20:28:41 +08:00
Your Name
1eb05c980d fix: optimize replacement prompt and preserve reference size
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-23 20:00:34 +08:00
Your Name
63a01e6a5a split canvas agent mode toggle from agent workspace
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-23 20:00:05 +08:00
Your Name
1c1f7026d5 fix: keep first detail reference image
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-23 19:07:38 +08:00
Your Name
583145c97c fix: improve detail replacement result sync
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-23 18:38:16 +08:00
Your Name
ccfaef9df9 feat: add detail reference stitch workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-23 17:36:55 +08:00
Your Name
01cce4ac55 feat: add workflow price bands and fix agent slots
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-23 13:48:37 +08:00
Your Name
8aa0595741 feat: collapse auto-selected workflow panels
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-23 11:18:05 +08:00
Your Name
cb4cf50337 feat: refine agent workflow step timeline
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-23 10:15:15 +08:00
Your Name
467de8ff1e feat: streamline agent detail selection workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-23 05:23:55 +08:00
Your Name
dc1665d7ba fix: restore sku goods info display
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-22 23:54:49 +08:00
Your Name
8e225e9ae1 feat: add sku goods info query support
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-22 23:39:16 +08:00
Your Name
47df3b644c fix: unlock detail reference preview layout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-22 23:09:12 +08:00
Your Name
4d1d973437 fix: restyle detail reference candidate cards
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-22 22:10:50 +08:00
Your Name
6e493cf589 fix: streamline detail candidate search layout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-22 21:56:41 +08:00
Your Name
1826cbf6b8 fix: align detail query control heights
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-22 21:44:13 +08:00
Your Name
3449a1d82b fix: compact detail reference candidate cards
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-22 21:37:07 +08:00
Your Name
b98196b96e fix: refine detail replacement workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-22 21:25:58 +08:00
Your Name
29f94248f4 fix: stream agent replacement results incrementally 2026-03-22 20:43:50 +08:00
Your Name
5fc5a5e47c feat: automate agent material selection flow 2026-03-22 19:43:31 +08:00
Your Name
9276e6c35c feat: accept kimi direct detail dsl output
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-22 12:42:33 +08:00
Your Name
05cf3ae54e feat: add detail ui dsl v2 renderer
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-22 12:23:37 +08:00
Your Name
ac02644d3f fix: scale detail overlay with canvas zoom
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-22 03:12:15 +08:00
Your Name
409770d386 feat: add subject-aware detail overlay layout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-22 03:00:56 +08:00
Your Name
139b14b3c6 feat: add subject-safe detail overlay templates
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-22 02:29:09 +08:00
Your Name
be58859906 feat: add detail overlay design tokens
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-22 02:16:55 +08:00
Your Name
f40489d4d7 feat: add detail surface overlay renderer
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-22 01:34:50 +08:00
Your Name
54f02be0f5 feat: refine detail catalog component layouts
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-22 00:28:58 +08:00
Your Name
2f1eff956a feat: add detail component catalog renderer
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 23:51:40 +08:00
Your Name
a1b6496976 feat: upgrade detail section component renderer
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 23:35:48 +08:00
Your Name
d8aa1d1661 fix: use kimi-safe detail temperatures
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 23:13:02 +08:00
Your Name
da3f4fad64 feat: support configurable agent reference sources
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 23:04:59 +08:00
Your Name
8d602bf543 fix: persist moonshot detail config to system settings
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 23:00:20 +08:00
Your Name
a2de2c6d6d fix: align moonshot detail config key
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 22:42:08 +08:00
Your Name
aa9abff5af fix: hide agent upload slots by default
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 22:36:02 +08:00
Your Name
b99eb6cf7a fix: enable local moonshot detail agent option
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 22:27:00 +08:00
Your Name
b702440347 refactor: simplify agent workspace header
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 22:23:42 +08:00
Your Name
c0d4375daa feat: improve agent workflow guidance
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 22:10:14 +08:00
Your Name
656aacbd01 feat: add moonshot detail agent model config
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 21:50:08 +08:00
Your Name
9a96824e2b fix: align agent preset detail search and timezone
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 21:14:11 +08:00
Your Name
043bbab84d feat: streamline agent mode selection flow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 21:03:55 +08:00
Your Name
0fab83251c chore: register detail surface migration
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 20:57:07 +08:00
Your Name
10e1096c83 feat: add detail surface protocol workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 20:55:04 +08:00
Your Name
f082c20141 fix: auto-fill brand model in agent workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 20:21:31 +08:00
Your Name
de947dd5aa style: tighten embedded preset cards layout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 19:32:19 +08:00
Your Name
3e4594273b feat: add detail ui protocol foundation
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 19:30:57 +08:00
Your Name
8bd15f25c6 fix: import preset mode config in canvas agent page
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 19:24:50 +08:00
Your Name
c608882f9f feat: limit workflow material selection by mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 19:18:15 +08:00
Your Name
2ae376bdfd feat: move brand setup to first step
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 19:03:12 +08:00
Your Name
5c10380666 feat: improve replacement workflow interactions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 18:36:12 +08:00
Your Name
ea06999769 fix: restore themed select dropdown backgrounds
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 17:42:17 +08:00
Your Name
52add30784 feat: refine workflow showcase particle banner
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 12:55:09 +08:00
Your Name
e2f4d5a3db feat: polish agent workflow material and banner ui
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 12:41:47 +08:00
Your Name
21a915cb13 fix: refine agent workflow step navigation
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 12:32:57 +08:00
Your Name
9631c10812 feat: unify agent workflow into anchored single page
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 12:17:16 +08:00
Your Name
44b2148bef feat: rename my modes tab and soften banner effect
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 11:30:56 +08:00
Your Name
ff4f48bffc feat: use original particle scatter banner effect
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 11:22:56 +08:00
Your Name
f83230d7e9 feat: add particle motion to agent showcase banner
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 11:05:37 +08:00
Your Name
fe8a8564e7 Update agent visibility fixes and local docs
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 02:01:56 +08:00
Your Name
ccc2e45b12 Add BFL image runtime provider
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 01:36:53 +08:00
Your Name
dbfc8e97d2 fix: align runway video params with model capabilities
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 01:15:24 +08:00
Your Name
fc88bbb3d0 Allow BFL image provider in DB constraint
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 01:13:35 +08:00
Your Name
9727da4072 Add BFL image provider option
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 01:05:40 +08:00
Your Name
404e50a4d6 feat: validate runway api key format
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 00:41:23 +08:00
Your Name
1eb23a015f fix: route runway video models through video branch
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 00:36:46 +08:00
Your Name
7e7685410f fix: remove stale canvas video provider references
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 00:30:54 +08:00
Your Name
b22c4c5c2f fix: include runway models in canvas video mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-21 00:18:26 +08:00
Your Name
dae5581c76 fix: recognize runway video models in canvas chat
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-20 23:51:43 +08:00
Your Name
7e7c7f3a1a feat: clarify admin model type labels
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-20 23:43:00 +08:00
Your Name
947af6d26f feat: add runway image and video runtime support
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-20 22:33:09 +08:00
Your Name
f09d8f1d25 feat: add runway provider option in admin settings
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-20 21:51:12 +08:00
Your Name
f5a360010c style: improve agent slot label readability
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-20 19:28:44 +08:00
Your Name
c6b250e0d6 style: tighten agent image card grid
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-20 19:12:05 +08:00
Your Name
c754383ffc feat: refine agent generation workflow ui
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-20 17:55:49 +08:00
Your Name
8d2066a3bf feat: expand agent workflow and project management
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-19 18:33:42 +08:00
Your Name
af7e98a4dc fix: stabilize agent mode state syncing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-19 14:17:35 +08:00
Your Name
337fd15471 fix: stabilize agent workspace result syncing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-19 13:41:28 +08:00
Your Name
ae5f1309b1 feat: improve sku material loading feedback
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-19 13:30:01 +08:00
Your Name
5d1d2eb653 fix: improve dark material picker contrast
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-19 13:23:47 +08:00
Your Name
87a6e6561d fix: compact embedded agent preset cards
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-19 13:18:10 +08:00
Your Name
98fe210b64 feat: separate agent projects from canvas storage
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-19 13:07:39 +08:00
Your Name
5be4d0e9df fix: preserve source aspect for alibaba auto size
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-19 01:08:31 +08:00
Your Name
d6a06e0cd0 fix: require explicit multimodal flags
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-19 00:46:47 +08:00
Your Name
7cd29fe2b5 fix: respect dark select dropdown styling
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-19 00:46:12 +08:00
Your Name
f2f856eec4 fix: treat multimodal image models as image mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-19 00:40:08 +08:00
Your Name
7ebfcfd4eb feat: refine agent workflow settings and studio ui
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-19 00:34:09 +08:00
Your Name
a4abda497c fix: persist composer mode during send
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-19 00:30:41 +08:00
Your Name
1a6a1a8d6e fix: isolate direct image mode context
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-19 00:12:27 +08:00
Your Name
0fa2c8b40e fix: sync composer mode with model selection
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 23:44:16 +08:00
Your Name
2a2d4f46b3 fix: stop agent generation workspace render loop
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 23:31:42 +08:00
Your Name
8a34cd4570 fix: respect direct composer execution mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 23:21:12 +08:00
Your Name
c22d9019bf fix: solidify select dropdown background
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 22:24:44 +08:00
Your Name
b6db6a671c feat: upgrade agent workflow generation modes
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 22:17:11 +08:00
Your Name
eb56b7967d feat: improve agent brand and store option editor
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 16:15:56 +08:00
Your Name
4f5d999ade style: align agent preset dialog with dark workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 15:33:57 +08:00
Your Name
89ac6d69db style: reduce agent step flow height
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 15:07:34 +08:00
Your Name
30d6e5e183 style: compact agent step flow header
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 14:46:38 +08:00
Your Name
69751f1ea1 feat: convert agent tools into step flow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 14:01:05 +08:00
Your Name
370a0055b7 fix: restore agent generation page scrolling
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 13:35:11 +08:00
Your Name
c0a8934c30 feat: refine agent workflow layout and settings
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 13:21:21 +08:00
Your Name
3b072fb44f fix: respect dark select popup styling
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 02:38:47 +08:00
Your Name
55774ef43e style: refine agent generation workspace dark ui
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 02:25:36 +08:00
Your Name
d6411076fa fix: restore agent workflow controls and nav
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 02:05:35 +08:00
Your Name
281593ff29 style: align agent workflow dark theme accents
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 01:37:44 +08:00
Your Name
0abded401b fix: keep agent workspace scrolling internal
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 01:27:39 +08:00
Your Name
1f078d5b37 feat: polish agent workflow showcase and layout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 01:18:58 +08:00
Your Name
f764767ba4 feat: refresh agent workflow dark ui
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-18 00:24:32 +08:00
Your Name
2e21d1b274 feat: refresh agent workflow frontend
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-17 23:40:42 +08:00
Your Name
ea5a189ae0 feat: add agent admin settings access
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-17 23:16:52 +08:00
Your Name
57b784f542 add switch for preferred aliyun oss uploads
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-17 23:01:17 +08:00
Your Name
67da32512a feat: support dynamic agent workflow steps
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-17 22:55:20 +08:00
Your Name
dae5f59417 feat: add brand model selection to agent flow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-17 22:42:03 +08:00
Your Name
afc9e187b6 refactor: simplify agent preset setup
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-17 22:11:36 +08:00
Your Name
4533af1495 fix: guard agent workspace client errors
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-17 21:57:36 +08:00
Your Name
50a2d84d2c feat: replace agent chat panel with generation workspace
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-17 21:48:05 +08:00
Your Name
3443dc01d3 feat: remove chat-style UI from generation workspace
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-17 21:16:23 +08:00
Your Name
7ce4d4cdc2 feat: make mode stage a full workspace
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-17 21:02:59 +08:00
Your Name
b977242dc7 feat: show workflow material picker in mode stage
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-17 20:52:44 +08:00
Your Name
590edd49f0 feat: make workflow stage full screen
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-17 20:36:42 +08:00
Your Name
38f1151099 feat: add workflow material selection flow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-17 20:26:58 +08:00
Your Name
5f81f596b8 feat: add searchable workflow selects
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-17 18:31:39 +08:00
Your Name
64cb485dfa feat: refine agent workflow flow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-17 18:23:32 +08:00
Your Name
fbbc63f865 fix: chain workflow with agent presets
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-17 18:04:30 +08:00
Your Name
0b6a794097 feat: add unified intent arbiter
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-17 17:34:00 +08:00
Your Name
4f1745b865 feat: separate agent workflow settings page
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-16 17:46:36 +08:00
Your Name
d4e6fb3fc8 fix: restore agent routing and isolate canvas context
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-16 17:25:15 +08:00
Your Name
77f1401333 Add Replicate realtime canvas provider
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-16 16:23:01 +08:00
Your Name
0a105b5c94 fix: preserve image follow-up context
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-16 15:50:01 +08:00
Your Name
3bd022fc15 fix: restore stable canvas page hooks
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-16 14:45:30 +08:00
Your Name
176fc94293 feat: add agent workflow mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-16 14:28:25 +08:00
Your Name
31f8c004b2 fix: prioritize task boundary decisions
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-16 14:25:02 +08:00
Your Name
6532cf6ebe feat: improve protocol debug panel
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-16 11:57:54 +08:00
Your Name
9ecd91376d feat: strengthen agent runtime boundaries
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-16 11:45:00 +08:00
Your Name
ea561418c4 fix: fast-path prompt-only image analysis
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-14 13:21:02 +08:00
Your Name
5d3c7c1cdc fix: fast-path pure greetings in agent mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-14 13:11:17 +08:00
Your Name
d1bcb7ce16 fix: use agent default for agent replies
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-14 13:00:42 +08:00
Your Name
161c04e23e fix: surface chat stream provider failures
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-13 17:07:39 +08:00
Your Name
810b530eb0 fix: harden video generation fallback
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-13 16:35:55 +08:00
Your Name
5a4e9fd582 feat: unify agent workflow protocol
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-13 16:16:09 +08:00
Your Name
159137b3d7 feat: formalize agent conversation state protocol
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-13 13:24:09 +08:00
Your Name
e7812060b8 Update conversation memory and canvas persistence
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-13 12:31:13 +08:00
Your Name
67d834f0dd Refine agent text mode workflow
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-13 11:35:07 +08:00
Your Name
ec988be30a Update agent workflow and video task handling
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-13 11:21:42 +08:00
Your Name
37db7e2d3a fix agent video runtime dependencies
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-13 10:40:45 +08:00
Your Name
6f0e49bfb9 fix agent video routing failures
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-13 10:28:00 +08:00
Your Name
b75dfa9295 Preserve auto ratio in OpenAI and RUXA image providers
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-13 09:47:50 +08:00
Your Name
232f634cc6 Preserve auto aspect ratio for image generation
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-12 18:23:02 +08:00
Your Name
c2a0568148 Add managed tree categories for agents
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-12 17:49:54 +08:00
Your Name
6936a2c203 feat: add privacy page for youdian biji
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-12 15:15:46 +08:00
Your Name
6ba0f89593 chore: update /riji to Youdian BijI support page
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-12 14:57:29 +08:00
Your Name
33048b7602 feat: add public /riji app intro page
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-12 14:44:12 +08:00
Your Name
c9808d4f5b avoid regex overflow in watermark base64 uploads
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-11 16:58:41 +08:00
Your Name
e300153a01 Add realtime canvas failover config
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-11 09:49:56 +08:00
Your Name
9c6abd93da Separate agent chat sessions from canvas chats
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-11 09:44:04 +08:00
Your Name
0a2b8b3045 serialize batch image processing requests
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-11 09:25:17 +08:00
Your Name
82233b5f3a Refine agent preview sizing and divider
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-11 09:20:04 +08:00
Your Name
5aabe83da7 Polish agent dialog and card copy
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-10 22:32:53 +08:00
Your Name
d27d25e6ff recognize prompt-only question phrasing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-10 22:16:08 +08:00
Your Name
bec7b32a76 Refine agent dialog and card layout
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-10 22:14:24 +08:00
Your Name
3afb31870b Adjust agent mode layout in canvas
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-10 21:47:35 +08:00
Your Name
b1afb74724 Add agent entry to lovart home sidebar
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-10 20:05:48 +08:00
Your Name
4b820f8ff4 Add canvas agent mode and management
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-10 19:56:25 +08:00
Your Name
1fa564ad90 clarify deictic target references in follow-up edits
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-10 12:18:09 +08:00
Your Name
3347bb0d87 fix false spatial targeting in canvas follow-up edits
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-10 12:11:18 +08:00
Your Name
6de541c1fb persist prompt-only flows and support multi-image analysis
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-10 11:00:21 +08:00
Your Name
c05eca89f2 avoid prompt-only shortcut when execution is requested
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-10 10:25:42 +08:00
Your Name
1e3e81ed0b return prompt analysis without generating images
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-10 10:22:11 +08:00
Your Name
7ff6af2065 add agent page development doc
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-10 10:12:51 +08:00
Your Name
a458baaca9 tighten agent execution intent gating
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-10 10:02:58 +08:00
Your Name
f1dd0e9da9 improve target image resolution for follow-up edits
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-10 09:43:13 +08:00
Your Name
641827031f optimize fallback image prompts in auto mode
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-10 09:22:05 +08:00
Your Name
54a971e3b1 remove google font build dependency
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-10 09:14:35 +08:00
Your Name
d62de1d239 replace execution plan with optimized prompt
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-09 22:29:00 +08:00
Your Name
45aabb9999 show optimized prompt in agent replies
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
2026-03-09 16:35:03 +08:00
3224 changed files with 818205 additions and 28036 deletions

View File

@ -1,45 +1,218 @@
# Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
AUTH_SECRET=****
# yedir-ai environment template
# Copy this file to .env.local and fill real values locally or in your hosting platform.
# Never commit real secrets.
# The following keys below are automatically created and
# added to your environment when you deploy on Vercel
# -----------------------------------------------------------------------------
# Core / Auth
# -----------------------------------------------------------------------------
# Instructions to create an AI Gateway API key here: https://vercel.com/ai-gateway
# API key required for non-Vercel deployments
# For Vercel deployments, OIDC tokens are used automatically
# https://vercel.com/ai-gateway
AI_GATEWAY_API_KEY=****
# Generate with: openssl rand -base64 32
AUTH_SECRET=replace-with-random-secret
# Stable pepper for user Access Key hashes. Generate independently from AUTH_SECRET.
ACCESS_KEY_PEPPER=replace-with-random-secret
# Instructions to create a Vercel Blob Store here: https://vercel.com/docs/vercel-blob
BLOB_READ_WRITE_TOKEN=****
# Authorizes trusted third-party services to provision user-scoped MCP keys.
# Must contain at least 32 characters. Never expose it to end users or browsers.
MCP_KEY_PROVISIONING_SECRET=replace-with-random-secret
# Instructions to create a PostgreSQL database here: https://vercel.com/docs/postgres
POSTGRES_URL=****
# Public application URL. Required for production and self-hosted Auth.js.
AUTH_URL=https://your-domain.com
NEXTAUTH_URL=https://your-domain.com
NEXT_PUBLIC_APP_URL=https://your-domain.com
# Only enable behind a trusted reverse proxy with validated Host headers.
AUTH_TRUST_HOST=false
# Instructions to create a Redis store here:
# https://vercel.com/docs/redis
REDIS_URL=****
# Optional OAuth providers.
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# DashScope / 通义千问配置
# 请求地址(阿里云 DashScope OpenAI 兼容接口)
# Optional initial admin provisioning.
ADMIN_EMAIL=
ADMIN_PASSWORD=
# Optional email verification.
RESEND_API_KEY=
RESEND_FROM_EMAIL=
EMAIL_VERIFICATION_SECRET=
# Optional scheduled job protection.
CRON_SECRET=
# -----------------------------------------------------------------------------
# Database
# -----------------------------------------------------------------------------
POSTGRES_URL=postgres://user:password@localhost:5432/yedir_ai
# Production-safe defaults used by lib/db/index.ts.
POSTGRES_MAX_CONNECTIONS=10
POSTGRES_IDLE_TIMEOUT_SECONDS=20
POSTGRES_CONNECT_TIMEOUT_SECONDS=5
POSTGRES_MIGRATION_TIMEOUT_SECONDS=120
# -----------------------------------------------------------------------------
# Object Storage
# -----------------------------------------------------------------------------
# Vercel Blob fallback.
BLOB_READ_WRITE_TOKEN=
# S3-compatible storage fallback. Database-configured storage takes precedence.
S3_ENDPOINT=
S3_REGION=auto
S3_BUCKET=
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_PUBLIC_URL=
# -----------------------------------------------------------------------------
# AI Providers
# -----------------------------------------------------------------------------
AI_GATEWAY_API_KEY=
NEXT_PUBLIC_DEFAULT_CHAT_MODEL=
# DashScope / Qwen OpenAI-compatible endpoint.
DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
DASHSCOPE_API_KEY=
# API Key(请在本地 .env.local 和 服务器环境中填写真实值,不要把真实 Key 提交到代码仓库)
DASHSCOPE_API_KEY=your-dashscope-api-key
# 第三方 GPT5 提供方配置(OpenAI 兼容接口)
# API 基础地址(例如:https://api.huandutech.com)
GTP5_API_URL=https://api.huandutech.com
# API Key(请在本地 .env.local 和 服务器环境中填写真实值,不要把真实 Key 提交到代码仓库)
GTP5_API_KEY=your-gtp5-api-key
# 模型名称(例如:gpt-5-nano)
# Third-party GPT-compatible provider.
GTP5_API_URL=
GTP5_API_KEY=
GTP5_MODEL=gpt-5-nano
# Tripo3D(可选)
# 优先使用系统设置-本地AI里的 `tripo-3d` 提供商;若未配置则回退到环境变量
TRIPO_API_KEY=your-tripo-api-key
# FAL image/video/realtime providers.
FAL_KEY=
FAL_API_KEY=
FAL_BASE_URL=https://fal.run
FAL_MODEL=
FAL_REALTIME_MODEL=
# Realtime Canvas fallback providers.
REPLICATE_API_TOKEN=
BFL_API_KEY=
BFL_KEY=
BFL_BASE_URL=
BFL_MODEL=
RUNWARE_MODEL=
# 3D / CAD providers.
TRIPO_API_KEY=
TRIPO_BASE_URL=https://api.tripo3d.ai/v2/openapi
WORLDLABS_API_KEY=
WORLDLABS_BASE_URL=
WORLDLABS_IMPORT_TOKEN=
WORLDLABS_KEY_POOL_IMPORT_TOKEN=
WORLDLABS_API_TIMEOUT_MS=180000
WORLDLABS_TASK_CREATE_TIMEOUT_MS=180000
WORLDLABS_TASK_STATUS_TIMEOUT_MS=180000
KITTYCAD_API_TOKEN=
KITTYCAD_API_KEY=
KITTYCAD_BASE_URL=https://api.zoo.dev
ZOO_API_TOKEN=
ZOO_BASE_URL=https://api.zoo.dev
# Pascal Editor self-hosted workspace. In local development, leaving this blank
# makes /pascal-editor auto-detect http://localhost:3002. For production or a
# fixed deployment, point this to your self-hosted Pascal service or static
# /vendor/pascal-editor/index.html bundle.
NEXT_PUBLIC_PASCAL_EDITOR_URL=
PASCAL_EDITOR_URL=
# Shared only by the main app, Nginx, and the private Pascal service. Generate a
# long random value; never expose it through NEXT_PUBLIC_* variables.
PASCAL_PROXY_SECRET=
# RUXA / video callback.
RUXA_CALLBACK_URL=
RUXA_VIDEO_CALLBACK_URL=
# -----------------------------------------------------------------------------
# Product Planning Integrations
# -----------------------------------------------------------------------------
PRODUCT_PLANNING_DETAIL_PLUS_BASE_URL=
PRODUCT_PLANNING_DETAIL_PLUS_INTERNAL_BASE_URL=
PRODUCT_PLANNING_DETAIL_PLUS_API_KEY=
PRODUCT_PLANNING_QA_TOKEN=
TAOBAO_API_KEY=
TAOBAO_API_SECRET=
TAOBAO_SALE_API_TOKEN=
PRODUCT_PLANNING_DB_HOST=
PRODUCT_PLANNING_DB_PORT=3306
PRODUCT_PLANNING_DB_DATABASE=
PRODUCT_PLANNING_DB_NAME=
PRODUCT_PLANNING_DB_USER=
PRODUCT_PLANNING_DB_PASSWORD=
PRODUCT_PLANNING_DB_CHARSET=utf8mb4
PRODUCT_PLANNING_DB_SSL=false
# -----------------------------------------------------------------------------
# Home Performance / Queues
# -----------------------------------------------------------------------------
CANVAS_IMAGE_TASK_CONCURRENCY=3
CANVAS_IMAGE_TASK_BATCH_DISPATCH_INTERVAL_MS=1000
EXTERNAL_IMAGE_API_MAX_CONCURRENT=3
EXTERNAL_IMAGE_API_MAX_QUEUE=20
EXTERNAL_IMAGE_API_QUEUE_TIMEOUT_MS=120000
# Browser direct uploads use signed OSS/S3 URLs. Configure CORS and lifecycle
# with the scripts below after changing the default storage bucket.
DIRECT_UPLOAD_CORS_ORIGINS=https://ai.yydd.ai,https://yydd.ai
DIRECT_UPLOAD_ABORT_INCOMPLETE_DAYS=2
DIRECT_UPLOAD_LIFECYCLE_PREFIX=uploads/direct/
DIRECT_UPLOAD_LIFECYCLE_RULE_ID=abort-incomplete-direct-upload-parts
# -----------------------------------------------------------------------------
# Agent / PSD Runtime
# -----------------------------------------------------------------------------
AGENT_FORCE_LLM_PLANNER=false
AGENT_FORCE_QWEN3VL_FALLBACK=false
AGENT_IMAGE_GEN_LLM_OPTIMIZER_ENABLED=true
AGENT_LLM_PROMPT_OPTIMIZER_ENABLED=true
AGENT_REPLACEMENT_PARALLEL_WORKER_COUNT=2
AGENT_PSD_LOCAL_CACHE_DIR=
AGENT_PSD_REMOTE_ASSET_CACHE_DISABLED=false
AGENT_PSD_REMOTE_ASSET_CACHE_MAX_BYTES=
AGENT_PSD_REMOTE_ASSET_CACHE_MAX_ENTRY_BYTES=
AGENT_PSD_REMOTE_ASSET_CACHE_TTL_MS=
AGENT_PSD_RENDER_PNG_COMPRESSION_LEVEL=6
AGENT_PSD_RENDER_TASK_CONCURRENCY=2
NEXT_PUBLIC_AGENT_STYLE_PERF_MONITOR=true
NEXT_PUBLIC_AGENT_STYLE_PERF_SAMPLE_RATE=0.01
NEXT_PUBLIC_AGENT_STYLE_OPTIMIZED_RENDER=true
# -----------------------------------------------------------------------------
# Logging / Observability
# -----------------------------------------------------------------------------
# debug | info | warn | error | silent
LOG_LEVEL=info
# Stack traces remain disabled in production unless explicitly enabled.
LOG_INCLUDE_STACK=false
# Client logs are silent in production unless this is true.
NEXT_PUBLIC_ENABLE_CLIENT_LOGS=false
# -----------------------------------------------------------------------------
# Optional Jobs
# -----------------------------------------------------------------------------
VIDEO_ARCHIVE_DELAY_HOURS=24
# -----------------------------------------------------------------------------
# Test / Local Dev
# -----------------------------------------------------------------------------
PORT=3001
PLAYWRIGHT=false
CI_PLAYWRIGHT=false
PLAYWRIGHT_TEST_BASE_URL=http://127.0.0.1:3001
SMOKE_BASE_URL=http://127.0.0.1:3001
SMOKE_TIMEOUT_MS=20000
DISABLE_PERF_GUARD_AUTO_INSTALL=false

35
.github/CODEOWNERS vendored Normal file
View File

@ -0,0 +1,35 @@
# Default owner for all changes.
* @lanmu1818-ship-it
# Repository governance and release safety.
/.github/ @lanmu1818-ship-it
/SECURITY.md @lanmu1818-ship-it
/CONTRIBUTING.md @lanmu1818-ship-it
/README.md @lanmu1818-ship-it
/docs/release-readiness.md @lanmu1818-ship-it
/docs/operations.md @lanmu1818-ship-it
/docs/deployment.md @lanmu1818-ship-it
# Core application surfaces.
/app/(admin)/settings/ @lanmu1818-ship-it
/app/(chat)/api/chat/ @lanmu1818-ship-it
/app/api/canvas/ @lanmu1818-ship-it
/components/canvas/ @lanmu1818-ship-it
/components/home/ @lanmu1818-ship-it
# Infrastructure, security, and data boundaries.
/lib/observability/ @lanmu1818-ship-it
/lib/security/ @lanmu1818-ship-it
/lib/storage/ @lanmu1818-ship-it
/lib/db/ @lanmu1818-ship-it
/lib/ai/ @lanmu1818-ship-it
/scripts/check-*.mjs @lanmu1818-ship-it
/scripts/check-*.ts @lanmu1818-ship-it
/scripts/security/ @lanmu1818-ship-it
# Quality gates and dependency manifests.
/package.json @lanmu1818-ship-it
/pnpm-lock.yaml @lanmu1818-ship-it
/tsconfig.json @lanmu1818-ship-it
/next.config.ts @lanmu1818-ship-it
/biome.jsonc @lanmu1818-ship-it

67
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,67 @@
name: Bug report
description: Report a reproducible product, API, or operations bug.
title: "[Bug]: "
labels: ["bug", "needs-triage"]
body:
- type: markdown
attributes:
value: |
Thanks for helping improve yedir-ai.
Do not include secrets, cookies, Authorization headers, OAuth codes, signed URLs, `.env.local` values, private prompts, base64 data, or private user content. If this is a security issue, do not open a public issue. Follow `SECURITY.md` instead.
- type: textarea
id: summary
attributes:
label: Summary
description: What happened, and what did you expect instead?
validations:
required: true
- type: textarea
id: steps
attributes:
label: Reproduction steps
description: Provide the smallest reliable reproduction.
placeholder: |
1. Go to ...
2. Click ...
3. Observe ...
validations:
required: true
- type: textarea
id: impact
attributes:
label: Impact
description: Who is affected and how severe is it?
validations:
required: true
- type: textarea
id: environment
attributes:
label: Environment
description: Browser, OS, deployment target, Node/pnpm versions, or route/API involved.
placeholder: |
Browser:
OS:
Node:
pnpm:
Route/API:
- type: textarea
id: logs
attributes:
label: Sanitized logs or screenshots
description: Include only sanitized output. Remove prompts, URLs, tokens, cookies, headers, base64, and private content.
- type: checkboxes
id: checks
attributes:
label: Pre-submit checklist
options:
- label: I removed secrets, tokens, signed URLs, cookies, private prompts, and private user content.
required: true
- label: I searched existing issues and docs before filing this.
required: true

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Security vulnerability
url: https://github.com/lanmu1818-ship-it/yedir-ai/blob/main/SECURITY.md
about: Do not open public issues for vulnerabilities. Follow SECURITY.md and report privately to the project maintainer.
- name: Release readiness and contribution rules
url: https://github.com/lanmu1818-ship-it/yedir-ai/blob/main/CONTRIBUTING.md
about: Read docs/release-readiness.md and CONTRIBUTING.md before opening broad engineering requests.
- name: Support and operations help
url: https://github.com/lanmu1818-ship-it/yedir-ai/blob/main/SUPPORT.md
about: Read SUPPORT.md for bug, feature, deployment, operations, and security support boundaries.

View File

@ -0,0 +1,68 @@
name: Feature request
description: Propose a product, workflow, integration, or engineering improvement.
title: "[Feature]: "
labels: ["enhancement", "needs-triage"]
body:
- type: markdown
attributes:
value: |
Please keep proposals focused and avoid sharing private customer data, prompts, screenshots with secrets, or provider credentials.
- type: textarea
id: problem
attributes:
label: Problem or opportunity
description: What user, operator, or developer problem does this solve?
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed solution
description: Describe the behavior, API, UI, or documentation change.
validations:
required: true
- type: dropdown
id: area
attributes:
label: Area
options:
- Lovart home
- Canvas / Agent
- Image or video generation
- 3D / CAD
- Settings / admin
- API / backend
- Performance
- Security / observability
- Documentation / CI
- Other
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: What other approaches did you consider?
- type: textarea
id: acceptance
attributes:
label: Acceptance criteria
description: How should reviewers verify that this is done?
placeholder: |
- ...
- ...
- type: checkboxes
id: checks
attributes:
label: Pre-submit checklist
options:
- label: This request does not include real secrets, private prompts, signed URLs, or private user data.
required: true
- label: I checked the README, release readiness notes, and existing issues first.
required: true

42
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,42 @@
## Summary
<!-- What changed, and why? Keep this focused on user/business impact. -->
## Scope
- [ ] UI / product behavior
- [ ] API / backend logic
- [ ] Settings / admin configuration
- [ ] Performance / caching
- [ ] Logging / security
- [ ] Documentation / CI / repository hygiene
## Required Verification
- [ ] `pnpm check:branch`
- [ ] `pnpm check:quality`
- [ ] `pnpm build`
- [ ] `AUTH_TRUST_HOST=true PORT=3001 pnpm dev`
- [ ] `SMOKE_BASE_URL=http://127.0.0.1:3001 pnpm smoke:routes`
If any required check was not run, explain why:
```text
```
## Release Readiness
- [ ] No real secrets, OAuth codes, cookies, Authorization headers, signed URLs, or `.env.local` values are included.
- [ ] No tracked local artifacts such as `.DS_Store`, swap files, `.claude/`, migration backups, or `tsconfig.tsbuildinfo`.
- [ ] No new public UI links point to `/classic` or `/chat/[id]`.
- [ ] `/api/chat` remains treated as an internal Canvas/Agent generation API unless this PR explicitly migrates callers.
- [ ] New or modified high-risk logging uses `createLogger` / `createClientLogger` and is covered by `pnpm check:logs` when applicable.
## Screenshots / Evidence
<!-- Add screenshots, route smoke output, CI links, or notes for reviewers. -->
## Follow-ups
<!-- List intentional non-blocking follow-ups, if any. -->

37
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,37 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "Asia/Shanghai"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "needs-triage"
commit-message:
prefix: "chore"
include: "scope"
groups:
minor-and-patch:
update-types:
- "minor"
- "patch"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:30"
timezone: "Asia/Shanghai"
open-pull-requests-limit: 3
labels:
- "dependencies"
- "ci"
- "needs-triage"
commit-message:
prefix: "ci"
include: "scope"

45
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: Production Build
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: build-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-22.04
timeout-minutes: 25
env:
AUTH_SECRET: ci-build-placeholder-secret
AUTH_URL: http://localhost:3000
NEXTAUTH_URL: http://localhost:3000
NEXT_PUBLIC_APP_URL: http://localhost:3000
POSTGRES_URL: postgres://user:password@localhost:5432/yedir_ai
DISABLE_PERF_GUARD_AUTO_INSTALL: "1"
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
run_install: false
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run production build
run: pnpm build

38
.github/workflows/dependency-audit.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: Dependency Audit
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: dependency-audit-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
audit:
runs-on: ubuntu-22.04
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
run_install: false
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Audit high and critical dependency advisories
run: pnpm audit:security

View File

@ -1,25 +1,38 @@
name: Lint
name: Quality Gates
on:
push:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: quality-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
quality:
runs-on: ubuntu-22.04
timeout-minutes: 20
strategy:
matrix:
node-version: [20]
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9.12.3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "pnpm"
- name: Install dependencies
run: pnpm install
- name: Run lint
run: pnpm lint
run: pnpm install --frozen-lockfile
- name: Run quality gate
run: pnpm check:quality

View File

@ -1,14 +1,18 @@
name: Playwright Tests
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: playwright-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
timeout-minutes: 30
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
env:
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
POSTGRES_URL: ${{ secrets.POSTGRES_URL }}
@ -22,37 +26,20 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: lts/*
node-version: 20
cache: "pnpm"
- name: Install pnpm
uses: pnpm/action-setup@v2
uses: pnpm/action-setup@v4
with:
version: latest
version: 9.12.3
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Cache Playwright browsers
uses: actions/cache@v3
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright

24
.gitignore vendored
View File

@ -11,12 +11,18 @@ coverage
# next.js
.next/
.next-build/
.next-previous/
out/
build
# misc
.DS_Store
*.swp
*.pem
*.log
*.har
tsconfig.tsbuildinfo
# debug
npm-debug.log*
@ -37,6 +43,7 @@ yarn-error.log*
.env
.vercel
.env*.local
.claude/
# Playwright
/test-results/
@ -52,9 +59,17 @@ migration-backup-*/
migration-backup-complete/
*.dump
*.sql
*.zip
*.tar
*.tar.gz
*.tgz
migration-*.log
migration-*.txt
# 本地抓包/竞品参考产物,不能提交
artifacts/liblib-*/
analysis/qiaodou-extract/
# 模型文件
*.model
*.pth
@ -74,5 +89,14 @@ checkpoints/
# 第三方参考源码(不提交到git)
reference-sources/
# Legacy external Pascal Editor checkout (the managed source is under services/).
.pascal-editor/
# Local self-hosted Zoo Design Studio checkout.
.zoo-modeling-app/
# Generated official Zoo Design Studio web bundle.
public/vendor/zoo-modeling-app/
# Drizzle migrations must be versioned (override the global *.sql ignore above)
!lib/db/migrations/*.sql

View File

@ -0,0 +1,332 @@
# AI 生图平台桶缓存落地对接清单
> 交付对象:AI 生图平台负责人
> 当前主接口域名:`https://sc.jiansun.vip`
> 当前资源主域名:`https://s3.jiansun.vip`
> 备用资源域名:`https://s3.yydd.ai`
> 文档时间:`2026-03-18`
## 1. 这次要做的事情
计划把新入口服务器 `180.97.215.57` 上独立数据盘中的 `180G` 空间,用作 AI 生图平台相关资源的**读取缓存**。
这件事的目标是:
- 提升 AI 生图平台读取图片资源时的稳定性
- 降低反复回源到生产机的压力
- 让热点资源第二次及之后访问更快
## 2. 这次不会变的东西
下面这些默认都**不变**:
- 桶本体位置不变,真实存储仍在生产机 `192.168.58.20:9000`
- AI 生图平台的接口入口仍然是 `https://sc.jiansun.vip`
- 对外资源主域名仍然是 `https://s3.jiansun.vip`
- `https://s3.yydd.ai` 先保留为备用域名,不作为默认对接地址
- 这次做的是“读缓存”,不是改上传链路,也不是迁桶
一句话说清楚:
**缓存只是在入口服务器上多放一份“最近常用内容的副本”,不是把桶搬走。**
## 3. 当前已经确认的事实
当前资源入口链路是:
```text
s3.jiansun.vip
-> 180.97.215.57
-> nginx 磁盘缓存
-> 127.0.0.1:29000
-> FRP
-> 192.168.58.20:9000
```
当前缓存盘配置已经到位:
- 缓存目录:`/data/nginx-cache/s3`
- 缓存上限:`180g`
- 当前入口机资源盘:`/data`,总容量约 `200G`
`2026-03-18` 实测真实桶链路时,使用了一个约 `145MB` 的真实桶对象做下载测试:
- 直回源 `127.0.0.1:29000`:约 `23.2 MB/s`
- 走 `https://s3.jiansun.vip` 第一次请求:`X-Cache-Status: MISS`,约 `22.7 MB/s`
- 同一个 URL 第二次请求:`X-Cache-Status: HIT`,约 `63.2 MB/s`
这说明:
- 首次访问主要受回源链路影响
- 命中缓存后,速度会明显提升
基于当前已验证结果,可以先明确 3 个结论:
- 这套 `180G` 读缓存方案本身是可行的
- 它适合承接 AI 生图平台里“会被重复访问”的热点资源
- 在不改业务入口、不迁桶的前提下,就可以先做灰度验证
## 4. 我还需要 AI 生图平台这边提供什么
为了把这 `180G` 真正用于 AI 生图平台的热点流量,而不是“缓存了一堆不重要的文件”,还需要下面这些信息。
### 4.1 需要确认的资源范围
请提供:
- AI 生图平台当前实际会读取的资源类型
- 这些资源对应的 URL 样例
- 最常访问的路径前缀
最好直接给:
- 近 3 天最常访问的 20 个资源 URL 样例
- 主要路径前缀清单
例如是否主要集中在:
- 生图输入参考图
- 生图结果图
- 放大图、变体图、蒙版图、压缩包等固定路径前缀
### 4.2 需要确认是否只使用一个主资源域名
这个点非常关键。
请确认:
- AI 生图平台后续是否统一只读 `https://s3.jiansun.vip`
- `https://s3.yydd.ai` 是否只作为备用 / 回切,不参与日常默认调用
推荐口径:
- 主用:`https://s3.jiansun.vip`
- 备用:`https://s3.yydd.ai`
如果两个域名同时长期对外使用,那么**同一份内容就可能同时出现两个公网 URL**。
### 4.3 需要确认文件是否“同 key 覆盖”
这个点决定缓存能不能放心加大。
请确认:
- 同一张图后续会不会“覆盖原来的对象 key”
- 还是每次生成新图都使用新文件名 / 新路径
最推荐的方式是:
- **新内容永远用新 key**
比如:
- `image-20260318-001.png`
- `image-20260318-002.png`
不推荐的方式是:
- 总是写回同一个 URL
比如始终覆盖:
- `latest.png`
原因很简单:
- 如果同一个 URL 对应的文件内容被覆盖了,缓存里可能暂时还是旧版本
- 这样就会出现“URL 没变,但内容短时间还是旧图”的情况
### 4.4 需要确认时效要求
请确认:
- 图片更新后,允许多久内出现旧图
- 删除后,是否允许短时间还能访问
- 是否存在“必须秒级生效”的路径
如果某些资源要求非常高的实时性,就不能和普通静态资源用完全同一套缓存策略。
### 4.5 需要确认流量规模
请提供一个大致量级即可,不需要特别精确。
需要知道:
- 峰值并发大概多少
- 每分钟 / 每小时大概多少请求
- 热门图片是否会被大量重复读取
- 单张图通常多大
- 是否有明显的突发高峰时段
### 4.6 需要一批联调样本
请提供:
- 5 到 10 个真实可访问的 AI 生图平台资源 URL
- 最好覆盖“结果图 / 参考图 / 较大文件”几类
这样我可以直接按真实资源做灰度验证,而不是拿不相关对象代替。
### 4.7 这次不需要 AI 生图平台配合改的内容
为了避免沟通误差,这里也明确一下当前**不要求**你们先改的东西:
- 不要求先改接口域名,接口仍然走 `https://sc.jiansun.vip`
- 不要求先迁桶,桶本体仍然在 `192.168.58.20:9000`
- 不要求先切到 `s3.yydd.ai`
- 不要求先改上传链路
这次优先要拿到的是“真实读流量信息”,不是让业务先大改。
## 5. 对正常使用有没有影响
正常情况下,**对用户是无感的**。
前提是满足下面两个条件:
- 对外主资源域名统一使用 `https://s3.jiansun.vip`
- 资源尽量使用“新内容新 key”的方式,不要反复覆盖同一个 URL
只要做到这两点,缓存的效果就是:
- 第一次访问走回源
- 后续重复访问直接命中入口缓存
- 用户看到的还是同一个公网 URL
## 6. 会不会出现“同一张图两个 URL”
### 情况 1:缓存本身
**不会。**
缓存不会凭空再生成一个给用户使用的新 URL。
用户访问的仍然是原来的公网地址,比如:
```text
https://s3.jiansun.vip/xxx.png
```
缓存只是入口服务器内部把这个 URL 对应的内容存到磁盘里。
也就是说:
- 对用户看到的 URL 不变
- 对程序调用的 URL 不变
- 只是入口机内部多了一份缓存文件
### 情况 2:两个域名同时都在用
**会。**
如果下面两个地址同时长期对外使用:
- `https://s3.jiansun.vip/xxx.png`
- `https://s3.yydd.ai/xxx.png`
那么从“公网 URL”角度看,就是两条 URL 指向了同一份内容。
所以当前建议非常明确:
- `s3.jiansun.vip` 主用
- `s3.yydd.ai` 备用
不要两个一起作为默认入口使用。
## 7. 会不会出现“缓存里一张图,桶里又是另一个 URL”
从用户角度看,**不会多出一个可见 URL**。
但有一个需要特别注意的现实问题:
### 如果同一个 URL 被反复覆盖内容
那么就可能出现:
- 桶里已经是新图
- 缓存里短时间还是旧图
这时候不是“两个 URL”,而是:
- **同一个 URL,在不同时间点可能先返回旧内容,再返回新内容**
这也是为什么我强烈建议:
- 新图使用新 key
- 不要反复覆盖同一个对象地址
如果业务上确实必须覆盖同一个 URL,那就必须额外做:
- 清缓存机制
- 或单独降低这类路径的缓存时间
## 8. 推荐的最终口径
建议 AI 生图平台后续按下面这套规则执行:
### 域名规则
- 主用资源域名:`https://s3.jiansun.vip`
- 备用资源域名:`https://s3.yydd.ai`
### URL 规则
- 对外只认主域名,不混用两个资源域名
- 新内容尽量使用新 key,不覆盖老 key
### 缓存规则
- 把 AI 生图平台的热点读流量优先收敛到 `s3.jiansun.vip`
- 入口机 `180G` 只做读缓存,不改桶本体位置
## 9. 现在就可以推进的下一步
如果 AI 生图平台负责人能补齐下面 4 项,我就可以继续往下收口:
1. 确认主用资源域名是否统一为 `s3.jiansun.vip`
2. 提供 AI 生图平台真实会读取的路径前缀或 URL 样例
3. 明确是否存在“同 key 覆盖”
4. 提供一组真实联调 URL 用于灰度测试
拿到这些信息后,我这边会继续做:
1. 按真实热点路径确认是否需要“分路径缓存策略”
2. 区分哪些资源适合长缓存,哪些资源要短缓存
3. 如果存在“同 key 覆盖”,评估是否需要补清缓存机制
4. 用真实 URL 做灰度验证,再决定是否正式放量
## 10. 建议负责人直接按下面模板回复
为了节省来回沟通时间,建议 AI 生图平台负责人直接按这个格式回:
```text
1. 主用资源域名:
例如:统一使用 https://s3.jiansun.vip
2. 是否保留备用域名:
例如:保留 https://s3.yydd.ai,仅用于备用,不作为默认入口
3. 主要读取的资源类型:
例如:生图结果图、参考图、放大图、变体图、压缩包
4. 主要路径前缀或 URL 样例:
例如:/gongxiangpan/ai-output/... 或 /gongxiangpan/ai-input/...
5. 是否存在同 key 覆盖:
例如:有 / 没有
6. 时效要求:
例如:更新后允许 5 分钟内旧图;某些路径要求秒级生效
7. 流量规模:
例如:高峰每分钟约多少次读取,热门图片是否会被重复访问
8. 联调样本:
例如:提供 5 到 10 个真实可访问 URL
```
---
如果只要一句最短版本,可以直接转给对方:
**这次不是迁桶,只是在 `180.97.215.57` 上给 `s3.jiansun.vip` 做 180G 读缓存。要继续推进,需要 AI 生图平台确认主用域名、真实热点路径、是否会覆盖同一 URL、以及给一组联调样本。只要统一用一个主域名,并尽量做到新内容新 key,缓存对用户基本无感,也不会凭空多出第二个 URL。**

45
CHANGELOG.md Normal file
View File

@ -0,0 +1,45 @@
# Changelog
All notable changes to yedir-ai will be documented in this file.
This project follows a pragmatic changelog format inspired by Keep a Changelog. Dates use ISO format.
## Unreleased
### Added
- Lovart home release smoke script via `pnpm smoke:routes`.
- Unified local and CI quality gate via `pnpm check:quality`.
- Release readiness checklist, deployment guide, operations guide, architecture overview, security policy, contribution guide, support policy, code of conduct, PR template, issue templates, CODEOWNERS, and Dependabot configuration.
- Scoped log hygiene gate for high-risk server/client files.
- Route-level helper tests for Canvas chat helper behavior.
### Changed
- `/` home now prioritizes a lightweight shell and progressively loads personalized data.
- `/classic` and `/chat/[id]` legacy UI routes redirect to `/`.
- `/api/chat` is documented and treated as an internal Canvas/Agent generation API.
- Admin settings local AI sections were modularized into focused components with shared section shells and types.
- Lovart home and Canvas/ImageNode presentation surfaces were split into more focused modules.
- Chat API internals were partially extracted into focused `_lib` helpers.
- CI now reuses the same quality gate command as local development.
### Security
- Production logging now uses structured redaction for storage/upload, auth, AI provider, Canvas, scene fusion, Pick Object, Tripo 3D, World Labs, and internal generation paths.
- Sensitive values such as tokens, cookies, Authorization headers, signed URLs, base64 payloads, prompts, provider responses, and private user content are documented as non-loggable/non-shareable.
- Tracked local artifact checks prevent `.env.local`, `.DS_Store`, swap files, `.claude/`, migration backups, and `tsconfig.tsbuildinfo` from entering Git.
- Security disclosure guidance is documented in `SECURITY.md`.
### Verification
- Current local release checks:
- `pnpm check:quality`
- `AUTH_TRUST_HOST=true PORT=3001 pnpm dev`
- `SMOKE_BASE_URL=http://127.0.0.1:3001 pnpm smoke:routes`
### Known Follow-ups
- Confirm remote GitHub Actions on a pushed PR.
- Continue optional log hygiene coverage for remaining historical image enhancement interfaces.
- Continue optional modularization of large Canvas/Agent historical modules.

31
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,31 @@
# Code of Conduct
yedir-ai contributors, maintainers, users, and reviewers are expected to keep collaboration respectful, professional, and focused on improving the project.
## Expected Behavior
- Be respectful and constructive in issues, pull requests, reviews, and discussions.
- Assume good intent, but ask for clarification when something is unclear.
- Give actionable feedback with context and concrete examples.
- Respect privacy and confidentiality. Do not post secrets, private user data, private prompts, signed URLs, screenshots with credentials, or sensitive business information.
- Keep disagreements technical and evidence-based.
## Unacceptable Behavior
- Harassment, intimidation, discrimination, personal attacks, or abusive language.
- Publishing private information without explicit permission.
- Posting real credentials, tokens, cookies, OAuth codes, Authorization headers, signed URLs, or private user content.
- Deliberately disrupting discussions, reviews, CI, or project operations.
- Ignoring maintainer requests to move security-sensitive details to a private channel.
## Security-Sensitive Reports
Do not report vulnerabilities in public issues. Follow `SECURITY.md` and contact the project maintainer privately.
## Enforcement
Maintainers may edit or remove comments, close issues, block users, or restrict participation when behavior violates this code of conduct. Enforcement should be proportional, documented when appropriate, and focused on protecting contributors and users.
## Scope
This code applies to project spaces including GitHub issues, pull requests, discussions, documentation, community chat, and any other collaboration channel used for yedir-ai.

69
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,69 @@
# Contributing to yedir-ai
Thank you for contributing. yedir-ai is maintained as a production-oriented AI design workspace, so changes should be easy to review, safe to roll back, and supported by clear verification evidence.
## Development Setup
Use Node.js 20 and pnpm 9.12.3.
```bash
pnpm install
cp .env.example .env.local
pnpm db:migrate
AUTH_TRUST_HOST=true PORT=3001 pnpm dev
```
Never commit `.env.local` or real credentials.
## Branches and Commits
- Use focused branches for a single theme, such as `refactor/settings-local-ai` or `chore/log-hygiene-canvas`.
- Keep commits small enough to review and revert independently.
- Do not mix broad formatting changes with functional changes.
- Do not push generated local artifacts, worktrees, swap files, migration backups, or `tsconfig.tsbuildinfo`.
## Change Scope
Prefer narrow, explicit scopes:
- UI changes should preserve existing product behavior unless the PR states otherwise.
- API changes must describe compatibility impact and response-shape changes.
- Settings changes must preserve existing setting keys, SWR keys, and save semantics unless a migration is included.
- Logging/security changes must avoid exposing prompts, URLs, base64 data, provider payloads, user content, tokens, cookies, or signed URLs.
`/api/chat` is currently an internal Canvas/Agent generation API. Do not treat it as a public legacy Chat UI contract unless the PR explicitly migrates callers.
## Required Checks
Before opening or updating a PR, run:
```bash
pnpm check:quality
AUTH_TRUST_HOST=true PORT=3001 pnpm dev
SMOKE_BASE_URL=http://127.0.0.1:3001 pnpm smoke:routes
```
`pnpm check:quality` covers lint, scoped log hygiene, typecheck, diff hygiene, and tracked artifact checks.
If a route or workflow cannot be tested locally, explain the gap in the PR.
## Pull Requests
Use the PR template and include:
- What changed and why.
- The user or operational impact.
- Verification output or screenshots when relevant.
- Any intentional follow-up work.
CODEOWNERS is configured for governance, security, CI, settings, Canvas, API, and infrastructure paths. If your PR touches those areas, expect owner review before merge.
For frontend changes, include screenshots or a short visual verification note. For route/API changes, include status codes, response-shape notes, or targeted test evidence.
## Security
Follow `SECURITY.md` for vulnerability reporting and sensitive-data handling. Report suspected vulnerabilities privately; do not open public issues for secrets, auth bypasses, signed URL leaks, or data exposure.
## Conduct
Follow `CODE_OF_CONDUCT.md` in issues, pull requests, reviews, and community spaces.

197
README.md
View File

@ -1,71 +1,152 @@
<a href="https://chat.vercel.ai/">
<img alt="Next.js 14 and App Router-ready AI chatbot." src="app/(chat)/opengraph-image.png">
<h1 align="center">Chat SDK</h1>
</a>
# yedir-ai
<p align="center">
Chat SDK is a free, open-source template built with Next.js and the AI SDK that helps you quickly build powerful chatbot applications.
</p>
yedir-ai 是一个面向商业化交付的 AI 设计工作台。它把 Lovart 风格的新首页、无限画布、Agent 工作流、图像生成与编辑、3D/CAD、本地 AI 配置中心和后台治理能力整合到同一个 Next.js 应用中。
<p align="center">
<a href="https://chat-sdk.dev"><strong>Read Docs</strong></a> ·
<a href="#features"><strong>Features</strong></a> ·
<a href="#model-providers"><strong>Model Providers</strong></a> ·
<a href="#deploy-your-own"><strong>Deploy Your Own</strong></a> ·
<a href="#running-locally"><strong>Running locally</strong></a>
</p>
<br/>
这个仓库已经不再定位为通用 Chat 模板。旧 Chat 页面入口已移除,`/api/chat` 目前保留为 Canvas/Agent 等内部生成链路使用。
## Features
## 当前能力
- [Next.js](https://nextjs.org) App Router
- Advanced routing for seamless navigation and performance
- React Server Components (RSCs) and Server Actions for server-side rendering and increased performance
- [AI SDK](https://ai-sdk.dev/docs/introduction)
- Unified API for generating text, structured objects, and tool calls with LLMs
- Hooks for building dynamic chat and generative user interfaces
- Supports xAI (default), OpenAI, Fireworks, and other model providers
- [shadcn/ui](https://ui.shadcn.com)
- Styling with [Tailwind CSS](https://tailwindcss.com)
- Component primitives from [Radix UI](https://radix-ui.com) for accessibility and flexibility
- Data Persistence
- [Neon Serverless Postgres](https://vercel.com/marketplace/neon) for saving chat history and user data
- [Vercel Blob](https://vercel.com/storage/blob) for efficient file storage
- [Auth.js](https://authjs.dev)
- Simple and secure authentication
- **Lovart 风格首页**:静态轻量壳优先渲染,站点配置、精选图、项目、教程和会员信息通过客户端渐进加载。
- **Canvas/Agent 工作台**:支持多模态生成、图片编辑、素材编排、Agent 模式和项目化创作流程。
- **图像与视频生成链路**:支持 Gemini、Volcano、Qwen、FAL、RUXA、Krea、Runway、Midjourney 等可配置供应商。
- **3D / CAD 能力**:集成 Tripo3D、World Labs、KittyCAD/Zoo,并支持后台模型别名和 Key 池配置。
- **后台设置中心**:本地 AI 设置已拆分为领域组件,支持折叠持久化、状态摘要、快捷目录和统一 section shell。
- **生产治理**:包含类型检查、lint、日志脱敏、首页公开接口缓存/降级、仓库污染物检查和 CI 质量门禁。
## Model Providers
## 技术栈
This template uses the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) to access multiple AI models through a unified interface. The default configuration includes [xAI](https://x.ai) models (`grok-2-vision-1212`, `grok-3-mini`) routed through the gateway.
- Next.js 16 App Router
- React 19 RC
- TypeScript
- Tailwind CSS + Radix UI
- Drizzle ORM + Postgres
- Auth.js
- Vercel Blob / S3-compatible object storage
- Playwright
- Ultracite / Biome
### AI Gateway Authentication
## 快速开始
**For Vercel deployments**: Authentication is handled automatically via OIDC tokens.
**For non-Vercel deployments**: You need to provide an AI Gateway API key by setting the `AI_GATEWAY_API_KEY` environment variable in your `.env.local` file.
With the [AI SDK](https://ai-sdk.dev/docs/introduction), you can also switch to direct LLM providers like [OpenAI](https://openai.com), [Anthropic](https://anthropic.com), [Cohere](https://cohere.com/), and [many more](https://ai-sdk.dev/providers/ai-sdk-providers) with just a few lines of code.
## Deploy Your Own
You can deploy your own version of the Next.js AI Chatbot to Vercel with one click:
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/templates/next.js/nextjs-ai-chatbot)
## Running locally
You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js AI Chatbot. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/projects/environment-variables) for this, but a `.env` file is all that is necessary.
> Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various AI and authentication provider accounts.
1. Install Vercel CLI: `npm i -g vercel`
2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link`
3. Download your environment variables: `vercel env pull`
准备 Node.js 20、pnpm 9.12.3 和 Postgres。
```bash
pnpm install
pnpm db:migrate # Setup database or apply latest database changes
pnpm dev
cp .env.example .env.local
pnpm db:migrate
AUTH_TRUST_HOST=true PORT=3001 pnpm dev
```
Your app template should now be running on [localhost:3000](http://localhost:3000).
默认访问地址:
```text
http://localhost:3001/
```
后台设置页:
```text
http://localhost:3001/settings
```
未登录访问后台会重定向到 `/login?callbackUrl=%2Fsettings`。
## 环境变量
先从 `.env.example` 复制到 `.env.local`,再按部署环境填写真实值。不要提交 `.env.local` 或任何真实密钥。
最小本地运行通常需要:
- `AUTH_SECRET`
- `AUTH_URL` 或本地开发使用 `AUTH_TRUST_HOST=true`
- `POSTGRES_URL`
- `BLOB_READ_WRITE_TOKEN` 或一组 S3 配置
- 至少一个可用 AI Provider Key,或在后台设置中配置供应商
更多分组说明见 [docs/deployment.md](docs/deployment.md)。
## 常用命令
```bash
pnpm dev
pnpm lint
pnpm check:logs
pnpm check:brand
pnpm check:ci
pnpm check:branch
pnpm check:env
pnpm check:secrets
pnpm check:metadata
pnpm check:tests
pnpm test:helpers
pnpm check:artifacts
pnpm check:quality
pnpm clean:artifacts
pnpm audit:security
pnpm verify:release
pnpm exec tsc --noEmit --pretty false
pnpm test
pnpm build
pnpm build:with-migrate
pnpm db:migrate
pnpm db:generate
```
推荐提交前至少运行:
```bash
git fetch github main --prune
pnpm check:branch
pnpm check:quality
pnpm audit:security
pnpm build
AUTH_TRUST_HOST=true PORT=3001 pnpm dev
SMOKE_BASE_URL=http://127.0.0.1:3001 pnpm smoke:routes
```
发布或创建 PR 前可使用一条命令串起质量、安全、生产构建和路由烟测:
```bash
SMOKE_BASE_URL=http://127.0.0.1:3001 pnpm verify:release
```
## 目录结构
```text
app/ Next.js routes, route handlers, admin pages
components/ UI, Canvas, home, editor and product modules
components/home/lovart/ Lovart home focused render sections
lib/ DB, AI providers, storage, observability, domain logic
docs/ Deployment, operations, architecture and feature notes
tests/ Playwright e2e and route-level tests
.github/workflows/ Manual quality gates, dependency audit and Playwright CI
```
## 部署与运维
- 生产部署建议使用受控反向代理,并只在 Host 头可信时设置 `AUTH_TRUST_HOST=true`。
- 首页公开接口应保持快速返回,慢数据库或外部服务异常时返回缓存或 fallback。
- 生产日志默认通过 `LOG_LEVEL=info` 控制,敏感字段会被 `lib/observability` 脱敏。
- GitHub Actions workflow 默认只允许手动触发,避免私有仓库免费额度被 push 自动消耗。
- Dependency Audit workflow 独立守住 high/critical 依赖审计基线,避免安全回归只靠人工发现。
- Playwright workflow 独立于基础质量门禁,避免把密钥依赖和 e2e 成本混入每次 lint/typecheck。
详细说明:
- [部署指南](docs/deployment.md)
- [运维与质量门禁](docs/operations.md)
- [系统架构说明](docs/architecture.md)
- [发布就绪检查清单](docs/release-readiness.md)
- [交付就绪审查摘要](docs/delivery-readiness-audit.md)
- [依赖风险登记](docs/dependency-risk-register.md)
- [安全策略](SECURITY.md)
- [贡献指南](CONTRIBUTING.md)
- [行为准则](CODE_OF_CONDUCT.md)
- [支持说明](SUPPORT.md)
- [更新日志](CHANGELOG.md)
## 开发边界
- `/classic` 和 `/chat/[id]` 旧 Chat UI 已重定向到 `/`。
- `/api/chat` 暂时是内部生成 API,不作为公开旧 Chat 页面入口。
- Canvas、Agent、图像处理和后台设置仍有大型历史模块,后续应按领域继续拆分。
- License 以仓库根目录 [LICENSE](LICENSE) 为准;本阶段未修改许可证和版权主体。

View File

@ -1,902 +0,0 @@
# Real-ESRGAN API 接口文档
## 服务信息
- **服务地址**: http://156.225.27.52:5233(通过frpc穿透)
- **本地端口**: 8002(仅服务器内部访问)
- **远程端口**: 5233(外部访问端口)
- **API版本**: 1.0.0
- **模型**: Real-ESRGAN_x4plus(图像超分辨率模型)
- **框架**: FastAPI
- **API文档**: http://156.225.27.52:5233/docs
- **OpenAPI规范**: http://156.225.27.52:5233/openapi.json
- **API密钥**: 所有核心接口需要API密钥认证(详见下方认证说明)
---
## 认证方式
所有核心接口(`/upscale/file`、`/upscale/base64` 和 `/upscale/url`)需要在请求头中添加API密钥:
**请求头**:
```
X-API-Key: realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw
```
**注意**: 以下端点无需API密钥:
- `GET /` - 根端点(服务状态检查)
- `GET /health` - 健康检查
- `GET /api-key-info` - API密钥信息
- `GET /models` - 支持的模型列表
---
## 接口列表
### 1. 根端点 - 服务状态检查
**接口地址**: `GET /`
**描述**: 检查服务是否正常运行
**请求示例**:
```bash
curl http://156.225.27.52:5233/
```
**响应示例**:
```json
{
"message": "Real-ESRGAN API 服务运行正常",
"version": "1.0.0",
"model": "RealESRGAN_x4plus",
"scale": 4
}
```
---
### 2. 健康检查
**接口地址**: `GET /health`
**描述**: 检查服务健康状态和模型加载状态(无需API密钥)
**请求示例**:
```bash
curl http://156.225.27.52:5233/health
```
**响应示例**:
```json
{
"status": "healthy",
"model_loaded": true,
"model": "RealESRGAN_x4plus",
"scale": 4,
"device": "cuda"
}
```
---
### 3. API密钥信息
**接口地址**: `GET /api-key-info`
**描述**: 获取API密钥使用说明(无需API密钥)
**请求示例**:
```bash
curl http://156.225.27.52:5233/api-key-info
```
**响应示例**:
```json
{
"api_key_name": "X-API-Key",
"description": "请在请求头中添加API密钥"
}
```
---
### 4. 支持的模型列表
**接口地址**: `GET /models`
**描述**: 获取所有支持的模型列表(无需API密钥)
**请求示例**:
```bash
curl http://156.225.27.52:5233/models
```
**响应示例**:
```json
{
"models": [
{
"name": "RealESRGAN_x2plus",
"scale": 2,
"description": "2倍放大模型"
},
{
"name": "RealESRGAN_x4plus",
"scale": 4,
"description": "4倍放大模型(推荐)"
},
{
"name": "RealESRGAN_x8plus",
"scale": 8,
"description": "8倍放大模型"
},
{
"name": "RealESRGAN_animevideov3",
"scale": 4,
"description": "动漫视频模型"
}
]
}
```
---
### 5. 文件上传方式放大图像
**接口地址**: `POST /upscale/file`
**描述**: 通过文件上传方式对图片进行超分辨率处理(需要API密钥)
**Content-Type**: `multipart/form-data`
**请求头**:
```
X-API-Key: realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw
```
**请求参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| file | File | 是 | - | 图片文件(支持常见图片格式) |
| scale | int | 否 | 4 | 放大倍数(2, 4, 8) |
| model_name | string | 否 | RealESRGAN_x4plus | 模型名称 |
| tile | int | 否 | 0 | 分块大小(0=不使用分块,可根据显存调整) |
| half | bool | 否 | false | 是否使用FP16(降低显存需求) |
**请求示例 (cURL)**:
```bash
curl -X POST http://156.225.27.52:5233/upscale/file \
-H "X-API-Key: realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw" \
-F "file=@/path/to/your/image.jpg" \
-F "scale=4" \
-F "model_name=RealESRGAN_x4plus"
```
**请求示例 (Python)**:
```python
import requests
url = "http://156.225.27.52:5233/upscale/file"
headers = {
"X-API-Key": "realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw"
}
files = {"file": open("image.jpg", "rb")}
params = {
"scale": 4,
"model_name": "RealESRGAN_x4plus",
"tile": 0,
"half": False
}
response = requests.post(url, files=files, headers=headers, params=params)
# 保存结果
with open("result.png", "wb") as f:
f.write(response.content)
print(f"处理时间: {response.headers.get('X-Processing-Time')} 秒")
print(f"放大倍数: {response.headers.get('X-Scale')}x")
print(f"使用模型: {response.headers.get('X-Model')}")
```
**请求示例 (JavaScript/Node.js)**:
```javascript
const FormData = require('form-data');
const fs = require('fs');
const axios = require('axios');
const form = new FormData();
form.append('file', fs.createReadStream('image.jpg'));
form.append('scale', '4');
form.append('model_name', 'RealESRGAN_x4plus');
axios.post('http://156.225.27.52:5233/upscale/file', form, {
headers: {
...form.getHeaders(),
'X-API-Key': 'realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw'
},
responseType: 'arraybuffer'
})
.then(response => {
fs.writeFileSync('result.png', response.data);
console.log('处理时间:', response.headers['x-processing-time'], '秒');
console.log('放大倍数:', response.headers['x-scale'], 'x');
});
```
**响应**:
- **Content-Type**: `image/png`
- **Body**: PNG格式的图片二进制数据(超分辨率处理后的图片)
- **Headers**:
- `X-Processing-Time`: 处理时间(秒)
- `X-Scale`: 放大倍数
- `X-Model`: 使用的模型名称
**错误响应**:
```json
{
"detail": "Invalid API key"
}
```
(当API密钥无效或缺失时)
---
### 6. Base64方式放大图像
**接口地址**: `POST /upscale/base64`
**描述**: 通过Base64编码方式对图片进行超分辨率处理(需要API密钥)
**Content-Type**: `application/json`
**请求头**:
```
X-API-Key: realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw
```
**请求体**:
```json
{
"image_base64": "base64编码的图片字符串",
"scale": 4,
"model_name": "RealESRGAN_x4plus"
}
```
**请求参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| image_base64 | string | 是 | - | Base64编码的图片数据 |
| scale | int | 否 | 4 | 放大倍数(2, 4, 8) |
| model_name | string | 否 | RealESRGAN_x4plus | 模型名称 |
| tile | int | 否 | 0 | 分块大小(Query参数) |
| half | bool | 否 | false | 是否使用FP16(Query参数) |
**请求示例 (cURL)**:
```bash
curl -X POST http://156.225.27.52:5233/upscale/base64 \
-H "X-API-Key: realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw" \
-H "Content-Type: application/json" \
-d '{
"image_base64": "iVBORw0KGgoAAAANSUhEUgAA...",
"scale": 4,
"model_name": "RealESRGAN_x4plus"
}'
```
**请求示例 (Python)**:
```python
import requests
import base64
# 读取图片并转换为base64
with open("image.jpg", "rb") as f:
image_base64 = base64.b64encode(f.read()).decode("utf-8")
url = "http://156.225.27.52:5233/upscale/base64"
headers = {
"X-API-Key": "realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw",
"Content-Type": "application/json"
}
data = {
"image_base64": image_base64,
"scale": 4,
"model_name": "RealESRGAN_x4plus"
}
params = {
"tile": 0,
"half": False
}
response = requests.post(url, json=data, headers=headers, params=params)
if response.status_code == 200:
result = response.json()
# 保存结果
result_image = base64.b64decode(result["image_base64"])
with open("result.png", "wb") as f:
f.write(result_image)
print(f"✅ 成功! 处理时间: {result['processing_time']:.2f}秒")
```
**请求示例 (JavaScript/Node.js)**:
```javascript
const fs = require('fs');
const axios = require('axios');
// 读取图片并转换为base64
const imageBuffer = fs.readFileSync('image.jpg');
const imageBase64 = imageBuffer.toString('base64');
axios.post('http://156.225.27.52:5233/upscale/base64', {
image_base64: imageBase64,
scale: 4,
model_name: 'RealESRGAN_x4plus'
}, {
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw'
},
params: {
tile: 0,
half: false
}
})
.then(response => {
if (response.data.success) {
const resultImage = Buffer.from(response.data.image_base64, 'base64');
fs.writeFileSync('result.png', resultImage);
console.log('处理时间:', response.data.processing_time, '秒');
}
});
```
**响应示例**:
```json
{
"success": true,
"image_base64": "iVBORw0KGgoAAAANSUhEUgAA...",
"processing_time": 2.345,
"scale": 4,
"model": "RealESRGAN_x4plus"
}
```
**响应参数**:
| 参数名 | 类型 | 说明 |
|--------|------|------|
| success | boolean | 是否成功 |
| image_base64 | string | Base64编码的结果图片(PNG格式) |
| processing_time | number | 处理时间(秒) |
| scale | int | 放大倍数 |
| model | string | 使用的模型名称 |
---
### 7. URL方式放大图像(上传到S3)
**接口地址**: `POST /upscale/url`
**描述**: 通过图片URL对图片进行超分辨率处理,处理后的图片自动上传到S3存储桶并返回URL(需要API密钥)
**Content-Type**: `application/json`
**请求头**:
```
X-API-Key: realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw
```
**请求体**:
```json
{
"image_url": "https://example.com/image.jpg",
"scale": 4,
"model_name": "RealESRGAN_x4plus"
}
```
**请求参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| image_url | string | 是 | - | 图片的URL地址 |
| scale | int | 否 | 4 | 放大倍数(2, 4, 8) |
| model_name | string | 否 | RealESRGAN_x4plus | 模型名称 |
| tile | int | 否 | 0 | 分块大小(Query参数) |
| half | bool | 否 | false | 是否使用FP16(Query参数) |
**请求示例 (cURL)**:
```bash
curl -X POST http://156.225.27.52:5233/upscale/url \
-H "X-API-Key: realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw" \
-H "Content-Type: application/json" \
-d '{
"image_url": "https://example.com/image.jpg",
"scale": 4,
"model_name": "RealESRGAN_x4plus"
}'
```
**请求示例 (Python)**:
```python
import requests
url = "http://156.225.27.52:5233/upscale/url"
headers = {
"X-API-Key": "realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw",
"Content-Type": "application/json"
}
data = {
"image_url": "https://example.com/image.jpg",
"scale": 4,
"model_name": "RealESRGAN_x4plus"
}
params = {
"tile": 0,
"half": False
}
response = requests.post(url, json=data, headers=headers, params=params)
if response.status_code == 200:
result = response.json()
print(f"✅ 成功!")
print(f"处理后的图片URL: {result['image_url']}")
print(f"下载时间: {result['download_time']:.2f}秒")
print(f"处理时间: {result['processing_time']:.2f}秒")
print(f"上传时间: {result['upload_time']:.2f}秒")
print(f"总时间: {result['total_time']:.2f}秒")
```
**请求示例 (JavaScript/Node.js)**:
```javascript
const axios = require('axios');
axios.post('http://156.225.27.52:5233/upscale/url', {
image_url: 'https://example.com/image.jpg',
scale: 4,
model_name: 'RealESRGAN_x4plus'
}, {
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw'
},
params: {
tile: 0,
half: false
}
})
.then(response => {
if (response.data.success) {
console.log('✅ 成功!');
console.log('处理后的图片URL:', response.data.image_url);
console.log('下载时间:', response.data.download_time, '秒');
console.log('处理时间:', response.data.processing_time, '秒');
console.log('上传时间:', response.data.upload_time, '秒');
console.log('总时间:', response.data.total_time, '秒');
}
})
.catch(error => {
console.error('❌ 失败:', error.response?.data || error.message);
});
```
**响应示例**:
```json
{
"success": true,
"image_url": "https://s3.1080p.hk/jsxiangche/realesrgan/20260104/abc123def456.png",
"processing_time": 2.345,
"download_time": 0.856,
"upload_time": 0.234,
"total_time": 3.435,
"scale": 4,
"model": "RealESRGAN_x4plus"
}
```
**响应参数**:
| 参数名 | 类型 | 说明 |
|--------|------|------|
| success | boolean | 是否成功 |
| image_url | string | 处理后的图片在S3中的URL地址 |
| processing_time | number | 图片处理时间(秒) |
| download_time | number | 图片下载时间(秒) |
| upload_time | number | 图片上传到S3的时间(秒) |
| total_time | number | 总耗时(秒) |
| scale | int | 放大倍数 |
| model | string | 使用的模型名称 |
**注意事项**:
- 此接口需要S3存储功能已启用(`USE_S3_STORAGE=true`)
- 图片URL必须可公开访问
- 支持的图片格式:JPG, PNG, JPEG等常见格式
- 图片大小限制:最大50MB
- 处理后的图片会自动上传到S3存储桶并返回可访问的URL
---
## 服务管理
Real-ESRGAN API服务通过systemd管理,支持自动启动和崩溃自动重启。
### 服务状态检查
```bash
# 查看服务状态
systemctl status realesrgan-api.service
# 查看服务日志
journalctl -u realesrgan-api.service -f
# 查看最近100行日志
journalctl -u realesrgan-api.service -n 100
```
### 服务控制命令
```bash
# 启动服务
systemctl start realesrgan-api.service
# 停止服务
systemctl stop realesrgan-api.service
# 重启服务
systemctl restart realesrgan-api.service
# 重新加载配置(无需重启)
systemctl reload realesrgan-api.service
# 查看服务状态
systemctl status realesrgan-api.service
```
### 自动启动配置
服务已配置为系统启动时自动启动,崩溃后自动重启:
- **自动启动**: 服务已启用(`systemctl enable realesrgan-api.service`)
- **崩溃重启**: `Restart=always`,服务崩溃后自动重启
- **重启延迟**: `RestartSec=10`,崩溃后等待10秒再重启
- **重启策略**: 服务异常退出时会自动重启,无需手动干预
### 服务配置
服务配置文件位置:`/etc/systemd/system/realesrgan-api.service`
主要配置项:
- **工作目录**: `/root/realesrgan-api`
- **运行用户**: `root`
- **GPU设备**: `CUDA_VISIBLE_DEVICES=1`(使用GPU1)
- **S3存储**: 已启用,环境变量已配置
- **日志输出**: 通过systemd journal管理
### 日志查看
```bash
# 实时查看日志
journalctl -u realesrgan-api.service -f
# 查看今天的日志
journalctl -u realesrgan-api.service --since today
# 查看最近的错误日志
journalctl -u realesrgan-api.service -p err -n 50
```
---
## 错误码说明
| HTTP状态码 | 说明 |
|-----------|------|
| 200 | 请求成功 |
| 400 | 请求参数错误(如文件格式不支持、图片URL无效等) |
| 401 | 未授权(API密钥无效或缺失) |
| 422 | 请求参数验证失败 |
| 500 | 服务器内部错误(如处理失败、S3上传失败等) |
---
## 注意事项
1. **服务地址**:
- 外部访问地址:http://156.225.27.52:5233(通过frpc穿透)
- 本地端口:8002(仅服务器内部访问)
- 远程端口:5233(外部访问端口)
2. **API密钥认证**:
- 所有核心接口(`/upscale/file`、`/upscale/base64` 和 `/upscale/url`)都需要在请求头中添加 `X-API-Key`
- API密钥:`realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw`
- 健康检查、模型列表、API密钥信息等端点无需密钥
3. **支持的图片格式**: 所有常见图片格式(JPG, PNG, JPEG等)
4. **输出格式**: 所有接口返回PNG格式的图片
5. **文件大小限制**:
- 上传文件:建议不超过50MB
- URL方式:图片下载大小限制50MB
6. **处理时间**: 处理时间取决于图片大小和服务器负载,通常在1-10秒之间
7. **放大倍数**: 支持2x、4x、8x放大,根据模型而定
8. **模型选择**:
- `RealESRGAN_x4plus`:推荐,4倍放大,适合大多数场景
- `RealESRGAN_x2plus`:2倍放大,处理速度快
- `RealESRGAN_x8plus`:8倍放大,需要更多显存
- `RealESRGAN_animevideov3`:动漫视频模型,4倍放大
9. **S3存储功能**:
- `/upscale/url` 接口需要S3存储功能启用
- S3配置可通过环境变量设置
- 处理后的图片会自动上传到S3并返回可访问的URL
10. **CORS**: 服务已配置CORS,支持跨域请求
11. **服务管理**:
- 服务通过systemd管理,服务名称:`realesrgan-api.service`
- 系统重启后服务会自动启动(已启用)
- 服务崩溃后会在10秒内自动重启(`Restart=always`)
- 日志通过systemd journal管理,使用 `journalctl -u realesrgan-api.service` 查看
- 服务控制命令:`systemctl start/stop/restart/status realesrgan-api.service`
---
## 完整使用示例
### Python完整示例
```python
import requests
import base64
from pathlib import Path
API_KEY = "realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw"
BASE_URL = "http://156.225.27.52:5233"
# 方法1: 文件上传
def upscale_file(image_path):
url = f"{BASE_URL}/upscale/file"
headers = {"X-API-Key": API_KEY}
with open(image_path, "rb") as f:
files = {"file": f}
params = {"scale": 4, "model_name": "RealESRGAN_x4plus"}
response = requests.post(url, files=files, headers=headers, params=params)
if response.status_code == 200:
output_path = Path(image_path).stem + "_upscaled.png"
with open(output_path, "wb") as f:
f.write(response.content)
print(f"✅ 成功! 结果已保存到: {output_path}")
return output_path
else:
print(f"❌ 失败: {response.json()}")
return None
# 方法2: Base64
def upscale_base64(image_path):
url = f"{BASE_URL}/upscale/base64"
headers = {
"X-API-Key": API_KEY,
"Content-Type": "application/json"
}
with open(image_path, "rb") as f:
image_base64 = base64.b64encode(f.read()).decode("utf-8")
data = {
"image_base64": image_base64,
"scale": 4,
"model_name": "RealESRGAN_x4plus"
}
response = requests.post(url, json=data, headers=headers)
if response.status_code == 200:
result = response.json()
result_image = base64.b64decode(result["image_base64"])
output_path = Path(image_path).stem + "_upscaled.png"
with open(output_path, "wb") as f:
f.write(result_image)
print(f"✅ 成功! 处理时间: {result['processing_time']:.2f}秒")
return output_path
else:
print(f"❌ 失败: {response.json()}")
return None
# 方法3: URL方式(上传到S3)
def upscale_url(image_url):
url = f"{BASE_URL}/upscale/url"
headers = {
"X-API-Key": API_KEY,
"Content-Type": "application/json"
}
data = {
"image_url": image_url,
"scale": 4,
"model_name": "RealESRGAN_x4plus"
}
response = requests.post(url, json=data, headers=headers)
if response.status_code == 200:
result = response.json()
print(f"✅ 成功!")
print(f"处理后的图片URL: {result['image_url']}")
print(f"总时间: {result['total_time']:.2f}秒")
return result["image_url"]
else:
print(f"❌ 失败: {response.json()}")
return None
# 使用示例
if __name__ == "__main__":
image_path = "test_image.jpg"
# 使用文件上传方式
upscale_file(image_path)
# 或使用Base64方式
# upscale_base64(image_path)
# 或使用URL方式
# upscale_url("https://example.com/image.jpg")
```
### JavaScript/Node.js完整示例
```javascript
const FormData = require('form-data');
const fs = require('fs');
const axios = require('axios');
const path = require('path');
const API_KEY = 'realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw';
const BASE_URL = 'http://156.225.27.52:5233';
// 方法1: 文件上传
async function upscaleFile(imagePath) {
const form = new FormData();
form.append('file', fs.createReadStream(imagePath));
form.append('scale', '4');
form.append('model_name', 'RealESRGAN_x4plus');
try {
const response = await axios.post(`${BASE_URL}/upscale/file`, form, {
headers: {
...form.getHeaders(),
'X-API-Key': API_KEY
},
responseType: 'arraybuffer'
});
const outputPath = path.parse(imagePath).name + '_upscaled.png';
fs.writeFileSync(outputPath, response.data);
console.log(`✅ 成功! 结果已保存到: ${outputPath}`);
return outputPath;
} catch (error) {
console.error('❌ 失败:', error.response?.data || error.message);
return null;
}
}
// 方法2: Base64
async function upscaleBase64(imagePath) {
const imageBuffer = fs.readFileSync(imagePath);
const imageBase64 = imageBuffer.toString('base64');
try {
const response = await axios.post(`${BASE_URL}/upscale/base64`, {
image_base64: imageBase64,
scale: 4,
model_name: 'RealESRGAN_x4plus'
}, {
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY
}
});
if (response.data.success) {
const outputPath = path.parse(imagePath).name + '_upscaled.png';
const resultImage = Buffer.from(response.data.image_base64, 'base64');
fs.writeFileSync(outputPath, resultImage);
console.log(`✅ 成功! 处理时间: ${response.data.processing_time.toFixed(2)}秒`);
return outputPath;
}
} catch (error) {
console.error('❌ 失败:', error.response?.data || error.message);
return null;
}
}
// 方法3: URL方式(上传到S3)
async function upscaleUrl(imageUrl) {
try {
const response = await axios.post(`${BASE_URL}/upscale/url`, {
image_url: imageUrl,
scale: 4,
model_name: 'RealESRGAN_x4plus'
}, {
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY
}
});
if (response.data.success) {
console.log('✅ 成功!');
console.log('处理后的图片URL:', response.data.image_url);
console.log('总时间:', response.data.total_time.toFixed(2), '秒');
return response.data.image_url;
}
} catch (error) {
console.error('❌ 失败:', error.response?.data || error.message);
return null;
}
}
// 使用示例
(async () => {
const imagePath = 'test_image.jpg';
// 使用文件上传方式
await upscaleFile(imagePath);
// 或使用Base64方式
// await upscaleBase64(imagePath);
// 或使用URL方式
// await upscaleUrl('https://example.com/image.jpg');
})();
```
---
## 交互式API文档
访问 http://156.225.27.52:5233/docs 可以在浏览器中查看交互式API文档(Swagger UI),支持在线测试接口。
**注意**: 在Swagger UI中测试需要API密钥的接口时,需要先点击右上角的"Authorize"按钮(如果有),或者直接在请求头中添加 `X-API-Key`。
---
## API密钥
当前API密钥:
```
realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw
```
**请求头格式**:
```
X-API-Key: realesrgan_sk_live_MAPF4kLW5QinFz5BO96w-oVk09WnmvBZSyNDG8AxUgo7HGlqssJ4y1XZ38SyQsG9QCsYYOLcp9df6Ku1Jw78Pw
```
**安全提示**:
- 请妥善保管API密钥,不要将其暴露在客户端代码中
- 建议通过环境变量或配置文件管理API密钥
---
## S3存储配置
如果使用 `/upscale/url` 接口,需要配置S3存储。配置方式如下:
**环境变量**:
```bash
USE_S3_STORAGE=true
MINIO_S3_ENDPOINT=s3.1080p.hk
MINIO_S3_USE_SSL=true
MINIO_S3_BUCKET=jsxiangche
MINIO_S3_ACCESS_KEY=AEvT1p0wSNjow3Asu9NV
MINIO_S3_SECRET_KEY=u8nzFl1S6dNoT5PQaQhkHz8Du6iX0hVnPswtvHhs
MINIO_S3_REGION=us-east-1
```
**说明**:
- `USE_S3_STORAGE`: 必须设置为 `true` 才能启用S3上传功能
- 处理后的图片会保存到S3存储桶中,路径格式:`realesrgan/YYYYMMDD/UUID.png`
- 返回的URL可以直接访问和下载

72
SECURITY.md Normal file
View File

@ -0,0 +1,72 @@
# Security Policy
Security is part of the release contract for yedir-ai. This project handles AI provider keys, OAuth credentials, object-storage URLs, user uploads, generated media, and internal generation workflows, so disclosures should be handled carefully and privately.
## Supported Versions
Security fixes are applied to the active `main` branch and release branches maintained by the project owner. Older experimental branches are not guaranteed to receive patches.
## Reporting a Vulnerability
Do not open a public issue for suspected vulnerabilities.
Report privately to the repository owner or project maintainer with:
- A short description of the issue and affected area.
- Reproduction steps or a minimal proof of concept.
- Potential impact, including whether secrets, user data, uploads, signed URLs, or internal APIs are exposed.
- Relevant logs with secrets, cookies, Authorization headers, OAuth codes, signed URLs, prompts, base64 data, and private user content removed.
Expected handling:
- The maintainer will acknowledge the report when it is received.
- Valid issues will be triaged by impact and exploitability.
- Fixes should include regression coverage or a documented verification path where practical.
- Public disclosure should wait until a fix or mitigation is available.
## Sensitive Data Rules
Never commit or paste:
- `.env.local` or real environment variables.
- API keys, OAuth client secrets, OAuth codes, cookies, session tokens, Authorization headers, or passwords.
- Signed object-storage URLs, raw upload URLs, private bucket paths, or third-party task payloads.
- Full prompts, base64 image/video data, or private generated media unless explicitly sanitized.
Use `.env.example` for placeholders only.
## Logging Rules
High-risk server logs should use `createLogger` from `lib/observability/logger`. Browser logs should use `createClientLogger` and stay silent in production unless explicitly enabled.
Before merging security-sensitive changes, run:
```bash
pnpm check:quality
pnpm audit:security
```
For route-level release smoke tests, also run:
```bash
AUTH_TRUST_HOST=true PORT=3001 pnpm dev
SMOKE_BASE_URL=http://127.0.0.1:3001 pnpm smoke:routes
```
If a file starts logging prompts, URLs, provider responses, upload details, OAuth data, or user content, add it to `scripts/check-log-hygiene.mjs` and ensure `pnpm check:logs` passes.
Dependency audit risk is tracked in `docs/dependency-risk-register.md`. Critical/high advisories in runtime dependencies should be remediated or explicitly accepted before production release.
## Deployment Security Notes
- Configure `AUTH_SECRET` and production `AUTH_URL`.
- Use `AUTH_TRUST_HOST=true` only behind a trusted reverse proxy with validated Host headers.
- Keep database, storage, OAuth, and AI provider credentials in the deployment secret store.
- Prefer short database connect timeouts and bounded connection pools.
- Verify `/settings` remains protected and redirects unauthenticated users to login.
See also:
- `docs/deployment.md`
- `docs/operations.md`
- `docs/release-readiness.md`

View File

@ -0,0 +1,228 @@
# 安全改进总结
**日期**: 2026-05-05
**改进范围**: Phase 1 (安全短板) + Phase 2 (部分工程基础)
---
## 已完成的改进
### Phase 1: 安全短板修复 ✅
#### 1. 修复硬编码管理员密码 (create-admin.ts)
**风险等级**: 🟢 低风险
**改动**:
- 从环境变量 `ADMIN_EMAIL` / `ADMIN_PASSWORD` 读取凭据
- 如未设置则自动生成安全随机密码
- 不再将密码打印到 console(仅在自动生成时打印一次)
**向后兼容**: ✅ 是(未设置环境变量时使用默认邮箱 + 随机密码)
**测试建议**:
```bash
# 测试 1: 使用环境变量
ADMIN_EMAIL=admin@test.com ADMIN_PASSWORD=SecurePass123! node create-admin.ts
# 测试 2: 自动生成密码
node create-admin.ts
```
---
#### 2. 修复 trustHost 安全配置
**风险等级**: 🟡 中等风险
**改动**:
- `app/(auth)/auth.config.ts`: `trustHost` 改为条件启用
- Vercel 环境 (`VERCEL=1`) 自动开启
- 其他环境需显式设置 `AUTH_TRUST_HOST=true`
- `app/(auth)/auth.ts`: 移除重复的 `trustHost: true` 覆盖
**潜在影响**:
- ⚠️ 非 Vercel 部署且未设置 `AUTH_TRUST_HOST=true` 时,NextAuth 会严格校验 Host 头
- 如果反向代理未正确传递 Host 头,可能导致登录失败
**测试建议**:
```bash
# 开发环境测试登录流程
pnpm dev
# 访问 http://localhost:3000/login 测试登录
# 生产环境部署前检查
echo $VERCEL # Vercel 环境应为 "1"
echo $AUTH_TRUST_HOST # 非 Vercel 环境应设为 "true"
```
---
#### 3. 添加 HTTP 安全头
**风险等级**: 🟢 低风险
**改动**: `next.config.ts` 新增 `headers()` 函数
- `X-Content-Type-Options: nosniff`
- `X-Frame-Options: SAMEORIGIN`
- `Referrer-Policy: strict-origin-when-cross-origin`
- `Permissions-Policy: camera=(), microphone=(), geolocation=()`
- `Content-Security-Policy` (开发环境允许 unsafe-eval,生产环境更严格)
**潜在影响**:
- CSP 可能阻止某些第三方脚本(如未在 `connect-src` 白名单中的 API)
- 如有嵌入需求(iframe),需调整 `X-Frame-Options`
**测试建议**:
```bash
# 启动后检查响应头
curl -I http://localhost:3000 | grep -E "X-Content-Type|X-Frame|CSP"
```
---
#### 4. 创建 middleware.ts 接入 proxy.ts
**风险等级**: 🔴 **高风险** - 需重点测试
**改动**: 新建 `/middleware.ts`
```typescript
export { proxy as middleware, config } from "./proxy";
```
**影响**:
- ✅ 限流功能**首次生效**(之前 proxy.ts 存在但未被 Next.js 加载)
- ✅ IP 黑名单检查生效
- ✅ 可疑请求检测生效
- ⚠️ 所有 API 路由现在都会经过中间件检查
**潜在问题**:
1. **限流过严**: 如果用户正常使用触发限流(如画布自动保存频繁),会返回 429
2. **误判可疑请求**: `lib/security/request-monitor.ts` 的规则可能误报
3. **性能影响**: 每个请求都要经过 IP 检查、限流检查、可疑模式匹配
**测试建议**:
```bash
# 1. 检查中间件是否加载
pnpm build
cat .next/server/middleware-manifest.json # 应有内容,不再为空
# 2. 测试限流
# 快速刷新页面 100 次,应触发 429
# 3. 测试正常流程
# 登录、创建画布、上传图片、聊天 - 确保都正常工作
# 4. 检查日志
# 查看是否有 "[Security] Rate limit exceeded" 等日志
```
---
#### 5. 补充 .env.example
**风险等级**: 🟢 无风险(纯文档)
**新增条目**:
- `AUTH_URL` / `NEXTAUTH_URL`
- `AUTH_TRUST_HOST`
- `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`
- `ADMIN_EMAIL` / `ADMIN_PASSWORD`
- `FAL_KEY`
---
### Phase 2: 工程基础改进 ✅
#### 6. 建立结构化日志系统
**风险等级**: 🟢 低风险
**新增文件**: `lib/logger.ts`
- 提供 `log.info()`, `log.error()`, `log.warn()`, `log.debug()`
- 支持模块化日志:`createLogger("ModuleName")`
- 开发环境可读格式,生产环境 JSON 格式
- 通过 `LOG_LEVEL` 环境变量控制输出级别
**使用示例**:
```typescript
import { createLogger } from "@/lib/logger";
const log = createLogger("MyService");
log.error("Operation failed", error, { userId: "123" });
```
---
#### 7. 修复 service 层错误吞没
**风险等级**: 🟡 中等风险
**改动**:
- `services/canvas/index.ts`: 添加 logger 导入和辅助函数(未全部迁移)
**潜在问题**:
- `handleError(err, context, fallbackMessage)` 参数顺序与之前不同
---
#### 8. 更新安全文档
**风险等级**: 🟢 无风险(纯文档)
**改动**: `lib/security/IMPLEMENTATION_GUIDE.md` 更新至实际状态
---
## 未完成的任务
### Phase 2 剩余
- ❌ 为缺少验证的路由补充 Zod schema (enhance, rmbg, canvas/text-edit 等)
- ❌ 迁移剩余 service 文件的错误处理(canvas 有 31 个 catch 块未完全迁移)
### Phase 3
- ❌ 补充核心单元测试
- ❌ 添加 Dockerfile 支持
---
## 回滚方案
如果部署后出现问题,可以快速回滚:
### 回滚 middleware(最高优先级)
```bash
# 删除 middleware.ts,限流和安全检查将停止工作
rm middleware.ts
pnpm build
```
### 回滚 trustHost
```bash
# 在 auth.config.ts 中改回
trustHost: true,
```
### 回滚其他改动
```bash
git diff HEAD # 查看所有改动
git checkout HEAD -- <file> # 回滚单个文件
```
---
## 部署前检查清单
- [ ] 在开发环境运行 `pnpm dev`,确保无编译错误
- [ ] 测试登录/登出流程
- [ ] 测试聊天、画布、图片生成等核心功能
- [ ] 快速刷新页面,确认限流触发(应看到 429 错误)
- [ ] 检查 `.next/server/middleware-manifest.json` 不为空
- [ ] 设置生产环境变量:
- `AUTH_TRUST_HOST=true`(如非 Vercel)
- `LOG_LEVEL=warn`(生产环境)
- `ADMIN_PASSWORD=<secure-password>`(如需创建管理员)
- [ ] 运行 `pnpm build` 确保生产构建成功
---
## 监控建议
部署后前 24 小时重点监控:
1. **错误率**: 是否有大量 429 (限流) 或 403 (IP 黑名单) 错误
2. **登录成功率**: trustHost 改动可能影响登录
3. **日志输出**: 检查是否有 "[Security]" 前缀的警告
4. **性能**: 中间件增加的延迟(应 <10ms)
---
## 联系与支持
如遇问题,优先检查:
1. `.next/server/middleware-manifest.json` 是否正确生成
2. 浏览器控制台是否有 CSP 违规警告
3. 服务器日志中是否有 "[Security]" 相关错误

View File

@ -0,0 +1,247 @@
# AI 生图平台白底图接口文档
> 用途:AI 生图平台输入 `SKU` 后,直接从素材库获取该 `SKU` 对应的白底图。
> 当前主域名:`https://sc.jiansun.vip`
> 旧域名:`https://3d.yydd.ai`,保留兼容并跳转到新域名
## 1. 基础信息
- **接口基地址**:`https://sc.jiansun.vip/api/external/`
- **鉴权方式**:请求头 `X-API-Key`
- **返回格式**:`application/json`
- **适用范围**:仅适用于 `标准模型` 类型素材
## 2. 接口说明
### 2.1 按 SKU 查询白底图
**请求**
```http
GET /api/external/standard-model-white-background?sku={SKU}
X-API-Key: 5073534d7a59babbc45397a8eac5943b
```
**请求参数**
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `sku` | string | 是 | SKU 名称。接口按大小写不敏感查询。 |
**请求头**
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `X-API-Key` | string | 是 | 外部接口访问密钥 |
## 3. 成功响应
**响应示例**
```json
{
"success": true,
"data": {
"sku": "text001452",
"materialId": "484bd047-c956-48cf-a0f4-401f696aef8d",
"productModel": "TEXT0014",
"whiteBackground": {
"fixed": [
{
"key": "leftFront45",
"label": "左前45°",
"fileName": "左前45°.png",
"fileSize": 68,
"downloadUrl": "https://3d.yydd.ai/api/external/download?token=xxx"
},
{
"key": "rightFront45",
"label": "右前45°",
"fileName": "右前45°.png",
"fileSize": 68,
"downloadUrl": "https://3d.yydd.ai/api/external/download?token=xxx"
}
],
"other": [
{
"key": "other",
"label": "其他图片",
"fileName": "01-other-1.png",
"fileSize": 68,
"downloadUrl": "https://3d.yydd.ai/api/external/download?token=xxx"
}
]
},
"downloadUrls": {
"zip": "https://3d.yydd.ai/api/external/download?token=xxx"
}
}
}
```
### 3.1 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| `data.sku` | string | 实际命中的 SKU 名称 |
| `data.materialId` | string | 素材 ID |
| `data.productModel` | string | 所属 SPU / 型号 |
| `data.whiteBackgroundUploaded` | boolean | 是否已有白底图 |
| `data.whiteBackgroundComplete` | boolean | 固定 8 张是否已补齐 |
| `data.whiteBackgroundFixedCount` | number | 当前已上传的固定角度图数量 |
| `data.whiteBackgroundOtherCount` | number | 当前已上传的 other 图片数量 |
| `data.whiteBackgroundCount` | number | 当前白底图总数 |
| `data.missingViews` | array | 当前仍缺少的固定角度图 |
| `data.whiteBackground.fixed` | array | 当前已有的固定角度白底图,按固定顺序返回已存在项 |
| `data.whiteBackground.other` | array | 其他图片,可为空数组 |
| `data.downloadUrls.zip` | string | 当前 SKU 白底图整包 ZIP 下载链接 |
### 3.2 `fixed` 固定图顺序
固定图返回顺序固定为:
1. `leftFront45`:左前45°
2. `rightFront45`:右前45°
3. `leftBack45`:左后45°
4. `rightBack45`:右后45°
5. `front`:正视图
6. `back`:后视图
7. `left`:左视图
8. `right`:右视图
### 3.3 单张图片对象字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `key` | string | 固定图为固定角度 key;其他图固定为 `other` |
| `label` | string | 图片说明 |
| `fileName` | string | 文件名 |
| `fileSize` | number | 文件大小,单位字节 |
| `downloadUrl` | string | 单张图片下载链接 |
## 4. 下载方式
### 4.1 下载单张白底图
直接请求响应里的 `downloadUrl` 即可:
```http
GET https://3d.yydd.ai/api/external/download?token=xxx
```
返回内容为对应图片文件,例如:
- `image/png`
- `image/jpeg`
### 4.2 下载整包白底图 ZIP
直接请求:
```http
GET https://3d.yydd.ai/api/external/download?token=xxx
```
当 token 对应的是白底图 ZIP 时,返回:
- `Content-Type: application/zip`
- 文件名:`{SKU}_白底图.zip`
## 5. 错误响应
### 5.1 SKU 不存在
```json
{
"success": false,
"error": {
"code": "SKU_NOT_FOUND",
"message": "未找到对应的标准模型 SKU"
}
}
```
### 5.2 SKU 数据重复
```json
{
"success": false,
"error": {
"code": "SKU_NOT_UNIQUE",
"message": "该 SKU 存在重复数据,暂无法对外提供白底图"
}
}
```
### 5.3 白底图不存在
```json
{
"success": false,
"error": {
"code": "WHITE_BACKGROUND_NOT_FOUND",
"message": "该 SKU 暂无白底图"
}
}
```
### 5.4 API Key 无效
```json
{
"success": false,
"error": {
"code": "INVALID_API_KEY",
"message": "API Key 无效或缺失"
}
}
```
## 6. 接入建议
- **推荐主流程**:先查接口 → 拿到 `fixed` / `other` → 按需下载单张图
- 如果业务需要一次性拉全量,也可以直接使用 `downloadUrls.zip`
- `sku` 查询支持大小写不敏感,但建议调用方统一按大写传参,便于排查
- 接口不再要求固定 8 张必须补齐;只要该 SKU 已有任意白底图,就会返回当前已有图片
- 下载链接为带 token 的受控链接,建议由服务端调用,不建议直接暴露到前端页面
## 7. cURL 示例
### 7.1 查询白底图
```bash
curl -X GET \
"https://3d.yydd.ai/api/external/standard-model-white-background?sku=TEXT001452" \
-H "X-API-Key: 5073534d7a59babbc45397a8eac5943b"
```
### 7.2 下载单张图片
```bash
curl -L \
"https://3d.yydd.ai/api/external/download?token=xxx" \
-o left-front-45.png
```
### 7.3 下载整包 ZIP
```bash
curl -L \
"https://3d.yydd.ai/api/external/download?token=xxx" \
-o TEXT001452_白底图.zip
```
## 8. 当前联调结论
已验证:
- 旧任务平台接口 `GET /api/external/product-assets` 正常
- 旧下载接口 `GET /api/external/download?token=...` 正常
- 新白底图接口 `GET /api/external/standard-model-white-background` 正常
- 新白底图单图下载正常
如果后续还要继续开放更多素材接口,建议继续复用当前这套:
- `X-API-Key`
- `/api/external/*`
- token 下载链接

504
SPU详情接口文档.md Normal file
View File

@ -0,0 +1,504 @@
# AI 生图平台素材接口文档
> 用途:AI 生图平台统一通过同一条接口,查询 `标准模型白底图` 或 `任务-详情整套预览下载链接`。
> 当前主域名:`https://sc.jiansun.vip`
> 当前资源公网域名:`https://s3.jiansun.vip`
> 旧应用域名:`https://3d.yydd.ai`(仅保留兼容跳转)
## 1. 基础信息
- 接口基地址:`https://sc.jiansun.vip/api/external/`
- 鉴权方式:请求头 `X-API-Key`
- 请求方式:`GET`
- 返回格式:`application/json`
- 主接口路径:`/api/external/standard-model-white-background`
- 默认资源类型:`white-background`
返回统一格式:
```json
{
"success": true,
"data": {}
}
```
失败时:
```json
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "错误说明"
}
}
```
## 2. 通用请求规则
### 2.1 请求头
| 参数 | 必填 | 说明 |
|---|---|---|
| `X-API-Key` | 是 | 外部接口访问密钥 |
### 2.2 查询参数
| 参数 | 必填 | 说明 |
|---|---|---|
| `resourceType` | 否 | 资源类型。可选 `white-background` 或 `task-detail`。不传时默认 `white-background` |
| `sku` | 条件必填 | `sku` 和 `spu` 二选一即可 |
| `spu` | 条件必填 | `sku` 和 `spu` 二选一即可 |
补充说明:
- `sku`、`spu` 都按大小写不敏感查询,建议调用方统一传大写。
- 如果 `sku` 和 `spu` 同时传入,接口优先按 `spu` 处理。
- `shopName` 当前与 `categoryName` 含义一致,都是“店铺”。
## 3. 白底图查询
### 3.1 适用场景
- `resourceType=white-background`
- 不传 `resourceType` 时也走这一分支,兼容老调用方
- 支持两种查询方式:
- `sku`:直接拿到该 SKU 的完整白底图信息
- `spu`:先拿到该 SPU 下全部 SKU 清单,再让用户选 SKU
---
### 3.2 按 SKU 查询白底图
**请求**
```http
GET /api/external/standard-model-white-background?resourceType=white-background&sku={SKU}
X-API-Key: your-api-key
```
兼容老调用方式:
```http
GET /api/external/standard-model-white-background?sku={SKU}
X-API-Key: your-api-key
```
**成功响应示例**
```json
{
"success": true,
"data": {
"resourceType": "white-background",
"sku": "TEXT001452",
"materialId": "484bd047-c956-48cf-a0f4-401f696aef8d",
"productModel": "TEXT0014",
"categoryName": "旗舰店",
"shopName": "旗舰店",
"whiteBackgroundUploaded": true,
"whiteBackgroundComplete": false,
"whiteBackgroundFixedCount": 2,
"whiteBackgroundOtherCount": 1,
"whiteBackgroundCount": 3,
"missingViews": [
{
"key": "leftBack45",
"label": "左后45°"
}
],
"whiteBackground": {
"fixed": [
{
"key": "leftFront45",
"label": "左前45°",
"fileName": "左前45°.png",
"fileSize": 123456,
"downloadUrl": "https://sc.jiansun.vip/api/external/download?token=xxx"
},
{
"key": "rightFront45",
"label": "右前45°",
"fileName": "右前45°.png",
"fileSize": 123456,
"downloadUrl": "https://sc.jiansun.vip/api/external/download?token=yyy"
}
],
"other": [
{
"key": "other-1",
"label": "其他图片1",
"fileName": "detail-1.png",
"fileSize": 123456,
"downloadUrl": "https://sc.jiansun.vip/api/external/download?token=zzz"
}
]
},
"downloadUrls": {
"zip": "https://sc.jiansun.vip/api/external/download?token=zip-token"
}
}
}
```
**字段说明**
| 字段 | 说明 |
|---|---|
| `resourceType` | 固定为 `white-background` |
| `sku` | 实际命中的 SKU |
| `materialId` | 标准模型素材 ID |
| `productModel` | 所属 SPU |
| `categoryName` | 店铺 |
| `shopName` | 店铺,当前与 `categoryName` 相同 |
| `whiteBackgroundUploaded` | 是否已有白底图 |
| `whiteBackgroundComplete` | 固定 8 个角度是否已补齐 |
| `whiteBackgroundFixedCount` | 已有固定角度图数量 |
| `whiteBackgroundOtherCount` | 已有 other 图数量 |
| `whiteBackgroundCount` | 白底图总数 |
| `missingViews` | 还缺少的固定角度图 |
| `whiteBackground.fixed` | 固定角度白底图列表 |
| `whiteBackground.other` | 其他白底图列表 |
| `downloadUrls.zip` | 当前 SKU 白底图 ZIP 下载链接 |
固定角度顺序固定为:
1. `leftFront45`
2. `rightFront45`
3. `leftBack45`
4. `rightBack45`
5. `front`
6. `back`
7. `left`
8. `right`
单张图片对象字段:
| 字段 | 说明 |
|---|---|
| `key` | 固定图为固定角度 key,other 图为 `other-*` |
| `label` | 图片说明 |
| `fileName` | 文件名 |
| `fileSize` | 文件大小,单位字节 |
| `downloadUrl` | 单张白底图下载链接 |
---
### 3.3 按 SPU 查询白底图 SKU 清单
**请求**
```http
GET /api/external/standard-model-white-background?resourceType=white-background&spu={SPU}
X-API-Key: your-api-key
```
**成功响应示例**
```json
{
"success": true,
"data": {
"resourceType": "white-background",
"spu": "TEXT0014",
"materialId": "484bd047-c956-48cf-a0f4-401f696aef8d",
"categoryName": "旗舰店",
"shopName": "旗舰店",
"skuCount": 2,
"skuList": [
{
"skuName": "TEXT001452",
"skuId": "sku-id-1",
"hasWhiteBackground": true,
"whiteBackgroundCount": 2,
"previewImage": {
"downloadUrl": "https://sc.jiansun.vip/api/external/download?token=xxx",
"fileName": "右前45°.png",
"fileSize": 123456
}
},
{
"skuName": "TEXT001453",
"skuId": "sku-id-2",
"hasWhiteBackground": false,
"whiteBackgroundCount": 0,
"previewImage": null
}
]
}
}
```
**字段说明**
| 字段 | 说明 |
|---|---|
| `resourceType` | 固定为 `white-background` |
| `spu` | 实际命中的 SPU |
| `materialId` | 标准模型素材 ID |
| `categoryName` | 店铺 |
| `shopName` | 店铺,当前与 `categoryName` 相同 |
| `skuCount` | 该 SPU 下 SKU 数量 |
| `skuList[].skuName` | SKU 名称 |
| `skuList[].skuId` | SKU 记录 ID |
| `skuList[].hasWhiteBackground` | 是否已有任意白底图 |
| `skuList[].whiteBackgroundCount` | 当前固定角度白底图数量 |
| `skuList[].previewImage` | 右前 45° 预览图;如果没有该角度则为 `null` |
说明:
- 这个接口主要给 AI 平台先展示 SKU 列表用。
- 选定 SKU 后,再调用“按 SKU 查询白底图”接口拿完整白底图信息。
## 4. 任务-详情整套查询
### 4.1 适用场景
- `resourceType=task-detail`
- 只返回:
- `materialType='任务-详情'`
- `isFullSet=true`
- `deletedAt=null`
的候选素材
- 返回的是**预览图直链数组**,不是 zip,也不是 token 下载链接
- 返回顺序按 `uploadedAt` 倒序,也就是最新上传的排前面
---
### 4.2 按 SPU 查询任务-详情整套候选
**请求**
```http
GET /api/external/standard-model-white-background?resourceType=task-detail&spu={SPU}
X-API-Key: your-api-key
```
**成功响应示例**
```json
{
"success": true,
"data": {
"resourceType": "task-detail",
"spu": "SPU001",
"sku": null,
"categoryName": "旗舰店",
"shopName": "旗舰店",
"candidateCount": 2,
"candidates": [
{
"materialId": "material-2",
"taskNo": "TASK202601141713525624",
"productModel": "SPU001",
"categoryName": "旗舰店",
"shopName": "旗舰店",
"sceneName": "客厅",
"uploadedAt": "2026-03-18T10:00:00.000Z",
"isFullSet": true,
"downloadUrls": {
"main": [
"https://s3.jiansun.vip/gongxiangpan/preview/main-1.jpg"
],
"sku": [
"https://s3.jiansun.vip/gongxiangpan/preview/sku-1.jpg"
],
"detail": [
"https://s3.jiansun.vip/gongxiangpan/preview/detail-1.jpg"
],
"all": [
"https://s3.jiansun.vip/gongxiangpan/preview/main-1.jpg",
"https://s3.jiansun.vip/gongxiangpan/preview/sku-1.jpg",
"https://s3.jiansun.vip/gongxiangpan/preview/detail-1.jpg"
]
}
}
]
}
}
```
---
### 4.3 按 SKU 查询任务-详情整套候选
**请求**
```http
GET /api/external/standard-model-white-background?resourceType=task-detail&sku={SKU}
X-API-Key: your-api-key
```
**处理逻辑**
- 先通过 `SKU -> 标准模型 SPU`
- 再按这个 SPU 查询所有整套 `任务-详情` 候选
**成功响应示例**
```json
{
"success": true,
"data": {
"resourceType": "task-detail",
"spu": "SPU001",
"sku": "SKU001",
"categoryName": "旗舰店",
"shopName": "旗舰店",
"candidateCount": 1,
"candidates": [
{
"materialId": "material-2",
"taskNo": "TASK202601141713525624",
"productModel": "SPU001",
"categoryName": "旗舰店",
"shopName": "旗舰店",
"sceneName": "客厅",
"uploadedAt": "2026-03-18T10:00:00.000Z",
"isFullSet": true,
"downloadUrls": {
"main": [],
"sku": [],
"detail": [],
"all": []
}
}
]
}
}
```
**候选素材字段说明**
| 字段 | 说明 |
|---|---|
| `materialId` | 任务-详情素材 ID |
| `taskNo` | 任务号 |
| `productModel` | 对应 SPU |
| `categoryName` | 店铺 |
| `shopName` | 店铺,当前与 `categoryName` 相同 |
| `sceneName` | 场景 |
| `uploadedAt` | 上传时间,ISO 8601 格式 |
| `isFullSet` | 是否整套,返回结果中固定为 `true` |
| `downloadUrls.main` | 主图预览直链数组 |
| `downloadUrls.sku` | SKU 图预览直链数组 |
| `downloadUrls.detail` | 详情图预览直链数组 |
| `downloadUrls.all` | `main + sku + detail` 合并后的数组 |
说明:
- `task-detail` 分支返回的是**全部候选**,不是只返回一条。
- 预览链接可以直接打开或下载。
- 历史 `任务-详情` 如果没有标记 `isFullSet=true`,不会被返回。
## 5. 下载说明
### 5.1 白底图
白底图返回的是下载链接:
- 单张图:`whiteBackground.fixed[*].downloadUrl` / `whiteBackground.other[*].downloadUrl`
- 整包 ZIP:`downloadUrls.zip`
调用方式:
```http
GET https://sc.jiansun.vip/api/external/download?token=xxx
```
### 5.2 任务-详情整套
任务-详情返回的是可直接访问的预览图直链数组:
- `downloadUrls.main`
- `downloadUrls.sku`
- `downloadUrls.detail`
- `downloadUrls.all`
这一分支**不返回 zip**,也**不需要再调下载 token 接口**。
## 6. 错误码
### 6.1 通用错误
| HTTP 状态码 | 错误码 | 说明 |
|---|---|---|
| 401 | `INVALID_API_KEY` | API Key 无效或缺失 |
| 400 | `MISSING_PARAM` | 没有传 `sku` 或 `spu` |
| 400 | `RESOURCE_TYPE_INVALID` | `resourceType` 不是 `white-background` 或 `task-detail` |
| 500 | `SERVER_ERROR` | 服务器内部错误 |
### 6.2 白底图分支错误
| HTTP 状态码 | 错误码 | 说明 |
|---|---|---|
| 404 | `SKU_NOT_FOUND` | 未找到对应的标准模型 SKU |
| 409 | `SKU_NOT_UNIQUE` | SKU 重复,暂无法对外提供白底图 |
| 404 | `SPU_NOT_FOUND` | 未找到对应的标准模型 SPU |
| 404 | `NO_SKUS` | 该 SPU 下没有 SKU 数据 |
| 404 | `WHITE_BACKGROUND_NOT_FOUND` | 该 SKU 暂无白底图 |
### 6.3 任务-详情分支错误
| HTTP 状态码 | 错误码 | 说明 |
|---|---|---|
| 404 | `SPU_NOT_FOUND` | 未找到对应的标准模型 SPU |
| 404 | `SKU_NOT_FOUND` | 未找到对应的标准模型 SKU |
| 409 | `SKU_NOT_UNIQUE` | SKU 重复,暂无法对外提供任务-详情整套素材 |
| 404 | `TASK_DETAIL_NOT_FOUND` | 该 SPU 或该 SKU 所属 SPU 下暂无任务-详情素材 |
| 404 | `FULL_SET_NOT_FOUND` | 有任务-详情素材,但没有任何一条被标记为整套 |
## 7. cURL 示例
### 7.1 按 SKU 查询白底图
```bash
curl -X GET \
"https://sc.jiansun.vip/api/external/standard-model-white-background?sku=TEXT001452" \
-H "X-API-Key: your-api-key"
```
### 7.2 按 SPU 查询白底图 SKU 清单
```bash
curl -X GET \
"https://sc.jiansun.vip/api/external/standard-model-white-background?resourceType=white-background&spu=TEXT0014" \
-H "X-API-Key: your-api-key"
```
### 7.3 按 SPU 查询任务-详情整套候选
```bash
curl -X GET \
"https://sc.jiansun.vip/api/external/standard-model-white-background?resourceType=task-detail&spu=SPU001" \
-H "X-API-Key: your-api-key"
```
### 7.4 按 SKU 查询任务-详情整套候选
```bash
curl -X GET \
"https://sc.jiansun.vip/api/external/standard-model-white-background?resourceType=task-detail&sku=SKU001" \
-H "X-API-Key: your-api-key"
```
## 8. 接入建议
- 老的白底图调用方可以不改,继续传 `sku` 即可。
- 如果是 AI 生图平台新流程,建议:
- 白底图:`spu -> sku -> 白底图详情`
- 任务-详情:直接按 `spu` 或 `sku` 查整套候选
- `task-detail` 返回多个候选时,建议调用方按 `uploadedAt` 再做一次前端展示排序或筛选。
- `task-detail` 使用的是预览图直链,不适合再按源文件 zip 的思路处理。
## 9. 当前联调状态
截至 `2026-03-18`,本地代码已完成并验证通过的内容:
- `/api/external/standard-model-white-background` 已支持 `white-background` 和 `task-detail`
- 不传 `resourceType` 时,白底图老调用方式保持兼容
- `task-detail` 已支持 `spu` / `sku` 两种查询
- 返回中已补充 `shopName`
- `任务-详情` 整套字段 `isFullSet` 已接入接口返回逻辑
- 本地构建 `npm run build` 已通过

558
SPU详情接口文档2.0.md Normal file
View File

@ -0,0 +1,558 @@
# AI 生图平台素材接口文档
> 用途:AI 生图平台统一通过同一条接口,查询 `标准模型白底图` 或 `任务-详情整套预览下载链接`
> 当前主域名:`https://sc.jiansun.vip`
> 当前资源主域名:`https://s3.jiansun.vip`
> 备用资源域名:`https://s3.yydd.ai`(备用 / 回切用,非默认对接地址)
> 旧应用域名:`https://3d.yydd.ai`(仅保留兼容跳转)
## 1. 基础信息
- 接口基地址:`https://sc.jiansun.vip/api/external/`
- 鉴权方式:请求头 `X-API-Key`
- 请求方式:`GET`
- 返回格式:`application/json`
- 主接口路径:`/api/external/standard-model-white-background`
- 默认资源类型:`white-background`
### 1.1 当前网络口径
- 接口查询、鉴权和下载 token 入口统一使用 `https://sc.jiansun.vip`
- `task-detail` 分支返回的预览图直链,当前默认资源域名使用 `https://s3.jiansun.vip`
- `https://s3.yydd.ai` 当前只保留为备用资源域名;除非单独通知切换,否则调用方继续以 `https://s3.jiansun.vip` 为准
返回统一格式:
```json
{
"success": true,
"data": {}
}
```
失败时:
```json
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "错误说明"
}
}
```
### 1.2 响应诊断头
为便于调用方排查“接口慢”到底是服务端慢还是链路慢,当前接口额外返回以下响应头:
| 响应头 | 说明 |
|---|---|
| `X-Trace-Id` | 本次请求追踪 ID,排查时可提供给素材平台定位服务端日志 |
| `X-Server-Duration-Ms` | 服务端实际处理耗时,单位毫秒 |
| `Server-Timing` | 标准服务端耗时头,内容与上面一致 |
| `X-Resource-Type` | 本次实际走的资源分支 |
| `X-Request-Sku` | 本次实际识别后的 SKU;未传时不返回 |
| `X-Request-Spu` | 本次实际识别后的 SPU;未传时不返回 |
## 2. 通用请求规则
### 2.1 请求头
| 参数 | 必填 | 说明 |
|---|---|---|
| `X-API-Key` | 是 | 外部接口访问密钥 |
### 2.2 查询参数
| 参数 | 必填 | 说明 |
|---|---|---|
| `resourceType` | 否 | 资源类型。可选 `white-background` 或 `task-detail`。不传时默认 `white-background` |
| `sku` | 条件必填 | `sku` 和 `spu` 二选一即可 |
| `spu` | 条件必填 | `sku` 和 `spu` 二选一即可 |
补充说明:
- `sku`、`spu` 都按大小写不敏感查询,建议调用方统一传大写。
- 当前服务端会先把 `sku`、`spu` 标准化为大写后再做精确匹配,建议调用方直接传大写,避免自身重复大小写转换。
- 如果 `sku` 和 `spu` 同时传入,接口优先按 `spu` 处理。
- `shopName` 当前与 `categoryName` 含义一致,都是“店铺”。
## 3. 白底图查询
### 3.1 适用场景
- `resourceType=white-background`
- 不传 `resourceType` 时也走这一分支,兼容老调用方
- 支持两种查询方式:
- `sku`:直接拿到该 SKU 的完整白底图信息
- `spu`:先拿到该 SPU 下全部 SKU 清单,再让用户选 SKU
---
### 3.2 按 SKU 查询白底图
**请求**
```http
GET /api/external/standard-model-white-background?resourceType=white-background&sku={SKU}
X-API-Key: your-api-key
```
兼容老调用方式:
```http
GET /api/external/standard-model-white-background?sku={SKU}
X-API-Key: your-api-key
```
**成功响应示例**
```json
{
"success": true,
"data": {
"resourceType": "white-background",
"sku": "TEXT001452",
"materialId": "484bd047-c956-48cf-a0f4-401f696aef8d",
"productModel": "TEXT0014",
"categoryName": "旗舰店",
"shopName": "旗舰店",
"whiteBackgroundUploaded": true,
"whiteBackgroundComplete": false,
"whiteBackgroundFixedCount": 2,
"whiteBackgroundOtherCount": 1,
"whiteBackgroundCount": 3,
"missingViews": [
{
"key": "leftBack45",
"label": "左后45°"
}
],
"whiteBackground": {
"fixed": [
{
"key": "leftFront45",
"label": "左前45°",
"fileName": "左前45°.png",
"fileSize": 123456,
"downloadUrl": "https://sc.jiansun.vip/api/external/download?token=xxx"
},
{
"key": "rightFront45",
"label": "右前45°",
"fileName": "右前45°.png",
"fileSize": 123456,
"downloadUrl": "https://sc.jiansun.vip/api/external/download?token=yyy"
}
],
"other": [
{
"key": "other-1",
"label": "其他图片1",
"fileName": "detail-1.png",
"fileSize": 123456,
"downloadUrl": "https://sc.jiansun.vip/api/external/download?token=zzz"
}
]
},
"downloadUrls": {
"zip": "https://sc.jiansun.vip/api/external/download?token=zip-token"
}
}
}
```
**字段说明**
| 字段 | 说明 |
|---|---|
| `resourceType` | 固定为 `white-background` |
| `sku` | 实际命中的 SKU |
| `materialId` | 标准模型素材 ID |
| `productModel` | 所属 SPU |
| `categoryName` | 店铺 |
| `shopName` | 店铺,当前与 `categoryName` 相同 |
| `whiteBackgroundUploaded` | 是否已有白底图 |
| `whiteBackgroundComplete` | 固定 8 个角度是否已补齐 |
| `whiteBackgroundFixedCount` | 已有固定角度图数量 |
| `whiteBackgroundOtherCount` | 已有 other 图数量 |
| `whiteBackgroundCount` | 白底图总数 |
| `missingViews` | 还缺少的固定角度图 |
| `whiteBackground.fixed` | 固定角度白底图列表 |
| `whiteBackground.other` | 其他白底图列表 |
| `downloadUrls.zip` | 当前 SKU 白底图 ZIP 下载链接 |
固定角度顺序固定为:
1. `leftFront45`
2. `rightFront45`
3. `leftBack45`
4. `rightBack45`
5. `front`
6. `back`
7. `left`
8. `right`
单张图片对象字段:
| 字段 | 说明 |
|---|---|
| `key` | 固定图为固定角度 key,other 图为 `other-*` |
| `label` | 图片说明 |
| `fileName` | 文件名 |
| `fileSize` | 文件大小,单位字节 |
| `downloadUrl` | 单张白底图下载链接 |
---
### 3.3 按 SPU 查询白底图 SKU 清单
**请求**
```http
GET /api/external/standard-model-white-background?resourceType=white-background&spu={SPU}
X-API-Key: your-api-key
```
**成功响应示例**
```json
{
"success": true,
"data": {
"resourceType": "white-background",
"spu": "TEXT0014",
"materialId": "484bd047-c956-48cf-a0f4-401f696aef8d",
"categoryName": "旗舰店",
"shopName": "旗舰店",
"skuCount": 2,
"skuList": [
{
"skuName": "TEXT001452",
"skuId": "sku-id-1",
"hasWhiteBackground": true,
"whiteBackgroundCount": 2,
"previewImage": {
"downloadUrl": "https://sc.jiansun.vip/api/external/download?token=xxx",
"fileName": "右前45°.png",
"fileSize": 123456
}
},
{
"skuName": "TEXT001453",
"skuId": "sku-id-2",
"hasWhiteBackground": false,
"whiteBackgroundCount": 0,
"previewImage": null
}
]
}
}
```
**字段说明**
| 字段 | 说明 |
|---|---|
| `resourceType` | 固定为 `white-background` |
| `spu` | 实际命中的 SPU |
| `materialId` | 标准模型素材 ID |
| `categoryName` | 店铺 |
| `shopName` | 店铺,当前与 `categoryName` 相同 |
| `skuCount` | 该 SPU 下 SKU 数量 |
| `skuList[].skuName` | SKU 名称 |
| `skuList[].skuId` | SKU 记录 ID |
| `skuList[].hasWhiteBackground` | 是否已有任意白底图 |
| `skuList[].whiteBackgroundCount` | 当前固定角度白底图数量 |
| `skuList[].previewImage` | 右前 45° 预览图;如果没有该角度则为 `null` |
说明:
- 这个接口主要给 AI 平台先展示 SKU 列表用。
- 选定 SKU 后,再调用“按 SKU 查询白底图”接口拿完整白底图信息。
## 4. 任务-详情整套查询
### 4.1 适用场景
- `resourceType=task-detail`
- 按 `任务-详情` 素材类型直接查询,不再要求先存在 `标准模型`
- 只返回:
- `materialType='任务-详情'`
- 同时拥有主图、SKU 图、详情图
- `deletedAt=null`
的候选素材
- 返回的是**预览图直链数组**,不是 zip,也不是 token 下载链接
- 返回顺序按 `uploadedAt` 倒序,也就是最新上传的排前面
- 当前默认返回的资源直链域名是 `https://s3.jiansun.vip`
- `https://s3.yydd.ai` 保留备用说明,但不是当前默认示例地址
---
### 4.2 按 SPU 查询任务-详情整套候选
**请求**
```http
GET /api/external/standard-model-white-background?resourceType=task-detail&spu={SPU}
X-API-Key: your-api-key
```
注意:
- `task-detail` 响应顶层的 `categoryName` / `shopName` 是便捷字段,取本次返回候选列表中的第一条候选素材。
- 如果 AI 平台要展示候选素材的实际店铺,请优先读取 `candidates[].categoryName` 或 `candidates[].shopName`。
- 如果同一个 SPU 下有多条候选素材,且它们店铺不同,请以各自 `candidates[]` 内的字段为准。
**成功响应示例**
```json
{
"success": true,
"data": {
"resourceType": "task-detail",
"spu": "SPU001",
"sku": null,
"categoryName": "旗舰店",
"shopName": "旗舰店",
"candidateCount": 1,
"candidates": [
{
"materialId": "material-2",
"taskNo": "TASK202601141713525624",
"productModel": "SPU001",
"categoryName": "旗舰店",
"shopName": "旗舰店",
"sceneName": "客厅",
"uploadedAt": "2026-03-18T10:00:00.000Z",
"isFullSet": true,
"downloadUrls": {
"main": [
"https://s3.jiansun.vip/gongxiangpan/preview/main-1.jpg"
],
"sku": [
"https://s3.jiansun.vip/gongxiangpan/preview/sku-1.jpg"
],
"detail": [
"https://s3.jiansun.vip/gongxiangpan/preview/detail-1.jpg"
],
"all": [
"https://s3.jiansun.vip/gongxiangpan/preview/main-1.jpg",
"https://s3.jiansun.vip/gongxiangpan/preview/sku-1.jpg",
"https://s3.jiansun.vip/gongxiangpan/preview/detail-1.jpg"
]
}
}
]
}
}
```
---
### 4.3 按 SKU 查询任务-详情整套候选
**请求**
```http
GET /api/external/standard-model-white-background?resourceType=task-detail&sku={SKU}
X-API-Key: your-api-key
```
**处理逻辑**
- 先通过 `SKU -> 标准模型 SPU`
- 再按这个 SPU 查询所有整套 `任务-详情` 候选
- 顶层 `categoryName` / `shopName` 是便捷字段,取本次返回候选列表中的第一条候选素材
- 如果 AI 平台要展示候选素材的实际店铺,请优先读取 `candidates[].categoryName` 或 `candidates[].shopName`
**成功响应示例**
```json
{
"success": true,
"data": {
"resourceType": "task-detail",
"spu": "SPU001",
"sku": "SKU001",
"categoryName": "旗舰店",
"shopName": "旗舰店",
"candidateCount": 1,
"candidates": [
{
"materialId": "material-2",
"taskNo": "TASK202601141713525624",
"productModel": "SPU001",
"categoryName": "旗舰店",
"shopName": "旗舰店",
"sceneName": "客厅",
"uploadedAt": "2026-03-18T10:00:00.000Z",
"isFullSet": true,
"downloadUrls": {
"main": [
"https://s3.jiansun.vip/gongxiangpan/preview/main-1.jpg"
],
"sku": [
"https://s3.jiansun.vip/gongxiangpan/preview/sku-1.jpg"
],
"detail": [
"https://s3.jiansun.vip/gongxiangpan/preview/detail-1.jpg"
],
"all": [
"https://s3.jiansun.vip/gongxiangpan/preview/main-1.jpg",
"https://s3.jiansun.vip/gongxiangpan/preview/sku-1.jpg",
"https://s3.jiansun.vip/gongxiangpan/preview/detail-1.jpg"
]
}
}
]
}
}
```
**候选素材字段说明**
| 字段 | 说明 |
|---|---|
| `materialId` | 任务-详情素材 ID |
| `taskNo` | 任务号 |
| `productModel` | 对应 SPU |
| `categoryName` | 店铺 |
| `shopName` | 店铺,当前与 `categoryName` 相同 |
| `sceneName` | 场景 |
| `uploadedAt` | 上传时间,ISO 8601 格式 |
| `isFullSet` | 是否整套;当前规则为“同时拥有主图、SKU 图、详情图” |
| `downloadUrls.main` | 主图预览直链数组 |
| `downloadUrls.sku` | SKU 图预览直链数组 |
| `downloadUrls.detail` | 详情图预览直链数组 |
| `downloadUrls.all` | `main + sku + detail` 合并后的数组 |
说明:
- `task-detail` 分支返回的是**全部候选**,不是只返回一条。
- 预览链接可以直接打开或下载。
- 历史 `任务-详情` 会按同一套资产规则判断,不再依赖人工勾选。
- 顶层 `categoryName` / `shopName` 是便捷字段;如果要给用户展示候选素材的真实店铺,请优先使用 `candidates[].shopName`。
## 5. 下载说明
### 5.1 白底图
白底图返回的是下载链接:
- 单张图:`whiteBackground.fixed[*].downloadUrl` / `whiteBackground.other[*].downloadUrl`
- 整包 ZIP:`downloadUrls.zip`
调用方式:
```http
GET https://sc.jiansun.vip/api/external/download?token=xxx
```
### 5.2 任务-详情整套
任务-详情返回的是可直接访问的预览图直链数组:
- `downloadUrls.main`
- `downloadUrls.sku`
- `downloadUrls.detail`
- `downloadUrls.all`
这一分支**不返回 zip**,也**不需要再调下载 token 接口**。
补充说明:
- 当前默认回包中的预览图资源域名为 `https://s3.jiansun.vip`
- `https://s3.yydd.ai` 仅保留为备用资源域名说明,不作为当前默认对接地址
## 6. 错误码
### 6.1 通用错误
| HTTP 状态码 | 错误码 | 说明 |
|---|---|---|
| 401 | `INVALID_API_KEY` | API Key 无效或缺失 |
| 400 | `MISSING_PARAM` | 没有传 `sku` 或 `spu` |
| 400 | `RESOURCE_TYPE_INVALID` | `resourceType` 不是 `white-background` 或 `task-detail` |
| 500 | `SERVER_ERROR` | 服务器内部错误 |
### 6.2 白底图分支错误
| HTTP 状态码 | 错误码 | 说明 |
|---|---|---|
| 404 | `SKU_NOT_FOUND` | 未找到对应的标准模型 SKU |
| 409 | `SKU_NOT_UNIQUE` | SKU 重复,暂无法对外提供白底图 |
| 404 | `SPU_NOT_FOUND` | 未找到对应的标准模型 SPU |
| 404 | `NO_SKUS` | 该 SPU 下没有 SKU 数据 |
| 404 | `WHITE_BACKGROUND_NOT_FOUND` | 该 SKU 暂无白底图 |
### 6.3 任务-详情分支错误
| HTTP 状态码 | 错误码 | 说明 |
|---|---|---|
| 404 | `SKU_NOT_FOUND` | 未找到对应的标准模型 SKU |
| 409 | `SKU_NOT_UNIQUE` | SKU 重复,暂无法对外提供任务-详情整套素材 |
| 404 | `TASK_DETAIL_NOT_FOUND` | 该 SPU 下,或该 SKU 所属 SPU 下暂无任务-详情素材 |
| 404 | `FULL_SET_NOT_FOUND` | 有任务-详情素材,但没有任何一条同时拥有主图、SKU 图、详情图 |
## 7. cURL 示例
### 7.1 按 SKU 查询白底图
```bash
curl -X GET \
"https://sc.jiansun.vip/api/external/standard-model-white-background?sku=TEXT001452" \
-H "X-API-Key: your-api-key"
```
### 7.2 按 SPU 查询白底图 SKU 清单
```bash
curl -X GET \
"https://sc.jiansun.vip/api/external/standard-model-white-background?resourceType=white-background&spu=TEXT0014" \
-H "X-API-Key: your-api-key"
```
### 7.3 按 SPU 查询任务-详情整套候选
```bash
curl -X GET \
"https://sc.jiansun.vip/api/external/standard-model-white-background?resourceType=task-detail&spu=SPU001" \
-H "X-API-Key: your-api-key"
```
### 7.4 按 SKU 查询任务-详情整套候选
```bash
curl -X GET \
"https://sc.jiansun.vip/api/external/standard-model-white-background?resourceType=task-detail&sku=SKU001" \
-H "X-API-Key: your-api-key"
```
## 8. 接入建议
- 老的白底图调用方可以不改,继续传 `sku` 即可。
- 如果是 AI 生图平台新流程,建议:
- 白底图:`spu -> sku -> 白底图详情`
- 任务-详情:直接按 `spu` 或 `sku` 查整套候选
- `task-detail` 返回多个候选时,建议调用方按 `uploadedAt` 再做一次前端展示排序或筛选。
- `task-detail` 使用的是预览图直链,不适合再按源文件 zip 的思路处理。
## 9. 当前联调状态
截至 `2026-03-21`,正式环境已部署并验证通过的内容:
- `/api/external/standard-model-white-background` 已支持 `white-background` 和 `task-detail`
- 不传 `resourceType` 时,白底图老调用方式保持兼容
- `task-detail` 已支持 `spu` / `sku` 两种查询
- 返回中已补充 `shopName`
- `任务-详情` 整套字段 `isFullSet` 已改为按主图 / SKU 图 / 详情图自动判定
- 对外文档默认资源域名已统一到 `https://s3.jiansun.vip`,同时保留 `https://s3.yydd.ai` 作为备用说明
- 相关测试已通过:`standard-model-white-background-route`、`task-detail-full-set`、`url-rewrite`
- 构建验证已通过:`npm run build`
- 正式环境健康检查已通过:`/api/health`
- 正式环境真实调用已验证:
- `resourceType=task-detail&spu=CTG0290` 返回 `200`
- `resourceType=task-detail&sku=DNZ025802` 返回 `200`
- 白底图 `spu=CTG0149` 返回 `200`

57
SUPPORT.md Normal file
View File

@ -0,0 +1,57 @@
# Support
This document explains where to ask for help and what information to include.
## Before Asking
Please check:
- `README.md` for setup and project boundaries.
- `docs/deployment.md` for production deployment.
- `docs/operations.md` for quality gates, logging, and smoke tests.
- `docs/release-readiness.md` for release verification.
- Existing GitHub issues.
## Bugs
Use the Bug report issue template when you can provide reproduction steps.
Include:
- What happened and what you expected.
- Route, workflow, or UI area affected.
- Browser, OS, Node, pnpm, and deployment context when relevant.
- Sanitized logs or screenshots.
Do not include secrets, cookies, OAuth codes, Authorization headers, signed URLs, `.env.local` values, private prompts, base64 data, or private user content.
## Feature Requests
Use the Feature request issue template. Keep proposals focused and include:
- The user or operator problem.
- Proposed behavior or API shape.
- Acceptance criteria.
- Relevant alternatives considered.
## Security Issues
Do not open public issues for vulnerabilities, auth bypasses, token leaks, signed URL exposure, private data exposure, or suspicious dependency behavior.
Follow `SECURITY.md` and report privately to the maintainer.
## Deployment and Operations Help
For deployment questions, include sanitized versions of:
- Hosting target.
- Node and pnpm versions.
- Database/storage provider type.
- Relevant command output.
- Whether `pnpm check:quality` and `pnpm smoke:routes` pass.
Never paste real environment variables. Use placeholder names from `.env.example`.
## Maintainer Expectations
The project is maintained as a production-oriented codebase. Issues and discussions that include unsafe secrets, private user data, or unsanitized provider payloads may be edited or closed to protect users and operators.

View File

@ -0,0 +1,2 @@
__pycache__/
archive/

View File

@ -0,0 +1,88 @@
# Is legal the same as legitimate: AI reimplementation and the erosion of copyleft
- **Date:** March 9, 2026
- **Author:** Hong Minhee
- **Source context:** _Hong Minhee on Things_ (English / 日本語 / 朝鮮語 (國漢文) / 한국어 (한글))
- **Archive note:** This copy was normalized from user-provided text for this repository's research/archive context. Site navigation/footer language links were converted into metadata.
Last week, Dan Blanchard, the maintainer of chardet—a Python library for detecting text encodings used by roughly 130 million projects a month—released a new version. Version 7.0 is 48 times faster than its predecessor, supports multiple cores, and was redesigned from the ground up. Anthropic's Claude is listed as a contributor. The license changed from LGPL to MIT.
Blanchard's account is that he never looked at the existing source code directly. He fed only the API and the test suite to Claude and asked it to reimplement the library from scratch. The resulting code shares less than 1.3% similarity with any prior version, as measured by JPlag. His conclusion: this is an independent new work, and he is under no obligation to carry forward the LGPL. Mark Pilgrim, the library's original author, opened a GitHub issue to object. The LGPL requires that modifications be distributed under the same license, and a reimplementation produced with ample exposure to the original codebase cannot, in Pilgrim's view, pass as a clean-room effort.
The dispute drew responses from two prominent figures in the open source world. Armin Ronacher, the creator of Flask, welcomed the relicensing. Salvatore Sanfilippo (antirez), the creator of Redis, published a broader defense of AI reimplementation, grounding it in copyright law and the history of the GNU project. Both conclude, by different routes, that what Blanchard did is legitimate. I respect both writers, and I think both are wrong—or more precisely, both are evading the question that actually matters.
That question is this: does legal mean legitimate? Neither piece answers it. Both move from “this is legally permissible” to “this is therefore fine,” without pausing at the gap between those two claims. Law sets a floor; clearing it does not mean the conduct is right. That gap is where this essay begins.
## The analogy points the wrong way
Antirez builds his case on history. When the GNU project reimplemented the UNIX userspace, it was lawful. So was Linux. Copyright law prohibits copying “protected expressions”—the actual code, its structure, its specific mechanisms—but it does not protect ideas or behavior. AI-assisted reimplementation occupies the same legal ground. Therefore, it is lawful.
The legal analysis is largely correct, and I am not disputing it. The problem lies in what antirez does next: he presents the legal conclusion as if it were also a social one, and uses a historical analogy that, examined more carefully, argues against his own position.
When GNU reimplemented the UNIX userspace, the vector ran from proprietary to free. Stallman was using the limits of copyright law to turn proprietary software into free software. The ethical force of that project did not come from its legal permissibility—it came from the direction it was moving, from the fact that it was expanding the commons. That is why people cheered.
The vector in the chardet case runs the other way. Software protected by a copyleft license—one that guarantees users the right to study, modify, and redistribute derivative works under the same terms—has been reimplemented under a permissive license that carries no such guarantee. This is not a reimplementation that expands the commons. It is one that removes the fencing that protected the commons. Derivative works built on chardet 7.0 are under no obligation to share their source code. That obligation, which applied to a library downloaded 130 million times a month, is now gone.
Antirez does not address this directional difference. He invokes the GNU precedent, but that precedent is a counterexample to his conclusion, not a supporting one.
## Does the GPL work against sharing?
Ronacher's argument is different. He discloses upfront that he has a stake in the outcome: “I personally have a horse in the race here because I too wanted chardet to be under a non-GPL license for many years. So consider me a very biased person in that regard.” He goes on to write that he considers “the GPL to run against that spirit by restricting what can be done with it”—the spirit being that society is better off when we share.
This claim rests on a fundamental misreading of what the GPL does.
Start with what the GPL actually prohibits. It does not prohibit keeping source code private. It imposes no constraint on privately modifying GPL software and using it yourself. The GPL's conditions are triggered only by distribution. If you distribute modified code, or offer it as a networked service, you must make the source available under the same terms. This is not a restriction on sharing. It is a condition placed on sharing: if you share, you must share in kind.
The requirement that improvements be returned to the commons is not a mechanism that suppresses sharing. It is a mechanism that makes sharing recursive and self-reinforcing. The claim that imposing contribution obligations on users of a commons undermines sharing culture does not hold together logically.
The contrast with the MIT license clarifies the point. Under MIT, anyone may take code, improve it, and close it off into a proprietary product. You can receive from the commons without giving back. If Ronacher calls this structure “more share-friendly,” he is using a concept of sharing with a specific directionality built in: sharing flows toward whoever has more capital and more engineers to take advantage of it.
The historical record bears this out. In the 1990s, companies routinely absorbed GPL code into proprietary products—not because they had chosen permissive licenses, but because copyleft enforcement was slack. The strengthening of copyleft mechanisms closed that gap. For individual developers and small projects without the resources to compete on anything but reciprocity, copyleft was what made the exchange approximately fair.
The creator of Flask knows this distinction. If he elides it anyway, the argument is not naïve—it is convenient.
## A self-refuting example
The most interesting moment in Ronacher's piece is not the argument but a detail he mentions in passing: Vercel reimplemented GNU Bash using AI and published it, then got visibly upset when Cloudflare reimplemented Next.js the same way.
Ronacher notes this as an irony and moves on. But the irony cuts deeper than he lets on. Next.js is MIT licensed. Cloudflare's vinext did not violate any license—it did exactly what Ronacher calls a contribution to the culture of openness, applied to a permissively licensed codebase. Vercel's reaction had nothing to do with license infringement; it was purely competitive and territorial. The implicit position is: reimplementing GPL software as MIT is a victory for sharing, but having our own MIT software reimplemented by a competitor is cause for outrage. This is what the claim that permissive licensing is “more share-friendly” than copyleft looks like in practice. The spirit of sharing, it turns out, runs in one direction only: outward from oneself.
Ronacher registers the contradiction and does not stop. “This development plays into my worldview,” he writes. When you present evidence that cuts against your own position, acknowledge it, and then proceed to your original conclusion unchanged, that is a signal that the conclusion preceded the argument.
## Legality and social legitimacy are different registers
Back to the question posed at the start. Is legal the same as legitimate?
Antirez closes his careful legal analysis as though it settles the matter. Ronacher acknowledges that “there is an obvious moral question here, but that isn't necessarily what I'm interested in.” Both pieces treat legal permissibility as a proxy for social legitimacy. But law only says what conduct it will not prevent—it does not certify that conduct as right. Aggressive tax minimization that never crosses into illegality may still be widely regarded as antisocial. A pharmaceutical company that legally acquires a patent on a long-generic drug and raises the price a hundredfold has done something legal, but that does not make it fine. Legality is a necessary condition; it is not a sufficient one.
In the chardet case, the distinction is sharper still. What the LGPL protected was not Blanchard's labor alone. It was a social compact agreed to by everyone who contributed to the library over twelve years. The terms of that compact were: if you take this and build on it, you share back under the same terms. This compact operated as a legal instrument, yes, but it was also the foundation of trust that made contribution rational. The fact that a reimplementation may qualify legally as a new work, and the fact that it breaks faith with the original contributors, are separate questions. If a court eventually rules in Blanchard's favor, that ruling will tell us what the law permits. It will not tell us that the act was right.
Zoë Kooyman, executive director of the FSF, put it plainly: “Refusing to grant others the rights you yourself received as a user is highly antisocial, no matter what method you use.”
## Whose perspective is the default?
Reading this debate, I keep returning to a question about position. From where are these two writers looking at the situation?
Antirez created Redis. Ronacher created Flask. Both are figures at the center of the open source ecosystem, with large audiences and well-established reputations. For them, falling costs of AI reimplementation means something specific: it is easier to reimplement things they want in a different form. Ronacher says explicitly that he had begun reimplementing GNU Readline precisely because of its copyleft terms.
For the people who have spent years contributing to a library like chardet, the same shift in costs means something else entirely: the copyleft protection around their contributions can be removed. The two writers are speaking from the former position to people in the latter, telling them that this was always lawful, that historical precedent supports it, and that the appropriate response is adaptation.
When positional asymmetry of this kind is ignored, and the argument is presented as universal analysis, what you get is not analysis but rationalization. Both writers arrive at conclusions that align precisely with their own interests. Readers should hold that fact in mind.
## What this fight points toward
Bruce Perens, who wrote the original Open Source Definition, told The Register: “The entire economics of software development are dead, gone, over, kaput!” He meant it as an alarm. Antirez, from a similar assessment of the situation, draws the conclusion: adapt. Ronacher says he finds the direction exciting.
None of the three responses addresses the central question. When copyleft becomes technically easier to circumvent, does that make it less necessary, or more?
I think more. What the GPL protected was not the scarcity of code but the freedom of users. The fact that producing code has become cheaper does not make it acceptable to use that code as a vehicle for eroding freedom. If anything, as the friction of reimplementation disappears, so does the friction of stripping copyleft from anything left exposed. The erosion of enforcement capacity is a legal problem. It does not touch the underlying normative judgment.
That judgment is this: those who take from the commons owe something back to the commons. The principle does not change depending on whether a reimplementation takes five years or five days. No court ruling on AI-generated code will alter its social weight.
This is where law and community norms diverge. Law is made slowly, after the fact, reflecting existing power arrangements. The norms that open source communities built over decades did not wait for court approval. People chose the GPL when the law offered them no guarantee of its enforcement, because it expressed the values of the communities they wanted to belong to. Those values do not expire when the law changes.
In previous writing, I argued for a training copyleft (TGPL) as the next step in this line of development. The chardet situation suggests the argument has to go further: to a specification copyleft covering the layer below source code. If source code can now be generated from a specification, the specification is where the essential intellectual content of a GPL project resides. Blanchard's own claim—that he worked only from the test suite and API without reading the source—is, paradoxically, an argument for protecting that test suite and API specification under copyleft terms.
The history of the GPL is the history of licensing tools evolving in response to new forms of exploitation: GPLv2 to GPLv3, then AGPL. What drove each evolution was not a court ruling but a community reaching a value judgment first and then seeking legal instruments to express it. The same sequence is available now. Whatever courts eventually decide about AI reimplementation, the question we need to answer first is not a legal one. It is a social one. Do those who take from the commons owe something back? I think they do. That judgment does not require a verdict.
What makes the pieces by antirez and Ronacher worth reading is not that they are right. It is that they make visible, with unusual clarity, what they are choosing not to see. When legality is used as a substitute for a value judgment, the question that actually matters gets buried in the footnotes of a law it has already outgrown.

View File

@ -0,0 +1,106 @@
# Claude Code Python Porting Workspace
> The primary `src/` tree in this repository is now dedicated to **Python porting work**. The March 31, 2026 Claude Code source exposure is part of the project's background, but the tracked repository is now centered on Python source rather than the exposed TypeScript snapshot.
---
## Porting Status
The main source tree is now Python-first.
- `src/` contains the active Python porting workspace
- `tests/` verifies the current Python workspace
- the exposed snapshot is no longer part of the tracked repository state
The current Python workspace is not yet a complete one-to-one replacement for the original system, but the primary implementation surface is now Python.
## Why this rewrite exists
I originally studied the exposed codebase to understand its harness, tool wiring, and agent workflow. After spending more time with the legal and ethical questions—and after reading the essay linked below—I did not want the exposed snapshot itself to remain the main tracked source tree.
This repository now focuses on Python porting work instead.
## Repository Layout
```text
.
├── src/ # Python porting workspace
│ ├── __init__.py
│ ├── commands.py
│ ├── main.py
│ ├── models.py
│ ├── port_manifest.py
│ ├── query_engine.py
│ ├── task.py
│ └── tools.py
├── tests/ # Python verification
├── assets/omx/ # OmX workflow screenshots
├── 2026-03-09-is-legal-the-same-as-legitimate-ai-reimplementation-and-the-erosion-of-copyleft.md
└── README.md
```
## Python Workspace Overview
The new Python `src/` tree currently provides:
- **`port_manifest.py`** — summarizes the current Python workspace structure
- **`models.py`** — dataclasses for subsystems, modules, and backlog state
- **`commands.py`** — Python-side command port metadata
- **`tools.py`** — Python-side tool port metadata
- **`query_engine.py`** — renders a Python porting summary from the active workspace
- **`main.py`** — a CLI entrypoint for manifest and summary output
## Quickstart
Render the Python porting summary:
```bash
python3 -m src.main summary
```
Print the current Python workspace manifest:
```bash
python3 -m src.main manifest
```
List the current Python modules:
```bash
python3 -m src.main subsystems --limit 16
```
Run verification:
```bash
python3 -m unittest discover -s tests -v
```
## Related Essay
- [*Is legal the same as legitimate: AI reimplementation and the erosion of copyleft*](https://writings.hongminhee.org/2026/03/legal-vs-legitimate/)
The essay is dated **March 9, 2026**, so it should be read as companion analysis that predates the **March 31, 2026** source exposure that motivated this rewrite direction.
## Built with `oh-my-codex`
The restructuring and documentation work on this repository was AI-assisted and orchestrated with Yeachan Heo's [oh-my-codex (OmX)](https://github.com/Yeachan-Heo/oh-my-codex), layered on top of Codex.
- **`$team` mode:** used for coordinated parallel review and architectural feedback
- **`$ralph` mode:** used for persistent execution, verification, and completion discipline
- **Codex-driven workflow:** used to turn the main `src/` tree into a Python-first porting workspace
### OmX workflow screenshots
![OmX workflow screenshot 1](assets/omx/omx-readme-review-1.png)
*Ralph/team orchestration view while the README and essay context were being reviewed in terminal panes.*
![OmX workflow screenshot 2](assets/omx/omx-readme-review-2.png)
*Split-pane review and verification flow during the final README wording pass.*
## Ownership / Affiliation Disclaimer
- This repository does **not** claim ownership of the original Claude Code source material.
- This repository is **not affiliated with, endorsed by, or maintained by Anthropic**.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -0,0 +1,16 @@
"""Python porting workspace for the Claude Code rewrite effort."""
from .commands import PORTED_COMMANDS, build_command_backlog
from .port_manifest import PortManifest, build_port_manifest
from .query_engine import QueryEnginePort
from .tools import PORTED_TOOLS, build_tool_backlog
__all__ = [
'PortManifest',
'QueryEnginePort',
'PORTED_COMMANDS',
'PORTED_TOOLS',
'build_command_backlog',
'build_port_manifest',
'build_tool_backlog',
]

View File

@ -0,0 +1,13 @@
from __future__ import annotations
from .models import PortingBacklog, PortingModule
PORTED_COMMANDS = (
PortingModule('main', 'Expose a Python CLI for manifest and backlog reporting', 'src/main.py', 'implemented'),
PortingModule('summary', 'Render a Markdown overview of the current porting workspace', 'src/query_engine.py', 'implemented'),
PortingModule('subsystems', 'List the current Python modules participating in the rewrite', 'src/port_manifest.py', 'implemented'),
)
def build_command_backlog() -> PortingBacklog:
return PortingBacklog(title='Command surface', modules=list(PORTED_COMMANDS))

View File

@ -0,0 +1,38 @@
from __future__ import annotations
import argparse
from .port_manifest import build_port_manifest
from .query_engine import QueryEnginePort
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description='Python porting workspace for the Claude Code rewrite effort')
subparsers = parser.add_subparsers(dest='command', required=True)
subparsers.add_parser('summary', help='render a Markdown summary of the Python porting workspace')
subparsers.add_parser('manifest', help='print the current Python workspace manifest')
list_parser = subparsers.add_parser('subsystems', help='list the current Python modules in the workspace')
list_parser.add_argument('--limit', type=int, default=16)
return parser
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
manifest = build_port_manifest()
if args.command == 'summary':
print(QueryEnginePort(manifest).render_summary())
return 0
if args.command == 'manifest':
print(manifest.to_markdown())
return 0
if args.command == 'subsystems':
for subsystem in manifest.top_level_modules[: args.limit]:
print(f'{subsystem.name} {subsystem.file_count} {subsystem.notes}')
return 0
parser.error(f'unknown command: {args.command}')
return 2
if __name__ == '__main__':
raise SystemExit(main())

View File

@ -0,0 +1,31 @@
from __future__ import annotations
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Subsystem:
name: str
path: str
file_count: int
notes: str
@dataclass(frozen=True)
class PortingModule:
name: str
responsibility: str
source_hint: str
status: str = 'planned'
@dataclass
class PortingBacklog:
title: str
modules: list[PortingModule] = field(default_factory=list)
def summary_lines(self) -> list[str]:
return [
f'- {module.name} [{module.status}] — {module.responsibility} (from {module.source_hint})'
for module in self.modules
]

View File

@ -0,0 +1,52 @@
from __future__ import annotations
from collections import Counter
from dataclasses import dataclass
from pathlib import Path
from .models import Subsystem
DEFAULT_SRC_ROOT = Path(__file__).resolve().parent
@dataclass(frozen=True)
class PortManifest:
src_root: Path
total_python_files: int
top_level_modules: tuple[Subsystem, ...]
def to_markdown(self) -> str:
lines = [
f'Port root: `{self.src_root}`',
f'Total Python files: **{self.total_python_files}**',
'',
'Top-level Python modules:',
]
for module in self.top_level_modules:
lines.append(f'- `{module.name}` ({module.file_count} files) — {module.notes}')
return '\n'.join(lines)
def build_port_manifest(src_root: Path | None = None) -> PortManifest:
root = src_root or DEFAULT_SRC_ROOT
files = [path for path in root.rglob('*.py') if path.is_file()]
counter = Counter(
path.relative_to(root).parts[0] if len(path.relative_to(root).parts) > 1 else path.name
for path in files
if path.name != '__pycache__'
)
notes = {
'__init__.py': 'package export surface',
'main.py': 'CLI entrypoint',
'port_manifest.py': 'workspace manifest generation',
'query_engine.py': 'port orchestration summary layer',
'commands.py': 'command backlog metadata',
'tools.py': 'tool backlog metadata',
'models.py': 'shared dataclasses',
'task.py': 'task-level planning structures',
}
modules = tuple(
Subsystem(name=name, path=f'src/{name}', file_count=count, notes=notes.get(name, 'Python port support module'))
for name, count in counter.most_common()
)
return PortManifest(src_root=root, total_python_files=len(files), top_level_modules=modules)

View File

@ -0,0 +1,32 @@
from __future__ import annotations
from dataclasses import dataclass
from .commands import build_command_backlog
from .port_manifest import PortManifest, build_port_manifest
from .tools import build_tool_backlog
@dataclass
class QueryEnginePort:
manifest: PortManifest
@classmethod
def from_workspace(cls) -> 'QueryEnginePort':
return cls(manifest=build_port_manifest())
def render_summary(self) -> str:
command_backlog = build_command_backlog()
tool_backlog = build_tool_backlog()
sections = [
'# Python Porting Workspace Summary',
'',
self.manifest.to_markdown(),
'',
f'{command_backlog.title}:',
*command_backlog.summary_lines(),
'',
f'{tool_backlog.title}:',
*tool_backlog.summary_lines(),
]
return '\n'.join(sections)

View File

@ -0,0 +1,10 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class PortingTask:
title: str
detail: str
completed: bool = False

View File

@ -0,0 +1,13 @@
from __future__ import annotations
from .models import PortingBacklog, PortingModule
PORTED_TOOLS = (
PortingModule('port_manifest', 'Inspect the active Python source tree and summarize the current rewrite surface', 'src/port_manifest.py', 'implemented'),
PortingModule('backlog_models', 'Represent subsystem and backlog metadata as Python dataclasses', 'src/models.py', 'implemented'),
PortingModule('query_engine', 'Coordinate Python-facing rewrite summaries and reporting', 'src/query_engine.py', 'implemented'),
)
def build_tool_backlog() -> PortingBacklog:
return PortingBacklog(title='Tool surface', modules=list(PORTED_TOOLS))

View File

@ -0,0 +1,34 @@
from __future__ import annotations
import subprocess
import sys
import unittest
from src.port_manifest import build_port_manifest
from src.query_engine import QueryEnginePort
class PortingWorkspaceTests(unittest.TestCase):
def test_manifest_counts_python_files(self) -> None:
manifest = build_port_manifest()
self.assertGreaterEqual(manifest.total_python_files, 7)
self.assertTrue(manifest.top_level_modules)
def test_query_engine_summary_mentions_workspace(self) -> None:
summary = QueryEnginePort.from_workspace().render_summary()
self.assertIn('Python Porting Workspace Summary', summary)
self.assertIn('Command surface', summary)
self.assertIn('Tool surface', summary)
def test_cli_summary_runs(self) -> None:
result = subprocess.run(
[sys.executable, '-m', 'src.main', 'summary'],
check=True,
capture_output=True,
text=True,
)
self.assertIn('Python Porting Workspace Summary', result.stdout)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,30 @@
"use client";
import type { ComponentProps, ReactNode } from "react";
import { usePathname } from "next/navigation";
import { TopNavbar } from "@/components/top-navbar";
type AdminLayoutShellProps = ComponentProps<typeof TopNavbar> & {
children: ReactNode;
};
function shouldHideSidebarChrome(pathname: string) {
return pathname === "/settings" || pathname.startsWith("/settings/");
}
export function AdminLayoutShell({
children,
...topNavbarProps
}: AdminLayoutShellProps) {
const pathname = usePathname();
const hideSidebarChrome = shouldHideSidebarChrome(pathname);
return (
<div className="flex min-h-screen flex-col bg-background">
{hideSidebarChrome ? null : <TopNavbar {...topNavbarProps} />}
<div className={hideSidebarChrome ? "flex-1" : "flex-1 pl-16"}>
{children}
</div>
</div>
);
}

View File

@ -1,6 +1,6 @@
import { auth } from "@/app/(auth)/auth";
import { TopNavbar } from "@/components/top-navbar";
import { getSiteSettings, getNavLinks, getTutorialVideos } from "@/lib/db/queries";
import { AdminLayoutShell } from "./_components/admin-layout-shell";
export default async function AdminLayout({
children,
@ -13,38 +13,45 @@ export default async function AdminLayout({
getNavLinks(),
getTutorialVideos(),
]);
const settingsHref =
session?.user?.type === "admin"
? "/settings"
: session?.user?.type === "agent_admin"
? "/settings/agent-workflow"
: null;
return (
<div className="flex min-h-screen flex-col bg-background">
<TopNavbar
siteName={siteSettings.siteName}
siteLogo={siteSettings.siteLogo || undefined}
siteLogoDark={siteSettings.siteLogoDark || undefined}
isAdmin={session?.user?.type === "admin"}
customLinks={navLinks.map((link) => ({
id: link.id,
name: link.name,
url: link.url,
order: link.order,
toolboxGroup: link.toolboxGroup || 1,
}))}
toolbox1Name={siteSettings.toolbox1Name || "工具箱"}
toolbox1Enabled={siteSettings.toolbox1Enabled ?? true}
toolbox2Name={siteSettings.toolbox2Name || "工具箱2"}
toolbox2Enabled={siteSettings.toolbox2Enabled ?? false}
tutorialVideos={tutorialVideos.map(v => ({
id: v.id,
title: v.title,
videoUrl: v.videoUrl,
coverUrl: v.coverUrl,
description: v.description,
order: v.order,
}))}
tutorialEnabled={siteSettings.tutorialEnabled ?? false}
rankingEnabled={siteSettings.rankingEnabled ?? true}
/>
<div className="flex-1 pl-16">{children}</div>
</div>
<AdminLayoutShell
siteName={siteSettings.siteName}
siteLogo={siteSettings.siteLogo || undefined}
siteLogoDark={siteSettings.siteLogoDark || undefined}
isAdmin={session?.user?.type === "admin" || session?.user?.type === "agent_admin"}
settingsHref={settingsHref}
customLinks={navLinks.map((link) => ({
id: link.id,
name: link.name,
url: link.url,
order: link.order,
toolboxGroup: link.toolboxGroup || 1,
}))}
toolbox1Name={siteSettings.toolbox1Name || "工具箱"}
toolbox1Enabled={siteSettings.toolbox1Enabled ?? true}
toolbox2Name={siteSettings.toolbox2Name || "工具箱2"}
toolbox2Enabled={siteSettings.toolbox2Enabled ?? false}
tutorialVideos={tutorialVideos.map((v) => ({
id: v.id,
title: v.title,
videoUrl: v.videoUrl,
coverUrl: v.coverUrl,
description: v.description,
order: v.order,
}))}
tutorialEnabled={siteSettings.tutorialEnabled ?? false}
rankingEnabled={siteSettings.rankingEnabled ?? true}
productPlanningEnabled={(siteSettings as any).productPlanningEnabled ?? true}
videoCanvasEnabled={(siteSettings as any).videoCanvasEnabled ?? true}
>
{children}
</AdminLayoutShell>
);
}

View File

@ -0,0 +1,706 @@
"use client";
import {
Activity,
AlertTriangle,
Ban,
Clock3,
Globe2,
LogIn,
Plus,
RefreshCw,
Search,
ShieldAlert,
Unlock,
UserCheck,
} from "lucide-react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import useSWR from "swr";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn, fetcher } from "@/lib/utils";
type Severity = "info" | "watch" | "high" | "critical";
type AccessLogEvent = {
id: string;
eventType: string;
ip: string;
userEmail: string | null;
userRole: string | null;
method: string | null;
path: string | null;
routeGroup: string | null;
statusCode: number | null;
outcome: string;
severity: Severity;
reasons: string[] | null;
userAgent: string | null;
createdAt: string;
};
type AccessLogTopIp = {
ip: string;
total: number;
uniqueUsers: number;
failureCount: number;
rateLimitedCount: number;
authFailureCount: number;
loginSuccessCount: number;
suspiciousCount: number;
firstSeen: string;
lastSeen: string;
severity: Severity;
failureRate: number;
reasons: string[];
};
type AccessLogResponse = {
overview: {
totalEvents: number;
uniqueIps: number;
abnormalEvents: number;
abnormalIps: number;
rateLimitedEvents: number;
manualBlockedEvents: number;
loginSuccessEvents: number;
unauthorizedEvents: number;
};
topIps: AccessLogTopIp[];
hourlyStats: Array<{
hour: string;
count: number;
abnormalCount: number;
}>;
pathStats: Array<{
path: string | null;
routeGroup: string | null;
count: number;
abnormalCount: number;
}>;
recentEvents: AccessLogEvent[];
rules: string[];
};
type AccessBlocklistEntry = {
id: string;
ip: string;
reason: string | null;
isActive: boolean;
expiresAt: string | null;
createdAt: string;
updatedAt: string;
createdByEmail: string | null;
};
const severityLabels: Record<Severity, string> = {
info: "正常",
watch: "关注",
high: "异常",
critical: "高危",
};
const severityClassNames: Record<Severity, string> = {
info:
"border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-800/70 dark:bg-emerald-950/45 dark:text-emerald-300",
watch:
"border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-800/70 dark:bg-amber-950/45 dark:text-amber-300",
high:
"border-orange-200 bg-orange-50 text-orange-700 dark:border-orange-800/70 dark:bg-orange-950/45 dark:text-orange-300",
critical:
"border-red-200 bg-red-50 text-red-700 dark:border-red-800/70 dark:bg-red-950/45 dark:text-red-300",
};
function formatNumber(value: number | null | undefined) {
return new Intl.NumberFormat("zh-CN").format(Number(value || 0));
}
function formatPercent(value: number | null | undefined) {
return `${Math.round(Number(value || 0) * 100)}%`;
}
function formatTime(value: string | null | undefined) {
if (!value) return "--";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "--";
return new Intl.DateTimeFormat("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
}).format(date);
}
function getStatusClassName(statusCode: number | null) {
if (!statusCode) return "text-muted-foreground";
if (statusCode >= 500) return "text-red-700 dark:text-red-300";
if (statusCode >= 400) return "text-orange-700 dark:text-orange-300";
if (statusCode >= 300) return "text-sky-700 dark:text-sky-300";
return "text-emerald-700 dark:text-emerald-300";
}
function AccessSeverityBadge({ severity }: { severity: Severity }) {
return (
<Badge
className={cn("shrink-0 border px-2 py-0.5", severityClassNames[severity])}
variant="outline"
>
{severityLabels[severity]}
</Badge>
);
}
export function AccessSecurityDashboard() {
const [hours, setHours] = useState("24");
const [severity, setSeverity] = useState("all");
const [query, setQuery] = useState("");
const [blockIp, setBlockIp] = useState("");
const [blockReason, setBlockReason] = useState("");
const [isSavingBlock, setIsSavingBlock] = useState(false);
const accessLogUrl = useMemo(() => {
const params = new URLSearchParams({
hours,
severity,
limit: "160",
});
if (query.trim()) params.set("q", query.trim());
return `/api/admin/access-logs?${params.toString()}`;
}, [hours, query, severity]);
const { data, isLoading, mutate } = useSWR<AccessLogResponse>(
accessLogUrl,
fetcher,
{ refreshInterval: 30_000 }
);
const { data: blocklistData, mutate: mutateBlocklist } = useSWR<{
entries: AccessBlocklistEntry[];
}>("/api/admin/access-blocklist", fetcher);
const activeBlocklistEntries = useMemo(
() => (blocklistData?.entries || []).filter((entry) => entry.isActive),
[blocklistData?.entries]
);
const blockedRules = useMemo(
() => new Set(activeBlocklistEntries.map((entry) => entry.ip.toLowerCase())),
[activeBlocklistEntries]
);
const saveBlockEntry = async (targetIp?: string, targetReason?: string) => {
const ip = String(targetIp || blockIp).trim();
const reason = String(targetReason ?? blockReason).trim();
if (!ip) {
toast.error("请输入要拉黑的 IP");
return;
}
setIsSavingBlock(true);
try {
const response = await fetch("/api/admin/access-blocklist", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ ip, reason }),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(payload?.message || payload?.error || "拉黑失败");
}
toast.success(`${ip} 已加入手动黑名单`);
setBlockIp("");
setBlockReason("");
await Promise.all([mutateBlocklist(), mutate()]);
} catch (error) {
toast.error(error instanceof Error ? error.message : "拉黑失败");
} finally {
setIsSavingBlock(false);
}
};
const updateBlockEntry = async (entry: AccessBlocklistEntry, isActive: boolean) => {
try {
const response = await fetch("/api/admin/access-blocklist", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify({
id: entry.id,
isActive,
reason: entry.reason || "",
expiresAt: entry.expiresAt || null,
}),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(payload?.message || payload?.error || "更新失败");
}
toast.success(isActive ? `${entry.ip} 已重新拉黑` : `${entry.ip} 已解除拉黑`);
await Promise.all([mutateBlocklist(), mutate()]);
} catch (error) {
toast.error(error instanceof Error ? error.message : "更新失败");
}
};
const overviewCards = [
{
key: "events",
label: "访问记录",
value: formatNumber(data?.overview.totalEvents),
note: `${hours} 小时窗口`,
icon: Activity,
className:
"border-slate-200 bg-slate-50 text-slate-900 dark:border-slate-700/70 dark:bg-slate-900/70 dark:text-slate-100",
},
{
key: "ips",
label: "独立 IP",
value: formatNumber(data?.overview.uniqueIps),
note: `${formatNumber(data?.overview.abnormalIps)} 个异常标注`,
icon: Globe2,
className:
"border-sky-200 bg-sky-50 text-sky-900 dark:border-sky-800/70 dark:bg-sky-950/45 dark:text-sky-100",
},
{
key: "abnormal",
label: "异常事件",
value: formatNumber(data?.overview.abnormalEvents),
note: `${formatNumber(data?.overview.manualBlockedEvents)} 次手动阻断`,
icon: ShieldAlert,
className:
"border-orange-200 bg-orange-50 text-orange-900 dark:border-orange-800/70 dark:bg-orange-950/45 dark:text-orange-100",
},
{
key: "login",
label: "成功登录",
value: formatNumber(data?.overview.loginSuccessEvents),
note: `${formatNumber(data?.overview.unauthorizedEvents)} 次未授权`,
icon: LogIn,
className:
"border-emerald-200 bg-emerald-50 text-emerald-900 dark:border-emerald-800/70 dark:bg-emerald-950/45 dark:text-emerald-100",
},
];
const maxHourlyCount = Math.max(
1,
...(data?.hourlyStats || []).map((item) => item.count)
);
const maxPathCount = Math.max(
1,
...(data?.pathStats || []).map((item) => item.count)
);
return (
<div className="space-y-4">
<Card>
<CardHeader className="pb-4">
<div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">
<div>
<CardTitle className="text-xl">访问安全分析</CardTitle>
<CardDescription>
IP 访问记录、登录来源、异常标注与请求趋势
</CardDescription>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<div className="relative sm:w-[260px]">
<Search className="-translate-y-1/2 absolute left-3 top-1/2 h-4 w-4 text-muted-foreground" />
<Input
className="h-9 pl-9"
onChange={(event) => setQuery(event.target.value)}
placeholder="搜索 IP、路径、用户"
value={query}
/>
</div>
<Select onValueChange={setHours} value={hours}>
<SelectTrigger className="h-9 sm:w-[132px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">近 1 小时</SelectItem>
<SelectItem value="6">近 6 小时</SelectItem>
<SelectItem value="24">近 24 小时</SelectItem>
<SelectItem value="72">近 3 天</SelectItem>
<SelectItem value="168">近 7 天</SelectItem>
</SelectContent>
</Select>
<Select onValueChange={setSeverity} value={severity}>
<SelectTrigger className="h-9 sm:w-[132px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">全部级别</SelectItem>
<SelectItem value="abnormal">仅异常</SelectItem>
<SelectItem value="watch">关注</SelectItem>
<SelectItem value="high">异常</SelectItem>
<SelectItem value="critical">高危</SelectItem>
</SelectContent>
</Select>
<Button
className="h-9 gap-2"
onClick={() => mutate()}
size="sm"
variant="outline"
>
<RefreshCw className={cn("h-4 w-4", isLoading && "animate-spin")} />
刷新
</Button>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
{overviewCards.map((item) => {
const Icon = item.icon;
return (
<div
className={cn("rounded-lg border p-4", item.className)}
key={item.key}
>
<div className="flex items-center justify-between gap-3">
<div className="text-sm text-current/70">{item.label}</div>
<Icon className="h-4 w-4" />
</div>
<div className="mt-3 font-semibold text-3xl tracking-tight">
{item.value}
</div>
<div className="mt-1 text-current/65 text-xs">{item.note}</div>
</div>
);
})}
</div>
<div className="rounded-lg border bg-background p-4">
<div className="mb-3 flex items-center justify-between gap-3">
<div className="flex items-center gap-2 font-medium text-sm">
<Clock3 className="h-4 w-4" />
小时趋势
</div>
<div className="text-muted-foreground text-xs">
橙色表示异常事件占比
</div>
</div>
<div className="flex h-[180px] items-end gap-1">
{(data?.hourlyStats || []).map((item) => {
const height = Math.max(4, (item.count / maxHourlyCount) * 100);
const abnormalHeight =
item.count > 0
? Math.max(0, (item.abnormalCount / item.count) * height)
: 0;
return (
<div
className="group relative flex min-w-[10px] flex-1 items-end overflow-hidden rounded bg-slate-200 dark:bg-slate-800"
key={item.hour}
style={{ height: `${height}%` }}
>
<div
className="w-full bg-orange-400"
style={{ height: `${abnormalHeight}%` }}
/>
<div className="-translate-x-1/2 pointer-events-none absolute bottom-full left-1/2 z-10 mb-2 hidden whitespace-nowrap rounded-md bg-zinc-950 px-2 py-1 text-white text-xs shadow-lg group-hover:block">
{item.hour} · {formatNumber(item.count)} 次 · 异常{" "}
{formatNumber(item.abnormalCount)}
</div>
</div>
);
})}
{(!data?.hourlyStats || data.hourlyStats.length === 0) && (
<div className="flex h-full w-full items-center justify-center rounded-lg border border-dashed text-muted-foreground text-sm">
暂无访问记录
</div>
)}
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-4">
<CardTitle className="text-lg">手动 IP 黑名单</CardTitle>
<CardDescription>
管理员手动加入后,命中 IP 会被阻断并写入访问日志
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-3 lg:grid-cols-[220px_minmax(240px,1fr)_auto]">
<Input
className="h-9"
onChange={(event) => setBlockIp(event.target.value)}
placeholder="IP 或 IPv4 CIDR"
value={blockIp}
/>
<Input
className="h-9"
onChange={(event) => setBlockReason(event.target.value)}
placeholder="拉黑原因"
value={blockReason}
/>
<Button
className="h-9 gap-2"
disabled={isSavingBlock}
onClick={() => saveBlockEntry()}
size="sm"
>
<Plus className="h-4 w-4" />
加入黑名单
</Button>
</div>
<div className="grid gap-2 md:grid-cols-2 xl:grid-cols-3">
{activeBlocklistEntries.map((entry) => (
<div
className="rounded-lg border border-red-100 bg-red-50/60 px-3 py-3 dark:border-red-900/60 dark:bg-red-950/30"
key={entry.id}
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex items-center gap-2">
<Ban className="h-4 w-4 text-red-600 dark:text-red-400" />
<span className="truncate font-mono font-semibold text-sm text-red-900 dark:text-red-200">
{entry.ip}
</span>
</div>
<div className="mt-1 line-clamp-2 text-red-800/75 text-xs dark:text-red-300/75">
{entry.reason || "未填写原因"}
</div>
<div className="mt-2 text-red-700/70 text-xs dark:text-red-400/70">
{formatTime(entry.updatedAt)}
</div>
</div>
<Button
className="h-8 shrink-0 gap-1 border-red-200 bg-white text-red-700 hover:bg-red-50 dark:border-red-800 dark:bg-red-950/30 dark:text-red-300 dark:hover:bg-red-950/60"
onClick={() => updateBlockEntry(entry, false)}
size="sm"
variant="outline"
>
<Unlock className="h-3.5 w-3.5" />
解除
</Button>
</div>
</div>
))}
{activeBlocklistEntries.length === 0 ? (
<div className="rounded-lg border border-dashed py-8 text-center text-muted-foreground text-sm md:col-span-2 xl:col-span-3">
暂无手动拉黑 IP
</div>
) : null}
</div>
</CardContent>
</Card>
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.3fr)_minmax(320px,0.7fr)]">
<Card>
<CardHeader className="pb-4">
<CardTitle className="text-lg">异常 IP 标注</CardTitle>
<CardDescription>
按限流、失败率、认证异常、敏感路径和请求量综合排序
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
{(data?.topIps || []).map((item) => (
<div
className="rounded-lg border bg-background px-4 py-3"
key={item.ip}
>
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="font-mono font-semibold text-sm">
{item.ip}
</span>
<AccessSeverityBadge severity={item.severity} />
</div>
<div className="mt-2 flex flex-wrap gap-2 text-muted-foreground text-xs">
<span>{formatNumber(item.total)} 次访问</span>
<span>{formatNumber(item.uniqueUsers)} 个用户</span>
<span>失败率 {formatPercent(item.failureRate)}</span>
<span>最近 {formatTime(item.lastSeen)}</span>
</div>
</div>
<div className="flex flex-wrap items-center gap-1 md:justify-end">
{(item.reasons.length > 0 ? item.reasons : ["正常访问"]).map(
(reason) => (
<Badge
className="border-zinc-200 bg-zinc-50 text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300"
key={reason}
variant="outline"
>
{reason}
</Badge>
)
)}
<Button
className="h-7 gap-1 px-2 text-xs"
disabled={blockedRules.has(item.ip.toLowerCase()) || isSavingBlock}
onClick={() =>
saveBlockEntry(
item.ip,
item.reasons.join("、") || "管理员从异常 IP 排行手动拉黑"
)
}
size="sm"
variant="outline"
>
<Ban className="h-3.5 w-3.5" />
{blockedRules.has(item.ip.toLowerCase()) ? "已拉黑" : "拉黑"}
</Button>
</div>
</div>
</div>
))}
{(!data?.topIps || data.topIps.length === 0) && (
<div className="rounded-lg border border-dashed py-10 text-center text-muted-foreground text-sm">
暂无 IP 聚合数据
</div>
)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-4">
<CardTitle className="text-lg">访问路径</CardTitle>
<CardDescription>按请求数量排序</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{(data?.pathStats || []).map((item) => {
const abnormalRate =
item.count > 0 ? item.abnormalCount / item.count : 0;
return (
<div key={`${item.routeGroup || "page"}-${item.path || "/"}`}>
<div className="flex items-center justify-between gap-3 text-sm">
<span className="min-w-0 truncate font-medium">
{item.path || "/"}
</span>
<span className="shrink-0 text-muted-foreground text-xs">
{formatNumber(item.count)}
</span>
</div>
<div className="mt-2 h-2 overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800">
<div
className="h-full rounded-full bg-zinc-900 dark:bg-zinc-200"
style={{
width: `${Math.max(4, (item.count / maxPathCount) * 100)}%`,
}}
/>
<div
className="-mt-2 h-full rounded-full bg-orange-400"
style={{
width: `${Math.round(abnormalRate * 100)}%`,
}}
/>
</div>
</div>
);
})}
{(!data?.pathStats || data.pathStats.length === 0) && (
<div className="rounded-lg border border-dashed py-10 text-center text-muted-foreground text-sm">
暂无路径统计
</div>
)}
</div>
</CardContent>
</Card>
</div>
<Card>
<CardHeader className="pb-4">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div>
<CardTitle className="text-lg">访问日志列表</CardTitle>
<CardDescription>最近 {formatNumber(data?.recentEvents.length)} 条</CardDescription>
</div>
<div className="flex flex-wrap gap-2">
{(data?.rules || []).map((rule) => (
<Badge
className="border-zinc-200 bg-zinc-50 text-zinc-600 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300"
key={rule}
variant="outline"
>
{rule}
</Badge>
))}
</div>
</div>
</CardHeader>
<CardContent>
<div className="overflow-x-auto rounded-lg border">
<div className="min-w-[980px]">
<div className="grid grid-cols-[138px_132px_86px_minmax(220px,1fr)_140px_120px] border-b bg-zinc-50 px-4 py-2 font-medium text-muted-foreground text-xs dark:bg-zinc-900/70">
<div>时间</div>
<div>IP</div>
<div>状态</div>
<div>路径</div>
<div>用户</div>
<div>标注</div>
</div>
{(data?.recentEvents || []).map((event) => (
<div
className="grid grid-cols-[138px_132px_86px_minmax(220px,1fr)_140px_120px] items-center border-b px-4 py-3 text-sm last:border-b-0"
key={event.id}
>
<div className="text-muted-foreground text-xs">
{formatTime(event.createdAt)}
</div>
<div className="font-mono text-xs">{event.ip}</div>
<div
className={cn(
"font-semibold text-xs",
getStatusClassName(event.statusCode)
)}
>
{event.statusCode || "--"}
</div>
<div className="min-w-0">
<div className="truncate font-medium">
{event.method || "GET"} {event.path || "/"}
</div>
<div className="mt-1 truncate text-muted-foreground text-xs">
{event.reasons?.join("、") || event.outcome}
</div>
</div>
<div className="min-w-0">
<div className="truncate text-xs">
{event.userEmail || "匿名"}
</div>
<div className="mt-1 flex items-center gap-1 text-muted-foreground text-xs">
<UserCheck className="h-3 w-3" />
{event.userRole || "unknown"}
</div>
</div>
<div className="flex items-center gap-2">
{event.severity === "high" || event.severity === "critical" ? (
<AlertTriangle className="h-4 w-4 text-orange-500" />
) : null}
<AccessSeverityBadge severity={event.severity} />
</div>
</div>
))}
{(!data?.recentEvents || data.recentEvents.length === 0) && (
<div className="py-10 text-center text-muted-foreground text-sm">
暂无访问日志
</div>
)}
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,148 @@
"use client";
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { ADMIN_SETTINGS_NAV_ITEMS } from "../_lib/settings-navigation";
import type { AdminSettingsNavItem } from "../_lib/types";
function SettingsQuickAction({
activeTab,
item,
onTabChange,
stretched = false,
}: {
activeTab: string;
item: AdminSettingsNavItem;
onTabChange: (value: string) => void;
stretched?: boolean;
}) {
const Icon = item.icon;
const className =
item.value === activeTab
? "border-foreground/10 bg-foreground text-background shadow-sm hover:!bg-foreground/90 hover:!text-background dark:border-white/20 dark:bg-white dark:text-black dark:hover:!bg-white/90 dark:hover:!text-black"
: "border-transparent bg-transparent text-muted-foreground hover:!border-border/80 hover:!bg-zinc-100 hover:!text-foreground dark:hover:!bg-zinc-900/50";
const layoutClassName = stretched
? "h-8 w-full min-w-0 justify-center gap-1.5 rounded-md border px-2.5 text-xs"
: "h-8 shrink-0 gap-1.5 rounded-md border px-2.5 text-xs";
if (item.href) {
return (
<Button
asChild
className={`${layoutClassName} ${className}`}
size="sm"
variant="ghost"
>
<Link href={item.href}>
<Icon className="h-3.5 w-3.5" />
<span className={stretched ? "truncate" : ""}>{item.label}</span>
</Link>
</Button>
);
}
return (
<Button
className={`${layoutClassName} ${className}`}
onClick={() => onTabChange(item.value)}
size="sm"
variant="ghost"
>
<Icon className="h-3.5 w-3.5" />
<span className={stretched ? "truncate" : ""}>{item.label}</span>
</Button>
);
}
export function AdminSettingsNavigation({
activeTab,
onTabChange,
}: {
activeTab: string;
onTabChange: (value: string) => void;
}) {
const router = useRouter();
const activeItem = ADMIN_SETTINGS_NAV_ITEMS.find(
(item) => item.value === activeTab
);
const handleSelectChange = (value: string) => {
const nextItem = ADMIN_SETTINGS_NAV_ITEMS.find(
(item) => item.value === value
);
if (nextItem?.href) {
router.push(nextItem.href);
return;
}
onTabChange(value);
};
return (
<nav
aria-label="系统设置导航"
className="sticky top-2 z-30 rounded-lg border bg-background/95 p-2 shadow-sm backdrop-blur supports-[backdrop-filter]:bg-background/85"
>
<div className="md:hidden">
<div className="flex items-center gap-2">
<Select onValueChange={handleSelectChange} value={activeTab}>
<SelectTrigger className="h-8 flex-1 border-border/70 bg-background shadow-none">
<SelectValue placeholder={activeItem?.label || "选择设置项"} />
</SelectTrigger>
<SelectContent>
{ADMIN_SETTINGS_NAV_ITEMS.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
asChild
className="h-8 rounded-md border-border/70 px-2.5 text-xs"
size="sm"
variant="outline"
>
<Link href="/">
<ArrowLeft className="h-3.5 w-3.5" />
返回
</Link>
</Button>
</div>
</div>
<div className="hidden min-w-0 grid-cols-7 gap-1 md:grid xl:grid-cols-[repeat(13,minmax(78px,1fr))_auto]">
{ADMIN_SETTINGS_NAV_ITEMS.map((item) => (
<SettingsQuickAction
activeTab={activeTab}
item={item}
key={item.value}
onTabChange={onTabChange}
stretched
/>
))}
<Button
asChild
className="h-8 min-w-0 rounded-md border-border/70 px-2.5 text-xs"
size="sm"
variant="outline"
>
<Link href="/">
<ArrowLeft className="h-3.5 w-3.5" />
返回工作台
</Link>
</Button>
</div>
</nav>
);
}

View File

@ -0,0 +1,148 @@
"use client";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { LOCAL_AI_QUICK_LINK_GROUPS } from "../_lib/local-ai-sections";
import type { LocalAiOverviewItem, LocalAiQuickLink } from "../_lib/types";
import { SettingStatusBadge } from "./setting-status-badge";
export function LocalAiCommandCenter({
openSectionCount,
providerCount,
modelCount,
overviewItems,
quickLinks,
isSectionExpanded,
onFocusSection,
onSetAllExpanded,
}: {
openSectionCount: number;
providerCount: number;
modelCount: number;
overviewItems: LocalAiOverviewItem[];
quickLinks: LocalAiQuickLink[];
isSectionExpanded: (sectionKey: string) => boolean;
onFocusSection: (sectionKey: string) => void;
onSetAllExpanded: (expanded: boolean) => void;
}) {
return (
<div className="overflow-hidden rounded-lg border bg-background">
<div className="border-b bg-muted/20 px-4 py-3">
<div className="flex flex-col gap-2.5 lg:flex-row lg:items-start lg:justify-between">
<div className="space-y-1">
<div className="font-semibold text-[10px] text-muted-foreground">
SYSTEM INTEGRATIONS
</div>
<h2 className="font-semibold text-lg">
高级能力与外部服务
</h2>
<p className="max-w-3xl text-muted-foreground text-xs leading-5">
把商品策划、图像处理、3D/CAD、实时画布和开放 API
按运维任务重新归类。折叠状态会保留,常用模块可以保持展开。
</p>
</div>
<div className="flex flex-wrap gap-1.5">
<SettingStatusBadge tone="neutral">
已展开 {openSectionCount} 个模块
</SettingStatusBadge>
<SettingStatusBadge tone="enabled">
供应商 {providerCount}
</SettingStatusBadge>
<SettingStatusBadge tone="neutral">
模型 {modelCount}
</SettingStatusBadge>
<Button
className="h-6 rounded-md px-2 text-[11px]"
onClick={() => onSetAllExpanded(true)}
size="sm"
type="button"
variant="outline"
>
全部展开
</Button>
<Button
className="h-6 rounded-md px-2 text-[11px]"
onClick={() => onSetAllExpanded(false)}
size="sm"
type="button"
variant="outline"
>
全部收起
</Button>
</div>
</div>
</div>
<div className="grid gap-px bg-border md:grid-cols-2 xl:grid-cols-4">
{overviewItems.map((item) => {
const Icon = item.icon;
return (
<button
className="group bg-background p-3 text-left transition-colors hover:bg-muted/30"
key={item.key}
onClick={() => onFocusSection(item.key)}
type="button"
>
<div className="flex items-start justify-between gap-3">
<div className="flex min-w-0 items-center gap-2">
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md border bg-muted/30">
<Icon className="h-3.5 w-3.5 text-muted-foreground" />
</div>
<div className="min-w-0">
<div className="truncate font-semibold text-xs">
{item.title}
</div>
<div className="mt-0.5 truncate text-[11px] text-muted-foreground">
{item.description}
</div>
</div>
</div>
<SettingStatusBadge tone={item.tone}>
{item.status}
</SettingStatusBadge>
</div>
</button>
);
})}
</div>
<div className="border-t bg-muted/10 px-4 py-3">
<div className="grid gap-3 lg:grid-cols-4">
{LOCAL_AI_QUICK_LINK_GROUPS.map((group) => (
<div className="space-y-1.5" key={group}>
<div className="font-semibold text-[11px] text-muted-foreground">
{group}
</div>
<div className="flex flex-wrap gap-1.5">
{quickLinks
.filter((item) => item.group === group)
.map((item) => (
<button
className={cn(
"inline-flex h-6 items-center gap-1 rounded-md border px-2 text-[11px] transition-colors hover:bg-background",
isSectionExpanded(item.key)
? "border-foreground/20 bg-background text-foreground"
: "border-border bg-transparent text-muted-foreground"
)}
key={item.key}
onClick={() => onFocusSection(item.key)}
type="button"
>
<span
className={cn(
"h-1.5 w-1.5 rounded-full",
item.tone === "enabled" && "bg-emerald-500",
item.tone === "disabled" && "bg-zinc-300",
item.tone === "warning" && "bg-amber-500",
item.tone === "neutral" && "bg-sky-500"
)}
/>
{item.label}
</button>
))}
</div>
</div>
))}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,121 @@
"use client";
import { ArrowDown, ArrowUp } from "lucide-react";
import type { ReactNode } from "react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { cn } from "@/lib/utils";
import type { SettingStatusTone } from "../_lib/types";
import { SettingStatusBadge } from "./setting-status-badge";
export function LocalAiGroupHeading({
eyebrow,
title,
description,
}: {
eyebrow: string;
title: string;
description: string;
}) {
return (
<div className="rounded-lg border bg-muted/20 px-3 py-2.5">
<div className="font-semibold text-[10px] text-muted-foreground uppercase">
{eyebrow}
</div>
<div className="mt-1 flex flex-col gap-1 md:flex-row md:items-end md:justify-between">
<h3 className="font-semibold text-sm text-foreground">{title}</h3>
<p className="max-w-3xl text-muted-foreground text-xs">{description}</p>
</div>
</div>
);
}
export function LocalAiSectionSummary({
items,
}: {
items: Array<{
label: string;
tone?: SettingStatusTone;
}>;
}) {
return (
<div className="flex flex-wrap gap-1.5 pt-1">
{items.map((item) => (
<SettingStatusBadge key={item.label} tone={item.tone || "neutral"}>
{item.label}
</SettingStatusBadge>
))}
</div>
);
}
export function LocalAiSectionToggleButton({
expanded,
onClick,
}: {
expanded: boolean;
onClick: () => void;
}) {
return (
<Button
aria-expanded={expanded}
className="h-7 shrink-0 gap-1 rounded-md px-2 text-[11px]"
onClick={onClick}
size="sm"
type="button"
variant="outline"
>
{expanded ? (
<ArrowUp className="h-3 w-3" />
) : (
<ArrowDown className="h-3 w-3" />
)}
{expanded ? "收起配置" : "展开配置"}
</Button>
);
}
export function LocalAiSectionShell({
id,
title,
description,
summary,
expanded,
onToggle,
children,
}: {
id: string;
title: ReactNode;
description: ReactNode;
summary?: ReactNode;
expanded: boolean;
onToggle: () => void;
children: ReactNode;
}) {
return (
<Card id={id}>
<CardHeader className="flex flex-col gap-3 space-y-0 md:flex-row md:items-start md:justify-between">
<div className="space-y-1">
<CardTitle className="text-base">{title}</CardTitle>
<CardDescription>{description}</CardDescription>
{summary}
</div>
<LocalAiSectionToggleButton expanded={expanded} onClick={onToggle} />
</CardHeader>
<CardContent
className={cn(
"local-ai-config-card-content space-y-4 pt-4",
!expanded && "hidden"
)}
>
{children}
</CardContent>
</Card>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,754 @@
"use client";
import { Save } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL,
DETAIL_AGENT_LOCAL_MOONSHOT_DISPLAY_NAME,
DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID,
} from "@/lib/ai/detail-agent-model-config";
import {
PRODUCT_PLANNING_AI_MODEL_DEFAULT_BASE_URL,
PRODUCT_PLANNING_AI_MODEL_DEFAULT_MODEL_ID,
} from "@/lib/product-planning/model-config";
import type { LocalAiPlanningSectionsProps } from "../../_lib/types";
import {
LocalAiGroupHeading,
LocalAiSectionShell,
LocalAiSectionSummary,
} from "../local-ai-section-shell";
export function LocalAiPlanningSections({
siteSettings,
setSiteSettings,
productPlanningApiConfig,
setProductPlanningApiConfig,
productPlanningAiModelConfig,
setProductPlanningAiModelConfig,
moonshotDetailAgentConfig,
setMoonshotDetailAgentConfig,
isLocalAiSectionExpanded,
toggleLocalAiSection,
handleSaveProductPlanningApiConfig,
handleSaveProductPlanningAiModelConfig,
handleSaveMoonshotDetailAgentConfig,
}: LocalAiPlanningSectionsProps) {
return (
<>
<LocalAiGroupHeading
description="把电商数据源、商品策划专用模型、详情 Agent 和业务数据库放在同一组,减少跨页面查找。"
eyebrow="Planning"
title="商品策划与数据集成"
/>
<LocalAiSectionShell
description={
<>
配置商品策划工具页面使用的淘宝 / 京东
Onebound、问大家和销量接口。保存后商品策划相关接口会优先读取这里的配置,未填写时才回退到环境变量。
</>
}
expanded={isLocalAiSectionExpanded("product-planning-api")}
id="local-ai-section-product-planning-api"
onToggle={() => toggleLocalAiSection("product-planning-api")}
summary={
<LocalAiSectionSummary
items={[
{
label: siteSettings.productPlanningEnabled
? "功能已启用"
: "功能已关闭",
tone: siteSettings.productPlanningEnabled
? "enabled"
: "disabled",
},
{
label: productPlanningApiConfig.oneboundBaseUrl
? "Onebound 已配置"
: "Onebound 待配置",
tone: productPlanningApiConfig.oneboundBaseUrl
? "enabled"
: "warning",
},
{
label: productPlanningApiConfig.planningDbHost
? "回写库已配置"
: "回写库待配置",
tone: productPlanningApiConfig.planningDbHost
? "enabled"
: "neutral",
},
]}
/>
}
title="商品策划工具 API 配置"
>
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="flex-1 space-y-1">
<Label className="text-base">启用商品策划工具</Label>
<div className="text-muted-foreground text-sm">
关闭后左侧导航入口会隐藏,商品策划页面和相关接口也会一并禁用。
</div>
</div>
<Switch
checked={siteSettings.productPlanningEnabled}
onCheckedChange={(checked) =>
setSiteSettings({
...siteSettings,
productPlanningEnabled: checked,
})
}
/>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="productPlanningOneboundBaseUrl">
Onebound API 地址
</Label>
<Input
id="productPlanningOneboundBaseUrl"
onChange={(e) =>
setProductPlanningApiConfig((prev) => ({
...prev,
oneboundBaseUrl: e.target.value,
}))
}
placeholder="https://api-gw.onebound.cn"
value={productPlanningApiConfig.oneboundBaseUrl}
/>
<div className="text-muted-foreground text-xs">
用于淘宝评价、商品详情、关键词洞察以及京东商品详情。
</div>
</div>
<div className="space-y-2">
<Label htmlFor="productPlanningDetailPlusBaseUrl">
详情增强接口地址(公网)
</Label>
<Input
id="productPlanningDetailPlusBaseUrl"
onChange={(e) =>
setProductPlanningApiConfig((prev) => ({
...prev,
detailPlusBaseUrl: e.target.value,
}))
}
placeholder="http://ltai5tdb2ogyj59fquyedh.yydd.ai:18082/api/public/taobao-detail-plus"
value={productPlanningApiConfig.detailPlusBaseUrl}
/>
<div className="text-muted-foreground text-xs">
竞品分析会优先用这个接口查询商品详情,查不到再走 Onebound 旧接口。
</div>
</div>
<div className="space-y-2">
<Label htmlFor="productPlanningDetailPlusInternalBaseUrl">
详情增强接口地址(内网,可选)
</Label>
<Input
id="productPlanningDetailPlusInternalBaseUrl"
onChange={(e) =>
setProductPlanningApiConfig((prev) => ({
...prev,
detailPlusInternalBaseUrl: e.target.value,
}))
}
placeholder="http://127.0.0.1:4000/api/public/taobao-detail-plus"
value={productPlanningApiConfig.detailPlusInternalBaseUrl}
/>
<div className="text-muted-foreground text-xs">
服务器同机部署时可填内网地址,系统会先尝试内网再尝试公网。
</div>
</div>
<div className="space-y-2 lg:col-span-2">
<Label htmlFor="productPlanningDetailPlusApiKey">
详情增强接口 API Key
</Label>
<Input
id="productPlanningDetailPlusApiKey"
onChange={(e) =>
setProductPlanningApiConfig((prev) => ({
...prev,
detailPlusApiKey: e.target.value,
}))
}
placeholder="留空保存时会保留原 API Key"
type="password"
value={productPlanningApiConfig.detailPlusApiKey}
/>
</div>
<div className="space-y-2">
<Label htmlFor="productPlanningQaApiUrl">问大家接口地址</Label>
<Input
id="productPlanningQaApiUrl"
onChange={(e) =>
setProductPlanningApiConfig((prev) => ({
...prev,
qaApiUrl: e.target.value,
}))
}
placeholder="http://119.91.237.2:5432/taobao_social_feed_aggregate"
value={productPlanningApiConfig.qaApiUrl}
/>
<div className="text-muted-foreground text-xs">
用于商品策划工具中的问大家抓取。
</div>
</div>
<div className="space-y-2">
<Label htmlFor="productPlanningTaobaoApiKey">
淘宝 / 京东 API Key
</Label>
<Input
id="productPlanningTaobaoApiKey"
onChange={(e) =>
setProductPlanningApiConfig((prev) => ({
...prev,
taobaoApiKey: e.target.value,
}))
}
placeholder="请输入 Onebound API Key"
type="password"
value={productPlanningApiConfig.taobaoApiKey}
/>
</div>
<div className="space-y-2">
<Label htmlFor="productPlanningTaobaoApiSecret">
淘宝 / 京东 API Secret
</Label>
<Input
id="productPlanningTaobaoApiSecret"
onChange={(e) =>
setProductPlanningApiConfig((prev) => ({
...prev,
taobaoApiSecret: e.target.value,
}))
}
placeholder="请输入 Onebound API Secret"
type="password"
value={productPlanningApiConfig.taobaoApiSecret}
/>
</div>
<div className="space-y-2">
<Label htmlFor="productPlanningQaToken">问大家 Token</Label>
<Input
id="productPlanningQaToken"
onChange={(e) =>
setProductPlanningApiConfig((prev) => ({
...prev,
qaToken: e.target.value,
}))
}
placeholder="请输入问大家抓取 Token"
type="password"
value={productPlanningApiConfig.qaToken}
/>
</div>
<div className="space-y-2">
<Label htmlFor="productPlanningSaleApiUrl">销量接口地址</Label>
<Input
id="productPlanningSaleApiUrl"
onChange={(e) =>
setProductPlanningApiConfig((prev) => ({
...prev,
saleApiUrl: e.target.value,
}))
}
placeholder="http://119.91.237.2:5432/taobao_item_month_sale"
value={productPlanningApiConfig.saleApiUrl}
/>
<div className="text-muted-foreground text-xs">
用于商品策划工具中的销量查询。
</div>
</div>
<div className="space-y-2">
<Label htmlFor="productPlanningSaleToken">销量 Token</Label>
<Input
id="productPlanningSaleToken"
onChange={(e) =>
setProductPlanningApiConfig((prev) => ({
...prev,
saleToken: e.target.value,
}))
}
placeholder="请输入销量查询 Token"
type="password"
value={productPlanningApiConfig.saleToken}
/>
</div>
<div className="rounded-lg border bg-muted/30 p-4 lg:col-span-2">
<div className="mb-4 flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
<div>
<div className="font-medium">企划数据库回写配置</div>
<div className="text-muted-foreground text-xs">
旧接口兜底查到的淘宝详情会同步写入新的企划详情表,供后续新接口命中。
</div>
</div>
<div className="flex items-center gap-2 text-sm">
<span className="text-muted-foreground">SSL</span>
<Switch
checked={productPlanningApiConfig.planningDbSsl}
onCheckedChange={(checked) =>
setProductPlanningApiConfig((prev) => ({
...prev,
planningDbSsl: checked,
}))
}
/>
</div>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="productPlanningDbHost">数据库 Host</Label>
<Input
id="productPlanningDbHost"
onChange={(e) =>
setProductPlanningApiConfig((prev) => ({
...prev,
planningDbHost: e.target.value,
}))
}
placeholder="156.225.27.52"
value={productPlanningApiConfig.planningDbHost}
/>
</div>
<div className="space-y-2">
<Label htmlFor="productPlanningDbPort">数据库端口</Label>
<Input
id="productPlanningDbPort"
onChange={(e) =>
setProductPlanningApiConfig((prev) => ({
...prev,
planningDbPort: e.target.value,
}))
}
placeholder="60023"
value={productPlanningApiConfig.planningDbPort}
/>
</div>
<div className="space-y-2">
<Label htmlFor="productPlanningDbDatabase">数据库名</Label>
<Input
id="productPlanningDbDatabase"
onChange={(e) =>
setProductPlanningApiConfig((prev) => ({
...prev,
planningDbDatabase: e.target.value,
}))
}
placeholder="planning_database"
value={productPlanningApiConfig.planningDbDatabase}
/>
</div>
<div className="space-y-2">
<Label htmlFor="productPlanningDbUser">数据库账号</Label>
<Input
id="productPlanningDbUser"
onChange={(e) =>
setProductPlanningApiConfig((prev) => ({
...prev,
planningDbUser: e.target.value,
}))
}
placeholder="planning_user"
value={productPlanningApiConfig.planningDbUser}
/>
</div>
<div className="space-y-2">
<Label htmlFor="productPlanningDbPassword">数据库密码</Label>
<Input
id="productPlanningDbPassword"
onChange={(e) =>
setProductPlanningApiConfig((prev) => ({
...prev,
planningDbPassword: e.target.value,
}))
}
placeholder="留空保存时会保留原密码"
type="password"
value={productPlanningApiConfig.planningDbPassword}
/>
</div>
<div className="space-y-2">
<Label htmlFor="productPlanningDbCharset">字符集</Label>
<Input
id="productPlanningDbCharset"
onChange={(e) =>
setProductPlanningApiConfig((prev) => ({
...prev,
planningDbCharset: e.target.value,
}))
}
placeholder="utf8mb4"
value={productPlanningApiConfig.planningDbCharset}
/>
</div>
</div>
</div>
</div>
<div className="space-y-1 rounded-lg border border-blue-200 bg-blue-50 p-4 text-blue-800 text-xs">
<p>
• 当前功能状态:
{siteSettings.productPlanningEnabled
? "已启用,左侧导航和接口开放中。"
: "已关闭,入口、页面和接口都会被禁用。"}
</p>
<p>
•
竞品分析会优先读取详情增强接口;未命中时自动走旧接口,并将旧结果回写到企划详情表。
</p>
<p>
•
若某个字段留空,系统会继续尝试读取旧环境变量,避免你切换配置期间功能直接中断。
</p>
</div>
<div className="flex justify-end">
<Button onClick={handleSaveProductPlanningApiConfig}>
<Save className="mr-2 h-4 w-4" />
保存商品策划工具设置
</Button>
</div>
</LocalAiSectionShell>
<LocalAiSectionShell
description={
<>
为商品策划工具单独绑定一套分析模型。开启后,评价分析、问大家分析、竞品图片分析和
SKU 卖点分析会优先走这里配置的 OpenAI
兼容模型,不再依赖站点默认模型。
</>
}
expanded={isLocalAiSectionExpanded("product-planning-ai-model")}
id="local-ai-section-product-planning-ai-model"
onToggle={() => toggleLocalAiSection("product-planning-ai-model")}
summary={
<LocalAiSectionSummary
items={[
{
label: productPlanningAiModelConfig.enabled
? "专用模型已启用"
: "回退默认模型",
tone: productPlanningAiModelConfig.enabled
? "enabled"
: "neutral",
},
{
label: productPlanningAiModelConfig.modelId || "模型 ID 待配置",
tone: productPlanningAiModelConfig.modelId
? "neutral"
: "warning",
},
]}
/>
}
title="商品策划专用 AI 模型"
>
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="flex-1 space-y-1">
<Label className="text-base">启用专用模型</Label>
<div className="text-muted-foreground text-sm">
推荐在商品策划需要固定分析口径时开启。关闭后会自动回退到站点当前可用的默认模型。
</div>
</div>
<Switch
checked={productPlanningAiModelConfig.enabled}
onCheckedChange={(checked) =>
setProductPlanningAiModelConfig((prev) => ({
...prev,
enabled: checked,
}))
}
/>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="productPlanningAiProviderName">提供商名称</Label>
<Input
id="productPlanningAiProviderName"
onChange={(e) =>
setProductPlanningAiModelConfig((prev) => ({
...prev,
providerName: e.target.value,
}))
}
placeholder="Product Planning Qwen3-VL"
value={productPlanningAiModelConfig.providerName}
/>
</div>
<div className="space-y-2">
<Label htmlFor="productPlanningAiDisplayName">显示名称</Label>
<Input
id="productPlanningAiDisplayName"
onChange={(e) =>
setProductPlanningAiModelConfig((prev) => ({
...prev,
displayName: e.target.value,
}))
}
placeholder="商品策划专用 Qwen3-VL"
value={productPlanningAiModelConfig.displayName}
/>
</div>
<div className="space-y-2">
<Label htmlFor="productPlanningAiBaseUrl">
OpenAI 兼容 API 地址
</Label>
<Input
id="productPlanningAiBaseUrl"
onChange={(e) =>
setProductPlanningAiModelConfig((prev) => ({
...prev,
baseUrl: e.target.value,
}))
}
placeholder={PRODUCT_PLANNING_AI_MODEL_DEFAULT_BASE_URL}
value={productPlanningAiModelConfig.baseUrl}
/>
<div className="text-muted-foreground text-xs">
支持填写服务根地址,例如
`http://103.118.245.6:6081`。系统会自动兼容补全 `/v1` 路径。
</div>
</div>
<div className="space-y-2">
<Label htmlFor="productPlanningAiModelId">模型 ID</Label>
<Input
id="productPlanningAiModelId"
onChange={(e) =>
setProductPlanningAiModelConfig((prev) => ({
...prev,
modelId: e.target.value,
}))
}
placeholder={PRODUCT_PLANNING_AI_MODEL_DEFAULT_MODEL_ID}
value={productPlanningAiModelConfig.modelId}
/>
<div className="text-muted-foreground text-xs">
你的 Qwen3-VL OpenAI 兼容接口若使用默认模型名,可直接保留
`Qwen3-VL-4B-Instruct`。
</div>
</div>
<div className="space-y-2 lg:col-span-2">
<Label htmlFor="productPlanningAiApiKey">API Key</Label>
<Input
id="productPlanningAiApiKey"
onChange={(e) =>
setProductPlanningAiModelConfig((prev) => ({
...prev,
apiKey: e.target.value,
}))
}
placeholder="请输入商品策划专用模型 API Key"
type="password"
value={productPlanningAiModelConfig.apiKey}
/>
</div>
</div>
<div className="space-y-1 rounded-lg border border-blue-200 bg-blue-50 p-4 text-blue-800 text-xs">
<p>
•
开启后,商品策划页的文本分析和图片分析都会优先使用这里配置的独立模型。
</p>
<p>
•
关闭后,系统会继续回退到站点默认文本/视觉模型,不会影响其他页面的模型设置。
</p>
</div>
<div className="flex justify-end">
<Button onClick={handleSaveProductPlanningAiModelConfig}>
<Save className="mr-2 h-4 w-4" />
保存商品策划专用模型
</Button>
</div>
</LocalAiSectionShell>
<LocalAiSectionShell
description={
<>
为 AI详情生成 单独配置 Kimi Code 编程套餐
模型。保存后会自动绑定到“AI详情生成 - Agent专用模型”。
</>
}
expanded={isLocalAiSectionExpanded("moonshot-detail-agent")}
id="local-ai-section-moonshot-detail-agent"
onToggle={() => toggleLocalAiSection("moonshot-detail-agent")}
summary={
<LocalAiSectionSummary
items={[
{
label: moonshotDetailAgentConfig.isEnabled
? "模型已启用"
: "模型已关闭",
tone: moonshotDetailAgentConfig.isEnabled
? "enabled"
: "disabled",
},
{
label: moonshotDetailAgentConfig.modelId || "模型 ID 待配置",
tone: moonshotDetailAgentConfig.modelId ? "neutral" : "warning",
},
]}
/>
}
title={<>Kimi Code 编程套餐配置</>}
>
<div className="space-y-2">
<Label htmlFor="moonshotProviderName">提供商名称</Label>
<Input
id="moonshotProviderName"
onChange={(e) =>
setMoonshotDetailAgentConfig((prev) => ({
...prev,
providerName: e.target.value,
}))
}
placeholder="Kimi Code 编程套餐"
value={moonshotDetailAgentConfig.providerName}
/>
</div>
<div className="space-y-2">
<Label htmlFor="moonshotBaseUrl">编程套餐专用 API 地址</Label>
<Input
id="moonshotBaseUrl"
onChange={(e) =>
setMoonshotDetailAgentConfig((prev) => ({
...prev,
baseUrl: e.target.value,
}))
}
placeholder={DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL}
value={moonshotDetailAgentConfig.baseUrl}
/>
<div className="text-muted-foreground text-xs">
根据 Kimi Code 文档,编程套餐 OpenAI 兼容接口地址为 `
{DETAIL_AGENT_LOCAL_MOONSHOT_BASE_URL}`
</div>
</div>
<div className="space-y-2">
<Label htmlFor="moonshotApiKey">
API Key{" "}
{moonshotDetailAgentConfig.providerId ? "(留空则不修改)" : "*"}
</Label>
<Input
id="moonshotApiKey"
onChange={(e) =>
setMoonshotDetailAgentConfig((prev) => ({
...prev,
apiKey: e.target.value,
}))
}
placeholder={
moonshotDetailAgentConfig.providerId
? "留空则不修改当前 Key"
: "请输入 Kimi Code 控制台 API Key"
}
type="password"
value={moonshotDetailAgentConfig.apiKey}
/>
</div>
<div className="space-y-2">
<Label htmlFor="moonshotModelId">Kimi Code API 模型 ID *</Label>
<Input
id="moonshotModelId"
onChange={(e) =>
setMoonshotDetailAgentConfig((prev) => ({
...prev,
modelId: e.target.value,
}))
}
placeholder={DETAIL_AGENT_LOCAL_MOONSHOT_MODEL_ID}
value={moonshotDetailAgentConfig.modelId}
/>
<div className="text-muted-foreground text-xs">
Kimi Code 编程套餐专用接口固定使用
`kimi-for-coding`;保存后系统会自动创建/更新对应模型记录。
</div>
</div>
<div className="space-y-2">
<Label htmlFor="moonshotDisplayName">模型显示名称</Label>
<Input
id="moonshotDisplayName"
onChange={(e) =>
setMoonshotDetailAgentConfig((prev) => ({
...prev,
displayName: e.target.value,
}))
}
placeholder={DETAIL_AGENT_LOCAL_MOONSHOT_DISPLAY_NAME}
value={moonshotDetailAgentConfig.displayName}
/>
</div>
<div className="flex items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<Label className="text-sm">启用该详情 Agent 模型</Label>
<div className="text-muted-foreground text-xs">
关闭后仍会保留记录,但不会被 AI详情生成 选作专用 Agent 模型。
</div>
</div>
<div className="flex items-center gap-2">
<Switch
checked={moonshotDetailAgentConfig.isEnabled}
onCheckedChange={(checked) =>
setMoonshotDetailAgentConfig((prev) => ({
...prev,
isEnabled: checked,
}))
}
/>
<span className="min-w-[3rem] font-medium text-muted-foreground text-sm">
{moonshotDetailAgentConfig.isEnabled ? "已开启" : "已关闭"}
</span>
</div>
</div>
<div className="space-y-1 rounded-lg border border-blue-200 bg-blue-50 p-4 text-blue-800 text-xs">
<p>
• 专用于 AI详情生成 的分镜规划与排版复审,不影响普通 Agent 对话。
</p>
<p>
• 系统会按多模态 Agent
模型写入:支持视觉、支持结构化规划、支持协议复审。
</p>
<p>• 保存成功后会自动写入“AI详情生成 - Agent专用模型”。</p>
</div>
<div className="flex justify-end">
<Button onClick={handleSaveMoonshotDetailAgentConfig}>
<Save className="mr-2 h-4 w-4" />
保存 Kimi Code 编程套餐配置
</Button>
</div>
</LocalAiSectionShell>
</>
);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
"use client";
import type { ReactNode } from "react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import type { SettingStatusTone } from "../_lib/types";
export function SettingStatusBadge({
tone,
children,
}: {
tone: SettingStatusTone;
children: ReactNode;
}) {
return (
<Badge
className={cn(
"h-5 rounded-md border px-1.5 font-medium text-[10px]",
tone === "enabled" &&
"border-emerald-200 bg-emerald-50 text-emerald-700",
tone === "disabled" && "border-zinc-200 bg-zinc-50 text-zinc-500",
tone === "warning" && "border-amber-200 bg-amber-50 text-amber-700",
tone === "neutral" && "border-sky-200 bg-sky-50 text-sky-700"
)}
variant="outline"
>
{children}
</Badge>
);
}

View File

@ -0,0 +1,33 @@
export const LOCAL_AI_EXPANDED_STORAGE_KEY =
"admin-settings:local-ai-expanded-sections";
export const DEFAULT_LOCAL_AI_EXPANDED_SECTIONS: Record<string, boolean> = {
"product-planning-api": true,
"product-planning-ai-model": false,
"moonshot-detail-agent": false,
tripo3d: false,
worldlabs3d: false,
"3d-model-alias": false,
"kittycad-cad": false,
rmbg: false,
enhance: false,
"enhance-pro": false,
"canvas-local-edit": false,
"ai-wash": false,
extend: false,
"watermark-remove": false,
"angle-edit": false,
"layer-split": false,
"third-party-sqlserver": false,
"external-image-api": true,
"realtime-canvas": false,
"canvas-image-task-queue": false,
"batch-process": false,
};
export const LOCAL_AI_QUICK_LINK_GROUPS = [
"商品策划",
"3D/CAD",
"图像处理",
"运行控制",
] as const;

View File

@ -0,0 +1,120 @@
import {
BarChart3,
Bell,
Bot,
HardDrive,
Image as ImageIcon,
Key,
Menu,
MessageSquare,
Scissors,
Settings,
ShieldAlert,
Sparkles,
User,
} from "lucide-react";
import type { AdminSettingsNavGroup } from "./types";
export const ADMIN_SETTINGS_NAV_GROUPS: AdminSettingsNavGroup[] = [
{
title: "运营与数据",
description: "日常运营最常用入口,优先看用户、资产记录和效果数据。",
items: [
{
value: "users",
label: "用户管理",
description: "用户、权限与账号状态",
icon: User,
},
{
value: "images",
label: "生图/视频记录",
description: "生成资产、任务与产物追踪",
icon: ImageIcon,
},
{
value: "analytics",
label: "数据分析",
description: "用量、趋势与运营指标",
icon: BarChart3,
},
{
value: "access-security",
label: "访问安全",
description: "访问 IP、异常标注与登录来源",
icon: ShieldAlert,
},
],
},
{
title: "站点体验",
description: "面向用户可见的站点配置,影响导航、文案、认证和通知体验。",
items: [
{
value: "site",
label: "网站设置",
description: "基础信息、展示与安全策略",
icon: Settings,
},
{
value: "style-pro",
label: "风格化 PRO",
description: "独立高级风格化配置",
icon: Sparkles,
href: "/settings/style-pro",
},
{
value: "nav",
label: "导航设置",
description: "菜单、入口与站内路径",
icon: Menu,
},
{
value: "questions",
label: "快捷短语",
description: "预设问题与提示短语",
icon: MessageSquare,
},
{
value: "auth",
label: "认证设置",
description: "登录、注册与访问控制",
icon: Key,
},
{
value: "notification",
label: "钉钉通知",
description: "告警、审批与业务通知",
icon: Bell,
},
],
},
{
title: "模型与基础设施",
description: "上线前重点检查,集中管理模型、存储和外部服务依赖。",
items: [
{
value: "ai",
label: "AI 模型",
description: "供应商、模型与 Key 池",
icon: Bot,
},
{
value: "storage",
label: "存储配置",
description: "对象存储、迁移与清理",
icon: HardDrive,
},
{
value: "rmbg",
label: "高级能力",
description: "本地 AI、3D/CAD 与图像处理",
icon: Scissors,
},
],
},
];
export const ADMIN_SETTINGS_NAV_ITEMS = ADMIN_SETTINGS_NAV_GROUPS.flatMap(
(group) => group.items
);

View File

@ -0,0 +1,472 @@
import type { LucideIcon } from "lucide-react";
import type { Dispatch, DragEvent, ReactNode, SetStateAction } from "react";
import type { CanvasImageTaskQueueConfig } from "@/lib/canvas/image-task-config";
import type { CanvasLocalEditConfig } from "@/lib/canvas/local-edit-config";
import type { AiWashConfig } from "@/lib/ai-wash/config";
import type {
EnhanceProKreaConfig,
EnhanceProKreaModel,
EnhanceProPreset,
} from "@/lib/enhance-pro/config";
import type { ThreeDModelAliasConfig } from "@/lib/model3d/model-alias-config";
import type { PointsSystemConfig } from "@/lib/points/config";
import type { ProductPlanningApiConfig } from "@/lib/product-planning/config";
import type { ProductPlanningAiModelConfig } from "@/lib/product-planning/model-config";
import type { DEFAULT_LOCAL_AI_EXPANDED_SECTIONS } from "./local-ai-sections";
export type SettingStatusTone = "enabled" | "disabled" | "warning" | "neutral";
export type AdminSettingsNavItem = {
value: string;
label: string;
description: string;
icon: LucideIcon;
href?: string;
};
export type AdminSettingsNavGroup = {
title: string;
description: string;
items: AdminSettingsNavItem[];
};
export type LocalAiOverviewItem = {
key: string;
title: string;
description: string;
status: string;
tone: SettingStatusTone;
icon: LucideIcon;
};
export type LocalAiQuickLinkGroup =
| "商品策划"
| "3D/CAD"
| "图像处理"
| "运行控制";
export type LocalAiQuickLink = {
key: string;
label: string;
group: LocalAiQuickLinkGroup;
tone: SettingStatusTone;
};
export type LocalAiSectionKey = keyof typeof DEFAULT_LOCAL_AI_EXPANDED_SECTIONS;
export type LocalAiSectionControlProps = {
isLocalAiSectionExpanded: (sectionKey: LocalAiSectionKey) => boolean;
toggleLocalAiSection: (sectionKey: LocalAiSectionKey) => void;
};
export type AsyncAction = () => void | Promise<void>;
type BivariantStateSetter<T> = {
bivarianceHack(value: SetStateAction<T>): void;
}["bivarianceHack"];
export type AdminAiProvider = {
id: string;
name: string;
type:
| "alibaba"
| "openai"
| "custom"
| "local"
| "gemini"
| "bltcy-video"
| "grsai-gpt-image"
| "grsai-video"
| "huanke-gemini"
| "qianwen-video"
| "doubao-video"
| "ruxa-video"
| "volcano-vision"
| "volcano-video"
| "alibaba-image"
| "bfl-image"
| "ruxa-image"
| "fal-image"
| "snapgen-image"
| "openai-image"
| "bananaimage"
| "visionary"
| "atlascloud"
| "vip98k"
| "stepfun-image"
| "midjourney"
| "fal-video"
| "zhipu"
| "tripo-3d"
| "worldlabs-3d"
| "kittycad-cad"
| "runway"
| "krea"
| "volcano-multimodal";
baseUrl: string;
apiKey: string;
isEnabled: boolean;
createdAt: string;
};
export type AdminAiModel = {
id: string;
providerId: string;
modelId: string;
displayName: string;
supportsVision: boolean;
supportsImageGen: boolean;
isEnabled: boolean;
isHidden: boolean;
isDefault: boolean;
};
export type AdminProvidersData = {
providers: AdminAiProvider[];
};
export type AdminModelsData = {
models: AdminAiModel[];
};
export type PlanningSiteSettings = {
productPlanningEnabled: boolean;
};
export type MoonshotDetailAgentConfig = {
providerId: string;
modelEntryId: string;
providerName: string;
baseUrl: string;
apiKey: string;
modelId: string;
displayName: string;
isEnabled: boolean;
};
export type LocalAiPlanningSectionsProps = LocalAiSectionControlProps & {
siteSettings: PlanningSiteSettings;
setSiteSettings: BivariantStateSetter<PlanningSiteSettings>;
productPlanningApiConfig: ProductPlanningApiConfig;
setProductPlanningApiConfig: Dispatch<
SetStateAction<ProductPlanningApiConfig>
>;
productPlanningAiModelConfig: ProductPlanningAiModelConfig;
setProductPlanningAiModelConfig: Dispatch<
SetStateAction<ProductPlanningAiModelConfig>
>;
moonshotDetailAgentConfig: MoonshotDetailAgentConfig;
setMoonshotDetailAgentConfig: Dispatch<
SetStateAction<MoonshotDetailAgentConfig>
>;
handleSaveProductPlanningApiConfig: AsyncAction;
handleSaveProductPlanningAiModelConfig: AsyncAction;
handleSaveMoonshotDetailAgentConfig: AsyncAction;
};
export type AiProviderSettingsConfig = {
providerId: string;
name: string;
baseUrl: string;
apiKey: string;
isEnabled: boolean;
};
export type WorldLabsKeyPoolConfig = {
importToken: string;
providerName: string;
baseUrl: string;
isEnabled: boolean;
};
export type LocalAiModel3dSectionsProps = LocalAiSectionControlProps & {
tripo3dConfig: AiProviderSettingsConfig;
setTripo3dConfig: Dispatch<SetStateAction<AiProviderSettingsConfig>>;
tripoProviders: AdminAiProvider[];
filteredTripoProviders: AdminAiProvider[];
enabledTripoProviderCount: number;
tripoProviderSearch: string;
setTripoProviderSearch: Dispatch<SetStateAction<string>>;
tripoProviderBusyId: string | null;
worldlabs3dConfig: AiProviderSettingsConfig;
setWorldlabs3dConfig: Dispatch<SetStateAction<AiProviderSettingsConfig>>;
worldlabsKeyPoolConfig: WorldLabsKeyPoolConfig;
setWorldlabsKeyPoolConfig: Dispatch<SetStateAction<WorldLabsKeyPoolConfig>>;
worldlabsApiKeysDraft: string;
setWorldlabsApiKeysDraft: Dispatch<SetStateAction<string>>;
worldlabsProviders: AdminAiProvider[];
filteredWorldlabsProviders: AdminAiProvider[];
enabledWorldlabsProviderCount: number;
worldlabsProviderSearch: string;
setWorldlabsProviderSearch: Dispatch<SetStateAction<string>>;
worldlabsProviderBusyId: string | null;
threeDModelAliasConfig: ThreeDModelAliasConfig;
setThreeDModelAliasConfig: Dispatch<SetStateAction<ThreeDModelAliasConfig>>;
kittycadCadConfig: AiProviderSettingsConfig;
setKittycadCadConfig: Dispatch<SetStateAction<AiProviderSettingsConfig>>;
isTestingKittycadCad: boolean;
getProviderDefaultBaseUrl: (type?: string) => string;
handleEditTripoPoolProvider: (provider: AdminAiProvider) => void;
handleToggleTripoPoolProvider: (
provider: AdminAiProvider,
checked: boolean
) => void | Promise<void>;
handleDeleteProvider: (id: string) => void | Promise<void>;
handleSaveTripo3dConfig: AsyncAction;
handleAddTripo3dPoolKey: AsyncAction;
handleImportWorldlabs3dPoolKeys: AsyncAction;
handleAddWorldlabs3dPoolKey: AsyncAction;
handleEditWorldlabsPoolProvider: (provider: AdminAiProvider) => void;
handleToggleWorldlabsPoolProvider: (
provider: AdminAiProvider,
checked: boolean
) => void | Promise<void>;
handleSaveWorldlabs3dConfig: AsyncAction;
handleAddThreeDModelAliasEntry: (
group: "tripoModels" | "worldModels"
) => void;
handleThreeDModelAliasEntryChange: (
group: "tripoModels" | "worldModels",
index: number,
field: keyof ThreeDModelAliasConfig["tripoModels"][number],
value: ThreeDModelAliasConfig["tripoModels"][number][keyof ThreeDModelAliasConfig["tripoModels"][number]]
) => void;
handleRemoveThreeDModelAliasEntry: (
group: "tripoModels" | "worldModels",
index: number
) => void;
handleResetThreeDModelAliasConfig: () => void;
handleSaveThreeDModelAliasConfig: AsyncAction;
handleTestKittycadCadConfig: AsyncAction;
handleSaveKittycadCadConfig: AsyncAction;
};
export type EnhanceProKeyRow = {
id: string;
name: string;
maskedKey: string;
index?: number;
};
export type RmbgConfig = {
provider: "vip98k" | "rmbg" | "photoroom" | "aoscdn";
apiUrl: string;
apiKey: string;
photoroomApiKey: string;
aosApiKey: string;
aosApiBaseUrl: string;
aosForegroundType: "auto" | "person" | "object" | "stamp";
aosCrop: boolean;
aosFormat: "png" | "jpg";
};
export type EnhanceConfig = {
provider: "vip98k" | "legacy";
apiUrl: string;
apiKey: string;
type: string;
scaleFactor: string;
useBackupApi: boolean;
backupApiUrl: string;
backupApiKey: string;
backupScale: string;
backupModel: string;
};
export type ExtendConfig = {
provider: "vip98k" | "dashscope";
apiKey: string;
xScale: string;
yScale: string;
};
export type WatermarkRemoveConfig = {
apiKey: string;
};
export type AngleEditConfig = {
enabled: boolean;
falApiKey: string;
loraScale: number;
inferenceSteps: number;
guidanceScale: number;
maxImageSize: number;
timeout: number;
};
export type LayerSplitConfig = {
enabled: boolean;
provider: "replicate";
replicateApiToken: string;
replicateModel: string;
layers: number;
resolution: number;
trueCfgScale: number;
numInferenceSteps: number;
cfgNormalize: boolean;
useEnPrompt: boolean;
negativePrompt: string;
timeoutMs: number;
};
export type LocalAiImagePipelineSectionsProps = LocalAiSectionControlProps & {
modelsData?: AdminModelsData;
rmbgConfig: RmbgConfig;
setRmbgConfig: Dispatch<SetStateAction<RmbgConfig>>;
enhanceConfig: EnhanceConfig;
setEnhanceConfig: Dispatch<SetStateAction<EnhanceConfig>>;
enhanceProConfig: EnhanceProKreaConfig;
setEnhanceProConfig: Dispatch<SetStateAction<EnhanceProKreaConfig>>;
enhanceProApiKeysDraft: string;
setEnhanceProApiKeysDraft: Dispatch<SetStateAction<string>>;
enhanceProApiKeysDraftCount: number;
draftEnhanceProKeyRows: EnhanceProKeyRow[];
savedEnhanceProKeyRows: EnhanceProKeyRow[];
enhanceProPresetEditorMode: "simple" | "advanced";
setEnhanceProPresetEditorMode: Dispatch<
SetStateAction<"simple" | "advanced">
>;
selectedEnhanceProPresetId: string;
setSelectedEnhanceProPresetId: Dispatch<SetStateAction<string>>;
selectedEnhanceProPreset: EnhanceProPreset | null | undefined;
selectedEnhanceProPresetIndex: number;
draggingEnhanceProPresetId: string;
aiWashConfig: AiWashConfig;
setAiWashConfig: Dispatch<SetStateAction<AiWashConfig>>;
aiWashModeIconUploading: boolean;
canvasLocalEditConfig: CanvasLocalEditConfig;
setCanvasLocalEditConfig: Dispatch<SetStateAction<CanvasLocalEditConfig>>;
extendConfig: ExtendConfig;
setExtendConfig: Dispatch<SetStateAction<ExtendConfig>>;
watermarkRemoveConfig: WatermarkRemoveConfig;
setWatermarkRemoveConfig: Dispatch<SetStateAction<WatermarkRemoveConfig>>;
angleEditConfig: AngleEditConfig;
setAngleEditConfig: Dispatch<SetStateAction<AngleEditConfig>>;
pointsSystemConfig: PointsSystemConfig;
updatePointsCostField: (
key: keyof PointsSystemConfig,
value: unknown
) => void;
layerSplitConfig: LayerSplitConfig;
setLayerSplitConfig: Dispatch<SetStateAction<LayerSplitConfig>>;
renderEnhanceProPresetEditor: (
preset: EnhanceProPreset,
index: number,
expanded?: boolean
) => ReactNode;
getEnhanceProPresetSummary: (preset: EnhanceProPreset) => string;
handleSaveRmbgConfig: AsyncAction;
handleSaveEnhanceConfig: AsyncAction;
handleSaveEnhanceProConfig: AsyncAction;
handleAddEnhanceProPoolKey: () => void;
handleDeleteEnhanceProPoolKey: (index: number) => void;
handleToggleEnhanceProModelVisible: (
model: EnhanceProKreaModel,
visible: boolean
) => void;
handleAddEnhanceProPreset: () => void;
handleResetEnhanceProPresets: () => void;
handleEnhanceProPresetDragStart: (
event: DragEvent<HTMLDivElement>,
presetId: string
) => void;
handleEnhanceProPresetDragEnd: () => void;
handleEnhanceProPresetDrop: (
event: DragEvent<HTMLDivElement>,
targetPresetId: string
) => void;
handleMoveEnhanceProPreset: (fromIndex: number, toIndex: number) => void;
handleAiWashModeIconUpload: (file: File) => Promise<void>;
handleSaveAiWashConfig: AsyncAction;
handleSaveCanvasLocalEditConfig: AsyncAction;
handleSaveExtendConfig: AsyncAction;
handleSaveWatermarkRemoveConfig: AsyncAction;
handleSaveAngleEditConfig: AsyncAction;
handleSaveLayerSplitConfig: AsyncAction;
};
export type ThirdPartySqlServerConfig = {
enabled: boolean;
host: string;
port: number;
database: string;
username: string;
password: string;
encrypt: boolean;
trustServerCertificate: boolean;
connectionTimeoutMs: number;
requestTimeoutMs: number;
};
export type RealtimeCanvasProviderType = "bfl" | "replicate" | "fal";
export type RealtimeCanvasConfig = {
enabled: boolean;
providerType: RealtimeCanvasProviderType;
bflApiKey: string;
bflBaseUrl: string;
bflModel: string;
falApiKey: string;
falBaseUrl: string;
falModel: string;
replicateApiToken: string;
replicateModel: string;
fallbackEnabled: boolean;
fallbackProviderType: RealtimeCanvasProviderType;
fallbackBflApiKey: string;
fallbackBflBaseUrl: string;
fallbackBflModel: string;
fallbackFalApiKey: string;
fallbackFalBaseUrl: string;
fallbackFalModel: string;
fallbackReplicateApiToken: string;
fallbackReplicateModel: string;
renderMode: "strict-structure" | "balanced" | "stylize";
throttleInterval: number;
maxBuffering: number;
maxInputResolution: number;
jpegQuality: number;
previewMegapixels: number;
detailMegapixels: number;
outputJpegQuality: number;
steps: number;
scheduleMu: number;
feedback: number;
imageSize: "square" | "square_hd";
};
export type SceneFusionConfig = {
enabled: boolean;
preRmbgEnabled: boolean;
defaultPrompt: string;
maxScenes: number;
modelId: string;
};
export type LocalAiRuntimeSectionsProps = LocalAiSectionControlProps & {
thirdPartySqlServerConfig: ThirdPartySqlServerConfig;
setThirdPartySqlServerConfig: Dispatch<
SetStateAction<ThirdPartySqlServerConfig>
>;
isThirdPartySqlServerFieldsVisible: boolean;
setIsThirdPartySqlServerFieldsVisible: Dispatch<SetStateAction<boolean>>;
isTestingThirdPartySqlServer: boolean;
realtimeCanvasConfig: RealtimeCanvasConfig;
setRealtimeCanvasConfig: Dispatch<SetStateAction<RealtimeCanvasConfig>>;
canvasImageTaskQueueConfig: CanvasImageTaskQueueConfig;
setCanvasImageTaskQueueConfig: Dispatch<
SetStateAction<CanvasImageTaskQueueConfig>
>;
sceneFusionConfig: SceneFusionConfig;
setSceneFusionConfig: Dispatch<SetStateAction<SceneFusionConfig>>;
hasThirdPartySqlServerConfiguredValue: boolean;
modelsData?: AdminModelsData;
providersData?: AdminProvidersData;
DEFAULT_BFL_BASE_URL: string;
DEFAULT_BFL_REALTIME_MODEL: string;
handleSaveThirdPartySqlServerConfig: AsyncAction;
handleTestThirdPartySqlServerConfig: AsyncAction;
handleSaveRealtimeCanvasConfig: AsyncAction;
handleSaveCanvasImageTaskQueueConfig: AsyncAction;
handleSaveSceneFusionConfig: AsyncAction;
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
import { requireAgentWorkflowAdmin } from "@/lib/middleware/admin";
import { redirect } from "next/navigation";
import { AgentWorkflowSettingsClient } from "./agent-workflow-settings-client";
export const dynamic = "force-dynamic";
export default async function AgentWorkflowSettingsPage() {
const session = await requireAgentWorkflowAdmin().catch(() => null);
if (!session) {
redirect("/");
}
return (
<AgentWorkflowSettingsClient
backHref={session.user.type === "admin" ? "/settings" : "/"}
backLabel={session.user.type === "admin" ? "返回设置" : "返回首页"}
/>
);
}

View File

@ -0,0 +1,19 @@
import { requireAgentWorkflowAdmin } from "@/lib/middleware/admin";
import { redirect } from "next/navigation";
import { PsdTemplatesSettingsClient } from "./psd-templates-settings-client";
export const dynamic = "force-dynamic";
export default async function AgentWorkflowPsdTemplatesPage() {
const session = await requireAgentWorkflowAdmin().catch(() => null);
if (!session) {
redirect("/");
}
return (
<PsdTemplatesSettingsClient
backHref="/settings/agent-workflow"
backLabel="返回风格化工作流配置"
/>
);
}

View File

@ -1,3 +1,4 @@
import { auth } from "@/app/(auth)/auth";
import { requireAdmin } from "@/lib/middleware/admin";
import { redirect } from "next/navigation";
import { AdminSettingsClient } from "./admin-settings-client";
@ -6,6 +7,11 @@ import { AdminSettingsClient } from "./admin-settings-client";
export const dynamic = "force-dynamic";
export default async function AdminSettingsPage() {
const session = await auth().catch(() => null);
if (session?.user?.type === "agent_admin") {
redirect("/settings/agent-workflow");
}
try {
await requireAdmin();
} catch {
@ -14,4 +20,3 @@ export default async function AdminSettingsPage() {
return <AdminSettingsClient />;
}

View File

@ -0,0 +1,16 @@
import { redirect } from "next/navigation";
import { requireAdmin } from "@/lib/middleware/admin";
import { StyleProSettingsClient } from "./style-pro-settings-client";
export const dynamic = "force-dynamic";
export default async function StyleProSettingsPage() {
try {
await requireAdmin();
} catch {
redirect("/");
}
return <StyleProSettingsClient />;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
import { redirect } from "next/navigation";
import { requireAdmin } from "@/lib/middleware/admin";
import { VideoCanvasSettingsClient } from "./video-canvas-settings-client";
export const dynamic = "force-dynamic";
export default async function VideoCanvasSettingsPage() {
try {
await requireAdmin();
} catch {
redirect("/");
}
return <VideoCanvasSettingsClient />;
}

View File

@ -0,0 +1,887 @@
"use client";
import Link from "next/link";
import { ArrowLeft, Plus, Trash2 } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import useSWR from "swr";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import {
isImageGenerationModelDescriptor,
isVideoModelDescriptor,
} from "@/lib/ai/model-classification";
import { fetcher } from "@/lib/utils";
import {
getVideoCanvasFunctionBillingCopy,
type VideoCanvasFunctionConfig,
type VideoCanvasFunctionDefinition,
type VideoCanvasEffectPreset,
type VideoCanvasMotionPreset,
type VideoCanvasRolePreset,
VIDEO_CANVAS_FUNCTION_CONFIG_KEY,
defaultVideoCanvasFunctionConfig,
normalizeVideoCanvasFunctionConfig,
} from "@/lib/video-canvas/config";
type SettingsResponse = {
setting?: {
value?: unknown;
};
};
type AiModelOption = {
id: string;
modelId: string;
displayName: string;
providerName?: string;
providerType?: string;
supportsVision?: boolean;
supportsImageGen?: boolean;
isEnabled?: boolean;
isHidden?: boolean;
isMultimodal?: boolean;
capabilities?: unknown;
};
type AiModelsResponse = {
models?: AiModelOption[];
};
const INHERIT_MODEL_VALUE = "__inherit__";
const functionKindLabels: Record<VideoCanvasFunctionDefinition["kind"], string> = {
audio: "音频",
image: "图片生成",
"image-edit": "图片编辑",
multimodal: "多模态",
utility: "工具",
video: "视频生成",
workflow: "工作流",
};
function getModelOptionValue(model: AiModelOption) {
return String(model.modelId || model.id || "").trim();
}
function getModelOptionLabel(model: AiModelOption) {
const name = model.displayName || model.modelId || model.id;
const id = model.modelId && model.modelId !== name ? ` · ${model.modelId}` : "";
return `${name}${id}`;
}
function isImageModelOption(model: AiModelOption) {
return isImageGenerationModelDescriptor({
providerType: model.providerType,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
});
}
function isVideoModelOption(model: AiModelOption) {
return isVideoModelDescriptor({
providerType: model.providerType,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
});
}
function filterModelsForFunctionKind(
models: AiModelOption[],
kind: VideoCanvasFunctionDefinition["kind"],
) {
const enabled = models.filter((model) => model.isEnabled !== false && getModelOptionValue(model));
let filtered = enabled;
if (kind === "image" || kind === "image-edit") {
filtered = enabled.filter(isImageModelOption);
} else if (kind === "video") {
filtered = enabled.filter(isVideoModelOption);
} else if (kind === "multimodal") {
filtered = enabled.filter(
(model) => model.isMultimodal || model.supportsVision || (!isImageModelOption(model) && !isVideoModelOption(model)),
);
}
return filtered.length ? filtered : enabled;
}
function ModelSelect({
allowInherit = false,
kind,
models,
onChange,
value,
}: {
allowInherit?: boolean;
kind: VideoCanvasFunctionDefinition["kind"];
models: AiModelOption[];
onChange: (value: string) => void;
value: string;
}) {
const filteredModels = filterModelsForFunctionKind(models, kind);
const normalizedValue = value?.trim() || (allowInherit ? INHERIT_MODEL_VALUE : "auto");
const hasCurrent =
normalizedValue === "auto" ||
normalizedValue === INHERIT_MODEL_VALUE ||
filteredModels.some((model) => getModelOptionValue(model) === normalizedValue);
return (
<Select
onValueChange={(nextValue) => {
if (nextValue === INHERIT_MODEL_VALUE) {
onChange("");
return;
}
onChange(nextValue);
}}
value={normalizedValue}
>
<SelectTrigger>
<SelectValue placeholder="选择模型" />
</SelectTrigger>
<SelectContent>
{allowInherit ? (
<SelectItem value={INHERIT_MODEL_VALUE}>继承全局默认模型</SelectItem>
) : (
<SelectItem value="auto">自动选择可用模型</SelectItem>
)}
{!hasCurrent && normalizedValue ? (
<SelectItem value={normalizedValue}>当前保存值:{normalizedValue}</SelectItem>
) : null}
{filteredModels.map((model) => {
const optionValue = getModelOptionValue(model);
return (
<SelectItem key={`${model.id}-${optionValue}`} value={optionValue}>
{getModelOptionLabel(model)}
</SelectItem>
);
})}
{filteredModels.length === 0 ? (
<SelectItem disabled value="__no_models__">
暂无已启用模型
</SelectItem>
) : null}
</SelectContent>
</Select>
);
}
export function VideoCanvasSettingsClient() {
const { data, isLoading, mutate } = useSWR<SettingsResponse>(
`/api/admin/settings?key=${VIDEO_CANVAS_FUNCTION_CONFIG_KEY}`,
fetcher,
);
const { data: aiModelsData } = useSWR<AiModelsResponse>("/api/admin/ai-models", fetcher);
const [config, setConfig] = useState<VideoCanvasFunctionConfig>(defaultVideoCanvasFunctionConfig);
const [saving, setSaving] = useState(false);
const modelOptions = useMemo(
() => (aiModelsData?.models || []).filter((model) => model.isEnabled !== false),
[aiModelsData?.models],
);
useEffect(() => {
if (!data) return;
setConfig(normalizeVideoCanvasFunctionConfig(data.setting?.value));
}, [data]);
const functions = useMemo(
() =>
Object.values(config.functions).sort((a, b) => {
const compare = (left: string, right: string) =>
left === right ? 0 : left < right ? -1 : 1;
return compare(a.kind, b.kind) || compare(a.label, b.label);
}),
[config.functions],
);
const updateRoot = (updates: Partial<VideoCanvasFunctionConfig>) => {
setConfig((current) => ({ ...current, ...updates }));
};
const updateFunction = (
key: string,
updates: Partial<VideoCanvasFunctionDefinition>,
) => {
setConfig((current) => ({
...current,
functions: {
...current.functions,
[key]: {
...current.functions[key],
...updates,
},
},
}));
};
const updateEffectPreset = (
index: number,
updates: Partial<VideoCanvasEffectPreset>,
) => {
setConfig((current) => ({
...current,
effectPresets: current.effectPresets.map((preset, presetIndex) =>
presetIndex === index ? { ...preset, ...updates } : preset,
),
}));
};
const updateMotionPreset = (
index: number,
updates: Partial<VideoCanvasMotionPreset>,
) => {
setConfig((current) => ({
...current,
motionPresets: current.motionPresets.map((preset, presetIndex) =>
presetIndex === index ? { ...preset, ...updates } : preset,
),
}));
};
const addMotionPreset = () => {
const createdAt = Date.now();
setConfig((current) => ({
...current,
motionPresets: [
...current.motionPresets,
{
id: `motion-${createdAt}`,
name: "新运镜预设",
previewUrl: "/video-canvas/effects/multi-angle.webp",
prompt: "镜头平稳移动,保持主体和空间关系清晰。",
},
],
}));
};
const removeMotionPreset = (index: number) => {
setConfig((current) => ({
...current,
motionPresets: current.motionPresets.filter(
(_, presetIndex) => presetIndex !== index,
),
}));
};
const addEffectPreset = () => {
setConfig((current) => ({
...current,
effectPresets: [
...current.effectPresets,
{
id: `effect-${Date.now()}`,
title: "新视频特效",
author: "官方预设",
usageCount: 0,
model: "Seedance 2.0",
thumbnailUrl: "/video-canvas/effects/product-light-sweep.webp",
prompt: "保持参考主体稳定,应用自然、连续的视频特效和镜头运动。",
commercial: true,
},
],
}));
};
const removeEffectPreset = (index: number) => {
setConfig((current) => ({
...current,
effectPresets: current.effectPresets.filter((_, presetIndex) => presetIndex !== index),
}));
};
const updateRolePreset = (
index: number,
updates: Partial<VideoCanvasRolePreset>,
) => {
setConfig((current) => ({
...current,
rolePresets: current.rolePresets.map((preset, presetIndex) =>
presetIndex === index ? { ...preset, ...updates } : preset,
),
}));
};
const addRolePreset = () => {
const createdAt = Date.now();
setConfig((current) => ({
...current,
rolePresets: [
...current.rolePresets,
{
id: `role-${createdAt}`,
assetId: `public-role:role-${createdAt}`,
name: "新角色预设",
accent: "#525252",
description: "管理员配置的公共角色素材。",
prompt: "保持角色身份、五官、发型、服装和体型在所有视频镜头中一致。",
tags: "角色",
compliant: true,
images: {
expression: "",
face: "",
full: "/video-canvas/effects/product-light-sweep.webp",
threeView: "",
},
filters: {
age: "青年",
build: "匀称",
culture: "东亚",
era: "现代",
gender: "中性",
hairColor: "黑发",
species: "人类",
},
},
],
}));
};
const removeRolePreset = (index: number) => {
setConfig((current) => ({
...current,
rolePresets: current.rolePresets.filter((_, presetIndex) => presetIndex !== index),
}));
};
const save = async () => {
setSaving(true);
try {
const response = await fetch("/api/admin/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
key: VIDEO_CANVAS_FUNCTION_CONFIG_KEY,
value: config,
}),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(payload?.message || payload?.error || "保存失败");
}
toast.success("视频画布功能配置已保存");
await mutate();
} catch (error) {
toast.error(error instanceof Error ? error.message : "保存失败");
} finally {
setSaving(false);
}
};
const resetToDefault = () => {
setConfig(defaultVideoCanvasFunctionConfig);
toast.info("已恢复默认配置,保存后生效");
};
return (
<main className="settings-workspace mx-auto flex w-full max-w-7xl flex-col gap-4 px-4 py-5 sm:px-5">
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div>
<Button asChild className="mb-2" size="sm" variant="outline">
<Link href="/settings">
<ArrowLeft className="mr-1.5 h-3.5 w-3.5" />
返回设置
</Link>
</Button>
<h1 className="text-xl font-semibold">视频画布功能设置</h1>
<p className="mt-1 max-w-3xl text-xs leading-5 text-muted-foreground">
这里集中配置复刻画布里的工具能力、预设提示词和默认模型。积分与扣费统一跟随全站模型/功能配置,不再在视频画布里单独维护一套价格。
</p>
</div>
<div className="flex gap-2">
<Button disabled={saving || isLoading} onClick={resetToDefault} type="button" variant="outline">
恢复默认
</Button>
<Button disabled={saving || isLoading} onClick={save} type="button">
{saving ? "保存中..." : "保存配置"}
</Button>
</div>
</div>
<Card>
<CardHeader className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div>
<CardTitle className="text-xl">全局模型与开关</CardTitle>
<CardDescription>
默认模型从后台已启用模型中选择;保持自动时由各接口按当前可用模型选择。
</CardDescription>
</div>
<div className="flex items-center gap-3">
<Label htmlFor="video-canvas-enabled">启用视频画布功能串联</Label>
<Switch
checked={config.enabled}
id="video-canvas-enabled"
onCheckedChange={(checked) => updateRoot({ enabled: checked })}
/>
</div>
</CardHeader>
<CardContent className="grid gap-4 md:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="defaultImageModel">默认图片/编辑模型</Label>
<ModelSelect
kind="image-edit"
models={modelOptions}
onChange={(value) => updateRoot({ defaultImageModel: value || "auto" })}
value={config.defaultImageModel}
/>
</div>
<div className="space-y-2">
<Label htmlFor="defaultVideoModel">默认视频模型</Label>
<ModelSelect
kind="video"
models={modelOptions}
onChange={(value) => updateRoot({ defaultVideoModel: value || "auto" })}
value={config.defaultVideoModel}
/>
</div>
<div className="space-y-2">
<Label htmlFor="defaultMultimodalModel">默认多模态模型</Label>
<ModelSelect
kind="multimodal"
models={modelOptions}
onChange={(value) => updateRoot({ defaultMultimodalModel: value || "auto" })}
value={config.defaultMultimodalModel}
/>
</div>
</CardContent>
</Card>
<section className="space-y-4">
<div className="flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
<div>
<h2 className="text-lg font-semibold tracking-tight">视频运镜素材预设</h2>
<p className="mt-1 max-w-3xl text-sm text-muted-foreground">
视频节点“运镜广场”的名称、预览图和插入提示词都从这里读取。点击运镜卡会按竞品规则把提示词追加到用户输入框,允许组合和重复使用。
</p>
</div>
<Button onClick={addMotionPreset} size="sm" type="button" variant="outline">
<Plus className="mr-1.5 h-4 w-4" />
添加运镜
</Button>
</div>
<div className="grid gap-3">
{config.motionPresets.map((preset, index) => (
<details
className="group rounded-lg border border-border bg-card px-4 py-3"
key={`${preset.id}-${index}`}
>
<summary className="flex cursor-pointer list-none items-center gap-3">
<img
alt=""
className="h-14 w-14 shrink-0 rounded-md object-cover"
src={preset.previewUrl}
/>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium">{preset.name}</div>
<div className="mt-1 truncate text-xs text-muted-foreground">
{preset.prompt || "未设置插入提示词"}
</div>
</div>
<Button
aria-label={`删除运镜 ${preset.name}`}
className="shrink-0"
onClick={(event) => {
event.preventDefault();
removeMotionPreset(index);
}}
size="icon"
title={`删除运镜 ${preset.name}`}
type="button"
variant="ghost"
>
<Trash2 className="h-4 w-4" />
</Button>
</summary>
<div className="mt-4 grid gap-4 border-t border-border pt-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`motion-name-${index}`}>运镜名称</Label>
<Input
id={`motion-name-${index}`}
onChange={(event) =>
updateMotionPreset(index, { name: event.target.value })
}
value={preset.name}
/>
</div>
<div className="space-y-2">
<Label htmlFor={`motion-preview-${index}`}>预览图地址</Label>
<Input
id={`motion-preview-${index}`}
onChange={(event) =>
updateMotionPreset(index, { previewUrl: event.target.value })
}
value={preset.previewUrl}
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor={`motion-prompt-${index}`}>插入提示词</Label>
<Textarea
id={`motion-prompt-${index}`}
onChange={(event) =>
updateMotionPreset(index, { prompt: event.target.value })
}
value={preset.prompt}
/>
</div>
</div>
</details>
))}
</div>
</section>
<section className="space-y-4">
<div className="flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
<div>
<h2 className="text-lg font-semibold tracking-tight">视频特效素材预设</h2>
<p className="mt-1 max-w-3xl text-sm text-muted-foreground">
视频节点“特效”面板、素材节点封面、模型约束和生成提示词都从这里读取。选择特效时会创建结构化素材节点,不会把描述硬写进用户提示词。
</p>
</div>
<Button onClick={addEffectPreset} size="sm" type="button" variant="outline">
<Plus className="mr-1.5 h-4 w-4" />
添加特效
</Button>
</div>
<div className="grid gap-3">
{config.effectPresets.map((preset, index) => (
<details
className="group rounded-lg border border-border bg-card px-4 py-3"
key={`${preset.id}-${index}`}
>
<summary className="flex cursor-pointer list-none items-center gap-3">
<img
alt=""
className="h-12 w-12 shrink-0 rounded-md object-cover"
src={preset.thumbnailUrl}
/>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium">{preset.title}</div>
<div className="mt-1 truncate text-xs text-muted-foreground">
{preset.author} · {preset.model} · {preset.usageCount.toLocaleString()} 次使用
</div>
</div>
<Button
aria-label={`删除特效 ${preset.title}`}
className="shrink-0"
onClick={(event) => {
event.preventDefault();
removeEffectPreset(index);
}}
size="icon"
title={`删除特效 ${preset.title}`}
type="button"
variant="ghost"
>
<Trash2 className="h-4 w-4" />
</Button>
</summary>
<div className="mt-4 grid gap-4 border-t border-border pt-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`effect-title-${index}`}>特效名称</Label>
<Input
id={`effect-title-${index}`}
onChange={(event) => updateEffectPreset(index, { title: event.target.value })}
value={preset.title}
/>
</div>
<div className="space-y-2">
<Label htmlFor={`effect-author-${index}`}>作者</Label>
<Input
id={`effect-author-${index}`}
onChange={(event) => updateEffectPreset(index, { author: event.target.value })}
value={preset.author}
/>
</div>
<div className="space-y-2">
<Label htmlFor={`effect-model-${index}`}>适用模型</Label>
<Input
id={`effect-model-${index}`}
onChange={(event) => updateEffectPreset(index, { model: event.target.value })}
value={preset.model}
/>
</div>
<div className="space-y-2">
<Label htmlFor={`effect-count-${index}`}>展示使用次数</Label>
<Input
id={`effect-count-${index}`}
min={0}
onChange={(event) =>
updateEffectPreset(index, {
usageCount: Math.max(0, Number(event.target.value) || 0),
})
}
type="number"
value={preset.usageCount}
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor={`effect-thumbnail-${index}`}>封面地址</Label>
<Input
id={`effect-thumbnail-${index}`}
onChange={(event) =>
updateEffectPreset(index, { thumbnailUrl: event.target.value })
}
value={preset.thumbnailUrl}
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor={`effect-prompt-${index}`}>生成提示词 / 后端规则</Label>
<Textarea
id={`effect-prompt-${index}`}
onChange={(event) => updateEffectPreset(index, { prompt: event.target.value })}
value={preset.prompt}
/>
</div>
<div className="flex items-center gap-3 md:col-span-2">
<Switch
checked={preset.commercial}
id={`effect-commercial-${index}`}
onCheckedChange={(checked) => updateEffectPreset(index, { commercial: checked })}
/>
<Label htmlFor={`effect-commercial-${index}`}>标记为可商用</Label>
</div>
</div>
</details>
))}
</div>
</section>
<section className="space-y-4">
<div className="flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
<div>
<h2 className="text-lg font-semibold tracking-tight">视频角色素材预设</h2>
<p className="mt-1 max-w-3xl text-sm text-muted-foreground">
视频节点“角色库”的公共角色、Seedance 合规资产 ID、四类角色图和隐藏生成提示词都从这里读取。角色引用通过画布连线保存,不会改写用户输入框。
</p>
</div>
<Button onClick={addRolePreset} size="sm" type="button" variant="outline">
<Plus className="mr-1.5 h-4 w-4" />
添加角色
</Button>
</div>
<div className="grid gap-3">
{config.rolePresets.map((preset, index) => (
<details
className="group rounded-lg border border-border bg-card px-4 py-3"
key={`${preset.id}-${index}`}
>
<summary className="flex cursor-pointer list-none items-center gap-3">
<img
alt=""
className="h-14 w-11 shrink-0 rounded-md object-cover object-top"
src={preset.images.full}
/>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium">{preset.name}</div>
<div className="mt-1 truncate text-xs text-muted-foreground">
{preset.assetId} · {preset.tags || "未设置标签"}
</div>
</div>
<Button
aria-label={`删除角色 ${preset.name}`}
className="shrink-0"
onClick={(event) => {
event.preventDefault();
removeRolePreset(index);
}}
size="icon"
title={`删除角色 ${preset.name}`}
type="button"
variant="ghost"
>
<Trash2 className="h-4 w-4" />
</Button>
</summary>
<div className="mt-4 grid gap-4 border-t border-border pt-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor={`role-name-${index}`}>角色名称</Label>
<Input
id={`role-name-${index}`}
onChange={(event) => updateRolePreset(index, { name: event.target.value })}
value={preset.name}
/>
</div>
<div className="space-y-2">
<Label htmlFor={`role-asset-${index}`}>合规资产 ID</Label>
<Input
id={`role-asset-${index}`}
onChange={(event) => updateRolePreset(index, { assetId: event.target.value })}
value={preset.assetId}
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor={`role-tags-${index}`}>展示标签</Label>
<Input
id={`role-tags-${index}`}
onChange={(event) => updateRolePreset(index, { tags: event.target.value })}
value={preset.tags}
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor={`role-description-${index}`}>角色说明</Label>
<Textarea
id={`role-description-${index}`}
onChange={(event) => updateRolePreset(index, { description: event.target.value })}
value={preset.description}
/>
</div>
<div className="space-y-2 md:col-span-2">
<Label htmlFor={`role-prompt-${index}`}>生成提示词 / 身份一致性规则</Label>
<Textarea
id={`role-prompt-${index}`}
onChange={(event) => updateRolePreset(index, { prompt: event.target.value })}
value={preset.prompt}
/>
</div>
{([
["full", "角色立绘"],
["face", "脸部近景"],
["expression", "表情参考"],
["threeView", "三视图"],
] as const).map(([key, label]) => (
<div className="space-y-2" key={key}>
<Label htmlFor={`role-${key}-${index}`}>{label}地址</Label>
<Input
id={`role-${key}-${index}`}
onChange={(event) =>
updateRolePreset(index, {
images: { ...preset.images, [key]: event.target.value },
})
}
value={preset.images[key]}
/>
</div>
))}
{([
["gender", "性别"],
["age", "年龄段"],
["species", "种族"],
["era", "时代"],
["culture", "文化区域"],
["build", "体型"],
["hairColor", "发色"],
] as const).map(([key, label]) => (
<div className="space-y-2" key={key}>
<Label htmlFor={`role-filter-${key}-${index}`}>{label}</Label>
<Input
id={`role-filter-${key}-${index}`}
onChange={(event) =>
updateRolePreset(index, {
filters: { ...preset.filters, [key]: event.target.value },
})
}
value={preset.filters[key]}
/>
</div>
))}
<div className="flex items-center gap-3 md:col-span-2">
<Switch
checked={preset.compliant}
id={`role-compliant-${index}`}
onCheckedChange={(checked) => updateRolePreset(index, { compliant: checked })}
/>
<Label htmlFor={`role-compliant-${index}`}>可在 Seedance2.0 合规素材库中使用</Label>
</div>
</div>
</details>
))}
</div>
</section>
<section className="space-y-4">
<div>
<h2 className="text-lg font-semibold tracking-tight">功能预设提示词</h2>
<p className="mt-1 max-w-3xl text-sm text-muted-foreground">
视频画布运行时会通过 /api/video-canvas/config 读取这里保存的提示词。比如“多机位九宫格”的分镜规则、黑色分割线要求和镜头说明,都由管理员在这里维护,前端不再写死覆盖。
</p>
<p className="mt-2 max-w-3xl text-sm text-muted-foreground">
计费说明只展示来源,不在这里改价;需要调整积分请回到全站积分设置、模型设置或对应功能设置统一修改。
</p>
</div>
<div className="grid gap-4">
{functions.map((item) => (
<Card className={!item.enabled ? "opacity-70" : undefined} key={item.key}>
<CardHeader className="space-y-4">
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div>
<CardTitle className="flex flex-wrap items-center gap-2 text-base">
<span>{item.label}</span>
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-normal text-muted-foreground">
{functionKindLabels[item.kind]}
</span>
<span className="rounded-full bg-muted px-2 py-0.5 text-xs font-normal text-muted-foreground">
{item.route}
</span>
</CardTitle>
<CardDescription className="mt-2">{item.description}</CardDescription>
</div>
<div className="flex items-center gap-3">
<Label htmlFor={`enabled-${item.key}`}>启用</Label>
<Switch
checked={item.enabled}
id={`enabled-${item.key}`}
onCheckedChange={(checked) => updateFunction(item.key, { enabled: checked })}
/>
</div>
</div>
<div className="grid gap-4 md:grid-cols-1">
<div className="space-y-2">
<Label htmlFor={`model-${item.key}`}>模型覆盖</Label>
<ModelSelect
allowInherit
kind={item.kind}
models={modelOptions}
onChange={(value) => updateFunction(item.key, { model: value })}
value={item.model || ""}
/>
</div>
<div className="rounded-lg border border-dashed border-border/70 bg-muted/20 px-3 py-2">
<div className="text-xs font-medium text-foreground">
积分来源:{getVideoCanvasFunctionBillingCopy(item).shortLabel}
</div>
<div className="mt-1 text-xs leading-5 text-muted-foreground">
{getVideoCanvasFunctionBillingCopy(item).description}
</div>
</div>
</div>
</CardHeader>
<CardContent>
<Label htmlFor={`prompt-${item.key}`}>管理员可编辑的预设提示词 / 后端规则</Label>
<Textarea
className="mt-2 min-h-28"
id={`prompt-${item.key}`}
onChange={(event) => updateFunction(item.key, { prompt: event.target.value })}
value={item.prompt}
/>
</CardContent>
</Card>
))}
</div>
</section>
</main>
);
}

View File

@ -1,9 +1 @@
import { GET as AuthGET, POST as AuthPOST } from "@/app/(auth)/auth";
export const GET = async (req: Request) => {
return AuthGET(req);
};
export const POST = async (req: Request) => {
return AuthPOST(req);
};
export { GET, POST } from "@/app/(auth)/auth";

View File

@ -6,8 +6,14 @@ export function buildAuthConfig(authUrl?: string | null): NextAuthConfig {
// Use host-only cookies to avoid cross-domain login issues in multi-domain deployments.
const cookieDomain = undefined;
// trustHost 仅在明确配置时开启,避免 host header 注入攻击
// 部署在 Vercel 或受信反向代理后时,设置 AUTH_TRUST_HOST=true
const trustHost =
process.env.AUTH_TRUST_HOST === "true" ||
process.env.VERCEL === "1";
return {
trustHost: true,
trustHost,
pages: {
signIn: "/login",
newUser: "/",

View File

@ -10,8 +10,16 @@ import { buildAuthConfig } from "./auth.config";
import { db } from "@/lib/db";
import { loginToken, user as userTable } from "@/lib/db/schema";
import { eq, and, gt } from "drizzle-orm";
import {
establishSingleActiveSession,
getValidatedSessionUser,
isManagedSessionUserType,
requiresSingleActiveSession,
revokeSingleActiveSession,
} from "@/lib/auth/session-control";
import { recordAccessIpEventFromHeaders } from "@/lib/security/access-log";
export type UserType = "guest" | "regular" | "admin";
export type UserType = "guest" | "regular" | "admin" | "agent_admin";
declare module "next-auth" {
interface Session extends DefaultSession {
@ -19,10 +27,10 @@ declare module "next-auth" {
id: string;
type: UserType;
name?: string | null;
sessionKey?: string;
} & DefaultSession["user"];
}
// biome-ignore lint/nursery/useConsistentTypeDefinitions: "Required"
interface User {
id?: string;
email?: string | null;
@ -36,6 +44,7 @@ declare module "next-auth/jwt" {
id: string;
type: UserType;
name?: string | null;
sessionKey?: string;
}
}
@ -191,13 +200,11 @@ async function resolveRuntimeAuthUrl(settingsAuthUrl?: string | null) {
return `${protocol}://${resolvedHost}`;
}
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth(async () => {
const runtimeAuthSettings = await getAuthSettings().catch(() => null as any);
const nextAuth = NextAuth(async () => {
const runtimeAuthSettings =
process.env.CANVAS_E2E_LOCAL === "true"
? null
: await getAuthSettings().catch(() => null as any);
const resolvedAuthUrl =
(await resolveRuntimeAuthUrl(runtimeAuthSettings?.authUrl)) || undefined;
const googleClientId =
@ -207,7 +214,6 @@ export const {
return {
...buildAuthConfig(resolvedAuthUrl),
trustHost: true,
providers: createBaseProviders(googleClientId, googleClientSecret),
callbacks: {
async signIn({ account, user, profile }) {
@ -233,10 +239,19 @@ export const {
return Boolean(dbUser?.isActive);
},
async jwt({ token, user, account, profile }) {
let resolvedUserId =
typeof token.id === "string" && token.id.length > 0 ? token.id : undefined;
let resolvedUserType =
typeof token.type === "string" && isManagedSessionUserType(token.type)
? token.type
: undefined;
if (user?.id && user?.type) {
token.id = user.id as string;
token.type = user.type;
token.name = user.name;
resolvedUserId = token.id;
resolvedUserType = token.type;
}
if (account?.provider === "google") {
@ -257,10 +272,47 @@ export const {
token.type = (dbUser.role as UserType) || "regular";
token.name = dbUser.name;
token.email = dbUser.email;
resolvedUserId = dbUser.id;
resolvedUserType = (dbUser.role as UserType) || "regular";
}
}
}
if (account && resolvedUserId && requiresSingleActiveSession(resolvedUserType)) {
const { sessionKey } = await establishSingleActiveSession({
userId: resolvedUserId,
});
token.sessionKey = sessionKey;
} else if (!requiresSingleActiveSession(resolvedUserType)) {
delete token.sessionKey;
}
if (account && resolvedUserId) {
await recordAccessIpEventFromHeaders({
eventType: "login_success",
userId: resolvedUserId,
userEmail:
typeof token.email === "string"
? token.email
: typeof user?.email === "string"
? user.email
: null,
userRole: resolvedUserType || "regular",
method: "POST",
path: `/api/auth/${account.provider}`,
routeGroup: "auth",
statusCode: 200,
outcome: "login_success",
severity: "info",
reasons: ["登录成功"],
metadata: {
provider: account.provider,
},
}).catch((error) => {
console.warn("[Auth] Failed to record login access event:", error);
});
}
return token;
},
session({ session, token }) {
@ -268,6 +320,8 @@ export const {
session.user.id = token.id;
session.user.type = token.type || "regular";
session.user.name = token.name as string;
session.user.sessionKey =
typeof token.sessionKey === "string" ? token.sessionKey : undefined;
}
return session;
@ -275,3 +329,68 @@ export const {
},
};
});
const {
handlers: { GET, POST },
auth: rawAuth,
signIn,
signOut: rawSignOut,
} = nextAuth;
export { GET, POST, signIn };
export async function auth() {
const session = await rawAuth();
if (!session?.user?.id) {
return session;
}
if (!requiresSingleActiveSession(session.user.type)) {
return session;
}
const sessionKey = session.user.sessionKey;
if (!sessionKey) {
return null;
}
try {
const validatedUser = await getValidatedSessionUser({
userId: session.user.id,
sessionKey,
});
if (!validatedUser) {
return null;
}
return {
...session,
user: {
...session.user,
email: validatedUser.email,
name: validatedUser.name ?? session.user.name,
type: validatedUser.role,
sessionKey,
},
};
} catch (error) {
console.error("[Auth] Failed to validate active session:", error);
return null;
}
}
export async function signOut(options?: Parameters<typeof rawSignOut>[0]) {
const session = await auth().catch(() => null);
if (session?.user?.id && requiresSingleActiveSession(session.user.type)) {
await revokeSingleActiveSession({
userId: session.user.id,
sessionKey: session.user.sessionKey,
}).catch((error) => {
console.error("[Auth] Failed to revoke active session during sign out:", error);
});
}
return rawSignOut(options);
}

View File

@ -20,7 +20,9 @@ import { toast } from "@/components/toast";
import { type LoginActionState, login } from "../actions";
import { Button } from "@/components/ui/button";
import { AntigravityBackground } from "@/components/antigravity-background";
import { createClientLogger } from "@/lib/observability/client-logger";
const logger = createClientLogger("auth-login");
export default function Page() {
const [email, setEmail] = useState("");
const [isSuccessful, setIsSuccessful] = useState(false);
@ -39,6 +41,7 @@ export default function Page() {
const { setTheme, resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
const [isHandlingToken, setIsHandlingToken] = useState(false);
const [isCheckingExistingSession, setIsCheckingExistingSession] = useState(true);
// 系统通知状态
const [notification, setNotification] = useState<{ enabled: boolean; title: string; content: string } | null>(null);
@ -56,16 +59,43 @@ export default function Page() {
}
);
useEffect(() => {
let cancelled = false;
fetch("/api/auth/validate-session", {
cache: "no-store",
credentials: "include",
})
.then((res) => res.json())
.then((data) => {
if (cancelled) return;
if (data?.valid === true && data?.userType !== "guest") {
window.location.replace("/");
return;
}
setIsCheckingExistingSession(false);
})
.catch(() => {
if (!cancelled) {
setIsCheckingExistingSession(false);
}
});
return () => {
cancelled = true;
};
}, []);
// 使用 token 完成钉钉登录
const completeDingTalkLogin = useCallback(async (token: string) => {
console.log("[DingTalk] Completing login with token:", token.substring(0, 10) + "...");
logger.debug("[DingTalk] Completing login with token", { hasToken: Boolean(token) });
setIsHandlingToken(true);
try {
// 获取 CSRF token
const csrfRes = await fetch("/api/auth/csrf");
const { csrfToken } = await csrfRes.json();
console.log("[DingTalk] Got CSRF token");
logger.debug("[DingTalk] CSRF token resolved");
// 手动构造登录请求
const loginRes = await fetch("/api/auth/callback/credentials", {
@ -81,16 +111,17 @@ export default function Page() {
credentials: "include", // 重要:确保 cookie 被发送和接收
});
console.log("[DingTalk] Login response status:", loginRes.status);
console.log("[DingTalk] Login response headers:", Object.fromEntries(loginRes.headers.entries()));
// 检查响应类型
const contentType = loginRes.headers.get("content-type");
console.log("[DingTalk] Content-Type:", contentType);
logger.debug("[DingTalk] Login response received", {
status: loginRes.status,
redirected: loginRes.redirected,
contentType,
});
if (loginRes.redirected || loginRes.status === 302) {
// 登录成功,被重定向
console.log("[DingTalk] Login successful (redirected to:", loginRes.url, ")");
logger.debug("[DingTalk] Login redirected successfully");
// 等待一小段时间确保 cookie 被设置
await new Promise(resolve => setTimeout(resolve, 300));
@ -98,10 +129,12 @@ export default function Page() {
// 验证 session 是否创建成功
const sessionRes = await fetch("/api/auth/session");
const session = await sessionRes.json();
console.log("[DingTalk] Session after login:", session);
logger.debug("[DingTalk] Session validation completed", {
hasUser: Boolean(session?.user),
});
if (session && session.user) {
console.log("[DingTalk] Session validated, redirecting to home...");
logger.debug("[DingTalk] Session validated, redirecting to home...");
setIsSuccessful(true);
// 使用 replace 而不是 href,避免在历史记录中留下登录页
// 增加短暂延迟确保 cookie 完全同步
@ -109,17 +142,21 @@ export default function Page() {
window.location.replace("/");
}, 200);
} else {
console.error("[DingTalk] Session not created after login");
logger.error("[DingTalk] Session not created after login");
setIsHandlingToken(false);
toast({ type: "error", description: "登录失败:无法创建会话" });
}
} else if (contentType?.includes("application/json")) {
const result = await loginRes.json();
console.log("[DingTalk] Login result:", result);
logger.debug("[DingTalk] Login JSON response received", {
ok: loginRes.ok,
hasUrl: Boolean(result?.url),
hasError: Boolean(result?.error),
});
if (result.url || loginRes.ok) {
// 登录成功
console.log("[DingTalk] Login successful");
logger.debug("[DingTalk] Login successful");
// 等待一小段时间确保 cookie 被设置
await new Promise(resolve => setTimeout(resolve, 300));
@ -127,10 +164,12 @@ export default function Page() {
// 验证 session
const sessionRes = await fetch("/api/auth/session");
const session = await sessionRes.json();
console.log("[DingTalk] Session after login:", session);
logger.debug("[DingTalk] Session validation completed", {
hasUser: Boolean(session?.user),
});
if (session && session.user) {
console.log("[DingTalk] Session validated, redirecting...");
logger.debug("[DingTalk] Session validated, redirecting...");
setIsSuccessful(true);
// 使用 replace 而不是 href,避免在历史记录中留下登录页
// 增加短暂延迟确保 cookie 完全同步
@ -138,25 +177,30 @@ export default function Page() {
window.location.replace(result.url || "/");
}, 200);
} else {
console.error("[DingTalk] Session not created");
logger.error("[DingTalk] Session not created");
setIsHandlingToken(false);
toast({ type: "error", description: "登录失败:无法创建会话" });
}
} else {
// 登录失败
console.error("[DingTalk] Login failed:", result);
logger.error("[DingTalk] Login failed", {
hasError: Boolean(result?.error),
});
setIsHandlingToken(false);
toast({ type: "error", description: result.error || "登录失败,请重试" });
}
} else {
// 未知响应
const responseText = await loginRes.text();
console.error("[DingTalk] Unexpected response type. Body:", responseText.substring(0, 200));
await loginRes.text();
logger.error("[DingTalk] Unexpected response type", {
status: loginRes.status,
contentType,
});
setIsHandlingToken(false);
toast({ type: "error", description: "登录失败:服务器响应异常" });
}
} catch (error) {
console.error("[DingTalk] Login error:", error);
logger.error("[DingTalk] Login error:", error);
setIsHandlingToken(false);
toast({ type: "error", description: "登录发生错误:" + (error instanceof Error ? error.message : String(error)) });
}
@ -167,7 +211,7 @@ export default function Page() {
const handleChunkError = (event: ErrorEvent) => {
const target = event.target as any;
if (target?.src?.includes("/_next/static/chunks/") || event.message?.includes("Failed to load chunk")) {
console.error("[ChunkError] 检测到chunk加载失败,3秒后自动刷新页面");
logger.error("[ChunkError] 检测到chunk加载失败,3秒后自动刷新页面");
// 3秒后自动刷新页面(清除缓存)
setTimeout(() => {
window.location.reload();
@ -183,33 +227,31 @@ export default function Page() {
useEffect(() => {
setMounted(true);
// 测试标记 - 确认代码已更新
console.log("=".repeat(50));
console.log("🔔 登录页面代码版本: 2024-12-10 v3.0");
console.log("=".repeat(50));
// 获取网站设置(添加时间戳防止缓存)
fetch(`/api/site-settings?t=${Date.now()}`)
fetch(`/api/site-settings?scope=auth&t=${Date.now()}`)
.then((res) => res.json())
.then((data) => {
console.log("[Login] Site settings data:", data);
logger.debug("[Login] Site settings loaded", {
hasSiteName: Boolean(data.siteName),
hasSiteLogo: Boolean(data.siteLogo),
hasNotification: Boolean(data.notification),
});
if (data.siteName) setSiteName(data.siteName);
if (data.siteLogo) setSiteLogo(data.siteLogo);
if (data.siteLogoDark) setSiteLogoDark(data.siteLogoDark);
// 处理系统通知
console.log("[Login] Notification data:", data.notification);
if (data.notification && data.notification.enabled) {
console.log("[Login] Setting notification and showing dialog");
logger.debug("[Login] Setting notification and showing dialog");
setNotification(data.notification);
// 每次访问登录页都显示通知(重要公告)
setShowNotification(true);
} else {
console.log("[Login] Notification not enabled or missing");
logger.debug("[Login] Notification not enabled or missing");
}
})
.catch((err) => {
console.error("[Login] Failed to fetch site settings:", err);
logger.error("[Login] Failed to fetch site settings:", err);
});
// 获取认证设置
@ -220,18 +262,29 @@ export default function Page() {
if (data.isGoogleEnabled) setIsGoogleEnabled(true);
if (data.isGoogleConfigured) setIsGoogleConfigured(true);
if (data.dingTalkAppKey) setDingTalkAppKey(data.dingTalkAppKey);
if (Array.isArray(data.organizations)) {
const rawOrganizations = Array.isArray(data.organizations)
? data.organizations
: Array.isArray(data.dingTalkOrganizations)
? data.dingTalkOrganizations
: [];
if (rawOrganizations.length > 0) {
setDingTalkOrganizations(
data.organizations
.map((org: { name?: string }) => org.name)
.filter((name: string | undefined): name is string => !!name),
rawOrganizations
.map((org: { name?: string } | string) =>
typeof org === "string" ? org : org.name,
)
.filter((name: string | undefined): name is string => Boolean(name?.trim()))
.map((name: string) => name.trim()),
);
}
if (data.isRegistrationEnabled !== undefined) {
setIsRegistrationEnabled(data.isRegistrationEnabled);
}
})
.catch(console.error);
.catch((error) => {
logger.error("[Login] Failed to fetch auth settings", error);
});
}, []);
const handleGoogleLogin = async () => {
@ -260,7 +313,10 @@ export default function Page() {
// 监听来自钉钉回调页面的消息
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
console.log("[DingTalk] Received message:", event.data);
logger.debug("[DingTalk] Received message", {
type: event.data?.type,
hasToken: Boolean(event.data?.token),
});
if (event.data && event.data.type === "dingtalk_login_success") {
setShowDingTalkModal(false);
setDingTalkIframeSrc("");
@ -337,7 +393,37 @@ export default function Page() {
};
}, []);
// 根据组织名称启动钉钉登录(在 iframe 中打开)
const getSafeLoginCallbackUrl = () => {
if (typeof window === "undefined") return "/";
const callbackUrl = new URLSearchParams(window.location.search).get(
"callbackUrl",
);
if (
callbackUrl &&
callbackUrl.startsWith("/") &&
!callbackUrl.startsWith("//")
) {
return callbackUrl;
}
return "/";
};
const buildDingTalkLoginUrl = (orgName?: string) => {
const params = new URLSearchParams();
if (orgName) params.set("org", orgName);
const callbackUrl = getSafeLoginCallbackUrl();
if (callbackUrl !== "/") {
params.set("callbackUrl", callbackUrl);
}
const query = params.toString();
return `/api/auth/dingtalk/login${query ? `?${query}` : ""}`;
};
// 根据组织名称启动钉钉登录。钉钉授权页禁止第三方 iframe 嵌入,必须使用顶层跳转。
const startDingTalkLoginForOrg = (orgName: string) => {
// 清除 URL 中的 token 参数,避免干扰
if (
@ -348,14 +434,15 @@ export default function Page() {
window.history.replaceState({}, "", newUrl);
}
const loginUrl = `/api/auth/dingtalk/login?org=${encodeURIComponent(
orgName,
)}`;
const loginUrl = buildDingTalkLoginUrl(orgName);
console.log("[DingTalk] Opening org login:", orgName, loginUrl);
setDingTalkIframeSrc(loginUrl);
logger.debug("[DingTalk] Redirecting org login", {
hasOrg: Boolean(orgName),
loginUrl,
});
setShowDingTalkModal(true);
setShowOrgMenu(false);
window.location.assign(loginUrl);
};
// 钉钉登录 - 支持多组织
@ -372,7 +459,6 @@ export default function Page() {
return;
}
// 兼容:直接使用旧的方式在 iframe 中打开
if (
typeof window === "undefined" ||
typeof window.location === "undefined"
@ -387,27 +473,9 @@ export default function Page() {
window.history.replaceState({}, "", newUrl);
}
const param = {
redirect_uri: encodeURIComponent(
window.location.origin + "/dingtalk-callback.html",
),
client_id: dingTalkAppKey,
scope: "openid",
response_type: "code",
state: "default",
prompt: "consent",
};
const redirectUrl =
"https://login.dingtalk.com/oauth2/auth?" +
Object.keys(param)
.map((key) => key + "=" + (param as any)[key])
.join("&");
console.log("[DingTalk] Opening OAuth URL (legacy):", redirectUrl);
setDingTalkIframeSrc(redirectUrl);
setShowDingTalkModal(true);
const loginUrl = buildDingTalkLoginUrl();
logger.debug("[DingTalk] Redirecting OAuth login", { loginUrl });
window.location.assign(loginUrl);
return;
}
@ -417,8 +485,10 @@ export default function Page() {
return;
}
// 多组织:点击时展开/收起选择菜单(hover 时也会展开)
setShowOrgMenu((prev) => !prev);
// 多组织:直接打开弹窗中的组织列表,避免悬浮菜单被页面层级覆盖
setDingTalkIframeSrc("");
setShowDingTalkModal(true);
setShowOrgMenu(false);
};
// 关闭钉钉登录弹窗
@ -435,6 +505,14 @@ export default function Page() {
(isDingTalkEnabled && (Boolean(dingTalkAppKey) || dingTalkOrganizations.length > 0)) ||
(isGoogleEnabled && isGoogleConfigured);
if (isCheckingExistingSession) {
return (
<div className="flex min-h-screen items-center justify-center text-muted-foreground text-sm">
正在检查登录状态...
</div>
);
}
return (
<div className="flex h-dvh w-screen items-start justify-center bg-background pt-12 md:items-center md:pt-0 relative overflow-hidden">
{/* Antigravity 动画背景 */}
@ -459,7 +537,11 @@ export default function Page() {
<div className="flex flex-col items-center justify-center gap-2 px-4 text-center sm:px-16 backdrop-blur-sm bg-background/80 rounded-2xl p-6 pointer-events-auto">
{displayLogo ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={displayLogo} alt={siteName} className="h-12 w-auto mb-2 object-contain" />
<img
src={displayLogo}
alt={siteName}
className="mb-2 h-12 w-auto object-contain"
/>
) : null}
<h3 className="font-semibold text-xl dark:text-zinc-50">登录 {siteName}</h3>
<p className="text-gray-500 text-sm dark:text-zinc-400">
@ -577,17 +659,7 @@ export default function Page() {
<button
key={org}
type="button"
onClick={() => {
const loginUrl = `/api/auth/dingtalk/login?org=${encodeURIComponent(
org,
)}`;
console.log(
"[DingTalk] Selected organization:",
org,
loginUrl,
);
setDingTalkIframeSrc(loginUrl);
}}
onClick={() => startDingTalkLoginForOrg(org)}
className="flex w-full items-center justify-between rounded-md border bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 dark:bg-zinc-900 dark:text-zinc-200 dark:hover:bg-zinc-800"
>
<span>{org}</span>
@ -595,7 +667,7 @@ export default function Page() {
))}
</div>
<p className="text-xs text-muted-foreground">
选择后将在下方加载钉钉登录页面。
选择后将跳转到钉钉完成授权。
</p>
</div>
)}

View File

@ -22,6 +22,7 @@ export default function Page() {
const [emailVerificationExpiresMinutes, setEmailVerificationExpiresMinutes] = useState(10);
const [isSendingCode, setIsSendingCode] = useState(false);
const [codeCooldown, setCodeCooldown] = useState(0);
const [isCheckingExistingSession, setIsCheckingExistingSession] = useState(true);
const redirectTimerRef = useRef<number | null>(null);
useEffect(() => {
@ -40,13 +41,33 @@ export default function Page() {
.catch(console.error);
// 获取网站设置(添加时间戳防止缓存)
fetch(`/api/site-settings?t=${Date.now()}`)
fetch(`/api/site-settings?scope=branding&t=${Date.now()}`)
.then((res) => res.json())
.then((data) => {
if (data.siteName) setSiteName(data.siteName);
if (data.siteLogo) setSiteLogo(data.siteLogo);
if (data.siteName) {
setSiteName(data.siteName);
}
if (data.siteLogo) {
setSiteLogo(data.siteLogo);
}
})
.catch(console.error);
fetch("/api/auth/validate-session", {
cache: "no-store",
credentials: "include",
})
.then((res) => res.json())
.then((data) => {
if (data?.valid === true && data?.userType !== "guest") {
window.location.replace("/");
return;
}
setIsCheckingExistingSession(false);
})
.catch(() => {
setIsCheckingExistingSession(false);
});
}, []);
useEffect(() => {
@ -66,7 +87,6 @@ export default function Page() {
}
);
// biome-ignore lint/correctness/useExhaustiveDependencies: router is a stable ref
useEffect(() => {
if (redirectTimerRef.current) {
window.clearTimeout(redirectTimerRef.current);
@ -119,6 +139,14 @@ export default function Page() {
formAction(formData);
};
if (isCheckingExistingSession) {
return (
<div className="flex min-h-screen items-center justify-center text-muted-foreground text-sm">
正在检查登录状态...
</div>
);
}
const handleSendCode = async () => {
const normalizedEmail = email.trim().toLowerCase();
if (!normalizedEmail) {

View File

@ -0,0 +1,44 @@
import { existsSync } from "node:fs";
import path from "node:path";
import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { auth } from "@/app/(auth)/auth";
const ZOO_MODELING_APP_SRC =
"/vendor/zoo-modeling-app/index.html?v=f2b345db-eng1";
export const dynamic = "force-dynamic";
export const metadata: Metadata = {
title: "HiCAD",
};
export const runtime = "nodejs";
export default async function AiCadPage() {
const session = await auth();
if (!session?.user?.id) {
redirect("/login?callbackUrl=%2Fai-cad");
}
const isReady = existsSync(
path.join(process.cwd(), "public/vendor/zoo-modeling-app/index.html")
);
if (isReady) {
redirect(ZOO_MODELING_APP_SRC);
}
return (
<main className="flex h-svh w-full items-center justify-center bg-[#10100e] p-6 text-[#f8f6ef]">
<section className="w-full max-w-xl rounded-lg border border-[#3a352c] bg-[#171510] p-6 shadow-2xl">
<h1 className="font-semibold text-lg">HiCAD 官方前端尚未构建</h1>
<p className="mt-2 text-[#d3cabb] text-sm leading-6">
请运行
<code className="mx-1 rounded bg-white/10 px-1.5 py-0.5">
pnpm zoo-modeling-app:vendor
</code>
生成官方前端,然后刷新本页。
</p>
</section>
</main>
);
}

View File

@ -63,7 +63,6 @@ export async function GET(
}
const emptyDataStream = createUIMessageStream<ChatMessage>({
// biome-ignore lint/suspicious/noEmptyBlockStatements: "Needs to exist"
execute: () => {},
});

View File

@ -0,0 +1,108 @@
import type { PostRequestBody } from "../schema";
export function sanitizeAgentOutputSettingsSection(params: {
message?: string;
userImageParams?: PostRequestBody["imageParams"];
hasUserCustomImageParams: boolean;
}): string | undefined {
const { message, userImageParams, hasUserCustomImageParams } = params;
if (!message?.trim()) return message;
const outputSectionRegex =
/(?:###\s*输出设置|【\s*输出设置\s*】)[\s\S]*?(?=\n(?:###\s*|【\s*[^】]+\s*】)|$)/;
if (!outputSectionRegex.test(message)) return message;
if (!hasUserCustomImageParams) {
return message.replace(
outputSectionRegex,
"【输出设置】\n- 参数:按用户面板当前设置执行(未自动改写)"
);
}
const lines: string[] = [];
const aspectRatio =
typeof userImageParams?.aspectRatio === "string" ? userImageParams.aspectRatio.trim() : "";
const imageSize =
typeof userImageParams?.imageSize === "string" ? userImageParams.imageSize.trim() : "";
const imageCount =
typeof userImageParams?.imageCount === "number" ? userImageParams.imageCount : undefined;
if (aspectRatio && aspectRatio !== "auto") {
lines.push(`- 宽高比:${aspectRatio}(来自用户面板设置)`);
}
if (imageSize && imageSize.toUpperCase() !== "2K") {
lines.push(`- 清晰度档位:${imageSize.toUpperCase()}(来自用户面板设置)`);
}
if (typeof imageCount === "number" && Number.isFinite(imageCount) && imageCount !== 1) {
lines.push(`- 出图数量:${Math.max(1, Math.round(imageCount))} 张(来自用户面板设置)`);
}
lines.push("- 其余参数:保持用户面板当前设置");
return message.replace(outputSectionRegex, `【输出设置】\n${lines.join("\n")}`);
}
export function normalizeAgentReportHeadingLine(line: string): string {
const trimmed = line.trim();
if (!trimmed) return line;
const markdownHeading = trimmed.match(/^#{1,6}\s*(.+?)\s*$/);
const plainTitle = markdownHeading?.[1]?.trim() || trimmed;
const normalizedTitle = plainTitle
.replace(/^[^\u4e00-\u9fa5A-Za-z0-9]+/, "")
.trim();
const knownTitles = [
"意图分析",
"任务规划",
"需求理解",
"图片分析",
"执行计划",
"优化后提示词",
"输出设置",
"脚本规划",
"分镜规划",
"执行说明",
"视频生成完成",
];
if (knownTitles.some((title) => normalizedTitle === title)) {
return `【${normalizedTitle}】`;
}
if (markdownHeading) {
return `【${normalizedTitle || plainTitle}】`;
}
return line;
}
export function stripAgentFriendlyMediaEmbeds(message: string): string {
if (!message) return message;
const withoutInlineMedia = message
.replace(/!\[[^\]]*]\((https?:\/\/[^\s)]+)\)/gi, "")
.replace(/<video[\s\S]*?<\/video>/gi, "")
.replace(/<img[^>]*>/gi, "");
const cleanedLines = withoutInlineMedia
.split("\n")
.filter((line) => {
const trimmed = line.trim();
if (!trimmed) return true;
if (/^https?:\/\/\S+$/i.test(trimmed)) return false;
if (/^\[[^\]]*]\(https?:\/\/[^\s)]+\)$/i.test(trimmed)) return false;
return true;
})
.join("\n");
return cleanedLines.replace(/\n{3,}/g, "\n\n").trim();
}
export function normalizeAgentReportForDisplay(message?: string): string | undefined {
if (!message?.trim()) return message;
const stripped = stripAgentFriendlyMediaEmbeds(message);
if (!stripped) return undefined;
return stripped
.split("\n")
.map((line) => normalizeAgentReportHeadingLine(line))
.join("\n");
}

View File

@ -0,0 +1,540 @@
export type AgentPresetMessageImagePart = {
type: "file";
url: string;
name: string;
mediaType: string;
assetId?: string;
};
export function extractCurrentMessageImageParts(
parts: Array<Record<string, any>>,
): AgentPresetMessageImagePart[] {
return parts
.filter(
(part): part is AgentPresetMessageImagePart =>
part?.type === "file" &&
typeof part?.url === "string" &&
/^https?:\/\//i.test(part.url) &&
typeof part?.mediaType === "string" &&
part.mediaType.startsWith("image/")
)
.map((part) => ({
type: "file",
url: String(part.url).trim(),
name: String(part.name || "图片").trim() || "图片",
mediaType: String(part.mediaType || "image/jpeg").trim() || "image/jpeg",
assetId:
typeof part.assetId === "string" && part.assetId.trim() ? part.assetId.trim() : undefined,
}));
}
export function getSceneUploadedImageBreakdown(params: {
uploadedImageParts: AgentPresetMessageImagePart[];
fallbackPrimaryCount: number;
fallbackIncludeModelImage: boolean;
}) {
const normalizedNames = params.uploadedImageParts.map((part) => String(part.name || "").trim());
const primaryCountFromNames = normalizedNames.filter((name) => name.startsWith("主SKU参考图")).length;
const secondaryCountFromNames = normalizedNames.filter((name) => name.startsWith("搭配SKU参考图")).length;
const hasModelFromNames = normalizedNames.some((name) => name.startsWith("品牌模特图"));
const hasNamedBreakdown =
primaryCountFromNames > 0 || secondaryCountFromNames > 0 || hasModelFromNames;
if (hasNamedBreakdown) {
return {
primaryProductImageCount: primaryCountFromNames,
bundleProductImageCount: secondaryCountFromNames,
includeModelImage: hasModelFromNames,
};
}
const uploadedImageCount = params.uploadedImageParts.length;
const includeModelImage =
params.fallbackIncludeModelImage && uploadedImageCount > params.fallbackPrimaryCount;
const bundleProductImageCount = Math.max(
0,
uploadedImageCount - params.fallbackPrimaryCount - (includeModelImage ? 1 : 0)
);
const primaryProductImageCount = Math.max(
0,
Math.min(
params.fallbackPrimaryCount,
uploadedImageCount - bundleProductImageCount - (includeModelImage ? 1 : 0)
)
);
return {
primaryProductImageCount,
bundleProductImageCount,
includeModelImage,
};
}
export function resolveScenePromptOptimizationImageUrls(params: {
uploadedImageParts: AgentPresetMessageImagePart[];
primaryProductImageCount: number;
bundleProductImageCount: number;
includeModelImage: boolean;
}) {
const namedPrimaryParts = params.uploadedImageParts.filter((part) =>
String(part.name || "").trim().startsWith("主SKU参考图")
);
const namedModelPart = params.uploadedImageParts.find((part) =>
String(part.name || "").trim().startsWith("品牌模特图")
);
if (namedPrimaryParts.length > 0 || namedModelPart) {
return {
primaryImageUrl: namedPrimaryParts[0]?.url || "",
modelImageUrl: namedModelPart?.url || null,
};
}
const primaryImageUrl =
params.uploadedImageParts.slice(0, Math.max(0, params.primaryProductImageCount))[0]?.url || "";
const modelIndex = params.primaryProductImageCount + params.bundleProductImageCount;
return {
primaryImageUrl,
modelImageUrl:
params.includeModelImage && modelIndex < params.uploadedImageParts.length
? params.uploadedImageParts[modelIndex]?.url || null
: null,
};
}
export function normalizeAgentSupplementPrompt(text: string, presetName?: string | null) {
const normalized = String(text || "").trim();
if (!normalized) return "";
const escapedPresetName = String(presetName || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const genericPattern = escapedPresetName
? new RegExp(`^请按「${escapedPresetName}」风格化配置顺序处理这\\d+张图片。?$`)
: /^请按「.+」风格化配置顺序处理这\d+张图片。?$/;
if (genericPattern.test(normalized)) {
return "";
}
return normalized;
}
export function normalizeAgentWorkflowPrompt(text: string) {
return String(text || "")
.split(/\n+/)
.map((line) => line.replace(/\s+/g, " ").trim())
.filter(Boolean)
.join("\n")
.trim();
}
export function parseAgentWorkflowPromptSections(text: string) {
const sectionMap = new Map<string, string[]>();
let activeSection = "__root__";
const normalizedPrompt = normalizeAgentWorkflowPrompt(text);
if (!normalizedPrompt) {
return sectionMap;
}
normalizedPrompt.split("\n").forEach((line) => {
const trimmedLine = line.trim();
if (!trimmedLine) {
return;
}
const headerMatch = trimmedLine.match(/^【([^】]{1,30})】$/);
if (headerMatch) {
activeSection = headerMatch[1].trim();
if (!sectionMap.has(activeSection)) {
sectionMap.set(activeSection, []);
}
return;
}
const currentLines = sectionMap.get(activeSection) || [];
currentLines.push(trimmedLine);
sectionMap.set(activeSection, currentLines);
});
return sectionMap;
}
export function buildAgentPromptSectionText(
title: string,
lines: string[],
options?: { bullet?: boolean }
) {
const normalizedLines = lines
.map((line) => String(line || "").trim())
.filter(Boolean);
if (normalizedLines.length === 0) {
return "";
}
const sectionBody = options?.bullet
? normalizedLines.map((line) => (line.startsWith("- ") ? line : `- ${line}`)).join("\n")
: normalizedLines.join("\n");
return `【${title}】\n${sectionBody}`;
}
export function buildOptimizedAgentSceneWorkflowPrompt(params: {
workflowPrompt: string;
optimizationResult?: {
sceneGoal: string;
environmentSummary: string;
propSummary: string;
cameraLightingSummary: string;
characterSummary: string;
negativeConstraints: string[];
conflicts: string[];
} | null;
}) {
const normalizedWorkflowPrompt = normalizeAgentWorkflowPrompt(params.workflowPrompt);
if (!normalizedWorkflowPrompt) {
return "";
}
const optimizationResult = params.optimizationResult || null;
if (!optimizationResult) {
return normalizedWorkflowPrompt;
}
const sectionMap = parseAgentWorkflowPromptSections(normalizedWorkflowPrompt);
const sceneGoalLines = [optimizationResult.sceneGoal, optimizationResult.environmentSummary].filter(
Boolean
);
const propLines = [optimizationResult.propSummary].filter(Boolean);
const cameraLightingLines = [optimizationResult.cameraLightingSummary].filter(Boolean);
const characterLines = [optimizationResult.characterSummary].filter(Boolean);
const negativeLines = Array.from(
new Set([
...optimizationResult.negativeConstraints,
...optimizationResult.conflicts.map((item) => `冲突校正:${item}`),
])
);
const optimizedSections = [
buildAgentPromptSectionText("项目背景", sectionMap.get("项目背景") || sectionMap.get("__root__") || []),
buildAgentPromptSectionText("主SKU锁定", sectionMap.get("主SKU锁定") || []),
buildAgentPromptSectionText("搭配SKU锁定", sectionMap.get("搭配SKU锁定") || []),
buildAgentPromptSectionText("参考图", sectionMap.get("参考图") || []),
buildAgentPromptSectionText("场景创作总纲", sceneGoalLines),
buildAgentPromptSectionText("陈列与道具", propLines),
buildAgentPromptSectionText("机位与光线", cameraLightingLines),
buildAgentPromptSectionText("人物规则", characterLines),
buildAgentPromptSectionText("负面约束", negativeLines, { bullet: true }),
].filter(Boolean);
return optimizedSections.join("\n\n").trim();
}
const AGENT_PROMPT_STRUCTURAL_LABELS = new Set([
"角色",
"技能",
"目标",
"约束条件",
"流程",
"输出",
]);
const AGENT_PROMPT_LABEL_SEMANTIC_ALIASES = new Map<string, string>([
["角色", "role"],
["技能", "skills"],
["目标", "goal"],
["约束条件", "constraints"],
["流程", "process"],
["输出", "output"],
["人群选择", "audience"],
["人物", "audience"],
["主题风格", "style"],
["整体风格", "style"],
["空间选择", "space"],
["空间布局", "space"],
["主空间", "space"],
["子空间选择", "subspace"],
["子空间", "subspace"],
["硬装方案", "hardDecoration"],
["硬装", "hardDecoration"],
["软装方案", "softDecoration"],
["软装", "softDecoration"],
["光氛选择", "lighting"],
["光源布置", "lighting"],
["灯具", "lighting"],
["机位选择", "camera"],
["拍摄角度", "camera"],
["配饰陈列", "accessories"],
["放在产品内的小件配饰", "accessories"],
["前景", "sceneLayout"],
["中景", "sceneLayout"],
["背景", "sceneLayout"],
]);
const AGENT_SCENE_WORKFLOW_OWNED_SEMANTICS = new Set([
"role",
"skills",
"goal",
"constraints",
"process",
"output",
"audience",
"style",
"space",
"subspace",
"hardDecoration",
"softDecoration",
"lighting",
"camera",
"accessories",
"sceneLayout",
]);
export function normalizePromptLineForCompare(line: string) {
return String(line || "")
.replace(/\s+/g, " ")
.replace(/[::]/g, ":")
.replace(/[。;;,,、!!??]+$/g, "")
.trim()
.toLowerCase();
}
export function parsePromptLineLabel(line: string) {
const normalized = String(line || "").replace(/\s+/g, " ").trim();
if (!normalized) return { label: "", value: "" };
const headerOnlyMatch = normalized.match(/^([^::]{1,20})[::]\s*$/);
if (headerOnlyMatch) {
return {
label: headerOnlyMatch[1].trim(),
value: "",
};
}
const colonMatch = normalized.match(/^([^::]{1,20})[::]\s*(.+)$/);
if (colonMatch) {
return {
label: colonMatch[1].trim(),
value: colonMatch[2].trim(),
};
}
const whitespaceMatch = normalized.match(/^([^\s]{1,20})\s+(.+)$/);
if (whitespaceMatch) {
return {
label: whitespaceMatch[1].trim(),
value: whitespaceMatch[2].trim(),
};
}
return { label: "", value: normalized };
}
export function normalizePromptLabel(label: string) {
return String(label || "")
.replace(/\s+/g, "")
.replace(/[::]/g, "")
.trim();
}
export function getPromptSemanticLabel(label: string) {
const normalized = normalizePromptLabel(label);
return AGENT_PROMPT_LABEL_SEMANTIC_ALIASES.get(normalized) || normalized;
}
export function collectPromptSemanticLabelsFromText(text: string) {
const semanticLabels = new Set<string>();
const normalizedText = String(text || "").replace(/\s+/g, " ").trim();
if (!normalizedText) {
return semanticLabels;
}
for (const [label, semantic] of AGENT_PROMPT_LABEL_SEMANTIC_ALIASES.entries()) {
if (!label) {
continue;
}
if (normalizedText.includes(`${label}:`) || normalizedText.includes(`${label}:`)) {
semanticLabels.add(semantic);
continue;
}
if (normalizedText.includes(`${label} `) || normalizedText.includes(label)) {
semanticLabels.add(semantic);
}
}
return semanticLabels;
}
export function shouldDropSceneSlotSemanticLine(
slotSemanticLabel: string,
workflowSemanticLabels: Set<string>,
) {
if (!slotSemanticLabel || !AGENT_SCENE_WORKFLOW_OWNED_SEMANTICS.has(slotSemanticLabel)) {
return false;
}
if (slotSemanticLabel === "sceneLayout") {
return [
"style",
"space",
"subspace",
"hardDecoration",
"softDecoration",
"accessories",
"camera",
"lighting",
].some((semantic) => workflowSemanticLabels.has(semantic));
}
return workflowSemanticLabels.has(slotSemanticLabel);
}
export function dedupeAgentSlotPromptAgainstWorkflow(params: {
workflowPrompt?: string | null;
slotPrompt?: string | null;
modeType?: "scene_generation" | "detail_replacement" | "sku_replacement" | "generic";
}) {
const dedupedSlotLines: string[] = [];
const seenExactLines = new Set<string>();
const workflowStructuralValues = new Map<string, string>();
const workflowSemanticLabels = new Set<string>();
const rememberLine = (line: string) => {
const normalizedLine = normalizePromptLineForCompare(line);
if (!normalizedLine) {
return;
}
seenExactLines.add(normalizedLine);
};
const normalizedWorkflowLines = normalizeAgentWorkflowPrompt(params.workflowPrompt || "")
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
normalizedWorkflowLines.forEach((line) => {
const parsed = parsePromptLineLabel(line);
if (parsed.label) {
workflowSemanticLabels.add(getPromptSemanticLabel(parsed.label));
}
collectPromptSemanticLabelsFromText(line).forEach((semantic) => {
workflowSemanticLabels.add(semantic);
});
collectPromptSemanticLabelsFromText(parsed.value || "").forEach((semantic) => {
workflowSemanticLabels.add(semantic);
});
if (parsed.label && AGENT_PROMPT_STRUCTURAL_LABELS.has(parsed.label)) {
workflowStructuralValues.set(
parsed.label,
normalizePromptLineForCompare(parsed.value || "")
);
}
rememberLine(line);
});
const normalizedSlotLines = String(params.slotPrompt || "")
.split(/\n+/)
.map((line) => line.replace(/\s+/g, " ").trim())
.filter(Boolean);
let activeSlotSemanticLabel = "";
normalizedSlotLines.forEach((line) => {
const normalizedLine = normalizePromptLineForCompare(line);
if (!normalizedLine || seenExactLines.has(normalizedLine)) {
return;
}
const parsed = parsePromptLineLabel(line);
const slotSemanticLabel = parsed.label ? getPromptSemanticLabel(parsed.label) : "";
if (slotSemanticLabel) {
activeSlotSemanticLabel = slotSemanticLabel;
}
if (
params.modeType === "scene_generation" &&
shouldDropSceneSlotSemanticLine(
slotSemanticLabel || activeSlotSemanticLabel,
workflowSemanticLabels,
)
) {
return;
}
if (parsed.label && AGENT_PROMPT_STRUCTURAL_LABELS.has(parsed.label)) {
const normalizedValue = normalizePromptLineForCompare(parsed.value || "");
const workflowValue = workflowStructuralValues.get(parsed.label);
if (workflowValue && workflowValue === normalizedValue) {
return;
}
}
seenExactLines.add(normalizedLine);
dedupedSlotLines.push(line);
});
return dedupedSlotLines.join("\n").trim();
}
export function buildAgentSlotPrompt(params: {
workflowPrompt?: string | null;
slotPrompt: string;
slotTitle?: string | null;
userSupplementPrompt?: string | null;
includeSlotTitle?: boolean;
modeType?: "scene_generation" | "detail_replacement" | "sku_replacement" | "generic";
}) {
const lines: string[] = [];
const workflowPrompt = normalizeAgentWorkflowPrompt(params.workflowPrompt || "");
if (workflowPrompt) {
lines.push(workflowPrompt);
}
const slotTitle = String(params.slotTitle || "").trim();
if (params.includeSlotTitle !== false && slotTitle) {
lines.push(`【槽位】${slotTitle}`);
}
const dedupedSlotPrompt = dedupeAgentSlotPromptAgainstWorkflow({
workflowPrompt,
slotPrompt: params.slotPrompt,
modeType: params.modeType || "generic",
});
if (dedupedSlotPrompt) {
lines.push(dedupedSlotPrompt);
}
const supplement = String(params.userSupplementPrompt || "").trim();
if (supplement) {
lines.push(`【补充要求】${supplement}`);
}
return lines.join("\n\n").trim();
}
export function buildAgentSceneWorkflowPrompt(params: {
workflowPrompt?: string | null;
slotPrompt?: string | null;
userSupplementPrompt?: string | null;
imageRolePrompt?: string | null;
}) {
const lines: string[] = [];
const workflowPrompt = normalizeAgentWorkflowPrompt(params.workflowPrompt || "");
if (workflowPrompt) {
lines.push(workflowPrompt);
}
const slotPrompt = String(params.slotPrompt || "")
.split(/\n+/)
.map((line) => line.replace(/\s+/g, " ").trim())
.filter(Boolean)
.join("\n")
.trim();
if (slotPrompt) {
lines.push(slotPrompt);
}
let imageRolePrompt = String(params.imageRolePrompt || "").trim();
if (workflowPrompt.includes("品牌模特图:已选") || workflowPrompt.includes("固定模特图")) {
imageRolePrompt = imageRolePrompt
.replace(
/^如当前请求未附带固定模特图,仅在工作流明确要求人物时再安排辅助人物,且人物不能喧宾夺主。$/m,
""
)
.replace(/^当前没有固定模特图,不要凭空生成强人物主体。$/m, "")
.split(/\n+/)
.map((line) => line.trim())
.filter(Boolean)
.join("\n");
}
if (imageRolePrompt) {
lines.push(imageRolePrompt);
}
const supplement = String(params.userSupplementPrompt || "").trim();
if (supplement) {
lines.push(`【补充要求】${supplement}`);
}
return lines.join("\n\n").trim();
}

View File

@ -0,0 +1,105 @@
import { createLogger } from "@/lib/observability/logger";
import { getModelGenerationCreditCost } from "@/lib/ai/image-model-pricing";
import {
consumeUserPoints,
getAiModelById,
getEnabledAiModelsWithProvider,
getPointsSystemConfig,
getUserById,
} from "@/lib/db/queries";
const chatApiLogger = createLogger("chat.billing");
const MODEL_UUID_REGEX =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
export function getModelCreditCost(
modelConfig: any,
target: "image" | "video",
options?: {
duration?: number | null;
imageSize?: string | null;
resolution?: string | null;
},
): number {
return getModelGenerationCreditCost(modelConfig, target, options);
}
export async function resolveBillingModelByReference(modelRef: string): Promise<any | undefined> {
if (!modelRef) return undefined;
if (MODEL_UUID_REGEX.test(modelRef)) {
return await getAiModelById({ id: modelRef }).catch(() => undefined);
}
const allModels = await getEnabledAiModelsWithProvider(true, true).catch(() => []);
const matched = allModels.find(
({ model }: any) => model.id === modelRef || model.modelId === modelRef,
);
return matched?.model;
}
export async function consumeGeneratedVideoPoints(params: {
userId: string;
chatId: string;
messageId: string;
modelRefs: Array<string | null | undefined>;
duration?: number | null;
resolution?: string | null;
}) {
try {
const pointsConfig = await getPointsSystemConfig();
if (!pointsConfig.enabled) return;
const userData = await getUserById({ id: params.userId }).catch(() => undefined);
if (userData?.isHighVip) {
chatApiLogger.info(`[Points] Skip video points for VIP user=${params.userId}`);
return;
}
let billingModel: any | undefined;
for (const ref of params.modelRefs) {
if (!ref) continue;
const resolved = await resolveBillingModelByReference(ref);
if (!resolved) continue;
billingModel = resolved;
if (
getModelCreditCost(resolved, "video", {
duration: params.duration,
resolution: params.resolution,
}) > 0
) {
break;
}
}
if (!billingModel) return;
const unitCost = getModelCreditCost(billingModel, "video", {
duration: params.duration,
resolution: params.resolution,
});
if (unitCost <= 0) return;
const consumeResult = await consumeUserPoints({
userId: params.userId,
amount: unitCost,
reason: "video_generation",
modelId: billingModel.id,
chatId: params.chatId,
messageId: params.messageId,
note: `模型(${billingModel.displayName || billingModel.modelId || billingModel.id}) 生视频 1 条${params.duration ? `(${params.duration} 秒)` : ""}${params.resolution ? `(${params.resolution})` : ""}`,
});
if (!consumeResult.ok) {
chatApiLogger.warn(`[Points] Failed to consume video points. user=${params.userId} model=${billingModel.id} cost=${unitCost}`);
} else if (consumeResult.skipped) {
chatApiLogger.info(`[Points] Skipped ${unitCost} video points for VIP user=${params.userId} model=${billingModel.id}`);
} else {
chatApiLogger.info(`[Points] Consumed ${unitCost} points for video. balance=${consumeResult.balance}`);
}
} catch (error) {
chatApiLogger.error("[Points] Failed to consume points after video generation:", error);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,192 @@
import { createLogger } from "@/lib/observability/logger";
import { NextResponse } from "next/server";
import { getRequestErrorMessage } from "@/lib/request-error";
import { generateUUID } from "@/lib/utils";
const chatApiLogger = createLogger("chat.errors");
const DEFAULT_CHAT_PRE_RESPONSE_SOFT_TIMEOUT_MS = 55_000;
export class OperationTimeoutError extends Error {
constructor(operation: string, timeoutMs: number) {
super(`${operation} timed out after ${timeoutMs}ms`);
this.name = "OperationTimeoutError";
}
}
export function getChatStreamErrorMessage(error: unknown): string {
const rawMessage =
error instanceof Error ? String(error.message || "").trim() : String(error || "").trim();
const normalized = rawMessage.toLowerCase();
if (
normalized.includes("token quota is not enough") ||
normalized.includes("pre_consume_token_quota_failed") ||
normalized.includes("quota is not enough")
) {
return "当前对话模型额度不足,请切换其他模型,或联系管理员补充该模型额度。";
}
if (
normalized.includes("timeout") ||
normalized.includes("timed out") ||
normalized.includes("body timeout")
) {
return "当前对话模型响应超时,请重试,或切换到其他可用模型。";
}
if (rawMessage) {
return `当前对话暂时失败:${rawMessage}`;
}
return "当前对话暂时失败,请重试或切换其他模型。";
}
function resolveChatApiFailure(error: unknown) {
const detailMessage = getRequestErrorMessage(error);
const normalized = String(detailMessage || "").toLowerCase();
if (
normalized.includes("payload too large") ||
normalized.includes("request entity too large") ||
normalized.includes("body exceeded") ||
normalized.includes("content length") ||
normalized.includes("413")
) {
return {
code: "payload_too_large:chat",
message: "本次请求内容过大,请减少图片或文字内容后重试。",
status: 413,
category: "payload_too_large",
detailMessage,
};
}
if (
error instanceof OperationTimeoutError ||
normalized.includes("timeout") ||
normalized.includes("timed out") ||
normalized.includes("body timeout") ||
normalized.includes("aborterror") ||
normalized.includes("aborted")
) {
return {
code: "timeout:chat",
message: "当前模型响应超时,请重试,或切换到其他可用模型。",
status: 504,
category: "timeout",
detailMessage,
};
}
if (
normalized.includes("fetch failed") ||
normalized.includes("econnreset") ||
normalized.includes("econnrefused") ||
normalized.includes("enotfound") ||
normalized.includes("etimedout") ||
normalized.includes("und_err") ||
normalized.includes("socket hang up")
) {
return {
code: "server_error:chat",
message: "模型服务连接失败,请稍后重试,或切换其他可用模型。",
status: 502,
category: "upstream_connection",
detailMessage,
};
}
if (
normalized.includes("database") ||
normalized.includes("postgres") ||
normalized.includes("drizzle")
) {
return {
code: "server_error:chat",
message: "服务数据库暂时繁忙,请稍后重试。",
status: 503,
category: "database",
detailMessage,
};
}
return {
code: "server_error:chat",
message: "服务临时繁忙,请稍后重试;如果连续出现,请联系管理员。",
status: 500,
category: "unknown",
detailMessage,
};
}
export function createChatApiFailureResponse(
error: unknown,
vercelId: string | null
) {
const failure = resolveChatApiFailure(error);
const traceId = generateUUID();
chatApiLogger.error("[Chat API] Unhandled request failure:", {
traceId,
vercelId,
category: failure.category,
status: failure.status,
detailMessage: failure.detailMessage,
error,
});
return NextResponse.json(
{
code: failure.code,
message: failure.message,
cause: `trace:${traceId}`,
},
{ status: failure.status }
);
}
export async function withOperationTimeout<T>(
operation: Promise<T>,
operationName: string,
timeoutMs: number
): Promise<T> {
let timer: ReturnType<typeof setTimeout> | null = null;
try {
return await Promise.race([
operation,
new Promise<never>((_, reject) => {
timer = setTimeout(
() => reject(new OperationTimeoutError(operationName, timeoutMs)),
timeoutMs
);
}),
]);
} finally {
if (timer) {
clearTimeout(timer);
}
}
}
export function getRemainingBudgetMs(startedAt: number, budgetMs: number) {
return Math.max(0, budgetMs - (Date.now() - startedAt));
}
export function getBudgetedTimeout(params: {
startedAt: number;
preferredMs: number;
minimumMs?: number;
budgetMs?: number;
}) {
const minimumMs = params.minimumMs ?? 6_000;
const remainingMs = getRemainingBudgetMs(
params.startedAt,
params.budgetMs ?? DEFAULT_CHAT_PRE_RESPONSE_SOFT_TIMEOUT_MS
);
if (remainingMs < minimumMs) {
return null;
}
return Math.min(params.preferredMs, remainingMs);
}

View File

@ -0,0 +1,507 @@
import { createLogger } from "@/lib/observability/logger";
import { isFalGptImage2EditModelId } from "@/lib/ai/providers/fal-image";
import { getLanguageModelById } from "@/lib/ai/dynamic-provider";
import { modelLacksReferenceImageInputSupport } from "@/lib/ai/reference-image-capability";
import type { ChatMessage } from "@/lib/types";
import { convertMessagesWithImages } from "./image-media";
const chatApiLogger = createLogger("chat.imageFailover");
export const IMAGE_FAILOVER_PROVIDER_ALLOWLIST = new Set([
"alibaba-image",
"bfl-image",
"ruxa-image",
"fal-image",
"snapgen-image",
"openai-image",
"bananaimage",
"visionary",
"atlascloud",
"vip98k",
"stepfun-image",
"runway",
"krea",
"grsai-gpt-image",
"volcano-vision",
"gemini",
"huanke-gemini",
]);
const IMAGE_FAILOVER_MODEL_COOLDOWN_MS = 30 * 60 * 1000;
const imageFailoverModelCooldown = new Map<string, { until: number; reason: string }>();
export function resolveImageModelRuntimeId(
model: { defaultParams?: unknown; modelId?: unknown; id?: unknown } | null | undefined,
) {
const defaultParams = model?.defaultParams;
const defaultEndpoint =
typeof defaultParams === "object" &&
defaultParams !== null &&
"endpoint" in defaultParams
? String((defaultParams as Record<string, unknown>).endpoint || "").trim()
: "";
if (defaultEndpoint) {
return defaultEndpoint;
}
const modelId = String(model?.modelId || "").trim();
if (modelId) {
return modelId;
}
return String(model?.id || "").trim();
}
export function normalizeFailoverErrorMessage(error: unknown): string {
if (!error) return "";
if (typeof error === "string") return error;
if (error instanceof Error) return error.message || String(error);
try {
return JSON.stringify(error);
} catch {
return String(error);
}
}
export function shouldCooldownImageFallbackModel(errorMessage: string): boolean {
const msg = errorMessage.toLowerCase();
return (
msg.includes("model not found") ||
msg.includes("model_not_found") ||
msg.includes("no available channel for model") ||
msg.includes("unknown model") ||
msg.includes("invalid model") ||
msg.includes("api key is missing") ||
msg.includes("invalid api key") ||
msg.includes("unauthorized") ||
msg.includes("cannot convert argument to a bytestring") ||
msg.includes("model maintenance") ||
msg.includes("maintenance") ||
msg.includes("维护中") ||
msg.includes("维护") ||
msg.includes("service unavailable") ||
msg.includes("temporarily unavailable") ||
msg.includes("系统繁忙") ||
msg.includes("无可用渠道") ||
/(?:^|\D)(?:429|500|502|503|504)(?:\D|$)/.test(msg)
);
}
export function isImageFallbackModelCoolingDown(modelId: string): boolean {
const snapshot = imageFailoverModelCooldown.get(modelId);
if (!snapshot) return false;
if (snapshot.until <= Date.now()) {
imageFailoverModelCooldown.delete(modelId);
return false;
}
return true;
}
export function setImageFallbackModelCooldown(modelId: string, reason: string): void {
imageFailoverModelCooldown.set(modelId, {
until: Date.now() + IMAGE_FAILOVER_MODEL_COOLDOWN_MS,
reason: reason.slice(0, 500),
});
}
export function isImageFailoverExcludedModel(model: { excludeFromFailover?: boolean } | null | undefined) {
return Boolean(model?.excludeFromFailover);
}
export type ImageFallbackCandidate = {
model: any;
provider: any;
configured: boolean;
sameProvider: boolean;
nameSimilarity: number;
sameName: boolean;
};
const IMAGE_FAILOVER_SAME_NAME_THRESHOLD = 0.5;
function getModelRefKeys(item: { model?: any } | null | undefined) {
return [
String(item?.model?.id || "").trim(),
String(item?.model?.modelId || "").trim(),
].filter(Boolean);
}
function normalizeModelNameForSimilarity(value: unknown) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/[「」『』【】[\]()(){}]/g, " ")
.replace(/[_/|::,,.。+]+/g, "-")
.replace(/\s+/g, " ");
}
function tokenizeModelNameForSimilarity(value: unknown) {
const normalized = normalizeModelNameForSimilarity(value);
const compact = normalized.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "");
const tokens = new Set(
normalized
.split(/[^a-z0-9\u4e00-\u9fa5]+/g)
.map((item) => item.trim())
.filter((item) => item.length >= 2)
);
if (compact.length >= 2) {
tokens.add(compact);
for (let index = 0; index < compact.length - 1; index += 1) {
tokens.add(compact.slice(index, index + 2));
}
}
return tokens;
}
function scoreModelNameSimilarity(
source: { model?: any } | null | undefined,
candidate: { model?: any } | null | undefined
) {
const sourceNames = [
source?.model?.displayName,
source?.model?.modelId,
].filter(Boolean);
const candidateNames = [
candidate?.model?.displayName,
candidate?.model?.modelId,
].filter(Boolean);
let bestScore = 0;
for (const sourceName of sourceNames) {
const sourceTokens = tokenizeModelNameForSimilarity(sourceName);
if (sourceTokens.size === 0) continue;
for (const candidateName of candidateNames) {
const candidateTokens = tokenizeModelNameForSimilarity(candidateName);
if (candidateTokens.size === 0) continue;
let overlap = 0;
sourceTokens.forEach((token) => {
if (candidateTokens.has(token)) overlap += 1;
});
const score = overlap / Math.max(sourceTokens.size, candidateTokens.size);
bestScore = Math.max(bestScore, score);
}
}
return bestScore;
}
function resolveCurrentImageModelEntry(
allModels: Array<{ model: any; provider: any }>,
refs: string[]
) {
const normalizedRefs = new Set(refs.map((item) => String(item || "").trim()).filter(Boolean));
if (normalizedRefs.size === 0) return null;
return (
allModels.find((item) =>
getModelRefKeys(item).some((key) => normalizedRefs.has(key))
) || null
);
}
function messagePartHasImageInput(part: any) {
if (!part || typeof part !== "object") return false;
if (part.type === "file" || part.type === "image" || part.type === "image_url") {
return true;
}
if (typeof part.url === "string" && /^https?:\/\//i.test(part.url)) {
return true;
}
if (typeof part.image === "string" && part.image.trim()) {
return true;
}
if (typeof part.image_url?.url === "string" && part.image_url.url.trim()) {
return true;
}
return false;
}
function countImageInputPartsInMessage(message: any) {
const countParts = (parts: any[] | undefined) =>
Array.isArray(parts) ? parts.filter(messagePartHasImageInput).length : 0;
const contentCount = countParts(Array.isArray(message?.content) ? message.content : undefined);
if (contentCount > 0) return contentCount;
return countParts(Array.isArray(message?.parts) ? message.parts : undefined);
}
export function countChatMessageImageInputs(messages: ChatMessage[]) {
return messages.reduce(
(count, message) => count + countImageInputPartsInMessage(message),
0
);
}
export function chatMessagesHaveImageInput(messages: ChatMessage[]) {
return countChatMessageImageInputs(messages) > 0;
}
export function getImageFailoverConvertProvider(providerType?: string | null) {
switch (providerType) {
case "bfl-image":
case "ruxa-image":
case "fal-image":
case "snapgen-image":
case "openai-image":
case "bananaimage":
case "visionary":
case "atlascloud":
case "vip98k":
case "stepfun-image":
case "midjourney":
case "runway":
case "krea":
case "gemini":
case "huanke-gemini":
return providerType;
case "grsai-gpt-image":
return "gemini";
case "volcano-vision":
return "volcano";
default:
return "alibaba-image";
}
}
export function buildImageFallbackCandidates(
allModels: Array<{ model: any; provider: any }>,
options: {
currentProviderId?: string;
currentModelId?: string;
currentModelKey?: string;
configuredFallbackIds?: string[];
}
) {
const { currentProviderId, currentModelId, currentModelKey, configuredFallbackIds = [] } = options;
const currentEntry = resolveCurrentImageModelEntry(allModels, [
currentModelId || "",
currentModelKey || "",
]);
const configuredRefs = configuredFallbackIds
.map((item) => String(item || "").trim())
.filter(Boolean);
const byModelRef = new Map<string, { model: any; provider: any }>();
const isEligible = (item: { model: any; provider: any }, sameProvider: boolean) => {
if (
!item?.model?.supportsImageGen ||
!item?.model?.isEnabled ||
item?.model?.isSubModel ||
isImageFailoverExcludedModel(item?.model) ||
item?.model?.modelId === currentModelKey ||
item?.model?.id === currentModelId
) {
return false;
}
const providerType = String(item?.provider?.type || "");
if (!sameProvider && !IMAGE_FAILOVER_PROVIDER_ALLOWLIST.has(providerType)) {
return false;
}
if (isImageFallbackModelCoolingDown(item.model.id)) {
const cooldown = imageFailoverModelCooldown.get(item.model.id);
chatApiLogger.info(`[Chat API] ⏭ Skip configured image fallback on cooldown: ${item.model.displayName} (${providerType}) - ${cooldown?.reason || "cooldown"}`);
return false;
}
return true;
};
allModels.forEach((item) => {
if (!item?.model) return;
byModelRef.set(String(item.model.id), item);
if (item.model.modelId) {
byModelRef.set(String(item.model.modelId), item);
}
});
const candidates: ImageFallbackCandidate[] = [];
const seenModelIds = new Set<string>();
const candidateOrder = new Map<string, number>();
const pushCandidate = (item: { model: any; provider: any } | undefined, configured: boolean) => {
if (!item?.model) return;
const sameProvider = Boolean(currentProviderId && item.provider?.id === currentProviderId);
if (!isEligible(item, sameProvider)) return;
const modelId = String(item.model.id || "");
if (!modelId || seenModelIds.has(modelId)) return;
seenModelIds.add(modelId);
candidateOrder.set(modelId, candidateOrder.size);
const nameSimilarity = scoreModelNameSimilarity(currentEntry, item);
candidates.push({
model: item.model,
provider: item.provider,
configured,
sameProvider,
nameSimilarity,
sameName: nameSimilarity >= IMAGE_FAILOVER_SAME_NAME_THRESHOLD,
});
};
configuredRefs.forEach((modelRef) => {
pushCandidate(byModelRef.get(modelRef), true);
});
if (currentProviderId) {
allModels
.filter((item) => item.provider?.id === currentProviderId)
.forEach((item) => pushCandidate(item, false));
}
if (currentEntry) {
allModels
.filter((item) => scoreModelNameSimilarity(currentEntry, item) >= IMAGE_FAILOVER_SAME_NAME_THRESHOLD)
.forEach((item) => pushCandidate(item, false));
}
return candidates.sort((left, right) => {
if (left.sameName !== right.sameName) return left.sameName ? -1 : 1;
if (left.configured !== right.configured) return left.configured ? -1 : 1;
if (left.sameProvider !== right.sameProvider) return left.sameProvider ? -1 : 1;
if (right.nameSimilarity !== left.nameSimilarity) {
return right.nameSimilarity - left.nameSimilarity;
}
return (
(candidateOrder.get(String(left.model.id || "")) ?? 0) -
(candidateOrder.get(String(right.model.id || "")) ?? 0)
);
});
}
export async function generateImageWithFailoverCandidate(params: {
candidate: ImageFallbackCandidate;
messages: ChatMessage[];
imageParams: any;
fallbackImageParams: any;
}) {
const { candidate, messages, imageParams, fallbackImageParams } = params;
const providerType = String(candidate.provider?.type || "");
const candidateRuntimeModelId = resolveImageModelRuntimeId(candidate.model);
if (
chatMessagesHaveImageInput(messages) &&
modelLacksReferenceImageInputSupport(candidate.model, candidate.provider)
) {
throw new Error(
`备用模型不支持参考图/图生图输入:${candidate.model.displayName || candidate.model.id}`
);
}
if (
providerType === "fal-image" &&
isFalGptImage2EditModelId(candidateRuntimeModelId)
) {
throw new Error("FAL GPT Image 2 Edit 仅支持异步队列调用,已跳过同步故障迁移");
}
const candidateLanguageModel = await getLanguageModelById(
candidate.model.id,
candidateRuntimeModelId
);
if (!candidateLanguageModel || typeof (candidateLanguageModel as any).doGenerate !== "function") {
throw new Error(`备用模型不支持当前生图故障迁移调用:${candidate.model.displayName || candidate.model.id}`);
}
const candidateMessages = await convertMessagesWithImages(
messages,
getImageFailoverConvertProvider(providerType)
);
const effectiveImageParams = imageParams || fallbackImageParams;
const imageInputCount = countChatMessageImageInputs(messages);
chatApiLogger.info("[Chat API] Image failover candidate params:", {
model: candidate.model?.modelId || candidate.model?.id,
providerType,
aspectRatio: effectiveImageParams?.aspectRatio,
imageSize: effectiveImageParams?.imageSize,
imageCount: effectiveImageParams?.imageCount,
hasImageInput: imageInputCount > 0,
imageInputCount,
multiImageInput: imageInputCount > 1,
});
const candidateResult = await (candidateLanguageModel as any).doGenerate({
messages: candidateMessages,
imageParams: effectiveImageParams,
imageGenConfig: candidate.model.imageGenConfig || null,
modelDisplayName: candidate.model.displayName,
overrideModelId: candidateRuntimeModelId || candidate.model.modelId,
});
if (candidateResult?.finishReason === "error") {
throw new Error(candidateResult.text || "图片生成失败");
}
return {
text: candidateResult.text || "",
usedModelId: String(candidate.model.modelId || candidate.model.id || ""),
displayName: String(candidate.model.displayName || candidate.model.modelId || candidate.model.id || ""),
};
}
export function getImageCrossProviderCandidates(
allModels: any[],
options: {
currentModelId: string;
currentModelKey?: string;
currentProviderId?: string;
providerPriority: Record<string, number>;
}
) {
const currentEntry = resolveCurrentImageModelEntry(allModels, [
options.currentModelId,
options.currentModelKey || "",
]);
return allModels
.filter((m) => {
if (
!m?.model?.supportsImageGen ||
!m?.model?.isEnabled ||
m?.model?.isSubModel ||
isImageFailoverExcludedModel(m?.model)
) {
return false;
}
if (!IMAGE_FAILOVER_PROVIDER_ALLOWLIST.has(String(m?.provider?.type || ""))) {
return false;
}
if (m.model.id === options.currentModelId) return false;
if (options.currentProviderId && m.provider.id === options.currentProviderId) return false;
if (isImageFallbackModelCoolingDown(m.model.id)) {
const cooldown = imageFailoverModelCooldown.get(m.model.id);
chatApiLogger.info(`[Chat API] ⏭ Skip image failover model on cooldown: ${m.model.displayName} (${m.provider.type}) - ${cooldown?.reason || "cooldown"}`);
return false;
}
return true;
})
.map((item, index) => {
const nameSimilarity = scoreModelNameSimilarity(currentEntry, item);
return {
...item,
__imageFailoverIndex: index,
__imageFailoverNameSimilarity: nameSimilarity,
__imageFailoverSameName:
nameSimilarity >= IMAGE_FAILOVER_SAME_NAME_THRESHOLD,
};
})
.sort((a, b) => {
if (a.__imageFailoverSameName !== b.__imageFailoverSameName) {
return a.__imageFailoverSameName ? -1 : 1;
}
if (b.__imageFailoverNameSimilarity !== a.__imageFailoverNameSimilarity) {
return b.__imageFailoverNameSimilarity - a.__imageFailoverNameSimilarity;
}
const left = options.providerPriority[a.provider.type as string] ?? 99;
const right = options.providerPriority[b.provider.type as string] ?? 99;
if (left !== right) return left - right;
const sortOrderDiff = (a.model.sortOrder ?? 0) - (b.model.sortOrder ?? 0);
if (sortOrderDiff !== 0) return sortOrderDiff;
return (a.__imageFailoverIndex ?? 0) - (b.__imageFailoverIndex ?? 0);
})
.map((item) => {
const {
__imageFailoverIndex,
__imageFailoverNameSimilarity,
__imageFailoverSameName,
...candidate
} = item;
return candidate;
});
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,173 @@
import { createLogger } from "@/lib/observability/logger";
import {
analyzeOptimizedPromptMerge,
buildOptimizedPromptPreview,
} from "@/lib/ai/runtime/optimized-prompt";
import type { ChatMessage } from "@/lib/types";
const chatApiLogger = createLogger("chat.messagePreparation");
/**
* 将图片 URL 转换为 base64 格式
* 用于将 S3 URL 的图片转换为 base64 发送给 Gemini API
*/
export async function convertImageUrlToBase64(url: string): Promise<string> {
try {
chatApiLogger.info("[Image Convert] Fetching image from:", url);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch image: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const base64 = buffer.toString("base64");
// 获取 MIME 类型
const contentType = response.headers.get("content-type") || "image/jpeg";
const dataUrl = `data:${contentType};base64,${base64}`;
chatApiLogger.info("[Image Convert] Converted to base64, size:", base64.length);
return dataUrl;
} catch (error) {
chatApiLogger.error("[Image Convert] Failed:", error);
throw error;
}
}
/**
* 检测消息中是否包含用户标注过的图片(文件名以 annotated- 开头)
*/
export function hasAnnotatedImage(messages: ChatMessage[]): boolean {
for (const message of messages) {
for (const part of message.parts) {
if (part.type === "file" && "name" in part && typeof part.name === "string") {
if (part.name.startsWith("annotated-")) {
return true;
}
}
}
}
return false;
}
/**
* 标注图片的系统提示词(用户不可见,后台自动添加)
*/
export const ANNOTATION_SYSTEM_PROMPT =
"【重要】用户提供的图片上有标注线条(如箭头、圆圈、画笔痕迹或文字说明),这些标注是用户用来指示需要修改的区域或方向。请仔细理解标注的含义,按照标注指示进行编辑,但在生成的新图片中不要保留这些标注线条,输出干净的编辑结果。";
export function applyHiddenPromptToMessages(
messages: ChatMessage[],
hiddenPrompt?: string,
): ChatMessage[] {
if (!hiddenPrompt || !hiddenPrompt.trim()) return messages;
const cloned = messages.map((msg) => ({
...msg,
parts: msg.parts.map((part) => ({ ...part })),
}));
for (let i = cloned.length - 1; i >= 0; i--) {
const msg = cloned[i];
if (msg.role !== "user") continue;
const textPart = msg.parts.find((part) => part.type === "text") as
| { type: "text"; text: string }
| undefined;
if (textPart) {
textPart.text = `${hiddenPrompt}\n\n${textPart.text}`.trim();
} else {
msg.parts.unshift({ type: "text", text: hiddenPrompt });
}
break;
}
return cloned;
}
export function applyOptimizedPromptToLatestUserMessage(
messages: ChatMessage[],
optimizedPrompt?: string,
hiddenPromptPrefix = "",
): ChatMessage[] {
const prompt = optimizedPrompt?.trim();
if (!prompt) return messages;
const cloned = messages.map((msg) => ({
...msg,
parts: msg.parts.map((part) => ({ ...part })),
}));
for (let i = cloned.length - 1; i >= 0; i -= 1) {
const msg = cloned[i];
if (msg.role !== "user") continue;
const textPart = msg.parts.find(
(part): part is { type: "text"; text: string } => part.type === "text",
);
const originalText = textPart?.text?.trim() || "";
const { normalizedOriginal, hasOriginalDemand, hasOriginalInOptimized } =
analyzeOptimizedPromptMerge(originalText, prompt, hiddenPromptPrefix);
const finalPrompt =
hasOriginalDemand && !hasOriginalInOptimized
? [
hiddenPromptPrefix.trim(),
`【核心目标(必须优先满足)】${normalizedOriginal}`,
`【执行优化细节】${prompt}`,
]
.filter(Boolean)
.join("\n\n")
.trim()
: `${hiddenPromptPrefix}${prompt}`.trim();
chatApiLogger.info("[Prompt Merge] Final image prompt prepared:", {
original: normalizedOriginal.substring(0, 120),
optimized: prompt.substring(0, 120),
merged: finalPrompt.substring(0, 180),
forcePreserveOriginal: hasOriginalDemand && !hasOriginalInOptimized,
});
if (textPart) {
textPart.text = finalPrompt;
} else {
msg.parts.unshift({ type: "text", text: finalPrompt });
}
break;
}
return cloned;
}
export function injectOptimizedPromptSection(params: {
message?: string;
originalText: string;
optimizedPrompt?: string;
hiddenPromptPrefix?: string;
}): string | undefined {
const { message, originalText, optimizedPrompt, hiddenPromptPrefix } = params;
if (!message?.trim()) return message;
const preview = buildOptimizedPromptPreview({
originalText,
optimizedPrompt,
hiddenPromptPrefix,
});
if (!preview) return message;
const optimizedPromptSection = `【优化后提示词】\n${preview}`;
const optimizedSectionRegex =
/(?:###\s*优化后提示词|【\s*优化后提示词\s*】)[\s\S]*?(?=\n(?:###\s*|【\s*[^】]+\s*】)|$)/;
if (optimizedSectionRegex.test(message)) {
return message.replace(optimizedSectionRegex, optimizedPromptSection);
}
const executionSectionRegex =
/(?:###\s*执行计划|【\s*执行计划\s*】)[\s\S]*?(?=\n(?:###\s*|【\s*[^】]+\s*】)|$)/;
if (executionSectionRegex.test(message)) {
return message.replace(executionSectionRegex, optimizedPromptSection);
}
const outputSectionRegex = /(?:###\s*输出设置|【\s*输出设置\s*】)/;
if (outputSectionRegex.test(message)) {
return message.replace(outputSectionRegex, `${optimizedPromptSection}\n$&`);
}
return `${message.trim()}\n\n${optimizedPromptSection}`;
}

View File

@ -0,0 +1,43 @@
export function resolveModelDisplayNameByModelId(
models: Array<{ model: any; provider?: any }> | undefined,
modelId: string | undefined,
): string {
if (!modelId || !models?.length) return "";
const match = models.find(
(item) => item.model.modelId === modelId || item.model.id === modelId,
);
return String(match?.model?.displayName || "");
}
export function resolveModelEntryByRuntimeModelId(
models: Array<{ model: any; provider?: any }> | undefined,
modelId: string | undefined,
fallbackRef?: string,
) {
const candidates = [modelId, fallbackRef]
.map((item) => String(item || "").trim())
.filter(Boolean);
if (!candidates.length || !models?.length) return null;
return (
models.find((item) =>
candidates.some(
(candidate) => item.model.modelId === candidate || item.model.id === candidate,
),
) || null
);
}
export function resolveModelRefByRuntimeModelId(
models: Array<{ model: any; provider?: any }> | undefined,
modelId: string | undefined,
fallbackRef?: string,
): string {
const normalizedModelId = String(modelId || "").trim();
if (!normalizedModelId) return fallbackRef || "";
const match = models?.find(
(item) => item.model.modelId === normalizedModelId || item.model.id === normalizedModelId,
);
return String(match?.model?.id || fallbackRef || normalizedModelId || "");
}

View File

@ -0,0 +1,24 @@
export function normalizeAssistantPartsForPersistence(
parts: Array<Record<string, any>> | undefined
) {
const safeParts = Array.isArray(parts) ? parts.filter(Boolean) : [];
return safeParts.filter((part) => {
const partType = String(part?.type || "").trim();
if (!partType) return false;
if (
partType === "data-keepalive" ||
partType === "step-start" ||
partType === "step-finish"
) {
return false;
}
return true;
});
}
export function bufferToArrayBuffer(buffer: Buffer): ArrayBuffer {
return buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength
) as ArrayBuffer;
}

View File

@ -0,0 +1,612 @@
import { createLogger } from "@/lib/observability/logger";
import { JsonToSseTransformStream } from "ai";
import { extractImageUrlsFromText } from "@/lib/image-url";
import type {
ResolvedChatMediaInput,
ResolvedImageRef,
ResolvedTargetAssets,
} from "@/lib/ai/context-memory/types";
import type { CanvasViewOperationPlan } from "@/types/canvas/agent-protocol";
import type { CanvasCapabilityClientCommand } from "@/lib/canvas/capabilities/client-command";
import { generateUUID } from "@/lib/utils";
const chatApiLogger = createLogger("chat.stream");
export type StreamWriter = {
write: (chunk: any) => void;
};
export function safeWriteStreamChunk(
writer: StreamWriter,
chunk: any,
label: string
): boolean {
try {
writer.write(chunk);
return true;
} catch (error) {
chatApiLogger.warn(`[Chat API] Stream write skipped (${label})`, error);
return false;
}
}
export function writeAssistantTextChunk(
writer: StreamWriter,
text: string,
label: string,
messageId = generateUUID()
) {
safeWriteStreamChunk(
writer,
{
type: "data-appendMessage",
data: JSON.stringify({
id: messageId,
role: "assistant",
parts: [{ type: "text", text }],
}),
},
label
);
return messageId;
}
export function writeStructuredDataChunk(
writer: StreamWriter,
type: string,
data: unknown,
label: string
) {
safeWriteStreamChunk(
writer,
{
type,
data: JSON.stringify(data),
},
label
);
}
function normalizeStreamIdList(value: unknown) {
return Array.isArray(value)
? value.map((id) => String(id || "").trim()).filter(Boolean)
: [];
}
function normalizeStreamMediaUrls(value: unknown) {
return Array.isArray(value)
? value.map((url) => String(url || "").trim())
: [];
}
function isValidStreamMediaUrl(url: string) {
return /^https?:\/\//i.test(url);
}
export function normalizeCanvasMediaStreamPayload(params: {
imageUrls?: unknown;
videoUrls?: unknown;
mediaItemIds?: unknown;
handledCanvasMediaItemIds?: unknown;
}) {
const rawImageUrls = normalizeStreamMediaUrls(params.imageUrls);
const rawVideoUrls = normalizeStreamMediaUrls(params.videoUrls);
const rawEntries = [
...rawImageUrls.map((url, index) => ({
type: "image" as const,
url,
originalIndex: index,
})),
...rawVideoUrls.map((url, index) => ({
type: "video" as const,
url,
originalIndex: rawImageUrls.length + index,
})),
];
const rawMediaItemIds = normalizeStreamIdList(params.mediaItemIds);
const validEntries = rawEntries.filter((entry) =>
isValidStreamMediaUrl(entry.url)
);
const idByValidIndex = new Map<number, string>();
if (rawMediaItemIds.length === rawEntries.length) {
validEntries.forEach((entry, validIndex) => {
const id = rawMediaItemIds[entry.originalIndex];
if (id) idByValidIndex.set(validIndex, id);
});
} else if (rawMediaItemIds.length === validEntries.length) {
rawMediaItemIds.forEach((id, index) => {
if (id) idByValidIndex.set(index, id);
});
}
const dedupedEntries: Array<{
type: "image" | "video";
url: string;
mediaItemId?: string;
}> = [];
const seenMediaKeys = new Set<string>();
validEntries.forEach((entry, validIndex) => {
const key = `${entry.type}:${entry.url}`;
if (seenMediaKeys.has(key)) return;
seenMediaKeys.add(key);
dedupedEntries.push({
type: entry.type,
url: entry.url,
mediaItemId: idByValidIndex.get(validIndex),
});
});
if (
dedupedEntries.length > 0 &&
dedupedEntries.some((entry) => !entry.mediaItemId) &&
rawMediaItemIds.length === dedupedEntries.length
) {
dedupedEntries.forEach((entry, index) => {
entry.mediaItemId = rawMediaItemIds[index];
});
}
const mediaItemIds = dedupedEntries.every((entry) => entry.mediaItemId)
? dedupedEntries.map((entry) => entry.mediaItemId!)
: [];
return {
imageUrls: dedupedEntries
.filter((entry) => entry.type === "image")
.map((entry) => entry.url),
videoUrls: dedupedEntries
.filter((entry) => entry.type === "video")
.map((entry) => entry.url),
mediaItemIds,
handledCanvasMediaItemIds: normalizeStreamIdList(
params.handledCanvasMediaItemIds
),
};
}
export function writeProtocolDebugChunk(
writer: StreamWriter,
enabled: boolean,
data: unknown,
label: string
) {
if (!enabled) return;
writeStructuredDataChunk(writer, "data-protocolDebug", data, label);
}
function normalizeImageLoadingProviderType(providerType?: string | null): string {
const normalized = String(providerType || "").trim().toLowerCase();
if (normalized === "gemini-image" || normalized === "huanke-gemini") {
return "gemini";
}
if (
normalized === "alibaba-image" ||
normalized === "bfl-image" ||
normalized === "ruxa-image" ||
normalized === "fal-image" ||
normalized === "snapgen-image" ||
normalized === "openai-image" ||
normalized === "bananaimage" ||
normalized === "visionary" ||
normalized === "atlascloud" ||
normalized === "vip98k" ||
normalized === "stepfun-image" ||
normalized === "midjourney" ||
normalized === "runway" ||
normalized === "krea" ||
normalized === "volcano-vision" ||
normalized === "gemini"
) {
return normalized;
}
return "alibaba-image";
}
export function writeImageGenerationStatusChunk(
writer: StreamWriter,
params: {
phase: "queued" | "processing" | "finalizing" | "failed" | "completed";
providerType?: string | null;
modelName?: string | null;
imageCount?: number | null;
queuePosition?: number | null;
sameModelRunningCount?: number | null;
concurrencyLimit?: number | null;
estimatedWaitMs?: number | null;
},
label: string
) {
writeStructuredDataChunk(
writer,
"data-imageGenerationStatus",
{
phase: params.phase,
providerType: normalizeImageLoadingProviderType(params.providerType),
modelName: String(params.modelName || "").trim() || undefined,
imageCount:
Number.isFinite(Number(params.imageCount)) && Number(params.imageCount) > 0
? Number(params.imageCount)
: undefined,
queuePosition:
Number.isFinite(Number(params.queuePosition)) &&
Number(params.queuePosition) >= 0
? Number(params.queuePosition)
: undefined,
sameModelRunningCount:
Number.isFinite(Number(params.sameModelRunningCount)) &&
Number(params.sameModelRunningCount) >= 0
? Number(params.sameModelRunningCount)
: undefined,
concurrencyLimit:
Number.isFinite(Number(params.concurrencyLimit)) &&
Number(params.concurrencyLimit) > 0
? Number(params.concurrencyLimit)
: undefined,
estimatedWaitMs:
Number.isFinite(Number(params.estimatedWaitMs)) &&
Number(params.estimatedWaitMs) >= 0
? Number(params.estimatedWaitMs)
: undefined,
},
label
);
}
export type AgentProgressStreamPayload = {
stage: "planning" | "clarify" | "executing" | "completed" | "failed";
currentStep:
| "lock_target"
| "analyze"
| "select_tool"
| "execute"
| "finalize";
title?: string;
message?: string;
toolName?: string;
capability?: string;
};
export function writeAgentProgressChunk(
writer: StreamWriter,
payload: AgentProgressStreamPayload,
label: string
) {
writeStructuredDataChunk(
writer,
"data-agentProgress",
{
stage: payload.stage,
currentStep: payload.currentStep,
title: String(payload.title || "").trim() || undefined,
message: String(payload.message || "").trim() || undefined,
toolName: String(payload.toolName || "").trim() || undefined,
capability: String(payload.capability || "").trim() || undefined,
},
label
);
}
export function writeCanvasMediaChunk(
writer: StreamWriter,
params: {
messageId: string;
text?: string;
imageUrls?: string[];
videoUrls?: string[];
prompt?: string | null;
sourceImageUrls?: string[] | null;
agentPresetId?: string | null;
agentPresetName?: string | null;
agentCategoryId?: string | null;
agentCategoryName?: string | null;
agentExecutionSnapshotId?: string | null;
mediaItemIds?: string[] | null;
handledCanvasMediaItemIds?: string[] | null;
},
label: string
) {
const inputImageUrls = Array.isArray(params.imageUrls)
? params.imageUrls
: extractImageUrlsFromText(String(params.text || ""));
const normalizedPayload = normalizeCanvasMediaStreamPayload({
imageUrls: inputImageUrls,
videoUrls: params.videoUrls,
mediaItemIds: params.mediaItemIds,
handledCanvasMediaItemIds: params.handledCanvasMediaItemIds,
});
if (
normalizedPayload.imageUrls.length === 0 &&
normalizedPayload.videoUrls.length === 0
) {
return;
}
writeStructuredDataChunk(
writer,
"data-canvasMedia",
{
messageId: params.messageId,
imageUrls: normalizedPayload.imageUrls,
videoUrls: normalizedPayload.videoUrls,
prompt: String(params.prompt || "").trim() || undefined,
sourceImageUrls: Array.isArray(params.sourceImageUrls)
? params.sourceImageUrls
.map((url) => String(url || "").trim())
.filter((url) => /^https?:\/\//i.test(url))
: [],
agentPresetId: String(params.agentPresetId || "").trim() || undefined,
agentPresetName: String(params.agentPresetName || "").trim() || undefined,
agentCategoryId: String(params.agentCategoryId || "").trim() || undefined,
agentCategoryName: String(params.agentCategoryName || "").trim() || undefined,
agentExecutionSnapshotId:
String(params.agentExecutionSnapshotId || "").trim() || undefined,
mediaItemIds: normalizedPayload.mediaItemIds,
handledCanvasMediaItemIds: normalizedPayload.handledCanvasMediaItemIds,
},
label
);
}
export function writeCanvasOperationChunk(
writer: StreamWriter,
params: {
projectId: string;
operationId: string;
operation: string;
status?: "applied" | "failed" | "shadowed";
createdNodeIds: string[];
createdNodeIdsByMediaItemId?: Record<string, string>;
updatedNodeIds?: string[];
createdEdgeIds?: string[];
canvasRevision?: string;
anchorNodeId?: string | null;
handledCanvasMediaItemIds?: string[];
mediaItemIds?: string[];
errorCode?: string;
errorMessage?: string;
fallbackToCanvasMedia?: boolean;
message?: string;
},
label: string
) {
writeStructuredDataChunk(
writer,
"data-canvasOperation",
{
projectId: params.projectId,
operationId: params.operationId,
operation: params.operation,
status: params.status || "applied",
createdNodeIds: params.createdNodeIds || [],
createdNodeIdsByMediaItemId: params.createdNodeIdsByMediaItemId || {},
updatedNodeIds: params.updatedNodeIds || [],
createdEdgeIds: params.createdEdgeIds || [],
canvasRevision: params.canvasRevision,
anchorNodeId: params.anchorNodeId || undefined,
handledCanvasMediaItemIds:
params.status && params.status !== "applied"
? []
: normalizeStreamIdList(params.handledCanvasMediaItemIds),
mediaItemIds: normalizeStreamIdList(params.mediaItemIds),
errorCode: params.errorCode,
errorMessage: params.errorMessage,
fallbackToCanvasMedia: params.fallbackToCanvasMedia,
message: params.message,
},
label
);
}
export function writeCanvasViewOperationChunk(
writer: StreamWriter,
params: CanvasViewOperationPlan,
label: string
) {
writeStructuredDataChunk(
writer,
"data-canvasViewOperation",
{
version: 1,
viewOperationId: params.viewOperationId,
operation: params.operation,
projectId: params.projectId,
nodeIds: params.nodeIds || [],
anchorNodeId: params.anchorNodeId,
zoom: params.zoom,
padding: params.padding,
durationMs: params.durationMs,
persist: false,
center: params.center,
reason: params.reason,
},
label
);
}
export function writeCanvasCapabilityCommandChunk(
writer: StreamWriter,
command: CanvasCapabilityClientCommand,
label: string
) {
writeStructuredDataChunk(
writer,
"data-canvasCapabilityCommand",
command,
label
);
}
type AgentTargetLockRefPreview = {
url: string;
title?: string;
nodeId?: string;
assetId?: string;
source: ResolvedImageRef["source"];
role: ResolvedImageRef["role"];
isSelected?: boolean;
positionLabel?: string;
};
export type AgentTargetLockStreamPayload = {
stage: "locked" | "clarify" | "unresolved";
resolutionMode: ResolvedTargetAssets["resolutionMode"];
confidence: number;
reason: string;
needsClarification: boolean;
clarificationMessage?: string;
targetCount: number;
candidateCount: number;
targets: AgentTargetLockRefPreview[];
candidates: AgentTargetLockRefPreview[];
};
function sortTargetLockRefsForCanvasOrdering(refs: ResolvedImageRef[]) {
return [...refs].sort((left, right) => {
const leftY = Number(left.y ?? 0);
const rightY = Number(right.y ?? 0);
if (Math.abs(leftY - rightY) > 12) {
return leftY - rightY;
}
return Number(left.x ?? 0) - Number(right.x ?? 0);
});
}
function getTargetLockPositionLabel(
ref: ResolvedImageRef,
refs: ResolvedImageRef[]
): string {
if (refs.length <= 1) return "中间";
const xs = refs.map((item) => Number(item.x ?? 0));
const ys = refs.map((item) => Number(item.y ?? 0));
const minX = Math.min(...xs);
const maxX = Math.max(...xs);
const minY = Math.min(...ys);
const maxY = Math.max(...ys);
const spanX = Math.max(1, maxX - minX);
const spanY = Math.max(1, maxY - minY);
const relX = (Number(ref.x ?? 0) - minX) / spanX;
const relY = (Number(ref.y ?? 0) - minY) / spanY;
const horizontal = relX <= 0.33 ? "左" : relX >= 0.66 ? "右" : "中";
const vertical = relY <= 0.33 ? "上" : relY >= 0.66 ? "下" : "中";
if (horizontal === "中" && vertical === "中") return "中间";
return `${vertical}${horizontal}`;
}
function serializeAgentTargetLockRefs(refs: ResolvedImageRef[]) {
const orderedRefs = sortTargetLockRefsForCanvasOrdering(refs).slice(0, 4);
return orderedRefs.map((ref) => ({
url: ref.url,
title: String(ref.title || "").trim() || undefined,
nodeId: ref.nodeId,
assetId: ref.assetId,
source: ref.source,
role: ref.role,
isSelected: ref.isSelected === true,
positionLabel: getTargetLockPositionLabel(ref, orderedRefs),
})) satisfies AgentTargetLockRefPreview[];
}
export function buildAgentTargetLockPayload(params: {
requestTargetResolution: ResolvedTargetAssets;
requestResolvedMediaInput: ResolvedChatMediaInput;
needsClarification: boolean;
clarificationMessage?: string;
}): AgentTargetLockStreamPayload {
const stage = params.needsClarification
? "clarify"
: params.requestTargetResolution.targetRefs.length > 0
? "locked"
: "unresolved";
return {
stage,
resolutionMode: params.requestTargetResolution.resolutionMode,
confidence: Number(params.requestTargetResolution.confidence || 0),
reason: params.requestTargetResolution.reason,
needsClarification: params.needsClarification,
clarificationMessage: params.clarificationMessage?.trim() || undefined,
targetCount: params.requestTargetResolution.targetRefs.length,
candidateCount: params.requestTargetResolution.candidateRefs.length,
targets: serializeAgentTargetLockRefs(params.requestTargetResolution.targetRefs),
candidates: serializeAgentTargetLockRefs(
params.requestTargetResolution.candidateRefs.length > 0
? params.requestTargetResolution.candidateRefs
: params.requestResolvedMediaInput.imageRefs
),
};
}
export function writeAgentTargetLockChunk(
writer: StreamWriter,
payload: AgentTargetLockStreamPayload,
label: string
) {
writeStructuredDataChunk(writer, "data-agentTargetLock", payload, label);
}
export function createSseResponse(stream: ReadableStream) {
return new Response(stream.pipeThrough(new JsonToSseTransformStream()), {
headers: {
"Content-Type": "text/event-stream; charset=utf-8",
"Cache-Control": "no-cache, no-transform",
Connection: "keep-alive",
"X-Accel-Buffering": "no",
},
});
}
export function startStreamKeepalive(
writer: StreamWriter,
label: string,
intervalMs = 15_000
) {
const timer = setInterval(() => {
safeWriteStreamChunk(
writer,
{
type: "data-keepalive",
data: JSON.stringify({ ts: Date.now(), label }),
},
`${label}:keepalive`
);
}, intervalMs);
return () => clearInterval(timer);
}
export async function runBoundedParallel<T>(
items: T[],
workerCount: number,
worker: (item: T, index: number) => Promise<void>
) {
const maxWorkers = Math.max(1, Math.min(workerCount, items.length));
let cursor = 0;
let firstError: unknown = null;
const workers = Array.from({ length: maxWorkers }, async () => {
while (cursor < items.length) {
const index = cursor;
cursor += 1;
if (firstError) {
return;
}
try {
await worker(items[index]!, index);
} catch (error) {
firstError = firstError || error;
}
}
});
await Promise.all(workers);
if (firstError) {
throw firstError;
}
}

View File

@ -0,0 +1,279 @@
import { createLogger } from "@/lib/observability/logger";
import { after } from "next/server";
import {
getAiModelById,
getEnabledAiModelsWithProvider,
updateMessageById,
} from "@/lib/db/queries";
import {
isImageGenerationModelDescriptor,
isVideoModelDescriptor,
} from "@/lib/ai/model-classification";
import type { StoryboardModelCandidate } from "@/lib/video-agent/types";
const chatApiLogger = createLogger("chat.videoModels");
export type VideoModeType =
| "text-to-video"
| "first-frame"
| "first-last-frame"
| "reference-images";
export function isVideoProviderType(
providerType?: string | null,
model?: {
modelId?: string | null;
capabilities?: unknown;
supportsImageGen?: boolean | null;
},
): boolean {
return isVideoModelDescriptor({
providerType,
modelId: model?.modelId,
capabilities: model?.capabilities,
supportsImageGen: model?.supportsImageGen,
});
}
export function isImageGenerationModelType(
providerType?: string | null,
model?: {
modelId?: string | null;
capabilities?: unknown;
supportsImageGen?: boolean | null;
},
): boolean {
return isImageGenerationModelDescriptor({
providerType,
modelId: model?.modelId,
capabilities: model?.capabilities,
supportsImageGen: model?.supportsImageGen,
});
}
export function inferSupportedVideoModesByModelId(modelId: string): VideoModeType[] {
const normalized = modelId.toLowerCase();
if (
normalized === "gen4.5" ||
normalized === "gen4_turbo" ||
normalized === "gen4_aleph" ||
normalized === "gen3a_turbo" ||
normalized === "veo3" ||
normalized === "veo3.1" ||
normalized === "veo3.1_fast"
) {
return ["text-to-video", "first-frame"];
}
if (normalized.includes("lite-t2v")) {
return ["text-to-video"];
}
if (normalized.includes("lite-i2v")) {
return ["first-frame", "first-last-frame", "reference-images"];
}
if (normalized.includes("pro-fast")) {
return ["text-to-video", "first-frame"];
}
if (normalized.includes("-pro-") || normalized.endsWith("-pro")) {
return ["text-to-video", "first-frame", "first-last-frame"];
}
return ["text-to-video", "first-frame", "first-last-frame", "reference-images"];
}
export function getSupportedVideoModesForModel(model: any): VideoModeType[] {
const capabilityModes = (((model?.capabilities as any)?.supportedModes ?? []) as string[]).filter(
(mode): mode is VideoModeType =>
mode === "text-to-video" ||
mode === "first-frame" ||
mode === "first-last-frame" ||
mode === "reference-images",
);
if (capabilityModes.length > 0) {
return capabilityModes;
}
const modelId = String(model?.modelId || "");
return inferSupportedVideoModesByModelId(modelId);
}
export function scoreVideoSubModelForMode(modelId: string, mode: VideoModeType): number {
const normalized = modelId.toLowerCase();
const isProFast = normalized.includes("pro-fast");
const isLiteT2V = normalized.includes("lite-t2v");
const isLiteI2V = normalized.includes("lite-i2v");
const isPro = (normalized.includes("-pro-") || normalized.endsWith("-pro")) && !isProFast;
const isV15Pro = normalized.includes("seedance-1-5-pro");
switch (mode) {
case "first-last-frame":
if (isV15Pro) return 150;
if (isPro) return 120;
if (isLiteI2V) return 90;
if (isProFast) return -1000;
if (isLiteT2V) return -1000;
return 10;
case "first-frame":
if (isV15Pro) return 140;
if (isProFast) return 130;
if (isPro) return 120;
if (isLiteI2V) return 100;
if (isLiteT2V) return 20;
return 10;
case "reference-images":
if (isV15Pro) return 150;
if (isLiteI2V) return 140;
if (isPro) return 110;
if (isProFast || isLiteT2V) return -1000;
return 10;
case "text-to-video":
default:
if (isV15Pro) return 140;
if (isLiteT2V) return 130;
if (isProFast) return 120;
if (isPro) return 100;
if (isLiteI2V) return 80;
return 10;
}
}
export function selectBestVideoSubModelForMode(
subModels: Array<{ model: any }>,
mode: VideoModeType,
): { model: any } | null {
const compatible = subModels.filter(({ model }) => {
const supportedModes = getSupportedVideoModesForModel(model);
return supportedModes.includes(mode);
});
const candidates = compatible.length > 0 ? compatible : subModels;
if (candidates.length === 0) return null;
return [...candidates].sort((a, b) => {
const scoreA = scoreVideoSubModelForMode(String(a.model?.modelId || ""), mode);
const scoreB = scoreVideoSubModelForMode(String(b.model?.modelId || ""), mode);
return scoreB - scoreA;
})[0];
}
export async function resolveStoryboardModelCandidates(
selectedModelRef: string,
): Promise<StoryboardModelCandidate[]> {
const selectedModel = await getAiModelById({ id: selectedModelRef }).catch(() => undefined);
if (!selectedModel) return [];
const configuredFallbackIds = [
selectedModel.fallbackModelId1,
selectedModel.fallbackModelId2,
].filter((item): item is string => Boolean(String(item || "").trim()));
const allModels = await getEnabledAiModelsWithProvider(true, true).catch(() => []);
const sameProvider = allModels
.filter(
({ model, provider }: any) =>
provider?.id === selectedModel.providerId && model?.id !== selectedModel.id && model?.isEnabled,
)
.map(({ model }: any) => String(model?.id || "").trim())
.filter(Boolean);
const candidateIds = Array.from(new Set([selectedModel.id, ...configuredFallbackIds, ...sameProvider]));
const byModelRef = new Map<string, any>();
allModels
.map(({ model }: any) => model)
.filter(Boolean)
.forEach((model: any) => {
byModelRef.set(String(model.id), model);
if (model.modelId) {
byModelRef.set(String(model.modelId), model);
}
});
return candidateIds
.map((id) => byModelRef.get(id))
.filter(Boolean)
.map((model: any) => ({
id: String(model.id),
modelId: String(model.modelId || "").trim(),
displayName: String(model.displayName || model.modelId || "").trim(),
}))
.filter((item) => item.modelId);
}
export function extractVideoUrlFromText(text: string): string | null {
const normalizedText = String(text || "");
if (!normalizedText) return null;
const htmlMatch = normalizedText.match(/<video[^>]+src="([^"]+)"/i);
if (htmlMatch?.[1]) return htmlMatch[1];
const markdownMatch = normalizedText.match(
/\[[^\]]*]\((https?:\/\/[^)\s]+\.mp4(?:\?[^)\s]*)?)\)/i,
);
if (markdownMatch?.[1]) return markdownMatch[1];
const plainMatch = normalizedText.match(/(https?:\/\/[^\s"'<>]+\.mp4(?:\?[^\s"'<>]+)?)/i);
return plainMatch?.[1] || null;
}
export function isAlreadyArchivedVideoUrl(url: string): boolean {
return /\/uploads\//i.test(url);
}
export function scheduleVideoArchive(params: {
chatId: string;
messageId: string;
text: string;
temporaryVideoUrl: string;
taskId?: string;
metadata?: Record<string, any> | null;
}) {
const { chatId, messageId, temporaryVideoUrl, taskId, metadata } = params;
if (!temporaryVideoUrl || isAlreadyArchivedVideoUrl(temporaryVideoUrl)) {
return;
}
const delayHoursRaw = Number(process.env.VIDEO_ARCHIVE_DELAY_HOURS || 48);
const delayHours = Number.isFinite(delayHoursRaw)
? Math.max(1, Math.min(24 * 30, Math.round(delayHoursRaw)))
: 48;
const archiveAfterAtMs = Date.now() + delayHours * 60 * 60 * 1000;
after(async () => {
try {
const nextMetadata = {
...(metadata || {}),
temporaryVideoUrl,
storageStatus: "temporary",
archiveMode: "delayed_job",
archiveAfterAt: archiveAfterAtMs,
archiveDelayHours: delayHours,
archiveTaskId: taskId || messageId,
updatedAt: Date.now(),
};
await updateMessageById({
messageId,
chatId,
videoStorageStatus: "temporary",
temporaryVideoUrl,
permanentVideoUrl: null,
archivedAt: null,
videoMetadata: nextMetadata as any,
});
chatApiLogger.info("[Chat API] 🕒 已登记延迟归档视频:", {
messageId,
archiveAfterAt: new Date(archiveAfterAtMs).toISOString(),
});
} catch (error) {
chatApiLogger.error("[Chat API] ❌ 登记视频延迟归档失败(保留临时地址):", {
messageId,
temporaryVideoUrl,
error,
});
}
});
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
import { z } from "zod";
import type { CanvasAgentContext } from "@/types/canvas/agent-protocol";
const textPartSchema = z.object({
type: z.enum(["text"]),
@ -11,6 +12,8 @@ const filePartSchema = z.object({
name: z.string().min(1).max(100),
url: z.string().url(),
assetId: z.string().uuid().optional(),
width: z.number().positive().optional(),
height: z.number().positive().optional(),
});
const partSchema = z.union([textPartSchema, filePartSchema]);
@ -30,11 +33,22 @@ const videoParamsSchema = z.object({
returnLastFrame: z.boolean().optional(),
}).optional();
const imageSizeValues = ["0.5K", "1K", "2K", "4K", "8K", "10K", "12K"] as const;
function normalizeImageSizeInput(value: unknown) {
if (typeof value !== "string") return undefined;
const normalized = value.trim().toUpperCase();
return imageSizeValues.includes(normalized as (typeof imageSizeValues)[number])
? normalized
: undefined;
}
// 🆕 图片生成参数 schema
const imageParamsSchema = z.object({
aspectRatio: z.string().optional(),
imageSize: z.string().optional(),
imageSize: z.preprocess(normalizeImageSizeInput, z.enum(imageSizeValues).optional()),
imageCount: z.number().min(1).max(10).optional(),
quality: z.enum(["auto", "low", "medium", "high"]).optional(),
// 阿里生图专用参数
size: z.string().optional(), // 自定义分辨率,如 "1024*1024"
negativePrompt: z.string().optional(), // 反向提示词
@ -50,7 +64,8 @@ const contextPayloadSchema = z
projectId: z.string().uuid().optional(),
selectedNodeIds: z.array(z.string().min(1).max(128)).max(20).optional(),
selectedAssetIds: z.array(z.string().uuid()).max(20).optional(),
focusPolicy: z.enum(["auto", "selected", "latest_result", "original"]).optional(),
focusPolicy: z.enum(["auto", "selected", "latest_result", "original", "slot"]).optional(),
canvasAgentContext: z.custom<CanvasAgentContext>().optional(),
canvasImageNodes: z
.array(
z.object({
@ -71,6 +86,41 @@ const contextPayloadSchema = z
})
.optional();
const agentModeSourceImageSchema = z.object({
url: z.string().url(),
label: z.string().max(255).optional(),
name: z.string().max(255).optional(),
assetId: z.string().min(1).max(128).nullable().optional(),
});
const agentModeReferenceSchema = z.object({
id: z.string().min(1).max(128),
label: z.string().min(1).max(255),
imageUrl: z.string().url(),
candidateId: z.string().min(1).max(128).nullable().optional(),
sortOrder: z.number().min(0).max(999).optional(),
assignedProductIndex: z.number().min(0).max(9).nullable().optional(),
passthrough: z.boolean().nullable().optional(),
rerunReferenceImages: z.array(agentModeSourceImageSchema).max(6).optional(),
});
const agentModePayloadSchema = z
.object({
modeType: z.enum(["detail_replacement", "sku_replacement"]),
replacementStrategy: z.enum(["scene_replace", "product_only"]).optional(),
query: z.string().max(64).optional(),
queryType: z.enum(["sku", "spu", "productId"]).optional(),
selectedCandidateId: z.string().min(1).max(128).nullable().optional(),
references: z.array(agentModeReferenceSchema).max(40),
sourceImages: z
.object({
productImages: z.array(agentModeSourceImageSchema).max(16).optional(),
modelImage: agentModeSourceImageSchema.nullable().optional(),
})
.optional(),
})
.optional();
export const postRequestBodySchema = z.object({
id: z.string().uuid(),
message: z.object({
@ -79,14 +129,19 @@ export const postRequestBodySchema = z.object({
parts: z.array(partSchema),
}),
hiddenPrompt: z.string().max(4000).optional(),
workflowPromptOptimizationEnabled: z.boolean().optional(),
autoContext: z.string().max(4000).optional(),
agentPresetId: z.string().uuid().optional(),
// 支持预定义模型名称或数据库中的模型 UUID
selectedChatModel: z.string().min(1).max(128),
selectedVisibilityType: z.enum(["public", "private"]),
composerMode: z.enum(["agent", "image", "video", "detail"]).optional(),
deepThinkingEnabled: z.boolean().optional(),
debugProtocol: z.boolean().optional(),
videoParams: videoParamsSchema, // 视频参数
imageParams: imageParamsSchema, // 🆕 图片生成参数
contextPayload: contextPayloadSchema,
agentModePayload: agentModePayloadSchema,
});
export type PostRequestBody = z.infer<typeof postRequestBodySchema>;

View File

@ -0,0 +1,660 @@
import {
AbortMultipartUploadCommand,
CompleteMultipartUploadCommand,
CreateMultipartUploadCommand,
HeadObjectCommand,
ListPartsCommand,
PutObjectCommand,
UploadPartCommand,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/app/(auth)/auth";
import { upsertMediaAsset } from "@/lib/db/queries";
import { createLogger } from "@/lib/observability/logger";
import {
buildS3PublicUrl,
buildStorageObjectKey,
createS3Client,
getContentType,
maybeEnqueueStorageMigrationTask,
resolveS3UploadTarget,
} from "@/lib/storage/service";
import {
getOpenClawChatFileContentType,
isOpenClawChatFolderFileAllowed,
OPENCLAW_CHAT_FILE_MAX_BYTES,
OPENCLAW_CHAT_FILE_UPLOAD_PURPOSE,
OPENCLAW_CHAT_FOLDER_UPLOAD_PURPOSE,
OPENCLAW_CHAT_VIDEO_MAX_BYTES,
OPENCLAW_CHAT_VIDEO_UPLOAD_PURPOSE,
} from "@/lib/openclaw-chat/media-attachments";
const logger = createLogger("direct-upload-api");
const DIRECT_UPLOAD_MAX_BYTES = OPENCLAW_CHAT_FILE_MAX_BYTES;
const DIRECT_UPLOAD_MEDIA_MAX_BYTES = 500 * 1024 * 1024;
const MULTIPART_THRESHOLD_BYTES = 50 * 1024 * 1024;
const DEFAULT_PART_SIZE_BYTES = 8 * 1024 * 1024;
const PRESIGNED_URL_EXPIRES_SECONDS = 6 * 60 * 60;
const DIRECT_UPLOAD_PREFIX = "uploads/direct";
function elapsedMs(startedAt: number) {
return Date.now() - startedAt;
}
const allowedDirectUploadTypes = new Set([
"image/jpeg",
"image/jpg",
"image/png",
"image/webp",
]);
const allowedOpenClawVideoUploadTypes = new Set([
"video/mp4",
"video/quicktime",
"video/webm",
"video/x-m4v",
"video/x-matroska",
]);
const InitiateSchema = z.object({
action: z.literal("initiate"),
filename: z.string().min(1).max(256),
contentType: z.string().min(1).max(128),
size: z.number().int().positive().max(DIRECT_UPLOAD_MAX_BYTES),
purpose: z.string().max(64).optional(),
clientUploadId: z.string().max(128).optional(),
});
const CompleteSchema = z.object({
action: z.literal("complete"),
key: z.string().min(1).max(512),
uploadId: z.string().min(1).max(512).optional(),
filename: z.string().min(1).max(256),
contentType: z.string().min(1).max(128),
size: z.number().int().positive().max(DIRECT_UPLOAD_MAX_BYTES),
parts: z
.array(
z.object({
ETag: z.string().min(1),
PartNumber: z.number().int().positive(),
})
)
.optional(),
purpose: z.string().max(64).optional(),
clientUploadId: z.string().max(128).optional(),
});
const ResumeSchema = z.object({
action: z.literal("resume"),
key: z.string().min(1).max(512),
uploadId: z.string().min(1).max(512),
filename: z.string().min(1).max(256),
contentType: z.string().min(1).max(128),
size: z.number().int().positive().max(DIRECT_UPLOAD_MAX_BYTES),
partSize: z.number().int().min(5 * 1024 * 1024).max(128 * 1024 * 1024),
purpose: z.string().max(64).optional(),
clientUploadId: z.string().max(128).optional(),
});
const AbortSchema = z.object({
action: z.literal("abort"),
key: z.string().min(1).max(512),
uploadId: z.string().min(1).max(512),
clientUploadId: z.string().max(128).optional(),
});
const RequestSchema = z.discriminatedUnion("action", [
InitiateSchema,
ResumeSchema,
CompleteSchema,
AbortSchema,
]);
function normalizeContentType(contentType: string, filename: string) {
const trimmed = contentType.trim().toLowerCase();
return trimmed || getContentType(filename);
}
function sanitizeDisplayName(filename: string) {
return (
filename
.trim()
.replace(/[^\u4e00-\u9fa5a-zA-Z0-9._-]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 120) || `upload-${Date.now()}`
);
}
function userUploadPrefix(userId: string) {
return `${DIRECT_UPLOAD_PREFIX}/${userId}`;
}
function assertUserOwnedKey(key: string, userId: string) {
const normalized = String(key || "").replace(/^\/+/, "");
const prefix = `${userUploadPrefix(userId)}/`;
if (!normalized.startsWith(prefix)) {
const error = new Error("Invalid upload object key");
error.name = "InvalidUploadObjectKeyError";
throw error;
}
return normalized;
}
function isAllowedDirectUpload(
contentType: string,
purpose: string | undefined,
filename: string
) {
return (
allowedDirectUploadTypes.has(contentType) ||
(purpose === OPENCLAW_CHAT_VIDEO_UPLOAD_PURPOSE &&
allowedOpenClawVideoUploadTypes.has(contentType)) ||
(purpose === OPENCLAW_CHAT_FILE_UPLOAD_PURPOSE &&
Boolean(
getOpenClawChatFileContentType({ name: filename, type: contentType })
)) ||
(purpose === OPENCLAW_CHAT_FOLDER_UPLOAD_PURPOSE &&
isOpenClawChatFolderFileAllowed({ name: filename, type: contentType }))
);
}
function getPurposeMaxBytes(purpose?: string) {
if (purpose === OPENCLAW_CHAT_VIDEO_UPLOAD_PURPOSE) {
return OPENCLAW_CHAT_VIDEO_MAX_BYTES;
}
if (
purpose === OPENCLAW_CHAT_FILE_UPLOAD_PURPOSE ||
purpose === OPENCLAW_CHAT_FOLDER_UPLOAD_PURPOSE
) {
return DIRECT_UPLOAD_MAX_BYTES;
}
return DIRECT_UPLOAD_MEDIA_MAX_BYTES;
}
function computePartSize(size: number) {
if (size <= 100 * 1024 * 1024) return DEFAULT_PART_SIZE_BYTES;
if (size <= 500 * 1024 * 1024) return 16 * 1024 * 1024;
if (size <= 1024 * 1024 * 1024) return 32 * 1024 * 1024;
return 64 * 1024 * 1024;
}
async function createPartUrls(params: {
client: ReturnType<typeof createS3Client>;
bucket: string;
key: string;
uploadId: string;
partNumbers: number[];
}) {
return Promise.all(
params.partNumbers.map(async (partNumber) => {
const command = new UploadPartCommand({
Bucket: params.bucket,
Key: params.key,
UploadId: params.uploadId,
PartNumber: partNumber,
});
return {
partNumber,
url: await getSignedUrl(params.client, command, {
expiresIn: PRESIGNED_URL_EXPIRES_SECONDS,
}),
};
})
);
}
export async function POST(request: Request) {
const requestStartedAt = Date.now();
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
let payload: z.infer<typeof RequestSchema>;
try {
payload = RequestSchema.parse(await request.json());
} catch (error) {
return NextResponse.json(
{
error: "Invalid direct upload request",
details: error instanceof Error ? error.message : String(error),
},
{ status: 400 }
);
}
logger.info("direct_upload_request_start", {
action: payload.action,
size: "size" in payload ? payload.size : null,
contentType: "contentType" in payload ? payload.contentType : null,
purpose: "purpose" in payload ? payload.purpose || null : null,
clientUploadId:
"clientUploadId" in payload ? payload.clientUploadId || null : null,
});
const resolveStartedAt = Date.now();
const target = await resolveS3UploadTarget().catch((error) => {
logger.error("resolve_storage_target_failed", error);
return null;
});
if (!target) {
return NextResponse.json(
{ error: "Direct upload is not available for current storage backend" },
{ status: 409 }
);
}
logger.info("direct_upload_storage_target_resolved", {
action: payload.action,
elapsedMs: elapsedMs(resolveStartedAt),
provider: target.config.provider || "s3",
bucket: target.config.bucket,
hasPublicUrl: Boolean(target.config.publicUrl),
});
const client = createS3Client(target.config);
const bucket = target.config.bucket;
const usePublicReadAcl = target.config.provider !== "qiniu-kodo";
try {
if (payload.action === "initiate") {
const contentType = normalizeContentType(
payload.contentType,
payload.filename
);
if (!isAllowedDirectUpload(contentType, payload.purpose, payload.filename)) {
logger.warn("unsupported_direct_upload_type", {
contentType,
filenameExtension: payload.filename.toLowerCase().split(".").pop() || null,
size: payload.size,
purpose: payload.purpose || null,
clientUploadId: payload.clientUploadId || null,
});
return NextResponse.json(
{
error:
"直传仅支持图片、Claw 视频、压缩包、表格和文件夹清单",
},
{ status: 400 }
);
}
if (payload.size > getPurposeMaxBytes(payload.purpose)) {
return NextResponse.json(
{ error: `文件不能超过 ${getPurposeMaxBytes(payload.purpose) / 1024 / 1024}MB` },
{ status: 413 }
);
}
const displayName = sanitizeDisplayName(payload.filename);
const key = buildStorageObjectKey(
displayName,
userUploadPrefix(session.user.id)
);
const multipartThreshold =
payload.purpose === OPENCLAW_CHAT_FILE_UPLOAD_PURPOSE ||
payload.purpose === OPENCLAW_CHAT_VIDEO_UPLOAD_PURPOSE ||
payload.purpose === OPENCLAW_CHAT_FOLDER_UPLOAD_PURPOSE
? DEFAULT_PART_SIZE_BYTES
: MULTIPART_THRESHOLD_BYTES;
if (payload.size < multipartThreshold) {
const presignStartedAt = Date.now();
const command = new PutObjectCommand({
Bucket: bucket,
Key: key,
ContentType: contentType,
CacheControl: "public, max-age=31536000, immutable",
...(usePublicReadAcl ? { ACL: "public-read" as const } : {}),
});
const url = await getSignedUrl(client, command, {
expiresIn: PRESIGNED_URL_EXPIRES_SECONDS,
});
logger.info("direct_upload_single_presigned", {
elapsedMs: elapsedMs(presignStartedAt),
totalElapsedMs: elapsedMs(requestStartedAt),
size: payload.size,
contentType,
clientUploadId: payload.clientUploadId || null,
});
const headers: Record<string, string> = {
"Content-Type": contentType,
"Cache-Control": "public, max-age=31536000, immutable",
};
if (usePublicReadAcl) {
headers["x-amz-acl"] = "public-read";
}
return NextResponse.json({
mode: "single",
key,
url,
headers,
expiresIn: PRESIGNED_URL_EXPIRES_SECONDS,
clientUploadId: payload.clientUploadId || null,
});
}
const multipartStartedAt = Date.now();
const createResult = await client.send(
new CreateMultipartUploadCommand({
Bucket: bucket,
Key: key,
ContentType: contentType,
CacheControl: "public, max-age=31536000, immutable",
...(usePublicReadAcl ? { ACL: "public-read" as const } : {}),
})
);
const uploadId = createResult.UploadId;
if (!uploadId) {
throw new Error("Storage provider did not return uploadId");
}
const partSize = computePartSize(payload.size);
const partCount = Math.ceil(payload.size / partSize);
const parts = await createPartUrls({
client,
bucket,
key,
uploadId,
partNumbers: Array.from({ length: partCount }, (_, index) => index + 1),
});
logger.info("direct_upload_multipart_prepared", {
elapsedMs: elapsedMs(multipartStartedAt),
totalElapsedMs: elapsedMs(requestStartedAt),
size: payload.size,
partCount,
partSize,
contentType,
clientUploadId: payload.clientUploadId || null,
});
return NextResponse.json({
mode: "multipart",
key,
uploadId,
partSize,
partCount,
parts,
expiresIn: PRESIGNED_URL_EXPIRES_SECONDS,
clientUploadId: payload.clientUploadId || null,
});
}
if (payload.action === "resume") {
const key = assertUserOwnedKey(payload.key, session.user.id);
const contentType = normalizeContentType(payload.contentType, payload.filename);
if (!isAllowedDirectUpload(contentType, payload.purpose, payload.filename)) {
return NextResponse.json({ error: "不支持续传该文件类型" }, { status: 400 });
}
if (payload.size > getPurposeMaxBytes(payload.purpose)) {
return NextResponse.json({ error: "文件超过允许的上传大小" }, { status: 413 });
}
const expectedPartSize = computePartSize(payload.size);
if (payload.partSize !== expectedPartSize) {
return NextResponse.json(
{ error: "续传分片参数已失效,请重新开始上传", code: "RESUME_SESSION_EXPIRED" },
{ status: 409 }
);
}
try {
const listed = await client.send(
new ListPartsCommand({
Bucket: bucket,
Key: key,
UploadId: payload.uploadId,
MaxParts: 1000,
})
);
const uploadedParts = (listed.Parts || []).flatMap((part) => {
if (!part.ETag || typeof part.PartNumber !== "number") return [];
return [
{
ETag: part.ETag,
PartNumber: part.PartNumber,
size: Number(part.Size || 0),
},
];
});
const uploadedPartNumbers = new Set(
uploadedParts.map((part) => part.PartNumber)
);
const partCount = Math.ceil(payload.size / expectedPartSize);
const missingPartNumbers = Array.from(
{ length: partCount },
(_, index) => index + 1
).filter((partNumber) => !uploadedPartNumbers.has(partNumber));
const parts = await createPartUrls({
client,
bucket,
key,
uploadId: payload.uploadId,
partNumbers: missingPartNumbers,
});
logger.info("direct_upload_multipart_resumed", {
totalElapsedMs: elapsedMs(requestStartedAt),
size: payload.size,
uploadedPartCount: uploadedParts.length,
missingPartCount: missingPartNumbers.length,
clientUploadId: payload.clientUploadId || null,
});
return NextResponse.json({
mode: "multipart",
key,
uploadId: payload.uploadId,
partSize: expectedPartSize,
partCount,
parts,
uploadedParts,
resumed: true,
expiresIn: PRESIGNED_URL_EXPIRES_SECONDS,
clientUploadId: payload.clientUploadId || null,
});
} catch (error) {
const head = await client
.send(new HeadObjectCommand({ Bucket: bucket, Key: key }))
.catch(() => null);
if (head?.ContentLength === payload.size) {
return NextResponse.json({
mode: "completed",
key,
clientUploadId: payload.clientUploadId || null,
});
}
logger.warn("direct_upload_resume_session_missing", error, {
clientUploadId: payload.clientUploadId || null,
});
return NextResponse.json(
{ error: "续传会话已失效,请重新开始上传", code: "RESUME_SESSION_EXPIRED" },
{ status: 409 }
);
}
}
if (payload.action === "abort") {
const key = assertUserOwnedKey(payload.key, session.user.id);
const abortStartedAt = Date.now();
await client.send(
new AbortMultipartUploadCommand({
Bucket: bucket,
Key: key,
UploadId: payload.uploadId,
})
);
logger.info("direct_upload_multipart_aborted", {
elapsedMs: elapsedMs(abortStartedAt),
totalElapsedMs: elapsedMs(requestStartedAt),
clientUploadId: payload.clientUploadId || null,
});
return NextResponse.json({ ok: true });
}
const key = assertUserOwnedKey(payload.key, session.user.id);
const contentType = normalizeContentType(payload.contentType, payload.filename);
if (!isAllowedDirectUpload(contentType, payload.purpose, payload.filename)) {
return NextResponse.json(
{
error:
"直传仅支持图片、Claw 视频、压缩包、表格和文件夹清单",
},
{ status: 400 }
);
}
if (payload.size > getPurposeMaxBytes(payload.purpose)) {
return NextResponse.json({ error: "文件超过允许的上传大小" }, { status: 413 });
}
if (payload.uploadId) {
const completeMultipartStartedAt = Date.now();
const parts = [...(payload.parts || [])].sort(
(a, b) => a.PartNumber - b.PartNumber
);
if (parts.length === 0) {
return NextResponse.json(
{ error: "Missing multipart upload parts" },
{ status: 400 }
);
}
await client.send(
new CompleteMultipartUploadCommand({
Bucket: bucket,
Key: key,
UploadId: payload.uploadId,
MultipartUpload: {
Parts: parts,
},
})
);
logger.info("direct_upload_multipart_completed", {
elapsedMs: elapsedMs(completeMultipartStartedAt),
partCount: parts.length,
clientUploadId: payload.clientUploadId || null,
});
}
const headStartedAt = Date.now();
const head = await client.send(
new HeadObjectCommand({
Bucket: bucket,
Key: key,
})
);
logger.info("direct_upload_head_object_completed", {
elapsedMs: elapsedMs(headStartedAt),
uploadMode: payload.uploadId ? "multipart" : "single",
clientUploadId: payload.clientUploadId || null,
});
const actualSize =
typeof head.ContentLength === "number" ? head.ContentLength : null;
if (actualSize !== null && actualSize !== payload.size) {
throw new Error(
`Uploaded object size mismatch: expected ${payload.size}, received ${actualSize}`
);
}
const url = buildS3PublicUrl(target.config, key);
const resolvedContentType =
head.ContentType || contentType || getContentType(payload.filename);
const assetStartedAt = Date.now();
const asset = await upsertMediaAsset({
userId: session.user.id,
mediaType:
payload.purpose === OPENCLAW_CHAT_FILE_UPLOAD_PURPOSE ||
payload.purpose === OPENCLAW_CHAT_FOLDER_UPLOAD_PURPOSE
? "file"
: resolvedContentType.startsWith("video/")
? "video"
: "image",
sourceType:
payload.purpose === OPENCLAW_CHAT_VIDEO_UPLOAD_PURPOSE
? "openclaw_chat_video_upload"
: payload.purpose === OPENCLAW_CHAT_FILE_UPLOAD_PURPOSE
? "openclaw_chat_file_upload"
: payload.purpose === OPENCLAW_CHAT_FOLDER_UPLOAD_PURPOSE
? "openclaw_chat_folder_file_upload"
: "upload",
status: "ready",
originalUrl: url,
previewUrl:
payload.purpose === OPENCLAW_CHAT_FILE_UPLOAD_PURPOSE ||
payload.purpose === OPENCLAW_CHAT_FOLDER_UPLOAD_PURPOSE
? null
: url,
thumbnailUrl:
payload.purpose === OPENCLAW_CHAT_FILE_UPLOAD_PURPOSE ||
payload.purpose === OPENCLAW_CHAT_FOLDER_UPLOAD_PURPOSE
? null
: url,
mimeType: resolvedContentType,
displayName: payload.filename,
sourceTable:
payload.purpose === OPENCLAW_CHAT_VIDEO_UPLOAD_PURPOSE
? "openclaw_chat_video_upload"
: payload.purpose === OPENCLAW_CHAT_FILE_UPLOAD_PURPOSE
? "openclaw_chat_file_upload"
: payload.purpose === OPENCLAW_CHAT_FOLDER_UPLOAD_PURPOSE
? "openclaw_chat_folder_file_upload"
: "direct_upload",
metadata: {
pathname: key,
size: actualSize ?? payload.size,
expectedSize: payload.size,
uploadMode: payload.uploadId ? "multipart" : "single",
purpose: payload.purpose || null,
clientUploadId: payload.clientUploadId || null,
},
});
logger.info("direct_upload_asset_registered", {
elapsedMs: elapsedMs(assetStartedAt),
totalElapsedMs: elapsedMs(requestStartedAt),
uploadMode: payload.uploadId ? "multipart" : "single",
assetId: asset?.id ?? null,
clientUploadId: payload.clientUploadId || null,
});
await maybeEnqueueStorageMigrationTask({
uploaded: { url, pathname: key },
storageConfig: target.storageConfig,
migrationConfig: target.migrationConfig,
}).catch((error) => {
logger.warn("enqueue_storage_migration_failed", error);
});
return NextResponse.json({
url,
pathname: key,
contentType: resolvedContentType,
assetId: asset?.id ?? null,
clientUploadId: payload.clientUploadId || null,
});
} catch (error) {
logger.error("direct_upload_request_failed", error, {
action: payload.action,
size: "size" in payload ? payload.size : null,
contentType: "contentType" in payload ? payload.contentType : null,
purpose: "purpose" in payload ? payload.purpose || null : null,
clientUploadId:
"clientUploadId" in payload ? payload.clientUploadId || null : null,
});
return NextResponse.json(
{
error: "Direct upload failed",
details: error instanceof Error ? error.message : String(error),
},
{
status:
error instanceof Error &&
error.name === "InvalidUploadObjectKeyError"
? 403
: 500,
}
);
}
}

View File

@ -2,9 +2,129 @@ import { NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/app/(auth)/auth";
import { createAgentWorkflowPreviewImageBuffer } from "@/lib/agents/workflow-preview-images";
import { upsertMediaAsset } from "@/lib/db/queries";
import { uploadFile } from "@/lib/storage/service";
import { isSiteAdminType } from "@/lib/middleware/admin";
import { getContentType, uploadFile } from "@/lib/storage/service";
import { createLogger } from "@/lib/observability/logger";
import { OPENCLAW_CHAT_VIDEO_UPLOAD_PURPOSE } from "@/lib/openclaw-chat/media-attachments";
const logger = createLogger("upload-api");
const ALLOWED_UPLOAD_IMAGE_TYPES = new Set([
"image/jpeg",
"image/jpg",
"image/png",
"image/webp",
"image/avif",
"image/tiff",
"image/x-tiff",
"image/svg+xml",
"image/heic",
"image/heif",
]);
const ALLOWED_ATLASCLOUD_VIDEO_TYPES = new Set([
"video/mp4",
"video/quicktime",
"video/webm",
]);
const ALLOWED_OPENCLAW_VIDEO_TYPES = new Set([
"video/mp4",
"video/quicktime",
"video/webm",
"video/x-m4v",
"video/x-matroska",
]);
const HEIC_UPLOAD_TYPES = new Set(["image/heic", "image/heif"]);
const SHARP_COMPATIBILITY_UPLOAD_TYPES = new Set([
"image/avif",
"image/tiff",
"image/x-tiff",
]);
function normalizeUploadContentType(contentType: string, filename: string) {
const normalized = contentType.trim().toLowerCase();
if (normalized && normalized !== "application/octet-stream") {
return normalized;
}
return getContentType(filename);
}
function createUnsupportedImageTypeMessage(contentType: string, filename: string) {
const extension = filename.toLowerCase().split(".").pop() || "unknown";
if (contentType === "image/heic" || contentType === "image/heif" || extension === "heic" || extension === "heif") {
return "HEIC/HEIF 图片转码失败,请在手机相册中导出为 JPG/PNG 后再上传。";
}
if (
SHARP_COMPATIBILITY_UPLOAD_TYPES.has(contentType) ||
extension === "avif" ||
extension === "tif" ||
extension === "tiff"
) {
return "AVIF/TIFF 图片转码失败,请导出为 JPG/PNG 后再上传。";
}
return "图片格式不支持,请上传 JPG、PNG、WebP、HEIC、AVIF 或 TIFF 图片。";
}
function isHeicUpload(contentType: string, filename: string) {
const extension = filename.toLowerCase().split(".").pop() || "";
return (
HEIC_UPLOAD_TYPES.has(contentType) ||
extension === "heic" ||
extension === "heif"
);
}
function isSharpCompatibilityUpload(contentType: string, filename: string) {
const extension = filename.toLowerCase().split(".").pop() || "";
return (
SHARP_COMPATIBILITY_UPLOAD_TYPES.has(contentType) ||
extension === "avif" ||
extension === "tif" ||
extension === "tiff"
);
}
function toJpegFilename(filename: string) {
const trimmed = filename.trim() || `upload-${Date.now()}`;
if (/\.(avif|heic|heif|tif|tiff)$/i.test(trimmed)) {
return trimmed.replace(/\.(avif|heic|heif|tif|tiff)$/i, ".jpg");
}
if (/\.[a-z0-9]+$/i.test(trimmed)) {
return trimmed.replace(/\.[a-z0-9]+$/i, ".jpg");
}
return `${trimmed}.jpg`;
}
async function convertHeicToJpeg(buffer: Buffer): Promise<Buffer> {
const convert = (await import("heic-convert")).default;
const output = await convert({
buffer,
format: "JPEG",
quality: 0.92,
});
if (output instanceof ArrayBuffer) {
return Buffer.from(new Uint8Array(output));
}
return Buffer.from(output);
}
async function convertSharpCompatibilityImageToJpeg(
buffer: Buffer
): Promise<Buffer> {
const sharp = (await import("sharp")).default;
return sharp(buffer, { animated: false, failOn: "none" })
.rotate()
.flatten({ background: "#ffffff" })
.jpeg({
quality: 92,
mozjpeg: true,
progressive: true,
})
.toBuffer();
}
/**
* 使用Sharp智能优化图片(保持原格式)
* 策略:避免双重压缩,只在需要时优化
@ -18,7 +138,7 @@ async function optimizeImage(buffer: Buffer, mimeType: string): Promise<Buffer>
// ✅ 智能判断:如果文件已经很小,跳过优化(避免双重压缩)
if (fileSizeMB < OPTIMIZE_THRESHOLD_MB) {
console.log(`[Upload] 文件已较小(${fileSizeMB.toFixed(2)}MB < ${OPTIMIZE_THRESHOLD_MB}MB),跳过优化,避免双重压缩`);
logger.debug(`[Upload] 文件已较小(${fileSizeMB.toFixed(2)}MB < ${OPTIMIZE_THRESHOLD_MB}MB),跳过优化,避免双重压缩`);
return buffer;
}
@ -43,10 +163,10 @@ async function optimizeImage(buffer: Buffer, mimeType: string): Promise<Buffer>
// 只有优化后文件明显更小才使用(避免不必要的处理)
const reductionPercent = ((1 - optimized.length / buffer.length) * 100);
if (reductionPercent > 5) { // 只有减少超过5%才使用
console.log(`[Upload] JPEG优化: ${buffer.length} → ${optimized.length} bytes (减少${reductionPercent.toFixed(1)}%)`);
logger.debug(`[Upload] JPEG优化: ${buffer.length} → ${optimized.length} bytes (减少${reductionPercent.toFixed(1)}%)`);
return optimized;
} else {
console.log(`[Upload] JPEG优化效果不明显(${reductionPercent.toFixed(1)}%),使用原图`);
logger.debug(`[Upload] JPEG优化效果不明显(${reductionPercent.toFixed(1)}%),使用原图`);
return buffer;
}
} else if (mimeType === 'image/png') {
@ -65,10 +185,10 @@ async function optimizeImage(buffer: Buffer, mimeType: string): Promise<Buffer>
// 只有优化后文件明显更小才使用
const reductionPercent = ((1 - optimized.length / buffer.length) * 100);
if (reductionPercent > 5) { // 只有减少超过5%才使用
console.log(`[Upload] PNG优化: ${buffer.length} → ${optimized.length} bytes (减少${reductionPercent.toFixed(1)}%)`);
logger.debug(`[Upload] PNG优化: ${buffer.length} → ${optimized.length} bytes (减少${reductionPercent.toFixed(1)}%)`);
return optimized;
} else {
console.log(`[Upload] PNG优化效果不明显(${reductionPercent.toFixed(1)}%),使用原图`);
logger.debug(`[Upload] PNG优化效果不明显(${reductionPercent.toFixed(1)}%),使用原图`);
return buffer;
}
}
@ -76,37 +196,38 @@ async function optimizeImage(buffer: Buffer, mimeType: string): Promise<Buffer>
// 其他格式返回原图
return buffer;
} catch (error) {
console.error("[Upload] 图片优化失败,使用原图:", error);
logger.error("[Upload] 图片优化失败,使用原图:", error);
// 优化失败时返回原图,不影响上传流程
return buffer;
}
}
function toArrayBuffer(buffer: Buffer): ArrayBuffer {
const view = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
return view.slice().buffer;
}
// Use Blob instead of File since File is not available in Node.js environment
const FileSchema = z.object({
file: z
.instanceof(Blob)
.refine((file) => file.size <= 50 * 1024 * 1024, {
message: "File size should be less than 50MB",
})
// Update the file type based on the kind of files you want to accept
.refine((file) => ["image/jpeg", "image/png", "image/webp"].includes(file.type), {
message: "File type should be JPEG, PNG or WebP",
}),
});
export async function POST(request: Request) {
console.log("[Upload API] Received upload request");
logger.debug("[Upload API] Received upload request");
const session = await auth();
if (!session) {
console.log("[Upload API] Unauthorized - no session");
logger.debug("[Upload API] Unauthorized - no session");
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (request.body === null) {
console.log("[Upload API] Request body is empty");
logger.debug("[Upload API] Request body is empty");
return new Response("Request body is empty", { status: 400 });
}
@ -114,15 +235,43 @@ export async function POST(request: Request) {
const formData = await request.formData();
const file = formData.get("file") as Blob;
console.log("[Upload API] FormData received, file exists:", !!file);
logger.debug("[Upload API] FormData received, file exists:", !!file);
if (!file) {
console.log("[Upload API] No file in FormData");
logger.debug("[Upload API] No file in FormData");
return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
}
console.log("[Upload API] File info:", {
type: file.type,
// Get filename from formData since Blob doesn't have name property
const originalFilename = (formData.get("file") as File).name;
const originalContentType = normalizeUploadContentType(
file.type,
originalFilename
);
let filename = originalFilename;
let contentType = originalContentType;
const uploadPurpose = String(formData.get("purpose") || "");
const isAtlasCloudReferenceVideo =
uploadPurpose === "canvas-video-process" &&
ALLOWED_ATLASCLOUD_VIDEO_TYPES.has(contentType);
const isOpenClawChatVideoUpload =
uploadPurpose === OPENCLAW_CHAT_VIDEO_UPLOAD_PURPOSE &&
ALLOWED_OPENCLAW_VIDEO_TYPES.has(contentType);
const clientUploadId =
String(formData.get("clientUploadId") || "").trim() || null;
const isSvgUpload = contentType === "image/svg+xml";
if (
isSvgUpload &&
(uploadPurpose !== "admin-svg-icon" || !isSiteAdminType(session.user.type))
) {
return NextResponse.json(
{ error: "SVG uploads are only allowed for administrator icon settings" },
{ status: 403 }
);
}
logger.debug("[Upload API] File info:", {
type: contentType,
size: file.size,
sizeInMB: (file.size / 1024 / 1024).toFixed(2) + "MB"
});
@ -134,28 +283,164 @@ export async function POST(request: Request) {
.map((error) => error.message)
.join(", ");
console.log("[Upload API] Validation failed:", errorMessage);
logger.debug("[Upload API] Validation failed:", errorMessage);
return NextResponse.json({ error: errorMessage }, { status: 400 });
}
// Get filename from formData since Blob doesn't have name property
const filename = (formData.get("file") as File).name;
console.log("[Upload API] Filename:", filename);
if (
!ALLOWED_UPLOAD_IMAGE_TYPES.has(contentType) &&
!isAtlasCloudReferenceVideo &&
!isOpenClawChatVideoUpload
) {
const errorMessage = createUnsupportedImageTypeMessage(contentType, filename);
logger.warn("[Upload API] Unsupported upload content type", {
contentType,
filenameExtension: filename.toLowerCase().split(".").pop() || null,
size: file.size,
purpose: uploadPurpose || null,
clientUploadId,
});
return NextResponse.json({ error: errorMessage }, { status: 400 });
}
logger.debug("[Upload API] Filename:", filename);
const fileBuffer = await file.arrayBuffer();
console.log("[Upload API] File buffer size:", fileBuffer.byteLength);
logger.debug("[Upload API] File buffer size:", fileBuffer.byteLength);
try {
let uploadBuffer: Buffer = Buffer.from(fileBuffer);
let convertedFrom: string | null = null;
if (isHeicUpload(contentType, filename)) {
try {
const convertedBuffer = await convertHeicToJpeg(uploadBuffer);
logger.info("[Upload API] HEIC image converted to JPEG", {
clientUploadId,
originalContentType: contentType,
originalSize: uploadBuffer.length,
convertedSize: convertedBuffer.length,
});
uploadBuffer = convertedBuffer;
filename = toJpegFilename(filename);
convertedFrom = contentType;
contentType = "image/jpeg";
} catch (heicError) {
logger.warn("[Upload API] HEIC image conversion failed", {
clientUploadId,
contentType,
size: uploadBuffer.length,
error:
heicError instanceof Error ? heicError.message : String(heicError),
});
return NextResponse.json(
{
error: createUnsupportedImageTypeMessage(contentType, filename),
clientUploadId,
},
{ status: 400 }
);
}
} else if (isSharpCompatibilityUpload(contentType, filename)) {
try {
const convertedBuffer =
await convertSharpCompatibilityImageToJpeg(uploadBuffer);
logger.info("[Upload API] Compatibility image converted to JPEG", {
clientUploadId,
originalContentType: contentType,
originalSize: uploadBuffer.length,
convertedSize: convertedBuffer.length,
});
uploadBuffer = convertedBuffer;
filename = toJpegFilename(filename);
convertedFrom = contentType;
contentType = "image/jpeg";
} catch (conversionError) {
logger.warn("[Upload API] Compatibility image conversion failed", {
clientUploadId,
contentType,
size: uploadBuffer.length,
error:
conversionError instanceof Error
? conversionError.message
: String(conversionError),
});
return NextResponse.json(
{
error: createUnsupportedImageTypeMessage(contentType, filename),
clientUploadId,
},
{ status: 400 }
);
}
}
if (uploadPurpose === "agent-workflow-preview" && contentType !== "image/svg+xml") {
const preview = await createAgentWorkflowPreviewImageBuffer(uploadBuffer);
const previewResult = await uploadFile(
`${filename.replace(/\.[^.]+$/, "") || "agent-workflow-preview"}.webp`,
toArrayBuffer(preview.buffer)
);
let assetId: string | null = null;
try {
const asset = await upsertMediaAsset({
userId: session.user.id,
mediaType: "image",
sourceType: "agent_workflow_preview_upload",
status: "ready",
originalUrl: previewResult.url,
previewUrl: previewResult.url,
thumbnailUrl: previewResult.url,
mimeType: "image/webp",
width: preview.width,
height: preview.height,
displayName: filename,
sourceTable: "agent_workflow_preview_upload",
metadata: {
pathname: previewResult.pathname,
originalSize: fileBuffer.byteLength,
previewSize: preview.buffer.length,
purpose: uploadPurpose,
clientUploadId,
convertedFrom,
},
});
assetId = asset?.id ?? null;
} catch (assetError) {
logger.warn("[Upload API] Failed to register workflow preview asset:", assetError);
}
return NextResponse.json({
...previewResult,
url: previewResult.url,
previewUrl: previewResult.url,
thumbnailUrl: previewResult.url,
originalUrl: previewResult.url,
previewImageSourceUrl: previewResult.url,
assetId,
width: preview.width,
height: preview.height,
optimizedFor: uploadPurpose,
});
}
// ✅ 使用Sharp优化图片(保持原格式)
let optimizedBuffer: Buffer = Buffer.from(fileBuffer);
let optimizedBuffer: Buffer = uploadBuffer;
// ✅ 优化原图(保持JPG/PNG格式)
// WebP格式不优化(用于预览图,保持原样)
if (file.type === 'image/jpeg' || file.type === 'image/jpg' || file.type === 'image/png') {
optimizedBuffer = await optimizeImage(optimizedBuffer, file.type);
} else if (file.type === 'image/webp') {
// WebP格式不优化(用于预览图),直接使用
console.log(`[Upload] WebP格式(预览图),跳过优化,直接上传`);
const shouldOptimizeImage =
uploadPurpose !== "canvas-upload" &&
(contentType === 'image/jpeg' || contentType === 'image/jpg' || contentType === 'image/png');
if (shouldOptimizeImage) {
optimizedBuffer = await optimizeImage(optimizedBuffer, contentType);
} else if (contentType === 'image/webp' || contentType === 'image/svg+xml') {
// WebP/SVG 不优化;SVG 是矢量文件,交给前端按状态反色渲染。
logger.debug(`[Upload] ${contentType}格式,跳过优化,直接上传`);
} else if (uploadPurpose === "canvas-upload") {
logger.debug("[Upload] Canvas upload, skip server-side image optimization for faster paste/drop");
}
// 将Buffer转换为ArrayBuffer用于上传
@ -172,39 +457,57 @@ export async function POST(request: Request) {
try {
const asset = await upsertMediaAsset({
userId: session.user.id,
mediaType: "image",
sourceType: "upload",
mediaType:
isAtlasCloudReferenceVideo || isOpenClawChatVideoUpload
? "video"
: "image",
sourceType: isOpenClawChatVideoUpload
? "openclaw_chat_video_upload"
: "upload",
status: "ready",
originalUrl: data.url,
previewUrl: data.url,
thumbnailUrl: data.url,
mimeType: data.contentType || file.type || null,
previewUrl:
isAtlasCloudReferenceVideo || isOpenClawChatVideoUpload
? null
: data.url,
thumbnailUrl:
isAtlasCloudReferenceVideo || isOpenClawChatVideoUpload
? null
: data.url,
mimeType: data.contentType || contentType || null,
displayName: filename,
sourceTable: "upload",
sourceTable: isOpenClawChatVideoUpload
? "openclaw_chat_video_upload"
: "upload",
metadata: {
pathname: data.pathname,
size: optimizedBuffer.length,
purpose: uploadPurpose || null,
clientUploadId,
convertedFrom,
},
});
assetId = asset?.id ?? null;
} catch (assetError) {
console.warn("[Upload API] Failed to register uploaded image as media asset:", assetError);
logger.warn("[Upload API] Failed to register uploaded image as media asset:", assetError);
}
console.log("[Upload API] Upload successful:", data.url);
logger.debug("[Upload API] Upload successful:", data.url);
return NextResponse.json({
...data,
assetId,
clientUploadId,
convertedFrom,
});
} catch (_error) {
console.error("[Upload API] Upload failed:", _error);
logger.error("[Upload API] Upload failed:", _error);
return NextResponse.json({
error: "Upload failed",
details: _error instanceof Error ? _error.message : String(_error)
}, { status: 500 });
}
} catch (_error) {
console.error("[Upload API] Request processing failed:", _error);
logger.error("[Upload API] Request processing failed:", _error);
return NextResponse.json(
{
error: "Failed to process request",

View File

@ -3,19 +3,24 @@ import {
getAgentAiModelWithProvider,
getDefaultAiModel,
getEnabledAiModelsWithProvider,
getFrontendDisplayAiModelsWithProvider,
getSiteSettings,
getSystemSetting,
getUserById,
} from "@/lib/db/queries";
import { getAiModelFrontendAvailability } from "@/lib/ai/model-frontend-availability";
import { buildVideoToolCapability } from "@/lib/ai/video-tool-capability";
import {
isImageGenerationModelDescriptor,
isVideoModelDescriptor,
} from "@/lib/ai/model-classification";
import { ChatSDKError } from "@/lib/errors";
import { isAiModelAgentAuto, isAiModelMultimodal } from "@/lib/db/ai-model-flags";
const VIDEO_PROVIDER_TYPES = new Set([
"volcano-video",
"doubao-video",
"qianwen-video",
"fal-video",
"ruxa-video",
]);
import {
AGENT_WORKFLOW_UI_CONFIG_KEY,
getAgentWorkflowBusinessModeSettings,
normalizeAgentWorkflowUiConfig,
} from "@/lib/agents/workflow-ui-config";
function isNanoBananaProModel(model: { displayName?: string | null; modelId?: string | null }) {
const displayName = String(model.displayName || "").toLowerCase();
@ -42,9 +47,98 @@ function sortModelsForDisplay<T extends { sortOrder?: number | null; displayName
});
}
function isWorkflowImageGenerationModel(model: {
providerType?: string | null;
modelId?: string | null;
capabilities?: unknown;
supportsImageGen?: boolean | null;
}) {
return (
isImageGenerationModelDescriptor({
providerType: model.providerType,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
}) &&
!isVideoModelDescriptor({
providerType: model.providerType,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
})
);
}
// 获取前端可用的 AI 模型列表(需要登录但不需要管理员权限)
export async function GET() {
export async function GET(request: Request) {
try {
const isLocalCanvasE2E = process.env.CANVAS_E2E_LOCAL === "true";
if (isLocalCanvasE2E) {
const models = [
{
id: "local-e2e-image-model",
modelId: "nano-banana-pro-local",
displayName: "Nano Banana Pro",
avatarUrl: null,
description: "本地视频画布测试图片模型",
supportsVision: true,
supportsImageGen: true,
isMultimodal: true,
isDefault: true,
providerName: "Local E2E",
providerType: "local",
isSubModel: false,
parentModelId: null,
capabilities: { image: true },
videoToolCapability: null,
imageGenConfig: {
creditCostPerImage: 1,
creditCostPerVideo: 20,
videoDurationOptions: [5, 10],
},
sortOrder: 0,
selectable: true,
},
{
id: "local-e2e-video-model",
modelId: "kling-3-local",
displayName: "可灵 3.0",
avatarUrl: null,
description: "本地视频画布测试视频模型",
supportsVision: true,
supportsImageGen: false,
isMultimodal: true,
isDefault: false,
providerName: "Local E2E",
providerType: "local",
isSubModel: false,
parentModelId: null,
capabilities: { video: true },
videoToolCapability: {
supported: true,
durations: [5, 10],
resolutions: ["720p", "1080p"],
},
imageGenConfig: {
creditCostPerVideo: 20,
videoDurationOptions: [5, 10],
},
sortOrder: 1,
selectable: true,
},
];
return Response.json({
models,
defaultModelId: models[0].id,
agentWorkflowConfiguredDefaultModelId: null,
agentWorkflowDefaultModelId: null,
agentWorkflowConfiguredModels: [],
agentModel: null,
});
}
const session = await auth();
if (!session?.user) {
@ -54,11 +148,62 @@ export async function GET() {
const currentUser = await getUserById({ id: session.user.id });
const allowHiddenModels =
session.user.type === "admin" || Boolean(currentUser?.isHighVip);
const includeUnavailable =
new URL(request.url).searchParams.get("includeUnavailable") === "1";
// 只获取主模型(不包括子模型),子模型由后端根据 videoParams.mode 自动选择
const modelsWithProvider = await getEnabledAiModelsWithProvider(false, true);
const modelsWithProvider = includeUnavailable
? await getFrontendDisplayAiModelsWithProvider(false, true)
: await getEnabledAiModelsWithProvider(false, true);
const agentModelWithProvider = await getAgentAiModelWithProvider();
const defaultModel = await getDefaultAiModel();
const [siteSettings, agentWorkflowUiConfigSetting] = await Promise.all([
getSiteSettings(),
getSystemSetting({ key: AGENT_WORKFLOW_UI_CONFIG_KEY }).catch(() => null),
]);
type ModelWithProvider = (typeof modelsWithProvider)[number];
const serializeModel = ({ model, provider }: ModelWithProvider) => ({
id: model.id,
modelId: model.modelId,
displayName: model.displayName,
avatarUrl: model.avatarUrl,
description: model.description,
supportsVision: model.supportsVision,
supportsImageGen: model.supportsImageGen,
isMultimodal: isAiModelMultimodal(model),
isDefault: model.isDefault,
providerName: provider.name,
providerType: provider.type,
isSubModel: model.isSubModel,
parentModelId: model.parentModelId,
capabilities: model.capabilities, // 🆕 返回子模型的能力配置
videoToolCapability: buildVideoToolCapability({
providerType: provider.type,
modelId: model.modelId,
rawCapabilities: model.capabilities,
imageGenConfig: (model.imageGenConfig || null) as
| { videoDurationOptions?: number[] | null }
| null,
}),
imageGenConfig: model.imageGenConfig,
sortOrder: model.sortOrder ?? 0,
...getAiModelFrontendAvailability(model),
});
const workflowImageModelById = new Map<string, ModelWithProvider>();
modelsWithProvider.forEach((entry) => {
const { model, provider } = entry;
if (
!isAiModelAgentAuto(model) &&
isWorkflowImageGenerationModel({
providerType: provider.type,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
})
) {
workflowImageModelById.set(model.id, entry);
}
});
const visibleModelsWithProvider = sortModelsForDisplay(
modelsWithProvider
.filter(
@ -78,39 +223,40 @@ export async function GET() {
});
// 转换为前端需要的格式
const models = visibleModelsWithProviderSorted.map(({ model, provider }) => ({
id: model.id,
modelId: model.modelId,
displayName: model.displayName,
avatarUrl: model.avatarUrl,
description: model.description,
supportsVision: model.supportsVision,
supportsImageGen: model.supportsImageGen,
isMultimodal: isAiModelMultimodal(model),
isDefault: model.isDefault,
providerName: provider.name,
providerType: provider.type,
isSubModel: model.isSubModel,
parentModelId: model.parentModelId,
capabilities: model.capabilities, // 🆕 返回子模型的能力配置
videoToolCapability: buildVideoToolCapability({
providerType: provider.type,
modelId: model.modelId,
rawCapabilities: model.capabilities,
}),
imageGenConfig: model.imageGenConfig,
sortOrder: model.sortOrder ?? 0,
}));
const models = visibleModelsWithProviderSorted.map(serializeModel);
const selectableModels = models.filter((model) => model.selectable !== false);
const preferredNanoBananaModel = models.find(
(model) =>
model.supportsImageGen &&
!VIDEO_PROVIDER_TYPES.has(model.providerType || "") &&
model.selectable !== false &&
isImageGenerationModelDescriptor({
providerType: model.providerType,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
}) &&
!isVideoModelDescriptor({
providerType: model.providerType,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
}) &&
isNanoBananaProModel(model)
);
const preferredImageModel = models.find(
(model) =>
model.supportsImageGen &&
!VIDEO_PROVIDER_TYPES.has(model.providerType || "")
model.selectable !== false &&
isImageGenerationModelDescriptor({
providerType: model.providerType,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
}) &&
!isVideoModelDescriptor({
providerType: model.providerType,
modelId: model.modelId,
capabilities: model.capabilities,
supportsImageGen: model.supportsImageGen,
})
);
const resolvedDefaultModelId =
@ -118,15 +264,51 @@ export async function GET() {
preferredImageModel?.id ??
(defaultModel &&
!isAiModelAgentAuto(defaultModel) &&
models.some((model) => model.id === defaultModel.id)
selectableModels.some((model) => model.id === defaultModel.id)
? defaultModel.id
: models.find((model) => model.isDefault)?.id)) ??
models[0]?.id ??
: selectableModels.find((model) => model.isDefault)?.id)) ??
selectableModels[0]?.id ??
null;
const configuredAgentWorkflowModelId = String(
siteSettings?.agentWorkflowDefaultModelId || ""
).trim();
const hasConfiguredAgentWorkflowModel = Boolean(
configuredAgentWorkflowModelId &&
configuredAgentWorkflowModelId !== "auto" &&
configuredAgentWorkflowModelId !== "__auto"
);
const agentWorkflowDefaultModel = hasConfiguredAgentWorkflowModel
? workflowImageModelById.get(configuredAgentWorkflowModelId) || null
: null;
const workflowUiConfig = normalizeAgentWorkflowUiConfig(
agentWorkflowUiConfigSetting?.value
);
const workflowBusinessModeSettings = getAgentWorkflowBusinessModeSettings(
workflowUiConfig
);
const configuredWorkflowModelIds = new Set<string>();
if (agentWorkflowDefaultModel) {
configuredWorkflowModelIds.add(agentWorkflowDefaultModel.model.id);
}
workflowBusinessModeSettings.modes.forEach((mode) => {
const modelId = String(mode.defaultModelId || "").trim();
if (modelId && workflowImageModelById.has(modelId)) {
configuredWorkflowModelIds.add(modelId);
}
});
const agentWorkflowConfiguredModels = Array.from(configuredWorkflowModelIds)
.map((id) => workflowImageModelById.get(id))
.filter((entry): entry is ModelWithProvider => Boolean(entry))
.map(serializeModel);
return Response.json({
models,
defaultModelId: resolvedDefaultModelId,
agentWorkflowConfiguredDefaultModelId: hasConfiguredAgentWorkflowModel
? configuredAgentWorkflowModelId
: null,
agentWorkflowDefaultModelId: agentWorkflowDefaultModel?.model.id || null,
agentWorkflowConfiguredModels,
agentModel: agentModelWithProvider
? {
id: agentModelWithProvider.model.id,

View File

@ -30,6 +30,9 @@ export async function GET(request: Request) {
providerType: provider.type,
modelId: model.modelId,
rawCapabilities: model.capabilities,
imageGenConfig: (model.imageGenConfig || null) as
| { videoDurationOptions?: number[] | null }
| null,
});
return Response.json({

View File

@ -0,0 +1,75 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/app/(auth)/auth";
import {
cancelVideoStoryboardTaskByUser,
getVideoStoryboardTaskDetail,
} from "@/lib/video-agent/task-store";
import {
requireStoryboardTaskAccess,
StoryboardTaskAccessError,
} from "@/lib/video-agent/user-task-access";
const paramsSchema = z.object({
taskId: z.string().uuid(),
});
export async function POST(
_request: Request,
{ params }: { params: Promise<{ taskId: string }> }
) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
}
const parsed = paramsSchema.parse(await params);
const detail = await requireStoryboardTaskAccess({
taskId: parsed.taskId,
userId: session.user.id,
});
const status = String((detail.task as any).status || "").trim();
if (status === "cancelled") {
return NextResponse.json({
success: true,
task: detail.task,
shots: detail.shots,
});
}
await cancelVideoStoryboardTaskByUser({
taskId: parsed.taskId,
error: "user_cancelled_via_api",
});
const nextDetail = await getVideoStoryboardTaskDetail(parsed.taskId);
return NextResponse.json({
success: true,
task: nextDetail?.task || detail.task,
shots: nextDetail?.shots || detail.shots,
});
} catch (error) {
if (error instanceof StoryboardTaskAccessError) {
return NextResponse.json(
{ success: false, error: error.message },
{ status: error.status }
);
}
if (error instanceof z.ZodError) {
return NextResponse.json(
{ success: false, error: error.message },
{ status: 400 }
);
}
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : "取消任务失败",
},
{ status: 500 }
);
}
}

View File

@ -0,0 +1,101 @@
import { after, NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/app/(auth)/auth";
import { executeConfirmedStoryboardTask } from "@/lib/video-agent/execute-storyboard-task";
import { getVideoStoryboardTaskDetail, markVideoStoryboardConfirmed } from "@/lib/video-agent/task-store";
import {
requireStoryboardTaskAccess,
StoryboardTaskAccessError,
} from "@/lib/video-agent/user-task-access";
const paramsSchema = z.object({
taskId: z.string().uuid(),
});
export async function POST(
_request: Request,
{ params }: { params: Promise<{ taskId: string }> }
) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
}
const parsed = paramsSchema.parse(await params);
const detail = await requireStoryboardTaskAccess({
taskId: parsed.taskId,
userId: session.user.id,
});
const task = detail.task as any;
const status = String(task.status || "").trim();
const confirmationStatus = String(task.confirmationStatus || "").trim();
if (status === "cancelled") {
return NextResponse.json(
{ success: false, error: "任务已取消,无法确认执行" },
{ status: 400 }
);
}
if (["succeeded", "partial_success", "failed"].includes(status)) {
return NextResponse.json(
{ success: false, error: "任务已结束,无法再次确认执行" },
{ status: 400 }
);
}
if (status === "running" && confirmationStatus === "confirmed") {
return NextResponse.json(
{ success: true, queued: false, task: detail.task, shots: detail.shots },
{ status: 200 }
);
}
await markVideoStoryboardConfirmed({
taskId: parsed.taskId,
});
after(async () => {
try {
await executeConfirmedStoryboardTask({
taskId: parsed.taskId,
});
} catch (error) {
console.error("[Storyboard Confirm API] Background execution failed:", error);
}
});
const nextDetail = await getVideoStoryboardTaskDetail(parsed.taskId);
return NextResponse.json(
{
success: true,
queued: true,
task: nextDetail?.task || detail.task,
shots: nextDetail?.shots || detail.shots,
},
{ status: 202 }
);
} catch (error) {
if (error instanceof StoryboardTaskAccessError) {
return NextResponse.json(
{ success: false, error: error.message },
{ status: error.status }
);
}
if (error instanceof z.ZodError) {
return NextResponse.json(
{ success: false, error: error.message },
{ status: 400 }
);
}
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : "确认执行失败",
},
{ status: 500 }
);
}
}

View File

@ -0,0 +1,56 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/app/(auth)/auth";
import {
requireStoryboardTaskAccess,
StoryboardTaskAccessError,
} from "@/lib/video-agent/user-task-access";
const paramsSchema = z.object({
taskId: z.string().uuid(),
});
export async function GET(
_request: Request,
{ params }: { params: Promise<{ taskId: string }> }
) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
}
const parsed = paramsSchema.parse(await params);
const detail = await requireStoryboardTaskAccess({
taskId: parsed.taskId,
userId: session.user.id,
});
return NextResponse.json({
success: true,
task: detail.task,
shots: detail.shots,
});
} catch (error) {
if (error instanceof StoryboardTaskAccessError) {
return NextResponse.json(
{ success: false, error: error.message },
{ status: error.status }
);
}
if (error instanceof z.ZodError) {
return NextResponse.json(
{ success: false, error: error.message },
{ status: 400 }
);
}
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : "查询任务失败",
},
{ status: 500 }
);
}
}

View File

@ -0,0 +1,73 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/app/(auth)/auth";
import { regenerateStoryboardShotHd } from "@/lib/video-agent/revise-storyboard-task";
import {
requireStoryboardTaskAccess,
StoryboardTaskAccessError,
} from "@/lib/video-agent/user-task-access";
const paramsSchema = z.object({
taskId: z.string().uuid(),
shotIndex: z.coerce.number().int().min(1).max(99),
});
export async function POST(
_request: Request,
{ params }: { params: Promise<{ taskId: string; shotIndex: string }> }
) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
}
const parsedParams = paramsSchema.parse(await params);
const detail = await requireStoryboardTaskAccess({
taskId: parsedParams.taskId,
userId: session.user.id,
});
const status = String((detail.task as any).status || "").trim();
const confirmationStatus = String((detail.task as any).confirmationStatus || "").trim();
if (status === "running" && confirmationStatus === "confirmed") {
return NextResponse.json(
{ success: false, error: "任务正在执行视频,暂时不能重生高清预览" },
{ status: 409 }
);
}
const result = await regenerateStoryboardShotHd({
taskId: parsedParams.taskId,
shotIndex: parsedParams.shotIndex,
});
return NextResponse.json({
success: result.ok,
message: result.message,
task: result.detail?.task || detail.task,
shots: result.detail?.shots || detail.shots,
});
} catch (error) {
if (error instanceof StoryboardTaskAccessError) {
return NextResponse.json(
{ success: false, error: error.message },
{ status: error.status }
);
}
if (error instanceof z.ZodError) {
return NextResponse.json(
{ success: false, error: error.message },
{ status: 400 }
);
}
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : "重生高清预览失败",
},
{ status: 500 }
);
}
}

View File

@ -0,0 +1,103 @@
import { NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/app/(auth)/auth";
import {
getVideoStoryboardTaskDetail,
markVideoStoryboardDraftForRevision,
updateVideoStoryboardShotPrompt,
} from "@/lib/video-agent/task-store";
import { updateStoryboardShotPromptAndRegenerateHd } from "@/lib/video-agent/revise-storyboard-task";
import {
requireStoryboardTaskAccess,
StoryboardTaskAccessError,
} from "@/lib/video-agent/user-task-access";
const paramsSchema = z.object({
taskId: z.string().uuid(),
shotIndex: z.coerce.number().int().min(1).max(99),
});
const bodySchema = z.object({
prompt: z.string().trim().min(1).max(4000),
regenerateHd: z.boolean().optional().default(true),
});
export async function PATCH(
request: Request,
{ params }: { params: Promise<{ taskId: string; shotIndex: string }> }
) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
}
const parsedParams = paramsSchema.parse(await params);
const parsedBody = bodySchema.parse(await request.json().catch(() => ({})));
const detail = await requireStoryboardTaskAccess({
taskId: parsedParams.taskId,
userId: session.user.id,
});
const status = String((detail.task as any).status || "").trim();
const confirmationStatus = String((detail.task as any).confirmationStatus || "").trim();
if (status === "running" && confirmationStatus === "confirmed") {
return NextResponse.json(
{ success: false, error: "任务正在执行视频,暂时不能修改镜头" },
{ status: 409 }
);
}
if (parsedBody.regenerateHd) {
const result = await updateStoryboardShotPromptAndRegenerateHd({
taskId: parsedParams.taskId,
shotIndex: parsedParams.shotIndex,
prompt: parsedBody.prompt,
});
return NextResponse.json({
success: result.ok,
message: result.message,
task: result.detail?.task || detail.task,
shots: result.detail?.shots || detail.shots,
});
}
await markVideoStoryboardDraftForRevision({
taskId: parsedParams.taskId,
});
await updateVideoStoryboardShotPrompt({
taskId: parsedParams.taskId,
shotIndex: parsedParams.shotIndex,
prompt: parsedBody.prompt,
});
const nextDetail = await getVideoStoryboardTaskDetail(parsedParams.taskId);
return NextResponse.json({
success: true,
message: `已更新第 ${parsedParams.shotIndex} 镜头文案。`,
task: nextDetail?.task || detail.task,
shots: nextDetail?.shots || detail.shots,
});
} catch (error) {
if (error instanceof StoryboardTaskAccessError) {
return NextResponse.json(
{ success: false, error: error.message },
{ status: error.status }
);
}
if (error instanceof z.ZodError) {
return NextResponse.json(
{ success: false, error: error.message },
{ status: 400 }
);
}
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : "更新镜头失败",
},
{ status: 500 }
);
}
}

View File

@ -1,59 +0,0 @@
"use client";
import { useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Loader2 } from "lucide-react";
// 默认进入最近画布,避免误触直接新建
export default function CanvasRedirectPage() {
const router = useRouter();
const searchParams = useSearchParams();
useEffect(() => {
const shouldCreateNew = searchParams.get("new") === "1";
const openCanvas = async () => {
if (shouldCreateNew) {
const newId = crypto.randomUUID();
router.replace(`/canvas/${newId}`);
return;
}
try {
const response = await fetch("/api/canvas", { cache: "no-store" });
if (response.ok) {
const result = await response.json();
const items = result?.data || [];
if (Array.isArray(items) && items.length > 0) {
const sorted = [...items].sort((a, b) => {
const aTime = new Date(a.updatedAt || a.createdAt || 0).getTime();
const bTime = new Date(b.updatedAt || b.createdAt || 0).getTime();
return bTime - aTime;
});
const latest = sorted[0];
if (latest?.id) {
router.replace(`/canvas/${latest.id}`);
return;
}
}
}
} catch (_error) {
// ignore and fallback
}
const newId = crypto.randomUUID();
router.replace(`/canvas/${newId}`);
};
void openCanvas();
}, [router, searchParams]);
return (
<div className="flex items-center justify-center h-full">
<div className="flex flex-col items-center gap-3">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
<span className="text-sm text-muted-foreground">正在打开画布...</span>
</div>
</div>
);
}

View File

@ -1,83 +1,5 @@
import { cookies } from "next/headers";
import { notFound, redirect } from "next/navigation";
import { Suspense } from "react";
import { redirect } from "next/navigation";
import { auth } from "@/app/(auth)/auth";
import { Chat } from "@/components/chat";
import { DataStreamHandler } from "@/components/data-stream-handler";
import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models";
import { getChatById, getMessagesByChatId } from "@/lib/db/queries";
import { convertToUIMessages } from "@/lib/utils";
export default function Page(props: { params: Promise<{ id: string }> }) {
return (
<Suspense fallback={<div className="flex h-dvh" />}>
<ChatPage params={props.params} />
</Suspense>
);
}
async function ChatPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const chat = await getChatById({ id });
if (!chat) {
notFound();
}
const session = await auth();
// 如果用户未登录,重定向到登录页面
if (!session?.user) {
redirect("/login");
}
if (chat.visibility === "private") {
if (session.user.id !== chat.userId) {
return notFound();
}
}
const messagesFromDb = await getMessagesByChatId({
id,
});
const uiMessages = convertToUIMessages(messagesFromDb);
const cookieStore = await cookies();
const chatModelFromCookie = cookieStore.get("chat-model");
if (!chatModelFromCookie) {
return (
<>
<Chat
autoResume={true}
id={chat.id}
initialChatModel={DEFAULT_CHAT_MODEL}
initialLastContext={chat.lastContext ?? undefined}
initialMessages={uiMessages}
initialVisibilityType={chat.visibility}
isReadonly={session?.user?.id !== chat.userId}
userType={session?.user?.type}
/>
<DataStreamHandler />
</>
);
}
return (
<>
<Chat
autoResume={true}
id={chat.id}
initialChatModel={chatModelFromCookie.value}
initialLastContext={chat.lastContext ?? undefined}
initialMessages={uiMessages}
initialVisibilityType={chat.visibility}
isReadonly={session?.user?.id !== chat.userId}
userType={session?.user?.type}
/>
<DataStreamHandler />
</>
);
export default function Page() {
redirect("/");
}

View File

@ -0,0 +1,12 @@
export default function FlowBoardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<link href="/vendor/drawnix.css" rel="stylesheet" />
{children}
</>
);
}

View File

@ -0,0 +1,21 @@
import type { Metadata } from "next";
import { FlowBoardClient } from "@/components/flow-board/flow-board-client";
import { getSiteSettings } from "@/lib/db/queries";
export const metadata: Metadata = {
title: "流程白板",
};
export const dynamic = "force-dynamic";
export default async function FlowBoardPage() {
const siteSettings = await getSiteSettings();
return (
<FlowBoardClient
siteLogo={siteSettings.siteLogo || null}
siteLogoDark={siteSettings.siteLogoDark || null}
siteName={siteSettings.siteName || "AI Chatbot"}
/>
);
}

View File

@ -9,6 +9,8 @@ import { Input } from "@/components/ui/input";
import Image from "next/image";
import { toast } from "sonner";
import { requestLongTaskResult } from "@/lib/client/long-task";
import { buildImageProxyUrl } from "@/lib/image-delivery";
import { inferOriginalImageUrl } from "@/lib/image-url";
import { formatBeijingTime } from "@/lib/utils";
type GalleryImage = {
@ -30,6 +32,35 @@ type Pagination = {
totalPages: number;
};
function buildGalleryDownloadFilename(url: string, alt: string) {
const rawUrl = String(url || "").trim();
const rawAlt = String(alt || "").trim();
const safeBase =
rawAlt
.replace(/[<>:"/\\|?*\x00-\x1F]/g, "-")
.replace(/\s+/g, " ")
.trim()
.slice(0, 80) || "gallery-image";
let extension = "png";
try {
const pathname = new URL(rawUrl).pathname;
const candidate = pathname.split("/").pop() || "";
const inferred = candidate.split(".").pop()?.trim().toLowerCase();
if (inferred && /^[a-z0-9]{2,5}$/.test(inferred)) {
extension = inferred;
}
} catch {
const candidate = rawUrl.split("/").pop()?.split("?")[0] || "";
const inferred = candidate.split(".").pop()?.trim().toLowerCase();
if (inferred && /^[a-z0-9]{2,5}$/.test(inferred)) {
extension = inferred;
}
}
return `${safeBase}-${Date.now()}.${extension}`;
}
export default function GalleryPage() {
const router = useRouter();
const [images, setImages] = useState<GalleryImage[]>([]);
@ -72,19 +103,41 @@ export default function GalleryPage() {
};
const handleDownload = async (url: string, alt: string) => {
const originalUrl = inferOriginalImageUrl(String(url || "").trim());
if (!originalUrl) {
toast.error("图片地址无效");
return;
}
try {
const response = await fetch(url);
toast.info("正在准备下载...");
const response = await fetch(
buildImageProxyUrl(originalUrl, { delivery: "proxy" })
);
if (!response.ok) {
throw new Error(`download failed: ${response.status}`);
}
const blob = await response.blob();
const blobUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = blobUrl;
link.download = `${alt || "image"}-${Date.now()}.png`;
link.download = buildGalleryDownloadFilename(originalUrl, alt);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
window.setTimeout(() => window.URL.revokeObjectURL(blobUrl), 1000);
toast.success("图片已开始下载");
} catch (error) {
console.error("Download failed:", error);
const fallbackLink = document.createElement("a");
fallbackLink.href = originalUrl;
fallbackLink.target = "_blank";
fallbackLink.rel = "noopener noreferrer";
document.body.appendChild(fallbackLink);
fallbackLink.click();
document.body.removeChild(fallbackLink);
toast.error("下载失败,已为你打开原图,可直接另存为");
}
};
@ -485,13 +538,17 @@ export default function GalleryPage() {
/>
</div>
</div>
<div className="p-4 border-t bg-background">
<div className="border-t bg-background p-4">
<div className="flex items-center justify-between">
<div className="flex-1">
<p className="font-medium">{selectedImage.alt}</p>
<p className="text-sm text-muted-foreground mt-1">
{formatBeijingTime(selectedImage.createdAt)}
</p>
<div className="flex flex-wrap items-center gap-3 text-sm">
<span className="font-medium text-foreground">
模型 ID: {selectedImage.modelId || "未知模型"}
</span>
<span className="text-muted-foreground">
{formatBeijingTime(selectedImage.createdAt)}
</span>
</div>
</div>
<Button
onClick={() => handleDownload(selectedImage.url, selectedImage.alt)}

View File

@ -1,5 +1,3 @@
import { cookies } from "next/headers";
import Script from "next/script";
import { Suspense } from "react";
import { ChatShell } from "@/components/chat-shell";
import { DataStreamProvider } from "@/components/data-stream-provider";
@ -7,49 +5,42 @@ import { auth } from "../(auth)/auth";
import {
getNavLinks,
getSiteSettings,
getSystemSetting,
getTutorialVideos,
} from "@/lib/db/queries";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<Script
src="https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js"
strategy="beforeInteractive"
/>
<DataStreamProvider>
<Suspense fallback={<div className="flex h-dvh" />}>
<SidebarWrapper>{children}</SidebarWrapper>
</Suspense>
</DataStreamProvider>
</>
<DataStreamProvider>
<Suspense fallback={<div className="flex h-dvh" />}>
<SidebarWrapper>{children}</SidebarWrapper>
</Suspense>
</DataStreamProvider>
);
}
async function SidebarWrapper({ children }: { children: React.ReactNode }) {
const [session, cookieStore, siteSettings, navLinks, tutorialVideos, homeVariantSetting] =
const [session, siteSettings, navLinks, tutorialVideos] =
await Promise.all([
auth(),
cookies(),
getSiteSettings(),
getNavLinks(),
getTutorialVideos(),
getSystemSetting({ key: "home_variant" }).catch(() => undefined),
]);
const isCollapsed = cookieStore.get("sidebar_state")?.value !== "true";
const homeVariant =
homeVariantSetting?.value === "lovart" ? "lovart" : "classic";
const settingsHref =
session?.user?.type === "admin"
? "/settings"
: session?.user?.type === "agent_admin"
? "/settings/agent-workflow"
: null;
return (
<ChatShell
user={session?.user}
isCollapsed={isCollapsed}
homeVariant={homeVariant}
siteName={siteSettings.siteName}
siteLogo={siteSettings.siteLogo || undefined}
siteLogoDark={siteSettings.siteLogoDark || undefined}
isAdmin={session?.user?.type === "admin"}
isAdmin={session?.user?.type === "admin" || session?.user?.type === "agent_admin"}
settingsHref={settingsHref}
customLinks={navLinks.map((link) => ({
id: link.id,
name: link.name,
@ -71,6 +62,9 @@ async function SidebarWrapper({ children }: { children: React.ReactNode }) {
}))}
tutorialEnabled={siteSettings.tutorialEnabled ?? false}
rankingEnabled={siteSettings.rankingEnabled ?? true}
productPlanningEnabled={(siteSettings as any).productPlanningEnabled ?? true}
agentEnabled={(siteSettings as any).agentEnabled ?? true}
videoCanvasEnabled={(siteSettings as any).videoCanvasEnabled ?? true}
>
{children}
</ChatShell>

View File

@ -0,0 +1,73 @@
import { randomUUID } from "node:crypto";
import { redirect } from "next/navigation";
import { auth } from "@/app/(auth)/auth";
import { db } from "@/lib/db";
import { canvasProject } from "@/lib/db/schema";
import { and, desc, eq } from "drizzle-orm";
export const dynamic = "force-dynamic";
type Model3dRedirectPageProps = {
searchParams?: Promise<Record<string, string | string[] | undefined>>;
};
function getSearchParam(
params: Record<string, string | string[] | undefined>,
key: string
) {
const value = params[key];
return Array.isArray(value) ? value[0] : value;
}
function buildModel3dUrl(
canvasId: string,
params: Record<string, string | string[] | undefined>
) {
const query = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (key === "new" || value === undefined) {
continue;
}
if (Array.isArray(value)) {
value.forEach((item) => query.append(key, item));
continue;
}
query.set(key, value);
}
const queryString = query.toString();
return `/canvas/${canvasId}/model3d${queryString ? `?${queryString}` : ""}`;
}
async function getLatestCanvasId(userId: string) {
const [latestProject] = await db
.select({ id: canvasProject.id })
.from(canvasProject)
.where(
and(
eq(canvasProject.userId, userId),
eq(canvasProject.workspaceType, "canvas")
)
)
.orderBy(desc(canvasProject.updatedAt))
.limit(1);
return latestProject?.id || null;
}
export default async function Model3dRedirectPage({
searchParams,
}: Model3dRedirectPageProps) {
const params = (await searchParams) || {};
const session = await auth();
if (!session?.user?.id) {
redirect("/login?callbackUrl=%2Fmodel3d");
}
const shouldCreateNew = getSearchParam(params, "new") === "1";
if (shouldCreateNew) {
redirect(buildModel3dUrl(randomUUID(), params));
}
const latestCanvasId = await getLatestCanvasId(session.user.id).catch(() => null);
redirect(buildModel3dUrl(latestCanvasId || randomUUID(), params));
}

View File

@ -1,165 +0,0 @@
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { Suspense } from "react";
import { Chat } from "@/components/chat";
import { DataStreamHandler } from "@/components/data-stream-handler";
import { LovartHome } from "@/components/home/lovart-home";
import { generateUUID } from "@/lib/utils";
import { auth } from "../(auth)/auth";
import {
getDefaultAiModel,
getEnabledAiModelsWithProvider,
getUserById,
getSiteSettings,
getSystemSetting,
} from "@/lib/db/queries";
import { isAiModelAgentAuto } from "@/lib/db/ai-model-flags";
const VIDEO_PROVIDER_TYPES = new Set([
"volcano-video",
"doubao-video",
"qianwen-video",
"fal-video",
"ruxa-video",
]);
function isNanoBananaProModel(model: { displayName?: string | null; modelId?: string | null }) {
const displayName = String(model.displayName || "").toLowerCase();
const modelId = String(model.modelId || "").toLowerCase();
return (
displayName.includes("nano banana pro") ||
displayName.includes("nano-banana-pro") ||
modelId.includes("nano-banana-pro") ||
modelId.includes("nano_banana_pro") ||
modelId.includes("nano-banana")
);
}
export default function Page() {
return (
<Suspense fallback={<div className="flex h-dvh" />}>
<NewChatPage />
</Suspense>
);
}
async function NewChatPage() {
const id = generateUUID();
const [session, cookieStore, siteSettings, homeVariantSetting, surpriseCategoriesSetting] =
await Promise.all([
auth(),
cookies(),
getSiteSettings(),
getSystemSetting({ key: "home_variant" }).catch(() => undefined),
getSystemSetting({ key: "home_surprise_categories" }).catch(() => undefined),
]);
const homeVariant = homeVariantSetting?.value === "lovart" ? "lovart" : "classic";
const surpriseCategories = Array.isArray(surpriseCategoriesSetting?.value)
? surpriseCategoriesSetting?.value
.map((item: unknown) => String(item || "").trim())
.filter(Boolean)
: ["全部", "精选"];
if (!session?.user && homeVariant !== "lovart") {
redirect("/login");
}
if (homeVariant === "lovart") {
return (
<LovartHome
user={
session?.user
? {
id: session.user.id,
email: session.user.email,
name: session.user.name,
type: session.user.type,
}
: null
}
isAuthenticated={Boolean(session?.user)}
siteName={siteSettings.siteName || "AI Chatbot"}
siteLogo={siteSettings.siteLogo || siteSettings.siteLogoDark || ""}
surpriseCategories={surpriseCategories}
heroPromoText={siteSettings.lovartPromoText}
heroPromoActionText={siteSettings.lovartPromoActionText}
heroTitleText={siteSettings.lovartHeroTitle}
heroSubtitleText={siteSettings.lovartHeroSubtitle}
/>
);
}
const modelIdFromCookie = cookieStore.get("chat-model");
let initialModelId = modelIdFromCookie?.value;
const currentUser = session?.user?.id
? await getUserById({ id: session.user.id }).catch(() => undefined)
: undefined;
const allowHiddenModels =
session?.user?.type === "admin" || Boolean(currentUser?.isHighVip);
const defaultModel = await getDefaultAiModel();
const enabledModels = await getEnabledAiModelsWithProvider(false, true);
const visibleModels = enabledModels.filter(
({ model }) =>
!isAiModelAgentAuto(model) &&
(allowHiddenModels || !model.isHidden)
);
const preferredImageModels = visibleModels.filter(
({ model, provider }) =>
model.supportsImageGen &&
!VIDEO_PROVIDER_TYPES.has(provider.type)
);
const nanoBananaModel = preferredImageModels.find(({ model }) =>
isNanoBananaProModel(model)
);
const cookieModel = initialModelId
? visibleModels.find(({ model }) => model.id === initialModelId)
: undefined;
if (
!initialModelId ||
!cookieModel
) {
if (nanoBananaModel) {
initialModelId = nanoBananaModel.model.id;
} else if (
defaultModel &&
!isAiModelAgentAuto(defaultModel) &&
(allowHiddenModels || !defaultModel.isHidden) &&
preferredImageModels.some(({ model }) => model.id === defaultModel.id)
) {
initialModelId = defaultModel.id;
} else if (preferredImageModels.length > 0) {
const preferredDefaultImageModel = preferredImageModels.find(
({ model }) => model.isDefault
);
initialModelId = (preferredDefaultImageModel || preferredImageModels[0]).model.id;
} else if (
defaultModel &&
!isAiModelAgentAuto(defaultModel) &&
(allowHiddenModels || !defaultModel.isHidden)
) {
initialModelId = defaultModel.id;
} else if (visibleModels.length > 0) {
initialModelId = visibleModels[0].model.id;
}
}
return (
<>
<Chat
autoResume={false}
id={id}
initialChatModel={initialModelId || ""}
initialMessages={[]}
initialVisibilityType="private"
isReadonly={false}
key={id}
userType={session?.user?.type}
/>
<DataStreamHandler />
</>
);
}

Some files were not shown because too many files have changed in this diff Show More