@tianacloud/agent-skills 1.0.16 → 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 +2 -2
- package/package.json +3 -2
- package/plugin.json +1 -1
- package/skills/tiana/SKILL.md +1 -1
- package/skills/tiana/references/getting-started.md +1 -1
- package/skills/tiana/references/instances-and-tokens.md +2 -0
- package/skills/tiana-function/SKILL.md +13 -3
- package/skills/tiana-function/assets/file-storage/explicit-web/main.go +80 -0
- package/skills/tiana-function/assets/file-storage/tenant-root/main.go +99 -0
- package/skills/tiana-function/references/commands.md +3 -1
- package/skills/tiana-function/references/file-storage.md +160 -0
- package/skills/tiana-function/references/go-backend.md +161 -4
- package/skills/tiana-function/references/sqlite-backend.md +55 -0
- package/skills/tiana-git/SKILL.md +1 -1
- package/skills/tiana-sqlite/SKILL.md +3 -1
- package/skills/tiana-web/SKILL.md +1 -1
- package/skills/tiana-web/references/files.md +1 -1
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ WorkBuddy、豆包按所用版本的目录/ZIP 导入方式加载这五个目
|
|
|
38
38
|
|
|
39
39
|
Web 技能每次任务都实际执行 `tiana web list-template --json`,使用当前 name/description 判断需求。模板 ZIP 原样下载解压到独立项目目录;Agent 按根部 metadata.json 替换变量、先 schema 后 seeds。模板源码和制作规范见 [template-apps](https://github.com/tianacloud/template-apps),客户安装模板由 CLI 和 MGR 目录完成。
|
|
40
40
|
|
|
41
|
-
技能安装器只安装技能。Agent 首次执行 Tiana 任务时检查 CLI,缺少时自动执行下面的安装并继续原任务;CLI 要求 1.0.
|
|
41
|
+
技能安装器只安装技能。Agent 首次执行 Tiana 任务时检查 CLI,缺少时自动执行下面的安装并继续原任务;CLI 要求 1.0.17 或更新正式版,需要 Node.js 20+。
|
|
42
42
|
|
|
43
43
|
```sh
|
|
44
44
|
npm install -g @tianacloud/cli@latest --registry=https://registry.npmjs.org/
|
|
@@ -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.
|
|
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": {
|
|
@@ -45,10 +45,11 @@
|
|
|
45
45
|
"validate:plugin": "node scripts/validate-plugin.mjs",
|
|
46
46
|
"validate:versions": "node scripts/validate-versions.mjs",
|
|
47
47
|
"validate:workbuddy": "node scripts/validate-workbuddy.mjs",
|
|
48
|
-
"validate:ci": "npm run validate:skills && npm run validate:references && npm run validate:plugin && npm run validate:versions && npm run validate:workbuddy && npm run validate:connector && npm run test:doubao-auth",
|
|
48
|
+
"validate:ci": "npm run validate:skills && npm run validate:references && npm run validate:plugin && npm run validate:versions && npm run validate:workbuddy && npm run validate:connector && npm run test:doubao-auth && npm run test:packaging",
|
|
49
49
|
"test:templates": "python3 -B -m unittest discover -s tests -p test_share_template_pack.py",
|
|
50
50
|
"test:installer": "node --test scripts/installer.test.mjs",
|
|
51
51
|
"test:release": "node --test scripts/release.test.mjs",
|
|
52
|
+
"test:packaging": "python3 -B scripts/doubao-packaging.test.py && python3 -B scripts/qwen-packaging.test.py",
|
|
52
53
|
"build:doubao": "python3 scripts/build-doubao.py"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
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.
|
|
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",
|
package/skills/tiana/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 开始使用
|
|
2
2
|
|
|
3
|
-
在 Agent 实际执行命令的环境检查 `tiana version`。本技能要求 CLI 1.0.
|
|
3
|
+
在 Agent 实际执行命令的环境检查 `tiana version`。本技能要求 CLI 1.0.17 或更新正式版,具备分步登录、sqlite、git、web、file、list-template/init-template 和 version check。CLI 不存在、版本过低或无法核实时,直接执行下面的 npm latest 安装并重新核验,无需再次询问;核验通过后再执行资源操作。
|
|
4
4
|
|
|
5
5
|
连接器环境中,安装或更新前先按[宿主说明](connector-updates.md)定位实际 Node/npm、CLI 安装 prefix 和技能根目录,沿用宿主运行环境。
|
|
6
6
|
|
|
@@ -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
|
|
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.
|
|
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
|
|
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,20 @@ 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
|
+
|
|
42
|
+
## WebSocket
|
|
43
|
+
|
|
44
|
+
开发实时连接时,按 [WebSocket 实时连接](references/go-backend.md#websocket-实时连接)完成 Go 服务、浏览器原生连接、源码保存和发布验证,再交给 Web skill 绑定与托管验收。
|
|
45
|
+
|
|
36
46
|
## 用户密钥库
|
|
37
47
|
|
|
38
48
|
初始化密钥库、查询名称与版本、写入或删除密钥时,阅读[密钥管理](references/secrets.md)。按操作回执报告 key、版本和结果;需要供函数使用时,继续配置密钥引用参数并核验生效。
|
|
@@ -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 操作:打开目标后端服务
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
107
|
+
在需要作者权限的 HTTP handler 内使用 Go SDK `v1.2.0`(包含参数 Get 错误语义和密钥引用),并将实际依赖固定到 go.mod:
|
|
104
108
|
|
|
105
109
|
```go
|
|
106
110
|
caller, present := function.CallerFromRequest(r)
|
|
@@ -118,3 +122,156 @@ if !present || !caller.IsAppOwner() {
|
|
|
118
122
|
在启动后台 goroutine 前调用 `job, err := function.StartJob(jobID)`,成功后才启动任务,任务结束调用 `job.Finish()` 并处理错误。不同任务使用不同 ID;活跃期间相同 ID 的重复 Start 幂等,Finish 可重试,Finish 后复用 ID 表示新任务。已登记 Job 会阻止自动休眠;重启会清空登记,强制休眠和故障仍可中断任务。需要恢复的进度写入持久存储。
|
|
119
123
|
|
|
120
124
|
本地参数文件采用 `{"MESSAGE":{"kind":"plain","value":"欢迎使用订单服务"}}`,设置 `FUNCTION_CONFIG_FILE` 指向它即可使用 SDK,不需解密密钥。平台提供的加密参数由 SDK 解密,业务继续使用 Get;读取错误应返回业务错误,不泄露参数内容。
|
|
125
|
+
|
|
126
|
+
## WebSocket 实时连接
|
|
127
|
+
|
|
128
|
+
先在 Web 应用概览的「允许调用的后端服务」中添加并保存目标服务,再编写页面。托管 Web 和 CLI 本地预览都提供 `window.tiana.functions.websocket(instanceId, path, {protocols?, signal?})`,返回在连接打开后兑现的 `Promise<WebSocket>`;普通 HTTP 继续使用 `functions.fetch`。
|
|
129
|
+
|
|
130
|
+
### Go 服务:HTTP 与文本/二进制回声
|
|
131
|
+
|
|
132
|
+
将下面完整程序保存为 `main.go`。它监听平台提供的 `PORT`,普通 HTTP 路由是 `/health`,WebSocket 路由是 `/ws`,握手及帧处理由标准 WebSocket 库负责。
|
|
133
|
+
|
|
134
|
+
```go
|
|
135
|
+
package main
|
|
136
|
+
|
|
137
|
+
import (
|
|
138
|
+
"log"
|
|
139
|
+
"net/http"
|
|
140
|
+
"os"
|
|
141
|
+
|
|
142
|
+
"github.com/gorilla/websocket"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
func main() {
|
|
146
|
+
mux := http.NewServeMux()
|
|
147
|
+
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
148
|
+
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
149
|
+
_, _ = w.Write([]byte("ok"))
|
|
150
|
+
})
|
|
151
|
+
upgrader := websocket.Upgrader{
|
|
152
|
+
CheckOrigin: func(r *http.Request) bool { return true },
|
|
153
|
+
}
|
|
154
|
+
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
|
155
|
+
conn, err := upgrader.Upgrade(w, r, nil)
|
|
156
|
+
if err != nil {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
defer conn.Close()
|
|
160
|
+
for {
|
|
161
|
+
kind, data, err := conn.ReadMessage()
|
|
162
|
+
if err != nil {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
if err := conn.WriteMessage(kind, data); err != nil {
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
log.Fatal(http.ListenAndServe(":"+os.Getenv("PORT"), mux))
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
示例的 `CheckOrigin` 显式允许跨来源页面,便于托管 Web 调用;业务需要限制来源时在自己的 handler 中实现 Origin 策略。普通 HTTP CORS 配置与 WebSocket Origin 校验独立。需要用户身份时,在 Upgrade 前通过 `function.CallerFromRequest(r)` 读取,并将本次调用者保存在该连接的局部状态中;Go SDK 的导入和版本沿用本页配套 SDK;从 `function.CallerFromRequest(r)` 获取身份后,在 Upgrade 前完成业务判断。
|
|
175
|
+
|
|
176
|
+
在仅含此示例的新目录中执行:
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
go mod init example.com/function-echo
|
|
180
|
+
go get github.com/gorilla/websocket@v1.5.3
|
|
181
|
+
go mod tidy
|
|
182
|
+
go build .
|
|
183
|
+
PORT=8080 go run .
|
|
184
|
+
# 另一终端验证普通 HTTP:
|
|
185
|
+
curl http://127.0.0.1:8080/health
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
保存 `main.go`、`go.mod`、`go.sum`,再编译发布:
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
tiana function create "实时回声" --source-dir /services/echo --wait --json
|
|
192
|
+
tiana function source list FUNCTION_ID --json
|
|
193
|
+
tiana function source save FUNCTION_ID --base-commit COMMIT --file main.go=./main.go --file go.mod=./go.mod --file go.sum=./go.sum --json
|
|
194
|
+
tiana function publish FUNCTION_ID --commit SAVED_COMMIT --wait --json
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
将 `FUNCTION_ID` 替换为创建回执 ID,`COMMIT` 使用 source list 返回的提交;完全空仓库首次保存才使用 `empty`。`SAVED_COMMIT` 使用 save 返回的新提交。已有 Web 共仓时,create 增加 `--git-instance-id GIT_ID`,保留该仓库其他文件。
|
|
198
|
+
|
|
199
|
+
### 浏览器完整入口模块
|
|
200
|
+
|
|
201
|
+
将下面保存为 Web 的 `app.js`,替换服务 ID。按钮提供连接、取消尚未完成的握手、正常关闭和手动重连;页面直接显示错误与回声结果。
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
const functionId = "<后端服务实例 ID>";
|
|
205
|
+
const panel = document.createElement("section");
|
|
206
|
+
panel.innerHTML = `<button id="connect">连接 / 重新连接</button>
|
|
207
|
+
<button id="cancel">取消连接</button><button id="close">关闭</button>
|
|
208
|
+
<pre role="status"></pre>`;
|
|
209
|
+
document.body.append(panel);
|
|
210
|
+
const status = panel.querySelector("pre");
|
|
211
|
+
let socket;
|
|
212
|
+
let pending;
|
|
213
|
+
function show(message) { status.textContent = message; }
|
|
214
|
+
panel.querySelector("#connect").onclick = async () => {
|
|
215
|
+
if (pending || (socket && socket.readyState < WebSocket.CLOSING)) return;
|
|
216
|
+
const controller = new AbortController();
|
|
217
|
+
pending = controller;
|
|
218
|
+
show("正在连接…");
|
|
219
|
+
try {
|
|
220
|
+
const ws = await window.tiana.functions.websocket(functionId, "/ws", {
|
|
221
|
+
signal: controller.signal,
|
|
222
|
+
});
|
|
223
|
+
socket = ws; // Promise 在 open 后返回,此时注册 message/error/close。
|
|
224
|
+
ws.binaryType = "arraybuffer";
|
|
225
|
+
ws.addEventListener("message", (event) => {
|
|
226
|
+
show(typeof event.data === "string"
|
|
227
|
+
? `文本回声:${event.data}` : `二进制回声:${event.data.byteLength} bytes`);
|
|
228
|
+
});
|
|
229
|
+
ws.addEventListener("error", () => show("连接发生错误,请检查服务后手动重连"));
|
|
230
|
+
ws.addEventListener("close", (event) => {
|
|
231
|
+
show(`连接已关闭:code=${event.code},clean=${event.wasClean}`);
|
|
232
|
+
if (socket === ws) socket = undefined;
|
|
233
|
+
});
|
|
234
|
+
show("已连接,正在发送文本和二进制消息");
|
|
235
|
+
ws.send("hello");
|
|
236
|
+
ws.send(new Uint8Array([1, 2, 3]));
|
|
237
|
+
} catch (error) {
|
|
238
|
+
show(error?.name === "AbortError" ? "已取消连接" : "连接失败,请检查应用授权和后端服务");
|
|
239
|
+
} finally {
|
|
240
|
+
if (pending === controller) pending = undefined;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
panel.querySelector("#cancel").onclick = () => pending?.abort();
|
|
244
|
+
panel.querySelector("#close").onclick = () => socket?.close(1000, "done");
|
|
245
|
+
window.addEventListener("pagehide", () => {
|
|
246
|
+
pending?.abort();
|
|
247
|
+
socket?.close(1000, "page exit");
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
平台通过现有 Site 授权提供短期凭据,以 `access_token` 查询参数用于握手;页面只保存服务 ID,不管理 Token,也不要记录 `socket.url`。浏览器握手失败通常只给通用错误,无法据此确定准确的 HTTP 原因。`signal` 只取消待完成的握手;连接打开后使用原生 `close()`。SDK 客户端 dispose、页面退出或检测到会话失效会关闭其连接;应用自己决定何时重新连接,不自动重连或重放消息。
|
|
252
|
+
|
|
253
|
+
示例没有申请子协议,成功连接的 `socket.protocol` 可以为空。设置 `protocols` 时,业务服务端应从客户端提供的列表中选择并确认一个双方支持的协议。平台透明转发协商,业务双方约定协议。
|
|
254
|
+
|
|
255
|
+
新建 Web 时指定固定入口,并先绑定服务:
|
|
256
|
+
|
|
257
|
+
```sh
|
|
258
|
+
tiana web create "实时回声页面" --entry app.js --json
|
|
259
|
+
tiana web functions get WEB_ID --json
|
|
260
|
+
tiana web functions set WEB_ID --function-id FUNCTION_ID --expected-revision REVISION --json
|
|
261
|
+
# 将 app.js 放在 dist/,已有 Web 使用其创建时固定的入口文件名。
|
|
262
|
+
tiana web serve WEB_ID --dir dist --port 4174
|
|
263
|
+
tiana web publish WEB_ID --dir dist --json
|
|
264
|
+
tiana web status WEB_ID --publish-id PUBLISH_ID --json
|
|
265
|
+
tiana web status WEB_ID --json
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
绑定 set 替换完整列表,保留仍需调用的其他服务;`REVISION` 来自 get。新 Web 初始停用,发布后在 Console Web 概览点击「启用应用」。核对当前内容 `state: running`,并确认 `remote_sha256`、`serving_sha256` 均等于发布回执的目标摘要;上传成功本身不代表正在服务。通过实际托管入口登录,验证文本、二进制回声、关闭 code 1000 和手动重连,同时用 `functions.fetch(FUNCTION_ID, "/health")` 验证普通 HTTP。
|
|
269
|
+
|
|
270
|
+
### 连接与服务生命周期
|
|
271
|
+
|
|
272
|
+
连接建立后没有固定时长上限,并阻止 Function 自动休眠。最后一条连接关闭后,平台结合现有 HTTP 请求、Jobs 和 idleN 判断休眠。连接准入计一次请求,消息不逐条计为请求;连接和普通调用共享并发预算。凭据到期或撤销影响后续新连接,不主动切断已经建立的连接。
|
|
273
|
+
|
|
274
|
+
发布代码在下次唤醒时使用;当前连接继续使用旧执行。Console「强制休眠」会断开连接,应用需自行处理关闭并按业务需要手动重连。
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
源码回执和构建失败恢复见[后端服务命令](commands.md#构建和发布);Web 资源关联与托管验收继续按 `tiana-web` 技能的“后端交接与接入验证”和“预览与发布”流程完成。报告实际验证环境与未完成项,配套平台验证通过后再交付使用。
|
|
@@ -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 SQLite 表结构、执行 SQL、选择数据库分支
|
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
6
|
author: Tiana Cloud
|
|
7
|
-
version: "1.0.
|
|
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
|
## 数据库分支
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# WebApp 文件存储
|
|
2
2
|
|
|
3
|
-
CLI 文件操作要求 1.0.
|
|
3
|
+
CLI 文件操作要求 1.0.17 或更新正式版,使用 `tiana file --help` 核验命令入口。WebApp 文件能力还需目标环境提供配套的 MGR、Web 与 serverless-js Bootstrap;使用前核对目标环境能力。
|
|
4
4
|
|
|
5
5
|
存储分工及静态小图片的 16 KiB 上限见[数据与素材存储](storage-and-assets.md)。
|
|
6
6
|
外置预置素材按[模板素材文件](template-files.md)使用 `template-assets/` 相对路径,
|