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
16 changes: 15 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 @@ -123,9 +130,16 @@ tier0 flow get --id 1
tier0 flow create --name "modbus-collector" --source --desc "Modbus 数据采集"
tier0 flow create --name "alert-handler" --event

# JSON 输出会在 data 中返回业务主键 id 和默认注入的 MQTT brokerID
tier0 flow create --name "modbus-collector" --source --json
# → {"code":200,"msg":"success","data":{"id":1,"brokerID":"broker-xxxx"}}

# 更新 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 +273,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
6 changes: 3 additions & 3 deletions cmd/uns_search.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ var unsSearchCmd = &cobra.Command{
Use: "search",
Short: i18n.T("Search UNS topics", "搜索 UNS 点位"),
Long: i18n.T(
"Search topics in the UNS by keyword, tag, or path prefix.\n\nExamples:\n tier0 uns search --keyword temp\n tier0 uns search --path-prefix /devices --size 50\n tier0 uns search --keyword temp --include-metadata",
"按关键字、标签或路径前缀搜索 UNS 中的点位。\n\n示例:\n tier0 uns search --keyword temp\n tier0 uns search --path-prefix /devices --size 50\n tier0 uns search --keyword temp --include-metadata",
"Search topics in the UNS by node name, any path segment, or path prefix.\n\nExamples:\n tier0 uns search --keyword Line1\n tier0 uns search --path-prefix /devices --size 50\n tier0 uns search --keyword temp --include-metadata",
"按节点名、任一级路径名或路径前缀搜索 UNS 点位。\n\n示例:\n tier0 uns search --keyword Line1\n tier0 uns search --path-prefix /devices --size 50\n tier0 uns search --keyword temp --include-metadata",
),
RunE: runUnsSearch,
}

func init() {
unsSearchCmd.Flags().StringP("keyword", "k", "",
i18n.T("Search by name keyword", "按名称关键字搜索"))
i18n.T("Search by node name or path keyword", "按节点名或路径关键字搜索"))
unsSearchCmd.Flags().String("path-prefix", "/",
i18n.T("Filter by path prefix", "按路径前缀过滤"))
unsSearchCmd.Flags().String("topic-type", "",
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