playlist-admin 2.15.36 → 2.15.38

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.
@@ -0,0 +1,59 @@
1
+ ---
2
+ name: podcast-knowledge
3
+ description: Use when the user asks a question about podcast transcripts, study materials, exam questions, or course content. Covers both podcast RAG (rag/query.py) and study RAG (rag/study_query.py). Trigger on words like podcast, 逐字稿, 節目, 試題, 課程, 齒輪, 螺栓, 機件原理, 機械力學, 統測, 四技二專, RAG.
4
+ ---
5
+
6
+ # 知識檢索(Podcast + Study RAG)
7
+
8
+ 兩個 ChromaDB collection,同一個 DB:
9
+ - **podcasts**:RSS podcast 逐字稿(`rag/query.py`)
10
+ - **study**:PDF 試題/課本 + 課程影片逐字稿(`rag/study_query.py`)
11
+
12
+ ## 流程
13
+
14
+ 1. 確認 Ollama 在跑:`Test-NetConnection localhost -Port 11434`
15
+ 2. 判斷查詢類型:
16
+ - **Podcast 相關**(「某集講什麼」「節目有沒有提到」)→ 用 `rag/query.py`
17
+ - **試題/課程相關**(「某題怎麼解」「齒輪有哪些」「機件原理」)→ 用 `rag/study_query.py`
18
+ 3. 查詢(在專案根目錄執行):
19
+
20
+ **Podcast:**
21
+ ```
22
+ python rag/query.py "<問題>" --json --no-full --topk 8
23
+ playlist-admin rag query "<問題>" --no-full --topk 8
24
+ ```
25
+
26
+ **Study:**
27
+ ```
28
+ python rag/study_query.py "<問題>" --topk 5
29
+ python rag/study_query.py "<問題>" --category 機械 --topk 3
30
+ playlist-admin study query "<問題>" --topk 5
31
+ ```
32
+
33
+ 4. 依 `similarity`(1.0 最高)挑 2–3 個命中回答。
34
+ 5. 用繁體中文回答,列出引用來源。
35
+
36
+ ## Study RAG 特有參數
37
+
38
+ - `--category <字串>`:只搜特定分類(如「機械力學」「112學年度」「課程/機械群許凱機件原理」)
39
+ - `--answer`:用 Ollama 生成回答(需要 qwen2.5:7b)
40
+ - `--json`:JSON 輸出(含 similarity、category、file、chunk)
41
+
42
+ ## 注意
43
+
44
+ - 若 collection 不存在:`python rag/study_build.py`(全量 ~15min)或 `playlist-admin study build`
45
+ - Podcast 重建:`playlist-admin rag build` 或 `python rag/build_db.py`
46
+ - 路徑與 DB 從 config.json 自動解析(`rag/_resolve.py`),不要假設路徑
47
+ - 問題保持原意,可直接用使用者原句
48
+
49
+ ## 安裝(opencode v2 全域生效)
50
+
51
+ 本 skill 單一真相來源:repo 內 `.opencode/skills/podcast-knowledge/SKILL.md`。
52
+ npm 包(`playlist-admin`)與 GUI 安裝程式都會附上,裝完後執行:
53
+
54
+ ```
55
+ playlist-admin skill install
56
+ ```
57
+
58
+ 會複製到 opencode v2 全域路徑 `~/.config/opencode/skills/podcast-knowledge/SKILL.md`
59
+ (opencode 重啟後生效)。其他位置:`playlist-admin skill list | path`。
package/cli/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn, spawnSync } from 'node:child_process';
3
3
  import fs from 'node:fs';
4
+ import os from 'node:os';
4
5
  import path from 'node:path';
5
6
  import { fileURLToPath } from 'node:url';
6
7
 
@@ -23,6 +24,9 @@ Usage:
23
24
  playlist-admin rag query "問題" [--topk N] [--show 節目] [--json]
24
25
  playlist-admin study build [--reset] Build study RAG (PDF + 課程)
25
26
  playlist-admin study query "問題" [--topk N] [--category X] [--json]
27
+ playlist-admin skill list List bundled opencode skills
28
+ playlist-admin skill install [--dir X] Install skill(s) to opencode (~/.config/opencode/skills)
29
+ playlist-admin skill path [name] Print installed path of a skill
26
30
  `;
27
31
 
28
32
  function projectRoot() {
@@ -139,6 +143,101 @@ function runStudy(args) {
139
143
  forwardPy([script, ...args.slice(1)]);
140
144
  }
141
145
 
146
+ // --- opencode skill ------------------------------------------------------
147
+ // 單一真相來源:.opencode/skills/<name>/SKILL.md(專案內)
148
+ // 發佈物內位置:npm 包同路徑;GUI 安裝目錄下 skills/<name>/SKILL.md。
149
+ // 安裝目標(opencode v2 官方):~/.config/opencode/skills/<name>/SKILL.md
150
+
151
+ function skillBaseDirs() {
152
+ const dirs = [];
153
+ for (const r of [projectRoot(), packageRoot()].filter(Boolean)) {
154
+ for (const sub of ['.opencode/skills', 'skills']) {
155
+ const d = path.join(r, sub);
156
+ if (fs.existsSync(d)) dirs.push(d);
157
+ }
158
+ }
159
+ // GUI 安裝目錄(Inno DefaultDirName {autopf}\playlist-admin)
160
+ const pf = [process.env.ProgramFiles, process.env['ProgramFiles(x86)']].filter(Boolean);
161
+ for (const p of pf) {
162
+ const d = path.join(p, 'playlist-admin', 'skills');
163
+ if (fs.existsSync(d)) dirs.push(d);
164
+ }
165
+ return [...new Set(dirs)];
166
+ }
167
+
168
+ function findSkills() {
169
+ // [{name, dir}],同名以前面的來源優先
170
+ const out = [];
171
+ const seen = new Set();
172
+ for (const base of skillBaseDirs()) {
173
+ let entries = [];
174
+ try {
175
+ entries = fs.readdirSync(base, { withFileTypes: true });
176
+ } catch {
177
+ continue;
178
+ }
179
+ for (const e of entries) {
180
+ if (!e.isDirectory() || seen.has(e.name)) continue;
181
+ if (!fs.existsSync(path.join(base, e.name, 'SKILL.md'))) continue;
182
+ seen.add(e.name);
183
+ out.push({ name: e.name, dir: path.join(base, e.name) });
184
+ }
185
+ }
186
+ return out;
187
+ }
188
+
189
+ function globalSkillsDir() {
190
+ return path.join(os.homedir(), '.config', 'opencode', 'skills');
191
+ }
192
+
193
+ function runSkill(args) {
194
+ const sub = args[0] || 'list';
195
+ if (sub === 'list') {
196
+ const skills = findSkills();
197
+ if (skills.length === 0) {
198
+ console.error('找不到內附的 skill(缺 .opencode/skills/ 或 skills/)');
199
+ process.exit(1);
200
+ }
201
+ for (const s of skills) console.log(`${s.name}\t${s.dir}`);
202
+ return;
203
+ }
204
+ if (sub === 'path') {
205
+ const name = args[1] || 'podcast-knowledge';
206
+ console.log(path.join(globalSkillsDir(), name, 'SKILL.md'));
207
+ return;
208
+ }
209
+ if (sub === 'install') {
210
+ // playlist-admin skill install [name] [--dir X](無 name = 全部)
211
+ const dirFlag = args.find((a) => a.startsWith('--dir='));
212
+ const dirIdx = args.indexOf('--dir');
213
+ const dirVal =
214
+ dirFlag?.slice('--dir='.length) ||
215
+ (dirIdx >= 0 && args[dirIdx + 1] ? args[dirIdx + 1] : null);
216
+ const rest = args.slice(1).filter((a, i, arr) => {
217
+ if (a.startsWith('--')) return false;
218
+ const prev = arr[i - 1];
219
+ if (prev === '--dir') return false; // --dir 的值不是 skill 名
220
+ return true;
221
+ });
222
+ const targetBase = dirVal || globalSkillsDir();
223
+ const skills = findSkills().filter((s) => rest.length === 0 || rest.includes(s.name));
224
+ if (skills.length === 0) {
225
+ console.error(`找不到 skill: ${rest.join(' ') || '(空)'}。先跑 playlist-admin skill list`);
226
+ process.exit(1);
227
+ }
228
+ for (const s of skills) {
229
+ const dest = path.join(targetBase, s.name);
230
+ fs.mkdirSync(dest, { recursive: true });
231
+ fs.cpSync(s.dir, dest, { recursive: true });
232
+ console.log(`已安裝 ${s.name} -> ${path.join(dest, 'SKILL.md')}`);
233
+ }
234
+ console.log('opencode 重啟後生效(全域: ~/.config/opencode/skills/<name>/SKILL.md)');
235
+ return;
236
+ }
237
+ console.error(`未知 skill 子命令: ${sub}\n用法: playlist-admin skill list | install | path`);
238
+ process.exit(1);
239
+ }
240
+
142
241
  async function main() {
143
242
  const args = process.argv.slice(2);
144
243
  if (args.length === 0) {
@@ -153,6 +252,10 @@ async function main() {
153
252
  runStudy(args.slice(1));
154
253
  return;
155
254
  }
255
+ if (args[0] === 'skill') {
256
+ runSkill(args.slice(1));
257
+ return;
258
+ }
156
259
 
157
260
  const root = projectRoot();
158
261
  if (!root) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playlist-admin",
3
- "version": "2.15.36",
3
+ "version": "2.15.38",
4
4
  "description": "playlist-admin CLI - pipeline, podcast, music + podcast RAG + study RAG (PDF 試題/課本)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,8 @@
15
15
  "files": [
16
16
  "cli/index.js",
17
17
  "rag/*.py",
18
- "rag/README.md"
18
+ "rag/README.md",
19
+ ".opencode/skills/podcast-knowledge/SKILL.md"
19
20
  ],
20
21
  "keywords": [
21
22
  "playlist",