botrun-horse 2.28.4 → 2.29.2
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/bin/bh.mjs +3 -3
- package/bin/commands/help.mjs +1 -1
- package/bin/commands/schema.mjs +15 -19
- package/bin/commands/skill.mjs +163 -0
- package/lib/core/cli-utils.mjs +1 -1
- package/lib/prompt/prompt-search.mjs +9 -27
- package/lib/prompt/prompt-store.mjs +13 -17
- package/lib/prompt/prompts/zero-framework/coding.md +2 -6
- package/lib/prompt/prompts/zero-framework/search.md +44 -6
- package/lib/prompt/prompts/zero-framework/slice.md +2 -6
- package/package.json +8 -3
- package/bin/commands/prompt.mjs +0 -180
package/bin/bh.mjs
CHANGED
|
@@ -166,9 +166,9 @@ switch (command) {
|
|
|
166
166
|
break;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
case '
|
|
170
|
-
const {
|
|
171
|
-
await
|
|
169
|
+
case 'skill': {
|
|
170
|
+
const { cmdSkill } = await import('./commands/skill.mjs');
|
|
171
|
+
await cmdSkill(subcommand, positionals, flags);
|
|
172
172
|
break;
|
|
173
173
|
}
|
|
174
174
|
|
package/bin/commands/help.mjs
CHANGED
|
@@ -79,7 +79,7 @@ botrun-horse — 多專案文件處理系統 CLI v${VERSION}
|
|
|
79
79
|
dag <子指令> DAG 依賴追蹤 (init|status|ready|list)
|
|
80
80
|
search <子指令> 全文檢索 (query) [需 --experimental-sqlite]
|
|
81
81
|
db <子指令> 資料庫管理 (stats|docs|pages) [需 --experimental-sqlite]
|
|
82
|
-
|
|
82
|
+
skill [關鍵字] 技能管理(無參數=全列出,帶關鍵字=模糊搜尋)
|
|
83
83
|
portal <子指令> 智慧入口 (預留)
|
|
84
84
|
ocr <子指令> OCR 辨識 (預留)
|
|
85
85
|
commands 所有指令的結構化 JSON schema
|
package/bin/commands/schema.mjs
CHANGED
|
@@ -201,33 +201,29 @@ export const COMMANDS = {
|
|
|
201
201
|
'node --experimental-sqlite bin/bh.mjs legal ask "問題" --skip-cache --project=legal-cache',
|
|
202
202
|
],
|
|
203
203
|
},
|
|
204
|
-
|
|
205
|
-
name: '
|
|
206
|
-
description: '
|
|
204
|
+
skill: {
|
|
205
|
+
name: 'skill',
|
|
206
|
+
description: '技能管理(無參數=全列出,帶關鍵字=模糊搜尋)— 零框架技能系列',
|
|
207
207
|
requires_sqlite: false,
|
|
208
208
|
subcommands: {
|
|
209
|
-
'(無)': '
|
|
210
|
-
'<關鍵字>': '
|
|
211
|
-
list: '
|
|
212
|
-
show: '
|
|
213
|
-
search: '
|
|
209
|
+
'(無)': '列出所有技能(依系列分組)',
|
|
210
|
+
'<關鍵字>': '模糊搜尋技能(加權全文比對)',
|
|
211
|
+
list: '列出所有技能(同無參數)',
|
|
212
|
+
show: '顯示指定技能全文(支援智慧 fallback 搜尋)',
|
|
213
|
+
search: '模糊搜尋技能(同 <關鍵字>)',
|
|
214
214
|
},
|
|
215
215
|
options: {
|
|
216
|
-
'--series=<名稱>': { type: 'string', description: '[list] 過濾指定系列' },
|
|
217
|
-
'--tag=<標籤>': { type: 'string', description: '[search] 按標籤過濾' },
|
|
218
216
|
'--format=json|text': { type: 'string', default: 'text', description: '輸出格式' },
|
|
219
217
|
'--quiet': { type: 'boolean', default: false, description: '靜默模式' },
|
|
220
218
|
},
|
|
221
219
|
examples: [
|
|
222
|
-
'bh
|
|
223
|
-
'bh
|
|
224
|
-
'bh
|
|
225
|
-
'bh
|
|
226
|
-
'bh
|
|
227
|
-
'bh
|
|
228
|
-
'bh
|
|
229
|
-
'bh prompt search 寫程式',
|
|
230
|
-
'bh prompt show 切 # 智慧 fallback:slug 不存在 → 自動搜尋',
|
|
220
|
+
'bh skill # 列出全部技能',
|
|
221
|
+
'bh skill 零幻覺 # 模糊搜尋「零幻覺」',
|
|
222
|
+
'bh skill show botrun-horse-coding',
|
|
223
|
+
'bh skill show botrun-horse-coding | pbcopy',
|
|
224
|
+
'bh skill show botrun-horse-coding --format=json',
|
|
225
|
+
'bh skill search 換位思考',
|
|
226
|
+
'bh skill show 切片 # 智慧 fallback:slug 不存在 → 自動搜尋',
|
|
231
227
|
],
|
|
232
228
|
},
|
|
233
229
|
portal: {
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// bin/commands/skill.mjs — skill 指令群組(list / show / search)
|
|
2
|
+
// 無參數 → 列出全部技能
|
|
3
|
+
// 不認識的子指令 → 當關鍵字做全文模糊搜尋
|
|
4
|
+
import { timer, jsonOut, emitError, logProgress } from '../../lib/core/cli-utils.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* skill 指令主路由
|
|
8
|
+
*
|
|
9
|
+
* bh skill → list(列出全部)
|
|
10
|
+
* bh skill list → list
|
|
11
|
+
* bh skill show <slug> → show
|
|
12
|
+
* bh skill search <kw> → search
|
|
13
|
+
* bh skill 零幻覺 → 自動搜尋「零幻覺」
|
|
14
|
+
*
|
|
15
|
+
* @param {string} subcommand - list / show / search / 或任意關鍵字
|
|
16
|
+
* @param {string[]} positionals - 位置參數
|
|
17
|
+
* @param {object} flags - CLI 旗標
|
|
18
|
+
*/
|
|
19
|
+
export async function cmdSkill(subcommand, positionals, flags) {
|
|
20
|
+
// --help(可能被 parseArgs 當成 subcommand)
|
|
21
|
+
if (!subcommand || subcommand === '--help' || subcommand === '-h') {
|
|
22
|
+
if (subcommand === '--help' || subcommand === '-h' || flags.help) {
|
|
23
|
+
const { showCommandHelp } = await import('./help.mjs');
|
|
24
|
+
showCommandHelp('skill'); return;
|
|
25
|
+
}
|
|
26
|
+
// 無參數 → 列出全部
|
|
27
|
+
await cmdSkillList(flags);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
switch (subcommand) {
|
|
32
|
+
case 'list': await cmdSkillList(flags); break;
|
|
33
|
+
case 'show': await cmdSkillShow(positionals, flags); break;
|
|
34
|
+
case 'search': await cmdSkillSearch(positionals, flags); break;
|
|
35
|
+
default:
|
|
36
|
+
// 不認識的子指令 → 把 subcommand + positionals 合併為搜尋關鍵字
|
|
37
|
+
await cmdSkillSearch([subcommand, ...positionals], flags);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** list — 列出所有技能(依系列分組) */
|
|
42
|
+
async function cmdSkillList(flags) {
|
|
43
|
+
const elapsed = timer();
|
|
44
|
+
const format = flags.format || 'text';
|
|
45
|
+
const { listPrompts } = await import('../../lib/prompt/prompt-store.mjs');
|
|
46
|
+
|
|
47
|
+
const all = listPrompts();
|
|
48
|
+
|
|
49
|
+
if (format === 'json') {
|
|
50
|
+
const data = all.map(p => ({
|
|
51
|
+
slug: p.slug,
|
|
52
|
+
name: p.meta.name || p.slug,
|
|
53
|
+
description: p.meta.description || null,
|
|
54
|
+
body: p.body,
|
|
55
|
+
}));
|
|
56
|
+
process.stdout.write(jsonOut('skill list', { count: data.length, skills: data }, elapsed()));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < all.length; i++) {
|
|
61
|
+
if (i > 0) process.stdout.write('\n');
|
|
62
|
+
process.stdout.write(all[i].rawContent + '\n');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** show — 顯示單一技能全文 */
|
|
67
|
+
async function cmdSkillShow(positionals, flags) {
|
|
68
|
+
const elapsed = timer();
|
|
69
|
+
const format = flags.format || 'text';
|
|
70
|
+
const slug = positionals[0];
|
|
71
|
+
|
|
72
|
+
if (!slug) {
|
|
73
|
+
emitError('skill show', '請提供技能 slug。用法: bh skill show <slug>', format);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const { getPrompt, listPrompts } = await import('../../lib/prompt/prompt-store.mjs');
|
|
78
|
+
let skill = getPrompt(slug);
|
|
79
|
+
|
|
80
|
+
// 智慧 fallback:精確 slug 找不到 → 自動轉搜尋
|
|
81
|
+
if (!skill) {
|
|
82
|
+
const { searchPrompts } = await import('../../lib/prompt/prompt-search.mjs');
|
|
83
|
+
const all = listPrompts();
|
|
84
|
+
const results = searchPrompts(all, slug);
|
|
85
|
+
|
|
86
|
+
if (results.length === 0) {
|
|
87
|
+
emitError('skill show', `找不到技能「${slug}」,搜尋也無結果`, format);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (results.length === 1) {
|
|
92
|
+
skill = results[0].prompt;
|
|
93
|
+
logProgress(`提示: slug「${slug}」不存在,自動搜尋命中「${skill.slug}」`, flags);
|
|
94
|
+
} else {
|
|
95
|
+
// 多筆命中 → 列出候選
|
|
96
|
+
if (format === 'json') {
|
|
97
|
+
const data = results.map(r => ({ slug: r.prompt.slug, title: r.prompt.meta.title, score: r.score }));
|
|
98
|
+
process.stdout.write(jsonOut('skill show', { match: 'multiple', candidates: data }, elapsed()));
|
|
99
|
+
} else {
|
|
100
|
+
process.stderr.write(`找不到精確 slug「${slug}」,搜尋到 ${results.length} 筆候選:\n`);
|
|
101
|
+
for (const r of results) {
|
|
102
|
+
process.stderr.write(` ${r.prompt.slug.padEnd(16)} ${r.prompt.meta.title} (分數: ${r.score})\n`);
|
|
103
|
+
}
|
|
104
|
+
process.stderr.write(`\n請使用精確 slug,例如: bh skill show ${results[0].prompt.slug}\n`);
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (format === 'json') {
|
|
111
|
+
const data = {
|
|
112
|
+
slug: skill.slug,
|
|
113
|
+
name: skill.meta.name || skill.slug,
|
|
114
|
+
description: skill.meta.description || null,
|
|
115
|
+
body: skill.body,
|
|
116
|
+
};
|
|
117
|
+
process.stdout.write(jsonOut('skill show', data, elapsed()));
|
|
118
|
+
} else {
|
|
119
|
+
process.stdout.write(skill.rawContent + '\n');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** search — 模糊搜尋技能(加權全文比對) */
|
|
124
|
+
async function cmdSkillSearch(positionals, flags) {
|
|
125
|
+
const elapsed = timer();
|
|
126
|
+
const format = flags.format || 'text';
|
|
127
|
+
const query = positionals.join(' ');
|
|
128
|
+
|
|
129
|
+
if (!query) {
|
|
130
|
+
emitError('skill search', '請提供搜尋關鍵字。用法: bh skill search <關鍵字>', format);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const { listPrompts } = await import('../../lib/prompt/prompt-store.mjs');
|
|
135
|
+
const { searchPrompts } = await import('../../lib/prompt/prompt-search.mjs');
|
|
136
|
+
|
|
137
|
+
const all = listPrompts();
|
|
138
|
+
const results = searchPrompts(all, query);
|
|
139
|
+
|
|
140
|
+
if (format === 'json') {
|
|
141
|
+
const data = results.map(r => ({
|
|
142
|
+
slug: r.prompt.slug,
|
|
143
|
+
name: r.prompt.meta.name || r.prompt.slug,
|
|
144
|
+
description: r.prompt.meta.description || null,
|
|
145
|
+
score: r.score,
|
|
146
|
+
body: r.prompt.body,
|
|
147
|
+
}));
|
|
148
|
+
process.stdout.write(jsonOut('skill search', { query, count: data.length, results: data }, elapsed()));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (results.length === 0) {
|
|
153
|
+
process.stdout.write(`搜尋「${query}」:無結果\n`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
logProgress(`搜尋「${query}」:${results.length} 筆結果`, flags);
|
|
158
|
+
|
|
159
|
+
for (let i = 0; i < results.length; i++) {
|
|
160
|
+
if (i > 0) process.stdout.write('\n');
|
|
161
|
+
process.stdout.write(results[i].prompt.rawContent + '\n');
|
|
162
|
+
}
|
|
163
|
+
}
|
package/lib/core/cli-utils.mjs
CHANGED
|
@@ -25,7 +25,7 @@ export function parseArgs(argv) {
|
|
|
25
25
|
const positionals = [];
|
|
26
26
|
|
|
27
27
|
// 判斷需跳過子指令的指令群組
|
|
28
|
-
const hasSubcommand = ['dag', 'db', 'doc', 'writing', 'search', 'portal', 'ocr', 'gemini', '
|
|
28
|
+
const hasSubcommand = ['dag', 'db', 'doc', 'writing', 'search', 'portal', 'ocr', 'gemini', 'skill'].includes(command);
|
|
29
29
|
const flagStart = hasSubcommand ? 2 : 1;
|
|
30
30
|
for (const arg of args.slice(flagStart)) {
|
|
31
31
|
if (arg.startsWith('--')) {
|
|
@@ -1,49 +1,31 @@
|
|
|
1
1
|
// lib/prompt/prompt-search.mjs — 加權評分模糊搜尋
|
|
2
|
-
// 依 slug →
|
|
2
|
+
// 依 slug/name → description → content 順序加權比對
|
|
3
|
+
// SKILL.md 標準 frontmatter(name, description)
|
|
3
4
|
|
|
4
|
-
const WEIGHTS = { slug: 10,
|
|
5
|
+
const WEIGHTS = { slug: 10, description: 5, content: 1 };
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* 加權模糊搜尋
|
|
8
|
-
* @param {Array<{ slug, meta, body }>} prompts -
|
|
9
|
+
* @param {Array<{ slug, meta, body }>} prompts - 所有技能
|
|
9
10
|
* @param {string} query - 搜尋關鍵字
|
|
10
|
-
* @param {{ tag?: string }} [opts] - 過濾選項
|
|
11
11
|
* @returns {Array<{ prompt, score }>} 按分數降序排列
|
|
12
12
|
*/
|
|
13
|
-
export function searchPrompts(prompts, query
|
|
13
|
+
export function searchPrompts(prompts, query) {
|
|
14
14
|
const q = query.toLowerCase();
|
|
15
15
|
const results = [];
|
|
16
16
|
|
|
17
17
|
for (const p of prompts) {
|
|
18
|
-
// --tag 前置過濾
|
|
19
|
-
if (opts.tag) {
|
|
20
|
-
const tags = (p.meta.tags || []).map(t => t.toLowerCase());
|
|
21
|
-
if (!tags.some(t => t.includes(opts.tag.toLowerCase()))) continue;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
18
|
let score = 0;
|
|
25
19
|
const slug = p.slug.toLowerCase();
|
|
26
|
-
const
|
|
27
|
-
const title = (p.meta.title || '').toLowerCase();
|
|
28
|
-
const tags = (p.meta.tags || []).map(t => t.toLowerCase());
|
|
29
|
-
const series = (p.meta.series || '').toLowerCase();
|
|
20
|
+
const description = (p.meta.description || '').toLowerCase();
|
|
30
21
|
const body = p.body.toLowerCase();
|
|
31
22
|
|
|
32
|
-
// slug 精確匹配 → 滿分;部分匹配 → 6 分
|
|
23
|
+
// slug/name 精確匹配 → 滿分;部分匹配 → 6 分
|
|
33
24
|
if (slug === q) score += WEIGHTS.slug;
|
|
34
25
|
else if (slug.includes(q)) score += 6;
|
|
35
26
|
|
|
36
|
-
//
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
// title 包含
|
|
40
|
-
if (title.includes(q)) score += WEIGHTS.title;
|
|
41
|
-
|
|
42
|
-
// tag 匹配
|
|
43
|
-
if (tags.some(t => t.includes(q) || q.includes(t))) score += WEIGHTS.tag;
|
|
44
|
-
|
|
45
|
-
// series 匹配
|
|
46
|
-
if (series.includes(q)) score += WEIGHTS.series;
|
|
27
|
+
// description 包含(SKILL.md 標準欄位)
|
|
28
|
+
if (description.includes(q)) score += WEIGHTS.description;
|
|
47
29
|
|
|
48
30
|
// 全文內容包含
|
|
49
31
|
if (body.includes(q)) score += WEIGHTS.content;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
// lib/prompt/prompt-store.mjs —
|
|
1
|
+
// lib/prompt/prompt-store.mjs — 技能 Repository 層
|
|
2
2
|
// 掃描 lib/prompt/prompts/ 目錄,解析 YAML frontmatter,提供 listPrompts / getPrompt
|
|
3
3
|
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { root } from '../core/paths.mjs';
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/** 技能目錄(相對於專案根目錄) */
|
|
9
9
|
const PROMPTS_DIR = () => path.join(root(), 'lib', 'prompt', 'prompts');
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* 零依賴 YAML frontmatter 解析
|
|
13
|
-
*
|
|
13
|
+
* SKILL.md 標準欄位:name, description
|
|
14
14
|
* @param {string} content - .md 檔案原始內容
|
|
15
15
|
* @returns {{ meta: object, body: string }}
|
|
16
16
|
*/
|
|
@@ -23,12 +23,7 @@ export function parseFrontmatter(content) {
|
|
|
23
23
|
const kv = line.match(/^(\w[\w-]*):\s*(.+)$/);
|
|
24
24
|
if (!kv) continue;
|
|
25
25
|
const [, key, rawVal] = kv;
|
|
26
|
-
|
|
27
|
-
if (rawVal.startsWith('[') && rawVal.endsWith(']')) {
|
|
28
|
-
meta[key] = rawVal.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean);
|
|
29
|
-
} else {
|
|
30
|
-
meta[key] = rawVal.trim();
|
|
31
|
-
}
|
|
26
|
+
meta[key] = rawVal.trim();
|
|
32
27
|
}
|
|
33
28
|
return { meta, body: match[2] };
|
|
34
29
|
}
|
|
@@ -54,27 +49,28 @@ function scanMarkdownFiles(dir, baseDir) {
|
|
|
54
49
|
}
|
|
55
50
|
|
|
56
51
|
/**
|
|
57
|
-
*
|
|
52
|
+
* 載入並解析單一技能檔案
|
|
53
|
+
* SKILL.md 標準 frontmatter:name, description
|
|
58
54
|
* @param {string} relPath - 相對於 prompts/ 的路徑(如 zero-framework/coding.md)
|
|
59
55
|
* @param {string} baseDir - prompts/ 目錄絕對路徑
|
|
60
|
-
* @returns {{ slug, meta, body,
|
|
56
|
+
* @returns {{ slug, meta, body, rawContent, filePath }}
|
|
61
57
|
*/
|
|
62
58
|
function loadPromptFile(relPath, baseDir) {
|
|
63
59
|
const fullPath = path.join(baseDir, relPath);
|
|
64
60
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
65
61
|
const { meta, body } = parseFrontmatter(content);
|
|
66
|
-
const slug = path.basename(relPath, '.md');
|
|
67
|
-
const series = path.dirname(relPath) === '.' ? null : path.dirname(relPath);
|
|
62
|
+
const slug = meta.name || path.basename(relPath, '.md');
|
|
68
63
|
return {
|
|
69
64
|
slug,
|
|
70
|
-
meta
|
|
65
|
+
meta,
|
|
71
66
|
body: body.trim(),
|
|
67
|
+
rawContent: content.trimEnd(),
|
|
72
68
|
filePath: fullPath,
|
|
73
69
|
};
|
|
74
70
|
}
|
|
75
71
|
|
|
76
72
|
/**
|
|
77
|
-
*
|
|
73
|
+
* 列出所有技能
|
|
78
74
|
* @returns {Array<{ slug, meta, body, filePath }>}
|
|
79
75
|
*/
|
|
80
76
|
export function listPrompts() {
|
|
@@ -84,8 +80,8 @@ export function listPrompts() {
|
|
|
84
80
|
}
|
|
85
81
|
|
|
86
82
|
/**
|
|
87
|
-
*
|
|
88
|
-
* @param {string} slug -
|
|
83
|
+
* 取得單一技能(精確 slug 匹配檔名)
|
|
84
|
+
* @param {string} slug - 技能 slug(如 'coding')
|
|
89
85
|
* @returns {{ slug, meta, body, filePath } | null}
|
|
90
86
|
*/
|
|
91
87
|
export function getPrompt(slug) {
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
tags: [換位思考, 三方案, 六軟工, BDD, TDD, SOLID, DRY, KISS, DDD, HTTPS代理, 待辦]
|
|
5
|
-
aliases: [coding, 扣頂, 寫程式, 開發]
|
|
2
|
+
name: botrun-horse-coding
|
|
3
|
+
description: 零框架扣頂 coding — 換位思考、查新文件、三方案打分、六軟工實作、DAG 待辦追蹤、HTTPS 代理相容
|
|
6
4
|
---
|
|
7
5
|
|
|
8
|
-
# 零框架扣頂 coding
|
|
9
|
-
|
|
10
6
|
🔄 換位思考:先掃描資料與提問,推理需求背後動機,具體列出 5 個潛在動機
|
|
11
7
|
📡 新文件:根據提問與需求上網查詢最新 SDK API 文件,因為隨時都在變化
|
|
12
8
|
🥊 三方案:提出 3 替代方案多維度打分,以使用者為中心選最適合方案
|
|
@@ -1,12 +1,50 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
tags: [搜尋, grounding, 引證, URL, 網路搜尋]
|
|
5
|
-
aliases: [search, 搜尋, 查詢, 找資料]
|
|
2
|
+
name: botrun-horse-search
|
|
3
|
+
description: 零框架搜尋 — Google Search grounding 搜尋接地、引證 URL、零幻覺多步驟交叉驗證問答
|
|
6
4
|
---
|
|
7
5
|
|
|
8
|
-
# 零框架搜尋
|
|
9
|
-
|
|
10
6
|
🔍 搜尋接地:啟用 Google Search grounding,確保回應以真實搜尋結果為依據
|
|
11
7
|
🔗 引證 URL:回應必須包含引證來源的 URL,讓使用者可驗證
|
|
12
8
|
🚫 零幻覺:不捏造不存在的資料,所有事實陳述須有搜尋結果佐證
|
|
9
|
+
|
|
10
|
+
## 系統提示詞模板
|
|
11
|
+
|
|
12
|
+
變數說明:
|
|
13
|
+
- `$0` / `{{機關單位}}` — 回答時的專業領域角色,例如「經濟部產業發展署」「衛生福利部」「內政部營建署」
|
|
14
|
+
- `$1` / `{{使用者提問}}` — 實際要搜尋回答的問題
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
<系統提示工程>
|
|
18
|
+
. 你是臺灣「{{機關單位}}」的資深專業人員,回答時不用說自己是誰,但要溫暖,換位思考使用者真正能聽懂的,必須用大白話淺顯的解說
|
|
19
|
+
. 請你分成多步驟進行
|
|
20
|
+
001 上網搜尋或抓取,並列出所有相關文字引證證據,絕對不可延伸文字,只能原文照抄引證
|
|
21
|
+
002 基於 001 嚴格的引證原文文字不能竄改,然後進行第一次初稿回答
|
|
22
|
+
003 假定 002 是很多幻覺很多錯的,驗證並且推翻 002 錯誤的部分,要請再次上網搜尋證據,只要有超出證據之外的,予以推翻,此時必須注意有些規定是正面表列規定,並沒有負向表列沒做會怎樣,不要弄錯規定
|
|
23
|
+
004 彙整最終回答,此時,每一句回答都必須引證001證據
|
|
24
|
+
回應時,每個步驟都請用以下XML標籤分隔明確
|
|
25
|
+
<001>
|
|
26
|
+
...
|
|
27
|
+
</001>
|
|
28
|
+
<002>
|
|
29
|
+
...
|
|
30
|
+
</002>
|
|
31
|
+
...
|
|
32
|
+
<004>
|
|
33
|
+
...
|
|
34
|
+
</004>
|
|
35
|
+
</系統提示工程>
|
|
36
|
+
|
|
37
|
+
<使用者提問>
|
|
38
|
+
{{使用者提問}}
|
|
39
|
+
</使用者提問>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 用法範例
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 直接帶入變數使用
|
|
46
|
+
bh gemini ask --system-prompt "你是臺灣「經濟部產業發展署」的資深專業人員..." "我如何辦理工廠危險品停用解列?"
|
|
47
|
+
|
|
48
|
+
# 搭配 pipe
|
|
49
|
+
echo "我的工廠預計4/1不再使用工廠危險品,如何辦理停用解列?" | bh gemini ask --project=ida
|
|
50
|
+
```
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
tags: [切片, 零幻覺引證, PDF, Office, 頁碼, 純文字]
|
|
5
|
-
aliases: [slice, 切片, 切頁, 文件處理]
|
|
2
|
+
name: botrun-horse-slice
|
|
3
|
+
description: 零框架切片 — PDF/Office 切頁保留頁碼轉純文字,零幻覺引證來源含網址、檔名、頁碼、原始文字
|
|
6
4
|
---
|
|
7
5
|
|
|
8
|
-
# 零框架切片
|
|
9
|
-
|
|
10
6
|
🔪 切片:如果資料檔案是 PDF 或 Office,記得先切頁(保留頁碼可引證)轉為純文字才去處理
|
|
11
7
|
📎 零幻覺引證:回應須引證來源,包含:網址、檔案名稱、頁碼、時間點、不竄改的原始文字
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "botrun-horse",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "多專案文件處理系統 CLI — AI
|
|
3
|
+
"version": "2.29.2",
|
|
4
|
+
"description": "多專案文件處理系統 CLI — AI 技能管理、文件擷取、公文撰寫",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"bh": "./bin/bh.mjs"
|
|
7
|
+
"bh": "./bin/bh.mjs",
|
|
8
|
+
"botrun-horse": "./bin/bh.mjs"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
11
|
"test": "node --test test/**/*.test.mjs",
|
|
@@ -24,6 +25,10 @@
|
|
|
24
25
|
"dag:status": "node src/infrastructure/dag-tracker.mjs status",
|
|
25
26
|
"dag:ready": "node src/infrastructure/dag-tracker.mjs ready"
|
|
26
27
|
},
|
|
28
|
+
"overrides": {
|
|
29
|
+
"rimraf": "^6.1.3",
|
|
30
|
+
"node-fetch": "npm:node-fetch-native@^1.6.7"
|
|
31
|
+
},
|
|
27
32
|
"dependencies": {
|
|
28
33
|
"@ai-sdk/openai-compatible": "^2.0.30",
|
|
29
34
|
"@google/genai": "^1.43.0",
|
package/bin/commands/prompt.mjs
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
// bin/commands/prompt.mjs — prompt 指令群組(list / show / search)
|
|
2
|
-
// 無參數 → 列出全部提示詞
|
|
3
|
-
// 不認識的子指令 → 當關鍵字做全文模糊搜尋
|
|
4
|
-
import { timer, jsonOut, emitError, logProgress } from '../../lib/core/cli-utils.mjs';
|
|
5
|
-
|
|
6
|
-
/** 去掉 body 開頭的 # 標題行(CLI 標頭已顯示,避免重複) */
|
|
7
|
-
function stripTitle(body) {
|
|
8
|
-
return body.replace(/^#\s+.+\n+/, '');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* prompt 指令主路由
|
|
13
|
-
*
|
|
14
|
-
* bh prompt → list(列出全部)
|
|
15
|
-
* bh prompt list → list
|
|
16
|
-
* bh prompt show <slug> → show
|
|
17
|
-
* bh prompt search <kw> → search
|
|
18
|
-
* bh prompt 零幻覺 → 自動搜尋「零幻覺」
|
|
19
|
-
*
|
|
20
|
-
* @param {string} subcommand - list / show / search / 或任意關鍵字
|
|
21
|
-
* @param {string[]} positionals - 位置參數
|
|
22
|
-
* @param {object} flags - CLI 旗標
|
|
23
|
-
*/
|
|
24
|
-
export async function cmdPrompt(subcommand, positionals, flags) {
|
|
25
|
-
// --help(可能被 parseArgs 當成 subcommand)
|
|
26
|
-
if (!subcommand || subcommand === '--help' || subcommand === '-h') {
|
|
27
|
-
if (subcommand === '--help' || subcommand === '-h' || flags.help) {
|
|
28
|
-
const { showCommandHelp } = await import('./help.mjs');
|
|
29
|
-
showCommandHelp('prompt'); return;
|
|
30
|
-
}
|
|
31
|
-
// 無參數 → 列出全部
|
|
32
|
-
await cmdPromptList(flags);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
switch (subcommand) {
|
|
37
|
-
case 'list': await cmdPromptList(flags); break;
|
|
38
|
-
case 'show': await cmdPromptShow(positionals, flags); break;
|
|
39
|
-
case 'search': await cmdPromptSearch(positionals, flags); break;
|
|
40
|
-
default:
|
|
41
|
-
// 不認識的子指令 → 把 subcommand + positionals 合併為搜尋關鍵字
|
|
42
|
-
await cmdPromptSearch([subcommand, ...positionals], flags);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** list — 列出所有提示詞(依系列分組) */
|
|
47
|
-
async function cmdPromptList(flags) {
|
|
48
|
-
const elapsed = timer();
|
|
49
|
-
const format = flags.format || 'text';
|
|
50
|
-
const { listPrompts } = await import('../../lib/prompt/prompt-store.mjs');
|
|
51
|
-
|
|
52
|
-
const all = listPrompts();
|
|
53
|
-
const filtered = flags.series
|
|
54
|
-
? all.filter(p => p.meta.series === flags.series)
|
|
55
|
-
: all;
|
|
56
|
-
|
|
57
|
-
if (format === 'json') {
|
|
58
|
-
const data = filtered.map(p => ({
|
|
59
|
-
slug: p.slug,
|
|
60
|
-
title: p.meta.title,
|
|
61
|
-
series: p.meta.series,
|
|
62
|
-
tags: p.meta.tags || [],
|
|
63
|
-
aliases: p.meta.aliases || [],
|
|
64
|
-
body: p.body,
|
|
65
|
-
}));
|
|
66
|
-
process.stdout.write(jsonOut('prompt list', { count: data.length, prompts: data }, elapsed()));
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
for (let i = 0; i < filtered.length; i++) {
|
|
71
|
-
const p = filtered[i];
|
|
72
|
-
if (i > 0) process.stdout.write('\n---\n\n');
|
|
73
|
-
process.stdout.write(`[${p.meta.series || ''}/${p.slug}] ${p.meta.title || p.slug}\n\n`);
|
|
74
|
-
process.stdout.write(stripTitle(p.body) + '\n');
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** show — 顯示單一提示詞全文 */
|
|
79
|
-
async function cmdPromptShow(positionals, flags) {
|
|
80
|
-
const elapsed = timer();
|
|
81
|
-
const format = flags.format || 'text';
|
|
82
|
-
const slug = positionals[0];
|
|
83
|
-
|
|
84
|
-
if (!slug) {
|
|
85
|
-
emitError('prompt show', '請提供提示詞 slug。用法: bh prompt show <slug>', format);
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const { getPrompt, listPrompts } = await import('../../lib/prompt/prompt-store.mjs');
|
|
90
|
-
let prompt = getPrompt(slug);
|
|
91
|
-
|
|
92
|
-
// 智慧 fallback:精確 slug 找不到 → 自動轉搜尋
|
|
93
|
-
if (!prompt) {
|
|
94
|
-
const { searchPrompts } = await import('../../lib/prompt/prompt-search.mjs');
|
|
95
|
-
const all = listPrompts();
|
|
96
|
-
const results = searchPrompts(all, slug);
|
|
97
|
-
|
|
98
|
-
if (results.length === 0) {
|
|
99
|
-
emitError('prompt show', `找不到提示詞「${slug}」,搜尋也無結果`, format);
|
|
100
|
-
process.exit(1);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (results.length === 1) {
|
|
104
|
-
prompt = results[0].prompt;
|
|
105
|
-
logProgress(`提示: slug「${slug}」不存在,自動搜尋命中「${prompt.slug}」`, flags);
|
|
106
|
-
} else {
|
|
107
|
-
// 多筆命中 → 列出候選
|
|
108
|
-
if (format === 'json') {
|
|
109
|
-
const data = results.map(r => ({ slug: r.prompt.slug, title: r.prompt.meta.title, score: r.score }));
|
|
110
|
-
process.stdout.write(jsonOut('prompt show', { match: 'multiple', candidates: data }, elapsed()));
|
|
111
|
-
} else {
|
|
112
|
-
process.stderr.write(`找不到精確 slug「${slug}」,搜尋到 ${results.length} 筆候選:\n`);
|
|
113
|
-
for (const r of results) {
|
|
114
|
-
process.stderr.write(` ${r.prompt.slug.padEnd(16)} ${r.prompt.meta.title} (分數: ${r.score})\n`);
|
|
115
|
-
}
|
|
116
|
-
process.stderr.write(`\n請使用精確 slug,例如: bh prompt show ${results[0].prompt.slug}\n`);
|
|
117
|
-
}
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (format === 'json') {
|
|
123
|
-
const data = {
|
|
124
|
-
slug: prompt.slug,
|
|
125
|
-
title: prompt.meta.title,
|
|
126
|
-
series: prompt.meta.series,
|
|
127
|
-
tags: prompt.meta.tags || [],
|
|
128
|
-
aliases: prompt.meta.aliases || [],
|
|
129
|
-
body: prompt.body,
|
|
130
|
-
};
|
|
131
|
-
process.stdout.write(jsonOut('prompt show', data, elapsed()));
|
|
132
|
-
} else {
|
|
133
|
-
process.stdout.write(stripTitle(prompt.body) + '\n');
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** search — 模糊搜尋提示詞(加權全文比對) */
|
|
138
|
-
async function cmdPromptSearch(positionals, flags) {
|
|
139
|
-
const elapsed = timer();
|
|
140
|
-
const format = flags.format || 'text';
|
|
141
|
-
const query = positionals.join(' ');
|
|
142
|
-
|
|
143
|
-
if (!query) {
|
|
144
|
-
emitError('prompt search', '請提供搜尋關鍵字。用法: bh prompt search <關鍵字>', format);
|
|
145
|
-
process.exit(1);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const { listPrompts } = await import('../../lib/prompt/prompt-store.mjs');
|
|
149
|
-
const { searchPrompts } = await import('../../lib/prompt/prompt-search.mjs');
|
|
150
|
-
|
|
151
|
-
const all = listPrompts();
|
|
152
|
-
const results = searchPrompts(all, query, { tag: flags.tag });
|
|
153
|
-
|
|
154
|
-
if (format === 'json') {
|
|
155
|
-
const data = results.map(r => ({
|
|
156
|
-
slug: r.prompt.slug,
|
|
157
|
-
title: r.prompt.meta.title,
|
|
158
|
-
series: r.prompt.meta.series,
|
|
159
|
-
score: r.score,
|
|
160
|
-
tags: r.prompt.meta.tags || [],
|
|
161
|
-
body: r.prompt.body,
|
|
162
|
-
}));
|
|
163
|
-
process.stdout.write(jsonOut('prompt search', { query, count: data.length, results: data }, elapsed()));
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (results.length === 0) {
|
|
168
|
-
process.stdout.write(`搜尋「${query}」:無結果\n`);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
logProgress(`搜尋「${query}」:${results.length} 筆結果`, flags);
|
|
173
|
-
|
|
174
|
-
for (let i = 0; i < results.length; i++) {
|
|
175
|
-
const r = results[i];
|
|
176
|
-
if (i > 0) process.stdout.write('\n---\n\n');
|
|
177
|
-
process.stdout.write(`[${r.prompt.meta.series || ''}/${r.prompt.slug}] ${r.prompt.meta.title}\n\n`);
|
|
178
|
-
process.stdout.write(stripTitle(r.prompt.body) + '\n');
|
|
179
|
-
}
|
|
180
|
-
}
|