@minus-ai/create-skill 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/index.mjs +658 -0
  2. package/package.json +12 -0
  3. package/templates/README.md.tpl +123 -0
  4. package/templates/asin/en-US.json.tpl +7 -0
  5. package/templates/asin/main.tsx.tpl +95 -0
  6. package/templates/asin/pipeline.py.tpl +11 -0
  7. package/templates/asin/zh-CN.json.tpl +7 -0
  8. package/templates/custom/en-US.json.tpl +8 -0
  9. package/templates/custom/main.tsx.tpl +93 -0
  10. package/templates/custom/pipeline.py.tpl +9 -0
  11. package/templates/custom/zh-CN.json.tpl +8 -0
  12. package/templates/default/en-US.json.tpl +4 -0
  13. package/templates/default/main.tsx.tpl +109 -0
  14. package/templates/default/pipeline.py.tpl +8 -0
  15. package/templates/default/zh-CN.json.tpl +4 -0
  16. package/templates/embed.html.tpl +13 -0
  17. package/templates/en-US.json.tpl +10 -0
  18. package/templates/env.example.tpl +3 -0
  19. package/templates/file/en-US.json.tpl +10 -0
  20. package/templates/file/main.tsx.tpl +104 -0
  21. package/templates/file/pipeline.py.tpl +11 -0
  22. package/templates/file/zh-CN.json.tpl +10 -0
  23. package/templates/frontend-package.json.tpl +22 -0
  24. package/templates/gitignore.tpl +10 -0
  25. package/templates/keyword/en-US.json.tpl +7 -0
  26. package/templates/keyword/main.tsx.tpl +95 -0
  27. package/templates/keyword/pipeline.py.tpl +11 -0
  28. package/templates/keyword/zh-CN.json.tpl +7 -0
  29. package/templates/main.tsx.tpl +193 -0
  30. package/templates/pipeline.py.tpl +12 -0
  31. package/templates/pnpm-workspace.yaml.tpl +2 -0
  32. package/templates/pyproject.toml.tpl +13 -0
  33. package/templates/root-package.json.tpl +18 -0
  34. package/templates/server.py.tpl +4 -0
  35. package/templates/tsconfig.json.tpl +15 -0
  36. package/templates/vite.config.ts.tpl +23 -0
  37. package/templates/zh-CN.json.tpl +10 -0
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@minus-skill/{{skillId}}-frontend",
3
+ "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsc -b && vite build",
8
+ "dev": "vite build --watch"
9
+ },
10
+ "dependencies": {
11
+ "sonner": "^2.0.7"
12
+ },
13
+ "devDependencies": {
14
+ "@minus-ai/dev-vite-plugin": "^0.1.0-beta.1",
15
+ "@types/node": "^20.14.0",
16
+ "@types/react": "^18.3.3",
17
+ "@types/react-dom": "^18.3.0",
18
+ "@vitejs/plugin-react": "^4.3.1",
19
+ "typescript": "^5.5.0",
20
+ "vite": "^5.4.0"
21
+ }
22
+ }
@@ -0,0 +1,10 @@
1
+ node_modules/
2
+ .env.local
3
+ __pycache__/
4
+ *.pyc
5
+ .venv/
6
+ static/
7
+ builds/
8
+ dist/
9
+ *.tsbuildinfo
10
+ .minus/dev-ports.json
@@ -0,0 +1,7 @@
1
+ {
2
+ "{{namespace}}.step.title": "Result",
3
+ "{{namespace}}.step.empty": "(empty)",
4
+ "{{namespace}}.home.placeholder": "Enter keyword, e.g. bluetooth earbuds",
5
+ "{{namespace}}.home.processing": "Processing…",
6
+ "{{namespace}}.home.send": "Start"
7
+ }
@@ -0,0 +1,95 @@
1
+ import { StrictMode, useMemo, useState } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { FlowApp, I18nProvider, mergeMessages, useT, type StepConfig } from '@minus/widget-framework';
4
+ import { AmazonSearchBar, CountrySelect, SearchSubmitButton, validateKeywords, platformWidgetMessages } from '@minus/platform-widgets';
5
+ import { Toaster } from 'sonner';
6
+ import zhCN from './locales/zh-CN.json';
7
+ import enUS from './locales/en-US.json';
8
+
9
+ /*
10
+ * 防御性编码提示(适用于所有 step render 函数):
11
+ * - 后端 data 不可信,所有字段都要 ?? 兜底
12
+ * - 数组:(data.list as T[] | undefined) ?? []
13
+ * - 字符串:(data.name as string | undefined) ?? ''
14
+ * - 数字:Number(data.count) || 0
15
+ * - 嵌套:(data.obj as Record<string, unknown> | undefined)?.field ?? fallback
16
+ * 框架已有 Error Boundary 兜底,但不应依赖——优先在数据层做防御。
17
+ */
18
+ function buildSteps(t: (k: string, fb?: string) => string): StepConfig[] {
19
+ return [
20
+ {
21
+ render: ({ data }) => (
22
+ <div className="minus-default-step-done">
23
+ {(data.text as string) ?? t('{{namespace}}.step.empty', '(空)')}
24
+ </div>
25
+ ),
26
+ },
27
+ ];
28
+ }
29
+
30
+ function Home({ title, description, useCases, tags, onStart }: { title: string; description?: string; useCases?: string[]; tags?: Array<{ code: string; label: string }>; onStart: (input: Record<string, unknown>) => Promise<void> }) {
31
+ const t = useT();
32
+ const [value, setValue] = useState('');
33
+ const [country, setCountry] = useState('US');
34
+ const [error, setError] = useState<string | null>(null);
35
+ const [loading, setLoading] = useState(false);
36
+
37
+ async function handleSubmit() {
38
+ if (loading) return;
39
+ const { keywords, error: kwError } = validateKeywords(value);
40
+ if (kwError) { setError(t(kwError.key, kwError.fallback, kwError.vars)); return; }
41
+ setError(null);
42
+ setLoading(true);
43
+ try { await onStart({ keywords: keywords.join(','), country }); } finally { setLoading(false); }
44
+ }
45
+
46
+ return (
47
+ <>
48
+ <h1>{title}</h1>
49
+ {description && <p className="minus-skill-hero__subtitle">{description}</p>}
50
+ {((useCases && useCases.length > 0) || (tags && tags.length > 0)) && (
51
+ <ul className="minus-skill-hero__use-cases">
52
+ {useCases?.map((uc, i) => (
53
+ <li key={`uc-${i}`} className="minus-skill-hero__use-case-chip">{uc}</li>
54
+ ))}
55
+ {tags?.map((tag) => (
56
+ <li key={`tag-${tag.code}`} className="minus-skill-hero__use-case-chip">{tag.label}</li>
57
+ ))}
58
+ </ul>
59
+ )}
60
+ <AmazonSearchBar
61
+ onSubmit={handleSubmit}
62
+ country={<CountrySelect onChange={setCountry} />}
63
+ input={<textarea value={value} onChange={(e) => { setValue(e.target.value); if (error) setError(null); }} onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSubmit(); } }} placeholder={t('{{namespace}}.home.placeholder', '输入关键词,多个关键词用逗号或换行分隔')} spellCheck={false} disabled={loading} rows={1} />}
64
+ submit={<SearchSubmitButton />}
65
+ error={error}
66
+ />
67
+ </>
68
+ );
69
+ }
70
+
71
+ const rootEl = document.getElementById('root');
72
+ if (!rootEl) throw new Error('#root not found');
73
+
74
+ const skillMessages = mergeMessages(platformWidgetMessages, { 'zh-CN': zhCN as Record<string, string>, 'en-US': enUS as Record<string, string> });
75
+
76
+ function SkillRoot() {
77
+ const t = useT();
78
+ const steps = useMemo(() => buildSteps(t), [t]);
79
+ return (
80
+ <FlowApp
81
+ steps={steps}
82
+ renderHome={({ flow, onStart }) => <Home title={flow.title} description={flow.description} useCases={flow.useCases} tags={flow.tags} onStart={onStart} />}
83
+ renderCompletion={() => null}
84
+ renderHistoryItem={(entry) => {
85
+ const inp = entry.entryParams as Record<string, string> | undefined;
86
+ return {
87
+ label: inp?.keywords ?? '—',
88
+ meta: inp?.country || undefined,
89
+ };
90
+ }}
91
+ />
92
+ );
93
+ }
94
+
95
+ createRoot(rootEl).render(<StrictMode><I18nProvider messages={skillMessages}><SkillRoot /><Toaster /></I18nProvider></StrictMode>);
@@ -0,0 +1,11 @@
1
+ from minus_ai_sdk import Pipeline, PipelineContext, StepOutcome
2
+
3
+
4
+ class {{className}}(Pipeline):
5
+
6
+ async def step_1(self, ctx: PipelineContext) -> StepOutcome:
7
+ keywords: str = ctx.entry_params.get("keywords", "")
8
+ country: str = ctx.entry_params.get("country", "US")
9
+
10
+ result = f"Keywords: {keywords} ({country})"
11
+ return StepOutcome.complete(payload={"text": result})
@@ -0,0 +1,7 @@
1
+ {
2
+ "{{namespace}}.step.title": "结果",
3
+ "{{namespace}}.step.empty": "(空)",
4
+ "{{namespace}}.home.placeholder": "输入关键词,如 bluetooth earbuds",
5
+ "{{namespace}}.home.processing": "处理中…",
6
+ "{{namespace}}.home.send": "开始"
7
+ }
@@ -0,0 +1,193 @@
1
+ import { StrictMode, useMemo, useState } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { FlowApp, I18nProvider, mergeMessages, useT, type StepConfig, type StepRecord } from '@minus/widget-framework';
4
+ import { AmazonSearchBar, CompletionPanel, CountrySelect, SearchSubmitButton, validateAsins, validateKeywords, platformWidgetMessages } from '@minus/platform-widgets';
5
+ import { Toaster } from 'sonner';
6
+ import zhCN from './locales/zh-CN.json';
7
+ import enUS from './locales/en-US.json';
8
+
9
+ type InputType = 'keyword' | 'asin';
10
+
11
+ /*
12
+ * 防御性编码提示(适用于所有 step render 函数):
13
+ * - 后端 data 不可信,所有字段都要 ?? 兜底
14
+ * - 数组:(data.list as T[] | undefined) ?? []
15
+ * - 字符串:(data.name as string | undefined) ?? ''
16
+ * - 数字:Number(data.count) || 0
17
+ * - 嵌套:(data.obj as Record<string, unknown> | undefined)?.field ?? fallback
18
+ * 框架已有 Error Boundary 兜底,但不应依赖——优先在数据层做防御。
19
+ */
20
+ function buildSteps(t: (k: string, fb?: string) => string): StepConfig[] {
21
+ return [
22
+ {
23
+ render: ({ data }) => (
24
+ <div className="minus-default-step-done">
25
+ {(data.text as string) ?? t('{{namespace}}.step.empty', '(空)')}
26
+ </div>
27
+ ),
28
+ },
29
+ ];
30
+ }
31
+
32
+ function Home({
33
+ title,
34
+ description,
35
+ useCases,
36
+ tags,
37
+ onStart,
38
+ }: {
39
+ title: string;
40
+ description?: string;
41
+ useCases?: string[];
42
+ tags?: Array<{ code: string; label: string }>;
43
+ onStart: (input: Record<string, unknown>) => Promise<void>;
44
+ }) {
45
+ const t = useT();
46
+ const [inputType, setInputType] = useState<InputType>('keyword');
47
+ const [value, setValue] = useState('');
48
+ const [country, setCountry] = useState('US');
49
+ const [error, setError] = useState<string | null>(null);
50
+ const [loading, setLoading] = useState(false);
51
+
52
+ async function handleSubmit() {
53
+ if (loading) return;
54
+
55
+ if (inputType === 'asin') {
56
+ const { asins, error: asinError } = validateAsins(value);
57
+ if (asinError) {
58
+ setError(t(asinError.key, asinError.fallback, asinError.vars));
59
+ return;
60
+ }
61
+ setError(null);
62
+ setLoading(true);
63
+ try {
64
+ await onStart({ inputType, value: asins.join(','), country });
65
+ } finally {
66
+ setLoading(false);
67
+ }
68
+ } else {
69
+ const { keywords, error: kwError } = validateKeywords(value);
70
+ if (kwError) {
71
+ setError(t(kwError.key, kwError.fallback, kwError.vars));
72
+ return;
73
+ }
74
+ setError(null);
75
+ setLoading(true);
76
+ try {
77
+ await onStart({ inputType, value: keywords.join(','), country });
78
+ } finally {
79
+ setLoading(false);
80
+ }
81
+ }
82
+ }
83
+
84
+ const placeholder = inputType === 'keyword'
85
+ ? t('{{namespace}}.home.placeholderKeyword', '输入关键词,如 bluetooth earbuds')
86
+ : t('{{namespace}}.home.placeholderAsin', '输入一个或多个ASIN');
87
+
88
+ return (
89
+ <>
90
+ <h1>{title}</h1>
91
+ {description && <p className="minus-skill-hero__subtitle">{description}</p>}
92
+ {((useCases && useCases.length > 0) || (tags && tags.length > 0)) && (
93
+ <ul className="minus-skill-hero__use-cases">
94
+ {useCases?.map((uc, i) => (
95
+ <li key={`uc-${i}`} className="minus-skill-hero__use-case-chip">{uc}</li>
96
+ ))}
97
+ {tags?.map((tag) => (
98
+ <li key={`tag-${tag.code}`} className="minus-skill-hero__use-case-chip">{tag.label}</li>
99
+ ))}
100
+ </ul>
101
+ )}
102
+
103
+ <div style={{ display: 'flex', gap: 0, marginBottom: 16 }}>
104
+ {(['keyword', 'asin'] as const).map((type) => (
105
+ <button
106
+ key={type}
107
+ onClick={() => { setInputType(type); setValue(''); setError(null); }}
108
+ style={{
109
+ padding: '6px 16px',
110
+ fontSize: 14,
111
+ border: '1px solid #d1d5db',
112
+ cursor: 'pointer',
113
+ background: inputType === type ? '#111827' : '#fff',
114
+ color: inputType === type ? '#fff' : '#374151',
115
+ borderRadius: type === 'keyword' ? '6px 0 0 6px' : '0 6px 6px 0',
116
+ marginLeft: type === 'asin' ? -1 : 0,
117
+ }}
118
+ >
119
+ {t(`{{namespace}}.home.tab.${type}`, type === 'keyword' ? '关键词' : 'ASIN')}
120
+ </button>
121
+ ))}
122
+ </div>
123
+
124
+ <AmazonSearchBar
125
+ onSubmit={handleSubmit}
126
+ country={<CountrySelect onChange={setCountry} />}
127
+ input={
128
+ <input
129
+ type="text"
130
+ value={value}
131
+ onChange={(e) => { setValue(e.target.value); if (error) setError(null); }}
132
+ placeholder={placeholder}
133
+ spellCheck={false}
134
+ disabled={loading}
135
+ />
136
+ }
137
+ submit={<SearchSubmitButton />}
138
+ error={error}
139
+ />
140
+ </>
141
+ );
142
+ }
143
+
144
+ const rootEl = document.getElementById('root');
145
+ if (!rootEl) throw new Error('#root not found');
146
+
147
+ const skillMessages = mergeMessages(
148
+ platformWidgetMessages,
149
+ { 'zh-CN': zhCN as Record<string, string>, 'en-US': enUS as Record<string, string> },
150
+ );
151
+
152
+ function SkillRoot() {
153
+ const t = useT();
154
+ const steps = useMemo(() => buildSteps(t), [t]);
155
+ return (
156
+ <FlowApp
157
+ steps={steps}
158
+ renderHome={({ flow, onStart }) => <Home title={flow.title} description={flow.description} useCases={flow.useCases} tags={flow.tags} onStart={onStart} />}
159
+ renderCompletion={({ stepHistory }) => {
160
+ const last = stepHistory.reduce<StepRecord | undefined>(
161
+ (acc, r) => (acc && acc.stepNumber > r.stepNumber ? acc : r),
162
+ undefined,
163
+ );
164
+ const data = last?.data ?? {};
165
+ return (
166
+ <CompletionPanel
167
+ title={data.title as string | undefined}
168
+ summary={data.summary as string | undefined}
169
+ filename={data.filename as string | undefined}
170
+ fileId={data.fileId as string | undefined}
171
+ sizeBytes={data.sizeBytes as number | undefined}
172
+ />
173
+ );
174
+ }}
175
+ renderHistoryItem={(entry) => {
176
+ const inp = entry.entryParams as Record<string, string> | undefined;
177
+ return {
178
+ label: inp?.value ?? '—',
179
+ meta: inp?.country || undefined,
180
+ };
181
+ }}
182
+ />
183
+ );
184
+ }
185
+
186
+ createRoot(rootEl).render(
187
+ <StrictMode>
188
+ <I18nProvider messages={skillMessages}>
189
+ <SkillRoot />
190
+ <Toaster />
191
+ </I18nProvider>
192
+ </StrictMode>,
193
+ );
@@ -0,0 +1,12 @@
1
+ from minus_ai_sdk import Pipeline, PipelineContext, StepOutcome
2
+
3
+
4
+ class {{className}}(Pipeline):
5
+
6
+ async def step_1(self, ctx: PipelineContext) -> StepOutcome:
7
+ input_type: str = ctx.entry_params.get("inputType", "keyword")
8
+ value: str = ctx.entry_params.get("value", "")
9
+ country: str = ctx.entry_params.get("country", "US")
10
+
11
+ result = f"[{input_type.upper()}] {value} ({country})"
12
+ return StepOutcome.complete(payload={"text": result})
@@ -0,0 +1,2 @@
1
+ packages:
2
+ - frontend
@@ -0,0 +1,13 @@
1
+ [project]
2
+ name = "minus-skill-{{skillId}}"
3
+ version = "1.0.0"
4
+ description = "{{description}}"
5
+ requires-python = ">=3.12"
6
+ dependencies = [
7
+ "minus-ai-sdk-python @ https://minus-ai-dev.oss-cn-shenzhen.aliyuncs.com/sdks/python/minus_ai_sdk_python-0.1.0-py3-none-any.whl",
8
+ "python-dotenv",
9
+ "uvicorn[standard]",
10
+ ]
11
+
12
+ [tool.setuptools]
13
+ py-modules = ["pipeline", "server"]
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "minus-skill-{{skillId}}",
3
+ "description": "{{description}}",
4
+ "private": true,
5
+ "type": "module",
6
+ "workspaces": ["frontend"],
7
+ "scripts": {
8
+ "dev": "minus-dev-cleanup && mkdir -p .minus && echo $$ > .minus/dev.pid && concurrently -n skill,fe \".venv/bin/uvicorn server:app --port {{port}} --reload --reload-include '*.py' --reload-include '.env.local' --env-file .env.local\" \"cd frontend && pnpm exec vite\"",
9
+ "build": "cd frontend && pnpm run build"
10
+ },
11
+ "devDependencies": {
12
+ "@minus-ai/dev-vite-plugin": "^0.1.0-beta.1",
13
+ "concurrently": "^9.1.2"
14
+ },
15
+ "pnpm": {
16
+ "onlyBuiltDependencies": ["esbuild"]
17
+ }
18
+ }
@@ -0,0 +1,4 @@
1
+ from minus_ai_sdk import SkillApp
2
+ from pipeline import {{className}}
3
+
4
+ app = SkillApp({{className}}).asgi()
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true,
12
+ "noEmit": true
13
+ },
14
+ "include": ["src"]
15
+ }
@@ -0,0 +1,23 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+ import path from 'node:path';
4
+ import { minusDev } from '@minus-ai/dev-vite-plugin';
5
+
6
+ export default defineConfig({
7
+ plugins: [react(), ...minusDev({ localBackend: 'http://localhost:{{port}}' })],
8
+ build: {
9
+ target: 'esnext',
10
+ outDir: path.resolve(__dirname, '../static'),
11
+ emptyOutDir: true,
12
+ rollupOptions: {
13
+ input: {
14
+ embed: path.resolve(__dirname, 'embed.html'),
15
+ },
16
+ },
17
+ },
18
+ base: './',
19
+ server: {
20
+ host: true,
21
+ open: false,
22
+ },
23
+ });
@@ -0,0 +1,10 @@
1
+ {
2
+ "{{namespace}}.step.title": "结果",
3
+ "{{namespace}}.step.empty": "(空)",
4
+ "{{namespace}}.home.tab.keyword": "关键词",
5
+ "{{namespace}}.home.tab.asin": "ASIN",
6
+ "{{namespace}}.home.placeholderKeyword": "输入关键词,如 bluetooth earbuds",
7
+ "{{namespace}}.home.placeholderAsin": "输入一个或多个ASIN",
8
+ "{{namespace}}.home.processing": "处理中…",
9
+ "{{namespace}}.home.send": "开始"
10
+ }