@tomako/tools-runtime 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/components/tools/engine/package-widget-runtime.tsx +5 -1
- package/src/components/tools/engine/task-recovery.test.ts +65 -0
- package/src/components/tools/engine/task-recovery.ts +109 -0
- package/src/components/tools/engine/use-legacy-llm-task.ts +16 -3
- package/src/components/tools/engine/use-tool-task.ts +77 -53
- package/src/features/tools/app-icon-resizer/app-icon-resizer.spec.ts +1 -1
- package/src/features/tools/app-store-screenshot-generator/app-store-screenshot-generator.container.tsx +6 -8
- package/src/features/tools/app-store-screenshot-generator/app-store-screenshot-generator.spec.ts +1 -1
- package/src/features/tools/app-store-screenshot-generator/widget/app-store-screenshot-generator.tsx +44 -6
- package/src/features/tools/app-store-screenshot-generator/widget/generation.test.ts +126 -0
- package/src/features/tools/app-store-screenshot-generator/widget/generation.ts +52 -14
- package/src/features/tools/cold-start-channel-selector/cold-start-channel-selector.spec.ts +1 -1
- package/src/features/tools/cold-start-channel-selector/widget/cold-start-channel-selector.tsx +45 -16
- package/src/features/tools/competitor-analysis/competitor-analysis.container.tsx +31 -26
- package/src/features/tools/competitor-analysis/competitor-analysis.spec.ts +1 -1
- package/src/features/tools/competitor-analysis/widget/competitor-analysis.tsx +47 -16
- package/src/features/tools/disclaimer-generator/disclaimer-generator.package.ts +4 -4
- package/src/features/tools/email-template-generator/email-template-generator.container.tsx +45 -173
- package/src/features/tools/email-template-generator/email-template-generator.spec.ts +1 -1
- package/src/features/tools/email-template-generator/widget/email-template-generator.tsx +37 -4
- package/src/features/tools/package/to-tool-module.ts +15 -0
- package/src/features/tools/product-name-generator/product-name-generator.spec.ts +1 -1
- package/src/features/tools/product-name-generator/widget/product-name-generator.tsx +60 -27
- package/src/features/tools/product-poster-generator/product-poster-generator.spec.ts +1 -1
- package/src/features/tools/product-poster-generator/widget/constants.ts +3 -1
- package/src/features/tools/product-poster-generator/widget/helpers.test.ts +51 -0
- package/src/features/tools/product-poster-generator/widget/helpers.ts +27 -0
- package/src/features/tools/product-poster-generator/widget/product-poster-generator.tsx +112 -12
- package/src/features/tools/slogan-generator/slogan-generator.package.ts +101 -29
- package/src/features/tools/twitter-gif-downloader/twitter-gif-downloader.container.tsx +2 -3
- package/src/features/tools/twitter-gif-downloader/twitter-gif-downloader.spec.ts +1 -1
- package/src/i18n/messages/en/tools/app-icon-resizer.ts +1 -1
- package/src/i18n/messages/en/tools/app-store-screenshot-generator.ts +3 -3
- package/src/i18n/messages/en/tools/cold-start-channel-selector.ts +4 -4
- package/src/i18n/messages/en/tools/competitor-analysis.ts +14 -10
- package/src/i18n/messages/en/tools/email-template-generator.ts +13 -13
- package/src/i18n/messages/en/tools/product-poster-generator.ts +3 -2
- package/src/i18n/messages/en/tools/twitter-gif-downloader.ts +1 -1
- package/src/i18n/messages/zh/tools/app-store-screenshot-generator.ts +4 -4
- package/src/i18n/messages/zh/tools/cold-start-channel-selector.ts +2 -2
- package/src/i18n/messages/zh/tools/competitor-analysis.ts +14 -10
- package/src/i18n/messages/zh/tools/email-template-generator.ts +12 -12
- package/src/i18n/messages/zh/tools/product-poster-generator.ts +2 -1
- package/src/i18n/messages/zh/tools/twitter-gif-downloader.ts +1 -1
- package/src/i18n/messages/zh-tw/tools/app-store-screenshot-generator.ts +44 -44
- package/src/i18n/messages/zh-tw/tools/cold-start-channel-selector.ts +105 -105
- package/src/i18n/messages/zh-tw/tools/competitor-analysis.ts +44 -40
- package/src/i18n/messages/zh-tw/tools/email-template-generator.ts +112 -112
- package/src/i18n/messages/zh-tw/tools/product-name-generator.ts +92 -92
- package/src/i18n/messages/zh-tw/tools/product-poster-generator.ts +74 -73
- package/src/i18n/messages/zh-tw/tools/twitter-gif-downloader.ts +48 -48
- package/src/lib/tools/cold-start-channel-selector-schema.test.ts +39 -0
- package/src/lib/tools/cold-start-channel-selector-schema.ts +8 -4
- package/src/lib/tools/competitor-analysis-schema.test.ts +69 -0
- package/src/lib/tools/competitor-analysis-schema.ts +53 -5
- package/src/lib/tools/ops-catalog.ts +10 -1
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
const productPosterGenerator = {
|
|
2
2
|
meta: {
|
|
3
|
-
title: "AI
|
|
4
|
-
displayTitle: "AI
|
|
3
|
+
title: "AI 產品海報產生器:用截圖、賣點和 CTA 製作宣傳圖",
|
|
4
|
+
displayTitle: "AI產品海報產生器",
|
|
5
5
|
description:
|
|
6
|
-
"
|
|
6
|
+
"輸入產品名稱、賣點、目標使用者、CTA,可補充產品 URL、Logo 和截圖,產生適合官網、社群媒體、廣告和 OG 分享的產品宣傳海報圖。",
|
|
7
7
|
intro: "把產品上下文整理成可下載的宣傳海報圖。",
|
|
8
|
-
category: "SEO
|
|
8
|
+
category: "SEO 行銷工具",
|
|
9
9
|
keywords: [
|
|
10
|
-
"AI
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"SaaS
|
|
10
|
+
"AI 產品海報產生器",
|
|
11
|
+
"產品宣傳海報產生器",
|
|
12
|
+
"產品宣傳圖產生器",
|
|
13
|
+
"SaaS 行銷圖產生器",
|
|
14
14
|
"產品截圖海報",
|
|
15
|
-
"App
|
|
15
|
+
"App 宣傳海報產生器",
|
|
16
16
|
"Web 產品廣告圖",
|
|
17
17
|
"品牌一致廣告圖",
|
|
18
18
|
],
|
|
@@ -24,26 +24,26 @@ const productPosterGenerator = {
|
|
|
24
24
|
{
|
|
25
25
|
label: "SaaS 官網宣傳圖",
|
|
26
26
|
input: "產品名稱、Dashboard 截圖、3 條核心賣點、CTA:Book demo",
|
|
27
|
-
expectedOutput: "
|
|
27
|
+
expectedOutput: "一張適合官網首屏和社群媒體發佈的產品海報圖,附帶發佈前複核清單。",
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
label: "App 發佈海報",
|
|
31
|
-
input: "App 截圖、Logo
|
|
32
|
-
expectedOutput: "
|
|
31
|
+
input: "App 截圖、Logo、目標使用者、通路:Product Hunt / X",
|
|
32
|
+
expectedOutput: "發佈海報預覽圖、通路比例線索、素材使用狀態和下一步檢查項。",
|
|
33
33
|
},
|
|
34
34
|
],
|
|
35
35
|
guide: {
|
|
36
|
-
workspaceHeading: "AI
|
|
36
|
+
workspaceHeading: "AI 產品海報產生器",
|
|
37
37
|
workspaceHint:
|
|
38
|
-
"
|
|
39
|
-
workspaceBadge: "
|
|
38
|
+
"填寫產品名稱、賣點、目標使用者、CTA 和用途;需要時補充 URL、Logo、截圖、尺寸和品牌色。產生後可直接預覽並下載 PNG。",
|
|
39
|
+
workspaceBadge: "產生 PNG 海報",
|
|
40
40
|
standard: {
|
|
41
41
|
benefits: {
|
|
42
|
-
title: "這個 AI
|
|
42
|
+
title: "這個 AI 產品海報產生器能幫你完成什麼",
|
|
43
43
|
cards: [
|
|
44
44
|
{
|
|
45
|
-
title: "
|
|
46
|
-
body: "
|
|
45
|
+
title: "產生可下載的產品海報圖",
|
|
46
|
+
body: "把產品名稱、核心賣點、目標使用者和 CTA 組合成一張可預覽、可下載的產品宣傳海報圖。",
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
title: "保持產品與品牌一致",
|
|
@@ -58,35 +58,35 @@ const productPosterGenerator = {
|
|
|
58
58
|
imageTextSections: [
|
|
59
59
|
{
|
|
60
60
|
id: "poster-brief",
|
|
61
|
-
title: "從產品上下文\n
|
|
61
|
+
title: "從產品上下文\n產生可下載海報圖",
|
|
62
62
|
body: [
|
|
63
|
-
"
|
|
64
|
-
"
|
|
63
|
+
"產品宣傳圖不只是好看的背景。它需要說清產品是什麼、給誰用、主推什麼價值,以及希望使用者點擊什麼 CTA。",
|
|
64
|
+
"你先填寫少量核心資訊,工具會把產品說明、受眾、用途和素材整理成一張產品海報預覽,適合繼續交給官網、社群媒體或增長流程使用。",
|
|
65
65
|
],
|
|
66
66
|
image: {
|
|
67
67
|
src: "/tools/product-poster-generator-section-primary-8bit-v2.webp",
|
|
68
|
-
alt: "像素風 AI
|
|
68
|
+
alt: "像素風 AI 產品海報產生器配圖,展示產品上下文變成可下載海報圖。",
|
|
69
69
|
},
|
|
70
70
|
imageFirst: true,
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
id: "poster-variations",
|
|
74
|
-
title: "同一套產品素材\n
|
|
74
|
+
title: "同一套產品素材\n適配不同發佈通路",
|
|
75
75
|
body: [
|
|
76
|
-
"
|
|
77
|
-
"
|
|
76
|
+
"官網首屏、社群媒體發佈、廣告測試、Product Hunt 和 OG 分享圖的資訊密度不同,不能只靠同一張圖硬裁。",
|
|
77
|
+
"你可以選擇目標用途、尺寸和視覺風格,讓海報先按通路安排標題、截圖、賣點和 CTA 的優先級。",
|
|
78
78
|
],
|
|
79
79
|
image: {
|
|
80
80
|
src: "/tools/product-poster-generator-section-variation-8bit-v2.webp",
|
|
81
|
-
alt: "
|
|
81
|
+
alt: "像素風產品海報通路適配配圖,展示 Web、Social 和 Ad 等多種比例。",
|
|
82
82
|
},
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
id: "poster-review",
|
|
86
86
|
title: "下載前先複核\n截圖、承諾和裁切",
|
|
87
87
|
body: [
|
|
88
|
-
"
|
|
89
|
-
"下載前請確認 UI
|
|
88
|
+
"產生圖可以進入設計、發佈或投放流程,但不等於已經完成品牌、法務和平臺審核。",
|
|
89
|
+
"下載前請確認 UI 截圖沒有客戶隱私或測試資料,廣告承諾沒有誇大,品牌素材有權使用,並按目標通路檢查裁切安全區。",
|
|
90
90
|
],
|
|
91
91
|
image: {
|
|
92
92
|
src: "/tools/product-poster-generator-section-support-8bit-v2.webp",
|
|
@@ -96,25 +96,25 @@ const productPosterGenerator = {
|
|
|
96
96
|
},
|
|
97
97
|
],
|
|
98
98
|
generation: {
|
|
99
|
-
title: "
|
|
100
|
-
imageAlt: "
|
|
101
|
-
ctaLabel: "
|
|
99
|
+
title: "如何產生一張\n產品宣傳海報圖",
|
|
100
|
+
imageAlt: "像素風產品海報產生流程配圖,展示從產品 brief 到 PNG 下載。",
|
|
101
|
+
ctaLabel: "產生產品海報",
|
|
102
102
|
steps: [
|
|
103
103
|
{
|
|
104
104
|
title: "填寫產品 brief",
|
|
105
|
-
body: "
|
|
105
|
+
body: "輸入產品名稱、產品上下文、目標使用者和主 CTA。賣點越具體,海報越容易聚焦。",
|
|
106
106
|
},
|
|
107
107
|
{
|
|
108
108
|
title: "選擇海報用途",
|
|
109
|
-
body: "
|
|
109
|
+
body: "從官網、社群媒體、廣告、Product Hunt 或 OG 分享中選擇目標用途,確定資訊層級和基礎比例。",
|
|
110
110
|
},
|
|
111
111
|
{
|
|
112
112
|
title: "補充品牌素材",
|
|
113
|
-
body: "按需上傳 Logo
|
|
113
|
+
body: "按需上傳 Logo、產品截圖,並選擇尺寸、視覺風格和品牌色。沒有素材也可以先產生第一版。",
|
|
114
114
|
},
|
|
115
115
|
{
|
|
116
116
|
title: "預覽並下載 PNG",
|
|
117
|
-
body: "
|
|
117
|
+
body: "產生完成後查看海報圖、素材狀態和發佈前檢查項,再下載 PNG 進入發佈或設計流程。",
|
|
118
118
|
},
|
|
119
119
|
],
|
|
120
120
|
},
|
|
@@ -125,35 +125,35 @@ const productPosterGenerator = {
|
|
|
125
125
|
faq: "常見問題",
|
|
126
126
|
faqItems: [
|
|
127
127
|
{
|
|
128
|
-
question: "
|
|
128
|
+
question: "產品宣傳海報產生需要準備哪些內容?",
|
|
129
129
|
answer:
|
|
130
|
-
"
|
|
130
|
+
"至少需要產品名稱、產品說明、目標使用者和 CTA。更好的輸入包括產品 URL、Logo、1 到 4 張 UI 截圖、品牌色和目標發佈通路。",
|
|
131
131
|
},
|
|
132
132
|
{
|
|
133
|
-
question: "
|
|
133
|
+
question: "這個產品截圖海報產生器適合哪些產品?",
|
|
134
134
|
answer:
|
|
135
|
-
"更適合 SaaS、App、Web 工具、插件、AI
|
|
135
|
+
"更適合 SaaS、App、Web 工具、插件、AI 產品和開發者工具。你可以用它準備官網首屏圖、社群媒體發佈圖、廣告圖、Product Hunt 圖和 OG 分享圖。",
|
|
136
136
|
},
|
|
137
137
|
{
|
|
138
|
-
question: "
|
|
138
|
+
question: "產生的產品宣傳海報圖可以直接商用嗎?",
|
|
139
139
|
answer:
|
|
140
140
|
"海報圖可以進入商用設計流程,但正式發佈前仍要檢查截圖隱私、商標授權、廣告承諾、平臺尺寸和投放規範。",
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
|
-
question: "沒有品牌色或 Logo
|
|
143
|
+
question: "沒有品牌色或 Logo,還能產生 App 宣傳海報嗎?",
|
|
144
144
|
answer:
|
|
145
|
-
"
|
|
145
|
+
"可以。你先提供產品說明、目標使用者和截圖,工具會使用默認視覺方向。品牌穩定後,再用 Logo 和品牌色產生更一致的版本。",
|
|
146
146
|
},
|
|
147
147
|
{
|
|
148
148
|
question: "它和 Canva、Adobe Express 海報模板有什麼區別?",
|
|
149
149
|
answer:
|
|
150
|
-
"通用設計工具適合從模板開始排版。這個工具更適合先理解產品上下文、UI
|
|
150
|
+
"通用設計工具適合從模板開始排版。這個工具更適合先理解產品上下文、UI 截圖、賣點和通路需求,再產生互聯網產品宣傳圖。",
|
|
151
151
|
},
|
|
152
152
|
],
|
|
153
|
-
ctaPrimary: "
|
|
154
|
-
ctaSecondary: "
|
|
153
|
+
ctaPrimary: "產生應用程式商店上架圖",
|
|
154
|
+
ctaSecondary: "先產生 Logo",
|
|
155
155
|
ctaHint:
|
|
156
|
-
"如果你要做 App
|
|
156
|
+
"如果你要做 App 商店素材,可以先產生上架圖。如果品牌標識還不穩定,可以先產生 Logo 方向。",
|
|
157
157
|
},
|
|
158
158
|
widget: {
|
|
159
159
|
productNameLabel: "產品名稱",
|
|
@@ -162,8 +162,8 @@ const productPosterGenerator = {
|
|
|
162
162
|
productUrlPlaceholder: "https://yourproduct.com",
|
|
163
163
|
contextLabel: "產品上下文和核心賣點",
|
|
164
164
|
contextPlaceholder:
|
|
165
|
-
"寫清產品做什麼、最想突出哪 2-3
|
|
166
|
-
audienceLabel: "
|
|
165
|
+
"寫清產品做什麼、最想突出哪 2-3 個賣點。例如:為獨立開發者產生上線所需的行銷物料。",
|
|
166
|
+
audienceLabel: "目標使用者",
|
|
167
167
|
audiencePlaceholder: "例如 SaaS 創始人、增長團隊、獨立開發者",
|
|
168
168
|
ctaLabel: "主 CTA",
|
|
169
169
|
ctaPlaceholder: "Try it free / Book demo / Download app",
|
|
@@ -183,49 +183,50 @@ const productPosterGenerator = {
|
|
|
183
183
|
screenshotRemove: "移除截圖",
|
|
184
184
|
screenshotCount: "{count} 張截圖",
|
|
185
185
|
uploadHint: "PNG / JPG / WebP",
|
|
186
|
-
submit: "
|
|
187
|
-
submitLoading: "
|
|
186
|
+
submit: "產生產品海報",
|
|
187
|
+
submitLoading: "正在產生...",
|
|
188
188
|
reset: "重置",
|
|
189
189
|
reviewNote: "請只上傳可公開素材。下載前複核截圖隱私、品牌授權、廣告承諾和平臺尺寸。",
|
|
190
|
-
emptyPreviewTitle: "
|
|
190
|
+
emptyPreviewTitle: "填寫資訊後產生海報",
|
|
191
191
|
emptyPreviewHint:
|
|
192
|
-
"
|
|
193
|
-
loadingTitle: "
|
|
192
|
+
"產品名稱、賣點、目標使用者、CTA 和用途會決定海報的主標題、畫面層級和下載結果。",
|
|
193
|
+
loadingTitle: "正在產生產品海報",
|
|
194
194
|
loadingTipLabel: "提示",
|
|
195
|
-
loadingTip: "
|
|
195
|
+
loadingTip: "通常需要幾十秒。保持本頁打開,產生完成後會在右側顯示可下載 PNG。",
|
|
196
196
|
loadingSteps: {
|
|
197
197
|
prepare: "整理產品上下文",
|
|
198
198
|
compose: "組合標題、賣點和 CTA",
|
|
199
199
|
render: "渲染海報預覽圖",
|
|
200
200
|
finish: "準備下載結果",
|
|
201
201
|
},
|
|
202
|
-
errorTitle: "
|
|
203
|
-
submitErrorTitle: "
|
|
204
|
-
taskErrorTitle: "
|
|
202
|
+
errorTitle: "先補充關鍵產品資訊",
|
|
203
|
+
submitErrorTitle: "海報產生請求未成功",
|
|
204
|
+
taskErrorTitle: "海報產生失敗",
|
|
205
205
|
resultErrorTitle: "暫時沒有拿到海報結果",
|
|
206
206
|
unreadableResultTitle: "結果暫時無法讀取",
|
|
207
|
-
timeoutErrorTitle: "
|
|
208
|
-
retry: "
|
|
207
|
+
timeoutErrorTitle: "產生時間過長",
|
|
208
|
+
retry: "重新產生",
|
|
209
209
|
errors: {
|
|
210
|
-
required: "
|
|
210
|
+
required: "請補充必填資訊,再產生產品海報。",
|
|
211
211
|
productName: "請填寫產品名稱。它會用於海報標題、文件名和產品定位。",
|
|
212
212
|
productContext: "請補充產品上下文和核心賣點。至少寫清產品做什麼,以及你最想突出的價值。",
|
|
213
|
-
audience: "
|
|
213
|
+
audience: "請填寫目標使用者。比如 SaaS 創始人、增長團隊、獨立開發者或企業採購。",
|
|
214
214
|
primaryCta: "請填寫主 CTA。比如 Try it free、Book demo、立即體驗或下載 App。",
|
|
215
215
|
invalidUrl: "產品 URL 需要是有效的 http 或 https 地址,或者先留空。",
|
|
216
216
|
weakContext: "產品上下文太像佔位內容。請補充產品做什麼、主要賣點是什麼。",
|
|
217
|
-
submitFailed: "
|
|
217
|
+
submitFailed: "海報產生服務暫時不可用,請稍後重試。",
|
|
218
218
|
pollFailed: "還沒有拿到海報結果,請稍後重試。",
|
|
219
|
-
imageFailed: "
|
|
219
|
+
imageFailed: "海報圖沒有產生成功。可以簡化賣點、換一個尺寸,或稍後再試。",
|
|
220
220
|
unreadableResult: "結果暫時無法讀取。請稍後重試。",
|
|
221
|
-
timeout: "
|
|
221
|
+
timeout: "產生時間較長。請稍後重試。",
|
|
222
|
+
downloadFailed: "PNG 下載失敗,請稍後重試。",
|
|
222
223
|
},
|
|
223
|
-
readyKicker: "
|
|
224
|
-
readyTitle: "
|
|
224
|
+
readyKicker: "海報已產生",
|
|
225
|
+
readyTitle: "產品宣傳海報圖已產生",
|
|
225
226
|
readyHint: "你可以預覽並下載 PNG。正式發佈前,請複核截圖隱私、廣告承諾和品牌授權。",
|
|
226
227
|
downloadPng: "下載 PNG",
|
|
227
|
-
generatedPosterAlt: "
|
|
228
|
-
posterDetails: "
|
|
228
|
+
generatedPosterAlt: "根據產品上下文產生的產品宣傳海報預覽圖。",
|
|
229
|
+
posterDetails: "海報設定",
|
|
229
230
|
posterChecklist: "發佈前檢查",
|
|
230
231
|
assetLogoApplied: "Logo:已作為精確圖層合成到最終 PNG",
|
|
231
232
|
assetLogoUploadedNotApplied: "Logo:已上傳,請複核最終圖中是否完成合成",
|
|
@@ -233,20 +234,20 @@ const productPosterGenerator = {
|
|
|
233
234
|
assetScreenshotReferenceCount: "產品截圖:{count} 張已作為生圖參考",
|
|
234
235
|
assetScreenshotInventoryOnly: "產品截圖:{count} 張僅作為文件清單,未進入生圖參考",
|
|
235
236
|
assetScreenshotMissing: "產品截圖:未上傳,生圖不會虛構真實界面",
|
|
236
|
-
assetFinalLogoLayer: "
|
|
237
|
+
assetFinalLogoLayer: "最終下載圖:已保留模型產生圖,併疊加上傳 Logo",
|
|
237
238
|
posterChecklistItems: [
|
|
238
239
|
"確認主標題是否說清產品價值,不要只寫抽象口號。",
|
|
239
|
-
"
|
|
240
|
+
"檢查截圖裡是否包含客戶隱私、測試帳號或未授權品牌。",
|
|
240
241
|
"按目標平臺複核裁切比例,確認 CTA 在移動端也清楚可讀。",
|
|
241
242
|
"正式投放前複核品牌授權、廣告承諾和平臺規範。",
|
|
242
243
|
],
|
|
243
244
|
posterIntentLabels: {
|
|
244
245
|
websiteConversion: "官網首屏轉化",
|
|
245
246
|
websiteFeature: "官網功能介紹",
|
|
246
|
-
socialLaunch: "
|
|
247
|
-
socialContent: "
|
|
247
|
+
socialLaunch: "社群媒體新品發佈",
|
|
248
|
+
socialContent: "社群媒體內容封面",
|
|
248
249
|
paidAcquisition: "廣告獲客測試",
|
|
249
|
-
paidRetargeting: "
|
|
250
|
+
paidRetargeting: "再行銷廣告圖",
|
|
250
251
|
productHuntLaunch: "Product Hunt 發佈",
|
|
251
252
|
ogBlogCover: "博客 / OG 分享圖",
|
|
252
253
|
},
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const twitterGifDownloader = {
|
|
2
2
|
meta: {
|
|
3
|
-
title: "Twitter / X GIF
|
|
4
|
-
description: "貼上公開 X 或 Twitter
|
|
5
|
-
intro: "把公開 X 或 Twitter
|
|
6
|
-
category: "
|
|
3
|
+
title: "Twitter / X GIF 下載器:儲存公開貼文動圖",
|
|
4
|
+
description: "貼上公開 X 或 Twitter 貼文連結,預覽媒體後下載真正的 GIF 或 MP4;儲存前先了解連結、存取與重用邊界。",
|
|
5
|
+
intro: "把公開 X 或 Twitter 貼文中的動圖儲存為可重用檔案。貼上貼文連結、預覽可轉換媒體後,在可用時下載 GIF 或 MP4。",
|
|
6
|
+
category: "社群媒體工具",
|
|
7
7
|
keywords: [
|
|
8
8
|
"Twitter GIF downloader",
|
|
9
9
|
"download Twitter GIF",
|
|
@@ -11,122 +11,122 @@ const twitterGifDownloader = {
|
|
|
11
11
|
"save GIF from Twitter",
|
|
12
12
|
"推特動圖下載",
|
|
13
13
|
],
|
|
14
|
-
inputSignal: "公開 twitter.com 或 x.com
|
|
15
|
-
outputSignal: "GIF 預覽、.gif
|
|
16
|
-
boundarySignal: "
|
|
14
|
+
inputSignal: "公開 twitter.com 或 x.com 貼文連結",
|
|
15
|
+
outputSignal: "GIF 預覽、.gif 檔案、MP4 源檔案",
|
|
16
|
+
boundarySignal: "私密、刪除、受限或登入後內容通常無法解析",
|
|
17
17
|
},
|
|
18
18
|
widget: {
|
|
19
|
-
workspaceTitle: "
|
|
20
|
-
workspaceIntro: "
|
|
21
|
-
urlLabel: "Tweet 或 X
|
|
19
|
+
workspaceTitle: "貼上 Tweet 連結",
|
|
20
|
+
workspaceIntro: "貼上公開 X/Twitter 貼文連結,儲存可預覽的 GIF,並保留 MP4 原始檔。",
|
|
21
|
+
urlLabel: "Tweet 或 X 貼文 URL",
|
|
22
22
|
submit: "下載 Twitter GIF",
|
|
23
23
|
working: "正在處理",
|
|
24
|
-
idleTitle: "
|
|
25
|
-
idleBody: "
|
|
24
|
+
idleTitle: "等待連結",
|
|
25
|
+
idleBody: "貼上一則公開 Tweet 或 X 貼文連結後,這裡會顯示解析進度、GIF 預覽、GIF 下載和 MP4 下載操作。",
|
|
26
26
|
lookupTitle: "正在查找媒體",
|
|
27
27
|
engineTitle: "正在準備轉換",
|
|
28
28
|
convertTitle: "正在轉換 GIF",
|
|
29
29
|
loadingBody: "較長或較大的動圖可能需要幾十秒。請保持頁面打開。",
|
|
30
30
|
doneTitle: "GIF 已準備好",
|
|
31
31
|
errorTitle: "無法完成轉換",
|
|
32
|
-
invalidUrl: "請輸入包含 /status/ 的公開 twitter.com 或 x.com
|
|
32
|
+
invalidUrl: "請輸入包含 /status/ 的公開 twitter.com 或 x.com 貼文連結。",
|
|
33
33
|
lookupError: "媒體解析服務暫時不可用。",
|
|
34
|
-
noMedia: "
|
|
35
|
-
conversionError: "
|
|
34
|
+
noMedia: "這則貼文沒有可轉換的影片或 GIF 媒體,或內容無法公開存取。",
|
|
35
|
+
conversionError: "轉換失敗。請換一則公開 GIF 貼文,或下載 MP4 原始檔後在本機轉換。",
|
|
36
36
|
tryAgain: "重新嘗試",
|
|
37
37
|
downloadGif: "下載 GIF",
|
|
38
38
|
downloadMp4: "下載 MP4",
|
|
39
39
|
previewAlt: "從 Tweet 媒體轉換得到的 GIF 預覽",
|
|
40
40
|
},
|
|
41
41
|
guide: {
|
|
42
|
-
heroVisualAlt: "公開 X
|
|
43
|
-
workspaceHeading: "
|
|
44
|
-
workspaceHint: "貼上帶 /status/
|
|
42
|
+
heroVisualAlt: "公開 X 貼文連結轉換成 GIF 和 MP4 下載檔案的 Hero 背景圖",
|
|
43
|
+
workspaceHeading: "Twitter / X GIF 下載器",
|
|
44
|
+
workspaceHint: "貼上帶 /status/ 的公開貼文連結,預覽可轉換媒體後儲存 GIF 或 MP4。私密、受限、已刪除或沒有媒體的貼文無法使用。",
|
|
45
45
|
workspaceBadge: "公開連結 → GIF 或 MP4",
|
|
46
46
|
resultHeading: "你會得到什麼",
|
|
47
47
|
resultBody: [
|
|
48
|
-
"貼上公開 X/Twitter
|
|
49
|
-
"
|
|
48
|
+
"貼上公開 X/Twitter 貼文連結後,頁面會尋找貼文裡的動圖媒體,顯示處理狀態,並在成功後提供 GIF 預覽、.gif 檔案下載和可用時的 MP4 下載。",
|
|
49
|
+
"許多使用者搜尋 Twitter GIF downloader 或 download Twitter GIF,其實是想把平台裡的動圖儲存成真正可重用的檔案。Tomako 把連結識別、媒體預覽、格式選擇和下載動作放在同一個工作區裡,你可以先確認動圖內容,再決定儲存 GIF 還是保留更小的 MP4。",
|
|
50
50
|
],
|
|
51
|
-
resultVisualAlt: "
|
|
52
|
-
supportVisualAlt: "
|
|
51
|
+
resultVisualAlt: "公開貼文連結轉換成 GIF 和 MP4 下載檔案的素材包視覺",
|
|
52
|
+
supportVisualAlt: "公開連結可下載、受限連結無法解析的邊界說明視覺",
|
|
53
53
|
standardsHeading: "好的 Twitter GIF 下載器應該具備什麼",
|
|
54
54
|
standardsBody: [
|
|
55
|
-
"選擇同類工具時,先看它能不能同時識別 twitter.com 和 x.com
|
|
56
|
-
"Tomako
|
|
57
|
-
"
|
|
55
|
+
"選擇同類工具時,先看它能不能同時識別 twitter.com 和 x.com 公開貼文連結、能不能先預覽再下載、是否提供 GIF 與 MP4 兩種儲存方式,以及失敗原因是否說得明白。",
|
|
56
|
+
"Tomako 把公開連結識別、預覽、格式選擇和轉換放在同一個工作區裡。你不需要自己尋找媒體位址,也不必處理瀏覽器外掛、指令碼或轉換參數。",
|
|
57
|
+
"請只使用你有權存取和儲存的公開貼文連結。下載後的 GIF 或 MP4 是否能重用,取決於原內容版權、平台規則和你的使用情境。",
|
|
58
58
|
],
|
|
59
|
-
comparisonHeading: "
|
|
59
|
+
comparisonHeading: "為什麼比手動找影片位址更省事",
|
|
60
60
|
comparisonBefore: "常見麻煩",
|
|
61
61
|
comparisonAfter: "Tomako 處理方式",
|
|
62
62
|
comparisonItems: [
|
|
63
63
|
{
|
|
64
|
-
pain: "
|
|
65
|
-
tomako: "
|
|
64
|
+
pain: "需要自己從頁面原始碼或開發者工具裡找影片位址。",
|
|
65
|
+
tomako: "貼上公開 twitter.com 或 x.com 貼文連結後,頁面會自動尋找可轉換媒體。",
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
|
-
pain: "
|
|
68
|
+
pain: "下載前不知道儲存的是 GIF、影片,還是無效檔案。",
|
|
69
69
|
tomako: "成功後先顯示 GIF 預覽,再提供 GIF 和 MP4 下載動作。",
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
|
-
pain: "
|
|
73
|
-
tomako: "
|
|
72
|
+
pain: "失敗時只看到空結果,不知道該換連結還是重試。",
|
|
73
|
+
tomako: "結果區會區分連結無效、沒有媒體、受限內容或轉換失敗。",
|
|
74
74
|
},
|
|
75
75
|
],
|
|
76
76
|
resultGuidanceHeading: "下載失敗或拿到結果後怎麼處理",
|
|
77
77
|
useCaseVisualAlt: "GIF、MP4 和素材歸檔包組成的下載結果使用視覺",
|
|
78
78
|
resultGuidanceItems: [
|
|
79
79
|
{
|
|
80
|
-
title: "
|
|
81
|
-
body: "
|
|
80
|
+
title: "先確認連結是否公開",
|
|
81
|
+
body: "如果頁面提示無法解析,先檢查連結是否來自 twitter.com 或 x.com、是否包含 /status/,貼文是否可公開存取,媒體是否仍然存在。",
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
title: "按用途選擇格式",
|
|
85
|
-
body: "
|
|
85
|
+
body: "需要跨工具貼上、簡報和快速分享時儲存 GIF;需要檔案更小、畫質更穩或繼續剪輯時保留 MP4。",
|
|
86
86
|
},
|
|
87
87
|
{
|
|
88
88
|
title: "複用前確認權限",
|
|
89
|
-
body: "
|
|
89
|
+
body: "下載檔案不等於獲得版權授權。用於廣告、商用發佈或公開素材庫前,請確認原作者授權和目標平台規則。",
|
|
90
90
|
},
|
|
91
91
|
],
|
|
92
92
|
useCases: "適用場景",
|
|
93
93
|
useCaseItems: [
|
|
94
|
-
"
|
|
95
|
-
"把 Twitter/X
|
|
96
|
-
"同時保留 MP4
|
|
97
|
-
"
|
|
94
|
+
"儲存公開 Tweet 或 X 貼文裡的動圖,用於素材庫、簡報或社群媒體回顧。",
|
|
95
|
+
"把 Twitter/X 平台內嵌的 GIF 動畫轉成通用 GIF 檔案,方便在其他工具裡繼續使用。",
|
|
96
|
+
"同時保留 MP4 原始檔,用於更小檔案、更清晰的影片素材。",
|
|
97
|
+
"整理公開使用者回饋、競品發佈或行銷案例時,把動圖素材儲存到本機資料庫。"
|
|
98
98
|
],
|
|
99
99
|
faq: "常見問題",
|
|
100
100
|
faqItems: [
|
|
101
101
|
{
|
|
102
102
|
question: "為什麼 Twitter GIF 需要轉換?",
|
|
103
103
|
answer:
|
|
104
|
-
"Twitter/X 通常把 GIF 作為 MP4
|
|
104
|
+
"Twitter/X 通常把 GIF 作為 MP4 動畫影片分發。下載器需要先取得影片來源,再轉換成真正的 .gif 檔案。",
|
|
105
105
|
},
|
|
106
106
|
{
|
|
107
|
-
question: "
|
|
107
|
+
question: "支援 x.com 連結嗎?",
|
|
108
108
|
answer:
|
|
109
|
-
"
|
|
109
|
+
"支援公開 x.com 和 twitter.com 貼文連結。連結需要包含 /status/ 和貼文 ID,且貼文媒體必須可公開存取。",
|
|
110
110
|
},
|
|
111
111
|
{
|
|
112
112
|
question: "可以只下載 MP4,不轉成 GIF 嗎?",
|
|
113
113
|
answer:
|
|
114
|
-
"可以。成功解析後,頁面會同時提供 GIF 下載和 MP4
|
|
114
|
+
"可以。成功解析後,頁面會同時提供 GIF 下載和 MP4 原始檔下載。MP4 通常檔案更小,也更適合剪輯或長期歸檔。",
|
|
115
115
|
},
|
|
116
116
|
{
|
|
117
|
-
question: "
|
|
117
|
+
question: "支援私密或登入後可見的貼文嗎?",
|
|
118
118
|
answer:
|
|
119
|
-
"
|
|
119
|
+
"不支援。請使用可公開存取的 X/Twitter 貼文連結。私密、刪除、受限或登入後可見內容通常無法解析,也不應該繞過原作者或平台的存取限制。",
|
|
120
120
|
},
|
|
121
121
|
{
|
|
122
|
-
question: "
|
|
122
|
+
question: "為什麼有些貼文無法下載?",
|
|
123
123
|
answer:
|
|
124
|
-
"
|
|
124
|
+
"私密、刪除、受限、沒有媒體、解析服務受到流量限制,或 X 平台改變公開資料結構時都可能失敗。可以先確認貼文是公開動圖,再換一則連結重試。",
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
127
|
question: "下載後的 GIF 可以直接商用嗎?",
|
|
128
128
|
answer:
|
|
129
|
-
"
|
|
129
|
+
"工具只負責把你提供的公開媒體轉換為檔案,不授予內容版權。公開發佈、廣告投放或商業使用前,請確認你有權使用原內容,並遵守 X/Twitter 和目標平台規則。",
|
|
130
130
|
},
|
|
131
131
|
],
|
|
132
132
|
ctaPrimary: "製作 OG 圖片",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { coldStartChannelSelectorInputSchema } from "./cold-start-channel-selector-schema";
|
|
5
|
+
|
|
6
|
+
const baseInput = {
|
|
7
|
+
productForm: "unknown" as const,
|
|
8
|
+
targetMarket: "unknown" as const,
|
|
9
|
+
stage: "private_beta" as const,
|
|
10
|
+
weeklyHours: "3_7" as const,
|
|
11
|
+
budget: "under_500" as const,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
test("cold start selector accepts a substantive Chinese product description", () => {
|
|
15
|
+
const parsed = coldStartChannelSelectorInputSchema.safeParse({
|
|
16
|
+
...baseInput,
|
|
17
|
+
productDescription:
|
|
18
|
+
"面向远程产品团队的访谈研究助手,可整理真实痛点并生成可追溯的优先级建议。",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
assert.equal(parsed.success, true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("cold start selector rejects punctuation and digits without meaningful content", () => {
|
|
25
|
+
const parsed = coldStartChannelSelectorInputSchema.safeParse({
|
|
26
|
+
...baseInput,
|
|
27
|
+
productDescription: "1234 ---- ____ ....",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
assert.equal(parsed.success, false);
|
|
31
|
+
if (!parsed.success) {
|
|
32
|
+
assert.equal(
|
|
33
|
+
parsed.error.issues.some(
|
|
34
|
+
(issue) => issue.message === "product_description_too_weak",
|
|
35
|
+
),
|
|
36
|
+
true,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
@@ -21,6 +21,10 @@ const urlField = z
|
|
|
21
21
|
const optionalShortText = (max = 240) =>
|
|
22
22
|
z.preprocess(emptyToUndefined, z.string().trim().min(2).max(max).optional());
|
|
23
23
|
|
|
24
|
+
function meaningfulTextLength(value: string): number {
|
|
25
|
+
return value.match(/[\p{L}\p{N}]/gu)?.length ?? 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
24
28
|
export const coldStartChannelSelectorInputSchema = z
|
|
25
29
|
.object({
|
|
26
30
|
productUrl: urlField,
|
|
@@ -62,10 +66,10 @@ export const coldStartChannelSelectorInputSchema = z
|
|
|
62
66
|
});
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
const
|
|
66
|
-
? value.productDescription
|
|
67
|
-
:
|
|
68
|
-
if (!value.productUrl &&
|
|
69
|
+
const descriptionLength = value.productDescription
|
|
70
|
+
? meaningfulTextLength(value.productDescription)
|
|
71
|
+
: 0;
|
|
72
|
+
if (!value.productUrl && descriptionLength < 12) {
|
|
69
73
|
ctx.addIssue({
|
|
70
74
|
code: z.ZodIssueCode.custom,
|
|
71
75
|
path: ["productDescription"],
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { competitorAnalysisResultSchema } from "./competitor-analysis-schema";
|
|
5
|
+
|
|
6
|
+
test("competitorAnalysisResultSchema accepts a sourced result with identity and a finding", () => {
|
|
7
|
+
const parsed = competitorAnalysisResultSchema.safeParse({
|
|
8
|
+
overview: {
|
|
9
|
+
name: "Example Analytics",
|
|
10
|
+
description: "A public analytics platform for product and growth teams.",
|
|
11
|
+
url: "https://example.com",
|
|
12
|
+
},
|
|
13
|
+
highlights: ["The public pricing page shows a self-serve monthly plan."],
|
|
14
|
+
sources: [
|
|
15
|
+
{ label: "Example Analytics pricing", url: "https://example.com/pricing" },
|
|
16
|
+
],
|
|
17
|
+
confidence: "medium",
|
|
18
|
+
summary: "Example Analytics sells a self-serve analytics product.",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
assert.equal(parsed.success, true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("competitorAnalysisResultSchema rejects a summary-only fake success", () => {
|
|
25
|
+
const parsed = competitorAnalysisResultSchema.safeParse({
|
|
26
|
+
summary: "Analysis completed.",
|
|
27
|
+
sources: [],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
assert.equal(parsed.success, false);
|
|
31
|
+
if (parsed.success) return;
|
|
32
|
+
assert.ok(parsed.error.issues.some((issue) => issue.path[0] === "overview"));
|
|
33
|
+
assert.ok(parsed.error.issues.some((issue) => issue.path[0] === "highlights"));
|
|
34
|
+
assert.ok(parsed.error.issues.some((issue) => issue.path[0] === "sources"));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("competitorAnalysisResultSchema requires a linkable evidence source", () => {
|
|
38
|
+
const parsed = competitorAnalysisResultSchema.safeParse({
|
|
39
|
+
overview: {
|
|
40
|
+
name: "Example Analytics",
|
|
41
|
+
category: "Product analytics",
|
|
42
|
+
},
|
|
43
|
+
highlights: ["The product offers funnel analysis."],
|
|
44
|
+
sources: [{ label: "Unlinked source" }],
|
|
45
|
+
confidence: "low",
|
|
46
|
+
summary: "A bounded public-source competitor profile.",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
assert.equal(parsed.success, false);
|
|
50
|
+
if (parsed.success) return;
|
|
51
|
+
assert.ok(parsed.error.issues.some((issue) => issue.path.join(".") === "sources.0.url"));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("competitorAnalysisResultSchema rejects non-web source protocols", () => {
|
|
55
|
+
const parsed = competitorAnalysisResultSchema.safeParse({
|
|
56
|
+
overview: {
|
|
57
|
+
name: "Example Analytics",
|
|
58
|
+
category: "Product analytics",
|
|
59
|
+
},
|
|
60
|
+
highlights: ["The product offers funnel analysis."],
|
|
61
|
+
sources: [{ label: "Local export", url: "file:///tmp/competitor.html" }],
|
|
62
|
+
confidence: "low",
|
|
63
|
+
summary: "A bounded public-source competitor profile.",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
assert.equal(parsed.success, false);
|
|
67
|
+
if (parsed.success) return;
|
|
68
|
+
assert.ok(parsed.error.issues.some((issue) => issue.path.join(".") === "sources.0.url"));
|
|
69
|
+
});
|