create-sailor 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -23
- package/dist/index.js +2760 -74
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,70 +1,1817 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{execSync as
|
|
3
|
-
`));return}
|
|
2
|
+
import{execSync as Je}from"child_process";import un from"fs";import pt from"path";import*as I from"@clack/prompts";import{Command as ts}from"commander";import T from"picocolors";import ns from"update-notifier";import xn from"cfonts";import Rn from"picocolors";import dt from"fs";import Ze from"path";import{fileURLToPath as Nn}from"url";function Tn(){try{let t=Ze.dirname(Nn(import.meta.url));for(let n=0;n<4;n++){let r=Ze.join(t,"package.json");if(dt.existsSync(r)){let o=dt.readFileSync(r,"utf-8"),s=JSON.parse(o);if(s.name==="create-sailor"&&typeof s.version=="string")return s.version}t=Ze.dirname(t)}return"0.0.0-dev"}catch{return"0.0.0-dev"}}var J=Tn();function ut(){if(process.env.NO_COLOR||!process.stdout.isTTY){process.stdout.write([`Sailor v${J}`,"AI-Native SaaS Unicorn Template","by Nebutra \xB7 https://nebutra.com","",""].join(`
|
|
3
|
+
`));return}xn.say("Sailor",{font:"block",align:"left",colors:["#0033FE","#0BF1C3"],gradient:["#0033FE","#0BF1C3"],transitionGradient:!0,space:!1}),process.stdout.write(Rn.dim(` AI-Native SaaS Template \xB7 v${J} \xB7 nebutra.com
|
|
4
4
|
|
|
5
|
-
`))}import
|
|
5
|
+
`))}import M from"picocolors";function kn(){return!process.env.NO_COLOR&&!!process.stdout.isTTY}function mt(e){let t=kn(),n=t?"\u2693":"-",r=t?"\u25B8":"->",o=t?"\u2605":"*",s=t?"\u{1F4E7}":"*",c=["",` ${t?M.bold(`${n} Done in ${e.elapsedSec}s \xB7 ${e.targetDir}/`):`${n} Done in ${e.elapsedSec}s \xB7 ${e.targetDir}/`}`,"",` ${t?M.bold("Next:"):"Next:"}`,` ${r} cd ${e.targetDir}`];e.skippedInstall&&c.push(` ${r} pnpm install (if --no-install was set)`),c.push(` ${r} pnpm dev ${t?M.dim("\u2192 http://localhost:3000"):"-> http://localhost:3000"}`,"",` ${t?M.bold("Customize:"):"Customize:"}`,` ${r} pnpm sailor brand init ${t?M.dim("\u2192 your colors, logo, domain"):"-> your colors, logo, domain"}`,` ${r} pnpm sailor preset apply ${t?M.dim("\u2192 toggle features"):"-> toggle features"}`,` ${r} pnpm sailor add <feat> ${t?M.dim("\u2192 add queue/search/cache/..."):"-> add queue/search/cache/..."}`,"",` ${t?M.bold("Add more features later:"):"Add more features later:"}`,` ${r} sailor add payment --provider=wechat`,` ${r} sailor add email --provider=postmark`,` ${r} sailor add storage --provider=supabase`,` ${t?M.dim("(more providers: `sailor add --list`)"):"(more providers: `sailor add --list`)"}`,"",` ${t?M.bold("Ship faster:"):"Ship faster:"}`,` ${o} Star us https://github.com/nebutra/nebutra-sailor`,` ${s} Free license https://nebutra.com/get-license`,""),process.stdout.write(c.join(`
|
|
6
6
|
`)+`
|
|
7
|
-
`)}var
|
|
7
|
+
`)}var On=`Usage: create-sailor [name] [options]
|
|
8
8
|
|
|
9
9
|
Arguments:
|
|
10
|
-
name
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-p, --pm <id>
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
--
|
|
20
|
-
--
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
--
|
|
27
|
-
--
|
|
28
|
-
--
|
|
29
|
-
|
|
30
|
-
|
|
10
|
+
name project directory (default: ./my-saas-app)
|
|
11
|
+
|
|
12
|
+
Core options:
|
|
13
|
+
-p, --pm <id> npm | pnpm | yarn | bun (auto-detected)
|
|
14
|
+
--region <id> global | cn | hybrid (default: global)
|
|
15
|
+
--auth <id> clerk | betterauth | none
|
|
16
|
+
--ai <ids> comma-separated provider ids (e.g. openai,anthropic)
|
|
17
|
+
|
|
18
|
+
Stack flags (region-based smart defaults):
|
|
19
|
+
--orm <id> prisma | drizzle | none (default: prisma)
|
|
20
|
+
--db <id> postgres | mysql | sqlite | none (default: postgres)
|
|
21
|
+
--payment <id> stripe | lemon | wechat | alipay | none (default: region-based)
|
|
22
|
+
--deploy <target> vercel | railway | cloudflare | selfhost (default: vercel)
|
|
23
|
+
--docs <id> fumadocs | mintlify | docusaurus | nextra | vitepress | none
|
|
24
|
+
|
|
25
|
+
Feature flags (region-based smart defaults):
|
|
26
|
+
--email <id> resend | postmark | ses | aliyun-dm | tencent-ses | netease | none
|
|
27
|
+
--storage <id> r2 | s3 | supabase | aliyun-oss | tencent-cos | qiniu | none
|
|
28
|
+
--monitoring <id> sentry | datadog | aliyun-arms | tingyun | none
|
|
29
|
+
--analytics <id> posthog | plausible | umami | baidu | sensors | none
|
|
30
|
+
--sms <id> twilio | aliyun-sms | tencent-sms | yunpian | none
|
|
31
|
+
|
|
32
|
+
Toggles:
|
|
33
|
+
--i18n enable i18n (default: true)
|
|
34
|
+
--no-i18n disable
|
|
35
|
+
--no-install skip package install
|
|
36
|
+
--no-git skip git init
|
|
37
|
+
-y, --yes accept all defaults (non-interactive)
|
|
38
|
+
--dry-run preview actions without writing files
|
|
39
|
+
--json machine-readable output
|
|
40
|
+
--no-color disable color output
|
|
41
|
+
-h, --help show this help
|
|
42
|
+
-v, --version show version
|
|
31
43
|
|
|
32
44
|
Examples:
|
|
33
45
|
$ npm create sailor@latest
|
|
34
46
|
$ npm create sailor@latest my-app -y
|
|
35
|
-
$ npm create sailor@latest my-app --
|
|
36
|
-
$ npm create sailor@latest --
|
|
37
|
-
|
|
38
|
-
`)}var ie=[{id:"anthropic",name:"Anthropic",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://docs.anthropic.com",envVarPrefix:"ANTHROPIC",requiredEnvVars:["ANTHROPIC_API_KEY"]},{id:"openai",name:"OpenAI",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://platform.openai.com/docs",envVarPrefix:"OPENAI",requiredEnvVars:["OPENAI_API_KEY"]},{id:"deepseek",name:"DeepSeek",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://platform.deepseek.com/api-docs",envVarPrefix:"DEEPSEEK",requiredEnvVars:["DEEPSEEK_API_KEY"]},{id:"xai",name:"xAI (Grok)",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://docs.x.ai/api",envVarPrefix:"XAI",requiredEnvVars:["XAI_API_KEY"]},{id:"moonshot",name:"Moonshot (Kimi)",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://platform.moonshot.cn/docs",envVarPrefix:"MOONSHOT",requiredEnvVars:["MOONSHOT_API_KEY","MOONSHOT_BASE_URL"]},{id:"google",name:"Google Gemini",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"ai-sdk",docs:"https://ai.google.dev/gemini-api/docs",envVarPrefix:"GOOGLE_GENERATIVE_AI",requiredEnvVars:["GOOGLE_GENERATIVE_AI_API_KEY"]},{id:"mistral",name:"Mistral AI",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"ai-sdk",docs:"https://docs.mistral.ai",envVarPrefix:"MISTRAL",requiredEnvVars:["MISTRAL_API_KEY"]},{id:"cohere",name:"Cohere",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"ai-sdk",docs:"https://docs.cohere.com",envVarPrefix:"COHERE",requiredEnvVars:["COHERE_API_KEY"]},{id:"perplexity",name:"Perplexity",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"cn-compatible",docs:"https://docs.perplexity.ai",baseURL:"https://api.perplexity.ai",envVarPrefix:"PERPLEXITY",requiredEnvVars:["PERPLEXITY_API_KEY"]},{id:"ai21",name:"AI21 Labs",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"cn-compatible",docs:"https://docs.ai21.com",baseURL:"https://api.ai21.com/studio/v1",envVarPrefix:"AI21",requiredEnvVars:["AI21_API_KEY"]},{id:"upstage",name:"Upstage (Solar)",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"cn-compatible",docs:"https://developers.upstage.ai/docs",baseURL:"https://api.upstage.ai/v1/solar",envVarPrefix:"UPSTAGE",requiredEnvVars:["UPSTAGE_API_KEY"]},{id:"siliconflow",name:"\u7845\u57FA\u6D41\u52A8 SiliconFlow",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://docs.siliconflow.cn",baseURL:"https://api.siliconflow.cn/v1",envVarPrefix:"SILICONFLOW",requiredEnvVars:["SILICONFLOW_API_KEY"]},{id:"volcengine-ark",name:"\u706B\u5C71\u5F15\u64CE ARK (\u8C46\u5305)",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://www.volcengine.com/docs/82379",baseURL:"https://ark.cn-beijing.volces.com/api/v3",envVarPrefix:"ARK",requiredEnvVars:["ARK_API_KEY"]},{id:"bailian",name:"\u767E\u70BC Bailian (\u901A\u4E49)",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://help.aliyun.com/zh/model-studio",baseURL:"https://dashscope.aliyuncs.com/compatible-mode/v1",envVarPrefix:"DASHSCOPE",requiredEnvVars:["DASHSCOPE_API_KEY"]},{id:"zhipu",name:"\u667A\u8C31 AI Zhipu",category:"\u56FD\u5185\u5E73\u53F0",status:"ai-sdk",docs:"https://docs.bigmodel.cn",baseURL:"https://open.bigmodel.cn/api/paas/v4",envVarPrefix:"ZHIPU",requiredEnvVars:["ZHIPU_API_KEY"]},{id:"baichuan",name:"\u767E\u5DDD\u667A\u80FD Baichuan",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.baichuan-ai.com/docs",baseURL:"https://api.baichuan-ai.com/v1",envVarPrefix:"BAICHUAN",requiredEnvVars:["BAICHUAN_API_KEY"]},{id:"minimax",name:"MiniMax (ABAB)",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.minimaxi.com/document/guides",baseURL:"https://api.minimax.chat/v1",envVarPrefix:"MINIMAX",requiredEnvVars:["MINIMAX_API_KEY","MINIMAX_GROUP_ID"]},{id:"stepfun",name:"\u9636\u8DC3\u661F\u8FB0 StepFun",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.stepfun.com/docs",baseURL:"https://api.stepfun.com/v1",envVarPrefix:"STEPFUN",requiredEnvVars:["STEPFUN_API_KEY"]},{id:"sensetime",name:"\u5546\u6C64 SenseNova",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.sensetime.com",baseURL:"https://api.sensenova.cn/v1",envVarPrefix:"SENSENOVA",requiredEnvVars:["SENSENOVA_API_KEY"]},{id:"tencent",name:"\u817E\u8BAF\u6DF7\u5143 Hunyuan",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://cloud.tencent.com/document/product/1729",baseURL:"https://api.hunyuan.cloud.tencent.com/v1",envVarPrefix:"HUNYUAN",requiredEnvVars:["HUNYUAN_API_KEY"]},{id:"lingyi",name:"\u96F6\u4E00\u4E07\u7269 Yi",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.lingyiwanwu.com/docs",baseURL:"https://api.lingyiwanwu.com/v1",envVarPrefix:"YI",requiredEnvVars:["YI_API_KEY"]},{id:"openrouter",name:"OpenRouter",category:"\u7EDF\u4E00\u7F51\u5173",status:"opencode",docs:"https://openrouter.ai/docs",envVarPrefix:"OPENROUTER",requiredEnvVars:["OPENROUTER_API_KEY"]},{id:"vercel-gateway",name:"Vercel AI Gateway",category:"\u7EDF\u4E00\u7F51\u5173",status:"opencode",docs:"https://vercel.com/docs/ai-gateway",envVarPrefix:"AI_GATEWAY",requiredEnvVars:["AI_GATEWAY_API_KEY"]},{id:"litellm",name:"LiteLLM Proxy",category:"\u7EDF\u4E00\u7F51\u5173",status:"cn-compatible",docs:"https://docs.litellm.ai",baseURL:"http://localhost:4000/v1",envVarPrefix:"LITELLM",requiredEnvVars:["LITELLM_API_KEY"]},{id:"portkey",name:"Portkey",category:"\u7EDF\u4E00\u7F51\u5173",status:"cn-compatible",docs:"https://portkey.ai/docs",baseURL:"https://api.portkey.ai/v1",envVarPrefix:"PORTKEY",requiredEnvVars:["PORTKEY_API_KEY"]},{id:"aws-bedrock",name:"AWS Bedrock",category:"\u4E91\u5E73\u53F0",status:"ai-sdk",docs:"https://aws.amazon.com/bedrock/",envVarPrefix:"AWS_BEDROCK",requiredEnvVars:["AWS_ACCESS_KEY_ID","AWS_SECRET_ACCESS_KEY","AWS_REGION"]},{id:"azure-openai",name:"Azure OpenAI",category:"\u4E91\u5E73\u53F0",status:"ai-sdk",docs:"https://azure.microsoft.com/en-us/products/cognitive-services/openai-service/",envVarPrefix:"AZURE_OPENAI",requiredEnvVars:["AZURE_OPENAI_API_KEY","AZURE_OPENAI_ENDPOINT"]},{id:"gcp-vertex",name:"GCP Vertex AI",category:"\u4E91\u5E73\u53F0",status:"ai-sdk",docs:"https://cloud.google.com/vertex-ai",envVarPrefix:"GOOGLE_VERTEX",requiredEnvVars:["GOOGLE_APPLICATION_CREDENTIALS"]},{id:"groq",name:"Groq",category:"\u63A8\u7406\u52A0\u901F",status:"opencode",docs:"https://console.groq.com/docs",envVarPrefix:"GROQ",requiredEnvVars:["GROQ_API_KEY"]},{id:"fireworks",name:"Fireworks AI",category:"\u63A8\u7406\u52A0\u901F",status:"opencode",docs:"https://docs.fireworks.ai",envVarPrefix:"FIREWORKS",requiredEnvVars:["FIREWORKS_API_KEY"]},{id:"together",name:"Together AI",category:"\u63A8\u7406\u52A0\u901F",status:"opencode",docs:"https://docs.together.ai",envVarPrefix:"TOGETHER",requiredEnvVars:["TOGETHER_API_KEY"]},{id:"huggingface",name:"Hugging Face",category:"\u63A8\u7406\u52A0\u901F",status:"opencode",docs:"https://huggingface.co/docs/inference-providers",envVarPrefix:"HF",requiredEnvVars:["HF_API_KEY"]},{id:"replicate",name:"Replicate",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://replicate.com/docs",envVarPrefix:"REPLICATE",requiredEnvVars:["REPLICATE_API_TOKEN"]},{id:"lepton",name:"Lepton AI",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://www.lepton.ai/docs",baseURL:"https://api.lepton.ai/v1",envVarPrefix:"LEPTON",requiredEnvVars:["LEPTON_API_KEY"]},{id:"anyscale",name:"Anyscale Endpoints",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://docs.endpoints.anyscale.com/",baseURL:"https://api.endpoints.anyscale.com/v1",envVarPrefix:"ANYSCALE",requiredEnvVars:["ANYSCALE_API_KEY"]},{id:"octoai",name:"OctoAI",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://octoai.cloud/docs",baseURL:"https://text.octoai.run/v1",envVarPrefix:"OCTOAI",requiredEnvVars:["OCTOAI_TOKEN"]},{id:"deepinfra",name:"DeepInfra",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://deepinfra.com/docs",baseURL:"https://api.deepinfra.com/v1/openai",envVarPrefix:"DEEPINFRA",requiredEnvVars:["DEEPINFRA_API_KEY"]},{id:"novita",name:"Novita AI",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://novita.ai/docs",baseURL:"https://api.novita.ai/v3/openai",envVarPrefix:"NOVITA",requiredEnvVars:["NOVITA_API_KEY"]},{id:"black-forest-labs",name:"Black Forest Labs (FLUX)",category:"\u591A\u6A21\u6001",status:"ai-sdk",docs:"https://docs.bfl.ml",envVarPrefix:"BFL",requiredEnvVars:["BFL_API_KEY"]},{id:"elevenlabs",name:"ElevenLabs (TTS)",category:"\u591A\u6A21\u6001",status:"ai-sdk",docs:"https://elevenlabs.io/docs",envVarPrefix:"ELEVENLABS",requiredEnvVars:["ELEVENLABS_API_KEY"]},{id:"runway",name:"Runway (Video)",category:"\u591A\u6A21\u6001",status:"pending",docs:"https://runwayml.com",envVarPrefix:"RUNWAY",requiredEnvVars:["RUNWAY_API_KEY"]},{id:"luma",name:"Luma (Video)",category:"\u591A\u6A21\u6001",status:"pending",docs:"https://lumalabs.ai",envVarPrefix:"LUMA",requiredEnvVars:["LUMA_API_KEY"]},{id:"midjourney",name:"Midjourney",category:"\u591A\u6A21\u6001",status:"pending",docs:"https://docs.midjourney.com",envVarPrefix:"MIDJOURNEY",requiredEnvVars:[]},{id:"ollama",name:"Ollama (\u672C\u5730)",category:"\u672C\u5730\u90E8\u7F72",status:"opencode",docs:"https://github.com/ollama/ollama",baseURL:"http://localhost:11434/v1",envVarPrefix:"OLLAMA",requiredEnvVars:["OLLAMA_BASE_URL"]},{id:"lmstudio",name:"LM Studio",category:"\u672C\u5730\u90E8\u7F72",status:"cn-compatible",docs:"https://lmstudio.ai/docs",baseURL:"http://localhost:1234/v1",envVarPrefix:"LMSTUDIO",requiredEnvVars:[]},{id:"vllm",name:"vLLM",category:"\u672C\u5730\u90E8\u7F72",status:"cn-compatible",docs:"https://docs.vllm.ai",baseURL:"http://localhost:8000/v1",envVarPrefix:"VLLM",requiredEnvVars:[]},{id:"tgi",name:"HF TGI",category:"\u672C\u5730\u90E8\u7F72",status:"cn-compatible",docs:"https://huggingface.co/docs/text-generation-inference",baseURL:"http://localhost:8080/v1",envVarPrefix:"TGI",requiredEnvVars:[]}],an=ie.reduce((e,t)=>(e[t.category]||(e[t.category]=[]),e[t.category].push(t),e),{});import x from"fs";import $ from"path";function ue(e){x.existsSync(e)&&x.rmSync(e,{recursive:!0,force:!0})}function xe(e,t){let n=$.join(e,".env.example");x.existsSync(n)?x.appendFileSync(n,`
|
|
39
|
-
`+t):x.writeFileSync(n,t)}function Se(e,t){if(!x.existsSync(e))return;let n=x.readFileSync(e,"utf8"),s=n.replace(/export type AuthProviderId\s*=\s*"[^"]+"\s*\|\s*"[^"]+";/,`export type AuthProviderId = "${t}";`);s!==n&&x.writeFileSync(e,s)}function ft(e,t){let n=$.join(e,"README.md");if(!x.existsSync(n)){x.writeFileSync(n,t);return}x.appendFileSync(n,`
|
|
40
|
-
`+t)}
|
|
47
|
+
$ npm create sailor@latest my-app --region=cn --auth=clerk
|
|
48
|
+
$ npm create sailor@latest my-app --region=hybrid --ai=openai,deepseek
|
|
49
|
+
$ npm create sailor@latest --dry-run --region=cn
|
|
50
|
+
`;function gt(){process.stdout.write(On)}import qe from"picocolors";function ft(){return!process.env.NO_COLOR&&!!process.stdout.isTTY}function Un(e){let t=ft()?"\u25B8":"->",n=`[${e.index}/${e.total}]`,r=e.label.padEnd(18," ");return ft()?`${qe.dim(n)} ${qe.bold(r)} ${qe.dim(t)} ${qe.cyan(e.value)}`:`${n} ${r} ${t} ${e.value}`}function yt(e){process.stdout.write(Un(e)+`
|
|
51
|
+
`)}var Ge=[{id:"anthropic",name:"Anthropic",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://docs.anthropic.com",envVarPrefix:"ANTHROPIC",requiredEnvVars:["ANTHROPIC_API_KEY"]},{id:"openai",name:"OpenAI",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://platform.openai.com/docs",envVarPrefix:"OPENAI",requiredEnvVars:["OPENAI_API_KEY"]},{id:"deepseek",name:"DeepSeek",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://platform.deepseek.com/api-docs",envVarPrefix:"DEEPSEEK",requiredEnvVars:["DEEPSEEK_API_KEY"]},{id:"xai",name:"xAI (Grok)",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://docs.x.ai/api",envVarPrefix:"XAI",requiredEnvVars:["XAI_API_KEY"]},{id:"moonshot",name:"Moonshot (Kimi)",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"opencode",docs:"https://platform.moonshot.cn/docs",envVarPrefix:"MOONSHOT",requiredEnvVars:["MOONSHOT_API_KEY","MOONSHOT_BASE_URL"]},{id:"google",name:"Google Gemini",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"ai-sdk",docs:"https://ai.google.dev/gemini-api/docs",envVarPrefix:"GOOGLE_GENERATIVE_AI",requiredEnvVars:["GOOGLE_GENERATIVE_AI_API_KEY"]},{id:"mistral",name:"Mistral AI",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"ai-sdk",docs:"https://docs.mistral.ai",envVarPrefix:"MISTRAL",requiredEnvVars:["MISTRAL_API_KEY"]},{id:"cohere",name:"Cohere",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"ai-sdk",docs:"https://docs.cohere.com",envVarPrefix:"COHERE",requiredEnvVars:["COHERE_API_KEY"]},{id:"perplexity",name:"Perplexity",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"cn-compatible",docs:"https://docs.perplexity.ai",baseURL:"https://api.perplexity.ai",envVarPrefix:"PERPLEXITY",requiredEnvVars:["PERPLEXITY_API_KEY"]},{id:"ai21",name:"AI21 Labs",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"cn-compatible",docs:"https://docs.ai21.com",baseURL:"https://api.ai21.com/studio/v1",envVarPrefix:"AI21",requiredEnvVars:["AI21_API_KEY"]},{id:"upstage",name:"Upstage (Solar)",category:"\u76F4\u63A5\u5B9E\u9A8C\u5BA4",status:"cn-compatible",docs:"https://developers.upstage.ai/docs",baseURL:"https://api.upstage.ai/v1/solar",envVarPrefix:"UPSTAGE",requiredEnvVars:["UPSTAGE_API_KEY"]},{id:"siliconflow",name:"\u7845\u57FA\u6D41\u52A8 SiliconFlow",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://docs.siliconflow.cn",baseURL:"https://api.siliconflow.cn/v1",envVarPrefix:"SILICONFLOW",requiredEnvVars:["SILICONFLOW_API_KEY"]},{id:"volcengine-ark",name:"\u706B\u5C71\u5F15\u64CE ARK (\u8C46\u5305)",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://www.volcengine.com/docs/82379",baseURL:"https://ark.cn-beijing.volces.com/api/v3",envVarPrefix:"ARK",requiredEnvVars:["ARK_API_KEY"]},{id:"bailian",name:"\u767E\u70BC Bailian (\u901A\u4E49)",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://help.aliyun.com/zh/model-studio",baseURL:"https://dashscope.aliyuncs.com/compatible-mode/v1",envVarPrefix:"DASHSCOPE",requiredEnvVars:["DASHSCOPE_API_KEY"]},{id:"zhipu",name:"\u667A\u8C31 AI Zhipu",category:"\u56FD\u5185\u5E73\u53F0",status:"ai-sdk",docs:"https://docs.bigmodel.cn",baseURL:"https://open.bigmodel.cn/api/paas/v4",envVarPrefix:"ZHIPU",requiredEnvVars:["ZHIPU_API_KEY"]},{id:"baichuan",name:"\u767E\u5DDD\u667A\u80FD Baichuan",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.baichuan-ai.com/docs",baseURL:"https://api.baichuan-ai.com/v1",envVarPrefix:"BAICHUAN",requiredEnvVars:["BAICHUAN_API_KEY"]},{id:"minimax",name:"MiniMax (ABAB)",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.minimaxi.com/document/guides",baseURL:"https://api.minimax.chat/v1",envVarPrefix:"MINIMAX",requiredEnvVars:["MINIMAX_API_KEY","MINIMAX_GROUP_ID"]},{id:"stepfun",name:"\u9636\u8DC3\u661F\u8FB0 StepFun",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.stepfun.com/docs",baseURL:"https://api.stepfun.com/v1",envVarPrefix:"STEPFUN",requiredEnvVars:["STEPFUN_API_KEY"]},{id:"sensetime",name:"\u5546\u6C64 SenseNova",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.sensetime.com",baseURL:"https://api.sensenova.cn/v1",envVarPrefix:"SENSENOVA",requiredEnvVars:["SENSENOVA_API_KEY"]},{id:"tencent",name:"\u817E\u8BAF\u6DF7\u5143 Hunyuan",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://cloud.tencent.com/document/product/1729",baseURL:"https://api.hunyuan.cloud.tencent.com/v1",envVarPrefix:"HUNYUAN",requiredEnvVars:["HUNYUAN_API_KEY"]},{id:"lingyi",name:"\u96F6\u4E00\u4E07\u7269 Yi",category:"\u56FD\u5185\u5E73\u53F0",status:"cn-compatible",docs:"https://platform.lingyiwanwu.com/docs",baseURL:"https://api.lingyiwanwu.com/v1",envVarPrefix:"YI",requiredEnvVars:["YI_API_KEY"]},{id:"openrouter",name:"OpenRouter",category:"\u7EDF\u4E00\u7F51\u5173",status:"opencode",docs:"https://openrouter.ai/docs",envVarPrefix:"OPENROUTER",requiredEnvVars:["OPENROUTER_API_KEY"]},{id:"vercel-gateway",name:"Vercel AI Gateway",category:"\u7EDF\u4E00\u7F51\u5173",status:"opencode",docs:"https://vercel.com/docs/ai-gateway",envVarPrefix:"AI_GATEWAY",requiredEnvVars:["AI_GATEWAY_API_KEY"]},{id:"litellm",name:"LiteLLM Proxy",category:"\u7EDF\u4E00\u7F51\u5173",status:"cn-compatible",docs:"https://docs.litellm.ai",baseURL:"http://localhost:4000/v1",envVarPrefix:"LITELLM",requiredEnvVars:["LITELLM_API_KEY"]},{id:"portkey",name:"Portkey",category:"\u7EDF\u4E00\u7F51\u5173",status:"cn-compatible",docs:"https://portkey.ai/docs",baseURL:"https://api.portkey.ai/v1",envVarPrefix:"PORTKEY",requiredEnvVars:["PORTKEY_API_KEY"]},{id:"aws-bedrock",name:"AWS Bedrock",category:"\u4E91\u5E73\u53F0",status:"ai-sdk",docs:"https://aws.amazon.com/bedrock/",envVarPrefix:"AWS_BEDROCK",requiredEnvVars:["AWS_ACCESS_KEY_ID","AWS_SECRET_ACCESS_KEY","AWS_REGION"]},{id:"azure-openai",name:"Azure OpenAI",category:"\u4E91\u5E73\u53F0",status:"ai-sdk",docs:"https://azure.microsoft.com/en-us/products/cognitive-services/openai-service/",envVarPrefix:"AZURE_OPENAI",requiredEnvVars:["AZURE_OPENAI_API_KEY","AZURE_OPENAI_ENDPOINT"]},{id:"gcp-vertex",name:"GCP Vertex AI",category:"\u4E91\u5E73\u53F0",status:"ai-sdk",docs:"https://cloud.google.com/vertex-ai",envVarPrefix:"GOOGLE_VERTEX",requiredEnvVars:["GOOGLE_APPLICATION_CREDENTIALS"]},{id:"groq",name:"Groq",category:"\u63A8\u7406\u52A0\u901F",status:"opencode",docs:"https://console.groq.com/docs",envVarPrefix:"GROQ",requiredEnvVars:["GROQ_API_KEY"]},{id:"fireworks",name:"Fireworks AI",category:"\u63A8\u7406\u52A0\u901F",status:"opencode",docs:"https://docs.fireworks.ai",envVarPrefix:"FIREWORKS",requiredEnvVars:["FIREWORKS_API_KEY"]},{id:"together",name:"Together AI",category:"\u63A8\u7406\u52A0\u901F",status:"opencode",docs:"https://docs.together.ai",envVarPrefix:"TOGETHER",requiredEnvVars:["TOGETHER_API_KEY"]},{id:"huggingface",name:"Hugging Face",category:"\u63A8\u7406\u52A0\u901F",status:"opencode",docs:"https://huggingface.co/docs/inference-providers",envVarPrefix:"HF",requiredEnvVars:["HF_API_KEY"]},{id:"replicate",name:"Replicate",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://replicate.com/docs",envVarPrefix:"REPLICATE",requiredEnvVars:["REPLICATE_API_TOKEN"]},{id:"lepton",name:"Lepton AI",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://www.lepton.ai/docs",baseURL:"https://api.lepton.ai/v1",envVarPrefix:"LEPTON",requiredEnvVars:["LEPTON_API_KEY"]},{id:"anyscale",name:"Anyscale Endpoints",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://docs.endpoints.anyscale.com/",baseURL:"https://api.endpoints.anyscale.com/v1",envVarPrefix:"ANYSCALE",requiredEnvVars:["ANYSCALE_API_KEY"]},{id:"octoai",name:"OctoAI",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://octoai.cloud/docs",baseURL:"https://text.octoai.run/v1",envVarPrefix:"OCTOAI",requiredEnvVars:["OCTOAI_TOKEN"]},{id:"deepinfra",name:"DeepInfra",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://deepinfra.com/docs",baseURL:"https://api.deepinfra.com/v1/openai",envVarPrefix:"DEEPINFRA",requiredEnvVars:["DEEPINFRA_API_KEY"]},{id:"novita",name:"Novita AI",category:"\u63A8\u7406\u52A0\u901F",status:"cn-compatible",docs:"https://novita.ai/docs",baseURL:"https://api.novita.ai/v3/openai",envVarPrefix:"NOVITA",requiredEnvVars:["NOVITA_API_KEY"]},{id:"black-forest-labs",name:"Black Forest Labs (FLUX)",category:"\u591A\u6A21\u6001",status:"ai-sdk",docs:"https://docs.bfl.ml",envVarPrefix:"BFL",requiredEnvVars:["BFL_API_KEY"]},{id:"elevenlabs",name:"ElevenLabs (TTS)",category:"\u591A\u6A21\u6001",status:"ai-sdk",docs:"https://elevenlabs.io/docs",envVarPrefix:"ELEVENLABS",requiredEnvVars:["ELEVENLABS_API_KEY"]},{id:"runway",name:"Runway (Video)",category:"\u591A\u6A21\u6001",status:"pending",docs:"https://runwayml.com",envVarPrefix:"RUNWAY",requiredEnvVars:["RUNWAY_API_KEY"]},{id:"luma",name:"Luma (Video)",category:"\u591A\u6A21\u6001",status:"pending",docs:"https://lumalabs.ai",envVarPrefix:"LUMA",requiredEnvVars:["LUMA_API_KEY"]},{id:"midjourney",name:"Midjourney",category:"\u591A\u6A21\u6001",status:"pending",docs:"https://docs.midjourney.com",envVarPrefix:"MIDJOURNEY",requiredEnvVars:[]},{id:"ollama",name:"Ollama (\u672C\u5730)",category:"\u672C\u5730\u90E8\u7F72",status:"opencode",docs:"https://github.com/ollama/ollama",baseURL:"http://localhost:11434/v1",envVarPrefix:"OLLAMA",requiredEnvVars:["OLLAMA_BASE_URL"]},{id:"lmstudio",name:"LM Studio",category:"\u672C\u5730\u90E8\u7F72",status:"cn-compatible",docs:"https://lmstudio.ai/docs",baseURL:"http://localhost:1234/v1",envVarPrefix:"LMSTUDIO",requiredEnvVars:[]},{id:"vllm",name:"vLLM",category:"\u672C\u5730\u90E8\u7F72",status:"cn-compatible",docs:"https://docs.vllm.ai",baseURL:"http://localhost:8000/v1",envVarPrefix:"VLLM",requiredEnvVars:[]},{id:"tgi",name:"HF TGI",category:"\u672C\u5730\u90E8\u7F72",status:"cn-compatible",docs:"https://huggingface.co/docs/text-generation-inference",baseURL:"http://localhost:8080/v1",envVarPrefix:"TGI",requiredEnvVars:[]}],hs=Ge.reduce((e,t)=>(e[t.category]||(e[t.category]=[]),e[t.category].push(t),e),{});import Ie from"fs";import A from"path";var St=[{id:"posthog",name:"PostHog",region:"both",envVars:["POSTHOG_KEY","NEXT_PUBLIC_POSTHOG_KEY","NEXT_PUBLIC_POSTHOG_HOST"],docs:"https://posthog.com/docs",pkgs:["posthog-js","posthog-node"],providerFile:"apps/web/src/lib/analytics/posthog.client.ts"},{id:"plausible",name:"Plausible",region:"global",envVars:["NEXT_PUBLIC_PLAUSIBLE_DOMAIN","NEXT_PUBLIC_PLAUSIBLE_SRC"],docs:"https://plausible.io/docs",providerFile:"apps/web/src/components/analytics/PlausibleScript.tsx"},{id:"umami",name:"Umami",region:"both",envVars:["NEXT_PUBLIC_UMAMI_WEBSITE_ID","NEXT_PUBLIC_UMAMI_SRC"],docs:"https://umami.is/docs",providerFile:"apps/web/src/components/analytics/UmamiScript.tsx"},{id:"mixpanel",name:"Mixpanel",region:"global",envVars:["MIXPANEL_TOKEN","NEXT_PUBLIC_MIXPANEL_TOKEN"],docs:"https://docs.mixpanel.com",pkgs:["mixpanel-browser","mixpanel"],providerFile:"apps/web/src/lib/analytics/mixpanel.client.ts"},{id:"baidu",name:"\u767E\u5EA6\u7EDF\u8BA1 Baidu Tongji",region:"cn",envVars:["NEXT_PUBLIC_BAIDU_STATS_ID"],docs:"https://tongji.baidu.com",providerFile:"apps/web/src/components/analytics/BaiduStatsScript.tsx"},{id:"sensors",name:"\u795E\u7B56 Sensors Analytics",region:"cn",envVars:["NEXT_PUBLIC_SENSORS_SERVER_URL","NEXT_PUBLIC_SENSORS_APP_JS_URL"],docs:"https://manual.sensorsdata.cn",pkgs:["sa-sdk-javascript"],providerFile:"apps/web/src/lib/analytics/sensors.client.ts"},{id:"growingio",name:"GrowingIO",region:"cn",envVars:["NEXT_PUBLIC_GROWINGIO_ACCOUNT_ID"],docs:"https://growingio.com/docs",providerFile:"apps/web/src/components/analytics/GrowingIoScript.tsx"},{id:"none",name:"None",region:"both",envVars:[],docs:""}],vs=St.reduce((e,t)=>(e[t.region]||(e[t.region]=[]),e[t.region].push(t),e),{});function Et(e){return St.find(t=>t.id===e)}function Ln(e){Ie.mkdirSync(e,{recursive:!0})}function j(e,t){Ln(A.dirname(e)),Ie.writeFileSync(e,t)}function Q(e,t){let n=A.join(e,".env.example");Ie.existsSync(n)?Ie.appendFileSync(n,`
|
|
52
|
+
`+t):Ie.writeFileSync(n,t)}function Mn(e){let t=`"use client";
|
|
53
|
+
/**
|
|
54
|
+
* PostHog browser SDK \u2014 lazy-initialized on first access.
|
|
55
|
+
*/
|
|
56
|
+
import posthog from "posthog-js";
|
|
57
|
+
|
|
58
|
+
let initialized = false;
|
|
59
|
+
|
|
60
|
+
export function initPostHog(): void {
|
|
61
|
+
if (initialized || typeof window === "undefined") return;
|
|
62
|
+
const key = process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
|
63
|
+
if (!key) return;
|
|
64
|
+
|
|
65
|
+
posthog.init(key, {
|
|
66
|
+
api_host:
|
|
67
|
+
process.env.NEXT_PUBLIC_POSTHOG_HOST ?? "https://us.i.posthog.com",
|
|
68
|
+
person_profiles: "identified_only",
|
|
69
|
+
capture_pageview: true,
|
|
70
|
+
capture_pageleave: true,
|
|
71
|
+
autocapture: true,
|
|
72
|
+
});
|
|
73
|
+
initialized = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { posthog };
|
|
77
|
+
`,n=`import { PostHog } from "posthog-node";
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* PostHog server-side client. Remember to await client.shutdown() at the end
|
|
81
|
+
* of serverless requests so events are flushed before the invocation dies.
|
|
82
|
+
*/
|
|
83
|
+
let client: PostHog | null = null;
|
|
84
|
+
|
|
85
|
+
export function getPostHogServerClient(): PostHog | null {
|
|
86
|
+
if (client) return client;
|
|
87
|
+
const key = process.env.POSTHOG_KEY ?? process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
|
88
|
+
if (!key) return null;
|
|
89
|
+
|
|
90
|
+
client = new PostHog(key, {
|
|
91
|
+
host: process.env.NEXT_PUBLIC_POSTHOG_HOST ?? "https://us.i.posthog.com",
|
|
92
|
+
flushAt: 1,
|
|
93
|
+
flushInterval: 0,
|
|
94
|
+
});
|
|
95
|
+
return client;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function captureServerEvent(
|
|
99
|
+
distinctId: string,
|
|
100
|
+
event: string,
|
|
101
|
+
properties?: Record<string, unknown>,
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
const ph = getPostHogServerClient();
|
|
104
|
+
if (!ph) return;
|
|
105
|
+
ph.capture({ distinctId, event, properties });
|
|
106
|
+
await ph.shutdown();
|
|
107
|
+
}
|
|
108
|
+
`,r=`"use client";
|
|
109
|
+
/**
|
|
110
|
+
* <AnalyticsProvider /> \u2014 wrap the app root to enable PostHog + auto page views.
|
|
111
|
+
*/
|
|
112
|
+
import { useEffect } from "react";
|
|
113
|
+
import { usePathname, useSearchParams } from "next/navigation";
|
|
114
|
+
import { initPostHog, posthog } from "../../lib/analytics/posthog.client";
|
|
115
|
+
|
|
116
|
+
export function AnalyticsProvider({ children }: { children: React.ReactNode }) {
|
|
117
|
+
const pathname = usePathname();
|
|
118
|
+
const searchParams = useSearchParams();
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
initPostHog();
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!pathname) return;
|
|
126
|
+
const url =
|
|
127
|
+
window.location.origin +
|
|
128
|
+
pathname +
|
|
129
|
+
(searchParams?.toString() ? \`?\${searchParams.toString()}\` : "");
|
|
130
|
+
posthog.capture("$pageview", { $current_url: url });
|
|
131
|
+
}, [pathname, searchParams]);
|
|
132
|
+
|
|
133
|
+
return <>{children}</>;
|
|
134
|
+
}
|
|
135
|
+
`;j(A.join(e,"src","lib","analytics","posthog.client.ts"),t),j(A.join(e,"src","lib","analytics","posthog.server.ts"),n),j(A.join(e,"src","components","analytics","AnalyticsProvider.tsx"),r)}function jn(){return`# PostHog \u2014 product analytics
|
|
136
|
+
POSTHOG_KEY=
|
|
137
|
+
NEXT_PUBLIC_POSTHOG_KEY=
|
|
138
|
+
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
|
139
|
+
`}function Dn(e){j(A.join(e,"src","components","analytics","PlausibleScript.tsx"),`"use client";
|
|
140
|
+
/**
|
|
141
|
+
* Plausible script tag \u2014 lightweight, privacy-friendly analytics.
|
|
142
|
+
*/
|
|
143
|
+
import Script from "next/script";
|
|
144
|
+
|
|
145
|
+
export function PlausibleScript() {
|
|
146
|
+
const domain = process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN;
|
|
147
|
+
const src =
|
|
148
|
+
process.env.NEXT_PUBLIC_PLAUSIBLE_SRC ?? "https://plausible.io/js/script.js";
|
|
149
|
+
if (!domain) return null;
|
|
150
|
+
return (
|
|
151
|
+
<Script
|
|
152
|
+
id="plausible"
|
|
153
|
+
strategy="afterInteractive"
|
|
154
|
+
data-domain={domain}
|
|
155
|
+
src={src}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
`)}function Fn(){return`# Plausible \u2014 privacy-friendly analytics
|
|
160
|
+
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=
|
|
161
|
+
NEXT_PUBLIC_PLAUSIBLE_SRC=https://plausible.io/js/script.js
|
|
162
|
+
`}function Bn(e){j(A.join(e,"src","components","analytics","UmamiScript.tsx"),`"use client";
|
|
163
|
+
/**
|
|
164
|
+
* Umami analytics \u2014 self-hosted OR umami.is cloud.
|
|
165
|
+
*/
|
|
166
|
+
import Script from "next/script";
|
|
167
|
+
|
|
168
|
+
export function UmamiScript() {
|
|
169
|
+
const websiteId = process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID;
|
|
170
|
+
const src =
|
|
171
|
+
process.env.NEXT_PUBLIC_UMAMI_SRC ?? "https://cloud.umami.is/script.js";
|
|
172
|
+
if (!websiteId) return null;
|
|
173
|
+
return (
|
|
174
|
+
<Script
|
|
175
|
+
id="umami"
|
|
176
|
+
strategy="afterInteractive"
|
|
177
|
+
src={src}
|
|
178
|
+
data-website-id={websiteId}
|
|
179
|
+
/>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
`)}function Yn(){return`# Umami \u2014 self-hostable analytics
|
|
183
|
+
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
|
|
184
|
+
NEXT_PUBLIC_UMAMI_SRC=https://cloud.umami.is/script.js
|
|
185
|
+
`}function Kn(e){let t=`"use client";
|
|
186
|
+
import mixpanel from "mixpanel-browser";
|
|
187
|
+
|
|
188
|
+
let initialized = false;
|
|
189
|
+
|
|
190
|
+
export function initMixpanel(): void {
|
|
191
|
+
if (initialized || typeof window === "undefined") return;
|
|
192
|
+
const token = process.env.NEXT_PUBLIC_MIXPANEL_TOKEN;
|
|
193
|
+
if (!token) return;
|
|
194
|
+
mixpanel.init(token, {
|
|
195
|
+
debug: process.env.NODE_ENV !== "production",
|
|
196
|
+
track_pageview: true,
|
|
197
|
+
persistence: "localStorage",
|
|
198
|
+
});
|
|
199
|
+
initialized = true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export { mixpanel };
|
|
203
|
+
`,n=`import Mixpanel from "mixpanel";
|
|
204
|
+
|
|
205
|
+
let client: Mixpanel.Mixpanel | null = null;
|
|
206
|
+
|
|
207
|
+
export function getMixpanelServerClient(): Mixpanel.Mixpanel | null {
|
|
208
|
+
if (client) return client;
|
|
209
|
+
const token = process.env.MIXPANEL_TOKEN ?? process.env.NEXT_PUBLIC_MIXPANEL_TOKEN;
|
|
210
|
+
if (!token) return null;
|
|
211
|
+
client = Mixpanel.init(token);
|
|
212
|
+
return client;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function trackServer(
|
|
216
|
+
distinctId: string,
|
|
217
|
+
event: string,
|
|
218
|
+
properties?: Record<string, unknown>,
|
|
219
|
+
): void {
|
|
220
|
+
const c = getMixpanelServerClient();
|
|
221
|
+
if (!c) return;
|
|
222
|
+
c.track(event, { distinct_id: distinctId, ...properties });
|
|
223
|
+
}
|
|
224
|
+
`;j(A.join(e,"src","lib","analytics","mixpanel.client.ts"),t),j(A.join(e,"src","lib","analytics","mixpanel.server.ts"),n)}function Vn(){return`# Mixpanel \u2014 product analytics
|
|
225
|
+
MIXPANEL_TOKEN=
|
|
226
|
+
NEXT_PUBLIC_MIXPANEL_TOKEN=
|
|
227
|
+
`}function $n(e){j(A.join(e,"src","components","analytics","BaiduStatsScript.tsx"),`"use client";
|
|
228
|
+
/**
|
|
229
|
+
* \u767E\u5EA6\u7EDF\u8BA1 Baidu Tongji \u2014 hm.js \u57CB\u70B9\u811A\u672C.
|
|
230
|
+
*/
|
|
231
|
+
import Script from "next/script";
|
|
232
|
+
|
|
233
|
+
export function BaiduStatsScript() {
|
|
234
|
+
const id = process.env.NEXT_PUBLIC_BAIDU_STATS_ID;
|
|
235
|
+
if (!id) return null;
|
|
236
|
+
const inline = \`
|
|
237
|
+
var _hmt = _hmt || [];
|
|
238
|
+
(function() {
|
|
239
|
+
var hm = document.createElement("script");
|
|
240
|
+
hm.src = "https://hm.baidu.com/hm.js?\${id}";
|
|
241
|
+
var s = document.getElementsByTagName("script")[0];
|
|
242
|
+
s.parentNode.insertBefore(hm, s);
|
|
243
|
+
})();
|
|
244
|
+
\`;
|
|
245
|
+
return (
|
|
246
|
+
<Script
|
|
247
|
+
id="baidu-stats"
|
|
248
|
+
strategy="afterInteractive"
|
|
249
|
+
dangerouslySetInnerHTML={{ __html: inline }}
|
|
250
|
+
/>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
`)}function qn(){return`# \u767E\u5EA6\u7EDF\u8BA1 \u2014 \u4E2D\u56FD\u533A\u7F51\u7AD9\u5206\u6790
|
|
254
|
+
NEXT_PUBLIC_BAIDU_STATS_ID=
|
|
255
|
+
`}function Gn(e){j(A.join(e,"src","lib","analytics","sensors.client.ts"),`"use client";
|
|
256
|
+
/**
|
|
257
|
+
* \u795E\u7B56\u5206\u6790 (Sensors Analytics) \u2014 sa-sdk-javascript \u5C01\u88C5.
|
|
258
|
+
*/
|
|
259
|
+
import sensors from "sa-sdk-javascript";
|
|
260
|
+
|
|
261
|
+
let initialized = false;
|
|
262
|
+
|
|
263
|
+
export function initSensors(): void {
|
|
264
|
+
if (initialized || typeof window === "undefined") return;
|
|
265
|
+
const serverUrl = process.env.NEXT_PUBLIC_SENSORS_SERVER_URL;
|
|
266
|
+
if (!serverUrl) return;
|
|
267
|
+
|
|
268
|
+
sensors.init({
|
|
269
|
+
server_url: serverUrl,
|
|
270
|
+
is_track_single_page: true,
|
|
271
|
+
use_client_time: true,
|
|
272
|
+
send_type: "beacon",
|
|
273
|
+
heatmap: {
|
|
274
|
+
clickmap: "default",
|
|
275
|
+
scroll_notice_map: "default",
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
sensors.quick("autoTrack");
|
|
279
|
+
initialized = true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export { sensors };
|
|
283
|
+
`)}function Wn(){return`# \u795E\u7B56\u5206\u6790 Sensors Analytics
|
|
284
|
+
NEXT_PUBLIC_SENSORS_SERVER_URL=
|
|
285
|
+
NEXT_PUBLIC_SENSORS_APP_JS_URL=
|
|
286
|
+
`}function Xn(e){j(A.join(e,"src","components","analytics","GrowingIoScript.tsx"),`"use client";
|
|
287
|
+
/**
|
|
288
|
+
* GrowingIO CDP \u2014 inline snippet installer.
|
|
289
|
+
*/
|
|
290
|
+
import Script from "next/script";
|
|
291
|
+
|
|
292
|
+
export function GrowingIoScript() {
|
|
293
|
+
const accountId = process.env.NEXT_PUBLIC_GROWINGIO_ACCOUNT_ID;
|
|
294
|
+
if (!accountId) return null;
|
|
295
|
+
const inline = \`
|
|
296
|
+
!(function(e,t,n,g,i){e[i]=e[i]||function(){(e[i].q=e[i].q||[]).push(arguments)};
|
|
297
|
+
var a=t.createElement(n);a.async=!0;a.src=g;
|
|
298
|
+
var s=t.getElementsByTagName(n)[0];s.parentNode.insertBefore(a,s);
|
|
299
|
+
})(window,document,"script","https://assets.giocdn.com/vds.js","gdp");
|
|
300
|
+
gdp("init", "\${accountId}", {});
|
|
301
|
+
gdp("send");
|
|
302
|
+
\`;
|
|
303
|
+
return (
|
|
304
|
+
<Script
|
|
305
|
+
id="growingio"
|
|
306
|
+
strategy="afterInteractive"
|
|
307
|
+
dangerouslySetInnerHTML={{ __html: inline }}
|
|
308
|
+
/>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
`)}function Hn(){return`# GrowingIO \u2014 \u5168\u57CB\u70B9 CDP (\u4E2D\u56FD\u533A)
|
|
312
|
+
NEXT_PUBLIC_GROWINGIO_ACCOUNT_ID=
|
|
313
|
+
`}async function ht(e,t,n="global"){let r=Et(t);if(!r||r.id==="none")return;let o=A.join(e,"apps","web");if(!Ie.existsSync(o))return;let s=r.id;try{switch(s){case"posthog":Mn(o),Q(e,jn());return;case"plausible":Dn(o),Q(e,Fn());return;case"umami":Bn(o),Q(e,Yn());return;case"mixpanel":Kn(o),Q(e,Vn());return;case"baidu":$n(o),Q(e,qn());return;case"sensors":Gn(o),Q(e,Wn());return;case"growingio":Xn(o),Q(e,Hn());return;default:return}}catch(a){throw console.error(`Failed to apply analytics selection "${t}":`,a),new Error(`Analytics scaffold for "${t}" failed \u2014 see stderr for details.`)}}import w from"fs";import Z from"path";function et(e){w.existsSync(e)&&w.rmSync(e,{recursive:!0,force:!0})}function _t(e,t){let n=Z.join(e,".env.example");w.existsSync(n)?w.appendFileSync(n,`
|
|
314
|
+
`+t):w.writeFileSync(n,t)}function vt(e,t){if(!w.existsSync(e))return;let n=w.readFileSync(e,"utf8"),r=n.replace(/export type AuthProviderId\s*=\s*"[^"]+"\s*\|\s*"[^"]+";/,`export type AuthProviderId = "${t}";`);r!==n&&w.writeFileSync(e,r)}function zn(e,t){let n=Z.join(e,"README.md");if(!w.existsSync(n)){w.writeFileSync(n,t);return}w.appendFileSync(n,`
|
|
315
|
+
`+t)}async function It(e,t){let n=Z.join(e,"packages","auth");if(!w.existsSync(n))return;if(t==="none"){et(n),zn(e,`
|
|
41
316
|
## Auth
|
|
42
317
|
|
|
43
318
|
Auth was skipped at scaffold time (\`--auth=none\`). Add your preferred auth provider manually (e.g. Clerk, Better Auth, NextAuth).
|
|
44
|
-
`);return}let
|
|
319
|
+
`);return}let r=Z.join(n,"src","providers"),o=Z.join(n,"src","types.ts");if(t==="clerk"){et(Z.join(r,"better-auth.ts")),vt(o,"clerk"),_t(e,`# Clerk auth
|
|
45
320
|
CLERK_PUBLISHABLE_KEY=
|
|
46
321
|
CLERK_SECRET_KEY=
|
|
47
|
-
`);return}if(t==="betterauth"){
|
|
322
|
+
`);return}if(t==="betterauth"){et(Z.join(r,"clerk.ts")),vt(o,"better-auth"),_t(e,`# Better Auth
|
|
48
323
|
BETTER_AUTH_SECRET=
|
|
49
324
|
BETTER_AUTH_URL=http://localhost:3000
|
|
50
|
-
`);return}}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
325
|
+
`);return}}var Re=[{id:"wechat",name:"\u5FAE\u4FE1\u5F00\u653E\u5E73\u53F0",region:"cn",envVars:["WECHAT_APP_ID","WECHAT_APP_SECRET"],docs:"https://open.weixin.qq.com/cgi-bin/showdocument",callbackUrl:"/api/auth/callback/wechat",tokenEndpoint:"https://api.weixin.qq.com/sns/oauth2/access_token"},{id:"qq",name:"QQ \u767B\u5F55",region:"cn",envVars:["QQ_APP_ID","QQ_APP_KEY"],docs:"https://wiki.connect.qq.com",callbackUrl:"/api/auth/callback/qq",tokenEndpoint:"https://graph.qq.com/oauth2.0/token"},{id:"dingtalk",name:"\u9489\u9489",region:"cn",envVars:["DINGTALK_APP_ID","DINGTALK_APP_SECRET"],docs:"https://open.dingtalk.com/document/",callbackUrl:"/api/auth/callback/dingtalk",tokenEndpoint:"https://api.dingtalk.com/v1.0/oauth2/userAccessToken"},{id:"workweixin",name:"\u4F01\u4E1A\u5FAE\u4FE1",region:"cn",envVars:["WORKWEIXIN_CORP_ID","WORKWEIXIN_AGENT_ID","WORKWEIXIN_SECRET"],docs:"https://developer.work.weixin.qq.com",callbackUrl:"/api/auth/callback/workweixin",tokenEndpoint:"https://qyapi.weixin.qq.com/cgi-bin/gettoken"},{id:"feishu",name:"\u98DE\u4E66 / Lark",region:"cn",envVars:["FEISHU_APP_ID","FEISHU_APP_SECRET"],docs:"https://open.feishu.cn/document/",callbackUrl:"/api/auth/callback/feishu",tokenEndpoint:"https://open.feishu.cn/open-apis/authen/v1/access_token"},{id:"weibo",name:"\u5FAE\u535A",region:"cn",envVars:["WEIBO_APP_KEY","WEIBO_APP_SECRET"],docs:"https://open.weibo.com/wiki/",callbackUrl:"/api/auth/callback/weibo",tokenEndpoint:"https://api.weibo.com/oauth2/access_token"}],Jn=new Set(Re.map(e=>e.id));function Pt(e){return Re.find(t=>t.id===e)}function bt(e){if(!e)return[];let t=e.split(",").map(o=>o.trim().toLowerCase()).filter(Boolean);if(t.includes("none"))return[];let n=[],r=new Set;for(let o of t)Jn.has(o)&&!r.has(o)&&(n.push(o),r.add(o));return n}import te from"fs";import ee from"path";function Qn(e,t){let n=`// apps/web/src/app/api/auth/callback/${e.id}/route.ts
|
|
326
|
+
// Generated by create-sailor \u2014 OAuth callback for ${e.name}
|
|
327
|
+
// Primary auth provider: ${t}
|
|
328
|
+
// Docs: ${e.docs}`,r={wechat:` const tokenRes = await fetch(
|
|
329
|
+
"${e.tokenEndpoint}?" +
|
|
330
|
+
new URLSearchParams({
|
|
331
|
+
appid: process.env.WECHAT_APP_ID ?? "",
|
|
332
|
+
secret: process.env.WECHAT_APP_SECRET ?? "",
|
|
333
|
+
code,
|
|
334
|
+
grant_type: "authorization_code",
|
|
335
|
+
}),
|
|
336
|
+
);
|
|
337
|
+
const token = (await tokenRes.json()) as {
|
|
338
|
+
access_token?: string;
|
|
339
|
+
openid?: string;
|
|
340
|
+
errcode?: number;
|
|
341
|
+
errmsg?: string;
|
|
342
|
+
};
|
|
343
|
+
if (!token.access_token || !token.openid) {
|
|
344
|
+
return NextResponse.redirect(new URL("/login?error=wechat_token", req.url));
|
|
345
|
+
}`,qq:` const tokenRes = await fetch(
|
|
346
|
+
"${e.tokenEndpoint}?" +
|
|
347
|
+
new URLSearchParams({
|
|
348
|
+
client_id: process.env.QQ_APP_ID ?? "",
|
|
349
|
+
client_secret: process.env.QQ_APP_KEY ?? "",
|
|
350
|
+
code,
|
|
351
|
+
grant_type: "authorization_code",
|
|
352
|
+
redirect_uri: new URL("${e.callbackUrl}", req.url).toString(),
|
|
353
|
+
fmt: "json",
|
|
354
|
+
}),
|
|
355
|
+
);
|
|
356
|
+
const token = (await tokenRes.json()) as { access_token?: string; openid?: string };
|
|
357
|
+
if (!token.access_token) {
|
|
358
|
+
return NextResponse.redirect(new URL("/login?error=qq_token", req.url));
|
|
359
|
+
}`,dingtalk:` const tokenRes = await fetch("${e.tokenEndpoint}", {
|
|
360
|
+
method: "POST",
|
|
361
|
+
headers: { "Content-Type": "application/json" },
|
|
362
|
+
body: JSON.stringify({
|
|
363
|
+
clientId: process.env.DINGTALK_APP_ID ?? "",
|
|
364
|
+
clientSecret: process.env.DINGTALK_APP_SECRET ?? "",
|
|
365
|
+
code,
|
|
366
|
+
grantType: "authorization_code",
|
|
367
|
+
}),
|
|
368
|
+
});
|
|
369
|
+
const token = (await tokenRes.json()) as { accessToken?: string };
|
|
370
|
+
if (!token.accessToken) {
|
|
371
|
+
return NextResponse.redirect(new URL("/login?error=dingtalk_token", req.url));
|
|
372
|
+
}`,workweixin:` // Enterprise WeChat returns an access_token by pairing corpId + secret
|
|
373
|
+
const tokenRes = await fetch(
|
|
374
|
+
"${e.tokenEndpoint}?" +
|
|
375
|
+
new URLSearchParams({
|
|
376
|
+
corpid: process.env.WORKWEIXIN_CORP_ID ?? "",
|
|
377
|
+
corpsecret: process.env.WORKWEIXIN_SECRET ?? "",
|
|
378
|
+
}),
|
|
379
|
+
);
|
|
380
|
+
const token = (await tokenRes.json()) as {
|
|
381
|
+
access_token?: string;
|
|
382
|
+
errcode?: number;
|
|
383
|
+
errmsg?: string;
|
|
384
|
+
};
|
|
385
|
+
if (!token.access_token) {
|
|
386
|
+
return NextResponse.redirect(new URL("/login?error=workweixin_token", req.url));
|
|
387
|
+
}`,feishu:` // Step 1: app_access_token
|
|
388
|
+
const appTokenRes = await fetch(
|
|
389
|
+
"https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal",
|
|
390
|
+
{
|
|
391
|
+
method: "POST",
|
|
392
|
+
headers: { "Content-Type": "application/json" },
|
|
393
|
+
body: JSON.stringify({
|
|
394
|
+
app_id: process.env.FEISHU_APP_ID ?? "",
|
|
395
|
+
app_secret: process.env.FEISHU_APP_SECRET ?? "",
|
|
396
|
+
}),
|
|
397
|
+
},
|
|
398
|
+
);
|
|
399
|
+
const appToken = (await appTokenRes.json()) as { app_access_token?: string };
|
|
400
|
+
if (!appToken.app_access_token) {
|
|
401
|
+
return NextResponse.redirect(new URL("/login?error=feishu_app_token", req.url));
|
|
402
|
+
}
|
|
403
|
+
// Step 2: user access_token
|
|
404
|
+
const tokenRes = await fetch("${e.tokenEndpoint}", {
|
|
405
|
+
method: "POST",
|
|
406
|
+
headers: {
|
|
407
|
+
"Content-Type": "application/json",
|
|
408
|
+
Authorization: \`Bearer \${appToken.app_access_token}\`,
|
|
409
|
+
},
|
|
410
|
+
body: JSON.stringify({ grant_type: "authorization_code", code }),
|
|
411
|
+
});
|
|
412
|
+
const token = (await tokenRes.json()) as { data?: { access_token?: string } };
|
|
413
|
+
if (!token.data?.access_token) {
|
|
414
|
+
return NextResponse.redirect(new URL("/login?error=feishu_token", req.url));
|
|
415
|
+
}`,weibo:` const tokenRes = await fetch("${e.tokenEndpoint}", {
|
|
416
|
+
method: "POST",
|
|
417
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
418
|
+
body: new URLSearchParams({
|
|
419
|
+
client_id: process.env.WEIBO_APP_KEY ?? "",
|
|
420
|
+
client_secret: process.env.WEIBO_APP_SECRET ?? "",
|
|
421
|
+
grant_type: "authorization_code",
|
|
422
|
+
code,
|
|
423
|
+
redirect_uri: new URL("${e.callbackUrl}", req.url).toString(),
|
|
424
|
+
}),
|
|
425
|
+
});
|
|
426
|
+
const token = (await tokenRes.json()) as { access_token?: string; uid?: string };
|
|
427
|
+
if (!token.access_token) {
|
|
428
|
+
return NextResponse.redirect(new URL("/login?error=weibo_token", req.url));
|
|
429
|
+
}`};return`${n}
|
|
430
|
+
import { type NextRequest, NextResponse } from "next/server";
|
|
431
|
+
|
|
432
|
+
export async function GET(req: NextRequest) {
|
|
433
|
+
const code = req.nextUrl.searchParams.get("code");
|
|
434
|
+
const state = req.nextUrl.searchParams.get("state");
|
|
435
|
+
|
|
436
|
+
if (!code) {
|
|
437
|
+
return NextResponse.redirect(new URL("/login?error=no_code", req.url));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
${r[e.id]}
|
|
441
|
+
|
|
442
|
+
// TODO: fetch user profile from ${e.name} using the access token.
|
|
443
|
+
// TODO: upsert the user into your primary auth provider (${t}).
|
|
444
|
+
// e.g. clerk.users.createUser(...) or betterAuth.signIn.oauth(...).
|
|
445
|
+
// Persist a session cookie before redirecting.
|
|
446
|
+
|
|
447
|
+
void state;
|
|
448
|
+
return NextResponse.redirect(new URL("/dashboard", req.url));
|
|
449
|
+
}
|
|
450
|
+
`}function Zn(e){return`"use client";
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* SocialLoginButtons \u2014 generated by create-sailor.
|
|
454
|
+
*
|
|
455
|
+
* Renders a button per selected China-region OAuth provider. Clicking a
|
|
456
|
+
* button navigates to the provider's authorization URL (\`/api/auth/oauth/<id>\`),
|
|
457
|
+
* which the app is expected to implement and redirect to the upstream consent
|
|
458
|
+
* page. The OAuth callback lives at \`/api/auth/callback/<id>\`.
|
|
459
|
+
*/
|
|
460
|
+
|
|
461
|
+
import { Button } from "@nebutra/ui/components";
|
|
462
|
+
|
|
463
|
+
interface SocialProvider {
|
|
464
|
+
id: string;
|
|
465
|
+
name: string;
|
|
466
|
+
href: string;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const PROVIDERS: SocialProvider[] = [
|
|
470
|
+
${e.map(n=>` { id: ${JSON.stringify(n.id)}, name: ${JSON.stringify(n.name)}, href: ${JSON.stringify(`/api/auth/oauth/${n.id}`)} }`).join(`,
|
|
471
|
+
`)},
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
export function SocialLoginButtons() {
|
|
475
|
+
return (
|
|
476
|
+
<div className="space-y-2">
|
|
477
|
+
{PROVIDERS.map((p) => (
|
|
478
|
+
<Button
|
|
479
|
+
key={p.id}
|
|
480
|
+
type="button"
|
|
481
|
+
variant="outline"
|
|
482
|
+
className="w-full"
|
|
483
|
+
onClick={() => {
|
|
484
|
+
window.location.href = p.href;
|
|
485
|
+
}}
|
|
486
|
+
>
|
|
487
|
+
Sign in with {p.name}
|
|
488
|
+
</Button>
|
|
489
|
+
))}
|
|
490
|
+
</div>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
`}function At(e){te.mkdirSync(e,{recursive:!0})}function er(e,t){let n=ee.join(e,".env.example");te.existsSync(n)?te.appendFileSync(n,`
|
|
494
|
+
`+t):te.writeFileSync(n,t)}function tr(e){let t=`# ${e.name} (${e.id}) \u2014 ${e.docs}`,n=e.envVars.map(r=>`${r}=`).join(`
|
|
495
|
+
`);return`${t}
|
|
496
|
+
${n}
|
|
497
|
+
`}async function Ct(e,t,n){if(!t||t.length===0)return;let r=ee.join(e,"apps","web");if(!te.existsSync(r))return;let o=new Set,s=[];for(let d of t){if(o.has(d))continue;o.add(d);let m=Pt(d);m&&s.push(m)}if(s.length===0)return;for(let d of s)er(e,tr(d));let a=ee.join(r,"src","app","api","auth","callback");for(let d of s){let m=ee.join(a,d.id);At(m);let S=ee.join(m,"route.ts");te.writeFileSync(S,Qn(d,n))}let c=ee.join(r,"src","components","auth");At(c);let i=ee.join(c,"SocialLoginButtons.tsx");te.writeFileSync(i,Zn(s))}import P from"fs";import v from"path";var nr=v.join("apps","web"),tt=v.join("src","components","compliance"),ke=v.join("src","app","[locale]");function D(e){return v.join(e,nr)}function rr(e){P.mkdirSync(e,{recursive:!0})}function k(e,t){rr(v.dirname(e)),P.writeFileSync(e,t,"utf8")}function nt(e,t){let n=v.join(e,".env.example");if(P.existsSync(n)){if(P.readFileSync(n,"utf8").includes(t.split(`
|
|
498
|
+
`)[0]??""))return;P.appendFileSync(n,`
|
|
499
|
+
`+t+`
|
|
500
|
+
`)}else P.writeFileSync(n,t+`
|
|
501
|
+
`)}var Oe=`> **TEMPLATE \u2014 NOT LEGAL ADVICE.** This document is a boilerplate scaffold only.
|
|
502
|
+
> Consult a qualified lawyer in every jurisdiction you operate in before publishing.
|
|
503
|
+
> \u672C\u6587\u6863\u4E3A\u6A21\u677F\u811A\u624B\u67B6\uFF0C\u4E0D\u6784\u6210\u6CD5\u5F8B\u610F\u89C1\u3002\u53D1\u5E03\u524D\u8BF7\u54A8\u8BE2\u76F8\u5173\u53F8\u6CD5\u8F96\u533A\u7684\u5408\u683C\u5F8B\u5E08\u3002`,or=`import Link from "next/link";
|
|
504
|
+
import { IcpFooter } from "@/components/compliance/IcpFooter";
|
|
505
|
+
import { PublicSecurityFooter } from "@/components/compliance/PublicSecurityFooter";
|
|
506
|
+
|
|
507
|
+
interface FooterProps {
|
|
508
|
+
region?: "global" | "cn" | "hybrid";
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function Footer({ region = "global" }: FooterProps) {
|
|
512
|
+
const showCn = region === "cn" || region === "hybrid";
|
|
513
|
+
return (
|
|
514
|
+
<footer className="border-t border-[var(--neutral-7)] bg-[var(--neutral-2)] py-8">
|
|
515
|
+
<div className="mx-auto flex max-w-[1400px] flex-col gap-4 px-4 text-sm text-[var(--neutral-11)] md:flex-row md:items-center md:justify-between md:px-6">
|
|
516
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
517
|
+
<Link href="/privacy" className="hover:underline">
|
|
518
|
+
Privacy Policy / \u9690\u79C1\u653F\u7B56
|
|
519
|
+
</Link>
|
|
520
|
+
<Link href="/terms" className="hover:underline">
|
|
521
|
+
Terms of Service / \u670D\u52A1\u6761\u6B3E
|
|
522
|
+
</Link>
|
|
523
|
+
<button
|
|
524
|
+
type="button"
|
|
525
|
+
onClick={() => window.dispatchEvent(new CustomEvent("cookie-consent-open"))}
|
|
526
|
+
className="hover:underline"
|
|
527
|
+
>
|
|
528
|
+
Cookie Settings
|
|
529
|
+
</button>
|
|
530
|
+
{showCn ? (
|
|
531
|
+
<>
|
|
532
|
+
<Link href="/aigc-disclosure" className="hover:underline">
|
|
533
|
+
AIGC \u8BF4\u660E
|
|
534
|
+
</Link>
|
|
535
|
+
<Link href="/cross-border-data" className="hover:underline">
|
|
536
|
+
\u6570\u636E\u51FA\u5883
|
|
537
|
+
</Link>
|
|
538
|
+
<Link href="/pii-collection" className="hover:underline">
|
|
539
|
+
\u4E2A\u4EBA\u4FE1\u606F\u6E05\u5355
|
|
540
|
+
</Link>
|
|
541
|
+
</>
|
|
542
|
+
) : null}
|
|
543
|
+
</div>
|
|
544
|
+
{showCn ? (
|
|
545
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
546
|
+
<IcpFooter />
|
|
547
|
+
<PublicSecurityFooter />
|
|
548
|
+
</div>
|
|
549
|
+
) : null}
|
|
550
|
+
</div>
|
|
551
|
+
</footer>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
`,sr=`export function IcpFooter() {
|
|
555
|
+
const icp = process.env.NEXT_PUBLIC_ICP_BEIAN;
|
|
556
|
+
if (!icp) return null;
|
|
557
|
+
return (
|
|
558
|
+
<a
|
|
559
|
+
href="https://beian.miit.gov.cn/"
|
|
560
|
+
target="_blank"
|
|
561
|
+
rel="noopener noreferrer"
|
|
562
|
+
className="text-xs text-[var(--neutral-11)] hover:underline"
|
|
563
|
+
>
|
|
564
|
+
{icp}
|
|
565
|
+
</a>
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
`,ir=`export function PublicSecurityFooter() {
|
|
569
|
+
const psb = process.env.NEXT_PUBLIC_PSB_BEIAN;
|
|
570
|
+
const psbCode = process.env.NEXT_PUBLIC_PSB_CODE;
|
|
571
|
+
if (!psb) return null;
|
|
572
|
+
return (
|
|
573
|
+
<a
|
|
574
|
+
href={\`http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=\${psbCode ?? ""}\`}
|
|
575
|
+
target="_blank"
|
|
576
|
+
rel="noopener noreferrer"
|
|
577
|
+
className="inline-flex items-center gap-1 text-xs text-[var(--neutral-11)] hover:underline"
|
|
578
|
+
>
|
|
579
|
+
{/* Place /public/compliance/psb-icon.png (\u5B98\u65B9\u516C\u5B89\u5907\u6848\u5C0F\u5FBD\u6807) */}
|
|
580
|
+
<img src="/compliance/psb-icon.png" alt="\u516C\u5B89\u5907\u6848" className="h-3 w-3" />
|
|
581
|
+
{psb}
|
|
582
|
+
</a>
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
`,ar=`"use client";
|
|
586
|
+
|
|
587
|
+
import { useCallback, useEffect, useState } from "react";
|
|
588
|
+
|
|
589
|
+
type ConsentMode = "accepted" | "rejected" | "custom";
|
|
590
|
+
|
|
591
|
+
interface CustomConsent {
|
|
592
|
+
necessary: true; // always true per PIPL/GDPR
|
|
593
|
+
analytics: boolean;
|
|
594
|
+
marketing: boolean;
|
|
595
|
+
preferences: boolean;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
interface StoredConsent {
|
|
599
|
+
mode: ConsentMode;
|
|
600
|
+
categories: CustomConsent;
|
|
601
|
+
timestamp: number;
|
|
602
|
+
version: 1;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const STORAGE_KEY = "cookie-consent";
|
|
606
|
+
const CONSENT_EVENT = "cookie-consent-changed";
|
|
607
|
+
const OPEN_EVENT = "cookie-consent-open";
|
|
608
|
+
|
|
609
|
+
const DEFAULT_CATEGORIES: CustomConsent = {
|
|
610
|
+
necessary: true,
|
|
611
|
+
analytics: false,
|
|
612
|
+
marketing: false,
|
|
613
|
+
preferences: false,
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
function emitConsent(consent: StoredConsent): void {
|
|
617
|
+
if (typeof window === "undefined") return;
|
|
618
|
+
window.dispatchEvent(new CustomEvent(CONSENT_EVENT, { detail: consent }));
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export function CookieBanner() {
|
|
622
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
623
|
+
const [showCustomize, setShowCustomize] = useState(false);
|
|
624
|
+
const [categories, setCategories] = useState<CustomConsent>(DEFAULT_CATEGORIES);
|
|
625
|
+
|
|
626
|
+
useEffect(() => {
|
|
627
|
+
try {
|
|
628
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
629
|
+
if (!stored) {
|
|
630
|
+
setIsOpen(true);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const parsed = JSON.parse(stored) as StoredConsent;
|
|
634
|
+
// Re-emit so analytics/marketing scripts mounted after consent can read state
|
|
635
|
+
emitConsent(parsed);
|
|
636
|
+
} catch {
|
|
637
|
+
setIsOpen(true);
|
|
638
|
+
}
|
|
639
|
+
}, []);
|
|
640
|
+
|
|
641
|
+
useEffect(() => {
|
|
642
|
+
const handler = () => {
|
|
643
|
+
setShowCustomize(true);
|
|
644
|
+
setIsOpen(true);
|
|
645
|
+
};
|
|
646
|
+
window.addEventListener(OPEN_EVENT, handler);
|
|
647
|
+
return () => window.removeEventListener(OPEN_EVENT, handler);
|
|
648
|
+
}, []);
|
|
649
|
+
|
|
650
|
+
const persist = useCallback((mode: ConsentMode, cats: CustomConsent) => {
|
|
651
|
+
const consent: StoredConsent = {
|
|
652
|
+
mode,
|
|
653
|
+
categories: cats,
|
|
654
|
+
timestamp: Date.now(),
|
|
655
|
+
version: 1,
|
|
656
|
+
};
|
|
657
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(consent));
|
|
658
|
+
emitConsent(consent);
|
|
659
|
+
setIsOpen(false);
|
|
660
|
+
setShowCustomize(false);
|
|
661
|
+
}, []);
|
|
662
|
+
|
|
663
|
+
const acceptAll = useCallback(() => {
|
|
664
|
+
persist("accepted", {
|
|
665
|
+
necessary: true,
|
|
666
|
+
analytics: true,
|
|
667
|
+
marketing: true,
|
|
668
|
+
preferences: true,
|
|
669
|
+
});
|
|
670
|
+
}, [persist]);
|
|
671
|
+
|
|
672
|
+
const rejectNonEssential = useCallback(() => {
|
|
673
|
+
persist("rejected", DEFAULT_CATEGORIES);
|
|
674
|
+
}, [persist]);
|
|
675
|
+
|
|
676
|
+
const saveCustom = useCallback(() => {
|
|
677
|
+
persist("custom", categories);
|
|
678
|
+
}, [persist, categories]);
|
|
679
|
+
|
|
680
|
+
if (!isOpen) return null;
|
|
681
|
+
|
|
682
|
+
return (
|
|
683
|
+
<div
|
|
684
|
+
role="dialog"
|
|
685
|
+
aria-modal="false"
|
|
686
|
+
aria-labelledby="cookie-banner-title"
|
|
687
|
+
className="fixed inset-x-0 bottom-0 z-50 border-t border-[var(--neutral-7)] bg-[var(--neutral-1)] shadow-lg"
|
|
688
|
+
>
|
|
689
|
+
<div className="mx-auto flex max-w-[1400px] flex-col gap-4 p-4 md:p-6">
|
|
690
|
+
<div>
|
|
691
|
+
<h2 id="cookie-banner-title" className="text-sm font-semibold text-[var(--neutral-12)]">
|
|
692
|
+
Cookie Preferences \xB7 Cookie \u504F\u597D\u8BBE\u7F6E
|
|
693
|
+
</h2>
|
|
694
|
+
<p className="mt-1 text-xs text-[var(--neutral-11)]">
|
|
695
|
+
We use cookies to provide essential functionality, analyze usage, and personalize content.
|
|
696
|
+
You can accept all, reject non-essential, or customize your choice. See our{" "}
|
|
697
|
+
<a href="/privacy" className="underline">
|
|
698
|
+
Privacy Policy
|
|
699
|
+
</a>
|
|
700
|
+
.
|
|
701
|
+
<br />
|
|
702
|
+
\u6211\u4EEC\u4F7F\u7528 Cookie \u63D0\u4F9B\u5FC5\u8981\u529F\u80FD\u3001\u5206\u6790\u4F7F\u7528\u60C5\u51B5\u5E76\u4E2A\u6027\u5316\u5185\u5BB9\u3002\u60A8\u53EF\u4EE5\u63A5\u53D7\u5168\u90E8\u3001\u62D2\u7EDD\u975E\u5FC5\u8981\uFF0C\u6216\u81EA\u5B9A\u4E49\u9009\u62E9\u3002\u8BE6\u89C1
|
|
703
|
+
<a href="/privacy" className="underline">
|
|
704
|
+
\u9690\u79C1\u653F\u7B56
|
|
705
|
+
</a>
|
|
706
|
+
\u3002
|
|
707
|
+
</p>
|
|
708
|
+
</div>
|
|
709
|
+
|
|
710
|
+
{showCustomize ? (
|
|
711
|
+
<fieldset className="grid gap-3 md:grid-cols-2">
|
|
712
|
+
<CategoryRow
|
|
713
|
+
id="necessary"
|
|
714
|
+
title="Necessary / \u5FC5\u8981"
|
|
715
|
+
description="Required for the site to function. Cannot be disabled. / \u7AD9\u70B9\u8FD0\u884C\u6240\u5FC5\u9700\uFF0C\u65E0\u6CD5\u5173\u95ED\u3002"
|
|
716
|
+
checked={true}
|
|
717
|
+
disabled
|
|
718
|
+
onChange={() => undefined}
|
|
719
|
+
/>
|
|
720
|
+
<CategoryRow
|
|
721
|
+
id="analytics"
|
|
722
|
+
title="Analytics / \u5206\u6790"
|
|
723
|
+
description="Helps us understand usage patterns. / \u5E2E\u52A9\u6211\u4EEC\u4E86\u89E3\u4F7F\u7528\u6A21\u5F0F\u3002"
|
|
724
|
+
checked={categories.analytics}
|
|
725
|
+
onChange={(v) => setCategories((c) => ({ ...c, analytics: v }))}
|
|
726
|
+
/>
|
|
727
|
+
<CategoryRow
|
|
728
|
+
id="marketing"
|
|
729
|
+
title="Marketing / \u8425\u9500"
|
|
730
|
+
description="Used for targeted advertising. / \u7528\u4E8E\u5B9A\u5411\u5E7F\u544A\u3002"
|
|
731
|
+
checked={categories.marketing}
|
|
732
|
+
onChange={(v) => setCategories((c) => ({ ...c, marketing: v }))}
|
|
733
|
+
/>
|
|
734
|
+
<CategoryRow
|
|
735
|
+
id="preferences"
|
|
736
|
+
title="Preferences / \u504F\u597D"
|
|
737
|
+
description="Remember your preferences (language, theme). / \u8BB0\u4F4F\u60A8\u7684\u504F\u597D\uFF08\u8BED\u8A00\u3001\u4E3B\u9898\uFF09\u3002"
|
|
738
|
+
checked={categories.preferences}
|
|
739
|
+
onChange={(v) => setCategories((c) => ({ ...c, preferences: v }))}
|
|
740
|
+
/>
|
|
741
|
+
</fieldset>
|
|
742
|
+
) : null}
|
|
743
|
+
|
|
744
|
+
<div className="flex flex-wrap gap-2 md:justify-end">
|
|
745
|
+
{showCustomize ? (
|
|
746
|
+
<button
|
|
747
|
+
type="button"
|
|
748
|
+
onClick={saveCustom}
|
|
749
|
+
className="rounded-md bg-[var(--blue-9)] px-4 py-2 text-sm font-medium text-white hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-[var(--blue-9)] focus:ring-offset-1"
|
|
750
|
+
>
|
|
751
|
+
Save Preferences / \u4FDD\u5B58\u504F\u597D
|
|
752
|
+
</button>
|
|
753
|
+
) : (
|
|
754
|
+
<button
|
|
755
|
+
type="button"
|
|
756
|
+
onClick={() => setShowCustomize(true)}
|
|
757
|
+
className="rounded-md border border-[var(--neutral-7)] px-4 py-2 text-sm font-medium text-[var(--neutral-12)] hover:bg-[var(--neutral-3)] focus:outline-none focus:ring-2 focus:ring-[var(--blue-9)] focus:ring-offset-1"
|
|
758
|
+
>
|
|
759
|
+
Customize / \u81EA\u5B9A\u4E49
|
|
760
|
+
</button>
|
|
761
|
+
)}
|
|
762
|
+
<button
|
|
763
|
+
type="button"
|
|
764
|
+
onClick={rejectNonEssential}
|
|
765
|
+
className="rounded-md border border-[var(--neutral-7)] px-4 py-2 text-sm font-medium text-[var(--neutral-12)] hover:bg-[var(--neutral-3)] focus:outline-none focus:ring-2 focus:ring-[var(--blue-9)] focus:ring-offset-1"
|
|
766
|
+
>
|
|
767
|
+
Reject Non-Essential / \u62D2\u7EDD\u975E\u5FC5\u8981
|
|
768
|
+
</button>
|
|
769
|
+
<button
|
|
770
|
+
type="button"
|
|
771
|
+
onClick={acceptAll}
|
|
772
|
+
className="rounded-md bg-[var(--blue-9)] px-4 py-2 text-sm font-medium text-white hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-[var(--blue-9)] focus:ring-offset-1"
|
|
773
|
+
>
|
|
774
|
+
Accept All / \u5168\u90E8\u63A5\u53D7
|
|
775
|
+
</button>
|
|
776
|
+
</div>
|
|
777
|
+
</div>
|
|
778
|
+
</div>
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
interface CategoryRowProps {
|
|
783
|
+
id: string;
|
|
784
|
+
title: string;
|
|
785
|
+
description: string;
|
|
786
|
+
checked: boolean;
|
|
787
|
+
disabled?: boolean;
|
|
788
|
+
onChange: (value: boolean) => void;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function CategoryRow({ id, title, description, checked, disabled, onChange }: CategoryRowProps) {
|
|
792
|
+
return (
|
|
793
|
+
<label
|
|
794
|
+
htmlFor={id}
|
|
795
|
+
className="flex items-start gap-3 rounded-md border border-[var(--neutral-7)] p-3"
|
|
796
|
+
>
|
|
797
|
+
<input
|
|
798
|
+
id={id}
|
|
799
|
+
type="checkbox"
|
|
800
|
+
checked={checked}
|
|
801
|
+
disabled={disabled}
|
|
802
|
+
onChange={(e) => onChange(e.target.checked)}
|
|
803
|
+
className="mt-0.5 h-4 w-4"
|
|
804
|
+
/>
|
|
805
|
+
<span className="flex flex-col">
|
|
806
|
+
<span className="text-sm font-medium text-[var(--neutral-12)]">{title}</span>
|
|
807
|
+
<span className="text-xs text-[var(--neutral-11)]">{description}</span>
|
|
808
|
+
</span>
|
|
809
|
+
</label>
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
`,cr=`interface AigcDisclaimerProps {
|
|
813
|
+
className?: string;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Inline disclaimer for AI-generated content (required by \u7F51\u4FE1\u529E\u300A\u751F\u6210\u5F0F\u4EBA\u5DE5\u667A\u80FD
|
|
818
|
+
* \u670D\u52A1\u7BA1\u7406\u6682\u884C\u529E\u6CD5\u300B2023/2024). Attach to any block/page that contains AI output.
|
|
819
|
+
*/
|
|
820
|
+
export function AigcDisclaimer({ className }: AigcDisclaimerProps) {
|
|
821
|
+
return (
|
|
822
|
+
<div className={className}>
|
|
823
|
+
<p className="text-xs text-[var(--neutral-11)]">
|
|
824
|
+
\u672C\u5185\u5BB9\u7531 AI \u751F\u6210 \xB7 AI-Generated Content \xB7{" "}
|
|
825
|
+
<a href="/aigc-disclosure" className="underline">
|
|
826
|
+
\u4E86\u89E3\u66F4\u591A / Learn more
|
|
827
|
+
</a>
|
|
828
|
+
</p>
|
|
829
|
+
</div>
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
`,pr=`---
|
|
833
|
+
title: Privacy Policy / \u9690\u79C1\u653F\u7B56
|
|
834
|
+
description: How {{COMPANY_NAME}} collects, uses, shares, and protects personal information.
|
|
835
|
+
lastUpdated: 2026-01-01
|
|
836
|
+
---
|
|
837
|
+
|
|
838
|
+
${Oe}
|
|
839
|
+
|
|
840
|
+
# Privacy Policy / \u9690\u79C1\u653F\u7B56
|
|
841
|
+
|
|
842
|
+
> **Effective Date:** 2026-01-01 \xB7 **Last Updated:** 2026-01-01
|
|
843
|
+
> **\u751F\u6548\u65E5\u671F\uFF1A** 2026-01-01 \xB7 **\u6700\u540E\u66F4\u65B0\uFF1A** 2026-01-01
|
|
844
|
+
|
|
845
|
+
This Privacy Policy describes how **{{COMPANY_NAME}}** ("we", "us", "our") collects, uses, shares,
|
|
846
|
+
and protects your personal information in connection with our services.
|
|
847
|
+
|
|
848
|
+
\u672C\u9690\u79C1\u653F\u7B56\u8BF4\u660E **{{COMPANY_NAME}}**\uFF08"\u6211\u4EEC"\uFF09\u5728\u63D0\u4F9B\u670D\u52A1\u8FC7\u7A0B\u4E2D\u5982\u4F55\u6536\u96C6\u3001\u4F7F\u7528\u3001\u5171\u4EAB\u548C\u4FDD\u62A4\u60A8\u7684\u4E2A\u4EBA\u4FE1\u606F\u3002
|
|
849
|
+
|
|
850
|
+
---
|
|
851
|
+
|
|
852
|
+
## 1. Data Collection Scope / \u6570\u636E\u6536\u96C6\u8303\u56F4
|
|
853
|
+
|
|
854
|
+
**EN:** We collect the following categories of personal information:
|
|
855
|
+
- **Account data:** name, email address, phone number, password (hashed).
|
|
856
|
+
- **Usage data:** IP address, browser, device, operating system, page views, clickstream, timestamps.
|
|
857
|
+
- **Billing data:** payment method tokens (handled by our payment processor), billing address.
|
|
858
|
+
- **Content data:** files, messages, and other content you upload or create.
|
|
859
|
+
- **Cookies & similar technologies:** see our Cookie Preferences banner for details.
|
|
860
|
+
|
|
861
|
+
**ZH:** \u6211\u4EEC\u6536\u96C6\u4EE5\u4E0B\u7C7B\u522B\u7684\u4E2A\u4EBA\u4FE1\u606F\uFF1A
|
|
862
|
+
- **\u8D26\u6237\u4FE1\u606F\uFF1A** \u59D3\u540D\u3001\u90AE\u7BB1\u3001\u624B\u673A\u53F7\u3001\u5BC6\u7801\uFF08\u52A0\u5BC6\u5B58\u50A8\uFF09\u3002
|
|
863
|
+
- **\u4F7F\u7528\u4FE1\u606F\uFF1A** IP \u5730\u5740\u3001\u6D4F\u89C8\u5668\u3001\u8BBE\u5907\u3001\u64CD\u4F5C\u7CFB\u7EDF\u3001\u9875\u9762\u6D4F\u89C8\u3001\u70B9\u51FB\u6D41\u3001\u65F6\u95F4\u6233\u3002
|
|
864
|
+
- **\u652F\u4ED8\u4FE1\u606F\uFF1A** \u652F\u4ED8\u4EE4\u724C\uFF08\u7531\u7B2C\u4E09\u65B9\u652F\u4ED8\u670D\u52A1\u5546\u5904\u7406\uFF09\u3001\u8D26\u5355\u5730\u5740\u3002
|
|
865
|
+
- **\u5185\u5BB9\u4FE1\u606F\uFF1A** \u60A8\u4E0A\u4F20\u6216\u521B\u5EFA\u7684\u6587\u4EF6\u3001\u6D88\u606F\u7B49\u5185\u5BB9\u3002
|
|
866
|
+
- **Cookie \u53CA\u7C7B\u4F3C\u6280\u672F\uFF1A** \u8BE6\u89C1 Cookie \u504F\u597D\u8BBE\u7F6E\u3002
|
|
867
|
+
|
|
868
|
+
---
|
|
869
|
+
|
|
870
|
+
## 2. Purpose / \u4F7F\u7528\u76EE\u7684
|
|
871
|
+
|
|
872
|
+
**EN:** We use personal information to (a) provide and maintain the service; (b) authenticate users;
|
|
873
|
+
(c) process payments; (d) detect and prevent fraud/abuse; (e) improve product quality; (f) comply with
|
|
874
|
+
legal obligations; (g) communicate about the service (with consent where required).
|
|
875
|
+
|
|
876
|
+
**ZH:** \u6211\u4EEC\u5C06\u4E2A\u4EBA\u4FE1\u606F\u7528\u4E8E\uFF1A(a) \u63D0\u4F9B\u548C\u7EF4\u62A4\u670D\u52A1\uFF1B(b) \u7528\u6237\u8EAB\u4EFD\u9A8C\u8BC1\uFF1B(c) \u5904\u7406\u652F\u4ED8\uFF1B(d) \u68C0\u6D4B\u548C\u9632\u8303
|
|
877
|
+
\u6B3A\u8BC8/\u6EE5\u7528\uFF1B(e) \u6539\u8FDB\u4EA7\u54C1\u8D28\u91CF\uFF1B(f) \u5C65\u884C\u6CD5\u5F8B\u4E49\u52A1\uFF1B(g) \u5C31\u670D\u52A1\u4E0E\u60A8\u6C9F\u901A\uFF08\u9700\u8981\u65F6\u53D6\u5F97\u60A8\u7684\u540C\u610F\uFF09\u3002
|
|
878
|
+
|
|
879
|
+
---
|
|
880
|
+
|
|
881
|
+
## 3. Sharing & Cross-Border Transfer / \u6570\u636E\u5171\u4EAB\u53CA\u8DE8\u5883\u4F20\u8F93
|
|
882
|
+
|
|
883
|
+
**EN:** We share personal information only with:
|
|
884
|
+
- **Processors** (cloud hosting, analytics, email, payment) bound by data processing agreements.
|
|
885
|
+
- **Legal authorities** when required by law.
|
|
886
|
+
- **Business transferees** in the event of merger, acquisition, or asset sale.
|
|
887
|
+
|
|
888
|
+
Some processors operate **outside your country**. For users in China, see our separate
|
|
889
|
+
[Cross-Border Data Notice](/cross-border-data). For users in the EEA/UK, transfers rely on
|
|
890
|
+
Standard Contractual Clauses or equivalent safeguards.
|
|
891
|
+
|
|
892
|
+
**ZH:** \u6211\u4EEC\u4EC5\u4E0E\u4EE5\u4E0B\u4E3B\u4F53\u5171\u4EAB\u4E2A\u4EBA\u4FE1\u606F\uFF1A
|
|
893
|
+
- **\u5904\u7406\u8005**\uFF08\u4E91\u670D\u52A1\u3001\u5206\u6790\u3001\u90AE\u4EF6\u3001\u652F\u4ED8\uFF09\u2014\u2014 \u53D7\u6570\u636E\u5904\u7406\u534F\u8BAE\u7EA6\u675F\u3002
|
|
894
|
+
- **\u53F8\u6CD5/\u76D1\u7BA1\u673A\u5173** \u2014\u2014 \u4F9D\u6CD5\u5F8B\u8981\u6C42\u3002
|
|
895
|
+
- **\u4E1A\u52A1\u7EE7\u53D7\u8005** \u2014\u2014 \u53D1\u751F\u5408\u5E76\u3001\u6536\u8D2D\u6216\u8D44\u4EA7\u51FA\u552E\u65F6\u3002
|
|
896
|
+
|
|
897
|
+
\u90E8\u5206\u5904\u7406\u8005\u4F4D\u4E8E**\u5883\u5916**\u3002\u4E2D\u56FD\u5883\u5185\u7528\u6237\u8BF7\u53C2\u9605\u300A[\u6570\u636E\u51FA\u5883\u544A\u77E5](/cross-border-data)\u300B\u3002
|
|
898
|
+
\u6B27\u76DF/\u82F1\u56FD\u7528\u6237\u7684\u8DE8\u5883\u4F20\u8F93\u4F9D\u636E\u300A\u6807\u51C6\u5408\u540C\u6761\u6B3E\u300B(SCCs) \u6216\u7B49\u6548\u4FDD\u62A4\u63AA\u65BD\u3002
|
|
899
|
+
|
|
900
|
+
---
|
|
901
|
+
|
|
902
|
+
## 4. Your Rights / \u7528\u6237\u6743\u5229
|
|
903
|
+
|
|
904
|
+
**EN:** Depending on your jurisdiction (GDPR/CCPA/PIPL/\u7B49), you may have the right to:
|
|
905
|
+
- **Access** your personal information.
|
|
906
|
+
- **Rectify** inaccurate data.
|
|
907
|
+
- **Delete** your data ("right to be forgotten").
|
|
908
|
+
- **Export** your data in a portable format.
|
|
909
|
+
- **Restrict or object** to certain processing.
|
|
910
|
+
- **Withdraw consent** at any time without affecting the lawfulness of prior processing.
|
|
911
|
+
- **Lodge a complaint** with your local supervisory authority.
|
|
912
|
+
|
|
913
|
+
To exercise these rights, contact **{{COMPANY_CONTACT_EMAIL}}**.
|
|
914
|
+
|
|
915
|
+
**ZH:** \u6839\u636E\u60A8\u6240\u5728\u53F8\u6CD5\u8F96\u533A\uFF08\u300A\u4E2A\u4EBA\u4FE1\u606F\u4FDD\u62A4\u6CD5\u300B/GDPR/CCPA \u7B49\uFF09\uFF0C\u60A8\u53EF\u80FD\u4EAB\u6709\u4EE5\u4E0B\u6743\u5229\uFF1A
|
|
916
|
+
- **\u8BBF\u95EE**\u60A8\u7684\u4E2A\u4EBA\u4FE1\u606F\u3002
|
|
917
|
+
- **\u66F4\u6B63**\u4E0D\u51C6\u786E\u4FE1\u606F\u3002
|
|
918
|
+
- **\u5220\u9664**\u60A8\u7684\u6570\u636E\uFF08\u88AB\u9057\u5FD8\u6743\uFF09\u3002
|
|
919
|
+
- **\u5BFC\u51FA**\u60A8\u7684\u6570\u636E\uFF08\u53EF\u643A\u5E26\u683C\u5F0F\uFF09\u3002
|
|
920
|
+
- **\u9650\u5236\u6216\u53CD\u5BF9**\u7279\u5B9A\u5904\u7406\u3002
|
|
921
|
+
- **\u968F\u65F6\u64A4\u56DE\u540C\u610F**\uFF08\u4E0D\u5F71\u54CD\u64A4\u56DE\u524D\u7684\u5904\u7406\u5408\u6CD5\u6027\uFF09\u3002
|
|
922
|
+
- \u5411\u5F53\u5730\u76D1\u7BA1\u673A\u6784**\u6295\u8BC9**\u3002
|
|
923
|
+
|
|
924
|
+
\u5982\u9700\u884C\u4F7F\u4E0A\u8FF0\u6743\u5229\uFF0C\u8BF7\u8054\u7CFB **{{COMPANY_CONTACT_EMAIL}}**\u3002
|
|
925
|
+
|
|
926
|
+
---
|
|
927
|
+
|
|
928
|
+
## 5. Cookies / Cookie \u4F7F\u7528
|
|
929
|
+
|
|
930
|
+
See our Cookie Preferences banner (footer \u2192 Cookie Settings). We categorize cookies as:
|
|
931
|
+
Necessary (cannot be disabled), Analytics, Marketing, Preferences.
|
|
932
|
+
|
|
933
|
+
\u8BF7\u53C2\u9605 Cookie \u504F\u597D\u8BBE\u7F6E\uFF08\u9875\u811A \u2192 Cookie Settings\uFF09\u3002Cookie \u7C7B\u522B\u5305\u62EC\uFF1A\u5FC5\u8981\uFF08\u4E0D\u53EF\u5173\u95ED\uFF09\u3001
|
|
934
|
+
\u5206\u6790\u3001\u8425\u9500\u3001\u504F\u597D\u3002
|
|
935
|
+
|
|
936
|
+
---
|
|
937
|
+
|
|
938
|
+
## 6. Data Security / \u6570\u636E\u5B89\u5168\u63AA\u65BD
|
|
939
|
+
|
|
940
|
+
**EN:** Administrative, technical, and physical safeguards include TLS 1.2+ in transit, AES-256 at rest
|
|
941
|
+
(envelope encryption for secrets via \`@nebutra/vault\`), role-based access control, audit logging,
|
|
942
|
+
periodic vulnerability scans, and least-privilege principles. No system is 100% secure; we notify
|
|
943
|
+
affected users and regulators of material breaches within statutory timelines.
|
|
944
|
+
|
|
945
|
+
**ZH:** \u6211\u4EEC\u91C7\u53D6\u7BA1\u7406\u3001\u6280\u672F\u548C\u7269\u7406\u5C42\u9762\u7684\u4FDD\u969C\u63AA\u65BD\uFF0C\u5305\u62EC\u4F20\u8F93\u4E2D TLS 1.2+\u3001\u9759\u6001 AES-256 \u52A0\u5BC6
|
|
946
|
+
\uFF08\u5BC6\u94A5\u901A\u8FC7 \`@nebutra/vault\` \u4FE1\u5C01\u52A0\u5BC6\u7BA1\u7406\uFF09\u3001\u57FA\u4E8E\u89D2\u8272\u7684\u8BBF\u95EE\u63A7\u5236\u3001\u5BA1\u8BA1\u65E5\u5FD7\u3001\u5B9A\u671F\u6F0F\u6D1E\u626B\u63CF
|
|
947
|
+
\u548C\u6700\u5C0F\u6743\u9650\u539F\u5219\u3002\u4EFB\u4F55\u7CFB\u7EDF\u90FD\u65E0\u6CD5\u505A\u5230\u7EDD\u5BF9\u5B89\u5168\uFF1B\u53D1\u751F\u91CD\u5927\u6570\u636E\u6CC4\u9732\u65F6\uFF0C\u6211\u4EEC\u5C06\u5728\u6CD5\u5B9A\u671F\u9650\u5185
|
|
948
|
+
\u901A\u77E5\u53D7\u5F71\u54CD\u7528\u6237\u548C\u76D1\u7BA1\u673A\u6784\u3002
|
|
949
|
+
|
|
950
|
+
---
|
|
951
|
+
|
|
952
|
+
## 7. Retention / \u6570\u636E\u4FDD\u7559
|
|
953
|
+
|
|
954
|
+
We retain personal information only as long as necessary to fulfill the purposes described, or as
|
|
955
|
+
required by law. Inactive accounts are deleted after 24 months unless retention is legally required.
|
|
956
|
+
|
|
957
|
+
\u6211\u4EEC\u4EC5\u5728\u5B9E\u73B0\u4E0A\u8FF0\u76EE\u7684\u6240\u9700\u7684\u671F\u95F4\u6216\u6CD5\u5F8B\u8981\u6C42\u7684\u671F\u95F4\u5185\u4FDD\u7559\u4E2A\u4EBA\u4FE1\u606F\u3002\u975E\u6D3B\u8DC3\u8D26\u6237\u5C06\u5728 24 \u4E2A\u6708\u540E
|
|
958
|
+
\u5220\u9664\uFF0C\u6CD5\u5F8B\u53E6\u6709\u8981\u6C42\u7684\u9664\u5916\u3002
|
|
959
|
+
|
|
960
|
+
---
|
|
961
|
+
|
|
962
|
+
## 8. Changes / \u53D8\u66F4\u901A\u77E5
|
|
963
|
+
|
|
964
|
+
We will notify you of material changes by posting the updated policy and, where required, by email
|
|
965
|
+
or in-app notification before the change takes effect.
|
|
966
|
+
|
|
967
|
+
\u6211\u4EEC\u5C06\u901A\u8FC7\u53D1\u5E03\u66F4\u65B0\u540E\u7684\u653F\u7B56\u901A\u77E5\u60A8\u91CD\u5927\u53D8\u66F4\uFF1B\u5982\u6CD5\u5F8B\u8981\u6C42\uFF0C\u8FD8\u5C06\u5728\u53D8\u66F4\u751F\u6548\u524D\u901A\u8FC7\u90AE\u4EF6\u6216\u5E94\u7528\u5185
|
|
968
|
+
\u901A\u77E5\u7684\u65B9\u5F0F\u544A\u77E5\u3002
|
|
969
|
+
|
|
970
|
+
---
|
|
971
|
+
|
|
972
|
+
## 9. Contact / \u8054\u7CFB\u65B9\u5F0F
|
|
973
|
+
|
|
974
|
+
**Data Controller / \u6570\u636E\u63A7\u5236\u8005:** {{COMPANY_NAME}}
|
|
975
|
+
**Address / \u5730\u5740:** {{COMPANY_ADDRESS}}
|
|
976
|
+
**Email / \u90AE\u7BB1:** {{COMPANY_CONTACT_EMAIL}}
|
|
977
|
+
`,lr=`---
|
|
978
|
+
title: Terms of Service / \u670D\u52A1\u6761\u6B3E
|
|
979
|
+
description: Terms governing your use of {{COMPANY_NAME}}.
|
|
980
|
+
lastUpdated: 2026-01-01
|
|
981
|
+
---
|
|
982
|
+
|
|
983
|
+
${Oe}
|
|
984
|
+
|
|
985
|
+
# Terms of Service / \u670D\u52A1\u6761\u6B3E
|
|
986
|
+
|
|
987
|
+
> **Effective Date:** 2026-01-01
|
|
988
|
+
> **\u751F\u6548\u65E5\u671F\uFF1A** 2026-01-01
|
|
989
|
+
|
|
990
|
+
## 1. Acceptance / \u63A5\u53D7\u6761\u6B3E
|
|
991
|
+
|
|
992
|
+
**EN:** By accessing or using the services provided by {{COMPANY_NAME}} ("Service"), you agree to be
|
|
993
|
+
bound by these Terms. If you do not agree, do not use the Service.
|
|
994
|
+
|
|
995
|
+
**ZH:** \u60A8\u8BBF\u95EE\u6216\u4F7F\u7528 {{COMPANY_NAME}} \u63D0\u4F9B\u7684\u670D\u52A1\uFF08"\u670D\u52A1"\uFF09\u5373\u8868\u793A\u540C\u610F\u53D7\u672C\u6761\u6B3E\u7EA6\u675F\u3002
|
|
996
|
+
\u5982\u4E0D\u540C\u610F\uFF0C\u8BF7\u52FF\u4F7F\u7528\u672C\u670D\u52A1\u3002
|
|
997
|
+
|
|
998
|
+
---
|
|
999
|
+
|
|
1000
|
+
## 2. Eligibility / \u7528\u6237\u8D44\u683C
|
|
1001
|
+
|
|
1002
|
+
You must be at least 13 years old (or the minimum age in your jurisdiction, whichever is higher) and
|
|
1003
|
+
have the legal capacity to enter into a contract.
|
|
1004
|
+
|
|
1005
|
+
\u7528\u6237\u987B\u5E74\u6EE1 13 \u5468\u5C81\uFF08\u6216\u5176\u6240\u5728\u53F8\u6CD5\u8F96\u533A\u89C4\u5B9A\u7684\u6700\u4F4E\u5E74\u9F84\uFF0C\u53D6\u8F83\u9AD8\u8005\uFF09\u5E76\u5177\u5907\u7B7E\u8BA2\u5408\u540C\u7684\u6CD5\u5F8B\u884C\u4E3A\u80FD\u529B\u3002
|
|
1006
|
+
|
|
1007
|
+
---
|
|
1008
|
+
|
|
1009
|
+
## 3. Accounts / \u8D26\u6237
|
|
1010
|
+
|
|
1011
|
+
You are responsible for maintaining the confidentiality of your credentials and for all activity
|
|
1012
|
+
under your account. Notify us immediately of any unauthorized use at {{COMPANY_CONTACT_EMAIL}}.
|
|
1013
|
+
|
|
1014
|
+
\u60A8\u8D1F\u8D23\u4FDD\u7BA1\u8D26\u6237\u51ED\u636E\u7684\u673A\u5BC6\u6027\uFF0C\u5E76\u5BF9\u8D26\u6237\u4E0B\u7684\u4E00\u5207\u6D3B\u52A8\u8D1F\u8D23\u3002\u53D1\u73B0\u672A\u7ECF\u6388\u6743\u7684\u4F7F\u7528\uFF0C\u8BF7\u7ACB\u5373\u901A\u8FC7
|
|
1015
|
+
{{COMPANY_CONTACT_EMAIL}} \u901A\u77E5\u6211\u4EEC\u3002
|
|
1016
|
+
|
|
1017
|
+
---
|
|
1018
|
+
|
|
1019
|
+
## 4. Acceptable Use / \u5408\u89C4\u4F7F\u7528
|
|
1020
|
+
|
|
1021
|
+
You agree not to: (a) violate any law; (b) infringe intellectual property rights; (c) transmit
|
|
1022
|
+
malware; (d) reverse engineer the Service except as permitted by law; (e) use the Service for
|
|
1023
|
+
activities prohibited in your jurisdiction (including content review regulations in the PRC).
|
|
1024
|
+
|
|
1025
|
+
\u60A8\u540C\u610F\u4E0D\u5F97\uFF1A(a) \u8FDD\u53CD\u4EFB\u4F55\u6CD5\u5F8B\uFF1B(b) \u4FB5\u72AF\u77E5\u8BC6\u4EA7\u6743\uFF1B(c) \u4F20\u64AD\u6076\u610F\u8F6F\u4EF6\uFF1B(d) \u8D85\u51FA\u6CD5\u5F8B\u5141\u8BB8\u8303\u56F4\u5BF9
|
|
1026
|
+
\u670D\u52A1\u8FDB\u884C\u9006\u5411\u5DE5\u7A0B\uFF1B(e) \u5C06\u670D\u52A1\u7528\u4E8E\u60A8\u6240\u5728\u53F8\u6CD5\u8F96\u533A\uFF08\u5305\u62EC\u4E2D\u56FD\u5883\u5185\u5185\u5BB9\u5BA1\u6838\u6CD5\u89C4\uFF09\u7981\u6B62\u7684\u6D3B\u52A8\u3002
|
|
1027
|
+
|
|
1028
|
+
---
|
|
1029
|
+
|
|
1030
|
+
## 5. Subscription & Billing / \u8BA2\u9605\u4E0E\u8BA1\u8D39
|
|
1031
|
+
|
|
1032
|
+
Paid plans are billed in advance on a recurring basis. You authorize us to charge your payment
|
|
1033
|
+
method until you cancel. Refunds are handled in accordance with applicable law and our refund policy.
|
|
1034
|
+
|
|
1035
|
+
\u4ED8\u8D39\u8BA2\u9605\u6309\u5468\u671F\u9884\u5148\u8BA1\u8D39\u3002\u60A8\u6388\u6743\u6211\u4EEC\u5411\u60A8\u7684\u652F\u4ED8\u65B9\u5F0F\u6301\u7EED\u6263\u6B3E\uFF0C\u76F4\u81F3\u60A8\u53D6\u6D88\u8BA2\u9605\u3002\u9000\u6B3E\u4F9D\u9002\u7528\u6CD5\u5F8B
|
|
1036
|
+
\u53CA\u672C\u670D\u52A1\u9000\u6B3E\u653F\u7B56\u5904\u7406\u3002
|
|
1037
|
+
|
|
1038
|
+
---
|
|
1039
|
+
|
|
1040
|
+
## 6. Intellectual Property / \u77E5\u8BC6\u4EA7\u6743
|
|
1041
|
+
|
|
1042
|
+
**EN:** The Service, including all software, design, and content provided by us, is owned by
|
|
1043
|
+
{{COMPANY_NAME}} and protected by copyright, trademark, and other laws. You retain ownership of
|
|
1044
|
+
content you upload; you grant us a worldwide, non-exclusive, royalty-free license to host, process,
|
|
1045
|
+
and display your content solely to provide the Service.
|
|
1046
|
+
|
|
1047
|
+
**ZH:** \u670D\u52A1\uFF08\u5305\u62EC\u6211\u4EEC\u63D0\u4F9B\u7684\u6240\u6709\u8F6F\u4EF6\u3001\u8BBE\u8BA1\u548C\u5185\u5BB9\uFF09\u5F52 {{COMPANY_NAME}} \u6240\u6709\uFF0C\u53D7\u8457\u4F5C\u6743\u3001
|
|
1048
|
+
\u5546\u6807\u53CA\u5176\u4ED6\u6CD5\u5F8B\u4FDD\u62A4\u3002\u60A8\u4FDD\u7559\u5BF9\u60A8\u4E0A\u4F20\u5185\u5BB9\u7684\u6240\u6709\u6743\uFF1B\u60A8\u6388\u4E88\u6211\u4EEC\u5168\u7403\u8303\u56F4\u5185\u3001\u975E\u72EC\u5360\u3001\u514D\u7248\u7A0E\u7684
|
|
1049
|
+
\u8BB8\u53EF\uFF0C\u4EC5\u7528\u4E8E\u6258\u7BA1\u3001\u5904\u7406\u548C\u5C55\u793A\u60A8\u7684\u5185\u5BB9\u4EE5\u63D0\u4F9B\u670D\u52A1\u3002
|
|
1050
|
+
|
|
1051
|
+
---
|
|
1052
|
+
|
|
1053
|
+
## 7. AI-Generated Content / \u751F\u6210\u5F0F AI \u5185\u5BB9
|
|
1054
|
+
|
|
1055
|
+
Outputs generated by AI models are provided "as-is". You are responsible for verifying accuracy
|
|
1056
|
+
and ensuring compliance with applicable laws (including Chinese AIGC regulations) before relying
|
|
1057
|
+
on or publishing such outputs. See the [AIGC Disclosure](/aigc-disclosure) for details.
|
|
1058
|
+
|
|
1059
|
+
AI \u6A21\u578B\u751F\u6210\u7684\u8F93\u51FA\u6309"\u73B0\u72B6"\u63D0\u4F9B\u3002\u60A8\u5E94\u8D1F\u8D23\u6838\u5B9E\u5176\u51C6\u786E\u6027\uFF0C\u5E76\u5728\u4F9D\u8D56\u6216\u53D1\u5E03\u524D\u786E\u4FDD\u7B26\u5408\u9002\u7528\u6CD5\u5F8B
|
|
1060
|
+
\uFF08\u5305\u62EC\u4E2D\u56FD AIGC \u65B0\u89C4\uFF09\u3002\u8BE6\u89C1\u300A[AIGC \u8BF4\u660E](/aigc-disclosure)\u300B\u3002
|
|
1061
|
+
|
|
1062
|
+
---
|
|
1063
|
+
|
|
1064
|
+
## 8. Disclaimer & Limitation of Liability / \u514D\u8D23\u58F0\u660E\u53CA\u8D23\u4EFB\u9650\u5236
|
|
1065
|
+
|
|
1066
|
+
**EN:** TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE SERVICE IS PROVIDED "AS IS" WITHOUT WARRANTIES.
|
|
1067
|
+
IN NO EVENT SHALL {{COMPANY_NAME}} BE LIABLE FOR INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES.
|
|
1068
|
+
|
|
1069
|
+
**ZH:** \u5728\u6CD5\u5F8B\u5141\u8BB8\u7684\u6700\u5927\u8303\u56F4\u5185\uFF0C\u670D\u52A1\u6309"\u73B0\u72B6"\u63D0\u4F9B\uFF0C\u4E0D\u4F5C\u4EFB\u4F55\u660E\u793A\u6216\u9ED8\u793A\u4FDD\u8BC1\u3002{{COMPANY_NAME}}
|
|
1070
|
+
\u6982\u4E0D\u5BF9\u95F4\u63A5\u3001\u5076\u7136\u6216\u540E\u679C\u6027\u635F\u5931\u627F\u62C5\u8D23\u4EFB\u3002
|
|
1071
|
+
|
|
1072
|
+
---
|
|
1073
|
+
|
|
1074
|
+
## 9. Termination / \u7EC8\u6B62
|
|
1075
|
+
|
|
1076
|
+
We may suspend or terminate your account for material breach. You may cancel at any time from
|
|
1077
|
+
your account settings.
|
|
1078
|
+
|
|
1079
|
+
\u56E0\u91CD\u5927\u8FDD\u7EA6\uFF0C\u6211\u4EEC\u53EF\u6682\u505C\u6216\u7EC8\u6B62\u60A8\u7684\u8D26\u6237\u3002\u60A8\u53EF\u968F\u65F6\u5728\u8D26\u6237\u8BBE\u7F6E\u4E2D\u53D6\u6D88\u8BA2\u9605\u3002
|
|
1080
|
+
|
|
1081
|
+
---
|
|
1082
|
+
|
|
1083
|
+
## 10. Governing Law / \u9002\u7528\u6CD5\u5F8B
|
|
1084
|
+
|
|
1085
|
+
These Terms are governed by the laws of **{{GOVERNING_JURISDICTION}}**, without regard to
|
|
1086
|
+
conflict-of-laws principles. Disputes shall be resolved in the competent courts of that jurisdiction.
|
|
1087
|
+
|
|
1088
|
+
\u672C\u6761\u6B3E\u53D7 **{{GOVERNING_JURISDICTION}}** \u6CD5\u5F8B\u7BA1\u8F96\uFF0C\u4E0D\u8003\u8651\u51B2\u7A81\u6CD5\u539F\u5219\u3002\u4E89\u8BAE\u5E94\u63D0\u4EA4\u81F3\u8BE5
|
|
1089
|
+
\u53F8\u6CD5\u8F96\u533A\u7684\u6709\u7BA1\u8F96\u6743\u6CD5\u9662\u89E3\u51B3\u3002
|
|
1090
|
+
|
|
1091
|
+
---
|
|
1092
|
+
|
|
1093
|
+
## 11. Contact / \u8054\u7CFB\u65B9\u5F0F
|
|
1094
|
+
|
|
1095
|
+
{{COMPANY_NAME}} \xB7 {{COMPANY_ADDRESS}} \xB7 {{COMPANY_CONTACT_EMAIL}}
|
|
1096
|
+
`,dr=`---
|
|
1097
|
+
title: AIGC Disclosure / \u751F\u6210\u5F0F AI \u670D\u52A1\u8BF4\u660E
|
|
1098
|
+
description: Disclosure required by \u7F51\u4FE1\u529E\u300A\u751F\u6210\u5F0F\u4EBA\u5DE5\u667A\u80FD\u670D\u52A1\u7BA1\u7406\u6682\u884C\u529E\u6CD5\u300B
|
|
1099
|
+
lastUpdated: 2026-01-01
|
|
1100
|
+
---
|
|
1101
|
+
|
|
1102
|
+
${Oe}
|
|
1103
|
+
|
|
1104
|
+
# AIGC Disclosure / \u751F\u6210\u5F0F AI \u670D\u52A1\u8BF4\u660E
|
|
1105
|
+
|
|
1106
|
+
This disclosure is provided in accordance with the **Interim Measures for the Management of
|
|
1107
|
+
Generative AI Services** (\u751F\u6210\u5F0F\u4EBA\u5DE5\u667A\u80FD\u670D\u52A1\u7BA1\u7406\u6682\u884C\u529E\u6CD5) promulgated by the Cyberspace
|
|
1108
|
+
Administration of China.
|
|
1109
|
+
|
|
1110
|
+
\u672C\u8BF4\u660E\u4F9D\u636E\u56FD\u5BB6\u4E92\u8054\u7F51\u4FE1\u606F\u529E\u516C\u5BA4\u300A\u751F\u6210\u5F0F\u4EBA\u5DE5\u667A\u80FD\u670D\u52A1\u7BA1\u7406\u6682\u884C\u529E\u6CD5\u300B\u7684\u8981\u6C42\u63D0\u4F9B\u3002
|
|
1111
|
+
|
|
1112
|
+
---
|
|
1113
|
+
|
|
1114
|
+
## 1. AI Models Used / \u4F7F\u7528\u7684 AI \u6A21\u578B
|
|
1115
|
+
|
|
1116
|
+
> TODO (fill in before launch / \u4E0A\u7EBF\u524D\u586B\u5199):
|
|
1117
|
+
|
|
1118
|
+
| Model / \u6A21\u578B | Provider / \u63D0\u4F9B\u65B9 | Use Case / \u7528\u9014 |
|
|
1119
|
+
| --- | --- | --- |
|
|
1120
|
+
| _e.g. GPT-4o_ | _OpenAI_ | _Chat, content drafting_ |
|
|
1121
|
+
| _e.g. Claude 3.5 Sonnet_ | _Anthropic_ | _Reasoning, code generation_ |
|
|
1122
|
+
| _e.g. \u901A\u4E49\u5343\u95EE_ | _\u963F\u91CC\u4E91_ | _\u4E2D\u6587\u5185\u5BB9\u751F\u6210_ |
|
|
1123
|
+
|
|
1124
|
+
---
|
|
1125
|
+
|
|
1126
|
+
## 2. Algorithm Filing / \u7B97\u6CD5\u5907\u6848\u53F7
|
|
1127
|
+
|
|
1128
|
+
**Filing Number / \u5907\u6848\u53F7:** \`{{AIGC_ALGORITHM_FILING_NUMBER}}\`
|
|
1129
|
+
(Apply at <https://beian.cac.gov.cn/> \xB7 \u4E8E\u4E2D\u56FD\u7F51\u4FE1\u529E\u5B8C\u6210\u7B97\u6CD5\u5907\u6848)
|
|
1130
|
+
|
|
1131
|
+
---
|
|
1132
|
+
|
|
1133
|
+
## 3. AI-Generated Content Labeling / \u751F\u6210\u5185\u5BB9\u6807\u8BC6\u673A\u5236
|
|
1134
|
+
|
|
1135
|
+
**EN:** All content generated by AI models in this product is labeled with:
|
|
1136
|
+
- An inline disclaimer: "\u672C\u5185\u5BB9\u7531 AI \u751F\u6210 \xB7 AI-Generated Content".
|
|
1137
|
+
- Machine-readable metadata (where the output format supports it): \`X-AI-Generated: true\`.
|
|
1138
|
+
- For images/videos: visible watermark and/or embedded metadata per GB/T 45438-2025.
|
|
1139
|
+
|
|
1140
|
+
**ZH:** \u672C\u4EA7\u54C1\u4E2D\u7531 AI \u6A21\u578B\u751F\u6210\u7684\u6240\u6709\u5185\u5BB9\u5747\u5E26\u6709\uFF1A
|
|
1141
|
+
- \u663E\u5F0F\u6587\u5B57\u6807\u8BC6\uFF1A"\u672C\u5185\u5BB9\u7531 AI \u751F\u6210 \xB7 AI-Generated Content"\u3002
|
|
1142
|
+
- \u673A\u5668\u53EF\u8BFB\u5143\u6570\u636E\uFF08\u89C6\u8F93\u51FA\u683C\u5F0F\u652F\u6301\uFF09\uFF1A\`X-AI-Generated: true\`\u3002
|
|
1143
|
+
- \u56FE\u7247/\u89C6\u9891\uFF1A\u53EF\u89C1\u6C34\u5370\u548C/\u6216\u4F9D GB/T 45438-2025 \u5D4C\u5165\u5143\u6570\u636E\u3002
|
|
1144
|
+
|
|
1145
|
+
---
|
|
1146
|
+
|
|
1147
|
+
## 4. Training Data / \u8BAD\u7EC3\u6570\u636E\u8BF4\u660E
|
|
1148
|
+
|
|
1149
|
+
We do **not** train foundation models in-house. We use third-party model APIs whose training data
|
|
1150
|
+
practices are described in their respective privacy notices. We do not use customer data to train
|
|
1151
|
+
third-party models by default (opt-out where offered).
|
|
1152
|
+
|
|
1153
|
+
\u6211\u4EEC**\u4E0D**\u81EA\u884C\u8BAD\u7EC3\u57FA\u7840\u6A21\u578B\u3002\u6211\u4EEC\u4F7F\u7528\u7B2C\u4E09\u65B9\u6A21\u578B API\uFF0C\u5176\u8BAD\u7EC3\u6570\u636E\u5B9E\u8DF5\u8BE6\u89C1\u5404\u81EA\u7684\u9690\u79C1\u8BF4\u660E\u3002
|
|
1154
|
+
\u9ED8\u8BA4\u60C5\u51B5\u4E0B\uFF0C\u6211\u4EEC\u4E0D\u4F1A\u4F7F\u7528\u5BA2\u6237\u6570\u636E\u8BAD\u7EC3\u7B2C\u4E09\u65B9\u6A21\u578B\uFF08\u5728\u53EF\u9009\u9000\u51FA\u7684\u60C5\u51B5\u4E0B\u5DF2\u9009\u62E9\u9000\u51FA\uFF09\u3002
|
|
1155
|
+
|
|
1156
|
+
---
|
|
1157
|
+
|
|
1158
|
+
## 5. Reporting Channel / \u4E3E\u62A5\u6E20\u9053
|
|
1159
|
+
|
|
1160
|
+
If you encounter illegal content, hallucinated output causing harm, or suspected misuse, report to:
|
|
1161
|
+
|
|
1162
|
+
\u5982\u60A8\u53D1\u73B0\u8FDD\u6CD5\u5185\u5BB9\u3001\u5BFC\u81F4\u635F\u5BB3\u7684\u5E7B\u89C9\u8F93\u51FA\u6216\u7591\u4F3C\u6EE5\u7528\uFF0C\u8BF7\u901A\u8FC7\u4EE5\u4E0B\u6E20\u9053\u4E3E\u62A5\uFF1A
|
|
1163
|
+
|
|
1164
|
+
- **Email / \u90AE\u7BB1:** {{COMPANY_CONTACT_EMAIL}}
|
|
1165
|
+
- **In-app:** Settings \u2192 Feedback \u2192 Report AI Content / \u5E94\u7528\u5185\uFF1A\u8BBE\u7F6E \u2192 \u53CD\u9988 \u2192 \u4E3E\u62A5 AI \u5185\u5BB9
|
|
1166
|
+
- **Regulator / \u76D1\u7BA1\u90E8\u95E8:** [\u56FD\u5BB6\u4E92\u8054\u7F51\u4FE1\u606F\u529E\u516C\u5BA4\u4E3E\u62A5\u4E2D\u5FC3](https://www.12377.cn/)
|
|
1167
|
+
|
|
1168
|
+
---
|
|
1169
|
+
|
|
1170
|
+
## 6. Limitations / \u5C40\u9650\u6027
|
|
1171
|
+
|
|
1172
|
+
AI outputs may be inaccurate, biased, or out of date. Do **not** rely on AI outputs for medical,
|
|
1173
|
+
legal, financial, or safety-critical decisions without independent verification by a qualified
|
|
1174
|
+
human professional.
|
|
1175
|
+
|
|
1176
|
+
AI \u8F93\u51FA\u53EF\u80FD\u4E0D\u51C6\u786E\u3001\u5B58\u5728\u504F\u89C1\u6216\u8FC7\u65F6\u3002**\u8BF7\u52FF**\u5728\u672A\u7ECF\u5408\u683C\u4E13\u4E1A\u4EBA\u58EB\u72EC\u7ACB\u6838\u5B9E\u7684\u60C5\u51B5\u4E0B\uFF0C\u5C06 AI \u8F93\u51FA
|
|
1177
|
+
\u7528\u4E8E\u533B\u7597\u3001\u6CD5\u5F8B\u3001\u91D1\u878D\u6216\u5B89\u5168\u6538\u5173\u7684\u51B3\u7B56\u3002
|
|
1178
|
+
`,ur=`---
|
|
1179
|
+
title: Cross-Border Data Notice / \u6570\u636E\u51FA\u5883\u544A\u77E5
|
|
1180
|
+
description: Notice of cross-border transfer of personal information, per PIPL.
|
|
1181
|
+
lastUpdated: 2026-01-01
|
|
1182
|
+
---
|
|
1183
|
+
|
|
1184
|
+
${Oe}
|
|
1185
|
+
|
|
1186
|
+
# Cross-Border Data Notice / \u6570\u636E\u51FA\u5883\u544A\u77E5
|
|
1187
|
+
|
|
1188
|
+
Pursuant to Articles 38\u201341 of the **Personal Information Protection Law of the PRC** (\u300A\u4E2A\u4EBA\u4FE1\u606F
|
|
1189
|
+
\u4FDD\u62A4\u6CD5\u300B, "PIPL"), we notify users that the following personal information may be transferred
|
|
1190
|
+
outside mainland China to provide the Service.
|
|
1191
|
+
|
|
1192
|
+
\u4F9D\u636E\u300A\u4E2D\u534E\u4EBA\u6C11\u5171\u548C\u56FD\u4E2A\u4EBA\u4FE1\u606F\u4FDD\u62A4\u6CD5\u300B\u7B2C\u4E09\u5341\u516B\u6761\u81F3\u7B2C\u56DB\u5341\u4E00\u6761\uFF0C\u6211\u4EEC\u544A\u77E5\u7528\u6237\uFF0C\u4EE5\u4E0B\u4E2A\u4EBA\u4FE1\u606F
|
|
1193
|
+
\u53EF\u80FD\u4E3A\u63D0\u4F9B\u670D\u52A1\u800C\u4F20\u8F93\u81F3\u4E2D\u56FD\u5927\u9646\u5883\u5916\u3002
|
|
1194
|
+
|
|
1195
|
+
---
|
|
1196
|
+
|
|
1197
|
+
## 1. Overseas Recipients / \u5883\u5916\u63A5\u6536\u65B9
|
|
1198
|
+
|
|
1199
|
+
> TODO (fill in before launch / \u4E0A\u7EBF\u524D\u586B\u5199):
|
|
1200
|
+
|
|
1201
|
+
| Recipient / \u63A5\u6536\u65B9 | Country / \u56FD\u5BB6 | Purpose / \u76EE\u7684 | Data Categories / \u6570\u636E\u7C7B\u522B |
|
|
1202
|
+
| --- | --- | --- | --- |
|
|
1203
|
+
| _AWS (example)_ | _US_ | _Hosting_ | _Account, usage logs, content_ |
|
|
1204
|
+
| _OpenAI_ | _US_ | _LLM inference_ | _Prompts, generated content_ |
|
|
1205
|
+
| _Resend_ | _US_ | _Transactional email_ | _Email address, message body_ |
|
|
1206
|
+
| _Cloudflare_ | _US / Global_ | _CDN, DDoS protection_ | _IP address, request metadata_ |
|
|
1207
|
+
|
|
1208
|
+
---
|
|
1209
|
+
|
|
1210
|
+
## 2. Legal Basis / \u5408\u6CD5\u6027\u57FA\u7840
|
|
1211
|
+
|
|
1212
|
+
Cross-border transfers rely on one of:
|
|
1213
|
+
- **User's separate consent** under PIPL Article 39.
|
|
1214
|
+
- **Standard Contract** with the overseas recipient per CAC Measures (CAC \u6807\u51C6\u5408\u540C).
|
|
1215
|
+
- **Security Assessment** by the CAC (for transfers meeting statutory thresholds).
|
|
1216
|
+
|
|
1217
|
+
\u8DE8\u5883\u4F20\u8F93\u4F9D\u636E\u4EE5\u4E0B\u4E4B\u4E00\uFF1A
|
|
1218
|
+
- \u300A\u4E2A\u4EBA\u4FE1\u606F\u4FDD\u62A4\u6CD5\u300B\u7B2C\u4E09\u5341\u4E5D\u6761\u4E0B\u7684**\u5355\u72EC\u540C\u610F**\u3002
|
|
1219
|
+
- \u4E0E\u5883\u5916\u63A5\u6536\u65B9\u7B7E\u8BA2\u7684 **CAC \u6807\u51C6\u5408\u540C**\u3002
|
|
1220
|
+
- \u8FBE\u5230\u6CD5\u5B9A\u95E8\u69DB\u65F6\u7684**\u7F51\u4FE1\u529E\u5B89\u5168\u8BC4\u4F30**\u3002
|
|
1221
|
+
|
|
1222
|
+
---
|
|
1223
|
+
|
|
1224
|
+
## 3. Your Rights / \u60A8\u7684\u6743\u5229
|
|
1225
|
+
|
|
1226
|
+
You may **refuse** cross-border transfer at any time by contacting {{COMPANY_CONTACT_EMAIL}}.
|
|
1227
|
+
Refusal may limit your ability to use features that rely on overseas services.
|
|
1228
|
+
|
|
1229
|
+
\u60A8\u53EF\u968F\u65F6\u8054\u7CFB {{COMPANY_CONTACT_EMAIL}} \u62D2\u7EDD\u8DE8\u5883\u4F20\u8F93\u3002\u62D2\u7EDD\u53EF\u80FD\u4F1A\u9650\u5236\u60A8\u4F7F\u7528\u4F9D\u8D56\u5883\u5916\u670D\u52A1\u7684
|
|
1230
|
+
\u529F\u80FD\u3002
|
|
1231
|
+
|
|
1232
|
+
---
|
|
1233
|
+
|
|
1234
|
+
## 4. Safeguards / \u4FDD\u969C\u63AA\u65BD
|
|
1235
|
+
|
|
1236
|
+
- Encryption in transit (TLS 1.2+) and at rest (AES-256).
|
|
1237
|
+
- Contractual clauses obligating overseas recipients to provide PIPL-equivalent protection.
|
|
1238
|
+
- Access controls, audit logging, and breach notification procedures.
|
|
1239
|
+
|
|
1240
|
+
- \u4F20\u8F93\u4E2D\u52A0\u5BC6 (TLS 1.2+) \u53CA\u9759\u6001\u52A0\u5BC6 (AES-256)\u3002
|
|
1241
|
+
- \u5408\u540C\u6761\u6B3E\u8981\u6C42\u5883\u5916\u63A5\u6536\u65B9\u63D0\u4F9B\u7B49\u540C\u4E8E PIPL \u7684\u4FDD\u62A4\u3002
|
|
1242
|
+
- \u8BBF\u95EE\u63A7\u5236\u3001\u5BA1\u8BA1\u65E5\u5FD7\u548C\u8FDD\u89C4\u901A\u77E5\u7A0B\u5E8F\u3002
|
|
1243
|
+
`,mr=`---
|
|
1244
|
+
title: Personal Information Collection List / \u4E2A\u4EBA\u4FE1\u606F\u6536\u96C6\u6E05\u5355
|
|
1245
|
+
description: Itemized list of personal information collected, per PIPL Article 17.
|
|
1246
|
+
lastUpdated: 2026-01-01
|
|
1247
|
+
---
|
|
1248
|
+
|
|
1249
|
+
${Oe}
|
|
1250
|
+
|
|
1251
|
+
# Personal Information Collection List / \u4E2A\u4EBA\u4FE1\u606F\u6536\u96C6\u6E05\u5355
|
|
1252
|
+
|
|
1253
|
+
Pursuant to Article 17 of the **PIPL**, we publish an itemized list of personal information
|
|
1254
|
+
collected, the purpose of processing, and retention period.
|
|
1255
|
+
|
|
1256
|
+
\u4F9D\u636E\u300A\u4E2A\u4EBA\u4FE1\u606F\u4FDD\u62A4\u6CD5\u300B\u7B2C\u5341\u4E03\u6761\uFF0C\u6211\u4EEC\u516C\u5F00\u4E2A\u4EBA\u4FE1\u606F\u6536\u96C6\u6E05\u5355\uFF0C\u5305\u62EC\u6536\u96C6\u9879\u3001\u5904\u7406\u76EE\u7684\u548C\u4FDD\u7559\u671F\u9650\u3002
|
|
1257
|
+
|
|
1258
|
+
---
|
|
1259
|
+
|
|
1260
|
+
| Category / \u7C7B\u522B | Item / \u9879 | Purpose / \u76EE\u7684 | Source / \u6765\u6E90 | Retention / \u4FDD\u7559\u671F | Required? / \u5FC5\u8981? |
|
|
1261
|
+
| --- | --- | --- | --- | --- | --- |
|
|
1262
|
+
| Account / \u8D26\u6237 | Email / \u90AE\u7BB1 | Registration, auth, notifications / \u6CE8\u518C\u3001\u767B\u5F55\u3001\u901A\u77E5 | User-provided | Until account deletion + 30d / \u8D26\u6237\u5220\u9664\u540E 30 \u5929 | Yes / \u662F |
|
|
1263
|
+
| Account / \u8D26\u6237 | Password (hashed) / \u5BC6\u7801\uFF08\u52A0\u5BC6\uFF09 | Authentication / \u8EAB\u4EFD\u9A8C\u8BC1 | User-provided | Until account deletion / \u8D26\u6237\u5220\u9664 | Yes / \u662F |
|
|
1264
|
+
| Account / \u8D26\u6237 | Phone / \u624B\u673A\u53F7 | 2FA, account recovery / \u53CC\u56E0\u7D20\u3001\u8D26\u6237\u6062\u590D | User-provided | Until account deletion / \u8D26\u6237\u5220\u9664 | Optional / \u53EF\u9009 |
|
|
1265
|
+
| Profile / \u8D44\u6599 | Display name, avatar / \u6635\u79F0\u3001\u5934\u50CF | Service personalization / \u670D\u52A1\u4E2A\u6027\u5316 | User-provided | Until updated / \u76F4\u81F3\u66F4\u65B0 | Optional / \u53EF\u9009 |
|
|
1266
|
+
| Usage / \u4F7F\u7528 | IP address / IP \u5730\u5740 | Security, abuse prevention / \u5B89\u5168\u3001\u9632\u6EE5\u7528 | Automatic / \u81EA\u52A8 | 90 days / 90 \u5929 | Yes / \u662F |
|
|
1267
|
+
| Usage / \u4F7F\u7528 | User agent / \u6D4F\u89C8\u5668\u6807\u8BC6 | Compatibility, security / \u517C\u5BB9\u6027\u3001\u5B89\u5168 | Automatic / \u81EA\u52A8 | 90 days / 90 \u5929 | Yes / \u662F |
|
|
1268
|
+
| Usage / \u4F7F\u7528 | Page views, clickstream / \u9875\u9762\u6D4F\u89C8\u3001\u70B9\u51FB\u6D41 | Analytics, product improvement / \u5206\u6790\u3001\u4EA7\u54C1\u6539\u8FDB | Automatic / \u81EA\u52A8 | 13 months / 13 \u4E2A\u6708 | Consent / \u9700\u540C\u610F |
|
|
1269
|
+
| Usage / \u4F7F\u7528 | Cookies / Cookie | See Cookie Preferences / \u8BE6\u89C1 Cookie \u504F\u597D | Automatic / \u81EA\u52A8 | Varies / \u4E0D\u4E00 | Consent / \u9700\u540C\u610F |
|
|
1270
|
+
| Billing / \u652F\u4ED8 | Billing address / \u8D26\u5355\u5730\u5740 | Payment processing / \u652F\u4ED8\u5904\u7406 | User-provided | 7 years (tax) / 7 \u5E74\uFF08\u7A0E\u52A1\uFF09 | If paid plan / \u4ED8\u8D39\u65F6 |
|
|
1271
|
+
| Billing / \u652F\u4ED8 | Payment method token / \u652F\u4ED8\u4EE4\u724C | Recurring billing / \u8BA2\u9605\u7EED\u8D39 | Stripe / payment processor | Until deletion / \u76F4\u81F3\u5220\u9664 | If paid plan / \u4ED8\u8D39\u65F6 |
|
|
1272
|
+
| Content / \u5185\u5BB9 | Uploaded files / \u4E0A\u4F20\u6587\u4EF6 | Service delivery / \u670D\u52A1\u4EA4\u4ED8 | User-provided | Until deletion by user / \u76F4\u81F3\u7528\u6237\u5220\u9664 | Optional / \u53EF\u9009 |
|
|
1273
|
+
| Content / \u5185\u5BB9 | Messages, prompts / \u6D88\u606F\u3001\u63D0\u793A\u8BCD | Service delivery, AI inference / \u670D\u52A1\u4EA4\u4ED8\u3001AI \u63A8\u7406 | User-provided | 30 days then purged unless saved / 30 \u5929\u540E\u6E05\u9664\uFF08\u9664\u975E\u4FDD\u5B58\uFF09 | Optional / \u53EF\u9009 |
|
|
1274
|
+
| Behavior / \u884C\u4E3A | Feature flags, experiments / \u529F\u80FD\u5F00\u5173\u3001\u5B9E\u9A8C | Product personalization / \u4EA7\u54C1\u4E2A\u6027\u5316 | Automatic / \u81EA\u52A8 | 13 months / 13 \u4E2A\u6708 | Consent / \u9700\u540C\u610F |
|
|
1275
|
+
| Device / \u8BBE\u5907 | Device ID, OS version / \u8BBE\u5907 ID\u3001\u7CFB\u7EDF\u7248\u672C | Security, compatibility / \u5B89\u5168\u3001\u517C\u5BB9\u6027 | Automatic / \u81EA\u52A8 | 90 days / 90 \u5929 | Yes / \u662F |
|
|
1276
|
+
|
|
1277
|
+
---
|
|
1278
|
+
|
|
1279
|
+
## Contact / \u8054\u7CFB\u65B9\u5F0F
|
|
1280
|
+
|
|
1281
|
+
To exercise your rights (access, rectify, delete, export, withdraw consent), contact
|
|
1282
|
+
**{{COMPANY_CONTACT_EMAIL}}**.
|
|
1283
|
+
|
|
1284
|
+
\u5982\u9700\u884C\u4F7F\u60A8\u7684\u6743\u5229\uFF08\u8BBF\u95EE\u3001\u66F4\u6B63\u3001\u5220\u9664\u3001\u5BFC\u51FA\u3001\u64A4\u56DE\u540C\u610F\uFF09\uFF0C\u8BF7\u8054\u7CFB **{{COMPANY_CONTACT_EMAIL}}**\u3002
|
|
1285
|
+
`;async function gr(e,t){let n=D(e);if(!P.existsSync(n))return;let r=v.join(n,"src","components","compliance","Footer.tsx");k(r,or);let o=v.join(n,"src","components","compliance","README.md");k(o,["# Compliance Components","",`Auto-generated by \`create-sailor\` for region: \`${t}\`.`,"","## Mounting the Footer","","```tsx",'import { Footer } from "@/components/compliance/Footer";','import { CookieBanner } from "@/components/compliance/CookieBanner";',"","export default function RootLayout({ children }) {"," return ("," <html>"," <body>"," {children}",` <Footer region="${t}" />`," <CookieBanner />"," </body>"," </html>"," );","}","```","","## Placeholders","","Policy MDX files contain `{{COMPANY_NAME}}`, `{{COMPANY_ADDRESS}}`,","`{{COMPANY_CONTACT_EMAIL}}`, `{{GOVERNING_JURISDICTION}}`, and","`{{AIGC_ALGORITHM_FILING_NUMBER}}`. These are filled by the `brand-apply` step.",""].join(`
|
|
1286
|
+
`))}async function fr(e,t){let n=D(e);if(!P.existsSync(n))return;let r=v.join(n,"src","components","compliance","CookieBanner.tsx");k(r,ar)}async function yr(e,t){let n=D(e);if(!P.existsSync(n))return;let r=v.join(n,ke,"privacy","page.mdx");k(r,pr)}async function Sr(e,t){let n=D(e);if(!P.existsSync(n))return;let r=v.join(n,ke,"terms","page.mdx");k(r,lr)}async function Er(e){let t=D(e);if(!P.existsSync(t))return;let n=v.join(t,tt,"IcpFooter.tsx");k(n,sr),nt(e,["# Chinese ICP filing (\u5DE5\u4FE1\u90E8 ICP \u5907\u6848) \u2014 required to legally host a","# public-facing site from mainland China. Apply at https://beian.miit.gov.cn/",'NEXT_PUBLIC_ICP_BEIAN="" # \u5982 \u82CFICP\u59072024XXXXXX\u53F7'].join(`
|
|
1287
|
+
`))}async function hr(e){let t=D(e);if(!P.existsSync(t))return;let n=v.join(t,tt,"PublicSecurityFooter.tsx");k(n,ir),nt(e,["# Chinese Public Security Bureau filing (\u516C\u5B89\u8054\u7F51\u5907\u6848)","# Required in addition to ICP. Apply at http://www.beian.gov.cn/",'NEXT_PUBLIC_PSB_BEIAN="" # \u5982 \u82CF\u516C\u7F51\u5B89\u5907 32011402XXXXXX\u53F7','NEXT_PUBLIC_PSB_CODE="" # 14-digit record code'].join(`
|
|
1288
|
+
`))}async function _r(e){let t=D(e);if(!P.existsSync(t))return;let n=v.join(t,tt,"AigcDisclaimer.tsx");k(n,cr),nt(e,["# Generative AI algorithm filing (\u7B97\u6CD5\u5907\u6848\u53F7) \u2014 required by","# \u7F51\u4FE1\u529E\u300A\u751F\u6210\u5F0F\u4EBA\u5DE5\u667A\u80FD\u670D\u52A1\u7BA1\u7406\u6682\u884C\u529E\u6CD5\u300Bfor services offered in China.",'NEXT_PUBLIC_AIGC_FILING_NUMBER="" # e.g. \u7F51\u4FE1\u7B97\u5907XXXXXXXX\u53F7'].join(`
|
|
1289
|
+
`))}async function vr(e){let t=D(e);if(!P.existsSync(t))return;let n=v.join(t,ke,"aigc-disclosure","page.mdx");k(n,dr)}async function Ir(e){let t=D(e);if(!P.existsSync(t))return;let n=v.join(t,ke,"cross-border-data","page.mdx");k(n,ur)}async function Pr(e){let t=D(e);if(!P.existsSync(t))return;let n=v.join(t,ke,"pii-collection","page.mdx");k(n,mr)}async function wt(e,t){try{await gr(e,t),await fr(e,t),await yr(e,t),await Sr(e,t),(t==="cn"||t==="hybrid")&&(await Er(e),await hr(e),await _r(e),await vr(e),await Ir(e),await Pr(e))}catch(n){throw console.error("applyComplianceTemplates failed:",n),new Error(`Failed to apply compliance templates for region "${t}". Check that ${e} is writable.`)}}import br from"fs";import Ar from"path";async function Nt(e,t){let n=Ar.join(e,"nebutra.config.json");br.writeFileSync(n,JSON.stringify(t,null,2)+`
|
|
1290
|
+
`)}import q from"fs";import rt from"path";function Cr(e){q.existsSync(e)&&q.rmSync(e,{recursive:!0,force:!0})}function Tt(e,t){return e.replace(/provider\s*=\s*"postgresql"/,`provider = "${t}"`)}function wr(e){return e.replace(/^\s*directUrl\s*=\s*env\([^)]*\)\s*\n/gm,"")}function xt(e){let t=e.replace(/^\s*extensions\s*=\s*\[[^\]]*\]\s*\n/gm,"");return t=t.replace(/previewFeatures\s*=\s*\[\s*"postgresqlExtensions"\s*\]\s*\n/g,""),t}function Nr(e,t){let n=rt.join(e,".env.example");q.existsSync(n)?q.appendFileSync(n,`
|
|
1291
|
+
`+t):q.writeFileSync(n,t)}async function Rt(e,t,n){let r=rt.join(e,"packages","db");if(t==="none"){Cr(r);return}let o=rt.join(r,"prisma","schema.prisma"),s;if(t==="postgresql"?s=`postgresql://postgres:postgres@localhost:5432/${n}`:t==="mysql"?s=`mysql://root:root@localhost:3306/${n}`:s="file:./dev.db",Nr(e,`# Database (${t})
|
|
1292
|
+
DATABASE_URL="${s}"
|
|
1293
|
+
`),!q.existsSync(o))return;let a=q.readFileSync(o,"utf8"),c=a;t==="mysql"?(c=Tt(c,"mysql"),c=xt(c)):t==="sqlite"&&(c=Tt(c,"sqlite"),c=wr(c),c=xt(c)),c!==a&&q.writeFileSync(o,c)}import Ue from"fs";import N from"path";import{fileURLToPath as Tr}from"url";var xr=Tr(import.meta.url),Rr=N.dirname(xr);async function kt(e,t){if(t==="none")return;let n=N.join(Rr,"../../templates/deploy");t==="vercel"?await Ue.promises.copyFile(N.join(n,"vercel.json"),N.join(e,"vercel.json")):t==="railway"?await Ue.promises.copyFile(N.join(n,"railway.toml"),N.join(e,"railway.toml")):t==="cloudflare"?await Ue.promises.copyFile(N.join(n,"wrangler.toml"),N.join(e,"wrangler.toml")):t==="selfhost"&&(await Ue.promises.copyFile(N.join(n,"docker-compose.yml"),N.join(e,"docker-compose.yml")),await Ue.promises.copyFile(N.join(n,"Dockerfile.web"),N.join(e,"Dockerfile.web")))}import G from"fs";import ne from"path";import{fileURLToPath as kr}from"url";var Or=kr(import.meta.url),Ot=ne.dirname(Or),Ur=/\.(md|mdx|json|ts|tsx|js|mjs|cjs|yaml|yml)$/;function Lr(e){let t=[ne.join(Ot,"..","templates","docs",e),ne.join(Ot,"..","..","templates","docs",e)];for(let n of t)if(G.existsSync(n))return n;return t[0]}async function Ut(e,t){if(t.framework==="none")return;let n=(t.framework==="fumadocs","fumadocs"),r=Lr(n),o=ne.join(e,"apps","docs");if(!G.existsSync(r))throw new Error(`Docs template not found: ${r}`);Lt(r,o),Mt(o,{"{PRODUCT_NAME}":t.projectName})}function Lt(e,t){G.mkdirSync(t,{recursive:!0});for(let n of G.readdirSync(e,{withFileTypes:!0})){let r=ne.join(e,n.name),o=ne.join(t,n.name);n.isDirectory()?Lt(r,o):G.copyFileSync(r,o)}}function Mt(e,t){for(let n of G.readdirSync(e,{withFileTypes:!0})){let r=ne.join(e,n.name);if(n.isDirectory())Mt(r,t);else if(Ur.test(n.name)){let o=G.readFileSync(r,"utf-8");for(let[s,a]of Object.entries(t))o=o.split(s).join(a);G.writeFileSync(r,o)}}}import oe from"fs";import re from"path";var We=[{id:"resend",name:"Resend",region:"global",envVars:["RESEND_API_KEY","RESEND_FROM"],docs:"https://resend.com/docs",pkg:"resend",pkgVersion:"^4.0.1"},{id:"postmark",name:"Postmark",region:"global",envVars:["POSTMARK_API_KEY","POSTMARK_FROM"],docs:"https://postmarkapp.com/developer",pkg:"postmark",pkgVersion:"^4.0.5"},{id:"ses",name:"AWS SES",region:"global",envVars:["AWS_ACCESS_KEY_ID","AWS_SECRET_ACCESS_KEY","AWS_SES_REGION","SES_FROM"],docs:"https://docs.aws.amazon.com/ses",pkg:"@aws-sdk/client-sesv2",pkgVersion:"^3.658.0"},{id:"aliyun-dm",name:"\u963F\u91CC\u4E91\u90AE\u4EF6\u63A8\u9001",region:"cn",envVars:["ALIYUN_DM_ACCESS_KEY","ALIYUN_DM_ACCESS_SECRET","ALIYUN_DM_ACCOUNT_NAME","ALIYUN_DM_FROM_ALIAS"],docs:"https://help.aliyun.com/product/29412.html",pkg:"@alicloud/dm20151123",pkgVersion:"^3.0.0"},{id:"tencent-ses",name:"\u817E\u8BAF\u4E91 SES",region:"cn",envVars:["TENCENT_SECRET_ID","TENCENT_SECRET_KEY","TENCENT_SES_FROM_EMAIL"],docs:"https://cloud.tencent.com/document/product/1288",pkg:"tencentcloud-sdk-nodejs-ses",pkgVersion:"^4.0.900"},{id:"netease",name:"\u7F51\u6613\u4F01\u4E1A\u90AE\u7BB1",region:"cn",envVars:["NETEASE_USER","NETEASE_PASS","NETEASE_FROM"],docs:"https://qiye.163.com/help",pkg:"nodemailer",pkgVersion:"^6.9.15"},{id:"none",name:"None",region:"global",envVars:[],docs:""}];function jt(e){return We.find(t=>t.id===e)}var Qs=We.reduce((e,t)=>(e[t.region]||(e[t.region]=[]),e[t.region].push(t),e),{});var se="@nebutra/email";function ot(e,t){oe.mkdirSync(re.dirname(e),{recursive:!0}),oe.writeFileSync(e,t)}function Mr(e,t){let n=re.join(e,".env.example");oe.existsSync(n)?oe.appendFileSync(n,`
|
|
1294
|
+
`+t):oe.writeFileSync(n,t)}function jr(e){if(e.envVars.length===0)return"";let t=`# Email provider: ${e.name}`,n=e.envVars.map(r=>`${r}=`).join(`
|
|
1295
|
+
`);return`${t}
|
|
1296
|
+
${n}
|
|
1297
|
+
`}var Dr=`/**
|
|
1298
|
+
* Public inputs accepted by the \`sendEmail\` wrapper.
|
|
1299
|
+
* Keep the surface minimal \u2014 individual providers may accept more, but
|
|
1300
|
+
* consumers should code against this shape for portability.
|
|
1301
|
+
*/
|
|
1302
|
+
export interface SendEmailInput {
|
|
1303
|
+
to: string | string[];
|
|
1304
|
+
subject: string;
|
|
1305
|
+
html?: string;
|
|
1306
|
+
text?: string;
|
|
1307
|
+
from?: string;
|
|
1308
|
+
replyTo?: string;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
export interface SendEmailResult {
|
|
1312
|
+
/** Provider-specific message identifier, when available. */
|
|
1313
|
+
id?: string;
|
|
1314
|
+
/** Raw provider response \u2014 useful for debugging, not a stable contract. */
|
|
1315
|
+
raw?: unknown;
|
|
1316
|
+
}
|
|
1317
|
+
`;function Fr(){return`/**
|
|
1318
|
+
* @${se.slice(1)} \u2014 Resend transactional email wrapper.
|
|
1319
|
+
*
|
|
1320
|
+
* Docs: https://resend.com/docs
|
|
1321
|
+
*/
|
|
1322
|
+
import { Resend } from "resend";
|
|
1323
|
+
|
|
1324
|
+
import type { SendEmailInput, SendEmailResult } from "./types.js";
|
|
1325
|
+
|
|
1326
|
+
const apiKey = process.env.RESEND_API_KEY;
|
|
1327
|
+
if (!apiKey) {
|
|
1328
|
+
throw new Error("RESEND_API_KEY not configured");
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
const resend = new Resend(apiKey);
|
|
1332
|
+
|
|
1333
|
+
export async function sendEmail(input: SendEmailInput): Promise<SendEmailResult> {
|
|
1334
|
+
const from = input.from ?? process.env.RESEND_FROM;
|
|
1335
|
+
if (!from) {
|
|
1336
|
+
throw new Error("RESEND_FROM (or input.from) must be provided");
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
const { data, error } = await resend.emails.send({
|
|
1340
|
+
from,
|
|
1341
|
+
to: Array.isArray(input.to) ? input.to : [input.to],
|
|
1342
|
+
subject: input.subject,
|
|
1343
|
+
html: input.html,
|
|
1344
|
+
text: input.text,
|
|
1345
|
+
...(input.replyTo ? { replyTo: input.replyTo } : {}),
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
if (error) {
|
|
1349
|
+
throw new Error(\`Resend send failed: \${error.message}\`);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
return { id: data?.id, raw: data };
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
export type { SendEmailInput, SendEmailResult } from "./types.js";
|
|
1356
|
+
`}function Br(){return`/**
|
|
1357
|
+
* @${se.slice(1)} \u2014 Postmark transactional email wrapper.
|
|
1358
|
+
*
|
|
1359
|
+
* Docs: https://postmarkapp.com/developer
|
|
1360
|
+
*/
|
|
1361
|
+
import { ServerClient } from "postmark";
|
|
1362
|
+
|
|
1363
|
+
import type { SendEmailInput, SendEmailResult } from "./types.js";
|
|
1364
|
+
|
|
1365
|
+
const apiKey = process.env.POSTMARK_API_KEY;
|
|
1366
|
+
if (!apiKey) {
|
|
1367
|
+
throw new Error("POSTMARK_API_KEY not configured");
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
const client = new ServerClient(apiKey);
|
|
1371
|
+
|
|
1372
|
+
export async function sendEmail(input: SendEmailInput): Promise<SendEmailResult> {
|
|
1373
|
+
const from = input.from ?? process.env.POSTMARK_FROM;
|
|
1374
|
+
if (!from) {
|
|
1375
|
+
throw new Error("POSTMARK_FROM (or input.from) must be provided");
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
const to = Array.isArray(input.to) ? input.to.join(", ") : input.to;
|
|
1379
|
+
|
|
1380
|
+
const response = await client.sendEmail({
|
|
1381
|
+
From: from,
|
|
1382
|
+
To: to,
|
|
1383
|
+
Subject: input.subject,
|
|
1384
|
+
HtmlBody: input.html,
|
|
1385
|
+
TextBody: input.text,
|
|
1386
|
+
ReplyTo: input.replyTo,
|
|
1387
|
+
MessageStream: "outbound",
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
return { id: response.MessageID, raw: response };
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
export type { SendEmailInput, SendEmailResult } from "./types.js";
|
|
1394
|
+
`}function Yr(){return`/**
|
|
1395
|
+
* @${se.slice(1)} \u2014 AWS SES v2 transactional email wrapper.
|
|
1396
|
+
*
|
|
1397
|
+
* Docs: https://docs.aws.amazon.com/ses
|
|
1398
|
+
*/
|
|
1399
|
+
import {
|
|
1400
|
+
SendEmailCommand,
|
|
1401
|
+
SESv2Client,
|
|
1402
|
+
} from "@aws-sdk/client-sesv2";
|
|
1403
|
+
|
|
1404
|
+
import type { SendEmailInput, SendEmailResult } from "./types.js";
|
|
1405
|
+
|
|
1406
|
+
const region = process.env.AWS_SES_REGION;
|
|
1407
|
+
if (!region) {
|
|
1408
|
+
throw new Error("AWS_SES_REGION not configured");
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
const client = new SESv2Client({
|
|
1412
|
+
region,
|
|
1413
|
+
credentials:
|
|
1414
|
+
process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY
|
|
1415
|
+
? {
|
|
1416
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
1417
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
1418
|
+
}
|
|
1419
|
+
: undefined,
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
export async function sendEmail(input: SendEmailInput): Promise<SendEmailResult> {
|
|
1423
|
+
const from = input.from ?? process.env.SES_FROM;
|
|
1424
|
+
if (!from) {
|
|
1425
|
+
throw new Error("SES_FROM (or input.from) must be provided");
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
const toAddresses = Array.isArray(input.to) ? input.to : [input.to];
|
|
1429
|
+
|
|
1430
|
+
const command = new SendEmailCommand({
|
|
1431
|
+
FromEmailAddress: from,
|
|
1432
|
+
Destination: { ToAddresses: toAddresses },
|
|
1433
|
+
ReplyToAddresses: input.replyTo ? [input.replyTo] : undefined,
|
|
1434
|
+
Content: {
|
|
1435
|
+
Simple: {
|
|
1436
|
+
Subject: { Data: input.subject, Charset: "UTF-8" },
|
|
1437
|
+
Body: {
|
|
1438
|
+
...(input.html ? { Html: { Data: input.html, Charset: "UTF-8" } } : {}),
|
|
1439
|
+
...(input.text ? { Text: { Data: input.text, Charset: "UTF-8" } } : {}),
|
|
1440
|
+
},
|
|
1441
|
+
},
|
|
1442
|
+
},
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
const response = await client.send(command);
|
|
1446
|
+
return { id: response.MessageId, raw: response };
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
export type { SendEmailInput, SendEmailResult } from "./types.js";
|
|
1450
|
+
`}function Kr(){return`/**
|
|
1451
|
+
* @${se.slice(1)} \u2014 \u963F\u91CC\u4E91\u90AE\u4EF6\u63A8\u9001 (Aliyun DirectMail) wrapper.
|
|
1452
|
+
*
|
|
1453
|
+
* Docs: https://help.aliyun.com/product/29412.html
|
|
1454
|
+
*/
|
|
1455
|
+
import Dm20151123, * as $Dm20151123 from "@alicloud/dm20151123";
|
|
1456
|
+
import * as $OpenApi from "@alicloud/openapi-client";
|
|
1457
|
+
|
|
1458
|
+
import type { SendEmailInput, SendEmailResult } from "./types.js";
|
|
1459
|
+
|
|
1460
|
+
const accessKeyId = process.env.ALIYUN_DM_ACCESS_KEY;
|
|
1461
|
+
const accessKeySecret = process.env.ALIYUN_DM_ACCESS_SECRET;
|
|
1462
|
+
if (!accessKeyId || !accessKeySecret) {
|
|
1463
|
+
throw new Error(
|
|
1464
|
+
"ALIYUN_DM_ACCESS_KEY and ALIYUN_DM_ACCESS_SECRET must be configured",
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
const config = new $OpenApi.Config({
|
|
1469
|
+
accessKeyId,
|
|
1470
|
+
accessKeySecret,
|
|
1471
|
+
endpoint: "dm.aliyuncs.com",
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
const client = new Dm20151123.default(config);
|
|
1475
|
+
|
|
1476
|
+
export async function sendEmail(input: SendEmailInput): Promise<SendEmailResult> {
|
|
1477
|
+
const accountName = process.env.ALIYUN_DM_ACCOUNT_NAME;
|
|
1478
|
+
if (!accountName) {
|
|
1479
|
+
throw new Error("ALIYUN_DM_ACCOUNT_NAME must be configured");
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
const toAddress = Array.isArray(input.to) ? input.to.join(",") : input.to;
|
|
1483
|
+
|
|
1484
|
+
const req = new $Dm20151123.SingleSendMailRequest({
|
|
1485
|
+
accountName,
|
|
1486
|
+
addressType: 1,
|
|
1487
|
+
replyToAddress: false,
|
|
1488
|
+
toAddress,
|
|
1489
|
+
subject: input.subject,
|
|
1490
|
+
htmlBody: input.html,
|
|
1491
|
+
textBody: input.text,
|
|
1492
|
+
fromAlias: process.env.ALIYUN_DM_FROM_ALIAS,
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1495
|
+
const response = await client.singleSendMail(req);
|
|
1496
|
+
return { id: response.body?.requestId, raw: response };
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
export type { SendEmailInput, SendEmailResult } from "./types.js";
|
|
1500
|
+
`}function Vr(){return`/**
|
|
1501
|
+
* @${se.slice(1)} \u2014 \u817E\u8BAF\u4E91 SES wrapper.
|
|
1502
|
+
*
|
|
1503
|
+
* Docs: https://cloud.tencent.com/document/product/1288
|
|
1504
|
+
*/
|
|
1505
|
+
import tencentcloud from "tencentcloud-sdk-nodejs-ses";
|
|
1506
|
+
|
|
1507
|
+
import type { SendEmailInput, SendEmailResult } from "./types.js";
|
|
1508
|
+
|
|
1509
|
+
const secretId = process.env.TENCENT_SECRET_ID;
|
|
1510
|
+
const secretKey = process.env.TENCENT_SECRET_KEY;
|
|
1511
|
+
if (!secretId || !secretKey) {
|
|
1512
|
+
throw new Error("TENCENT_SECRET_ID and TENCENT_SECRET_KEY must be configured");
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
const SesClient = tencentcloud.ses.v20201002.Client;
|
|
1516
|
+
|
|
1517
|
+
const client = new SesClient({
|
|
1518
|
+
credential: { secretId, secretKey },
|
|
1519
|
+
region: "ap-hongkong",
|
|
1520
|
+
profile: {
|
|
1521
|
+
httpProfile: { endpoint: "ses.tencentcloudapi.com" },
|
|
1522
|
+
},
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
export async function sendEmail(input: SendEmailInput): Promise<SendEmailResult> {
|
|
1526
|
+
const from = input.from ?? process.env.TENCENT_SES_FROM_EMAIL;
|
|
1527
|
+
if (!from) {
|
|
1528
|
+
throw new Error("TENCENT_SES_FROM_EMAIL (or input.from) must be provided");
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
const destination = Array.isArray(input.to) ? input.to : [input.to];
|
|
1532
|
+
|
|
1533
|
+
const response = await client.SendEmail({
|
|
1534
|
+
FromEmailAddress: from,
|
|
1535
|
+
Destination: destination,
|
|
1536
|
+
Subject: input.subject,
|
|
1537
|
+
ReplyToAddresses: input.replyTo,
|
|
1538
|
+
Simple: {
|
|
1539
|
+
Html: input.html,
|
|
1540
|
+
Text: input.text,
|
|
1541
|
+
},
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
return { id: response.MessageId, raw: response };
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
export type { SendEmailInput, SendEmailResult } from "./types.js";
|
|
1548
|
+
`}function $r(){return`/**
|
|
1549
|
+
* @${se.slice(1)} \u2014 \u7F51\u6613\u4F01\u4E1A\u90AE\u7BB1 via SMTP (nodemailer).
|
|
1550
|
+
*
|
|
1551
|
+
* Docs: https://qiye.163.com/help
|
|
1552
|
+
*/
|
|
1553
|
+
import nodemailer from "nodemailer";
|
|
1554
|
+
|
|
1555
|
+
import type { SendEmailInput, SendEmailResult } from "./types.js";
|
|
1556
|
+
|
|
1557
|
+
const user = process.env.NETEASE_USER;
|
|
1558
|
+
const pass = process.env.NETEASE_PASS;
|
|
1559
|
+
if (!user || !pass) {
|
|
1560
|
+
throw new Error("NETEASE_USER and NETEASE_PASS must be configured");
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
const transporter = nodemailer.createTransport({
|
|
1564
|
+
host: "smtp.qiye.163.com",
|
|
1565
|
+
port: 465,
|
|
1566
|
+
secure: true,
|
|
1567
|
+
auth: { user, pass },
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
export async function sendEmail(input: SendEmailInput): Promise<SendEmailResult> {
|
|
1571
|
+
const from = input.from ?? process.env.NETEASE_FROM ?? user;
|
|
1572
|
+
|
|
1573
|
+
const response = await transporter.sendMail({
|
|
1574
|
+
from,
|
|
1575
|
+
to: Array.isArray(input.to) ? input.to.join(", ") : input.to,
|
|
1576
|
+
subject: input.subject,
|
|
1577
|
+
html: input.html,
|
|
1578
|
+
text: input.text,
|
|
1579
|
+
replyTo: input.replyTo,
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
return { id: response.messageId, raw: response };
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
export type { SendEmailInput, SendEmailResult } from "./types.js";
|
|
1586
|
+
`}function qr(e){switch(e){case"resend":return Fr();case"postmark":return Br();case"ses":return Yr();case"aliyun-dm":return Kr();case"tencent-ses":return Vr();case"netease":return $r();case"none":return`export {};
|
|
1587
|
+
`}}function Gr(e){let t={};e.pkg&&e.pkgVersion&&(t[e.pkg]=e.pkgVersion),e.id==="aliyun-dm"&&(t["@alicloud/openapi-client"]="^0.4.12");let n={name:se,version:"0.1.0",private:!0,description:`Transactional email via ${e.name}`,type:"module",main:"./dist/index.js",exports:{".":{import:"./dist/index.js",types:"./dist/index.d.ts"}},scripts:{build:"tsup",typecheck:"tsc --noEmit",clean:"rm -rf dist .turbo"},dependencies:t,devDependencies:{tsup:"^8.5.0",typescript:"catalog:"}};return JSON.stringify(n,null,2)+`
|
|
1588
|
+
`}async function Dt(e,t,n){if(t==="none")return;let r=jt(t);if(!r)throw new Error(`Unknown email provider "${t}". Valid ids: ${We.map(a=>a.id).join(", ")}`);let o=re.join(e,"packages","email"),s=re.join(o,"src");oe.existsSync(s)&&oe.rmSync(s,{recursive:!0,force:!0}),ot(re.join(o,"package.json"),Gr(r)),ot(re.join(s,"types.ts"),Dr),ot(re.join(s,"index.ts"),qr(r.id)),Mr(e,jr(r))}import Bt from"crypto";import Le from"fs";import Ft from"path";function ie(e){return Bt.randomBytes(e).toString("base64").replace(/=+$/,"")}function Wr(e){return Bt.randomBytes(e).toString("hex")}function Xr(){return{AUTH_SECRET:ie(32),NEXTAUTH_SECRET:ie(32),BETTER_AUTH_SECRET:ie(32),JWT_SECRET:ie(32),WEBHOOK_SECRET:Wr(16),ENCRYPTION_KEY:ie(32),API_SECRET_KEY:ie(32),SESSION_SECRET:ie(32)}}function Hr(e){return e.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}function zr(e,t,n){let r=new RegExp(`^(?!#)\\s*${t}=.*$`,"gm"),o=Hr(n);return e.replace(r,`${t}="${o}"`)}async function Yt(e){let t=Ft.join(e,".env.example"),n=Ft.join(e,".env.local");if(!Le.existsSync(t))return null;try{let r=Le.readFileSync(t,"utf8"),o=Xr(),a=Le.existsSync(n)?Le.readFileSync(n,"utf8"):r;for(let c of Object.keys(o))a=zr(a,c,o[c]);return Le.writeFileSync(n,a),o}catch(r){let o=r instanceof Error?r.message:String(r);throw console.error(`[create-sailor] Failed to generate env secrets: ${o}`),new Error(`Unable to write .env.local: ${o}`)}}import Xe from"fs";import Kt from"path";async function Vt(e,t){let n=`# Automatically injected by create-sailor
|
|
55
1589
|
DATABASE_URL="${String(t.databaseUrl)}"
|
|
56
1590
|
|
|
57
1591
|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="${String(t.clerkPublishable)}"
|
|
58
1592
|
CLERK_SECRET_KEY="${String(t.clerkSecret)}"
|
|
59
1593
|
|
|
60
1594
|
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
|
|
61
|
-
`,
|
|
62
|
-
`+n)}import{execSync as
|
|
1595
|
+
`,r=Kt.join(e,".env"),o=Kt.join(e,".env.local");!Xe.existsSync(r)&&!Xe.existsSync(o)?Xe.writeFileSync(o,n):Xe.appendFileSync(o,`
|
|
1596
|
+
`+n)}import{execSync as Jr}from"child_process";import F from"fs";import Pe from"path";import Qr from"ignore";var Zr={mirror:{repo:process.env.SAILOR_TEMPLATE_REPO||"Nebutra/Sailor-Template",ref:process.env.SAILOR_TEMPLATE_REF||"main",applyIgnore:!1},main:{repo:"Nebutra/Nebutra-Sailor",ref:"main",applyIgnore:!0}};function eo(e,t){return`https://github.com/${e}/archive/refs/heads/${t}.tar.gz`}function $t(e,t,n){let r=F.readdirSync(t,{withFileTypes:!0});for(let o of r){if(o.name==="node_modules"||o.name===".git")continue;let s=Pe.join(t,o.name),a=Pe.relative(e,s).split(Pe.sep).join("/");o.isDirectory()?(n.push(a+"/"),$t(e,s,n)):n.push(a)}}function to(e){let t=Pe.join(e,".templateignore");if(!F.existsSync(t))return;let n=F.readFileSync(t,"utf8"),r=Qr().add(n),o=[];$t(e,e,o);let s=o.map(i=>i.endsWith("/")?i.slice(0,-1):i),a=new Set(r.filter(s)),c=s.filter(i=>!a.has(i)).sort((i,d)=>d.split("/").length-i.split("/").length);for(let i of c){let d=Pe.join(e,i);try{F.existsSync(d)&&F.rmSync(d,{recursive:!0,force:!0})}catch{}}try{F.existsSync(t)&&F.rmSync(t,{force:!0})}catch{}}function no(e,t){Jr(`set -o pipefail; curl -sSfL ${e} | tar -xz -C "${t}" --strip-components=1`,{stdio:"ignore",shell:"bash"})}async function qt(e){F.mkdirSync(e,{recursive:!0});let n=process.env.SAILOR_TEMPLATE_SOURCE==="main"?["main"]:["mirror","main"],r;for(let o of n){let s=Zr[o],a=eo(s.repo,s.ref);try{if(no(a,e),s.applyIgnore)try{to(e)}catch(c){throw new Error(`Template cloned, but .templateignore processing failed: ${c instanceof Error?c.message:String(c)}`)}return}catch(c){r=c instanceof Error?c:new Error(String(c));try{let i=F.readdirSync(e);for(let d of i)F.rmSync(Pe.join(e,d),{recursive:!0,force:!0})}catch{}}}throw new Error(`Failed to download the template from any source. Last error: ${r?.message??"unknown"}
|
|
63
1597
|
|
|
64
1598
|
Ensure you have internet access and curl/tar installed.
|
|
65
|
-
You can override the source with SAILOR_TEMPLATE_REPO / SAILOR_TEMPLATE_REF env vars.`)}import
|
|
66
|
-
|
|
67
|
-
|
|
1599
|
+
You can override the source with SAILOR_TEMPLATE_REPO / SAILOR_TEMPLATE_REF env vars.`)}import be from"fs";import C from"path";var Gt=[{id:"sentry",name:"Sentry",region:"both",envVars:["SENTRY_DSN","NEXT_PUBLIC_SENTRY_DSN"],docs:"https://docs.sentry.io",pkg:"@sentry/nextjs",instrumentationFile:"apps/web/instrumentation.ts"},{id:"datadog",name:"Datadog",region:"global",envVars:["DD_API_KEY","DD_APP_KEY","DD_SITE"],docs:"https://docs.datadoghq.com",pkg:"dd-trace",instrumentationFile:"apps/web/instrumentation.ts"},{id:"bugsnag",name:"Bugsnag",region:"global",envVars:["BUGSNAG_API_KEY","NEXT_PUBLIC_BUGSNAG_API_KEY"],docs:"https://docs.bugsnag.com",pkg:"@bugsnag/js",instrumentationFile:"apps/web/src/lib/monitoring/bugsnag.ts"},{id:"aliyun-arms",name:"\u963F\u91CC\u4E91 ARMS",region:"cn",envVars:["ALIYUN_ARMS_LICENSE_KEY","ALIYUN_ARMS_APP_NAME","NEXT_PUBLIC_ARMS_PID"],docs:"https://help.aliyun.com/product/34364.html",instrumentationFile:"apps/web/src/components/monitoring/ArmsScript.tsx"},{id:"tingyun",name:"\u542C\u4E91 TingYun",region:"cn",envVars:["TINGYUN_LICENSE_KEY","NEXT_PUBLIC_TINGYUN_APP_KEY"],docs:"https://www.tingyun.com/docs",instrumentationFile:"apps/web/src/components/monitoring/TingYunScript.tsx"},{id:"none",name:"None",region:"both",envVars:[],docs:""}],yi=Gt.reduce((e,t)=>(e[t.region]||(e[t.region]=[]),e[t.region].push(t),e),{});function Wt(e){return Gt.find(t=>t.id===e)}function ro(e){be.mkdirSync(e,{recursive:!0})}function B(e,t){ro(C.dirname(e)),be.writeFileSync(e,t)}function Me(e,t){let n=C.join(e,".env.example");be.existsSync(n)?be.appendFileSync(n,`
|
|
1600
|
+
`+t):be.writeFileSync(n,t)}function oo(e){let t=`import * as Sentry from "@sentry/nextjs";
|
|
1601
|
+
|
|
1602
|
+
Sentry.init({
|
|
1603
|
+
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
1604
|
+
tracesSampleRate: 1.0,
|
|
1605
|
+
replaysOnErrorSampleRate: 1.0,
|
|
1606
|
+
replaysSessionSampleRate: 0.1,
|
|
1607
|
+
integrations: [
|
|
1608
|
+
Sentry.replayIntegration({
|
|
1609
|
+
maskAllText: true,
|
|
1610
|
+
blockAllMedia: true,
|
|
1611
|
+
}),
|
|
1612
|
+
],
|
|
1613
|
+
environment: process.env.NODE_ENV,
|
|
1614
|
+
});
|
|
1615
|
+
`,n=`import * as Sentry from "@sentry/nextjs";
|
|
1616
|
+
|
|
1617
|
+
Sentry.init({
|
|
1618
|
+
dsn: process.env.SENTRY_DSN ?? process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
1619
|
+
tracesSampleRate: 1.0,
|
|
1620
|
+
environment: process.env.NODE_ENV,
|
|
1621
|
+
});
|
|
1622
|
+
`,r=`import * as Sentry from "@sentry/nextjs";
|
|
1623
|
+
|
|
1624
|
+
Sentry.init({
|
|
1625
|
+
dsn: process.env.SENTRY_DSN ?? process.env.NEXT_PUBLIC_SENTRY_DSN,
|
|
1626
|
+
tracesSampleRate: 1.0,
|
|
1627
|
+
environment: process.env.NODE_ENV,
|
|
1628
|
+
});
|
|
1629
|
+
`,o=`/**
|
|
1630
|
+
* Next.js instrumentation hook \u2014 wires Sentry into Node and Edge runtimes.
|
|
1631
|
+
* See https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
|
|
1632
|
+
*/
|
|
1633
|
+
export async function register() {
|
|
1634
|
+
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
1635
|
+
await import("./sentry.server.config");
|
|
1636
|
+
}
|
|
1637
|
+
if (process.env.NEXT_RUNTIME === "edge") {
|
|
1638
|
+
await import("./sentry.edge.config");
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
export { captureRequestError as onRequestError } from "@sentry/nextjs";
|
|
1643
|
+
`;B(C.join(e,"sentry.client.config.ts"),t),B(C.join(e,"sentry.server.config.ts"),n),B(C.join(e,"sentry.edge.config.ts"),r),B(C.join(e,"instrumentation.ts"),o)}function so(){return`# Sentry \u2014 error tracking + performance monitoring
|
|
1644
|
+
# Get your DSN from https://sentry.io/settings/projects/<project>/keys/
|
|
1645
|
+
SENTRY_DSN=
|
|
1646
|
+
NEXT_PUBLIC_SENTRY_DSN=
|
|
1647
|
+
SENTRY_ORG=
|
|
1648
|
+
SENTRY_PROJECT=
|
|
1649
|
+
SENTRY_AUTH_TOKEN=
|
|
1650
|
+
`}function io(e){let t=`/**
|
|
1651
|
+
* Next.js instrumentation hook \u2014 wires Datadog APM (dd-trace) into Node runtime.
|
|
1652
|
+
* Edge runtime is not supported by dd-trace; only nodejs is initialized.
|
|
1653
|
+
*/
|
|
1654
|
+
export async function register() {
|
|
1655
|
+
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
1656
|
+
const tracer = (await import("dd-trace")).default;
|
|
1657
|
+
tracer.init({
|
|
1658
|
+
service: process.env.DD_SERVICE ?? "sailor-web",
|
|
1659
|
+
env: process.env.DD_ENV ?? process.env.NODE_ENV,
|
|
1660
|
+
version: process.env.DD_VERSION,
|
|
1661
|
+
logInjection: true,
|
|
1662
|
+
runtimeMetrics: true,
|
|
1663
|
+
profiling: true,
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
`,n=`"use client";
|
|
1668
|
+
/**
|
|
1669
|
+
* Datadog RUM (Real User Monitoring) \u2014 browser-side tracking.
|
|
1670
|
+
* Initialize once in your root layout via <DatadogRumProvider />.
|
|
1671
|
+
*/
|
|
1672
|
+
import { datadogRum } from "@datadog/browser-rum";
|
|
1673
|
+
|
|
1674
|
+
let initialized = false;
|
|
1675
|
+
|
|
1676
|
+
export function initDatadogRum(): void {
|
|
1677
|
+
if (initialized || typeof window === "undefined") return;
|
|
1678
|
+
const applicationId = process.env.NEXT_PUBLIC_DD_APPLICATION_ID;
|
|
1679
|
+
const clientToken = process.env.NEXT_PUBLIC_DD_CLIENT_TOKEN;
|
|
1680
|
+
if (!applicationId || !clientToken) return;
|
|
1681
|
+
|
|
1682
|
+
datadogRum.init({
|
|
1683
|
+
applicationId,
|
|
1684
|
+
clientToken,
|
|
1685
|
+
site: process.env.NEXT_PUBLIC_DD_SITE ?? "datadoghq.com",
|
|
1686
|
+
service: process.env.NEXT_PUBLIC_DD_SERVICE ?? "sailor-web",
|
|
1687
|
+
env: process.env.NEXT_PUBLIC_DD_ENV ?? "production",
|
|
1688
|
+
version: process.env.NEXT_PUBLIC_DD_VERSION,
|
|
1689
|
+
sessionSampleRate: 100,
|
|
1690
|
+
sessionReplaySampleRate: 20,
|
|
1691
|
+
trackUserInteractions: true,
|
|
1692
|
+
trackResources: true,
|
|
1693
|
+
trackLongTasks: true,
|
|
1694
|
+
defaultPrivacyLevel: "mask-user-input",
|
|
1695
|
+
});
|
|
1696
|
+
initialized = true;
|
|
1697
|
+
}
|
|
1698
|
+
`;B(C.join(e,"instrumentation.ts"),t),B(C.join(e,"src","lib","monitoring","datadog-rum.ts"),n)}function ao(){return`# Datadog \u2014 APM + RUM
|
|
1699
|
+
# Server APM (dd-trace)
|
|
1700
|
+
DD_API_KEY=
|
|
1701
|
+
DD_APP_KEY=
|
|
1702
|
+
DD_SITE=datadoghq.com
|
|
1703
|
+
DD_SERVICE=sailor-web
|
|
1704
|
+
DD_ENV=production
|
|
1705
|
+
DD_VERSION=
|
|
1706
|
+
# Browser RUM
|
|
1707
|
+
NEXT_PUBLIC_DD_APPLICATION_ID=
|
|
1708
|
+
NEXT_PUBLIC_DD_CLIENT_TOKEN=
|
|
1709
|
+
NEXT_PUBLIC_DD_SITE=datadoghq.com
|
|
1710
|
+
NEXT_PUBLIC_DD_SERVICE=sailor-web
|
|
1711
|
+
NEXT_PUBLIC_DD_ENV=production
|
|
1712
|
+
NEXT_PUBLIC_DD_VERSION=
|
|
1713
|
+
`}function co(e){let t=`/**
|
|
1714
|
+
* Bugsnag server-side initialization.
|
|
1715
|
+
* Import this once at the top of your server entrypoint or a shared init module.
|
|
1716
|
+
*/
|
|
1717
|
+
import Bugsnag from "@bugsnag/js";
|
|
1718
|
+
|
|
1719
|
+
let started = false;
|
|
1720
|
+
|
|
1721
|
+
export function startBugsnagServer(): void {
|
|
1722
|
+
if (started) return;
|
|
1723
|
+
const apiKey = process.env.BUGSNAG_API_KEY;
|
|
1724
|
+
if (!apiKey) return;
|
|
1725
|
+
|
|
1726
|
+
Bugsnag.start({
|
|
1727
|
+
apiKey,
|
|
1728
|
+
releaseStage: process.env.NODE_ENV,
|
|
1729
|
+
appVersion: process.env.APP_VERSION,
|
|
1730
|
+
});
|
|
1731
|
+
started = true;
|
|
1732
|
+
}
|
|
1733
|
+
`,n=`"use client";
|
|
1734
|
+
/**
|
|
1735
|
+
* Bugsnag browser initialization \u2014 wrap your app root with the returned provider.
|
|
1736
|
+
*/
|
|
1737
|
+
import Bugsnag from "@bugsnag/js";
|
|
1738
|
+
import BugsnagPluginReact from "@bugsnag/plugin-react";
|
|
1739
|
+
import React from "react";
|
|
1740
|
+
|
|
1741
|
+
let errorBoundary: React.ComponentType<{
|
|
1742
|
+
children: React.ReactNode;
|
|
1743
|
+
FallbackComponent?: React.ComponentType<{ error: Error }>;
|
|
1744
|
+
}> | null = null;
|
|
1745
|
+
|
|
1746
|
+
export function getBugsnagErrorBoundary() {
|
|
1747
|
+
if (errorBoundary) return errorBoundary;
|
|
1748
|
+
const apiKey = process.env.NEXT_PUBLIC_BUGSNAG_API_KEY;
|
|
1749
|
+
if (!apiKey || typeof window === "undefined") return null;
|
|
1750
|
+
|
|
1751
|
+
Bugsnag.start({
|
|
1752
|
+
apiKey,
|
|
1753
|
+
plugins: [new BugsnagPluginReact()],
|
|
1754
|
+
releaseStage: process.env.NODE_ENV,
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
const plugin = Bugsnag.getPlugin("react");
|
|
1758
|
+
if (!plugin) return null;
|
|
1759
|
+
errorBoundary = plugin.createErrorBoundary(React) as typeof errorBoundary;
|
|
1760
|
+
return errorBoundary;
|
|
1761
|
+
}
|
|
1762
|
+
`;B(C.join(e,"src","lib","monitoring","bugsnag.server.ts"),t),B(C.join(e,"src","lib","monitoring","bugsnag.client.tsx"),n)}function po(){return`# Bugsnag \u2014 error tracking
|
|
1763
|
+
BUGSNAG_API_KEY=
|
|
1764
|
+
NEXT_PUBLIC_BUGSNAG_API_KEY=
|
|
1765
|
+
APP_VERSION=
|
|
1766
|
+
`}function lo(e){B(C.join(e,"src","components","monitoring","ArmsScript.tsx"),`"use client";
|
|
1767
|
+
/**
|
|
1768
|
+
* \u963F\u91CC\u4E91 ARMS (Application Real-time Monitoring Service) browser agent.
|
|
1769
|
+
* Injects the official JS agent via next/script.
|
|
1770
|
+
* Docs: https://help.aliyun.com/document_detail/58655.html
|
|
1771
|
+
*/
|
|
1772
|
+
import Script from "next/script";
|
|
1773
|
+
|
|
1774
|
+
export function ArmsScript() {
|
|
1775
|
+
const pid = process.env.NEXT_PUBLIC_ARMS_PID;
|
|
1776
|
+
if (!pid) return null;
|
|
1777
|
+
return (
|
|
1778
|
+
<Script
|
|
1779
|
+
id="aliyun-arms"
|
|
1780
|
+
strategy="afterInteractive"
|
|
1781
|
+
dangerouslySetInnerHTML={{
|
|
1782
|
+
__html: \`!(function(c,b,d,a){c[a]||(c[a]={});c[a].config={pid:"\${pid}",imgUrl:"https://arms-retcode.aliyuncs.com/r.png?",sendResource:true,enableLinkTrace:true,behavior:true};with(b)with(body)with(insertBefore(createElement("script"),firstChild))setAttribute("crossorigin","",src=d)})(window,document,"https://retcode.alicdn.com/retcode/bl.js","__bl");\`,
|
|
1783
|
+
}}
|
|
1784
|
+
/>
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
`)}function uo(){return`# \u963F\u91CC\u4E91 ARMS \u2014 APM (\u4E2D\u56FD\u533A)
|
|
1788
|
+
ALIYUN_ARMS_LICENSE_KEY=
|
|
1789
|
+
ALIYUN_ARMS_APP_NAME=
|
|
1790
|
+
NEXT_PUBLIC_ARMS_PID=
|
|
1791
|
+
`}function mo(e){B(C.join(e,"src","components","monitoring","TingYunScript.tsx"),`"use client";
|
|
1792
|
+
/**
|
|
1793
|
+
* \u542C\u4E91 Browser Agent (\u524D\u7AEF\u6027\u80FD\u76D1\u63A7).
|
|
1794
|
+
* Docs: https://www.tingyun.com/docs
|
|
1795
|
+
*/
|
|
1796
|
+
import Script from "next/script";
|
|
1797
|
+
|
|
1798
|
+
export function TingYunScript() {
|
|
1799
|
+
const appKey = process.env.NEXT_PUBLIC_TINGYUN_APP_KEY;
|
|
1800
|
+
if (!appKey) return null;
|
|
1801
|
+
return (
|
|
1802
|
+
<Script
|
|
1803
|
+
id="tingyun-browser"
|
|
1804
|
+
strategy="afterInteractive"
|
|
1805
|
+
src={\`https://rs.tingyun.com/rum/\${appKey}.js\`}
|
|
1806
|
+
/>
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
`)}function go(){return`# \u542C\u4E91 TingYun \u2014 APM (\u4E2D\u56FD\u533A)
|
|
1810
|
+
TINGYUN_LICENSE_KEY=
|
|
1811
|
+
NEXT_PUBLIC_TINGYUN_APP_KEY=
|
|
1812
|
+
`}async function Xt(e,t,n="global"){let r=Wt(t);if(!r||r.id==="none")return;let o=C.join(e,"apps","web");if(!be.existsSync(o))return;let s=r.id;try{switch(s){case"sentry":oo(o),Me(e,so());return;case"datadog":io(o),Me(e,ao());return;case"bugsnag":co(o),Me(e,po());return;case"aliyun-arms":lo(o),Me(e,uo());return;case"tingyun":mo(o),Me(e,go());return;default:return}}catch(a){throw console.error(`Failed to apply monitoring selection "${t}":`,a),new Error(`Monitoring scaffold for "${t}" failed \u2014 see stderr for details.`)}}import st from"fs";import fo from"path";async function Ht(e,t){let n=fo.join(e,"package.json");if(st.existsSync(n)){let r=st.readFileSync(n,"utf8"),o=JSON.parse(r);o.name=t,o.version="0.1.0",st.writeFileSync(n,JSON.stringify(o,null,2)+`
|
|
1813
|
+
`)}}import K from"fs";import je from"path";var yo=["stripe","lemonsqueezy","chinapay","polar"];function zt(e){K.existsSync(e)&&K.rmSync(e,{recursive:!0,force:!0})}function So(e){switch(e){case"stripe":return["stripe"];case"lemon":return["lemonsqueezy"];case"wechat":case"alipay":return["chinapay"]}}function Eo(e,t){let n=je.join(e,".env.example");K.existsSync(n)?K.appendFileSync(n,`
|
|
1814
|
+
`+t):K.writeFileSync(n,t)}function ho(e){switch(e){case"stripe":return`# Stripe
|
|
68
1815
|
STRIPE_SECRET_KEY=
|
|
69
1816
|
STRIPE_WEBHOOK_SECRET=
|
|
70
1817
|
`;case"lemon":return`# Lemon Squeezy
|
|
@@ -79,14 +1826,14 @@ CHINAPAY_WECHAT_ENABLED=true
|
|
|
79
1826
|
CHINAPAY_APP_ID=
|
|
80
1827
|
CHINAPAY_APP_SECRET=
|
|
81
1828
|
CHINAPAY_ALIPAY_ENABLED=true
|
|
82
|
-
`}}async function
|
|
83
|
-
`),
|
|
84
|
-
`),t)}function
|
|
85
|
-
`).filter(
|
|
86
|
-
`);return e.replace(
|
|
87
|
-
`+
|
|
88
|
-
`)}}import
|
|
89
|
-
`)}let
|
|
1829
|
+
`}}async function Jt(e,t){let n=je.join(e,"packages","billing");if(!K.existsSync(n))return;if(t==="none"){zt(n);return}let r=je.join(n,"src"),o=new Set(So(t));for(let a of yo)o.has(a)||zt(je.join(r,a));let s=je.join(r,"checkout","types.ts");if(K.existsSync(s)){let a=t==="stripe"?"stripe":t==="lemon"?"lemonsqueezy":"chinapay",c=K.readFileSync(s,"utf8"),i=c.replace(/export type CheckoutProviderType\s*=\s*[^;]+;/,`export type CheckoutProviderType = "${a}";`);i!==c&&K.writeFileSync(s,i)}Eo(e,ho(t))}import W from"fs";import Ae from"path";var en=new Set(["siliconflow","volcengine-ark","bailian","moonshot"]),it=/@sailor:([^\s*]+)/;function _o(e,t){let{providerIds:n,customEndpoint:r}=t;if(e.startsWith("provider:")){let o=e.slice(9);return{marker:e,keep:n.includes(o)}}if(e==="cn-compatible"){let o=n.some(s=>en.has(s));return{marker:e,keep:o||!!r}}if(e.startsWith("cn-instance:")){let o=e.slice(12);return{marker:e,keep:n.includes(o)}}return e==="custom-endpoint"?{marker:e,keep:!!r}:e==="registry-entries"?{marker:e,keep:!0}:{marker:e,keep:!0}}function Qt(e){return e.replace(/\s*\/\/\s*@sailor:[^\n]*$/,"")}function Zt(e){return e.replace(/\s*#\s*@sailor:[^\n]*$/,"")}function tn(e,t){let n=e.split(`
|
|
1830
|
+
`),r=[],o=e.includes("#"),s=0;for(;s<n.length;){let a=n[s],c=a.match(it);if(!c){r.push(a),s++;continue}let{marker:i,keep:d}=_o(c[1],t);if(i==="registry-entries"){r.push(o?Zt(a):Qt(a)),s++;continue}if(d){let m=o?Zt(a):Qt(a);m.trim().length>0&&r.push(m);let S=s+1;for(;S<n.length&&n[S].trim()!==""&&!it.test(n[S]);)r.push(n[S]),S++;S<n.length&&n[S].trim()===""&&(r.push(n[S]),S++),s=S}else{let m=s+1;for(;m<n.length&&n[m].trim()!==""&&!it.test(n[m]);)m++;m<n.length&&n[m].trim()===""&&m++,s=m}}return vo(r.join(`
|
|
1831
|
+
`),t)}function vo(e,t){let{providerIds:n,customEndpoint:r}=t,o=new Set;for(let d of n)["openai","anthropic","google","mistral","xai"].includes(d)&&o.add(d),o.add(d.replace(/-/g,"_")),o.add(d);r&&o.add("custom");let s=/createProviderRegistry\(\{([\s\S]*?)\}\)/,a=e.match(s);if(!a)return e;let i=a[1].split(`
|
|
1832
|
+
`).filter(d=>{let m=d.trim();if(m===""||m.startsWith("//"))return!0;let S=m.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*,?$/);return S?o.has(S[1]):!0}).join(`
|
|
1833
|
+
`);return e.replace(s,`createProviderRegistry({${i}})`)}function Io(e,t){let n=Ae.join(t,"registry.ts.template"),r=W.readFileSync(n,"utf-8");return tn(r,e)}function Po(e,t){let n=Ae.join(t,".env.example.template"),r=W.readFileSync(n,"utf-8");return tn(r,e)}async function nn(e,t,n){let r=Io(t,n),o=Po(t,n),s=Ae.join(e,"packages/ai-sdk/src");await W.promises.mkdir(s,{recursive:!0}),await W.promises.writeFile(Ae.join(s,"registry.ts"),r),await W.promises.appendFile(Ae.join(e,".env.example"),`
|
|
1834
|
+
`+o);let a={ai:"latest"},{providerIds:c,customEndpoint:i}=t;(c.includes("openai")||i||c.some(m=>en.has(m)))&&(a["@ai-sdk/openai"]="latest",a["@ai-sdk/openai-compatible"]="latest"),c.includes("anthropic")&&(a["@ai-sdk/anthropic"]="latest"),c.includes("google")&&(a["@ai-sdk/google"]="latest"),c.includes("mistral")&&(a["@ai-sdk/mistral"]="latest"),c.includes("xai")&&(a["@ai-sdk/openai"]="latest");let d=Ae.join(e,"package.json");if(W.existsSync(d)){let m=JSON.parse(await W.promises.readFile(d,"utf-8"));m.dependencies||(m.dependencies={});for(let[S,me]of Object.entries(a))m.dependencies[S]=me;await W.promises.writeFile(d,JSON.stringify(m,null,2)+`
|
|
1835
|
+
`)}}import u from"fs";import E from"path";function rn(e,t){if(!u.existsSync(e))return;let n=u.readdirSync(e);for(let r of n){let o=E.join(e,r);u.statSync(o).isDirectory()?rn(o,t):t(o)}}async function on(e,t){let n=E.join(e,"package.json");if(!u.existsSync(n))return;let r=JSON.parse(u.readFileSync(n,"utf8"));if(t.orm!=="prisma"){let o=E.join(e,"packages","db");if(u.existsSync(o)){let s=E.join(o,"prisma");u.existsSync(s)&&u.rmSync(s,{recursive:!0,force:!0});let a=E.join(o,"package.json");if(u.existsSync(a)){let i=JSON.parse(u.readFileSync(a,"utf8"));i.dependencies&&delete i.dependencies["@prisma/client"],i.devDependencies&&delete i.devDependencies.prisma,u.writeFileSync(a,JSON.stringify(i,null,2)+`
|
|
1836
|
+
`)}let c=E.join(o,"src","index.ts");u.existsSync(c)&&u.writeFileSync(c,`
|
|
90
1837
|
export const prisma = {} as any;
|
|
91
1838
|
export const db = {} as any;
|
|
92
1839
|
export type PrismaClient = any;
|
|
@@ -132,7 +1879,7 @@ export type UsageAggregate = any;
|
|
|
132
1879
|
export type UsageLedgerEntry = any;
|
|
133
1880
|
export type UsageType = any;
|
|
134
1881
|
export type UsageLedgerSource = any;
|
|
135
|
-
`)}
|
|
1882
|
+
`)}r.scripts?.postinstall&&(r.scripts.postinstall=r.scripts.postinstall.replace(/prisma generate/g,'echo "ORM Disabled"'))}if(!t.i18n){let o=E.join(e,"apps");if(u.existsSync(o)){let s=u.readdirSync(o);for(let a of s){let c=E.join(o,a);if(!u.statSync(c).isDirectory())continue;let i=E.join(c,"src","app");if(!u.existsSync(i))continue;["[lang]","[locale]"].forEach(f=>{let p=E.join(i,f);if(u.existsSync(p)){let b=u.readdirSync(p);for(let Fe of b)u.renameSync(E.join(p,Fe),E.join(i,Fe));u.rmdirSync(p)}}),["src/middleware.ts","middleware.ts"].forEach(f=>{let p=E.join(c,f);u.existsSync(p)&&u.rmSync(p)});let d=E.join(c,"src","lib");u.existsSync(d)||u.mkdirSync(d,{recursive:!0});let m=E.join(d,"mock-i18n.ts");u.writeFileSync(m,`
|
|
136
1883
|
import React from 'react';
|
|
137
1884
|
export function useTranslations(namespace?: string): any { return (key: string) => key; }
|
|
138
1885
|
export function useLocale() { return 'en'; }
|
|
@@ -150,7 +1897,7 @@ export const useRouter = () => ({ push: (...args: any[]) => {}, replace: (...arg
|
|
|
150
1897
|
export default function createMiddleware(...args: any[]) { return (req: any) => req; }
|
|
151
1898
|
export function createNavigation(...args: any[]) { return { Link, redirect, usePathname, useRouter, getPathname: () => '/' }; }
|
|
152
1899
|
export function defineRouting<T>(config: T): T { return config; }
|
|
153
|
-
`);let
|
|
1900
|
+
`);let me=E.join(d,"mock-i18n.d.ts");u.writeFileSync(me,`declare module '@/lib/mock-i18n' {
|
|
154
1901
|
export function useTranslations(namespace?: string): any;
|
|
155
1902
|
export function useLocale(): string;
|
|
156
1903
|
export const NextIntlClientProvider: React.FC<any>;
|
|
@@ -168,30 +1915,969 @@ export function defineRouting<T>(config: T): T { return config; }
|
|
|
168
1915
|
export function createNavigation(...args: any[]): any;
|
|
169
1916
|
export function defineRouting<T>(config: T): T;
|
|
170
1917
|
}
|
|
171
|
-
`),
|
|
1918
|
+
`),u.writeFileSync(E.join(d,"mock-i18n-index.ts"),`
|
|
172
1919
|
export * from './mock-i18n';
|
|
173
1920
|
export { default } from './mock-i18n';
|
|
174
|
-
`);let
|
|
1921
|
+
`);let ge=E.join(c,"tsconfig.json");if(u.existsSync(ge))try{let f=u.readFileSync(ge,"utf8");if(f.includes('"paths": {')){let p=f.replace(/"paths":\s*\{/,`"paths": {
|
|
175
1922
|
"next-intl": ["./src/lib/mock-i18n"],
|
|
176
|
-
"next-intl/*": ["./src/lib/mock-i18n"],`);
|
|
177
|
-
`)}}}}
|
|
178
|
-
`)}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
1923
|
+
"next-intl/*": ["./src/lib/mock-i18n"],`);u.writeFileSync(ge,p)}}catch{}let Ce=E.join(c,"src");u.existsSync(Ce)&&rn(Ce,f=>{if(!f.endsWith(".ts")&&!f.endsWith(".tsx"))return;let p=u.readFileSync(f,"utf8"),b=!1;f.endsWith("src/app/layout.tsx")&&p.includes("../providers")&&(p=p.replace(/from\s+['"]\.\.\/providers['"]/g,"from './providers'"),b=!0),p=p.replace(/params:\s*Promise<\{[^}]*(lang|locale)[^}]*\}>/g,"params: any"),p=p.replace(/\(\{\s*params\s*\}\s*:\s*\{\s*params:\s*any\s*\}\)/g,"({ params }: any)"),p.match(/locale\s*:\s*any/)&&(p=p.replace(/locale\s*:\s*any/g,"locale: string"),b=!0),p.match(/lang\s*:\s*any/)&&(p=p.replace(/lang\s*:\s*any/g,"lang: string"),b=!0),p.match(/requestLocale\s*:\s*any/)&&(p=p.replace(/requestLocale\s*:\s*any/g,"requestLocale: string"),b=!0),p.match(/\(locale\s*=\s*(.*)\)/)&&(p=p.replace(/\(locale\s*=\s*(.*)\)/g,"() "),b=!0),p.match(/export\s+default\s+async\s+function\s+LangLayout/)&&(p=p.replace(/export\s+default\s+async\s+function\s+LangLayout\s*\(\s*\{\s*children\s*,\s*params\s*\}\s*:\s*LangLayoutProps\s*\)/g,"export default async function LangLayout({ children }: { children: React.ReactNode })"),b=!0),(f.endsWith("src/app/layout.tsx")||f.endsWith("src/app/sitemap.ts")||f.endsWith("src/app/robots.ts")||f.endsWith("src/components/ui/locale-switcher.tsx")||f.endsWith("src/app/(marketing)/blog/[slug]/page.tsx"))&&(p=p.replace(/const\s+\{\s*lang\s*\}\s*=\s*(await\s+)?params;?/g,'const lang = "en";'),p=p.replace(/const\s+\{\s*locale\s*\}\s*=\s*(await\s+)?params;?/g,'const locale = "en";'),p=p.replace(/const\s+locale\s*=\s*lang\s+as\s+Locale;?/g,'const locale = "en";'),p=p.replace(/\(l\)\s*=>/g,"(l: any) =>"),b=!0),f.endsWith("src/i18n/request.ts")&&(p=p.replace(/async\s*\(\{\s*requestLocale\s*\}\)/g,"async ({ requestLocale }: any)"),b=!0),(p.match(/locale\s*:/)||p.match(/lang\s*:/))&&(b=!0),b&&u.writeFileSync(f,p)});let we=E.join(c,"package.json");if(u.existsSync(we)){let f=JSON.parse(u.readFileSync(we,"utf8"));f.dependencies&&delete f.dependencies["next-intl"],u.writeFileSync(we,JSON.stringify(f,null,2)+`
|
|
1924
|
+
`)}}}}u.writeFileSync(n,JSON.stringify(r,null,2)+`
|
|
1925
|
+
`)}import X from"fs";import ae from"path";function bo(e){let t=[ae.join(e,"packages","db"),ae.join(e,"apps","web")];for(let n of t){let r=ae.join(n,"prisma","schema.prisma");if(X.existsSync(r))return{packageDir:n,seedPath:ae.join(n,"prisma","seed.ts"),packageJsonPath:ae.join(n,"package.json")}}return null}function Ao(e){try{let t=X.readFileSync(e,"utf8"),n=new Set,r=/^\s*model\s+(\w+)\s*\{/gm,o=r.exec(t);for(;o;)n.add(o[1]),o=r.exec(t);return n}catch{return new Set}}function Co(e,t){return!t||e==="none"?` // Skipping admin user \u2014 auth=${JSON.stringify(e)} (or no User model).`:e==="clerk"?` try {
|
|
1926
|
+
const admin = await prisma.user.create({
|
|
1927
|
+
data: {
|
|
1928
|
+
clerkId: "user_mock_admin",
|
|
1929
|
+
email: "admin@example.com",
|
|
1930
|
+
name: "Admin",
|
|
1931
|
+
},
|
|
1932
|
+
});
|
|
1933
|
+
console.log("\\u2713 Created admin user:", admin.email);
|
|
1934
|
+
} catch (err) {
|
|
1935
|
+
console.warn("\\u26A0 Admin user creation skipped:", (err as Error).message);
|
|
1936
|
+
}`:e==="betterauth"?` try {
|
|
1937
|
+
// NOTE: in production hash passwords with the project's password hasher
|
|
1938
|
+
// (e.g. argon2id via @nebutra/auth). This is a scaffold placeholder.
|
|
1939
|
+
const admin = await prisma.user.create({
|
|
1940
|
+
data: {
|
|
1941
|
+
email: "admin@example.com",
|
|
1942
|
+
name: "Admin",
|
|
1943
|
+
hashedPassword: "CHANGE_ME_scaffold_placeholder",
|
|
1944
|
+
},
|
|
1945
|
+
});
|
|
1946
|
+
console.log("\\u2713 Created admin user:", admin.email);
|
|
1947
|
+
} catch (err) {
|
|
1948
|
+
console.warn("\\u26A0 Admin user creation skipped:", (err as Error).message);
|
|
1949
|
+
}`:` try {
|
|
1950
|
+
const admin = await prisma.user.create({
|
|
1951
|
+
data: { email: "admin@example.com", name: "Admin" },
|
|
1952
|
+
});
|
|
1953
|
+
console.log("\\u2713 Created admin user:", admin.email);
|
|
1954
|
+
} catch (err) {
|
|
1955
|
+
console.warn("\\u26A0 Admin user creation skipped:", (err as Error).message);
|
|
1956
|
+
}`}function wo(e,t){let n=t.has("Tenant"),r=t.has("User"),o=t.has("Project"),s=Co(e,r),a=n?` let tenants: Array<{ id: string; name: string }> = [];
|
|
1957
|
+
try {
|
|
1958
|
+
tenants = await Promise.all([
|
|
1959
|
+
prisma.tenant.create({
|
|
1960
|
+
data: { name: "Acme Corp", slug: "acme", plan: "pro" },
|
|
1961
|
+
}),
|
|
1962
|
+
prisma.tenant.create({
|
|
1963
|
+
data: { name: "Widget Co", slug: "widget", plan: "free" },
|
|
1964
|
+
}),
|
|
1965
|
+
prisma.tenant.create({
|
|
1966
|
+
data: { name: "Startup Inc", slug: "startup", plan: "startup" },
|
|
1967
|
+
}),
|
|
1968
|
+
]);
|
|
1969
|
+
console.log(\`\\u2713 Created \${tenants.length} tenants\`);
|
|
1970
|
+
} catch (err) {
|
|
1971
|
+
console.warn("\\u26A0 Tenant creation skipped:", (err as Error).message);
|
|
1972
|
+
}`:` const tenants: Array<{ id: string; name: string }> = [];
|
|
1973
|
+
console.log("\\u2139 No Tenant model in schema \u2014 skipping tenant seed.");`,c=o&&n?` for (const tenant of tenants) {
|
|
1974
|
+
try {
|
|
1975
|
+
await prisma.project.createMany({
|
|
1976
|
+
data: [
|
|
1977
|
+
{ name: \`\${tenant.name} - Sample Project 1\`, tenantId: tenant.id },
|
|
1978
|
+
{ name: \`\${tenant.name} - Sample Project 2\`, tenantId: tenant.id },
|
|
1979
|
+
],
|
|
1980
|
+
});
|
|
1981
|
+
} catch (err) {
|
|
1982
|
+
console.warn(
|
|
1983
|
+
\`\\u26A0 Project seed skipped for \${tenant.name}:\`,
|
|
1984
|
+
(err as Error).message,
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
}`:' console.log("\\u2139 No Project model (or no tenants) \u2014 skipping project seed.");';return`/**
|
|
1988
|
+
* Database seed \u2014 generated by create-sailor.
|
|
1989
|
+
*
|
|
1990
|
+
* Run with: pnpm db:seed
|
|
1991
|
+
*
|
|
1992
|
+
* Auth provider: ${e}
|
|
1993
|
+
* Detected models at scaffold time: ${[...t].sort().join(", ")||"(none)"}
|
|
1994
|
+
*
|
|
1995
|
+
* Each block is wrapped in try/catch so evolving the schema does not
|
|
1996
|
+
* break the seed \u2014 missing models log a warning and move on.
|
|
1997
|
+
*/
|
|
1998
|
+
import { PrismaClient } from "@prisma/client";
|
|
1999
|
+
|
|
2000
|
+
const prisma = new PrismaClient();
|
|
2001
|
+
|
|
2002
|
+
async function main(): Promise<void> {
|
|
2003
|
+
console.log("\\uD83C\\uDF31 Seeding database...");
|
|
2004
|
+
|
|
2005
|
+
// --- Tenants ---------------------------------------------------------
|
|
2006
|
+
${a}
|
|
2007
|
+
|
|
2008
|
+
// --- Admin user ------------------------------------------------------
|
|
2009
|
+
${s}
|
|
2010
|
+
|
|
2011
|
+
// --- Sample projects per tenant --------------------------------------
|
|
2012
|
+
${c}
|
|
2013
|
+
|
|
2014
|
+
console.log("\\u2713 Seed complete");
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
main()
|
|
2018
|
+
.catch((err) => {
|
|
2019
|
+
console.error("Seed failed:", err);
|
|
2020
|
+
process.exitCode = 1;
|
|
2021
|
+
})
|
|
2022
|
+
.finally(async () => {
|
|
2023
|
+
await prisma.$disconnect();
|
|
2024
|
+
});
|
|
2025
|
+
`}function No(e){if(X.existsSync(e))try{let t=X.readFileSync(e,"utf8"),n=JSON.parse(t),r=n.scripts??{},o=!1;r["db:seed"]||(r["db:seed"]="prisma db seed",o=!0);let s=n.prisma??{};if(s.seed||(s.seed="tsx prisma/seed.ts",o=!0),!o)return;let a={...n,scripts:r,prisma:s};X.writeFileSync(e,JSON.stringify(a,null,2)+`
|
|
2026
|
+
`)}catch(t){let n=t instanceof Error?t.message:String(t);console.warn(`[create-sailor] Failed to patch seed scripts in ${e}: ${n}`)}}async function sn(e,t){let n=bo(e);if(n)try{let r=ae.join(n.packageDir,"prisma","schema.prisma"),o=Ao(r),s=wo(t,o),a=ae.dirname(n.seedPath);X.existsSync(a)||X.mkdirSync(a,{recursive:!0}),X.writeFileSync(n.seedPath,s),No(n.packageJsonPath)}catch(r){let o=r instanceof Error?r.message:String(r);throw console.error(`[create-sailor] Failed to generate seed data: ${o}`),new Error(`Unable to write prisma/seed.ts: ${o}`)}}import H from"fs";import ce from"path";var He=[{id:"twilio",name:"Twilio",region:"global",envVars:["TWILIO_ACCOUNT_SID","TWILIO_AUTH_TOKEN","TWILIO_FROM_NUMBER"],docs:"https://www.twilio.com/docs/sms",pkgs:["twilio"]},{id:"messagebird",name:"MessageBird (Bird)",region:"global",envVars:["MESSAGEBIRD_ACCESS_KEY","MESSAGEBIRD_ORIGINATOR"],docs:"https://developers.messagebird.com/api/sms-messaging/",pkgs:["messagebird"]},{id:"plivo",name:"Plivo",region:"global",envVars:["PLIVO_AUTH_ID","PLIVO_AUTH_TOKEN","PLIVO_FROM_NUMBER"],docs:"https://www.plivo.com/docs/sms/",pkgs:["plivo"]},{id:"aliyun-sms",name:"\u963F\u91CC\u4E91\u77ED\u4FE1 Aliyun SMS",region:"cn",envVars:["ALIYUN_SMS_ACCESS_KEY","ALIYUN_SMS_ACCESS_SECRET","ALIYUN_SMS_SIGN_NAME","ALIYUN_SMS_TEMPLATE_CODE","ALIYUN_SMS_ENDPOINT"],docs:"https://help.aliyun.com/product/44282.html",pkgs:["@alicloud/dysmsapi20170525","@alicloud/openapi-client"]},{id:"tencent-sms",name:"\u817E\u8BAF\u4E91\u77ED\u4FE1 Tencent SMS",region:"cn",envVars:["TENCENT_SMS_SECRET_ID","TENCENT_SMS_SECRET_KEY","TENCENT_SMS_SDK_APP_ID","TENCENT_SMS_SIGN_NAME","TENCENT_SMS_TEMPLATE_ID","TENCENT_SMS_REGION"],docs:"https://cloud.tencent.com/document/product/382",pkgs:["tencentcloud-sdk-nodejs-sms"]},{id:"yunpian",name:"\u4E91\u7247 Yunpian",region:"cn",envVars:["YUNPIAN_API_KEY","YUNPIAN_SIGN"],docs:"https://www.yunpian.com/official/document/sms/zh_CN/introduction_product_intro",pkgs:["yunpian-nodejs-sdk"]},{id:"none",name:"None",region:"both",envVars:[],docs:""}],ji=He.reduce((e,t)=>(e[t.region]||(e[t.region]=[]),e[t.region].push(t),e),{});function an(e){return He.find(t=>t.id===e)}var To="@nebutra/sms",xo={twilio:"^5.3.6",messagebird:"^4.0.1",plivo:"^4.73.0","@alicloud/dysmsapi20170525":"^2.0.24","@alicloud/openapi-client":"^0.4.12","tencentcloud-sdk-nodejs-sms":"^4.0.970","yunpian-nodejs-sdk":"^0.0.2"};function at(e,t){H.mkdirSync(ce.dirname(e),{recursive:!0}),H.writeFileSync(e,t)}function Ro(e,t){let n=ce.join(e,".env.example");H.existsSync(n)?H.appendFileSync(n,`
|
|
2027
|
+
`+t):H.writeFileSync(n,t)}function ko(e){if(e.envVars.length===0)return"";let t=`# SMS provider: ${e.name}`,n=e.envVars.map(r=>`${r}=`).join(`
|
|
2028
|
+
`);return`${t}
|
|
2029
|
+
${n}
|
|
2030
|
+
`}var Oo=`/**
|
|
2031
|
+
* Inputs for the \`sendSms\` wrapper. Keep this surface portable across
|
|
2032
|
+
* providers \u2014 provider-specific options belong in the wrapper itself.
|
|
2033
|
+
*/
|
|
2034
|
+
export interface SendSmsInput {
|
|
2035
|
+
/** E.164 (+1415...) for global providers, or 11-digit string for CN providers. */
|
|
2036
|
+
to: string;
|
|
2037
|
+
/** Template code / ID registered with the provider. */
|
|
2038
|
+
templateCode?: string;
|
|
2039
|
+
/** Template variables \u2014 usually { code: "123456" } or { name, orderId, ... }. */
|
|
2040
|
+
params?: Record<string, string | number>;
|
|
2041
|
+
/** Free-form text body (only honored by providers that allow ad-hoc content). */
|
|
2042
|
+
body?: string;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
export interface SendSmsResult {
|
|
2046
|
+
messageId?: string;
|
|
2047
|
+
provider: string;
|
|
2048
|
+
raw?: unknown;
|
|
2049
|
+
}
|
|
2050
|
+
`;function Uo(){return`import twilio from "twilio";
|
|
2051
|
+
import type { SendSmsInput, SendSmsResult } from "./types.js";
|
|
2052
|
+
|
|
2053
|
+
let client: ReturnType<typeof twilio> | null = null;
|
|
2054
|
+
|
|
2055
|
+
function getClient() {
|
|
2056
|
+
if (client) return client;
|
|
2057
|
+
const sid = process.env.TWILIO_ACCOUNT_SID;
|
|
2058
|
+
const token = process.env.TWILIO_AUTH_TOKEN;
|
|
2059
|
+
if (!sid || !token) {
|
|
2060
|
+
throw new Error("TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN must be set");
|
|
2061
|
+
}
|
|
2062
|
+
client = twilio(sid, token);
|
|
2063
|
+
return client;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
export async function sendSms(input: SendSmsInput): Promise<SendSmsResult> {
|
|
2067
|
+
const from = process.env.TWILIO_FROM_NUMBER;
|
|
2068
|
+
if (!from) throw new Error("TWILIO_FROM_NUMBER must be set");
|
|
2069
|
+
|
|
2070
|
+
const body =
|
|
2071
|
+
input.body ??
|
|
2072
|
+
Object.values(input.params ?? {}).join(" ") ??
|
|
2073
|
+
"";
|
|
2074
|
+
|
|
2075
|
+
const message = await getClient().messages.create({
|
|
2076
|
+
to: input.to,
|
|
2077
|
+
from,
|
|
2078
|
+
body,
|
|
2079
|
+
});
|
|
2080
|
+
|
|
2081
|
+
return { messageId: message.sid, provider: "twilio", raw: message };
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
export type { SendSmsInput, SendSmsResult } from "./types.js";
|
|
2085
|
+
`}function Lo(){return`import messagebird from "messagebird";
|
|
2086
|
+
import type { SendSmsInput, SendSmsResult } from "./types.js";
|
|
2087
|
+
|
|
2088
|
+
let client: ReturnType<typeof messagebird> | null = null;
|
|
2089
|
+
|
|
2090
|
+
function getClient() {
|
|
2091
|
+
if (client) return client;
|
|
2092
|
+
const key = process.env.MESSAGEBIRD_ACCESS_KEY;
|
|
2093
|
+
if (!key) throw new Error("MESSAGEBIRD_ACCESS_KEY must be set");
|
|
2094
|
+
client = messagebird(key);
|
|
2095
|
+
return client;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
export async function sendSms(input: SendSmsInput): Promise<SendSmsResult> {
|
|
2099
|
+
const originator = process.env.MESSAGEBIRD_ORIGINATOR;
|
|
2100
|
+
if (!originator) throw new Error("MESSAGEBIRD_ORIGINATOR must be set");
|
|
2101
|
+
|
|
2102
|
+
const body =
|
|
2103
|
+
input.body ??
|
|
2104
|
+
Object.values(input.params ?? {}).join(" ") ??
|
|
2105
|
+
"";
|
|
2106
|
+
|
|
2107
|
+
return await new Promise<SendSmsResult>((resolve, reject) => {
|
|
2108
|
+
getClient().messages.create(
|
|
2109
|
+
{ originator, recipients: [input.to], body },
|
|
2110
|
+
(err, response) => {
|
|
2111
|
+
if (err) return reject(err);
|
|
2112
|
+
resolve({
|
|
2113
|
+
messageId: response?.id,
|
|
2114
|
+
provider: "messagebird",
|
|
2115
|
+
raw: response,
|
|
2116
|
+
});
|
|
2117
|
+
},
|
|
2118
|
+
);
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
export type { SendSmsInput, SendSmsResult } from "./types.js";
|
|
2123
|
+
`}function Mo(){return`import plivo from "plivo";
|
|
2124
|
+
import type { SendSmsInput, SendSmsResult } from "./types.js";
|
|
2125
|
+
|
|
2126
|
+
let client: InstanceType<typeof plivo.Client> | null = null;
|
|
2127
|
+
|
|
2128
|
+
function getClient() {
|
|
2129
|
+
if (client) return client;
|
|
2130
|
+
const authId = process.env.PLIVO_AUTH_ID;
|
|
2131
|
+
const authToken = process.env.PLIVO_AUTH_TOKEN;
|
|
2132
|
+
if (!authId || !authToken) {
|
|
2133
|
+
throw new Error("PLIVO_AUTH_ID and PLIVO_AUTH_TOKEN must be set");
|
|
2134
|
+
}
|
|
2135
|
+
client = new plivo.Client(authId, authToken);
|
|
2136
|
+
return client;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
export async function sendSms(input: SendSmsInput): Promise<SendSmsResult> {
|
|
2140
|
+
const from = process.env.PLIVO_FROM_NUMBER;
|
|
2141
|
+
if (!from) throw new Error("PLIVO_FROM_NUMBER must be set");
|
|
2142
|
+
|
|
2143
|
+
const body =
|
|
2144
|
+
input.body ??
|
|
2145
|
+
Object.values(input.params ?? {}).join(" ") ??
|
|
2146
|
+
"";
|
|
2147
|
+
|
|
2148
|
+
const response = await getClient().messages.create({
|
|
2149
|
+
src: from,
|
|
2150
|
+
dst: input.to,
|
|
2151
|
+
text: body,
|
|
2152
|
+
});
|
|
2153
|
+
|
|
2154
|
+
return {
|
|
2155
|
+
messageId: response.messageUuid?.[0],
|
|
2156
|
+
provider: "plivo",
|
|
2157
|
+
raw: response,
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
export type { SendSmsInput, SendSmsResult } from "./types.js";
|
|
2162
|
+
`}function jo(){return`import Dysmsapi, { SendSmsRequest } from "@alicloud/dysmsapi20170525";
|
|
2163
|
+
import * as $OpenApi from "@alicloud/openapi-client";
|
|
2164
|
+
import type { SendSmsInput, SendSmsResult } from "./types.js";
|
|
2165
|
+
|
|
2166
|
+
let client: Dysmsapi | null = null;
|
|
2167
|
+
|
|
2168
|
+
function getClient(): Dysmsapi {
|
|
2169
|
+
if (client) return client;
|
|
2170
|
+
const accessKeyId = process.env.ALIYUN_SMS_ACCESS_KEY;
|
|
2171
|
+
const accessKeySecret = process.env.ALIYUN_SMS_ACCESS_SECRET;
|
|
2172
|
+
if (!accessKeyId || !accessKeySecret) {
|
|
2173
|
+
throw new Error(
|
|
2174
|
+
"ALIYUN_SMS_ACCESS_KEY and ALIYUN_SMS_ACCESS_SECRET must be set",
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
const config = new $OpenApi.Config({ accessKeyId, accessKeySecret });
|
|
2178
|
+
config.endpoint = process.env.ALIYUN_SMS_ENDPOINT ?? "dysmsapi.aliyuncs.com";
|
|
2179
|
+
client = new Dysmsapi(config);
|
|
2180
|
+
return client;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
export async function sendSms(input: SendSmsInput): Promise<SendSmsResult> {
|
|
2184
|
+
const signName = process.env.ALIYUN_SMS_SIGN_NAME;
|
|
2185
|
+
const templateCode = input.templateCode ?? process.env.ALIYUN_SMS_TEMPLATE_CODE;
|
|
2186
|
+
if (!signName) throw new Error("ALIYUN_SMS_SIGN_NAME must be set");
|
|
2187
|
+
if (!templateCode) throw new Error("templateCode or ALIYUN_SMS_TEMPLATE_CODE must be provided");
|
|
2188
|
+
|
|
2189
|
+
const req = new SendSmsRequest({
|
|
2190
|
+
phoneNumbers: input.to,
|
|
2191
|
+
signName,
|
|
2192
|
+
templateCode,
|
|
2193
|
+
templateParam: JSON.stringify(input.params ?? {}),
|
|
2194
|
+
});
|
|
2195
|
+
|
|
2196
|
+
const response = await getClient().sendSms(req);
|
|
2197
|
+
const body = response.body;
|
|
2198
|
+
|
|
2199
|
+
if (body?.code !== "OK") {
|
|
2200
|
+
throw new Error(\`Aliyun SMS failed: \${body?.code} \${body?.message}\`);
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
return {
|
|
2204
|
+
messageId: body?.bizId,
|
|
2205
|
+
provider: "aliyun-sms",
|
|
2206
|
+
raw: body,
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
export type { SendSmsInput, SendSmsResult } from "./types.js";
|
|
2211
|
+
`}function Do(){return`import * as tencentcloud from "tencentcloud-sdk-nodejs-sms";
|
|
2212
|
+
import type { SendSmsInput, SendSmsResult } from "./types.js";
|
|
2213
|
+
|
|
2214
|
+
const SmsClient = tencentcloud.sms.v20210111.Client;
|
|
2215
|
+
type SmsClientInstance = InstanceType<typeof SmsClient>;
|
|
2216
|
+
|
|
2217
|
+
let client: SmsClientInstance | null = null;
|
|
2218
|
+
|
|
2219
|
+
function getClient(): SmsClientInstance {
|
|
2220
|
+
if (client) return client;
|
|
2221
|
+
const secretId = process.env.TENCENT_SMS_SECRET_ID;
|
|
2222
|
+
const secretKey = process.env.TENCENT_SMS_SECRET_KEY;
|
|
2223
|
+
if (!secretId || !secretKey) {
|
|
2224
|
+
throw new Error(
|
|
2225
|
+
"TENCENT_SMS_SECRET_ID and TENCENT_SMS_SECRET_KEY must be set",
|
|
2226
|
+
);
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
client = new SmsClient({
|
|
2230
|
+
credential: { secretId, secretKey },
|
|
2231
|
+
region: process.env.TENCENT_SMS_REGION ?? "ap-guangzhou",
|
|
2232
|
+
profile: {
|
|
2233
|
+
httpProfile: { endpoint: "sms.tencentcloudapi.com" },
|
|
2234
|
+
},
|
|
2235
|
+
});
|
|
2236
|
+
return client;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
export async function sendSms(input: SendSmsInput): Promise<SendSmsResult> {
|
|
2240
|
+
const sdkAppId = process.env.TENCENT_SMS_SDK_APP_ID;
|
|
2241
|
+
const signName = process.env.TENCENT_SMS_SIGN_NAME;
|
|
2242
|
+
const templateId = input.templateCode ?? process.env.TENCENT_SMS_TEMPLATE_ID;
|
|
2243
|
+
|
|
2244
|
+
if (!sdkAppId) throw new Error("TENCENT_SMS_SDK_APP_ID must be set");
|
|
2245
|
+
if (!signName) throw new Error("TENCENT_SMS_SIGN_NAME must be set");
|
|
2246
|
+
if (!templateId) {
|
|
2247
|
+
throw new Error("templateCode or TENCENT_SMS_TEMPLATE_ID must be provided");
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
// Tencent expects phone numbers with + prefix (E.164). Also expects ordered
|
|
2251
|
+
// template params as a string array matching the template placeholders.
|
|
2252
|
+
const to = input.to.startsWith("+") ? input.to : \`+\${input.to}\`;
|
|
2253
|
+
const templateParamSet = Object.values(input.params ?? {}).map(String);
|
|
2254
|
+
|
|
2255
|
+
const response = await getClient().SendSms({
|
|
2256
|
+
SmsSdkAppId: sdkAppId,
|
|
2257
|
+
SignName: signName,
|
|
2258
|
+
TemplateId: templateId,
|
|
2259
|
+
PhoneNumberSet: [to],
|
|
2260
|
+
TemplateParamSet: templateParamSet,
|
|
2261
|
+
});
|
|
2262
|
+
|
|
2263
|
+
const first = response.SendStatusSet?.[0];
|
|
2264
|
+
if (first && first.Code !== "Ok") {
|
|
2265
|
+
throw new Error(\`Tencent SMS failed: \${first.Code} \${first.Message}\`);
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
return {
|
|
2269
|
+
messageId: first?.SerialNo,
|
|
2270
|
+
provider: "tencent-sms",
|
|
2271
|
+
raw: response,
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
export type { SendSmsInput, SendSmsResult } from "./types.js";
|
|
2276
|
+
`}function Fo(){return`// @ts-expect-error \u2014 yunpian-nodejs-sdk ships no type declarations
|
|
2277
|
+
import Yunpian from "yunpian-nodejs-sdk";
|
|
2278
|
+
import type { SendSmsInput, SendSmsResult } from "./types.js";
|
|
2279
|
+
|
|
2280
|
+
let client: { sms: { single_send(opts: unknown): Promise<unknown>; tpl_single_send(opts: unknown): Promise<unknown> } } | null =
|
|
2281
|
+
null;
|
|
2282
|
+
|
|
2283
|
+
function getClient() {
|
|
2284
|
+
if (client) return client;
|
|
2285
|
+
const apiKey = process.env.YUNPIAN_API_KEY;
|
|
2286
|
+
if (!apiKey) throw new Error("YUNPIAN_API_KEY must be set");
|
|
2287
|
+
client = new Yunpian(apiKey);
|
|
2288
|
+
return client!;
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
/**
|
|
2292
|
+
* Yunpian (\u4E91\u7247) supports two modes:
|
|
2293
|
+
* - tpl_single_send: registered template + tpl_value
|
|
2294
|
+
* - single_send: ad-hoc text (must include registered \u7B7E\u540D)
|
|
2295
|
+
*/
|
|
2296
|
+
export async function sendSms(input: SendSmsInput): Promise<SendSmsResult> {
|
|
2297
|
+
const sign = process.env.YUNPIAN_SIGN ?? "";
|
|
2298
|
+
|
|
2299
|
+
if (input.templateCode) {
|
|
2300
|
+
const tplValue = Object.entries(input.params ?? {})
|
|
2301
|
+
.map(([k, v]) => \`#\${k}#=\${encodeURIComponent(String(v))}\`)
|
|
2302
|
+
.join("&");
|
|
2303
|
+
|
|
2304
|
+
const raw = await getClient().sms.tpl_single_send({
|
|
2305
|
+
mobile: input.to,
|
|
2306
|
+
tpl_id: input.templateCode,
|
|
2307
|
+
tpl_value: tplValue,
|
|
2308
|
+
});
|
|
2309
|
+
|
|
2310
|
+
return { provider: "yunpian", raw };
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
const text = input.body
|
|
2314
|
+
? input.body.startsWith("\u3010")
|
|
2315
|
+
? input.body
|
|
2316
|
+
: \`\${sign}\${input.body}\`
|
|
2317
|
+
: \`\${sign}\${Object.values(input.params ?? {}).join(" ")}\`;
|
|
2318
|
+
|
|
2319
|
+
const raw = await getClient().sms.single_send({
|
|
2320
|
+
mobile: input.to,
|
|
2321
|
+
text,
|
|
2322
|
+
});
|
|
2323
|
+
|
|
2324
|
+
return { provider: "yunpian", raw };
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
export type { SendSmsInput, SendSmsResult } from "./types.js";
|
|
2328
|
+
`}function Bo(e){switch(e){case"twilio":return Uo();case"messagebird":return Lo();case"plivo":return Mo();case"aliyun-sms":return jo();case"tencent-sms":return Do();case"yunpian":return Fo();case"none":return`export {};
|
|
2329
|
+
`}}function Yo(e){let t={};for(let r of e.pkgs??[]){let o=xo[r];o&&(t[r]=o)}let n={name:To,version:"0.1.0",private:!0,description:`Transactional SMS via ${e.name}`,type:"module",main:"./dist/index.js",exports:{".":{import:"./dist/index.js",types:"./dist/index.d.ts"}},scripts:{build:"tsup",typecheck:"tsc --noEmit",clean:"rm -rf dist .turbo"},dependencies:t,devDependencies:{tsup:"^8.5.0",typescript:"catalog:"}};return JSON.stringify(n,null,2)+`
|
|
2330
|
+
`}async function cn(e,t,n="global"){if(t==="none")return;let r=an(t);if(!r)throw new Error(`Unknown SMS provider "${t}". Valid ids: ${He.map(a=>a.id).join(", ")}`);let o=ce.join(e,"packages","sms");if(!H.existsSync(o))return;let s=ce.join(o,"src");H.existsSync(s)&&H.rmSync(s,{recursive:!0,force:!0});try{at(ce.join(o,"package.json"),Yo(r)),at(ce.join(s,"types.ts"),Oo),at(ce.join(s,"index.ts"),Bo(r.id)),Ro(e,ko(r))}catch(a){throw console.error(`Failed to apply SMS selection "${t}":`,a),new Error(`SMS scaffold for "${t}" failed \u2014 see stderr for details.`)}}import le from"fs";import pe from"path";var ze=[{id:"r2",name:"Cloudflare R2",region:"global",envVars:["R2_ACCOUNT_ID","R2_ACCESS_KEY_ID","R2_SECRET_ACCESS_KEY","R2_BUCKET"],docs:"https://developers.cloudflare.com/r2/api/s3/api/",pkg:"@aws-sdk/client-s3",pkgVersion:"^3.658.0"},{id:"s3",name:"AWS S3",region:"global",envVars:["AWS_ACCESS_KEY_ID","AWS_SECRET_ACCESS_KEY","AWS_S3_REGION","S3_BUCKET"],docs:"https://docs.aws.amazon.com/s3/",pkg:"@aws-sdk/client-s3",pkgVersion:"^3.658.0"},{id:"supabase-storage",name:"Supabase Storage",region:"global",envVars:["SUPABASE_URL","SUPABASE_SERVICE_ROLE_KEY","SUPABASE_BUCKET"],docs:"https://supabase.com/docs/guides/storage",pkg:"@supabase/supabase-js",pkgVersion:"^2.45.4"},{id:"aliyun-oss",name:"\u963F\u91CC\u4E91 OSS",region:"cn",envVars:["ALIYUN_OSS_ACCESS_KEY","ALIYUN_OSS_ACCESS_SECRET","ALIYUN_OSS_REGION","ALIYUN_OSS_BUCKET"],docs:"https://help.aliyun.com/product/31815.html",pkg:"ali-oss",pkgVersion:"^6.21.0"},{id:"tencent-cos",name:"\u817E\u8BAF\u4E91 COS",region:"cn",envVars:["TENCENT_COS_SECRET_ID","TENCENT_COS_SECRET_KEY","TENCENT_COS_BUCKET","TENCENT_COS_REGION"],docs:"https://cloud.tencent.com/document/product/436",pkg:"cos-nodejs-sdk-v5",pkgVersion:"^2.13.7"},{id:"qiniu",name:"\u4E03\u725B\u4E91 Kodo",region:"cn",envVars:["QINIU_ACCESS_KEY","QINIU_SECRET_KEY","QINIU_BUCKET","QINIU_DOMAIN"],docs:"https://developer.qiniu.com/kodo",pkg:"qiniu",pkgVersion:"^7.14.0"},{id:"none",name:"None",region:"global",envVars:[],docs:""}];function pn(e){return ze.find(t=>t.id===e)}var Vi=ze.reduce((e,t)=>(e[t.region]||(e[t.region]=[]),e[t.region].push(t),e),{});var de="@nebutra/storage";function ct(e,t){le.mkdirSync(pe.dirname(e),{recursive:!0}),le.writeFileSync(e,t)}function Ko(e,t){let n=pe.join(e,".env.example");le.existsSync(n)?le.appendFileSync(n,`
|
|
2331
|
+
`+t):le.writeFileSync(n,t)}function Vo(e){if(e.envVars.length===0)return"";let t=`# Storage provider: ${e.name}`,n=e.envVars.map(r=>`${r}=`).join(`
|
|
2332
|
+
`);return`${t}
|
|
2333
|
+
${n}
|
|
2334
|
+
`}var $o=`/**
|
|
2335
|
+
* Inputs for the \`uploadFile\` wrapper. Keep this surface portable across
|
|
2336
|
+
* providers \u2014 provider-specific options belong in the wrapper itself.
|
|
2337
|
+
*/
|
|
2338
|
+
export interface UploadFileInput {
|
|
2339
|
+
key: string;
|
|
2340
|
+
body: Buffer | Uint8Array | string;
|
|
2341
|
+
contentType?: string;
|
|
2342
|
+
/** Optional access control. Most providers default to private. */
|
|
2343
|
+
acl?: "private" | "public-read";
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
export interface UploadFileResult {
|
|
2347
|
+
key: string;
|
|
2348
|
+
/** Public or signed URL, whichever the provider returns by default. */
|
|
2349
|
+
url?: string;
|
|
2350
|
+
raw?: unknown;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
export interface SignedUrlOptions {
|
|
2354
|
+
/** Time-to-live in seconds. Defaults to 3600 (1h). */
|
|
2355
|
+
expiresIn?: number;
|
|
2356
|
+
/** Operation \u2014 "get" for download, "put" for direct upload. */
|
|
2357
|
+
operation?: "get" | "put";
|
|
2358
|
+
contentType?: string;
|
|
2359
|
+
}
|
|
2360
|
+
`;function qo(){return`/**
|
|
2361
|
+
* @${de.slice(1)} \u2014 Cloudflare R2 via the S3-compatible API.
|
|
2362
|
+
*
|
|
2363
|
+
* Docs: https://developers.cloudflare.com/r2/api/s3/api/
|
|
2364
|
+
*/
|
|
2365
|
+
import {
|
|
2366
|
+
GetObjectCommand,
|
|
2367
|
+
PutObjectCommand,
|
|
2368
|
+
S3Client,
|
|
2369
|
+
} from "@aws-sdk/client-s3";
|
|
2370
|
+
import { getSignedUrl as awsGetSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
2371
|
+
|
|
2372
|
+
import type {
|
|
2373
|
+
SignedUrlOptions,
|
|
2374
|
+
UploadFileInput,
|
|
2375
|
+
UploadFileResult,
|
|
2376
|
+
} from "./types.js";
|
|
2377
|
+
|
|
2378
|
+
const accountId = process.env.R2_ACCOUNT_ID;
|
|
2379
|
+
const accessKeyId = process.env.R2_ACCESS_KEY_ID;
|
|
2380
|
+
const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY;
|
|
2381
|
+
const bucket = process.env.R2_BUCKET;
|
|
2382
|
+
|
|
2383
|
+
if (!accountId || !accessKeyId || !secretAccessKey || !bucket) {
|
|
2384
|
+
throw new Error(
|
|
2385
|
+
"R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET must be configured",
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
const client = new S3Client({
|
|
2390
|
+
region: "auto",
|
|
2391
|
+
endpoint: \`https://\${accountId}.r2.cloudflarestorage.com\`,
|
|
2392
|
+
credentials: { accessKeyId, secretAccessKey },
|
|
2393
|
+
});
|
|
2394
|
+
|
|
2395
|
+
export async function uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
|
|
2396
|
+
const response = await client.send(
|
|
2397
|
+
new PutObjectCommand({
|
|
2398
|
+
Bucket: bucket,
|
|
2399
|
+
Key: input.key,
|
|
2400
|
+
Body: input.body,
|
|
2401
|
+
ContentType: input.contentType,
|
|
2402
|
+
}),
|
|
2403
|
+
);
|
|
2404
|
+
return { key: input.key, raw: response };
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
export async function getSignedUrl(
|
|
2408
|
+
key: string,
|
|
2409
|
+
opts: SignedUrlOptions = {},
|
|
2410
|
+
): Promise<string> {
|
|
2411
|
+
const expiresIn = opts.expiresIn ?? 3600;
|
|
2412
|
+
const command =
|
|
2413
|
+
opts.operation === "put"
|
|
2414
|
+
? new PutObjectCommand({
|
|
2415
|
+
Bucket: bucket,
|
|
2416
|
+
Key: key,
|
|
2417
|
+
ContentType: opts.contentType,
|
|
2418
|
+
})
|
|
2419
|
+
: new GetObjectCommand({ Bucket: bucket, Key: key });
|
|
2420
|
+
return awsGetSignedUrl(client, command, { expiresIn });
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
export type { UploadFileInput, UploadFileResult, SignedUrlOptions } from "./types.js";
|
|
2424
|
+
`}function Go(){return`/**
|
|
2425
|
+
* @${de.slice(1)} \u2014 AWS S3 wrapper.
|
|
2426
|
+
*
|
|
2427
|
+
* Docs: https://docs.aws.amazon.com/s3/
|
|
2428
|
+
*/
|
|
2429
|
+
import {
|
|
2430
|
+
GetObjectCommand,
|
|
2431
|
+
PutObjectCommand,
|
|
2432
|
+
S3Client,
|
|
2433
|
+
} from "@aws-sdk/client-s3";
|
|
2434
|
+
import { getSignedUrl as awsGetSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
2435
|
+
|
|
2436
|
+
import type {
|
|
2437
|
+
SignedUrlOptions,
|
|
2438
|
+
UploadFileInput,
|
|
2439
|
+
UploadFileResult,
|
|
2440
|
+
} from "./types.js";
|
|
2441
|
+
|
|
2442
|
+
const region = process.env.AWS_S3_REGION;
|
|
2443
|
+
const bucket = process.env.S3_BUCKET;
|
|
2444
|
+
if (!region || !bucket) {
|
|
2445
|
+
throw new Error("AWS_S3_REGION and S3_BUCKET must be configured");
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
const client = new S3Client({
|
|
2449
|
+
region,
|
|
2450
|
+
credentials:
|
|
2451
|
+
process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY
|
|
2452
|
+
? {
|
|
2453
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
2454
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
2455
|
+
}
|
|
2456
|
+
: undefined,
|
|
2457
|
+
});
|
|
2458
|
+
|
|
2459
|
+
export async function uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
|
|
2460
|
+
const response = await client.send(
|
|
2461
|
+
new PutObjectCommand({
|
|
2462
|
+
Bucket: bucket,
|
|
2463
|
+
Key: input.key,
|
|
2464
|
+
Body: input.body,
|
|
2465
|
+
ContentType: input.contentType,
|
|
2466
|
+
ACL: input.acl,
|
|
2467
|
+
}),
|
|
2468
|
+
);
|
|
2469
|
+
return { key: input.key, raw: response };
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
export async function getSignedUrl(
|
|
2473
|
+
key: string,
|
|
2474
|
+
opts: SignedUrlOptions = {},
|
|
2475
|
+
): Promise<string> {
|
|
2476
|
+
const expiresIn = opts.expiresIn ?? 3600;
|
|
2477
|
+
const command =
|
|
2478
|
+
opts.operation === "put"
|
|
2479
|
+
? new PutObjectCommand({
|
|
2480
|
+
Bucket: bucket,
|
|
2481
|
+
Key: key,
|
|
2482
|
+
ContentType: opts.contentType,
|
|
2483
|
+
})
|
|
2484
|
+
: new GetObjectCommand({ Bucket: bucket, Key: key });
|
|
2485
|
+
return awsGetSignedUrl(client, command, { expiresIn });
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
export type { UploadFileInput, UploadFileResult, SignedUrlOptions } from "./types.js";
|
|
2489
|
+
`}function Wo(){return`/**
|
|
2490
|
+
* @${de.slice(1)} \u2014 Supabase Storage wrapper.
|
|
2491
|
+
*
|
|
2492
|
+
* Docs: https://supabase.com/docs/guides/storage
|
|
2493
|
+
*/
|
|
2494
|
+
import { createClient } from "@supabase/supabase-js";
|
|
2495
|
+
|
|
2496
|
+
import type {
|
|
2497
|
+
SignedUrlOptions,
|
|
2498
|
+
UploadFileInput,
|
|
2499
|
+
UploadFileResult,
|
|
2500
|
+
} from "./types.js";
|
|
2501
|
+
|
|
2502
|
+
const url = process.env.SUPABASE_URL;
|
|
2503
|
+
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
2504
|
+
const bucket = process.env.SUPABASE_BUCKET;
|
|
2505
|
+
if (!url || !serviceRoleKey || !bucket) {
|
|
2506
|
+
throw new Error(
|
|
2507
|
+
"SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_BUCKET must be configured",
|
|
2508
|
+
);
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
const client = createClient(url, serviceRoleKey, {
|
|
2512
|
+
auth: { persistSession: false },
|
|
2513
|
+
});
|
|
2514
|
+
|
|
2515
|
+
export async function uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
|
|
2516
|
+
const { data, error } = await client.storage
|
|
2517
|
+
.from(bucket)
|
|
2518
|
+
.upload(input.key, input.body, {
|
|
2519
|
+
contentType: input.contentType,
|
|
2520
|
+
upsert: true,
|
|
2521
|
+
});
|
|
2522
|
+
if (error) {
|
|
2523
|
+
throw new Error(\`Supabase upload failed: \${error.message}\`);
|
|
2524
|
+
}
|
|
2525
|
+
return { key: data?.path ?? input.key, raw: data };
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
export async function getSignedUrl(
|
|
2529
|
+
key: string,
|
|
2530
|
+
opts: SignedUrlOptions = {},
|
|
2531
|
+
): Promise<string> {
|
|
2532
|
+
const expiresIn = opts.expiresIn ?? 3600;
|
|
2533
|
+
if (opts.operation === "put") {
|
|
2534
|
+
const { data, error } = await client.storage
|
|
2535
|
+
.from(bucket)
|
|
2536
|
+
.createSignedUploadUrl(key);
|
|
2537
|
+
if (error || !data) {
|
|
2538
|
+
throw new Error(\`Supabase signed upload url failed: \${error?.message ?? "unknown"}\`);
|
|
2539
|
+
}
|
|
2540
|
+
return data.signedUrl;
|
|
2541
|
+
}
|
|
2542
|
+
const { data, error } = await client.storage
|
|
2543
|
+
.from(bucket)
|
|
2544
|
+
.createSignedUrl(key, expiresIn);
|
|
2545
|
+
if (error || !data) {
|
|
2546
|
+
throw new Error(\`Supabase signed url failed: \${error?.message ?? "unknown"}\`);
|
|
2547
|
+
}
|
|
2548
|
+
return data.signedUrl;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
export type { UploadFileInput, UploadFileResult, SignedUrlOptions } from "./types.js";
|
|
2552
|
+
`}function Xo(){return`/**
|
|
2553
|
+
* @${de.slice(1)} \u2014 \u963F\u91CC\u4E91 OSS wrapper via ali-oss.
|
|
2554
|
+
*
|
|
2555
|
+
* Docs: https://help.aliyun.com/product/31815.html
|
|
2556
|
+
*/
|
|
2557
|
+
import OSS from "ali-oss";
|
|
2558
|
+
|
|
2559
|
+
import type {
|
|
2560
|
+
SignedUrlOptions,
|
|
2561
|
+
UploadFileInput,
|
|
2562
|
+
UploadFileResult,
|
|
2563
|
+
} from "./types.js";
|
|
2564
|
+
|
|
2565
|
+
const accessKeyId = process.env.ALIYUN_OSS_ACCESS_KEY;
|
|
2566
|
+
const accessKeySecret = process.env.ALIYUN_OSS_ACCESS_SECRET;
|
|
2567
|
+
const region = process.env.ALIYUN_OSS_REGION;
|
|
2568
|
+
const bucket = process.env.ALIYUN_OSS_BUCKET;
|
|
2569
|
+
if (!accessKeyId || !accessKeySecret || !region || !bucket) {
|
|
2570
|
+
throw new Error(
|
|
2571
|
+
"ALIYUN_OSS_ACCESS_KEY, ALIYUN_OSS_ACCESS_SECRET, ALIYUN_OSS_REGION, ALIYUN_OSS_BUCKET must be configured",
|
|
2572
|
+
);
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
const client = new OSS({ accessKeyId, accessKeySecret, region, bucket });
|
|
2576
|
+
|
|
2577
|
+
export async function uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
|
|
2578
|
+
const body =
|
|
2579
|
+
typeof input.body === "string" ? Buffer.from(input.body) : Buffer.from(input.body);
|
|
2580
|
+
const response = await client.put(input.key, body, {
|
|
2581
|
+
mime: input.contentType,
|
|
2582
|
+
});
|
|
2583
|
+
return { key: response.name, url: response.url, raw: response };
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
export async function getSignedUrl(
|
|
2587
|
+
key: string,
|
|
2588
|
+
opts: SignedUrlOptions = {},
|
|
2589
|
+
): Promise<string> {
|
|
2590
|
+
const expires = opts.expiresIn ?? 3600;
|
|
2591
|
+
return client.signatureUrl(key, {
|
|
2592
|
+
expires,
|
|
2593
|
+
method: opts.operation === "put" ? "PUT" : "GET",
|
|
2594
|
+
"Content-Type": opts.contentType,
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
export type { UploadFileInput, UploadFileResult, SignedUrlOptions } from "./types.js";
|
|
2599
|
+
`}function Ho(){return`/**
|
|
2600
|
+
* @${de.slice(1)} \u2014 \u817E\u8BAF\u4E91 COS wrapper via cos-nodejs-sdk-v5.
|
|
2601
|
+
*
|
|
2602
|
+
* Docs: https://cloud.tencent.com/document/product/436
|
|
2603
|
+
*/
|
|
2604
|
+
import COS from "cos-nodejs-sdk-v5";
|
|
2605
|
+
|
|
2606
|
+
import type {
|
|
2607
|
+
SignedUrlOptions,
|
|
2608
|
+
UploadFileInput,
|
|
2609
|
+
UploadFileResult,
|
|
2610
|
+
} from "./types.js";
|
|
2611
|
+
|
|
2612
|
+
const secretId = process.env.TENCENT_COS_SECRET_ID;
|
|
2613
|
+
const secretKey = process.env.TENCENT_COS_SECRET_KEY;
|
|
2614
|
+
const bucket = process.env.TENCENT_COS_BUCKET;
|
|
2615
|
+
const region = process.env.TENCENT_COS_REGION;
|
|
2616
|
+
if (!secretId || !secretKey || !bucket || !region) {
|
|
2617
|
+
throw new Error(
|
|
2618
|
+
"TENCENT_COS_SECRET_ID, TENCENT_COS_SECRET_KEY, TENCENT_COS_BUCKET, TENCENT_COS_REGION must be configured",
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
const cos = new COS({ SecretId: secretId, SecretKey: secretKey });
|
|
2623
|
+
|
|
2624
|
+
export async function uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
|
|
2625
|
+
const body =
|
|
2626
|
+
typeof input.body === "string" ? Buffer.from(input.body) : Buffer.from(input.body);
|
|
2627
|
+
const response = await cos.putObject({
|
|
2628
|
+
Bucket: bucket,
|
|
2629
|
+
Region: region,
|
|
2630
|
+
Key: input.key,
|
|
2631
|
+
Body: body,
|
|
2632
|
+
ContentType: input.contentType,
|
|
2633
|
+
});
|
|
2634
|
+
return { key: input.key, url: response.Location, raw: response };
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
export async function getSignedUrl(
|
|
2638
|
+
key: string,
|
|
2639
|
+
opts: SignedUrlOptions = {},
|
|
2640
|
+
): Promise<string> {
|
|
2641
|
+
const expires = opts.expiresIn ?? 3600;
|
|
2642
|
+
const url = cos.getObjectUrl({
|
|
2643
|
+
Bucket: bucket,
|
|
2644
|
+
Region: region,
|
|
2645
|
+
Key: key,
|
|
2646
|
+
Sign: true,
|
|
2647
|
+
Method: opts.operation === "put" ? "PUT" : "GET",
|
|
2648
|
+
Expires: expires,
|
|
2649
|
+
} as unknown as COS.GetObjectUrlParams);
|
|
2650
|
+
return url as unknown as string;
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
export type { UploadFileInput, UploadFileResult, SignedUrlOptions } from "./types.js";
|
|
2654
|
+
`}function zo(){return`/**
|
|
2655
|
+
* @${de.slice(1)} \u2014 \u4E03\u725B\u4E91 Kodo wrapper.
|
|
2656
|
+
*
|
|
2657
|
+
* Docs: https://developer.qiniu.com/kodo
|
|
2658
|
+
*/
|
|
2659
|
+
import qiniu from "qiniu";
|
|
2660
|
+
|
|
2661
|
+
import type {
|
|
2662
|
+
SignedUrlOptions,
|
|
2663
|
+
UploadFileInput,
|
|
2664
|
+
UploadFileResult,
|
|
2665
|
+
} from "./types.js";
|
|
2666
|
+
|
|
2667
|
+
const accessKey = process.env.QINIU_ACCESS_KEY;
|
|
2668
|
+
const secretKey = process.env.QINIU_SECRET_KEY;
|
|
2669
|
+
const bucket = process.env.QINIU_BUCKET;
|
|
2670
|
+
const domain = process.env.QINIU_DOMAIN;
|
|
2671
|
+
if (!accessKey || !secretKey || !bucket || !domain) {
|
|
2672
|
+
throw new Error(
|
|
2673
|
+
"QINIU_ACCESS_KEY, QINIU_SECRET_KEY, QINIU_BUCKET, QINIU_DOMAIN must be configured",
|
|
2674
|
+
);
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
|
|
2678
|
+
const config = new qiniu.conf.Config();
|
|
2679
|
+
const formUploader = new qiniu.form_up.FormUploader(config);
|
|
2680
|
+
const bucketManager = new qiniu.rs.BucketManager(mac, config);
|
|
2681
|
+
|
|
2682
|
+
function uploadToken(key: string): string {
|
|
2683
|
+
const putPolicy = new qiniu.rs.PutPolicy({ scope: \`\${bucket}:\${key}\` });
|
|
2684
|
+
return putPolicy.uploadToken(mac);
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
export async function uploadFile(input: UploadFileInput): Promise<UploadFileResult> {
|
|
2688
|
+
const body =
|
|
2689
|
+
typeof input.body === "string" ? Buffer.from(input.body) : Buffer.from(input.body);
|
|
2690
|
+
|
|
2691
|
+
return new Promise<UploadFileResult>((resolve, reject) => {
|
|
2692
|
+
formUploader.put(
|
|
2693
|
+
uploadToken(input.key),
|
|
2694
|
+
input.key,
|
|
2695
|
+
body,
|
|
2696
|
+
new qiniu.form_up.PutExtra(undefined, undefined, input.contentType),
|
|
2697
|
+
(err, body, info) => {
|
|
2698
|
+
if (err) return reject(err);
|
|
2699
|
+
if (info.statusCode !== 200) {
|
|
2700
|
+
return reject(new Error(\`Qiniu upload failed: \${info.statusCode}\`));
|
|
2701
|
+
}
|
|
2702
|
+
resolve({ key: input.key, raw: body });
|
|
2703
|
+
},
|
|
2704
|
+
);
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
export async function getSignedUrl(
|
|
2709
|
+
key: string,
|
|
2710
|
+
opts: SignedUrlOptions = {},
|
|
2711
|
+
): Promise<string> {
|
|
2712
|
+
const expires = opts.expiresIn ?? 3600;
|
|
2713
|
+
if (opts.operation === "put") {
|
|
2714
|
+
// Qiniu issues upload tokens, not signed PUT URLs.
|
|
2715
|
+
return uploadToken(key);
|
|
2716
|
+
}
|
|
2717
|
+
const deadline = Math.floor(Date.now() / 1000) + expires;
|
|
2718
|
+
return bucketManager.privateDownloadUrl(domain, key, deadline);
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
export type { UploadFileInput, UploadFileResult, SignedUrlOptions } from "./types.js";
|
|
2722
|
+
`}function Jo(e){switch(e){case"r2":return qo();case"s3":return Go();case"supabase-storage":return Wo();case"aliyun-oss":return Xo();case"tencent-cos":return Ho();case"qiniu":return zo();case"none":return`export {};
|
|
2723
|
+
`}}function Qo(e){let t={};e.pkg&&e.pkgVersion&&(t[e.pkg]=e.pkgVersion),(e.id==="r2"||e.id==="s3")&&(t["@aws-sdk/s3-request-presigner"]="^3.658.0");let n={name:de,version:"0.1.0",private:!0,description:`Object storage via ${e.name}`,type:"module",main:"./dist/index.js",exports:{".":{import:"./dist/index.js",types:"./dist/index.d.ts"}},scripts:{build:"tsup",typecheck:"tsc --noEmit",clean:"rm -rf dist .turbo"},dependencies:t,devDependencies:{tsup:"^8.5.0",typescript:"catalog:"}};return JSON.stringify(n,null,2)+`
|
|
2724
|
+
`}async function ln(e,t,n){if(t==="none")return;let r=pn(t);if(!r)throw new Error(`Unknown storage provider "${t}". Valid ids: ${ze.map(a=>a.id).join(", ")}`);let o=pe.join(e,"packages","storage"),s=pe.join(o,"src");le.existsSync(s)&&le.rmSync(s,{recursive:!0,force:!0}),ct(pe.join(o,"package.json"),Qo(r)),ct(pe.join(s,"types.ts"),$o),ct(pe.join(s,"index.ts"),Jo(r.id)),Ko(e,Vo(r))}import ue from"fs";import De from"path";function Zo(e){return`import Link from "next/link";
|
|
2725
|
+
|
|
2726
|
+
export default function WelcomePage() {
|
|
2727
|
+
const projectName = ${JSON.stringify(e)};
|
|
2728
|
+
return (
|
|
2729
|
+
<div className="mx-auto max-w-3xl px-6 py-16">
|
|
2730
|
+
<div className="space-y-8">
|
|
2731
|
+
<header className="space-y-2">
|
|
2732
|
+
<p className="text-sm text-muted-foreground">Powered by Sailor \u2693</p>
|
|
2733
|
+
<h1 className="text-4xl font-bold tracking-tight">
|
|
2734
|
+
Welcome to {projectName}
|
|
2735
|
+
</h1>
|
|
2736
|
+
<p className="text-lg text-muted-foreground">
|
|
2737
|
+
Your AI-native SaaS template is ready. Here's what to do next:
|
|
2738
|
+
</p>
|
|
2739
|
+
</header>
|
|
2740
|
+
|
|
2741
|
+
<section className="space-y-4">
|
|
2742
|
+
<h2 className="text-2xl font-semibold">Getting started</h2>
|
|
2743
|
+
<ol className="space-y-3">
|
|
2744
|
+
<Step
|
|
2745
|
+
number={1}
|
|
2746
|
+
title="Configure environment"
|
|
2747
|
+
description="Fill in your .env.local \u2014 we've generated random secrets for you"
|
|
2748
|
+
command=".env.local"
|
|
2749
|
+
/>
|
|
2750
|
+
<Step
|
|
2751
|
+
number={2}
|
|
2752
|
+
title="Run database migrations"
|
|
2753
|
+
description="Sync your Prisma schema with your database"
|
|
2754
|
+
command="pnpm db:migrate"
|
|
2755
|
+
/>
|
|
2756
|
+
<Step
|
|
2757
|
+
number={3}
|
|
2758
|
+
title="Seed example data"
|
|
2759
|
+
description="1 admin user + 3 tenants + sample records"
|
|
2760
|
+
command="pnpm db:seed"
|
|
2761
|
+
/>
|
|
2762
|
+
<Step
|
|
2763
|
+
number={4}
|
|
2764
|
+
title="Customize your brand"
|
|
2765
|
+
description="Colors, domain, logo, SEO \u2014 all in one file"
|
|
2766
|
+
command="pnpm sailor brand init"
|
|
2767
|
+
/>
|
|
2768
|
+
</ol>
|
|
2769
|
+
</section>
|
|
2770
|
+
|
|
2771
|
+
<section className="space-y-4">
|
|
2772
|
+
<h2 className="text-2xl font-semibold">Resources</h2>
|
|
2773
|
+
<ul className="list-inside list-disc space-y-2 text-sm">
|
|
2774
|
+
<li>
|
|
2775
|
+
<Link href="/docs" className="underline">
|
|
2776
|
+
Documentation
|
|
2777
|
+
</Link>
|
|
2778
|
+
</li>
|
|
2779
|
+
<li>
|
|
2780
|
+
<a
|
|
2781
|
+
href="https://github.com/nebutra/nebutra-sailor"
|
|
2782
|
+
className="underline"
|
|
2783
|
+
>
|
|
2784
|
+
GitHub
|
|
2785
|
+
</a>
|
|
2786
|
+
</li>
|
|
2787
|
+
<li>
|
|
2788
|
+
<a
|
|
2789
|
+
href="https://nebutra.com/get-license"
|
|
2790
|
+
className="underline"
|
|
2791
|
+
>
|
|
2792
|
+
Get free license (Individual/OPC)
|
|
2793
|
+
</a>
|
|
2794
|
+
</li>
|
|
2795
|
+
</ul>
|
|
2796
|
+
</section>
|
|
2797
|
+
|
|
2798
|
+
<footer className="border-t pt-6 text-xs text-muted-foreground">
|
|
2799
|
+
Delete this page after setup:{" "}
|
|
2800
|
+
<code>apps/web/src/app/[locale]/welcome/</code>
|
|
2801
|
+
</footer>
|
|
2802
|
+
</div>
|
|
2803
|
+
</div>
|
|
2804
|
+
);
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
function Step({
|
|
2808
|
+
number,
|
|
2809
|
+
title,
|
|
2810
|
+
description,
|
|
2811
|
+
command,
|
|
2812
|
+
}: {
|
|
2813
|
+
number: number;
|
|
2814
|
+
title: string;
|
|
2815
|
+
description: string;
|
|
2816
|
+
command: string;
|
|
2817
|
+
}) {
|
|
2818
|
+
return (
|
|
2819
|
+
<li className="flex gap-4">
|
|
2820
|
+
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary text-sm font-medium text-primary-foreground">
|
|
2821
|
+
{number}
|
|
2822
|
+
</span>
|
|
2823
|
+
<div className="space-y-1">
|
|
2824
|
+
<h3 className="font-medium">{title}</h3>
|
|
2825
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
2826
|
+
<code className="inline-block rounded bg-muted px-2 py-0.5 text-xs">
|
|
2827
|
+
{command}
|
|
2828
|
+
</code>
|
|
2829
|
+
</div>
|
|
2830
|
+
</li>
|
|
2831
|
+
);
|
|
2832
|
+
}
|
|
2833
|
+
`}function es(e,t){return`# Next steps \u2014 ${e}
|
|
2834
|
+
|
|
2835
|
+
Region: ${t}
|
|
2836
|
+
|
|
2837
|
+
Your AI-native SaaS scaffold is ready. Complete these four steps to go from template to running dev server:
|
|
2838
|
+
|
|
2839
|
+
1. **Configure environment** \u2014 fill in \`.env.local\`
|
|
2840
|
+
> Random cryptographic secrets have been generated for you. Add your provider keys (DB, auth, payments, AI).
|
|
2841
|
+
|
|
2842
|
+
2. **Run database migrations** \u2014 sync your Prisma schema with your database
|
|
2843
|
+
\`\`\`bash
|
|
2844
|
+
pnpm db:migrate
|
|
2845
|
+
\`\`\`
|
|
2846
|
+
|
|
2847
|
+
3. **Seed example data** \u2014 1 admin user + 3 tenants + sample records
|
|
2848
|
+
\`\`\`bash
|
|
2849
|
+
pnpm db:seed
|
|
2850
|
+
\`\`\`
|
|
2851
|
+
|
|
2852
|
+
4. **Customize your brand** \u2014 colors, domain, logo, SEO \u2014 all in one file
|
|
2853
|
+
\`\`\`bash
|
|
2854
|
+
pnpm sailor brand init
|
|
2855
|
+
\`\`\`
|
|
2856
|
+
|
|
2857
|
+
After setup, delete the welcome route at \`apps/web/src/app/[locale]/welcome/\`.
|
|
2858
|
+
|
|
2859
|
+
## Resources
|
|
2860
|
+
|
|
2861
|
+
- [Documentation](/docs)
|
|
2862
|
+
- [GitHub](https://github.com/nebutra/nebutra-sailor)
|
|
2863
|
+
- [Get free license (Individual/OPC)](https://nebutra.com/get-license)
|
|
2864
|
+
`}async function dn(e,t){let{projectName:n,region:r}=t;try{let o=De.join(e,".sailor");ue.existsSync(o)||ue.mkdirSync(o,{recursive:!0}),ue.writeFileSync(De.join(o,"next-steps.md"),es(n,r))}catch(o){let s=o instanceof Error?o.message:String(o);console.warn(`[create-sailor] Failed to write .sailor/next-steps.md: ${s}`)}try{let o=De.join(e,"apps","web");if(!ue.existsSync(o))return;let s=De.join(o,"src","app","[locale]","welcome");ue.existsSync(s)||ue.mkdirSync(s,{recursive:!0}),ue.writeFileSync(De.join(s,"page.tsx"),Zo(n))}catch(o){let s=o instanceof Error?o.message:String(o);console.warn(`[create-sailor] Failed to write welcome page: ${s}`)}}var mn="create-sailor";function l(e,t){e&&process.stdout.write(JSON.stringify(t)+`
|
|
2865
|
+
`)}function rs(){let e=process.env.npm_config_user_agent??"";return e.startsWith("pnpm")?"pnpm":e.startsWith("yarn")?"yarn":e.startsWith("bun")?"bun":"npm"}function gn(e){switch(e){case"postgres":case"postgresql":return"postgresql";case"mysql":return"mysql";case"sqlite":return"sqlite";case"none":return"none";default:return"postgresql"}}function fn(e){return e==="drizzle"?"drizzle":e==="none"?"none":"prisma"}function yn(e){return e==="lemon"||e==="lemonsqueezy"?"lemonsqueezy":e==="none"?"none":"stripe"}function Sn(e){if(!e)return"stripe";let t=e.toLowerCase();return t==="lemon"||t==="lemonsqueezy"?"lemon":t==="wechat"?"wechat":t==="alipay"?"alipay":t==="none"?"none":"stripe"}function En(e){if(!e)return"clerk";let t=e.toLowerCase();return t==="betterauth"||t==="better-auth"?"betterauth":t==="none"?"none":"clerk"}function hn(e){if(!e)return["openai"];let t=e.split(",").map(n=>n.trim().toLowerCase());return t.includes("none")?[]:t}var _n={mintlify:"Mintlify",docusaurus:"Docusaurus",nextra:"Nextra",vitepress:"VitePress"};function vn(e,t){if(!e)return"fumadocs";let n=e.toLowerCase();if(n==="fumadocs"||n==="none")return n;if(n in _n){let r=_n[n];return t?l(!0,{event:"notice",kind:"docs-fallback",requested:n,effective:"fumadocs"}):process.stdout.write(T.yellow(`\u26A0 ${r} support is coming in v1.2. Falling back to fumadocs.
|
|
2866
|
+
`)+T.dim(` Track progress: https://github.com/Nebutra/Nebutra-Sailor/issues
|
|
2867
|
+
`)),"fumadocs"}return"fumadocs"}function In(e){switch(e){case"vercel":return"vercel";case"railway":return"railway";case"cloudflare":return"cloudflare";case"selfhost":return"selfhost";case"none":return"none";default:return"vercel"}}function Pn(e){if(!e)return"global";let t=e.toLowerCase();return t==="cn"?"cn":t==="hybrid"?"hybrid":"global"}function os(e){return e==="cn"?{email:"aliyun-dm",storage:"aliyun-oss",monitoring:"sentry",analytics:"baidu",sms:"aliyun-sms"}:e==="hybrid"?{email:"resend",storage:"aliyun-oss",monitoring:"sentry",analytics:"posthog",sms:"aliyun-sms"}:{email:"resend",storage:"r2",monitoring:"sentry",analytics:"posthog",sms:"twilio"}}function bn(e){return e==="cn"?"wechat":"stripe"}async function ss(){let e=new ts;e.name(mn).description("Nebutra-Sailor \u2014 AI-Native SaaS template").version(J,"-v, --version").helpOption(!1).argument("[name]","project directory",void 0).option("-p, --pm <id>","npm | pnpm | yarn | bun").option("--region <id>","global | cn | hybrid").option("--orm <id>","prisma | drizzle | none").option("--db <id>","postgres | mysql | sqlite | none").option("--auth <id>","clerk | betterauth | none").option("--social-login <ids>","CN social login providers \u2014 wechat | qq | dingtalk | workweixin | feishu | weibo (comma-separated)").option("--payment <id>","stripe | lemon | wechat | alipay | none").option("--ai <ids>","comma-separated provider ids").option("--deploy <target>","vercel | railway | cloudflare | selfhost").option("--docs <id>","fumadocs | mintlify | docusaurus | nextra | vitepress | none").option("--email <id>","resend | postmark | ses | aliyun-dm | tencent-ses | netease | none").option("--storage <id>","r2 | s3 | supabase | aliyun-oss | tencent-cos | qiniu | none").option("--monitoring <id>","sentry | datadog | aliyun-arms | tingyun | none").option("--analytics <id>","posthog | plausible | umami | baidu | sensors | none").option("--sms <id>","twilio | aliyun-sms | tencent-sms | yunpian | none").option("--i18n","enable i18n").option("--no-i18n","disable i18n").option("--no-install","skip package install").option("--no-git","skip git init").option("-y, --yes","accept all defaults (non-interactive)").option("--dry-run","preview actions without writing files").option("--json","machine-readable output").option("--no-color","disable color output").option("-h, --help","show help"),e.parse(process.argv);let t=e.opts(),[n]=e.args;t.help&&(gt(),process.exit(0));let r=!!t.json,o=!!t.dryRun,s=!!t.yes,a=s||!process.stdin.isTTY;r||ut(),l(r,{event:"start",version:J});let c=n??(s?"./my-saas-app":void 0),i;if(c)i=c;else if(a)i="./my-saas-app";else{let g=await I.group({name:()=>I.text({message:"Where should we create your project?",placeholder:"./my-saas-app",defaultValue:"./my-saas-app",validate:y=>{if(y.length===0)return"Please enter a path."}})},{onCancel:()=>{process.stdout.write(T.red(`\u2718 Cancelled
|
|
2868
|
+
`)),process.exit(130)}});i=String(g.name)}let d=pt.basename(pt.resolve(i)),m=t.pm??rs(),S=!!t.region,me=!!t.auth,ge=!!t.payment,Ce=!!t.ai,we=!!t.email,f=!!t.storage,p=!!t.monitoring,b=!!t.analytics,Fe=!!t.sms,lt=t.i18n!==void 0,h,Ne,Y,Qe,V,O,U=bt(t.socialLogin),z,fe,$,x,Be;if(a){h=Pn(t.region),Ne=fn(t.orm),Y=gn(t.db);let g=ge?t.payment:bn(h);Qe=yn(g),V=Sn(g),O=En(t.auth),z=hn(t.ai),$=In(t.deploy),x=vn(t.docs,r),Be=lt?!!t.i18n:!0}else{let g={};if(S||(g.region=()=>I.select({message:"Target region?",options:[{value:"global",label:"global \u2014 \u6D77\u5916\u4F18\u5148"},{value:"cn",label:"cn \u2014 \u56FD\u5185\u4F18\u5148"},{value:"hybrid",label:"hybrid \u2014 \u53CC\u8F68\uFF08\u56FD\u5185+\u51FA\u6D77\uFF09"}],initialValue:"global"})),me||(g.auth=()=>I.select({message:"Auth provider?",options:[{value:"clerk",label:"Clerk"},{value:"betterauth",label:"Better Auth"},{value:"none",label:"None"}],initialValue:"clerk"})),!Ce){let Ve=Array.from(new Set(Ge.map(_=>_.category)));g.aiCategories=()=>I.multiselect({message:"Which AI Provider categories do you want to explore? (Select multiple)",options:Ve.map(_=>({value:_,label:_})),initialValues:["\u76F4\u63A5\u5B9E\u9A8C\u5BA4","\u7EDF\u4E00\u7F51\u5173"]}),g.aiProviders=({results:_})=>{let L=_.aiCategories||[],$e=Ge.filter(xe=>L.includes(xe.category)).map(xe=>({value:xe.id,label:`[${xe.category}] ${xe.name}`}));return $e.push({value:"custom",label:"[\u81EA\u5B9A\u4E49] Custom OpenAI-compatible endpoint"}),I.multiselect({message:"Select AI Providers (Space to select, Enter to submit)",options:$e,initialValues:["openai","anthropic"],required:!1})},g.enableCustom=({results:_})=>_.aiProviders?.includes("custom")?I.confirm({message:"Configure custom OpenAI-compatible endpoint?",initialValue:!0}):Promise.resolve(!1),g.customAiName=({results:_})=>_.enableCustom?I.text({message:"Custom endpoint name (e.g. proxy, local):",defaultValue:"custom",placeholder:"custom"}):Promise.resolve(void 0),g.customAiBaseUrl=({results:_})=>_.enableCustom?I.text({message:"Custom endpoint base URL (e.g. https://api.proxy.com/v1):",validate:L=>{if(L.length===0)return"Base URL is required."}}):Promise.resolve(void 0),g.customAiApiKeyEnv=({results:_})=>_.enableCustom?I.text({message:"Environment variable name for the API Key:",defaultValue:"CUSTOM_AI_API_KEY",placeholder:"CUSTOM_AI_API_KEY"}):Promise.resolve(void 0)}let y=Object.keys(g).length>0?await I.group(g,{onCancel:()=>{process.stdout.write(T.red(`\u2718 Cancelled
|
|
2869
|
+
`)),process.exit(130)}}):{};h=Pn(S?t.region:y.region),Ne=fn(t.orm),Y=gn(t.db);let R=ge?t.payment:bn(h);Qe=yn(R),V=Sn(R),O=En(me?t.auth:y.auth),z=Ce?hn(t.ai):y.aiProviders?.filter(Ve=>Ve!=="custom")??[],y.enableCustom&&(fe={name:y.customAiName,baseURL:y.customAiBaseUrl,apiKeyEnvName:y.customAiApiKeyEnv}),$=In(t.deploy),x=vn(t.docs,r),Be=lt?!!t.i18n:!0}let Te=os(h),ye=we?t.email:Te.email,Se=f?t.storage:Te.storage,Ee=p?t.monitoring:Te.monitoring,he=b?t.analytics:Te.analytics,_e=Fe?t.sms:Te.sms,Ye={region:h,orm:Ne,database:Y,payment:Qe,aiProviders:z,customAiEndpoint:fe,deployTarget:$,docs:x,i18n:Be,email:ye,storage:Se,monitoring:Ee,analytics:he,sms:_e},Ke=[["Project name",d],["Region",h],["Auth",O],["Social login",U.length>0?U.map(g=>Re.find(y=>y.id===g)?.name??g).join(", "):"none"],["ORM",Ne],["Database",Y],["Payment",V],["AI",Array.isArray(z)?z.join(", ")+(fe?", custom":""):String(z)],["Email",ye],["Storage",Se],["Monitoring",Ee],["Analytics",he],["SMS",_e],["Deploy Target",$],["Docs Framework",x]];r?Ke.forEach(([g,y],R)=>{l(!0,{event:"step",step:g,value:y,index:R+1,total:Ke.length})}):Ke.forEach(([g,y],R)=>{yt({index:R+1,total:Ke.length,label:g,value:y})});let An={databaseUrl:"postgresql://postgres:postgres@localhost:5432/nebutra",clerkPublishable:"",clerkSecret:""},Cn=Date.now();if(o){let g=z.length,y=[`clone template \u2192 ${i}`,"write nebutra.config.json",`prune template (orm=${Ne}, i18n=${Be})`,`region \u2192 ${h}`,`auth \u2192 ${O==="none"?"skip (remove packages/auth)":`configure ${O}`}`,...U.length>0?[`social-login \u2192 generate ${U.length} callback route${U.length===1?"":"s"} + SocialLoginButtons.tsx (${U.join(", ")})`]:[],`db \u2192 ${Y==="none"?"skip (remove packages/db)":`configure Prisma for ${Y}`}`,`payment \u2192 ${V==="none"?"skip (remove packages/billing)":`configure ${V}`}`,...x!=="none"?[`docs \u2192 scaffold apps/docs (${x==="fumadocs"?"fumadocs":`${x} \u2192 fumadocs fallback`})`]:[],...g>0||fe?[`ai-providers \u2192 generate registry.ts + env (${g}${fe?" + custom":""} provider${g===1&&!fe?"":"s"})`]:[],...ye!=="none"?[`email \u2192 configure ${ye}`]:[],...Se!=="none"?[`storage \u2192 configure ${Se}`]:[],...Ee!=="none"?[`monitoring \u2192 configure ${Ee}`]:[],...he!=="none"?[`analytics \u2192 configure ${he}`]:[],..._e!=="none"?[`sms \u2192 configure ${_e}`]:[],`compliance \u2192 inject ${h} boilerplate (ICP/Cookie/AIGC/Privacy)`,"welcome \u2192 generate dev welcome page","env \u2192 generate random secrets (AUTH_SECRET, JWT_SECRET)","seed \u2192 generate prisma/seed.ts (1 admin + 3 tenants)",...$!=="none"?[`deploy \u2192 inject ${$} config`]:[],"inject .env.local",t.install===!1?"skip install":`run ${m} install`,t.git===!1?"skip git init":"run git init"];if(r)y.forEach(R=>l(!0,{event:"plan",action:R})),l(!0,{event:"done",dryRun:!0});else{process.stdout.write(`
|
|
2870
|
+
`+T.bold(`Dry run \u2014 planned actions:
|
|
2871
|
+
`));for(let R of y)process.stdout.write(` \u2022 ${R}
|
|
2872
|
+
`);process.stdout.write(T.dim(`
|
|
187
2873
|
No files were written.
|
|
188
|
-
`))}process.exit(0)}let
|
|
189
|
-
`+
|
|
190
|
-
`))
|
|
191
|
-
`))
|
|
192
|
-
`));try{
|
|
193
|
-
`))}}else
|
|
194
|
-
`))}}else
|
|
2874
|
+
`))}process.exit(0)}let wn=async()=>{process.stdout.write(`
|
|
2875
|
+
`+T.red(`\u2718 Cancelled
|
|
2876
|
+
`)),un.existsSync(i)&&await I.confirm({message:`Cleanup partial install at ${i}?`,initialValue:!0})===!0&&(un.rmSync(i,{recursive:!0,force:!0}),process.stdout.write(T.dim(` \u2713 Removed ${i}
|
|
2877
|
+
`))),process.exit(130)};process.on("SIGINT",wn);try{l(r,{event:"step",step:"clone",status:"start"}),await qt(i),l(r,{event:"step",step:"clone",status:"ok"}),l(r,{event:"step",step:"package",status:"start"}),await Ht(i,d),l(r,{event:"step",step:"package",status:"ok"}),l(r,{event:"step",step:"config",status:"start"}),await Nt(i,Ye),l(r,{event:"step",step:"config",status:"ok"}),l(r,{event:"step",step:"prune",status:"start"}),await on(i,Ye),l(r,{event:"step",step:"prune",status:"ok"}),l(r,{event:"step",step:"auth",choice:O,status:"start"}),await It(i,O),l(r,{event:"step",step:"auth",choice:O,status:"ok"}),U.length>0?(l(r,{event:"step",step:"social-login",providers:U,status:"start"}),await Ct(i,U,O),l(r,{event:"step",step:"social-login",providers:U,status:"ok"})):l(r,{event:"step",step:"social-login",status:"skip"}),l(r,{event:"step",step:"db",choice:Y,status:"start"}),await Rt(i,Y,d),l(r,{event:"step",step:"db",choice:Y,status:"ok"}),l(r,{event:"step",step:"payment",choice:V,status:"start"}),await Jt(i,V),l(r,{event:"step",step:"payment",choice:V,status:"ok"}),await Dt(i,ye,h),r&&l(!0,{event:"step",step:"email",choice:ye,status:"ok"}),await ln(i,Se,h),r&&l(!0,{event:"step",step:"storage",choice:Se,status:"ok"}),await Xt(i,Ee,h),r&&l(!0,{event:"step",step:"monitoring",choice:Ee,status:"ok"}),await ht(i,he,h),r&&l(!0,{event:"step",step:"analytics",choice:he,status:"ok"}),await cn(i,_e,h),r&&l(!0,{event:"step",step:"sms",choice:_e,status:"ok"}),await wt(i,h),r&&l(!0,{event:"step",step:"compliance",region:h,status:"ok"}),await Yt(i),await sn(i,O),await dn(i,{projectName:d,region:h}),l(r,{event:"step",step:"ai-providers",status:"start"});let g={providerIds:Ye.aiProviders,customEndpoint:Ye.customAiEndpoint},y=pt.join(i,"packages/ai-providers/templates");if(await nn(i,g,y),l(r,{event:"step",step:"ai-providers",status:"ok"}),l(r,{event:"step",step:"docs",status:"start"}),x!=="none"?(await Ut(i,{framework:x,projectName:d}),l(r,{event:"step",step:"docs",framework:"fumadocs",requested:x,status:"ok"})):l(r,{event:"step",step:"docs",status:"skip"}),l(r,{event:"step",step:"deploy-target",status:"start"}),$!=="none"&&await kt(i,$),l(r,{event:"step",step:"deploy-target",status:"ok"}),l(r,{event:"step",step:"env",status:"start"}),await Vt(i,An),l(r,{event:"step",step:"env",status:"ok"}),t.install!==!1){let L=m==="bun"?"bun install":`${m} install`;r?l(!0,{event:"step",step:"install",pm:m,status:"start"}):process.stdout.write(T.dim(` Installing dependencies with ${m}\u2026
|
|
2878
|
+
`));try{Je(L,{cwd:i,stdio:r?"ignore":"inherit"}),l(r,{event:"step",step:"install",status:"ok"})}catch(ve){let $e=ve instanceof Error?ve.message:String(ve);r?l(!0,{event:"step",step:"install",status:"error",error:$e}):process.stdout.write(T.yellow(` \u26A0 install failed \u2014 run '${L}' manually.
|
|
2879
|
+
`))}}else l(r,{event:"step",step:"install",status:"skip"});if(t.git!==!1){r&&l(!0,{event:"step",step:"git-init",status:"start"});try{Je("git init -q",{cwd:i,stdio:"ignore"}),Je("git add -A",{cwd:i,stdio:"ignore"}),Je('git -c user.email=you@example.com -c user.name="You" commit -q -m "chore: initial scaffold from create-sailor"',{cwd:i,stdio:"ignore"}),l(r,{event:"step",step:"git-init",status:"ok"})}catch(L){let ve=L instanceof Error?L.message:String(L);r?l(!0,{event:"step",step:"git-init",status:"error",error:ve}):process.stdout.write(T.yellow(` \u26A0 git init skipped \u2014 not fatal.
|
|
2880
|
+
`))}}else l(r,{event:"step",step:"git-init",status:"skip"});let _=Math.max(1,Math.round((Date.now()-Cn)/1e3));r?l(!0,{event:"done",status:"ok",elapsedSec:_,targetDir:i}):mt({elapsedSec:_,targetDir:i,skippedInstall:t.install===!1});try{ns({pkg:{name:mn,version:J},updateCheckInterval:1e3*60*60*24}).notify({defer:!1,isGlobal:!0})}catch{}process.exit(0)}catch(g){let y=g instanceof Error?g.message:String(g);r?l(!0,{event:"error",message:y}):process.stdout.write(T.red(`
|
|
195
2881
|
\u2718 Failed: ${y}
|
|
196
|
-
`)),process.exit(1)}}
|
|
2882
|
+
`)),process.exit(1)}}ss().catch(e=>{process.stderr.write(String(e)+`
|
|
197
2883
|
`),process.exit(1)});
|