@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.
- package/index.mjs +658 -0
- package/package.json +12 -0
- package/templates/README.md.tpl +123 -0
- package/templates/asin/en-US.json.tpl +7 -0
- package/templates/asin/main.tsx.tpl +95 -0
- package/templates/asin/pipeline.py.tpl +11 -0
- package/templates/asin/zh-CN.json.tpl +7 -0
- package/templates/custom/en-US.json.tpl +8 -0
- package/templates/custom/main.tsx.tpl +93 -0
- package/templates/custom/pipeline.py.tpl +9 -0
- package/templates/custom/zh-CN.json.tpl +8 -0
- package/templates/default/en-US.json.tpl +4 -0
- package/templates/default/main.tsx.tpl +109 -0
- package/templates/default/pipeline.py.tpl +8 -0
- package/templates/default/zh-CN.json.tpl +4 -0
- package/templates/embed.html.tpl +13 -0
- package/templates/en-US.json.tpl +10 -0
- package/templates/env.example.tpl +3 -0
- package/templates/file/en-US.json.tpl +10 -0
- package/templates/file/main.tsx.tpl +104 -0
- package/templates/file/pipeline.py.tpl +11 -0
- package/templates/file/zh-CN.json.tpl +10 -0
- package/templates/frontend-package.json.tpl +22 -0
- package/templates/gitignore.tpl +10 -0
- package/templates/keyword/en-US.json.tpl +7 -0
- package/templates/keyword/main.tsx.tpl +95 -0
- package/templates/keyword/pipeline.py.tpl +11 -0
- package/templates/keyword/zh-CN.json.tpl +7 -0
- package/templates/main.tsx.tpl +193 -0
- package/templates/pipeline.py.tpl +12 -0
- package/templates/pnpm-workspace.yaml.tpl +2 -0
- package/templates/pyproject.toml.tpl +13 -0
- package/templates/root-package.json.tpl +18 -0
- package/templates/server.py.tpl +4 -0
- package/templates/tsconfig.json.tpl +15 -0
- package/templates/vite.config.ts.tpl +23 -0
- 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,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,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,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,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,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
|
+
}
|