@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,123 @@
1
+ # {{displayName}}
2
+
3
+ Minus Skill 项目,由 `create-skill` 脚手架生成。
4
+
5
+ ## 环境要求
6
+
7
+ | 依赖 | 最低版本 | 说明 |
8
+ |---|---|---|
9
+ | Node.js | >= 18 | 前端构建和开发服务 |
10
+ | pnpm | >= 9 | 包管理(全局 store 共享,多 skill 不重复下载) |
11
+ | Python | >= 3.12 | 后端 Pipeline 运行时 |
12
+ | uv | >= 0.4 | Python 包管理(全局缓存,秒级安装) |
13
+ | Git | >= 2 | 版本管理 |
14
+
15
+ ## 项目结构
16
+
17
+ ```
18
+ {{folder}}/
19
+ ├── pipeline.py # 后端 Pipeline 逻辑(step_1, step_2 ...)
20
+ ├── server.py # ASGI 服务入口
21
+ ├── pyproject.toml # Python 依赖声明
22
+ ├── package.json # Node 根配置(workspaces + dev 脚本)
23
+ ├── .env.local # 运行时凭证(不提交到 Git)
24
+ ├── .env.example # 凭证示例
25
+ ├── .minus/skill.json # Skill ID 标识
26
+ ├── frontend/
27
+ │ ├── src/main.tsx # 前端入口(输入表单 + 结果渲染)
28
+ │ ├── src/locales/ # 多语言文件(zh-CN / en-US)
29
+ │ ├── embed.html # iframe 宿主页
30
+ │ ├── vite.config.ts # Vite 构建配置
31
+ │ └── package.json # 前端依赖
32
+ └── tests/ # 测试用例
33
+ ```
34
+
35
+ ## 安装依赖
36
+
37
+ ### 前端(Node)
38
+
39
+ ```bash
40
+ pnpm install
41
+ ```
42
+
43
+ ### 后端(Python)
44
+
45
+ ```bash
46
+ uv venv .venv
47
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
48
+ uv pip install -e .
49
+ ```
50
+
51
+ ## 配置
52
+
53
+ 复制环境变量文件并填入真实凭证:
54
+
55
+ ```bash
56
+ cp .env.example .env.local
57
+ ```
58
+
59
+ `.env.local` 中需要配置:
60
+
61
+ | 变量 | 说明 |
62
+ |---|---|
63
+ | `MINUS_AI_SKILL_API_KEY` | Skill 的 API Key(创建时自动生成) |
64
+ | `MINUS_AI_PLATFORM_URL` | 平台地址(默认已填好) |
65
+
66
+ ## 开发
67
+
68
+ 一键启动前后端开发服务:
69
+
70
+ ```bash
71
+ pnpm run dev
72
+ ```
73
+
74
+ 这会同时启动:
75
+ - **后端** — `uvicorn` 监听 `http://localhost:{{port}}`,修改 `.py` 文件自动重载
76
+ - **前端** — `vite` 开发服务,修改前端代码自动热更新
77
+
78
+ 单独启动前端:
79
+
80
+ ```bash
81
+ cd frontend && pnpm exec vite
82
+ ```
83
+
84
+ ## 构建
85
+
86
+ ```bash
87
+ pnpm run build
88
+ ```
89
+
90
+ 产物输出到 `static/` 目录。
91
+
92
+ ## 发布
93
+
94
+ 在 Claude Code 中使用命令发布到 Minus 平台:
95
+
96
+ ```
97
+ /minus-publish
98
+ ```
99
+
100
+ 或在 Minus 开发模式下:
101
+
102
+ ```
103
+ /minus publish
104
+ ```
105
+
106
+ ## 常用开发命令
107
+
108
+ | 命令 | 说明 |
109
+ |---|---|
110
+ | `pnpm run dev` | 启动开发环境(前后端) |
111
+ | `pnpm run build` | 构建前端产物 |
112
+ | `cd frontend && pnpm exec vite` | 仅启动前端 |
113
+ | `.venv/bin/uvicorn server:app --port {{port}} --reload --env-file .env.local` | 仅启动后端 |
114
+
115
+ ## 进入开发模式(Claude Code)
116
+
117
+ 在项目目录下启动 Claude Code,会自动激活 Minus 开发环境:
118
+
119
+ ```bash
120
+ cd ~/minus/{{folder}} && claude
121
+ ```
122
+
123
+ 进入后输入 `/minus` 即可开始开发。
@@ -0,0 +1,7 @@
1
+ {
2
+ "{{namespace}}.step.title": "Result",
3
+ "{{namespace}}.step.empty": "(empty)",
4
+ "{{namespace}}.home.placeholder": "Enter one or more ASINs",
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, validateAsins, 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 | undefined) ?? 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 { asins, error: asinError } = validateAsins(value);
40
+ if (asinError) { setError(t(asinError.key, asinError.fallback, asinError.vars)); return; }
41
+ setError(null);
42
+ setLoading(true);
43
+ try { await onStart({ asins: asins.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={<input type="text" value={value} onChange={(e) => { setValue(e.target.value); if (error) setError(null); }} placeholder={t('{{namespace}}.home.placeholder', '输入一个或多个ASIN')} spellCheck={false} disabled={loading} />}
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?.asins ?? '—',
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
+ asins: str = ctx.entry_params.get("asins", "")
8
+ country: str = ctx.entry_params.get("country", "US")
9
+
10
+ result = f"ASINs: {asins} ({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": "输入一个或多个ASIN",
5
+ "{{namespace}}.home.processing": "处理中…",
6
+ "{{namespace}}.home.send": "开始"
7
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "{{namespace}}.step.title": "Result",
3
+ "{{namespace}}.step.empty": "(empty)",
4
+ "{{namespace}}.home.fieldLabel": "Input",
5
+ "{{namespace}}.home.placeholder": "Enter...",
6
+ "{{namespace}}.home.processing": "Processing…",
7
+ "{{namespace}}.home.send": "Start"
8
+ }
@@ -0,0 +1,93 @@
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 { 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 [loading, setLoading] = useState(false);
34
+
35
+ async function handleSubmit() {
36
+ if (!value.trim() || loading) return;
37
+ setLoading(true);
38
+ try { await onStart({ text: value.trim() }); } finally { setLoading(false); }
39
+ }
40
+
41
+ return (
42
+ <div style={{ maxWidth: 480, padding: '24px 0' }}>
43
+ <h2 style={{ margin: '0 0 4px', fontSize: 20, fontWeight: 600 }}>{title}</h2>
44
+ {description && <p className="minus-skill-hero__subtitle">{description}</p>}
45
+ {((useCases && useCases.length > 0) || (tags && tags.length > 0)) && (
46
+ <ul className="minus-skill-hero__use-cases" style={{ justifyContent: 'flex-start' }}>
47
+ {useCases?.map((uc, i) => (
48
+ <li key={`uc-${i}`} className="minus-skill-hero__use-case-chip">{uc}</li>
49
+ ))}
50
+ {tags?.map((tag) => (
51
+ <li key={`tag-${tag.code}`} className="minus-skill-hero__use-case-chip">{tag.label}</li>
52
+ ))}
53
+ </ul>
54
+ )}
55
+ <div style={{ background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8, padding: 20, display: 'flex', flexDirection: 'column', gap: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.04)' }}>
56
+ <label className="minus-field">
57
+ <span>{t('{{namespace}}.home.fieldLabel', '输入')}</span>
58
+ <input type="text" value={value} onChange={(e) => setValue(e.target.value)} placeholder={t('{{namespace}}.home.placeholder', '请输入...')} onKeyDown={(e) => e.key === 'Enter' && handleSubmit()} />
59
+ </label>
60
+ <button className="minus-btn minus-btn-primary" onClick={handleSubmit} disabled={!value.trim() || loading}>
61
+ {loading ? t('{{namespace}}.home.processing', '处理中…') : t('{{namespace}}.home.send', '开始')}
62
+ </button>
63
+ </div>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ const rootEl = document.getElementById('root');
69
+ if (!rootEl) throw new Error('#root not found');
70
+
71
+ const skillMessages = mergeMessages(platformWidgetMessages, { 'zh-CN': zhCN as Record<string, string>, 'en-US': enUS as Record<string, string> });
72
+
73
+ function SkillRoot() {
74
+ const t = useT();
75
+ const steps = useMemo(() => buildSteps(t), [t]);
76
+ return (
77
+ <FlowApp
78
+ steps={steps}
79
+ homeLayout="left"
80
+ renderHome={({ flow, onStart }) => <Home title={flow.title} description={flow.description} useCases={flow.useCases} tags={flow.tags} onStart={onStart} />}
81
+ renderCompletion={() => null}
82
+ renderHistoryItem={(entry) => {
83
+ const inp = entry.entryParams as Record<string, string> | undefined;
84
+ return {
85
+ label: inp?.text ?? '—',
86
+ meta: inp?.country || undefined,
87
+ };
88
+ }}
89
+ />
90
+ );
91
+ }
92
+
93
+ createRoot(rootEl).render(<StrictMode><I18nProvider messages={skillMessages}><SkillRoot /><Toaster /></I18nProvider></StrictMode>);
@@ -0,0 +1,9 @@
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
+ text: str = ctx.entry_params.get("text", "")
8
+
9
+ return StepOutcome.complete(payload={"text": text})
@@ -0,0 +1,8 @@
1
+ {
2
+ "{{namespace}}.step.title": "结果",
3
+ "{{namespace}}.step.empty": "(空)",
4
+ "{{namespace}}.home.fieldLabel": "输入",
5
+ "{{namespace}}.home.placeholder": "请输入...",
6
+ "{{namespace}}.home.processing": "处理中…",
7
+ "{{namespace}}.home.send": "开始"
8
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "{{namespace}}.step.title": "Result",
3
+ "{{namespace}}.step.empty": "(empty)"
4
+ }
@@ -0,0 +1,109 @@
1
+ import { StrictMode, useMemo } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { FlowApp, I18nProvider, mergeMessages, useT, type StepConfig } from '@minus/widget-framework';
4
+ import { CompletionPanel, 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 | undefined) ?? t('{{namespace}}.step.empty', '(空)')}
24
+ </div>
25
+ ),
26
+ },
27
+ ];
28
+ }
29
+
30
+ function Home({
31
+ title,
32
+ description,
33
+ useCases,
34
+ tags,
35
+ }: {
36
+ title: string;
37
+ description?: string;
38
+ useCases?: string[];
39
+ tags?: Array<{ code: string; label: string }>;
40
+ }) {
41
+ return (
42
+ <>
43
+ <h1>{title}</h1>
44
+ {description && <p className="minus-skill-hero__subtitle">{description}</p>}
45
+ {((useCases && useCases.length > 0) || (tags && tags.length > 0)) && (
46
+ <ul className="minus-skill-hero__use-cases">
47
+ {useCases?.map((uc, i) => (
48
+ <li key={`uc-${i}`} className="minus-skill-hero__use-case-chip">{uc}</li>
49
+ ))}
50
+ {tags?.map((tag) => (
51
+ <li key={`tag-${tag.code}`} className="minus-skill-hero__use-case-chip">{tag.label}</li>
52
+ ))}
53
+ </ul>
54
+ )}
55
+ </>
56
+ );
57
+ }
58
+
59
+ const rootEl = document.getElementById('root');
60
+ if (!rootEl) throw new Error('#root not found');
61
+
62
+ const skillMessages = mergeMessages(
63
+ platformWidgetMessages,
64
+ { 'zh-CN': zhCN as Record<string, string>, 'en-US': enUS as Record<string, string> },
65
+ );
66
+
67
+ function SkillRoot() {
68
+ const t = useT();
69
+ const steps = useMemo(() => buildSteps(t), [t]);
70
+ return (
71
+ <FlowApp
72
+ steps={steps}
73
+ renderHome={({ flow }) => (
74
+ <Home title={flow.title} description={flow.description} useCases={flow.useCases} tags={flow.tags} />
75
+ )}
76
+ renderCompletion={({ stepHistory }) => {
77
+ const last = stepHistory.reduce<import('@minus/widget-framework').StepRecord | undefined>(
78
+ (acc, r) => (acc && acc.stepNumber > r.stepNumber ? acc : r),
79
+ undefined,
80
+ );
81
+ const data = last?.data ?? {};
82
+ return (
83
+ <CompletionPanel
84
+ title={data.title as string | undefined}
85
+ summary={data.summary as string | undefined}
86
+ filename={data.filename as string | undefined}
87
+ fileId={data.fileId as string | undefined}
88
+ sizeBytes={data.sizeBytes as number | undefined}
89
+ />
90
+ );
91
+ }}
92
+ renderHistoryItem={(entry) => {
93
+ const inp = entry.entryParams as Record<string, string> | undefined;
94
+ return {
95
+ label: inp?.text ?? '—',
96
+ };
97
+ }}
98
+ />
99
+ );
100
+ }
101
+
102
+ createRoot(rootEl).render(
103
+ <StrictMode>
104
+ <I18nProvider messages={skillMessages}>
105
+ <SkillRoot />
106
+ <Toaster />
107
+ </I18nProvider>
108
+ </StrictMode>,
109
+ );
@@ -0,0 +1,8 @@
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
+ # TODO: implement step logic
8
+ return StepOutcome.complete(payload={"text": "done"})
@@ -0,0 +1,4 @@
1
+ {
2
+ "{{namespace}}.step.title": "结果",
3
+ "{{namespace}}.step.empty": "(空)"
4
+ }
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>minus-{{displayName}}</title>
7
+ <script async src="//at.alicdn.com/t/c/font_4940197_un471494j1t.js"></script>
8
+ </head>
9
+ <body style="margin:0">
10
+ <div id="root"></div>
11
+ <script type="module" src="./src/main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,10 @@
1
+ {
2
+ "{{namespace}}.step.title": "Result",
3
+ "{{namespace}}.step.empty": "(empty)",
4
+ "{{namespace}}.home.tab.keyword": "Keyword",
5
+ "{{namespace}}.home.tab.asin": "ASIN",
6
+ "{{namespace}}.home.placeholderKeyword": "Enter keyword, e.g. bluetooth earbuds",
7
+ "{{namespace}}.home.placeholderAsin": "Enter one or more ASINs",
8
+ "{{namespace}}.home.processing": "Processing…",
9
+ "{{namespace}}.home.send": "Start"
10
+ }
@@ -0,0 +1,3 @@
1
+ MINUS_AI_SKILL_API_KEY=ska_REPLACE_ME_WITH_REAL_KEY
2
+ MINUS_AI_SKILL_ID=skl_REPLACE_ME_WITH_SKILL_ID
3
+ MINUS_AI_PLATFORM_URL=http://47.107.144.22:18990
@@ -0,0 +1,10 @@
1
+ {
2
+ "{{namespace}}.step.title": "Result",
3
+ "{{namespace}}.step.empty": "(empty)",
4
+ "{{namespace}}.home.fieldLabel": "File",
5
+ "{{namespace}}.home.filePlaceholder": "Click to select a file",
6
+ "{{namespace}}.home.fileEmpty": "Please select a file",
7
+ "{{namespace}}.home.uploadFailed": "File upload failed",
8
+ "{{namespace}}.home.processing": "Uploading…",
9
+ "{{namespace}}.home.send": "Start"
10
+ }
@@ -0,0 +1,104 @@
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 { FilePicker, uploadFile, 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 [file, setFile] = useState<File | null>(null);
33
+ const [error, setError] = useState<string | null>(null);
34
+ const [loading, setLoading] = useState(false);
35
+
36
+ async function handleSubmit() {
37
+ if (loading) return;
38
+ if (!file) { setError(t('{{namespace}}.home.fileEmpty', '请选择一个文件')); return; }
39
+ setError(null);
40
+ setLoading(true);
41
+ try {
42
+ const { fileId } = await uploadFile(file);
43
+ await onStart({ fileId, fileName: file.name });
44
+ } catch (e: any) {
45
+ setError(e?.message ?? t('{{namespace}}.home.uploadFailed', '文件上传失败'));
46
+ } finally {
47
+ setLoading(false);
48
+ }
49
+ }
50
+
51
+ return (
52
+ <div style={{ maxWidth: 480, padding: '24px 0' }}>
53
+ <h2 style={{ margin: '0 0 4px', fontSize: 20, fontWeight: 600 }}>{title}</h2>
54
+ {description && <p className="minus-skill-hero__subtitle">{description}</p>}
55
+ {((useCases && useCases.length > 0) || (tags && tags.length > 0)) && (
56
+ <ul className="minus-skill-hero__use-cases" style={{ justifyContent: 'flex-start' }}>
57
+ {useCases?.map((uc, i) => (
58
+ <li key={`uc-${i}`} className="minus-skill-hero__use-case-chip">{uc}</li>
59
+ ))}
60
+ {tags?.map((tag) => (
61
+ <li key={`tag-${tag.code}`} className="minus-skill-hero__use-case-chip">{tag.label}</li>
62
+ ))}
63
+ </ul>
64
+ )}
65
+ <div style={{ background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8, padding: 20, display: 'flex', flexDirection: 'column', gap: 16, boxShadow: '0 1px 2px rgba(0,0,0,0.04)' }}>
66
+ <label className="minus-field">
67
+ <span>{t('{{namespace}}.home.fieldLabel', '文件')}</span>
68
+ <FilePicker value={file} onChange={(f) => { setFile(f); if (error) setError(null); }} disabled={loading} />
69
+ </label>
70
+ {error && <p style={{ margin: 0, fontSize: 13, color: 'var(--minus-color-danger, #ef4444)' }}>{error}</p>}
71
+ <button className="minus-btn minus-btn-primary" onClick={handleSubmit} disabled={!file || loading}>
72
+ {loading ? t('{{namespace}}.home.processing', '上传中…') : t('{{namespace}}.home.send', '开始')}
73
+ </button>
74
+ </div>
75
+ </div>
76
+ );
77
+ }
78
+
79
+ const rootEl = document.getElementById('root');
80
+ if (!rootEl) throw new Error('#root not found');
81
+
82
+ const skillMessages = mergeMessages(platformWidgetMessages, { 'zh-CN': zhCN as Record<string, string>, 'en-US': enUS as Record<string, string> });
83
+
84
+ function SkillRoot() {
85
+ const t = useT();
86
+ const steps = useMemo(() => buildSteps(t), [t]);
87
+ return (
88
+ <FlowApp
89
+ steps={steps}
90
+ homeLayout="left"
91
+ renderHome={({ flow, onStart }) => <Home title={flow.title} description={flow.description} useCases={flow.useCases} tags={flow.tags} onStart={onStart} />}
92
+ renderCompletion={() => null}
93
+ renderHistoryItem={(entry) => {
94
+ const inp = entry.entryParams as Record<string, string> | undefined;
95
+ return {
96
+ label: inp?.fileName ?? '—',
97
+ meta: inp?.country || undefined,
98
+ };
99
+ }}
100
+ />
101
+ );
102
+ }
103
+
104
+ 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
+ file_id: str = ctx.entry_params.get("fileId", "")
8
+ file_name: str = ctx.entry_params.get("fileName", "")
9
+
10
+ result = f"File: {file_name} (id: {file_id})"
11
+ return StepOutcome.complete(payload={"text": result})
@@ -0,0 +1,10 @@
1
+ {
2
+ "{{namespace}}.step.title": "结果",
3
+ "{{namespace}}.step.empty": "(空)",
4
+ "{{namespace}}.home.fieldLabel": "文件",
5
+ "{{namespace}}.home.filePlaceholder": "点击选择文件",
6
+ "{{namespace}}.home.fileEmpty": "请选择一个文件",
7
+ "{{namespace}}.home.uploadFailed": "文件上传失败",
8
+ "{{namespace}}.home.processing": "上传中…",
9
+ "{{namespace}}.home.send": "开始"
10
+ }