Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e95d03606 | ||
|
|
6e3f3cbd24 | ||
|
|
ce5e919c52 |
@@ -6,6 +6,16 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.34] - 2026-06-03
|
||||
|
||||
### Changed
|
||||
|
||||
- **Service discovery path now carries a version-coded segment** (`internal/market/registry.go`) — the server-list endpoint moves from `/cli/discovery/apis` to `/cli/discovery/apis/bamboo`. The path is now a single `discoveryAPIPath` constant so future version bumps touch one place. Only the path changes; the MCP base host stays on production `https://mcp.dingtalk.com` and the auth / skill / doctor endpoints are untouched. Discovery via the edition `DiscoveryURL` hook (full-URL `FetchServersFromURL`) is unaffected. Server side must serve the new path.
|
||||
|
||||
### Removed
|
||||
|
||||
- **`dws aiapp` — AI application product taken offline** — removed the `aiapp` product surface (`create` / `query` / `modify`) from the CLI: deleted `internal/helpers/aiapp.go`, dropped it from the generator coverage targets and `knownRegistryProducts`, removed the `aiapp` skill references (mono `references/products/aiapp.md` + `dingtalk-aiapp` multi skill), and unpublished the `aiapp` server from the service-discovery envelope. Product count drops from 19 to 18.
|
||||
|
||||
## [1.0.33] - 2026-06-02
|
||||
|
||||
This release merges the multi-contributor `pre-mcp-discovery` feature branch into `main` as a single squash (#391), bringing a large batch of new product surface — full DingTalk **docs** (`doc`), **knowledge base** (`wiki`), **AI app** (`aiapp`), AI-table **forms** + **import/export**, and reworked **mail** / **todo** / **report** command trees — while keeping service discovery pinned to production `https://mcp.dingtalk.com` (the branch's `pre-mcp.dingtalk.com` endpoint change was deliberately excluded; the four host constants in `skill_command.go` / `auth/endpoints.go` / `cli/loader.go` / `market/registry.go` stay on prod). It also folds in the portable auth bundle (`dws auth export` / `import`, #357) and PAT batch authorization (#389).
|
||||
|
||||
@@ -277,7 +277,7 @@ dws aitable record query --base-id BASE_ID --table-id TABLE_ID --limit 10
|
||||
The repo ships a complete Agent Skill system under `skills/`, now organized into two layouts:
|
||||
|
||||
- `skills/mono/` — single-skill layout (one `SKILL.md` + `references/products/`), recommended default.
|
||||
- `skills/multi/` — per-product skills (`dingtalk-aitable/`, `dingtalk-calendar/`, `dingtalk-chat/`, ... 20 products in total), each with its own `SKILL.md`. 🧪 **EXPERIMENTAL / preview — see banner in each multi `SKILL.md` for caveats.**
|
||||
- `skills/multi/` — per-product skills (`dingtalk-aitable/`, `dingtalk-calendar/`, `dingtalk-chat/`, ... 19 products in total), each with its own `SKILL.md`. 🧪 **EXPERIMENTAL / preview — see banner in each multi `SKILL.md` for caveats.**
|
||||
|
||||
After installing, AI tools like Claude Code / Cursor can operate DingTalk directly through natural language:
|
||||
|
||||
@@ -494,11 +494,10 @@ dws chat message send-by-bot --robot-code BOT_CODE --group GROUP_ID \
|
||||
| Wiki | `wiki` | 21 | `space` `member` `node` `doc` `file` | Knowledge base management: spaces (`create` / `get` / `list` / `search`), members (`add` / `list` / `update`), node tree, docs & files |
|
||||
| DevDoc | `devdoc` | 1 | `article` | Search the DingTalk Open Platform documentation |
|
||||
| AI Search | `aisearch` | 3 | `person` | Enterprise people search by name / department / position / duty / supervisor / subordinate / phone / job-number (single command, multi-dimension filter) |
|
||||
| AI App | `aiapp` | 4 | — | AI application lifecycle: `create` (with prompt / attachments / skills) / `query` (by task ID) / `modify` (by thread ID) |
|
||||
| Live | `live` | 1 | `stream` | DingTalk live streaming: list my lives |
|
||||
| Raw API | `api` | 1 | — | Call any DingTalk OpenAPI directly (api / oapi dual-form), with automatic app-level token management |
|
||||
|
||||
> **334 commands across 19 products.** Full listing with descriptions and usage scenarios: [`docs/command-index.md`](./docs/command-index.md). Run `dws --help` for the top-level tree, or `dws <service> --help` for subcommands.
|
||||
> **330 commands across 18 products.** Full listing with descriptions and usage scenarios: [`docs/command-index.md`](./docs/command-index.md). Run `dws --help` for the top-level tree, or `dws <service> --help` for subcommands.
|
||||
|
||||
> **Note on `chat bot`**: bot capabilities (`send-by-bot` / `recall-by-bot` / `add-bot` / `send-by-webhook` / bot search) are merged into the relevant `chat` subtrees (e.g. `dws chat message send-by-bot`, `dws chat group members add-bot`) so the agent-facing command surface stays flat and discoverable. There is no longer a separate top-level `bot` product.
|
||||
|
||||
|
||||
+4
-5
@@ -71,9 +71,9 @@ irm https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/ma
|
||||
| 模式 | 安装内容 | 适合场景 |
|
||||
|------|----------|----------|
|
||||
| **mono**(稳定,默认) | 一个 `dws` skill,覆盖全部产品 | 跨产品组合操作;单一入口召唤 |
|
||||
| **multi** 🧪 **试验版 / Preview** | 20 个独立产品 skill(`dingtalk-aitable` / `dingtalk-calendar` / `dingtalk-chat` ...) | 单产品任务;每次召唤上下文更小 |
|
||||
| **multi** 🧪 **试验版 / Preview** | 19 个独立产品 skill(`dingtalk-aitable` / `dingtalk-calendar` / `dingtalk-chat` ...) | 单产品任务;每次召唤上下文更小 |
|
||||
|
||||
> 🧪 **multi 模式当前为 EXPERIMENTAL(试验版 / Preview)**。20 个独立 skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整。生产 / 共享环境建议优先用 `mono`。问题请提 issue 反馈。
|
||||
> 🧪 **multi 模式当前为 EXPERIMENTAL(试验版 / Preview)**。19 个独立 skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整。生产 / 共享环境建议优先用 `mono`。问题请提 issue 反馈。
|
||||
|
||||
怎么选:
|
||||
|
||||
@@ -274,7 +274,7 @@ dws aitable record query --base-id BASE_ID --table-id TABLE_ID --limit 10
|
||||
仓库内置完整的 Agent Skill 体系(`skills/` 目录),目前重组为两套布局:
|
||||
|
||||
- `skills/mono/` — 单 skill 布局(一个 `SKILL.md` + `references/products/`),默认推荐。
|
||||
- `skills/multi/` — 每个产品一个独立 skill(`dingtalk-aitable/` / `dingtalk-calendar/` / `dingtalk-chat/` ... 共 20 个),每个 skill 自带 `SKILL.md`。🧪 **试验版 / Preview — 各 multi `SKILL.md` 头部有详细注意事项。**
|
||||
- `skills/multi/` — 每个产品一个独立 skill(`dingtalk-aitable/` / `dingtalk-calendar/` / `dingtalk-chat/` ... 共 19 个),每个 skill 自带 `SKILL.md`。🧪 **试验版 / Preview — 各 multi `SKILL.md` 头部有详细注意事项。**
|
||||
|
||||
安装之后,Claude Code / Cursor 等 AI 工具就能通过自然语言直接操作钉钉:
|
||||
|
||||
@@ -490,11 +490,10 @@ dws chat message send-by-bot --robot-code BOT_CODE --group GROUP_ID \
|
||||
| 知识库 | `wiki` | 21 | `space` `member` `node` `doc` `file` | 知识库管理:空间(`create` / `get` / `list` / `search`)、成员(`add` / `list` / `update`)、节点树、文档与文件 |
|
||||
| 开发者文档 | `devdoc` | 1 | `article` | 搜索钉钉开放平台文档 |
|
||||
| AI 搜问 | `aisearch` | 3 | `person` | 企业人员搜索:按姓名 / 部门 / 职位 / 职责 / 上级 / 下级 / 手机号 / 工号 多维度过滤(单命令) |
|
||||
| AI 应用 | `aiapp` | 4 | — | AI 应用生命周期:`create`(含 prompt / attachments / skills)/ `query`(按任务 ID)/ `modify`(按 thread ID) |
|
||||
| 直播 | `live` | 1 | `stream` | 钉钉直播:查看我的直播列表 |
|
||||
| Raw API | `api` | 1 | — | 直接调用任意钉钉 OpenAPI(api / oapi 双形态),自动管理应用级 Token |
|
||||
|
||||
> **19 个产品,334 条命令。** 完整命令清单(带描述与使用场景):[`docs/command-index.md`](./docs/command-index.md)。运行 `dws --help` 查看顶层命令树,或 `dws <service> --help` 查看子命令。
|
||||
> **18 个产品,330 条命令。** 完整命令清单(带描述与使用场景):[`docs/command-index.md`](./docs/command-index.md)。运行 `dws --help` 查看顶层命令树,或 `dws <service> --help` 查看子命令。
|
||||
|
||||
> **关于 `chat bot`**:机器人能力(`send-by-bot` / `recall-by-bot` / `add-bot` / `send-by-webhook` / bot 搜索)已合并到对应的 `chat` 子树下(例如 `dws chat message send-by-bot`、`dws chat group members add-bot`),保持 agent 视角下的命令面扁平易发现。不再有独立的顶层 `bot` 产品。
|
||||
|
||||
|
||||
@@ -359,7 +359,7 @@ func TestLoadDynamicCommandsDoesNotSynchronouslyFetchDetailMetadata(t *testing.T
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.URL.Path == "/cli/discovery/apis":
|
||||
case r.URL.Path == "/cli/discovery/apis/bamboo":
|
||||
payload := map[string]any{
|
||||
"metadata": map[string]any{"count": 2, "nextCursor": ""},
|
||||
"servers": []any{
|
||||
@@ -433,7 +433,7 @@ func TestLoadDynamicCommandsDoesNotSynchronouslyFetchDetailMetadataWhenRegistryT
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.URL.Path == "/cli/discovery/apis":
|
||||
case r.URL.Path == "/cli/discovery/apis/bamboo":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"metadata": map[string]any{"count": 2, "nextCursor": ""},
|
||||
"servers": []any{
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestCacheRefreshClearsExistingCachesAndSkipsCLISkippedServers(t *testing.T)
|
||||
var srv *httptest.Server
|
||||
srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/cli/discovery/apis":
|
||||
case "/cli/discovery/apis/bamboo":
|
||||
_ = json.NewEncoder(w).Encode(market.ListResponse{
|
||||
Metadata: market.ListMetadata{Count: 2},
|
||||
Servers: []market.ServerEnvelope{
|
||||
@@ -146,7 +146,7 @@ func TestCacheRefreshHonorsEditionDiscoveryURL(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
case "/cli/discovery/apis":
|
||||
case "/cli/discovery/apis/bamboo":
|
||||
marketHits.Add(1)
|
||||
http.Error(w, "market endpoint must not be called when edition DiscoveryURL is set", http.StatusNotFound)
|
||||
default:
|
||||
|
||||
@@ -13,13 +13,13 @@ import (
|
||||
)
|
||||
|
||||
// newTestMCPServer returns an httptest.Server that handles both market registry
|
||||
// and MCP JSON-RPC endpoints. marketOK controls whether /cli/discovery/apis
|
||||
// and MCP JSON-RPC endpoints. marketOK controls whether /cli/discovery/apis/bamboo
|
||||
// succeeds, and mcpOK controls whether initialize+tools/list succeed.
|
||||
func newTestMCPServer(t *testing.T, marketOK, mcpOK bool) *httptest.Server {
|
||||
t.Helper()
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.HandleFunc("/cli/discovery/apis", func(w http.ResponseWriter, r *http.Request) {
|
||||
mux.HandleFunc("/cli/discovery/apis/bamboo", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !marketOK {
|
||||
http.Error(w, "market unavailable", http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
@@ -259,7 +259,7 @@ func newDocsMCPGateway(expectations []docsServerExpectation) *httptest.Server {
|
||||
mux := http.NewServeMux()
|
||||
server := httptest.NewServer(mux)
|
||||
|
||||
mux.HandleFunc("/cli/discovery/apis", func(w http.ResponseWriter, r *http.Request) {
|
||||
mux.HandleFunc("/cli/discovery/apis/bamboo", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
|
||||
@@ -82,7 +82,6 @@ var writeOperationTokens = map[string]struct{}{
|
||||
}
|
||||
|
||||
var legacy17CoverageTargets = []string{
|
||||
"aiapp",
|
||||
"aitable",
|
||||
"attendance",
|
||||
"calendar",
|
||||
@@ -102,7 +101,6 @@ var legacy17CoverageTargets = []string{
|
||||
}
|
||||
|
||||
var extended22CoverageTargets = []string{
|
||||
"aiapp",
|
||||
"aitable",
|
||||
"attendance",
|
||||
"calendar",
|
||||
|
||||
@@ -77,7 +77,6 @@ type RecipeEntry struct {
|
||||
}
|
||||
|
||||
var knownRegistryProducts = map[string]struct{}{
|
||||
"aiapp": {},
|
||||
"aidesign": {},
|
||||
"aitable": {},
|
||||
"attendance": {},
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd"
|
||||
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterPublic(func() Handler {
|
||||
return aiappHandler{}
|
||||
})
|
||||
}
|
||||
|
||||
type aiappHandler struct{}
|
||||
|
||||
func (aiappHandler) Name() string {
|
||||
return "aiapp"
|
||||
}
|
||||
|
||||
func (aiappHandler) Command(runner executor.Runner) *cobra.Command {
|
||||
root := &cobra.Command{
|
||||
Use: "aiapp",
|
||||
Short: "AI 应用创建 / 查询 / 修改",
|
||||
Args: cobra.NoArgs,
|
||||
TraverseChildren: true,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return cmd.Help()
|
||||
},
|
||||
}
|
||||
root.AddCommand(
|
||||
newAIAppCreateCommand(runner),
|
||||
newAIAppQueryCommand(runner),
|
||||
newAIAppModifyCommand(runner),
|
||||
)
|
||||
return root
|
||||
}
|
||||
|
||||
func newAIAppCreateCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "create",
|
||||
Short: "创建 AI 应用",
|
||||
Args: cobra.NoArgs,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
prompt, err := aiappRequiredFlag(cmd, "prompt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
params := map[string]any{"prompt": prompt}
|
||||
if err := addAIAppOptionalInputs(cmd, params); err != nil {
|
||||
return err
|
||||
}
|
||||
return runAIAppTool(cmd, runner, "create_ai_app", params)
|
||||
},
|
||||
}
|
||||
preferLegacyLeaf(cmd)
|
||||
cmd.Flags().String("prompt", "", "创建 AI 应用的 prompt (必填)")
|
||||
cmd.Flags().String("attachments", "", "附件对象数组 JSON")
|
||||
cmd.Flags().String("skills", "", "技能 ID 列表,逗号分隔")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newAIAppQueryCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "query",
|
||||
Short: "查询 AI 应用任务",
|
||||
Args: cobra.NoArgs,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
taskID, err := aiappRequiredFlag(cmd, "task-id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runAIAppTool(cmd, runner, "query_ai_app", map[string]any{"taskId": taskID})
|
||||
},
|
||||
}
|
||||
preferLegacyLeaf(cmd)
|
||||
cmd.Flags().String("task-id", "", "AI 应用任务 ID (必填)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newAIAppModifyCommand(runner executor.Runner) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "modify",
|
||||
Short: "修改 AI 应用",
|
||||
Args: cobra.NoArgs,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
prompt, err := aiappRequiredFlag(cmd, "prompt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
threadID, err := aiappRequiredFlag(cmd, "thread-id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
params := map[string]any{
|
||||
"prompt": prompt,
|
||||
"threadId": threadID,
|
||||
}
|
||||
if skills := aiappStringFlag(cmd, "skills"); skills != "" {
|
||||
params["officialSkillUids"] = aiappCSV(skills)
|
||||
}
|
||||
return runAIAppTool(cmd, runner, "modify_ai_app", params)
|
||||
},
|
||||
}
|
||||
preferLegacyLeaf(cmd)
|
||||
cmd.Flags().String("prompt", "", "新的 prompt (必填)")
|
||||
cmd.Flags().String("thread-id", "", "threadId (必填)")
|
||||
cmd.Flags().String("skills", "", "技能 ID 列表,逗号分隔")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func addAIAppOptionalInputs(cmd *cobra.Command, params map[string]any) error {
|
||||
if attachments := aiappStringFlag(cmd, "attachments"); attachments != "" {
|
||||
var values []any
|
||||
if err := json.Unmarshal([]byte(attachments), &values); err != nil {
|
||||
return apperrors.NewValidation("--attachments must be a JSON array: " + err.Error())
|
||||
}
|
||||
if len(values) > 0 {
|
||||
params["attachments"] = values
|
||||
}
|
||||
}
|
||||
if skills := aiappStringFlag(cmd, "skills"); skills != "" {
|
||||
params["officialSkillUids"] = aiappCSV(skills)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runAIAppTool(cmd *cobra.Command, runner executor.Runner, tool string, params map[string]any) error {
|
||||
invocation := executor.NewHelperInvocation(
|
||||
cobracmd.LegacyCommandPath(cmd),
|
||||
"aiapp",
|
||||
tool,
|
||||
params,
|
||||
)
|
||||
if commandDryRun(cmd) {
|
||||
return writeCommandPayload(cmd, invocation)
|
||||
}
|
||||
result, err := runner.Run(cmd.Context(), invocation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeCommandPayload(cmd, result)
|
||||
}
|
||||
|
||||
func aiappRequiredFlag(cmd *cobra.Command, name string) (string, error) {
|
||||
if value := aiappStringFlag(cmd, name); value != "" {
|
||||
return value, nil
|
||||
}
|
||||
return "", apperrors.NewValidation("--" + name + " is required")
|
||||
}
|
||||
|
||||
func aiappStringFlag(cmd *cobra.Command, name string) string {
|
||||
if value, err := cmd.Flags().GetString(name); err == nil && strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func aiappCSV(raw string) []string {
|
||||
parts := strings.Split(raw, ",")
|
||||
values := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if value := strings.TrimSpace(part); value != "" {
|
||||
values = append(values, value)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright 2026 Alibaba Group
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
|
||||
)
|
||||
|
||||
type aiappCommandRunner struct {
|
||||
last executor.Invocation
|
||||
}
|
||||
|
||||
func (r *aiappCommandRunner) Run(_ context.Context, invocation executor.Invocation) (executor.Result, error) {
|
||||
r.last = invocation
|
||||
return executor.Result{Invocation: invocation}, nil
|
||||
}
|
||||
|
||||
func TestAIAppModifyMatchesWukongPayload(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
runner := &aiappCommandRunner{}
|
||||
cmd := aiappHandler{}.Command(runner)
|
||||
var out, errOut bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
cmd.SetErr(&errOut)
|
||||
cmd.SetArgs([]string{
|
||||
"modify",
|
||||
"--prompt", "根据新图片优化首页视觉风格",
|
||||
"--thread-id", "THREAD_001",
|
||||
"--skills", "s1,s2",
|
||||
})
|
||||
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("Execute() error = %v\nstderr:\n%s", err, errOut.String())
|
||||
}
|
||||
if runner.last.Tool != "modify_ai_app" {
|
||||
t.Fatalf("tool = %q, want modify_ai_app", runner.last.Tool)
|
||||
}
|
||||
if got := runner.last.Params["threadId"]; got != "THREAD_001" {
|
||||
t.Fatalf("threadId = %#v, want THREAD_001", got)
|
||||
}
|
||||
if _, ok := runner.last.Params["attachments"]; ok {
|
||||
t.Fatalf("attachments should not be sent by modify_ai_app: %#v", runner.last.Params["attachments"])
|
||||
}
|
||||
skills, ok := runner.last.Params["officialSkillUids"].([]string)
|
||||
if !ok || len(skills) != 2 || skills[0] != "s1" || skills[1] != "s2" {
|
||||
t.Fatalf("officialSkillUids = %#v, want [s1 s2]", runner.last.Params["officialSkillUids"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAIAppCreateRejectsInvalidAttachmentsJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
runner := &aiappCommandRunner{}
|
||||
cmd := aiappHandler{}.Command(runner)
|
||||
var out, errOut bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
cmd.SetErr(&errOut)
|
||||
cmd.SetArgs([]string{"create", "--prompt", "创建应用", "--attachments", `{"bad":true}`})
|
||||
|
||||
err := cmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("Execute() error = nil, want JSON array validation failure")
|
||||
}
|
||||
if runner.last.Tool != "" {
|
||||
t.Fatalf("tool = %q, want no invocation", runner.last.Tool)
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,12 @@ import (
|
||||
const (
|
||||
defaultBaseURL = "https://mcp.dingtalk.com"
|
||||
registryMetadataKey = "com.dingtalk.mcp.registry/metadata"
|
||||
|
||||
// discoveryAPIPath is the path appended to BaseURL when fetching the
|
||||
// MCP server list. Kept as a single constant so the version-coded
|
||||
// segment (".../bamboo") lives in one place and future version bumps
|
||||
// only touch here. See registry_test.go for the matching fixture path.
|
||||
discoveryAPIPath = "/cli/discovery/apis/bamboo"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -544,7 +550,7 @@ func (c *Client) FetchServersFromURL(ctx context.Context, fullURL string) (ListR
|
||||
}
|
||||
|
||||
func (c *Client) fetchServersPage(ctx context.Context, limit int, cursor string) (ListResponse, error) {
|
||||
reqURL, err := url.Parse(c.BaseURL + "/cli/discovery/apis")
|
||||
reqURL, err := url.Parse(c.BaseURL + discoveryAPIPath)
|
||||
if err != nil {
|
||||
return ListResponse{}, apperrors.NewDiscovery("failed to build market servers URL")
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ func TestFetchServers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/cli/discovery/apis" {
|
||||
if r.URL.Path != "/cli/discovery/apis/bamboo" {
|
||||
t.Fatalf("unexpected path %q", r.URL.Path)
|
||||
}
|
||||
payload := ListResponse{
|
||||
@@ -231,7 +231,7 @@ func TestFetchServersFollowsNextCursor(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/cli/discovery/apis" {
|
||||
if r.URL.Path != "/cli/discovery/apis/bamboo" {
|
||||
t.Fatalf("unexpected path %q", r.URL.Path)
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ personas:
|
||||
- name: ops-analyst
|
||||
title: 运营分析师
|
||||
description: "围绕数据与运营流程执行:表格、听记、邮件与应用协同。"
|
||||
services: [aitable, minutes, mail, aiapp, drive, doc]
|
||||
services: [aitable, minutes, mail, drive, doc]
|
||||
workflows: [minutes-to-todo, drive-upload-and-announce]
|
||||
instructions:
|
||||
- "先获取原始数据,再做结构化汇总与分发。"
|
||||
|
||||
@@ -35,7 +35,6 @@ cli_version: ">=1.0.15"
|
||||
|
||||
| 产品 | 用途 | 参考文件 |
|
||||
|-------------------|------------------------------------------------------|----------------------------------------------------------------|
|
||||
| `aiapp` | AI应用:创建/查询/修改AI应用 | [aiapp.md](./references/products/aiapp.md) |
|
||||
| `aisearch` | AI搜问(搜人首选):按姓名/部门/职位/职责/上级/下级/手机号/工号维度找人,"谁负责 XX/XX 的负责人/某事项/某项目的人"统一走本产品 | [aisearch.md](./references/products/aisearch.md) |
|
||||
| `aitable` | AI表格:Base/数据表/字段/记录/视图/附件/图表/仪表盘/导入导出/模板搜索 | [aitable.md](./references/products/aitable.md) |
|
||||
| `attendance` | 考勤:打卡结果/打卡流水/考勤组查询/考勤规则/汇总统计/假期类型/假期余额(P0 已落地,部分管理类命令仍属 P1) | [attendance.md](./references/products/attendance.md) |
|
||||
@@ -83,7 +82,6 @@ cli_version: ">=1.0.15"
|
||||
|
||||
## 意图判断决策树
|
||||
|
||||
用户提到"AI应用/创建应用/生成系统/做工具/管理后台/低代码" → `aiapp`
|
||||
用户提到"找人/搜人/谁负责 XX/某事项的负责人/某项目的人/团队成员/上级/下级/按工号找人/按手机号找人" → `aisearch`
|
||||
用户提到"表格/多维表/AI表格/记录/数据/视图/图表/仪表盘" → `aitable`
|
||||
用户提到"考勤/打卡/排班" → `attendance`
|
||||
|
||||
@@ -1,420 +0,0 @@
|
||||
# AI应用 (aiapp) 命令参考
|
||||
|
||||
## 强制触发规则
|
||||
|
||||
以下意图默认必须优先使用 `aiapp`,即使用户没有说“AI应用”:
|
||||
|
||||
- 创建一个新应用
|
||||
- 创建一个新系统
|
||||
- 生成一个业务工具
|
||||
- 按需求生成一个管理后台、工作台应用、表单系统、业务原型
|
||||
- 修改一个已经生成过的应用
|
||||
|
||||
典型触发词包括但不限于:
|
||||
|
||||
- 应用、系统、平台、工具、后台、页面、原型、工作台应用
|
||||
- CRM、OA、库存、仓库、审批、销售、报销、进销存、客户管理、数据录入、查询工具
|
||||
- 计算器、待办工具、表单应用、信息收集应用、报表应用
|
||||
- “帮我做一个…”
|
||||
- “帮我创建一个…”
|
||||
- “按这个需求生成一个…”
|
||||
- “把这个需求做成应用”
|
||||
|
||||
典型例子:
|
||||
|
||||
- “帮我创建个简易的计算器应用”
|
||||
- “做一个仓库管理系统”
|
||||
- “生成一个客户管理工具”
|
||||
- “按这个 PRD 创建一个钉钉应用”
|
||||
- “修改刚才那个应用,把首页改得更简洁”
|
||||
|
||||
不要误路由到其他产品:
|
||||
|
||||
- 不要因为用户提到“表格/记录/字段”,就优先走 `aitable`,如果用户的真实目标是“生成一个完整应用”
|
||||
- 不要因为用户提到“文档/需求”,就优先走 `doc`,如果用户的真实目标是“把需求做成应用”
|
||||
- 不要因为用户提到“任务/项目”,就优先走 `tb`,如果用户的真实目标是“生成一个管理系统”
|
||||
|
||||
只有在以下场景不要走 `aiapp`:
|
||||
|
||||
- 用户明确要操作一个现成的多维表、文档、项目、日历、机器人消息
|
||||
- 用户不是要“生成应用”,而是要“修改已有产品对象”
|
||||
|
||||
## 命令总览
|
||||
|
||||
### 创建 AI 应用
|
||||
```
|
||||
Usage:
|
||||
dws aiapp create [flags]
|
||||
Example:
|
||||
dws aiapp create --prompt "创建一个天气查询应用"
|
||||
dws aiapp create --prompt "翻译应用" --skills s1,s2
|
||||
dws aiapp create --prompt "根据附件里的 Excel 创建一个仓库管理应用" --attachments '[{"name":"warehouse.xlsx","type":"excel","url":"https://tmp/warehouse.xlsx"}]'
|
||||
Flags:
|
||||
--prompt string 创建 AI 应用的 prompt (必填)
|
||||
--skills string 技能 ID 列表,逗号分隔
|
||||
--attachments string 附件对象数组的 JSON 字符串,用于把 Excel/图片等输入材料一起传入
|
||||
```
|
||||
|
||||
### 查询 AI 应用
|
||||
```
|
||||
Usage:
|
||||
dws aiapp query [flags]
|
||||
Example:
|
||||
dws aiapp query --task-id <taskId>
|
||||
Flags:
|
||||
--task-id string AI 应用任务 ID (必填)
|
||||
```
|
||||
|
||||
### 修改 AI 应用
|
||||
```
|
||||
Usage:
|
||||
dws aiapp modify [flags]
|
||||
Example:
|
||||
dws aiapp modify --prompt "改为翻译应用" --thread-id <threadId>
|
||||
dws aiapp modify --prompt "新描述" --thread-id <threadId> --skills s1,s2
|
||||
dws aiapp modify --prompt "根据新图片优化首页视觉风格" --thread-id <threadId> --attachments '[{"name":"homepage.png","type":"image/png","url":"https://tmp/homepage.png"}]'
|
||||
Flags:
|
||||
--prompt string 新的 prompt (必填)
|
||||
--skills string 技能 ID 列表,逗号分隔
|
||||
--thread-id string threadId (必填)
|
||||
--attachments string 附件对象数组的 JSON 字符串;当前 modify 仅支持图片附件,不支持 Excel
|
||||
```
|
||||
|
||||
## 三个动作的关系
|
||||
|
||||
`aiapp` 的三个动作不是彼此独立的,它们描述的是同一个应用任务的生命周期:
|
||||
|
||||
1. `create` 用来发起一次新的应用生成任务,返回 `taskId` 和 `threadId`
|
||||
2. `query` 用来跟踪这次任务是否完成,以及读取生成结果
|
||||
3. `modify` 用来基于同一个 `threadId` 继续和已生成的应用对话,本质上是对已有应用做增量修改
|
||||
|
||||
可以把它理解为:
|
||||
|
||||
- `create` = 新建一个应用会话
|
||||
- `query` = 查看这个会话对应任务的执行状态和结果
|
||||
- `modify` = 在这个会话里继续追加新的需求,让同一个应用继续演化
|
||||
|
||||
关键标识符的关系:
|
||||
|
||||
- `taskId` 表示某一次具体的执行任务,主要给 `query` 使用
|
||||
- `threadId` 表示这个应用的持续会话,主要给 `modify` 使用
|
||||
- 一次 `create` 会产生一个新的 `taskId`,同时也会产出或确认一个 `threadId`
|
||||
- 后续每次 `modify` 都是在同一个 `threadId` 上发起新的任务,因此也会产生新的 `taskId`
|
||||
|
||||
因此推荐把它们看成一条闭环链路,而不是三个平级命令:
|
||||
|
||||
`create -> query -> modify -> query -> modify ...`
|
||||
|
||||
## `attachments` 的含义
|
||||
|
||||
`attachments` 是 `aiapp` 的输入材料参数,用来把文件和当前任务一起提交给 `aiapp`。
|
||||
|
||||
它适合这些场景:
|
||||
|
||||
- 根据 Excel 创建应用
|
||||
- 根据需求文档、表格、截图继续完善应用
|
||||
- 在修改已有应用时补充新的结构化材料
|
||||
|
||||
可以把它理解为:
|
||||
|
||||
- `prompt` 描述“希望系统做什么”
|
||||
- `attachments` 提供“系统可直接参考的文件材料”
|
||||
|
||||
### 正确心智
|
||||
|
||||
- `attachments` 是 `create` 或 `modify` 的补充输入,不是独立动作
|
||||
- 即使提供了附件,主动作仍然是 `aiapp create` 或 `aiapp modify`
|
||||
- 当用户说“按这个 Excel 建一个应用”时,优先走 `aiapp`,并把 Excel 作为 `attachments` 传入
|
||||
|
||||
### 数据结构
|
||||
|
||||
`attachments` 不是单个字符串,而是对象数组。每个对象当前按以下结构传递:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "warehouse.xlsx",
|
||||
"type": "excel",
|
||||
"url": "https://tmp/warehouse.xlsx",
|
||||
"size": 102400
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
每个附件对象当前可用字段:
|
||||
|
||||
- `name`:文件名
|
||||
- `type`:文件类型或 MIME 类型
|
||||
- `url`:可直接访问的临时下载地址
|
||||
- `size`:文件大小,可选
|
||||
|
||||
如果要传多个附件,就在数组中放多个对象。
|
||||
|
||||
### Excel 场景
|
||||
|
||||
如果用户上传了 Excel,通常应该:
|
||||
|
||||
- 在 `prompt` 里明确写“根据附件中的 Excel 创建应用”或“根据附件继续修改应用”
|
||||
- 同时把 Excel 通过 `attachments` 传给 `aiapp`
|
||||
|
||||
这类需求都应优先视为 `aiapp`:
|
||||
|
||||
- “根据这个 Excel 建一个库存管理应用”
|
||||
- “把附件里的表格做成一个 CRM 应用”
|
||||
- “按这个 Excel 继续补充刚才那个应用”
|
||||
|
||||
### 使用规则
|
||||
|
||||
- `create` 可以带图片、Excel 等附件
|
||||
- `modify` 当前只建议带图片附件,不支持 Excel
|
||||
- `query` 不使用 `attachments`
|
||||
- 附件应按对象数组传递,而不是传单个 ID 或随意文本
|
||||
- 如果用户提供本地文件 + 任意意图 → 先上传文件获取 URL,再执行对应命令
|
||||
- 每个附件对象通常至少应包含可访问的临时下载地址 `url`
|
||||
- 如果有附件但没有特别完整的 prompt,也仍然建议补一句明确意图,避免模型误判
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 根据 Excel 创建应用
|
||||
dws aiapp create --prompt "根据附件里的 Excel 创建一个仓库管理应用" \
|
||||
--attachments '[{"name":"warehouse.xlsx","type":"excel","url":"https://tmp/warehouse.xlsx"}]' \
|
||||
--format json
|
||||
|
||||
# 在已有应用上继续结合图片修改
|
||||
dws aiapp modify --prompt "根据新图片优化首页布局和视觉风格" \
|
||||
--thread-id <threadId> \
|
||||
--attachments '[{"name":"homepage.png","type":"image/png","url":"https://tmp/homepage.png"}]' \
|
||||
--format json
|
||||
```
|
||||
|
||||
## `--skills` 的含义
|
||||
|
||||
`--skills` 是 `aiapp` 的附加能力参数,不是一个独立动作。
|
||||
|
||||
它的作用不是“先执行这些技能,再单独执行 `aiapp`”,而是:
|
||||
|
||||
- 在 `create` 时,把这些官方技能作为本次应用生成任务的附加约束或附加能力一起传入
|
||||
- 在 `modify` 时,把这些官方技能作为本次修改任务的附加约束或附加能力一起传入
|
||||
|
||||
可以把它理解为:
|
||||
|
||||
- `prompt` 描述“你想做什么应用”
|
||||
- `--skills` 描述“这次生成或修改时,要额外挂载哪些官方能力或规范”
|
||||
|
||||
因此 `--skills` 必须依附于 `create` 或 `modify` 使用,本身不能单独完成创建应用。
|
||||
|
||||
### 正确心智
|
||||
|
||||
- `aiapp` 是主动作
|
||||
- `--skills` 是主动作的附加参数
|
||||
- 用户是否挂载官方技能,会影响应用生成或修改效果
|
||||
- 即使用户提到了某个官方技能,最终仍然应该优先调用 `aiapp create` 或 `aiapp modify`
|
||||
|
||||
### 常见误区
|
||||
|
||||
不要把以下场景理解错:
|
||||
|
||||
- 用户说“创建一个应用,并遵循设计规范”
|
||||
- 正确做法:调用 `aiapp create ... --skills <design-skill-id>`
|
||||
- 不正确做法:把“设计规范技能”当成一个和 `aiapp` 平级、单独执行的动作
|
||||
|
||||
- 用户说“继续修改刚才那个应用,并带上设计规范”
|
||||
- 正确做法:调用 `aiapp modify ... --thread-id <threadId> --skills <design-skill-id>`
|
||||
- 不正确做法:先执行设计规范技能,再单独调用 `modify`
|
||||
|
||||
- 用户只是在描述“希望页面更漂亮、更标准”
|
||||
- 如果已知对应的官方技能 ID,可以通过 `--skills` 传入
|
||||
- 如果没有明确的技能 ID,不要编造,优先把需求写进 `prompt`
|
||||
|
||||
### 什么时候传 `--skills`
|
||||
|
||||
适合传 `--skills` 的情况:
|
||||
|
||||
- 用户明确要求遵循某个已知的官方能力、官方规范、官方模板
|
||||
- 当前上下文里已经拿到了可用的官方技能 ID/Code
|
||||
- 需要在创建或修改时显式挂载这些技能
|
||||
|
||||
不适合传 `--skills` 的情况:
|
||||
|
||||
- 没有明确的技能 ID
|
||||
- 只是模糊地希望“更专业”“更好看”,但没有对应官方技能可挂
|
||||
- 技能本身不是 `aiapp` 官方子技能,而是其他产品的独立操作能力
|
||||
|
||||
### 使用规则
|
||||
|
||||
- `create` 和 `modify` 都可以带 `--skills`
|
||||
- `query` 不使用 `--skills`
|
||||
- `--skills` 里的值必须是技能 ID,多个值用逗号分隔
|
||||
- 不要猜测、编造技能 ID,必须来自已知上下文或系统返回
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 创建应用,并挂载一个钉钉设计规范技能
|
||||
dws aiapp create --prompt "创建一个仓库管理应用" \
|
||||
--skills dingtalk_design_spec --format json
|
||||
|
||||
# 修改已有应用,并继续带上官方技能
|
||||
dws aiapp modify --prompt "增加个库存大盘图表页" \
|
||||
--thread-id <threadId> \
|
||||
--skills dingtalk_design_spec --format json
|
||||
```
|
||||
|
||||
## 意图判断
|
||||
|
||||
用户说以下任何一种,都走 `create`:
|
||||
|
||||
- “做个应用 / 创建应用 / 生成应用 / 搭个应用”
|
||||
- “做个系统 / 平台 / 后台 / 工具 / 页面”
|
||||
- “帮我做个计算器 / 仓库管理 / CRM / OA / 审批 / 报表 / 信息收集应用”
|
||||
- “按这段需求生成一个钉钉应用”
|
||||
|
||||
用户说以下任何一种,都走 `query`:
|
||||
|
||||
- “查看应用状态”
|
||||
- “这个应用创建好了没”
|
||||
- “查一下 taskId 对应的进度”
|
||||
- “继续轮询应用创建进展”
|
||||
|
||||
用户说以下任何一种,都走 `modify`:
|
||||
|
||||
- “修改应用”
|
||||
- “更新刚才那个应用”
|
||||
- “基于这个 thread/task 继续改”
|
||||
- “把首页改一下 / 把功能补上 / 换个风格”
|
||||
|
||||
## 核心工作流
|
||||
|
||||
```bash
|
||||
# 1. 创建 AI 应用 — 提取 taskId 和 threadId
|
||||
dws aiapp create --prompt "创建一个天气查询应用" --format json
|
||||
|
||||
# 2. 查询应用状态
|
||||
dws aiapp query --task-id <taskId> --format json
|
||||
|
||||
# 3. 修改应用
|
||||
dws aiapp modify --prompt "新描述" --thread-id <threadId> \
|
||||
--skills skill1,skill2 --format json
|
||||
```
|
||||
|
||||
补充理解:
|
||||
|
||||
- 如果用户还没有现成应用,先走 `create`
|
||||
- 如果用户刚发起了创建,通常先走 `query` 看是否完成
|
||||
- 如果用户说“继续改刚才那个应用”,优先找上一次返回里的 `threadId`,走 `modify`
|
||||
- `modify` 完成后,仍然可以继续用新的 `taskId` 走 `query`
|
||||
- 不要把 `modify` 理解成“覆盖旧应用”,它更像是在同一个应用会话中追加新需求
|
||||
- `attachments` 不改变 `create/query/modify` 的主流程,它只是给 `create` 或 `modify` 增加文件输入
|
||||
- 如果附件是 Excel,优先理解为 `create` 场景;`modify` 当前不要用 Excel 附件
|
||||
- `--skills` 不改变 `create/query/modify` 的主流程,它只是给 `create` 或 `modify` 增加官方附加能力
|
||||
|
||||
推荐决策顺序:
|
||||
|
||||
1. 先判断用户是不是要“生成一个新的应用/系统”
|
||||
2. 如果是,直接用 `aiapp create`
|
||||
3. 如果用户是在已有应用基础上继续改,用 `aiapp modify`
|
||||
4. 如果只是查进度或结果,用 `aiapp query`
|
||||
|
||||
## 查询与轮询预期
|
||||
|
||||
`aiapp` 的创建和修改通常不是秒级任务,而是分钟级任务。
|
||||
|
||||
- 简单应用通常需要几分钟
|
||||
- 复杂应用可能持续十几分钟到几十分钟
|
||||
|
||||
因此在使用 `query` 时,不要假设“连续几次没有完成”就代表任务卡死。
|
||||
|
||||
### 正确的轮询策略
|
||||
|
||||
- 不要每 5-10 秒高频轮询
|
||||
- 建议以 30 秒为默认轮询间隔
|
||||
- 如果任务已经明确进入长流程,可进一步放宽到 60 秒
|
||||
- 除非用户明确要求实时刷新,否则不要自行创建过于频繁的定时查询
|
||||
|
||||
### 代理行为约束
|
||||
|
||||
当 agent 已经帮用户发起了 `aiapp create` 或 `aiapp modify` 后,不要只查询 1-2 次就把后续进度甩给用户自己处理。
|
||||
|
||||
- 只要任务仍处于 `queued` 或 `running`,且用户没有明确要求停止,agent 应继续主动跟踪进度
|
||||
- 如果当前会话无法持续等待,可以创建定时任务继续查询,但定时任务必须是“以拿到最终结果为目标”的,而不是无条件永久轮询
|
||||
- 如果返回了 `threadViewUrl`,可以告诉用户“你也可以点这个链接查看实时进度”,但这不能替代 agent 自己继续追踪任务
|
||||
|
||||
### 定时任务规则
|
||||
|
||||
如果 agent 选择创建定时任务来继续查询 `aiapp` 状态,必须同时设置结束条件:
|
||||
|
||||
- 当 `status = succeeded` 且已拿到 `appPreviewUrl` 时,立即停止后续查询,并主动删除或关闭该定时任务
|
||||
- 当 `status = failed` 时,立即停止后续查询,并主动删除或关闭该定时任务
|
||||
- 不要在已经拿到目标结果后继续保留定时任务
|
||||
- 不要创建“只会查询、不会自终止”的定时任务
|
||||
|
||||
换句话说,定时任务应当是“查询直到成功或失败,然后自动结束”的一次性追踪任务,而不是长期后台轮询
|
||||
|
||||
### 如何理解 `query` 返回
|
||||
|
||||
`query` 的返回里,除了总状态外,还可能包含进度信息,例如:
|
||||
|
||||
- 当前正在执行哪个步骤
|
||||
- 已完成多少步骤
|
||||
- 最近一次活动时间
|
||||
- 当前步骤列表
|
||||
- 当前 thread 的访问链接
|
||||
|
||||
因此判断任务是否还在推进时,不要只看 `status` 是否还是 `running`,还要结合以下字段一起看:
|
||||
|
||||
- `updatedAt` / `lastActivityAt`
|
||||
- `progress.summary`
|
||||
- `progress.currentStep`
|
||||
- `progress.steps`
|
||||
- `threadViewUrl`
|
||||
|
||||
只要这些字段仍在变化,通常就说明任务仍在正常推进。
|
||||
|
||||
### `threadViewUrl` 与 `appPreviewUrl` 的区别
|
||||
|
||||
`query` 或 `create` 的返回里,可能会有两个不同的链接:
|
||||
|
||||
- `threadViewUrl`:当前 thread 的访问链接。用于打开 `aiapp` 站点里的会话/生成进度页面,适合在应用还没生成完成时查看实时状态
|
||||
- `appPreviewUrl`:最终生成出的应用访问地址。只有应用创建成功后才会出现,适合真正打开应用去使用
|
||||
|
||||
不要把这两个链接混用:
|
||||
|
||||
- 任务还在 `queued/running` 时,优先使用 `threadViewUrl`
|
||||
- 任务 `succeeded` 且结果里已有 `appPreviewUrl` 时,再使用 `appPreviewUrl`
|
||||
|
||||
### 何时停止轮询
|
||||
|
||||
- 当 `status = succeeded` 时,停止轮询,并从结果中提取 `threadId`、应用链接等信息
|
||||
- 当 `status = failed` 时,停止轮询,并向用户报告失败
|
||||
- 当任务长时间保持 `running`,但 `lastActivityAt` 和 `progress` 都没有变化时,再考虑提示可能卡住,而不是过早下结论
|
||||
- 如果此前为查询进度创建了定时任务,在 `succeeded` 或 `failed` 后要主动删除该定时任务,而不是留给用户处理
|
||||
|
||||
## 上下文传递表
|
||||
|
||||
| 操作 | 从返回中提取 | 用于 |
|
||||
|------|-------------|------|
|
||||
| `create` | `taskId`, `threadId`, `threadViewUrl` | query 的 --task-id, modify 的 --thread-id,或直接打开进度页 |
|
||||
| `query` | `threadViewUrl`, `appPreviewUrl`, 应用详情 | 查看进度页,或在成功后打开应用预览并决定是否 modify |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `create` 通过自然语言 prompt 描述应用功能,系统自动生成
|
||||
- 对“应用/系统/工具/后台/原型”类需求,优先使用 `aiapp`,不要改用其他产品命令模拟实现
|
||||
- 对“根据附件 / 根据 Excel / 根据上传表格生成应用”这类需求,也优先使用 `aiapp`
|
||||
- `attachments` 需要按对象数组传递,当前结构是 `[{name,type,url,size}]`
|
||||
- `create` 可使用 Excel 附件;`modify` 当前仅支持图片附件,不支持 Excel
|
||||
- `--skills` 传入官方技能 UID 列表,逗号分隔,可增强应用能力
|
||||
- `modify` 需要 `--thread-id` 来标识修改哪个会话线程
|
||||
- 应用未生成完成前,如果需要让用户查看实时状态,优先使用返回里的 `threadViewUrl`
|
||||
- 应用生成成功后,如果需要真正打开应用,再使用 `appPreviewUrl`
|
||||
- 创建后可通过 `query` 轮询 `taskId` 查看创建进度,但轮询间隔默认应为 20-30 秒,不要 10 秒级频繁轮询
|
||||
- agent 不应只查 1-2 次就放弃;如果任务仍在运行,应继续跟踪,或创建带终止条件的定时任务继续跟踪
|
||||
- 如果 agent 创建了用于查询进度的定时任务,在任务成功或失败后必须主动删除或结束该定时任务
|
||||
- 不要直接使用用户本地文件路径作为附件 URL,必须先上传转换为互联网可访问链接
|
||||
|
||||
## 自动化脚本
|
||||
|
||||
| 脚本 | 场景 | 用法 |
|
||||
|------|------|------|
|
||||
| [aiapp_create_and_poll.py](../../scripts/aiapp_create_and_poll.py) | 一键创建 AI 应用并自动轮询进度直到完成 | `python aiapp_create_and_poll.py --prompt "创建一个天气查询应用"` |
|
||||
@@ -1,138 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
创建 AI 应用并自动轮询等待完成
|
||||
|
||||
用法:
|
||||
python aiapp_create_and_poll.py \
|
||||
--prompt "创建一个仓库管理应用"
|
||||
|
||||
python aiapp_create_and_poll.py \
|
||||
--prompt "生成客户管理 CRM" \
|
||||
--skills skill1,skill2 \
|
||||
--interval 30 \
|
||||
--timeout 600
|
||||
|
||||
python aiapp_create_and_poll.py --dry-run --prompt "test"
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import argparse
|
||||
import time
|
||||
from typing import List, Any, Optional
|
||||
|
||||
|
||||
def run_dws(
|
||||
args: List[str], dry_run: bool = False,
|
||||
) -> Optional[Any]:
|
||||
cmd = ['dws'] + args
|
||||
if dry_run:
|
||||
print(f"[dry-run] {' '.join(cmd)}")
|
||||
return {'dry_run': True}
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=120
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f" ✗ 错误:{result.stderr.strip()}")
|
||||
return None
|
||||
return json.loads(result.stdout)
|
||||
except (subprocess.TimeoutExpired, json.JSONDecodeError,
|
||||
FileNotFoundError) as e:
|
||||
print(f" ✗ 错误:{e}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='创建 AI 应用并轮询等待完成'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--prompt', required=True, help='应用描述'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skills', default='', help='技能 ID 列表'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--interval', type=int, default=30,
|
||||
help='轮询间隔秒 (默认 30)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--timeout', type=int, default=600,
|
||||
help='最大等待秒 (默认 600)',
|
||||
)
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f'🚀 创建 AI 应用...')
|
||||
print(f' Prompt: {args.prompt}')
|
||||
cmd_args = [
|
||||
'aiapp', 'create',
|
||||
'--prompt', args.prompt,
|
||||
'--format', 'json',
|
||||
]
|
||||
if args.skills:
|
||||
cmd_args.extend(['--skills', args.skills])
|
||||
|
||||
create_data = run_dws(cmd_args, dry_run=args.dry_run)
|
||||
if args.dry_run:
|
||||
run_dws([
|
||||
'aiapp', 'query',
|
||||
'--task-id', '<TASK_ID>',
|
||||
'--format', 'json',
|
||||
], dry_run=True)
|
||||
return
|
||||
|
||||
if not create_data:
|
||||
sys.exit(1)
|
||||
|
||||
task_id = create_data.get('taskId') or create_data.get('id', '')
|
||||
thread_id = create_data.get('threadId', '')
|
||||
print(f" ✓ 任务已创建")
|
||||
print(f" taskId: {task_id}")
|
||||
print(f" threadId: {thread_id}")
|
||||
|
||||
print(f'\n⏳ 轮询等待 (间隔 {args.interval}s, '
|
||||
f'超时 {args.timeout}s)...')
|
||||
elapsed = 0
|
||||
while elapsed < args.timeout:
|
||||
time.sleep(args.interval)
|
||||
elapsed += args.interval
|
||||
|
||||
query_data = run_dws([
|
||||
'aiapp', 'query',
|
||||
'--task-id', task_id,
|
||||
'--format', 'json',
|
||||
])
|
||||
if not query_data:
|
||||
print(f" [{elapsed}s] ⚠ 查询失败,继续等待...")
|
||||
continue
|
||||
|
||||
status = (query_data.get('status')
|
||||
or query_data.get('state', 'unknown'))
|
||||
progress = query_data.get('progress', {})
|
||||
step = ''
|
||||
if isinstance(progress, dict):
|
||||
step = progress.get('currentStep', '')
|
||||
|
||||
if status == 'succeeded':
|
||||
print(f" [{elapsed}s] ✅ 应用创建成功!")
|
||||
if thread_id:
|
||||
print(f" threadId: {thread_id}")
|
||||
return
|
||||
elif status == 'failed':
|
||||
print(f" [{elapsed}s] ❌ 创建失败")
|
||||
sys.exit(1)
|
||||
else:
|
||||
info = f" [{elapsed}s] ⏳ {status}"
|
||||
if step:
|
||||
info += f" ({step})"
|
||||
print(info)
|
||||
|
||||
print(f"\n⏰ 超时 ({args.timeout}s),任务可能仍在运行")
|
||||
print(f" 可手动查询: dws aiapp query --task-id {task_id}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: dingtalk-aiapp
|
||||
description: 钉钉 AI 应用生成。Use when 用户说 创建应用/生成系统/做工具/管理后台/工作台应用/表单系统/业务原型/页面/平台。强制触发:用户提到「应用 / 系统 / 平台 / 工具 / 后台 / 页面 / 原型」时优先匹配此 skill。Distinct from dingtalk-workbench(工作台应用列表)、dingtalk-wiki(知识库)。命令前缀:dws aiapp。
|
||||
cli_version: ">=0.2.14"
|
||||
metadata:
|
||||
category: product
|
||||
stability: experimental
|
||||
requires:
|
||||
bins:
|
||||
- dws
|
||||
---
|
||||
|
||||
# 钉钉 AI 应用 Skill
|
||||
|
||||
> 🧪 **EXPERIMENTAL · 试验版 / Preview** — multi 模式当前未达 stable 标准。20 个 dingtalk-* skill 全部通过 dispatch verifier,但接口、命名、跨 skill 引用后续可能调整;生产 / 共享环境请优先使用 mono 模式(`dws skill setup --mode mono`)。问题请提 issue 反馈。
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性可能因企业服务发现配置而异**。本文档列出的命令基于 dws envelope schema 与本仓库 v1.0.30 实测,但部分命令的 cobra 子命令暴露与否还取决于你的企业 MCP gateway 是否注册了对应 tool。如果跑某条命令报 `unknown command` 或 fall back 到父级 help,说明当前账号企业未开通该能力。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
> 命令参考:[aiapp.md](references/aiapp.md)。
|
||||
|
||||
## 意图表
|
||||
|
||||
| 用户说 | 命令 |
|
||||
|--------|------|
|
||||
| "创建一个 XX 应用 / 系统 / 工具" | `python scripts/aiapp_create_and_poll.py --prompt "<用户原始需求>"`(自动轮询进度) |
|
||||
| "查 AI 应用 / 修改已有应用" | 见 [aiapp.md](references/aiapp.md) |
|
||||
|
||||
## 跨产品协作
|
||||
|
||||
- 已存在的工作台应用列表 → 切到 `dingtalk-workbench`
|
||||
@@ -1,417 +0,0 @@
|
||||
# AI应用 (aiapp) 命令参考
|
||||
|
||||
## 强制触发规则
|
||||
|
||||
以下意图默认必须优先使用 `aiapp`,即使用户没有说“AI应用”:
|
||||
|
||||
- 创建一个新应用
|
||||
- 创建一个新系统
|
||||
- 生成一个业务工具
|
||||
- 按需求生成一个管理后台、工作台应用、表单系统、业务原型
|
||||
- 修改一个已经生成过的应用
|
||||
|
||||
典型触发词包括但不限于:
|
||||
|
||||
- 应用、系统、平台、工具、后台、页面、原型、工作台应用
|
||||
- CRM、OA、库存、仓库、审批、销售、报销、进销存、客户管理、数据录入、查询工具
|
||||
- 计算器、待办工具、表单应用、信息收集应用、报表应用
|
||||
- “帮我做一个…”
|
||||
- “帮我创建一个…”
|
||||
- “按这个需求生成一个…”
|
||||
- “把这个需求做成应用”
|
||||
|
||||
典型例子:
|
||||
|
||||
- “帮我创建个简易的计算器应用”
|
||||
- “做一个仓库管理系统”
|
||||
- “生成一个客户管理工具”
|
||||
- “按这个 PRD 创建一个钉钉应用”
|
||||
- “修改刚才那个应用,把首页改得更简洁”
|
||||
|
||||
不要误路由到其他产品:
|
||||
|
||||
- 不要因为用户提到“表格/记录/字段”,就优先走 `aitable`,如果用户的真实目标是“生成一个完整应用”
|
||||
- 不要因为用户提到“文档/需求”,就优先走 `doc`,如果用户的真实目标是“把需求做成应用”
|
||||
- 不要因为用户提到“任务/项目”,就优先走 `tb`,如果用户的真实目标是“生成一个管理系统”
|
||||
|
||||
只有在以下场景不要走 `aiapp`:
|
||||
|
||||
- 用户明确要操作一个现成的多维表、文档、项目、日历、机器人消息
|
||||
- 用户不是要“生成应用”,而是要“修改已有产品对象”
|
||||
|
||||
## 命令总览
|
||||
|
||||
### 创建 AI 应用
|
||||
```
|
||||
Usage:
|
||||
dws aiapp create [flags]
|
||||
Example:
|
||||
dws aiapp create --prompt "创建一个天气查询应用"
|
||||
dws aiapp create --prompt "翻译应用" --skills s1,s2
|
||||
dws aiapp create --prompt "根据附件里的 Excel 创建一个仓库管理应用" --attachments '[{"name":"warehouse.xlsx","type":"excel","url":"https://tmp/warehouse.xlsx"}]'
|
||||
Flags:
|
||||
--prompt string 创建 AI 应用的 prompt (必填)
|
||||
--skills string 技能 ID 列表,逗号分隔
|
||||
--attachments string 附件对象数组的 JSON 字符串,用于把 Excel/图片等输入材料一起传入
|
||||
```
|
||||
|
||||
### 查询 AI 应用
|
||||
```
|
||||
Usage:
|
||||
dws aiapp query [flags]
|
||||
Example:
|
||||
dws aiapp query --task-id <taskId>
|
||||
Flags:
|
||||
--task-id string AI 应用任务 ID (必填)
|
||||
```
|
||||
|
||||
### 修改 AI 应用
|
||||
```
|
||||
Usage:
|
||||
dws aiapp modify [flags]
|
||||
Example:
|
||||
dws aiapp modify --prompt "改为翻译应用" --thread-id <threadId>
|
||||
dws aiapp modify --prompt "新描述" --thread-id <threadId> --skills s1,s2
|
||||
Flags:
|
||||
--prompt string 新的 prompt (必填)
|
||||
--skills string 技能 ID 列表,逗号分隔
|
||||
--thread-id string threadId (必填)
|
||||
```
|
||||
|
||||
## 三个动作的关系
|
||||
|
||||
`aiapp` 的三个动作不是彼此独立的,它们描述的是同一个应用任务的生命周期:
|
||||
|
||||
1. `create` 用来发起一次新的应用生成任务,返回 `taskId` 和 `threadId`
|
||||
2. `query` 用来跟踪这次任务是否完成,以及读取生成结果
|
||||
3. `modify` 用来基于同一个 `threadId` 继续和已生成的应用对话,本质上是对已有应用做增量修改
|
||||
|
||||
可以把它理解为:
|
||||
|
||||
- `create` = 新建一个应用会话
|
||||
- `query` = 查看这个会话对应任务的执行状态和结果
|
||||
- `modify` = 在这个会话里继续追加新的需求,让同一个应用继续演化
|
||||
|
||||
关键标识符的关系:
|
||||
|
||||
- `taskId` 表示某一次具体的执行任务,主要给 `query` 使用
|
||||
- `threadId` 表示这个应用的持续会话,主要给 `modify` 使用
|
||||
- 一次 `create` 会产生一个新的 `taskId`,同时也会产出或确认一个 `threadId`
|
||||
- 后续每次 `modify` 都是在同一个 `threadId` 上发起新的任务,因此也会产生新的 `taskId`
|
||||
|
||||
因此推荐把它们看成一条闭环链路,而不是三个平级命令:
|
||||
|
||||
`create -> query -> modify -> query -> modify ...`
|
||||
|
||||
## `attachments` 的含义
|
||||
|
||||
`attachments` 是 `aiapp` 的输入材料参数,用来把文件和当前任务一起提交给 `aiapp`。
|
||||
|
||||
它适合这些场景:
|
||||
|
||||
- 根据 Excel 创建应用
|
||||
- 根据需求文档、表格、截图继续完善应用
|
||||
- 在创建应用时补充新的结构化材料
|
||||
|
||||
可以把它理解为:
|
||||
|
||||
- `prompt` 描述“希望系统做什么”
|
||||
- `attachments` 提供“系统可直接参考的文件材料”
|
||||
|
||||
### 正确心智
|
||||
|
||||
- 当前 CLI 中,`attachments` 是 `create` 的补充输入,不是独立动作
|
||||
- 即使提供了附件,主动作仍然是 `aiapp create`
|
||||
- 当用户说“按这个 Excel 建一个应用”时,优先走 `aiapp`,并把 Excel 作为 `attachments` 传入
|
||||
|
||||
### 数据结构
|
||||
|
||||
`attachments` 不是单个字符串,而是对象数组。每个对象当前按以下结构传递:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "warehouse.xlsx",
|
||||
"type": "excel",
|
||||
"url": "https://tmp/warehouse.xlsx",
|
||||
"size": 102400
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
每个附件对象当前可用字段:
|
||||
|
||||
- `name`:文件名
|
||||
- `type`:文件类型或 MIME 类型
|
||||
- `url`:可直接访问的临时下载地址
|
||||
- `size`:文件大小,可选
|
||||
|
||||
如果要传多个附件,就在数组中放多个对象。
|
||||
|
||||
### Excel 场景
|
||||
|
||||
如果用户上传了 Excel,通常应该:
|
||||
|
||||
- 在 `prompt` 里明确写“根据附件中的 Excel 创建应用”
|
||||
- 同时把 Excel 通过 `attachments` 传给 `aiapp`
|
||||
|
||||
这类需求都应优先视为 `aiapp`:
|
||||
|
||||
- “根据这个 Excel 建一个库存管理应用”
|
||||
- “把附件里的表格做成一个 CRM 应用”
|
||||
- “按这个 Excel 生成一个新应用”
|
||||
|
||||
### 使用规则
|
||||
|
||||
- `create` 可以带图片、Excel 等附件
|
||||
- 当前 `modify` 不支持 `--attachments` 参数;需要结合新材料修改应用时,把材料内容概括进 `prompt`
|
||||
- `query` 不使用 `attachments`
|
||||
- 附件应按对象数组传递,而不是传单个 ID 或随意文本
|
||||
- 如果用户提供本地文件并要创建应用 → 先上传文件获取 URL,再执行 `aiapp create`
|
||||
- 每个附件对象通常至少应包含可访问的临时下载地址 `url`
|
||||
- 如果有附件但没有特别完整的 prompt,也仍然建议补一句明确意图,避免模型误判
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 根据 Excel 创建应用
|
||||
dws aiapp create --prompt "根据附件里的 Excel 创建一个仓库管理应用" \
|
||||
--attachments '[{"name":"warehouse.xlsx","type":"excel","url":"https://tmp/warehouse.xlsx"}]' \
|
||||
--format json
|
||||
|
||||
# 创建应用时结合图片材料
|
||||
dws aiapp create --prompt "根据附件图片创建一个首页风格参考应用" \
|
||||
--attachments '[{"name":"homepage.png","type":"image/png","url":"https://tmp/homepage.png"}]' \
|
||||
--format json
|
||||
```
|
||||
|
||||
## `--skills` 的含义
|
||||
|
||||
`--skills` 是 `aiapp` 的附加能力参数,不是一个独立动作。
|
||||
|
||||
它的作用不是“先执行这些技能,再单独执行 `aiapp`”,而是:
|
||||
|
||||
- 在 `create` 时,把这些官方技能作为本次应用生成任务的附加约束或附加能力一起传入
|
||||
- 在 `modify` 时,把这些官方技能作为本次修改任务的附加约束或附加能力一起传入
|
||||
|
||||
可以把它理解为:
|
||||
|
||||
- `prompt` 描述“你想做什么应用”
|
||||
- `--skills` 描述“这次生成或修改时,要额外挂载哪些官方能力或规范”
|
||||
|
||||
因此 `--skills` 必须依附于 `create` 或 `modify` 使用,本身不能单独完成创建应用。
|
||||
|
||||
### 正确心智
|
||||
|
||||
- `aiapp` 是主动作
|
||||
- `--skills` 是主动作的附加参数
|
||||
- 用户是否挂载官方技能,会影响应用生成或修改效果
|
||||
- 即使用户提到了某个官方技能,最终仍然应该优先调用 `aiapp create` 或 `aiapp modify`
|
||||
|
||||
### 常见误区
|
||||
|
||||
不要把以下场景理解错:
|
||||
|
||||
- 用户说“创建一个应用,并遵循设计规范”
|
||||
- 正确做法:调用 `aiapp create ... --skills <design-skill-id>`
|
||||
- 不正确做法:把“设计规范技能”当成一个和 `aiapp` 平级、单独执行的动作
|
||||
|
||||
- 用户说“继续修改刚才那个应用,并带上设计规范”
|
||||
- 正确做法:调用 `aiapp modify ... --thread-id <threadId> --skills <design-skill-id>`
|
||||
- 不正确做法:先执行设计规范技能,再单独调用 `modify`
|
||||
|
||||
- 用户只是在描述“希望页面更漂亮、更标准”
|
||||
- 如果已知对应的官方技能 ID,可以通过 `--skills` 传入
|
||||
- 如果没有明确的技能 ID,不要编造,优先把需求写进 `prompt`
|
||||
|
||||
### 什么时候传 `--skills`
|
||||
|
||||
适合传 `--skills` 的情况:
|
||||
|
||||
- 用户明确要求遵循某个已知的官方能力、官方规范、官方模板
|
||||
- 当前上下文里已经拿到了可用的官方技能 ID/Code
|
||||
- 需要在创建或修改时显式挂载这些技能
|
||||
|
||||
不适合传 `--skills` 的情况:
|
||||
|
||||
- 没有明确的技能 ID
|
||||
- 只是模糊地希望“更专业”“更好看”,但没有对应官方技能可挂
|
||||
- 技能本身不是 `aiapp` 官方子技能,而是其他产品的独立操作能力
|
||||
|
||||
### 使用规则
|
||||
|
||||
- `create` 和 `modify` 都可以带 `--skills`
|
||||
- `query` 不使用 `--skills`
|
||||
- `--skills` 里的值必须是技能 ID,多个值用逗号分隔
|
||||
- 不要猜测、编造技能 ID,必须来自已知上下文或系统返回
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# 创建应用,并挂载一个钉钉设计规范技能
|
||||
dws aiapp create --prompt "创建一个仓库管理应用" \
|
||||
--skills dingtalk_design_spec --format json
|
||||
|
||||
# 修改已有应用,并继续带上官方技能
|
||||
dws aiapp modify --prompt "增加个库存大盘图表页" \
|
||||
--thread-id <threadId> \
|
||||
--skills dingtalk_design_spec --format json
|
||||
```
|
||||
|
||||
## 意图判断
|
||||
|
||||
用户说以下任何一种,都走 `create`:
|
||||
|
||||
- “做个应用 / 创建应用 / 生成应用 / 搭个应用”
|
||||
- “做个系统 / 平台 / 后台 / 工具 / 页面”
|
||||
- “帮我做个计算器 / 仓库管理 / CRM / OA / 审批 / 报表 / 信息收集应用”
|
||||
- “按这段需求生成一个钉钉应用”
|
||||
|
||||
用户说以下任何一种,都走 `query`:
|
||||
|
||||
- “查看应用状态”
|
||||
- “这个应用创建好了没”
|
||||
- “查一下 taskId 对应的进度”
|
||||
- “继续轮询应用创建进展”
|
||||
|
||||
用户说以下任何一种,都走 `modify`:
|
||||
|
||||
- “修改应用”
|
||||
- “更新刚才那个应用”
|
||||
- “基于这个 thread/task 继续改”
|
||||
- “把首页改一下 / 把功能补上 / 换个风格”
|
||||
|
||||
## 核心工作流
|
||||
|
||||
```bash
|
||||
# 1. 创建 AI 应用 — 提取 taskId 和 threadId
|
||||
dws aiapp create --prompt "创建一个天气查询应用" --format json
|
||||
|
||||
# 2. 查询应用状态
|
||||
dws aiapp query --task-id <taskId> --format json
|
||||
|
||||
# 3. 修改应用
|
||||
dws aiapp modify --prompt "新描述" --thread-id <threadId> \
|
||||
--skills skill1,skill2 --format json
|
||||
```
|
||||
|
||||
补充理解:
|
||||
|
||||
- 如果用户还没有现成应用,先走 `create`
|
||||
- 如果用户刚发起了创建,通常先走 `query` 看是否完成
|
||||
- 如果用户说“继续改刚才那个应用”,优先找上一次返回里的 `threadId`,走 `modify`
|
||||
- `modify` 完成后,仍然可以继续用新的 `taskId` 走 `query`
|
||||
- 不要把 `modify` 理解成“覆盖旧应用”,它更像是在同一个应用会话中追加新需求
|
||||
- `attachments` 不改变 `create/query/modify` 的主流程,它只是给 `create` 增加文件输入
|
||||
- 如果附件是 Excel,优先理解为 `create` 场景;`modify` 当前不要使用附件参数
|
||||
- `--skills` 不改变 `create/query/modify` 的主流程,它只是给 `create` 或 `modify` 增加官方附加能力
|
||||
|
||||
推荐决策顺序:
|
||||
|
||||
1. 先判断用户是不是要“生成一个新的应用/系统”
|
||||
2. 如果是,直接用 `aiapp create`
|
||||
3. 如果用户是在已有应用基础上继续改,用 `aiapp modify`
|
||||
4. 如果只是查进度或结果,用 `aiapp query`
|
||||
|
||||
## 查询与轮询预期
|
||||
|
||||
`aiapp` 的创建和修改通常不是秒级任务,而是分钟级任务。
|
||||
|
||||
- 简单应用通常需要几分钟
|
||||
- 复杂应用可能持续十几分钟到几十分钟
|
||||
|
||||
因此在使用 `query` 时,不要假设“连续几次没有完成”就代表任务卡死。
|
||||
|
||||
### 正确的轮询策略
|
||||
|
||||
- 不要每 5-10 秒高频轮询
|
||||
- 建议以 30 秒为默认轮询间隔
|
||||
- 如果任务已经明确进入长流程,可进一步放宽到 60 秒
|
||||
- 除非用户明确要求实时刷新,否则不要自行创建过于频繁的定时查询
|
||||
|
||||
### 代理行为约束
|
||||
|
||||
当 agent 已经帮用户发起了 `aiapp create` 或 `aiapp modify` 后,不要只查询 1-2 次就把后续进度甩给用户自己处理。
|
||||
|
||||
- 只要任务仍处于 `queued` 或 `running`,且用户没有明确要求停止,agent 应继续主动跟踪进度
|
||||
- 如果当前会话无法持续等待,可以创建定时任务继续查询,但定时任务必须是“以拿到最终结果为目标”的,而不是无条件永久轮询
|
||||
- 如果返回了 `threadViewUrl`,可以告诉用户“你也可以点这个链接查看实时进度”,但这不能替代 agent 自己继续追踪任务
|
||||
|
||||
### 定时任务规则
|
||||
|
||||
如果 agent 选择创建定时任务来继续查询 `aiapp` 状态,必须同时设置结束条件:
|
||||
|
||||
- 当 `status = succeeded` 且已拿到 `appPreviewUrl` 时,立即停止后续查询,并主动删除或关闭该定时任务
|
||||
- 当 `status = failed` 时,立即停止后续查询,并主动删除或关闭该定时任务
|
||||
- 不要在已经拿到目标结果后继续保留定时任务
|
||||
- 不要创建“只会查询、不会自终止”的定时任务
|
||||
|
||||
换句话说,定时任务应当是“查询直到成功或失败,然后自动结束”的一次性追踪任务,而不是长期后台轮询
|
||||
|
||||
### 如何理解 `query` 返回
|
||||
|
||||
`query` 的返回里,除了总状态外,还可能包含进度信息,例如:
|
||||
|
||||
- 当前正在执行哪个步骤
|
||||
- 已完成多少步骤
|
||||
- 最近一次活动时间
|
||||
- 当前步骤列表
|
||||
- 当前 thread 的访问链接
|
||||
|
||||
因此判断任务是否还在推进时,不要只看 `status` 是否还是 `running`,还要结合以下字段一起看:
|
||||
|
||||
- `updatedAt` / `lastActivityAt`
|
||||
- `progress.summary`
|
||||
- `progress.currentStep`
|
||||
- `progress.steps`
|
||||
- `threadViewUrl`
|
||||
|
||||
只要这些字段仍在变化,通常就说明任务仍在正常推进。
|
||||
|
||||
### `threadViewUrl` 与 `appPreviewUrl` 的区别
|
||||
|
||||
`query` 或 `create` 的返回里,可能会有两个不同的链接:
|
||||
|
||||
- `threadViewUrl`:当前 thread 的访问链接。用于打开 `aiapp` 站点里的会话/生成进度页面,适合在应用还没生成完成时查看实时状态
|
||||
- `appPreviewUrl`:最终生成出的应用访问地址。只有应用创建成功后才会出现,适合真正打开应用去使用
|
||||
|
||||
不要把这两个链接混用:
|
||||
|
||||
- 任务还在 `queued/running` 时,优先使用 `threadViewUrl`
|
||||
- 任务 `succeeded` 且结果里已有 `appPreviewUrl` 时,再使用 `appPreviewUrl`
|
||||
|
||||
### 何时停止轮询
|
||||
|
||||
- 当 `status = succeeded` 时,停止轮询,并从结果中提取 `threadId`、应用链接等信息
|
||||
- 当 `status = failed` 时,停止轮询,并向用户报告失败
|
||||
- 当任务长时间保持 `running`,但 `lastActivityAt` 和 `progress` 都没有变化时,再考虑提示可能卡住,而不是过早下结论
|
||||
- 如果此前为查询进度创建了定时任务,在 `succeeded` 或 `failed` 后要主动删除该定时任务,而不是留给用户处理
|
||||
|
||||
## 上下文传递表
|
||||
|
||||
| 操作 | 从返回中提取 | 用于 |
|
||||
|------|-------------|------|
|
||||
| `create` | `taskId`, `threadId`, `threadViewUrl` | query 的 --task-id, modify 的 --thread-id,或直接打开进度页 |
|
||||
| `query` | `threadViewUrl`, `appPreviewUrl`, 应用详情 | 查看进度页,或在成功后打开应用预览并决定是否 modify |
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `create` 通过自然语言 prompt 描述应用功能,系统自动生成
|
||||
- 对“应用/系统/工具/后台/原型”类需求,优先使用 `aiapp`,不要改用其他产品命令模拟实现
|
||||
- 对“根据附件 / 根据 Excel / 根据上传表格生成应用”这类需求,也优先使用 `aiapp`
|
||||
- `attachments` 需要按对象数组传递,当前结构是 `[{name,type,url,size}]`
|
||||
- `create` 可使用 Excel、图片等附件;`modify` 当前不支持附件参数
|
||||
- `--skills` 传入官方技能 UID 列表,逗号分隔,可增强应用能力
|
||||
- `modify` 需要 `--thread-id` 来标识修改哪个会话线程
|
||||
- 应用未生成完成前,如果需要让用户查看实时状态,优先使用返回里的 `threadViewUrl`
|
||||
- 应用生成成功后,如果需要真正打开应用,再使用 `appPreviewUrl`
|
||||
- 创建后可通过 `query` 轮询 `taskId` 查看创建进度,但轮询间隔默认应为 20-30 秒,不要 10 秒级频繁轮询
|
||||
- agent 不应只查 1-2 次就放弃;如果任务仍在运行,应继续跟踪,或创建带终止条件的定时任务继续跟踪
|
||||
- 如果 agent 创建了用于查询进度的定时任务,在任务成功或失败后必须主动删除或结束该定时任务
|
||||
- 不要直接使用用户本地文件路径作为附件 URL,必须先上传转换为互联网可访问链接
|
||||
|
||||
## 自动化脚本
|
||||
|
||||
| 脚本 | 场景 | 用法 |
|
||||
|------|------|------|
|
||||
| [aiapp_create_and_poll.py](../../scripts/aiapp_create_and_poll.py) | 一键创建 AI 应用并自动轮询进度直到完成 | `python aiapp_create_and_poll.py --prompt "创建一个天气查询应用"` |
|
||||
@@ -1,138 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
创建 AI 应用并自动轮询等待完成
|
||||
|
||||
用法:
|
||||
python aiapp_create_and_poll.py \
|
||||
--prompt "创建一个仓库管理应用"
|
||||
|
||||
python aiapp_create_and_poll.py \
|
||||
--prompt "生成客户管理 CRM" \
|
||||
--skills skill1,skill2 \
|
||||
--interval 30 \
|
||||
--timeout 600
|
||||
|
||||
python aiapp_create_and_poll.py --dry-run --prompt "test"
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
import argparse
|
||||
import time
|
||||
from typing import List, Any, Optional
|
||||
|
||||
|
||||
def run_dws(
|
||||
args: List[str], dry_run: bool = False,
|
||||
) -> Optional[Any]:
|
||||
cmd = ['dws'] + args
|
||||
if dry_run:
|
||||
print(f"[dry-run] {' '.join(cmd)}")
|
||||
return {'dry_run': True}
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, capture_output=True, text=True, timeout=120
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(f" ✗ 错误:{result.stderr.strip()}")
|
||||
return None
|
||||
return json.loads(result.stdout)
|
||||
except (subprocess.TimeoutExpired, json.JSONDecodeError,
|
||||
FileNotFoundError) as e:
|
||||
print(f" ✗ 错误:{e}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='创建 AI 应用并轮询等待完成'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--prompt', required=True, help='应用描述'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--skills', default='', help='技能 ID 列表'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--interval', type=int, default=30,
|
||||
help='轮询间隔秒 (默认 30)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--timeout', type=int, default=600,
|
||||
help='最大等待秒 (默认 600)',
|
||||
)
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f'🚀 创建 AI 应用...')
|
||||
print(f' Prompt: {args.prompt}')
|
||||
cmd_args = [
|
||||
'aiapp', 'create',
|
||||
'--prompt', args.prompt,
|
||||
'--format', 'json',
|
||||
]
|
||||
if args.skills:
|
||||
cmd_args.extend(['--skills', args.skills])
|
||||
|
||||
create_data = run_dws(cmd_args, dry_run=args.dry_run)
|
||||
if args.dry_run:
|
||||
run_dws([
|
||||
'aiapp', 'query',
|
||||
'--task-id', '<TASK_ID>',
|
||||
'--format', 'json',
|
||||
], dry_run=True)
|
||||
return
|
||||
|
||||
if not create_data:
|
||||
sys.exit(1)
|
||||
|
||||
task_id = create_data.get('taskId') or create_data.get('id', '')
|
||||
thread_id = create_data.get('threadId', '')
|
||||
print(f" ✓ 任务已创建")
|
||||
print(f" taskId: {task_id}")
|
||||
print(f" threadId: {thread_id}")
|
||||
|
||||
print(f'\n⏳ 轮询等待 (间隔 {args.interval}s, '
|
||||
f'超时 {args.timeout}s)...')
|
||||
elapsed = 0
|
||||
while elapsed < args.timeout:
|
||||
time.sleep(args.interval)
|
||||
elapsed += args.interval
|
||||
|
||||
query_data = run_dws([
|
||||
'aiapp', 'query',
|
||||
'--task-id', task_id,
|
||||
'--format', 'json',
|
||||
])
|
||||
if not query_data:
|
||||
print(f" [{elapsed}s] ⚠ 查询失败,继续等待...")
|
||||
continue
|
||||
|
||||
status = (query_data.get('status')
|
||||
or query_data.get('state', 'unknown'))
|
||||
progress = query_data.get('progress', {})
|
||||
step = ''
|
||||
if isinstance(progress, dict):
|
||||
step = progress.get('currentStep', '')
|
||||
|
||||
if status == 'succeeded':
|
||||
print(f" [{elapsed}s] ✅ 应用创建成功!")
|
||||
if thread_id:
|
||||
print(f" threadId: {thread_id}")
|
||||
return
|
||||
elif status == 'failed':
|
||||
print(f" [{elapsed}s] ❌ 创建失败")
|
||||
sys.exit(1)
|
||||
else:
|
||||
info = f" [{elapsed}s] ⏳ {status}"
|
||||
if step:
|
||||
info += f" ({step})"
|
||||
print(info)
|
||||
|
||||
print(f"\n⏰ 超时 ({args.timeout}s),任务可能仍在运行")
|
||||
print(f" 可手动查询: dws aiapp query --task-id {task_id}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -17,11 +17,11 @@ func TestMain(m *testing.M) {
|
||||
absFixture, _ := filepath.Abs("testdata/empty_catalog.json")
|
||||
os.Setenv(cli.CatalogFixtureEnv, absFixture)
|
||||
|
||||
// Serve the local servers.json fixture at /cli/discovery/apis so that
|
||||
// Serve the local servers.json fixture at /cli/discovery/apis/bamboo so that
|
||||
// the dynamic command generator can build CLI commands without network
|
||||
// access. FetchServers calls {baseURL}/cli/discovery/apis.
|
||||
// access. FetchServers calls {baseURL}/cli/discovery/apis/bamboo.
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/cli/discovery/apis", func(w http.ResponseWriter, r *http.Request) {
|
||||
mux.HandleFunc("/cli/discovery/apis/bamboo", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "testdata/servers.json")
|
||||
})
|
||||
srv := httptest.NewServer(mux)
|
||||
|
||||
@@ -17,11 +17,11 @@ func TestMain(m *testing.M) {
|
||||
absFixture, _ := filepath.Abs("testdata/empty_catalog.json")
|
||||
os.Setenv(cli.CatalogFixtureEnv, absFixture)
|
||||
|
||||
// Serve the local servers.json fixture at /cli/discovery/apis so that
|
||||
// Serve the local servers.json fixture at /cli/discovery/apis/bamboo so that
|
||||
// the dynamic command generator can build CLI commands without network
|
||||
// access. FetchServers calls {baseURL}/cli/discovery/apis.
|
||||
// access. FetchServers calls {baseURL}/cli/discovery/apis/bamboo.
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/cli/discovery/apis", func(w http.ResponseWriter, r *http.Request) {
|
||||
mux.HandleFunc("/cli/discovery/apis/bamboo", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "testdata/servers.json")
|
||||
})
|
||||
srv := httptest.NewServer(mux)
|
||||
|
||||
@@ -361,7 +361,7 @@ func newDocsMCPGateway(t *testing.T, expectations []docsServerExpectation) *http
|
||||
mux := http.NewServeMux()
|
||||
server := httptest.NewServer(mux)
|
||||
|
||||
mux.HandleFunc("/cli/discovery/apis", func(w http.ResponseWriter, r *http.Request) {
|
||||
mux.HandleFunc("/cli/discovery/apis/bamboo", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
|
||||
@@ -237,7 +237,7 @@ func newRuntimeMarketServer(t *testing.T, fixture mockmcp.Fixture) *httptest.Ser
|
||||
mux := http.NewServeMux()
|
||||
srv := httptest.NewServer(mux)
|
||||
|
||||
mux.HandleFunc("/cli/discovery/apis", func(w http.ResponseWriter, r *http.Request) {
|
||||
mux.HandleFunc("/cli/discovery/apis/bamboo", func(w http.ResponseWriter, r *http.Request) {
|
||||
response := market.ListResponse{}
|
||||
for _, server := range fixture.Servers {
|
||||
response.Servers = append(response.Servers, market.ServerEnvelope{
|
||||
|
||||
@@ -134,7 +134,7 @@ func newEvolvingRuntimeMarketServer(t *testing.T, fixture mockmcp.Fixture) *evol
|
||||
s.SetFixture(t, fixture)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/cli/discovery/apis", s.serveMarketServers)
|
||||
mux.HandleFunc("/cli/discovery/apis/bamboo", s.serveMarketServers)
|
||||
mux.HandleFunc("/mcp/market/detail", s.serveMarketDetail)
|
||||
mux.HandleFunc("/", s.serveMCP)
|
||||
s.server = httptest.NewServer(mux)
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestEndToEndDiscoveryFlow(t *testing.T) {
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
mux.HandleFunc("/cli/discovery/apis", func(w http.ResponseWriter, r *http.Request) {
|
||||
mux.HandleFunc("/cli/discovery/apis/bamboo", func(w http.ResponseWriter, r *http.Request) {
|
||||
payload := market.ListResponse{
|
||||
Metadata: market.ListMetadata{Count: 1},
|
||||
Servers: []market.ServerEnvelope{
|
||||
|
||||
@@ -24,7 +24,7 @@ func NewServer(fixture Fixture) (*Server, error) {
|
||||
|
||||
s := &Server{Fixture: fixture}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/cli/discovery/apis", s.serveMarketServers)
|
||||
mux.HandleFunc("/cli/discovery/apis/bamboo", s.serveMarketServers)
|
||||
mux.HandleFunc("/mcp/market/detail", s.serveMarketDetail)
|
||||
mux.HandleFunc("/", s.serveMCP)
|
||||
|
||||
@@ -45,7 +45,7 @@ func DefaultServer() *Server {
|
||||
}
|
||||
|
||||
func (s *Server) MarketURL() string {
|
||||
return s.URL + "/cli/discovery/apis"
|
||||
return s.URL + "/cli/discovery/apis/bamboo"
|
||||
}
|
||||
|
||||
func (s *Server) DetailURL(mcpID int) string {
|
||||
|
||||
@@ -315,7 +315,6 @@ def run_test(test_case: TestCase) -> dict:
|
||||
def get_reference_file(product: str) -> str:
|
||||
"""Get the reference file for a product"""
|
||||
product_to_file = {
|
||||
'aiapp': 'aiapp.md',
|
||||
'aitable': 'aitable.md',
|
||||
'attendance': 'attendance.md',
|
||||
'calendar': 'calendar.md',
|
||||
|
||||
@@ -210,7 +210,7 @@ func TestDiscoverAllRuntimeKeepsWorkingServersWhenOneFails(t *testing.T) {
|
||||
server := httptest.NewServer(mux)
|
||||
defer server.Close()
|
||||
|
||||
mux.HandleFunc("/cli/discovery/apis", func(w http.ResponseWriter, r *http.Request) {
|
||||
mux.HandleFunc("/cli/discovery/apis/bamboo", func(w http.ResponseWriter, r *http.Request) {
|
||||
payload := market.ListResponse{
|
||||
Metadata: market.ListMetadata{Count: 2},
|
||||
Servers: []market.ServerEnvelope{
|
||||
|
||||
Reference in New Issue
Block a user