/** * hwlab-code-agent 客户端核心逻辑 * 直接调 HWLAB v0.2 cloud-api,自动识别环境变量 HWLAB_API_KEY * 遵循 cli-spec:配置从 config.json 单一来源 */ import { readFileSync } from "node:fs"; import path from "node:path"; // ---- config ---- const SKILL_ROOT = path.resolve(import.meta.dir, "..", ".."); const configPath = path.join(SKILL_ROOT, "config.json"); interface Config { apiBaseUrl: string; defaultProjectId: string; defaultPollTimeoutMs: number; } const INHERITED_PROVIDER_PROFILE_ENV = "HWLAB_CODE_AGENT_PROVIDER_PROFILE"; const PARENT_TRACE_ID_ENV = "HWLAB_CODE_AGENT_PARENT_TRACE_ID"; let _cfg: Config | null = null; function config(): Config { if (!_cfg) { try { _cfg = JSON.parse(readFileSync(configPath, "utf-8")) as Config; } catch { throw new Error(`hwlab-code-agent: failed to load config from ${configPath}`); } if (!_cfg.apiBaseUrl) throw new Error("hwlab-code-agent: config.apiBaseUrl is required"); } return _cfg; } function textValue(value: string | undefined): string { return String(value ?? "").trim(); } function resolveProviderProfile(args: Record) { const explicit = textValue(args.profile || args.providerProfile); if (explicit) return { profile: explicit, source: "explicit" }; const inherited = textValue(process.env[INHERITED_PROVIDER_PROFILE_ENV]); if (inherited) return { profile: inherited, source: "env" }; fail("provider_profile_required", `spawn requires --profile or ${INHERITED_PROVIDER_PROFILE_ENV}`); } const REMOVED_WORKSPACE_FILE_ARGS = new Map([ ["specPath", "--specPath"], ["spec-path", "--spec-path"] ]); const REMOVED_WORKSPACE_FILE_ENV = ["HWPOD_SPEC_CONTENT", "HWPOD_SPEC"]; export function findRemovedWorkspaceFileInput(args: Record, env: Record = process.env): string | null { for (const [key, label] of REMOVED_WORKSPACE_FILE_ARGS.entries()) { if (textValue(args[key])) return label; } for (const name of REMOVED_WORKSPACE_FILE_ENV) { if (textValue(env[name])) return name; } return null; } export function createAgentChatRequestBody(input: { message: string; providerProfile: string; sessionId: string; conversationId: string; projectId: string; traceId: string; }): Record { return { message: input.message, providerProfile: input.providerProfile, sessionId: input.sessionId, conversationId: input.conversationId, projectId: input.projectId, traceId: input.traceId, shortConnection: true }; } function apiKey(): string { const key = process.env.HWLAB_API_KEY; if (!key) throw new Error("HWLAB_API_KEY not set in environment"); return key; } function authHeaders(): Record { return { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey()}` }; } // ---- JSON out ---- function out(data: Record, exitCode = 0): void { process.stdout.write(JSON.stringify(data, null, 2) + "\n"); if (exitCode !== 0) process.exit(exitCode); } function fail(code: string, msg: string, exitCode = 1): never { process.stderr.write(JSON.stringify({ ok: false, error: { code, message: msg } }) + "\n"); process.exit(exitCode); } // ---- API ---- export interface SpawnResult { ok: boolean; action: string; sessionId: string; traceId: string; conversationId: string; accepted: boolean; acceptedBody: unknown; } export async function spawn(args: Record): Promise { const cfg = config(); const resolvedProfile = resolveProviderProfile(args); const profile = resolvedProfile.profile; const projectId = args.projectId || cfg.defaultProjectId; const message = args.message || (args.messageFile ? readFileSync(args.messageFile, "utf-8") : null); if (!message) fail("message_required", "--message or --message-file required"); const removedWorkspaceFileInput = findRemovedWorkspaceFileInput(args); if (removedWorkspaceFileInput) { fail("legacy_workspace_files_removed", `${removedWorkspaceFileInput} was removed; use AgentRun ResourceBundleRef kind=gitbundle with bundles[]`); } // session let sessionId = args.sessionId || ""; let conversationId = args.conversationId || ""; if (!sessionId) { const r = await fetch(`${cfg.apiBaseUrl}/v1/agent/sessions`, { method: "POST", headers: authHeaders(), body: JSON.stringify({ providerProfile: profile, projectId }) }); const b = await r.json() as Record; if (!r.ok) fail("session_create_failed", JSON.stringify(b)); const s = (b.session || b) as Record; sessionId = s.sessionId as string; conversationId = conversationId || (s.conversationId as string) || ""; } if (!conversationId) conversationId = `cnv_cli_${Date.now()}`; // submit const traceId = `trc_${profile}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`; const r = await fetch(`${cfg.apiBaseUrl}/v1/agent/chat`, { method: "POST", headers: { ...authHeaders(), "prefer": "respond-async", "x-trace-id": traceId, "x-hwlab-short-connection": "1" }, body: JSON.stringify(createAgentChatRequestBody({ message, providerProfile: profile, sessionId, conversationId, projectId, traceId })) }); const b = await r.json(); out({ ok: true, action: "spawn", sessionId, traceId, conversationId, providerProfile: profile, resolvedProviderProfile: profile, profileSource: resolvedProfile.source, parentTraceId: textValue(process.env[PARENT_TRACE_ID_ENV]) || null, accepted: r.ok, acceptedBody: b, pollCommand: `bun scripts/hwlab-code-agent-cli.ts poll ${traceId}`, resultCommand: `bun scripts/hwlab-code-agent-cli.ts result ${traceId}` }); } export async function poll(traceId: string, args: Record): Promise { const cfg = config(); const timeout = parseInt(args.timeout || String(cfg.defaultPollTimeoutMs), 10); const deadline = Date.now() + timeout; while (Date.now() < deadline) { const r = await fetch(`${cfg.apiBaseUrl}/v1/agent/chat/result/${encodeURIComponent(traceId)}`, { headers: authHeaders() }); const b = await r.json() as Record; const s = (b.status || b.traceStatus) as string; if (s === "completed" || s === "result") { out({ ok: true, action: "poll", traceId, status: "completed", body: b }); return; } if (s === "failed" || s === "error") { fail("poll_failed", JSON.stringify(b)); } await new Promise(r => setTimeout(r, 2000)); } fail("poll_timeout", `timeout after ${timeout}ms`); } export async function getResult(traceId: string): Promise { const cfg = config(); const r = await fetch(`${cfg.apiBaseUrl}/v1/agent/chat/result/${encodeURIComponent(traceId)}`, { headers: authHeaders() }); out({ ok: r.ok, action: "result", traceId, body: await r.json() }); } export async function getTrace(traceId: string, args: Record): Promise { const cfg = config(); const q = args.full ? "?full=true" : ""; const r = await fetch(`${cfg.apiBaseUrl}/v1/agent/chat/trace/${encodeURIComponent(traceId)}${q}`, { headers: authHeaders() }); out({ ok: r.ok, action: "trace", traceId, body: await r.json() }); }