@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tianacloud/agent-skills",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
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.18",
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",
@@ -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.18"
7
+ version: "1.0.20"
8
8
  source: https://github.com/tianacloud/agent-skills/tree/main/skills/tiana
9
9
  ---
10
10
 
@@ -4,7 +4,7 @@ description: 使用 Tiana 创建和管理 Go 后端服务,读取及保存 Git
4
4
  license: Apache-2.0
5
5
  metadata:
6
6
  author: Tiana Cloud
7
- version: "1.0.18"
7
+ version: "1.0.20"
8
8
  parent: tiana
9
9
  source: https://github.com/tianacloud/agent-skills/tree/main/skills/tiana-function
10
10
  ---
@@ -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.18"
7
+ version: "1.0.20"
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.18"
7
+ version: "1.0.20"
8
8
  parent: tiana
9
9
  source: https://github.com/tianacloud/agent-skills/tree/main/skills/tiana-sqlite
10
10
  ---
@@ -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.18"
7
+ version: "1.0.20"
8
8
  parent: tiana
9
9
  source: https://github.com/tianacloud/agent-skills/tree/main/skills/tiana-web
10
10
  ---
@@ -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 ValueError(f"Duplicate metadata JSON key: {key}")
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 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']}")
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
- 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")
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
- validate_backend_materials(metadata, snapshot, site)
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
- print(json.dumps(pack(args.source, args.dist, args.entry, args.output, args.without_source), ensure_ascii=False))
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))