codewave-openclaw-installer 2.2.3 → 2.3.1

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/README.md CHANGED
@@ -16,17 +16,17 @@ npx codewave-openclaw-installer
16
16
 
17
17
  | 工具 | 包管理器 | 说明 |
18
18
  |------|---------|------|
19
- | `razel-py-cli` | pipx | Python CLI(验收文档、音视频转写等) |
20
- | `razel-cli` | npm | Node CLI(PoPo 文档、PoPo 机器人、腾讯会议等) |
19
+ | `razel-py-cli` | 插件私有运行时 | Python CLI(验收文档、音视频转写等) |
20
+ | `razel-cli` | 插件私有运行时 | Node CLI(PoPo 文档、PoPo 机器人、腾讯会议等) |
21
21
 
22
- Python CLI 安装建议:
22
+ 说明:
23
23
 
24
24
  ```bash
25
- brew install pipx
26
- pipx ensurepath
27
- pipx install razel-py-cli
25
+ npx codewave-openclaw-installer
28
26
  ```
29
27
 
28
+ 安装器会优先使用仓库 `pkg/` 里的离线包,在 `~/.openclaw/runtime/codewave-openclaw-installer/` 下创建插件私有 Python venv 和 Node 运行目录,并在 `~/.local/bin/` 写入 `razel-py-cli` / `razel-cli` wrapper。
29
+
30
30
  ### OpenClaw Skills(自动注册)
31
31
 
32
32
  安装后 Skills 作为 OpenClaw Extension 自动加载,无需手动复制:
@@ -66,7 +66,7 @@ pipx install razel-py-cli
66
66
 
67
67
  1. `preflight`:检测本机依赖、配置和渠道线索
68
68
  2. `core-install`:安装并初始化 `razel-py-cli` / `razel-cli`
69
- 3. `extension-install`:将本包安装到 `~/.openclaw/extensions/codewave-toolkit/`
69
+ 3. `extension-install`:通过 OpenClaw 官方插件安装流程将本包注册到 `~/.openclaw/extensions/codewave-openclaw-installer/`
70
70
  4. `postflight`:重启 OpenClaw Gateway,并运行统一检查输出最终状态
71
71
  5. `channel-bootstrap`:检测到飞书或钉钉线索时,按官方入口自动安装并初始化
72
72
 
@@ -107,7 +107,9 @@ codewave-check --json
107
107
  ## 卸载
108
108
 
109
109
  ```bash
110
- rm -rf ~/.openclaw/extensions/codewave-toolkit
110
+ rm -rf ~/.openclaw/extensions/codewave-openclaw-installer
111
+ rm -rf ~/.openclaw/runtime/codewave-openclaw-installer
112
+ rm -f ~/.local/bin/razel-py-cli ~/.local/bin/razel-cli
111
113
  openclaw gateway restart
112
114
  ```
113
115
 
package/bin/check.mjs CHANGED
@@ -17,7 +17,9 @@ import { existsSync, readFileSync } from 'node:fs';
17
17
  import { join } from 'node:path';
18
18
  import { homedir, platform } from 'node:os';
19
19
  import {
20
+ CODEWAVE_TOOLKIT_RUNTIME_ROOT,
20
21
  CHANNEL_DEFINITIONS,
22
+ getManagedCommandPath,
21
23
  detectDesktopAppInstallMatches,
22
24
  getDesktopAppCandidates,
23
25
  } from './install-lib.mjs';
@@ -47,6 +49,10 @@ let windowsUninstallDisplayNamesPromise = null;
47
49
  // ─── 工具函数 ───
48
50
 
49
51
  function commandExists(cmd) {
52
+ const managed = getManagedCommandPath(cmd);
53
+ if (managed && existsSync(managed)) {
54
+ return Promise.resolve(true);
55
+ }
50
56
  return new Promise((resolve) => {
51
57
  const check = isWin ? 'where' : 'which';
52
58
  const child = spawn(check, [cmd], { stdio: 'ignore', shell: isWin });
@@ -56,8 +62,9 @@ function commandExists(cmd) {
56
62
  }
57
63
 
58
64
  function getCommandVersion(cmd, args = ['--version']) {
65
+ const executable = (getManagedCommandPath(cmd) && existsSync(getManagedCommandPath(cmd))) ? getManagedCommandPath(cmd) : cmd;
59
66
  return new Promise((resolve) => {
60
- const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], shell: isWin });
67
+ const child = spawn(executable, args, { stdio: ['ignore', 'pipe', 'pipe'], shell: isWin && !existsSync(executable) });
61
68
  let stdout = '';
62
69
  child.stdout?.on('data', (d) => { stdout += d.toString(); });
63
70
  child.on('close', (code) => resolve(code === 0 ? stdout.trim().split('\n')[0] : null));
@@ -90,16 +97,18 @@ function openclawChannelHints() {
90
97
  }
91
98
 
92
99
  function run(cmd, args) {
100
+ const executable = (getManagedCommandPath(cmd) && existsSync(getManagedCommandPath(cmd))) ? getManagedCommandPath(cmd) : cmd;
93
101
  return new Promise((resolve) => {
94
- const child = spawn(cmd, args, { stdio: 'inherit', shell: isWin });
102
+ const child = spawn(executable, args, { stdio: 'inherit', shell: isWin && !existsSync(executable) });
95
103
  child.on('close', (code) => resolve(code ?? 1));
96
104
  child.on('error', () => resolve(1));
97
105
  });
98
106
  }
99
107
 
100
108
  function runCapture(cmd, args) {
109
+ const executable = (getManagedCommandPath(cmd) && existsSync(getManagedCommandPath(cmd))) ? getManagedCommandPath(cmd) : cmd;
101
110
  return new Promise((resolve) => {
102
- const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], shell: isWin });
111
+ const child = spawn(executable, args, { stdio: ['ignore', 'pipe', 'pipe'], shell: isWin && !existsSync(executable) });
103
112
  let stdout = '';
104
113
  let stderr = '';
105
114
  child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); });
@@ -202,9 +211,17 @@ async function checkOpenClawConfig() {
202
211
  }
203
212
 
204
213
  async function checkPythonDocx() {
214
+ const managedPython = join(
215
+ CODEWAVE_TOOLKIT_RUNTIME_ROOT,
216
+ 'py',
217
+ isWin ? 'Scripts' : 'bin',
218
+ isWin ? 'python.exe' : 'python3',
219
+ );
220
+ const pythonExecutable = existsSync(managedPython) ? managedPython : 'python3';
205
221
  return new Promise((resolve) => {
206
- const child = spawn('python3', ['-c', 'import docx; print(docx.__version__)'], {
222
+ const child = spawn(pythonExecutable, ['-c', 'import docx; print(docx.__version__)'], {
207
223
  stdio: ['ignore', 'pipe', 'pipe'],
224
+ shell: isWin && !existsSync(pythonExecutable),
208
225
  });
209
226
  let stdout = '';
210
227
  child.stdout?.on('data', (d) => { stdout += d.toString(); });
@@ -216,12 +233,12 @@ async function checkPythonDocx() {
216
233
  name: 'python-docx',
217
234
  status: 'warn',
218
235
  message: '未安装(DOCX 解析的 fallback 方案之一)',
219
- fix: 'pip3 install python-docx',
236
+ fix: '重新运行 npx codewave-openclaw-installer,或手动补齐 python-docx',
220
237
  });
221
238
  }
222
239
  });
223
240
  child.on('error', () => {
224
- resolve({ name: 'python-docx', status: 'warn', message: 'python3 不可用' });
241
+ resolve({ name: 'python-docx', status: 'warn', message: 'Python 运行时不可用' });
225
242
  });
226
243
  });
227
244
  }
@@ -243,7 +260,7 @@ async function checkDocxPipeline() {
243
260
  name: 'DOCX 解析工具链',
244
261
  status: 'fail',
245
262
  message: '缺少所有 DOCX 解析方案 (pandoc / textutil / python-docx)',
246
- fix: isMac ? 'brew install pandoc' : isLinux ? 'sudo apt install pandoc' : 'pip3 install python-docx',
263
+ fix: isMac ? 'brew install pandoc,或重新运行 npx codewave-openclaw-installer' : isLinux ? 'sudo apt install pandoc,或重新运行 npx codewave-openclaw-installer' : '重新运行 npx codewave-openclaw-installer',
247
264
  };
248
265
  }
249
266
  return {
@@ -433,10 +450,10 @@ async function main() {
433
450
 
434
451
  // CLI 工具
435
452
  checks['razel-py-cli'] = await checkCli('razel-py-cli', {
436
- installCmd: 'pipx install razel-py-cli && razel-py-cli init',
453
+ installCmd: '重新运行 npx codewave-openclaw-installer(将优先安装插件私有 razel-py-cli 运行时)',
437
454
  });
438
455
  checks['razel-cli'] = await checkCli('razel-cli', {
439
- installCmd: 'npm install -g @lcap-delivery/razel-cli@latest && razel-cli init',
456
+ installCmd: '重新运行 npx codewave-openclaw-installer(将优先安装插件私有 razel-cli 运行时)',
440
457
  });
441
458
  checks['jq'] = await checkCli('jq', {
442
459
  installCmd: isMac ? 'brew install jq' : 'sudo apt install jq',
@@ -3,6 +3,12 @@ import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
  import * as path from 'node:path';
5
5
 
6
+ export const CODEWAVE_TOOLKIT_CONFIG_PATH = join(homedir(), '.openclaw', 'codewave-toolkit.json');
7
+ export const CODEWAVE_TOOLKIT_EXTENSION_ID = 'codewave-openclaw-installer';
8
+ export const CODEWAVE_TOOLKIT_EXTENSION_ROOT = join(homedir(), '.openclaw', 'extensions', CODEWAVE_TOOLKIT_EXTENSION_ID);
9
+ export const CODEWAVE_TOOLKIT_RUNTIME_ROOT = join(homedir(), '.openclaw', 'runtime', CODEWAVE_TOOLKIT_EXTENSION_ID);
10
+ export const CODEWAVE_TOOLKIT_WRAPPER_BIN = join(homedir(), '.local', 'bin');
11
+
6
12
  export const SKILL_SUMMARIES = [
7
13
  'acceptance-doc-entry — 项目验收文档生成',
8
14
  'pmo-weekly-report — PMO 项目看板周报',
@@ -199,13 +205,13 @@ export function classifyPythonInstallFailure(stderr = '') {
199
205
  if (stderr.includes('externally-managed-environment') || stderr.includes('PEP 668')) {
200
206
  return {
201
207
  code: 'pep668',
202
- message: '当前 Python 环境受 PEP 668 管理,建议改用 pipx 安装 razel-py-cli',
208
+ message: '当前 Python 环境受 PEP 668 管理,建议改用安装器创建插件私有运行时',
203
209
  };
204
210
  }
205
211
 
206
212
  return {
207
213
  code: 'generic',
208
- message: 'Python CLI 安装失败,请检查 python3/pip3 是否可用',
214
+ message: 'Python CLI 安装失败,请检查 python3/venv/pip 是否可用',
209
215
  };
210
216
  }
211
217
 
@@ -511,3 +517,56 @@ export function buildRazelConfigBootstrap({
511
517
 
512
518
  return { writes, missing };
513
519
  }
520
+
521
+ export function getSmartCustomerServiceDocsDir({
522
+ env = process.env,
523
+ toolkitConfig = {},
524
+ } = {}) {
525
+ const envDir = typeof env.SMART_CS_DOCS_DIR === 'string' ? env.SMART_CS_DOCS_DIR.trim() : '';
526
+ if (envDir) {
527
+ return envDir;
528
+ }
529
+
530
+ const configDir = typeof toolkitConfig?.smartCustomerService?.docsDir === 'string'
531
+ ? toolkitConfig.smartCustomerService.docsDir.trim()
532
+ : '';
533
+ if (configDir) {
534
+ return configDir;
535
+ }
536
+
537
+ return '';
538
+ }
539
+
540
+ export function getSmartCustomerServiceVectorConfig({
541
+ env = process.env,
542
+ toolkitConfig = {},
543
+ } = {}) {
544
+ const config = toolkitConfig?.smartCustomerService?.vector || {};
545
+ const enabled = env.SMART_CS_VECTOR_ENABLED
546
+ ? ['1', 'true', 'yes', 'on'].includes(String(env.SMART_CS_VECTOR_ENABLED).trim().toLowerCase())
547
+ : config.enabled === true;
548
+
549
+ return {
550
+ enabled,
551
+ baseUrl: (env.SMART_CS_VECTOR_BASE_URL || config.baseUrl || '').trim?.() || '',
552
+ apiKey: (env.SMART_CS_VECTOR_API_KEY || config.apiKey || '').trim?.() || '',
553
+ model: (env.SMART_CS_VECTOR_MODEL || config.model || '').trim?.() || '',
554
+ };
555
+ }
556
+
557
+ export function getManagedCommandPath(command, {
558
+ platform = process.platform,
559
+ runtimeRoot = CODEWAVE_TOOLKIT_RUNTIME_ROOT,
560
+ } = {}) {
561
+ const isWindows = platform === 'win32';
562
+
563
+ if (command === 'razel-py-cli') {
564
+ return join(runtimeRoot, 'py', isWindows ? 'Scripts' : 'bin', isWindows ? 'razel-py-cli.exe' : 'razel-py-cli');
565
+ }
566
+
567
+ if (command === 'razel-cli') {
568
+ return join(runtimeRoot, 'node', 'node_modules', '.bin', isWindows ? 'razel-cli.cmd' : 'razel-cli');
569
+ }
570
+
571
+ return null;
572
+ }
package/bin/install.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawn } from 'node:child_process';
4
- import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
4
+ import { chmodSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
5
5
  import { dirname, resolve, join } from 'node:path';
6
6
  import * as path from 'node:path';
7
7
  import { homedir } from 'node:os';
@@ -9,16 +9,21 @@ import { fileURLToPath } from 'node:url';
9
9
  import { createInterface } from 'node:readline/promises';
10
10
 
11
11
  import {
12
+ CODEWAVE_TOOLKIT_CONFIG_PATH,
13
+ CODEWAVE_TOOLKIT_EXTENSION_ROOT,
14
+ CODEWAVE_TOOLKIT_RUNTIME_ROOT,
15
+ CODEWAVE_TOOLKIT_WRAPPER_BIN,
12
16
  CHANNEL_DEFINITIONS,
13
17
  SKILL_SUMMARIES,
14
18
  buildRazelConfigBootstrap,
15
- classifyPythonInstallFailure,
16
19
  createInstallContext,
17
20
  detectDesktopAppInstallMatches,
18
21
  evaluateChannelDetection,
19
22
  getCoreBootstrapPlan,
20
23
  getDesktopAppCandidates,
21
- getPipxBootstrapPlan,
24
+ getManagedCommandPath,
25
+ getSmartCustomerServiceDocsDir,
26
+ getSmartCustomerServiceVectorConfig,
22
27
  preserveEnabledTruePreference,
23
28
  resolvePromptDefault,
24
29
  shouldDryRun,
@@ -56,7 +61,21 @@ function refreshLocalToolPaths() {
56
61
  : path.posix.join(homedir(), '.local', 'bin'));
57
62
  }
58
63
 
64
+ refreshLocalToolPaths();
65
+
66
+ function resolveManagedExecutable(cmd) {
67
+ const managedPath = getManagedCommandPath(cmd);
68
+ if (managedPath && existsSync(managedPath)) {
69
+ return managedPath;
70
+ }
71
+ return null;
72
+ }
73
+
59
74
  function resolveExecutable(cmd) {
75
+ const managed = resolveManagedExecutable(cmd);
76
+ if (managed) {
77
+ return managed;
78
+ }
60
79
  if (!isWin) {
61
80
  return cmd;
62
81
  }
@@ -114,6 +133,10 @@ function runCapture(cmd, args, opts = {}) {
114
133
  }
115
134
 
116
135
  function commandExists(cmd) {
136
+ const managed = resolveManagedExecutable(cmd);
137
+ if (managed) {
138
+ return Promise.resolve(true);
139
+ }
117
140
  return new Promise((resolve) => {
118
141
  const check = isWin ? 'where' : 'which';
119
142
  const child = spawn(check, [cmd], { stdio: 'ignore', shell: false });
@@ -134,6 +157,69 @@ function readJson(filePath) {
134
157
  }
135
158
  }
136
159
 
160
+ function ensureParentDir(filePath) {
161
+ mkdirSync(dirname(filePath), { recursive: true });
162
+ }
163
+
164
+ function writeJson(filePath, value) {
165
+ ensureParentDir(filePath);
166
+ writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
167
+ }
168
+
169
+ function readToolkitConfig() {
170
+ return readJson(CODEWAVE_TOOLKIT_CONFIG_PATH) || {};
171
+ }
172
+
173
+ function writeToolkitConfig(updater) {
174
+ if (DRY_RUN) {
175
+ return false;
176
+ }
177
+ const current = readToolkitConfig();
178
+ const next = updater(current) || current;
179
+ writeJson(CODEWAVE_TOOLKIT_CONFIG_PATH, next);
180
+ return true;
181
+ }
182
+
183
+ function getPrivatePythonBin(command) {
184
+ return getManagedCommandPath(command, { runtimeRoot: CODEWAVE_TOOLKIT_RUNTIME_ROOT });
185
+ }
186
+
187
+ function getPrivatePythonPipPath() {
188
+ return join(CODEWAVE_TOOLKIT_RUNTIME_ROOT, 'py', isWin ? 'Scripts' : 'bin', isWin ? 'pip.exe' : 'pip');
189
+ }
190
+
191
+ function getPrivateNodeRoot() {
192
+ return join(CODEWAVE_TOOLKIT_RUNTIME_ROOT, 'node');
193
+ }
194
+
195
+ function getWrapperPath(command) {
196
+ const filename = isWin ? `${command}.cmd` : command;
197
+ return join(CODEWAVE_TOOLKIT_WRAPPER_BIN, filename);
198
+ }
199
+
200
+ function ensureWrapper(command, targetPath) {
201
+ if (DRY_RUN || !targetPath) {
202
+ return false;
203
+ }
204
+ mkdirSync(CODEWAVE_TOOLKIT_WRAPPER_BIN, { recursive: true });
205
+ const wrapperPath = getWrapperPath(command);
206
+ try {
207
+ if (existsSync(wrapperPath) || lstatSync(wrapperPath).isSymbolicLink()) {
208
+ rmSync(wrapperPath, { force: true });
209
+ }
210
+ } catch {
211
+ // ignore cleanup errors and let writeFileSync surface real failures
212
+ }
213
+ const content = isWin
214
+ ? `@echo off\r\n"${targetPath}" %*\r\n`
215
+ : `#!/bin/sh\nexec "${targetPath}" "$@"\n`;
216
+ writeFileSync(wrapperPath, content, 'utf8');
217
+ if (!isWin) {
218
+ chmodSync(wrapperPath, 0o755);
219
+ }
220
+ return true;
221
+ }
222
+
137
223
  function clearPluginRegistration(pluginIds) {
138
224
  if (DRY_RUN) {
139
225
  return false;
@@ -548,94 +634,54 @@ async function resolvePythonRuntime() {
548
634
  };
549
635
  }
550
636
 
551
- async function ensurePipx(context) {
552
- if (await commandExists('pipx')) {
553
- contextResult(context, 'pipx', 'reused');
554
- return {
555
- available: true,
556
- invoke: { cmd: 'pipx', args: [] },
557
- pythonCommand: (await resolvePythonRuntime()).pythonCommand,
558
- };
637
+ async function installPythonCli(context) {
638
+ const managedCli = getPrivatePythonBin('razel-py-cli');
639
+ if (managedCli && existsSync(managedCli)) {
640
+ console.log(dim(' ↺ 已检测到私有 razel-py-cli 运行时,跳过安装'));
641
+ ensureWrapper('razel-py-cli', managedCli);
642
+ contextResult(context, 'razel-py-cli', 'reused (private)');
643
+ return;
559
644
  }
560
645
 
561
646
  const runtime = await resolvePythonRuntime();
562
- if (!runtime.pythonCommand && !runtime.pythonLauncher) {
563
- console.log(yellow(' ⚠️ 缺少 Python 运行时,无法安装 pipx'));
564
- contextResult(context, 'pipx', 'blocked (missing python3)');
565
- return { available: false, invoke: null, pythonCommand: null };
566
- }
567
-
568
- const pipxPlan = getPipxBootstrapPlan({
569
- platform: process.platform,
570
- hasBrew: await commandExists('brew'),
571
- pythonCommand: runtime.pythonCommand || 'python3',
572
- pythonLauncher: runtime.pythonLauncher,
573
- });
574
-
575
- if (!(await commandExists(pipxPlan.install.cmd))) {
576
- console.log(yellow(` ⚠️ 缺少 ${pipxPlan.install.cmd},无法自动安装 pipx`));
577
- contextResult(context, 'pipx', `blocked (${pipxPlan.install.cmd} missing)`);
578
- return { available: false, invoke: null, pythonCommand: runtime.pythonCommand };
579
- }
580
-
581
- console.log(dim(' 正在安装 pipx...'));
582
- const installCode = await run(pipxPlan.install.cmd, pipxPlan.install.args);
583
- if (installCode !== 0) {
584
- if (runtime.pipCommand) {
585
- const classification = classifyPythonInstallFailure('externally-managed-environment');
586
- console.log(dim(` 提示: ${classification.message}`));
587
- }
588
- contextResult(context, 'pipx', 'failed');
589
- return { available: false, invoke: null, pythonCommand: runtime.pythonCommand };
590
- }
591
-
592
- await run(pipxPlan.ensurePath.cmd, pipxPlan.ensurePath.args);
593
- refreshLocalToolPaths();
594
- const pipxAvailable = await commandExists('pipx');
595
- contextResult(context, 'pipx', pipxAvailable ? 'ok' : 'ok (path refresh may be needed)');
596
- return {
597
- available: true,
598
- invoke: pipxAvailable ? { cmd: 'pipx', args: [] } : pipxPlan.invoke,
599
- pythonCommand: runtime.pythonCommand,
600
- };
601
- }
602
-
603
- async function installPythonCli(context) {
604
- const pipxState = await ensurePipx(context);
605
-
606
- if (await commandExists('razel-py-cli')) {
607
- console.log(dim(' ↺ 已检测到 razel-py-cli,跳过安装'));
608
- contextResult(context, 'razel-py-cli', 'reused');
647
+ if (!runtime.pythonCommand) {
648
+ console.log(yellow(' ⚠️ 缺少 Python 运行时,无法继续安装私有 razel-py-cli'));
649
+ contextResult(context, 'razel-py-cli', 'blocked (missing python3)');
609
650
  return;
610
651
  }
611
652
 
612
- if (!pipxState.available || !pipxState.invoke) {
613
- console.log(yellow(' ⚠️ 未检测到 pipx,无法继续安装 razel-py-cli'));
614
- contextResult(context, 'razel-py-cli', 'blocked (pipx missing)');
653
+ const privatePyRoot = join(CODEWAVE_TOOLKIT_RUNTIME_ROOT, 'py');
654
+ rmSync(privatePyRoot, { recursive: true, force: true });
655
+ mkdirSync(CODEWAVE_TOOLKIT_RUNTIME_ROOT, { recursive: true });
656
+
657
+ console.log(dim(' 正在创建私有 Python 运行时...'));
658
+ const venvCode = await run(runtime.pythonCommand, ['-m', 'venv', privatePyRoot]);
659
+ if (venvCode !== 0) {
660
+ console.log(yellow(` ⚠️ 私有 Python 运行时创建失败${runtime.pythonCommand ? ` (python=${runtime.pythonCommand})` : ''}`));
661
+ contextResult(context, 'razel-py-cli', 'failed (venv)');
615
662
  return;
616
663
  }
617
664
 
618
- const pipxArgs = [...pipxState.invoke.args, 'install'];
619
- if (pipxState.pythonCommand) {
620
- pipxArgs.push('--python', pipxState.pythonCommand);
665
+ const pipPath = getPrivatePythonPipPath();
666
+ const upgradeCode = await run(pipPath, ['install', '--upgrade', 'pip']);
667
+ if (upgradeCode !== 0) {
668
+ console.log(dim(' 提示: pip 升级失败,继续尝试安装 razel-py-cli'));
621
669
  }
622
- pipxArgs.push('razel-py-cli');
623
670
 
624
- const pipxCode = await run(pipxState.invoke.cmd, pipxArgs);
625
- if (pipxCode !== 0) {
626
- console.log(yellow(` ⚠️ razel-py-cli 通过 pipx 安装失败${pipxState.pythonCommand ? ` (python=${pipxState.pythonCommand})` : ''}`));
627
- contextResult(context, 'razel-py-cli', 'failed (pipx)');
671
+ const localWhl = join(PKG_ROOT, 'pkg', 'razel_py_cli-0.0.4-py3-none-any.whl');
672
+ const source = existsSync(localWhl) ? localWhl : 'razel-py-cli';
673
+ const installCode = await run(pipPath, ['install', source]);
674
+ if (installCode !== 0) {
675
+ console.log(yellow(` ⚠️ razel-py-cli 私有安装失败${runtime.pythonCommand ? ` (python=${runtime.pythonCommand})` : ''}`));
676
+ contextResult(context, 'razel-py-cli', 'failed (private)');
628
677
  return;
629
678
  }
630
679
 
631
- console.log(green('razel-py-cli 通过 pipx 安装成功'));
632
- refreshLocalToolPaths();
680
+ ensureWrapper('razel-py-cli', managedCli);
681
+ console.log(green(' ✅ razel-py-cli 私有安装成功'));
633
682
  console.log(dim(' 正在初始化...'));
634
- let initCode = await run('razel-py-cli', ['init']);
635
- if (initCode !== 0 && pipxState.invoke) {
636
- initCode = await run(pipxState.invoke.cmd, [...pipxState.invoke.args, 'run', 'razel-py-cli', 'init']);
637
- }
638
- contextResult(context, 'razel-py-cli', initCode === 0 ? 'ok' : 'installed via pipx (init failed)');
683
+ const initCode = await run(managedCli, ['init']);
684
+ contextResult(context, 'razel-py-cli', initCode === 0 ? 'ok (private)' : 'installed (private init failed)');
639
685
  }
640
686
 
641
687
  async function applyConfigWrites(writes) {
@@ -786,6 +832,74 @@ async function collectInteractiveConfigWrites(missing, options = {}) {
786
832
  console.log(dim(' 已选择跳过 PoPo 服务配置'));
787
833
  }
788
834
 
835
+ const toolkitConfig = readToolkitConfig();
836
+ const configuredDocsDir = getSmartCustomerServiceDocsDir({
837
+ env: process.env,
838
+ toolkitConfig,
839
+ });
840
+ const configuredVector = getSmartCustomerServiceVectorConfig({
841
+ env: process.env,
842
+ toolkitConfig,
843
+ });
844
+ console.log(dim(' [智能客服] 可配置外部知识目录;留空则继续使用 skill 自带 docs/'));
845
+ if (configuredDocsDir) {
846
+ console.log(dim(` 当前知识目录: ${configuredDocsDir}`));
847
+ }
848
+ const wantsDocsDir = await askYesNo(' 是否现在配置智能客服知识目录?[Y/n] ', false);
849
+ if (wantsDocsDir) {
850
+ const docsDir = await ask(' SMART_CS_DOCS_DIR / docsDir absolute path: ');
851
+ if (docsDir) {
852
+ writeToolkitConfig((current) => ({
853
+ ...current,
854
+ smartCustomerService: {
855
+ ...(current.smartCustomerService || {}),
856
+ docsDir,
857
+ configuredAt: new Date().toISOString(),
858
+ },
859
+ }));
860
+ console.log(green(` ✅ 已写入智能客服知识目录: ${docsDir}`));
861
+ } else {
862
+ console.log(dim(' 未填写知识目录,将继续使用默认 docs/'));
863
+ }
864
+ }
865
+
866
+ console.log(dim(' [智能客服] 可选配置向量检索;安装时只记录配置,不立即构建索引'));
867
+ if (configuredVector.enabled) {
868
+ console.log(dim(` 当前向量配置: enabled=true, model=${configuredVector.model || '(unset)'}`));
869
+ }
870
+ const wantsVector = await askYesNo(' 是否现在配置智能客服向量模型?[y/N] ', true);
871
+ if (wantsVector) {
872
+ console.log(dim(' [Vector] 回车可跳过;若填写完整,首次使用时自动建索引,后续按文档变更增量重建'));
873
+ const baseUrl = await ask(' SMART_CS_VECTOR_BASE_URL: ');
874
+ const apiKey = await ask(' SMART_CS_VECTOR_API_KEY: ');
875
+ const model = await ask(' SMART_CS_VECTOR_MODEL: ');
876
+ if (baseUrl && apiKey && model) {
877
+ writeToolkitConfig((current) => ({
878
+ ...current,
879
+ smartCustomerService: {
880
+ ...(current.smartCustomerService || {}),
881
+ retrieval: {
882
+ mode: 'auto',
883
+ },
884
+ vector: {
885
+ enabled: true,
886
+ baseUrl,
887
+ apiKey,
888
+ model,
889
+ },
890
+ index: {
891
+ buildStrategy: 'lazy-on-first-use',
892
+ rebuildStrategy: 'incremental-by-mtime-or-hash',
893
+ },
894
+ configuredAt: new Date().toISOString(),
895
+ },
896
+ }));
897
+ console.log(green(` ✅ 已写入智能客服向量配置: ${model}`));
898
+ } else {
899
+ console.log(dim(' 向量配置未填写完整,保持 grep / 默认检索路径'));
900
+ }
901
+ }
902
+
789
903
  rl.close();
790
904
  return { writes, skipPopo };
791
905
  }
@@ -1076,7 +1190,39 @@ async function runCoreInstall(context) {
1076
1190
  await installCoreDependency(spec, context);
1077
1191
  }
1078
1192
  await installPythonCli(context);
1079
- await ensureCommand('razel-cli', [NPM, 'install', '-g', '@lcap-delivery/razel-cli@latest'], 'razel-cli', context, ['init']);
1193
+ const managedRazelCli = getManagedCommandPath('razel-cli', { runtimeRoot: CODEWAVE_TOOLKIT_RUNTIME_ROOT });
1194
+ if (managedRazelCli && existsSync(managedRazelCli)) {
1195
+ console.log(dim(' ↺ 已检测到私有 razel-cli 运行时,跳过安装'));
1196
+ ensureWrapper('razel-cli', managedRazelCli);
1197
+ contextResult(context, 'razel-cli', 'reused (private)');
1198
+ return;
1199
+ }
1200
+
1201
+ if (!(await commandExists('npm'))) {
1202
+ console.log(yellow(' ⚠️ 未检测到 npm,无法继续安装私有 razel-cli'));
1203
+ contextResult(context, 'razel-cli', 'blocked (npm missing)');
1204
+ return;
1205
+ }
1206
+
1207
+ const privateNodeRoot = getPrivateNodeRoot();
1208
+ rmSync(privateNodeRoot, { recursive: true, force: true });
1209
+ mkdirSync(privateNodeRoot, { recursive: true });
1210
+
1211
+ const localTgz = join(PKG_ROOT, 'pkg', 'lcap-delivery-razel-cli-0.0.4.tgz');
1212
+ const razelCliSource = existsSync(localTgz) ? localTgz : '@lcap-delivery/razel-cli@latest';
1213
+ console.log(dim(' 正在安装私有 razel-cli 运行时...'));
1214
+ const installCode = await run(NPM, ['install', '--prefix', privateNodeRoot, '--no-audit', '--no-fund', razelCliSource]);
1215
+ if (installCode !== 0) {
1216
+ console.log(yellow(' ⚠️ 私有 razel-cli 安装失败'));
1217
+ contextResult(context, 'razel-cli', 'failed (private)');
1218
+ return;
1219
+ }
1220
+
1221
+ ensureWrapper('razel-cli', managedRazelCli);
1222
+ console.log(green(' ✅ razel-cli 私有安装成功'));
1223
+ console.log(dim(' 正在初始化...'));
1224
+ const initCode = await run(managedRazelCli, ['init']);
1225
+ contextResult(context, 'razel-cli', initCode === 0 ? 'ok (private)' : 'installed (private init failed)');
1080
1226
  }
1081
1227
 
1082
1228
  async function runConfigBootstrap(context) {
@@ -1313,6 +1459,20 @@ async function main() {
1313
1459
  console.log(` Skill 就绪情况: ${context.postflight.summary.ok} ok / ${context.postflight.summary.warn} warn / ${context.postflight.summary.fail} fail`);
1314
1460
  console.log('');
1315
1461
  }
1462
+ const toolkitConfig = readToolkitConfig();
1463
+ const smartDocsDir = getSmartCustomerServiceDocsDir({
1464
+ env: process.env,
1465
+ toolkitConfig,
1466
+ });
1467
+ const smartVector = getSmartCustomerServiceVectorConfig({
1468
+ env: process.env,
1469
+ toolkitConfig,
1470
+ });
1471
+ const smartMode = smartVector.enabled ? 'vector(auto)' : 'grep';
1472
+ console.log(' 智能客服:');
1473
+ console.log(` 知识目录 : ${smartDocsDir || '内置 docs/'}`);
1474
+ console.log(` 检索模式 : ${smartMode}`);
1475
+ console.log('');
1316
1476
  console.log(' 使用方式:');
1317
1477
  console.log(cyan(' razel-py-cli --help'));
1318
1478
  console.log(cyan(' razel-cli --help'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codewave-openclaw-installer",
3
- "version": "2.2.3",
3
+ "version": "2.3.1",
4
4
  "description": "网易智企 CodeWave OpenClaw 扩展:Skills + CLI 依赖一键安装",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -24,6 +24,7 @@
24
24
  "index.js",
25
25
  "bin/",
26
26
  "skills/",
27
+ "pkg/",
27
28
  "openclaw.plugin.json"
28
29
  ],
29
30
  "peerDependencies": {
@@ -30,9 +30,9 @@ razel-py-cli acceptance-toolkit <command>
30
30
  | 命令 | 必填参数 | 说明 |
31
31
  |------|---------|------|
32
32
  | `parse-template` | `--template <PATH>` | 解析模板结构,创建任务目录 |
33
- | `plan` | `--task-dir <DIR> --materials <M1> [M2...]` | 解析素材 + LLM 语义映射 |
33
+ | `plan` | `--task-dir <DIR> --materials <M1> [--materials <M2> ...]` | 解析素材 + LLM 语义映射 |
34
34
  | `build` | `--task-dir <DIR>` | 生成草稿 DOCX |
35
- | `revise` | `--task-dir <DIR> --add-materials <M1> [M2...]` | 追加素材 → 重新 plan → 重新 build |
35
+ | `revise` | `--task-dir <DIR> --add-materials <M1> [--add-materials <M2> ...]` | 追加素材 → 重新 plan → 重新 build |
36
36
  | `finalize` | `--task-dir <DIR> --confirmed` | 生成正式版(清理草稿标记) |
37
37
 
38
38
  可选参数:`parse-template` 支持 `--task-id <ID>` 指定任务标识。
@@ -54,7 +54,61 @@ razel-py-cli acceptance-toolkit parse-template --template <TEMPLATE.docx> [--tas
54
54
  ### 2. plan — 解析素材 + 映射
55
55
 
56
56
  ```bash
57
- razel-py-cli acceptance-toolkit plan --task-dir <TASK_DIR> --materials <M1.docx> <M2.docx>
57
+ razel-py-cli acceptance-toolkit plan --task-dir <TASK_DIR> --materials <M1.docx> --materials <M2.docx>
58
+ ```
59
+
60
+ ⚠️ `plan` 的 `--materials` 当前实现为“可重复参数”,不是“一次接收多个文件”。
61
+ 也就是说,正确写法是重复传 `--materials`:
62
+
63
+ ```bash
64
+ razel-py-cli acceptance-toolkit plan \
65
+ --task-dir <TASK_DIR> \
66
+ --materials /path/a.docx \
67
+ --materials /path/b.docx \
68
+ --materials /path/c.docx
69
+ ```
70
+
71
+ 如果要把某个目录下的多个文件一起传入,应先在 Agent 或 shell 层展开成多次 `--materials`,不要写成:
72
+
73
+ ```bash
74
+ # 错误示例:当前 CLI 不支持
75
+ razel-py-cli acceptance-toolkit plan --task-dir <TASK_DIR> --materials a.docx b.docx c.docx
76
+ ```
77
+
78
+ ### 目录与批量素材规则
79
+
80
+ 当用户给的是“目录”“文件夹”“一批素材”“多个附件”时,Agent 不要把目录路径原样传给 CLI,而要先展开成文件列表,再重复传 `--materials`。
81
+
82
+ 执行规则:
83
+ - 如果用户给的是单个文件,直接传一次 `--materials <FILE>`
84
+ - 如果用户给的是多个文件,按文件数重复传 `--materials`
85
+ - 如果用户给的是目录,先枚举目录下的素材文件,再按文件数重复传 `--materials`
86
+ - 如果目录下包含子目录,默认递归查找常见素材类型;若用户明确只要当前层,则只扫当前层
87
+ - 默认只纳入验收相关常见素材:`.docx`、`.doc`、`.pdf`、`.xlsx`、`.xls`、`.csv`、`.md`、`.txt`
88
+ - 图片、聊天导出、录音转写等非 DOCX 素材,只有在底层 CLI 已支持或前置处理后才传入;否则先由 Agent 提炼或转换,再决定是否进入 `plan`
89
+ - 传参前先去重,并保持稳定顺序(建议按文件名或修改时间排序),避免同一素材重复进入 task
90
+
91
+ 推荐做法:
92
+
93
+ ```bash
94
+ # 用户给目录时,先展开,再重复传参
95
+ razel-py-cli acceptance-toolkit plan \
96
+ --task-dir <TASK_DIR> \
97
+ --materials /materials/01-项目方案.docx \
98
+ --materials /materials/02-实施报告.docx \
99
+ --materials /materials/03-测试记录.pdf
100
+ ```
101
+
102
+ 错误做法:
103
+
104
+ ```bash
105
+ # 错误:不要把目录原样传给 --materials
106
+ razel-py-cli acceptance-toolkit plan --task-dir <TASK_DIR> --materials /materials
107
+ ```
108
+
109
+ ```bash
110
+ # 错误:不要把多个文件堆在一个 --materials 后面
111
+ razel-py-cli acceptance-toolkit plan --task-dir <TASK_DIR> --materials a.docx b.docx c.docx
58
112
  ```
59
113
 
60
114
  行为:
@@ -91,9 +145,20 @@ razel-py-cli acceptance-toolkit build --task-dir <TASK_DIR>
91
145
  ### 4. revise — 补充素材重新生成
92
146
 
93
147
  ```bash
94
- razel-py-cli acceptance-toolkit revise --task-dir <TASK_DIR> --add-materials <M3.docx> [M4.docx...]
148
+ razel-py-cli acceptance-toolkit revise --task-dir <TASK_DIR> --add-materials <M3.docx> [--add-materials <M4.docx> ...]
149
+ ```
150
+
151
+ 同理,`revise` 的 `--add-materials` 也需要重复传参:
152
+
153
+ ```bash
154
+ razel-py-cli acceptance-toolkit revise \
155
+ --task-dir <TASK_DIR> \
156
+ --add-materials /path/new-1.docx \
157
+ --add-materials /path/new-2.docx
95
158
  ```
96
159
 
160
+ 如果用户补充的是一个目录,也要先展开成多个文件,再重复传 `--add-materials`,不要把目录路径直接传给 CLI。
161
+
97
162
  行为(三步串联):
98
163
  1. **backup** — 备份当前草稿为 `acceptance-doc.draft.prev.docx`
99
164
  2. **plan** — 追加新素材到 input,重新解析全部素材,重新 LLM 映射
@@ -184,7 +249,7 @@ razel-py-cli acceptance-toolkit finalize --task-dir <TASK_DIR> --confirmed
184
249
 
185
250
  `revise` 命令会自动串联 plan + build,跳过用户确认映射的环节。正确做法:
186
251
 
187
- 1. 用户补充素材时,使用 `plan --task-dir <DIR> --materials <全部素材文件>`(包含新旧所有素材)
252
+ 1. 用户补充素材时,使用 `plan --task-dir <DIR> --materials <旧素材1> --materials <旧素材2> --materials <新素材...>`(包含新旧所有素材,并重复传 `--materials`)
188
253
  2. 展示映射计划,等待用户确认
189
254
  3. 用户确认后,再执行 `build`
190
255
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: smart-customer-service
3
- description: 严格依据 docs/ 目录下的产品文档、开发手册等文档,为平台上线后提供技术答疑支持。文档未涵盖的问题一律拒绝回答。激活场景:平台使用、配置、部署、报错排查、技术答疑、产品文档等问题咨询。
3
+ description: 严格依据已配置知识目录中的产品文档、开发手册等文档,为平台上线后提供技术答疑支持。安装时应要求用户配置知识目录,并可选配置向量模型;若未配置,则回退到 skill 自带 docs/ 与 grep 检索。文档未涵盖的问题一律拒绝回答。激活场景:平台使用、配置、部署、报错排查、技术答疑、产品文档等问题咨询。
4
4
  version: 1.0.0
5
5
  ---
6
6
 
@@ -8,18 +8,102 @@ version: 1.0.0
8
8
 
9
9
  ## 概述
10
10
 
11
- - **职责**:平台上线后技术答疑,仅依据 `smart-customer-service/docs/` 作答
11
+ - **职责**:平台上线后技术答疑,仅依据“已配置知识目录”或 skill 自带 `docs/` 作答
12
12
  - **知识边界**:严禁编造、推断;文档未涵盖时明确无法解答并引导补充信息
13
13
  - **激活**:用户提及平台使用、配置、部署、报错、故障、技术答疑、产品文档、开发手册等
14
14
 
15
+ ## 安装与知识目录配置
16
+
17
+ 此 skill 保留内置 `docs/` 作为默认知识源,但在安装或首次启用时,应优先要求用户配置业务文档目录。
18
+
19
+ ### 推荐配置方式
20
+
21
+ - 用户显式提供知识目录路径,例如:`/data/customer-service-docs`
22
+ - 安装时将该目录记录为固定配置,例如环境变量 `SMART_CS_DOCS_DIR`
23
+ - 若安装器已写入 `~/.openclaw/codewave-toolkit.json`,则从其中的 `smartCustomerService.docsDir` 读取
24
+ - 若安装器已配置向量模型,则从同一配置文件中的 `smartCustomerService.vector` 读取
25
+ - 如果用户配置了多个知识目录,可按顺序逐个检索,或先检索用户指定目录,再回退内置 `docs/`
26
+
27
+ ### 知识源优先级
28
+
29
+ 按以下顺序选择知识源:
30
+
31
+ 1. 用户在当前对话中明确指定的检索目录
32
+ 2. 安装时配置的外部知识目录,例如 `SMART_CS_DOCS_DIR`
33
+ 3. 安装器落盘的固定配置:`~/.openclaw/codewave-toolkit.json` 中的 `smartCustomerService.docsDir`
34
+ 4. skill 自带默认目录:`smart-customer-service/docs/`
35
+
36
+ ## 检索模式与索引策略
37
+
38
+ ### 检索模式优先级
39
+
40
+ 默认使用 `auto` 模式:
41
+
42
+ 1. 若用户明确指定“只用 grep”或“只用向量检索”,按用户要求执行
43
+ 2. 若已配置向量模型,优先尝试向量检索
44
+ 3. 若向量配置缺失、索引不可用、建索引失败或检索失败,则自动回退到 `grep`
45
+
46
+ ### 向量模型配置
47
+
48
+ 推荐在安装时记录以下配置:
49
+
50
+ - `SMART_CS_VECTOR_BASE_URL`
51
+ - `SMART_CS_VECTOR_API_KEY`
52
+ - `SMART_CS_VECTOR_MODEL`
53
+
54
+ 也可由安装器写入:
55
+
56
+ - `~/.openclaw/codewave-toolkit.json`
57
+ - `smartCustomerService.vector.enabled`
58
+ - `smartCustomerService.vector.baseUrl`
59
+ - `smartCustomerService.vector.apiKey`
60
+ - `smartCustomerService.vector.model`
61
+
62
+ ### 索引构建策略
63
+
64
+ 当向量模型已配置时,索引生命周期按以下规则执行:
65
+
66
+ 1. 安装时只记录知识目录和向量模型配置,不立即建索引
67
+ 2. 首次使用时,如果索引不存在,则自动构建索引
68
+ 3. 后续每次使用前,检查知识目录变更
69
+ 4. 若目录更新时间变化、文件数量变化、或文件 hash 变化,则执行增量重建
70
+ 5. 若增量重建失败,则回退到全量重建;若仍失败,则降级为 `grep`
71
+
72
+ ### Agent 执行要求
73
+
74
+ - 不要因为配置了向量模型,就跳过原有的原文引用校验
75
+ - 向量检索只负责召回候选片段,后续仍需做去重、扩展、强弱匹配和引用核对
76
+ - 若向量召回结果置信度低、来源分散或无明确原文支撑,必须回退到 `grep` 或判定为弱匹配
77
+ - 若用户明确要求“仅依据原文精确匹配”,优先使用 `grep` 或 `grep + 精读`
78
+
79
+ ### 安装时要求
80
+
81
+ 安装或启用本 skill 时,应主动要求用户确认以下内容:
82
+
83
+ 1. 是否已有独立的产品/交付/运维知识库目录
84
+ 2. 该目录的绝对路径是什么
85
+ 3. 若未提供,则明确告知当前将回退使用 skill 自带 `docs/`
86
+
87
+ ### 未配置时的行为
88
+
89
+ - 若已配置外部目录,则默认检索外部目录
90
+ - 若外部目录不存在、无权限或为空,则提示用户并回退到 skill 自带 `docs/`
91
+ - 若用户明确要求“只查外部目录”,则外部目录不可用时应直接报出原因,不自动回退
92
+
93
+ ### 推荐提示语
94
+
95
+ ```text
96
+ 为保证智能客服回答基于你们自己的产品文档,建议在安装或首次启用时配置知识目录绝对路径。若未配置,将暂时使用 skill 自带 docs/ 目录作为默认知识源。
97
+ ```
98
+
15
99
  ## 核心工作流
16
100
 
17
101
  ```
18
- 1.问题归类 ──→ 2.Query改写 ──→ 3.检索(grep) ──→ 4.后处理(去重+扩展) ──→ 5.判定与分级(判定强/弱/无匹配) ──→ 6.自检与生成(校验原文引用并按模板输出)
19
- │ ↑
20
- │ 非技术 │ │ 强/弱匹配
21
- ▼ │
22
- 引导话术结束 └─────────────────────────────────────── 无匹配:ReAct 回步骤2,最多3轮;仍无匹配 → 无法回答话术
102
+ 1.问题归类 ──→ 2.Query改写 ──→ 3.检索(auto: vector 或 grep) ──→ 4.后处理(去重+扩展) ──→ 5.判定与分级(判定强/弱/无匹配) ──→ 6.自检与生成(校验原文引用并按模板输出)
103
+ │ ↑
104
+ │ 非技术 │ │ 强/弱匹配
105
+ ▼ │
106
+ 引导话术结束 └─────────────────────────────────────────────── 无匹配:ReAct 回步骤2,最多3轮;仍无匹配 → 无法回答话术
23
107
  ```
24
108
 
25
109
  ## 标准工作流步骤说明(内部执行,不对外暴露)
@@ -28,7 +112,7 @@ version: 1.0.0
28
112
  |------|----------|------|
29
113
  | 1 | **问题归类**:判断是否属于平台技术/使用/配置范畴等技术类问题;**非技术** → 使用引导话术结束(不进入检索);否则进入步骤 2 | [response-templates 四](templates/response-templates.md#四非技术问题引导)、[指南 步骤1](references/retrieval-guide.md#步骤-1-问题归类) |
30
114
  | 2 | **Query 改写**:口语→文档表述、同义词;按需 HyDE / Step-Back / 意图分解 等 | [指南 步骤2](references/retrieval-guide.md#步骤-2-query-改写) |
31
- | 3 | **检索**:用户明确指定检索目录时,在按指定目录检索;否则默认在 `docs/` 目录下检索 | [指南 步骤3](references/retrieval-guide.md#步骤-3-检索) |
115
+ | 3 | **检索**:用户明确指定检索目录时,按指定目录检索;否则按“当前对话目录 → 安装时配置目录 → 内置 docs/”优先级检索。若已配置向量模型,优先走向量召回;失败时回退 grep | [指南 步骤3](references/retrieval-guide.md#步骤-3-检索) |
32
116
  | 4 | **后处理**:去重 + 上下文扩展 | [指南 步骤4](references/retrieval-guide.md#步骤-4-后处理) |
33
117
  | 5 | **判定与分级**:已匹配材料与原问题强/弱/无匹配;**无匹配 → ReAct 回步骤2**(最多3轮),仍无匹配→无法回答 | [指南 步骤5](references/retrieval-guide.md#步骤-5-判定与分级)、下表 |
34
118
  | 6 | **自检与生成**:严格校验是否有原文引用,按强/弱匹配 模板输出| [response-templates](templates/response-templates.md) |
@@ -56,7 +140,10 @@ version: 1.0.0
56
140
 
57
141
  ## 附加资源
58
142
 
59
- - **文档路径**:`smart-customer-service/docs/`
143
+ - **默认文档路径**:`smart-customer-service/docs/`
144
+ - **推荐外部知识目录配置项**:`SMART_CS_DOCS_DIR`
145
+ - **推荐向量配置项**:`SMART_CS_VECTOR_BASE_URL`、`SMART_CS_VECTOR_API_KEY`、`SMART_CS_VECTOR_MODEL`
146
+ - **安装器配置文件**:`~/.openclaw/codewave-toolkit.json`
60
147
  - 文档目录:[docs/README.md](docs/README.md)
61
148
  - 应答模板:[templates/response-templates.md](templates/response-templates.md)
62
149
  - 检索指南:[references/retrieval-guide.md](references/retrieval-guide.md)