Files
Arcrun/cli/src/commands/push.ts
T
uncle6me-web 7e631a890a t158 迴歸修復「部署≠發現」:複製路徑回毫秒級純編圖,誠實化只留在 discover
leo 07-31 定調:「這裡只是複製一些工作流的 data 過去,沒有要在這裡驗證,難怪這麼慢。
就算是我自己寫了錯的工作流,也可以跑跑看,如果錯誤就修改,
沒有說有錯誤還要一個個驗證這回事。」

定性=迴歸非新設計:部署路徑純複製是既有設計(arcrun-rag installer/src/index.js:13
「workflows.json 是既有 workflows/*.local.yaml 的搬運(打包期抽 flow/config)」+
arcrun-rag wiki「workflow 打包期預編成 workflows.json(worker 免帶 parser)」)。
5cadc60 起誠實化漏進 /cypher/search ⇒ 複製路徑也逐節點跑兩庫查詢+相似搜尋
(每 missing 節點 1+9 次 HTTP+recipe KV 掃)⇒ 冷實例 8 節點實測 25.7s、
安裝器 15s timeout 必炸(leo stage 實走 rag_takedown_direct aborted)。

改動:
- /cypher/search 加 mode:compile=純編圖零查詢(安裝器/acr push 複製路徑);
  discover=誠實查詢預設(AI 問「有沒有」的既有契約,not_found+分型指路全保留)
- /cypher/execute 一律 compile(存在性由 component-loader 執行時決定=原權威)
- compile 的節點 status 標 unchecked(誠實「沒查」,不回假 found)
- discover 批次化:registry 新增 GET /components/catalog(一次回全目錄含
  input_schema,補 CP2-B「沒有列表端點」缺口)+recipe 清單一次抓,
  存在判定與相似度全記憶體比對;舊 registry 無 catalog 端點 → 退回逐顆(相容);
  registry 整個查不通 → unknown 照舊(不誤判 not_found)
- cli push 帶 mode:compile+拔 missing 擋(push 不看 missing;要問有沒有走 validate)

驗(本地 wrangler dev 誠實環境,registry 種 20 合約):
- compile 編圖:graph_neighbors 39ms/rag_chat(11節點) 3ms/rag_ingest_card 2ms/
  rag_takedown_direct(8節點) 3ms——回迴歸前毫秒級
- 安裝器 pushWorkflowTo(mode:compile)4/4 ok(44/7/7/5ms)
- 故意引用不存在零件的 workflow:部署 ok=true,trigger 執行時誠實報
  「找不到零件…」+可用零件清單(部署≠發現實證)
- discover 契約:邏輯名 missing=2+not_found+suggestion(21ms);
  頂層 verify.sh 01 組 5/5+03 組 4/4 全綠
- cypher+registry tsc 全綠;vitest 9 failed/179 passed=5cadc60 基線完全相同

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 13:31:33 +08:00

151 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* acr push <workflow.yaml>
*
* 解析 workflow.yaml,透過 /cypher/search 取得執行圖,
* 然後 POST 至 cypher.arcrun.dev/webhooks/named(帶 X-Arcrun-API-Key)。
* Server 以 {api_key}:wf:{name} 為 KV key 存入 WEBHOOKS KV。
*
* 不再需要用戶的 CF API Token 或 KV Namespace ID。
*/
import chalk from 'chalk';
import ora from 'ora';
import { loadConfig, getCypherExecutorUrl } from '../lib/config.js';
import { loadWorkflowYaml, parseTriplets, validateRelations } from '../lib/yaml-parser.js';
export async function cmdPush(filePath: string): Promise<void> {
const config = loadConfig();
if (config.mode === 'local') {
console.error(chalk.red('Local 模式不支援 acr pushWebhook 部署需要 Standard 模式)。'));
console.log(chalk.gray('請執行 acr init 取得 API Key,或直接用 acr run 本機測試。'));
process.exit(1);
}
if (!config.api_key) {
if (config.mode === 'self-hosted') {
console.error(chalk.red('缺少 NAMESPACE(你的資料分區標籤)。'));
console.log(chalk.gray('在專案 .env 設一行(明碼即可):'));
console.log(chalk.cyan(' NAMESPACE=leo'));
} else {
console.error(chalk.red('缺少 api_key,請重新執行 acr init。'));
}
process.exit(1);
}
// 解析 YAML
const spinner = ora('解析 workflow.yaml').start();
let workflow;
try {
workflow = loadWorkflowYaml(filePath);
const triplets = parseTriplets(workflow.flow);
validateRelations(triplets);
spinner.succeed(`解析完成:${workflow.name}${triplets.length} 條三元組)`);
} catch (e) {
spinner.fail(chalk.red(`解析失敗:${e instanceof Error ? e.message : e}`));
process.exit(1);
}
const executorUrl = getCypherExecutorUrl(config);
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'X-Arcrun-API-Key': config.api_key,
};
// 向 /cypher/search 取得執行圖
const searchSpinner = ora('取得執行圖').start();
let graph: unknown;
try {
// t158「部署≠發現」(leo:「這裡只是複製工作流的 data 過去,沒有要在這裡驗證」):
// push=複製路徑,帶 mode:compile 純編圖——寫錯的 workflow 照樣部署,錯在執行時現形。
const res = await fetch(`${executorUrl}/cypher/search`, {
method: 'POST',
headers,
body: JSON.stringify({ triplets: workflow.flow, mode: 'compile' }),
});
if (!res.ok) {
const err = await res.text();
searchSpinner.fail(chalk.red(`執行圖解析失敗(${res.status}):${err.slice(0, 200)}`));
process.exit(1);
}
const data = await res.json() as { cypher: { nodes: unknown[]; edges: unknown[] }; missing: string[] };
// t158push 不看 missingcompile 模式亦恆空)——存在性由執行時 component-loader 決定;
// 要「先問有沒有」用 acr validateMCP 查詢(discover 路徑)。
// 附上 id / name,並將 workflow.config 套入節點(componentId + data
const rawGraph = data.cypher as { nodes: Array<{ id: string; componentId?: string; data?: Record<string, unknown> }>; edges: unknown[] };
const cfg = (workflow.config ?? {}) as Record<string, Record<string, unknown>>;
const nodes = rawGraph.nodes.map(node => {
const nodeCfg = cfg[node.id];
if (!nodeCfg) return node;
const { component, ...params } = nodeCfg;
return {
...node,
componentId: typeof component === 'string' ? component : node.componentId,
data: Object.keys(params).length > 0 ? { ...(node.data ?? {}), ...params } : node.data,
};
});
graph = { id: workflow.name, name: workflow.name, nodes, edges: rawGraph.edges };
searchSpinner.succeed('執行圖解析完成');
} catch (e) {
searchSpinner.fail(chalk.red(`網路錯誤:${e instanceof Error ? e.message : e}`));
process.exit(1);
}
// 暴露 consent 閘已移除(leo 2026-06-29Arcrun#13):arcrun 是給 AI 用的系統,
// push/暴露不再需要人類確認,AI/MCP 隨時可部署。暴露風險由用戶自負(同 n8n 建 webhook)。
// POST 至 /webhooks/named
const deploySpinner = ora(`部署 "${workflow.name}" 至 ${executorUrl}`).start();
try {
const res = await fetch(`${executorUrl}/webhooks/named`, {
method: 'POST',
headers,
body: JSON.stringify({
name: workflow.name,
graph,
config: workflow.config ?? {},
description: workflow.description ?? '',
}),
});
if (!res.ok) {
const err = await res.text();
deploySpinner.fail(chalk.red(`部署失敗(${res.status}):${err.slice(0, 200)}`));
process.exit(1);
}
const data = await res.json() as { name: string; webhook_url: string; created_at: string };
deploySpinner.succeed(chalk.green(`✓ "${workflow.name}" 已部署`));
// self-hostednamespace 明碼 → 給「namespace 進 path」的公開 URL(公開表單可直接打,免 header)。
// standard:仍走 header(平台多租戶,api_key 是密碼不可進 path)。
if (config.mode === 'self-hosted') {
const pathUrl = `${executorUrl}/webhooks/named/${config.api_key}/${workflow.name}/trigger`;
console.log(chalk.bold(`\n Webhook URL(公開可打,免 header):${chalk.cyan(pathUrl)}`));
console.log(chalk.gray(' namespace 在 path 是明碼分區標籤(非密碼);要防外部濫用請對 webhook 加保護。'));
console.log('');
console.log(chalk.gray(' 公開表單 / curl 觸發:'));
console.log(` ${chalk.cyan(`curl -X POST ${pathUrl} \\`)}`);
console.log(` ${chalk.cyan(` -H 'Content-Type: application/json' -d '{"key": "value"}'`)}`);
} else {
console.log(chalk.bold(`\n Webhook URL${chalk.cyan(data.webhook_url)}`));
console.log(chalk.gray(` 需帶 HeaderX-Arcrun-API-Key: ${config.api_key.slice(0, 8)}...`));
console.log('');
console.log(chalk.gray(' curl 觸發範例:'));
console.log(` ${chalk.cyan(`curl -X POST ${data.webhook_url} \\`)}`);
console.log(` ${chalk.cyan(` -H 'X-Arcrun-API-Key: ${config.api_key}' \\`)}`);
console.log(` ${chalk.cyan(` -H 'Content-Type: application/json' -d '{"key": "value"}'`)}`);
}
console.log('');
console.log(chalk.gray(' 測試執行:') + ` ${chalk.cyan(`acr run ${workflow.name}`)}`);
console.log('');
} catch (e) {
deploySpinner.fail(chalk.red(`部署失敗:${e instanceof Error ? e.message : e}`));
process.exit(1);
}
}