Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions shortcuts/apps/apps_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ var AppsCreate = common.Shortcut{
{Name: "app-type", Desc: "app type (HTML or fullstack)", Required: true},
{Name: "description", Desc: "app description"},
{Name: "icon-url", Desc: "app icon URL (server uses default if omitted)"},
{Name: "message", Desc: "user message describing the app to build (required when --app-type is fullstack)"},
},
Validate: func(ctx context.Context, rctx *common.RuntimeContext) error {
if strings.TrimSpace(rctx.Str("name")) == "" {
Expand All @@ -40,9 +39,6 @@ var AppsCreate = common.Shortcut{
if !validAppTypes[appType] {
return output.ErrValidation(fmt.Sprintf("--app-type %q is not supported (allowed: HTML, fullstack)", appType))
}
if appType == "fullstack" && strings.TrimSpace(rctx.Str("message")) == "" {
return output.ErrValidation("--message is required when --app-type is fullstack")
}
return nil
},
DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI {
Expand Down Expand Up @@ -81,8 +77,5 @@ func buildAppsCreateBody(rctx *common.RuntimeContext) map[string]interface{} {
if icon := strings.TrimSpace(rctx.Str("icon-url")); icon != "" {
body["icon_url"] = icon
}
if msg := strings.TrimSpace(rctx.Str("message")); appType == "fullstack" && msg != "" {
body["message"] = msg
}
return body
}
61 changes: 6 additions & 55 deletions shortcuts/apps/apps_create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func TestAppsCreate_RejectsWrongCaseFullstack(t *testing.T) {
t.Run(appType, func(t *testing.T) {
factory, stdout, _ := newAppsExecuteFactory(t)
err := runAppsShortcut(t, AppsCreate,
[]string{"+create", "--name", "Demo", "--app-type", appType, "--message", "m", "--as", "user"},
[]string{"+create", "--name", "Demo", "--app-type", appType, "--as", "user"},
factory, stdout)
if err == nil || !strings.Contains(err.Error(), "not supported") {
t.Fatalf("expected case-sensitive rejection of %q, got %v", appType, err)
Expand All @@ -187,26 +187,6 @@ func TestAppsCreate_RejectsWrongCaseFullstack(t *testing.T) {
}
}

func TestAppsCreate_FullstackRequiresMessage(t *testing.T) {
factory, stdout, _ := newAppsExecuteFactory(t)
err := runAppsShortcut(t, AppsCreate,
[]string{"+create", "--name", "Demo", "--app-type", "fullstack", "--as", "user"},
factory, stdout)
if err == nil || !strings.Contains(err.Error(), "message is required") {
t.Fatalf("expected message-required error, got %v", err)
}
}

func TestAppsCreate_FullstackBlankMessageRejected(t *testing.T) {
factory, stdout, _ := newAppsExecuteFactory(t)
err := runAppsShortcut(t, AppsCreate,
[]string{"+create", "--name", "Demo", "--app-type", "fullstack", "--message", " ", "--as", "user"},
factory, stdout)
if err == nil || !strings.Contains(err.Error(), "message is required") {
t.Fatalf("expected blank message rejected, got %v", err)
}
}

func TestAppsCreate_DryRun(t *testing.T) {
factory, stdout, _ := newAppsExecuteFactory(t)
if err := runAppsShortcut(t, AppsCreate,
Expand Down Expand Up @@ -241,7 +221,7 @@ func TestAppsCreate_FullstackSuccess(t *testing.T) {
reg.Register(stub)

if err := runAppsShortcut(t, AppsCreate,
[]string{"+create", "--name", "Demo", "--app-type", "fullstack", "--message", "build a CRM", "--as", "user"},
[]string{"+create", "--name", "Demo", "--app-type", "fullstack", "--as", "user"},
factory, stdout); err != nil {
t.Fatalf("execute err=%v", err)
}
Expand All @@ -253,52 +233,23 @@ func TestAppsCreate_FullstackSuccess(t *testing.T) {
if sent["app_type"] != "fullstack" {
t.Fatalf("body.app_type = %v (want fullstack)", sent["app_type"])
}
if sent["message"] != "build a CRM" {
t.Fatalf("body.message = %v (want \"build a CRM\")", sent["message"])
}
}

func TestAppsCreate_HTMLIgnoresMessage(t *testing.T) {
factory, stdout, reg := newAppsExecuteFactory(t)
stub := &httpmock.Stub{
Method: "POST",
URL: "/open-apis/spark/v1/apps",
Body: map[string]interface{}{
"code": 0,
"data": map[string]interface{}{
"app": map[string]interface{}{"app_id": "app_x", "name": "Demo"},
},
},
}
reg.Register(stub)

if err := runAppsShortcut(t, AppsCreate,
[]string{"+create", "--name", "Demo", "--app-type", "HTML", "--message", "ignored", "--as", "user"},
factory, stdout); err != nil {
t.Fatalf("execute err=%v", err)
}

var sent map[string]interface{}
if err := json.Unmarshal(stub.CapturedBody, &sent); err != nil {
t.Fatalf("decode body: %v", err)
}
if _, present := sent["message"]; present {
t.Fatalf("message should be omitted for HTML app-type: %v", sent)
t.Fatalf("message should never be sent: %v", sent)
}
}

func TestAppsCreate_FullstackDryRun(t *testing.T) {
factory, stdout, _ := newAppsExecuteFactory(t)
if err := runAppsShortcut(t, AppsCreate,
[]string{"+create", "--name", "Demo", "--app-type", "fullstack", "--message", "m", "--dry-run", "--as", "user"},
[]string{"+create", "--name", "Demo", "--app-type", "fullstack", "--dry-run", "--as", "user"},
factory, stdout); err != nil {
t.Fatalf("dry-run err=%v", err)
}
got := stdout.String()
if !strings.Contains(got, `"app_type": "fullstack"`) {
t.Fatalf("dry-run missing app_type fullstack: %s", got)
}
if !strings.Contains(got, `"message": "m"`) {
t.Fatalf("dry-run missing message: %s", got)
if strings.Contains(got, `"message"`) {
t.Fatalf("dry-run should not contain message: %s", got)
}
}
6 changes: 3 additions & 3 deletions skills/lark-apps/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ metadata:
```bash
# 常用示例
lark-cli apps +create --name "客户调研问卷" --app-type HTML
lark-cli apps +create --name "团队任务看板" --app-type fullstack --message "带登录和数据库的任务看板"
lark-cli apps +create --name "团队任务看板" --app-type fullstack --description "带登录和数据库的任务看板"
lark-cli apps +html-publish --app-id app_xxx --path ./dist
lark-cli apps +access-scope-set --app-id app_xxx --scope tenant
```
Expand Down Expand Up @@ -83,7 +83,7 @@ lark-cli auth login --domain apps
### HTML vs fullstack 意图分流

- 用户要**纯静态页面 / HTML / PPT / 幻灯片 / 单页 / 演示 / Web demo**(无后端逻辑)→ `--app-type HTML`,走现有端到端流程(+create → +html-publish)
- 用户要**带后端能力的全栈应用**(数据库 / 登录鉴权 / API / 表单提交存储 / 用户系统 / 增删改查 / 持久化 / 服务端逻辑 / "全栈" / "带后台" / "后台管理")→ `--app-type fullstack --message "<用户原话>"`,到 `+create` 为止(fullstack 后续本地开发链路待 `+git-credential-init` 就绪后补充)
- 用户要**带后端能力的全栈应用**(数据库 / 登录鉴权 / API / 表单提交存储 / 用户系统 / 增删改查 / 持久化 / 服务端逻辑 / "全栈" / "带后台" / "后台管理")→ `--app-type fullstack`(从用户描述生成 `--name` 和 `--description`),到 `+create` 为止(fullstack 后续本地开发链路待 `+git-credential-init` 就绪后补充)
- 意图模糊、无法判断 → 默认 `HTML`(更轻、且为现有成熟流程),必要时追问一句澄清
- 详细判定规则见 [`references/lark-apps-create.md`](references/lark-apps-create.md) 的「意图识别」小节

Expand All @@ -108,7 +108,7 @@ Shortcut 是对常用操作的高级封装(`lark-cli apps +<verb> [flags]`)

| Shortcut | 说明 |
|----------|------|
| [`+create`](references/lark-apps-create.md) | 创建妙搭应用(HTML / fullstack;fullstack 需 --message) |
| [`+create`](references/lark-apps-create.md) | 创建妙搭应用(HTML / fullstack;从用户输入生成 name/description) |
| [`+update`](references/lark-apps-update.md) | 部分更新应用名 / 描述(只发传入字段) |
| [`+access-scope-set`](references/lark-apps-access-scope-set.md) | 设置应用可用范围(specific / public / tenant,三态互斥校验) |
| [`+access-scope-get`](references/lark-apps-access-scope-get.md) | 查看应用当前可用范围(响应 scope 枚举 `All` / `Tenant` / `Range`;可作"备份 / 复制 scope 配置"前置读) |
Expand Down
12 changes: 5 additions & 7 deletions skills/lark-apps/references/lark-apps-create.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ lark-cli apps +create \
# Dry-run(仅打印请求,不执行)
lark-cli apps +create --name "Demo" --app-type HTML --dry-run

# 创建 fullstack(全栈)应用:必带 --message,原样透传用户需求
# 创建 fullstack(全栈)应用:从用户描述生成 name/description
lark-cli apps +create --name "团队任务看板" --app-type fullstack \
--message "做一个团队任务看板,支持登录、任务增删改查、按人筛选"
--description "支持登录、任务增删改查、按人筛选的团队任务看板"
```

## 参数
Expand All @@ -33,7 +33,6 @@ lark-cli apps +create --name "团队任务看板" --app-type fullstack \
| `--app-type <enum>` | ✅ | 应用类型,可选值:`HTML` 或 `fullstack`(均区分大小写) |
| `--description <str>` | ❌ | 应用描述 |
| `--icon-url <url>` | ❌ | 应用图标 URL;不传服务端给默认图标 |
| `--message <str>` | fullstack 时 ✅,HTML 时忽略 | 用户描述应用需求的原话原文;`--app-type fullstack` 时必填,原样透传给服务端;`--app-type HTML` 时即使传了也被静默忽略(不进请求体、不报错) |

## 返回值

Expand Down Expand Up @@ -71,7 +70,6 @@ lark-cli apps +create --name "团队任务看板" --app-type fullstack \
## 字段语义

- `app_type` 是应用类型枚举,**区分大小写**,当前支持 `HTML` 和 `fullstack`(两者均大小写敏感精确匹配,不在白名单的取值 CLI 端直接拒绝)
- `message` 仅 `app_type=fullstack` 时生效,原样透传用户描述需求的原话,不改写、不总结;`HTML` 类型时不进请求体
- `created_at` 是 ISO 8601 UTC 时间字符串
- `error.hint` 是 CLI 给出的可执行修复建议,**优先**转述给用户;hint 为空时退回 `error.message`
- 不要原样把 envelope JSON 复述给用户
Expand All @@ -86,7 +84,7 @@ lark-cli apps +create --name "团队任务看板" --app-type fullstack \
| 需要后端能力:数据库、登录鉴权、API、表单提交存储、用户系统、增删改查、持久化、服务端逻辑、"全栈"、"带后台 / 后台管理" | 全栈应用 | `fullstack` |
| 模糊不清、无法判断 | 默认 `HTML`(更轻、且为现有成熟流程),必要时追问一句澄清 | `HTML` |

fullstack 判定后:`--message` 取用户描述需求的**原话原文**,不改写、不总结
判定类型后:从用户的自然语言输入**生成**一个简洁的 `name` 和一句 `description`,通过 `--name` / `--description` 传入(HTML 与 fullstack 都适用),不要求用户显式给出应用名

## 典型场景

Expand Down Expand Up @@ -116,11 +114,11 @@ lark-cli apps +create --name "Q4 调研" --app-type HTML --description "..."

### 场景 3:创建 fullstack 应用

识别到用户需要全栈/带后端能力的应用后(如需要登录、数据库、增删改查、用户系统等),使用 `--app-type fullstack`,并将用户原话原文作为 `--message` 传入
识别到用户需要全栈/带后端能力的应用后(如需要登录、数据库、增删改查、用户系统等),使用 `--app-type fullstack`,并从用户描述生成 `name` 和 `description`

```bash
lark-cli apps +create --name "团队任务看板" --app-type fullstack \
--message "做一个团队任务看板,支持登录、任务增删改查、按人筛选"
--description "支持登录、任务增删改查、按人筛选的团队任务看板"
```

向用户报告:
Expand Down