android2harmony 0.1.5 → 0.1.6
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/agents/self-tester.md +33 -354
- package/dist/index.js +172 -76
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/skills/a2h-resource-convert/SKILL.md +36 -7
- package/skills/a2h-resource-convert/scripts/a2h_resource_convert.js +20 -0
- package/skills/a2h-resource-convert/scripts/app_identity.js +741 -0
- package/skills/a2h-ui-transfer/SKILL.md +14 -3
- package/skills/a2h-ui-transfer/references/conversion-procedure.md +5 -30
- package/skills/a2h-ui-transfer/scripts/android_parse_fast.js +137 -20
- package/skills/hmos-fix-build-errors/SKILL.md +1 -1
- package/skills/hmos-incremental-ui-align/README.md +251 -251
- package/skills/hmos-incremental-ui-align/SKILL.md +364 -364
- package/skills/hmos-integration-test/README.md +341 -0
- package/skills/hmos-integration-test/SKILL.md +446 -0
- package/skills/hmos-integration-test/scripts/report-tool.mjs +646 -0
- package/skills/hmos-integration-test/scripts/resolve-metadata-tool.mjs +147 -0
- package/skills/hmos-integration-test/scripts/self-test-runner.mjs +1006 -0
- package/skills/hmos-integration-test/scripts/testcases-tool.mjs +189 -0
- package/skills/hmos-spec-generate/SKILL.md +26 -24
- package/skills/hmos-spec-generate/scripts/parse_requirements.ts +515 -0
- package/skills/hmos-spec-generate/template/REQ.txt +22 -0
- package/skills/hmos-spec-generate/template/REQ.xlsx +0 -0
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
2
|
+
import path6 from "node:path";
|
|
3
3
|
|
|
4
4
|
// src/paths.ts
|
|
5
5
|
import path from "node:path";
|
|
@@ -70,22 +70,11 @@ async function buildA2HEnvBase(deps) {
|
|
|
70
70
|
return env;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
// src/tools/a2h-
|
|
74
|
-
import { spawn } from "node:child_process";
|
|
73
|
+
// src/tools/a2h-app-feature-verify.ts
|
|
75
74
|
import path4 from "node:path";
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
path: path4.join(
|
|
80
|
-
PKG_ROOT,
|
|
81
|
-
"skills",
|
|
82
|
-
"hmos-incremental-ui-align",
|
|
83
|
-
"scripts",
|
|
84
|
-
"app_feature_verify.js"
|
|
85
|
-
),
|
|
86
|
-
argsHelp: 'UI \u5BFB\u8DEF phone-agent:\u5728 HarmonyOS(hdc)\u6216 Android(adb)\u5DF2\u8FDE\u63A5\u8BBE\u5907\u4E0A,\u6309\u8DEF\u5F84/\u63D0\u793A\u5BFC\u822A\u5230\u76EE\u6807\u9875\u9762\u5E76\u622A\u56FE+\u6A21\u578B\u5224\u5B9A\u662F\u5426\u5230\u8FBE\u3002\n\n| \u5B57\u6BB5 | \u7C7B\u578B | \u7EA6\u675F | \u8BF4\u660E |\n|---|---|---|---|\n| app | string | \u5FC5\u586B | \u76EE\u6807\u5E94\u7528\u663E\u793A\u540D |\n| package | string | \u5FC5\u586B | \u76EE\u6807\u5E94\u7528\u5305\u540D |\n| device | string | \u9ED8\u8BA4 hdc | \u8BBE\u5907\u9A71\u52A8,\u53D6\u503C hdc \u6216 adb |\n| prompt | string | \u4E09\u9009\u4E00 | \u81EA\u7531\u5BFC\u822A\u63CF\u8FF0 |\n| task | string | \u4E09\u9009\u4E00 | L1\u2026>L2\u2026>L3\u2026 \u5C42\u7EA7\u8DEF\u5F84 |\n| feature | string | \u4E09\u9009\u4E00 | features.md \u8DEF\u5F84 |\n| maxSteps | number | \u9ED8\u8BA4 30 | \u6700\u5927 agent \u6B65\u6570 |\n| serial | string | \u53EF\u9009 | \u8BBE\u5907\u5E8F\u5217\u53F7(\u591A\u8BBE\u5907\u65F6) |\n| appHints | string | \u53EF\u9009 | \u5E94\u7528\u5BFC\u822A\u63D0\u793A |\n| limit | number | \u53EF\u9009 | --feature \u6A21\u5F0F\u53D6\u524D N \u6761 |\n| offset | number | \u9ED8\u8BA4 1 | --feature \u6A21\u5F0F\u504F\u79FB |\n| quiet | boolean | \u53EF\u9009 | true \u65F6\u6291\u5236\u63D0\u793A\u6027\u8F93\u51FA |\n\n\u7EA6\u675F:app \u4E0E package \u5FC5\u586B,\u4E14 prompt/task/feature \u4E09\u9009\u4E00\u3002\n\u793A\u4F8B:commandArgs = {"app":"\u56FE\u5E93","package":"com.x.gallery","device":"adb","prompt":"\u8FDB\u5165\u8BBE\u7F6E\u9875\u9762","maxSteps":15}'
|
|
87
|
-
}
|
|
88
|
-
];
|
|
75
|
+
|
|
76
|
+
// src/tools/spawn-helpers.ts
|
|
77
|
+
import { spawn } from "node:child_process";
|
|
89
78
|
function cap(s, max = 5e4) {
|
|
90
79
|
if (s.length <= max) return s;
|
|
91
80
|
const half = Math.floor(max / 2);
|
|
@@ -93,17 +82,24 @@ function cap(s, max = 5e4) {
|
|
|
93
82
|
\u2026[truncated ${s.length - max} chars]\u2026
|
|
94
83
|
` + s.slice(-half);
|
|
95
84
|
}
|
|
96
|
-
async function runModelScript(deps, context,
|
|
85
|
+
async function runModelScript(deps, context, scriptPath, argv, opts = {}) {
|
|
86
|
+
const label = opts.label ?? scriptPath;
|
|
97
87
|
const model = await deps.resolveModelParams(context.worktree);
|
|
98
88
|
if (!model || !model.apiKey) {
|
|
99
89
|
throw new Error(
|
|
100
|
-
`
|
|
90
|
+
`a2h (${label}): host did not provide model params (apiKey missing)`
|
|
101
91
|
);
|
|
102
92
|
}
|
|
103
93
|
const finalArgv = [...argv, "--model-stdin"];
|
|
104
|
-
const child = spawn("node", [
|
|
94
|
+
const child = spawn("node", [scriptPath, ...finalArgv], {
|
|
105
95
|
cwd: context.directory,
|
|
106
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
96
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
97
|
+
// DevEco Code is a GUI host with no attached console; without windowsHide,
|
|
98
|
+
// Windows allocates a new console window for the child — visible as a
|
|
99
|
+
// flash on every a2h_self_test start and on every a2h_self_test_status
|
|
100
|
+
// poll, and as a persistent window during a2h_app_feature_verify's long
|
|
101
|
+
// sync run. Hide it.
|
|
102
|
+
windowsHide: true
|
|
107
103
|
});
|
|
108
104
|
child.stdin.end(
|
|
109
105
|
JSON.stringify({ apiKey: model.apiKey, modelName: model.modelName, baseURL: model.baseURL })
|
|
@@ -141,22 +137,65 @@ async function runModelScript(deps, context, entry, argv) {
|
|
|
141
137
|
});
|
|
142
138
|
});
|
|
143
139
|
context.abort.removeEventListener("abort", onAbort);
|
|
144
|
-
if (code !== 0) {
|
|
140
|
+
if (code !== 0 && opts.throwOnNonZero !== false) {
|
|
145
141
|
if (spawnError) {
|
|
146
|
-
throw new Error(`
|
|
142
|
+
throw new Error(`a2h (${label}): failed to spawn node \u2014 ${spawnError}`);
|
|
147
143
|
}
|
|
148
144
|
const tail = stderr.slice(-2e3);
|
|
149
145
|
throw new Error(
|
|
150
|
-
`
|
|
146
|
+
`a2h (${label}) failed (exit ${code}): ${tail || "(no stderr)"}`
|
|
151
147
|
);
|
|
152
148
|
}
|
|
153
149
|
return { ok: true, exitCode: code, output: cap(stdout) };
|
|
154
150
|
}
|
|
151
|
+
async function runScriptCollect(scriptPath, argv, context, label = scriptPath) {
|
|
152
|
+
const child = spawn("node", [scriptPath, ...argv], {
|
|
153
|
+
cwd: context.directory,
|
|
154
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
155
|
+
// Hide the console window — this runs on every a2h_self_test_status poll,
|
|
156
|
+
// so without it the host machine flashes a cmd window repeatedly.
|
|
157
|
+
windowsHide: true
|
|
158
|
+
});
|
|
159
|
+
let stdout = "";
|
|
160
|
+
let stderr = "";
|
|
161
|
+
let spawnError = "";
|
|
162
|
+
child.stdout.on("data", (chunk) => {
|
|
163
|
+
stdout += chunk.toString("utf8");
|
|
164
|
+
});
|
|
165
|
+
child.stderr.on("data", (chunk) => {
|
|
166
|
+
stderr += chunk.toString("utf8");
|
|
167
|
+
});
|
|
168
|
+
const onAbort = () => {
|
|
169
|
+
if (!child.killed) child.kill("SIGTERM");
|
|
170
|
+
};
|
|
171
|
+
context.abort.addEventListener("abort", onAbort);
|
|
172
|
+
const code = await new Promise((resolve) => {
|
|
173
|
+
child.on("close", resolve);
|
|
174
|
+
child.on("error", (err) => {
|
|
175
|
+
spawnError = err.message;
|
|
176
|
+
resolve(-1);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
context.abort.removeEventListener("abort", onAbort);
|
|
180
|
+
if (spawnError) {
|
|
181
|
+
throw new Error(`a2h (${label}): failed to spawn node \u2014 ${spawnError}`);
|
|
182
|
+
}
|
|
183
|
+
return { ok: true, exitCode: code, output: cap(stdout) };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/tools/a2h-app-feature-verify.ts
|
|
187
|
+
var APP_FEATURE_VERIFY_PATH = path4.join(
|
|
188
|
+
PKG_ROOT,
|
|
189
|
+
"skills",
|
|
190
|
+
"hmos-incremental-ui-align",
|
|
191
|
+
"scripts",
|
|
192
|
+
"app_feature_verify.js"
|
|
193
|
+
);
|
|
155
194
|
var kebab = (s) => "--" + s.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
156
195
|
function flattenParams(obj) {
|
|
157
196
|
const out = [];
|
|
158
197
|
for (const [k, v] of Object.entries(obj)) {
|
|
159
|
-
if (v == null) continue;
|
|
198
|
+
if (v == null || v === "") continue;
|
|
160
199
|
if (typeof v === "boolean") {
|
|
161
200
|
if (v) out.push(kebab(k));
|
|
162
201
|
} else {
|
|
@@ -165,54 +204,106 @@ function flattenParams(obj) {
|
|
|
165
204
|
}
|
|
166
205
|
return out;
|
|
167
206
|
}
|
|
168
|
-
|
|
169
|
-
function makeA2hRunModelScriptTool(deps) {
|
|
207
|
+
function makeA2hAppFeatureVerifyTool(deps) {
|
|
170
208
|
return {
|
|
171
|
-
description:
|
|
209
|
+
description: "UI \u5BFB\u8DEF phone-agent:\u5728 HarmonyOS(hdc)\u6216 Android(adb)\u5DF2\u8FDE\u63A5\u8BBE\u5907\u4E0A,\u6309\u8DEF\u5F84/\u63D0\u793A\u5BFC\u822A\u5230\u76EE\u6807\u9875\u9762\u5E76\u622A\u56FE + \u6A21\u578B\u5224\u5B9A\u662F\u5426\u5230\u8FBE\u3002\u6A21\u578B\u914D\u7F6E(apiKey \u7B49)\u7531\u5DE5\u5177\u5185\u90E8\u7ECF stdin \u6CE8\u5165,\u4E0D\u8981\u4F20 apiKey/modelStdin \u5B57\u6BB5\u3002\n\n| \u5B57\u6BB5 | \u7C7B\u578B | \u7EA6\u675F | \u8BF4\u660E |\n|---|---|---|---|\n| app | string | \u5FC5\u586B | \u76EE\u6807\u5E94\u7528\u663E\u793A\u540D |\n| package | string | \u5FC5\u586B | \u76EE\u6807\u5E94\u7528\u5305\u540D |\n| device | string | \u53EF\u9009(\u9ED8\u8BA4 hdc) | \u8BBE\u5907\u9A71\u52A8,\u53D6\u503C hdc \u6216 adb |\n| prompt | string | \u4E09\u9009\u4E00 | \u81EA\u7531\u5BFC\u822A\u63CF\u8FF0 |\n| task | string | \u4E09\u9009\u4E00 | L1>L2>L3 \u5C42\u7EA7\u8DEF\u5F84 |\n| feature | string | \u4E09\u9009\u4E00 | features.md \u8DEF\u5F84(\u987B .md \u7ED3\u5C3E) |\n| maxSteps | number | \u53EF\u9009(\u9ED8\u8BA4 30) | \u6700\u5927 agent \u6B65\u6570 |\n| serial | string | \u53EF\u9009 | \u8BBE\u5907\u5E8F\u5217\u53F7(\u591A\u8BBE\u5907\u65F6) |\n| appHints | string | \u53EF\u9009 | \u5E94\u7528\u5BFC\u822A\u63D0\u793A |\n| limit | number | \u53EF\u9009 | --feature \u6A21\u5F0F\u53D6\u524D N \u6761 |\n| offset | number | \u53EF\u9009(\u9ED8\u8BA4 1) | --feature \u6A21\u5F0F\u504F\u79FB |\n| quiet | boolean | \u53EF\u9009 | true \u65F6\u6291\u5236\u63D0\u793A\u6027\u8F93\u51FA |\n\n\u7EA6\u675F:app \u4E0E package \u5FC5\u586B,\u4E14 prompt/task/feature \u4E09\u9009\u4E00(\u4F20\u4E00\u4E2A)\u3002",
|
|
172
210
|
// `args` is a flat field-name→field-schema MAP (not a full `{type:"object",
|
|
173
211
|
// properties, required}` schema object). The host's `legacyJsonSchema` reads
|
|
174
212
|
// this map and builds the LLM-facing schema itself: `{type:"object",
|
|
175
|
-
// properties:{
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
213
|
+
// properties:{...}, required:[...every field...]}` (it marks every field
|
|
214
|
+
// required). So optional fields MUST be nullable (`["string","null"]` etc.)
|
|
215
|
+
// -- the agent passes null (or "") and `flattenParams` drops it, letting the
|
|
216
|
+
// script's own default apply. Declaring the full-schema wrapper here would
|
|
217
|
+
// make `legacyJsonSchema` mis-read `properties` as a literal field name. Keep
|
|
218
|
+
// it a field-map.
|
|
180
219
|
args: {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
},
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
220
|
+
app: { type: "string", description: "\u76EE\u6807\u5E94\u7528\u663E\u793A\u540D" },
|
|
221
|
+
package: { type: "string", description: "\u76EE\u6807\u5E94\u7528\u5305\u540D" },
|
|
222
|
+
device: { type: ["string", "null"], description: "\u8BBE\u5907\u9A71\u52A8 hdc \u6216 adb,\u53EF\u9009;\u9ED8\u8BA4 hdc" },
|
|
223
|
+
prompt: { type: ["string", "null"], description: "\u81EA\u7531\u5BFC\u822A\u63CF\u8FF0(\u4E0E task/feature \u4E09\u9009\u4E00)" },
|
|
224
|
+
task: { type: ["string", "null"], description: "L1>L2>L3 \u5C42\u7EA7\u8DEF\u5F84(\u4E0E prompt/feature \u4E09\u9009\u4E00)" },
|
|
225
|
+
feature: { type: ["string", "null"], description: "features.md \u8DEF\u5F84,\u987B .md \u7ED3\u5C3E(\u4E0E prompt/task \u4E09\u9009\u4E00)" },
|
|
226
|
+
maxSteps: { type: ["number", "null"], description: "\u6700\u5927 agent \u6B65\u6570,\u53EF\u9009;\u9ED8\u8BA4 30" },
|
|
227
|
+
serial: { type: ["string", "null"], description: "\u8BBE\u5907\u5E8F\u5217\u53F7(\u591A\u8BBE\u5907\u65F6),\u53EF\u9009" },
|
|
228
|
+
appHints: { type: ["string", "null"], description: "\u5E94\u7528\u5BFC\u822A\u63D0\u793A,\u53EF\u9009" },
|
|
229
|
+
limit: { type: ["number", "null"], description: "--feature \u6A21\u5F0F\u53D6\u524D N \u6761,\u53EF\u9009" },
|
|
230
|
+
offset: { type: ["number", "null"], description: "--feature \u6A21\u5F0F\u504F\u79FB,\u53EF\u9009;\u9ED8\u8BA4 1" },
|
|
231
|
+
quiet: { type: ["boolean", "null"], description: "true \u65F6\u6291\u5236\u63D0\u793A\u6027\u8F93\u51FA,\u53EF\u9009" }
|
|
190
232
|
},
|
|
191
233
|
execute: async (args, context) => {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
if (!entry) {
|
|
234
|
+
const params = { ...args };
|
|
235
|
+
if (params.prompt == null && params.task == null && params.feature == null) {
|
|
195
236
|
throw new Error(
|
|
196
|
-
|
|
237
|
+
"a2h_app_feature_verify: must pass at least one of prompt / task / feature"
|
|
197
238
|
);
|
|
198
239
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
240
|
+
return runModelScript(deps, context, APP_FEATURE_VERIFY_PATH, flattenParams(params), {
|
|
241
|
+
label: "app_feature_verify"
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/tools/a2h-self-test.ts
|
|
248
|
+
import path5 from "node:path";
|
|
249
|
+
var SELF_TEST_RUNNER = path5.join(
|
|
250
|
+
PKG_ROOT,
|
|
251
|
+
"skills",
|
|
252
|
+
"hmos-integration-test",
|
|
253
|
+
"scripts",
|
|
254
|
+
"self-test-runner.mjs"
|
|
255
|
+
);
|
|
256
|
+
function makeA2hSelfTestTool(deps) {
|
|
257
|
+
return {
|
|
258
|
+
description: '\u542F\u52A8 HarmonyOS \u5E94\u7528\u81EA\u6D4B:\u5B89\u88C5 HAP,spawn AutoTest batch_runner(detached) \u540E\u7ACB\u5373\u8FD4\u56DE RUNNING + task_subdir,\u4E0D\u963B\u585E\u7B49\u5F85\u5B8C\u6210\u3002\u7528 a2h_self_test_status \u8F6E\u8BE2\u7EC8\u6001,\u8D85\u65F6\u5219\u7528 Bash kill \u6389\u8FD4\u56DE\u7684 pid\u3002\u6A21\u578B\u914D\u7F6E(apiKey \u7B49)\u7531\u5DE5\u5177\u5185\u90E8\u7ECF stdin \u6CE8\u5165,\u4E0D\u8981\u4F20 apiKey/modelStdin \u5B57\u6BB5\u3002\n\n| \u5B57\u6BB5 | \u7C7B\u578B | \u7EA6\u675F | \u8BF4\u660E |\n|---|---|---|---|\n| testcases | string | \u5FC5\u586B | testcases.json \u8DEF\u5F84 |\n| hap | string | \u5FC5\u586B | \u7B7E\u540D\u5305(.hap/.hsp \u6587\u4EF6/\u76EE\u5F55,\u9017\u53F7\u5206\u9694\u591A\u5305) |\n| bundleName | string | \u5FC5\u586B | \u5E94\u7528\u5305\u540D |\n| taskDir | string | \u5FC5\u586B | \u4EFB\u52A1\u8F93\u51FA\u76EE\u5F55(\u901A\u5E38 <output>/task) |\n| outputDir | string | \u53EF\u9009 | \u65E5\u5FD7\u76EE\u5F55(\u901A\u5E38 <output>);\u4E0D\u4F20\u5219\u7528 taskDir |\n| category | string | \u53EF\u9009 | \u6D4B\u8BD5\u5206\u7C7B\u540D(\u9ED8\u8BA4 self_test,\u901A\u5E38\u4F20 app_name) |\n| mode | string | \u53EF\u9009 | single(\u9ED8\u8BA4)\u6216 layered(Planner+Executor \u53CC Agent) |\n\n\u8FD4\u56DE {ok, exitCode, output},output \u4E3A self-test-runner stdout JSON\u3002\u6B63\u5E38\u4E3A {status:"RUNNING", pid, task_dir, task_subdir}(\u5B58 pid \u4E0E task_subdir \u4F9B\u8F6E\u8BE2\u4E0E\u8D85\u65F6 kill);\u73AF\u5883\u5931\u8D25\u65F6\u4E3A {status:"FAILED", error}(\u5982 @autotest/agent \u81EA\u52A8\u5B89\u88C5\u5931\u8D25\u6216 HAP \u5B89\u88C5/\u7528\u4F8B\u51C6\u5907\u629B\u9519)\u2014\u2014\u6B64\u65F6\u4E0D\u8981\u8F6E\u8BE2,\u76F4\u63A5\u628A error \u62A5\u7ED9\u7528\u6237\u5E76\u505C\u6B62\u3002\u5DE5\u5177\u5BF9 FAILED \u4E0D\u629B\u9519(\u975E\u96F6\u9000\u51FA\u7801 = FAILED),\u9700\u89E3\u6790 output \u6309 status \u5206\u652F\u3002',
|
|
259
|
+
args: {
|
|
260
|
+
testcases: { type: "string", description: "testcases.json \u8DEF\u5F84" },
|
|
261
|
+
hap: { type: "string", description: "\u7B7E\u540D\u5305\u8DEF\u5F84(.hap/.hsp \u6587\u4EF6/\u76EE\u5F55,\u9017\u53F7\u5206\u9694)" },
|
|
262
|
+
bundleName: { type: "string", description: "\u5E94\u7528\u5305\u540D" },
|
|
263
|
+
taskDir: { type: "string", description: "\u4EFB\u52A1\u8F93\u51FA\u76EE\u5F55,\u901A\u5E38 <output>/task" },
|
|
264
|
+
outputDir: { type: ["string", "null"], description: "\u65E5\u5FD7\u76EE\u5F55,\u53EF\u9009;\u4E0D\u4F20\u5219\u7528 taskDir" },
|
|
265
|
+
category: { type: ["string", "null"], description: "\u6D4B\u8BD5\u5206\u7C7B\u540D,\u53EF\u9009;\u9ED8\u8BA4 self_test,\u901A\u5E38\u4F20 app_name" },
|
|
266
|
+
mode: { type: ["string", "null"], description: "single \u6216 layered,\u53EF\u9009;\u9ED8\u8BA4 single" }
|
|
267
|
+
},
|
|
268
|
+
execute: async (args, context) => {
|
|
269
|
+
const argv = [];
|
|
270
|
+
const push = (flag, v) => {
|
|
271
|
+
if (v == null || v === "") return;
|
|
272
|
+
argv.push(flag, String(v));
|
|
273
|
+
};
|
|
274
|
+
push("--testcases", args.testcases);
|
|
275
|
+
push("--hap", args.hap);
|
|
276
|
+
push("--bundle-name", args.bundleName);
|
|
277
|
+
push("--task-dir", args.taskDir);
|
|
278
|
+
push("--output-dir", args.outputDir);
|
|
279
|
+
push("--category", args.category);
|
|
280
|
+
push("--mode", args.mode);
|
|
281
|
+
return runModelScript(deps, context, SELF_TEST_RUNNER, argv, {
|
|
282
|
+
label: "self_test",
|
|
283
|
+
// Non-zero exit = a FAILED start (failJson prints {status:"FAILED",error}
|
|
284
|
+
// and exits 1). Return it as output -- not throw -- so the agent can read
|
|
285
|
+
// the structured error; the description contract tells it to branch on
|
|
286
|
+
// status instead of polling. (Normal RUNNING start also exits 0.)
|
|
287
|
+
throwOnNonZero: false
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/tools/a2h-self-test-status.ts
|
|
294
|
+
function makeA2hSelfTestStatusTool() {
|
|
295
|
+
return {
|
|
296
|
+
description: '\u67E5\u8BE2 a2h_self_test \u542F\u52A8\u7684\u81EA\u6D4B\u6267\u884C\u72B6\u6001(\u8BFB summary.json / task_results.jsonl / \u65E5\u5FD7 mtime)\u3002\u4E0D\u9700\u8981 apiKey\u3002\u53CD\u590D\u8C03\u7528\u76F4\u5230\u7EC8\u6001(COMPLETED/CRASHED/NOT_STARTED),RUNNING \u5219\u95F4\u9694\u540E\u91CD\u8BD5\u3002\n\n| \u5B57\u6BB5 | \u7C7B\u578B | \u7EA6\u675F | \u8BF4\u660E |\n|---|---|---|---|\n| taskDir | string | \u5FC5\u586B | a2h_self_test \u8FD4\u56DE\u7684 task_dir |\n| outputDir | string | \u53EF\u9009 | \u65E5\u5FD7\u76EE\u5F55(\u540C a2h_self_test \u7684 outputDir) |\n\n\u8FD4\u56DE {ok, exitCode, output},output \u4E3A\u72B6\u6001 JSON:\n- COMPLETED(exit 0):{status, task_subdir, cases_done, pass_count, fail_count, unknown_count, pass_rate, log_tail}\n- RUNNING(exit 2):{status, cases_done, last_case, task_subdir, log_tail}\n- CRASHED(exit 3):\u540C RUNNING \u5F62\u72B6\n- NOT_STARTED(exit 4):{status:"NOT_STARTED"}\nCOMPLETED/CRASHED \u65F6 runner \u4F1A\u5220\u9664\u4E34\u65F6 autotest.yaml(\u542B key)\u3002',
|
|
297
|
+
args: {
|
|
298
|
+
taskDir: { type: "string", description: "a2h_self_test \u8FD4\u56DE\u7684 task_dir" },
|
|
299
|
+
outputDir: { type: ["string", "null"], description: "\u65E5\u5FD7\u76EE\u5F55,\u53EF\u9009" }
|
|
300
|
+
},
|
|
301
|
+
execute: async (args, context) => {
|
|
302
|
+
const argv = ["--status", "--task-dir", String(args.taskDir)];
|
|
303
|
+
if (args.outputDir != null && args.outputDir !== "") {
|
|
304
|
+
argv.push("--output-dir", String(args.outputDir));
|
|
214
305
|
}
|
|
215
|
-
return
|
|
306
|
+
return runScriptCollect(SELF_TEST_RUNNER, argv, context, "self_test_status");
|
|
216
307
|
}
|
|
217
308
|
};
|
|
218
309
|
}
|
|
@@ -220,8 +311,8 @@ function makeA2hRunModelScriptTool(deps) {
|
|
|
220
311
|
// src/index.ts
|
|
221
312
|
function createServer(deps) {
|
|
222
313
|
return async (input) => {
|
|
223
|
-
const skillsRoot =
|
|
224
|
-
const agentsRoot =
|
|
314
|
+
const skillsRoot = path6.join(PKG_ROOT, "skills");
|
|
315
|
+
const agentsRoot = path6.join(PKG_ROOT, "agents");
|
|
225
316
|
const agentPrompts = discoverA2HAgents(agentsRoot);
|
|
226
317
|
return {
|
|
227
318
|
dispose: async () => {
|
|
@@ -250,27 +341,32 @@ function createServer(deps) {
|
|
|
250
341
|
},
|
|
251
342
|
// ── shell.env hook: inject DevEco Studio / SDK path vars only. The ──
|
|
252
343
|
// multimodal model apiKey is deliberately NOT put in env (it would leak
|
|
253
|
-
// to every adb/hdc descendant); the
|
|
344
|
+
// to every adb/hdc descendant); the a2h model-script tools pipe it to
|
|
254
345
|
// the script over stdin instead. See src/env.ts.
|
|
255
346
|
"shell.env": async (shellInput, output) => {
|
|
256
347
|
const a2hEnv = await buildA2HEnvBase(deps);
|
|
257
348
|
Object.assign(output.env, a2hEnv);
|
|
258
349
|
},
|
|
259
350
|
// ── tool hook: run skill scripts that need the multimodal model without
|
|
260
|
-
// leaking the apiKey into env/argv.
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
// `
|
|
264
|
-
//
|
|
265
|
-
//
|
|
351
|
+
// leaking the apiKey into env/argv. Three fixed-schema tools, each
|
|
352
|
+
// resolving the apiKey from the host and piping it over stdin
|
|
353
|
+
// (`--model-stdin`) — never env/argv, so adb/hdc descendants can't see it:
|
|
354
|
+
// • `a2h_app_feature_verify` (src/tools/a2h-app-feature-verify.ts) —
|
|
355
|
+
// phone-agent UI navigation + screenshot + model verification
|
|
356
|
+
// (app_feature_verify.js). Synchronous run; throws on non-zero exit.
|
|
357
|
+
// • `a2h_self_test` (src/tools/a2h-self-test.ts) +
|
|
358
|
+
// `a2h_self_test_status` (src/tools/a2h-self-test-status.ts) — the
|
|
359
|
+
// integration-test flow. The former spawns self-test-runner.mjs
|
|
360
|
+
// which spawns batch_runner detached and returns RUNNING immediately
|
|
361
|
+
// (no blocking); the latter polls it via `--status` (no key needed).
|
|
266
362
|
//
|
|
267
|
-
// The AutoTest engine (
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
// "Resolving the AutoTest Tools Directory" section). a2h therefore ships
|
|
271
|
-
// only this one plugin tool and carries no autotest runtime dependency.
|
|
363
|
+
// The AutoTest engine (batch_runner / testcases / report / resolve-metadata)
|
|
364
|
+
// lives in @autotest/agent + the HomeTrans package, invoked via `node`;
|
|
365
|
+
// a2h carries no autotest runtime dependency.
|
|
272
366
|
tool: {
|
|
273
|
-
|
|
367
|
+
a2h_app_feature_verify: makeA2hAppFeatureVerifyTool(deps),
|
|
368
|
+
a2h_self_test: makeA2hSelfTestTool(deps),
|
|
369
|
+
a2h_self_test_status: makeA2hSelfTestStatusTool()
|
|
274
370
|
}
|
|
275
371
|
};
|
|
276
372
|
};
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/index.ts", "../src/paths.ts", "../src/discovery.ts", "../src/env.ts", "../src/tools/a2h-
|
|
4
|
-
"sourcesContent": ["import path from \"node:path\"\r\nimport type { A2HHostDeps, Plugin, PluginInput, PluginHooks } from \"./types\"\r\nimport { PKG_ROOT } from \"./paths\"\r\nimport { discoverA2HAgents } from \"./discovery\"\r\nimport { buildA2HEnvBase } from \"./env\"\r\nimport { makeA2hRunModelScriptTool } from \"./tools/a2h-run-model-script\"\r\n\r\n// Public re-exports \u2014 the package's external surface (dev eco code loads\r\n// dist/index.js at runtime and calls `createServer`; `A2HHostDeps` /\r\n// `PLUGIN_ID` are kept exported for any TS consumers).\r\nexport type { A2HHostDeps } from \"./types\"\r\nexport { PLUGIN_ID } from \"./paths\"\r\n\r\n/**\r\n * Create the A2H DevEco Code plugin.\r\n *\r\n * This is a self-contained plugin - skills/, agents/, and tools/\r\n * are bundled inside the package, NOT referenced from HomeTrans.\r\n * A2H-Plugin is the independently publishable \"plugin edition\" of HomeTrans.\r\n *\r\n * @param deps - host dependencies injected by DevEco Code's thin bridge.\r\n * The A2H Plugin defines what it needs (A2HHostDeps);\r\n * DevEco Code provides how to resolve it.\r\n * @returns A Plugin function ready to be registered as a built-in plugin.\r\n */\r\nexport function createServer(deps: A2HHostDeps): Plugin {\r\n return async (input: PluginInput): Promise<PluginHooks> => {\r\n // All A2H assets live inside this package (self-contained, no HomeTrans dep)\r\n const skillsRoot = path.join(PKG_ROOT, \"skills\")\r\n const agentsRoot = path.join(PKG_ROOT, \"agents\")\r\n\r\n // Auto-discover agents: scan top-level agents/*.md, parse `name:` from\r\n // frontmatter, strip frontmatter for prompt body. Adding/removing/renaming\r\n // an agent is just a file drop \u2014 no code change, no rebuild.\r\n const agentPrompts = discoverA2HAgents(agentsRoot)\r\n\r\n return {\r\n dispose: async () => {\r\n agentPrompts.clear()\r\n },\r\n\r\n // \u2500\u2500 config hook: register skills search paths and agents \u2500\u2500\r\n config: async (cfg: Record<string, unknown>) => {\r\n // --- Skills ---\r\n const skills = (cfg.skills as Record<string, unknown>) || {}\r\n cfg.skills = skills\r\n\r\n // Register the bundled skills/ directory as an additional search path.\r\n // All bundled skills are user-visible \u2014 there is no partial-visibility\r\n // (hidden) layer anymore, so we only register the search path here.\r\n const paths = (skills.paths as string[]) || []\r\n skills.paths = paths\r\n if (!paths.includes(skillsRoot)) {\r\n paths.push(skillsRoot)\r\n }\r\n\r\n // --- Agents ---\r\n const agent = (cfg.agent as Record<string, Record<string, unknown>>) || {}\r\n cfg.agent = agent\r\n\r\n for (const [name, prompt] of agentPrompts) {\r\n // Don't overwrite if the user already configured this agent\r\n if (!agent[name]) {\r\n agent[name] = {\r\n mode: \"subagent\",\r\n hidden: true,\r\n prompt,\r\n }\r\n }\r\n }\r\n\r\n },\r\n\r\n // \u2500\u2500 shell.env hook: inject DevEco Studio / SDK path vars only. The \u2500\u2500\r\n // multimodal model apiKey is deliberately NOT put in env (it would leak\r\n // to every adb/hdc descendant); the a2h_run_model_script tool pipes it to\r\n // the script over stdin instead. See src/env.ts.\r\n \"shell.env\": async (\r\n shellInput: { cwd: string; sessionID?: string; callID?: string },\r\n output: { env: Record<string, string> },\r\n ) => {\r\n const a2hEnv = await buildA2HEnvBase(deps)\r\n Object.assign(output.env, a2hEnv)\r\n },\r\n\r\n // \u2500\u2500 tool hook: run skill scripts that need the multimodal model without\r\n // leaking the apiKey into env/argv. The generic `a2h_run_model_script`\r\n // tool is driven by a registry of scripts (src/tools/a2h-run-model-\r\n // script.ts): the agent picks a registered `script` and passes its CLI\r\n // `commandArgs`; the tool resolves the apiKey from the host and pipes it to the\r\n // script over stdin. Adding a model script = one registry entry (no new\r\n // tool file, no index.ts change).\r\n //\r\n // The AutoTest engine (selftest / testcases / report / resolve-metadata)\r\n // is no longer vendored here \u2014 it lives in the HomeTrans package and is\r\n // invoked directly via `node` by agents/self-tester.md (see that file's\r\n // \"Resolving the AutoTest Tools Directory\" section). a2h therefore ships\r\n // only this one plugin tool and carries no autotest runtime dependency.\r\n tool: {\r\n a2h_run_model_script: makeA2hRunModelScriptTool(deps),\r\n },\r\n }\r\n }\r\n}\r\n", "import path from \"node:path\"\r\nimport { fileURLToPath } from \"node:url\"\r\n\r\n// Resolve package root (one directory above dist/ or src/). Computed once here\r\n// and imported everywhere else so there's a single source of truth. After\r\n// esbuild bundling, import.meta.url points at dist/index.js, so this still\r\n// resolves to the package root at runtime.\r\nconst __filename = fileURLToPath(import.meta.url)\r\nconst __dirname = path.dirname(__filename)\r\n\r\n/** Absolute path to the A2H-Plugin package root. */\r\nexport const PKG_ROOT = path.resolve(__dirname, \"..\")\r\n\r\n/** Public plugin id. */\r\nexport const PLUGIN_ID = \"android2harmony\"\r\n", "import path from \"node:path\"\r\nimport fs from \"node:fs\"\r\n\r\n/**\r\n * Strip YAML frontmatter (--- ... ---) from agent .md files.\r\n * Agent .md files have metadata in frontmatter; only the body\r\n * should be used as the LLM prompt.\r\n */\r\nexport function stripFrontmatter(content: string): string {\r\n const match = content.match(/^---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n/)\r\n return match ? content.slice(match[0].length).trim() : content\r\n}\r\n\r\n/**\r\n * Minimal YAML frontmatter extractor for A2H-owned `.md` files (SKILL.md\r\n * and agent `.md`). A2H controls the frontmatter format, so we only need\r\n * the top-level `name` (a scalar). This avoids pulling a YAML dependency\r\n * into the plugin.\r\n *\r\n * Returns the parsed `name`, or null when the file has no usable frontmatter\r\n * (e.g. a README dropped into a directory that's also scanned for agents).\r\n */\r\nexport function parseFrontmatter(content: string): string | null {\r\n const match = content.match(/^---\\s*\\r?\\n([\\s\\S]*?)\\r?\\n---/)\r\n if (!match) return null\r\n const lines = match[1].split(/\\r?\\n/)\r\n\r\n for (const line of lines) {\r\n if (!line.trim() || line.trim().startsWith(\"#\")) continue\r\n if (line.length - line.trimStart().length !== 0) continue // skip indented (e.g. metadata block) lines\r\n\r\n const kv = line.trim().match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\\s*(.*)$/)\r\n if (!kv) continue\r\n if (kv[1] === \"name\") {\r\n return kv[2].trim().replace(/^[\"']|[\"']$/g, \"\")\r\n }\r\n }\r\n\r\n return null\r\n}\r\n\r\n/**\r\n * Scan the bundled agents/ directory and return a `name \u2192 prompt body` map.\r\n *\r\n * Only top-level `.md` files are considered (subdirectories like `scripts/`\r\n * are skipped via `isFile()`). Files without a `name:` field in frontmatter\r\n * (e.g. a stray README) are silently skipped \u2014 the frontmatter name is the\r\n * source of truth, not the file basename. The body (frontmatter stripped)\r\n * becomes the LLM prompt.\r\n *\r\n * Adding a new agent is therefore a zero-code, zero-rebuild change: just\r\n * drop an `.md` with `name:` frontmatter into `agents/`.\r\n */\r\nexport function discoverA2HAgents(agentsRoot: string): Map<string, string> {\r\n const result = new Map<string, string>()\r\n let entries: fs.Dirent[]\r\n try {\r\n entries = fs.readdirSync(agentsRoot, { withFileTypes: true })\r\n } catch {\r\n return result\r\n }\r\n for (const entry of entries) {\r\n if (!entry.isFile() || !entry.name.endsWith(\".md\")) continue\r\n const file = path.join(agentsRoot, entry.name)\r\n let raw: string\r\n try {\r\n raw = fs.readFileSync(file, \"utf-8\")\r\n } catch {\r\n continue\r\n }\r\n const name = parseFrontmatter(raw)\r\n if (!name) continue // not a valid agent file (no `name:` frontmatter)\r\n result.set(name, stripFrontmatter(raw))\r\n }\r\n return result\r\n}\r\n", "import path from \"node:path\"\r\nimport type { A2HHostDeps } from \"./types\"\r\n\r\n/**\r\n * Build the A2H env var set from host-injected deps. Shared by the `shell.env`\r\n * hook (so every shell session sees these). Env vars are the single source of\r\n * truth in the plugin context for non-secret paths.\r\n *\r\n * Contains only DevEco/SDK path vars. The multimodal model apiKey is NOT\r\n * injected here anymore \u2014 putting it in `process.env` leaked it to every\r\n * descendant process (adb/hdc, sub-agents), to `/proc/<pid>/environ`, crash\r\n * dumps, and any `env`/`printenv` output. Instead the `a2h_run_model_script`\r\n * plugin tool resolves the params via `A2HHostDeps.resolveModelParams` and pipes\r\n * them to the script over stdin (`--model-stdin`), so the key stays in process\r\n * memory + the stdin pipe and never enters env/argv/disk.\r\n *\r\n * Nothing secret is present in this env set, but it is still good hygiene not\r\n * to dump it into logs verbatim.\r\n */\r\nexport async function buildA2HEnvBase(\r\n deps: A2HHostDeps,\r\n): Promise<Record<string, string>> {\r\n const env: Record<string, string> = {}\r\n\r\n const home = await deps.resolveDevEcoHome()\r\n if (home) {\r\n const sdk = deps.resolveSdkPath(home)\r\n env[\"DEVECO_HOME\"] = home\r\n env[\"DEVECO_SDK_HOME\"] = sdk\r\n env[\"OHOS_SDK_PATH\"] = path.join(sdk, \"default\", \"openharmony\", \"ets\")\r\n env[\"HMS_SDK_PATH\"] = path.join(sdk, \"default\", \"hms\", \"ets\")\r\n }\r\n\r\n return env\r\n}\r\n", "import { spawn } from \"node:child_process\"\r\nimport path from \"node:path\"\r\nimport { PKG_ROOT } from \"../paths\"\r\nimport type { A2HHostDeps, ToolContext, ToolDefinition } from \"../types\"\r\n\r\n/**\r\n * Generic \"run a skill script that needs the multimodal model\" tool.\r\n *\r\n * One tool, driven by a registry of scripts. The agent picks a `script` and\r\n * passes its params as an object (camelCase fields); the tool flattens them to\r\n * `--kebab-case` CLI argv and spawns the script. The model apiKey is resolved\r\n * from the host and piped to the script's stdin \u2014 the key traverses only the OS\r\n * pipe + process memory, never env/argv/disk, so it is invisible to the spawned\r\n * adb/hdc descendants. Adding a model script = one registry entry (no new tool\r\n * file, no index.ts change); the tool description auto-extends.\r\n */\r\n\r\ninterface ModelScriptEntry {\r\n /** `script` enum value the agent passes. */\r\n name: string\r\n /** Absolute path to the .js script (source; runs under node_modules without native type-stripping). */\r\n path: string\r\n /** Per-script params summary; documents the camelCase fields the agent passes. */\r\n argsHelp: string\r\n}\r\n\r\n/**\r\n * Registry of model-consuming skill scripts. This is the security allowlist\r\n * (the agent cannot pass an arbitrary path \u2014 only a registered `name`) and the\r\n * sole place to edit when adding a script. The tool description is built from\r\n * this so it auto-extends without touching tool code.\r\n */\r\nconst MODEL_SCRIPTS: ModelScriptEntry[] = [\r\n {\r\n name: \"app_feature_verify\",\r\n path: path.join(\r\n PKG_ROOT,\r\n \"skills\",\r\n \"hmos-incremental-ui-align\",\r\n \"scripts\",\r\n \"app_feature_verify.js\",\r\n ),\r\n argsHelp:\r\n \"UI \u5BFB\u8DEF phone-agent:\u5728 HarmonyOS(hdc)\u6216 Android(adb)\u5DF2\u8FDE\u63A5\u8BBE\u5907\u4E0A,\u6309\u8DEF\u5F84/\u63D0\u793A\u5BFC\u822A\u5230\u76EE\u6807\u9875\u9762\u5E76\u622A\u56FE+\u6A21\u578B\u5224\u5B9A\u662F\u5426\u5230\u8FBE\u3002\\n\\n\" +\r\n \"| \u5B57\u6BB5 | \u7C7B\u578B | \u7EA6\u675F | \u8BF4\u660E |\\n\" +\r\n \"|---|---|---|---|\\n\" +\r\n \"| app | string | \u5FC5\u586B | \u76EE\u6807\u5E94\u7528\u663E\u793A\u540D |\\n\" +\r\n \"| package | string | \u5FC5\u586B | \u76EE\u6807\u5E94\u7528\u5305\u540D |\\n\" +\r\n \"| device | string | \u9ED8\u8BA4 hdc | \u8BBE\u5907\u9A71\u52A8,\u53D6\u503C hdc \u6216 adb |\\n\" +\r\n \"| prompt | string | \u4E09\u9009\u4E00 | \u81EA\u7531\u5BFC\u822A\u63CF\u8FF0 |\\n\" +\r\n \"| task | string | \u4E09\u9009\u4E00 | L1\u2026>L2\u2026>L3\u2026 \u5C42\u7EA7\u8DEF\u5F84 |\\n\" +\r\n \"| feature | string | \u4E09\u9009\u4E00 | features.md \u8DEF\u5F84 |\\n\" +\r\n \"| maxSteps | number | \u9ED8\u8BA4 30 | \u6700\u5927 agent \u6B65\u6570 |\\n\" +\r\n \"| serial | string | \u53EF\u9009 | \u8BBE\u5907\u5E8F\u5217\u53F7(\u591A\u8BBE\u5907\u65F6) |\\n\" +\r\n \"| appHints | string | \u53EF\u9009 | \u5E94\u7528\u5BFC\u822A\u63D0\u793A |\\n\" +\r\n \"| limit | number | \u53EF\u9009 | --feature \u6A21\u5F0F\u53D6\u524D N \u6761 |\\n\" +\r\n \"| offset | number | \u9ED8\u8BA4 1 | --feature \u6A21\u5F0F\u504F\u79FB |\\n\" +\r\n \"| quiet | boolean | \u53EF\u9009 | true \u65F6\u6291\u5236\u63D0\u793A\u6027\u8F93\u51FA |\\n\\n\" +\r\n \"\u7EA6\u675F:app \u4E0E package \u5FC5\u586B,\u4E14 prompt/task/feature \u4E09\u9009\u4E00\u3002\\n\" +\r\n '\u793A\u4F8B:commandArgs = {\"app\":\"\u56FE\u5E93\",\"package\":\"com.x.gallery\",\"device\":\"adb\",\"prompt\":\"\u8FDB\u5165\u8BBE\u7F6E\u9875\u9762\",\"maxSteps\":15}',\r\n },\r\n]\r\n\r\n/** Cap stdout returned to the host so a runaway loop can't overflow the chat. */\r\nfunction cap(s: string, max = 50000): string {\r\n if (s.length <= max) return s\r\n const half = Math.floor(max / 2)\r\n return s.slice(0, half) + `\\n\u2026[truncated ${s.length - max} chars]\u2026\\n` + s.slice(-half)\r\n}\r\n\r\n/**\r\n * Shared runner: resolve host model params, spawn the script with the model\r\n * config piped to stdin, collect stdout/stderr, honor abort, and return the\r\n * capped stdout (or throw on non-zero exit).\r\n *\r\n * The apiKey is resolved from the host and piped to the script over stdin\r\n * (`--model-stdin`); the agent never handles it.\r\n */\r\nasync function runModelScript(\r\n deps: A2HHostDeps,\r\n context: ToolContext,\r\n entry: ModelScriptEntry,\r\n argv: string[],\r\n): Promise<{ ok: true; exitCode: number; output: string }> {\r\n const model = await deps.resolveModelParams(context.worktree)\r\n if (!model || !model.apiKey) {\r\n throw new Error(\r\n `a2h_run_model_script (${entry.name}): host did not provide model params (apiKey missing)`,\r\n )\r\n }\r\n\r\n const finalArgv = [...argv, \"--model-stdin\"]\r\n\r\n // Spawn via PATH `node`. process.execPath might be an older/different host\r\n // node (e.g. a bundled runtime), so prefer PATH resolution to match prior\r\n // working behavior. The script is precompiled .js, so no native type-stripping\r\n // is involved and it runs fine even though the path sits under node_modules.\r\n const child = spawn(\"node\", [entry.path, ...finalArgv], {\r\n cwd: context.directory,\r\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\r\n })\r\n\r\n // Pipe the model config to the child's stdin and close it \u2014 the key traverses\r\n // only the OS pipe, never env/argv/disk.\r\n child.stdin.end(\r\n JSON.stringify({ apiKey: model.apiKey, modelName: model.modelName, baseURL: model.baseURL }),\r\n )\r\n\r\n let stdout = \"\"\r\n let stderr = \"\"\r\n let spawnError = \"\"\r\n child.stdout.on(\"data\", (chunk: Buffer) => {\r\n const text = chunk.toString(\"utf8\")\r\n stdout += text\r\n // Surface the last non-empty line as a running title so the host can show\r\n // progress. Generic (not script-specific) \u2014 the agent sees no secrets.\r\n const lines = text.split(/\\r?\\n/)\r\n for (let i = lines.length - 1; i >= 0; i--) {\r\n const line = lines[i].trim()\r\n if (line) {\r\n try {\r\n context.metadata({ title: line.slice(0, 80) })\r\n } catch {\r\n /* host may not implement metadata \u2014 non-essential */\r\n }\r\n break\r\n }\r\n }\r\n })\r\n child.stderr.on(\"data\", (chunk: Buffer) => {\r\n stderr += chunk.toString(\"utf8\")\r\n })\r\n\r\n // Honor abort: kill the child if the host cancels the tool call.\r\n const onAbort = () => {\r\n if (!child.killed) child.kill(\"SIGTERM\")\r\n }\r\n context.abort.addEventListener(\"abort\", onAbort)\r\n\r\n const code: number = await new Promise((resolve) => {\r\n child.on(\"close\", resolve)\r\n child.on(\"error\", (err: Error) => {\r\n spawnError = err.message\r\n resolve(-1)\r\n })\r\n })\r\n context.abort.removeEventListener(\"abort\", onAbort)\r\n\r\n if (code !== 0) {\r\n if (spawnError) {\r\n throw new Error(`a2h_run_model_script (${entry.name}): failed to spawn node \u2014 ${spawnError}`)\r\n }\r\n const tail = stderr.slice(-2000)\r\n throw new Error(\r\n `a2h_run_model_script (${entry.name}) failed (exit ${code}): ${tail || \"(no stderr)\"}`,\r\n )\r\n }\r\n return { ok: true, exitCode: code, output: cap(stdout) }\r\n}\r\n\r\n/** camelCase \u2192 --kebab-case flag (maxSteps \u2192 --max-steps). */\r\nconst kebab = (s: string): string => \"--\" + s.replace(/([a-z0-9])([A-Z])/g, \"$1-$2\").toLowerCase()\r\n\r\n/**\r\n * Flatten a params object into CLI argv: each entry \u2192 `--kebab-case` flag + its\r\n * value; booleans \u2192 flag-only when true (false/null/undefined skipped). Generic\r\n * over any script whose flags are kebab-cased, so it is not coupled to a schema.\r\n */\r\nfunction flattenParams(obj: Record<string, unknown>): string[] {\r\n const out: string[] = []\r\n for (const [k, v] of Object.entries(obj)) {\r\n if (v == null) continue\r\n if (typeof v === \"boolean\") {\r\n if (v) out.push(kebab(k))\r\n } else {\r\n out.push(kebab(k), String(v))\r\n }\r\n }\r\n return out\r\n}\r\n\r\nconst DESCRIPTION_PREFIX =\r\n \"\u8FD0\u884C\u9700\u8981\u591A\u6A21\u6001\u6A21\u578B\u7684 skill \u811A\u672C:\u5728 HarmonyOS \u6216 Android \u8BBE\u5907\u4E0A\u6267\u884C\u7531 `script` \u9009\u5B9A\u7684\u811A\u672C,\u628A\u53C2\u6570\u4F5C\u4E3A\u5BF9\u8C61(camelCase \u5B57\u6BB5)\u4F20\u5165,\u5DE5\u5177\u81EA\u52A8\u8F6C\u4E3A --kebab-case \u547D\u4EE4\u884C\u53C2\u6570\u900F\u4F20\u7ED9 `node` \u8FDB\u7A0B,\u8FD0\u884C\u5B8C\u6BD5\u8FD4\u56DE\u811A\u672C stdout\u3002\u4F60\u53EA\u9700\u63D0\u4F9B `script` \u4E0E `commandArgs`,\u6A21\u578B\u76F8\u5173\u914D\u7F6E(apiKey \u7B49)\u7531\u5DE5\u5177\u5185\u90E8\u7ECF stdin \u6CE8\u5165,\u4E0D\u8981\u4F20 `apiKey`/`modelStdin` \u5B57\u6BB5\u3002\\n\\n\" +\r\n \"\u53EF\u7528\u811A\u672C\u53CA\u5176\u53C2\u6570:\\n\"\r\n\r\n/**\r\n * Build the `a2h_run_model_script` plugin tool.\r\n *\r\n * Generic over scripts: the agent selects a registered `script` and passes its\r\n * params as an object; the tool flattens them to CLI argv. The model apiKey is\r\n * resolved from the host and piped to the script over stdin \u2014 never env/argv/\r\n * disk, so invisible to adb/hdc descendants. Use this instead of running the\r\n * script via `node` directly (a direct run can only get the key from\r\n * env/argv/config, which leaks).\r\n */\r\nexport function makeA2hRunModelScriptTool(deps: A2HHostDeps): ToolDefinition {\r\n return {\r\n description:\r\n DESCRIPTION_PREFIX +\r\n MODEL_SCRIPTS.map((s) => `${s.name}: ${s.argsHelp}`).join(\"\\n\"),\r\n // `args` is a flat field-name\u2192field-schema MAP (not a full `{type:\"object\",\r\n // properties, required}` schema object). The host's `legacyJsonSchema` reads\r\n // this map and builds the LLM-facing schema itself: `{type:\"object\",\r\n // properties:{script, commandArgs}, required:[script, commandArgs]}` (it\r\n // marks every field required). Declaring the full-schema wrapper here would\r\n // make `legacyJsonSchema` mis-read `properties` as a literal field name \u2192 the\r\n // model receives a schema with a single `properties` field and wraps its\r\n // input under it \u2192 \"unknown script\". Keep it a field-map.\r\n args: {\r\n script: {\r\n type: \"string\",\r\n enum: MODEL_SCRIPTS.map((s) => s.name),\r\n description: \"\u8981\u8FD0\u884C\u7684\u6A21\u578B\u811A\u672C\u540D,\u89C1\u4E0A\u65B9\u5404\u6761\u76EE\",\r\n },\r\n commandArgs: {\r\n type: \"object\",\r\n description:\r\n '\u8BE5\u811A\u672C\u7684\u53C2\u6570\u5BF9\u8C61(camelCase \u5B57\u6BB5,\u5DE5\u5177\u81EA\u52A8\u8F6C\u4E3A --kebab-case \u547D\u4EE4\u884C\u53C2\u6570),\u6309\u4E0A\u65B9\u5BF9\u5E94\u6761\u76EE\u8BF4\u660E\u586B\u5199,\u5F62\u5982 {\"device\":\"adb\",\"app\":\"\u56FE\u5E93\",\"package\":\"com.x\",\"prompt\":\"\u8FDB\u5165\u8BBE\u7F6E\",\"maxSteps\":15}',\r\n },\r\n },\r\n execute: async (args: Record<string, unknown>, context: ToolContext) => {\r\n const name = String(args.script ?? \"\")\r\n const entry = MODEL_SCRIPTS.find((s) => s.name === name)\r\n if (!entry) {\r\n throw new Error(\r\n `a2h_run_model_script: unknown script \"${name}\". Registered: ${MODEL_SCRIPTS.map((s) => s.name).join(\", \")}`,\r\n )\r\n }\r\n // `commandArgs` may arrive as an object, a JSON string of that object, or as\r\n // top-level fields (model-dependent) \u2014 collect them all, then flatten.\r\n const params: Record<string, unknown> = {}\r\n const a = args.commandArgs\r\n if (a && typeof a === \"object\") {\r\n Object.assign(params, a as Record<string, unknown>)\r\n } else if (typeof a === \"string\" && a.trim()) {\r\n try {\r\n const parsed: unknown = JSON.parse(a)\r\n if (parsed && typeof parsed === \"object\") {\r\n Object.assign(params, parsed as Record<string, unknown>)\r\n }\r\n } catch {\r\n /* not JSON \u2014 fall through to top-level fields */\r\n }\r\n }\r\n for (const [k, v] of Object.entries(args)) {\r\n if (k !== \"script\" && k !== \"commandArgs\") params[k] = v\r\n }\r\n return runModelScript(deps, context, entry, flattenParams(params))\r\n },\r\n }\r\n}\r\n"],
|
|
5
|
-
"mappings": ";AAAA,OAAOA,WAAU;;;ACAjB,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAM9B,IAAM,aAAa,cAAc,YAAY,GAAG;AAChD,IAAM,YAAY,KAAK,QAAQ,UAAU;AAGlC,IAAM,WAAW,KAAK,QAAQ,WAAW,IAAI;AAG7C,IAAM,YAAY;;;ACdzB,OAAOC,WAAU;AACjB,OAAO,QAAQ;AAOR,SAAS,iBAAiB,SAAyB;AACxD,QAAM,QAAQ,QAAQ,MAAM,+BAA+B;AAC3D,SAAO,QAAQ,QAAQ,MAAM,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,IAAI;AACzD;AAWO,SAAS,iBAAiB,SAAgC;AAC/D,QAAM,QAAQ,QAAQ,MAAM,gCAAgC;AAC5D,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,QAAQ,MAAM,CAAC,EAAE,MAAM,OAAO;AAEpC,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,KAAK,KAAK,KAAK,KAAK,KAAK,EAAE,WAAW,GAAG,EAAG;AACjD,QAAI,KAAK,SAAS,KAAK,UAAU,EAAE,WAAW,EAAG;AAEjD,UAAM,KAAK,KAAK,KAAK,EAAE,MAAM,qCAAqC;AAClE,QAAI,CAAC,GAAI;AACT,QAAI,GAAG,CAAC,MAAM,QAAQ;AACpB,aAAO,GAAG,CAAC,EAAE,KAAK,EAAE,QAAQ,gBAAgB,EAAE;AAAA,IAChD;AAAA,EACF;AAEA,SAAO;AACT;AAcO,SAAS,kBAAkB,YAAyC;AACzE,QAAM,SAAS,oBAAI,IAAoB;AACvC,MAAI;AACJ,MAAI;AACF,cAAU,GAAG,YAAY,YAAY,EAAE,eAAe,KAAK,CAAC;AAAA,EAC9D,QAAQ;AACN,WAAO;AAAA,EACT;AACA,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,MAAM,OAAO,KAAK,CAAC,MAAM,KAAK,SAAS,KAAK,EAAG;AACpD,UAAM,OAAOA,MAAK,KAAK,YAAY,MAAM,IAAI;AAC7C,QAAI;AACJ,QAAI;AACF,YAAM,GAAG,aAAa,MAAM,OAAO;AAAA,IACrC,QAAQ;AACN;AAAA,IACF;AACA,UAAM,OAAO,iBAAiB,GAAG;AACjC,QAAI,CAAC,KAAM;AACX,WAAO,IAAI,MAAM,iBAAiB,GAAG,CAAC;AAAA,EACxC;AACA,SAAO;AACT;;;AC3EA,OAAOC,WAAU;
|
|
6
|
-
"names": ["path", "path", "path", "path", "path", "path"]
|
|
3
|
+
"sources": ["../src/index.ts", "../src/paths.ts", "../src/discovery.ts", "../src/env.ts", "../src/tools/a2h-app-feature-verify.ts", "../src/tools/spawn-helpers.ts", "../src/tools/a2h-self-test.ts", "../src/tools/a2h-self-test-status.ts"],
|
|
4
|
+
"sourcesContent": ["import path from \"node:path\"\r\nimport type { A2HHostDeps, Plugin, PluginInput, PluginHooks } from \"./types\"\r\nimport { PKG_ROOT } from \"./paths\"\r\nimport { discoverA2HAgents } from \"./discovery\"\r\nimport { buildA2HEnvBase } from \"./env\"\r\nimport { makeA2hAppFeatureVerifyTool } from \"./tools/a2h-app-feature-verify\"\r\nimport { makeA2hSelfTestTool } from \"./tools/a2h-self-test\"\r\nimport { makeA2hSelfTestStatusTool } from \"./tools/a2h-self-test-status\"\r\n\r\n// Public re-exports \u2014 the package's external surface (dev eco code loads\r\n// dist/index.js at runtime and calls `createServer`; `A2HHostDeps` /\r\n// `PLUGIN_ID` are kept exported for any TS consumers).\r\nexport type { A2HHostDeps } from \"./types\"\r\nexport { PLUGIN_ID } from \"./paths\"\r\n\r\n/**\r\n * Create the A2H DevEco Code plugin.\r\n *\r\n * This is a self-contained plugin - skills/, agents/, and tools/\r\n * are bundled inside the package, NOT referenced from HomeTrans.\r\n * A2H-Plugin is the independently publishable \"plugin edition\" of HomeTrans.\r\n *\r\n * @param deps - host dependencies injected by DevEco Code's thin bridge.\r\n * The A2H Plugin defines what it needs (A2HHostDeps);\r\n * DevEco Code provides how to resolve it.\r\n * @returns A Plugin function ready to be registered as a built-in plugin.\r\n */\r\nexport function createServer(deps: A2HHostDeps): Plugin {\r\n return async (input: PluginInput): Promise<PluginHooks> => {\r\n // All A2H assets live inside this package (self-contained, no HomeTrans dep)\r\n const skillsRoot = path.join(PKG_ROOT, \"skills\")\r\n const agentsRoot = path.join(PKG_ROOT, \"agents\")\r\n\r\n // Auto-discover agents: scan top-level agents/*.md, parse `name:` from\r\n // frontmatter, strip frontmatter for prompt body. Adding/removing/renaming\r\n // an agent is just a file drop \u2014 no code change, no rebuild.\r\n const agentPrompts = discoverA2HAgents(agentsRoot)\r\n\r\n return {\r\n dispose: async () => {\r\n agentPrompts.clear()\r\n },\r\n\r\n // \u2500\u2500 config hook: register skills search paths and agents \u2500\u2500\r\n config: async (cfg: Record<string, unknown>) => {\r\n // --- Skills ---\r\n const skills = (cfg.skills as Record<string, unknown>) || {}\r\n cfg.skills = skills\r\n\r\n // Register the bundled skills/ directory as an additional search path.\r\n // All bundled skills are user-visible \u2014 there is no partial-visibility\r\n // (hidden) layer anymore, so we only register the search path here.\r\n const paths = (skills.paths as string[]) || []\r\n skills.paths = paths\r\n if (!paths.includes(skillsRoot)) {\r\n paths.push(skillsRoot)\r\n }\r\n\r\n // --- Agents ---\r\n const agent = (cfg.agent as Record<string, Record<string, unknown>>) || {}\r\n cfg.agent = agent\r\n\r\n for (const [name, prompt] of agentPrompts) {\r\n // Don't overwrite if the user already configured this agent\r\n if (!agent[name]) {\r\n agent[name] = {\r\n mode: \"subagent\",\r\n hidden: true,\r\n prompt,\r\n }\r\n }\r\n }\r\n\r\n },\r\n\r\n // \u2500\u2500 shell.env hook: inject DevEco Studio / SDK path vars only. The \u2500\u2500\r\n // multimodal model apiKey is deliberately NOT put in env (it would leak\r\n // to every adb/hdc descendant); the a2h model-script tools pipe it to\r\n // the script over stdin instead. See src/env.ts.\r\n \"shell.env\": async (\r\n shellInput: { cwd: string; sessionID?: string; callID?: string },\r\n output: { env: Record<string, string> },\r\n ) => {\r\n const a2hEnv = await buildA2HEnvBase(deps)\r\n Object.assign(output.env, a2hEnv)\r\n },\r\n\r\n // \u2500\u2500 tool hook: run skill scripts that need the multimodal model without\r\n // leaking the apiKey into env/argv. Three fixed-schema tools, each\r\n // resolving the apiKey from the host and piping it over stdin\r\n // (`--model-stdin`) \u2014 never env/argv, so adb/hdc descendants can't see it:\r\n // \u2022 `a2h_app_feature_verify` (src/tools/a2h-app-feature-verify.ts) \u2014\r\n // phone-agent UI navigation + screenshot + model verification\r\n // (app_feature_verify.js). Synchronous run; throws on non-zero exit.\r\n // \u2022 `a2h_self_test` (src/tools/a2h-self-test.ts) +\r\n // `a2h_self_test_status` (src/tools/a2h-self-test-status.ts) \u2014 the\r\n // integration-test flow. The former spawns self-test-runner.mjs\r\n // which spawns batch_runner detached and returns RUNNING immediately\r\n // (no blocking); the latter polls it via `--status` (no key needed).\r\n //\r\n // The AutoTest engine (batch_runner / testcases / report / resolve-metadata)\r\n // lives in @autotest/agent + the HomeTrans package, invoked via `node`;\r\n // a2h carries no autotest runtime dependency.\r\n tool: {\r\n a2h_app_feature_verify: makeA2hAppFeatureVerifyTool(deps),\r\n a2h_self_test: makeA2hSelfTestTool(deps),\r\n a2h_self_test_status: makeA2hSelfTestStatusTool(),\r\n },\r\n }\r\n }\r\n}\r\n", "import path from \"node:path\"\r\nimport { fileURLToPath } from \"node:url\"\r\n\r\n// Resolve package root (one directory above dist/ or src/). Computed once here\r\n// and imported everywhere else so there's a single source of truth. After\r\n// esbuild bundling, import.meta.url points at dist/index.js, so this still\r\n// resolves to the package root at runtime.\r\nconst __filename = fileURLToPath(import.meta.url)\r\nconst __dirname = path.dirname(__filename)\r\n\r\n/** Absolute path to the A2H-Plugin package root. */\r\nexport const PKG_ROOT = path.resolve(__dirname, \"..\")\r\n\r\n/** Public plugin id. */\r\nexport const PLUGIN_ID = \"android2harmony\"\r\n", "import path from \"node:path\"\r\nimport fs from \"node:fs\"\r\n\r\n/**\r\n * Strip YAML frontmatter (--- ... ---) from agent .md files.\r\n * Agent .md files have metadata in frontmatter; only the body\r\n * should be used as the LLM prompt.\r\n */\r\nexport function stripFrontmatter(content: string): string {\r\n const match = content.match(/^---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n/)\r\n return match ? content.slice(match[0].length).trim() : content\r\n}\r\n\r\n/**\r\n * Minimal YAML frontmatter extractor for A2H-owned `.md` files (SKILL.md\r\n * and agent `.md`). A2H controls the frontmatter format, so we only need\r\n * the top-level `name` (a scalar). This avoids pulling a YAML dependency\r\n * into the plugin.\r\n *\r\n * Returns the parsed `name`, or null when the file has no usable frontmatter\r\n * (e.g. a README dropped into a directory that's also scanned for agents).\r\n */\r\nexport function parseFrontmatter(content: string): string | null {\r\n const match = content.match(/^---\\s*\\r?\\n([\\s\\S]*?)\\r?\\n---/)\r\n if (!match) return null\r\n const lines = match[1].split(/\\r?\\n/)\r\n\r\n for (const line of lines) {\r\n if (!line.trim() || line.trim().startsWith(\"#\")) continue\r\n if (line.length - line.trimStart().length !== 0) continue // skip indented (e.g. metadata block) lines\r\n\r\n const kv = line.trim().match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\\s*(.*)$/)\r\n if (!kv) continue\r\n if (kv[1] === \"name\") {\r\n return kv[2].trim().replace(/^[\"']|[\"']$/g, \"\")\r\n }\r\n }\r\n\r\n return null\r\n}\r\n\r\n/**\r\n * Scan the bundled agents/ directory and return a `name \u2192 prompt body` map.\r\n *\r\n * Only top-level `.md` files are considered (subdirectories like `scripts/`\r\n * are skipped via `isFile()`). Files without a `name:` field in frontmatter\r\n * (e.g. a stray README) are silently skipped \u2014 the frontmatter name is the\r\n * source of truth, not the file basename. The body (frontmatter stripped)\r\n * becomes the LLM prompt.\r\n *\r\n * Adding a new agent is therefore a zero-code, zero-rebuild change: just\r\n * drop an `.md` with `name:` frontmatter into `agents/`.\r\n */\r\nexport function discoverA2HAgents(agentsRoot: string): Map<string, string> {\r\n const result = new Map<string, string>()\r\n let entries: fs.Dirent[]\r\n try {\r\n entries = fs.readdirSync(agentsRoot, { withFileTypes: true })\r\n } catch {\r\n return result\r\n }\r\n for (const entry of entries) {\r\n if (!entry.isFile() || !entry.name.endsWith(\".md\")) continue\r\n const file = path.join(agentsRoot, entry.name)\r\n let raw: string\r\n try {\r\n raw = fs.readFileSync(file, \"utf-8\")\r\n } catch {\r\n continue\r\n }\r\n const name = parseFrontmatter(raw)\r\n if (!name) continue // not a valid agent file (no `name:` frontmatter)\r\n result.set(name, stripFrontmatter(raw))\r\n }\r\n return result\r\n}\r\n", "import path from \"node:path\"\r\nimport type { A2HHostDeps } from \"./types\"\r\n\r\n/**\r\n * Build the A2H env var set from host-injected deps. Shared by the `shell.env`\r\n * hook (so every shell session sees these). Env vars are the single source of\r\n * truth in the plugin context for non-secret paths.\r\n *\r\n * Contains only DevEco/SDK path vars. The multimodal model apiKey is NOT\r\n * injected here anymore \u2014 putting it in `process.env` leaked it to every\r\n * descendant process (adb/hdc, sub-agents), to `/proc/<pid>/environ`, crash\r\n * dumps, and any `env`/`printenv` output. Instead the a2h model-script tools\r\n * (`a2h_app_feature_verify` / `a2h_self_test`) resolve the params via\r\n * `A2HHostDeps.resolveModelParams` and pipe them to the script over stdin\r\n * (`--model-stdin`), so the key stays in process memory + the stdin pipe and\r\n * never enters env/argv/disk.\r\n *\r\n * Nothing secret is present in this env set, but it is still good hygiene not\r\n * to dump it into logs verbatim.\r\n */\r\nexport async function buildA2HEnvBase(\r\n deps: A2HHostDeps,\r\n): Promise<Record<string, string>> {\r\n const env: Record<string, string> = {}\r\n\r\n const home = await deps.resolveDevEcoHome()\r\n if (home) {\r\n const sdk = deps.resolveSdkPath(home)\r\n env[\"DEVECO_HOME\"] = home\r\n env[\"DEVECO_SDK_HOME\"] = sdk\r\n env[\"OHOS_SDK_PATH\"] = path.join(sdk, \"default\", \"openharmony\", \"ets\")\r\n env[\"HMS_SDK_PATH\"] = path.join(sdk, \"default\", \"hms\", \"ets\")\r\n }\r\n\r\n return env\r\n}\r\n", "import path from \"node:path\"\r\nimport { PKG_ROOT } from \"../paths\"\r\nimport type { A2HHostDeps, ToolContext, ToolDefinition } from \"../types\"\r\nimport { runModelScript } from \"./spawn-helpers\"\r\n\r\n/**\r\n * `a2h_app_feature_verify` tool \u2014 fixed-schema tool over the app_feature_verify.js\r\n * skill script (phone-agent UI navigation + screenshot + model verification on\r\n * hdc/adb devices).\r\n *\r\n * `kebab` / `flattenParams` are local to this tool (the self-test tools hand-build\r\n * their argv instead), so they live here rather than in the shared spawn-helpers.\r\n */\r\n\r\n/** Absolute path to the app_feature_verify skill script (precompiled .js). */\r\nconst APP_FEATURE_VERIFY_PATH = path.join(\r\n PKG_ROOT,\r\n \"skills\",\r\n \"hmos-incremental-ui-align\",\r\n \"scripts\",\r\n \"app_feature_verify.js\",\r\n)\r\n\r\n/** camelCase \u2192 --kebab-case flag (maxSteps \u2192 --max-steps). */\r\nconst kebab = (s: string): string => \"--\" + s.replace(/([a-z0-9])([A-Z])/g, \"$1-$2\").toLowerCase()\r\n\r\n/**\r\n * Flatten a params object into CLI argv: each entry \u2192 `--kebab-case` flag + its\r\n * value; booleans \u2192 flag-only when true (false/null/undefined skipped).\r\n * `v == null || v === \"\"` skips null/undefined AND empty string, so an optional\r\n * passed as \"\" falls through to the script's own default instead of pushing\r\n * `--flag \"\"` and overriding it (see the device=\"\" gotcha in args comment below).\r\n */\r\nfunction flattenParams(obj: Record<string, unknown>): string[] {\r\n const out: string[] = []\r\n for (const [k, v] of Object.entries(obj)) {\r\n if (v == null || v === \"\") continue\r\n if (typeof v === \"boolean\") {\r\n if (v) out.push(kebab(k))\r\n } else {\r\n out.push(kebab(k), String(v))\r\n }\r\n }\r\n return out\r\n}\r\n\r\n/**\r\n * Build the `a2h_app_feature_verify` plugin tool. The agent passes navigation\r\n * params as top-level fields; the tool flattens them to `--kebab-case` argv and\r\n * resolves the apiKey from the host, piping it over stdin \u2014 never env/argv/disk,\r\n * so invisible to adb/hdc descendants. Use this instead of running the script via\r\n * `node` directly (a direct run can only get the key from env/argv/config,\r\n * which leaks).\r\n */\r\nexport function makeA2hAppFeatureVerifyTool(deps: A2HHostDeps): ToolDefinition {\r\n return {\r\n description:\r\n \"UI \u5BFB\u8DEF phone-agent:\u5728 HarmonyOS(hdc)\u6216 Android(adb)\u5DF2\u8FDE\u63A5\u8BBE\u5907\u4E0A,\u6309\u8DEF\u5F84/\u63D0\u793A\u5BFC\u822A\u5230\u76EE\u6807\u9875\u9762\u5E76\u622A\u56FE + \u6A21\u578B\u5224\u5B9A\u662F\u5426\u5230\u8FBE\u3002\u6A21\u578B\u914D\u7F6E(apiKey \u7B49)\u7531\u5DE5\u5177\u5185\u90E8\u7ECF stdin \u6CE8\u5165,\u4E0D\u8981\u4F20 apiKey/modelStdin \u5B57\u6BB5\u3002\\n\\n\" +\r\n \"| \u5B57\u6BB5 | \u7C7B\u578B | \u7EA6\u675F | \u8BF4\u660E |\\n\" +\r\n \"|---|---|---|---|\\n\" +\r\n \"| app | string | \u5FC5\u586B | \u76EE\u6807\u5E94\u7528\u663E\u793A\u540D |\\n\" +\r\n \"| package | string | \u5FC5\u586B | \u76EE\u6807\u5E94\u7528\u5305\u540D |\\n\" +\r\n \"| device | string | \u53EF\u9009(\u9ED8\u8BA4 hdc) | \u8BBE\u5907\u9A71\u52A8,\u53D6\u503C hdc \u6216 adb |\\n\" +\r\n \"| prompt | string | \u4E09\u9009\u4E00 | \u81EA\u7531\u5BFC\u822A\u63CF\u8FF0 |\\n\" +\r\n \"| task | string | \u4E09\u9009\u4E00 | L1>L2>L3 \u5C42\u7EA7\u8DEF\u5F84 |\\n\" +\r\n \"| feature | string | \u4E09\u9009\u4E00 | features.md \u8DEF\u5F84(\u987B .md \u7ED3\u5C3E) |\\n\" +\r\n \"| maxSteps | number | \u53EF\u9009(\u9ED8\u8BA4 30) | \u6700\u5927 agent \u6B65\u6570 |\\n\" +\r\n \"| serial | string | \u53EF\u9009 | \u8BBE\u5907\u5E8F\u5217\u53F7(\u591A\u8BBE\u5907\u65F6) |\\n\" +\r\n \"| appHints | string | \u53EF\u9009 | \u5E94\u7528\u5BFC\u822A\u63D0\u793A |\\n\" +\r\n \"| limit | number | \u53EF\u9009 | --feature \u6A21\u5F0F\u53D6\u524D N \u6761 |\\n\" +\r\n \"| offset | number | \u53EF\u9009(\u9ED8\u8BA4 1) | --feature \u6A21\u5F0F\u504F\u79FB |\\n\" +\r\n \"| quiet | boolean | \u53EF\u9009 | true \u65F6\u6291\u5236\u63D0\u793A\u6027\u8F93\u51FA |\\n\\n\" +\r\n \"\u7EA6\u675F:app \u4E0E package \u5FC5\u586B,\u4E14 prompt/task/feature \u4E09\u9009\u4E00(\u4F20\u4E00\u4E2A)\u3002\",\r\n // `args` is a flat field-name\u2192field-schema MAP (not a full `{type:\"object\",\r\n // properties, required}` schema object). The host's `legacyJsonSchema` reads\r\n // this map and builds the LLM-facing schema itself: `{type:\"object\",\r\n // properties:{...}, required:[...every field...]}` (it marks every field\r\n // required). So optional fields MUST be nullable (`[\"string\",\"null\"]` etc.)\r\n // -- the agent passes null (or \"\") and `flattenParams` drops it, letting the\r\n // script's own default apply. Declaring the full-schema wrapper here would\r\n // make `legacyJsonSchema` mis-read `properties` as a literal field name. Keep\r\n // it a field-map.\r\n args: {\r\n app: { type: \"string\", description: \"\u76EE\u6807\u5E94\u7528\u663E\u793A\u540D\" },\r\n package: { type: \"string\", description: \"\u76EE\u6807\u5E94\u7528\u5305\u540D\" },\r\n device: { type: [\"string\", \"null\"], description: \"\u8BBE\u5907\u9A71\u52A8 hdc \u6216 adb,\u53EF\u9009;\u9ED8\u8BA4 hdc\" },\r\n prompt: { type: [\"string\", \"null\"], description: \"\u81EA\u7531\u5BFC\u822A\u63CF\u8FF0(\u4E0E task/feature \u4E09\u9009\u4E00)\" },\r\n task: { type: [\"string\", \"null\"], description: \"L1>L2>L3 \u5C42\u7EA7\u8DEF\u5F84(\u4E0E prompt/feature \u4E09\u9009\u4E00)\" },\r\n feature: { type: [\"string\", \"null\"], description: \"features.md \u8DEF\u5F84,\u987B .md \u7ED3\u5C3E(\u4E0E prompt/task \u4E09\u9009\u4E00)\" },\r\n maxSteps: { type: [\"number\", \"null\"], description: \"\u6700\u5927 agent \u6B65\u6570,\u53EF\u9009;\u9ED8\u8BA4 30\" },\r\n serial: { type: [\"string\", \"null\"], description: \"\u8BBE\u5907\u5E8F\u5217\u53F7(\u591A\u8BBE\u5907\u65F6),\u53EF\u9009\" },\r\n appHints: { type: [\"string\", \"null\"], description: \"\u5E94\u7528\u5BFC\u822A\u63D0\u793A,\u53EF\u9009\" },\r\n limit: { type: [\"number\", \"null\"], description: \"--feature \u6A21\u5F0F\u53D6\u524D N \u6761,\u53EF\u9009\" },\r\n offset: { type: [\"number\", \"null\"], description: \"--feature \u6A21\u5F0F\u504F\u79FB,\u53EF\u9009;\u9ED8\u8BA4 1\" },\r\n quiet: { type: [\"boolean\", \"null\"], description: \"true \u65F6\u6291\u5236\u63D0\u793A\u6027\u8F93\u51FA,\u53EF\u9009\" },\r\n },\r\n execute: async (args: Record<string, unknown>, context: ToolContext) => {\r\n const params: Record<string, unknown> = { ...args }\r\n // The script enforces \"at least one\" of prompt/task/feature (priority\r\n // prompt > task > feature; passing two silently takes the higher-priority\r\n // one). Mirror \"at least one\" here for a clearer error before spawning.\r\n if (params.prompt == null && params.task == null && params.feature == null) {\r\n throw new Error(\r\n \"a2h_app_feature_verify: must pass at least one of prompt / task / feature\",\r\n )\r\n }\r\n return runModelScript(deps, context, APP_FEATURE_VERIFY_PATH, flattenParams(params), {\r\n label: \"app_feature_verify\",\r\n })\r\n },\r\n }\r\n}\r\n", "import { spawn } from \"node:child_process\"\r\nimport type { A2HHostDeps, ToolContext } from \"../types\"\r\n\r\n/**\r\n * Shared spawn helpers for the a2h model-script tools.\r\n *\r\n * `runModelScript` resolves the model apiKey from the host and pipes it to the\r\n * spawned script over stdin (`--model-stdin`); the key traverses only the OS\r\n * pipe + process memory, never env/argv/disk, so it is invisible to the spawned\r\n * adb/hdc descendants. `runScriptCollect` spawns without stdin/throw for poll\r\n * tools whose non-zero exit codes are valid states. Both are pure spawn\r\n * mechanics \u2014 argv construction (camelCase \u2192 --kebab-case, null-skipping) lives\r\n * in each tool file, not here.\r\n */\r\n\r\n/** Cap stdout returned to the host so a runaway loop can't overflow the chat. */\r\nfunction cap(s: string, max = 50000): string {\r\n if (s.length <= max) return s\r\n const half = Math.floor(max / 2)\r\n return s.slice(0, half) + `\\n\u2026[truncated ${s.length - max} chars]\u2026\\n` + s.slice(-half)\r\n}\r\n\r\n/**\r\n * Shared runner: resolve host model params, spawn the script with the model\r\n * config piped to stdin, collect stdout/stderr, honor abort, and return the\r\n * capped stdout (or throw on non-zero exit).\r\n *\r\n * The apiKey is resolved from the host and piped to the script over stdin\r\n * (`--model-stdin`); the agent never handles it.\r\n */\r\nexport async function runModelScript(\r\n deps: A2HHostDeps,\r\n context: ToolContext,\r\n scriptPath: string,\r\n argv: string[],\r\n opts: { label?: string; throwOnNonZero?: boolean } = {},\r\n): Promise<{ ok: true; exitCode: number; output: string }> {\r\n const label = opts.label ?? scriptPath\r\n const model = await deps.resolveModelParams(context.worktree)\r\n if (!model || !model.apiKey) {\r\n throw new Error(\r\n `a2h (${label}): host did not provide model params (apiKey missing)`,\r\n )\r\n }\r\n\r\n const finalArgv = [...argv, \"--model-stdin\"]\r\n\r\n // Spawn via PATH `node`. process.execPath might be an older/different host\r\n // node (e.g. a bundled runtime), so prefer PATH resolution to match prior\r\n // working behavior. The script is precompiled .js, so no native type-stripping\r\n // is involved and it runs fine even though the path sits under node_modules.\r\n const child = spawn(\"node\", [scriptPath, ...finalArgv], {\r\n cwd: context.directory,\r\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\r\n // DevEco Code is a GUI host with no attached console; without windowsHide,\r\n // Windows allocates a new console window for the child \u2014 visible as a\r\n // flash on every a2h_self_test start and on every a2h_self_test_status\r\n // poll, and as a persistent window during a2h_app_feature_verify's long\r\n // sync run. Hide it.\r\n windowsHide: true,\r\n })\r\n\r\n // Pipe the model config to the child's stdin and close it \u2014 the key traverses\r\n // only the OS pipe, never env/argv/disk.\r\n child.stdin.end(\r\n JSON.stringify({ apiKey: model.apiKey, modelName: model.modelName, baseURL: model.baseURL }),\r\n )\r\n\r\n let stdout = \"\"\r\n let stderr = \"\"\r\n let spawnError = \"\"\r\n child.stdout.on(\"data\", (chunk: Buffer) => {\r\n const text = chunk.toString(\"utf8\")\r\n stdout += text\r\n // Surface the last non-empty line as a running title so the host can show\r\n // progress. Generic (not script-specific) \u2014 the agent sees no secrets.\r\n const lines = text.split(/\\r?\\n/)\r\n for (let i = lines.length - 1; i >= 0; i--) {\r\n const line = lines[i].trim()\r\n if (line) {\r\n try {\r\n context.metadata({ title: line.slice(0, 80) })\r\n } catch {\r\n /* host may not implement metadata \u2014 non-essential */\r\n }\r\n break\r\n }\r\n }\r\n })\r\n child.stderr.on(\"data\", (chunk: Buffer) => {\r\n stderr += chunk.toString(\"utf8\")\r\n })\r\n\r\n // Honor abort: kill the child if the host cancels the tool call.\r\n const onAbort = () => {\r\n if (!child.killed) child.kill(\"SIGTERM\")\r\n }\r\n context.abort.addEventListener(\"abort\", onAbort)\r\n\r\n const code: number = await new Promise((resolve) => {\r\n child.on(\"close\", resolve)\r\n child.on(\"error\", (err: Error) => {\r\n spawnError = err.message\r\n resolve(-1)\r\n })\r\n })\r\n context.abort.removeEventListener(\"abort\", onAbort)\r\n\r\n if (code !== 0 && opts.throwOnNonZero !== false) {\r\n if (spawnError) {\r\n throw new Error(`a2h (${label}): failed to spawn node \u2014 ${spawnError}`)\r\n }\r\n const tail = stderr.slice(-2000)\r\n throw new Error(\r\n `a2h (${label}) failed (exit ${code}): ${tail || \"(no stderr)\"}`,\r\n )\r\n }\r\n return { ok: true, exitCode: code, output: cap(stdout) }\r\n}\r\n\r\n/**\r\n * Spawn a script, collect stdout, and return it WITHOUT throwing on non-zero\r\n * exit. Used by tools whose non-zero exit codes are valid states (e.g.\r\n * self-test-runner `--status` exits 2=RUNNING / 3=CRASHED / 4=NOT_STARTED).\r\n * No stdin injection, no model params \u2014 for status/poll tools that need no key.\r\n * Only throws on a true spawn failure (node not found, etc.).\r\n */\r\nexport async function runScriptCollect(\r\n scriptPath: string,\r\n argv: string[],\r\n context: ToolContext,\r\n label = scriptPath,\r\n): Promise<{ ok: true; exitCode: number; output: string }> {\r\n const child = spawn(\"node\", [scriptPath, ...argv], {\r\n cwd: context.directory,\r\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\r\n // Hide the console window \u2014 this runs on every a2h_self_test_status poll,\r\n // so without it the host machine flashes a cmd window repeatedly.\r\n windowsHide: true,\r\n })\r\n let stdout = \"\"\r\n let stderr = \"\"\r\n let spawnError = \"\"\r\n child.stdout.on(\"data\", (chunk: Buffer) => {\r\n stdout += chunk.toString(\"utf8\")\r\n })\r\n child.stderr.on(\"data\", (chunk: Buffer) => {\r\n stderr += chunk.toString(\"utf8\")\r\n })\r\n const onAbort = () => {\r\n if (!child.killed) child.kill(\"SIGTERM\")\r\n }\r\n context.abort.addEventListener(\"abort\", onAbort)\r\n const code: number = await new Promise((resolve) => {\r\n child.on(\"close\", resolve)\r\n child.on(\"error\", (err: Error) => {\r\n spawnError = err.message\r\n resolve(-1)\r\n })\r\n })\r\n context.abort.removeEventListener(\"abort\", onAbort)\r\n if (spawnError) {\r\n throw new Error(`a2h (${label}): failed to spawn node \u2014 ${spawnError}`)\r\n }\r\n return { ok: true, exitCode: code, output: cap(stdout) }\r\n}\r\n", "import path from \"node:path\"\r\nimport type { A2HHostDeps, ToolDefinition } from \"../types\"\r\nimport { PKG_ROOT } from \"../paths\"\r\nimport { runModelScript } from \"./spawn-helpers\"\r\n\r\n/**\r\n * self-test-runner.mjs absolute path \u2014 shared with a2h-self-test-status.ts\r\n * (the status tool polls the very runner this tool starts). Runs under node fine\r\n * inside node_modules (.mjs, no native type-stripping concern).\r\n */\r\nexport const SELF_TEST_RUNNER = path.join(\r\n PKG_ROOT,\r\n \"skills\",\r\n \"hmos-integration-test\",\r\n \"scripts\",\r\n \"self-test-runner.mjs\",\r\n)\r\n\r\n/**\r\n * a2h_self_test \u2014 start an on-device self-test.\r\n *\r\n * Spawns self-test-runner.mjs in RUN mode WITHOUT --timeout. self-test-runner\r\n * installs the HAP, spawns batch_runner detached, prints\r\n * `{status:\"RUNNING\", pid, task_dir, task_subdir}` and exits immediately \u2014 so\r\n * this tool call returns fast (does NOT block for the test duration). The agent\r\n * polls `a2h_self_test_status` until a terminal state, and kills `pid` via Bash\r\n * if it exceeds the elapsed budget.\r\n *\r\n * The model apiKey is resolved from the host and piped to the script's stdin\r\n * (`--model-stdin`, injected by runModelScript); the agent never handles it.\r\n * self-test-runner writes the key into a temp autotest.yaml (0o600) in\r\n * task_subdir \u2014 batch_runner only accepts `--config <yaml file>` (third-party\r\n * constraint). The temp yaml is deleted by `--status` once the run reaches a\r\n * terminal state. The key never enters env/argv (hdc descendants can't see it).\r\n */\r\nexport function makeA2hSelfTestTool(deps: A2HHostDeps): ToolDefinition {\r\n return {\r\n description:\r\n \"\u542F\u52A8 HarmonyOS \u5E94\u7528\u81EA\u6D4B:\u5B89\u88C5 HAP,spawn AutoTest batch_runner(detached) \u540E\u7ACB\u5373\u8FD4\u56DE RUNNING + task_subdir,\u4E0D\u963B\u585E\u7B49\u5F85\u5B8C\u6210\u3002\u7528 a2h_self_test_status \u8F6E\u8BE2\u7EC8\u6001,\u8D85\u65F6\u5219\u7528 Bash kill \u6389\u8FD4\u56DE\u7684 pid\u3002\u6A21\u578B\u914D\u7F6E(apiKey \u7B49)\u7531\u5DE5\u5177\u5185\u90E8\u7ECF stdin \u6CE8\u5165,\u4E0D\u8981\u4F20 apiKey/modelStdin \u5B57\u6BB5\u3002\\n\\n\" +\r\n \"| \u5B57\u6BB5 | \u7C7B\u578B | \u7EA6\u675F | \u8BF4\u660E |\\n\" +\r\n \"|---|---|---|---|\\n\" +\r\n \"| testcases | string | \u5FC5\u586B | testcases.json \u8DEF\u5F84 |\\n\" +\r\n \"| hap | string | \u5FC5\u586B | \u7B7E\u540D\u5305(.hap/.hsp \u6587\u4EF6/\u76EE\u5F55,\u9017\u53F7\u5206\u9694\u591A\u5305) |\\n\" +\r\n \"| bundleName | string | \u5FC5\u586B | \u5E94\u7528\u5305\u540D |\\n\" +\r\n \"| taskDir | string | \u5FC5\u586B | \u4EFB\u52A1\u8F93\u51FA\u76EE\u5F55(\u901A\u5E38 <output>/task) |\\n\" +\r\n \"| outputDir | string | \u53EF\u9009 | \u65E5\u5FD7\u76EE\u5F55(\u901A\u5E38 <output>);\u4E0D\u4F20\u5219\u7528 taskDir |\\n\" +\r\n \"| category | string | \u53EF\u9009 | \u6D4B\u8BD5\u5206\u7C7B\u540D(\u9ED8\u8BA4 self_test,\u901A\u5E38\u4F20 app_name) |\\n\" +\r\n \"| mode | string | \u53EF\u9009 | single(\u9ED8\u8BA4)\u6216 layered(Planner+Executor \u53CC Agent) |\\n\\n\" +\r\n \"\u8FD4\u56DE {ok, exitCode, output},output \u4E3A self-test-runner stdout JSON\u3002\u6B63\u5E38\u4E3A {status:\\\"RUNNING\\\", pid, task_dir, task_subdir}(\u5B58 pid \u4E0E task_subdir \u4F9B\u8F6E\u8BE2\u4E0E\u8D85\u65F6 kill);\u73AF\u5883\u5931\u8D25\u65F6\u4E3A {status:\\\"FAILED\\\", error}(\u5982 @autotest/agent \u81EA\u52A8\u5B89\u88C5\u5931\u8D25\u6216 HAP \u5B89\u88C5/\u7528\u4F8B\u51C6\u5907\u629B\u9519)\u2014\u2014\u6B64\u65F6\u4E0D\u8981\u8F6E\u8BE2,\u76F4\u63A5\u628A error \u62A5\u7ED9\u7528\u6237\u5E76\u505C\u6B62\u3002\u5DE5\u5177\u5BF9 FAILED \u4E0D\u629B\u9519(\u975E\u96F6\u9000\u51FA\u7801 = FAILED),\u9700\u89E3\u6790 output \u6309 status \u5206\u652F\u3002\",\r\n args: {\r\n testcases: { type: \"string\", description: \"testcases.json \u8DEF\u5F84\" },\r\n hap: { type: \"string\", description: \"\u7B7E\u540D\u5305\u8DEF\u5F84(.hap/.hsp \u6587\u4EF6/\u76EE\u5F55,\u9017\u53F7\u5206\u9694)\" },\r\n bundleName: { type: \"string\", description: \"\u5E94\u7528\u5305\u540D\" },\r\n taskDir: { type: \"string\", description: \"\u4EFB\u52A1\u8F93\u51FA\u76EE\u5F55,\u901A\u5E38 <output>/task\" },\r\n outputDir: { type: [\"string\", \"null\"], description: \"\u65E5\u5FD7\u76EE\u5F55,\u53EF\u9009;\u4E0D\u4F20\u5219\u7528 taskDir\" },\r\n category: { type: [\"string\", \"null\"], description: \"\u6D4B\u8BD5\u5206\u7C7B\u540D,\u53EF\u9009;\u9ED8\u8BA4 self_test,\u901A\u5E38\u4F20 app_name\" },\r\n mode: { type: [\"string\", \"null\"], description: \"single \u6216 layered,\u53EF\u9009;\u9ED8\u8BA4 single\" },\r\n },\r\n execute: async (args, context) => {\r\n const argv: string[] = []\r\n const push = (flag: string, v: unknown) => {\r\n if (v == null || v === \"\") return\r\n argv.push(flag, String(v))\r\n }\r\n push(\"--testcases\", args.testcases)\r\n push(\"--hap\", args.hap)\r\n push(\"--bundle-name\", args.bundleName)\r\n push(\"--task-dir\", args.taskDir)\r\n push(\"--output-dir\", args.outputDir)\r\n push(\"--category\", args.category)\r\n push(\"--mode\", args.mode)\r\n // No --timeout: self-test-runner spawns batch_runner detached and returns\r\n // RUNNING immediately. Poll the terminal state via a2h_self_test_status.\r\n // runModelScript appends --model-stdin and pipes the key over stdin.\r\n return runModelScript(deps, context, SELF_TEST_RUNNER, argv, {\r\n label: \"self_test\",\r\n // Non-zero exit = a FAILED start (failJson prints {status:\"FAILED\",error}\r\n // and exits 1). Return it as output -- not throw -- so the agent can read\r\n // the structured error; the description contract tells it to branch on\r\n // status instead of polling. (Normal RUNNING start also exits 0.)\r\n throwOnNonZero: false,\r\n })\r\n },\r\n }\r\n}\r\n", "import type { ToolDefinition } from \"../types\"\r\nimport { runScriptCollect } from \"./spawn-helpers\"\r\nimport { SELF_TEST_RUNNER } from \"./a2h-self-test\"\r\n\r\n/**\r\n * a2h_self_test_status \u2014 poll a self-test run started by a2h_self_test.\r\n *\r\n * Spawns self-test-runner.mjs `--status` mode, which reads summary.json /\r\n * task_results.jsonl / log mtime and returns the current state. Needs NO apiKey\r\n * (status mode never calls the model). Non-zero exit codes are valid states\r\n * (0=COMPLETED, 2=RUNNING, 3=CRASHED, 4=NOT_STARTED), so runScriptCollect does\r\n * not throw on them. On COMPLETED/CRASHED, self-test-runner deletes the temp\r\n * autotest.yaml (the one place the key sat on disk).\r\n *\r\n * Imports `SELF_TEST_RUNNER` from ./a2h-self-test \u2014 the status tool polls the\r\n * very runner the start tool spawns, so the script path is owned there.\r\n */\r\nexport function makeA2hSelfTestStatusTool(): ToolDefinition {\r\n return {\r\n description:\r\n \"\u67E5\u8BE2 a2h_self_test \u542F\u52A8\u7684\u81EA\u6D4B\u6267\u884C\u72B6\u6001(\u8BFB summary.json / task_results.jsonl / \u65E5\u5FD7 mtime)\u3002\u4E0D\u9700\u8981 apiKey\u3002\u53CD\u590D\u8C03\u7528\u76F4\u5230\u7EC8\u6001(COMPLETED/CRASHED/NOT_STARTED),RUNNING \u5219\u95F4\u9694\u540E\u91CD\u8BD5\u3002\\n\\n\" +\r\n \"| \u5B57\u6BB5 | \u7C7B\u578B | \u7EA6\u675F | \u8BF4\u660E |\\n\" +\r\n \"|---|---|---|---|\\n\" +\r\n \"| taskDir | string | \u5FC5\u586B | a2h_self_test \u8FD4\u56DE\u7684 task_dir |\\n\" +\r\n \"| outputDir | string | \u53EF\u9009 | \u65E5\u5FD7\u76EE\u5F55(\u540C a2h_self_test \u7684 outputDir) |\\n\\n\" +\r\n \"\u8FD4\u56DE {ok, exitCode, output},output \u4E3A\u72B6\u6001 JSON:\\n\" +\r\n \"- COMPLETED(exit 0):{status, task_subdir, cases_done, pass_count, fail_count, unknown_count, pass_rate, log_tail}\\n\" +\r\n \"- RUNNING(exit 2):{status, cases_done, last_case, task_subdir, log_tail}\\n\" +\r\n \"- CRASHED(exit 3):\u540C RUNNING \u5F62\u72B6\\n\" +\r\n \"- NOT_STARTED(exit 4):{status:\\\"NOT_STARTED\\\"}\\n\" +\r\n \"COMPLETED/CRASHED \u65F6 runner \u4F1A\u5220\u9664\u4E34\u65F6 autotest.yaml(\u542B key)\u3002\",\r\n args: {\r\n taskDir: { type: \"string\", description: \"a2h_self_test \u8FD4\u56DE\u7684 task_dir\" },\r\n outputDir: { type: [\"string\", \"null\"], description: \"\u65E5\u5FD7\u76EE\u5F55,\u53EF\u9009\" },\r\n },\r\n execute: async (args, context) => {\r\n const argv: string[] = [\"--status\", \"--task-dir\", String(args.taskDir)]\r\n if (args.outputDir != null && args.outputDir !== \"\") {\r\n argv.push(\"--output-dir\", String(args.outputDir))\r\n }\r\n return runScriptCollect(SELF_TEST_RUNNER, argv, context, \"self_test_status\")\r\n },\r\n }\r\n}\r\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["path", "path", "path", "path", "path", "path", "path", "path"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "android2harmony",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "HomeTrans Plugin for DevEco Code — standalone plugin containing A2H skills, agents, and tools for converting Android apps to HarmonyOS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|