Compare commits

...

1 Commits

8 changed files with 320 additions and 8 deletions
+5
View File
@@ -47,6 +47,11 @@
--- ---
## 0.18.562026-09-17
- 🔴 **被「雲端一次處理太多」擋下、已暫停的筆記,雲端更新後會自己送上去**:免費方案的知識庫在 9 月 13 日前有個上限問題,內容多一點的卡片會被雲端退回「Too many subrequests」,記滿 8 次後小幫手就暫停不再試。雲端早已修好(1.4.64 起),但那批暫停的檔以前要改檔或按「立刻同步」才會再送。現在小幫手看到知識庫已經是修好的版本,就會自己再送一次,不用你動手。
- **雲端還沒更新的話,照舊暫停、不會一直重撞**;在已經修好的雲端上仍被退回的卡(內容真的太大),也只會試這一次,不會反覆消耗額度。
## 0.18.552026-09-16 ## 0.18.552026-09-16
- 🔴 **以前被記成「連續失敗 8 次、已暫停」的空白筆記,更新後會自己消掉警告**:上一版只讓新遇到的空白檔直接略過,舊版留下的那批(被雲端退回的空日記)還一直掛著「已暫停自動重試」。現在也會被認出是空白檔,改成略過,資料夾不再掛警告。 - 🔴 **以前被記成「連續失敗 8 次、已暫停」的空白筆記,更新後會自己消掉警告**:上一版只讓新遇到的空白檔直接略過,舊版留下的那批(被雲端退回的空日記)還一直掛著「已暫停自動重試」。現在也會被認出是空白檔,改成略過,資料夾不再掛警告。
+8 -6
View File
@@ -1,10 +1,10 @@
{ {
"_algo": 4, "_algo": 4,
"version": "0.18.55", "version": "0.18.56",
"fingerprint": "287d990df2baa78c", "fingerprint": "d333ded9d0608cd9",
"files": { "files": {
".gitignore": "c7a98c44ecc882a174720e43dabd4c04d7cf2503586426dcccaa213d5a27df38", ".gitignore": "c7a98c44ecc882a174720e43dabd4c04d7cf2503586426dcccaa213d5a27df38",
"CHANGELOG.md": "20a49e8ef559b89087d1ecd37ffcfe01627ca41480091a7ba8ffbd2343933293", "CHANGELOG.md": "17a4329d7e6ac9c9e426ec1e51658b18a48c15b8b6f5598c739404cf394ca572",
"DAEMON_LINE": "d5019abbdc8a5f2919e9e3510391891cd7fbdf0765bf16ec83caa779f370116d", "DAEMON_LINE": "d5019abbdc8a5f2919e9e3510391891cd7fbdf0765bf16ec83caa779f370116d",
"README.md": "9d92cac236b20a0b183eea3e7f5e39ad492f05192c4ea602eb11c3d09967327f", "README.md": "9d92cac236b20a0b183eea3e7f5e39ad492f05192c4ea602eb11c3d09967327f",
"cardloop_test.go": "fdf09b62650c328f5d8075d964a0ca2dada5f8d9a271202768472998b45e31a8", "cardloop_test.go": "fdf09b62650c328f5d8075d964a0ca2dada5f8d9a271202768472998b45e31a8",
@@ -130,7 +130,7 @@
"convert_table_test.go": "d0371b7566ef3152f9dd42f9f990e0dffa1c50a0c8e874a28415fa2e4c188394", "convert_table_test.go": "d0371b7566ef3152f9dd42f9f990e0dffa1c50a0c8e874a28415fa2e4c188394",
"convert_test.go": "04f3fa30d1be5f910c0e0be3308ced2963191ab030a9eaccd986ef581fcd4e18", "convert_test.go": "04f3fa30d1be5f910c0e0be3308ced2963191ab030a9eaccd986ef581fcd4e18",
"convert_wiring_test.go": "3389817e596341369a3fc102846bf5b081cee668e86ebf4830eb633978de652b", "convert_wiring_test.go": "3389817e596341369a3fc102846bf5b081cee668e86ebf4830eb633978de652b",
"direct.go": "7c2354adea90717611cad3b5e73f6165c74182a6c995f2087e77d9c5d0c10068", "direct.go": "e3d1aa9ad54bec0ff80dfa96fd0331f47590a1863cb9586497f49fae3c560277",
"direct_extract_test.go": "317d0057d2d368fabc406351a7477f72e074d2b8c8459b1dc021febdf16902f4", "direct_extract_test.go": "317d0057d2d368fabc406351a7477f72e074d2b8c8459b1dc021febdf16902f4",
"direct_multi_test.go": "a76593fef26231e18b7402f6f7376624d2471f8a719b3ce08fc4a0f5a3d3cf1e", "direct_multi_test.go": "a76593fef26231e18b7402f6f7376624d2471f8a719b3ce08fc4a0f5a3d3cf1e",
"direct_pacing.go": "b54838ce85f72ecd2d3388fa7cf89050c87fb10bd67dca07cd18d49568a8837c", "direct_pacing.go": "b54838ce85f72ecd2d3388fa7cf89050c87fb10bd67dca07cd18d49568a8837c",
@@ -177,7 +177,7 @@
"machineid_test.go": "5b54ac62ee1fb28b8f0a873888aeddb844af0aa74472bfb849573cfc6691a290", "machineid_test.go": "5b54ac62ee1fb28b8f0a873888aeddb844af0aa74472bfb849573cfc6691a290",
"machinemark.go": "592a39597fd011e8b1603318eaedf452aa58fca0639244a80218aa9a0ac6dbf5", "machinemark.go": "592a39597fd011e8b1603318eaedf452aa58fca0639244a80218aa9a0ac6dbf5",
"main.go": "fe2f69739f3ee92713f8aaca925068ecf97b33d476eee826fc1e0d5def8f0d10", "main.go": "fe2f69739f3ee92713f8aaca925068ecf97b33d476eee826fc1e0d5def8f0d10",
"manifest.go": "9b63f8fbd0a1ccf5b3cf2f0945f4fdcdb8726a8684c74f1a37947fc7f36420ac", "manifest.go": "5af553d949b40a79d9342aee870f07ff445e797357ad4dafce5193f484769828",
"manifest_extractedby_test.go": "8019fe35675a1e801b7bda829ee1d647de32db03c029b1c651f71573e644c69e", "manifest_extractedby_test.go": "8019fe35675a1e801b7bda829ee1d647de32db03c029b1c651f71573e644c69e",
"manifest_retry_test.go": "e6b4a321ccddfeed8cdbfabe6d031d5a9c59a33b47baf0b6d21150fa8406748a", "manifest_retry_test.go": "e6b4a321ccddfeed8cdbfabe6d031d5a9c59a33b47baf0b6d21150fa8406748a",
"multiaccount_connected_test.go": "ba1817b73a5dd0dd5d300750e54d5c53bd82a75ef6e43caeddfcb2303d64ff66", "multiaccount_connected_test.go": "ba1817b73a5dd0dd5d300750e54d5c53bd82a75ef6e43caeddfcb2303d64ff66",
@@ -195,7 +195,7 @@
"routebackoff_201_test.go": "9099a5e84bf124f30e1b64a9887196406ea2ea66f12d0310c7e0cdc10da354d4", "routebackoff_201_test.go": "9099a5e84bf124f30e1b64a9887196406ea2ea66f12d0310c7e0cdc10da354d4",
"safewrite.go": "d5dd09ffe894fb9fb94656a4bb50d246bd8c65ab21a7ad81463f43dfdacc8a57", "safewrite.go": "d5dd09ffe894fb9fb94656a4bb50d246bd8c65ab21a7ad81463f43dfdacc8a57",
"safewrite_test.go": "2980f0f86f78fb265403e31d4cba6da43f4dbc16e7c4711cfccba23175578358", "safewrite_test.go": "2980f0f86f78fb265403e31d4cba6da43f4dbc16e7c4711cfccba23175578358",
"scan.go": "a91ca567cff71864cb5a0a2af234a478df7118c47bfa24727f1b37c3f0466540", "scan.go": "867d7327ac980fbdcdac88fbfa730888f2e0da5b1fc1bee56a2fb2c928632a7e",
"scan_carry_lasterror_test.go": "6fa542422fb3e00b5ec4355b5db8df7086361b2dacefd67755d75c0ddfa55575", "scan_carry_lasterror_test.go": "6fa542422fb3e00b5ec4355b5db8df7086361b2dacefd67755d75c0ddfa55575",
"scan_dedup_scale_test.go": "573b3c78bec86614bf4d2f99b7c3c3157f92c24af73cae7290428fc7f43f0362", "scan_dedup_scale_test.go": "573b3c78bec86614bf4d2f99b7c3c3157f92c24af73cae7290428fc7f43f0362",
"scan_dedup_test.go": "580ec58596ca54aa3d29b417e7d098fd2e3455fd39bf10e7da02d453c0786df0", "scan_dedup_test.go": "580ec58596ca54aa3d29b417e7d098fd2e3455fd39bf10e7da02d453c0786df0",
@@ -206,6 +206,7 @@
"sourcerepair_test.go": "2a1ddddaad8ac7adc36350859e2d742c0fea12b99370a4d4831d390db1cc6720", "sourcerepair_test.go": "2a1ddddaad8ac7adc36350859e2d742c0fea12b99370a4d4831d390db1cc6720",
"stallguard.go": "3f5d36d72f84cc3431536dbb20219bd4793b11e573b5ac7f817cbe9e45d234d7", "stallguard.go": "3f5d36d72f84cc3431536dbb20219bd4793b11e573b5ac7f817cbe9e45d234d7",
"stallguard_test.go": "678b82a8448d1394a0a85d4af1f133c980f5ff0dcb54953b0fcf1671d186147f", "stallguard_test.go": "678b82a8448d1394a0a85d4af1f133c980f5ff0dcb54953b0fcf1671d186147f",
"subrequest_resume_196_test.go": "7ee211839f2a85ea369fa40316d23c95e5ad18e5239fc9c816a5db553ff394a0",
"supervisor/hidewindow_other.go": "291b4ecb0783ff3e813c68b5464161d9af2f84d31c773c7f1f710f218bb5d610", "supervisor/hidewindow_other.go": "291b4ecb0783ff3e813c68b5464161d9af2f84d31c773c7f1f710f218bb5d610",
"supervisor/hidewindow_windows.go": "29796dbe90ecb35e6f1451dacf956d42928c6a393271befc5c7f6863d5315fb3", "supervisor/hidewindow_windows.go": "29796dbe90ecb35e6f1451dacf956d42928c6a393271befc5c7f6863d5315fb3",
"supervisor/supervisor.go": "aec9357579bc0832e5179918912e86dbe74fc86f49904e881a6be807f59bc0ae", "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.local.sh": "7a611abc8b597d7c047e1b73c4a19c011ba1719f6688351b677b4feee0a4d4fe",
"templatefs/system-dev/workflows/tasks-project-sync.yaml": "e8c7dd78d338e64219ad0eb7aae27aec02b6ac2ad962f309145055a87f1cd5db", "templatefs/system-dev/workflows/tasks-project-sync.yaml": "e8c7dd78d338e64219ad0eb7aae27aec02b6ac2ad962f309145055a87f1cd5db",
"templateuntouched.go": "74e6c0dda22228ac65cc841ce83460d81026134d41b4b16c528bf7ce760db26f", "templateuntouched.go": "74e6c0dda22228ac65cc841ce83460d81026134d41b4b16c528bf7ce760db26f",
"testdata/196_geek6688_paused.json": "bedd74c2de3dd837f9fdd5713fc47879a4cc512173cd3107f78a679d4d1d1946",
"testdata/grounding/card-access-paraphrase.md": "2bbf4427b1ea2c5b73995b135752f60c4978f2c9121647c4a0aa7b42d6063570", "testdata/grounding/card-access-paraphrase.md": "2bbf4427b1ea2c5b73995b135752f60c4978f2c9121647c4a0aa7b42d6063570",
"testdata/grounding/card-delivery-paraphrase.md": "727f6d09cb4ebb0e8248988bb9872573d4339f2256e6723a588eceb04aa4fd42", "testdata/grounding/card-delivery-paraphrase.md": "727f6d09cb4ebb0e8248988bb9872573d4339f2256e6723a588eceb04aa4fd42",
"testdata/grounding/card-geek6688-faithful.md": "7b7200aa2d142720cde51c45b7266e32a5b9f278a808a115c2546443903f09f8", "testdata/grounding/card-geek6688-faithful.md": "7b7200aa2d142720cde51c45b7266e32a5b9f278a808a115c2546443903f09f8",
+2 -1
View File
@@ -25,5 +25,6 @@
"0.18.52": "5ab62f966033ea4b", "0.18.52": "5ab62f966033ea4b",
"0.18.53": "32f62120aa472b65", "0.18.53": "32f62120aa472b65",
"0.18.54": "44cf7dbf8246968a", "0.18.54": "44cf7dbf8246968a",
"0.18.55": "287d990df2baa78c" "0.18.55": "287d990df2baa78c",
"0.18.56": "d333ded9d0608cd9"
} }
+6
View File
@@ -101,6 +101,10 @@ type DirectConfig struct {
// 不寫進 config.json`json:"-"`):它是單次執行旗標,不是使用者設定。 // 不寫進 config.json`json:"-"`):它是單次執行旗標,不是使用者設定。
ForceSync bool `json:"-"` ForceSync bool `json:"-"`
// CloudVersion=這一輪這個帳號的雲端版本(/health 的 bundle_version,查不到時沿用上一輪)。
// 單次執行資訊,不寫進 config.json。給逐檔退避判斷「舊雲端造成的暫停該不該再試」(#196)。
CloudVersion string `json:"-"`
// B2 萃取品質檢查(daemon 端 lint,草案 §3 第一層;分支 work/b2-quality-lint-0726 移入, // B2 萃取品質檢查(daemon 端 lint,草案 §3 第一層;分支 work/b2-quality-lint-0726 移入,
// 2026-08-09 重新接到現行 direct.go——原分支落後 main 130 筆,不能硬併,見 // 2026-08-09 重新接到現行 direct.go——原分支落後 main 130 筆,不能硬併,見
// wiki status.md:1570-1573):extractor 模式萃完、POST 前跑。 // wiki status.md:1570-1573):extractor 模式萃完、POST 前跑。
@@ -853,6 +857,7 @@ func RunDirectOnce(cfg *DirectConfig, dryRun bool) ([]DirectResult, int, *Trigge
// t215:這個帳號要不要更新——與 portal 版本卡同一套判準(見檔頭)。 // t215:這個帳號要不要更新——與 portal 版本卡同一套判準(見檔頭)。
// 第二個參數傳「我們知不知道它的版本」而不是「這一輪連不連得上」: // 第二個參數傳「我們知不知道它的版本」而不是「這一輪連不連得上」:
// 版本是**事實**,可達性是**當下狀態**,兩者不是同一個問題。 // 版本是**事實**,可達性是**當下狀態**,兩者不是同一個問題。
accCfg.CloudVersion = cloudVer // #196:逐檔退避要知道雲端是不是已經修好子請求那面牆
cloudUpd := EvalCloudUpdate(cloudVer, strings.TrimSpace(cloudVer) != "", latestRelease, latestOK) cloudUpd := EvalCloudUpdate(cloudVer, strings.TrimSpace(cloudVer) != "", latestRelease, latestOK)
accSt := AccountSyncStatus{ accSt := AccountSyncStatus{
LastSync: time.Now().Format(time.RFC3339), LastSync: time.Now().Format(time.RFC3339),
@@ -1540,6 +1545,7 @@ func runDirectOnceRoot(cfg *DirectConfig, root string, dryRun bool, qs *quotaSta
if err != nil { if err != nil {
return append(results, DirectResult{Status: "failed", Error: err.Error()}), 1, nil, rootProgress{} return append(results, DirectResult{Status: "failed", Error: err.Error()}), 1, nil, rootProgress{}
} }
m.CloudVersion = cfg.CloudVersion // #196
// 2026-08-07 task 3(斷點續傳):每個事件處理完就立刻存檔,不要等整輪跑完。 // 2026-08-07 task 3(斷點續傳):每個事件處理完就立刻存檔,不要等整輪跑完。
// 舊行為=整個 for 迴圈跑完才 Save 一次——process 在跑到一半被殺掉(重開機、 // 舊行為=整個 for 迴圈跑完才 Save 一次——process 在跑到一半被殺掉(重開機、
// 換版、當機)時,**已經成功的那些也會遺失**,下次重開等於從頭來過, // 換版、當機)時,**已經成功的那些也會遺失**,下次重開等於從頭來過,
+36 -1
View File
@@ -56,6 +56,11 @@ type ManifestEntry struct {
// 真因(Cloudflare「當日免費額度用完」/「這份 PDF 沒有文字層」)**當場消失** // 真因(Cloudflare「當日免費額度用完」/「這份 PDF 沒有文字層」)**當場消失**
// ⇒ 使用者以為是我們壞掉。原因必須跟著 entry 存活到下次成功為止。 // ⇒ 使用者以為是我們壞掉。原因必須跟著 entry 存活到下次成功為止。
LastError string `json:"last_error,omitempty"` 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)────────────────────── // ── 雲端對帳(`inkstone/arcrun-rag#140`2026-08-26)──────────────────────
// 病:上面那個 IngestedHash 的章**永遠不會過期**。雲端 08-14 被重裝/清空之後, // 病:上面那個 IngestedHash 的章**永遠不會過期**。雲端 08-14 被重裝/清空之後,
@@ -91,6 +96,9 @@ const MaxFailBeforeSkip = 8
// Manifest 對應一個被勾選的資料夾。 // Manifest 對應一個被勾選的資料夾。
type Manifest struct { type Manifest struct {
FolderID string `json:"folder_id"` FolderID string `json:"folder_id"`
// CloudVersion=這一輪這個帳號的雲端版本(執行期由呼叫端填,不寫進檔案)。
// MarkFailed 把它記進病歷、ShouldRetry 拿它判斷「雲端是不是已經修好那面牆」(#196)。
CloudVersion string `json:"-"`
// FolderCardHashes=每個子資料夾索引卡的內容雜湊(鍵=相對監看根的目錄路徑)。 // FolderCardHashes=每個子資料夾索引卡的內容雜湊(鍵=相對監看根的目錄路徑)。
// 冪等用:內容沒變就不重送(同 InventoryHash 的角色,只是一層變多層)。 // 冪等用:內容沒變就不重送(同 InventoryHash 的角色,只是一層變多層)。
// 資料夾消失時由 syncFolderCards 清掉對應的鍵,不讓這張表無限長大。 // 資料夾消失時由 syncFolderCards 清掉對應的鍵,不讓這張表無限長大。
@@ -234,6 +242,7 @@ func (m *Manifest) MarkIngestedBy(path, sourceHash string, at int64, extractor s
// 成功即清掉失敗狀態(t195):下次再壞會從第一階退避重新算起。 // 成功即清掉失敗狀態(t195):下次再壞會從第一階退避重新算起。
e.FailCount, e.LastFailAt, e.NextRetry = 0, 0, 0 e.FailCount, e.LastFailAt, e.NextRetry = 0, 0, 0
e.LastError = "" e.LastError = ""
e.FailCloudVersion = ""
// #140:預設「這次有送卡上雲」;真的一張卡都沒送的那條路由呼叫端補打 // #140:預設「這次有送卡上雲」;真的一張卡都沒送的那條路由呼叫端補打
// MarkNoCloudCard(見 direct.go 的 cards 為空分支)。預設值放這裡而不是 // MarkNoCloudCard(見 direct.go 的 cards 為空分支)。預設值放這裡而不是
// 讓呼叫端每次都設,是因為漏設的方向要落在**安全的那一邊**: // 讓呼叫端每次都設,是因為漏設的方向要落在**安全的那一邊**:
@@ -275,6 +284,7 @@ func (m *Manifest) MarkFailed(path string, at int64, reason string) bool {
} }
e.FailCount++ e.FailCount++
e.LastFailAt = at e.LastFailAt = at
e.FailCloudVersion = m.CloudVersion // #196:記下這次撞的是哪一版雲端
if strings.TrimSpace(reason) != "" { if strings.TrimSpace(reason) != "" {
e.LastError = reason // 存真因;退避訊息由呼叫端另外組,不覆蓋這裡 e.LastError = reason // 存真因;退避訊息由呼叫端另外組,不覆蓋這裡
} }
@@ -344,11 +354,36 @@ func (m *Manifest) ShouldRetry(path string, now int64, force bool) bool {
if e.FailCount >= MaxFailBeforeSkip { if e.FailCount >= MaxFailBeforeSkip {
// #201:「雲端當時還是舊版、沒有 AI」不是這個檔的病——雲端更新之後要自己再試。 // #201:「雲端當時還是舊版、沒有 AI」不是這個檔的病——雲端更新之後要自己再試。
// 退避窗口照舊(最長 6 小時一次),雲端還沒更新的話頂多每 6 小時白問一發。 // 退避窗口照舊(最長 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 return now >= e.NextRetry
} }
// subrequestFixedRelease=雲端第一個帶「每次呼叫附帶子請求改成常數」修正的版本
// Arcrun 1eb26c9,首次出貨=1.4.64inkstone/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=病歷上寫的是「雲端還沒有這個功能」(舊版雲端),不是檔案本身的問題。 // isOldCloudText=病歷上寫的是「雲端還沒有這個功能」(舊版雲端),不是檔案本身的問題。
func isOldCloudText(msg string) bool { func isOldCloudText(msg string) bool {
return strings.Contains(msg, "沒有綁定 Workers AI") || strings.Contains(msg, "沒有雲端萃取功能") return strings.Contains(msg, "沒有綁定 Workers AI") || strings.Contains(msg, "沒有雲端萃取功能")
+1
View File
@@ -706,6 +706,7 @@ func Scan(root string, m *Manifest, opts ScanOptions) (*TriggerPayload, error) {
// ⇒ 畫面只剩 humanizeFailure 的最後退路「當時沒有記下原因」。 // ⇒ 畫面只剩 humanizeFailure 的最後退路「當時沒有記下原因」。
// 這正是上面那句警告的第二次實例——真因是我們自己刪掉的,不是沒記。 // 這正是上面那句警告的第二次實例——真因是我們自己刪掉的,不是沒記。
ne.LastError = carry.LastError ne.LastError = carry.LastError
ne.FailCloudVersion = carry.FailCloudVersion // #196:同上那句警告——漏 carry 就分不出暫停是在哪一版雲端記的
// 🔴 #1402026-08-26):雲端對帳的三個欄位同樣要 carry,而且漏了會**很貴**: // 🔴 #1402026-08-26):雲端對帳的三個欄位同樣要 carry,而且漏了會**很貴**:
// - CloudCheckedAt 歸零 ⇒ 每輪都判「太久沒對帳」⇒ 每輪重問整批(請求風暴) // - CloudCheckedAt 歸零 ⇒ 每輪都判「太久沒對帳」⇒ 每輪重問整批(請求風暴)
// - CloudMissingAt 歸零 ⇒ **防重送迴圈的 grace 消失** ⇒ 補送 → 下一輪又拔章 // - CloudMissingAt 歸零 ⇒ **防重送迴圈的 grace 消失** ⇒ 補送 → 下一輪又拔章
+245
View File
@@ -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)
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"_source": "geek6688arcrun-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"
}
}
}