@tianacloud/agent-skills 1.0.18 → 1.0.20
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/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/tiana/SKILL.md +1 -1
- package/skills/tiana-function/SKILL.md +1 -1
- package/skills/tiana-git/SKILL.md +1 -1
- package/skills/tiana-sqlite/SKILL.md +1 -1
- package/skills/tiana-web/SKILL.md +1 -1
- package/skills/tiana-web/references/share-template.md +4 -0
- package/skills/tiana-web/scripts/pack-template.py +228 -37
package/package.json
CHANGED
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.20",
|
|
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
|
@@ -73,6 +73,10 @@
|
|
|
73
73
|
没有外置素材且发布者选择不分享源码时增加 `--without-source`。声明素材时保留
|
|
74
74
|
source.zip,并生成与其逐字节相同的独立 `files/`、固定格式/能力及文件清单。
|
|
75
75
|
复核目标环境已支持并启用该能力。脚本不替代数据、公开内容或应用行为检查。
|
|
76
|
+
|
|
77
|
+
若返回 `[preflight/...]`,保存原始报告,并逐项向作者说明阶段、字段/文件位置、原因和修复办法,可等义整理,不只转述为“打包失败”。字段位置中的数组下标从0开始;报告截断时说明仍有问题未展示,修复后再次检查。在既有任务授权内修正明确的机械错误;数据库需求、私有数据库访问方式或内容可公开性不明确时,先让作者判断。
|
|
78
|
+
|
|
79
|
+
修正公开副本后,重新执行第1–2步,直到原命令成功并返回本轮回执;编辑本地文件不会更新已有source.zip。这类前置失败不读取dist、不生成或覆盖安装包;已有output未更新,不能当作本次成功产物。尚未执行的构建材料和服务端检查仍需继续完成。
|
|
76
80
|
3. 复查脚本输出的大小、SHA-256、源码选择及数据库依赖。有素材时再核对 file_count、file_bytes、file_manifest_sha256、源/独立素材字节和干净构建。安装包上限 32 MiB,单材料 16 MiB,所有解析层累计展开 128 MiB、5,000 个条目;site.tweb 同时遵守 16 MiB 和 4,097 条目的 Web 归档限制。超限时先移除无用依赖、压缩资源或精简材料。
|
|
77
81
|
4. 执行 `tiana web publish-share-template WEB_ID --file /绝对路径/install.zip --wait --json`。上传峰值包含原模板材料、中转 ZIP 和新材料,全部计入当前租户额度。CLI 申请精确对象授权、直传上传并提交异步发布。
|
|
78
82
|
5. 用 `tiana web status-share-template WEB_ID --json` 核对当前 `share.package_id`、Web ID、模板名称和 URL。支持配套字段的 CLI 还会保留 share.version、material_version 和 manifest.files;材料版本应等于本次完整安装包摘要。上传响应未知或等待被中断时先查询原状态,不把新上传当作恢复。发布失败时核对错误和中转清理。完整历史能力上线后旧版发布材料继续保留和计费,中转 ZIP 确认删除后才释放其占用额度。
|
|
@@ -15,6 +15,7 @@ MAX_FILE = 16 * MIB
|
|
|
15
15
|
MAX_EXPANDED = 128 * MIB
|
|
16
16
|
MAX_ENTRIES = 5000
|
|
17
17
|
MAX_FILE_SEEDS = 256
|
|
18
|
+
MAX_PREFLIGHT_ISSUES = 20
|
|
18
19
|
FILE_TYPES = {
|
|
19
20
|
".webp": "image/webp", ".png": "image/png", ".jpg": "image/jpeg",
|
|
20
21
|
".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml",
|
|
@@ -27,6 +28,16 @@ FILE_TYPES = {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
class TemplatePreflightError(ValueError):
|
|
32
|
+
def __init__(self, issues):
|
|
33
|
+
super().__init__("\n".join([
|
|
34
|
+
"Template preflight failed; install.zip was not written or updated.",
|
|
35
|
+
*issues,
|
|
36
|
+
"Fix the source copy, re-export source.zip and rebuild from the same reviewed commit, then pack again.",
|
|
37
|
+
"Remaining packaging checks have not run; this is not publishing approval.",
|
|
38
|
+
]))
|
|
39
|
+
|
|
40
|
+
|
|
30
41
|
def archive(files):
|
|
31
42
|
output = io.BytesIO()
|
|
32
43
|
with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as result:
|
|
@@ -164,32 +175,220 @@ def unique_json_object(pairs):
|
|
|
164
175
|
result = {}
|
|
165
176
|
for key, value in pairs:
|
|
166
177
|
if key in result:
|
|
167
|
-
raise
|
|
178
|
+
raise TemplatePreflightError([
|
|
179
|
+
"[preflight/metadata] metadata.json: duplicate object key.\n"
|
|
180
|
+
" Fix: keep one intended value per key; remove the duplicate declaration.",
|
|
181
|
+
])
|
|
168
182
|
result[key] = value
|
|
169
183
|
return result
|
|
170
184
|
|
|
171
185
|
|
|
172
|
-
def
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
186
|
+
def preflight(snapshot, without_source):
|
|
187
|
+
# Only fixed fields and material names enter diagnostics, never input values.
|
|
188
|
+
if "metadata.json" not in snapshot:
|
|
189
|
+
raise TemplatePreflightError([
|
|
190
|
+
"[preflight/metadata] metadata.json: missing.\n"
|
|
191
|
+
" Fix: include metadata.json at the root of the source snapshot.",
|
|
192
|
+
])
|
|
193
|
+
raw_metadata = snapshot["metadata.json"]
|
|
194
|
+
if len(raw_metadata) > MIB:
|
|
195
|
+
raise TemplatePreflightError([
|
|
196
|
+
"[preflight/metadata] metadata.json: exceeds 1 MiB.\n"
|
|
197
|
+
" Fix: reduce metadata to at most 1 MiB; keep application data outside metadata.",
|
|
198
|
+
])
|
|
199
|
+
try:
|
|
200
|
+
metadata = json.loads(raw_metadata.decode("utf-8"), object_pairs_hook=unique_json_object)
|
|
201
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
202
|
+
raise TemplatePreflightError([
|
|
203
|
+
"[preflight/metadata] metadata.json: invalid UTF-8 or JSON.\n"
|
|
204
|
+
" Fix: save a valid UTF-8 JSON object before checking its fields.",
|
|
205
|
+
]) from None
|
|
206
|
+
if type(metadata) is not dict:
|
|
207
|
+
raise TemplatePreflightError([
|
|
208
|
+
"[preflight/metadata] metadata.json: must be an object.\n"
|
|
209
|
+
" Fix: put the template fields in a JSON object, not a list or scalar.",
|
|
210
|
+
])
|
|
211
|
+
|
|
212
|
+
issues = []
|
|
213
|
+
missing = object()
|
|
214
|
+
|
|
215
|
+
def issue(location, problem, fix, stage="metadata"):
|
|
216
|
+
if len(issues) < MAX_PREFLIGHT_ISSUES:
|
|
217
|
+
if stage == "metadata":
|
|
218
|
+
location = "metadata.json:" + location
|
|
219
|
+
issues.append(f"[preflight/{stage}] {location}: {problem}.\n Fix: {fix}")
|
|
220
|
+
elif len(issues) == MAX_PREFLIGHT_ISSUES:
|
|
221
|
+
issues.append(f"More problems found; showing the first {MAX_PREFLIGHT_ISSUES}. Fix these and run again.")
|
|
222
|
+
|
|
223
|
+
def string(value, location):
|
|
224
|
+
if value is missing:
|
|
225
|
+
issue(location, "missing required field", "add a nonempty string for this field.")
|
|
226
|
+
elif type(value) is not str or not value.strip():
|
|
227
|
+
issue(location, "requires a nonempty string", "supply text, not null, a number, a list or an object.")
|
|
228
|
+
else:
|
|
229
|
+
try:
|
|
230
|
+
value.encode("utf-8")
|
|
231
|
+
return value
|
|
232
|
+
except UnicodeEncodeError:
|
|
233
|
+
issue(location, "contains invalid Unicode", "replace unpaired Unicode surrogates with valid text.")
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
def objects(value, location):
|
|
237
|
+
if type(value) is not list:
|
|
238
|
+
issue(location, "requires an array", "use an array of objects; omit this optional field if unused.")
|
|
239
|
+
return
|
|
240
|
+
for index, item in enumerate(value):
|
|
241
|
+
where = f"{location}[{index}]"
|
|
242
|
+
if type(item) is not dict:
|
|
243
|
+
issue(where, "requires an object", "supply the fields of this declaration in a JSON object.")
|
|
244
|
+
else:
|
|
245
|
+
yield where, item
|
|
246
|
+
|
|
247
|
+
def variables(value, location, prefix):
|
|
248
|
+
for where, variable in objects(value, location):
|
|
249
|
+
for field in ("name", "description"):
|
|
250
|
+
string(variable.get(field, missing), f"{where}.{field}")
|
|
251
|
+
placeholder = string(variable.get("placeholder", missing), f"{where}.placeholder")
|
|
252
|
+
files = variable.get("files")
|
|
253
|
+
if type(files) is not list or not files:
|
|
254
|
+
issue(f"{where}.files", "requires a nonempty array of file paths",
|
|
255
|
+
"list the files that contain this variable's placeholder.")
|
|
256
|
+
continue
|
|
257
|
+
for index, value in enumerate(files):
|
|
258
|
+
at = f"{where}.files[{index}]"
|
|
259
|
+
name = string(value, at)
|
|
260
|
+
# site_variables refer to dist, which is checked only after preflight.
|
|
261
|
+
if name is None or prefix is None:
|
|
262
|
+
continue
|
|
263
|
+
content = snapshot.get(prefix + name)
|
|
264
|
+
if content is None:
|
|
265
|
+
issue(at, "declared source file is missing",
|
|
266
|
+
"include this file in source.zip or correct its path relative to the declared source root.")
|
|
267
|
+
elif placeholder is not None and placeholder.encode("utf-8") not in content:
|
|
268
|
+
issue(at, "Source placeholder missing",
|
|
269
|
+
"restore the declared placeholder in this file or correct the variable declaration.")
|
|
270
|
+
|
|
271
|
+
for field in ("name", "display_name", "description"):
|
|
272
|
+
string(metadata.get(field, missing), field)
|
|
273
|
+
if type(metadata.get("database_required")) is not bool:
|
|
274
|
+
issue("database_required", "requires a JSON boolean",
|
|
275
|
+
"use true or false, not a quoted value, after confirming the app's database needs.\n"
|
|
276
|
+
" Declared schema/seeds require true; do not remove required SQL just to pass.")
|
|
277
|
+
if "private_database" in metadata and type(metadata["private_database"]) is not bool:
|
|
278
|
+
issue("private_database", "requires a JSON boolean",
|
|
279
|
+
"use true or false without quotes; true keeps the database off the browser's Web binding.")
|
|
280
|
+
|
|
281
|
+
sql = []
|
|
282
|
+
for field in ("schema", "seeds"):
|
|
283
|
+
if field not in metadata:
|
|
284
|
+
continue
|
|
285
|
+
name = string(metadata[field], field)
|
|
286
|
+
if name is not None:
|
|
287
|
+
sql.append(name)
|
|
288
|
+
if name not in snapshot:
|
|
289
|
+
issue(field, "declared SQL file is missing",
|
|
290
|
+
"include the declared SQL file in source.zip or correct its path relative to the snapshot root.")
|
|
291
|
+
if sql and metadata.get("database_required") is False:
|
|
292
|
+
issue("database_required", "SQL materials require database_required=true",
|
|
293
|
+
"confirm the database dependency and use true for SQL-backed apps; do not remove required SQL just to pass.")
|
|
294
|
+
has_file_seeds = any(name == "template-files" or name.startswith("template-files/") for name in snapshot)
|
|
295
|
+
if "file_seeds" in metadata:
|
|
296
|
+
if metadata["file_seeds"] != "template-files":
|
|
297
|
+
issue("file_seeds", "must be the string template-files",
|
|
298
|
+
"use the declared template-files directory; omit this optional field only when no file seeds are needed.")
|
|
299
|
+
elif not has_file_seeds:
|
|
300
|
+
issue("file_seeds", "declared material files are missing",
|
|
301
|
+
"include the reviewed files under template-files/ in source.zip.")
|
|
302
|
+
elif has_file_seeds:
|
|
303
|
+
issue("file_seeds", "missing declaration for template-files",
|
|
304
|
+
"declare file_seeds as template-files after reviewing the files for public distribution.")
|
|
305
|
+
|
|
306
|
+
variables(metadata.get("variables", []), "variables", "")
|
|
307
|
+
repositories = {}
|
|
308
|
+
for where, repo in objects(metadata.get("repositories", []), "repositories"):
|
|
309
|
+
key = string(repo.get("key", missing), f"{where}.key")
|
|
310
|
+
path = string(repo.get("path", missing), f"{where}.path")
|
|
311
|
+
prefix = None
|
|
312
|
+
if path is not None:
|
|
313
|
+
if path != "." and ("\\" in path or any(part in ("", ".", "..") for part in path.split("/"))):
|
|
314
|
+
issue(f"{where}.path", "requires a relative source directory",
|
|
315
|
+
"use '.' or a relative directory without empty, dot or parent segments.")
|
|
316
|
+
else:
|
|
317
|
+
prefix = "" if path == "." else path + "/"
|
|
318
|
+
if not any(name.startswith(prefix) for name in snapshot):
|
|
319
|
+
issue(f"{where}.path", "Repository source missing",
|
|
320
|
+
"include the reviewed repository snapshot under this directory.")
|
|
321
|
+
prefix = None
|
|
322
|
+
if key is not None:
|
|
323
|
+
if key in repositories:
|
|
324
|
+
issue(f"{where}.key", "duplicate repository key", "give each logical repository a unique key.")
|
|
325
|
+
else:
|
|
326
|
+
repositories[key] = prefix
|
|
327
|
+
variables(repo.get("variables", []), f"{where}.variables", prefix)
|
|
328
|
+
repositories_typed = type(metadata.get("repositories", [])) is list
|
|
329
|
+
if "web_repository" in metadata:
|
|
330
|
+
key = string(metadata["web_repository"], "web_repository")
|
|
331
|
+
if key is not None and repositories_typed and key not in repositories:
|
|
332
|
+
issue("web_repository", "references an undeclared repository",
|
|
333
|
+
"use the key of the declared repository containing the Web source.")
|
|
334
|
+
|
|
335
|
+
function_keys = set()
|
|
336
|
+
for where, fn in objects(metadata.get("functions", []), "functions"):
|
|
337
|
+
key = string(fn.get("key", missing), f"{where}.key")
|
|
338
|
+
string(fn.get("name", missing), f"{where}.name")
|
|
339
|
+
repo = string(fn.get("repository", missing), f"{where}.repository")
|
|
340
|
+
directory = string(fn.get("source_dir", missing), f"{where}.source_dir")
|
|
341
|
+
if key is not None:
|
|
342
|
+
if key in function_keys:
|
|
343
|
+
issue(f"{where}.key", "duplicate function key", "give each function a unique key.")
|
|
344
|
+
function_keys.add(key)
|
|
345
|
+
if repo is not None and repositories_typed and repo not in repositories:
|
|
346
|
+
issue(f"{where}.repository", "Function repository missing", "reference a declared repository key.")
|
|
347
|
+
if directory is not None:
|
|
348
|
+
directory = directory.rstrip("/")
|
|
349
|
+
if fn["source_dir"] != "/" and (not directory.startswith("/") or any(part in ("", ".", "..") for part in directory[1:].split("/")) or "\\" in directory or "\0" in directory):
|
|
350
|
+
issue(f"{where}.source_dir", "requires a repository path starting with /",
|
|
351
|
+
"use the function's actual source directory, without dot or parent segments.")
|
|
352
|
+
elif repo in repositories and repositories[repo] is not None:
|
|
353
|
+
prefix = repositories[repo] + (directory[1:] + "/" if directory else "")
|
|
354
|
+
if not any(name.startswith(prefix) and "/" not in name[len(prefix):] and name.endswith(".go") for name in snapshot):
|
|
355
|
+
issue(f"{where}.source_dir", "Function entry Go source missing",
|
|
356
|
+
"include the function's Go entry source directly in this repository directory.")
|
|
357
|
+
for field in ("parameters", "cors", "database"):
|
|
358
|
+
if field in fn and type(fn[field]) is not dict:
|
|
359
|
+
issue(f"{where}.{field}", "requires an object", "use the documented object fields or omit this optional declaration.")
|
|
360
|
+
database = fn.get("database")
|
|
361
|
+
if type(database) is dict:
|
|
362
|
+
endpoint = string(database.get("endpoint_parameter", missing), f"{where}.database.endpoint_parameter")
|
|
363
|
+
token = string(database.get("token_parameter", missing), f"{where}.database.token_parameter")
|
|
364
|
+
if endpoint is not None and endpoint == token:
|
|
365
|
+
issue(f"{where}.database", "requires distinct endpoint and token parameter names",
|
|
366
|
+
"declare separate backend parameters; credentials are supplied during installation.")
|
|
367
|
+
if metadata.get("database_required") is False:
|
|
368
|
+
issue(f"{where}.database", "requires database_required=true",
|
|
369
|
+
"confirm the backend database dependency; do not remove required configuration just to pass.")
|
|
370
|
+
variables(metadata.get("site_variables", []), "site_variables", None)
|
|
371
|
+
|
|
372
|
+
names = ["showcase/preview.html", "showcase/cover.webp"]
|
|
373
|
+
if not without_source:
|
|
374
|
+
names.append("showcase/prompt.md")
|
|
375
|
+
fixes = [
|
|
376
|
+
"add the self-contained preview HTML to the source snapshot.",
|
|
377
|
+
"add the WebP cover made from the reviewed preview to the source snapshot.",
|
|
378
|
+
"add the customization prompt for source-sharing mode to the source snapshot.",
|
|
379
|
+
]
|
|
380
|
+
for name, fix in zip(names, fixes):
|
|
381
|
+
if name not in snapshot:
|
|
382
|
+
issue(name, "missing", fix, "showcase")
|
|
383
|
+
elif name == "showcase/preview.html" and len(snapshot[name]) > 500_000:
|
|
384
|
+
issue(name, "exceeds 500,000 bytes",
|
|
385
|
+
"simplify the self-contained preview to at most 500,000 bytes before compression.", "showcase")
|
|
386
|
+
if issues:
|
|
387
|
+
raise TemplatePreflightError(issues)
|
|
388
|
+
return metadata, names
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def validate_site_variables(metadata, site):
|
|
193
392
|
for variable in metadata.get("site_variables", []):
|
|
194
393
|
for name in variable["files"]:
|
|
195
394
|
if variable["placeholder"].encode() not in site.get("data/" + name, b""):
|
|
@@ -199,25 +398,13 @@ def validate_backend_materials(metadata, snapshot, site):
|
|
|
199
398
|
def pack(source_zip, dist, entry, output, without_source=False):
|
|
200
399
|
source = read_file(Path(source_zip))
|
|
201
400
|
snapshot, source_size, source_count = source_files(source)
|
|
202
|
-
|
|
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")
|
|
208
|
-
if type(metadata.get("database_required")) is not bool:
|
|
209
|
-
raise ValueError("metadata.json requires an explicit database_required boolean")
|
|
401
|
+
metadata, names = preflight(snapshot, without_source)
|
|
210
402
|
if without_source and metadata.get("functions"):
|
|
211
403
|
raise ValueError("Function templates require source.zip to compile their backends")
|
|
212
404
|
seeds = template_file_materials(snapshot, metadata)
|
|
213
405
|
if seeds is not None and without_source:
|
|
214
406
|
raise ValueError("File templates require source.zip for server byte verification; --without-source is only available for templates without file_seeds")
|
|
215
|
-
names = ["showcase/preview.html", "showcase/cover.webp"]
|
|
216
|
-
if not without_source:
|
|
217
|
-
names.append("showcase/prompt.md")
|
|
218
407
|
files = {name: snapshot[name] for name in names}
|
|
219
|
-
if len(files["showcase/preview.html"]) > 500_000:
|
|
220
|
-
raise ValueError("showcase/preview.html exceeds 500,000 bytes")
|
|
221
408
|
sql_names = {}
|
|
222
409
|
for field, target in (("schema", "schema.sql"), ("seeds", "seed.sql")):
|
|
223
410
|
name = metadata.get(field)
|
|
@@ -248,7 +435,7 @@ def pack(source_zip, dist, entry, output, without_source=False):
|
|
|
248
435
|
raise ValueError(f"Build contains a nonregular entry: {relative}")
|
|
249
436
|
if not entry or "data/" + entry not in site:
|
|
250
437
|
raise ValueError("Build is missing its fixed entry")
|
|
251
|
-
|
|
438
|
+
validate_site_variables(metadata, site)
|
|
252
439
|
compiled = archive(site)
|
|
253
440
|
if len(compiled) > MAX_FILE:
|
|
254
441
|
raise ValueError("site.tweb exceeds 16 MiB")
|
|
@@ -295,4 +482,8 @@ if __name__ == "__main__":
|
|
|
295
482
|
parser.add_argument("--output", required=True, help="Destination install.zip")
|
|
296
483
|
parser.add_argument("--without-source", action="store_true", help="No-file templates only: omit source and customization prompt")
|
|
297
484
|
args = parser.parse_args()
|
|
298
|
-
|
|
485
|
+
try:
|
|
486
|
+
result = pack(args.source, args.dist, args.entry, args.output, args.without_source)
|
|
487
|
+
except TemplatePreflightError as error:
|
|
488
|
+
parser.exit(1, f"{error}\n")
|
|
489
|
+
print(json.dumps(result, ensure_ascii=False))
|