@tianacloud/agent-skills 1.0.10 → 1.0.12
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 +9 -8
- package/bin/tiana-skills.mjs +3 -2
- package/package.json +2 -2
- package/plugin.json +2 -2
- package/skills/tiana/SKILL.md +11 -7
- package/skills/tiana/references/connector-updates.md +39 -0
- package/skills/tiana/references/getting-started.md +40 -12
- package/skills/tiana/references/windows-install.md +1 -1
- package/skills/tiana-function/SKILL.md +38 -0
- package/skills/tiana-function/references/commands.md +115 -0
- package/skills/tiana-function/references/go-backend.md +116 -0
- package/skills/tiana-git/SKILL.md +10 -7
- package/skills/tiana-git/references/source-releases.md +5 -5
- package/skills/tiana-sqlite/SKILL.md +9 -6
- package/skills/tiana-web/SKILL.md +32 -13
- package/skills/tiana-web/references/backend-apps.md +40 -0
- package/skills/tiana-web/references/csr-apps.md +56 -4
- package/skills/tiana-web/references/files.md +4 -0
- package/skills/tiana-web/references/function-templates.md +74 -0
- package/skills/tiana-web/references/share-template.md +19 -6
- package/skills/tiana-web/references/storage-and-assets.md +49 -0
- package/skills/tiana-web/references/template-files.md +95 -0
- package/skills/tiana-web/references/template-init.md +13 -2
- package/skills/tiana-web/scripts/pack-template.py +150 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 使用模板工程
|
|
2
2
|
|
|
3
|
-
模板目录来自本次 `tiana web list-template --json` 实际返回的数据。每页 `data.items` 含 name、display_name、description、status 和变量摘要;CLI 的 `--after`
|
|
3
|
+
模板目录来自本次 `tiana web list-template --json` 实际返回的数据。每页 `data.items` 含 name、display_name、description、status 和变量摘要;CLI 的 `--after` 查询下一页。即使 items 为空,存在 next_cursor 时仍继续需要的页。选择符合用户核心功能与数据结构的工程;没有匹配项时按通用 Web 方法制作。
|
|
4
4
|
|
|
5
5
|
新应用默认由 Agent 创建所需的 SQLite 和 Git,按当前应用命名并保存 CLI 返回的实际实例 ID;用户明确指定已有实例时核实后复用。保存资源回执后执行:
|
|
6
6
|
|
|
@@ -10,6 +10,8 @@ tiana web init-template NAME --dir PROJECT_DIR --json
|
|
|
10
10
|
|
|
11
11
|
成功结果的 `data.directory` 是工程根目录,`data.metadata_path` 是元信息文件。CLI 负责下载校验和解压,工程仍保留占位符、SQL 和原始文件。下载失败先核对实际结果,再重新运行;不用链接地址当作创建成功依据。
|
|
12
12
|
|
|
13
|
+
metadata.json 包含 `repositories` 或 `functions` 时,按[后端服务模板的源码开发步骤](function-templates.md#下载源码后继续开发)保留资源关系;后端私有数据库不绑定给 Web。以下单仓库、Web 直连 SQLite 的步骤适用于相应模板。
|
|
14
|
+
|
|
13
15
|
## metadata.json 和变量
|
|
14
16
|
|
|
15
17
|
读取根部 metadata.json、README、依赖锁文件及构建脚本,按实际工程开发。元信息示例:
|
|
@@ -19,6 +21,7 @@ tiana web init-template NAME --dir PROJECT_DIR --json
|
|
|
19
21
|
"name": "notes",
|
|
20
22
|
"display_name": "记事本",
|
|
21
23
|
"description": "个人笔记、文件夹、全文搜索和自动保存。",
|
|
24
|
+
"database_required": true,
|
|
22
25
|
"variables": [
|
|
23
26
|
{
|
|
24
27
|
"name": "SQLITE_INSTANCE_ID",
|
|
@@ -55,10 +58,18 @@ JSON 中要正确转义字符串,SQL 中按实际语法处理,源码中保
|
|
|
55
58
|
|
|
56
59
|
SQL 返回失败、断线或未知结果时停止后续发布,用独立只读查询核对已完成的结构与数据,不自动重放写入。初始化成功后进行应用定制、依赖安装、源码同步、构建及发布;工程 README 中的步骤与实际帮助一起核对。
|
|
57
60
|
|
|
61
|
+
## 预置素材
|
|
62
|
+
|
|
63
|
+
按[数据与素材存储](storage-and-assets.md)检查声明及现有实现。`file_seeds` 为
|
|
64
|
+
`template-files` 时,按[模板素材文件](template-files.md)核对源字节、相对路径、
|
|
65
|
+
当前 Web ID 的首次文件准备和干净构建。CLI 解压成功不会自动上传素材,普通
|
|
66
|
+
`web publish` 也不会灌入文件。没有素材声明时沿用原流程;已有应用不因再次发布
|
|
67
|
+
而恢复被用户修改或删除的初始文件。
|
|
68
|
+
|
|
58
69
|
## 模板图标
|
|
59
70
|
|
|
60
71
|
模板的源 `icon.png` 是用户应用的初始图标,复用工程的静态资源复制配置。变量替换时不把 PNG 当文本处理。新应用缺图、继承的是通用占位/标题首字图片或图形与本次功能不符时,按[应用图标](csr-apps.md#应用图标)优先调用 AI 生图制作并保存体现功能的图标,用户明确指定的图片优先。干净构建后确认产物根目录 `icon.png` 可以解码且与选定源图一致,再发布;以后更新模板本身不会直接改变已有用户应用的图标。
|
|
61
72
|
|
|
62
73
|
## 构建和来源
|
|
63
74
|
|
|
64
|
-
模板 `source.repository`、`source.commit` 说明下载包的源工程来源,区别于用户应用自己的 Git 提交。创建用户仓库后记录实际 git_instance_id;提交和构建按 [源码流程](../../tiana-git/references/source-releases.md)。按下载工程的 README
|
|
75
|
+
模板 `source.repository`、`source.commit` 说明下载包的源工程来源,区别于用户应用自己的 Git 提交。创建用户仓库后记录实际 git_instance_id;提交和构建按 [源码流程](../../tiana-git/references/source-releases.md)。按下载工程的 README 和构建脚本确定命令、入口与产物目录。创建 Web 时传入实际 entry 和 Git 绑定;直连模式的数据库绑定必须与本次执行 schema/seeds 的 SQLite 一致。源码提交号记录为构建证据。核对模块、资源及 MGR Web 项目的 SQLite/Git 绑定后执行 `web publish`,再用 publish_id 查询状态。
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""Package a reviewed source snapshot and its clean build as an installable template."""
|
|
3
3
|
import argparse
|
|
4
|
+
import base64
|
|
4
5
|
import hashlib
|
|
5
6
|
import io
|
|
6
7
|
import json
|
|
@@ -13,6 +14,17 @@ MIB = 1 << 20
|
|
|
13
14
|
MAX_FILE = 16 * MIB
|
|
14
15
|
MAX_EXPANDED = 128 * MIB
|
|
15
16
|
MAX_ENTRIES = 5000
|
|
17
|
+
MAX_FILE_SEEDS = 256
|
|
18
|
+
FILE_TYPES = {
|
|
19
|
+
".webp": "image/webp", ".png": "image/png", ".jpg": "image/jpeg",
|
|
20
|
+
".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml",
|
|
21
|
+
".avif": "image/avif", ".pdf": "application/pdf", ".txt": "text/plain",
|
|
22
|
+
".json": "application/json", ".csv": "text/csv", ".md": "text/markdown",
|
|
23
|
+
".mp3": "audio/mpeg", ".wav": "audio/wav", ".ogg": "audio/ogg",
|
|
24
|
+
".mp4": "video/mp4", ".webm": "video/webm", ".woff": "font/woff",
|
|
25
|
+
".woff2": "font/woff2", ".ttf": "font/ttf", ".otf": "font/otf",
|
|
26
|
+
".glb": "model/gltf-binary", ".gltf": "model/gltf+json",
|
|
27
|
+
}
|
|
16
28
|
|
|
17
29
|
|
|
18
30
|
def archive(files):
|
|
@@ -58,17 +70,31 @@ def directory_count(data):
|
|
|
58
70
|
|
|
59
71
|
def source_files(data):
|
|
60
72
|
count = directory_count(data)
|
|
61
|
-
files, total, seen = {}, 0, set()
|
|
73
|
+
files, directories, total, seen = {}, set(), 0, set()
|
|
62
74
|
with zipfile.ZipFile(io.BytesIO(data)) as source:
|
|
63
75
|
for item in source.infolist():
|
|
64
|
-
|
|
76
|
+
decoded = item.orig_filename
|
|
77
|
+
if not item.flag_bits & 0x800:
|
|
78
|
+
decoded = decoded.encode("cp437").decode("utf-8")
|
|
79
|
+
name = decoded.removesuffix("/")
|
|
65
80
|
parts = name.split("/")
|
|
66
81
|
mode = item.external_attr >> 16
|
|
67
82
|
if (not name or "\\" in name or "\0" in name or ":" in name
|
|
68
83
|
or any(part in ("", ".", "..") or part.lower() == ".git" for part in parts)
|
|
69
84
|
or PurePosixPath(name).is_absolute() or name in seen
|
|
70
|
-
or stat.S_IFMT(mode) not in (0, stat.S_IFREG, stat.S_IFDIR)
|
|
85
|
+
or stat.S_IFMT(mode) not in (0, stat.S_IFREG, stat.S_IFDIR)
|
|
86
|
+
or item.is_dir() and stat.S_IFMT(mode) == stat.S_IFREG
|
|
87
|
+
or not item.is_dir() and stat.S_IFMT(mode) == stat.S_IFDIR
|
|
88
|
+
or name in directories and not item.is_dir()):
|
|
71
89
|
raise ValueError(f"Unsafe source ZIP path or type: {name}")
|
|
90
|
+
for parent in PurePosixPath(name).parents:
|
|
91
|
+
if str(parent) == ".":
|
|
92
|
+
continue
|
|
93
|
+
if str(parent) in files:
|
|
94
|
+
raise ValueError(f"Source ZIP file/directory collision: {name}")
|
|
95
|
+
directories.add(str(parent))
|
|
96
|
+
if item.is_dir():
|
|
97
|
+
directories.add(name)
|
|
72
98
|
seen.add(name)
|
|
73
99
|
if item.file_size > MAX_FILE or total + item.file_size > MAX_EXPANDED:
|
|
74
100
|
raise ValueError("Source ZIP exceeds expanded size budget")
|
|
@@ -77,17 +103,115 @@ def source_files(data):
|
|
|
77
103
|
if len(value) > MAX_FILE or total + len(value) > MAX_EXPANDED:
|
|
78
104
|
raise ValueError("Source ZIP exceeds actual expanded size budget")
|
|
79
105
|
total += len(value)
|
|
106
|
+
if item.is_dir() and value:
|
|
107
|
+
raise ValueError(f"Source ZIP directory contains file bytes: {name}")
|
|
80
108
|
if not item.is_dir():
|
|
81
109
|
files[name] = value
|
|
82
110
|
return files, total, count
|
|
83
111
|
|
|
84
112
|
|
|
113
|
+
def template_file_materials(snapshot, metadata):
|
|
114
|
+
names = [name for name in snapshot if name == "template-files" or name.startswith("template-files/")]
|
|
115
|
+
if "file_seeds" not in metadata:
|
|
116
|
+
if names:
|
|
117
|
+
raise ValueError("template-files requires a file_seeds declaration")
|
|
118
|
+
return None
|
|
119
|
+
if metadata["file_seeds"] != "template-files":
|
|
120
|
+
raise ValueError("file_seeds must be template-files")
|
|
121
|
+
if not 1 <= len(names) <= MAX_FILE_SEEDS:
|
|
122
|
+
raise ValueError("template-files requires 1..256 files")
|
|
123
|
+
entries, materials, total = [], {}, 0
|
|
124
|
+
for name in sorted(names, key=lambda value: value.encode("utf-8")):
|
|
125
|
+
relative = name[len("template-files/"):]
|
|
126
|
+
if (not relative or len(relative.encode("utf-8")) > 1024
|
|
127
|
+
or any(part in ("", ".", "..") for part in relative.split("/"))
|
|
128
|
+
or any(character in "\\%" or ord(character) < 32 or 127 <= ord(character) <= 159 for character in relative)):
|
|
129
|
+
raise ValueError(f"Invalid template file path: {relative}")
|
|
130
|
+
value = snapshot[name]
|
|
131
|
+
total += len(value)
|
|
132
|
+
if len(value) > MAX_FILE or total > MAX_FILE:
|
|
133
|
+
raise ValueError("template-files exceeds 16 MiB")
|
|
134
|
+
entries.append({"path": relative, "size": len(value),
|
|
135
|
+
"sha256": hashlib.sha256(value).hexdigest(),
|
|
136
|
+
"content_type": FILE_TYPES.get(PurePosixPath(relative).suffix.lower(), "application/octet-stream")})
|
|
137
|
+
materials["files/" + relative] = value
|
|
138
|
+
return {"manifest": {"version": 1, "root": "template-assets/", "entries": entries},
|
|
139
|
+
"materials": materials, "bytes": total}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def check_seed_build_output(name, data, seeds):
|
|
143
|
+
if seeds is None:
|
|
144
|
+
return
|
|
145
|
+
if any(part in ("template-files", "template-assets") for part in PurePosixPath(name).parts):
|
|
146
|
+
raise ValueError(f"External template files must not enter the Web build: {name}")
|
|
147
|
+
# The platform requires this exact root icon independently of file seeds.
|
|
148
|
+
if name == "icon.png":
|
|
149
|
+
return
|
|
150
|
+
for seed, content in seeds["materials"].items():
|
|
151
|
+
if content and (data == content or len(content) >= 16 and (content in data or base64.b64encode(content) in data)):
|
|
152
|
+
raise ValueError(f"External template file embedded in Web build: {seed} in {name}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def encode_install_manifest(manifest, seeds):
|
|
156
|
+
if seeds is None:
|
|
157
|
+
return (json.dumps(manifest, ensure_ascii=False, allow_nan=False) + "\n").encode(), None
|
|
158
|
+
raw_files = json.dumps(seeds["manifest"], ensure_ascii=False, separators=(",", ":"), allow_nan=False).encode()
|
|
159
|
+
base = json.dumps({**manifest, "format_version": 2, "requires": ["template_files_v1"]}, ensure_ascii=False, allow_nan=False).encode()
|
|
160
|
+
return base[:-1] + b',"files":' + raw_files + b"}\n", hashlib.sha256(raw_files).hexdigest()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def unique_json_object(pairs):
|
|
164
|
+
result = {}
|
|
165
|
+
for key, value in pairs:
|
|
166
|
+
if key in result:
|
|
167
|
+
raise ValueError(f"Duplicate metadata JSON key: {key}")
|
|
168
|
+
result[key] = value
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def validate_backend_materials(metadata, snapshot, site):
|
|
173
|
+
repositories = {r["key"]: r for r in metadata.get("repositories", [])}
|
|
174
|
+
for repo in repositories.values():
|
|
175
|
+
prefix = "" if repo["path"] == "." else repo["path"] + "/"
|
|
176
|
+
if not any(name.startswith(prefix) for name in snapshot):
|
|
177
|
+
raise ValueError(f"Repository source missing: {repo['key']}")
|
|
178
|
+
for variable in repo.get("variables", []):
|
|
179
|
+
for name in variable["files"]:
|
|
180
|
+
if variable["placeholder"].encode() not in snapshot.get(prefix + name, b""):
|
|
181
|
+
raise ValueError(f"Source placeholder missing: {repo['key']}/{name}")
|
|
182
|
+
for fn in metadata.get("functions", []):
|
|
183
|
+
repo = repositories.get(fn["repository"])
|
|
184
|
+
if repo is None:
|
|
185
|
+
raise ValueError(f"Function repository missing: {fn['key']}")
|
|
186
|
+
prefix = "" if repo["path"] == "." else repo["path"] + "/"
|
|
187
|
+
directory = fn.get("source_dir", "").rstrip("/")
|
|
188
|
+
if fn.get("source_dir") != "/" and (not directory.startswith("/") or any(part in ("", ".", "..") for part in directory[1:].split("/")) or "\\" in directory or "\0" in directory):
|
|
189
|
+
raise ValueError("Function source_dir must be a repository path starting with /")
|
|
190
|
+
prefix += directory[1:] + "/" if directory else ""
|
|
191
|
+
if not any(name.startswith(prefix) and "/" not in name[len(prefix):] and name.endswith(".go") for name in snapshot):
|
|
192
|
+
raise ValueError(f"Function entry Go source missing: {fn['key']}")
|
|
193
|
+
for variable in metadata.get("site_variables", []):
|
|
194
|
+
for name in variable["files"]:
|
|
195
|
+
if variable["placeholder"].encode() not in site.get("data/" + name, b""):
|
|
196
|
+
raise ValueError(f"Built site placeholder missing: {name}")
|
|
197
|
+
|
|
198
|
+
|
|
85
199
|
def pack(source_zip, dist, entry, output, without_source=False):
|
|
86
200
|
source = read_file(Path(source_zip))
|
|
87
201
|
snapshot, source_size, source_count = source_files(source)
|
|
88
|
-
|
|
202
|
+
raw_metadata = snapshot["metadata.json"]
|
|
203
|
+
if len(raw_metadata) > MIB:
|
|
204
|
+
raise ValueError("metadata.json exceeds 1 MiB")
|
|
205
|
+
metadata = json.loads(raw_metadata.decode("utf-8"), object_pairs_hook=unique_json_object)
|
|
206
|
+
if type(metadata) is not dict:
|
|
207
|
+
raise ValueError("metadata.json must be an object")
|
|
89
208
|
if type(metadata.get("database_required")) is not bool:
|
|
90
209
|
raise ValueError("metadata.json requires an explicit database_required boolean")
|
|
210
|
+
if without_source and metadata.get("functions"):
|
|
211
|
+
raise ValueError("Function templates require source.zip to compile their backends")
|
|
212
|
+
seeds = template_file_materials(snapshot, metadata)
|
|
213
|
+
if seeds is not None and without_source:
|
|
214
|
+
raise ValueError("File templates require source.zip for server byte verification; --without-source is only available for templates without file_seeds")
|
|
91
215
|
names = ["showcase/preview.html", "showcase/cover.webp"]
|
|
92
216
|
if not without_source:
|
|
93
217
|
names.append("showcase/prompt.md")
|
|
@@ -104,18 +228,27 @@ def pack(source_zip, dist, entry, output, without_source=False):
|
|
|
104
228
|
sql_names[name] = target
|
|
105
229
|
site = {"web.yaml": b""}
|
|
106
230
|
dist = Path(dist)
|
|
231
|
+
if dist.is_symlink() or not dist.is_dir():
|
|
232
|
+
raise ValueError("Build directory must be a real directory")
|
|
107
233
|
site_size = 0
|
|
108
234
|
for file in sorted(dist.rglob("*")):
|
|
109
235
|
if file.is_symlink():
|
|
110
236
|
raise ValueError(f"Build contains a symlink: {file.relative_to(dist)}")
|
|
237
|
+
relative = file.relative_to(dist).as_posix()
|
|
238
|
+
if seeds is not None and any(part in ("template-files", "template-assets") for part in file.relative_to(dist).parts):
|
|
239
|
+
raise ValueError(f"External template files must not enter the Web build: {relative}")
|
|
111
240
|
if file.is_file():
|
|
112
241
|
data = read_file(file)
|
|
242
|
+
check_seed_build_output(relative, data, seeds)
|
|
113
243
|
site_size += len(data)
|
|
114
244
|
if site_size > MAX_FILE or len(site) >= 4097:
|
|
115
245
|
raise ValueError("Build exceeds the Web archive resource budget")
|
|
116
246
|
site["data/" + file.relative_to(dist).as_posix()] = data
|
|
247
|
+
elif not file.is_dir():
|
|
248
|
+
raise ValueError(f"Build contains a nonregular entry: {relative}")
|
|
117
249
|
if not entry or "data/" + entry not in site:
|
|
118
250
|
raise ValueError("Build is missing its fixed entry")
|
|
251
|
+
validate_backend_materials(metadata, snapshot, site)
|
|
119
252
|
compiled = archive(site)
|
|
120
253
|
if len(compiled) > MAX_FILE:
|
|
121
254
|
raise ValueError("site.tweb exceeds 16 MiB")
|
|
@@ -132,7 +265,14 @@ def pack(source_zip, dist, entry, output, without_source=False):
|
|
|
132
265
|
files["icon.png"] = site["data/icon.png"]
|
|
133
266
|
files["site.tweb"] = compiled
|
|
134
267
|
manifest = {"metadata": metadata, "entry": entry, "database_required": metadata["database_required"], "variables": variables}
|
|
135
|
-
|
|
268
|
+
for field in ("private_database", "web_repository", "repositories", "functions", "site_variables"):
|
|
269
|
+
if field in metadata:
|
|
270
|
+
manifest[field] = metadata[field]
|
|
271
|
+
files["install.json"], file_manifest_digest = encode_install_manifest(manifest, seeds)
|
|
272
|
+
if len(files["install.json"]) > MIB:
|
|
273
|
+
raise ValueError("install.json exceeds 1 MiB")
|
|
274
|
+
if seeds is not None:
|
|
275
|
+
files.update(seeds["materials"])
|
|
136
276
|
expanded = sum(map(len, files.values())) + site_size + (0 if without_source else source_size)
|
|
137
277
|
entries = len(files) + len(site) + (0 if without_source else source_count)
|
|
138
278
|
if expanded > MAX_EXPANDED or entries > MAX_ENTRIES or any(len(value) > MAX_FILE for value in files.values()):
|
|
@@ -141,7 +281,10 @@ def pack(source_zip, dist, entry, output, without_source=False):
|
|
|
141
281
|
if len(data) > 32 * MIB:
|
|
142
282
|
raise ValueError("install.zip exceeds 32 MiB")
|
|
143
283
|
Path(output).write_bytes(data)
|
|
144
|
-
|
|
284
|
+
result = {"name": metadata["name"], "file": str(Path(output).resolve()), "size": len(data), "sha256": hashlib.sha256(data).hexdigest(), "has_source": not without_source, "database_required": metadata["database_required"], "expanded_bytes": expanded, "entries": entries}
|
|
285
|
+
if seeds is not None:
|
|
286
|
+
result.update({"format_version": 2, "file_count": len(seeds["manifest"]["entries"]), "file_bytes": seeds["bytes"], "file_manifest_sha256": file_manifest_digest})
|
|
287
|
+
return result
|
|
145
288
|
|
|
146
289
|
|
|
147
290
|
if __name__ == "__main__":
|
|
@@ -150,6 +293,6 @@ if __name__ == "__main__":
|
|
|
150
293
|
parser.add_argument("--dist", required=True, help="Clean build output directory")
|
|
151
294
|
parser.add_argument("--entry", required=True, help="Fixed entry relative to dist")
|
|
152
295
|
parser.add_argument("--output", required=True, help="Destination install.zip")
|
|
153
|
-
parser.add_argument("--without-source", action="store_true", help="
|
|
296
|
+
parser.add_argument("--without-source", action="store_true", help="No-file templates only: omit source and customization prompt")
|
|
154
297
|
args = parser.parse_args()
|
|
155
298
|
print(json.dumps(pack(args.source, args.dist, args.entry, args.output, args.without_source), ensure_ascii=False))
|