Files
pikasTech-HWLAB/skills/hwlab-code-agent/scripts/src/client.ts
T
2026-06-08 12:19:31 +08:00

203 lines
7.2 KiB
TypeScript

/**
* 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<string, string | undefined>) {
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<string, string | undefined>, env: Record<string, string | undefined> = 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<string, unknown> {
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<string, string> {
return { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey()}` };
}
// ---- JSON out ----
function out(data: Record<string, unknown>, 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<string, string | undefined>): Promise<void> {
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<string, unknown>;
if (!r.ok) fail("session_create_failed", JSON.stringify(b));
const s = (b.session || b) as Record<string, unknown>;
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<string, string | undefined>): Promise<void> {
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<string, unknown>;
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<void> {
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<string, string | undefined>): Promise<void> {
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() });
}