mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
feat(apps): gate apps domain off on Lark brand (#1025)
* feat(apps): gate apps domain off on Lark brand The Miaoda apps OpenAPI is Feishu-only. On Lark brand: - shortcut subtree is registered + hidden, RunE returns a structured brand-restriction error so users see a clear message instead of cobra's generic "unknown command" - auth login `--domain apps` is treated as unknown; `--domain all` skips apps; help text omits it - scope collection skips apps shortcuts so spark:* scopes are never requested The leaf-stub pattern mirrors internal/cmdpolicy/apply.go::installDenyStub (DisableFlagParsing + ArbitraryArgs + leaf-level PersistentPreRunE override) so cobra can't short-circuit the stub with a missing-flag or parent-PreRunE detour. Change-Id: I5817e87ae6fedabdb5faf05d0d32ea988f7effc9
This commit is contained in:
@@ -68,7 +68,13 @@ run --device-code in a later step after the user confirms authorization.`,
|
||||
|
||||
cmd.Flags().StringVar(&opts.Scope, "scope", "", "scopes to request (space- or comma-separated). Combines additively with --domain/--recommend")
|
||||
cmd.Flags().BoolVar(&opts.Recommend, "recommend", false, "request only recommended (auto-approve) scopes")
|
||||
available := sortedKnownDomains()
|
||||
var helpBrand core.LarkBrand
|
||||
if f != nil && f.Config != nil {
|
||||
if cfg, err := f.Config(); err == nil && cfg != nil {
|
||||
helpBrand = cfg.Brand
|
||||
}
|
||||
}
|
||||
available := sortedKnownDomains(helpBrand)
|
||||
cmd.Flags().StringSliceVar(&opts.Domains, "domain", nil,
|
||||
fmt.Sprintf("domain (repeatable or comma-separated, e.g. --domain calendar,task)\navailable: %s, all", strings.Join(available, ", ")))
|
||||
cmd.Flags().StringSliceVar(&opts.Exclude, "exclude", nil,
|
||||
@@ -139,14 +145,14 @@ func authLoginRun(opts *LoginOptions) error {
|
||||
// Expand --domain all to all available domains (from_meta projects + shortcut services)
|
||||
for _, d := range selectedDomains {
|
||||
if strings.EqualFold(d, "all") {
|
||||
selectedDomains = sortedKnownDomains()
|
||||
selectedDomains = sortedKnownDomains(config.Brand)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Validate domain names and suggest corrections for unknown ones
|
||||
if len(selectedDomains) > 0 {
|
||||
knownDomains := allKnownDomains()
|
||||
knownDomains := allKnownDomains(config.Brand)
|
||||
for _, d := range selectedDomains {
|
||||
if !knownDomains[d] {
|
||||
if suggestion := suggestDomain(d, knownDomains); suggestion != "" {
|
||||
@@ -170,7 +176,7 @@ func authLoginRun(opts *LoginOptions) error {
|
||||
|
||||
if !hasAnyOption {
|
||||
if !opts.JSON && f.IOStreams.IsTerminal {
|
||||
result, err := runInteractiveLogin(f.IOStreams, lang, msg)
|
||||
result, err := runInteractiveLogin(f.IOStreams, lang, msg, config.Brand)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -208,10 +214,10 @@ func authLoginRun(opts *LoginOptions) error {
|
||||
if len(selectedDomains) > 0 || opts.Recommend {
|
||||
var candidateScopes []string
|
||||
if len(selectedDomains) > 0 {
|
||||
candidateScopes = collectScopesForDomains(selectedDomains, "user")
|
||||
candidateScopes = collectScopesForDomains(selectedDomains, "user", config.Brand)
|
||||
} else {
|
||||
// --recommend without --domain: all domains
|
||||
candidateScopes = collectScopesForDomains(sortedKnownDomains(), "user")
|
||||
candidateScopes = collectScopesForDomains(sortedKnownDomains(config.Brand), "user", config.Brand)
|
||||
}
|
||||
|
||||
// Filter to auto-approve scopes if --recommend or interactive "common"
|
||||
@@ -490,7 +496,7 @@ func findProfileByName(multi *core.MultiAppConfig, profileName string) *core.App
|
||||
// shortcut scopes for the given domain names.
|
||||
// Domains with auth_domain children are automatically expanded to include
|
||||
// their children's scopes.
|
||||
func collectScopesForDomains(domains []string, identity string) []string {
|
||||
func collectScopesForDomains(domains []string, identity string, brand core.LarkBrand) []string {
|
||||
scopeSet := make(map[string]bool)
|
||||
|
||||
// 1. API scopes from from_meta projects
|
||||
@@ -509,6 +515,9 @@ func collectScopesForDomains(domains []string, identity string) []string {
|
||||
|
||||
// 3. Shortcut scopes matching by Service (only include shortcuts supporting the identity)
|
||||
for _, sc := range shortcuts.AllShortcuts() {
|
||||
if !shortcuts.IsShortcutServiceAvailable(sc.Service, brand) {
|
||||
continue
|
||||
}
|
||||
if domainSet[sc.Service] && shortcutSupportsIdentity(sc, identity) {
|
||||
for _, s := range sc.DeclaredScopesForIdentity(identity) {
|
||||
scopeSet[s] = true
|
||||
@@ -528,7 +537,7 @@ func collectScopesForDomains(domains []string, identity string) []string {
|
||||
// allKnownDomains returns all valid auth domain names (from_meta projects +
|
||||
// shortcut services), excluding domains that have auth_domain set (they are
|
||||
// folded into their parent domain).
|
||||
func allKnownDomains() map[string]bool {
|
||||
func allKnownDomains(brand core.LarkBrand) map[string]bool {
|
||||
domains := make(map[string]bool)
|
||||
for _, p := range registry.ListFromMetaProjects() {
|
||||
if !registry.HasAuthDomain(p) {
|
||||
@@ -536,6 +545,9 @@ func allKnownDomains() map[string]bool {
|
||||
}
|
||||
}
|
||||
for _, sc := range shortcuts.AllShortcuts() {
|
||||
if !shortcuts.IsShortcutServiceAvailable(sc.Service, brand) {
|
||||
continue
|
||||
}
|
||||
if !registry.HasAuthDomain(sc.Service) {
|
||||
domains[sc.Service] = true
|
||||
}
|
||||
@@ -544,8 +556,8 @@ func allKnownDomains() map[string]bool {
|
||||
}
|
||||
|
||||
// sortedKnownDomains returns all valid domain names sorted alphabetically.
|
||||
func sortedKnownDomains() []string {
|
||||
m := allKnownDomains()
|
||||
func sortedKnownDomains(brand core.LarkBrand) []string {
|
||||
m := allKnownDomains(brand)
|
||||
domains := make([]string, 0, len(m))
|
||||
for d := range m {
|
||||
domains = append(domains, d)
|
||||
|
||||
32
cmd/auth/login_brand_filter_test.go
Normal file
32
cmd/auth/login_brand_filter_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
)
|
||||
|
||||
func TestBrandFilter_AppsExcludedOnLark(t *testing.T) {
|
||||
feishuDomains := allKnownDomains(core.BrandFeishu)
|
||||
if !feishuDomains["apps"] {
|
||||
t.Errorf("expected apps domain to be known on Feishu brand")
|
||||
}
|
||||
|
||||
larkDomains := allKnownDomains(core.BrandLark)
|
||||
if larkDomains["apps"] {
|
||||
t.Errorf("expected apps domain to be EXCLUDED on Lark brand")
|
||||
}
|
||||
|
||||
feishuScopes := collectScopesForDomains([]string{"apps"}, "user", core.BrandFeishu)
|
||||
if len(feishuScopes) == 0 {
|
||||
t.Errorf("expected non-empty scopes for apps on Feishu brand, got %d", len(feishuScopes))
|
||||
}
|
||||
|
||||
larkScopes := collectScopesForDomains([]string{"apps"}, "user", core.BrandLark)
|
||||
if len(larkScopes) != 0 {
|
||||
t.Errorf("expected empty scopes for apps on Lark brand, got %d: %v", len(larkScopes), larkScopes)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/charmbracelet/huh"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/registry"
|
||||
"github.com/larksuite/cli/shortcuts"
|
||||
@@ -105,7 +106,7 @@ func buildDomainMeta(name, lang string) domainMeta {
|
||||
}
|
||||
|
||||
// runInteractiveLogin shows an interactive TUI form for domain and permission selection.
|
||||
func runInteractiveLogin(ios *cmdutil.IOStreams, lang string, msg *loginMsg) (*interactiveResult, error) {
|
||||
func runInteractiveLogin(ios *cmdutil.IOStreams, lang string, msg *loginMsg, brand core.LarkBrand) (*interactiveResult, error) {
|
||||
allDomains := getDomainMetadata(lang)
|
||||
|
||||
// Build multi-select options
|
||||
@@ -165,7 +166,7 @@ func runInteractiveLogin(ios *cmdutil.IOStreams, lang string, msg *loginMsg) (*i
|
||||
}
|
||||
|
||||
// Compute scope summary
|
||||
scopes := collectScopesForDomains(selectedDomains, "user")
|
||||
scopes := collectScopesForDomains(selectedDomains, "user", brand)
|
||||
if permLevel == "common" {
|
||||
scopes = registry.FilterAutoApproveScopes(scopes)
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ func TestCompleteDomain_CommaSeparated(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAllKnownDomains(t *testing.T) {
|
||||
domains := allKnownDomains()
|
||||
domains := allKnownDomains("")
|
||||
if len(domains) == 0 {
|
||||
t.Fatal("expected non-empty known domains")
|
||||
}
|
||||
@@ -185,7 +185,7 @@ func TestAllKnownDomains(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSortedKnownDomains(t *testing.T) {
|
||||
sorted := sortedKnownDomains()
|
||||
sorted := sortedKnownDomains("")
|
||||
if len(sorted) == 0 {
|
||||
t.Fatal("expected non-empty sorted domains")
|
||||
}
|
||||
@@ -195,7 +195,7 @@ func TestSortedKnownDomains(t *testing.T) {
|
||||
}
|
||||
|
||||
// Should match allKnownDomains
|
||||
known := allKnownDomains()
|
||||
known := allKnownDomains("")
|
||||
if len(sorted) != len(known) {
|
||||
t.Errorf("sorted (%d) and known (%d) length mismatch", len(sorted), len(known))
|
||||
}
|
||||
@@ -220,7 +220,7 @@ func TestCollectScopesForDomains(t *testing.T) {
|
||||
t.Skip("no from_meta data available")
|
||||
}
|
||||
|
||||
scopes := collectScopesForDomains([]string{"calendar"}, "user")
|
||||
scopes := collectScopesForDomains([]string{"calendar"}, "user", "")
|
||||
if len(scopes) == 0 {
|
||||
t.Fatal("expected non-empty scopes for calendar domain")
|
||||
}
|
||||
@@ -247,7 +247,7 @@ func TestCollectScopesForDomains(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCollectScopesForDomains_NonexistentDomain(t *testing.T) {
|
||||
scopes := collectScopesForDomains([]string{"nonexistent_domain_xyz"}, "user")
|
||||
scopes := collectScopesForDomains([]string{"nonexistent_domain_xyz"}, "user", "")
|
||||
if len(scopes) != 0 {
|
||||
t.Errorf("expected empty scopes for nonexistent domain, got %d", len(scopes))
|
||||
}
|
||||
@@ -1077,7 +1077,7 @@ func TestGetDomainMetadata_ExcludesEvent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAllKnownDomains_ExcludesAuthDomainChildren(t *testing.T) {
|
||||
domains := allKnownDomains()
|
||||
domains := allKnownDomains("")
|
||||
if domains["whiteboard"] {
|
||||
t.Error("whiteboard should not appear in known auth domains (it has auth_domain=docs)")
|
||||
}
|
||||
@@ -1087,7 +1087,7 @@ func TestAllKnownDomains_ExcludesAuthDomainChildren(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCollectScopesForDomains_ExpandsAuthDomainChildren(t *testing.T) {
|
||||
scopes := collectScopesForDomains([]string{"docs"}, "user")
|
||||
scopes := collectScopesForDomains([]string{"docs"}, "user", "")
|
||||
// docs domain should include whiteboard shortcut scopes (board:whiteboard:*)
|
||||
found := false
|
||||
for _, s := range scopes {
|
||||
|
||||
@@ -5,12 +5,16 @@ package shortcuts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/larksuite/cli/shortcuts/okr"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdmeta"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
"github.com/larksuite/cli/internal/registry"
|
||||
"github.com/larksuite/cli/shortcuts/apps"
|
||||
"github.com/larksuite/cli/shortcuts/base"
|
||||
@@ -32,6 +36,23 @@ import (
|
||||
"github.com/larksuite/cli/shortcuts/wiki"
|
||||
)
|
||||
|
||||
// Empty brand (no config loaded) is treated as no-restriction so bootstrap
|
||||
// paths and tests without config still see the full service list.
|
||||
var brandRestrictedServices = map[string][]core.LarkBrand{
|
||||
"apps": {core.BrandFeishu},
|
||||
}
|
||||
|
||||
func IsShortcutServiceAvailable(service string, brand core.LarkBrand) bool {
|
||||
allowed, ok := brandRestrictedServices[service]
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if brand == "" {
|
||||
return true
|
||||
}
|
||||
return slices.Contains(allowed, brand)
|
||||
}
|
||||
|
||||
// allShortcuts aggregates shortcuts from all domain packages.
|
||||
var allShortcuts []common.Shortcut
|
||||
|
||||
@@ -69,6 +90,14 @@ func RegisterShortcuts(program *cobra.Command, f *cmdutil.Factory) {
|
||||
}
|
||||
|
||||
func RegisterShortcutsWithContext(ctx context.Context, program *cobra.Command, f *cmdutil.Factory) {
|
||||
// Factory.Config may be nil in tests that pass a zero-value factory.
|
||||
var brand core.LarkBrand
|
||||
if f != nil && f.Config != nil {
|
||||
if cfg, err := f.Config(); err == nil && cfg != nil {
|
||||
brand = cfg.Brand
|
||||
}
|
||||
}
|
||||
|
||||
// Group by service
|
||||
byService := make(map[string][]common.Shortcut)
|
||||
for _, s := range allShortcuts {
|
||||
@@ -117,5 +146,46 @@ func RegisterShortcutsWithContext(ctx context.Context, program *cobra.Command, f
|
||||
if service == "mail" {
|
||||
mail.InstallOnMail(svc)
|
||||
}
|
||||
|
||||
if !IsShortcutServiceAvailable(service, brand) {
|
||||
installBrandRestrictionGuard(svc, service, brand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors internal/cmdpolicy/apply.go::installDenyStub: DisableFlagParsing +
|
||||
// ArbitraryArgs keep cobra from short-circuiting with "missing required flag"
|
||||
// before our RunE runs; leaf-level PersistentPreRunE defeats cobra's "first
|
||||
// PreRunE wins" walk-up that would otherwise shadow the stub.
|
||||
func installBrandRestrictionGuard(svc *cobra.Command, service string, brand core.LarkBrand) {
|
||||
stub := func(c *cobra.Command, _ []string) error {
|
||||
c.SilenceUsage = true
|
||||
return output.ErrValidation(
|
||||
"the %q feature is not yet supported on the %s brand",
|
||||
service, brand,
|
||||
)
|
||||
}
|
||||
noopPreRun := func(c *cobra.Command, _ []string) error {
|
||||
c.SilenceUsage = true
|
||||
return nil
|
||||
}
|
||||
var walk func(c *cobra.Command)
|
||||
walk = func(c *cobra.Command) {
|
||||
c.Hidden = true
|
||||
c.DisableFlagParsing = true
|
||||
c.Args = cobra.ArbitraryArgs
|
||||
c.PreRunE = nil
|
||||
c.PreRun = nil
|
||||
c.PersistentPreRunE = noopPreRun
|
||||
c.PersistentPreRun = nil
|
||||
c.RunE = stub
|
||||
c.Run = nil
|
||||
for _, child := range c.Commands() {
|
||||
walk(child)
|
||||
}
|
||||
}
|
||||
walk(svc)
|
||||
|
||||
// --help bypasses RunE, so surface the restriction in Long too.
|
||||
svc.Long = fmt.Sprintf("The %q feature is not yet supported on the %s brand.", service, brand)
|
||||
}
|
||||
|
||||
122
shortcuts/register_brand_guard_test.go
Normal file
122
shortcuts/register_brand_guard_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package shortcuts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
func newFactoryWithBrand(brand core.LarkBrand) *cmdutil.Factory {
|
||||
return &cmdutil.Factory{
|
||||
Config: func() (*core.CliConfig, error) {
|
||||
return &core.CliConfig{Brand: brand}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func findChild(root *cobra.Command, name string) *cobra.Command {
|
||||
for _, c := range root.Commands() {
|
||||
if c.Name() == name {
|
||||
return c
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestBrandGuard_AppsStaysRegisteredOnLark(t *testing.T) {
|
||||
program := &cobra.Command{Use: "root"}
|
||||
RegisterShortcuts(program, newFactoryWithBrand(core.BrandLark))
|
||||
|
||||
apps := findChild(program, "apps")
|
||||
if apps == nil {
|
||||
t.Fatal("apps service command should be registered on Lark brand (so users see a clear brand error, not 'unknown command')")
|
||||
}
|
||||
if !apps.Hidden {
|
||||
t.Error("apps service command should be Hidden on Lark brand")
|
||||
}
|
||||
if len(apps.Commands()) == 0 {
|
||||
t.Error("apps subcommands should still be mounted (so children also hit the brand-restriction stub)")
|
||||
}
|
||||
for _, child := range apps.Commands() {
|
||||
if !child.Hidden {
|
||||
t.Errorf("apps child %q should be Hidden on Lark brand", child.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrandGuard_AppsExecuteReturnsBrandError(t *testing.T) {
|
||||
program := &cobra.Command{Use: "root"}
|
||||
RegisterShortcuts(program, newFactoryWithBrand(core.BrandLark))
|
||||
|
||||
apps := findChild(program, "apps")
|
||||
if apps == nil {
|
||||
t.Fatal("apps should be registered")
|
||||
}
|
||||
create := findChild(apps, "+create")
|
||||
if create == nil {
|
||||
t.Fatal("apps +create should be registered")
|
||||
}
|
||||
|
||||
err := create.RunE(create, []string{"--name", "x"})
|
||||
if err == nil {
|
||||
t.Fatal("expected brand-restriction error, got nil")
|
||||
}
|
||||
exitErr, ok := err.(*output.ExitError)
|
||||
if !ok {
|
||||
t.Fatalf("expected *output.ExitError, got %T: %v", err, err)
|
||||
}
|
||||
if exitErr.Code != output.ExitValidation {
|
||||
t.Errorf("expected ExitValidation (%d), got %d", output.ExitValidation, exitErr.Code)
|
||||
}
|
||||
if !strings.Contains(exitErr.Error(), "apps") || !strings.Contains(exitErr.Error(), "lark") {
|
||||
t.Errorf("expected error to mention apps + lark, got: %s", exitErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrandGuard_AppsExecutableOnFeishu(t *testing.T) {
|
||||
program := &cobra.Command{Use: "root"}
|
||||
RegisterShortcuts(program, newFactoryWithBrand(core.BrandFeishu))
|
||||
|
||||
apps := findChild(program, "apps")
|
||||
if apps == nil {
|
||||
t.Fatal("apps should be registered on Feishu brand")
|
||||
}
|
||||
if apps.Hidden {
|
||||
t.Error("apps should NOT be Hidden on Feishu brand")
|
||||
}
|
||||
create := findChild(apps, "+create")
|
||||
if create == nil {
|
||||
t.Fatal("apps +create should be registered on Feishu brand")
|
||||
}
|
||||
if create.DisableFlagParsing {
|
||||
t.Error("apps +create should not have DisableFlagParsing on Feishu (the guard must not have run)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrandGuard_DispatchHitsStubViaCobra(t *testing.T) {
|
||||
program := &cobra.Command{Use: "root"}
|
||||
RegisterShortcuts(program, newFactoryWithBrand(core.BrandLark))
|
||||
|
||||
program.SetArgs([]string{"apps", "+create", "--name", "x", "--app-type", "HTML"})
|
||||
program.SetContext(context.Background())
|
||||
err := program.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error from dispatching apps +create on Lark brand")
|
||||
}
|
||||
exitErr, ok := err.(*output.ExitError)
|
||||
if !ok {
|
||||
t.Fatalf("expected *output.ExitError from cobra dispatch, got %T: %v", err, err)
|
||||
}
|
||||
if !strings.Contains(exitErr.Error(), "lark") {
|
||||
t.Errorf("dispatched error should mention lark brand, got: %s", exitErr.Error())
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
name: lark-apps
|
||||
description: "飞书妙搭应用(lark-cli apps):把本地 HTML 文件或目录部署为可访问、可分享的妙搭应用(静态网站 / Web 页面),返回访问 URL;并提供应用创建、更新、列出、设置可用范围(specific 指定可见 / public 互联网公开 / tenant 企业全员)等管理能力。当用户说『用 HTML / 网页开发 PPT / 幻灯片 / 演示文稿 / 可演示的 demo』、『部署 / 发布 HTML / 静态网站 / 网页 / dist 目录』、『把 /xxx 中的 HTML 文件用 lark-cli 部署 / 发到妙搭』、『开发一个 xxx 并部署成可以分享的网站 / 可访问的链接 / 可分享 URL』、『生成一个可以发给别人看的 PPT / 页面 / demo』,或提到 妙搭 / miaoda / apps / app_id / 可用范围 / open-to-tenant / open-to-public 等关键词时使用。**部署策略:用户明示『部署 / 发布 / 分享 / 可访问 / 可分享 URL』时直接走 `apps +html-publish` 自动部署并返回 URL;用户只说『可演示 / 写一个 PPT / 做个 demo』等模糊意图时,HTML 写完后先询问『要部署到妙搭以便分享吗?』再决定。**"
|
||||
description: "把本地 HTML 文件或目录部署到飞书妙搭(Miaoda),生成可分享访问的 Web 页面并返回 URL;管理应用的创建、更新、列表和访问范围。当用户要把 HTML、静态网站或 Web demo 发布成可分享链接,或提到妙搭 / Miaoda 时使用。不用于:上传普通文件到云空间(用 lark-drive)、编辑飞书云文档内容(用 lark-doc)、创建飞书原生幻灯片 / 演示文稿(用 lark-slides)。"
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["lark-cli"]
|
||||
cliHelp: "lark-cli apps --help; lark-cli apps +create --help; lark-cli apps +html-publish --help; lark-cli apps +access-scope-set --help; lark-cli apps +update --help"
|
||||
cliHelp: "lark-cli apps --help"
|
||||
---
|
||||
|
||||
# apps (v1)
|
||||
@@ -16,6 +16,10 @@ lark-cli apps +html-publish --app-id app_xxx --path ./dist
|
||||
lark-cli apps +access-scope-set --app-id app_xxx --scope tenant
|
||||
```
|
||||
|
||||
## 品牌可用性(先做)
|
||||
|
||||
跑 `lark-cli apps --help`;若提示暂未支持,告诉用户敬请期待并停止。
|
||||
|
||||
## 前置条件 — 执行操作前必读
|
||||
|
||||
**CRITICAL — 执行对应操作前,MUST 先用 Read 工具读取以下文件,缺一不可:**
|
||||
|
||||
Reference in New Issue
Block a user