@tianacloud/agent-skills 1.0.17 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,7 +52,7 @@ CLI 主包及当前系统的原生平台包全部来自 npm,提供 tiana 和 g
52
52
 
53
53
  开发者也可以向已运行的 Agent 要求:“设置 `TIANA_API_ORIGIN=https://console.tianacloud-staging.net`,安装 Tiana skills,然后创建一个账单应用。”技能会在本任务每次 CLI/Git 调用中传递这一选择。staging CLI 自动加载内置 CA;开发者需已有 VPN/DNS 和浏览器 CA 信任。仅在安装器子进程中 export 一次不会配置后续命令。
54
54
 
55
- WorkBuddy Connector 固定 online,配置和当前使用说明见 [Connector](docs/workbuddy-connector.md)。
55
+ WorkBuddy Connector 固定 online,配置和当前使用说明见 [Connector](https://github.com/tianacloud/agent-skills/blob/main/docs/workbuddy-connector.md)。
56
56
 
57
57
  ## 检查与更新
58
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tianacloud/agent-skills",
3
- "version": "1.0.17",
3
+ "version": "1.0.18",
4
4
  "description": "Tiana Cloud Skills for accounts, SQLite, Git, Functions and Web applications",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "tiana",
4
- "version": "1.0.17",
4
+ "version": "1.0.18",
5
5
  "description": "Tiana Cloud account, SQLite, Git, Functions and Web application workflows",
6
6
  "author": {
7
7
  "name": "Tiana Cloud",
@@ -4,7 +4,7 @@ description: 使用 Tiana Cloud 管理实例、安装 CLI、登录和恢复账
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Tiana Cloud
7
- version: "1.0.17"
7
+ version: "1.0.18"
8
8
  source: https://github.com/tianacloud/agent-skills/tree/main/skills/tiana
9
9
  ---
10
10
 
@@ -6,6 +6,8 @@
6
6
 
7
7
  账号凭据由 CLI 的安全存储管理。不要在聊天、源码、构建文件或 Git URL 中复制凭据。缺少权限、配额超限与登录失效是不同问题,按实际错误处理。
8
8
 
9
+ 后端服务需要独立 Instance Token 时,使用 `tiana token create INSTANCE --name NAME --json`;先用 `--help` 核实配套 CLI 能力。该 Token 覆盖实例各分支,默认不过期,可用 `--expires-at` 指定 Unix 秒。明文只在创建成功回执中交付,直接保存到密钥库;完整步骤见 `tiana-function` 的“SQLite 访问配置”。回执缺少明文或结果未知时先在 Console 检查原 Token,重复请求不能恢复明文。
10
+
9
11
  浏览器应用通过 `window.tiana.connection()` 获取连接元信息,通过共享的 `window.tiana.auth` 获取动态 access token。应用不使用 `connection.tianaToken` 快照、不传静态 Token,也不回退到实例 Token。
10
12
 
11
13
  CLI 本地预览优先复用已有 CLI 登录,通过一次性本地启动链接授权浏览器;没有可用账号时才走独立浏览器登录。刷新和持久化由 CLI/SDK 负责,refresh token 不交给页面。access token 具有账号/租户级权限,清单中的数据库 ID 不收窄凭据权限。
@@ -1,17 +1,17 @@
1
1
  ---
2
2
  name: tiana-function
3
- description: 使用 Tiana 创建和管理 Go 后端服务,读取及保存 Git 源码、编译发布、修改参数与 CORS、开发 WebSocket 实时接口;为 Web 应用开发访问 SQLite 的后端服务,或初始化用户密钥库、列出密钥名称与版本、写入和删除密钥时使用。前端页面开发配合 tiana-web。
3
+ description: 使用 Tiana 创建和管理 Go 后端服务,读取及保存 Git 源码、编译发布、修改参数与 CORS、开发 WebSocket 实时接口、在函数中访问文件存储;为 Web 应用开发访问 SQLite 的后端服务,或初始化用户密钥库、列出密钥名称与版本、写入和删除密钥时使用。前端页面开发配合 tiana-web。
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Tiana Cloud
7
- version: "1.0.17"
7
+ version: "1.0.18"
8
8
  parent: tiana
9
9
  source: https://github.com/tianacloud/agent-skills/tree/main/skills/tiana-function
10
10
  ---
11
11
 
12
12
  # Tiana Function
13
13
 
14
- 本 skill 负责后端服务的创建与管理、源码、运行参数、用户密钥库、Go 接口及数据库访问、编译发布。为 Web 开发后端时,接收 `tiana-web` 技能确定的 Git ID、接口需求和数据依赖;完成后返回服务 ID、source_dir、发布回执、接口契约和参数生效结果,由 Web skill 继续前端接入、绑定、发布及分享验收。
14
+ 本 skill 负责后端服务的创建与管理、源码、运行参数、用户密钥库、Go 接口、数据库与文件存储访问、编译发布。为 Web 开发后端时,接收 `tiana-web` 技能确定的 Git ID、接口需求和数据依赖;完成后返回服务 ID、source_dir、发布回执、接口契约和参数生效结果,由 Web skill 继续前端接入、绑定、发布及分享验收。
15
15
 
16
16
  安装、账号与环境选择按 `tiana` 技能。共享各 Tiana 技能本会话的实际更新检查记录;尚未检查时先完成该入口的检查,包括从本会话实际加载的 SKILL.md 的 `metadata.version` 取得 `SKILLS_VERSION`,在同一次 shell 调用中赋值并实际执行 `tiana version check --skills-version "$SKILLS_VERSION" --json` 并按结果自动更新 CLI 和整套 Skills、补齐新增技能并核验,在对话中报告结果与重载要求;`should_notify=false` 也执行。两个包各自比较自身 npm 最新版,检查与安装无需 Tiana 登录;暂不登录或不操作云端资源时仍完成此流程。执行前确认 `tiana function --help` 可用;当前最新正式 CLI 仍缺少命令时,报告配套能力尚未可用及两个包的实际版本,等待已验证的配套发布。
17
17
 
@@ -29,10 +29,16 @@ metadata:
29
29
 
30
30
  数据库 Endpoint 使用明文参数;Token 和第三方密钥先按[用户密钥库](references/secrets.md)写入密钥,再保存为后端服务的密钥引用参数,不写入 Git、前端源码、业务响应或日志。缺少实际凭据时说明待配置项,不使用占位值宣称联调成功。Web 的数据访问方式、资源绑定与模板制作由 Tiana Web 负责。
31
31
 
32
+ 后端服务需要访问 SQLite 时,必须先完成 [SQLite 访问配置](references/sqlite-backend.md):从实例取得 Endpoint、用 CLI 创建 Instance Token、写入密钥库并配置参数,再发布函数和验证真实查询。执行前通过 `tiana token create --help` 核对配套 CLI 能力。
33
+
32
34
  调用方身份通过 SDK 从请求上下文读取,业务需要区分用户数据时使用该身份,不能相信请求体中自报的用户 ID。完成接口核验后,将所需调用身份与数据范围一并交给 Web skill 验证。
33
35
 
34
36
  App 作者专属的初始化和权限管理接口,按 [App 作者与初始权限](references/go-backend.md#app-作者与初始权限)使用 `caller.IsAppOwner()` 校验;作者指本次来源 Web 的创建者,与租户角色分开。前端的 `is_app_owner` 展示由 Tiana Web 负责。
35
37
 
38
+ ## 文件存储
39
+
40
+ 函数需要上传、读取、列目录、删除文件或返回浏览器文件 URL 时,先阅读[函数文件存储](references/file-storage.md),按函数所属租户选择根目录或 Web 子目录,再接入示例。存储连接与凭据由平台注入;业务参数只保存业务配置。结合可信调用身份实现业务权限,完成真实文件读写及浏览器授权访问验收。
41
+
36
42
  ## WebSocket
37
43
 
38
44
  开发实时连接时,按 [WebSocket 实时连接](references/go-backend.md#websocket-实时连接)完成 Go 服务、浏览器原生连接、源码保存和发布验证,再交给 Web skill 绑定与托管验收。
@@ -0,0 +1,80 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "errors"
8
+ "fmt"
9
+ "net/http"
10
+ "os"
11
+ "time"
12
+
13
+ "github.com/tianacloud/sdk-go/files"
14
+ "github.com/tianacloud/sdk-go/function"
15
+ )
16
+
17
+ func main() {
18
+ http.HandleFunc("/generate", func(w http.ResponseWriter, r *http.Request) {
19
+ caller, ok := function.CallerFromRequest(r)
20
+ if !ok || !caller.IsAppOwner() {
21
+ http.Error(w, "仅 App 作者可执行", http.StatusForbidden)
22
+ return
23
+ }
24
+ if r.Method != http.MethodPost {
25
+ w.Header().Set("Allow", "POST")
26
+ http.Error(w, "需要 POST", http.StatusMethodNotAllowed)
27
+ return
28
+ }
29
+ config, err := function.ReadConfig()
30
+ if err != nil {
31
+ http.Error(w, "读取业务配置失败", http.StatusInternalServerError)
32
+ return
33
+ }
34
+ webID, err := config.GetContext(r.Context(), "REPORT_WEB_ID")
35
+ if err != nil {
36
+ http.Error(w, "请配置 REPORT_WEB_ID", http.StatusInternalServerError)
37
+ return
38
+ }
39
+ result, err := generateReport(webID)
40
+ if err != nil {
41
+ http.Error(w, err.Error(), http.StatusBadGateway)
42
+ return
43
+ }
44
+ w.Header().Set("Content-Type", "application/json")
45
+ _ = json.NewEncoder(w).Encode(result)
46
+ })
47
+ if err := http.ListenAndServe(":"+os.Getenv("PORT"), nil); err != nil {
48
+ panic(err)
49
+ }
50
+ }
51
+
52
+ // generateReport can also be called by a business job without an HTTP request.
53
+ func generateReport(webID string) (map[string]any, error) {
54
+ client, err := function.FilesByWebApp(webID)
55
+ if err != nil {
56
+ return nil, err
57
+ }
58
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
59
+ defer cancel()
60
+ const path = "reports/task.txt"
61
+ content := []byte("generated report\n")
62
+ receipt, err := client.Put(ctx, path, bytes.NewReader(content), files.PutOptions{
63
+ Size: int64(len(content)), ContentType: "text/plain; charset=utf-8", Overwrite: true,
64
+ })
65
+ if err == nil {
66
+ return map[string]any{"state": "written", "receipt": receipt}, nil
67
+ }
68
+ var fileErr *files.Error
69
+ if !errors.As(err, &fileErr) || fileErr.Outcome != "unknown" || fileErr.WriteID == "" {
70
+ return nil, err
71
+ }
72
+ // Inspect the current object once. A failed write is never automatically replayed.
73
+ checkCtx, checkCancel := context.WithTimeout(context.Background(), 5*time.Second)
74
+ defer checkCancel()
75
+ metadata, checkErr := client.Metadata(checkCtx, path)
76
+ if checkErr == nil && metadata.WriteID == fileErr.WriteID && metadata.Size == int64(len(content)) {
77
+ return map[string]any{"state": "current_object_matches", "metadata": metadata}, nil
78
+ }
79
+ return nil, fmt.Errorf("写入结果未确认,请按业务状态处理;write_id=%s delete_confirmed=%t: %w", fileErr.WriteID, fileErr.DeleteConfirmed, err)
80
+ }
@@ -0,0 +1,99 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "errors"
8
+ "fmt"
9
+ "io"
10
+ "net/http"
11
+ "os"
12
+ "time"
13
+
14
+ "github.com/tianacloud/sdk-go/files"
15
+ "github.com/tianacloud/sdk-go/function"
16
+ )
17
+
18
+ func main() {
19
+ http.HandleFunc("/file-demo", func(w http.ResponseWriter, r *http.Request) {
20
+ caller, ok := function.CallerFromRequest(r)
21
+ if !ok || !caller.IsAppOwner() {
22
+ http.Error(w, "仅 App 作者可执行", http.StatusForbidden)
23
+ return
24
+ }
25
+ if r.Method != http.MethodPost {
26
+ w.Header().Set("Allow", "POST")
27
+ http.Error(w, "需要 POST", http.StatusMethodNotAllowed)
28
+ return
29
+ }
30
+ result, err := exercise(function.ContextFromRequest(r))
31
+ if err != nil {
32
+ status := http.StatusBadGateway
33
+ var fileErr *files.Error
34
+ if errors.As(err, &fileErr) {
35
+ switch fileErr.Code {
36
+ case "FILE_EXISTS":
37
+ status = http.StatusConflict
38
+ case "FILE_WRITE_BLOCKED":
39
+ status = http.StatusForbidden
40
+ }
41
+ }
42
+ http.Error(w, err.Error(), status)
43
+ return
44
+ }
45
+ w.Header().Set("Content-Type", "application/json")
46
+ _ = json.NewEncoder(w).Encode(result)
47
+ })
48
+ if err := http.ListenAndServe(":"+os.Getenv("PORT"), nil); err != nil {
49
+ panic(err)
50
+ }
51
+ }
52
+
53
+ func exercise(ctx context.Context) (map[string]any, error) {
54
+ client := function.Files()
55
+ capabilities, err := client.Capabilities(ctx)
56
+ if err != nil {
57
+ return nil, err
58
+ }
59
+ path := fmt.Sprintf("file-demo/%d.txt", time.Now().UnixNano())
60
+ content := []byte("tenant root file\n")
61
+ receipt, err := client.Put(ctx, path, bytes.NewReader(content), files.PutOptions{
62
+ Size: int64(len(content)), ContentType: "text/plain; charset=utf-8",
63
+ })
64
+ if err != nil {
65
+ return nil, err
66
+ }
67
+ metadata, err := client.Metadata(ctx, receipt.Path)
68
+ if err != nil {
69
+ return nil, err
70
+ }
71
+ listed := 0
72
+ for cursor := ""; ; {
73
+ page, err := client.List(ctx, "file-demo/", files.ListOptions{Cursor: cursor})
74
+ if err != nil {
75
+ return nil, err
76
+ }
77
+ listed += len(page.Entries)
78
+ if page.NextCursor == "" {
79
+ break
80
+ }
81
+ cursor = page.NextCursor
82
+ }
83
+ response, err := client.Get(ctx, receipt.Path, files.GetOptions{})
84
+ if err != nil {
85
+ return nil, err
86
+ }
87
+ read, readErr := io.Copy(io.Discard, response.Body)
88
+ closeErr := response.Body.Close()
89
+ if err := errors.Join(readErr, closeErr); err != nil {
90
+ return nil, err
91
+ }
92
+ if err := client.Delete(ctx, receipt.Path); err != nil {
93
+ return nil, err
94
+ }
95
+ return map[string]any{
96
+ "capabilities": capabilities, "written": receipt, "metadata": metadata,
97
+ "listed": listed, "read_bytes": read, "deleted": true,
98
+ }, nil
99
+ }
@@ -45,7 +45,9 @@ tiana function logs FUNCTION_ID --build SUBMISSION_ID --json
45
45
 
46
46
  先列出代码实际需要的参数名和用途,例如 `SQLITE_ENDPOINT`(数据库连接地址)、`SQLITE_TOKEN`(该数据库实例 Token)、`PAYMENT_API_KEY`(第三方接口密钥)。使用已核实的目标资源取得实际值;缺少第三方密钥时请用户在 Console 配置,不要求在对话中粘贴。
47
47
 
48
- Console 操作:打开目标后端服务 →「参数配置」→「参数列表」,分别填写参数名和值并保存。参数名须与代码 Get 的名称一致。当前只支持 `kind=plain`;敏感值也通过此入口配置,不能将它描述为已支持密钥引用管理。
48
+ Console 操作:打开目标后端服务 →「参数配置」→「参数列表」,选择明文或密钥引用,填写参数名和值并保存。参数名须与代码 Get 的名称一致;密钥引用填写密钥库中的 key。
49
+
50
+ 为 SQLite 准备 Endpoint 和实例 Token 时,先按 [SQLite 访问配置](sqlite-backend.md)完成凭据交付,再使用本节更新参数。
49
51
 
50
52
  CLI 操作:
51
53
 
@@ -0,0 +1,160 @@
1
+ # 函数文件存储
2
+
3
+ 使用 Go SDK `v1.2.0`。在函数源码目录固定依赖:
4
+
5
+ ```sh
6
+ go get github.com/tianacloud/sdk-go@v1.2.0
7
+ ```
8
+
9
+ SDK 至少需要 Go 1.25;平台 Go 1.26 工具链满足要求。服务端部署验收与正式 SDK 发布完成后,本技能随配套版本发布。
10
+
11
+ ## 选择目录与权限
12
+
13
+ 导入 `github.com/tianacloud/sdk-go/function` 与 `github.com/tianacloud/sdk-go/files`。
14
+
15
+ | 构造入口 | 当前根目录 |
16
+ | --- | --- |
17
+ | `function.Files()` | 函数所属租户的 files 根 |
18
+ | `function.FilesByWebApp(webID)` | 同一租户根下的 `<webID>/` |
19
+ | `function.FilesFromRequest(r)` | 从 Gateway 注入的 `Caller.WebID` 选择 Web 子目录;无来源 Web 返回 `files.ErrNoWebContext` |
20
+
21
+ 构造不发请求。`FilesByWebApp` 和 `FilesFromRequest` 返回 `(*files.Client, error)`;`Files` 直接返回客户端。平台已配置 OSS,函数代码无需账号密钥、bucket、endpoint 或租户根路径。
22
+
23
+ 客户端始终属于**函数资源所属租户**,与调用用户的租户无关。填写另一租户的 Web ID 只会选择本函数租户里的同名子目录。共享函数要访问特定 Web,使用 `FilesByWebApp`;使用可信来源 Web 时选 `FilesFromRequest`。同一租户内的 Web 子目录是目录选择,业务访问权限由 handler 根据 `CallerFromRequest` 判断。不要把请求体里的租户、用户或 Web ID 当作授权依据。
24
+
25
+ 所有方法接收未进行 URL 编码的相对路径,例如 `reports/账单.csv`;列表、元数据和写入回执中的 Path 也相对当前客户端根。路径不能以 `/` 开头,目录段不能为 `.` 或 `..`。`List(ctx, "", ...)` 列当前根;列表分页用返回的 `NextCursor` 原样交回同一个客户端和目录,直到为空。
26
+
27
+ ## 无入站请求时显式选择 Web
28
+
29
+ 可直接编译的 [显式 Web 示例](../assets/file-storage/explicit-web/main.go) 提供 `POST /generate`,由 App 作者调用;先在函数业务参数中设置 `REPORT_WEB_ID`。其中 `generateReport(webID)` 不依赖 HTTP request,创建自有 context,写入后遇到未知结果只核对当前 Metadata,不重放写入。同一对象从租户客户端访问的路径是 `<webID>/reports/task.txt`。如果把该方法用于异步业务任务,沿函数技能的 jobs 用法管理任务生命周期。
30
+
31
+ 例如业务已有确定的 Web ID,创建自有 context 后查询;client 的构造不需要 HTTP request:
32
+
33
+ ```go
34
+ func reportMetadata(ctx context.Context, webID string) (files.Metadata, error) {
35
+ client, err := function.FilesByWebApp(webID)
36
+ if err != nil { return files.Metadata{}, err }
37
+ return client.Metadata(ctx, "reports/latest.txt")
38
+ }
39
+ ```
40
+
41
+ 补充导入 `context`。`function.Files().Metadata(ctx, webID+"/reports/latest.txt")` 指向同一个对象,返回 Path 包含 Web 前缀;显式 Web 客户端返回的 Path 是 `reports/latest.txt`。两种 GetURL 分别使用租户根或 Web 文件入口授权范围,前端必须授权对应范围。
42
+
43
+ ## 上传并返回浏览器地址
44
+
45
+ 下面的完整 HTTP 程序允许来源 Web 的作者生成固定报表,并允许该 Web 的已登录用户下载。实际应用根据自己的业务模型决定读写权限、文件名和内容。
46
+
47
+ ```go
48
+ package main
49
+
50
+ import (
51
+ "bytes"
52
+ "encoding/json"
53
+ "io"
54
+ "net/http"
55
+ "os"
56
+
57
+ "github.com/tianacloud/sdk-go/files"
58
+ "github.com/tianacloud/sdk-go/function"
59
+ )
60
+
61
+ func main() {
62
+ http.HandleFunc("/report", report)
63
+ if err := http.ListenAndServe(":"+os.Getenv("PORT"), nil); err != nil {
64
+ panic(err)
65
+ }
66
+ }
67
+
68
+ func report(w http.ResponseWriter, r *http.Request) {
69
+ caller, present := function.CallerFromRequest(r)
70
+ if !present || caller.UserID == "" {
71
+ http.Error(w, "请登录", http.StatusUnauthorized)
72
+ return
73
+ }
74
+ client, err := function.FilesFromRequest(r)
75
+ if err != nil {
76
+ http.Error(w, "需要从 Web 应用访问", http.StatusBadRequest)
77
+ return
78
+ }
79
+ ctx := function.ContextFromRequest(r)
80
+ const path = "reports/latest.txt"
81
+ switch r.Method {
82
+ case http.MethodPost:
83
+ if !caller.IsAppOwner() {
84
+ http.Error(w, "仅 App 作者可生成报表", http.StatusForbidden)
85
+ return
86
+ }
87
+ content := []byte("本次报表内容\n")
88
+ receipt, err := client.Put(ctx, path, bytes.NewReader(content), files.PutOptions{
89
+ Size: int64(len(content)), ContentType: "text/plain; charset=utf-8", Overwrite: true,
90
+ })
91
+ if err != nil {
92
+ http.Error(w, "报表写入未确认,请查询文件状态", http.StatusBadGateway)
93
+ return
94
+ }
95
+ publicURL, err := client.GetURL(ctx, receipt.Path)
96
+ if err != nil {
97
+ http.Error(w, "文件已写入,获取访问地址失败", http.StatusBadGateway)
98
+ return
99
+ }
100
+ w.Header().Set("Content-Type", "application/json")
101
+ _ = json.NewEncoder(w).Encode(map[string]any{"path": receipt.Path, "url": publicURL, "write_id": receipt.WriteID})
102
+ case http.MethodGet:
103
+ response, err := client.Get(ctx, path, files.GetOptions{Range: r.Header.Get("Range")})
104
+ if err != nil {
105
+ http.Error(w, "读取报表失败", http.StatusBadGateway)
106
+ return
107
+ }
108
+ defer response.Body.Close()
109
+ for _, key := range []string{"Content-Type", "Content-Length", "Content-Range", "Accept-Ranges", "ETag"} {
110
+ if value := response.Header.Get(key); value != "" { w.Header().Set(key, value) }
111
+ }
112
+ w.WriteHeader(response.StatusCode)
113
+ if _, err := io.Copy(w, response.Body); err != nil { return }
114
+ default:
115
+ w.Header().Set("Allow", "GET, POST")
116
+ http.Error(w, "不支持的方法", http.StatusMethodNotAllowed)
117
+ }
118
+ }
119
+ ```
120
+
121
+ `PutOptions.Size` 是准确字节数,包含零字节;最大单文件 5 GiB。请求体已有可靠 ContentLength 时可直接将 `r.Body` 流式传给 Put;未知长度先在业务允许的大小内准备临时内容并确定长度。不要为流式接口无界读取整个上传体。默认同名冲突,显式 `Overwrite: true` 会先删除再上传,操作不是原子的,失败后旧文件可能已经删除。
122
+
123
+ `GetURL` 返回当前环境的完整公网文件入口 URL,和前端 SDK 一样仍需要浏览器自身的文件授权;它不是免授权下载链接,也不安装 Cookie。把此 URL 交给已经通过前端文件 SDK 完成授权的页面。函数自己的 Get 直接返回文件流,调用方负责 Close;可用 `GetOptions{Method: http.MethodHead}` 或 Range,传入的 context 可取消请求。
124
+
125
+ ## 查询、分页和删除
126
+
127
+ 可直接编译的 [租户根示例](../assets/file-storage/tenant-root/main.go) 提供 `POST /file-demo`,由 App 作者调用。它创建唯一的演示对象,读取 Capabilities、上传、查询元数据、遍历演示目录的所有页、流式读取并关闭 Body,最后删除刚创建的对象。请求失败时会返回错误而不自动重放;若演示中断,用返回的写入状态或目录查询处理遗留对象。
128
+
129
+ 两个源码示例分别作为 `main.go` 放到独立目录,执行 `go mod init example.com/file-demo`,按本技能的配套 SDK 版本执行 `go get`,再运行 `go mod tidy && go build .`;保存生成的 go.mod/go.sum。平台 Go 1.26 工具链满足 SDK 的 Go 1.25 最低要求。它们与上面的来源 Web 程序分别发布为独立函数入口;通过 CLI/Console 取得真实调用入口,不把 `supervisor.tiana.com` 当作客户端公网地址。
130
+
131
+ 下面片段在已有 `ctx`、`client` 的业务方法中使用,错误交给该方法的返回值处理:
132
+
133
+ ```go
134
+ for cursor := ""; ; {
135
+ page, err := client.List(ctx, "reports/", files.ListOptions{Cursor: cursor})
136
+ if err != nil { return err }
137
+ for _, entry := range page.Entries {
138
+ // 使用 entry.Path、IsDir、Size、ETag、LastModified 构造业务结果。
139
+ _ = entry
140
+ }
141
+ if page.NextCursor == "" { break }
142
+ cursor = page.NextCursor
143
+ }
144
+ metadata, err := client.Metadata(ctx, "reports/latest.txt")
145
+ if err != nil { return err }
146
+ _ = metadata // Size、ETag、ContentType、LastModified、WriteID。
147
+ capabilities, err := client.Capabilities(ctx)
148
+ if err != nil { return err }
149
+ _ = capabilities // MaxFileBytes、最近已应用的 WriteBlocked。
150
+ // 用户明确要求删除时,在业务权限判断后执行:
151
+ if err := client.Delete(ctx, "reports/latest.txt"); err != nil { return err }
152
+ ```
153
+
154
+ Delete 删除当前对象,不存在也成功。每次 List 计 100 文件 read 消耗;Get、Metadata 不增加该 List 消耗。存储字节异步核对,额度状态存在传播窗口;`WriteBlocked` 不是实时剩余容量。禁止写入时 Put/Overwrite 失败,文件 API 的读、列目录、删除仍可用;Gateway 的整租户准入可能阻止新的函数业务调用,释放空间也可使用 Console/CLI 文件管理。
155
+
156
+ ## 写入结果与验收
157
+
158
+ 用 `errors.As(err, &fileErr)` 读取 `*files.Error` 的 Code、Stage、Outcome、DeleteConfirmed、WriteID 和 RequestID。`Outcome == "unknown"` 表示结果未确认;不要自动重放 Put 或 Overwrite。用 Metadata 核对当前对象的 WriteID、大小及业务内容;WriteID 只是写入标识,不提供幂等或历史版本保证。请求取消也可能发生在 OSS 已接收写入之后。
159
+
160
+ 发布前编译示例并保存真实 go.mod/go.sum。发布后在目标函数验证一次上传、Metadata、流式/Range 读取、分页和删除;验证来源 Web、明确选择 Web 及无 Web 上下文的行为。需要交付公网 URL 时,继续在对应已登录 Web 中验证前端文件授权与访问。平台未注入文件配置时会返回 `FILE_NOT_CONFIGURED`,报告服务配置缺口,不在函数源码中填入 OSS 凭据。
@@ -3,7 +3,7 @@
3
3
  后端服务是监听 `PORT` 环境变量指定端口的 Go HTTP 程序。平台固定 Go 1.26 工具链。source_dir 包含 package main,所属 go.mod 可在该目录或仓库内上级目录,多个入口可共享模块;依赖由构建流程下载。本页的 Get(value, error) 与密钥引用示例使用下列配套 SDK;将解析后的 go.mod、go.sum 一起保存到源码仓库。
4
4
 
5
5
  ```sh
6
- go get github.com/tianacloud/sdk-go@v1.1.0-function.4.0.20261102183840-52d162f75066
6
+ go get github.com/tianacloud/sdk-go@v1.2.0
7
7
  ```
8
8
 
9
9
  最小程序返回 JSON:
@@ -68,7 +68,7 @@ req.Header.Set("Authorization", "Bearer "+apiKey)
68
68
 
69
69
  ## 访问 SQLite
70
70
 
71
- 先用 `tiana-sqlite` 技能核实数据库实例、表结构及初始化 SQL。数据库 Endpoint 和实例 Token 分别保存为后端服务参数 `SQLITE_ENDPOINT`、`SQLITE_TOKEN`,Token 在该数据库的连接管理中创建。只在后端服务内读取并传给 SDK:
71
+ 先用 `tiana-sqlite` 技能核实数据库实例、表结构及初始化 SQL,再按 [SQLite 访问配置](sqlite-backend.md)通过 CLI 创建实例 Token 并保存参数。`SQLITE_ENDPOINT` 使用 `sqlite show --json` 返回的 `data.connection.hostname`,不含 URL scheme 或端口。连接端口沿用 SDK 默认值。只在后端服务内读取并传给 SDK:
72
72
 
73
73
  ```go
74
74
  endpoint, endpointErr := params.Get("SQLITE_ENDPOINT")
@@ -96,11 +96,15 @@ defer db.Close()
96
96
 
97
97
  对应导入为 `database/sql`、`time`、`tiana "github.com/tianacloud/sdk-go"`、`tianasqlite "github.com/tianacloud/sdk-go-sqlite"`。OpenDB 创建句柄并不证明数据库已连接,继续执行实际查询并检查错误。后续 SQL 使用 `r.Context()` 与占位符绑定业务值。对需要按用户隔离的数据,从 SDK 读取可信请求身份并在查询条件中限定所属用户;不要提供任意 SQL 执行接口。
98
98
 
99
- 沙箱内包含公共 CA 包,正式环境公共证书通常无需业务配置 RootCAs。本地环境私有 CA 按该环境的测试指导提供,不关闭证书验证。只有 tmpfs 用于临时写入,持久业务数据保存到 SQLite 或平台文件存储。
99
+ 沙箱内包含公共 CA 包,SDK 默认使用系统信任。只有 tmpfs 用于临时写入,持久业务数据保存到 SQLite 或平台文件存储。
100
+
101
+ ## 访问文件存储
102
+
103
+ 需要持久保存业务文件、流式下载或生成前端可用文件 URL 时,按[函数文件存储](file-storage.md)使用 `function.Files()`、`FilesByWebApp` 或 `FilesFromRequest`。文件连接由平台配置,业务代码使用相对路径。
100
104
 
101
105
  ## App 作者与初始权限
102
106
 
103
- 在需要作者权限的 HTTP handler 内使用 Go SDK `v1.1.0-function.4.0.20261102183840-52d162f75066`(包含参数 Get 错误语义和密钥引用),并将实际依赖固定到 go.mod:
107
+ 在需要作者权限的 HTTP handler 内使用 Go SDK `v1.2.0`(包含参数 Get 错误语义和密钥引用),并将实际依赖固定到 go.mod:
104
108
 
105
109
  ```go
106
110
  caller, present := function.CallerFromRequest(r)
@@ -0,0 +1,55 @@
1
+ # SQLite 访问配置
2
+
3
+ 本流程需要提供 `tiana token create` 的配套 CLI。先执行 `tiana token create --help`;缺少命令时说明配套能力尚未可用。沿用用户已选定的登录。
4
+
5
+ ## 准备资源
6
+
7
+ 1. 核实 SQLite 实例和目标分支;缺少数据库才创建。用 `sqlite shell` 准备业务表与少量可核验数据。
8
+ 2. 创建或复用后端服务,记录完整 `FUNCTION_ID`。创建、源码目录和发布按 [后端服务命令](commands.md)。
9
+ 3. 执行 `tiana secrets init --json`。确认 `data.state=ACTIVE` 后继续;若仍 accepted,保留原实例/任务,稍后再次 init 查看同一密钥库的状态。
10
+
11
+ ## 创建凭据并配置
12
+
13
+ 下例为 POSIX shell 和 Python 3。先将 `DB_ID`、`FUNCTION_ID` 设为已确认的完整实例 ID;需要非默认分支时,在 sqlite show 中增加 `--branch NAME`。`secret_key` 为本函数独立的 key,已有函数的更新只修改本次数据库参数。
14
+
15
+ ```sh
16
+ set -e
17
+ umask 077
18
+ work_dir=$(mktemp -d)
19
+ secret_key="functions/$FUNCTION_ID/sqlite-token"
20
+ tiana sqlite show "$DB_ID" --json > "$work_dir/database.json"
21
+ tiana token create "$DB_ID" --name "$FUNCTION_ID SQLite" --json > "$work_dir/token.json"
22
+ python3 - "$work_dir/token.json" "$secret_key" <<'PY'
23
+ import json, subprocess, sys
24
+ with open(sys.argv[1]) as f:
25
+ receipt = json.load(f)
26
+ assert receipt['status'] == 'succeeded' and receipt['data']['token']
27
+ subprocess.run(['tiana', 'secrets', 'set', sys.argv[2], '--stdin', '--json'],
28
+ input=receipt['data']['token'], text=True, check=True)
29
+ PY
30
+ tiana function parameters get "$FUNCTION_ID" --json > "$work_dir/current.json"
31
+ python3 - "$work_dir" "$secret_key" <<'PY'
32
+ import json, pathlib, sys
33
+ root = pathlib.Path(sys.argv[1])
34
+ connection = json.loads((root / 'database.json').read_text())['data']['connection']
35
+ parameters = json.loads((root / 'current.json').read_text())['data']['parameters']
36
+ parameters['SQLITE_ENDPOINT'] = {'kind': 'plain', 'value': connection['hostname']}
37
+ parameters['SQLITE_TOKEN'] = {'kind': 'secret', 'value': sys.argv[2]}
38
+ (root / 'parameters.json').write_text(json.dumps(parameters))
39
+ PY
40
+ tiana function parameters set "$FUNCTION_ID" --file "$work_dir/parameters.json" --json > "$work_dir/applied.json"
41
+ python3 - "$work_dir/applied.json" <<'PY'
42
+ import json, sys
43
+ with open(sys.argv[1]) as f:
44
+ result = json.load(f)
45
+ print('Parameter application:', result['data']['application'])
46
+ PY
47
+ ```
48
+
49
+ `token create` 默认不过期,业务有到期要求时增加 `--expires-at UNIX_SECONDS`。退出码非零时停止后续步骤;保留 token ID、request ID 和错误,不因重跑整段脚本签发另一枚 Token。未交付明文或响应未知时先在 Console 查看原 Token,必要时撤销后明确创建替代凭据。密钥写入失败而原回执仍在时,从原回执恢复写入。
50
+
51
+ 参数 set 替换整个对象,上例保留其余参数。`applied` 或 `next_start` 后执行真实函数请求;`pending` 先核对配置下发状态。保留受限临时目录用于未完成步骤的恢复;凭据写入、参数保存及验证成功后删除本次临时目录,源码和交付记录只保留资源身份、密钥 key 和结果。
52
+
53
+ ## 使用参数
54
+
55
+ 按 [Go 后端服务开发](go-backend.md#访问-sqlite)读取 `SQLITE_ENDPOINT` 和 `SQLITE_TOKEN` 并连接数据库。Endpoint 使用主机名,端口沿用 SDK 默认值。完成源码发布后,通过应用的业务接口执行实际查询,确认返回预期数据;仅创建数据库句柄或编译成功不能确认连接成功。
@@ -4,7 +4,7 @@ description: 使用 Tiana Git 管理 Git 实例和原生仓库,关联项目远
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Tiana Cloud
7
- version: "1.0.17"
7
+ version: "1.0.18"
8
8
  parent: tiana
9
9
  source: https://github.com/tianacloud/agent-skills/tree/main/skills/tiana-git
10
10
  ---
@@ -4,7 +4,7 @@ description: 查询 Tiana SQLite 表结构、执行 SQL、选择数据库分支
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Tiana Cloud
7
- version: "1.0.17"
7
+ version: "1.0.18"
8
8
  parent: tiana
9
9
  source: https://github.com/tianacloud/agent-skills/tree/main/skills/tiana-sqlite
10
10
  ---
@@ -44,6 +44,8 @@ CLI 从保存的账号登录态取得连接凭据。缺少或过期时按 `tiana
44
44
 
45
45
  ## 应用连接
46
46
 
47
+ Go 后端服务访问 SQLite 时,转到 `tiana-function` 的“SQLite 访问配置”,通过 CLI 创建 Instance Token、保存密钥引用和 Endpoint,再验证实际 SQL。浏览器直连按下文流程使用账号鉴权。
48
+
47
49
  JavaScript 应用阅读 [JS SDK 与 SQL 协议](references/js-sdk.md),使用参数化 SQL。应用必须使用支持 `auth` 的 SDK 和 Bootstrap 的 `window.tiana.auth`,通过 `connection()` 获取连接元信息;不传静态 Token、不保留旧版回退。能力缺失时报告未就绪,不把 Token 写进源码。终端连接见 [CLI 连接](references/cli.md),Rust 客户端见 [Rust SDK](references/rust-sdk.md)。
48
50
 
49
51
  ## 数据库分支
@@ -4,7 +4,7 @@ description: 使用 Tiana 创建、修改、预览和发布 Web 应用。用户
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Tiana Cloud
7
- version: "1.0.17"
7
+ version: "1.0.18"
8
8
  parent: tiana
9
9
  source: https://github.com/tianacloud/agent-skills/tree/main/skills/tiana-web
10
10
  ---