Compare commits
194 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5d696556e | |||
| 5919c6b90f | |||
| b6ef0f07dc | |||
| 507620e313 | |||
| 1e94f8451e | |||
| dcb6ad693b | |||
| 3b0238bb28 | |||
| 788295ed71 | |||
| 797e7f751c | |||
| d1c44a5878 | |||
| a7e23badf2 | |||
| eebb691426 | |||
| c76e10d314 | |||
| 9a29eb5af6 | |||
| 8d49d883c0 | |||
| 417d69ceb3 | |||
| 035e8b255b | |||
| 1c630ecfd4 | |||
| a99d3e5a3e | |||
| 894408181f | |||
| d022ca067b | |||
| 6715402bcc | |||
| c4cee35adb | |||
| ae81d22775 | |||
| 9740794050 | |||
| 453dec60b1 | |||
| bfc98fe41a | |||
| be9b92eb28 | |||
| 19c82df05f | |||
| 23d36b311a | |||
| ebd4bf5d97 | |||
| 889b70b8f9 | |||
| 21be6d19f7 | |||
| 6846d6ddae | |||
| 8eb10049b8 | |||
| 831cb62d2e | |||
| 894d9abeb1 | |||
| 3447efc94e | |||
| 4ca23c256a | |||
| aa6b899276 | |||
| 466e56bc2d | |||
| 07cc7f51b5 | |||
| 42cb1d7aa9 | |||
| e730b3f831 | |||
| 84471659af | |||
| 93b1140bf1 | |||
| 962d863ef7 | |||
| 7dbd4f59e7 | |||
| d779a11958 | |||
| c7a0b317cb | |||
| 046ceba29c | |||
| ac4fb56c91 | |||
| 1e2ef6806a | |||
| 5388f40c03 | |||
| 83aa1f6bb2 | |||
| 9344562258 | |||
| 7ba78552a4 | |||
| 60688c3108 | |||
| 36a5630c63 | |||
| 05b215c9f7 | |||
| 0ff369f818 | |||
| fe0ee8296a | |||
| b9b5d98d75 | |||
| 3d21bf0725 | |||
| 5783420fcf | |||
| 1eefce952b | |||
| 6c5706ba7d | |||
| 5d78806806 | |||
| 1b6b775c0e | |||
| 66f1b59f7f | |||
| eee3f93815 | |||
| f3af5d3631 | |||
| 47c6aaea03 | |||
| 5b983c47b8 | |||
| 36cdc8fba6 | |||
| 0d49989c19 | |||
| c735b911a0 | |||
| eefbed98b6 | |||
| a9a47b7c37 | |||
| e688d805ea | |||
| c9feb15a37 | |||
| 5f5c0a89e2 | |||
| 323ccc8475 | |||
| 9f777ff43e | |||
| 46afea83c2 | |||
| 341bcb13c8 | |||
| 832f270f52 | |||
| 5c5109cd45 | |||
| e744ad1f96 | |||
| cae0d7be3b | |||
| 062757f1b1 | |||
| 9c9aff0046 | |||
| d48f83ae6f | |||
| ea1c0571c1 | |||
| 7e631a890a | |||
| 7e87a3336b | |||
| 1794072179 | |||
| 747dc3f843 | |||
| c56863a7e0 | |||
| fc6c98cd58 | |||
| 0a426c4e16 | |||
| b8ca98cb49 | |||
| cda1c7b261 | |||
| fac7d6c9bb | |||
| 0686d39fa1 | |||
| 156cdcbe7b | |||
| 5cadc60e36 | |||
| e28e19069f | |||
| cdca296044 | |||
| e8bd518efa | |||
| 159f0b07dc | |||
| 9d38d580f2 | |||
| 0860e84d22 | |||
| 6d4980d3d7 | |||
| ccb86481ae | |||
| eb9f2db513 | |||
| 429b2d965f | |||
| f6728974ea | |||
| 2ff36962be | |||
| e36cd2d990 | |||
| ba92d10a3f | |||
| a909072dc1 | |||
| 11e772496f | |||
| 53c6334fd9 | |||
| d7fab7c6aa | |||
| 139d4c5ed1 | |||
| e7fe83a872 | |||
| 8d19d0b2d7 | |||
| a8d246ebe9 | |||
| 11ffd42899 | |||
| 24f989d818 | |||
| 9bbd25fdfc | |||
| 76b89be6d7 | |||
| beb0653e15 | |||
| 646e689b65 | |||
| dd78b770a9 | |||
| ab8e07201f | |||
| ad367e450d | |||
| 5a164843ef | |||
| 1b687fedb0 | |||
| 98d87d5d3f | |||
| 5edb97c49d | |||
| efc29b78c2 | |||
| 8d8b01d240 | |||
| df1b953d1f | |||
| 20c7610371 | |||
| b9b94d7852 | |||
| 762d2f5141 | |||
| 10dda9e6f5 | |||
| 289cd495b4 | |||
| 343572bf5d | |||
| bdfc2e3e0b | |||
| 15fe012006 | |||
| bad68ef149 | |||
| 0ba685af9f | |||
| 7eeab9f26f | |||
| 1ee0dbd2fe | |||
| 65d85eb08f | |||
| 2324537165 | |||
| b29de1b216 | |||
| a73de77a7f | |||
| ffd50e7510 | |||
| 98869c5abc | |||
| d6a051dbfc | |||
| 14d4d65848 | |||
| ca57fd6a1a | |||
| e1071edfe8 | |||
| fa2ecf7226 | |||
| 50836a3ec8 | |||
| 0ca3af4d1a | |||
| a82c9bbb82 | |||
| 6e0df065fd | |||
| 6ff1355fe8 | |||
| eaaf894b23 | |||
| 7bd7b4b26a | |||
| db0a39fdbd | |||
| 8accdacc3c | |||
| fde1827928 | |||
| c01c6c4d02 | |||
| 204cd5bff3 | |||
| 1e85dfb49b | |||
| 52dbd31dea | |||
| 3577e9255c | |||
| 1260d8cffb | |||
| 7f409646e5 | |||
| 18c0846af0 | |||
| cbebb36c01 | |||
| 2e1005ea67 | |||
| 1c4f79b623 | |||
| e90688c608 | |||
| 1bc83c7124 | |||
| 5cd790c94a | |||
| 5491409003 | |||
| 0617dab70a |
@@ -4,6 +4,26 @@
|
||||
|
||||
---
|
||||
|
||||
## 生命週期(單一活性鐵律,全文見 `system-dev/docs/3-specs/SDD-LIFECYCLE.md`)
|
||||
|
||||
五條鐵律摘要:
|
||||
|
||||
1. **單一活性**:任何時刻整個 repo 只允許一份 `status: active` 的 SDD;所有開發任務對應它的 tasks,找不到對應任務 → 停下來問,不准直接做。
|
||||
2. **禁止自行建立 SDD**:澄清問題→回答不動文件;任務層變更→更新現行 SDD 的 tasks(標日期與原因);規格層變更→走第 3 條。
|
||||
3. **規格變更只有一條路**:change proposal 寫進 `system-dev/docs/3-specs/pending-changes.md`(摘要+觸發原因+影響分析),然後**停止**等使用者「confirm」。
|
||||
4. **開新 SDD 的唯一時機**:使用者 confirm 後——先把舊 SDD 未完成任務逐條搬入新 SDD(做完前不准寫 code)→ 舊的標 `closed` + `superseded_by` 移入 `archive/` → 新 SDD changelog 記繼承 → 列搬移/作廢清單請最終確認。
|
||||
5. **每次 session 開始**先讀 active SDD 與 pending-changes.md,回報三個數字:
|
||||
|
||||
```
|
||||
📐 現行規格:〈SDD 名稱〉
|
||||
📋 未完成任務:N
|
||||
⚖️ 待裁決 proposal:M
|
||||
```
|
||||
|
||||
若出現**兩份 active=規則已被違反,當場糾正**(收斂到一份,其餘 paused/closed)。
|
||||
|
||||
---
|
||||
|
||||
## 執行流程
|
||||
|
||||
### 第一步:理解任務
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
# /wiki-extract — vault 增量萃取(Logseq / Obsidian → system-dev/wiki)
|
||||
|
||||
把**筆記 vault**(Logseq graph 如 `notes`/`kb`、或 Obsidian)的原始筆記,**增量、冪等**地
|
||||
萃成 `system-dev/wiki/` 的精耕卡+`[[wikilink]]`。這是知識一庫 ingest 的**前段**:
|
||||
AI 只產卡片檔,下游 Arcrun ingest 再從 wikilink 機械拉三元組進 KBDB。
|
||||
|
||||
> **跟 `/wiki-init` 的分工**:
|
||||
> - `/wiki-init` 是**首次**建結構 + 全庫首萃(一次性)。
|
||||
> - `/wiki-extract` 是**之後每次**的增量重萃——vault 會被 Syncthing/cron 持續灌新筆記,
|
||||
> 這支負責「只萃變動的、沒變的不碰、不浪費 AI run」。給 Routine / cloud-worker 反覆跑。
|
||||
> - **跑它的是你(CC / Routine)=LLM 本人,不需任何 token**。
|
||||
|
||||
> **邊界(硬規矩,別越界)**
|
||||
> - 只往 `system-dev/wiki/` 寫。**絕不寫入 KBDB、絕不拉三元組紀錄**——三元組是下游
|
||||
> Arcrun 從你產的 `[[wikilink]]` + `## 關聯` 機械映射(另一張 issue),不是這支的事。
|
||||
> - **原始筆記唯讀**:`journals/`、`pages/`、Obsidian 根 `.md` 是 leo 的手寫真身,
|
||||
> 改了會被 Syncthing 推回他手機污染筆記 App。萃取=只讀原文、只寫 wiki。
|
||||
> - **D16 精耕非 RAG**:萃「知識點」成自包含原子卡 + 建 wikilink,**不地毯灌原文全文**。
|
||||
|
||||
---
|
||||
|
||||
## 執行流程
|
||||
|
||||
### 第一步:確認這是 vault repo,定位 raw source
|
||||
|
||||
偵測邏輯**同 install.sh / wiki-init**:
|
||||
|
||||
| 偵測到 | 型態 | raw source(要掃的原文) |
|
||||
|--------|------|--------------------------|
|
||||
| 根目錄有 `logseq/` | Logseq vault | `journals/*.md` + `pages/*.md` |
|
||||
| 根目錄有 `.obsidian/` | Obsidian vault | vault 根下所有 `.md` |
|
||||
| 都沒有 | **不是 vault** | → 停手。這支只處理 vault;一般 dev repo 開發時就手寫 `.claude`/`system-dev/wiki`,不需萃取 |
|
||||
|
||||
沒有 `system-dev/wiki/`?→ 先跑 `/wiki-init`(首次建結構+首萃),再回來用這支做增量。
|
||||
|
||||
### 第二步:content_hash 冪等 —— 決定哪些檔要萃(省 run 的核心)
|
||||
|
||||
讀萃取 manifest:`system-dev/wiki/.extract-manifest.json`(不存在=首次,視同全部要萃)。
|
||||
格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"algo": "sha256",
|
||||
"sources": {
|
||||
"journals/2026_07_01.md": {
|
||||
"content_hash": "<sha256 of file bytes>",
|
||||
"extracted_at": "2026-07-06",
|
||||
"cards": ["Prompt能力即拆解自己邏輯的能力", "程式化邏輯可圖解任何主題不限AI"],
|
||||
"skipped_reason": null
|
||||
},
|
||||
"journals/2026_06_25.md": {
|
||||
"content_hash": "<sha256>",
|
||||
"extracted_at": "2026-07-06",
|
||||
"cards": [],
|
||||
"skipped_reason": "空檔/訊息量不足,無可萃知識點"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
對每個 raw source 檔:
|
||||
|
||||
1. 算目前 `content_hash`(`sha256sum <file>`,取檔案 bytes 的 hash)。
|
||||
2. 跟 manifest 裡該檔的 `content_hash` 比:
|
||||
- **相同 → skip,不讀不萃、不呼叫任何 AI 推理**(就算它上次 `cards: []` 也 skip——空檔沒變還是空)。
|
||||
- **不同或不在 manifest → 這檔要(重)萃**。
|
||||
3. manifest 有、但檔已不存在 → 該檔被刪,把它的 entry 從 manifest 移除(卡片是否連帶處理見第五步)。
|
||||
|
||||
> **這一步是「省 run」的重點**:vault 每天可能只動 1~2 個 journal,其餘幾十個檔 hash 沒變
|
||||
> 就整批跳過,AI 只對真正變動的檔動腦。**重跑一個沒變動的 vault = 零 AI 呼叫、零 diff。**
|
||||
|
||||
### 第三步:對「要萃」的檔,抓知識點 + 任務
|
||||
|
||||
逐個變動檔讀原文,分兩類抽取:
|
||||
|
||||
**(a) 知識點 → 概念原子卡**
|
||||
判準與卡片格式**完全依 `/wiki-init` 第五步**(frontmatter `tags:`/`gloss:`、H1、麵包屑
|
||||
`← [[<bucket>/00-INDEX]]`、`**來源**`、`## 摘要`、`## 重點`、`## 實體`、`## 關聯` 的
|
||||
typed-edge 三元組、TAXONOMY 受控標籤、硬自檢等)——**不在這裡重寫格式,一律回去讀那份**。
|
||||
廢話/訊息量薄的段落略過(在 manifest 記 `skipped_reason`,誠實留痕、不留卡)。
|
||||
|
||||
**(b) Logseq 任務 marker → 任務卡(task_status)**
|
||||
解析**完全依** `system-dev/docs/4-guides/logseq-markers.md`(單一真相源,與 template#4
|
||||
tasks 投影共用同一套;**別自己另寫 mapping**)。摘要:
|
||||
|
||||
- 任務行 regex:`^\s*- (TODO|DOING|NOW|LATER|WAITING|DONE|CANCELED|CANCELLED)\s+`
|
||||
- 狀態正規化:TODO/LATER→`todo`、DOING/NOW→`in-progress`、WAITING→`blocked`、
|
||||
DONE→`done`、CANCELED/CANCELLED→`closed`。
|
||||
- 跳過 `:LOGBOOK:…:END:` 區塊與 `key:: value` 屬性行(`collapsed::`、`id::`、
|
||||
`SCHEDULED::`、`DEADLINE::`…),**別把 marker 或屬性當任務內文**。
|
||||
|
||||
有實質內容的任務 → 產一張任務卡進 `cards/tasks/` bucket,frontmatter 帶 `task_status`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
tags: [<領域標籤,依 TAXONOMY>]
|
||||
task_status: todo # ← 依上表正規名;這是任務卡才有的欄位
|
||||
gloss: 一句話定義這個任務要達成什麼(供下游 normalize)
|
||||
---
|
||||
# <任務一句話標題(marker 後的內文,去掉 marker)>
|
||||
|
||||
← [[tasks/00-INDEX]]
|
||||
|
||||
**來源**:`journals/2026_07_01.md`(TODO block)
|
||||
**最後更新**:YYYY-MM-DD
|
||||
|
||||
## 摘要
|
||||
[任務要做什麼、脈絡]
|
||||
|
||||
## 實體
|
||||
- **<關鍵實體正規名>**(<同義詞>)— <一句描述>
|
||||
|
||||
## 關聯
|
||||
### 內文知識關係(端點=上方 `## 實體` 正規名,一字不差)
|
||||
- <實體A> >> <謂詞> >> <實體B>
|
||||
### 卡片關係(卡對卡)
|
||||
- [[本任務卡]] >> 涉及 >> [[相關概念卡]]
|
||||
```
|
||||
|
||||
> 純瑣事任務(「買菜」這種無知識量)不必成獨立卡——可在 `cards/tasks/00-INDEX.md`
|
||||
> 列一行帶狀態即可,避免灌垃圾卡。判準同 D16:有沒有知識/專案價值。
|
||||
|
||||
### 第四步:更新桶索引與 INDEX
|
||||
|
||||
- 每個動到的 bucket(如 `cards/notes/`、`cards/tasks/`)更新其 `00-INDEX.md`
|
||||
(容器:只連不重寫,H2/H3 分節)。
|
||||
- 更新 `system-dev/wiki/INDEX.md` 的標籤視圖與卡片清單。
|
||||
- 任務卡可在 INDEX 開一個「任務視圖」按 `task_status` 聚類。
|
||||
|
||||
### 第五步:寫回 manifest + 驗證原文 0 動
|
||||
|
||||
1. 把這次萃過的每個檔的**新 `content_hash`**、`extracted_at`、產出的 `cards`、
|
||||
(或 `skipped_reason`)寫回 `system-dev/wiki/.extract-manifest.json`。
|
||||
**沒動到的檔的 entry 原樣保留**(別整檔重寫掉別人的 hash)。
|
||||
2. 驗證原文零異動(踩過的坑):
|
||||
```
|
||||
git status --short journals/ pages/ # Obsidian 則看根目錄 .md ——須 0 新增 0 修改
|
||||
```
|
||||
有任何原文變動 → 你誤寫了 raw source,回滾。
|
||||
|
||||
### 第六步:完成報告
|
||||
|
||||
```
|
||||
✅ wiki-extract 完成(增量)
|
||||
掃描:N 個 raw source 檔
|
||||
萃取:M 個(content_hash 變動)→ 產出 X 張概念卡 + Y 張任務卡
|
||||
跳過:K 個(hash 未變,零 AI 呼叫)
|
||||
任務狀態分布:todo A / in-progress B / done C / …
|
||||
原文驗證:journals/ pages/ git status 0 異動 ✅
|
||||
manifest:system-dev/wiki/.extract-manifest.json 已更新
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 冪等自檢(Routine 反覆跑必守)
|
||||
|
||||
- [ ] 跑之前先讀 manifest,hash 相同的檔**完全不進 AI**(不是「讀了才發現一樣」,是靠 hash 先擋)。
|
||||
- [ ] 對「同一個沒變動的 vault」連跑兩次:第二次應是**零萃取、零卡片 diff、零 manifest 變化**。
|
||||
- [ ] 只有 `system-dev/wiki/` 有寫入;`journals/`、`pages/` git status 全乾淨。
|
||||
- [ ] 任務狀態用正規名,marker/屬性沒混進內文(照 `logseq-markers.md` 自檢)。
|
||||
@@ -228,3 +228,8 @@ git status --short pages/ journals/ # 或一般專案的 docs/ ——須 0
|
||||
原文驗證:pages/ journals/ git status 0 異動 ✅
|
||||
下一步:用 /wiki-capture 把重要決策存進 wiki
|
||||
```
|
||||
|
||||
> **vault repo 首萃後的增量重萃**:Logseq / Obsidian vault 會被持續灌新筆記。首萃(本命令)
|
||||
> 之後,改用 **`/wiki-extract`** 做增量——它靠 content_hash 只萃變動的檔(沒變=零 AI 呼叫),
|
||||
> 並解析 Logseq 大寫任務 marker(TODO/DOING/DONE…→ `task_status`,見
|
||||
> `system-dev/docs/4-guides/logseq-markers.md`)。適合掛給 Routine / cloud-worker 反覆跑。
|
||||
|
||||
@@ -239,6 +239,7 @@ if [[ "$FILE_PATH" == *"docs/3-specs/"* ]]; then
|
||||
"docs/3-specs/resumable-workflow" # richblack 確認新建(可恢復工作流)
|
||||
"docs/3-specs/workflow-discovery" # 2026-06-27 總管 issue #8 交辦新建(工作流 description slot + search_workflow,北極星入口缺口)
|
||||
"docs/3-specs/thin-shell-alignment" # 2026-06-27 總管 issue #11 交辦新建(CLI/MCP 薄殼漂移全面盤點 + 防複發機制)
|
||||
"docs/3-specs/portal-auth" # 2026-07-13 總管 issue #24/#25 交辦新建(RAG Portal 多人授權,rag-wave1 T1/T2)
|
||||
)
|
||||
IN_KNOWN=false
|
||||
for K in "${KNOWN_SDDS[@]}"; do
|
||||
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
# publish-lag-check.sh — SessionStart hook:偵測「公開 mirror 落後工作區」並出聲
|
||||
#
|
||||
# 病根(leo 2026-07-21 點名的真實風險):
|
||||
# Gitea(草稿/工作現場)與 GitHub(正稿/成品櫥窗)是**手動同步**的
|
||||
# (靠人跑 scripts/publish-github.sh --push,且需 D20 arm)。
|
||||
# → 改了零件、重編 wasm 後若沒人記得發佈,**用戶抓到舊版且沒有任何錯誤訊息,
|
||||
# 只是行為不對**——這種靜默失敗只有外部使用者會撞到,我們自己永遠測不到。
|
||||
#
|
||||
# 實例:安裝器的懶載會從
|
||||
# cdn.jsdelivr.net/gh/youlinhsieh/Arcrun@main/.component-builds/<名>/component.wasm
|
||||
# 抓 wasm。那個位址永遠指向 GitHub 上的**最後一次發佈**,不是我們本機的最新版。
|
||||
#
|
||||
# 原理:比對「工作區 HEAD」與「.github-public 最後一個 release commit 記錄的 snapshot」。
|
||||
# publish-github.sh 的 commit 訊息格式固定為:release: snapshot <短hash> (<日期>)
|
||||
# → 從中取出 hash,看它是不是工作區 HEAD 的祖先/相同。
|
||||
#
|
||||
# 只提醒不阻擋(exit 0):發不發佈是人的決定(且 push GitHub 需 leo 親跑 arm),
|
||||
# hook 的職責只是消滅「忘了」這個失敗模式。
|
||||
set -euo pipefail
|
||||
|
||||
MIRROR_DIR=".github-public"
|
||||
|
||||
# 沒裝發佈管線的 repo 直接安靜退出
|
||||
[ -d "$MIRROR_DIR/.git" ] || exit 0
|
||||
[ -f "scripts/publish-github.sh" ] || exit 0
|
||||
git rev-parse --git-dir >/dev/null 2>&1 || exit 0
|
||||
|
||||
HEAD_SHORT="$(git rev-parse --short HEAD 2>/dev/null || echo '')"
|
||||
[ -z "$HEAD_SHORT" ] && exit 0
|
||||
|
||||
# 從 mirror 最後一個 commit 訊息取出它當初發佈的來源 hash
|
||||
LAST_MSG="$(git -C "$MIRROR_DIR" log -1 --format=%s 2>/dev/null || echo '')"
|
||||
PUBLISHED="$(printf '%s' "$LAST_MSG" | sed -n 's/.*snapshot \([0-9a-f]\{6,\}\).*/\1/p')"
|
||||
|
||||
if [ -z "$PUBLISHED" ]; then
|
||||
# mirror 存在但沒有可辨識的 release commit(可能還沒發過)
|
||||
echo "════════════════════════════════════════════════"
|
||||
echo "📦 這個 repo 有公開發佈管線,但 mirror 還沒發過任何版本"
|
||||
echo "════════════════════════════════════════════════"
|
||||
echo " 若已有用戶依賴公開版(例如安裝器從 jsDelivr 抓 wasm),現在是空的。"
|
||||
echo " 發佈:leo 在頂層跑 scripts/github-arm.sh,再於本 repo 跑"
|
||||
echo " GITHUB_REMOTE=... bash scripts/publish-github.sh --push"
|
||||
echo ""
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 已發佈的那個 commit 就是現在的 HEAD → 同步,安靜
|
||||
if [ "$PUBLISHED" = "$HEAD_SHORT" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 算出落後幾個 commit(發佈點 → HEAD)。取不到就不顯示數字。
|
||||
BEHIND="$(git rev-list --count "${PUBLISHED}..HEAD" 2>/dev/null || echo '')"
|
||||
|
||||
# 落後 0 且 hash 不同 → 可能是 mirror 比工作區新(罕見,例如剛 rebase),一樣提醒
|
||||
echo "════════════════════════════════════════════════"
|
||||
if [ -n "$BEHIND" ] && [ "$BEHIND" != "0" ]; then
|
||||
printf '📤 公開 mirror 落後工作區 %s 個 commit(最後發佈:%s,現在:%s)\n' \
|
||||
"$BEHIND" "$PUBLISHED" "$HEAD_SHORT"
|
||||
else
|
||||
printf '📤 公開 mirror 與工作區不一致(最後發佈:%s,現在:%s)\n' "$PUBLISHED" "$HEAD_SHORT"
|
||||
fi
|
||||
echo "════════════════════════════════════════════════"
|
||||
echo "⚠️ 外部使用者拿到的仍是舊版,而且**不會有任何錯誤訊息**——只是行為不對。"
|
||||
echo " (安裝器的懶載直接從公開位址抓 wasm,落後=裝到舊零件。)"
|
||||
echo ""
|
||||
echo " 要發佈:① leo 在頂層跑 bash scripts/github-arm.sh \"<任務描述>\" 30"
|
||||
echo " ② 本 repo 跑 GITHUB_REMOTE=https://github.com/<帳號>/<repo>.git \\"
|
||||
echo " bash scripts/publish-github.sh --push"
|
||||
echo " 不急著發也沒關係——這只是提醒,別讓它靜默漏掉。"
|
||||
|
||||
# 若這次落後的內容碰到 wasm,額外警告(那是用戶會直接抓的東西)
|
||||
if git diff --name-only "${PUBLISHED}..HEAD" 2>/dev/null | grep -q '\.wasm$'; then
|
||||
echo ""
|
||||
echo " 🔴 這批改動**包含 .wasm 變更** → 用戶抓到的零件會跟你本機不同,優先發佈。"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
exit 0
|
||||
+82
-14
@@ -1,10 +1,16 @@
|
||||
#!/bin/bash
|
||||
# PreToolUse hook — 動 code 前檢查有沒有對應 SDD
|
||||
# PreToolUse hook — 動 code 前檢查 SDD + 單一活性 SDD 鐵律(issue #6)
|
||||
# wishlist §2:把 /sdd-check 從「命令要人打」升級成「hook 自動攔」。
|
||||
# 生命週期規則全文:system-dev/docs/3-specs/SDD-LIFECYCLE.md
|
||||
#
|
||||
# 掛在 settings.json 的 PreToolUse(matcher: Write|Edit)。
|
||||
# stdin 收到 JSON:{ tool_name, tool_input: { file_path, ... } }
|
||||
# 行為:動到 code 檔(.ts/.go/...)但 system-dev/docs/3-specs/ 下沒有任何 SDD → 警告(exit 2 擋)。
|
||||
# 行為:
|
||||
# 1. status: active 的 SDD > 1 份 → 單一活性鐵律已被違反,**不論寫什麼檔**一律擋(exit 2),
|
||||
# 先收斂到一份再說。
|
||||
# 2. 動 code 檔(.ts/.go/...)→ 需要「恰好 1 份」active SDD;0 份 → 擋。
|
||||
# 3. 向下相容:3-specs 下完全沒有任何 design.md 帶 frontmatter(老 repo 尚未遷移生命週期制度)
|
||||
# → 退回舊行為:有 design.md 就放行+提醒,沒有才擋。避免 template update 後老 repo 立刻全紅。
|
||||
#
|
||||
# 誠實限制(抄 arcrun):只擋語法層明顯違規(直接寫 code 檔)。
|
||||
# 藏在 helper 裡、用 bash 繞道的改動擋不到。
|
||||
@@ -24,6 +30,42 @@ fi
|
||||
# 拿不到路徑 → 不擋(容錯,寧可放過也不誤殺)
|
||||
[ -z "$FILE_PATH" ] && exit 0
|
||||
|
||||
SPECS_DIR="system-dev/docs/3-specs"
|
||||
|
||||
# ── 統計 active / frontmatter ──────────────────────
|
||||
# 排除 archive/(已封存)與 TEMPLATE(範本自帶 status: draft frontmatter,不算數——
|
||||
# 否則 update 一鋪新版 TEMPLATE-sdd,老 repo 就被誤判「已遷移」而全紅,向下相容破功)。
|
||||
# frontmatter 判定=design.md 前 10 行有 ^status: 行(機器可查,見 SDD-LIFECYCLE.md)。
|
||||
ACTIVE_COUNT=0
|
||||
FM_COUNT=0
|
||||
ACTIVE_LIST=""
|
||||
if [ -d "$SPECS_DIR" ]; then
|
||||
while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
HEAD10=$(head -10 "$f" 2>/dev/null || true)
|
||||
if printf '%s\n' "$HEAD10" | grep -q '^status:[[:space:]]*'; then
|
||||
FM_COUNT=$((FM_COUNT + 1))
|
||||
if printf '%s\n' "$HEAD10" | grep -q '^status:[[:space:]]*active'; then
|
||||
ACTIVE_COUNT=$((ACTIVE_COUNT + 1))
|
||||
ACTIVE_LIST="${ACTIVE_LIST} • ${f}
|
||||
"
|
||||
fi
|
||||
fi
|
||||
done < <(find "$SPECS_DIR" -name 'design.md' -not -path '*TEMPLATE*' -not -path '*/archive/*' 2>/dev/null)
|
||||
fi
|
||||
|
||||
# ── 鐵律 1:單一活性被違反(active > 1)→ 不論寫什麼檔一律擋 ──
|
||||
if [ "$ACTIVE_COUNT" -gt 1 ]; then
|
||||
cat >&2 <<EOF
|
||||
🚫 SDD 單一活性鐵律違反:偵測到 ${ACTIVE_COUNT} 份 status: active 的 SDD(任何時刻整個 repo 最多一份):
|
||||
${ACTIVE_LIST}
|
||||
請先收斂到一份:其餘改 status: paused / closed(closed 且被取代者填 superseded_by 並移入 3-specs/archive/)。
|
||||
規則全文見 system-dev/docs/3-specs/SDD-LIFECYCLE.md。收斂前擋下所有寫檔。
|
||||
(本 hook 攔 Write/Edit;修 frontmatter 可用 bash 直改,或由人裁決哪份是現行。)
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# 只管 code 檔。docs/markdown/設定檔等放行。
|
||||
case "$FILE_PATH" in
|
||||
*.ts|*.tsx|*.js|*.jsx|*.go|*.py|*.rs|*.java|*.rb|*.php|*.c|*.cpp|*.h|*.hpp|*.swift|*.kt) ;;
|
||||
@@ -36,28 +78,54 @@ case "$FILE_PATH" in
|
||||
*_test.*|*.test.*|*.spec.*|*/tests/*|*/test/*) exit 0 ;;
|
||||
esac
|
||||
|
||||
# system-dev/docs/3-specs/ 下完全沒有 design.md → 攔
|
||||
SDD_COUNT=0
|
||||
if [ -d "system-dev/docs/3-specs" ]; then
|
||||
SDD_COUNT=$(find system-dev/docs/3-specs -name 'design.md' -not -path '*TEMPLATE*' 2>/dev/null | wc -l | tr -d ' ')
|
||||
fi
|
||||
# ── 向下相容:整個 3-specs 沒有任何帶 frontmatter 的 design.md ──
|
||||
# =老 repo 還沒遷移生命週期制度 → 退回舊行為(有 design.md 就放行+提醒),
|
||||
# 避免 template update 一裝新 hook,老 repo 所有 code 寫入立刻全紅。
|
||||
if [ "$FM_COUNT" -eq 0 ]; then
|
||||
SDD_COUNT=0
|
||||
if [ -d "$SPECS_DIR" ]; then
|
||||
SDD_COUNT=$(find "$SPECS_DIR" -name 'design.md' -not -path '*TEMPLATE*' -not -path '*/archive/*' 2>/dev/null | wc -l | tr -d ' ')
|
||||
fi
|
||||
|
||||
if [ "$SDD_COUNT" -eq 0 ]; then
|
||||
cat >&2 <<EOF
|
||||
🚫 SDD 協議攔截:要動 code 檔 ($FILE_PATH),但 system-dev/docs/3-specs/ 下找不到任何 SDD。
|
||||
if [ "$SDD_COUNT" -eq 0 ]; then
|
||||
cat >&2 <<EOF
|
||||
🚫 SDD 協議攔截:要動 code 檔 ($FILE_PATH),但 ${SPECS_DIR}/ 下找不到任何 SDD。
|
||||
|
||||
絕對鐵律:任何 code 變動前必須有對應 SDD(design.md)。
|
||||
絕對鐵律:任何 code 變動前必須有對應 SDD(design.md),且遵守單一活性生命週期
|
||||
(system-dev/docs/3-specs/SDD-LIFECYCLE.md)。
|
||||
|
||||
請先:
|
||||
1. 確認這個改動屬於哪個子系統
|
||||
2. 在 system-dev/docs/3-specs/[子系統]/ 建立 design.md(可用 /sdd-check 協助)
|
||||
2. 在 ${SPECS_DIR}/[子系統]/ 建立 design.md(可用 /sdd-check 協助),frontmatter 標 status: active
|
||||
3. 在回覆開頭宣告已讀 SDD + 對應 task
|
||||
|
||||
小修改(修 bug、改文字)若確定豁免,請明確說明範圍後由人放行。
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# 舊行為放行 + 提醒遷移(stderr 警告,不擋)
|
||||
echo "📋 提醒:${SPECS_DIR}/ 有 SDD 但尚未掛生命週期 frontmatter(老結構)。動手前確認已讀對應 design.md;建議依 SDD-LIFECYCLE.md 補 status 標記(現行那份標 active)。" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── 新行為:寫 code 檔需「恰好 1 份」active SDD ──
|
||||
if [ "$ACTIVE_COUNT" -eq 0 ]; then
|
||||
cat >&2 <<EOF
|
||||
🚫 SDD 協議攔截:要動 code 檔 ($FILE_PATH),但 ${SPECS_DIR}/ 下沒有任何 status: active 的 SDD。
|
||||
|
||||
單一活性鐵律:所有開發任務唯一對應源=那份 active SDD(規則見 system-dev/docs/3-specs/SDD-LIFECYCLE.md)。
|
||||
|
||||
請先(擇一,都是人的決定,CC 不得自行建 SDD):
|
||||
1. 把現行規格的 design.md frontmatter 標成 status: active(一份、只能一份)
|
||||
2. 或依 SDD-LIFECYCLE.md 第 3、4 條:proposal 進 pending-changes.md → 使用者 confirm → 開新 SDD 標 active
|
||||
然後在回覆開頭宣告已讀 active SDD + 對應 task。
|
||||
|
||||
小修改(修 bug、改文字)若確定豁免,請明確說明範圍後由人放行。
|
||||
EOF
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# 有 SDD:放行,但留痕提醒要宣告(stderr 警告,不擋)
|
||||
echo "📋 提醒:system-dev/docs/3-specs/ 下有 SDD。動手前請確認已讀對應 design.md 並在回覆宣告。" >&2
|
||||
# 恰好 1 份 active:放行,留痕提醒要宣告(stderr 警告,不擋)
|
||||
printf '📋 提醒:現行 active SDD=\n%s動手前請確認已讀它的 design.md、對應到 tasks,並在回覆宣告。\n' "$ACTIVE_LIST" >&2
|
||||
exit 0
|
||||
|
||||
@@ -18,8 +18,10 @@ cat <<'EOF'
|
||||
→ 只能 TinyGo (main.go) 或 AssemblyScript,編譯成 .wasm
|
||||
|
||||
2. cypher-executor TS 裡禁止實作 credential/auth/JWT 業務邏輯
|
||||
→ crypto.subtle.decrypt / sign 只准出現在 wasi-shim.ts 的 host function
|
||||
→ crypto.subtle.sign 只准出現在 wasi-shim.ts 的 host function
|
||||
(crypto_decrypt 已廢除成永遠回失敗的 stub,credential 走 CF Workers Secrets)
|
||||
→ {{secret.X}} template 展開屬於 WASM 零件職責
|
||||
(例外:*-seeds.ts 是資料宣告非呼叫實作,見 rule 07)
|
||||
→ 禁止 hard-code gmail/telegram/sheets API endpoint
|
||||
|
||||
3. 禁止新增 Service Binding 綁零件
|
||||
@@ -41,23 +43,20 @@ cat <<'EOF'
|
||||
🚧 執行範圍:修改/建立/刪除 <檔案>
|
||||
4. 每完成一個 task,立刻更新 tasks.md 的 [x],不批次
|
||||
|
||||
🔥 當前進行中 Phase:Credential Primitives TS → WASM
|
||||
🔥 當前 active SDD:以 frontmatter `status: active` 為唯一判準(不靠本檔硬寫)
|
||||
|
||||
SDD:docs/3-specs/arcrun/credential-primitives-wasm/
|
||||
已完成:Phase 0.1-0.5(核心合併 + u6u-core 刪除)
|
||||
未完成硬前置:
|
||||
- Phase 0.6 wasi-shim 加 host functions(kv_get / crypto_decrypt / crypto_sign_rs256)
|
||||
- Phase 0.7 component-loader WASM runner 路徑
|
||||
未完成主要任務:
|
||||
- Phase 1 auth_static_key WASM 零件(TinyGo)
|
||||
- Phase 2 auth_service_account WASM 零件
|
||||
- Phase 3 刪除 cypher-executor 的三套違規 TS:
|
||||
* src/actions/credential-injector.ts(整檔刪)
|
||||
* src/lib/jwt-signer.ts(整檔刪)
|
||||
* src/lib/component-loader.ts 的 BUILTIN_API_RECIPES + BUILTIN_CREDENTIALS_MAP(整段刪)
|
||||
查法:bash system-dev/scripts/sdd-active-check.sh
|
||||
規則:system-dev/docs/3-specs/SDD-LIFECYCLE.md(單一活性鐵律,D35)
|
||||
session 開場請回報三個數字:現行規格名稱 + 未完成任務數 + pending-changes 待裁決數
|
||||
|
||||
⛔ 封測狀態:推遲(richblack 2026-04-19 決定)
|
||||
原因:違規 TS 未清,不封測。
|
||||
📦 已封存(closed,勿當進行中):
|
||||
- credential-primitives-wasm → system-dev/docs/3-specs/archive/credential-primitives-wasm/
|
||||
主線已達成:cypher-executor TS 不再有 credential/auth 業務邏輯
|
||||
(credential-injector.ts / jwt-signer.ts / BUILTIN_API_RECIPES 皆已移除)
|
||||
credential 現行做法=CF Workers per-script Secrets + D1 目錄,arcrun 不自管加密金鑰
|
||||
→ 見 .claude/rules/01-tech-stack.md「Credential 儲存規範」
|
||||
⚠️ 殘留缺口(未隨封存視為完成):auth_mtls 從未實作、7.6 self-hosted auth 鏈
|
||||
端到端未驗;要做需另立新 SDD。細節見該卷 tasks.md「封存時仍未完成的項目」
|
||||
|
||||
📚 詳細規範:
|
||||
.claude/rules/00-sdd-protocol.md — SDD 協議
|
||||
|
||||
@@ -32,6 +32,33 @@ SDD 協議要求:code 和 SDD 必須同步更新。
|
||||
EOF
|
||||
fi
|
||||
|
||||
# ── console-ui:對外網址上是不是還跑著舊世代?(2026-08-08)────────────────
|
||||
#
|
||||
# 病(leo:「已經發生過一次這個錯誤,把舊版界面上到 prod,你要確定不可再犯」):
|
||||
# 前端改完、commit 了、甚至 wiki 都寫了,但**沒有人把它推上去**——
|
||||
# 而線上不會報錯,只是繼續展示半個月前的介面。08-08 實測:三個對外網址的
|
||||
# apiBase/profile 全綠,跑的卻是 07-22 那一代。**組態對 ≠ 世代對。**
|
||||
#
|
||||
# 為什麼掛在 Stop:這裡正是 CC 要說「做完了」的那一刻。
|
||||
# 不連網(每回合都跑),只比對「手上這一代」與「最後一次**通過線上實測**的部署紀錄」
|
||||
# (.deploy-state.json 只在 deploy.mjs 驗過線上後才寫,不是跑過指令就寫)。
|
||||
# 要問線上真實現況:cd console-ui && npm run verify(那支才連網)。
|
||||
if [ -d console-ui/scripts ] && command -v node >/dev/null 2>&1; then
|
||||
LAG="$(cd console-ui && node scripts/verify-live.mjs --offline-lag 2>/dev/null)"
|
||||
if [ -n "$LAG" ]; then
|
||||
cat >&2 <<EOF
|
||||
|
||||
🕰️ console-ui:手上這一代**還沒送出去過**
|
||||
$(echo "$LAG" | sed 's/^/ · /')
|
||||
|
||||
對外網址不會因此報錯——它只會繼續展示舊介面,而所有只驗組態的檢查都會說它是綠的。
|
||||
要看線上現在真的在跑哪一代: cd console-ui && npm run verify
|
||||
要送出去(含推完自動回頭驗線上):cd console-ui && npm run deploy:personal
|
||||
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
|
||||
# 若有暫存的 tasks.md 變動,提醒 commit
|
||||
TASKS_DIFF=$(git -C "$(pwd)" status --porcelain -- 'docs/3-specs/**/tasks.md' 2>/dev/null | head -5)
|
||||
if [[ -n "$TASKS_DIFF" ]]; then
|
||||
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
# subagent-wiki-guard.sh — PreToolUse(Task) hook:subagent 聽到「查」就自己先查 wiki
|
||||
#
|
||||
# 病根(2026-07-20):總管兩次派 agent 查 ENCRYPTION_KEY,prompt 都只叫它「去查 repo 程式碼」。
|
||||
# agent 於是從**稿子**推論出「這東西還活著、不能動」,總管照單全收去擋 leo 三輪。
|
||||
#
|
||||
# 🔑 設計轉向(leo 2026-07-21):
|
||||
# 第一版是「上游沒交代讀 wiki 就擋下」——但那**還是依賴上游記得寫**,
|
||||
# 跟「我記得讀 wiki」是同一個病。leo 點破:
|
||||
# 「subagent 的問題跟你一樣。你叫它去查,就算你沒說要先查 wiki,
|
||||
# 但它**只要聽到查,就應該主動查 wiki**,因為每個 repo 都有維護自己的 wiki。」
|
||||
# → 改成 **注入式**:不擋、不要求上游改 prompt,直接把「先查 wiki」這條
|
||||
# 以 additionalContext 注入給 subagent,讓它自己做。零依賴任何人記得。
|
||||
#
|
||||
# 行為:偵測到查證/實作類任務 → exit 0 並用 hookSpecificOutput 注入指示。
|
||||
# 已含 wiki 指示、或非查證類任務 → 靜默放行(不重複注入)。
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
PROMPT=$(printf '%s' "$INPUT" | python3 -c "
|
||||
import json,sys
|
||||
try:
|
||||
d=json.load(sys.stdin)
|
||||
print(d.get('tool_input',{}).get('prompt',''))
|
||||
except Exception: print('')
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
[ -z "$PROMPT" ] && exit 0
|
||||
|
||||
# 上游已經交代了 → 不必重複注入
|
||||
if printf '%s' "$PROMPT" | grep -qiE "wiki|agent-memory|mistakes\.md|decisions-summary"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 只對「查證/實作」類任務注入(純寫作、計算、潤稿等不需要)
|
||||
if ! printf '%s' "$PROMPT" | grep -qiE "查|盤點|核實|確認|調查|研究|找出|repo|程式碼|原始碼|source|實作|移除|刪除|重構|修|grep|codebase|\.ts|\.go|src/"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
|
||||
guidance = """【自動注入:查任何東西之前,先查 wiki】
|
||||
|
||||
你所在的 repo 有維護自己的 wiki(通常在 `system-dev/wiki/`,舊結構在 `.claude/wiki/`)。
|
||||
**接到「查/盤點/核實/實作」類任務時,第一個動作是搜尋 wiki,不是翻程式碼。**
|
||||
|
||||
做法(30 秒,省下大量白工):
|
||||
grep -rin "<本題關鍵字>" system-dev/wiki/ 2>/dev/null || grep -rin "<關鍵字>" .claude/wiki/
|
||||
|
||||
為什麼這是划算的:
|
||||
• wiki 是前人已經查過、驗證過、被負責人糾正過的結論——**判準**。
|
||||
• 程式碼與歷史文件是**稿子**:它反映「還沒清乾淨」,不等於「還在用」。
|
||||
從稿子推論會系統性得出過時結論。
|
||||
• wiki 沒記載,才值得花力氣翻原文。
|
||||
|
||||
三條硬規則:
|
||||
1. **wiki 與程式碼衝突 → 以 wiki 為準**,並在回報中明確指出衝突,
|
||||
不要自行用 code 推翻 wiki。
|
||||
2. wiki 寫「不可動/待廢除/進行中」→ **讀它的解除條件並逐條核對**。
|
||||
那是當時狀態,不是永久禁令;條件已滿足就是可動。
|
||||
(2026-07-20 實際事故:agent 只看到「不可動」就回報不能動,
|
||||
實際上解除條件早已滿足,害負責人被擋三輪。)
|
||||
3. 翻原文後若得到**新結論**,回報時明講「wiki 該更新」——wiki 過時是債,要還。
|
||||
"""
|
||||
|
||||
print(json.dumps({
|
||||
"hookSpecificOutput": {
|
||||
"hookEventName": "PreToolUse",
|
||||
"additionalContext": guidance
|
||||
}
|
||||
}, ensure_ascii=False))
|
||||
PY
|
||||
|
||||
exit 0
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
# wiki-first-search.sh — PreToolUse hook:要去翻原文/程式碼前,先把 wiki 命中結果推到眼前
|
||||
#
|
||||
# 病根(2026-07-20 leo 點破,mistakes 第一鐵律):
|
||||
# 總管 session 開頭讀了 agent-memory 前 50 行就開工,關鍵那條在第 56 行 → 拿過期記憶擋了 leo 三輪。
|
||||
# leo:「如果你不是讀而是**搜尋** wiki 就不會只讀 50 行就下定論,
|
||||
# 而是就像我直接在頁面 cmd+F,那些都會高亮。」
|
||||
#
|
||||
# 設計要點(為什麼是這個形狀):
|
||||
# 1. **搜尋 ≠ 通讀**:開場 push 全文(session-start-recall.sh)解決不了這題——量大必然只讀開頭。
|
||||
# 這支反過來:在「你正要去查 code/原文」的當下,用你自己的關鍵字 grep wiki,只推命中行。
|
||||
# 2. **時機是關鍵**:不是開場推、不是寫入時擋,而是**查詢動作發生的那一刻**介入。
|
||||
# 3. **提醒不阻擋**(exit 0):wiki 沒記載時本來就該去翻原文,擋下來反而礙事。
|
||||
# 唯一目的是消滅「不知道 wiki 有寫」這件事。
|
||||
#
|
||||
# 觸發:Grep / Glob / Read 打向 code 或 docs 時(見下方 should_check)。
|
||||
# 輸出:stdout 注入 context(命中的 wiki 行 + 檔名:行號)。
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
TOOL=$(printf '%s' "$INPUT" | python3 -c "import json,sys;print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null || echo "")
|
||||
|
||||
# 取出這次查詢的關鍵字:Grep 用 pattern,Glob/Read 用路徑的檔名部分
|
||||
QUERY=$(printf '%s' "$INPUT" | python3 -c "
|
||||
import json,sys,os,re
|
||||
try:
|
||||
d=json.load(sys.stdin); ti=d.get('tool_input',{})
|
||||
q = ti.get('pattern') or ''
|
||||
if not q:
|
||||
p = ti.get('file_path') or ti.get('path') or ''
|
||||
q = os.path.splitext(os.path.basename(p))[0] if p else ''
|
||||
if not q:
|
||||
# Bash:2026-07-21 補的破口——原版只掛 Grep|Glob|Read,
|
||||
# 但「用 curl/wrangler 亂試部署方法」走的是 Bash,整支 hook 不觸發。
|
||||
# leo 當場點破:wiki 早記著「寄信已驗證可用」,我卻沒查又自創方法。
|
||||
# 只認「會動到外部系統/部署」的高風險指令,避免每個 ls 都洗版。
|
||||
cmd = ti.get('command') or ''
|
||||
if re.search(r'\b(wrangler|curl|npx|acr|gh|deploy|push)\b', cmd):
|
||||
# 取指令中最具識別度的詞(worker 名/資源名/子命令)當搜尋詞
|
||||
cand = re.findall(r'[A-Za-z_][A-Za-z0-9_-]{4,}', cmd)
|
||||
skip = {'https','http','client','accounts','workers','scripts',
|
||||
'application','content','Authorization','Bearer','python3',
|
||||
'curl','npx','bash','echo','grep','local','branch','origin'}
|
||||
cand = [c for c in cand if c not in skip and not c.startswith('-')]
|
||||
q = max(cand, key=len) if cand else ''
|
||||
# grep pattern 常含 regex 元字元;取最長的英數/底線詞當搜尋詞
|
||||
words = re.findall(r'[A-Za-z_][A-Za-z0-9_]{3,}', q)
|
||||
print(max(words, key=len) if words else '')
|
||||
except Exception:
|
||||
print('')
|
||||
" 2>/dev/null || echo "")
|
||||
|
||||
[ -z "$QUERY" ] && exit 0
|
||||
|
||||
WIKI_DIR="system-dev/wiki"
|
||||
[ -d "$WIKI_DIR" ] || exit 0
|
||||
|
||||
# 只在「查程式碼/文件」時提醒;查 wiki 本身就不用了(已經在讀了)
|
||||
TARGET=$(printf '%s' "$INPUT" | python3 -c "
|
||||
import json,sys
|
||||
try:
|
||||
d=json.load(sys.stdin); ti=d.get('tool_input',{})
|
||||
print(ti.get('file_path') or ti.get('path') or '')
|
||||
except Exception: print('')
|
||||
" 2>/dev/null || echo "")
|
||||
case "$TARGET" in
|
||||
*system-dev/wiki*) exit 0 ;;
|
||||
esac
|
||||
|
||||
# grep wiki(不分大小寫、含行號),最多 12 行避免洗版
|
||||
HITS=$(grep -rin --include="*.md" -- "$QUERY" "$WIKI_DIR" 2>/dev/null | head -12 || true)
|
||||
[ -z "$HITS" ] && exit 0
|
||||
|
||||
COUNT=$(printf '%s\n' "$HITS" | wc -l | tr -d ' ')
|
||||
|
||||
echo "════════════════════════════════════════════════"
|
||||
printf '📚 wiki 已有「%s」的記載(%s 處,先看這裡再翻原文)\n' "$QUERY" "$COUNT"
|
||||
echo "════════════════════════════════════════════════"
|
||||
printf '%s\n' "$HITS" | sed 's|^system-dev/wiki/| |'
|
||||
echo ""
|
||||
echo "⚠️ wiki 是判準,程式碼與歷史文件只是稿子(mistakes 第一鐵律)。"
|
||||
echo " • 上面若與你將要查的原文衝突 → **以 wiki 為準**,別用 code 推翻 wiki。"
|
||||
echo " • 看到「不可動/待廢除/進行中」→ 先讀它的**解除條件**並逐條核對,"
|
||||
echo " 那是當時狀態不是永久禁令;條件已滿足就是可動。"
|
||||
echo " • wiki 沒答案才值得翻原文——翻完若得到新結論,**回頭更新 wiki**。"
|
||||
echo ""
|
||||
|
||||
exit 0
|
||||
@@ -14,9 +14,7 @@
|
||||
|
||||
| 任務類型 | 對應 SDD |
|
||||
|---------|---------|
|
||||
| Auth primitive WASM 零件(static_key/oauth2/service_account/mtls) | `docs/3-specs/arcrun/credential-primitives-wasm/` |
|
||||
| 清除 cypher-executor 裡的 TS 業務邏輯 | `docs/3-specs/arcrun/credential-primitives-wasm/` |
|
||||
| WASI shim host functions(kv_get / crypto_decrypt / crypto_sign_rs256) | `docs/3-specs/arcrun/credential-primitives-wasm/` |
|
||||
| Auth primitive WASM 零件 / credential 儲存 / WASI shim host functions | **已封存**(`system-dev/docs/3-specs/archive/credential-primitives-wasm/`,`status: closed`)。現行規範見 `.claude/rules/01-tech-stack.md`「Credential 儲存規範」;**新工作需另立 SDD**(含未實作的 `auth_mtls`),不得掛在已封存的卷上 |
|
||||
| Auth Recipe 系統(recipe schema、KV 格式) | `docs/3-specs/arcrun/auth-recipe.md` |
|
||||
| Landing Page | `docs/3-specs/arcrun/landing-page.md` |
|
||||
| CLI / SDK(Python/JS) | `docs/3-specs/arcrun/sdk-and-website/` |
|
||||
|
||||
@@ -30,7 +30,8 @@
|
||||
|---|---|
|
||||
| `u6u.http_request` | 發 HTTP 請求 |
|
||||
| `u6u.kv_get` | 讀 Cloudflare KV(Worker 側依 key 前綴路由到正確 KV) |
|
||||
| `u6u.crypto_decrypt` | AES-GCM 解密(encryption key 永不暴露給 WASM) |
|
||||
| `u6u.secret_get` | 讀 CF Workers Secrets(只放行 `CRED_` 前綴) |
|
||||
| `u6u.crypto_decrypt` | ⚠️ 已廢除,保留成永遠回失敗的 stub(現役 wasm 仍宣告此 import) |
|
||||
| `u6u.crypto_sign_rs256` | RSA-SHA256 簽章(PKCS8 bytes 傳入) |
|
||||
|
||||
**所有 host function 在 `cypher-executor/src/lib/wasi-shim.ts` 實作**。零件透過 WASI import 使用。
|
||||
@@ -50,16 +51,22 @@
|
||||
|
||||
**警告:R2 不存平台內建零件的 WASM**。平台零件已 bundle 進各自的 Worker binary(`[[wasm_modules]]` 或 `import ... assert { type: 'webassembly' }`)。
|
||||
|
||||
## 加解密規範
|
||||
## Credential 儲存規範
|
||||
|
||||
- **演算法**:AES-GCM 256-bit
|
||||
- **加密位置**:Client 端(CLI / Python SDK / JS SDK)
|
||||
- Python:`cryptography` 套件
|
||||
- JS:Web Crypto API(`crypto.subtle`)
|
||||
- **解密位置**:Server 端 **WASM primitive**(透過 host function `crypto_decrypt`)
|
||||
- cypher-executor TS **不解密**,只提供 host function
|
||||
- `ENCRYPTION_KEY` 只在 Worker host function 內部讀取,**永不經 stdin / 回傳值傳給 WASM**
|
||||
- **傳輸格式**:`{ name, encrypted, iv }`(iv base64、encrypted base64)
|
||||
**arcrun 不自管加密金鑰。** credential 明文由 **Cloudflare Workers Secrets** 託管
|
||||
(per-script secret,掛在用戶自己的 cypher worker 上):
|
||||
|
||||
- secret 名稱 = `CRED_{NAME}_{sha256(api_key)[:8]}`(跨租戶命名隔離)
|
||||
- D1 `credentials` 表只存**目錄**(api_key / name / service / secret_ref),不存值
|
||||
- D19:擁有目錄,不擁有內容物——連 owner 都讀不回,只能覆寫/刪除
|
||||
- 需要 worker 設 `CF_SECRETS_API_TOKEN`(機密,用戶手動 put)+ `CF_ACCOUNT_ID`(自動注入)
|
||||
|
||||
> 註:`crypto_decrypt` host function 是**永遠回失敗的 stub**,因為現役三個 `auth_*`
|
||||
> `.wasm` 仍宣告該 import(缺項會讓 WASM instantiate 失敗)。三個零件重編後即可刪除。
|
||||
- **取用位置**:Server 端 **WASM primitive**(透過 host function `secret_get(ref)`)
|
||||
- cypher-executor TS 不碰業務邏輯,只提供 host function
|
||||
- `secret_get` 只放行 `CRED_` 前綴,WASM 讀不到 worker 本身的其他機密
|
||||
- **傳輸格式**:明文值走 TLS(`POST /credentials`),不做 client 端加密
|
||||
|
||||
## 網路部署
|
||||
|
||||
|
||||
@@ -33,10 +33,13 @@ Auth primitive 必須透過 `component-worker-template/` 搭配 WASM binary 部
|
||||
## 第二類:cypher-executor TS 的禁令
|
||||
|
||||
### 2.1 禁止新增任何 credential / auth / jwt 相關的 TS 檔案
|
||||
**已存在但要刪**(Phase 1-3 範圍):
|
||||
- `cypher-executor/src/actions/credential-injector.ts` → 刪除(走 WASM auth primitive)
|
||||
- `cypher-executor/src/lib/jwt-signer.ts` → 刪除(RS256 移入 auth_service_account WASM)
|
||||
- `cypher-executor/src/lib/component-loader.ts` 的 `BUILTIN_API_RECIPES` 和 `BUILTIN_CREDENTIALS_MAP` → 整段刪除
|
||||
**清除已完成**(2026-07-20,credential-primitives-wasm 卷已封存)。下列曾違規的 TS **均已不存在**,
|
||||
列此僅為「禁止重新引入」的清單:
|
||||
- ~~`cypher-executor/src/actions/credential-injector.ts`~~ → 已刪(auth 走 WASM primitive)
|
||||
- ~~`cypher-executor/src/lib/jwt-signer.ts`~~ → 已刪(RS256 在 auth_service_account WASM)
|
||||
- ~~`component-loader.ts` 的 `BUILTIN_API_RECIPES` / `BUILTIN_CREDENTIALS_MAP`~~ → 已整段刪
|
||||
|
||||
**重新建立上述任一者 = 違規**。
|
||||
|
||||
**Hook 會擋**:新增任何路徑含以下關鍵字的 `.ts` 檔案:
|
||||
- `credential-injector`、`credential_injector`
|
||||
@@ -83,7 +86,7 @@ Auth primitive 必須透過 `component-worker-template/` 搭配 WASM binary 部
|
||||
|
||||
13 個現有的 `SVC_*` 綁定(`cypher-executor/wrangler.toml`,邏輯零件)是歷史遺產(效能優化),**保留但不新增**。
|
||||
|
||||
> **2026-06-06 註(credential-primitives-wasm Phase 7)**:self-hosted 的 cypher 與 auth worker 同在 `{sub}.workers.dev` zone,cypher `fetch()` 打 auth 觸發 CF **same-zone 1042**(壓測階段 11)。**未用 service binding 解**(評估後廢:service binding 靜態、加/改要重 deploy cypher)。改用 **`global_fetch_strictly_public` compatibility flag**(cypher wrangler.toml)讓 same-zone fetch 走公網前門 → 同 zone 也通,**auth 維持 HTTP fetch、不加 binding**。故本禁令不變。
|
||||
> **2026-06-06 註**(來源:credential-primitives-wasm Phase 7,該卷已封存於 `system-dev/docs/3-specs/archive/`;**本註記述的規則仍現行有效**):self-hosted 的 cypher 與 auth worker 同在 `{sub}.workers.dev` zone,cypher `fetch()` 打 auth 觸發 CF **same-zone 1042**(壓測階段 11)。**未用 service binding 解**(評估後廢:service binding 靜態、加/改要重 deploy cypher)。改用 **`global_fetch_strictly_public` compatibility flag**(cypher wrangler.toml)讓 same-zone fetch 走公網前門 → 同 zone 也通,**auth 維持 HTTP fetch、不加 binding**。故本禁令不變。
|
||||
|
||||
**Hook 會擋**:bash 指令含 `wrangler tail` 以外、涉及 `[[services]]` 新增的 pattern;Edit wrangler.toml 新增 `[[services]]` 區塊時警告確認。
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ Service binding 需要 `wrangler.toml` 裡寫死 `[[services]]`,且要 redeplo
|
||||
|
||||
**禁止新增任何 Service Binding**。所有新零件(含 auth primitive)都走 HTTP URL 路徑。
|
||||
|
||||
**same-zone 1042 的解(credential-primitives-wasm Phase 7,2026-06-06)**:self-hosted 的 cypher 與 auth worker 同在 `{sub}.workers.dev` zone,cypher `fetch()` 打 auth 觸發 CF **1042**(官方 docs:「fetch from another Worker on the **same zone**」;官方 cypher 在 `cypher.arcrun.dev`、打 `*.workers.dev` 屬跨 zone 故不踩——非官方有 flag)。**解法不是 service binding**(評估後廢:靜態、加/改要重 deploy),而是 cypher wrangler.toml 加 **`global_fetch_strictly_public` flag**——讓 same-zone fetch 走公網前門 → 同 zone 也通。auth 維持 HTTP fetch、不加 binding。官方加此 flag 行為不變(本就跨 zone),self-host 被修好 → **官方與 self-host 共用同一份 toml**。
|
||||
**same-zone 1042 的解**(來源:credential-primitives-wasm Phase 7,2026-06-06;該卷已封存於 `system-dev/docs/3-specs/archive/`,**但本段規則仍現行有效**):self-hosted 的 cypher 與 auth worker 同在 `{sub}.workers.dev` zone,cypher `fetch()` 打 auth 觸發 CF **1042**(官方 docs:「fetch from another Worker on the **same zone**」;官方 cypher 在 `cypher.arcrun.dev`、打 `*.workers.dev` 屬跨 zone 故不踩——非官方有 flag)。**解法不是 service binding**(評估後廢:靜態、加/改要重 deploy),而是 cypher wrangler.toml 加 **`global_fetch_strictly_public` flag**——讓 same-zone fetch 走公網前門 → 同 zone 也通。auth 維持 HTTP fetch、不加 binding。官方加此 flag 行為不變(本就跨 zone),self-host 被修好 → **官方與 self-host 共用同一份 toml**。
|
||||
|
||||
**仍禁止**:為**用戶自製 / 服務專屬零件**(`gmail-worker`、`notion-worker` 之類)新增 binding——那些是 recipe 的事,不該有 binding。**workflow 層(用戶串零件)一律 HTTP URL 不變。**
|
||||
|
||||
|
||||
@@ -1,77 +1,75 @@
|
||||
# 當前進度(SessionStart 會注入此檔重點)
|
||||
|
||||
> 更新時間:2026-04-19
|
||||
> 權威來源:`docs/3-specs/arcrun/credential-primitives-wasm/tasks.md`
|
||||
> 此檔僅摘要,詳細狀態以 tasks.md 為準。
|
||||
> 更新時間:2026-07-21
|
||||
> **權威來源=design.md frontmatter `status: active`**(機器可查),不是本檔。
|
||||
> 查法:`bash system-dev/scripts/sdd-active-check.sh`
|
||||
> 本檔只是索引,任何與 frontmatter 衝突之處以 frontmatter 為準。
|
||||
|
||||
---
|
||||
|
||||
## 封測狀態
|
||||
## 現行 active SDD
|
||||
|
||||
**原定明天封測,richblack 決定推遲**,原因:cypher-executor 有三套 TS 業務邏輯違反「零件一律 WASM」架構原則(Phase 1-3 要清除的程式碼),在清除前不封測。
|
||||
**RAG Portal 多人授權** — `system-dev/docs/3-specs/portal-auth/`
|
||||
|
||||
依 SDD 生命週期鐵律(D35,全文 `system-dev/docs/3-specs/SDD-LIFECYCLE.md`):
|
||||
任何時刻整個 repo 只允許**一份** `status: active`。所有開發任務必須對應它的 tasks,
|
||||
找不到對應 → 停下來問,不准直接做。
|
||||
|
||||
**session 開場請回報三個數字**:現行規格名稱 + 未完成任務數 + `pending-changes.md` 待裁決 proposal 數。
|
||||
|
||||
---
|
||||
|
||||
## 目前 Phase:Credential Primitives TS → WASM
|
||||
## 已封存(closed,勿當進行中)
|
||||
|
||||
**SDD 位置**:`docs/3-specs/arcrun/credential-primitives-wasm/design.md` + `tasks.md`
|
||||
### Credential Primitives TS → WASM
|
||||
|
||||
### 已完成
|
||||
`system-dev/docs/3-specs/archive/credential-primitives-wasm/`(2026-07-21 封存)
|
||||
|
||||
- **Phase 0.1–0.5**:核心合併(u6u-core 併入 arcrun、21 個零件 contract 完整、刪除重複 `credentials/` 目錄、CREDENTIALS_KV binding 確認、刪除 `matrix/u6u-core/`)
|
||||
- `registry/components/` 下 21 個零件(邏輯 + API)都有 `main.go` + `.wasm`
|
||||
**主線已達成**:cypher-executor TS 不再實作任何 credential / auth 業務邏輯。
|
||||
`credential-injector.ts`、`jwt-signer.ts`、`BUILTIN_API_RECIPES` / `BUILTIN_CREDENTIALS_MAP`
|
||||
全數移除(最後一項 T10 於 2026-07-20 commit `20c7610` 完成)。
|
||||
|
||||
### 進行中 / 未完成
|
||||
**credential 現行做法**(非被另一卷 supersede,是機制整個換掉):
|
||||
- 密文 → **CF Workers per-script Secrets**(`CRED_{NAME}_{hash}`),arcrun 讀不回明文
|
||||
- 目錄 → **D1** `credentials` 表(name / service / secret_ref,不含值)
|
||||
- **arcrun 不自管任何加密金鑰**;`crypto_decrypt` host function 已成永遠回失敗的 stub
|
||||
- 完整規範:`.claude/rules/01-tech-stack.md`「Credential 儲存規範」
|
||||
|
||||
| Task | 狀態 | 阻擋關係 |
|
||||
|-----|------|---------|
|
||||
| 0.6 wasi-shim 新增 `kv_get` / `crypto_decrypt` / `crypto_sign_rs256` host functions | ⬜ 未開始 | **Phase 1-3 的硬前置** |
|
||||
| 0.7 component-loader 新增 WASM runner 路徑 | ⬜ 未開始 | **Phase 1-3 的硬前置** |
|
||||
| 1.1-1.8 `auth_static_key` WASM 零件(TinyGo) | ⬜ 未開始 | 涵蓋 80% 服務 |
|
||||
| 2.1-2.6 `auth_service_account` WASM 零件(JWT signing) | ⬜ 未開始 | Google Service Account 等 |
|
||||
| 3.1-3.5 清除 `component-loader.ts` 的 `BUILTIN_API_RECIPES` | ⬜ 未開始 | 要先有 Phase 1-2 的 WASM 零件 |
|
||||
| 4.1-4.4 `auth_oauth2` + `auth_mtls`(封測後) | ⬜ 未開始 | 非阻擋項 |
|
||||
| 5.1-5.7 核心穩定驗證(全域搜尋確認無殘餘 TS) | ⬜ 未開始 | 封測啟動門檻 |
|
||||
⚠️ **封存 ≠ 全部做完**。真實殘留缺口(要做需另立新 SDD,不得掛回已封存的卷):
|
||||
- **`auth_mtls` 從未實作**(`registry/components/auth_mtls/` 不存在,mTLS 認證不支援)
|
||||
- **7.6 self-hosted auth 鏈端到端未驗**(`global_fetch_strictly_public` flag 是否真解
|
||||
same-zone 1042,在自架帳號上從未實測)
|
||||
- 數項端到端測試(1.7/1.8/2.6/3.5/3.6/5.1)無驗證記錄
|
||||
|
||||
### Phase 1-3 要**徹底刪除**的 TS 檔案(不是搬、不是改,是刪)
|
||||
|
||||
| 檔案 | 違反什麼 |
|
||||
|-----|---------|
|
||||
| `cypher-executor/src/actions/credential-injector.ts` | AES 解密、template 展開、JWT 邏輯 —— 應在 WASM |
|
||||
| `cypher-executor/src/lib/jwt-signer.ts` | RS256 JWT 簽章邏輯 —— 應在 `auth_service_account.wasm` |
|
||||
| `cypher-executor/src/lib/component-loader.ts` 的 `BUILTIN_API_RECIPES`(~100 行) | gmail/telegram/line/gsheets/http_request/cron 的 TS 實作 —— 應全部走對應 WASM 零件 |
|
||||
|
||||
---
|
||||
|
||||
## 下一個 session 第一件要做的事
|
||||
|
||||
**讀 `docs/3-specs/arcrun/credential-primitives-wasm/tasks.md`**,然後決定從 Phase 0.6 還是 0.7 開始。
|
||||
|
||||
0.6(host functions)和 0.7(WASM runner)是並列的前置工作,哪個先都可以,但都要在 Phase 1 開始之前完成。
|
||||
逐條核實見該卷 `tasks.md` 的「封存時仍未完成的項目」段。
|
||||
|
||||
---
|
||||
|
||||
## SDD 索引
|
||||
|
||||
> 「進行中」與否一律以 frontmatter 為準,下表僅路徑速查。
|
||||
|
||||
| 子系統 | SDD |
|
||||
|--------|-----|
|
||||
| **主要(正在動)** Credential Primitives WASM 改寫 | `docs/3-specs/arcrun/credential-primitives-wasm/` |
|
||||
| **LI (LLM Interface)** — AI 操盤手使用體驗(2026-05-16 新建,mira dogfood 痛點轉化) | `docs/3-specs/llm-interface/` |
|
||||
| arcrun 總進度 | `docs/3-specs/arcrun/arcrun.md` |
|
||||
| Auth Recipe 系統(schema、預建 20 個服務) | `docs/3-specs/arcrun/auth-recipe.md` |
|
||||
| Landing Page | `docs/3-specs/arcrun/landing-page.md` |
|
||||
| SDK + Website | `docs/3-specs/arcrun/sdk-and-website/design.md` |
|
||||
| arcrun MVP 整體 | `docs/3-specs/arcrun-core-mvp/design.md` |
|
||||
| Credential 長期規格(需求源) | `docs/user_requirements/credential_parts.md` |
|
||||
| Platform Evolution | `docs/3-specs/arcrun-platform-evolution/design.md` |
|
||||
| Tech Stack 詳細 | `docs/3-specs/tech.md` |
|
||||
| **現行 active** RAG Portal 多人授權 | `system-dev/docs/3-specs/portal-auth/` |
|
||||
| ~~Credential Primitives WASM~~(closed) | `system-dev/docs/3-specs/archive/credential-primitives-wasm/` |
|
||||
| LLM Interface(AI 操盤手使用體驗) | `system-dev/docs/3-specs/llm-interface/` |
|
||||
| arcrun 總進度 | `system-dev/docs/3-specs/arcrun/arcrun.md` |
|
||||
| Auth Recipe 系統(schema、預建服務) | `system-dev/docs/3-specs/arcrun/auth-recipe.md` |
|
||||
| SDK + Website | `system-dev/docs/3-specs/arcrun/sdk-and-website/` |
|
||||
| arcrun MVP 整體 | `system-dev/docs/3-specs/arcrun-core-mvp/` |
|
||||
| Platform Evolution | `system-dev/docs/3-specs/arcrun-platform-evolution/` |
|
||||
| Tech Stack 詳細 | `system-dev/docs/3-specs/tech.md` |
|
||||
| 生命週期鐵律 | `system-dev/docs/3-specs/SDD-LIFECYCLE.md` |
|
||||
| 待裁決 proposal | `system-dev/docs/3-specs/pending-changes.md` |
|
||||
|
||||
---
|
||||
|
||||
## 技術備註(CC 常搞錯的點)
|
||||
|
||||
1. **每個 WASM 零件 = 獨立 Worker = 公開 URL**(例:`gmail.arcrun.dev`)。不是從 R2 動態讀。
|
||||
1. **每個 WASM 零件 = 獨立 Worker = 獨立 URL**。不是從 R2 動態讀。
|
||||
2. **Cypher binding = YAML 裡寫 URL 清單**。不是 Cloudflare service binding。
|
||||
3. **cypher-executor 只做 routing + host functions**。業務邏輯全在 WASM 零件。
|
||||
4. **TinyGo 有限制**:`crypto/rsa` 支援不全 → 用 host function `crypto_sign_rs256` 讓 Worker 代簽。
|
||||
5. 詳見 `.claude/rules/03-component-architecture.md`。
|
||||
5. **credential 不自管金鑰**:走 `secret_get(ref)` 讀 CF Workers Secrets,`crypto_decrypt` 是 stub。
|
||||
6. 詳見 `.claude/rules/03-component-architecture.md` 與 `01-tech-stack.md`。
|
||||
|
||||
@@ -62,10 +62,13 @@ find . -name 'wrangler.toml' -not -path '*/node_modules/*' -not -name 'wrangler.
|
||||
**CI 只提供 Cloudflare 驗證,不碰 runtime secret**。
|
||||
|
||||
- GH Actions secrets:`CLOUDFLARE_API_TOKEN`、`CLOUDFLARE_ACCOUNT_ID`(一次性設好)
|
||||
- Runtime secret(例:`ENCRYPTION_KEY`、`OPENAI_KEY`、`GOOGLE_API_KEY`):
|
||||
- Runtime secret(例:`CF_SECRETS_API_TOKEN`、`OPENAI_KEY`、`GOOGLE_API_KEY`):
|
||||
- **由 richblack 一次性手動** `wrangler secret put <KEY>` 設進各 Worker
|
||||
- 不進 CI,不進 `wrangler.toml` `[vars]`
|
||||
- 需要的 Worker:`auth_static_key`、`auth_service_account`(兩個都要 `ENCRYPTION_KEY`)
|
||||
- 需要的 Worker:`arcrun-cypher-executor`(`CF_SECRETS_API_TOKEN`——寫 credential 進
|
||||
Workers Secrets 用;`CF_ACCOUNT_ID` 非機密由 `acr init` 自動注入)
|
||||
|
||||
> `auth_static_key` / `auth_service_account` 不需要任何 secret。
|
||||
|
||||
---
|
||||
|
||||
|
||||
+33
-1
@@ -10,6 +10,15 @@
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "startup|resume|clear",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/publish-lag-check.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
@@ -25,6 +34,11 @@
|
||||
"type": "command",
|
||||
"command": "bash .claude/hooks/wiki-secret-scan.sh",
|
||||
"timeout": 5
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash .claude/hooks/sdd-guard.sh",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -37,6 +51,24 @@
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Grep|Glob|Read|Bash",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/wiki-first-search.sh"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Task",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/subagent-wiki-guard.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
@@ -63,4 +95,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,4 +19,3 @@ id = "e7f4320f88d343f187e35e3543dd74c9"
|
||||
binding = "RECIPES"
|
||||
id = "9cf9db905c6241f78503199e58b2ffe0"
|
||||
|
||||
# ENCRYPTION_KEY 透過 wrangler secret put 設定
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
*
|
||||
* 安全邊界:
|
||||
* - api_key 經 stdin 傳進 WASM,同時綁到 host function 的 kv_get 做越權檢查
|
||||
* - ENCRYPTION_KEY 只存在於 host function 的 closure 中,不會進入 WASM 記憶體
|
||||
* - private key 只以 PKCS8 bytes 傳給 crypto_sign_rs256 host function,decrypt 後 plaintext 不離開 WASM
|
||||
*/
|
||||
|
||||
|
||||
@@ -20,5 +20,3 @@ id = "e7f4320f88d343f187e35e3543dd74c9"
|
||||
binding = "RECIPES"
|
||||
id = "9cf9db905c6241f78503199e58b2ffe0"
|
||||
|
||||
# ENCRYPTION_KEY 透過 wrangler secret set 設定
|
||||
# wrangler secret put ENCRYPTION_KEY
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
*
|
||||
* 安全邊界:
|
||||
* - api_key 經 stdin 傳進 WASM,同時綁到 host function 的 kv_get 做越權檢查
|
||||
* - ENCRYPTION_KEY 只存在於 host function 的 closure 中,不會進入 WASM 記憶體
|
||||
*/
|
||||
|
||||
import componentWasm from '../component.wasm' assert { type: 'webassembly' };
|
||||
|
||||
@@ -20,5 +20,3 @@ id = "e7f4320f88d343f187e35e3543dd74c9"
|
||||
binding = "RECIPES"
|
||||
id = "9cf9db905c6241f78503199e58b2ffe0"
|
||||
|
||||
# ENCRYPTION_KEY 透過 wrangler secret set 設定
|
||||
# wrangler secret put ENCRYPTION_KEY
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
name = "arcrun-http-request"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2025-02-19"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
# global_fetch_strictly_public:self-hosted 同帳號部署時,http_request 零件外呼同 workers.dev
|
||||
# zone 的目標會撞 CF same-zone 1042——此 flag 讓 fetch 走公網前門(Arcrun#33 同族;
|
||||
# 正解同 cypher 前例,見 .claude/rules/03 的 1042 段)。官方部署本就跨 zone(arcrun.dev route),
|
||||
# 加此 flag 行為不變 → 官方/self-hosted 共用同一份 toml。
|
||||
compatibility_flags = ["nodejs_compat", "global_fetch_strictly_public"]
|
||||
workers_dev = true
|
||||
|
||||
[vars]
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"name": "arcrun-kbdb-upsert-block",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"hono": "^4.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20250408.0",
|
||||
"typescript": "^5.4.0",
|
||||
"wrangler": "^4.0.0"
|
||||
}
|
||||
}
|
||||
-898
@@ -1,898 +0,0 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
hono:
|
||||
specifier: ^4.7.0
|
||||
version: 4.12.18
|
||||
devDependencies:
|
||||
'@cloudflare/workers-types':
|
||||
specifier: ^4.20250408.0
|
||||
version: 4.20260511.1
|
||||
typescript:
|
||||
specifier: ^5.4.0
|
||||
version: 5.9.3
|
||||
wrangler:
|
||||
specifier: ^4.0.0
|
||||
version: 4.90.1(@cloudflare/workers-types@4.20260511.1)
|
||||
|
||||
packages:
|
||||
|
||||
'@cloudflare/kv-asset-handler@0.5.0':
|
||||
resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==}
|
||||
engines: {node: '>=22.0.0'}
|
||||
|
||||
'@cloudflare/unenv-preset@2.16.1':
|
||||
resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==}
|
||||
peerDependencies:
|
||||
unenv: 2.0.0-rc.24
|
||||
workerd: '>1.20260305.0 <2.0.0-0'
|
||||
peerDependenciesMeta:
|
||||
workerd:
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-darwin-64@1.20260508.1':
|
||||
resolution: {integrity: sha512-IT3r6VgiSwIesL4AJbxjgxvIxwWZqM7BKkhYAzOKHl4GF2M0TxeOahUIXd+CYXVZgHX8ceEg+MXbEehPelJyNg==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@cloudflare/workerd-darwin-arm64@1.20260508.1':
|
||||
resolution: {integrity: sha512-JTVsisOJPcNKw0qovPjqyBWYahfdhUh7/9NICiG5wxaEQ45PYKdoqNq0hOAAIqvqoxsKZBvTgcPTJREPqk7avA==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@cloudflare/workerd-linux-64@1.20260508.1':
|
||||
resolution: {integrity: sha512-zO38pCc27YlsZiPYcaZnosy0/t7abXrRU3VEO1oKfUvnaCpHgphDG+VsrmHL+kntda6hrtNwg2jLeMAqqIjnjw==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@cloudflare/workerd-linux-arm64@1.20260508.1':
|
||||
resolution: {integrity: sha512-XhJa780Ia6MNIrtxn/ruZHS79b9pu5EKPfRNReaUqxy8erPT2fs93axMfFoS9kIkcaRRj/1TOUKcTeAMoywY7w==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@cloudflare/workerd-windows-64@1.20260508.1':
|
||||
resolution: {integrity: sha512-QdDOK3B/Ul1s3QmIwDrFyx9230to6LsNmWcVR8w+TYjNZuRPzqQBgusp78LO7MlqCoEl9dvIcN00jkJnLtBSfw==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@cloudflare/workers-types@4.20260511.1':
|
||||
resolution: {integrity: sha512-FA+si7cOq9i/gtCHhIc0XJL0l1F/ApF+m00752Aj7WZFJrj3ZulT2T8/+rT3BabMT0QEnqFEGIqCgrmqhgEfMg==}
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@emnapi/runtime@1.10.0':
|
||||
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.27.3':
|
||||
resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.27.3':
|
||||
resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.27.3':
|
||||
resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.27.3':
|
||||
resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.27.3':
|
||||
resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.27.3':
|
||||
resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.3':
|
||||
resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.3':
|
||||
resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.3':
|
||||
resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.27.3':
|
||||
resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.27.3':
|
||||
resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.27.3':
|
||||
resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.27.3':
|
||||
resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.27.3':
|
||||
resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||
|
||||
'@poppinss/colors@4.1.6':
|
||||
resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==}
|
||||
|
||||
'@poppinss/dumper@0.6.5':
|
||||
resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==}
|
||||
|
||||
'@poppinss/exception@1.2.3':
|
||||
resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==}
|
||||
|
||||
'@sindresorhus/is@7.2.0':
|
||||
resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@speed-highlight/core@1.2.15':
|
||||
resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==}
|
||||
|
||||
blake3-wasm@2.1.5:
|
||||
resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==}
|
||||
|
||||
cookie@1.1.1:
|
||||
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
error-stack-parser-es@1.0.5:
|
||||
resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
|
||||
|
||||
esbuild@0.27.3:
|
||||
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
hono@4.12.18:
|
||||
resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
|
||||
kleur@4.1.5:
|
||||
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
miniflare@4.20260508.0:
|
||||
resolution: {integrity: sha512-h3aG+PA8jEH76V4ZtBAbs3g7kjMfHJUF8hPvxeeajLTKwir+G+dqfBODg5yF9MT29LqrZKCRQRqzfHPWX4kCIg==}
|
||||
engines: {node: '>=22.0.0'}
|
||||
hasBin: true
|
||||
|
||||
path-to-regexp@6.3.0:
|
||||
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
semver@7.8.0:
|
||||
resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
sharp@0.34.5:
|
||||
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
|
||||
supports-color@10.2.2:
|
||||
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici@7.24.8:
|
||||
resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
unenv@2.0.0-rc.24:
|
||||
resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==}
|
||||
|
||||
workerd@1.20260508.1:
|
||||
resolution: {integrity: sha512-VlnjyH3AjVddpSK7J54nsCVgf8i2733pl8GjKttfNi7vN/hEjjAk20d2b1nDToOLKvRQpTewRnVkqaaeGHCaAw==}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
wrangler@4.90.1:
|
||||
resolution: {integrity: sha512-u2KrieKSMfRM0toTst/CfDtcRraeoVjmcExcMWgILM/ytq3qcDhuOAULoZSyPHzma43lfLJy1BC544drFyqe1A==}
|
||||
engines: {node: '>=22.0.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@cloudflare/workers-types': ^4.20260508.1
|
||||
peerDependenciesMeta:
|
||||
'@cloudflare/workers-types':
|
||||
optional: true
|
||||
|
||||
ws@8.18.0:
|
||||
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
youch-core@0.3.3:
|
||||
resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==}
|
||||
|
||||
youch@4.1.0-beta.10:
|
||||
resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@cloudflare/kv-asset-handler@0.5.0': {}
|
||||
|
||||
'@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260508.1)':
|
||||
dependencies:
|
||||
unenv: 2.0.0-rc.24
|
||||
optionalDependencies:
|
||||
workerd: 1.20260508.1
|
||||
|
||||
'@cloudflare/workerd-darwin-64@1.20260508.1':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-darwin-arm64@1.20260508.1':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-linux-64@1.20260508.1':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-linux-arm64@1.20260508.1':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-windows-64@1.20260508.1':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workers-types@4.20260511.1': {}
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
|
||||
'@emnapi/runtime@1.10.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@img/colour@1.1.0': {}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.10.0
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@poppinss/colors@4.1.6':
|
||||
dependencies:
|
||||
kleur: 4.1.5
|
||||
|
||||
'@poppinss/dumper@0.6.5':
|
||||
dependencies:
|
||||
'@poppinss/colors': 4.1.6
|
||||
'@sindresorhus/is': 7.2.0
|
||||
supports-color: 10.2.2
|
||||
|
||||
'@poppinss/exception@1.2.3': {}
|
||||
|
||||
'@sindresorhus/is@7.2.0': {}
|
||||
|
||||
'@speed-highlight/core@1.2.15': {}
|
||||
|
||||
blake3-wasm@2.1.5: {}
|
||||
|
||||
cookie@1.1.1: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
error-stack-parser-es@1.0.5: {}
|
||||
|
||||
esbuild@0.27.3:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.3
|
||||
'@esbuild/android-arm': 0.27.3
|
||||
'@esbuild/android-arm64': 0.27.3
|
||||
'@esbuild/android-x64': 0.27.3
|
||||
'@esbuild/darwin-arm64': 0.27.3
|
||||
'@esbuild/darwin-x64': 0.27.3
|
||||
'@esbuild/freebsd-arm64': 0.27.3
|
||||
'@esbuild/freebsd-x64': 0.27.3
|
||||
'@esbuild/linux-arm': 0.27.3
|
||||
'@esbuild/linux-arm64': 0.27.3
|
||||
'@esbuild/linux-ia32': 0.27.3
|
||||
'@esbuild/linux-loong64': 0.27.3
|
||||
'@esbuild/linux-mips64el': 0.27.3
|
||||
'@esbuild/linux-ppc64': 0.27.3
|
||||
'@esbuild/linux-riscv64': 0.27.3
|
||||
'@esbuild/linux-s390x': 0.27.3
|
||||
'@esbuild/linux-x64': 0.27.3
|
||||
'@esbuild/netbsd-arm64': 0.27.3
|
||||
'@esbuild/netbsd-x64': 0.27.3
|
||||
'@esbuild/openbsd-arm64': 0.27.3
|
||||
'@esbuild/openbsd-x64': 0.27.3
|
||||
'@esbuild/openharmony-arm64': 0.27.3
|
||||
'@esbuild/sunos-x64': 0.27.3
|
||||
'@esbuild/win32-arm64': 0.27.3
|
||||
'@esbuild/win32-ia32': 0.27.3
|
||||
'@esbuild/win32-x64': 0.27.3
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
hono@4.12.18: {}
|
||||
|
||||
kleur@4.1.5: {}
|
||||
|
||||
miniflare@4.20260508.0:
|
||||
dependencies:
|
||||
'@cspotcode/source-map-support': 0.8.1
|
||||
sharp: 0.34.5
|
||||
undici: 7.24.8
|
||||
workerd: 1.20260508.1
|
||||
ws: 8.18.0
|
||||
youch: 4.1.0-beta.10
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
path-to-regexp@6.3.0: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
semver@7.8.0: {}
|
||||
|
||||
sharp@0.34.5:
|
||||
dependencies:
|
||||
'@img/colour': 1.1.0
|
||||
detect-libc: 2.1.2
|
||||
semver: 7.8.0
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.34.5
|
||||
'@img/sharp-darwin-x64': 0.34.5
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
'@img/sharp-linux-arm': 0.34.5
|
||||
'@img/sharp-linux-arm64': 0.34.5
|
||||
'@img/sharp-linux-ppc64': 0.34.5
|
||||
'@img/sharp-linux-riscv64': 0.34.5
|
||||
'@img/sharp-linux-s390x': 0.34.5
|
||||
'@img/sharp-linux-x64': 0.34.5
|
||||
'@img/sharp-linuxmusl-arm64': 0.34.5
|
||||
'@img/sharp-linuxmusl-x64': 0.34.5
|
||||
'@img/sharp-wasm32': 0.34.5
|
||||
'@img/sharp-win32-arm64': 0.34.5
|
||||
'@img/sharp-win32-ia32': 0.34.5
|
||||
'@img/sharp-win32-x64': 0.34.5
|
||||
|
||||
supports-color@10.2.2: {}
|
||||
|
||||
tslib@2.8.1:
|
||||
optional: true
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
undici@7.24.8: {}
|
||||
|
||||
unenv@2.0.0-rc.24:
|
||||
dependencies:
|
||||
pathe: 2.0.3
|
||||
|
||||
workerd@1.20260508.1:
|
||||
optionalDependencies:
|
||||
'@cloudflare/workerd-darwin-64': 1.20260508.1
|
||||
'@cloudflare/workerd-darwin-arm64': 1.20260508.1
|
||||
'@cloudflare/workerd-linux-64': 1.20260508.1
|
||||
'@cloudflare/workerd-linux-arm64': 1.20260508.1
|
||||
'@cloudflare/workerd-windows-64': 1.20260508.1
|
||||
|
||||
wrangler@4.90.1(@cloudflare/workers-types@4.20260511.1):
|
||||
dependencies:
|
||||
'@cloudflare/kv-asset-handler': 0.5.0
|
||||
'@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260508.1)
|
||||
blake3-wasm: 2.1.5
|
||||
esbuild: 0.27.3
|
||||
miniflare: 4.20260508.0
|
||||
path-to-regexp: 6.3.0
|
||||
unenv: 2.0.0-rc.24
|
||||
workerd: 1.20260508.1
|
||||
optionalDependencies:
|
||||
'@cloudflare/workers-types': 4.20260511.1
|
||||
fsevents: 2.3.3
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
ws@8.18.0: {}
|
||||
|
||||
youch-core@0.3.3:
|
||||
dependencies:
|
||||
'@poppinss/exception': 1.2.3
|
||||
error-stack-parser-es: 1.0.5
|
||||
|
||||
youch@4.1.0-beta.10:
|
||||
dependencies:
|
||||
'@poppinss/colors': 4.1.6
|
||||
'@poppinss/dumper': 0.6.5
|
||||
'@speed-highlight/core': 1.2.15
|
||||
cookie: 1.1.1
|
||||
youch-core: 0.3.3
|
||||
@@ -1,8 +0,0 @@
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
sharp: true
|
||||
workerd: true
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
- sharp
|
||||
- workerd
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* arcrun WASM 零件 Worker (kbdb_upsert_block)
|
||||
* POST / → JSON input → WASM (WASI preview1) → JSON output
|
||||
* SDD: polaris/mira/.agents/specs/mira-app/design.md §3.5.12.4.1
|
||||
* matrix/arcrun/.agents/specs/arcrun/arcrun.md 三-B 新零件加入紀錄
|
||||
*/
|
||||
|
||||
import componentWasm from '../component.wasm' assert { type: 'webassembly' };
|
||||
import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
import { createWasiShim, type WasiHostFunctions } from '../../../cypher-executor/src/lib/wasi-shim';
|
||||
|
||||
const app = new Hono();
|
||||
app.use('*', cors());
|
||||
|
||||
app.get('/', (c) => c.json({ ok: true, component: 'kbdb_upsert_block' }));
|
||||
|
||||
app.post('/', async (c) => {
|
||||
let input: unknown;
|
||||
try {
|
||||
input = await c.req.json();
|
||||
} catch {
|
||||
return c.json({ success: false, error: 'request body must be JSON' }, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await runWasm(input);
|
||||
return c.json(result);
|
||||
} catch (e) {
|
||||
return c.json(
|
||||
{ success: false, error: e instanceof Error ? e.message : String(e) },
|
||||
500,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
||||
async function runWasm(input: unknown): Promise<unknown> {
|
||||
const hostFunctions: WasiHostFunctions = {
|
||||
http_request: async (url, method, headersJson, body) => {
|
||||
const headers: Record<string, string> = {};
|
||||
if (headersJson) {
|
||||
try {
|
||||
const parsed = JSON.parse(headersJson);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
|
||||
if (typeof v === 'string') headers[k] = v;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
const init: RequestInit = { method, headers };
|
||||
if (body && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') {
|
||||
init.body = body;
|
||||
}
|
||||
const res = await fetch(url, init);
|
||||
const text = await res.text();
|
||||
// 修架構債(同 http_request):非 2xx 包成帶 "error" key 的 envelope,
|
||||
// 讓 WASM 端既有的 error 判定正確識別失敗(原本只回 body 丟掉 status → 4xx 被判 success)。
|
||||
if (!res.ok) {
|
||||
return JSON.stringify({ error: `HTTP ${res.status}`, status: res.status, body: text });
|
||||
}
|
||||
return text;
|
||||
},
|
||||
};
|
||||
|
||||
const shim = createWasiShim(JSON.stringify(input), hostFunctions);
|
||||
const instance = await WebAssembly.instantiate(
|
||||
componentWasm as WebAssembly.Module,
|
||||
shim.imports,
|
||||
);
|
||||
shim.setMemory(instance.exports.memory as WebAssembly.Memory);
|
||||
await shim.run(instance);
|
||||
|
||||
const stdout = shim.getStdout().trim();
|
||||
const stderr = shim.getStderr().trim();
|
||||
if (stderr) console.error('[kbdb_upsert_block wasm stderr]', stderr);
|
||||
if (!stdout) throw new Error('WASM component produced no output');
|
||||
return JSON.parse(stdout);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["@cloudflare/workers-types"],
|
||||
"strict": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
name = "arcrun-kbdb-upsert-block"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2025-02-19"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
workers_dev = true
|
||||
|
||||
[vars]
|
||||
COMPONENT_ID = "kbdb_upsert_block"
|
||||
|
||||
[[routes]]
|
||||
pattern = "kbdb-upsert-block.arcrun.dev/*"
|
||||
zone_name = "arcrun.dev"
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"name": "arcrun-km-writer",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"hono": "^4.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20250408.0",
|
||||
"typescript": "^5.4.0",
|
||||
"wrangler": "^4.0.0"
|
||||
}
|
||||
}
|
||||
-898
@@ -1,898 +0,0 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
hono:
|
||||
specifier: ^4.7.0
|
||||
version: 4.12.14
|
||||
devDependencies:
|
||||
'@cloudflare/workers-types':
|
||||
specifier: ^4.20250408.0
|
||||
version: 4.20260420.1
|
||||
typescript:
|
||||
specifier: ^5.4.0
|
||||
version: 5.9.3
|
||||
wrangler:
|
||||
specifier: ^4.0.0
|
||||
version: 4.83.0(@cloudflare/workers-types@4.20260420.1)
|
||||
|
||||
packages:
|
||||
|
||||
'@cloudflare/kv-asset-handler@0.4.2':
|
||||
resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@cloudflare/unenv-preset@2.16.0':
|
||||
resolution: {integrity: sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg==}
|
||||
peerDependencies:
|
||||
unenv: 2.0.0-rc.24
|
||||
workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0
|
||||
peerDependenciesMeta:
|
||||
workerd:
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-darwin-64@1.20260415.1':
|
||||
resolution: {integrity: sha512-dsxaKsQm3LnPGNPEdsRv09QN3Y4DqCw7kX5j6noKqbAtro2jTr95sVlYM1jUxZ5FkOl1f7SXgaKKB9t5H5Nkbg==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@cloudflare/workerd-darwin-arm64@1.20260415.1':
|
||||
resolution: {integrity: sha512-+JgSgVA49KyKteHRA1SnonE4Zn5Ei5zdAp5FQMxFmXI8qulZw4Hl7safXxRyK4i9sTO8gl7TFOKO5Q64VPvSDQ==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@cloudflare/workerd-linux-64@1.20260415.1':
|
||||
resolution: {integrity: sha512-tU+9pwsqCy8afOVlGtiWrWQc/fedQK4SRm4KPIAt+zOiQWDxWASm6YGBUJis5c648WN80yz47qnmdDi8DQNOcA==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@cloudflare/workerd-linux-arm64@1.20260415.1':
|
||||
resolution: {integrity: sha512-bR9uITnV19r5NQ14xnypi2xHXu2iQvfYV8cVgx0JouFUmWwTEEAwFVojDdssGq93VHX9hr/pi2IRUZeegbYBog==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@cloudflare/workerd-windows-64@1.20260415.1':
|
||||
resolution: {integrity: sha512-4NuMLlerI0Ijua3Ir8HXQ+qyNvCUDEG5gDco5Om+sAiK6rnWiz+aGoSlbB8W16yW9QAgzCstbmXLiVknUBflfQ==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@cloudflare/workers-types@4.20260420.1':
|
||||
resolution: {integrity: sha512-DHT9JnSn9cIiCSdL76OxW+Xvc1+ml1CWzWvgVwreoHQ+E604aeFxPPHp9X7nE+XRWm2NH4l0OgtxUI5T/nuI3g==}
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@emnapi/runtime@1.10.0':
|
||||
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.27.3':
|
||||
resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.27.3':
|
||||
resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.27.3':
|
||||
resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.27.3':
|
||||
resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.27.3':
|
||||
resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.27.3':
|
||||
resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.3':
|
||||
resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.3':
|
||||
resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.3':
|
||||
resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.27.3':
|
||||
resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.27.3':
|
||||
resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.27.3':
|
||||
resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.27.3':
|
||||
resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.27.3':
|
||||
resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||
|
||||
'@poppinss/colors@4.1.6':
|
||||
resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==}
|
||||
|
||||
'@poppinss/dumper@0.6.5':
|
||||
resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==}
|
||||
|
||||
'@poppinss/exception@1.2.3':
|
||||
resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==}
|
||||
|
||||
'@sindresorhus/is@7.2.0':
|
||||
resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@speed-highlight/core@1.2.15':
|
||||
resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==}
|
||||
|
||||
blake3-wasm@2.1.5:
|
||||
resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==}
|
||||
|
||||
cookie@1.1.1:
|
||||
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
error-stack-parser-es@1.0.5:
|
||||
resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
|
||||
|
||||
esbuild@0.27.3:
|
||||
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
hono@4.12.14:
|
||||
resolution: {integrity: sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
|
||||
kleur@4.1.5:
|
||||
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
miniflare@4.20260415.0:
|
||||
resolution: {integrity: sha512-JoExRWN4YBI2luA5BoSMFEgi8rQWXUGzo3mtE+58VXCLV3jj/Xnk5Yeqs/IXWz8Es5GJIaq6BtsixDvAxXSIng==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
path-to-regexp@6.3.0:
|
||||
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
semver@7.7.4:
|
||||
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
sharp@0.34.5:
|
||||
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
|
||||
supports-color@10.2.2:
|
||||
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici@7.24.8:
|
||||
resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
unenv@2.0.0-rc.24:
|
||||
resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==}
|
||||
|
||||
workerd@1.20260415.1:
|
||||
resolution: {integrity: sha512-phyPjRnx+mQDfkhN9ENPioL1L0SdhYs4S0YmJK/xF9Oga+ykNfdSy1MHnsOj8yqnOV96zcVQMx32dJ0r3pq0jQ==}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
wrangler@4.83.0:
|
||||
resolution: {integrity: sha512-gw5g3LCiuAqVWxaoKY6+quE0HzAUEFb/FV3oAlNkE1ttd4XP3FiV91XDkkzUCcdqxS4WjhQvPhIDBNdhEi8P0A==}
|
||||
engines: {node: '>=20.3.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@cloudflare/workers-types': ^4.20260415.1
|
||||
peerDependenciesMeta:
|
||||
'@cloudflare/workers-types':
|
||||
optional: true
|
||||
|
||||
ws@8.18.0:
|
||||
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
youch-core@0.3.3:
|
||||
resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==}
|
||||
|
||||
youch@4.1.0-beta.10:
|
||||
resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@cloudflare/kv-asset-handler@0.4.2': {}
|
||||
|
||||
'@cloudflare/unenv-preset@2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260415.1)':
|
||||
dependencies:
|
||||
unenv: 2.0.0-rc.24
|
||||
optionalDependencies:
|
||||
workerd: 1.20260415.1
|
||||
|
||||
'@cloudflare/workerd-darwin-64@1.20260415.1':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-darwin-arm64@1.20260415.1':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-linux-64@1.20260415.1':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-linux-arm64@1.20260415.1':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-windows-64@1.20260415.1':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workers-types@4.20260420.1': {}
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
|
||||
'@emnapi/runtime@1.10.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@img/colour@1.1.0': {}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.10.0
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@poppinss/colors@4.1.6':
|
||||
dependencies:
|
||||
kleur: 4.1.5
|
||||
|
||||
'@poppinss/dumper@0.6.5':
|
||||
dependencies:
|
||||
'@poppinss/colors': 4.1.6
|
||||
'@sindresorhus/is': 7.2.0
|
||||
supports-color: 10.2.2
|
||||
|
||||
'@poppinss/exception@1.2.3': {}
|
||||
|
||||
'@sindresorhus/is@7.2.0': {}
|
||||
|
||||
'@speed-highlight/core@1.2.15': {}
|
||||
|
||||
blake3-wasm@2.1.5: {}
|
||||
|
||||
cookie@1.1.1: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
error-stack-parser-es@1.0.5: {}
|
||||
|
||||
esbuild@0.27.3:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.3
|
||||
'@esbuild/android-arm': 0.27.3
|
||||
'@esbuild/android-arm64': 0.27.3
|
||||
'@esbuild/android-x64': 0.27.3
|
||||
'@esbuild/darwin-arm64': 0.27.3
|
||||
'@esbuild/darwin-x64': 0.27.3
|
||||
'@esbuild/freebsd-arm64': 0.27.3
|
||||
'@esbuild/freebsd-x64': 0.27.3
|
||||
'@esbuild/linux-arm': 0.27.3
|
||||
'@esbuild/linux-arm64': 0.27.3
|
||||
'@esbuild/linux-ia32': 0.27.3
|
||||
'@esbuild/linux-loong64': 0.27.3
|
||||
'@esbuild/linux-mips64el': 0.27.3
|
||||
'@esbuild/linux-ppc64': 0.27.3
|
||||
'@esbuild/linux-riscv64': 0.27.3
|
||||
'@esbuild/linux-s390x': 0.27.3
|
||||
'@esbuild/linux-x64': 0.27.3
|
||||
'@esbuild/netbsd-arm64': 0.27.3
|
||||
'@esbuild/netbsd-x64': 0.27.3
|
||||
'@esbuild/openbsd-arm64': 0.27.3
|
||||
'@esbuild/openbsd-x64': 0.27.3
|
||||
'@esbuild/openharmony-arm64': 0.27.3
|
||||
'@esbuild/sunos-x64': 0.27.3
|
||||
'@esbuild/win32-arm64': 0.27.3
|
||||
'@esbuild/win32-ia32': 0.27.3
|
||||
'@esbuild/win32-x64': 0.27.3
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
hono@4.12.14: {}
|
||||
|
||||
kleur@4.1.5: {}
|
||||
|
||||
miniflare@4.20260415.0:
|
||||
dependencies:
|
||||
'@cspotcode/source-map-support': 0.8.1
|
||||
sharp: 0.34.5
|
||||
undici: 7.24.8
|
||||
workerd: 1.20260415.1
|
||||
ws: 8.18.0
|
||||
youch: 4.1.0-beta.10
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
path-to-regexp@6.3.0: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
semver@7.7.4: {}
|
||||
|
||||
sharp@0.34.5:
|
||||
dependencies:
|
||||
'@img/colour': 1.1.0
|
||||
detect-libc: 2.1.2
|
||||
semver: 7.7.4
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.34.5
|
||||
'@img/sharp-darwin-x64': 0.34.5
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
'@img/sharp-linux-arm': 0.34.5
|
||||
'@img/sharp-linux-arm64': 0.34.5
|
||||
'@img/sharp-linux-ppc64': 0.34.5
|
||||
'@img/sharp-linux-riscv64': 0.34.5
|
||||
'@img/sharp-linux-s390x': 0.34.5
|
||||
'@img/sharp-linux-x64': 0.34.5
|
||||
'@img/sharp-linuxmusl-arm64': 0.34.5
|
||||
'@img/sharp-linuxmusl-x64': 0.34.5
|
||||
'@img/sharp-wasm32': 0.34.5
|
||||
'@img/sharp-win32-arm64': 0.34.5
|
||||
'@img/sharp-win32-ia32': 0.34.5
|
||||
'@img/sharp-win32-x64': 0.34.5
|
||||
|
||||
supports-color@10.2.2: {}
|
||||
|
||||
tslib@2.8.1:
|
||||
optional: true
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
undici@7.24.8: {}
|
||||
|
||||
unenv@2.0.0-rc.24:
|
||||
dependencies:
|
||||
pathe: 2.0.3
|
||||
|
||||
workerd@1.20260415.1:
|
||||
optionalDependencies:
|
||||
'@cloudflare/workerd-darwin-64': 1.20260415.1
|
||||
'@cloudflare/workerd-darwin-arm64': 1.20260415.1
|
||||
'@cloudflare/workerd-linux-64': 1.20260415.1
|
||||
'@cloudflare/workerd-linux-arm64': 1.20260415.1
|
||||
'@cloudflare/workerd-windows-64': 1.20260415.1
|
||||
|
||||
wrangler@4.83.0(@cloudflare/workers-types@4.20260420.1):
|
||||
dependencies:
|
||||
'@cloudflare/kv-asset-handler': 0.4.2
|
||||
'@cloudflare/unenv-preset': 2.16.0(unenv@2.0.0-rc.24)(workerd@1.20260415.1)
|
||||
blake3-wasm: 2.1.5
|
||||
esbuild: 0.27.3
|
||||
miniflare: 4.20260415.0
|
||||
path-to-regexp: 6.3.0
|
||||
unenv: 2.0.0-rc.24
|
||||
workerd: 1.20260415.1
|
||||
optionalDependencies:
|
||||
'@cloudflare/workers-types': 4.20260420.1
|
||||
fsevents: 2.3.3
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
ws@8.18.0: {}
|
||||
|
||||
youch-core@0.3.3:
|
||||
dependencies:
|
||||
'@poppinss/exception': 1.2.3
|
||||
error-stack-parser-es: 1.0.5
|
||||
|
||||
youch@4.1.0-beta.10:
|
||||
dependencies:
|
||||
'@poppinss/colors': 4.1.6
|
||||
'@poppinss/dumper': 0.6.5
|
||||
'@speed-highlight/core': 1.2.15
|
||||
cookie: 1.1.1
|
||||
youch-core: 0.3.3
|
||||
@@ -1,8 +0,0 @@
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
sharp: true
|
||||
workerd: true
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
- sharp
|
||||
- workerd
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* arcrun API component Worker (km_writer)
|
||||
*
|
||||
* POST / → JSON input → WASM (WASI preview1 stdin/stdout) → JSON output
|
||||
*
|
||||
* 提供 http_request host function,讓 WASM 零件呼叫 Mira /km/* API。
|
||||
*/
|
||||
|
||||
import componentWasm from '../component.wasm' assert { type: 'webassembly' };
|
||||
import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
import { createWasiShim, type WasiHostFunctions } from '../../../cypher-executor/src/lib/wasi-shim';
|
||||
|
||||
const app = new Hono();
|
||||
app.use('*', cors());
|
||||
|
||||
app.get('/', (c) => c.json({ ok: true, component: 'km_writer' }));
|
||||
|
||||
app.post('/', async (c) => {
|
||||
let input: unknown;
|
||||
try {
|
||||
input = await c.req.json();
|
||||
} catch {
|
||||
return c.json({ success: false, error: 'request body must be JSON' }, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await runWasm(input);
|
||||
return c.json(result);
|
||||
} catch (e) {
|
||||
return c.json(
|
||||
{ success: false, error: e instanceof Error ? e.message : String(e) },
|
||||
500,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
||||
async function runWasm(input: unknown): Promise<unknown> {
|
||||
const hostFunctions: WasiHostFunctions = {
|
||||
http_request: async (url, method, headersJson, body) => {
|
||||
const headers: Record<string, string> = {};
|
||||
if (headersJson) {
|
||||
try {
|
||||
const parsed = JSON.parse(headersJson);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
|
||||
if (typeof v === 'string') headers[k] = v;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore header parse errors
|
||||
}
|
||||
}
|
||||
const init: RequestInit = { method, headers };
|
||||
if (body && method.toUpperCase() !== 'GET' && method.toUpperCase() !== 'HEAD') {
|
||||
init.body = body;
|
||||
}
|
||||
const res = await fetch(url, init);
|
||||
const text = await res.text();
|
||||
// 修架構債(同 http_request):非 2xx 包成帶 "error" key 的 envelope,
|
||||
// 讓 WASM 端既有的 error 判定正確識別失敗(原本只回 body 丟掉 status → 4xx 被判 success)。
|
||||
if (!res.ok) {
|
||||
return JSON.stringify({ error: `HTTP ${res.status}`, status: res.status, body: text });
|
||||
}
|
||||
return text;
|
||||
},
|
||||
};
|
||||
|
||||
const shim = createWasiShim(JSON.stringify(input), hostFunctions);
|
||||
|
||||
const instance = await WebAssembly.instantiate(
|
||||
componentWasm as WebAssembly.Module,
|
||||
shim.imports,
|
||||
);
|
||||
shim.setMemory(instance.exports.memory as WebAssembly.Memory);
|
||||
await shim.run(instance);
|
||||
|
||||
const stdout = shim.getStdout().trim();
|
||||
if (!stdout) throw new Error('WASM component produced no output');
|
||||
return JSON.parse(stdout);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["@cloudflare/workers-types"],
|
||||
"strict": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
name = "arcrun-km-writer"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2025-02-19"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
workers_dev = true
|
||||
|
||||
[vars]
|
||||
COMPONENT_ID = "km_writer"
|
||||
|
||||
[[routes]]
|
||||
pattern = "km-writer.arcrun.dev/*"
|
||||
zone_name = "arcrun.dev"
|
||||
Binary file not shown.
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"name": "arcrun-platform-crypto",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"hono": "^4.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20250408.0",
|
||||
"typescript": "^5.4.0",
|
||||
"wrangler": "^4.0.0"
|
||||
}
|
||||
}
|
||||
-898
@@ -1,898 +0,0 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
hono:
|
||||
specifier: ^4.7.0
|
||||
version: 4.12.17
|
||||
devDependencies:
|
||||
'@cloudflare/workers-types':
|
||||
specifier: ^4.20250408.0
|
||||
version: 4.20260505.1
|
||||
typescript:
|
||||
specifier: ^5.4.0
|
||||
version: 5.9.3
|
||||
wrangler:
|
||||
specifier: ^4.0.0
|
||||
version: 4.87.0(@cloudflare/workers-types@4.20260505.1)
|
||||
|
||||
packages:
|
||||
|
||||
'@cloudflare/kv-asset-handler@0.5.0':
|
||||
resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==}
|
||||
engines: {node: '>=22.0.0'}
|
||||
|
||||
'@cloudflare/unenv-preset@2.16.1':
|
||||
resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==}
|
||||
peerDependencies:
|
||||
unenv: 2.0.0-rc.24
|
||||
workerd: '>1.20260305.0 <2.0.0-0'
|
||||
peerDependenciesMeta:
|
||||
workerd:
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-darwin-64@1.20260430.1':
|
||||
resolution: {integrity: sha512-ADohZUHf7NBvPp2PdZig2Opxx+hDkk3ve7jrTne3JRx9kDSB73zc4LzcEeEN8LKkbAcqZmvfRJfpChSlusu0lA==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@cloudflare/workerd-darwin-arm64@1.20260430.1':
|
||||
resolution: {integrity: sha512-/DoYC/1wHs+YRZzzqSQg1/EHB4hiv1yV5U8FnmapRRIzVaPtnt+ApeOXeMrIdKidgKOI8TqQzgBU8xbIM7Cl4Q==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@cloudflare/workerd-linux-64@1.20260430.1':
|
||||
resolution: {integrity: sha512-koJhBWvEVZPKCVFtMLp2iMHlYr+lFCF47wGbnlKdHVlemV0zTxJEyHI8aLlrhPLhBmOmYLp46rXw09/qJkRIhQ==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@cloudflare/workerd-linux-arm64@1.20260430.1':
|
||||
resolution: {integrity: sha512-hMdapNAzNQZDXGGkg4Slydc3fRJP5FUZLJVVcZCW/+imhhJro9Z1rv5n/wfR+txKoSWhTYR8eOp8Pyi2bzLzlw==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@cloudflare/workerd-windows-64@1.20260430.1':
|
||||
resolution: {integrity: sha512-jS3ffixjb5USOwz4frw4WzCz0HrjVxkgyU3WiYb06N7hBAfN6eOrveAJ4QRef0+suK4V1vQFoB1oKdRBsXe9Dw==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@cloudflare/workers-types@4.20260505.1':
|
||||
resolution: {integrity: sha512-Uz9D2hcwB4/pdnmCU7RsgknY8TQ5st0cQMMN6h/hvWt1TCt99GUkbi6dMgWdP7jXfIfh+S/EI5zQugI9RZn4Bw==}
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@emnapi/runtime@1.10.0':
|
||||
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.27.3':
|
||||
resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.27.3':
|
||||
resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.27.3':
|
||||
resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.27.3':
|
||||
resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.27.3':
|
||||
resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.27.3':
|
||||
resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.3':
|
||||
resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.3':
|
||||
resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.3':
|
||||
resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.27.3':
|
||||
resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.27.3':
|
||||
resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.27.3':
|
||||
resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.27.3':
|
||||
resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.27.3':
|
||||
resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||
|
||||
'@poppinss/colors@4.1.6':
|
||||
resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==}
|
||||
|
||||
'@poppinss/dumper@0.6.5':
|
||||
resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==}
|
||||
|
||||
'@poppinss/exception@1.2.3':
|
||||
resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==}
|
||||
|
||||
'@sindresorhus/is@7.2.0':
|
||||
resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@speed-highlight/core@1.2.15':
|
||||
resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==}
|
||||
|
||||
blake3-wasm@2.1.5:
|
||||
resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==}
|
||||
|
||||
cookie@1.1.1:
|
||||
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
error-stack-parser-es@1.0.5:
|
||||
resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==}
|
||||
|
||||
esbuild@0.27.3:
|
||||
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
hono@4.12.17:
|
||||
resolution: {integrity: sha512-FbJJNb/XgX7YW0hX/V8w5oYLztKEsRLykCMZWt1WdLtsfjzMvmoqWBA4H4t5norinq8/rh20oiZYr+WSl4UzAQ==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
|
||||
kleur@4.1.5:
|
||||
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
miniflare@4.20260430.0:
|
||||
resolution: {integrity: sha512-MWvMm3Siho9Yj7lbJZidLs8hbrRvIcOrif2mnsHQZdvoKfedpea+GaN8XJxbpRcq0B2WzNI1BB1ihdnqes3/ZA==}
|
||||
engines: {node: '>=22.0.0'}
|
||||
hasBin: true
|
||||
|
||||
path-to-regexp@6.3.0:
|
||||
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
semver@7.7.4:
|
||||
resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
sharp@0.34.5:
|
||||
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
|
||||
supports-color@10.2.2:
|
||||
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici@7.24.8:
|
||||
resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
unenv@2.0.0-rc.24:
|
||||
resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==}
|
||||
|
||||
workerd@1.20260430.1:
|
||||
resolution: {integrity: sha512-KEgIWyiw3Jmn+DCd/L3ePo5fmiiYb/UcwKvDWPf/nLLOiwShDFzDSsegU5NY/JcwgvO/QsLHVi2FYrbkcXNY5Q==}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
wrangler@4.87.0:
|
||||
resolution: {integrity: sha512-lfhfKwLfQlowwgV0xhlYgE9fU3n0I30d4ccGY/rTCEm/n42Mjvlr0Ng3ZPNqlsrsKBcDR531V7dsPkgELvrk/Q==}
|
||||
engines: {node: '>=22.0.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@cloudflare/workers-types': ^4.20260430.1
|
||||
peerDependenciesMeta:
|
||||
'@cloudflare/workers-types':
|
||||
optional: true
|
||||
|
||||
ws@8.18.0:
|
||||
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
youch-core@0.3.3:
|
||||
resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==}
|
||||
|
||||
youch@4.1.0-beta.10:
|
||||
resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@cloudflare/kv-asset-handler@0.5.0': {}
|
||||
|
||||
'@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260430.1)':
|
||||
dependencies:
|
||||
unenv: 2.0.0-rc.24
|
||||
optionalDependencies:
|
||||
workerd: 1.20260430.1
|
||||
|
||||
'@cloudflare/workerd-darwin-64@1.20260430.1':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-darwin-arm64@1.20260430.1':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-linux-64@1.20260430.1':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-linux-arm64@1.20260430.1':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-windows-64@1.20260430.1':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workers-types@4.20260505.1': {}
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
|
||||
'@emnapi/runtime@1.10.0':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@img/colour@1.1.0': {}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.10.0
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@poppinss/colors@4.1.6':
|
||||
dependencies:
|
||||
kleur: 4.1.5
|
||||
|
||||
'@poppinss/dumper@0.6.5':
|
||||
dependencies:
|
||||
'@poppinss/colors': 4.1.6
|
||||
'@sindresorhus/is': 7.2.0
|
||||
supports-color: 10.2.2
|
||||
|
||||
'@poppinss/exception@1.2.3': {}
|
||||
|
||||
'@sindresorhus/is@7.2.0': {}
|
||||
|
||||
'@speed-highlight/core@1.2.15': {}
|
||||
|
||||
blake3-wasm@2.1.5: {}
|
||||
|
||||
cookie@1.1.1: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
error-stack-parser-es@1.0.5: {}
|
||||
|
||||
esbuild@0.27.3:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.3
|
||||
'@esbuild/android-arm': 0.27.3
|
||||
'@esbuild/android-arm64': 0.27.3
|
||||
'@esbuild/android-x64': 0.27.3
|
||||
'@esbuild/darwin-arm64': 0.27.3
|
||||
'@esbuild/darwin-x64': 0.27.3
|
||||
'@esbuild/freebsd-arm64': 0.27.3
|
||||
'@esbuild/freebsd-x64': 0.27.3
|
||||
'@esbuild/linux-arm': 0.27.3
|
||||
'@esbuild/linux-arm64': 0.27.3
|
||||
'@esbuild/linux-ia32': 0.27.3
|
||||
'@esbuild/linux-loong64': 0.27.3
|
||||
'@esbuild/linux-mips64el': 0.27.3
|
||||
'@esbuild/linux-ppc64': 0.27.3
|
||||
'@esbuild/linux-riscv64': 0.27.3
|
||||
'@esbuild/linux-s390x': 0.27.3
|
||||
'@esbuild/linux-x64': 0.27.3
|
||||
'@esbuild/netbsd-arm64': 0.27.3
|
||||
'@esbuild/netbsd-x64': 0.27.3
|
||||
'@esbuild/openbsd-arm64': 0.27.3
|
||||
'@esbuild/openbsd-x64': 0.27.3
|
||||
'@esbuild/openharmony-arm64': 0.27.3
|
||||
'@esbuild/sunos-x64': 0.27.3
|
||||
'@esbuild/win32-arm64': 0.27.3
|
||||
'@esbuild/win32-ia32': 0.27.3
|
||||
'@esbuild/win32-x64': 0.27.3
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
hono@4.12.17: {}
|
||||
|
||||
kleur@4.1.5: {}
|
||||
|
||||
miniflare@4.20260430.0:
|
||||
dependencies:
|
||||
'@cspotcode/source-map-support': 0.8.1
|
||||
sharp: 0.34.5
|
||||
undici: 7.24.8
|
||||
workerd: 1.20260430.1
|
||||
ws: 8.18.0
|
||||
youch: 4.1.0-beta.10
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
path-to-regexp@6.3.0: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
semver@7.7.4: {}
|
||||
|
||||
sharp@0.34.5:
|
||||
dependencies:
|
||||
'@img/colour': 1.1.0
|
||||
detect-libc: 2.1.2
|
||||
semver: 7.7.4
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.34.5
|
||||
'@img/sharp-darwin-x64': 0.34.5
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
'@img/sharp-linux-arm': 0.34.5
|
||||
'@img/sharp-linux-arm64': 0.34.5
|
||||
'@img/sharp-linux-ppc64': 0.34.5
|
||||
'@img/sharp-linux-riscv64': 0.34.5
|
||||
'@img/sharp-linux-s390x': 0.34.5
|
||||
'@img/sharp-linux-x64': 0.34.5
|
||||
'@img/sharp-linuxmusl-arm64': 0.34.5
|
||||
'@img/sharp-linuxmusl-x64': 0.34.5
|
||||
'@img/sharp-wasm32': 0.34.5
|
||||
'@img/sharp-win32-arm64': 0.34.5
|
||||
'@img/sharp-win32-ia32': 0.34.5
|
||||
'@img/sharp-win32-x64': 0.34.5
|
||||
|
||||
supports-color@10.2.2: {}
|
||||
|
||||
tslib@2.8.1:
|
||||
optional: true
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
undici@7.24.8: {}
|
||||
|
||||
unenv@2.0.0-rc.24:
|
||||
dependencies:
|
||||
pathe: 2.0.3
|
||||
|
||||
workerd@1.20260430.1:
|
||||
optionalDependencies:
|
||||
'@cloudflare/workerd-darwin-64': 1.20260430.1
|
||||
'@cloudflare/workerd-darwin-arm64': 1.20260430.1
|
||||
'@cloudflare/workerd-linux-64': 1.20260430.1
|
||||
'@cloudflare/workerd-linux-arm64': 1.20260430.1
|
||||
'@cloudflare/workerd-windows-64': 1.20260430.1
|
||||
|
||||
wrangler@4.87.0(@cloudflare/workers-types@4.20260505.1):
|
||||
dependencies:
|
||||
'@cloudflare/kv-asset-handler': 0.5.0
|
||||
'@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260430.1)
|
||||
blake3-wasm: 2.1.5
|
||||
esbuild: 0.27.3
|
||||
miniflare: 4.20260430.0
|
||||
path-to-regexp: 6.3.0
|
||||
unenv: 2.0.0-rc.24
|
||||
workerd: 1.20260430.1
|
||||
optionalDependencies:
|
||||
'@cloudflare/workers-types': 4.20260505.1
|
||||
fsevents: 2.3.3
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
ws@8.18.0: {}
|
||||
|
||||
youch-core@0.3.3:
|
||||
dependencies:
|
||||
'@poppinss/exception': 1.2.3
|
||||
error-stack-parser-es: 1.0.5
|
||||
|
||||
youch@4.1.0-beta.10:
|
||||
dependencies:
|
||||
'@poppinss/colors': 4.1.6
|
||||
'@poppinss/dumper': 0.6.5
|
||||
'@speed-highlight/core': 1.2.15
|
||||
cookie: 1.1.1
|
||||
youch-core: 0.3.3
|
||||
@@ -1,8 +0,0 @@
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
sharp: true
|
||||
workerd: true
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
- sharp
|
||||
- workerd
|
||||
@@ -1,108 +0,0 @@
|
||||
/**
|
||||
* arcrun platform_crypto Worker
|
||||
*
|
||||
* POST / → JSON input {action, ...} → JSON output
|
||||
*
|
||||
* Actions:
|
||||
* generate_api_key — HMAC-SHA256(email, ENCRYPTION_KEY) → ak_{hex[:32]}
|
||||
* encrypt — AES-GCM(plaintext, ENCRYPTION_KEY) → {encrypted, iv}(base64)
|
||||
* random_token — crypto random bytes → hex string
|
||||
*
|
||||
* 安全邊界:ENCRYPTION_KEY 只存在於 closure,永不進入外部(rule 02 §2.2)。
|
||||
* 此 Worker 直接用 crypto.subtle 實作,不走 WASM runner。
|
||||
* TinyGo WASM async host function 在 Cloudflare Workers 的 u6u namespace 不支援 Promise.
|
||||
* WASM 零件 (registry/components/platform_crypto/) 保留作為 edge-Go 移植時的參考。
|
||||
*/
|
||||
|
||||
import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
|
||||
type Env = {
|
||||
ENCRYPTION_KEY: string;
|
||||
};
|
||||
|
||||
type Input = {
|
||||
action: string;
|
||||
email?: string;
|
||||
plaintext?: string;
|
||||
bytes?: number;
|
||||
};
|
||||
|
||||
const app = new Hono<{ Bindings: Env }>();
|
||||
app.use('*', cors());
|
||||
|
||||
app.get('/', (c) => c.json({ ok: true, component: 'platform_crypto' }));
|
||||
|
||||
app.post('/', async (c) => {
|
||||
let input: Input;
|
||||
try {
|
||||
input = await c.req.json() as Input;
|
||||
} catch {
|
||||
return c.json({ success: false, error: 'request body must be JSON' }, 400);
|
||||
}
|
||||
|
||||
const encryptionKey = c.env.ENCRYPTION_KEY;
|
||||
if (!encryptionKey) {
|
||||
return c.json({ success: false, error: 'ENCRYPTION_KEY not configured' }, 503);
|
||||
}
|
||||
|
||||
try {
|
||||
switch (input.action) {
|
||||
case 'generate_api_key': {
|
||||
if (!input.email) return c.json({ success: false, error: 'email 必填' }, 400);
|
||||
const apiKey = await generateApiKey(input.email, encryptionKey);
|
||||
return c.json({ success: true, api_key: apiKey });
|
||||
}
|
||||
case 'encrypt': {
|
||||
if (!input.plaintext) return c.json({ success: false, error: 'plaintext 必填' }, 400);
|
||||
const { encrypted, iv } = await aesEncrypt(input.plaintext, encryptionKey);
|
||||
return c.json({ success: true, encrypted, iv });
|
||||
}
|
||||
case 'random_token': {
|
||||
const numBytes = (input.bytes ?? 32) > 0 ? (input.bytes ?? 32) : 32;
|
||||
const token = randomHex(numBytes);
|
||||
return c.json({ success: true, token });
|
||||
}
|
||||
default:
|
||||
return c.json({ success: false, error: `不支援的 action: ${input.action}` }, 400);
|
||||
}
|
||||
} catch (e) {
|
||||
return c.json(
|
||||
{ success: false, error: e instanceof Error ? e.message : String(e) },
|
||||
500,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
||||
// ── Crypto implementations (rule 02 §2.2: crypto.subtle 只准在 wasi-shim.ts 或 platform_crypto) ──
|
||||
|
||||
async function generateApiKey(email: string, encryptionKey: string): Promise<string> {
|
||||
const keyBytes = new TextEncoder().encode(encryptionKey.slice(0, 32));
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
|
||||
);
|
||||
const sig = await crypto.subtle.sign('HMAC', cryptoKey, new TextEncoder().encode(email));
|
||||
const hex = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
return 'ak_' + hex.slice(0, 32);
|
||||
}
|
||||
|
||||
async function aesEncrypt(plaintext: string, encryptionKey: string): Promise<{ encrypted: string; iv: string }> {
|
||||
const keyBytes = new TextEncoder().encode(encryptionKey.slice(0, 32));
|
||||
const cryptoKey = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt']);
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const enc = await crypto.subtle.encrypt(
|
||||
{ name: 'AES-GCM', iv },
|
||||
cryptoKey,
|
||||
new TextEncoder().encode(plaintext),
|
||||
);
|
||||
const toB64 = (buf: ArrayBuffer | Uint8Array) =>
|
||||
btoa(String.fromCharCode(...new Uint8Array(buf instanceof ArrayBuffer ? buf : buf)));
|
||||
return { encrypted: toB64(enc), iv: toB64(iv) };
|
||||
}
|
||||
|
||||
function randomHex(numBytes: number): string {
|
||||
const arr = crypto.getRandomValues(new Uint8Array(numBytes));
|
||||
return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["@cloudflare/workers-types"],
|
||||
"strict": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
name = "arcrun-platform-crypto"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2025-02-19"
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
workers_dev = true
|
||||
|
||||
[vars]
|
||||
COMPONENT_ID = "platform_crypto"
|
||||
|
||||
[[routes]]
|
||||
pattern = "platform-crypto.arcrun.dev/*"
|
||||
zone_name = "arcrun.dev"
|
||||
|
||||
# ENCRYPTION_KEY 透過 wrangler secret set 設定
|
||||
# wrangler secret put ENCRYPTION_KEY
|
||||
+3
-7
@@ -30,19 +30,15 @@ CLOUDFLARE_ACCOUNT_ID=
|
||||
CLOUDFLARE_API_TOKEN=
|
||||
|
||||
|
||||
# ── ② 身份與加密(自架單人用,這兩格你自己決定/保管)──────────────────────────
|
||||
# ── ② 身份(自架單人用)──────────────────────────────────────────────────────
|
||||
#
|
||||
# NAMESPACE:你的資料分區標籤。隨便取個英數小名即可(例:leo、myteam)。
|
||||
# 這不是密碼,只是用來分隔你的資料。
|
||||
#
|
||||
# ENCRYPTION_KEY:你的 credential 加密金鑰,64 個以上的 hex 字元。你自己保管。
|
||||
# 不會的話,AI 可以幫你產一串:
|
||||
# node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
# ⚠️ 這串忘了 = 你之前上傳加密的 credential 就解不開了,請留底。
|
||||
# (安裝完還要把「同一串」設進你的 worker,acr init 會印確切指令給你跟著做。)
|
||||
# (credential 不再需要自管加密金鑰:明文由 CF Workers Secrets 託管,
|
||||
# `acr creds push` 會直接寫進你自己的 worker。)
|
||||
#
|
||||
NAMESPACE=
|
||||
ENCRYPTION_KEY=
|
||||
|
||||
|
||||
# ── ③ 各服務的 token(要連哪個服務才填哪個;可之後再加)────────────────────────
|
||||
|
||||
+24
@@ -6,6 +6,10 @@ dist/
|
||||
# 例外:放行 .component-builds 的部署物 wasm — self-host 用戶 / acr init 從 repo 直接拿這份部署
|
||||
# (推翻 rule 05 原「wasm 不 commit」慣例,見 .agents/specs/arcrun/sdk-and-website/self-hosted-init.md §6)
|
||||
!.component-builds/**/component.wasm
|
||||
# 例外:Arcrun#80 tier2 worker 官方編譯成品(cypher-executor/kbdb/http_request/code/mcp 的
|
||||
# esbuild bundle + 隨附 wasm part)——commit 進 repo 同一套理由:固定位置、any clone 都拿得到,
|
||||
# 不必自己再編一次(見 scripts/build-worker-artifacts.mjs)。
|
||||
!.worker-builds/**/*.wasm
|
||||
# 例外:code 零件(自足 Worker)的 vendored quickjs.wasm 同屬部署物 —— acr init/update 從
|
||||
# repo archive 直接部署(同上 .component-builds 放行邏輯)。來源=npm 套件
|
||||
# @jitl/quickjs-wasmfile-release-sync 的 emscripten-module.wasm,由 postinstall vendor-wasm.mjs
|
||||
@@ -48,3 +52,23 @@ scripts/__pycache__/
|
||||
# D1 備份/匯出(wrangler d1 export 產物,含整庫全量資料=機敏,絕不 commit)
|
||||
*.sql
|
||||
backup-*.sql
|
||||
|
||||
# GitHub 公開 mirror 工作目錄(publish-github.sh 產物)
|
||||
.github-public/
|
||||
wrangler.leo21c.toml
|
||||
|
||||
# deploy-all.mjs 產的共用依賴(部署時 npm 安裝 wrangler 等,非 repo 內容)
|
||||
# 2026-08-07:每次本機跑部署都會冒出來吵未推警察,且含不該進版控的鎖檔
|
||||
/package.json
|
||||
/package-lock.json
|
||||
|
||||
# console-ui 部署產物(deploy.mjs 依 deploy.targets.json 即時產生,不是原始碼)
|
||||
console-ui/.staging/
|
||||
# 「上一次通過線上實測的部署」紀錄——本機事實,不隨 repo 走
|
||||
# (刻意不進版控:新 checkout 沒有紀錄 ⇒ 狀態未知 ⇒ 該被大聲提醒,而不是繼承別人的綠燈)
|
||||
console-ui/.deploy-state.json
|
||||
|
||||
# Wrangler 本機開發用的密鑰檔——絕不進版控(2026-08-09 補:原本沒被擋,
|
||||
# 而同目錄有 agent 在動工,一次 git add -A 就會把金鑰推上去)
|
||||
.dev.vars
|
||||
**/.dev.vars
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"built_for": "arcrun-tier2-worker-artifacts",
|
||||
"generated_at": "2026-08-11T05:33:37.988Z",
|
||||
"repo_head": "d8bbf2241bd6b117d76fb27d9e386ecfb0ffe8f7",
|
||||
"repo_dirty": false,
|
||||
"workers": [
|
||||
{
|
||||
"name": "arcrun-cypher-executor",
|
||||
"source_dir": "cypher-executor",
|
||||
"source_commit": "797e7f751cc42cb1f5d9e2e187f18cf51eb981a1",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-cypher-executor/worker.mjs",
|
||||
"js_bytes": 568855,
|
||||
"content_sha256": "66e2a6341854e8b2de0567a46282b94669e73b95d152b05b17b0f8b58e257fec",
|
||||
"modules": [],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [
|
||||
"nodejs_compat",
|
||||
"global_fetch_strictly_public"
|
||||
],
|
||||
"requires": {
|
||||
"kv": [
|
||||
"EXEC_CONTEXT",
|
||||
"WEBHOOKS",
|
||||
"CREDENTIALS_KV",
|
||||
"ANALYTICS_KV",
|
||||
"RECIPES",
|
||||
"USERS_KV",
|
||||
"SESSIONS_KV"
|
||||
],
|
||||
"d1": [
|
||||
{
|
||||
"binding": "CREDENTIALS_DB",
|
||||
"database_name": "arcrun-kbdb"
|
||||
}
|
||||
],
|
||||
"vectorize": 0,
|
||||
"ai": true,
|
||||
"vars": {
|
||||
"ENVIRONMENT": "production",
|
||||
"CF_ACCOUNT_ID": "",
|
||||
"WORKER_SUBDOMAIN": "uncle6-me",
|
||||
"KBDB_BASE_URL": "https://arcrun-kbdb.uncle6-me.workers.dev",
|
||||
"CONSOLE_TENANT": "leo",
|
||||
"PORTAL_SESSION_TTL": "604800",
|
||||
"PORTAL_SHOW_WORKFLOWS": "admin",
|
||||
"GITEA_BASE_URL": "https://git.uncle6.me",
|
||||
"GITEA_SPRINT_REPO": "Leo/InkStoneCo",
|
||||
"GITEA_SPRINT_DIR": "system-dev/docs/3-specs/autonomy-dispatch"
|
||||
}
|
||||
},
|
||||
"stripped": {
|
||||
"services": 13
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-kbdb",
|
||||
"source_dir": "kbdb",
|
||||
"source_commit": "a7e23badf2a771be779a861e69e7efa6e8141dfe",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-kbdb/worker.mjs",
|
||||
"js_bytes": 135910,
|
||||
"content_sha256": "5e5a7a030f4fd1f5549ace6791c3827b6497b0bfdd9add46af041af47c472905",
|
||||
"modules": [],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [
|
||||
"nodejs_compat"
|
||||
],
|
||||
"requires": {
|
||||
"kv": [],
|
||||
"d1": [
|
||||
{
|
||||
"binding": "DB",
|
||||
"database_name": "arcrun-kbdb"
|
||||
}
|
||||
],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"ENVIRONMENT": "production"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-http-request",
|
||||
"source_dir": ".component-builds/http_request",
|
||||
"source_commit": "1e85dfb49b0e8d81c0854781d93ee4e6a300c7b3",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-http-request/worker.mjs",
|
||||
"js_bytes": 80073,
|
||||
"content_sha256": "9a9dcb71879a7bdfd9fec1bd94eb9742e12cb63733d822ce63eeb1be30008d15",
|
||||
"modules": [
|
||||
{
|
||||
"name": "component.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-http-request/component.wasm",
|
||||
"sha256": "cc15cc785703e7bbb8dbff2d38dc84a4ac24e2f44316182730abae0f170ef133"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [
|
||||
"nodejs_compat",
|
||||
"global_fetch_strictly_public"
|
||||
],
|
||||
"requires": {
|
||||
"kv": [],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "http_request"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-code",
|
||||
"source_dir": "registry/components/code",
|
||||
"source_commit": "621cb8d948d61be6202063fd02effb3f538437fe",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-code/worker.mjs",
|
||||
"js_bytes": 153671,
|
||||
"content_sha256": "285a7406ec694ae47dccfaf48517f712c74d207a1689dffa15c39f1555b45be5",
|
||||
"modules": [
|
||||
{
|
||||
"name": "quickjs.wasm",
|
||||
"type": "application/wasm",
|
||||
"file": "arcrun-code/quickjs.wasm",
|
||||
"sha256": "105c3bed22d457e43e3d1c3c1c6959fda62a8fe06f0fc8a985303c3a2be72232"
|
||||
}
|
||||
],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [],
|
||||
"requires": {
|
||||
"kv": [],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {
|
||||
"COMPONENT_ID": "code"
|
||||
}
|
||||
},
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"name": "arcrun-mcp",
|
||||
"source_dir": "mcp",
|
||||
"source_commit": "035e8b255b0dcbd4238707f7d2ac8ccf9ee1ba72",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-mcp/worker.mjs",
|
||||
"js_bytes": 1165130,
|
||||
"content_sha256": "be15033f32e605f03f69bd10cd87782dafa34dbafeee2ce367bd7361a062a291",
|
||||
"modules": [],
|
||||
"compat_date": "2024-11-27",
|
||||
"compat_flags": [
|
||||
"nodejs_compat"
|
||||
],
|
||||
"requires": {
|
||||
"kv": [
|
||||
"OAUTH_KV"
|
||||
],
|
||||
"d1": [],
|
||||
"vectorize": 0,
|
||||
"ai": false,
|
||||
"vars": {}
|
||||
},
|
||||
"warnings": []
|
||||
}
|
||||
],
|
||||
"notes": []
|
||||
}
|
||||
+11
-4
@@ -19,7 +19,7 @@
|
||||
- [~] **credential 注入 401 修復** — `{{credential.X}}` 注入失敗,用戶被迫把 token 明文寫進 workflow。
|
||||
**根因**:此語法系統沒實裝(三條 template 路徑都不認 `credential.` namespace)。
|
||||
**修法**(design §8,richblack 2026-06-10 確認):auth_static_key 加 `resolve_credentials` action(WASM 解密)
|
||||
+ graph-executor `resolveCredentialRefs` 偵測回填(不碰 ENCRYPTION_KEY,rule 02 §2.2)。
|
||||
+ graph-executor `resolveCredentialRefs` 偵測回填(不碰金鑰,rule 02 §2.2)。
|
||||
**8.1-8.4 done**(tinygo build OK + tsc 0 + §2.2 自檢綠)。**待 8.5 端對端驗收**(部署 + 真 OpenAI key + 全新帳號打 2xx)。
|
||||
- [ ] **§8 P1/P2 recipe/workflow list 遷 D1** — CF KV list 免費僅 1000/日,不修=用戶用一用就 429,
|
||||
免費承諾破功。D1 現已可建(依賴解除),架構拍板走 kbdb `/entries` HTTP 雙寫不加 binding。
|
||||
@@ -27,14 +27,21 @@
|
||||
|
||||
### 🟡 P1(封測門檻 / 技術債)
|
||||
|
||||
- [ ] credential-primitives-wasm **Phase 0.7**(component-loader WASM runner)→ **Phase 1-2**(auth_static_key / auth_service_account WASM 零件)。0.6 已完成。
|
||||
- [ ] **Phase 3** 清除違規 TS(credential-injector.ts / jwt-signer.ts / BUILTIN_*)—— 須先有 Phase 1-2 WASM 頂上。
|
||||
- [x] ~~credential-primitives-wasm Phase 0.7 / Phase 1-2 / Phase 3~~ — **全數完成**,該卷已封存
|
||||
(`system-dev/docs/3-specs/archive/credential-primitives-wasm/`)。auth_static_key /
|
||||
auth_service_account / auth_oauth2 WASM 零件到位;credential-injector.ts、jwt-signer.ts、
|
||||
`BUILTIN_API_RECIPES` / `BUILTIN_CREDENTIALS_MAP` 全數移除(grep 0 筆,最後一項 T10 於
|
||||
2026-07-20 commit `20c7610`)。credential 現行做法=CF Workers Secrets + D1 目錄。
|
||||
- [ ] **`auth_mtls` 零件從未實作**(原 Phase 4.3/4.4 遺留,`registry/components/auth_mtls/` 不存在)。
|
||||
mTLS 認證目前不支援;要做需**另立新 SDD**(封存卷不得復用)。
|
||||
- [ ] **self-hosted auth 鏈端到端驗收**(原 Phase 7.6 遺留):`global_fetch_strictly_public` flag
|
||||
是否真解 same-zone 1042,在自架帳號上從未實測。同屬需另立 SDD 的殘留缺口。
|
||||
- [ ] 4 份 inline http_request host fn 抽共用 helper(dedup;假綠修是逐份改的)。
|
||||
|
||||
### ⚪ P2(不擋封測)
|
||||
|
||||
- [ ] `arcrun.dev/llms.txt` serve(landing/public 補檔)
|
||||
- [ ] ENCRYPTION_KEY 冪等性、MCP account-source、recipe submit uuid 回傳
|
||||
- [ ] MCP account-source、recipe submit uuid 回傳
|
||||
- [ ] 文件遷移階段二/三 + wiki modules/
|
||||
- [ ] 下方第一期殘項:步驟 2(acr recipe test)、步驟 5b(資料外流警示 SDD)、步驟 6(搬家拆 matrix)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
1. **任何 code 變動前必須先讀對應 SDD**,在回覆開頭宣告已讀清單與對應 task 編號(格式見 `.claude/rules/00-sdd-protocol.md`)
|
||||
2. **零件只能用 TinyGo 或 AssemblyScript 編譯成 WASM**;`registry/components/` 下禁止 TypeScript
|
||||
3. **cypher-executor TS 禁止實作 credential / auth / JWT / template 展開業務邏輯**;這些全在 WASM 零件
|
||||
4. **Cypher binding = YAML 裡的 URL 清單**,不是 Cloudflare service binding;零件串接走 HTTP URL(含 auth primitive)。self-hosted same-zone 1042 用 `global_fetch_strictly_public` flag 解,不新增 binding(credential-primitives-wasm Phase 7)
|
||||
4. **Cypher binding = YAML 裡的 URL 清單**,不是 Cloudflare service binding;零件串接走 HTTP URL(含 auth primitive)。self-hosted same-zone 1042 用 `global_fetch_strictly_public` flag 解,不新增 binding(來源:credential-primitives-wasm Phase 7,該卷已封存,規則仍有效)
|
||||
5. **每個 WASM 零件 = 獨立 Worker = 公開 URL**;不從 R2 動態讀(R2 只 Phase 5 啟用)
|
||||
6. **修改現有程式碼,不是新建資料夾重做**
|
||||
7. **每完成一個 task 立刻更新 tasks.md 的 `[x]`**,不批次
|
||||
@@ -29,6 +29,19 @@
|
||||
|
||||
---
|
||||
|
||||
## SDD 生命週期鐵律(2026-07-17 leo 拍板,全文見 `system-dev/docs/3-specs/SDD-LIFECYCLE.md`)
|
||||
|
||||
> 現行規格以 design.md frontmatter `status: active` 為準(機器可查),
|
||||
> hook `.claude/hooks/sdd-guard.sh` + `system-dev/scripts/sdd-active-check.sh` 強制。
|
||||
|
||||
1. **單一活性**:任何時刻整個 repo 最多一份 `status: active` 的 SDD;所有開發任務必須對應這份 SDD 的 tasks,找不到對應 → 停下來問。
|
||||
2. **CC 禁止自行建立 SDD**:任何情況下不得主動建新 SDD(既有 hook 規則 4.3 同源)。
|
||||
3. **規格層變更只有一條路**:寫 change proposal 進 `system-dev/docs/3-specs/pending-changes.md`(變更摘要+影響分析)然後**停止**,等使用者明說「confirm」;沒 confirm 就照現行 SDD 繼續。任務層小改直接更新現行 tasks 並標日期原因。
|
||||
4. **開新 SDD(confirm 後)**:先把舊 SDD 未完成且仍有效的任務**逐條搬入**新 SDD——搬完前不准寫任何程式碼;舊 SDD 改 `closed` 填 `superseded_by` 並移入 `3-specs/archive/`;向使用者列「已搬移/已作廢」清單請最終確認。
|
||||
5. **每次 session 開始**:先讀現行 active SDD 與 pending-changes.md,回報三個數字——「現行規格〈名稱〉+未完成任務 N+待裁決 proposal M」——再開工。
|
||||
|
||||
---
|
||||
|
||||
## 工作流程(強制)
|
||||
|
||||
開始任一任務,按順序:
|
||||
@@ -50,6 +63,29 @@
|
||||
|
||||
---
|
||||
|
||||
## 🔴 第一鐵律:wiki 是判準,不准跳過(2026-07-20/21 leo 兩度點破)
|
||||
|
||||
**要查任何東西之前,先搜尋 wiki——用 grep,不是只讀開頭幾行。**
|
||||
|
||||
> leo:「花很多力氣去產生 wiki,最重要的就是要可以查詢,**結果要查的時候就跳過,那就白寫了**。」
|
||||
> 「重點是你自己的記憶對嗎?而你有按照規定去切實讀 wiki 嗎?」
|
||||
|
||||
```bash
|
||||
grep -rin "<本題關鍵字>" system-dev/wiki/
|
||||
```
|
||||
|
||||
**三條硬規則**:
|
||||
1. **wiki 與程式碼/歷史文件衝突 → 以 wiki 為準**。程式碼反映「還沒清乾淨」,不等於「還在用」。
|
||||
2. wiki 寫「不可動/待廢除/進行中」→ **讀它的解除條件並逐條核對**。那是當時狀態,不是永久禁令。
|
||||
3. 翻原文後得到新結論 → **回頭更新 wiki**(wiki 過時是債,要還)。
|
||||
|
||||
**動外部系統(部署/curl/wrangler/acr/gh)前**:先找 repo 有沒有**現成腳本或 README 部署段**,
|
||||
別自創方法。(實例:2026-07-21 明明有 `npx wrangler deploy` 這條驗過的路,卻自己 curl 硬幹踩坑。)
|
||||
|
||||
> hook `wiki-first-search.sh` 會在你查 code/下高風險指令時自動推 wiki 命中行;
|
||||
> **但機制只是提醒,判斷是你的責任**。
|
||||
|
||||
|
||||
## Wiki(每次 session 的讀取順序)
|
||||
|
||||
| 檔案 | 時機 | 用途 |
|
||||
@@ -78,9 +114,12 @@
|
||||
|
||||
## SDD 位置速查
|
||||
|
||||
> **現行(active)SDD 唯一判準=frontmatter `status: active`**(2026-07-17 起),查法:`bash system-dev/scripts/sdd-active-check.sh`。下表僅路徑索引,「進行中」標記以 frontmatter 為準。
|
||||
|
||||
| 子系統 | 路徑 |
|
||||
|-------|------|
|
||||
| **進行中** Credential Primitives WASM | `docs/3-specs/arcrun/credential-primitives-wasm/` |
|
||||
| **現行 active** RAG Portal 多人授權 | `system-dev/docs/3-specs/portal-auth/` |
|
||||
| ~~Credential Primitives WASM~~(**closed,已封存**) | `system-dev/docs/3-specs/archive/credential-primitives-wasm/` — credential 現行做法見 `.claude/rules/01-tech-stack.md`;殘留缺口(`auth_mtls` 未實作等)要做需另立新 SDD |
|
||||
| arcrun 總進度 | `docs/3-specs/arcrun/arcrun.md` |
|
||||
| Auth Recipe 系統 | `docs/3-specs/arcrun/auth-recipe.md` |
|
||||
| Landing Page | `docs/3-specs/arcrun/landing-page.md` |
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# 想貢獻零件(component)?先確認你真的需要
|
||||
|
||||
> **99% 的需求不需要新零件。** 零件是**專業等級**、走 PR 審核;
|
||||
> **recipe / workflow / app 誰都可以做**,隨建隨用、不必部署。
|
||||
|
||||
---
|
||||
|
||||
## 先照這個順序找,多半不用寫零件
|
||||
|
||||
**1. 語意搜尋知識庫**(最強——它能找到你沒猜中的用詞)
|
||||
```
|
||||
kbdb_search(q="我想達成什麼(用一句話描述)", mode="semantic")
|
||||
kbdb_get_map() # 不確定該查哪個庫,先看藏書地圖
|
||||
```
|
||||
|
||||
**2. 看現成的服務整合**(26 個:GitHub/Notion/Gemini/Slack…)
|
||||
```bash
|
||||
acr auth-recipe list
|
||||
acr auth-recipe scaffold github # 直接吐出 credentials 範本 + workflow 範例
|
||||
```
|
||||
|
||||
**3. 看零件全集**(21 顆通用零件)
|
||||
```bash
|
||||
acr parts
|
||||
```
|
||||
特別注意 **`http_request`**:它能打**任意** HTTP API。
|
||||
「平台沒有 XX 服務的零件」通常不成立——用 `http_request` + 一份 recipe 就有了。
|
||||
|
||||
**4. 看有沒有現成 workflow 可以直接接**
|
||||
```bash
|
||||
acr list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三層責任分工
|
||||
|
||||
| 層 | 誰做 | 怎麼做 |
|
||||
|---|---|---|
|
||||
| **通用能力** | 平台提供 | `http_request` + auth-recipe 機制=**能打任何 API**,這是地基 |
|
||||
| **熱門服務 recipe** | 平台預鋪 | 減少常見情境的摩擦(現 26 個) |
|
||||
| **冷門/特殊** | **誰用到誰開發** | recipe 是**設定不是程式**,門檻低 |
|
||||
|
||||
> 我們不會包辦全世界所有服務的 API。**用到就自己補一份 recipe**,那是設定檔不是程式碼。
|
||||
|
||||
---
|
||||
|
||||
## 什麼時候才真的需要新零件
|
||||
|
||||
**只有這種情況**:需要**新的原語能力**,而且**無法用既有零件組合出來**。例如——
|
||||
|
||||
- 一種新的控制流(現有 `if_control`/`switch`/`foreach_control`/`filter`/`try_catch` 都表達不了)
|
||||
- 一種新的資料轉換原語(`code` 零件的沙箱做不到)
|
||||
- 需要 WASM 層才能做的事(純計算、特殊編解碼)
|
||||
|
||||
**不算的情況**(這些都用 recipe/workflow 解):
|
||||
- 「我要接 XX 服務的 API」→ `http_request` + recipe
|
||||
- 「我要做 XX 業務邏輯」→ workflow 組合既有零件
|
||||
- 「我要處理某種資料格式」→ `code` 零件(沙箱 JS)
|
||||
|
||||
---
|
||||
|
||||
## 真的要貢獻零件的話
|
||||
|
||||
零件是 WASM(TinyGo/AssemblyScript),有嚴格的沙箱約束
|
||||
(禁網路 syscall、禁檔案系統、禁 goroutine、體積上限 2MB、
|
||||
唯一 I/O 模型是 stdin/stdout JSON)。
|
||||
|
||||
**流程**:走 Arcrun repo 的 PR,過 `docs/component-pr-review-standard.md` 審核。
|
||||
撰寫規範與 contract schema 見 `registry/` 底下的既有零件範例。
|
||||
|
||||
---
|
||||
|
||||
## 為什麼 MCP 不再暴露 `publish_component` / `get_component_guide`
|
||||
|
||||
(2026-07-21 leo 拍板停用)
|
||||
|
||||
那兩個工具對一般使用者是**誤導危機**:搜不到東西時,系統會建議「去提交新零件」,
|
||||
把人推向最難、最該擋的那條路。
|
||||
|
||||
**實測**:總管想寫一支「定期打 API 然後通知」的 workflow(Python 約 10 行),
|
||||
問 `foreach_control` 怎麼用,MCP 回傳的是**TinyGo 寫 WASM 零件的教學**
|
||||
(白名單、syscall 限制、contract schema)——完全是另一件事,導致 40 分鐘未完成。
|
||||
|
||||
**設計判準**(leo):
|
||||
> 前端界面要**人類友善**,Arcrun 要 **AI 友善**——都要**從終點看**。
|
||||
> Arcrun = **讓 AI 輕易建立程式碼**;AI 要覺得 **Arcrun 比 Python 還簡單**,
|
||||
> 因此沒有寫 Python 的慾望。**絕不可迷路、搞不懂。**
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
**讓 AI 用的工作流軟體(目前只支援 Claude Code)**
|
||||
|
||||
> 想先看用它做出來的產品?**[Arcrun RAG](https://github.com/youlinhsieh/arcrun-rag)** —— 企業知識庫(丟檔案自動長出可查詢、可問答的知識庫)。
|
||||
>
|
||||
> 目前**沒有公開試玩站**(早期那個共用示範站已於 2026-08-08 退場)。想直接看產出長什麼樣,
|
||||
> 可以看示範知識庫的公開鏡像 [arcrun-rag-demo-knowledge](https://github.com/youlinhsieh/arcrun-rag-demo-knowledge)——純靜態、免登入。
|
||||
|
||||
AI 很會寫程式,就要除錯,過程浪費很多 Token 及時間,但絕大部分是重複內容,例如登入認證、存取資料庫等。
|
||||
|
||||
既然做的差不多,何不預先寫好?AI 只要把除錯完成的「零件」粘起來,只除錯粘膠程式部分,就省時、省錢。
|
||||
@@ -159,27 +164,21 @@ acr init --self-hosted
|
||||
|
||||
你不需要懂 git、不需要懂 tinygo、不需要手動建任何東西——預編譯好的零件(`.wasm`)直接從 GitHub 下載,用**你自己的** CF token 部署到**你的**帳號。
|
||||
|
||||
**最後一步:身份設定(你自己持有,工具不碰)。** self-hosted 是單租戶——你不需要平台發的 API Key,只需要兩個你自己填的值。在專案建一個 `.env`:
|
||||
**最後一步:身份設定(你自己持有,工具不碰)。** self-hosted 是單租戶——你不需要平台發的 API Key,只需要一個你自己填的值。在專案建一個 `.env`:
|
||||
|
||||
```bash
|
||||
# .env(已被 gitignore;CLI 會自動讀)
|
||||
NAMESPACE=leo # 你的資料分區標籤(明碼即可,這不是密碼)
|
||||
ENCRYPTION_KEY=<64+ hex> # credential 加密金鑰,你自己保管(忘了 = 解不開已上傳的 credential)
|
||||
# 生成 key:node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||
```
|
||||
|
||||
> `NAMESPACE` 只是「你的資料放哪個分區」的標籤,不是密碼——要防別人呼叫你的 webhook,請對 webhook 加保護(見下)。
|
||||
|
||||
把**同一把** `ENCRYPTION_KEY` 也設進你的 worker(runtime 解密要用,CLI 會印確切指令):
|
||||
credential **不需要**你自管加密金鑰:`acr creds push` 走 TLS 把值送進你自己的 worker,由 **Cloudflare Workers Secrets** 託管(連你自己都讀不回,只能覆寫/刪除)。這需要 cypher worker 有一把能打 Workers Scripts secrets API 的 CF token(`acr init` 會印確切指令):
|
||||
|
||||
```bash
|
||||
wrangler secret put ENCRYPTION_KEY --name arcrun-cypher-executor
|
||||
wrangler secret put ENCRYPTION_KEY --name arcrun-auth-static-key
|
||||
wrangler secret put ENCRYPTION_KEY --name arcrun-auth-service-account
|
||||
wrangler secret put CF_SECRETS_API_TOKEN --name arcrun-cypher-executor
|
||||
```
|
||||
|
||||
> 不想自己 put?跑 `acr init` 時明示同意,AI 可代你設——但預設由你自己 put(金鑰是你持有的)。
|
||||
|
||||
完成。之後有新版零件,跑 `acr update` 一樣自動拉新、重部署。
|
||||
|
||||
> 想先不碰 Cloudflare、純在本機感受語法?`acr init --local` 然後直接跳到下面「寫一個工作流」。
|
||||
@@ -314,7 +313,7 @@ acr update self-hosted:拉新版零件/引擎並重新
|
||||
acr update --force 強制重部所有 worker(忽略未變動跳過快取)
|
||||
```
|
||||
|
||||
> 給 AI 操盤手:開始前讀 `.claude/rules/06-mindset.md`(或 arcrun-mindset Skill)——它說明 arcrun 的世界觀(工作流是 default、零件稀有且不自製、一切外部 API 皆 recipe),讓你一開始就走在正路上。
|
||||
> 給 AI 操盤手:開始前讀 [`llms.txt`](llms.txt)——它說明 arcrun 的世界觀(工作流是 default、零件稀有且不自製、一切外部 API 皆 recipe),讓你一開始就走在正路上。跑過 `acr install-harness` 之後,同一套規範會裝進你的專案(arcrun-mindset Skill)。
|
||||
|
||||
---
|
||||
|
||||
@@ -337,7 +336,7 @@ MIT
|
||||
|
||||
arcrun 的核心架構、WASM 零件、CLI 工具鏈與這份文件,由以下貢獻者共同打造:
|
||||
|
||||
- **[@richblack](https://github.com/richblack)** — 創始人,產品設計與架構決策
|
||||
- **[@youlinhsieh](https://github.com/youlinhsieh)** — 創始人,產品設計與架構決策
|
||||
- **Claude(Anthropic)** — 實作夥伴:零件開發、executor 架構、CLI 實作與程式碼審查
|
||||
|
||||
歡迎加入:[CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
想投稿零件?看 [CONTRIBUTING-components.md](CONTRIBUTING-components.md)。
|
||||
|
||||
+49
-25
@@ -1,41 +1,65 @@
|
||||
<!-- arcrun-harness:start -->
|
||||
## 這個專案用 arcrun 做自動化
|
||||
## 這個專案用 Arcrun 做自動化
|
||||
|
||||
當需要「串服務 / 排程 / 打 API / 資料處理自動化」時,用 arcrun 工作流,**不要自己寫 Python/Node 一次性腳本**。arcrun 工作流是純文字、可複用、跑在使用者自己的 Cloudflare。
|
||||
當需要「串服務/排程/打 API/資料處理自動化」時,用 Arcrun 工作流,**不要自己寫 Python/Node 一次性腳本**。
|
||||
Arcrun 工作流是純文字、可複用、跑在使用者自己的 Cloudflare。
|
||||
|
||||
arcrun 只有三種東西,先分清楚就不會做歪:
|
||||
⚠️ **不要上網搜 Arcrun 文件**——網路上沒有,找到的都是錯的。
|
||||
語法與範本全在 **arcrun-mindset** Skill 裡,`acr` 指令查得到現況。
|
||||
|
||||
### 怎麼開始:先寫「意圖」,不要先寫 YAML
|
||||
|
||||
你**不需要先知道有哪些零件**。把需求寫成一串「誰接誰」,丟去查,系統會告訴你哪些存在:
|
||||
|
||||
```
|
||||
input >> ON_SUCCESS >> fetch_rows
|
||||
fetch_rows >> 對每個 row >> notify
|
||||
```
|
||||
|
||||
- 第一個節點固定是 `input`(觸發時帶進來的資料)
|
||||
- **邊有這些**:`ON_SUCCESS`、`對每個 <變數>`(FOREACH)、`ON_TRUE`/`ON_FALSE`(配 `if_control`)、`ON_BRANCH`+`branch:`(配 `switch`/`try_catch`)
|
||||
- **沒有** `ON_FAILURE`——要處理失敗用 `try_catch` + `ON_BRANCH(catch)`。
|
||||
|
||||
完整語法、四份實跑過的範本、節點命名慣例 → 讀 **arcrun-mindset** Skill。
|
||||
|
||||
### Arcrun 只有三種東西,先分清楚就不會做歪
|
||||
|
||||
| 東西 | 是什麼 | 你能做的 |
|
||||
|---|---|---|
|
||||
| **工作流(workflow)** | 把零件/recipe 串起來的純文字流程 | **預設就寫這個**,自由寫 |
|
||||
| **recipe** | 打「一個固定外部 API」的設定(http_request + endpoint/header/body 模板) | 自由寫、**而且該投稿**(見下) |
|
||||
| **零件(component)** | WASM 程式(流程控制 / 資料處理 / http_request / auth),固定一小套 | **你不自製**,由維護者管,走 GitHub PR |
|
||||
| **工作流(workflow)** | 把零件/recipe 串起來的純文字流程 | **預設就寫這個**,自由寫 |
|
||||
| **recipe** | 打「一個固定外部 API」的設定(endpoint/header/body 模板) | 自由寫、**而且該投稿** |
|
||||
| **零件(component)** | WASM 程式(流程控制/資料處理/`http_request`/auth),固定一小套 | **你不自製**,走 PR |
|
||||
|
||||
> **一句話判準**:打一個固定外部 endpoint → 寫 **recipe**;流程控制 / 資料處理 / 通用 HTTP → 用既有**零件**;其他 → 寫**工作流**串起來。
|
||||
> **一句話判準**:打一個固定外部 endpoint → 寫 **recipe**;流程控制/資料處理/通用 HTTP → 用既有**零件**;其他 → 寫**工作流**串起來。
|
||||
>
|
||||
> ⚠️ 常見錯誤:把 `telegram_send`/`gmail_send`/`kbdb_get` 當**零件**寫。它們是 **recipe**。
|
||||
|
||||
### 三個最常踩的坑(讀懂這三條,就不會像前人誤解四輪)
|
||||
### 查詢回 `not_found` 時:兩條路,都不准改寫成 code
|
||||
|
||||
**① recipe 是公共資產,發現缺的就自己做一個投稿,不必問人。**
|
||||
`acr recipe push` = 把 recipe **投稿到公共庫**,不是存私人腳本。公開/私有只是 recipe 的一個屬性(同一身份可有公私版本並存),不是兩條不同的路。
|
||||
→ 所以「想打某個 API 但沒有現成 recipe」時,**正解是自己寫一個 recipe 並 push 上去**(會 inject credential、push 時幫你檢查打不打得通)。這是被鼓勵的,別誤以為要自用、不上傳。
|
||||
查詢會誠實回報(兩庫都查過才點名):
|
||||
|
||||
**② 缺一個能力 → 去補 API(cypher endpoint),不准用 recipe / 多步工作流拼裝出來。**
|
||||
判準口訣:**「這段邏輯換一個介面(CLI→MCP)要不要重寫?要重寫 → 它是『能力』,該長在 API。」**
|
||||
- ❌ 缺 `upsert` → 在工作流裡拼「先查、沒有再建」、或寫個 recipe 假裝補上。
|
||||
- ✅ 缺 `upsert` → 去 API 加一個 `upsert` endpoint,CLI/MCP/recipe 都呼叫它。
|
||||
recipe 只負責「打一個固定外部 API」這件單純事;它不是用來補 arcrun 自己缺的能力的。缺能力就回報 / 補在 API,不要繞。
|
||||
| status | 意思 | 你該做什麼 |
|
||||
|---|---|---|
|
||||
| `found` / `resolved` | 有現成的可用 | **只填 payload** |
|
||||
| `not_found` | 零件庫與 recipe 庫都沒有 | 照回應的 `suggestion` 走(見下兩條路),並看 `similar_components`/`similar_recipes` 有沒有能直接用的 |
|
||||
| `unknown` | 查不到 registry(未部署/網路失敗) | **不代表不存在**,別據此改寫成 code |
|
||||
|
||||
**③ 已經有自製零件(例如 mira 的那幾個)→ 讓它退場,別再加新的。**
|
||||
你不該自製零件;既有的自製零件要往這三條退場:
|
||||
- `claude_api` 之類「工作流回頭叫 LLM」→ **刪掉**,需要 AI 判斷時是**你(操盤的 CC)自己做**,再叫工作流做確定性的下一步。arcrun 是 AI 用的工具,不是工具回頭用 AI。
|
||||
- `kbdb_*` 之類資料存取 → 改走已備好的 **`acr kbdb` 薄殼 / `kbdb_*` MCP 工具**(template + record 模型),不要當零件。
|
||||
- 純粹打某個固定外部 API 的假零件 → **改寫成 recipe** 投稿(見①)。
|
||||
- **缺外部 API** → **自己寫一個 recipe** 並 `acr recipe push`(幾行 YAML,不用部署 Worker、不用寫程式)。
|
||||
recipe 是公共資產,發現缺的就補一個投稿,不必問人。
|
||||
- **缺計算能力**(加解密/壓縮這類純運算) → 投稿**零件 PR**(要人類確認,罕見)。
|
||||
|
||||
🔴 **查不到就改寫成 `code` 節點 =「腹語術」**(表面用 Arcrun、實際全寫 JS)。
|
||||
`code` 只用於**局部整形**(例:剝掉 LLM 回應的雜訊、切段落),不用來取代零件與流程控制。
|
||||
> 實錄:每一個寫進 `code` 的 `if` 都是沒被測過的新 bug;零件的價值是「被測過 1000 次」,寫進 code 就歸零。
|
||||
|
||||
### 其餘鐵律
|
||||
|
||||
- **先查能力再動手**:`acr parts`(看可用零件)、`acr auth-recipe list`(看支援的認證服務)、`acr kbdb`(資料存取)。
|
||||
- **暴露資料要人類同意**:部署對外 webhook / push recipe 會讓東西可被外部呼叫 → 停下來讓使用者明示同意,不替他決定公開。
|
||||
- **誠實**:沒打通就誠實說(缺 credential 標「未驗收:缺 X」),不假裝成功;完成以 HTTP 2xx / trace 為證,不口頭宣布。
|
||||
- **先查能力再動手**:`acr search <關鍵字>`(一次掃零件/recipe/auth-recipe/workflow)、
|
||||
`acr parts`(零件)、`acr recipe list`(recipe)、`acr auth-recipe list`(支援的認證)。
|
||||
- **需要 AI 判斷時你自己做**,不要讓工作流回頭呼叫 LLM。Arcrun 是 AI 用的工具,不是工具回頭用 AI。
|
||||
- **金鑰只拿名字**:定義裡只寫 `{{credential.<名字>}}`,真身絕不寫進 workflow/recipe 檔案。
|
||||
- **暴露資料要人類同意**:`acr push`/`acr recipe push` 會讓東西可被外部呼叫 → 停下來讓使用者明示同意,不替他決定公開。
|
||||
- **誠實**:沒打通就誠實說(缺 credential 標「未驗收:缺 X」),不假裝成功;完成以 HTTP 2xx/trace 為證,不口頭宣布。
|
||||
|
||||
開始前讀 **arcrun-mindset** Skill(世界觀)。使用者技術細節交給你,CLI 操作你來做。
|
||||
開始前讀 **arcrun-mindset** Skill(意圖語法+範本+世界觀)。使用者技術細節交給你,CLI 操作你來做。
|
||||
<!-- arcrun-harness:end -->
|
||||
|
||||
@@ -1,26 +1,60 @@
|
||||
# 用 arcrun 完成這個自動化需求
|
||||
# 用 Arcrun 完成這個自動化需求
|
||||
|
||||
使用者想做一個自動化。你的任務:用 arcrun 做出來,全程不要讓使用者自己寫程式。
|
||||
使用者想做一個自動化。你的任務:用 Arcrun 做出來,全程不要讓使用者自己寫程式。
|
||||
|
||||
⚠️ **不要上網搜 Arcrun 文件**(網路上沒有)。先讀 **arcrun-mindset** Skill,再用 `acr` 指令查現況。
|
||||
|
||||
## 鐵則
|
||||
- **用 arcrun 工作流 / recipe,絕不自己寫 Python/Node 腳本。** 使用者選 arcrun 就是不想要一次性腳本。
|
||||
- 打外部 API → 寫 recipe(`acr recipe push`),不自刻 HTTP client。
|
||||
- 不自製零件(WASM)—— 零件由 arcrun 維護。你能用的是現有零件 + recipe + 工作流。
|
||||
- 需要 AI 判斷時你自己做,不要讓工作流回頭呼叫 LLM。
|
||||
- **用 Arcrun 工作流/recipe,絕不自己寫 Python/Node 腳本。** 使用者選 Arcrun 就是不想要一次性腳本。
|
||||
- **打外部 API → 寫 recipe**(`acr recipe push`),不自刻 HTTP client。缺 recipe 就自己補一個,不必問人。
|
||||
- **不自製零件(WASM)**——零件由 Arcrun 維護。你能用的是現有零件 + recipe + 工作流。
|
||||
- **需要 AI 判斷時你自己做**,不要讓工作流回頭呼叫 LLM。
|
||||
- 🔴 **查不到零件就改寫成 `code` 節點 = 腹語術**,禁止。缺 API 寫 recipe、缺能力投稿零件。
|
||||
|
||||
## 步驟
|
||||
1. 先讀 **arcrun-mindset** Skill(世界觀 + 資源去哪取)。
|
||||
2. 跑 `acr parts` 看零件、`acr auth-recipe list` 看支援的認證。**先查再動手。**
|
||||
3. 把使用者需求拆成工作流(哪些零件、什麼順序、什麼條件),寫成 `.yaml`。
|
||||
4. 需要 credential(API key / token)→ 用 `acr auth-recipe scaffold <service>` 看要哪些,
|
||||
明確告訴使用者去哪取得、怎麼 `acr creds push`。
|
||||
5. `acr validate` 通過後 `acr push` 部署,告訴使用者 webhook URL / 怎麼 `acr run`。
|
||||
6. 完成給客觀證據(HTTP 2xx / trace),不要只說「做好了」。
|
||||
|
||||
## 遇到要暴露資料(對外 webhook)
|
||||
### 1. 先寫「意圖」,不要先寫 YAML
|
||||
把使用者的需求寫成一串「誰接誰」(**不必是真實零件名**,用你想得到的名字即可):
|
||||
|
||||
```
|
||||
input >> ON_SUCCESS >> fetch_rows
|
||||
fetch_rows >> 對每個 row >> notify
|
||||
```
|
||||
|
||||
- 第一個節點固定是 `input`
|
||||
- 邊有 `ON_SUCCESS`、`對每個 <變數>`(FOREACH)、`ON_TRUE`/`ON_FALSE`(配 `if_control`)、`ON_BRANCH`+`branch:`(配 `switch`/`try_catch`);**沒有** `ON_FAILURE`
|
||||
- 需要判斷 → 用條件邊(`if_control` 配 `ON_TRUE`/`ON_FALSE`),不要寫 code 判斷
|
||||
|
||||
語法細節、四份實跑過的範本、節點命名慣例 → **arcrun-mindset** Skill。
|
||||
|
||||
### 2. 丟去查,讓系統告訴你有什麼
|
||||
`acr search <關鍵字>` 一次掃零件/recipe/auth-recipe/workflow;
|
||||
或把意圖串丟 `/cypher/search`,逐節點拿 `found` / `resolved` / `not_found` / `unknown`。
|
||||
|
||||
- `found`/`resolved` → **只填 payload**
|
||||
- `not_found` → 照回應的 `suggestion` 走(缺 API 寫 recipe、缺計算能力投稿零件),
|
||||
並看 `similar_components`/`similar_recipes` 有沒有現成能用的
|
||||
- `unknown` → **不代表不存在**,別據此改寫成 code
|
||||
|
||||
### 3. 把意圖變成 workflow YAML
|
||||
節點填上查到的真實零件/recipe + payload。
|
||||
需要 credential 時:`acr auth-recipe scaffold <service>` 看要哪些,明確告訴使用者去哪取得、怎麼 `acr creds push`。
|
||||
🔑 定義裡只寫 `{{credential.<名字>}}`,**真身絕不寫進檔案**。
|
||||
|
||||
### 4. 驗證 → 部署 → 給證據
|
||||
```bash
|
||||
acr validate <workflow>.yaml # 先驗
|
||||
acr push <workflow>.yaml # 部署(暴露動作,見下)
|
||||
acr run <workflow> # 觸發一次
|
||||
acr logs <workflow> # 看執行紀錄
|
||||
```
|
||||
完成要給客觀證據(HTTP 2xx/trace),不要只說「做好了」。
|
||||
|
||||
## 遇到要暴露資料(對外 webhook/recipe 投稿)
|
||||
停下來,明確告訴使用者「這會讓 X 可被外部呼叫」,要他同意。不要替他決定公開。
|
||||
非互動環境下把完整指令印給使用者自己貼上跑。
|
||||
|
||||
## 還沒設定好 arcrun?
|
||||
## 還沒設定好 Arcrun?
|
||||
若 `acr` 指令不存在或還沒 `acr init`:先帶使用者完成前置設定
|
||||
(裝 CLI → 拿 Cloudflare 帳號的兩串憑證 → `acr init --self-hosted`)。
|
||||
拿 Cloudflare 憑證時用白話照抄式引導,不要對使用者講 KV / Worker / R2 等術語。
|
||||
|
||||
@@ -66,7 +66,7 @@ if echo "$CMD" | grep -qE "acr (push|recipe push)\b"; then
|
||||
if echo "$EXEC_PART" | grep -qE "(^|[;&|][[:space:]]*)acr[[:space:]]+(push|recipe[[:space:]]+push)\b"; then
|
||||
if [ ! -t 0 ] && [ "${ARCRUN_HUMAN_CONFIRMED:-}" != "1" ]; then
|
||||
block "在非互動環境自動執行暴露動作(acr push / recipe push 會讓東西可被外部呼叫)" \
|
||||
"交人類在終端機執行(真 TTY 會自動放行)。可把指令完整複製給使用者貼上自己跑:\`acr push <你的 workflow.yaml>\`。或使用者先在對話明示同意後親自於終端機執行。不要替使用者決定公開。"
|
||||
"交人類在終端機執行(真 TTY 會自動放行)。可把指令完整複製給使用者貼上自己跑:\`acr push <你的 workflow.yaml>\`。或使用者先在對話明示同意後親自於終端機執行。不要替使用者決定公開。(部署前的正路見 arcrun-mindset Skill:先 \`acr validate\`)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -76,15 +76,29 @@ fi
|
||||
if echo "$CMD" | grep -qE "(^|[;&| ])(python3?|node)[ ]+[^ ]+\.(py|js|mjs|ts)\b"; then
|
||||
# 排除明顯的測試 / 既有工具呼叫(pytest / npm test / jest 等)降低誤判
|
||||
if ! echo "$CMD" | grep -qE "(pytest|jest|vitest|npm (run )?test|mocha|\btest_)"; then
|
||||
remind "偵測到用 python/node 跑腳本。這專案用 arcrun,串服務/自動化不要自刻一次性腳本。" \
|
||||
"先跑 \`acr parts\` 看有哪些零件,把需求寫成 workflow.yaml 用 \`acr run\`。若這確實不是自動化(例如跑測試/別的工具),忽略本提醒。"
|
||||
remind "偵測到用 python/node 跑腳本。這專案用 Arcrun,串服務/自動化不要自刻一次性腳本。" \
|
||||
"讀 arcrun-mindset Skill,先把需求寫成「意圖」串(\`input >> ON_SUCCESS >> <下一步>\`,邊只有 ON_SUCCESS 與「對每個 X」),再用 \`acr search <關鍵字>\` 查哪些零件/recipe 存在,最後才寫 workflow.yaml → \`acr validate\` → \`acr run\`。若這確實不是自動化(例如跑測試/別的工具),忽略本提醒。"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 提醒(不硬擋):自寫打固定 API 的 script,而非 recipe ──────────────
|
||||
if echo "$CMD" | grep -qE "(curl|fetch|requests\.(get|post)|axios).*https?://"; then
|
||||
remind "偵測到自己打外部 API。arcrun 裡「打固定 endpoint」應寫成 recipe,不自刻 HTTP 呼叫。" \
|
||||
"用 \`acr recipe push\` 把這個 API 包成 recipe,workflow 裡用 component 引用它。見 arcrun-mindset Skill。"
|
||||
remind "偵測到自己打外部 API。Arcrun 裡「打固定 endpoint」應寫成 recipe,不自刻 HTTP 呼叫。" \
|
||||
"先 \`acr recipe search <服務名>\` 看有沒有現成的;沒有就自己寫幾行 YAML(canonical_id/endpoint/method/auth_service)用 \`acr recipe push\` 投稿,workflow 裡用 \`http_request\` + 該 recipe 引用它。缺 recipe 就自己補,不必問人。寫法見 arcrun-mindset Skill。"
|
||||
fi
|
||||
|
||||
# ── 提醒(不硬擋):把 code 節點當成缺零件的替代品(「腹語術」)──────────────
|
||||
# 查詢回 not_found 就改寫成 code = 表面用 Arcrun、實際全寫 JS。這是現世代最常見的走歪。
|
||||
if [ "$TOOL" = "Write" ] || [ "$TOOL" = "Edit" ] || [ "$TOOL" = "MultiEdit" ]; then
|
||||
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
|
||||
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string // ""')
|
||||
if echo "$FILE" | grep -qE '\.(ya?ml)$' && echo "$CONTENT" | grep -qE 'component:[[:space:]]*["'"'"']?code\b'; then
|
||||
# 只在 code 內容看起來在做流程控制/取代零件時提醒(含 if/for/fetch),單純整形不吵
|
||||
if echo "$CONTENT" | grep -qE '\b(if[[:space:]]*\(|for[[:space:]]*\(|fetch\(|await[[:space:]]+fetch)'; then
|
||||
remind "workflow 裡的 \`code\` 節點含流程控制/HTTP 呼叫——這可能是「腹語術」(表面用 Arcrun、實際全寫 JS)。" \
|
||||
"\`code\` 只用於局部整形(例:剝掉 LLM 回應的雜訊、切段落)。缺外部 API → 寫 recipe(\`acr recipe push\`);缺計算能力 → 投稿零件 PR;要判斷 → 用條件邊(\`if_control\` 配 \`ON_TRUE\`/\`ON_FALSE\`,或 \`switch\`/\`try_catch\` 配 \`ON_BRANCH\`),不要寫 code 判斷。每個寫進 code 的 if 都是沒被測過的新 bug。見 arcrun-mindset Skill。"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -10,6 +10,16 @@
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Write|Edit|MultiEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": ".claude/hooks/arcrun-guard.sh",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,78 +1,285 @@
|
||||
---
|
||||
name: arcrun-mindset
|
||||
description: >-
|
||||
arcrun 的世界觀 — 用 arcrun 開發自動化時的預設心態 + 資源去哪取。當你(AI 操盤手)要在
|
||||
arcrun 上做任何事(串服務、處理資料、認證、把東西開放給人用)前讀這個。它讓你做出「方向對」
|
||||
的選擇、知道資源在哪,避免技術上能跑但架構上錯、或自己重刻 arcrun 已有的東西。
|
||||
在 Arcrun 上做任何事時使用(用戶說「幫我用 Arcrun 做 X」「用 arcrun 寫一個工作流」「把 X 自動化」)。
|
||||
Arcrun 是跑在 Cloudflare 上的工作流引擎——你用 `>>` 寫「意圖」,系統告訴你有哪些現成零件與 recipe,
|
||||
你只填 payload,不必自己寫程式。**不要上網搜 Arcrun 文件**(網路上沒有),也不要自己猜 YAML 格式:
|
||||
先讀本 skill,再用 `acr` 指令(或 MCP 工具)查現成零件。
|
||||
涵蓋:意圖工作流語法、四份實跑過的範本、零件 vs recipe 的分別、缺件的兩條路、已知的坑。
|
||||
---
|
||||
|
||||
# arcrun mindset(給 AI 操盤手)
|
||||
# Arcrun:怎麼寫意圖工作流
|
||||
|
||||
你在 arcrun 上幫使用者開發自動化。arcrun 很簡單,簡單到你常會把它想複雜、或退回自己熟悉的
|
||||
Python/Node 自刻。這份幫你在岔路上選對方向,並告訴你資源在哪。
|
||||
> **你已經配備 Arcrun**(此專案裝了 `acr` CLI,可能另有 `arcrun_*` MCP 工具)。
|
||||
> **別上網找文件**——網路上沒有 Arcrun 的文件,找到的都是錯的。答案都在本 skill 與 `acr` 指令裡。
|
||||
|
||||
## 先做這三件(照順序)
|
||||
|
||||
1. `acr whoami` — 確認連到哪個帳號(**勿自行 curl 猜帳號 URL**)
|
||||
2. 讀本 skill 下面的語法與範本 → 寫出 `>>` 意圖
|
||||
3. `acr parts`/`acr recipe list`(或 `acr search <關鍵字>` 一次掃全部)— 確認零件與 recipe 真的存在
|
||||
|
||||
**卡住時**:`acr search <關鍵字>` 跨類搜尋;有 MCP 就 `arcrun_get_skill('INDEX')` 拿全館導航。
|
||||
|
||||
---
|
||||
|
||||
## 0. 一句話世界觀
|
||||
|
||||
**arcrun 裡幾乎所有東西都是工作流(workflow)。** 工作流 = 一張紙,寫「用哪些零件、什麼順序、什麼條件」。
|
||||
你大部分時間在寫紙、改紙,不是在造新零件、也不是自己寫腳本。
|
||||
**Arcrun 裡幾乎所有東西都是工作流(workflow)。** 工作流 = 一張紙,寫「用哪些零件、什麼順序、什麼條件」。
|
||||
你大部分時間在**寫紙、改紙**,不是在造新零件、也不是自己寫腳本。
|
||||
|
||||
**Arcrun 只有三種東西,先分清楚就不會做歪:**
|
||||
|
||||
| 東西 | 是什麼 | 你能做的 |
|
||||
|---|---|---|
|
||||
| **工作流(workflow)** | 把零件/recipe 串起來的純文字流程 | **預設就寫這個**,自由寫 |
|
||||
| **recipe** | 打「一個固定外部 API」的設定(endpoint/header/body 模板) | 自由寫、**而且該投稿**(缺就自己補) |
|
||||
| **零件(component)** | WASM 程式(流程控制/資料處理/`http_request`/auth),固定一小套 | **你不自製**,走 PR 由維護者管 |
|
||||
|
||||
> **一句話判準**:打一個固定外部 endpoint → 寫 **recipe**;流程控制/資料處理/通用 HTTP → 用既有**零件**;其他 → 寫**工作流**串起來。
|
||||
|
||||
---
|
||||
|
||||
## 1. 工作流是 default,不要退回自己寫 Python
|
||||
<!-- 以下正文由 registry/skills/write_intent_workflow.md 於建置期複製而來(單一真相源)。
|
||||
不要直接編輯本段——改 registry 那份,然後跑 `npm run build:harness`。 -->
|
||||
|
||||
使用者選 arcrun,就是不要「每次重刻、跑完即丟」的腳本。所以你的預設順序:
|
||||
## 1. 意圖工作流的語法
|
||||
|
||||
1. **先想能不能用工作流做**(串現有零件 / recipe + 流程控制)。99% 可以。
|
||||
2. 要打的服務有 HTTP API、但沒有對應 recipe → **寫一個 recipe**(http_request + 固定設定 YAML,不用部署、不用審核)。
|
||||
3. **只有**封閉純邏輯(流程控制 / 資料處理)、現有零件不夠、且值得全 arcrun 重用 → 才考慮零件(而零件走 PR,不是你現在做)。
|
||||
一串「誰接誰」,每行一個關係:
|
||||
|
||||
> 典型走歪:「我先用 Python 測一下」。停。使用者要的是 arcrun 工作流。先 `acr parts` 看有什麼,用工作流串。
|
||||
```
|
||||
<節點A> >> <邊> >> <節點B>
|
||||
```
|
||||
|
||||
## 2. 資源去哪取(不要自己重造 arcrun 已有的)
|
||||
- **節點**=一個步驟。用你想得到的名字(中文可以),**不必是真實零件名**
|
||||
- **邊**=什麼情況下往下走
|
||||
|
||||
## 2. 邊有這些
|
||||
|
||||
| 邊 | 意思 | 真例 |
|
||||
|---|---|---|
|
||||
| `ON_SUCCESS` | 上一步成功就往下 | `input >> ON_SUCCESS >> prep` |
|
||||
| `對每個 <變數>` | 上一步產出清單,逐項處理(FOREACH)| `parse_card >> 對每個 block >> post_block` |
|
||||
| `ON_TRUE` / `ON_FALSE` | 條件成立/不成立各走一條(配 `if_control`)| `判斷有沒有新資料 >> ON_TRUE >> 傳到 telegram` |
|
||||
| `ON_BRANCH`+`branch:` | 依標籤選路(配 `switch` 每個 case、`try_catch` 的 try/catch)| `my_switch >> ON_BRANCH(branch_active) >> 處理啟用` |
|
||||
|
||||
### 2.1 條件分支怎麼寫(2026-08-01 起引擎支援)
|
||||
|
||||
**需要判斷時,用分支邊,不要寫 `code` 判斷。**
|
||||
三顆流程控制零件都輸出 `data.branch` 標籤,引擎依標籤選路:
|
||||
|
||||
| 零件 | 輸出的標籤 | 接法 |
|
||||
|---|---|---|
|
||||
| `if_control` | `"true"` / `"false"` | `ON_TRUE`/`ON_FALSE` 各一條 |
|
||||
| `switch` | 你在 `cases[].branch` 取的名字(沒中則 `default_branch`)| 每條路一條 `ON_BRANCH`,邊上標 `branch` |
|
||||
| `try_catch` | `"try"`(沒錯)/`"catch"`(有錯)| 兩條 `ON_BRANCH`,標 `try` 與 `catch` |
|
||||
|
||||
```
|
||||
判斷有沒有新資料 >> ON_TRUE >> 傳到 telegram
|
||||
判斷有沒有新資料 >> ON_FALSE >> 結束
|
||||
```
|
||||
中文語意詞亦可:「成立時」=`ON_TRUE`、「否則」=`ON_FALSE`。
|
||||
|
||||
💡 **不必背**:查零件時回應會附 `branch_hint`(有哪些標籤、用哪些邊型、可照抄的範例),
|
||||
照著接就對了。
|
||||
|
||||
⚠️ 仍然**不要寫 `ON_FAILURE`**(沒有這種邊;要處理失敗用 `try_catch` + `ON_BRANCH(catch)`)。
|
||||
|
||||
### 2.2 怎麼確認分支真的走對了(**別看不懂就以為壞掉**)
|
||||
|
||||
分支工作流「有沒有成功」看兩件事,**不是看某條沒走的路沒有輸出**:
|
||||
|
||||
1. **`verdict`**:`GET /workflows/<name>/executions?limit=1`
|
||||
→ `data.executions[0].verdict === "success"` 就是成功了。
|
||||
2. **`trace` 裡有沒有出現該走的節點**:走 TRUE 路時 FALSE 路的節點**本來就不該出現**
|
||||
——**那是正確行為,不是失敗**。
|
||||
|
||||
```
|
||||
# 條件成立 → 只有 true 那條的節點在 trace
|
||||
{"amount": 5000} → if_control 回 branch="true" → 走 ON_TRUE 那條
|
||||
{"amount": 100} → if_control 回 branch="false" → 走 ON_FALSE 那條
|
||||
```
|
||||
|
||||
🔴 **實撞(2026-08-01 考試)**:有考生的分支工作流**其實完全正常**
|
||||
(`amount=5000`→true、`amount=100`→false 都對),但它以為「跑不通」而放棄改寫成 code。
|
||||
**看到只有一條路有輸出=分支正在正確運作**,不要因此判定失敗。
|
||||
|
||||
## 3. 第一個節點固定是 `input`
|
||||
|
||||
所有真範本都以 `input` 起頭——那是「觸發時帶進來的資料」。
|
||||
|
||||
---
|
||||
|
||||
## 4. 真範本(照抄結構、改內容)
|
||||
|
||||
> 以下四份**全部是實際部署且 `verdict=success` 的 workflow**,不是簡化示範。
|
||||
> 用 `acr logs <name>`(有 MCP 則 `arcrun_get_workflow(<name>)`) 可以拿完整定義。
|
||||
|
||||
### A. 最短:取資料 → 處理 (`graph_neighbors`)
|
||||
```
|
||||
input >> ON_SUCCESS >> fetch_triplets
|
||||
fetch_triplets >> ON_SUCCESS >> bfs_neighbors
|
||||
```
|
||||
|
||||
### B. 長鏈:多次查詢 → 組裝 → 問 AI → 收尾 (`rag_chat`)
|
||||
```
|
||||
input >> ON_SUCCESS >> prep
|
||||
prep >> ON_SUCCESS >> kw_search
|
||||
kw_search >> ON_SUCCESS >> sem_search
|
||||
sem_search >> ON_SUCCESS >> fetch_triplets
|
||||
fetch_triplets >> ON_SUCCESS >> fetch_blocks_a
|
||||
fetch_blocks_a >> ON_SUCCESS >> assemble
|
||||
assemble >> ON_SUCCESS >> ask_llm
|
||||
ask_llm >> ON_SUCCESS >> finalize
|
||||
```
|
||||
`prep` 前處理/`assemble` 組 prompt/`finalize` 收拾回應——三個常見的整形節點。
|
||||
|
||||
### C. 一節點分岔兩條 FOREACH (`rag_ingest_card`)
|
||||
```
|
||||
input >> ON_SUCCESS >> parse_card
|
||||
parse_card >> 對每個 block >> post_block
|
||||
parse_card >> 對每個 rel >> post_triplet
|
||||
```
|
||||
同一節點可有多條出邊,各自處理不同清單。
|
||||
|
||||
### D. 混合:直線 + 兩段 FOREACH (`rag_takedown_direct`)
|
||||
```
|
||||
input >> ON_SUCCESS >> prep
|
||||
prep >> ON_SUCCESS >> list_dead_blocks
|
||||
list_dead_blocks >> ON_SUCCESS >> build_deprecations
|
||||
build_deprecations >> 對每個 dead_entry >> deprecate_entry
|
||||
build_deprecations >> ON_SUCCESS >> list_triplets
|
||||
list_triplets >> ON_SUCCESS >> pick_dead_triplets
|
||||
pick_dead_triplets >> 對每個 dead_record >> deprecate_triplet
|
||||
```
|
||||
`build_deprecations` 同時有 FOREACH 出邊與 `ON_SUCCESS` 出邊——
|
||||
前者處理清單、後者繼續主線。
|
||||
|
||||
---
|
||||
|
||||
## 5. 節點怎麼命名(照真範本的模式,查詢較容易媒合)
|
||||
|
||||
| 意圖 | 模式 | 真例 |
|
||||
|---|---|---|
|
||||
| 前處理/正規化 | `prep` | `rag_chat.prep` |
|
||||
| 取一批資料 | `fetch_*`/`list_*` | `fetch_triplets`/`list_dead_blocks` |
|
||||
| 搜尋 | `*_search` | `kw_search`/`sem_search` |
|
||||
| 解析/切塊 | `parse_*` | `parse_card` |
|
||||
| 寫入 | `post_*` | `post_block`/`post_triplet` |
|
||||
| 組裝 | `assemble`/`build_*` | `assemble`/`build_deprecations` |
|
||||
| 問 AI | `ask_llm` | `rag_chat.ask_llm` |
|
||||
| 收尾整形 | `finalize` | `rag_chat.finalize` |
|
||||
|
||||
---
|
||||
|
||||
## 6. 寫完一定要查(**不要直接部署**)
|
||||
|
||||
```bash
|
||||
curl -s -X POST https://arcrun-cypher-executor.<subdomain>.workers.dev/cypher/search \
|
||||
-H 'content-type: application/json' -H 'X-Arcrun-API-Key: <namespace>' \
|
||||
-d '{"triplets":["input >> ON_SUCCESS >> fetch_data","fetch_data >> ON_SUCCESS >> notify"]}'
|
||||
```
|
||||
|
||||
回應的每個節點會有:
|
||||
|
||||
| status | 意思 | 你該做什麼 |
|
||||
|---|---|---|
|
||||
| `found` | 有這個節點。`source: component` 附 `input_schema`(怎麼填 payload)與 `success_rate`;`source: recipe` 附 description/endpoint | **只填 payload** |
|
||||
| `not_found` | **兩庫(零件 registry+recipe 庫)都查過,確定沒有** | 照 `suggestion` 欄走:缺 API → 寫 recipe(skill `write_recipe`);缺計算能力 → 投稿零件 PR(skill `add_new_wasm_component`)。`similar_components`/`similar_recipes` 是相近候選——先看有沒有現成的能直接用 |
|
||||
| `unknown` | 查不到 registry | **不代表不存在**,別據此改寫成 code |
|
||||
|
||||
> 註(2026-07-31):`/cypher/search` 曾對任何節點名都回假 `found`,已修為真查兩庫。
|
||||
> 舊實例(未更新部署)仍可能假 found——status 可信度以該實例部署版本為準。
|
||||
|
||||
---
|
||||
|
||||
## 7. 常犯的錯
|
||||
|
||||
1. **用不存在的邊**(`ON_FAILURE`)→ 沒有這種邊;要處理失敗用 `try_catch` + `ON_BRANCH(catch)`
|
||||
⚠️ `ON_TRUE`/`ON_FALSE`/`ON_BRANCH` **是存在的**(2026-08-01 起),見 §2.1——
|
||||
本行以前寫「ON_TRUE 不存在」是舊世代,已更正
|
||||
2. **第一個節點不是 `input`**
|
||||
3. **把 recipe 當零件寫**——`telegram_send`/`gmail`/`kbdb_get` 是 **recipe** 不是零件
|
||||
→ 寫成 `http_request` + 該 recipe
|
||||
4. 🔴 **查詢回 `not_found` 就改寫成 `code` 節點**
|
||||
→ 那叫「腹語術」(表面用 Arcrun、實際全寫 JS)。正解:缺 API 寫 recipe、缺能力投稿零件。
|
||||
`code` 只用在**局部整形**(例:剝掉 LLM 回應的雜訊),不用來取代零件與流程控制。
|
||||
|
||||
---
|
||||
|
||||
## 8. 相關
|
||||
|
||||
- 完整版指引與十題考卷(含 haiku 實測 10/10):
|
||||
頂層 repo `system-dev/docs/3-specs/arcrun-usable/`
|
||||
- 下一步該讀哪支 skill(需 MCP):`arcrun_list_skills()`
|
||||
- 定期掃資料 → `build_watcher_workflow`
|
||||
- RAG 檢索問答 → `rag_with_arcrun`
|
||||
- workflow 卡住不動 → `debug_paused_workflow`
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 9. 資源去哪取(不要自己重造 Arcrun 已有的)
|
||||
|
||||
| 你想知道 | 跑這個 |
|
||||
|---|---|
|
||||
| 有哪些零件可用 | `acr parts` |
|
||||
| 某零件的設定範本 | `acr parts scaffold <name>` |
|
||||
| 有哪些 recipe | `acr recipe list`/`acr recipe search <關鍵字>` |
|
||||
| 支援哪些服務的認證 | `acr auth-recipe list` |
|
||||
| 某服務認證要哪些 credential + 範例 | `acr auth-recipe scaffold <service>` |
|
||||
| 已上傳的 recipe | `acr recipe list` |
|
||||
| 某服務認證要哪些 credential + 範例 | `acr auth-recipe scaffold <service>` |
|
||||
| **一次掃全部**(零件/recipe/auth-recipe/workflow) | `acr search <關鍵字>` |
|
||||
| 已部署的 workflow | `acr list` |
|
||||
| 某次執行為什麼失敗 | `acr logs <workflow>` |
|
||||
| 工作流語法、指令 | `acr --help` |
|
||||
|
||||
**先查再動手**——arcrun 多半已經有你要的零件 / recipe / 認證,不要自刻。
|
||||
**先查再動手**——Arcrun 多半已經有你要的零件/recipe/認證,不要自刻。
|
||||
|
||||
## 3. arcrun 是你(AI)用的工具,不是工具回頭呼叫 AI
|
||||
## 10. 做出來以後:驗證 → 部署
|
||||
|
||||
需要智慧判斷 / 自然語言轉換時,**你自己做**,再呼叫工作流執行確定性的下一步。
|
||||
**不要在工作流中間放零件回頭呼叫 LLM**。arcrun 的大腦就是操盤的你。
|
||||
```bash
|
||||
acr validate <workflow>.yaml # 先驗,別直接部署
|
||||
acr push <workflow>.yaml # 部署(暴露動作,見 §12)
|
||||
acr run <workflow> # 觸發一次,看實際結果
|
||||
acr logs <workflow> # 看執行紀錄/失敗原因
|
||||
```
|
||||
|
||||
## 4. arcrun 不替你做授權判斷
|
||||
需要 credential(API key/token)時:`acr auth-recipe scaffold <service>` 看要哪些,
|
||||
明確告訴使用者去哪取得、怎麼 `acr creds push`。
|
||||
🔑 **金鑰只拿名字**:workflow/recipe 裡只寫 `{{credential.<名字>}}`,
|
||||
**真身絕不寫進定義檔**(執行前才由系統回填)。
|
||||
|
||||
API 打不打得通由發 key 的服務決定。401/403 是對方服務在行使授權,**不是 arcrun 的 bug、不是你做錯**。
|
||||
不要在 arcrun 裡建「允許/禁止某 endpoint」的二次授權清單。
|
||||
## 11. Arcrun 是你(AI)用的工具,不是工具回頭呼叫 AI
|
||||
|
||||
## 5. 把東西開放給別人用 = 要使用者明示同意
|
||||
需要智慧判斷/自然語言轉換時,**你自己做**,再呼叫工作流執行確定性的下一步。
|
||||
**不要在工作流中間放零件回頭呼叫 LLM**——Arcrun 的大腦就是操盤的你。
|
||||
(唯一例外:`ask_llm` 這種「內容生成本身就是流程的一步」,見範本 B。)
|
||||
|
||||
部署對外 webhook、push recipe 會讓資料/能力**可被外部呼叫**(暴露面):
|
||||
## 12. 把東西開放給別人用 = 要使用者明示同意
|
||||
|
||||
`acr push`(部署 workflow)與 `acr recipe push`(投稿 recipe)會讓資料/能力**可被外部呼叫**:
|
||||
- 停下來,明確告訴使用者「這會讓 X 可被外部呼叫」,要他同意。**不替他決定公開。**
|
||||
- 非互動環境(你直跑)遇到 → 停,要人類確認,絕不自己塞 confirm 假裝同意。
|
||||
- arcrun 可提供保護(要求呼叫者帶 key / 限流)——提醒使用者。
|
||||
- 非互動環境(你直跑)遇到 → 停,把完整指令印給使用者自己貼上跑,絕不自己塞 confirm 假裝同意。
|
||||
- Arcrun 可提供保護(要求呼叫者帶 key/限流)——提醒使用者。
|
||||
|
||||
## 6. 誠實(最重要)
|
||||
## 13. Arcrun 不替你做授權判斷
|
||||
|
||||
API 打不打得通由發 key 的服務決定。401/403 是對方服務在行使授權,**不是 Arcrun 的 bug、不是你做錯**。
|
||||
不要在 Arcrun 裡建「允許/禁止某 endpoint」的二次授權清單。
|
||||
|
||||
## 14. 誠實(最重要)
|
||||
|
||||
- **不假綠**:沒打通就誠實說。缺 credential 打不到 2xx → 標「未驗收:缺 X」,不 mock 充綠燈。
|
||||
- **不假裝防偽 / 不代替人類確認**有風險的動作(暴露資料)。
|
||||
- **完成 = 客觀證據**(HTTP 2xx + trace),不是口頭「做好了」。
|
||||
- **不假裝防偽/不代替人類確認**有風險的動作(暴露資料)。
|
||||
- **完成 = 客觀證據**(HTTP 2xx + trace),不是口頭「做好了」。
|
||||
|
||||
---
|
||||
|
||||
## 怎麼用這份 mindset
|
||||
## 動手前的自檢清單
|
||||
|
||||
每次準備動手,先過一遍:
|
||||
1. 這能用工作流 / recipe 做嗎?(多半能 → 別自己寫 Python、別造零件)
|
||||
2. 我查過 `acr parts` / `acr auth-recipe` 了嗎?(arcrun 可能已有)
|
||||
3. 我是不是讓工作流回頭呼叫 AI?(是 → 改成我自己做)
|
||||
4. 這動作會把資料開放給別人嗎?(會 → 要使用者明示同意)
|
||||
5. 我有沒有假裝(假綠 / 假防偽 / 代替人類確認)?(有 → 停,誠實標明)
|
||||
1. 我把意圖寫成 `>>` 串了嗎?(還是直接跳去寫 YAML/寫程式)
|
||||
2. 我查過 `acr search` / `acr parts` / `acr recipe list` 了嗎?
|
||||
3. 查詢回 `not_found` 時,我走的是 recipe/零件 PR 兩條路,**還是偷偷改寫成 `code`**?(後者=腹語術)
|
||||
4. 我是不是讓工作流回頭呼叫 AI 做判斷?(是 → 改成我自己做)
|
||||
5. 這動作會把資料開放給別人嗎?(會 → 要使用者明示同意)
|
||||
6. 我有沒有假裝(假綠/假防偽/代替人類確認)?(有 → 停,誠實標明)
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: arcrun-mindset
|
||||
description: >-
|
||||
在 Arcrun 上做任何事時使用(用戶說「幫我用 Arcrun 做 X」「用 arcrun 寫一個工作流」「把 X 自動化」)。
|
||||
Arcrun 是跑在 Cloudflare 上的工作流引擎——你用 `>>` 寫「意圖」,系統告訴你有哪些現成零件與 recipe,
|
||||
你只填 payload,不必自己寫程式。**不要上網搜 Arcrun 文件**(網路上沒有),也不要自己猜 YAML 格式:
|
||||
先讀本 skill,再用 `acr` 指令(或 MCP 工具)查現成零件。
|
||||
涵蓋:意圖工作流語法、四份實跑過的範本、零件 vs recipe 的分別、缺件的兩條路、已知的坑。
|
||||
---
|
||||
|
||||
# Arcrun:怎麼寫意圖工作流
|
||||
|
||||
> **你已經配備 Arcrun**(此專案裝了 `acr` CLI,可能另有 `arcrun_*` MCP 工具)。
|
||||
> **別上網找文件**——網路上沒有 Arcrun 的文件,找到的都是錯的。答案都在本 skill 與 `acr` 指令裡。
|
||||
|
||||
## 先做這三件(照順序)
|
||||
|
||||
1. `acr whoami` — 確認連到哪個帳號(**勿自行 curl 猜帳號 URL**)
|
||||
2. 讀本 skill 下面的語法與範本 → 寫出 `>>` 意圖
|
||||
3. `acr parts`/`acr recipe list`(或 `acr search <關鍵字>` 一次掃全部)— 確認零件與 recipe 真的存在
|
||||
|
||||
**卡住時**:`acr search <關鍵字>` 跨類搜尋;有 MCP 就 `arcrun_get_skill('INDEX')` 拿全館導航。
|
||||
|
||||
---
|
||||
|
||||
## 0. 一句話世界觀
|
||||
|
||||
**Arcrun 裡幾乎所有東西都是工作流(workflow)。** 工作流 = 一張紙,寫「用哪些零件、什麼順序、什麼條件」。
|
||||
你大部分時間在**寫紙、改紙**,不是在造新零件、也不是自己寫腳本。
|
||||
|
||||
**Arcrun 只有三種東西,先分清楚就不會做歪:**
|
||||
|
||||
| 東西 | 是什麼 | 你能做的 |
|
||||
|---|---|---|
|
||||
| **工作流(workflow)** | 把零件/recipe 串起來的純文字流程 | **預設就寫這個**,自由寫 |
|
||||
| **recipe** | 打「一個固定外部 API」的設定(endpoint/header/body 模板) | 自由寫、**而且該投稿**(缺就自己補) |
|
||||
| **零件(component)** | WASM 程式(流程控制/資料處理/`http_request`/auth),固定一小套 | **你不自製**,走 PR 由維護者管 |
|
||||
|
||||
> **一句話判準**:打一個固定外部 endpoint → 寫 **recipe**;流程控制/資料處理/通用 HTTP → 用既有**零件**;其他 → 寫**工作流**串起來。
|
||||
|
||||
---
|
||||
@@ -0,0 +1,67 @@
|
||||
|
||||
---
|
||||
|
||||
## 9. 資源去哪取(不要自己重造 Arcrun 已有的)
|
||||
|
||||
| 你想知道 | 跑這個 |
|
||||
|---|---|
|
||||
| 有哪些零件可用 | `acr parts` |
|
||||
| 某零件的設定範本 | `acr parts scaffold <name>` |
|
||||
| 有哪些 recipe | `acr recipe list`/`acr recipe search <關鍵字>` |
|
||||
| 支援哪些服務的認證 | `acr auth-recipe list` |
|
||||
| 某服務認證要哪些 credential + 範例 | `acr auth-recipe scaffold <service>` |
|
||||
| **一次掃全部**(零件/recipe/auth-recipe/workflow) | `acr search <關鍵字>` |
|
||||
| 已部署的 workflow | `acr list` |
|
||||
| 某次執行為什麼失敗 | `acr logs <workflow>` |
|
||||
| 工作流語法、指令 | `acr --help` |
|
||||
|
||||
**先查再動手**——Arcrun 多半已經有你要的零件/recipe/認證,不要自刻。
|
||||
|
||||
## 10. 做出來以後:驗證 → 部署
|
||||
|
||||
```bash
|
||||
acr validate <workflow>.yaml # 先驗,別直接部署
|
||||
acr push <workflow>.yaml # 部署(暴露動作,見 §12)
|
||||
acr run <workflow> # 觸發一次,看實際結果
|
||||
acr logs <workflow> # 看執行紀錄/失敗原因
|
||||
```
|
||||
|
||||
需要 credential(API key/token)時:`acr auth-recipe scaffold <service>` 看要哪些,
|
||||
明確告訴使用者去哪取得、怎麼 `acr creds push`。
|
||||
🔑 **金鑰只拿名字**:workflow/recipe 裡只寫 `{{credential.<名字>}}`,
|
||||
**真身絕不寫進定義檔**(執行前才由系統回填)。
|
||||
|
||||
## 11. Arcrun 是你(AI)用的工具,不是工具回頭呼叫 AI
|
||||
|
||||
需要智慧判斷/自然語言轉換時,**你自己做**,再呼叫工作流執行確定性的下一步。
|
||||
**不要在工作流中間放零件回頭呼叫 LLM**——Arcrun 的大腦就是操盤的你。
|
||||
(唯一例外:`ask_llm` 這種「內容生成本身就是流程的一步」,見範本 B。)
|
||||
|
||||
## 12. 把東西開放給別人用 = 要使用者明示同意
|
||||
|
||||
`acr push`(部署 workflow)與 `acr recipe push`(投稿 recipe)會讓資料/能力**可被外部呼叫**:
|
||||
- 停下來,明確告訴使用者「這會讓 X 可被外部呼叫」,要他同意。**不替他決定公開。**
|
||||
- 非互動環境(你直跑)遇到 → 停,把完整指令印給使用者自己貼上跑,絕不自己塞 confirm 假裝同意。
|
||||
- Arcrun 可提供保護(要求呼叫者帶 key/限流)——提醒使用者。
|
||||
|
||||
## 13. Arcrun 不替你做授權判斷
|
||||
|
||||
API 打不打得通由發 key 的服務決定。401/403 是對方服務在行使授權,**不是 Arcrun 的 bug、不是你做錯**。
|
||||
不要在 Arcrun 裡建「允許/禁止某 endpoint」的二次授權清單。
|
||||
|
||||
## 14. 誠實(最重要)
|
||||
|
||||
- **不假綠**:沒打通就誠實說。缺 credential 打不到 2xx → 標「未驗收:缺 X」,不 mock 充綠燈。
|
||||
- **不假裝防偽/不代替人類確認**有風險的動作(暴露資料)。
|
||||
- **完成 = 客觀證據**(HTTP 2xx + trace),不是口頭「做好了」。
|
||||
|
||||
---
|
||||
|
||||
## 動手前的自檢清單
|
||||
|
||||
1. 我把意圖寫成 `>>` 串了嗎?(還是直接跳去寫 YAML/寫程式)
|
||||
2. 我查過 `acr search` / `acr parts` / `acr recipe list` 了嗎?
|
||||
3. 查詢回 `not_found` 時,我走的是 recipe/零件 PR 兩條路,**還是偷偷改寫成 `code`**?(後者=腹語術)
|
||||
4. 我是不是讓工作流回頭呼叫 AI 做判斷?(是 → 改成我自己做)
|
||||
5. 這動作會把資料開放給別人嗎?(會 → 要使用者明示同意)
|
||||
6. 我有沒有假裝(假綠/假防偽/代替人類確認)?(有 → 停,誠實標明)
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "arcrun",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.14",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "arcrun",
|
||||
"version": "1.3.13",
|
||||
"version": "1.3.14",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
|
||||
+4
-2
@@ -8,7 +8,9 @@
|
||||
"main": "./dist/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build": "npm run build:harness && npm run check:harness && tsc",
|
||||
"build:harness": "node scripts/build-harness-skill.mjs",
|
||||
"check:harness": "node scripts/check-harness-generation.mjs",
|
||||
"dev": "tsc --watch",
|
||||
"test": "node --test \"tests/**/*.test.ts\"",
|
||||
"prepublishOnly": "npm run build && chmod +x dist/index.js"
|
||||
@@ -42,6 +44,6 @@
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/uncle6me-web/Arcrun.git"
|
||||
"url": "git+https://github.com/youlinhsieh/Arcrun.git"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* build-harness-skill.mjs — 由 registry/skills/ 組出 harness 的 arcrun-mindset SKILL.md
|
||||
*
|
||||
* 【為什麼是「建置期複製」而不是人工維護兩份】
|
||||
* `registry/skills/write_intent_workflow.md` 是意圖語法的**單一真相源**——它同時是
|
||||
* MCP `arcrun_get_skill()` 回給雲端 AI 的內容。harness 的 skill 若人工再抄一份,
|
||||
* 兩份必然漂移(2026-07-31 實錄:harness 那份停在上一代,grep「意圖」「>>」= 0 命中,
|
||||
* 只講世界觀,害新裝的用戶 AI 學不到 `>>`)。
|
||||
*
|
||||
* 作法:harness skill = 三段拼接
|
||||
* SKILL.md.head ← harness 專屬(frontmatter/CLI 入口/三種東西的分型)
|
||||
* registry 的 write_intent_workflow.md 正文 ← 單一真相源,只此一份被維護
|
||||
* SKILL.md.tail ← harness 專屬(acr 指令表/暴露同意/誠實鐵律)
|
||||
*
|
||||
* 為什麼不用 symlink / npm 打包直接引用:npm `files` 只收 `harness/`,
|
||||
* registry/ 不進套件;symlink 在 npm pack 與 Windows 上不可靠。建置期複製最單純。
|
||||
*
|
||||
* 產物 `SKILL.md` **有 commit 進 repo**(npm 套件裝的是它,不會跑 build),
|
||||
* 由 check-harness-generation.mjs 驗證它與 registry 沒有漂移。
|
||||
*/
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url)); // cli/scripts
|
||||
const repoRoot = join(here, '..', '..'); // repo 根
|
||||
const skillDir = join(here, '..', 'harness', 'skills', 'arcrun-mindset');
|
||||
const registrySkill = join(repoRoot, 'registry', 'skills', 'write_intent_workflow.md');
|
||||
|
||||
const head = readFileSync(join(skillDir, 'SKILL.md.head'), 'utf8').trimEnd();
|
||||
const tail = readFileSync(join(skillDir, 'SKILL.md.tail'), 'utf8').trimEnd();
|
||||
const body = readFileSync(registrySkill, 'utf8');
|
||||
|
||||
// 取 registry skill 的正文:去掉它自己的 H1 標題與「何時用這個 skill」那段
|
||||
// (harness 的 head 已用 CLI 語境寫過入口),從第一個 `## 1.` 章節起收。
|
||||
const idx = body.indexOf('## 1. 意圖工作流的語法');
|
||||
if (idx < 0) {
|
||||
console.error('❌ registry/skills/write_intent_workflow.md 找不到「## 1. 意圖工作流的語法」章節;');
|
||||
console.error(' registry skill 結構變了 → 請同步更新 cli/scripts/build-harness-skill.mjs 的取段規則。');
|
||||
process.exit(1);
|
||||
}
|
||||
const middle = body
|
||||
.slice(idx)
|
||||
// registry 版把 MCP 工具當預設介面;harness 裝在有 acr CLI 的專案 → 補上 CLI 等價指令
|
||||
.replace(/`arcrun_get_workflow\(<name>\)`/g, '`acr logs <name>`(有 MCP 則 `arcrun_get_workflow(<name>)`)')
|
||||
.replace(/`arcrun_list_components` \/ `arcrun_search_components`/g, '`acr parts` / `acr search`')
|
||||
.replace(/下一步該讀哪支 skill:`arcrun_list_skills\(\)`/g, '下一步該讀哪支 skill(需 MCP):`arcrun_list_skills()`')
|
||||
.trimEnd();
|
||||
|
||||
const out = [
|
||||
head,
|
||||
'',
|
||||
'<!-- 以下正文由 registry/skills/write_intent_workflow.md 於建置期複製而來(單一真相源)。',
|
||||
' 不要直接編輯本段——改 registry 那份,然後跑 `npm run build:harness`。 -->',
|
||||
'',
|
||||
middle,
|
||||
'',
|
||||
tail,
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
writeFileSync(join(skillDir, 'SKILL.md'), out, 'utf8');
|
||||
console.log(`✓ harness skill 已由 registry 重建:${out.length} bytes`);
|
||||
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-harness-generation.mjs — 世代閘:harness 內容脫節就讓 build/publish 失敗
|
||||
*
|
||||
* 【為什麼要這道閘】
|
||||
* 2026-07-31 實錄:`acr install-harness` 的管道一直是好的,但它鋪出去的**內容停在上一代**——
|
||||
* harness skill grep「意圖」「>>」= 0 命中,只講世界觀。管道綠燈、交付物過時,
|
||||
* 沒有任何機械檢查會抱怨 ⇒ 世代脫節可以無聲存在好幾個月。
|
||||
*
|
||||
* 這道閘檢查四件交付物的「現世代指紋」。缺指紋 = exit 1,擋掉 build 與 npm publish。
|
||||
* 指紋要挑「上一代絕不會有、現世代一定有」的字串,不是隨便的關鍵字。
|
||||
*/
|
||||
import { readFileSync, existsSync, statSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const harness = join(here, '..', 'harness');
|
||||
const repoRoot = join(here, '..', '..');
|
||||
|
||||
/** @type {{file: string, must: [string, string][], mustNot?: [string,string][]}[]} */
|
||||
const CHECKS = [
|
||||
{
|
||||
file: 'skills/arcrun-mindset/SKILL.md',
|
||||
must: [
|
||||
['>>', '意圖語法(`A >> 邊 >> B`)——步驟 1 的核心教材'],
|
||||
['ON_SUCCESS', '合法邊之一'],
|
||||
['對每個', 'FOREACH 邊(十題裡有四題要用)'],
|
||||
['input', '第一個節點固定是 input'],
|
||||
['not_found', '現世代查詢狀態(舊版寫 missing/假 found)'],
|
||||
['腹語術', '缺件不准改寫成 code 的紅線'],
|
||||
['recipe', '零件 vs recipe 分型'],
|
||||
// 條件邊自 2026-08-01 起引擎已支援(cypher-executor/src/graph-executor.ts
|
||||
// case 'ON_TRUE'/'ON_FALSE'/'ON_BRANCH',31 個測試全過)。教材該教會怎麼用,
|
||||
// 不是教「不存在」——這條 must 同時防「哪天又被改回舊世代說法」的回歸。
|
||||
['ON_TRUE', '條件邊(配 if_control)自 2026-08-01 起引擎已支援,教材須教會用法'],
|
||||
],
|
||||
mustNot: [
|
||||
// ON_FAILURE 才是真的不存在(VALID_EDGE_TYPES 只有 ON_FAIL,見
|
||||
// cypher-executor/src/lib/constants.ts)。只准出現在「教它不存在」的脈絡。
|
||||
// 2026-08-10 修正:這道閘原本擋的是 ON_TRUE——但 ON_TRUE/ON_FALSE/ON_BRANCH
|
||||
// 已是引擎現世代能力,正確教材反而被這道閘擋下,是閘的判準過時了,不是教材寫錯。
|
||||
['ON_FAILURE', '引擎沒有這種邊(只有 ON_FAIL);教材不該把它教成可用的邊', /不要寫|不存在|沒有這種|❌|非法/],
|
||||
],
|
||||
},
|
||||
{
|
||||
file: 'CLAUDE.block.md',
|
||||
must: [
|
||||
['>>', '意圖語法要在 CLAUDE.md 就先亮相'],
|
||||
['not_found', '缺件兩條路的觸發點'],
|
||||
],
|
||||
},
|
||||
{
|
||||
file: 'commands/arcrun.md',
|
||||
must: [
|
||||
['>>', '/arcrun 的第一步就該是寫意圖'],
|
||||
['acr search', '現世代的跨類搜尋指令'],
|
||||
],
|
||||
},
|
||||
{
|
||||
file: 'hooks/arcrun-guard.sh',
|
||||
must: [
|
||||
['arcrun-mindset', 'hook 被擋下時要把 AI 導向 skill,而不是叫它去翻 repo 文件'],
|
||||
['>>', 'hook 的正路提示要提到意圖語法'],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
let fail = 0;
|
||||
const say = (s) => console.log(s);
|
||||
|
||||
say('\n 世代閘:檢查 harness 交付物是否為現世代內容\n');
|
||||
|
||||
for (const c of CHECKS) {
|
||||
const p = join(harness, c.file);
|
||||
if (!existsSync(p)) {
|
||||
say(` ❌ ${c.file} — 檔案不存在`);
|
||||
fail++;
|
||||
continue;
|
||||
}
|
||||
const text = readFileSync(p, 'utf8');
|
||||
const missing = c.must.filter(([needle]) => !text.includes(needle));
|
||||
const badNot = (c.mustNot ?? []).filter(([needle, , allowIfNear]) => {
|
||||
if (!text.includes(needle)) return false;
|
||||
if (!allowIfNear) return true;
|
||||
// 允許「在教『不要用』的脈絡裡」出現:看該字串所在行是否有豁免詞
|
||||
return !text
|
||||
.split('\n')
|
||||
.filter((l) => l.includes(needle))
|
||||
.every((l) => allowIfNear.test(l));
|
||||
});
|
||||
|
||||
if (missing.length === 0 && badNot.length === 0) {
|
||||
say(` ✓ ${c.file}`);
|
||||
} else {
|
||||
fail++;
|
||||
say(` ❌ ${c.file}`);
|
||||
for (const [needle, why] of missing) say(` 缺指紋「${needle}」— ${why}`);
|
||||
for (const [needle, why] of badNot) say(` 不該出現「${needle}」— ${why}`);
|
||||
}
|
||||
}
|
||||
|
||||
// harness skill 必須是由 registry 重建的最新版(防「改了 registry 忘了重跑 build」)
|
||||
const skillPath = join(harness, 'skills', 'arcrun-mindset', 'SKILL.md');
|
||||
const registrySkill = join(repoRoot, 'registry', 'skills', 'write_intent_workflow.md');
|
||||
if (existsSync(skillPath) && existsSync(registrySkill)) {
|
||||
try {
|
||||
execFileSync(process.execPath, [join(here, 'build-harness-skill.mjs')], { stdio: 'pipe' });
|
||||
const rebuilt = readFileSync(skillPath, 'utf8');
|
||||
const before = statSync(skillPath); // 重建後內容即為期望值
|
||||
void before;
|
||||
// 重建是冪等的:若重建後與 git 中的版本不同,git diff 會在 CI 顯示;
|
||||
// 這裡直接比對「重建結果是否含 registry 當前的關鍵段落」
|
||||
const reg = readFileSync(registrySkill, 'utf8');
|
||||
const marker = reg.includes('## 7. 常犯的錯') ? '## 7. 常犯的錯' : null;
|
||||
if (marker && !rebuilt.includes(marker)) {
|
||||
say(` ❌ harness skill 與 registry 漂移:registry 有「${marker}」但重建產物沒有`);
|
||||
fail++;
|
||||
} else {
|
||||
say(' ✓ harness skill 與 registry/skills/write_intent_workflow.md 同步');
|
||||
}
|
||||
} catch (e) {
|
||||
say(` ❌ 無法由 registry 重建 harness skill:${e.message}`);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
say('');
|
||||
if (fail) {
|
||||
say(` 🔴 世代閘擋下(${fail} 項)。harness 交付的內容落後於現世代。`);
|
||||
say(' 修法:改 registry/skills/write_intent_workflow.md(單一真相源)或對應的');
|
||||
say(' cli/harness/ 檔案,然後跑 `npm run build:harness` 重建,再跑本檢查。\n');
|
||||
process.exit(1);
|
||||
}
|
||||
say(' ✅ 世代閘通過:四件交付物都帶現世代指紋\n');
|
||||
@@ -18,7 +18,7 @@ const SOURCE_LABEL: Record<ConfigSource, string> = {
|
||||
};
|
||||
|
||||
/** 敏感欄位只印前綴,避免把 token 完整印到終端 / log。*/
|
||||
const SENSITIVE = new Set(['api_key', 'encryption_key', 'cf_api_token']);
|
||||
const SENSITIVE = new Set(['api_key', 'cf_api_token']);
|
||||
|
||||
function mask(field: string, value: string): string {
|
||||
if (SENSITIVE.has(field) && value.length > 8) return `${value.slice(0, 8)}…`;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
/**
|
||||
* acr creds push/list/replace/delete
|
||||
*
|
||||
* credential-store-migration T5(2026-07-03,Arcrun#2)已把 server 端寫入路徑從
|
||||
* 「client AES-GCM 加密 + {name,encrypted,iv}」改為「明文值 + TLS 傳輸」(§2.4 選項甲:
|
||||
* arcrun 不再自管 ENCRYPTION_KEY,密文改由 CF Workers Secrets 託管)。T9(§3 治理端點)
|
||||
* 對應把 CLI 薄殼換成新的 list/replace/delete 三支指令:全部只做「讀 argv/yaml → 呼叫
|
||||
* cypher-executor API → 印結果」,不做任何加解密或業務邏輯(rule 07 薄殼原則)。
|
||||
* 寫入路徑=「明文值 + TLS 傳輸」,值交由 CF Workers Secrets 託管。CLI 是薄殼:
|
||||
* list/replace/delete 三支指令全部只做「讀 argv/yaml → 呼叫 cypher-executor API →
|
||||
* 印結果」,不做任何加解密或業務邏輯(rule 07 薄殼原則)。
|
||||
*
|
||||
* 移除任何「印出 credential 值」的路徑(D19:擁有目錄,不擁有內容物,連 owner 都讀不回)。
|
||||
*/
|
||||
|
||||
+22
-47
@@ -11,7 +11,6 @@ import { saveConfig, type ArcrunConfig } from '../lib/config.js';
|
||||
import { CfAccountClient } from '../lib/cf-api.js';
|
||||
import {
|
||||
REQUIRED_KV_NAMESPACES,
|
||||
SECRET_TARGET_WORKERS,
|
||||
downloadAndDeploy,
|
||||
type DeployContext,
|
||||
} from '../lib/deploy.js';
|
||||
@@ -19,7 +18,7 @@ import { cmdInstallHarness } from './install-harness.js';
|
||||
import { cmdMcpSetup } from './mcp-setup.js';
|
||||
import { detectEnvironment, printPreflight, verifyInstall } from '../lib/preflight.js';
|
||||
|
||||
const ARCRUN_REGISTER_URL = 'https://cypher.arcrun.dev/register';
|
||||
const ARCRUN_LOGIN_URL = 'https://arcrun.dev/login';
|
||||
|
||||
async function prompt(rl: ReturnType<typeof createInterface>, question: string): Promise<string> {
|
||||
const answer = await rl.question(chalk.cyan(`? ${question}: `));
|
||||
@@ -103,40 +102,23 @@ async function initLocal(): Promise<void> {
|
||||
}
|
||||
|
||||
async function initStandard(rl: ReturnType<typeof createInterface>): Promise<void> {
|
||||
console.log(chalk.gray(' Standard 模式:只需要 email,不需要 Cloudflare 帳號\n'));
|
||||
console.log(chalk.gray(' Standard 模式:用 arcrun.dev 帳號登入取得 API Key\n'));
|
||||
|
||||
const email = await prompt(rl, 'Email(用來取得 API Key)');
|
||||
// API Key 發放走網站 OAuth 登入(/auth/google/start、/auth/github/start)。
|
||||
// CLI 是薄殼,不自己發 key(rule 07),只引導用戶去拿再貼回來。
|
||||
console.log(' 1. 開啟 ' + chalk.cyan(ARCRUN_LOGIN_URL) + ' 用 Google / GitHub 登入');
|
||||
console.log(' 2. 在 Dashboard 複製你的 API Key(ak_ 開頭)\n');
|
||||
|
||||
process.stdout.write(chalk.gray('\n → 向 arcrun.dev 取得 API Key...'));
|
||||
const apiKey = (await prompt(rl, 'API Key(ak_...)')).trim();
|
||||
|
||||
let apiKey = '';
|
||||
let encryptionKey = '';
|
||||
try {
|
||||
const res = await fetch(ARCRUN_REGISTER_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`取得失敗(${res.status}):${err}`);
|
||||
}
|
||||
|
||||
const data = await res.json() as { api_key: string; encryption_key: string };
|
||||
apiKey = data.api_key;
|
||||
encryptionKey = data.encryption_key;
|
||||
console.log(chalk.green(' ✓'));
|
||||
} catch (e) {
|
||||
console.log(chalk.yellow(` ✗ ${e instanceof Error ? e.message : e}`));
|
||||
console.log(chalk.yellow(' 請確認網路連線後重新執行 acr init\n'));
|
||||
if (!apiKey.startsWith('ak_')) {
|
||||
console.log(chalk.yellow('\n ✗ API Key 應以 ak_ 開頭,請重新執行 acr init\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const config: ArcrunConfig = {
|
||||
mode: 'standard',
|
||||
api_key: apiKey,
|
||||
encryption_key: encryptionKey,
|
||||
};
|
||||
|
||||
saveConfig(config);
|
||||
@@ -248,15 +230,16 @@ async function initSelfHosted(
|
||||
console.log(chalk.yellow(` ⚠ 查 subdomain 失敗(${e instanceof Error ? e.message : e}),稍後可手動補`));
|
||||
}
|
||||
|
||||
// 3.5 語義查詢開關(issue #7 / T2.4):問用戶要不要開(預設關,free-tier 友善)。
|
||||
// 開 → deploy 建 CF Vectorize index + 注入 binding。關 → base 維持 LIKE keyword,零花費。
|
||||
// 之後想開:跟 CC 說「幫我開語義查詢」或設 kbdb_embed:true + acr update(不必重 init)。
|
||||
// 3.5 語義查詢(issue #7 / T2.4):**預設開**(2026-08-09 翻轉,leo:「語義搜尋已經
|
||||
// 確定是一安裝就提供的功能」——預設關會產出一批「看起來裝好了、其實少一條腿」的
|
||||
// 實例,之後畫面上還被誤說成「沒開通」)。顯式回答 n 才關(極端省額度者自選)。
|
||||
// 開 → deploy 建 CF Vectorize index + 注入 binding。關 → base 維持 LIKE keyword。
|
||||
const embedAns = (await prompt(
|
||||
rl,
|
||||
'要開語義查詢嗎?(KBDB 加 AI 向量搜尋;用 CF Vectorize,可能多花費;預設關,之後可隨時開) [y/N]',
|
||||
'要開語義查詢嗎?(內建功能,建議保持開啟;用 CF Vectorize,有免費額度) [Y/n]',
|
||||
)).trim().toLowerCase();
|
||||
const kbdbEmbed = embedAns === 'y' || embedAns === 'yes';
|
||||
if (kbdbEmbed) console.log(chalk.gray(' → 已選開語義查詢:部署時會建 Vectorize index。'));
|
||||
const kbdbEmbed = !(embedAns === 'n' || embedAns === 'no');
|
||||
if (!kbdbEmbed) console.log(chalk.yellow(' → 已選關語義查詢:這台實例將只有關鍵字搜尋(之後可設 kbdb_embed:true + acr update 補開)。'));
|
||||
|
||||
// 4. 下載 repo 部署物(含預編譯 wasm)+ 注入 KV id + wrangler deploy 全部 Worker
|
||||
console.log(chalk.gray('\n → 下載部署物 + 部署 Worker(從 GitHub 拉預編譯 wasm,用你的 CF token 部署)...'));
|
||||
@@ -322,28 +305,20 @@ async function initSelfHosted(
|
||||
console.log(chalk.green(' ✓ 設定寫入 ~/.arcrun/config.yaml'));
|
||||
console.log(chalk.green(' ✓ 建立 credentials.yaml'));
|
||||
|
||||
// 下一步:身份設定(self-hosted 單租戶——namespace 明碼用戶自填、encryption_key 用戶自保管)。
|
||||
// 下一步:身份設定(self-hosted 單租戶——namespace 明碼用戶自填)。
|
||||
// 工具不生成、不 hash、不外傳任何 key(守 rule 05 精神:secret 不進自動化,由用戶持有)。
|
||||
console.log(chalk.bold('\n 下一步 ①:在這個專案建 .env(你自己填,工具不碰):'));
|
||||
console.log(chalk.cyan(' NAMESPACE=leo # 你的資料分區標籤(明碼即可,不是密碼)'));
|
||||
console.log(chalk.cyan(' ENCRYPTION_KEY=<64+ hex> # credential 加密金鑰,你自己保管'));
|
||||
console.log(chalk.gray(' 生成 key:node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"'));
|
||||
console.log(chalk.gray(' (NAMESPACE 是分區標籤非密碼;要防外部呼叫請對 webhook 加保護。'));
|
||||
console.log(chalk.gray(' ENCRYPTION_KEY 忘了 = 解不開已上傳的 credential。.env 已被 gitignore。)'));
|
||||
|
||||
console.log(chalk.bold('\n 下一步 ②:把同一把 ENCRYPTION_KEY 設進你的 worker(runtime 解密要用):'));
|
||||
for (const w of SECRET_TARGET_WORKERS) {
|
||||
console.log(chalk.cyan(` wrangler secret put ENCRYPTION_KEY --name ${w}`));
|
||||
}
|
||||
console.log(chalk.gray(` ${SECRET_TARGET_WORKERS.length} 個 Worker 共用同一把(與 .env 的 ENCRYPTION_KEY 一致)。`));
|
||||
console.log(chalk.gray(' 不想自己跑?跑 acr init 時授權(明示同意)我可代設——但預設由你自己 put(你持有 key)。\n'));
|
||||
console.log(chalk.gray(' .env 已被 gitignore。)'));
|
||||
console.log(chalk.gray(' credential 不需要自管加密金鑰:明文由 CF Workers Secrets 託管。\n'));
|
||||
|
||||
// credential-store-migration T3(§2.3):cypher worker 要有一把「能打 CF Workers Scripts
|
||||
// secrets 管理 API 的 token」才能讓 POST/PUT /credentials 把密文寫進 Workers Secrets。
|
||||
// 比照 ENCRYPTION_KEY 的既有模式(印手動指令,不是工具自動 put)——CF_ACCOUNT_ID 非機密,
|
||||
// 已由 downloadAndDeploy/injectWranglerConfig 自動注入(同 WORKER_SUBDOMAIN 模式),
|
||||
// 印手動指令而非工具自動 put——CF_ACCOUNT_ID 非機密,已由 downloadAndDeploy/
|
||||
// injectWranglerConfig 自動注入(同 WORKER_SUBDOMAIN 模式),
|
||||
// 只有 CF_SECRETS_API_TOKEN(機密)需要用戶手動 put。
|
||||
console.log(chalk.bold(' 下一步 ③:把能打 Workers Scripts secrets API 的 CF token 設進 cypher worker:'));
|
||||
console.log(chalk.bold(' 下一步 ②:把能打 Workers Scripts secrets API 的 CF token 設進 cypher worker:'));
|
||||
console.log(chalk.cyan(` wrangler secret put CF_SECRETS_API_TOKEN --name arcrun-cypher-executor`));
|
||||
console.log(chalk.gray(' 貼你剛才用來部署的同一個 CF API Token(需含 Workers Scripts:Edit 權限)。'));
|
||||
console.log(chalk.gray(' 用途:POST/PUT /credentials 把密文寫進 Workers per-script Secrets(credential-store-migration T5)。'));
|
||||
|
||||
@@ -110,11 +110,18 @@ function mergeSettings(cwd: string, src: string): void {
|
||||
writeFileSync(path, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
||||
}
|
||||
|
||||
/** 遞迴複製目錄樹(覆蓋同名檔)。 */
|
||||
/** 建置期產物的來源片段(`SKILL.md.head` / `.tail`),只給 build-harness-skill.mjs 用,
|
||||
* 不該被鋪進使用者專案(使用者拿到的是拼接好的 `SKILL.md`)。 */
|
||||
function isBuildSource(name: string): boolean {
|
||||
return name.endsWith('.head') || name.endsWith('.tail');
|
||||
}
|
||||
|
||||
/** 遞迴複製目錄樹(覆蓋同名檔;跳過建置期來源片段)。 */
|
||||
function copyTree(srcDir: string, dstDir: string): void {
|
||||
if (!existsSync(srcDir)) return;
|
||||
mkdirSync(dstDir, { recursive: true });
|
||||
for (const name of readdirSync(srcDir, { withFileTypes: true })) {
|
||||
if (isBuildSource(name.name)) continue;
|
||||
const s = join(srcDir, name.name);
|
||||
const d = join(dstDir, name.name);
|
||||
if (name.isDirectory()) copyTree(s, d);
|
||||
|
||||
@@ -55,10 +55,12 @@ export async function cmdPush(filePath: string): Promise<void> {
|
||||
const searchSpinner = ora('取得執行圖').start();
|
||||
let graph: unknown;
|
||||
try {
|
||||
// t158「部署≠發現」(leo:「這裡只是複製工作流的 data 過去,沒有要在這裡驗證」):
|
||||
// push=複製路徑,帶 mode:compile 純編圖——寫錯的 workflow 照樣部署,錯在執行時現形。
|
||||
const res = await fetch(`${executorUrl}/cypher/search`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ triplets: workflow.flow }),
|
||||
body: JSON.stringify({ triplets: workflow.flow, mode: 'compile' }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -68,10 +70,8 @@ export async function cmdPush(filePath: string): Promise<void> {
|
||||
}
|
||||
|
||||
const data = await res.json() as { cypher: { nodes: unknown[]; edges: unknown[] }; missing: string[] };
|
||||
if (data.missing?.length > 0) {
|
||||
searchSpinner.fail(chalk.red(`以下零件不存在:${data.missing.join(', ')}\n執行 acr parts 查看可用零件。`));
|
||||
process.exit(1);
|
||||
}
|
||||
// t158:push 不看 missing(compile 模式亦恆空)——存在性由執行時 component-loader 決定;
|
||||
// 要「先問有沒有」用 acr validate/MCP 查詢(discover 路徑)。
|
||||
|
||||
// 附上 id / name,並將 workflow.config 套入節點(componentId + data)
|
||||
const rawGraph = data.cypher as { nodes: Array<{ id: string; componentId?: string; data?: Record<string, unknown> }>; edges: unknown[] };
|
||||
|
||||
@@ -84,9 +84,13 @@ export async function cmdUpdate(opts: { force?: boolean } = {}): Promise<void> {
|
||||
// self-hosted → 注入 MULTI_TENANT="false"(mcp-account-source §5.5,修 acr update 部署的 MCP 401)。
|
||||
// config 源頭:init 寫 multi_tenant:false + mode:'self-hosted'。acr update 只在 self-hosted 跑。
|
||||
selfHosted: config.mode === 'self-hosted' || config.multi_tenant === false,
|
||||
// 語義查詢開關(issue #7):config.kbdb_embed:true → 部署建 Vectorize index + 注入 binding。
|
||||
// 這也是「CC 幫開」的落地路徑:CC 寫 kbdb_embed:true 進 config → acr update redeploy 即生效。
|
||||
kbdbEmbed: config.kbdb_embed === true,
|
||||
// 語義查詢(issue #7):預設**開**,只有 config 顯式寫 kbdb_embed:false 才關。
|
||||
// 🔴 2026-08-09 翻轉預設(leo:「語義搜尋已經確定是一安裝就提供的功能」)。
|
||||
// 舊判斷 `=== true` 的實害:config 沒這個欄位(舊 config / 一鍵安裝實例本機補跑 update)
|
||||
// 時 redeploy 會把 kbdb 的 [[vectorize]]+[ai] binding 靜默剝掉——一台**原本正常**的
|
||||
// 實例就這樣失去語意搜尋,畫面上還被說成「還沒開通」。wrangler deploy 是整份覆蓋,
|
||||
// binding 不在 toml 裡=直接消失,這正是「裝好的實例壞掉」的機制之一。
|
||||
kbdbEmbed: config.kbdb_embed !== false,
|
||||
};
|
||||
|
||||
const result = await downloadAndDeploy(ctx, 'main', { force: opts.force });
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* acr workflow export <name> / acr workflow import <file> — workflow 可攜原語(t158)。
|
||||
*
|
||||
* leo 07-31 定調:「你要做的就是一個叫 export,另一個是 import,打包好的幾個工作流
|
||||
* 準備好直接 import 就好了。現在如果我要把我做的工作流分享給同事,我要怎麼 export?
|
||||
* 他要如何 import?是缺了功能用 search 來湊嗎?在從前就是寫成幾個 yaml 丟過去
|
||||
* 讓新的送進 KBDB 不是嗎?」
|
||||
*
|
||||
* - export:GET /webhooks/named/:name/definition → 寫成 .workflow.yaml 可攜檔
|
||||
* (name/description/flow[從 graph.edges 反推,供人讀]/config/graph[可執行形,引擎產])。
|
||||
* - import:讀可攜檔 → **直接 POST /webhooks/named**。零編圖、零 /cypher/search、
|
||||
* 零存在性驗證(部署≠發現,V2 純複製)——缺件的 workflow 照樣進,跑錯再改。
|
||||
* 手寫的 yaml(無 graph 欄)請走 acr push(那條才需要編圖)。
|
||||
* - 安裝器走同一條路:workflows.json 打包期預編 graph,pushWorkflow 直接 POST——
|
||||
* 不准安裝器走私有路徑。
|
||||
*/
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import yaml from 'js-yaml';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { loadConfig, getCypherExecutorUrl } from '../lib/config.js';
|
||||
|
||||
type GraphShape = {
|
||||
nodes?: Array<{ id?: string }>;
|
||||
edges?: Array<{ from?: string; to?: string; type?: string }>;
|
||||
};
|
||||
|
||||
/** graph.edges → flow 三元組(人讀用;graph 才是可執行真相)。 */
|
||||
function flowFromGraph(graph: GraphShape): string[] {
|
||||
return (graph.edges ?? [])
|
||||
.filter(e => e.from && e.to)
|
||||
.map(e => `${e.from} >> ${e.type ?? 'ON_SUCCESS'} >> ${e.to}`);
|
||||
}
|
||||
|
||||
function requireStandardConfig(): { executorUrl: string; apiKey: string } {
|
||||
const config = loadConfig();
|
||||
if (config.mode === 'local') {
|
||||
console.error(chalk.red('Local 模式不支援 workflow export/import(需要連上實例)。'));
|
||||
process.exit(1);
|
||||
}
|
||||
if (!config.api_key) {
|
||||
console.error(chalk.red('缺少 api_key/NAMESPACE,請先 acr init。'));
|
||||
process.exit(1);
|
||||
}
|
||||
return { executorUrl: getCypherExecutorUrl(config), apiKey: config.api_key };
|
||||
}
|
||||
|
||||
export async function cmdWorkflowExport(name: string, options: { output?: string }): Promise<void> {
|
||||
const { executorUrl, apiKey } = requireStandardConfig();
|
||||
const spinner = ora(`從 ${executorUrl} 匯出 "${name}"`).start();
|
||||
try {
|
||||
const res = await fetch(`${executorUrl}/webhooks/named/${encodeURIComponent(name)}/definition`, {
|
||||
headers: { 'X-Arcrun-API-Key': apiKey },
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
spinner.fail(chalk.red(`匯出失敗(${res.status}):${err.slice(0, 200)}`));
|
||||
process.exit(1);
|
||||
}
|
||||
const def = await res.json() as {
|
||||
name: string; description: string;
|
||||
graph: GraphShape; config: Record<string, unknown>;
|
||||
};
|
||||
const out = options.output ?? `${def.name}.workflow.yaml`;
|
||||
const doc = {
|
||||
name: def.name,
|
||||
description: def.description,
|
||||
// flow=從 graph 反推的可讀視圖;import 用的是 graph(可執行真相)
|
||||
flow: flowFromGraph(def.graph),
|
||||
config: def.config ?? {},
|
||||
graph: def.graph,
|
||||
};
|
||||
writeFileSync(out, yaml.dump(doc, { lineWidth: 120, noRefs: true }), 'utf8');
|
||||
spinner.succeed(chalk.green(`✓ 已匯出 → ${out}`));
|
||||
console.log(chalk.gray(` 給同事:把這個檔傳過去,對方 acr workflow import ${out} 即可。`));
|
||||
} catch (e) {
|
||||
spinner.fail(chalk.red(`網路錯誤:${e instanceof Error ? e.message : e}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdWorkflowImport(filePath: string): Promise<void> {
|
||||
const { executorUrl, apiKey } = requireStandardConfig();
|
||||
let doc: { name?: string; description?: string; config?: Record<string, unknown>; graph?: GraphShape };
|
||||
try {
|
||||
doc = yaml.load(readFileSync(filePath, 'utf8')) as typeof doc;
|
||||
} catch (e) {
|
||||
console.error(chalk.red(`讀不了 ${filePath}:${e instanceof Error ? e.message : e}`));
|
||||
process.exit(1);
|
||||
}
|
||||
if (!doc?.name) {
|
||||
console.error(chalk.red('檔案缺 name 欄位。'));
|
||||
process.exit(1);
|
||||
}
|
||||
if (!doc.graph || !Array.isArray(doc.graph.nodes)) {
|
||||
// 手寫 yaml(只有 flow 沒 graph)=acr push 的場景(那條會編圖)。import 專吃 export 檔。
|
||||
console.error(chalk.red('這個檔沒有 graph 欄位(不是 export 產物)。'));
|
||||
console.log(chalk.gray('手寫的 workflow.yaml 請改用:acr push ' + filePath));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const spinner = ora(`匯入 "${doc.name}" → ${executorUrl}`).start();
|
||||
try {
|
||||
// 純複製:graph 直接送,不編圖、不打 /cypher/search、不驗零件存在(跑錯再改)。
|
||||
const res = await fetch(`${executorUrl}/webhooks/named`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Arcrun-API-Key': apiKey },
|
||||
body: JSON.stringify({
|
||||
name: doc.name,
|
||||
graph: { ...doc.graph, id: doc.name, name: doc.name },
|
||||
config: doc.config ?? {},
|
||||
description: doc.description ?? '',
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
spinner.fail(chalk.red(`匯入失敗(${res.status}):${err.slice(0, 200)}`));
|
||||
process.exit(1);
|
||||
}
|
||||
const data = await res.json() as { webhook_url?: string };
|
||||
spinner.succeed(chalk.green(`✓ "${doc.name}" 已匯入`));
|
||||
if (data.webhook_url) console.log(chalk.bold(` Webhook URL:${chalk.cyan(data.webhook_url)}`));
|
||||
console.log(chalk.gray(' 沒驗零件存在——跑起來若報「找不到零件」,補上零件/recipe 或改 config 再跑。'));
|
||||
} catch (e) {
|
||||
spinner.fail(chalk.red(`網路錯誤:${e instanceof Error ? e.message : e}`));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -164,43 +164,3 @@ export class CfAccountClient {
|
||||
return result.uuid;
|
||||
}
|
||||
}
|
||||
|
||||
/** AES-GCM 加密 credential(與 cypher-executor credential-injector 解密邏輯對應)*/
|
||||
export async function encryptCredential(value: string, encryptionKey: string): Promise<string> {
|
||||
if (!encryptionKey || encryptionKey.length < 64) {
|
||||
throw new Error(
|
||||
'ARCRUN_ENCRYPTION_KEY 未設定或長度不足(需要 256-bit hex,即 64 個十六進位字元)\n' +
|
||||
'生成指令:node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"'
|
||||
);
|
||||
}
|
||||
|
||||
const keyBytes = hexToUint8Array(encryptionKey);
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
keyBytes.buffer as ArrayBuffer,
|
||||
{ name: 'AES-GCM' },
|
||||
false,
|
||||
['encrypt'],
|
||||
);
|
||||
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const encoded = new TextEncoder().encode(value);
|
||||
const cipherBuffer = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, cryptoKey, encoded);
|
||||
|
||||
return JSON.stringify({
|
||||
encrypted: uint8ArrayToBase64(new Uint8Array(cipherBuffer)),
|
||||
iv: uint8ArrayToBase64(iv),
|
||||
});
|
||||
}
|
||||
|
||||
function hexToUint8Array(hex: string): Uint8Array {
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function uint8ArrayToBase64(arr: Uint8Array): string {
|
||||
return Buffer.from(arr).toString('base64');
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ export interface ArcrunConfig {
|
||||
mode: 'local' | 'standard' | 'self-hosted';
|
||||
// Standard 模式
|
||||
api_key?: string; // arcrun.dev API Key(ak_前綴)
|
||||
encryption_key?: string; // AES-GCM key,與 cypher-executor ENCRYPTION_KEY secret 一致
|
||||
// Self-hosted 模式
|
||||
cloudflare_account_id?: string;
|
||||
user_kv_namespace_id?: string;
|
||||
@@ -29,10 +28,12 @@ export interface ArcrunConfig {
|
||||
mcp_url?: string;
|
||||
multi_tenant?: boolean;
|
||||
// 語義查詢開關(issue #7 / SDD T2.4,self-hosted 從零做)。
|
||||
// true → deploy 時建 CF Vectorize index 並注入 kbdb worker 的 [[vectorize]]+[ai] binding;
|
||||
// 🔴 2026-08-09 預設翻轉(leo:「語義搜尋已經確定是一安裝就提供的功能」):
|
||||
// 未設 → **視同開**(init/update 皆以 `!== false` 判斷)。只有顯式 false 才關。
|
||||
// true/未設 → deploy 時建 CF Vectorize index 並注入 kbdb worker 的 [[vectorize]]+[ai] binding;
|
||||
// kbdb embed 模組啟用(寫入時對標記 embed 的 entry embed、search 支援 mode=semantic)。
|
||||
// 未設/false → base 維持 LIKE keyword(free-tier 友善,不建 index、不花費)。
|
||||
// 開法:設 kbdb_embed:true → redeploy(acr update)。「CC 幫開」=CC 寫此欄 true + 跑 acr update。
|
||||
// false → base 維持 LIKE keyword(顯式選擇才有這個狀態;缺欄位不再等於關——
|
||||
// 舊語意會讓 acr update 把正常實例的 binding 靜默剝掉,畫面再謊稱「沒開通」)。
|
||||
kbdb_embed?: boolean;
|
||||
// 暴露 consent 閘已移除(leo 2026-06-29,Arcrun#13)。此欄位保留只為向後相容舊 config.yaml
|
||||
// (讀到不報錯,不再寫入/檢查)。
|
||||
@@ -58,8 +59,6 @@ const ENV_MAP: Record<string, keyof ArcrunConfig> = {
|
||||
NAMESPACE: 'api_key',
|
||||
ARCRUN_NAMESPACE: 'api_key',
|
||||
ARCRUN_API_KEY: 'api_key',
|
||||
ARCRUN_ENCRYPTION_KEY: 'encryption_key',
|
||||
ENCRYPTION_KEY: 'encryption_key',
|
||||
ARCRUN_CYPHER_EXECUTOR_URL: 'cypher_executor_url',
|
||||
ARCRUN_MCP_URL: 'mcp_url',
|
||||
CLOUDFLARE_ACCOUNT_ID: 'cloudflare_account_id',
|
||||
@@ -117,7 +116,7 @@ function readProjectConfig(): Partial<ArcrunConfig> | undefined {
|
||||
|
||||
/**
|
||||
* 載入 .env(就近往上找,同 .arcrun.yaml)到 process.env,讓用戶照 Node/Python 慣例
|
||||
* 在 .env 設 NAMESPACE / ENCRYPTION_KEY 等即生效。不覆蓋「已存在於 shell」的 env(shell > .env)。
|
||||
* 在 .env 設 NAMESPACE / CLOUDFLARE_* 等即生效。不覆蓋「已存在於 shell」的 env(shell > .env)。
|
||||
* 自己解析(不引入 dotenv 依賴)。只認單純 KEY=VALUE,忽略空行/註解/引號。
|
||||
*/
|
||||
let _envFileLoaded = false;
|
||||
@@ -193,7 +192,7 @@ export function resolveConfigSources(): Array<{ field: keyof ArcrunConfig; value
|
||||
const project = readProjectConfig() ?? {};
|
||||
const env = readEnvOverrides();
|
||||
const fields: (keyof ArcrunConfig)[] = [
|
||||
'mode', 'api_key', 'encryption_key', 'cloudflare_account_id',
|
||||
'mode', 'api_key', 'cloudflare_account_id',
|
||||
'cf_api_token', 'cypher_executor_url', 'mcp_url',
|
||||
];
|
||||
const rows: Array<{ field: keyof ArcrunConfig; value: string; source: ConfigSource }> = [];
|
||||
|
||||
+65
-22
@@ -129,13 +129,6 @@ export const REQUIRED_KV_NAMESPACES = [
|
||||
'OAUTH_KV',
|
||||
] as const;
|
||||
|
||||
/** 部署後要提示用戶手動 `wrangler secret put ENCRYPTION_KEY` 的 Worker。*/
|
||||
export const SECRET_TARGET_WORKERS = [
|
||||
'arcrun-cypher-executor',
|
||||
'arcrun-auth-static-key',
|
||||
'arcrun-auth-service-account',
|
||||
] as const;
|
||||
|
||||
/** 共享部署依賴(downloadAndDeploy 2.5:tarball root 裝一次,各 worker 往上 resolve)。
|
||||
* 含全部 worker 的 runtime deps:tier1 component 只要 hono;tier2 cypher/registry/mcp/kbdb
|
||||
* 另需 zod / @hono/zod-openapi / @modelcontextprotocol/sdk / js-yaml / yaml;
|
||||
@@ -170,8 +163,27 @@ export interface DeployContext {
|
||||
kbdbEmbed?: boolean;
|
||||
}
|
||||
|
||||
/** Vectorize index 名(kbdb embed 模組用)。bge-base-en-v1.5 = 768 維、cosine。 */
|
||||
export const KBDB_VECTORIZE_INDEX = 'arcrun-kbdb-embed';
|
||||
/**
|
||||
* Vectorize index 名(kbdb embed 模組用)。**bge-m3 = 1024 維、cosine。**
|
||||
*
|
||||
* 🔴 2026-08-03 換代(leo 拍板;5 組中文測資實證:舊 `bge-base-en-v1.5` 排序 2/5、
|
||||
* margin −0.0413=**中文根本不能用**;`bge-m3` 5/5、+0.1410、959ms)。
|
||||
* leo 08-05:「換 embed model 當然要合併,當然要換 vectorize,原本的根本不能用」。
|
||||
*
|
||||
* **換模型必須換 index,且必須換「名字」**:
|
||||
* ① 維度 768→1024,舊 index 收不進新向量
|
||||
* ② 就算維度相同也不能沿用——不同模型的向量混在同一 index,比對出來是垃圾;
|
||||
* 而 #58(Vectorize vector delete 未接)代表舊向量刪不掉
|
||||
* ⇒ **開新名字的 index 反而順手繞開 #58**,且新舊並存可回滾。
|
||||
*
|
||||
* ⚠️ 這個常數同時被 `ensureVectorizeMetadataIndexes()` 使用(deploy.ts:426)
|
||||
* ⇒ t36 的四個 metadata index(owner_id/entry_type/source/library,Arcrun#11 根因修復)
|
||||
* 會自動建在新 index 上,**不會因為改名而遺失**(已查證,非假設)。
|
||||
*
|
||||
* 既有實例遷移:部署後 `POST /embed/backfill {"reindex":true}` 重嵌到 remaining=0,
|
||||
* 確認語意查詢正常後,舊的 `arcrun-kbdb-embed` 可自行刪除。
|
||||
*/
|
||||
export const KBDB_VECTORIZE_INDEX = 'arcrun-kbdb-embed-m3';
|
||||
|
||||
export interface DeployResult {
|
||||
implemented: boolean;
|
||||
@@ -324,20 +336,49 @@ export async function downloadAndDeploy(
|
||||
failures.push(`D1 migration: 部署物缺 kbdb/migrations/0001_base.sql(${migPath})`);
|
||||
}
|
||||
|
||||
// 3.6 credential-primitives-wasm T2(credential-store-migration.md §2.2):
|
||||
// credentials 目錄表(api_key/name/service/sensitivity/secret_ref/created_at/last_used_at)。
|
||||
// 同一顆 D1(與 KBDB base 共用),冪等 IF NOT EXISTS,套用機制與 0001_base.sql 完全相同
|
||||
// (同一個 applyD1Migration helper,同一支 CF D1 query API)。D19:這張表不含密文,
|
||||
// 密文本體住在 Workers per-script Secrets(見 cypher-executor/src/routes/credentials.ts)。
|
||||
const credMigPath = join(root, 'kbdb', 'migrations', '0002_credentials.sql');
|
||||
if (existsSync(credMigPath)) {
|
||||
// 3.6 credential template seed(D38 圍牆修復,總管交辦,2026-08-07):credential 目錄改走
|
||||
// KBDB template 機制(entries 表 entry_type='credential',比照 recipe_stat/execution_log
|
||||
// 慣例),取代舊的獨立 credentials 表(0002,已退役,見該檔頭部說明)。冪等,套用機制
|
||||
// 與 0001_base.sql 完全相同。密文本體仍住 Workers per-script Secrets(見
|
||||
// cypher-executor/src/routes/credentials.ts),D19「擁有目錄不擁有內容物」不變。
|
||||
const credTplMigPath = join(root, 'kbdb', 'migrations', '0005_credential_template.sql');
|
||||
if (existsSync(credTplMigPath)) {
|
||||
try {
|
||||
await applyD1Migration(ctx, readFileSync(credMigPath, 'utf8'));
|
||||
await applyD1Migration(ctx, readFileSync(credTplMigPath, 'utf8'));
|
||||
} catch (e) {
|
||||
failures.push(`D1 migration 0002_credentials (${ctx.d1DatabaseId}): ${e instanceof Error ? e.message : String(e)}`);
|
||||
failures.push(`D1 migration 0005_credential_template (${ctx.d1DatabaseId}): ${e instanceof Error ? e.message : String(e)}`);
|
||||
}
|
||||
} else {
|
||||
failures.push(`D1 migration: 部署物缺 kbdb/migrations/0002_credentials.sql(${credMigPath})`);
|
||||
failures.push(`D1 migration: 部署物缺 kbdb/migrations/0005_credential_template.sql(${credTplMigPath})`);
|
||||
}
|
||||
|
||||
// 3.6b 退役舊 credentials 表(D38,2026-08-07):把該表殘留資料(若有)搬進 entries 後
|
||||
// 拆表,讓 KBDB 回到「只有三張核心表」的狀態。冪等且對「從未跑過 0002」的全新實例
|
||||
// 無害(表不存在時本檔第一步先補空殼再立刻拆掉,詳見檔頭)。每次部署都會重跑,
|
||||
// 但真資料只搬一次(NOT EXISTS 判斷防重複)。
|
||||
const dropCredMigPath = join(root, 'kbdb', 'migrations', '0006_drop_credentials_table.sql');
|
||||
if (existsSync(dropCredMigPath)) {
|
||||
try {
|
||||
await applyD1Migration(ctx, readFileSync(dropCredMigPath, 'utf8'));
|
||||
} catch (e) {
|
||||
failures.push(`D1 migration 0006_drop_credentials_table (${ctx.d1DatabaseId}): ${e instanceof Error ? e.message : String(e)}`);
|
||||
}
|
||||
} else {
|
||||
failures.push(`D1 migration: 部署物缺 kbdb/migrations/0006_drop_credentials_table.sql(${dropCredMigPath})`);
|
||||
}
|
||||
|
||||
// 3.7 execution_log template seed(KV 額度事故修復,2026-08-07):workflow 執行紀錄改走
|
||||
// KBDB template 機制(entries 表 entry_type='execution_log',比照 recipe_stat 慣例;
|
||||
// schema 零異動,只 seed 一列 template 定義,同 0001_base.sql §3 手法,self-hosted 同步套用)。
|
||||
const execLogMigPath = join(root, 'kbdb', 'migrations', '0004_execution_log_template.sql');
|
||||
if (existsSync(execLogMigPath)) {
|
||||
try {
|
||||
await applyD1Migration(ctx, readFileSync(execLogMigPath, 'utf8'));
|
||||
} catch (e) {
|
||||
failures.push(`D1 migration 0004_execution_log_template (${ctx.d1DatabaseId}): ${e instanceof Error ? e.message : String(e)}`);
|
||||
}
|
||||
} else {
|
||||
failures.push(`D1 migration: 部署物缺 kbdb/migrations/0004_execution_log_template.sql(${execLogMigPath})`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -395,7 +436,9 @@ async function applyD1Migration(ctx: DeployContext, sql: string): Promise<void>
|
||||
|
||||
/**
|
||||
* 確保 KBDB embed 用的 Vectorize index 存在(issue #7 / T2.4)。
|
||||
* REST `POST /accounts/{id}/vectorize/v2/indexes`(dimensions=768/metric=cosine,對齊 bge-base-en-v1.5)。
|
||||
* REST `POST /accounts/{id}/vectorize/v2/indexes`(dimensions=1024 / metric=cosine,對齊 bge-m3)。
|
||||
* ⚠️ 這行別寫成 `**dimensions=1024**/metric`——`*` 緊接 `/` 會提早關掉 block comment(實撞 TS1127)。
|
||||
* 維度必須與 `kbdb/src/embed.ts` 的 `DEFAULT_EMBED_MODEL` 一致——不一致時 upsert 直接被 CF 拒絕。
|
||||
* 冪等:已存在(CF 回「already exists」類錯)視為成功,不報錯。用 init 已驗的 apiToken+accountId。
|
||||
*/
|
||||
async function ensureVectorizeIndex(ctx: DeployContext): Promise<void> {
|
||||
@@ -405,8 +448,8 @@ async function ensureVectorizeIndex(ctx: DeployContext): Promise<void> {
|
||||
headers: { Authorization: `Bearer ${ctx.apiToken}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: KBDB_VECTORIZE_INDEX,
|
||||
config: { dimensions: 768, metric: 'cosine' },
|
||||
description: 'arcrun KBDB optional embed module (issue #7)',
|
||||
config: { dimensions: 1024, metric: 'cosine' },
|
||||
description: 'arcrun KBDB embed module — bge-m3 1024d (issue #7 / #59)',
|
||||
}),
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"_readme": [
|
||||
"部署目標定義檔(leo 2026-07-22 立)。一個目標=一組『帳號+profile+apiBase+專案名+對外網址』。",
|
||||
"",
|
||||
"為什麼要這個檔:5a16484 把 UI 搬 CF Pages 後,這些值從 worker 環境變數變成部署期參數。",
|
||||
"誰部署誰要記得帶 → 帶漏了就退回預設,而預設值對兩邊都不對。實際踩過的:",
|
||||
" · demo 站漏 CONSOLE_PROFILE=rag → 顯示個人版 7 頁駕駛艙(leo 看到『Mira 介面』的真因)",
|
||||
" · 兩站都漏 ARCRUN_API_BASE → apiBase 空字串 → 前端打自己回 405 → 登不進去",
|
||||
" · 兩個帳號有同名 arcrun-console-ui 專案,wrangler 又登入在 uncle6",
|
||||
" → 不指定帳號直接 deploy 會部到 demo 站上(差點蓋掉)",
|
||||
"",
|
||||
"🔴 第四次(2026-08-08 發現,同一種病換了形式):",
|
||||
" 上面三次的『解』是 deploy.targets.json + build.mjs 在 build 時把 profile/apiBase",
|
||||
" 烤進產物。但 t160(e744ad1)為了清世代債把 build.mjs 整支刪掉、改成直接託管 public/,",
|
||||
" **沒有人把『把宣告值寫進產物』這件事接手過去** ⇒ deploy.mjs 照樣在終端機印",
|
||||
" 『profile:full / apiBase:…leo21c…』,推上去的卻是 public/config.js 裡凍住的",
|
||||
" cypher.arcrun.dev + 凍在 4 頁的 VIEWS。也就是說:",
|
||||
" **`npm run deploy:personal` 會把個人站的 API 打到企業 demo 的後端、頁面砍成 4 頁**,",
|
||||
" 而終端機從頭到尾顯示『成功』。(第三次的 accountId 是靠 env 傳的,倖存;前兩次的解等於被還原。)",
|
||||
"",
|
||||
" → 現在的規矩:**產物由 deploy.mjs 依本檔即時產生(.staging/<目標>),",
|
||||
" 推之前驗產物、推之後驗線上網址**。public/ 裡不再放任何跟目標有關的值。",
|
||||
" · public/config.js 已刪除——它是產物不是原始碼(自架站的 /config.js 由",
|
||||
" arcrun-rag 的 build-ui-bundle 動態產生,不吃這個檔)",
|
||||
" · public/console/index.html 的 VIEWS/HOME 只是本機 preview 的預設值,",
|
||||
" 部署時一律被 _profiles 覆寫,覆寫沒命中就中止部署",
|
||||
"",
|
||||
"🔴 第五次(2026-08-08 同日,leo:「已經發生過一次這個錯誤,把舊版界面上到 prod,",
|
||||
" 你要確定不可再犯」):**組態對 ≠ 世代對**。",
|
||||
" 當天實測:三個對外網址的 apiBase/views/home **三項全過**,",
|
||||
" 但它們跑的是 07-22 那一代的 portal(82,911 bytes、舊金色 serif 品牌、Songti 12 處),",
|
||||
" repo 已是 343,969 bytes 的新品牌世代。**組態全綠、介面落後半個月,沒有任何檢查會叫。**",
|
||||
" → 故 verify-live 加第二層「世代指紋」:逐一抓線上資產、遮掉本來就該隨目標不同的",
|
||||
" 那兩行(VIEWS/HOME),其餘按位元組比對 repo public/。",
|
||||
" 不用關鍵字清單——清單要人維護,而舊世代能無聲上線正是因為沒人記得維護它。",
|
||||
"",
|
||||
"版本差異(leo 2026-07-22 定調):頁面都存在,由 profile 決定顯示哪些。",
|
||||
" personal(full) 個人版:7 頁全開,落地駕駛艙",
|
||||
" enterprise(rag) 企業版:只留 搜尋/工作流/設定/card,落地搜尋頁",
|
||||
" 未來擴充:個人版新用戶上限 1、知識庫權限不可用 → 加在對應目標的欄位裡,別再散進部署指令。",
|
||||
"",
|
||||
"🧊 frozen 欄位(2026-08-08 leo 立):標了 frozen 的目標=**這個帳號的資源不歸我們動**。",
|
||||
" deploy 拒絕部署它,verify 連抓都不抓(不 curl、不探測)。",
|
||||
" 它不是「壞掉所以跳過」,是刻意的邊界;要解凍是人的決定(拿掉欄位並說明理由)。",
|
||||
" 目標本身**保留不刪**——刪掉就變成下一個 AI 眼中「從來沒有過這個站」的失憶。",
|
||||
"",
|
||||
"用法:npm run deploy:personal",
|
||||
" npm run deploy:personal -- --dry-run (只產出並驗產物,不推)",
|
||||
" npm run verify (不部署,只驗線上:組態=宣告值、世代=當代)",
|
||||
" npm run verify -- --url <網址> (只問某個網址:它跑的是不是當代的)"
|
||||
],
|
||||
|
||||
"_profiles": {
|
||||
"full": {
|
||||
"description": "個人版:7 頁全開,落地駕駛艙",
|
||||
"views": ["cockpit", "search", "card", "workflows", "creds", "inbox", "settings"],
|
||||
"home": "cockpit"
|
||||
},
|
||||
"rag": {
|
||||
"description": "企業版:搜尋/card/工作流/設定,落地搜尋頁",
|
||||
"views": ["search", "card", "workflows", "settings"],
|
||||
"home": "search"
|
||||
}
|
||||
},
|
||||
|
||||
"personal": {
|
||||
"description": "leo 私人實例(原 Mira)。入口 mira.uncle6.me → leo21c worker。",
|
||||
"accountId": "51a01bfa2665bd7bc3fd080dc40cf3e1",
|
||||
"projectName": "arcrun-console-ui",
|
||||
"profile": "full",
|
||||
"brand": "Arcrun",
|
||||
"apiBase": "https://arcrun-cypher-executor.leo21c.workers.dev",
|
||||
"verifyUrls": ["https://mira.uncle6.me", "https://arcrun-console-ui.pages.dev"],
|
||||
"limits": {
|
||||
"maxUsers": 1,
|
||||
"libraryPermissions": false
|
||||
}
|
||||
},
|
||||
|
||||
"enterprise": {
|
||||
"frozen": "leo 2026-08-08:「要看範例只在 youlin 網站,不要去碰 uncle6」——這站是 uncle6 帳號的資源,已廢。不更新、不下架、不探測。要動它是 leo 的閘。",
|
||||
"description": "【已凍結・沿革】企業版 demo 站(uncle6 帳號)。保留紀錄用,不是現行部署對象。",
|
||||
"accountId": "58309bb90fd93ad6d0fe0aae99170e9d",
|
||||
"projectName": "arcrun-console-ui",
|
||||
"profile": "rag",
|
||||
"brand": "Arcrun",
|
||||
"apiBase": "https://cypher.arcrun.dev",
|
||||
"verifyUrls": ["https://rag-demo.arcrun.dev"],
|
||||
"limits": {
|
||||
"maxUsers": null,
|
||||
"libraryPermissions": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "arcrun-console-ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Arcrun Console / Portal 靜態前端——public/ 是唯一世代真身(t160:舊 src/+build 已 git rm);部署時由 deploy.mjs 依 deploy.targets.json 產出 .staging/<目標> 再推",
|
||||
"scripts": {
|
||||
"deploy": "node scripts/deploy.mjs",
|
||||
"deploy:personal": "node scripts/deploy.mjs personal",
|
||||
"verify": "node scripts/verify-live.mjs",
|
||||
"preview": "node scripts/deploy.mjs personal --dry-run && npx serve .staging/personal"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
@@ -0,0 +1,300 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-Hant">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Arcrun 駕駛艙</title>
|
||||
<script>
|
||||
// 主題預載(防閃色):預設淺色(leo 2026-07-04 明示),與 /console 共用同一 localStorage key
|
||||
document.documentElement.setAttribute('data-theme', (function () {
|
||||
try { return localStorage.getItem('arcrun_console_theme') === 'dark' ? 'dark' : 'light'; } catch (e) { return 'light'; }
|
||||
})());
|
||||
</script>
|
||||
<style>
|
||||
/* Mira Console 定稿視覺(紙感「2a」,Mira Style Guide 2026-07-04):
|
||||
紙紋底 repeating-linear-gradient、明體標題級聯、琥珀強調、呼吸狀態球嵌單字。
|
||||
2026-07-04 二輪:CSS custom properties 兩份色板——預設淺色(宣紙米白+墨字),深色=原定稿暖黑不動。 */
|
||||
* { box-sizing: border-box; }
|
||||
:root {
|
||||
--paper-a: #f4eddc; --paper-b: #f1e9d6;
|
||||
--ink: #2f2a20; --ink-rgb: 30,24,14;
|
||||
--amber: #8a5f1e; --amber-rgb: 138,95,30;
|
||||
--ok: #1d7a48; --ok-rgb: 29,122,72;
|
||||
--err: #b03a26; --err-rgb: 176,58,38;
|
||||
--track: rgba(30,24,14,.12);
|
||||
}
|
||||
:root[data-theme="dark"] {
|
||||
--paper-a: #191410; --paper-b: #1b1611;
|
||||
--ink: #ede4d3; --ink-rgb: 237,228,211;
|
||||
--amber: #e8b45a; --amber-rgb: 232,180,90;
|
||||
--ok: #7fe0a8; --ok-rgb: 63,190,120;
|
||||
--err: #e58575; --err-rgb: 217,95,76;
|
||||
--track: rgba(255,255,255,.08);
|
||||
}
|
||||
html, body { margin: 0; background: repeating-linear-gradient(0deg,var(--paper-a) 0px,var(--paper-a) 3px,var(--paper-b) 3px,var(--paper-b) 4px); color: var(--ink);
|
||||
font-family: -apple-system, "PingFang TC", "Microsoft JhengHei", system-ui, sans-serif; font-size: 16px; -webkit-font-smoothing: antialiased; }
|
||||
.serif { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; }
|
||||
main { max-width: 560px; margin: 0 auto; padding: 0 20px 40px; }
|
||||
.pagehead { padding: 22px 2px 14px; border-bottom: 2px solid rgba(var(--amber-rgb),.4); display: flex; justify-content: space-between; align-items: baseline; }
|
||||
.pagehead .title { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 23px; letter-spacing: .2em; }
|
||||
.pagehead .title small { font-size: 14px; letter-spacing: .3em; color: rgba(var(--ink-rgb),.5); }
|
||||
.pagehead .date { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 14px; color: rgba(var(--ink-rgb),.55); }
|
||||
.orb-row { display: flex; align-items: center; gap: 20px; padding: 26px 2px 20px; }
|
||||
.orb { width: 84px; height: 84px; border-radius: 50%; flex: none; display: grid; place-items: center; }
|
||||
.orb span { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 30px; font-weight: 600; color: rgba(10,20,14,.85); text-shadow: 0 1px 0 rgba(255,255,255,.25); }
|
||||
.orb-title { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 23px; font-weight: 600; }
|
||||
.orb-sub { margin-top: 5px; font-size: 15px; color: rgba(var(--ink-rgb),.6); line-height: 1.55; }
|
||||
@keyframes breatheGreen { 0%,100% { box-shadow: 0 0 24px 6px rgba(var(--ok-rgb),.35); } 50% { box-shadow: 0 0 42px 14px rgba(var(--ok-rgb),.55); } }
|
||||
@keyframes breatheAmber { 0%,100% { box-shadow: 0 0 24px 6px rgba(var(--amber-rgb),.35); } 50% { box-shadow: 0 0 42px 14px rgba(var(--amber-rgb),.6); } }
|
||||
@keyframes breatheRed { 0%,100% { box-shadow: 0 0 24px 6px rgba(var(--err-rgb),.4); } 50% { box-shadow: 0 0 44px 16px rgba(var(--err-rgb),.65); } }
|
||||
.bricks { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.brick { padding: 16px; border-radius: 12px; }
|
||||
.brick.amber { background: rgba(var(--amber-rgb),.07); border: 1px solid rgba(var(--amber-rgb),.22); }
|
||||
.brick.plain { background: rgba(var(--ink-rgb),.04); border: 1px solid rgba(var(--ink-rgb),.14); }
|
||||
.brick .lbl { font-size: 13.5px; color: rgba(var(--ink-rgb),.55); margin-bottom: 6px; }
|
||||
.brick .num { font-family: ui-monospace, Menlo, monospace; font-size: 26px; color: var(--amber); }
|
||||
.brick .num small { font-size: 15px; color: rgba(var(--ink-rgb),.5); }
|
||||
.bar { margin-top: 10px; height: 6px; border-radius: 3px; background: var(--track); }
|
||||
.bar > i { display: block; height: 100%; border-radius: 3px; background: linear-gradient(90deg,#b98330,#e8b45a); transition: width .6s; }
|
||||
.wait-box { margin-top: 14px; padding: 20px; border-radius: 12px; border: 1px dashed rgba(var(--ok-rgb),.3); background: rgba(var(--ok-rgb),.05); }
|
||||
.wait-box.has { border-color: rgba(var(--amber-rgb),.45); background: rgba(var(--amber-rgb),.05); }
|
||||
.wait-head { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 16px; letter-spacing: .2em; color: rgba(var(--ink-rgb),.6); margin-bottom: 10px; text-align: center; }
|
||||
.wait-none { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 20px; color: var(--ok); letter-spacing: .08em; text-align: center; }
|
||||
.wait-item { display: flex; align-items: center; gap: 12px; padding: 12px 14px; margin-top: 8px; border-radius: 10px; background: rgba(var(--amber-rgb),.1); border: 1px solid rgba(var(--amber-rgb),.3); font-size: 16px; line-height: 1.5; }
|
||||
.wait-item .dm { color: var(--amber); font-size: 17px; flex: none; }
|
||||
.wait-meta { margin-top: 10px; text-align: center; font-size: 12.5px; color: rgba(var(--ink-rgb),.45); line-height: 1.7; }
|
||||
.wait-meta .warn { color: var(--err); }
|
||||
.subhead { display: flex; justify-content: space-between; align-items: baseline; margin: 24px 0 10px; }
|
||||
.subhead .t { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 16px; letter-spacing: .2em; color: rgba(var(--ink-rgb),.6); }
|
||||
.subhead .m { font-size: 13px; color: rgba(var(--ink-rgb),.4); }
|
||||
ul.route { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
|
||||
ul.route li { display: flex; align-items: flex-start; gap: 12px; padding: 13px 16px; border-radius: 11px; background: rgba(var(--ink-rgb),.045); border: 1px solid transparent; font-size: 16px; line-height: 1.4; }
|
||||
ul.route li.doing { background: rgba(var(--amber-rgb),.09); border-color: rgba(var(--amber-rgb),.3); }
|
||||
ul.route li .ic { flex: none; font-size: 15px; margin-top: 2px; }
|
||||
ul.route li.done { color: rgba(var(--ink-rgb),.65); }
|
||||
ul.route li.done .ic { color: var(--ok); }
|
||||
ul.route li.doing .ic { color: var(--amber); }
|
||||
ul.route li.todo { color: rgba(var(--ink-rgb),.6); }
|
||||
ul.route li.todo .ic { color: rgba(var(--ink-rgb),.35); }
|
||||
ul.route li.blocked .ic { color: var(--err); }
|
||||
ul.route.faded li { opacity: .55; }
|
||||
.sys { margin-top: 6px; display: flex; flex-direction: column; gap: 6px; }
|
||||
.sys .row { display: flex; justify-content: space-between; align-items: baseline; padding: 10px 14px; border-radius: 10px; background: rgba(var(--ink-rgb),.04); border: 1px solid rgba(var(--ink-rgb),.12); font-size: 14.5px; }
|
||||
.sys .row .k { color: rgba(var(--ink-rgb),.6); }
|
||||
.sys .row .v { font-family: ui-monospace, Menlo, monospace; font-size: 14px; }
|
||||
.sys .ok { color: var(--ok); }
|
||||
.sys .bad { color: var(--err); }
|
||||
.sys .off { color: rgba(var(--ink-rgb),.5); }
|
||||
.muted { color: rgba(var(--ink-rgb),.45); font-size: 14px; }
|
||||
.err { color: var(--err); font-size: 14px; }
|
||||
.stamp { margin: 16px 0 8px; text-align: center; font-size: 12.5px; color: rgba(var(--ink-rgb),.35); line-height: 1.8; }
|
||||
.enter { display: block; text-align: center; font-size: 13.5px; color: rgba(var(--amber-rgb),.75); text-decoration: none; margin-top: 6px; }
|
||||
.theme-btn { flex: none; margin-left: 12px; width: 34px; height: 34px; border-radius: 50%; border: 1px solid rgba(var(--ink-rgb),.25); background: none; color: rgba(var(--ink-rgb),.65); font-size: 16px; cursor: pointer; line-height: 1; align-self: center; }
|
||||
</style>
|
||||
<script src="/config.js"></script>
|
||||
<script>
|
||||
// 2026-08-01(arcrun-rag#10 同族):拔掉寫死中央位址的 fallback。
|
||||
// apiBase 由 worker 動態產生的 /config.js 注入;缺它就讓它明顯壞掉,
|
||||
// **不要靜默把請求(可能含金鑰)送去中央實例**。
|
||||
window.ARCRUN_API_BASE = (window.ARCRUN_CONFIG && window.ARCRUN_CONFIG.apiBase) || "";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="pagehead">
|
||||
<div class="title serif">Arcrun<small> 駕駛艙</small></div>
|
||||
<div style="display:flex;align-items:baseline">
|
||||
<div class="date serif" id="date-str"></div>
|
||||
<button class="theme-btn" id="theme-btn" title="切換深/淺色">☾</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="orb-row">
|
||||
<div class="orb" id="orb" style="background:radial-gradient(circle at 36% 30%,#8fe8b4,#3fbe78 55%,#22754a 100%)"><span id="orb-char">…</span></div>
|
||||
<div>
|
||||
<div class="orb-title" id="orb-title">載入中</div>
|
||||
<div class="orb-sub" id="orb-sub"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bricks">
|
||||
<div class="brick amber">
|
||||
<div class="lbl">今日完成</div>
|
||||
<div class="num"><span id="done-n">–</span><small> / <span id="total-n">–</span> 件</small></div>
|
||||
<div class="bar"><i id="bar-fill" style="width:0%"></i></div>
|
||||
</div>
|
||||
<div class="brick plain">
|
||||
<div class="lbl">收件匣未處理</div>
|
||||
<div class="num"><span id="inbox-n">–</span><small> 條</small></div>
|
||||
<div class="lbl" style="margin:10px 0 0">來自 Telegram</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wait-box" id="wait-box">
|
||||
<div class="wait-head">等你的事</div>
|
||||
<div id="wait-body" class="wait-none">載入中…</div>
|
||||
<div class="wait-meta" id="wait-meta"></div>
|
||||
</div>
|
||||
<div class="subhead"><span class="t">今日路線</span><span class="m" id="route-m"></span></div>
|
||||
<ul class="route" id="today-list"><li class="todo"><span class="ic">○</span>載入中…</li></ul>
|
||||
<div class="subhead" id="week-head" style="display:none"><span class="t">本週</span></div>
|
||||
<ul class="route" id="week-list"></ul>
|
||||
<div class="subhead"><span class="t">系統狀況</span><span class="m">live 健康信號</span></div>
|
||||
<div class="sys" id="sys-list"><div class="row"><span class="k">載入中…</span></div></div>
|
||||
<div class="stamp" id="stamp">每 60 秒自動刷新</div>
|
||||
<a class="enter" href="/console">進入完整控制台 ›</a>
|
||||
</main>
|
||||
<script>
|
||||
(function () {
|
||||
var API_BASE = window.ARCRUN_API_BASE || '';
|
||||
// 台北時間 helper(lib/taipei-time.ts 注入,與 server 判定同一套——顯示不隨看的裝置時區漂移)
|
||||
var TAIPEI_OFFSET_MS = 28800000; // UTC+8,台北無 DST
|
||||
function tpePad(n) { n = String(n); return n.length < 2 ? '0' + n : n; }
|
||||
function taipeiDayKey(ms) { return new Date(ms + TAIPEI_OFFSET_MS).toISOString().slice(0, 10); }
|
||||
function taipeiDateStr(ms) { return taipeiDayKey(ms); }
|
||||
function taipeiDateTimeStr(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return taipeiDayKey(ms) + ' ' + tpePad(d.getUTCHours()) + ':' + tpePad(d.getUTCMinutes()); }
|
||||
function taipeiTimeStr(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return tpePad(d.getUTCHours()) + ':' + tpePad(d.getUTCMinutes()) + ':' + tpePad(d.getUTCSeconds()); }
|
||||
function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return { month: d.getUTCMonth() + 1, day: d.getUTCDate() }; }
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const LIGHT = {
|
||||
green: { ch: '安', title: '系統運轉中', grad: 'radial-gradient(circle at 36% 30%,#8fe8b4,#3fbe78 55%,#22754a 100%)', anim: 'breatheGreen' },
|
||||
yellow: { ch: '趕', title: '落後趕工中', grad: 'radial-gradient(circle at 36% 30%,#f2d194,#e8b45a 55%,#8a5f1e 100%)', anim: 'breatheAmber' },
|
||||
red: { ch: '滯', title: '卡住或斷訊', grad: 'radial-gradient(circle at 36% 30%,#f0a094,#d95f4c 55%,#7e2c20 100%)', anim: 'breatheRed' }
|
||||
};
|
||||
const ICONS = { done: '✓', doing: '◐', todo: '○', blocked: '●' };
|
||||
function esc(s) {
|
||||
return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||
}
|
||||
function taskLine(t) {
|
||||
const cls = ICONS[t.status] ? t.status : 'blocked';
|
||||
const ic = ICONS[t.status] || '●';
|
||||
return '<li class="' + cls + '"><span class="ic">' + ic + '</span><span>' + esc(t.title) + '</span></li>';
|
||||
}
|
||||
function humanAge(m) {
|
||||
if (m == null || m < 0) return '時間不明';
|
||||
if (m < 60) return m + ' 分鐘前';
|
||||
if (m < 2880) return Math.round(m / 60) + ' 小時前';
|
||||
return Math.round(m / 1440) + ' 天前';
|
||||
}
|
||||
const CNUM = ['零','一','二','三','四','五','六','七','八','九','十'];
|
||||
function cnDay(n) { return n <= 10 ? CNUM[n] : (n < 20 ? '十' + (n % 10 ? CNUM[n % 10] : '') : CNUM[Math.floor(n / 10)] + '十' + (n % 10 ? CNUM[n % 10] : '')); }
|
||||
// 頁首日期=台北日(原本用瀏覽器本地時區,換裝置會漂)
|
||||
const nowTpe = taipeiMonthDay(Date.now());
|
||||
$('date-str').textContent = CNUM[nowTpe.month] + '月' + cnDay(nowTpe.day) + '日';
|
||||
// 深/淺切換(與 /console 共用 arcrun_console_theme;預設淺色)
|
||||
function syncThemeBtn() { $('theme-btn').textContent = document.documentElement.getAttribute('data-theme') === 'dark' ? '☀' : '☾'; }
|
||||
$('theme-btn').addEventListener('click', () => {
|
||||
const next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||||
document.documentElement.setAttribute('data-theme', next);
|
||||
try { localStorage.setItem('arcrun_console_theme', next); } catch (e) { /* 私密模式忽略 */ }
|
||||
syncThemeBtn();
|
||||
});
|
||||
syncThemeBtn();
|
||||
// fetch 失敗(斷網)的裸訊息 → 友善誠實文案;60 秒定時器常駐,網路恢復自動刷回
|
||||
function friendlyErr(e) {
|
||||
const m = e && e.message ? String(e.message) : String(e);
|
||||
return /failed to fetch|load failed|networkerror|network request failed/i.test(m) ? '連線中斷' : m;
|
||||
}
|
||||
function sysRow(k, v, cls) {
|
||||
return '<div class="row"><span class="k">' + esc(k) + '</span><span class="v ' + cls + '">' + esc(v) + '</span></div>';
|
||||
}
|
||||
async function load() {
|
||||
try {
|
||||
const res = await fetch(API_BASE + '/console/dashboard-data');
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
const d = await res.json();
|
||||
const cfg = LIGHT[d.light] || LIGHT.green;
|
||||
const orb = $('orb');
|
||||
orb.style.background = cfg.grad;
|
||||
orb.style.animation = cfg.anim + ' 3.4s ease-in-out infinite';
|
||||
$('orb-char').textContent = cfg.ch;
|
||||
$('orb-title').textContent = cfg.title;
|
||||
$('orb-sub').textContent = (d.last_beat
|
||||
? d.last_beat.actor + '・' + d.last_beat.ago_minutes + ' 分鐘前' + (d.last_beat.note ? '・' + d.last_beat.note : '')
|
||||
: '尚無心跳資料') + (d.light !== 'green' && d.light_reason ? '(' + d.light_reason + ')' : '');
|
||||
const done = d.today_done || 0, total = d.today_total || 0;
|
||||
$('done-n').textContent = done; $('total-n').textContent = total;
|
||||
$('bar-fill').style.width = (total ? Math.round((done / total) * 100) : 0) + '%';
|
||||
$('inbox-n').textContent = d.inbox_new || 0;
|
||||
// ── 等你的事:來源 + 維護時間攤開講,stale 一定警示 ──
|
||||
const wb = $('wait-box'), body = $('wait-body'), wmeta = $('wait-meta');
|
||||
const wm = d.waiting_meta || {};
|
||||
if (d.waiting && d.waiting.length) {
|
||||
wb.classList.add('has');
|
||||
body.className = '';
|
||||
body.innerHTML = d.waiting.map((w) =>
|
||||
'<div class="wait-item"><span class="dm">' + (w.urgency ? esc(w.urgency) : '◆') + '</span><span>' +
|
||||
(w.id ? '<b>#' + esc(w.id) + '</b> ' : '') + esc(w.title) + '</span></div>').join('');
|
||||
} else {
|
||||
wb.classList.remove('has');
|
||||
body.className = 'wait-none';
|
||||
body.textContent = wm.source === 'none' ? '(管線未接)' : '無,你不用做任何事';
|
||||
}
|
||||
let metaTxt = '';
|
||||
if (wm.source === 'gitea_sprint') {
|
||||
metaTxt = '來源:sprint 等leo清單(' + esc((wm.sprint_files || []).join('、')) + ')・清單維護於 ' + humanAge(wm.updated_ago_minutes);
|
||||
if (wm.stale) metaTxt += '<br><span class="warn">⚠ 清單超過 2 天沒維護,可能過時</span>';
|
||||
} else if (wm.source === 'kbdb_dash_wait') {
|
||||
metaTxt = '<span class="warn">⚠ ' + esc(wm.note || 'dash_wait 殘資料') + '・上次寫入 ' + humanAge(wm.updated_ago_minutes) + ',可能過時</span>';
|
||||
} else {
|
||||
metaTxt = '<span class="warn">管線未接:Gitea sprint 清單與 dash_wait 皆無資料</span>';
|
||||
}
|
||||
wmeta.innerHTML = metaTxt;
|
||||
// ── 今日路線:sprint 任務板優先(來源攤開講);dash_task fallback 沿舊誠實降級 ──
|
||||
const rm = d.route_meta || {};
|
||||
const today = (d.tasks || []).filter((t) => t.scope === 'today');
|
||||
const week = (d.tasks || []).filter((t) => t.scope === 'week');
|
||||
if (rm.source === 'gitea_sprint_board') {
|
||||
$('route-m').textContent = '來源 sprint 任務板・更新於 ' + humanAge(rm.updated_ago_minutes);
|
||||
$('today-list').className = 'route';
|
||||
const staleHead = rm.is_today ? '' :
|
||||
'<li class="todo"><span class="ic">○</span><span class="muted">⚠ 今日任務板未更新(最後 ' + humanAge(rm.updated_ago_minutes) + ')——以下是板上現況</span></li>';
|
||||
$('today-list').innerHTML = staleHead + (today.length
|
||||
? today.map(taskLine).join('')
|
||||
: '<li class="todo"><span class="ic">○</span><span class="muted">任務板上沒有可解析的事項</span></li>');
|
||||
} else if (rm.is_today) {
|
||||
$('route-m').textContent = '更新於 ' + humanAge(rm.updated_ago_minutes);
|
||||
$('today-list').className = 'route';
|
||||
$('today-list').innerHTML = today.length ? today.map(taskLine).join('') : '<li class="todo"><span class="ic">○</span><span class="muted">今日無排定項目</span></li>';
|
||||
} else if (today.length) {
|
||||
$('route-m').textContent = '最後路線・' + humanAge(rm.updated_ago_minutes) + '寫入';
|
||||
$('today-list').className = 'route faded';
|
||||
$('today-list').innerHTML =
|
||||
'<li class="todo"><span class="ic">○</span><span class="muted">今日尚無路線寫入——以下是 ' + humanAge(rm.updated_ago_minutes) +
|
||||
'的殘留路線(sprint 任務板→dashboard 投影管線未接,等leo清單#15 裁決中)</span></li>' + today.map(taskLine).join('');
|
||||
} else {
|
||||
$('route-m').textContent = '';
|
||||
$('today-list').className = 'route';
|
||||
$('today-list').innerHTML = '<li class="todo"><span class="ic">○</span><span class="muted">無資料——dash_task 管線未接</span></li>';
|
||||
}
|
||||
$('week-head').style.display = week.length ? '' : 'none';
|
||||
$('week-list').innerHTML = week.map(taskLine).join('');
|
||||
// ── 系統狀況 + 總庫規模(全 live,讀不到就標讀不到)──
|
||||
const sys = d.system || {}, kb = d.kb || {};
|
||||
const rows = [];
|
||||
rows.push(sysRow('KBDB 基本盤', sys.kbdb_ok ? '● 正常' : '● 打不通', sys.kbdb_ok ? 'ok' : 'bad'));
|
||||
if (sys.embed) {
|
||||
rows.push(sys.embed.enabled
|
||||
? sysRow('語意嵌入', '● 啟用(已嵌 ' + (sys.embed.embedded ?? '?') + '・待嵌 ' + (sys.embed.pending ?? '?') + ')', 'ok')
|
||||
: sysRow('語意嵌入', '○ 停用(已嵌 ' + (sys.embed.embedded ?? '?') + '・待嵌 ' + (sys.embed.pending ?? '?') + ')', 'bad'));
|
||||
} else {
|
||||
rows.push(sysRow('語意嵌入', '狀態讀不到', 'off'));
|
||||
}
|
||||
rows.push(sys.graph && sys.graph.ok
|
||||
? sysRow('知識圖譜', '● 正常・三元組 ' + (sys.graph.triplets == null ? '?' : sys.graph.triplets), 'ok')
|
||||
: sysRow('知識圖譜', '● 打不通', 'bad'));
|
||||
rows.push(sysRow('工作流', sys.workflow_total == null ? '讀不到' : sys.workflow_total + ' 條', sys.workflow_total == null ? 'off' : ''));
|
||||
// 精耕層 wiki 卡(leo 2026-07-07 裁:14-E 遺產總數 deprecated 不再顯示,只顯示真的新的;
|
||||
// 三元組/已嵌入 已各有一列)
|
||||
rows.push(sysRow('精耕層 wiki 卡', kb.wiki_card_total == null ? '讀不到' : kb.wiki_card_total + ' 張', kb.wiki_card_total == null ? 'off' : ''));
|
||||
$('sys-list').innerHTML = rows.join('');
|
||||
$('stamp').innerHTML = '每 60 秒自動刷新・上次 ' + esc(taipeiTimeStr(Date.parse(d.generated_at))) + '(台北)<br>此頁不含機敏內容,免登入';
|
||||
} catch (e) {
|
||||
$('orb-char').textContent = '?';
|
||||
$('orb-title').textContent = '讀不到狀態';
|
||||
$('orb-sub').innerHTML = '<span class="err">' + esc(friendlyErr(e)) + '・每 60 秒自動重試</span>';
|
||||
}
|
||||
}
|
||||
load();
|
||||
setInterval(load, 60000);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,68 +1,9 @@
|
||||
/**
|
||||
* Mira Console 完整版(Arcrun#3 console 系,2026-07-04 總管派工)
|
||||
*
|
||||
* 視覺:claude design 定稿「紙感暖黑 2a」(system-dev/docs/6-user/Mira Console 設計規劃/
|
||||
* Mira Console.dc.html + Mira Style Guide.dc.html)——紙紋底 repeating-linear-gradient、
|
||||
* 明體標題(Songti TC 級聯)、琥珀 var(--amber) 強調、墨綠呼吸球嵌「安」字、正文 ≥16px、
|
||||
* 手機優先 390px + ≥1024px 側欄、頁面本體不出水平捲軸。
|
||||
*
|
||||
* 資訊架構:brief 8 頁(docs/1-vision/mira-console-design-brief.md)=
|
||||
* 1 登入/首次設定 2 駕駛艙 3 總庫搜尋 4 卡片詳頁+關聯 5 工作流 6 憑證管理 7 收件匣 8 設定。
|
||||
* 單檔 HTML + 原生 JS hash routing(#/cockpit …),零外部資源、零 build step。
|
||||
*
|
||||
* 薄殼(rule 07):零業務邏輯,只 fetch 既有 API 渲染。各頁資料源(誠實原則,無源的顯示
|
||||
* 誠實空狀態,禁假資料):
|
||||
* - 駕駛艙:GET /console/dashboard-data(免登入聚合端點,console-dashboard.ts)
|
||||
* - 總庫搜尋:GET /kbdb/search(mode=semantic 未開 Vectorize 會誠實降級 + capability_hint;
|
||||
* chips 用 entry_type——KBDB 真能篩的欄位,不編造「電腦筆記/手機筆記」等不存在的分類);
|
||||
* 頭部統計來自 GET /console/kb-scale-data(精耕層 wiki 卡/三元組/已嵌入——leo 2026-07-07
|
||||
* 裁:45.8 萬 14-E 搬遷遺產 deprecated 不再當招牌數字;搜尋本身仍搜全庫)
|
||||
* - 卡片詳頁:GET /kbdb/entries/:id;關聯視圖走 GET /kbdb/graph/neighbors/:name
|
||||
* (cypher 代轉 kbdb-graph-plugin,kbdb-proxy.ts;查無 triplet → 「尚無關聯資料」)
|
||||
* - 工作流:GET /webhooks/named(list)+ POST /webhooks/named/:name/trigger(手動觸發)
|
||||
* + GET /workflows/:name/executions(最近執行);釘選存 localStorage
|
||||
* - 憑證:GET /credentials/catalog(D1 目錄唯讀,絕不含密文)+ POST /credentials(新增)
|
||||
* + PUT /credentials/:name(整筆替換);刪除後端未接 D1(T9 範圍)→ 標「即將開通」
|
||||
* - 收件匣:GET /console/inbox-data(需 session——訊息原文屬機敏,計數才免登入)
|
||||
* - 設定:vectorize 狀態探測(讀 /kbdb/search 回應的 mode/capability_hint,無新端點;
|
||||
* 開關本體需部署端 config kbdb_embed + acr update → 誠實文案講清楚、不假裝能遠端開)
|
||||
* + MCP token TTL 誠實佔位(/console/settings-data;可調功能=Arcrun#19)
|
||||
* + /console/setup/reset 換帳密 + 系統資訊(精耕層規模)
|
||||
*
|
||||
* 2026-07-07 fix/console-truth-audit(總管稽核派工 + leo 裁決):
|
||||
* 1. 全站日期/時間顯示統一 Asia/Taipei(lib/taipei-time.ts,與駕駛艙 #36 台北日判定同套;
|
||||
* 原本用瀏覽器本地 getter,換裝置就換日期;憑證頁加顯時分——7/6 22:57 建立不再被誤讀)。
|
||||
* 2. 總庫搜尋/設定頁頭部統計改精耕層(458,732 是真數字——cypher 與 base 一手同值實證——
|
||||
* 但 leo 裁 14-E 遺產 deprecated 不再顯示)。
|
||||
* 3. 駕駛艙「今日完成/今日路線」接 sprint 任務板真資料(dashboard-data 端,同 #36 模式)。
|
||||
*
|
||||
* 認證模型沿 v1(console-auth.ts):單一管理員 email+password,session token 在 localStorage,
|
||||
* 實際打 /kbdb/* 用後端回的固定租戶字串(CONSOLE_TENANT)。駕駛艙免登入版在 /console/dashboard。
|
||||
*
|
||||
* v0 的 components/recipes 查詢區(Arcrun#3 原搜尋台)收進「工作流」頁下方折疊區,不砍功能。
|
||||
*
|
||||
* 2026-07-04 leo 實測三回饋(總管派工二輪):
|
||||
* 1. 深/淺主題:CSS custom properties 兩份色板(:root=淺色紙感宣紙米白+墨字,
|
||||
* :root[data-theme=dark]=原定稿暖黑不動),預設淺色,選擇存 localStorage
|
||||
* (key arcrun_console_theme),head 預載腳本防閃色。切換入口=登入/首次設定頁小鈕、
|
||||
* 側欄項、設定頁正式開關。金琥珀例外(按鈕漸層/狀態球/toast/gcenter)兩版不變。
|
||||
* 2. 登入/首次設定置中:.authwrap.view.on 補 display:flex(原被 .view.on 的 block 蓋掉 → 靠左)。
|
||||
* 3. fetch 韌性:friendlyErr() 把裸 Failed to fetch / Load failed 轉「連線中斷」誠實文案;
|
||||
* 駕駛艙 60 秒定時器常駐,網路恢復自動刷回。
|
||||
*/
|
||||
import { Hono } from 'hono';
|
||||
import type { Bindings } from '../types';
|
||||
import { TAIPEI_CLIENT_JS } from '../lib/taipei-time';
|
||||
|
||||
export const consoleRouter = new Hono<{ Bindings: Bindings }>();
|
||||
|
||||
function renderConsoleHtml(registryBase: string): string {
|
||||
return `<!doctype html>
|
||||
<!doctype html>
|
||||
<html lang="zh-Hant">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Mira Console</title>
|
||||
<title>Arcrun Console</title>
|
||||
<script>
|
||||
// 主題預載(防閃色):預設淺色(leo 2026-07-04 明示),選擇存 localStorage
|
||||
document.documentElement.setAttribute('data-theme', (function () {
|
||||
@@ -231,7 +172,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
.gcenter span { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 13.5px; font-weight: 600; line-height: 1.35; color: #241804; word-break: break-word; }
|
||||
.nbrow { display: flex; align-items: center; gap: 11px; padding: 12px 14px; border-radius: 10px; cursor: pointer; background: rgba(var(--ink-rgb),.045); border: 1px solid rgba(var(--ink-rgb),.1); font-size: 15.5px; }
|
||||
|
||||
/* ── 工作流 / 憑證 / 收件匣 清單 ── */
|
||||
/* ── 工作流 / 憑證 / 分流台 清單 ── */
|
||||
.listcol { display: flex; flex-direction: column; gap: 10px; padding: 16px 0 8px; }
|
||||
.rowhead { display: flex; align-items: center; gap: 12px; padding: 16px 18px; cursor: pointer; }
|
||||
.rowbody { padding: 0 18px 18px; border-top: 1px solid rgba(var(--ink-rgb),.1); }
|
||||
@@ -241,10 +182,34 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
.urlbox .u { font-family: ui-monospace, Menlo, monospace; font-size: 13.5px; color: var(--amber); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
||||
.avatar { width: 38px; height: 38px; border-radius: 10px; background: rgba(var(--amber-rgb),.1); border: 1px solid rgba(var(--amber-rgb),.25); display: grid; place-items: center; font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 17px; color: var(--amber); flex: none; }
|
||||
.maskline { display: flex; align-items: center; gap: 10px; padding: 14px; border-radius: 10px; background: var(--well); border: 1px solid rgba(var(--ink-rgb),.1); }
|
||||
.inbox-item { padding: 16px 18px; border-radius: 12px; }
|
||||
.inbox-item.new { background: rgba(var(--amber-rgb),.07); border: 1px solid rgba(var(--amber-rgb),.28); }
|
||||
.inbox-item.done { background: rgba(var(--ink-rgb),.04); border: 1px solid rgba(var(--ink-rgb),.1); }
|
||||
.pulse-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--amber); flex: none; animation: breatheAmber 2.6s ease-in-out infinite; }
|
||||
/* 分流台(Arcrun#9):三欄 80/15/5 手機直排;欄色點 ai=綠 collab=琥珀 leo=紅 */
|
||||
.tri-sec { margin-top: 24px; }
|
||||
.tri-sechead { display: flex; align-items: baseline; gap: 9px; }
|
||||
.tri-sechead .dot { flex: none; width: 9px; height: 9px; border-radius: 50%; align-self: center; }
|
||||
.tri-sec.ai .dot { background: var(--ok); } .tri-sec.collab .dot { background: var(--amber); } .tri-sec.leo .dot { background: var(--err); }
|
||||
.tri-sechead .nm { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 17px; letter-spacing: .14em; }
|
||||
.tri-sechead .pc { font-size: 12.5px; color: rgba(var(--ink-rgb),.4); }
|
||||
.tri-sechead .ct { margin-left: auto; font-family: ui-monospace, Menlo, monospace; font-size: 14px; color: rgba(var(--ink-rgb),.55); }
|
||||
.tri-item { padding: 14px 16px; border-radius: 12px; margin-top: 9px; }
|
||||
.tri-item.new { background: rgba(var(--amber-rgb),.07); border: 1px solid rgba(var(--amber-rgb),.28); }
|
||||
.tri-sec.leo .tri-item.new { background: rgba(var(--err-rgb),.06); border-color: rgba(var(--err-rgb),.3); }
|
||||
.tri-item.done { background: rgba(var(--ink-rgb),.04); border: 1px solid rgba(var(--ink-rgb),.1); opacity: .7; }
|
||||
.tri-item.done .tx { text-decoration: line-through; color: rgba(var(--ink-rgb),.6); }
|
||||
.tri-item .tx { font-size: 16.5px; line-height: 1.6; word-break: break-word; }
|
||||
.tri-item .mt { margin-top: 9px; display: flex; gap: 7px; align-items: center; flex-wrap: wrap; font-size: 12.5px; color: rgba(var(--ink-rgb),.45); }
|
||||
.tri-item .row { display: flex; gap: 12px; align-items: flex-start; }
|
||||
.tri-item .row .tx { flex: 1; min-width: 0; }
|
||||
/* 勾掉鈕:≥44px 圓形(手機好按),綠框空心=待勾;按下去由後端回寫才算數(誠實,不先假裝成功) */
|
||||
.tri-check { flex: none; width: 44px; height: 44px; border-radius: 50%; cursor: pointer; font-size: 19px; line-height: 1;
|
||||
border: 1.5px solid rgba(var(--ok-rgb),.55); background: rgba(var(--ok-rgb),.06); color: var(--ok); }
|
||||
.tri-check:active { background: rgba(var(--ok-rgb),.22); }
|
||||
.tri-check:disabled { opacity: .45; cursor: default; }
|
||||
/* 還原鈕(誤勾救濟,只出現在「顯示已完成」清單):小顆但仍 44px 高好按 */
|
||||
.tri-restore { flex: none; min-height: 32px; padding: 5px 14px; border-radius: 999px; cursor: pointer; font-size: 13px;
|
||||
border: 1px solid rgba(var(--amber-rgb),.5); background: none; color: var(--amber); }
|
||||
.tri-restore:disabled { opacity: .45; cursor: default; }
|
||||
.tag.red { background: rgba(var(--err-rgb),.12); color: var(--err); }
|
||||
.tag.dim { background: rgba(var(--ink-rgb),.08); color: rgba(var(--ink-rgb),.55); }
|
||||
|
||||
/* ── 設定 ── */
|
||||
.setcol { display: flex; flex-direction: column; gap: 14px; padding: 20px 0 40px; }
|
||||
@@ -254,6 +219,13 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
.switch.on i { transform: translateX(24px); }
|
||||
.kvline { display: flex; justify-content: space-between; gap: 12px; font-size: 15px; margin: 5px 0; }
|
||||
</style>
|
||||
<script src="/config.js"></script>
|
||||
<script>
|
||||
// 2026-08-01(arcrun-rag#10 同族):拔掉寫死中央位址的 fallback。
|
||||
// apiBase 由 worker 動態產生的 /config.js 注入;缺它就讓它明顯壞掉,
|
||||
// **不要靜默把請求(可能含金鑰)送去中央實例**。
|
||||
window.ARCRUN_API_BASE = (window.ARCRUN_CONFIG && window.ARCRUN_CONFIG.apiBase) || "";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -261,7 +233,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
<div class="authwrap view" id="v-login">
|
||||
<div class="authbox">
|
||||
<div>
|
||||
<div class="brand">Mira</div>
|
||||
<div class="brand">Arcrun</div>
|
||||
<div class="brand-sub">私人系統・僅供擁有者進入</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:12px;text-align:left">
|
||||
@@ -271,7 +243,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
<div id="login-status" class="err" style="font-size:14px;min-height:1.2em"></div>
|
||||
</div>
|
||||
<div style="font-size:13.5px;color:rgba(var(--ink-rgb),.4);line-height:1.7">這是一個人的智慧總部。<br>若你不是擁有者,這裡沒有你要找的東西。</div>
|
||||
<a href="/console/dashboard" style="font-size:13.5px;color:rgba(var(--amber-rgb),.7)">免登入看駕駛艙 ›</a>
|
||||
|
||||
<button class="btn3 themelabel" data-themetoggle style="align-self:center;padding:8px 16px;font-size:13.5px;border-radius:999px">☾ 切深色</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -280,7 +252,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
<div class="authwrap view" id="v-setup">
|
||||
<div class="authbox" style="max-width:440px;text-align:left">
|
||||
<div style="text-align:center">
|
||||
<div class="brand" style="font-size:38px">Mira</div>
|
||||
<div class="brand" style="font-size:38px">Arcrun</div>
|
||||
<div class="serif" style="margin-top:12px;font-size:21px">首次設定</div>
|
||||
<div style="margin-top:8px;font-size:15px;color:rgba(var(--ink-rgb),.55);line-height:1.7">系統偵測到尚未設定擁有者帳號。<br>設定一組 Email 與密碼,之後只有你能進入。</div>
|
||||
</div>
|
||||
@@ -298,12 +270,11 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
<!-- 主應用 -->
|
||||
<div id="shell">
|
||||
<div id="sidenav">
|
||||
<div class="logo">Mira</div>
|
||||
<div class="nav" data-nav="cockpit">駕駛艙</div>
|
||||
<div class="logo">Arcrun</div>
|
||||
|
||||
<div class="nav" data-nav="search">總庫搜尋</div>
|
||||
<div class="nav" data-nav="workflows">工作流</div>
|
||||
<div class="nav" data-nav="creds">憑證管理</div>
|
||||
<div class="nav" data-nav="inbox">收件匣</div>
|
||||
|
||||
<div class="nav" data-nav="settings">設定</div>
|
||||
<div class="nav themelabel" data-themetoggle style="margin-top:8px;font-size:14.5px">☾ 切深色</div>
|
||||
<div class="foot" id="side-foot"></div>
|
||||
@@ -312,7 +283,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
|
||||
<!-- 駕駛艙 -->
|
||||
<div class="view page" id="v-cockpit">
|
||||
<div class="pagehead"><span class="t">Mira<span style="font-size:14px;letter-spacing:.3em;color:rgba(var(--ink-rgb),.5)"> 控制台</span></span><span class="m serif" id="ck-date"></span></div>
|
||||
<div class="pagehead"><span class="t">Arcrun<span style="font-size:14px;letter-spacing:.3em;color:rgba(var(--ink-rgb),.5)"> 控制台</span></span><span class="m serif" id="ck-date"></span></div>
|
||||
<div class="cockgrid">
|
||||
<div>
|
||||
<div class="orb-row">
|
||||
@@ -352,14 +323,22 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 總庫搜尋 -->
|
||||
<!-- 總庫搜尋(library-map SDD M5/R4:搜尋框上方=藏書地圖——先看全館有哪些庫,點庫卡片帶 library filter 進庫搜尋) -->
|
||||
<div class="view page wide" id="v-search">
|
||||
<div class="pagehead"><span class="t">總庫搜尋</span><span class="m mono" id="se-scale"></span></div>
|
||||
<div style="padding-top:20px">
|
||||
<div id="lm-section" style="margin-bottom:24px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:10px">
|
||||
<span class="subhead">藏書地圖</span><span id="lm-meta" style="font-size:13px;color:rgba(var(--ink-rgb),.4)"></span>
|
||||
</div>
|
||||
<div class="kgrid" id="lm-cards"><div class="muted" style="font-size:14px">地圖載入中…</div></div>
|
||||
<div id="lm-bridges"></div>
|
||||
</div>
|
||||
<div class="searchrow">
|
||||
<input id="se-q" placeholder="搜尋知識…" autocomplete="off">
|
||||
<button class="sembtn" id="se-sem">語意</button>
|
||||
</div>
|
||||
<div id="se-libbar"></div>
|
||||
<div class="hscroll" id="se-chips" style="margin-top:12px"></div>
|
||||
<div id="se-banner"></div>
|
||||
<div id="se-count" style="margin:20px 0 12px;font-size:14px;color:rgba(var(--ink-rgb),.5)"></div>
|
||||
@@ -417,10 +396,11 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
<div class="listcol" id="cred-list"><div class="muted">載入中…</div></div>
|
||||
</div>
|
||||
|
||||
<!-- 收件匣 -->
|
||||
<!-- 分流台(原收件匣,Arcrun#9 改裝=全部待辦分流台) -->
|
||||
<div class="view page narrow" id="v-inbox">
|
||||
<div class="pagehead"><span class="t">收件匣</span><span class="m">Telegram 指令</span></div>
|
||||
<div id="ib-out" style="padding-top:6px"><div class="muted" style="padding:14px 0">載入中…</div></div>
|
||||
<div class="pagehead"><span class="t">分流台</span><span class="m">全部待辦・80/15/5</span></div>
|
||||
<div class="hscroll" id="tg-chips" style="margin-top:14px"></div>
|
||||
<div id="tg-out" style="padding-top:2px"><div class="muted" style="padding:14px 0">載入中…</div></div>
|
||||
</div>
|
||||
|
||||
<!-- 設定 -->
|
||||
@@ -437,16 +417,14 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div style="display:flex;align-items:center;gap:14px">
|
||||
<div style="min-width:0">
|
||||
<div style="font-size:17px;font-weight:600">語意搜尋(vectorize)</div>
|
||||
<div id="st-vec" style="margin-top:4px;font-size:14px;line-height:1.65;color:rgba(var(--ink-rgb),.55)">狀態偵測中…</div>
|
||||
</div>
|
||||
<div class="switch" id="st-vec-switch" title="此開關不能遠端改,只如實顯示狀態"><i></i></div>
|
||||
</div>
|
||||
<div style="margin-top:12px;padding:12px 14px;border-radius:10px;background:rgba(var(--amber-rgb),.07);border:1px dashed rgba(var(--amber-rgb),.35);font-size:14px;line-height:1.7;color:rgba(var(--ink-rgb),.65)">
|
||||
誠實提示:開關真相=部署端 <code style="font-size:12.5px">~/.arcrun/config.yaml</code> 的 <code style="font-size:12.5px">kbdb_embed: true</code> + <code style="font-size:12.5px">acr update</code> 重部署(#32 教訓:wrangler 直推改的形態 config 要同步)。Console 只如實顯示狀態,不假裝能遠端開啟。目前狀態:<b style="color:var(--amber)" id="st-vec-state">偵測中…</b>
|
||||
</div>
|
||||
<!-- t36(leo 2026-07-25 定案:語意搜尋預設開啟、不做開關):
|
||||
這裡原本是一個「長得像開關、其實不能按」的唯讀狀態燈(title 自承「不能遠端改」),
|
||||
旁邊還教用戶去部署端改 config.yaml 跑 CLI——對一鍵安裝進來的用戶那是天書,
|
||||
而且安裝器現在裝機時就把語意索引開好了,那段指示已經不成立。
|
||||
改成單純的狀態列:能用就說能用,不能用就說原因,不擺一個按不動的開關讓人誤會。 -->
|
||||
<div style="font-size:17px;font-weight:600">語意搜尋</div>
|
||||
<div id="st-vec" style="margin-top:4px;font-size:14px;line-height:1.65;color:rgba(var(--ink-rgb),.55)">狀態偵測中…</div>
|
||||
<div id="st-vec-hint" style="margin-top:12px;padding:12px 14px;border-radius:10px;background:rgba(var(--amber-rgb),.07);border:1px dashed rgba(var(--amber-rgb),.35);font-size:14px;line-height:1.7;color:rgba(var(--ink-rgb),.65);display:none"></div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div style="font-size:17px;font-weight:600">MCP token 有效期(TTL)</div>
|
||||
@@ -466,12 +444,15 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
<div id="st-pw-status" style="font-size:14px;min-height:1.2em"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel" style="display:flex;align-items:center;gap:12px;cursor:pointer" data-nav="creds">
|
||||
<div>
|
||||
<div style="font-size:17px;font-weight:600">憑證管理</div>
|
||||
<div style="margin-top:4px;font-size:14px;color:rgba(var(--ink-rgb),.55)">只保存目錄與加密值,不可回看</div>
|
||||
|
||||
<div class="panel">
|
||||
<div style="font-size:17px;font-weight:600">Portal 帳號密碼救援</div>
|
||||
<div style="margin-top:4px;font-size:14px;line-height:1.65;color:rgba(var(--ink-rgb),.55)">忘記某個 Portal(RAG 搜尋頁)帳號的密碼,包含你自己那組管理員帳號——不需要先登進 Portal。輸入該帳號的 Email,會產生一組新密碼,只顯示這一次,請立刻抄下並拿去 Portal 登入頁使用。</div>
|
||||
<div style="margin-top:14px;display:flex;flex-direction:column;gap:10px">
|
||||
<input type="email" id="st-portal-recover-email" class="txt" placeholder="Portal 帳號 Email">
|
||||
<button class="btn" id="st-portal-recover-btn">產生新密碼</button>
|
||||
<div id="st-portal-recover-status" style="font-size:14px;min-height:1.2em"></div>
|
||||
</div>
|
||||
<span style="margin-left:auto;color:rgba(var(--amber-rgb),.6);font-size:18px">›</span>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<div style="font-size:17px;font-weight:600;margin-bottom:12px">系統資訊</div>
|
||||
@@ -486,10 +467,10 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
|
||||
<!-- 手機底部導覽 -->
|
||||
<div id="tabbar">
|
||||
<div class="tab" data-nav="cockpit">駕駛艙</div>
|
||||
|
||||
<div class="tab" data-nav="search">搜尋</div>
|
||||
<div class="tab" data-nav="workflows">工作流</div>
|
||||
<div class="tab" data-nav="inbox">收件匣</div>
|
||||
|
||||
<div class="tab" data-nav="settings">設定</div>
|
||||
</div>
|
||||
|
||||
@@ -497,11 +478,18 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var API_BASE = window.ARCRUN_API_BASE || '';
|
||||
'use strict';
|
||||
// 台北時間 helper(lib/taipei-time.ts 注入,與駕駛艙 #36 台北日判定同一套)——
|
||||
// console 所有日期/時間顯示固定 Asia/Taipei,不隨看的裝置時區漂移
|
||||
${TAIPEI_CLIENT_JS}
|
||||
var REGISTRY_BASE = ${JSON.stringify(registryBase)};
|
||||
var TAIPEI_OFFSET_MS = 28800000; // UTC+8,台北無 DST
|
||||
function tpePad(n) { n = String(n); return n.length < 2 ? '0' + n : n; }
|
||||
function taipeiDayKey(ms) { return new Date(ms + TAIPEI_OFFSET_MS).toISOString().slice(0, 10); }
|
||||
function taipeiDateStr(ms) { return taipeiDayKey(ms); }
|
||||
function taipeiDateTimeStr(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return taipeiDayKey(ms) + ' ' + tpePad(d.getUTCHours()) + ':' + tpePad(d.getUTCMinutes()); }
|
||||
function taipeiTimeStr(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return tpePad(d.getUTCHours()) + ':' + tpePad(d.getUTCMinutes()) + ':' + tpePad(d.getUTCSeconds()); }
|
||||
function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return { month: d.getUTCMonth() + 1, day: d.getUTCDate() }; }
|
||||
var REGISTRY_BASE = "https://registry.arcrun.dev";
|
||||
var $ = function (id) { return document.getElementById(id); };
|
||||
|
||||
// ── 狀態 ──
|
||||
@@ -509,6 +497,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
token: localStorage.getItem('arcrun_console_session') || '',
|
||||
tenant: '',
|
||||
chip: '', // entry_type filter('' = 全部)
|
||||
library: '', // library filter('' = 全館;藏書地圖點庫卡片設定,M5/R4)
|
||||
semantic: false,
|
||||
cardId: '',
|
||||
graphCenter: '',
|
||||
@@ -560,7 +549,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
function parseAtMs(v) {
|
||||
if (v == null || v === '') return null;
|
||||
if (typeof v === 'number') return v < 1e12 ? v * 1000 : v;
|
||||
if (/^\\d+$/.test(v)) { var n = Number(v); return n < 1e12 ? n * 1000 : n; }
|
||||
if (/^\d+$/.test(v)) { var n = Number(v); return n < 1e12 ? n * 1000 : n; }
|
||||
var s = /T/.test(v) ? v : String(v).replace(' ', 'T') + 'Z';
|
||||
var ms = Date.parse(s);
|
||||
return isNaN(ms) ? null : ms;
|
||||
@@ -580,31 +569,31 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
// ── 極簡 Markdown 渲染(先全 escape 再上格式,防 XSS)──
|
||||
function mdInline(s) {
|
||||
return s
|
||||
.replace(/\\*\\*([^*]+)\\*\\*/g, '<b>$1</b>')
|
||||
.replace(/(^|[^\\\\])\\x60([^\\x60]+)\\x60/g, '$1<code>$2</code>');
|
||||
.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>')
|
||||
.replace(/(^|[^\\])\x60([^\x60]+)\x60/g, '$1<code>$2</code>');
|
||||
}
|
||||
function mdRender(src) {
|
||||
var lines = String(src || '').split(/\\r?\\n/);
|
||||
var lines = String(src || '').split(/\r?\n/);
|
||||
var out = [], inCode = false, listMode = '';
|
||||
function closeList() { if (listMode) { out.push(listMode === 'ul' ? '</ul>' : '</ol>'); listMode = ''; } }
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var raw = lines[i];
|
||||
if (/^\\s*\\x60\\x60\\x60/.test(raw)) {
|
||||
if (/^\s*\x60\x60\x60/.test(raw)) {
|
||||
closeList();
|
||||
if (inCode) { out.push('</code></pre>'); inCode = false; }
|
||||
else { out.push('<pre><code>'); inCode = true; }
|
||||
continue;
|
||||
}
|
||||
if (inCode) { out.push(esc(raw) + '\\n'); continue; }
|
||||
if (inCode) { out.push(esc(raw) + '\n'); continue; }
|
||||
var line = esc(raw);
|
||||
var h = raw.match(/^(#{1,4})\\s+(.*)$/);
|
||||
var h = raw.match(/^(#{1,4})\s+(.*)$/);
|
||||
if (h) { closeList(); out.push('<h' + h[1].length + '>' + mdInline(esc(h[2])) + '</h' + h[1].length + '>'); continue; }
|
||||
if (/^\\s*>\\s?/.test(line)) { closeList(); out.push('<blockquote>' + mdInline(line.replace(/^\\s*>\\s?/, '')) + '</blockquote>'); continue; }
|
||||
var li = raw.match(/^\\s*[-*+]\\s+(.*)$/);
|
||||
if (/^\s*>\s?/.test(line)) { closeList(); out.push('<blockquote>' + mdInline(line.replace(/^\s*>\s?/, '')) + '</blockquote>'); continue; }
|
||||
var li = raw.match(/^\s*[-*+]\s+(.*)$/);
|
||||
if (li) { if (listMode !== 'ul') { closeList(); out.push('<ul>'); listMode = 'ul'; } out.push('<li>' + mdInline(esc(li[1])) + '</li>'); continue; }
|
||||
var oli = raw.match(/^\\s*\\d+[.)]\\s+(.*)$/);
|
||||
var oli = raw.match(/^\s*\d+[.)]\s+(.*)$/);
|
||||
if (oli) { if (listMode !== 'ol') { closeList(); out.push('<ol>'); listMode = 'ol'; } out.push('<li>' + mdInline(esc(oli[1])) + '</li>'); continue; }
|
||||
if (/^\\s*$/.test(raw)) { closeList(); continue; }
|
||||
if (/^\s*$/.test(raw)) { closeList(); continue; }
|
||||
closeList();
|
||||
out.push('<p>' + mdInline(line) + '</p>');
|
||||
}
|
||||
@@ -614,11 +603,14 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
}
|
||||
|
||||
// ── 路由 ──
|
||||
var VIEWS = ['cockpit', 'search', 'card', 'workflows', 'creds', 'inbox', 'settings'];
|
||||
// VIEWS / HOME 由 server 依 CONSOLE_PROFILE 注入(full=全套 7 頁落地駕駛艙;rag=4 頁落地搜尋)。
|
||||
// 不在 VIEWS 裡的 hash(含被 profile 裁掉的頁)一律導回 HOME——手打 #/cockpit 也進不去。
|
||||
var VIEWS = ["search","card","workflows","settings"];
|
||||
var HOME = "search";
|
||||
function currentRoute() {
|
||||
var h = location.hash.replace(/^#\\/?/, '');
|
||||
var h = location.hash.replace(/^#\/?/, '');
|
||||
var seg = h.split('/');
|
||||
return { view: VIEWS.indexOf(seg[0]) >= 0 ? seg[0] : 'cockpit', arg: seg.slice(1).join('/') };
|
||||
return { raw: seg[0] || '', view: VIEWS.indexOf(seg[0]) >= 0 ? seg[0] : HOME, arg: seg.slice(1).join('/') };
|
||||
}
|
||||
function nav(view, arg) {
|
||||
location.hash = '#/' + view + (arg ? '/' + encodeURIComponent(arg) : '');
|
||||
@@ -631,9 +623,11 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
});
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
var LOADERS = { cockpit: loadCockpit, search: initSearchOnce, card: loadCard, workflows: loadWorkflows, creds: loadCreds, inbox: loadInbox, settings: loadSettings };
|
||||
var LOADERS = { cockpit: loadCockpit, search: initSearchOnce, card: loadCard, workflows: loadWorkflows, creds: loadCreds, inbox: loadTriage, settings: loadSettings };
|
||||
function route() {
|
||||
var r = currentRoute();
|
||||
// 隱藏頁/不存在頁 guard:hash 不在 VIEWS → 改寫成 HOME(hashchange 會再進來一次正常渲染)
|
||||
if (r.raw && r.raw !== r.view) { location.hash = '#/' + r.view; return; }
|
||||
if (r.view === 'card' && r.arg) S.cardId = decodeURIComponent(r.arg);
|
||||
showView(r.view);
|
||||
if (LOADERS[r.view]) LOADERS[r.view]();
|
||||
@@ -657,12 +651,12 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
$('shell').classList.add('on');
|
||||
$('tabbar').classList.add('on');
|
||||
$('side-foot').innerHTML = '租戶 ' + esc(S.tenant) + '<br>' + esc(location.host);
|
||||
if (!location.hash) location.hash = '#/cockpit';
|
||||
if (!location.hash) location.hash = '#/' + HOME;
|
||||
route();
|
||||
}
|
||||
function init() {
|
||||
if (S.token) {
|
||||
fetch('/console/session', { headers: { Authorization: 'Bearer ' + S.token } })
|
||||
fetch(API_BASE + '/console/session', { headers: { Authorization: 'Bearer ' + S.token } })
|
||||
.then(function (r) { return r.ok ? r.json() : Promise.reject(new Error('session 失效')); })
|
||||
.then(function (d) { S.tenant = d.tenant || ''; showApp(); })
|
||||
.catch(function () {
|
||||
@@ -674,7 +668,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
}
|
||||
}
|
||||
function checkConfigured() {
|
||||
fetch('/console/auth-status')
|
||||
fetch(API_BASE + '/console/auth-status')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) { showAuth(d.configured ? 'login' : 'setup'); })
|
||||
.catch(function () { showAuth('login'); });
|
||||
@@ -688,7 +682,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
$('login-submit').addEventListener('click', function () {
|
||||
var email = $('login-email').value.trim(), password = $('login-password').value;
|
||||
$('login-status').textContent = '';
|
||||
fetch('/console/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email, password: password }) })
|
||||
fetch(API_BASE + '/console/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email, password: password }) })
|
||||
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
|
||||
.then(function (x) { if (!x.ok) { $('login-status').textContent = x.d.error || '登入失敗'; return; } handleAuthResponse(x.d); })
|
||||
.catch(function (e) { $('login-status').textContent = '請求失敗:' + friendlyErr(e); });
|
||||
@@ -698,7 +692,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
var email = $('setup-email').value.trim(), pw = $('setup-password').value, pw2 = $('setup-password2').value;
|
||||
if (pw !== pw2) { $('setup-status').textContent = '兩次密碼不一致'; return; }
|
||||
$('setup-status').textContent = '';
|
||||
fetch('/console/setup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email, password: pw }) })
|
||||
fetch(API_BASE + '/console/setup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: email, password: pw }) })
|
||||
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
|
||||
.then(function (x) { if (!x.ok) { $('setup-status').textContent = x.d.error || '設定失敗'; return; } handleAuthResponse(x.d); })
|
||||
.catch(function (e) { $('setup-status').textContent = '請求失敗:' + friendlyErr(e); });
|
||||
@@ -726,7 +720,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
function loadCockpit() {
|
||||
var nowTpe = taipeiMonthDay(Date.now()); // 頁首日期=台北日,不吃裝置時區
|
||||
$('ck-date').textContent = CNUM[nowTpe.month] + '月' + cnDay(nowTpe.day) + '日';
|
||||
fetch('/console/dashboard-data')
|
||||
fetch(API_BASE + '/console/dashboard-data')
|
||||
.then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
||||
.then(function (d) {
|
||||
var cfg = LIGHT[d.light] || LIGHT.green;
|
||||
@@ -812,6 +806,115 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
searchInited = true;
|
||||
renderChips();
|
||||
loadScale();
|
||||
loadLibraryMap();
|
||||
}
|
||||
|
||||
// ── 藏書地圖(library-map SDD M5/R4)──
|
||||
// 資料源:GET /kbdb/map(cypher 代轉 KBDB 基本盤,kbdb-proxy.ts——同 /kbdb/search 慣例)。
|
||||
// 兩段式渲染:先用全館列表(top_entities=名字 top3)立即出卡片,再逐庫拉 /kbdb/map/:library
|
||||
// 補 degree/bridges/commit_hash(庫數少;詳圖拉不到就維持名字版——加分項不擋渲染)。
|
||||
// 空陣列/錯誤=誠實空狀態,只佔地圖區塊,不擋下方搜尋。
|
||||
var LM = { libs: null, details: {} }; // 地圖快取:點卡片重繪「搜尋中」標記時不用重打 API
|
||||
function lmHonest(head, body) {
|
||||
$('lm-cards').innerHTML = '<div class="honest" style="grid-column:1/-1"><div class="h">' + esc(head) + '</div><div class="b">' + body + '</div></div>';
|
||||
$('lm-bridges').innerHTML = '';
|
||||
$('lm-meta').textContent = '';
|
||||
}
|
||||
function lmEntityLine(ents) {
|
||||
// ents:詳圖版 [{name, degree}] 或全館列表版 ['name']——兩形都渲染,degree 有才顯示
|
||||
return (ents || []).slice(0, 5).map(function (t) {
|
||||
var name = typeof t === 'string' ? t : (t && t.name) || '';
|
||||
var deg = t && typeof t === 'object' && typeof t.degree === 'number' ? t.degree : null;
|
||||
return '<span class="tag green">' + esc(name) + (deg != null ? '<span style="opacity:.65">・' + deg + '</span>' : '') + '</span>';
|
||||
}).join(' ');
|
||||
}
|
||||
function renderLmCards(libs, details) {
|
||||
$('lm-meta').textContent = libs.length + ' 個庫・點卡片進該庫搜尋';
|
||||
$('lm-cards').innerHTML = libs.map(function (l) {
|
||||
var d = details[l.library];
|
||||
var ents = d && d.top_entities && d.top_entities.length ? d.top_entities : l.top_entities;
|
||||
// 更新時間感:commit_hash(詳圖有才顯示,短碼)+ updated_at(epoch 秒,台北時間)
|
||||
var stamp = (d && d.commit_hash ? '<span class="mono">' + esc(String(d.commit_hash).slice(0, 7)) + '</span>・' : '') +
|
||||
'更新 ' + esc(fmtDateTime(l.updated_at) || '—');
|
||||
return '<div class="kcard" data-lib="' + esc(l.library) + '">' +
|
||||
'<div class="kt">' + esc(l.library) + (S.library === l.library ? ' <span class="tag" style="font-size:11.5px;vertical-align:middle">搜尋中</span>' : '') + '</div>' +
|
||||
'<div class="ks">' + (l.narrative ? esc(l.narrative) : '<span class="dim">(此庫尚無 narrative——recompute 時可帶入)</span>') + '</div>' +
|
||||
(ents && ents.length ? '<div style="display:flex;gap:6px;flex-wrap:wrap">' + lmEntityLine(ents) + '</div>' : '') +
|
||||
'<div class="km"><span class="mono" style="color:var(--amber)">' + (Number(l.triplet_count) || 0) + ' 三元組</span>' +
|
||||
'<span class="dim" style="margin-left:auto;font-size:12.5px">' + stamp + '</span></div></div>';
|
||||
}).join('');
|
||||
}
|
||||
function renderLmBridges(details) {
|
||||
// 跨庫橋:詳圖 bridges=[{entity, libraries[]}],多庫詳圖去重後列一小區;沒資料就整區不出
|
||||
var seen = {}, rows = [];
|
||||
Object.keys(details).forEach(function (k) {
|
||||
(details[k].bridges || []).forEach(function (b) {
|
||||
if (!b || !b.entity || seen[b.entity]) return;
|
||||
seen[b.entity] = true;
|
||||
rows.push(b);
|
||||
});
|
||||
});
|
||||
if (!rows.length) { $('lm-bridges').innerHTML = ''; return; }
|
||||
$('lm-bridges').innerHTML = '<div style="margin-top:14px;padding:14px 16px;border-radius:12px;background:rgba(var(--ok-rgb),.05);border:1px solid rgba(var(--ok-rgb),.2)">' +
|
||||
'<div style="font-size:13px;letter-spacing:.14em;color:rgba(var(--ink-rgb),.5);margin-bottom:8px">跨庫橋(同一 entity 出現在多個庫)</div>' +
|
||||
'<div style="display:flex;gap:8px;flex-wrap:wrap">' + rows.slice(0, 12).map(function (b) {
|
||||
return '<span class="tag green">' + esc(b.entity) + '</span><span class="dim" style="font-size:12.5px;align-self:center">' + esc((b.libraries || []).join(' ↔ ')) + '</span>';
|
||||
}).join('<span style="width:6px"></span>') + '</div></div>';
|
||||
}
|
||||
function loadLibraryMap() {
|
||||
fetch(API_BASE + '/kbdb/map', { headers: apiHeaders() })
|
||||
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, status: r.status, d: d }; }); })
|
||||
.then(function (x) {
|
||||
if (!x.ok || x.d.success === false) {
|
||||
lmHonest('讀不到藏書地圖', esc(x.d.error || ('HTTP ' + x.status)) + '<br>不影響下方搜尋,可直接搜全庫。');
|
||||
return;
|
||||
}
|
||||
var libs = x.d.libraries || [];
|
||||
if (!libs.length) {
|
||||
lmHonest('還沒有藏書地圖', '這個租戶目前沒有任何三元組資料(地圖是查詢時即時核對重算的,不是要人手動 backfill——資料一進來下次載入就會出現)。<br>不影響下方搜尋,可直接搜全庫。');
|
||||
return;
|
||||
}
|
||||
LM.libs = libs; LM.details = {};
|
||||
renderLmCards(libs, {});
|
||||
Promise.allSettled(libs.map(function (l) {
|
||||
return fetch(API_BASE + '/kbdb/map/' + encodeURIComponent(l.library), { headers: apiHeaders() })
|
||||
.then(function (r) { return r.ok ? r.json() : null; });
|
||||
})).then(function (rs) {
|
||||
var details = {};
|
||||
rs.forEach(function (r) {
|
||||
if (r.status === 'fulfilled' && r.value && r.value.map && r.value.map.library) details[r.value.map.library] = r.value.map;
|
||||
});
|
||||
LM.details = details;
|
||||
renderLmCards(libs, details);
|
||||
renderLmBridges(details);
|
||||
});
|
||||
})
|
||||
.catch(function (e) { lmHonest('地圖服務不可達', esc(friendlyErr(e)) + '<br>不影響下方搜尋,可直接搜全庫。'); });
|
||||
}
|
||||
// 點庫卡片=設 library filter(/kbdb/search 既有 library 參數,多值逗號分隔——此處單庫)進庫搜尋
|
||||
$('lm-cards').addEventListener('click', function (ev) {
|
||||
var t = ev.target.closest('[data-lib]');
|
||||
if (!t) return;
|
||||
S.library = t.getAttribute('data-lib');
|
||||
renderLibBar();
|
||||
if (LM.libs) renderLmCards(LM.libs, LM.details);
|
||||
$('se-q').focus();
|
||||
if ($('se-q').value.trim()) doSearch();
|
||||
toast('已鎖定庫「' + S.library + '」——輸入關鍵字搜這個庫');
|
||||
});
|
||||
function renderLibBar() {
|
||||
$('se-libbar').innerHTML = S.library
|
||||
? '<div style="margin-top:10px;display:flex;align-items:center;gap:8px;font-size:14px">' +
|
||||
'<span class="tag">庫 ' + esc(S.library) + '</span>' +
|
||||
'<button class="chip" id="se-libclear" style="padding:5px 12px;font-size:13px">✕ 清除庫篩選(搜全館)</button></div>'
|
||||
: '';
|
||||
var clear = document.getElementById('se-libclear');
|
||||
if (clear) clear.addEventListener('click', function () {
|
||||
S.library = '';
|
||||
renderLibBar();
|
||||
if (LM.libs) renderLmCards(LM.libs, LM.details);
|
||||
if ($('se-q').value.trim()) doSearch();
|
||||
});
|
||||
}
|
||||
function renderChips() {
|
||||
$('se-chips').innerHTML = CHIPS.map(function (c, i) {
|
||||
@@ -828,7 +931,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
// 頭部統計=精耕層 live 數字(leo 2026-07-07 裁:45.8 萬 14-E 搬遷遺產已 deprecated、
|
||||
// 之後要刪——不再拿遺產總數撐場面,只顯示真的新的)。搜尋本身仍搜全庫,資料不藏。
|
||||
function loadScale() {
|
||||
fetch('/console/kb-scale-data')
|
||||
fetch(API_BASE + '/console/kb-scale-data')
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
if (!d) return;
|
||||
@@ -848,14 +951,14 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
$('se-q').addEventListener('keydown', function (ev) { if (ev.key === 'Enter') doSearch(); });
|
||||
function entryTitle(e) {
|
||||
if (e.page_name) return e.page_name;
|
||||
var first = String(e.content || '').split(/\\r?\\n/).find(function (l) { return l.trim(); }) || '';
|
||||
first = first.replace(/^#+\\s*/, '').replace(/^[-*>]\\s*/, '').trim();
|
||||
var first = String(e.content || '').split(/\r?\n/).find(function (l) { return l.trim(); }) || '';
|
||||
first = first.replace(/^#+\s*/, '').replace(/^[-*>]\s*/, '').trim();
|
||||
if (first.length > 60) first = first.slice(0, 60) + '…';
|
||||
return first || '(無標題・' + (e.entry_type || 'entry') + ')';
|
||||
}
|
||||
function entrySnippet(e) {
|
||||
var lines = String(e.content || '').split(/\\r?\\n/).filter(function (l) { return l.trim(); });
|
||||
var body = lines.slice(1).join(' ').replace(/\\s+/g, ' ').trim();
|
||||
var lines = String(e.content || '').split(/\r?\n/).filter(function (l) { return l.trim(); });
|
||||
var body = lines.slice(1).join(' ').replace(/\s+/g, ' ').trim();
|
||||
if (!body) body = lines.join(' ');
|
||||
if (body.length > 140) body = body.slice(0, 140) + '…';
|
||||
return body;
|
||||
@@ -866,18 +969,22 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
$('se-banner').innerHTML = '';
|
||||
$('se-count').textContent = '查詢中…';
|
||||
$('se-results').innerHTML = '';
|
||||
var url = '/kbdb/search?q=' + encodeURIComponent(q) + '&mode=' + (S.semantic ? 'semantic' : 'keyword');
|
||||
var url = API_BASE + '/kbdb/search?q=' + encodeURIComponent(q) + '&mode=' + (S.semantic ? 'semantic' : 'keyword');
|
||||
if (S.chip) url += '&entry_type=' + encodeURIComponent(S.chip);
|
||||
// 藏書地圖進庫(M5/R4):library filter=/kbdb/search 既有參數(portal-auth P1 順延項),不新造
|
||||
if (S.library) url += '&library=' + encodeURIComponent(S.library);
|
||||
fetch(url, { headers: apiHeaders() })
|
||||
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, status: r.status, d: d }; }); })
|
||||
.then(function (x) {
|
||||
if (!x.ok) { $('se-count').innerHTML = '<span class="err">' + esc(x.d.error || ('查詢失敗(HTTP ' + x.status + ')')) + '</span>'; return; }
|
||||
var d = x.d;
|
||||
if (S.semantic && d.mode === 'keyword') {
|
||||
$('se-banner').innerHTML = '<div class="honest" style="margin-top:18px"><div class="h">語意搜尋尚未啟用</div><div class="b">語意搜尋用「意思」找資料,不是字面比對。<br>' + esc(d.capability_hint || '部署端尚未開啟 Vectorize——不會假裝有語意結果,以下是關鍵字結果。') + '</div></div>';
|
||||
// 2026-08-09 leo:語意搜尋是安裝即提供的功能,降級=故障,不說「尚未啟用」。
|
||||
$('se-banner').innerHTML = '<div class="honest" style="margin-top:18px"><div class="h">語意搜尋目前故障</div><div class="b">' + esc(d.capability_hint || '語意搜尋目前故障(實例缺 Vectorize/AI 設定),以下先給關鍵字結果,不假裝是語意結果。') + '<br>維運資訊:' + esc(d.admin_hint || '(此版本後端未回報細節)') + '</div></div>';
|
||||
}
|
||||
var entries = d.entries || [];
|
||||
$('se-count').textContent = '命中 ' + entries.length + ' 筆・模式 ' + (d.mode || 'keyword') + '・搜尋範圍=全庫(含 14-E 遺產)';
|
||||
$('se-count').textContent = '命中 ' + entries.length + ' 筆・模式 ' + (d.mode || 'keyword') +
|
||||
'・搜尋範圍=' + (S.library ? '庫「' + S.library + '」' : '全庫(含 14-E 遺產)');
|
||||
if (!entries.length) {
|
||||
$('se-results').innerHTML = '<div class="muted" style="padding:30px 10px;text-align:center;grid-column:1/-1">找不到「' + esc(q) + '」——換個關鍵字試試。</div>';
|
||||
return;
|
||||
@@ -907,7 +1014,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
function loadCard() {
|
||||
if (!S.cardId) { $('cd-main').innerHTML = '<div class="muted" style="padding:24px 0">沒有指定卡片——從「總庫搜尋」點一張進來。</div>'; renderGraphEmpty('尚未載入卡片'); return; }
|
||||
$('cd-main').innerHTML = '<div class="muted" style="padding:24px 0">載入中…</div>';
|
||||
fetch('/kbdb/entries/' + encodeURIComponent(S.cardId), { headers: apiHeaders() })
|
||||
fetch(API_BASE + '/kbdb/entries/' + encodeURIComponent(S.cardId), { headers: apiHeaders() })
|
||||
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
|
||||
.then(function (x) {
|
||||
if (!x.ok || !x.d.entry) { $('cd-main').innerHTML = '<div class="err" style="padding:24px 0">' + esc(x.d.error || '讀不到這張卡片') + '</div>'; renderGraphEmpty('讀不到卡片'); return; }
|
||||
@@ -935,7 +1042,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
}
|
||||
function loadNeighbors(name) {
|
||||
renderGraphEmpty('查詢關聯中…');
|
||||
fetch('/kbdb/graph/neighbors/' + encodeURIComponent(name), { headers: apiHeaders() })
|
||||
fetch(API_BASE + '/kbdb/graph/neighbors/' + encodeURIComponent(name), { headers: apiHeaders() })
|
||||
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, status: r.status, d: d }; }); })
|
||||
.then(function (x) {
|
||||
if (!x.ok) { renderGraphEmpty(x.d.error || ('關聯查詢失敗(HTTP ' + x.status + ')')); return; }
|
||||
@@ -978,7 +1085,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
|
||||
// ── 工作流 ──
|
||||
function loadWorkflows() {
|
||||
fetch('/webhooks/named', { headers: apiHeaders() })
|
||||
fetch(API_BASE + '/webhooks/named', { headers: apiHeaders() })
|
||||
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
|
||||
.then(function (x) {
|
||||
if (!x.ok) { $('wf-list').innerHTML = '<div class="err">' + esc(x.d.error || '讀取失敗') + '</div>'; return; }
|
||||
@@ -1055,7 +1162,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
function fetchWfRuns(name) {
|
||||
var box = document.getElementById('wf-runs');
|
||||
if (!box) return;
|
||||
fetch('/workflows/' + encodeURIComponent(name) + '/executions?limit=3', { headers: apiHeaders() })
|
||||
fetch(API_BASE + '/workflows/' + encodeURIComponent(name) + '/executions?limit=3', { headers: apiHeaders() })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
var runs = (d.data && d.data.executions) || [];
|
||||
@@ -1073,7 +1180,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
function triggerWf(name, btn) {
|
||||
btn.disabled = true; btn.textContent = '執行中…';
|
||||
var msg = document.getElementById('wf-runmsg');
|
||||
fetch('/webhooks/named/' + encodeURIComponent(name) + '/trigger', {
|
||||
fetch(API_BASE + '/webhooks/named/' + encodeURIComponent(name) + '/trigger', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({ 'Content-Type': 'application/json' }, apiHeaders()),
|
||||
body: '{}'
|
||||
@@ -1105,7 +1212,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
out.innerHTML = '<div class="muted">查詢中…</div>';
|
||||
Promise.allSettled([
|
||||
fetch(REGISTRY_BASE + '/components/search?q=' + encodeURIComponent(q)).then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); }),
|
||||
fetch('/public-recipes?q=' + encodeURIComponent(q)).then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
|
||||
fetch(API_BASE + '/public-recipes?q=' + encodeURIComponent(q)).then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
|
||||
]).then(function (rs) {
|
||||
var html = '';
|
||||
var comp = rs[0];
|
||||
@@ -1146,9 +1253,9 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
var name = $('cred-name').value.trim(), service = $('cred-service').value.trim(), value = $('cred-secret').value;
|
||||
var st = $('cred-form-status');
|
||||
if (!name || !value) { st.innerHTML = '<span class="err">名稱與密文值必填</span>'; return; }
|
||||
if (!/^\\w+$/.test(name)) { st.innerHTML = '<span class="err">名稱只能包含英文字母、數字和底線</span>'; return; }
|
||||
if (!/^\w+$/.test(name)) { st.innerHTML = '<span class="err">名稱只能包含英文字母、數字和底線</span>'; return; }
|
||||
st.textContent = '保存中…';
|
||||
fetch('/credentials', {
|
||||
fetch(API_BASE + '/credentials', {
|
||||
method: 'POST',
|
||||
headers: Object.assign({ 'Content-Type': 'application/json' }, apiHeaders()),
|
||||
body: JSON.stringify({ name: name, value: value, service: service || undefined })
|
||||
@@ -1167,7 +1274,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
var credOpen = '';
|
||||
var credCache = [];
|
||||
function loadCreds() {
|
||||
fetch('/credentials/catalog', { headers: apiHeaders() })
|
||||
fetch(API_BASE + '/credentials/catalog', { headers: apiHeaders() })
|
||||
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
|
||||
.then(function (x) {
|
||||
if (!x.ok || x.d.success === false) {
|
||||
@@ -1215,7 +1322,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
if (!val.trim()) { if (msg) msg.innerHTML = '<span class="err">先貼上新值</span>'; return; }
|
||||
rep.disabled = true;
|
||||
var cur = credCache.find(function (c) { return c.name === name; });
|
||||
fetch('/credentials/' + encodeURIComponent(name), {
|
||||
fetch(API_BASE + '/credentials/' + encodeURIComponent(name), {
|
||||
method: 'PUT',
|
||||
headers: Object.assign({ 'Content-Type': 'application/json' }, apiHeaders()),
|
||||
body: JSON.stringify({ value: val, service: cur && cur.service ? cur.service : undefined })
|
||||
@@ -1239,42 +1346,114 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
}
|
||||
});
|
||||
|
||||
// ── 收件匣 ──
|
||||
function loadInbox() {
|
||||
var out = $('ib-out');
|
||||
// ── 分流台(原收件匣,Arcrun#9:todo+inbox 二源、三欄 80/15/5、per-project chips)──
|
||||
// 頁面只渲染 /console/triage-data 的結果(分欄/計數判定在後端純函式);chips 切換與
|
||||
// 「顯示已完成」是同一包資料的前端過濾,零額外請求(leo 手機看,省來回)。
|
||||
var TG = { data: null, project: '', showDone: false };
|
||||
var TG_TIERS = [
|
||||
{ key: 'ai', nm: 'AI 會幫我搞定', pc: '80%', empty: '這欄空的——還沒有分給 AI 的待辦' },
|
||||
{ key: 'collab', nm: '協作・AI 做完你接手', pc: '15%', empty: '這欄空的——沒有要你配合的事' },
|
||||
{ key: 'leo', nm: '非我不可', pc: '5%', empty: '這欄空的——沒有非你不可的事' }
|
||||
];
|
||||
function triItem(m) {
|
||||
var done = m.status === 'done';
|
||||
var meta = '';
|
||||
if (m.marker) meta += '<span class="tag dim">' + esc(m.marker) + '</span>';
|
||||
meta += '<span class="tag">' + esc(m.project || '未分項') + '</span>';
|
||||
if (m.source) meta += '<span>' + esc(m.source) + '</span>';
|
||||
if (m.unclassified && m.origin === 'todo') meta += '<span class="tag red">未分流</span>';
|
||||
// 誤勾救濟:done 清單裡給「還原」(status 回 new + 移除 checked_via/checked_at,端點同一個)
|
||||
if (done) meta += '<button class="tri-restore" data-trirestore="' + esc(m.id) + '">還原</button>';
|
||||
meta += '<span class="mono" style="margin-left:auto">' + esc(fmtDateTime(m.at)) + '</span>';
|
||||
// 勾掉=雙向銷案的 console 端終局(checked_via:console,萃取端絕不復活);
|
||||
// 後端回寫成功才改本地狀態移出三欄(done 預設隱藏),不樂觀假裝
|
||||
var body = done
|
||||
? '<div class="tx">' + esc(m.text) + '</div>'
|
||||
: '<div class="row"><div class="tx">' + esc(m.text) + '</div><button class="tri-check" data-tricheck="' + esc(m.id) + '" title="勾掉(銷案)">✓</button></div>';
|
||||
return '<div class="tri-item ' + (done ? 'done' : 'new') + '">' + body + '<div class="mt">' + meta + '</div></div>';
|
||||
}
|
||||
function triChip(label, val, on) {
|
||||
return '<button class="chip' + (on ? ' on' : '') + '" data-triproj="' + esc(val) + '">' + esc(label) + '</button>';
|
||||
}
|
||||
function renderTriage() {
|
||||
var d = TG.data || {};
|
||||
var items = d.items || [];
|
||||
var out = $('tg-out');
|
||||
// per-project chips:全部+各 project+(有未分項才出)未分項
|
||||
var chips = triChip('全部', '', TG.project === '');
|
||||
(d.projects || []).forEach(function (p) { chips += triChip(p, p, TG.project === p); });
|
||||
if (items.some(function (i) { return !i.project; })) chips += triChip('未分項', '__none__', TG.project === '__none__');
|
||||
$('tg-chips').innerHTML = items.length ? chips : '';
|
||||
if (!items.length) {
|
||||
out.innerHTML = '<div class="honest" style="margin-top:14px"><div class="h">分流台是空的</div><div class="b">資料源=KBDB todo entries(Logseq 萃取,ingest 灌入中)+ inbox entries(Telegram)。<br>寫入方尚未灌資料時這裡誠實顯示空。</div></div>';
|
||||
return;
|
||||
}
|
||||
var vis = items.filter(function (i) {
|
||||
return TG.project === '' || (TG.project === '__none__' ? !i.project : i.project === TG.project);
|
||||
});
|
||||
var doneN = vis.filter(function (i) { return i.status === 'done'; }).length;
|
||||
var html = '';
|
||||
TG_TIERS.forEach(function (t) {
|
||||
var news = vis.filter(function (i) { return i.tier === t.key && i.status !== 'done'; });
|
||||
var dones = TG.showDone ? vis.filter(function (i) { return i.tier === t.key && i.status === 'done'; }) : [];
|
||||
html += '<div class="tri-sec ' + t.key + '"><div class="tri-sechead"><span class="dot"></span><span class="nm serif">' + t.nm + '</span><span class="pc">' + t.pc + '</span><span class="ct">' + news.length + ' 件</span></div>';
|
||||
if (!news.length && !dones.length) html += '<div class="muted" style="font-size:14px;padding:10px 2px 0">' + t.empty + '</div>';
|
||||
html += news.map(triItem).join('') + dones.map(triItem).join('');
|
||||
html += '</div>';
|
||||
});
|
||||
html += '<div style="margin:24px 0 20px;text-align:center"><button class="btn3" id="tg-donetoggle" style="padding:9px 18px;font-size:13.5px;border-radius:999px">' +
|
||||
(TG.showDone ? '隱藏已完成' : '顯示已完成(' + doneN + ')') + '</button></div>';
|
||||
out.innerHTML = html;
|
||||
$('tg-donetoggle').addEventListener('click', function () { TG.showDone = !TG.showDone; renderTriage(); });
|
||||
}
|
||||
$('tg-chips').addEventListener('click', function (ev) {
|
||||
var t = ev.target.closest('[data-triproj]');
|
||||
if (!t) return;
|
||||
TG.project = t.getAttribute('data-triproj');
|
||||
renderTriage();
|
||||
});
|
||||
// 勾掉/還原:POST /console/triage-check(console session 保護的小端點——瀏覽器沒有 KBDB
|
||||
// token,PATCH 由 server 端做,先 GET 原 entry 整串回寫防蓋掉別的欄位)。成功才動本地
|
||||
// 狀態重繪(該筆立即移出/回到三欄),失敗誠實 toast、按鈕解鎖可重試。
|
||||
function triageAct(id, action, btn) {
|
||||
btn.disabled = true;
|
||||
fetch(API_BASE + '/console/triage-check', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + S.token },
|
||||
body: JSON.stringify({ entry_id: id, action: action })
|
||||
})
|
||||
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, status: r.status, d: d }; }); })
|
||||
.then(function (x) {
|
||||
if (!x.ok) { btn.disabled = false; toast(x.d.error || ('失敗(HTTP ' + x.status + ')')); return; }
|
||||
var items = (TG.data && TG.data.items) || [];
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i].id === id) items[i].status = (action === 'restore' ? 'new' : 'done');
|
||||
}
|
||||
renderTriage();
|
||||
toast(action === 'restore' ? '已還原' : '已勾掉');
|
||||
})
|
||||
.catch(function (e) { btn.disabled = false; toast('請求失敗:' + friendlyErr(e)); });
|
||||
}
|
||||
$('tg-out').addEventListener('click', function (ev) {
|
||||
var chk = ev.target.closest('[data-tricheck]');
|
||||
if (chk) { triageAct(chk.getAttribute('data-tricheck'), 'check', chk); return; }
|
||||
var rst = ev.target.closest('[data-trirestore]');
|
||||
if (rst) triageAct(rst.getAttribute('data-trirestore'), 'restore', rst);
|
||||
});
|
||||
function loadTriage() {
|
||||
var out = $('tg-out');
|
||||
out.innerHTML = '<div class="muted" style="padding:14px 0">載入中…</div>';
|
||||
fetch('/console/inbox-data', { headers: { Authorization: 'Bearer ' + S.token } })
|
||||
fetch(API_BASE + '/console/triage-data', { headers: { Authorization: 'Bearer ' + S.token } })
|
||||
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, status: r.status, d: d }; }); })
|
||||
.then(function (x) {
|
||||
if (!x.ok) {
|
||||
out.innerHTML = '<div class="honest" style="margin-top:14px"><div class="h">讀不到收件匣</div><div class="b">' + esc(x.d.error || ('HTTP ' + x.status)) + '</div></div>';
|
||||
out.innerHTML = '<div class="honest" style="margin-top:14px"><div class="h">讀不到分流台</div><div class="b">' + esc(x.d.error || ('HTTP ' + x.status)) + '</div></div>';
|
||||
return;
|
||||
}
|
||||
var items = x.d.items || [];
|
||||
if (!items.length) {
|
||||
out.innerHTML = '<div class="honest" style="margin-top:14px"><div class="h">收件匣是空的</div><div class="b">從 Telegram 丟指令給機器人,這裡會列出「新的」與「已處理」。<br>(資料源:KBDB inbox entries——寫入方尚未灌資料時這裡誠實顯示空。)</div></div>';
|
||||
return;
|
||||
}
|
||||
var news = items.filter(function (i) { return i.status !== 'done'; });
|
||||
var done = items.filter(function (i) { return i.status === 'done'; });
|
||||
var html = '';
|
||||
html += '<div class="serif" style="margin:20px 0 10px;font-size:16px;letter-spacing:.18em;color:var(--amber)">新的・' + news.length + ' 條</div>';
|
||||
html += news.length
|
||||
? '<div style="display:flex;flex-direction:column;gap:9px">' + news.map(function (m) {
|
||||
return '<div class="inbox-item new"><div style="font-size:16.5px;line-height:1.6;word-break:break-word">' + esc(m.text) + '</div>' +
|
||||
'<div style="margin-top:8px;display:flex;gap:10px;align-items:center;font-size:13px;color:rgba(var(--ink-rgb),.45)"><span class="pulse-dot"></span><span>等待 AI 處理</span><span class="mono" style="margin-left:auto">' + esc(fmtDateTime(m.at)) + '</span></div></div>';
|
||||
}).join('') + '</div>'
|
||||
: '<div class="muted" style="font-size:14px">沒有待處理的指令</div>';
|
||||
html += '<div class="serif" style="margin:26px 0 10px;font-size:16px;letter-spacing:.18em;color:rgba(var(--ink-rgb),.55)">已處理・' + done.length + ' 條</div>';
|
||||
html += done.length
|
||||
? '<div style="display:flex;flex-direction:column;gap:9px;padding-bottom:20px">' + done.map(function (m) {
|
||||
return '<div class="inbox-item done"><div style="font-size:16px;line-height:1.6;color:rgba(var(--ink-rgb),.7);word-break:break-word">' + esc(m.text) + '</div>' +
|
||||
'<div style="margin-top:8px;display:flex;gap:9px;align-items:baseline;font-size:14px"><span class="ok" style="flex:none">✓</span>' +
|
||||
'<span style="color:rgba(127,224,168,.8);line-height:1.5;word-break:break-word">' + esc(m.result || '已處理') + '</span>' +
|
||||
'<span class="dim mono" style="margin-left:auto;flex:none;font-size:13px">' + esc(fmtDateTime(m.at)) + '</span></div></div>';
|
||||
}).join('') + '</div>'
|
||||
: '<div class="muted" style="font-size:14px;padding-bottom:20px">還沒有已處理的紀錄</div>';
|
||||
out.innerHTML = html;
|
||||
TG.data = x.d;
|
||||
// 記著的 project 篩選若已不存在(資料變了)→ 退回全部,別讓頁面空得不明不白
|
||||
if (TG.project && TG.project !== '__none__' && (x.d.projects || []).indexOf(TG.project) < 0) TG.project = '';
|
||||
renderTriage();
|
||||
})
|
||||
.catch(function (e) { out.innerHTML = '<div class="err" style="padding:14px 0">請求失敗:' + esc(friendlyErr(e)) + '</div>'; });
|
||||
}
|
||||
@@ -1282,22 +1461,34 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
// ── 設定 ──
|
||||
function loadSettings() {
|
||||
// vectorize 狀態:探測既有 /kbdb/search 的 mode / capability_hint(無新端點,誠實讀降級訊號)
|
||||
fetch('/kbdb/search?q=mira&mode=semantic', { headers: apiHeaders() })
|
||||
fetch(API_BASE + '/kbdb/search?q=mira&mode=semantic', { headers: apiHeaders() })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
// t36:狀態照實顯示(live 探測 mode,不是讀設定值)。啟用時不再顯示任何操作指示——
|
||||
// 沒有東西要用戶操作;未啟用才給一句人話與下一步。
|
||||
// 2026-08-09 leo:語意搜尋是安裝即提供的功能——探測到降級=這台實例壞了,
|
||||
// 照實標「故障」,不說「尚未啟用」(那會把 bug 說成沒提供的功能)。
|
||||
var on = d.mode === 'semantic';
|
||||
$('st-vec-switch').classList.toggle('on', on);
|
||||
$('st-vec').textContent = on
|
||||
? '已啟用・語意搜尋可用(搜尋頁切「語意」)'
|
||||
: '未啟用・' + (d.capability_hint || '部署端尚未開啟 Vectorize(kbdb_embed)');
|
||||
$('st-vec-state').textContent = on ? '已啟用(live 探測 mode=semantic)' : '未啟用(live 探測降級 keyword)';
|
||||
? '● 正常——搜尋頁切到「語意」就能用意思找資料。'
|
||||
: '○ 故障——語意搜尋是內建功能,這台實例現在少了它(系統端問題,不是操作問題)。';
|
||||
var hint = $('st-vec-hint');
|
||||
if (on) {
|
||||
hint.style.display = 'none';
|
||||
} else {
|
||||
hint.style.display = '';
|
||||
hint.innerHTML = '修復方式:重新跑一次安裝流程(用原本的 Cloudflare 帳號),會把缺的語意索引設定補回來;已建好的資料不會重來。'
|
||||
+ (d.admin_hint ? '<br>維運資訊:' + esc(d.admin_hint) : '');
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
$('st-vec').textContent = '狀態偵測失敗(KBDB 不可達)';
|
||||
$('st-vec-state').textContent = '偵測失敗';
|
||||
$('st-vec').textContent = '狀態偵測失敗(知識庫服務目前連不上)';
|
||||
var hint = $('st-vec-hint');
|
||||
hint.style.display = '';
|
||||
hint.textContent = '這通常是暫時的,稍後重新整理這一頁再看。';
|
||||
});
|
||||
// MCP token TTL(誠實佔位:只顯示目前生效值,不假裝能改)
|
||||
fetch('/console/settings-data')
|
||||
fetch(API_BASE + '/console/settings-data')
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (d) {
|
||||
if (!d) { $('st-mcp-ttl').textContent = '讀取失敗'; return; }
|
||||
@@ -1308,8 +1499,8 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
.catch(function () { $('st-mcp-ttl').textContent = '讀取失敗'; });
|
||||
// 系統資訊:worker 自報 + 精耕層規模(14-E 遺產總數不再顯示,leo 2026-07-07 裁)
|
||||
Promise.allSettled([
|
||||
fetch('/').then(function (r) { return r.json(); }),
|
||||
fetch('/console/kb-scale-data').then(function (r) { return r.ok ? r.json() : null; })
|
||||
fetch(API_BASE + '/').then(function (r) { return r.json(); }),
|
||||
fetch(API_BASE + '/console/kb-scale-data').then(function (r) { return r.ok ? r.json() : null; })
|
||||
]).then(function (rs) {
|
||||
var svc = rs[0].status === 'fulfilled' ? rs[0].value : {};
|
||||
var kb = rs[1].status === 'fulfilled' ? rs[1].value : null;
|
||||
@@ -1330,7 +1521,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
if (!email) { st.innerHTML = '<span class="err">請輸入 Email(沿用原本的也要填)</span>'; return; }
|
||||
if (!newPw || newPw.length < 8) { st.innerHTML = '<span class="err">新密碼至少 8 碼</span>'; return; }
|
||||
st.textContent = '更新中…';
|
||||
fetch('/console/setup/reset', {
|
||||
fetch(API_BASE + '/console/setup/reset', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ current_password: oldPw, email: email, password: newPw })
|
||||
@@ -1344,12 +1535,33 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
})
|
||||
.catch(function (e) { st.innerHTML = '<span class="err">請求失敗:' + esc(friendlyErr(e)) + '</span>'; });
|
||||
});
|
||||
$('st-vec-switch').addEventListener('click', function () {
|
||||
toast('此開關不能遠端改——部署端 config.yaml 開 kbdb_embed 後 acr update 重部署');
|
||||
// arcrun-rag#25:portal admin 密碼救援——只吃 console owner session(S.token,本頁登入用的
|
||||
// 那把),不吃 portal session,所以就算忘記 portal 密碼、進不去 portal 也走得通。
|
||||
$('st-portal-recover-btn').addEventListener('click', function () {
|
||||
var email = $('st-portal-recover-email').value.trim();
|
||||
var st = $('st-portal-recover-status');
|
||||
if (!email) { st.innerHTML = '<span class="err">請輸入 Email</span>'; return; }
|
||||
st.textContent = '處理中…';
|
||||
fetch(API_BASE + '/portal/admin/recover-password', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + S.token },
|
||||
body: JSON.stringify({ email: email })
|
||||
})
|
||||
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
|
||||
.then(function (x) {
|
||||
if (!x.ok) { st.innerHTML = '<span class="err">' + esc(x.d.error || '失敗') + '</span>'; return; }
|
||||
st.innerHTML = '<span class="ok">新密碼:<code style="font-size:15px;user-select:all">' + esc(x.d.password) + '</code>(只顯示這一次,請立刻抄下)</span>';
|
||||
$('st-portal-recover-email').value = '';
|
||||
toast('新密碼已產生,請立刻抄下');
|
||||
})
|
||||
.catch(function (e) { st.innerHTML = '<span class="err">請求失敗:' + esc(friendlyErr(e)) + '</span>'; });
|
||||
});
|
||||
// t36:原本這裡綁在那顆假開關上(點了只會 toast 一段 CLI 指示)。開關已移除,
|
||||
// 這個 handler 也必須一起拿掉——留著會讓 $('st-vec-switch') 回 null、addEventListener
|
||||
// 當場拋錯,把後面所有綁定(含登出)一起打斷。
|
||||
$('st-logout').addEventListener('click', function () {
|
||||
var t = S.token;
|
||||
if (t) fetch('/console/logout', { method: 'POST', headers: { Authorization: 'Bearer ' + t } }).catch(function () {});
|
||||
if (t) fetch(API_BASE + '/console/logout', { method: 'POST', headers: { Authorization: 'Bearer ' + t } }).catch(function () {});
|
||||
S.token = ''; S.tenant = '';
|
||||
localStorage.removeItem('arcrun_console_session');
|
||||
location.hash = '';
|
||||
@@ -1361,15 +1573,3 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
// GET /console — Mira Console 完整版(Arcrun#3 console 系)。registry base 現算:同帳號
|
||||
// arcrun-registry worker(WORKER_SUBDOMAIN 沿用既有 KBDB_BASE_URL 那套組法),沒設就退回官方公開 registry。
|
||||
consoleRouter.get('/console', (c) => {
|
||||
const subdomain = c.env.WORKER_SUBDOMAIN;
|
||||
const registryBase = subdomain
|
||||
? `https://arcrun-registry.${subdomain}.workers.dev`
|
||||
: 'https://registry.arcrun.dev';
|
||||
return c.html(renderConsoleHtml(registryBase));
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-label="arcrun icon"><title>arcrun icon</title><rect width="1024" height="1024" fill="#17181A"/><path fill="#FDFCFB" fill-rule="nonzero" d="M463.01,612.91 L436.06,612.91 L436.06,485.41 L435.86,477.78 L435.27,470.46 L434.28,463.44 L432.89,456.73 L431.11,450.31 L428.93,444.20 L426.36,438.39 L423.39,432.88 L420.02,427.68 L416.26,422.78 L412.10,418.18 L407.55,413.88 L402.62,409.91 L397.31,406.28 L391.65,402.99 L385.61,400.06 L379.21,397.47 L372.44,395.22 L365.30,393.32 L357.79,391.76 L349.92,390.55 L341.68,389.69 L333.08,389.17 L324.10,389.00 L317.39,389.10 L310.90,389.40 L304.63,389.89 L298.59,390.58 L292.77,391.47 L287.17,392.56 L281.80,393.85 L276.65,395.33 L271.72,397.02 L267.02,398.90 L262.53,400.98 L258.28,403.25 L254.20,405.69 L250.26,408.26 L246.45,410.95 L242.78,413.76 L239.25,416.71 L235.86,419.77 L232.60,422.97 L229.48,426.29 L226.50,429.74 L223.65,433.31 L220.94,437.01 L218.37,440.83 L257.76,476.08 L259.43,473.77 L261.16,471.52 L262.96,469.32 L264.81,467.18 L266.73,465.09 L268.71,463.05 L270.75,461.07 L272.85,459.15 L275.01,457.27 L277.23,455.45 L279.51,453.69 L281.86,451.98 L284.30,450.36 L286.86,448.89 L289.55,447.55 L292.37,446.36 L295.31,445.31 L298.38,444.39 L301.58,443.62 L304.90,442.99 L308.34,442.50 L311.91,442.15 L315.61,441.94 L319.44,441.87 L323.74,441.95 L327.85,442.21 L331.75,442.65 L335.45,443.25 L338.95,444.03 L342.24,444.98 L345.34,446.10 L348.23,447.40 L350.93,448.87 L353.42,450.51 L355.71,452.32 L357.79,454.31 L359.70,456.45 L361.44,458.74 L363.01,461.18 L364.42,463.75 L365.66,466.47 L366.73,469.34 L367.64,472.35 L368.39,475.50 L368.97,478.80 L369.38,482.24 L369.63,485.82 L369.71,489.55 L369.71,509.25 L323.58,509.25 L314.52,509.39 L305.80,509.82 L297.44,510.53 L289.43,511.52 L281.78,512.80 L274.47,514.37 L267.52,516.22 L260.93,518.35 L254.68,520.77 L248.79,523.47 L243.25,526.45 L238.06,529.72 L233.26,533.28 L228.88,537.14 L224.91,541.30 L221.36,545.76 L218.23,550.52 L215.52,555.57 L213.22,560.93 L211.34,566.58 L209.88,572.53 L208.84,578.78 L208.21,585.33 L208.00,592.18 L208.15,598.13 L208.62,603.87 L209.39,609.41 L210.48,614.75 L211.87,619.89 L213.57,624.83 L215.58,629.57 L217.91,634.11 L220.54,638.44 L223.48,642.57 L226.73,646.50 L230.29,650.23 L234.14,653.71 L238.25,656.88 L242.63,659.75 L247.28,662.32 L252.19,664.59 L257.37,666.56 L262.82,668.22 L268.53,669.58 L274.51,670.64 L280.75,671.40 L287.26,671.85 L294.04,672.00 L299.07,671.91 L303.96,671.63 L308.72,671.17 L313.33,670.53 L317.81,669.71 L322.16,668.70 L326.37,667.50 L330.44,666.13 L334.37,664.57 L338.17,662.82 L341.83,660.89 L345.35,658.78 L348.71,656.49 L351.88,654.01 L354.85,651.35 L357.62,648.50 L360.20,645.47 L362.59,642.26 L364.78,638.87 L366.78,635.29 L368.58,631.52 L370.19,627.58 L371.60,623.45 L372.82,619.13 L375.93,619.13 L376.52,622.61 L377.24,625.98 L378.09,629.22 L379.07,632.35 L380.19,635.36 L381.44,638.24 L382.83,641.01 L384.34,643.67 L385.99,646.20 L387.78,648.61 L389.69,650.91 L391.74,653.08 L393.92,655.11 L396.23,656.96 L398.66,658.64 L401.22,660.14 L403.90,661.46 L406.71,662.61 L409.64,663.58 L412.71,664.37 L415.89,664.99 L419.21,665.43 L422.65,665.69 L426.21,665.78 L463.01,665.78 L463.01,612.91 Z M475.77,630.42 L546.23,713.58 L762.31,530.50 L546.23,347.42 L475.77,430.58 L593.69,530.50 L475.77,630.42 Z M667.77,630.42 L738.23,713.58 L954.31,530.50 L738.23,347.42 L667.77,430.58 L785.69,530.50 L667.77,630.42 Z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,32 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-Hant">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Arcrun RAG</title>
|
||||
<!--
|
||||
根目錄直接導向搜尋 Portal。
|
||||
|
||||
為什麼不做「選擇介面」的導覽頁(2026-07-21 leo 實際撞到):
|
||||
這份 UI 部署出去的網址是給**使用者**的入口(個人站 mira.uncle6.me,
|
||||
以及自架用戶自己的網址),進站就是要能用——多一層選擇=多一個困惑點,
|
||||
(2026-08-08 更正:原註解寫「這個網域=rag-demo.arcrun.dev 是客戶測試入口」,
|
||||
那是 uncle6 帳號那個已廢的 demo 站,leo 已定案不再拿它當範例;
|
||||
註解留著會把下一個人導向錯的環境,故改寫。理由本身仍然成立。)
|
||||
而且會讓客戶看到 Admin Console 這個維運介面(不該對客戶露出)。
|
||||
|
||||
維運者要進 console 直接打 /console/ 即可。
|
||||
-->
|
||||
<meta http-equiv="refresh" content="0; url=/portal/">
|
||||
<link rel="canonical" href="/portal/">
|
||||
<script>location.replace('/portal/');</script>
|
||||
<style>
|
||||
body{margin:0;min-height:100vh;display:grid;place-items:center;
|
||||
background:#faf8f5;color:#6b635a;
|
||||
font:15px/1.7 system-ui,-apple-system,"Noto Sans TC",sans-serif}
|
||||
@media(prefers-color-scheme:dark){body{background:#1a1816;color:#9a9186}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>正在前往搜尋頁… <a href="/portal/">沒有自動跳轉請點這裡</a></p>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,56 @@
|
||||
import fs from 'node:fs';
|
||||
const html = fs.readFileSync(new URL('./index.html', import.meta.url).pathname,'utf8');
|
||||
// 抽出 daemonPick 相關函式(從 DAEMON_BASE_DEFAULT 到 daemonHint 結尾)
|
||||
//
|
||||
// 🔴 2026-08-05:結尾標記本來寫死 daemonHint 的**整句文案**,於是同日改 Mac 提示語
|
||||
// (zip→DMG 的步驟不同)就讓這支自測直接炸「抽不到函式區塊」,而且沒人發現。
|
||||
// ⇒ 改成錨定「函式結束」這個結構,不再綁文案——文案本來就會改,測試不該為此壞掉。
|
||||
const start = html.indexOf('var DAEMON_BASE_DEFAULT');
|
||||
const hintAt = html.indexOf('function daemonHint', start);
|
||||
const endMark = '\n }';
|
||||
const end = hintAt < 0 ? -1 : html.indexOf(endMark, hintAt) + endMark.length;
|
||||
if (start < 0 || hintAt < 0 || end < start) throw new Error('抽不到函式區塊');
|
||||
const src = html.slice(start, end);
|
||||
|
||||
const cases = [
|
||||
['Windows', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36'],
|
||||
['Mac', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Safari/605.1.15'],
|
||||
['iPhone', 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Safari/604.1'],
|
||||
['Linux', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120 Safari/537.36'],
|
||||
];
|
||||
let pass=0, fail=0;
|
||||
const chk=(l,c,extra='')=>{ if(c){console.log('PASS:',l);pass++;} else {console.log('FAIL:',l,extra);fail++;} };
|
||||
|
||||
for (const [name, ua] of cases) {
|
||||
const fn = new Function('navigator','window', src + '; return {daemonPick:daemonPick, daemonHint:daemonHint, daemonBase:daemonBase};');
|
||||
const api = fn({userAgent: ua}, {});
|
||||
const d = api.daemonPick();
|
||||
const label = d.sure ? d.pick.label : '(兩個都給)';
|
||||
const url = d.sure ? d.pick.url : d.mac.url + ' + ' + d.win.url;
|
||||
console.log(`\n[${name}] sure=${d.sure} → ${label}`);
|
||||
console.log(` url: ${url}`);
|
||||
if (name==='Windows') {
|
||||
chk('Windows 給 win zip', d.sure && d.pick.url.endsWith('ArcrunRAG-win-unsigned.zip'), d.pick&&d.pick.url);
|
||||
chk('Windows 另一版是 Mac', d.other && d.other.url.endsWith('ArcrunRAG-mac.dmg'));
|
||||
chk('Windows 話術提 藍色視窗', api.daemonHint('win').includes('仍要執行'));
|
||||
}
|
||||
if (name==='Mac') {
|
||||
// 2026-08-05:Mac 一律給 DMG(拖進 Applications 的標準安裝畫面),不再給 zip
|
||||
// ——zip 解開就是一個裸 .app,使用者會直接在「下載」資料夾雙擊執行,自更新會蓋錯位置。
|
||||
chk('Mac 給 dmg(不是 zip)', d.sure && d.pick.url.endsWith('ArcrunRAG-mac.dmg'));
|
||||
chk('Mac 另一版是 Windows', d.other && d.other.url.endsWith('win-unsigned.zip'));
|
||||
chk('Mac 話術提 右鍵打開', api.daemonHint('mac').includes('右鍵'));
|
||||
}
|
||||
if (name==='iPhone' || name==='Linux') {
|
||||
// iPhone 含 "Mac OS X" 但不是桌機 Mac;Linux 兩者皆非 → 都該落在「不確定=兩個都給」
|
||||
if (name==='Linux') chk('Linux 判不出來→兩個都給', d.sure===false);
|
||||
if (name==='iPhone') chk('iPhone 不該被判成 Mac(手機→兩個都給)', d.sure===false, 'sure='+d.sure);
|
||||
}
|
||||
}
|
||||
// 舊 key 相容
|
||||
const fn2 = new Function('navigator','window', src + '; return daemonBase();');
|
||||
console.log('\n[相容] daemonDownload 舊 key →', fn2({userAgent:''},{ARCRUN_CONFIG:{daemonDownload:'https://x.dev/d/ArcrunRAG-mac-unsigned.zip'}}));
|
||||
chk('舊 key 推得出目錄', fn2({userAgent:''},{ARCRUN_CONFIG:{daemonDownload:'https://x.dev/d/ArcrunRAG-mac-unsigned.zip'}})==='https://x.dev/d/');
|
||||
chk('daemonBase 新 key 優先', fn2({userAgent:''},{ARCRUN_CONFIG:{daemonBase:'https://y.dev/z'}})==='https://y.dev/z/');
|
||||
console.log(`\n=== ${pass} passed, ${fail} failed ===`);
|
||||
process.exit(fail?1:0);
|
||||
@@ -0,0 +1,46 @@
|
||||
import fs from 'node:fs';
|
||||
const html = fs.readFileSync(new URL('./index.html', import.meta.url).pathname,'utf8');
|
||||
|
||||
// 抽出 safeJson 與 friendlyErr 求值
|
||||
const grab = (name) => {
|
||||
const i = html.indexOf(`function ${name}(`);
|
||||
if (i < 0) throw new Error(`找不到 ${name}`);
|
||||
let d=0, j=html.indexOf('{', i);
|
||||
for (let k=j;k<html.length;k++){ if(html[k]==='{')d++; if(html[k]==='}'){d--; if(!d){ return html.slice(i,k+1);} } }
|
||||
throw new Error('括號不平衡');
|
||||
};
|
||||
const fn = new Function(grab('safeJson') + '\n' + grab('friendlyErr') + '\nreturn {safeJson, friendlyErr};')();
|
||||
|
||||
let pass=0, fail=0;
|
||||
const t=(l,c,e='')=>{c?(console.log('PASS:',l),pass++):(console.log('FAIL:',l,e),fail++)};
|
||||
|
||||
// ① safeJson:非 JSON 不可拋例外(同事撞到的 404 HTML 頁)
|
||||
const html404 = '<!DOCTYPE html><html><body>404 Not Found</body></html>';
|
||||
await fn.safeJson({ text: () => Promise.resolve(html404) })
|
||||
.then(d => t('404 HTML → 回空物件不拋錯', typeof d === 'object' && d !== null))
|
||||
.catch(e => t('404 HTML → 不該拋錯', false, e.message));
|
||||
|
||||
await fn.safeJson({ text: () => Promise.resolve('') })
|
||||
.then(d => t('空回應 → 回空物件', JSON.stringify(d)==='{}'))
|
||||
.catch(() => t('空回應 → 不該拋錯', false));
|
||||
|
||||
await fn.safeJson({ text: () => Promise.resolve('{"error":"帳號或密碼不對"}') })
|
||||
.then(d => t('正常 JSON 仍要解析得出來', d.error === '帳號或密碼不對'), )
|
||||
.catch(() => t('正常 JSON 不該拋錯', false));
|
||||
|
||||
// ② friendlyErr:不可把技術訊息噴給使用者
|
||||
const leak = fn.friendlyErr(new Error('Unexpected non-whitespace character after JSON at position 4'));
|
||||
t('JSON 錯誤 → 不外洩原文', !/JSON|position/i.test(leak), `實得: ${leak}`);
|
||||
t('JSON 錯誤 → 說人話', /伺服器回應異常/.test(leak), `實得: ${leak}`);
|
||||
|
||||
const net = fn.friendlyErr(new Error('Failed to fetch'));
|
||||
t('網路錯誤 → 既有訊息保留', /連線中斷/.test(net), `實得: ${net}`);
|
||||
|
||||
const ours = fn.friendlyErr(new Error('帳號或密碼不對——用你在知識庫網站設定的那組'));
|
||||
t('我們自己的中文訊息 → 原樣顯示', /帳號或密碼不對/.test(ours), `實得: ${ours}`);
|
||||
|
||||
const stack = fn.friendlyErr(new Error('TypeError: Cannot read properties of undefined'));
|
||||
t('英文技術訊息 → 收斂不外洩', !/TypeError|undefined/.test(stack), `實得: ${stack}`);
|
||||
|
||||
console.log(`\n=== ${pass} passed, ${fail} failed ===`);
|
||||
process.exit(fail?1:0);
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* deploy.mjs — 依具名目標部署 console-ui 到 Cloudflare Pages
|
||||
*
|
||||
* 用法:npm run deploy:personal
|
||||
* npm run deploy:personal -- --dry-run (只產出並驗產物,不推)
|
||||
*
|
||||
* 為什麼不直接用 `wrangler pages deploy`(2026-07-22 leo 立,實際踩到才補):
|
||||
* **兩個帳號都有名為 arcrun-console-ui 的 Pages 專案**
|
||||
* wrangler 若 OAuth 登入在別的帳號,`--project-name arcrun-console-ui` 會部到別人的站上。
|
||||
* 本腳本強制帶目標的 accountId,並在部署前印出目標,避免部錯帳號。
|
||||
*
|
||||
* 同時把 profile/apiBase 綁進目標(deploy.targets.json),不再靠部署者記得帶環境變數——
|
||||
* 帶漏過三次:漏 profile 顯示成錯的版本、漏 apiBase 導致登入 405。
|
||||
*
|
||||
* 🔴 三道閘,全部**讀磁碟上真的要被推的那份**,不看本腳本自己印了什麼
|
||||
* (2026-08-08 事故的形狀正是「印的是 A、推的是 B」):
|
||||
* ① 產物閘 :宣告值有沒有真的寫進產物(apiBase / VIEWS / HOME)
|
||||
* ② 世代閘 :產物是不是當代(指紋+t160 的文字指紋)
|
||||
* ③ 線上閘 :推完回頭抓線上,組態+世代都要對上,否則本次部署算失敗
|
||||
* 三閘都過才寫 .deploy-state.json(那份紀錄是「經過線上實測」的意思,不是「我跑過指令」)。
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { join } from 'node:path';
|
||||
import { ROOT, assertArtifact, buildArtifact, loadTargets, resolveTarget, writeState } from './targets.mjs';
|
||||
import { printReport, verifyTarget } from './verify-live.mjs';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const dryRun = args.includes('--dry-run');
|
||||
const name = args.find((a) => !a.startsWith('--'));
|
||||
|
||||
let t;
|
||||
try {
|
||||
if (!name) throw Object.assign(new Error('沒有指定部署目標'), { usage: true });
|
||||
t = resolveTarget(name);
|
||||
} catch (e) {
|
||||
console.error(`✘ ${e.message}`);
|
||||
if (e.usage) console.error(`用法:npm run deploy:<target>\n可用目標:${loadTargets().active.join(' / ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (t.frozen) {
|
||||
console.error(`✘ 目標 ${name} 已凍結,拒絕部署。\n ${t.frozen}`);
|
||||
console.error(' (要解凍是人的決定:改 deploy.targets.json 拿掉 frozen 欄位,並說明理由。)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n部署目標:${name}`);
|
||||
console.log(` 說明 :${t.description}`);
|
||||
console.log(` 帳號 :${t.accountId}`);
|
||||
console.log(` 專案 :${t.projectName}`);
|
||||
console.log(` profile :${t.profile}`);
|
||||
console.log(` apiBase :${t.apiBase}`);
|
||||
|
||||
// ── ①② 產出 + 驗產物 ────────────────────────────────────────────────
|
||||
const outDir = join(ROOT, '.staging', name);
|
||||
try {
|
||||
buildArtifact(t, outDir);
|
||||
} catch (e) {
|
||||
console.error(`\n✘ 產出失敗:${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const gate = assertArtifact(t, outDir);
|
||||
console.log(`\n產物:${outDir}`);
|
||||
console.log(` 世代指紋:${gate.generation.slice(0, 12)}`);
|
||||
if (!gate.ok) {
|
||||
console.error('\n✘ 產物閘不通過——推上去的會跟宣告的不一樣,拒絕部署:');
|
||||
for (const p of gate.problems) console.error(` · ${p}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(' ✅ 產物閘:宣告值確實寫進產物,且是當代。');
|
||||
|
||||
if (dryRun) {
|
||||
console.log('\n(--dry-run:到此為止,沒有推任何東西。)');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// ── 推 ───────────────────────────────────────────────────────────────
|
||||
const env = { ...process.env, DEPLOY_TARGET: name, CLOUDFLARE_ACCOUNT_ID: t.accountId };
|
||||
// --commit-dirty:本地部署常有未提交變更,不因此中斷
|
||||
const deploy = spawnSync(
|
||||
'npx',
|
||||
['wrangler', 'pages', 'deploy', outDir, '--project-name', t.projectName, '--commit-dirty=true'],
|
||||
{ stdio: 'inherit', cwd: ROOT, env },
|
||||
);
|
||||
if (deploy.status !== 0) {
|
||||
console.error('\n✘ wrangler 部署失敗。');
|
||||
process.exit(deploy.status ?? 1);
|
||||
}
|
||||
|
||||
// ── ③ 線上閘 ─────────────────────────────────────────────────────────
|
||||
console.log('\n── 回頭驗線上(組態+世代)──');
|
||||
const report = await verifyTarget(name, { wait: true });
|
||||
printReport([report]);
|
||||
if (!report.ok) {
|
||||
console.error('\n✘ 推上去了,但線上跑的 ≠ 我們手上這一份。**本次部署視為失敗**。');
|
||||
console.error(' (wrangler 說成功不代表對外網址就對——這正是要被擋掉的那個病。)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
writeState(name, {
|
||||
generation: gate.generation,
|
||||
apiBase: t.apiBase,
|
||||
profile: t.profile,
|
||||
urls: t.verifyUrls,
|
||||
verifiedAt: new Date().toISOString(),
|
||||
});
|
||||
console.log('\n✅ 部署完成,且線上實測=宣告值+當代世代。已記入 .deploy-state.json。');
|
||||
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* targets.mjs — 部署目標的唯一讀取點(deploy.mjs 與 verify-live.mjs 共用)。
|
||||
*
|
||||
* 存在的理由:宣告值(deploy.targets.json)只准被解讀一次。
|
||||
* 「部署時印在終端機的值」「寫進產物的值」「事後驗線上的值」若各自去讀、各自算,
|
||||
* 三者就會漂移——2026-08-08 那場事故的形狀正是「印的是 A、推的是 B」。
|
||||
* 這支把「一個目標展開成期望的產物長相」定死成一個函式,三邊共用同一個答案。
|
||||
*
|
||||
* 🔴 2026-08-08 第二層(leo:「已經發生過一次這個錯誤,把舊版界面上到 prod,
|
||||
* 你要確定不可再犯」):組態對 ≠ 世代對。
|
||||
* 一個網址可以 apiBase/profile 全部正確,卻對外展示一套早就被淘汰的介面,
|
||||
* 而所有只驗組態的檢查都說它綠。故本檔另外定義「世代指紋」(見下半段):
|
||||
* 把「線上這一份是不是當代的」變成一個可機械比對的值。
|
||||
*/
|
||||
import { createHash } from 'node:crypto';
|
||||
import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
export const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
export const PUBLIC_DIR = join(ROOT, 'public');
|
||||
|
||||
export function loadTargets() {
|
||||
const raw = JSON.parse(readFileSync(join(ROOT, 'deploy.targets.json'), 'utf8'));
|
||||
const profiles = raw._profiles;
|
||||
if (!profiles) throw new Error('deploy.targets.json 缺 _profiles(profile → views/home 對照)');
|
||||
const names = Object.keys(raw).filter((k) => !k.startsWith('_'));
|
||||
const active = names.filter((n) => !raw[n].frozen);
|
||||
return { raw, profiles, names, active };
|
||||
}
|
||||
|
||||
export function resolveTarget(name) {
|
||||
const { raw, profiles, names } = loadTargets();
|
||||
const t = raw[name];
|
||||
if (!t) {
|
||||
const err = new Error(`未知的部署目標:"${name}"。可用:${names.join(' / ')}`);
|
||||
err.usage = true;
|
||||
throw err;
|
||||
}
|
||||
// 凍結目標:連讀都不准碰(frozen.reason 說明是誰、何時、為什麼)。
|
||||
// 這不是「壞掉所以跳過」,是「這個帳號的資源不歸我們動」——工具自己守,不靠人記得。
|
||||
if (t.frozen) return { name, ...t, frozen: t.frozen, views: profiles[t.profile]?.views, home: profiles[t.profile]?.home };
|
||||
const p = profiles[t.profile];
|
||||
if (!p) {
|
||||
throw new Error(
|
||||
`目標 ${name} 的 profile="${t.profile}" 在 _profiles 裡沒有定義(可用:${Object.keys(profiles).join(' / ')})。` +
|
||||
'\n宣告了一個沒人知道怎麼落地的 profile ⇒ 拒絕部署,不要猜。',
|
||||
);
|
||||
}
|
||||
if (!t.apiBase) throw new Error(`目標 ${name} 沒有 apiBase——空值會讓前端安靜地連不上,拒絕部署。`);
|
||||
if (!t.accountId) throw new Error(`目標 ${name} 沒有 accountId——不指定帳號可能部到別人的站上,拒絕部署。`);
|
||||
if (!Array.isArray(t.verifyUrls) || t.verifyUrls.length === 0) {
|
||||
throw new Error(`目標 ${name} 沒有 verifyUrls——沒有對外網址就無法驗「站上跑的=宣告的」,拒絕部署。`);
|
||||
}
|
||||
return { name, ...t, views: p.views, home: p.home };
|
||||
}
|
||||
|
||||
/** 這個目標「應該長成什麼樣」——產物閘與線上閘都比對這一份。 */
|
||||
export function expected(t) {
|
||||
return {
|
||||
configJs: configJsFor(t),
|
||||
apiBase: t.apiBase,
|
||||
viewsLine: ` var VIEWS = ${JSON.stringify(t.views)};`,
|
||||
homeLine: ` var HOME = ${JSON.stringify(t.home)};`,
|
||||
};
|
||||
}
|
||||
|
||||
export function configJsFor(t) {
|
||||
return (
|
||||
'// 由 console-ui/scripts/deploy.mjs 於部署時依 deploy.targets.json 產生——請勿手改,也不進 git。\n' +
|
||||
`// 目標:${t.name}(${t.description})\n` +
|
||||
`window.ARCRUN_CONFIG = { apiBase: ${JSON.stringify(t.apiBase)} };\n`
|
||||
);
|
||||
}
|
||||
|
||||
/** 從 config.js 的文字裡取出 apiBase(線上/產物共用同一個解析法)。 */
|
||||
export function parseApiBase(text) {
|
||||
const m = text.match(/apiBase\s*:\s*"([^"]*)"/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 世代指紋(2026-08-08 第二層)
|
||||
//
|
||||
// 問題:verify-live 原本只驗組態(apiBase / VIEWS / HOME)。實測當天三個對外網址
|
||||
// 這三項全綠,但線上跑的是 2026-07-22 那一代的 portal(82,911 bytes、
|
||||
// 金色 serif「Arcrun」品牌、Songti 12 處),repo 是 343,969 bytes 的
|
||||
// 「arc >> run」新代——**組態全對、介面整整落後半個月,機械檢查一片綠**。
|
||||
//
|
||||
// 判準:「線上這一份,是不是我們手上這一份?」不加解釋、不留模糊地帶——
|
||||
// 逐一抓下線上資產、遮掉「本來就該隨部署目標不同」的那幾行,其餘按位元組比對。
|
||||
//
|
||||
// 為什麼是位元組而不是「找幾個關鍵字」:
|
||||
// 關鍵字清單要人維護,而人只會在「這次剛好想到」時更新它。舊世代之所以能無聲上線,
|
||||
// 正是因為沒有人記得去更新那張清單。位元組比對不需要任何人記得任何事:
|
||||
// repo 改了一個字,指紋就不同,線上沒跟上就是 ❌。
|
||||
//
|
||||
// 誠實的 trade-off(mindset §7,不假裝完美):
|
||||
// ① 只要 repo 動過而還沒部署,這個檢查就會說「線上落後」——那是**正確的**,
|
||||
// 因為那時線上確實不是當代的。它會吵,但吵的是真的。
|
||||
// ② 若哪天 CF 邊緣開始改寫 HTML(Rocket Loader 之類),會出現假 ❌。
|
||||
// 2026-08-08 實測 mira.uncle6.me 與 pages.dev 回傳位元組完全相同(sha 一致),
|
||||
// 證明目前沒有改寫。真出現時它會大聲壞掉、有人來查——
|
||||
// **假 ❌ 的代價遠低於假 ✅**(假 ✅ 就是這次事故本身)。
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** 納入世代指紋的資產:file=public/ 底下的路徑,urlPath=線上要抓的位址。 */
|
||||
export const GENERATION_ASSETS = [
|
||||
{ file: 'index.html', urlPath: '/' },
|
||||
{ file: 'portal/index.html', urlPath: '/portal/' },
|
||||
{ file: 'console/index.html', urlPath: '/console/' },
|
||||
{ file: 'favicon.svg', urlPath: '/favicon.svg' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 「本來就該隨部署目標不同」的行——比世代時遮掉,否則個人版與企業版永遠指紋不同。
|
||||
* 遮的只有這兩行;其餘全部按原樣比對。
|
||||
* config.js 整支不納入世代(它是純產物,由 apiBase 那一項單獨驗)。
|
||||
*/
|
||||
const TARGET_DEPENDENT_LINES = [
|
||||
{ file: 'console/index.html', re: /^[ \t]*var VIEWS = .*$/m, tag: '«VIEWS:由部署目標決定»' },
|
||||
{ file: 'console/index.html', re: /^[ \t]*var HOME = .*$/m, tag: '«HOME:由部署目標決定»' },
|
||||
];
|
||||
|
||||
/** 遮掉目標相依的行。抓不到就原樣回傳(線上是舊世代時本來就可能沒有那幾行 → 該判 ❌)。 */
|
||||
export function maskTargetValues(file, bytes) {
|
||||
const rules = TARGET_DEPENDENT_LINES.filter((r) => r.file === file);
|
||||
if (!rules.length) return bytes;
|
||||
let text = Buffer.from(bytes).toString('utf8');
|
||||
for (const r of rules) text = text.replace(r.re, r.tag);
|
||||
return Buffer.from(text, 'utf8');
|
||||
}
|
||||
|
||||
export function sha256(bytes) {
|
||||
return createHash('sha256').update(bytes).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 由「檔名 → 位元組(抓不到給 null)」算出世代指紋。
|
||||
* @param {Array<{file:string, bytes:Buffer|null}>} entries
|
||||
*/
|
||||
export function fingerprintOf(entries) {
|
||||
const assets = {};
|
||||
const lines = [];
|
||||
for (const { file, bytes } of entries) {
|
||||
if (bytes == null) {
|
||||
assets[file] = { sha: null, size: null, missing: true };
|
||||
lines.push(`${file}\tMISSING`);
|
||||
continue;
|
||||
}
|
||||
const masked = maskTargetValues(file, bytes);
|
||||
const sha = sha256(masked);
|
||||
assets[file] = { sha, size: Buffer.from(bytes).length, missing: false };
|
||||
lines.push(`${file}\t${sha}`);
|
||||
}
|
||||
return { assets, digest: sha256(Buffer.from(lines.join('\n'), 'utf8')) };
|
||||
}
|
||||
|
||||
/** repo(或某個產物目錄)現在這一代長什麼樣。這就是「當代」的定義。 */
|
||||
export function generationOfDir(dir = PUBLIC_DIR) {
|
||||
return fingerprintOf(
|
||||
GENERATION_ASSETS.map(({ file }) => {
|
||||
let bytes = null;
|
||||
try {
|
||||
bytes = readFileSync(join(dir, file));
|
||||
} catch {
|
||||
bytes = null;
|
||||
}
|
||||
return { file, bytes };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 產物:把宣告值真的寫進去(e730b3f 標的 WIP,本次收掉)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 依目標把 public/ 展開成「要推上去的那一份」。
|
||||
* 🔴 覆寫沒命中就中止——宣告了卻沒寫進產物,正是這串事故的根。
|
||||
*/
|
||||
export function buildArtifact(t, outDir) {
|
||||
rmSync(outDir, { recursive: true, force: true });
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
cpSync(PUBLIC_DIR, outDir, { recursive: true });
|
||||
|
||||
const exp = expected(t);
|
||||
|
||||
// ① config.js:產物,不是原始碼(public/ 裡不留)
|
||||
writeFileSync(join(outDir, 'config.js'), exp.configJs, 'utf8');
|
||||
|
||||
// ② console 的 VIEWS/HOME:public/ 裡那兩行只是本機 preview 的預設值
|
||||
const consolePath = join(outDir, 'console', 'index.html');
|
||||
let html = readFileSync(consolePath, 'utf8');
|
||||
for (const [re, line, what] of [
|
||||
[/^[ \t]*var VIEWS = .*$/m, exp.viewsLine, 'VIEWS'],
|
||||
[/^[ \t]*var HOME = .*$/m, exp.homeLine, 'HOME'],
|
||||
]) {
|
||||
if (!re.test(html)) {
|
||||
throw new Error(
|
||||
`產物覆寫沒命中:console/index.html 找不到 ${what} 那一行 ⇒ 中止部署。\n` +
|
||||
'(前端改版把那行換了寫法時會發生。宣告值寫不進去就不准推——這正是 2026-08-08 事故的形狀。)',
|
||||
);
|
||||
}
|
||||
html = html.replace(re, line);
|
||||
}
|
||||
writeFileSync(consolePath, html, 'utf8');
|
||||
|
||||
return outDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 產物閘:推之前,回頭讀「真的要被推上去的那些檔案」,確認=宣告值。
|
||||
* 不看 deploy.mjs 自己印了什麼——只看磁碟上那份。
|
||||
*/
|
||||
export function assertArtifact(t, outDir) {
|
||||
const exp = expected(t);
|
||||
const problems = [];
|
||||
|
||||
const cfg = readFileSync(join(outDir, 'config.js'), 'utf8');
|
||||
const gotApiBase = parseApiBase(cfg);
|
||||
if (gotApiBase !== t.apiBase) problems.push(`config.js 的 apiBase:宣告 ${t.apiBase},產物 ${gotApiBase}`);
|
||||
|
||||
const html = readFileSync(join(outDir, 'console', 'index.html'), 'utf8');
|
||||
const gotViews = html.match(/^[ \t]*var VIEWS = .*$/m)?.[0];
|
||||
const gotHome = html.match(/^[ \t]*var HOME = .*$/m)?.[0];
|
||||
if (gotViews !== exp.viewsLine) problems.push(`console VIEWS:宣告 ${exp.viewsLine.trim()},產物 ${gotViews?.trim()}`);
|
||||
if (gotHome !== exp.homeLine) problems.push(`console HOME:宣告 ${exp.homeLine.trim()},產物 ${gotHome?.trim()}`);
|
||||
|
||||
// 世代閘(產物側):注入不得改動世代相關位元組
|
||||
const src = generationOfDir(PUBLIC_DIR);
|
||||
const art = generationOfDir(outDir);
|
||||
if (src.digest !== art.digest) {
|
||||
problems.push(`產物世代指紋 ${art.digest.slice(0, 12)} ≠ public/ 的 ${src.digest.slice(0, 12)}(注入改到了不該改的位元組)`);
|
||||
}
|
||||
|
||||
// 世代閘(內容側,沿用 t160 的文字指紋——擋「整份 public 被換成舊代」)
|
||||
//
|
||||
// 🔴 只看「使用者看得到的內容」,比對前先剝掉 HTML 註解。
|
||||
// 2026-08-08 實撞:原版直接對全文比對「登記新庫」,而 66f1b59(08-03)在 portal 裡
|
||||
// 加了一則**說明「已經把登記新庫拿掉了」的註解** ⇒ 這道閘從那天起每次都誤判,
|
||||
// `npm run deploy:personal` 連續五天推不出去、而錯誤訊息說的是「你的 UI 是舊代」。
|
||||
// ⇒ 手工維護的關鍵字清單會腐爛,這就是實例;世代的主判準因此改用位元組指紋,
|
||||
// 這道文字閘只留來擋「整份 public 被換成舊代」,且必須剝註解才不會自傷。
|
||||
const portalRaw = readFileSync(join(outDir, 'portal', 'index.html'), 'utf8');
|
||||
const portal = portalRaw.replace(/<!--[\s\S]*?-->/g, '');
|
||||
if (!portal.includes('不需要人工新增') || portal.includes('登記新庫')) {
|
||||
problems.push('portal/index.html 不是現行世代(可見內容缺「不需要人工新增」或仍有「登記新庫」)');
|
||||
}
|
||||
|
||||
return { ok: problems.length === 0, problems, generation: art.digest };
|
||||
}
|
||||
|
||||
/** 部署狀態記錄檔(只在「線上實測通過」之後才寫,見 deploy.mjs)。 */
|
||||
export const STATE_FILE = join(ROOT, '.deploy-state.json');
|
||||
|
||||
export function readState() {
|
||||
try {
|
||||
return JSON.parse(readFileSync(STATE_FILE, 'utf8'));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function writeState(name, record) {
|
||||
const state = readState();
|
||||
state[name] = record;
|
||||
writeFileSync(STATE_FILE, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* verify-live.mjs — 驗「線上網址現在真的在跑的那一份」=「我們手上這一份」。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/verify-live.mjs 驗全部服役中目標的全部對外網址
|
||||
* node scripts/verify-live.mjs personal 只驗某個目標
|
||||
* node scripts/verify-live.mjs --wait 容忍 CF Pages 生效延遲(重試)
|
||||
* node scripts/verify-live.mjs --url <網址> 只對某個網址驗世代(不需要是宣告目標)
|
||||
* npm run verify
|
||||
*
|
||||
* 兩層,缺一不可:
|
||||
* ① 組態層:apiBase/profile 的 views/home = deploy.targets.json 宣告值
|
||||
* ② 世代層:線上資產的位元組指紋 = repo public/ 的指紋
|
||||
*
|
||||
* 為什麼要第二層(2026-08-08,leo:「已經發生過一次這個錯誤,把舊版界面上到 prod,
|
||||
* 你要確定不可再犯」):當天實測三個對外網址,第一層**三項全過**,
|
||||
* 而它們跑的是 07-22 那一代的 portal(82,911 bytes、金色 serif 舊品牌),
|
||||
* repo 是 343,969 bytes 的新品牌世代。
|
||||
* ⇒ **組態可以完全正確,同時展示一套早就被淘汰的介面,而機械檢查一片綠。**
|
||||
* 第二層就是為了讓這個狀態不可能無聲存在。
|
||||
*
|
||||
* 🔴 一律帶 no-cache(快取害人誤判過)。curl|grep 不算驗前端,但 config.js/VIEWS/HOME
|
||||
* 與世代指紋都是**純文字資產比對**,抓原始碼比對是這幾項的正確驗法;
|
||||
* 「頁面真的能用」另外走瀏覽器實載。
|
||||
* 🔴 frozen 目標(見 deploy.targets.json)連抓都不抓——不是我們的帳號,不碰。
|
||||
*/
|
||||
import {
|
||||
GENERATION_ASSETS,
|
||||
fingerprintOf,
|
||||
generationOfDir,
|
||||
loadTargets,
|
||||
parseApiBase,
|
||||
readState,
|
||||
resolveTarget,
|
||||
} from './targets.mjs';
|
||||
|
||||
const NOCACHE = { 'Cache-Control': 'no-cache', Pragma: 'no-cache' };
|
||||
|
||||
async function get(url) {
|
||||
const res = await fetch(`${url}${url.includes('?') ? '&' : '?'}_nc=${Date.now()}`, {
|
||||
headers: NOCACHE,
|
||||
cache: 'no-store',
|
||||
redirect: 'follow',
|
||||
});
|
||||
const buf = Buffer.from(await res.arrayBuffer());
|
||||
return { status: res.status, bytes: buf, text: buf.toString('utf8') };
|
||||
}
|
||||
|
||||
/** 抓線上的世代資產,算指紋。抓不到的當 MISSING(照樣算,缺檔本來就是另一代)。 */
|
||||
async function liveGeneration(base) {
|
||||
const entries = [];
|
||||
const detail = {};
|
||||
for (const { file, urlPath } of GENERATION_ASSETS) {
|
||||
try {
|
||||
const r = await get(`${base.replace(/\/$/, '')}${urlPath}`);
|
||||
const ok = r.status === 200;
|
||||
entries.push({ file, bytes: ok ? r.bytes : null });
|
||||
detail[file] = { status: r.status, text: ok ? r.text : null };
|
||||
} catch (e) {
|
||||
entries.push({ file, bytes: null });
|
||||
detail[file] = { status: `連線失敗:${e.message}`, text: null };
|
||||
}
|
||||
}
|
||||
return { ...fingerprintOf(entries), detail };
|
||||
}
|
||||
|
||||
/** 驗一個網址。t 給 null=只驗世代(ad-hoc 模式)。 */
|
||||
export async function verifyUrl(t, url, want) {
|
||||
const checks = [];
|
||||
const base = url.replace(/\/$/, '');
|
||||
const live = await liveGeneration(base);
|
||||
|
||||
// ── 世代層 ──────────────────────────────────────────────
|
||||
const genOk = live.digest === want.digest;
|
||||
const diffs = Object.entries(want.assets)
|
||||
.filter(([f, a]) => live.assets[f]?.sha !== a.sha)
|
||||
.map(([f, a]) => {
|
||||
const l = live.assets[f] ?? {};
|
||||
const st = live.detail[f]?.status;
|
||||
return `${f}:repo ${a.size ?? '缺'} bytes / 線上 ${l.missing ? `抓不到(${st})` : `${l.size} bytes`}`;
|
||||
});
|
||||
checks.push({
|
||||
name: '世代',
|
||||
ok: genOk,
|
||||
want: `${want.digest.slice(0, 12)}(repo public/)`,
|
||||
got: genOk
|
||||
? `${live.digest.slice(0, 12)}`
|
||||
: `${live.digest.slice(0, 12)}\n 不同的資產:\n ${diffs.join('\n ')}`,
|
||||
});
|
||||
|
||||
if (!t) return { url, ok: genOk, checks };
|
||||
|
||||
// ── 組態層 ──────────────────────────────────────────────
|
||||
try {
|
||||
const cfg = await get(`${base}/config.js`);
|
||||
const got = cfg.status === 200 ? parseApiBase(cfg.text) : `HTTP ${cfg.status}`;
|
||||
checks.push({ name: 'apiBase', ok: got === t.apiBase, want: t.apiBase, got: got ?? '(config.js 裡找不到 apiBase)' });
|
||||
} catch (e) {
|
||||
checks.push({ name: 'apiBase', ok: false, want: t.apiBase, got: `連線失敗:${e.message}` });
|
||||
}
|
||||
|
||||
const con = live.detail['console/index.html'];
|
||||
const conText = con?.text;
|
||||
const views = conText?.match(/var VIEWS = (\[[^\]]*\]);/);
|
||||
const home = conText?.match(/var HOME = "([^"]*)";/);
|
||||
const gotViews = conText ? (views ? views[1] : '(找不到 VIEWS)') : `HTTP ${con?.status}`;
|
||||
const gotHome = conText ? (home ? home[1] : '(找不到 HOME)') : `HTTP ${con?.status}`;
|
||||
checks.push({
|
||||
name: `profile(${t.profile}).views`,
|
||||
ok: gotViews === JSON.stringify(t.views),
|
||||
want: JSON.stringify(t.views),
|
||||
got: gotViews,
|
||||
});
|
||||
checks.push({ name: `profile(${t.profile}).home`, ok: gotHome === t.home, want: t.home, got: gotHome });
|
||||
|
||||
return { url, ok: checks.every((c) => c.ok), checks };
|
||||
}
|
||||
|
||||
export async function verifyTarget(name, { wait = false } = {}) {
|
||||
const t = resolveTarget(name);
|
||||
if (t.frozen) return { name, target: t, skipped: true, ok: true, results: [] };
|
||||
const want = generationOfDir();
|
||||
const attempts = wait ? 8 : 1;
|
||||
let results = [];
|
||||
for (let i = 1; i <= attempts; i++) {
|
||||
results = [];
|
||||
for (const url of t.verifyUrls) results.push(await verifyUrl(t, url, want));
|
||||
if (results.every((r) => r.ok) || i === attempts) break;
|
||||
process.stdout.write(` … 尚未生效,5s 後重試(${i}/${attempts - 1})\n`);
|
||||
await new Promise((r) => setTimeout(r, 5000));
|
||||
}
|
||||
return { name, target: t, ok: results.every((r) => r.ok), results };
|
||||
}
|
||||
|
||||
export function printReport(reports) {
|
||||
for (const r of reports) {
|
||||
console.log(`\n【${r.name}】${r.target.description}`);
|
||||
if (r.skipped) {
|
||||
console.log(` ⏸️ 已凍結,不抓不驗:${r.target.frozen}`);
|
||||
continue;
|
||||
}
|
||||
console.log(` 宣告:profile=${r.target.profile} apiBase=${r.target.apiBase}`);
|
||||
for (const u of r.results) {
|
||||
console.log(` ${u.ok ? '✅' : '❌'} ${u.url}`);
|
||||
for (const c of u.checks) {
|
||||
if (c.ok) console.log(` ✓ ${c.name} = ${c.got}`);
|
||||
else console.log(` ✗ ${c.name}\n 我們手上:${c.want}\n 線上跑的:${c.got}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyAll(names, opts) {
|
||||
const reports = [];
|
||||
for (const n of names) reports.push(await verifyTarget(n, opts));
|
||||
return reports;
|
||||
}
|
||||
|
||||
const isCli = process.argv[1] && import.meta.url === `file://${process.argv[1]}`;
|
||||
if (isCli) {
|
||||
const args = process.argv.slice(2);
|
||||
const wait = args.includes('--wait');
|
||||
const urlIdx = args.indexOf('--url');
|
||||
|
||||
if (args.includes('--offline-lag')) {
|
||||
// 不連網,只問一句:「我手上這一代,有沒有真的送出去過?」
|
||||
// 給 Stop hook 用(每回合都跑,所以不准連網、不准慢)。
|
||||
// 唯一的事實來源是 .deploy-state.json,而它**只在線上實測通過後**才被寫(見 deploy.mjs)
|
||||
// ⇒ 它說綠就是真的有人驗過線上,不是「我跑過部署指令」。
|
||||
const here = generationOfDir().digest;
|
||||
const state = readState();
|
||||
const stale = [];
|
||||
for (const n of loadTargets().active) {
|
||||
const s = state[n];
|
||||
if (!s) stale.push(`${n}:沒有任何一次通過線上實測的部署紀錄(線上是哪一代,現在沒人知道)`);
|
||||
else if (s.generation !== here) {
|
||||
stale.push(`${n}:最後一次驗過的是 ${s.generation.slice(0, 12)}(${s.verifiedAt.slice(0, 10)}),現在手上是 ${here.slice(0, 12)}`);
|
||||
}
|
||||
}
|
||||
if (stale.length) {
|
||||
console.log(stale.join('\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (urlIdx !== -1) {
|
||||
// ad-hoc:只問「這個網址上跑的是不是當代的」——不需要它是宣告過的目標。
|
||||
const url = args[urlIdx + 1];
|
||||
if (!url) {
|
||||
console.error('用法:node scripts/verify-live.mjs --url <網址>');
|
||||
process.exit(2);
|
||||
}
|
||||
const want = generationOfDir();
|
||||
const r = await verifyUrl(null, url, want);
|
||||
console.log(`\n【世代檢查】${url}`);
|
||||
for (const c of r.checks) {
|
||||
if (c.ok) console.log(` ✅ ${c.name} = ${c.got}`);
|
||||
else console.log(` ❌ ${c.name}\n 我們手上:${c.want}\n 線上跑的:${c.got}`);
|
||||
}
|
||||
if (!r.ok) {
|
||||
console.error('\n❌ 這個網址上跑的不是當代的前端——它展示的是一套已經被淘汰的介面。');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('\n✅ 這個網址上跑的=我們手上這一份。');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const picked = args.filter((a) => !a.startsWith('--'));
|
||||
const names = picked.length ? picked : loadTargets().names;
|
||||
const reports = await verifyAll(names, { wait });
|
||||
printReport(reports);
|
||||
const bad = reports.filter((r) => !r.ok);
|
||||
if (bad.length) {
|
||||
console.error(`\n❌ ${bad.length} 個目標與宣告/當代不符:${bad.map((b) => b.name).join('、')}`);
|
||||
console.error(' (線上實際在跑的 ≠ 我們手上這一份——這正是要被擋掉的那個病)');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('\n✅ 所有服役中目標:線上組態=宣告值,線上世代=repo 當代。');
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
name = "arcrun-console-ui"
|
||||
pages_build_output_dir = "public"
|
||||
compatibility_date = "2026-07-21"
|
||||
@@ -6,113 +6,88 @@
|
||||
*
|
||||
* 嚴格邊界(rule 02 §2.2):
|
||||
* - 本檔**不做**任何 credential 解密 / template 展開 / JWT 簽章
|
||||
* - 那些全部在 auth primitive WASM 零件內執行(透過 host function `crypto_decrypt` 等)
|
||||
* - 那些全部在 auth primitive WASM 零件內執行(透過 host function `secret_get` 等)
|
||||
* - 本檔只做「查 recipe 決定走哪個 primitive Worker」+「HTTP fetch 取回注入結果」
|
||||
*
|
||||
* 目前階段接上 `auth_static_key` + `auth_service_account` + `auth_oauth2`,
|
||||
* Phase 4 剩 `auth_mtls`(mTLS handshake 在 Worker runtime 層)。
|
||||
*
|
||||
* 執行時機:graph-executor 在節點 runner 執行前呼叫,取回的 ctx 會:
|
||||
* 1. 先試本 dispatcher(命中才 return enriched ctx)
|
||||
* 2. 沒命中 fallback 到 `injectCredentials`(Phase 1.9 才刪除)
|
||||
* 1. 本 dispatcher 命中 → return enriched ctx
|
||||
* 2. 沒命中 → ctx 原樣往下(T10 起舊的 injectCredentials 雙讀 fallback 已移除)
|
||||
*/
|
||||
|
||||
import type { Bindings } from '../types';
|
||||
import { resolveAuthRecipe, resolveRecipe } from '../routes/recipes';
|
||||
import { wasmWorkerUrl } from '../lib/component-loader';
|
||||
import { createArcrunHostFunctions } from '../lib/wasi-shim';
|
||||
import { getCredentialSecretRefs, touchLastUsed } from '../routes/credentials';
|
||||
|
||||
// ── credential-store 遷移 T6/T7(方案 A,D19)────────────────────────────────
|
||||
// ── credential-store 遷移 T6/T7(方案 A,D19)+ D38 圍牆修復(2026-08-07)───────────
|
||||
//
|
||||
// 密文值住 cypher-executor 自己的 per-script secrets(T5 寫入)。解密發生在獨立的
|
||||
// auth_static_key / auth_service_account worker 上,它們讀不到 cypher 的 secrets。
|
||||
// 故 cypher 這一層先查 D1 拿 secret_ref → 用 secret_get(ref)(即 env[ref],T4)取明文
|
||||
// → 塞進送給 auth WASM 的 payload 新欄位 `resolved_secrets`。WASM 收到優先用它,沒有
|
||||
// 才 fallback 舊 KV + crypto_decrypt(那個 fallback 即 T7 雙讀)。
|
||||
// 故 cypher 這一層先取這個租戶的 credential 目錄(name → secret_ref)→ 用 secret_get(ref)
|
||||
// (即 env[ref],T4)取明文 → 塞進送給 auth WASM 的 payload 新欄位 `resolved_secrets`。
|
||||
// WASM 收到優先用它,沒有才 fallback 舊 KV + crypto_decrypt(那個 fallback 即 T7 雙讀)。
|
||||
//
|
||||
// 嚴格邊界(rule 02 §2.2):本檔只做「查 D1 ref → secret_get 取值 → 當字串塞 payload」。
|
||||
// D38(leo 2026-06-14 立、2026-08-07 擴大):目錄不再直連 D1,改走 KBDB HTTP API
|
||||
// (`credentials.ts` 的 `getCredentialSecretRefs`,內建 60 秒租戶級快取——這是熱路徑,
|
||||
// 每次 workflow 執行都會呼叫,映射「幾乎不變」故快取後多數命中零網路呼叫,效能不因改走
|
||||
// API 而變差,見 credentials.ts 檔頭「效能」段的實測數字)。
|
||||
//
|
||||
// 嚴格邊界(rule 02 §2.2):本檔只做「查目錄拿 ref → secret_get 取值 → 當字串塞 payload」。
|
||||
// **不解密、不展開模板、不組 JWT**——secret_get 的實作(env[ref])在 wasi-shim host function
|
||||
// 內,解密/注入邏輯仍全在 WASM 零件。
|
||||
|
||||
/** D1 credentials 目錄一列(只取本檔需要的欄位)。 */
|
||||
interface CredentialRefRow {
|
||||
name: string;
|
||||
secret_ref: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 對一組 credential name,從新家(cypher per-script secrets)取明文。
|
||||
*
|
||||
* 流程:查 D1 `credentials`(api_key + name)拿 `secret_ref` → 用 `secret_get(ref)`
|
||||
* (host function,實作 = env[ref])取值。
|
||||
* 流程:查 KBDB credential 目錄(api_key + name,快取命中零網路呼叫)拿 `secret_ref`
|
||||
* → 用 `secret_get(ref)`(host function,實作 = env[ref])取值。
|
||||
*
|
||||
* ⚠️ 只把「D1 有 ref 且 secret_get 真的取到值」的 name 放進回傳 map。查不到 ref、
|
||||
* ⚠️ 只把「目錄有 ref 且 secret_get 真的取到值」的 name 放進回傳 map。查不到 ref、
|
||||
* 或 secret_get 回 null(新家還沒這把值)→ **該 name 缺席**(不是放空字串!),
|
||||
* 讓 WASM 對這把 key 走 fallback 舊 KV 路徑(T7 雙讀)。放空字串會讓 WASM 誤判命中用空值。
|
||||
*
|
||||
* 取到值的 name 順手更新 D1 `last_used_at`(§2.5 治理面 last_used)。
|
||||
* 取到值的 name 順手更新 last_used_at(§2.5 治理面 last_used,見 touchLastUsed——
|
||||
* fire-and-forget、非同步、不阻塞本函式回傳,失敗吞掉)。
|
||||
*
|
||||
* D1 未建表 / migration 未跑 / CREDENTIALS_DB 未綁 → 回空 map(整組走 fallback),
|
||||
* KBDB 不可達 / 這個租戶還沒有任何 credential → 回空 map(整組走 fallback),
|
||||
* 不 throw——遷移過渡期(雙讀)本就允許「新家還沒資料」。
|
||||
*/
|
||||
/** credential name → 明文值對照(獨立型別別名,避免函式簽章直接內嵌逗號分隔泛型)。 */
|
||||
type ResolvedSecretMap = Record<string, string>;
|
||||
|
||||
export async function resolveSecretsFromNewHome(
|
||||
env: Bindings,
|
||||
apiKey: string,
|
||||
names: string[],
|
||||
): Promise<Record<string, string>> {
|
||||
const resolved: Record<string, string> = {};
|
||||
): Promise<ResolvedSecretMap> {
|
||||
const resolved: ResolvedSecretMap = {};
|
||||
if (names.length === 0) return resolved;
|
||||
|
||||
const db = env.CREDENTIALS_DB;
|
||||
if (!db) return resolved; // 未綁 D1 → 整組走 fallback
|
||||
|
||||
// 1. 查 D1 拿每個 name 的 secret_ref
|
||||
let rows: CredentialRefRow[];
|
||||
try {
|
||||
const placeholders = names.map(() => '?').join(', ');
|
||||
const result = await db
|
||||
.prepare(
|
||||
`SELECT name, secret_ref FROM credentials
|
||||
WHERE api_key = ? AND name IN (${placeholders})`,
|
||||
)
|
||||
.bind(apiKey, ...names)
|
||||
.all<CredentialRefRow>();
|
||||
rows = result.results ?? [];
|
||||
} catch {
|
||||
// D1 未建表 / query 失敗 → 過渡期整組走 fallback(雙讀),不假綠
|
||||
return resolved;
|
||||
}
|
||||
if (rows.length === 0) return resolved;
|
||||
// 1. 拿這個租戶的 credential 目錄(name → secret_ref,快取層見 credentials.ts)
|
||||
const refs = await getCredentialSecretRefs(env, apiKey);
|
||||
if (Object.keys(refs).length === 0) return resolved; // 目錄空 / KBDB 不可達 → 整組走 fallback
|
||||
|
||||
// 2. 用 secret_ref 從新家取值(host function secret_get = env[ref])
|
||||
const secretGet = createArcrunHostFunctions(env, apiKey).secret_get;
|
||||
if (!secretGet) return resolved; // host function 未就緒 → 走 fallback
|
||||
|
||||
const resolvedNames: string[] = [];
|
||||
for (const row of rows) {
|
||||
const value = await secretGet(row.secret_ref);
|
||||
for (const name of names) {
|
||||
const ref = refs[name];
|
||||
if (!ref) continue; // 目錄沒這個 name → 缺席,走 fallback
|
||||
const value = await secretGet(ref);
|
||||
// null(新家沒這把值 / 非 CRED_ 前綴被拒)→ 不放進 map,讓 WASM fallback 舊 KV
|
||||
if (value === null) continue;
|
||||
resolved[row.name] = value;
|
||||
resolvedNames.push(row.name);
|
||||
resolved[name] = value;
|
||||
resolvedNames.push(name);
|
||||
}
|
||||
|
||||
// 3. 順手更新 last_used_at(只更新真的從新家取到值的 name)
|
||||
if (resolvedNames.length > 0) {
|
||||
try {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const placeholders = resolvedNames.map(() => '?').join(', ');
|
||||
await db
|
||||
.prepare(
|
||||
`UPDATE credentials SET last_used_at = ?
|
||||
WHERE api_key = ? AND name IN (${placeholders})`,
|
||||
)
|
||||
.bind(now, apiKey, ...resolvedNames)
|
||||
.run();
|
||||
} catch {
|
||||
// last_used 更新失敗不影響注入主流程(治理面欄位,非關鍵路徑)
|
||||
}
|
||||
}
|
||||
// 3. 順手更新 last_used_at(只更新真的從新家取到值的 name;fire-and-forget,非關鍵路徑)
|
||||
if (resolvedNames.length > 0) touchLastUsed(env, apiKey, resolvedNames);
|
||||
|
||||
return resolved;
|
||||
}
|
||||
@@ -250,7 +225,7 @@ function replaceCredentialRefs(value: unknown, resolved: Record<string, string>)
|
||||
*
|
||||
* 嚴格邊界(rule 02 §2.2):本函式**不解密**。偵測到 {{credential.X}} 後,把 names 交給
|
||||
* auth_static_key WASM 的 `resolve_credentials` action(WASM 內 kv_get + crypto_decrypt),
|
||||
* 拿回明文後只做字串回填。ENCRYPTION_KEY 永不經此處。
|
||||
* 拿回明文後只做字串回填。本檔不解密、不持有任何金鑰。
|
||||
*
|
||||
* - 無 {{credential.}} → 原樣回傳(不打 WASM,零開銷)
|
||||
* - 解密失敗 / 缺 credential → throw(誠實報錯,不假綠)
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
/**
|
||||
* Credential Injector
|
||||
*
|
||||
* 執行順序:
|
||||
* 1. 檢查是否有對應的 auth recipe(auth_recipe:{componentId} in RECIPES KV)
|
||||
* → 有:走 auth recipe 路徑(支援 static_key, service_account)
|
||||
* → 無:走舊有 flat injection 路徑(向後相容)
|
||||
*
|
||||
* Auth Recipe 路徑:
|
||||
* - static_key:展開 inject.header/query/body 的 {{secret.KEY}} 模板
|
||||
* - service_account:JWT signing → token exchange → 展開 {{runtime.access_token}}
|
||||
* - 注入結果以 _auth_headers / _auth_query / _auth_body 攜帶,不污染業務欄位
|
||||
*
|
||||
* 舊有路徑(向後相容):
|
||||
* - 從 RECIPES KV 讀取 credentials_required(動態 recipe)
|
||||
* - 或從 BUILTIN_CREDENTIALS_MAP(內建清單)
|
||||
* - 解密後以 inject_as 欄位名稱直接注入 context
|
||||
*/
|
||||
|
||||
import type { Bindings } from '../types';
|
||||
import { resolveRecipe, resolveAuthRecipe } from '../routes/recipes';
|
||||
import type { AuthRecipeDefinition } from '../routes/recipes';
|
||||
|
||||
export interface CredentialRequirement {
|
||||
key: string; // CREDENTIALS_KV 的 credential 名稱(如 gmail_token)
|
||||
inject_as: string; // 注入到 input 的欄位名稱(如 access_token)
|
||||
}
|
||||
|
||||
/** 內建 API recipe 的 credentials_required(對應 component-loader 的 BUILTIN_API_RECIPES)*/
|
||||
const BUILTIN_CREDENTIALS_MAP: Record<string, CredentialRequirement[]> = {
|
||||
gmail: [{ key: 'gmail_token', inject_as: 'access_token' }],
|
||||
google_sheets: [{ key: 'google_oauth', inject_as: 'access_token' }],
|
||||
telegram: [{ key: 'telegram_bot_token', inject_as: 'bot_token' }],
|
||||
line_notify: [{ key: 'line_token', inject_as: 'token' }],
|
||||
};
|
||||
|
||||
// ── AES-GCM 解密 ──────────────────────────────────────────────────────────────
|
||||
|
||||
async function decryptCredential(encryptedJson: string, encryptionKey: string): Promise<string> {
|
||||
const { encrypted, iv } = JSON.parse(encryptedJson) as { encrypted: string; iv: string };
|
||||
|
||||
const keyBytes = hexToUint8Array(encryptionKey);
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt'],
|
||||
);
|
||||
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: base64ToUint8Array(iv) },
|
||||
cryptoKey,
|
||||
base64ToUint8Array(encrypted),
|
||||
);
|
||||
|
||||
return new TextDecoder().decode(decrypted);
|
||||
}
|
||||
|
||||
function hexToUint8Array(hex: string): Uint8Array {
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function base64ToUint8Array(b64: string): Uint8Array {
|
||||
const binary = atob(b64);
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// ── 解密所有 required_secrets → { key: decryptedValue } ──────────────────────
|
||||
|
||||
async function decryptSecrets(
|
||||
recipe: AuthRecipeDefinition,
|
||||
apiKey: string,
|
||||
env: Bindings,
|
||||
): Promise<Record<string, string>> {
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
for (const req of recipe.required_secrets) {
|
||||
if (req.optional) continue;
|
||||
|
||||
const kvKey = `${apiKey}:cred:${req.key}`;
|
||||
const record = await env.CREDENTIALS_KV.get(kvKey);
|
||||
|
||||
if (!record) {
|
||||
throw new Error(
|
||||
`缺少 credential:${req.key}(${req.label})\n` +
|
||||
`修復步驟:\n` +
|
||||
` 1. 在 credentials.yaml 加入 ${req.key}: "your-value"\n` +
|
||||
` 2. 執行:acr creds push`,
|
||||
);
|
||||
}
|
||||
|
||||
result[req.key] = await decryptCredential(record, env.ENCRYPTION_KEY);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Template 展開:{{secret.KEY}} 和 {{runtime.KEY}} ─────────────────────────
|
||||
|
||||
function interpolateTemplate(
|
||||
template: string,
|
||||
secrets: Record<string, string>,
|
||||
runtime: Record<string, string>,
|
||||
): string {
|
||||
return template.replace(/\{\{(secret|runtime)\.(\w+)\}\}/g, (_, ns, key) => {
|
||||
if (ns === 'secret') return secrets[key] ?? '';
|
||||
if (ns === 'runtime') return runtime[key] ?? '';
|
||||
return '';
|
||||
});
|
||||
}
|
||||
|
||||
function interpolateRecord(
|
||||
record: Record<string, string>,
|
||||
secrets: Record<string, string>,
|
||||
runtime: Record<string, string>,
|
||||
): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
for (const [k, v] of Object.entries(record)) {
|
||||
result[k] = interpolateTemplate(v, secrets, runtime);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Auth Recipe 注入(新路徑)────────────────────────────────────────────────
|
||||
|
||||
async function injectFromAuthRecipe(
|
||||
recipe: AuthRecipeDefinition,
|
||||
input: Record<string, unknown>,
|
||||
env: Bindings,
|
||||
apiKey: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
// 解密所有 required_secrets
|
||||
const secrets = await decryptSecrets(recipe, apiKey, env);
|
||||
|
||||
// runtime token:service_account 路徑已改走 auth-dispatcher → auth_service_account WASM;
|
||||
// 這條 TS fallback 只處理 static_key (runtime 為空即可),service_account 永遠不會走到這裡
|
||||
const runtime: Record<string, string> = {};
|
||||
|
||||
if (recipe.primitive === 'service_account') {
|
||||
throw new Error(
|
||||
`service_account primitive 應由 auth-dispatcher → auth_service_account WASM 處理,` +
|
||||
`不應進到 credential-injector TS fallback (service=${recipe.service})`,
|
||||
);
|
||||
}
|
||||
|
||||
// 展開 inject 模板
|
||||
const authHeaders = recipe.inject.header
|
||||
? interpolateRecord(recipe.inject.header, secrets, runtime)
|
||||
: {};
|
||||
const authQuery = recipe.inject.query
|
||||
? interpolateRecord(recipe.inject.query, secrets, runtime)
|
||||
: {};
|
||||
const authBody = recipe.inject.body
|
||||
? interpolateRecord(recipe.inject.body, secrets, runtime)
|
||||
: {};
|
||||
|
||||
return {
|
||||
...input,
|
||||
_auth_headers: authHeaders,
|
||||
_auth_query: authQuery,
|
||||
_auth_body: authBody,
|
||||
};
|
||||
}
|
||||
|
||||
// ── 舊有路徑:flat injection(向後相容)──────────────────────────────────────
|
||||
|
||||
async function loadCredentialsRequired(
|
||||
componentId: string,
|
||||
env: Bindings,
|
||||
): Promise<CredentialRequirement[]> {
|
||||
const recipe = await resolveRecipe(componentId, env.RECIPES);
|
||||
if (recipe?.credentials_required?.length) {
|
||||
return recipe.credentials_required;
|
||||
}
|
||||
return BUILTIN_CREDENTIALS_MAP[componentId] ?? [];
|
||||
}
|
||||
|
||||
// ── 主入口 ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* 執行 credential 注入。
|
||||
*
|
||||
* @param componentId - 零件 canonical_id 或 hash
|
||||
* @param input - 節點的 merged context
|
||||
* @param env - Cloudflare Worker Bindings
|
||||
* @param apiKey - 用戶的 API Key(ak_前綴),作為 KV namespace
|
||||
*/
|
||||
export async function injectCredentials(
|
||||
componentId: string,
|
||||
input: Record<string, unknown>,
|
||||
env: Bindings,
|
||||
apiKey?: string,
|
||||
): Promise<Record<string, unknown>> {
|
||||
// 沒有 api_key → local 模式,略過
|
||||
if (!apiKey) return input;
|
||||
|
||||
// ── 新路徑:auth recipe ──
|
||||
const authRecipe = await resolveAuthRecipe(componentId, env.RECIPES);
|
||||
if (authRecipe) {
|
||||
return injectFromAuthRecipe(authRecipe, input, env, apiKey);
|
||||
}
|
||||
|
||||
// ── 舊路徑:flat injection(向後相容)──
|
||||
const required = await loadCredentialsRequired(componentId, env);
|
||||
if (required.length === 0) return input;
|
||||
|
||||
const enriched = { ...input };
|
||||
|
||||
for (const cred of required) {
|
||||
const kvKey = `${apiKey}:cred:${cred.key}`;
|
||||
const record = await env.CREDENTIALS_KV.get(kvKey);
|
||||
|
||||
if (!record) {
|
||||
throw new Error(
|
||||
`缺少 credential:${cred.key}\n` +
|
||||
`修復步驟:\n` +
|
||||
` 1. 在 credentials.yaml 中加入 ${cred.key}: "your-token"\n` +
|
||||
` 2. 執行:acr creds push`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const decrypted = await decryptCredential(record, env.ENCRYPTION_KEY);
|
||||
enriched[cred.inject_as] = decrypted;
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`credential "${cred.key}" 解密失敗:${e instanceof Error ? e.message : String(e)}\n` +
|
||||
`修復步驟:重新執行 acr creds push。`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return enriched;
|
||||
}
|
||||
@@ -3,24 +3,33 @@ import { ExecutionError, WorkflowPaused } from '../types';
|
||||
import { GraphExecutor } from '../graph-executor';
|
||||
import { graphSchema } from '../lib/schemas';
|
||||
import { createComponentLoader } from '../lib/component-loader';
|
||||
import { writeEvaluation, updateComponentStats } from './execution-evaluator';
|
||||
import { recordComponentStats } from './execution-evaluator';
|
||||
import { parseTriplets } from './triplet-parser';
|
||||
import { searchNodes } from './search-nodes';
|
||||
import { searchNodes, type SearchMode, type SearchTarget } from './search-nodes';
|
||||
import { buildExecutionGraph } from './graph-builder';
|
||||
|
||||
export async function handleCypherSearch(
|
||||
triplets: unknown[],
|
||||
env: Bindings,
|
||||
mode: SearchMode = 'discover',
|
||||
target?: SearchTarget,
|
||||
): Promise<{ nodes: Record<string, unknown>; cypher: unknown; missing: string[] }> {
|
||||
const parsed = parseTriplets(triplets);
|
||||
if (!parsed) {
|
||||
throw new Error('無法解析任何節點');
|
||||
}
|
||||
|
||||
const { nodeResults } = searchNodes(parsed);
|
||||
// 2026-07-30:查 registry 判真實存在(workflow-discovery)。
|
||||
// `missing` 以前寫死 [],等於告訴 AI「什麼都有」——那是「腹語術」的入口。
|
||||
//
|
||||
// t158(07-31 迴歸修復,leo:「這裡只是複製一些工作流的 data 過去,沒有要在這裡驗證」):
|
||||
// 誠實化只屬於 **discover**(AI 問「有沒有」);**compile**(部署/推送的複製路徑)
|
||||
// 純編圖零查詢——那本來就是既有設計(workflows.json=打包期預編的搬運),
|
||||
// 5cadc60 起誠實化漏進複製路徑=迴歸(冷實例 8 節點 25.7s、安裝器 timeout 炸)。
|
||||
const { nodeResults, missingNodes } = await searchNodes(parsed, undefined, env, mode, target);
|
||||
|
||||
const graph = buildExecutionGraph(parsed, nodeResults, 'cypher-search-result', 'Cypher Search Result');
|
||||
return { nodes: nodeResults, cypher: { nodes: graph.nodes, edges: graph.edges }, missing: [] };
|
||||
return { nodes: nodeResults, cypher: { nodes: graph.nodes, edges: graph.edges }, missing: missingNodes };
|
||||
}
|
||||
|
||||
export async function handleCypherExecute(
|
||||
@@ -50,7 +59,9 @@ export async function handleCypherExecute(
|
||||
throw new Error('無法解析任何節點');
|
||||
}
|
||||
|
||||
const { nodeResults } = searchNodes(parsed, config);
|
||||
// t158:執行路徑=compile(零 discovery round-trip)——存在性由 component-loader
|
||||
// 在載入該節點時決定(原本的權威),查詢層不重複驗。
|
||||
const { nodeResults } = await searchNodes(parsed, config, env, 'compile');
|
||||
|
||||
const graph = buildExecutionGraph(parsed, nodeResults, graphId, graphName, config);
|
||||
const parseResult = graphSchema.safeParse(graph);
|
||||
@@ -66,18 +77,8 @@ export async function handleCypherExecute(
|
||||
const result = await executor.execute(parseResult.data as ExecutionGraph, context ?? {}, env.EXEC_CONTEXT);
|
||||
const duration_ms = Date.now() - start;
|
||||
|
||||
// 非同步記錄統計(Phase 7 補充 analytics,目前為 no-op)
|
||||
const componentId = graph.nodes.find(n => n.componentId)?.componentId ?? graphId;
|
||||
const runId = `${graphId}-${Date.now()}`;
|
||||
waitUntil(writeEvaluation(env, {
|
||||
run_id: runId,
|
||||
workflow_id: graphId,
|
||||
component_id: componentId,
|
||||
verdict: 'success',
|
||||
duration_ms,
|
||||
evaluated_at: Date.now(),
|
||||
}));
|
||||
waitUntil(updateComponentStats(env, componentId, 'success', duration_ms));
|
||||
// 非同步回寫每顆零件的執行統計(design.md「執行統計設計」;fire-and-forget 不阻擋回應)
|
||||
waitUntil(recordComponentStats(env, graph.nodes, result.trace));
|
||||
|
||||
return { success: true, data: result.data, trace: result.trace, duration_ms, graph };
|
||||
} catch (err) {
|
||||
@@ -99,19 +100,10 @@ export async function handleCypherExecute(
|
||||
}
|
||||
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
const componentId = graph.nodes.find(n => n.componentId)?.componentId ?? graphId;
|
||||
const runId = `${graphId}-${Date.now()}`;
|
||||
waitUntil(writeEvaluation(env, {
|
||||
run_id: runId,
|
||||
workflow_id: graphId,
|
||||
component_id: componentId,
|
||||
verdict: 'failed',
|
||||
duration_ms,
|
||||
error_message: errMsg.slice(0, 200),
|
||||
evaluated_at: Date.now(),
|
||||
}));
|
||||
waitUntil(updateComponentStats(env, componentId, 'failed', duration_ms));
|
||||
// 失敗路徑同樣回寫每顆零件統計:ExecutionError 帶完整 trace(失敗節點有 error、
|
||||
// 之前成功的節點照記成功);非 ExecutionError 無 trace 可歸因 → 不記(誠實:不瞎猜)。
|
||||
if (err instanceof ExecutionError) {
|
||||
waitUntil(recordComponentStats(env, graph.nodes, err.trace));
|
||||
const traceFormatted = err.trace.map(s => ({
|
||||
node: s.nodeId,
|
||||
status: s.error ? 'failed' : 'success',
|
||||
|
||||
@@ -1,36 +1,96 @@
|
||||
/**
|
||||
* Execution Analytics — 零件執行後的統計記錄
|
||||
* Execution Analytics — 零件執行後的統計回寫
|
||||
*
|
||||
* Phase 1 MVP:stub(不寫入任何外部服務)
|
||||
* Phase 7 補充:fire-and-forget POST 至 registry.arcrun.dev/analytics/record
|
||||
* SDD: system-dev/docs/3-specs/arcrun-core-mvp/design.md「執行統計設計」
|
||||
* 執行完成處(cypher-handlers / webhook-handlers 收尾)對本次用到的**每顆零件**
|
||||
* fire-and-forget POST registry `/analytics/record`——統計失敗不影響執行、不增加同步延遲
|
||||
* (呼叫端一律用 waitUntil 包,仿 recordRecipeStats / recordTelemetry 既有慣例)。
|
||||
*
|
||||
* 每顆零件的成敗判定來源=執行 trace(per-node):
|
||||
* - trace step 有 `error` → 失敗(runner throw)
|
||||
* - output 是物件且 `success === false` → 失敗(makeHttpRunner 對非 2xx 不 throw,回這種)
|
||||
* - 其餘 → 成功
|
||||
* FOREACH 重複執行同一節點 → trace 有幾筆就記幾次(每次真實執行都算一次樣本)。
|
||||
*/
|
||||
|
||||
import type { Bindings } from '../types';
|
||||
import type { GraphNode, TraceStep } from '../types';
|
||||
import { wasmWorkerUrl } from '../lib/component-loader';
|
||||
|
||||
export interface EvaluationRecord {
|
||||
run_id: string;
|
||||
workflow_id: string;
|
||||
/** 本模組需要的環境子集(傳整份 Bindings 也相容,仿 SearchNodesEnv 慣例)。 */
|
||||
export type AnalyticsEnv = {
|
||||
WORKER_SUBDOMAIN?: string;
|
||||
/** registry 位置覆蓋(可選;本地 wrangler dev / self-hosted 用)。未設 → wasmWorkerUrl('registry', WORKER_SUBDOMAIN)。 */
|
||||
REGISTRY_BASE_URL?: string;
|
||||
};
|
||||
|
||||
export interface ComponentVerdict {
|
||||
component_id: string;
|
||||
verdict: 'success' | 'failed' | 'timeout';
|
||||
success: boolean;
|
||||
duration_ms: number;
|
||||
error_message?: string;
|
||||
evaluated_at: number;
|
||||
}
|
||||
|
||||
/** 記錄執行結果(MVP:no-op,Phase 7 補充 analytics)*/
|
||||
export async function writeEvaluation(
|
||||
_env: Bindings,
|
||||
_record: EvaluationRecord,
|
||||
): Promise<void> {
|
||||
// Phase 7: POST to registry.arcrun.dev/analytics/record
|
||||
/** 從執行 trace 導出每顆零件的成敗(只算 type=Component 且有 componentId 的節點)。 */
|
||||
export function componentVerdictsFromTrace(
|
||||
nodes: GraphNode[],
|
||||
trace: TraceStep[],
|
||||
): ComponentVerdict[] {
|
||||
const componentByNodeId = new Map<string, string>();
|
||||
for (const n of nodes) {
|
||||
if (n.type === 'Component' && n.componentId) componentByNodeId.set(n.id, n.componentId);
|
||||
}
|
||||
|
||||
const verdicts: ComponentVerdict[] = [];
|
||||
for (const step of trace) {
|
||||
const componentId = componentByNodeId.get(step.nodeId);
|
||||
if (!componentId) continue;
|
||||
|
||||
const out = step.output;
|
||||
const outputSaysFailed =
|
||||
typeof out === 'object' && out !== null && !Array.isArray(out) &&
|
||||
(out as Record<string, unknown>).success === false;
|
||||
|
||||
verdicts.push({
|
||||
component_id: componentId,
|
||||
success: !step.error && !outputSaysFailed,
|
||||
duration_ms: Math.max(0, Number(step.duration_ms) || 0),
|
||||
});
|
||||
}
|
||||
return verdicts;
|
||||
}
|
||||
|
||||
/** 更新零件統計(MVP:no-op,Phase 7 補充)*/
|
||||
export async function updateComponentStats(
|
||||
_env: Bindings,
|
||||
_componentId: string,
|
||||
_verdict: 'success' | 'failed' | 'timeout',
|
||||
_durationMs: number,
|
||||
/**
|
||||
* 對本次執行用到的每顆零件回寫統計到 registry(design.md「Analytics Record」)。
|
||||
* 永不 throw;呼叫端用 waitUntil 包,不阻擋主流程。
|
||||
*/
|
||||
export async function recordComponentStats(
|
||||
env: AnalyticsEnv,
|
||||
nodes: GraphNode[],
|
||||
trace: TraceStep[],
|
||||
): Promise<void> {
|
||||
// Phase 7: update ANALYTICS_KV via registry worker
|
||||
try {
|
||||
const base = (
|
||||
env.REGISTRY_BASE_URL ??
|
||||
(env.WORKER_SUBDOMAIN ? wasmWorkerUrl('registry', env.WORKER_SUBDOMAIN) : undefined)
|
||||
)?.replace(/\/$/, '');
|
||||
if (!base) return;
|
||||
|
||||
const verdicts = componentVerdictsFromTrace(nodes, trace);
|
||||
if (verdicts.length === 0) return;
|
||||
|
||||
await Promise.all(
|
||||
verdicts.map(v =>
|
||||
fetch(`${base}/analytics/record`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
canonical_id: v.component_id,
|
||||
success: v.success,
|
||||
duration_ms: v.duration_ms,
|
||||
}),
|
||||
}).catch(() => undefined), // 統計失敗不影響執行
|
||||
),
|
||||
);
|
||||
} catch {
|
||||
// fire-and-forget:不拋錯,不影響主流程
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,56 @@
|
||||
/**
|
||||
* Execution Logger — 執行結果寫入 ANALYTICS_KV(fire-and-forget)
|
||||
* Execution Logger — 執行結果寫入 KBDB(fire-and-forget)
|
||||
*
|
||||
* 設計:每次 workflow 執行後,將統計數據寫入 ANALYTICS_KV(key = stats:{workflowId})。
|
||||
* Phase 7 可升級為 POST 至 registry.arcrun.dev/analytics/record。
|
||||
* KV 額度事故修復(總管交辦,2026-08-07):舊版寫 ANALYTICS_KV(Workers KV),
|
||||
* key = stats:{workflowId}:{timestamp}(註解寫「避免覆蓋」)⇒ 只增不減、永不覆蓋。
|
||||
* 封測者 Evan 處理約 690 個檔案,KV 免費層 write 上限 1,000/日被打爆(實測 1,070 write)。
|
||||
*
|
||||
* KBDB 鐵律(leo 2026-06-14):KBDB=API-as-Wall,零 SQL——任何存取一律走 KBDB 的 HTTP API,
|
||||
* 不准直接對它的 D1 下 SQL。本檔因此**不直連任何 D1**,改 fire-and-forget POST
|
||||
* `{KBDB_BASE_URL}/execution-log/record`(連法/認證頭完全比照既有 recordRecipeStats
|
||||
* 慣例,見 webhook-handlers.ts;儲存/降級實作在 kbdb/src/actions/execution-log.ts)。
|
||||
*
|
||||
* leo 兩條判準:
|
||||
* ① 執行紀錄是稽核資料 → 搬去 D1(entries 表,rows written 100,000/日,額度是 KV 的 100 倍)。
|
||||
* ② 不是 n8n、不靠 Execution 計費 → 少記:不留每節點輸入輸出,只留時間/workflow/verdict/
|
||||
* duration/錯誤訊息/(可得的)目標;成功記最少,失敗多記一點(截斷長度不對稱,見 KBDB 端)。
|
||||
*
|
||||
* A2 自我降級(門檻與降級邏輯全在 KBDB 端,見 execution-log.ts):D1 額度仍與知識卡共用,
|
||||
* 超過門檻 KBDB 會回報 mode='skip'/'log_failure_only',但**這件事對呼叫端透明**——
|
||||
* 本函式不管 KBDB 決定寫或不寫,一律 fire-and-forget、永不 throw,workflow 執行不受影響。
|
||||
*/
|
||||
|
||||
import type { Bindings, GraphNode } from '../types';
|
||||
import { kbdbBase } from '../routes/kbdb-proxy';
|
||||
|
||||
export interface ExecutionVerdict {
|
||||
workflow_id: string;
|
||||
component_ids: string[];
|
||||
verdict: 'success' | 'failed';
|
||||
duration_ms: number;
|
||||
message: string;
|
||||
recorded_at: string;
|
||||
target?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 寫入執行結果至 ANALYTICS_KV(fire-and-forget,不阻擋主流程)
|
||||
* 由 c.executionCtx.waitUntil() 包裹呼叫
|
||||
* 從觸發時的 trigger context 擷取這次處理的目標(page_name / path),供「哪些檔沒進去」
|
||||
* 這種問題答得出來。只認這兩個 key(少記,不做窮舉式欄位挖掘/猜測)。
|
||||
*/
|
||||
function extractTarget(input?: Record<string, unknown>): string | undefined {
|
||||
if (!input) return undefined;
|
||||
const raw = input.page_name ?? input.path;
|
||||
if (raw === undefined || raw === null) return undefined;
|
||||
return typeof raw === 'string' ? raw : JSON.stringify(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 寫入執行結果至 KBDB(fire-and-forget,不阻擋主流程)。
|
||||
* 由 c.executionCtx.waitUntil() 包裹呼叫。
|
||||
*
|
||||
* @param nodes 保留參數相容既有呼叫端簽名(原本用來算 component_ids);「不記每節點」
|
||||
* 是本次修復的明確要求(少記),此參數現不使用。
|
||||
* @param input 觸發時的 trigger context(可選)——只用來抓 page_name / path 當 target,
|
||||
* 不整包送出(少記:不留每節點輸入輸出,這裡也不例外)。
|
||||
* @param apiKey 觸發者的租戶(可選,/execute 舊路徑無租戶概念)。
|
||||
*/
|
||||
export async function writeExecutionVerdict(
|
||||
env: Bindings,
|
||||
@@ -27,27 +59,25 @@ export async function writeExecutionVerdict(
|
||||
verdict: 'success' | 'failed',
|
||||
durationMs: number,
|
||||
message: string,
|
||||
input?: Record<string, unknown>,
|
||||
apiKey?: string,
|
||||
): Promise<void> {
|
||||
void nodes; // 少記:不再從節點算 component_ids,保留參數只為呼叫端相容
|
||||
try {
|
||||
const componentIds = nodes
|
||||
.filter(n => n.type === 'Component' && n.componentId)
|
||||
.map(n => n.componentId!);
|
||||
|
||||
const record: ExecutionVerdict = {
|
||||
workflow_id: workflowId,
|
||||
component_ids: componentIds,
|
||||
verdict,
|
||||
duration_ms: durationMs,
|
||||
message,
|
||||
recorded_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// ANALYTICS_KV key = stats:{workflowId}:{timestamp}(避免覆蓋)
|
||||
const key = `stats:${workflowId}:${Date.now()}`;
|
||||
await env.ANALYTICS_KV.put(key, JSON.stringify(record), {
|
||||
expirationTtl: 60 * 60 * 24 * 90, // 保留 90 天
|
||||
const { base, headers } = kbdbBase(env);
|
||||
await fetch(`${base}/execution-log/record`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
workflow_id: workflowId,
|
||||
owner_id: apiKey ?? null,
|
||||
verdict,
|
||||
duration_ms: Math.max(0, Math.round(durationMs)),
|
||||
message: message ?? '',
|
||||
target: extractTarget(input) ?? null,
|
||||
}),
|
||||
});
|
||||
} catch {
|
||||
// fire-and-forget:不拋錯,不影響主流程
|
||||
// fire-and-forget:任何錯誤(含 KBDB 端額度打滿、網路失敗)都吞掉、不影響主流程
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,12 +43,28 @@ export function buildExecutionGraph(
|
||||
iterator = foreachMatch[1];
|
||||
label = '對每個'; // 改回標準 label 走 SEMANTIC_EDGE_MAP
|
||||
}
|
||||
const edge: { from: string; to: string; type: ReturnType<typeof toEdgeType>; iterator?: string } = {
|
||||
|
||||
// 「ON_BRANCH(標籤)」抽 branch:意圖語法表達具名分支(SDD workflow-discovery 3.11)
|
||||
// 例:'my_switch >> ON_BRANCH(branch_active) >> 處理啟用' → type=ON_BRANCH, branch='branch_active'
|
||||
// 沒有這段的話,帶括號的 label 會落到 toEdgeType 的預設值 PIPE ⇒ 分支靜默失效
|
||||
// (即「教了語法但引擎不收」——比沒做更糟,故與 skill 文件同批補上)
|
||||
let branch: string | undefined;
|
||||
const branchMatch = label.match(/^(?:ON_BRANCH|分支)\s*[((]\s*([\w-]+)\s*[))]$/i);
|
||||
if (branchMatch) {
|
||||
branch = branchMatch[1];
|
||||
label = 'ON_BRANCH';
|
||||
}
|
||||
|
||||
const edge: {
|
||||
from: string; to: string; type: ReturnType<typeof toEdgeType>;
|
||||
iterator?: string; branch?: string;
|
||||
} = {
|
||||
from: e.from.toLowerCase().replace(/\s+/g, '-'),
|
||||
to: e.to.toLowerCase().replace(/\s+/g, '-'),
|
||||
type: toEdgeType(label),
|
||||
};
|
||||
if (iterator) edge.iterator = iterator;
|
||||
if (branch) edge.branch = branch;
|
||||
return edge;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,40 +1,721 @@
|
||||
import type { ParsedTriplets, NodeRole } from './triplet-parser';
|
||||
import { resolveNodeRole } from './triplet-parser';
|
||||
import { resolveNodeRole, isVirtualIoName } from './triplet-parser';
|
||||
import { wasmWorkerUrl } from '../lib/component-loader';
|
||||
import { resolveRecipe } from '../routes/recipes';
|
||||
import type { RecipeDefinition } from '../routes/recipes';
|
||||
import { branchHintFor } from '../lib/branch-hints';
|
||||
import type { BranchHint } from '../lib/branch-hints';
|
||||
|
||||
/**
|
||||
* `not_found` 而非 `missing`:欄位契約以頂層機械考
|
||||
* `system-dev/docs/3-specs/arcrun-usable/verify.sh` 為準(01 組 grep `not_found`)。
|
||||
*/
|
||||
/** `unchecked`=compile 模式的誠實標記:沒查、不知道有沒有(≠found 的假信號)。 */
|
||||
/** `resolved`=意圖節點被媒合替換成真實零件/recipe(步驟 4;≠字面 exact 的 found)。 */
|
||||
export type NodeStatus = 'found' | 'not_found' | 'unknown' | 'unchecked' | 'resolved';
|
||||
|
||||
/**
|
||||
* 意圖節點 → 真實零件/recipe 的替換結果(CP 步驟 4,workflow-discovery 3.x 搜尋端延伸)。
|
||||
* 目的(CP 原文):AI 只要填 payload——系統把「傳到 telegram」翻成
|
||||
* `http_request`+recipe `telegram_send`,並明說缺什麼。
|
||||
*/
|
||||
export type NodeSubstitution = {
|
||||
/** 原始意圖節點名(替換前)。 */
|
||||
from: string;
|
||||
/**
|
||||
* 執行底層零件:component 替換=該零件本身;
|
||||
* recipe 替換=`http_request`(recipe 是 http_request+參數模板的具名封裝)。
|
||||
*/
|
||||
componentId: string;
|
||||
/** recipe 替換時的 canonical_id——workflow config 寫 `component: <此值>` 即可直接用。 */
|
||||
recipe?: string;
|
||||
/** 為什麼這樣換(簡單可解釋規則的命中說明,不接 LLM)。 */
|
||||
reason: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 指定搜尋對象(leo 07-31:「難道我不能指定要搜尋工作流或節點或 recipe 嗎?」)。
|
||||
* 不給=現行混搜兩庫+意圖替換;`component`=只查零件 registry;`recipe`=只查 recipe 庫。
|
||||
* `workflow` 不進本函式——workflow 搜尋是名字搜尋(route 層走既有 /workflows/search 機制)。
|
||||
*/
|
||||
export type SearchTarget = 'component' | 'recipe';
|
||||
|
||||
export type NodeInfo = {
|
||||
status: NodeStatus;
|
||||
componentId?: string;
|
||||
type: NodeRole;
|
||||
/** found 時標來源庫:零件 registry(component)或 recipe 庫(recipe)。 */
|
||||
source?: 'component' | 'recipe';
|
||||
/** 零件契約(found 時附上,讓 AI 知道怎麼填 payload)。 */
|
||||
input_schema?: unknown;
|
||||
/** 成功率(found 時附上,讓「被測過幾次」看得見)。 */
|
||||
success_rate?: number;
|
||||
stability?: string;
|
||||
/** recipe found 時附上(AI 看得懂這個 recipe 在打哪個 API)。 */
|
||||
description?: string;
|
||||
endpoint?: string;
|
||||
/**
|
||||
* recipe 的 payload/回應用法自我說明(3.12,同 branch_hint 的動機):
|
||||
* 逐顆查 recipe 時光看 endpoint 不知道「payload 怎麼填、回應怎麼取值」⇒ 會退回寫 code。
|
||||
*/
|
||||
payload_hint?: {
|
||||
/** 這個 recipe 期望的 body 形狀(body_template 的欄位骨架,值是 {{var}} 佔位) */
|
||||
body_template?: unknown;
|
||||
/** 回應正規化規則存在時,說明取值路徑等 */
|
||||
response_map?: unknown;
|
||||
/** 一行說明:怎麼用這個 recipe */
|
||||
usage: string;
|
||||
};
|
||||
/**
|
||||
* not_found 時的分型指路(task 3.7):兩庫(零件 registry+recipe 庫)都查過才點名,
|
||||
* 並告訴 AI 該走哪條補件路+去哪裡看做法。欄位名 `suggestion`(單數字串)=verify.sh 03 組契約。
|
||||
*/
|
||||
suggestion?: string;
|
||||
/** not_found 時的相近零件候選(自然語言節點名 → 既有零件的媒合)。 */
|
||||
similar_components?: string[];
|
||||
/** not_found 時的相近 recipe 候選。 */
|
||||
similar_recipes?: string[];
|
||||
/** resolved 時的替換明細(步驟 4:意圖節點 → 真實零件/recipe)。 */
|
||||
substitution?: NodeSubstitution;
|
||||
/**
|
||||
* 分支用法自我說明(3.11):只有「本身會分岔」的零件才有
|
||||
* (if_control/switch/try_catch)。
|
||||
* 存在的理由=走 n8n 式「逐顆查、自己組圖」的 AI,光看 input_schema 不知道
|
||||
* 「判斷完之後兩條路怎麼接」⇒ 會回頭寫 code。判準:只看這一顆的回應就知道怎麼接下一步。
|
||||
*/
|
||||
branch_hint?: BranchHint;
|
||||
};
|
||||
|
||||
export type SearchResult = {
|
||||
nodeResults: Record<string, { status: 'found' | 'missing'; componentId?: string; type: NodeRole }>;
|
||||
nodeResults: Record<string, NodeInfo>;
|
||||
missingNodes: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 對所有節點進行解析,確認每個節點對應的零件 ID。
|
||||
*
|
||||
* 注意:此步驟只做靜態解析,不做遠端查找。
|
||||
* 零件是否真的存在由 component-loader 在執行時決定(Service Binding / KV / URL)。
|
||||
*
|
||||
* 優先序:
|
||||
* 1. Input/Output 角色:自動標記,componentId = 小寫節點名稱
|
||||
* 2. config[nodeName].component 已指定:使用 config 提供的 componentId
|
||||
* 3. 其他:componentId = 節點名稱(交給 component-loader 在執行時解析)
|
||||
* t158(leo 07-31 定調「部署≠發現」):
|
||||
* 「這裡只是複製一些工作流的 data 過去,沒有要在這裡驗證,難怪這麼慢。
|
||||
* 就算是我自己寫了錯的工作流,也可以跑跑看,如果錯誤就修改,
|
||||
* 沒有說有錯誤還要一個個驗證這回事。」
|
||||
* - `compile`=純編圖:**零外部查詢**(不打 registry、不掃 recipe、不算相似度、不擋 missing)。
|
||||
* 部署/推送/執行路徑用——寫錯的 workflow 照樣部署,錯在執行時現形。
|
||||
* - `discover`=誠實查詢(預設,`/cypher/search` 的既有契約):AI 問「有沒有」時用,
|
||||
* not_found+分型指路+相似候選全保留。
|
||||
*/
|
||||
export function searchNodes(
|
||||
export type SearchMode = 'discover' | 'compile';
|
||||
|
||||
/** searchNodes 需要的環境子集(cypher-handlers 傳整份 Bindings 進來也相容)。 */
|
||||
export type SearchNodesEnv = {
|
||||
WORKER_SUBDOMAIN?: string;
|
||||
/**
|
||||
* registry 位置覆蓋(可選,非機密)。未設 → 用 wasmWorkerUrl('registry', WORKER_SUBDOMAIN)
|
||||
* 現算(比照 KBDB_GRAPH_URL 慣例)。本地 wrangler dev / self-hosted 把 registry 掛別處時用。
|
||||
*/
|
||||
REGISTRY_BASE_URL?: string;
|
||||
/** recipe 庫(本 worker 自己的 KV;task 3.6 兩庫都查的第二庫)。 */
|
||||
RECIPES?: KVNamespace;
|
||||
};
|
||||
|
||||
/**
|
||||
* 對所有節點進行解析,確認每個節點對應的零件/recipe 是否**真的存在**。
|
||||
*
|
||||
* ⚠️ 2026-07-30 改為會查 registry(workflow-discovery task 3.x);
|
||||
* 2026-07-31 再加 recipe 庫查詢+缺件分型指路(task 3.6/3.7)。
|
||||
*
|
||||
* 改之前的行為(病灶):無條件回 `status: 'found'`、`missingNodes` 永遠是空陣列——
|
||||
* 實測「完全不存在的東西xyz」也回 found。
|
||||
*
|
||||
* 為什麼這是嚴重問題(leo 2026-07-30 定性「腹語術」):
|
||||
* AI 寫意圖 → 查詢回「都 found」(假信號)→ 實際零件不存在
|
||||
* → 部署/執行才發現 → 最快的修法是改寫成 `code` 節點自己寫 JS
|
||||
* → 於是正式 workflow 只用 2 個零件、8 個 code 節點含 if×61
|
||||
* ⇒ 「零件被測過 1000 次所以 AI 只要填 payload」的價值完全落空。
|
||||
*
|
||||
* 設計基調(leo 2026-07-31 二次定調):**回覆的重點是「缺哪些」不是「有哪些」**——
|
||||
* 有的照常編圖不必報告;缺的要兩庫(零件 registry+recipe 庫)都搜過後點名+給正確指示:
|
||||
* 缺外部 API → 自己寫 recipe(skill `write_recipe`);
|
||||
* 缺計算原語 → 投稿零件 PR(skill `add_new_wasm_component`)。
|
||||
*
|
||||
* 誠實限制:查不到 registry(未部署/網路失敗)時回 `'unknown'` 而不是 `'not_found'`——
|
||||
* 不能因為查詢失敗就宣告零件不存在(那會讓 AI 誤判而重寫 code,正是要避免的事)。
|
||||
*/
|
||||
export async function searchNodes(
|
||||
parsed: ParsedTriplets,
|
||||
config?: Record<string, Record<string, unknown>>,
|
||||
): SearchResult {
|
||||
const nodeResults: Record<string, { status: 'found' | 'missing'; componentId?: string; type: NodeRole }> = {};
|
||||
env?: SearchNodesEnv,
|
||||
mode: SearchMode = 'discover',
|
||||
target?: SearchTarget,
|
||||
): Promise<SearchResult> {
|
||||
const nodeResults: Record<string, NodeInfo> = {};
|
||||
const missingNodes: string[] = [];
|
||||
|
||||
// ── compile:純編圖,零外部查詢(t158,部署≠發現)─────────────────────────
|
||||
if (mode === 'compile') {
|
||||
for (const nodeName of parsed.nodeNames) {
|
||||
const role = resolveNodeRole(nodeName, parsed);
|
||||
if ((role === 'Input' || role === 'Output') && isVirtualIoName(nodeName)) {
|
||||
nodeResults[nodeName] = { status: 'found', componentId: nodeName.toLowerCase(), type: role };
|
||||
continue;
|
||||
}
|
||||
const configComponent = config?.[nodeName]?.component as string | undefined;
|
||||
// unchecked=誠實「沒查」;存在性由 component-loader 在執行時決定
|
||||
nodeResults[nodeName] = {
|
||||
status: configComponent ? 'found' : 'unchecked',
|
||||
componentId: configComponent ?? nodeName,
|
||||
type: role,
|
||||
};
|
||||
}
|
||||
return { nodeResults, missingNodes };
|
||||
}
|
||||
|
||||
const sub = env?.WORKER_SUBDOMAIN;
|
||||
const registryBase = env?.REGISTRY_BASE_URL ?? (sub ? wasmWorkerUrl('registry', sub) : undefined);
|
||||
|
||||
// target 限庫(leo 07-31):component=只查零件 registry;recipe=只查 recipe 庫。
|
||||
// 不給=混搜兩庫(既有行為)。
|
||||
const wantComponents = target !== 'recipe';
|
||||
const wantRecipes = target !== 'component';
|
||||
|
||||
// ── discover 批次化(t158):兩庫各抓**一次**,之後全在記憶體內比對。────────
|
||||
// 病史(07-31 stage 實測):舊版對每個 missing 節點各打「1 次逐顆查+最多 9 次
|
||||
// 相似搜尋+一輪 recipe KV 掃描」⇒ 冷實例 8 節點 /cypher/search 25.7s,
|
||||
// 安裝器 15s timeout 必炸。批次化後每 request 固定 1 次 catalog+1 次 recipe 清單。
|
||||
// 步驟 4 的意圖替換也在**同一份清單**上做——不加任何新 round-trip。
|
||||
const catalog = !wantComponents
|
||||
? { status: 'ok' as const, entries: [] } // target=recipe:registry 不參與,不因此回 unknown
|
||||
: registryBase ? await fetchCatalog(registryBase) : { status: 'unreachable' as const, entries: [] };
|
||||
const recipes = wantRecipes && env?.RECIPES ? await listAllRecipes(env.RECIPES) : [];
|
||||
const byId = new Map<string, CatalogFullRecord>();
|
||||
for (const e of catalog.entries) {
|
||||
const prev = byId.get(e.canonical_id);
|
||||
if (!prev || (e.score ?? 0) > (prev.score ?? 0)) byId.set(e.canonical_id, e);
|
||||
for (const a of e.aliases ?? []) if (!byId.has(a)) byId.set(a, e);
|
||||
}
|
||||
|
||||
for (const nodeName of parsed.nodeNames) {
|
||||
const role = resolveNodeRole(nodeName, parsed);
|
||||
|
||||
if (role === 'Input' || role === 'Output') {
|
||||
// 只有**字面上的虛擬 IO 名**(input/trigger/…/output/done)才免查——
|
||||
// 位置上是頭節點但名字是真零件(`aes_encrypt >> … >> code` 的頭,role 也是 Input)
|
||||
// 仍要照常查兩庫,否則缺件被角色掩蓋、又回到「假 found」。
|
||||
if ((role === 'Input' || role === 'Output') && isVirtualIoName(nodeName)) {
|
||||
nodeResults[nodeName] = { status: 'found', componentId: nodeName.toLowerCase(), type: role };
|
||||
continue;
|
||||
}
|
||||
|
||||
const configComponent = config?.[nodeName]?.component as string | undefined;
|
||||
const componentId = configComponent ?? nodeName;
|
||||
nodeResults[nodeName] = { status: 'found', componentId, type: role };
|
||||
|
||||
// config 明確給了 component(多半是安裝器代入的 worker URL 或既有 workflow)
|
||||
// → 不判 not_found。這條路徑的存在性由 component-loader 在執行時決定(原行為)。
|
||||
if (configComponent) {
|
||||
nodeResults[nodeName] = { status: 'found', componentId, type: role };
|
||||
continue;
|
||||
}
|
||||
|
||||
// registry 完全查不通(未部署/網路失敗)⇒ 誠實回 unknown。
|
||||
// **不能誤判 not_found**——那會讓 AI 以為零件不存在而重寫 code,正是要避免的事。
|
||||
// 舊 registry 沒有 /catalog 端點(no_endpoint)→ 退回逐顆查(相容路徑)。
|
||||
if (catalog.status === 'unreachable') {
|
||||
nodeResults[nodeName] = { status: 'unknown', componentId, type: role };
|
||||
continue;
|
||||
}
|
||||
if (catalog.status === 'no_endpoint') {
|
||||
const legacy = await legacyPerNodeLookup(registryBase!, componentId, nodeName, role, env, recipes);
|
||||
nodeResults[nodeName] = legacy.info;
|
||||
if (legacy.missing) missingNodes.push(nodeName);
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── 第一庫:零件 catalog(記憶體)────────────────────────────────────────
|
||||
const hit = byId.get(componentId);
|
||||
if (hit) {
|
||||
nodeResults[nodeName] = {
|
||||
status: 'found',
|
||||
componentId,
|
||||
type: role,
|
||||
source: 'component',
|
||||
input_schema: hit.input_schema,
|
||||
success_rate: typeof hit.success_rate === 'number' ? hit.success_rate : undefined,
|
||||
stability: typeof hit.stability === 'string' ? hit.stability : undefined,
|
||||
branch_hint: branchHintFor(componentId),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── 第二庫:recipe 清單(記憶體;canonical_id 精確比對)──────────────────
|
||||
const recipe = recipes.find(r => r.canonical_id === componentId);
|
||||
if (recipe) {
|
||||
nodeResults[nodeName] = {
|
||||
status: 'found',
|
||||
componentId: recipe.canonical_id,
|
||||
type: role,
|
||||
source: 'recipe',
|
||||
description: recipe.description,
|
||||
endpoint: recipe.endpoint,
|
||||
payload_hint: buildPayloadHint(recipe),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── 步驟 4:意圖節點 → 真實零件/recipe 替換(同一份清單、全記憶體)────────
|
||||
// 字面 exact 兩庫都落空的自然語言節點(例「傳到 telegram」「判斷有沒有新資料」),
|
||||
// 先試保守的替換規則;換得到=resolved(回應直接可組 workflow),換不到才 not_found。
|
||||
const substituted = trySubstitution(nodeName, catalog.entries, recipes);
|
||||
if (substituted) {
|
||||
nodeResults[nodeName] = { ...substituted, type: role };
|
||||
continue;
|
||||
}
|
||||
|
||||
// ── 兩庫都沒有 ⇒ not_found + 分型指路(task 3.7)+ 相近候選(全記憶體)──
|
||||
const similarComponents = similarFromCatalog(catalog.entries, nodeName);
|
||||
const similarRecipes = similarFromRecipes(recipes, nodeName);
|
||||
|
||||
nodeResults[nodeName] = {
|
||||
status: 'not_found',
|
||||
componentId,
|
||||
type: role,
|
||||
suggestion: buildSuggestion(componentId),
|
||||
...(similarComponents.length > 0 ? { similar_components: similarComponents } : {}),
|
||||
...(similarRecipes.length > 0 ? { similar_recipes: similarRecipes } : {}),
|
||||
};
|
||||
missingNodes.push(nodeName);
|
||||
}
|
||||
|
||||
return { nodeResults, missingNodes: [] };
|
||||
return { nodeResults, missingNodes };
|
||||
}
|
||||
|
||||
// ── t158 批次化 helpers ────────────────────────────────────────────────────────
|
||||
|
||||
type CatalogFullRecord = {
|
||||
canonical_id: string;
|
||||
display_name?: string;
|
||||
description?: string;
|
||||
aliases?: string[];
|
||||
tags?: string[];
|
||||
score?: number;
|
||||
input_schema?: unknown;
|
||||
success_rate?: number;
|
||||
stability?: string;
|
||||
};
|
||||
|
||||
type CatalogFetch = { status: 'ok' | 'no_endpoint' | 'unreachable'; entries: CatalogFullRecord[] };
|
||||
|
||||
/** 一次抓 registry 全目錄。404=舊版 registry 沒這端點 → 呼叫端退回逐顆查。 */
|
||||
async function fetchCatalog(registryBase: string): Promise<CatalogFetch> {
|
||||
try {
|
||||
const res = await fetch(`${registryBase}/components/catalog`, { signal: AbortSignal.timeout(10000) });
|
||||
if (res.status === 404) return { status: 'no_endpoint', entries: [] };
|
||||
if (!res.ok) return { status: 'unreachable', entries: [] };
|
||||
const body = (await res.json()) as { data?: { components?: CatalogFullRecord[] } };
|
||||
return { status: 'ok', entries: body.data?.components ?? [] };
|
||||
} catch {
|
||||
return { status: 'unreachable', entries: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/** 一次抓 recipe 全清單(本部署 recipe 數量小;exact 與相似度共用同一份)。
|
||||
* export 給 target=recipe 的名字搜尋(actions/target-search.ts)共用同一份讀法。 */
|
||||
export async function listAllRecipes(kv: KVNamespace): Promise<RecipeDefinition[]> {
|
||||
try {
|
||||
const list = await kv.list({ prefix: 'recipe:' });
|
||||
return (await Promise.all(
|
||||
list.keys.map(k => kv.get(k.name, 'json') as Promise<RecipeDefinition | null>),
|
||||
)).filter(Boolean) as RecipeDefinition[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 相似零件(記憶體版):全名 substring 優先,否則斷詞計數 top3——判準與舊 HTTP 版一致。 */
|
||||
function similarFromCatalog(entries: CatalogFullRecord[], nodeName: string): string[] {
|
||||
const searchableOf = (e: CatalogFullRecord) =>
|
||||
[e.canonical_id, e.display_name ?? '', e.description ?? '', ...(e.aliases ?? []), ...(e.tags ?? [])]
|
||||
.join(' ').toLowerCase();
|
||||
const full = nodeName.toLowerCase();
|
||||
const direct = entries.filter(e => searchableOf(e).includes(full)).map(e => e.canonical_id);
|
||||
if (direct.length > 0) return [...new Set(direct)].slice(0, 3);
|
||||
|
||||
const tokens = extractTokens(nodeName);
|
||||
if (tokens.length === 0) return [];
|
||||
const count = new Map<string, number>();
|
||||
for (const e of entries) {
|
||||
const hay = searchableOf(e);
|
||||
const hits = tokens.filter(t => hay.includes(t)).length;
|
||||
if (hits > 0) count.set(e.canonical_id, Math.max(count.get(e.canonical_id) ?? 0, hits));
|
||||
}
|
||||
return [...count.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([id]) => id);
|
||||
}
|
||||
|
||||
/** 相似 recipe(記憶體版;判準沿用 searchSimilarRecipes)。 */
|
||||
function similarFromRecipes(recipes: RecipeDefinition[], nodeName: string): string[] {
|
||||
const tokens = [nodeName.toLowerCase(), ...extractTokens(nodeName)];
|
||||
const seen = new Set<string>();
|
||||
const matched: string[] = [];
|
||||
for (const r of recipes) {
|
||||
if (seen.has(r.canonical_id)) continue;
|
||||
const hay = `${r.canonical_id} ${r.display_name ?? ''} ${r.description ?? ''}`.toLowerCase();
|
||||
if (tokens.some(t => hay.includes(t))) {
|
||||
seen.add(r.canonical_id);
|
||||
matched.push(r.canonical_id);
|
||||
}
|
||||
}
|
||||
return matched.slice(0, 3);
|
||||
}
|
||||
|
||||
/** 舊 registry(無 /catalog 端點)的相容路徑:維持逐顆查語義。 */
|
||||
async function legacyPerNodeLookup(
|
||||
registryBase: string,
|
||||
componentId: string,
|
||||
nodeName: string,
|
||||
role: NodeRole,
|
||||
env: SearchNodesEnv | undefined,
|
||||
recipes: RecipeDefinition[],
|
||||
): Promise<{ info: NodeInfo; missing: boolean }> {
|
||||
const q = await fetchComponent(registryBase, componentId);
|
||||
if (!q.ok) return { info: { status: 'unknown', componentId, type: role }, missing: false };
|
||||
if (q.entry) {
|
||||
return {
|
||||
info: {
|
||||
status: 'found', componentId, type: role, source: 'component',
|
||||
input_schema: q.entry.input_schema, success_rate: q.entry.success_rate, stability: q.entry.stability,
|
||||
branch_hint: branchHintFor(componentId),
|
||||
},
|
||||
missing: false,
|
||||
};
|
||||
}
|
||||
const recipe = recipes.find(r => r.canonical_id === componentId)
|
||||
?? (env?.RECIPES ? await resolveRecipe(componentId, env.RECIPES) : null);
|
||||
if (recipe) {
|
||||
return {
|
||||
info: {
|
||||
status: 'found', componentId: recipe.canonical_id, type: role, source: 'recipe',
|
||||
description: recipe.description, endpoint: recipe.endpoint,
|
||||
payload_hint: buildPayloadHint(recipe),
|
||||
},
|
||||
missing: false,
|
||||
};
|
||||
}
|
||||
const similarComponents = await searchSimilarComponents(registryBase, nodeName);
|
||||
const similarRecipes = similarFromRecipes(recipes, nodeName);
|
||||
return {
|
||||
info: {
|
||||
status: 'not_found', componentId, type: role, suggestion: buildSuggestion(componentId),
|
||||
...(similarComponents.length > 0 ? { similar_components: similarComponents } : {}),
|
||||
...(similarRecipes.length > 0 ? { similar_recipes: similarRecipes } : {}),
|
||||
},
|
||||
missing: true,
|
||||
};
|
||||
}
|
||||
|
||||
// ── 步驟 4:意圖節點 → 真實零件/recipe 替換 ────────────────────────────────────
|
||||
//
|
||||
// 目的(CP arcrun-usable 步驟 4):AI 只要填 payload——系統把「傳到 telegram」翻成
|
||||
// `http_request`+recipe `telegram_send`。媒合在「一次抓好的兩庫清單」記憶體內做,
|
||||
// 零新增 round-trip;規則沿用 task 3.7 的服務詞判型+既有斷詞媒合(extractTokens),
|
||||
// 刻意簡單可解釋、不接 LLM。
|
||||
//
|
||||
// 兩條規則(保守——換錯比不換更糟,寧可 not_found+候選讓 AI 自己選):
|
||||
// A) 服務詞規則(recipe 路):節點名含 SERVICE_HINTS 服務詞 → 名字裡**全部**服務詞
|
||||
// 都命中同一個 recipe、且該 recipe **唯一**才替換。
|
||||
// 例「傳到 telegram」:服務詞 [telegram] → 唯一命中 telegram_send ⇒ 換。
|
||||
// 反例「google_slides_create」:服務詞 [google, slides] → google_sheets_* 只中
|
||||
// google 不中 slides ⇒ 不換(照 3.7 指去寫 recipe)。
|
||||
// 有服務詞的節點**不落入規則 B**——外部服務就該是 recipe,不硬配零件
|
||||
// (否則「google_slides」會被 display_name 含 Google 的零件誤吃)。
|
||||
// B) 強欄位規則(零件路):斷詞後只算**強欄位**(canonical_id/display_name/aliases)
|
||||
// 命中為主:分數=強命中×10+弱命中(description/tags)×1,
|
||||
// 需「至少一個強命中」且「分數唯一最高」才替換。
|
||||
// 例「判斷有沒有新資料」:2-gram「判斷」命中 if_control display_name「條件判斷」
|
||||
// (強 10 分),try_catch 只在 description 中「判斷」(弱 1 分)⇒ 唯一最高 ⇒ 換。
|
||||
// 反例「aes_encrypt」:無任何強命中 ⇒ 不換(照 3.7 指去投零件 PR)。
|
||||
|
||||
type SubstitutionHit = Pick<
|
||||
NodeInfo,
|
||||
'status' | 'componentId' | 'source' | 'substitution' |
|
||||
'input_schema' | 'success_rate' | 'stability' | 'description' | 'endpoint' | 'branch_hint'
|
||||
>;
|
||||
|
||||
function trySubstitution(
|
||||
nodeName: string,
|
||||
catalogEntries: CatalogFullRecord[],
|
||||
recipes: RecipeDefinition[],
|
||||
): SubstitutionHit | null {
|
||||
const lower = nodeName.toLowerCase();
|
||||
const serviceHits = SERVICE_HINTS.filter(w => lower.includes(w));
|
||||
|
||||
// 規則 A:服務詞 → recipe(全部服務詞命中+唯一)
|
||||
if (serviceHits.length > 0) {
|
||||
const matched = new Map<string, RecipeDefinition>();
|
||||
for (const r of recipes) {
|
||||
const hay = `${r.canonical_id} ${r.display_name ?? ''} ${r.description ?? ''}`.toLowerCase();
|
||||
if (serviceHits.every(h => hay.includes(h))) matched.set(r.canonical_id, r);
|
||||
}
|
||||
if (matched.size !== 1) return null; // 0=真缺件走 not_found;≥2=歧義,候選留給 similar_recipes
|
||||
const recipe = [...matched.values()][0];
|
||||
return {
|
||||
status: 'resolved',
|
||||
componentId: recipe.canonical_id,
|
||||
source: 'recipe',
|
||||
description: recipe.description,
|
||||
endpoint: recipe.endpoint,
|
||||
substitution: {
|
||||
from: nodeName,
|
||||
componentId: 'http_request', // recipe=http_request+參數模板的具名封裝
|
||||
recipe: recipe.canonical_id,
|
||||
reason:
|
||||
`服務詞「${serviceHits.join('、')}」唯一命中 recipe「${recipe.canonical_id}」;` +
|
||||
`workflow config 寫 component: ${recipe.canonical_id}(底層零件=http_request),只需填 payload`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// 規則 B:強欄位斷詞媒合 → 零件(至少一強命中+分數唯一最高)
|
||||
const tokens = extractTokens(nodeName);
|
||||
if (tokens.length === 0) return null;
|
||||
|
||||
type Scored = { entry: CatalogFullRecord; score: number; strongHits: string[] };
|
||||
const byCanonical = new Map<string, Scored>();
|
||||
for (const e of catalogEntries) {
|
||||
const strongHay = [e.canonical_id, e.display_name ?? '', ...(e.aliases ?? [])].join(' ').toLowerCase();
|
||||
const weakHay = [e.description ?? '', ...(e.tags ?? [])].join(' ').toLowerCase();
|
||||
const strongHits = tokens.filter(t => strongHay.includes(t));
|
||||
const weakCount = tokens.filter(t => weakHay.includes(t)).length;
|
||||
const score = strongHits.length * 10 + weakCount;
|
||||
if (score === 0) continue;
|
||||
const prev = byCanonical.get(e.canonical_id);
|
||||
if (!prev || score > prev.score) byCanonical.set(e.canonical_id, { entry: e, score, strongHits });
|
||||
}
|
||||
const ranked = [...byCanonical.values()].sort((a, b) => b.score - a.score);
|
||||
const top = ranked[0];
|
||||
if (!top || top.strongHits.length === 0) return null; // 沒有強命中=證據不足
|
||||
if (ranked[1] && ranked[1].score >= top.score) return null; // 同分歧義=不硬猜
|
||||
|
||||
return {
|
||||
status: 'resolved',
|
||||
componentId: top.entry.canonical_id,
|
||||
source: 'component',
|
||||
input_schema: top.entry.input_schema,
|
||||
success_rate: typeof top.entry.success_rate === 'number' ? top.entry.success_rate : undefined,
|
||||
stability: typeof top.entry.stability === 'string' ? top.entry.stability : undefined,
|
||||
// 替換成分岔零件時(例「判斷有沒有新資料」→ if_control)一併附分支用法,
|
||||
// 否則 AI 換到零件卻不知道怎麼接兩條路,仍會退回寫 code。
|
||||
branch_hint: branchHintFor(top.entry.canonical_id),
|
||||
substitution: {
|
||||
from: nodeName,
|
||||
componentId: top.entry.canonical_id,
|
||||
reason:
|
||||
`斷詞「${top.strongHits.join('、')}」命中零件「${top.entry.canonical_id}」` +
|
||||
`(${top.entry.display_name ?? ''})強欄位且分數唯一最高;只需照 input_schema 填 payload`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ── 缺件分型(task 3.7)────────────────────────────────────────────────────────
|
||||
//
|
||||
// 分型判準(刻意用簡單可解釋的規則,不接 LLM——查詢端點要快、要可預測):
|
||||
// 1) 名字含**外部服務詞**(google/telegram/slack…)→「外部 API 樣貌」
|
||||
// → recipe 路:recipe 是 http_request+參數模板的具名封裝,用戶自己就能寫,不用改平台。
|
||||
// 2) 否則名字含**計算原語詞**(encrypt/hash/encode…)→「計算原語樣貌」
|
||||
// → 零件路:純計算得進 WASM 沙箱跑,要走 GitHub PR 投稿(人 merge=人類閘門,mindset §4)。
|
||||
// 3) 都不含 → 判不出型,誠實說判不出,兩條路都給(不硬猜——猜錯會把人指去錯的路)。
|
||||
// 判斷順序:服務詞優先於計算詞——「google_sheets_parse」雖含 parse,本質仍是打外部 API。
|
||||
|
||||
const SERVICE_HINTS = [
|
||||
'google', 'gmail', 'sheets', 'slides', 'gdocs', 'drive', 'calendar', 'youtube',
|
||||
'slack', 'telegram', 'discord', 'line', 'whatsapp', 'twilio',
|
||||
'notion', 'airtable', 'trello', 'jira', 'asana', 'linear',
|
||||
'github', 'gitea', 'gitlab', 'bitbucket',
|
||||
'stripe', 'paypal', 'shopify', 'hubspot', 'salesforce',
|
||||
'openai', 'anthropic', 'claude', 'gemini', 'groq',
|
||||
'twitter', 'facebook', 'instagram', 'linkedin', 'dropbox', 'zoom',
|
||||
'sendgrid', 'mailgun', 'kbdb',
|
||||
];
|
||||
|
||||
const COMPUTE_HINTS = [
|
||||
'encrypt', 'decrypt', 'cipher', 'aes', 'rsa', 'sha', 'md5', 'hmac', 'hash',
|
||||
'sign', 'verify', 'encode', 'decode', 'base64', 'hex',
|
||||
'compress', 'decompress', 'zip', 'gzip',
|
||||
'uuid', 'random', 'regex', 'math', 'calc',
|
||||
'sort', 'dedup', 'diff', 'template', 'render', 'convert', 'transform',
|
||||
'parse', 'format', 'csv', 'xml',
|
||||
];
|
||||
|
||||
function buildSuggestion(componentId: string): string {
|
||||
const lower = componentId.toLowerCase();
|
||||
const serviceHit = SERVICE_HINTS.find(w => lower.includes(w));
|
||||
const computeHit = COMPUTE_HINTS.find(w => lower.includes(w));
|
||||
|
||||
if (serviceHit) {
|
||||
return (
|
||||
`兩庫都查過,零件 registry 與 recipe 庫皆無「${componentId}」。` +
|
||||
`名字含服務詞「${serviceHit}」=外部 API 樣貌 → 沒有此 recipe,可自己寫:` +
|
||||
`寫法看 skill「write_recipe」(arcrun_get_skill('write_recipe')),` +
|
||||
`寫好用 acr recipe push 或 POST /recipes 裝上即可用,不用改平台。`
|
||||
);
|
||||
}
|
||||
if (computeHit) {
|
||||
return (
|
||||
`兩庫都查過,零件 registry 與 recipe 庫皆無「${componentId}」。` +
|
||||
`名字含計算詞「${computeHit}」=計算原語樣貌 → 沒有此零件,可投稿 PR 新增 WASM component:` +
|
||||
`做法看 skill「add_new_wasm_component」(arcrun_get_skill('add_new_wasm_component'))。`
|
||||
);
|
||||
}
|
||||
return (
|
||||
`兩庫都查過,零件 registry 與 recipe 庫皆無「${componentId}」,且名字判不出型。` +
|
||||
`缺外部 API → 自己寫 recipe(skill「write_recipe」);` +
|
||||
`缺計算能力 → 投稿零件 PR(skill「add_new_wasm_component」,component 進 WASM 沙箱)。`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* recipe 的 payload/回應用法自我說明(3.12)。
|
||||
* 動機同 branch_hint:逐顆查 recipe(n8n 式)時,光看 endpoint 不知道 payload 怎麼填、
|
||||
* 回應怎麼取值 ⇒ AI 會退回把整包寫進 workflow code。
|
||||
*/
|
||||
export function buildPayloadHint(recipe: RecipeDefinition): NodeInfo['payload_hint'] {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (recipe.body_template) {
|
||||
parts.push('payload 已收在 recipe 的 body_template 裡,你只要把 {{變數}} 對應的值放進節點 context');
|
||||
} else if (recipe.body) {
|
||||
parts.push('payload 形狀見 body 欄位({{變數}} 由節點 context 填)');
|
||||
} else {
|
||||
parts.push('未定義 body_template:節點 context 會整包當 body 送出(_ 開頭的內部欄位會被剔除)');
|
||||
}
|
||||
|
||||
if (recipe.response_map) {
|
||||
parts.push('回應已正規化:執行結果除了原始 data,另附 text(取值路徑等規則寫在 recipe 裡,換源不必改 workflow)');
|
||||
} else {
|
||||
parts.push('未定義 response_map:回應原樣放在 data,取值要自己指路徑');
|
||||
}
|
||||
|
||||
if (recipe.auth === 'binding') {
|
||||
parts.push(`認證=binding(免金鑰,用平台內建 ${recipe.binding_name ?? 'AI'})`);
|
||||
} else if (recipe.auth_service) {
|
||||
parts.push(`認證走 auth recipe「${recipe.auth_service}」(金鑰由系統在執行前注入,你不必也不該填)`);
|
||||
}
|
||||
|
||||
return {
|
||||
body_template: recipe.body_template,
|
||||
response_map: recipe.response_map,
|
||||
usage: parts.join(';') + '。',
|
||||
};
|
||||
}
|
||||
|
||||
// ── registry 查詢 ─────────────────────────────────────────────────────────────
|
||||
|
||||
type CatalogEntry = {
|
||||
input_schema?: unknown;
|
||||
success_rate?: number;
|
||||
stability?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* 查單一零件是否存在於 registry。
|
||||
*
|
||||
* ⚠️ 為什麼逐個查而非抓整份目錄:registry **沒有列表端點**
|
||||
* (實測 `GET /components` → 404,只有 `GET /components/<id>`)。
|
||||
* 這是 CP2-B 記載的缺口(「修 /components 404」)——補了列表端點後可改為抓一次。
|
||||
* 現階段逐個查:節點數通常 <10,且有 5s timeout,可接受。
|
||||
*
|
||||
* 回傳 `ok:false` 代表「查不到 registry」,由呼叫端區分:
|
||||
* 整體查不通 → `unknown`;查得通但這顆沒有 → 繼續查 recipe 庫。
|
||||
*/
|
||||
async function fetchComponent(
|
||||
registryBase: string,
|
||||
id: string,
|
||||
): Promise<{ ok: boolean; entry?: CatalogEntry }> {
|
||||
try {
|
||||
const res = await fetch(`${registryBase}/components/${encodeURIComponent(id)}`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (res.status === 404) return { ok: true }; // registry 活著,但沒這顆
|
||||
if (!res.ok) return { ok: false };
|
||||
const body = (await res.json()) as { success?: boolean; data?: Record<string, unknown> };
|
||||
if (body.success === false) return { ok: true }; // 同上:回「零件不存在」
|
||||
const d = body.data ?? (body as unknown as Record<string, unknown>);
|
||||
return {
|
||||
ok: true,
|
||||
entry: {
|
||||
input_schema: d.input_schema,
|
||||
success_rate: typeof d.success_rate === 'number' ? d.success_rate : undefined,
|
||||
stability: typeof d.stability === 'string' ? d.stability : undefined,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return { ok: false };
|
||||
}
|
||||
}
|
||||
|
||||
// ── 相近候選(自然語言節點名 → 既有零件/recipe 的媒合)──────────────────────────
|
||||
//
|
||||
// 節點名常是自然語言(例「判斷有沒有新資料」)。leo:「AI 不用知道零件存在」——
|
||||
// 所以 not_found 時要主動給相近候選,讓 AI 看回覆就知道「其實有 if_control 可用」。
|
||||
// 做法:先拿全名打 registry `/components/search`;沒中再斷詞重試——
|
||||
// ASCII 取 3 字以上的詞、中日韓取 2-gram(registry search 是子字串比對,整句中文必落空,
|
||||
// 2-gram 才撈得到「判斷」→ if_control(display_name「條件判斷」)這種命中)。
|
||||
|
||||
function extractTokens(name: string): string[] {
|
||||
const tokens: string[] = [];
|
||||
const ascii = name.toLowerCase().match(/[a-z0-9]{3,}/g) ?? [];
|
||||
tokens.push(...ascii);
|
||||
const cjkRuns = name.match(/[一-鿿]+/g) ?? [];
|
||||
for (const run of cjkRuns) {
|
||||
for (let i = 0; i + 2 <= run.length; i++) tokens.push(run.slice(i, i + 2));
|
||||
}
|
||||
return [...new Set(tokens)].slice(0, 8); // 上限 8 個 token,避免對 registry 掃太多輪
|
||||
}
|
||||
|
||||
async function searchRegistryIds(registryBase: string, q: string): Promise<string[]> {
|
||||
try {
|
||||
const res = await fetch(`${registryBase}/components/search?q=${encodeURIComponent(q)}`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const body = (await res.json()) as { data?: { results?: Array<{ canonical_id?: string }> } };
|
||||
return (body.data?.results ?? []).map(r => r.canonical_id).filter((s): s is string => !!s);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function searchSimilarComponents(registryBase: string, nodeName: string): Promise<string[]> {
|
||||
// 1) 全名直接搜
|
||||
const direct = await searchRegistryIds(registryBase, nodeName);
|
||||
if (direct.length > 0) return direct.slice(0, 3);
|
||||
|
||||
// 2) 斷詞搜,依命中次數排序
|
||||
const tokens = extractTokens(nodeName);
|
||||
if (tokens.length === 0) return [];
|
||||
const hits = await Promise.all(tokens.map(t => searchRegistryIds(registryBase, t)));
|
||||
const count = new Map<string, number>();
|
||||
for (const ids of hits) {
|
||||
for (const id of ids) count.set(id, (count.get(id) ?? 0) + 1);
|
||||
}
|
||||
return [...count.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map(([id]) => id);
|
||||
}
|
||||
|
||||
/** recipe 庫的相近候選:KV 全列(本部署 recipe 數量小)後子字串比對。 */
|
||||
async function searchSimilarRecipes(kv: KVNamespace, nodeName: string): Promise<string[]> {
|
||||
try {
|
||||
const list = await kv.list({ prefix: 'recipe:' });
|
||||
const all = (await Promise.all(
|
||||
list.keys.map(k => kv.get(k.name, 'json') as Promise<RecipeDefinition | null>),
|
||||
)).filter(Boolean) as RecipeDefinition[];
|
||||
|
||||
const tokens = [nodeName.toLowerCase(), ...extractTokens(nodeName)];
|
||||
const seen = new Set<string>();
|
||||
const matched: string[] = [];
|
||||
for (const r of all) {
|
||||
if (seen.has(r.canonical_id)) continue;
|
||||
const hay = `${r.canonical_id} ${r.display_name ?? ''} ${r.description ?? ''}`.toLowerCase();
|
||||
if (tokens.some(t => hay.includes(t))) {
|
||||
seen.add(r.canonical_id);
|
||||
matched.push(r.canonical_id);
|
||||
}
|
||||
}
|
||||
return matched.slice(0, 3);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* target-search — POST /cypher/search 的「指定搜尋對象」名字搜尋(t159)
|
||||
*
|
||||
* leo 07-31:「search 節點名稱和 search 工作流名稱是同一個?一個死了另一個不能動?
|
||||
* 難道我不能指定要搜尋工作流或節點或 recipe 嗎?」
|
||||
*
|
||||
* ⇒ discover 入口加 `target`(component/recipe/workflow)+`query`:
|
||||
* - target=component → 轉發 registry GET /components/search(MCP arcrun_search_components 同一條路)
|
||||
* - target=recipe → 掃私庫 RECIPES KV(與 discover 混搜的第二庫**同一份讀法** listAllRecipes);
|
||||
* 公庫(多作者市場)另有 /public-recipes=MCP arcrun_recipe_search,回應註明
|
||||
* - target=workflow → lib/workflow-search.ts(GET /workflows/search=MCP arcrun_search_workflows 同一條路)
|
||||
*
|
||||
* 「外部 API 只有一條一致的路」:三個 target 各自對應**既有**搜尋機制,本檔只做轉接,
|
||||
* 不新造第二套搜尋。flag 安全:主動 pull,無輪詢。
|
||||
*/
|
||||
|
||||
import { wasmWorkerUrl } from '../lib/component-loader';
|
||||
import { fetchTenantWorkflowSearch } from '../lib/workflow-search';
|
||||
import { listAllRecipes, buildPayloadHint, type SearchNodesEnv } from './search-nodes';
|
||||
import { branchHintFor } from '../lib/branch-hints';
|
||||
|
||||
export type TargetQueryEnv = SearchNodesEnv & {
|
||||
KBDB_BASE_URL?: string;
|
||||
KBDB_INTERNAL_TOKEN?: string;
|
||||
};
|
||||
|
||||
export type TargetQueryResult =
|
||||
| { ok: true; body: Record<string, unknown> }
|
||||
| { ok: false; status: 400 | 401 | 502; error: string };
|
||||
|
||||
export async function searchByTarget(
|
||||
target: 'component' | 'recipe' | 'workflow',
|
||||
query: string,
|
||||
env: TargetQueryEnv,
|
||||
apiKey?: string,
|
||||
): Promise<TargetQueryResult> {
|
||||
if (target === 'component') {
|
||||
const sub = env.WORKER_SUBDOMAIN;
|
||||
const registryBase = env.REGISTRY_BASE_URL ?? (sub ? wasmWorkerUrl('registry', sub) : undefined);
|
||||
if (!registryBase) return { ok: false, status: 502, error: 'registry 位置未設定(WORKER_SUBDOMAIN/REGISTRY_BASE_URL 皆缺)' };
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${registryBase}/components/search?q=${encodeURIComponent(query)}`,
|
||||
{ signal: AbortSignal.timeout(10000) },
|
||||
);
|
||||
if (!res.ok) return { ok: false, status: 502, error: `registry 搜尋失敗(HTTP ${res.status})` };
|
||||
const body = (await res.json()) as { data?: { results?: unknown[]; count?: number } };
|
||||
// 3.11:逐顆查零件(n8n 式「自己一顆一顆填」)時,會分岔的零件要自我說明分支用法。
|
||||
// leo 08-01:「它可以一一查詢自己手工填寫每個零件,就像在 n8n 那樣」——
|
||||
// 這條路徑若只回 input_schema,AI 拿到 if_control/switch 仍不知道兩條路怎麼接 ⇒ 回頭寫 code。
|
||||
const results = (body.data?.results ?? []).map(r => {
|
||||
if (!r || typeof r !== 'object') return r;
|
||||
const rec = r as Record<string, unknown>;
|
||||
const hint = branchHintFor(typeof rec.canonical_id === 'string' ? rec.canonical_id : undefined);
|
||||
return hint ? { ...rec, branch_hint: hint } : rec;
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
body: {
|
||||
target,
|
||||
query,
|
||||
results,
|
||||
count: body.data?.count ?? 0,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
return { ok: false, status: 502, error: `registry 查不通:${e instanceof Error ? e.message : String(e)}` };
|
||||
}
|
||||
}
|
||||
|
||||
if (target === 'recipe') {
|
||||
if (!env.RECIPES) return { ok: false, status: 502, error: 'RECIPES KV 未綁定' };
|
||||
const all = await listAllRecipes(env.RECIPES);
|
||||
const q = query.toLowerCase();
|
||||
// 與 discover 混搜同一份庫(私庫=workflow 實際引用得到的);子字串比對、canonical 去重
|
||||
const seen = new Set<string>();
|
||||
const results: Array<{
|
||||
canonical_id: string; display_name?: string; description?: string; endpoint: string;
|
||||
payload_hint?: unknown;
|
||||
}> = [];
|
||||
for (const r of all) {
|
||||
if (seen.has(r.canonical_id)) continue;
|
||||
const hay = `${r.canonical_id} ${r.display_name ?? ''} ${r.description ?? ''}`.toLowerCase();
|
||||
if (!hay.includes(q)) continue;
|
||||
seen.add(r.canonical_id);
|
||||
results.push({
|
||||
canonical_id: r.canonical_id,
|
||||
display_name: r.display_name,
|
||||
description: r.description,
|
||||
endpoint: r.endpoint,
|
||||
// 3.12:逐顆查 recipe 時也要說得出「payload 怎麼填、回應怎麼取值」
|
||||
payload_hint: buildPayloadHint(r),
|
||||
});
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
body: {
|
||||
target,
|
||||
query,
|
||||
results,
|
||||
count: results.length,
|
||||
note: '搜的是本部署私庫(workflow 可直接 component: <canonical_id> 引用)。公庫(多作者市場)走 MCP arcrun_recipe_search/GET /public-recipes。',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// target === 'workflow':租戶隔離,必帶 API key(同 GET /workflows/search 的既有契約)
|
||||
if (!apiKey) return { ok: false, status: 401, error: 'target=workflow 需要 X-Arcrun-API-Key header(workflow 搜尋限本租戶)' };
|
||||
const res = await fetchTenantWorkflowSearch(env, apiKey, query);
|
||||
if (!res.ok) return { ok: false, status: 502, error: `workflow 搜尋失敗(KBDB HTTP ${res.status})` };
|
||||
const body = (await res.json()) as Record<string, unknown>;
|
||||
return { ok: true, body: { target, query, ...body } };
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user