@geekbeer/minion 3.52.0 → 3.53.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/.env.example +7 -0
- package/core/db/migrations/20260508000000_page_recipes.js +33 -0
- package/core/lib/web-extract/extractor.js +142 -0
- package/core/lib/web-extract/fingerprint.js +63 -0
- package/core/lib/web-extract/html-cleaner.js +72 -0
- package/core/lib/web-extract/index.js +21 -0
- package/core/lib/web-extract/playwright-runner.js +129 -0
- package/core/lib/web-extract/recipe-generator.js +247 -0
- package/core/lib/web-extract/url-normalize.js +90 -0
- package/core/routes/web.js +94 -0
- package/core/stores/page-recipe-store.js +143 -0
- package/docs/api-reference.md +66 -0
- package/docs/task-guides.md +58 -0
- package/linux/routes/chat.js +36 -4
- package/linux/server.js +2 -0
- package/mac/server.js +2 -0
- package/package.json +6 -2
- package/rules/core.md +21 -1
- package/win/routes/chat.js +37 -2
- package/win/server.js +2 -0
package/docs/task-guides.md
CHANGED
|
@@ -4,6 +4,64 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## Webページの読み取り・要約 🧪 (experimental, v3.53.0〜)
|
|
8
|
+
|
|
9
|
+
ユーザーから「このURLを要約して」「このページの情報を抽出して」「このページの一覧を取ってきて」と依頼された場合の手順。
|
|
10
|
+
|
|
11
|
+
### まずローカルAPIを試す
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
curl -X POST http://localhost:8080/api/web/extract \
|
|
15
|
+
-H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \
|
|
16
|
+
-d '{"url": "対象URL", "hint": "何を抽出したいか短く (任意)"}' | jq
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
返ってきた JSON の `title` `content` `structured` を使って要約・回答を作成する。`structured` には初回アクセス時に LLM が選んだフィールドが入る。
|
|
20
|
+
|
|
21
|
+
このAPIは内部で Playwright + Readability を回して **メインセッションには結果 JSON だけ返す** ため、Playwright MCP を使うときに起きていたチャットコンテキストのトークン肥大化が回避できる。
|
|
22
|
+
|
|
23
|
+
### Playwright MCP を使うべき場面
|
|
24
|
+
|
|
25
|
+
`/api/web/extract` で対応できないのは以下のケース。このときだけ `mcp__playwright__*` を使う:
|
|
26
|
+
|
|
27
|
+
- ログイン必須ページ (Cookie/2FA 等の認証必要)
|
|
28
|
+
- フォーム入力・複数ページ遷移を伴う操作
|
|
29
|
+
- ボタンクリック→動的に追加されるコンテンツの取得
|
|
30
|
+
- Lancers コンペ応募など、明らかに対話的操作が必要なフロー
|
|
31
|
+
|
|
32
|
+
**単純な閲覧・抽出用途では MCP を使わない。**
|
|
33
|
+
|
|
34
|
+
### よくあるパターン
|
|
35
|
+
|
|
36
|
+
| ユーザー依頼 | 推奨手段 |
|
|
37
|
+
|--------------|----------|
|
|
38
|
+
| 「このQiita記事を要約」 | `/api/web/extract` |
|
|
39
|
+
| 「Lancersコンペの一覧を取得」 | `/api/web/extract` |
|
|
40
|
+
| 「このプロダクトページから価格を抽出」 | `/api/web/extract` |
|
|
41
|
+
| 「ログインしてダッシュボード操作」 | Playwright MCP |
|
|
42
|
+
| 「フォームを送信」 | Playwright MCP |
|
|
43
|
+
| 「複数ページ巡回して全件取得」 | `/api/web/extract` をループ呼び出し (各ページに対して) |
|
|
44
|
+
|
|
45
|
+
### キャッシュの確認・破棄 (debug)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# キャッシュ済みレシピ一覧
|
|
49
|
+
curl -H "Authorization: Bearer $API_TOKEN" http://localhost:8080/api/web/recipes | jq
|
|
50
|
+
|
|
51
|
+
# 特定のレシピを削除 (壊れたセレクタを強制再生成させたい場合)
|
|
52
|
+
curl -X DELETE -H "Authorization: Bearer $API_TOKEN" \
|
|
53
|
+
"http://localhost:8080/api/web/recipes?template=lancers.jp/work/proposal/:id&fingerprint=abc123def456"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 失敗時の対処
|
|
57
|
+
|
|
58
|
+
- `503 PLAYWRIGHT_UNAVAILABLE` → ホスト側で `npx playwright install chromium` を実行 (sudo 不要)
|
|
59
|
+
- `503 LLM_UNAVAILABLE` → primary LLM 未設定。`PUT /api/llm/config -d '{"primary":"claude"}'` で primary を指定するか、fallback として `PUT /api/secrets/ANTHROPIC_API_KEY` で API キーを投入
|
|
60
|
+
- `502 PRIMARY_LLM_BAD_JSON` → primary LLM が JSON 形式で返さなかった。プラグインの認証状態を `GET /api/llm/plugins` で確認し、必要なら別 plugin を primary にする
|
|
61
|
+
- `500 extract timeout` → ページが重すぎる/JSレンダリング待ちが長すぎる。Playwright MCP に切り替えて手動操作
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
7
65
|
## スキルの修正
|
|
8
66
|
|
|
9
67
|
### 1. ローカルのスキルを編集する
|
package/linux/routes/chat.js
CHANGED
|
@@ -362,6 +362,29 @@ async function buildContextPrefix(message, context, sessionId, workspaceId, refe
|
|
|
362
362
|
)
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
// Web page extraction guidance (experimental, v3.53.0)
|
|
366
|
+
if (!sessionId) {
|
|
367
|
+
const port = require('../../core/config').config.AGENT_PORT
|
|
368
|
+
parts.push(
|
|
369
|
+
'[Webページ読み取りについて 🧪 experimental]',
|
|
370
|
+
'Webページの読み取り・要約・情報抽出が必要なときは、まず以下のローカルAPIを試すこと:',
|
|
371
|
+
'',
|
|
372
|
+
'```bash',
|
|
373
|
+
`curl -X POST http://localhost:${port}/api/web/extract \\`,
|
|
374
|
+
' -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \\',
|
|
375
|
+
' -d \'{"url": "対象URL", "hint": "抽出したい内容を短く"}\'',
|
|
376
|
+
'```',
|
|
377
|
+
'',
|
|
378
|
+
'このAPIは内部で Playwright + Readability を回し、抽出済みJSONだけを返すため、',
|
|
379
|
+
'DOM全体がチャットに流れ込んでトークン肥大化することを防げる。',
|
|
380
|
+
'初回アクセスで学習したセレクタはSQLiteにキャッシュされ、2回目以降はLLM呼び出しなしで抽出される。',
|
|
381
|
+
'',
|
|
382
|
+
'Playwright MCP (`mcp__playwright__*`) は **ログイン・フォーム入力・複数画面の対話操作**が必要な場合のみ使用する。',
|
|
383
|
+
'単純な閲覧・要約・一覧取得用途ではMCPを使わない。',
|
|
384
|
+
''
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
|
|
365
388
|
// File output guidance — always inject on new sessions
|
|
366
389
|
if (!sessionId) {
|
|
367
390
|
parts.push(
|
|
@@ -770,6 +793,11 @@ function streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, original
|
|
|
770
793
|
child.on('close', async (code) => {
|
|
771
794
|
activeChatChild = null
|
|
772
795
|
|
|
796
|
+
console.log(`[Chat] child closed: code=${code}, response=${fullResponse.length}chars, turns=${turnCount}, stderr=${stderrBuffer.length}bytes, session=${resolvedSessionId}`)
|
|
797
|
+
if (stderrBuffer.trim()) {
|
|
798
|
+
console.log(`[Chat] final stderr (tail 500): ${stderrBuffer.slice(-500)}`)
|
|
799
|
+
}
|
|
800
|
+
|
|
773
801
|
// Store messages in chat-store
|
|
774
802
|
if (resolvedSessionId) {
|
|
775
803
|
// If this was a new session, also store the user message now
|
|
@@ -782,11 +810,15 @@ function streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, original
|
|
|
782
810
|
}
|
|
783
811
|
}
|
|
784
812
|
|
|
785
|
-
|
|
786
|
-
if (code !== 0 && !fullResponse) {
|
|
813
|
+
if (code !== 0) {
|
|
787
814
|
const errorMsg = stderrBuffer.trim() || `Claude CLI exited with code ${code}`
|
|
788
|
-
console.error(`[Chat] CLI failed (exit ${code}): ${errorMsg}`)
|
|
789
|
-
const errorEvent = JSON.stringify({
|
|
815
|
+
console.error(`[Chat] CLI failed (exit ${code}, partial=${!!fullResponse}): ${errorMsg}`)
|
|
816
|
+
const errorEvent = JSON.stringify({
|
|
817
|
+
type: 'error',
|
|
818
|
+
error: errorMsg,
|
|
819
|
+
partial: !!fullResponse,
|
|
820
|
+
exit_code: code,
|
|
821
|
+
})
|
|
790
822
|
res.write(`data: ${errorEvent}\n\n`)
|
|
791
823
|
}
|
|
792
824
|
|
package/linux/server.js
CHANGED
|
@@ -85,6 +85,7 @@ const { todoRoutes } = require('../core/routes/todos')
|
|
|
85
85
|
const { emailRoutes } = require('../core/routes/emails')
|
|
86
86
|
const { daemonRoutes } = require('../core/routes/daemons')
|
|
87
87
|
const { llmRoutes } = require('../core/routes/llm')
|
|
88
|
+
const { webRoutes } = require('../core/routes/web')
|
|
88
89
|
|
|
89
90
|
// Linux-specific routes
|
|
90
91
|
const { commandRoutes, getProcessManager, getAllowedCommands } = require('./routes/commands')
|
|
@@ -298,6 +299,7 @@ async function registerAllRoutes(app) {
|
|
|
298
299
|
await app.register(emailRoutes)
|
|
299
300
|
await app.register(daemonRoutes, { heartbeatStatus: () => ({ running: !!heartbeatTimer, last_beat_at: lastBeatAt }) })
|
|
300
301
|
await app.register(llmRoutes)
|
|
302
|
+
await app.register(webRoutes)
|
|
301
303
|
|
|
302
304
|
// Linux-specific routes
|
|
303
305
|
await app.register(commandRoutes)
|
package/mac/server.js
CHANGED
|
@@ -72,6 +72,7 @@ const { todoRoutes } = require('../core/routes/todos')
|
|
|
72
72
|
const { emailRoutes } = require('../core/routes/emails')
|
|
73
73
|
const { daemonRoutes } = require('../core/routes/daemons')
|
|
74
74
|
const { llmRoutes } = require('../core/routes/llm')
|
|
75
|
+
const { webRoutes } = require('../core/routes/web')
|
|
75
76
|
|
|
76
77
|
// macOS-specific routes
|
|
77
78
|
const { commandRoutes, getProcessManager, getAllowedCommands } = require('./routes/commands')
|
|
@@ -284,6 +285,7 @@ async function registerAllRoutes(app) {
|
|
|
284
285
|
await app.register(emailRoutes)
|
|
285
286
|
await app.register(daemonRoutes, { heartbeatStatus: () => ({ running: !!heartbeatTimer, last_beat_at: lastBeatAt }) })
|
|
286
287
|
await app.register(llmRoutes)
|
|
288
|
+
await app.register(webRoutes)
|
|
287
289
|
|
|
288
290
|
// macOS-specific routes
|
|
289
291
|
await app.register(commandRoutes)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekbeer/minion",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.53.1",
|
|
4
4
|
"description": "AI Agent runtime for Minion - manages status and skill deployment on VPS",
|
|
5
5
|
"main": "linux/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -33,13 +33,17 @@
|
|
|
33
33
|
"db:migration:new": "node scripts/new-migration.js"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@mozilla/readability": "^0.5.0",
|
|
36
37
|
"croner": "^9.0.0",
|
|
37
38
|
"fastify": "^5.2.2",
|
|
39
|
+
"linkedom": "^0.18.0",
|
|
40
|
+
"turndown": "^7.2.0",
|
|
38
41
|
"ws": "^8.0.0"
|
|
39
42
|
},
|
|
40
43
|
"optionalDependencies": {
|
|
41
44
|
"better-sqlite3": "^11.0.0",
|
|
42
|
-
"node-pty": "^1.0.0"
|
|
45
|
+
"node-pty": "^1.0.0",
|
|
46
|
+
"playwright": "^1.48.0"
|
|
43
47
|
},
|
|
44
48
|
"engines": {
|
|
45
49
|
"node": ">=22.0.0"
|
package/rules/core.md
CHANGED
|
@@ -103,7 +103,27 @@ minion-cli --version # バージョン確認
|
|
|
103
103
|
|
|
104
104
|
`http://localhost:8080` — 認証: `Authorization: Bearer $API_TOKEN`
|
|
105
105
|
|
|
106
|
-
主なカテゴリ: Health, Skills, Workflows, Executions, Terminal, Files, Commands, Permissions, Admin (HQ-pushed freeze)
|
|
106
|
+
主なカテゴリ: Health, Skills, Workflows, Executions, Terminal, Files, Commands, Permissions, Admin (HQ-pushed freeze), Web Extraction (experimental)
|
|
107
|
+
|
|
108
|
+
#### Web Page Extraction 🧪 (experimental, v3.53.0〜)
|
|
109
|
+
|
|
110
|
+
Webページの読み取り・要約・情報抽出には、**Playwright MCP より先に** ローカルAPI `POST /api/web/extract` を試すこと。
|
|
111
|
+
|
|
112
|
+
このAPIは内部でヘッドレスブラウザ→本文クリーン (Readability) →セレクタ抽出 (LLMサブプロセスで自動学習) →JSON 返却までを完結させる。**メインセッションに巨大な DOM が流れ込まないため、トークン肥大化やセッション終了を防げる。**
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
curl -X POST http://localhost:8080/api/web/extract \
|
|
116
|
+
-H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \
|
|
117
|
+
-d '{"url": "https://example.com/article/123", "hint": "本文と著者を抽出"}'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
レシピは初回アクセス時に LLM (Haiku) で生成・SQLite (`page_recipes` テーブル) に保存され、2回目以降の構造的に同じページでは LLM 呼び出しなしで抽出される。
|
|
121
|
+
|
|
122
|
+
Playwright MCP (`mcp__playwright__*`) は **フォーム入力・クリック・複数画面遷移など対話的な操作**が必要な場合のみ使用すること。単に「ページを読む」目的では MCP を使わない。
|
|
123
|
+
|
|
124
|
+
**実験的機能**: レスポンス形状は予告なく変わる可能性がある。要件: (1) primary LLM 設定済み (`PUT /api/llm/config` で `claude` 等を選択、`hq llm primary <name>` でも可) または `ANTHROPIC_API_KEY` シークレット設定済み、(2) ホスト上で `npx playwright install chromium` 実行済み。primary LLM が設定されていれば API キー不要 (Claude Code CLI の認証情報を再利用)。
|
|
125
|
+
|
|
126
|
+
詳細仕様は `~/.minion/docs/api-reference.md` の「Web Page Extraction」セクション、ユースケースは `~/.minion/docs/task-guides.md` の「Webページの読み取り・要約」を参照。
|
|
107
127
|
|
|
108
128
|
#### Billing-Driven Freeze (v3.52.0〜)
|
|
109
129
|
|
package/win/routes/chat.js
CHANGED
|
@@ -423,6 +423,29 @@ async function buildContextPrefix(message, context, sessionId, workspaceId, refe
|
|
|
423
423
|
)
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
+
// Web page extraction guidance (experimental, v3.53.0)
|
|
427
|
+
if (!sessionId) {
|
|
428
|
+
const port = require('../../core/config').config.AGENT_PORT
|
|
429
|
+
parts.push(
|
|
430
|
+
'[Webページ読み取りについて 🧪 experimental]',
|
|
431
|
+
'Webページの読み取り・要約・情報抽出が必要なときは、まず以下のローカルAPIを試すこと:',
|
|
432
|
+
'',
|
|
433
|
+
'```bash',
|
|
434
|
+
`curl -X POST http://localhost:${port}/api/web/extract \\`,
|
|
435
|
+
' -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \\',
|
|
436
|
+
' -d \'{"url": "対象URL", "hint": "抽出したい内容を短く"}\'',
|
|
437
|
+
'```',
|
|
438
|
+
'',
|
|
439
|
+
'このAPIは内部で Playwright + Readability を回し、抽出済みJSONだけを返すため、',
|
|
440
|
+
'DOM全体がチャットに流れ込んでトークン肥大化することを防げる。',
|
|
441
|
+
'初回アクセスで学習したセレクタはSQLiteにキャッシュされ、2回目以降はLLM呼び出しなしで抽出される。',
|
|
442
|
+
'',
|
|
443
|
+
'Playwright MCP (`mcp__playwright__*`) は **ログイン・フォーム入力・複数画面の対話操作**が必要な場合のみ使用する。',
|
|
444
|
+
'単純な閲覧・要約・一覧取得用途ではMCPを使わない。',
|
|
445
|
+
''
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
|
|
426
449
|
// File output guidance — always inject on new sessions
|
|
427
450
|
if (!sessionId) {
|
|
428
451
|
parts.push(
|
|
@@ -790,6 +813,12 @@ function streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, original
|
|
|
790
813
|
|
|
791
814
|
child.on('close', async (code) => {
|
|
792
815
|
activeChatChild = null
|
|
816
|
+
|
|
817
|
+
console.log(`[Chat] child closed: code=${code}, response=${fullResponse.length}chars, turns=${turnCount}, stderr=${stderrBuffer.length}bytes, session=${resolvedSessionId}`)
|
|
818
|
+
if (stderrBuffer.trim()) {
|
|
819
|
+
console.log(`[Chat] final stderr (tail 500): ${stderrBuffer.slice(-500)}`)
|
|
820
|
+
}
|
|
821
|
+
|
|
793
822
|
if (resolvedSessionId) {
|
|
794
823
|
if (!sessionId) {
|
|
795
824
|
await chatStore.addMessage(resolvedSessionId, { role: 'user', content: originalMessage || prompt }, undefined, workspaceId)
|
|
@@ -798,9 +827,15 @@ function streamViaLegacyLlmCommand(res, prompt, sessionId, workspaceId, original
|
|
|
798
827
|
await chatStore.addMessage(resolvedSessionId, { role: 'assistant', content: fullResponse }, turnCount, workspaceId)
|
|
799
828
|
}
|
|
800
829
|
}
|
|
801
|
-
if (code !== 0
|
|
830
|
+
if (code !== 0) {
|
|
802
831
|
const errorMsg = stderrBuffer.trim() || `Claude CLI exited with code ${code}`
|
|
803
|
-
|
|
832
|
+
console.error(`[Chat] CLI failed (exit ${code}, partial=${!!fullResponse}): ${errorMsg}`)
|
|
833
|
+
res.write(`data: ${JSON.stringify({
|
|
834
|
+
type: 'error',
|
|
835
|
+
error: errorMsg,
|
|
836
|
+
partial: !!fullResponse,
|
|
837
|
+
exit_code: code,
|
|
838
|
+
})}\n\n`)
|
|
804
839
|
}
|
|
805
840
|
|
|
806
841
|
const session = await chatStore.load(workspaceId)
|
package/win/server.js
CHANGED
|
@@ -67,6 +67,7 @@ const { todoRoutes } = require('../core/routes/todos')
|
|
|
67
67
|
const { emailRoutes } = require('../core/routes/emails')
|
|
68
68
|
const { daemonRoutes } = require('../core/routes/daemons')
|
|
69
69
|
const { llmRoutes } = require('../core/routes/llm')
|
|
70
|
+
const { webRoutes } = require('../core/routes/web')
|
|
70
71
|
|
|
71
72
|
// Validate configuration
|
|
72
73
|
validate()
|
|
@@ -232,6 +233,7 @@ async function registerRoutes(app) {
|
|
|
232
233
|
await app.register(emailRoutes)
|
|
233
234
|
await app.register(daemonRoutes, { heartbeatStatus: () => ({ running: !!heartbeatTimer, last_beat_at: lastBeatAt }) })
|
|
234
235
|
await app.register(llmRoutes)
|
|
236
|
+
await app.register(webRoutes)
|
|
235
237
|
|
|
236
238
|
// Shutdown endpoint — allows detached restart/update scripts to trigger
|
|
237
239
|
// graceful shutdown (offline heartbeat) before force-killing the process.
|