@geekbeer/minion 3.51.2 → 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/api.js +26 -2
- package/core/db/migrations/20260508000000_page_recipes.js +33 -0
- package/core/lib/board-task-poller.js +12 -1
- package/core/lib/dag-cron-poller.js +14 -1
- package/core/lib/dag-step-poller.js +17 -1
- package/core/lib/frozen-state.js +64 -0
- package/core/lib/revision-watcher.js +5 -1
- package/core/lib/step-poller.js +5 -1
- package/core/lib/thread-watcher.js +5 -1
- 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/admin.js +49 -0
- package/core/routes/web.js +94 -0
- package/core/stores/page-recipe-store.js +143 -0
- package/docs/api-reference.md +83 -0
- package/docs/task-guides.md +58 -0
- package/linux/routes/chat.js +36 -4
- package/linux/server.js +4 -0
- package/mac/server.js +2 -0
- package/package.json +6 -2
- package/rules/core.md +29 -1
- package/win/routes/chat.js +37 -2
- package/win/server.js +4 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page Recipe Store (SQLite, experimental — v3.53.0)
|
|
3
|
+
*
|
|
4
|
+
* Backs POST /api/web/extract. See core/lib/web-extract/extractor.js for the
|
|
5
|
+
* orchestrator that decides hot vs cold paths.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { getDb } = require('../db')
|
|
9
|
+
|
|
10
|
+
const MAX_FAIL_COUNT = 3
|
|
11
|
+
|
|
12
|
+
function find({ urlTemplate, domFingerprint }) {
|
|
13
|
+
const db = getDb()
|
|
14
|
+
const row = db.prepare(`
|
|
15
|
+
SELECT url_template, dom_fingerprint, selectors_json, page_type,
|
|
16
|
+
hit_count, fail_count, last_verified_at, created_at
|
|
17
|
+
FROM page_recipes
|
|
18
|
+
WHERE url_template = ? AND dom_fingerprint = ?
|
|
19
|
+
`).get(urlTemplate, domFingerprint)
|
|
20
|
+
if (!row) return null
|
|
21
|
+
return parseRow(row)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findByTemplate(urlTemplate) {
|
|
25
|
+
const db = getDb()
|
|
26
|
+
const rows = db.prepare(`
|
|
27
|
+
SELECT url_template, dom_fingerprint, selectors_json, page_type,
|
|
28
|
+
hit_count, fail_count, last_verified_at, created_at
|
|
29
|
+
FROM page_recipes
|
|
30
|
+
WHERE url_template = ?
|
|
31
|
+
ORDER BY last_verified_at DESC NULLS LAST, created_at DESC
|
|
32
|
+
`).all(urlTemplate)
|
|
33
|
+
return rows.map(parseRow)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function upsert({ urlTemplate, domFingerprint, selectors, pageType }) {
|
|
37
|
+
const db = getDb()
|
|
38
|
+
const json = JSON.stringify(selectors || {})
|
|
39
|
+
const now = new Date().toISOString()
|
|
40
|
+
db.prepare(`
|
|
41
|
+
INSERT INTO page_recipes (url_template, dom_fingerprint, selectors_json, page_type, hit_count, fail_count, last_verified_at, created_at)
|
|
42
|
+
VALUES (?, ?, ?, ?, 0, 0, ?, ?)
|
|
43
|
+
ON CONFLICT(url_template, dom_fingerprint) DO UPDATE SET
|
|
44
|
+
selectors_json = excluded.selectors_json,
|
|
45
|
+
page_type = excluded.page_type,
|
|
46
|
+
fail_count = 0,
|
|
47
|
+
last_verified_at = excluded.last_verified_at
|
|
48
|
+
`).run(urlTemplate, domFingerprint, json, pageType || null, now, now)
|
|
49
|
+
return find({ urlTemplate, domFingerprint })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function incrementHit({ urlTemplate, domFingerprint }) {
|
|
53
|
+
const db = getDb()
|
|
54
|
+
db.prepare(`
|
|
55
|
+
UPDATE page_recipes
|
|
56
|
+
SET hit_count = hit_count + 1
|
|
57
|
+
WHERE url_template = ? AND dom_fingerprint = ?
|
|
58
|
+
`).run(urlTemplate, domFingerprint)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function setLastVerified({ urlTemplate, domFingerprint }) {
|
|
62
|
+
const db = getDb()
|
|
63
|
+
db.prepare(`
|
|
64
|
+
UPDATE page_recipes
|
|
65
|
+
SET last_verified_at = ?, fail_count = 0
|
|
66
|
+
WHERE url_template = ? AND dom_fingerprint = ?
|
|
67
|
+
`).run(new Date().toISOString(), urlTemplate, domFingerprint)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Increment fail count. Returns true if the recipe was deleted (>= MAX_FAIL_COUNT).
|
|
72
|
+
*/
|
|
73
|
+
function incrementFail({ urlTemplate, domFingerprint }) {
|
|
74
|
+
const db = getDb()
|
|
75
|
+
const row = db.prepare(`
|
|
76
|
+
SELECT fail_count FROM page_recipes
|
|
77
|
+
WHERE url_template = ? AND dom_fingerprint = ?
|
|
78
|
+
`).get(urlTemplate, domFingerprint)
|
|
79
|
+
if (!row) return false
|
|
80
|
+
const next = (row.fail_count || 0) + 1
|
|
81
|
+
if (next >= MAX_FAIL_COUNT) {
|
|
82
|
+
remove({ urlTemplate, domFingerprint })
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
db.prepare(`
|
|
86
|
+
UPDATE page_recipes
|
|
87
|
+
SET fail_count = ?
|
|
88
|
+
WHERE url_template = ? AND dom_fingerprint = ?
|
|
89
|
+
`).run(next, urlTemplate, domFingerprint)
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function remove({ urlTemplate, domFingerprint }) {
|
|
94
|
+
const db = getDb()
|
|
95
|
+
const result = db.prepare(`
|
|
96
|
+
DELETE FROM page_recipes
|
|
97
|
+
WHERE url_template = ? AND dom_fingerprint = ?
|
|
98
|
+
`).run(urlTemplate, domFingerprint)
|
|
99
|
+
return result.changes > 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function listAll({ limit = 100 } = {}) {
|
|
103
|
+
const db = getDb()
|
|
104
|
+
const rows = db.prepare(`
|
|
105
|
+
SELECT url_template, dom_fingerprint, selectors_json, page_type,
|
|
106
|
+
hit_count, fail_count, last_verified_at, created_at
|
|
107
|
+
FROM page_recipes
|
|
108
|
+
ORDER BY last_verified_at DESC NULLS LAST, created_at DESC
|
|
109
|
+
LIMIT ?
|
|
110
|
+
`).all(limit)
|
|
111
|
+
return rows.map(parseRow)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseRow(row) {
|
|
115
|
+
let selectors = {}
|
|
116
|
+
try {
|
|
117
|
+
selectors = JSON.parse(row.selectors_json || '{}')
|
|
118
|
+
} catch {
|
|
119
|
+
selectors = {}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
url_template: row.url_template,
|
|
123
|
+
dom_fingerprint: row.dom_fingerprint,
|
|
124
|
+
selectors,
|
|
125
|
+
page_type: row.page_type,
|
|
126
|
+
hit_count: row.hit_count,
|
|
127
|
+
fail_count: row.fail_count,
|
|
128
|
+
last_verified_at: row.last_verified_at,
|
|
129
|
+
created_at: row.created_at,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
find,
|
|
135
|
+
findByTemplate,
|
|
136
|
+
upsert,
|
|
137
|
+
incrementHit,
|
|
138
|
+
incrementFail,
|
|
139
|
+
setLastVerified,
|
|
140
|
+
remove,
|
|
141
|
+
listAll,
|
|
142
|
+
MAX_FAIL_COUNT,
|
|
143
|
+
}
|
package/docs/api-reference.md
CHANGED
|
@@ -696,6 +696,72 @@ PUT `/api/email/inbox/:id` body:
|
|
|
696
696
|
|
|
697
697
|
Note: 既読メールは受信後90日で自動削除される。未読メールは保持される。
|
|
698
698
|
|
|
699
|
+
### Web Page Extraction 🧪 (experimental, v3.53.0〜)
|
|
700
|
+
|
|
701
|
+
Web ページの読み取り・要約・情報抽出をミニオン内のサブプロセスで完結させ、メインの Claude Code セッションには結果 JSON だけを返すための実験的 API。Playwright MCP で DOM 全体がチャットに流れ込みトークン肥大化を起こす問題への対処として導入。
|
|
702
|
+
|
|
703
|
+
| Method | Endpoint | Description |
|
|
704
|
+
|--------|----------|-------------|
|
|
705
|
+
| POST | `/api/web/extract` | URL を抽出済み JSON に変換 (タイトル・本文・主要構造化データ) |
|
|
706
|
+
| GET | `/api/web/recipes` | キャッシュされたレシピ一覧 (debug) |
|
|
707
|
+
| DELETE | `/api/web/recipes?template=...&fingerprint=...` | レシピを削除 |
|
|
708
|
+
|
|
709
|
+
**前提条件 (LLM の解決順):**
|
|
710
|
+
1. **primary LLM プラグインが設定されていればそれを使用** (推奨)。`PUT /api/llm/config` で `claude` 等を primary に指定すると、CLI の認証情報 (`~/.claude/.credentials.json`) がそのまま使われる。API キーの別途設定は不要。
|
|
711
|
+
2. primary 未設定で `ANTHROPIC_API_KEY` シークレットが設定されていれば、そちらを fallback として使用 (Anthropic Messages API の tool_use で JSON Schema を強制)。
|
|
712
|
+
3. どちらもなければ 503 (`LLM_UNAVAILABLE`) を返す。
|
|
713
|
+
|
|
714
|
+
その他の前提:
|
|
715
|
+
- ホスト上で `npx playwright install chromium` を実行済みであること (未実行の場合 503 が返る)
|
|
716
|
+
|
|
717
|
+
**`POST /api/web/extract` リクエスト:**
|
|
718
|
+
```json
|
|
719
|
+
{
|
|
720
|
+
"url": "https://example.com/article/123",
|
|
721
|
+
"hint": "本文と著者を抽出してほしい (任意, 抽出フィールドのヒント)"
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
**レスポンス (success):**
|
|
726
|
+
```json
|
|
727
|
+
{
|
|
728
|
+
"success": true,
|
|
729
|
+
"experimental": true,
|
|
730
|
+
"url": "https://example.com/article/123",
|
|
731
|
+
"finalUrl": "https://example.com/article/123",
|
|
732
|
+
"statusCode": 200,
|
|
733
|
+
"recipeMode": "cold",
|
|
734
|
+
"recipeId": "example.com/article/:id#abc123def456",
|
|
735
|
+
"pageType": "article",
|
|
736
|
+
"title": "...",
|
|
737
|
+
"content": "Markdown 本文...",
|
|
738
|
+
"structured": { "title": "...", "author": "...", "publishedAt": "..." },
|
|
739
|
+
"selectors": { "title": { "selector": "h1" }, "author": { "selector": "a[rel=author]" } }
|
|
740
|
+
}
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
**動作:**
|
|
744
|
+
- 初回アクセス (cold): Playwright でレンダリング → Readability で本文抽出 → Anthropic Haiku でセレクタ生成 → SQLite (`page_recipes`) に保存 → セレクタで再抽出して返却
|
|
745
|
+
- 2回目以降 (hot): URL 正規化・テンプレート化 → DOM フィンガープリントで保存済みレシピを照合 → セレクタで抽出のみ (LLM 呼び出しなし)
|
|
746
|
+
- セルフヒール: hot 実行で空結果が返ったら `fail_count++`、3回失敗で破棄して次回 cold 再生成
|
|
747
|
+
|
|
748
|
+
**URL 正規化ルール:**
|
|
749
|
+
- `utm_*` `fbclid` `gclid` `ref` 等のトラッキングクエリは除去
|
|
750
|
+
- `page` `p` `offset` 等のページネーション値は `:n` プレースホルダ化
|
|
751
|
+
- パスセグメントは数値→`:id`、UUID→`:uuid`、20文字以上の英数字→`:slug` に置換
|
|
752
|
+
- 例: `https://www.lancers.jp/work/proposal/123456?utm_source=foo&page=2` → `lancers.jp/work/proposal/:id?page=:n`
|
|
753
|
+
|
|
754
|
+
**エラー:**
|
|
755
|
+
- 401: APIトークン不正
|
|
756
|
+
- 400: URL 欠落・不正
|
|
757
|
+
- 502: primary LLM 呼び出し失敗 (`PRIMARY_LLM_FAILED`) / 返却 JSON が parse 不能 (`PRIMARY_LLM_BAD_JSON`)
|
|
758
|
+
- 503: Playwright 未インストール (`PLAYWRIGHT_UNAVAILABLE`) / LLM 未設定 (`LLM_UNAVAILABLE`)
|
|
759
|
+
- 500: 抽出失敗 (タイムアウト等)
|
|
760
|
+
|
|
761
|
+
**注意 (experimental):**
|
|
762
|
+
- レスポンス形状・URL 正規化ルール・キャッシュスキーマは予告なく変更される可能性がある
|
|
763
|
+
- 認証付きページ (ログイン必須) は対応外。Cookie や対話的操作が必要な場合は Playwright MCP を使用すること
|
|
764
|
+
|
|
699
765
|
### Commands
|
|
700
766
|
|
|
701
767
|
| Method | Endpoint | Description |
|
|
@@ -705,6 +771,23 @@ Note: 既読メールは受信後90日で自動削除される。未読メール
|
|
|
705
771
|
|
|
706
772
|
Available commands: `restart-agent`, `update-agent`, `restart-display`, `restart-all`, `status-services`
|
|
707
773
|
|
|
774
|
+
### Admin (HQ → Minion only)
|
|
775
|
+
|
|
776
|
+
HQ が課金状態の変化に応じてミニオンに直接プッシュするエンドポイント。
|
|
777
|
+
|
|
778
|
+
| Method | Endpoint | Description |
|
|
779
|
+
|--------|----------|-------------|
|
|
780
|
+
| POST | `/api/admin/freeze` | ポーラーを停止しHQへの発信を抑止する。Body: `{reason}` |
|
|
781
|
+
| GET | `/api/admin/state` | 現在のfrozen状態を取得 |
|
|
782
|
+
|
|
783
|
+
**Freeze の挙動 (v3.52.0〜):**
|
|
784
|
+
- ミニオン内のグローバル `frozen=true` (in-memory only) を立てる。
|
|
785
|
+
- 各ポーラー (step, dag-step, board-task, thread-watcher, dag-cron, revision-watcher, heartbeat) が `pollOnce()` 直前で `isFrozen()` を確認しスキップ。
|
|
786
|
+
- 共通HTTPクライアント (`core/api.js`, 各pollerの `*Request`) は HQ から `402 billing_frozen` を受信した時点で自動的に `frozen=true` をセット (HQからのpushが届かなくても自己防衛)。
|
|
787
|
+
- 状態は**ディスク永続化されない**。プロセス再起動で自動的にクリアされ、課金復活後の `restart-agent` コマンドで自然に解除される。
|
|
788
|
+
|
|
789
|
+
復旧は HQ が既存の `POST /api/command { command: "restart-agent" }` をプッシュすることで行う (専用 unfreeze エンドポイントは無い)。
|
|
790
|
+
|
|
708
791
|
---
|
|
709
792
|
|
|
710
793
|
## HQ API Endpoints (https://<HQ_URL>)
|
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
|
@@ -74,6 +74,7 @@ const { skillRoutes } = require('../core/routes/skills')
|
|
|
74
74
|
const { workflowRoutes } = require('../core/routes/workflows')
|
|
75
75
|
const { routineRoutes } = require('../core/routes/routines')
|
|
76
76
|
const { authRoutes } = require('../core/routes/auth')
|
|
77
|
+
const { adminRoutes } = require('../core/routes/admin')
|
|
77
78
|
const { variableRoutes } = require('../core/routes/variables')
|
|
78
79
|
const { memoryRoutes } = require('../core/routes/memory')
|
|
79
80
|
const { dailyLogRoutes } = require('../core/routes/daily-logs')
|
|
@@ -84,6 +85,7 @@ const { todoRoutes } = require('../core/routes/todos')
|
|
|
84
85
|
const { emailRoutes } = require('../core/routes/emails')
|
|
85
86
|
const { daemonRoutes } = require('../core/routes/daemons')
|
|
86
87
|
const { llmRoutes } = require('../core/routes/llm')
|
|
88
|
+
const { webRoutes } = require('../core/routes/web')
|
|
87
89
|
|
|
88
90
|
// Linux-specific routes
|
|
89
91
|
const { commandRoutes, getProcessManager, getAllowedCommands } = require('./routes/commands')
|
|
@@ -286,6 +288,7 @@ async function registerAllRoutes(app) {
|
|
|
286
288
|
await app.register(workflowRoutes, { workflowRunner })
|
|
287
289
|
await app.register(routineRoutes, { routineRunner })
|
|
288
290
|
await app.register(authRoutes)
|
|
291
|
+
await app.register(adminRoutes)
|
|
289
292
|
await app.register(variableRoutes)
|
|
290
293
|
await app.register(memoryRoutes)
|
|
291
294
|
await app.register(dailyLogRoutes)
|
|
@@ -296,6 +299,7 @@ async function registerAllRoutes(app) {
|
|
|
296
299
|
await app.register(emailRoutes)
|
|
297
300
|
await app.register(daemonRoutes, { heartbeatStatus: () => ({ running: !!heartbeatTimer, last_beat_at: lastBeatAt }) })
|
|
298
301
|
await app.register(llmRoutes)
|
|
302
|
+
await app.register(webRoutes)
|
|
299
303
|
|
|
300
304
|
// Linux-specific routes
|
|
301
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,35 @@ 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
|
|
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ページの読み取り・要約」を参照。
|
|
127
|
+
|
|
128
|
+
#### Billing-Driven Freeze (v3.52.0〜)
|
|
129
|
+
|
|
130
|
+
ユーザーの決済が `past_due` に陥ると、HQ から `POST /api/admin/freeze` がプッシュされ、ミニオン全ポーラーが停止する。
|
|
131
|
+
- 状態は in-memory のみ (frozen.json などのディスクファイルは存在しない)。
|
|
132
|
+
- HQ への発信中に `402 billing_frozen` を受けた場合も自動的にfrozen化される (HQ pushが届かなくても自己防衛)。
|
|
133
|
+
- 復旧は HQ が `restart-agent` コマンドをプッシュ → プロセス再起動 → in-memory フラグが消えて自動再開。
|
|
134
|
+
- API仕様は `~/.minion/docs/api-reference.md` の「Admin」セクション参照。
|
|
107
135
|
|
|
108
136
|
#### Permission Management
|
|
109
137
|
|
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
|
@@ -57,6 +57,7 @@ const { skillRoutes } = require('../core/routes/skills')
|
|
|
57
57
|
const { workflowRoutes } = require('../core/routes/workflows')
|
|
58
58
|
const { routineRoutes } = require('../core/routes/routines')
|
|
59
59
|
const { authRoutes } = require('../core/routes/auth')
|
|
60
|
+
const { adminRoutes } = require('../core/routes/admin')
|
|
60
61
|
const { variableRoutes } = require('../core/routes/variables')
|
|
61
62
|
const { memoryRoutes } = require('../core/routes/memory')
|
|
62
63
|
const { dailyLogRoutes } = require('../core/routes/daily-logs')
|
|
@@ -66,6 +67,7 @@ const { todoRoutes } = require('../core/routes/todos')
|
|
|
66
67
|
const { emailRoutes } = require('../core/routes/emails')
|
|
67
68
|
const { daemonRoutes } = require('../core/routes/daemons')
|
|
68
69
|
const { llmRoutes } = require('../core/routes/llm')
|
|
70
|
+
const { webRoutes } = require('../core/routes/web')
|
|
69
71
|
|
|
70
72
|
// Validate configuration
|
|
71
73
|
validate()
|
|
@@ -221,6 +223,7 @@ async function registerRoutes(app) {
|
|
|
221
223
|
await app.register(workflowRoutes, { workflowRunner })
|
|
222
224
|
await app.register(routineRoutes, { routineRunner })
|
|
223
225
|
await app.register(authRoutes)
|
|
226
|
+
await app.register(adminRoutes)
|
|
224
227
|
await app.register(variableRoutes)
|
|
225
228
|
await app.register(memoryRoutes)
|
|
226
229
|
await app.register(dailyLogRoutes)
|
|
@@ -230,6 +233,7 @@ async function registerRoutes(app) {
|
|
|
230
233
|
await app.register(emailRoutes)
|
|
231
234
|
await app.register(daemonRoutes, { heartbeatStatus: () => ({ running: !!heartbeatTimer, last_beat_at: lastBeatAt }) })
|
|
232
235
|
await app.register(llmRoutes)
|
|
236
|
+
await app.register(webRoutes)
|
|
233
237
|
|
|
234
238
|
// Shutdown endpoint — allows detached restart/update scripts to trigger
|
|
235
239
|
// graceful shutdown (offline heartbeat) before force-killing the process.
|