fix: improve device pod trace diagnostics

This commit is contained in:
Codex
2026-05-28 12:22:34 +08:00
parent da06165cac
commit 067ccf6b7e
6 changed files with 200 additions and 27 deletions
+127 -8
View File
@@ -32,6 +32,9 @@ function classifyBlocker(message) {
if (/profile not found/u.test(message)) return 'profile-missing';
if (/profile invalid|requires/u.test(message)) return 'profile-invalid';
if (/approval/u.test(message)) return 'approval-required';
if (/resource is not registered under the requested gateway session|resourceGatewaySessionId/u.test(message)) return 'gateway-session-mismatch';
if (/gateway session .*not found|gateway session .*not registered|gateway.*not.*registered/u.test(message)) return 'gateway-session-unavailable';
if (/spawn C:\\Windows\\system32\\cmd\.exe ENOENT|cmd\.exe ENOENT/u.test(message)) return 'host-cmd-unavailable';
if (/unsupported|invalid target selector|invalid io-probe (?:path|selector)/u.test(message)) return 'invalid-request';
return 'operation-failed';
}
@@ -46,6 +49,8 @@ function nextForBlocker(blocker, details = {}) {
];
}
if (blocker === 'profile-invalid') {
const names = availableProfileNames();
if (names.length > 1) return profileChoiceNext(names);
return [
`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId}`,
`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile show --pod-id ${podId}`
@@ -60,9 +65,39 @@ function nextForBlocker(blocker, details = {}) {
`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId}`
];
}
if (blocker === 'gateway-session-mismatch') {
return [
`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId} --probe --full`,
'update the profile gatewaySessionId to the resourceGatewaySessionId reported by cloud-api, then rerun doctor --probe',
'do not bypass device-pod-cli with generic gateway shell'
];
}
if (blocker === 'gateway-session-unavailable') {
return [
`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId} --probe --full`,
'restart or reconnect the target hwlab-gateway session, then rerun the same device-pod-cli command',
'do not guess another gateway session without updating the profile'
];
}
if (blocker === 'host-cmd-unavailable') {
return [
`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId} --probe --full`,
'the gateway route was reached but cannot spawn Windows cmd.exe; repair the Windows gateway host or use a profile bound to a Windows cmd-capable gateway',
'stop retrying lower-level gateway shell commands until doctor --probe is green'
];
}
return [`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId}`];
}
function profileChoiceNext(names) {
const script = 'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs';
return [
`${script} profile list`,
...names.map((name) => `${script} doctor --pod-id ${name}`),
...names.map((name) => `${script} health --pod-id ${name}`)
];
}
function podIdFromMessage(message) {
const match = String(message || '').match(/([A-Za-z0-9_.-]+)\.json\b/u);
return match?.[1] || null;
@@ -143,11 +178,15 @@ function parseOptions(argv) {
function resolvePodId(requested) {
if (requested) return requested;
if (process.env.DEVICE_POD_ID) return process.env.DEVICE_POD_ID;
const names = fs.existsSync(PROFILE_DIR)
? fs.readdirSync(PROFILE_DIR).filter((name) => name.endsWith('.json')).map((name) => path.basename(name, '.json'))
: [];
const names = availableProfileNames();
if (names.length === 1) return names[0];
throw new Error('profile invalid: pass <devicePodId>:<surface> or set DEVICE_POD_ID when multiple/no profiles exist');
throw new Error(`profile invalid: pass <devicePodId>:<surface> or set DEVICE_POD_ID when multiple/no profiles exist; available profiles: ${names.join(', ') || '(none)'}`);
}
function availableProfileNames() {
return fs.existsSync(PROFILE_DIR)
? fs.readdirSync(PROFILE_DIR).filter((name) => name.endsWith('.json')).map((name) => path.basename(name, '.json')).sort()
: [];
}
function readProfile(podId) {
@@ -228,7 +267,7 @@ function conStart710007511Profile(overrides = {}) {
]
},
cloudApiUrl: 'http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667',
gatewaySessionId: 'gws_DESKTOP-1MHOD9I',
gatewaySessionId: 'gws_YUAN',
resourceId: 'res_windows_host',
capabilityId: 'cap_windows_cmd_exec',
hostWorkspaceRoot: 'F:\\Work\\ConStart\\projects\\71-00075-11',
@@ -302,18 +341,22 @@ function profileMissingDoctor(podId, file) {
};
}
function doctor(parsed) {
async function doctor(parsed) {
const podId = parsed.podId || parsed._[0] || process.env.DEVICE_POD_ID || null;
try {
const profile = readProfile(podId);
const script = 'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs';
const selector = `${profile.podId}:workspace:/`;
const hostProfile = hostProfilePath(profile);
const hostHealthCommand = buildHostHealthCommand(profile);
const probe = parsed.probe === true
? await probeHostHealth(profile, hostHealthCommand, parsed)
: undefined;
const uploadRoot = `${profile.gatewaySessionId}:${String(profile.hostWorkspaceRoot || '').replace(/^[A-Za-z]:/u, (drive) => `/${drive[0].toLowerCase()}`).replace(/\\/gu, '/')}`;
print({
ok: true,
action: 'doctor',
status: 'succeeded',
status: probe?.ok === false ? 'degraded' : 'succeeded',
...profileSummary(profile),
profileDir: PROFILE_DIR,
route: routeSummary(profile),
@@ -327,10 +370,12 @@ function doctor(parsed) {
show: `${script} profile show --pod-id ${profile.podId}`,
health: `${script} health --pod-id ${profile.podId}`,
plan: `${script} ${selector} ls --dry-run`,
probe: `${script} doctor --pod-id ${profile.podId} --probe`,
uploadHostCli: `node /app/tools/tran.mjs ${uploadRoot} upload /app/skills/device-pod-cli/assets/device-host-cli.mjs tools/device-host-cli.mjs`,
uploadProfile: `node /app/tools/tran.mjs ${uploadRoot} upload ${profile.__profilePath} .device-pod/${profile.podId}.json`,
hostHealth: `node /app/tools/tran.mjs ${uploadRoot} cmd -- node tools\\device-host-cli.mjs --profile ${hostProfile} --pod-id ${profile.podId} health`
},
probe,
valuesRedacted: true,
secretMaterialStored: false
});
@@ -345,6 +390,35 @@ function doctor(parsed) {
}
}
function buildHostHealthCommand(profile) {
const hostArgs = ['--profile', hostProfilePath(profile), '--pod-id', profile.podId, 'health'];
return `${profile.hostCli} ${hostArgs.map(cmdQuote).join(' ')}`;
}
async function probeHostHealth(profile, command, parsed) {
const result = await invoke(profile, command, {
...parsed,
timeoutMs: Number(parsed.timeoutMs || 10000),
traceId: parsed.traceId || `trc_device_pod_doctor_${Date.now()}`
});
const diagnosis = diagnoseInvokeFailure(profile, result, { operation: 'doctor.host-health' });
return {
ok: result.status === 'succeeded',
status: result.status,
command,
traceId: result.traceId,
operationId: result.operationId,
requestId: result.requestId,
httpStatus: result.httpStatus,
blocker: diagnosis.blocker,
next: diagnosis.next,
dispatch: parsed.full ? result.dispatch : compactDispatch(result.dispatch),
hostJson: result.hostJson,
evidence: { auditId: result.dispatch.auditId, evidenceId: result.dispatch.evidenceId },
gatewayBody: parsed.full ? result.gatewayBody : undefined
};
}
function handleProfileCommand(parsed) {
const subcommand = parsed._[0] || 'list';
if (subcommand === 'list') return listProfiles();
@@ -477,6 +551,35 @@ async function invoke(profile, command, parsed) {
return { requestId, operationId, traceId, httpStatus: response.status, status, dispatch, hostJson, gatewayBody: response.body };
}
function diagnoseInvokeFailure(profile, result, context = {}) {
if (result.status === 'succeeded') {
return { ok: true, blocker: null, next: [] };
}
const text = JSON.stringify({
error: result.gatewayBody?.error,
dispatch: result.dispatch,
hostJson: result.hostJson
});
const blocker = classifyBlocker(text);
const next = nextForBlocker(blocker, { podId: profile.podId, message: text });
return {
ok: false,
blocker,
next,
operation: context.operation || null,
summary: summarizeFailureText(text),
route: routeSummary(profile),
valuesRedacted: true,
secretMaterialStored: false
};
}
function summarizeFailureText(text) {
return String(text || '')
.replace(/\s+/gu, ' ')
.slice(0, 500);
}
function requestJson(url, options = {}) {
const body = JSON.stringify(options.body || {});
const target = new URL(url);
@@ -533,6 +636,7 @@ function hostJsonSummary(hostJson) {
function help() {
print({ ok: true, action: 'help', scope: 'HWLAB internal code agents only; not for external UniDesk workspaces.', usage: [
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id device-pod-71-freq',
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id device-pod-71-freq --probe',
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile list',
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile show --pod-id device-pod-71-freq',
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile create --pod-id device-pod-71-00075-11 --force',
@@ -553,6 +657,20 @@ async function main() {
const parsed = parseOptions(argv);
if (selector.kind === 'doctor') return doctor(parsed);
if (selector.kind === 'profile') return handleProfileCommand(parsed);
if (selector.kind === 'health' && !parsed.podId && !process.env.DEVICE_POD_ID) {
const names = availableProfileNames();
if (names.length !== 1) return print({
ok: false,
action: 'health',
status: 'failed',
blocker: 'profile-invalid',
profileDir: PROFILE_DIR,
availableProfiles: names,
next: profileChoiceNext(names),
valuesRedacted: true,
secretMaterialStored: false
}, 1);
}
const profile = readProfile(selector.podId || parsed.podId);
if (selector.kind === 'health') return print({ ok: true, action: 'health', status: 'succeeded', ...profileSummary(profile), route: routeSummary(profile), next: [`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${profile.podId}`] });
const built = await buildHostCommand(selector, parsed);
@@ -561,7 +679,8 @@ async function main() {
if (parsed.dryRun) return print({ ok: true, action: 'device-pod.plan', status: 'planned', selector, operation: built.operation, mutating: built.mutating, command, ...profileSummary(profile), route: routeSummary(profile) });
const result = await invoke(profile, command, parsed);
const ok = result.status === 'succeeded';
print({ ok, action: 'device-pod.invoke', status: result.status, selector, operation: built.operation, mutating: built.mutating, command, ...profileSummary(profile), route: routeSummary(profile), traceId: result.traceId, operationId: result.operationId, requestId: result.requestId, httpStatus: result.httpStatus, ...hostJsonSummary(result.hostJson), dispatch: parsed.full ? result.dispatch : compactDispatch(result.dispatch), hostJson: result.hostJson, evidence: { auditId: result.dispatch.auditId, evidenceId: result.dispatch.evidenceId }, gatewayBody: parsed.full ? result.gatewayBody : undefined }, ok ? 0 : 1);
const diagnosis = ok ? null : diagnoseInvokeFailure(profile, result, { operation: built.operation });
print({ ok, action: 'device-pod.invoke', status: result.status, selector, operation: built.operation, mutating: built.mutating, command, ...profileSummary(profile), route: routeSummary(profile), traceId: result.traceId, operationId: result.operationId, requestId: result.requestId, httpStatus: result.httpStatus, blocker: diagnosis?.blocker, next: diagnosis?.next, diagnosis, ...hostJsonSummary(result.hostJson), dispatch: parsed.full ? result.dispatch : compactDispatch(result.dispatch), hostJson: result.hostJson, evidence: { auditId: result.dispatch.auditId, evidenceId: result.dispatch.evidenceId }, gatewayBody: parsed.full ? result.gatewayBody : undefined }, ok ? 0 : 1);
}
main().catch((error) => fail('device-pod-cli', error));