@geekbeer/minion 4.4.0 → 4.7.0
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/core/db/migrations/20260607000000_chat_runs.js +48 -0
- package/core/db/migrations/20260607120000_page_recipes_ready_selector.js +22 -0
- package/core/lib/chat-run-manager.js +406 -0
- package/core/lib/web-extract/extractor.js +27 -7
- package/core/lib/web-extract/playwright-runner.js +199 -1
- package/core/lib/web-extract/recipe-generator.js +19 -2
- package/core/routes/variables.js +47 -5
- package/core/routes/web.js +12 -3
- package/core/stores/chat-store.js +119 -2
- package/core/stores/page-recipe-store.js +9 -7
- package/core/stores/variable-store.js +63 -0
- package/docs/api-reference.md +82 -4
- package/docs/task-guides.md +20 -2
- package/linux/routes/chat.js +159 -193
- package/package.json +1 -1
- package/rules/core.md +12 -2
- package/win/routes/chat.js +155 -157
|
@@ -177,6 +177,36 @@ function deleteEntry(table, workspaceId, key) {
|
|
|
177
177
|
return { removed: info.changes > 0, wsId }
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Move a single entry from one workspace scope to another, atomically.
|
|
182
|
+
*
|
|
183
|
+
* Used to fix entries that were registered under the wrong workspace scope.
|
|
184
|
+
* The destination is NEVER overwritten: if the key already exists at the
|
|
185
|
+
* destination, the move is refused (`status: 'conflict'`) so no value is lost.
|
|
186
|
+
*
|
|
187
|
+
* @returns {{status: 'moved'|'not_found'|'conflict'|'same_scope', value?: string, from?: string, to?: string}}
|
|
188
|
+
*/
|
|
189
|
+
function moveEntry(table, fromWorkspaceId, toWorkspaceId, key) {
|
|
190
|
+
const from = normalizeWorkspaceId(fromWorkspaceId)
|
|
191
|
+
const to = normalizeWorkspaceId(toWorkspaceId)
|
|
192
|
+
if (from === to) return { status: 'same_scope' }
|
|
193
|
+
|
|
194
|
+
const db = getDb()
|
|
195
|
+
const src = db.prepare(`SELECT value FROM ${table} WHERE workspace_id = ? AND key = ?`).get(from, key)
|
|
196
|
+
if (!src) return { status: 'not_found' }
|
|
197
|
+
|
|
198
|
+
const dst = db.prepare(`SELECT 1 FROM ${table} WHERE workspace_id = ? AND key = ?`).get(to, key)
|
|
199
|
+
if (dst) return { status: 'conflict' }
|
|
200
|
+
|
|
201
|
+
const tx = db.transaction(() => {
|
|
202
|
+
db.prepare(`INSERT INTO ${table} (workspace_id, key, value, updated_at) VALUES (?, ?, ?, ?)`)
|
|
203
|
+
.run(to, key, src.value, Date.now())
|
|
204
|
+
db.prepare(`DELETE FROM ${table} WHERE workspace_id = ? AND key = ?`).run(from, key)
|
|
205
|
+
})
|
|
206
|
+
tx()
|
|
207
|
+
return { status: 'moved', value: src.value, from, to }
|
|
208
|
+
}
|
|
209
|
+
|
|
180
210
|
function listScopes(table) {
|
|
181
211
|
const db = getDb()
|
|
182
212
|
const rows = db.prepare(`SELECT DISTINCT workspace_id FROM ${table}`).all()
|
|
@@ -219,6 +249,19 @@ function removeVariable(workspaceId, key) {
|
|
|
219
249
|
return removed
|
|
220
250
|
}
|
|
221
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Move a variable from one workspace scope to another. The destination is not
|
|
254
|
+
* overwritten on conflict — see `moveEntry`.
|
|
255
|
+
*/
|
|
256
|
+
function moveVariable(fromWorkspaceId, toWorkspaceId, key) {
|
|
257
|
+
migrateLegacyVariablesFile()
|
|
258
|
+
const r = moveEntry('variables', fromWorkspaceId, toWorkspaceId, key)
|
|
259
|
+
if (r.status === 'moved') {
|
|
260
|
+
console.log(`[VariableStore] Moved variable ${key}: ${r.from || 'minion-wide'} -> ${r.to || 'minion-wide'}`)
|
|
261
|
+
}
|
|
262
|
+
return r
|
|
263
|
+
}
|
|
264
|
+
|
|
222
265
|
function listVariableKeys(workspaceId) {
|
|
223
266
|
migrateLegacyVariablesFile()
|
|
224
267
|
const rows = getRowsForScope('variables', workspaceId)
|
|
@@ -280,6 +323,24 @@ function removeSecret(workspaceId, key) {
|
|
|
280
323
|
return removed
|
|
281
324
|
}
|
|
282
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Move a secret from one workspace scope to another. The destination is not
|
|
328
|
+
* overwritten on conflict — see `moveEntry`.
|
|
329
|
+
*
|
|
330
|
+
* Keeps `process.env` in sync: leaving the minion-wide bucket removes the env
|
|
331
|
+
* var; entering it sets the env var. WS-scoped secrets never touch process.env.
|
|
332
|
+
*/
|
|
333
|
+
function moveSecret(fromWorkspaceId, toWorkspaceId, key) {
|
|
334
|
+
migrateLegacySecretsFile()
|
|
335
|
+
const r = moveEntry('secrets', fromWorkspaceId, toWorkspaceId, key)
|
|
336
|
+
if (r.status === 'moved') {
|
|
337
|
+
if (r.from === '') delete process.env[key]
|
|
338
|
+
if (r.to === '') process.env[key] = String(r.value ?? '')
|
|
339
|
+
console.log(`[VariableStore] Moved secret ${key}: ${r.from || 'minion-wide'} -> ${r.to || 'minion-wide'}`)
|
|
340
|
+
}
|
|
341
|
+
return r
|
|
342
|
+
}
|
|
343
|
+
|
|
283
344
|
function listSecretScopes() {
|
|
284
345
|
migrateLegacySecretsFile()
|
|
285
346
|
return listScopes('secrets')
|
|
@@ -345,6 +406,7 @@ module.exports = {
|
|
|
345
406
|
getVariable,
|
|
346
407
|
setVariable,
|
|
347
408
|
removeVariable,
|
|
409
|
+
moveVariable,
|
|
348
410
|
listVariableKeys,
|
|
349
411
|
listVariableScopes,
|
|
350
412
|
migrateLegacyVariablesFile,
|
|
@@ -355,6 +417,7 @@ module.exports = {
|
|
|
355
417
|
listSecretKeys,
|
|
356
418
|
setSecret,
|
|
357
419
|
removeSecret,
|
|
420
|
+
moveSecret,
|
|
358
421
|
listSecretScopes,
|
|
359
422
|
migrateLegacySecretsFile,
|
|
360
423
|
}
|
package/docs/api-reference.md
CHANGED
|
@@ -251,6 +251,37 @@ Response:
|
|
|
251
251
|
}
|
|
252
252
|
```
|
|
253
253
|
|
|
254
|
+
### Chat (Detached Execution)
|
|
255
|
+
|
|
256
|
+
チャットメッセージは **デタッチ実行** される (v4.6.0〜)。`POST /api/chat` で起動した LLM プロセスは
|
|
257
|
+
HTTP リクエストではなく内部の **run manager** が所有するため、SSE 接続が切れても (タブを閉じる・
|
|
258
|
+
リロード・プロキシのアイドルタイムアウト・回線断) 処理は中断されない。接続は単なる「覗き窓」であり、
|
|
259
|
+
切断時はサブスクライブを解除するだけでプロセスは kill されない。LLM を実際に停止するのは
|
|
260
|
+
`POST /api/chat/abort` のみ。
|
|
261
|
+
|
|
262
|
+
| Method | Endpoint | Description |
|
|
263
|
+
|--------|----------|-------------|
|
|
264
|
+
| POST | `/api/chat` | メッセージ送信 → 内部で run を起動し、その run のイベントを SSE で tail。SSE の最初のイベントは `{ "type": "run", "run_id": "..." }`(再接続用) |
|
|
265
|
+
| GET | `/api/chat/stream` | 進行中(または直近完了)の run に **再接続**し、`cursor` 以降のイベントから tail を再開。`run_id` または `workspace_id`(アクティブ run 自動解決) と任意の `cursor`(=最後に受け取った `seq`) を指定 |
|
|
266
|
+
| POST | `/api/chat/abort` | アクティブ run を明示的に kill (SIGTERM → 2秒後 SIGKILL)。Body に `workspace_id` / `run_id`(任意) |
|
|
267
|
+
| GET | `/api/chat/session` | アクティブセッションに加え、進行中 run があれば `active_run: { run_id, status, last_seq }` を返す(クライアントはこれを見て `/api/chat/stream` にアタッチする) |
|
|
268
|
+
|
|
269
|
+
各 SSE イベントには単調増加の `seq` が付与され、これが再接続時の `cursor` になる。終端は必ず
|
|
270
|
+
`{ "type": "done", "session_id, turn_count }`(失敗時は直前に `{ "type": "error" }`)。run の
|
|
271
|
+
イベントログは `$DATA_DIR/chat-runs/{run_id}.ndjson` に永続化され、完了から一定時間(既定10分)後に
|
|
272
|
+
メモリから evict、24時間でログ/索引を prune する。
|
|
273
|
+
|
|
274
|
+
> **再起動の挙動 (Phase 1):** run manager はミニオンプロセス内で動作するため、ミニオン自体の再起動は
|
|
275
|
+
> 生存しない。起動時に `running` のまま残った run は `interrupted` に掃き出される (クライアントは待ち続けない)。
|
|
276
|
+
> 接続断への耐性が主目的であり、再起動生存は将来の拡張 (tmux バックエンド) で対応予定。
|
|
277
|
+
|
|
278
|
+
`GET /api/chat/stream` 例:
|
|
279
|
+
```bash
|
|
280
|
+
# 直近のアクティブ run に seq 12 以降から再接続
|
|
281
|
+
curl -N -H "Authorization: Bearer $API_TOKEN" \
|
|
282
|
+
"http://localhost:8080/api/chat/stream?workspace_id=ws_abc123&cursor=12"
|
|
283
|
+
```
|
|
284
|
+
|
|
254
285
|
### Self-Reflection Schedule (自己反省時間)
|
|
255
286
|
|
|
256
287
|
The minion has a built-in daily scheduler that automatically runs end-of-day processing
|
|
@@ -714,10 +745,32 @@ Web ページの読み取り・要約・情報抽出をミニオン内のサブ
|
|
|
714
745
|
```json
|
|
715
746
|
{
|
|
716
747
|
"url": "https://example.com/article/123",
|
|
717
|
-
"hint": "本文と著者を抽出してほしい (任意, 抽出フィールドのヒント)"
|
|
748
|
+
"hint": "本文と著者を抽出してほしい (任意, 抽出フィールドのヒント)",
|
|
749
|
+
"scroll": {
|
|
750
|
+
"strategy": "count",
|
|
751
|
+
"targetItems": 50,
|
|
752
|
+
"itemSelector": ".feed-item",
|
|
753
|
+
"maxScrolls": 20,
|
|
754
|
+
"maxMs": 15000,
|
|
755
|
+
"settleMs": 600
|
|
756
|
+
}
|
|
718
757
|
}
|
|
719
758
|
```
|
|
720
759
|
|
|
760
|
+
**`scroll` (任意, v4.7.0〜):** 無限スクロール / 遅延ロードのページで「どこまでコンテンツを読み込むか」を**呼び出し側が宣言**するためのオプション。省略時はスクロールしない (従来動作)。
|
|
761
|
+
|
|
762
|
+
| フィールド | 説明 |
|
|
763
|
+
|-----------|------|
|
|
764
|
+
| `strategy` | `"count"` (件数到達まで) / `"untilStable"` (件数=増加が止まるまで) / `"fixed"` (回数固定)。未指定/不正ならスクロールしない |
|
|
765
|
+
| `targetItems` | `count` の目標件数。`itemSelector` が解決できる場合のみ有効 |
|
|
766
|
+
| `itemSelector` | 件数を数える CSS セレクタ。省略時はレシピ内の最初の `multiple: true` セレクタを流用 |
|
|
767
|
+
| `maxScrolls` | スクロール回数の上限 (default 10、サーバー上限 50) |
|
|
768
|
+
| `maxMs` | スクロールに使う最大時間 (default 15000、サーバー上限 45000) |
|
|
769
|
+
| `settleMs` | 1スクロールごとの描画待ち静止時間 (default 600) |
|
|
770
|
+
| `times` | `fixed` のスクロール回数 (default 10) |
|
|
771
|
+
|
|
772
|
+
> 値はサーバー側で上限にクランプされる。スクロール有効時はリクエスト全体のタイムアウトが 60s→120s に拡張される。
|
|
773
|
+
|
|
721
774
|
**レスポンス (success):**
|
|
722
775
|
```json
|
|
723
776
|
{
|
|
@@ -732,15 +785,24 @@ Web ページの読み取り・要約・情報抽出をミニオン内のサブ
|
|
|
732
785
|
"title": "...",
|
|
733
786
|
"content": "Markdown 本文...",
|
|
734
787
|
"structured": { "title": "...", "author": "...", "publishedAt": "..." },
|
|
735
|
-
"selectors": { "title": { "selector": "h1" }, "author": { "selector": "a[rel=author]" } }
|
|
788
|
+
"selectors": { "title": { "selector": "h1" }, "author": { "selector": "a[rel=author]" } },
|
|
789
|
+
"scrollInfo": { "scrolls": 12, "items": 50, "reachedTarget": true, "stoppedReason": "reachedTarget" }
|
|
736
790
|
}
|
|
737
791
|
```
|
|
738
792
|
|
|
793
|
+
- `scrollInfo` は `scroll` 指定時のみ含まれる。目標未達で上限打ち切りの場合は `reachedTarget: false` と `warning` が返るので、`maxScrolls` / `maxMs` を上げて再試行できる (サイレントに打ち切らない)。
|
|
794
|
+
|
|
739
795
|
**動作:**
|
|
740
|
-
- 初回アクセス (cold): Playwright でレンダリング → Readability で本文抽出 → Anthropic Haiku
|
|
741
|
-
- 2回目以降 (hot): URL 正規化・テンプレート化 → DOM フィンガープリントで保存済みレシピを照合 →
|
|
796
|
+
- 初回アクセス (cold): Playwright でレンダリング (**DOM が静止するまで待機**) → Readability で本文抽出 → Anthropic Haiku でセレクタ + `ready_selector` (描画完了の合図となる要素) を生成 → SQLite (`page_recipes`) に保存 → セレクタで再抽出して返却
|
|
797
|
+
- 2回目以降 (hot): URL 正規化・テンプレート化 → DOM フィンガープリントで保存済みレシピを照合 → **`ready_selector` の出現を待機**してからセレクタで抽出 (LLM 呼び出しなし)
|
|
742
798
|
- セルフヒール: hot 実行で空結果が返ったら `fail_count++`、3回失敗で破棄して次回 cold 再生成
|
|
743
799
|
|
|
800
|
+
**SPA (クライアントレンダリング) への対応 (v4.7.0〜):**
|
|
801
|
+
- `page.goto` は `domcontentloaded` で解決するが、SPA はその時点では中身が空のシェルなので、ナビゲーション後に追加で描画完了を待つ:
|
|
802
|
+
1. レシピに `ready_selector` があればその要素の出現を待つ
|
|
803
|
+
2. 無ければ **DOM が `settleMs` の間ミューテーションしなくなるまで待つ** (MutationObserver、コンテンツ量に依存せず自己校正)
|
|
804
|
+
- これにより「SPA の描画が始まる前に空 DOM を掴んでタイムアウト/空結果になる」問題を回避する
|
|
805
|
+
|
|
744
806
|
**URL 正規化ルール:**
|
|
745
807
|
- `utm_*` `fbclid` `gclid` `ref` 等のトラッキングクエリは除去
|
|
746
808
|
- `page` `p` `offset` 等のページネーション値は `:n` プレースホルダ化
|
|
@@ -932,6 +994,20 @@ Response:
|
|
|
932
994
|
|
|
933
995
|
同名キーがある場合はワークスペース別が優先される。`/api/secrets/*` エンドポイントは `?workspace_id=<uuid>` クエリパラメータでスコープを指定する。省略または空文字でミニオン全体を操作する。シークレット値はHQ DBには保存されず、HQ APIはpass-throughとして中継するのみ。
|
|
934
996
|
|
|
997
|
+
#### スコープ間の移動
|
|
998
|
+
|
|
999
|
+
誤ったワークスペーススコープに登録してしまったシークレット/変数は、削除・再登録せずに別スコープへ**移動**できる。移動はミニオン内部で1トランザクションとして実行され、シークレット値はミニオンの外に出ない。
|
|
1000
|
+
|
|
1001
|
+
| Method | Endpoint | Description |
|
|
1002
|
+
|--------|----------|-------------|
|
|
1003
|
+
| POST | `/api/variables/:key/move` | 変数を別スコープへ移動。Body: `{from_workspace_id, to_workspace_id}` |
|
|
1004
|
+
| POST | `/api/secrets/:key/move` | シークレットを別スコープへ移動。Body: `{from_workspace_id, to_workspace_id}` |
|
|
1005
|
+
|
|
1006
|
+
- `from_workspace_id` / `to_workspace_id` は省略または空文字 `""` でミニオン全体スコープを指す。
|
|
1007
|
+
- 移動先に同名キーが既に存在する場合、**上書きせず `409` を返す**(値の消失を防ぐため)。先に移動先の既存キーを削除すること。
|
|
1008
|
+
- 移動元にキーが無い場合は `404`、移動元と移動先が同一スコープの場合は `400`。
|
|
1009
|
+
- シークレットを移動した場合、ミニオン全体スコープから外れると `process.env` から除去され、ミニオン全体スコープに入ると `process.env` に反映される。
|
|
1010
|
+
|
|
935
1011
|
### Project Tasks
|
|
936
1012
|
|
|
937
1013
|
タスクは5段階Kanban (`backlog`/`todo`/`doing`/`review`/`done`)で管理される。親子関係は2階層まで(孫タスク禁止)。担当は **ミニオンか人間の二択**(両方は不可、後勝ち null)。
|
|
@@ -2160,6 +2236,8 @@ POST `/api/minion/workspaces/:id/notes` body:
|
|
|
2160
2236
|
|
|
2161
2237
|
PATCH body は workspace 版と共通: `{title?, content?, change_summary?}` (status は workspace 経由のみ受理)。
|
|
2162
2238
|
|
|
2239
|
+
> **ロックされたノート (423 Locked):** ユーザーが「ロック」したノート(パスワード等の保護用)は、ミニオンからの更新がすべて拒否され、`HTTP 423` (`{"code":"note_locked"}`) が返る。**ロックの解除は人間のみが HQ ダッシュボードで行える。ミニオンは解除できない。** 423 を受け取ったら、上書きを試み続けず、必要なら threads でユーザーにロック解除を依頼すること。ノート詳細(GET)では `is_locked` / `is_masked` フィールドで状態を確認できる(`is_masked` は HQ UI 上の表示マスクで、API レスポンスの本文には影響しない)。
|
|
2240
|
+
|
|
2163
2241
|
#### hq CLI ラッパー
|
|
2164
2242
|
|
|
2165
2243
|
```bash
|
package/docs/task-guides.md
CHANGED
|
@@ -20,16 +20,34 @@ curl -X POST http://localhost:8080/api/web/extract \
|
|
|
20
20
|
|
|
21
21
|
このAPIは内部で Playwright + Readability を回して **メインセッションには結果 JSON だけ返す** ため、Playwright MCP を使うときに起きていたチャットコンテキストのトークン肥大化が回避できる。
|
|
22
22
|
|
|
23
|
+
### SPA / 無限スクロールのページ (v4.7.0〜)
|
|
24
|
+
|
|
25
|
+
- **SPA (React/Vue 等でクライアント描画するページ)** もそのまま `/api/web/extract` でよい。内部で DOM が静止するまで待ってから抽出するため、空シェルを掴む問題は解消済み。
|
|
26
|
+
- **無限スクロール / 「もっと見る」で件数が増えるページ**で十分な件数を確保したい場合は `scroll` オプションを付ける。**どれだけ集めるかは呼び出し側が決める**:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
curl -X POST http://localhost:8080/api/web/extract \
|
|
30
|
+
-H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \
|
|
31
|
+
-d '{
|
|
32
|
+
"url": "対象URL",
|
|
33
|
+
"scroll": { "strategy": "count", "targetItems": 50, "maxScrolls": 20, "maxMs": 15000 }
|
|
34
|
+
}' | jq
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- `count`: 目標件数 (`targetItems`) に達するまでスクロール。`untilStable`: 件数が増えなくなるまで。`fixed`: 回数固定。
|
|
38
|
+
- レスポンスの `scrollInfo.reachedTarget` が `false` なら上限で打ち切られている → `maxScrolls` / `maxMs` を上げて再試行する。
|
|
39
|
+
- スクロール上限はサーバー側でクランプされる (maxScrolls≤50 / maxMs≤45s)。それ以上の網羅が要るなら**ページネーションURLをループ呼び出し**する方が確実。
|
|
40
|
+
|
|
23
41
|
### Playwright MCP を使うべき場面
|
|
24
42
|
|
|
25
43
|
`/api/web/extract` で対応できないのは以下のケース。このときだけ `mcp__playwright__*` を使う:
|
|
26
44
|
|
|
27
45
|
- ログイン必須ページ (Cookie/2FA 等の認証必要)
|
|
28
46
|
- フォーム入力・複数ページ遷移を伴う操作
|
|
29
|
-
-
|
|
47
|
+
- 「もっと見る」**ボタンのクリック**で追加ロードするページ (スクロールでは増えないもの。`scroll` はスクロール式のみ対応)
|
|
30
48
|
- Lancers コンペ応募など、明らかに対話的操作が必要なフロー
|
|
31
49
|
|
|
32
|
-
|
|
50
|
+
**単純な閲覧・抽出 (SPA・無限スクロール含む) では MCP を使わない。**
|
|
33
51
|
|
|
34
52
|
### よくあるパターン
|
|
35
53
|
|