@tomako/tools-runtime 0.1.5 → 0.1.7

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.
Files changed (47) hide show
  1. package/package.json +2 -1
  2. package/src/components/tools/engine/package-widget-runtime.tsx +5 -1
  3. package/src/components/tools/engine/task-recovery.test.ts +65 -0
  4. package/src/components/tools/engine/task-recovery.ts +109 -0
  5. package/src/components/tools/engine/use-legacy-llm-task.ts +16 -3
  6. package/src/components/tools/engine/use-tool-task.ts +77 -53
  7. package/src/features/tools/app-store-screenshot-generator/widget/app-store-screenshot-generator.tsx +88 -16
  8. package/src/features/tools/app-store-screenshot-generator/widget/asset-slots.test.ts +52 -0
  9. package/src/features/tools/app-store-screenshot-generator/widget/asset-slots.ts +3 -2
  10. package/src/features/tools/app-store-screenshot-generator/widget/generation.test.ts +126 -0
  11. package/src/features/tools/app-store-screenshot-generator/widget/generation.ts +52 -14
  12. package/src/features/tools/app-store-screenshot-generator/widget/input-media.test.ts +81 -0
  13. package/src/features/tools/app-store-screenshot-generator/widget/input-media.ts +20 -8
  14. package/src/features/tools/cold-start-channel-selector/widget/cold-start-channel-selector.tsx +45 -16
  15. package/src/features/tools/competitor-analysis/competitor-analysis.container.tsx +26 -20
  16. package/src/features/tools/competitor-analysis/widget/competitor-analysis.tsx +47 -16
  17. package/src/features/tools/disclaimer-generator/disclaimer-generator.package.ts +3 -3
  18. package/src/features/tools/email-template-generator/widget/email-template-generator.tsx +37 -4
  19. package/src/features/tools/package/to-tool-module.ts +15 -0
  20. package/src/features/tools/product-name-generator/widget/product-name-generator.tsx +60 -27
  21. package/src/features/tools/product-poster-generator/widget/constants.ts +3 -1
  22. package/src/features/tools/product-poster-generator/widget/helpers.test.ts +51 -0
  23. package/src/features/tools/product-poster-generator/widget/helpers.ts +27 -0
  24. package/src/features/tools/product-poster-generator/widget/product-poster-generator.tsx +112 -12
  25. package/src/features/tools/slogan-generator/slogan-generator.package.ts +28 -28
  26. package/src/i18n/messages/en/tools/app-icon-resizer.ts +1 -1
  27. package/src/i18n/messages/en/tools/app-store-screenshot-generator.ts +2 -2
  28. package/src/i18n/messages/en/tools/cold-start-channel-selector.ts +4 -4
  29. package/src/i18n/messages/en/tools/competitor-analysis.ts +9 -8
  30. package/src/i18n/messages/en/tools/email-template-generator.ts +10 -10
  31. package/src/i18n/messages/en/tools/product-poster-generator.ts +1 -0
  32. package/src/i18n/messages/zh/tools/app-store-screenshot-generator.ts +3 -3
  33. package/src/i18n/messages/zh/tools/cold-start-channel-selector.ts +2 -2
  34. package/src/i18n/messages/zh/tools/competitor-analysis.ts +10 -9
  35. package/src/i18n/messages/zh/tools/email-template-generator.ts +11 -11
  36. package/src/i18n/messages/zh/tools/product-poster-generator.ts +1 -0
  37. package/src/i18n/messages/zh-tw/tools/app-store-screenshot-generator.ts +44 -44
  38. package/src/i18n/messages/zh-tw/tools/cold-start-channel-selector.ts +105 -105
  39. package/src/i18n/messages/zh-tw/tools/competitor-analysis.ts +41 -40
  40. package/src/i18n/messages/zh-tw/tools/email-template-generator.ts +111 -111
  41. package/src/i18n/messages/zh-tw/tools/product-name-generator.ts +92 -92
  42. package/src/i18n/messages/zh-tw/tools/product-poster-generator.ts +74 -73
  43. package/src/i18n/messages/zh-tw/tools/twitter-gif-downloader.ts +47 -47
  44. package/src/lib/tools/cold-start-channel-selector-schema.test.ts +39 -0
  45. package/src/lib/tools/cold-start-channel-selector-schema.ts +8 -4
  46. package/src/lib/tools/competitor-analysis-schema.test.ts +69 -0
  47. package/src/lib/tools/competitor-analysis-schema.ts +53 -5
@@ -1,18 +1,18 @@
1
1
  const productPosterGenerator = {
2
2
  meta: {
3
- title: "AI 產品海報生成器:用截圖、賣點和 CTA 製作宣傳圖",
4
- displayTitle: "AI產品海報生成器",
3
+ title: "AI 產品海報產生器:用截圖、賣點和 CTA 製作宣傳圖",
4
+ displayTitle: "AI產品海報產生器",
5
5
  description:
6
- "輸入產品名稱、賣點、目標用戶、CTA,可補充產品 URL、Logo 和截圖,生成適合官網、社媒、廣告和 OG 分享的產品宣傳海報圖。",
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、目標用戶、渠道:Product Hunt / X",
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
- "填寫產品名稱、賣點、目標用戶、CTA 和用途;需要時補充 URL、Logo、截圖、尺寸和品牌色。生成後可直接預覽並下載 PNG。",
39
- workspaceBadge: "生成 PNG 海報",
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: "把產品名稱、核心賣點、目標用戶和 CTA 組合成一張可預覽、可下載的產品宣傳海報圖。",
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
- "產品宣傳圖不只是好看的背景。它需要說清產品是什麼、給誰用、主推什麼價值,以及希望用戶點擊什麼 CTA。",
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
- "官網首屏、社媒發佈、廣告測試、Product Hunt 和 OG 分享圖的信息密度不同,不能只靠同一張圖硬裁。",
77
- "你可以選擇目標用途、尺寸和視覺風格,讓海報先按渠道安排標題、截圖、賣點和 CTA 的優先級。",
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: "像素風產品海報渠道適配配圖,展示 Web、Social 和 Ad 等多種比例。",
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: "如何生成一張\n產品宣傳海報圖",
100
- imageAlt: "像素風產品海報生成流程配圖,展示從產品 brief 到 PNG 下載。",
101
- ctaLabel: "生成產品海報",
99
+ title: "如何產生一張\n產品宣傳海報圖",
100
+ imageAlt: "像素風產品海報產生流程配圖,展示從產品 brief 到 PNG 下載。",
101
+ ctaLabel: "產生產品海報",
102
102
  steps: [
103
103
  {
104
104
  title: "填寫產品 brief",
105
- body: "輸入產品名稱、產品上下文、目標用戶和主 CTA。賣點越具體,海報越容易聚焦。",
105
+ body: "輸入產品名稱、產品上下文、目標使用者和主 CTA。賣點越具體,海報越容易聚焦。",
106
106
  },
107
107
  {
108
108
  title: "選擇海報用途",
109
- body: "從官網、社媒、廣告、Product Hunt 或 OG 分享中選擇目標用途,確定信息層級和基礎比例。",
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: "生成完成後查看海報圖、素材狀態和發佈前檢查項,再下載 PNG 進入發佈或設計流程。",
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
- "至少需要產品名稱、產品說明、目標用戶和 CTA。更好的輸入包括產品 URL、Logo、1 到 4 張 UI 截圖、品牌色和目標發佈渠道。",
130
+ "至少需要產品名稱、產品說明、目標使用者和 CTA。更好的輸入包括產品 URL、Logo、1 到 4 張 UI 截圖、品牌色和目標發佈通路。",
131
131
  },
132
132
  {
133
- question: "這個產品截圖海報生成器適合哪些產品?",
133
+ question: "這個產品截圖海報產生器適合哪些產品?",
134
134
  answer:
135
- "更適合 SaaS、App、Web 工具、插件、AI 產品和開發者工具。你可以用它準備官網首屏圖、社媒發佈圖、廣告圖、Product Hunt 圖和 OG 分享圖。",
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,還能生成 App 宣傳海報嗎?",
143
+ question: "沒有品牌色或 Logo,還能產生 App 宣傳海報嗎?",
144
144
  answer:
145
- "可以。你先提供產品說明、目標用戶和截圖,工具會使用默認視覺方向。品牌穩定後,再用 Logo 和品牌色生成更一致的版本。",
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: "先生成 Logo",
153
+ ctaPrimary: "產生應用程式商店上架圖",
154
+ ctaSecondary: "先產生 Logo",
155
155
  ctaHint:
156
- "如果你要做 App 商店素材,可以先生成上架圖。如果品牌標識還不穩定,可以先生成 Logo 方向。",
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
- "產品名稱、賣點、目標用戶、CTA 和用途會決定海報的主標題、畫面層級和下載結果。",
193
- loadingTitle: "正在生成產品海報",
192
+ "產品名稱、賣點、目標使用者、CTA 和用途會決定海報的主標題、畫面層級和下載結果。",
193
+ loadingTitle: "正在產生產品海報",
194
194
  loadingTipLabel: "提示",
195
- loadingTip: "通常需要幾十秒。保持本頁打開,生成完成後會在右側顯示可下載 PNG。",
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: "請填寫目標用戶。比如 SaaS 創始人、增長團隊、獨立開發者或企業採購。",
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: "最終下載圖:已保留模型生成圖,併疊加上傳 Logo",
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 帖子連結,預覽媒體後下載真正的 GIF 或 MP4;保存前先了解連結、存取與重用邊界。",
5
- intro: "把公開 X 或 Twitter 帖子中的動圖保存為可重用檔案。貼上帖子連結、預覽可轉換媒體後,在可用時下載 GIF 或 MP4。",
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 文件、MP4 源文件",
16
- boundarySignal: "私密、刪除、受限或登錄後內容通常無法解析",
14
+ inputSignal: "公開 twitter.com 或 x.com 貼文連結",
15
+ outputSignal: "GIF 預覽、.gif 檔案、MP4 源檔案",
16
+ boundarySignal: "私密、刪除、受限或登入後內容通常無法解析",
17
17
  },
18
18
  widget: {
19
- workspaceTitle: "粘貼 Tweet 鏈接",
20
- workspaceIntro: "粘貼公開 X/Twitter 帖子鏈接,保存可預覽的 GIF,並保留 MP4 源文件。",
21
- urlLabel: "Tweet 或 X 帖子 URL",
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: "粘貼一條公開 Tweet 或 X 帖子鏈接後,這裡會顯示解析進度、GIF 預覽、GIF 下載和 MP4 下載操作。",
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: "這條帖子沒有可轉換的視頻或 GIF 媒體,或內容不是公開可訪問。",
35
- conversionError: "轉換失敗。請換一條公開 GIF 帖子,或下載 MP4 源文件後本地轉換。",
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 帖子鏈接轉換成 GIF 和 MP4 下載文件的 Hero 背景圖",
42
+ heroVisualAlt: "公開 X 貼文連結轉換成 GIF 和 MP4 下載檔案的 Hero 背景圖",
43
43
  workspaceHeading: "Twitter / X GIF 下載器",
44
- workspaceHint: "貼上帶 /status/ 的公開帖子連結,預覽可轉換媒體後保存 GIF 或 MP4。私密、受限、已刪除或沒有媒體的帖子無法使用。",
44
+ workspaceHint: "貼上帶 /status/ 的公開貼文連結,預覽可轉換媒體後儲存 GIF 或 MP4。私密、受限、已刪除或沒有媒體的貼文無法使用。",
45
45
  workspaceBadge: "公開連結 → GIF 或 MP4",
46
46
  resultHeading: "你會得到什麼",
47
47
  resultBody: [
48
- "貼上公開 X/Twitter 帖子連結後,頁面會查找帖子裡的動圖媒體,顯示處理狀態,並在成功後提供 GIF 預覽、.gif 檔案下載和可用時的 MP4 下載。",
49
- "很多用戶搜索 Twitter GIF downloader 或 download Twitter GIF,其實是想把平臺裡的動圖保存成真正可複用的文件。Tomako 把鏈接識別、媒體預覽、格式選擇和下載動作放在同一個工作臺裡,你可以先確認動圖內容,再決定保存 GIF 還是保留更小的 MP4。",
48
+ "貼上公開 X/Twitter 貼文連結後,頁面會尋找貼文裡的動圖媒體,顯示處理狀態,並在成功後提供 GIF 預覽、.gif 檔案下載和可用時的 MP4 下載。",
49
+ "許多使用者搜尋 Twitter GIF downloader 或 download Twitter GIF,其實是想把平台裡的動圖儲存成真正可重用的檔案。Tomako 把連結識別、媒體預覽、格式選擇和下載動作放在同一個工作區裡,你可以先確認動圖內容,再決定儲存 GIF 還是保留更小的 MP4。",
50
50
  ],
51
- resultVisualAlt: "公開帖子鏈接轉換成 GIF 和 MP4 下載文件的素材包視覺",
52
- supportVisualAlt: "公開鏈接可下載、受限鏈接無法解析的邊界說明視覺",
51
+ resultVisualAlt: "公開貼文連結轉換成 GIF 和 MP4 下載檔案的素材包視覺",
52
+ supportVisualAlt: "公開連結可下載、受限連結無法解析的邊界說明視覺",
53
53
  standardsHeading: "好的 Twitter GIF 下載器應該具備什麼",
54
54
  standardsBody: [
55
- "選擇同類工具時,先看它能不能同時識別 twitter.com 和 x.com 公開帖子鏈接、能不能先預覽再下載、是否提供 GIF 與 MP4 兩種保存方式,以及失敗原因是否說得明白。",
56
- "Tomako 把公開連結識別、預覽、格式選擇和轉換放在同一個工作臺裡。你不需要自己尋找媒體地址,也不必處理瀏覽器插件、腳本或轉換參數。",
57
- "請只使用你有權訪問和保存的公開帖子鏈接。下載後的 GIF 或 MP4 是否能複用,取決於原內容版權、平臺規則和你的使用場景。",
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: "粘貼公開 twitter.com 或 x.com 帖子鏈接後,頁面會自動尋找可轉換媒體。",
64
+ pain: "需要自己從頁面原始碼或開發者工具裡找影片位址。",
65
+ tomako: "貼上公開 twitter.com 或 x.com 貼文連結後,頁面會自動尋找可轉換媒體。",
66
66
  },
67
67
  {
68
- pain: "下載前不知道保存的是 GIF、視頻,還是無效文件。",
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: "如果頁面提示無法解析,先檢查鏈接是否來自 twitter.com 或 x.com、是否包含 /status/,帖子是否公開可訪問,媒體是否仍然存在。",
80
+ title: "先確認連結是否公開",
81
+ body: "如果頁面提示無法解析,先檢查連結是否來自 twitter.com 或 x.com、是否包含 /status/,貼文是否可公開存取,媒體是否仍然存在。",
82
82
  },
83
83
  {
84
84
  title: "按用途選擇格式",
85
- body: "需要跨工具粘貼、演示和快速分享時保存 GIF;需要體積更小、畫質更穩或繼續剪輯時保留 MP4。",
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
- "保存公開 Tweet 或 X 帖子裡的動圖,用於素材庫、演示文稿或社媒覆盤。",
95
- "把 Twitter/X 平臺內嵌的 GIF 動畫轉成通用 GIF 文件,方便在其他工具裡繼續使用。",
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 動畫視頻分發。下載器需要先拿到視頻源,再轉換成真正的 .gif 文件。",
104
+ "Twitter/X 通常把 GIF 作為 MP4 動畫影片分發。下載器需要先取得影片來源,再轉換成真正的 .gif 檔案。",
105
105
  },
106
106
  {
107
- question: "支持 x.com 鏈接嗎?",
107
+ question: "支援 x.com 連結嗎?",
108
108
  answer:
109
- "支持公開 x.com 和 twitter.com 帖子鏈接。鏈接需要包含 /status/ 和帖子 ID,且帖子媒體必須能公開訪問。",
109
+ "支援公開 x.com 和 twitter.com 貼文連結。連結需要包含 /status/ 和貼文 ID,且貼文媒體必須可公開存取。",
110
110
  },
111
111
  {
112
112
  question: "可以只下載 MP4,不轉成 GIF 嗎?",
113
113
  answer:
114
- "可以。成功解析後,頁面會同時提供 GIF 下載和 MP4 源文件下載。MP4 通常體積更小,也更適合剪輯或長期歸檔。",
114
+ "可以。成功解析後,頁面會同時提供 GIF 下載和 MP4 原始檔下載。MP4 通常檔案更小,也更適合剪輯或長期歸檔。",
115
115
  },
116
116
  {
117
- question: "支持私密或登錄後可見的帖子嗎?",
117
+ question: "支援私密或登入後可見的貼文嗎?",
118
118
  answer:
119
- "不支持。請使用公開可訪問的 X/Twitter 帖子鏈接。私密、刪除、受限或登錄後可見內容通常無法解析,也不應該繞過原作者或平臺的訪問限制。",
119
+ "不支援。請使用可公開存取的 X/Twitter 貼文連結。私密、刪除、受限或登入後可見內容通常無法解析,也不應該繞過原作者或平台的存取限制。",
120
120
  },
121
121
  {
122
- question: "為什麼有些帖子無法下載?",
122
+ question: "為什麼有些貼文無法下載?",
123
123
  answer:
124
- "私密、刪除、受限、沒有媒體、解析服務被限流,或 X 平臺改變公開數據結構時都可能失敗。可以先確認帖子是公開動圖,再換一條鏈接重試。",
124
+ "私密、刪除、受限、沒有媒體、解析服務受到流量限制,或 X 平台改變公開資料結構時都可能失敗。可以先確認貼文是公開動圖,再換一則連結重試。",
125
125
  },
126
126
  {
127
127
  question: "下載後的 GIF 可以直接商用嗎?",
128
128
  answer:
129
- "工具只負責把你提供的公開媒體轉換為文件,不授予內容版權。公開發布、廣告投放或商業使用前,請確認你有權使用原內容,並遵守 X/Twitter 和目標平臺規則。",
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 weakDescription = value.productDescription
66
- ? value.productDescription.replace(/[\s\d\W_]/g, "")
67
- : "";
68
- if (!value.productUrl && weakDescription.length < 12) {
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
+ });