7e631a890a
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>
151 lines
6.5 KiB
TypeScript
151 lines
6.5 KiB
TypeScript
/**
|
||
* 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 push(Webhook 部署需要 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[] };
|
||
// t158:push 不看 missing(compile 模式亦恆空)——存在性由執行時 component-loader 決定;
|
||
// 要「先問有沒有」用 acr validate/MCP 查詢(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-29,Arcrun#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-hosted:namespace 明碼 → 給「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(` 需帶 Header:X-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);
|
||
}
|
||
}
|