lite-questionnaire 1.0.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/LICENSE +21 -0
- package/README.md +358 -0
- package/core.ts +477 -0
- package/design/questionnaire-openapi.yaml +522 -0
- package/index.ts +396 -0
- package/input.ts +513 -0
- package/modules/confirm.ts +42 -0
- package/modules/multiSelect.ts +100 -0
- package/modules/rating.ts +105 -0
- package/modules/select.ts +118 -0
- package/modules/shared.ts +10 -0
- package/modules/text.ts +71 -0
- package/package.json +39 -0
- package/render.ts +319 -0
- package/skills/lite-questionnaire/SKILL.md +276 -0
- package/state.ts +39 -0
- package/types.ts +137 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lite-questionnaire
|
|
3
|
+
description: >
|
|
4
|
+
Use the `questionnaire` tool to collect structured information from users via
|
|
5
|
+
interactive forms. Supports select (single choice), multiSelect (checkboxes),
|
|
6
|
+
text (single/multiline), confirm (Y/N), and rating (1-5 slider). Use when you
|
|
7
|
+
need to ask 2+ questions together, when questions depend on previous answers
|
|
8
|
+
(conditional sub-questions), when you need input validation, or when you want
|
|
9
|
+
session-persisted drafts. Do NOT use for a single yes/no or trivial
|
|
10
|
+
one-question prompts — ask those inline instead.
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# lite-questionnaire — interactive form skill
|
|
14
|
+
|
|
15
|
+
## When to use
|
|
16
|
+
|
|
17
|
+
- 2+ questions that belong together (e.g., project scaffolding wizard)
|
|
18
|
+
- Answers affect later questions (conditional sub-questions)
|
|
19
|
+
- Input needs validation (min/max length, regex, min/max selections)
|
|
20
|
+
- You want the user to be able to navigate back and edit previous answers
|
|
21
|
+
- Reusable question sets (session persistence restores drafts across calls)
|
|
22
|
+
|
|
23
|
+
## When NOT to use
|
|
24
|
+
|
|
25
|
+
- Single trivial question — ask inline
|
|
26
|
+
- Simple yes/no — use `confirm` question type directly, or ask inline
|
|
27
|
+
- Purely informational prompts — no answer collection needed
|
|
28
|
+
|
|
29
|
+
## Tool name
|
|
30
|
+
|
|
31
|
+
The pi tool is registered as `questionnaire`. Invoke it with:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
questionnaire({ questions: [...] })
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick reference — question types
|
|
38
|
+
|
|
39
|
+
| type | icon | maxSelect | key fields | default |
|
|
40
|
+
|------|------|-----------|------------|---------|
|
|
41
|
+
| `select` | ○ | 1 (fixed) | `options` | none |
|
|
42
|
+
| `multiSelect` | ☐ | >= 2 | `options`, `constraints` | none |
|
|
43
|
+
| `text` | ✎ | — | `placeholder`, `multiline` | none (required) |
|
|
44
|
+
| `confirm` | ✓/✗ | — | `yesLabel`, `noLabel` | yes |
|
|
45
|
+
| `rating` | ★ | — | `range`, `showEmoji`, `annotations` | mid-value (3) |
|
|
46
|
+
|
|
47
|
+
## Common patterns
|
|
48
|
+
|
|
49
|
+
### Project scaffolding wizard
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"questions": [
|
|
54
|
+
{
|
|
55
|
+
"id": "projectName",
|
|
56
|
+
"type": "text",
|
|
57
|
+
"label": "名称",
|
|
58
|
+
"prompt": "项目名称?",
|
|
59
|
+
"placeholder": "my-app",
|
|
60
|
+
"constraints": [
|
|
61
|
+
{ "type": "minLength", "value": 2, "message": "项目名至少 2 个字符" },
|
|
62
|
+
{ "type": "pattern", "value": "^[a-z0-9-]+$", "message": "仅允许小写字母、数字和连字符" }
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "language",
|
|
67
|
+
"type": "select",
|
|
68
|
+
"maxSelect": 1,
|
|
69
|
+
"label": "语言",
|
|
70
|
+
"prompt": "使用哪种编程语言?",
|
|
71
|
+
"options": [
|
|
72
|
+
{ "value": "ts", "label": "TypeScript" },
|
|
73
|
+
{ "value": "py", "label": "Python" },
|
|
74
|
+
{ "value": "rs", "label": "Rust" },
|
|
75
|
+
{ "value": "go", "label": "Go" }
|
|
76
|
+
],
|
|
77
|
+
"children": [
|
|
78
|
+
{
|
|
79
|
+
"id": "framework",
|
|
80
|
+
"type": "select",
|
|
81
|
+
"maxSelect": 1,
|
|
82
|
+
"label": "框架",
|
|
83
|
+
"prompt": "使用哪个框架?",
|
|
84
|
+
"showIf": { "value": "ts" },
|
|
85
|
+
"options": [
|
|
86
|
+
{ "value": "react", "label": "React" },
|
|
87
|
+
{ "value": "vue", "label": "Vue" },
|
|
88
|
+
{ "value": "express", "label": "Express" }
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"id": "pyFramework",
|
|
93
|
+
"type": "select",
|
|
94
|
+
"maxSelect": 1,
|
|
95
|
+
"label": "框架",
|
|
96
|
+
"prompt": "使用哪个框架?",
|
|
97
|
+
"showIf": { "value": "py" },
|
|
98
|
+
"options": [
|
|
99
|
+
{ "value": "fastapi", "label": "FastAPI" },
|
|
100
|
+
{ "value": "django", "label": "Django" },
|
|
101
|
+
{ "value": "flask", "label": "Flask" }
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"id": "features",
|
|
108
|
+
"type": "multiSelect",
|
|
109
|
+
"maxSelect": 5,
|
|
110
|
+
"label": "功能",
|
|
111
|
+
"prompt": "需要哪些功能?",
|
|
112
|
+
"options": [
|
|
113
|
+
{ "value": "auth", "label": "用户认证" },
|
|
114
|
+
{ "value": "api", "label": "REST API" },
|
|
115
|
+
{ "value": "db", "label": "数据库" },
|
|
116
|
+
{ "value": "ws", "label": "WebSocket" },
|
|
117
|
+
{ "value": "file", "label": "文件上传" }
|
|
118
|
+
],
|
|
119
|
+
"constraints": [
|
|
120
|
+
{ "type": "minSelect", "value": 1, "message": "至少选择一项功能" }
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"id": "description",
|
|
125
|
+
"type": "text",
|
|
126
|
+
"label": "描述",
|
|
127
|
+
"prompt": "简要描述项目目标(可选)",
|
|
128
|
+
"placeholder": "一个高性能的...",
|
|
129
|
+
"multiline": true,
|
|
130
|
+
"constraints": [
|
|
131
|
+
{ "type": "maxLength", "value": 500, "message": "描述不超过 500 字" }
|
|
132
|
+
]
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Code review survey
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"questions": [
|
|
143
|
+
{
|
|
144
|
+
"id": "quality",
|
|
145
|
+
"type": "rating",
|
|
146
|
+
"label": "质量",
|
|
147
|
+
"prompt": "代码整体质量评分",
|
|
148
|
+
"range": { "min": 1, "max": 5 },
|
|
149
|
+
"showEmoji": true,
|
|
150
|
+
"annotations": { "1": "需要重写", "3": "可接受", "5": "非常优秀" }
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"id": "issues",
|
|
154
|
+
"type": "multiSelect",
|
|
155
|
+
"maxSelect": 4,
|
|
156
|
+
"label": "问题",
|
|
157
|
+
"prompt": "发现哪些问题?",
|
|
158
|
+
"options": [
|
|
159
|
+
{ "value": "perf", "label": "性能问题" },
|
|
160
|
+
{ "value": "security", "label": "安全隐患" },
|
|
161
|
+
{ "value": "style", "label": "代码风格" },
|
|
162
|
+
{ "value": "tests", "label": "测试覆盖不足" }
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"id": "approve",
|
|
167
|
+
"type": "confirm",
|
|
168
|
+
"label": "审批",
|
|
169
|
+
"prompt": "是否批准合并?",
|
|
170
|
+
"yesLabel": "批准",
|
|
171
|
+
"noLabel": "拒绝"
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### User preference picker
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"questions": [
|
|
182
|
+
{
|
|
183
|
+
"id": "theme",
|
|
184
|
+
"type": "select",
|
|
185
|
+
"maxSelect": 1,
|
|
186
|
+
"label": "主题",
|
|
187
|
+
"prompt": "选择编辑器主题",
|
|
188
|
+
"options": [
|
|
189
|
+
{ "value": "dark", "label": "暗色", "description": "护眼,适合夜间 coding" },
|
|
190
|
+
{ "value": "light", "label": "亮色", "description": "清晰,适合白天工作" },
|
|
191
|
+
{ "value": "high-contrast", "label": "高对比度", "description": "无障碍友好" }
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"id": "fontSize",
|
|
196
|
+
"type": "rating",
|
|
197
|
+
"label": "字号",
|
|
198
|
+
"prompt": "编辑器字号偏好",
|
|
199
|
+
"range": { "min": 1, "max": 5 },
|
|
200
|
+
"annotations": { "1": "很小 (10px)", "3": "适中 (14px)", "5": "很大 (20px)" }
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Conditional sub-questions
|
|
207
|
+
|
|
208
|
+
Use `children` + `showIf` to branch questions based on previous answers:
|
|
209
|
+
|
|
210
|
+
- `children` goes on the parent question
|
|
211
|
+
- Each child question has `showIf: { "value": "<parent answer value>" }`
|
|
212
|
+
- When the parent answer matches `showIf.value`, the child is inserted right after the parent in the tab sequence
|
|
213
|
+
- Children can nest their own `children` arbitrarily deep
|
|
214
|
+
- Sub-questions automatically expand into the flat tab sequence — the user navigates them with ←/→
|
|
215
|
+
|
|
216
|
+
## Constraints
|
|
217
|
+
|
|
218
|
+
Available validation rules:
|
|
219
|
+
|
|
220
|
+
| type | applies to | value | example message |
|
|
221
|
+
|------|-----------|-------|-----------------|
|
|
222
|
+
| `required` | all | — | "此问题必须回答" |
|
|
223
|
+
| `minSelect` | multiSelect | number | "至少选择 1 项" |
|
|
224
|
+
| `maxSelect` | multiSelect | number | "最多选择 3 项" |
|
|
225
|
+
| `minLength` | text | number | "至少输入 10 个字符" |
|
|
226
|
+
| `maxLength` | text | number | "不超过 500 个字符" |
|
|
227
|
+
| `pattern` | text | regex string | "仅允许字母和数字" |
|
|
228
|
+
|
|
229
|
+
- Constraints are validated on submission; invalid answers block progress.
|
|
230
|
+
- `maxSelect` for `select` is always 1 (single choice).
|
|
231
|
+
- `maxSelect` for `multiSelect` must be >= 2.
|
|
232
|
+
- `rating` `range` is locked to `{ min: 1, max: 5 }`.
|
|
233
|
+
|
|
234
|
+
## Result handling
|
|
235
|
+
|
|
236
|
+
The tool returns `QuestionnaireResult`:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
// Normal submission
|
|
240
|
+
{
|
|
241
|
+
"answers": {
|
|
242
|
+
"projectName": { "text": "my-app" },
|
|
243
|
+
"language": { "value": "ts", "label": "TypeScript", "wasCustom": false },
|
|
244
|
+
"features": { "values": ["auth","api"], "labels": ["用户认证","REST API"], "wasCustom": false },
|
|
245
|
+
"quality": { "value": 4, "annotation": "" },
|
|
246
|
+
"approve": { "confirmed": true, "label": "批准" }
|
|
247
|
+
},
|
|
248
|
+
"submittedAt": "2026-05-26T12:00:00.000Z"
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// User pressed Esc
|
|
252
|
+
{
|
|
253
|
+
"cancelled": true,
|
|
254
|
+
"message": "User cancelled the questionnaire"
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
After receiving results, extract answers by `result.answers.<questionId>` and proceed with the next step. If `cancelled` is true, stop and inform the user.
|
|
259
|
+
|
|
260
|
+
## Session persistence
|
|
261
|
+
|
|
262
|
+
- Questionnaire state persists across TUI sessions automatically.
|
|
263
|
+
- Calling the tool again with the **same `questions` list** restores previous answers and progress.
|
|
264
|
+
- Esc cancels but does NOT clear drafts — next call restores them.
|
|
265
|
+
- Submission clears state — next call with same questions starts fresh.
|
|
266
|
+
- You (the AI) don't need to handle this — just call `questionnaire` normally.
|
|
267
|
+
|
|
268
|
+
## Tips
|
|
269
|
+
|
|
270
|
+
1. **Labels matter**: Tab bar is narrow; keep `label` to 2–4 Chinese characters or 6–8 ASCII characters.
|
|
271
|
+
2. **Add descriptions to options**: `description` helps users understand nuanced choices.
|
|
272
|
+
3. **Always validate text**: Use `constraints` with `minLength`/`maxLength`/`pattern` for text fields.
|
|
273
|
+
4. **Emoji for satisfaction**: Set `showEmoji: true` on rating questions for satisfaction surveys — 😡😟😐😊😍 gives better UX.
|
|
274
|
+
5. **Custom option is automatic**: Select/multiSelect always have a trailing "custom" option — users can type free-form answers.
|
|
275
|
+
6. **Single-question form**: When there's only 1 question, there's no submit page — the answer is returned immediately.
|
|
276
|
+
7. **Reuse question sets**: Because of session persistence, identical `questions` arrays will restore drafts. Use consistent `id` values for recurring forms.
|
package/state.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Questionnaire 会话持久化
|
|
3
|
+
*
|
|
4
|
+
* 支持暂存/恢复问卷状态,跨 TUI 会话保持答案。
|
|
5
|
+
* 通过 pi.appendEntry() 将状态写入会话文件。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import type { Core, QuestionnaireSnapshot } from "./core";
|
|
10
|
+
|
|
11
|
+
/** 会话条目标记 */
|
|
12
|
+
const SNAPSHOT_CUSTOM_TYPE = "questionnaire_snapshot";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 保存当前问卷状态到会话文件。
|
|
16
|
+
*/
|
|
17
|
+
export function saveSnapshot(pi: ExtensionAPI, core: Core, key?: string): void {
|
|
18
|
+
const snapshot = { ...core.serialize(), key };
|
|
19
|
+
pi.appendEntry(SNAPSHOT_CUSTOM_TYPE, snapshot);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 从会话文件恢复问卷状态。
|
|
24
|
+
* 返回恢复后的快照,如果没有保存的状态则返回 null。
|
|
25
|
+
*/
|
|
26
|
+
export function loadSnapshot(
|
|
27
|
+
entries: Array<{ type: string; customType?: string; data?: unknown }>,
|
|
28
|
+
key?: string,
|
|
29
|
+
): QuestionnaireSnapshot | null {
|
|
30
|
+
// 从后往前查找最近的、属于当前问卷结构的快照
|
|
31
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
32
|
+
const entry = entries[i];
|
|
33
|
+
if (entry.type !== "custom" || entry.customType !== SNAPSHOT_CUSTOM_TYPE || !entry.data) continue;
|
|
34
|
+
const snapshot = entry.data as QuestionnaireSnapshot;
|
|
35
|
+
if (key && snapshot.key !== key) continue;
|
|
36
|
+
return snapshot;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
package/types.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Questionnaire 类型定义
|
|
3
|
+
*
|
|
4
|
+
* 定义所有问题类型、约束条件、答案结构和状态接口。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ─── 选项 ────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export interface Option {
|
|
10
|
+
value: string; // 返回的值
|
|
11
|
+
label: string; // 显示标签
|
|
12
|
+
description?: string; // 可选描述
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ─── 约束条件 ──────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export interface Constraint {
|
|
18
|
+
type: "required" | "minSelect" | "maxSelect" | "minLength" | "maxLength" | "pattern";
|
|
19
|
+
value?: number | string; // 约束参数值
|
|
20
|
+
message: string; // 校验失败时的错误提示
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ─── 基础问题 ─────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface BaseQuestion {
|
|
26
|
+
id: string; // 唯一标识
|
|
27
|
+
label: string; // Tab 栏短标签
|
|
28
|
+
prompt: string; // 完整问题文本
|
|
29
|
+
constraints?: Constraint[]; // 约束条件对象数组
|
|
30
|
+
children?: Question[]; // 条件子问题(分步插入)
|
|
31
|
+
showIf?: { value: string }; // 仅当父问题答案为指定值时显示
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── 具体问题类型 ─────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export interface SelectQuestion extends BaseQuestion {
|
|
37
|
+
type: "select";
|
|
38
|
+
maxSelect: 1;
|
|
39
|
+
options: Option[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface MultiSelectQuestion extends BaseQuestion {
|
|
43
|
+
type: "multiSelect";
|
|
44
|
+
maxSelect: number; // >1
|
|
45
|
+
options: Option[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface TextQuestion extends BaseQuestion {
|
|
49
|
+
type: "text";
|
|
50
|
+
placeholder?: string; // 输入框占位符
|
|
51
|
+
multiline?: boolean; // 是否多行文本编辑
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ConfirmQuestion extends BaseQuestion {
|
|
55
|
+
type: "confirm";
|
|
56
|
+
yesLabel?: string; // 默认 "是"
|
|
57
|
+
noLabel?: string; // 默认 "否"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface RatingQuestion extends BaseQuestion {
|
|
61
|
+
type: "rating";
|
|
62
|
+
range: { min: 1; max: 5 };
|
|
63
|
+
showEmoji?: boolean; // 是否显示表情量表
|
|
64
|
+
annotations?: Record<string, string>; // 数值 → 文字注释
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type Question = SelectQuestion | MultiSelectQuestion | TextQuestion | ConfirmQuestion | RatingQuestion;
|
|
68
|
+
|
|
69
|
+
// ─── 扁平化问题(含展开子问题后的元数据) ──────────────
|
|
70
|
+
|
|
71
|
+
export interface FlatQuestion extends Question {
|
|
72
|
+
_depth: number; // 嵌套深度(0 = 顶层)
|
|
73
|
+
_parentId: string | null; // 父问题 id
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── 每个问题的 UI 瞬时状态 ───────────────────────────
|
|
77
|
+
|
|
78
|
+
export interface QuestionUIState {
|
|
79
|
+
optionIndex: number; // 光标在选项列表中的位置
|
|
80
|
+
selectedIndices: number[]; // Space 勾选的选项索引
|
|
81
|
+
customText: string | null; // 自定义选项的编辑内容(临时保留)
|
|
82
|
+
confirmValue: boolean; // confirm 类型当前高亮的按钮(true=是)
|
|
83
|
+
ratingValue: number; // rating 类型当前滑块值
|
|
84
|
+
textDraft: string; // text 类型当前编辑内容
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── 答案(按 OpenAPI 规范区分 5 种类型) ──────────────
|
|
88
|
+
|
|
89
|
+
export interface SelectAnswer {
|
|
90
|
+
value: string;
|
|
91
|
+
label: string;
|
|
92
|
+
wasCustom?: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface MultiSelectAnswer {
|
|
96
|
+
values: string[];
|
|
97
|
+
labels: string[];
|
|
98
|
+
wasCustom?: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface TextAnswer {
|
|
102
|
+
text: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ConfirmAnswer {
|
|
106
|
+
confirmed: boolean;
|
|
107
|
+
label: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface RatingAnswer {
|
|
111
|
+
value: number;
|
|
112
|
+
annotation: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type Answer = SelectAnswer | MultiSelectAnswer | TextAnswer | ConfirmAnswer | RatingAnswer;
|
|
116
|
+
|
|
117
|
+
// ─── 问卷参数(顶层输入) ─────────────────────────────
|
|
118
|
+
|
|
119
|
+
export interface QuestionnaireParams {
|
|
120
|
+
questions: Question[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─── 结果 ─────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
export interface QuestionnaireResult {
|
|
126
|
+
answers: Record<string, Answer>;
|
|
127
|
+
submittedAt: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface CancelledResult {
|
|
131
|
+
cancelled: true;
|
|
132
|
+
message: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── 进度点颜色 ───────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
export type ProgressColor = "green" | "red" | "none";
|