Skip to content
Open
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
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ tier0 api /openapi/v1/uns/read --body '{"topics":["demo"]}'

# 浏览命名空间
tier0 api /openapi/v1/uns/browse --body '{"path":"/"}'

# 上传 / 查询 UNS 附件
tier0 uns attachments upload --uns-id 10001 --file manual.pdf
tier0 uns attachments list --uns-id 10001

# 绑定 UNS 节点到 SourceFlow
tier0 uns bind-flow --uns-id 10001 --flow-id 20001
```

### 查看当前 API Key
Expand Down Expand Up @@ -126,6 +133,9 @@ tier0 flow create --name "alert-handler" --event
# 更新 Flow(名称、描述、收藏)
tier0 flow update --id 1 --name "new-name" --favorite

# 绑定 UNS 节点到 SourceFlow(flow-id 使用上面列表里的业务主键 ID)
tier0 uns bind-flow --uns-id 10001 --flow-id 1

# 删除 Flow(支持多个 ID)
tier0 flow delete --id 1 --id 2
tier0 flow delete 1,2,3
Expand Down Expand Up @@ -259,7 +269,7 @@ npx skills remove FREEZONEX/Tier0-skill

| Version | Date | Notes |
|---------|------|-------|
| Unreleased | — | 新增 `tier0 auth whoami` 和 `tier0 flow nodes` |
| Unreleased | — | 新增 `tier0 auth whoami`、`tier0 flow nodes`、`tier0 uns attachments`、`tier0 uns bind-flow`,`tier0 uns create` 支持 `--persistence` |
| [v0.4.11](https://github.com/FREEZONEX/Tier0-cli/releases/tag/v0.4.11) | 2026-05-26 | 修复写操作忽略后端业务错误的问题 |
| [v0.4.10](https://github.com/FREEZONEX/Tier0-cli/releases/tag/v0.4.10) | 2026-05-26 | 新增 `tier0 config --api-key` 直接设置 API Key |
| [v0.4.9](https://github.com/FREEZONEX/Tier0-cli/releases/tag/v0.4.9) | 2026-05-26 | 新增 `tier0 uninstall`;修复安装版本错误(直接用 npm 包版本);修复 release.sh JSON 400 |
Expand Down
2 changes: 2 additions & 0 deletions cmd/uns.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ func init() {
unsCmd.AddCommand(unsSearchCmd)
unsCmd.AddCommand(unsHistoryCmd)
unsCmd.AddCommand(unsRestoreCmd)
unsCmd.AddCommand(unsAttachmentsCmd)
unsCmd.AddCommand(unsBindFlowCmd)
}
176 changes: 176 additions & 0 deletions cmd/uns_attachments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package cmd

import (
"encoding/json"
"fmt"
"net/url"
"strconv"
"strings"

"github.com/FREEZONEX/Tier0-cli/internal/cmdutil"
"github.com/FREEZONEX/Tier0-cli/internal/i18n"
"github.com/FREEZONEX/Tier0-cli/internal/notice"
"github.com/spf13/cobra"
)

var unsAttachmentsCmd = &cobra.Command{
Use: "attachments",
Aliases: []string{"attachment", "files"},
Short: i18n.T("Manage UNS attachments", "管理 UNS 附件"),
Long: i18n.T(
"Upload and list files bound to a UNS node by unsId.",
"按 unsId 上传和查询绑定到 UNS 节点的附件。",
),
}

var unsAttachmentUploadCmd = &cobra.Command{
Use: "upload",
Short: i18n.T("Upload a file attachment to a UNS node", "上传 UNS 节点附件"),
RunE: runUnsAttachmentUpload,
}

var unsAttachmentListCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: i18n.T("List attachments of a UNS node", "查询 UNS 节点附件"),
RunE: runUnsAttachmentList,
}

func init() {
unsAttachmentsCmd.AddCommand(unsAttachmentUploadCmd)
unsAttachmentsCmd.AddCommand(unsAttachmentListCmd)

unsAttachmentUploadCmd.Flags().Int64("uns-id", 0,
i18n.T("UNS node ID (required)", "UNS 节点 ID(必填)"))
unsAttachmentUploadCmd.Flags().StringP("file", "f", "",
i18n.T("Local file path to upload (required)", "要上传的本地文件路径(必填)"))
unsAttachmentUploadCmd.Flags().String("file-name", "",
i18n.T("Override attachment fileName", "覆盖附件 fileName"))
unsAttachmentUploadCmd.Flags().String("sha256", "",
i18n.T("Optional client-side sha256", "可选的客户端 sha256"))
unsAttachmentUploadCmd.MarkFlagRequired("uns-id")
unsAttachmentUploadCmd.MarkFlagRequired("file")

unsAttachmentListCmd.Flags().Int64("uns-id", 0,
i18n.T("UNS node ID (required)", "UNS 节点 ID(必填)"))
unsAttachmentListCmd.Flags().Int("page-no", 1,
i18n.T("Page number", "页码"))
unsAttachmentListCmd.Flags().Int("page-size", 20,
i18n.T("Page size", "每页条数"))
unsAttachmentListCmd.Flags().Bool("include-file-url", true,
i18n.T("Include downloadable fileUrl", "返回可下载 fileUrl"))
unsAttachmentListCmd.MarkFlagRequired("uns-id")
}

func runUnsAttachmentUpload(cmd *cobra.Command, args []string) error {
checker := notice.Start()
jsonMode, _ := cmd.Flags().GetBool("json")
debug, _ := cmd.Flags().GetBool("debug")
unsID, _ := cmd.Flags().GetInt64("uns-id")
filePath, _ := cmd.Flags().GetString("file")
fileName, _ := cmd.Flags().GetString("file-name")
sha256, _ := cmd.Flags().GetString("sha256")

if unsID <= 0 {
return fmt.Errorf(i18n.T("uns-id is required", "uns-id 为必填"))
}
if strings.TrimSpace(filePath) == "" {
return fmt.Errorf(i18n.T("file is required", "file 为必填"))
}

endpoint := fmt.Sprintf("/openapi/v1/uns/%d/attachments", unsID)
fields := map[string]string{
"fileName": fileName,
"sha256": sha256,
}
resp, err := cmdutil.DoMultipart(cmd.Context(), endpoint, "file", filePath, fileName, fields, debug)
if err != nil {
return cmdutil.HandleCommandError(cmd.ErrOrStderr(), err, jsonMode)
}
if err := cmdutil.CheckOK(resp); err != nil {
return cmdutil.HandleCommandError(cmd.ErrOrStderr(), err, jsonMode)
}

stdout := cmd.OutOrStdout()
if jsonMode {
checker.Emit(resp, true, stdout, cmd.ErrOrStderr())
return nil
}
var result struct {
UnsID int64 `json:"unsId"`
FileName string `json:"fileName"`
FilePath string `json:"filePath"`
FileURL string `json:"fileUrl"`
}
if err := json.Unmarshal([]byte(cmdutil.ExtractData(resp)), &result); err != nil {
fmt.Fprintln(stdout, resp)
checker.Emit("", false, stdout, cmd.ErrOrStderr())
return nil
}
fmt.Fprintf(stdout, i18n.T("✓ Attachment uploaded: %s\n", "✓ 附件上传成功: %s\n"), result.FileName)
fmt.Fprintf(stdout, "unsId: %d\nfilePath: %s\n", result.UnsID, result.FilePath)
if result.FileURL != "" {
fmt.Fprintf(stdout, "fileUrl: %s\n", result.FileURL)
}
checker.Emit("", false, stdout, cmd.ErrOrStderr())
return nil
}

func runUnsAttachmentList(cmd *cobra.Command, args []string) error {
checker := notice.Start()
jsonMode, _ := cmd.Flags().GetBool("json")
debug, _ := cmd.Flags().GetBool("debug")
unsID, _ := cmd.Flags().GetInt64("uns-id")
pageNo, _ := cmd.Flags().GetInt("page-no")
pageSize, _ := cmd.Flags().GetInt("page-size")
includeFileURL, _ := cmd.Flags().GetBool("include-file-url")

if unsID <= 0 {
return fmt.Errorf(i18n.T("uns-id is required", "uns-id 为必填"))
}
q := url.Values{}
q.Set("pageNo", strconv.Itoa(pageNo))
q.Set("pageSize", strconv.Itoa(pageSize))
q.Set("includeFileUrl", strconv.FormatBool(includeFileURL))
endpoint := fmt.Sprintf("/openapi/v1/uns/%d/attachments/list?%s", unsID, q.Encode())

resp, err := cmdutil.DoAPI(cmd.Context(), endpoint, "POST", "", debug)
if err != nil {
return cmdutil.HandleCommandError(cmd.ErrOrStderr(), err, jsonMode)
}
if err := cmdutil.CheckOK(resp); err != nil {
return cmdutil.HandleCommandError(cmd.ErrOrStderr(), err, jsonMode)
}

stdout := cmd.OutOrStdout()
if jsonMode {
checker.Emit(resp, true, stdout, cmd.ErrOrStderr())
return nil
}
var result struct {
List []struct {
FileName string `json:"fileName"`
FilePath string `json:"filePath"`
FileURL string `json:"fileUrl"`
} `json:"list"`
Total int64 `json:"total"`
}
if err := json.Unmarshal([]byte(cmdutil.ExtractData(resp)), &result); err != nil {
fmt.Fprintln(stdout, resp)
checker.Emit("", false, stdout, cmd.ErrOrStderr())
return nil
}
if len(result.List) == 0 {
checker.Emit("", false, stdout, cmd.ErrOrStderr())
fmt.Fprintln(stdout, i18n.T("No attachments found.", "暂无附件。"))
return nil
}
fmt.Fprintf(stdout, "%-30s %-48s %s\n", i18n.T("FileName", "文件名"), "FilePath", "FileUrl")
fmt.Fprintln(stdout, strings.Repeat("-", 120))
for _, item := range result.List {
fmt.Fprintf(stdout, "%-30s %-48s %s\n", item.FileName, item.FilePath, item.FileURL)
}
fmt.Fprintf(stdout, i18n.T("Total: %d\n", "总数: %d\n"), result.Total)
checker.Emit("", false, stdout, cmd.ErrOrStderr())
return nil
}
73 changes: 73 additions & 0 deletions cmd/uns_bind_flow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package cmd

import (
"encoding/json"
"fmt"
"strconv"

"github.com/FREEZONEX/Tier0-cli/internal/cmdutil"
"github.com/FREEZONEX/Tier0-cli/internal/i18n"
"github.com/FREEZONEX/Tier0-cli/internal/notice"
"github.com/spf13/cobra"
)

var unsBindFlowCmd = &cobra.Command{
Use: "bind-flow",
Short: i18n.T("Bind a UNS node to a SourceFlow", "绑定 UNS 节点到 SourceFlow"),
Long: i18n.T(
"Bind a UNS node to a SourceFlow using unsId and flow business ID.",
"使用 unsId 和 Flow 业务主键 ID 关联 UNS 节点与 SourceFlow。",
),
RunE: runUnsBindFlow,
}

func init() {
unsBindFlowCmd.Flags().Int64("uns-id", 0,
i18n.T("UNS node ID (required)", "UNS 节点 ID(必填)"))
unsBindFlowCmd.Flags().Int64("flow-id", 0,
i18n.T("Flow business ID (required)", "Flow 业务主键 ID(必填)"))
unsBindFlowCmd.MarkFlagRequired("uns-id")
unsBindFlowCmd.MarkFlagRequired("flow-id")
}

func runUnsBindFlow(cmd *cobra.Command, args []string) error {
checker := notice.Start()
jsonMode, _ := cmd.Flags().GetBool("json")
debug, _ := cmd.Flags().GetBool("debug")
unsID, _ := cmd.Flags().GetInt64("uns-id")
flowID, _ := cmd.Flags().GetInt64("flow-id")

if unsID == 0 && len(args) > 0 {
unsID, _ = strconv.ParseInt(args[0], 10, 64)
}
if flowID == 0 && len(args) > 1 {
flowID, _ = strconv.ParseInt(args[1], 10, 64)
}
if unsID <= 0 || flowID <= 0 {
return fmt.Errorf(i18n.T(
"specify --uns-id <id> and --flow-id <id>",
"请指定 --uns-id <id> 和 --flow-id <id>",
))
}

body, _ := json.Marshal(map[string]int64{
"unsId": unsID,
"flowId": flowID,
})
resp, err := cmdutil.DoAPI(cmd.Context(), "/openapi/v1/uns/unsBindFlow", "POST", string(body), debug)
if err != nil {
return cmdutil.HandleCommandError(cmd.ErrOrStderr(), err, jsonMode)
}
if err := cmdutil.CheckOK(resp); err != nil {
return cmdutil.HandleCommandError(cmd.ErrOrStderr(), err, jsonMode)
}

stdout := cmd.OutOrStdout()
if jsonMode {
checker.Emit(resp, true, stdout, cmd.ErrOrStderr())
return nil
}
fmt.Fprintf(stdout, i18n.T("✓ UNS %d bound to Flow %d\n", "✓ UNS %d 已绑定到 Flow %d\n"), unsID, flowID)
checker.Emit("", false, stdout, cmd.ErrOrStderr())
return nil
}
6 changes: 6 additions & 0 deletions cmd/uns_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ func init() {
i18n.T("Topic type", "Topic 类型"))
unsCreateCmd.Flags().String("fields", "",
i18n.T("Schema fields JSON array (e.g. '[{\"name\":\"temp\",\"type\":\"float\"}]')", "Schema 字段 JSON 数组"))
unsCreateCmd.Flags().Bool("persistence", false,
i18n.T("Persist topic values to history storage", "持久化保存点位历史数据"))
}

func runUnsCreate(cmd *cobra.Command, args []string) error {
Expand All @@ -52,6 +54,7 @@ func runUnsCreate(cmd *cobra.Command, args []string) error {
file, _ := cmd.Flags().GetString("file")
topicType, _ := cmd.Flags().GetString("topic-type")
fields, _ := cmd.Flags().GetString("fields")
persistence, _ := cmd.Flags().GetBool("persistence")

var namespace []any

Expand Down Expand Up @@ -93,6 +96,9 @@ func runUnsCreate(cmd *cobra.Command, args []string) error {
}
node["fields"] = fieldList
}
if persistence {
node["persistence"] = true
}
namespace = []any{node}
}

Expand Down
4 changes: 2 additions & 2 deletions internal/auth/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func PollSetupCheck(ctx context.Context, baseURL, setupCode string, onPoll func(
APIKey string `json:"apiKey"`
WorkspaceID string `json:"workspaceID"`
WorkspaceName string `json:"workspaceName"`
ExpiresAt string `json:"expiresAt"`
ExpiresAt any `json:"expiresAt"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
Expand All @@ -103,7 +103,7 @@ func PollSetupCheck(ctx context.Context, baseURL, setupCode string, onPoll func(
}
resp.Body.Close()

if result.Code != 200 {
if result.Code != 0 && result.Code != 200 {
err := fmt.Errorf("setup-check failed: %s", result.Msg)
if onPoll != nil {
onPoll(i, maxPollCount, false, err)
Expand Down
Loading