@kkeve/suite 0.0.13 → 0.0.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/bin/keve.js +2 -1
- package/dist/commands/clean.js +2 -2
- package/dist/commands/config.js +7 -7
- package/dist/commands/init.js +2 -2
- package/dist/commands/report.js +7 -7
- package/dist/commands/run.js +16 -12
- package/dist/commands/test.js +7 -7
- package/dist/config.js +7 -7
- package/dist/core/action-log-writer.js +4 -1
- package/dist/core/decorator/keve-decorators.d.ts +128 -0
- package/dist/core/decorator/keve-decorators.js +254 -0
- package/dist/core/decorator/keve-host.d.ts +40 -0
- package/dist/core/decorator/keve-host.js +33 -0
- package/dist/core/{keve-registry.d.ts → decorator/keve-registry.d.ts} +19 -0
- package/dist/core/{keve-registry.js → decorator/keve-registry.js} +13 -1
- package/dist/core/generateReport/report.d.ts +1 -1
- package/dist/core/generateReport/report.js +2 -2
- package/dist/core/generateReport/reportData.d.ts +3 -0
- package/dist/core/generateReport/reportData.js +55 -18
- package/dist/core/judge/confidence.d.ts +119 -0
- package/dist/core/judge/confidence.js +295 -0
- package/dist/core/judge/goal-heuristics.d.ts +32 -0
- package/dist/core/judge/goal-heuristics.js +128 -0
- package/dist/core/judge/pngCodec.d.ts +23 -0
- package/dist/core/judge/pngCodec.js +215 -0
- package/dist/core/judge/screenshotDiff.d.ts +112 -0
- package/dist/core/judge/screenshotDiff.js +420 -0
- package/dist/core/page-object/definePage.d.ts +95 -0
- package/dist/core/page-object/definePage.js +62 -0
- package/dist/core/page-object/elementResolver.d.ts +32 -0
- package/dist/core/page-object/elementResolver.js +236 -0
- package/dist/core/page-object/engine-types.d.ts +140 -0
- package/dist/core/page-object/engine-types.js +34 -0
- package/dist/core/sso-cookie.d.ts +18 -0
- package/dist/core/sso-cookie.js +10 -0
- package/dist/dsl/index.d.ts +15 -0
- package/dist/dsl/index.js +29 -0
- package/dist/dsl/locatorSource.d.ts +25 -0
- package/dist/dsl/locatorSource.js +323 -0
- package/dist/dsl/ops.d.ts +46 -0
- package/dist/dsl/ops.js +95 -0
- package/dist/dsl/toDecorator.d.ts +98 -0
- package/dist/dsl/toDecorator.js +747 -0
- package/dist/engine-cypress/adapter.d.ts +108 -0
- package/dist/engine-cypress/adapter.js +514 -0
- package/dist/engine-cypress/agent-page.d.ts +159 -0
- package/dist/engine-cypress/agent-page.js +566 -0
- package/dist/engine-cypress/agentDom.d.ts +22 -0
- package/dist/engine-cypress/agentDom.js +590 -0
- package/dist/engine-cypress/bridge.d.ts +39 -0
- package/dist/engine-cypress/bridge.js +598 -0
- package/dist/engine-cypress/cdp.d.ts +78 -0
- package/dist/engine-cypress/cdp.js +339 -0
- package/dist/engine-cypress/host.d.ts +22 -0
- package/dist/engine-cypress/host.js +480 -0
- package/dist/engine-cypress/index.d.ts +26 -0
- package/dist/engine-cypress/index.js +58 -0
- package/dist/engine-cypress/keveGoal.d.ts +43 -0
- package/dist/engine-cypress/keveGoal.js +265 -0
- package/dist/engine-cypress/pageOps.d.ts +41 -0
- package/dist/engine-cypress/pageOps.js +439 -0
- package/dist/engine-cypress/reporter.d.ts +20 -0
- package/dist/engine-cypress/reporter.js +128 -0
- package/dist/engine-cypress/setup.d.ts +21 -0
- package/dist/engine-cypress/setup.js +27 -0
- package/dist/engine-cypress/shotBuffer.d.ts +40 -0
- package/dist/engine-cypress/shotBuffer.js +41 -0
- package/dist/engine-cypress/state.d.ts +56 -0
- package/dist/engine-cypress/state.js +81 -0
- package/dist/engine-playwright/engineAdapter.d.ts +78 -0
- package/dist/engine-playwright/engineAdapter.js +371 -0
- package/dist/engine-playwright/global-setup.d.ts +28 -0
- package/dist/engine-playwright/global-setup.js +142 -0
- package/dist/{core → engine-playwright}/keve-report.d.ts +0 -1
- package/dist/engine-playwright/keve-report.js +137 -0
- package/dist/{core → engine-playwright}/keve-test.d.ts +10 -8
- package/dist/{core → engine-playwright}/keve-test.js +257 -207
- package/dist/{core → engine-playwright}/pw-config.js +8 -7
- package/dist/index.d.ts +13 -8
- package/dist/index.js +43 -28
- package/dist/page-agent/agent.d.ts +98 -7
- package/dist/page-agent/agent.js +338 -84
- package/dist/page-agent/diagnostics.d.ts +184 -0
- package/dist/page-agent/diagnostics.js +377 -0
- package/dist/page-agent/hooks.d.ts +3 -2
- package/dist/page-agent/hooks.js +4 -3
- package/dist/page-agent/page-like.d.ts +116 -0
- package/dist/page-agent/page-like.js +14 -0
- package/dist/page-agent/playwright-page.d.ts +20 -0
- package/dist/page-agent/playwright-page.js +98 -0
- package/dist/page-agent/system-prompt.d.ts +7 -0
- package/dist/page-agent/system-prompt.js +10 -0
- package/dist/page-agent/system_prompt.md +1 -1
- package/dist/page-agent/tools.d.ts +1 -1
- package/dist/page-agent/tools.js +441 -9
- package/doc/skills/keve-e2e-gen/references/script-generation-guide.md +11 -0
- package/package.json +24 -11
- package/scripts/gen-system-prompt.mjs +36 -0
- package/scripts/run-dsl-task.mjs +619 -0
- package/scripts/runtime/cypress-preprocessor.mjs +44 -0
- package/scripts/runtime/cypress.config.mjs +35 -0
- package/scripts/runtime/playwright.config.mjs +41 -0
- package/scripts/verify-shot-diff.mjs +243 -0
- package/dist/commands/auth-update.d.ts +0 -16
- package/dist/commands/auth-update.js +0 -88
- package/dist/core/auth.d.ts +0 -17
- package/dist/core/auth.js +0 -224
- package/dist/core/cdp-resolver.d.ts +0 -5
- package/dist/core/cdp-resolver.js +0 -93
- package/dist/core/global-setup.d.ts +0 -6
- package/dist/core/global-setup.js +0 -113
- package/dist/core/keve-decorators.d.ts +0 -51
- package/dist/core/keve-decorators.js +0 -95
- package/dist/core/keve-report.js +0 -376
- /package/dist/core/{keve-aspect.d.ts → decorator/keve-aspect.d.ts} +0 -0
- /package/dist/core/{keve-aspect.js → decorator/keve-aspect.js} +0 -0
- /package/dist/{core → engine-playwright}/pw-config.d.ts +0 -0
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* keve-test — Playwright test fixture: keveGoal (唯一操作原语)
|
|
4
4
|
*
|
|
5
|
-
* keveGoal 执行 fn 后 → 截图+fn逻辑+expected →
|
|
6
|
-
* 一致 → 通过
|
|
7
|
-
* 不一致 → AI 告诉你还差什么 → Re-Act 探索 → 探索成功
|
|
5
|
+
* keveGoal 执行 fn 后 → 截图+fn逻辑+expected → Agent Re-Act 观察页面后给最终结论
|
|
8
6
|
* fn 无 → Re-Act 直接探索
|
|
7
|
+
* fn 成功 → 仍进 Re-Act,确认预期达成并产出推理轨迹
|
|
9
8
|
* fn 异常 → Re-Act 自愈
|
|
9
|
+
* 确定性事实(环境阻断 / 页面已关闭 / 截图差异超阈值)→ 短路,不烧 token
|
|
10
10
|
*
|
|
11
11
|
* keveAssert 已被吸收:keveGoal 的 expected 即断言语义
|
|
12
12
|
*
|
|
@@ -56,33 +56,38 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
56
56
|
};
|
|
57
57
|
})();
|
|
58
58
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
59
|
-
exports.test = exports.sceneGoalsMap = exports.sceneEvalMetaMap = exports.sceneCodeMap = exports.getModelScenes = exports.keveScene = exports.keveModel = exports.expect = void 0;
|
|
59
|
+
exports.test = exports.sceneGoalsMap = exports.sceneMetaMap = exports.sceneEvalMetaMap = exports.sceneCodeMap = exports.getModelScenes = exports.keveScene = exports.keveModel = exports.expect = void 0;
|
|
60
60
|
const test_1 = require("@playwright/test");
|
|
61
61
|
Object.defineProperty(exports, "expect", { enumerable: true, get: function () { return test_1.expect; } });
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
62
|
+
const keve_registry_js_1 = require("../core/decorator/keve-registry.js");
|
|
63
|
+
const agent_js_1 = require("../page-agent/agent.js");
|
|
64
|
+
const keve_aspect_js_1 = require("../core/decorator/keve-aspect.js");
|
|
65
|
+
const learned_actions_js_1 = require("../core/learned-actions.js");
|
|
66
|
+
const config_js_1 = require("../config.js");
|
|
66
67
|
const fs = __importStar(require("node:fs"));
|
|
67
68
|
const path = __importStar(require("node:path"));
|
|
68
|
-
// ───
|
|
69
|
-
// CDP
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
Object.defineProperty(exports, "
|
|
77
|
-
Object.defineProperty(exports, "
|
|
69
|
+
// ─── 登录态注入 ─────────────────────────────────────────────────────
|
|
70
|
+
// 不再由测试侧启动浏览器 / CDP 接管。登录 cookie 由服务端解析后随环境变量下发,
|
|
71
|
+
// 这里只负责在启动浏览器前把 storageState 就位(global-setup 已写好文件)。
|
|
72
|
+
const global_setup_js_1 = require("./global-setup.js");
|
|
73
|
+
const screenshotDiff_js_1 = require("../core/judge/screenshotDiff.js");
|
|
74
|
+
const goal_heuristics_js_1 = require("../core/judge/goal-heuristics.js");
|
|
75
|
+
var keve_decorators_js_1 = require("../core/decorator/keve-decorators.js");
|
|
76
|
+
Object.defineProperty(exports, "keveModel", { enumerable: true, get: function () { return keve_decorators_js_1.keveModel; } });
|
|
77
|
+
Object.defineProperty(exports, "keveScene", { enumerable: true, get: function () { return keve_decorators_js_1.keveScene; } });
|
|
78
|
+
Object.defineProperty(exports, "getModelScenes", { enumerable: true, get: function () { return keve_decorators_js_1.getModelScenes; } });
|
|
79
|
+
var keve_registry_js_2 = require("../core/decorator/keve-registry.js");
|
|
80
|
+
Object.defineProperty(exports, "sceneCodeMap", { enumerable: true, get: function () { return keve_registry_js_2.sceneCodeMap; } });
|
|
81
|
+
Object.defineProperty(exports, "sceneEvalMetaMap", { enumerable: true, get: function () { return keve_registry_js_2.sceneEvalMetaMap; } });
|
|
82
|
+
Object.defineProperty(exports, "sceneMetaMap", { enumerable: true, get: function () { return keve_registry_js_2.sceneMetaMap; } });
|
|
78
83
|
// Re-export goal metadata for consumer convenience
|
|
79
|
-
var
|
|
80
|
-
Object.defineProperty(exports, "sceneGoalsMap", { enumerable: true, get: function () { return
|
|
84
|
+
var keve_registry_js_3 = require("../core/decorator/keve-registry.js");
|
|
85
|
+
Object.defineProperty(exports, "sceneGoalsMap", { enumerable: true, get: function () { return keve_registry_js_3.sceneGoalsMap; } });
|
|
81
86
|
// ─── Register Built-in Aspects ───────────────────────────────────────
|
|
82
87
|
// NOTE: action-log-write aspect 已废弃。
|
|
83
88
|
// AI 探索过程数据现在通过 keveGoalResult attachment 直接流到 KeveReporter,
|
|
84
89
|
// 由 KeveReporter 统一写入 confidence-data.jsonl(含 reactSteps)。
|
|
85
|
-
|
|
90
|
+
keve_aspect_js_1.keveAspect.register({
|
|
86
91
|
name: 'learnedActions-update',
|
|
87
92
|
phase: 'after',
|
|
88
93
|
order: 20,
|
|
@@ -90,7 +95,7 @@ keve_aspect_1.keveAspect.register({
|
|
|
90
95
|
if (!result?.actions?.length)
|
|
91
96
|
return;
|
|
92
97
|
const url = ctx.page.url();
|
|
93
|
-
|
|
98
|
+
learned_actions_js_1.learnedActions.add(ctx.step, result.actions, url, result.success);
|
|
94
99
|
},
|
|
95
100
|
});
|
|
96
101
|
// ─── (diagnostic extraction moved to page-agent/diagnostic.ts, called via onAfterTask hook) ──
|
|
@@ -98,47 +103,52 @@ keve_aspect_1.keveAspect.register({
|
|
|
98
103
|
exports.test = test_1.test.extend({
|
|
99
104
|
// ── 将内置 testInfo 包装为可解构 fixture ─────────────────────────
|
|
100
105
|
// 测试方法可直接写 { page, keveGoal, testInfo },不再报 "unknown parameter" 错误
|
|
106
|
+
// ── engine: 引擎无关操作适配器 ──────────────────────────────────────
|
|
107
|
+
engine: async ({ page }, use) => {
|
|
108
|
+
const { createEngine } = await Promise.resolve().then(() => __importStar(require('./engineAdapter.js')));
|
|
109
|
+
const engine = createEngine(page);
|
|
110
|
+
await use(engine);
|
|
111
|
+
},
|
|
101
112
|
testInfo: async ({}, use, testInfo) => {
|
|
102
113
|
await use(testInfo);
|
|
103
114
|
},
|
|
104
115
|
browser: async ({}, use, testInfo) => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// CDP 模式下不关闭浏览器(共享连接,关闭会影响后续用例)
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
const browser = await test_1.chromium.launch(testInfo.project?.use?.launchOptions || {});
|
|
112
|
-
await use(browser);
|
|
113
|
-
await browser.close();
|
|
114
|
-
}
|
|
116
|
+
const browser = await test_1.chromium.launch(testInfo.project?.use?.launchOptions || {});
|
|
117
|
+
await use(browser);
|
|
118
|
+
await browser.close();
|
|
115
119
|
},
|
|
116
|
-
page: async ({ browser }, use) => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
page = existingPages.find(p => p.url() !== 'about:blank') || existingPages[0];
|
|
126
|
-
// 导航到 about:blank 作为起始页(清空之前的状态,但保留 cookie)
|
|
127
|
-
try {
|
|
128
|
-
await page.goto('about:blank', { timeout: 3000 });
|
|
129
|
-
}
|
|
130
|
-
catch { /* ignore */ }
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
page = await cdpContext.newPage();
|
|
120
|
+
page: async ({ browser }, use, testInfo) => {
|
|
121
|
+
// 建 context 时带上 storageState(服务端 cookies 写入的登录态),
|
|
122
|
+
// 裸 newPage() 会丢失 cookies 导致用例未登录失败。
|
|
123
|
+
// global-setup 正常会先写好;直接裸跑 playwright test 时这里兜底就地生成。
|
|
124
|
+
let storageState = testInfo.project?.use?.storageState;
|
|
125
|
+
if (!storageState && (0, global_setup_js_1.readInjectedCookies)().length > 0) {
|
|
126
|
+
storageState = (0, global_setup_js_1.resolveStorageStatePath)();
|
|
127
|
+
try {
|
|
128
|
+
(0, global_setup_js_1.writeStorageState)(storageState);
|
|
134
129
|
}
|
|
135
|
-
|
|
136
|
-
// CDP 模式下不关闭 page(保留浏览器 tab 给下一个用例复用)
|
|
130
|
+
catch { /* 写失败则裸跑,用例会自行暴露登录问题 */ }
|
|
137
131
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
132
|
+
// 视频录制:playwright config use.video = { mode:'on', dir } → recordVideo
|
|
133
|
+
//(自定义 context 不会自动附加视频附件,需在 context 关闭后显式 attach)
|
|
134
|
+
const videoOpt = testInfo.project?.use?.video;
|
|
135
|
+
const videoOn = !!videoOpt && (videoOpt === 'on' || videoOpt.mode === 'on' || videoOpt.mode === 'retain-on-failure');
|
|
136
|
+
const context = await browser.newContext({
|
|
137
|
+
storageState,
|
|
138
|
+
...(videoOn ? { recordVideo: { dir: typeof videoOpt === 'object' && videoOpt.dir ? videoOpt.dir : undefined } } : {}),
|
|
139
|
+
});
|
|
140
|
+
const page = await context.newPage();
|
|
141
|
+
await use(page);
|
|
142
|
+
const video = page.video();
|
|
143
|
+
await page.close();
|
|
144
|
+
await context.close();
|
|
145
|
+
// 视频文件在 context 关闭后才定稿:显式挂附件 → results.json attachments
|
|
146
|
+
if (video) {
|
|
147
|
+
try {
|
|
148
|
+
const videoPath = await video.path();
|
|
149
|
+
await testInfo.attach('video', { path: videoPath });
|
|
150
|
+
}
|
|
151
|
+
catch { /* 无视频不阻塞 */ }
|
|
142
152
|
}
|
|
143
153
|
},
|
|
144
154
|
// ── keveGoal: 唯一操作原语 ────────────────────────────────────────
|
|
@@ -152,9 +162,9 @@ exports.test = test_1.test.extend({
|
|
|
152
162
|
order: goalOrder++,
|
|
153
163
|
};
|
|
154
164
|
const sceneTitle = testInfo.title;
|
|
155
|
-
if (!
|
|
156
|
-
|
|
157
|
-
|
|
165
|
+
if (!keve_registry_js_1.sceneGoalsMap.has(sceneTitle))
|
|
166
|
+
keve_registry_js_1.sceneGoalsMap.set(sceneTitle, []);
|
|
167
|
+
keve_registry_js_1.sceneGoalsMap.get(sceneTitle).push(goalMeta);
|
|
158
168
|
const ctx = {
|
|
159
169
|
page,
|
|
160
170
|
step: options.step,
|
|
@@ -165,7 +175,7 @@ exports.test = test_1.test.extend({
|
|
|
165
175
|
specFilePath: testInfo.file,
|
|
166
176
|
};
|
|
167
177
|
// Run before aspects
|
|
168
|
-
await
|
|
178
|
+
await keve_aspect_js_1.keveAspect.runPhase('before', ctx);
|
|
169
179
|
// ── Capture goal-before screenshot (fn 之前的页面原始状态) ──
|
|
170
180
|
// 语义:记录"测试动作发生前"的页面,供报告 before/after 对照
|
|
171
181
|
let goalScreenshotBefore = '';
|
|
@@ -176,24 +186,46 @@ exports.test = test_1.test.extend({
|
|
|
176
186
|
let result;
|
|
177
187
|
const fnSource = fn?.toString() || undefined;
|
|
178
188
|
let fnError;
|
|
189
|
+
let fnErrorRaw; // 原始错误对象(ScreenshotDiffError 等带结构的错误需要保留结构)
|
|
190
|
+
let fnResult; // fn 返回值透传(flow 产物,如 createDashboard 的看板名)
|
|
179
191
|
let fnBlocked = false; // true when fn error is environment-blocked (skip agent Re-Act)
|
|
192
|
+
// 截图对比:fn 前开缓冲、fn 后收集 —— engine.expectScreenshot 的结果经此进 attachment
|
|
193
|
+
(0, screenshotDiff_js_1.beginGoalShotDiffCapture)();
|
|
180
194
|
if (fn) {
|
|
181
195
|
try {
|
|
182
|
-
await fn();
|
|
196
|
+
fnResult = await fn();
|
|
183
197
|
console.log(`[keveGoal] "${options.step}" fn executed`);
|
|
184
198
|
}
|
|
185
199
|
catch (err) {
|
|
186
200
|
fnError = err?.message || String(err);
|
|
201
|
+
fnErrorRaw = err;
|
|
187
202
|
console.log(`[keveGoal] "${options.step}" fn error: ${fnError}`);
|
|
188
203
|
// ── Detect environment-blocked errors: skip Agent Re-Act, directly BLOCKED ──
|
|
189
204
|
// When fn fails due to empty URL, connection refused, timeout, or navigation to invalid URL,
|
|
190
205
|
// Agent exploration cannot fix environment issues — waste of time and tokens.
|
|
191
|
-
fnBlocked = isEnvironmentBlockedError(err);
|
|
206
|
+
fnBlocked = (0, goal_heuristics_js_1.isEnvironmentBlockedError)(err);
|
|
192
207
|
if (fnBlocked) {
|
|
193
208
|
console.log(`[keveGoal] 🚫 "${options.step}" BLOCKED — environment error detected, skipping agent Re-Act`);
|
|
194
209
|
}
|
|
210
|
+
// ── Detect page-level permission block (forbidden / unauthorized) ──
|
|
211
|
+
// When the page redirected to a forbidden/unauthorized URL (e.g. kwaibi /pc/dashboard/forbidden),
|
|
212
|
+
// the fn error is typically a locator timeout — but the root cause is access denial,
|
|
213
|
+
// not a missing element. Mark as blocked and annotate the error for accurate reporting.
|
|
214
|
+
if (!fnBlocked && fnError) {
|
|
215
|
+
try {
|
|
216
|
+
const blockedUrl = page.url();
|
|
217
|
+
if (/\/forbidden|\/unauthorized|\/403|\/no-permission/i.test(blockedUrl)) {
|
|
218
|
+
fnBlocked = true;
|
|
219
|
+
fnError = `页面访问被阻断(当前 URL=${blockedUrl}),目标元素未渲染 — 权限或数据问题,非用例缺陷`;
|
|
220
|
+
console.log(`[keveGoal] 🚫 "${options.step}" BLOCKED — page redirected to forbidden/unauthorized: ${blockedUrl}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch { /* page.url() may fail if page is closed */ }
|
|
224
|
+
}
|
|
195
225
|
}
|
|
196
226
|
}
|
|
227
|
+
// fn 结束(无论成败)即取出本 goal 的对比结果;不再有引擎路径会往里写
|
|
228
|
+
const shotDiffs = (0, screenshotDiff_js_1.drainGoalShotDiff)();
|
|
197
229
|
// ── Capture fn-after screenshot (fn 执行后的页面状态,传给 agent 说明执行前后) ──
|
|
198
230
|
let fnAfterScreenshot = '';
|
|
199
231
|
if (fn) {
|
|
@@ -204,7 +236,7 @@ exports.test = test_1.test.extend({
|
|
|
204
236
|
}
|
|
205
237
|
// ── Hand off to agent: agent does Re-Act (OR shortcut to blocked if fnBlocked) ──
|
|
206
238
|
const currentUrl = page.url();
|
|
207
|
-
const learnedHint =
|
|
239
|
+
const learnedHint = learned_actions_js_1.learnedActions.getHint(options.step, currentUrl);
|
|
208
240
|
let reactResult;
|
|
209
241
|
let reactTimedOut = false;
|
|
210
242
|
if (fnBlocked) {
|
|
@@ -224,43 +256,92 @@ exports.test = test_1.test.extend({
|
|
|
224
256
|
};
|
|
225
257
|
}
|
|
226
258
|
else {
|
|
227
|
-
// ──
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const goalTimeout = setTimeout(() => {
|
|
232
|
-
goalTimeoutController.abort();
|
|
233
|
-
console.log(`[keveGoal] ⏰ "${options.step}" timed out after ${GOAL_TIMEOUT_MS}ms — stopping agent`);
|
|
234
|
-
}, GOAL_TIMEOUT_MS);
|
|
259
|
+
// ── Page liveness check: if fn killed the page, skip Agent Re-Act ──
|
|
260
|
+
// When fn error involves page/browser close, the page is dead —
|
|
261
|
+
// Agent cannot take a11y snapshots or interact. Skip immediately.
|
|
262
|
+
let pageAlive = true;
|
|
235
263
|
try {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
fnResult: fnError ? { error: fnError } : { success: true },
|
|
241
|
-
signal: goalTimeoutController.signal,
|
|
242
|
-
goalScreenshotBefore: goalScreenshotBefore || undefined,
|
|
243
|
-
fnAfterScreenshot: fnAfterScreenshot || undefined,
|
|
244
|
-
});
|
|
264
|
+
await page.url();
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
pageAlive = false;
|
|
245
268
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const isGoalTimeout = goalTimeoutController.signal.aborted;
|
|
249
|
-
const msg = isGoalTimeout
|
|
250
|
-
? `Goal timed out (${GOAL_TIMEOUT_MS}ms)`
|
|
251
|
-
: (reactErr?.message || String(reactErr));
|
|
252
|
-
console.log(`[keveGoal] reactLoop interrupted: ${msg.slice(0, 200)}`);
|
|
269
|
+
if (!pageAlive) {
|
|
270
|
+
console.log(`[keveGoal] 🚫 "${options.step}" BLOCKED — page is dead after fn error, skipping agent Re-Act`);
|
|
253
271
|
reactResult = {
|
|
254
272
|
expectedMet: false,
|
|
255
|
-
actions: [
|
|
273
|
+
actions: [{
|
|
274
|
+
action: { tool: 'done', verdict: 'blocked', text: `Page closed after fn error: ${fnError}` },
|
|
275
|
+
toolOutput: '',
|
|
276
|
+
result: 'ok',
|
|
277
|
+
}],
|
|
256
278
|
finalSnapshot: '',
|
|
257
279
|
agentScreenshots: [],
|
|
258
|
-
conclusion:
|
|
280
|
+
conclusion: 'blocked',
|
|
259
281
|
};
|
|
260
282
|
}
|
|
261
|
-
|
|
262
|
-
|
|
283
|
+
else if (fnErrorRaw instanceof screenshotDiff_js_1.ScreenshotDiffError) {
|
|
284
|
+
// ── 截图对比超阈值:确定性事实,跳过 Agent Re-Act ──
|
|
285
|
+
// diff 不是探索能修复的(页面就该和基线不一样才报错),探索只会烧 token;
|
|
286
|
+
// 对比结果已入 shotDiffs,attachment 会带上完整差异证据。
|
|
287
|
+
console.log(`[keveGoal] 📊 "${options.step}" screenshot diff exceeded — skipping agent Re-Act`);
|
|
288
|
+
reactResult = {
|
|
289
|
+
expectedMet: false,
|
|
290
|
+
actions: [{
|
|
291
|
+
action: { tool: 'done', verdict: 'fail', text: fnError },
|
|
292
|
+
toolOutput: fnErrorRaw.shotDiff?.message || '',
|
|
293
|
+
result: 'ok',
|
|
294
|
+
}],
|
|
295
|
+
finalSnapshot: '',
|
|
296
|
+
agentScreenshots: [],
|
|
297
|
+
conclusion: 'fail',
|
|
298
|
+
};
|
|
263
299
|
}
|
|
300
|
+
else {
|
|
301
|
+
// ── Per-goal timeout: prevent one slow goal from exhausting the test timeout ──
|
|
302
|
+
// Default 300s per goal; total test timeout should be ≥ (goals × 300s + overhead)
|
|
303
|
+
const GOAL_TIMEOUT_MS = 300_000;
|
|
304
|
+
const goalTimeoutController = new AbortController();
|
|
305
|
+
const goalTimeout = setTimeout(() => {
|
|
306
|
+
goalTimeoutController.abort();
|
|
307
|
+
console.log(`[keveGoal] ⏰ "${options.step}" timed out after ${GOAL_TIMEOUT_MS}ms — stopping agent`);
|
|
308
|
+
}, GOAL_TIMEOUT_MS);
|
|
309
|
+
try {
|
|
310
|
+
reactResult = await (0, agent_js_1.reactLoop)(page, options.step, options.expected, {
|
|
311
|
+
learnedActionsHint: learnedHint,
|
|
312
|
+
specFilePath: ctx.specFilePath,
|
|
313
|
+
fnSource,
|
|
314
|
+
fnResult: fnError ? { error: fnError } : { success: true },
|
|
315
|
+
signal: goalTimeoutController.signal,
|
|
316
|
+
goalScreenshotBefore: goalScreenshotBefore || undefined,
|
|
317
|
+
fnAfterScreenshot: fnAfterScreenshot || undefined,
|
|
318
|
+
targetUrl: process.env.KEVE_TARGET_URL || '',
|
|
319
|
+
getEnv: (name) => process.env[name] || '',
|
|
320
|
+
saveScreenshot: saveAgentScreenshot,
|
|
321
|
+
loadInitialImages: loadInitialImages,
|
|
322
|
+
llm: reactLlmConfig(),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
catch (reactErr) {
|
|
326
|
+
reactTimedOut = true;
|
|
327
|
+
const isGoalTimeout = goalTimeoutController.signal.aborted;
|
|
328
|
+
const msg = isGoalTimeout
|
|
329
|
+
? `Goal timed out (${GOAL_TIMEOUT_MS}ms)`
|
|
330
|
+
: (reactErr?.message || String(reactErr));
|
|
331
|
+
console.log(`[keveGoal] reactLoop interrupted: ${msg.slice(0, 200)}`);
|
|
332
|
+
reactResult = {
|
|
333
|
+
expectedMet: false,
|
|
334
|
+
actions: [],
|
|
335
|
+
finalSnapshot: '',
|
|
336
|
+
agentScreenshots: [],
|
|
337
|
+
conclusion: isGoalTimeout ? 'blocked' : 'fail',
|
|
338
|
+
diagnostics: undefined,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
clearTimeout(goalTimeout);
|
|
343
|
+
}
|
|
344
|
+
} // end of page-alive else (agent Re-Act path)
|
|
264
345
|
} // end of else (non-blocked path)
|
|
265
346
|
// ── Deterministic override: fn assertion failure cannot be overridden by Agent "pass" ──
|
|
266
347
|
// When fn has a hard assertion failure (Expected X, Received Y), the value mismatch
|
|
@@ -268,7 +349,7 @@ exports.test = test_1.test.extend({
|
|
|
268
349
|
// show 15px, so consistent") and override to pass, but 15px ≠ 16px is a definitive failure.
|
|
269
350
|
// This is the per-goal equivalent of pipeline afterHook (case-gen-validate) —
|
|
270
351
|
// deterministic check > LLM judgment.
|
|
271
|
-
if (fnError && !fnBlocked && reactResult.expectedMet && isAssertionFailure(fnError)) {
|
|
352
|
+
if (fnError && !fnBlocked && reactResult.expectedMet && (0, goal_heuristics_js_1.isAssertionFailure)(fnError)) {
|
|
272
353
|
console.log(`[keveGoal] ⚠️ "${options.step}" Agent said PASS but fn had assertion failure — overriding to FAIL`);
|
|
273
354
|
console.log(`[keveGoal] fn assertion: ${fnError}`);
|
|
274
355
|
reactResult.expectedMet = false;
|
|
@@ -282,13 +363,19 @@ exports.test = test_1.test.extend({
|
|
|
282
363
|
? undefined
|
|
283
364
|
: reactResult.conclusion === 'blocked'
|
|
284
365
|
? new Error(`Blocked: ${fnBlocked ? fnError : (reactTimedOut ? `Agent 超时 (300000ms)` : (reactResult.actions?.filter((a) => a.action?.tool === 'done').pop()?.action?.text || 'Agent blocked'))}`)
|
|
285
|
-
: (
|
|
286
|
-
? new Error(
|
|
287
|
-
:
|
|
366
|
+
: (fnErrorRaw instanceof screenshotDiff_js_1.ScreenshotDiffError)
|
|
367
|
+
? new Error(fnError) // 截图对比:保留带差异率/区块数/基线版本的结构化错误文案
|
|
368
|
+
: (fnError && (0, goal_heuristics_js_1.isAssertionFailure)(fnError) && !fnBlocked)
|
|
369
|
+
? new Error(`Assertion failed: ${fnError}`)
|
|
370
|
+
: new Error(`Expected not achieved: ${options.expected}`),
|
|
288
371
|
};
|
|
289
372
|
// Attach Agent conclusion for downstream consumers
|
|
290
373
|
if (reactResult.conclusion)
|
|
291
374
|
result.conclusion = reactResult.conclusion;
|
|
375
|
+
// Attach runtime diagnostics for downstream consumers (Playwright side must
|
|
376
|
+
// copy them here because the attachment reads from `result`).
|
|
377
|
+
if (reactResult.diagnostics)
|
|
378
|
+
result.diagnostics = reactResult.diagnostics;
|
|
292
379
|
console.log(reactResult.expectedMet
|
|
293
380
|
? `[keveGoal] ✅ "${options.step}" PASSED`
|
|
294
381
|
: `[keveGoal] ❌ "${options.step}" FAILED (${reactResult.conclusion || 'fail'}) — ${result.error?.message || 'expected not achieved'}`);
|
|
@@ -384,21 +471,31 @@ exports.test = test_1.test.extend({
|
|
|
384
471
|
}),
|
|
385
472
|
finalSnapshot: result.finalSnapshot ? String(result.finalSnapshot).slice(0, 500) : undefined,
|
|
386
473
|
diagnosticHints,
|
|
474
|
+
diagnostics: result.diagnostics,
|
|
387
475
|
goalScreenshotBefore: goalScreenshotBefore || undefined,
|
|
388
476
|
goalScreenshotAfter: goalScreenshotAfter || undefined,
|
|
389
|
-
|
|
477
|
+
// agent 未单独留图时,用 fn-after 兜底,保证报告至少有一张执行后截图。
|
|
478
|
+
// 不能写 `|| [...]`:空数组是 truthy,会把兜底截图短路丢弃。
|
|
479
|
+
agentScreenshots: result.agentScreenshots?.length
|
|
480
|
+
? result.agentScreenshots
|
|
481
|
+
: (fnAfterScreenshot ? [fnAfterScreenshot] : []),
|
|
482
|
+
// 截图对比证据(matched/created/exceeded 全量保留;平台 stepsDetail 回收为 evidence.screenshotDiff)
|
|
483
|
+
screenshotDiff: shotDiffs.length ? shotDiffs : undefined,
|
|
390
484
|
// refinePatch: (result as any).refinePatch || undefined, // 已注释:scriptRefine 已禁用
|
|
391
485
|
conclusion: result.conclusion || undefined,
|
|
392
486
|
}), 'utf-8'),
|
|
393
487
|
});
|
|
394
488
|
// Run after aspects
|
|
395
|
-
await
|
|
489
|
+
await keve_aspect_js_1.keveAspect.runPhase('after', ctx, result, result.error);
|
|
396
490
|
// Throw if not successful
|
|
397
491
|
if (!result.success) {
|
|
398
492
|
const failReason = result.error?.message || 'expected state not achieved';
|
|
399
493
|
console.log(`[keveGoal] ❌ "${options.step}" FAILED — reason: ${failReason}`);
|
|
400
494
|
throw result.error || new Error(`keveGoal "${options.step}" failed: ${failReason}`);
|
|
401
495
|
}
|
|
496
|
+
// 透传 fn 返回值:flow 辅助函数(如 createDashboard 返回看板名)可包进 keveGoal,
|
|
497
|
+
// 脚本失败时由 Agent Re-Act 自愈,成功时把产物继续传给后续步骤
|
|
498
|
+
return fnResult;
|
|
402
499
|
};
|
|
403
500
|
await use(keveGoalFn);
|
|
404
501
|
// keveGoals attachment 已删除:step/expected/precondition/order 已合并进
|
|
@@ -407,124 +504,77 @@ exports.test = test_1.test.extend({
|
|
|
407
504
|
}
|
|
408
505
|
});
|
|
409
506
|
// Register extended test with keve-decorators so @keveModel uses the correct test
|
|
410
|
-
const
|
|
411
|
-
(0,
|
|
507
|
+
const keve_decorators_js_2 = require("../core/decorator/keve-decorators.js");
|
|
508
|
+
(0, keve_decorators_js_2.setKeveTest)(exports.test);
|
|
412
509
|
// ── 辅助函数 ──
|
|
413
510
|
/**
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
* When fn (deterministic script) fails with these patterns, Agent Re-Act cannot
|
|
417
|
-
* fix the issue — the error is environmental, not application-level:
|
|
418
|
-
*
|
|
419
|
-
* 1. Empty/undefined URL → page.goto(''), page.goto(undefined)
|
|
420
|
-
* 2. Connection refused / net::ERR_CONNECTION_REFUSED
|
|
421
|
-
* 3. Navigation timeout (page not reachable)
|
|
422
|
-
* 4. Invalid URL format
|
|
423
|
-
* 5. SSO redirect / auth failure (not the app's fault)
|
|
424
|
-
*
|
|
425
|
-
* Returns true → keveGoal skips Agent Re-Act and directly reports BLOCKED
|
|
426
|
-
* with the fn error as the blocking reason.
|
|
511
|
+
* 截图并保存到当前轮次的 reports/round-N,返回相对 taskDir 的路径
|
|
512
|
+
* @param phase 'before' | 'after-fn' — 用于文件名前缀
|
|
427
513
|
*/
|
|
428
|
-
function
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|| msg.includes('invalid url') || msg.includes('url is undefined')
|
|
433
|
-
|| msg.includes('navigation to ""') || msg.includes("navigation to ''")) {
|
|
434
|
-
return true;
|
|
435
|
-
}
|
|
436
|
-
// page.goto(undefined): Playwright type error "url: expected string, got undefined"
|
|
437
|
-
// This happens when process.env.PAGE_* is not set — environment config issue, not app issue
|
|
438
|
-
if ((msg.includes('expected string') || msg.includes('expected a string'))
|
|
439
|
-
&& (msg.includes('got undefined') || msg.includes('got null') || msg.includes('received undefined'))) {
|
|
440
|
-
return true;
|
|
441
|
-
}
|
|
442
|
-
// Connection refused / network unreachable
|
|
443
|
-
if (msg.includes('err_connection_refused') || msg.includes('connection refused')
|
|
444
|
-
|| msg.includes('net::err_connection') || msg.includes('err_name_not_resolved')
|
|
445
|
-
|| msg.includes('err_address_unreachable') || msg.includes('err_internet_disconnected')
|
|
446
|
-
|| msg.includes('err_connection_timed_out') || msg.includes('err_connection_reset')) {
|
|
447
|
-
return true;
|
|
448
|
-
}
|
|
449
|
-
// Navigation timeout (page not reachable in time)
|
|
450
|
-
if (msg.includes('navigation timeout of') || msg.includes('timeout of') && msg.includes('exceeded')
|
|
451
|
-
|| msg.includes('page.goto: timeout') || msg.includes('navigating to') && msg.includes('timed out')) {
|
|
452
|
-
return true;
|
|
453
|
-
}
|
|
454
|
-
// Playwright locator/waitForSelector timeout — selector not found on page (fn script issue, not app crash)
|
|
455
|
-
// e.g. "Timeout 15000ms exceeded while waiting for locator('.selector') to be visible"
|
|
456
|
-
// This is a script-quality issue (wrong selector), not an env blocked error.
|
|
457
|
-
// Return false here — let Agent take over rather than treating as BLOCKED.
|
|
458
|
-
// Note: we intentionally do NOT return true; these should fall through to Agent Re-Act.
|
|
459
|
-
// IDC network segment restriction
|
|
460
|
-
if (msg.includes('unable to access on the idc network segment')
|
|
461
|
-
|| msg.includes('idc network segment') || msg.includes('40314')) {
|
|
462
|
-
return true;
|
|
463
|
-
}
|
|
464
|
-
// SSO / auth redirect (not the app's fault)
|
|
465
|
-
if (msg.includes('sso redirect') || msg.includes('login redirect detected')
|
|
466
|
-
|| msg.includes('err_too_many_redirects')) {
|
|
467
|
-
return true;
|
|
468
|
-
}
|
|
469
|
-
return false;
|
|
514
|
+
async function captureScreenshot(page, stepName, order, phase) {
|
|
515
|
+
const buf = await page.screenshot({ type: 'png', timeout: 15000 });
|
|
516
|
+
const safeName = stepName.replace(/[^a-zA-Z0-9一-鿿]/g, '_').slice(0, 30);
|
|
517
|
+
return writeScreenshot(buf, `goal-${phase}-${order}-${safeName}`);
|
|
470
518
|
}
|
|
471
519
|
/**
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
* Expected vs Received values, the mismatch is a *fact*, not a judgment.
|
|
476
|
-
* The Agent may rationalize "consistency" (e.g. "both pages show 15px → consistent → pass")
|
|
477
|
-
* and override to pass, but 15px ≠ 16px is a definitive failure.
|
|
478
|
-
*
|
|
479
|
-
* This function identifies such assertion failures so keveGoal can:
|
|
480
|
-
* - Override Agent "pass" → "fail" when the fn proved a value mismatch
|
|
481
|
-
* - Use the precise assertion message in the error (instead of generic "expected not achieved")
|
|
482
|
-
*
|
|
483
|
-
* Returns true for assertion errors, false for environment/runtime errors.
|
|
520
|
+
* 当前轮次截图目录绝对路径(与 Cypress 桥同名同层)。
|
|
521
|
+
* 新布局为 <taskRoot>/reports/round-N/screenshots;缺失时回退旧布局,
|
|
522
|
+
* 兼容历史执行环境。
|
|
484
523
|
*/
|
|
485
|
-
function
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
// e.g. "page.goto: url: expected string, got undefined" — this is an env config error,
|
|
489
|
-
// not a functional assertion failure. isEnvironmentBlockedError() handles it instead.
|
|
490
|
-
if (msg.includes('expected string') || msg.includes('expected a string')
|
|
491
|
-
|| msg.includes('expected number') || msg.includes('expected boolean')) {
|
|
492
|
-
// Only exclude if it looks like an API parameter error (not an assertion)
|
|
493
|
-
if (msg.includes('got undefined') || msg.includes('got null')
|
|
494
|
-
|| msg.includes('received undefined') || msg.includes('received null')) {
|
|
495
|
-
return false;
|
|
496
|
-
}
|
|
524
|
+
function screenshotsDir() {
|
|
525
|
+
if (process.env.KEVE_RESULT_DIR) {
|
|
526
|
+
return path.join(path.resolve(process.env.KEVE_RESULT_DIR), 'screenshots');
|
|
497
527
|
}
|
|
498
|
-
// Jest/Vitest/playwright assertion: "Expected: X, Received: Y"
|
|
499
|
-
if (msg.includes('expected') && msg.includes('received'))
|
|
500
|
-
return true;
|
|
501
|
-
// AssertionError / assert keyword
|
|
502
|
-
if (msg.includes('assertionerror') || msg.includes('assertion failed')
|
|
503
|
-
|| msg.includes('assertion error'))
|
|
504
|
-
return true;
|
|
505
|
-
// expect(x).toBe(y) / expect(x).toEqual(y) pattern
|
|
506
|
-
// Note: must NOT match Playwright timeout messages like "Timeout 15000ms exceeded waiting for locator(...) to be visible"
|
|
507
|
-
// Those are env/timeout errors, not value assertion failures.
|
|
508
|
-
if ((msg.includes(' to be ') && !msg.includes('timeout') && !msg.includes('waiting for') && !msg.includes('exceeded'))
|
|
509
|
-
|| msg.includes(' to equal ') || msg.includes(' to deeply equal '))
|
|
510
|
-
return true;
|
|
511
|
-
// Playwright expect: "expected string, received number" etc.
|
|
512
|
-
if (msg.includes('expected ') && (msg.includes(' but ') || msg.includes(' got ') || msg.includes(' received ')))
|
|
513
|
-
return true;
|
|
514
|
-
return false;
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* 截图并保存到 test-artifacts,返回相对 taskDir 的路径
|
|
518
|
-
* @param phase 'before' | 'after-fn' — 用于文件名前缀
|
|
519
|
-
*/
|
|
520
|
-
async function captureScreenshot(page, stepName, order, phase) {
|
|
521
|
-
const buf = await page.screenshot({ type: 'png', timeout: 15000 });
|
|
522
528
|
const taskDir = process.env.KEVE_TASK_DIR || '.keve';
|
|
523
529
|
const round = process.env.KEVE_ROUND || 'latest';
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
530
|
+
return path.join(taskDir, 'test-artifacts', `round-${round}`, 'screenshots');
|
|
531
|
+
}
|
|
532
|
+
/** 落盘 PNG,返回相对 taskDir 的路径 */
|
|
533
|
+
function writeScreenshot(png, baseName) {
|
|
534
|
+
const taskDir = process.env.KEVE_TASK_DIR || '.keve';
|
|
535
|
+
const dir = screenshotsDir();
|
|
536
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
537
|
+
const file = path.join(dir, `${baseName}-${Date.now()}.png`);
|
|
538
|
+
fs.writeFileSync(file, png);
|
|
529
539
|
return path.relative(taskDir, file);
|
|
530
540
|
}
|
|
541
|
+
/** Agent 单步截图落盘(宿主注入给 reactLoop,与 Cypress 桥的命名同形) */
|
|
542
|
+
async function saveAgentScreenshot(pngBase64, stepIndex, stepName) {
|
|
543
|
+
const safeName = stepName.replace(/[^a-zA-Z0-9一-鿿]/g, '_').slice(0, 30);
|
|
544
|
+
return writeScreenshot(Buffer.from(pngBase64, 'base64'), `agent-step${stepIndex}-${safeName}`);
|
|
545
|
+
}
|
|
546
|
+
/** 读取 goal-before / fn-after 图片,转成多模态 ContentItem(唯一注意:路径是相对 taskDir) */
|
|
547
|
+
async function loadInitialImages(paths) {
|
|
548
|
+
const taskDir = process.env.KEVE_TASK_DIR || '.keve';
|
|
549
|
+
const items = [];
|
|
550
|
+
for (const [label, rel] of [
|
|
551
|
+
['goal-before', paths.goalScreenshotBefore],
|
|
552
|
+
['fn-after', paths.fnAfterScreenshot],
|
|
553
|
+
]) {
|
|
554
|
+
if (!rel)
|
|
555
|
+
continue;
|
|
556
|
+
try {
|
|
557
|
+
const file = path.isAbsolute(rel) ? rel : path.join(taskDir, rel);
|
|
558
|
+
if (!fs.existsSync(file))
|
|
559
|
+
continue;
|
|
560
|
+
const b64 = fs.readFileSync(file).toString('base64');
|
|
561
|
+
items.push({ type: 'text', text: `Screenshot (${label}):` });
|
|
562
|
+
items.push({ type: 'image_url', image_url: { url: `data:image/png;base64,${b64}` } });
|
|
563
|
+
}
|
|
564
|
+
catch { /* 单张图读失败不影响探索 */ }
|
|
565
|
+
}
|
|
566
|
+
return items;
|
|
567
|
+
}
|
|
568
|
+
/** Re-Act 的 LLM 配置:优先 keve.yaml,其次进程环境变量(与 config.ts DEFAULT_CONFIG 同源) */
|
|
569
|
+
function reactLlmConfig() {
|
|
570
|
+
let cfg = { base_url: '', model: '', api_key: '' };
|
|
571
|
+
try {
|
|
572
|
+
cfg = (0, config_js_1.loadConfig)().llm;
|
|
573
|
+
}
|
|
574
|
+
catch { /* 配置缺失时退回环境变量 */ }
|
|
575
|
+
return {
|
|
576
|
+
baseURL: cfg.base_url || process.env.KEVE_LLM_BASE_URL || '',
|
|
577
|
+
model: cfg.model || process.env.KEVE_LLM_MODEL_NAME || process.env.KEVE_LLM_MODEL || '',
|
|
578
|
+
apiKey: cfg.api_key || process.env.KEVE_LLM_API_KEY || '',
|
|
579
|
+
};
|
|
580
|
+
}
|