@harrylabsj/kiwi 0.7.15 → 0.7.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +310 -12
- package/dist/cli.js.map +1 -1
- package/dist/config/profile.d.ts +16 -0
- package/dist/config/profile.js +31 -0
- package/dist/config/profile.js.map +1 -1
- package/dist/merchant/decision-backend.js +8 -11
- package/dist/merchant/decision-backend.js.map +1 -1
- package/dist/product-cli.d.ts +1 -1
- package/dist/product-cli.js +15 -3
- package/dist/product-cli.js.map +1 -1
- package/dist/product-init.d.ts +19 -0
- package/dist/product-init.js +50 -2
- package/dist/product-init.js.map +1 -1
- package/dist/product-setup-public.d.ts +51 -0
- package/dist/product-setup-public.js +165 -0
- package/dist/product-setup-public.js.map +1 -0
- package/examples/profiles/merchant.deepseek.yaml +2 -5
- package/examples/profiles/merchant.fake.yaml +2 -4
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -51,6 +51,10 @@ interface ParsedArgs {
|
|
|
51
51
|
relogin: boolean;
|
|
52
52
|
qrScale: number;
|
|
53
53
|
noQr: boolean;
|
|
54
|
+
domain?: string;
|
|
55
|
+
check: boolean;
|
|
56
|
+
caddyfile?: string;
|
|
57
|
+
file?: string;
|
|
54
58
|
}
|
|
55
59
|
/** 导出供测试(审查 P2-L:--a2a flag 解析回归锁定)。 */
|
|
56
60
|
export declare function parseArgs(argv: string[]): ParsedArgs;
|
package/dist/cli.js
CHANGED
|
@@ -27,7 +27,9 @@
|
|
|
27
27
|
* kiwi init|up|status|logs|down --dir <instance-dir>
|
|
28
28
|
* Managed-local product lifecycle.
|
|
29
29
|
*/
|
|
30
|
-
import {
|
|
30
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
31
|
+
import { existsSync, mkdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
32
|
+
import { homedir, userInfo } from "node:os";
|
|
31
33
|
import path from "node:path";
|
|
32
34
|
import { fileURLToPath } from "node:url";
|
|
33
35
|
import { startA2aNode } from "./a2a/node.js";
|
|
@@ -55,11 +57,12 @@ import { parseLogLines, runLogs } from "./supervisor/logs.js";
|
|
|
55
57
|
import { StackConfigError } from "./supervisor/stack-config.js";
|
|
56
58
|
import { cmdProductDoctor, productHelp, PRODUCT_VERSION, DEFAULT_CATALOG_URL, DEFAULT_PROFILE_PATH, } from "./product-cli.js";
|
|
57
59
|
import { cmdWeixin, weixinUsage } from "./weixin/cli-weixin.js";
|
|
58
|
-
import { merchantInit, slugifyMerchantId } from "./product-init.js";
|
|
60
|
+
import { DEFAULT_SHOPPING_DB_PATH, loadMerchantCredentials, merchantInit, slugifyMerchantId } from "./product-init.js";
|
|
59
61
|
import readline from "node:readline";
|
|
60
62
|
import { buyerInit, buyerSearch, buyerTasks } from "./product-buyer.js";
|
|
61
63
|
import { merchantPublish } from "./product-publish.js";
|
|
62
64
|
import { catalogServe } from "./product-catalog.js";
|
|
65
|
+
import { extractPublicDomain, runMerchantSetupPublic, SetupPublicError, validatePublicDomain } from "./product-setup-public.js";
|
|
63
66
|
const USAGE = `kiwi ${PRODUCT_VERSION} — commerce negotiation agent runtime
|
|
64
67
|
|
|
65
68
|
Usage:
|
|
@@ -93,6 +96,8 @@ Usage:
|
|
|
93
96
|
Product layer (product-strategy rev1.1 §10/§19):
|
|
94
97
|
kiwi buyer --help Kiwi Buyer command tree
|
|
95
98
|
kiwi merchant --help Kiwi Merchant command tree
|
|
99
|
+
kiwi setup-hermes One-click: wire kiwi-buyer-mcp into Hermes
|
|
100
|
+
(hermes mcp add + skills install)
|
|
96
101
|
kiwi network --help Kiwi Network command tree
|
|
97
102
|
kiwi demo [a|b] One-command local multi-merchant demo
|
|
98
103
|
(Issue 13; 1 buyer + catalog + 3 merchants
|
|
@@ -155,6 +160,10 @@ export function parseArgs(argv) {
|
|
|
155
160
|
let relogin = false;
|
|
156
161
|
let qrScale = 1;
|
|
157
162
|
let noQr = false;
|
|
163
|
+
let domain;
|
|
164
|
+
let check = false;
|
|
165
|
+
let caddyfile;
|
|
166
|
+
let file;
|
|
158
167
|
for (let i = 0; i < argv.length; i++) {
|
|
159
168
|
const arg = argv[i];
|
|
160
169
|
if (arg === "--profile") {
|
|
@@ -268,6 +277,18 @@ export function parseArgs(argv) {
|
|
|
268
277
|
else if (arg === "--no-qr") {
|
|
269
278
|
noQr = true;
|
|
270
279
|
}
|
|
280
|
+
else if (arg === "--domain") {
|
|
281
|
+
domain = argv[++i];
|
|
282
|
+
}
|
|
283
|
+
else if (arg === "--check") {
|
|
284
|
+
check = true;
|
|
285
|
+
}
|
|
286
|
+
else if (arg === "--caddyfile") {
|
|
287
|
+
caddyfile = argv[++i];
|
|
288
|
+
}
|
|
289
|
+
else if (arg === "--file") {
|
|
290
|
+
file = argv[++i];
|
|
291
|
+
}
|
|
271
292
|
else if (arg !== undefined && arg.startsWith("--profile=")) {
|
|
272
293
|
profile = arg.slice("--profile=".length);
|
|
273
294
|
}
|
|
@@ -289,6 +310,15 @@ export function parseArgs(argv) {
|
|
|
289
310
|
else if (arg !== undefined && arg.startsWith("--data-dir=")) {
|
|
290
311
|
dataDir = arg.slice("--data-dir=".length);
|
|
291
312
|
}
|
|
313
|
+
else if (arg !== undefined && arg.startsWith("--domain=")) {
|
|
314
|
+
domain = arg.slice("--domain=".length);
|
|
315
|
+
}
|
|
316
|
+
else if (arg !== undefined && arg.startsWith("--caddyfile=")) {
|
|
317
|
+
caddyfile = arg.slice("--caddyfile=".length);
|
|
318
|
+
}
|
|
319
|
+
else if (arg !== undefined && arg.startsWith("--file=")) {
|
|
320
|
+
file = arg.slice("--file=".length);
|
|
321
|
+
}
|
|
292
322
|
else if (arg !== undefined && !arg.startsWith("-")) {
|
|
293
323
|
command.push(arg);
|
|
294
324
|
}
|
|
@@ -311,6 +341,7 @@ export function parseArgs(argv) {
|
|
|
311
341
|
relogin,
|
|
312
342
|
qrScale,
|
|
313
343
|
noQr,
|
|
344
|
+
check,
|
|
314
345
|
};
|
|
315
346
|
if (profile !== undefined)
|
|
316
347
|
out.profile = profile;
|
|
@@ -354,6 +385,12 @@ export function parseArgs(argv) {
|
|
|
354
385
|
out.port = port;
|
|
355
386
|
if (catalogDb !== undefined)
|
|
356
387
|
out.catalogDb = catalogDb;
|
|
388
|
+
if (domain !== undefined)
|
|
389
|
+
out.domain = domain;
|
|
390
|
+
if (caddyfile !== undefined)
|
|
391
|
+
out.caddyfile = caddyfile;
|
|
392
|
+
if (file !== undefined)
|
|
393
|
+
out.file = file;
|
|
357
394
|
if (catalogHost !== undefined)
|
|
358
395
|
out.catalogHost = catalogHost;
|
|
359
396
|
return out;
|
|
@@ -370,6 +407,20 @@ function requireProfile(args) {
|
|
|
370
407
|
}
|
|
371
408
|
return loadProfile(args.profile);
|
|
372
409
|
}
|
|
410
|
+
/**
|
|
411
|
+
* 读取 profile:`--profile` 优先;缺省回退 `KIWI_DEFAULT_PROFILE` env 或
|
|
412
|
+
* `DEFAULT_PROFILE_PATH`(`kiwi merchant init` 写入的默认路径)。文件不存在 →
|
|
413
|
+
* fail-closed 提示先 init。供商家命令(start/publish/setup-public)参数化使用,
|
|
414
|
+
* 让第 4-5 步不带 `--profile`。
|
|
415
|
+
*/
|
|
416
|
+
function requireProfileOrDefault(args) {
|
|
417
|
+
if (args.profile)
|
|
418
|
+
return loadProfile(args.profile);
|
|
419
|
+
const fallback = process.env.KIWI_DEFAULT_PROFILE ?? DEFAULT_PROFILE_PATH;
|
|
420
|
+
if (existsSync(fallback))
|
|
421
|
+
return loadProfile(fallback);
|
|
422
|
+
throw new ProfileError(`未找到商家配置(${fallback})——先运行 kiwi merchant init,或用 --profile <file> 指定`);
|
|
423
|
+
}
|
|
373
424
|
function printReportJsonl(report) {
|
|
374
425
|
process.stdout.write(`${JSON.stringify(report)}\n`);
|
|
375
426
|
}
|
|
@@ -555,11 +606,12 @@ export function resolveServeDataDir(dataDir, agentId) {
|
|
|
555
606
|
return dataDir ?? path.resolve(".kiwi", "agents", agentId);
|
|
556
607
|
}
|
|
557
608
|
async function cmdAgentServe(args) {
|
|
558
|
-
const profile =
|
|
609
|
+
const profile = requireProfileOrDefault(args);
|
|
559
610
|
if (profile.role !== "merchant") {
|
|
560
611
|
process.stderr.write("kiwi agent serve 需要 merchant profile(role: merchant)\n");
|
|
561
612
|
return EXIT.CONFIG;
|
|
562
613
|
}
|
|
614
|
+
loadMerchantCredentials(); // 加载 init 写入的 credentials.env(KIWI_MERCHANT_TOKEN)
|
|
563
615
|
const catalog = args.catalog ?? process.env.KIWI_CATALOG_URL ?? DEFAULT_CATALOG_URL;
|
|
564
616
|
const merchantToken = process.env.KIWI_MERCHANT_TOKEN || "";
|
|
565
617
|
// 审查 P1-09:serve 的初始 A2A 节点必须用稳定 dataDir——此前初始节点缺省走
|
|
@@ -567,11 +619,13 @@ async function cmdAgentServe(args) {
|
|
|
567
619
|
// negotiation 可重开、已发 conditional offer 返回 offer_unknown。稳定目录
|
|
568
620
|
// 让重启可恢复状态(resolveServeDataDir 见上)。
|
|
569
621
|
const serveDataDir = resolveServeDataDir(args.dataDir, profile.agent_id);
|
|
622
|
+
const profilePublicUrl = profile.merchant_public?.public_url;
|
|
570
623
|
let node = await startA2aNode({
|
|
571
624
|
profile,
|
|
572
625
|
catalog,
|
|
573
|
-
preferredPort: args.port,
|
|
626
|
+
preferredPort: args.port ?? profile.merchant_public?.a2a_port,
|
|
574
627
|
dataDir: serveDataDir,
|
|
628
|
+
...(profilePublicUrl ? { publicBaseUrl: `https://${profilePublicUrl}` } : {}),
|
|
575
629
|
...(merchantToken ? { ownerToken: merchantToken } : {}),
|
|
576
630
|
ownerTokenSecret: process.env.KIWI_CATALOG_OWNER_TOKEN_SECRET,
|
|
577
631
|
});
|
|
@@ -824,6 +878,9 @@ async function cmdBuyerInit(args) {
|
|
|
824
878
|
...(args.force ? { force: true } : {}),
|
|
825
879
|
});
|
|
826
880
|
printJson(report);
|
|
881
|
+
if (report.ok) {
|
|
882
|
+
process.stdout.write("✓ 商家配置完成。下一步:`kiwi merchant up` 上线(需先装 Caddy、DNS 指向服务器);或 `kiwi merchant setup-public` 查看公网配置。\n");
|
|
883
|
+
}
|
|
827
884
|
return report.ok ? EXIT.OK : EXIT.CONFIG;
|
|
828
885
|
}
|
|
829
886
|
/** `kiwi buyer search`(D4):Product-first 搜索(M3 链路)。 */
|
|
@@ -890,6 +947,10 @@ async function routeMerchant(sub, args) {
|
|
|
890
947
|
return await cmdMerchantInit(args);
|
|
891
948
|
if (sub === "publish")
|
|
892
949
|
return await cmdMerchantPublish(args);
|
|
950
|
+
if (sub === "setup-public")
|
|
951
|
+
return await cmdMerchantSetupPublic(args);
|
|
952
|
+
if (sub === "up")
|
|
953
|
+
return await cmdMerchantUp(args);
|
|
893
954
|
if (sub === "listings")
|
|
894
955
|
return notImplementedProduct("kiwi merchant listings", "D2");
|
|
895
956
|
if (sub === "status")
|
|
@@ -921,9 +982,15 @@ async function cmdMerchantInit(args) {
|
|
|
921
982
|
// --merchant-id / --name flag 优先,env 回退;TTY 下交互提示,缺省自动派生。
|
|
922
983
|
let merchantId = args.merchantId ?? process.env.KIWI_MERCHANT_ID ?? "";
|
|
923
984
|
let name = args.merchantName ?? process.env.KIWI_MERCHANT_NAME ?? "";
|
|
985
|
+
let publicUrl = "";
|
|
986
|
+
let merchantToken = "";
|
|
924
987
|
if (process.stdin.isTTY) {
|
|
925
988
|
merchantId = await promptLine(`merchant_id(回车自动生成${name !== "" ? `,建议 ${slugifyMerchantId(name)}` : ""}): `, merchantId);
|
|
926
989
|
name = await promptLine(`商家名称(回车用缺省): `, name);
|
|
990
|
+
publicUrl = await promptLine("公网域名(可选,回车跳过;如 merchant.example.com): ", "");
|
|
991
|
+
if (publicUrl !== "")
|
|
992
|
+
publicUrl = publicUrl.trim().toLowerCase();
|
|
993
|
+
merchantToken = await promptLine("商家令牌(从商家后台获取,可稍后设置;直接回车跳过): ", "");
|
|
927
994
|
// TTY 下可全回车:merchant_id 自动生成(避免固定名碰撞)。
|
|
928
995
|
if (merchantId === "")
|
|
929
996
|
merchantId = `merchant-${Math.random().toString(36).slice(2, 8)}`;
|
|
@@ -945,6 +1012,8 @@ async function cmdMerchantInit(args) {
|
|
|
945
1012
|
const report = await merchantInit({
|
|
946
1013
|
merchantName: name,
|
|
947
1014
|
...(merchantId !== "" ? { merchantId } : {}),
|
|
1015
|
+
...(publicUrl !== "" ? { publicUrl } : {}),
|
|
1016
|
+
...(merchantToken !== "" ? { merchantToken } : {}),
|
|
948
1017
|
shoppingCliUrl: process.env.SHOPPING_CLI_URL ?? "http://127.0.0.1:8765",
|
|
949
1018
|
shoppingCliDb: process.env.SHOPPING_DB_PATH,
|
|
950
1019
|
catalogUrl: args.catalog ?? process.env.KIWI_CATALOG_URL ?? DEFAULT_CATALOG_URL,
|
|
@@ -965,27 +1034,35 @@ async function cmdMerchantInit(args) {
|
|
|
965
1034
|
* fail-closed:任一步失败 → 非零退出 + 分步报告,不假装全成功。
|
|
966
1035
|
*/
|
|
967
1036
|
async function cmdMerchantPublish(args) {
|
|
968
|
-
const profile =
|
|
1037
|
+
const profile = requireProfileOrDefault(args);
|
|
969
1038
|
if (profile.role !== "merchant") {
|
|
970
1039
|
process.stderr.write("kiwi merchant publish 需要 merchant profile(role: merchant)\n");
|
|
971
1040
|
return EXIT.CONFIG;
|
|
972
1041
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1042
|
+
loadMerchantCredentials(); // 加载 init 写入的 credentials.env(KIWI_MERCHANT_TOKEN)
|
|
1043
|
+
// 缺省路径与 shopping-cli 的 DEFAULT_DB_PATH 一致——商家无需设置 SHOPPING_DB_PATH。
|
|
1044
|
+
const shoppingCliDb = args.shoppingCliDb ?? process.env.SHOPPING_DB_PATH ?? DEFAULT_SHOPPING_DB_PATH;
|
|
1045
|
+
// `--file <csv>`:先导入商品(shopping-cli import-csv-excel)再发布。
|
|
1046
|
+
if (args.file !== undefined && args.file !== "") {
|
|
1047
|
+
const imp = spawnSync("shopping-cli", ["import-csv-excel", "--file", args.file, "--merchant", profile.agent_id, "--format", "json"], { env: { ...process.env, SHOPPING_DB_PATH: shoppingCliDb }, stdio: "inherit" });
|
|
1048
|
+
if (imp.status !== 0) {
|
|
1049
|
+
process.stderr.write(`商品导入失败(shopping-cli import-csv-excel 退出码 ${imp.status ?? "?"})——请检查 CSV 格式\n`);
|
|
1050
|
+
return EXIT.CONFIG;
|
|
1051
|
+
}
|
|
1052
|
+
process.stdout.write(`已导入商品:${args.file}\n`);
|
|
977
1053
|
}
|
|
978
1054
|
if (!existsSync(shoppingCliDb)) {
|
|
979
|
-
process.stderr.write(`shopping-cli 数据库不存在:${shoppingCliDb}
|
|
1055
|
+
process.stderr.write(`shopping-cli 数据库不存在:${shoppingCliDb}(先运行 shopping-cli import-csv-excel 导入商品)\n`);
|
|
980
1056
|
return EXIT.CONFIG;
|
|
981
1057
|
}
|
|
982
1058
|
const ownerTokenSecret = process.env.KIWI_CATALOG_OWNER_TOKEN_SECRET;
|
|
983
|
-
const
|
|
1059
|
+
const merchantTokenEnv = profile.merchant_public?.merchant_token_env;
|
|
1060
|
+
const merchantToken = (merchantTokenEnv ? process.env[merchantTokenEnv] : undefined) || process.env.KIWI_MERCHANT_TOKEN || "";
|
|
984
1061
|
if (!merchantToken && !ownerTokenSecret) {
|
|
985
1062
|
process.stderr.write("需要 KIWI_MERCHANT_TOKEN(随机 token,推荐)或 KIWI_CATALOG_OWNER_TOKEN_SECRET(legacy HMAC)\n");
|
|
986
1063
|
return EXIT.CONFIG;
|
|
987
1064
|
}
|
|
988
|
-
const catalog = args.catalog ?? process.env.KIWI_CATALOG_URL ?? DEFAULT_CATALOG_URL;
|
|
1065
|
+
const catalog = args.catalog ?? profile.merchant_public?.catalog_url ?? process.env.KIWI_CATALOG_URL ?? DEFAULT_CATALOG_URL;
|
|
989
1066
|
const report = await merchantPublish({
|
|
990
1067
|
profile,
|
|
991
1068
|
catalogBaseUrl: catalog,
|
|
@@ -1033,6 +1110,225 @@ async function cmdDemo(args) {
|
|
|
1033
1110
|
return EXIT.TRANSIENT;
|
|
1034
1111
|
}
|
|
1035
1112
|
}
|
|
1113
|
+
/**
|
|
1114
|
+
* `kiwi merchant setup-public`(D3)——公网 A2A 暴露引导向导。
|
|
1115
|
+
* 检测公网 IP、检查域名 DNS、生成 Caddy 反代配置、输出启动与验证命令。
|
|
1116
|
+
* well-known 文件由跑着的 `kiwi merchant start` 节点自动生成,商家无需手写。
|
|
1117
|
+
*/
|
|
1118
|
+
async function cmdMerchantSetupPublic(args) {
|
|
1119
|
+
const profile = requireProfileOrDefault(args); // 无 --profile 时回退缺省 ~/.kiwi/kiwi.yaml
|
|
1120
|
+
const port = args.port ?? profile.merchant_public?.a2a_port ?? (Number(process.env.KIWI_A2A_PORT ?? "") || 9000); // 与 startA2aNode 缺省一致
|
|
1121
|
+
// 单一来源:--domain 显式 > profile.merchant_public.public_url(init 引导写入)>
|
|
1122
|
+
// KIWI_A2A_PUBLIC_URL env 提取;TTY 下再交互提示。
|
|
1123
|
+
let domain = args.domain ?? profile.merchant_public?.public_url ?? extractPublicDomain(process.env.KIWI_A2A_PUBLIC_URL) ?? "";
|
|
1124
|
+
if (domain === "" && process.stdin.isTTY) {
|
|
1125
|
+
domain = await promptLine("公网域名(如 merchant.example.com,TLS 证书就用它): ", "");
|
|
1126
|
+
}
|
|
1127
|
+
if (domain === "") {
|
|
1128
|
+
process.stderr.write("--domain <公网域名> 必填(TTY 下可交互输入)。例:kiwi merchant setup-public --domain merchant.example.com\n");
|
|
1129
|
+
return EXIT.CONFIG;
|
|
1130
|
+
}
|
|
1131
|
+
let normalized;
|
|
1132
|
+
try {
|
|
1133
|
+
normalized = validatePublicDomain(domain);
|
|
1134
|
+
}
|
|
1135
|
+
catch (err) {
|
|
1136
|
+
if (err instanceof SetupPublicError) {
|
|
1137
|
+
process.stderr.write(`${err.message}\n`);
|
|
1138
|
+
return EXIT.CONFIG;
|
|
1139
|
+
}
|
|
1140
|
+
throw err;
|
|
1141
|
+
}
|
|
1142
|
+
const caddyfilePath = args.caddyfile ?? "Caddyfile.kiwi";
|
|
1143
|
+
try {
|
|
1144
|
+
const report = await runMerchantSetupPublic({
|
|
1145
|
+
domain: normalized,
|
|
1146
|
+
port,
|
|
1147
|
+
caddyfilePath,
|
|
1148
|
+
merchantAgentId: profile.agent_id,
|
|
1149
|
+
profilePath: args.profile,
|
|
1150
|
+
checkNow: args.check,
|
|
1151
|
+
});
|
|
1152
|
+
process.stdout.write(`merchant ${report.merchantAgentId} · 端口 ${report.port} · 域名 ${report.domain}\n`);
|
|
1153
|
+
process.stdout.write(`公网 IP:${report.publicIp ?? "(未能自动检测,请手动确认)"}\n`);
|
|
1154
|
+
const dnsMsg = {
|
|
1155
|
+
ok: `DNS 已指向本机(${report.dns.resolved})`,
|
|
1156
|
+
mismatch: `DNS 解析到 ${report.dns.resolved},与公网 IP ${report.dns.expected} 不一致——请到域名服务商把 ${report.domain} 的 A 记录指向 ${report.dns.expected}`,
|
|
1157
|
+
unresolved: `域名 ${report.domain} 当前无法解析——请先到域名服务商添加 A 记录指向服务器公网 IP`,
|
|
1158
|
+
skipped: `无法对比 DNS(未检测到公网 IP),请手动确认 ${report.domain} 的 A 记录指向本机`,
|
|
1159
|
+
};
|
|
1160
|
+
process.stdout.write(`DNS:${dnsMsg[report.dns.status]}\n`);
|
|
1161
|
+
process.stdout.write(`已写入 ${report.caddyfilePath}:\n${report.caddyfile}`);
|
|
1162
|
+
for (const line of report.instructions)
|
|
1163
|
+
process.stdout.write(`${line}\n`);
|
|
1164
|
+
if (report.check !== null) {
|
|
1165
|
+
const status = report.check.httpStatus;
|
|
1166
|
+
process.stdout.write(`\n--check:${status === 200 ? "✓" : status === null ? "✗ 无法连接" : `✗ HTTP ${status}`} ${report.check.url}\n`);
|
|
1167
|
+
}
|
|
1168
|
+
return EXIT.OK;
|
|
1169
|
+
}
|
|
1170
|
+
catch (err) {
|
|
1171
|
+
if (err instanceof SetupPublicError) {
|
|
1172
|
+
process.stderr.write(`${err.message}\n`);
|
|
1173
|
+
return EXIT.CONFIG;
|
|
1174
|
+
}
|
|
1175
|
+
throw err;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* `kiwi merchant up`(D3)——一条命令上线:setup-public(生成 Caddyfile)→ 起 Caddy
|
|
1180
|
+
* 反代(子进程)→ 起 A2A 节点 → 退出时清理 Caddy。把第 4 步三命令合成一个。
|
|
1181
|
+
*/
|
|
1182
|
+
async function cmdMerchantUp(args) {
|
|
1183
|
+
const profile = requireProfileOrDefault(args);
|
|
1184
|
+
if (profile.role !== "merchant") {
|
|
1185
|
+
process.stderr.write("kiwi merchant up 需要 merchant profile(role: merchant)\n");
|
|
1186
|
+
return EXIT.CONFIG;
|
|
1187
|
+
}
|
|
1188
|
+
loadMerchantCredentials(); // 加载 init 写入的 credentials.env(KIWI_MERCHANT_TOKEN)
|
|
1189
|
+
// ── 域名 / 端口 / Caddyfile(profile 兜底)──
|
|
1190
|
+
const domain = args.domain ?? profile.merchant_public?.public_url ?? extractPublicDomain(process.env.KIWI_A2A_PUBLIC_URL) ?? "";
|
|
1191
|
+
if (domain === "") {
|
|
1192
|
+
process.stderr.write("需要公网域名——先 `kiwi merchant init` 填公网域名,或 --domain <域名>\n");
|
|
1193
|
+
return EXIT.CONFIG;
|
|
1194
|
+
}
|
|
1195
|
+
const port = args.port ?? profile.merchant_public?.a2a_port ?? (Number(process.env.KIWI_A2A_PORT ?? "") || 9000);
|
|
1196
|
+
const caddyfilePath = args.caddyfile ?? "Caddyfile.kiwi";
|
|
1197
|
+
// ── setup-public:检测 IP / DNS 检查 / 生成 Caddyfile(幂等)──
|
|
1198
|
+
await runMerchantSetupPublic({
|
|
1199
|
+
domain,
|
|
1200
|
+
port,
|
|
1201
|
+
caddyfilePath,
|
|
1202
|
+
merchantAgentId: profile.agent_id,
|
|
1203
|
+
profilePath: args.profile,
|
|
1204
|
+
});
|
|
1205
|
+
process.stdout.write(`[merchant up] 域名 ${domain} · 端口 ${port} · Caddyfile ${caddyfilePath}\n`);
|
|
1206
|
+
// ── 检查 Caddy 是否安装(fail-closed)──
|
|
1207
|
+
const caddyOk = spawnSync("caddy", ["version"], { stdio: "ignore" }).status === 0;
|
|
1208
|
+
if (!caddyOk) {
|
|
1209
|
+
process.stderr.write("未检测到 Caddy(反代 + TLS)。请先安装:`brew install caddy`(或 https://caddyserver.com/download),然后重试。\n");
|
|
1210
|
+
return EXIT.CONFIG;
|
|
1211
|
+
}
|
|
1212
|
+
// ── 起 Caddy 反代(子进程,日志透传)──
|
|
1213
|
+
const caddy = spawn("caddy", ["run", "--config", caddyfilePath], { stdio: "inherit" });
|
|
1214
|
+
// ── 起 A2A 节点(与 merchant start --no-chat 同一路径)──
|
|
1215
|
+
const catalog = args.catalog ?? profile.merchant_public?.catalog_url ?? process.env.KIWI_CATALOG_URL ?? DEFAULT_CATALOG_URL;
|
|
1216
|
+
const merchantToken = process.env.KIWI_MERCHANT_TOKEN || "";
|
|
1217
|
+
const serveDataDir = resolveServeDataDir(args.dataDir, profile.agent_id);
|
|
1218
|
+
let node = await startA2aNode({
|
|
1219
|
+
profile,
|
|
1220
|
+
catalog,
|
|
1221
|
+
preferredPort: port,
|
|
1222
|
+
dataDir: serveDataDir,
|
|
1223
|
+
...(domain ? { publicBaseUrl: `https://${domain}` } : {}),
|
|
1224
|
+
...(merchantToken ? { ownerToken: merchantToken } : {}),
|
|
1225
|
+
ownerTokenSecret: process.env.KIWI_CATALOG_OWNER_TOKEN_SECRET,
|
|
1226
|
+
});
|
|
1227
|
+
process.stdout.write(`[merchant up] A2A server: ${node.agentCardUrl}(local ${node.url})· catalog: ${node.catalogAgentId ?? "?"}\n`);
|
|
1228
|
+
process.stdout.write("[merchant up] 已上线 — Ctrl+C to stop(会同时停掉 Caddy 与节点)\n");
|
|
1229
|
+
const shutdown = async () => {
|
|
1230
|
+
await node?.stop().catch(() => undefined);
|
|
1231
|
+
node = null;
|
|
1232
|
+
caddy.kill(); // SIGTERM → Caddy 优雅退出
|
|
1233
|
+
process.exit(0);
|
|
1234
|
+
};
|
|
1235
|
+
process.on("SIGINT", shutdown);
|
|
1236
|
+
process.on("SIGTERM", shutdown);
|
|
1237
|
+
await new Promise(() => { });
|
|
1238
|
+
return EXIT.OK;
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* `kiwi setup-hermes`(买家侧)——一键把 Kiwi 买家连接器接入 Hermes:
|
|
1242
|
+
* 配置 kiwi-buyer-mcp MCP server + 安装 kiwi-buyer skill。之后 Hermes 重启
|
|
1243
|
+
* 即可调用 9 个采购工具。等价于手动执行:
|
|
1244
|
+
* hermes mcp add kiwi-buyer-mcp --command node --args <cli> mcp serve ...
|
|
1245
|
+
* hermes skills install <SKILL.md URL>
|
|
1246
|
+
*/
|
|
1247
|
+
async function cmdSetupHermes() {
|
|
1248
|
+
// 1. 检查 Hermes CLI
|
|
1249
|
+
const hermesOk = spawnSync("hermes", ["--version"], { stdio: "ignore" }).status === 0;
|
|
1250
|
+
if (!hermesOk) {
|
|
1251
|
+
process.stderr.write("未检测到 Hermes CLI(hermes)——请先安装 Hermes,再运行本命令。\n");
|
|
1252
|
+
return EXIT.CONFIG;
|
|
1253
|
+
}
|
|
1254
|
+
// 2. 本机 kiwi cli 路径(dist/cli.js)、principal、db 路径、skill 落盘路径
|
|
1255
|
+
const cliPath = fileURLToPath(import.meta.url);
|
|
1256
|
+
let username = "";
|
|
1257
|
+
try {
|
|
1258
|
+
username = userInfo().username;
|
|
1259
|
+
}
|
|
1260
|
+
catch {
|
|
1261
|
+
username = process.env.USER ?? "user";
|
|
1262
|
+
}
|
|
1263
|
+
const dbPath = path.join(homedir(), ".kiwi", "buyer.sqlite");
|
|
1264
|
+
const skillUrl = "https://raw.githubusercontent.com/harrylabsj/kiwi/main/integrations/hosts/hermes/SKILL.md";
|
|
1265
|
+
const skillPath = path.join(homedir(), ".hermes", "skills", "kiwi-buyer", "SKILL.md");
|
|
1266
|
+
const configured = [];
|
|
1267
|
+
const already = [];
|
|
1268
|
+
// 3. MCP server:已存在则跳过,否则添加(避免 overwrite 交互提示)
|
|
1269
|
+
const mcpList = spawnSync("hermes", ["mcp", "list"], { encoding: "utf-8" });
|
|
1270
|
+
const mcpExists = (mcpList.stdout ?? "").includes("kiwi-buyer-mcp");
|
|
1271
|
+
if (mcpExists) {
|
|
1272
|
+
already.push("MCP server kiwi-buyer-mcp");
|
|
1273
|
+
}
|
|
1274
|
+
else {
|
|
1275
|
+
process.stdout.write("[setup-hermes] 配置 kiwi-buyer-mcp MCP server ...\n");
|
|
1276
|
+
const mcp = spawnSync("hermes", [
|
|
1277
|
+
"mcp",
|
|
1278
|
+
"add",
|
|
1279
|
+
"kiwi-buyer-mcp",
|
|
1280
|
+
"--command",
|
|
1281
|
+
"node",
|
|
1282
|
+
"--args",
|
|
1283
|
+
cliPath,
|
|
1284
|
+
"mcp",
|
|
1285
|
+
"serve",
|
|
1286
|
+
"--db",
|
|
1287
|
+
dbPath,
|
|
1288
|
+
"--principal",
|
|
1289
|
+
`hermes:${username}`,
|
|
1290
|
+
"--agent",
|
|
1291
|
+
"buyer-agent:hermes",
|
|
1292
|
+
], { stdio: "inherit" });
|
|
1293
|
+
if (mcp.status !== 0) {
|
|
1294
|
+
process.stderr.write(`hermes mcp add 失败(退出码 ${mcp.status ?? "?"})\n`);
|
|
1295
|
+
return EXIT.CONFIG;
|
|
1296
|
+
}
|
|
1297
|
+
configured.push("MCP server kiwi-buyer-mcp");
|
|
1298
|
+
}
|
|
1299
|
+
// 4. kiwi-buyer skill:已存在则跳过;否则 hermes install(失败则直接 fetch 落盘兜底)
|
|
1300
|
+
if (existsSync(skillPath)) {
|
|
1301
|
+
already.push("skill kiwi-buyer");
|
|
1302
|
+
}
|
|
1303
|
+
else {
|
|
1304
|
+
process.stdout.write("[setup-hermes] 安装 kiwi-buyer skill ...\n");
|
|
1305
|
+
const skill = spawnSync("hermes", ["skills", "install", skillUrl, "--yes"], { stdio: "inherit" });
|
|
1306
|
+
if (skill.status !== 0 || !existsSync(skillPath)) {
|
|
1307
|
+
// 兜底:直接 fetch SKILL.md 写入 ~/.hermes/skills/kiwi-buyer/(Hermes skills 为 local 存储)
|
|
1308
|
+
process.stdout.write("[setup-hermes] hermes skills install 不可用,改用直接写入 ...\n");
|
|
1309
|
+
try {
|
|
1310
|
+
const res = await fetch(skillUrl, { signal: AbortSignal.timeout(15000) });
|
|
1311
|
+
if (!res.ok)
|
|
1312
|
+
throw new Error(`HTTP ${res.status}`);
|
|
1313
|
+
const content = await res.text();
|
|
1314
|
+
mkdirSync(path.dirname(skillPath), { recursive: true, mode: 0o700 });
|
|
1315
|
+
writeFileSync(skillPath, content, { mode: 0o600 });
|
|
1316
|
+
}
|
|
1317
|
+
catch (err) {
|
|
1318
|
+
process.stderr.write(`安装 kiwi-buyer skill 失败:${err instanceof Error ? err.message : String(err)}\n`);
|
|
1319
|
+
return EXIT.CONFIG;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
configured.push("skill kiwi-buyer");
|
|
1323
|
+
}
|
|
1324
|
+
// 5. 报告
|
|
1325
|
+
for (const item of already)
|
|
1326
|
+
process.stdout.write(` ✓ ${item} 已配置,跳过\n`);
|
|
1327
|
+
for (const item of configured)
|
|
1328
|
+
process.stdout.write(` ✓ ${item} 已配置\n`);
|
|
1329
|
+
process.stdout.write("✓ Kiwi 已接入 Hermes。请重启 Hermes(或开新会话),然后直接说出采购需求(如「帮我买 200 个 USB-C 扩展坞,7 天内到」)。\n");
|
|
1330
|
+
return EXIT.OK;
|
|
1331
|
+
}
|
|
1036
1332
|
function notImplementedProduct(name, target) {
|
|
1037
1333
|
process.stderr.write(`${name} 尚未实现(产品层完成定义 ${target},见 docs/kiwi-product-layer-refactor-rev1.1.md §19)。\n`);
|
|
1038
1334
|
return EXIT.CONFIG;
|
|
@@ -1087,6 +1383,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1087
1383
|
return await routeBuyer(sub, args);
|
|
1088
1384
|
if (cmd === "merchant")
|
|
1089
1385
|
return await routeMerchant(sub, args);
|
|
1386
|
+
if (cmd === "setup-hermes")
|
|
1387
|
+
return await cmdSetupHermes();
|
|
1090
1388
|
if (cmd === "network")
|
|
1091
1389
|
return await routeNetwork(sub);
|
|
1092
1390
|
if (cmd === "demo")
|