Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba584002d0 |
@@ -47,6 +47,11 @@
|
||||
|
||||
---
|
||||
|
||||
## 0.18.56(2026-09-17)
|
||||
|
||||
- 🔴 **被「雲端一次處理太多」擋下、已暫停的筆記,雲端更新後會自己送上去**:免費方案的知識庫在 9 月 13 日前有個上限問題,內容多一點的卡片會被雲端退回「Too many subrequests」,記滿 8 次後小幫手就暫停不再試。雲端早已修好(1.4.64 起),但那批暫停的檔以前要改檔或按「立刻同步」才會再送。現在小幫手看到知識庫已經是修好的版本,就會自己再送一次,不用你動手。
|
||||
- **雲端還沒更新的話,照舊暫停、不會一直重撞**;在已經修好的雲端上仍被退回的卡(內容真的太大),也只會試這一次,不會反覆消耗額度。
|
||||
|
||||
## 0.18.55(2026-09-16)
|
||||
|
||||
- 🔴 **以前被記成「連續失敗 8 次、已暫停」的空白筆記,更新後會自己消掉警告**:上一版只讓新遇到的空白檔直接略過,舊版留下的那批(被雲端退回的空日記)還一直掛著「已暫停自動重試」。現在也會被認出是空白檔,改成略過,資料夾不再掛警告。
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"_algo": 4,
|
||||
"version": "0.18.55",
|
||||
"fingerprint": "287d990df2baa78c",
|
||||
"version": "0.18.56",
|
||||
"fingerprint": "d333ded9d0608cd9",
|
||||
"files": {
|
||||
".gitignore": "c7a98c44ecc882a174720e43dabd4c04d7cf2503586426dcccaa213d5a27df38",
|
||||
"CHANGELOG.md": "20a49e8ef559b89087d1ecd37ffcfe01627ca41480091a7ba8ffbd2343933293",
|
||||
"CHANGELOG.md": "17a4329d7e6ac9c9e426ec1e51658b18a48c15b8b6f5598c739404cf394ca572",
|
||||
"DAEMON_LINE": "d5019abbdc8a5f2919e9e3510391891cd7fbdf0765bf16ec83caa779f370116d",
|
||||
"README.md": "9d92cac236b20a0b183eea3e7f5e39ad492f05192c4ea602eb11c3d09967327f",
|
||||
"cardloop_test.go": "fdf09b62650c328f5d8075d964a0ca2dada5f8d9a271202768472998b45e31a8",
|
||||
@@ -130,7 +130,7 @@
|
||||
"convert_table_test.go": "d0371b7566ef3152f9dd42f9f990e0dffa1c50a0c8e874a28415fa2e4c188394",
|
||||
"convert_test.go": "04f3fa30d1be5f910c0e0be3308ced2963191ab030a9eaccd986ef581fcd4e18",
|
||||
"convert_wiring_test.go": "3389817e596341369a3fc102846bf5b081cee668e86ebf4830eb633978de652b",
|
||||
"direct.go": "7c2354adea90717611cad3b5e73f6165c74182a6c995f2087e77d9c5d0c10068",
|
||||
"direct.go": "e3d1aa9ad54bec0ff80dfa96fd0331f47590a1863cb9586497f49fae3c560277",
|
||||
"direct_extract_test.go": "317d0057d2d368fabc406351a7477f72e074d2b8c8459b1dc021febdf16902f4",
|
||||
"direct_multi_test.go": "a76593fef26231e18b7402f6f7376624d2471f8a719b3ce08fc4a0f5a3d3cf1e",
|
||||
"direct_pacing.go": "b54838ce85f72ecd2d3388fa7cf89050c87fb10bd67dca07cd18d49568a8837c",
|
||||
@@ -177,7 +177,7 @@
|
||||
"machineid_test.go": "5b54ac62ee1fb28b8f0a873888aeddb844af0aa74472bfb849573cfc6691a290",
|
||||
"machinemark.go": "592a39597fd011e8b1603318eaedf452aa58fca0639244a80218aa9a0ac6dbf5",
|
||||
"main.go": "fe2f69739f3ee92713f8aaca925068ecf97b33d476eee826fc1e0d5def8f0d10",
|
||||
"manifest.go": "9b63f8fbd0a1ccf5b3cf2f0945f4fdcdb8726a8684c74f1a37947fc7f36420ac",
|
||||
"manifest.go": "5af553d949b40a79d9342aee870f07ff445e797357ad4dafce5193f484769828",
|
||||
"manifest_extractedby_test.go": "8019fe35675a1e801b7bda829ee1d647de32db03c029b1c651f71573e644c69e",
|
||||
"manifest_retry_test.go": "e6b4a321ccddfeed8cdbfabe6d031d5a9c59a33b47baf0b6d21150fa8406748a",
|
||||
"multiaccount_connected_test.go": "ba1817b73a5dd0dd5d300750e54d5c53bd82a75ef6e43caeddfcb2303d64ff66",
|
||||
@@ -195,7 +195,7 @@
|
||||
"routebackoff_201_test.go": "9099a5e84bf124f30e1b64a9887196406ea2ea66f12d0310c7e0cdc10da354d4",
|
||||
"safewrite.go": "d5dd09ffe894fb9fb94656a4bb50d246bd8c65ab21a7ad81463f43dfdacc8a57",
|
||||
"safewrite_test.go": "2980f0f86f78fb265403e31d4cba6da43f4dbc16e7c4711cfccba23175578358",
|
||||
"scan.go": "a91ca567cff71864cb5a0a2af234a478df7118c47bfa24727f1b37c3f0466540",
|
||||
"scan.go": "867d7327ac980fbdcdac88fbfa730888f2e0da5b1fc1bee56a2fb2c928632a7e",
|
||||
"scan_carry_lasterror_test.go": "6fa542422fb3e00b5ec4355b5db8df7086361b2dacefd67755d75c0ddfa55575",
|
||||
"scan_dedup_scale_test.go": "573b3c78bec86614bf4d2f99b7c3c3157f92c24af73cae7290428fc7f43f0362",
|
||||
"scan_dedup_test.go": "580ec58596ca54aa3d29b417e7d098fd2e3455fd39bf10e7da02d453c0786df0",
|
||||
@@ -206,6 +206,7 @@
|
||||
"sourcerepair_test.go": "2a1ddddaad8ac7adc36350859e2d742c0fea12b99370a4d4831d390db1cc6720",
|
||||
"stallguard.go": "3f5d36d72f84cc3431536dbb20219bd4793b11e573b5ac7f817cbe9e45d234d7",
|
||||
"stallguard_test.go": "678b82a8448d1394a0a85d4af1f133c980f5ff0dcb54953b0fcf1671d186147f",
|
||||
"subrequest_resume_196_test.go": "7ee211839f2a85ea369fa40316d23c95e5ad18e5239fc9c816a5db553ff394a0",
|
||||
"supervisor/hidewindow_other.go": "291b4ecb0783ff3e813c68b5464161d9af2f84d31c773c7f1f710f218bb5d610",
|
||||
"supervisor/hidewindow_windows.go": "29796dbe90ecb35e6f1451dacf956d42928c6a393271befc5c7f6863d5315fb3",
|
||||
"supervisor/supervisor.go": "aec9357579bc0832e5179918912e86dbe74fc86f49904e881a6be807f59bc0ae",
|
||||
@@ -254,6 +255,7 @@
|
||||
"templatefs/system-dev/workflows/tasks-project-sync.local.sh": "7a611abc8b597d7c047e1b73c4a19c011ba1719f6688351b677b4feee0a4d4fe",
|
||||
"templatefs/system-dev/workflows/tasks-project-sync.yaml": "e8c7dd78d338e64219ad0eb7aae27aec02b6ac2ad962f309145055a87f1cd5db",
|
||||
"templateuntouched.go": "74e6c0dda22228ac65cc841ce83460d81026134d41b4b16c528bf7ce760db26f",
|
||||
"testdata/196_geek6688_paused.json": "bedd74c2de3dd837f9fdd5713fc47879a4cc512173cd3107f78a679d4d1d1946",
|
||||
"testdata/grounding/card-access-paraphrase.md": "2bbf4427b1ea2c5b73995b135752f60c4978f2c9121647c4a0aa7b42d6063570",
|
||||
"testdata/grounding/card-delivery-paraphrase.md": "727f6d09cb4ebb0e8248988bb9872573d4339f2256e6723a588eceb04aa4fd42",
|
||||
"testdata/grounding/card-geek6688-faithful.md": "7b7200aa2d142720cde51c45b7266e32a5b9f278a808a115c2546443903f09f8",
|
||||
|
||||
@@ -25,5 +25,6 @@
|
||||
"0.18.52": "5ab62f966033ea4b",
|
||||
"0.18.53": "32f62120aa472b65",
|
||||
"0.18.54": "44cf7dbf8246968a",
|
||||
"0.18.55": "287d990df2baa78c"
|
||||
"0.18.55": "287d990df2baa78c",
|
||||
"0.18.56": "d333ded9d0608cd9"
|
||||
}
|
||||
|
||||
@@ -101,6 +101,10 @@ type DirectConfig struct {
|
||||
// 不寫進 config.json(`json:"-"`):它是單次執行旗標,不是使用者設定。
|
||||
ForceSync bool `json:"-"`
|
||||
|
||||
// CloudVersion=這一輪這個帳號的雲端版本(/health 的 bundle_version,查不到時沿用上一輪)。
|
||||
// 單次執行資訊,不寫進 config.json。給逐檔退避判斷「舊雲端造成的暫停該不該再試」(#196)。
|
||||
CloudVersion string `json:"-"`
|
||||
|
||||
// B2 萃取品質檢查(daemon 端 lint,草案 §3 第一層;分支 work/b2-quality-lint-0726 移入,
|
||||
// 2026-08-09 重新接到現行 direct.go——原分支落後 main 130 筆,不能硬併,見
|
||||
// wiki status.md:1570-1573):extractor 模式萃完、POST 前跑。
|
||||
@@ -853,6 +857,7 @@ func RunDirectOnce(cfg *DirectConfig, dryRun bool) ([]DirectResult, int, *Trigge
|
||||
// t215:這個帳號要不要更新——與 portal 版本卡同一套判準(見檔頭)。
|
||||
// 第二個參數傳「我們知不知道它的版本」而不是「這一輪連不連得上」:
|
||||
// 版本是**事實**,可達性是**當下狀態**,兩者不是同一個問題。
|
||||
accCfg.CloudVersion = cloudVer // #196:逐檔退避要知道雲端是不是已經修好子請求那面牆
|
||||
cloudUpd := EvalCloudUpdate(cloudVer, strings.TrimSpace(cloudVer) != "", latestRelease, latestOK)
|
||||
accSt := AccountSyncStatus{
|
||||
LastSync: time.Now().Format(time.RFC3339),
|
||||
@@ -1540,6 +1545,7 @@ func runDirectOnceRoot(cfg *DirectConfig, root string, dryRun bool, qs *quotaSta
|
||||
if err != nil {
|
||||
return append(results, DirectResult{Status: "failed", Error: err.Error()}), 1, nil, rootProgress{}
|
||||
}
|
||||
m.CloudVersion = cfg.CloudVersion // #196
|
||||
// 2026-08-07 task 3(斷點續傳):每個事件處理完就立刻存檔,不要等整輪跑完。
|
||||
// 舊行為=整個 for 迴圈跑完才 Save 一次——process 在跑到一半被殺掉(重開機、
|
||||
// 換版、當機)時,**已經成功的那些也會遺失**,下次重開等於從頭來過,
|
||||
|
||||
+36
-1
@@ -56,6 +56,11 @@ type ManifestEntry struct {
|
||||
// 真因(Cloudflare「當日免費額度用完」/「這份 PDF 沒有文字層」)**當場消失**
|
||||
// ⇒ 使用者以為是我們壞掉。原因必須跟著 entry 存活到下次成功為止。
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
// FailCloudVersion=最後一次失敗時,這個帳號的雲端版本(/health 的 bundle_version;""=當時不知道)。
|
||||
// 為什麼要記(inkstone/arcrun-rag#196):「單次呼叫子請求太多」在 1.4.64 之前是雲端引擎的病,
|
||||
// 之後就是這張卡真的太大。只看錯誤原文分不出是哪一種——要知道**當時打的是哪一版雲端**,
|
||||
// 才能做到「舊雲端造成的暫停,雲端更新後自己再試一次;新雲端上還撞牆就照舊暫停,不反覆燒額度」。
|
||||
FailCloudVersion string `json:"fail_cloud_version,omitempty"`
|
||||
|
||||
// ── 雲端對帳(`inkstone/arcrun-rag#140`,2026-08-26)──────────────────────
|
||||
// 病:上面那個 IngestedHash 的章**永遠不會過期**。雲端 08-14 被重裝/清空之後,
|
||||
@@ -91,6 +96,9 @@ const MaxFailBeforeSkip = 8
|
||||
// Manifest 對應一個被勾選的資料夾。
|
||||
type Manifest struct {
|
||||
FolderID string `json:"folder_id"`
|
||||
// CloudVersion=這一輪這個帳號的雲端版本(執行期由呼叫端填,不寫進檔案)。
|
||||
// MarkFailed 把它記進病歷、ShouldRetry 拿它判斷「雲端是不是已經修好那面牆」(#196)。
|
||||
CloudVersion string `json:"-"`
|
||||
// FolderCardHashes=每個子資料夾索引卡的內容雜湊(鍵=相對監看根的目錄路徑)。
|
||||
// 冪等用:內容沒變就不重送(同 InventoryHash 的角色,只是一層變多層)。
|
||||
// 資料夾消失時由 syncFolderCards 清掉對應的鍵,不讓這張表無限長大。
|
||||
@@ -234,6 +242,7 @@ func (m *Manifest) MarkIngestedBy(path, sourceHash string, at int64, extractor s
|
||||
// 成功即清掉失敗狀態(t195):下次再壞會從第一階退避重新算起。
|
||||
e.FailCount, e.LastFailAt, e.NextRetry = 0, 0, 0
|
||||
e.LastError = ""
|
||||
e.FailCloudVersion = ""
|
||||
// #140:預設「這次有送卡上雲」;真的一張卡都沒送的那條路由呼叫端補打
|
||||
// MarkNoCloudCard(見 direct.go 的 cards 為空分支)。預設值放這裡而不是
|
||||
// 讓呼叫端每次都設,是因為漏設的方向要落在**安全的那一邊**:
|
||||
@@ -275,6 +284,7 @@ func (m *Manifest) MarkFailed(path string, at int64, reason string) bool {
|
||||
}
|
||||
e.FailCount++
|
||||
e.LastFailAt = at
|
||||
e.FailCloudVersion = m.CloudVersion // #196:記下這次撞的是哪一版雲端
|
||||
if strings.TrimSpace(reason) != "" {
|
||||
e.LastError = reason // 存真因;退避訊息由呼叫端另外組,不覆蓋這裡
|
||||
}
|
||||
@@ -344,11 +354,36 @@ func (m *Manifest) ShouldRetry(path string, now int64, force bool) bool {
|
||||
if e.FailCount >= MaxFailBeforeSkip {
|
||||
// #201:「雲端當時還是舊版、沒有 AI」不是這個檔的病——雲端更新之後要自己再試。
|
||||
// 退避窗口照舊(最長 6 小時一次),雲端還沒更新的話頂多每 6 小時白問一發。
|
||||
return isOldCloudText(e.LastError) && now >= e.NextRetry
|
||||
// #196:同理,「單次呼叫子請求太多」在雲端 1.4.64 之前是引擎的病(每個節點多打 3 發附帶呼叫)。
|
||||
// 病歷是在舊雲端上記的、而現在的雲端已經修好 ⇒ 再試。
|
||||
// 在已修好的雲端上又撞 ⇒ 這次 MarkFailed 會記下新版本號 ⇒ 不再自動重試(卡真的太大,重撞只是燒額度)。
|
||||
return (isOldCloudText(e.LastError) || m.subrequestWallFixedSince(e)) && now >= e.NextRetry
|
||||
}
|
||||
return now >= e.NextRetry
|
||||
}
|
||||
|
||||
// subrequestFixedRelease=雲端第一個帶「每次呼叫附帶子請求改成常數」修正的版本
|
||||
// (Arcrun 1eb26c9,首次出貨=1.4.64,inkstone/arcrun-rag#196)。
|
||||
const subrequestFixedRelease = "1.4.64"
|
||||
|
||||
// isSubrequestLimitText=病歷是 Cloudflare「單次 Worker 呼叫子請求太多」。
|
||||
func isSubrequestLimitText(msg string) bool {
|
||||
return strings.Contains(msg, "Too many subrequests")
|
||||
}
|
||||
|
||||
// cloudHasSubrequestFix=這個雲端版本已經帶子請求修正(""、舊日期格式、比 1.4.64 舊 ⇒ false)。
|
||||
func cloudHasSubrequestFix(version string) bool {
|
||||
head := strings.TrimSpace(strings.SplitN(version, "+", 2)[0])
|
||||
return isSemverLike(head) && compareSemver(head, subrequestFixedRelease) >= 0
|
||||
}
|
||||
|
||||
// subrequestWallFixedSince=這筆暫停是舊雲端的子請求牆造成的,而這一輪的雲端已經修好。
|
||||
func (m *Manifest) subrequestWallFixedSince(e *ManifestEntry) bool {
|
||||
return isSubrequestLimitText(e.LastError) &&
|
||||
cloudHasSubrequestFix(m.CloudVersion) &&
|
||||
!cloudHasSubrequestFix(e.FailCloudVersion)
|
||||
}
|
||||
|
||||
// isOldCloudText=病歷上寫的是「雲端還沒有這個功能」(舊版雲端),不是檔案本身的問題。
|
||||
func isOldCloudText(msg string) bool {
|
||||
return strings.Contains(msg, "沒有綁定 Workers AI") || strings.Contains(msg, "沒有雲端萃取功能")
|
||||
|
||||
@@ -706,6 +706,7 @@ func Scan(root string, m *Manifest, opts ScanOptions) (*TriggerPayload, error) {
|
||||
// ⇒ 畫面只剩 humanizeFailure 的最後退路「當時沒有記下原因」。
|
||||
// 這正是上面那句警告的第二次實例——真因是我們自己刪掉的,不是沒記。
|
||||
ne.LastError = carry.LastError
|
||||
ne.FailCloudVersion = carry.FailCloudVersion // #196:同上那句警告——漏 carry 就分不出暫停是在哪一版雲端記的
|
||||
// 🔴 #140(2026-08-26):雲端對帳的三個欄位同樣要 carry,而且漏了會**很貴**:
|
||||
// - CloudCheckedAt 歸零 ⇒ 每輪都判「太久沒對帳」⇒ 每輪重問整批(請求風暴)
|
||||
// - CloudMissingAt 歸零 ⇒ **防重送迴圈的 grace 消失** ⇒ 補送 → 下一輪又拔章
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
package collector
|
||||
|
||||
// inkstone/arcrun-rag#196:雲端 1.4.64 起修掉「單次呼叫子請求太多」那面牆,
|
||||
// 但修之前被記滿 8 次、暫停自動重試的檔不會自己動——用戶不改檔、不按「立刻同步」就永遠送不上去。
|
||||
// 09-17 實查 geek6688:13 份掛著這種暫停,含本票量測用的兩張卡。
|
||||
// 本檔用那兩張卡在 geek6688 帳本上的**原樣病歷**(testdata/196_geek6688_paused.json)重演。
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type pausedFixture struct {
|
||||
Entries map[string]struct {
|
||||
FailCount int `json:"fail_count"`
|
||||
LastFailAt int64 `json:"last_fail_at"`
|
||||
NextRetry int64 `json:"next_retry"`
|
||||
LastError string `json:"last_error"`
|
||||
} `json:"entries"`
|
||||
}
|
||||
|
||||
const (
|
||||
card196Sample = "system-dev/wiki/cards/arcrun-sample-knowledge.md"
|
||||
card196N8n = "system-dev/wiki/cards/arcrun-n8n版本比較表新-2025.11-範本.md"
|
||||
)
|
||||
|
||||
// fakeCloud196 =一台假知識庫:萃取一律成功;收卡依 wall 決定回成功或回 geek6688 那句 500。
|
||||
type fakeCloud196 struct {
|
||||
mu sync.Mutex
|
||||
version string // /health 的 bundle_version
|
||||
wall map[string]bool // 這些檔收卡時回「Too many subrequests」
|
||||
posts map[string]int // 每個檔實際打到 rag_ingest_card 幾次
|
||||
wallErr string
|
||||
}
|
||||
|
||||
func (f *fakeCloud196) handler(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
var req struct {
|
||||
PageName string `json:"page_name"`
|
||||
Text string `json:"text"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
_ = json.Unmarshal(body, &req)
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
switch {
|
||||
case strings.HasSuffix(r.URL.Path, "/portal/daemon/extract"):
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"success": true, "output": cardFixture(req.PageName, "測試")})
|
||||
case strings.HasSuffix(r.URL.Path, "/rag_ingest_card/trigger"):
|
||||
f.posts[req.Path]++
|
||||
if f.wall[req.Path] {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = io.WriteString(w, f.wallErr)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"success": true})
|
||||
default:
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"success": true})
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fakeCloud196) postsOf(p string) int {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return f.posts[p]
|
||||
}
|
||||
|
||||
// setup196 建出「geek6688 今天的樣子」:兩張卡在帳本上是舊版留下的暫停病歷(沒記雲端版本)。
|
||||
func setup196(t *testing.T) (cfg *DirectConfig, mp, root string, cloud *fakeCloud196, clock *time.Time) {
|
||||
t.Helper()
|
||||
cloudRoutes.reset()
|
||||
t.Cleanup(cloudRoutes.reset)
|
||||
withCloudCheckInterval(t, 0)
|
||||
|
||||
raw, err := os.ReadFile(filepath.Join("testdata", "196_geek6688_paused.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var fx pausedFixture
|
||||
if err := json.Unmarshal(raw, &fx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(fx.Entries) != 2 {
|
||||
t.Fatalf("fixture 應有兩張卡:%d", len(fx.Entries))
|
||||
}
|
||||
|
||||
root = t.TempDir()
|
||||
writeFile(t, root, card196Sample, "# Arcrun 範例知識\n\nArcrun 是跑在 Cloudflare 上的工作流引擎。", baseTime)
|
||||
writeFile(t, root, card196N8n, "# n8n 版本比較表\n\n| 版本 | 說明 |\n|---|---|\n| 1.0 | 測試 |", baseTime)
|
||||
|
||||
cloud = &fakeCloud196{wall: map[string]bool{}, posts: map[string]int{},
|
||||
wallErr: fx.Entries[card196Sample].LastError[len("HTTP 500:"):]}
|
||||
oldFetch := fetchCloudVersion
|
||||
fetchCloudVersion = func(string) (string, bool) {
|
||||
cloud.mu.Lock()
|
||||
defer cloud.mu.Unlock()
|
||||
return cloud.version, true
|
||||
}
|
||||
t.Cleanup(func() { fetchCloudVersion = oldFetch })
|
||||
|
||||
// 先占位址再關掉:第一輪連不上(只為了建出帳本),之後在同一個位址開假雲端。
|
||||
ln0, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
addr := ln0.Addr().String()
|
||||
_ = ln0.Close()
|
||||
|
||||
c := time.Unix(fx.Entries[card196Sample].NextRetry, 0).Add(4 * 24 * time.Hour) // 09-17:暫停早已過了退避窗口
|
||||
clock = &c
|
||||
oldNow := directNow
|
||||
directNow = func() time.Time { return *clock }
|
||||
t.Cleanup(func() { directNow = oldNow })
|
||||
|
||||
cfg = &DirectConfig{
|
||||
Manifest: filepath.Join(t.TempDir(), "m.json"),
|
||||
CardIngestWF: "rag_ingest_card", IngestWF: "rag_ingest_direct", RemovedWF: "rag_takedown_direct",
|
||||
Extractor: "workers-ai", ExtractorExplicit: true, MachineLabel: "測試機",
|
||||
MaxRemoved: DefaultMaxRemovedRatio,
|
||||
Accounts: []AccountConfig{{CypherURL: "http://" + addr, Namespace: "demo", APIKey: "demo",
|
||||
InstanceName: "geek6688", WatchFolders: []string{root}}},
|
||||
}
|
||||
RunDirectOnce(cfg, false)
|
||||
cloudRoutes.reset() // 建帳本那一輪的連線失敗不要帶進正式情境
|
||||
acc := cfg.makeAccountSubConfig(cfg.Accounts[0])
|
||||
mp = acc.manifestPathFor(root)
|
||||
m, err := LoadManifest(mp, root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for p, f := range fx.Entries {
|
||||
e := m.Entries[p]
|
||||
if e == nil {
|
||||
t.Fatalf("帳本裡沒有 %s", p)
|
||||
}
|
||||
e.IngestedHash, e.IngestedAt, e.NoCloudCard = "", 0, false
|
||||
e.FailCount, e.LastFailAt, e.NextRetry, e.LastError = f.FailCount, f.LastFailAt, f.NextRetry, f.LastError
|
||||
e.FailCloudVersion = "" // 舊版小幫手不記這欄
|
||||
}
|
||||
if err := m.Save(mp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
t.Skipf("無法重新綁定同一個埠(%v)", err)
|
||||
}
|
||||
srv := &http.Server{Handler: http.HandlerFunc(cloud.handler)}
|
||||
go func() { _ = srv.Serve(ln) }()
|
||||
t.Cleanup(func() { _ = srv.Close() })
|
||||
if p := mustLoad(t, mp, root).Progress(); p.Stuck != 2 {
|
||||
t.Fatalf("前置條件:兩張卡都該是暫停:%+v", p)
|
||||
}
|
||||
return cfg, mp, root, cloud, clock
|
||||
}
|
||||
|
||||
func mustLoad(t *testing.T, mp, root string) *Manifest {
|
||||
t.Helper()
|
||||
m, err := LoadManifest(mp, root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// 雲端還是舊版 ⇒ 照舊暫停、不重撞;雲端更新到 1.4.64 以上 ⇒ 不用人動手,兩張卡自己送上去、算完成。
|
||||
func TestDirect196_SubrequestPausedResumesAfterCloudFix(t *testing.T) {
|
||||
cfg, mp, root, cloud, clock := setup196(t)
|
||||
|
||||
cloud.version = "1.4.63"
|
||||
results, _, _ := RunDirectOnce(cfg, false)
|
||||
if n := cloud.postsOf(card196Sample) + cloud.postsOf(card196N8n); n != 0 {
|
||||
t.Fatalf("雲端還是 1.4.63(沒修那面牆)時不該重撞,卻送了 %d 發", n)
|
||||
}
|
||||
for _, r := range results {
|
||||
if (r.Path == card196Sample || r.Path == card196N8n) && !strings.Contains(r.Error, "已暫停自動重試") {
|
||||
t.Fatalf("舊雲端上 %s 應維持暫停說明,拿到 %q", r.Path, r.Error)
|
||||
}
|
||||
}
|
||||
if p := mustLoad(t, mp, root).Progress(); p.Stuck != 2 {
|
||||
t.Fatalf("舊雲端上兩張都該維持暫停:%+v", p)
|
||||
}
|
||||
|
||||
cloud.version = "1.4.67" // geek6688 09-17 的 /health
|
||||
*clock = clock.Add(time.Minute)
|
||||
RunDirectOnce(cfg, false)
|
||||
if cloud.postsOf(card196Sample) == 0 || cloud.postsOf(card196N8n) == 0 {
|
||||
t.Fatalf("雲端更新後兩張都要自己送出:sample=%d n8n=%d", cloud.postsOf(card196Sample), cloud.postsOf(card196N8n))
|
||||
}
|
||||
m := mustLoad(t, mp, root)
|
||||
if p := m.Progress(); p.Stuck != 0 || p.Done != p.Total {
|
||||
t.Fatalf("兩張都要算完成:%+v", p)
|
||||
}
|
||||
for _, p := range []string{card196Sample, card196N8n} {
|
||||
if e := m.Entries[p]; e.FailCount != 0 || e.LastError != "" || e.FailCloudVersion != "" {
|
||||
t.Fatalf("%s 送成功後病歷要清乾淨:%+v", p, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在已經修好的雲端上**又**撞同一面牆(卡真的太大)⇒ 記下這一版,之後不再自動重撞燒額度。
|
||||
func TestDirect196_StillTooBigOnFixedCloudStaysPaused(t *testing.T) {
|
||||
cfg, mp, root, cloud, clock := setup196(t)
|
||||
cloud.version = "1.4.67"
|
||||
cloud.wall[card196Sample] = true
|
||||
|
||||
RunDirectOnce(cfg, false)
|
||||
first := cloud.postsOf(card196Sample)
|
||||
if first == 0 {
|
||||
t.Fatal("雲端已修好時,舊暫停的卡應該要再試一次")
|
||||
}
|
||||
m := mustLoad(t, mp, root)
|
||||
e := m.Entries[card196Sample]
|
||||
if e.FailCount < MaxFailBeforeSkip || e.FailCloudVersion != "1.4.67" || !isSubrequestLimitText(e.LastError) {
|
||||
t.Fatalf("在新雲端上又撞牆,病歷要記下 1.4.67:%+v", e)
|
||||
}
|
||||
if p := m.Progress(); p.Done != 1 || p.Stuck != 1 {
|
||||
t.Fatalf("n8n 那張送成、sample 那張暫停:%+v", p)
|
||||
}
|
||||
|
||||
*clock = clock.Add(7 * 24 * time.Hour) // 遠超過最長退避
|
||||
cloudRoutes.reset()
|
||||
RunDirectOnce(cfg, false)
|
||||
if got := cloud.postsOf(card196Sample); got != first {
|
||||
t.Fatalf("已在修好的雲端上撞過牆,不該再自動重撞:%d → %d", first, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloudHasSubrequestFix196(t *testing.T) {
|
||||
for v, want := range map[string]bool{
|
||||
"": false, "1.4.63": false, "1.4.64": true, "1.4.67+b08a8cee": true, "1.10.0": true,
|
||||
"2026-09-13+1eb26c9": false, "unknown": false,
|
||||
} {
|
||||
if got := cloudHasSubrequestFix(v); got != want {
|
||||
t.Errorf("cloudHasSubrequestFix(%q)=%v,要 %v", v, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"_source": "geek6688(arcrun-fc9490d5)~/Desktop/geek6688-test1 的帳本原樣,2026-09-17 讀出;inkstone/arcrun-rag#196",
|
||||
"entries": {
|
||||
"system-dev/wiki/cards/arcrun-sample-knowledge.md": {
|
||||
"fail_count": 8,
|
||||
"last_fail_at": 1788188201,
|
||||
"next_retry": 1788209801,
|
||||
"last_error": "HTTP 500:{\"success\":false,\"error\":\"Node post_block failed: Too many subrequests by single Worker invocation. To configure this limit, refer to https://developers.cloudflare.com/workers/wrangler/configuration/#limits\",\"trace\":[{\"node\":\"input\",\"status\":\"success\"},{\"node\":\"list_old_blocks\",\"status\":\"success\"},{\"node\":\"list_old_triplets\",\"status\":\"success\"},{\"node\":\"pick_stale\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_record\",\"status\":\"success\"},{\"node\":\"delete_record\",\"status\":\"success\"},{\"node\":\"delete_record\",\"status\":\"success\"},{\"node\":\"delete_record\",\"status\":\"success\"},{\"node\":\"parse_card\",\"status\":\"success\"},{\"node\":\"post_block\",\"status\":\"f"
|
||||
},
|
||||
"system-dev/wiki/cards/arcrun-n8n版本比較表新-2025.11-範本.md": {
|
||||
"fail_count": 8,
|
||||
"last_fail_at": 1788188201,
|
||||
"next_retry": 1788209801,
|
||||
"last_error": "HTTP 500:{\"success\":false,\"error\":\"Node post_block failed: Too many subrequests by single Worker invocation. To configure this limit, refer to https://developers.cloudflare.com/workers/wrangler/configuration/#limits\",\"trace\":[{\"node\":\"input\",\"status\":\"success\"},{\"node\":\"list_old_blocks\",\"status\":\"success\"},{\"node\":\"list_old_triplets\",\"status\":\"success\"},{\"node\":\"pick_stale\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_entry\",\"status\":\"success\"},{\"node\":\"delete_record\",\"status\":\"success\"},{\"node\":\"delete_record\",\"status\":\"success\"},{\"node\":\"delete_record\",\"status\":\"success\"},{\"node\":\"delete_record\",\"status\":\"success\"},{\"node\":\"parse_card\",\"status\":\"success\"},{\"node\":\"post_block\",\"status\":\"f"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user