@petercjl/product-asset-set 0.0.0-stage → 0.1.1

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/INSTALL.md ADDED
@@ -0,0 +1,5 @@
1
+ # 安装
2
+
3
+ > 请安装 `@petercjl/product-asset-set` 提供的 Skill。
4
+
5
+ 当前 Agent 从 npm 获取并核对指定版本,读取包内 `skills/product-asset-set/SKILL.md`。按自身支持的安装及注册方式,将该目录和全部资源实体复制到自己的 Skill 环境。保留已存在的非受管文件和本地改动,安装后核对版本、文件哈希与实际加载状态。运行本地校验工具需要 Node.js 22 或更新版本。图像执行能力以 Skill 的能力声明为准。
package/README.md CHANGED
@@ -1,3 +1,9 @@
1
- # Temporary Holding Version
1
+ # product-asset-set
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Build reusable product reference image sets.
4
+
5
+ 请把下面的提示词交给当前使用的 Agent。
6
+
7
+ > 请安装 `@petercjl/product-asset-set` 提供的 Skill。
8
+
9
+ [安装说明](INSTALL.md) · [更新说明](UPDATE.md)
package/UPDATE.md ADDED
@@ -0,0 +1,5 @@
1
+ # 更新
2
+
3
+ > 请检查并更新 `@petercjl/product-asset-set` 提供的 Skill。
4
+
5
+ 当前 Agent 先定位自己的已安装副本,再从 npm 获取稳定版本。保留可恢复备份,核对包版本和资源,更新同一个实体安装,即使版本相同也检查资源是否完整。按自身机制刷新注册,验证已加载版本与文件哈希;有本地编辑时先保留编辑,再处理冲突。
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@petercjl/product-asset-set",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Build reusable product reference image sets.",
5
+ "license": "UNLICENSED",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/petercjl/product-asset-set.git"
9
+ },
10
+ "engines": {
11
+ "node": ">=22"
12
+ },
13
+ "files": [
14
+ "skills",
15
+ "INSTALL.md",
16
+ "UPDATE.md",
17
+ "scripts/verify-package.mjs"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "agentPackage": {
23
+ "kind": "application-skill",
24
+ "skills": [
25
+ {
26
+ "name": "product-asset-set",
27
+ "path": "skills/product-asset-set"
28
+ }
29
+ ],
30
+ "installation": "host-managed-physical-copy",
31
+ "installGuide": "INSTALL.md",
32
+ "updateGuide": "UPDATE.md"
33
+ },
34
+ "scripts": {
35
+ "check": "node scripts/verify-package.mjs",
36
+ "prepack": "node scripts/verify-package.mjs",
37
+ "test": "node --test test/*.test.mjs"
38
+ }
39
+ }
@@ -0,0 +1,24 @@
1
+ import assert from 'node:assert/strict';
2
+ import {readFileSync, readdirSync, lstatSync} from 'node:fs';
3
+ import {join} from 'node:path';
4
+ import {spawnSync} from 'node:child_process';
5
+
6
+ const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
7
+ const [skill] = pkg.agentPackage.skills;
8
+ assert.equal(pkg.agentPackage.skills.length, 1);
9
+ assert.equal(pkg.name, '@petercjl/' + skill.name);
10
+ assert.equal(skill.path, 'skills/' + skill.name);
11
+ assert.match(readFileSync(join(skill.path, 'SKILL.md'), 'utf8'), new RegExp('^---\\nname: ' + skill.name + '\\n'));
12
+ assert.equal(JSON.parse(readFileSync(join(skill.path, 'capabilities.json'), 'utf8')).skill, skill.name);
13
+ for (const path of ['INSTALL.md', 'UPDATE.md']) assert.ok(readFileSync(path, 'utf8').includes(pkg.name));
14
+ function verifyTree(directory) {
15
+ for (const name of readdirSync(directory)) {
16
+ const path = join(directory, name);
17
+ const stat = lstatSync(path);
18
+ assert.ok(stat.isFile() || stat.isDirectory(), 'Expected a physical file or directory: ' + path);
19
+ if (stat.isDirectory()) verifyTree(path);
20
+ }
21
+ }
22
+ verifyTree(skill.path);
23
+ const result = spawnSync(process.execPath, ['--check', join(skill.path, 'scripts', 'asset-guide.mjs')], {stdio:'inherit'});
24
+ assert.equal(result.status, 0);
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: product-asset-set
3
+ description: 从产品原图、使用场景或详情页制作可复用的独立高清产品参考图组,交付资产说明.txt,支持逐张验收、修订和定稿。
4
+ ---
5
+
6
+ # 产品资产生成
7
+
8
+ 输入是产品资料、可选的指定款色与形态基准。交付一个版本目录,包含独立高清 PNG 和一份 UTF-8 `资产说明.txt`。Agent 根据每张图的产品自然形态、拍摄角度和证据用途独立选择画幅,同一图组可同时包含横图、竖图和方图。用户明确指定的尺寸优先;与关键证据完整性冲突时说明取舍并确认。结束时提供版本目录、各图链接及用途;若用户想继续替换,提示另行提供目标场景和这套资产。
9
+
10
+ 1. **核对来源与形态。** 逐个打开实际文件,记录路径、编号、哈希和观察。每个查看结果与同次调用的文件标识绑定;文件名与描述冲突时,单独重开该文件核实。详情页查看全部商品相关区域,必要时按原像素裁切。比较轮廓、比例和可见结构,区分透视、遮挡与正常受压。如果有真实冲突或无法排除冲突,展示对应原图,等待用户指定基准对象;已有明确选择则复用。记录基准与相关素材各自的证据职责。
11
+ 2. **规划独立图片。** 至少包含一张完整全貌(`overall`)和一张有可辨参照物或人物的相对尺度(`scale`)。按新增证据规划 `structure`、`texture`、`marking` 或 `use`。同一用途优先留最清楚的一张。为每张分配稳定编号,列出画幅、目标尺寸、选择理由、证据来源、保留内容、编辑范围和验收点。先确定产品与必要参照物的自然朝向,再选择能完整容纳主体且主体占比足够的画幅。横向长条产品全貌优先评估 16:9 或 3:2,竖向产品或人物使用图优先评估 3:4、2:3 或 9:16,紧凑全貌和局部细节可用 1:1 或 4:3;按实际内容选择。产品的摆放、姿态和比例服从原始参考,避免为了适配画幅而旋转成长对角线、悬浮或改变产品形态。源图缺少必要证据时请求补图,可交付清楚标明缺口的阶段性预览。
12
+ 3. **先全貌,再逐张制作。** 优先用原照片和原人物、动作、场景。需要生成时,发现当前宿主的参考图编辑能力,核对所选画幅、服务支持的尺寸和费用,按授权每次生成一张。先对照原始基准验收全貌,再做依赖该全貌的图片。使用场景图由原图提供人物和使用关系,产品身份由基准参考提供。提示词聚焦构图和必要编辑,产品结构与材质服从参考。记录每张实际输出尺寸和比例。实际比例与计划略有偏差时,先看完整性与主体可读性;需要精确比例时仅采用保留关键证据的等比整理。逐张查看真实结果,验收身份、用途、清晰度和主体占比。每次调用记录原始输入、请求、可用的真实任务标识和结果;未知提交先查原任务。
13
+ 4. **打包、修订和定稿。** 按 [资产交接协议](references/asset-format.md) 构建说明并检查每个文件。技术验收通过后交给用户确认。局部修订创建新版本目录,复用其余已通过原件并比对哈希,只改对应编号与说明;更改形态基准时重新核对受影响图片。定稿依据为用户真实确认,记录在资产交付目录外。
14
+
15
+ 运行记录随项目保存,依次记录来源核对、图片计划、各编号的制作及验收、交付及确认。保留失败图与历史版本,展示给用户时提供每个业务阶段的结果和可打开的证据。
@@ -0,0 +1,36 @@
1
+ {
2
+ "schema": "portable-skill-adapter",
3
+ "platform": "generic",
4
+ "mappings": [
5
+ {
6
+ "capability_id": "asset.guide",
7
+ "status": "implemented",
8
+ "features": [
9
+ "verify-file-hashes",
10
+ "verify-png-dimensions"
11
+ ],
12
+ "implementation": {
13
+ "type": "node-script",
14
+ "path": "scripts/asset-guide.mjs",
15
+ "command": "check",
16
+ "runtime": "Node.js 22+"
17
+ }
18
+ },
19
+ {
20
+ "capability_id": "media.image.edit",
21
+ "status": "implemented",
22
+ "features": [
23
+ "ordered-references",
24
+ "one-image",
25
+ "local-artifact"
26
+ ],
27
+ "implementation": {
28
+ "type": "host-discovery",
29
+ "discovery": "Resolve the current host advertised image editing capability and verify required features before invocation.",
30
+ "on_missing": "CAPABILITY_UNAVAILABLE",
31
+ "on_unsupported": "FEATURE_UNSUPPORTED",
32
+ "output": "Return a local image and actual invocation or task receipt when exposed."
33
+ }
34
+ }
35
+ ]
36
+ }
@@ -0,0 +1,3 @@
1
+ interface:
2
+ display_name: "产品资产生成"
3
+ short_description: "Build reusable product reference image sets"
@@ -0,0 +1,54 @@
1
+ {
2
+ "schema": "portable-skill-capabilities",
3
+ "schema_version": "1.0.0",
4
+ "skill": "product-asset-set",
5
+ "target_platforms": [
6
+ "generic"
7
+ ],
8
+ "capabilities": [
9
+ {
10
+ "id": "asset.guide",
11
+ "purpose": "Validate the portable asset handoff",
12
+ "required": true,
13
+ "required_features": [
14
+ "verify-file-hashes",
15
+ "verify-png-dimensions"
16
+ ],
17
+ "input_fields": [
18
+ "guide"
19
+ ],
20
+ "output_fields": [
21
+ "verified-assets"
22
+ ],
23
+ "side_effects": {
24
+ "external_mutation": false,
25
+ "may_cost_money": false,
26
+ "authorization": "Local read access"
27
+ }
28
+ },
29
+ {
30
+ "id": "media.image.edit",
31
+ "purpose": "Produce one reference-conditioned image",
32
+ "required": false,
33
+ "required_features": [
34
+ "ordered-references",
35
+ "one-image",
36
+ "local-artifact"
37
+ ],
38
+ "input_fields": [
39
+ "references",
40
+ "prompt",
41
+ "aspect-ratio"
42
+ ],
43
+ "output_fields": [
44
+ "artifact",
45
+ "receipt"
46
+ ],
47
+ "side_effects": {
48
+ "external_mutation": true,
49
+ "may_cost_money": true,
50
+ "authorization": "Approved inputs, provider and generation budget"
51
+ }
52
+ }
53
+ ]
54
+ }
@@ -0,0 +1,22 @@
1
+ # 产品资产图组交接协议 v2
2
+
3
+ 版本目录包含 `资产说明.txt` 和其列出的独立 PNG。每张有稳定编号、相对文件名、角色、用途、选用条件、局限、实际像素尺寸及 SHA-256。图组至少包含全貌与尺度。每张图按内容选择自己的画幅并记录实际比例,同一图组可混用横、竖、方图。格式 v2 在每张图的说明中记录比例;读取工具同时支持格式 v1 的整套统一比例。使用新图组的 Agent 需使用支持格式 v2 的读取工具,旧读取工具遇到 v2 时应先更新。
4
+
5
+ 用 Node.js 22+ 执行 `node scripts/asset-guide.mjs check PATH/资产说明.txt`。命令返回经过文件校验的 JSON,其中包含绝对路径、编号、用途和哈希。
6
+
7
+ 制作端执行 `node scripts/asset-guide.mjs write SET_DIR PLAN.json`,对目录中已备好的 PNG 生成说明。命令拒绝覆盖现有说明,修订写到新版本目录。计划示例:
8
+
9
+ ```json
10
+ {
11
+ "product": "黄白月亮抱枕",
12
+ "version": "v1",
13
+ "images": [
14
+ {"id":"A01","file":"01-overall.png","role":"overall","ratio":"16:9","use":"完整轮廓","when":"整体替换","limits":"仅展示可见面"},
15
+ {"id":"A02","file":"02-scale.png","role":"scale","ratio":"3:4","use":"人物相对尺度","when":"抱持场景","limits":"仅相对尺度"}
16
+ ]
17
+ }
18
+ ```
19
+
20
+ 角色为 overall、scale、structure、texture、marking、use。文件名使用单段英文 ASCII 名称。校验检查尺寸及文件身份,图像内容和用途由执行者实际看图验收。
21
+
22
+ 计划中每张图可填写 ratio,填写时工具核对实际像素比例;省略时从实际图片尺寸约分得到比例。顶层 ratio 作为旧计划的统一默认值继续兼容,逐图 ratio 优先。检查输出包含每图 ratio,整套 ratio 在所有图片比例一致时返回该值,混合比例时返回 null。
@@ -0,0 +1,122 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstat, readFile, writeFile } from 'node:fs/promises';
3
+ import { dirname, join, resolve } from 'node:path';
4
+
5
+ const guideName = '资产说明.txt';
6
+ const roles = new Set(['overall', 'scale', 'structure', 'texture', 'marking', 'use']);
7
+ const pngSignature = Buffer.from('89504e470d0a1a0a', 'hex');
8
+ const labels = { role: '角色', use: '可用于', when: '选用条件', limits: '局限', size: '尺寸', hash: 'SHA-256' };
9
+
10
+ function requireValue(condition, message) {
11
+ if (!condition) throw new Error(message);
12
+ }
13
+
14
+ function line(value, label) {
15
+ requireValue(typeof value === 'string' && value === value.trim() &&
16
+ value.length && !/[\u0000-\u001f\u007f\u2028\u2029]/u.test(value), `字段无效:${label}`);
17
+ return value;
18
+ }
19
+
20
+ function ratioParts(ratio) {
21
+ requireValue(typeof ratio === 'string' && /^[1-9]\d{0,9}:[1-9]\d{0,9}$/u.test(ratio), '比例无效');
22
+ return ratio.split(':').map(Number);
23
+ }
24
+
25
+ function reducedRatio(width, height) {
26
+ requireValue(width > 0 && height > 0, '图片尺寸无效');
27
+ let divisor = width;
28
+ let remainder = height;
29
+ while (remainder) {
30
+ [divisor, remainder] = [remainder, divisor % remainder];
31
+ }
32
+ return `${width / divisor}:${height / divisor}`;
33
+ }
34
+
35
+ function parseGuide(source) {
36
+ const [header, ...blocks] = source.replace(/\r\n/g, '\n').trimEnd().split('\n\n');
37
+ const match = /^格式版本:([12])\n产品:(.+)\n资产版本:(.+)(?:\n比例:(\d+:\d+))?$/u.exec(header);
38
+ requireValue(match, '说明头部格式不符');
39
+ const [, formatVersion, product, version, ratio] = match;
40
+ requireValue(formatVersion === '1' ? Boolean(ratio) : !ratio, '说明版本与比例字段不符');
41
+ const images = blocks.map(block => {
42
+ const [title, ...fields] = block.split('\n');
43
+ const heading = /^\[(A\d{2,})\] (.+)$/u.exec(title);
44
+ requireValue(heading, `图编号格式无效:${title}`);
45
+ const [, id, file] = heading;
46
+ const entries = Object.entries(formatVersion === '2' ? { ratio: '比例', ...labels } : labels);
47
+ requireValue(fields.length === entries.length, `图片说明字段数不符:${id}`);
48
+ return { id, file, ...Object.fromEntries(entries.map(([key, label], index) => {
49
+ const prefix = label + ':';
50
+ requireValue(fields[index].startsWith(prefix), `图片字段不符:${id} ${label}`);
51
+ return [key, fields[index].slice(prefix.length)];
52
+ })) };
53
+ });
54
+ return { product, version, ratio, images };
55
+ }
56
+
57
+ async function verify(plan, root, checking) {
58
+ const product = line(plan.product, '产品');
59
+ const version = line(plan.version, '资产版本');
60
+ const defaultRatio = plan.ratio;
61
+ if (defaultRatio !== undefined) ratioParts(defaultRatio);
62
+ requireValue(Array.isArray(plan.images) && plan.images.some(image => image?.role === 'overall') &&
63
+ plan.images.some(image => image?.role === 'scale'), '完整图组需要全貌和尺度图');
64
+ const ids = new Set();
65
+ const files = new Set();
66
+ const images = [];
67
+ for (const image of plan.images) {
68
+ const { id, file, role } = image;
69
+ requireValue(typeof id === 'string' && /^A\d{2,}$/u.test(id) && !ids.has(id), `编号无效或重复:${id}`);
70
+ requireValue(typeof file === 'string' && /^[a-z0-9][a-z0-9_-]*\.png$/iu.test(file) &&
71
+ !/^(con|prn|aux|nul|com[0-9]|lpt[0-9])\.png$/iu.test(file) && !files.has(file.toLowerCase()), `文件名无效或重复:${file}`);
72
+ requireValue(roles.has(role), `角色无效:${id}`);
73
+ ids.add(id);
74
+ files.add(file.toLowerCase());
75
+ const use = line(image.use, '可用于');
76
+ const when = line(image.when, '选用条件');
77
+ const limits = line(image.limits, '局限');
78
+ const path = join(root, file);
79
+ requireValue((await lstat(path)).isFile(), `需要实体图片文件:${file}`);
80
+ const bytes = await readFile(path);
81
+ requireValue(bytes.length >= 45 && bytes.subarray(0, 8).equals(pngSignature) &&
82
+ bytes.readUInt32BE(8) === 13 && bytes.toString('ascii', 12, 16) === 'IHDR' &&
83
+ bytes.subarray(-12).equals(Buffer.from('0000000049454e44ae426082', 'hex')), `PNG 文件头或结束块无效:${file}`);
84
+ const width = bytes.readUInt32BE(16);
85
+ const height = bytes.readUInt32BE(20);
86
+ const ratio = image.ratio ?? defaultRatio ?? reducedRatio(width, height);
87
+ const [rw, rh] = ratioParts(ratio);
88
+ requireValue(width > 0 && height > 0 && BigInt(width) * BigInt(rh) === BigInt(height) * BigInt(rw), `图片比例不符:${file} ${width}x${height}`);
89
+ const size = `${width}x${height}`;
90
+ const hash = createHash('sha256').update(bytes).digest('hex');
91
+ requireValue(!checking || (size === image.size && hash === image.hash), `尺寸或哈希不符:${file}`);
92
+ images.push({ id, file, path, ratio, role, use, when, limits, size, hash });
93
+ }
94
+ return { product, version, ratio: images.every(image => image.ratio === images[0].ratio) ? images[0].ratio : null, images };
95
+ }
96
+
97
+ function formatGuide({ product, version, images }) {
98
+ const header = `格式版本:2\n产品:${product}\n资产版本:${version}`;
99
+ const blocks = images.map(image => [
100
+ `[${image.id}] ${image.file}`,
101
+ ...Object.entries({ ratio: '比例', ...labels }).map(([key, label]) => `${label}:${image[key]}`),
102
+ ].join('\n'));
103
+ return [header, ...blocks].join('\n\n') + '\n';
104
+ }
105
+
106
+ async function main([command, location, planPath, ...extra]) {
107
+ const checking = command === 'check';
108
+ requireValue(location && !extra.length && ((checking && !planPath) ||
109
+ (command === 'write' && planPath)), '用法:check GUIDE 或 write SET_DIR PLAN.json');
110
+ const target = checking ? resolve(location) : join(resolve(location), guideName);
111
+ if (checking) requireValue((await lstat(target)).isFile(), '说明需为实体文件');
112
+ const source = new TextDecoder('utf-8', { fatal: true }).decode(await readFile(checking ? target : planPath));
113
+ const plan = checking ? parseGuide(source) : JSON.parse(source);
114
+ const result = await verify(plan, dirname(target), checking);
115
+ if (!checking) await writeFile(target, formatGuide(result), { encoding: 'utf8', flag: 'wx' });
116
+ process.stdout.write(JSON.stringify({ ok: true, guide: target, ...result }, null, 2) + '\n');
117
+ }
118
+
119
+ main(process.argv.slice(2)).catch(error => {
120
+ process.stderr.write(JSON.stringify({ ok: false, error: error.message }) + '\n');
121
+ process.exitCode = 1;
122
+ });