kc-beta 0.2.1 → 0.3.0

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.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/src/agent/context.js +8 -4
  3. package/src/agent/engine.js +65 -9
  4. package/src/agent/pipelines/initializer.js +53 -8
  5. package/src/agent/session-state.js +1 -0
  6. package/src/agent/skill-loader.js +13 -1
  7. package/src/agent/tools/document-parse.js +104 -21
  8. package/src/agent/tools/document-search.js +24 -8
  9. package/src/agent/tools/sandbox-exec.js +16 -5
  10. package/src/agent/tools/workspace-file.js +47 -20
  11. package/src/agent/workspace.js +24 -1
  12. package/src/cli/components.js +8 -1
  13. package/src/cli/config.js +100 -6
  14. package/src/cli/index.js +14 -1
  15. package/src/cli/onboard.js +70 -1
  16. package/src/config.js +43 -3
  17. package/src/model-tiers.json +153 -0
  18. package/src/providers.js +63 -66
  19. package/template/AGENT.md +20 -0
  20. package/template/skills/en/meta/compliance-judgment/SKILL.md +10 -42
  21. package/template/skills/en/meta/document-chunking/SKILL.md +32 -0
  22. package/template/skills/en/meta/document-parsing/SKILL.md +11 -18
  23. package/template/skills/en/meta/entity-extraction/SKILL.md +13 -28
  24. package/template/skills/en/meta/tree-processing/SKILL.md +19 -1
  25. package/template/skills/en/meta-meta/auto-model-selection/SKILL.md +53 -0
  26. package/template/skills/en/meta-meta/pdf-review-dashboard/SKILL.md +57 -0
  27. package/template/skills/en/meta-meta/pdf-review-dashboard/scripts/generate_review.js +262 -0
  28. package/template/skills/en/meta-meta/rule-extraction/SKILL.md +24 -1
  29. package/template/skills/en/meta-meta/skill-authoring/SKILL.md +6 -0
  30. package/template/skills/en/meta-meta/skill-to-workflow/SKILL.md +4 -0
  31. package/template/skills/zh/meta/compliance-judgment/SKILL.md +41 -262
  32. package/template/skills/zh/meta/document-chunking/SKILL.md +32 -0
  33. package/template/skills/zh/meta/document-parsing/SKILL.md +65 -132
  34. package/template/skills/zh/meta/entity-extraction/SKILL.md +68 -230
  35. package/template/skills/zh/meta/tree-processing/SKILL.md +82 -194
  36. package/template/skills/zh/meta-meta/auto-model-selection/SKILL.md +51 -0
  37. package/template/skills/zh/meta-meta/pdf-review-dashboard/SKILL.md +55 -0
  38. package/template/skills/zh/meta-meta/pdf-review-dashboard/scripts/generate_review.js +262 -0
  39. package/template/skills/zh/meta-meta/rule-extraction/SKILL.md +79 -164
  40. package/template/skills/zh/meta-meta/skill-authoring/SKILL.md +64 -185
  41. package/template/skills/zh/meta-meta/skill-to-workflow/SKILL.md +95 -216
@@ -5,9 +5,9 @@ import { BaseTool, ToolResult } from "./base.js";
5
5
  const MAX_READ = 50_000;
6
6
 
7
7
  /**
8
- * Read, write, or list files in the workspace directory.
9
- * All paths are resolved relative to the workspace root with
10
- * traversal protection. VersionManager hooks into writes for automatic versioning.
8
+ * Read, write, or list files in the workspace or project directory.
9
+ * All paths are resolved relative to the chosen scope with
10
+ * traversal protection. VersionManager hooks into workspace writes for automatic versioning.
11
11
  */
12
12
  export class WorkspaceFileTool extends BaseTool {
13
13
  /**
@@ -24,9 +24,10 @@ export class WorkspaceFileTool extends BaseTool {
24
24
 
25
25
  get description() {
26
26
  return (
27
- "Read, write, or list files in the workspace directory. " +
28
- "Operations: read (returns file content), write (creates/overwrites a file), " +
29
- "list (shows directory contents)."
27
+ "Read, write, or list files. " +
28
+ "scope='workspace' (default): KC's working directory for rules, skills, workflows, results. " +
29
+ "scope='project': the user's project folder where KC was launched — source regulations and samples live here. " +
30
+ "Operations: read (returns file content), write (creates/overwrites a file), list (shows directory contents)."
30
31
  );
31
32
  }
32
33
 
@@ -41,34 +42,58 @@ export class WorkspaceFileTool extends BaseTool {
41
42
  },
42
43
  path: {
43
44
  type: "string",
44
- description: "Relative path within the workspace. Defaults to '.' for list.",
45
+ description: "Relative path within the chosen scope. Defaults to '.' for list.",
45
46
  },
46
47
  content: {
47
48
  type: "string",
48
49
  description: "File content to write (required for write operation)",
49
50
  },
51
+ scope: {
52
+ type: "string",
53
+ enum: ["workspace", "project"],
54
+ description: "Which directory to operate in. 'workspace' (default) = KC's workspace. 'project' = user's project directory.",
55
+ },
50
56
  },
51
57
  required: ["operation"],
52
58
  };
53
59
  }
54
60
 
61
+ _resolveForScope(filePath, scope) {
62
+ if (scope === "project") {
63
+ return this._workspace.resolveProjectPath(filePath);
64
+ }
65
+ return this._workspace.resolvePath(filePath);
66
+ }
67
+
68
+ _baseForScope(scope) {
69
+ if (scope === "project") {
70
+ return this._workspace.projectDir;
71
+ }
72
+ return this._workspace.cwd;
73
+ }
74
+
55
75
  async execute(input) {
56
76
  const op = input.operation || "";
57
77
  const filePath = input.path || ".";
58
78
  const content = input.content || "";
79
+ const scope = input.scope || "workspace";
80
+
81
+ if (scope === "project" && !this._workspace.projectDir) {
82
+ return new ToolResult("No project directory available. KC was launched without a project context.", true);
83
+ }
59
84
 
60
85
  try {
61
- if (op === "read") return this._read(filePath);
62
- if (op === "write") return this._write(filePath, content);
63
- if (op === "list") return this._list(filePath);
86
+ if (op === "read") return this._read(filePath, scope);
87
+ if (op === "write") return this._write(filePath, content, scope);
88
+ if (op === "list") return this._list(filePath, scope);
64
89
  return new ToolResult(`Unknown operation: ${op}`, true);
65
90
  } catch (err) {
66
91
  return new ToolResult(`File error: ${err.message}`, true);
67
92
  }
68
93
  }
69
94
 
70
- _read(filePath) {
71
- const resolved = this._workspace.resolvePath(filePath);
95
+ _read(filePath, scope) {
96
+ const resolved = this._resolveForScope(filePath, scope);
72
97
  if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) {
73
98
  return new ToolResult(`File not found: ${filePath}`, true);
74
99
  }
@@ -79,27 +104,28 @@ export class WorkspaceFileTool extends BaseTool {
79
104
  return new ToolResult(text);
80
105
  }
81
106
 
82
- _write(filePath, content) {
107
+ _write(filePath, content, scope) {
83
108
  if (!filePath || filePath === ".") {
84
109
  return new ToolResult("Path required for write operation", true);
85
110
  }
86
- const resolved = this._workspace.resolvePath(filePath);
111
+ const resolved = this._resolveForScope(filePath, scope);
87
112
  fs.mkdirSync(path.dirname(resolved), { recursive: true });
88
113
  fs.writeFileSync(resolved, content, "utf-8");
89
114
 
90
- // Version tracking (structural cannot be bypassed)
115
+ // Version tracking only for workspace writes
91
116
  let traceId = null;
92
- if (this._versionManager) {
117
+ if (scope === "workspace" && this._versionManager) {
93
118
  traceId = this._versionManager.onWrite(filePath, content);
94
119
  }
95
120
 
96
- let msg = `Wrote ${content.length} chars to ${filePath}`;
121
+ const label = scope === "project" ? `[project] ${filePath}` : filePath;
122
+ let msg = `Wrote ${content.length} chars to ${label}`;
97
123
  if (traceId) msg += ` [trace: ${traceId}]`;
98
124
  return new ToolResult(msg);
99
125
  }
100
126
 
101
- _list(filePath) {
102
- const resolved = this._workspace.resolvePath(filePath);
127
+ _list(filePath, scope) {
128
+ const resolved = this._resolveForScope(filePath, scope);
103
129
  if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
104
130
  return new ToolResult(`Not a directory: ${filePath}`, true);
105
131
  }
@@ -112,8 +138,9 @@ export class WorkspaceFileTool extends BaseTool {
112
138
  if (entries.length === 0) {
113
139
  return new ToolResult("(empty directory)");
114
140
  }
141
+ const base = this._baseForScope(scope);
115
142
  const lines = entries.map((e) => {
116
- const rel = path.relative(this._workspace.cwd, path.join(resolved, e.name));
143
+ const rel = path.relative(base, path.join(resolved, e.name));
117
144
  const marker = e.isDirectory() ? "[dir] " : " ";
118
145
  return `${marker}${rel}`;
119
146
  });
@@ -11,11 +11,13 @@ export class Workspace {
11
11
  /**
12
12
  * @param {string} root - Workspace root directory
13
13
  * @param {string} [sessionId] - Session identifier (auto-generated if omitted)
14
+ * @param {string} [projectDir] - User's project directory (CWD at launch)
14
15
  */
15
- constructor(root, sessionId) {
16
+ constructor(root, sessionId, projectDir) {
16
17
  this.root = path.resolve(root);
17
18
  this.sessionId = sessionId || crypto.randomUUID().replace(/-/g, "").slice(0, 12);
18
19
  this.path = path.resolve(this.root, this.sessionId);
20
+ this.projectDir = projectDir ? path.resolve(projectDir) : null;
19
21
  fs.mkdirSync(this.path, { recursive: true });
20
22
  }
21
23
 
@@ -42,6 +44,27 @@ export class Workspace {
42
44
  return resolved;
43
45
  }
44
46
 
47
+ /**
48
+ * Resolve a user-supplied relative path against the project directory.
49
+ * Same traversal protection as resolvePath() but for the project folder.
50
+ * @param {string} userPath
51
+ * @returns {string}
52
+ */
53
+ resolveProjectPath(userPath) {
54
+ if (!this.projectDir) {
55
+ throw new Error("No project directory available");
56
+ }
57
+ if (path.isAbsolute(userPath)) {
58
+ throw new Error(`Absolute paths not allowed: ${userPath}`);
59
+ }
60
+ const resolved = path.resolve(this.projectDir, userPath);
61
+ const base = path.resolve(this.projectDir);
62
+ if (resolved !== base && !resolved.startsWith(base + path.sep)) {
63
+ throw new Error(`Path escapes project directory: ${userPath}`);
64
+ }
65
+ return resolved;
66
+ }
67
+
45
68
  /**
46
69
  * Rename the workspace folder. Returns the new sessionId.
47
70
  * @param {string} newName
@@ -54,7 +54,7 @@ export function StatusBar({ sessionId, phase, contextTokens, contextLimit }) {
54
54
 
55
55
  // --- Welcome banner ---
56
56
 
57
- export function WelcomeBanner() {
57
+ export function WelcomeBanner({ projectDir } = {}) {
58
58
  return h(Box, { flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: "gray", paddingLeft: 1, paddingRight: 1 },
59
59
  h(Box, null,
60
60
  h(Text, { bold: true }, "KC AGENT CLI"),
@@ -62,6 +62,13 @@ export function WelcomeBanner() {
62
62
  ),
63
63
  h(Text, { dimColor: true }, "Hope you never know what KC was."),
64
64
  h(Text, null, ""),
65
+ projectDir
66
+ ? h(Box, { flexDirection: "column" },
67
+ h(Text, { dimColor: true }, `Project: ${projectDir}`),
68
+ h(Text, { color: "yellow", dimColor: true }, "KC has full read/write access to this directory. We recommend backing up important files."),
69
+ )
70
+ : null,
71
+ h(Text, null, ""),
65
72
  h(Text, { dimColor: true }, "Product of Memium / kitchen-engineer42"),
66
73
  );
67
74
  }
package/src/cli/config.js CHANGED
@@ -23,7 +23,7 @@ const L = {
23
23
  noConfig: "No config found. Run 'kc-beta onboard' first.",
24
24
  menu: "Configuration Categories",
25
25
  choose: "Choose category (q to quit)",
26
- categories: ["LLM Provider & API Key", "Model Tiers", "Quality Thresholds", "Language"],
26
+ categories: ["LLM Provider & API Key", "Model Tiers", "VLM Tiers (Vision/OCR)", "Worker LLM Provider", "Quality Thresholds", "Language"],
27
27
  saved: "Saved.",
28
28
  back: "← Back to menu",
29
29
  enterKeep: "Press Enter to keep",
@@ -41,7 +41,7 @@ const L = {
41
41
  noConfig: "未找到配置。请先运行 'kc-beta onboard'。",
42
42
  menu: "配置类别",
43
43
  choose: "选择类别(q 退出)",
44
- categories: ["大模型服务商 & API 密钥", "模型分层", "质量阈值", "语言"],
44
+ categories: ["大模型服务商 & API 密钥", "模型分层", "VLM 视觉模型分层", "Worker LLM 服务商", "质量阈值", "语言"],
45
45
  saved: "已保存。",
46
46
  back: "← 返回菜单",
47
47
  enterKeep: "回车保留当前值",
@@ -118,11 +118,26 @@ async function editProvider(rl, config, t) {
118
118
  // Base URL
119
119
  if (provider.id === "custom") {
120
120
  config.base_url = await ask(rl, ` ${CYAN}${t.baseUrl}${RESET}`, config.base_url || "");
121
+ } else if (provider.supportsCodingPlanKey) {
122
+ // Providers with coding plan support — ask which key type
123
+ const keyTypeLabel = config.language === "zh" ? "密钥类型" : "Key Type";
124
+ const opt1 = config.language === "zh" ? "API Key(按量付费)" : "API Key (pay-per-use)";
125
+ const opt2 = config.language === "zh" ? "Coding Plan Key(包年包月)" : "Coding Plan Key (subscription)";
126
+ console.log(` ${CYAN}${keyTypeLabel}:${RESET}`);
127
+ const isCodingPlan = config.base_url === provider.codingPlanUrl;
128
+ console.log(` 1. ${opt1}${!isCodingPlan ? ` ${GREEN}(${t.currentValue})${RESET}` : ""}`);
129
+ console.log(` 2. ${opt2}${isCodingPlan ? ` ${GREEN}(${t.currentValue})${RESET}` : ""}`);
130
+ const keyTypeDefault = isCodingPlan ? "2" : "1";
131
+ const keyTypeChoice = await ask(rl, ` ${GRAY}>${RESET}`, keyTypeDefault);
132
+ config.base_url = keyTypeChoice === "2" ? provider.codingPlanUrl : provider.baseUrl;
133
+ console.log(` ${CYAN}${t.baseUrl}${RESET}: ${DIM}${config.base_url}${RESET}`);
121
134
  } else {
135
+ // Keep existing base_url if it matches this provider, otherwise use default
122
136
  const defaultUrl = provider.baseUrl;
123
137
  console.log(` ${CYAN}${t.baseUrl}${RESET}: ${DIM}${defaultUrl}${RESET}`);
124
138
  config.base_url = defaultUrl;
125
139
  }
140
+ console.log();
126
141
 
127
142
  // API Key
128
143
  const masked = maskKey(config.api_key);
@@ -161,14 +176,93 @@ async function editTiers(rl, config, t) {
161
176
  }
162
177
 
163
178
  /**
164
- * Category 3: Quality Thresholds
179
+ * Category 3: VLM Tiers (Vision/OCR)
165
180
  */
166
- async function editThresholds(rl, config, t) {
181
+ async function editVlmTiers(rl, config, t) {
167
182
  console.log();
168
183
  console.log(` ${BOLD}${t.categories[2]}${RESET}`);
169
184
  console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
170
185
  console.log();
171
186
 
187
+ const vlmTiers = config.vlm_tiers || {};
188
+ const provider = getProviderById(config.provider);
189
+ const defaults = provider?.defaultVlm || {};
190
+
191
+ for (const tier of ["tier1", "tier2", "tier3"]) {
192
+ const current = vlmTiers[tier] || defaults[tier] || "";
193
+ vlmTiers[tier] = await ask(rl, ` ${CYAN}${tier.toUpperCase()}${RESET}`, current, t.enterKeep);
194
+ }
195
+ config.vlm_tiers = vlmTiers;
196
+ console.log();
197
+ }
198
+
199
+ /**
200
+ * Category 4: Worker LLM Provider
201
+ */
202
+ async function editWorkerProvider(rl, config, t) {
203
+ console.log();
204
+ console.log(` ${BOLD}${t.categories[3]}${RESET}`);
205
+ console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
206
+ console.log();
207
+
208
+ const currentWorker = config.worker_provider || "";
209
+ const statusLabel = currentWorker
210
+ ? `${currentWorker} (${getProviderById(currentWorker)?.name || "unknown"})`
211
+ : config.language === "zh" ? "(与主服务商相同)" : "(same as conductor)";
212
+ console.log(` ${DIM}${t.currentValue}: ${statusLabel}${RESET}`);
213
+ console.log();
214
+
215
+ const sameLabel = config.language === "zh" ? "使用与主服务商相同配置?" : "Use same provider as conductor?";
216
+ const sameChoice = await ask(rl, ` ${sameLabel}`, "Y", "Y/n");
217
+
218
+ if (sameChoice.toLowerCase() === "n" || sameChoice.toLowerCase() === "no") {
219
+ const providers = getProviders();
220
+ const labels = getProviderLabels(config.language || "en");
221
+ console.log();
222
+ console.log(` ${CYAN}${t.categories[3]}:${RESET}`);
223
+ for (let i = 0; i < labels.length; i++) {
224
+ const marker = providers[i].id === config.worker_provider ? ` ${GREEN}(${t.currentValue})${RESET}` : "";
225
+ console.log(` ${i + 1}. ${labels[i].label}${marker}`);
226
+ }
227
+ const wIdx = parseInt(await ask(rl, ` ${GRAY}>${RESET}`, "1"), 10) - 1;
228
+ const wp = providers[Math.max(0, Math.min(wIdx, providers.length - 1))];
229
+ config.worker_provider = wp.id;
230
+ config.worker_auth_type = wp.authType;
231
+ config.worker_api_format = wp.apiFormat;
232
+
233
+ if (wp.id === "custom") {
234
+ config.worker_base_url = await ask(rl, ` ${CYAN}Base URL${RESET}`, config.worker_base_url || "");
235
+ } else {
236
+ config.worker_base_url = wp.baseUrl;
237
+ }
238
+
239
+ // Worker API Key
240
+ const masked = maskKey(config.worker_api_key);
241
+ const keyPrompt = masked
242
+ ? ` ${CYAN}API Key (Worker)${RESET} ${DIM}(${masked})${RESET}`
243
+ : ` ${CYAN}API Key (Worker)${RESET}`;
244
+ const newKey = await ask(rl, keyPrompt, "", masked ? t.enterKeep : "");
245
+ if (newKey) config.worker_api_key = newKey;
246
+ } else {
247
+ // Clear worker-specific config (use conductor)
248
+ config.worker_provider = "";
249
+ config.worker_api_key = "";
250
+ config.worker_base_url = "";
251
+ config.worker_auth_type = "";
252
+ config.worker_api_format = "";
253
+ }
254
+ console.log();
255
+ }
256
+
257
+ /**
258
+ * Category 5: Quality Thresholds
259
+ */
260
+ async function editThresholds(rl, config, t) {
261
+ console.log();
262
+ console.log(` ${BOLD}${t.categories[4]}${RESET}`);
263
+ console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
264
+ console.log();
265
+
172
266
  config.accuracy_threshold = parseFloat(
173
267
  await ask(rl, ` ${CYAN}Accuracy threshold${RESET}`, String(config.accuracy_threshold ?? 0.9), t.enterKeep)
174
268
  );
@@ -189,7 +283,7 @@ async function editThresholds(rl, config, t) {
189
283
  */
190
284
  async function editLanguage(rl, config, t) {
191
285
  console.log();
192
- console.log(` ${BOLD}${t.categories[3]}${RESET}`);
286
+ console.log(` ${BOLD}${t.categories[5]}${RESET}`);
193
287
  console.log(` ${GRAY}${"─".repeat(35)}${RESET}`);
194
288
  console.log();
195
289
 
@@ -202,7 +296,7 @@ async function editLanguage(rl, config, t) {
202
296
  console.log();
203
297
  }
204
298
 
205
- const CATEGORY_HANDLERS = [editProvider, editTiers, editThresholds, editLanguage];
299
+ const CATEGORY_HANDLERS = [editProvider, editTiers, editVlmTiers, editWorkerProvider, editThresholds, editLanguage];
206
300
 
207
301
  /**
208
302
  * Main config editor loop.
package/src/cli/index.js CHANGED
@@ -163,6 +163,7 @@ function App({ engine, config }) {
163
163
  `Model: ${config.kcModel}\n` +
164
164
  `Provider: ${config.provider || "unknown"}\n` +
165
165
  `LLM URL: ${config.llmBaseUrl}\n` +
166
+ `Project: ${engineRef.current.workspace.projectDir || "(none)"}\n` +
166
167
  `Workspace: ${engineRef.current.workspace.cwd}\n` +
167
168
  `Tools: ${engineRef.current.toolRegistry.size} registered\n` +
168
169
  `History: ${engineRef.current.history.messages.length} messages\n` +
@@ -320,7 +321,7 @@ function App({ engine, config }) {
320
321
 
321
322
  return h(Box, { flexDirection: "column" },
322
323
  // Welcome banner
323
- showWelcome ? h(WelcomeBanner) : null,
324
+ showWelcome ? h(WelcomeBanner, { projectDir: config.projectDir }) : null,
324
325
 
325
326
  // Message history
326
327
  ...messages.map((msg, i) => {
@@ -389,6 +390,9 @@ function App({ engine, config }) {
389
390
  export async function main({ languageOverride } = {}) {
390
391
  const config = loadSettings();
391
392
 
393
+ // Capture user's project directory (CWD at launch)
394
+ config.projectDir = process.cwd();
395
+
392
396
  // Session-only language override (does NOT persist to config)
393
397
  if (languageOverride) {
394
398
  config.language = languageOverride;
@@ -399,6 +403,15 @@ export async function main({ languageOverride } = {}) {
399
403
  process.exit(1);
400
404
  }
401
405
 
406
+ // Warn if all worker LLM tiers are blank
407
+ const allTiersBlank = !config.tier1 && !config.tier2 && !config.tier3 && !config.tier4;
408
+ if (allTiersBlank) {
409
+ const msg = config.language === "zh"
410
+ ? " ⚠ 所有 Worker LLM 分层为空。DISTILL 模式将不可用。运行 'kc-beta config' 或 'kc-beta onboard' 配置模型分层。"
411
+ : " ⚠ All worker LLM tiers are blank. DISTILL mode will not work. Run 'kc-beta config' or 'kc-beta onboard' to configure model tiers.";
412
+ console.log(`\x1b[33m${msg}\x1b[0m\n`);
413
+ }
414
+
402
415
  const client = new LLMClient({
403
416
  apiKey: config.llmApiKey,
404
417
  baseUrl: config.llmBaseUrl,
@@ -37,7 +37,11 @@ const L = {
37
37
  keyTypeOptions: ["API Key (pay-per-use)", "Coding Plan Key (subscription)"],
38
38
  conductorModel: "Conductor Model",
39
39
  workerTiers: "Worker LLM Tiers",
40
+ vlmTiers: "VLM Tiers (Vision/OCR)",
40
41
  tierHint: "Press Enter to accept defaults",
42
+ workerConfig: "Worker LLM Provider",
43
+ workerSameProvider: "Use same provider for worker LLMs?",
44
+ yesNo: "Y/n",
41
45
  accuracy: "Accuracy Threshold",
42
46
  saved: "Saved to",
43
47
  runHint: "Run {cmd} to start the agent.",
@@ -67,7 +71,11 @@ const L = {
67
71
  keyTypeOptions: ["API Key(按量付费)", "Coding Plan Key(包年包月)"],
68
72
  conductorModel: "主模型",
69
73
  workerTiers: "Worker 模型分层",
74
+ vlmTiers: "VLM 视觉模型分层(OCR)",
70
75
  tierHint: "回车接受默认值",
76
+ workerConfig: "Worker LLM 服务商",
77
+ workerSameProvider: "Worker LLM 使用同一服务商?",
78
+ yesNo: "Y/n",
71
79
  accuracy: "准确率阈值",
72
80
  saved: "已保存至",
73
81
  runHint: "运行 {cmd} 启动 Agent。",
@@ -237,7 +245,7 @@ export async function onboard() {
237
245
  );
238
246
  console.log();
239
247
 
240
- // --- Worker tiers ---
248
+ // --- Worker LLM tiers ---
241
249
  console.log(` ${CYAN}${t.workerTiers}${RESET} ${DIM}(${t.tierHint})${RESET}`);
242
250
  const tiers = {};
243
251
  for (const tier of ["tier1", "tier2", "tier3", "tier4"]) {
@@ -251,6 +259,59 @@ export async function onboard() {
251
259
  }
252
260
  console.log();
253
261
 
262
+ // --- VLM tiers (vision/OCR) ---
263
+ console.log(` ${CYAN}${t.vlmTiers}${RESET} ${DIM}(${t.tierHint})${RESET}`);
264
+ const vlmTiers = {};
265
+ for (const tier of ["tier1", "tier2", "tier3"]) {
266
+ const def = provider.defaultVlm?.[tier] || existing?.vlm_tiers?.[tier] || "";
267
+ vlmTiers[tier] = await ask(
268
+ rl,
269
+ ` ${tier.toUpperCase()}`,
270
+ def,
271
+ );
272
+ }
273
+ console.log();
274
+
275
+ // --- Worker LLM provider (optional) ---
276
+ console.log(` ${CYAN}${t.workerConfig}${RESET}`);
277
+ const sameProvider = await ask(rl, ` ${t.workerSameProvider}`, "Y", t.yesNo);
278
+ let workerProvider = "";
279
+ let workerApiKey = "";
280
+ let workerBaseUrl = "";
281
+ let workerAuthType = "";
282
+ let workerApiFormat = "";
283
+
284
+ if (sameProvider.toLowerCase() === "n" || sameProvider.toLowerCase() === "no") {
285
+ // Pick a different provider for workers
286
+ console.log();
287
+ console.log(` ${CYAN}${t.providerPrompt} (Worker):${RESET}`);
288
+ for (let i = 0; i < labels.length; i++) {
289
+ console.log(` ${i + 1}. ${labels[i].label}`);
290
+ }
291
+ const wIdx = parseInt(await ask(rl, ` ${GRAY}>${RESET} ${t.choose}`, "1"), 10) - 1;
292
+ const wp = providers[Math.max(0, Math.min(wIdx, providers.length - 1))];
293
+ workerProvider = wp.id;
294
+ workerAuthType = wp.authType;
295
+ workerApiFormat = wp.apiFormat;
296
+ workerBaseUrl = wp.baseUrl;
297
+
298
+ if (wp.id === "custom") {
299
+ workerBaseUrl = await ask(rl, ` ${t.baseUrl}`, existing.worker_base_url || "");
300
+ }
301
+
302
+ // Worker API key
303
+ const wMasked = existing.worker_api_key ? existing.worker_api_key.slice(0, 6) + "..." + existing.worker_api_key.slice(-4) : "";
304
+ const wKeyHint = wMasked ? t.apiKeyKeep : t.apiKeyRequired;
305
+ workerApiKey = await ask(
306
+ rl,
307
+ ` ${CYAN}${t.apiKey} (Worker)${RESET}`,
308
+ "",
309
+ wKeyHint,
310
+ );
311
+ workerApiKey = workerApiKey || existing.worker_api_key || "";
312
+ }
313
+ console.log();
314
+
254
315
  rl.close();
255
316
 
256
317
  // Preserve existing thresholds or set defaults (editable via 'kc-beta config')
@@ -268,6 +329,14 @@ export async function onboard() {
268
329
  api_format: provider.apiFormat,
269
330
  conductor_model: model,
270
331
  tiers,
332
+ vlm_tiers: vlmTiers,
333
+ // Worker LLM (optional — empty means use conductor config)
334
+ worker_provider: workerProvider,
335
+ worker_api_key: workerApiKey,
336
+ worker_base_url: workerBaseUrl,
337
+ worker_auth_type: workerAuthType,
338
+ worker_api_format: workerApiFormat,
339
+ // Thresholds
271
340
  accuracy_threshold: accuracy,
272
341
  systemic_threshold: systemicThreshold,
273
342
  spot_check_rate: spotCheckRate,
package/src/config.js CHANGED
@@ -55,7 +55,7 @@ export function loadSettings(workspacePath) {
55
55
  const provider = gc.provider || "siliconflow";
56
56
  const providerDef = getProviderById(provider);
57
57
 
58
- return {
58
+ const settings = {
59
59
  // Provider identity
60
60
  provider,
61
61
  authType: gc.auth_type || providerDef?.authType || "bearer",
@@ -73,8 +73,17 @@ export function loadSettings(workspacePath) {
73
73
  tier3: env.TIER3 || gc.tiers?.tier3 || "",
74
74
  tier4: env.TIER4 || gc.tiers?.tier4 || "",
75
75
 
76
- // OCR models
77
- ocrModelTier1: env.OCR_MODEL_TIER1 || "zai-org/GLM-4.6V",
76
+ // VLM tiers (vision/OCR models)
77
+ vlmTier1: env.VLM_TIER1 || gc.vlm_tiers?.tier1 || "",
78
+ vlmTier2: env.VLM_TIER2 || gc.vlm_tiers?.tier2 || "",
79
+ vlmTier3: env.VLM_TIER3 || gc.vlm_tiers?.tier3 || "",
80
+
81
+ // Worker LLM — optional, defaults to conductor config
82
+ workerProvider: gc.worker_provider || "",
83
+ workerApiKey: env.WORKER_API_KEY || gc.worker_api_key || "",
84
+ workerBaseUrl: env.WORKER_BASE_URL || gc.worker_base_url || "",
85
+ workerAuthType: gc.worker_auth_type || "",
86
+ workerApiFormat: gc.worker_api_format || "",
78
87
 
79
88
  // Document parsing
80
89
  mineruApiUrl: env.MINERU_API_URL || "",
@@ -106,6 +115,37 @@ export function loadSettings(workspacePath) {
106
115
  // Language
107
116
  language: env.LANGUAGE || gc.language || "en",
108
117
  };
118
+
119
+ // Effective worker config (falls back to conductor config)
120
+ settings.effectiveWorkerProvider = () => settings.workerProvider || settings.provider;
121
+ settings.effectiveWorkerApiKey = () => settings.workerApiKey || settings.llmApiKey;
122
+ settings.effectiveWorkerBaseUrl = () => {
123
+ if (settings.workerBaseUrl) return settings.workerBaseUrl;
124
+ // If worker uses a different provider, use that provider's default base URL
125
+ if (settings.workerProvider && settings.workerProvider !== settings.provider) {
126
+ const wp = getProviderById(settings.workerProvider);
127
+ return wp?.baseUrl || settings.llmBaseUrl;
128
+ }
129
+ return settings.llmBaseUrl;
130
+ };
131
+ settings.effectiveWorkerAuthType = () => {
132
+ if (settings.workerAuthType) return settings.workerAuthType;
133
+ if (settings.workerProvider && settings.workerProvider !== settings.provider) {
134
+ const wp = getProviderById(settings.workerProvider);
135
+ return wp?.authType || settings.authType;
136
+ }
137
+ return settings.authType;
138
+ };
139
+ settings.effectiveWorkerApiFormat = () => {
140
+ if (settings.workerApiFormat) return settings.workerApiFormat;
141
+ if (settings.workerProvider && settings.workerProvider !== settings.provider) {
142
+ const wp = getProviderById(settings.workerProvider);
143
+ return wp?.apiFormat || settings.apiFormat;
144
+ }
145
+ return settings.apiFormat;
146
+ };
147
+
148
+ return settings;
109
149
  }
110
150
 
111
151
  export { GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_PATH };