feat: 薄殼原則落地 + seed 下沉 API + MCP 進主庫 + 部署一致性
壓測四橫向問題修正(docs 壓測報告):
① 薄殼原則成鐵律:能力長在 API,CLI/MCP/lib 只暴露
- seed 下沉成 API 行為:cypher-executor POST /init/seed(一次灌 API+auth recipe),
種子資料移到 server src/lib/api-recipe-seeds.ts,CLI 改薄殼一次呼叫
- 解除 deployFullyOk 連坐 + init 補 seed auth recipe + update 補 seed/全 KV
- registry SUBMISSIONS_KV 補進 REQUIRED_KV_NAMESPACES(修 20/21)
② MCP 統一帳號來源(單一 remote MCP + .env 切 MCP URL)
- MCP 從 sibling repo 搬進 arcrun/mcp/(remote Worker,route 改 mcp.arcrun.dev)
- config 加 mcp_url 三層解析 + getMcpUrl + DEFAULT_MCP_URL
- 新增 acr mcp-setup:依 config 寫專案 .mcp.json(接案切資料夾自動切 MCP)
- acr --version 改動態讀 package.json(根治漂移)
③ Deploy 一致性
- tests/release.feature + scripts/check-release.sh
- local-deploy.sh:CLI npm publish + auto patch bump + CHANGELOG
- local-deploy.sh bash 3.2 相容修正(mapfile / 空陣列 set -u)
- builtins/pnpm-lock.yaml
④ README self-hosted 同步現況(移除 R2 殘留、加 flag/env、多帳號)
CLI bump → 1.3.0
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+36
-35
@@ -16,8 +16,8 @@ import {
|
||||
downloadAndDeploy,
|
||||
type DeployContext,
|
||||
} from '../lib/deploy.js';
|
||||
import { API_RECIPE_SEEDS } from '../lib/api-recipe-seeds.js';
|
||||
import { cmdInstallHarness } from './install-harness.js';
|
||||
import { cmdMcpSetup } from './mcp-setup.js';
|
||||
|
||||
const ARCRUN_REGISTER_URL = 'https://cypher.arcrun.dev/register';
|
||||
|
||||
@@ -57,6 +57,14 @@ export async function cmdInit(options: InitOptions): Promise<void> {
|
||||
} catch (e) {
|
||||
console.log(chalk.gray(` (harness 安裝略過:${e instanceof Error ? e.message : e};可稍後跑 acr install-harness)`));
|
||||
}
|
||||
|
||||
// 順便寫專案 .mcp.json,讓 Claude Code 連對的 MCP(依 config 的 mcp_url,SDD mcp-account-source.md)。
|
||||
// 失敗不影響 init(可事後 acr mcp-setup 補)。
|
||||
try {
|
||||
cmdMcpSetup();
|
||||
} catch (e) {
|
||||
console.log(chalk.gray(` (.mcp.json 略過:${e instanceof Error ? e.message : e};可稍後跑 acr mcp-setup)`));
|
||||
}
|
||||
}
|
||||
|
||||
async function initLocal(): Promise<void> {
|
||||
@@ -129,7 +137,7 @@ async function initStandard(rl: ReturnType<typeof createInterface>): Promise<voi
|
||||
|
||||
/**
|
||||
* Self-hosted installer:用戶只提供 CF Account ID + API Token,其餘自動。
|
||||
* 驗 token → 建 7 KV(冪等)→ 查 subdomain → 下載 release 部署 Worker
|
||||
* 驗 token → 建 KV(冪等,數量見 REQUIRED_KV_NAMESPACES)→ 查 subdomain → 下載 release 部署 Worker
|
||||
* → seed auth+api recipe → 寫 config → 印手動 secret 提示。
|
||||
* SDD:.agents/specs/arcrun/sdk-and-website/self-hosted-init.md
|
||||
*/
|
||||
@@ -208,6 +216,8 @@ async function initSelfHosted(
|
||||
const deploy = await downloadAndDeploy(deployCtx);
|
||||
const cypherUrl = deploy.cypherExecutorUrl
|
||||
?? (workerSubdomain ? `https://arcrun-cypher-executor.${workerSubdomain}.workers.dev` : '');
|
||||
// 誠實回報部署結果;但**不**用「全部成功」字串 gate 後續 seed(壓測 §4.1:
|
||||
// registry 一個無關 worker 失敗就連坐讓 seed 永遠被跳過)。seed 只看 cypher-executor 是否可達。
|
||||
const deployFullyOk = /全部成功/.test(deploy.message);
|
||||
console.log(deployFullyOk ? chalk.green(` ✓ ${deploy.message}`) : chalk.yellow(` ⚠ ${deploy.message}`));
|
||||
|
||||
@@ -224,15 +234,14 @@ async function initSelfHosted(
|
||||
saveConfig(config);
|
||||
createCredentialsYamlIfMissing();
|
||||
|
||||
// 6. seed API recipe(部署成功 + 有 cypher URL 才打;否則提示稍後 acr update 後再 seed)
|
||||
if (deployFullyOk && cypherUrl) {
|
||||
await seedApiRecipes(cypherUrl);
|
||||
} else if (cypherUrl) {
|
||||
console.log(chalk.gray(` → recipe seed 待部署穩定後再執行(${API_RECIPE_SEEDS.length} 個;acr update 會重試)`));
|
||||
// 6. seed recipe(薄殼:呼叫 API 的 /init/seed 一次,由 API 灌 API recipe + auth recipe)。
|
||||
// 只要 cypher-executor 可達就 seed——不被無關 worker(registry)的失敗連坐(壓測 §4.1)。
|
||||
if (cypherUrl) {
|
||||
await callSeedEndpoint(cypherUrl);
|
||||
}
|
||||
|
||||
// 結果回報(誠實:部分失敗時明說,不假綠 — mindset §7)
|
||||
console.log(chalk.green('\n ✓ Cloudflare 資源就緒(7 KV,免費額度即可,無需綁卡)'));
|
||||
console.log(chalk.green(`\n ✓ Cloudflare 資源就緒(${REQUIRED_KV_NAMESPACES.length} KV,免費額度即可,無需綁卡)`));
|
||||
console.log(chalk.green(' ✓ 設定寫入 ~/.arcrun/config.yaml'));
|
||||
console.log(chalk.green(' ✓ 建立 credentials.yaml'));
|
||||
|
||||
@@ -245,35 +254,27 @@ async function initSelfHosted(
|
||||
console.log(chalk.gray(' 生成:node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"\n'));
|
||||
}
|
||||
|
||||
/** seed API recipe 到目標 cypher-executor(部署完成後)。*/
|
||||
async function seedApiRecipes(cypherUrl: string): Promise<void> {
|
||||
process.stdout.write(chalk.gray(` → seed ${API_RECIPE_SEEDS.length} 個 API recipe...`));
|
||||
let ok = 0;
|
||||
for (const r of API_RECIPE_SEEDS) {
|
||||
try {
|
||||
const res = await fetch(`${cypherUrl}/recipes`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
canonical_id: r.canonical_id,
|
||||
display_name: r.display_name,
|
||||
description: r.description,
|
||||
endpoint: r.endpoint,
|
||||
method: r.method,
|
||||
auth_service: r.auth_service,
|
||||
exposure_consent: {
|
||||
confirmed_by_human: true,
|
||||
understood: `platform seed recipe: ${r.canonical_id} → ${r.endpoint}`,
|
||||
confirmed_at: new Date().toISOString(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (res.ok) ok++;
|
||||
} catch {
|
||||
// 單筆失敗不中斷整個 init;最終回報數量
|
||||
/**
|
||||
* 薄殼:呼叫 API 的 /init/seed 一次(rule 07)。
|
||||
* seed 的編排 + 種子資料全在 cypher-executor(routes/init-seed.ts),CLI 不自己迴圈 POST。
|
||||
* 之前 seedApiRecipes 在 CLI 迴圈 POST + deployFullyOk gate 是壓測 §4.1 的反例,已移除。
|
||||
*/
|
||||
async function callSeedEndpoint(cypherUrl: string): Promise<void> {
|
||||
process.stdout.write(chalk.gray(' → seed recipe(API recipe + auth recipe,由 API 灌入)...'));
|
||||
try {
|
||||
const res = await fetch(`${cypherUrl}/init/seed`, { method: 'POST' });
|
||||
const body = await res.json().catch(() => null) as
|
||||
| { success?: boolean; message?: string }
|
||||
| null;
|
||||
if (res.ok && body?.success) {
|
||||
console.log(chalk.green(` ✓ ${body.message ?? ''}`));
|
||||
} else {
|
||||
// 誠實:不假綠。seed 沒全成就明說,提示 acr update 可重跑(冪等)。
|
||||
console.log(chalk.yellow(` ⚠ ${body?.message ?? `HTTP ${res.status}`}(可 acr update 重跑,seed 冪等)`));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(chalk.yellow(` ⚠ seed 端點呼叫失敗(${e instanceof Error ? e.message : e});cypher 穩定後 acr update 重跑`));
|
||||
}
|
||||
console.log(ok === API_RECIPE_SEEDS.length ? chalk.green(' ✓') : chalk.yellow(` ${ok}/${API_RECIPE_SEEDS.length}`));
|
||||
}
|
||||
|
||||
function createHelloYamlIfMissing(): void {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* acr mcp-setup — 在目前資料夾寫 / 更新 .mcp.json,讓 Claude Code 連對的 arcrun MCP。
|
||||
*
|
||||
* 薄殼原則(rule 07 + SDD mcp-account-source.md):
|
||||
* MCP URL 與 cypher URL 一樣,由同一份三層 config 解析(env > 專案 .arcrun.yaml > 全域)決定。
|
||||
* 「切換帳號」= 在客戶資料夾跑 acr mcp-setup(讀該資料夾的 .arcrun.yaml)→ 產指向客戶 MCP 的 .mcp.json。
|
||||
* 進哪個專案資料夾 → Claude Code 讀該專案 .mcp.json → 連對應 MCP(綁該帳號的 cypher)。
|
||||
*
|
||||
* 不重實作身份解析:呼叫 config.ts 的 getMcpUrl()(唯一解析來源)。
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { loadConfig, getMcpUrl, DEFAULT_MCP_URL } from '../lib/config.js';
|
||||
|
||||
/** Claude Code .mcp.json 的 server 條目(remote HTTP MCP)。 */
|
||||
interface McpServerEntry {
|
||||
type: 'http';
|
||||
url: string;
|
||||
}
|
||||
interface McpJson {
|
||||
mcpServers?: Record<string, McpServerEntry | Record<string, unknown>>;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
const SERVER_KEY = 'arcrun';
|
||||
|
||||
export function cmdMcpSetup(): void {
|
||||
const config = loadConfig();
|
||||
const mcpUrl = getMcpUrl(config);
|
||||
|
||||
const target = join(process.cwd(), '.mcp.json');
|
||||
|
||||
// 讀既有 .mcp.json(保留其他 MCP server 條目,只覆寫 arcrun 這條)
|
||||
let doc: McpJson = {};
|
||||
if (existsSync(target)) {
|
||||
try {
|
||||
doc = JSON.parse(readFileSync(target, 'utf8')) as McpJson;
|
||||
} catch {
|
||||
console.log(chalk.yellow(` ⚠ 既有 .mcp.json 解析失敗,將以新內容覆寫整檔。`));
|
||||
doc = {};
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc.mcpServers || typeof doc.mcpServers !== 'object') doc.mcpServers = {};
|
||||
doc.mcpServers[SERVER_KEY] = { type: 'http', url: mcpUrl };
|
||||
|
||||
writeFileSync(target, JSON.stringify(doc, null, 2) + '\n', 'utf8');
|
||||
|
||||
console.log(chalk.green(`\n ✓ 已寫入 ${target}`));
|
||||
console.log(chalk.gray(` arcrun MCP → ${mcpUrl}`));
|
||||
if (mcpUrl === DEFAULT_MCP_URL && (!config.mcp_url || config.mcp_url.trim() === '')) {
|
||||
console.log(chalk.gray(` (用平台預設;要連自己/客戶的 MCP,在 config 設 mcp_url 或 ARCRUN_MCP_URL env,再重跑)`));
|
||||
}
|
||||
console.log(chalk.gray(` Claude Code 進此資料夾會自動連這台 MCP。切帳號 = 在對應資料夾重跑 acr mcp-setup。\n`));
|
||||
}
|
||||
@@ -12,9 +12,11 @@
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { loadConfig } from '../lib/config.js';
|
||||
import { CfAccountClient } from '../lib/cf-api.js';
|
||||
import {
|
||||
wranglerAvailable,
|
||||
downloadAndDeploy,
|
||||
REQUIRED_KV_NAMESPACES,
|
||||
type DeployContext,
|
||||
} from '../lib/deploy.js';
|
||||
|
||||
@@ -40,20 +42,50 @@ export async function cmdUpdate(): Promise<void> {
|
||||
|
||||
console.log(chalk.bold('\n acr update — 拉新 release 並重新部署\n'));
|
||||
|
||||
// 重新解析「全部」KV namespace id(冪等:已存在則重用),不只 config 存的兩個。
|
||||
// 壓測 §4.1.3:舊版 update 只注入 WEBHOOKS+CREDENTIALS_KV,其餘 6 個注入成空字串 →
|
||||
// 重部署反而可能弄壞需要 RECIPES/EXEC_CONTEXT/... 的 worker。改為與 init 同樣全建妥。
|
||||
const cf = new CfAccountClient(config.cloudflare_account_id, config.cf_api_token);
|
||||
const kvNamespaceIds: Record<string, string> = {};
|
||||
try {
|
||||
const existing = await cf.listKvNamespaces();
|
||||
for (const title of REQUIRED_KV_NAMESPACES) {
|
||||
kvNamespaceIds[title] = await cf.ensureKvNamespace(title, existing);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(chalk.yellow(`\n ✗ 解析 KV namespace 失敗:${e instanceof Error ? e.message : e}\n`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const ctx: DeployContext = {
|
||||
accountId: config.cloudflare_account_id,
|
||||
apiToken: config.cf_api_token,
|
||||
workerSubdomain: extractSubdomain(config.cypher_executor_url),
|
||||
kvNamespaceIds: {
|
||||
WEBHOOKS: config.webhooks_kv_namespace_id ?? '',
|
||||
CREDENTIALS_KV: config.credentials_kv_namespace_id ?? '',
|
||||
},
|
||||
kvNamespaceIds,
|
||||
};
|
||||
|
||||
const result = await downloadAndDeploy(ctx);
|
||||
|
||||
if (result.implemented) {
|
||||
console.log(chalk.green('\n ✓ 更新完成\n'));
|
||||
console.log(chalk.green('\n ✓ 部署完成'));
|
||||
// 重跑 seed(薄殼:呼叫 API /init/seed;冪等,覆寫既有)。
|
||||
// 修壓測 §4.1.3「update 不做 seed,但 init 提示說 update 會重試 seed」的矛盾。
|
||||
const cypherUrl = config.cypher_executor_url
|
||||
?? result.cypherExecutorUrl
|
||||
?? (ctx.workerSubdomain ? `https://arcrun-cypher-executor.${ctx.workerSubdomain}.workers.dev` : '');
|
||||
if (cypherUrl) {
|
||||
process.stdout.write(chalk.gray(' → 重新 seed recipe(API + auth,由 API 灌入)...'));
|
||||
try {
|
||||
const res = await fetch(`${cypherUrl}/init/seed`, { method: 'POST' });
|
||||
const body = await res.json().catch(() => null) as { success?: boolean; message?: string } | null;
|
||||
console.log(res.ok && body?.success
|
||||
? chalk.green(` ✓ ${body.message ?? ''}`)
|
||||
: chalk.yellow(` ⚠ ${body?.message ?? `HTTP ${res.status}`}`));
|
||||
} catch (e) {
|
||||
console.log(chalk.yellow(` ⚠ seed 失敗(${e instanceof Error ? e.message : e})`));
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
} else {
|
||||
console.log(chalk.yellow(' ⚠ 更新尚未自動化:'));
|
||||
console.log(chalk.gray(' ' + result.message.split('\n').join('\n ')) + '\n');
|
||||
|
||||
+22
-1
@@ -7,6 +7,9 @@
|
||||
* 使用:acr <指令>
|
||||
*/
|
||||
import { Command } from 'commander';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { cmdInit } from './commands/init.js';
|
||||
import { cmdConfig } from './commands/config.js';
|
||||
import { cmdCredsPush } from './commands/creds.js';
|
||||
@@ -19,14 +22,26 @@ import { cmdList } from './commands/list.js';
|
||||
import { cmdLogs } from './commands/logs.js';
|
||||
import { cmdUpdate } from './commands/update.js';
|
||||
import { cmdInstallHarness } from './commands/install-harness.js';
|
||||
import { cmdMcpSetup } from './commands/mcp-setup.js';
|
||||
import { cmdAuthRecipeList, cmdAuthRecipeInfo, cmdAuthRecipeScaffold } from './commands/auth-recipe.js';
|
||||
|
||||
const program = new Command();
|
||||
|
||||
// 版本從 package.json 動態讀(dist/index.js 旁的 ../package.json),不 hardcode → 永不漂移。
|
||||
// 之前 hardcode '1.1.0' 與 package.json '1.2.0' 不一致,正是「忘了改」的反例。
|
||||
function readVersion(): string {
|
||||
try {
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
return (JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8')) as { version?: string }).version ?? '0.0.0';
|
||||
} catch {
|
||||
return '0.0.0';
|
||||
}
|
||||
}
|
||||
|
||||
program
|
||||
.name('acr')
|
||||
.description('arcrun — AI Workflow CLI for Cloudflare Workers + WASM')
|
||||
.version('1.1.0');
|
||||
.version(readVersion());
|
||||
|
||||
// acr init [--self-hosted] [--account-id <id>] [--api-token <token>]
|
||||
program
|
||||
@@ -146,4 +161,10 @@ program
|
||||
.description('把 arcrun 的 CC harness(mindset/提醒/防做歪 hook/指令)裝進當前專案')
|
||||
.action(() => cmdInstallHarness());
|
||||
|
||||
// acr mcp-setup(依 config 解析的 mcp_url 寫專案 .mcp.json,讓 Claude Code 連對的 MCP)
|
||||
program
|
||||
.command('mcp-setup')
|
||||
.description('在目前資料夾寫 .mcp.json 連對的 arcrun MCP(依 config 的 mcp_url;接案切資料夾自動切)')
|
||||
.action(() => cmdMcpSetup());
|
||||
|
||||
program.parse(process.argv);
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
/**
|
||||
* api-recipe-seeds.ts
|
||||
*
|
||||
* 現役 API recipe 的種子定義。self-host 新帳號 init 時把這些灌進空的 RECIPES KV
|
||||
* (透過 cypher-executor 的 POST /recipes,或 CF KV REST API 直接寫)。
|
||||
*
|
||||
* API recipe = http_request + 固定設定(endpoint/method 模板)。
|
||||
* 不需 deploy Worker,cypher-executor 執行時直接 fetch(見 cypher-executor/src/routes/recipes.ts)。
|
||||
*
|
||||
* 放在 CLI 端而非 cypher-executor/src:
|
||||
* - seed 資料是「installer 要灌進用戶 KV 的種子」,本就屬 CLI 職責(SDD self-hosted-init.md §4)。
|
||||
* - rule 02 §2.2 hook 擋 cypher-executor TS hard-code API endpoint;seed 的 endpoint 是資料欄位,
|
||||
* 放 CLI 端避開誤判,也更符合職責切分。
|
||||
*
|
||||
* 來源:2026-06-01 從 prod cypher.arcrun.dev/recipes 逐一查得的現役定義。
|
||||
* 對應 SDD:.agents/specs/arcrun/sdk-and-website/self-hosted-init.md §5
|
||||
*
|
||||
* KBDB recipe(kbdb_*)採 Supabase 模式(richblack 2026-06-02):
|
||||
* 進 seed = 展示能力(引子)。使用者要用 → 去 arcrun 取統一 API Key 當 credential。
|
||||
* FOLLOW-UP(KBDB 端):endpoint 現為 kbdb.finally.click,KBDB 應改用統一對外網址;
|
||||
* KBDB 改網址後同步更新此處。seed 先照現況進。
|
||||
*/
|
||||
|
||||
export interface ApiRecipeSeed {
|
||||
canonical_id: string;
|
||||
display_name: string;
|
||||
description?: string;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
auth_service?: string;
|
||||
}
|
||||
|
||||
export const API_RECIPE_SEEDS: ApiRecipeSeed[] = [
|
||||
// ── KBDB(Supabase 模式,auth_service=kbdb static_key)──
|
||||
{
|
||||
canonical_id: 'kbdb_get',
|
||||
display_name: 'KBDB Get',
|
||||
description: 'GET 讀取 block / 查詢。_path 帶查詢路徑。auth: kbdb static_key。',
|
||||
endpoint: 'https://kbdb.finally.click{{_path}}',
|
||||
method: 'GET',
|
||||
auth_service: 'kbdb',
|
||||
},
|
||||
{
|
||||
canonical_id: 'kbdb_create_block',
|
||||
display_name: 'KBDB Create Block',
|
||||
description: 'POST /blocks 建立 block。body 帶 block 欄位(content/type/page_name/source/user_id 等)。auth: kbdb static_key。',
|
||||
endpoint: 'https://kbdb.finally.click/blocks',
|
||||
method: 'POST',
|
||||
auth_service: 'kbdb',
|
||||
},
|
||||
{
|
||||
canonical_id: 'kbdb_patch_block',
|
||||
display_name: 'KBDB Patch Block',
|
||||
description: 'PATCH /blocks/:id 局部更新。_path 帶 /blocks/{id},body 帶要改的欄位。auth: kbdb static_key。',
|
||||
endpoint: 'https://kbdb.finally.click{{_path}}',
|
||||
method: 'PATCH',
|
||||
auth_service: 'kbdb',
|
||||
},
|
||||
{
|
||||
canonical_id: 'kbdb_delete',
|
||||
display_name: 'KBDB Delete',
|
||||
description: 'DELETE /blocks/:id 刪除 block。_path 帶 /blocks/{id}。auth: kbdb static_key。',
|
||||
endpoint: 'https://kbdb.finally.click{{_path}}',
|
||||
method: 'DELETE',
|
||||
auth_service: 'kbdb',
|
||||
},
|
||||
{
|
||||
canonical_id: 'kbdb_ingest',
|
||||
display_name: 'KBDB Ingest',
|
||||
description: 'POST /blocks/ingest 批次寫入。body 帶 input。auth: kbdb static_key。',
|
||||
endpoint: 'https://kbdb.finally.click/blocks/ingest',
|
||||
method: 'POST',
|
||||
auth_service: 'kbdb',
|
||||
},
|
||||
|
||||
// ── Google(service_account)──
|
||||
{
|
||||
canonical_id: 'gmail_send',
|
||||
display_name: 'Gmail Send',
|
||||
description: '寄 Gmail。POST messages/send,body 帶 raw(base64url MIME)。auth: google service_account。',
|
||||
endpoint: 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send',
|
||||
method: 'POST',
|
||||
auth_service: 'google_gmail_sa',
|
||||
},
|
||||
{
|
||||
canonical_id: 'google_sheets_append',
|
||||
display_name: 'Google Sheets Append',
|
||||
description: '寫 Sheets。PUT values?valueInputOption=RAW,body 帶 values。auth: google service_account。',
|
||||
endpoint: 'https://sheets.googleapis.com{{_path}}',
|
||||
method: 'PUT',
|
||||
auth_service: 'google_sheets_sa',
|
||||
},
|
||||
{
|
||||
canonical_id: 'google_sheets_read',
|
||||
display_name: 'Google Sheets Read',
|
||||
description: '讀 Sheets。GET values。_path 帶完整路徑。auth: google service_account。',
|
||||
endpoint: 'https://sheets.googleapis.com{{_path}}',
|
||||
method: 'GET',
|
||||
auth_service: 'google_sheets_sa',
|
||||
},
|
||||
|
||||
// ── 訊息(static_key)──
|
||||
{
|
||||
canonical_id: 'telegram_send',
|
||||
display_name: 'Telegram Send',
|
||||
description: 'Telegram sendMessage。token 在 URL path({{auth.bot_token}}),body 帶 chat_id+text。auth: static_key path 注入。',
|
||||
endpoint: 'https://api.telegram.org/bot{{auth.bot_token}}/sendMessage',
|
||||
method: 'POST',
|
||||
auth_service: 'telegram',
|
||||
},
|
||||
{
|
||||
canonical_id: 'line_notify_send',
|
||||
display_name: 'LINE Notify',
|
||||
description: 'LINE Notify 推訊息。POST notify,body 帶 message(form-urlencoded)。auth: static_key Bearer line token。',
|
||||
endpoint: 'https://notify-api.line.me/api/notify',
|
||||
method: 'POST',
|
||||
auth_service: 'line_notify',
|
||||
},
|
||||
];
|
||||
+23
-1
@@ -21,6 +21,12 @@ export interface ArcrunConfig {
|
||||
credentials_kv_namespace_id?: string;
|
||||
webhooks_kv_namespace_id?: string;
|
||||
// 共用
|
||||
// MCP server URL(薄殼原則:CLI 與 MCP 同一份身份解析)。
|
||||
// self-hosted / 接案:指向自己 / 客戶的 remote MCP Worker(綁該帳號的 cypher)。
|
||||
// 未設 → fallback 平台預設(SaaS 用戶)。acr mcp-setup 依此寫專案 .mcp.json,
|
||||
// 讓「進哪個專案資料夾 → Claude Code 連那台 MCP」自動生效。
|
||||
// SDD: sdk-and-website/mcp-account-source.md
|
||||
mcp_url?: string;
|
||||
multi_tenant?: boolean;
|
||||
// 資料外流警示:本機記住「已同意暴露 / 選擇不再警示」的資源,避免每次 push 重問(§3 首次問記住)。
|
||||
// key 格式:`{kind}:{resourceName}`(如 "webhook:contacts_lookup" / "recipe:kbdb_get")。
|
||||
@@ -43,10 +49,17 @@ const ENV_MAP: Record<string, keyof ArcrunConfig> = {
|
||||
ARCRUN_API_KEY: 'api_key',
|
||||
ARCRUN_ENCRYPTION_KEY: 'encryption_key',
|
||||
ARCRUN_CYPHER_EXECUTOR_URL: 'cypher_executor_url',
|
||||
ARCRUN_MCP_URL: 'mcp_url',
|
||||
CLOUDFLARE_ACCOUNT_ID: 'cloudflare_account_id',
|
||||
CLOUDFLARE_API_TOKEN: 'cf_api_token',
|
||||
};
|
||||
|
||||
/**
|
||||
* 平台預設 MCP URL(mcp_url 未設時的 fallback,SaaS 用戶用)。
|
||||
* MCP 搬進 arcrun 主庫後改用 arcrun.dev zone(mcp/wrangler.toml route = mcp.arcrun.dev)。
|
||||
*/
|
||||
export const DEFAULT_MCP_URL = 'https://mcp.arcrun.dev';
|
||||
|
||||
export function configExists(): boolean {
|
||||
return existsSync(CONFIG_PATH) || findProjectConfig() !== undefined;
|
||||
}
|
||||
@@ -114,7 +127,7 @@ export function resolveConfigSources(): Array<{ field: keyof ArcrunConfig; value
|
||||
const env = readEnvOverrides();
|
||||
const fields: (keyof ArcrunConfig)[] = [
|
||||
'mode', 'api_key', 'encryption_key', 'cloudflare_account_id',
|
||||
'cf_api_token', 'cypher_executor_url',
|
||||
'cf_api_token', 'cypher_executor_url', 'mcp_url',
|
||||
];
|
||||
const rows: Array<{ field: keyof ArcrunConfig; value: string; source: ConfigSource }> = [];
|
||||
for (const f of fields) {
|
||||
@@ -146,3 +159,12 @@ export function getCypherExecutorUrl(config: ArcrunConfig): string {
|
||||
}
|
||||
return 'https://cypher.arcrun.dev';
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得 MCP server URL(薄殼原則:與 cypher_url 同一份 config 解析)。
|
||||
* config 有 mcp_url(env/專案/全域 任一層)→ 用它;否則 fallback 平台預設。
|
||||
* acr mcp-setup 用此決定要寫進專案 .mcp.json 的 URL → 切資料夾自動切 MCP。
|
||||
*/
|
||||
export function getMcpUrl(config: ArcrunConfig): string {
|
||||
return config.mcp_url && config.mcp_url.trim() !== '' ? config.mcp_url : DEFAULT_MCP_URL;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,13 @@ import { join } from 'node:path';
|
||||
* 注意:repo 名大小寫敏感(codeload 路徑需完全一致)。*/
|
||||
const ARCRUN_REPO = process.env.ARCRUN_REPO ?? 'uncle6me-web/Arcrun';
|
||||
|
||||
/** init 要建立的 7 個 KV namespace(title)。權威來源:.claude/rules/01-tech-stack.md 資料儲存表。*/
|
||||
/**
|
||||
* init 要建立的 KV namespace(title)。
|
||||
* 前 7 個權威來源:.claude/rules/01-tech-stack.md 資料儲存表(cypher-executor 用)。
|
||||
* SUBMISSIONS_KV:registry worker 用(component 投稿)。漏建會讓 registry deploy 失敗 →
|
||||
* 壓測 §2.6/#11「20/21」根因(registry/wrangler.toml 綁 SUBMISSIONS_KV,但注入清單沒有它,
|
||||
* 殘留官方舊 id → wrangler deploy 因 KV 不存在而失敗)。補進來後回到 21/21。
|
||||
*/
|
||||
export const REQUIRED_KV_NAMESPACES = [
|
||||
'WEBHOOKS',
|
||||
'CREDENTIALS_KV',
|
||||
@@ -26,6 +32,7 @@ export const REQUIRED_KV_NAMESPACES = [
|
||||
'SESSIONS_KV',
|
||||
'ANALYTICS_KV',
|
||||
'EXEC_CONTEXT',
|
||||
'SUBMISSIONS_KV',
|
||||
] as const;
|
||||
|
||||
/** 部署後要提示用戶手動 `wrangler secret put ENCRYPTION_KEY` 的 Worker。*/
|
||||
|
||||
Reference in New Issue
Block a user